style: 全局 UI 主题皮肤重构与样式模块化

引入 Element Plus 主题定制和主题皮肤 composable,将全局
样式拆分为组件级独立 CSS 文件(侧边栏、顶栏、工作台等),
统一色彩变量和间距规范,重构所有视图和组件样式以适配新
主题系统,优化图表和知识图谱组件视觉表现,提取审计和差
旅报销相关子组件。
This commit is contained in:
caoxiaozhu
2026-05-27 09:17:57 +08:00
parent df49103f23
commit 2dcc72102d
112 changed files with 10983 additions and 8996 deletions

View File

@@ -0,0 +1,334 @@
<template>
<article class="skill-list panel">
<nav class="status-tabs" aria-label="能力类型">
<button
v-for="tab in tabs"
:key="tab.id"
type="button"
:class="{ active: activeType === tab.id }"
@click="emit('update:activeType', tab.id)"
>
{{ tab.label }}
</button>
</nav>
<div class="list-toolbar">
<div class="filter-set">
<label class="search-filter">
<i class="mdi mdi-magnify"></i>
<input
:value="keyword"
type="search"
:placeholder="searchPlaceholder"
@input="emit('update:keyword', $event.target.value)"
/>
</label>
<AuditPickerFilter
id="domain"
title="选择业务域"
close-label="关闭业务域选择"
:active-filter-popover="activeFilterPopover"
:label="selectedDomainLabel"
:options="domainOptions"
:selected-value="selectedDomain"
@toggle="emit('toggle-filter-popover', $event)"
@close="emit('close-filter-popover')"
@select="selectFilter('domain', $event)"
/>
<AuditPickerFilter
v-if="showOwnerFilter"
id="owner"
title="选择负责人"
close-label="关闭负责人选择"
:active-filter-popover="activeFilterPopover"
:label="selectedOwnerLabel"
:options="ownerOptions"
:selected-value="selectedOwner"
@toggle="emit('toggle-filter-popover', $event)"
@close="emit('close-filter-popover')"
@select="selectFilter('owner', $event)"
/>
<AuditPickerFilter
v-if="showRiskLevelFilter"
id="riskLevel"
title="选择风险等级"
close-label="关闭风险等级选择"
:active-filter-popover="activeFilterPopover"
:label="selectedRiskLevelLabel"
:options="riskLevelOptions"
:selected-value="selectedRiskLevel"
@toggle="emit('toggle-filter-popover', $event)"
@close="emit('close-filter-popover')"
@select="selectFilter('riskLevel', $event)"
/>
<AuditPickerFilter
v-if="showRiskScenarioFilter"
id="riskScenario"
title="选择使用场景"
close-label="关闭使用场景选择"
:active-filter-popover="activeFilterPopover"
:label="selectedRiskScenarioLabel"
:options="riskScenarioOptions"
:selected-value="selectedRiskScenario"
@toggle="emit('toggle-filter-popover', $event)"
@close="emit('close-filter-popover')"
@select="selectFilter('riskScenario', $event)"
/>
<AuditPickerFilter
v-if="showOnlineFilter"
id="online"
title="选择上线状态"
close-label="关闭上线状态选择"
:active-filter-popover="activeFilterPopover"
:label="selectedOnlineStateLabel"
:options="onlineStateOptions"
:selected-value="selectedOnlineState"
@toggle="emit('toggle-filter-popover', $event)"
@close="emit('close-filter-popover')"
@select="selectFilter('online', $event)"
/>
<AuditPickerFilter
v-if="showEnabledFilter"
id="enabled"
title="选择启用状态"
close-label="关闭启用状态选择"
:active-filter-popover="activeFilterPopover"
:label="selectedEnabledStateLabel"
:options="enabledStateOptions"
:selected-value="selectedEnabledState"
@toggle="emit('toggle-filter-popover', $event)"
@close="emit('close-filter-popover')"
@select="selectFilter('enabled', $event)"
/>
<AuditPickerFilter
v-if="showStatusFilter"
id="status"
title="选择状态"
close-label="关闭状态选择"
:active-filter-popover="activeFilterPopover"
:label="selectedStatusLabel"
:options="statusOptions"
:selected-value="selectedStatus"
@toggle="emit('toggle-filter-popover', $event)"
@close="emit('close-filter-popover')"
@select="selectFilter('status', $event)"
/>
</div>
<div class="toolbar-actions">
<button
v-if="activeFilterTokens.length"
class="ghost-filter-btn"
type="button"
@click="emit('reset-filters')"
>
<i class="mdi mdi-filter-remove-outline"></i>
<span>清空筛选</span>
</button>
<button
class="create-btn"
type="button"
:disabled="!canCreateRiskRule"
@click="emit('create-risk-rule')"
>
<i class="mdi mdi-plus"></i>
<span>{{ createButtonLabel }}</span>
</button>
</div>
</div>
<p class="hint"><i class="mdi mdi-information-outline"></i> {{ hintText }}</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"
: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>
<p>{{ errorMessage }}</p>
</div>
<TableEmptyState
v-else-if="!visibleSkills.length"
: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>
<tr>
<th>{{ tableColumns.name }}</th>
<th>{{ tableColumns.category }}</th>
<th>{{ tableColumns.owner }}</th>
<th>{{ tableColumns.scope }}</th>
<th v-if="showRuntimeColumn">{{ tableColumns.runtime }}</th>
<th v-if="showVersionColumn">{{ tableColumns.version }}</th>
<th v-if="showStatusColumn">{{ tableColumns.status || '状态' }}</th>
<th v-if="showMetricColumn">{{ tableColumns.metric }}</th>
<th v-if="showOnlineColumn">是否上线</th>
<th v-if="showEnabledColumn">是否启用</th>
<th>{{ tableColumns.updatedAt || '最近更新' }}</th>
</tr>
</thead>
<tbody>
<tr
v-for="skill in visibleSkills"
:key="skill.id"
:class="{ 'is-disabled': skill.usesJsonRiskRule && skill.statusValue === 'generating' }"
@click="emit('open-asset-detail', skill)"
>
<td>
<div class="skill-name-cell">
<span class="skill-avatar" :class="skill.badgeTone">{{ skill.short }}</span>
<div>
<strong>{{ skill.name }}</strong>
<span class="skill-list-subtitle">{{ skill.listSubtitle || skill.summary }}</span>
</div>
</div>
</td>
<td>{{ skill.category }}</td>
<td>
<span
v-if="skill.usesJsonRiskRule"
class="json-risk-meta-badge"
:class="skill.riskLevelTone"
>
{{ skill.riskLevelLabel || '-' }}
</span>
<template v-else>{{ skill.owner }}</template>
</td>
<td><span class="scope-pill">{{ skill.scope }}</span></td>
<td v-if="showRuntimeColumn">{{ skill.model }}</td>
<td v-if="showVersionColumn">{{ skill.versionDisplay || skill.version }}</td>
<td v-if="showStatusColumn">
<span class="status-pill" :class="skill.statusTone">{{ skill.status }}</span>
</td>
<td v-if="showMetricColumn">{{ skill.hitRate }}</td>
<td v-if="showOnlineColumn">
<span class="status-pill" :class="skill.isOnlineTone">{{ skill.isOnlineLabel }}</span>
</td>
<td v-if="showEnabledColumn">
<span class="status-pill" :class="skill.isEnabledTone">{{ skill.isEnabledLabel }}</span>
</td>
<td>{{ skill.updatedAt }}</td>
</tr>
</tbody>
</table>
</div>
<footer v-if="!loading && !errorMessage && visibleSkills.length" class="list-foot">
<span class="page-summary">当前展示 {{ visibleSkills.length }} 条资产</span>
</footer>
</article>
</template>
<script setup>
import AuditPickerFilter from './AuditPickerFilter.vue'
import TableEmptyState from '../shared/TableEmptyState.vue'
import TableLoadingState from '../shared/TableLoadingState.vue'
defineOptions({
name: 'AuditAssetList'
})
defineProps({
tabs: { type: Array, default: () => [] },
activeType: { type: String, default: '' },
activeTabLabel: { type: String, default: '' },
keyword: { type: String, default: '' },
searchPlaceholder: { type: String, default: '' },
createButtonLabel: { type: String, default: '' },
hintText: { type: String, default: '' },
tableColumns: { type: Object, default: () => ({}) },
showRuntimeColumn: { type: Boolean, default: false },
showVersionColumn: { type: Boolean, default: false },
showMetricColumn: { type: Boolean, default: false },
showStatusColumn: { type: Boolean, default: false },
showOnlineColumn: { type: Boolean, default: false },
showEnabledColumn: { type: Boolean, default: false },
visibleSkills: { type: Array, default: () => [] },
auditEmptyState: { type: Object, default: () => ({}) },
loading: { type: Boolean, default: false },
errorMessage: { type: String, default: '' },
selectedDomain: { type: String, default: '' },
selectedOwner: { type: String, default: '' },
selectedRiskLevel: { type: String, default: '' },
selectedStatus: { type: String, default: '' },
selectedRiskScenario: { type: String, default: '' },
selectedOnlineState: { type: String, default: '' },
selectedEnabledState: { type: String, default: '' },
selectedDomainLabel: { type: String, default: '' },
selectedOwnerLabel: { type: String, default: '' },
selectedRiskLevelLabel: { type: String, default: '' },
selectedStatusLabel: { type: String, default: '' },
selectedRiskScenarioLabel: { type: String, default: '' },
selectedOnlineStateLabel: { type: String, default: '' },
selectedEnabledStateLabel: { type: String, default: '' },
showRiskScenarioFilter: { type: Boolean, default: false },
showOwnerFilter: { type: Boolean, default: false },
showRiskLevelFilter: { type: Boolean, default: false },
showStatusFilter: { type: Boolean, default: false },
showOnlineFilter: { type: Boolean, default: false },
showEnabledFilter: { type: Boolean, default: false },
domainOptions: { type: Array, default: () => [] },
ownerOptions: { type: Array, default: () => [] },
riskLevelOptions: { type: Array, default: () => [] },
statusOptions: { type: Array, default: () => [] },
riskScenarioOptions: { type: Array, default: () => [] },
onlineStateOptions: { type: Array, default: () => [] },
enabledStateOptions: { type: Array, default: () => [] },
activeFilterPopover: { type: String, default: '' },
activeFilterTokens: { type: Array, default: () => [] },
canCreateRiskRule: { type: Boolean, default: false }
})
const emit = defineEmits([
'update:activeType',
'update:keyword',
'toggle-filter-popover',
'close-filter-popover',
'select-filter',
'reset-filters',
'create-risk-rule',
'empty-action',
'open-asset-detail'
])
function selectFilter(type, value) {
emit('select-filter', type, value)
}
</script>
<style scoped src="../../assets/styles/views/audit-view.css"></style>
<style scoped src="../../assets/styles/views/audit-view-part2.css"></style>

