后端新增员工行为画像算法模块,支持标签规则引擎和评分计算, 完善员工模型、银行信息、序列化和导入逻辑,优化报销审批流 和工作流常量,增强 Hermes 同步和知识同步能力,前端新增费 用画像详情弹窗、雷达图和风险卡片组件,完善登录页和工作台 样式,优化文档中心和归档中心交互,补充单元测试。
285 lines
9.8 KiB
JavaScript
285 lines
9.8 KiB
JavaScript
import { computed, ref } from 'vue'
|
|
|
|
import TableLoadingState from '../../components/shared/TableLoadingState.vue'
|
|
import TableEmptyState from '../../components/shared/TableEmptyState.vue'
|
|
import { mapExpenseClaimToRequest } from '../../composables/useRequests.js'
|
|
import { fetchArchivedExpenseClaims } from '../../services/reimbursements.js'
|
|
import {
|
|
ARCHIVE_FILTER_ALL,
|
|
applyArchiveListFilters,
|
|
buildArchiveMonthFilterOptions,
|
|
buildDepartmentFilterOptions,
|
|
buildTypeFilterOptions,
|
|
countClaimRisks,
|
|
extractArchiveMonth,
|
|
formatArchiveMonthLabel,
|
|
formatArchiveRiskCountLabel,
|
|
hasActiveArchiveListFilters,
|
|
resolveArchiveRiskTone
|
|
} from '../../utils/archiveCenterListFilters.js'
|
|
import { normalizeRequestForUi } from '../../utils/requestViewModel.js'
|
|
import TravelRequestDetailView from '../TravelRequestDetailView.vue'
|
|
|
|
const ARCHIVE_TAB_ALL = '全部归档'
|
|
const ARCHIVE_TAB_APPLICATION = '申请归档'
|
|
const ARCHIVE_TAB_REIMBURSEMENT = '报销归档'
|
|
const ARCHIVE_TYPE_APPLICATION = '申请'
|
|
const ARCHIVE_TYPE_APPLICATION_CODE = 'application'
|
|
const ARCHIVE_TYPE_REIMBURSEMENT = '报销'
|
|
const ARCHIVE_TYPE_REIMBURSEMENT_CODE = 'reimbursement'
|
|
const tabs = [ARCHIVE_TAB_ALL, ARCHIVE_TAB_APPLICATION, ARCHIVE_TAB_REIMBURSEMENT]
|
|
const RISK_FILTER_OPTIONS = [
|
|
{ value: ARCHIVE_FILTER_ALL, label: '全部风险' },
|
|
{ value: 'has', label: '有风险' },
|
|
{ value: 'none', label: '无风险' },
|
|
{ value: 'high', label: '高风险' },
|
|
{ value: 'medium', label: '中风险' },
|
|
{ value: 'low', label: '低风险' }
|
|
]
|
|
|
|
function formatCurrency(value) {
|
|
const amount = Number(value)
|
|
return new Intl.NumberFormat('zh-CN', {
|
|
style: 'currency',
|
|
currency: 'CNY',
|
|
minimumFractionDigits: 0,
|
|
maximumFractionDigits: Number.isInteger(amount) ? 0 : 2
|
|
}).format(Number.isFinite(amount) ? amount : 0)
|
|
}
|
|
|
|
function buildArchiveRow(request) {
|
|
const normalized = normalizeRequestForUi(request)
|
|
const riskCount = countClaimRisks(normalized.riskFlags, normalized.riskSummary)
|
|
const riskTone = riskCount > 0 ? resolveArchiveRiskTone(normalized.riskFlags, normalized.riskSummary) : 'none'
|
|
const hasRisk = riskCount > 0
|
|
const isApplicationDocument = normalized.documentTypeCode === 'application'
|
|
const archiveMonth = extractArchiveMonth(
|
|
normalized.updatedAt,
|
|
normalized.submittedAt,
|
|
normalized.createdAt,
|
|
normalized.occurredAt,
|
|
normalized.applyTime
|
|
)
|
|
|
|
return {
|
|
...normalized,
|
|
applicant: normalized.person,
|
|
avatar: String(normalized.person || '?').trim().slice(0, 1) || '?',
|
|
department: normalized.dept,
|
|
type: normalized.typeLabel,
|
|
amount: formatCurrency(normalized.amountValue),
|
|
time: normalized.applyTime,
|
|
archivedAt: normalized.updatedAt || normalized.applyTime,
|
|
archiveMonth,
|
|
archiveMonthLabel: formatArchiveMonthLabel(archiveMonth),
|
|
archiveType: isApplicationDocument ? ARCHIVE_TYPE_APPLICATION : ARCHIVE_TYPE_REIMBURSEMENT,
|
|
archiveTypeCode: isApplicationDocument ? ARCHIVE_TYPE_APPLICATION_CODE : ARCHIVE_TYPE_REIMBURSEMENT_CODE,
|
|
node: isApplicationDocument ? '申请归档' : (normalized.workflowNode || '已付款'),
|
|
hasRisk,
|
|
riskCount,
|
|
risk: formatArchiveRiskCountLabel(riskCount),
|
|
riskTone,
|
|
status: '已归档',
|
|
statusTone: 'archived',
|
|
archiveTab: isApplicationDocument ? ARCHIVE_TAB_APPLICATION : ARCHIVE_TAB_REIMBURSEMENT
|
|
}
|
|
}
|
|
|
|
function resolveFilterLabel(options, activeValue, fallbackLabel) {
|
|
return options.find((item) => item.value === activeValue)?.label || fallbackLabel
|
|
}
|
|
|
|
export default {
|
|
name: 'ArchiveCenterView',
|
|
components: {
|
|
TravelRequestDetailView,
|
|
TableLoadingState,
|
|
TableEmptyState
|
|
},
|
|
setup() {
|
|
const activeTab = ref(ARCHIVE_TAB_ALL)
|
|
const activeRiskFilter = ref(ARCHIVE_FILTER_ALL)
|
|
const activeTypeFilter = ref(ARCHIVE_FILTER_ALL)
|
|
const activeDepartmentFilter = ref(ARCHIVE_FILTER_ALL)
|
|
const activeArchiveMonthFilter = ref(ARCHIVE_FILTER_ALL)
|
|
const selectedClaimId = ref('')
|
|
const listKeyword = ref('')
|
|
const rows = ref([])
|
|
const loading = ref(false)
|
|
const error = ref('')
|
|
|
|
const typeFilterOptions = computed(() => buildTypeFilterOptions(rows.value))
|
|
const departmentFilterOptions = computed(() => buildDepartmentFilterOptions(rows.value))
|
|
const archiveMonthFilterOptions = computed(() => buildArchiveMonthFilterOptions(rows.value))
|
|
|
|
const riskFilterLabel = computed(() => resolveFilterLabel(RISK_FILTER_OPTIONS, activeRiskFilter.value, '全部风险'))
|
|
const typeFilterLabel = computed(() => resolveFilterLabel(typeFilterOptions.value, activeTypeFilter.value, '归档类型'))
|
|
const departmentFilterLabel = computed(() => resolveFilterLabel(departmentFilterOptions.value, activeDepartmentFilter.value, '所属部门'))
|
|
const archiveMonthFilterLabel = computed(() => resolveFilterLabel(archiveMonthFilterOptions.value, activeArchiveMonthFilter.value, '归档月份'))
|
|
|
|
const filterMenus = computed(() => [
|
|
{
|
|
key: 'risk',
|
|
label: riskFilterLabel.value,
|
|
options: RISK_FILTER_OPTIONS,
|
|
activeValue: activeRiskFilter.value
|
|
},
|
|
{
|
|
key: 'type',
|
|
label: typeFilterLabel.value,
|
|
options: typeFilterOptions.value,
|
|
activeValue: activeTypeFilter.value
|
|
},
|
|
{
|
|
key: 'department',
|
|
label: departmentFilterLabel.value,
|
|
options: departmentFilterOptions.value,
|
|
activeValue: activeDepartmentFilter.value
|
|
},
|
|
{
|
|
key: 'archiveMonth',
|
|
label: archiveMonthFilterLabel.value,
|
|
options: archiveMonthFilterOptions.value,
|
|
activeValue: activeArchiveMonthFilter.value
|
|
}
|
|
])
|
|
|
|
const selectedRow = computed({
|
|
get() {
|
|
return rows.value.find((row) => row.claimId === selectedClaimId.value) || null
|
|
},
|
|
set(value) {
|
|
selectedClaimId.value = value?.claimId || ''
|
|
}
|
|
})
|
|
|
|
const visibleRows = computed(() => applyArchiveListFilters(rows.value, {
|
|
tab: activeTab.value,
|
|
risk: activeRiskFilter.value,
|
|
type: activeTypeFilter.value,
|
|
department: activeDepartmentFilter.value,
|
|
archiveMonth: activeArchiveMonthFilter.value,
|
|
keyword: listKeyword.value
|
|
}))
|
|
|
|
const showEmpty = computed(() => !loading.value && !error.value && visibleRows.value.length === 0)
|
|
const archiveEmptyState = computed(() => {
|
|
if (!rows.value.length) {
|
|
return {
|
|
eyebrow: '归档中心',
|
|
title: '当前还没有已归档单据',
|
|
desc: '财务确认付款后进入「已付款」状态的报销单会自动汇总到这里,形成公司级财务归档库。',
|
|
icon: 'mdi mdi-archive-check-outline',
|
|
actionLabel: null,
|
|
actionIcon: null,
|
|
tone: 'slate',
|
|
artLabel: 'ARCHIVE',
|
|
tips: ['仅展示已付款或已归档的单据', '申请人仍可在报销中心查看自己的归档记录']
|
|
}
|
|
}
|
|
|
|
const filtersActive = hasActiveArchiveListFilters({
|
|
tab: activeTab.value,
|
|
risk: activeRiskFilter.value,
|
|
type: activeTypeFilter.value,
|
|
department: activeDepartmentFilter.value,
|
|
archiveMonth: activeArchiveMonthFilter.value,
|
|
keyword: listKeyword.value
|
|
})
|
|
|
|
return {
|
|
eyebrow: filtersActive ? '筛选结果为空' : '归档中心',
|
|
title: filtersActive ? '没有符合当前筛选条件的归档单据' : `“${activeTab.value}”里暂时没有归档单据`,
|
|
desc: filtersActive
|
|
? '可以调整风险、归档类型、部门或归档月份筛选,也可以修改搜索关键词后重试。'
|
|
: '可以切换到其他归档分类查看,或调整筛选条件后重新检索。',
|
|
icon: 'mdi mdi-archive-outline',
|
|
actionLabel: null,
|
|
actionIcon: null,
|
|
tone: 'sky',
|
|
artLabel: filtersActive ? 'FILTER' : 'ARCHIVE',
|
|
tips: ['归档中心保存全公司归档数据', '非申请人无法在报销中心查看他人归档单']
|
|
}
|
|
})
|
|
|
|
function resetListFilters() {
|
|
activeTab.value = ARCHIVE_TAB_ALL
|
|
activeRiskFilter.value = ARCHIVE_FILTER_ALL
|
|
activeTypeFilter.value = ARCHIVE_FILTER_ALL
|
|
activeDepartmentFilter.value = ARCHIVE_FILTER_ALL
|
|
activeArchiveMonthFilter.value = ARCHIVE_FILTER_ALL
|
|
listKeyword.value = ''
|
|
}
|
|
|
|
function handleEmptyAction() {
|
|
if (!rows.value.length) {
|
|
void reload()
|
|
return
|
|
}
|
|
|
|
resetListFilters()
|
|
}
|
|
|
|
function selectFilterValue(key, value) {
|
|
if (key === 'risk') {
|
|
activeRiskFilter.value = value
|
|
} else if (key === 'type') {
|
|
activeTypeFilter.value = value
|
|
} else if (key === 'department') {
|
|
activeDepartmentFilter.value = value
|
|
} else if (key === 'archiveMonth') {
|
|
activeArchiveMonthFilter.value = value
|
|
}
|
|
|
|
}
|
|
|
|
function closeSelectedDetail() {
|
|
selectedClaimId.value = ''
|
|
}
|
|
|
|
async function reload() {
|
|
loading.value = true
|
|
error.value = ''
|
|
|
|
try {
|
|
const payload = await fetchArchivedExpenseClaims()
|
|
const mappedRows = (Array.isArray(payload) ? payload : [])
|
|
.map((item) => mapExpenseClaimToRequest(item))
|
|
.filter(Boolean)
|
|
.map((item) => buildArchiveRow(item))
|
|
rows.value = mappedRows
|
|
if (!mappedRows.some((item) => item.claimId === selectedClaimId.value)) {
|
|
selectedClaimId.value = ''
|
|
}
|
|
} catch (nextError) {
|
|
rows.value = []
|
|
selectedClaimId.value = ''
|
|
error.value = nextError instanceof Error ? nextError.message : '归档中心加载失败。'
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
void reload()
|
|
|
|
return {
|
|
activeTab,
|
|
archiveEmptyState,
|
|
closeSelectedDetail,
|
|
error,
|
|
filterMenus,
|
|
handleEmptyAction,
|
|
listKeyword,
|
|
loading,
|
|
reload,
|
|
resetListFilters,
|
|
rows,
|
|
selectFilterValue,
|
|
selectedRow,
|
|
showEmpty,
|
|
tabs,
|
|
visibleRows
|
|
}
|
|
}
|
|
}
|