feat: 重构 AuditView 支持 Skills/MCP/定时任务三种类型管理

This commit is contained in:
caoxiaozhu
2026-05-09 15:46:16 +00:00
parent da6f0e2589
commit 683c75f364
8 changed files with 1024 additions and 19422 deletions

View File

@@ -2,46 +2,18 @@
"version": 1,
"documents": [
{
"id": "8af9350f0e02488aaf0df2001286b764",
"folder": "财务知识库",
"original_name": "差旅费季度报销258878.xlsx",
"stored_name": "8af9350f0e02488aaf0df2001286b764__差旅费季度报销258878.xlsx",
"mime_type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"extension": "xlsx",
"size_bytes": 11123,
"sha256": "ea02e59d3a22a4a02284172acce3fd4c6367a26f1a4fd196dc4f65afed1bd4c5",
"created_at": "2026-05-09T05:46:24.699125+00:00",
"updated_at": "2026-05-09T05:46:24.699125+00:00",
"uploaded_by": "admin",
"version_number": 1
},
{
"id": "6cad2936d57242d29d26f6fbb6314767",
"folder": "财务知识库",
"original_name": "2508.19855v3.pdf",
"stored_name": "6cad2936d57242d29d26f6fbb6314767__2508.19855v3.pdf",
"id": "bf761bd8eccf402bb676423d64401a56",
"folder": "报销制度",
"original_name": "远光《公司支出管理办法2024》.pdf",
"stored_name": "bf761bd8eccf402bb676423d64401a56__远光《公司支出管理办法2024》.pdf",
"mime_type": "application/pdf",
"extension": "pdf",
"size_bytes": 4097809,
"sha256": "9061363b164aaba132454e239ecc107076c81f61ecab1eb39cb43405d481e46a",
"created_at": "2026-05-09T06:06:51.631071+00:00",
"updated_at": "2026-05-09T06:06:51.631071+00:00",
"size_bytes": 621401,
"sha256": "67a74538bce0dec71ccbb947256cc2c9c0e672d148de49406b967ae1379dbece",
"created_at": "2026-05-09T08:39:53.788042+00:00",
"updated_at": "2026-05-09T08:39:53.788042+00:00",
"uploaded_by": "admin",
"version_number": 1
},
{
"id": "b01fe587d3d941f0a25d500751b27094",
"folder": "财务知识库",
"original_name": "面向财务领域的大语言模型 Fin-R1 研究内容与实施计划 (1).docx",
"stored_name": "b01fe587d3d941f0a25d500751b27094__面向财务领域的大语言模型 Fin-R1 研究内容与实施计划 (1).docx",
"mime_type": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"extension": "docx",
"size_bytes": 35521,
"sha256": "b300ba9c4c5bb03f4cbb27b52eea1932d1594398ae7b0f0c51c6c250deaceab4",
"created_at": "2026-05-09T06:07:10.525556+00:00",
"updated_at": "2026-05-09T07:17:28.581707+00:00",
"uploaded_by": "admin",
"version_number": 3
}
]
}

File diff suppressed because one or more lines are too long

View File