View File

@@ -0,0 +1,173 @@
<template>
<section class="json-risk-editor-shell panel">
<header class="json-risk-editor-head asset-detail-topbar list-toolbar">
<div class="json-risk-editor-title asset-detail-topbar-main filter-set">
<div class="json-risk-head-copy">
<div class="json-risk-head-title-row">
<h2>{{ selectedSkill.name }}</h2>
</div>
<p class="json-risk-head-subtitle">
{{ selectedSkill.riskRuleSubtitle || '平台通用风险规则' }}
</p>
<div class="json-risk-head-meta">
<span v-if="selectedSkill.riskCategory">适用场景{{ selectedSkill.riskCategory }}</span>
<span>业务域{{ selectedSkill.category || '-' }}</span>
<span>最近更新{{ selectedSkill.updatedAt || '-' }}</span>
</div>
</div>
</div>
<div
class="json-risk-score-ring"
:class="selectedSkill.riskRuleScoreLevel || selectedSkill.riskRuleSeverity"
>
<strong>{{ selectedSkill.riskRuleScore ?? '--' }}</strong>
<span>风险分</span>
<em>{{ selectedSkill.riskRuleScoreLabel || selectedSkill.riskRuleSeverityLabel }}</em>
</div>
</header>
<div
v-if="selectedSkill.riskRuleGenerationFailed"
class="json-risk-generation-failure"
>
<i class="mdi mdi-alert-circle-outline"></i>
<div>
<h3>风险规则生成失败</h3>
<p>这条规则没有生成出可执行的 JSON 模板和流程图管理员可以删除后重新创建</p>
<small v-if="selectedSkill.riskRuleGenerationError">
失败原因{{ selectedSkill.riskRuleGenerationError }}
</small>
</div>
</div>
<div v-else class="json-risk-editor-body">
<section class="json-risk-main-stage">
<article class="detail-card panel json-risk-summary-card">
<div class="card-head">
<div>
<h3>基本信息</h3>
<p>这条规则的业务域风险等级创建时间上线状态和最近操作</p>
</div>
</div>
<div class="json-risk-meta-grid">
<div class="json-risk-meta-item">
<span class="json-risk-meta-label">业务域</span>
<span class="json-risk-meta-value">{{ selectedSkill.category || '-' }}</span>
</div>
<div class="json-risk-meta-item">
<span class="json-risk-meta-label">适用场景</span>
<span class="json-risk-meta-value">{{ selectedSkill.riskCategory || selectedSkill.scope || '-' }}</span>
</div>
<div class="json-risk-meta-item">
<span class="json-risk-meta-label">业务环节</span>
<span class="json-risk-meta-value">{{ selectedSkill.businessStageLabel || '-' }}</span>
</div>
<div class="json-risk-meta-item">
<span class="json-risk-meta-label">风险等级</span>
<span class="json-risk-meta-value">
<span class="json-risk-meta-badge" :class="selectedSkill.riskRuleSeverity">
{{ selectedSkill.riskRuleSeverityLabel || '-' }}
</span>
</span>
</div>
<div class="json-risk-meta-item">
<span class="json-risk-meta-label">是否上线</span>
<span class="json-risk-meta-value">
<span class="json-risk-meta-badge" :class="selectedSkill.isOnlineValue ? 'test-passed' : 'test-pending'">
{{ selectedSkill.isOnlineLabel || '待上线' }}
</span>
</span>
</div>
<div class="json-risk-meta-item">
<span class="json-risk-meta-label">测试状态</span>
<span class="json-risk-meta-value">
<span class="json-risk-meta-badge" :class="riskRuleTestPassed ? 'test-passed' : 'test-pending'">
{{ riskRuleTestPassed ? '已确认通过' : '待测试确认' }}
</span>
</span>
</div>
<div class="json-risk-meta-item">
<span class="json-risk-meta-label">创建者</span>
<span class="json-risk-meta-value">{{ selectedSkill.creator || selectedSkill.publisher || '-' }}</span>
</div>
<div class="json-risk-meta-item">
<span class="json-risk-meta-label">创建时间</span>
<span class="json-risk-meta-value">
{{ selectedSkill.riskRuleCreatedAt || selectedSkill.updatedAt }}
<span v-if="selectedSkill.riskRuleAgeLabel" class="meta-value-hint">
({{ selectedSkill.riskRuleAgeLabel }})
</span>
</span>
</div>
<div class="json-risk-meta-item">
<span class="json-risk-meta-label">上线时间</span>
<span class="json-risk-meta-value">{{ selectedSkill.publishedAt || '-' }}</span>
</div>
<div class="json-risk-meta-item">
<span class="json-risk-meta-label">最后操作</span>
<span class="json-risk-meta-value">{{ selectedSkill.lastOperationLabel || '-' }}</span>
</div>
<div class="json-risk-meta-item full-width">
<span class="json-risk-meta-label">使用字段</span>
<span class="json-risk-meta-value">{{ selectedSkill.riskRuleFieldSummary || '-' }}</span>
</div>
</div>
</article>
<article
v-if="selectedSkill.riskRuleBusinessDescription"
class="detail-card panel json-risk-description-card"
>
<div class="card-head">
<div>
<h3>业务说明</h3>
<p>面向规则制定者和审核人的自然语言说明</p>
</div>
</div>
<p class="json-risk-description-text">{{ selectedSkill.riskRuleBusinessDescription }}</p>
<p
v-if="selectedSkill.riskRuleSourceRef"
class="json-risk-description-source"
>
来源{{ selectedSkill.riskRuleSourceRef }}
</p>
</article>
<article class="detail-card panel json-risk-flow-card">
<div class="card-head">
<div>
<h3>判断流程</h3>
<p>规则从业务单据开始读取字段证据后按判断依据决定是否进入复核</p>
</div>
</div>
<RiskRuleFlowDiagram
:svg="selectedSkill.riskRuleFlowDiagramSvg"
:flow="selectedSkill.riskRuleFlow"
:fields="selectedSkill.riskRuleFields"
:severity="selectedSkill.riskRuleSeverity"
:severity-label="selectedSkill.riskRuleSeverityLabel"
/>
</article>
</section>
</div>
</section>
</template>
<script setup>
import RiskRuleFlowDiagram from '../shared/RiskRuleFlowDiagram.vue'
defineOptions({
name: 'AuditJsonRiskRuleDetail'
})
defineProps({
selectedSkill: { type: Object, required: true },
riskRuleTestPassed: { type: Boolean, default: false }
})
</script>
<style scoped src="../../assets/styles/views/audit-view.css"></style>
<style scoped src="../../assets/styles/views/audit-view-part2.css"></style>

