feat: 报销审批流重构与管家计划全链路贯通

- 重构报销状态注册表、审批流路由与平台风险标记
- 完善管家意图规划器与模型计划构建器全链路
- 新增 OCR Worker 脚本、数据库会话管理与通知状态
- 优化文档中心、日志视图、预算中心与员工管理交互
- 增强工作台摘要、图标资源与全局主题样式
- 补充审批路由、状态注册、OCR 服务与管家规划器测试覆盖
This commit is contained in:
caoxiaozhu
2026-06-06 17:19:07 +08:00
parent f60cebadb8
commit e124e4bbcb
162 changed files with 9161 additions and 1941 deletions

View File

@@ -1,10 +1,10 @@
<template>
<div class="picker-filter" :class="{ open: activeFilterPopover === id }">
<div class="picker-filter document-filter" :class="{ open: activeFilterPopover === id }">
<button
class="picker-trigger"
class="picker-trigger filter-btn"
type="button"
:aria-expanded="activeFilterPopover === id"
aria-haspopup="dialog"
aria-haspopup="listbox"
@click="emit('toggle', id)"
>
<span class="picker-label">{{ label }}</span>
@@ -12,28 +12,21 @@
</button>
<div
v-if="activeFilterPopover === id"
class="picker-popover"
role="dialog"
class="picker-popover document-filter-menu"
role="listbox"
: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>
<button
v-for="option in options"
:key="option.value || `all-${id}`"
type="button"
role="option"
:aria-selected="selectedValue === option.value"
:class="{ active: selectedValue === option.value }"
@click="emit('select', option.value)"
>
{{ option.label }}
</button>
</div>
</div>
</template>
@@ -56,5 +49,4 @@ defineProps({
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>
<style scoped src="../../assets/styles/components/document-list-shared.css"></style>

View File

@@ -1,12 +1,13 @@
<template>
<article v-if="visible" class="detail-card panel run-products-card">
<div class="card-head">
<div>
<h3>本次任务产物</h3>
<p>{{ productSubtitle }}</p>
</div>
<EnterpriseDetailCard
v-if="visible"
class="run-products-card"
title="本次任务产物"
:description="productSubtitle"
>
<template #actions>
<span class="edit-badge">{{ productBadge }}</span>
</div>
</template>
<div v-if="loading" class="run-product-state">
<i class="mdi mdi-loading mdi-spin"></i>
@@ -211,12 +212,13 @@
</div>
</section>
</template>
</article>
</EnterpriseDetailCard>
</template>
<script setup>
import { computed, ref, watch } from 'vue'
import EnterpriseDetailCard from '../shared/EnterpriseDetailCard.vue'
import { fetchRunRiskObservations } from '../../services/riskObservations.js'
import {
extractWorkRecordToolSummary,

View File

@@ -157,34 +157,26 @@
</template>
</EnterpriseListPage>
<!-- 详情视图 (全屏样式,参考 AuditJsonRiskRuleDetail) -->
<div v-else key="detail" class="json-risk-editor-shell panel work-records-detail-stage">
<div v-if="detailLoading" class="work-record-detail-state panel" style="min-height: 200px; display: grid; place-items: center; border: 0;">
<TableLoadingState
variant="panel"
title="详情加载中"
message="正在读取该次工作记录的完整执行信息"
icon="mdi mdi-clipboard-text-search-outline"
/>
</div>
<div v-else-if="detailError" class="work-record-detail-state error panel" style="min-height: 200px; display: grid; place-items: center; text-align: center; border: 0; color: #dc2626;">
<i class="mdi mdi-alert-circle-outline" style="font-size: 32px; margin-bottom: 8px;"></i>
<strong>工作记录详情加载失败</strong>
<p>{{ detailError }}</p>
<button class="minor-action" type="button" @click="reloadSelectedDetail" style="margin-top: 12px;">重新加载</button>
</div>
<div v-else class="json-risk-editor-body work-record-detail-shell">
<section class="json-risk-main-stage work-record-detail-body inline-detail">
<!-- 卡片1基本信息 -->
<article class="detail-card panel json-risk-summary-card">
<div class="card-head">
<div>
<h3>基本信息</h3>
<p>此次运行的执行周期、触发来源、标识信息与最终状态。</p>
</div>
</div>
<EnterpriseDetailPage
v-else
key="detail"
variant="work-record-detail-page"
actions-class="work-record-detail-actions"
:loading="detailLoading"
:error="detailError"
error-title="工作记录详情加载失败"
loading-title="详情加载中"
loading-message="正在读取该次工作记录的完整执行信息"
loading-icon="mdi mdi-clipboard-text-search-outline"
back-label="返回工作记录列表"
@back="closeWorkRecordDetail"
>
<template #main>
<EnterpriseDetailCard
class="work-record-detail-card work-record-summary-card"
title="基本信息"
description="此次运行的执行周期触发来源标识信息与最终状态"
>
<div class="json-risk-meta-grid">
<div class="json-risk-meta-item">
<span class="json-risk-meta-label">Run ID</span>
@@ -211,75 +203,63 @@
<span class="json-risk-meta-value">{{ resolveWorkRecordStatusNote(selectedRunDetail) || '-' }}</span>
</div>
</div>
</article>
</EnterpriseDetailCard>
<!-- 卡片2执行摘要 -->
<article class="detail-card panel json-risk-description-card">
<div class="card-head">
<div>
<h3>执行摘要</h3>
<p>本次数字员工工作流的执行内容与结果摘要。</p>
</div>
<EnterpriseDetailCard
class="work-record-detail-card work-record-summary-copy-card"
title="执行摘要"
description="本次数字员工工作流的执行内容与结果摘要"
>
<template #actions>
<span class="edit-badge">{{ resolveWorkRecordSummaryMeta(selectedRunDetail) }}</span>
</div>
<p class="json-risk-description-text" style="padding: 0 12px 12px; margin: 0;">{{ selectedRunDetail.result_summary || '暂无执行摘要。' }}</p>
<p v-if="selectedRunDetail.error_message" class="work-record-error-text" style="margin: 0 12px 12px; padding: 10px 12px; border: 1px solid #fecaca; border-radius: 4px; background: #fef2f2; color: #b91c1c;">
</template>
<p class="json-risk-description-text work-record-summary-text">{{ selectedRunDetail.result_summary || '暂无执行摘要。' }}</p>
<p v-if="selectedRunDetail.error_message" class="work-record-error-text">
{{ selectedRunDetail.error_message }}
</p>
</article>
</p>
</EnterpriseDetailCard>
<DigitalEmployeeRunProducts :run="selectedRunDetail" />
<DigitalEmployeeRunProducts :run="selectedRunDetail" />
</template>
<!-- 卡片3工具调用 -->
<article class="detail-card panel">
<div class="card-head">
<div>
<h3>工具调用</h3>
<p>此任务在执行期间调用的外部系统/工具细节与执行状态。</p>
</div>
<template #side>
<EnterpriseDetailCard
class="work-record-detail-card"
title="工具调用"
description="此任务在执行期间调用的外部系统/工具细节与执行状态"
>
<template #actions>
<span class="edit-badge">{{ (selectedRunDetail.tool_calls || []).length }} 次调用</span>
</div>
<div v-if="(selectedRunDetail.tool_calls || []).length" class="work-record-tool-list" style="padding: 0 12px 12px; display: grid; gap: 8px;">
<article v-for="toolCall in selectedRunDetail.tool_calls" :key="toolCall.id" style="display: flex; align-items: center; justify-content: space-between; padding: 10px 12px; border: 1px solid #edf2f7; border-radius: 4px; background: #f8fafc;">
<strong style="color: #0f172a; font-size: 13px;">{{ toolCall.tool_name }}</strong>
<span style="color: #64748b; font-size: 12px;">{{ toolCall.tool_type || 'tool' }} · {{ toolCall.status || 'unknown' }}</span>
</template>
<div v-if="(selectedRunDetail.tool_calls || []).length" class="work-record-tool-list">
<article v-for="toolCall in selectedRunDetail.tool_calls" :key="toolCall.id" class="work-record-tool-item">
<strong>{{ toolCall.tool_name }}</strong>
<span>{{ toolCall.tool_type || 'tool' }} · {{ toolCall.status || 'unknown' }}</span>
</article>
</div>
<div v-else class="work-record-inline-empty" style="padding: 0 12px 12px; color: #94a3b8; font-size: 13px;">当前暂无工具调用明细。</div>
</article>
</div>
<div v-else class="work-record-inline-empty">当前暂无工具调用明细。</div>
</EnterpriseDetailCard>
<!-- 卡片4执行上下文 -->
<article class="detail-card panel">
<div class="card-head">
<div>
<h3>执行上下文</h3>
<p>后台调度的运行时配置与状态信息JSON 格式)。</p>
</div>
</div>
<div style="padding: 0 12px 12px;">
<pre class="work-record-code-block" style="max-height: 320px; margin: 0; padding: 12px; overflow: auto; border: 1px solid #e2e8f0; border-radius: 4px; background: #0f172a; color: #e2e8f0; font-size: 12px; line-height: 1.55;">{{ formatJson(selectedRunDetail.route_json) }}</pre>
</div>
</article>
</section>
</div>
<EnterpriseDetailCard
class="work-record-detail-card"
title="执行上下文"
description="后台调度的运行时配置与状态信息JSON 格式"
>
<pre class="work-record-code-block">{{ formatJson(selectedRunDetail.route_json) }}</pre>
</EnterpriseDetailCard>
</template>
<footer class="detail-actions">
<button class="back-action" type="button" @click="closeWorkRecordDetail">
<i class="mdi mdi-arrow-left"></i>
<span>返回工作记录列表</span>
<template #actions>
<button class="minor-action" type="button" :disabled="!selectedRunDetail?.run_id" @click="openTraceCenter">
<i class="mdi mdi-timeline-text-outline"></i>
<span>查看 Trace</span>
</button>
<div class="detail-action-group">
<button class="minor-action" type="button" :disabled="!selectedRunDetail?.run_id" @click="openTraceCenter">
<i class="mdi mdi-timeline-text-outline"></i>
<span>查看 Trace</span>
</button>
<button class="minor-action" type="button" :disabled="detailLoading" @click="reloadSelectedDetail">
<i class="mdi mdi-refresh"></i>
<span>{{ detailLoading ? '刷新中...' : '刷新详情' }}</span>
</button>
</div>
</footer>
</div>
<button class="minor-action" type="button" :disabled="detailLoading" @click="reloadSelectedDetail">
<i class="mdi mdi-refresh"></i>
<span>{{ detailLoading ? '刷新中...' : '刷新详情' }}</span>
</button>
</template>
</EnterpriseDetailPage>
</Transition>
</section>
</template>
@@ -290,8 +270,9 @@ import { useRouter } from 'vue-router'
import AuditPickerFilter from './AuditPickerFilter.vue'
import DigitalEmployeeRunProducts from './DigitalEmployeeRunProducts.vue'
import EnterpriseDetailCard from '../shared/EnterpriseDetailCard.vue'
import EnterpriseDetailPage from '../shared/EnterpriseDetailPage.vue'
import EnterpriseListPage from '../shared/EnterpriseListPage.vue'
import TableLoadingState from '../shared/TableLoadingState.vue'
import { fetchAgentRunDetail, fetchAgentRuns } from '../../services/agentAssets.js'
import { useToast } from '../../composables/useToast.js'
import {
@@ -650,103 +631,5 @@ onBeforeUnmount(() => {
<style scoped src="../../assets/styles/views/audit-view.css"></style>
<style scoped src="../../assets/styles/views/audit-view-part2.css"></style>
<style scoped>
.digital-work-records {
height: 100%;
}
.digital-employee-list-panel,
.digital-work-records-list-stage {
flex: 1 1 0;
min-height: 0;
display: flex;
flex-direction: column;
padding: 0;
overflow: hidden;
}
.digital-work-records-table {
min-width: 1180px;
table-layout: fixed;
}
.digital-work-records-table .col-time { width: 14%; }
.digital-work-records-table .col-module { width: 13%; }
.digital-work-records-table .col-source { width: 10%; }
.digital-work-records-table .col-status { width: 16%; }
.digital-work-records-table .col-summary { width: 31%; }
.digital-work-records-table .col-trace { width: 16%; }
.work-record-row {
outline: none;
}
.work-record-row:focus-visible {
box-shadow: inset 0 0 0 2px rgba(58, 124, 165, 0.28);
}
.work-record-summary-cell {
text-align: left !important;
}
.work-record-summary-cell strong,
.work-record-summary-cell span,
.work-record-summary-cell em {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.work-record-summary-cell strong {
color: #0f172a;
font-size: 13px;
font-weight: 800;
}
.work-record-summary-cell span {
margin-top: 4px;
color: #64748b;
font-size: 13px;
line-height: 1.5;
}
.work-record-summary-cell em {
margin-top: 6px;
color: #94a3b8;
font-size: 12px;
font-style: normal;
}
.work-record-trace-cell {
color: #2563eb !important;
}
.work-records-detail-stage,
.work-record-detail-shell {
flex: 1 1 0;
min-height: 0;
}
.digital-work-records :deep(.toolbar-actions .picker-filter),
.digital-work-records :deep(.toolbar-actions .picker-trigger) {
min-width: 148px;
}
.digital-refresh-now {
width: 40px;
min-width: 40px;
padding: 0;
}
.digital-refresh-now .mdi {
font-size: 18px;
}
.work-record-detail-body.inline-detail {
padding: 0;
overflow: visible;
background: transparent;
}
</style>
<style scoped src="../../assets/styles/components/digital-employee-work-records.css"></style>
<style scoped src="../../assets/styles/components/digital-employee-work-records-overrides.css"></style>