3 Commits

Author SHA1 Message Date
caoxiaozhu
4a72b977ba feat(web): update travel request and reimbursement views 2026-05-14 07:10:46 +00:00
caoxiaozhu
476d5fdf93 feat(web): update TopBar component and useAppShell composable 2026-05-14 07:10:31 +00:00
caoxiaozhu
64ea1bc5fd style(web): update travel views styles 2026-05-14 07:10:20 +00:00
9 changed files with 523 additions and 434 deletions

View File

@@ -24,6 +24,7 @@
--assistant-scale: min(1, var(--assistant-fit-scale-width), var(--assistant-fit-scale-height));
width: calc(var(--assistant-base-width-px) * var(--assistant-scale));
height: calc(var(--assistant-base-height-px) * var(--assistant-scale));
position: relative;
display: block;
background: transparent;
box-shadow: none;
@@ -63,7 +64,7 @@
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 22px 26px 18px;
padding: 22px 172px 18px 26px;
border-bottom: 1px solid rgba(203, 213, 225, 0.78);
background: linear-gradient(180deg, rgba(247, 250, 249, 0.82) 0%, rgba(240, 246, 244, 0.7) 100%);
}
@@ -110,20 +111,25 @@
}
.assistant-header-actions {
position: absolute;
top: calc(22px * var(--assistant-scale));
right: calc(26px * var(--assistant-scale));
z-index: 40;
display: flex;
align-items: center;
gap: 10px;
gap: calc(10px * var(--assistant-scale));
pointer-events: auto;
}
.assistant-toggle-btn,
.session-trash-btn {
width: 38px;
height: 38px;
width: calc(38px * var(--assistant-scale));
height: calc(38px * var(--assistant-scale));
display: grid;
place-items: center;
padding: 0;
border: 1px solid rgba(248, 113, 113, 0.28);
border-radius: 14px;
border-radius: calc(14px * var(--assistant-scale));
flex: none;
}
@@ -131,7 +137,7 @@
border-color: rgba(16, 185, 129, 0.18);
background: rgba(245, 252, 249, 0.96);
color: #166534;
font-size: 16px;
font-size: calc(16px * var(--assistant-scale));
box-shadow: 0 8px 18px rgba(16, 185, 129, 0.1);
}
@@ -150,7 +156,7 @@
.session-trash-btn {
background: rgba(254, 242, 242, 0.96);
color: #dc2626;
font-size: 16px;
font-size: calc(16px * var(--assistant-scale));
box-shadow: 0 8px 18px rgba(239, 68, 68, 0.12);
}
@@ -165,17 +171,41 @@
box-shadow: none;
}
.assistant-close-btn,
.close-btn {
width: 38px;
height: 38px;
position: relative;
width: calc(38px * var(--assistant-scale));
height: calc(38px * var(--assistant-scale));
display: grid;
place-items: center;
padding: 0;
flex: none;
border: 1px solid rgba(193, 204, 216, 0.92);
border-radius: 14px;
border-radius: calc(14px * var(--assistant-scale));
background: rgba(248, 251, 251, 0.94);
color: #475569;
font-size: 16px;
font-size: calc(16px * var(--assistant-scale));
box-shadow: 0 8px 18px rgba(148, 163, 184, 0.18);
cursor: pointer;
pointer-events: auto;
user-select: none;
-webkit-user-select: none;
}
.assistant-close-btn {
z-index: 30;
pointer-events: auto;
}
.assistant-close-btn i {
pointer-events: none;
}
.assistant-close-btn:hover,
.close-btn:hover {
background: rgba(241, 245, 249, 0.98);
border-color: rgba(148, 163, 184, 0.34);
color: #0f172a;
}
.assistant-layout {
@@ -3291,15 +3321,21 @@
}
.assistant-header-actions {
width: 100%;
top: 18px;
right: 18px;
gap: 10px;
width: auto;
justify-content: space-between;
}
.assistant-toggle-btn,
.session-trash-btn,
.assistant-header-actions .close-btn {
.assistant-close-btn,
.close-btn {
width: 40px;
height: 40px;
border-radius: 14px;
font-size: 16px;
}
.assistant-layout {

View File

@@ -32,270 +32,189 @@
.detail-hero {
display: grid;
gap: 14px;
padding: 18px 22px 20px;
gap: 10px;
padding: 18px 24px 18px;
border: 1px solid #edf2f7;
background:
radial-gradient(circle at 100% 100%, rgba(45, 212, 191, .18), transparent 18%),
radial-gradient(circle at 92% 92%, rgba(125, 211, 252, .14), transparent 24%),
linear-gradient(180deg, #ffffff 0%, #fbfdff 100%);
overflow: visible;
}
.hero-topline {
.progress-card {
padding: 18px 22px 20px;
border: 1px solid #edf2f7;
background: linear-gradient(180deg, #ffffff 0%, #fbfdff 100%);
}
.hero-banner {
display: grid;
grid-template-columns: minmax(0, 1.18fr) minmax(0, 1.12fr);
gap: 18px;
gap: 0;
}
.hero-banner-main {
display: grid;
grid-template-columns: minmax(260px, 1.1fr) minmax(0, 2fr);
align-items: center;
padding-bottom: 14px;
border-bottom: 1px solid #e5eaf0;
gap: 16px;
min-height: 104px;
}
.applicant-card {
display: grid;
grid-template-columns: 48px minmax(0, 1fr);
align-items: start;
gap: 12px;
grid-template-columns: 88px minmax(0, 1fr);
align-items: center;
gap: 18px;
min-width: 0;
padding: 0;
border: 0;
border-radius: 0;
background: transparent;
}
.portrait {
width: 44px;
height: 44px;
display: grid;
place-items: center;
border: 1px solid #dde6ef;
border-radius: 10px;
background: #f3f6fa;
color: #0f766e;
font-size: 18px;
font-weight: 800;
width: 88px;
height: 88px;
overflow: hidden;
border: 1px solid #e2e8f0;
border-radius: 999px;
background: linear-gradient(180deg, #eff6ff 0%, #ecfeff 100%);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, .9);
}
.applicant-card h2 {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
color: #0f172a;
font-size: 17px;
font-weight: 800;
line-height: 1.35;
}
.applicant-card h2 span {
margin-left: 0;
padding: 2px 8px;
border: 1px solid #dbe4ee;
border-radius: 6px;
background: #fff;
color: #475569;
font-size: 11px;
font-weight: 700;
}
.applicant-card p {
margin-top: 4px;
color: #64748b;
font-size: 12px;
line-height: 1.5;
.portrait img {
width: 100%;
height: 100%;
display: block;
object-fit: cover;
}
.applicant-copy {
min-width: 0;
}
.applicant-meta {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 8px 14px;
margin-top: 10px;
gap: 14px;
}
.applicant-meta-item {
min-width: 0;
display: grid;
gap: 3px;
.applicant-name-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
}
.applicant-meta-item span {
color: #94a3b8;
font-size: 11px;
font-weight: 700;
letter-spacing: .03em;
text-transform: uppercase;
}
.applicant-meta-item strong {
.applicant-card h2 {
color: #0f172a;
font-size: 12px;
font-weight: 700;
line-height: 1.45;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: 20px;
font-weight: 900;
line-height: 1.2;
}
.hero-stat-strip {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 0;
overflow: hidden;
border: 1px solid #e5eaf0;
border-radius: 10px;
background: #fff;
}
.hero-stat {
display: grid;
align-content: center;
gap: 6px;
min-width: 0;
padding: 12px 16px;
border: 0;
border-left: 1px solid #eef2f7;
border-radius: 0;
background: transparent;
}
.hero-stat:first-child {
border-left: 0;
}
.hero-stat span {
color: #64748b;
font-size: 11px;
font-weight: 700;
letter-spacing: .03em;
text-transform: uppercase;
}
.hero-stat strong {
color: #0f172a;
font-size: 17px;
font-weight: 800;
line-height: 1.35;
}
.hero-stat.emphasis strong {
font-size: 22px;
}
.hero-stat b {
.identity-badge {
display: inline-flex;
align-items: center;
}
.risk-pill,
.state-pill,
.approval-pill {
width: max-content;
max-width: 100%;
padding: 4px 10px;
border-radius: 6px;
font-size: 12px;
min-height: 24px;
padding: 0 9px;
border-radius: 999px;
background: #ecfdf5;
border: 1px solid rgba(16, 185, 129, .16);
color: #10b981;
font-size: 11px;
font-weight: 800;
line-height: 1.4;
}
.state-pill.info,
.approval-pill.info,
.risk-pill.info {
background: #eff6ff;
color: #2563eb;
.applicant-meta-line {
display: flex;
flex-wrap: wrap;
gap: 8px 0;
}
.state-pill.success,
.approval-pill.success,
.risk-pill.success,
.risk-pill.low {
background: #dcfce7;
color: #059669;
}
.state-pill.warning,
.approval-pill.warning,
.risk-pill.warning,
.risk-pill.medium {
background: #fff7ed;
color: #ea580c;
}
.state-pill.danger,
.approval-pill.danger,
.risk-pill.danger,
.risk-pill.high {
background: #fef2f2;
color: #dc2626;
}
.state-pill.draft,
.approval-pill.draft {
background: #fffbeb;
color: #d97706;
}
.risk-pill.neutral {
background: #f1f5f9;
.applicant-meta-line span {
min-width: 0;
position: relative;
display: inline-flex;
align-items: center;
gap: 6px;
color: #475569;
}
.countdown {
display: inline-flex;
align-items: center;
gap: 6px;
color: #f97316 !important;
}
.hero-summary-panel {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 0;
border-top: 1px solid #e5eaf0;
border-bottom: 1px solid #e5eaf0;
}
.hero-summary-item {
min-width: 0;
display: grid;
gap: 5px;
align-content: center;
padding: 12px 14px;
border: 0;
border-right: 1px solid #eef2f7;
border-radius: 0;
background: transparent;
}
.hero-summary-item:last-child {
border-right: 0;
}
.hero-summary-label {
display: inline-flex;
align-items: center;
gap: 6px;
color: #64748b;
font-size: 11px;
font-weight: 700;
letter-spacing: .03em;
text-transform: uppercase;
}
.hero-summary-icon {
color: #64748b;
font-size: 12px;
}
.hero-summary-item strong {
display: block;
color: #0f172a;
font-size: 13px;
font-weight: 800;
line-height: 1.5;
}
.applicant-meta-line span + span {
margin-left: 16px;
}
.applicant-meta-line span + span::before {
content: "•";
position: absolute;
left: -10px;
color: #cbd5e1;
font-size: 12px;
}
.applicant-meta-line em {
font-style: normal;
color: #64748b;
}
.applicant-meta-line strong {
color: #0f172a;
font-weight: 800;
}
.hero-fact-grid {
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 0;
}
.hero-fact {
display: grid;
align-content: center;
gap: 8px;
min-width: 0;
min-height: 92px;
padding: 8px 22px;
background: transparent;
border-left: 1px solid #eaf0f6;
}
.hero-fact:first-child {
border-left: 0;
}
.hero-fact-label {
display: inline-flex;
align-items: center;
gap: 8px;
color: #64748b;
font-size: 11px;
font-weight: 800;
letter-spacing: .03em;
text-transform: uppercase;
}
.hero-fact-label i {
font-size: 15px;
color: #94a3b8;
}
.hero-fact strong {
color: #0f172a;
font-size: 16px;
font-weight: 800;
line-height: 1.4;
white-space: nowrap;
}
.hero-fact strong.amount {
font-size: 22px;
font-weight: 900;
}
.hero-fact strong.status {
color: #f97316;
}
.progress-block {
padding-top: 16px;
border-top: 1px solid #e5eaf0;
padding-top: 0;
border-top: 0;
}
.progress-head {
@@ -1621,25 +1540,19 @@
.detail-modal-leave-to .detail-modal { transform: translateY(8px); opacity: 0; }
@media (max-width: 1320px) {
.detail-hero {
gap: 14px;
.hero-banner-main {
grid-template-columns: 1fr;
gap: 16px;
min-height: 0;
}
.hero-summary-panel {
grid-template-columns: repeat(2, minmax(0, 1fr));
.hero-fact-grid {
grid-template-columns: repeat(5, minmax(132px, 1fr));
overflow-x: auto;
}
.hero-summary-item {
border-right: 1px solid #eef2f7;
border-bottom: 1px solid #eef2f7;
}
.hero-summary-item:nth-child(2n) {
border-right: 0;
}
.hero-summary-item:nth-last-child(-n + 2) {
border-bottom: 0;
.hero-fact {
min-width: 132px;
}
.detail-expense-table {
@@ -1661,32 +1574,69 @@
}
@media (max-width: 760px) {
.detail-hero { gap: 12px; padding: 16px; }
.detail-hero { gap: 10px; padding: 16px; }
.progress-card { padding: 16px; }
.hero-topline {
grid-template-columns: 1fr;
gap: 14px;
.applicant-card {
grid-template-columns: 60px minmax(0, 1fr);
gap: 12px;
}
.applicant-meta {
.portrait {
width: 60px;
height: 60px;
}
.applicant-copy {
gap: 8px;
}
.applicant-card h2 {
font-size: 16px;
}
.applicant-meta-line {
display: grid;
gap: 6px;
}
.applicant-meta-line span + span {
margin-left: 0;
}
.applicant-meta-line span + span::before {
content: none;
}
.hero-fact-grid {
grid-template-columns: 1fr 1fr;
gap: 0;
overflow: hidden;
border-top: 1px solid #edf2f7;
}
.hero-stat-strip {
grid-template-columns: 1fr;
}
.hero-stat {
.hero-fact {
min-width: 0;
min-height: 78px;
padding: 14px 12px 12px;
border-left: 0;
border-top: 1px solid #eef2f7;
border-bottom: 1px solid #edf2f7;
}
.hero-stat:first-child {
border-top: 0;
.hero-fact:nth-child(2n) {
border-left: 1px solid #edf2f7;
}
.hero-summary-panel {
grid-template-columns: 1fr 1fr;
.hero-fact:last-child:nth-child(odd) {
grid-column: 1 / -1;
}
.hero-fact:nth-last-child(-n + 2) {
border-bottom: 0;
}
.hero-fact strong {
white-space: normal;
}
.detail-card {

View File

@@ -81,11 +81,25 @@
</div>
</div>
</div>
</template>
<template v-else-if="isRequests">
<div class="kpi-chips">
<div v-for="kpi in requestKpis" :key="kpi.label" class="kpi-chip" :style="{ '--chip-color': kpi.color }">
</template>
<template v-else-if="isRequestDetail">
<div class="detail-alert-strip">
<span
v-for="alert in detailAlerts"
:key="alert.label"
class="detail-alert-pill"
:class="alert.tone"
>
<i class="mdi mdi-alert-circle-outline"></i>
<span>{{ alert.label }}</span>
</span>
</div>
</template>
<template v-else-if="isRequests">
<div class="kpi-chips">
<div v-for="kpi in requestKpis" :key="kpi.label" class="kpi-chip" :style="{ '--chip-color': kpi.color }">
<span class="chip-value">{{ kpi.value }}<small></small></span>
<span class="chip-label">{{ kpi.label }}</span>
<span class="chip-delta" :class="kpi.trend">{{ kpi.delta }} <i :class="kpi.arrow"></i></span>
@@ -152,6 +166,14 @@ const props = defineProps({
type: Object,
default: () => null
},
detailMode: {
type: Boolean,
default: false
},
detailAlerts: {
type: Array,
default: () => []
},
customRange: {
type: Object,
default: () => ({ start: '2024-07-06', end: '2024-07-12' })
@@ -169,6 +191,7 @@ const emit = defineEmits([
const isChat = computed(() => props.activeView === 'chat')
const isOverview = computed(() => props.activeView === 'overview')
const isRequestDetail = computed(() => props.activeView === 'requests' && props.detailMode)
const isRequests = computed(() => props.activeView === 'requests')
const isApproval = computed(() => props.activeView === 'approval')
const isPolicies = computed(() => props.activeView === 'policies')
@@ -593,14 +616,53 @@ function buildPresetRangeLabel(label) {
background: #cbd5e1;
}
.kpi-chips {
display: flex;
gap: 10px;
}
.kpi-chip {
display: grid;
grid-template-columns: auto auto;
.kpi-chips {
display: flex;
gap: 10px;
}
.detail-alert-strip {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 10px;
flex-wrap: wrap;
}
.detail-alert-pill {
display: inline-flex;
align-items: center;
gap: 6px;
min-height: 32px;
padding: 0 12px;
border: 1px solid #fed7aa;
border-radius: 999px;
background: #fff7ed;
color: #ea580c;
font-size: 12px;
font-weight: 800;
white-space: nowrap;
}
.detail-alert-pill i {
font-size: 14px;
}
.detail-alert-pill.success {
border-color: #bbf7d0;
background: #f0fdf4;
color: #059669;
}
.detail-alert-pill.danger {
border-color: #fecaca;
background: #fff1f2;
color: #dc2626;
}
.kpi-chip {
display: grid;
grid-template-columns: auto auto;
grid-template-rows: auto auto;
gap: 2px 10px;
padding: 8px 16px;
@@ -677,12 +739,13 @@ function buildPresetRangeLabel(label) {
align-items: stretch;
}
.top-actions,
.search-wrap,
.search-wrap.wide,
.month-chip,
.qa-filter,
.new-question-btn {
.top-actions,
.search-wrap,
.search-wrap.wide,
.detail-alert-strip,
.month-chip,
.qa-filter,
.new-question-btn {
width: 100%;
}

View File

@@ -6,6 +6,79 @@ import { useRequests } from './useRequests.js'
import { useToast } from './useToast.js'
import { normalizeRequestForUi } from '../utils/requestViewModel.js'
function isPlaceholderValue(value) {
const text = String(value || '').trim()
if (!text) {
return true
}
return ['待补充', '暂无', '无', '未知', '处理中'].includes(text.replace(/\s+/g, ''))
}
function hasMissingAttachment(request) {
const expenseItems = Array.isArray(request?.expenseItems) ? request.expenseItems : []
if (expenseItems.length) {
return expenseItems.some((item) => !String(item?.invoiceId || item?.invoice_id || '').trim())
}
const attachmentSummary = String(request?.attachmentSummary || '').trim()
const secondaryStatusValue = String(request?.secondaryStatusValue || '').trim()
return /待|缺|未/.test(attachmentSummary) || /待|缺|未/.test(secondaryStatusValue)
}
function hasPendingInfo(request) {
if (!request) {
return false
}
if (request.approvalKey === 'draft' || request.approvalKey === 'supplement') {
return true
}
if (!Number.isFinite(Number(request.amountValue)) || Number(request.amountValue) <= 0) {
return true
}
return [
request.profileDepartment,
request.profilePosition,
request.profileGrade,
request.profileManager,
request.reason,
request.occurredDisplay
].some(isPlaceholderValue)
}
function resolveDetailAlertTone(request) {
if (request?.approvalKey === 'completed') return 'success'
if (request?.approvalKey === 'rejected') return 'danger'
return 'warning'
}
function buildDetailAlerts(request) {
if (!request) {
return []
}
const alerts = []
const nodeLabel = String(request.node || request.approval || '').trim()
if (nodeLabel) {
alerts.push({ label: nodeLabel, tone: resolveDetailAlertTone(request) })
}
if (hasMissingAttachment(request)) {
alerts.push({ label: '缺少票据', tone: 'warning' })
}
if (hasPendingInfo(request)) {
alerts.push({ label: '待补信息', tone: 'warning' })
}
return alerts.filter((item, index, list) => list.findIndex((entry) => entry.label === item.label) === index).slice(0, 3)
}
export function useAppShell() {
const route = useRoute()
const router = useRouter()
@@ -46,6 +119,7 @@ export function useAppShell() {
})
const detailMode = computed(() => route.name === 'app-request-detail')
const detailAlerts = computed(() => (detailMode.value ? buildDetailAlerts(selectedRequest.value) : []))
const topBarView = computed(() => {
if (detailMode.value) {
@@ -182,6 +256,7 @@ export function useAppShell() {
smartEntryContext,
smartEntryOpen,
smartEntrySessionId,
detailAlerts,
toast,
topBarView
}

View File

@@ -34,6 +34,8 @@
:employee-summary="employeeSummary"
:knowledge-summary="knowledgeSummary"
:request-summary="requestSummary"
:detail-mode="detailMode"
:detail-alerts="detailAlerts"
:custom-range="customRange"
@update:search="search = $event"
@update:active-range="activeRange = $event"
@@ -150,6 +152,7 @@ const {
closeRequestDetail,
closeSmartEntry,
customRange,
detailAlerts,
detailMode,
filteredRequests,
filters,

View File

@@ -3,6 +3,40 @@
<Transition name="assistant-modal">
<div class="assistant-overlay">
<section class="assistant-modal">
<div class="assistant-header-actions">
<button
type="button"
class="assistant-toggle-btn"
:class="{ disabled: !hasInsightPanelContent }"
:disabled="!hasInsightPanelContent || sessionSwitchBusy"
:title="insightPanelToggleLabel"
:aria-label="insightPanelToggleLabel"
@click="toggleInsightPanel"
>
<i :class="showInsightPanel ? 'mdi mdi-arrow-collapse-right' : 'mdi mdi-arrow-expand-right'"></i>
</button>
<button
type="button"
class="session-trash-btn"
:disabled="!canDeleteCurrentSession || submitting || reviewActionBusy || deleteSessionBusy || sessionSwitchBusy"
title="删除当前会话"
aria-label="删除当前会话"
@click="openDeleteSessionDialog"
>
<i class="mdi mdi-delete-outline"></i>
</button>
<button
class="assistant-close-btn"
type="button"
title="关闭工作台"
aria-label="关闭对话工作台"
@pointerdown.stop.prevent="requestCloseWorkbench"
@click.stop.prevent="requestCloseWorkbench"
>
<i class="mdi mdi-close"></i>
</button>
</div>
<div class="assistant-modal-stage">
<header class="assistant-header">
<div class="assistant-header-main">
@@ -11,39 +45,6 @@
<p>个人工作台发起报销智能录入统一走这里右侧会根据你的意图实时切换状态视图</p>
</div>
</div>
<div class="assistant-header-actions">
<button
type="button"
class="assistant-toggle-btn"
:class="{ disabled: !hasInsightPanelContent }"
:disabled="!hasInsightPanelContent || sessionSwitchBusy"
:title="insightPanelToggleLabel"
:aria-label="insightPanelToggleLabel"
@click="toggleInsightPanel"
>
<i :class="showInsightPanel ? 'mdi mdi-arrow-collapse-right' : 'mdi mdi-arrow-expand-right'"></i>
</button>
<button
type="button"
class="session-trash-btn"
:disabled="!canDeleteCurrentSession || submitting || reviewActionBusy || deleteSessionBusy || sessionSwitchBusy"
title="删除当前会话"
aria-label="删除当前会话"
@click="openDeleteSessionDialog"
>
<i class="mdi mdi-delete-outline"></i>
</button>
<button
class="close-btn"
type="button"
title="关闭工作台"
aria-label="关闭对话工作台"
@click="requestCloseWorkbench"
>
<i class="mdi mdi-close"></i>
</button>
</div>
</header>
<div class="assistant-layout" :class="{ 'can-show-insight': hasInsightPanelContent, 'has-insight': showInsightPanel }">
@@ -882,22 +883,6 @@
@confirm="confirmDeleteCurrentSession"
/>
<ConfirmDialog
:open="leaveKnowledgeDialogOpen"
badge="离开问答"
badge-tone="warning"
title="离开页面会清除当前知识问答会话"
description="确认离开后,当前财务知识问答 session 会被清除且不再保留。刷新页面不算退出。"
cancel-text="继续停留"
confirm-text="确认离开"
busy-text="清理中..."
confirm-tone="danger"
confirm-icon="mdi mdi-exit-to-app"
:busy="leaveKnowledgeBusy"
@close="closeLeaveKnowledgeDialog"
@confirm="confirmLeaveKnowledgeSession"
/>
<ConfirmDialog
:open="reviewCancelDialogOpen"
badge="取消核对"

View File

@@ -3,40 +3,39 @@
<div class="approval-detail">
<div class="detail-scroll">
<article class="detail-hero panel">
<div class="hero-topline">
<div class="applicant-card">
<div class="portrait">{{ profile.avatar }}</div>
<div class="applicant-copy">
<h2>{{ profile.name }}</h2>
<p>{{ profile.position }}</p>
<div class="applicant-meta">
<div v-for="item in profile.facts" :key="item.label" class="applicant-meta-item">
<span>{{ item.label }}</span>
<strong>{{ item.value }}</strong>
<div class="hero-banner">
<div class="hero-banner-main">
<div class="applicant-card">
<div class="portrait">
<img src="/assets/person.png" alt="" />
</div>
<div class="applicant-copy">
<div class="applicant-name-row">
<h2>{{ profile.name }}</h2>
<span class="identity-badge">{{ profile.identity }}</span>
</div>
<div class="applicant-meta-line">
<span><em>部门</em><strong>{{ profile.department }}</strong></span>
<span><em>职级</em><strong>{{ profile.grade }}</strong></span>
<span><em>直属上司</em><strong>{{ profile.manager }}</strong></span>
</div>
</div>
</div>
</div>
<div class="hero-stat-strip">
<div v-for="stat in heroStats" :key="stat.label" :class="['hero-stat', { emphasis: stat.emphasis }]">
<span>{{ stat.label }}</span>
<strong v-if="stat.kind === 'text'">{{ stat.value }}</strong>
<b v-else :class="[stat.className, stat.tone]">{{ stat.value }}</b>
<div class="hero-fact-grid">
<div v-for="item in heroFactItems" :key="item.key" class="hero-fact">
<div class="hero-fact-label">
<i v-if="item.icon" :class="item.icon"></i>
<span>{{ item.label }}</span>
</div>
<strong :class="item.valueClass">{{ item.value }}</strong>
</div>
</div>
</div>
</div>
</article>
<div class="hero-summary-panel">
<div v-for="item in heroSummaryItems" :key="item.label" class="hero-summary-item">
<div class="hero-summary-label">
<span class="hero-summary-icon"><i :class="item.icon"></i></span>
<span>{{ item.label }}</span>
</div>
<strong>{{ item.value }}</strong>
</div>
</div>
<article class="progress-card panel">
<div class="progress-block">
<div class="progress-head">
<h3>{{ isTravelRequest ? '差旅进度' : '报销进度' }}</h3>

View File

@@ -1976,10 +1976,8 @@ export default {
const reviewCancelDialogOpen = ref(false)
const reviewEditDialogOpen = ref(false)
const deleteSessionDialogOpen = ref(false)
const leaveKnowledgeDialogOpen = ref(false)
const reviewActionBusy = ref(false)
const deleteSessionBusy = ref(false)
const leaveKnowledgeBusy = ref(false)
const reviewEditFields = ref([])
const reviewActionMessageId = ref('')
const reviewInlineForm = ref(createEmptyInlineReviewState())
@@ -2035,6 +2033,7 @@ export default {
}
return labels[currentInsight.value.intent] ?? 'AI 处理中'
})
let knowledgeSessionResetPromise = Promise.resolve()
const canDeleteCurrentSession = computed(
() => Boolean(conversationId.value) || messages.value.some((item) => item.role === 'user')
)
@@ -2190,6 +2189,27 @@ export default {
return buildEmptySessionState(targetSessionType)
}
function resetKnowledgeSessionSnapshot() {
const emptyKnowledgeState = buildEmptySessionState(SESSION_TYPE_KNOWLEDGE)
sessionSnapshots.value[SESSION_TYPE_KNOWLEDGE] = emptyKnowledgeState
if (activeSessionType.value === SESSION_TYPE_KNOWLEDGE) {
applySessionState(emptyKnowledgeState)
}
}
function clearKnowledgeSessionOnEntry() {
resetKnowledgeSessionSnapshot()
knowledgeSessionResetPromise = clearUserConversations(resolveCurrentUserId(), SESSION_TYPE_KNOWLEDGE)
.catch((error) => {
console.warn('Failed to clear knowledge session on entry:', error)
})
.finally(() => {
resetKnowledgeSessionSnapshot()
})
return knowledgeSessionResetPromise
}
async function switchSessionType(targetSessionType) {
const normalizedTarget = String(targetSessionType || '').trim() || SESSION_TYPE_EXPENSE
if (normalizedTarget === activeSessionType.value || sessionSwitchBusy.value) {
@@ -2257,6 +2277,7 @@ export default {
)
onMounted(() => {
void clearKnowledgeSessionOnEntry()
currentInsight.value = currentInsight.value || buildWelcomeInsight(props.entrySource, linkedRequest.value, activeSessionType.value)
if (props.initialPrompt?.trim() || props.initialFiles.length) {
const initialMerge = mergeFilesWithLimit([], Array.from(props.initialFiles), MAX_ATTACHMENTS)
@@ -2585,15 +2606,6 @@ export default {
}
function requestCloseWorkbench() {
if (submitting.value || reviewActionBusy.value || deleteSessionBusy.value || leaveKnowledgeBusy.value || sessionSwitchBusy.value) {
return
}
if (isKnowledgeSession.value) {
leaveKnowledgeDialogOpen.value = true
return
}
emit('close')
}
@@ -2663,31 +2675,6 @@ export default {
}
}
function closeLeaveKnowledgeDialog() {
if (leaveKnowledgeBusy.value) {
return
}
leaveKnowledgeDialogOpen.value = false
}
async function confirmLeaveKnowledgeSession() {
if (leaveKnowledgeBusy.value || sessionSwitchBusy.value) {
return
}
leaveKnowledgeBusy.value = true
try {
await clearUserConversations(resolveCurrentUserId(), SESSION_TYPE_KNOWLEDGE)
sessionSnapshots.value[SESSION_TYPE_KNOWLEDGE] = buildEmptySessionState(SESSION_TYPE_KNOWLEDGE)
leaveKnowledgeDialogOpen.value = false
emit('close')
} catch (error) {
toast(error?.message || '清理知识问答会话失败,请稍后重试。')
} finally {
leaveKnowledgeBusy.value = false
}
}
async function saveInlineReviewChanges() {
if (!activeReviewPayload.value || !reviewHasUnsavedChanges.value || reviewActionBusy.value) return
@@ -3092,10 +3079,8 @@ export default {
reviewCancelDialogOpen,
reviewEditDialogOpen,
deleteSessionDialogOpen,
leaveKnowledgeDialogOpen,
reviewActionBusy,
deleteSessionBusy,
leaveKnowledgeBusy,
reviewEditFields,
documentPreviewDialog,
shortcuts,
@@ -3144,8 +3129,6 @@ export default {
openDeleteSessionDialog,
closeDeleteSessionDialog,
confirmDeleteCurrentSession,
closeLeaveKnowledgeDialog,
confirmLeaveKnowledgeSession,
openInlineReviewEditor,
closeInlineReviewEditor,
commitInlineReviewEditor,

View File

@@ -452,11 +452,6 @@ export default {
department: request.value.profileDepartment,
grade: request.value.profileGrade,
manager: request.value.profileManager,
facts: [
{ label: '部门', value: request.value.profileDepartment },
{ label: '职级', value: request.value.profileGrade },
{ label: '直属上司', value: request.value.profileManager }
],
avatar: request.value.profileAvatar
}))
@@ -484,43 +479,44 @@ export default {
{ immediate: true }
)
const heroStats = computed(() => [
const heroFactItems = computed(() => [
{
key: 'document',
label: '报销单号',
value: request.value.documentNo || request.value.id,
icon: 'mdi mdi-camera-outline',
valueClass: ''
},
{
key: 'date',
label: '日期',
value: request.value.applyTime || request.value.occurredDisplay,
icon: 'mdi mdi-calendar-month-outline',
valueClass: ''
},
{
key: 'amount',
label: '报销金额',
value: request.value.amountDisplay,
kind: 'text',
emphasis: true
icon: '',
valueClass: 'amount'
},
{
label: '报销类型',
key: 'type',
label: isTravelRequest.value ? '差旅类型' : '报销类型',
value: request.value.typeLabel,
kind: 'text'
icon: '',
valueClass: ''
},
{
label: '当前节点',
key: 'status',
label: '当前状态',
value: request.value.node,
kind: 'pill',
className: 'state-pill',
tone: request.value.approvalTone
},
{
label: '审批状态',
value: request.value.approval,
kind: 'pill',
className: 'approval-pill',
tone: request.value.approvalTone
icon: '',
valueClass: 'status'
}
])
const heroSummaryItems = computed(() => {
return [
{ label: '单号', value: request.value.id, icon: 'mdi mdi-pound-box-outline' },
{ label: '发生时间', value: request.value.occurredDisplay, icon: 'mdi mdi-calendar-month-outline' },
{ label: '费用明细', value: `${expenseItems.value.length}`, icon: 'mdi mdi-format-list-bulleted-square' },
{ label: '申请时间', value: request.value.applyTime, icon: 'mdi mdi-timer-sand' }
]
})
const progressSteps = computed(() =>
Array.isArray(request.value.progressSteps) && request.value.progressSteps.length
? request.value.progressSteps
@@ -1136,8 +1132,7 @@ export default {
handleExpenseFileChange,
handleSubmit,
hasExpenseRiskColumn,
heroStats,
heroSummaryItems,
heroFactItems,
isDraftRequest,
isTravelRequest,
locationInputPlaceholder,