feat: 新增预算中心本体与风险规则评分回填
后端新增预算本体解析模块和风险规则评分回填服务,优化规则 生成本体对齐和提示词构建,增强费用类型关键词和本体验证, 完善报销查询和审计接口,前端预算中心页面增加对话框和本 体工具函数,重构审计页面元数据和视图模型,补充单元测试。
This commit is contained in:
@@ -31,7 +31,7 @@ import {
|
||||
updateAgentAsset
|
||||
} from '../../services/agentAssets.js'
|
||||
import { loadOnlyOfficeApi } from '../../services/onlyoffice.js'
|
||||
import { isFinanceUser, isManagerUser } from '../../utils/accessControl.js'
|
||||
import { isFinanceUser, isManagerUser, isPlatformAdminUser } from '../../utils/accessControl.js'
|
||||
import { buildOnlyOfficeEditorConfig } from './onlyOfficePreviewConfig.js'
|
||||
import {
|
||||
buildReviewNote,
|
||||
@@ -69,6 +69,7 @@ import {
|
||||
} from './auditViewModel.js'
|
||||
import {
|
||||
createDefaultRiskRuleForm,
|
||||
RISK_RULE_BUSINESS_STAGE_OPTIONS,
|
||||
RISK_RULE_EXPENSE_CATEGORY_OPTIONS
|
||||
} from './auditViewRiskRuleModel.js'
|
||||
|
||||
@@ -144,11 +145,11 @@ export default {
|
||||
financialRules: [],
|
||||
riskRules: [],
|
||||
skills: [],
|
||||
mcp: [],
|
||||
tasks: []
|
||||
mcp: []
|
||||
})
|
||||
|
||||
const isAdmin = computed(() => isManagerUser(currentUser.value))
|
||||
const isAdmin = computed(() => isPlatformAdminUser(currentUser.value))
|
||||
const isRuleManager = computed(() => isManagerUser(currentUser.value))
|
||||
const isFinance = computed(() => isFinanceUser(currentUser.value))
|
||||
const activeMeta = computed(() => TAB_META[activeType.value])
|
||||
const activeTabLabel = computed(() => activeMeta.value.label)
|
||||
@@ -162,7 +163,7 @@ export default {
|
||||
const showVersionColumn = computed(() => activeMeta.value.showVersionColumn !== false)
|
||||
const showStatusColumn = computed(() => activeMeta.value.showStatusColumn !== false)
|
||||
const showOnlineColumn = computed(() => false)
|
||||
const showEnabledColumn = computed(() => activeType.value === 'riskRules')
|
||||
const showEnabledColumn = computed(() => false)
|
||||
const selectedSkillIsRule = computed(() => selectedSkill.value?.type === 'rules')
|
||||
const selectedSkillUsesSpreadsheet = computed(
|
||||
() => selectedSkillIsRule.value && Boolean(selectedSkill.value?.usesSpreadsheetRule)
|
||||
@@ -171,6 +172,9 @@ export default {
|
||||
() => selectedSkillIsRule.value && Boolean(selectedSkill.value?.usesJsonRiskRule)
|
||||
)
|
||||
const canManageSelected = computed(
|
||||
() => isRuleManager.value && Boolean(selectedSkill.value) && !selectedSkill.value?.isPreviewMock
|
||||
)
|
||||
const canAdminOperateSelected = computed(
|
||||
() => isAdmin.value && Boolean(selectedSkill.value) && !selectedSkill.value?.isPreviewMock
|
||||
)
|
||||
const canEditSelected = computed(
|
||||
@@ -180,7 +184,7 @@ export default {
|
||||
(isAdmin.value || isFinance.value)
|
||||
)
|
||||
const canCreateRiskRule = computed(
|
||||
() => activeType.value === 'riskRules' && (isAdmin.value || isFinance.value) && !detailBusy.value
|
||||
() => activeType.value === 'riskRules' && isRuleManager.value && !detailBusy.value
|
||||
)
|
||||
const latestRiskRuleTestSummary = computed(() => selectedSkill.value?.latestTestSummary || null)
|
||||
const riskRuleTestPassed = computed(() => Boolean(latestRiskRuleTestSummary.value?.test_passed))
|
||||
@@ -196,27 +200,20 @@ export default {
|
||||
const canOpenRiskRuleTest = computed(
|
||||
() =>
|
||||
selectedSkillUsesJsonRisk.value &&
|
||||
canEditSelected.value &&
|
||||
canAdminOperateSelected.value &&
|
||||
Boolean(selectedSkill.value?.id) &&
|
||||
!riskRuleGenerationBusy.value &&
|
||||
!riskRuleGenerationFailed.value &&
|
||||
!detailBusy.value
|
||||
!riskRuleGenerationFailed.value
|
||||
)
|
||||
const canDeleteRiskRule = computed(
|
||||
() =>
|
||||
selectedSkillUsesJsonRisk.value &&
|
||||
canEditSelected.value &&
|
||||
canAdminOperateSelected.value &&
|
||||
Boolean(selectedSkill.value?.id) &&
|
||||
!normalizeText(selectedSkill.value?.publishedVersion).replace('-', '') &&
|
||||
!detailBusy.value
|
||||
!normalizeText(selectedSkill.value?.publishedVersion).replace('-', '')
|
||||
)
|
||||
const canOpenRiskRuleReviewSubmit = computed(
|
||||
() =>
|
||||
selectedSkillUsesJsonRisk.value &&
|
||||
canSubmitReview.value &&
|
||||
!riskRuleInReview.value &&
|
||||
!riskRuleGenerationBusy.value &&
|
||||
!riskRuleGenerationFailed.value
|
||||
() => false
|
||||
)
|
||||
const canSubmitRiskRuleReview = computed(
|
||||
() =>
|
||||
@@ -224,17 +221,14 @@ export default {
|
||||
riskRuleTestPassed.value
|
||||
)
|
||||
const canReturnRiskRule = computed(
|
||||
() => selectedSkillUsesJsonRisk.value && canManageSelected.value && riskRuleInReview.value
|
||||
() => false
|
||||
)
|
||||
const canPublishRiskRule = computed(
|
||||
() =>
|
||||
selectedSkillUsesJsonRisk.value &&
|
||||
canManageSelected.value &&
|
||||
riskRuleInReview.value &&
|
||||
riskRuleTestPassed.value
|
||||
false
|
||||
)
|
||||
const canToggleRiskRuleEnabled = computed(
|
||||
() => selectedSkillUsesJsonRisk.value && canManageSelected.value && !detailBusy.value
|
||||
() => selectedSkillUsesJsonRisk.value && canManageSelected.value
|
||||
)
|
||||
const riskRuleCreateBusy = computed(() => actionState.value === 'generate-risk-rule')
|
||||
const canEditMarkdown = computed(() => canEditSelected.value && selectedSkillIsRule.value)
|
||||
@@ -242,7 +236,11 @@ export default {
|
||||
() => selectedSkill.value?.displayVersion === selectedSkill.value?.workingVersion
|
||||
)
|
||||
const canSubmitReview = computed(
|
||||
() => canEditSelected.value && selectedSkillIsRule.value && isDisplayingWorkingVersion.value
|
||||
() =>
|
||||
!selectedSkillUsesJsonRisk.value &&
|
||||
canEditSelected.value &&
|
||||
selectedSkillIsRule.value &&
|
||||
isDisplayingWorkingVersion.value
|
||||
)
|
||||
const hasReviewSubmitReviewers = computed(() => reviewSubmitReviewerOptions.value.length > 0)
|
||||
const canReviewSelected = computed(
|
||||
@@ -370,7 +368,7 @@ export default {
|
||||
)
|
||||
const showStatusFilter = computed(() => true)
|
||||
const showOnlineFilter = computed(() => false)
|
||||
const showEnabledFilter = computed(() => activeType.value === 'riskRules')
|
||||
const showEnabledFilter = computed(() => false)
|
||||
const selectedRiskScenarioLabel = computed(
|
||||
() =>
|
||||
RISK_SCENARIO_OPTIONS.find((item) => item.value === selectedRiskScenario.value)?.label ||
|
||||
@@ -646,6 +644,7 @@ export default {
|
||||
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),
|
||||
@@ -1007,8 +1006,13 @@ export default {
|
||||
}
|
||||
|
||||
async function loadAssets(options = {}) {
|
||||
loading.value = true
|
||||
errorMessage.value = ''
|
||||
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 })
|
||||
@@ -1037,6 +1041,9 @@ export default {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (options.silent || options.background) {
|
||||
return
|
||||
}
|
||||
if (activeMeta.value.assetType === 'rule') {
|
||||
assetBuckets.value = {
|
||||
...assetBuckets.value,
|
||||
@@ -1056,12 +1063,14 @@ export default {
|
||||
toast(errorMessage.value)
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
if (shouldShowLoading) {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshCurrentAssets() {
|
||||
await loadAssets({ force: true, silent: true })
|
||||
await loadAssets({ force: true, silent: true, background: true })
|
||||
}
|
||||
|
||||
async function loadSelectedAssetDetail(assetId) {
|
||||
@@ -1110,6 +1119,39 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
async function loadRiskRuleJson(assetId) {
|
||||
if (!assetId || !selectedSkill.value?.usesJsonRiskRule) {
|
||||
return
|
||||
@@ -1525,6 +1567,9 @@ export default {
|
||||
}
|
||||
|
||||
function openRiskRuleTestDialog() {
|
||||
if (detailBusy.value) {
|
||||
return
|
||||
}
|
||||
if (!canOpenRiskRuleTest.value) {
|
||||
if (!selectedSkill.value?.id) {
|
||||
toast('规则详情还没有加载完成,请稍后再测试。')
|
||||
@@ -1544,7 +1589,8 @@ export default {
|
||||
}
|
||||
await refreshCurrentAssets()
|
||||
if (selectedSkill.value?.id) {
|
||||
await loadSelectedAssetDetail(selectedSkill.value.id)
|
||||
const detail = await fetchAgentAssetDetail(selectedSkill.value.id)
|
||||
mergeSelectedRuleLifecycle(detail)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1659,15 +1705,15 @@ export default {
|
||||
return
|
||||
}
|
||||
const assetId = selectedSkill.value.id
|
||||
const nextEnabled = !selectedSkill.value.isEnabledValue
|
||||
const nextEnabled = !selectedSkill.value.isOnlineValue
|
||||
actionState.value = 'toggle-risk-rule-enabled'
|
||||
try {
|
||||
await setRiskRuleAssetEnabled(assetId, nextEnabled, { actor: resolveActor() })
|
||||
const detail = await setRiskRuleAssetEnabled(assetId, nextEnabled, { actor: resolveActor() })
|
||||
mergeSelectedRuleLifecycle(detail)
|
||||
await refreshCurrentAssets()
|
||||
await loadSelectedAssetDetail(assetId)
|
||||
toast(nextEnabled ? '风险规则已启用。' : '风险规则已停用,不会进入业务扫描。')
|
||||
toast(nextEnabled ? '风险规则已上线。' : '风险规则已下线,不会进入业务扫描。')
|
||||
} catch (error) {
|
||||
toast(error?.message || '风险规则启用状态更新失败,请稍后重试。')
|
||||
toast(error?.message || '风险规则上线状态更新失败,请稍后重试。')
|
||||
} finally {
|
||||
actionState.value = ''
|
||||
}
|
||||
@@ -1851,6 +1897,7 @@ export default {
|
||||
riskRuleReturnOpen,
|
||||
riskRulePublishOpen,
|
||||
riskRuleReturnNote,
|
||||
riskRuleBusinessStageOptions: RISK_RULE_BUSINESS_STAGE_OPTIONS,
|
||||
riskRuleExpenseCategoryOptions: RISK_RULE_EXPENSE_CATEGORY_OPTIONS,
|
||||
showReviewNote,
|
||||
spreadsheetUploadInput,
|
||||
|
||||
@@ -1,7 +1,18 @@
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
|
||||
import BudgetTrendChart from '../../components/charts/BudgetTrendChart.vue'
|
||||
import { fetchEmployeeMeta } from '../../services/employees.js'
|
||||
import {
|
||||
BUDGET_CONTROL_ACTION_OPTIONS,
|
||||
BUDGET_EXPENSE_TYPE_OPTIONS,
|
||||
BUDGET_QUARTER_OPTIONS,
|
||||
BUDGET_STATUS_OPTIONS,
|
||||
BUDGET_WARNING_OPTIONS,
|
||||
BUDGET_YEAR_OPTIONS,
|
||||
buildBudgetOntologyContext,
|
||||
formatBudgetPeriod,
|
||||
resolveBudgetExpenseTypeLabel
|
||||
} from '../../utils/budgetOntology.js'
|
||||
|
||||
const FALLBACK_DEPARTMENTS = [
|
||||
{ code: 'MARKET-DEPT', name: '市场部', costCenter: 'CC-4100' },
|
||||
@@ -12,13 +23,34 @@ const FALLBACK_DEPARTMENTS = [
|
||||
{ code: 'PRESIDENT-OFFICE', name: '总裁办', costCenter: 'CC-1000' }
|
||||
]
|
||||
|
||||
const EXPENSE_BLUEPRINTS = [
|
||||
{ expenseType: '市场推广费', total: 500000, used: 186400, occupied: 120000, warning: 80, action: '提醒' },
|
||||
{ expenseType: '差旅费', total: 600000, used: 242300, occupied: 150000, warning: 80, action: '提醒' },
|
||||
{ expenseType: '办公费', total: 300000, used: 68500, occupied: 60000, warning: 70, action: '正常' },
|
||||
{ expenseType: '培训费', total: 200000, used: 42300, occupied: 20000, warning: 70, action: '正常' },
|
||||
{ expenseType: '软件服务费', total: 600000, used: 249500, occupied: 240800, warning: 80, action: '管控' }
|
||||
]
|
||||
const EXPENSE_BUDGET_SEED = {
|
||||
travel: { total: 600000, used: 242300, occupied: 150000, warning: 80, action: '提醒' },
|
||||
hotel: { total: 360000, used: 139800, occupied: 84000, warning: 80, action: '提醒' },
|
||||
transport: { total: 280000, used: 104600, occupied: 56000, warning: 75, action: '提醒' },
|
||||
meal: { total: 420000, used: 168200, occupied: 118000, warning: 80, action: '管控' },
|
||||
meeting: { total: 260000, used: 84500, occupied: 52000, warning: 75, action: '提醒' },
|
||||
marketing: { total: 500000, used: 186400, occupied: 120000, warning: 80, action: '提醒' },
|
||||
office: { total: 300000, used: 68500, occupied: 60000, warning: 70, action: '正常' },
|
||||
training: { total: 200000, used: 42300, occupied: 20000, warning: 70, action: '正常' },
|
||||
software: { total: 600000, used: 249500, occupied: 240800, warning: 80, action: '管控' },
|
||||
communication: { total: 120000, used: 38600, occupied: 18000, warning: 70, action: '正常' },
|
||||
welfare: { total: 240000, used: 96500, occupied: 42000, warning: 75, action: '提醒' }
|
||||
}
|
||||
|
||||
const DEFAULT_EXPENSE_BUDGET = {
|
||||
total: 100000,
|
||||
used: 0,
|
||||
occupied: 0,
|
||||
warning: 70,
|
||||
action: '正常'
|
||||
}
|
||||
|
||||
const EXPENSE_BLUEPRINTS = BUDGET_EXPENSE_TYPE_OPTIONS.map((option) => ({
|
||||
...DEFAULT_EXPENSE_BUDGET,
|
||||
...EXPENSE_BUDGET_SEED[option.value],
|
||||
budgetSubjectCode: option.value,
|
||||
expenseType: option.label
|
||||
}))
|
||||
|
||||
const currency = (value) =>
|
||||
Number(value || 0).toLocaleString('zh-CN', {
|
||||
@@ -26,14 +58,29 @@ const currency = (value) =>
|
||||
maximumFractionDigits: 2
|
||||
})
|
||||
|
||||
const comparison = (value, direction) => ({
|
||||
value,
|
||||
tone: direction === 'down' ? 'down' : 'up',
|
||||
icon: direction === 'down' ? 'mdi mdi-arrow-down' : 'mdi mdi-arrow-up'
|
||||
})
|
||||
|
||||
const parseBudgetAmount = (value) => Number(String(value || '').replace(/[^\d.-]/g, '')) || 0
|
||||
const makeBudgetRowId = () => `budget-row-${Date.now()}-${Math.random().toString(16).slice(2)}`
|
||||
const BUDGET_PAGE_SIZE_OPTIONS = [5, 10]
|
||||
|
||||
function buildDepartmentRows(departmentCode) {
|
||||
const seed = Array.from(String(departmentCode || '')).reduce((sum, char) => sum + char.charCodeAt(0), 0)
|
||||
const seed = Array.from(String(departmentCode || '')).reduce(
|
||||
(sum, char) => sum + char.charCodeAt(0),
|
||||
0
|
||||
)
|
||||
const factor = 0.88 + (seed % 18) / 100
|
||||
|
||||
return EXPENSE_BLUEPRINTS.map((item, index) => {
|
||||
const totalAmount = Math.round(item.total * factor)
|
||||
const usedAmount = Math.round(item.used * (0.9 + ((seed + index) % 12) / 100))
|
||||
const occupiedAmount = Math.round(item.occupied * (0.92 + ((seed + index * 3) % 10) / 100))
|
||||
const occupiedAmount = Math.round(
|
||||
item.occupied * (0.92 + ((seed + index * 3) % 10) / 100)
|
||||
)
|
||||
const leftAmount = Math.max(totalAmount - usedAmount - occupiedAmount, 0)
|
||||
const rate = Number((((usedAmount + occupiedAmount) / totalAmount) * 100).toFixed(2))
|
||||
|
||||
@@ -80,18 +127,36 @@ export default {
|
||||
const activeDepartmentCode = ref(FALLBACK_DEPARTMENTS[0].code)
|
||||
const departmentKeyword = ref('')
|
||||
const filters = ref({
|
||||
period: '2026年度',
|
||||
year: '2026',
|
||||
quarter: 'Q1',
|
||||
expenseType: '全部',
|
||||
status: '全部'
|
||||
})
|
||||
const budgetPage = ref(1)
|
||||
const budgetPageSize = ref(5)
|
||||
const budgetEditOpen = ref(false)
|
||||
const budgetEditForm = ref({
|
||||
budgetYear: '2026',
|
||||
budgetQuarter: 'Q1',
|
||||
budgetPeriod: '2026年Q1',
|
||||
departmentCode: FALLBACK_DEPARTMENTS[0].code,
|
||||
costCenter: FALLBACK_DEPARTMENTS[0].costCenter,
|
||||
budgetOwner: '张晓明',
|
||||
budgetVersion: 'V1.0(初始版本)',
|
||||
budgetStatus: '编制中',
|
||||
budgetDescription: ''
|
||||
})
|
||||
const budgetEditRows = ref([])
|
||||
|
||||
const activeDepartment = computed(() =>
|
||||
departments.value.find((item) => item.code === activeDepartmentCode.value) || departments.value[0]
|
||||
)
|
||||
|
||||
const activeDepartmentName = computed(() => activeDepartment.value?.name || '市场部')
|
||||
const departmentRows = computed(() => buildDepartmentRows(activeDepartment.value?.code || activeDepartmentCode.value))
|
||||
const visibleBudgetRows = computed(() =>
|
||||
const departmentRows = computed(() =>
|
||||
buildDepartmentRows(activeDepartment.value?.code || activeDepartmentCode.value)
|
||||
)
|
||||
const filteredBudgetRows = computed(() =>
|
||||
departmentRows.value
|
||||
.filter((row) => filters.value.expenseType === '全部' || row.expenseType === filters.value.expenseType)
|
||||
.filter((row) => {
|
||||
@@ -101,6 +166,21 @@ export default {
|
||||
return row.rateTone === 'ok'
|
||||
})
|
||||
)
|
||||
const totalBudgetRows = computed(() => filteredBudgetRows.value.length)
|
||||
const totalBudgetPages = computed(() =>
|
||||
Math.max(1, Math.ceil(totalBudgetRows.value / Number(budgetPageSize.value || 5)))
|
||||
)
|
||||
const currentBudgetPage = computed(() =>
|
||||
Math.min(Math.max(1, budgetPage.value), totalBudgetPages.value)
|
||||
)
|
||||
const budgetPageNumbers = computed(() =>
|
||||
Array.from({ length: totalBudgetPages.value }, (_, index) => index + 1)
|
||||
)
|
||||
const visibleBudgetRows = computed(() => {
|
||||
const pageSize = Number(budgetPageSize.value || 5)
|
||||
const start = (currentBudgetPage.value - 1) * pageSize
|
||||
return filteredBudgetRows.value.slice(start, start + pageSize)
|
||||
})
|
||||
|
||||
const totals = computed(() => {
|
||||
const rows = departmentRows.value
|
||||
@@ -119,30 +199,34 @@ export default {
|
||||
{
|
||||
label: '预算总额',
|
||||
value: `¥${currency(totals.value.total)}`,
|
||||
note: '本年累计',
|
||||
yoy: comparison('+8.42%', 'up'),
|
||||
mom: comparison('+2.16%', 'up'),
|
||||
tone: 'green',
|
||||
icon: 'mdi mdi-wallet-outline'
|
||||
},
|
||||
{
|
||||
label: '已发生',
|
||||
value: `¥${currency(totals.value.used)}`,
|
||||
note: `占比 ${((totals.value.used / totals.value.total) * 100).toFixed(2)}%`,
|
||||
yoy: comparison('+12.68%', 'up'),
|
||||
mom: comparison('+4.35%', 'up'),
|
||||
tone: 'blue',
|
||||
icon: 'mdi mdi-chart-line'
|
||||
},
|
||||
{
|
||||
label: '已占用',
|
||||
value: `¥${currency(totals.value.occupied)}`,
|
||||
note: `占比 ${((totals.value.occupied / totals.value.total) * 100).toFixed(2)}%`,
|
||||
yoy: comparison('+6.37%', 'up'),
|
||||
mom: comparison('-1.84%', 'down'),
|
||||
tone: 'orange',
|
||||
icon: 'mdi mdi-briefcase-check-outline'
|
||||
},
|
||||
{
|
||||
label: '剩余可用',
|
||||
value: `¥${currency(totals.value.left)}`,
|
||||
note: `占比 ${((totals.value.left / totals.value.total) * 100).toFixed(2)}%`,
|
||||
yoy: comparison('-3.26%', 'down'),
|
||||
mom: comparison('-2.08%', 'down'),
|
||||
tone: 'green',
|
||||
icon: 'mdi mdi-currency-cny'
|
||||
icon: 'mdi mdi-cash'
|
||||
}
|
||||
])
|
||||
|
||||
@@ -170,6 +254,103 @@ export default {
|
||||
)
|
||||
|
||||
const trendData = computed(() => buildTrendData(departmentRows.value))
|
||||
const budgetEditTotal = computed(() =>
|
||||
currency(
|
||||
budgetEditRows.value.reduce(
|
||||
(sum, row) => sum + parseBudgetAmount(row.budgetAmount),
|
||||
0
|
||||
)
|
||||
)
|
||||
)
|
||||
const budgetOntologyContext = computed(() =>
|
||||
buildBudgetOntologyContext({
|
||||
form: budgetEditForm.value,
|
||||
rows: budgetEditRows.value,
|
||||
departments: departments.value
|
||||
})
|
||||
)
|
||||
|
||||
function buildEditableRows() {
|
||||
return departmentRows.value.map((row) => ({
|
||||
id: makeBudgetRowId(),
|
||||
budgetSubject: row.expenseType,
|
||||
budgetSubjectCode: row.budgetSubjectCode || '',
|
||||
budgetAmount: currency(row.totalAmount),
|
||||
warningThreshold: `${row.warning}%`,
|
||||
controlAction: row.action,
|
||||
budgetRemark: `${row.expenseType}相关费用`
|
||||
}))
|
||||
}
|
||||
|
||||
function resolveNextExpenseTypeOption() {
|
||||
const usedCodes = new Set(budgetEditRows.value.map((row) => row.budgetSubjectCode))
|
||||
return (
|
||||
BUDGET_EXPENSE_TYPE_OPTIONS.find((item) => !usedCodes.has(item.value)) ||
|
||||
BUDGET_EXPENSE_TYPE_OPTIONS[0]
|
||||
)
|
||||
}
|
||||
|
||||
function syncBudgetRowSubject(row) {
|
||||
row.budgetSubject = resolveBudgetExpenseTypeLabel(row.budgetSubjectCode, row.budgetSubject)
|
||||
}
|
||||
|
||||
function openBudgetEditDialog() {
|
||||
const department = activeDepartment.value
|
||||
const budgetPeriod = formatBudgetPeriod(filters.value.year, filters.value.quarter)
|
||||
budgetEditForm.value = {
|
||||
budgetYear: filters.value.year,
|
||||
budgetQuarter: filters.value.quarter,
|
||||
budgetPeriod,
|
||||
departmentCode: department?.code || activeDepartmentCode.value,
|
||||
costCenter: department?.costCenter || '',
|
||||
budgetOwner: '张晓明',
|
||||
budgetVersion: 'V1.0(初始版本)',
|
||||
budgetStatus: '编制中',
|
||||
budgetDescription: `${department?.name || '当前部门'}2026年度预算编制,用于指导费用支出及控制成本,确保资源合理使用。`
|
||||
}
|
||||
budgetEditRows.value = buildEditableRows()
|
||||
budgetEditOpen.value = true
|
||||
}
|
||||
|
||||
function closeBudgetEditDialog() {
|
||||
budgetEditOpen.value = false
|
||||
}
|
||||
|
||||
function addBudgetDetailRow() {
|
||||
const option = resolveNextExpenseTypeOption()
|
||||
budgetEditRows.value.push({
|
||||
id: makeBudgetRowId(),
|
||||
budgetSubject: option.label,
|
||||
budgetSubjectCode: option.value,
|
||||
budgetAmount: '0.00',
|
||||
warningThreshold: '70%',
|
||||
controlAction: '正常',
|
||||
budgetRemark: ''
|
||||
})
|
||||
}
|
||||
|
||||
function removeBudgetDetailRow(rowId) {
|
||||
if (budgetEditRows.value.length <= 1) return
|
||||
budgetEditRows.value = budgetEditRows.value.filter((row) => row.id !== rowId)
|
||||
}
|
||||
|
||||
function goToBudgetPage(page) {
|
||||
budgetPage.value = Math.min(Math.max(Number(page) || 1, 1), totalBudgetPages.value)
|
||||
}
|
||||
|
||||
function changeBudgetPage(direction) {
|
||||
goToBudgetPage(currentBudgetPage.value + direction)
|
||||
}
|
||||
|
||||
function saveBudgetDraft() {
|
||||
budgetEditForm.value.budgetStatus = '编制中'
|
||||
closeBudgetEditDialog()
|
||||
}
|
||||
|
||||
function publishBudget() {
|
||||
budgetEditForm.value.budgetStatus = '已发布'
|
||||
closeBudgetEditDialog()
|
||||
}
|
||||
|
||||
async function loadDepartments() {
|
||||
try {
|
||||
@@ -198,19 +379,65 @@ export default {
|
||||
void loadDepartments()
|
||||
})
|
||||
|
||||
watch(
|
||||
[
|
||||
activeDepartmentCode,
|
||||
budgetPageSize,
|
||||
() => filters.value.year,
|
||||
() => filters.value.quarter,
|
||||
() => filters.value.expenseType,
|
||||
() => filters.value.status
|
||||
],
|
||||
() => {
|
||||
budgetPage.value = 1
|
||||
}
|
||||
)
|
||||
|
||||
watch(totalBudgetPages, (pages) => {
|
||||
if (budgetPage.value > pages) {
|
||||
budgetPage.value = pages
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
activeDepartmentCode,
|
||||
activeDepartmentName,
|
||||
addBudgetDetailRow,
|
||||
budgetEditForm,
|
||||
budgetEditOpen,
|
||||
budgetEditRows,
|
||||
budgetEditTotal,
|
||||
budgetMetrics,
|
||||
budgetOntologyContext,
|
||||
budgetPage: currentBudgetPage,
|
||||
budgetPageNumbers,
|
||||
budgetPageSize,
|
||||
budgetPageSizeOptions: BUDGET_PAGE_SIZE_OPTIONS,
|
||||
closeBudgetEditDialog,
|
||||
controlActionOptions: BUDGET_CONTROL_ACTION_OPTIONS,
|
||||
changeBudgetPage,
|
||||
departmentKeyword,
|
||||
departments,
|
||||
expenseTypeOptions: BUDGET_EXPENSE_TYPE_OPTIONS,
|
||||
expenseTypes: ['全部', ...EXPENSE_BLUEPRINTS.map((item) => item.expenseType)],
|
||||
filters,
|
||||
periods: ['2026年度', '2026年Q2', '2026年5月'],
|
||||
openBudgetEditDialog,
|
||||
quarters: BUDGET_QUARTER_OPTIONS,
|
||||
publishBudget,
|
||||
removeBudgetDetailRow,
|
||||
saveBudgetDraft,
|
||||
statusOptions: BUDGET_STATUS_OPTIONS,
|
||||
statuses: ['全部', '正常', '预警', '管控'],
|
||||
syncBudgetRowSubject,
|
||||
goToBudgetPage,
|
||||
totalBudgetPages,
|
||||
totalBudgetRows,
|
||||
trendData,
|
||||
visibleBudgetRows,
|
||||
visibleDepartments,
|
||||
warnings
|
||||
warningOptions: BUDGET_WARNING_OPTIONS,
|
||||
warnings,
|
||||
years: BUDGET_YEAR_OPTIONS
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,23 +56,6 @@ export const TYPE_META = {
|
||||
version: '当前版本',
|
||||
metric: '超时配置'
|
||||
}
|
||||
},
|
||||
tasks: {
|
||||
assetType: 'task',
|
||||
label: '任务',
|
||||
typeLabel: '任务',
|
||||
createButtonLabel: '任务已接入',
|
||||
hintText: '任务页签已接到真实资产 API,可查看调度周期、执行 Agent 和最近执行结果。',
|
||||
searchPlaceholder: '搜索任务名称、编码或负责人',
|
||||
tableColumns: {
|
||||
name: '任务名称',
|
||||
category: '业务域',
|
||||
owner: '负责人',
|
||||
scope: '适用场景',
|
||||
runtime: '调度周期',
|
||||
version: '当前版本',
|
||||
metric: '执行 Agent'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,20 +96,15 @@ export const TAB_META = {
|
||||
...TYPE_META.mcp,
|
||||
typeKey: 'mcp',
|
||||
badgeTone: 'amber'
|
||||
},
|
||||
tasks: {
|
||||
...TYPE_META.tasks,
|
||||
typeKey: 'tasks',
|
||||
badgeTone: 'violet'
|
||||
}
|
||||
}
|
||||
|
||||
export const STATUS_META = {
|
||||
generating: { label: '生成中', tone: 'info' },
|
||||
draft: { label: '草稿中', tone: 'draft' },
|
||||
draft: { label: '待上线', tone: 'draft' },
|
||||
review: { label: '待审核', tone: 'warning' },
|
||||
active: { label: '已上线', tone: 'success' },
|
||||
disabled: { label: '已停用', tone: 'disabled' },
|
||||
disabled: { label: '已下线', tone: 'disabled' },
|
||||
failed: { label: '生成失败', tone: 'danger' }
|
||||
}
|
||||
|
||||
@@ -230,34 +208,16 @@ export const DETAIL_TITLES = {
|
||||
historyDesc: '最近版本记录',
|
||||
publishTitle: '服务状态',
|
||||
publishDesc: 'MCP 资产已接入规则中心,但真实外部调用仍以后续链路集成为准。'
|
||||
},
|
||||
tasks: {
|
||||
configTitle: '任务配置',
|
||||
configDesc: '展示调度周期、执行 Agent 和任务编码。',
|
||||
detailTitle: '任务结构',
|
||||
detailDesc: '按调度计划、目标场景和运行结果组织任务信息。',
|
||||
outputTitle: '运行要求',
|
||||
outputDesc: '任务详情重点展示调度 Agent、最近运行结果和运行日志入口。',
|
||||
ruleListTitle: '运行要求',
|
||||
checkListTitle: '最近执行',
|
||||
triggerTitle: '适用场景',
|
||||
triggerDesc: '当前任务覆盖的业务场景',
|
||||
toolTitle: '最近调用',
|
||||
toolDesc: '根据 AgentRun 中的最近执行记录回显任务运行情况',
|
||||
historyTitle: '版本历史',
|
||||
historyDesc: '最近版本记录',
|
||||
publishTitle: '调度状态',
|
||||
publishDesc: '任务资产已接入规则中心,后续 Day 4 运行时会继续消费这些配置。'
|
||||
}
|
||||
}
|
||||
|
||||
export const STATUS_OPTIONS = [
|
||||
{ value: '', label: '全部状态' },
|
||||
{ value: 'generating', label: '生成中' },
|
||||
{ value: 'draft', label: '草稿中' },
|
||||
{ value: 'draft', label: '待上线' },
|
||||
{ value: 'review', label: '待审核' },
|
||||
{ value: 'active', label: '已上线' },
|
||||
{ value: 'disabled', label: '已停用' },
|
||||
{ value: 'disabled', label: '已下线' },
|
||||
{ value: 'failed', label: '生成失败' }
|
||||
]
|
||||
|
||||
|
||||
@@ -207,6 +207,65 @@ export function resolveRiskRuleEnabled(source, rulePayload = null) {
|
||||
return true
|
||||
}
|
||||
|
||||
const LAST_OPERATION_LABELS = {
|
||||
generate: '开始生成',
|
||||
create: '创建',
|
||||
test: '测试',
|
||||
online: '上线',
|
||||
offline: '下线',
|
||||
delete: '删除',
|
||||
update: '更新'
|
||||
}
|
||||
|
||||
const RISK_RULE_BUSINESS_STAGE_LABELS = {
|
||||
expense_application: '费用申请',
|
||||
reimbursement: '费用报销'
|
||||
}
|
||||
|
||||
function resolveRiskRuleBusinessStage(source, rulePayload = null) {
|
||||
const configJson = readConfigJson(source)
|
||||
const metadata = rulePayload && typeof rulePayload === 'object' ? rulePayload.metadata || {} : {}
|
||||
const stage =
|
||||
normalizeText(configJson.business_stage) ||
|
||||
normalizeText(metadata.business_stage) ||
|
||||
normalizeText(rulePayload?.business_stage)
|
||||
const label =
|
||||
normalizeText(configJson.business_stage_label) ||
|
||||
normalizeText(metadata.business_stage_label) ||
|
||||
RISK_RULE_BUSINESS_STAGE_LABELS[stage]
|
||||
return {
|
||||
value: stage || 'reimbursement',
|
||||
label: label || '费用报销'
|
||||
}
|
||||
}
|
||||
|
||||
function resolveRiskRuleOnlineMeta(statusValue) {
|
||||
if (statusValue === 'active') {
|
||||
return { label: '已上线', tone: 'success', online: true }
|
||||
}
|
||||
if (statusValue === 'disabled') {
|
||||
return { label: '已下线', tone: 'disabled', online: false }
|
||||
}
|
||||
if (statusValue === 'generating') {
|
||||
return { label: '生成中', tone: 'info', online: false }
|
||||
}
|
||||
if (statusValue === 'failed') {
|
||||
return { label: '生成失败', tone: 'danger', online: false }
|
||||
}
|
||||
return { label: '待上线', tone: 'draft', online: false }
|
||||
}
|
||||
|
||||
function resolveLastOperationLabel(source, fallback = {}) {
|
||||
const configJson = readConfigJson(source)
|
||||
const operation = isPlainObject(configJson.last_operation) ? configJson.last_operation : {}
|
||||
const action = normalizeText(operation.action) || normalizeText(fallback.action) || 'create'
|
||||
const actor = normalizeText(operation.actor) || normalizeText(fallback.actor) || '系统'
|
||||
const at = normalizeText(operation.at) || normalizeText(fallback.at)
|
||||
const actionLabel = LAST_OPERATION_LABELS[action] || action
|
||||
const timeLabel = formatDateTime(at)
|
||||
return timeLabel && timeLabel !== '-' ? `${actionLabel}:${actor} · ${timeLabel}` : `${actionLabel}:${actor}`
|
||||
}
|
||||
|
||||
export function readRuleDocumentMeta(value) {
|
||||
const configJson = readConfigJson(value)
|
||||
return isPlainObject(configJson.rule_document) ? configJson.rule_document : null
|
||||
@@ -458,12 +517,13 @@ export function applyRiskRuleJsonState(target, payload, apiPayload) {
|
||||
normalizeText(apiConfig.expense_category_label) ||
|
||||
normalizeText(rulePayload.risk_category) ||
|
||||
resolveRiskRuleCategory({ ...target, risk_category: rulePayload.risk_category, config_json: rulePayload })
|
||||
const businessStage = resolveRiskRuleBusinessStage(target, rulePayload)
|
||||
const riskRuleFields = resolveRiskRuleFields(rulePayload)
|
||||
const riskRuleCreatedAt = resolveRiskRuleCreatedAt(rulePayload, target.createdAt || target.updatedAt)
|
||||
const riskRuleScoreLevel = resolveRiskRuleScoreLevel(rulePayload, apiConfig)
|
||||
|
||||
const statusValue = apiPayload?.status || target.statusValue || 'draft'
|
||||
const isOnlineLabel = statusValue === 'active' ? '是' : '否'
|
||||
const onlineMeta = resolveRiskRuleOnlineMeta(statusValue)
|
||||
const isEnabledValue = resolveRiskRuleEnabled(target, rulePayload)
|
||||
|
||||
const publisher =
|
||||
@@ -488,6 +548,8 @@ export function applyRiskRuleJsonState(target, payload, apiPayload) {
|
||||
riskRuleBusinessDescription: resolveRiskRuleBusinessDescription(rulePayload, fullDescription),
|
||||
riskRuleSubtitle: buildRiskListSubtitle(fullDescription, 48),
|
||||
riskCategory,
|
||||
businessStageValue: businessStage.value,
|
||||
businessStageLabel: businessStage.label,
|
||||
scope: riskCategory,
|
||||
riskRuleSourceRef: resolveRiskRuleSourceRef(rulePayload),
|
||||
riskRuleSeverity: riskRuleScoreLevel || resolveRiskRuleSeverity(rulePayload),
|
||||
@@ -521,10 +583,16 @@ export function applyRiskRuleJsonState(target, payload, apiPayload) {
|
||||
outcomes: apiPayload?.outcomes || rulePayload.outcomes || {}
|
||||
},
|
||||
riskRuleJsonText: JSON.stringify(rulePayload, null, 2),
|
||||
isOnlineLabel,
|
||||
isOnlineValue: onlineMeta.online,
|
||||
isOnlineLabel: onlineMeta.label,
|
||||
isOnlineTone: onlineMeta.tone,
|
||||
isEnabledValue,
|
||||
isEnabledLabel: isEnabledValue ? '是' : '否',
|
||||
isEnabledTone: isEnabledValue ? 'success' : 'disabled',
|
||||
lastOperationLabel: resolveLastOperationLabel(target, {
|
||||
actor: publisher,
|
||||
at: riskRuleCreatedAt
|
||||
}),
|
||||
publisher,
|
||||
publishedAt
|
||||
}
|
||||
@@ -747,7 +815,7 @@ export function resolveTypeKey(assetType) {
|
||||
if (assetType === 'mcp') {
|
||||
return 'mcp'
|
||||
}
|
||||
return 'tasks'
|
||||
return ''
|
||||
}
|
||||
|
||||
export function formatSeverity(value) {
|
||||
@@ -778,23 +846,6 @@ export function formatOutputSummary(items) {
|
||||
return `${items.length} 项输出`
|
||||
}
|
||||
|
||||
export function formatTaskRisk(scenarios) {
|
||||
if (Array.isArray(scenarios) && scenarios.includes('risk_check')) {
|
||||
return '高风险'
|
||||
}
|
||||
if (
|
||||
Array.isArray(scenarios) &&
|
||||
(scenarios.includes('accounts_receivable') || scenarios.includes('accounts_payable'))
|
||||
) {
|
||||
return '中风险'
|
||||
}
|
||||
return '常规'
|
||||
}
|
||||
|
||||
export function findLatestTaskRun(runs, assetId) {
|
||||
return runs.find((item) => item.task_id === assetId) || null
|
||||
}
|
||||
|
||||
export function findLatestMcpCall(runs, assetCode) {
|
||||
const expectedToolName = normalizeText(assetCode).replace(/^mcp\./, '')
|
||||
|
||||
@@ -827,7 +878,7 @@ export function buildRowRuntime(asset, typeKey) {
|
||||
if (typeKey === 'mcp') {
|
||||
return normalizeText(asset.config_json?.endpoint) || '未配置地址'
|
||||
}
|
||||
return normalizeText(asset.config_json?.cron) || '未配置调度'
|
||||
return ''
|
||||
}
|
||||
|
||||
export function buildRowMetric(asset, typeKey) {
|
||||
@@ -840,7 +891,7 @@ export function buildRowMetric(asset, typeKey) {
|
||||
if (typeKey === 'mcp') {
|
||||
return asset.config_json?.timeout_ms ? `${asset.config_json.timeout_ms} ms` : '未配置超时'
|
||||
}
|
||||
return normalizeText(asset.config_json?.agent) || '未配置 Agent'
|
||||
return ''
|
||||
}
|
||||
|
||||
export function formatSpreadsheetChangeSummary(summary) {
|
||||
@@ -885,7 +936,8 @@ export function buildListItem(asset) {
|
||||
const listSubtitle = isRiskRule
|
||||
? buildRiskListSubtitle(asset.description)
|
||||
: normalizeText(asset.description)
|
||||
const isOnlineValue = asset.status === 'active'
|
||||
const onlineMeta = resolveRiskRuleOnlineMeta(asset.status)
|
||||
const isOnlineValue = onlineMeta.online
|
||||
const isEnabledValue = usesJsonRiskRule ? resolveRiskRuleEnabled(asset) : true
|
||||
const reviewer = normalizeText(asset.reviewer) || '待分配'
|
||||
const creator =
|
||||
@@ -895,6 +947,9 @@ export function buildListItem(asset) {
|
||||
'未知'
|
||||
const publisher = isRiskRule ? creator : ''
|
||||
const riskRuleCreatedAt = formatDateTime(asset.created_at || asset.updated_at)
|
||||
const businessStage = usesJsonRiskRule
|
||||
? resolveRiskRuleBusinessStage(asset)
|
||||
: { value: '', label: '' }
|
||||
|
||||
return {
|
||||
id: asset.id,
|
||||
@@ -915,6 +970,8 @@ export function buildListItem(asset) {
|
||||
reviewer,
|
||||
scope: typeKey === 'rules' ? ruleScenarioCategory || '通用' : formatScenarioList(asset.scenario_json),
|
||||
riskCategory: ruleScenarioCategory,
|
||||
businessStageValue: businessStage.value,
|
||||
businessStageLabel: businessStage.label,
|
||||
model: buildRowRuntime(asset, typeKey),
|
||||
version: workingVersion,
|
||||
versionDisplay: typeKey === 'rules' ? `${changeCount} 次` : workingVersion,
|
||||
@@ -928,8 +985,8 @@ export function buildListItem(asset) {
|
||||
publisher,
|
||||
publishedAt: isOnlineValue ? formatDateTime(asset.published_at || asset.updated_at) : '-',
|
||||
isOnlineValue,
|
||||
isOnlineLabel: isOnlineValue ? '是' : '否',
|
||||
isOnlineTone: isOnlineValue ? 'success' : 'disabled',
|
||||
isOnlineLabel: onlineMeta.label,
|
||||
isOnlineTone: onlineMeta.tone,
|
||||
isEnabledValue,
|
||||
isEnabledLabel: isEnabledValue ? '是' : '否',
|
||||
isEnabledTone: isEnabledValue ? 'success' : 'disabled',
|
||||
@@ -1002,21 +1059,7 @@ export function buildMcpFields(detail, latestCall) {
|
||||
]
|
||||
}
|
||||
|
||||
export function buildTaskFields(detail, latestRun) {
|
||||
const content = detail.current_version_content || {}
|
||||
return [
|
||||
{ label: '任务编码', value: detail.code },
|
||||
{ label: 'Cron', value: normalizeText(detail.config_json?.cron) || normalizeText(content.schedule) || '未配置' },
|
||||
{ label: '执行 Agent', value: normalizeText(detail.config_json?.agent) || normalizeText(content.target_agent) || '未配置' },
|
||||
{ label: '风险等级', value: formatTaskRisk(detail.scenario_json) },
|
||||
{
|
||||
label: '最近执行',
|
||||
value: latestRun ? formatDateTime(latestRun.started_at) : '暂无执行记录'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
export function buildFields(detail, typeKey, latestRun, latestCall) {
|
||||
export function buildFields(detail, typeKey, latestCall) {
|
||||
if (typeKey === 'rules') {
|
||||
return buildRuleFields(detail)
|
||||
}
|
||||
@@ -1026,10 +1069,10 @@ export function buildFields(detail, typeKey, latestRun, latestCall) {
|
||||
if (typeKey === 'mcp') {
|
||||
return buildMcpFields(detail, latestCall)
|
||||
}
|
||||
return buildTaskFields(detail, latestRun)
|
||||
return []
|
||||
}
|
||||
|
||||
export function buildPromptSections(detail, typeKey, latestRun, latestCall) {
|
||||
export function buildPromptSections(detail, typeKey) {
|
||||
const content = detail.current_version_content || {}
|
||||
|
||||
if (typeKey === 'skills') {
|
||||
@@ -1075,26 +1118,10 @@ export function buildPromptSections(detail, typeKey, latestRun, latestCall) {
|
||||
]
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
title: '任务场景',
|
||||
intent: '调度目标',
|
||||
content: formatScenarioList(detail.scenario_json)
|
||||
},
|
||||
{
|
||||
title: '执行 Agent',
|
||||
intent: '运行主体',
|
||||
content: normalizeText(content.target_agent) || normalizeText(detail.config_json?.agent) || '未配置执行 Agent。'
|
||||
},
|
||||
{
|
||||
title: '最近执行结果',
|
||||
intent: '运行反馈',
|
||||
content: latestRun?.result_summary || latestRun?.error_message || '暂无执行记录。'
|
||||
}
|
||||
]
|
||||
return []
|
||||
}
|
||||
|
||||
export function buildOutputRules(detail, typeKey, latestRun, latestCall) {
|
||||
export function buildOutputRules(detail, typeKey) {
|
||||
const content = detail.current_version_content || {}
|
||||
|
||||
if (typeKey === 'rules') {
|
||||
@@ -1130,15 +1157,10 @@ export function buildOutputRules(detail, typeKey, latestRun, latestCall) {
|
||||
]
|
||||
}
|
||||
|
||||
return [
|
||||
`调度周期:${normalizeText(detail.config_json?.cron) || normalizeText(content.schedule) || '未配置'}`,
|
||||
`执行 Agent:${normalizeText(detail.config_json?.agent) || normalizeText(content.target_agent) || '未配置'}`,
|
||||
`风险等级:${formatTaskRisk(detail.scenario_json)}`,
|
||||
`最近执行结果:${latestRun?.status || '暂无执行记录'}`
|
||||
]
|
||||
return []
|
||||
}
|
||||
|
||||
export function buildTests(detail, typeKey, latestRun, latestCall) {
|
||||
export function buildTests(detail, typeKey, latestCall) {
|
||||
if (typeKey === 'rules') {
|
||||
const reviewMeta = resolveReviewMeta(detail.latest_review?.review_status)
|
||||
return [
|
||||
@@ -1195,23 +1217,10 @@ export function buildTests(detail, typeKey, latestRun, latestCall) {
|
||||
]
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
name: '最近运行状态',
|
||||
input: latestRun?.run_id || '暂无运行',
|
||||
result: latestRun?.status || '未记录',
|
||||
tone: latestRun?.status === 'failed' || latestRun?.status === 'blocked' ? 'danger' : 'success'
|
||||
},
|
||||
{
|
||||
name: '结果摘要',
|
||||
input: latestRun?.agent || normalizeText(detail.config_json?.agent) || '未配置',
|
||||
result: latestRun?.result_summary || '暂无摘要',
|
||||
tone: 'success'
|
||||
}
|
||||
]
|
||||
return []
|
||||
}
|
||||
|
||||
export function buildTools(detail, typeKey, latestRun, latestCall) {
|
||||
export function buildTools(detail, typeKey, latestCall) {
|
||||
const content = detail.current_version_content || {}
|
||||
|
||||
if (typeKey === 'skills') {
|
||||
@@ -1246,26 +1255,7 @@ export function buildTools(detail, typeKey, latestRun, latestCall) {
|
||||
]
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
name: normalizeText(detail.config_json?.agent) || normalizeText(content.target_agent) || '未配置 Agent',
|
||||
scope: '执行 Agent',
|
||||
mode: '调度',
|
||||
tone: 'active'
|
||||
},
|
||||
{
|
||||
name: latestRun?.run_id || '暂无执行记录',
|
||||
scope: '最近 Run',
|
||||
mode: latestRun?.status || '未执行',
|
||||
tone: latestRun?.status === 'failed' || latestRun?.status === 'blocked' ? 'danger' : 'active'
|
||||
},
|
||||
{
|
||||
name: latestRun?.permission_level || '未记录',
|
||||
scope: '权限级别',
|
||||
mode: 'Trace',
|
||||
tone: 'safe'
|
||||
}
|
||||
]
|
||||
return []
|
||||
}
|
||||
|
||||
export function buildPublishDescription(detail, typeKey) {
|
||||
@@ -1279,14 +1269,16 @@ export function buildPublishDescription(detail, typeKey) {
|
||||
return '当前规则需要先完成审核,再调用上线接口正式激活。'
|
||||
}
|
||||
|
||||
return DETAIL_TITLES[typeKey].publishDesc
|
||||
return DETAIL_TITLES[typeKey]?.publishDesc || ''
|
||||
}
|
||||
|
||||
export function buildDetailViewModel(detail, runs) {
|
||||
const typeKey = resolveTypeKey(detail.asset_type)
|
||||
const tabId = resolveTabId(detail, typeKey) || typeKey
|
||||
if (!typeKey || !tabId) {
|
||||
return null
|
||||
}
|
||||
const tabMeta = resolveTabMeta(tabId, typeKey)
|
||||
const latestRun = typeKey === 'tasks' ? findLatestTaskRun(runs, detail.id) : null
|
||||
const latestCall = typeKey === 'mcp' ? findLatestMcpCall(runs, detail.code) : null
|
||||
const configJson = readConfigJson(detail)
|
||||
const statusMeta = resolveStatusMeta(detail.status)
|
||||
@@ -1320,6 +1312,14 @@ export function buildDetailViewModel(detail, runs) {
|
||||
normalizeText(detail.owner) ||
|
||||
normalizeText(detail.recent_versions?.[0]?.created_by) ||
|
||||
'未知'
|
||||
const onlineMeta = resolveRiskRuleOnlineMeta(detail.status)
|
||||
const businessStage = usesJsonRiskRule
|
||||
? resolveRiskRuleBusinessStage(detail)
|
||||
: { value: '', label: '' }
|
||||
|
||||
const initialRiskRuleScore = resolveRiskRuleScore(configJson, configJson)
|
||||
const initialRiskRuleScoreLevel = resolveRiskRuleScoreLevel(configJson, configJson)
|
||||
const initialRiskRuleSeverity = initialRiskRuleScoreLevel || resolveRiskRuleSeverity(configJson)
|
||||
|
||||
return {
|
||||
id: detail.id,
|
||||
@@ -1335,6 +1335,8 @@ export function buildDetailViewModel(detail, runs) {
|
||||
reviewer: detail.reviewer || detail.latest_review?.reviewer || '待分配',
|
||||
category: resolveDomainLabel(detail.domain),
|
||||
scope: typeKey === 'rules' ? ruleScenarioCategory || '通用' : formatScenarioList(detail.scenario_json),
|
||||
businessStageValue: businessStage.value,
|
||||
businessStageLabel: businessStage.label,
|
||||
version: detail.working_version || detail.current_version || '-',
|
||||
currentVersion: detail.current_version || '-',
|
||||
publishedVersion: detail.published_version || '-',
|
||||
@@ -1356,15 +1358,19 @@ export function buildDetailViewModel(detail, runs) {
|
||||
riskRuleBusinessDescription: '',
|
||||
riskRuleSubtitle: usesJsonRiskRule ? buildRiskListSubtitle(detail.description) : '',
|
||||
riskRuleSourceRef: '',
|
||||
riskRuleSeverity: 'medium',
|
||||
riskRuleSeverityLabel: '中风险',
|
||||
riskRuleScore: null,
|
||||
riskRuleScoreLabel: '待计算',
|
||||
riskRuleScoreLevel: 'medium',
|
||||
riskRuleScoreDetail: null,
|
||||
riskRuleSeverity: initialRiskRuleSeverity,
|
||||
riskRuleScore: initialRiskRuleScore,
|
||||
riskRuleScoreLevel: initialRiskRuleScoreLevel || initialRiskRuleSeverity,
|
||||
riskRuleScoreDetail: resolveRiskRuleScoreDetail(configJson, configJson),
|
||||
riskRuleSeverityLabel: initialRiskRuleScoreLevel
|
||||
? resolveRiskRuleScoreLabel(configJson, configJson)
|
||||
: resolveRiskRuleSeverityLabel(configJson),
|
||||
riskRuleScoreLabel: resolveRiskRuleScoreLabel(configJson, configJson),
|
||||
riskRuleCreatedAt: formatDateTime(detail.created_at),
|
||||
riskRuleAgeLabel: formatRiskRuleAge(detail.created_at),
|
||||
isOnlineLabel: detail.status === 'active' ? '是' : '否',
|
||||
isOnlineValue: onlineMeta.online,
|
||||
isOnlineLabel: onlineMeta.label,
|
||||
isOnlineTone: onlineMeta.tone,
|
||||
isEnabledValue,
|
||||
isEnabledLabel: isEnabledValue ? '是' : '否',
|
||||
isEnabledTone: isEnabledValue ? 'success' : 'disabled',
|
||||
@@ -1381,6 +1387,11 @@ export function buildDetailViewModel(detail, runs) {
|
||||
history.find((item) => item.isPublished || item.lifecycleState === 'published')?.time ||
|
||||
(detail.published_at ? formatDateTime(detail.published_at) : '') ||
|
||||
(detail.latest_review?.reviewed_at ? formatDateTime(detail.latest_review.reviewed_at) : '-'),
|
||||
lastOperationLabel: resolveLastOperationLabel(detail, {
|
||||
actor: riskRuleCreator,
|
||||
at: detail.created_at
|
||||
}),
|
||||
lastOperationTone: onlineMeta.tone,
|
||||
riskRuleFields: [],
|
||||
riskRuleFieldSummary: '未识别字段',
|
||||
riskRuleFlow: resolveRiskRuleFlow({}, []),
|
||||
@@ -1411,13 +1422,12 @@ export function buildDetailViewModel(detail, runs) {
|
||||
reviewStatusValue: detail.latest_review?.review_status || '',
|
||||
reviewTimeLabel: formatDateTime(detail.latest_review?.reviewed_at),
|
||||
reviewNote: detail.latest_review?.review_note || '',
|
||||
latestRun,
|
||||
latestCall,
|
||||
fields: buildFields(detail, typeKey, latestRun, latestCall),
|
||||
fields: buildFields(detail, typeKey, latestCall),
|
||||
promptSections:
|
||||
typeKey === 'rules' ? [] : buildPromptSections(detail, typeKey, latestRun, latestCall),
|
||||
outputRules: buildOutputRules(detail, typeKey, latestRun, latestCall),
|
||||
tests: buildTests(detail, typeKey, latestRun, latestCall),
|
||||
typeKey === 'rules' ? [] : buildPromptSections(detail, typeKey),
|
||||
outputRules: buildOutputRules(detail, typeKey),
|
||||
tests: buildTests(detail, typeKey, latestCall),
|
||||
triggers:
|
||||
typeKey === 'rules'
|
||||
? [ruleScenarioCategory || '通用']
|
||||
@@ -1446,7 +1456,7 @@ export function buildDetailViewModel(detail, runs) {
|
||||
tone: 'safe'
|
||||
}
|
||||
]
|
||||
: buildTools(detail, typeKey, latestRun, latestCall),
|
||||
: buildTools(detail, typeKey, latestCall),
|
||||
history,
|
||||
configTitle: titles.configTitle,
|
||||
configDesc: titles.configDesc,
|
||||
@@ -1467,9 +1477,7 @@ export function buildDetailViewModel(detail, runs) {
|
||||
publishMeta:
|
||||
typeKey === 'rules'
|
||||
? `最近保存:${formatDateTime(detail.updated_at)}`
|
||||
: latestRun
|
||||
? `最近运行:${formatDateTime(latestRun.started_at)}`
|
||||
: `最近更新:${formatDateTime(detail.updated_at)}`,
|
||||
: `最近更新:${formatDateTime(detail.updated_at)}`,
|
||||
publishState: statusMeta.label,
|
||||
latestReviewVersion: detail.latest_review?.version || detail.current_version || '-',
|
||||
loading: false
|
||||
|
||||
@@ -16,6 +16,11 @@ export const RISK_RULE_EXPENSE_CATEGORY_OPTIONS = [
|
||||
{ value: 'welfare', label: '福利费' }
|
||||
]
|
||||
|
||||
export const RISK_RULE_BUSINESS_STAGE_OPTIONS = [
|
||||
{ value: 'expense_application', label: '费用申请' },
|
||||
{ value: 'reimbursement', label: '费用报销' }
|
||||
]
|
||||
|
||||
export const RISK_RULE_LEVEL_OPTIONS = [
|
||||
{ value: 'low', label: '低风险' },
|
||||
{ value: 'medium', label: '中风险' },
|
||||
@@ -49,6 +54,7 @@ const CITY_ROUTE_SEMANTIC_TYPES = new Set([
|
||||
export function createDefaultRiskRuleForm() {
|
||||
return {
|
||||
business_domain: 'expense',
|
||||
business_stage: 'reimbursement',
|
||||
expense_category: 'travel',
|
||||
rule_title: '',
|
||||
requires_attachment: false,
|
||||
|
||||
@@ -25,8 +25,10 @@ export const EXPENSE_TYPE_LABELS = {
|
||||
meal: '业务招待费',
|
||||
meeting: '会务费',
|
||||
entertainment: '业务招待费',
|
||||
marketing: '市场推广费',
|
||||
office: '办公用品费',
|
||||
training: '培训费',
|
||||
software: '软件服务费',
|
||||
communication: '通讯费',
|
||||
welfare: '福利费',
|
||||
other: '其他费用'
|
||||
@@ -96,8 +98,10 @@ export const REVIEW_FALLBACK_GROUP_CODES = [
|
||||
'hotel',
|
||||
'meal',
|
||||
'meeting',
|
||||
'marketing',
|
||||
'office',
|
||||
'training',
|
||||
'software',
|
||||
'communication',
|
||||
'welfare'
|
||||
]
|
||||
@@ -113,7 +117,9 @@ export const REVIEW_CATEGORY_PRESET_OPTIONS = [
|
||||
|
||||
export const REVIEW_OTHER_CATEGORY_OPTIONS = [
|
||||
{ key: 'meeting', label: '会务费' },
|
||||
{ key: 'marketing', label: '市场推广费' },
|
||||
{ key: 'training', label: '培训费' },
|
||||
{ key: 'software', label: '软件服务费' },
|
||||
{ key: 'communication', label: '通讯费' },
|
||||
{ key: 'welfare', label: '福利费' },
|
||||
{ key: 'other', label: '其他费用' }
|
||||
@@ -140,9 +146,11 @@ export const CATEGORY_CONFIDENCE_KEYWORDS = {
|
||||
transport: [TRANSPORT_KEYWORD_PATTERN],
|
||||
meal: [/业务招待|招待|宴请|请客|请客户|客户.*吃饭|商务用餐|陪同|餐费|用餐|午餐|晚餐|早餐|伙食|餐饮/],
|
||||
meeting: [/会务|会议|论坛|展会|参会|会场/],
|
||||
marketing: [/市场推广|推广费|广告|投放|品牌宣传|营销物料|推广物料/],
|
||||
entertainment: [/招待|宴请|请客|请客户|客户.*吃饭|商务用餐|陪同/],
|
||||
office: [/办公|工位|耗材|白板|键盘|鼠标|打印|文具|采购/],
|
||||
training: [/培训|授课|讲师|课程|签到|讲义/],
|
||||
software: [/软件|SaaS|订阅|系统服务|云服务|云资源|平台服务|技术服务/],
|
||||
communication: [/通讯|电话|流量|话费|宽带|网络/],
|
||||
welfare: [/福利|体检|团建|节日|慰问|关怀/]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user