View File

@@ -0,0 +1,60 @@
<template>
<div class="picker-filter" :class="{ open: activeFilterPopover === id }">
<button
class="picker-trigger"
type="button"
:aria-expanded="activeFilterPopover === id"
aria-haspopup="dialog"
@click="emit('toggle', id)"
>
<span class="picker-label">{{ label }}</span>
<i class="mdi mdi-chevron-down"></i>
</button>
<div
v-if="activeFilterPopover === id"
class="picker-popover"
role="dialog"
:aria-label="title"
>
<header>
<strong>{{ title }}</strong>
<button type="button" :aria-label="closeLabel" @click="emit('close')">
<i class="mdi mdi-close"></i>
</button>
</header>
<div class="picker-option-list">
<button
v-for="option in options"
:key="option.value || `all-${id}`"
type="button"
class="picker-option"
:class="{ active: selectedValue === option.value }"
@click="emit('select', option.value)"
>
{{ option.label }}
</button>
</div>
</div>
</div>
</template>
<script setup>
defineOptions({
name: 'AuditPickerFilter'
})
defineProps({
id: { type: String, required: true },
title: { type: String, required: true },
closeLabel: { type: String, required: true },
activeFilterPopover: { type: String, default: '' },
label: { type: String, default: '' },
options: { type: Array, default: () => [] },
selectedValue: { type: [String, Number, Boolean], default: '' }
})
const emit = defineEmits(['toggle', 'close', 'select'])
</script>
<style scoped src="../../assets/styles/views/audit-view.css"></style>
<style scoped src="../../assets/styles/views/audit-view-part2.css"></style>

