refactor(audit): split list detail flows
This commit is contained in:
@@ -241,10 +241,6 @@
|
||||
.main.settings-main {
|
||||
grid-template-rows: minmax(0, 1fr);
|
||||
}
|
||||
.main.audit-detail-main,
|
||||
.main.digital-employees-detail-main {
|
||||
grid-template-rows: minmax(0, 1fr);
|
||||
}
|
||||
.workarea { min-height: 0; overflow: auto; padding: 24px; }
|
||||
.workarea.requests-workarea,
|
||||
.workarea.documents-workarea,
|
||||
|
||||
@@ -243,6 +243,22 @@
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.detail-topbar-actions {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.detail-kpi-chips {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.detail-kpi-chip {
|
||||
min-width: 142px;
|
||||
}
|
||||
|
||||
.detail-alert-strip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -1337,213 +1337,6 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.asset-detail-topbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 14px 0 10px;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.asset-detail-topbar.panel {
|
||||
padding: 14px 0 10px;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.asset-detail-topbar-main {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.asset-detail-topbar-main h2 {
|
||||
margin: 0;
|
||||
color: #0f172a;
|
||||
font-size: 18px;
|
||||
font-weight: 850;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.asset-detail-topbar-main p {
|
||||
flex-basis: 100%;
|
||||
margin: 0;
|
||||
max-width: 860px;
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.asset-detail-topbar-meta {
|
||||
flex: 0 0 auto;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.asset-detail-topbar .hero-review-meta {
|
||||
flex-basis: 100%;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.asset-detail-topbar .review-note-block {
|
||||
flex-basis: 100%;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.asset-detail-topbar .hero-stats {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.asset-detail-topbar .hero-stat {
|
||||
min-height: 30px;
|
||||
padding: 0 10px;
|
||||
border-radius: 999px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.asset-detail-topbar .hero-stat span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.asset-detail-topbar .hero-stat strong {
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.json-risk-editor-head {
|
||||
align-items: center;
|
||||
padding-top: 4px;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.json-risk-score-ring {
|
||||
--score-ring: #f97316;
|
||||
--score-ring-bg: #fff7ed;
|
||||
flex: 0 0 auto;
|
||||
width: 82px;
|
||||
height: 82px;
|
||||
border-radius: 999px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
align-content: center;
|
||||
gap: 1px;
|
||||
border: 2px solid var(--score-ring);
|
||||
background: var(--score-ring-bg);
|
||||
box-shadow: 0 12px 30px rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
|
||||
.json-risk-score-ring strong {
|
||||
color: #0f172a;
|
||||
font-size: 22px;
|
||||
font-weight: 900;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.json-risk-score-ring span,
|
||||
.json-risk-score-ring em {
|
||||
color: #64748b;
|
||||
font-size: 11px;
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.json-risk-score-ring em {
|
||||
color: var(--score-ring);
|
||||
}
|
||||
|
||||
.json-risk-score-ring.low {
|
||||
--score-ring: #2563eb;
|
||||
--score-ring-bg: #eff6ff;
|
||||
}
|
||||
|
||||
.json-risk-score-ring.medium {
|
||||
--score-ring: #f97316;
|
||||
--score-ring-bg: #fff7ed;
|
||||
}
|
||||
|
||||
.json-risk-score-ring.high {
|
||||
--score-ring: #dc2626;
|
||||
--score-ring-bg: #fef2f2;
|
||||
}
|
||||
|
||||
.json-risk-score-ring.critical {
|
||||
--score-ring: #991b1b;
|
||||
--score-ring-bg: #fff1f2;
|
||||
}
|
||||
|
||||
.json-risk-editor-title {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.json-risk-head-copy {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.json-risk-head-title-row {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.json-risk-editor-title h2 {
|
||||
color: #0f172a;
|
||||
font-size: 18px;
|
||||
font-weight: 850;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.json-risk-editor-title p {
|
||||
margin-top: 2px;
|
||||
max-width: 760px;
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.json-risk-head-subtitle {
|
||||
display: -webkit-box;
|
||||
margin: 0;
|
||||
max-width: 760px;
|
||||
overflow: hidden;
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
line-height: 1.55;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
|
||||
.json-risk-head-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.json-risk-head-meta span {
|
||||
min-height: 24px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0 8px;
|
||||
border-radius: 999px;
|
||||
background: #f8fafc;
|
||||
color: #475569;
|
||||
font-size: 12px;
|
||||
font-weight: 750;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.skill-name-cell .skill-list-subtitle {
|
||||
display: -webkit-box;
|
||||
overflow: hidden;
|
||||
@@ -1554,40 +1347,6 @@
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
|
||||
.json-risk-editor-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.json-risk-mode-pill {
|
||||
min-height: 28px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0 10px;
|
||||
border-radius: 999px;
|
||||
background: #fff1f2;
|
||||
color: #be123c;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.json-risk-mode-pill.high {
|
||||
background: #fef2f2;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.json-risk-mode-pill.medium {
|
||||
background: #fff7ed;
|
||||
color: #ea580c;
|
||||
}
|
||||
|
||||
.json-risk-mode-pill.low {
|
||||
background: var(--success-soft);
|
||||
color: var(--success-hover);
|
||||
}
|
||||
|
||||
.json-risk-editor-body {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
@@ -1853,15 +1612,3 @@
|
||||
grid-column: span 1;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 860px) {
|
||||
.json-risk-editor-head {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.json-risk-editor-actions {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -609,7 +609,8 @@ tbody tr.is-disabled:hover {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.skill-badge {
|
||||
.skill-badge,
|
||||
.skill-detail :deep(.skill-badge) {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 24px;
|
||||
@@ -620,11 +621,16 @@ tbody tr.is-disabled:hover {
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.skill-badge.primary { background: var(--theme-gradient-primary); }
|
||||
.skill-badge.rose { background: linear-gradient(135deg, #f43f5e, #e11d48); }
|
||||
.skill-badge.violet { background: linear-gradient(135deg, #8b5cf6, #7c3aed); }
|
||||
.skill-badge.blue { background: linear-gradient(135deg, #3b82f6, #2563eb); }
|
||||
.skill-badge.amber { background: linear-gradient(135deg, #f59e0b, #ea580c); }
|
||||
.skill-badge.primary,
|
||||
.skill-detail :deep(.skill-badge.primary) { background: var(--theme-gradient-primary); }
|
||||
.skill-badge.rose,
|
||||
.skill-detail :deep(.skill-badge.rose) { background: linear-gradient(135deg, #f43f5e, #e11d48); }
|
||||
.skill-badge.violet,
|
||||
.skill-detail :deep(.skill-badge.violet) { background: linear-gradient(135deg, #8b5cf6, #7c3aed); }
|
||||
.skill-badge.blue,
|
||||
.skill-detail :deep(.skill-badge.blue) { background: linear-gradient(135deg, #3b82f6, #2563eb); }
|
||||
.skill-badge.amber,
|
||||
.skill-detail :deep(.skill-badge.amber) { background: linear-gradient(135deg, #f59e0b, #ea580c); }
|
||||
|
||||
.hero-title h2 {
|
||||
margin-top: 10px;
|
||||
@@ -868,41 +874,6 @@ tbody tr.is-disabled:hover {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.spreadsheet-editor-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.spreadsheet-editor-title {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.spreadsheet-editor-title h2 {
|
||||
color: #0f172a;
|
||||
font-size: 18px;
|
||||
font-weight: 850;
|
||||
}
|
||||
|
||||
.spreadsheet-editor-title p {
|
||||
margin-top: 2px;
|
||||
max-width: 760px;
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.spreadsheet-editor-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.spreadsheet-editor-meta {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
|
||||
@@ -1,23 +1,5 @@
|
||||
<template>
|
||||
<section class="json-risk-editor-shell panel digital-worker-detail-shell">
|
||||
<header class="json-risk-editor-head asset-detail-topbar list-toolbar">
|
||||
<div class="json-risk-editor-title asset-detail-topbar-main filter-set">
|
||||
<div class="json-risk-head-copy">
|
||||
<div class="json-risk-head-title-row">
|
||||
<h2>{{ selectedSkill.name }}</h2>
|
||||
</div>
|
||||
<p class="json-risk-head-subtitle">
|
||||
{{ selectedSkill.summary || '后台自动执行的数字员工技能。' }}
|
||||
</p>
|
||||
<div class="json-risk-head-meta">
|
||||
<span>技能编号:{{ selectedSkill.code || '-' }}</span>
|
||||
<span>执行计划:{{ digitalEmployee.scheduleLabel || selectedSkill.scope || '-' }}</span>
|
||||
<span>最近更新:{{ selectedSkill.updatedAt || '-' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="json-risk-editor-body">
|
||||
<section class="json-risk-main-stage">
|
||||
<article class="detail-card panel json-risk-summary-card">
|
||||
|
||||
@@ -1,31 +1,5 @@
|
||||
<template>
|
||||
<section class="json-risk-editor-shell panel">
|
||||
<header class="json-risk-editor-head asset-detail-topbar list-toolbar">
|
||||
<div class="json-risk-editor-title asset-detail-topbar-main filter-set">
|
||||
<div class="json-risk-head-copy">
|
||||
<div class="json-risk-head-title-row">
|
||||
<h2>{{ selectedSkill.name }}</h2>
|
||||
</div>
|
||||
<p class="json-risk-head-subtitle">
|
||||
{{ selectedSkill.riskRuleSubtitle || '平台通用风险规则' }}
|
||||
</p>
|
||||
<div class="json-risk-head-meta">
|
||||
<span v-if="selectedSkill.riskCategory">适用场景:{{ selectedSkill.riskCategory }}</span>
|
||||
<span>业务域:{{ selectedSkill.category || '-' }}</span>
|
||||
<span>最近更新:{{ selectedSkill.updatedAt || '-' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="json-risk-score-ring"
|
||||
:class="selectedSkill.riskRuleScoreLevel || selectedSkill.riskRuleSeverity"
|
||||
>
|
||||
<strong>{{ selectedSkill.riskRuleScore ?? '--' }}</strong>
|
||||
<span>风险分</span>
|
||||
<em>{{ selectedSkill.riskRuleScoreLabel || selectedSkill.riskRuleSeverityLabel }}</em>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div
|
||||
v-if="selectedSkill.riskRuleGenerationFailed"
|
||||
class="json-risk-generation-failure"
|
||||
|
||||
@@ -1,21 +1,5 @@
|
||||
<template>
|
||||
<section class="spreadsheet-editor-shell panel">
|
||||
<header class="spreadsheet-editor-head asset-detail-topbar list-toolbar">
|
||||
<div class="spreadsheet-editor-title asset-detail-topbar-main filter-set">
|
||||
<div class="skill-badge" :class="selectedSkill.badgeTone">{{ selectedSkill.typeLabel }}</div>
|
||||
<div>
|
||||
<h2>{{ selectedSkill.name }}</h2>
|
||||
<p>{{ selectedSkill.summary || '当前资产尚未补充说明。' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="spreadsheet-editor-actions asset-detail-topbar-meta toolbar-actions">
|
||||
<span class="spreadsheet-mode-pill">
|
||||
{{ selectedSpreadsheetModeLabel }}
|
||||
</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<input
|
||||
ref="fileInput"
|
||||
class="spreadsheet-upload-input"
|
||||
@@ -126,7 +110,6 @@ defineOptions({
|
||||
|
||||
defineProps({
|
||||
selectedSkill: { type: Object, required: true },
|
||||
selectedSpreadsheetModeLabel: { type: String, default: '' },
|
||||
selectedSpreadsheetFileName: { type: String, default: '' },
|
||||
selectedSpreadsheetChangeRecords: { type: Array, default: () => [] },
|
||||
spreadsheetOnlyOfficeHostId: { type: String, required: true },
|
||||
|
||||
@@ -175,32 +175,6 @@
|
||||
|
||||
<!-- 详情视图 (全屏样式,参考 AuditJsonRiskRuleDetail) -->
|
||||
<div v-else key="detail" class="json-risk-editor-shell panel work-records-detail-stage">
|
||||
<header class="json-risk-editor-head asset-detail-topbar list-toolbar">
|
||||
<div class="json-risk-editor-title asset-detail-topbar-main filter-set">
|
||||
<div class="json-risk-head-copy">
|
||||
<div class="json-risk-head-title-row">
|
||||
<h2>{{ resolveWorkRecordTitle(selectedRunDetail) }}</h2>
|
||||
</div>
|
||||
<p class="json-risk-head-subtitle">
|
||||
执行工作流:{{ resolveWorkRecordModuleLabel(selectedRunDetail) }}
|
||||
</p>
|
||||
<div class="json-risk-head-meta">
|
||||
<span>Run ID:{{ selectedRunDetail.run_id }}</span>
|
||||
<span>触发来源:{{ resolveWorkRecordSourceLabel(selectedRunDetail.source) }}</span>
|
||||
<span>开始时间:{{ formatWorkRecordDateTime(selectedRunDetail.started_at) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="json-risk-score-ring"
|
||||
:class="selectedRunDetail.status"
|
||||
>
|
||||
<strong style="font-size: 16px; font-weight: 900;">{{ resolveWorkRecordStatusLabel(selectedRunDetail) }}</strong>
|
||||
<span>运行状态</span>
|
||||
<em>{{ resolveWorkRecordStatusNote(selectedRunDetail) || '执行完毕' }}</em>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div v-if="detailLoading" class="work-record-detail-state panel" style="min-height: 200px; display: grid; place-items: center; border: 0;">
|
||||
<TableLoadingState
|
||||
variant="panel"
|
||||
@@ -345,7 +319,7 @@ defineOptions({
|
||||
name: 'DigitalEmployeeWorkRecords'
|
||||
})
|
||||
|
||||
const emit = defineEmits(['summary-change', 'detail-open-change'])
|
||||
const emit = defineEmits(['summary-change', 'detail-open-change', 'detail-topbar-change'])
|
||||
|
||||
const { toast } = useToast()
|
||||
const runs = ref([])
|
||||
@@ -362,9 +336,57 @@ watch(detailOpen, (newVal) => {
|
||||
}, { immediate: true })
|
||||
let pollTimer = 0
|
||||
|
||||
const totalCount = computed(() => runs.value.length)
|
||||
const successCount = computed(() => runs.value.filter((run) => run.status === 'succeeded').length)
|
||||
const failedCount = computed(() => runs.value.filter((run) => run.status === 'failed').length)
|
||||
const workRecordSummary = computed(() =>
|
||||
runs.value.reduce(
|
||||
(summary, run) => {
|
||||
summary.total += 1
|
||||
if (run.status === 'succeeded') {
|
||||
summary.succeeded += 1
|
||||
} else if (run.status === 'failed') {
|
||||
summary.failed += 1
|
||||
}
|
||||
return summary
|
||||
},
|
||||
{ total: 0, succeeded: 0, failed: 0 }
|
||||
)
|
||||
)
|
||||
const workRecordDetailTopBar = computed(() => {
|
||||
const detail = selectedRunDetail.value
|
||||
if (!detail) return null
|
||||
|
||||
const status = String(detail.status || '').toLowerCase()
|
||||
const statusColor =
|
||||
status === 'failed'
|
||||
? '#ef4444'
|
||||
: status === 'succeeded'
|
||||
? 'var(--success)'
|
||||
: '#3b82f6'
|
||||
|
||||
return {
|
||||
view: {
|
||||
title: resolveWorkRecordTitle(detail),
|
||||
desc: `执行工作流:${resolveWorkRecordModuleLabel(detail)}`
|
||||
},
|
||||
kpis: [
|
||||
{
|
||||
label: '运行状态',
|
||||
value: resolveWorkRecordStatusLabel(detail),
|
||||
unit: '',
|
||||
meta: resolveWorkRecordStatusNote(detail) || '执行完毕',
|
||||
trend: status === 'failed' ? 'down' : 'up',
|
||||
color: statusColor
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
watch(
|
||||
workRecordDetailTopBar,
|
||||
(value) => {
|
||||
emit('detail-topbar-change', value)
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
)
|
||||
|
||||
const listKeyword = ref('')
|
||||
const activeModule = ref('全部')
|
||||
@@ -479,9 +501,9 @@ async function loadWorkRecords(showToast = false) {
|
||||
const payload = await fetchAgentRuns({ agent: 'hermes', limit: 100 })
|
||||
runs.value = Array.isArray(payload) ? payload : []
|
||||
emit('summary-change', {
|
||||
total: totalCount.value,
|
||||
succeeded: successCount.value,
|
||||
failed: failedCount.value
|
||||
total: workRecordSummary.value.total,
|
||||
succeeded: workRecordSummary.value.succeeded,
|
||||
failed: workRecordSummary.value.failed
|
||||
})
|
||||
} catch (error) {
|
||||
errorMessage.value = error?.message || '工作记录加载失败,请稍后重试。'
|
||||
@@ -646,19 +668,4 @@ onBeforeUnmount(() => {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* 风险环的成功、失败、执行中状态配色 */
|
||||
.json-risk-score-ring.succeeded {
|
||||
--score-ring: #16a34a;
|
||||
--score-ring-bg: #f0fdf4;
|
||||
}
|
||||
|
||||
.json-risk-score-ring.failed {
|
||||
--score-ring: #dc2626;
|
||||
--score-ring-bg: #fef2f2;
|
||||
}
|
||||
|
||||
.json-risk-score-ring.running {
|
||||
--score-ring: #2563eb;
|
||||
--score-ring-bg: #eff6ff;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -81,21 +81,35 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="isRequestDetail">
|
||||
<div class="detail-alert-strip">
|
||||
<span
|
||||
v-for="alert in detailAlerts"
|
||||
:key="alert.label"
|
||||
class="detail-alert-pill"
|
||||
:class="alert.tone"
|
||||
>
|
||||
<i :class="alert.icon || 'mdi mdi-alert-circle-outline'"></i>
|
||||
<span>{{ alert.label }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<template v-else-if="isRequestDetail">
|
||||
<div class="detail-topbar-actions">
|
||||
<div v-if="detailKpis.length" class="kpi-chips detail-kpi-chips">
|
||||
<div
|
||||
v-for="kpi in detailKpis"
|
||||
:key="kpi.label"
|
||||
class="kpi-chip detail-kpi-chip"
|
||||
:style="{ '--chip-color': kpi.color }"
|
||||
>
|
||||
<span class="chip-value">{{ kpi.value }}<small>{{ kpi.unit }}</small></span>
|
||||
<span class="chip-label">{{ kpi.label }}</span>
|
||||
<span class="chip-delta" :class="kpi.trend">{{ kpi.meta }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="detailAlerts.length" class="detail-alert-strip">
|
||||
<span
|
||||
v-for="alert in detailAlerts"
|
||||
:key="alert.label"
|
||||
class="detail-alert-pill"
|
||||
:class="alert.tone"
|
||||
>
|
||||
<i :class="alert.icon || 'mdi mdi-alert-circle-outline'"></i>
|
||||
<span>{{ alert.label }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="isWorkbench">
|
||||
<div class="topbar-toolset" aria-label="工作台快捷工具">
|
||||
@@ -243,10 +257,14 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
detailAlerts: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
detailAlerts: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
detailKpis: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
customRange: {
|
||||
type: Object,
|
||||
default: () => ({ start: '2024-07-06', end: '2024-07-12' })
|
||||
@@ -265,7 +283,7 @@ const emit = defineEmits([
|
||||
const isChat = computed(() => props.activeView === 'chat')
|
||||
const isOverview = computed(() => props.activeView === 'overview')
|
||||
const isWorkbench = computed(() => props.activeView === 'workbench')
|
||||
const isRequestDetail = computed(() => ['requests', 'documents'].includes(props.activeView) && props.detailMode)
|
||||
const isRequestDetail = computed(() => ['requests', 'documents', 'audit', 'digitalEmployees'].includes(props.activeView) && props.detailMode)
|
||||
const isDocuments = computed(() => props.activeView === 'documents' && !props.detailMode)
|
||||
const isRequests = computed(() => props.activeView === 'requests')
|
||||
const isLogs = computed(() => props.activeView === 'logs' && !props.logDetailMode)
|
||||
|
||||
@@ -55,8 +55,8 @@
|
||||
}"
|
||||
>
|
||||
<TopBar
|
||||
v-if="activeView !== 'settings' && !(activeView === 'audit' && auditDetailOpen) && !(activeView === 'digitalEmployees' && digitalEmployeeDetailOpen)"
|
||||
:current-view="topBarView"
|
||||
v-if="activeView !== 'settings'"
|
||||
:current-view="resolvedTopBarView"
|
||||
:search="search"
|
||||
:active-view="activeView"
|
||||
:ranges="ranges"
|
||||
@@ -68,9 +68,10 @@
|
||||
:document-summary="documentSummary"
|
||||
:digital-employee-summary="digitalEmployeeSummary"
|
||||
:company-name="ENTERPRISE_DISPLAY_NAME"
|
||||
:detail-mode="detailMode"
|
||||
:detail-mode="resolvedDetailMode"
|
||||
:log-detail-mode="logDetailMode"
|
||||
:detail-alerts="detailAlerts"
|
||||
:detail-alerts="resolvedDetailAlerts"
|
||||
:detail-kpis="resolvedDetailKpis"
|
||||
:custom-range="customRange"
|
||||
@update:search="search = $event"
|
||||
@update:active-range="activeRange = $event"
|
||||
@@ -145,11 +146,16 @@
|
||||
@open-assistant="openSmartEntry"
|
||||
/>
|
||||
<PoliciesView v-else-if="activeView === 'policies'" @summary-change="knowledgeSummary = $event" />
|
||||
<AuditView v-else-if="activeView === 'audit'" @detail-open-change="auditDetailOpen = $event" />
|
||||
<AuditView
|
||||
v-else-if="activeView === 'audit'"
|
||||
@detail-open-change="auditDetailOpen = $event"
|
||||
@detail-topbar-change="detailTopBarPayload = $event"
|
||||
/>
|
||||
<DigitalEmployeesView
|
||||
v-else-if="activeView === 'digitalEmployees'"
|
||||
@summary-change="digitalEmployeeSummary = $event"
|
||||
@detail-open-change="digitalEmployeeDetailOpen = $event"
|
||||
@detail-topbar-change="detailTopBarPayload = $event"
|
||||
/>
|
||||
<LogDetailView v-else-if="activeView === 'logs' && logDetailMode" />
|
||||
<LogsView v-else-if="activeView === 'logs'" @summary-change="logsSummary = $event" />
|
||||
@@ -175,35 +181,37 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { computed, defineAsyncComponent, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
|
||||
import SidebarRail from '../components/layout/SidebarRail.vue'
|
||||
import TopBar from '../components/layout/TopBar.vue'
|
||||
import FilterBar from '../components/layout/FilterBar.vue'
|
||||
import OverviewView from './OverviewView.vue'
|
||||
import PersonalWorkbenchView from './PersonalWorkbenchView.vue'
|
||||
import TravelReimbursementCreateView from './TravelReimbursementCreateView.vue'
|
||||
import TravelRequestDetailView from './TravelRequestDetailView.vue'
|
||||
import DocumentsCenterView from './DocumentsCenterView.vue'
|
||||
import BudgetCenterView from './BudgetCenterView.vue'
|
||||
import PoliciesView from './PoliciesView.vue'
|
||||
import AuditView from './AuditView.vue'
|
||||
import DigitalEmployeesView from './DigitalEmployeesView.vue'
|
||||
import LogsView from './LogsView.vue'
|
||||
import LogDetailView from './LogDetailView.vue'
|
||||
import EmployeeManagementView from './EmployeeManagementView.vue'
|
||||
import SettingsView from './SettingsView.vue'
|
||||
|
||||
import { useAppShell } from '../composables/useAppShell.js'
|
||||
import { useSystemState } from '../composables/useSystemState.js'
|
||||
import { filterNavItemsByAccess } from '../utils/accessControl.js'
|
||||
import { consumeLoginEntryTransition } from '../utils/loginEntryTransition.js'
|
||||
|
||||
const OverviewView = defineAsyncComponent(() => import('./OverviewView.vue'))
|
||||
const PersonalWorkbenchView = defineAsyncComponent(() => import('./PersonalWorkbenchView.vue'))
|
||||
const TravelReimbursementCreateView = defineAsyncComponent(() => import('./TravelReimbursementCreateView.vue'))
|
||||
const TravelRequestDetailView = defineAsyncComponent(() => import('./TravelRequestDetailView.vue'))
|
||||
const DocumentsCenterView = defineAsyncComponent(() => import('./DocumentsCenterView.vue'))
|
||||
const BudgetCenterView = defineAsyncComponent(() => import('./BudgetCenterView.vue'))
|
||||
const PoliciesView = defineAsyncComponent(() => import('./PoliciesView.vue'))
|
||||
const AuditView = defineAsyncComponent(() => import('./AuditView.vue'))
|
||||
const DigitalEmployeesView = defineAsyncComponent(() => import('./DigitalEmployeesView.vue'))
|
||||
const LogsView = defineAsyncComponent(() => import('./LogsView.vue'))
|
||||
const LogDetailView = defineAsyncComponent(() => import('./LogDetailView.vue'))
|
||||
const EmployeeManagementView = defineAsyncComponent(() => import('./EmployeeManagementView.vue'))
|
||||
const SettingsView = defineAsyncComponent(() => import('./SettingsView.vue'))
|
||||
|
||||
const employeeSummary = ref(null)
|
||||
const knowledgeSummary = ref(null)
|
||||
const logsSummary = ref(null)
|
||||
const documentSummary = ref(null)
|
||||
const digitalEmployeeSummary = ref(null)
|
||||
const detailTopBarPayload = ref(null)
|
||||
const auditDetailOpen = ref(false)
|
||||
const digitalEmployeeDetailOpen = ref(false)
|
||||
const loginEntryAnimating = ref(false)
|
||||
@@ -282,6 +290,37 @@ const { companyProfile, currentUser, logout } = useSystemState()
|
||||
const PRODUCT_DISPLAY_NAME = '易财费控'
|
||||
const ENTERPRISE_DISPLAY_NAME = '远光软件股份有限公司'
|
||||
const filteredNavItems = computed(() => filterNavItemsByAccess(navItems, currentUser.value))
|
||||
const DETAIL_TOPBAR_FALLBACKS = {
|
||||
audit: {
|
||||
title: '规则中心详情',
|
||||
desc: '查看规则配置、版本审核、测试结果与上线状态。'
|
||||
},
|
||||
digitalEmployees: {
|
||||
title: '数字员工详情',
|
||||
desc: '查看数字员工配置、执行计划、运行记录与源文件。'
|
||||
}
|
||||
}
|
||||
const customDetailTopBarActive = computed(() => (
|
||||
(activeView.value === 'audit' && auditDetailOpen.value) ||
|
||||
(activeView.value === 'digitalEmployees' && digitalEmployeeDetailOpen.value)
|
||||
))
|
||||
const resolvedTopBarView = computed(() => (
|
||||
customDetailTopBarActive.value
|
||||
? detailTopBarPayload.value?.view || DETAIL_TOPBAR_FALLBACKS[activeView.value] || topBarView.value
|
||||
: topBarView.value
|
||||
))
|
||||
const resolvedDetailMode = computed(() => (
|
||||
detailMode.value ||
|
||||
customDetailTopBarActive.value
|
||||
))
|
||||
const resolvedDetailAlerts = computed(() => (
|
||||
customDetailTopBarActive.value
|
||||
? detailTopBarPayload.value?.alerts || []
|
||||
: detailAlerts.value
|
||||
))
|
||||
const resolvedDetailKpis = computed(() => (
|
||||
customDetailTopBarActive.value ? detailTopBarPayload.value?.kpis || [] : []
|
||||
))
|
||||
|
||||
function handleLogout() {
|
||||
logout('manual')
|
||||
|
||||
@@ -11,67 +11,6 @@
|
||||
}"
|
||||
>
|
||||
<div class="detail-scroll">
|
||||
<section
|
||||
v-if="!selectedSkill.usesSpreadsheetRule && !selectedSkill.usesJsonRiskRule"
|
||||
class="detail-hero panel asset-detail-topbar list-toolbar"
|
||||
>
|
||||
<div class="hero-title asset-detail-topbar-main filter-set">
|
||||
<div class="skill-badge" :class="selectedSkill.badgeTone">{{ selectedSkill.typeLabel }}</div>
|
||||
<h2>{{ selectedSkill.name }}</h2>
|
||||
<p>{{ selectedSkill.summary || '当前资产尚未补充说明。' }}</p>
|
||||
|
||||
<div class="hero-review-meta">
|
||||
<span>
|
||||
<i class="mdi mdi-code-tags"></i>
|
||||
{{ selectedSkill.code }}
|
||||
</span>
|
||||
<span>
|
||||
<i class="mdi mdi-account-outline"></i>
|
||||
负责人:{{ selectedSkill.owner }}
|
||||
</span>
|
||||
<span>
|
||||
<i class="mdi mdi-account-check-outline"></i>
|
||||
审核人:{{ selectedSkill.reviewer }}
|
||||
</span>
|
||||
<b :class="['status-pill', selectedSkill.statusTone]">{{ selectedSkill.status }}</b>
|
||||
<b
|
||||
v-if="selectedSkillIsRule"
|
||||
:class="['status-pill', selectedSkill.reviewStatusTone]"
|
||||
>
|
||||
{{ selectedSkill.reviewStatusLabel }}
|
||||
</b>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedSkillIsRule" class="review-note-block">
|
||||
<strong>上线约束</strong>
|
||||
<p>{{ activateBlockedReason || '当前规则版本审核通过后可正式上线。' }}</p>
|
||||
<span v-if="showReviewNote">
|
||||
审核时间:{{ selectedSkill.reviewTimeLabel }}
|
||||
<template v-if="selectedSkill.reviewNote"> · 审核意见:{{ selectedSkill.reviewNote }}</template>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hero-stats asset-detail-topbar-meta toolbar-actions">
|
||||
<div class="hero-stat">
|
||||
<span>资产编码</span>
|
||||
<strong>{{ selectedSkill.code }}</strong>
|
||||
</div>
|
||||
<div class="hero-stat">
|
||||
<span>业务域</span>
|
||||
<strong>{{ selectedSkill.category }}</strong>
|
||||
</div>
|
||||
<div class="hero-stat">
|
||||
<span>{{ selectedSkillIsRule ? '当前展示版本' : '当前版本' }}</span>
|
||||
<strong>{{ selectedSkill.displayVersion || selectedSkill.version }}</strong>
|
||||
</div>
|
||||
<div class="hero-stat">
|
||||
<span>最近更新</span>
|
||||
<strong>{{ selectedSkill.updatedAt }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="detailError" class="detail-inline-state panel error">
|
||||
<i class="mdi mdi-alert-circle-outline"></i>
|
||||
<div>
|
||||
@@ -94,7 +33,6 @@
|
||||
v-else-if="selectedSkill.usesSpreadsheetRule"
|
||||
ref="spreadsheetUploadInput"
|
||||
:selected-skill="selectedSkill"
|
||||
:selected-spreadsheet-mode-label="selectedSpreadsheetModeLabel"
|
||||
:selected-spreadsheet-file-name="selectedSpreadsheetFileName"
|
||||
:selected-spreadsheet-change-records="selectedSpreadsheetChangeRecords"
|
||||
:spreadsheet-only-office-host-id="spreadsheetOnlyOfficeHostId"
|
||||
|
||||
@@ -267,6 +267,7 @@
|
||||
class="digital-work-records-section"
|
||||
@summary-change="emit('summary-change', $event)"
|
||||
@detail-open-change="workRecordDetailOpen = $event"
|
||||
@detail-topbar-change="workRecordDetailTopBar = $event"
|
||||
/>
|
||||
</article>
|
||||
</Transition>
|
||||
@@ -307,8 +308,6 @@ import { runOrchestrator } from '../services/orchestrator.js'
|
||||
import { isPlatformAdminUser } from '../utils/accessControl.js'
|
||||
import {
|
||||
DIGITAL_EMPLOYEE_SKILL_CATEGORY_OPTIONS,
|
||||
buildDigitalEmployeeDetailMeta,
|
||||
buildDigitalEmployeeListMeta,
|
||||
formatDigitalEmployeeCron,
|
||||
isDigitalEmployeeAsset
|
||||
} from './scripts/auditViewDigitalEmployeeModel.js'
|
||||
@@ -319,28 +318,55 @@ import {
|
||||
resolveDigitalEmployeeScheduleValue
|
||||
} from './scripts/digitalEmployeeScheduleModel.js'
|
||||
import { incrementVersion } from './scripts/auditViewRuntimeModel.js'
|
||||
import {
|
||||
DIGITAL_EMPLOYEE_EXECUTION_MODE_OPTIONS,
|
||||
buildDigitalEmployeeDetailTopBar,
|
||||
buildEmployeeDetail,
|
||||
buildEmployeeListItem,
|
||||
buildEmployeePlaceholder,
|
||||
filterDigitalEmployees,
|
||||
sortEmployees
|
||||
} from './scripts/digitalEmployeesViewModel.js'
|
||||
import {
|
||||
ENABLED_STATE_OPTIONS,
|
||||
formatDateTime,
|
||||
normalizeText,
|
||||
resolveStatusMeta,
|
||||
STATUS_OPTIONS
|
||||
} from './scripts/auditViewModel.js'
|
||||
|
||||
const { currentUser } = useSystemState()
|
||||
const { toast } = useToast()
|
||||
const emit = defineEmits(['summary-change', 'detail-open-change'])
|
||||
const emit = defineEmits(['summary-change', 'detail-open-change', 'detail-topbar-change'])
|
||||
|
||||
const employees = ref([])
|
||||
const selectedEmployee = ref(null)
|
||||
const selectedEmployeeId = ref('')
|
||||
const activeSection = ref('skills')
|
||||
const workRecordDetailOpen = ref(false)
|
||||
const workRecordDetailTopBar = ref(null)
|
||||
const isDetailOpen = computed(() => Boolean(selectedEmployee.value) || (activeSection.value === 'workRecords' && workRecordDetailOpen.value))
|
||||
const digitalEmployeeDetailTopBar = computed(() => {
|
||||
const employee = selectedEmployee.value
|
||||
if (employee) {
|
||||
return buildDigitalEmployeeDetailTopBar(employee)
|
||||
}
|
||||
|
||||
if (activeSection.value === 'workRecords' && workRecordDetailOpen.value) {
|
||||
return workRecordDetailTopBar.value
|
||||
}
|
||||
|
||||
return null
|
||||
})
|
||||
|
||||
watch(isDetailOpen, (newVal) => {
|
||||
emit('detail-open-change', newVal)
|
||||
}, { immediate: true })
|
||||
watch(
|
||||
digitalEmployeeDetailTopBar,
|
||||
(value) => {
|
||||
emit('detail-topbar-change', value)
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
)
|
||||
const keyword = ref('')
|
||||
const selectedStatus = ref('')
|
||||
const selectedEnabledState = ref('')
|
||||
@@ -365,11 +391,7 @@ const scheduleEditorBusy = computed(() => actionState.value === 'save-digital-sc
|
||||
|
||||
const statusOptions = STATUS_OPTIONS
|
||||
const enabledStateOptions = ENABLED_STATE_OPTIONS
|
||||
const executionModeOptions = [
|
||||
{ value: '', label: '全部执行方式' },
|
||||
{ value: 'timed', label: '定时执行' },
|
||||
{ value: 'manual', label: '手动触发' }
|
||||
]
|
||||
const executionModeOptions = DIGITAL_EMPLOYEE_EXECUTION_MODE_OPTIONS
|
||||
|
||||
const selectedStatusLabel = computed(() =>
|
||||
statusOptions.find((item) => item.value === selectedStatus.value)?.label || '全部状态'
|
||||
@@ -403,32 +425,11 @@ const schedulePreviewLabel = computed(() => {
|
||||
})
|
||||
|
||||
const visibleEmployees = computed(() => {
|
||||
const searchText = normalizeText(keyword.value).toLowerCase()
|
||||
return employees.value.filter((item) => {
|
||||
const matchesKeyword = searchText
|
||||
? [
|
||||
item.name,
|
||||
item.code,
|
||||
item.summary,
|
||||
item.owner,
|
||||
item.scope,
|
||||
item.executionMode,
|
||||
item.skillCategory,
|
||||
item.status,
|
||||
item.enabledLabel
|
||||
]
|
||||
.filter(Boolean)
|
||||
.some((value) => String(value).toLowerCase().includes(searchText))
|
||||
: true
|
||||
const matchesStatus = selectedStatus.value ? item.statusValue === selectedStatus.value : true
|
||||
const matchesEnabled = selectedEnabledState.value
|
||||
? (selectedEnabledState.value === 'enabled') === Boolean(item.isEnabledValue)
|
||||
: true
|
||||
const matchesExecutionMode = selectedExecutionMode.value
|
||||
? item.executionModeValue === selectedExecutionMode.value
|
||||
: true
|
||||
|
||||
return matchesKeyword && matchesStatus && matchesEnabled && matchesExecutionMode
|
||||
return filterDigitalEmployees(employees.value, {
|
||||
keyword: keyword.value,
|
||||
selectedEnabledState: selectedEnabledState.value,
|
||||
selectedExecutionMode: selectedExecutionMode.value,
|
||||
selectedStatus: selectedStatus.value
|
||||
})
|
||||
})
|
||||
|
||||
@@ -466,91 +467,6 @@ function resolveActor() {
|
||||
return normalizeText(user.name) || normalizeText(user.username) || 'system'
|
||||
}
|
||||
|
||||
function buildEmployeeListItem(asset) {
|
||||
const meta = buildDigitalEmployeeListMeta(asset)
|
||||
const statusMeta = resolveStatusMeta(asset.status)
|
||||
const displayName = meta.name || '数字员工技能'
|
||||
|
||||
return {
|
||||
id: asset.id,
|
||||
rawCode: asset.code,
|
||||
short: displayName.slice(0, 2),
|
||||
badgeTone: 'blue',
|
||||
name: displayName,
|
||||
code: meta.code,
|
||||
summary: meta.summary,
|
||||
owner: meta.owner,
|
||||
scope: meta.scope,
|
||||
executionMode: meta.executionMode,
|
||||
executionModeValue: meta.executionMode === '定时执行' ? 'timed' : 'manual',
|
||||
skillCategory: meta.skillCategory,
|
||||
version: asset.working_version || asset.current_version || '-',
|
||||
currentVersion: asset.current_version || '-',
|
||||
status: statusMeta.label,
|
||||
statusValue: asset.status,
|
||||
statusTone: statusMeta.tone,
|
||||
enabledLabel: meta.enabledLabel,
|
||||
enabledTone: meta.enabledTone,
|
||||
isEnabledValue: meta.enabled,
|
||||
configJson: asset.config_json || {},
|
||||
updatedAt: formatDateTime(asset.updated_at),
|
||||
updatedAtRaw: asset.updated_at || '',
|
||||
digitalEmployee: meta
|
||||
}
|
||||
}
|
||||
|
||||
function buildEmployeePlaceholder(employee) {
|
||||
return {
|
||||
...employee,
|
||||
type: 'digitalEmployees',
|
||||
typeLabel: '数字员工',
|
||||
currentVersion: employee.currentVersion || employee.version || '-',
|
||||
workingVersion: employee.version || '-',
|
||||
markdownContent: '',
|
||||
loading: true
|
||||
}
|
||||
}
|
||||
|
||||
function buildEmployeeDetail(asset) {
|
||||
const meta = buildDigitalEmployeeDetailMeta({
|
||||
...asset,
|
||||
updated_at: formatDateTime(asset.updated_at)
|
||||
})
|
||||
const statusMeta = resolveStatusMeta(asset.status)
|
||||
|
||||
return {
|
||||
id: asset.id,
|
||||
type: 'digitalEmployees',
|
||||
typeLabel: '数字员工',
|
||||
rawCode: asset.code,
|
||||
short: meta.name.slice(0, 2),
|
||||
name: meta.name,
|
||||
code: meta.code,
|
||||
summary: meta.description,
|
||||
owner: meta.owner,
|
||||
reviewer: meta.reviewer,
|
||||
category: meta.category,
|
||||
scope: meta.scope,
|
||||
version: asset.working_version || asset.current_version || '-',
|
||||
currentVersion: asset.current_version || '-',
|
||||
workingVersion: asset.working_version || asset.current_version || '-',
|
||||
status: statusMeta.label,
|
||||
statusValue: asset.status,
|
||||
statusTone: statusMeta.tone,
|
||||
configJson: asset.config_json || {},
|
||||
updatedAt: formatDateTime(asset.updated_at),
|
||||
markdownContent: meta.sourceMarkdown,
|
||||
digitalEmployee: meta,
|
||||
loading: false
|
||||
}
|
||||
}
|
||||
|
||||
function sortEmployees(items) {
|
||||
return [...items].sort((left, right) =>
|
||||
String(right.updatedAtRaw || '').localeCompare(String(left.updatedAtRaw || ''))
|
||||
)
|
||||
}
|
||||
|
||||
async function loadEmployees() {
|
||||
loading.value = true
|
||||
errorMessage.value = ''
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
</article>
|
||||
|
||||
<template v-else-if="isHermes && hermesRun">
|
||||
<article v-if="!isKnowledgeIngestRunDetail" class="detail-hero panel">
|
||||
<article class="detail-hero panel">
|
||||
<div class="hero-copy">
|
||||
<div class="hero-tags">
|
||||
<span class="level-pill" :class="resolveLevelTone(resolveRunLevel(hermesRun))">
|
||||
@@ -43,12 +43,7 @@
|
||||
{{ hermesRunAlert.message }}
|
||||
</article>
|
||||
|
||||
<KnowledgeIngestRunPanel
|
||||
v-if="isKnowledgeIngestRunDetail"
|
||||
:run="hermesRun"
|
||||
/>
|
||||
|
||||
<div v-if="!isKnowledgeIngestRunDetail" class="detail-grid">
|
||||
<div class="detail-grid">
|
||||
<article class="panel detail-card wide">
|
||||
<div class="card-head">
|
||||
<h3>基本信息</h3>
|
||||
@@ -68,7 +63,7 @@
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article v-if="!isKnowledgeIngestRunDetail" class="panel detail-card">
|
||||
<article class="panel detail-card">
|
||||
<div class="card-head">
|
||||
<h3>处理链路</h3>
|
||||
<p>按工具调用顺序查看执行链。</p>
|
||||
@@ -97,7 +92,7 @@
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article v-if="selectedToolCall && !isKnowledgeIngestRunDetail" class="panel detail-card">
|
||||
<article v-if="selectedToolCall" class="panel detail-card">
|
||||
<div class="card-head">
|
||||
<h3>当前 ToolCall</h3>
|
||||
<p>查看当前工具调用的请求与返回。</p>
|
||||
@@ -199,7 +194,6 @@
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import KnowledgeIngestRunPanel from '../components/logs/KnowledgeIngestRunPanel.vue'
|
||||
import { fetchAgentRunDetail } from '../services/agentAssets.js'
|
||||
import { fetchSystemLogEntry } from '../services/systemLogs.js'
|
||||
import {
|
||||
@@ -210,7 +204,6 @@ import {
|
||||
resolveAgentRunHeartbeat,
|
||||
resolveAgentRunStatus
|
||||
} from '../utils/agentRunMonitor.js'
|
||||
import { isKnowledgeIngestRun } from '../utils/knowledgeIngestLogModel.js'
|
||||
|
||||
const SOURCE_LABELS = {
|
||||
schedule: '定时任务',
|
||||
@@ -230,7 +223,6 @@ let pollTimer = 0
|
||||
|
||||
const isHermes = computed(() => route.params.logKind === 'hermes')
|
||||
const isSystem = computed(() => route.params.logKind === 'system')
|
||||
const isKnowledgeIngestRunDetail = computed(() => isKnowledgeIngestRun(hermesRun.value))
|
||||
const selectedToolCall = computed(() =>
|
||||
(hermesRun.value?.tool_calls || []).find((item) => item.id === selectedToolCallId.value) || null
|
||||
)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
57
web/src/views/scripts/auditViewDetailTopBar.js
Normal file
57
web/src/views/scripts/auditViewDetailTopBar.js
Normal file
@@ -0,0 +1,57 @@
|
||||
import { normalizeText } from './auditViewModel.js'
|
||||
|
||||
function resolveRiskScoreCardColor(level) {
|
||||
const normalized = normalizeText(level).toLowerCase()
|
||||
if (['critical', 'high'].includes(normalized)) return '#ef4444'
|
||||
if (['medium', 'warning'].includes(normalized)) return '#f59e0b'
|
||||
if (['low', 'success'].includes(normalized)) return 'var(--success)'
|
||||
return 'var(--theme-primary)'
|
||||
}
|
||||
|
||||
export function buildAuditDetailTopBar({
|
||||
skill,
|
||||
usesJsonRiskRule = false,
|
||||
usesSpreadsheetRule = false,
|
||||
spreadsheetModeLabel = '',
|
||||
spreadsheetFileName = '',
|
||||
canEditSpreadsheetInline = false
|
||||
} = {}) {
|
||||
if (!skill) return null
|
||||
|
||||
const title = normalizeText(skill.name) || '规则中心详情'
|
||||
const desc =
|
||||
normalizeText(skill.riskRuleSubtitle) ||
|
||||
normalizeText(skill.summary) ||
|
||||
normalizeText(skill.configDesc) ||
|
||||
'查看规则配置、版本审核、测试结果与上线状态。'
|
||||
const kpis = []
|
||||
|
||||
if (usesJsonRiskRule) {
|
||||
const scoreLevel = skill.riskRuleScoreLevel || skill.riskRuleSeverity
|
||||
const score = skill.riskRuleScore ?? '--'
|
||||
kpis.push({
|
||||
label: '风险分',
|
||||
value: String(score),
|
||||
unit: score === '--' ? '' : '分',
|
||||
meta: normalizeText(skill.riskRuleScoreLabel || skill.riskRuleSeverityLabel) || '待评估',
|
||||
trend: ['critical', 'high', 'medium', 'warning'].includes(normalizeText(scoreLevel).toLowerCase())
|
||||
? 'down'
|
||||
: 'up',
|
||||
color: resolveRiskScoreCardColor(scoreLevel)
|
||||
})
|
||||
} else if (usesSpreadsheetRule) {
|
||||
kpis.push({
|
||||
label: '编辑模式',
|
||||
value: spreadsheetModeLabel,
|
||||
unit: '',
|
||||
meta: spreadsheetFileName,
|
||||
trend: canEditSpreadsheetInline ? 'up' : 'down',
|
||||
color: canEditSpreadsheetInline ? 'var(--success)' : '#64748b'
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
view: { title, desc },
|
||||
kpis
|
||||
}
|
||||
}
|
||||
235
web/src/views/scripts/auditViewListFilters.js
Normal file
235
web/src/views/scripts/auditViewListFilters.js
Normal file
@@ -0,0 +1,235 @@
|
||||
import { computed } from 'vue'
|
||||
|
||||
import {
|
||||
ENABLED_STATE_OPTIONS,
|
||||
ONLINE_STATE_OPTIONS,
|
||||
RISK_SCENARIO_OPTIONS,
|
||||
STATUS_OPTIONS
|
||||
} from './auditViewMetadata.js'
|
||||
import { RISK_RULE_LEVEL_OPTIONS } from './auditViewRiskRuleModel.js'
|
||||
import {
|
||||
normalizeText,
|
||||
resolveDomainLabel,
|
||||
resolveStatusMeta
|
||||
} from './auditViewModel.js'
|
||||
import { filterAuditAssets } from './auditViewRuntimeModel.js'
|
||||
|
||||
function buildOptions(items, valueGetter, labelGetter, defaultLabel) {
|
||||
const values = []
|
||||
const seen = new Set()
|
||||
|
||||
items.forEach((item) => {
|
||||
const value = normalizeText(valueGetter(item))
|
||||
if (!value || seen.has(value)) {
|
||||
return
|
||||
}
|
||||
seen.add(value)
|
||||
values.push(value)
|
||||
})
|
||||
|
||||
return [
|
||||
{ value: '', label: defaultLabel },
|
||||
...values.map((value) => ({
|
||||
value,
|
||||
label: labelGetter(value)
|
||||
}))
|
||||
]
|
||||
}
|
||||
|
||||
export function useAuditListFilters({
|
||||
activeType,
|
||||
activeTabLabel,
|
||||
currentAssets,
|
||||
keyword,
|
||||
selectedDomain,
|
||||
selectedOwner,
|
||||
selectedRiskLevel,
|
||||
selectedStatus,
|
||||
selectedRiskScenario,
|
||||
selectedOnlineState,
|
||||
selectedEnabledState
|
||||
}) {
|
||||
const showRiskScenarioFilter = computed(() =>
|
||||
['financialRules', 'riskRules'].includes(activeType.value)
|
||||
)
|
||||
const showOwnerFilter = computed(() => activeType.value !== 'riskRules')
|
||||
const showRiskLevelFilter = computed(() => activeType.value === 'riskRules')
|
||||
const showStatusFilter = computed(() => true)
|
||||
const showOnlineFilter = computed(() => false)
|
||||
const showEnabledFilter = computed(() => false)
|
||||
|
||||
const domainOptions = computed(() =>
|
||||
buildOptions(
|
||||
currentAssets.value,
|
||||
(item) => item.domainValue,
|
||||
(value) => resolveDomainLabel(value),
|
||||
'全部业务域'
|
||||
)
|
||||
)
|
||||
const ownerOptions = computed(() =>
|
||||
buildOptions(
|
||||
currentAssets.value,
|
||||
(item) => item.owner,
|
||||
(value) => value,
|
||||
'全部负责人'
|
||||
)
|
||||
)
|
||||
const riskLevelOptions = computed(() => [
|
||||
{ value: '', label: '全部风险等级' },
|
||||
...RISK_RULE_LEVEL_OPTIONS
|
||||
])
|
||||
|
||||
const selectedDomainLabel = computed(
|
||||
() => domainOptions.value.find((item) => item.value === selectedDomain.value)?.label || '业务域'
|
||||
)
|
||||
const selectedOwnerLabel = computed(
|
||||
() =>
|
||||
ownerOptions.value.find((item) => item.value === selectedOwner.value)?.label ||
|
||||
'负责人'
|
||||
)
|
||||
const selectedRiskLevelLabel = computed(
|
||||
() =>
|
||||
riskLevelOptions.value.find((item) => item.value === selectedRiskLevel.value)?.label ||
|
||||
'风险等级'
|
||||
)
|
||||
const selectedStatusLabel = computed(
|
||||
() => STATUS_OPTIONS.find((item) => item.value === selectedStatus.value)?.label || '状态'
|
||||
)
|
||||
const selectedRiskScenarioLabel = computed(
|
||||
() =>
|
||||
RISK_SCENARIO_OPTIONS.find((item) => item.value === selectedRiskScenario.value)?.label ||
|
||||
'使用场景'
|
||||
)
|
||||
const selectedOnlineStateLabel = computed(
|
||||
() =>
|
||||
ONLINE_STATE_OPTIONS.find((item) => item.value === selectedOnlineState.value)?.label ||
|
||||
'是否上线'
|
||||
)
|
||||
const selectedEnabledStateLabel = computed(
|
||||
() =>
|
||||
ENABLED_STATE_OPTIONS.find((item) => item.value === selectedEnabledState.value)?.label ||
|
||||
'是否启用'
|
||||
)
|
||||
|
||||
const activeFilterTokens = computed(() => {
|
||||
const tokens = []
|
||||
|
||||
if (selectedDomain.value) {
|
||||
tokens.push(`业务域:${resolveDomainLabel(selectedDomain.value)}`)
|
||||
}
|
||||
if (showRiskScenarioFilter.value && selectedRiskScenario.value) {
|
||||
tokens.push(`使用场景:${selectedRiskScenario.value}`)
|
||||
}
|
||||
if (showStatusFilter.value && selectedStatus.value) {
|
||||
tokens.push(`状态:${resolveStatusMeta(selectedStatus.value).label}`)
|
||||
}
|
||||
if (showOnlineFilter.value && selectedOnlineState.value) {
|
||||
tokens.push(`是否上线:${selectedOnlineStateLabel.value}`)
|
||||
}
|
||||
if (showEnabledFilter.value && selectedEnabledState.value) {
|
||||
tokens.push(`是否启用:${selectedEnabledStateLabel.value}`)
|
||||
}
|
||||
if (showOwnerFilter.value && selectedOwner.value) {
|
||||
tokens.push(`负责人:${selectedOwner.value}`)
|
||||
}
|
||||
if (showRiskLevelFilter.value && selectedRiskLevel.value) {
|
||||
tokens.push(`风险等级:${selectedRiskLevelLabel.value}`)
|
||||
}
|
||||
if (keyword.value.trim()) {
|
||||
tokens.push(`搜索:${keyword.value.trim()}`)
|
||||
}
|
||||
|
||||
return tokens
|
||||
})
|
||||
|
||||
const visibleSkills = computed(() =>
|
||||
filterAuditAssets(currentAssets.value, {
|
||||
keyword: keyword.value,
|
||||
selectedDomain: selectedDomain.value,
|
||||
selectedOwner: selectedOwner.value,
|
||||
selectedRiskLevel: selectedRiskLevel.value,
|
||||
selectedStatus: selectedStatus.value,
|
||||
selectedRiskScenario: selectedRiskScenario.value,
|
||||
selectedOnlineState: selectedOnlineState.value,
|
||||
selectedEnabledState: selectedEnabledState.value,
|
||||
showStatusFilter: showStatusFilter.value,
|
||||
showRiskScenarioFilter: showRiskScenarioFilter.value,
|
||||
showOnlineFilter: showOnlineFilter.value,
|
||||
showEnabledFilter: showEnabledFilter.value
|
||||
})
|
||||
)
|
||||
|
||||
const auditEmptyState = computed(() => {
|
||||
const hasFilters = activeFilterTokens.value.length > 0
|
||||
const supportedFilters = [
|
||||
'业务域',
|
||||
...(showOwnerFilter.value ? ['负责人'] : []),
|
||||
...(showRiskLevelFilter.value ? ['风险等级'] : []),
|
||||
...(showRiskScenarioFilter.value ? ['使用场景'] : []),
|
||||
...(showStatusFilter.value ? ['状态'] : []),
|
||||
...(showOnlineFilter.value ? ['是否上线'] : []),
|
||||
...(showEnabledFilter.value ? ['是否启用'] : []),
|
||||
'关键词'
|
||||
]
|
||||
|
||||
if (!currentAssets.value.length) {
|
||||
return {
|
||||
eyebrow: `${activeTabLabel.value}资产`,
|
||||
title: `${activeTabLabel.value}列表暂时还是空的`,
|
||||
desc: `当前环境里还没有可展示的${activeTabLabel.value}资产。完成接入或同步后,会统一展示在这里。`,
|
||||
icon: 'mdi mdi-database-search-outline',
|
||||
actionLabel: '',
|
||||
actionIcon: '',
|
||||
tone: 'amber',
|
||||
artLabel: 'ASSET',
|
||||
tips: [
|
||||
'切换页签可查看其他资产类型',
|
||||
`支持按${supportedFilters.slice(0, -1).join('、')}和关键词做过滤`
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
eyebrow: '筛选结果为空',
|
||||
title: `没有找到匹配的${activeTabLabel.value}`,
|
||||
desc: hasFilters
|
||||
? `试试清空${supportedFilters.join('、')}筛选,再重新查看。`
|
||||
: `当前列表中还没有满足展示条件的${activeTabLabel.value}资产。`,
|
||||
icon: hasFilters ? 'mdi mdi-tune-variant' : 'mdi mdi-view-grid-outline',
|
||||
actionLabel: hasFilters ? '清空筛选' : '',
|
||||
actionIcon: hasFilters ? 'mdi mdi-filter-remove-outline' : '',
|
||||
tone: hasFilters ? 'primary' : 'slate',
|
||||
artLabel: hasFilters ? 'FILTER' : 'QUEUE',
|
||||
tips: hasFilters
|
||||
? [
|
||||
`${supportedFilters.join('、')}会叠加过滤`,
|
||||
showRiskScenarioFilter.value
|
||||
? '可以换个规则名称或场景分类继续搜索'
|
||||
: '可以换个编码、名称或负责人关键词继续搜索'
|
||||
]
|
||||
: ['列表展示来自真实资产 API', '切换资产类型后会自动重新拉取数据']
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
activeFilterTokens,
|
||||
auditEmptyState,
|
||||
domainOptions,
|
||||
ownerOptions,
|
||||
riskLevelOptions,
|
||||
selectedDomainLabel,
|
||||
selectedEnabledStateLabel,
|
||||
selectedOnlineStateLabel,
|
||||
selectedOwnerLabel,
|
||||
selectedRiskLevelLabel,
|
||||
selectedRiskScenarioLabel,
|
||||
selectedStatusLabel,
|
||||
showEnabledFilter,
|
||||
showOnlineFilter,
|
||||
showOwnerFilter,
|
||||
showRiskLevelFilter,
|
||||
showRiskScenarioFilter,
|
||||
showStatusFilter,
|
||||
visibleSkills
|
||||
}
|
||||
}
|
||||
@@ -1043,6 +1043,17 @@ export function buildListItem(asset) {
|
||||
const displayEnabledValue = isEnabledValue
|
||||
const displayEnabledLabel = isEnabledValue ? '是' : '否'
|
||||
const displayEnabledTone = isEnabledValue ? 'success' : 'disabled'
|
||||
const searchText = [
|
||||
displayName,
|
||||
displayCode,
|
||||
displaySummary,
|
||||
displayOwner,
|
||||
displayScope,
|
||||
riskLevelLabel
|
||||
]
|
||||
.map((value) => normalizeText(value).toLowerCase())
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
|
||||
return {
|
||||
id: asset.id,
|
||||
@@ -1092,7 +1103,8 @@ export function buildListItem(asset) {
|
||||
changeCount,
|
||||
updatedAt: isRiskRule ? riskRuleCreatedAt : formatDateTime(asset.updated_at),
|
||||
badgeTone: tabMeta.badgeTone,
|
||||
domainValue: asset.domain
|
||||
domainValue: asset.domain,
|
||||
searchText
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -84,50 +84,53 @@ export function buildSpreadsheetChangeRecordKey(records = []) {
|
||||
|
||||
export function filterAuditAssets(assets = [], filters = {}) {
|
||||
const normalizedKeyword = normalizeText(filters.keyword).toLowerCase()
|
||||
const hasKeyword = Boolean(normalizedKeyword)
|
||||
const hasDomain = Boolean(filters.selectedDomain)
|
||||
const hasOwner = Boolean(filters.selectedOwner)
|
||||
const hasRiskLevel = Boolean(filters.selectedRiskLevel)
|
||||
const hasStatus = Boolean(filters.showStatusFilter && filters.selectedStatus)
|
||||
const hasRiskScenario = Boolean(filters.showRiskScenarioFilter && filters.selectedRiskScenario)
|
||||
const hasOnline = Boolean(filters.showOnlineFilter && filters.selectedOnlineState)
|
||||
const hasEnabled = Boolean(filters.showEnabledFilter && filters.selectedEnabledState)
|
||||
|
||||
return assets.filter((item) => {
|
||||
const matchesKeyword = normalizedKeyword
|
||||
? [item.name, item.code, item.summary, item.owner, item.scope, item.riskLevelLabel]
|
||||
.filter(Boolean)
|
||||
.some((value) => String(value).toLowerCase().includes(normalizedKeyword))
|
||||
: true
|
||||
const matchesDomain = filters.selectedDomain ? item.domainValue === filters.selectedDomain : true
|
||||
const matchesOwner = filters.selectedOwner ? item.owner === filters.selectedOwner : true
|
||||
const matchesRiskLevel = filters.selectedRiskLevel
|
||||
? item.riskLevelValue === filters.selectedRiskLevel
|
||||
: true
|
||||
const matchesStatus = filters.showStatusFilter
|
||||
? filters.selectedStatus
|
||||
? item.statusValue === filters.selectedStatus
|
||||
: true
|
||||
: true
|
||||
const matchesRiskScenario = filters.showRiskScenarioFilter
|
||||
? filters.selectedRiskScenario
|
||||
? Array.isArray(item.scenarioList) && item.scenarioList.length
|
||||
? item.scenarioList.includes(filters.selectedRiskScenario)
|
||||
: item.riskCategory === filters.selectedRiskScenario
|
||||
: true
|
||||
: true
|
||||
const matchesOnline = filters.showOnlineFilter
|
||||
? filters.selectedOnlineState
|
||||
? (filters.selectedOnlineState === 'online') === Boolean(item.isOnlineValue)
|
||||
: true
|
||||
: true
|
||||
const matchesEnabled = filters.showEnabledFilter
|
||||
? filters.selectedEnabledState
|
||||
? (filters.selectedEnabledState === 'enabled') === Boolean(item.isEnabledValue)
|
||||
: true
|
||||
: true
|
||||
if (hasKeyword) {
|
||||
const searchText = item.searchText || [item.name, item.code, item.summary, item.owner, item.scope, item.riskLevelLabel]
|
||||
.map((value) => normalizeText(value).toLowerCase())
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
if (!searchText.includes(normalizedKeyword)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if (hasDomain && item.domainValue !== filters.selectedDomain) {
|
||||
return false
|
||||
}
|
||||
if (hasOwner && item.owner !== filters.selectedOwner) {
|
||||
return false
|
||||
}
|
||||
if (hasRiskLevel && item.riskLevelValue !== filters.selectedRiskLevel) {
|
||||
return false
|
||||
}
|
||||
if (hasStatus && item.statusValue !== filters.selectedStatus) {
|
||||
return false
|
||||
}
|
||||
if (hasRiskScenario) {
|
||||
const hasScenarioList = Array.isArray(item.scenarioList) && item.scenarioList.length
|
||||
const matched = hasScenarioList
|
||||
? item.scenarioList.includes(filters.selectedRiskScenario)
|
||||
: item.riskCategory === filters.selectedRiskScenario
|
||||
if (!matched) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if (hasOnline && (filters.selectedOnlineState === 'online') !== Boolean(item.isOnlineValue)) {
|
||||
return false
|
||||
}
|
||||
if (hasEnabled && (filters.selectedEnabledState === 'enabled') !== Boolean(item.isEnabledValue)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return (
|
||||
matchesKeyword &&
|
||||
matchesDomain &&
|
||||
matchesOwner &&
|
||||
matchesRiskLevel &&
|
||||
matchesStatus &&
|
||||
matchesRiskScenario &&
|
||||
matchesOnline &&
|
||||
matchesEnabled
|
||||
)
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
180
web/src/views/scripts/digitalEmployeesViewModel.js
Normal file
180
web/src/views/scripts/digitalEmployeesViewModel.js
Normal file
@@ -0,0 +1,180 @@
|
||||
import {
|
||||
buildDigitalEmployeeDetailMeta,
|
||||
buildDigitalEmployeeListMeta
|
||||
} from './auditViewDigitalEmployeeModel.js'
|
||||
import {
|
||||
formatDateTime,
|
||||
normalizeText,
|
||||
resolveStatusMeta
|
||||
} from './auditViewModel.js'
|
||||
|
||||
export const DIGITAL_EMPLOYEE_EXECUTION_MODE_OPTIONS = [
|
||||
{ value: '', label: '全部执行方式' },
|
||||
{ value: 'timed', label: '定时执行' },
|
||||
{ value: 'manual', label: '手动触发' }
|
||||
]
|
||||
|
||||
export function resolveDigitalEmployeeStatusColor(statusValue) {
|
||||
const normalized = normalizeText(statusValue).toLowerCase()
|
||||
if (normalized === 'active') return 'var(--success)'
|
||||
if (['failed', 'error'].includes(normalized)) return '#ef4444'
|
||||
if (['disabled', 'inactive'].includes(normalized)) return '#f59e0b'
|
||||
return 'var(--theme-primary)'
|
||||
}
|
||||
|
||||
export function buildDigitalEmployeeDetailTopBar(employee) {
|
||||
if (!employee) return null
|
||||
|
||||
const meta = employee.digitalEmployee || {}
|
||||
const statusValue = normalizeText(employee.statusValue).toLowerCase()
|
||||
|
||||
return {
|
||||
view: {
|
||||
title: normalizeText(employee.name) || '数字员工详情',
|
||||
desc:
|
||||
normalizeText(meta.description) ||
|
||||
normalizeText(employee.summary) ||
|
||||
'查看数字员工配置、执行计划、运行记录与源文件。'
|
||||
},
|
||||
kpis: [
|
||||
{
|
||||
label: '运行状态',
|
||||
value: normalizeText(employee.status) || (statusValue === 'active' ? '运行中' : '未运行'),
|
||||
unit: '',
|
||||
meta:
|
||||
normalizeText(meta.scheduleLabel) ||
|
||||
normalizeText(employee.scope) ||
|
||||
normalizeText(employee.executionMode) ||
|
||||
'待配置执行计划',
|
||||
trend: statusValue === 'active' ? 'up' : 'down',
|
||||
color: resolveDigitalEmployeeStatusColor(statusValue)
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
export function buildEmployeeListItem(asset) {
|
||||
const meta = buildDigitalEmployeeListMeta(asset)
|
||||
const statusMeta = resolveStatusMeta(asset.status)
|
||||
const displayName = meta.name || '数字员工技能'
|
||||
const executionModeValue = meta.executionMode === '定时执行' ? 'timed' : 'manual'
|
||||
const searchText = [
|
||||
displayName,
|
||||
meta.code,
|
||||
meta.summary,
|
||||
meta.owner,
|
||||
meta.scope,
|
||||
meta.executionMode,
|
||||
meta.skillCategory,
|
||||
statusMeta.label,
|
||||
meta.enabledLabel
|
||||
]
|
||||
.map((value) => normalizeText(value).toLowerCase())
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
|
||||
return {
|
||||
id: asset.id,
|
||||
rawCode: asset.code,
|
||||
short: displayName.slice(0, 2),
|
||||
badgeTone: 'blue',
|
||||
name: displayName,
|
||||
code: meta.code,
|
||||
summary: meta.summary,
|
||||
owner: meta.owner,
|
||||
scope: meta.scope,
|
||||
executionMode: meta.executionMode,
|
||||
executionModeValue,
|
||||
skillCategory: meta.skillCategory,
|
||||
version: asset.working_version || asset.current_version || '-',
|
||||
currentVersion: asset.current_version || '-',
|
||||
status: statusMeta.label,
|
||||
statusValue: asset.status,
|
||||
statusTone: statusMeta.tone,
|
||||
enabledLabel: meta.enabledLabel,
|
||||
enabledTone: meta.enabledTone,
|
||||
isEnabledValue: meta.enabled,
|
||||
configJson: asset.config_json || {},
|
||||
updatedAt: formatDateTime(asset.updated_at),
|
||||
updatedAtRaw: asset.updated_at || '',
|
||||
digitalEmployee: meta,
|
||||
searchText
|
||||
}
|
||||
}
|
||||
|
||||
export function buildEmployeePlaceholder(employee) {
|
||||
return {
|
||||
...employee,
|
||||
type: 'digitalEmployees',
|
||||
typeLabel: '数字员工',
|
||||
currentVersion: employee.currentVersion || employee.version || '-',
|
||||
workingVersion: employee.version || '-',
|
||||
markdownContent: '',
|
||||
loading: true
|
||||
}
|
||||
}
|
||||
|
||||
export function buildEmployeeDetail(asset) {
|
||||
const meta = buildDigitalEmployeeDetailMeta({
|
||||
...asset,
|
||||
updated_at: formatDateTime(asset.updated_at)
|
||||
})
|
||||
const statusMeta = resolveStatusMeta(asset.status)
|
||||
|
||||
return {
|
||||
id: asset.id,
|
||||
type: 'digitalEmployees',
|
||||
typeLabel: '数字员工',
|
||||
rawCode: asset.code,
|
||||
short: meta.name.slice(0, 2),
|
||||
name: meta.name,
|
||||
code: meta.code,
|
||||
summary: meta.description,
|
||||
owner: meta.owner,
|
||||
reviewer: meta.reviewer,
|
||||
category: meta.category,
|
||||
scope: meta.scope,
|
||||
version: asset.working_version || asset.current_version || '-',
|
||||
currentVersion: asset.current_version || '-',
|
||||
workingVersion: asset.working_version || asset.current_version || '-',
|
||||
status: statusMeta.label,
|
||||
statusValue: asset.status,
|
||||
statusTone: statusMeta.tone,
|
||||
configJson: asset.config_json || {},
|
||||
updatedAt: formatDateTime(asset.updated_at),
|
||||
markdownContent: meta.sourceMarkdown,
|
||||
digitalEmployee: meta,
|
||||
loading: false
|
||||
}
|
||||
}
|
||||
|
||||
export function sortEmployees(items) {
|
||||
return [...items].sort((left, right) =>
|
||||
String(right.updatedAtRaw || '').localeCompare(String(left.updatedAtRaw || ''))
|
||||
)
|
||||
}
|
||||
|
||||
export function filterDigitalEmployees(items = [], filters = {}) {
|
||||
const searchText = normalizeText(filters.keyword).toLowerCase()
|
||||
const hasKeyword = Boolean(searchText)
|
||||
const hasStatus = Boolean(filters.selectedStatus)
|
||||
const hasEnabled = Boolean(filters.selectedEnabledState)
|
||||
const hasExecutionMode = Boolean(filters.selectedExecutionMode)
|
||||
|
||||
return items.filter((item) => {
|
||||
if (hasKeyword && !normalizeText(item.searchText).includes(searchText)) {
|
||||
return false
|
||||
}
|
||||
if (hasStatus && item.statusValue !== filters.selectedStatus) {
|
||||
return false
|
||||
}
|
||||
if (hasEnabled && (filters.selectedEnabledState === 'enabled') !== Boolean(item.isEnabledValue)) {
|
||||
return false
|
||||
}
|
||||
if (hasExecutionMode && item.executionModeValue !== filters.selectedExecutionMode) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
}
|
||||
214
web/src/views/scripts/useAuditAssetData.js
Normal file
214
web/src/views/scripts/useAuditAssetData.js
Normal file
@@ -0,0 +1,214 @@
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import {
|
||||
fetchAgentAssetDetail,
|
||||
fetchAgentAssets,
|
||||
fetchAgentRuns
|
||||
} from '../../services/agentAssets.js'
|
||||
import {
|
||||
buildDetailViewModel,
|
||||
buildListItem
|
||||
} from './auditViewModel.js'
|
||||
|
||||
export function useAuditAssetData({
|
||||
activeType,
|
||||
activeMeta,
|
||||
selectedSkill,
|
||||
loadVersionTimeline,
|
||||
loadSpreadsheetChangeRecords,
|
||||
loadRiskRuleJson,
|
||||
toast
|
||||
}) {
|
||||
const loading = ref(false)
|
||||
const errorMessage = ref('')
|
||||
const detailLoading = ref(false)
|
||||
const detailError = ref('')
|
||||
const runLoading = ref(false)
|
||||
const runs = ref([])
|
||||
const assetBuckets = ref({
|
||||
financialRules: [],
|
||||
riskRules: [],
|
||||
mcp: []
|
||||
})
|
||||
|
||||
const currentAssets = computed(() => assetBuckets.value[activeType.value] || [])
|
||||
|
||||
async function loadRuns(options = {}) {
|
||||
if (runLoading.value && !options.force) {
|
||||
return
|
||||
}
|
||||
|
||||
runLoading.value = true
|
||||
try {
|
||||
const payload = await fetchAgentRuns({ limit: 50 })
|
||||
runs.value = Array.isArray(payload) ? payload : []
|
||||
} finally {
|
||||
runLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadAssets(options = {}) {
|
||||
const shouldShowLoading = !options.silent && !options.background
|
||||
if (shouldShowLoading) {
|
||||
loading.value = true
|
||||
}
|
||||
if (!options.silent) {
|
||||
errorMessage.value = ''
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = await fetchAgentAssets({ assetType: activeMeta.value.assetType })
|
||||
const items = Array.isArray(payload) ? payload.map(buildListItem).filter(Boolean) : []
|
||||
|
||||
if (activeMeta.value.assetType === 'rule') {
|
||||
const nextBuckets = {
|
||||
financialRules: [],
|
||||
riskRules: []
|
||||
}
|
||||
|
||||
items.forEach((item) => {
|
||||
if (item?.tabId === 'financialRules' || item?.tabId === 'riskRules') {
|
||||
nextBuckets[item.tabId].push(item)
|
||||
}
|
||||
})
|
||||
|
||||
assetBuckets.value = {
|
||||
...assetBuckets.value,
|
||||
...nextBuckets
|
||||
}
|
||||
} else {
|
||||
assetBuckets.value = {
|
||||
...assetBuckets.value,
|
||||
[activeType.value]: items
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (options.silent || options.background) {
|
||||
return
|
||||
}
|
||||
if (activeMeta.value.assetType === 'rule') {
|
||||
assetBuckets.value = {
|
||||
...assetBuckets.value,
|
||||
financialRules:
|
||||
activeType.value === 'financialRules' ? [] : assetBuckets.value.financialRules,
|
||||
riskRules: []
|
||||
}
|
||||
} else {
|
||||
assetBuckets.value = {
|
||||
...assetBuckets.value,
|
||||
[activeType.value]: []
|
||||
}
|
||||
}
|
||||
errorMessage.value = error?.message || '资产数据加载失败,请稍后重试。'
|
||||
toast(errorMessage.value)
|
||||
} finally {
|
||||
if (shouldShowLoading) {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshCurrentAssets() {
|
||||
await loadAssets({ force: true, silent: true, background: true })
|
||||
}
|
||||
|
||||
async function loadSelectedAssetDetail(assetId) {
|
||||
detailLoading.value = true
|
||||
detailError.value = ''
|
||||
|
||||
try {
|
||||
if (!runs.value.length) {
|
||||
await loadRuns()
|
||||
}
|
||||
const detail = await fetchAgentAssetDetail(assetId)
|
||||
selectedSkill.value = buildDetailViewModel(detail, runs.value)
|
||||
if (selectedSkill.value?.type !== 'rules') {
|
||||
return
|
||||
}
|
||||
|
||||
if (!selectedSkill.value.usesSpreadsheetRule && !selectedSkill.value.usesJsonRiskRule) {
|
||||
loadVersionTimeline(assetId, { silent: true }).catch(() => {})
|
||||
}
|
||||
if (selectedSkill.value.usesSpreadsheetRule) {
|
||||
loadSpreadsheetChangeRecords(assetId).catch(() => {})
|
||||
}
|
||||
if (!selectedSkill.value.usesJsonRiskRule) {
|
||||
return
|
||||
}
|
||||
if (selectedSkill.value.riskRuleGenerationFailed || selectedSkill.value.riskRuleGenerationBusy) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await loadRiskRuleJson(assetId)
|
||||
} catch (jsonError) {
|
||||
console.warn('Failed to load risk rule JSON:', jsonError)
|
||||
const jsonMessage =
|
||||
jsonError?.message || '风险规则 JSON 文件缺失或无法读取,请同步规则库后重试。'
|
||||
toast(jsonMessage)
|
||||
selectedSkill.value = {
|
||||
...selectedSkill.value,
|
||||
riskRuleJsonText: '{}',
|
||||
riskRuleDescription:
|
||||
selectedSkill.value.riskRuleDescription ||
|
||||
'规则 JSON 尚未就绪,请联系管理员执行平台风险规则同步。'
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
detailError.value = error?.message || '资产详情加载失败,请稍后重试。'
|
||||
toast(detailError.value)
|
||||
} finally {
|
||||
detailLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function mergeSelectedRuleLifecycle(detail) {
|
||||
if (!selectedSkill.value || !detail) {
|
||||
return
|
||||
}
|
||||
|
||||
const next = buildDetailViewModel(detail, runs.value)
|
||||
selectedSkill.value = {
|
||||
...selectedSkill.value,
|
||||
status: next.status,
|
||||
statusValue: next.statusValue,
|
||||
statusTone: next.statusTone,
|
||||
publishedVersion: next.publishedVersion,
|
||||
workingVersion: next.workingVersion,
|
||||
currentVersion: next.currentVersion,
|
||||
displayVersion: next.displayVersion,
|
||||
reviewer: next.reviewer,
|
||||
publisher: next.publisher,
|
||||
publishedAt: next.publishedAt,
|
||||
isOnlineValue: next.isOnlineValue,
|
||||
isOnlineLabel: next.isOnlineLabel,
|
||||
isOnlineTone: next.isOnlineTone,
|
||||
isEnabledValue: next.isEnabledValue,
|
||||
isEnabledLabel: next.isEnabledLabel,
|
||||
isEnabledTone: next.isEnabledTone,
|
||||
latestTestSummary: next.latestTestSummary,
|
||||
lastOperationLabel: next.lastOperationLabel,
|
||||
lastOperationTone: next.lastOperationTone,
|
||||
publishMeta: next.publishMeta,
|
||||
publishState: next.publishState,
|
||||
updatedAt: next.updatedAt,
|
||||
configJson: next.configJson
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
loading,
|
||||
errorMessage,
|
||||
detailLoading,
|
||||
detailError,
|
||||
runLoading,
|
||||
runs,
|
||||
assetBuckets,
|
||||
currentAssets,
|
||||
loadRuns,
|
||||
loadAssets,
|
||||
refreshCurrentAssets,
|
||||
loadSelectedAssetDetail,
|
||||
mergeSelectedRuleLifecycle
|
||||
}
|
||||
}
|
||||
226
web/src/views/scripts/useAuditRiskRuleActions.js
Normal file
226
web/src/views/scripts/useAuditRiskRuleActions.js
Normal file
@@ -0,0 +1,226 @@
|
||||
import { ref } from 'vue'
|
||||
|
||||
import {
|
||||
deleteAgentAsset,
|
||||
fetchAgentAssetDetail,
|
||||
publishRiskRuleAsset,
|
||||
returnRiskRuleAsset,
|
||||
setRiskRuleAssetEnabled
|
||||
} from '../../services/agentAssets.js'
|
||||
import { normalizeText } from './auditViewModel.js'
|
||||
|
||||
export function useAuditRiskRuleActions({
|
||||
selectedSkill,
|
||||
detailBusy,
|
||||
actionState,
|
||||
canOpenRiskRuleTest,
|
||||
canDeleteRiskRule,
|
||||
canReturnRiskRule,
|
||||
canPublishRiskRule,
|
||||
canToggleRiskRuleEnabled,
|
||||
riskRuleTestPassed,
|
||||
refreshCurrentAssets,
|
||||
loadSelectedAssetDetail,
|
||||
mergeSelectedRuleLifecycle,
|
||||
closeDetail,
|
||||
resolveActor,
|
||||
toast
|
||||
}) {
|
||||
const riskRuleTestOpen = ref(false)
|
||||
const riskRuleDeleteOpen = ref(false)
|
||||
const riskRuleReturnOpen = ref(false)
|
||||
const riskRulePublishOpen = ref(false)
|
||||
const riskRuleReturnNote = ref('')
|
||||
|
||||
function resetRiskRuleActionDialogs() {
|
||||
riskRuleTestOpen.value = false
|
||||
riskRuleDeleteOpen.value = false
|
||||
riskRuleReturnOpen.value = false
|
||||
riskRulePublishOpen.value = false
|
||||
riskRuleReturnNote.value = ''
|
||||
}
|
||||
|
||||
function openRiskRuleTestDialog() {
|
||||
if (detailBusy.value) {
|
||||
return
|
||||
}
|
||||
if (!canOpenRiskRuleTest.value) {
|
||||
if (!selectedSkill.value?.id) {
|
||||
toast('规则详情还没有加载完成,请稍后再测试。')
|
||||
}
|
||||
return
|
||||
}
|
||||
riskRuleTestOpen.value = true
|
||||
}
|
||||
|
||||
function closeRiskRuleTestDialog() {
|
||||
riskRuleTestOpen.value = false
|
||||
}
|
||||
|
||||
async function handleRiskRuleReportSaved(summary) {
|
||||
if (selectedSkill.value) {
|
||||
selectedSkill.value.latestTestSummary = summary
|
||||
}
|
||||
await refreshCurrentAssets()
|
||||
if (selectedSkill.value?.id) {
|
||||
const detail = await fetchAgentAssetDetail(selectedSkill.value.id)
|
||||
mergeSelectedRuleLifecycle(detail)
|
||||
}
|
||||
}
|
||||
|
||||
function openDeleteRiskRuleDialog() {
|
||||
if (!canDeleteRiskRule.value) {
|
||||
return
|
||||
}
|
||||
riskRuleDeleteOpen.value = true
|
||||
}
|
||||
|
||||
function closeDeleteRiskRuleDialog() {
|
||||
if (detailBusy.value) {
|
||||
return
|
||||
}
|
||||
riskRuleDeleteOpen.value = false
|
||||
}
|
||||
|
||||
async function deleteSelectedRiskRule() {
|
||||
if (!selectedSkill.value || !canDeleteRiskRule.value || detailBusy.value) {
|
||||
return
|
||||
}
|
||||
|
||||
actionState.value = 'delete-risk-rule'
|
||||
try {
|
||||
await deleteAgentAsset(selectedSkill.value.id, { actor: resolveActor() })
|
||||
riskRuleDeleteOpen.value = false
|
||||
const deletedName = selectedSkill.value.name
|
||||
closeDetail()
|
||||
await refreshCurrentAssets()
|
||||
toast(`风险规则“${deletedName}”已删除。`)
|
||||
} catch (error) {
|
||||
toast(error?.message || '风险规则删除失败,请稍后重试。')
|
||||
} finally {
|
||||
actionState.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
function openReturnRiskRuleDialog() {
|
||||
if (!canReturnRiskRule.value) {
|
||||
return
|
||||
}
|
||||
riskRuleReturnNote.value = ''
|
||||
riskRuleReturnOpen.value = true
|
||||
}
|
||||
|
||||
function closeReturnRiskRuleDialog() {
|
||||
if (detailBusy.value) {
|
||||
return
|
||||
}
|
||||
riskRuleReturnOpen.value = false
|
||||
}
|
||||
|
||||
async function returnSelectedRiskRule() {
|
||||
if (!selectedSkill.value || !canReturnRiskRule.value || detailBusy.value) {
|
||||
return
|
||||
}
|
||||
|
||||
const note = normalizeText(riskRuleReturnNote.value)
|
||||
if (!note) {
|
||||
toast('请填写回退原因。')
|
||||
return
|
||||
}
|
||||
|
||||
actionState.value = 'return-risk-rule'
|
||||
try {
|
||||
await returnRiskRuleAsset(selectedSkill.value.id, { note }, { actor: resolveActor() })
|
||||
riskRuleReturnOpen.value = false
|
||||
await refreshCurrentAssets()
|
||||
await loadSelectedAssetDetail(selectedSkill.value.id)
|
||||
toast('风险规则已回退到草稿。')
|
||||
} catch (error) {
|
||||
toast(error?.message || '风险规则回退失败,请稍后重试。')
|
||||
} finally {
|
||||
actionState.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
function openPublishRiskRuleDialog() {
|
||||
if (!canPublishRiskRule.value) {
|
||||
if (!riskRuleTestPassed.value) {
|
||||
toast('请先确认测试报告通过,再发布上线。')
|
||||
}
|
||||
return
|
||||
}
|
||||
riskRulePublishOpen.value = true
|
||||
}
|
||||
|
||||
function closePublishRiskRuleDialog() {
|
||||
if (detailBusy.value) {
|
||||
return
|
||||
}
|
||||
riskRulePublishOpen.value = false
|
||||
}
|
||||
|
||||
async function publishSelectedRiskRule() {
|
||||
if (!selectedSkill.value || !canPublishRiskRule.value || detailBusy.value) {
|
||||
return
|
||||
}
|
||||
|
||||
actionState.value = 'publish-risk-rule'
|
||||
try {
|
||||
await publishRiskRuleAsset(selectedSkill.value.id, { actor: resolveActor() })
|
||||
riskRulePublishOpen.value = false
|
||||
await refreshCurrentAssets()
|
||||
await loadSelectedAssetDetail(selectedSkill.value.id)
|
||||
toast('风险规则已发布上线。')
|
||||
} catch (error) {
|
||||
toast(error?.message || '风险规则发布失败,请稍后重试。')
|
||||
} finally {
|
||||
actionState.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleSelectedRiskRuleEnabled() {
|
||||
if (!selectedSkill.value || !canToggleRiskRuleEnabled.value || detailBusy.value) {
|
||||
return
|
||||
}
|
||||
|
||||
const assetId = selectedSkill.value.id
|
||||
const nextEnabled = !selectedSkill.value.isOnlineValue
|
||||
actionState.value = 'toggle-risk-rule-enabled'
|
||||
try {
|
||||
const detail = await setRiskRuleAssetEnabled(assetId, nextEnabled, { actor: resolveActor() })
|
||||
mergeSelectedRuleLifecycle(detail)
|
||||
await refreshCurrentAssets()
|
||||
toast(
|
||||
nextEnabled
|
||||
? '风险规则已上线。'
|
||||
: '风险规则已下线,不会进入业务扫描。'
|
||||
)
|
||||
} catch (error) {
|
||||
toast(error?.message || '风险规则上线状态更新失败,请稍后重试。')
|
||||
} finally {
|
||||
actionState.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
riskRuleTestOpen,
|
||||
riskRuleDeleteOpen,
|
||||
riskRuleReturnOpen,
|
||||
riskRulePublishOpen,
|
||||
riskRuleReturnNote,
|
||||
resetRiskRuleActionDialogs,
|
||||
openRiskRuleTestDialog,
|
||||
closeRiskRuleTestDialog,
|
||||
handleRiskRuleReportSaved,
|
||||
openDeleteRiskRuleDialog,
|
||||
closeDeleteRiskRuleDialog,
|
||||
deleteSelectedRiskRule,
|
||||
openReturnRiskRuleDialog,
|
||||
closeReturnRiskRuleDialog,
|
||||
returnSelectedRiskRule,
|
||||
openPublishRiskRuleDialog,
|
||||
closePublishRiskRuleDialog,
|
||||
publishSelectedRiskRule,
|
||||
toggleSelectedRiskRuleEnabled
|
||||
}
|
||||
}
|
||||
133
web/src/views/scripts/useAuditRiskRuleCreateFlow.js
Normal file
133
web/src/views/scripts/useAuditRiskRuleCreateFlow.js
Normal file
@@ -0,0 +1,133 @@
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import { generateRiskRuleAsset } from '../../services/agentAssets.js'
|
||||
import { normalizeText } from './auditViewModel.js'
|
||||
import {
|
||||
createDefaultRiskRuleForm
|
||||
} from './auditViewRiskRuleModel.js'
|
||||
|
||||
export function useAuditRiskRuleCreateFlow({
|
||||
activeType,
|
||||
isRuleManager,
|
||||
detailBusy,
|
||||
actionState,
|
||||
assetBuckets,
|
||||
refreshCurrentAssets,
|
||||
resolveActor,
|
||||
toast
|
||||
}) {
|
||||
const riskRuleCreateOpen = ref(false)
|
||||
const riskRuleCreateForm = ref(createDefaultRiskRuleForm())
|
||||
const riskRuleGenerationPollTimers = new Map()
|
||||
|
||||
const riskRuleCreateBusy = computed(() => actionState.value === 'generate-risk-rule')
|
||||
const canCreateRiskRule = computed(
|
||||
() => activeType.value === 'riskRules' && isRuleManager.value && !detailBusy.value
|
||||
)
|
||||
|
||||
function openRiskRuleCreateDialog() {
|
||||
if (activeType.value !== 'riskRules') {
|
||||
return
|
||||
}
|
||||
riskRuleCreateForm.value = createDefaultRiskRuleForm()
|
||||
riskRuleCreateOpen.value = true
|
||||
}
|
||||
|
||||
function closeRiskRuleCreateDialog() {
|
||||
if (riskRuleCreateBusy.value) {
|
||||
return
|
||||
}
|
||||
riskRuleCreateOpen.value = false
|
||||
}
|
||||
|
||||
async function submitRiskRuleCreate() {
|
||||
if (!canCreateRiskRule.value || riskRuleCreateBusy.value) {
|
||||
return
|
||||
}
|
||||
|
||||
const naturalLanguage = String(riskRuleCreateForm.value.natural_language || '').trim()
|
||||
const ruleTitle = String(riskRuleCreateForm.value.rule_title || '').trim()
|
||||
if (ruleTitle.length < 2) {
|
||||
toast('请输入至少 2 个字的规则标题。')
|
||||
return
|
||||
}
|
||||
if (naturalLanguage.length < 8) {
|
||||
toast('请至少输入 8 个字的风险规则描述。')
|
||||
return
|
||||
}
|
||||
|
||||
actionState.value = 'generate-risk-rule'
|
||||
try {
|
||||
const detail = await generateRiskRuleAsset(
|
||||
{
|
||||
business_domain: 'expense',
|
||||
business_stage: riskRuleCreateForm.value.business_stage,
|
||||
expense_category: riskRuleCreateForm.value.expense_category,
|
||||
rule_title: ruleTitle,
|
||||
requires_attachment: Boolean(riskRuleCreateForm.value.requires_attachment),
|
||||
natural_language: naturalLanguage
|
||||
},
|
||||
{ actor: resolveActor() }
|
||||
)
|
||||
riskRuleCreateOpen.value = false
|
||||
await refreshCurrentAssets()
|
||||
scheduleRiskRuleGenerationPoll(detail.id)
|
||||
toast('风险规则已进入后台生成,列表会先显示生成中。')
|
||||
} catch (error) {
|
||||
toast(error?.message || '风险规则生成失败,请稍后重试。')
|
||||
} finally {
|
||||
actionState.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
function stopRiskRuleGenerationPoll(assetId) {
|
||||
const timer = riskRuleGenerationPollTimers.get(assetId)
|
||||
if (timer) {
|
||||
window.clearTimeout(timer)
|
||||
riskRuleGenerationPollTimers.delete(assetId)
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleRiskRuleGenerationPoll(assetId, attempt = 0) {
|
||||
const normalizedAssetId = normalizeText(assetId)
|
||||
if (!normalizedAssetId) {
|
||||
return
|
||||
}
|
||||
|
||||
stopRiskRuleGenerationPoll(normalizedAssetId)
|
||||
const timer = window.setTimeout(async () => {
|
||||
try {
|
||||
await refreshCurrentAssets()
|
||||
const latest = (assetBuckets.value.riskRules || []).find((item) => item.id === normalizedAssetId)
|
||||
if (!latest || latest.statusValue !== 'generating' || attempt >= 59) {
|
||||
riskRuleGenerationPollTimers.delete(normalizedAssetId)
|
||||
return
|
||||
}
|
||||
scheduleRiskRuleGenerationPoll(normalizedAssetId, attempt + 1)
|
||||
} catch {
|
||||
if (attempt < 59) {
|
||||
scheduleRiskRuleGenerationPoll(normalizedAssetId, attempt + 1)
|
||||
} else {
|
||||
riskRuleGenerationPollTimers.delete(normalizedAssetId)
|
||||
}
|
||||
}
|
||||
}, attempt === 0 ? 1200 : 3000)
|
||||
riskRuleGenerationPollTimers.set(normalizedAssetId, timer)
|
||||
}
|
||||
|
||||
function stopAllRiskRuleGenerationPolls() {
|
||||
riskRuleGenerationPollTimers.forEach((timer) => window.clearTimeout(timer))
|
||||
riskRuleGenerationPollTimers.clear()
|
||||
}
|
||||
|
||||
return {
|
||||
canCreateRiskRule,
|
||||
riskRuleCreateOpen,
|
||||
riskRuleCreateForm,
|
||||
riskRuleCreateBusy,
|
||||
openRiskRuleCreateDialog,
|
||||
closeRiskRuleCreateDialog,
|
||||
submitRiskRuleCreate,
|
||||
stopAllRiskRuleGenerationPolls
|
||||
}
|
||||
}
|
||||
101
web/src/views/scripts/useAuditRiskRuleJsonEditor.js
Normal file
101
web/src/views/scripts/useAuditRiskRuleJsonEditor.js
Normal file
@@ -0,0 +1,101 @@
|
||||
import {
|
||||
fetchAgentAssetRuleJson,
|
||||
saveAgentAssetRuleJson
|
||||
} from '../../services/agentAssets.js'
|
||||
import {
|
||||
applyRiskRuleJsonState,
|
||||
resolveRiskRuleDescription
|
||||
} from './auditViewModel.js'
|
||||
|
||||
function readJsonPayload(payload) {
|
||||
return payload?.payload && typeof payload.payload === 'object' ? payload.payload : payload
|
||||
}
|
||||
|
||||
function downloadTextFile({ content, fileName, type }) {
|
||||
const blob = new Blob([content], { type })
|
||||
const objectUrl = URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = objectUrl
|
||||
link.download = fileName
|
||||
link.click()
|
||||
URL.revokeObjectURL(objectUrl)
|
||||
}
|
||||
|
||||
export function useAuditRiskRuleJsonEditor({
|
||||
selectedSkill,
|
||||
canEditMarkdown,
|
||||
actionState,
|
||||
toast
|
||||
}) {
|
||||
async function loadRiskRuleJson(assetId) {
|
||||
if (!assetId || !selectedSkill.value?.usesJsonRiskRule) {
|
||||
return
|
||||
}
|
||||
|
||||
const payload = await fetchAgentAssetRuleJson(assetId)
|
||||
selectedSkill.value = applyRiskRuleJsonState(
|
||||
selectedSkill.value,
|
||||
readJsonPayload(payload),
|
||||
payload
|
||||
)
|
||||
}
|
||||
|
||||
async function saveRiskRuleJson() {
|
||||
if (!selectedSkill.value?.id || !canEditMarkdown.value) {
|
||||
return
|
||||
}
|
||||
|
||||
actionState.value = 'save-risk-json'
|
||||
try {
|
||||
const parsed = JSON.parse(String(selectedSkill.value.riskRuleJsonText || '{}'))
|
||||
const saved = await saveAgentAssetRuleJson(selectedSkill.value.id, { payload: parsed })
|
||||
selectedSkill.value = applyRiskRuleJsonState(
|
||||
selectedSkill.value,
|
||||
readJsonPayload(saved),
|
||||
saved
|
||||
)
|
||||
toast('风险规则 JSON 已保存。')
|
||||
} catch (error) {
|
||||
toast(error?.message || '风险规则 JSON 保存失败。')
|
||||
} finally {
|
||||
actionState.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
function formatRiskRuleJson() {
|
||||
if (!selectedSkill.value?.usesJsonRiskRule) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(String(selectedSkill.value.riskRuleJsonText || '{}'))
|
||||
selectedSkill.value = applyRiskRuleJsonState(selectedSkill.value, parsed, {
|
||||
name: selectedSkill.value.name,
|
||||
description: resolveRiskRuleDescription(parsed)
|
||||
})
|
||||
} catch (error) {
|
||||
toast(error?.message || 'JSON 格式无效,无法格式化。')
|
||||
}
|
||||
}
|
||||
|
||||
function downloadRiskRuleJson() {
|
||||
if (!selectedSkill.value?.usesJsonRiskRule) {
|
||||
return
|
||||
}
|
||||
|
||||
downloadTextFile({
|
||||
content: String(selectedSkill.value.riskRuleJsonText || '{}'),
|
||||
fileName:
|
||||
selectedSkill.value.ruleDocument?.file_name ||
|
||||
`${selectedSkill.value.code || 'risk-rule'}.json`,
|
||||
type: 'application/json;charset=utf-8'
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
loadRiskRuleJson,
|
||||
saveRiskRuleJson,
|
||||
formatRiskRuleJson,
|
||||
downloadRiskRuleJson
|
||||
}
|
||||
}
|
||||
187
web/src/views/scripts/useAuditRuleReviewFlow.js
Normal file
187
web/src/views/scripts/useAuditRuleReviewFlow.js
Normal file
@@ -0,0 +1,187 @@
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import { fetchEmployees } from '../../services/employees.js'
|
||||
import { createAgentAssetReview } from '../../services/agentAssets.js'
|
||||
import {
|
||||
buildReviewNote
|
||||
} from './auditViewRuntimeModel.js'
|
||||
import {
|
||||
normalizeText,
|
||||
resolveReviewMeta
|
||||
} from './auditViewModel.js'
|
||||
|
||||
export function useAuditRuleReviewFlow({
|
||||
selectedSkill,
|
||||
selectedSkillIsRule,
|
||||
selectedSkillUsesJsonRisk,
|
||||
canEditSelected,
|
||||
canManageSelected,
|
||||
isDisplayingWorkingVersion,
|
||||
canOpenRiskRuleReviewSubmit,
|
||||
riskRuleTestPassed,
|
||||
detailBusy,
|
||||
actionState,
|
||||
refreshCurrentAssets,
|
||||
loadSelectedAssetDetail,
|
||||
resolveActor,
|
||||
toast
|
||||
}) {
|
||||
const reviewSubmitOpen = ref(false)
|
||||
const reviewSubmitVersion = ref('')
|
||||
const reviewSubmitReviewer = ref('')
|
||||
const reviewSubmitReviewerLoading = ref(false)
|
||||
const reviewSubmitReviewerOptions = ref([])
|
||||
|
||||
const canSubmitReview = computed(
|
||||
() =>
|
||||
!selectedSkillUsesJsonRisk.value &&
|
||||
canEditSelected.value &&
|
||||
selectedSkillIsRule.value &&
|
||||
isDisplayingWorkingVersion.value
|
||||
)
|
||||
const hasReviewSubmitReviewers = computed(() => reviewSubmitReviewerOptions.value.length > 0)
|
||||
const canReviewSelected = computed(
|
||||
() => canManageSelected.value && selectedSkillIsRule.value && isDisplayingWorkingVersion.value
|
||||
)
|
||||
|
||||
async function reviewSelectedRule(reviewStatus) {
|
||||
if (!selectedSkill.value || !selectedSkillIsRule.value || detailBusy.value) {
|
||||
return
|
||||
}
|
||||
if (reviewStatus === 'pending' && !canSubmitReview.value) {
|
||||
return
|
||||
}
|
||||
if (reviewStatus !== 'pending' && !canReviewSelected.value) {
|
||||
return
|
||||
}
|
||||
|
||||
actionState.value = `review-${reviewStatus}`
|
||||
try {
|
||||
await createAgentAssetReview(
|
||||
selectedSkill.value.id,
|
||||
{
|
||||
version: selectedSkill.value.workingVersion,
|
||||
reviewer: resolveActor(),
|
||||
review_status: reviewStatus,
|
||||
review_note: buildReviewNote(reviewStatus)
|
||||
},
|
||||
{ actor: resolveActor() }
|
||||
)
|
||||
await refreshCurrentAssets()
|
||||
await loadSelectedAssetDetail(selectedSkill.value.id)
|
||||
toast(`当前规则版本已标记为${resolveReviewMeta(reviewStatus).label}。`)
|
||||
} catch (error) {
|
||||
toast(error?.message || '规则审核提交失败,请稍后重试。')
|
||||
} finally {
|
||||
actionState.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
async function loadReviewSubmitReviewers() {
|
||||
reviewSubmitReviewerLoading.value = true
|
||||
try {
|
||||
const employees = await fetchEmployees()
|
||||
reviewSubmitReviewerOptions.value = (Array.isArray(employees) ? employees : [])
|
||||
.filter(
|
||||
(item) =>
|
||||
item.status === '在职' &&
|
||||
Array.isArray(item.roleCodes) &&
|
||||
item.roleCodes.includes('manager')
|
||||
)
|
||||
.map((item) => ({
|
||||
value: item.name,
|
||||
label: `${item.name} · ${item.position || '高级管理员'}`
|
||||
}))
|
||||
} catch (error) {
|
||||
reviewSubmitReviewerOptions.value = []
|
||||
toast(error?.message || '审核人列表加载失败,请稍后重试。')
|
||||
} finally {
|
||||
reviewSubmitReviewerLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function openSubmitReviewDialog() {
|
||||
if (
|
||||
selectedSkillUsesJsonRisk.value &&
|
||||
!canOpenRiskRuleReviewSubmit.value
|
||||
) {
|
||||
return
|
||||
}
|
||||
if (!selectedSkill.value || !canSubmitReview.value || detailBusy.value) {
|
||||
return
|
||||
}
|
||||
|
||||
reviewSubmitVersion.value = selectedSkill.value.workingVersion || selectedSkill.value.displayVersion || ''
|
||||
reviewSubmitReviewer.value = selectedSkill.value.reviewer || ''
|
||||
reviewSubmitOpen.value = true
|
||||
await loadReviewSubmitReviewers()
|
||||
if (!reviewSubmitReviewerOptions.value.some((item) => item.value === reviewSubmitReviewer.value)) {
|
||||
reviewSubmitReviewer.value = reviewSubmitReviewerOptions.value[0]?.value || ''
|
||||
}
|
||||
}
|
||||
|
||||
function closeSubmitReviewDialog() {
|
||||
if (detailBusy.value) {
|
||||
return
|
||||
}
|
||||
reviewSubmitOpen.value = false
|
||||
}
|
||||
|
||||
async function submitSelectedRuleForReview() {
|
||||
if (!selectedSkill.value || !canSubmitReview.value || detailBusy.value) {
|
||||
return
|
||||
}
|
||||
if (selectedSkillUsesJsonRisk.value && !riskRuleTestPassed.value) {
|
||||
toast('当前规则版本尚未确认测试通过,不能提交审核。')
|
||||
return
|
||||
}
|
||||
|
||||
const version = normalizeText(reviewSubmitVersion.value)
|
||||
const reviewer = normalizeText(reviewSubmitReviewer.value)
|
||||
if (!version) {
|
||||
toast('请输入送审版本号。')
|
||||
return
|
||||
}
|
||||
if (!reviewer) {
|
||||
toast('请选择审核人。')
|
||||
return
|
||||
}
|
||||
|
||||
actionState.value = 'review-pending'
|
||||
try {
|
||||
await createAgentAssetReview(
|
||||
selectedSkill.value.id,
|
||||
{
|
||||
version,
|
||||
reviewer,
|
||||
review_status: 'pending',
|
||||
review_note: buildReviewNote('pending')
|
||||
},
|
||||
{ actor: resolveActor() }
|
||||
)
|
||||
reviewSubmitOpen.value = false
|
||||
await refreshCurrentAssets()
|
||||
await loadSelectedAssetDetail(selectedSkill.value.id)
|
||||
toast(`规则版本 ${version} 已提交给 ${reviewer} 审核。`)
|
||||
} catch (error) {
|
||||
toast(error?.message || '规则审核提交失败,请稍后重试。')
|
||||
} finally {
|
||||
actionState.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
reviewSubmitOpen,
|
||||
reviewSubmitVersion,
|
||||
reviewSubmitReviewer,
|
||||
reviewSubmitReviewerLoading,
|
||||
reviewSubmitReviewerOptions,
|
||||
canSubmitReview,
|
||||
hasReviewSubmitReviewers,
|
||||
canReviewSelected,
|
||||
reviewSelectedRule,
|
||||
openSubmitReviewDialog,
|
||||
closeSubmitReviewDialog,
|
||||
submitSelectedRuleForReview
|
||||
}
|
||||
}
|
||||
151
web/src/views/scripts/useAuditRuleVersionActions.js
Normal file
151
web/src/views/scripts/useAuditRuleVersionActions.js
Normal file
@@ -0,0 +1,151 @@
|
||||
import {
|
||||
activateAgentAsset,
|
||||
createAgentAssetVersion,
|
||||
restoreAgentAssetVersion,
|
||||
updateAgentAsset
|
||||
} from '../../services/agentAssets.js'
|
||||
import {
|
||||
buildRuleConfigPayload,
|
||||
incrementVersion
|
||||
} from './auditViewRuntimeModel.js'
|
||||
import {
|
||||
buildMarkdownVersionContent,
|
||||
normalizeText,
|
||||
parseRuntimeRuleText
|
||||
} from './auditViewModel.js'
|
||||
|
||||
export function useAuditRuleVersionActions({
|
||||
selectedSkill,
|
||||
selectedSkillIsRule,
|
||||
canEditMarkdown,
|
||||
canManageSelected,
|
||||
actionState,
|
||||
detailBusy,
|
||||
refreshCurrentAssets,
|
||||
loadSelectedAssetDetail,
|
||||
resolveActor,
|
||||
toast
|
||||
}) {
|
||||
async function persistRuleRuntimeConfig(asset, runtimeRule) {
|
||||
await updateAgentAsset(
|
||||
asset.id,
|
||||
{
|
||||
config_json: buildRuleConfigPayload(asset, runtimeRule)
|
||||
},
|
||||
{ actor: resolveActor() }
|
||||
)
|
||||
}
|
||||
|
||||
async function saveRuleVersion({ action, changeNote, successLabel }) {
|
||||
if (
|
||||
!selectedSkill.value ||
|
||||
!selectedSkillIsRule.value ||
|
||||
selectedSkill.value.usesSpreadsheetRule ||
|
||||
!canEditMarkdown.value ||
|
||||
detailBusy.value
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!normalizeText(selectedSkill.value.markdownContent)) {
|
||||
toast('规则 Markdown 内容不能为空。')
|
||||
return
|
||||
}
|
||||
|
||||
const runtimeRule = parseRuntimeRuleText(selectedSkill.value.runtimeRuleText)
|
||||
if (!runtimeRule) {
|
||||
toast('运行时 JSON 必须是合法的对象。')
|
||||
return
|
||||
}
|
||||
|
||||
const nextVersion = incrementVersion(selectedSkill.value.currentVersion)
|
||||
actionState.value = action
|
||||
|
||||
try {
|
||||
await createAgentAssetVersion(
|
||||
selectedSkill.value.id,
|
||||
{
|
||||
version: nextVersion,
|
||||
content: buildMarkdownVersionContent(selectedSkill.value.markdownContent, runtimeRule),
|
||||
content_type: 'markdown',
|
||||
change_note: changeNote,
|
||||
created_by: resolveActor()
|
||||
},
|
||||
{ actor: resolveActor() }
|
||||
)
|
||||
await persistRuleRuntimeConfig(selectedSkill.value, runtimeRule)
|
||||
await refreshCurrentAssets()
|
||||
await loadSelectedAssetDetail(selectedSkill.value.id)
|
||||
toast(`${successLabel} ${nextVersion}。`)
|
||||
} catch (error) {
|
||||
toast(error?.message || `${successLabel}失败,请稍后重试。`)
|
||||
} finally {
|
||||
actionState.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
async function saveRuleMarkdown() {
|
||||
await saveRuleVersion({
|
||||
action: 'save-markdown',
|
||||
changeNote: '通过规则中心保存 Markdown 规则内容,并同步运行时 JSON。',
|
||||
successLabel: '规则 Markdown 已保存为'
|
||||
})
|
||||
}
|
||||
|
||||
async function saveRuleRuntimeJson() {
|
||||
await saveRuleVersion({
|
||||
action: 'save-runtime-json',
|
||||
changeNote: '通过规则中心保存运行时 JSON 配置。',
|
||||
successLabel: '规则 JSON 已保存为'
|
||||
})
|
||||
}
|
||||
|
||||
async function activateSelectedRule() {
|
||||
if (!selectedSkill.value || !selectedSkillIsRule.value || !canManageSelected.value || detailBusy.value) {
|
||||
return
|
||||
}
|
||||
|
||||
actionState.value = 'activate'
|
||||
try {
|
||||
await activateAgentAsset(selectedSkill.value.id, { actor: resolveActor() })
|
||||
await refreshCurrentAssets()
|
||||
await loadSelectedAssetDetail(selectedSkill.value.id)
|
||||
toast('规则已正式上线。')
|
||||
} catch (error) {
|
||||
toast(error?.message || '规则上线失败,请稍后重试。')
|
||||
} finally {
|
||||
actionState.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
async function restoreSelectedVersion(version) {
|
||||
if (
|
||||
!selectedSkill.value ||
|
||||
!selectedSkillIsRule.value ||
|
||||
!canManageSelected.value ||
|
||||
detailBusy.value ||
|
||||
!version
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
actionState.value = `restore-${version}`
|
||||
try {
|
||||
await restoreAgentAssetVersion(selectedSkill.value.id, version, { actor: resolveActor() })
|
||||
await refreshCurrentAssets()
|
||||
await loadSelectedAssetDetail(selectedSkill.value.id)
|
||||
toast(`已基于 ${version} 生成新的工作版本。`)
|
||||
} catch (error) {
|
||||
toast(error?.message || '历史版本恢复失败,请稍后重试。')
|
||||
} finally {
|
||||
actionState.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
saveRuleMarkdown,
|
||||
saveRuleRuntimeJson,
|
||||
activateSelectedRule,
|
||||
restoreSelectedVersion
|
||||
}
|
||||
}
|
||||
423
web/src/views/scripts/useAuditSpreadsheetEditor.js
Normal file
423
web/src/views/scripts/useAuditSpreadsheetEditor.js
Normal file
@@ -0,0 +1,423 @@
|
||||
import { computed, nextTick, ref } from 'vue'
|
||||
|
||||
import {
|
||||
fetchAgentAssetSpreadsheetBlob,
|
||||
fetchAgentAssetSpreadsheetChangeRecords,
|
||||
fetchAgentAssetSpreadsheetOnlyOfficeConfig,
|
||||
importAgentAssetSpreadsheetContent
|
||||
} from '../../services/agentAssets.js'
|
||||
import { loadOnlyOfficeApi } from '../../services/onlyoffice.js'
|
||||
import { buildOnlyOfficeEditorConfig } from './onlyOfficePreviewConfig.js'
|
||||
import { buildSpreadsheetChangeRecordKey } from './auditViewRuntimeModel.js'
|
||||
import {
|
||||
formatDateTime,
|
||||
formatSpreadsheetChangeSummary,
|
||||
normalizeText,
|
||||
resolveDiffChangeMeta
|
||||
} from './auditViewModel.js'
|
||||
|
||||
export function useAuditSpreadsheetEditor({
|
||||
selectedSkill,
|
||||
selectedSkillUsesSpreadsheet,
|
||||
canEditSpreadsheetInline,
|
||||
canUploadSpreadsheet,
|
||||
canDownloadSpreadsheet,
|
||||
selectedSpreadsheetFileName,
|
||||
actionState,
|
||||
refreshCurrentAssets,
|
||||
loadSelectedAssetDetail,
|
||||
resolveActor,
|
||||
toast
|
||||
}) {
|
||||
const spreadsheetUploadInput = ref(null)
|
||||
const spreadsheetOnlyOfficeLoading = ref(false)
|
||||
const spreadsheetOnlyOfficeError = ref('')
|
||||
const spreadsheetOnlyOfficeEditor = ref(null)
|
||||
const spreadsheetOnlyOfficeReady = ref(false)
|
||||
const spreadsheetOnlyOfficeHostId = ref('audit-rule-onlyoffice')
|
||||
const spreadsheetChangeRecordsByAsset = ref({})
|
||||
const spreadsheetChangeDetailOpen = ref(false)
|
||||
const selectedSpreadsheetChangeRecord = ref(null)
|
||||
|
||||
let spreadsheetOnlyOfficeMountSeq = 0
|
||||
let spreadsheetOnlyOfficeLoadTimer = null
|
||||
let spreadsheetOnlyOfficeHadLocalEdits = false
|
||||
let spreadsheetOnlyOfficeSyncSeq = 0
|
||||
let spreadsheetOnlyOfficeChangePollTimer = null
|
||||
|
||||
const selectedSpreadsheetChangeRecords = computed(() => {
|
||||
if (!selectedSkillUsesSpreadsheet.value || !selectedSkill.value?.id) {
|
||||
return []
|
||||
}
|
||||
return (spreadsheetChangeRecordsByAsset.value[selectedSkill.value.id] || [])
|
||||
.filter((item) => item?.changed_at)
|
||||
.map((item) => {
|
||||
const sheetNames = [
|
||||
...(Array.isArray(item.sheet_changes)
|
||||
? item.sheet_changes.map((change) => normalizeText(change.sheet_name))
|
||||
: []),
|
||||
...(Array.isArray(item.cell_changes)
|
||||
? item.cell_changes.map((change) => normalizeText(change.sheet_name))
|
||||
: [])
|
||||
].filter(Boolean)
|
||||
const changedSheetNames = [...new Set(sheetNames)]
|
||||
const previewChanges = Array.isArray(item.cell_changes) ? item.cell_changes.slice(0, 3) : []
|
||||
return {
|
||||
...item,
|
||||
time: formatDateTime(item.changed_at),
|
||||
summary: formatSpreadsheetChangeSummary(item.summary),
|
||||
changeCountLabel: item.changed_cell_count
|
||||
? `${item.changed_cell_count} 处改动`
|
||||
: `${item.changed_sheet_count || changedSheetNames.length || 0} 个工作表`,
|
||||
changedSheetNames,
|
||||
sheetPreview: changedSheetNames.slice(0, 4),
|
||||
remainingSheetCount: Math.max(changedSheetNames.length - 4, 0),
|
||||
previewChanges,
|
||||
remainingChangeCount: Math.max((item.changed_cell_count || 0) - previewChanges.length, 0)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const selectedSpreadsheetChangeSheetRows = computed(() =>
|
||||
Array.isArray(selectedSpreadsheetChangeRecord.value?.sheet_changes)
|
||||
? selectedSpreadsheetChangeRecord.value.sheet_changes.map((item) => ({
|
||||
...item,
|
||||
meta: resolveDiffChangeMeta(item.change_type)
|
||||
}))
|
||||
: []
|
||||
)
|
||||
|
||||
const selectedSpreadsheetChangeCellRows = computed(() =>
|
||||
Array.isArray(selectedSpreadsheetChangeRecord.value?.cell_changes)
|
||||
? selectedSpreadsheetChangeRecord.value.cell_changes.map((item) => ({
|
||||
...item,
|
||||
meta: resolveDiffChangeMeta(item.change_type)
|
||||
}))
|
||||
: []
|
||||
)
|
||||
|
||||
function stopSpreadsheetOnlyOfficeChangeSync() {
|
||||
if (spreadsheetOnlyOfficeChangePollTimer) {
|
||||
window.clearTimeout(spreadsheetOnlyOfficeChangePollTimer)
|
||||
spreadsheetOnlyOfficeChangePollTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
function destroySpreadsheetOnlyOfficeEditor() {
|
||||
if (spreadsheetOnlyOfficeLoadTimer) {
|
||||
window.clearTimeout(spreadsheetOnlyOfficeLoadTimer)
|
||||
spreadsheetOnlyOfficeLoadTimer = null
|
||||
}
|
||||
stopSpreadsheetOnlyOfficeChangeSync()
|
||||
spreadsheetOnlyOfficeHadLocalEdits = false
|
||||
spreadsheetOnlyOfficeSyncSeq += 1
|
||||
if (spreadsheetOnlyOfficeEditor.value?.destroyEditor) {
|
||||
spreadsheetOnlyOfficeEditor.value.destroyEditor()
|
||||
}
|
||||
spreadsheetOnlyOfficeEditor.value = null
|
||||
spreadsheetOnlyOfficeReady.value = false
|
||||
}
|
||||
|
||||
function getLatestSpreadsheetChangeKey(assetId) {
|
||||
return buildSpreadsheetChangeRecordKey(spreadsheetChangeRecordsByAsset.value[assetId] || [])
|
||||
}
|
||||
|
||||
async function loadSpreadsheetChangeRecords(assetId) {
|
||||
if (!assetId) {
|
||||
return
|
||||
}
|
||||
const payload = await fetchAgentAssetSpreadsheetChangeRecords(assetId, 30)
|
||||
spreadsheetChangeRecordsByAsset.value = {
|
||||
...spreadsheetChangeRecordsByAsset.value,
|
||||
[assetId]: Array.isArray(payload) ? payload : []
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshSpreadsheetChangeRecordsAfterSave(assetId, previousLatestKey = '', attempt = 0) {
|
||||
const normalizedAssetId = normalizeText(assetId)
|
||||
if (!normalizedAssetId || selectedSkill.value?.id !== normalizedAssetId) {
|
||||
return false
|
||||
}
|
||||
|
||||
await loadSpreadsheetChangeRecords(normalizedAssetId)
|
||||
const nextLatestKey = getLatestSpreadsheetChangeKey(normalizedAssetId)
|
||||
if (nextLatestKey && nextLatestKey !== previousLatestKey) {
|
||||
return true
|
||||
}
|
||||
if (attempt >= 9) {
|
||||
return false
|
||||
}
|
||||
await new Promise((resolve) => window.setTimeout(resolve, 800))
|
||||
return refreshSpreadsheetChangeRecordsAfterSave(normalizedAssetId, previousLatestKey, attempt + 1)
|
||||
}
|
||||
|
||||
function scheduleSpreadsheetOnlyOfficeChangeSync(assetId, attempt = 0) {
|
||||
const normalizedAssetId = normalizeText(assetId)
|
||||
if (!normalizedAssetId) {
|
||||
return
|
||||
}
|
||||
|
||||
const syncSeq = ++spreadsheetOnlyOfficeSyncSeq
|
||||
stopSpreadsheetOnlyOfficeChangeSync()
|
||||
const previousLatestChangeKey = getLatestSpreadsheetChangeKey(normalizedAssetId)
|
||||
|
||||
const runSync = async () => {
|
||||
if (syncSeq !== spreadsheetOnlyOfficeSyncSeq || selectedSkill.value?.id !== normalizedAssetId) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const changeRecordRefreshed = await refreshSpreadsheetChangeRecordsAfterSave(
|
||||
normalizedAssetId,
|
||||
previousLatestChangeKey
|
||||
)
|
||||
if (changeRecordRefreshed) {
|
||||
await refreshCurrentAssets()
|
||||
stopSpreadsheetOnlyOfficeChangeSync()
|
||||
return
|
||||
}
|
||||
} catch {
|
||||
// 临时轮询失败不打断编辑器,继续在窗口期内重试。
|
||||
}
|
||||
|
||||
if (syncSeq !== spreadsheetOnlyOfficeSyncSeq || selectedSkill.value?.id !== normalizedAssetId) {
|
||||
return
|
||||
}
|
||||
if (attempt >= 29) {
|
||||
return
|
||||
}
|
||||
spreadsheetOnlyOfficeChangePollTimer = window.setTimeout(() => {
|
||||
scheduleSpreadsheetOnlyOfficeChangeSync(normalizedAssetId, attempt + 1)
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
spreadsheetOnlyOfficeChangePollTimer = window.setTimeout(() => {
|
||||
runSync().catch(() => {})
|
||||
}, attempt === 0 ? 800 : 2000)
|
||||
}
|
||||
|
||||
function isSpreadsheetOnlyOfficeMountStale(mountSeq, assetId) {
|
||||
return (
|
||||
mountSeq !== spreadsheetOnlyOfficeMountSeq ||
|
||||
!selectedSkillUsesSpreadsheet.value ||
|
||||
selectedSkill.value?.id !== assetId ||
|
||||
selectedSkill.value?.loading
|
||||
)
|
||||
}
|
||||
|
||||
async function mountSpreadsheetOnlyOfficeEditor(retryAttempt = 0) {
|
||||
if (!selectedSkillUsesSpreadsheet.value || !selectedSkill.value?.id || selectedSkill.value?.loading) {
|
||||
destroySpreadsheetOnlyOfficeEditor()
|
||||
return
|
||||
}
|
||||
|
||||
const mountSeq = ++spreadsheetOnlyOfficeMountSeq
|
||||
const assetId = selectedSkill.value.id
|
||||
const editable = canEditSpreadsheetInline.value
|
||||
|
||||
spreadsheetOnlyOfficeLoading.value = true
|
||||
spreadsheetOnlyOfficeError.value = ''
|
||||
spreadsheetOnlyOfficeReady.value = false
|
||||
destroySpreadsheetOnlyOfficeEditor()
|
||||
|
||||
try {
|
||||
const payload = await fetchAgentAssetSpreadsheetOnlyOfficeConfig(assetId)
|
||||
if (isSpreadsheetOnlyOfficeMountStale(mountSeq, assetId)) {
|
||||
return
|
||||
}
|
||||
|
||||
await loadOnlyOfficeApi(payload.documentServerUrl)
|
||||
if (isSpreadsheetOnlyOfficeMountStale(mountSeq, assetId)) {
|
||||
return
|
||||
}
|
||||
if (!window.DocsAPI?.DocEditor) {
|
||||
throw new Error('表格编辑器未正确加载。')
|
||||
}
|
||||
|
||||
// ONLYOFFICE 会改写宿主节点;每次挂载使用新 id 避免复用脏容器。
|
||||
spreadsheetOnlyOfficeHostId.value = `audit-rule-onlyoffice-${assetId}-${mountSeq}`
|
||||
await nextTick()
|
||||
if (isSpreadsheetOnlyOfficeMountStale(mountSeq, assetId)) {
|
||||
return
|
||||
}
|
||||
|
||||
const config = buildOnlyOfficeEditorConfig(payload.config, {
|
||||
viewportHeight: window.innerHeight,
|
||||
editable,
|
||||
fillContainer: true
|
||||
})
|
||||
const upstreamEvents = config.events || {}
|
||||
spreadsheetOnlyOfficeLoadTimer = window.setTimeout(() => {
|
||||
if (isSpreadsheetOnlyOfficeMountStale(mountSeq, assetId)) {
|
||||
return
|
||||
}
|
||||
if (retryAttempt < 1) {
|
||||
destroySpreadsheetOnlyOfficeEditor()
|
||||
spreadsheetOnlyOfficeLoading.value = true
|
||||
window.setTimeout(() => {
|
||||
mountSpreadsheetOnlyOfficeEditor(retryAttempt + 1).catch(() => {})
|
||||
}, 600)
|
||||
return
|
||||
}
|
||||
spreadsheetOnlyOfficeError.value = '表格加载超时,请退出详情后重试。'
|
||||
spreadsheetOnlyOfficeLoading.value = false
|
||||
destroySpreadsheetOnlyOfficeEditor()
|
||||
}, 15000)
|
||||
config.events = {
|
||||
...upstreamEvents,
|
||||
onAppReady(event) {
|
||||
if (isSpreadsheetOnlyOfficeMountStale(mountSeq, assetId)) {
|
||||
return
|
||||
}
|
||||
if (spreadsheetOnlyOfficeLoadTimer) {
|
||||
window.clearTimeout(spreadsheetOnlyOfficeLoadTimer)
|
||||
spreadsheetOnlyOfficeLoadTimer = null
|
||||
}
|
||||
spreadsheetOnlyOfficeReady.value = true
|
||||
spreadsheetOnlyOfficeLoading.value = false
|
||||
upstreamEvents.onAppReady?.(event)
|
||||
},
|
||||
onError(event) {
|
||||
if (isSpreadsheetOnlyOfficeMountStale(mountSeq, assetId)) {
|
||||
return
|
||||
}
|
||||
if (spreadsheetOnlyOfficeLoadTimer) {
|
||||
window.clearTimeout(spreadsheetOnlyOfficeLoadTimer)
|
||||
spreadsheetOnlyOfficeLoadTimer = null
|
||||
}
|
||||
const errorCode = event?.data?.errorCode
|
||||
const errorDescription = event?.data?.errorDescription
|
||||
spreadsheetOnlyOfficeError.value = errorDescription
|
||||
? `表格加载失败:${errorDescription}`
|
||||
: `表格加载失败${errorCode ? `(错误码 ${errorCode})` : '。'}`
|
||||
spreadsheetOnlyOfficeLoading.value = false
|
||||
upstreamEvents.onError?.(event)
|
||||
},
|
||||
onDocumentStateChange(event) {
|
||||
const hasChanges = Boolean(event?.data)
|
||||
if (hasChanges) {
|
||||
spreadsheetOnlyOfficeHadLocalEdits = true
|
||||
if (!spreadsheetOnlyOfficeChangePollTimer) {
|
||||
scheduleSpreadsheetOnlyOfficeChangeSync(assetId)
|
||||
}
|
||||
} else if (
|
||||
spreadsheetOnlyOfficeHadLocalEdits &&
|
||||
editable &&
|
||||
!isSpreadsheetOnlyOfficeMountStale(mountSeq, assetId)
|
||||
) {
|
||||
spreadsheetOnlyOfficeHadLocalEdits = false
|
||||
scheduleSpreadsheetOnlyOfficeChangeSync(assetId)
|
||||
}
|
||||
upstreamEvents.onDocumentStateChange?.(event)
|
||||
}
|
||||
}
|
||||
spreadsheetOnlyOfficeEditor.value = new window.DocsAPI.DocEditor(
|
||||
spreadsheetOnlyOfficeHostId.value,
|
||||
config
|
||||
)
|
||||
if (isSpreadsheetOnlyOfficeMountStale(mountSeq, assetId)) {
|
||||
destroySpreadsheetOnlyOfficeEditor()
|
||||
}
|
||||
} catch (error) {
|
||||
if (isSpreadsheetOnlyOfficeMountStale(mountSeq, assetId)) {
|
||||
return
|
||||
}
|
||||
spreadsheetOnlyOfficeError.value = error?.message || '规则表加载失败,请稍后重试。'
|
||||
spreadsheetOnlyOfficeLoading.value = false
|
||||
toast(spreadsheetOnlyOfficeError.value)
|
||||
}
|
||||
}
|
||||
|
||||
function triggerSpreadsheetUpload() {
|
||||
if (!canUploadSpreadsheet.value) {
|
||||
return
|
||||
}
|
||||
spreadsheetUploadInput.value?.click?.()
|
||||
}
|
||||
|
||||
async function downloadSpreadsheetFile() {
|
||||
if (!canDownloadSpreadsheet.value || !selectedSkill.value?.id) {
|
||||
return
|
||||
}
|
||||
|
||||
actionState.value = 'download-spreadsheet'
|
||||
try {
|
||||
const blob = await fetchAgentAssetSpreadsheetBlob(
|
||||
selectedSkill.value.id,
|
||||
'attachment'
|
||||
)
|
||||
const objectUrl = URL.createObjectURL(blob)
|
||||
const anchor = document.createElement('a')
|
||||
anchor.href = objectUrl
|
||||
anchor.download = selectedSpreadsheetFileName.value || '规则表.xlsx'
|
||||
document.body.appendChild(anchor)
|
||||
anchor.click()
|
||||
anchor.remove()
|
||||
URL.revokeObjectURL(objectUrl)
|
||||
} catch (error) {
|
||||
toast(error?.message || '规则表下载失败,请稍后重试。')
|
||||
} finally {
|
||||
actionState.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
async function uploadSpreadsheetFile(file) {
|
||||
if (!file || !selectedSkill.value?.id || !canUploadSpreadsheet.value) {
|
||||
return
|
||||
}
|
||||
|
||||
actionState.value = 'upload-spreadsheet'
|
||||
try {
|
||||
await importAgentAssetSpreadsheetContent(selectedSkill.value.id, file, {
|
||||
actor: resolveActor()
|
||||
})
|
||||
await refreshCurrentAssets()
|
||||
await loadSelectedAssetDetail(selectedSkill.value.id)
|
||||
await loadSpreadsheetChangeRecords(selectedSkill.value.id)
|
||||
toast(`已导入 ${file.name} 的表格内容,右侧会记录本次修改。`)
|
||||
} catch (error) {
|
||||
toast(error?.message || '规则表内容导入失败,请稍后重试。')
|
||||
} finally {
|
||||
actionState.value = ''
|
||||
spreadsheetUploadInput.value?.reset?.()
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSpreadsheetFileInput(event) {
|
||||
await uploadSpreadsheetFile(event?.target?.files?.[0] || null)
|
||||
}
|
||||
|
||||
function openSpreadsheetChangeDetail(item) {
|
||||
if (!item?.changed_at) {
|
||||
return
|
||||
}
|
||||
selectedSpreadsheetChangeRecord.value = item
|
||||
spreadsheetChangeDetailOpen.value = true
|
||||
}
|
||||
|
||||
function closeSpreadsheetChangeDetail() {
|
||||
spreadsheetChangeDetailOpen.value = false
|
||||
}
|
||||
|
||||
return {
|
||||
spreadsheetUploadInput,
|
||||
spreadsheetOnlyOfficeLoading,
|
||||
spreadsheetOnlyOfficeError,
|
||||
spreadsheetOnlyOfficeReady,
|
||||
spreadsheetOnlyOfficeHostId,
|
||||
spreadsheetChangeDetailOpen,
|
||||
selectedSpreadsheetChangeRecord,
|
||||
selectedSpreadsheetChangeRecords,
|
||||
selectedSpreadsheetChangeSheetRows,
|
||||
selectedSpreadsheetChangeCellRows,
|
||||
destroySpreadsheetOnlyOfficeEditor,
|
||||
mountSpreadsheetOnlyOfficeEditor,
|
||||
triggerSpreadsheetUpload,
|
||||
downloadSpreadsheetFile,
|
||||
uploadSpreadsheetFile,
|
||||
handleSpreadsheetFileInput,
|
||||
loadSpreadsheetChangeRecords,
|
||||
openSpreadsheetChangeDetail,
|
||||
closeSpreadsheetChangeDetail
|
||||
}
|
||||
}
|
||||
169
web/src/views/scripts/useAuditVersionTimeline.js
Normal file
169
web/src/views/scripts/useAuditVersionTimeline.js
Normal file
@@ -0,0 +1,169 @@
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import { fetchAgentAssetVersionTimeline } from '../../services/agentAssets.js'
|
||||
import {
|
||||
buildDefaultRuntimeRule,
|
||||
formatDateTime,
|
||||
normalizeText,
|
||||
resolveRuleTemplateLabel,
|
||||
resolveTimelineEventMeta,
|
||||
stringifyRuntimeRule
|
||||
} from './auditViewModel.js'
|
||||
|
||||
const VERSION_TIMELINE_CACHE_TTL = 60 * 1000
|
||||
|
||||
function normalizeAssetId(assetId) {
|
||||
return normalizeText(assetId)
|
||||
}
|
||||
|
||||
function readVersionTimelineCache(timelineCache, assetId) {
|
||||
const key = normalizeAssetId(assetId)
|
||||
if (!key) {
|
||||
return null
|
||||
}
|
||||
|
||||
const cached = timelineCache.get(key)
|
||||
if (!cached) {
|
||||
return null
|
||||
}
|
||||
|
||||
const isExpired = Date.now() - cached.timestamp > VERSION_TIMELINE_CACHE_TTL
|
||||
return isExpired ? null : cached.items
|
||||
}
|
||||
|
||||
function writeVersionTimelineCache(timelineCache, assetId, items) {
|
||||
const key = normalizeAssetId(assetId)
|
||||
if (!key) {
|
||||
return
|
||||
}
|
||||
|
||||
timelineCache.set(key, {
|
||||
items,
|
||||
timestamp: Date.now()
|
||||
})
|
||||
}
|
||||
|
||||
function applyVersionPayloadToRulePreview(skill, version) {
|
||||
if (!skill || !version) {
|
||||
return
|
||||
}
|
||||
|
||||
const selectedVersion = version.version
|
||||
skill.displayVersion = selectedVersion
|
||||
skill.displayVersionChangeNote = version.note || '无版本说明'
|
||||
|
||||
if (skill.usesSpreadsheetRule) {
|
||||
return
|
||||
}
|
||||
|
||||
if (typeof version.markdownContent === 'string') {
|
||||
skill.markdownContent = version.markdownContent
|
||||
}
|
||||
|
||||
const runtimeRule = version.runtimeRule || buildDefaultRuntimeRule(skill)
|
||||
skill.runtimeRuleText = stringifyRuntimeRule(runtimeRule)
|
||||
skill.runtimeKind = normalizeText(runtimeRule.kind) || skill.runtimeKind || 'policy_rule_draft'
|
||||
skill.ruleTemplateKey = normalizeText(runtimeRule.template_key) || skill.ruleTemplateKey
|
||||
skill.ruleTemplateLabel = resolveRuleTemplateLabel(skill.ruleTemplateKey)
|
||||
}
|
||||
|
||||
export function useAuditVersionTimeline({ selectedSkill, toast }) {
|
||||
const versionSwitchTarget = ref(null)
|
||||
const versionTimelineOpen = ref(false)
|
||||
const versionTimelineLoading = ref(false)
|
||||
const versionTimelineError = ref('')
|
||||
const versionTimelineItems = ref([])
|
||||
const versionTimelineCache = new Map()
|
||||
|
||||
const selectedVersionTimelineItems = computed(() =>
|
||||
versionTimelineItems.value.map((item) => ({
|
||||
...item,
|
||||
meta: resolveTimelineEventMeta(item.event_type),
|
||||
timeLabel: formatDateTime(item.event_time)
|
||||
}))
|
||||
)
|
||||
|
||||
async function loadVersionTimeline(assetId = selectedSkill.value?.id, options = {}) {
|
||||
if (!assetId) {
|
||||
return
|
||||
}
|
||||
|
||||
const cachedItems = options.force ? null : readVersionTimelineCache(versionTimelineCache, assetId)
|
||||
if (cachedItems) {
|
||||
versionTimelineItems.value = cachedItems
|
||||
return
|
||||
}
|
||||
|
||||
versionTimelineLoading.value = true
|
||||
versionTimelineError.value = ''
|
||||
try {
|
||||
const payload = await fetchAgentAssetVersionTimeline(assetId)
|
||||
const nextItems = Array.isArray(payload) ? payload : []
|
||||
versionTimelineItems.value = nextItems
|
||||
writeVersionTimelineCache(versionTimelineCache, assetId, nextItems)
|
||||
} catch (error) {
|
||||
versionTimelineError.value = error?.message || '操作记录加载失败,请稍后重试。'
|
||||
if (!options.silent) {
|
||||
toast(versionTimelineError.value)
|
||||
}
|
||||
versionTimelineItems.value = []
|
||||
writeVersionTimelineCache(versionTimelineCache, assetId, [])
|
||||
} finally {
|
||||
versionTimelineLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function openVersionTimeline() {
|
||||
if (!selectedSkill.value?.id) {
|
||||
return
|
||||
}
|
||||
versionTimelineOpen.value = true
|
||||
await loadVersionTimeline(selectedSkill.value.id)
|
||||
}
|
||||
|
||||
function closeVersionTimeline() {
|
||||
versionTimelineOpen.value = false
|
||||
}
|
||||
|
||||
function clearVersionTimelineState() {
|
||||
versionTimelineOpen.value = false
|
||||
versionTimelineItems.value = []
|
||||
versionTimelineError.value = ''
|
||||
versionSwitchTarget.value = null
|
||||
}
|
||||
|
||||
function openVersionSwitch(version) {
|
||||
if (!selectedSkill.value || version.version === selectedSkill.value.displayVersion) {
|
||||
return
|
||||
}
|
||||
versionSwitchTarget.value = version
|
||||
}
|
||||
|
||||
function cancelVersionSwitch() {
|
||||
versionSwitchTarget.value = null
|
||||
}
|
||||
|
||||
function confirmVersionSwitch() {
|
||||
if (!selectedSkill.value || !versionSwitchTarget.value) {
|
||||
return
|
||||
}
|
||||
|
||||
applyVersionPayloadToRulePreview(selectedSkill.value, versionSwitchTarget.value)
|
||||
versionSwitchTarget.value = null
|
||||
}
|
||||
|
||||
return {
|
||||
versionSwitchTarget,
|
||||
versionTimelineOpen,
|
||||
versionTimelineLoading,
|
||||
versionTimelineError,
|
||||
selectedVersionTimelineItems,
|
||||
loadVersionTimeline,
|
||||
openVersionTimeline,
|
||||
closeVersionTimeline,
|
||||
clearVersionTimelineState,
|
||||
openVersionSwitch,
|
||||
cancelVersionSwitch,
|
||||
confirmVersionSwitch
|
||||
}
|
||||
}
|
||||
@@ -1043,8 +1043,32 @@ export default defineConfig({
|
||||
'/api': {
|
||||
target: `http://127.0.0.1:${process.env.SERVER_PORT || 8000}`,
|
||||
changeOrigin: true
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: [vue(), localSetupPlugin()]
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
build: {
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks(id) {
|
||||
if (!id.includes('node_modules')) {
|
||||
return undefined
|
||||
}
|
||||
if (id.includes('element-plus') || id.includes('@element-plus')) {
|
||||
return 'vendor-element-plus'
|
||||
}
|
||||
if (id.includes('echarts') || id.includes('zrender')) {
|
||||
return 'vendor-echarts'
|
||||
}
|
||||
if (id.includes('@vueuse')) {
|
||||
return 'vendor-vueuse'
|
||||
}
|
||||
if (id.includes('primeicons') || id.includes('primevue')) {
|
||||
return 'vendor-prime'
|
||||
}
|
||||
return 'vendor'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: [vue(), localSetupPlugin()]
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user