feat(web): update TopBar component and useAppShell composable
This commit is contained in:
@@ -81,11 +81,25 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-else-if="isRequests">
|
<template v-else-if="isRequestDetail">
|
||||||
<div class="kpi-chips">
|
<div class="detail-alert-strip">
|
||||||
<div v-for="kpi in requestKpis" :key="kpi.label" class="kpi-chip" :style="{ '--chip-color': kpi.color }">
|
<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-value">{{ kpi.value }}<small>单</small></span>
|
||||||
<span class="chip-label">{{ kpi.label }}</span>
|
<span class="chip-label">{{ kpi.label }}</span>
|
||||||
<span class="chip-delta" :class="kpi.trend">{{ kpi.delta }} <i :class="kpi.arrow"></i></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,
|
type: Object,
|
||||||
default: () => null
|
default: () => null
|
||||||
},
|
},
|
||||||
|
detailMode: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
detailAlerts: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
customRange: {
|
customRange: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => ({ start: '2024-07-06', end: '2024-07-12' })
|
default: () => ({ start: '2024-07-06', end: '2024-07-12' })
|
||||||
@@ -169,6 +191,7 @@ const emit = defineEmits([
|
|||||||
|
|
||||||
const isChat = computed(() => props.activeView === 'chat')
|
const isChat = computed(() => props.activeView === 'chat')
|
||||||
const isOverview = computed(() => props.activeView === 'overview')
|
const isOverview = computed(() => props.activeView === 'overview')
|
||||||
|
const isRequestDetail = computed(() => props.activeView === 'requests' && props.detailMode)
|
||||||
const isRequests = computed(() => props.activeView === 'requests')
|
const isRequests = computed(() => props.activeView === 'requests')
|
||||||
const isApproval = computed(() => props.activeView === 'approval')
|
const isApproval = computed(() => props.activeView === 'approval')
|
||||||
const isPolicies = computed(() => props.activeView === 'policies')
|
const isPolicies = computed(() => props.activeView === 'policies')
|
||||||
@@ -593,14 +616,53 @@ function buildPresetRangeLabel(label) {
|
|||||||
background: #cbd5e1;
|
background: #cbd5e1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.kpi-chips {
|
.kpi-chips {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.kpi-chip {
|
.detail-alert-strip {
|
||||||
display: grid;
|
display: flex;
|
||||||
grid-template-columns: auto auto;
|
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;
|
grid-template-rows: auto auto;
|
||||||
gap: 2px 10px;
|
gap: 2px 10px;
|
||||||
padding: 8px 16px;
|
padding: 8px 16px;
|
||||||
@@ -677,12 +739,13 @@ function buildPresetRangeLabel(label) {
|
|||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
|
|
||||||
.top-actions,
|
.top-actions,
|
||||||
.search-wrap,
|
.search-wrap,
|
||||||
.search-wrap.wide,
|
.search-wrap.wide,
|
||||||
.month-chip,
|
.detail-alert-strip,
|
||||||
.qa-filter,
|
.month-chip,
|
||||||
.new-question-btn {
|
.qa-filter,
|
||||||
|
.new-question-btn {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,79 @@ import { useRequests } from './useRequests.js'
|
|||||||
import { useToast } from './useToast.js'
|
import { useToast } from './useToast.js'
|
||||||
import { normalizeRequestForUi } from '../utils/requestViewModel.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() {
|
export function useAppShell() {
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -46,6 +119,7 @@ export function useAppShell() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const detailMode = computed(() => route.name === 'app-request-detail')
|
const detailMode = computed(() => route.name === 'app-request-detail')
|
||||||
|
const detailAlerts = computed(() => (detailMode.value ? buildDetailAlerts(selectedRequest.value) : []))
|
||||||
|
|
||||||
const topBarView = computed(() => {
|
const topBarView = computed(() => {
|
||||||
if (detailMode.value) {
|
if (detailMode.value) {
|
||||||
@@ -182,6 +256,7 @@ export function useAppShell() {
|
|||||||
smartEntryContext,
|
smartEntryContext,
|
||||||
smartEntryOpen,
|
smartEntryOpen,
|
||||||
smartEntrySessionId,
|
smartEntrySessionId,
|
||||||
|
detailAlerts,
|
||||||
toast,
|
toast,
|
||||||
topBarView
|
topBarView
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,8 @@
|
|||||||
:employee-summary="employeeSummary"
|
:employee-summary="employeeSummary"
|
||||||
:knowledge-summary="knowledgeSummary"
|
:knowledge-summary="knowledgeSummary"
|
||||||
:request-summary="requestSummary"
|
:request-summary="requestSummary"
|
||||||
|
:detail-mode="detailMode"
|
||||||
|
:detail-alerts="detailAlerts"
|
||||||
:custom-range="customRange"
|
:custom-range="customRange"
|
||||||
@update:search="search = $event"
|
@update:search="search = $event"
|
||||||
@update:active-range="activeRange = $event"
|
@update:active-range="activeRange = $event"
|
||||||
@@ -150,6 +152,7 @@ const {
|
|||||||
closeRequestDetail,
|
closeRequestDetail,
|
||||||
closeSmartEntry,
|
closeSmartEntry,
|
||||||
customRange,
|
customRange,
|
||||||
|
detailAlerts,
|
||||||
detailMode,
|
detailMode,
|
||||||
filteredRequests,
|
filteredRequests,
|
||||||
filters,
|
filters,
|
||||||
|
|||||||
Reference in New Issue
Block a user