@@ -252,8 +252,8 @@ tbody tr.spotlight {
.detail-hero {
display: grid;
gap: 18px;
padding: 18px 20px;
gap: 10px;
padding: 16px 20px;
}
.hero-title {
@@ -291,32 +291,72 @@ tbody tr.spotlight {
line-height: 1.6;
}
.hero-review-meta {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
margin-top: 12px;
}
.hero-review-meta span {
display: inline-flex;
align-items: center;
gap: 5px;
min-height: 26px;
padding: 0 9px;
border-radius: 999px;
background: #f8fafc;
color: #475569;
font-size: 12px;
font-weight: 800;
}
.hero-review-meta i {
color: #059669;
font-size: 15px;
}
.hero-stats {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
}
.hero-stat {
padding: 14px 16px;
border-radius: 12px;
background: linear-gradient(180deg, #ffffff, #f8fafc);
border: 1px solid #edf2f7;
min-height: 54px;
display: grid;
align-content: center;
gap: 4px;
padding: 9px 12px;
border-radius: 10px;
background: #f8fafc;
border: 1px solid #e5eaf0;
}
.hero-stat span {
display: block;
color: #64748b;
font-size: 12px;
font-weight: 700;
font-size: 11px;
font-weight: 800;
line-height: 1.2;
}
.hero-stat strong {
display: block;
margin-top: 8px;
color: #0f172a;
font-size: 20px;
font-size: 15px;
font-weight: 850;
line-height: 1.25;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.hero-stat .status-pill {
width: fit-content;
min-height: 22px;
padding: 0 8px;
}
.detail-grid {
@@ -325,6 +365,15 @@ tbody tr.spotlight {
gap: 16px;
}
.detail-grid.skill-md-detail-grid {
grid-template-columns: minmax(0, 1fr) 360px;
align-items: stretch;
}
.detail-grid.skill-md-detail-grid .detail-main {
height: 100%;
}
.detail-main,
.detail-side {
display: grid;
@@ -405,6 +454,220 @@ tbody tr.spotlight {
resize: vertical;
}
.markdown-card {
min-height: 620px;
display: grid;
grid-template-rows: auto minmax(0, 1fr);
}
.markdown-card .field {
min-height: 0;
grid-template-rows: auto minmax(0, 1fr);
}
.markdown-editor {
min-height: 520px;
height: 100%;
font-family: "Cascadia Mono", "Consolas", monospace;
font-size: 13px;
line-height: 1.65;
white-space: pre;
}
.skill-review-side {
align-content: start;
}
.review-card {
position: sticky;
top: 0;
}
.reviewer-card {
border-color: rgba(16, 185, 129, 0.24);
background: linear-gradient(180deg, #ffffff, #f8fffc);
}
.review-list {
display: grid;
gap: 0;
}
.review-row {
display: grid;
gap: 6px;
padding: 12px 0;
border-top: 1px solid #edf2f7;
}
.review-row:first-child {
border-top: 0;
padding-top: 0;
}
.review-row span {
color: #64748b;
font-size: 12px;
font-weight: 800;
}
.review-row strong {
color: #0f172a;
font-size: 13px;
font-weight: 850;
line-height: 1.45;
}
.version-list {
display: grid;
gap: 0;
}
.version-row {
display: grid;
gap: 6px;
width: 100%;
padding: 10px 12px;
border-top: 1px solid #edf2f7;
border-right: 0;
border-bottom: 0;
border-left: 0;
background: transparent;
text-align: left;
cursor: pointer;
transition: background 180ms ease;
}
.version-row:first-child {
border-top: 0;
}
.version-row:hover {
border-radius: 10px;
background: #f8fafc;
}
.version-row.active {
border-radius: 10px;
background: rgba(16, 185, 129, 0.08);
}
.version-row-head {
display: grid;
grid-template-columns: minmax(52px, 1fr) 46px 82px;
align-items: center;
gap: 8px;
}
.version-row strong {
color: #0f172a;
font-size: 13px;
font-weight: 850;
}
.version-row span {
color: #94a3b8;
font-size: 11px;
font-weight: 800;
white-space: nowrap;
text-align: right;
}
.version-current-slot {
min-width: 46px;
display: grid;
place-items: center;
text-align: center;
}
.version-current-slot .current-version {
min-height: 20px;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0 7px;
border-radius: 999px;
background: #dcfce7;
color: #059669;
font-size: 11px;
font-weight: 850;
}
.version-row p {
color: #64748b;
font-size: 12px;
line-height: 1.5;
}
.modal-backdrop {
position: fixed;
inset: 0;
z-index: 60;
display: grid;
place-items: center;
padding: 24px;
background: rgba(15, 23, 42, 0.34);
}
.version-modal {
width: min(480px, 100%);
padding: 20px;
border-radius: 12px;
box-shadow: 0 24px 60px rgba(15, 23, 42, 0.2);
}
.version-modal-summary {
display: grid;
grid-template-columns: minmax(0, 1fr) 28px minmax(0, 1fr);
align-items: center;
gap: 10px;
margin-top: 4px;
}
.version-modal-summary div {
padding: 12px;
border: 1px solid #edf2f7;
border-radius: 10px;
background: #f8fafc;
}
.version-modal-summary span,
.version-modal-note span {
display: block;
color: #64748b;
font-size: 12px;
font-weight: 800;
}
.version-modal-summary strong,
.version-modal-note strong {
display: block;
margin-top: 6px;
color: #0f172a;
font-size: 15px;
font-weight: 850;
}
.version-modal-summary i {
color: #94a3b8;
text-align: center;
}
.version-modal-note {
margin-top: 12px;
padding: 12px;
border: 1px solid #edf2f7;
border-radius: 10px;
background: #fff;
}
.modal-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
margin-top: 16px;
}
.prompt-stack {
display: grid;
gap: 14px;
@@ -632,6 +895,10 @@ tbody tr.spotlight {
}
@media (max-width: 1320px) {
.detail-hero {
grid-template-columns: 1fr;
}
.hero-stats,
.form-grid,
.contract-grid {
@@ -641,6 +908,10 @@ tbody tr.spotlight {
.detail-grid {
grid-template-columns: 1fr;
}
.detail-grid.skill-md-detail-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 860px) {

View File

@@ -5,71 +5,70 @@
<div class="detail-scroll">
<section class="detail-hero panel">
<div class="hero-title">
<div class="skill-badge" :class="selectedSkill.badgeTone">{{ selectedSkill.scope }}</div>
<div class="skill-badge" :class="selectedSkill.badgeTone">{{ selectedSkill.typeLabel }}</div>
<h2>{{ selectedSkill.name }}</h2>
<p>{{ selectedSkill.summary }}</p>
</div>
<div class="hero-stats">
<div class="hero-stat">
<span>版本</span>
<strong>{{ selectedSkill.version }}</strong>
</div>
<div class="hero-stat">
<span>状态</span>
<div v-if="selectedSkill.type === 'skills'" class="hero-review-meta">
<span>
<i class="mdi mdi-account-check-outline"></i>
审核人{{ selectedSkill.reviewer }}
</span>
<b :class="['status-pill', selectedSkill.statusTone]">{{ selectedSkill.status }}</b>
</div>
<div class="hero-stat">
<span>触发命中率</span>
<strong>{{ selectedSkill.hitRate }}</strong>
</div>
<div class="hero-stat">
<span>负责人</span>
<strong>{{ selectedSkill.owner }}</strong>
<span>{{ selectedSkill.status === '已上线' ? '审核通过,可上线' : '待审核通过后上线' }}</span>
</div>
</div>
</section>
<div class="detail-grid">
<div class="detail-grid" :class="{ 'skill-md-detail-grid': selectedSkill.type === 'skills' }">
<section class="detail-main">
<article class="detail-card panel">
<article v-if="selectedSkill.type === 'skills'" class="detail-card panel markdown-card">
<div class="card-head">
<div>
<h3>基础配置</h3>
<p>定义 skill 的定位适用场景和默认执行策略</p>
<h3>Markdown 规则内容</h3>
<p>管理员直接编辑该 Skill 对应的 .md 审查规则文件内容</p>
</div>
<button class="mini-btn">
<i class="mdi mdi-content-save-outline"></i>
<span>保存 .md</span>
</button>
</div>
<label class="field">
<span>{{ selectedSkill.fields.find((field) => field.label === '文件路径')?.value }}</span>
<textarea
v-model="selectedSkill.markdownContent"
class="markdown-editor"
spellcheck="false"
></textarea>
</label>
</article>
<article v-else class="detail-card panel">
<div class="card-head">
<div>
<h3>{{ selectedSkill.configTitle }}</h3>
<p>{{ selectedSkill.configDesc }}</p>
</div>
<button class="mini-btn">保存草稿</button>
</div>
<div class="form-grid">
<label class="field">
<span>技能名称</span>
<input :value="selectedSkill.name" />
</label>
<label class="field">
<span>技能分类</span>
<input :value="selectedSkill.category" />
<label v-for="field in selectedSkill.fields" :key="field.label" class="field">
<span>{{ field.label }}</span>
<input :value="field.value" />
</label>
<label class="field span-2">
<span>适用描述</span>
<span>说明</span>
<textarea rows="3" :value="selectedSkill.summary"></textarea>
</label>
<label class="field">
<span>触发方式</span>
<input :value="selectedSkill.triggerMode" />
</label>
<label class="field">
<span>默认模型</span>
<input :value="selectedSkill.model" />
</label>
</div>
</article>
<article class="detail-card panel">
<article v-if="selectedSkill.type !== 'skills'" class="detail-card panel">
<div class="card-head">
<div>
<h3>提示词结构</h3>
<p>按系统约束输入期望输出格式三个层级组织 skill 行为</p>
<h3>{{ selectedSkill.detailTitle }}</h3>
<p>{{ selectedSkill.detailDesc }}</p>
</div>
<span class="edit-badge">{{ selectedSkill.promptSections.length }} </span>
</div>
@@ -85,24 +84,24 @@
</div>
</article>
<article class="detail-card panel">
<article v-if="selectedSkill.type !== 'skills'" class="detail-card panel">
<div class="card-head">
<div>
<h3>输出契约与测试样例</h3>
<p>确保 skill 在高频场景下输出稳定格式清晰</p>
<h3>{{ selectedSkill.outputTitle }}</h3>
<p>{{ selectedSkill.outputDesc }}</p>
</div>
<button class="mini-btn primary">运行测试</button>
</div>
<div class="contract-grid">
<div class="contract-panel">
<h4>输出要求</h4>
<h4>{{ selectedSkill.ruleListTitle }}</h4>
<ul>
<li v-for="rule in selectedSkill.outputRules" :key="rule">{{ rule }}</li>
</ul>
</div>
<div class="contract-panel">
<h4>测试样例</h4>
<h4>{{ selectedSkill.checkListTitle }}</h4>
<div v-for="test in selectedSkill.tests" :key="test.name" class="test-row">
<div>
<strong>{{ test.name }}</strong>
@@ -115,12 +114,43 @@
</article>
</section>
<aside class="detail-side">
<aside v-if="selectedSkill.type === 'skills'" class="detail-side skill-review-side">
<article class="side-card panel review-card">
<div class="card-head">
<div>
<h3>版本信息</h3>
<p>最近 5 个规则版本</p>
</div>
</div>
<div class="version-list">
<button
v-for="item in selectedSkill.history.slice(0, 5)"
:key="item.version + item.time"
class="version-row"
:class="{ active: item.version === selectedSkill.version }"
type="button"
@click="openVersionSwitch(item)"
>
<div class="version-row-head">
<strong>{{ item.version }}</strong>
<span class="version-current-slot">
<b v-if="item.version === selectedSkill.version" class="current-version">当前</b>
</span>
<span>{{ item.time }}</span>
</div>
<p>{{ item.note }}</p>
</button>
</div>
</article>
</aside>
<aside v-else class="detail-side">
<article class="side-card panel">
<div class="card-head">
<div>
<h3>触发规则</h3>
<p>当前命中策略</p>
<h3>{{ selectedSkill.triggerTitle }}</h3>
<p>{{ selectedSkill.triggerDesc }}</p>
</div>
</div>
<div class="tag-list">
@@ -131,8 +161,8 @@
<article class="side-card panel">
<div class="card-head">
<div>
<h3>工具权限</h3>
<p>可调用能力</p>
<h3>{{ selectedSkill.toolTitle }}</h3>
<p>{{ selectedSkill.toolDesc }}</p>
</div>
</div>
<div class="tool-list">
@@ -149,8 +179,8 @@
<article class="side-card panel">
<div class="card-head">
<div>
<h3>版本历史</h3>
<p>最近变更</p>
<h3>{{ selectedSkill.historyTitle }}</h3>
<p>{{ selectedSkill.historyDesc }}</p>
</div>
</div>
<div class="history-list">
@@ -164,12 +194,12 @@
<article class="side-card panel publish-card">
<div>
<h3>发布控制</h3>
<p>当前配置已通过核心检查可进入灰度或正式发布</p>
<h3>{{ selectedSkill.publishTitle }}</h3>
<p>{{ selectedSkill.publishDesc }}</p>
</div>
<div class="publish-summary">
<span>最近评审2026-05-05 14:20</span>
<strong>可发布</strong>
<span>{{ selectedSkill.publishMeta }}</span>
<strong>{{ selectedSkill.publishState }}</strong>
</div>
</article>
</aside>
@@ -179,7 +209,7 @@
<footer class="detail-actions">
<button class="back-action" type="button" @click="selectedSkill = null">
<i class="mdi mdi-arrow-left"></i>
<span>返回能列表</span>
<span>返回能列表</span>
</button>
<div class="detail-action-group">
@@ -200,15 +230,15 @@
</article>
<article v-else key="list" class="skill-list panel">
<nav class="status-tabs" aria-label="技能状态">
<nav class="status-tabs" aria-label="能力类型">
<button
v-for="tab in tabs"
:key="tab"
:key="tab.id"
type="button"
:class="{ active: activeTab === tab }"
@click="activeTab = tab"
:class="{ active: activeType === tab.id }"
@click="activeType = tab.id"
>
{{ tab }}
{{ tab.label }}
</button>
</nav>
@@ -221,24 +251,24 @@
</div>
<button class="create-btn" type="button">
<i class="mdi mdi-plus"></i>
<span>新建 Skill</span>
<span>{{ createButtonLabel }}</span>
</button>
</div>
<p class="hint"><i class="mdi mdi-information-outline"></i> 点击任意技能行进入设计与编辑界面</p>
<p class="hint"><i class="mdi mdi-information-outline"></i> {{ hintText }}</p>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>技能名称</th>
<th>分类</th>
<th>负责人</th>
<th>适用范围</th>
<th>模型</th>
<th>版本</th>
<th>{{ tableColumns.name }}</th>
<th>{{ tableColumns.category }}</th>
<th>{{ tableColumns.owner }}</th>
<th>{{ tableColumns.scope }}</th>
<th>{{ tableColumns.runtime }}</th>
<th>{{ tableColumns.version }}</th>
<th>状态</th>
<th>命中率</th>
<th>{{ tableColumns.metric }}</th>
<th>最近更新</th>
<th>操作</th>
</tr>
@@ -278,6 +308,41 @@
</div>
</article>
</Transition>
<Teleport to="body">
<div v-if="versionSwitchTarget" class="modal-backdrop" @click.self="cancelVersionSwitch">
<section class="version-modal panel" role="dialog" aria-modal="true" aria-labelledby="version-switch-title">
<div class="card-head">
<div>
<h3 id="version-switch-title">切换规则版本</h3>
<p>切换后编辑器会加载该版本的 .md 内容当前未保存内容不会自动发布</p>
</div>
</div>
<div class="version-modal-summary">
<div>
<span>当前版本</span>
<strong>{{ selectedSkill?.version }}</strong>
</div>
<i class="mdi mdi-arrow-right"></i>
<div>
<span>目标版本</span>
<strong>{{ versionSwitchTarget.version }}</strong>
</div>
</div>
<div class="version-modal-note">
<strong>{{ versionSwitchTarget.note }}</strong>
<span>{{ versionSwitchTarget.time }}</span>
</div>
<footer class="modal-actions">
<button class="minor-action" type="button" @click="cancelVersionSwitch">取消</button>
<button class="major-action" type="button" @click="confirmVersionSwitch">确认切换</button>
</footer>
</section>
</div>
</Teleport>
</section>
</template>

View File

@@ -1,249 +1,609 @@
import { computed, ref } from 'vue'
import { computed, ref } from 'vue'
const TYPE_META = {
skills: {
createButtonLabel: '新建 Skill',
hintText: 'Skills 对应报销审查规则 .md 文件,点击任意行查看规则配置与命中产出。',
filters: ['按规则场景筛选', '按风险等级筛选', '按负责人筛选'],
tableColumns: {
name: '规则文件',
category: '规则分类',
owner: '负责人',
scope: '适用范围',
runtime: '执行节点',
version: '版本',
metric: '命中率'
}
},
mcp: {
createButtonLabel: '接入 MCP',
hintText: 'MCP 管理外部服务连接如发票验真、预算、银行流水、OCR 与差旅平台。',
filters: ['按服务类型筛选', '按权限筛选', '按状态筛选'],
tableColumns: {
name: 'MCP 服务',
category: '服务类型',
owner: '维护人',
scope: '服务范围',
runtime: '调用方式',
version: '协议',
metric: '可用率'
}
},
schedules: {
createButtonLabel: '新建定时任务',
hintText: '定时任务用于每日风险检查、知识积累、报销报账和账款信息统计。',
filters: ['按运行频率筛选', '按产出类型筛选', '按告警级别筛选'],
tableColumns: {
name: '任务名称',
category: '任务类型',
owner: '负责人',
scope: '统计范围',
runtime: '运行计划',
version: '调度',
metric: '成功率'
}
}
}
function buildAsset({
type,
typeLabel,
id,
short,
name,
summary,
category,
owner,
reviewer = '待分配',
scope,
model,
version,
status,
statusTone,
hitRate,
updatedAt,
badgeTone,
triggerMode,
spotlight = false,
fields,
sections,
rules,
checks,
triggers,
tools,
history,
titles,
publish,
markdownContent = ''
}) {
return {
type,
typeLabel,
id,
short,
name,
summary,
category,
owner,
reviewer,
scope,
model,
version,
status,
statusTone,
hitRate,
updatedAt,
badgeTone,
triggerMode,
spotlight,
markdownContent,
fields,
promptSections: sections,
outputRules: rules,
tests: checks,
triggers,
tools,
history,
configTitle: titles.configTitle,
configDesc: titles.configDesc,
detailTitle: titles.detailTitle,
detailDesc: titles.detailDesc,
outputTitle: titles.outputTitle,
outputDesc: titles.outputDesc,
ruleListTitle: titles.ruleListTitle,
checkListTitle: titles.checkListTitle,
triggerTitle: titles.triggerTitle,
triggerDesc: titles.triggerDesc,
toolTitle: titles.toolTitle,
toolDesc: titles.toolDesc,
historyTitle: titles.historyTitle,
historyDesc: titles.historyDesc,
publishTitle: titles.publishTitle,
publishDesc: titles.publishDesc,
publishMeta: publish.meta,
publishState: publish.state,
heroStats: [
{ label: type === 'schedules' ? '调度计划' : '版本', value: version },
{ label: '状态', value: status, kind: 'status' },
{ label: type === 'mcp' ? '服务可用率' : type === 'schedules' ? '运行成功率' : '触发命中率', value: hitRate },
{ label: '负责人', value: owner }
]
}
}
const assets = [
buildAsset({
type: 'skills',
typeLabel: 'Skills',
id: 'SKL-001',
short: 'DR',
name: '重复报销识别规则.md',
summary: '识别同一发票号、金额、商户和日期组合下的重复报销风险。',
category: '报销审查规则',
owner: '财务风控组',
reviewer: '周敏',
scope: '发票与费用明细',
model: '提交时 + 夜间复核',
version: 'v1.9',
status: '已上线',
statusTone: 'success',
hitRate: '96.8%',
updatedAt: '2026-05-09 08:10',
badgeTone: 'emerald',
triggerMode: '申请提交 / 财务初审 / 夜间批扫',
spotlight: true,
fields: [
{ label: '规则名称', value: '重复报销识别规则.md' },
{ label: '文件路径', value: 'skills/reimbursement/duplicate-invoice.md' },
{ label: '规则等级', value: '高风险' },
{ label: '执行节点', value: '提交时 + 夜间复核' }
],
markdownContent: `# 重复报销识别规则
## 目标
识别同一发票、同一商户金额组合或高度相似附件导致的重复报销风险。
## 适用范围
- 费用类型:差旅、办公采购、业务招待、供应商报账
- 执行节点:申请提交、财务初审、夜间批扫
- 风险等级:高风险
## 输入字段
- invoice_code发票代码
- invoice_number发票号码
- seller_name销售方名称
- seller_tax_no销售方税号
- total_amount价税合计金额
- issue_date开票日期
- attachment_hash附件影像哈希
- applicant_id申请人
- reimbursement_id当前报销单号
## 判断规则
1. 发票代码和发票号码均一致时,直接命中高风险。
2. 发票号码缺失时,销售方、金额、开票日期和附件哈希相似度超过 0.92,进入人工复核。
3. 已驳回或已作废单据只作为参考证据,不直接判定重复。
4. 同一供应商周期性账单需按账期字段排除误报。
## 输出
\`\`\`json
{
"risk_code": "duplicate_invoice",
"risk_level": "high",
"matched_reimbursement_ids": [],
"evidence": [],
"suggestion": "人工复核或要求申请人补充说明"
}
\`\`\`
## 管理员备注
- 调整阈值前需要抽样复核最近 7 日误报样例。
- 新增排除条件必须补充测试样例。`,
sections: [
{ title: '输入字段', intent: '审查依赖', content: '发票号、销售方、金额、开票日期、附件 OCR、历史报销单号、提交人和成本中心。' },
{ title: '判断逻辑', intent: '规则主体', content: '同一发票号直接命中;发票号缺失时按销售方、金额、日期和影像哈希计算相似度。' },
{ title: '输出格式', intent: '风险结果', content: '输出重复风险标签、历史单据编号、相似字段、相似度和建议处置动作。' }
],
rules: ['命中同一发票号时必须标记高风险。', '相似度超过 0.92 时进入人工复核。', '必须返回历史单据证据,不允许只给结论。'],
checks: [
{ name: '同发票号重复', input: '发票号完全一致', result: '通过', tone: 'success' },
{ name: '影像相似重复', input: '发票号模糊但金额日期一致', result: '通过', tone: 'success' },
{ name: '商户简称误报', input: '同商户不同日期', result: '待评审', tone: 'warning' }
],
triggers: ['重复发票', '重复报销', '历史账本比对', '夜间风险扫描'],
tools: [
{ name: '发票 OCR MCP', scope: '票面字段识别', mode: '只读', tone: 'safe' },
{ name: '报销账本', scope: '历史单据检索', mode: '只读', tone: 'safe' },
{ name: '风险标记服务', scope: '写入审查结果', mode: '写入', tone: 'active' }
],
history: [
{ version: 'v1.9', note: '收紧金额差异阈值', time: '05-09 08:10' },
{ version: 'v1.8', note: '加入附件哈希比对', time: '05-06 18:30' },
{ version: 'v1.7', note: '补充同供应商周期账单排除条件', time: '05-02 16:15' },
{ version: 'v1.6', note: '增加历史驳回单据参考证据', time: '04-28 11:20' },
{ version: 'v1.5', note: '上线发票号精确匹配规则', time: '04-22 09:40' }
],
titles: {
configTitle: '规则文件配置',
configDesc: 'Skills 以 .md 文件维护报销审查规则,配置规则路径、等级和执行节点。',
detailTitle: '规则结构',
detailDesc: '按输入字段、判断逻辑和输出格式组织审查规则。',
outputTitle: '产出与校验',
outputDesc: '确保规则能稳定输出风险标签、证据和处置建议。',
ruleListTitle: '输出要求',
checkListTitle: '测试样例',
triggerTitle: '触发规则',
triggerDesc: '当前命中策略',
toolTitle: '依赖能力',
toolDesc: '规则运行时调用',
historyTitle: '版本历史',
historyDesc: '最近变更',
publishTitle: '发布控制',
publishDesc: '规则已通过核心样例,可进入线上审查链路。',
},
publish: { meta: '最近评审2026-05-09 08:10', state: '可发布' }
}),
buildAsset({
type: 'skills',
typeLabel: 'Skills',
id: 'SKL-002',
short: 'TS',
name: '差旅标准超额检查.md',
summary: '按城市、职级和费用类型检查差旅报销是否超过制度标准。',
category: '差旅审查规则',
owner: '制度运营组',
reviewer: '刘佳',
scope: '差旅报销',
model: '提交时触发',
version: 'v2.4',
status: '待评审',
statusTone: 'warning',
hitRate: '91.4%',
updatedAt: '2026-05-08 18:35',
badgeTone: 'amber',
triggerMode: '申请提交 / 审批中心',
fields: [
{ label: '规则名称', value: '差旅标准超额检查.md' },
{ label: '文件路径', value: 'skills/travel/standard-overrun.md' },
{ label: '规则等级', value: '中高风险' },
{ label: '执行节点', value: '提交时触发' }
],
markdownContent: `# 差旅标准超额检查
## 目标
根据公司差旅制度,检查住宿、交通、餐补等费用是否超过员工职级和城市等级对应标准。
## 适用范围
- 单据类型:差旅报销、差旅借款冲销
- 费用类型:住宿、机票、火车票、出租车、餐补
- 执行节点:申请提交、审批中心
## 输入字段
- employee_grade员工职级
- destination_city目的地城市
- city_tier城市等级
- expense_type费用类型
- expense_amount费用金额
- travel_days出差天数
- policy_refs制度条款映射
- exception_reason例外说明
## 判断规则
1. 按城市等级和员工职级读取报销标准上限。
2. 单项费用超过标准时,计算超额金额和超额比例。
3. 会议指定酒店、客户指定地点等例外原因存在时,转人工复核。
4. 无例外说明且超额比例超过 20%,标记中高风险。
## 输出
\`\`\`json
{
"risk_code": "travel_standard_overrun",
"risk_level": "medium_high",
"over_limit_amount": 0,
"policy_refs": [],
"exception_required": true,
"suggestion": "补充例外说明或调整报销金额"
}
\`\`\`
## 管理员备注
- 城市等级表每季度同步一次。
- 制度条款更新后必须同步更新 policy_refs。`,
sections: [
{ title: '制度映射', intent: '条款来源', content: '关联城市等级、员工职级、住宿/交通/餐补标准和例外审批条款。' },
{ title: '判断逻辑', intent: '标准比对', content: '按目的地城市等级和职级读取上限,逐条计算超额金额。' },
{ title: '输出格式', intent: '审批依据', content: '输出超额金额、制度引用、例外条件和补充说明要求。' }
],
rules: ['必须给出超额金额。', '必须引用制度条款。', '例外审批必须标记人工复核。'],
checks: [
{ name: '住宿超标', input: '一线城市住宿超 18%', result: '通过', tone: 'success' },
{ name: '会议酒店例外', input: '指定会议酒店超标', result: '评审中', tone: 'warning' }
],
triggers: ['差旅标准', '住宿超标', '交通超标', '例外审批'],
tools: [
{ name: '制度知识库', scope: '条款引用', mode: '只读', tone: 'safe' },
{ name: '员工组织服务', scope: '职级与部门', mode: '只读', tone: 'safe' }
],
history: [
{ version: 'v2.4', note: '新增会议酒店例外分支', time: '05-08 18:35' },
{ version: 'v2.3', note: '同步 2026 差旅制度', time: '05-06 10:20' },
{ version: 'v2.2', note: '调整新一线城市住宿上限', time: '05-01 14:05' },
{ version: 'v2.1', note: '增加餐补按天数校验', time: '04-25 17:30' },
{ version: 'v2.0', note: '重构城市等级映射表', time: '04-18 10:10' }
],
titles: {
configTitle: '规则文件配置',
configDesc: '维护差旅制度条款映射、城市等级表和例外审批条件。',
detailTitle: '规则结构',
detailDesc: '按制度映射、判断逻辑和输出格式组织规则。',
outputTitle: '产出与校验',
outputDesc: '超额检查结果进入审批意见和员工补充说明。',
ruleListTitle: '输出要求',
checkListTitle: '测试样例',
triggerTitle: '触发规则',
triggerDesc: '当前命中策略',
toolTitle: '依赖能力',
toolDesc: '规则运行时调用',
historyTitle: '版本历史',
historyDesc: '最近变更',
publishTitle: '评审控制',
publishDesc: '当前规则仍有例外分支待评审。',
},
publish: { meta: '最近评审2026-05-08 18:35', state: '待评审' }
}),
buildAsset({
type: 'mcp',
typeLabel: 'MCP',
id: 'MCP-001',
short: 'IV',
name: '发票验真 MCP',
summary: '连接税务票据验真、OCR 识别和发票状态查询能力。',
category: '票据服务',
owner: '平台集成组',
scope: '发票验真',
model: 'API 调用',
version: 'MCP 1.0',
status: '健康',
statusTone: 'success',
hitRate: '99.3%',
updatedAt: '2026-05-09 07:50',
badgeTone: 'blue',
triggerMode: '规则调用 / 批量任务调用',
fields: [
{ label: '服务名称', value: '发票验真 MCP' },
{ label: '服务地址', value: 'mcp://invoice-verification' },
{ label: '鉴权方式', value: 'API Key + IP 白名单' },
{ label: '降级策略', value: '转 OCR 本地解析并标记待验真' }
],
sections: [
{ title: '输入参数', intent: '服务调用', content: '附件文件、发票代码、发票号码、开票日期、金额和请求来源 trace_id。' },
{ title: '调用链路', intent: '外部服务', content: '先 OCR 识别票面字段,再调用验真接口查询真伪、作废、红冲状态。' },
{ title: '返回结构', intent: '标准输出', content: '返回票据字段、验真状态、异常原因、耗时和外部服务流水号。' }
],
rules: ['超时时间 8 秒。', '只读外部系统,不写入第三方。', '失败时必须返回可降级状态。'],
checks: [
{ name: '健康检查', input: '最近一次探活', result: '通过', tone: 'success' },
{ name: '批量验真', input: '436 张票据', result: '通过', tone: 'success' }
],
triggers: ['发票 OCR', '验真查询', '作废红冲', '重复报销规则'],
tools: [
{ name: '税务票据平台', scope: '验真查询', mode: '外部', tone: 'active' },
{ name: '密钥管理', scope: 'API Key 加密', mode: '安全', tone: 'safe' }
],
history: [
{ version: '07:50', note: '健康检查通过,延迟 580ms', time: '今日' },
{ version: '02:00', note: '夜间任务批量核验 436 张票据', time: '今日' }
],
titles: {
configTitle: 'MCP 连接配置',
configDesc: '配置外部服务地址、鉴权方式、权限范围和失败降级策略。',
detailTitle: '服务协议',
detailDesc: '按输入参数、调用链路和返回结构描述 MCP 能力。',
outputTitle: '返回与检查',
outputDesc: '服务输出会被审查规则、审批中心和定时任务消费。',
ruleListTitle: '调用约束',
checkListTitle: '健康检查',
triggerTitle: '服务场景',
triggerDesc: '当前被哪些能力调用',
toolTitle: '外部依赖',
toolDesc: 'MCP 背后连接',
historyTitle: '调用记录',
historyDesc: '最近运行',
publishTitle: '连接状态',
publishDesc: '服务健康,可供规则与定时任务调用。',
},
publish: { meta: '最近探活2026-05-09 07:50', state: '可用' }
}),
buildAsset({
type: 'schedules',
typeLabel: '定时任务',
id: 'JOB-001',
short: 'RK',
name: '每日风险巡检',
summary: '每天定时检查报销、报账、发票和账款数据,输出待处理风险队列。',
category: '风险巡检',
owner: '财务风控组',
scope: '全量待审与已付单据',
model: '每天 02:00',
version: '0 2 * * *',
status: '已调度',
statusTone: 'success',
hitRate: '100%',
updatedAt: '2026-05-09 02:18',
badgeTone: 'emerald',
triggerMode: 'Cron 定时调度',
fields: [
{ label: '任务名称', value: '每日风险巡检' },
{ label: 'Cron', value: '0 2 * * *' },
{ label: '扫描窗口', value: 'T-1 00:00 至 23:59' },
{ label: '告警阈值', value: '高风险 >= 10 或单笔 >= 50,000' }
],
sections: [
{ title: '数据抽取', intent: '扫描范围', content: '读取昨日新增报销、报账、发票、付款流水和审批反馈。' },
{ title: '规则执行', intent: '风险判断', content: '运行重复报销、超标、作废票、异常付款等 Skills并调用必要 MCP。' },
{ title: '结果写入', intent: '任务产出', content: '生成风险日报、风险工单、审计日志,并通知负责人。' }
],
rules: ['任务失败必须告警。', '每次运行记录规则版本和扫描窗口。', '高风险工单必须进入审批中心。'],
checks: [
{ name: '今日运行', input: '扫描 2146 条记录', result: '成功', tone: 'success' },
{ name: '高风险推送', input: '19 条风险工单', result: '成功', tone: 'success' }
],
triggers: ['每日风险检查', '发票验真', '账款核对', '风险日报'],
tools: [
{ name: '报销审查 Skills', scope: '规则执行', mode: '调用', tone: 'active' },
{ name: '发票验真 MCP', scope: '票据核验', mode: '调用', tone: 'active' },
{ name: '账款流水 MCP', scope: '付款核对', mode: '调用', tone: 'active' }
],
history: [
{ version: '02:18', note: '今日运行成功,生成 19 条高风险工单', time: '今日' },
{ version: '02:00', note: '任务开始,扫描 2026-05-08 数据', time: '今日' }
],
titles: {
configTitle: '定时任务配置',
configDesc: '配置运行频率、扫描范围、依赖能力和告警出口。',
detailTitle: '任务流程',
detailDesc: '从数据抽取、规则执行到结果写入描述调度链路。',
outputTitle: '产出与检查',
outputDesc: '定时任务产出进入风险看板、审批中心和审计留痕。',
ruleListTitle: '运行要求',
checkListTitle: '最近检查',
triggerTitle: '任务场景',
triggerDesc: '当前覆盖的日常运营事项',
toolTitle: '依赖能力',
toolDesc: '任务运行时调用',
historyTitle: '运行记录',
historyDesc: '最近执行',
publishTitle: '调度控制',
publishDesc: '任务已纳入每日自动巡检。',
},
publish: { meta: '最近运行2026-05-09 02:18', state: '已调度' }
}),
buildAsset({
type: 'schedules',
typeLabel: '定时任务',
id: 'JOB-002',
short: 'FN',
name: '每日报销报账与账款统计',
summary: '每天统计报销、报账、付款和账款信息,更新运营总览和财务日报。',
category: '财务统计',
owner: '财务运营组',
scope: '报销、报账、账款',
model: '每天 06:00',
version: '0 6 * * *',
status: '已调度',
statusTone: 'success',
hitRate: '99.1%',
updatedAt: '2026-05-09 06:12',
badgeTone: 'blue',
triggerMode: 'Cron 定时调度',
fields: [
{ label: '任务名称', value: '每日报销报账与账款统计' },
{ label: 'Cron', value: '0 6 * * *' },
{ label: '统计口径', value: '自然日 + 财务月' },
{ label: '刷新对象', value: '运营总览、财务日报、账款看板' }
],
sections: [
{ title: '账款同步', intent: '外部数据', content: '拉取银行流水、付款结果和供应商收款状态。' },
{ title: '指标聚合', intent: '统计口径', content: '按部门、费用类型、报账状态、付款状态和账龄聚合。' },
{ title: '日报刷新', intent: '看板产出', content: '写入财务日报快照,并刷新运营总览。' }
],
rules: ['统计口径必须记录。', '账款同步失败时保留上一版快照并告警。', '日报刷新后写入审计日志。'],
checks: [
{ name: '日报刷新', input: '总览指标更新', result: '成功', tone: 'success' },
{ name: '账款匹配', input: '识别 8 条超期待付款', result: '成功', tone: 'success' }
],
triggers: ['报销统计', '报账统计', '账款统计', '财务日报'],
tools: [
{ name: '账款流水 MCP', scope: '付款与流水', mode: '调用', tone: 'active' },
{ name: '报销报账数据库', scope: '统计读取', mode: '只读', tone: 'safe' }
],
history: [
{ version: '06:12', note: '日报刷新完成,总览看板已更新', time: '今日' },
{ version: '06:04', note: '账款匹配完成,识别 8 条超期待付款', time: '今日' }
],
titles: {
configTitle: '定时任务配置',
configDesc: '配置统计口径、维度、账款数据源和看板刷新策略。',
detailTitle: '任务流程',
detailDesc: '从账款同步、指标聚合到日报刷新描述调度链路。',
outputTitle: '产出与检查',
outputDesc: '统计产出用于总览、日报和账款追踪。',
ruleListTitle: '运行要求',
checkListTitle: '最近检查',
triggerTitle: '任务场景',
triggerDesc: '当前覆盖的日常运营事项',
toolTitle: '依赖能力',
toolDesc: '任务运行时调用',
historyTitle: '运行记录',
historyDesc: '最近执行',
publishTitle: '调度控制',
publishDesc: '任务已纳入每日财务运营统计。',
},
publish: { meta: '最近运行2026-05-09 06:12', state: '已调度' }
})
]
export default {
name: 'AuditView' ,
setup(props, { emit }) {
const tabs = ['全部技能', '已上线', '草稿中', '待评审', '异常告警']
const filters = ['按分类筛选', '按模型筛选', '按负责人筛选']
const activeTab = ref(tabs[0])
const selectedSkill = ref(null)
const skills = [
{
id: 'SKL-001',
short: 'TR',
name: '差旅申请助手',
summary: '生成出差申请、补齐行程信息并关联预订动作。',
category: '流程型 Skill',
owner: '张晓明',
scope: '员工自助',
model: 'GPT-5.4',
version: 'v2.3',
status: '已上线',
statusTone: 'success',
hitRate: '92.6%',
updatedAt: '2026-05-05 09:20',
badgeTone: 'emerald',
triggerMode: '显式入口 + 语义触发',
spotlight: true,
promptSections: [
{
title: '系统定位',
intent: '约束 Skill 目标与边界',
content: '负责帮助员工完成差旅申请草稿生成、行程补齐和预订前核对。禁止直接跳过必要审批节点。'
},
{
title: '输入预期',
intent: '定义需要抽取的字段',
content: '抽取出发地、目的地、出差日期、事由、同行人、预算中心与是否需要预订机票/酒店。缺失时逐步追问。'
},
{
title: '输出格式',
intent: '约束最终返回结构',
content: '输出申请摘要、缺失项清单、下一步操作建议。若信息齐全,生成结构化草稿并提示用户确认。'
}
],
outputRules: [
'优先返回结构化摘要,再给行动建议。',
'缺失信息必须列成 checklist不可混写在段落里。',
'遇到预算冲突时必须提示人工审批节点。'
],
tests: [
{ name: '基础申请生成', input: '北京到上海,后天出差两天', result: '通过', tone: 'success' },
{ name: '缺失预算中心追问', input: '我要去深圳见客户', result: '通过', tone: 'success' },
{ name: '异常日期冲突', input: '返回日期早于出发日期', result: '待修复', tone: 'warning' }
],
triggers: ['差旅申请', '出差申请', '预订机票', '补齐行程'],
tools: [
{ name: '预订系统 API', scope: '机票 / 酒店查询', mode: '只读', tone: 'safe' },
{ name: '报销草稿生成器', scope: '创建申请草稿', mode: '写入', tone: 'active' },
{ name: '预算中心校验', scope: '预算占用校验', mode: '校验', tone: 'safe' }
],
history: [
{ version: 'v2.3', note: '补充预算冲突追问逻辑', time: '05-05 09:20' },
{ version: 'v2.2', note: '优化酒店预订字段抽取', time: '05-01 17:45' },
{ version: 'v2.1', note: '新增同行人识别', time: '04-28 11:10' }
]
},
{
id: 'SKL-002',
short: 'AU',
name: '审批意见生成器',
summary: '基于单据、风险点和制度命中结果生成审批意见。',
category: '审核型 Skill',
owner: '李文静',
scope: '财务审批',
model: 'GPT-5.4',
version: 'v1.8',
status: '待评审',
statusTone: 'warning',
hitRate: '88.4%',
updatedAt: '2026-05-04 19:10',
badgeTone: 'violet',
triggerMode: '审批中心按钮触发',
promptSections: [
{
title: '系统定位',
intent: '聚焦审批建议生成',
content: '读取单据、制度命中和风险标签后,生成可直接复用的审批意见,不代替最终审批决定。'
},
{
title: '输入预期',
intent: '依赖字段',
content: '依赖报销类型、金额、风险项、附件齐备情况、历史审批结论。'
},
{
title: '输出格式',
intent: '生成标准话术',
content: '输出通过 / 驳回 / 补件三种意见模板,并附上判断依据。'
}
],
outputRules: [
'意见必须引用风险点或制度条款作为依据。',
'驳回类结论需明确补充动作。',
'避免输出过长段落,优先三段式表达。'
],
tests: [
{ name: '高风险驳回意见', input: '重复发票 + 缺附件', result: '通过', tone: 'success' },
{ name: '低风险通过意见', input: '规则全通过', result: '通过', tone: 'success' },
{ name: '混合场景表达', input: '超标但说明充分', result: '评审中', tone: 'warning' }
],
triggers: ['生成审批意见', '通过意见', '驳回意见', '补件说明'],
tools: [
{ name: '审批单据上下文', scope: '当前单据读取', mode: '只读', tone: 'safe' },
{ name: '制度命中服务', scope: '条款引用', mode: '校验', tone: 'safe' },
{ name: '审批结果写回', scope: '保存意见', mode: '写入', tone: 'active' }
],
history: [
{ version: 'v1.8', note: '调整高风险话术严谨度', time: '05-04 19:10' },
{ version: 'v1.7', note: '补充制度条款引用模板', time: '05-02 10:30' }
]
},
{
id: 'SKL-003',
short: 'KB',
name: '知识检索编排器',
summary: '根据问题意图匹配制度、FAQ 与最近更新文档。',
category: '知识型 Skill',
owner: '王磊',
scope: '知识管理',
model: 'GPT-5.2',
version: 'v3.1',
status: '已上线',
statusTone: 'success',
hitRate: '94.1%',
updatedAt: '2026-05-03 15:40',
badgeTone: 'blue',
triggerMode: '问答语义召回',
promptSections: [
{
title: '系统定位',
intent: '文档命中与答案编排',
content: '识别问题主题后优先召回制度文档、FAQ 与近期更新资料,再组织成引用式回答。'
},
{
title: '输入预期',
intent: '需要识别的意图',
content: '识别报销、发票、差旅、借款、预算等主题,以及用户是否在追问例外情况。'
},
{
title: '输出格式',
intent: '答案结构',
content: '先结论,再条款引用,再相关文档链接。若知识不足,明确提示未命中。'
}
],
outputRules: [
'必须区分“制度原文依据”和“解释性建议”。',
'引用命中不足时,不可编造制度条款。',
'输出需附上最近更新时间。'
],
tests: [
{ name: '标准知识问答', input: '住宿超标怎么办', result: '通过', tone: 'success' },
{ name: '跨文档综合问答', input: '差旅借款后如何冲销', result: '通过', tone: 'success' }
],
triggers: ['制度查询', '差旅标准', '发票规范', '借款冲销'],
tools: [
{ name: '知识库索引', scope: '文档召回', mode: '只读', tone: 'safe' },
{ name: 'FAQ 排序器', scope: '答案重排', mode: '校验', tone: 'safe' }
],
history: [
{ version: 'v3.1', note: '加入最近更新知识优先级', time: '05-03 15:40' },
{ version: 'v3.0', note: '知识命中格式重构', time: '04-29 18:20' }
]
},
{
id: 'SKL-004',
short: 'RK',
name: '风险解释助手',
summary: '向员工解释拦截原因,并给出补件或修正建议。',
category: '解释型 Skill',
owner: '陈杰',
scope: '员工自助',
model: 'GPT-5.4-Mini',
version: 'v1.4',
status: '草稿中',
statusTone: 'draft',
hitRate: '79.8%',
updatedAt: '2026-05-02 11:05',
badgeTone: 'amber',
triggerMode: '风险拦截后提示入口',
promptSections: [
{
title: '系统定位',
intent: '解释风控结论',
content: '将复杂风控规则解释成员工可执行的修正动作,不暴露内部评分细节。'
},
{
title: '输入预期',
intent: '关注异常标签',
content: '读取异常标签、相关票据、制度限制和当前流程节点。'
},
{
title: '输出格式',
intent: '行动导向',
content: '按“原因 - 影响 - 处理建议”输出,不使用过于生硬的审计口吻。'
}
],
outputRules: [
'建议必须可以执行,避免空泛表述。',
'不展示内部风控分值。',
'涉及附件缺失时输出具体材料名称。'
],
tests: [
{ name: '住宿超标解释', input: '酒店单晚超标 18%', result: '通过', tone: 'success' },
{ name: '重复发票风险解释', input: '发票号重复', result: '待修复', tone: 'warning' }
],
triggers: ['为什么被拦截', '风险原因', '补件说明'],
tools: [
{ name: '风险标签读取', scope: '异常原因', mode: '只读', tone: 'safe' },
{ name: '制度比对服务', scope: '规则解释', mode: '校验', tone: 'safe' }
],
history: [
{ version: 'v1.4', note: '新增补件导向模板', time: '05-02 11:05' },
{ version: 'v1.3', note: '优化语气控制', time: '04-30 16:48' }
]
}
name: 'AuditView',
setup() {
const tabs = [
{ id: 'skills', label: 'Skills' },
{ id: 'mcp', label: 'MCP' },
{ id: 'schedules', label: '定时任务' }
]
const activeType = ref('skills')
const selectedSkill = ref(null)
const versionSwitchTarget = ref(null)
const visibleSkills = computed(() => {
if (activeTab.value === '全部技能') return skills
const map = {
已上线: '已上线',
草稿中: '草稿中',
待评审: '待评审',
异常告警: '异常告警'
const activeMeta = computed(() => TYPE_META[activeType.value])
const filters = computed(() => activeMeta.value.filters)
const createButtonLabel = computed(() => activeMeta.value.createButtonLabel)
const hintText = computed(() => activeMeta.value.hintText)
const tableColumns = computed(() => activeMeta.value.tableColumns)
const visibleSkills = computed(() => assets.filter((item) => item.type === activeType.value))
function openVersionSwitch(version) {
if (!selectedSkill.value || version.version === selectedSkill.value.version) {
return
}
return skills.filter((item) => item.status === map[activeTab.value])
})
versionSwitchTarget.value = version
}
function cancelVersionSwitch() {
versionSwitchTarget.value = null
}
function confirmVersionSwitch() {
if (!selectedSkill.value || !versionSwitchTarget.value) {
return
}
selectedSkill.value.version = versionSwitchTarget.value.version
selectedSkill.value.updatedAt = versionSwitchTarget.value.time
if (versionSwitchTarget.value.markdownContent) {
selectedSkill.value.markdownContent = versionSwitchTarget.value.markdownContent
} else {
selectedSkill.value.markdownContent = `${selectedSkill.value.markdownContent}\n\n<!-- 已切换到 ${versionSwitchTarget.value.version}${versionSwitchTarget.value.note} -->`
}
versionSwitchTarget.value = null
}
return {
tabs,
filters,
activeTab,
activeType,
createButtonLabel,
hintText,
tableColumns,
selectedSkill,
skills,
visibleSkills
versionSwitchTarget,
visibleSkills,
openVersionSwitch,
cancelVersionSwitch,
confirmVersionSwitch
}
}
}