View File

@@ -0,0 +1,293 @@
<template>
<ConfirmDialog
:open="riskRuleCreateOpen"
badge="自然语言规则"
badge-tone="info"
title="新建风险规则"
description="默认创建费用类风险规则。选择业务环节和费用领域后填写规则标题与自然语言描述,系统会根据评分模型自动计算风险分数和等级。"
cancel-text="取消"
confirm-text="开始生成"
busy-text="生成中..."
confirm-tone="primary"
confirm-icon="mdi mdi-auto-fix"
:busy="riskRuleCreateBusy"
:close-on-mask="!riskRuleCreateBusy"
@close="emit('close-risk-rule-create')"
@confirm="emit('submit-risk-rule-create')"
>
<div class="risk-rule-create-form">
<label>
<span>业务环节</span>
<EnterpriseSelect
v-model="riskRuleCreateForm.business_stage"
:options="riskRuleBusinessStageOptions"
:disabled="riskRuleCreateBusy"
/>
</label>
<label>
<span>费用领域</span>
<EnterpriseSelect
v-model="riskRuleCreateForm.expense_category"
:options="riskRuleExpenseCategoryOptions"
:disabled="riskRuleCreateBusy"
/>
</label>
<label>
<span>是否上传附件</span>
<EnterpriseSelect
v-model="riskRuleCreateForm.requires_attachment"
:options="riskRuleAttachmentOptions"
:disabled="riskRuleCreateBusy"
/>
</label>
<label class="span-2">
<span>规则标题</span>
<input
v-model="riskRuleCreateForm.rule_title"
:disabled="riskRuleCreateBusy"
maxlength="80"
placeholder="例如:差旅目的地与票据城市一致性校验"
/>
</label>
<label class="span-2">
<span>自然语言规则</span>
<textarea
v-model="riskRuleCreateForm.natural_language"
:disabled="riskRuleCreateBusy"
placeholder="例如:住宿城市必须出现在本次差旅行程城市中,否则提示高风险并要求补充说明。"
></textarea>
</label>
</div>
</ConfirmDialog>
<RiskRuleTestDialog
:open="riskRuleTestOpen"
:rule="selectedSkill"
@close="emit('close-risk-rule-test')"
@report-saved="emit('report-saved', $event)"
/>
<ConfirmDialog
:open="riskRuleDeleteOpen"
badge="删除规则"
badge-tone="danger"
title="删除未发布风险规则"
description="该操作会删除规则草稿、版本记录和关联 JSON 文件。只有从未发布过的规则允许删除。"
cancel-text="取消"
confirm-text="确认删除"
busy-text="删除中..."
confirm-tone="danger"
confirm-icon="mdi mdi-delete-outline"
:busy="actionState === 'delete-risk-rule'"
@close="emit('close-delete-risk-rule')"
@confirm="emit('delete-selected-risk-rule')"
>
<div class="risk-rule-action-confirm">
<span>规则名称</span>
<strong>{{ selectedSkill?.name }}</strong>
</div>
</ConfirmDialog>
<ConfirmDialog
:open="riskRuleReturnOpen"
badge="回退规则"
badge-tone="warning"
title="回退风险规则"
description="回退后规则会回到草稿状态,编写人需要根据原因重新调整并测试。"
cancel-text="取消"
confirm-text="确认回退"
busy-text="回退中..."
confirm-tone="warning"
confirm-icon="mdi mdi-keyboard-return"
:busy="actionState === 'return-risk-rule'"
@close="emit('close-return-risk-rule')"
@confirm="emit('return-selected-risk-rule')"
>
<label class="risk-rule-action-note">
<span>回退原因</span>
<textarea
v-model="returnNoteModel"
rows="4"
:disabled="actionState === 'return-risk-rule'"
placeholder="请说明需要编写人调整的规则问题"
></textarea>
</label>
</ConfirmDialog>
<ConfirmDialog
:open="riskRulePublishOpen"
badge="发布上线"
badge-tone="info"
title="发布风险规则"
description="发布后该规则会进入真实业务风险扫描,只加载正式上线规则。"
cancel-text="取消"
confirm-text="确认发布"
busy-text="发布中..."
confirm-tone="primary"
confirm-icon="mdi mdi-rocket-launch-outline"
:busy="actionState === 'publish-risk-rule'"
@close="emit('close-publish-risk-rule')"
@confirm="emit('publish-selected-risk-rule')"
>
<div class="risk-rule-action-confirm">
<span>测试状态</span>
<strong>{{ riskRuleTestPassed ? '已确认通过' : '未确认通过' }}</strong>
</div>
</ConfirmDialog>
<ConfirmDialog
:open="Boolean(versionSwitchTarget)"
badge="切换版本"
badge-tone="info"
title="切换规则版本"
description="切换后编辑器只会替换当前展示内容,不会直接回滚后端当前版本。"
cancel-text="取消"
confirm-text="确认切换"
busy-text="切换中..."
confirm-tone="primary"
confirm-icon="mdi mdi-swap-horizontal"
@close="emit('cancel-version-switch')"
@confirm="emit('confirm-version-switch')"
>
<div class="version-modal-summary">
<div>
<span>当前展示版本</span>
<strong>{{ selectedSkill?.displayVersion }}</strong>
</div>
<i class="mdi mdi-arrow-right"></i>
<div>
<span>目标版本</span>
<strong>{{ versionSwitchTarget?.version }}</strong>
</div>
</div>
<div v-if="versionSwitchTarget" class="version-modal-note">
<strong>{{ versionSwitchTarget.note }}</strong>
<span>{{ versionSwitchTarget.time }}</span>
</div>
</ConfirmDialog>
<ConfirmDialog
:open="reviewSubmitOpen"
badge="提交审核"
badge-tone="info"
title="提交规则版本审核"
description="请先确认本次送审采用的版本号,并选择负责审核的高级管理员。若填写新的版本号,系统会将当前工作稿固化为该版本后再送审。"
cancel-text="取消"
confirm-text="确认提交"
busy-text="提交中..."
confirm-tone="primary"
confirm-icon="mdi mdi-send-outline"
:busy="actionState === 'review-pending'"
@close="emit('close-submit-review')"
@confirm="emit('submit-selected-rule-for-review')"
>
<div class="review-submit-form">
<label>
<span>送审版本号</span>
<input
v-model="reviewSubmitVersionModel"
type="text"
placeholder="例如v1.1.0"
:disabled="actionState === 'review-pending'"
/>
</label>
<label>
<span>审核人</span>
<EnterpriseSelect
v-model="reviewSubmitReviewerModel"
:options="reviewSubmitReviewerOptions"
:placeholder="reviewSubmitReviewerLoading ? '加载审核人中...' : '请选择高级管理员'"
:disabled="reviewSubmitReviewerLoading || actionState === 'review-pending'"
/>
</label>
<p
v-if="!reviewSubmitReviewerLoading && !hasReviewSubmitReviewers"
class="review-submit-hint"
>
当前没有可选的高级管理员请先在员工管理中配置具备管理员角色的员工
</p>
<div v-if="selectedSkillUsesJsonRisk" class="review-submit-test-state">
<span>测试确认</span>
<strong :class="{ passed: riskRuleTestPassed }">
{{ riskRuleTestPassed ? '当前版本已通过测试确认' : '当前版本尚未确认测试通过' }}
</strong>
<p>只有保存测试报告的风险规则才能提交给高级财务人员审核</p>
</div>
</div>
</ConfirmDialog>
</template>
<script setup>
import { computed } from 'vue'
import ConfirmDialog from '../shared/ConfirmDialog.vue'
import EnterpriseSelect from '../shared/EnterpriseSelect.vue'
import RiskRuleTestDialog from '../shared/RiskRuleTestDialog.vue'
defineOptions({
name: 'AuditRuleDialogs'
})
const props = defineProps({
selectedSkill: { type: Object, default: null },
versionSwitchTarget: { type: Object, default: null },
actionState: { type: String, default: '' },
riskRuleCreateOpen: { type: Boolean, default: false },
riskRuleCreateBusy: { type: Boolean, default: false },
riskRuleCreateForm: { type: Object, required: true },
riskRuleBusinessStageOptions: { type: Array, default: () => [] },
riskRuleExpenseCategoryOptions: { type: Array, default: () => [] },
riskRuleAttachmentOptions: { type: Array, default: () => [] },
riskRuleTestOpen: { type: Boolean, default: false },
riskRuleDeleteOpen: { type: Boolean, default: false },
riskRuleReturnOpen: { type: Boolean, default: false },
riskRulePublishOpen: { type: Boolean, default: false },
riskRuleReturnNote: { type: String, default: '' },
riskRuleTestPassed: { type: Boolean, default: false },
reviewSubmitOpen: { type: Boolean, default: false },
reviewSubmitVersion: { type: String, default: '' },
reviewSubmitReviewer: { type: String, default: '' },
reviewSubmitReviewerLoading: { type: Boolean, default: false },
reviewSubmitReviewerOptions: { type: Array, default: () => [] },
hasReviewSubmitReviewers: { type: Boolean, default: false },
selectedSkillUsesJsonRisk: { type: Boolean, default: false }
})
const emit = defineEmits([
'update:riskRuleReturnNote',
'update:reviewSubmitVersion',
'update:reviewSubmitReviewer',
'close-risk-rule-create',
'submit-risk-rule-create',
'close-risk-rule-test',
'report-saved',
'close-delete-risk-rule',
'delete-selected-risk-rule',
'close-return-risk-rule',
'return-selected-risk-rule',
'close-publish-risk-rule',
'publish-selected-risk-rule',
'cancel-version-switch',
'confirm-version-switch',
'close-submit-review',
'submit-selected-rule-for-review'
])
const returnNoteModel = computed({
get: () => props.riskRuleReturnNote,
set: (value) => emit('update:riskRuleReturnNote', value)
})
const reviewSubmitVersionModel = computed({
get: () => props.reviewSubmitVersion,
set: (value) => emit('update:reviewSubmitVersion', value)
})
const reviewSubmitReviewerModel = computed({
get: () => props.reviewSubmitReviewer,
set: (value) => emit('update:reviewSubmitReviewer', value)
})
</script>
<style scoped src="../../assets/styles/views/audit-view.css"></style>
<style scoped src="../../assets/styles/views/audit-view-part2.css"></style>

