index.vue 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794
  1. <template>
  2. <div :class="prefixCls" class="financial-wealth-page">
  3. <div class="page-header">
  4. <div class="page-title">金融理财</div>
  5. <div class="page-subtitle">按对象与日期筛选理财交易,支持关键词维护、分页浏览和明细下钻查看。</div>
  6. </div>
  7. <div class="filter-bar">
  8. <div class="filter-item">
  9. <span class="filter-label">选择对象:</span>
  10. <Select
  11. v-model:value="filters.target"
  12. mode="multiple"
  13. allow-clear
  14. max-tag-count="responsive"
  15. class="filter-input"
  16. :options="targetOptions"
  17. placeholder="全部"
  18. />
  19. </div>
  20. <div class="filter-item">
  21. <span class="filter-label">交易日期:</span>
  22. <RangePicker v-model:value="filters.dateRange" class="filter-date" />
  23. </div>
  24. <div class="toolbar-actions">
  25. <div class="filter-actions">
  26. <Button type="primary" :loading="queryLoading" @click="handleSearch">查询</Button>
  27. <Button @click="handleReset">重置</Button>
  28. </div>
  29. <div class="extra-actions">
  30. <Button @click="openKeywordModal">关键词配置</Button>
  31. </div>
  32. </div>
  33. </div>
  34. <ConfigManageModal
  35. :open="keywordModalOpen"
  36. title="关键词管理"
  37. :width="960"
  38. wrapClassName="financial-keyword-modal"
  39. summary="支持关键词与标签双字段维护,统一当前页面筛选口径"
  40. :refresh-loading="keywordLoading"
  41. :table-height="420"
  42. :total="keywordData.length"
  43. :current="keywordPagination.current"
  44. :total-pages="keywordTotalPages"
  45. :page-size="keywordPagination.pageSize"
  46. :page-size-options="pageSizeOptions"
  47. :jump-page="keywordJumpPage"
  48. @update:open="handleKeywordModalChange"
  49. @update:jumpPage="handleKeywordJumpPageInput"
  50. @create="openKeywordEditor()"
  51. @prev-page="handleKeywordPrevPage"
  52. @next-page="handleKeywordNextPage"
  53. @page-size-change="handleKeywordPageSizeChange"
  54. @jump-page="handleKeywordJumpPage"
  55. >
  56. <template #default="{ tableMaxHeight }">
  57. <BasicTable
  58. :columns="keywordColumns"
  59. :data-source="keywordPagedData"
  60. :loading="keywordLoading"
  61. :pagination="false"
  62. :show-table-setting="false"
  63. :table-setting="{ size: true, setting: true, redo: false, fullScreen: false }"
  64. :can-resize="true"
  65. :max-height="tableMaxHeight"
  66. row-key="rowKey"
  67. >
  68. <template #keywordAction="{ record }">
  69. <TableAction :actions="getKeywordActions(record)" />
  70. </template>
  71. </BasicTable>
  72. </template>
  73. </ConfigManageModal>
  74. <ConfigEditorModal
  75. v-model:open="keywordEditorOpen"
  76. :title="keywordEditorTitle"
  77. :confirmLoading="keywordSaving"
  78. wrapClassName="financial-keyword-editor-modal"
  79. @ok="handleSaveKeyword"
  80. @cancel="closeKeywordEditor"
  81. >
  82. <Form layout="vertical" class="keyword-form">
  83. <Form.Item
  84. label="关键字"
  85. :validate-status="keywordFormErrors.financialKey ? 'error' : ''"
  86. :help="keywordFormErrors.financialKey"
  87. >
  88. <Input v-model:value="keywordForm.financialKey" :maxlength="100" placeholder="请输入关键字" allow-clear />
  89. </Form.Item>
  90. <Form.Item label="标签" :validate-status="keywordFormErrors.tag ? 'error' : ''" :help="keywordFormErrors.tag">
  91. <Input v-model:value="keywordForm.tag" :maxlength="100" placeholder="请输入标签" allow-clear />
  92. </Form.Item>
  93. </Form>
  94. </ConfigEditorModal>
  95. <div class="content-panel">
  96. <div class="table-card">
  97. <div class="panel-header">
  98. <div>
  99. <div class="panel-title">理财交易列表</div>
  100. <div class="panel-desc">按关键词命中结果展示理财相关交易记录,支持快速查看当天对应的交易明细。</div>
  101. </div>
  102. </div>
  103. <div v-if="!hasSearched" class="query-state-panel">
  104. <EmptyState
  105. variant="query"
  106. title="请点击查询"
  107. description="设置条件精细查询,也可点击查询展示所有信息"
  108. min-height="100%"
  109. description-width="min(100%, 420px)"
  110. />
  111. </div>
  112. <div v-else class="table-panel">
  113. <div class="table-wrapper">
  114. <BasicTable
  115. :columns="columns"
  116. :data-source="tableData"
  117. :loading="queryLoading"
  118. :pagination="false"
  119. :show-table-setting="false"
  120. :table-setting="{ size: true, setting: true, redo: false, fullScreen: false }"
  121. :can-resize="true"
  122. :is-can-resize-parent="true"
  123. :resize-height-offset="32"
  124. row-key="rowKey"
  125. >
  126. <template #action="{ record }">
  127. <Button type="link" size="small" @click="handleViewDetail(record)">查看详情</Button>
  128. </template>
  129. </BasicTable>
  130. </div>
  131. </div>
  132. <div v-if="hasSearched" class="pagination-bar">
  133. <AnalysisPagination
  134. :total="pagination.total"
  135. :current="pagination.current"
  136. :page-size="pagination.pageSize"
  137. :page-size-options="pageSizeOptions"
  138. :loading="queryLoading"
  139. @change="handlePaginationChange"
  140. />
  141. </div>
  142. </div>
  143. </div>
  144. <TransRecordModal
  145. v-model:open="transRecordModalOpen"
  146. :base-query="transRecordBaseQuery"
  147. :hide-technical-id-columns="true"
  148. />
  149. </div>
  150. </template>
  151. <script lang="ts" setup name="ViewsFinancialWealthIndex">
  152. import { computed, onMounted, reactive, ref } from 'vue';
  153. import dayjs, { Dayjs } from 'dayjs';
  154. import { Button, DatePicker, Form, Input, Select } from 'ant-design-vue';
  155. import AnalysisPagination from '@jeesite/core/components/AnalysisPagination/src/AnalysisPagination.vue';
  156. import { EmptyState } from '@jeesite/core/components/EmptyState';
  157. import { BasicTable, TableAction } from '@jeesite/core/components/Table';
  158. import { useAnalysisRefreshPrompt } from '@jeesite/core/hooks/web/useAnalysisRefreshPrompt';
  159. import { useDesign } from '@jeesite/core/hooks/web/useDesign';
  160. import { useMessage } from '@jeesite/core/hooks/web/useMessage';
  161. import { useTableSkill } from '@jeesite/core/hooks/web/useTableSkill';
  162. import ConfigManageModal from '@jeesite/core/components/AnalysisConfigModal/src/ConfigManageModal.vue';
  163. import ConfigEditorModal from '@jeesite/core/components/AnalysisConfigModal/src/ConfigEditorModal.vue';
  164. import { mergeDetailDateRange, pickDetailValue } from '@jeesite/core/utils/detailQuery';
  165. import componentSetting from '@jeesite/core/settings/componentSetting';
  166. import { getPersonOptions } from '@jeesite/person/api/personApi';
  167. import TransRecordModal from '../components/TransRecordModal.vue';
  168. import {
  169. createOrUpdateFinancialKey,
  170. deleteFinancialKey,
  171. listFinancialKeys,
  172. statFirstTransFinancial,
  173. } from '@jeesite/trans/api/transApi';
  174. import type { TransFinancialDTO, TransFinancialKey } from '@jeesite/trans/api/types/dto';
  175. import type { TransFinancialQuery, TransRecordQuery } from '@jeesite/trans/api/types/query';
  176. interface KeywordRow extends TransFinancialKey {
  177. rowKey: number | string;
  178. rowNo: number;
  179. }
  180. interface KeywordFormState {
  181. id?: string | number;
  182. financialKey: string;
  183. tag: string;
  184. }
  185. interface FinancialRow {
  186. rowKey: number | string;
  187. rowNo: number;
  188. personName: string;
  189. personCardNo: string;
  190. otherName: string;
  191. otherCardNo: string;
  192. transDateTime: string;
  193. transSummary: string;
  194. remark: string;
  195. raw: TransFinancialDTO;
  196. }
  197. const DEFAULT_SORT_KEY = 'transDateTime';
  198. const DEFAULT_SORT_ORDER = 'DESC' as const;
  199. const { prefixCls } = useDesign('financial-wealth');
  200. const { queryLoading, runWithQueryLoading } = useTableSkill();
  201. const { createMessage } = useMessage();
  202. const RangePicker = DatePicker.RangePicker;
  203. type RangeValue = [Dayjs, Dayjs] | null;
  204. const filters = ref({
  205. target: [] as string[],
  206. dateRange: null as RangeValue,
  207. });
  208. const keywordModalOpen = ref(false);
  209. const keywordEditorOpen = ref(false);
  210. const keywordLoading = ref(false);
  211. const keywordSaving = ref(false);
  212. const keywordData = ref<KeywordRow[]>([]);
  213. const keywordJumpPage = ref('1');
  214. const keywordPagination = ref({
  215. current: 1,
  216. pageSize: componentSetting.table.defaultPageSize,
  217. });
  218. const tableData = ref<FinancialRow[]>([]);
  219. const hasSearched = ref(false);
  220. const { syncAnalysisDataRefreshState } = useAnalysisRefreshPrompt({
  221. refresh: () => fetchTable(),
  222. hasData: () => hasSearched.value && tableData.value.length > 0,
  223. });
  224. const targetOptions = ref<Array<{ label: string; value: string }>>([]);
  225. const jumpPage = ref('1');
  226. const transRecordModalOpen = ref(false);
  227. const transRecordBaseQuery = ref<Partial<TransRecordQuery>>({});
  228. const normalizeSelectedTargets = (value?: string[] | string | null) => {
  229. if (Array.isArray(value)) {
  230. const list = value.map((item) => String(item).trim()).filter(Boolean);
  231. return list.length ? Array.from(new Set(list)) : [];
  232. }
  233. if (!value) return [];
  234. const list = String(value)
  235. .split(',')
  236. .map((item) => item.trim())
  237. .filter(Boolean);
  238. return list.length ? Array.from(new Set(list)) : [];
  239. };
  240. const pagination = ref({
  241. current: 1,
  242. pageSize: componentSetting.table.defaultPageSize,
  243. total: 0,
  244. });
  245. const keywordForm = reactive<KeywordFormState>({
  246. id: undefined,
  247. financialKey: '',
  248. tag: '',
  249. });
  250. const keywordFormErrors = reactive({
  251. financialKey: '',
  252. tag: '',
  253. });
  254. const keywordEditorTitle = computed(() => (keywordForm.id ? '编辑关键词' : '新建关键词'));
  255. const keywordTotalPages = computed(() => {
  256. if (!keywordData.value.length) return 1;
  257. return Math.max(1, Math.ceil(keywordData.value.length / keywordPagination.value.pageSize));
  258. });
  259. const keywordPagedData = computed(() => {
  260. const start = (keywordPagination.value.current - 1) * keywordPagination.value.pageSize;
  261. const end = start + keywordPagination.value.pageSize;
  262. return keywordData.value.slice(start, end);
  263. });
  264. const totalPages = computed(() => {
  265. if (!pagination.value.total) return 1;
  266. return Math.max(1, Math.ceil(pagination.value.total / pagination.value.pageSize));
  267. });
  268. const pageSizeOptions = componentSetting.table.pageSizeOptions.map((value) => ({
  269. label: value,
  270. value: Number(value),
  271. }));
  272. const columns = [
  273. { title: '序号', dataIndex: 'rowNo', key: 'rowNo', width: 80, align: 'center' },
  274. { title: '本方姓名', dataIndex: 'personName', key: 'personName', width: 120, align: 'center' },
  275. { title: '本方卡号', dataIndex: 'personCardNo', key: 'personCardNo', width: 180, align: 'center' },
  276. { title: '对方姓名', dataIndex: 'otherName', key: 'otherName', width: 120, align: 'center' },
  277. { title: '对方卡号', dataIndex: 'otherCardNo', key: 'otherCardNo', width: 180, align: 'center' },
  278. { title: '交易时间', dataIndex: 'transDateTime', key: 'transDateTime', width: 180, align: 'center' },
  279. { title: '交易摘要', dataIndex: 'transSummary', key: 'transSummary', width: 360 },
  280. { title: '备注', dataIndex: 'remark', key: 'remark', width: 160, align: 'center' },
  281. {
  282. title: '操作',
  283. dataIndex: 'action',
  284. key: 'action',
  285. width: 100,
  286. align: 'center',
  287. fixed: 'right',
  288. slots: { customRender: 'action' },
  289. },
  290. ];
  291. const keywordColumns = [
  292. { title: '序号', dataIndex: 'rowNo', key: 'rowNo', width: 80, align: 'center' },
  293. { title: '关键字', dataIndex: 'financialKey', key: 'financialKey', width: 320, align: 'left' },
  294. { title: '标签', dataIndex: 'tag', key: 'tag', width: 240, align: 'center' },
  295. {
  296. title: '操作',
  297. dataIndex: 'action',
  298. key: 'action',
  299. width: 160,
  300. align: 'center',
  301. slots: { customRender: 'keywordAction' },
  302. },
  303. ];
  304. const getKeywordActions = (record: KeywordRow) => [
  305. {
  306. label: '编辑',
  307. onClick: () => openKeywordEditor(record),
  308. },
  309. {
  310. label: '删除',
  311. color: 'error' as const,
  312. popConfirm: {
  313. title: '是否确认删除该关键字?',
  314. confirm: () => handleDeleteKeyword(record),
  315. },
  316. },
  317. ];
  318. const normalizeKeywordList = (resp: any): TransFinancialKey[] => {
  319. if (Array.isArray(resp)) return resp;
  320. if (Array.isArray(resp?.data)) return resp.data;
  321. if (Array.isArray(resp?.list)) return resp.list;
  322. if (Array.isArray(resp?.data?.list)) return resp.data.list;
  323. return [];
  324. };
  325. const normalizeFinancialList = (resp: any): TransFinancialDTO[] => {
  326. if (Array.isArray(resp)) return resp;
  327. if (Array.isArray(resp?.records)) return resp.records;
  328. if (Array.isArray(resp?.data?.records)) return resp.data.records;
  329. if (Array.isArray(resp?.list)) return resp.list;
  330. if (Array.isArray(resp?.data)) return resp.data;
  331. if (Array.isArray(resp?.data?.list)) return resp.data.list;
  332. return [];
  333. };
  334. const normalizeTotal = (resp: any, listLen: number) => {
  335. const total = resp?.total ?? resp?.data?.total ?? resp?.count ?? resp?.data?.count ?? listLen;
  336. return Number.isFinite(Number(total)) ? Number(total) : listLen;
  337. };
  338. const mapKeywordRows = (list: TransFinancialKey[]): KeywordRow[] =>
  339. list.map((item, index) => ({
  340. ...item,
  341. rowKey: item.id ?? `financial-key-${index}`,
  342. rowNo: index + 1,
  343. }));
  344. const mapFinancialRows = (list: TransFinancialDTO[]): FinancialRow[] =>
  345. list.map((item, index) => ({
  346. rowKey: item.id ?? `financial-row-${pagination.value.current}-${index}`,
  347. rowNo: (pagination.value.current - 1) * pagination.value.pageSize + index + 1,
  348. personName: item.personName || '-',
  349. personCardNo: item.personCardNo || '-',
  350. otherName: item.otherName || '-',
  351. otherCardNo: item.otherCardNo || '-',
  352. transDateTime: item.transDateTime || '-',
  353. transSummary: item.transSummary || '-',
  354. remark: item.remark || '-',
  355. raw: item,
  356. }));
  357. const getSortTimestamp = (value?: string) => {
  358. if (!value) return 0;
  359. const timestamp = dayjs(value).valueOf();
  360. return Number.isFinite(timestamp) ? timestamp : 0;
  361. };
  362. const sortFinancialList = (list: TransFinancialDTO[]) =>
  363. [...list].sort((left, right) => {
  364. const leftTime = getSortTimestamp(left.transDateTime);
  365. const rightTime = getSortTimestamp(right.transDateTime);
  366. if (rightTime !== leftTime) return rightTime - leftTime;
  367. return String(left.personName || left.personCardNo || '').localeCompare(
  368. String(right.personName || right.personCardNo || ''),
  369. );
  370. });
  371. const buildQuery = (): TransFinancialQuery => {
  372. const range = filters.value.dateRange as RangeValue;
  373. return {
  374. page: pagination.value.current,
  375. limit: pagination.value.pageSize,
  376. startDate: range?.[0] ? dayjs(range[0]).format('YYYY-MM-DD') : undefined,
  377. endDate: range?.[1] ? dayjs(range[1]).format('YYYY-MM-DD') : undefined,
  378. personNames: (() => {
  379. const personNames = normalizeSelectedTargets(filters.value.target);
  380. return personNames.length ? personNames : undefined;
  381. })(),
  382. orderKey: DEFAULT_SORT_KEY,
  383. sort: DEFAULT_SORT_ORDER,
  384. };
  385. };
  386. const buildTransRecordBaseQuery = (record: FinancialRow): Partial<TransRecordQuery> => {
  387. const currentQuery = buildQuery();
  388. const transDate = record.raw.transDateTime ? dayjs(record.raw.transDateTime).format('YYYY-MM-DD') : undefined;
  389. const { startDate, endDate } = mergeDetailDateRange(
  390. {
  391. startDate: currentQuery.startDate,
  392. endDate: currentQuery.endDate,
  393. },
  394. {
  395. startDate: transDate,
  396. endDate: transDate,
  397. },
  398. );
  399. return {
  400. personName: pickDetailValue(record.raw.personName, currentQuery.personNames?.[0]),
  401. personCardNo: pickDetailValue(record.raw.personCardNo),
  402. otherName: pickDetailValue(record.raw.otherName),
  403. otherCardNo: pickDetailValue(record.raw.otherCardNo),
  404. startDate,
  405. endDate,
  406. };
  407. };
  408. const resetKeywordForm = () => {
  409. keywordForm.id = undefined;
  410. keywordForm.financialKey = '';
  411. keywordForm.tag = '';
  412. keywordFormErrors.financialKey = '';
  413. keywordFormErrors.tag = '';
  414. };
  415. const loadKeywordList = async (showSuccessMessage = false) => {
  416. keywordLoading.value = true;
  417. try {
  418. const resp = await listFinancialKeys();
  419. keywordData.value = mapKeywordRows(normalizeKeywordList(resp));
  420. keywordPagination.value.current = 1;
  421. keywordJumpPage.value = '1';
  422. if (showSuccessMessage) {
  423. createMessage.success('关键词列表已刷新');
  424. }
  425. } catch (error) {
  426. keywordData.value = [];
  427. keywordPagination.value.current = 1;
  428. keywordJumpPage.value = '1';
  429. createMessage.warning('获取关键词失败');
  430. } finally {
  431. keywordLoading.value = false;
  432. }
  433. };
  434. const handleKeywordPrevPage = () => {
  435. if (keywordPagination.value.current <= 1) return;
  436. keywordPagination.value.current -= 1;
  437. keywordJumpPage.value = String(keywordPagination.value.current);
  438. };
  439. const handleKeywordNextPage = () => {
  440. if (keywordPagination.value.current >= keywordTotalPages.value) return;
  441. keywordPagination.value.current += 1;
  442. keywordJumpPage.value = String(keywordPagination.value.current);
  443. };
  444. const handleKeywordPageSizeChange = (value: string | number) => {
  445. keywordPagination.value.pageSize = Number(value);
  446. keywordPagination.value.current = 1;
  447. keywordJumpPage.value = '1';
  448. };
  449. const handleKeywordJumpPage = () => {
  450. const next = Number(keywordJumpPage.value);
  451. if (!Number.isInteger(next) || next < 1) {
  452. createMessage.warning('请输入正确页码');
  453. keywordJumpPage.value = String(keywordPagination.value.current);
  454. return;
  455. }
  456. keywordPagination.value.current = Math.min(next, keywordTotalPages.value);
  457. keywordJumpPage.value = String(keywordPagination.value.current);
  458. };
  459. const handleKeywordJumpPageInput = (value: string) => {
  460. keywordJumpPage.value = value;
  461. };
  462. const openKeywordModal = async () => {
  463. keywordModalOpen.value = true;
  464. await loadKeywordList();
  465. };
  466. const handleKeywordModalChange = async (open: boolean) => {
  467. keywordModalOpen.value = open;
  468. if (open && !keywordData.value.length) {
  469. await loadKeywordList();
  470. }
  471. };
  472. const openKeywordEditor = (record?: KeywordRow) => {
  473. resetKeywordForm();
  474. if (record) {
  475. keywordForm.id = record.id !== undefined && record.id !== null ? String(record.id) : undefined;
  476. keywordForm.financialKey = record.financialKey || '';
  477. keywordForm.tag = record.tag || '';
  478. }
  479. keywordEditorOpen.value = true;
  480. };
  481. const closeKeywordEditor = () => {
  482. keywordEditorOpen.value = false;
  483. resetKeywordForm();
  484. };
  485. const validateKeywordForm = () => {
  486. const financialKey = keywordForm.financialKey.trim();
  487. const tag = keywordForm.tag.trim();
  488. keywordFormErrors.financialKey = financialKey ? '' : '请输入关键字';
  489. keywordFormErrors.tag = tag ? '' : '请输入标签';
  490. if (!financialKey || !tag) return false;
  491. keywordForm.financialKey = financialKey;
  492. keywordForm.tag = tag;
  493. return true;
  494. };
  495. const handleSaveKeyword = async () => {
  496. if (!validateKeywordForm()) return;
  497. keywordSaving.value = true;
  498. try {
  499. await createOrUpdateFinancialKey({
  500. id: keywordForm.id,
  501. financialKey: keywordForm.financialKey,
  502. tag: keywordForm.tag,
  503. });
  504. createMessage.success(keywordForm.id ? '关键词更新成功' : '关键词新增成功');
  505. keywordEditorOpen.value = false;
  506. resetKeywordForm();
  507. await loadKeywordList();
  508. } catch (error) {
  509. createMessage.warning(keywordForm.id ? '关键词更新失败' : '关键词新增失败');
  510. } finally {
  511. keywordSaving.value = false;
  512. }
  513. };
  514. const handleDeleteKeyword = async (record: KeywordRow) => {
  515. if (!record.id) {
  516. createMessage.warning('缺少关键词ID,无法删除');
  517. return;
  518. }
  519. try {
  520. await deleteFinancialKey(String(record.id));
  521. createMessage.success('关键词删除成功');
  522. await loadKeywordList();
  523. } catch (error) {
  524. createMessage.warning('关键词删除失败');
  525. }
  526. };
  527. const fetchTable = async () => {
  528. await runWithQueryLoading(async () => {
  529. const resp = await statFirstTransFinancial(buildQuery());
  530. const list = sortFinancialList(normalizeFinancialList(resp));
  531. tableData.value = mapFinancialRows(list);
  532. pagination.value.total = normalizeTotal(resp, list.length);
  533. hasSearched.value = true;
  534. syncAnalysisDataRefreshState(list.length > 0);
  535. const current = Number((resp as any)?.current ?? (resp as any)?.data?.current ?? pagination.value.current);
  536. const size = Number((resp as any)?.size ?? (resp as any)?.data?.size ?? pagination.value.pageSize);
  537. if (Number.isFinite(current) && current > 0) pagination.value.current = current;
  538. if (Number.isFinite(size) && size > 0) pagination.value.pageSize = size;
  539. jumpPage.value = String(pagination.value.current);
  540. });
  541. };
  542. const handleSearch = async () => {
  543. pagination.value.current = 1;
  544. await fetchTable();
  545. };
  546. const handleReset = async () => {
  547. filters.value = {
  548. target: [],
  549. dateRange: null,
  550. };
  551. pagination.value.current = 1;
  552. await fetchTable();
  553. };
  554. const handlePrevPage = async () => {
  555. if (queryLoading.value || pagination.value.current <= 1) return;
  556. pagination.value.current -= 1;
  557. await fetchTable();
  558. };
  559. const handleNextPage = async () => {
  560. if (queryLoading.value || pagination.value.current >= totalPages.value) return;
  561. pagination.value.current += 1;
  562. await fetchTable();
  563. };
  564. const handlePaginationChange = async (page: number, pageSize: number) => {
  565. if (queryLoading.value) return;
  566. if (pageSize !== pagination.value.pageSize) {
  567. pagination.value.pageSize = pageSize;
  568. await handlePageSizeChange();
  569. return;
  570. }
  571. if (page === pagination.value.current - 1) {
  572. await handlePrevPage();
  573. return;
  574. }
  575. if (page === pagination.value.current + 1) {
  576. await handleNextPage();
  577. return;
  578. }
  579. if (page === pagination.value.current) return;
  580. jumpPage.value = String(page);
  581. await handleJumpPage();
  582. };
  583. const handlePageSizeChange = async () => {
  584. pagination.value.current = 1;
  585. await fetchTable();
  586. };
  587. const handleJumpPage = async () => {
  588. const next = Number(jumpPage.value);
  589. if (!Number.isInteger(next) || next < 1) {
  590. createMessage.warning('请输入正确页码');
  591. jumpPage.value = String(pagination.value.current);
  592. return;
  593. }
  594. pagination.value.current = Math.min(next, totalPages.value);
  595. await fetchTable();
  596. };
  597. const handleViewDetail = (record: FinancialRow) => {
  598. transRecordBaseQuery.value.id = record.raw.id || undefined;
  599. transRecordModalOpen.value = true;
  600. };
  601. const fetchTargetOptions = async () => {
  602. try {
  603. targetOptions.value = await getPersonOptions(false);
  604. } catch (error) {
  605. targetOptions.value = [];
  606. }
  607. };
  608. onMounted(async () => {
  609. await fetchTargetOptions();
  610. });
  611. </script>
  612. <style lang="less">
  613. @import '../styles/trans-analysis-table-ui.less';
  614. @prefix-cls: ~'jeesite-financial-wealth';
  615. .@{prefix-cls}.financial-wealth-page {
  616. .analysis-page-frame();
  617. .filter-input {
  618. width: 180px;
  619. }
  620. .filter-date {
  621. width: 360px;
  622. }
  623. .toolbar-actions {
  624. display: flex;
  625. align-items: center;
  626. gap: 8px;
  627. margin-left: auto;
  628. flex-wrap: wrap;
  629. }
  630. .filter-actions {
  631. display: flex;
  632. gap: 8px;
  633. }
  634. .extra-actions {
  635. display: flex;
  636. }
  637. .content-panel {
  638. flex: 1 1 auto;
  639. min-height: 0;
  640. display: flex;
  641. flex-direction: column;
  642. gap: 2px;
  643. }
  644. .table-card {
  645. .analysis-card-shell();
  646. flex: 1 1 auto;
  647. }
  648. .table-wrapper .jeesite-basic-table {
  649. height: 100%;
  650. display: flex;
  651. flex-direction: column;
  652. }
  653. .table-wrapper .ant-table-wrapper {
  654. height: 100%;
  655. min-height: 0;
  656. }
  657. .table-wrapper .ant-spin-nested-loading,
  658. .table-wrapper .ant-spin-container {
  659. height: 100%;
  660. min-height: 0;
  661. }
  662. .table-wrapper .ant-spin-container,
  663. .table-wrapper .ant-table,
  664. .table-wrapper .ant-table-container {
  665. height: 100%;
  666. min-height: 0;
  667. display: flex;
  668. flex-direction: column;
  669. }
  670. .table-wrapper .ant-table-body {
  671. max-height: 100% !important;
  672. }
  673. .query-state-panel {
  674. flex: 1 1 auto;
  675. min-height: 0;
  676. display: flex;
  677. align-items: center;
  678. justify-content: center;
  679. padding: 12px 0 0;
  680. }
  681. .table-wrapper .ant-table-cell {
  682. font-size: 12px;
  683. }
  684. .pagination-input {
  685. width: 48px;
  686. }
  687. .keyword-form {
  688. padding-top: 8px;
  689. }
  690. :deep(.ant-select-selector),
  691. :deep(.ant-input),
  692. :deep(.ant-picker),
  693. :deep(.ant-btn) {
  694. border-radius: 8px;
  695. }
  696. @media screen and (max-width: 992px) {
  697. .toolbar-actions {
  698. width: 100%;
  699. margin-left: 0;
  700. }
  701. .filter-input,
  702. .filter-date {
  703. width: 100%;
  704. }
  705. .pagination-bar {
  706. justify-content: flex-start;
  707. }
  708. }
  709. }
  710. </style>