DESKTOP-6LTVLN7\Liumouren пре 3 недеља
родитељ
комит
bc47aaadb9

+ 1 - 1
public/config/config.js

@@ -74,7 +74,7 @@ window.AppConfig = {
    */
   rolePermissions: {
     village: ['/dashboard', '/base', '/alerts'],
-    town: ['/dashboard', '/approvals', '/reports'],
+    town: ['/dashboard', '/approvals', '/reports', '/alerts'],
     social: [
       '/dashboard',
       '/insurance',

+ 35 - 2
src/api/biz.ts

@@ -112,6 +112,11 @@ export function fetchUnresolvedInsuranceCount(batchId: string) {
   return real.fetchUnresolvedInsuranceCount(batchId)
 }
 
+export function fetchUnresolvedInsuranceCounts(batchIds: string[]) {
+  if (useMock()) return delay({} as Record<string, number>)
+  return real.fetchUnresolvedInsuranceCounts(batchIds)
+}
+
 export function fetchPeople(params?: {
   batchNo?: string
   town?: string
@@ -678,7 +683,10 @@ export function exportInsuranceMatch(params: {
 }
 
 /** 触发居保匹配:产出比对结果清单,批次进入「比对结果已生成」;支持单条或批量 */
-export function triggerInsuranceMatch(batchIds?: string | string[]) {
+export function triggerInsuranceMatch(
+  batchIds?: string | string[],
+  personnelIds?: string | string[],
+) {
   if (useMock()) {
     const ids = batchIds == null ? null : Array.isArray(batchIds) ? batchIds : [batchIds]
     const targets = ids
@@ -689,7 +697,7 @@ export function triggerInsuranceMatch(batchIds?: string | string[]) {
     }
     return delay({ ok: true, count: targets.length, lastSummary: null })
   }
-  return real.triggerInsuranceMatch(batchIds)
+  return real.triggerInsuranceMatch(batchIds, personnelIds)
 }
 
 /** 居保匹配任务执行日志 */
@@ -1419,6 +1427,7 @@ export function fetchVillageMonthlyBatchesPage(params?: {
   town?: string
   pageNum?: number
   pageSize?: number
+  withTownTotals?: boolean
 }) {
   if (useMock()) {
     let list = mockVillageBatches.filter((b) => MONTHLY_LIST_STAGES.includes(b.stage))
@@ -2094,6 +2103,7 @@ export function pageAdminBatches(params?: {
   status?: string
   pageNum?: number
   pageSize?: number
+  withTownTotals?: boolean
 }) {
   if (useMock()) return delay({ list: [], total: 0, pageNum: 1, pageSize: 10 })
   return real.pageAdminBatches(params)
@@ -2176,6 +2186,20 @@ export function fetchRefundStats() {
   return real.fetchRefundStats()
 }
 
+export function fetchReportTopicPage(
+  type: real.ReportTopicType,
+  params?: {
+    townId?: string
+    payMonth?: string
+    bizType?: string
+    pageNum?: number
+    pageSize?: number
+  },
+) {
+  if (useMock()) return delay({ list: [] as Record<string, unknown>[], total: 0 })
+  return real.fetchReportTopicPage(type, params)
+}
+
 export function fetchReportTopic(
   type: real.ReportTopicType,
   params?: { townId?: string; payMonth?: string; bizType?: string },
@@ -2184,6 +2208,15 @@ export function fetchReportTopic(
   return real.fetchReportTopic(type, params)
 }
 
+export function fetchReportPageBootstrap(params?: {
+  payMonth?: string
+  town?: string
+  village?: string
+}) {
+  if (useMock()) return delay(null)
+  return real.fetchReportPageBootstrap(params)
+}
+
 export function exportReportSummary(payMonth?: string) {
   if (useMock()) {
     ElMessage.info('演示模式:请切换联调后导出')

+ 19 - 4
src/api/sjnmtybt/mappers.ts

@@ -434,8 +434,16 @@ export function mapVillageMonthly(raw: Record<string, unknown>): VillageMonthlyB
     peopleCount: b.peopleCount,
     normalAmount: Number(raw.normalAmount || 0),
     extraAmount: Number(raw.supplementAmount || 0),
+    funeralAmount: Number(raw.funeralAmount || 0),
     deductAmount: Number(raw.deductAmount || 0),
-    totalAmount: Number(raw.totalAmount || b.totalAmount || 0),
+    totalAmount: (() => {
+      const normal = Number(raw.normalAmount || 0)
+      const extra = Number(raw.supplementAmount || 0)
+      const funeral = Number(raw.funeralAmount || 0)
+      const deduct = Number(raw.deductAmount || 0)
+      const total = Number(raw.totalAmount || b.totalAmount || 0)
+      return total || normal + extra + funeral - deduct
+    })(),
     status: BATCH_STATUS_TO_MONTHLY[status] || b.stage,
     stage: b.stage,
     submitter: b.submitter,
@@ -445,6 +453,13 @@ export function mapVillageMonthly(raw: Record<string, unknown>): VillageMonthlyB
   }
 }
 
+function parseFailCountFromBatch(raw: Record<string, unknown>): number {
+  const remarks = String(raw.remarks || '')
+  const m = remarks.match(/FAIL_CNT[=|](\d+)/i)
+  if (m) return Number(m[1]) || 0
+  return 0
+}
+
 export function mapDisk(raw: Record<string, unknown>): DiskRecord {
   const status = String(raw.status || '')
   const returnStatusRaw = String(raw.returnStatus || '未回盘')
@@ -475,8 +490,8 @@ export function mapDisk(raw: Record<string, unknown>): DiskRecord {
     amount: Number(raw.totalAmount || 0),
     status: diskStatus,
     returnStatus,
-    failCount: 0,
-    returnFileName: undefined,
+    failCount: Number(raw.failCount ?? parseFailCountFromBatch(raw)) || 0,
+    returnFileName: raw.returnFileName ? String(raw.returnFileName) : undefined,
   }
 }
 
@@ -489,7 +504,7 @@ export function mapReturnFail(raw: Record<string, unknown>, diskId: string): Dis
     village: regionName(String(raw.villageId || '')),
     personName: String(raw.name || ''),
     idCard: String(raw.idNumber || ''),
-    bankCard: undefined,
+    bankCard: raw.bankCardNumber ? String(raw.bankCardNumber) : undefined,
     failReason: String(raw.failReasonDesc || raw.failReasonCode || ''),
     amount: Number(raw.amount ?? raw.actualAmount ?? 0),
     payStatus: raw.success === false ? '待确认' : '已确认发放',

+ 169 - 15
src/api/sjnmtybt/realBiz.ts

@@ -152,6 +152,7 @@ async function buildTownVillageChartsFromApi(townId: string, payMonth: string) {
       townId,
       payMonth,
       batchLevel: 'TOWN',
+      lite: true,
     }),
   )
   const peopleMap = new Map<string, number>()
@@ -295,6 +296,7 @@ async function fetchWorkbenchLegacy(
         townId: townId || undefined,
         villageId: villageId || undefined,
         batchLevel: townId ? 'TOWN' : 'DISTRICT',
+        lite: true,
       },
       { silent: true },
     ).catch(() => null),
@@ -678,6 +680,7 @@ export async function fetchVillageBatches(): Promise<VillageBatch[]> {
     pageNum: 1,
     pageSize: 50,
     batchLevel: 'DISTRICT',
+    lite: true,
   })
   let list = districtPage?.list || []
   if (!list.length) {
@@ -685,6 +688,7 @@ export async function fetchVillageBatches(): Promise<VillageBatch[]> {
       pageNum: 1,
       pageSize: 50,
       batchLevel: 'TOWN',
+      lite: true,
     })
     const towns = townPage?.list || []
     const seen = new Set<string>()
@@ -733,6 +737,12 @@ export type PeoplePageParams = {
   /** 退养资格 WITH_LAND / WITHOUT_LAND / INELIGIBLE / UNSET */
   retireQualifyType?: string
   source?: string
+  /** 排除业务状态,逗号分隔 */
+  bizStatusNotIn?: string
+  /** 仅包含业务状态,逗号分隔 */
+  bizStatusIn?: string
+  /** 非本村视角隐藏他村 DRAFT */
+  hideDraftUnlessVillage?: boolean
   pageNum?: number
   pageSize?: number
 }
@@ -766,6 +776,9 @@ export async function fetchPeoplePage(
         : undefined,
     retireQualifyUnset: params?.retireQualifyType === 'UNSET' ? true : undefined,
     source: params?.source || undefined,
+    bizStatusNotIn: params?.bizStatusNotIn || undefined,
+    bizStatusIn: params?.bizStatusIn || undefined,
+    hideDraftUnlessVillage: params?.hideDraftUnlessVillage ? true : undefined,
     payMonth: params?.batchNo || undefined,
   }, { timeout: 90_000 })
   const list = (res?.list || []).map((p) => mapPerson(p, params?.batchNo || currentPayMonth()))
@@ -1543,6 +1556,7 @@ export async function fetchInsuranceBatchesPage(params?: {
   tab?: 'todo' | 'history'
   batchNo?: string
   stage?: string
+  townId?: string
   pageNum?: number
   pageSize?: number
 }): Promise<{ list: VillageBatch[]; total: number; pageNum: number; pageSize: number }> {
@@ -1554,21 +1568,28 @@ export async function fetchInsuranceBatchesPage(params?: {
     : params?.tab === 'history'
       ? 'PENDING_LEADER_APPROVE,READY_EXPORT,EXPORTED,RETURNED,ARCHIVED'
       : 'PENDING_INSURANCE_MATCH,PENDING_STATUS_MAINTAIN'
+  const townId = params?.townId || undefined
+  const batchLevel = townId ? 'TOWN' : 'DISTRICT'
   const res = await post<PageResult<Record<string, unknown>>>('/batches/page', {
     pageNum,
     pageSize,
-    batchLevel: 'DISTRICT',
+    batchLevel,
+    townId,
     batchNo: params?.batchNo || undefined,
     statusIn: statusIn || undefined,
+    lite: true,
+    withTownTotals: true,
   })
   let list = (res?.list || []).map(mapVillageBatch)
-  if (!list.length && !params?.batchNo) {
+  if (!list.length && !params?.batchNo && !townId) {
     const fallback = await post<PageResult<Record<string, unknown>>>('/batches/page', {
       pageNum,
       pageSize,
       batchLevel: 'TOWN',
       batchNo: params?.batchNo || undefined,
       statusIn: statusIn || undefined,
+      lite: true,
+      withTownTotals: true,
     })
     list = (fallback?.list || []).map(mapVillageBatch)
     return {
@@ -1656,6 +1677,7 @@ export async function fetchInsuranceResultsPage(params?: {
         pageNum,
         pageSize,
       },
+      { timeout: 120_000 },
     )
     const rows = Array.isArray(raw) ? raw : raw?.list || []
     const total = Array.isArray(raw) ? raw.length : Number(raw?.total || rows.length)
@@ -1683,8 +1705,16 @@ export async function fetchInsuranceResults(params?: {
   return page.list
 }
 
-export async function triggerInsuranceMatch(batchIds?: string | string[]) {
+export async function triggerInsuranceMatch(
+  batchIds?: string | string[],
+  personnelIds?: string | string[],
+) {
   const ids = batchIds == null ? [] : Array.isArray(batchIds) ? batchIds : [batchIds]
+  const pids = personnelIds == null
+    ? []
+    : Array.isArray(personnelIds)
+      ? personnelIds.map(String)
+      : [String(personnelIds)]
   let count = 0
   let lastSummary: Record<string, unknown> | null = null
   for (const batchId of ids) {
@@ -1695,7 +1725,11 @@ export async function triggerInsuranceMatch(batchIds?: string | string[]) {
       successCount?: number
       manualCount?: number
       items?: Array<Record<string, unknown>>
-    }>('/insurance-match/trigger', { batchId, operator: operatorOf() })
+    }>('/insurance-match/trigger', {
+      batchId,
+      personnelIds: pids.length ? pids : undefined,
+      operator: operatorOf(),
+    })
     lastSummary = {
       batchId,
       ...(res || {}),
@@ -1760,6 +1794,20 @@ export async function fetchUnresolvedInsuranceCount(batchId: string): Promise<nu
   return Number(res?.count || 0)
 }
 
+/** 批量查询居保未闭环人数(月补贴清单列表用) */
+export async function fetchUnresolvedInsuranceCounts(
+  batchIds: string[],
+): Promise<Record<string, number>> {
+  const ids = (batchIds || []).filter(Boolean)
+  if (!ids.length) return {}
+  const res = await post<Record<string, number>>(
+    '/batches/unresolved-insurance-counts',
+    { batchIds: ids },
+    { silent: true },
+  )
+  return res || {}
+}
+
 export async function exportCompareList(_batchId?: string) {
   return {
     ok: false as const,
@@ -1921,6 +1969,7 @@ export async function fetchApprovals(params?: {
       batchLevel: 'TOWN',
       status: 'PENDING_LEADER_APPROVE',
       batchNo: params?.batchNo || undefined,
+      lite: true,
     })
     const specials = await collectSpecialApprovals({
       townId,
@@ -1963,6 +2012,8 @@ async function pageTownStatRows(opts: {
       batchLevel: 'TOWN',
       status: opts.status,
       batchNo: opts.batchNo,
+      lite: true,
+      withTownTotals: true,
     }),
   )
 }
@@ -1975,6 +2026,8 @@ async function pageDistrictBatchRows(opts?: { status?: string; batchNo?: string
       batchLevel: 'DISTRICT',
       status: opts?.status,
       batchNo: opts?.batchNo,
+      lite: true,
+      withDistrictTotals: true,
     }),
   )
 }
@@ -2092,6 +2145,12 @@ async function collectSpecialApprovals(opts: {
       fileUrls: s.fileUrls ? String(s.fileUrls) : undefined,
       deathDate: deathDate || undefined,
       amount: s.amount != null ? Number(s.amount) : undefined,
+      opinion:
+        statusRaw === '待分管领导审批' || statusRaw === '待审批' || !statusRaw
+          ? undefined
+          : s.opinion
+            ? String(s.opinion)
+            : undefined,
       heirName: s.heirName ? String(s.heirName) : undefined,
       idCard: idCard || undefined,
     })
@@ -2180,6 +2239,7 @@ export async function decideApproval(payload: {
   } else {
     await post('/special-biz/leader-approve', body)
   }
+  clearGetCache()
   return { ok: true }
 }
 
@@ -2843,6 +2903,7 @@ export async function fetchAdjustReissueRoster(params?: {
     pageSize: 20,
     batchLevel: 'DISTRICT',
     batchNo: params?.batchNo,
+    lite: true,
   })
   const batches = batchRes?.list || []
   const adjustBatches = batches.filter((b) => {
@@ -2949,6 +3010,8 @@ export async function fetchVillageMonthlyBatchesPage(params?: {
   town?: string
   pageNum?: number
   pageSize?: number
+  /** 默认 true;列表首屏可传 false 先出表,再异步补全镇人数/金额 */
+  withTownTotals?: boolean
 }): Promise<{ list: VillageMonthlyBatch[]; total: number; pageNum: number; pageSize: number }> {
   await ensureTowns()
   const cfg = getAppConfig()
@@ -2957,6 +3020,7 @@ export async function fetchVillageMonthlyBatchesPage(params?: {
   if (townId) await ensureVillages(townId)
   const pageNum = params?.pageNum || 1
   const pageSize = params?.pageSize || 10
+  const withTownTotals = params?.withTownTotals !== false
   const res = await post<PageResult<Record<string, unknown>>>('/batches/page', {
     pageNum,
     pageSize,
@@ -2964,6 +3028,8 @@ export async function fetchVillageMonthlyBatchesPage(params?: {
     batchLevel: 'TOWN',
     statusIn:
       'PENDING_STATUS_MAINTAIN,PENDING_LEADER_APPROVE,READY_EXPORT,EXPORTED,RETURNED,ARCHIVED',
+    lite: true,
+    withTownTotals,
   })
   const list = (res?.list || []).map(mapVillageMonthly)
   return { list, total: Number(res?.total || list.length), pageNum, pageSize }
@@ -3207,23 +3273,101 @@ export async function fetchRefundStats() {
 
 export type ReportTopicType = 'pay-abnormal' | 'personnel-change' | 'special-biz'
 
-/** 专题报表 topics */
-export async function fetchReportTopic(
+/** 专题报表 topics(分页) */
+export async function fetchReportTopicPage(
   type: ReportTopicType,
-  params?: { townId?: string; payMonth?: string; bizType?: string },
-) {
+  params?: {
+    townId?: string
+    payMonth?: string
+    bizType?: string
+    pageNum?: number
+    pageSize?: number
+  },
+): Promise<{ list: Record<string, unknown>[]; total: number }> {
   const path =
     type === 'pay-abnormal'
       ? '/reports/topics/pay-abnormal'
       : type === 'personnel-change'
         ? '/reports/topics/personnel-change'
         : '/reports/topics/special-biz'
-  const list = await get<Record<string, unknown>[]>(path, {
+  const res = await get<PageResult<Record<string, unknown>>>(path, {
     townId: params?.townId || undefined,
     payMonth: params?.payMonth || undefined,
     bizType: params?.bizType || undefined,
+    pageNum: params?.pageNum || 1,
+    pageSize: params?.pageSize || 20,
   })
-  return Array.isArray(list) ? list : []
+  return {
+    list: Array.isArray(res?.list) ? res.list : [],
+    total: Number(res?.total || 0),
+  }
+}
+
+/** @deprecated use fetchReportTopicPage */
+export async function fetchReportTopic(
+  type: ReportTopicType,
+  params?: { townId?: string; payMonth?: string; bizType?: string },
+) {
+  const page = await fetchReportTopicPage(type, { ...params, pageNum: 1, pageSize: 20 })
+  return page.list
+}
+
+/** 报表页首屏:summary + region-stats,避免拉完整 workbench 扫全库人员 */
+export async function fetchReportPageBootstrap(params?: {
+  payMonth?: string
+  town?: string
+  village?: string
+}): Promise<{
+  personnelCount: number
+  townQualify: WorkbenchApi['townQualify']
+  villageCharts: WorkbenchApi['villageCharts']
+} | null> {
+  await ensureTowns()
+  const cfg = getAppConfig()
+  const role = cfg.roles.find((r) => r.key === cfg.currentRole)
+  const { townId, villageId } = await resolveScopeIds(
+    params?.town || role?.town,
+    params?.village || role?.village,
+  )
+  const payMonth = params?.payMonth || currentPayMonth()
+  const [summary, region] = await Promise.all([
+    get<WorkbenchApi['summary']>('/dashboard/summary', {
+      payMonth,
+      townId: townId || undefined,
+    }).catch(() => null),
+    get<RegionDashRow>('/dashboard/region-stats', {
+      payMonth,
+      townId: townId || undefined,
+      villageId: villageId || undefined,
+      months: 1,
+    }).catch(() => null),
+  ])
+  if (!summary && !region) return null
+  const townQualify = (summary?.townStats || []).map((s) => ({
+    townId: String(s.townId || ''),
+    peopleCount: Number(s.shouldPayCount || 0),
+    villageCount: 0,
+    withLandCount: 0,
+    withoutLandCount: 0,
+    qualifiedCount: 0,
+    amount: s.shouldPayAmount,
+  }))
+  let villageCharts: WorkbenchApi['villageCharts']
+  if (townId && !villageId) {
+    const charts = await buildTownVillageChartsFromApi(townId, payMonth).catch(() => null)
+    if (charts) {
+      villageCharts = charts.people.map((p, idx) => ({
+        villageId: p.name,
+        peopleCount: p.value,
+        amount: charts.amount[idx]?.value || 0,
+      }))
+    }
+  }
+  return {
+    personnelCount: Number(region?.personnelCount || summary?.totalPeople || 0),
+    townQualify,
+    villageCharts,
+  }
 }
 
 /** 月度汇总导出 csv */
@@ -3234,6 +3378,9 @@ export async function exportReportSummary(payMonth?: string) {
     timeout: 120_000,
   })
   const blob = res.data as Blob
+  if (blob.size <= 80) {
+    throw new Error('所选月份暂无批次/发放明细,无法导出月度汇总')
+  }
   let fileName = `monthly-summary-${payMonth || 'all'}.csv`
   const cd = String(res.headers?.['content-disposition'] || '')
   const m = /filename\*=UTF-8''([^;]+)|filename="?([^";]+)"?/i.exec(cd)
@@ -3511,6 +3658,8 @@ export async function fetchDisksPage(params?: {
     townId: townId || undefined,
     batchLevel: 'TOWN',
     statusIn: 'EXPORTED,RETURNED,ARCHIVED',
+    lite: true,
+    withTownTotals: true,
   })
   const filtered = res?.list || []
   if (!townId) {
@@ -3566,16 +3715,15 @@ export async function fetchDiskFailPeoplePage(
   params?: { pageNum?: number; pageSize?: number },
 ): Promise<{ list: DiskFailPerson[]; total: number; pageNum: number; pageSize: number }> {
   const pageNum = params?.pageNum || 1
-  const pageSize = params?.pageSize || 10
+  const pageSize = params?.pageSize || 200
   const res = await post<PageResult<Record<string, unknown>>>('/return-disk/details/page', {
     pageNum,
     pageSize,
     batchId: diskId,
   })
-  const list = (res?.list || [])
-    .filter((d) => d.success === false)
-    .map((d) => mapReturnFail(d, diskId))
-  return { list, total: Number(res?.total || list.length), pageNum, pageSize }
+  const failed = (res?.list || []).filter((d) => d.success === false)
+  const list = failed.map((d) => mapReturnFail(d, diskId))
+  return { list, total: failed.length, pageNum, pageSize }
 }
 
 export async function fetchDiskFailPeople(diskId: string): Promise<DiskFailPerson[]> {
@@ -3718,6 +3866,8 @@ export async function pageAdminBatches(params?: {
   status?: string
   pageNum?: number
   pageSize?: number
+  /** 默认 false;列表首屏先出表,再异步 true 补镇人数/金额 */
+  withTownTotals?: boolean
 }): Promise<{
   list: Array<Record<string, unknown>>
   total: number
@@ -3727,6 +3877,7 @@ export async function pageAdminBatches(params?: {
   await ensureTowns()
   const pageNum = params?.pageNum || 1
   const pageSize = params?.pageSize || 10
+  const withTownTotals = params?.withTownTotals === true
   const res = await post<PageResult<Record<string, unknown>>>('/batches/page', {
     pageNum,
     pageSize,
@@ -3735,6 +3886,9 @@ export async function pageAdminBatches(params?: {
     villageId: params?.villageId || undefined,
     status: params?.status || undefined,
     batchLevel: params?.batchLevel || undefined,
+    lite: true,
+    withTownTotals,
+    withDistrictTotals: withTownTotals,
   })
   const list = (res?.list || [])
     .filter((b) => String(b.batchLevel || '') !== 'VILLAGE')

+ 12 - 24
src/components/biz/BatchDetailDrawer.vue

@@ -24,18 +24,15 @@
             <el-descriptions-item label="人数">
               {{ peopleLoading ? (batch.peopleCount ?? '—') : peopleTotal }}
             </el-descriptions-item>
-            <el-descriptions-item label="合计金额">
-              {{ formatAmount(batch.totalAmount) }}
+            <el-descriptions-item label="应发合计(明细汇总)" :span="2">
+              <strong>{{ formatAmount(peopleAmountSummary.payable) }}</strong>
             </el-descriptions-item>
-            <el-descriptions-item v-if="batch.normalAmount != null" label="正常金额">
+            <el-descriptions-item v-if="batch.normalAmount != null && !isAdjustBatch" label="正常金额">
               {{ formatAmount(batch.normalAmount) }}
             </el-descriptions-item>
-            <el-descriptions-item v-if="batch.supplementAmount != null" label="补发金额">
+            <el-descriptions-item v-if="batch.supplementAmount != null && !isAdjustBatch" label="补发金额">
               {{ formatAmount(batch.supplementAmount) }}
             </el-descriptions-item>
-            <el-descriptions-item label="总发放金额(明细汇总)">
-              <strong>{{ formatAmount(peopleAmountSummary.payable) }}</strong>
-            </el-descriptions-item>
             <el-descriptions-item label="出盘状态">
               {{ batch.exportStatus || '未出盘' }}
             </el-descriptions-item>
@@ -370,7 +367,7 @@ import { actionLabel } from '@/constants/batchActions'
 import { resultLabelOf } from '@/utils/statusLabels'
 import { useServerPagination } from '@/composables/useServerPagination'
 import { guardBusy } from '@/composables/useSubmitLock'
-import { paymentSummaryFromApi, sumBatchMemberAmounts } from '@/utils/batchAmounts'
+import { paymentSummaryFromApi, resolveBatchAmountSummary, sumBatchMemberAmounts } from '@/utils/batchAmounts'
 import { isYearlyAdjustBatchLike } from '@/utils/batchAdjust'
 import { formatAmount } from '@/utils/mask'
 import { useUserStore } from '@/stores/user'
@@ -479,22 +476,13 @@ const isAdjustBatch = computed(() =>
   }),
 )
 
-const peopleAmountSummary = computed(() => {
-  if (paymentSummary.value) return paymentSummary.value
-  const b = batch.value
-  if (b) {
-    return {
-      count: peopleTotal.value || Number(b.peopleCount || 0),
-      monthly: Number(b.normalAmount || 0),
-      oneTime: 0,
-      supplement: Number(b.supplementAmount || 0),
-      funeral: 0,
-      deduct: Number(b.deductAmount || 0),
-      payable: Number(b.totalAmount || 0),
-    }
-  }
-  return sumBatchMemberAmounts(pagedPeople.value, { adjustOnly: isAdjustBatch.value })
-})
+const peopleAmountSummary = computed(() =>
+  resolveBatchAmountSummary(
+    paymentSummary.value,
+    batch.value || undefined,
+    sumBatchMemberAmounts(pagedPeople.value, { adjustOnly: isAdjustBatch.value }),
+  ),
+)
 
 const pageAmountSummary = computed(() =>
   sumBatchMemberAmounts(pagedPeople.value, { adjustOnly: isAdjustBatch.value }),

+ 24 - 0
src/components/biz/PersonPickerDialog.vue

@@ -101,6 +101,8 @@ const props = withDefaults(
     town: string
     /** 不可选的状态文案 */
     excludeStatuses?: string[]
+    /** 仅允许选择的状态(与 excludeStatuses 同时生效时优先) */
+    includeStatuses?: string[]
     /** 不可选的人员 ID */
     excludeIds?: string[]
   }>(),
@@ -108,6 +110,7 @@ const props = withDefaults(
     title: '选择人员',
     tip: '',
     excludeStatuses: () => [],
+    includeStatuses: () => [],
     excludeIds: () => [],
   },
 )
@@ -146,22 +149,43 @@ const {
   resetAndLoad,
 } = useServerPagination<Person>(async (pageNum, pageSize) => {
   if (!props.town) return { list: [], total: 0 }
+  const includeCodes =
+    (props.includeStatuses || []).length > 0
+      ? (props.includeStatuses || [])
+          .map((s) => (s === '死亡丧葬' ? 'FUNERAL' : s))
+          .filter(Boolean)
+          .join(',')
+      : undefined
   return fetchPeoplePage({
     town: props.town,
     village: query.village || undefined,
     name: query.name || undefined,
     idCard: query.idCard || undefined,
+    bizStatusIn: includeCodes || undefined,
     pageNum,
     pageSize,
   })
 })
 
 const excludeStatusSet = () => new Set((props.excludeStatuses || []).map(String))
+const includeStatusSet = () => new Set((props.includeStatuses || []).map(String))
 const excludeIdSet = () => new Set((props.excludeIds || []).map(String))
 
 function canPick(row: Person) {
   if (excludeIdSet().has(String(row.id))) return false
   const st = String(row.status || '')
+  const includes = includeStatusSet()
+  if (includes.size > 0) {
+    let hit = false
+    for (const ok of includes) {
+      if (!ok) continue
+      if (st === ok || st.includes(ok)) {
+        hit = true
+        break
+      }
+    }
+    if (!hit) return false
+  }
   for (const bad of excludeStatusSet()) {
     if (!bad) continue
     if (st === bad || st.includes(bad)) return false

+ 1 - 0
src/types/biz.ts

@@ -237,6 +237,7 @@ export interface VillageMonthlyBatch {
   peopleCount: number
   normalAmount: number
   extraAmount: number
+  funeralAmount?: number
   deductAmount: number
   totalAmount: number
   parentBatchId?: string

+ 1 - 1
src/utils/appConfig.ts

@@ -85,7 +85,7 @@ const fallbackConfig: AppConfig = {
   ],
   rolePermissions: {
     village: ['/dashboard', '/base', '/alerts'],
-    town: ['/dashboard', '/approvals', '/reports'],
+    town: ['/dashboard', '/approvals', '/reports', '/alerts'],
     social: [
       '/dashboard',
       '/insurance',

+ 63 - 0
src/utils/batchAmounts.ts

@@ -70,6 +70,69 @@ export function paymentSummaryFromApi(
   const funeral = Number(raw.funeralAmount ?? 0)
   const deduct = Number(raw.deductAmount ?? 0)
   const payable = Number(raw.totalAmount ?? raw.payableAmount ?? 0)
+  const hasAmountKey =
+    raw.monthlyAmount != null ||
+    raw.oneTimeAmount != null ||
+    raw.supplementAmount != null ||
+    raw.funeralAmount != null ||
+    raw.deductAmount != null ||
+    raw.totalAmount != null ||
+    raw.payableAmount != null
+  // 后端冷缓存仅回 peopleCount 时视为未完成汇总,避免应发合计误显 0
+  if (count && !hasAmountKey) return null
   if (!count && !monthly && !supplement && !funeral && !deduct && !payable) return null
   return { count, monthly, oneTime, supplement, funeral, deduct, payable }
 }
+
+/** 批次头金额字段 → 明细汇总展示(totalAmount 为空时用分项推算) */
+export function batchHeaderAmountSummary(batch: {
+  peopleCount?: number
+  normalAmount?: number
+  extraAmount?: number
+  supplementAmount?: number
+  funeralAmount?: number
+  deductAmount?: number
+  totalAmount?: number
+}): BatchAmountSummary {
+  const monthly = Number(batch.normalAmount ?? 0)
+  const supplement = Number(batch.extraAmount ?? batch.supplementAmount ?? 0)
+  const funeral = Number(batch.funeralAmount ?? 0)
+  const deduct = Number(batch.deductAmount ?? 0)
+  let payable = Number(batch.totalAmount ?? 0)
+  if (!payable) payable = monthly + supplement + funeral - deduct
+  return {
+    count: Number(batch.peopleCount ?? 0),
+    monthly,
+    oneTime: 0,
+    supplement,
+    funeral,
+    deduct,
+    payable,
+  }
+}
+
+/** API 汇总优先;payable 为 0 且批次头有金额时回退批次头 */
+export function resolveBatchAmountSummary(
+  api: BatchAmountSummary | null,
+  batch?: Parameters<typeof batchHeaderAmountSummary>[0] | null,
+  pageFallback?: BatchAmountSummary,
+): BatchAmountSummary {
+  if (api) {
+    const parts = api.monthly + api.supplement + api.funeral - api.deduct
+    if (api.payable > 0 || parts > 0) {
+      return api.payable > 0 ? api : { ...api, payable: parts }
+    }
+    if (batch) {
+      const fromHeader = batchHeaderAmountSummary(batch)
+      if (fromHeader.payable > 0) {
+        return { ...fromHeader, count: api.count || fromHeader.count }
+      }
+    }
+    return api
+  }
+  if (batch) {
+    const fromHeader = batchHeaderAmountSummary(batch)
+    if (fromHeader.payable > 0 || fromHeader.count) return fromHeader
+  }
+  return pageFallback || { count: 0, monthly: 0, oneTime: 0, supplement: 0, funeral: 0, deduct: 0, payable: 0 }
+}

+ 68 - 13
src/views/admin/AdminBatchesView.vue

@@ -54,9 +54,19 @@
           <template #default="{ row }">{{ levelLabel((row as AdminBatch).batchLevel) }}</template>
         </el-table-column>
         <el-table-column prop="town" label="街镇" width="100" />
-        <el-table-column prop="peopleCount" label="人数" width="70" align="center" />
+        <el-table-column prop="peopleCount" label="人数" width="70" align="center">
+          <template #default="{ row }">
+            <span :class="{ 'is-totals-pending': totalsLoading && (row as AdminBatch).batchLevel === 'TOWN' }">
+              {{ formatPeopleCount(row as AdminBatch) }}
+            </span>
+          </template>
+        </el-table-column>
         <el-table-column label="合计" width="110" align="right">
-          <template #default="{ row }">{{ formatAmt(row.totalAmount) }}</template>
+          <template #default="{ row }">
+            <span :class="{ 'is-totals-pending': totalsLoading && (row as AdminBatch).batchLevel === 'TOWN' }">
+              {{ formatAmtCell(row as AdminBatch) }}
+            </span>
+          </template>
         </el-table-column>
         <el-table-column label="状态" width="140" align="center">
           <template #default="{ row }">
@@ -152,10 +162,60 @@ const q = reactive({
 })
 const towns = ref<RegionRow[]>([])
 const generating = ref(false)
+const totalsLoading = ref(false)
+const totalsEnrichGen = ref(0)
 const drawer = ref(false)
 const drawerId = ref('')
 const drawerTab = ref<'detail' | 'flow'>('detail')
 
+function levelLabel(level?: string) {
+  if (level === 'DISTRICT') return '全区'
+  if (level === 'TOWN') return '镇统计'
+  return level || '—'
+}
+
+function formatAmt(n: number) {
+  return Number(n || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
+}
+
+function formatPeopleCount(row: AdminBatch) {
+  if (totalsLoading.value && row.batchLevel === 'TOWN') return '…'
+  return String(row.peopleCount ?? 0)
+}
+
+function formatAmtCell(row: AdminBatch) {
+  if (totalsLoading.value && row.batchLevel === 'TOWN') return '…'
+  return formatAmt(row.totalAmount)
+}
+
+function mergeBatchTotals(enriched: AdminBatch[]) {
+  const map = new Map(enriched.map((r) => [r.id, r]))
+  pagedList.value = pagedList.value.map((row) => {
+    const e = map.get(row.id)
+    if (!e) return row
+    return { ...row, peopleCount: e.peopleCount, totalAmount: e.totalAmount }
+  })
+}
+
+async function enrichBatchTotals(pageNum: number, pageSize: number) {
+  const gen = ++totalsEnrichGen.value
+  totalsLoading.value = true
+  try {
+    const full = await pageAdminBatches({
+      payMonth: q.payMonth || undefined,
+      townId: q.townId || undefined,
+      batchLevel: q.batchLevel || undefined,
+      pageNum,
+      pageSize,
+      withTownTotals: true,
+    }).catch(() => null)
+    if (!full || gen !== totalsEnrichGen.value) return
+    mergeBatchTotals((full.list || []) as AdminBatch[])
+  } finally {
+    if (gen === totalsEnrichGen.value) totalsLoading.value = false
+  }
+}
+
 const {
   page,
   pageSize,
@@ -170,21 +230,12 @@ const {
     batchLevel: q.batchLevel || undefined,
     pageNum,
     pageSize,
+    withTownTotals: false,
   })
+  void enrichBatchTotals(pageNum, pageSize)
   return { list: (res.list || []) as AdminBatch[], total: Number(res.total || 0) }
 })
 
-function levelLabel(level?: string) {
-  if (level === 'DISTRICT') return '全区'
-  if (level === 'TOWN') return '镇统计'
-  return level || '—'
-}
-
-function formatAmt(n: number) {
-  return Number(n || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
-}
-
-
 function reset() {
   q.payMonth = ''
   q.townId = ''
@@ -259,4 +310,8 @@ onMounted(async () => {
 .query-panel {
   margin-bottom: 10px;
 }
+
+.is-totals-pending {
+  color: @text-secondary;
+}
 </style>

+ 26 - 1
src/views/approvals/ApprovalsView.vue

@@ -212,9 +212,16 @@
           {{ formatAmount(currentRow.amount) }}
         </el-descriptions-item>
         <el-descriptions-item label="审批意见" :span="2">
-          {{ currentRow.opinion || '-' }}
+          {{ currentRow.status === '待审批' ? '-' : (currentRow.opinion || '-') }}
         </el-descriptions-item>
         <template v-if="isSpecialApproval(currentRow) || isQualifyApproval(currentRow)">
+          <el-descriptions-item
+            v-if="isAdjustSpecialApproval(currentRow)"
+            label="调标金额变化"
+            :span="2"
+          >
+            {{ adjustSpecialSummary(currentRow) }}
+          </el-descriptions-item>
           <el-descriptions-item v-if="currentRow.personName" label="人员">
             {{ currentRow.personName }}
           </el-descriptions-item>
@@ -754,6 +761,7 @@ import { parseApprovalId } from '@/api/sjnmtybt/mappers'
 import { useServerPagination } from '@/composables/useServerPagination'
 import { useTownVillageOptions } from '@/composables/useTownVillageOptions'
 import { guardBusy } from '@/composables/useSubmitLock'
+import { clearGetCache } from '@/utils/request'
 import { useUserStore } from '@/stores/user'
 import {
   paymentSummaryFromApi,
@@ -876,6 +884,22 @@ function isQualifyApproval(row: Approval | null | undefined): boolean {
   return parsed?.kind === 'qualify' || row.bizType === '退养资格审批'
 }
 
+function isAdjustSpecialApproval(row: Approval | null | undefined): boolean {
+  if (!row) return false
+  return row.bizType.includes('调标')
+}
+
+function adjustSpecialSummary(row: Approval): string {
+  const reason = String(row.reason || '')
+  if (reason.includes('旧标') && reason.includes('新标')) {
+    return reason
+  }
+  if (row.totalAmount != null) {
+    return `补发合计 ${formatAmount(row.totalAmount)} 元(详见申请说明)`
+  }
+  return reason || '—'
+}
+
 function syncRejectFiles(_file: UploadFile, fileList: UploadFile[]) {
   rejectFileList.value = fileList
   rejectRawFiles.value = fileList.map((f) => f.raw).filter((f): f is File => !!f)
@@ -1408,6 +1432,7 @@ async function doDecide(
     )
   }
   detailVisible.value = false
+  clearGetCache()
   await loadApprovals()
 }
 

+ 32 - 2
src/views/insurance/InsuranceMatchView.vue

@@ -275,7 +275,7 @@
           </template>
         </el-table-column>
         <el-table-column prop="remark" label="备注" min-width="140" show-overflow-tooltip />
-        <el-table-column label="操作" width="100" align="center" fixed="right">
+        <el-table-column label="操作" width="180" align="center" fixed="right">
           <template #default="{ row }">
             <el-button
               v-if="!isHistoryTab && row.insuranceStatus !== '匹配通过'"
@@ -286,7 +286,17 @@
             >
               人工确认
             </el-button>
-            <span v-else class="muted">已通过</span>
+            <el-button
+              v-if="!isHistoryTab"
+              link
+              type="warning"
+              size="small"
+              :loading="rematchingId === String(row.id)"
+              @click="onRematchOne(row as Person)"
+            >
+              重新匹配
+            </el-button>
+            <span v-else-if="isHistoryTab && row.insuranceStatus === '匹配通过'" class="muted">已通过</span>
           </template>
         </el-table-column>
       </el-table>
@@ -366,6 +376,7 @@ import {
   resultLabelOf,
 } from '@/utils/statusLabels'
 import type { Person, VillageBatch } from '@/types/biz'
+import { resolveScopeIds } from '@/api/sjnmtybt/regionStore'
 
 const userStore = useUserStore()
 const isAdmin = computed(() => userStore.roleInfo?.key === 'admin')
@@ -394,6 +405,7 @@ const stageOptions = computed(() =>
 
 const resultLoading = computed(() => resultPagerLoading.value)
 const matching = ref(false)
+const rematchingId = ref('')
 const exporting = ref(false)
 const exportingBatchId = ref('')
 const selectedBatches = ref<VillageBatch[]>([])
@@ -485,10 +497,13 @@ const {
   resetAndLoad: loadBatches,
 } = useServerPagination<VillageBatch>(async (pageNum, pageSize) => {
   selectedBatches.value = []
+  const townName = isAdmin.value ? filterTown.value || '' : scopedTownName()
+  const { townId } = await resolveScopeIds(townName || undefined, undefined)
   return fetchInsuranceBatchesPage({
     tab: isHistoryTab.value ? 'history' : 'todo',
     batchNo: filterBatchNo.value.trim() || undefined,
     stage: filterStage.value || undefined,
+    townId: townId || undefined,
     pageNum,
     pageSize,
   })
@@ -655,6 +670,21 @@ async function submitManualConfirm(pass: boolean) {
   }
 }
 
+async function onRematchOne(row: Person) {
+  if (!currentBatch.value?.id) return
+  if (guardBusy(matching)) return
+  rematchingId.value = String(row.id)
+  matching.value = true
+  try {
+    await triggerInsuranceMatch(currentBatch.value.id, String(row.id))
+    ElMessage.success(`${row.name || '该人员'}已重新匹配`)
+    await loadVillageResults(currentBatch.value)
+  } finally {
+    rematchingId.value = ''
+    matching.value = false
+  }
+}
+
 async function onExportBatch(row: VillageBatch) {
   if (!row.id) return
   if (guardBusy(exportingBatchId)) return

+ 77 - 46
src/views/monthly/MonthlyPaymentView.vue

@@ -51,10 +51,13 @@
         <el-table-column label="补发金额" width="100" align="right">
           <template #default="{ row }">{{ formatAmount(row.extraAmount) }}</template>
         </el-table-column>
+        <el-table-column label="丧葬金额" width="100" align="right">
+          <template #default="{ row }">{{ formatAmount(row.funeralAmount) }}</template>
+        </el-table-column>
         <el-table-column label="扣减金额" width="100" align="right">
           <template #default="{ row }">{{ formatAmount(row.deductAmount) }}</template>
         </el-table-column>
-        <el-table-column label="合计金额" width="110" align="right">
+        <el-table-column label="应发合计" width="110" align="right">
           <template #default="{ row }">{{ formatAmount(row.totalAmount) }}</template>
         </el-table-column>
         <el-table-column label="状态" width="100" align="center">
@@ -205,13 +208,10 @@
           <el-descriptions-item label="补发金额">
             {{ formatAmount(currentRow.extraAmount) }}
           </el-descriptions-item>
-          <el-descriptions-item label="扣减金额">
+            <el-descriptions-item label="扣减金额">
             {{ formatAmount(currentRow.deductAmount) }}
           </el-descriptions-item>
-          <el-descriptions-item label="合计金额(批次头)">
-            {{ formatAmount(currentRow.totalAmount) }}
-          </el-descriptions-item>
-          <el-descriptions-item label="总发放金额(明细汇总)" :span="2">
+          <el-descriptions-item label="应发合计(明细汇总)" :span="2">
             <strong>{{ formatAmount(detailAmountSummary.payable) }}</strong>
             <span class="muted" style="margin-left: 8px">
               <template v-if="isAdjustDetailBatch">(调标差额批:应发=补发 {{ formatAmount(detailAmountSummary.supplement) }})</template>
@@ -225,7 +225,7 @@
           </el-descriptions-item>
         </el-descriptions>
         <p class="monthly-payment-view__sub-tip">
-          总发放按本批发放明细实时汇总;含停止后剔除、补发/丧葬等变动。分列见下表或点「详情」
+          应发合计按本批发放明细实时汇总(标准+补发+丧葬−扣减);含停止后剔除、补发/丧葬等变动
           <SensitiveRevealSwitch v-model="revealSensitive" style="margin-left: 12px" />
         </p>
         <el-table
@@ -418,6 +418,7 @@ import {
   fetchVillageMonthlyBatchesPage,
   fetchPendingSpecialCount,
   fetchUnresolvedInsuranceCount,
+  fetchUnresolvedInsuranceCounts,
   issueVillageMonthlyDisk,
   listCalcRounds,
   lockCalc,
@@ -428,7 +429,7 @@ import { useServerPagination } from '@/composables/useServerPagination'
 import { guardBusy, runSubmitId } from '@/composables/useSubmitLock'
 import { useTownVillageOptions } from '@/composables/useTownVillageOptions'
 import { useUserStore } from '@/stores/user'
-import { paymentSummaryFromApi, sumBatchMemberAmounts } from '@/utils/batchAmounts'
+import { paymentSummaryFromApi, resolveBatchAmountSummary, sumBatchMemberAmounts } from '@/utils/batchAmounts'
 import { isYearlyAdjustBatchLike } from '@/utils/batchAdjust'
 import { formatAmount } from '@/utils/mask'
 import SensitiveValue from '@/components/common/SensitiveValue.vue'
@@ -466,22 +467,13 @@ const isAdjustDetailBatch = computed(() =>
   }),
 )
 
-const detailAmountSummary = computed(() => {
-  if (detailPaymentSummary.value) return detailPaymentSummary.value
-  const row = currentRow.value
-  if (row) {
-    return {
-      count: Number(row.peopleCount || 0),
-      monthly: Number(row.normalAmount || 0),
-      oneTime: 0,
-      supplement: Number(row.extraAmount || 0),
-      funeral: 0,
-      deduct: Number(row.deductAmount || 0),
-      payable: Number(row.totalAmount || 0),
-    }
-  }
-  return sumBatchMemberAmounts(pagedDetailPeople.value, { adjustOnly: isAdjustDetailBatch.value })
-})
+const detailAmountSummary = computed(() =>
+  resolveBatchAmountSummary(
+    detailPaymentSummary.value,
+    currentRow.value || undefined,
+    sumBatchMemberAmounts(pagedDetailPeople.value, { adjustOnly: isAdjustDetailBatch.value }),
+  ),
+)
 
 const detailPageAmountSummary = computed(() =>
   sumBatchMemberAmounts(pagedDetailPeople.value, { adjustOnly: isAdjustDetailBatch.value }),
@@ -532,6 +524,45 @@ function openBatchDrawer(row: VillageMonthlyBatch, tab: 'detail' | 'flow' = 'det
   batchDrawerVisible.value = true
 }
 
+const totalsEnrichGen = ref(0)
+
+function resolveListTown() {
+  return isAdmin.value
+    ? filterTown.value || undefined
+    : userStore.roleInfo?.allData
+      ? undefined
+      : userStore.roleInfo?.town
+}
+
+function mergeTownTotals(enriched: VillageMonthlyBatch[]) {
+  const map = new Map(enriched.map((r) => [r.id, r]))
+  list.value = list.value.map((row) => {
+    const e = map.get(row.id)
+    if (!e) return row
+    return {
+      ...row,
+      peopleCount: e.peopleCount,
+      normalAmount: e.normalAmount,
+      extraAmount: e.extraAmount,
+      deductAmount: e.deductAmount,
+      totalAmount: e.totalAmount,
+    }
+  })
+}
+
+async function enrichTownTotals(pageNum: number, pageSize: number) {
+  const gen = ++totalsEnrichGen.value
+  const town = resolveListTown()
+  const full = await fetchVillageMonthlyBatchesPage({
+    town,
+    pageNum,
+    pageSize,
+    withTownTotals: true,
+  }).catch(() => null)
+  if (!full || gen !== totalsEnrichGen.value) return
+  mergeTownTotals(full.list)
+}
+
 const {
   page,
   pageSize,
@@ -540,12 +571,18 @@ const {
   loading,
   resetAndLoad: loadList,
 } = useServerPagination<VillageMonthlyBatch>(async (pageNum, pageSize) => {
-  const town = isAdmin.value
-    ? filterTown.value || undefined
-    : userStore.roleInfo?.allData
-      ? undefined
-      : userStore.roleInfo?.town
-  return fetchVillageMonthlyBatchesPage({ town, pageNum, pageSize })
+  const town = resolveListTown()
+  const needAsyncTotals = isAdmin.value && !filterTown.value
+  const page = await fetchVillageMonthlyBatchesPage({
+    town,
+    pageNum,
+    pageSize,
+    withTownTotals: !needAsyncTotals,
+  })
+  if (needAsyncTotals) {
+    void enrichTownTotals(pageNum, pageSize)
+  }
+  return page
 })
 const list = pagedList
 
@@ -784,24 +821,18 @@ async function onToggleCalcLock(row: VillageMonthlyBatch) {
   })
 }
 
-async function loadData() {
-  await loadList()
+async function loadPendingMeta() {
   const town = userStore.roleInfo?.allData ? undefined : userStore.roleInfo?.town
   pendingSpecialCount.value = await fetchPendingSpecialCount(town).catch(() => 0)
-  const nextIns: Record<string, number> = { ...unresolvedInsuranceMap.value }
-  await Promise.all(
-    list.value
-      .filter((row) => row.status === '待提交' && isCurrentBatch(row))
-      .map(async (row) => {
-        nextIns[row.id] = await fetchUnresolvedInsuranceCount(row.id).catch(() => nextIns[row.id] || 0)
-      }),
-  )
-  unresolvedInsuranceMap.value = nextIns
-  for (const row of list.value) {
-    if (['待提交', '待审核', '已审核'].includes(row.status)) {
-      void refreshCalcLocked(row.id)
-    }
-  }
+  const targets = list.value.filter((row) => row.status === '待提交' && isCurrentBatch(row))
+  if (!targets.length) return
+  const counts = await fetchUnresolvedInsuranceCounts(targets.map((row) => row.id)).catch(() => ({}))
+  unresolvedInsuranceMap.value = { ...unresolvedInsuranceMap.value, ...counts }
+}
+
+async function loadData() {
+  await loadList()
+  void loadPendingMeta()
 }
 
 onMounted(loadData)

+ 69 - 59
src/views/reports/ReportsView.vue

@@ -5,7 +5,7 @@
       <div class="reports-view__actions">
         <el-button type="primary" :loading="exporting" @click="onExportSummary">导出月度汇总</el-button>
         <el-button @click="onPrint">打印本页</el-button>
-        <el-button :loading="topicLoading" @click="loadTopics">刷新专题</el-button>
+        <el-button :loading="topicLoading" @click="resetTopicPage">刷新专题</el-button>
         <el-button v-if="isDistrict" :loading="yoyLoading" @click="loadYoy">刷新同比环比</el-button>
       </div>
     </div>
@@ -40,6 +40,11 @@
           show-overflow-tooltip
         />
       </el-table>
+      <TablePagination
+        v-model:page="topicPage"
+        v-model:page-size="topicPageSize"
+        :total="topicTotal"
+      />
     </PagePanel>
 
     <!-- 顶部统计(镇经发 / 镇社区事务:本镇下属各村合计) -->
@@ -716,8 +721,8 @@ import {
   fetchAlertsPage,
   fetchPeoplePage,
   fetchReportBatches,
-  fetchReportTopic,
-  fetchWorkbenchStats,
+  fetchReportPageBootstrap,
+  fetchReportTopicPage,
   fetchYoy,
 } from '@/api/biz'
 import { useClientPagination } from '@/composables/useClientPagination'
@@ -730,7 +735,7 @@ import { maskIdCard, maskBankCard, formatAmount } from '@/utils/mask'
 import { splitByShare } from '@/utils/fundShare'
 import type { AlertItem, Person, VillageBatch } from '@/types/biz'
 import type { ReportTopicType } from '@/api/sjnmtybt/realBiz'
-import { regionName } from '@/api/sjnmtybt/regionStore'
+import { regionName, resolveScopeIds } from '@/api/sjnmtybt/regionStore'
 import {
   displayBizText,
   personnelStatusLabel,
@@ -762,10 +767,8 @@ const peopleLoading = ref(false)
 /** 首次进入 / 切角色:整页 loading(顶部汇总此前未挂 loading) */
 const pageLoading = ref(false)
 const exporting = ref(false)
-const topicLoading = ref(false)
 const yoyLoading = ref(false)
 const topicTab = ref<ReportTopicType>('pay-abnormal')
-const topicRows = ref<Record<string, unknown>[]>([])
 const yoyMeta = reactive({
   payMonth: '',
   prevMonth: '',
@@ -1216,6 +1219,7 @@ const {
       bizStatus: peopleStatusToCode(peopleQuery.status) || undefined,
       source: peopleQuery.source || undefined,
       insuranceMatchResult: insuranceToCode(peopleQuery.insuranceStatus) || undefined,
+      hideDraftUnlessVillage: !peopleQueryVillage(),
       retireQualifyType:
         peopleQuery.retireQualifyType === '有土地退养'
           ? 'WITH_LAND'
@@ -1397,6 +1401,35 @@ function mapTopicRows(type: ReportTopicType, rows: Record<string, unknown>[]) {
   })
 }
 
+const {
+  page: topicPage,
+  pageSize: topicPageSize,
+  total: topicTotal,
+  list: topicRows,
+  loading: topicLoading,
+  resetAndLoad: resetTopicPage,
+} = useServerPagination<Record<string, unknown>>(
+  async (pageNum, pageSize) => {
+    const payMonth = (userStore.currentMonth || '').replace('-', '')
+    const { townId } = await resolveScopeIds(lockedTown.value || undefined, undefined)
+    const res = await fetchReportTopicPage(topicTab.value, {
+      payMonth: payMonth || undefined,
+      townId: townId || undefined,
+      pageNum,
+      pageSize,
+    })
+    return {
+      list: mapTopicRows(topicTab.value, res.list),
+      total: res.total,
+    }
+  },
+  { pageSize: 20 },
+)
+
+function onTopicTabChange() {
+  void resetTopicPage()
+}
+
 const yoyCards = computed(() => {
   const r = yoyRaw.value
   if (!r || !Object.keys(r).length) return []
@@ -1436,26 +1469,42 @@ const yoyCards = computed(() => {
   ]
 })
 
-async function loadTopics() {
-  topicLoading.value = true
+async function loadAll() {
+  pageLoading.value = true
+  peopleLoading.value = true
   try {
+    const role = userStore.roleInfo
     const payMonth = (userStore.currentMonth || '').replace('-', '')
-    const raw = await fetchReportTopic(topicTab.value, {
-      payMonth: payMonth || undefined,
-      townId: lockedTown.value || undefined,
-    })
-    topicRows.value = mapTopicRows(topicTab.value, raw || [])
-  } catch {
-    topicRows.value = []
+    const [batchData, alertPage, bootstrap] = await Promise.all([
+      fetchReportBatches(),
+      fetchAlertsPage({
+        pageNum: 1,
+        pageSize: 20,
+        town: lockedTown.value || undefined,
+        village: lockedVillage.value || undefined,
+      }),
+      fetchReportPageBootstrap({
+        payMonth: payMonth || undefined,
+        town: lockedTown.value || undefined,
+        village: lockedVillage.value || undefined,
+      }),
+    ])
+    batches.value = role ? filterByRoleScope(batchData, role) : batchData
+    alerts.value = role ? filterByRoleScope(alertPage.list, role) : alertPage.list
+    alertTotalCount.value = Number(alertPage.total || 0)
+    peopleHeadcount.value = Number(bootstrap?.personnelCount || 0)
+    townQualifyRows.value = bootstrap?.townQualify || []
+    villageChartRows.value = bootstrap?.villageCharts || []
+    resetTownSummaryPage()
+    resetVillageSummaryPage()
+    resetBatchPage()
+    await Promise.all([resetTopicPage(), resetPeoplePage()])
   } finally {
-    topicLoading.value = false
+    peopleLoading.value = false
+    pageLoading.value = false
   }
 }
 
-function onTopicTabChange() {
-  void loadTopics()
-}
-
 async function loadYoy() {
   if (!isDistrict.value) return
   yoyLoading.value = true
@@ -1493,45 +1542,6 @@ function onPrint() {
   window.print()
 }
 
-async function loadAll() {
-  pageLoading.value = true
-  peopleLoading.value = true
-  topicLoading.value = true
-  try {
-    const role = userStore.roleInfo
-    const payMonth = (userStore.currentMonth || '').replace('-', '')
-    const [batchData, alertPage, wb] = await Promise.all([
-      fetchReportBatches(),
-      fetchAlertsPage({
-        pageNum: 1,
-        pageSize: 20,
-        town: lockedTown.value || undefined,
-        village: lockedVillage.value || undefined,
-      }),
-      fetchWorkbenchStats({
-        payMonth: payMonth || undefined,
-        town: lockedTown.value || undefined,
-        village: lockedVillage.value || undefined,
-        months: 1,
-        roleKey: role?.key,
-      }),
-    ])
-    batches.value = role ? filterByRoleScope(batchData, role) : batchData
-    alerts.value = role ? filterByRoleScope(alertPage.list, role) : alertPage.list
-    alertTotalCount.value = Number(alertPage.total || 0)
-    peopleHeadcount.value = Number(wb?.personnelCount || 0)
-    townQualifyRows.value = wb?.townQualify || []
-    villageChartRows.value = wb?.villageCharts || []
-    resetTownSummaryPage()
-    resetVillageSummaryPage()
-    resetBatchPage()
-    await Promise.all([loadTopics(), loadYoy(), resetPeoplePage()])
-  } finally {
-    peopleLoading.value = false
-    pageLoading.value = false
-  }
-}
-
 onMounted(loadAll)
 
 watch(

+ 10 - 1
src/views/special/SpecialBusinessView.vue

@@ -852,6 +852,7 @@
       :tip="personPickerTip"
       :town="personPickerTown"
       :exclude-statuses="personPickerExcludeStatuses"
+      :include-statuses="personPickerIncludeStatuses"
       :exclude-ids="personPickerExcludeIds"
       @select="onPersonPicked"
     />
@@ -1121,6 +1122,7 @@ const {
     village: activeVillage.value || undefined,
     name: statusSearchName.value.trim() || undefined,
     idCard: statusSearchIdCard.value.trim() || undefined,
+    bizStatusNotIn: 'DRAFT,PENDING_TOWN_APPROVE,NEW',
     pageNum,
     pageSize,
   })
@@ -1257,6 +1259,13 @@ const personPickerExcludeStatuses = computed(() => {
   return ['已归档', '无资格']
 })
 
+const personPickerIncludeStatuses = computed(() => {
+  if (personPickerMode.value === 'funeral') {
+    return ['死亡丧葬', 'FUNERAL']
+  }
+  return [] as string[]
+})
+
 const personPickerExcludeIds = computed(() => {
   if (personPickerMode.value === 'death') return [...pendingDeathPersonIds.value]
   return [] as string[]
@@ -1935,7 +1944,7 @@ async function onTriggerAdjustReissue() {
   }
   try {
     await ElMessageBox.confirm(
-      `确认发起本镇(${town || '全区'})调标补发申请?差额将挂到当年/当月全区批次补发明细,不再新建村批。`,
+      `确认发起本镇(${town || '全区'})调标补发申请?审批通过后将按村生成业务号 YYYY99 差额专批,次月单独出盘,不影响当月全区月批。`,
       '发起调标补发申请',
       { type: 'warning', confirmButtonText: '确认发起', cancelButtonText: '取消' },
     )