feat(web): update TopBar component and useAppShell composable
This commit is contained in:
@@ -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%;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user