You've already forked agentic-coding-workflow
feat(web): prompt-templates view + chat/admin routes + nav + isAdmin guard
This commit is contained in:
@@ -19,6 +19,45 @@
|
|||||||
>
|
>
|
||||||
项目
|
项目
|
||||||
</RouterLink>
|
</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>
|
</nav>
|
||||||
<span
|
<span
|
||||||
v-else
|
v-else
|
||||||
|
|||||||
@@ -62,6 +62,62 @@ const routes: RouteRecordRaw[] = [
|
|||||||
component: () => import('@/views/workspaces/WorkspaceHomeView.vue'),
|
component: () => import('@/views/workspaces/WorkspaceHomeView.vue'),
|
||||||
meta: { requiresAuth: true },
|
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({
|
const router = createRouter({
|
||||||
@@ -74,6 +130,9 @@ router.beforeEach((to) => {
|
|||||||
if (to.meta.requiresAuth && !auth.isAuthenticated) {
|
if (to.meta.requiresAuth && !auth.isAuthenticated) {
|
||||||
return { name: 'login', query: { next: to.fullPath } }
|
return { name: 'login', query: { next: to.fullPath } }
|
||||||
}
|
}
|
||||||
|
if (to.meta.requiresAdmin && !auth.isAdmin) {
|
||||||
|
return { name: 'home' }
|
||||||
|
}
|
||||||
if (to.name === 'login' && auth.isAuthenticated) {
|
if (to.name === 'login' && auth.isAuthenticated) {
|
||||||
return { name: 'home' }
|
return { name: 'home' }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ export const useAuthStore = defineStore('auth', () => {
|
|||||||
const user = ref<User | null>(safeReadJSON<User | null>('user', null))
|
const user = ref<User | null>(safeReadJSON<User | null>('user', null))
|
||||||
|
|
||||||
const isAuthenticated = computed(() => !!token.value)
|
const isAuthenticated = computed(() => !!token.value)
|
||||||
|
const isAdmin = computed(() => !!user.value?.is_admin)
|
||||||
|
|
||||||
function setSession(t: string, u: User) {
|
function setSession(t: string, u: User) {
|
||||||
token.value = t
|
token.value = t
|
||||||
@@ -68,5 +69,5 @@ export const useAuthStore = defineStore('auth', () => {
|
|||||||
safeRemove('user')
|
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>
|
||||||
Reference in New Issue
Block a user