feat(ui): finalize shared shells and loading states

This commit is contained in:
caoxiaozhu
2026-05-29 13:17:39 +08:00
parent 64cc76c970
commit e080105f9f
52 changed files with 1559 additions and 861 deletions

View File

@@ -10,16 +10,13 @@
<div class="mobile-overlay" aria-hidden="true" @click="mobileSidebarOpen = false"></div>
<Transition name="login-entry-veil">
<div v-if="loginEntryAnimating" class="login-entry-veil" aria-live="polite" aria-label="登录成功,正在进入工作台">
<div class="login-entry-card">
<span class="login-entry-mark" aria-hidden="true">
<i class="mdi mdi-shield-check-outline"></i>
</span>
<div class="login-entry-copy">
<strong>登录成功</strong>
<span>正在进入工作台</span>
</div>
<span class="login-entry-progress" aria-hidden="true"></span>
</div>
<FloatingLightBandWindow
icon="mdi mdi-shield-check-outline"
message="正在进入工作台"
motion="entry"
title="登录成功"
variant="entry"
/>
</div>
</Transition>
<div class="app-sidebar">
@@ -49,7 +46,6 @@
'audit-detail-main': activeView === 'audit' && auditDetailOpen,
'digital-employees-detail-main': activeView === 'digitalEmployees' && digitalEmployeeDetailOpen,
'digital-employees-main': activeView === 'digitalEmployees',
'logs-main': activeView === 'logs',
'employees-main': activeView === 'employees',
'settings-main': activeView === 'settings'
}"
@@ -63,13 +59,11 @@
:active-range="activeRange"
:employee-summary="employeeSummary"
:knowledge-summary="knowledgeSummary"
:logs-summary="logsSummary"
:request-summary="requestSummary"
:document-summary="documentSummary"
:digital-employee-summary="digitalEmployeeSummary"
:company-name="ENTERPRISE_DISPLAY_NAME"
:detail-mode="resolvedDetailMode"
:log-detail-mode="logDetailMode"
:detail-alerts="resolvedDetailAlerts"
:detail-kpis="resolvedDetailKpis"
:custom-range="customRange"
@@ -81,7 +75,7 @@
/>
<FilterBar
v-if="activeView !== 'overview' && activeView !== 'workbench' && activeView !== 'documents' && activeView !== 'budget' && activeView !== 'policies' && activeView !== 'audit' && activeView !== 'digitalEmployees' && activeView !== 'logs' && activeView !== 'employees' && activeView !== 'settings'"
v-if="activeView !== 'overview' && activeView !== 'workbench' && activeView !== 'documents' && activeView !== 'budget' && activeView !== 'policies' && activeView !== 'audit' && activeView !== 'digitalEmployees' && activeView !== 'employees' && activeView !== 'settings'"
:compact="activeView === 'overview'"
:filters="filters"
:ranges="ranges"
@@ -98,7 +92,6 @@
'policies-workarea': activeView === 'policies',
'audit-workarea': activeView === 'audit',
'digital-employees-workarea': activeView === 'digitalEmployees',
'logs-workarea': activeView === 'logs',
'employees-workarea': activeView === 'employees',
'settings-workarea': activeView === 'settings'
}"
@@ -157,8 +150,6 @@
@detail-open-change="digitalEmployeeDetailOpen = $event"
@detail-topbar-change="detailTopBarPayload = $event"
/>
<LogDetailView v-else-if="activeView === 'logs' && logDetailMode" />
<LogsView v-else-if="activeView === 'logs'" @summary-change="logsSummary = $event" />
<EmployeeManagementView v-else-if="activeView === 'employees'" @overview-change="employeeSummary = $event" />
<SettingsView v-else />
</section>
@@ -181,11 +172,13 @@
</template>
<script setup>
import { computed, defineAsyncComponent, onBeforeUnmount, onMounted, ref } from 'vue'
import { computed, defineAsyncComponent, h, onBeforeUnmount, onMounted, ref } from 'vue'
import SidebarRail from '../components/layout/SidebarRail.vue'
import TopBar from '../components/layout/TopBar.vue'
import FilterBar from '../components/layout/FilterBar.vue'
import FloatingLightBandWindow from '../components/shared/FloatingLightBandWindow.vue'
import TableLoadingState from '../components/shared/TableLoadingState.vue'
import { useAppShell } from '../composables/useAppShell.js'
import { useSystemState } from '../composables/useSystemState.js'
@@ -197,18 +190,30 @@ const PersonalWorkbenchView = defineAsyncComponent(() => import('./PersonalWorkb
const TravelReimbursementCreateView = defineAsyncComponent(() => import('./TravelReimbursementCreateView.vue'))
const TravelRequestDetailView = defineAsyncComponent(() => import('./TravelRequestDetailView.vue'))
const DocumentsCenterView = defineAsyncComponent(() => import('./DocumentsCenterView.vue'))
const BudgetCenterView = defineAsyncComponent(() => import('./BudgetCenterView.vue'))
const BudgetCenterRouteLoading = {
name: 'BudgetCenterRouteLoading',
render: () =>
h(TableLoadingState, {
title: '预算数据同步中',
message: '正在加载预算中心模块与预算数据',
icon: 'mdi mdi-chart-donut',
floating: true,
blocking: true
})
}
const BudgetCenterView = defineAsyncComponent({
loader: () => import('./BudgetCenterView.vue'),
loadingComponent: BudgetCenterRouteLoading,
delay: 0
})
const PoliciesView = defineAsyncComponent(() => import('./PoliciesView.vue'))
const AuditView = defineAsyncComponent(() => import('./AuditView.vue'))
const DigitalEmployeesView = defineAsyncComponent(() => import('./DigitalEmployeesView.vue'))
const LogsView = defineAsyncComponent(() => import('./LogsView.vue'))
const LogDetailView = defineAsyncComponent(() => import('./LogDetailView.vue'))
const EmployeeManagementView = defineAsyncComponent(() => import('./EmployeeManagementView.vue'))
const SettingsView = defineAsyncComponent(() => import('./SettingsView.vue'))
const employeeSummary = ref(null)
const knowledgeSummary = ref(null)
const logsSummary = ref(null)
const documentSummary = ref(null)
const digitalEmployeeSummary = ref(null)
const detailTopBarPayload = ref(null)
@@ -254,7 +259,6 @@ const {
customRange,
detailAlerts,
detailMode,
logDetailMode,
filteredRequests,
filters,
handleApprove,

View File

@@ -1,5 +1,13 @@
<template>
<section class="budget-center-page">
<TableLoadingState
v-if="budgetLoading"
title="预算数据同步中"
message="正在加载预算额度、使用情况与预警明细"
icon="mdi mdi-chart-donut"
floating
blocking
/>
<section class="budget-summary-grid" aria-label="预算概览">
<article

View File

@@ -67,7 +67,7 @@
:class="{ active: activeSection === 'skills' }"
@click="activeSection = 'skills'"
>
数字员工
员工能力
</button>
<button
type="button"

View File

@@ -140,6 +140,7 @@
title="单据数据同步中"
message="正在汇总当前报销、审批待办与归档单据"
icon="mdi mdi-file-document-multiple-outline"
floating
/>
</div>
@@ -246,6 +247,7 @@ import { computed, onMounted, ref, watch } from 'vue'
import EnterpriseSelect from '../components/shared/EnterpriseSelect.vue'
import TableEmptyState from '../components/shared/TableEmptyState.vue'
import TableLoadingState from '../components/shared/TableLoadingState.vue'
import { useMinimumVisibleState } from '../composables/useMinimumVisibleState.js'
import { mapExpenseClaimToRequest } from '../composables/useRequests.js'
import { fetchApprovalExpenseClaims, fetchArchivedExpenseClaims } from '../services/reimbursements.js'
import { countNewDocuments, isNewDocument, markDocumentViewed, readDocumentScope, readViewedDocumentKeys, writeDocumentScope } from '../utils/documentCenterNewState.js'
@@ -262,6 +264,7 @@ const DOCUMENT_SCOPE_REIMBURSEMENT = '报销单'
const DOCUMENT_SCOPE_REVIEW = '审核单'
const DOCUMENT_SCOPE_ARCHIVE = '归档'
const scopeTabs = [DOCUMENT_SCOPE_ALL, DOCUMENT_SCOPE_APPLICATION, DOCUMENT_SCOPE_REIMBURSEMENT, DOCUMENT_SCOPE_REVIEW, DOCUMENT_SCOPE_ARCHIVE]
const DOCUMENT_LOADING_MIN_VISIBLE_MS = 720
const statusTabs = ['全部', '草稿', '待提交', '审批中', '待补充', '待付款', '已完成']
const FILTER_CONFIG_BY_SCOPE = {
[DOCUMENT_SCOPE_ALL]: {
@@ -465,7 +468,11 @@ const visibleRows = computed(() => {
return filteredRows.value.slice(start, start + pageSize.value)
})
const showLoading = computed(() => (props.loading || supportingLoading.value) && !visibleRows.value.length)
const documentLoadingSource = computed(() => (props.loading || supportingLoading.value) && !visibleRows.value.length)
const visibleDocumentLoading = useMinimumVisibleState(documentLoadingSource, {
minVisibleMs: DOCUMENT_LOADING_MIN_VISIBLE_MS
})
const showLoading = computed(() => visibleDocumentLoading.value)
const showError = computed(() => Boolean(props.error) && !visibleRows.value.length)
const errorMessage = computed(() => props.error || supportingError.value || '单据中心加载失败。')
const showEmpty = computed(() => !showLoading.value && !showError.value && visibleRows.value.length === 0)

View File

@@ -567,6 +567,7 @@
title="员工数据同步中"
message="正在加载员工档案与角色权限"
icon="mdi mdi-account-group-outline"
floating
/>
</div>

View File

@@ -182,10 +182,10 @@
</div>
<footer class="detail-actions">
<button class="back-action" type="button" @click="backToLogs">
<i class="mdi mdi-arrow-left"></i>
<span>返回日志列表</span>
</button>
<button class="back-action" type="button" @click="backToLogs">
<i class="mdi mdi-arrow-left"></i>
<span>返回系统日志</span>
</button>
</footer>
</section>
</template>
@@ -435,9 +435,9 @@ async function loadDetail(options = {}) {
}
}
function backToLogs() {
router.push({ name: 'app-logs' })
}
function backToLogs() {
router.push({ name: 'app-settings', query: { section: 'systemLogs' } })
}
watch(
() => [route.params.logKind, route.params.logId],

View File

@@ -100,14 +100,44 @@
<i class="mdi mdi-filter-remove-outline"></i>
<span>清空筛选</span>
</button>
<div class="document-filter refresh-interval-filter" :class="{ open: openFilterKey === 'refreshInterval' }">
<button
class="filter-btn refresh-interval-trigger"
type="button"
:aria-expanded="openFilterKey === 'refreshInterval'"
@click="toggleFilter('refreshInterval')"
>
<i class="mdi mdi-clock-outline"></i>
<span>刷新时间 {{ refreshIntervalLabel }}</span>
<i class="mdi mdi-chevron-down"></i>
</button>
<div
v-if="openFilterKey === 'refreshInterval'"
class="document-filter-menu refresh-interval-menu"
role="listbox"
aria-label="刷新时间"
>
<button
v-for="option in refreshIntervalOptions"
:key="option.value"
type="button"
role="option"
:aria-selected="refreshInterval === option.value"
:class="{ active: refreshInterval === option.value }"
@click="changeRefreshInterval(option.value)"
>
每 {{ option.label }}
</button>
</div>
</div>
<button
type="button"
class="create-request-btn"
class="create-request-btn icon-refresh-action"
:disabled="systemLogLoading"
aria-label="立即刷新系统日志"
@click="loadSystemLogs(true)"
>
<i class="mdi mdi-refresh"></i>
<span>{{ systemLogLoading ? '刷新中...' : '刷新日志' }}</span>
<i :class="systemLogLoading ? 'mdi mdi-loading mdi-spin' : 'mdi mdi-refresh'"></i>
</button>
</template>

View File

@@ -65,7 +65,15 @@
</div>
<div class="doc-table-wrap">
<table class="knowledge-document-table">
<TableLoadingState
v-if="loading && !visibleDocuments.length"
title="知识库文件同步中"
message="正在加载当前文件夹的知识库文件"
icon="mdi mdi-folder-table-outline"
floating
/>
<table class="knowledge-document-table">
<thead>
<tr>
<th>文件名称</th>
@@ -129,16 +137,7 @@
</div>
</td>
</tr>
<tr v-if="loading && !visibleDocuments.length">
<td colspan="8" class="empty-row table-loading-row">
<TableLoadingState
title="知识库文件同步中"
message="正在加载当前文件夹的知识库文件"
icon="mdi mdi-folder-table-outline"
/>
</td>
</tr>
<tr v-else-if="!visibleDocuments.length">
<tr v-if="!loading && !visibleDocuments.length">
<td colspan="8" class="empty-row">
当前文件夹暂无文件
</td>

View File

@@ -35,15 +35,15 @@
<p>{{ activeSectionConfig.longDesc }}</p>
</div>
<div class="settings-toolbar-actions">
<button class="save-button" type="button" @click="saveActiveSection">
<i class="mdi mdi-content-save-outline"></i>
<span>{{ activeSectionConfig.actionLabel }}</span>
</button>
<div class="settings-toolbar-actions">
<button v-if="activeSectionConfig.actionLabel" class="save-button" type="button" @click="saveActiveSection">
<i class="mdi mdi-content-save-outline"></i>
<span>{{ activeSectionConfig.actionLabel }}</span>
</button>
</div>
</header>
<div class="settings-content">
<div class="settings-content" :class="{ 'settings-content-fill': activeSection === 'systemLogs' }">
<template v-if="activeSection === 'profile'">
<section class="settings-card">
<div class="card-head">
@@ -379,8 +379,8 @@
</section>
</template>
<template v-else-if="activeSection === 'logs'">
<section class="settings-card">
<template v-else-if="activeSection === 'logs'">
<section class="settings-card log-policy-card">
<div class="card-head">
<div class="card-title-with-icon">
<div class="model-icon-box slate">
@@ -388,7 +388,7 @@
</div>
<div>
<h4>日志级别与留存</h4>
<p>定义系统记录粒度归档周期和告警接收人方便后续审计与排障</p>
<p>定义系统记录粒度归档周期写入路径和告警接收人方便后续排障追踪</p>
</div>
</div>
</div>
@@ -429,53 +429,18 @@
<label class="field field-full">
<span>告警邮箱</span>
<input v-model="pageState.logForm.alertEmail" type="email" placeholder="用于接收日志异常提醒" />
</label>
</div>
</section>
<section class="settings-card">
<div class="card-head">
<div class="card-title-with-icon">
<div class="model-icon-box slate">
<i class="mdi mdi-eye-check-outline"></i>
</div>
<div>
<h4>审计策略</h4>
<p>决定是否记录关键操作登录行为以及是否对敏感字段进行脱敏处理</p>
</div>
</div>
</div>
<div class="switch-group">
<button class="switch-row" type="button" @click="toggleBoolean('logForm', 'operationAudit')">
<span class="switch-copy">
<strong>记录关键操作日志</strong>
<small>保存配置修改审批动作和账户管理等重要事件</small>
</span>
<span class="switch-btn" :class="{ active: pageState.logForm.operationAudit }"><i></i></span>
</button>
<button class="switch-row" type="button" @click="toggleBoolean('logForm', 'loginAudit')">
<span class="switch-copy">
<strong>记录登录审计</strong>
<small>追踪登录来源登录结果和异常登录行为</small>
</span>
<span class="switch-btn" :class="{ active: pageState.logForm.loginAudit }"><i></i></span>
</button>
<button class="switch-row" type="button" @click="toggleBoolean('logForm', 'maskSensitive')">
<span class="switch-copy">
<strong>敏感字段脱敏</strong>
<small>日志写入时自动隐藏密码密钥与认证令牌</small>
</span>
<span class="switch-btn" :class="{ active: pageState.logForm.maskSensitive }"><i></i></span>
</button>
</div>
</section>
</template>
<template v-else-if="activeSection === 'mail'">
<MailSettingsPanel :mail-form="pageState.mailForm" />
</label>
</div>
</section>
</template>
<template v-else-if="activeSection === 'systemLogs'">
<LogDetailView v-if="systemLogDetailMode" class="settings-log-detail-view" />
<LogsView v-else class="settings-logs-view" />
</template>
<template v-else-if="activeSection === 'mail'">
<MailSettingsPanel :mail-form="pageState.mailForm" />
</template>
</div>
</div>

View File

@@ -30,7 +30,7 @@
<strong>{{ section.title }}</strong>
<small>{{ section.desc }}</small>
</span>
<i v-if="section.complete" class="pi pi-check setup-nav-check"></i>
<i v-if="section.complete" class="mdi mdi-check setup-nav-check"></i>
</button>
</nav>
@@ -42,11 +42,11 @@
<div v-if="canSubmit" class="setup-complete">
<p>所有必要步骤已通过检测可以写入配置并进入登录界面</p>
<button class="primary-btn setup-complete-btn" type="button" :disabled="submitting" @click="submitForm">
<i :class="['pi', submitting ? 'pi-spin pi-spinner' : 'pi-check']"></i>
<i :class="['mdi', submitting ? 'mdi-loading mdi-spin' : 'mdi-check']"></i>
<span>{{ submitting ? '写入配置中...' : '完成初始化并进入登录' }}</span>
</button>
<p v-if="progressMessage" class="setup-complete-progress">
<i class="pi pi-spin pi-spinner"></i>
<i class="mdi mdi-loading mdi-spin"></i>
<span>{{ progressMessage }}</span>
</p>
</div>
@@ -240,7 +240,7 @@
<span>{{ progressMessage || '正在准备后端服务...' }}</span>
</div>
<div class="setup-startup-spinner" aria-hidden="true">
<i v-if="!startupCountdownSeconds" class="pi pi-spin pi-spinner"></i>
<i v-if="!startupCountdownSeconds" class="mdi mdi-loading mdi-spin"></i>
<strong v-else>{{ startupCountdownSeconds }}</strong>
</div>
</header>
@@ -358,19 +358,19 @@ const {
function startupStepIcon(status) {
if (status === 'success') {
return 'pi pi-check-circle'
return 'mdi mdi-check-circle'
}
if (status === 'error') {
return 'pi pi-times-circle'
return 'mdi mdi-close-circle'
}
if (status === 'running') {
return 'pi pi-spin pi-spinner'
return 'mdi mdi-loading mdi-spin'
}
return 'pi pi-circle'
}
</script>
<style scoped src="../assets/styles/views/setup-view.css"></style>
return 'mdi mdi-circle-outline'
}
</script>
<style scoped src="../assets/styles/views/setup-view.css"></style>

View File

@@ -177,12 +177,6 @@ export default {
() =>
normalizeText(selectedSkill.value?.ruleDocument?.file_name) || '未上传规则表'
)
const selectedSpreadsheetModeLabel = computed(() => {
if (selectedSkill.value?.isPreviewMock) {
return canEditSpreadsheetInline.value ? '可编辑' : '只读'
}
return canEditSpreadsheetInline.value ? '在线可编辑' : '只读'
})
const {
versionSwitchTarget,
versionTimelineOpen,
@@ -438,11 +432,7 @@ export default {
const auditDetailTopBar = computed(() =>
buildAuditDetailTopBar({
skill: selectedSkill.value,
usesJsonRiskRule: selectedSkillUsesJsonRisk.value,
usesSpreadsheetRule: selectedSkillUsesSpreadsheet.value,
spreadsheetModeLabel: selectedSpreadsheetModeLabel.value,
spreadsheetFileName: selectedSpreadsheetFileName.value,
canEditSpreadsheetInline: canEditSpreadsheetInline.value
usesJsonRiskRule: selectedSkillUsesJsonRisk.value
})
)
@@ -711,7 +701,6 @@ export default {
selectedSkillUsesSpreadsheet,
selectedSkillUsesJsonRisk,
selectedSpreadsheetFileName,
selectedSpreadsheetModeLabel,
selectedVersionTimelineItems,
selectedSpreadsheetChangeRecords,
detailBusy,

View File

@@ -3,6 +3,7 @@ import { ElButton, ElInput, ElPagination, ElTable, ElTableColumn } from 'element
import BudgetTrendChart from '../../components/charts/BudgetTrendChart.vue'
import EnterpriseSelect from '../../components/shared/EnterpriseSelect.vue'
import TableLoadingState from '../../components/shared/TableLoadingState.vue'
import { fetchBudgetSummary } from '../../services/budgets.js'
import { fetchEmployeeMeta } from '../../services/employees.js'
import {
@@ -217,6 +218,7 @@ export default {
components: {
BudgetTrendChart,
EnterpriseSelect,
TableLoadingState,
ElButton,
ElInput,
ElPagination,
@@ -238,7 +240,7 @@ export default {
const budgetTableKeyword = ref('')
const budgetRows = ref([])
const budgetSummary = ref(null)
const budgetLoading = ref(false)
const budgetLoading = ref(true)
const budgetError = ref('')
const canEditBudget = computed(() => canEditBudgetCenter(props.currentUser))
const canSwitchDepartments = computed(() => canSwitchBudgetDepartments(props.currentUser))
@@ -424,6 +426,7 @@ export default {
}
async function loadDepartments() {
budgetLoading.value = true
try {
const payload = await fetchEmployeeMeta()
const options = Array.isArray(payload?.organizationOptions) ? payload.organizationOptions : []

View File

@@ -5,8 +5,12 @@ import EnterpriseListPage from '../../components/shared/EnterpriseListPage.vue'
import { useSystemState } from '../../composables/useSystemState.js'
import { useToast } from '../../composables/useToast.js'
import { fetchSystemLogEntries } from '../../services/systemLogs.js'
import { AGENT_RUN_POLL_INTERVAL_MS } from '../../utils/agentRunMonitor.js'
import { isManagerUser } from '../../utils/accessControl.js'
import {
DEFAULT_REFRESH_INTERVAL_MS,
REFRESH_INTERVAL_OPTIONS,
formatRefreshInterval
} from '../../utils/refreshIntervalOptions.js'
function formatDateTime(value) {
if (!value) {
@@ -79,6 +83,8 @@ export default {
const pageSize = ref(10)
const pageSizes = [10, 20, 50]
const pageSizeOptions = pageSizes.map((size) => ({ label: `${size} 条/页`, value: size }))
const refreshInterval = ref(DEFAULT_REFRESH_INTERVAL_MS)
const refreshIntervalOptions = REFRESH_INTERVAL_OPTIONS
let pollTimer = 0
const isAdmin = computed(() => isManagerUser(currentUser.value))
@@ -102,6 +108,7 @@ export default {
const systemEventTypeFilterLabel = computed(() =>
systemEventTypeFilterOptions.value.find((item) => item.value === systemEventTypeFilter.value)?.label || '全部类型'
)
const refreshIntervalLabel = computed(() => formatRefreshInterval(refreshInterval.value))
const hasActiveFilters = computed(() =>
Boolean(systemSearchKeyword.value.trim() || systemLevelFilter.value || systemEventTypeFilter.value)
)
@@ -175,6 +182,12 @@ export default {
openFilterKey.value = ''
}
function changeRefreshInterval(value) {
refreshInterval.value = Number(value) || DEFAULT_REFRESH_INTERVAL_MS
openFilterKey.value = ''
startPolling()
}
function resetFilters() {
systemSearchKeyword.value = ''
systemLevelFilter.value = ''
@@ -211,7 +224,7 @@ export default {
stopPolling()
pollTimer = window.setInterval(() => {
loadSystemLogs(false)
}, AGENT_RUN_POLL_INTERVAL_MS)
}, refreshInterval.value)
}
function stopPolling() {
@@ -253,6 +266,7 @@ export default {
return {
changePageSize,
changeRefreshInterval,
currentPage,
filteredSystemLogEntries,
formatDateTime,
@@ -263,6 +277,9 @@ export default {
openFilterKey,
pageSize,
pageSizeOptions,
refreshInterval,
refreshIntervalLabel,
refreshIntervalOptions,
resetFilters,
resolveSystemLevelTone,
resolveSystemOutcomeTone,

View File

@@ -1,5 +1,7 @@
import HermesEmployeeSettingsPanel from '../HermesEmployeeSettingsPanel.vue'
import LlmSettingsPanel from '../LlmSettingsPanel.vue'
import LogDetailView from '../LogDetailView.vue'
import LogsView from '../LogsView.vue'
import MailSettingsPanel from '../MailSettingsPanel.vue'
import EnterpriseSelect from '../../components/shared/EnterpriseSelect.vue'
import { useSettings } from '../../composables/useSettings.js'
@@ -10,6 +12,8 @@ export default {
HermesEmployeeSettingsPanel,
EnterpriseSelect,
LlmSettingsPanel,
LogDetailView,
LogsView,
MailSettingsPanel
},
setup() {

View File

@@ -10,11 +10,7 @@ function resolveRiskScoreCardColor(level) {
export function buildAuditDetailTopBar({
skill,
usesJsonRiskRule = false,
usesSpreadsheetRule = false,
spreadsheetModeLabel = '',
spreadsheetFileName = '',
canEditSpreadsheetInline = false
usesJsonRiskRule = false
} = {}) {
if (!skill) return null
@@ -39,15 +35,6 @@ export function buildAuditDetailTopBar({
: 'up',
color: resolveRiskScoreCardColor(scoreLevel)
})
} else if (usesSpreadsheetRule) {
kpis.push({
label: '编辑模式',
value: spreadsheetModeLabel,
unit: '',
meta: spreadsheetFileName,
trend: canEditSpreadsheetInline ? 'up' : 'down',
color: canEditSpreadsheetInline ? 'var(--success)' : '#64748b'
})
}
return {

View File

@@ -7,7 +7,10 @@ import {
VERSION_STATE_META
} from './auditViewMetadata.js'
import {
formatRiskRuleAge,
resolveRiskRuleFlow,
resolveRiskRuleScore,
resolveRiskRuleScoreDetail,
resolveRiskRuleScoreLabel,
resolveRiskRuleScoreLevel,
resolveRiskRuleSeverity,
@@ -71,6 +74,7 @@ import {
applyRiskRuleJsonState,
resolveRiskRuleBusinessStage,
resolveRiskRuleEnabled,
resolveLastOperationLabel,
resolveRiskRuleOnlineMeta
} from './auditViewRiskRuleState.js'
import {

View File

@@ -20,6 +20,7 @@ import {
} from './auditViewDataUtils.js'
import { formatDateTime } from './auditViewFormatters.js'
import {
buildRiskListSubtitle,
resolveRiskRuleCategory,
resolveRiskRuleDescription,
resolveRiskRuleSourceRef
@@ -83,7 +84,7 @@ export function resolveRiskRuleOnlineMeta(statusValue) {
return { label: '待上线', tone: 'draft', online: false }
}
function resolveLastOperationLabel(source, fallback = {}) {
export 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'
@@ -129,9 +130,12 @@ export function applyRiskRuleJsonState(target, payload, apiPayload) {
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) : '-')
const publishedVersionObj = apiPayload.recent_versions.find((item) =>
item?.is_current || item?.version === apiPayload?.published_version
)
publishedAt = publishedVersionObj?.created_at
? formatDateTime(publishedVersionObj.created_at)
: (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)
}