feat(ui): finalize shared shells and loading states
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
:class="{ active: activeSection === 'skills' }"
|
||||
@click="activeSection = 'skills'"
|
||||
>
|
||||
数字员工
|
||||
员工能力
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -567,6 +567,7 @@
|
||||
title="员工数据同步中"
|
||||
message="正在加载员工档案与角色权限"
|
||||
icon="mdi mdi-account-group-outline"
|
||||
floating
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 : []
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user