View File

@@ -0,0 +1,112 @@
<template>
<Transition name="drawer-fade">
<div v-if="open" class="rule-drawer-backdrop" @click.self="emit('close')">
<aside class="rule-drawer compare-drawer change-detail-drawer">
<header class="rule-drawer-head">
<div>
<span>最近修改</span>
<h3>修改详情</h3>
</div>
<button type="button" @click="emit('close')">
<i class="mdi mdi-close"></i>
</button>
</header>
<div v-if="record" class="compare-content change-detail-content">
<section class="change-detail-meta">
<article>
<span>修改人</span>
<strong>{{ record.actor }}</strong>
</article>
<article>
<span>修改时间</span>
<strong>{{ record.time }}</strong>
</article>
<article>
<span>修改工作表</span>
<strong>{{ record.changed_sheet_count }}</strong>
</article>
<article>
<span>变更单元格</span>
<strong>{{ record.changed_cell_count }}</strong>
</article>
</section>
<section class="compare-panel">
<header>
<strong>本次修改摘要</strong>
</header>
<p>{{ record.summary }}</p>
</section>
<section class="compare-panel">
<header>
<strong>工作表变化</strong>
</header>
<div v-if="sheetRows.length" class="compare-sheet-list">
<span
v-for="item in sheetRows"
:key="`${item.sheet_name}-${item.change_type}`"
:class="item.meta.tone"
>
{{ item.sheet_name }} · {{ item.meta.label }}
</span>
</div>
<p v-else>本次没有工作表级变化</p>
</section>
<section class="compare-panel compare-cell-panel">
<header>
<strong>单元格差异</strong>
<small>最多展示前 500 </small>
</header>
<div v-if="cellRows.length" class="compare-table-wrap">
<table>
<thead>
<tr>
<th>工作表</th>
<th>位置</th>
<th>类型</th>
<th>旧值</th>
<th>新值</th>
</tr>
</thead>
<tbody>
<tr
v-for="item in cellRows"
:key="`${item.sheet_name}-${item.cell}`"
>
<td>{{ item.sheet_name }}</td>
<td>{{ item.cell }}</td>
<td><b :class="item.meta.tone">{{ item.meta.label }}</b></td>
<td>{{ item.before_value ?? '-' }}</td>
<td>{{ item.after_value ?? '-' }}</td>
</tr>
</tbody>
</table>
</div>
<p v-else>本次没有发现单元格级差异</p>
</section>
</div>
</aside>
</div>
</Transition>
</template>
<script setup>
defineOptions({
name: 'AuditSpreadsheetChangeDrawer'
})
defineProps({
open: { type: Boolean, default: false },
record: { type: Object, default: null },
sheetRows: { type: Array, default: () => [] },
cellRows: { type: Array, default: () => [] }
})
const emit = defineEmits(['close'])
</script>
<style scoped src="../../assets/styles/views/audit-view.css"></style>
<style scoped src="../../assets/styles/views/audit-view-part2.css"></style>

