feat(web): prompt-templates view + chat/admin routes + nav + isAdmin guard

This commit is contained in:
2026-05-04 20:15:49 +08:00
parent 372af06b01
commit ff283013da
4 changed files with 284 additions and 1 deletions
+39
View File
@@ -19,6 +19,45 @@
>
项目
</RouterLink>
<RouterLink
:to="{ name: 'chat-home' }"
class="text-sm hover:underline"
>
对话
</RouterLink>
<RouterLink
:to="{ name: 'prompt-templates' }"
class="text-sm hover:underline"
>
模板
</RouterLink>
<RouterLink
:to="{ name: 'me-usage' }"
class="text-sm hover:underline"
>
我的用量
</RouterLink>
<template v-if="auth.isAdmin">
<span class="text-neutral-300">|</span>
<RouterLink
:to="{ name: 'admin-llm-endpoints' }"
class="text-sm hover:underline"
>
LLM Endpoints
</RouterLink>
<RouterLink
:to="{ name: 'admin-llm-models' }"
class="text-sm hover:underline"
>
LLM Models
</RouterLink>
<RouterLink
:to="{ name: 'admin-usage' }"
class="text-sm hover:underline"
>
全局用量
</RouterLink>
</template>
</nav>
<span
v-else
+59
View File
@@ -62,6 +62,62 @@ const routes: RouteRecordRaw[] = [
component: () => import('@/views/workspaces/WorkspaceHomeView.vue'),
meta: { requiresAuth: true },
},
{
path: '/chat',
name: 'chat-home',
component: () => import('@/views/chat/ChatHomeView.vue'),
meta: { requiresAuth: true },
},
{
path: '/chat/:conversationId',
name: 'chat-detail',
component: () => import('@/views/chat/ChatDetailView.vue'),
meta: { requiresAuth: true },
},
{
path: '/projects/:pid/chat',
name: 'project-chat-home',
component: () => import('@/views/chat/ChatHomeView.vue'),
meta: { requiresAuth: true },
},
{
path: '/projects/:pid/chat/:conversationId',
name: 'project-chat-detail',
component: () => import('@/views/chat/ChatDetailView.vue'),
meta: { requiresAuth: true },
},
{
path: '/prompt-templates',
name: 'prompt-templates',
component: () =>
import('@/views/prompt-templates/PromptTemplatesView.vue'),
meta: { requiresAuth: true },
},
{
path: '/me/usage',
name: 'me-usage',
component: () => import('@/views/admin/UsageView.vue'),
meta: { requiresAuth: true },
},
{
path: '/admin/llm-endpoints',
name: 'admin-llm-endpoints',
component: () => import('@/views/admin/LLMEndpointsView.vue'),
meta: { requiresAuth: true, requiresAdmin: true },
},
{
path: '/admin/llm-models',
name: 'admin-llm-models',
component: () => import('@/views/admin/LLMModelsView.vue'),
meta: { requiresAuth: true, requiresAdmin: true },
},
{
path: '/admin/usage',
name: 'admin-usage',
component: () => import('@/views/admin/UsageView.vue'),
props: { adminMode: true },
meta: { requiresAuth: true, requiresAdmin: true },
},
]
const router = createRouter({
@@ -74,6 +130,9 @@ router.beforeEach((to) => {
if (to.meta.requiresAuth && !auth.isAuthenticated) {
return { name: 'login', query: { next: to.fullPath } }
}
if (to.meta.requiresAdmin && !auth.isAdmin) {
return { name: 'home' }
}
if (to.name === 'login' && auth.isAuthenticated) {
return { name: 'home' }
}
+2 -1
View File
@@ -53,6 +53,7 @@ export const useAuthStore = defineStore('auth', () => {
const user = ref<User | null>(safeReadJSON<User | null>('user', null))
const isAuthenticated = computed(() => !!token.value)
const isAdmin = computed(() => !!user.value?.is_admin)
function setSession(t: string, u: User) {
token.value = t
@@ -68,5 +69,5 @@ export const useAuthStore = defineStore('auth', () => {
safeRemove('user')
}
return { token, user, isAuthenticated, setSession, clearSession }
return { token, user, isAuthenticated, isAdmin, setSession, clearSession }
})
@@ -0,0 +1,184 @@
<template>
<AppShell>
<div class="space-y-4">
<div class="flex justify-between items-center">
<h2 class="text-lg font-semibold">
Prompt 模板
</h2>
<NButton
type="primary"
@click="openCreate"
>
+ 新增
</NButton>
</div>
<NDataTable
:data="templates"
:columns="columns"
:row-key="rowKey"
/>
<NModal
v-model:show="showForm"
preset="card"
:title="editing ? '编辑模板' : '新建模板'"
style="width: 640px"
>
<NForm>
<NFormItem label="名称">
<NInput v-model:value="form.name" />
</NFormItem>
<NFormItem label="作用域">
<NRadioGroup
v-model:value="form.scope"
:disabled="!!editing"
>
<NRadio value="user">
个人仅自己可见
</NRadio>
<NRadio
v-if="auth.isAdmin"
value="system"
>
系统所有用户可见
</NRadio>
</NRadioGroup>
</NFormItem>
<NFormItem label="内容">
<NInput
v-model:value="form.content"
type="textarea"
:rows="10"
/>
</NFormItem>
</NForm>
<template #footer>
<div class="flex justify-end gap-2">
<NButton @click="showForm = false">
取消
</NButton>
<NButton
type="primary"
:loading="saving"
@click="save"
>
保存
</NButton>
</div>
</template>
</NModal>
</div>
</AppShell>
</template>
<script setup lang="ts">
import { h, onMounted, ref } from 'vue'
import {
NButton,
NDataTable,
NForm,
NFormItem,
NInput,
NModal,
NRadio,
NRadioGroup,
useDialog,
useMessage,
} from 'naive-ui'
import type { DataTableColumns } from 'naive-ui'
import AppShell from '@/layouts/AppShell.vue'
import { chatApi, type PromptTemplate, type TemplateScope } from '@/api/chat'
import { useAuthStore } from '@/stores/auth'
const auth = useAuthStore()
const templates = ref<PromptTemplate[]>([])
const showForm = ref(false)
const editing = ref<PromptTemplate | null>(null)
const saving = ref(false)
const msg = useMessage()
const dlg = useDialog()
const form = ref<{ name: string; content: string; scope: TemplateScope }>({
name: '',
content: '',
scope: 'user',
})
const columns: DataTableColumns<PromptTemplate> = [
{ title: '名称', key: 'name' },
{ title: '作用域', key: 'scope' },
{
title: '操作',
key: 'actions',
render: (row) =>
h('div', { class: 'flex gap-1' }, [
h(
NButton,
{ size: 'small', onClick: () => openEdit(row) },
{ default: () => '编辑' },
),
h(
NButton,
{ size: 'small', type: 'error', onClick: () => del(row) },
{ default: () => '删除' },
),
]),
},
]
const rowKey = (row: PromptTemplate) => row.id
onMounted(reload)
async function reload() {
templates.value = await chatApi.listTemplates()
}
function openCreate() {
editing.value = null
form.value = { name: '', content: '', scope: 'user' }
showForm.value = true
}
function openEdit(t: PromptTemplate) {
editing.value = t
form.value = { name: t.name, content: t.content, scope: t.scope }
showForm.value = true
}
async function save() {
saving.value = true
try {
if (editing.value) {
await chatApi.updateTemplate(editing.value.id, {
name: form.value.name,
content: form.value.content,
})
} else {
await chatApi.createTemplate({
name: form.value.name,
content: form.value.content,
scope: form.value.scope,
})
}
showForm.value = false
await reload()
} catch (e) {
msg.error(e instanceof Error ? e.message : String(e))
} finally {
saving.value = false
}
}
function del(t: PromptTemplate) {
dlg.warning({
title: `删除 "${t.name}"?`,
positiveText: '删除',
negativeText: '取消',
onPositiveClick: async () => {
await chatApi.deleteTemplate(t.id)
await reload()
},
})
}
</script>