0.11.0: 旗舰版功能补齐(License/登录记录/会话管理/角色管理/门户定时任务/模型配额/输出脱敏/供应链扫描/记忆管理/AI助手/真实概览)
- 新增迁移 000031-000034(登录日志/角色/模型配额/记忆) - 新增包: license/memory/modelquota/assistant,扫描引擎 - 全部功能后端+前端+端到端验证通过(25 包单测)
This commit is contained in:
@@ -268,7 +268,8 @@
|
||||
"user": "User Manage",
|
||||
"role": "Role Manage",
|
||||
"userCenter": "User Center",
|
||||
"menu": "Menu Manage"
|
||||
"menu": "Menu Manage",
|
||||
"license": "License"
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
|
||||
@@ -268,7 +268,8 @@
|
||||
"user": "用户管理",
|
||||
"role": "角色管理",
|
||||
"userCenter": "个人中心",
|
||||
"menu": "菜单管理"
|
||||
"menu": "菜单管理",
|
||||
"license": "License 授权"
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
|
||||
@@ -30,6 +30,16 @@ export const systemRoutes: AppRouteRecord = {
|
||||
roles: ['R_SUPER']
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'license',
|
||||
name: 'License',
|
||||
component: '/system/license',
|
||||
meta: {
|
||||
title: 'menus.system.license',
|
||||
keepAlive: true,
|
||||
roles: ['R_SUPER']
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'user-center',
|
||||
name: 'UserCenter',
|
||||
|
||||
Executable → Regular
+86
-31
@@ -1,41 +1,96 @@
|
||||
<!-- 工作台页面 -->
|
||||
<template>
|
||||
<div>
|
||||
<CardList></CardList>
|
||||
<div class="page-content">
|
||||
<div class="mb-5 flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold">运行概览</h2>
|
||||
<p class="text-g-500 mt-1 text-sm">平台实时运行状态与近 24 小时用量</p>
|
||||
</div>
|
||||
<ElButton :loading="loading" @click="load">刷新</ElButton>
|
||||
</div>
|
||||
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :sm="24" :md="12" :lg="10">
|
||||
<ActiveUser />
|
||||
</ElCol>
|
||||
<ElCol :sm="24" :md="12" :lg="14">
|
||||
<SalesOverview />
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
<div class="mb-5 grid grid-cols-2 gap-4 lg:grid-cols-4">
|
||||
<ElCard shadow="never">
|
||||
<div class="text-g-500 text-sm">启用供应商</div>
|
||||
<b class="mt-2 block text-2xl">{{ info.enabled_providers ?? '—' }}</b>
|
||||
</ElCard>
|
||||
<ElCard shadow="never">
|
||||
<div class="text-g-500 text-sm">启用模型</div>
|
||||
<b class="mt-2 block text-2xl">{{ info.enabled_models ?? '—' }}</b>
|
||||
</ElCard>
|
||||
<ElCard shadow="never">
|
||||
<div class="text-g-500 text-sm">启用 API Key</div>
|
||||
<b class="mt-2 block text-2xl">{{ info.enabled_api_keys ?? '—' }}</b>
|
||||
</ElCard>
|
||||
<ElCard shadow="never">
|
||||
<div class="text-g-500 text-sm">运行时长</div>
|
||||
<b class="mt-2 block text-2xl">{{ uptimeText }}</b>
|
||||
</ElCard>
|
||||
</div>
|
||||
|
||||
<ElRow :gutter="20">
|
||||
<ElCol :sm="24" :md="24" :lg="12">
|
||||
<NewUser />
|
||||
</ElCol>
|
||||
<ElCol :sm="24" :md="12" :lg="6">
|
||||
<Dynamic />
|
||||
</ElCol>
|
||||
<ElCol :sm="24" :md="12" :lg="6">
|
||||
<TodoList />
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
<div class="mb-5 grid grid-cols-2 gap-4 lg:grid-cols-4">
|
||||
<ElCard shadow="never">
|
||||
<div class="text-g-500 text-sm">24h 请求</div>
|
||||
<b class="mt-2 block text-2xl">{{ overview.requests ?? 0 }}</b>
|
||||
</ElCard>
|
||||
<ElCard shadow="never">
|
||||
<div class="text-g-500 text-sm">24h 失败</div>
|
||||
<b class="mt-2 block text-2xl text-red-500">{{ overview.failed_requests ?? 0 }}</b>
|
||||
</ElCard>
|
||||
<ElCard shadow="never">
|
||||
<div class="text-g-500 text-sm">24h Tokens</div>
|
||||
<b class="mt-2 block text-2xl">{{ ((overview.prompt_tokens ?? 0) + (overview.completion_tokens ?? 0)).toLocaleString() }}</b>
|
||||
</ElCard>
|
||||
<ElCard shadow="never">
|
||||
<div class="text-g-500 text-sm">平均延迟</div>
|
||||
<b class="mt-2 block text-2xl">{{ overview.avg_latency_ms != null ? overview.avg_latency_ms.toFixed(1) + ' ms' : '—' }}</b>
|
||||
</ElCard>
|
||||
</div>
|
||||
|
||||
<AboutProject />
|
||||
<ElDescriptions :column="3" border>
|
||||
<ElDescriptionsItem label="版本">{{ info.version }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="Go">{{ info.go_version }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="License">{{ license.edition_name || '社区版 Free' }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="数据库">{{ info.database }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="对象存储">{{ info.object_storage ? '已启用' : '未启用' }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="统计窗口">{{ overview.window }}</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import CardList from './modules/card-list.vue'
|
||||
import ActiveUser from './modules/active-user.vue'
|
||||
import SalesOverview from './modules/sales-overview.vue'
|
||||
import NewUser from './modules/new-user.vue'
|
||||
import Dynamic from './modules/dynamic-stats.vue'
|
||||
import TodoList from './modules/todo-list.vue'
|
||||
import AboutProject from './modules/about-project.vue'
|
||||
import request from '@/utils/http'
|
||||
|
||||
defineOptions({ name: 'Console' })
|
||||
const loading = ref(false)
|
||||
const info = ref<Record<string, any>>({})
|
||||
const overview = ref<Record<string, any>>({})
|
||||
const license = ref<Record<string, any>>({})
|
||||
|
||||
// license API 可能因权限不存在而失败,保持默认值
|
||||
|
||||
|
||||
const uptimeText = computed(() => {
|
||||
const seconds = info.value.uptime_seconds || 0
|
||||
const days = Math.floor(seconds / 86400)
|
||||
const hours = Math.floor((seconds % 86400) / 3600)
|
||||
const minutes = Math.floor((seconds % 3600) / 60)
|
||||
return days > 0 ? `${days} 天 ${hours} 小时` : `${hours} 小时 ${minutes} 分`
|
||||
})
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
const [system, monitor, lic] = await Promise.all([
|
||||
request.get<any>({ url: '/api/v1/admin/system-info' }),
|
||||
request.get<any>({ url: '/api/v1/admin/monitoring/overview' }),
|
||||
request.get<any>({ url: '/api/v1/admin/license' }).catch(() => ({}))
|
||||
])
|
||||
info.value = system
|
||||
overview.value = monitor
|
||||
license.value = lic
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<div class="mb-5">
|
||||
<h2 class="text-xl font-semibold">AI 助手</h2>
|
||||
<p class="text-g-500 mt-1 text-sm">基于平台实时状态(供应商/模型/账号/用量/事件投递)回答管理问题</p>
|
||||
</div>
|
||||
<ElCard shadow="never" class="mb-4">
|
||||
<div class="max-h-96 space-y-3 overflow-y-auto">
|
||||
<div v-for="(item, index) in messages" :key="index" class="flex" :class="item.role === 'user' ? 'justify-end' : 'justify-start'">
|
||||
<div
|
||||
class="max-w-[80%] whitespace-pre-wrap rounded-lg px-4 py-2 text-sm"
|
||||
:class="item.role === 'user' ? 'bg-primary text-white' : 'bg-g-100 text-g-800'"
|
||||
>{{ item.content }}</div>
|
||||
</div>
|
||||
<div v-if="thinking" class="text-g-400 text-sm">助手思考中…</div>
|
||||
</div>
|
||||
</ElCard>
|
||||
<div class="flex gap-2">
|
||||
<ElInput v-model="input" placeholder="例如:平台目前有多少个供应商和 API Key?今日 Token 消耗多少?" @keyup.enter="send" />
|
||||
<ElButton type="primary" :loading="thinking" @click="send">发送</ElButton>
|
||||
</div>
|
||||
<div class="mt-3 flex flex-wrap gap-2">
|
||||
<ElTag v-for="suggestion in suggestions" :key="suggestion" class="cursor-pointer" @click="ask(suggestion)">
|
||||
{{ suggestion }}
|
||||
</ElTag>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import request from '@/utils/http'
|
||||
|
||||
interface ChatItem {
|
||||
role: 'user' | 'assistant'
|
||||
content: string
|
||||
}
|
||||
|
||||
const messages = ref<ChatItem[]>([])
|
||||
const input = ref('')
|
||||
const thinking = ref(false)
|
||||
const suggestions = ['平台概览', '今日用量与 Token 消耗', '有哪些待处理事件?', '如何新增模型供应商?']
|
||||
|
||||
function ask(text: string) {
|
||||
input.value = text
|
||||
send()
|
||||
}
|
||||
|
||||
async function send() {
|
||||
const message = input.value.trim()
|
||||
if (!message || thinking.value) return
|
||||
input.value = ''
|
||||
messages.value.push({ role: 'user', content: message })
|
||||
thinking.value = true
|
||||
try {
|
||||
const result = await request.post<{ answer: string }>({ url: '/api/v1/admin/assistant/chat', params: { message } })
|
||||
messages.value.push({ role: 'assistant', content: result.answer })
|
||||
} finally {
|
||||
thinking.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,109 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<div class="mb-5 flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold">License 授权</h2>
|
||||
<p class="text-g-500 mt-1 text-sm">平台版本、账号数上限与授权有效期管控</p>
|
||||
</div>
|
||||
<ElButton type="primary" :loading="uploading" @click="openUpload">上传 License</ElButton>
|
||||
</div>
|
||||
|
||||
<ElAlert
|
||||
v-if="!info.licensed"
|
||||
class="mb-4"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
title="当前为社区版 Free(账号上限 3 个)。上传有效 License 后可启用专业版/旗舰版能力与更多账号。"
|
||||
/>
|
||||
|
||||
<ElDescriptions v-loading="loading" :column="2" border class="mb-4">
|
||||
<ElDescriptionsItem label="当前版本">{{ info.edition_name }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="版本标识">{{ info.edition }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="授权对象">{{ info.issued_to || '—' }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="账号数上限">
|
||||
{{ info.max_accounts === 0 ? '不限' : info.max_accounts + ' 个' }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="有效期至">{{ info.expires }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="License 文件">{{ info.file || '未配置' }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="授权特性" :span="2">
|
||||
{{ info.features?.length ? info.features.join('、') : '—' }}
|
||||
</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
|
||||
<ElDialog v-model="dialogVisible" title="上传 License" width="560px">
|
||||
<ElInput
|
||||
v-model="content"
|
||||
type="textarea"
|
||||
:rows="10"
|
||||
placeholder="粘贴 License 文件完整内容(JSON,含 signature 字段)"
|
||||
/>
|
||||
<template #footer>
|
||||
<ElButton @click="dialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" :loading="uploading" @click="submit">保存并生效</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import request from '@/utils/http'
|
||||
|
||||
interface LicenseInfo {
|
||||
edition: string
|
||||
edition_name: string
|
||||
issued_to?: string
|
||||
max_accounts: number
|
||||
features: string[]
|
||||
expires: string
|
||||
file: string
|
||||
licensed: boolean
|
||||
}
|
||||
|
||||
const loading = ref(false)
|
||||
const uploading = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
const content = ref('')
|
||||
const info = ref<LicenseInfo>({
|
||||
edition: 'free',
|
||||
edition_name: '社区版 Free',
|
||||
max_accounts: 3,
|
||||
features: [],
|
||||
expires: '永久',
|
||||
file: '',
|
||||
licensed: false
|
||||
})
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
info.value = await request.get<LicenseInfo>({ url: '/api/v1/admin/license' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openUpload() {
|
||||
content.value = ''
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
if (!content.value.trim()) {
|
||||
ElMessage.warning('请粘贴 License 内容')
|
||||
return
|
||||
}
|
||||
uploading.value = true
|
||||
try {
|
||||
const result = await request.post<LicenseInfo>({ url: '/api/v1/admin/license', params: { content: content.value } })
|
||||
info.value = result
|
||||
dialogVisible.value = false
|
||||
ElMessage.success('License 已生效')
|
||||
await load()
|
||||
} finally {
|
||||
uploading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
@@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<div class="mb-5 flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold">登录记录</h2>
|
||||
<p class="text-g-500 mt-1 text-sm">全量登录尝试审计(管理端/门户),可筛选账号</p>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<ElInput v-model="loginFilter" placeholder="按登录名筛选" clearable class="w-52" @keyup.enter="load" />
|
||||
<ElButton @click="load">查询</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
<ElTable v-loading="loading" :data="logs" row-key="id">
|
||||
<ElTableColumn prop="created_at" label="时间" width="190" />
|
||||
<ElTableColumn prop="kind" label="端" width="80" />
|
||||
<ElTableColumn prop="login" label="登录名" width="180" />
|
||||
<ElTableColumn label="结果" width="90">
|
||||
<template #default="{ row }">
|
||||
<ElTag :type="row.success ? 'success' : 'danger'">{{ row.success ? '成功' : '失败' }}</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="ip" label="IP" width="150">
|
||||
<template #default="{ row }">{{ row.ip || '—' }}</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="reason" label="原因" width="170" />
|
||||
<ElTableColumn prop="user_agent" label="客户端" min-width="220" show-overflow-tooltip />
|
||||
</ElTable>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import request from '@/utils/http'
|
||||
|
||||
interface LoginLog {
|
||||
id: string
|
||||
kind: string
|
||||
login: string
|
||||
success: boolean
|
||||
ip?: string
|
||||
reason: string
|
||||
user_agent: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
const loading = ref(false)
|
||||
const loginFilter = ref('')
|
||||
const logs = ref<LoginLog[]>([])
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
logs.value = await request.get<LoginLog[]>({
|
||||
url: '/api/v1/admin/login-logs',
|
||||
params: loginFilter.value ? { login: loginFilter.value, limit: 100 } : { limit: 100 }
|
||||
})
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
Executable → Regular
+132
-214
@@ -1,241 +1,159 @@
|
||||
<!-- 角色管理页面 -->
|
||||
<!-- 角色管理:内置角色 + 自定义角色 CRUD,权限字符串编辑 -->
|
||||
<template>
|
||||
<div class="art-full-height">
|
||||
<RoleSearch
|
||||
v-show="showSearchBar"
|
||||
v-model="searchForm"
|
||||
@search="handleSearch"
|
||||
@reset="resetSearchParams"
|
||||
></RoleSearch>
|
||||
<div class="page-content">
|
||||
<div class="mb-5 flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold">角色管理</h2>
|
||||
<p class="text-g-500 mt-1 text-sm">内置角色不可修改;自定义角色权限在分配账号时展开生效</p>
|
||||
</div>
|
||||
<ElButton type="primary" @click="openCreate">新增角色</ElButton>
|
||||
</div>
|
||||
|
||||
<ElCard class="art-table-card" :style="{ 'margin-top': showSearchBar ? '12px' : '0' }">
|
||||
<ArtTableHeader
|
||||
v-model:columns="columnChecks"
|
||||
v-model:showSearchBar="showSearchBar"
|
||||
:loading="loading"
|
||||
@refresh="refreshData"
|
||||
>
|
||||
<template #left>
|
||||
<ElSpace wrap>
|
||||
<ElButton @click="showDialog('add')" v-ripple>新增角色</ElButton>
|
||||
</ElSpace>
|
||||
<ElTable v-loading="loading" :data="roles" row-key="id">
|
||||
<ElTableColumn prop="code" label="代码" width="160" />
|
||||
<ElTableColumn prop="name" label="名称" min-width="140" />
|
||||
<ElTableColumn prop="description" label="描述" min-width="180" show-overflow-tooltip />
|
||||
<ElTableColumn label="类型" width="100">
|
||||
<template #default="{ row }">
|
||||
<ElTag :type="row.builtin ? 'info' : 'success'">{{ row.builtin ? '内置' : '自定义' }}</ElTag>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="权限数" width="90">
|
||||
<template #default="{ row }">{{ (row.permissions || []).length }}</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn label="操作" width="200" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<ElButton link type="primary" :disabled="row.builtin" @click="openEdit(row)">编辑</ElButton>
|
||||
<ElButton link type="danger" :disabled="row.builtin" @click="remove(row)">删除</ElButton>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
:loading="loading"
|
||||
:data="data"
|
||||
:columns="columns"
|
||||
:pagination="pagination"
|
||||
@pagination:size-change="handleSizeChange"
|
||||
@pagination:current-change="handleCurrentChange"
|
||||
>
|
||||
</ArtTable>
|
||||
</ElCard>
|
||||
|
||||
<!-- 角色编辑弹窗 -->
|
||||
<RoleEditDialog
|
||||
v-model="dialogVisible"
|
||||
:dialog-type="dialogType"
|
||||
:role-data="currentRoleData"
|
||||
@success="refreshData"
|
||||
/>
|
||||
|
||||
<!-- 菜单权限弹窗 -->
|
||||
<RolePermissionDialog
|
||||
v-model="permissionDialog"
|
||||
:role-data="currentRoleData"
|
||||
@success="refreshData"
|
||||
/>
|
||||
<ElDialog v-model="dialogVisible" :title="editingId ? '编辑角色' : '新增角色'" width="640px">
|
||||
<ElForm ref="formRef" :model="form" :rules="rules" label-width="90px">
|
||||
<ElFormItem label="代码" prop="code">
|
||||
<ElInput v-model="form.code" :disabled="!!editingId" placeholder="小写字母开头,如 ops-lead" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="名称" prop="name"><ElInput v-model="form.name" maxlength="64" /></ElFormItem>
|
||||
<ElFormItem label="描述"><ElInput v-model="form.description" maxlength="512" /></ElFormItem>
|
||||
<ElFormItem label="权限">
|
||||
<ElSelect v-model="form.permissions" multiple filterable collapse-tags class="w-full" placeholder="选择权限字符串">
|
||||
<ElOption v-for="permission in permissionOptions" :key="permission.value" :label="permission.label" :value="permission.value" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<ElButton @click="dialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" :loading="saving" @click="submit">保存</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ButtonMoreItem } from '@/components/core/forms/art-button-more/index.vue'
|
||||
import { useTable } from '@/hooks/core/useTable'
|
||||
import { fetchGetRoleList } from '@/api/system-manage'
|
||||
import ArtButtonMore from '@/components/core/forms/art-button-more/index.vue'
|
||||
import RoleSearch from './modules/role-search.vue'
|
||||
import RoleEditDialog from './modules/role-edit-dialog.vue'
|
||||
import RolePermissionDialog from './modules/role-permission-dialog.vue'
|
||||
import { ElTag, ElMessageBox } from 'element-plus'
|
||||
import { ElMessage, ElMessageBox, FormInstance, FormRules } from 'element-plus'
|
||||
import request from '@/utils/http'
|
||||
|
||||
defineOptions({ name: 'Role' })
|
||||
|
||||
type RoleListItem = Api.SystemManage.RoleListItem
|
||||
type RoleSearchFormParams = Api.SystemManage.RoleSearchParams & {
|
||||
daterange?: string[]
|
||||
interface Role {
|
||||
id: string
|
||||
code: string
|
||||
name: string
|
||||
description: string
|
||||
permissions: string[]
|
||||
builtin: boolean
|
||||
}
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = ref<RoleSearchFormParams>({
|
||||
roleName: undefined,
|
||||
roleCode: undefined,
|
||||
description: undefined,
|
||||
enabled: undefined,
|
||||
daterange: undefined
|
||||
})
|
||||
|
||||
const showSearchBar = ref(false)
|
||||
const permissionOptions = [
|
||||
{ label: '身份管理 identity:manage', value: 'identity:manage' },
|
||||
{ label: '供应商管理 provider:manage', value: 'provider:manage' },
|
||||
{ label: '供应商查看 provider:read', value: 'provider:read' },
|
||||
{ label: 'API Key 管理 api_key:manage', value: 'api_key:manage' },
|
||||
{ label: 'API Key 查看 api_key:read', value: 'api_key:read' },
|
||||
{ label: '审计查看 audit:read', value: 'audit:read' },
|
||||
{ label: '用量查看 usage:read', value: 'usage:read' },
|
||||
{ label: 'Outbox 管理 outbox:manage', value: 'outbox:manage' },
|
||||
{ label: '内容策略管理 content_policy:manage', value: 'content_policy:manage' },
|
||||
{ label: '定价管理 pricing:manage', value: 'pricing:manage' },
|
||||
{ label: 'Prompt 管理 prompt:manage', value: 'prompt:manage' },
|
||||
{ label: '知识库管理 knowledge:manage', value: 'knowledge:manage' },
|
||||
{ label: '工具管理 tool:manage', value: 'tool:manage' },
|
||||
{ label: '应用管理 application:manage', value: 'application:manage' },
|
||||
{ label: '通知管理 notification:manage', value: 'notification:manage' },
|
||||
{ label: 'MCP 管理 mcp_server:manage', value: 'mcp_server:manage' },
|
||||
{ label: 'Skill 管理 skill:manage', value: 'skill:manage' },
|
||||
{ label: '数字员工管理 digital_employee:manage', value: 'digital_employee:manage' },
|
||||
{ label: '市场管理 marketplace:manage', value: 'marketplace:manage' },
|
||||
{ label: '文件管理 file:manage', value: 'file:manage' },
|
||||
{ label: '消息管理 inbox:manage', value: 'inbox:manage' },
|
||||
{ label: '定时任务管理 scheduled_task:manage', value: 'scheduled_task:manage' },
|
||||
{ label: 'Trace 查看 trace:read', value: 'trace:read' },
|
||||
{ label: '智能体节点管理 agent_node:manage', value: 'agent_node:manage' },
|
||||
{ label: '系统管理 system:manage', value: 'system:manage' }
|
||||
]
|
||||
|
||||
const roles = ref<Role[]>([])
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
const permissionDialog = ref(false)
|
||||
const currentRoleData = ref<RoleListItem | undefined>(undefined)
|
||||
const editingId = ref('')
|
||||
const formRef = ref<FormInstance>()
|
||||
const form = reactive({ code: '', name: '', description: '', permissions: [] as string[] })
|
||||
const rules: FormRules = {
|
||||
code: [{ required: true, message: '请输入角色代码', trigger: 'blur' }],
|
||||
name: [{ required: true, message: '请输入角色名称', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
const {
|
||||
columns,
|
||||
columnChecks,
|
||||
data,
|
||||
loading,
|
||||
pagination,
|
||||
getData,
|
||||
searchParams,
|
||||
resetSearchParams,
|
||||
handleSizeChange,
|
||||
handleCurrentChange,
|
||||
refreshData
|
||||
} = useTable({
|
||||
// 核心配置
|
||||
core: {
|
||||
apiFn: fetchGetRoleList,
|
||||
apiParams: {
|
||||
current: 1,
|
||||
size: 20
|
||||
},
|
||||
// 排除 apiParams 中的属性
|
||||
excludeParams: ['daterange'],
|
||||
columnsFactory: () => [
|
||||
{
|
||||
prop: 'roleId',
|
||||
label: '角色ID',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
prop: 'roleName',
|
||||
label: '角色名称',
|
||||
minWidth: 120
|
||||
},
|
||||
{
|
||||
prop: 'roleCode',
|
||||
label: '角色编码',
|
||||
minWidth: 120
|
||||
},
|
||||
{
|
||||
prop: 'description',
|
||||
label: '角色描述',
|
||||
minWidth: 150,
|
||||
showOverflowTooltip: true
|
||||
},
|
||||
{
|
||||
prop: 'enabled',
|
||||
label: '角色状态',
|
||||
width: 100,
|
||||
formatter: (row) => {
|
||||
const statusConfig = row.enabled
|
||||
? { type: 'success', text: '启用' }
|
||||
: { type: 'warning', text: '禁用' }
|
||||
return h(
|
||||
ElTag,
|
||||
{ type: statusConfig.type as 'success' | 'warning' },
|
||||
() => statusConfig.text
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'createTime',
|
||||
label: '创建日期',
|
||||
width: 180,
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
prop: 'operation',
|
||||
label: '操作',
|
||||
width: 80,
|
||||
fixed: 'right',
|
||||
formatter: (row) =>
|
||||
h('div', [
|
||||
h(ArtButtonMore, {
|
||||
list: [
|
||||
{
|
||||
key: 'permission',
|
||||
label: '菜单权限',
|
||||
icon: 'ri:user-3-line'
|
||||
},
|
||||
{
|
||||
key: 'edit',
|
||||
label: '编辑角色',
|
||||
icon: 'ri:edit-2-line'
|
||||
},
|
||||
{
|
||||
key: 'delete',
|
||||
label: '删除角色',
|
||||
icon: 'ri:delete-bin-4-line',
|
||||
color: '#f56c6c'
|
||||
}
|
||||
],
|
||||
onClick: (item: ButtonMoreItem) => buttonMoreClick(item, row)
|
||||
})
|
||||
])
|
||||
}
|
||||
]
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
roles.value = await request.get<Role[]>({ url: '/api/v1/admin/roles' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const dialogType = ref<'add' | 'edit'>('add')
|
||||
|
||||
const showDialog = (type: 'add' | 'edit', row?: RoleListItem) => {
|
||||
function openCreate() {
|
||||
editingId.value = ''
|
||||
Object.assign(form, { code: '', name: '', description: '', permissions: [] })
|
||||
dialogVisible.value = true
|
||||
dialogType.value = type
|
||||
currentRoleData.value = row
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索处理
|
||||
* @param params 搜索参数
|
||||
*/
|
||||
const handleSearch = (params: RoleSearchFormParams) => {
|
||||
// 处理日期区间参数,把 daterange 转换为 startTime 和 endTime
|
||||
const { daterange, ...filtersParams } = params
|
||||
const [startTime, endTime] = Array.isArray(daterange) ? daterange : [null, null]
|
||||
|
||||
// 搜索参数赋值
|
||||
Object.assign(searchParams, { ...filtersParams, startTime, endTime })
|
||||
getData()
|
||||
function openEdit(row: Role) {
|
||||
editingId.value = row.id
|
||||
Object.assign(form, { code: row.code, name: row.name, description: row.description, permissions: [...row.permissions] })
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const buttonMoreClick = (item: ButtonMoreItem, row: RoleListItem) => {
|
||||
switch (item.key) {
|
||||
case 'permission':
|
||||
showPermissionDialog(row)
|
||||
break
|
||||
case 'edit':
|
||||
showDialog('edit', row)
|
||||
break
|
||||
case 'delete':
|
||||
deleteRole(row)
|
||||
break
|
||||
async function submit() {
|
||||
if (!(await formRef.value?.validate())) return
|
||||
saving.value = true
|
||||
try {
|
||||
if (editingId.value) {
|
||||
await request.put({ url: `/api/v1/admin/roles/${editingId.value}`, params: { ...form } })
|
||||
ElMessage.success('角色已更新')
|
||||
} else {
|
||||
await request.post({ url: '/api/v1/admin/roles', params: { ...form } })
|
||||
ElMessage.success('角色已创建')
|
||||
}
|
||||
dialogVisible.value = false
|
||||
await load()
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const showPermissionDialog = (row?: RoleListItem) => {
|
||||
permissionDialog.value = true
|
||||
currentRoleData.value = row
|
||||
async function remove(row: Role) {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确认删除角色「${row.name}」?已分配该角色的账号权限不会自动回收。`, '删除角色', { type: 'warning' })
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
try {
|
||||
await request.del({ url: `/api/v1/admin/roles/${row.id}` })
|
||||
ElMessage.success('已删除')
|
||||
await load()
|
||||
} catch { /* 全局错误提示 */ }
|
||||
}
|
||||
|
||||
const deleteRole = (row: RoleListItem) => {
|
||||
ElMessageBox.confirm(`确定删除角色"${row.roleName}"吗?此操作不可恢复!`, '删除确认', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
.then(() => {
|
||||
// TODO: 调用删除接口
|
||||
ElMessage.success('删除成功')
|
||||
refreshData()
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessage.info('已取消删除')
|
||||
})
|
||||
}
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<div class="mb-5">
|
||||
<h2 class="text-xl font-semibold">登录记录</h2>
|
||||
<p class="text-g-500 mt-1 text-sm">最近 50 条本账号的登录尝试(含失败原因),如发现异常请立即修改密码</p>
|
||||
</div>
|
||||
<ElTable v-loading="loading" :data="logs" row-key="id">
|
||||
<ElTableColumn prop="created_at" label="时间" width="190" />
|
||||
<ElTableColumn label="结果" width="90">
|
||||
<template #default="{ row }">
|
||||
<ElTag :type="row.success ? 'success' : 'danger'">{{ row.success ? '成功' : '失败' }}</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="ip" label="IP" width="150">
|
||||
<template #default="{ row }">{{ row.ip || '—' }}</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="user_agent" label="客户端" min-width="220" show-overflow-tooltip />
|
||||
<ElTableColumn prop="reason" label="原因" width="160" />
|
||||
</ElTable>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import request from '@/utils/http'
|
||||
|
||||
interface LoginLog {
|
||||
id: string
|
||||
success: boolean
|
||||
ip?: string
|
||||
user_agent: string
|
||||
reason: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
const loading = ref(false)
|
||||
const logs = ref<LoginLog[]>([])
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
logs.value = await request.get<LoginLog[]>({ url: '/api/v1/portal/login-logs' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
@@ -0,0 +1,154 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<div class="mb-5 flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold">记忆管理</h2>
|
||||
<p class="text-g-500 mt-1 text-sm">沉淀个人经验与偏好,对话时按语义自动召回;可授权给其他用户</p>
|
||||
</div>
|
||||
<ElButton type="primary" @click="openCreate">添加记忆</ElButton>
|
||||
</div>
|
||||
|
||||
<ElCard shadow="never" class="mb-4">
|
||||
<div class="flex gap-2">
|
||||
<ElInput v-model="recallQuery" placeholder="语义检索我的记忆(如:我常用的代码风格)" @keyup.enter="recall" />
|
||||
<ElButton type="primary" plain :loading="recalling" @click="recall">召回</ElButton>
|
||||
</div>
|
||||
<div v-if="recalled.length" class="mt-3 space-y-2">
|
||||
<div v-for="item in recalled" :key="item.id" class="rounded bg-g-50 p-3 text-sm">
|
||||
<div class="font-medium">{{ item.category }} · 重要度 {{ item.importance }}</div>
|
||||
<div class="text-g-600 mt-1">{{ item.content }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</ElCard>
|
||||
|
||||
<ElTable v-loading="loading" :data="entries" row-key="id">
|
||||
<ElTableColumn prop="category" label="分类" width="120" />
|
||||
<ElTableColumn prop="content" label="内容" min-width="300" show-overflow-tooltip />
|
||||
<ElTableColumn prop="importance" label="重要度" width="90" />
|
||||
<ElTableColumn label="授权给" width="160">
|
||||
<template #default="{ row }">{{ (row.shared_with || []).length ? row.shared_with.length + ' 人' : '—' }}</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="updated_at" label="更新时间" width="180" />
|
||||
<ElTableColumn label="操作" width="140" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<ElButton link type="primary" @click="openEdit(row)">编辑</ElButton>
|
||||
<ElButton link type="danger" @click="remove(row)">删除</ElButton>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
|
||||
<ElDialog v-model="dialogVisible" :title="editingId ? '编辑记忆' : '添加记忆'" width="600px">
|
||||
<ElForm label-width="90px">
|
||||
<ElFormItem label="分类"><ElInput v-model="form.category" placeholder="如 工作偏好 / 代码风格 / 常用工具" /></ElFormItem>
|
||||
<ElFormItem label="内容" required>
|
||||
<ElInput v-model="form.content" type="textarea" :rows="4" maxlength="8000" show-word-limit placeholder="记忆内容,对话时按语义召回" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="重要度">
|
||||
<ElRate v-model="form.importance" :max="10" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="授权用户ID">
|
||||
<ElInput v-model="sharedText" placeholder="逗号分隔的用户 ID(授权后对方可召回你的这条记忆)" />
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<ElButton @click="dialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" :loading="saving" @click="submit">保存</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import request from '@/utils/http'
|
||||
|
||||
interface MemoryEntry {
|
||||
id: string
|
||||
category: string
|
||||
content: string
|
||||
importance: number
|
||||
shared_with: string[]
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
const entries = ref<MemoryEntry[]>([])
|
||||
const recalled = ref<MemoryEntry[]>([])
|
||||
const loading = ref(false)
|
||||
const recalling = ref(false)
|
||||
const saving = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
const editingId = ref('')
|
||||
const recallQuery = ref('')
|
||||
const sharedText = ref('')
|
||||
const form = reactive({ category: 'general', content: '', importance: 5 })
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
entries.value = await request.get<MemoryEntry[]>({ url: '/api/v1/portal/memories' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function recall() {
|
||||
if (!recallQuery.value.trim()) return
|
||||
recalling.value = true
|
||||
try {
|
||||
recalled.value = await request.post<MemoryEntry[]>({ url: '/api/v1/portal/memories/recall', params: { query: recallQuery.value.trim(), limit: 5 } })
|
||||
} finally {
|
||||
recalling.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
editingId.value = ''
|
||||
Object.assign(form, { category: 'general', content: '', importance: 5 })
|
||||
sharedText.value = ''
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
function openEdit(row: MemoryEntry) {
|
||||
editingId.value = row.id
|
||||
Object.assign(form, { category: row.category, content: row.content, importance: row.importance })
|
||||
sharedText.value = (row.shared_with || []).join(',')
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
if (!form.content.trim()) {
|
||||
ElMessage.warning('请输入记忆内容')
|
||||
return
|
||||
}
|
||||
saving.value = true
|
||||
try {
|
||||
const shared = sharedText.value.split(',').map((v) => v.trim()).filter(Boolean)
|
||||
const payload = { ...form, shared_with: shared }
|
||||
if (editingId.value) {
|
||||
await request.put({ url: `/api/v1/portal/memories/${editingId.value}`, params: payload })
|
||||
} else {
|
||||
await request.post({ url: '/api/v1/portal/memories', params: payload })
|
||||
}
|
||||
ElMessage.success('已保存')
|
||||
dialogVisible.value = false
|
||||
await load()
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function remove(row: MemoryEntry) {
|
||||
try {
|
||||
await ElMessageBox.confirm('确认删除这条记忆?', '删除记忆', { type: 'warning' })
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
try {
|
||||
await request.del({ url: `/api/v1/portal/memories/${row.id}` })
|
||||
ElMessage.success('已删除')
|
||||
await load()
|
||||
} catch { /* 全局错误提示 */ }
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
@@ -0,0 +1,228 @@
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<div class="mb-5 flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold">定时任务</h2>
|
||||
<p class="text-g-500 mt-1 text-sm">创建自动化任务:按 Cron 计划执行应用/数字员工,支持查看执行历史</p>
|
||||
</div>
|
||||
<ElButton type="primary" @click="openCreate">新建任务</ElButton>
|
||||
</div>
|
||||
|
||||
<ElTable v-loading="loading" :data="tasks" row-key="id">
|
||||
<ElTableColumn prop="name" label="名称" min-width="160" />
|
||||
<ElTableColumn prop="target_type" label="目标类型" width="110">
|
||||
<template #default="{ row }">{{ row.target_type === 'application' ? '应用' : '数字员工' }}</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="target_code" label="目标" min-width="150" />
|
||||
<ElTableColumn prop="cron_expression" label="Cron" width="110" />
|
||||
<ElTableColumn prop="timezone" label="时区" width="130" />
|
||||
<ElTableColumn label="状态" width="90">
|
||||
<template #default="{ row }">
|
||||
<ElTag :type="row.enabled ? 'success' : 'info'">{{ row.enabled ? '启用' : '暂停' }}</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="last_status" label="上次结果" width="100">
|
||||
<template #default="{ row }">
|
||||
<ElTag :type="row.last_status === 'success' ? 'success' : row.last_status ? 'danger' : 'info'">
|
||||
{{ row.last_status || '—' }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="next_run_at" label="下次执行" width="180" />
|
||||
<ElTableColumn label="操作" width="260" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<ElButton link type="success" :disabled="row.enabled" @click="setEnabled(row, true)">启动</ElButton>
|
||||
<ElButton link type="warning" :disabled="!row.enabled" @click="setEnabled(row, false)">暂停</ElButton>
|
||||
<ElButton link type="primary" @click="runNow(row)">立即执行</ElButton>
|
||||
<ElButton link type="primary" @click="openRuns(row)">历史</ElButton>
|
||||
<ElButton link type="primary" @click="openEdit(row)">编辑</ElButton>
|
||||
<ElButton link type="danger" @click="remove(row)">删除</ElButton>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
|
||||
<ElDialog v-model="dialogVisible" :title="editingId ? '编辑任务' : '新建任务'" width="640px">
|
||||
<ElForm ref="formRef" :model="form" :rules="rules" label-width="110px">
|
||||
<ElFormItem label="名称" prop="name"><ElInput v-model="form.name" maxlength="128" /></ElFormItem>
|
||||
<ElFormItem label="目标类型" prop="target_type">
|
||||
<ElSelect v-model="form.target_type" class="w-full">
|
||||
<ElOption label="应用" value="application" />
|
||||
<ElOption label="数字员工" value="digital_employee" />
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="目标代码" prop="target_code"><ElInput v-model="form.target_code" placeholder="如 my-app / my-employee" /></ElFormItem>
|
||||
<ElFormItem label="Cron 表达式" prop="cron_expression"><ElInput v-model="form.cron_expression" placeholder="五字段,如 0 9 * * 1" /></ElFormItem>
|
||||
<ElFormItem label="时区">
|
||||
<ElInput v-model="form.timezone" placeholder="Asia/Shanghai" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="提示词">
|
||||
<ElInput v-model="form.prompt" type="textarea" :rows="3" placeholder="可选,追加给目标的任务提示" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="执行 API Key">
|
||||
<ElInput v-model="form.api_key" type="password" show-password :placeholder="editingId ? '留空表示不更换' : '必填'" />
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<ElButton @click="dialogVisible = false">取消</ElButton>
|
||||
<ElButton type="primary" :loading="saving" @click="submit">保存</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
|
||||
<ElDialog v-model="runsVisible" title="执行历史" width="820px">
|
||||
<ElTable v-loading="runsLoading" :data="runs" size="small">
|
||||
<ElTableColumn prop="scheduled_for" label="计划时间" width="170" />
|
||||
<ElTableColumn prop="started_at" label="开始时间" width="170" />
|
||||
<ElTableColumn label="状态" width="90">
|
||||
<template #default="{ row }">
|
||||
<ElTag :type="row.status === 'success' ? 'success' : row.status === 'failed' ? 'danger' : 'warning'">
|
||||
{{ row.status }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn prop="attempts" label="尝试" width="70" />
|
||||
<ElTableColumn prop="error" label="错误" min-width="200" show-overflow-tooltip />
|
||||
</ElTable>
|
||||
</ElDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ElMessage, ElMessageBox, FormInstance, FormRules } from 'element-plus'
|
||||
import request from '@/utils/http'
|
||||
|
||||
interface Task {
|
||||
id: string
|
||||
name: string
|
||||
target_type: string
|
||||
target_code: string
|
||||
cron_expression: string
|
||||
timezone: string
|
||||
prompt?: string
|
||||
enabled: boolean
|
||||
last_status?: string
|
||||
next_run_at?: string
|
||||
}
|
||||
|
||||
interface TaskInput {
|
||||
name: string
|
||||
target_type: string
|
||||
target_code: string
|
||||
cron_expression: string
|
||||
timezone: string
|
||||
prompt?: string
|
||||
api_key?: string
|
||||
}
|
||||
|
||||
const tasks = ref<Task[]>([])
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
const editingId = ref('')
|
||||
const runsVisible = ref(false)
|
||||
const runsLoading = ref(false)
|
||||
const runs = ref<any[]>([])
|
||||
const formRef = ref<FormInstance>()
|
||||
const form = reactive<TaskInput>({
|
||||
name: '',
|
||||
target_type: 'application',
|
||||
target_code: '',
|
||||
cron_expression: '0 9 * * 1',
|
||||
timezone: 'Asia/Shanghai',
|
||||
prompt: '',
|
||||
api_key: ''
|
||||
})
|
||||
const rules: FormRules = {
|
||||
name: [{ required: true, message: '请输入任务名称', trigger: 'blur' }],
|
||||
target_type: [{ required: true, message: '请选择目标类型', trigger: 'change' }],
|
||||
target_code: [{ required: true, message: '请输入目标代码', trigger: 'blur' }],
|
||||
cron_expression: [{ required: true, message: '请输入 Cron 表达式', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
tasks.value = await request.get<Task[]>({ url: '/api/v1/portal/scheduled-tasks' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
editingId.value = ''
|
||||
Object.assign(form, { name: '', target_type: 'application', target_code: '', cron_expression: '0 9 * * 1', timezone: 'Asia/Shanghai', prompt: '', api_key: '' })
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
function openEdit(row: Task) {
|
||||
editingId.value = row.id
|
||||
Object.assign(form, {
|
||||
name: row.name,
|
||||
target_type: row.target_type,
|
||||
target_code: row.target_code,
|
||||
cron_expression: row.cron_expression,
|
||||
timezone: row.timezone,
|
||||
prompt: row.prompt || '',
|
||||
api_key: ''
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
if (!(await formRef.value?.validate())) return
|
||||
saving.value = true
|
||||
try {
|
||||
const payload: TaskInput = { ...form }
|
||||
if (!payload.api_key) delete payload.api_key
|
||||
if (editingId.value) {
|
||||
await request.put({ url: `/api/v1/portal/scheduled-tasks/${editingId.value}`, params: payload })
|
||||
ElMessage.success('任务已更新')
|
||||
} else {
|
||||
await request.post({ url: '/api/v1/portal/scheduled-tasks', params: payload })
|
||||
ElMessage.success('任务已创建')
|
||||
}
|
||||
dialogVisible.value = false
|
||||
await load()
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function setEnabled(row: Task, enabled: boolean) {
|
||||
try {
|
||||
await request.post({ url: `/api/v1/portal/scheduled-tasks/${row.id}/${enabled ? 'start' : 'pause'}` })
|
||||
await load()
|
||||
} catch { /* 全局错误提示 */ }
|
||||
}
|
||||
|
||||
async function runNow(row: Task) {
|
||||
try {
|
||||
await request.post({ url: `/api/v1/portal/scheduled-tasks/${row.id}/run` })
|
||||
ElMessage.success('已加入执行队列')
|
||||
} catch { /* 全局错误提示 */ }
|
||||
}
|
||||
|
||||
async function remove(row: Task) {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确认删除任务「${row.name}」?`, '删除任务', { type: 'warning' })
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
try {
|
||||
await request.del({ url: `/api/v1/portal/scheduled-tasks/${row.id}` })
|
||||
ElMessage.success('已删除')
|
||||
await load()
|
||||
} catch { /* 全局错误提示 */ }
|
||||
}
|
||||
|
||||
async function openRuns(row: Task) {
|
||||
runsVisible.value = true
|
||||
runsLoading.value = true
|
||||
try {
|
||||
runs.value = await request.get<any[]>({ url: `/api/v1/portal/scheduled-tasks/${row.id}/runs` })
|
||||
} finally {
|
||||
runsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
Reference in New Issue
Block a user