2026-05-24 21:44:17 +08:00
|
|
|
<template>
|
|
|
|
|
<section class="documents-page">
|
|
|
|
|
<article class="documents-list panel">
|
|
|
|
|
<nav class="status-tabs document-scope-tabs" aria-label="单据工作视角">
|
|
|
|
|
<button
|
|
|
|
|
v-for="tab in scopeTabItems"
|
|
|
|
|
:key="tab.value"
|
|
|
|
|
type="button"
|
|
|
|
|
:class="{ active: activeScopeTab === tab.value }"
|
|
|
|
|
@click="activeScopeTab = tab.value"
|
|
|
|
|
>
|
2026-06-03 16:46:13 +08:00
|
|
|
<span class="scope-tab-label">
|
|
|
|
|
{{ tab.label }}
|
|
|
|
|
<span v-if="tab.badgeCount > 0" class="scope-tab-badge" aria-label="新增单据数">
|
|
|
|
|
{{ tab.badgeCount > 99 ? '99+' : tab.badgeCount }}
|
|
|
|
|
</span>
|
2026-05-24 21:44:17 +08:00
|
|
|
</span>
|
|
|
|
|
</button>
|
|
|
|
|
</nav>
|
|
|
|
|
|
|
|
|
|
<div class="document-toolbar">
|
|
|
|
|
<div class="filter-set">
|
|
|
|
|
<div class="list-search">
|
|
|
|
|
<i class="mdi mdi-magnify"></i>
|
|
|
|
|
<input v-model="listKeyword" type="search" :placeholder="activeFilterConfig.searchPlaceholder" />
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="document-status-filter" :aria-label="activeFilterConfig.statusTitle">
|
|
|
|
|
<div class="document-filter status-dropdown-filter" :class="{ open: openFilterKey === 'status' }">
|
|
|
|
|
<button
|
|
|
|
|
class="filter-btn status-filter-trigger"
|
|
|
|
|
type="button"
|
|
|
|
|
:aria-expanded="openFilterKey === 'status'"
|
|
|
|
|
@click="toggleFilter('status')"
|
|
|
|
|
>
|
|
|
|
|
<i class="mdi mdi-filter-variant"></i>
|
|
|
|
|
<span>{{ statusFilterLabel }}</span>
|
|
|
|
|
<i class="mdi mdi-chevron-down"></i>
|
|
|
|
|
</button>
|
|
|
|
|
<div
|
|
|
|
|
v-if="openFilterKey === 'status'"
|
|
|
|
|
class="document-filter-menu status-filter-menu"
|
|
|
|
|
role="listbox"
|
2026-06-17 14:39:12 +08:00
|
|
|
aria-label="风险等级"
|
2026-05-24 21:44:17 +08:00
|
|
|
>
|
|
|
|
|
<button
|
|
|
|
|
v-for="option in statusFilterOptions"
|
|
|
|
|
:key="option.value"
|
|
|
|
|
type="button"
|
|
|
|
|
role="option"
|
|
|
|
|
:aria-selected="activeStatusTab === option.value"
|
|
|
|
|
:class="{ active: activeStatusTab === option.value }"
|
|
|
|
|
@click="selectStatusTab(option.value)"
|
|
|
|
|
>
|
|
|
|
|
{{ option.label }}
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div v-if="showDocumentTypeFilter" class="document-filter">
|
|
|
|
|
<button class="filter-btn" type="button" @click="toggleFilter('documentType')">
|
|
|
|
|
<span>{{ documentTypeFilterLabel }}</span>
|
|
|
|
|
<i class="mdi mdi-chevron-down"></i>
|
|
|
|
|
</button>
|
|
|
|
|
<div v-if="openFilterKey === 'documentType'" class="document-filter-menu">
|
|
|
|
|
<button
|
|
|
|
|
v-for="option in documentTypeOptions"
|
|
|
|
|
:key="option.value"
|
|
|
|
|
type="button"
|
|
|
|
|
:class="{ active: activeDocumentType === option.value }"
|
|
|
|
|
@click="selectDocumentType(option.value)"
|
|
|
|
|
>
|
|
|
|
|
{{ option.label }}
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="document-filter">
|
|
|
|
|
<button class="filter-btn" type="button" @click="toggleFilter('scene')">
|
|
|
|
|
<span>{{ sceneFilterLabel }}</span>
|
|
|
|
|
<i class="mdi mdi-chevron-down"></i>
|
|
|
|
|
</button>
|
|
|
|
|
<div v-if="openFilterKey === 'scene'" class="document-filter-menu">
|
|
|
|
|
<button
|
|
|
|
|
v-for="option in sceneFilterOptions"
|
|
|
|
|
:key="option.value"
|
|
|
|
|
type="button"
|
|
|
|
|
:class="{ active: activeScene === option.value }"
|
|
|
|
|
@click="selectScene(option.value)"
|
|
|
|
|
>
|
|
|
|
|
{{ option.label }}
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="date-range-filter" :class="{ open: datePopover }">
|
2026-05-27 12:27:17 +08:00
|
|
|
<button class="filter-btn date-range-trigger" type="button" @click="toggleDatePopover">
|
2026-05-24 21:44:17 +08:00
|
|
|
<span class="date-range-label">{{ dateRangeLabel }}</span>
|
|
|
|
|
<i class="mdi mdi-calendar"></i>
|
|
|
|
|
</button>
|
|
|
|
|
<div v-if="datePopover" class="date-range-popover" role="dialog" aria-label="选择时间段">
|
|
|
|
|
<header>
|
|
|
|
|
<strong>选择时间段</strong>
|
|
|
|
|
<button type="button" aria-label="关闭" @click="datePopover = false">
|
|
|
|
|
<i class="mdi mdi-close"></i>
|
|
|
|
|
</button>
|
|
|
|
|
</header>
|
|
|
|
|
<div class="date-range-fields">
|
|
|
|
|
<label>
|
|
|
|
|
<span>开始日期</span>
|
|
|
|
|
<input v-model="rangeStart" type="date" />
|
|
|
|
|
</label>
|
|
|
|
|
<label>
|
|
|
|
|
<span>结束日期</span>
|
|
|
|
|
<input v-model="rangeEnd" type="date" />
|
|
|
|
|
</label>
|
|
|
|
|
</div>
|
|
|
|
|
<footer>
|
|
|
|
|
<button class="ghost-btn" type="button" @click="clearDateRange">清空</button>
|
|
|
|
|
<button class="apply-btn" type="button" :disabled="!rangeStart || !rangeEnd" @click="applyDateRange">应用</button>
|
|
|
|
|
</footer>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-06-03 16:46:13 +08:00
|
|
|
<div v-if="showToolbarActions" class="document-actions">
|
|
|
|
|
<button
|
|
|
|
|
v-if="totalNewDocumentCount > 0"
|
|
|
|
|
class="mark-read-btn"
|
|
|
|
|
type="button"
|
|
|
|
|
@click="markAllDocumentsRead"
|
|
|
|
|
>
|
|
|
|
|
<i class="mdi mdi-check-all"></i>
|
|
|
|
|
<span>一键已读</span>
|
|
|
|
|
</button>
|
2026-05-24 21:44:17 +08:00
|
|
|
<button v-if="activeScopeTab === DOCUMENT_SCOPE_APPLICATION" class="create-request-btn" type="button" @click="emit('create-application')">
|
|
|
|
|
<i class="mdi mdi-file-plus-outline"></i>
|
|
|
|
|
<span>发起申请</span>
|
|
|
|
|
</button>
|
|
|
|
|
<button v-if="activeScopeTab === DOCUMENT_SCOPE_REIMBURSEMENT" class="create-request-btn" type="button" @click="emit('create-request')">
|
|
|
|
|
<i class="mdi mdi-plus-circle-outline"></i>
|
|
|
|
|
<span>发起报销</span>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="table-wrap" :class="{ 'is-empty': showEmpty }">
|
|
|
|
|
<div v-if="showLoading" class="table-state">
|
|
|
|
|
<TableLoadingState
|
|
|
|
|
title="单据数据同步中"
|
|
|
|
|
message="正在汇总当前报销、审批待办与归档单据"
|
|
|
|
|
icon="mdi mdi-file-document-multiple-outline"
|
2026-05-29 13:17:39 +08:00
|
|
|
floating
|
2026-05-24 21:44:17 +08:00
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div v-else-if="showError" class="table-state error">
|
|
|
|
|
<i class="mdi mdi-alert-circle-outline"></i>
|
|
|
|
|
<strong>单据中心加载失败</strong>
|
|
|
|
|
<p>{{ errorMessage }}</p>
|
|
|
|
|
<button class="retry-btn" type="button" @click="reloadAll">重新加载</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<TableEmptyState
|
|
|
|
|
v-else-if="showEmpty"
|
|
|
|
|
:eyebrow="emptyState.eyebrow"
|
|
|
|
|
:title="emptyState.title"
|
|
|
|
|
:description="emptyState.desc"
|
|
|
|
|
:icon="emptyState.icon"
|
|
|
|
|
:action-label="emptyState.actionLabel"
|
|
|
|
|
:action-icon="emptyState.actionIcon"
|
|
|
|
|
:tone="emptyState.tone"
|
|
|
|
|
:art-label="emptyState.artLabel"
|
|
|
|
|
:tips="emptyState.tips"
|
|
|
|
|
@action="handleEmptyAction"
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<table v-else>
|
|
|
|
|
<colgroup>
|
|
|
|
|
<col class="col-id">
|
|
|
|
|
<col class="col-created">
|
|
|
|
|
<col v-if="showStayTimeColumn" class="col-stay">
|
|
|
|
|
<col class="col-doc-type">
|
|
|
|
|
<col class="col-scene">
|
2026-05-27 17:31:27 +08:00
|
|
|
<col class="col-initiator">
|
2026-05-24 21:44:17 +08:00
|
|
|
<col class="col-title">
|
|
|
|
|
<col class="col-amount">
|
|
|
|
|
<col class="col-node">
|
2026-06-17 14:39:12 +08:00
|
|
|
<col class="col-risk">
|
2026-05-24 21:44:17 +08:00
|
|
|
<col class="col-updated">
|
|
|
|
|
</colgroup>
|
|
|
|
|
<thead>
|
|
|
|
|
<tr>
|
|
|
|
|
<th>单号</th>
|
|
|
|
|
<th>创建时间</th>
|
|
|
|
|
<th v-if="showStayTimeColumn">停留时间</th>
|
|
|
|
|
<th>单据类型</th>
|
|
|
|
|
<th>费用场景</th>
|
2026-05-27 17:31:27 +08:00
|
|
|
<th>发起人</th>
|
2026-05-24 21:44:17 +08:00
|
|
|
<th>事项</th>
|
|
|
|
|
<th>金额</th>
|
|
|
|
|
<th>当前环节</th>
|
2026-06-17 14:39:12 +08:00
|
|
|
<th>风险等级</th>
|
2026-05-24 21:44:17 +08:00
|
|
|
<th>更新时间</th>
|
|
|
|
|
</tr>
|
|
|
|
|
</thead>
|
|
|
|
|
<tbody>
|
|
|
|
|
<tr v-for="row in visibleRows" :key="row.documentKey" @click="openDocument(row)">
|
2026-06-02 14:01:51 +08:00
|
|
|
<td data-label="单号">
|
2026-05-25 13:35:39 +08:00
|
|
|
<span v-if="row.isNewDocument" class="new-document-badge">NEW</span>
|
|
|
|
|
<strong class="doc-id">{{ row.documentNo }}</strong>
|
|
|
|
|
</td>
|
2026-06-02 14:01:51 +08:00
|
|
|
<td data-label="创建时间">{{ row.createdAtDisplay }}</td>
|
|
|
|
|
<td v-if="showStayTimeColumn" data-label="停留时间">{{ row.stayTimeDisplay }}</td>
|
|
|
|
|
<td data-label="单据类型"><span class="doc-kind-tag" :class="row.documentTypeCode">{{ row.documentTypeLabel }}</span></td>
|
|
|
|
|
<td data-label="费用场景"><span class="type-tag" :class="row.typeTone">{{ row.typeLabel }}</span></td>
|
|
|
|
|
<td data-label="发起人">{{ row.initiatorName }}</td>
|
|
|
|
|
<td data-label="事项">{{ row.reason }}</td>
|
|
|
|
|
<td data-label="金额">{{ row.amountDisplay }}</td>
|
|
|
|
|
<td data-label="当前环节">{{ row.node }}</td>
|
2026-06-17 14:39:12 +08:00
|
|
|
<td data-label="风险等级">
|
|
|
|
|
<span class="risk-level-tags">
|
|
|
|
|
<span
|
|
|
|
|
v-for="tag in row.riskTags"
|
|
|
|
|
:key="tag.label"
|
|
|
|
|
class="risk-level-tag"
|
|
|
|
|
:class="tag.tone"
|
|
|
|
|
>
|
|
|
|
|
{{ tag.label }}
|
|
|
|
|
</span>
|
|
|
|
|
</span>
|
|
|
|
|
</td>
|
2026-06-02 14:01:51 +08:00
|
|
|
<td data-label="更新时间">{{ row.updatedAtDisplay }}</td>
|
2026-05-24 21:44:17 +08:00
|
|
|
</tr>
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-06-03 09:25:23 +08:00
|
|
|
<EnterprisePagination
|
|
|
|
|
v-if="showTable"
|
|
|
|
|
:current-page="currentPage"
|
|
|
|
|
:page-size="pageSize"
|
|
|
|
|
:page-size-options="pageSizeOptions"
|
|
|
|
|
:summary="pageSummary"
|
|
|
|
|
:total-pages="totalPages"
|
|
|
|
|
@page-size-change="changePageSize"
|
|
|
|
|
@update:current-page="currentPage = $event"
|
|
|
|
|
/>
|
2026-05-24 21:44:17 +08:00
|
|
|
</article>
|
|
|
|
|
</section>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup>
|
|
|
|
|
import { computed, onMounted, ref, watch } from 'vue'
|
2026-06-03 15:46:56 +08:00
|
|
|
import { useRoute, useRouter } from 'vue-router'
|
2026-06-03 09:25:23 +08:00
|
|
|
import EnterprisePagination from '../components/shared/EnterprisePagination.vue'
|
2026-05-24 21:44:17 +08:00
|
|
|
import TableEmptyState from '../components/shared/TableEmptyState.vue'
|
|
|
|
|
import TableLoadingState from '../components/shared/TableLoadingState.vue'
|
2026-05-29 13:17:39 +08:00
|
|
|
import { useMinimumVisibleState } from '../composables/useMinimumVisibleState.js'
|
2026-06-20 10:17:37 +08:00
|
|
|
import { useSystemState } from '../composables/useSystemState.js'
|
2026-05-24 21:44:17 +08:00
|
|
|
import { mapExpenseClaimToRequest } from '../composables/useRequests.js'
|
2026-06-06 17:19:07 +08:00
|
|
|
import {
|
|
|
|
|
extractExpenseClaimItems,
|
2026-06-09 08:32:00 +00:00
|
|
|
fetchAllApprovalExpenseClaims,
|
|
|
|
|
fetchAllArchivedExpenseClaims
|
2026-06-06 17:19:07 +08:00
|
|
|
} from '../services/reimbursements.js'
|
|
|
|
|
import { fetchNotificationStates, patchNotificationStates } from '../services/notificationStates.js'
|
|
|
|
|
import {
|
|
|
|
|
buildDocumentViewedStatePatch,
|
|
|
|
|
buildDocumentsViewedStatePatches,
|
|
|
|
|
countNewDocuments,
|
|
|
|
|
isNewDocument,
|
|
|
|
|
markDocumentViewed,
|
|
|
|
|
markDocumentsViewed,
|
|
|
|
|
mergeNotificationStatesIntoViewedDocumentKeys,
|
|
|
|
|
readDocumentScope,
|
|
|
|
|
readViewedDocumentKeys,
|
|
|
|
|
writeDocumentScope
|
|
|
|
|
} from '../utils/documentCenterNewState.js'
|
2026-06-22 11:58:53 +08:00
|
|
|
import { excludeArchivedDocumentRows, filterApplicationScopeNewRows, prepareApplicationScopeRows } from '../utils/documentCenterRows.js'
|
|
|
|
|
import {
|
|
|
|
|
DOCUMENT_CENTER_QUERY_KEYS, DOCUMENT_LOADING_MIN_VISIBLE_MS, DOCUMENT_SCOPE_ALL,
|
|
|
|
|
DOCUMENT_SCOPE_APPLICATION, DOCUMENT_SCOPE_ARCHIVE, DOCUMENT_SCOPE_REIMBURSEMENT,
|
|
|
|
|
DOCUMENT_SCOPE_REVIEW, DOCUMENT_TYPE_ALL, DOCUMENT_TYPE_APPLICATION,
|
|
|
|
|
DOCUMENT_TYPE_REIMBURSEMENT, FILTER_CONFIG_BY_SCOPE, SCENE_ALL,
|
|
|
|
|
buildDocumentCenterEmptyState, buildDocumentRow, documentTypeOptions,
|
|
|
|
|
filterDocumentRows, hasDocumentCenterActiveFilters, mergeDocumentRows,
|
|
|
|
|
pageSizeOptions, pageSizeValues, routeQueryEquals, scopeTabs
|
|
|
|
|
} from '../utils/documentCenterViewModel.js'
|
2026-06-03 15:46:56 +08:00
|
|
|
const route = useRoute()
|
|
|
|
|
const router = useRouter()
|
2026-05-24 21:44:17 +08:00
|
|
|
const props = defineProps({
|
|
|
|
|
filteredRequests: { type: Array, required: true },
|
|
|
|
|
hasData: { type: Boolean, default: false },
|
|
|
|
|
loading: { type: Boolean, default: false },
|
2026-06-03 09:25:23 +08:00
|
|
|
error: { type: String, default: '' },
|
|
|
|
|
refreshToken: { type: Number, default: 0 }
|
2026-05-24 21:44:17 +08:00
|
|
|
})
|
|
|
|
|
const emit = defineEmits([
|
|
|
|
|
'open-document',
|
|
|
|
|
'create-request',
|
|
|
|
|
'create-application',
|
|
|
|
|
'reload',
|
|
|
|
|
'summary-change'
|
|
|
|
|
])
|
2026-06-03 15:46:56 +08:00
|
|
|
|
2026-06-20 10:17:37 +08:00
|
|
|
const { currentUser } = useSystemState()
|
|
|
|
|
|
2026-06-03 15:46:56 +08:00
|
|
|
function readDocumentCenterQueryText(key) {
|
|
|
|
|
const value = route.query?.[key]
|
|
|
|
|
return String(Array.isArray(value) ? value[0] || '' : value || '').trim()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function readDocumentCenterQueryNumber(key, fallback) {
|
|
|
|
|
const parsed = Number(readDocumentCenterQueryText(key))
|
|
|
|
|
return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : fallback
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function resolveInitialScopeTab() {
|
|
|
|
|
const queryScope = readDocumentCenterQueryText('dc_scope')
|
|
|
|
|
if (scopeTabs.includes(queryScope)) {
|
|
|
|
|
return queryScope
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return readDocumentScope(DOCUMENT_SCOPE_ALL, scopeTabs)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function resolveInitialStatusTab(scope) {
|
|
|
|
|
const queryStatus = readDocumentCenterQueryText('dc_status') || '全部'
|
|
|
|
|
const config = FILTER_CONFIG_BY_SCOPE[scope] || FILTER_CONFIG_BY_SCOPE[DOCUMENT_SCOPE_ALL]
|
|
|
|
|
return config.statusTabs.includes(queryStatus) ? queryStatus : '全部'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function resolveInitialDocumentType() {
|
|
|
|
|
const queryType = readDocumentCenterQueryText('dc_doc_type')
|
|
|
|
|
return documentTypeOptions.some((item) => item.value === queryType)
|
|
|
|
|
? queryType
|
|
|
|
|
: DOCUMENT_TYPE_ALL
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function resolveInitialPageSize() {
|
|
|
|
|
const queryPageSize = readDocumentCenterQueryNumber('dc_page_size', 20)
|
|
|
|
|
return pageSizeValues.includes(queryPageSize) ? queryPageSize : 20
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function buildDocumentCenterRouteQuery() {
|
|
|
|
|
const nextQuery = {}
|
|
|
|
|
Object.entries(route.query || {}).forEach(([key, value]) => {
|
|
|
|
|
if (!DOCUMENT_CENTER_QUERY_KEYS.has(key)) {
|
|
|
|
|
nextQuery[key] = value
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if (currentPage.value > 1) nextQuery.dc_page = String(currentPage.value)
|
|
|
|
|
if (pageSize.value !== 20) nextQuery.dc_page_size = String(pageSize.value)
|
|
|
|
|
if (activeScopeTab.value !== DOCUMENT_SCOPE_ALL) nextQuery.dc_scope = activeScopeTab.value
|
|
|
|
|
if (activeStatusTab.value !== '全部') nextQuery.dc_status = activeStatusTab.value
|
|
|
|
|
if (showDocumentTypeFilter.value && activeDocumentType.value !== DOCUMENT_TYPE_ALL) {
|
|
|
|
|
nextQuery.dc_doc_type = activeDocumentType.value
|
|
|
|
|
}
|
|
|
|
|
if (activeScene.value !== SCENE_ALL) nextQuery.dc_scene = activeScene.value
|
|
|
|
|
if (listKeyword.value.trim()) nextQuery.dc_q = listKeyword.value.trim()
|
|
|
|
|
if (appliedStart.value) nextQuery.dc_start = appliedStart.value
|
|
|
|
|
if (appliedEnd.value) nextQuery.dc_end = appliedEnd.value
|
|
|
|
|
|
|
|
|
|
return nextQuery
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const initialScopeTab = resolveInitialScopeTab()
|
|
|
|
|
const initialAppliedStart = readDocumentCenterQueryText('dc_start')
|
|
|
|
|
const initialAppliedEnd = readDocumentCenterQueryText('dc_end')
|
|
|
|
|
const activeScopeTab = ref(initialScopeTab)
|
|
|
|
|
const activeStatusTab = ref(resolveInitialStatusTab(initialScopeTab))
|
|
|
|
|
const activeDocumentType = ref(resolveInitialDocumentType())
|
|
|
|
|
const activeScene = ref(readDocumentCenterQueryText('dc_scene') || SCENE_ALL)
|
2026-05-24 21:44:17 +08:00
|
|
|
const openFilterKey = ref('')
|
2026-06-03 15:46:56 +08:00
|
|
|
const listKeyword = ref(readDocumentCenterQueryText('dc_q'))
|
2026-05-24 21:44:17 +08:00
|
|
|
const datePopover = ref(false)
|
2026-06-03 15:46:56 +08:00
|
|
|
const rangeStart = ref(initialAppliedStart)
|
|
|
|
|
const rangeEnd = ref(initialAppliedEnd)
|
|
|
|
|
const appliedStart = ref(initialAppliedStart)
|
|
|
|
|
const appliedEnd = ref(initialAppliedEnd)
|
|
|
|
|
const currentPage = ref(readDocumentCenterQueryNumber('dc_page', 1))
|
|
|
|
|
const pageSize = ref(resolveInitialPageSize())
|
2026-05-24 21:44:17 +08:00
|
|
|
const archiveRows = ref([])
|
|
|
|
|
const approvalRows = ref([])
|
|
|
|
|
const supportingLoading = ref(false)
|
|
|
|
|
const supportingError = ref('')
|
2026-05-25 13:35:39 +08:00
|
|
|
const viewedDocumentKeys = ref(readViewedDocumentKeys())
|
2026-05-24 21:44:17 +08:00
|
|
|
const activeFilterConfig = computed(() =>
|
|
|
|
|
FILTER_CONFIG_BY_SCOPE[activeScopeTab.value] || FILTER_CONFIG_BY_SCOPE[DOCUMENT_SCOPE_APPLICATION]
|
|
|
|
|
)
|
|
|
|
|
const showDocumentTypeFilter = computed(() => Boolean(activeFilterConfig.value.showDocumentType))
|
|
|
|
|
const documentTypeFilterLabel = computed(() =>
|
|
|
|
|
documentTypeOptions.find((item) => item.value === activeDocumentType.value)?.label || '单据类型'
|
|
|
|
|
)
|
|
|
|
|
const statusFilterOptions = computed(() =>
|
|
|
|
|
activeFilterConfig.value.statusTabs.map((tab) => ({
|
|
|
|
|
value: tab,
|
2026-06-17 14:39:12 +08:00
|
|
|
label: tab === '全部' ? '全部风险' : tab
|
2026-05-24 21:44:17 +08:00
|
|
|
}))
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const dateRangeLabel = computed(() => {
|
|
|
|
|
if (appliedStart.value && appliedEnd.value) {
|
|
|
|
|
return `${appliedStart.value} ~ ${appliedEnd.value}`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return activeFilterConfig.value.dateLabel
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const ownedRows = computed(() =>
|
2026-05-26 09:15:14 +08:00
|
|
|
excludeArchivedDocumentRows(
|
|
|
|
|
props.filteredRequests
|
2026-06-22 11:58:53 +08:00
|
|
|
.map((item) => buildDocumentRow(item, {
|
|
|
|
|
source: 'owned',
|
|
|
|
|
currentUser: currentUser.value,
|
|
|
|
|
viewedDocumentKeys: viewedDocumentKeys.value
|
|
|
|
|
}))
|
2026-05-26 09:15:14 +08:00
|
|
|
.filter(Boolean)
|
|
|
|
|
)
|
2026-05-24 21:44:17 +08:00
|
|
|
)
|
|
|
|
|
|
2026-05-25 13:35:39 +08:00
|
|
|
const nonArchivedRows = computed(() => mergeDocumentRows([...ownedRows.value, ...approvalRows.value]))
|
2026-05-27 14:35:17 +08:00
|
|
|
const applicationScopeRows = computed(() => prepareApplicationScopeRows(ownedRows.value))
|
2026-05-24 21:44:17 +08:00
|
|
|
|
|
|
|
|
const scopeNewCountMap = computed(() => ({
|
2026-05-25 13:35:39 +08:00
|
|
|
[DOCUMENT_SCOPE_ALL]: countNewDocuments(nonArchivedRows.value, viewedDocumentKeys.value),
|
2026-05-27 14:35:17 +08:00
|
|
|
[DOCUMENT_SCOPE_APPLICATION]: countNewDocuments(filterApplicationScopeNewRows(applicationScopeRows.value), viewedDocumentKeys.value),
|
2026-05-25 13:35:39 +08:00
|
|
|
[DOCUMENT_SCOPE_REIMBURSEMENT]: countNewDocuments(ownedRows.value.filter((row) => row.documentTypeCode === DOCUMENT_TYPE_REIMBURSEMENT), viewedDocumentKeys.value),
|
|
|
|
|
[DOCUMENT_SCOPE_REVIEW]: countNewDocuments(approvalRows.value, viewedDocumentKeys.value),
|
|
|
|
|
[DOCUMENT_SCOPE_ARCHIVE]: countNewDocuments(archiveRows.value, viewedDocumentKeys.value)
|
2026-05-24 21:44:17 +08:00
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
const scopeTabItems = computed(() =>
|
|
|
|
|
scopeTabs.map((tab) => ({
|
|
|
|
|
value: tab,
|
|
|
|
|
label: tab,
|
|
|
|
|
badgeCount: scopeNewCountMap.value[tab] || 0
|
|
|
|
|
}))
|
|
|
|
|
)
|
2026-06-03 16:46:13 +08:00
|
|
|
const allReadableDocumentRows = computed(() => [
|
|
|
|
|
...nonArchivedRows.value,
|
|
|
|
|
...filterApplicationScopeNewRows(applicationScopeRows.value),
|
|
|
|
|
...ownedRows.value.filter((row) => row.documentTypeCode === DOCUMENT_TYPE_REIMBURSEMENT),
|
|
|
|
|
...approvalRows.value
|
|
|
|
|
])
|
|
|
|
|
const totalNewDocumentCount = computed(() => countNewDocuments(allReadableDocumentRows.value, viewedDocumentKeys.value))
|
|
|
|
|
const showCreateDocumentActions = computed(() =>
|
|
|
|
|
[DOCUMENT_SCOPE_APPLICATION, DOCUMENT_SCOPE_REIMBURSEMENT].includes(activeScopeTab.value)
|
|
|
|
|
)
|
|
|
|
|
const showToolbarActions = computed(() => showCreateDocumentActions.value || totalNewDocumentCount.value > 0)
|
2026-05-24 21:44:17 +08:00
|
|
|
|
|
|
|
|
const activeScopeRows = computed(() => {
|
2026-05-25 13:35:39 +08:00
|
|
|
if (activeScopeTab.value === DOCUMENT_SCOPE_ALL) return nonArchivedRows.value
|
|
|
|
|
|
2026-05-24 21:44:17 +08:00
|
|
|
if (activeScopeTab.value === DOCUMENT_SCOPE_APPLICATION) {
|
2026-05-27 14:35:17 +08:00
|
|
|
return applicationScopeRows.value
|
2026-05-24 21:44:17 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (activeScopeTab.value === DOCUMENT_SCOPE_REIMBURSEMENT) {
|
|
|
|
|
return ownedRows.value.filter((row) => row.documentTypeCode === DOCUMENT_TYPE_REIMBURSEMENT)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (activeScopeTab.value === DOCUMENT_SCOPE_REVIEW) {
|
|
|
|
|
return approvalRows.value
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (activeScopeTab.value === DOCUMENT_SCOPE_ARCHIVE) {
|
|
|
|
|
return archiveRows.value
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-25 13:35:39 +08:00
|
|
|
return nonArchivedRows.value
|
2026-05-24 21:44:17 +08:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const sceneFilterOptions = computed(() => {
|
|
|
|
|
const sceneMap = new Map([[SCENE_ALL, activeFilterConfig.value.sceneFallbackLabel]])
|
|
|
|
|
activeScopeRows.value.forEach((row) => {
|
|
|
|
|
if (row.typeCode && row.typeLabel) {
|
|
|
|
|
sceneMap.set(row.typeCode, row.typeLabel)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return Array.from(sceneMap, ([value, label]) => ({ value, label }))
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const sceneFilterLabel = computed(() =>
|
|
|
|
|
sceneFilterOptions.value.find((item) => item.value === activeScene.value)?.label || activeFilterConfig.value.sceneFallbackLabel
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const statusFilterLabel = computed(() =>
|
2026-06-17 14:39:12 +08:00
|
|
|
statusFilterOptions.value.find((item) => item.value === activeStatusTab.value)?.label || '全部风险'
|
2026-05-24 21:44:17 +08:00
|
|
|
)
|
|
|
|
|
|
2026-06-22 11:58:53 +08:00
|
|
|
const filteredRows = computed(() => filterDocumentRows(activeScopeRows.value, {
|
|
|
|
|
keyword: listKeyword.value,
|
|
|
|
|
showDocumentTypeFilter: showDocumentTypeFilter.value,
|
|
|
|
|
activeDocumentType: activeDocumentType.value,
|
|
|
|
|
activeScene: activeScene.value,
|
|
|
|
|
activeStatusTab: activeStatusTab.value,
|
|
|
|
|
activeScopeTab: activeScopeTab.value,
|
|
|
|
|
appliedStart: appliedStart.value,
|
|
|
|
|
appliedEnd: appliedEnd.value
|
|
|
|
|
}))
|
2026-05-24 21:44:17 +08:00
|
|
|
|
|
|
|
|
const totalPages = computed(() => Math.max(1, Math.ceil(filteredRows.value.length / pageSize.value)))
|
2026-06-03 09:25:23 +08:00
|
|
|
const pageSummary = computed(() => `共 ${filteredRows.value.length} 条,目前第 ${currentPage.value} / ${totalPages.value} 页`)
|
2026-06-04 14:25:14 +08:00
|
|
|
|
|
|
|
|
function resolveLiveDocumentRow(row) {
|
|
|
|
|
return {
|
|
|
|
|
...row,
|
|
|
|
|
isNewDocument: isNewDocument(row, viewedDocumentKeys.value)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-24 21:44:17 +08:00
|
|
|
const visibleRows = computed(() => {
|
|
|
|
|
const start = (currentPage.value - 1) * pageSize.value
|
2026-06-04 14:25:14 +08:00
|
|
|
return filteredRows.value.slice(start, start + pageSize.value).map(resolveLiveDocumentRow)
|
2026-05-24 21:44:17 +08:00
|
|
|
})
|
|
|
|
|
|
2026-05-29 13:17:39 +08:00
|
|
|
const documentLoadingSource = computed(() => (props.loading || supportingLoading.value) && !visibleRows.value.length)
|
|
|
|
|
const visibleDocumentLoading = useMinimumVisibleState(documentLoadingSource, {
|
|
|
|
|
minVisibleMs: DOCUMENT_LOADING_MIN_VISIBLE_MS
|
|
|
|
|
})
|
|
|
|
|
const showLoading = computed(() => visibleDocumentLoading.value)
|
2026-05-24 21:44:17 +08:00
|
|
|
const showError = computed(() => Boolean(props.error) && !visibleRows.value.length)
|
|
|
|
|
const errorMessage = computed(() => props.error || supportingError.value || '单据中心加载失败。')
|
|
|
|
|
const showEmpty = computed(() => !showLoading.value && !showError.value && visibleRows.value.length === 0)
|
|
|
|
|
const showTable = computed(() => !showLoading.value && !showError.value && visibleRows.value.length > 0)
|
|
|
|
|
const showStayTimeColumn = computed(() =>
|
|
|
|
|
[DOCUMENT_SCOPE_APPLICATION, DOCUMENT_SCOPE_REVIEW].includes(activeScopeTab.value)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const documentSummary = computed(() => {
|
2026-05-25 13:35:39 +08:00
|
|
|
const rows = nonArchivedRows.value
|
2026-05-24 21:44:17 +08:00
|
|
|
return {
|
|
|
|
|
total: rows.length,
|
|
|
|
|
toSubmit: rows.filter((row) => ['draft', 'pending_submit'].includes(row.statusGroup)).length,
|
|
|
|
|
toProcess: approvalRows.value.length,
|
|
|
|
|
archived: archiveRows.value.length
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
2026-06-22 11:58:53 +08:00
|
|
|
const emptyState = computed(() => buildDocumentCenterEmptyState({
|
|
|
|
|
hasActiveFilters: hasActiveFilters(),
|
|
|
|
|
activeScopeTab: activeScopeTab.value,
|
|
|
|
|
activeDocumentType: activeDocumentType.value
|
|
|
|
|
}))
|
2026-05-24 21:44:17 +08:00
|
|
|
|
|
|
|
|
function hasActiveFilters() {
|
2026-06-22 11:58:53 +08:00
|
|
|
return hasDocumentCenterActiveFilters({
|
|
|
|
|
listKeyword: listKeyword.value,
|
|
|
|
|
activeStatusTab: activeStatusTab.value,
|
|
|
|
|
showDocumentTypeFilter: showDocumentTypeFilter.value,
|
|
|
|
|
activeDocumentType: activeDocumentType.value,
|
|
|
|
|
activeScene: activeScene.value,
|
|
|
|
|
appliedStart: appliedStart.value,
|
|
|
|
|
appliedEnd: appliedEnd.value
|
|
|
|
|
})
|
2026-05-24 21:44:17 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function toggleFilter(key) {
|
|
|
|
|
openFilterKey.value = openFilterKey.value === key ? '' : key
|
2026-05-27 12:27:17 +08:00
|
|
|
if (openFilterKey.value) {
|
|
|
|
|
datePopover.value = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function toggleDatePopover() {
|
|
|
|
|
datePopover.value = !datePopover.value
|
|
|
|
|
if (datePopover.value) {
|
|
|
|
|
openFilterKey.value = ''
|
|
|
|
|
}
|
2026-05-24 21:44:17 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function selectDocumentType(value) {
|
|
|
|
|
activeDocumentType.value = value
|
|
|
|
|
openFilterKey.value = ''
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function selectScene(value) {
|
|
|
|
|
activeScene.value = value
|
|
|
|
|
openFilterKey.value = ''
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function selectStatusTab(value) {
|
|
|
|
|
activeStatusTab.value = value
|
|
|
|
|
openFilterKey.value = ''
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function applyDateRange() {
|
|
|
|
|
if (!rangeStart.value || !rangeEnd.value) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
appliedStart.value = rangeStart.value
|
|
|
|
|
appliedEnd.value = rangeEnd.value
|
|
|
|
|
datePopover.value = false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function clearDateRange() {
|
|
|
|
|
rangeStart.value = ''
|
|
|
|
|
rangeEnd.value = ''
|
|
|
|
|
appliedStart.value = ''
|
|
|
|
|
appliedEnd.value = ''
|
|
|
|
|
datePopover.value = false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function resetFilters() {
|
|
|
|
|
activeStatusTab.value = '全部'
|
|
|
|
|
activeDocumentType.value = DOCUMENT_TYPE_ALL
|
|
|
|
|
activeScene.value = SCENE_ALL
|
|
|
|
|
listKeyword.value = ''
|
|
|
|
|
clearDateRange()
|
|
|
|
|
openFilterKey.value = ''
|
|
|
|
|
currentPage.value = 1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleEmptyAction() {
|
|
|
|
|
if (activeDocumentType.value === DOCUMENT_TYPE_APPLICATION) {
|
|
|
|
|
emit('create-application')
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (hasActiveFilters()) {
|
|
|
|
|
resetFilters()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
emit('create-request')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function changePageSize(size) {
|
|
|
|
|
pageSize.value = size
|
|
|
|
|
currentPage.value = 1
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-06 17:19:07 +08:00
|
|
|
function applyRemoteViewedDocumentStates(states) {
|
|
|
|
|
viewedDocumentKeys.value = mergeNotificationStatesIntoViewedDocumentKeys(states, viewedDocumentKeys.value)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function loadRemoteViewedDocumentKeys() {
|
|
|
|
|
try {
|
|
|
|
|
applyRemoteViewedDocumentStates(await fetchNotificationStates())
|
|
|
|
|
} catch {
|
|
|
|
|
// 接口不可用时保留本机已读缓存,避免影响单据中心主流程。
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function syncDocumentViewedPatches(patches) {
|
|
|
|
|
const normalizedPatches = (Array.isArray(patches) ? patches : [patches]).filter(Boolean)
|
|
|
|
|
if (!normalizedPatches.length) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
applyRemoteViewedDocumentStates(await patchNotificationStates(normalizedPatches))
|
|
|
|
|
} catch {
|
|
|
|
|
// 本机状态已先落地;远端失败时等待下次操作或刷新重试。
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-24 21:44:17 +08:00
|
|
|
function openDocument(row) {
|
2026-05-25 13:35:39 +08:00
|
|
|
writeDocumentScope(activeScopeTab.value, scopeTabs)
|
2026-06-06 17:19:07 +08:00
|
|
|
const viewedPatch = buildDocumentViewedStatePatch(row)
|
2026-05-25 13:35:39 +08:00
|
|
|
viewedDocumentKeys.value = markDocumentViewed(row, viewedDocumentKeys.value)
|
2026-06-06 17:19:07 +08:00
|
|
|
void syncDocumentViewedPatches([viewedPatch])
|
2026-05-24 21:44:17 +08:00
|
|
|
emit('open-document', row.rawRequest || row)
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-03 16:46:13 +08:00
|
|
|
function markAllDocumentsRead() {
|
|
|
|
|
if (!totalNewDocumentCount.value) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-06 17:19:07 +08:00
|
|
|
const viewedPatches = buildDocumentsViewedStatePatches(allReadableDocumentRows.value, viewedDocumentKeys.value)
|
2026-06-03 16:46:13 +08:00
|
|
|
viewedDocumentKeys.value = markDocumentsViewed(allReadableDocumentRows.value, viewedDocumentKeys.value)
|
2026-06-06 17:19:07 +08:00
|
|
|
void syncDocumentViewedPatches(viewedPatches)
|
2026-06-03 16:46:13 +08:00
|
|
|
}
|
|
|
|
|
|
2026-05-24 21:44:17 +08:00
|
|
|
async function loadSupportingRows() {
|
|
|
|
|
supportingLoading.value = true
|
|
|
|
|
supportingError.value = ''
|
|
|
|
|
|
|
|
|
|
const [approvalResult, archiveResult] = await Promise.allSettled([
|
2026-06-09 08:32:00 +00:00
|
|
|
fetchAllApprovalExpenseClaims(),
|
|
|
|
|
fetchAllArchivedExpenseClaims()
|
2026-05-24 21:44:17 +08:00
|
|
|
])
|
|
|
|
|
|
|
|
|
|
if (approvalResult.status === 'fulfilled') {
|
2026-05-26 09:15:14 +08:00
|
|
|
approvalRows.value = excludeArchivedDocumentRows(
|
2026-06-06 17:19:07 +08:00
|
|
|
extractExpenseClaimItems(approvalResult.value)
|
2026-05-26 09:15:14 +08:00
|
|
|
.map((item) => mapExpenseClaimToRequest(item))
|
2026-06-22 11:58:53 +08:00
|
|
|
.map((item) => buildDocumentRow(item, {
|
|
|
|
|
source: 'approval',
|
|
|
|
|
currentUser: currentUser.value,
|
|
|
|
|
viewedDocumentKeys: viewedDocumentKeys.value
|
|
|
|
|
}))
|
2026-05-26 09:15:14 +08:00
|
|
|
.filter(Boolean)
|
|
|
|
|
)
|
2026-05-24 21:44:17 +08:00
|
|
|
} else {
|
|
|
|
|
approvalRows.value = []
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (archiveResult.status === 'fulfilled') {
|
2026-06-06 17:19:07 +08:00
|
|
|
archiveRows.value = extractExpenseClaimItems(archiveResult.value)
|
|
|
|
|
.map((item) => mapExpenseClaimToRequest(item))
|
2026-06-22 11:58:53 +08:00
|
|
|
.map((item) => buildDocumentRow(item, {
|
|
|
|
|
source: 'archive',
|
|
|
|
|
archived: true,
|
|
|
|
|
currentUser: currentUser.value,
|
|
|
|
|
viewedDocumentKeys: viewedDocumentKeys.value
|
|
|
|
|
}))
|
2026-06-06 17:19:07 +08:00
|
|
|
.filter(Boolean)
|
2026-05-24 21:44:17 +08:00
|
|
|
} else {
|
|
|
|
|
archiveRows.value = []
|
|
|
|
|
supportingError.value = archiveResult.reason instanceof Error
|
|
|
|
|
? archiveResult.reason.message
|
|
|
|
|
: '归档数据加载失败。'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
supportingLoading.value = false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function reloadAll() {
|
|
|
|
|
emit('reload')
|
2026-06-06 17:19:07 +08:00
|
|
|
void loadRemoteViewedDocumentKeys()
|
2026-05-24 21:44:17 +08:00
|
|
|
void loadSupportingRows()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
watch(
|
|
|
|
|
[activeScopeTab, activeStatusTab, activeDocumentType, activeScene, listKeyword, appliedStart, appliedEnd],
|
|
|
|
|
() => {
|
|
|
|
|
currentPage.value = 1
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
2026-06-03 15:46:56 +08:00
|
|
|
watch(
|
|
|
|
|
[currentPage, pageSize, activeScopeTab, activeStatusTab, activeDocumentType, activeScene, listKeyword, appliedStart, appliedEnd],
|
|
|
|
|
() => {
|
|
|
|
|
if (route.name !== 'app-documents') {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const nextQuery = buildDocumentCenterRouteQuery()
|
|
|
|
|
if (!routeQueryEquals(route.query, nextQuery)) {
|
|
|
|
|
router.replace({ name: 'app-documents', query: nextQuery })
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
2026-05-24 21:44:17 +08:00
|
|
|
watch(activeFilterConfig, () => {
|
|
|
|
|
openFilterKey.value = ''
|
|
|
|
|
datePopover.value = false
|
|
|
|
|
|
|
|
|
|
if (!showDocumentTypeFilter.value) {
|
|
|
|
|
activeDocumentType.value = DOCUMENT_TYPE_ALL
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!statusFilterOptions.value.some((item) => item.value === activeStatusTab.value)) {
|
|
|
|
|
activeStatusTab.value = '全部'
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
watch(sceneFilterOptions, (options) => {
|
|
|
|
|
if (!options.some((item) => item.value === activeScene.value)) {
|
|
|
|
|
activeScene.value = SCENE_ALL
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
watch(documentSummary, (summary) => {
|
|
|
|
|
emit('summary-change', summary)
|
|
|
|
|
}, { immediate: true })
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
2026-06-06 17:19:07 +08:00
|
|
|
void loadRemoteViewedDocumentKeys()
|
2026-05-24 21:44:17 +08:00
|
|
|
void loadSupportingRows()
|
|
|
|
|
})
|
2026-06-03 09:25:23 +08:00
|
|
|
|
|
|
|
|
watch(
|
|
|
|
|
() => props.refreshToken,
|
|
|
|
|
(token, previousToken) => {
|
|
|
|
|
if (token && token !== previousToken) {
|
2026-06-06 17:19:07 +08:00
|
|
|
void loadRemoteViewedDocumentKeys()
|
2026-06-03 09:25:23 +08:00
|
|
|
void loadSupportingRows()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
)
|
2026-05-24 21:44:17 +08:00
|
|
|
</script>
|
|
|
|
|
|
2026-05-29 14:51:18 +08:00
|
|
|
<style scoped src="../assets/styles/components/document-list-shared.css"></style>
|
2026-05-24 21:44:17 +08:00
|
|
|
<style scoped src="../assets/styles/views/documents-center-view.css"></style>
|