feat(web): update TopBar component and useAppShell composable

This commit is contained in:
caoxiaozhu
2026-05-14 07:10:31 +00:00
parent 64ea1bc5fd
commit 476d5fdf93
3 changed files with 160 additions and 19 deletions

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,