index.vue 83 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912
  1. <template>
  2. <div :class="prefixCls" class="case-data-page">
  3. <div class="page-main">
  4. <div class="header-bar">
  5. <div class="import-tabs" role="tablist" aria-label="导入数据页签">
  6. <button
  7. type="button"
  8. class="import-tabs__item"
  9. :class="{ 'is-active': activeTab === 'import' }"
  10. role="tab"
  11. :aria-selected="activeTab === 'import'"
  12. @click="activeTab = 'import'"
  13. >
  14. 导入数据
  15. </button>
  16. <button
  17. type="button"
  18. class="import-tabs__item"
  19. :class="{ 'is-active': activeTab === 'files' }"
  20. role="tab"
  21. :aria-selected="activeTab === 'files'"
  22. @click="activeTab = 'files'"
  23. >
  24. 导入文件
  25. </button>
  26. </div>
  27. <div class="header-actions">
  28. <Button v-if="cleanMode === 'smart'" type="primary" class="header-actions__smart" @click="goSmartAnalysis">
  29. 进行智能分析
  30. </Button>
  31. <Button v-else-if="cleanMode === 'manual'" class="header-actions__result" @click="activeTab = 'import'">
  32. 查看结果
  33. </Button>
  34. <Button type="primary" class="header-actions__upload" @click="handleUploadNavigate('single')"
  35. >清洗数据</Button
  36. >
  37. <Button type="primary" class="header-actions__govern" @click="startCalcTask">数据治理</Button>
  38. <span class="header-actions__divider" aria-hidden="true"></span>
  39. <Button class="header-actions__config" @click="handleOpenGovernConfig">治理设置</Button>
  40. </div>
  41. </div>
  42. <div v-show="activeTab === 'import'" class="page-layout">
  43. <aside class="left-panel">
  44. <div class="sidebar-content tree-scroll-shell">
  45. <BasicTree
  46. class="case-tree"
  47. :tree-data="treeData"
  48. :tree-data-simple-mode="false"
  49. :field-names="{ key: 'key', title: 'title', children: 'children' }"
  50. :search="true"
  51. :toolbar="true"
  52. :showIcon="true"
  53. expandAction="doubleclick"
  54. :default-expand-all="true"
  55. v-model:selectedKeys="selectedKeys"
  56. @select="handleTreeSelect"
  57. />
  58. </div>
  59. </aside>
  60. <section ref="rightPanelRef" class="right-panel">
  61. <div class="panel-header">
  62. <div>
  63. <div class="panel-title">数据结果列表</div>
  64. </div>
  65. <div class="table-actions">
  66. <TableSetting
  67. :key="resultTableSettingKey"
  68. class="table-setting-trigger"
  69. :setting="{ setting: true, size: true, redo: false, fullScreen: false }"
  70. />
  71. <Popover
  72. v-if="showCustomQueryButton"
  73. v-model:open="customQueryPopoverOpen"
  74. trigger="click"
  75. placement="bottomRight"
  76. :overlay-style="{ padding: '12px' }"
  77. >
  78. <template #content>
  79. <CustomSearch ref="customSearchRef" :table-column="columns" @search="handleCustomSearch" />
  80. </template>
  81. <Button type="primary">自定义查询</Button>
  82. </Popover>
  83. </div>
  84. </div>
  85. <div class="table-wrapper">
  86. <BasicTable
  87. :key="resultTableRenderKey"
  88. :columns="columns"
  89. :data-source="tableData"
  90. :pagination="false"
  91. :loading="loading"
  92. :show-table-setting="false"
  93. :table-setting="{ setting: true, size: true, redo: false, fullScreen: false }"
  94. :can-resize="true"
  95. :is-can-resize-parent="true"
  96. :resize-height-offset="32"
  97. :immediate="false"
  98. row-key="id"
  99. @change="handleResultTableChange"
  100. @register="handleResultTableRegister"
  101. />
  102. </div>
  103. <div class="table-footer">
  104. <div class="footer-right">
  105. <div class="total">共 {{ totalCount }} 条</div>
  106. <Select
  107. v-model:value="pageSize"
  108. class="page-size"
  109. :options="pageSizeOptions"
  110. :getPopupContainer="getPagePopupContainer"
  111. @change="handlePageSizeChange"
  112. />
  113. <Pagination
  114. v-model:current="currentPage"
  115. :total="totalCount"
  116. :page-size="pageSize"
  117. :show-size-changer="false"
  118. @change="handlePageChange"
  119. />
  120. <div class="jump">
  121. <span>前往</span>
  122. <Input v-model:value="jumpPage" class="jump-input" @pressEnter="handleJump" />
  123. <span>页</span>
  124. </div>
  125. </div>
  126. </div>
  127. </section>
  128. </div>
  129. <div v-show="activeTab === 'files'" class="file-panel">
  130. <div class="panel-header file-panel-header">
  131. <div>
  132. <div class="panel-title">导入文件管理</div>
  133. </div>
  134. </div>
  135. <div class="file-panel__table">
  136. <ImportList :active="activeTab === 'files'" />
  137. </div>
  138. </div>
  139. </div>
  140. <Modal
  141. v-model:open="governConfigVisible"
  142. title="治理配置"
  143. :centered="true"
  144. width="480px"
  145. :footer="null"
  146. wrapClassName="govern-config-modal"
  147. @cancel="governConfigVisible = false"
  148. >
  149. <div class="govern-config-panel">
  150. <Form layout="vertical" class="govern-config-form">
  151. <div class="govern-config-item">
  152. <Form.Item label="治理去掉重复数据">
  153. <RadioGroup v-model:value="governConfig.removeDuplicate" @change="scheduleGovernConfigSave">
  154. <Radio value="是">是</Radio>
  155. <Radio value="否">否</Radio>
  156. </RadioGroup>
  157. </Form.Item>
  158. </div>
  159. <div class="govern-config-item">
  160. <Form.Item>
  161. <template #label>
  162. <span class="govern-config-label">
  163. <span>通信关联开户销户时间</span>
  164. <Tooltip
  165. title="选择是后,则只分析对象开户时间之后的通信记录"
  166. :getPopupContainer="getModalPopupContainer"
  167. >
  168. <span class="config-tip-icon">?</span>
  169. </Tooltip>
  170. </span>
  171. </template>
  172. <RadioGroup v-model:value="governConfig.associateAccountTime" @change="scheduleGovernConfigSave">
  173. <Radio value="是">是</Radio>
  174. <Radio value="否">否</Radio>
  175. </RadioGroup>
  176. </Form.Item>
  177. </div>
  178. <div class="govern-config-item">
  179. <Form.Item>
  180. <template #label>
  181. <span class="govern-config-label">
  182. <span>导入数据立即治理</span>
  183. <Tooltip title="选择是后,导入数据后立即治理" :getPopupContainer="getModalPopupContainer">
  184. <span class="config-tip-icon">?</span>
  185. </Tooltip>
  186. </span>
  187. </template>
  188. <RadioGroup v-model:value="governConfig.skipGovernOnImport" @change="scheduleGovernConfigSave">
  189. <Radio value="是">是</Radio>
  190. <Radio value="否">否</Radio>
  191. </RadioGroup>
  192. </Form.Item>
  193. </div>
  194. </Form>
  195. </div>
  196. </Modal>
  197. <GovernProgressModal
  198. :open="governProgressVisible"
  199. @update:open="handleGovernProgressOpenChange"
  200. @finished="handleGovernTaskFinished"
  201. />
  202. <TransRecordModal v-model:open="transRecordModalOpen" title="交易记录" :base-query="transRecordBaseQuery" />
  203. <CallRecordModal v-model:open="callRecordModalOpen" title="通信记录" :base-query="callRecordBaseQuery" />
  204. </div>
  205. </template>
  206. <script lang="ts" setup name="ViewsDataIndex">
  207. import { computed, h, ref, nextTick, onActivated, onBeforeUnmount, onDeactivated, onMounted } from 'vue';
  208. import { Button, Select, Pagination, Input, Modal, Radio, Tooltip, Popover, Form, RadioGroup } from 'ant-design-vue';
  209. import { BasicTable, type BasicColumn } from '@/core/components/Table';
  210. import TableSetting from '@/core/components/Table/src/components/settings/index.vue';
  211. import { createTableContext } from '@/core/components/Table/src/hooks/useTableContext';
  212. import { BasicTree } from '@/core/components/Tree';
  213. import { useDesign } from '@/core/hooks/web/useDesign';
  214. import { useGo } from '@/core/hooks/web/usePage';
  215. import { PageEnum } from '@/core/enums/pageEnum';
  216. import { getModalPopupContainer as resolveModalPopupContainer } from '@/core/utils';
  217. import { useMessage } from '@/core/hooks/web/useMessage';
  218. import { useEmitter, useUserStore } from '@/core/store/modules/case';
  219. import { markAnalysisDataChanged } from '@/core/logics/analysisRefreshPrompt';
  220. import CustomSearch from './components/CustomSearch.vue';
  221. import GovernProgressModal from './components/GovernProgressModal.vue';
  222. import { getActiveImportProgress } from './importProgressSession';
  223. import {
  224. downloadOfficeFileById,
  225. getOpenOfficeFileErrorMessage,
  226. resolveOfficeFileId,
  227. } from '@/core/utils/openOfficeFile';
  228. import CallRecordModal from '@/call/views/components/CallRecordModal.vue';
  229. import TransRecordModal from '@/trans/views/components/TransRecordModal.vue';
  230. import {
  231. aiCleanDataPage,
  232. getGovernConfList,
  233. getTree,
  234. treeTablePage,
  235. getTreeTransPersonPage,
  236. getTreeTransBankPage,
  237. getTreeCallPersonPage,
  238. getTreeCallOperPage,
  239. saveGovernConf,
  240. } from '@/case/api/govern/governApi';
  241. import type { GovernConfItem } from '@/case/api/govern/governApi';
  242. import componentSetting from '@/core/settings/componentSetting';
  243. import ImportList from './importList.vue';
  244. import { DataTableDTO, GovernTree } from '@/case/api/govern/types/dto';
  245. import { GovernTreeQuery } from '@/case/api/govern/types/query';
  246. import type { CallRecordQuery } from '@/call/api/types/query';
  247. import type { TransRecordQuery } from '@/trans/api/types/query';
  248. const { prefixCls } = useDesign('case-data');
  249. const go = useGo();
  250. const emitter = useEmitter();
  251. const userStore = useUserStore();
  252. const CASE_DATA_REFRESH_EVENT = 'case-data:refresh';
  253. const CASE_IMPORT_FILE_REFRESH_EVENT = 'case-import-file:refresh';
  254. /**
  255. * 记录里「下载文件」需要的字段:fileId 回查后端取文件流,fileName 只用于展示与兜底命名。
  256. * 其余列由后端表头决定(同一行既可能是话单字段也可能是勘察字段),不参与判断。
  257. */
  258. interface DownloadableFileRecord {
  259. fileId?: ApiId | number | null;
  260. fileName?: ApiText | null;
  261. [column: string]: unknown;
  262. }
  263. interface TableItem {
  264. id: number;
  265. ownNumber: string;
  266. otherNumber: string;
  267. contactTime: string;
  268. otherName: string;
  269. customerName: string;
  270. lac: string;
  271. cellId: string;
  272. baseId: string;
  273. carrier: string;
  274. }
  275. interface GovernTreeNodeOption {
  276. title: string;
  277. key: string;
  278. level: number;
  279. dataRef: GovernTree;
  280. children: GovernTreeNodeOption[];
  281. }
  282. type GovernTreeBranchKind = 'call' | 'trans' | 'default';
  283. type GovernConfigValue = '是' | '否';
  284. interface GovernConfigFormState {
  285. removeDuplicate: GovernConfigValue;
  286. associateAccountTime: GovernConfigValue;
  287. skipGovernOnImport: GovernConfigValue;
  288. }
  289. type DataPageTabKey = 'import' | 'files';
  290. // 支持 ?tab=files 直达「导入文件」页签(仅作 URL 直连兜底;正常流程由清洗进度页经 sessionStorage 切换)
  291. const initialTab = new URLSearchParams(window.location.search).get('tab') === 'files' ? 'files' : 'import';
  292. const activeTab = ref<DataPageTabKey>(initialTab as DataPageTabKey);
  293. /** 清洗来源模式(?mode=smart|manual,仅作 URL 直连兜底):决定展示「进行智能分析」还是「查看结果」 */
  294. const readCleanMode = (): '' | 'smart' | 'manual' => {
  295. const mode = new URLSearchParams(window.location.search).get('mode');
  296. return mode === 'smart' || mode === 'manual' ? mode : '';
  297. };
  298. const cleanMode = ref<'' | 'smart' | 'manual'>(readCleanMode());
  299. /** 智能模式 CTA:去工作台新建对话,绑定默认研判方案进行分析 */
  300. const goSmartAnalysis = () => {
  301. go({ path: PageEnum.BASE_WORKBENCH, query: { newChat: '1' } });
  302. };
  303. const governConfigVisible = ref(false);
  304. const governProgressVisible = ref(false);
  305. const customQueryPopoverOpen = ref(false);
  306. const customSearchRef = ref<InstanceType<typeof CustomSearch> | null>(null);
  307. const customQueryParams = ref<any>(null);
  308. const saving = ref(false);
  309. const governConfigLoaded = ref(false);
  310. const createDefaultGovernConfig = (): GovernConfigFormState => ({
  311. removeDuplicate: '否',
  312. associateAccountTime: '否',
  313. skipGovernOnImport: '否',
  314. });
  315. const governConfig = ref<GovernConfigFormState>(createDefaultGovernConfig());
  316. const governConfigIdMap = ref<Record<string, string | number | undefined>>({});
  317. let governConfigSaveTimer: ReturnType<typeof setTimeout> | undefined;
  318. const { createMessage } = useMessage();
  319. const getPagePopupContainer = (triggerNode?: HTMLElement) =>
  320. (triggerNode?.closest('.case-data-page') as HTMLElement | null) || document.body;
  321. const getModalPopupContainer = (triggerNode?: HTMLElement) => resolveModalPopupContainer(triggerNode);
  322. const getTemplateCreatePopupContainer = () => document.body;
  323. let restoringImportProgress = false;
  324. const restoreActiveImportProgress = async () => {
  325. if (restoringImportProgress || !getActiveImportProgress(userStore.getCaseInfo?.id)) return;
  326. restoringImportProgress = true;
  327. try {
  328. await go(PageEnum.BASE_CLEAN_PROGRESS, true);
  329. } finally {
  330. restoringImportProgress = false;
  331. }
  332. };
  333. onActivated(() => {
  334. void restoreActiveImportProgress();
  335. // 页面被 keep-alive 缓存,重新激活时要重选页签。
  336. // 清洗进度页「查看结果」跳转不带任何 URL 参数,页签(导入文件)与清洗来源全走 sessionStorage
  337. // 一次性上下文;URL 参数只在直连(手输/收藏)时作兜底。
  338. let ctx: { tab?: string; mode?: string } | null = null;
  339. try {
  340. const raw = sessionStorage.getItem('case_clean_result_context');
  341. if (raw) {
  342. ctx = JSON.parse(raw);
  343. sessionStorage.removeItem('case_clean_result_context');
  344. }
  345. } catch (e) {
  346. ctx = null;
  347. }
  348. const tabQuery = (ctx && ctx.tab) || new URLSearchParams(window.location.search).get('tab');
  349. if (tabQuery === 'files' || tabQuery === 'import') {
  350. activeTab.value = tabQuery;
  351. }
  352. const modeQuery = (ctx && ctx.mode) || new URLSearchParams(window.location.search).get('mode');
  353. cleanMode.value = modeQuery === 'smart' || modeQuery === 'manual' ? modeQuery : '';
  354. });
  355. onDeactivated(() => {
  356. restoringImportProgress = false;
  357. });
  358. const handleUploadNavigate = (mode?: 'single' | 'folder') => {
  359. go({ path: PageEnum.BASE_UPLOAD, query: { mode: mode || 'single' } });
  360. };
  361. const getRequestErrorMessage = (error: any, fallback = '操作失败') =>
  362. error?.response?.data?.message || error?.message || fallback;
  363. const normalizeGovernConfValue = (value?: string | null): GovernConfigValue =>
  364. String(value ?? '')
  365. .trim()
  366. .toUpperCase() === 'Y'
  367. ? '是'
  368. : '否';
  369. const normalizeHasGovernToSkipValue = (value?: string | null): GovernConfigValue =>
  370. String(value ?? '')
  371. .trim()
  372. .toUpperCase() === 'Y'
  373. ? '是'
  374. : '否';
  375. const toGovernConfFlag = (value?: GovernConfigValue) => (value === '是' ? 'Y' : 'N');
  376. const toHasGovernFlag = (value?: GovernConfigValue) => (value === '是' ? 'Y' : 'N');
  377. const buildGovernConfPayloadItem = (confType: string, confStr: string): GovernConfItem => {
  378. const id = governConfigIdMap.value[confType];
  379. return id === undefined || id === null || String(id).trim() === ''
  380. ? { confType, confStr }
  381. : { id, confType, confStr };
  382. };
  383. const loadGovernConfig = async () => {
  384. const nextConfig = createDefaultGovernConfig();
  385. const nextIdMap: Record<string, string | number | undefined> = {};
  386. const list = await getGovernConfList();
  387. (Array.isArray(list) ? list : []).forEach((item) => {
  388. const confType = String(item?.confType ?? '').trim();
  389. if (!confType) return;
  390. nextIdMap[confType] = item?.id;
  391. if (confType === 'duplicate') {
  392. nextConfig.removeDuplicate = normalizeGovernConfValue(item?.confStr);
  393. } else if (confType === 'useOpenInfo') {
  394. nextConfig.associateAccountTime = normalizeGovernConfValue(item?.confStr);
  395. } else if (confType === 'hasGovern') {
  396. nextConfig.skipGovernOnImport = normalizeHasGovernToSkipValue(item?.confStr);
  397. }
  398. });
  399. governConfigIdMap.value = nextIdMap;
  400. governConfig.value = nextConfig;
  401. };
  402. const handleOpenGovernConfig = async () => {
  403. governConfigLoaded.value = false;
  404. governConfig.value = createDefaultGovernConfig();
  405. governConfigIdMap.value = {};
  406. governConfigVisible.value = true;
  407. try {
  408. await loadGovernConfig();
  409. } catch (error) {
  410. createMessage.error(getRequestErrorMessage(error, '获取治理配置失败'));
  411. } finally {
  412. governConfigLoaded.value = true;
  413. }
  414. };
  415. const handleSaveGovernConfig = async (options?: { closeAfterSave?: boolean; silentSuccess?: boolean }) => {
  416. const { closeAfterSave = true, silentSuccess = false } = options || {};
  417. saving.value = true;
  418. try {
  419. const payload: GovernConfItem[] = [
  420. buildGovernConfPayloadItem('duplicate', toGovernConfFlag(governConfig.value.removeDuplicate)),
  421. buildGovernConfPayloadItem('useOpenInfo', toGovernConfFlag(governConfig.value.associateAccountTime)),
  422. buildGovernConfPayloadItem('hasGovern', toHasGovernFlag(governConfig.value.skipGovernOnImport)),
  423. ];
  424. await saveGovernConf(payload);
  425. if (!silentSuccess) {
  426. createMessage.success('保存成功');
  427. }
  428. if (closeAfterSave) {
  429. governConfigVisible.value = false;
  430. }
  431. } catch (error) {
  432. createMessage.error(getRequestErrorMessage(error, '保存失败'));
  433. } finally {
  434. saving.value = false;
  435. }
  436. };
  437. const scheduleGovernConfigSave = () => {
  438. if (!governConfigLoaded.value) return;
  439. if (governConfigSaveTimer) {
  440. clearTimeout(governConfigSaveTimer);
  441. }
  442. governConfigSaveTimer = setTimeout(() => {
  443. governConfigSaveTimer = undefined;
  444. void handleSaveGovernConfig({ closeAfterSave: false, silentSuccess: true });
  445. }, 120);
  446. };
  447. const startCalcTask = () => {
  448. governProgressVisible.value = true;
  449. };
  450. const handleGovernTaskFinished = async () => {
  451. markAnalysisDataChanged();
  452. await fetchTreeData({ preserveActive: true });
  453. emitter.emit(CASE_IMPORT_FILE_REFRESH_EVENT);
  454. };
  455. const handleGovernProgressOpenChange = async (open: boolean) => {
  456. const wasOpen = governProgressVisible.value;
  457. governProgressVisible.value = open;
  458. if (!open && wasOpen) {
  459. await fetchTreeData({ preserveActive: true });
  460. }
  461. };
  462. const treeData = ref<GovernTreeNodeOption[]>([]);
  463. const columns = ref<BasicColumn[]>([]);
  464. const selectedKeys = ref<string[]>([]);
  465. const tableData = ref<TableItem[]>([]);
  466. const rightPanelRef = ref<HTMLElement | null>(null);
  467. const resultTableAction = ref<any>(null);
  468. const totalCount = ref(0);
  469. const loading = ref(false);
  470. const pageSize = ref(componentSetting.table.defaultPageSize);
  471. const lastPageSize = ref(pageSize.value);
  472. const currentPage = ref(1);
  473. const jumpPage = ref('1');
  474. const activeNode = ref<GovernTree | null>(null);
  475. const activeNodeType = ref<string | undefined>(undefined);
  476. const activeParentName = ref<string | undefined>(undefined);
  477. const activeNodeLevel = ref<number>(0);
  478. const activeBranchKind = ref<GovernTreeBranchKind>('default');
  479. const resultSortOrderKey = ref<string>('');
  480. const resultSortDirection = ref<'ASC' | 'DESC' | ''>('');
  481. const rawTableRecords = ref<Recordable[]>([]);
  482. const callRecordModalOpen = ref(false);
  483. const callRecordBaseQuery = ref<Partial<CallRecordQuery> & Record<string, any>>({});
  484. const transRecordModalOpen = ref(false);
  485. const transRecordBaseQuery = ref<Partial<TransRecordQuery> & Record<string, any>>({});
  486. const handleResultTableRegister = (action: any) => {
  487. resultTableAction.value = action;
  488. };
  489. const getResultTableColumns = (...args: any[]) => resultTableAction.value?.getColumns?.(...args) || [];
  490. const resultTableSettingKey = computed(() =>
  491. columns.value
  492. .map((column, index) => {
  493. const dataIndex = Array.isArray(column?.dataIndex)
  494. ? column.dataIndex.join('.')
  495. : String(column?.dataIndex ?? '').trim();
  496. const key = String(column?.key ?? '').trim();
  497. const title = String(column?.title ?? '').trim();
  498. return `${index}:${key || dataIndex || title}`;
  499. })
  500. .join('|'),
  501. );
  502. const resultTableRenderKey = computed(() =>
  503. [
  504. activeNodeLevel.value,
  505. activeNodeType.value || '',
  506. activeNode.value?.tableNameEn || activeNode.value?.tableNameCn || '',
  507. columns.value
  508. .map((column, index) => {
  509. const dataIndex = Array.isArray(column?.dataIndex)
  510. ? column.dataIndex.join('.')
  511. : String(column?.dataIndex ?? '').trim();
  512. const key = String(column?.key ?? '').trim();
  513. return `${index}:${key || dataIndex}:${column?.sorter ? 'S' : 'N'}:${column?.sortOrder || ''}`;
  514. })
  515. .join('|'),
  516. ].join('::'),
  517. );
  518. createTableContext({
  519. wrapRef: rightPanelRef,
  520. getBindValues: computed(() => ({
  521. columns: columns.value,
  522. showIndexColumn: true,
  523. rowSelection: undefined,
  524. isTreeTable: false,
  525. })),
  526. getDefaultRowSelection: () => resultTableAction.value?.getDefaultRowSelection?.(),
  527. getColumns: getResultTableColumns,
  528. getCacheColumns: () => resultTableAction.value?.getCacheColumns?.() || [],
  529. setColumns: (nextColumns: any) => resultTableAction.value?.setColumns?.(nextColumns),
  530. setProps: (props: any) => resultTableAction.value?.setProps?.(props),
  531. } as any);
  532. const clearTableState = () => {
  533. activeNode.value = null;
  534. activeNodeType.value = undefined;
  535. activeParentName.value = undefined;
  536. activeNodeLevel.value = 0;
  537. activeBranchKind.value = 'default';
  538. resultSortOrderKey.value = '';
  539. resultSortDirection.value = '';
  540. selectedKeys.value = [];
  541. rawTableRecords.value = [];
  542. tableData.value = [];
  543. totalCount.value = 0;
  544. currentPage.value = 1;
  545. jumpPage.value = '1';
  546. columns.value = defaultColumns.map((column) => applyResultSortProps(column));
  547. };
  548. const formatTreeTitle = (node: GovernTree) => {
  549. const name = node.tableNameCn ?? node.tableNameEn ?? '';
  550. const countText = typeof node.dataCount === 'number' ? ` (${node.dataCount})` : '';
  551. return `${name}${countText}` || `节点-${node.id ?? ''}`;
  552. };
  553. const normalizeGovernTreeKeyPart = (value: unknown) => {
  554. const text = String(value ?? '').trim();
  555. return (text || 'na').replace(/[|/]/g, '_');
  556. };
  557. const isSameGovernNode = (left?: GovernTree | null, right?: GovernTree | null) => {
  558. if (!left || !right) return false;
  559. return (
  560. String(left.id ?? '') === String(right.id ?? '') &&
  561. String(left.pid ?? '') === String(right.pid ?? '') &&
  562. String(left.type ?? '') === String(right.type ?? '') &&
  563. String(left.tableNameEn ?? '') === String(right.tableNameEn ?? '') &&
  564. String(left.tableNameCn ?? '') === String(right.tableNameCn ?? '')
  565. );
  566. };
  567. const buildGovernTreeNodeKey = (node: GovernTree, parentKey = 'root', index = 0) =>
  568. [
  569. parentKey,
  570. normalizeGovernTreeKeyPart(node.type),
  571. normalizeGovernTreeKeyPart(node.id),
  572. normalizeGovernTreeKeyPart(node.pid),
  573. normalizeGovernTreeKeyPart(node.tableNameEn ?? node.tableNameCn),
  574. String(index),
  575. ].join('/');
  576. const mapGovernTree = (nodes: GovernTree[] = [], level = 1, parentKey = 'root'): GovernTreeNodeOption[] =>
  577. nodes.map((node, index) => {
  578. const nodeKey = buildGovernTreeNodeKey(node, parentKey, index);
  579. return {
  580. title: formatTreeTitle(node),
  581. key: nodeKey,
  582. level,
  583. dataRef: node,
  584. children: mapGovernTree(node.children ?? [], level + 1, nodeKey),
  585. };
  586. });
  587. const flattenGovernNodes = (nodes: GovernTree[] = []): GovernTree[] =>
  588. nodes.reduce<GovernTree[]>((result, node) => {
  589. result.push(node);
  590. if (node.children?.length) {
  591. result.push(...flattenGovernNodes(node.children));
  592. }
  593. return result;
  594. }, []);
  595. const findGovernTreeSelectionByKey = (
  596. nodes: GovernTreeNodeOption[],
  597. key: string,
  598. parent?: GovernTreeNodeOption,
  599. ancestors: GovernTreeNodeOption[] = [],
  600. ): { node: GovernTreeNodeOption; parent?: GovernTreeNodeOption; ancestors: GovernTreeNodeOption[] } | undefined => {
  601. for (const item of nodes) {
  602. if (item.key === key) return { node: item, parent, ancestors };
  603. if (item.children?.length) {
  604. const matched = findGovernTreeSelectionByKey(item.children, key, item, [...ancestors, item]);
  605. if (matched) return matched;
  606. }
  607. }
  608. return undefined;
  609. };
  610. const resolveGovernTreeBranchKind = (node?: GovernTree | null): GovernTreeBranchKind => {
  611. const branchText = `${String(node?.tableNameCn ?? '')} ${String(node?.tableNameEn ?? '')}`.toLowerCase();
  612. if (
  613. branchText.includes('通讯') ||
  614. branchText.includes('通信') ||
  615. branchText.includes('电话') ||
  616. branchText.includes('通话') ||
  617. branchText.includes('短信') ||
  618. branchText.includes('运营商') ||
  619. branchText.includes('call') ||
  620. branchText.includes('sms') ||
  621. branchText.includes('oper')
  622. ) {
  623. return 'call';
  624. }
  625. if (
  626. branchText.includes('银行') ||
  627. branchText.includes('交易') ||
  628. branchText.includes('资金') ||
  629. branchText.includes('trans') ||
  630. branchText.includes('bank')
  631. ) {
  632. return 'trans';
  633. }
  634. return 'default';
  635. };
  636. const resolveGovernTreeKey = (target?: GovernTree | null, nodes: GovernTreeNodeOption[] = treeData.value): string => {
  637. if (!target) return '';
  638. for (const item of nodes) {
  639. if (isSameGovernNode(item.dataRef, target)) return item.key;
  640. if (item.children?.length) {
  641. const childKey = resolveGovernTreeKey(target, item.children);
  642. if (childKey) return childKey;
  643. }
  644. }
  645. return '';
  646. };
  647. const initTreeSelection = (nodes: GovernTreeNodeOption[]) => {
  648. if (!nodes.length) return;
  649. const firstKey = nodes[0]?.key;
  650. if (firstKey) {
  651. selectedKeys.value = [String(firstKey)];
  652. }
  653. };
  654. const templateCategoryOptions = ref([
  655. { label: '全部', value: '全部' },
  656. { label: '金融类', value: '金融类' },
  657. { label: '工商类', value: '工商类' },
  658. ]);
  659. const updateTemplateCategories = (nodes: GovernTree[] = []) => {
  660. const categories = new Set<string>();
  661. flattenGovernNodes(nodes).forEach((node) => {
  662. if (node.type) {
  663. categories.add(node.type);
  664. }
  665. });
  666. const dynamic = Array.from(categories).map((item) => ({ label: item, value: item }));
  667. templateCategoryOptions.value = [{ label: '全部', value: '全部' }, ...dynamic];
  668. };
  669. const toTableRows = (records: Recordable[]): TableItem[] =>
  670. records.map((item, index) => ({
  671. id: (item as any).id ?? index + 1,
  672. ownNumber: (item as any).personPhone ?? (item as any).phoneNumber ?? (item as any).ownNumber ?? '-',
  673. otherNumber: (item as any).otherPhone ?? (item as any).otherCardNo ?? (item as any).otherNumber ?? '-',
  674. contactTime: (item as any).contactTime ?? (item as any).callDate ?? (item as any).transDate ?? '-',
  675. otherName: (item as any).otherName ?? '-',
  676. customerName: (item as any).customerName ?? (item as any).personName ?? '-',
  677. lac: (item as any).lac ?? (item as any).lacCode ?? '-',
  678. cellId: (item as any).cellId ?? (item as any).cellid ?? '-',
  679. baseId: (item as any).baseId ?? (item as any).stationId ?? '-',
  680. carrier: (item as any).carrier ?? (item as any).operatorName ?? '-',
  681. }));
  682. const extractTablePayload = (payload: DataTableDTO) => {
  683. if (payload.pages) {
  684. const pages = payload.pages as any;
  685. const total = Number(pages.total ?? pages.count ?? 0);
  686. return {
  687. list: pages.records ?? [],
  688. total,
  689. head: payload.head,
  690. };
  691. }
  692. if (Array.isArray(payload.list)) {
  693. return {
  694. list: payload.list ?? [],
  695. total: Number((payload as any).total ?? (payload as any).count ?? payload.list?.length ?? 0),
  696. head: payload.head,
  697. };
  698. }
  699. return {
  700. list: [],
  701. total: 0,
  702. head: payload.head,
  703. };
  704. };
  705. const normalizeDetailQueryText = (value?: string | number | null) => {
  706. const text = String(value ?? '').trim();
  707. if (!text || text === '-' || text === 'null' || text === 'undefined') return undefined;
  708. return text;
  709. };
  710. const toSingleQueryArray = (value?: string | number | null) => {
  711. const text = normalizeDetailQueryText(value);
  712. return text ? [text] : undefined;
  713. };
  714. const isTradeCountColumn = (column: BasicColumn) => {
  715. const title = String(column?.title ?? '').trim();
  716. const key = String(column?.key ?? '').trim();
  717. const dataIndex = String(column?.dataIndex ?? '').trim();
  718. return title === '交易总次数' || key === 'transCount' || dataIndex === 'transCount';
  719. };
  720. const resolveCallCountColumnDirection = (column: BasicColumn) => {
  721. const title = String(column?.title ?? '').trim();
  722. const key = String(column?.key ?? '')
  723. .trim()
  724. .toLowerCase();
  725. const dataIndex = String(column?.dataIndex ?? '')
  726. .trim()
  727. .toLowerCase();
  728. if (
  729. title === '主叫总次数' ||
  730. key.includes('maincall') ||
  731. key.includes('outcall') ||
  732. dataIndex.includes('maincall')
  733. ) {
  734. return 'CALL_OUT';
  735. }
  736. if (
  737. title === '被叫总次数' ||
  738. key.includes('calledcall') ||
  739. key.includes('incall') ||
  740. dataIndex.includes('calledcall')
  741. ) {
  742. return 'CALL_IN';
  743. }
  744. return undefined;
  745. };
  746. const getCallCountColumnDrilldown = (column: BasicColumn) => {
  747. const title = String(column?.title ?? '').trim();
  748. const key = String(column?.key ?? '')
  749. .trim()
  750. .toLowerCase();
  751. const dataIndex = String(column?.dataIndex ?? '')
  752. .trim()
  753. .toLowerCase();
  754. const isCallCountColumn =
  755. title === '通话总次数' ||
  756. key === 'callcount' ||
  757. key === 'totalcallcount' ||
  758. dataIndex === 'callcount' ||
  759. dataIndex === 'totalcallcount';
  760. if (isCallCountColumn) {
  761. return {
  762. direction: resolveCallCountColumnDirection(column),
  763. };
  764. }
  765. const currentNodeLabel = `${activeParentName.value ?? ''} ${activeNode.value?.tableNameCn ?? ''} ${
  766. activeNode.value?.tableNameEn ?? ''
  767. }`
  768. .toLowerCase()
  769. .trim();
  770. const isSmsContext = currentNodeLabel.includes('短信') || currentNodeLabel.includes('sms');
  771. const isSmsContactCountColumn =
  772. title === '联系总次数' ||
  773. key === 'contactcount' ||
  774. key === 'smscount' ||
  775. key === 'totalsmscount' ||
  776. dataIndex === 'contactcount' ||
  777. dataIndex === 'smscount' ||
  778. dataIndex === 'totalsmscount';
  779. if (!isSmsContext || !isSmsContactCountColumn) return undefined;
  780. return {
  781. smsOnly: true,
  782. };
  783. };
  784. const canOpenTradeRecordModal = () => ['trans_person', 'bank'].includes(String(activeNodeType.value ?? ''));
  785. const canOpenCallRecordModal = () => ['call_person', 'yys'].includes(String(activeNodeType.value ?? ''));
  786. const shouldAppendOpenFileActionColumn = () => activeNodeLevel.value === 1 || activeNodeLevel.value === 2;
  787. const openTradeRecordModal = (record: Recordable) => {
  788. if (!record || !canOpenTradeRecordModal()) return;
  789. const personName = record.personName ?? activeParentName.value ?? activeNode.value?.tableNameCn;
  790. const personCardNo = record.personCardNo ?? record.personAccount ?? record.cardNo;
  791. const bankName = activeNodeType.value === 'bank' ? activeNode.value?.tableNameCn : record.bankName;
  792. const transCount = Number(record.transCount ?? record['交易总次数']);
  793. transRecordBaseQuery.value = {
  794. personName: personName || undefined,
  795. personCardNo: personCardNo || undefined,
  796. bankName: bankName || undefined,
  797. otherName: record.otherName || undefined,
  798. otherCardNo: record.otherCardNo || undefined,
  799. transCount: Number.isFinite(transCount) ? transCount : undefined,
  800. };
  801. transRecordModalOpen.value = true;
  802. };
  803. const openCallRecordModalByColumn = (record: Recordable, column: BasicColumn) => {
  804. if (!record || !canOpenCallRecordModal()) return;
  805. const personName =
  806. normalizeDetailQueryText(record.personName ?? record.customerName) ||
  807. normalizeDetailQueryText(activeParentName.value) ||
  808. normalizeDetailQueryText(activeNode.value?.tableNameCn);
  809. const personPhone = normalizeDetailQueryText(record.personPhone ?? record.phoneNumber ?? record.ownNumber);
  810. const otherName = normalizeDetailQueryText(record.otherName);
  811. const otherPhone = normalizeDetailQueryText(record.otherPhone ?? record.otherNumber);
  812. const operatorId =
  813. normalizeDetailQueryText(record.operatorId ?? record.operName ?? record.operatorName) ||
  814. (activeNodeType.value === 'yys' ? normalizeDetailQueryText(activeNode.value?.tableNameCn) : undefined);
  815. const drilldown = getCallCountColumnDrilldown(column);
  816. const nodeTableName = normalizeDetailQueryText(activeNode.value?.tableNameEn);
  817. const nodeTableNameCn = normalizeDetailQueryText(activeNode.value?.tableNameCn);
  818. const nodeType = normalizeDetailQueryText(activeNodeType.value);
  819. callRecordBaseQuery.value = {
  820. personName,
  821. personPhones: toSingleQueryArray(personPhone),
  822. otherName,
  823. otherPhones: toSingleQueryArray(otherPhone),
  824. operatorId,
  825. callDirection: drilldown?.direction,
  826. tableName: nodeTableName,
  827. tableNameCn: nodeTableNameCn,
  828. type: nodeType,
  829. personType: activeNodeType.value === 'call_person' ? nodeType : undefined,
  830. operName: activeNodeType.value === 'yys' ? nodeTableNameCn : undefined,
  831. smsOnly: drilldown?.smsOnly,
  832. };
  833. callRecordModalOpen.value = true;
  834. };
  835. /**
  836. * 下载记录关联的源文件(Web 化:只传文件ID,服务端查库定位文件流,浏览器另存)。
  837. *
  838. * 下载落盘走统一出口(桌面端由下载管理器写到你配置的目录,见 core/utils/file/download.ts)。
  839. */
  840. const handleDownloadRecordFile = async (record: DownloadableFileRecord) => {
  841. const fileId = resolveOfficeFileId(record);
  842. if (!fileId) {
  843. createMessage.warning('当前记录没有关联的源文件,无法下载');
  844. return;
  845. }
  846. try {
  847. await downloadOfficeFileById(fileId, String(record.fileName ?? '').trim() || '文件');
  848. } catch (error) {
  849. createMessage.error(getOpenOfficeFileErrorMessage(error, '文件下载失败'));
  850. }
  851. };
  852. const createOpenFileActionColumn = (): BasicColumn => ({
  853. title: '操作',
  854. dataIndex: 'openFileAction',
  855. key: 'openFileAction',
  856. width: 120,
  857. fixed: 'right',
  858. align: 'center',
  859. customRender: ({ record }: { record: DownloadableFileRecord }) => {
  860. // 不按 fileId 置灰:汇总行没有来源文件时由 handleDownloadRecordFile 明确提示原因
  861. return h(
  862. Button,
  863. {
  864. type: 'link',
  865. size: 'small',
  866. onClick: (event: MouseEvent) => {
  867. event.stopPropagation();
  868. void handleDownloadRecordFile(record);
  869. },
  870. },
  871. { default: () => '下载文件' },
  872. );
  873. },
  874. });
  875. const getDrilldownDisplayText = (record: Recordable, column: BasicColumn, text: unknown) => {
  876. const dataIndex = typeof column?.dataIndex === 'string' ? column.dataIndex : '';
  877. const key = typeof column?.key === 'string' ? column.key : '';
  878. const title = String(column?.title ?? '').trim();
  879. const candidateValues = [
  880. text,
  881. dataIndex ? (record as any)?.[dataIndex] : undefined,
  882. key ? (record as any)?.[key] : undefined,
  883. title ? (record as any)?.[title] : undefined,
  884. ];
  885. for (const item of candidateValues) {
  886. if (item === 0) return '0';
  887. if (item === null || item === undefined) continue;
  888. if (typeof item === 'object') continue;
  889. const normalized = String(item).trim();
  890. if (normalized) return normalized;
  891. }
  892. return '-';
  893. };
  894. const normalizeResultColumnDataIndex = (column: BasicColumn, fallbackIndex: number) => {
  895. if (Array.isArray(column?.dataIndex) && column.dataIndex.length) {
  896. return column.dataIndex;
  897. }
  898. const dataIndex = typeof column?.dataIndex === 'string' ? column.dataIndex.trim() : '';
  899. if (dataIndex) return dataIndex;
  900. const titleText = typeof column?.title === 'string' ? column.title.trim() : '';
  901. if (titleText) return titleText;
  902. return `column_${fallbackIndex + 1}`;
  903. };
  904. const normalizeResultColumns = (head: BasicColumn[] = [], parentKey = 'column') =>
  905. head.map((column, index) => {
  906. const resolvedDataIndex = normalizeResultColumnDataIndex(column, index);
  907. const fallbackKey =
  908. `${parentKey}_${index + 1}_` +
  909. String(Array.isArray(resolvedDataIndex) ? resolvedDataIndex.join('_') : resolvedDataIndex)
  910. .trim()
  911. .replace(/\s+/g, '_');
  912. const resolvedKey = (typeof column?.key === 'string' && column.key.trim()) || fallbackKey;
  913. return {
  914. ...column,
  915. dataIndex: resolvedDataIndex,
  916. key: resolvedKey,
  917. children: Array.isArray(column?.children)
  918. ? normalizeResultColumns(column.children as BasicColumn[], resolvedKey)
  919. : column.children,
  920. };
  921. });
  922. const resolveSortableColumnField = (column: BasicColumn) => {
  923. if (typeof column?.dataIndex === 'string' && column.dataIndex.trim()) return column.dataIndex.trim();
  924. if (Array.isArray(column?.dataIndex) && column.dataIndex.length) return String(column.dataIndex[0] ?? '').trim();
  925. return String(column?.key ?? '').trim();
  926. };
  927. const getColumnSearchText = (column: BasicColumn) => {
  928. const title = String(column?.title ?? '')
  929. .trim()
  930. .toLowerCase();
  931. const key = String(column?.key ?? '')
  932. .trim()
  933. .toLowerCase();
  934. const dataIndex = Array.isArray(column?.dataIndex)
  935. ? column.dataIndex.join('_').trim().toLowerCase()
  936. : String(column?.dataIndex ?? '')
  937. .trim()
  938. .toLowerCase();
  939. return `${title}|${key}|${dataIndex}`;
  940. };
  941. const isSecondLevelCallNodeSortable = () =>
  942. activeNodeLevel.value === 2 && String(activeNodeType.value ?? '') === 'call_person';
  943. const isSecondLevelTradeNodeSortable = () =>
  944. activeNodeLevel.value === 2 && String(activeNodeType.value ?? '') === 'trans_person';
  945. const isTimeSortColumn = (column: BasicColumn) => {
  946. const text = getColumnSearchText(column);
  947. return (
  948. text.includes('时间') ||
  949. text.includes('日期') ||
  950. text.includes('datetime') ||
  951. text.includes('date') ||
  952. text.includes('time')
  953. );
  954. };
  955. const isAmountSortColumn = (column: BasicColumn) => {
  956. const text = getColumnSearchText(column);
  957. return (
  958. text.includes('金额') ||
  959. text.includes('余额') ||
  960. text.includes('amount') ||
  961. text.includes('money') ||
  962. text.includes('balance')
  963. );
  964. };
  965. const shouldEnableResultSorter = (column: BasicColumn) => {
  966. if (isSecondLevelCallNodeSortable()) {
  967. return isTimeSortColumn(column);
  968. }
  969. if (isSecondLevelTradeNodeSortable()) {
  970. return isAmountSortColumn(column) || isTimeSortColumn(column);
  971. }
  972. return false;
  973. };
  974. const toggleResultSort = (field: string) => {
  975. if (!field) return;
  976. if (resultSortOrderKey.value !== field) {
  977. resultSortOrderKey.value = field;
  978. resultSortDirection.value = 'DESC';
  979. } else if (resultSortDirection.value === 'DESC') {
  980. resultSortDirection.value = 'ASC';
  981. } else if (resultSortDirection.value === 'ASC') {
  982. resultSortOrderKey.value = '';
  983. resultSortDirection.value = '';
  984. } else {
  985. resultSortDirection.value = 'DESC';
  986. }
  987. currentPage.value = 1;
  988. jumpPage.value = '1';
  989. fetchByActiveNode();
  990. };
  991. const applyResultSortProps = (column: BasicColumn): BasicColumn => {
  992. const nextColumn: BasicColumn = {
  993. ...column,
  994. children: Array.isArray(column?.children)
  995. ? (column.children as BasicColumn[]).map((child) => applyResultSortProps(child))
  996. : column.children,
  997. };
  998. if (nextColumn.children?.length) {
  999. return nextColumn;
  1000. }
  1001. if (!shouldEnableResultSorter(nextColumn)) {
  1002. return nextColumn;
  1003. }
  1004. const sortField = resolveSortableColumnField(nextColumn);
  1005. const currentSortOrder =
  1006. sortField && sortField === resultSortOrderKey.value
  1007. ? resultSortDirection.value === 'ASC'
  1008. ? 'ascend'
  1009. : resultSortDirection.value === 'DESC'
  1010. ? 'descend'
  1011. : undefined
  1012. : undefined;
  1013. const customHeaderCell = nextColumn.customHeaderCell;
  1014. return {
  1015. ...nextColumn,
  1016. ellipsis: false,
  1017. sorter: true,
  1018. sortOrder: currentSortOrder,
  1019. customHeaderCell: (col: any) => {
  1020. const baseCell = typeof customHeaderCell === 'function' ? customHeaderCell(col) : {};
  1021. return {
  1022. ...(baseCell || {}),
  1023. class: [baseCell?.class, 'case-data-sortable-header'].filter(Boolean).join(' '),
  1024. onClick: (event: any) => {
  1025. if (typeof baseCell?.onClick === 'function') {
  1026. baseCell.onClick(event);
  1027. }
  1028. event.stopPropagation();
  1029. toggleResultSort(sortField);
  1030. },
  1031. };
  1032. },
  1033. };
  1034. };
  1035. const enhanceColumns = (head: BasicColumn[]) => {
  1036. const normalizedColumns = normalizeResultColumns(head ?? []).map((column) => {
  1037. const callDrilldown = getCallCountColumnDrilldown(column);
  1038. const sortableColumn = applyResultSortProps(column);
  1039. const isActionColumn = String(sortableColumn?.title ?? '').trim() === '操作';
  1040. if (isActionColumn) {
  1041. return {
  1042. ...sortableColumn,
  1043. width: Math.max(Number(sortableColumn.width ?? 0) || 0, 180),
  1044. customRender: ({ record, text }: { record: DownloadableFileRecord; text?: unknown }) => {
  1045. const rawDisplayText = text && typeof text !== 'object' ? String(text).trim() : text === 0 ? '0' : '';
  1046. return h(
  1047. 'div',
  1048. {
  1049. style: {
  1050. display: 'inline-flex',
  1051. alignItems: 'center',
  1052. gap: '8px',
  1053. justifyContent: 'center',
  1054. flexWrap: 'wrap',
  1055. },
  1056. },
  1057. [
  1058. rawDisplayText ? h('span', rawDisplayText) : null,
  1059. // 不按 fileId 置灰:汇总行没有来源文件时由 handleDownloadRecordFile 明确提示原因
  1060. h(
  1061. Button,
  1062. {
  1063. type: 'link',
  1064. size: 'small',
  1065. onClick: (event: MouseEvent) => {
  1066. event.stopPropagation();
  1067. void handleDownloadRecordFile(record);
  1068. },
  1069. },
  1070. { default: () => '下载文件' },
  1071. ),
  1072. ],
  1073. );
  1074. },
  1075. };
  1076. }
  1077. if (!isTradeCountColumn(column) && !callDrilldown) return sortableColumn;
  1078. return {
  1079. ...sortableColumn,
  1080. customRender: ({ record, text }: any) => {
  1081. const canOpenTrade = isTradeCountColumn(column) && canOpenTradeRecordModal();
  1082. const canOpenCall = !!callDrilldown && canOpenCallRecordModal();
  1083. const displayText = getDrilldownDisplayText(record, column, text);
  1084. if (!canOpenTrade && !canOpenCall) {
  1085. return displayText;
  1086. }
  1087. return h(
  1088. Button,
  1089. {
  1090. type: 'link',
  1091. size: 'small',
  1092. onClick: (event: MouseEvent) => {
  1093. event.stopPropagation();
  1094. if (isTradeCountColumn(column)) {
  1095. openTradeRecordModal(record);
  1096. return;
  1097. }
  1098. openCallRecordModalByColumn(record, column);
  1099. },
  1100. },
  1101. { default: () => displayText },
  1102. );
  1103. },
  1104. };
  1105. });
  1106. if (!shouldAppendOpenFileActionColumn()) {
  1107. return normalizedColumns;
  1108. }
  1109. const hasActionColumn = normalizedColumns.some((column) => String(column?.title ?? '').trim() === '操作');
  1110. return hasActionColumn ? normalizedColumns : [...normalizedColumns, createOpenFileActionColumn()];
  1111. };
  1112. const applyTableData = (payload: DataTableDTO) => {
  1113. const { list, total, head } = extractTablePayload(payload);
  1114. rawTableRecords.value = Array.isArray(list) ? list : [];
  1115. // 优先使用API返回的表头数据
  1116. if (head?.length) {
  1117. columns.value = enhanceColumns(head);
  1118. // 如果有表头数据,直接使用原始记录
  1119. tableData.value = rawTableRecords.value as TableItem[];
  1120. } else {
  1121. // 如果没有表头数据,使用默认列并转换数据格式
  1122. const baseColumns = defaultColumns.map((column) => applyResultSortProps(column));
  1123. columns.value = shouldAppendOpenFileActionColumn() ? [...baseColumns, createOpenFileActionColumn()] : baseColumns;
  1124. tableData.value = toTableRows(rawTableRecords.value);
  1125. }
  1126. // 设置总条数
  1127. totalCount.value = total ?? 0;
  1128. // 确保jumpPage与currentPage同步
  1129. jumpPage.value = currentPage.value.toString();
  1130. void nextTick(() => {
  1131. resultTableAction.value?.setColumns?.(columns.value);
  1132. });
  1133. };
  1134. const handleResultTableChange = () => {};
  1135. const buildResultSortQuery = () =>
  1136. resultSortOrderKey.value && resultSortDirection.value
  1137. ? {
  1138. orderKey: resultSortOrderKey.value,
  1139. sort: resultSortDirection.value,
  1140. }
  1141. : {};
  1142. const fetchByNode = (node?: GovernTree, resetPage = true) => {
  1143. if (!node) return;
  1144. activeNode.value = node;
  1145. activeNodeType.value = node.type;
  1146. activeParentName.value = undefined;
  1147. activeNodeLevel.value = 1;
  1148. const selectedKey = resolveGovernTreeKey(node);
  1149. selectedKeys.value = selectedKey ? [selectedKey] : [];
  1150. if (resetPage) {
  1151. currentPage.value = 1;
  1152. }
  1153. fetchTableData({
  1154. page: currentPage.value,
  1155. limit: pageSize.value,
  1156. tableName: node.tableNameEn,
  1157. pid: node.pid,
  1158. type: node.type,
  1159. personName: node.tableNameCn,
  1160. conditionSql: customQueryParams.value ?? undefined,
  1161. ...buildResultSortQuery(),
  1162. });
  1163. };
  1164. /** 治理树上 id<0 的节点是 AI 动态表(真实 id 取负),既有 /gt/treeTablePage 走实体注册查不到它 */
  1165. const isAiTableNode = (node?: GovernTree | null) => Number(node?.id ?? 0) < 0;
  1166. /**
  1167. * AI 动态表对应的 ai_clean_template.id(字符串)。
  1168. * 不能用 -Number(node.id):雪花 id 是 19 位,超过 JS 安全整数,JSON number 解析时就已经丢精度
  1169. * (实测 ...0003968 会变成 ...0004000),回传后端就查不到表。后端已额外下发字符串版 aiTemplateId。
  1170. */
  1171. const resolveAiTemplateId = (node?: GovernTree | null): string => {
  1172. const precise = String(node?.aiTemplateId ?? '').trim();
  1173. if (precise) {
  1174. return precise;
  1175. }
  1176. // 兜底:从物理表名反解(ai_clean_{id} / ai_t{id})
  1177. const matched = /^ai_(?:clean_|t)(\d+)$/.exec(String(node?.tableNameEn ?? '').trim());
  1178. if (matched?.[1]) {
  1179. return matched[1];
  1180. }
  1181. return String(node?.id ?? '').replace(/^-/, '');
  1182. };
  1183. const fetchTableData = async (params: GovernTreeQuery = {}) => {
  1184. loading.value = true;
  1185. try {
  1186. // 翻页/排序/搜索都从这一个漏斗走,所以分流也放在这里:只换请求函数,
  1187. // 表头与分页仍由 applyTableData 统一处理(/aiClean/data/page 的返回结构与 treeTablePage 一致)
  1188. const aiParams: {
  1189. aiTemplateId: string;
  1190. orderKey?: string;
  1191. asc?: boolean;
  1192. page?: number;
  1193. size?: number;
  1194. } = {
  1195. aiTemplateId: resolveAiTemplateId(activeNode.value),
  1196. page: params.page,
  1197. size: params.limit,
  1198. };
  1199. // 没排序时不要给 orderKey 占位:请求层会把空值序列化成字符串 "null",
  1200. // 后端拼成 ORDER BY null 会让整页查询直接失败(返空)
  1201. if (params.orderKey && params.sort) {
  1202. aiParams.orderKey = params.orderKey;
  1203. aiParams.asc = params.sort !== 'DESC';
  1204. }
  1205. const res = isAiTableNode(activeNode.value) ? await aiCleanDataPage(aiParams) : await treeTablePage(params);
  1206. applyTableData(res);
  1207. } catch (error) {
  1208. console.error('获取治理树表格数据失败', error);
  1209. tableData.value = [];
  1210. totalCount.value = 0;
  1211. } finally {
  1212. loading.value = false;
  1213. }
  1214. };
  1215. const fetchSecondNodeTrans = async (node?: GovernTree, resetPage = true) => {
  1216. if (!node) return;
  1217. activeNode.value = node;
  1218. activeNodeType.value = node.type;
  1219. activeParentName.value = undefined;
  1220. activeNodeLevel.value = 2;
  1221. const selectedKey = resolveGovernTreeKey(node);
  1222. selectedKeys.value = selectedKey ? [selectedKey] : [];
  1223. if (resetPage) {
  1224. currentPage.value = 1;
  1225. }
  1226. loading.value = true;
  1227. const params = {
  1228. page: currentPage.value,
  1229. limit: pageSize.value,
  1230. tableName: node.tableNameEn,
  1231. pid: node.pid,
  1232. type: node.type,
  1233. personName: node.tableNameCn,
  1234. conditionSql: customQueryParams.value ?? undefined,
  1235. ...buildResultSortQuery(),
  1236. };
  1237. try {
  1238. const res = await getTreeTransPersonPage(params);
  1239. applyTableData(res);
  1240. } catch (error) {
  1241. console.error('获取治理树表格数据失败', error);
  1242. tableData.value = [];
  1243. totalCount.value = tableData.value.length;
  1244. } finally {
  1245. loading.value = false;
  1246. }
  1247. };
  1248. const fetchTrirdNodeTrans = async (node?: GovernTree, parentName?: string, resetPage = true) => {
  1249. if (!node) return;
  1250. activeNode.value = node;
  1251. activeNodeType.value = node.type;
  1252. activeParentName.value = parentName;
  1253. activeNodeLevel.value = 3;
  1254. const selectedKey = resolveGovernTreeKey(node);
  1255. selectedKeys.value = selectedKey ? [selectedKey] : [];
  1256. if (resetPage) {
  1257. currentPage.value = 1;
  1258. }
  1259. loading.value = true;
  1260. const params = {
  1261. page: currentPage.value,
  1262. limit: pageSize.value,
  1263. tableName: node.tableNameEn,
  1264. pid: node.pid,
  1265. type: node.type,
  1266. personName: parentName,
  1267. bankName: node.tableNameCn,
  1268. conditionSql: customQueryParams.value ?? undefined,
  1269. ...buildResultSortQuery(),
  1270. };
  1271. try {
  1272. const res = await getTreeTransBankPage(params);
  1273. applyTableData(res);
  1274. } catch (error) {
  1275. console.error('获取治理树表格数据失败', error);
  1276. tableData.value = [];
  1277. totalCount.value = 0;
  1278. } finally {
  1279. loading.value = false;
  1280. }
  1281. };
  1282. const fetchSecondNodeCall = async (node?: GovernTree, resetPage = true) => {
  1283. if (!node) return;
  1284. activeNode.value = node;
  1285. activeNodeType.value = node.type;
  1286. activeParentName.value = undefined;
  1287. activeNodeLevel.value = 2;
  1288. const selectedKey = resolveGovernTreeKey(node);
  1289. selectedKeys.value = selectedKey ? [selectedKey] : [];
  1290. if (resetPage) {
  1291. currentPage.value = 1;
  1292. }
  1293. loading.value = true;
  1294. const params = {
  1295. page: currentPage.value,
  1296. limit: pageSize.value,
  1297. tableName: node.tableNameEn,
  1298. pid: node.pid,
  1299. type: node.type,
  1300. personName: node.tableNameCn,
  1301. personType: node.type,
  1302. conditionSql: customQueryParams.value ?? undefined,
  1303. ...buildResultSortQuery(),
  1304. };
  1305. try {
  1306. const res = await getTreeCallPersonPage(params);
  1307. applyTableData(res);
  1308. } catch (error) {
  1309. console.error('获取治理树表格数据失败', error);
  1310. tableData.value = [];
  1311. totalCount.value = 0;
  1312. } finally {
  1313. loading.value = false;
  1314. }
  1315. };
  1316. const fetchTrirdNodeCall = async (node?: GovernTree, parentName?: string, resetPage = true) => {
  1317. if (!node) return;
  1318. activeNode.value = node;
  1319. activeNodeType.value = node.type;
  1320. activeParentName.value = parentName;
  1321. activeNodeLevel.value = 3;
  1322. const selectedKey = resolveGovernTreeKey(node);
  1323. selectedKeys.value = selectedKey ? [selectedKey] : [];
  1324. if (resetPage) {
  1325. currentPage.value = 1;
  1326. }
  1327. loading.value = true;
  1328. const params = {
  1329. page: currentPage.value,
  1330. limit: pageSize.value,
  1331. tableName: node.tableNameEn,
  1332. pid: node.pid,
  1333. type: node.type,
  1334. personName: parentName,
  1335. operName: node.tableNameCn,
  1336. conditionSql: customQueryParams.value ?? undefined,
  1337. ...buildResultSortQuery(),
  1338. };
  1339. try {
  1340. const res = await getTreeCallOperPage(params);
  1341. applyTableData(res);
  1342. } catch (error) {
  1343. console.error('获取治理树表格数据失败', error);
  1344. tableData.value = [];
  1345. totalCount.value = 0;
  1346. } finally {
  1347. loading.value = false;
  1348. }
  1349. };
  1350. const fetchByTreeSelection = (
  1351. node?: GovernTree,
  1352. level = 1,
  1353. parentName?: string,
  1354. branchKind: GovernTreeBranchKind = 'default',
  1355. resetPage = true,
  1356. ) => {
  1357. if (!node) return;
  1358. activeBranchKind.value = branchKind;
  1359. if (level <= 1) {
  1360. fetchByNode(node, resetPage);
  1361. return;
  1362. }
  1363. if (level === 2) {
  1364. if (branchKind === 'call') {
  1365. fetchSecondNodeCall(node, resetPage);
  1366. } else if (branchKind === 'trans') {
  1367. fetchSecondNodeTrans(node, resetPage);
  1368. } else {
  1369. fetchByNode(node, resetPage);
  1370. }
  1371. return;
  1372. }
  1373. if (branchKind === 'call') {
  1374. fetchTrirdNodeCall(node, parentName, resetPage);
  1375. } else if (branchKind === 'trans') {
  1376. fetchTrirdNodeTrans(node, parentName, resetPage);
  1377. } else {
  1378. fetchByNode(node, resetPage);
  1379. }
  1380. };
  1381. const handleTreeSelect = (keys: Array<string | number>) => {
  1382. if (!keys.length) return;
  1383. // 切换节点时清空自定义查询条件
  1384. customQueryParams.value = null;
  1385. customSearchRef.value?.handleReset({ silent: true });
  1386. resultSortOrderKey.value = '';
  1387. resultSortDirection.value = '';
  1388. const selectedKey = String(keys[0] ?? '');
  1389. const matchedSelection = findGovernTreeSelectionByKey(treeData.value, selectedKey);
  1390. const node = matchedSelection?.node?.dataRef;
  1391. const level = matchedSelection?.node?.level ?? 1;
  1392. const parentName = matchedSelection?.parent?.dataRef?.tableNameCn as string | undefined;
  1393. const rootNode = matchedSelection?.ancestors?.[0]?.dataRef ?? node;
  1394. const branchKind = resolveGovernTreeBranchKind(rootNode);
  1395. fetchByTreeSelection(node, level, parentName, branchKind);
  1396. };
  1397. const fetchByActiveNode = () => {
  1398. if (!activeNode.value) return;
  1399. fetchByTreeSelection(
  1400. activeNode.value,
  1401. activeNodeLevel.value || 1,
  1402. activeParentName.value,
  1403. activeBranchKind.value,
  1404. false,
  1405. );
  1406. };
  1407. const handlePageChange = (page: number) => {
  1408. currentPage.value = page;
  1409. jumpPage.value = page.toString(); // 同步跳转页码输入框
  1410. fetchByActiveNode();
  1411. };
  1412. const handleJump = () => {
  1413. let target = Math.max(1, Number(jumpPage.value) || 1);
  1414. // 计算最大页数
  1415. const maxPage = Math.max(1, Math.ceil(totalCount.value / pageSize.value));
  1416. if (target > maxPage) {
  1417. target = maxPage;
  1418. jumpPage.value = target.toString();
  1419. }
  1420. if (target !== currentPage.value) {
  1421. currentPage.value = target;
  1422. fetchByActiveNode();
  1423. }
  1424. };
  1425. const handlePageSizeChange = (value: any) => {
  1426. const nextValue = Number(value ?? lastPageSize.value);
  1427. if (!Number.isFinite(nextValue) || nextValue === lastPageSize.value) return;
  1428. lastPageSize.value = nextValue;
  1429. pageSize.value = nextValue;
  1430. currentPage.value = 1;
  1431. jumpPage.value = '1'; // 重置跳转页码
  1432. // 如果总条数小于等于每页条数,重置到第一页
  1433. if (totalCount.value <= nextValue) {
  1434. currentPage.value = 1;
  1435. }
  1436. fetchByActiveNode();
  1437. };
  1438. const showCustomQueryButton = computed(() => {
  1439. return columns.value.length > 0 && activeNodeLevel.value > 0 && activeNodeLevel.value <= 2;
  1440. });
  1441. const handleCustomSearch = (val: any) => {
  1442. customQueryParams.value = val;
  1443. currentPage.value = 1;
  1444. jumpPage.value = '1';
  1445. fetchByActiveNode();
  1446. };
  1447. const findGovernNodeByCurrent = (nodes: GovernTree[]) => {
  1448. const current = activeNode.value;
  1449. if (!current?.id) return undefined;
  1450. return flattenGovernNodes(nodes).find((node) => isSameGovernNode(node, current));
  1451. };
  1452. const fetchTreeData = async (options: { preserveActive?: boolean } = {}) => {
  1453. try {
  1454. const data = await getTree();
  1455. const source = data;
  1456. const mappedTree = mapGovernTree(source);
  1457. treeData.value = mappedTree;
  1458. updateTemplateCategories(source);
  1459. if (!mappedTree.length) {
  1460. clearTableState();
  1461. return;
  1462. }
  1463. const matchedNode = options.preserveActive ? findGovernNodeByCurrent(source) : undefined;
  1464. if (matchedNode) {
  1465. activeNode.value = matchedNode;
  1466. activeNodeLevel.value = 0;
  1467. const matchedKey = resolveGovernTreeKey(matchedNode, mappedTree);
  1468. selectedKeys.value = matchedKey ? [matchedKey] : [];
  1469. fetchByActiveNode();
  1470. return;
  1471. }
  1472. initTreeSelection(mappedTree);
  1473. const firstNode = mappedTree[0]?.dataRef as GovernTree | undefined;
  1474. if (firstNode) {
  1475. fetchByNode(firstNode);
  1476. }
  1477. } catch (error) {
  1478. console.error('获取治理树失败', error);
  1479. treeData.value = [];
  1480. clearTableState();
  1481. }
  1482. };
  1483. const handleCaseDataRefresh = () => {
  1484. // 从清洗完成跳转而来时(cleanMode 非空)保留跳转指定的页签(导入文件统计),不强切回导入数据
  1485. if (!cleanMode.value) {
  1486. activeTab.value = 'import';
  1487. }
  1488. customQueryParams.value = null;
  1489. clearTableState();
  1490. void fetchTreeData();
  1491. };
  1492. onMounted(() => {
  1493. emitter.on(CASE_DATA_REFRESH_EVENT, handleCaseDataRefresh);
  1494. jumpPage.value = currentPage.value.toString();
  1495. fetchTreeData();
  1496. void restoreActiveImportProgress();
  1497. });
  1498. onBeforeUnmount(() => {
  1499. emitter.off(CASE_DATA_REFRESH_EVENT, handleCaseDataRefresh, true);
  1500. });
  1501. const defaultColumns: BasicColumn[] = [
  1502. { title: '本方号码', dataIndex: 'ownNumber', key: 'ownNumber', width: 120 },
  1503. { title: '对方号码', dataIndex: 'otherNumber', key: 'otherNumber', width: 120 },
  1504. { title: '通信类型/联系日期', dataIndex: 'contactTime', key: 'contactTime', width: 170 },
  1505. { title: '对方姓名', dataIndex: 'otherName', key: 'otherName', width: 120 },
  1506. { title: '客户名称', dataIndex: 'customerName', key: 'customerName', width: 120 },
  1507. { title: '本机LAC号', dataIndex: 'lac', key: 'lac', width: 120 },
  1508. { title: '本机cellid', dataIndex: 'cellId', key: 'cellId', width: 120 },
  1509. { title: '本机基站ID', dataIndex: 'baseId', key: 'baseId', width: 140 },
  1510. { title: '本机归属运营商', dataIndex: 'carrier', key: 'carrier', width: 160 },
  1511. ];
  1512. columns.value = defaultColumns.map((column) => applyResultSortProps(column));
  1513. const pageSizeOptions = componentSetting.table.pageSizeOptions.map((value) => ({
  1514. label: value + '条/页',
  1515. value: Number(value),
  1516. }));
  1517. const tableScrollY = ref(340);
  1518. </script>
  1519. <style lang="less">
  1520. @prefix-cls: ~'jeesite-case-data';
  1521. .@{prefix-cls}.case-data-page {
  1522. padding: 0;
  1523. width: calc(100% + 24px);
  1524. margin: 0 -12px;
  1525. background: #f0f2f5;
  1526. display: flex;
  1527. flex-direction: column;
  1528. min-height: 0;
  1529. height: 100%;
  1530. box-sizing: border-box;
  1531. overflow: hidden;
  1532. .page-main {
  1533. flex: 1 1 auto;
  1534. display: flex;
  1535. flex-direction: column;
  1536. gap: 2px;
  1537. min-width: 0;
  1538. min-height: 0;
  1539. height: 100%;
  1540. padding: 2px;
  1541. background: #f0f2f5;
  1542. box-sizing: border-box;
  1543. overflow: hidden;
  1544. }
  1545. .header-bar {
  1546. flex-shrink: 0;
  1547. display: flex;
  1548. align-items: center;
  1549. justify-content: space-between;
  1550. gap: 12px;
  1551. padding: 5px 12px;
  1552. border: 1px solid #e2e8f0;
  1553. border-radius: 12px;
  1554. background: #fff;
  1555. box-shadow: none;
  1556. }
  1557. .header-actions {
  1558. display: flex;
  1559. gap: 8px;
  1560. align-items: center;
  1561. flex-wrap: wrap;
  1562. justify-content: flex-end;
  1563. &__upload,
  1564. &__govern {
  1565. min-width: 92px;
  1566. height: 32px;
  1567. padding: 0 16px;
  1568. border-radius: 16px;
  1569. font-size: 14px;
  1570. box-shadow: none;
  1571. }
  1572. &__divider {
  1573. width: 1px;
  1574. height: 24px;
  1575. background: #d9dee8;
  1576. margin: 0 4px;
  1577. flex: 0 0 auto;
  1578. }
  1579. &__config {
  1580. min-width: 92px;
  1581. height: 32px;
  1582. padding: 0 16px;
  1583. border-radius: 16px;
  1584. font-size: 14px;
  1585. }
  1586. }
  1587. .import-tabs {
  1588. position: relative;
  1589. display: inline-flex;
  1590. align-items: center;
  1591. gap: 28px;
  1592. flex: 0 0 auto;
  1593. min-width: max-content;
  1594. padding: 0 2px;
  1595. &::after {
  1596. position: absolute;
  1597. right: 0;
  1598. bottom: -5px;
  1599. left: 0;
  1600. height: 1px;
  1601. background: #e5e7eb;
  1602. content: '';
  1603. }
  1604. &__item {
  1605. position: relative;
  1606. z-index: 1;
  1607. display: inline-flex;
  1608. align-items: center;
  1609. justify-content: center;
  1610. height: 32px;
  1611. padding: 0;
  1612. border: 0;
  1613. background: transparent;
  1614. color: #111827;
  1615. font-size: 14px;
  1616. line-height: 32px;
  1617. cursor: pointer;
  1618. &::after {
  1619. position: absolute;
  1620. right: 0;
  1621. bottom: -5px;
  1622. left: 0;
  1623. z-index: 1;
  1624. height: 3px;
  1625. border-radius: 3px 3px 0 0;
  1626. background: transparent;
  1627. content: '';
  1628. }
  1629. &.is-active {
  1630. color: #2f54eb;
  1631. font-weight: 600;
  1632. &::after {
  1633. background: #2f54eb;
  1634. }
  1635. }
  1636. }
  1637. }
  1638. .page-layout {
  1639. display: grid;
  1640. grid-template-columns: 200px minmax(0, 1fr);
  1641. gap: 2px;
  1642. align-items: stretch;
  1643. min-height: 0;
  1644. flex: 1 1 auto;
  1645. overflow: hidden;
  1646. }
  1647. .left-panel {
  1648. display: flex;
  1649. flex-direction: column;
  1650. gap: 8px;
  1651. min-height: 0;
  1652. height: 100%;
  1653. padding: 8px;
  1654. border: 1px solid #e2e8f0;
  1655. border-radius: 12px;
  1656. background: #fff;
  1657. box-shadow: none;
  1658. box-sizing: border-box;
  1659. overflow: hidden;
  1660. .sidebar-content {
  1661. width: 100%;
  1662. display: flex;
  1663. flex-direction: column;
  1664. min-height: 0;
  1665. }
  1666. .tree-scroll-shell {
  1667. flex: 1 1 auto;
  1668. min-height: 0;
  1669. width: 100%;
  1670. }
  1671. .case-tree {
  1672. width: 100%;
  1673. height: 100%;
  1674. .jeesite-basic-tree-header {
  1675. padding: 6px 0;
  1676. border-bottom: 1px solid #eef2f7;
  1677. gap: 8px;
  1678. }
  1679. .jeesite-basic-tree-header > .flex {
  1680. gap: 8px;
  1681. min-width: 0;
  1682. }
  1683. .jeesite-basic-title {
  1684. font-size: 14px;
  1685. font-weight: 600;
  1686. color: #111827;
  1687. }
  1688. .jeesite-basic-tree-header .w-full {
  1689. flex: 1 1 auto;
  1690. margin-right: 0;
  1691. min-width: 0;
  1692. }
  1693. .jeesite-basic-tree-header .ant-input-affix-wrapper {
  1694. height: 26px;
  1695. border-radius: 6px;
  1696. font-size: 12px;
  1697. padding-inline: 8px;
  1698. width: 100%;
  1699. }
  1700. .jeesite-basic-tree-header .anticon {
  1701. font-size: 14px;
  1702. color: #6b7280;
  1703. }
  1704. .jeesite-basic-tree-header .ant-dropdown-trigger {
  1705. display: inline-flex;
  1706. align-items: center;
  1707. justify-content: center;
  1708. flex: 0 0 auto;
  1709. height: 26px;
  1710. width: 26px;
  1711. min-width: 26px;
  1712. padding: 0;
  1713. border-radius: 6px;
  1714. color: #6b7280;
  1715. line-height: 1;
  1716. cursor: pointer;
  1717. transition:
  1718. color 0.2s ease,
  1719. background-color 0.2s ease;
  1720. &:hover {
  1721. color: #2563eb;
  1722. background: #eff6ff;
  1723. }
  1724. .jeesite-icon,
  1725. .anticon {
  1726. display: inline-flex !important;
  1727. align-items: center;
  1728. justify-content: center;
  1729. font-size: 14px;
  1730. }
  1731. &::after {
  1732. content: none;
  1733. }
  1734. }
  1735. .ant-tree {
  1736. margin: 8px 0 0;
  1737. }
  1738. .ant-tree-indent-unit {
  1739. width: 16px;
  1740. }
  1741. .ant-tree-node-content-wrapper {
  1742. padding: 4px 6px !important;
  1743. border-radius: 4px;
  1744. font-size: 12px;
  1745. color: #1f2937;
  1746. }
  1747. .ant-tree-switcher {
  1748. display: flex;
  1749. align-items: center;
  1750. width: 16px;
  1751. }
  1752. .ant-tree-iconEle {
  1753. width: 16px;
  1754. color: #94a3b8;
  1755. }
  1756. .ant-tree-treenode {
  1757. padding: 2px 0;
  1758. }
  1759. .ant-tree-indent-unit::before {
  1760. border-color: #e5e7eb;
  1761. }
  1762. .ant-tree-switcher-icon {
  1763. font-size: 10px;
  1764. color: #64748b;
  1765. }
  1766. .ant-tree-treenode:hover .ant-tree-node-content-wrapper {
  1767. background: #f5f7fb;
  1768. }
  1769. .ant-tree-treenode-selected .ant-tree-node-content-wrapper,
  1770. .ant-tree-node-content-wrapper.ant-tree-node-selected {
  1771. background: #eaf2ff;
  1772. color: #2563eb;
  1773. font-weight: 600;
  1774. }
  1775. .ant-tree-title {
  1776. display: flex;
  1777. align-items: center;
  1778. gap: 6px;
  1779. font-size: 12px;
  1780. }
  1781. }
  1782. }
  1783. .right-panel {
  1784. background: #fff;
  1785. border: 1px solid #e2e8f0;
  1786. border-radius: 12px;
  1787. padding: 8px;
  1788. display: flex;
  1789. flex-direction: column;
  1790. gap: 8px;
  1791. min-width: 0;
  1792. min-height: 0;
  1793. height: 100%;
  1794. overflow: hidden;
  1795. box-sizing: border-box;
  1796. box-shadow: none;
  1797. .panel-header {
  1798. flex: 0 0 auto;
  1799. display: flex;
  1800. align-items: center;
  1801. justify-content: space-between;
  1802. gap: 8px;
  1803. min-height: 32px;
  1804. padding: 0 0 8px;
  1805. }
  1806. .panel-title {
  1807. font-size: 15px;
  1808. font-weight: 600;
  1809. color: #111827;
  1810. }
  1811. .panel-desc {
  1812. margin-top: 4px;
  1813. font-size: 12px;
  1814. line-height: 1.6;
  1815. color: #6b7280;
  1816. }
  1817. .table-actions {
  1818. flex-shrink: 0;
  1819. display: flex;
  1820. align-items: center;
  1821. justify-content: flex-end;
  1822. margin-left: auto;
  1823. gap: 8px;
  1824. flex-wrap: wrap;
  1825. min-height: 32px;
  1826. }
  1827. .table-setting-trigger {
  1828. display: inline-flex;
  1829. align-items: center;
  1830. min-height: 32px;
  1831. }
  1832. .table-wrapper {
  1833. flex: 1 1 auto;
  1834. display: flex;
  1835. flex-direction: column;
  1836. min-height: 0;
  1837. width: 100%;
  1838. background: transparent;
  1839. border: 0;
  1840. border-radius: 0;
  1841. overflow: hidden;
  1842. }
  1843. .table-wrapper .jeesite-basic-table {
  1844. flex: 1 1 auto;
  1845. height: 100%;
  1846. display: flex;
  1847. flex-direction: column;
  1848. }
  1849. .table-wrapper .ant-table-wrapper {
  1850. flex: 1 1 auto;
  1851. height: 100%;
  1852. min-height: 0;
  1853. padding: 0 !important;
  1854. background: transparent;
  1855. border-radius: 0;
  1856. }
  1857. .table-wrapper .ant-spin-nested-loading,
  1858. .table-wrapper .ant-spin-container,
  1859. .table-wrapper .ant-table,
  1860. .table-wrapper .ant-table-container {
  1861. height: 100%;
  1862. min-height: 0;
  1863. }
  1864. .table-wrapper .ant-spin-nested-loading,
  1865. .table-wrapper .ant-spin-container,
  1866. .table-wrapper .ant-table,
  1867. .table-wrapper .ant-table-container {
  1868. display: flex;
  1869. flex-direction: column;
  1870. }
  1871. .table-wrapper .ant-table-body {
  1872. flex: 1 1 auto;
  1873. max-height: 100% !important;
  1874. }
  1875. .table-wrapper .ant-table-thead > tr > th {
  1876. background: #f8fafc;
  1877. font-weight: 600;
  1878. color: #374151;
  1879. }
  1880. .table-wrapper .ant-table-thead > tr > th.case-data-sortable-header {
  1881. cursor: pointer;
  1882. user-select: none;
  1883. }
  1884. .table-wrapper .ant-table-tbody > tr > td {
  1885. color: #374151;
  1886. }
  1887. .table-wrapper .ant-table-tbody > tr:hover > td {
  1888. background: #f5f7ff;
  1889. }
  1890. .table-wrapper .ant-table-body::-webkit-scrollbar {
  1891. width: 8px;
  1892. height: 8px;
  1893. }
  1894. .table-wrapper .ant-table-body::-webkit-scrollbar-thumb {
  1895. background: rgba(148, 163, 184, 0.6);
  1896. border-radius: 999px;
  1897. }
  1898. .table-wrapper .ant-table-body::-webkit-scrollbar-track {
  1899. background: transparent;
  1900. }
  1901. .jeesite-basic-table-header__toolbar {
  1902. gap: 6px;
  1903. }
  1904. .table-footer {
  1905. flex-shrink: 0;
  1906. display: flex;
  1907. justify-content: space-between;
  1908. align-items: center;
  1909. gap: 16px;
  1910. min-height: 40px;
  1911. padding-top: 0;
  1912. color: #6b7280;
  1913. .total {
  1914. white-space: nowrap;
  1915. font-size: 12px;
  1916. line-height: 1.5;
  1917. }
  1918. .footer-right {
  1919. display: flex;
  1920. align-items: center;
  1921. margin-left: auto;
  1922. gap: 12px;
  1923. flex-wrap: nowrap;
  1924. min-width: 0;
  1925. }
  1926. .page-size {
  1927. width: 112px;
  1928. flex: 0 0 auto;
  1929. }
  1930. .jump {
  1931. display: flex;
  1932. align-items: center;
  1933. gap: 8px;
  1934. white-space: nowrap;
  1935. }
  1936. .jump-input {
  1937. width: 64px;
  1938. flex: 0 0 auto;
  1939. }
  1940. }
  1941. .page-tip {
  1942. color: #6b7280;
  1943. font-size: 12px;
  1944. }
  1945. }
  1946. .template-modal {
  1947. .ant-modal-content {
  1948. border-radius: 10px;
  1949. padding: 0;
  1950. overflow: hidden;
  1951. }
  1952. .ant-modal-close {
  1953. top: 12px;
  1954. right: 12px;
  1955. }
  1956. .ant-modal-header {
  1957. display: none;
  1958. }
  1959. .ant-modal-body {
  1960. padding: 18px 20px 14px;
  1961. }
  1962. .template-modal__line {
  1963. height: 1px;
  1964. background: #eef2f7;
  1965. margin: 4px 0 12px;
  1966. }
  1967. .template-modal__header {
  1968. display: flex;
  1969. align-items: center;
  1970. justify-content: space-between;
  1971. margin-bottom: 12px;
  1972. }
  1973. .template-modal__title {
  1974. display: flex;
  1975. align-items: center;
  1976. gap: 8px;
  1977. font-size: 16px;
  1978. font-weight: 600;
  1979. color: #1f2937;
  1980. }
  1981. .template-modal__header .template-modal__title {
  1982. margin-bottom: 2px;
  1983. }
  1984. .template-modal__icon {
  1985. width: 22px;
  1986. height: 22px;
  1987. border-radius: 6px;
  1988. background: #ffe2a8;
  1989. display: flex;
  1990. align-items: center;
  1991. justify-content: center;
  1992. font-size: 14px;
  1993. }
  1994. .template-modal__header .template-modal__icon {
  1995. transform: translateY(-1px);
  1996. }
  1997. .template-modal__tabs {
  1998. display: flex !important;
  1999. align-items: center !important;
  2000. gap: 10px;
  2001. padding-bottom: 6px;
  2002. flex-wrap: nowrap !important;
  2003. white-space: nowrap;
  2004. width: 200px;
  2005. }
  2006. .template-modal__tab {
  2007. display: inline-flex !important;
  2008. align-items: center !important;
  2009. padding: 6px 16px;
  2010. border-radius: 999px;
  2011. background: #eef2ff;
  2012. color: #1d4ed8;
  2013. font-size: 13px;
  2014. font-weight: 600;
  2015. cursor: pointer;
  2016. transition: all 0.2s ease;
  2017. width: 500px;
  2018. &.active {
  2019. background: #2f54eb;
  2020. color: #fff;
  2021. box-shadow: 0 4px 10px rgba(47, 84, 235, 0.35);
  2022. }
  2023. }
  2024. .template-modal__body {
  2025. display: flex;
  2026. flex-direction: column;
  2027. gap: 12px;
  2028. }
  2029. .template-modal__filters {
  2030. display: flex;
  2031. align-items: center;
  2032. justify-content: space-between;
  2033. gap: 12px;
  2034. padding: 10px 0 16px;
  2035. flex-wrap: wrap;
  2036. }
  2037. .template-modal__filters-left .ant-input,
  2038. .template-modal__filters-left .ant-select {
  2039. flex: 0 0 auto !important;
  2040. }
  2041. .template-modal__filters-left {
  2042. display: flex !important;
  2043. align-items: center !important;
  2044. flex-wrap: nowrap !important;
  2045. gap: 10px;
  2046. min-width: 0;
  2047. max-width: 620px;
  2048. }
  2049. .template-modal__search {
  2050. width: 220px !important;
  2051. max-width: 220px !important;
  2052. height: 32px;
  2053. }
  2054. .template-modal__filter {
  2055. display: flex;
  2056. align-items: center;
  2057. gap: 8px;
  2058. color: #4b5563;
  2059. font-size: 13px;
  2060. }
  2061. .template-modal__select {
  2062. width: 120px;
  2063. }
  2064. .template-modal__btn {
  2065. height: 32px;
  2066. padding: 0 18px;
  2067. border-radius: 6px;
  2068. &.secondary {
  2069. color: #334155;
  2070. }
  2071. }
  2072. .template-modal__actions {
  2073. display: flex;
  2074. align-items: center;
  2075. gap: 8px;
  2076. flex-wrap: wrap;
  2077. .ant-btn {
  2078. height: 32px;
  2079. padding: 0 14px;
  2080. border-radius: 6px;
  2081. }
  2082. }
  2083. .ant-table-wrapper {
  2084. border: 1px solid #e5e7eb;
  2085. border-radius: 8px;
  2086. overflow: hidden;
  2087. }
  2088. .ant-table-tbody > tr > td {
  2089. padding: 10px 12px;
  2090. color: #374151;
  2091. }
  2092. .jeesite-table-action {
  2093. display: flex;
  2094. gap: 12px;
  2095. flex-wrap: nowrap;
  2096. }
  2097. .ant-table-thead > tr > th {
  2098. background: #f5f7fb;
  2099. font-weight: 600;
  2100. color: #374151;
  2101. }
  2102. .template-modal__footer {
  2103. display: flex;
  2104. align-items: center;
  2105. justify-content: space-between;
  2106. padding: 12px 0 4px;
  2107. color: #6b7280;
  2108. font-size: 12px;
  2109. flex-wrap: wrap;
  2110. gap: 10px;
  2111. }
  2112. .disposition-modal,
  2113. .import-modal,
  2114. .download-modal {
  2115. display: flex;
  2116. flex-direction: column;
  2117. gap: 16px;
  2118. }
  2119. .disposition-form {
  2120. display: flex;
  2121. flex-direction: column;
  2122. gap: 12px;
  2123. }
  2124. .disposition-form .form-row {
  2125. display: flex;
  2126. flex-direction: column;
  2127. gap: 6px;
  2128. }
  2129. .disposition-form .form-label {
  2130. color: #4b5563;
  2131. font-size: 13px;
  2132. }
  2133. .disposition-actions,
  2134. .import-actions,
  2135. .download-actions {
  2136. display: flex;
  2137. justify-content: flex-end;
  2138. gap: 10px;
  2139. }
  2140. .import-desc,
  2141. .download-tip {
  2142. color: #6b7280;
  2143. font-size: 13px;
  2144. }
  2145. .template-modal__pagination {
  2146. display: flex;
  2147. align-items: center;
  2148. gap: 10px;
  2149. }
  2150. .template-modal__page-size {
  2151. width: 110px;
  2152. }
  2153. .template-modal__jump {
  2154. display: flex;
  2155. align-items: center;
  2156. gap: 6px;
  2157. }
  2158. .template-modal__jump-input {
  2159. width: 56px;
  2160. }
  2161. }
  2162. .file-panel {
  2163. background: #fff;
  2164. border: 1px solid #e2e8f0;
  2165. border-radius: 12px;
  2166. padding: 8px;
  2167. flex: 1 1 auto;
  2168. display: flex;
  2169. flex-direction: column;
  2170. gap: 8px;
  2171. min-width: 0;
  2172. min-height: 0;
  2173. height: 100%;
  2174. margin-top: 0;
  2175. overflow: hidden;
  2176. box-sizing: border-box;
  2177. box-shadow: none;
  2178. }
  2179. .file-panel-header {
  2180. flex-shrink: 0;
  2181. padding-bottom: 0;
  2182. }
  2183. .file-panel__table {
  2184. flex: 1 1 auto;
  2185. display: flex;
  2186. flex-direction: column;
  2187. min-height: 0;
  2188. width: 100%;
  2189. overflow: hidden;
  2190. > * {
  2191. flex: 1 1 auto;
  2192. height: 100%;
  2193. min-height: 0;
  2194. }
  2195. }
  2196. .file-filters {
  2197. display: flex;
  2198. align-items: center;
  2199. gap: 12px;
  2200. padding: 8px 0 4px;
  2201. border-bottom: 1px solid #e5e7eb;
  2202. flex-wrap: wrap;
  2203. }
  2204. .filter-item {
  2205. display: flex;
  2206. align-items: center;
  2207. gap: 6px;
  2208. }
  2209. .filter-label {
  2210. color: #6b7280;
  2211. font-size: 12px;
  2212. white-space: nowrap;
  2213. }
  2214. .filter-input {
  2215. width: 160px;
  2216. }
  2217. .filter-actions {
  2218. display: flex;
  2219. gap: 8px;
  2220. }
  2221. .filter-btn {
  2222. height: 28px;
  2223. padding: 0 16px;
  2224. }
  2225. .batch-btn {
  2226. margin-left: auto;
  2227. }
  2228. .file-header {
  2229. display: flex;
  2230. align-items: center;
  2231. justify-content: space-between;
  2232. }
  2233. .file-header .ant-btn {
  2234. height: 28px;
  2235. padding: 0 14px;
  2236. }
  2237. .file-title {
  2238. font-size: 16px;
  2239. font-weight: 600;
  2240. color: #1f2937;
  2241. }
  2242. .file-actions {
  2243. display: flex;
  2244. gap: 12px;
  2245. }
  2246. .file-body {
  2247. border: 1px dashed #e5e7eb;
  2248. border-radius: 12px;
  2249. padding: 24px;
  2250. color: #94a3b8;
  2251. text-align: center;
  2252. }
  2253. .file-panel .ant-table-wrapper {
  2254. border: 1px solid #e5e7eb;
  2255. border-radius: 8px;
  2256. }
  2257. .file-panel .ant-table-thead > tr > th {
  2258. background: #f9fafb;
  2259. font-weight: 600;
  2260. }
  2261. .case-template-modal {
  2262. .ant-modal {
  2263. width: 980px !important;
  2264. max-width: calc(100vw - 80px);
  2265. top: 40px;
  2266. }
  2267. .ant-modal-content {
  2268. border-radius: 12px;
  2269. overflow: hidden;
  2270. }
  2271. .ant-modal-header {
  2272. padding: 12px 18px;
  2273. border-bottom: 1px solid #eef2f7;
  2274. background: @modal-header-bg-color;
  2275. }
  2276. .ant-modal-body {
  2277. padding: 16px 18px 18px;
  2278. }
  2279. }
  2280. .template-modal-body {
  2281. display: flex;
  2282. flex-direction: column;
  2283. gap: 16px;
  2284. }
  2285. .template-modal-header {
  2286. display: flex;
  2287. justify-content: space-between;
  2288. align-items: center;
  2289. padding-bottom: 8px;
  2290. border-bottom: 1px solid #eef2f7;
  2291. }
  2292. .template-modal-title {
  2293. font-size: 16px;
  2294. font-weight: 700;
  2295. color: #0f172a;
  2296. }
  2297. .template-modal-subtitle {
  2298. font-size: 12px;
  2299. color: #64748b;
  2300. margin-top: 4px;
  2301. }
  2302. .template-modal-form {
  2303. display: flex;
  2304. align-items: flex-end;
  2305. gap: 12px;
  2306. }
  2307. .template-modal-form .jeesite-basic-form {
  2308. flex: 1;
  2309. }
  2310. .template-form-actions {
  2311. display: flex;
  2312. gap: 8px;
  2313. justify-content: flex-end;
  2314. padding-bottom: 4px;
  2315. }
  2316. .template-actions {
  2317. display: flex;
  2318. gap: 10px;
  2319. flex-wrap: nowrap;
  2320. align-items: center;
  2321. .ant-btn-link {
  2322. padding: 0 4px;
  2323. height: 24px;
  2324. display: inline-flex;
  2325. align-items: center;
  2326. gap: 4px;
  2327. }
  2328. }
  2329. .template-modal-footer {
  2330. display: flex;
  2331. justify-content: space-between;
  2332. align-items: center;
  2333. color: #6b7280;
  2334. font-size: 12px;
  2335. margin-top: 6px;
  2336. }
  2337. .template-pagination {
  2338. display: flex;
  2339. align-items: center;
  2340. gap: 12px;
  2341. }
  2342. .template-page-size {
  2343. width: 110px;
  2344. }
  2345. .template-table {
  2346. .ant-table {
  2347. border: 1px solid #eef2f7;
  2348. border-radius: 10px;
  2349. overflow: hidden;
  2350. }
  2351. .ant-table-thead > tr > th {
  2352. background: #f8fafc;
  2353. font-weight: 600;
  2354. color: #374151;
  2355. }
  2356. .ant-table-tbody > tr > td {
  2357. color: #374151;
  2358. }
  2359. .ant-table-tbody > tr:hover > td {
  2360. background: #f3f6ff;
  2361. }
  2362. }
  2363. }
  2364. .govern-config-modal {
  2365. .ant-modal-content {
  2366. border-radius: 14px;
  2367. overflow: hidden;
  2368. }
  2369. .ant-modal-header {
  2370. padding: 12px 16px;
  2371. border-bottom: 1px solid #e5e7eb;
  2372. background: @modal-header-bg-color;
  2373. }
  2374. .ant-modal-title {
  2375. color: #111827;
  2376. font-size: 16px;
  2377. font-weight: 600;
  2378. line-height: 24px;
  2379. }
  2380. .ant-modal-body {
  2381. padding: 16px;
  2382. }
  2383. .ant-modal-footer {
  2384. padding: 8px 16px 10px;
  2385. border-top: 1px solid #e5e7eb;
  2386. background: #fff;
  2387. }
  2388. .govern-config-panel {
  2389. display: flex;
  2390. flex-direction: column;
  2391. gap: 12px;
  2392. }
  2393. .govern-config-form {
  2394. display: flex;
  2395. flex-direction: column;
  2396. gap: 12px;
  2397. margin-bottom: 0;
  2398. .ant-form-item {
  2399. margin-bottom: 0;
  2400. }
  2401. .ant-form-item-label {
  2402. padding-bottom: 8px;
  2403. }
  2404. .ant-form-item-label > label {
  2405. color: #334155;
  2406. font-size: 14px;
  2407. font-weight: 600;
  2408. }
  2409. .ant-radio-group {
  2410. display: flex;
  2411. align-items: center;
  2412. gap: 24px;
  2413. }
  2414. .ant-radio-wrapper {
  2415. margin-right: 0;
  2416. color: #475569;
  2417. font-size: 13px;
  2418. }
  2419. .ant-radio-wrapper .ant-radio {
  2420. top: 0;
  2421. }
  2422. }
  2423. .govern-config-item {
  2424. padding: 14px 16px;
  2425. border: 1px solid #e6edf6;
  2426. border-radius: 12px;
  2427. background: #fff;
  2428. box-shadow: 0 8px 24px rgba(15, 23, 42, 0.04);
  2429. }
  2430. .govern-config-label {
  2431. display: inline-flex;
  2432. align-items: center;
  2433. gap: 6px;
  2434. }
  2435. .config-tip-icon {
  2436. display: inline-flex;
  2437. align-items: center;
  2438. justify-content: center;
  2439. width: 18px;
  2440. height: 18px;
  2441. border-radius: 50%;
  2442. background: #3150e6;
  2443. color: #fff;
  2444. font-size: 12px;
  2445. font-weight: 700;
  2446. line-height: 1;
  2447. cursor: pointer;
  2448. }
  2449. .govern-config-footer {
  2450. display: flex;
  2451. justify-content: flex-end;
  2452. gap: 10px;
  2453. .ant-btn {
  2454. min-width: 96px;
  2455. height: 40px;
  2456. border-radius: 10px;
  2457. }
  2458. }
  2459. }
  2460. @media (max-width: 960px) {
  2461. .case-data-header-teleport {
  2462. display: none !important;
  2463. }
  2464. .@{prefix-cls}.case-data-page {
  2465. .header-bar,
  2466. .right-panel .panel-header {
  2467. flex-direction: column;
  2468. align-items: flex-start;
  2469. }
  2470. .header-actions,
  2471. .right-panel .table-actions {
  2472. width: 100%;
  2473. justify-content: flex-start;
  2474. }
  2475. .page-layout {
  2476. grid-template-columns: 1fr;
  2477. }
  2478. .table-footer {
  2479. flex-direction: column;
  2480. align-items: flex-start;
  2481. gap: 12px;
  2482. }
  2483. .table-footer .footer-right {
  2484. flex-wrap: wrap;
  2485. }
  2486. }
  2487. }
  2488. html[data-theme='dark'] {
  2489. .@{prefix-cls}.case-data-page {
  2490. background: #0f172a;
  2491. .panel-title,
  2492. .file-title,
  2493. .template-modal__title {
  2494. color: #e5e7eb;
  2495. }
  2496. .panel-desc {
  2497. color: #9ca3af;
  2498. }
  2499. .left-panel,
  2500. .right-panel,
  2501. .file-panel,
  2502. .template-modal .ant-modal-content {
  2503. background: #111827;
  2504. }
  2505. .template-modal {
  2506. .ant-table-wrapper {
  2507. border-color: #1f2937;
  2508. }
  2509. .ant-table-thead > tr > th {
  2510. background: #0f172a;
  2511. color: #cbd5f5;
  2512. }
  2513. }
  2514. .tree-item {
  2515. background: #1f2937;
  2516. color: #e5e7eb;
  2517. &.active {
  2518. background: #1e40af;
  2519. color: #fff;
  2520. }
  2521. }
  2522. .tree-child {
  2523. background: #0f172a;
  2524. border-color: #1f2937;
  2525. }
  2526. .tree-title {
  2527. color: #e5e7eb;
  2528. }
  2529. .case-tree {
  2530. background: #111827;
  2531. height: 100%;
  2532. .jeesite-basic-tree-header {
  2533. border-bottom-color: #1f2937;
  2534. }
  2535. .jeesite-basic-tree-header .ant-input-affix-wrapper {
  2536. background: #0f172a;
  2537. border-color: #1f2937;
  2538. }
  2539. .jeesite-basic-tree-header .anticon {
  2540. color: #94a3b8;
  2541. }
  2542. .jeesite-basic-tree-header .ant-dropdown-trigger {
  2543. color: #cbd5e1;
  2544. &:hover {
  2545. color: #93c5fd;
  2546. background: rgba(30, 64, 175, 0.24);
  2547. }
  2548. }
  2549. .ant-tree-node-content-wrapper {
  2550. color: #e5e7eb;
  2551. }
  2552. .ant-tree-treenode:hover .ant-tree-node-content-wrapper {
  2553. background: #1f2937;
  2554. }
  2555. .ant-tree-treenode-selected .ant-tree-node-content-wrapper,
  2556. .ant-tree-node-content-wrapper.ant-tree-node-selected {
  2557. background: #1e3a8a;
  2558. color: #e0e7ff;
  2559. }
  2560. }
  2561. .tree-sub,
  2562. .tree-count,
  2563. .table-footer,
  2564. .page-tip,
  2565. .file-body,
  2566. .template-modal__footer {
  2567. color: #9ca3af;
  2568. }
  2569. }
  2570. }
  2571. </style>