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
|
||||
: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
|
||||
|
||||
@@ -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' }
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user