212 lines
6.6 KiB
Vue
212 lines
6.6 KiB
Vue
<template>
|
|
<article class="panel workbench-card progress-panel" style="--delay: 200ms;">
|
|
<div class="section-head progress-section-head">
|
|
<h2>单据进度</h2>
|
|
<div
|
|
class="progress-range-control"
|
|
@click.stop
|
|
@mousedown.stop
|
|
@pointerdown.stop
|
|
>
|
|
<EnterpriseSelect
|
|
v-model="selectedProgressRange"
|
|
class="progress-range-select"
|
|
:options="PROGRESS_RANGE_OPTIONS"
|
|
size="small"
|
|
:teleported="true"
|
|
aria-label="单据进度时间范围"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="visibleProgressItems.length" class="progress-table-shell">
|
|
<div class="progress-table-header" aria-hidden="true">
|
|
<span class="header-cell header-time">更新时间</span>
|
|
<span class="header-cell header-applicant">提单人</span>
|
|
<span class="header-cell header-identity">单据信息</span>
|
|
<span class="header-cell header-type">类型归属</span>
|
|
<span class="header-cell header-steps">办理进度</span>
|
|
<span class="header-cell header-result">涉及金额</span>
|
|
</div>
|
|
|
|
<div class="progress-list">
|
|
<button
|
|
v-for="(item, index) in visibleProgressItems"
|
|
:key="`${item.id}-${index}`"
|
|
type="button"
|
|
class="progress-row"
|
|
:style="{ '--item-index': index }"
|
|
@click="handleProgressItemClick($event, item)"
|
|
>
|
|
<span class="progress-time-wrapper">
|
|
<span class="expense-type-icon" :class="`expense-type-icon--${item.expenseTypeTone}`">
|
|
<i :class="item.expenseTypeIcon"></i>
|
|
</span>
|
|
<span class="progress-time">
|
|
<time :datetime="item.updatedAt || ''">{{ item.displayTime }}</time>
|
|
</span>
|
|
</span>
|
|
|
|
<span class="progress-applicant" title="申请人">
|
|
<strong>{{ item.applicantLabel || '待补充' }}</strong>
|
|
</span>
|
|
|
|
<span class="progress-identity">
|
|
<strong>{{ item.title }}</strong>
|
|
<small>{{ item.id }}</small>
|
|
</span>
|
|
|
|
<span class="progress-type" :title="`${item.documentTypeLabel} · ${item.expenseTypeLabel || '其他费用'}`">
|
|
<strong>{{ item.documentTypeLabel }} · {{ item.expenseTypeLabel || '其他费用' }}</strong>
|
|
</span>
|
|
|
|
<span class="progress-steps" aria-hidden="true">
|
|
<span
|
|
v-for="step in item.steps"
|
|
:key="step.label"
|
|
class="progress-step"
|
|
:class="{
|
|
'is-done': step.done,
|
|
'is-current': step.current,
|
|
'is-future': !step.done && !step.current
|
|
}"
|
|
>
|
|
<i :class="step.done || step.current ? 'mdi mdi-check' : 'mdi mdi-minus'"></i>
|
|
<small>{{ step.label }}</small>
|
|
</span>
|
|
</span>
|
|
|
|
<span class="progress-result">
|
|
<strong>{{ item.amount }}</strong>
|
|
</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-else class="progress-empty-state" role="status">
|
|
<span class="progress-empty-icon" aria-hidden="true">
|
|
<i class="mdi mdi-file-document-search-outline"></i>
|
|
</span>
|
|
<strong>当前范围暂无单据</strong>
|
|
<p>{{ progressRangeLabel }}内没有申请单或报销单进度。</p>
|
|
</div>
|
|
</article>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { computed, ref } from 'vue'
|
|
import EnterpriseSelect from '../shared/EnterpriseSelect.vue'
|
|
|
|
const PROGRESS_RANGE_OPTIONS = Object.freeze([
|
|
{ value: '10d', label: '近10日' },
|
|
{ value: '30d', label: '近30日' },
|
|
{ value: '3m', label: '近3个月' }
|
|
])
|
|
const DAY_MS = 24 * 60 * 60 * 1000
|
|
|
|
const props = defineProps({
|
|
progressItems: { type: Array, default: () => [] }
|
|
})
|
|
|
|
const emit = defineEmits(['open-target'])
|
|
const selectedProgressRange = ref('30d')
|
|
|
|
const progressRangeLabel = computed(() =>
|
|
PROGRESS_RANGE_OPTIONS.find((item) => item.value === selectedProgressRange.value)?.label || '近30日'
|
|
)
|
|
|
|
const visibleProgressItems = computed(() => {
|
|
const rows = Array.isArray(props.progressItems) ? props.progressItems : []
|
|
return rows
|
|
.filter((item) => isInSelectedProgressRange(item?.updatedAt))
|
|
.map((item) => {
|
|
const expenseStyle = resolveExpenseTypeStyle(item.expenseTypeLabel)
|
|
return {
|
|
...item,
|
|
displayTime: formatProgressTime(item?.updatedAt),
|
|
expenseTypeIcon: expenseStyle.icon,
|
|
expenseTypeTone: expenseStyle.tone
|
|
}
|
|
})
|
|
})
|
|
|
|
function resolveExpenseTypeStyle(label) {
|
|
if (label === '差旅交通') return { icon: 'mdi mdi-airplane', tone: 'blue' }
|
|
if (label === '业务招待') return { icon: 'mdi mdi-silverware-fork-knife', tone: 'amber' }
|
|
if (label === '办公采购') return { icon: 'mdi mdi-cart-outline', tone: 'emerald' }
|
|
if (label === '培训会议') return { icon: 'mdi mdi-projector', tone: 'violet' }
|
|
if (label === '市场活动') return { icon: 'mdi mdi-bullhorn-outline', tone: 'cyan' }
|
|
return { icon: 'mdi mdi-receipt-text-outline', tone: 'muted' }
|
|
}
|
|
|
|
function formatProgressTime(value) {
|
|
const text = String(value || '').trim()
|
|
if (!text) {
|
|
return '最近更新'
|
|
}
|
|
|
|
const match = /^(\d{4})-(\d{2})-(\d{2})(?:[T\s](\d{2}):(\d{2}))?/.exec(text)
|
|
if (match) {
|
|
return match[4] ? `${match[2]}-${match[3]} ${match[4]}:${match[5]}` : `${match[2]}-${match[3]}`
|
|
}
|
|
|
|
return text
|
|
}
|
|
|
|
function parseProgressDate(value) {
|
|
const text = String(value || '').trim()
|
|
if (!text) {
|
|
return null
|
|
}
|
|
|
|
const localDateMatch = /^(\d{4})-(\d{2})-(\d{2})$/.exec(text)
|
|
if (localDateMatch) {
|
|
return new Date(
|
|
Number(localDateMatch[1]),
|
|
Number(localDateMatch[2]) - 1,
|
|
Number(localDateMatch[3])
|
|
)
|
|
}
|
|
|
|
const date = new Date(text)
|
|
return Number.isNaN(date.getTime()) ? null : date
|
|
}
|
|
|
|
function resolveProgressRangeStart() {
|
|
const start = new Date()
|
|
start.setHours(0, 0, 0, 0)
|
|
|
|
if (selectedProgressRange.value === '10d') {
|
|
start.setDate(start.getDate() - 10)
|
|
return start
|
|
}
|
|
|
|
if (selectedProgressRange.value === '3m') {
|
|
start.setMonth(start.getMonth() - 3)
|
|
return start
|
|
}
|
|
|
|
start.setDate(start.getDate() - 30)
|
|
return start
|
|
}
|
|
|
|
function isInSelectedProgressRange(value) {
|
|
const date = parseProgressDate(value)
|
|
if (!date) {
|
|
return true
|
|
}
|
|
|
|
return date.getTime() >= resolveProgressRangeStart().getTime()
|
|
}
|
|
|
|
function handleProgressItemClick(event, item) {
|
|
const target = event?.target
|
|
if (target?.closest?.('.progress-range-control, .enterprise-select-popper, .el-select-dropdown, .el-popper')) {
|
|
return
|
|
}
|
|
emit('open-target', item)
|
|
}
|
|
</script>
|
|
|
|
<style scoped src="../../assets/styles/components/personal-workbench-progress.css"></style>
|