View File

@@ -0,0 +1,159 @@
<template>
<section class="spreadsheet-editor-shell panel">
<header class="spreadsheet-editor-head asset-detail-topbar list-toolbar">
<div class="spreadsheet-editor-title asset-detail-topbar-main filter-set">
<div class="skill-badge" :class="selectedSkill.badgeTone">{{ selectedSkill.typeLabel }}</div>
<div>
<h2>{{ selectedSkill.name }}</h2>
<p>{{ selectedSkill.summary || '当前资产尚未补充说明。' }}</p>
</div>
</div>
<div class="spreadsheet-editor-actions asset-detail-topbar-meta toolbar-actions">
<span class="spreadsheet-mode-pill">
{{ selectedSpreadsheetModeLabel }}
</span>
</div>
</header>
<input
ref="fileInput"
class="spreadsheet-upload-input"
type="file"
accept=".xlsx,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
@change="emit('file-change', $event)"
/>
<div class="spreadsheet-editor-body">
<section class="spreadsheet-main-stage">
<div class="spreadsheet-editor-meta">
<span><strong>文件</strong>{{ selectedSpreadsheetFileName }}</span>
<span><strong>负责人</strong>{{ selectedSkill.owner }}</span>
<span><strong>最近更新</strong>{{ selectedSkill.updatedAt }}</span>
</div>
<div class="spreadsheet-workbench">
<div
:key="spreadsheetOnlyOfficeHostId"
:id="spreadsheetOnlyOfficeHostId"
class="rule-spreadsheet-host"
:class="{ hidden: !spreadsheetOnlyOfficeReady && !spreadsheetOnlyOfficeError }"
></div>
<TableLoadingState
v-if="spreadsheetOnlyOfficeLoading"
class="rule-spreadsheet-state"
variant="overlay"
tone="sky"
message="正在加载 Excel 规则表"
icon="mdi mdi-table-large"
:show-skeleton="false"
/>
<div v-else-if="spreadsheetOnlyOfficeError" class="rule-spreadsheet-state error">
<i class="mdi mdi-alert-circle-outline"></i>
<span>{{ spreadsheetOnlyOfficeError }}</span>
</div>
</div>
<footer class="spreadsheet-editor-foot">
<span>
{{
canEditSpreadsheetInline
? '可直接在线编辑;保存后,右侧会自动记录本次修改内容。'
: '当前为只读预览模式。'
}}
</span>
<span>右侧仅展示最近 30 次修改操作</span>
</footer>
</section>
<aside class="spreadsheet-change-center">
<header class="change-center-head">
<div>
<h3>最近修改</h3>
<p>展示最近 30 次保存后的具体改动</p>
</div>
</header>
<section class="change-center-section change-history-section">
<div v-if="selectedSpreadsheetChangeRecords.length" class="change-center-list">
<button
v-for="item in selectedSpreadsheetChangeRecords"
:key="`spreadsheet-change-${item.id || item.changed_at}-${item.actor}`"
type="button"
class="change-center-item change-record-item"
@click="emit('open-spreadsheet-change-detail', item)"
>
<div class="change-record-head">
<div>
<strong>{{ item.actor }}</strong>
<span>{{ item.time }}</span>
</div>
<b>{{ item.changeCountLabel }}</b>
</div>
<p>{{ item.summary }}</p>
<small v-if="item.sheetPreview.length">
涉及工作表{{ item.sheetPreview.join('') }}
<template v-if="item.remainingSheetCount"> {{ item.changedSheetNames.length }} </template>
</small>
<div v-if="item.previewChanges.length" class="change-record-preview">
<span
v-for="change in item.previewChanges"
:key="`${change.sheet_name}-${change.cell}`"
>
{{ change.sheet_name }}!{{ change.cell }}
{{ change.before_value ?? '-' }} {{ change.after_value ?? '-' }}
</span>
</div>
<small v-if="item.remainingChangeCount" class="change-record-more">
另有 {{ item.remainingChangeCount }} 处改动
</small>
</button>
</div>
<p v-else class="change-flow-empty">暂无修改记录</p>
</section>
</aside>
</div>
</section>
</template>
<script setup>
import { ref } from 'vue'
import TableLoadingState from '../shared/TableLoadingState.vue'
defineOptions({
name: 'AuditSpreadsheetRuleDetail'
})
defineProps({
selectedSkill: { type: Object, required: true },
selectedSpreadsheetModeLabel: { type: String, default: '' },
selectedSpreadsheetFileName: { type: String, default: '' },
selectedSpreadsheetChangeRecords: { type: Array, default: () => [] },
spreadsheetOnlyOfficeHostId: { type: String, required: true },
spreadsheetOnlyOfficeReady: { type: Boolean, default: false },
spreadsheetOnlyOfficeError: { type: String, default: '' },
spreadsheetOnlyOfficeLoading: { type: Boolean, default: false },
canEditSpreadsheetInline: { type: Boolean, default: false }
})
const emit = defineEmits(['file-change', 'open-spreadsheet-change-detail'])
const fileInput = ref(null)
function click() {
fileInput.value?.click()
}
function reset() {
if (fileInput.value) {
fileInput.value.value = ''
}
}
defineExpose({
click,
reset
})
</script>
<style scoped src="../../assets/styles/views/audit-view.css"></style>
<style scoped src="../../assets/styles/views/audit-view-part2.css"></style>

