refactor(audit): reuse list shells and split models
This commit is contained in:
@@ -1,19 +1,21 @@
|
|||||||
<template>
|
<template>
|
||||||
<article class="skill-list panel">
|
<EnterpriseListPage
|
||||||
<nav class="status-tabs" aria-label="能力类型">
|
variant="skill-list"
|
||||||
<button
|
:tabs="shellTabs"
|
||||||
v-for="tab in tabs"
|
:active-tab="activeType"
|
||||||
:key="tab.id"
|
tabs-label="能力类型"
|
||||||
type="button"
|
:loading="loading"
|
||||||
:class="{ active: activeType === tab.id }"
|
:error="errorMessage"
|
||||||
@click="emit('update:activeType', tab.id)"
|
:empty="!visibleSkills.length"
|
||||||
|
:empty-state="auditEmptyState"
|
||||||
|
:loading-title="`${activeTabLabel}资产同步中`"
|
||||||
|
:loading-message="`正在加载${activeTabLabel}资产`"
|
||||||
|
loading-icon="mdi mdi-view-list-outline"
|
||||||
|
:hint="hintText"
|
||||||
|
@update:active-tab="emit('update:activeType', $event)"
|
||||||
|
@empty-action="emit('empty-action')"
|
||||||
>
|
>
|
||||||
{{ tab.label }}
|
<template #filters>
|
||||||
</button>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<div class="list-toolbar">
|
|
||||||
<div class="filter-set">
|
|
||||||
<label class="search-filter">
|
<label class="search-filter">
|
||||||
<i class="mdi mdi-magnify"></i>
|
<i class="mdi mdi-magnify"></i>
|
||||||
<input
|
<input
|
||||||
@@ -120,9 +122,9 @@
|
|||||||
@close="emit('close-filter-popover')"
|
@close="emit('close-filter-popover')"
|
||||||
@select="selectFilter('status', $event)"
|
@select="selectFilter('status', $event)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</template>
|
||||||
|
|
||||||
<div class="toolbar-actions">
|
<template #actions>
|
||||||
<button
|
<button
|
||||||
v-if="activeFilterTokens.length"
|
v-if="activeFilterTokens.length"
|
||||||
class="ghost-filter-btn"
|
class="ghost-filter-btn"
|
||||||
@@ -142,50 +144,23 @@
|
|||||||
<i class="mdi mdi-plus"></i>
|
<i class="mdi mdi-plus"></i>
|
||||||
<span>{{ createButtonLabel }}</span>
|
<span>{{ createButtonLabel }}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</template>
|
||||||
</div>
|
|
||||||
|
|
||||||
<p class="hint"><i class="mdi mdi-information-outline"></i> {{ hintText }}</p>
|
|
||||||
|
|
||||||
|
<template #meta>
|
||||||
<div v-if="activeFilterTokens.length" class="active-filter-strip">
|
<div v-if="activeFilterTokens.length" class="active-filter-strip">
|
||||||
<span v-for="token in activeFilterTokens" :key="token" class="active-filter-chip">
|
<span v-for="token in activeFilterTokens" :key="token" class="active-filter-chip">
|
||||||
{{ token }}
|
{{ token }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<div
|
<template #error>
|
||||||
class="table-wrap"
|
|
||||||
:class="{ 'is-empty': !loading && !errorMessage && !visibleSkills.length }"
|
|
||||||
>
|
|
||||||
<div v-if="loading" class="table-state">
|
|
||||||
<TableLoadingState
|
|
||||||
variant="panel"
|
|
||||||
:title="`${activeTabLabel}资产同步中`"
|
|
||||||
:message="`正在加载${activeTabLabel}资产`"
|
|
||||||
icon="mdi mdi-view-list-outline"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else-if="errorMessage" class="table-state error">
|
|
||||||
<i class="mdi mdi-alert-circle-outline"></i>
|
<i class="mdi mdi-alert-circle-outline"></i>
|
||||||
<p>{{ errorMessage }}</p>
|
<p>{{ errorMessage }}</p>
|
||||||
</div>
|
</template>
|
||||||
|
|
||||||
<TableEmptyState
|
<template #table>
|
||||||
v-else-if="!visibleSkills.length"
|
<table>
|
||||||
:eyebrow="auditEmptyState.eyebrow"
|
|
||||||
:title="auditEmptyState.title"
|
|
||||||
:description="auditEmptyState.desc"
|
|
||||||
:icon="auditEmptyState.icon"
|
|
||||||
:action-label="auditEmptyState.actionLabel"
|
|
||||||
:action-icon="auditEmptyState.actionIcon"
|
|
||||||
:tone="auditEmptyState.tone"
|
|
||||||
:art-label="auditEmptyState.artLabel"
|
|
||||||
:tips="auditEmptyState.tips"
|
|
||||||
@action="emit('empty-action')"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<table v-else>
|
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>{{ tableColumns.name }}</th>
|
<th>{{ tableColumns.name }}</th>
|
||||||
@@ -245,24 +220,27 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</template>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
<footer v-if="!loading && !errorMessage && visibleSkills.length" class="list-foot">
|
<footer v-if="!loading && !errorMessage && visibleSkills.length" class="list-foot">
|
||||||
<span class="page-summary">当前展示 {{ visibleSkills.length }} 条资产</span>
|
<span class="page-summary">当前展示 {{ visibleSkills.length }} 条资产</span>
|
||||||
</footer>
|
</footer>
|
||||||
</article>
|
</template>
|
||||||
|
</EnterpriseListPage>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
import AuditPickerFilter from './AuditPickerFilter.vue'
|
import AuditPickerFilter from './AuditPickerFilter.vue'
|
||||||
import TableEmptyState from '../shared/TableEmptyState.vue'
|
import EnterpriseListPage from '../shared/EnterpriseListPage.vue'
|
||||||
import TableLoadingState from '../shared/TableLoadingState.vue'
|
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'AuditAssetList'
|
name: 'AuditAssetList'
|
||||||
})
|
})
|
||||||
|
|
||||||
defineProps({
|
const props = defineProps({
|
||||||
tabs: { type: Array, default: () => [] },
|
tabs: { type: Array, default: () => [] },
|
||||||
activeType: { type: String, default: '' },
|
activeType: { type: String, default: '' },
|
||||||
activeTabLabel: { type: String, default: '' },
|
activeTabLabel: { type: String, default: '' },
|
||||||
@@ -325,6 +303,13 @@ const emit = defineEmits([
|
|||||||
'open-asset-detail'
|
'open-asset-detail'
|
||||||
])
|
])
|
||||||
|
|
||||||
|
const shellTabs = computed(() =>
|
||||||
|
props.tabs.map((tab) => ({
|
||||||
|
value: tab.id,
|
||||||
|
label: tab.label
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
function selectFilter(type, value) {
|
function selectFilter(type, value) {
|
||||||
emit('select-filter', type, value)
|
emit('select-filter', type, value)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<section class="digital-employee-list-panel">
|
<EnterpriseListPage
|
||||||
<div class="list-toolbar">
|
variant="digital-employee-list-panel"
|
||||||
<div class="filter-set">
|
:panel="false"
|
||||||
|
:loading="loading"
|
||||||
|
:error="errorMessage"
|
||||||
|
:empty="!visibleEmployees.length"
|
||||||
|
:empty-state="emptyState"
|
||||||
|
loading-title="数字员工资产同步中"
|
||||||
|
loading-message="正在加载数字员工资产"
|
||||||
|
loading-icon="mdi mdi-view-list-outline"
|
||||||
|
hint="归集后台自动执行的数字员工技能,可查看技能内容、执行计划、启动状态和最近版本。"
|
||||||
|
>
|
||||||
|
<template #filters>
|
||||||
<label class="search-filter">
|
<label class="search-filter">
|
||||||
<i class="mdi mdi-magnify"></i>
|
<i class="mdi mdi-magnify"></i>
|
||||||
<input
|
<input
|
||||||
@@ -50,9 +60,9 @@
|
|||||||
@close="emit('close-filter-popover')"
|
@close="emit('close-filter-popover')"
|
||||||
@select="selectFilter('executionMode', $event)"
|
@select="selectFilter('executionMode', $event)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</template>
|
||||||
|
|
||||||
<div class="toolbar-actions">
|
<template #actions>
|
||||||
<button
|
<button
|
||||||
v-if="keyword || activeFilterTokens.length"
|
v-if="keyword || activeFilterTokens.length"
|
||||||
class="ghost-filter-btn"
|
class="ghost-filter-btn"
|
||||||
@@ -71,50 +81,23 @@
|
|||||||
<i class="mdi mdi-refresh"></i>
|
<i class="mdi mdi-refresh"></i>
|
||||||
<span>{{ loading ? '刷新中...' : '刷新' }}</span>
|
<span>{{ loading ? '刷新中...' : '刷新' }}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</template>
|
||||||
</div>
|
|
||||||
|
|
||||||
<p class="hint">
|
|
||||||
<i class="mdi mdi-information-outline"></i>
|
|
||||||
归集后台自动执行的数字员工技能,可查看技能内容、执行计划、启动状态和最近版本。
|
|
||||||
</p>
|
|
||||||
|
|
||||||
|
<template #meta>
|
||||||
<div v-if="activeFilterTokens.length" class="active-filter-strip">
|
<div v-if="activeFilterTokens.length" class="active-filter-strip">
|
||||||
<span v-for="token in activeFilterTokens" :key="token" class="active-filter-chip">
|
<span v-for="token in activeFilterTokens" :key="token" class="active-filter-chip">
|
||||||
{{ token }}
|
{{ token }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<div
|
<template #error>
|
||||||
class="table-wrap digital-table-wrap"
|
|
||||||
:class="{ 'is-empty': !loading && !errorMessage && !visibleEmployees.length }"
|
|
||||||
>
|
|
||||||
<div v-if="loading" class="table-state">
|
|
||||||
<TableLoadingState
|
|
||||||
variant="panel"
|
|
||||||
title="数字员工资产同步中"
|
|
||||||
message="正在加载数字员工资产"
|
|
||||||
icon="mdi mdi-view-list-outline"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else-if="errorMessage" class="table-state error">
|
|
||||||
<i class="mdi mdi-alert-circle-outline"></i>
|
<i class="mdi mdi-alert-circle-outline"></i>
|
||||||
<p>{{ errorMessage }}</p>
|
<p>{{ errorMessage }}</p>
|
||||||
</div>
|
</template>
|
||||||
|
|
||||||
<TableEmptyState
|
<template #table>
|
||||||
v-else-if="!visibleEmployees.length"
|
<table class="digital-employees-table">
|
||||||
eyebrow="数字员工"
|
|
||||||
title="暂无匹配的数字员工"
|
|
||||||
description="当前没有符合搜索条件的后台执行技能。"
|
|
||||||
icon="mdi mdi-account-cog-outline"
|
|
||||||
tone="theme"
|
|
||||||
art-label="STAFF"
|
|
||||||
:tips="['数字员工已从规则中心拆出为独立入口', '运行与定时操作统一进入详情后处理']"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<table v-else class="digital-employees-table">
|
|
||||||
<colgroup>
|
<colgroup>
|
||||||
<col class="col-skill">
|
<col class="col-skill">
|
||||||
<col class="col-skill-type">
|
<col class="col-skill-type">
|
||||||
@@ -164,8 +147,9 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</template>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
<footer v-if="!loading && !errorMessage && visibleEmployees.length" class="list-foot digital-employee-pagination">
|
<footer v-if="!loading && !errorMessage && visibleEmployees.length" class="list-foot digital-employee-pagination">
|
||||||
<span class="page-summary">共 {{ visibleEmployees.length }} 条,目前第 {{ currentPage }} / {{ totalPages }} 页</span>
|
<span class="page-summary">共 {{ visibleEmployees.length }} 条,目前第 {{ currentPage }} / {{ totalPages }} 页</span>
|
||||||
<div class="pager" aria-label="员工技能分页">
|
<div class="pager" aria-label="员工技能分页">
|
||||||
@@ -187,15 +171,15 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</section>
|
</template>
|
||||||
|
</EnterpriseListPage>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
|
|
||||||
import AuditPickerFilter from './AuditPickerFilter.vue'
|
import AuditPickerFilter from './AuditPickerFilter.vue'
|
||||||
import TableEmptyState from '../shared/TableEmptyState.vue'
|
import EnterpriseListPage from '../shared/EnterpriseListPage.vue'
|
||||||
import TableLoadingState from '../shared/TableLoadingState.vue'
|
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'DigitalEmployeeListPanel'
|
name: 'DigitalEmployeeListPanel'
|
||||||
@@ -244,6 +228,15 @@ const pageNumbers = computed(() => {
|
|||||||
const start = Math.max(1, Math.min(currentPage.value - 3, total - 6))
|
const start = Math.max(1, Math.min(currentPage.value - 3, total - 6))
|
||||||
return Array.from({ length: 7 }, (_, index) => start + index)
|
return Array.from({ length: 7 }, (_, index) => start + index)
|
||||||
})
|
})
|
||||||
|
const emptyState = {
|
||||||
|
eyebrow: '数字员工',
|
||||||
|
title: '暂无匹配的数字员工',
|
||||||
|
description: '当前没有符合搜索条件的后台执行技能。',
|
||||||
|
icon: 'mdi mdi-account-cog-outline',
|
||||||
|
tone: 'theme',
|
||||||
|
artLabel: 'STAFF',
|
||||||
|
tips: ['数字员工已从规则中心拆出为独立入口', '运行与定时操作统一进入详情后处理']
|
||||||
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => [props.keyword, props.selectedStatus, props.selectedEnabledState, props.selectedExecutionMode],
|
() => [props.keyword, props.selectedStatus, props.selectedEnabledState, props.selectedExecutionMode],
|
||||||
@@ -279,7 +272,7 @@ function selectFilter(type, value) {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.digital-employee-list-panel .digital-table-wrap {
|
.digital-employee-list-panel :deep(.table-wrap) {
|
||||||
flex: 1 1 0;
|
flex: 1 1 0;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
@@ -302,7 +295,7 @@ function selectFilter(type, value) {
|
|||||||
gap: 6px;
|
gap: 6px;
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
border: 1px solid #e2e8f0;
|
border: 1px solid #e2e8f0;
|
||||||
border-radius: 12px;
|
border-radius: 4px;
|
||||||
background: #f8fafc;
|
background: #f8fafc;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -310,7 +303,7 @@ function selectFilter(type, value) {
|
|||||||
width: 32px;
|
width: 32px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 9px;
|
border-radius: 4px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: #334155;
|
color: #334155;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
@@ -321,7 +314,7 @@ function selectFilter(type, value) {
|
|||||||
.digital-employee-list-panel .pager button:hover:not(.active):not(:disabled) {
|
.digital-employee-list-panel .pager button:hover:not(.active):not(:disabled) {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
color: var(--theme-primary-active);
|
color: var(--theme-primary-active);
|
||||||
box-shadow: 0 1px 4px rgba(15, 23, 42, .08);
|
box-shadow: 0 1px 4px rgba(15, 23, 42, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
.digital-employee-list-panel .pager button.active {
|
.digital-employee-list-panel .pager button.active {
|
||||||
|
|||||||
@@ -1,17 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<article class="enterprise-detail-page" :class="variant">
|
<article class="enterprise-detail-page" :class="variant">
|
||||||
<div class="detail-scroll">
|
<div class="detail-scroll">
|
||||||
<TableLoadingState
|
<section v-if="error" class="detail-inline-state panel error">
|
||||||
v-if="loading"
|
|
||||||
class="detail-loading-state panel"
|
|
||||||
variant="panel"
|
|
||||||
:title="loadingTitle"
|
|
||||||
:message="loadingMessage"
|
|
||||||
:icon="loadingIcon"
|
|
||||||
:show-skeleton="false"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<section v-else-if="error" class="detail-inline-state panel error">
|
|
||||||
<slot name="error" :error="error">
|
<slot name="error" :error="error">
|
||||||
<i class="mdi mdi-alert-circle-outline"></i>
|
<i class="mdi mdi-alert-circle-outline"></i>
|
||||||
<div>
|
<div>
|
||||||
@@ -21,6 +11,16 @@
|
|||||||
</slot>
|
</slot>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<TableLoadingState
|
||||||
|
v-else-if="loading"
|
||||||
|
class="detail-loading-state panel"
|
||||||
|
variant="panel"
|
||||||
|
:title="loadingTitle"
|
||||||
|
:message="loadingMessage"
|
||||||
|
:icon="loadingIcon"
|
||||||
|
:show-skeleton="false"
|
||||||
|
/>
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<section v-if="$slots.hero" class="detail-hero panel">
|
<section v-if="$slots.hero" class="detail-hero panel">
|
||||||
<slot name="hero"></slot>
|
<slot name="hero"></slot>
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer v-if="backLabel || $slots.actions" class="detail-actions">
|
<footer v-if="backLabel || $slots.actions" class="detail-actions" :class="actionsClass">
|
||||||
<button v-if="backLabel" class="back-action" type="button" @click="emit('back')">
|
<button v-if="backLabel" class="back-action" type="button" @click="emit('back')">
|
||||||
<i class="mdi mdi-arrow-left"></i>
|
<i class="mdi mdi-arrow-left"></i>
|
||||||
<span>{{ backLabel }}</span>
|
<span>{{ backLabel }}</span>
|
||||||
@@ -57,6 +57,7 @@
|
|||||||
import TableLoadingState from './TableLoadingState.vue'
|
import TableLoadingState from './TableLoadingState.vue'
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
|
actionsClass: { type: [String, Array, Object], default: '' },
|
||||||
backLabel: { type: String, default: '' },
|
backLabel: { type: String, default: '' },
|
||||||
error: { type: String, default: '' },
|
error: { type: String, default: '' },
|
||||||
errorTitle: { type: String, default: '详情加载失败' },
|
errorTitle: { type: String, default: '详情加载失败' },
|
||||||
@@ -64,7 +65,7 @@ defineProps({
|
|||||||
loadingIcon: { type: String, default: 'mdi mdi-file-document-outline' },
|
loadingIcon: { type: String, default: 'mdi mdi-file-document-outline' },
|
||||||
loadingMessage: { type: String, default: '' },
|
loadingMessage: { type: String, default: '' },
|
||||||
loadingTitle: { type: String, default: '正在加载详情' },
|
loadingTitle: { type: String, default: '正在加载详情' },
|
||||||
variant: { type: String, default: '' }
|
variant: { type: [String, Array, Object], default: '' }
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['back'])
|
const emit = defineEmits(['back'])
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<article class="enterprise-list-page panel" :class="variant">
|
<article class="enterprise-list-page" :class="[variant, { panel }]">
|
||||||
<slot name="before"></slot>
|
<slot name="before"></slot>
|
||||||
|
|
||||||
<nav v-if="hasTabs" class="status-tabs" :aria-label="tabsLabel">
|
<nav v-if="hasTabs" class="status-tabs" :aria-label="tabsLabel">
|
||||||
@@ -51,12 +51,15 @@
|
|||||||
</slot>
|
</slot>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<slot name="meta"></slot>
|
||||||
|
|
||||||
<div class="table-wrap" :class="{ 'is-empty': empty, 'has-error': Boolean(error) }">
|
<div class="table-wrap" :class="{ 'is-empty': empty, 'has-error': Boolean(error) }">
|
||||||
<div v-if="loading" class="table-state">
|
<div v-if="loading" class="table-state">
|
||||||
<TableLoadingState
|
<TableLoadingState
|
||||||
:title="loadingTitle"
|
:title="loadingTitle"
|
||||||
:message="loadingMessage"
|
:message="loadingMessage"
|
||||||
:icon="loadingIcon"
|
:icon="loadingIcon"
|
||||||
|
floating
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -135,6 +138,7 @@ const props = defineProps({
|
|||||||
type: Array,
|
type: Array,
|
||||||
default: () => []
|
default: () => []
|
||||||
},
|
},
|
||||||
|
panel: { type: Boolean, default: true },
|
||||||
pages: {
|
pages: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: () => []
|
default: () => []
|
||||||
|
|||||||
@@ -1,47 +1,29 @@
|
|||||||
<template>
|
<template>
|
||||||
<section class="digital-employees-view skill-center">
|
<section class="digital-employees-view skill-center">
|
||||||
<Transition name="skill-view" mode="out-in">
|
<Transition name="skill-view" mode="out-in">
|
||||||
<article
|
<EnterpriseDetailPage
|
||||||
v-if="selectedEmployee"
|
v-if="selectedEmployee"
|
||||||
key="detail"
|
key="detail"
|
||||||
class="skill-detail digital-employee-detail json-risk-skill-detail"
|
variant="skill-detail digital-employee-detail json-risk-skill-detail"
|
||||||
|
actions-class="digital-employee-detail-actions"
|
||||||
|
:error="detailError"
|
||||||
|
error-title="数字员工详情加载失败"
|
||||||
|
:loading="detailLoading && selectedEmployee.loading"
|
||||||
|
loading-title="正在加载数字员工详情"
|
||||||
|
loading-message="列表数据已就绪,正在补充 Skills 源文件和执行配置"
|
||||||
|
loading-icon="mdi mdi-account-cog-outline"
|
||||||
|
back-label="返回数字员工列表"
|
||||||
|
@back="closeEmployeeDetail"
|
||||||
>
|
>
|
||||||
<div class="detail-scroll">
|
|
||||||
<section v-if="detailError" class="detail-inline-state panel error">
|
|
||||||
<i class="mdi mdi-alert-circle-outline"></i>
|
|
||||||
<div>
|
|
||||||
<strong>数字员工详情加载失败</strong>
|
|
||||||
<p>{{ detailError }}</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<TableLoadingState
|
|
||||||
v-else-if="detailLoading && selectedEmployee.loading"
|
|
||||||
class="detail-loading-state panel"
|
|
||||||
variant="panel"
|
|
||||||
title="正在加载数字员工详情"
|
|
||||||
message="列表数据已就绪,正在补充 Skills 源文件和执行配置"
|
|
||||||
icon="mdi mdi-account-cog-outline"
|
|
||||||
:show-skeleton="false"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<AuditDigitalEmployeeDetail
|
<AuditDigitalEmployeeDetail
|
||||||
v-else
|
|
||||||
:selected-skill="selectedEmployee"
|
:selected-skill="selectedEmployee"
|
||||||
:can-edit="canEditDigitalEmployeeSource"
|
:can-edit="canEditDigitalEmployeeSource"
|
||||||
:detail-busy="detailBusy"
|
:detail-busy="detailBusy"
|
||||||
:action-state="actionState"
|
:action-state="actionState"
|
||||||
@save-source="saveDigitalEmployeeSource"
|
@save-source="saveDigitalEmployeeSource"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
<footer class="detail-actions digital-employee-detail-actions">
|
<template #actions>
|
||||||
<button class="back-action" type="button" @click="closeEmployeeDetail">
|
|
||||||
<i class="mdi mdi-arrow-left"></i>
|
|
||||||
<span>返回数字员工列表</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div class="detail-action-group">
|
|
||||||
<button
|
<button
|
||||||
class="minor-action enable-action"
|
class="minor-action enable-action"
|
||||||
:class="{ 'is-on': selectedEmployee.statusValue === 'active' }"
|
:class="{ 'is-on': selectedEmployee.statusValue === 'active' }"
|
||||||
@@ -70,9 +52,8 @@
|
|||||||
<i class="mdi mdi-play-circle-outline"></i>
|
<i class="mdi mdi-play-circle-outline"></i>
|
||||||
<span>{{ actionBusy(selectedEmployee.id, 'run-digital-now') ? '运行中...' : '立即运行' }}</span>
|
<span>{{ actionBusy(selectedEmployee.id, 'run-digital-now') ? '运行中...' : '立即运行' }}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</template>
|
||||||
</footer>
|
</EnterpriseDetailPage>
|
||||||
</article>
|
|
||||||
|
|
||||||
<article
|
<article
|
||||||
v-else
|
v-else
|
||||||
@@ -98,168 +79,29 @@
|
|||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<template v-if="activeSection === 'skills'">
|
<template v-if="activeSection === 'skills'">
|
||||||
<div class="list-toolbar">
|
<DigitalEmployeeListPanel
|
||||||
<div class="filter-set">
|
v-model:keyword="keyword"
|
||||||
<label class="search-filter">
|
|
||||||
<i class="mdi mdi-magnify"></i>
|
|
||||||
<input v-model="keyword" type="search" placeholder="搜索技能名称、编号、执行计划或维护人" />
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<AuditPickerFilter
|
|
||||||
id="status"
|
|
||||||
title="选择资产状态"
|
|
||||||
close-label="关闭资产状态选择"
|
|
||||||
:active-filter-popover="activeFilterPopover"
|
:active-filter-popover="activeFilterPopover"
|
||||||
:label="selectedStatusLabel"
|
:selected-status="selectedStatus"
|
||||||
:options="statusOptions"
|
:selected-status-label="selectedStatusLabel"
|
||||||
:selected-value="selectedStatus"
|
:status-options="statusOptions"
|
||||||
@toggle="toggleFilterPopover"
|
:selected-enabled-state="selectedEnabledState"
|
||||||
@close="closeFilterPopover"
|
:selected-enabled-label="selectedEnabledLabel"
|
||||||
@select="selectFilter('status', $event)"
|
:enabled-state-options="enabledStateOptions"
|
||||||
|
:selected-execution-mode="selectedExecutionMode"
|
||||||
|
:selected-execution-mode-label="selectedExecutionModeLabel"
|
||||||
|
:execution-mode-options="executionModeOptions"
|
||||||
|
:active-filter-tokens="activeFilterTokens"
|
||||||
|
:loading="loading"
|
||||||
|
:error-message="errorMessage"
|
||||||
|
:visible-employees="visibleEmployees"
|
||||||
|
@toggle-filter-popover="toggleFilterPopover"
|
||||||
|
@close-filter-popover="closeFilterPopover"
|
||||||
|
@select-filter="selectFilter"
|
||||||
|
@reset-filters="resetFilters"
|
||||||
|
@load-employees="loadEmployees"
|
||||||
|
@open-employee-detail="openEmployeeDetail"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AuditPickerFilter
|
|
||||||
id="enabled"
|
|
||||||
title="选择启动状态"
|
|
||||||
close-label="关闭启动状态选择"
|
|
||||||
:active-filter-popover="activeFilterPopover"
|
|
||||||
:label="selectedEnabledLabel"
|
|
||||||
:options="enabledStateOptions"
|
|
||||||
:selected-value="selectedEnabledState"
|
|
||||||
@toggle="toggleFilterPopover"
|
|
||||||
@close="closeFilterPopover"
|
|
||||||
@select="selectFilter('enabled', $event)"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<AuditPickerFilter
|
|
||||||
id="executionMode"
|
|
||||||
title="选择执行方式"
|
|
||||||
close-label="关闭执行方式选择"
|
|
||||||
:active-filter-popover="activeFilterPopover"
|
|
||||||
:label="selectedExecutionModeLabel"
|
|
||||||
:options="executionModeOptions"
|
|
||||||
:selected-value="selectedExecutionMode"
|
|
||||||
@toggle="toggleFilterPopover"
|
|
||||||
@close="closeFilterPopover"
|
|
||||||
@select="selectFilter('executionMode', $event)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="toolbar-actions">
|
|
||||||
<button
|
|
||||||
v-if="keyword || activeFilterTokens.length"
|
|
||||||
class="ghost-filter-btn"
|
|
||||||
type="button"
|
|
||||||
@click="resetFilters"
|
|
||||||
>
|
|
||||||
<i class="mdi mdi-filter-remove-outline"></i>
|
|
||||||
<span>清空筛选</span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="create-btn digital-refresh-action"
|
|
||||||
type="button"
|
|
||||||
:disabled="loading"
|
|
||||||
@click="loadEmployees"
|
|
||||||
>
|
|
||||||
<i class="mdi mdi-refresh"></i>
|
|
||||||
<span>{{ loading ? '刷新中...' : '刷新' }}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p class="hint">
|
|
||||||
<i class="mdi mdi-information-outline"></i>
|
|
||||||
集中查看后台自动执行的技能、执行计划和运行状态。
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div v-if="activeFilterTokens.length" class="active-filter-strip">
|
|
||||||
<span v-for="token in activeFilterTokens" :key="token" class="active-filter-chip">
|
|
||||||
{{ token }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="table-wrap digital-table-wrap"
|
|
||||||
:class="{ 'is-empty': !loading && !errorMessage && !visibleEmployees.length }"
|
|
||||||
>
|
|
||||||
<div v-if="loading" class="table-state">
|
|
||||||
<TableLoadingState
|
|
||||||
variant="panel"
|
|
||||||
title="数字员工同步中"
|
|
||||||
message="正在读取后台自动执行技能列表"
|
|
||||||
icon="mdi mdi-account-cog-outline"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else-if="errorMessage" class="table-state error">
|
|
||||||
<i class="mdi mdi-alert-circle-outline"></i>
|
|
||||||
<strong>数字员工加载失败</strong>
|
|
||||||
<p>{{ errorMessage }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<TableEmptyState
|
|
||||||
v-else-if="!visibleEmployees.length"
|
|
||||||
eyebrow="数字员工"
|
|
||||||
title="暂无匹配的数字员工"
|
|
||||||
description="当前没有符合搜索条件的后台执行技能。"
|
|
||||||
icon="mdi mdi-account-cog-outline"
|
|
||||||
tone="theme"
|
|
||||||
art-label="STAFF"
|
|
||||||
:tips="['数字员工已从规则中心拆出为独立入口', '运行与定时操作统一进入详情后处理']"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<table v-else class="digital-employees-table">
|
|
||||||
<colgroup>
|
|
||||||
<col class="col-skill">
|
|
||||||
<col class="col-schedule">
|
|
||||||
<col class="col-mode">
|
|
||||||
<col class="col-skill-type">
|
|
||||||
<col class="col-status">
|
|
||||||
<col class="col-enabled">
|
|
||||||
<col class="col-updated">
|
|
||||||
</colgroup>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>技能名称</th>
|
|
||||||
<th>执行计划</th>
|
|
||||||
<th>触发方式</th>
|
|
||||||
<th>技能类型</th>
|
|
||||||
<th>资产状态</th>
|
|
||||||
<th>启动状态</th>
|
|
||||||
<th>最近更新</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr
|
|
||||||
v-for="employee in visibleEmployees"
|
|
||||||
:key="employee.id"
|
|
||||||
@click="openEmployeeDetail(employee)"
|
|
||||||
>
|
|
||||||
<td>
|
|
||||||
<div class="skill-name-cell">
|
|
||||||
<span class="skill-avatar" :class="employee.badgeTone">{{ employee.short }}</span>
|
|
||||||
<div>
|
|
||||||
<strong>{{ employee.name }}</strong>
|
|
||||||
<span class="skill-list-subtitle">{{ employee.code }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td><span class="scope-pill">{{ employee.scope }}</span></td>
|
|
||||||
<td>{{ employee.executionMode }}</td>
|
|
||||||
<td><span class="scope-pill skill-type-pill">{{ employee.skillCategory }}</span></td>
|
|
||||||
<td>
|
|
||||||
<span :class="['status-pill', employee.statusTone]">{{ employee.status }}</span>
|
|
||||||
</td>
|
|
||||||
<td><span :class="['status-pill', employee.enabledTone]">{{ employee.enabledLabel }}</span></td>
|
|
||||||
<td>{{ employee.updatedAt || '-' }}</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<footer v-if="!loading && !errorMessage && visibleEmployees.length" class="list-foot">
|
|
||||||
<span class="page-summary">当前展示 {{ visibleEmployees.length }} 条数字员工</span>
|
|
||||||
</footer>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<DigitalEmployeeWorkRecords
|
<DigitalEmployeeWorkRecords
|
||||||
@@ -290,11 +132,10 @@
|
|||||||
import { computed, onMounted, ref, watch } from 'vue'
|
import { computed, onMounted, ref, watch } from 'vue'
|
||||||
|
|
||||||
import AuditDigitalEmployeeDetail from '../components/audit/AuditDigitalEmployeeDetail.vue'
|
import AuditDigitalEmployeeDetail from '../components/audit/AuditDigitalEmployeeDetail.vue'
|
||||||
import AuditPickerFilter from '../components/audit/AuditPickerFilter.vue'
|
import DigitalEmployeeListPanel from '../components/audit/DigitalEmployeeListPanel.vue'
|
||||||
import DigitalEmployeeScheduleDialog from '../components/audit/DigitalEmployeeScheduleDialog.vue'
|
import DigitalEmployeeScheduleDialog from '../components/audit/DigitalEmployeeScheduleDialog.vue'
|
||||||
import DigitalEmployeeWorkRecords from '../components/audit/DigitalEmployeeWorkRecords.vue'
|
import DigitalEmployeeWorkRecords from '../components/audit/DigitalEmployeeWorkRecords.vue'
|
||||||
import TableEmptyState from '../components/shared/TableEmptyState.vue'
|
import EnterpriseDetailPage from '../components/shared/EnterpriseDetailPage.vue'
|
||||||
import TableLoadingState from '../components/shared/TableLoadingState.vue'
|
|
||||||
import { useSystemState } from '../composables/useSystemState.js'
|
import { useSystemState } from '../composables/useSystemState.js'
|
||||||
import { useToast } from '../composables/useToast.js'
|
import { useToast } from '../composables/useToast.js'
|
||||||
import {
|
import {
|
||||||
|
|||||||
17
web/src/views/scripts/auditViewDataUtils.js
Normal file
17
web/src/views/scripts/auditViewDataUtils.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
export function normalizeText(value) {
|
||||||
|
return String(value || '').trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isPlainObject(value) {
|
||||||
|
return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readConfigJson(value) {
|
||||||
|
if (isPlainObject(value?.configJson)) {
|
||||||
|
return value.configJson
|
||||||
|
}
|
||||||
|
if (isPlainObject(value?.config_json)) {
|
||||||
|
return value.config_json
|
||||||
|
}
|
||||||
|
return {}
|
||||||
|
}
|
||||||
128
web/src/views/scripts/auditViewFormatters.js
Normal file
128
web/src/views/scripts/auditViewFormatters.js
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
import {
|
||||||
|
DOMAIN_LABELS,
|
||||||
|
REVIEW_META,
|
||||||
|
SCENARIO_LABELS,
|
||||||
|
STATUS_META
|
||||||
|
} from './auditViewMetadata.js'
|
||||||
|
import { normalizeText } from './auditViewDataUtils.js'
|
||||||
|
|
||||||
|
export function makeShort(value) {
|
||||||
|
const text = normalizeText(value).replace(/\s+/g, '')
|
||||||
|
if (!text) {
|
||||||
|
return 'AG'
|
||||||
|
}
|
||||||
|
return text.slice(0, 2).toUpperCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatDateTime(value) {
|
||||||
|
if (!value) {
|
||||||
|
return '未记录'
|
||||||
|
}
|
||||||
|
|
||||||
|
const date = new Date(value)
|
||||||
|
if (Number.isNaN(date.getTime())) {
|
||||||
|
return String(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Intl.DateTimeFormat('zh-CN', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
hour12: false
|
||||||
|
})
|
||||||
|
.format(date)
|
||||||
|
.replace(/\//g, '-')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveDomainLabel(value) {
|
||||||
|
return DOMAIN_LABELS[value] || normalizeText(value) || '未分类'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveStatusMeta(value) {
|
||||||
|
return STATUS_META[value] || { label: normalizeText(value) || '未知状态', tone: 'draft' }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveReviewMeta(value) {
|
||||||
|
return REVIEW_META[value] || { label: '暂无审核', tone: 'draft' }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveTimelineEventMeta(value) {
|
||||||
|
return {
|
||||||
|
created: { label: '创建工作稿', icon: 'mdi mdi-file-document-edit-outline', tone: 'draft' },
|
||||||
|
submitted: { label: '提交审核', icon: 'mdi mdi-send-outline', tone: 'warning' },
|
||||||
|
approved: { label: '审核通过', icon: 'mdi mdi-check-decagram-outline', tone: 'success' },
|
||||||
|
rejected: { label: '审核驳回', icon: 'mdi mdi-close-octagon-outline', tone: 'danger' },
|
||||||
|
published: { label: '正式上线', icon: 'mdi mdi-rocket-launch-outline', tone: 'success' },
|
||||||
|
restored: { label: '恢复生成工作稿', icon: 'mdi mdi-history', tone: 'info' }
|
||||||
|
}[normalizeText(value)] || { label: normalizeText(value) || '版本事件', icon: 'mdi mdi-circle-medium', tone: 'draft' }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveDiffChangeMeta(value) {
|
||||||
|
return {
|
||||||
|
added: { label: '新增', tone: 'success' },
|
||||||
|
removed: { label: '删除', tone: 'danger' },
|
||||||
|
modified: { label: '修改', tone: 'warning' }
|
||||||
|
}[normalizeText(value)] || { label: normalizeText(value) || '变化', tone: 'draft' }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatScenarioList(items) {
|
||||||
|
if (!Array.isArray(items) || !items.length) {
|
||||||
|
return '未配置场景'
|
||||||
|
}
|
||||||
|
|
||||||
|
return items
|
||||||
|
.map((item) => SCENARIO_LABELS[item] || item)
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' / ')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveTypeKey(assetType) {
|
||||||
|
if (assetType === 'rule') {
|
||||||
|
return 'rules'
|
||||||
|
}
|
||||||
|
if (assetType === 'mcp') {
|
||||||
|
return 'mcp'
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatSeverity(value) {
|
||||||
|
const severity = normalizeText(value).toLowerCase()
|
||||||
|
if (severity === 'high') {
|
||||||
|
return '高风险'
|
||||||
|
}
|
||||||
|
if (severity === 'medium') {
|
||||||
|
return '中风险'
|
||||||
|
}
|
||||||
|
if (severity === 'low') {
|
||||||
|
return '低风险'
|
||||||
|
}
|
||||||
|
return '未配置'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatInputSummary(items) {
|
||||||
|
if (!Array.isArray(items) || !items.length) {
|
||||||
|
return '无输入'
|
||||||
|
}
|
||||||
|
return `${items.length} 项输入`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatOutputSummary(items) {
|
||||||
|
if (!Array.isArray(items) || !items.length) {
|
||||||
|
return '无输出'
|
||||||
|
}
|
||||||
|
return `${items.length} 项输出`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatSpreadsheetChangeSummary(summary) {
|
||||||
|
const normalized = normalizeText(summary)
|
||||||
|
return (
|
||||||
|
normalized
|
||||||
|
.replace(/^(ONLYOFFICE\s*)?在线编辑[::]\s*/i, '')
|
||||||
|
.replace(/^ONLYOFFICE\s*在线编辑保存[。.]?\s*/i, '')
|
||||||
|
.replace(/^保存表格[::]\s*/i, '')
|
||||||
|
.trim() || '表格内容已保存。'
|
||||||
|
)
|
||||||
|
}
|
||||||
205
web/src/views/scripts/auditViewListItemModel.js
Normal file
205
web/src/views/scripts/auditViewListItemModel.js
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
import {
|
||||||
|
resolveRiskRuleScoreLabel,
|
||||||
|
resolveRiskRuleScoreLevel,
|
||||||
|
resolveRiskRuleSeverity,
|
||||||
|
resolveRiskRuleSeverityLabel
|
||||||
|
} from './auditViewRiskRuleModel.js'
|
||||||
|
import { normalizeText } from './auditViewDataUtils.js'
|
||||||
|
import {
|
||||||
|
formatDateTime,
|
||||||
|
formatScenarioList,
|
||||||
|
formatSeverity,
|
||||||
|
makeShort,
|
||||||
|
resolveDomainLabel,
|
||||||
|
resolveStatusMeta,
|
||||||
|
resolveTypeKey
|
||||||
|
} from './auditViewFormatters.js'
|
||||||
|
import {
|
||||||
|
buildRiskListSubtitle,
|
||||||
|
isJsonRiskRuleSource,
|
||||||
|
isSpreadsheetRuleSource,
|
||||||
|
readRuleDocumentMeta,
|
||||||
|
resolveRuleScenarioCategory,
|
||||||
|
resolveRuleScenarioList,
|
||||||
|
resolveTabId,
|
||||||
|
resolveTabMeta
|
||||||
|
} from './auditViewRuleClassifier.js'
|
||||||
|
import {
|
||||||
|
resolveRiskRuleBusinessStage,
|
||||||
|
resolveRiskRuleEnabled,
|
||||||
|
resolveRiskRuleOnlineMeta
|
||||||
|
} from './auditViewRiskRuleState.js'
|
||||||
|
export function findLatestMcpCall(runs, assetCode) {
|
||||||
|
const expectedToolName = normalizeText(assetCode).replace(/^mcp\./, '')
|
||||||
|
|
||||||
|
for (const run of runs) {
|
||||||
|
for (const toolCall of run.tool_calls || []) {
|
||||||
|
const toolName = normalizeText(toolCall.tool_name)
|
||||||
|
if (
|
||||||
|
toolName === expectedToolName ||
|
||||||
|
toolName.endsWith(expectedToolName) ||
|
||||||
|
expectedToolName.endsWith(toolName)
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
run,
|
||||||
|
toolCall
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildRowRuntime(asset, typeKey) {
|
||||||
|
if (typeKey === 'rules') {
|
||||||
|
return formatSeverity(asset.config_json?.severity)
|
||||||
|
}
|
||||||
|
if (typeKey === 'mcp') {
|
||||||
|
return normalizeText(asset.config_json?.endpoint) || '未配置地址'
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildRowMetric(asset, typeKey) {
|
||||||
|
if (typeKey === 'rules') {
|
||||||
|
return normalizeText(asset.modified_by) || '未记录'
|
||||||
|
}
|
||||||
|
if (typeKey === 'mcp') {
|
||||||
|
return asset.config_json?.timeout_ms ? `${asset.config_json.timeout_ms} ms` : '未配置超时'
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function buildListItem(asset) {
|
||||||
|
const typeKey = resolveTypeKey(asset.asset_type)
|
||||||
|
const tabId = resolveTabId(asset, typeKey)
|
||||||
|
if (!tabId) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const tabMeta = resolveTabMeta(tabId, typeKey)
|
||||||
|
const statusMeta = resolveStatusMeta(asset.status)
|
||||||
|
const workingVersion = asset.working_version || asset.current_version || '-'
|
||||||
|
const changeCount =
|
||||||
|
typeof asset.change_count === 'number'
|
||||||
|
? asset.change_count
|
||||||
|
: Array.isArray(asset.recent_versions)
|
||||||
|
? Math.max(asset.recent_versions.length - 1, 0)
|
||||||
|
: 0
|
||||||
|
const modifiedBy =
|
||||||
|
normalizeText(asset.modified_by) ||
|
||||||
|
normalizeText(
|
||||||
|
Array.isArray(asset.recent_versions)
|
||||||
|
? asset.recent_versions.find((item) => item.version === workingVersion)?.created_by
|
||||||
|
: ''
|
||||||
|
)
|
||||||
|
const isRiskRule = tabId === 'riskRules'
|
||||||
|
const usesSpreadsheetRule = typeKey === 'rules' && isSpreadsheetRuleSource(asset)
|
||||||
|
const usesJsonRiskRule = typeKey === 'rules' && isJsonRiskRuleSource(asset)
|
||||||
|
const ruleDocument = readRuleDocumentMeta(asset)
|
||||||
|
const ruleScenarioCategory = typeKey === 'rules' ? resolveRuleScenarioCategory(asset, tabId) : ''
|
||||||
|
const listSubtitle = isRiskRule
|
||||||
|
? buildRiskListSubtitle(asset.description)
|
||||||
|
: normalizeText(asset.description)
|
||||||
|
const onlineMeta = resolveRiskRuleOnlineMeta(asset.status)
|
||||||
|
const isOnlineValue = onlineMeta.online
|
||||||
|
const isEnabledValue = usesJsonRiskRule ? resolveRiskRuleEnabled(asset) : true
|
||||||
|
const reviewer = normalizeText(asset.reviewer) || '待分配'
|
||||||
|
const creator =
|
||||||
|
normalizeText(asset.owner) ||
|
||||||
|
normalizeText(asset.config_json?.generation_request?.actor) ||
|
||||||
|
modifiedBy ||
|
||||||
|
'未知'
|
||||||
|
const publisher = isRiskRule ? creator : ''
|
||||||
|
const riskRuleCreatedAt = formatDateTime(asset.created_at || asset.updated_at)
|
||||||
|
const businessStage = usesJsonRiskRule
|
||||||
|
? resolveRiskRuleBusinessStage(asset)
|
||||||
|
: { value: '', label: '' }
|
||||||
|
const ruleScenarioList = typeKey === 'rules' ? resolveRuleScenarioList(asset, tabId) : []
|
||||||
|
const riskScoreLevel = usesJsonRiskRule
|
||||||
|
? resolveRiskRuleScoreLevel(asset.config_json, asset.config_json)
|
||||||
|
: ''
|
||||||
|
const riskLevelValue = usesJsonRiskRule
|
||||||
|
? riskScoreLevel || resolveRiskRuleSeverity(asset.config_json)
|
||||||
|
: ''
|
||||||
|
const riskLevelLabel = usesJsonRiskRule
|
||||||
|
? riskScoreLevel
|
||||||
|
? resolveRiskRuleScoreLabel(asset.config_json, asset.config_json) || resolveRiskRuleSeverityLabel(asset.config_json)
|
||||||
|
: resolveRiskRuleSeverityLabel(asset.config_json)
|
||||||
|
: ''
|
||||||
|
const displayName = asset.name
|
||||||
|
const displayCode = asset.code
|
||||||
|
const displaySummary = listSubtitle
|
||||||
|
const displayOwner = isRiskRule ? creator : asset.owner
|
||||||
|
const displayReviewer = reviewer
|
||||||
|
const displayCategory = resolveDomainLabel(asset.domain)
|
||||||
|
const displayScope = typeKey === 'rules' ? ruleScenarioCategory || '通用' : formatScenarioList(asset.scenario_json)
|
||||||
|
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,
|
||||||
|
tabId,
|
||||||
|
type: typeKey,
|
||||||
|
isPreviewMock: Boolean(asset.isPreviewMock),
|
||||||
|
usesSpreadsheetRule,
|
||||||
|
usesJsonRiskRule,
|
||||||
|
ruleDocument,
|
||||||
|
typeLabel: tabMeta.typeLabel,
|
||||||
|
short: makeShort(displayName),
|
||||||
|
name: displayName,
|
||||||
|
code: displayCode,
|
||||||
|
rawCode: asset.code,
|
||||||
|
summary: displaySummary,
|
||||||
|
listSubtitle: displaySummary,
|
||||||
|
category: displayCategory,
|
||||||
|
owner: displayOwner,
|
||||||
|
reviewer: displayReviewer,
|
||||||
|
scope: displayScope,
|
||||||
|
riskCategory: ruleScenarioCategory,
|
||||||
|
scenarioList: ruleScenarioList,
|
||||||
|
businessStageValue: businessStage.value,
|
||||||
|
businessStageLabel: businessStage.label,
|
||||||
|
riskLevelValue,
|
||||||
|
riskLevelLabel,
|
||||||
|
riskLevelTone: riskLevelValue,
|
||||||
|
model: buildRowRuntime(asset, typeKey),
|
||||||
|
version: workingVersion,
|
||||||
|
versionDisplay: typeKey === 'rules' ? `${changeCount} 次` : workingVersion,
|
||||||
|
publishedVersion: asset.published_version || '-',
|
||||||
|
workingVersion,
|
||||||
|
status: statusMeta.label,
|
||||||
|
statusValue: asset.status,
|
||||||
|
statusTone: statusMeta.tone,
|
||||||
|
hitRate: isRiskRule ? publisher : buildRowMetric({ ...asset, modified_by: modifiedBy }, typeKey),
|
||||||
|
creator,
|
||||||
|
publisher,
|
||||||
|
publishedAt: isOnlineValue ? formatDateTime(asset.published_at || asset.updated_at) : '-',
|
||||||
|
isOnlineValue,
|
||||||
|
isOnlineLabel: onlineMeta.label,
|
||||||
|
isOnlineTone: onlineMeta.tone,
|
||||||
|
isEnabledValue: displayEnabledValue,
|
||||||
|
isEnabledLabel: displayEnabledLabel,
|
||||||
|
isEnabledTone: displayEnabledTone,
|
||||||
|
modifiedBy,
|
||||||
|
changeCount,
|
||||||
|
updatedAt: isRiskRule ? riskRuleCreatedAt : formatDateTime(asset.updated_at),
|
||||||
|
badgeTone: tabMeta.badgeTone,
|
||||||
|
domainValue: asset.domain,
|
||||||
|
searchText
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
193
web/src/views/scripts/auditViewRiskRuleState.js
Normal file
193
web/src/views/scripts/auditViewRiskRuleState.js
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
import {
|
||||||
|
buildRiskRuleFieldSummary,
|
||||||
|
formatRiskRuleAge,
|
||||||
|
resolveRiskRuleBusinessDescription,
|
||||||
|
resolveRiskRuleCreatedAt,
|
||||||
|
resolveRiskRuleFields,
|
||||||
|
resolveRiskRuleFlow,
|
||||||
|
resolveRiskRuleFlowDiagramSvg,
|
||||||
|
resolveRiskRuleScore,
|
||||||
|
resolveRiskRuleScoreDetail,
|
||||||
|
resolveRiskRuleScoreLabel,
|
||||||
|
resolveRiskRuleScoreLevel,
|
||||||
|
resolveRiskRuleSeverity,
|
||||||
|
resolveRiskRuleSeverityLabel
|
||||||
|
} from './auditViewRiskRuleModel.js'
|
||||||
|
import {
|
||||||
|
isPlainObject,
|
||||||
|
normalizeText,
|
||||||
|
readConfigJson
|
||||||
|
} from './auditViewDataUtils.js'
|
||||||
|
import { formatDateTime } from './auditViewFormatters.js'
|
||||||
|
import {
|
||||||
|
resolveRiskRuleCategory,
|
||||||
|
resolveRiskRuleDescription,
|
||||||
|
resolveRiskRuleSourceRef
|
||||||
|
} from './auditViewRuleClassifier.js'
|
||||||
|
export function resolveRiskRuleEnabled(source, rulePayload = null) {
|
||||||
|
const configJson = readConfigJson(source)
|
||||||
|
if (isPlainObject(rulePayload) && rulePayload.enabled === false) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (source?.enabled === false || configJson.enabled === false) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const LAST_OPERATION_LABELS = {
|
||||||
|
generate: '开始生成',
|
||||||
|
create: '创建',
|
||||||
|
test: '测试',
|
||||||
|
online: '上线',
|
||||||
|
offline: '下线',
|
||||||
|
delete: '删除',
|
||||||
|
update: '更新'
|
||||||
|
}
|
||||||
|
|
||||||
|
const RISK_RULE_BUSINESS_STAGE_LABELS = {
|
||||||
|
expense_application: '费用申请',
|
||||||
|
reimbursement: '费用报销'
|
||||||
|
}
|
||||||
|
|
||||||
|
export 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 || '费用报销'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export 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 applyRiskRuleJsonState(target, payload, apiPayload) {
|
||||||
|
const rulePayload = isPlainObject(payload) ? payload : {}
|
||||||
|
const metadata = rulePayload.metadata && typeof rulePayload.metadata === 'object'
|
||||||
|
? rulePayload.metadata
|
||||||
|
: {}
|
||||||
|
const apiConfig = apiPayload?.config_json && typeof apiPayload.config_json === 'object'
|
||||||
|
? apiPayload.config_json
|
||||||
|
: {}
|
||||||
|
const fullDescription =
|
||||||
|
resolveRiskRuleDescription(rulePayload) ||
|
||||||
|
normalizeText(apiPayload?.description) ||
|
||||||
|
normalizeText(target.riskRuleDescription)
|
||||||
|
const riskCategory =
|
||||||
|
normalizeText(metadata.expense_category_label) ||
|
||||||
|
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 onlineMeta = resolveRiskRuleOnlineMeta(statusValue)
|
||||||
|
const isEnabledValue = resolveRiskRuleEnabled(target, rulePayload)
|
||||||
|
|
||||||
|
const publisher =
|
||||||
|
target.creator ||
|
||||||
|
normalizeText(apiPayload?.owner) ||
|
||||||
|
normalizeText(metadata.created_by) ||
|
||||||
|
normalizeText(apiPayload?.recent_versions?.[0]?.created_by) ||
|
||||||
|
'未知'
|
||||||
|
|
||||||
|
let publishedAt = target.publishedAt || '-'
|
||||||
|
if (apiPayload?.recent_versions) {
|
||||||
|
const history = buildHistory(apiPayload.recent_versions, { ...target, config_json: payload })
|
||||||
|
const publishedVersionObj = history.find((item) => item.isPublished || item.lifecycleState === 'published')
|
||||||
|
publishedAt = publishedVersionObj ? publishedVersionObj.time : (apiPayload?.latest_review?.reviewed_at ? formatDateTime(apiPayload.latest_review.reviewed_at) : '-')
|
||||||
|
} else if (apiPayload?.latest_review?.reviewed_at) {
|
||||||
|
publishedAt = formatDateTime(apiPayload.latest_review.reviewed_at)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...target,
|
||||||
|
riskRuleDescription: fullDescription,
|
||||||
|
riskRuleBusinessDescription: resolveRiskRuleBusinessDescription(rulePayload, fullDescription),
|
||||||
|
riskRuleSubtitle: buildRiskListSubtitle(fullDescription, 48),
|
||||||
|
riskCategory,
|
||||||
|
businessStageValue: businessStage.value,
|
||||||
|
businessStageLabel: businessStage.label,
|
||||||
|
scope: riskCategory,
|
||||||
|
riskRuleSourceRef: resolveRiskRuleSourceRef(rulePayload),
|
||||||
|
riskRuleSeverity: riskRuleScoreLevel || resolveRiskRuleSeverity(rulePayload),
|
||||||
|
riskRuleSeverityLabel: riskRuleScoreLevel
|
||||||
|
? resolveRiskRuleScoreLabel(rulePayload, apiConfig)
|
||||||
|
: resolveRiskRuleSeverityLabel(rulePayload),
|
||||||
|
riskRuleScore: resolveRiskRuleScore(rulePayload, apiConfig),
|
||||||
|
riskRuleScoreLabel: resolveRiskRuleScoreLabel(rulePayload, apiConfig),
|
||||||
|
riskRuleScoreLevel: riskRuleScoreLevel || resolveRiskRuleSeverity(rulePayload),
|
||||||
|
riskRuleScoreDetail: resolveRiskRuleScoreDetail(rulePayload, apiConfig),
|
||||||
|
riskRuleCreatedAt: formatDateTime(riskRuleCreatedAt),
|
||||||
|
riskRuleAgeLabel: formatRiskRuleAge(riskRuleCreatedAt),
|
||||||
|
riskRuleFields,
|
||||||
|
riskRuleFieldSummary: buildRiskRuleFieldSummary(riskRuleFields),
|
||||||
|
riskRuleFlow: resolveRiskRuleFlow(rulePayload, riskRuleFields),
|
||||||
|
riskRuleFlowDiagramSvg: resolveRiskRuleFlowDiagramSvg({
|
||||||
|
...rulePayload,
|
||||||
|
flow_diagram_svg: normalizeText(apiPayload?.flow_diagram_svg) || rulePayload?.flow_diagram_svg
|
||||||
|
}),
|
||||||
|
riskRuleRequiresAttachment: Boolean(
|
||||||
|
rulePayload.requires_attachment ||
|
||||||
|
metadata.requires_attachment ||
|
||||||
|
apiConfig.requires_attachment ||
|
||||||
|
target.configJson?.requires_attachment
|
||||||
|
),
|
||||||
|
riskRuleSummary: {
|
||||||
|
name: apiPayload?.name || target.name,
|
||||||
|
evaluator: apiPayload?.evaluator || rulePayload.evaluator || '',
|
||||||
|
ontologySignal: apiPayload?.ontology_signal || rulePayload.ontology_signal || '',
|
||||||
|
inputs: apiPayload?.inputs || rulePayload.inputs || {},
|
||||||
|
outcomes: apiPayload?.outcomes || rulePayload.outcomes || {}
|
||||||
|
},
|
||||||
|
riskRuleJsonText: JSON.stringify(rulePayload, null, 2),
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
328
web/src/views/scripts/auditViewRuleClassifier.js
Normal file
328
web/src/views/scripts/auditViewRuleClassifier.js
Normal file
@@ -0,0 +1,328 @@
|
|||||||
|
import {
|
||||||
|
JSON_RISK_DETAIL_MODE,
|
||||||
|
LEGACY_RISK_SCENARIO_KEYS,
|
||||||
|
RISK_SCENARIO_VALUES,
|
||||||
|
RULE_TAB_TAG_ALIASES,
|
||||||
|
SPREADSHEET_DETAIL_MODE,
|
||||||
|
TAB_META,
|
||||||
|
TYPE_META
|
||||||
|
} from './auditViewMetadata.js'
|
||||||
|
import {
|
||||||
|
isPlainObject,
|
||||||
|
normalizeText,
|
||||||
|
readConfigJson
|
||||||
|
} from './auditViewDataUtils.js'
|
||||||
|
import { formatScenarioList } from './auditViewFormatters.js'
|
||||||
|
const EXPENSE_TYPE_SCENARIO_LABELS = {
|
||||||
|
travel: '差旅费',
|
||||||
|
hotel: '住宿费',
|
||||||
|
transport: '交通费',
|
||||||
|
meal: '业务招待费',
|
||||||
|
meeting: '会务费',
|
||||||
|
marketing: '市场推广费',
|
||||||
|
office: '办公用品费',
|
||||||
|
training: '培训费',
|
||||||
|
software: '软件服务费',
|
||||||
|
communication: '通信费',
|
||||||
|
welfare: '福利费'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readRuleDocumentMeta(value) {
|
||||||
|
const configJson = readConfigJson(value)
|
||||||
|
return isPlainObject(configJson.rule_document) ? configJson.rule_document : null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isSpreadsheetRuleSource(value) {
|
||||||
|
const configJson = readConfigJson(value)
|
||||||
|
return normalizeText(configJson.detail_mode || configJson.rule_detail_mode).toLowerCase() === SPREADSHEET_DETAIL_MODE
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isJsonRiskRuleSource(value) {
|
||||||
|
const configJson = readConfigJson(value)
|
||||||
|
return normalizeText(configJson.detail_mode || configJson.rule_detail_mode).toLowerCase() === JSON_RISK_DETAIL_MODE
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeRuleTagValue(value) {
|
||||||
|
return normalizeText(value).toLowerCase().replace(/[\s_-]+/g, '')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function collectRuleTagValues(source) {
|
||||||
|
const configJson = readConfigJson(source)
|
||||||
|
const rawValues = [
|
||||||
|
configJson.tag,
|
||||||
|
configJson.rule_tag,
|
||||||
|
...(Array.isArray(configJson.tags) ? configJson.tags : []),
|
||||||
|
...(Array.isArray(configJson.rule_tags) ? configJson.rule_tags : [])
|
||||||
|
]
|
||||||
|
|
||||||
|
return rawValues.map((item) => normalizeText(item)).filter(Boolean)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveRuleTabId(source) {
|
||||||
|
const code = normalizeText(source?.code || '').toLowerCase()
|
||||||
|
if (code.startsWith('risk.')) {
|
||||||
|
return 'riskRules'
|
||||||
|
}
|
||||||
|
if (isJsonRiskRuleSource(source)) {
|
||||||
|
return 'riskRules'
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedTags = collectRuleTagValues(source).map((item) => normalizeRuleTagValue(item))
|
||||||
|
|
||||||
|
if (normalizedTags.some((item) => RULE_TAB_TAG_ALIASES.riskRules.has(item))) {
|
||||||
|
return 'riskRules'
|
||||||
|
}
|
||||||
|
if (normalizedTags.some((item) => RULE_TAB_TAG_ALIASES.financialRules.has(item))) {
|
||||||
|
return 'financialRules'
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveTabId(source, typeKey) {
|
||||||
|
if (typeKey === 'rules') {
|
||||||
|
return resolveRuleTabId(source)
|
||||||
|
}
|
||||||
|
return typeKey
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveTabMeta(tabId, typeKey) {
|
||||||
|
if (TAB_META[tabId]) {
|
||||||
|
return TAB_META[tabId]
|
||||||
|
}
|
||||||
|
if (typeKey === 'rules') {
|
||||||
|
return {
|
||||||
|
...TYPE_META.rules,
|
||||||
|
typeKey: 'rules',
|
||||||
|
badgeTone: 'primary'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return TAB_META[typeKey]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveRiskRuleDescription(payload) {
|
||||||
|
if (!isPlainObject(payload)) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
return normalizeText(payload.description)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveRiskRuleSourceRef(payload) {
|
||||||
|
if (!isPlainObject(payload)) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
const metadata = isPlainObject(payload.metadata) ? payload.metadata : {}
|
||||||
|
return normalizeText(metadata.source_ref)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function inferRiskCategoryFromCode(code) {
|
||||||
|
const normalized = normalizeText(code).toLowerCase()
|
||||||
|
if (normalized.startsWith('risk.travel.')) {
|
||||||
|
return '差旅'
|
||||||
|
}
|
||||||
|
if (normalized.startsWith('risk.invoice.')) {
|
||||||
|
return '发票'
|
||||||
|
}
|
||||||
|
if (normalized.includes('entertainment') || normalized.includes('meal_localized')) {
|
||||||
|
return '餐饮招待'
|
||||||
|
}
|
||||||
|
if (normalized.includes('consecutive_transport')) {
|
||||||
|
return '交通出行'
|
||||||
|
}
|
||||||
|
if (normalized.startsWith('risk.expense.')) {
|
||||||
|
return '费用科目'
|
||||||
|
}
|
||||||
|
return '通用'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeRiskScenarioCategory(value) {
|
||||||
|
const normalized = normalizeText(value)
|
||||||
|
const alias = normalized === '通讯费' ? '通信费' : normalized
|
||||||
|
return RISK_SCENARIO_VALUES.has(alias) ? alias : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeExpenseTypeScenarioLabels(value) {
|
||||||
|
const values = Array.isArray(value) ? value : normalizeText(value) ? [value] : []
|
||||||
|
const labels = []
|
||||||
|
const seen = new Set()
|
||||||
|
|
||||||
|
values.forEach((item) => {
|
||||||
|
const key = normalizeText(item).toLowerCase()
|
||||||
|
const label = EXPENSE_TYPE_SCENARIO_LABELS[key] || normalizeRiskScenarioCategory(item)
|
||||||
|
if (!label || seen.has(label)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
seen.add(label)
|
||||||
|
labels.push(label)
|
||||||
|
})
|
||||||
|
|
||||||
|
return labels
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readRiskRuleExpenseTypes(source) {
|
||||||
|
const configJson = readConfigJson(source)
|
||||||
|
const metadata = isPlainObject(configJson.metadata) ? configJson.metadata : {}
|
||||||
|
const appliesTo = isPlainObject(configJson.applies_to) ? configJson.applies_to : {}
|
||||||
|
const values = []
|
||||||
|
|
||||||
|
;[
|
||||||
|
configJson.expense_types,
|
||||||
|
metadata.expense_types,
|
||||||
|
appliesTo.expense_types,
|
||||||
|
source?.expense_types
|
||||||
|
].forEach((item) => {
|
||||||
|
if (Array.isArray(item)) {
|
||||||
|
values.push(...item)
|
||||||
|
} else if (normalizeText(item)) {
|
||||||
|
values.push(item)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return values
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readScenarioItems(source) {
|
||||||
|
if (Array.isArray(source?.scenario_json)) {
|
||||||
|
return source.scenario_json
|
||||||
|
}
|
||||||
|
if (Array.isArray(source?.scenarioList)) {
|
||||||
|
return source.scenarioList
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveRiskRuleCategory(source) {
|
||||||
|
const configJson = readConfigJson(source)
|
||||||
|
const expenseScenarioLabels = normalizeExpenseTypeScenarioLabels(readRiskRuleExpenseTypes(source))
|
||||||
|
if (expenseScenarioLabels.length) {
|
||||||
|
return formatScenarioList(expenseScenarioLabels)
|
||||||
|
}
|
||||||
|
|
||||||
|
const expenseCategoryLabel =
|
||||||
|
normalizeText(configJson.expense_category_label) ||
|
||||||
|
normalizeText(configJson.metadata?.expense_category_label) ||
|
||||||
|
normalizeText(source?.expense_category_label)
|
||||||
|
if (expenseCategoryLabel) {
|
||||||
|
return expenseCategoryLabel
|
||||||
|
}
|
||||||
|
|
||||||
|
const explicit = normalizeRiskScenarioCategory(configJson.risk_category)
|
||||||
|
if (explicit) {
|
||||||
|
return explicit
|
||||||
|
}
|
||||||
|
|
||||||
|
const payloadCategory = normalizeRiskScenarioCategory(source?.risk_category)
|
||||||
|
if (payloadCategory) {
|
||||||
|
return payloadCategory
|
||||||
|
}
|
||||||
|
|
||||||
|
const scenarioItems = readScenarioItems(source)
|
||||||
|
const businessScenario = scenarioItems
|
||||||
|
.map((item) => normalizeText(item))
|
||||||
|
.find((item) => item && !LEGACY_RISK_SCENARIO_KEYS.has(item) && RISK_SCENARIO_VALUES.has(item))
|
||||||
|
if (businessScenario) {
|
||||||
|
return businessScenario
|
||||||
|
}
|
||||||
|
|
||||||
|
return inferRiskCategoryFromCode(source?.code)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function inferFinancialRuleCategory(source) {
|
||||||
|
const configJson = readConfigJson(source)
|
||||||
|
const explicit =
|
||||||
|
normalizeRiskScenarioCategory(configJson.scenario_category) ||
|
||||||
|
normalizeRiskScenarioCategory(configJson.ai_review_category) ||
|
||||||
|
normalizeRiskScenarioCategory(configJson.risk_category) ||
|
||||||
|
normalizeRiskScenarioCategory(source?.scenario_category) ||
|
||||||
|
normalizeRiskScenarioCategory(source?.risk_category)
|
||||||
|
if (explicit) {
|
||||||
|
return explicit
|
||||||
|
}
|
||||||
|
|
||||||
|
const scenarioCategory = readScenarioItems(source)
|
||||||
|
.map((item) => normalizeRiskScenarioCategory(item))
|
||||||
|
.find(Boolean)
|
||||||
|
if (scenarioCategory) {
|
||||||
|
return scenarioCategory
|
||||||
|
}
|
||||||
|
|
||||||
|
const configRuntimeRule = isPlainObject(configJson.runtime_rule) ? configJson.runtime_rule : {}
|
||||||
|
const haystack = [
|
||||||
|
source?.code,
|
||||||
|
source?.name,
|
||||||
|
source?.description,
|
||||||
|
configJson.runtime_kind,
|
||||||
|
configRuntimeRule.kind,
|
||||||
|
configRuntimeRule.scenario,
|
||||||
|
configRuntimeRule.template_key,
|
||||||
|
...readScenarioItems(source)
|
||||||
|
]
|
||||||
|
.map((item) => normalizeText(item).toLowerCase())
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ')
|
||||||
|
|
||||||
|
if (!haystack) {
|
||||||
|
return '通用'
|
||||||
|
}
|
||||||
|
if (/(travel|trip|差旅|出差|住宿|酒店)/i.test(haystack)) {
|
||||||
|
return '差旅'
|
||||||
|
}
|
||||||
|
if (/(invoice|receipt|attachment|票据|发票|单据|附件)/i.test(haystack)) {
|
||||||
|
return '发票'
|
||||||
|
}
|
||||||
|
if (/(meal|dining|entertainment|餐饮|招待|餐费|用餐)/i.test(haystack)) {
|
||||||
|
return '餐饮招待'
|
||||||
|
}
|
||||||
|
if (/(transport|traffic|taxi|交通|出行|打车|机票|火车|高铁|地铁|公交)/i.test(haystack)) {
|
||||||
|
return '交通出行'
|
||||||
|
}
|
||||||
|
if (/(office|material|suppl|办公|物料|耗材)/i.test(haystack)) {
|
||||||
|
return '办公物料'
|
||||||
|
}
|
||||||
|
if (/(communication|telecom|phone|通信|通讯|手机)/i.test(haystack)) {
|
||||||
|
return '通信费'
|
||||||
|
}
|
||||||
|
if (/(welfare|福利)/i.test(haystack)) {
|
||||||
|
return '福利费'
|
||||||
|
}
|
||||||
|
if (/(expense_standard|费用科目|费用标准|补贴|科目)/i.test(haystack)) {
|
||||||
|
return '费用科目'
|
||||||
|
}
|
||||||
|
return '通用'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveRuleScenarioCategory(source, tabId = '') {
|
||||||
|
const scenarioList = resolveRuleScenarioList(source, tabId)
|
||||||
|
if (scenarioList.length) {
|
||||||
|
return formatScenarioList(scenarioList)
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveRuleScenarioList(source, tabId = '') {
|
||||||
|
const resolvedTabId = tabId || resolveRuleTabId(source)
|
||||||
|
if (resolvedTabId === 'riskRules' || isJsonRiskRuleSource(source)) {
|
||||||
|
const expenseScenarioLabels = normalizeExpenseTypeScenarioLabels(readRiskRuleExpenseTypes(source))
|
||||||
|
if (expenseScenarioLabels.length) {
|
||||||
|
return expenseScenarioLabels
|
||||||
|
}
|
||||||
|
const riskCategory = resolveRiskRuleCategory(source)
|
||||||
|
return riskCategory ? [riskCategory] : []
|
||||||
|
}
|
||||||
|
if (resolvedTabId === 'financialRules') {
|
||||||
|
const financialCategory = inferFinancialRuleCategory(source)
|
||||||
|
return financialCategory ? [financialCategory] : []
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildRiskListSubtitle(text, maxLength = 42) {
|
||||||
|
const normalized = normalizeText(text)
|
||||||
|
if (!normalized) {
|
||||||
|
return '平台内置风险规则'
|
||||||
|
}
|
||||||
|
const firstSentence = normalized.split(/[。;;!?\n]/)[0] || normalized
|
||||||
|
if (firstSentence.length <= maxLength) {
|
||||||
|
return firstSentence
|
||||||
|
}
|
||||||
|
return `${firstSentence.slice(0, maxLength)}…`
|
||||||
|
}
|
||||||
117
web/src/views/scripts/auditViewRuleContentModel.js
Normal file
117
web/src/views/scripts/auditViewRuleContentModel.js
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
import {
|
||||||
|
EXPENSE_RULE_BLOCK_PATTERN,
|
||||||
|
RULE_SPREADSHEET_BLOCK_PATTERN,
|
||||||
|
RULE_TEMPLATE_LABELS
|
||||||
|
} from './auditViewMetadata.js'
|
||||||
|
import {
|
||||||
|
isPlainObject,
|
||||||
|
normalizeText,
|
||||||
|
readConfigJson
|
||||||
|
} from './auditViewDataUtils.js'
|
||||||
|
|
||||||
|
export function cloneJsonObject(value) {
|
||||||
|
if (!isPlainObject(value)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return JSON.parse(JSON.stringify(value))
|
||||||
|
} catch {
|
||||||
|
return { ...value }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveRuleTemplateLabel(value) {
|
||||||
|
const templateKey = normalizeText(value)
|
||||||
|
return RULE_TEMPLATE_LABELS[templateKey] || templateKey || '未指定模板'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extractRuntimeRuleFromMarkdown(markdown) {
|
||||||
|
const match = String(markdown || '').match(EXPENSE_RULE_BLOCK_PATTERN)
|
||||||
|
if (!match) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload = JSON.parse(match[1])
|
||||||
|
return isPlainObject(payload) ? payload : null
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extractSpreadsheetMetaFromMarkdown(markdown) {
|
||||||
|
const match = String(markdown || '').match(RULE_SPREADSHEET_BLOCK_PATTERN)
|
||||||
|
if (!match) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload = JSON.parse(match[1])
|
||||||
|
return isPlainObject(payload) ? payload : null
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stripRuntimeRuleBlock(markdown) {
|
||||||
|
return String(markdown || '')
|
||||||
|
.replace(EXPENSE_RULE_BLOCK_PATTERN, '')
|
||||||
|
.replace(/\n{3,}/g, '\n\n')
|
||||||
|
.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stringifyRuntimeRule(runtimeRule) {
|
||||||
|
return JSON.stringify(isPlainObject(runtimeRule) ? runtimeRule : {}, null, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseRuntimeRuleText(runtimeRuleText) {
|
||||||
|
const text = normalizeText(runtimeRuleText)
|
||||||
|
if (!text) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload = JSON.parse(text)
|
||||||
|
return isPlainObject(payload) ? payload : null
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildDefaultRuntimeRule(source) {
|
||||||
|
const configJson = readConfigJson(source)
|
||||||
|
const scenarioItems = Array.isArray(source?.scenario_json)
|
||||||
|
? source.scenario_json
|
||||||
|
: Array.isArray(source?.scenarioList)
|
||||||
|
? source.scenarioList
|
||||||
|
: []
|
||||||
|
const configRuntimeRule = cloneJsonObject(configJson.runtime_rule)
|
||||||
|
|
||||||
|
return {
|
||||||
|
kind: normalizeText(configRuntimeRule?.kind || configJson.runtime_kind) || 'policy_rule_draft',
|
||||||
|
version:
|
||||||
|
typeof configRuntimeRule?.version === 'number' && Number.isFinite(configRuntimeRule.version)
|
||||||
|
? configRuntimeRule.version
|
||||||
|
: 1,
|
||||||
|
template_key:
|
||||||
|
normalizeText(configRuntimeRule?.template_key || configJson.rule_template_key) || 'general_policy_v1',
|
||||||
|
rule_name: normalizeText(configRuntimeRule?.rule_name || source?.name) || '未命名规则',
|
||||||
|
scenario: normalizeText(configRuntimeRule?.scenario || scenarioItems[0]) || 'expense',
|
||||||
|
review_required:
|
||||||
|
typeof configRuntimeRule?.review_required === 'boolean' ? configRuntimeRule.review_required : true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveRuntimeRuleForVersion(source, rawMarkdown, runtimeRuleFallback = null) {
|
||||||
|
return (
|
||||||
|
cloneJsonObject(extractRuntimeRuleFromMarkdown(rawMarkdown)) ||
|
||||||
|
cloneJsonObject(runtimeRuleFallback) ||
|
||||||
|
buildDefaultRuntimeRule(source)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildMarkdownVersionContent(markdownContent, runtimeRule) {
|
||||||
|
const body = stripRuntimeRuleBlock(markdownContent)
|
||||||
|
const runtimeBlock = ['```expense-rule', stringifyRuntimeRule(runtimeRule), '```'].join('\n')
|
||||||
|
return body ? `${body}\n\n${runtimeBlock}` : runtimeBlock
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user