style: 全局 UI 主题皮肤重构与样式模块化
引入 Element Plus 主题定制和主题皮肤 composable,将全局 样式拆分为组件级独立 CSS 文件(侧边栏、顶栏、工作台等), 统一色彩变量和间距规范,重构所有视图和组件样式以适配新 主题系统,优化图表和知识图谱组件视觉表现,提取审计和差 旅报销相关子组件。
This commit is contained in:
334
web/src/components/audit/AuditAssetList.vue
Normal file
334
web/src/components/audit/AuditAssetList.vue
Normal 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>
|
||||
173
web/src/components/audit/AuditJsonRiskRuleDetail.vue
Normal file
173
web/src/components/audit/AuditJsonRiskRuleDetail.vue
Normal 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>
|
||||
60
web/src/components/audit/AuditPickerFilter.vue
Normal file
60
web/src/components/audit/AuditPickerFilter.vue
Normal 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>
|
||||
293
web/src/components/audit/AuditRuleDialogs.vue
Normal file
293
web/src/components/audit/AuditRuleDialogs.vue
Normal 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>
|
||||
112
web/src/components/audit/AuditSpreadsheetChangeDrawer.vue
Normal file
112
web/src/components/audit/AuditSpreadsheetChangeDrawer.vue
Normal 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>
|
||||
159
web/src/components/audit/AuditSpreadsheetRuleDetail.vue
Normal file
159
web/src/components/audit/AuditSpreadsheetRuleDetail.vue
Normal 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>
|
||||
71
web/src/components/audit/AuditVersionTimelineDrawer.vue
Normal file
71
web/src/components/audit/AuditVersionTimelineDrawer.vue
Normal 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>
|
||||
Reference in New Issue
Block a user