View File

@@ -0,0 +1,71 @@
<template>
<Transition name="drawer-fade">
<div v-if="open" class="rule-drawer-backdrop" @click.self="emit('close')">
<aside class="rule-drawer timeline-drawer">
<header class="rule-drawer-head">
<div>
<span>修改记录</span>
<h3>文档操作记录</h3>
</div>
<button type="button" @click="emit('close')">
<i class="mdi mdi-close"></i>
</button>
</header>
<TableLoadingState
v-if="loading"
class="rule-drawer-state"
variant="drawer"
message="正在加载操作记录"
icon="mdi mdi-history"
:show-skeleton="false"
/>
<div v-else-if="error" class="rule-drawer-state error">
<i class="mdi mdi-alert-circle-outline"></i>
<span>{{ error }}</span>
</div>
<div v-else-if="items.length" class="rule-timeline-list">
<article
v-for="item in items"
:key="`${item.event_type}-${item.version}-${item.event_time}`"
class="rule-timeline-item"
>
<i :class="[item.meta.icon, item.meta.tone]"></i>
<div>
<header>
<strong>{{ item.meta.label }}</strong>
<span>{{ item.timeLabel }}</span>
</header>
<p>{{ item.description || item.note || '暂无补充说明' }}</p>
<small>操作人{{ item.actor }}</small>
</div>
</article>
</div>
<div v-else class="rule-drawer-state">
<i class="mdi mdi-history"></i>
<span>暂无操作记录</span>
</div>
</aside>
</div>
</Transition>
</template>
<script setup>
import TableLoadingState from '../shared/TableLoadingState.vue'
defineOptions({
name: 'AuditVersionTimelineDrawer'
})
defineProps({
open: { type: Boolean, default: false },
loading: { type: Boolean, default: false },
error: { type: String, default: '' },
items: { type: Array, default: () => [] }
})
const emit = defineEmits(['close'])
</script>
<style scoped src="../../assets/styles/views/audit-view.css"></style>
<style scoped src="../../assets/styles/views/audit-view-part2.css"></style>