| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875 |
- <template>
- <div
- class="dashboard-view page-stack"
- :class="{
- 'dashboard-view--village': isVillage || isTown || isSocial || isLeader,
- 'dashboard-view--district': isDistrictPolicy,
- }"
- v-loading="loading"
- >
- <!-- 核心指标:在册总人数 ≠ 本月发放人数 -->
- <StatCards :items="statItems" />
- <p class="dashboard-stat-note">
- <strong>在册总人数</strong>是当前角色范围内的人员档案底数(含暂停、死亡等仍在库人员),
- <strong>不是</strong>本月实际发放人数。
- 本月发放人数与金额按当前发放周期的月补清单口径统计,见下方对照表。
- </p>
- <!-- 5.1.1.1 近期告警滚动;点击跳转告警页 -->
- <section class="alert-ticker" aria-label="近期告警">
- <div class="alert-ticker__label">
- <el-icon><Bell /></el-icon>
- <span>近期告警</span>
- <em v-if="alertTickerItems.length">未关闭 {{ alertTickerItems.length }}</em>
- </div>
- <div class="alert-ticker__track-wrap" @mouseenter="tickerPaused = true" @mouseleave="tickerPaused = false">
- <div
- v-if="alertTickerItems.length"
- class="alert-ticker__track"
- :class="{ 'is-paused': tickerPaused }"
- :style="{ animationDuration: `${Math.max(12, alertTickerItems.length * 4)}s` }"
- >
- <button
- v-for="(a, idx) in alertTickerLoop"
- :key="`${a.id}-${idx}`"
- type="button"
- class="alert-ticker__item"
- @click="goAlert(a.id)"
- >
- <StatusTag :status="a.level" mode="level" />
- <span class="alert-ticker__type">{{ a.type || '告警' }}</span>
- <span class="alert-ticker__text">
- {{ a.personName ? `${a.personName}:` : '' }}{{ a.content || '—' }}
- </span>
- </button>
- </div>
- <div v-else class="alert-ticker__empty">暂无未关闭告警</div>
- </div>
- <el-button link type="primary" size="small" class="alert-ticker__more" @click="goAlert()">
- 全部告警
- </el-button>
- </section>
- <!-- 本批次发放概览 -->
- <section class="batch-overview">
- <div class="batch-overview__head">
- <div class="batch-overview__title">
- <span class="batch-overview__bar" />
- <div>
- <h3>{{ overviewTitle }}</h3>
- <p>
- <template v-if="isVillage || isTown">
- 统计范围
- <em>{{ data?.batchInfo.scope || '-' }}</em>
- · {{ roleTip }}
- </template>
- <template v-else>
- 全区月批 {{ data?.batchInfo.batchNo || '-' }} · 统计范围
- <em>{{ data?.batchInfo.scope || '-' }}</em>
- · {{ roleTip }}
- </template>
- </p>
- </div>
- </div>
- <div class="batch-overview__period">
- <span class="batch-overview__period-label">发放周期</span>
- <el-date-picker
- :model-value="userStore.currentMonth"
- type="month"
- value-format="YYYY-MM"
- format="YYYY-MM"
- size="small"
- placeholder="选择月份"
- :clearable="false"
- class="batch-overview__period-picker"
- @update:model-value="onMonthChange"
- />
- </div>
- </div>
- <div class="batch-overview__metrics">
- <div
- v-for="item in batchMetrics"
- :key="item.label"
- class="metric-item"
- :class="item.tone"
- >
- <div class="metric-item__icon">
- <el-icon><component :is="item.icon" /></el-icon>
- </div>
- <div class="metric-item__body">
- <div class="metric-item__label">{{ item.label }}</div>
- <div class="metric-item__value">
- {{ item.value }}
- <em v-if="item.unit">{{ item.unit }}</em>
- </div>
- </div>
- </div>
- </div>
- </section>
- <!-- 待办单独一行,放在状态占比/发放表之上 -->
- <PagePanel
- v-if="(data?.todos?.length ?? 0) > 0"
- title="待办事项"
- class="dashboard-todos-panel"
- >
- <template #extra>
- <span class="dashboard-todos-panel__count">
- 未读 {{ unreadTodoCount }} / 共 {{ data?.todos?.length ?? 0 }}
- </span>
- </template>
- <el-table
- :data="pagedTodos"
- border
- stripe
- size="small"
- class="dashboard-todos-table"
- @row-click="(row) => onTodoClick(row as TodoItem)"
- >
- <el-table-column prop="type" label="类型" width="120" show-overflow-tooltip />
- <el-table-column prop="title" label="事项" min-width="220" show-overflow-tooltip>
- <template #default="{ row }">
- <span :class="{ 'is-todo-read': (row as TodoItem).read }">{{ (row as TodoItem).title }}</span>
- </template>
- </el-table-column>
- <el-table-column prop="node" label="当前节点" width="150" show-overflow-tooltip />
- <el-table-column label="状态" width="80" align="center">
- <template #default="{ row }">
- <el-tag size="small" :type="(row as TodoItem).read ? 'info' : 'warning'" effect="plain">
- {{ (row as TodoItem).read ? '已读' : '未读' }}
- </el-tag>
- </template>
- </el-table-column>
- <el-table-column label="操作" width="100" align="center">
- <template #default="{ row }">
- <el-button
- v-if="canHandleTodo(row as TodoItem)"
- link
- type="primary"
- size="small"
- @click.stop="onTodoClick(row as TodoItem)"
- >
- 办理
- </el-button>
- <span v-else class="text-muted">—</span>
- </template>
- </el-table-column>
- </el-table>
- <TablePagination
- v-if="todoTotal > todoPageSize"
- v-model:page="todoPage"
- v-model:page-size="todoPageSize"
- :total="todoTotal"
- :page-sizes="TODO_PAGE_SIZES"
- />
- </PagePanel>
- <!-- 村 / 镇:发放表与状态饼图并排;待办已在上方单独一行 -->
- <div v-if="isTownLevelBoard" class="village-workspace">
- <div class="village-workspace__main" :class="{ 'has-pie': sidePieItems.length > 0 }">
- <PieMixPanel
- v-if="sidePieItems.length > 0"
- class="village-workspace__pie"
- :title="sidePieTitle"
- :items="sidePieItems"
- />
- <PagePanel :title="payTableTitle" class="village-workspace__table-panel village-workspace__pay">
- <el-table
- v-loading="payTableLoading"
- :data="payTableRows"
- border
- stripe
- size="small"
- max-height="300"
- >
- <el-table-column prop="name" :label="payTableNameLabel" min-width="110" />
- <el-table-column label="发放人数" width="100" align="right">
- <template #default="{ row }">{{ formatHeadcount(row.people) }}</template>
- </el-table-column>
- <el-table-column label="发放金额(元)" min-width="120" align="right">
- <template #default="{ row }">{{ formatAmount(row.amount) }}</template>
- </el-table-column>
- </el-table>
- </PagePanel>
- </div>
- <div class="village-workspace__trend">
- <PagePanel title="月度发放人数与金额">
- <div class="village-workspace__chart-host">
- <ChartBox :option="townMonthPayCombineOption" height="100%" />
- </div>
- </PagePanel>
- <PagePanel title="年度发放人数与金额">
- <div class="village-workspace__chart-host">
- <ChartBox :option="townYearPayCombineOption" height="100%" />
- </div>
- </PagePanel>
- </div>
- <WorkbenchTipsPanel
- :title="workspaceTipsTitle"
- :tips="workspaceTips"
- :reset-key="tipsResetKey"
- />
- </div>
- <!-- 区农委 / 区人社局:全区各镇各村监测(无流程待办,图表丰富) -->
- <div v-else-if="isDistrictPolicy" class="district-workspace">
- <div class="district-workspace__charts">
- <!-- 农委侧重资格;人社侧重金额;共用人数图 -->
- <PagePanel :title="isAgri ? '各街镇有土地退养人数' : '各街镇发放人数与金额'">
- <div class="district-workspace__chart-host">
- <ChartBox
- :option="isAgri ? districtWithLandOption : districtTownPayCombineOption"
- height="100%"
- />
- </div>
- </PagePanel>
- <PagePanel :title="isAgri ? '各街镇无土地有资格人数' : '近半年发放人数与金额'">
- <div class="district-workspace__chart-host">
- <ChartBox
- :option="isAgri ? districtWithoutLandOption : districtHalfYearPayCombineOption"
- height="100%"
- />
- </div>
- </PagePanel>
- <PagePanel :title="isAgri ? '本月退养资格结构' : '近半年发放金额'">
- <div class="district-workspace__chart-host">
- <ChartBox
- :option="isAgri ? districtQualifyPieOption : districtHalfYearAmountOption"
- height="100%"
- />
- </div>
- </PagePanel>
- <PagePanel :title="isAgri ? '各街镇补助人数' : '近半年各批次发放人数'">
- <div class="district-workspace__chart-host">
- <ChartBox
- :option="isAgri ? districtTownPeopleOption : districtHalfYearPeopleOption"
- height="100%"
- />
- </div>
- </PagePanel>
- <PagePanel title="本月业务状态占比">
- <div class="district-workspace__chart-host">
- <ChartBox :option="statusPieMonthOption" height="100%" />
- </div>
- </PagePanel>
- <PagePanel :title="isAgri ? '近半年发放人数' : '本年度业务状态占比'">
- <div class="district-workspace__chart-host">
- <ChartBox
- :option="isAgri ? districtHalfYearPeopleOption : statusPieYearOption"
- height="100%"
- />
- </div>
- </PagePanel>
- </div>
- <PagePanel title="全区各街镇土地退养概况" class="district-overview-panel">
- <el-table
- class="district-overview-table"
- :data="data?.districtOverview?.townSummary ?? []"
- border
- stripe
- size="small"
- table-layout="fixed"
- >
- <el-table-column prop="town" label="街镇" min-width="100" align="center" />
- <el-table-column
- prop="villageCount"
- label="涉及村数"
- min-width="88"
- align="center"
- header-align="center"
- />
- <el-table-column
- prop="peopleCount"
- label="发放人数"
- min-width="88"
- align="center"
- header-align="center"
- />
- <el-table-column
- label="发放金额(元)"
- min-width="120"
- align="right"
- header-align="center"
- >
- <template #default="{ row }">{{ formatAmount(row.amount) }}</template>
- </el-table-column>
- <el-table-column
- label="人均(元)"
- min-width="88"
- align="right"
- header-align="center"
- >
- <template #default="{ row }">{{ formatPerCapita(row.amount, row.peopleCount) }}</template>
- </el-table-column>
- <el-table-column
- prop="withLandCount"
- label="有土地退养"
- min-width="100"
- align="center"
- header-align="center"
- />
- <el-table-column
- prop="withoutLandCount"
- label="无土地有资格"
- min-width="110"
- align="center"
- header-align="center"
- />
- <el-table-column
- v-if="isAgri"
- prop="qualifiedCount"
- label="资格已核验/标注"
- min-width="120"
- align="center"
- header-align="center"
- />
- </el-table>
- </PagePanel>
- <WorkbenchTipsPanel
- :title="districtTipsTitle"
- :tips="districtTips"
- :reset-key="tipsResetKey"
- />
- </div>
- <!-- 其他角色:图表 + 待办告警 -->
- <template v-else>
- <div class="dashboard-view__charts">
- <PagePanel :title="data?.barChartTitle || '发放金额统计'">
- <ChartBox :option="roleBarOption" height="300px" />
- </PagePanel>
- <PagePanel title="近半年发放金额对比">
- <ChartBox :option="batchCompareOption" height="300px" />
- </PagePanel>
- <PagePanel title="本月业务状态占比">
- <ChartBox :option="statusPieMonthOption" height="300px" />
- </PagePanel>
- <PagePanel title="本年度业务状态占比">
- <ChartBox :option="statusPieYearOption" height="300px" />
- </PagePanel>
- </div>
- <div class="dashboard-view__tables">
- <PagePanel title="近期告警">
- <template #extra>
- <span class="dashboard-todos-panel__count">共 {{ alertTotal }} 条</span>
- </template>
- <el-table :data="pagedAlerts" border stripe size="small">
- <el-table-column prop="level" label="等级" width="72" align="center">
- <template #default="{ row }">
- <StatusTag :status="row.level" mode="level" />
- </template>
- </el-table-column>
- <el-table-column prop="personName" label="人员" width="90" />
- <el-table-column prop="type" label="类型" width="110" show-overflow-tooltip />
- <el-table-column prop="content" label="内容" min-width="180" show-overflow-tooltip />
- </el-table>
- <TablePagination
- v-if="alertTotal > alertPageSize"
- v-model:page="alertPage"
- v-model:page-size="alertPageSize"
- :total="alertTotal"
- :page-sizes="TODO_PAGE_SIZES"
- />
- </PagePanel>
- </div>
- </template>
- </div>
- </template>
- <script setup lang="ts">
- import { computed, onMounted, onUnmounted, ref, shallowRef, watch } from 'vue'
- import { useRouter } from 'vue-router'
- import { Bell } from '@element-plus/icons-vue'
- import type { EChartsCoreOption } from 'echarts/core'
- import StatCards from '@/components/common/StatCards.vue'
- import PagePanel from '@/components/common/PagePanel.vue'
- import TablePagination from '@/components/common/TablePagination.vue'
- import ChartBox from '@/components/common/ChartBox.vue'
- import PieMixPanel from '@/components/common/PieMixPanel.vue'
- import WorkbenchTipsPanel from '@/components/common/WorkbenchTipsPanel.vue'
- import { useClientPagination } from '@/composables/useClientPagination'
- import StatusTag from '@/components/common/StatusTag.vue'
- import { fetchDashboard, fetchTownVillagePayTable, markWorkbenchTodoRead } from '@/api/biz'
- import { useUserStore } from '@/stores/user'
- import { formatAmount } from '@/utils/mask'
- import {
- ADMIN_WORKBENCH_TIPS,
- AGRI_WORKBENCH_TIPS,
- HR_WORKBENCH_TIPS,
- } from '@/utils/buildDashboard'
- import type { AlertItem, ChartBarItem, DashboardData, TodoItem } from '@/types/biz'
- const router = useRouter()
- const tickerPaused = ref(false)
- let refreshTimer: ReturnType<typeof setInterval> | null = null
- /** 指标/告警自动刷新间隔(可研:实时刷新) */
- const DASHBOARD_REFRESH_MS = 120_000
- /** 图表主色:政务蓝行政台 */
- const CHART_COLOR = '#2B73F8'
- const CHART_COLORS = ['#2B73F8', '#27C98E', '#FA8C16', '#722ED1', '#1890FF']
- const CHART_COLOR_GREEN = '#27C98E'
- const userStore = useUserStore()
- /** shallowRef:整包替换工作台数据,避免深层响应拖慢图表重算 */
- const data = shallowRef<DashboardData | null>(null)
- const loading = ref(false)
- const payTableLoading = ref(false)
- let loadSeq = 0
- let loadTimer: ReturnType<typeof setTimeout> | null = null
- /** 是否村合作社视角 */
- const isVillage = computed(() => Boolean(userStore.roleInfo?.village))
- /** 是否镇经发中心视角 */
- const isTown = computed(() => userStore.roleInfo?.key === 'town')
- /** 是否镇社区事务受理中心视角 */
- const isSocial = computed(() => userStore.roleInfo?.key === 'social')
- /** 是否镇社区受理中心分管领导视角 */
- const isLeader = computed(() => userStore.roleInfo?.key === 'leader')
- /** 是否区农委 */
- const isAgri = computed(() => userStore.roleInfo?.key === 'agri')
- /** 是否区人社局 */
- const isHr = computed(() => userStore.roleInfo?.key === 'hr')
- /** 是否系统管理员 */
- const isAdmin = computed(() => userStore.roleInfo?.key === 'admin')
- /** 区级政策监测(农委 / 人社 / 系统管理员) */
- const isDistrictPolicy = computed(() => isAgri.value || isHr.value || isAdmin.value)
- /** 村 / 镇四级:发放表 + 饼图共用同一套栅格 */
- const isTownLevelBoard = computed(
- () => isVillage.value || isTown.value || isSocial.value || isLeader.value,
- )
- const payTableTitle = computed(() => {
- if (isVillage.value) return '本村近半年发放人数与金额'
- if (isTown.value) return '本镇各村发放人数与金额'
- return '本月本镇各村发放人数与金额'
- })
- const payTableNameLabel = computed(() => (isVillage.value ? '发放周期' : '村居'))
- const payTableRows = computed(() =>
- isVillage.value ? villagePayRows.value : townVillagePayRows.value,
- )
- const sidePieTitle = computed(() => (isTown.value ? '本月退养资格结构' : '本月业务状态占比'))
- const sidePieItems = computed(() =>
- isTown.value ? townQualifyPieItems.value : (data.value?.statusPieMonth ?? []),
- )
- const workspaceTipsTitle = computed(() => {
- if (isVillage.value) return '村级业务操作提示'
- if (isTown.value) return '资格审批要点'
- if (isSocial.value) return '业务办理要点'
- if (isLeader.value) return '审批要点'
- return '操作提示'
- })
- const workspaceTips = computed(() => {
- if (isVillage.value) return data.value?.villageTips ?? []
- if (isTown.value) return data.value?.townReview?.tips ?? []
- if (isSocial.value) return data.value?.socialWorkbench?.tips ?? []
- if (isLeader.value) return data.value?.leaderWorkbench?.tips ?? []
- return []
- })
- /** 概览标题 */
- const overviewTitle = computed(() => {
- if (isVillage.value) return '本村人员与资格'
- if (isTown.value) return '本镇退养资格审批'
- if (isSocial.value) return '本镇居保匹配与发放办理'
- if (isLeader.value) return '本镇月度清单线上审批'
- if (isAgri.value) return '全区土地退养资格监测'
- if (isHr.value) return '全区土地退养发放监测'
- if (isAdmin.value) return '全区业务数据监测'
- return '本月发放统计'
- })
- /** 角色提示文案 */
- const roleTip = computed(() => {
- const role = userStore.roleInfo
- if (!role) return ''
- if (role.key === 'admin') return '运维全区月批与基础数据,不替代各镇业务审批'
- if (role.key === 'agri') return '全区土地退养资格只读监测'
- if (role.key === 'hr') return '全区发放与居保只读监测(告警不可关闭)'
- if (role.allData) return '全区数据视角'
- if (role.village) return '本村人员录入与提交资格'
- if (role.key === 'town') return '本镇按人审退养资格'
- if (role.key === 'social') return '本镇居保、清单、特殊业务与出盘回盘'
- if (role.key === 'leader') return '审本镇月度清单与特殊业务(镇社区受理中心分管领导)'
- if (role.town) return '本镇数据视角'
- return role.label
- })
- const districtTips = computed(() => {
- if (isAgri.value) return AGRI_WORKBENCH_TIPS
- if (isHr.value) return HR_WORKBENCH_TIPS
- if (isAdmin.value) return ADMIN_WORKBENCH_TIPS
- return []
- })
- const districtTipsTitle = computed(() => {
- if (isAgri.value) return '政策监测要点'
- if (isHr.value) return '发放监测要点'
- if (isAdmin.value) return '运维要点'
- return '工作要点'
- })
- const tipsResetKey = computed(() => userStore.currentRole || '')
- const REGISTERED_COUNT_TIP =
- '当前角色范围内人员档案底数(含暂停、死亡等仍在库人员),不是本月月补清单发放人数。'
- const MONTH_PAY_COUNT_TIP = '当前发放周期纳入月补清单的人数,与在册总人数口径不同。'
- const MONTH_AMOUNT_TIP = '当前发放周期月补清单应发合计。'
- /** 核心指标:在册总人数与本月发放人数分列,避免混用 */
- const statItems = computed(() => {
- const s = data.value?.stats
- const payPeople = data.value?.batchInfo?.peopleCount
- if (!s) {
- return [
- { label: '在册总人数', value: '-', unit: '人', tip: REGISTERED_COUNT_TIP },
- { label: '本月发放人数', value: '-', unit: '人', tip: MONTH_PAY_COUNT_TIP },
- { label: '本月新增', value: '-', unit: '人', tip: '本月新纳入在册的人数。' },
- { label: '本月减少', value: '-', unit: '人', tip: '本月因暂停/停止/死亡等退出发放的人数。' },
- { label: '本月发放金额', value: '-', unit: '元', tip: MONTH_AMOUNT_TIP },
- ]
- }
- return [
- { label: '在册总人数', value: s.normal, unit: '人', tip: REGISTERED_COUNT_TIP },
- { label: '本月发放人数', value: payPeople ?? 0, unit: '人', tip: MONTH_PAY_COUNT_TIP },
- { label: '本月新增', value: s.monthIncrease ?? 0, unit: '人', tip: '本月新纳入在册的人数。' },
- { label: '本月减少', value: s.monthDecrease ?? 0, unit: '人', tip: '本月因暂停/停止/死亡等退出发放的人数。' },
- { label: '本月发放金额', value: formatAmount(s.amount), unit: '元', tip: MONTH_AMOUNT_TIP },
- ]
- })
- const alertTickerItems = computed(() => data.value?.recentAlerts ?? [])
- /** 双份拼接实现无缝横向滚动 */
- const alertTickerLoop = computed(() => {
- const list = alertTickerItems.value
- if (!list.length) return [] as AlertItem[]
- return list.length === 1 ? list : [...list, ...list]
- })
- function goAlert(alertId?: string) {
- if (alertId) {
- void router.push({ path: '/alerts', query: { id: alertId } })
- } else {
- void router.push('/alerts')
- }
- }
- /** 概览指标(按角色侧重) */
- const batchMetrics = computed(() => {
- const info = data.value?.batchInfo
- if (isVillage.value) {
- const s = data.value?.stats
- return [
- {
- label: '本村在册',
- value: s?.normal ?? info?.peopleCount ?? '-',
- unit: '人',
- icon: 'User',
- tone: 'tone-green',
- },
- {
- label: '本月新增',
- value: s?.monthIncrease ?? 0,
- unit: '人',
- icon: 'Checked',
- tone: 'tone-plain',
- },
- {
- label: '本月减少',
- value: s?.monthDecrease ?? 0,
- unit: '人',
- icon: 'Bell',
- tone: 'tone-gold',
- },
- {
- label: '预计发放',
- value: s ? formatAmount(s.amount) : info ? formatAmount(info.amount) : '-',
- unit: '元',
- icon: 'Wallet',
- tone: 'tone-gold',
- },
- ]
- }
- if (isTown.value) {
- const review = data.value?.townReview
- return [
- {
- label: '发放月份',
- value: userStore.currentMonth || '-',
- icon: 'Calendar',
- tone: 'tone-gold',
- },
- {
- label: '待资格审批',
- value: review?.pendingCount ?? 0,
- unit: '人',
- icon: 'User',
- tone: 'tone-green',
- },
- {
- label: '有土地退养',
- value: review?.withLandCount ?? 0,
- unit: '人',
- icon: 'OfficeBuilding',
- tone: 'tone-plain',
- },
- {
- label: '无土地有资格',
- value: review?.withoutLandCount ?? 0,
- unit: '人',
- icon: 'Wallet',
- tone: 'tone-gold',
- },
- ]
- }
- if (isSocial.value) {
- const sw = data.value?.socialWorkbench
- return [
- {
- label: '待居保匹配',
- value: sw?.matchPendingCount ?? 0,
- unit: '批',
- icon: 'Connection',
- tone: 'tone-green',
- },
- {
- label: '待提交领导',
- value: sw?.compareReadyCount ?? 0,
- unit: '批',
- icon: 'Checked',
- tone: 'tone-plain',
- },
- {
- label: '待回盘',
- value: sw?.diskPendingCount ?? 0,
- unit: '批',
- icon: 'Ticket',
- tone: 'tone-gold',
- },
- {
- label: '未关闭告警',
- value: sw?.alertCount ?? 0,
- unit: '条',
- icon: 'Bell',
- tone: 'tone-gold',
- },
- ]
- }
- if (isLeader.value) {
- const lw = data.value?.leaderWorkbench
- return [
- {
- label: '月补贴待审',
- value: lw?.pendingApprovalCount ?? 0,
- unit: '件',
- icon: 'Document',
- tone: 'tone-green',
- },
- {
- label: '特殊业务待审',
- value: lw?.specialPendingCount ?? 0,
- unit: '件',
- icon: 'Checked',
- tone: 'tone-plain',
- },
- {
- label: '待出盘',
- value: lw?.issueReadyCount ?? 0,
- unit: '批',
- icon: 'Ticket',
- tone: 'tone-gold',
- },
- {
- label: '未关闭告警',
- value: lw?.alertCount ?? 0,
- unit: '条',
- icon: 'Bell',
- tone: 'tone-gold',
- },
- ]
- }
- if (isAgri.value) {
- const d = data.value?.districtOverview
- return [
- {
- label: '全区月批',
- value: info?.batchNo ?? '-',
- icon: 'Ticket',
- tone: 'tone-gold',
- },
- {
- label: '覆盖街镇/村',
- value: `${d?.townCount ?? 0}/${d?.villageCount ?? 0}`,
- icon: 'OfficeBuilding',
- tone: 'tone-plain',
- },
- {
- label: '有土地退养',
- value: d?.withLandCount ?? 0,
- unit: '人',
- icon: 'User',
- tone: 'tone-green',
- },
- {
- label: '无土地有资格',
- value: d?.withoutLandCount ?? 0,
- unit: '人',
- icon: 'Checked',
- tone: 'tone-gold',
- },
- ]
- }
- if (isHr.value || isAdmin.value) {
- const d = data.value?.districtOverview
- return [
- {
- label: '全区月批',
- value: info?.batchNo ?? '-',
- icon: 'Ticket',
- tone: 'tone-gold',
- },
- {
- label: '覆盖街镇/村',
- value: `${d?.townCount ?? 0}/${d?.villageCount ?? 0}`,
- icon: 'OfficeBuilding',
- tone: 'tone-plain',
- },
- {
- label: '涉及人数',
- value: d?.peopleCount ?? 0,
- unit: '人',
- icon: 'User',
- tone: 'tone-green',
- },
- {
- label: '预计发放总额',
- value: d ? formatAmount(d.amount) : '-',
- unit: '元',
- icon: 'Wallet',
- tone: 'tone-gold',
- },
- ]
- }
- return [
- {
- label: '全区月批',
- value: info?.batchNo ?? '-',
- icon: 'Ticket',
- tone: 'tone-gold',
- },
- {
- label: '统计范围',
- value: info?.scope ?? '-',
- icon: 'OfficeBuilding',
- tone: 'tone-plain',
- },
- {
- label: '涉及人数',
- value: info?.peopleCount ?? '-',
- unit: '人',
- icon: 'User',
- tone: 'tone-green',
- },
- {
- label: '预计发放金额',
- value: info ? formatAmount(info.amount) : '-',
- unit: '元',
- icon: 'Wallet',
- tone: 'tone-gold',
- },
- ]
- })
- /** 通用柱状图配置 */
- function buildBarOption(
- list: ChartBarItem[],
- yName: string,
- valueFormatter?: (v: unknown) => string,
- barColorStops = [
- { offset: 0, color: '#4A8AFF' },
- { offset: 1, color: '#2B73F8' },
- ],
- ): EChartsCoreOption {
- const names = list.map((i) => i.name)
- const many = names.length > 6
- const endPercent = many ? Math.min(100, Math.round((6 / names.length) * 100)) : 100
- return {
- color: [CHART_COLOR],
- tooltip: {
- trigger: 'axis',
- backgroundColor: 'rgba(255,255,255,0.96)',
- borderColor: '#d6e4ff',
- textStyle: { color: '#333', fontSize: 12 },
- valueFormatter,
- },
- grid: { left: 56, right: 16, top: 28, bottom: many ? 80 : 36 },
- dataZoom: many
- ? [
- {
- type: 'inside',
- xAxisIndex: 0,
- zoomOnMouseWheel: true,
- moveOnMouseMove: true,
- },
- {
- type: 'slider',
- xAxisIndex: 0,
- height: 18,
- bottom: 6,
- start: 0,
- end: endPercent,
- showDetail: false,
- brushSelect: false,
- },
- ]
- : undefined,
- xAxis: {
- type: 'category',
- data: names,
- axisLine: { lineStyle: { color: '#e0d6c8' } },
- axisLabel: {
- color: '#8c8c8c',
- fontSize: many ? 10 : 12,
- interval: 0,
- rotate: many ? 40 : 0,
- hideOverlap: false,
- },
- axisTick: { show: false },
- },
- yAxis: {
- type: 'value',
- name: yName,
- nameTextStyle: { color: '#8c8c8c', fontSize: 11 },
- axisLabel: { color: '#8c8c8c', fontSize: 11 },
- splitLine: { lineStyle: { color: '#f0ebe3', type: 'dashed' } },
- axisLine: { show: false },
- minInterval: yName.includes('人') ? 1 : undefined,
- },
- series: [
- {
- type: 'bar',
- barMaxWidth: 36,
- data: list.map((i) => i.value),
- itemStyle: {
- borderRadius: [2, 2, 0, 0],
- color: {
- type: 'linear',
- x: 0,
- y: 0,
- x2: 0,
- y2: 1,
- colorStops: barColorStops,
- },
- },
- },
- ],
- }
- }
- function formatHeadcount(n: number) {
- return Number(n || 0).toLocaleString('zh-CN')
- }
- function formatPerCapita(amount: number, people: number) {
- const p = Number(people || 0)
- if (!p) return '—'
- return formatAmount(Number(amount || 0) / p)
- }
- function mergePayRows(people: ChartBarItem[] = [], amounts: ChartBarItem[] = []) {
- const map = new Map<string, { name: string; people: number; amount: number }>()
- for (const p of people) {
- map.set(p.name, { name: p.name, people: Number(p.value || 0), amount: 0 })
- }
- for (const a of amounts) {
- const row = map.get(a.name) || { name: a.name, people: 0, amount: 0 }
- row.amount = Number(a.value || 0)
- map.set(a.name, row)
- }
- return [...map.values()]
- }
- function buildPayCombineOption(list: Array<{ name: string; people: number; amount: number }>): EChartsCoreOption {
- return {
- color: [CHART_COLOR, '#1A56C4'],
- tooltip: {
- trigger: 'axis',
- backgroundColor: 'rgba(255,255,255,0.96)',
- borderColor: '#d6e4ff',
- textStyle: { color: '#333', fontSize: 12 },
- },
- legend: {
- data: ['发放金额', '发放人数'],
- top: 0,
- left: 'center',
- orient: 'horizontal',
- itemGap: 18,
- itemWidth: 12,
- itemHeight: 8,
- textStyle: { color: '#595959', fontSize: 12 },
- },
- grid: { left: 8, right: 8, top: 36, bottom: 4, containLabel: true },
- xAxis: {
- type: 'category',
- data: list.map((i) => i.name),
- axisLine: { lineStyle: { color: '#e0d6c8' } },
- axisLabel: { color: '#8c8c8c', fontSize: 11 },
- axisTick: { show: false },
- },
- yAxis: [
- {
- type: 'value',
- name: '金额(元)',
- nameGap: 14,
- nameTextStyle: { color: '#8c8c8c', fontSize: 11 },
- axisLabel: {
- color: '#8c8c8c',
- fontSize: 11,
- formatter: (v: number) => (Math.abs(v) >= 10000 ? `${v / 10000}万` : String(v)),
- },
- splitLine: { lineStyle: { color: '#f0ebe3', type: 'dashed' } },
- axisLine: { show: false },
- },
- {
- type: 'value',
- name: '人数',
- nameGap: 14,
- nameTextStyle: { color: '#8c8c8c', fontSize: 11 },
- axisLabel: { color: '#8c8c8c', fontSize: 11 },
- splitLine: { show: false },
- axisLine: { show: false },
- },
- ],
- series: [
- {
- name: '发放金额',
- type: 'bar',
- barMaxWidth: 28,
- itemStyle: {
- borderRadius: [2, 2, 0, 0],
- color: {
- type: 'linear',
- x: 0,
- y: 0,
- x2: 0,
- y2: 1,
- colorStops: [
- { offset: 0, color: '#4A8AFF' },
- { offset: 1, color: '#2B73F8' },
- ],
- },
- },
- data: list.map((i) => i.amount),
- },
- {
- name: '发放人数',
- type: 'line',
- yAxisIndex: 1,
- smooth: true,
- symbol: 'circle',
- symbolSize: 6,
- lineStyle: { width: 2, color: '#1A56C4' },
- itemStyle: { color: '#1A56C4', borderWidth: 2, borderColor: '#fff' },
- data: list.map((i) => i.people),
- },
- ],
- }
- }
- const villagePayRows = computed(() =>
- mergePayRows(data.value?.villageHalfYearPeopleChart, data.value?.villageHalfYearAmountChart),
- )
- const townVillagePayRows = computed(() =>
- mergePayRows(data.value?.townVillagePeopleChart, data.value?.townVillageAmountChart),
- )
- const townMonthPayRows = computed(() =>
- mergePayRows(data.value?.monthPeopleChart, data.value?.monthAmountChart),
- )
- const townYearPayRows = computed(() =>
- mergePayRows(data.value?.yearPeopleChart, data.value?.yearAmountChart),
- )
- const townMonthPayCombineOption = computed(() => buildPayCombineOption(townMonthPayRows.value))
- const townYearPayCombineOption = computed(() => buildPayCombineOption(townYearPayRows.value))
- const districtTownPayRows = computed(() =>
- mergePayRows(
- data.value?.districtOverview?.townPeopleChart,
- data.value?.districtOverview?.townAmountChart,
- ),
- )
- const districtHalfYearPayRows = computed(() =>
- mergePayRows(
- data.value?.districtOverview?.halfYearPeopleChart,
- data.value?.districtOverview?.halfYearAmountChart,
- ),
- )
- const districtTownPayCombineOption = computed(() => buildPayCombineOption(districtTownPayRows.value))
- const districtHalfYearPayCombineOption = computed(() =>
- buildPayCombineOption(districtHalfYearPayRows.value),
- )
- const townQualifyPieItems = computed(() => {
- const pie = data.value?.townReview?.qualifyPie
- if (pie?.length) return pie
- const r = data.value?.townReview
- return [
- { name: '有土地退养', value: r?.withLandCount ?? 0 },
- { name: '无土地有资格', value: r?.withoutLandCount ?? 0 },
- ].filter((i) => i.value > 0)
- })
- /** 角色维度柱状图(标题与聚合维度均随角色变化) */
- const roleBarOption = computed(() =>
- buildBarOption(
- data.value?.townAmountChart ?? [],
- '金额(元)',
- (v) => `¥${Number(v).toLocaleString('zh-CN')}`,
- ),
- )
- /** 不同批次发放金额对比(柱+折线) */
- const batchCompareOption = computed<EChartsCoreOption>(() => {
- const list = data.value?.batchCompareChart ?? []
- return {
- color: [CHART_COLOR, '#1A56C4'],
- tooltip: {
- trigger: 'axis',
- backgroundColor: 'rgba(255,255,255,0.96)',
- borderColor: '#d6e4ff',
- textStyle: { color: '#333', fontSize: 12 },
- },
- legend: {
- data: ['发放金额', '发放人数'],
- top: 0,
- left: 'center',
- orient: 'horizontal',
- itemGap: 18,
- itemWidth: 12,
- itemHeight: 8,
- textStyle: { color: '#595959', fontSize: 12 },
- },
- grid: { left: 8, right: 8, top: 36, bottom: 4, containLabel: true },
- xAxis: {
- type: 'category',
- data: list.map((i) => i.name),
- axisLine: { lineStyle: { color: '#e0d6c8' } },
- axisLabel: { color: '#8c8c8c', fontSize: 12 },
- axisTick: { show: false },
- },
- yAxis: [
- {
- type: 'value',
- name: '金额(元)',
- nameGap: 14,
- nameTextStyle: { color: '#8c8c8c', fontSize: 11 },
- axisLabel: {
- color: '#8c8c8c',
- fontSize: 11,
- formatter: (v: number) => (Math.abs(v) >= 10000 ? `${v / 10000}万` : String(v)),
- },
- splitLine: { lineStyle: { color: '#f0ebe3', type: 'dashed' } },
- axisLine: { show: false },
- },
- {
- type: 'value',
- name: '人数',
- nameGap: 14,
- nameTextStyle: { color: '#8c8c8c', fontSize: 11 },
- axisLabel: { color: '#8c8c8c', fontSize: 11 },
- splitLine: { show: false },
- axisLine: { show: false },
- },
- ],
- series: [
- {
- name: '发放金额',
- type: 'bar',
- barMaxWidth: 36,
- itemStyle: {
- borderRadius: [2, 2, 0, 0],
- color: {
- type: 'linear',
- x: 0,
- y: 0,
- x2: 0,
- y2: 1,
- colorStops: [
- { offset: 0, color: '#4A8AFF' },
- { offset: 1, color: '#2B73F8' },
- ],
- },
- },
- data: list.map((i) => i.value),
- },
- {
- name: '发放人数',
- type: 'line',
- yAxisIndex: 1,
- smooth: true,
- symbol: 'circle',
- symbolSize: 7,
- lineStyle: { width: 2, color: '#1A56C4' },
- itemStyle: { color: '#1A56C4', borderWidth: 2, borderColor: '#fff' },
- data: list.map((i) => i.count ?? 0),
- },
- ],
- }
- })
- function buildPieOption(
- list: { name: string; value: number }[],
- title: string,
- ): EChartsCoreOption {
- if (!list.length) {
- return {
- title: {
- text: '暂无数据',
- left: 'center',
- top: 'middle',
- textStyle: { color: '#8c8c8c', fontSize: 13, fontWeight: 400 },
- },
- series: [],
- }
- }
- return {
- color: CHART_COLORS,
- tooltip: {
- trigger: 'item',
- formatter: '{b}: {c} ({d}%)',
- backgroundColor: 'rgba(255,255,255,0.96)',
- borderColor: '#d6e4ff',
- textStyle: { color: '#333', fontSize: 12 },
- },
- legend: {
- orient: 'vertical',
- right: 12,
- top: 'center',
- itemWidth: 10,
- itemHeight: 10,
- itemGap: 12,
- textStyle: { color: '#595959', fontSize: 12 },
- },
- series: [
- {
- name: title,
- type: 'pie',
- radius: ['42%', '68%'],
- center: ['38%', '52%'],
- data: list,
- label: { formatter: '{d}%', color: '#595959', fontSize: 11 },
- labelLine: {
- length: 10,
- length2: 8,
- lineStyle: { color: '#d0c4b0' },
- },
- itemStyle: { borderColor: '#fff', borderWidth: 2 },
- emphasis: {
- scaleSize: 4,
- itemStyle: { shadowBlur: 8, shadowColor: 'rgba(43,115,248,0.35)' },
- },
- },
- ],
- }
- }
- const statusPieMonthOption = computed(() =>
- buildPieOption(data.value?.statusPieMonth ?? [], '本月状态'),
- )
- const statusPieYearOption = computed(() =>
- buildPieOption(data.value?.statusPieYear ?? [], '年度状态'),
- )
- /** 区级:各街镇人数 */
- const districtTownPeopleOption = computed(() =>
- buildBarOption(
- data.value?.districtOverview?.townPeopleChart ?? [],
- '人数(人)',
- (v) => `${Number(v).toLocaleString('zh-CN')} 人`,
- [
- { offset: 0, color: '#a8b89a' },
- { offset: 1, color: CHART_COLOR_GREEN },
- ],
- ),
- )
- /** 区级:有土地退养 */
- const districtWithLandOption = computed(() =>
- buildBarOption(
- data.value?.districtOverview?.townWithLandChart ?? [],
- '人数(人)',
- (v) => `${Number(v).toLocaleString('zh-CN')} 人`,
- [
- { offset: 0, color: '#a8b89a' },
- { offset: 1, color: CHART_COLOR_GREEN },
- ],
- ),
- )
- /** 区级:无土地有资格 */
- const districtWithoutLandOption = computed(() =>
- buildBarOption(
- data.value?.districtOverview?.townWithoutLandChart ?? [],
- '人数(人)',
- (v) => `${Number(v).toLocaleString('zh-CN')} 人`,
- [
- { offset: 0, color: '#4A8AFF' },
- { offset: 1, color: '#2B73F8' },
- ],
- ),
- )
- /** 区级:资格结构饼图 */
- const districtQualifyPieOption = computed(() =>
- buildPieOption(data.value?.districtOverview?.qualifyPie ?? [], '退养资格'),
- )
- /** 区级:近半年人数 */
- const districtHalfYearPeopleOption = computed(() =>
- buildBarOption(
- data.value?.districtOverview?.halfYearPeopleChart ?? [],
- '人数(人)',
- (v) => `${Number(v).toLocaleString('zh-CN')} 人`,
- [
- { offset: 0, color: '#a8b89a' },
- { offset: 1, color: CHART_COLOR_GREEN },
- ],
- ),
- )
- /** 区级:近半年金额 */
- const districtHalfYearAmountOption = computed(() =>
- buildBarOption(
- data.value?.districtOverview?.halfYearAmountChart ?? [],
- '金额(元)',
- (v) => `¥${Number(v).toLocaleString('zh-CN')}`,
- ),
- )
- /** 未读待办数 */
- const unreadTodoCount = computed(
- () => (data.value?.todos ?? []).filter((t) => !t.read).length,
- )
- const TODO_PAGE_SIZES = [5, 10, 20]
- const {
- page: todoPage,
- pageSize: todoPageSize,
- total: todoTotal,
- pagedList: pagedTodos,
- } = useClientPagination<TodoItem>(() => data.value?.todos ?? [], { pageSize: 5 })
- const {
- page: alertPage,
- pageSize: alertPageSize,
- total: alertTotal,
- pagedList: pagedAlerts,
- } = useClientPagination<AlertItem>(() => data.value?.recentAlerts ?? [], { pageSize: 5 })
- function canHandleTodo(row: TodoItem) {
- const path = row.path || ''
- if (!path) return false
- return userStore.canAccess(path)
- }
- /** 待办点击:标记已读并跳转业务页 */
- async function onTodoClick(row: TodoItem) {
- if (!row || !canHandleTodo(row)) return
- const key = row.id || ''
- if (key && !row.read) {
- row.read = true
- await markWorkbenchTodoRead(key)
- if (data.value?.batchInfo) {
- data.value.batchInfo.todoCount = unreadTodoCount.value
- }
- }
- void router.push(row.path || '/approvals')
- }
- /** 切换发放周期(写入 store,各角色数据展板/业务页共用) */
- function onMonthChange(month: string | null) {
- if (!month) return
- userStore.setMonth(month)
- }
- /** 按当前角色与发放周期加载数据展板(防抖 + 竞态序号) */
- async function loadData(opts?: { forceLoading?: boolean }) {
- const seq = ++loadSeq
- // 仅首屏或切换角色/月份时盖 loading;定时静默刷新保留旧数据,避免整页长时间转圈
- const blockUi = opts?.forceLoading || !data.value
- const needVillagePayments =
- (isTown.value || isSocial.value || isLeader.value) && !isVillage.value
- if (blockUi) loading.value = true
- try {
- const lite = await fetchDashboard({
- roleKey: userStore.currentRole,
- currentMonth: userStore.currentMonth,
- light: needVillagePayments,
- })
- if (seq !== loadSeq) return
- data.value = lite
- if (blockUi) loading.value = false
- const hasVillagePay = (lite.townVillagePeopleChart || []).some((i) => Number(i.value) > 0)
- if (needVillagePayments && !hasVillagePay) {
- payTableLoading.value = true
- try {
- const charts = await fetchTownVillagePayTable({
- roleKey: userStore.currentRole,
- currentMonth: userStore.currentMonth,
- })
- if (seq !== loadSeq) return
- if (data.value) {
- data.value.townVillagePeopleChart = charts.people
- data.value.townVillageAmountChart = charts.amount
- data.value.townAmountChart = charts.amount
- }
- } finally {
- if (seq === loadSeq) payTableLoading.value = false
- }
- }
- } finally {
- if (seq === loadSeq) loading.value = false
- }
- }
- function scheduleLoad(forceLoading = false) {
- if (loadTimer) clearTimeout(loadTimer)
- loadTimer = setTimeout(() => {
- loadTimer = null
- void loadData({ forceLoading })
- }, 80)
- }
- onMounted(() => {
- void loadData({ forceLoading: true })
- refreshTimer = setInterval(() => {
- void loadData()
- }, DASHBOARD_REFRESH_MS)
- })
- onUnmounted(() => {
- if (loadTimer) clearTimeout(loadTimer)
- if (refreshTimer) {
- clearInterval(refreshTimer)
- refreshTimer = null
- }
- })
- // 切换角色 / 发放周期时刷新(短防抖,避免连点)
- watch(
- () => [userStore.currentRole, userStore.currentMonth],
- () => {
- data.value = null
- scheduleLoad(true)
- },
- )
- </script>
- <style scoped lang="less">
- .dashboard-view {
- display: flex;
- flex-direction: column;
- gap: 12px;
- }
- .dashboard-todos-panel__count {
- font-size: 12px;
- color: #8a7a66;
- }
- .dashboard-todos-table {
- cursor: pointer;
- :deep(.is-todo-read) {
- color: #9a8b7a;
- }
- .text-muted {
- color: #b5a898;
- }
- }
- .alert-ticker {
- display: flex;
- align-items: center;
- gap: 10px;
- min-height: 40px;
- padding: 6px 10px;
- background: #fff8ef;
- border: 1px solid @border-light;
- border-left: 3px solid #2B73F8;
- &__label {
- display: inline-flex;
- align-items: center;
- gap: 4px;
- flex-shrink: 0;
- font-size: 13px;
- font-weight: 600;
- color: @primary-dark;
- em {
- font-style: normal;
- min-width: 18px;
- height: 18px;
- padding: 0 5px;
- border-radius: 9px;
- background: #c45c26;
- color: #fff;
- font-size: 11px;
- line-height: 18px;
- text-align: center;
- }
- }
- &__track-wrap {
- flex: 1;
- min-width: 0;
- overflow: hidden;
- }
- &__track {
- display: flex;
- width: max-content;
- gap: 28px;
- animation-name: alert-ticker-scroll;
- animation-timing-function: linear;
- animation-iteration-count: infinite;
- &.is-paused {
- animation-play-state: paused;
- }
- }
- &__item {
- display: inline-flex;
- align-items: center;
- gap: 8px;
- border: 0;
- background: transparent;
- padding: 0;
- cursor: pointer;
- font: inherit;
- color: @text-primary;
- white-space: nowrap;
- &:hover .alert-ticker__text {
- color: @primary-dark;
- text-decoration: underline;
- }
- }
- &__type {
- font-size: 12px;
- color: #8a7350;
- }
- &__text {
- font-size: 13px;
- max-width: 420px;
- overflow: hidden;
- text-overflow: ellipsis;
- }
- &__empty {
- font-size: 13px;
- color: @text-secondary;
- }
- &__more {
- flex-shrink: 0;
- }
- }
- @keyframes alert-ticker-scroll {
- from {
- transform: translateX(0);
- }
- to {
- transform: translateX(-50%);
- }
- }
- .dashboard-stat-note {
- margin: -4px 0 12px;
- font-size: 12px;
- color: @text-secondary;
- line-height: 1.55;
- }
- /** 村 / 镇展板:表格与饼图并排,不再把饼图单独拉满一行 */
- .dashboard-view--village {
- min-height: 0;
- }
- /** 区农委 / 区人社局:全区监测工作台 */
- .dashboard-view--district {
- min-height: calc(100vh - @header-height - @content-padding * 2);
- }
- .district-workspace {
- flex: 1;
- min-height: 0;
- display: flex;
- flex-direction: column;
- gap: 12px;
- &__charts {
- display: grid;
- grid-template-columns: repeat(3, minmax(0, 1fr));
- gap: 12px;
- @media (max-width: 1200px) {
- grid-template-columns: repeat(2, minmax(0, 1fr));
- }
- @media (max-width: 768px) {
- grid-template-columns: 1fr;
- }
- :deep(.content-panel) {
- display: flex;
- flex-direction: column;
- min-height: 280px;
- }
- }
- &__chart-host {
- flex: 1;
- min-height: 220px;
- }
- }
- /** 区级概况表:列宽均分,避免前几列挤左侧、金额飘到最右 */
- .district-overview-panel {
- :deep(.page-panel__body),
- :deep(.content-panel__body) {
- padding-top: 4px;
- }
- }
- .district-overview-table {
- width: 100%;
- :deep(.el-table__header th),
- :deep(.el-table__body td) {
- padding: 8px 6px;
- }
- :deep(.el-table__header th.el-table__cell) {
- background: #f5f2eb;
- color: @text-regular;
- font-weight: 600;
- }
- :deep(.el-table__body .cell) {
- line-height: 1.4;
- white-space: nowrap;
- }
- :deep(.el-table__row:hover > td.el-table__cell) {
- background: #faf6ee !important;
- }
- }
- .village-workspace {
- display: flex;
- flex-direction: column;
- gap: 12px;
- &__main {
- display: grid;
- grid-template-columns: minmax(0, 1fr);
- grid-template-areas: 'pay';
- gap: 12px;
- align-items: start;
- &.has-pie {
- grid-template-columns: minmax(0, 1.55fr) minmax(300px, 0.9fr);
- grid-template-areas: 'pay pie';
- }
- @media (max-width: 1100px) {
- grid-template-columns: 1fr;
- grid-template-areas: 'pay';
- &.has-pie {
- grid-template-areas:
- 'pay'
- 'pie';
- }
- }
- :deep(.content-panel) {
- margin-bottom: 0;
- min-height: 0;
- overflow: visible;
- display: flex;
- flex-direction: column;
- }
- :deep(.page-panel__title),
- :deep(.block-title) {
- flex-shrink: 0;
- }
- }
- &__pie {
- grid-area: pie;
- }
- &__pay {
- grid-area: pay;
- }
- &__table-panel {
- :deep(.el-table) {
- width: 100%;
- }
- }
- &__trend {
- display: grid;
- grid-template-columns: repeat(2, minmax(0, 1fr));
- gap: 12px;
- @media (max-width: 1100px) {
- grid-template-columns: 1fr;
- }
- :deep(.content-panel) {
- margin-bottom: 0;
- }
- }
- &__chart-host {
- height: 260px;
- }
- }
- .batch-overview {
- background: @block-bg;
- border: 1px solid @border-light;
- overflow: hidden;
- &__head {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 12px;
- padding: 12px 16px;
- background: linear-gradient(90deg, #eef4ff 0%, #f7f9fc 55%, #fff 100%);
- border-bottom: 1px solid #d6e4ff;
- }
- &__title {
- display: flex;
- align-items: flex-start;
- gap: 10px;
- min-width: 0;
- h3 {
- margin: 0;
- font-size: 15px;
- font-weight: 700;
- color: @text-primary;
- line-height: 1.3;
- }
- p {
- margin: 4px 0 0;
- font-size: 12px;
- color: @text-secondary;
- line-height: 1.4;
- em {
- font-style: normal;
- font-weight: 600;
- color: @primary-dark;
- }
- }
- }
- &__bar {
- flex-shrink: 0;
- width: 3px;
- height: 28px;
- margin-top: 2px;
- background: linear-gradient(180deg, @brand-gold-deep, @primary-color);
- border-radius: 1px;
- }
- &__period {
- flex-shrink: 0;
- display: flex;
- align-items: center;
- gap: 8px;
- padding: 2px 6px 2px 10px;
- font-size: 12px;
- color: @primary-dark;
- background: rgba(43, 115, 248, 0.12);
- border: 1px solid rgba(43, 115, 248, 0.35);
- border-radius: @radius-sm;
- }
- &__period-label {
- flex-shrink: 0;
- white-space: nowrap;
- }
- &__period-picker {
- width: 120px;
- :deep(.el-input__wrapper) {
- background: #fff;
- box-shadow: none;
- border: 1px solid rgba(43, 115, 248, 0.45);
- }
- :deep(.el-input__inner) {
- font-size: 12px;
- color: @primary-dark;
- font-weight: 600;
- }
- }
- &__metrics {
- display: grid;
- grid-template-columns: repeat(4, 1fr);
- gap: 0;
- @media (max-width: 900px) {
- grid-template-columns: repeat(2, 1fr);
- }
- }
- }
- .metric-item {
- display: flex;
- align-items: center;
- gap: 10px;
- padding: 16px 14px;
- border-right: 1px solid @border-light;
- transition: background 0.15s;
- &:nth-child(4n),
- &:last-child {
- border-right: none;
- }
- @media (max-width: 900px) {
- border-bottom: 1px solid @border-light;
- &:nth-child(2n) {
- border-right: none;
- }
- &:nth-child(n + 3) {
- border-bottom: none;
- }
- }
- &:hover {
- background: @el-primary-light-9;
- }
- &__icon {
- flex-shrink: 0;
- width: 36px;
- height: 36px;
- display: flex;
- align-items: center;
- justify-content: center;
- border-radius: @radius-sm;
- font-size: 18px;
- background: @el-primary-light-9;
- color: @primary-color;
- }
- &__body {
- min-width: 0;
- }
- &__label {
- font-size: 12px;
- color: @text-secondary;
- margin-bottom: 4px;
- white-space: nowrap;
- }
- &__value {
- font-size: 18px;
- font-weight: 700;
- color: @text-primary;
- line-height: 1.2;
- word-break: break-all;
- em {
- margin-left: 2px;
- font-size: 12px;
- font-style: normal;
- font-weight: 400;
- color: @text-secondary;
- }
- }
- &.tone-gold .metric-item__icon {
- background: #e8f1ff;
- color: @primary-dark;
- }
- &.tone-green .metric-item__icon {
- background: @el-success-light-9;
- color: @success-color;
- }
- &.tone-warn .metric-item__icon {
- background: @el-warning-light-9;
- color: @warning-color;
- }
- &.tone-danger .metric-item__icon {
- background: @el-danger-light-9;
- color: @danger-color;
- }
- &.tone-plain .metric-item__icon {
- background: #f3f3f3;
- color: @text-regular;
- }
- }
- .dashboard-view__charts,
- .dashboard-view__tables {
- display: grid;
- grid-template-columns: repeat(2, 1fr);
- gap: 12px;
- @media (max-width: 1100px) {
- grid-template-columns: 1fr;
- }
- :deep(.content-panel) {
- margin-bottom: 0;
- height: 100%;
- }
- }
- </style>
|