index.vue 61 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820
  1. <template>
  2. <div :class="prefixCls" class="ui-scheme-page">
  3. <div class="page-header">
  4. <div class="page-title">UI组件示例</div>
  5. <div class="page-subtitle">使用 mock 数据展示当前系统常见的筛选、统计卡片、表格、详情弹窗和图表视图。</div>
  6. </div>
  7. <div class="page-content">
  8. <Alert
  9. message="页面说明"
  10. description="这个页面只做组件示意,不接接口,方便在一个界面里查看常用的业务展示组件效果。"
  11. type="info"
  12. show-icon
  13. />
  14. <Tabs v-model:activeKey="activeTab" class="section-tabs">
  15. <Tabs.TabPane key="modal" tab="弹窗组件" :forceRender="true">
  16. <div class="tab-pane">
  17. <Card class="modal-gallery-card" title="项目弹窗组件">
  18. <div class="modal-gallery">
  19. <div v-for="item in modalExamples" :key="item.key" class="modal-item">
  20. <div class="modal-item__title">{{ item.title }}</div>
  21. <div class="modal-item__desc">{{ item.description }}</div>
  22. <div class="modal-item__meta">
  23. <span class="modal-item__meta-label">来源文件</span>
  24. <span class="modal-item__meta-value">{{ item.sourceFile }}</span>
  25. </div>
  26. <div class="modal-item__meta">
  27. <span class="modal-item__meta-label">适用场景</span>
  28. <span class="modal-item__meta-value">{{ item.scene }}</span>
  29. </div>
  30. <div class="modal-item__meta">
  31. <span class="modal-item__meta-label">使用组件</span>
  32. <span class="modal-item__meta-value">{{ item.component }}</span>
  33. </div>
  34. <div class="modal-item__meta">
  35. <span class="modal-item__meta-label">数据来源</span>
  36. <span class="modal-item__meta-value">{{ item.dataSource }}</span>
  37. </div>
  38. <Button :type="item.buttonType" @click="openModalExample(item.key)">{{ item.buttonText }}</Button>
  39. </div>
  40. </div>
  41. </Card>
  42. </div>
  43. </Tabs.TabPane>
  44. <Tabs.TabPane key="table" tab="表格视图" :forceRender="true">
  45. <div class="tab-pane">
  46. <div class="filter-bar">
  47. <div class="filter-item">
  48. <span class="filter-label">分析场景:</span>
  49. <Select v-model:value="filters.scene" class="filter-input" :options="sceneOptions" />
  50. </div>
  51. <div class="filter-item">
  52. <span class="filter-label">风险等级:</span>
  53. <Select v-model:value="filters.riskLevel" class="filter-input" :options="riskOptions" />
  54. </div>
  55. <div class="filter-item">
  56. <span class="filter-label">关键词:</span>
  57. <Input
  58. v-model:value="filters.keyword"
  59. class="filter-input filter-input-wide"
  60. placeholder="对象 / 对手方 / 行业"
  61. />
  62. </div>
  63. <div class="filter-item">
  64. <span class="filter-label">日期范围:</span>
  65. <RangePicker v-model:value="filters.dateRange" class="filter-date" />
  66. </div>
  67. <div class="filter-actions">
  68. <Button type="primary" @click="handleSearch">查询</Button>
  69. <Button @click="handleReset">重置</Button>
  70. </div>
  71. </div>
  72. <div class="stats-grid">
  73. <Card v-for="item in statCards" :key="item.title" class="stat-card" :bordered="true">
  74. <Statistic :title="item.title" :value="item.value" :precision="item.precision" :suffix="item.suffix" />
  75. </Card>
  76. </div>
  77. <div class="main-grid">
  78. <Card class="table-card" title="表格数据">
  79. <div class="table-panel">
  80. <BasicTable
  81. :columns="columns"
  82. :data-source="tableData"
  83. :pagination="false"
  84. :can-resize="true"
  85. :is-can-resize-parent="true"
  86. :rowClassName="rowClassName"
  87. row-key="id"
  88. @row-click="handleRowClick"
  89. >
  90. <template #riskLevel="{ record }">
  91. <Tag :color="riskColorMap[record.riskLevel]">{{ record.riskLevel }}</Tag>
  92. </template>
  93. <template #amount="{ record }">{{ formatAmount(record.amount) }}</template>
  94. <template #status="{ record }">
  95. <Tag :color="statusColorMap[record.status]">{{ record.status }}</Tag>
  96. </template>
  97. <template #action="{ record }">
  98. <Button type="link" size="small" @click.stop="handleRowClick(record)">查看详情</Button>
  99. </template>
  100. </BasicTable>
  101. </div>
  102. </Card>
  103. <Card class="detail-card" title="详情面板">
  104. <div v-if="selectedRow" class="detail-panel">
  105. <Descriptions bordered size="small" :column="1">
  106. <DescriptionsItem label="对象">{{ selectedRow.personName }}</DescriptionsItem>
  107. <DescriptionsItem label="对手方">{{ selectedRow.peerName }}</DescriptionsItem>
  108. <DescriptionsItem label="分析场景">{{ selectedRow.scene }}</DescriptionsItem>
  109. <DescriptionsItem label="所属行业">{{ selectedRow.industry }}</DescriptionsItem>
  110. <DescriptionsItem label="所在地区">{{ selectedRow.region }}</DescriptionsItem>
  111. <DescriptionsItem label="风险等级">
  112. <Tag :color="riskColorMap[selectedRow.riskLevel]">{{ selectedRow.riskLevel }}</Tag>
  113. </DescriptionsItem>
  114. <DescriptionsItem label="近30日命中">{{ selectedRow.togetherCount }} 次</DescriptionsItem>
  115. <DescriptionsItem label="涉及金额">{{ formatAmount(selectedRow.amount) }}</DescriptionsItem>
  116. <DescriptionsItem label="最新时间">{{ selectedRow.latestDate }}</DescriptionsItem>
  117. <DescriptionsItem label="状态">
  118. <Tag :color="statusColorMap[selectedRow.status]">{{ selectedRow.status }}</Tag>
  119. </DescriptionsItem>
  120. </Descriptions>
  121. <div class="tag-list">
  122. <Tag v-for="tag in selectedRow.tags" :key="tag">{{ tag }}</Tag>
  123. </div>
  124. <div class="detail-summary">
  125. {{ selectedRow.summary }}
  126. </div>
  127. <div class="detail-actions">
  128. <Button type="primary" @click="openDetailModal">详情弹窗</Button>
  129. <Button @click="openFormModal">表单弹窗</Button>
  130. <Button @click="openDrawerExample">详情抽屉</Button>
  131. <Button @click="openTransRecordModal">交易记录弹窗</Button>
  132. </div>
  133. </div>
  134. <Empty v-else description="暂无选中数据" />
  135. </Card>
  136. </div>
  137. </div>
  138. </Tabs.TabPane>
  139. <Tabs.TabPane key="chart" tab="图表视图" :forceRender="true">
  140. <div class="tab-pane">
  141. <div class="stats-grid">
  142. <Card v-for="item in statCards" :key="`chart-${item.title}`" class="stat-card" :bordered="true">
  143. <Statistic :title="item.title" :value="item.value" :precision="item.precision" :suffix="item.suffix" />
  144. </Card>
  145. </div>
  146. <Card title="图表说明">
  147. <div class="detail-summary">
  148. 当前图表使用筛选后的 mock 数据汇总生成,左侧折线图展示近 6 个月趋势,右侧环图展示风险等级分布。
  149. </div>
  150. </Card>
  151. <div class="chart-grid">
  152. <Card class="chart-card" title="可视化视图 - 近6个月命中趋势">
  153. <div ref="trendChartRef" class="chart-box"></div>
  154. </Card>
  155. <Card class="chart-card" title="可视化视图 - 风险等级分布">
  156. <div ref="riskChartRef" class="chart-box"></div>
  157. </Card>
  158. </div>
  159. </div>
  160. </Tabs.TabPane>
  161. <Tabs.TabPane key="form" tab="表单组件" :forceRender="true">
  162. <div class="tab-pane">
  163. <div class="form-grid">
  164. <Card title="常用表单组件">
  165. <Form layout="vertical" class="mock-form">
  166. <Form.Item label="分析场景">
  167. <Select v-model:value="formModalModel.scene" :options="formSceneOptions" />
  168. </Form.Item>
  169. <Form.Item label="负责人员">
  170. <Input v-model:value="formModalModel.owner" placeholder="请输入负责人员" />
  171. </Form.Item>
  172. <Form.Item label="研判日期">
  173. <DatePicker v-model:value="formModalModel.judgeDate" value-format="YYYY-MM-DD" class="form-date" />
  174. </Form.Item>
  175. <Form.Item label="同步轨迹数据">
  176. <Switch v-model:checked="formModalModel.syncTrack" />
  177. </Form.Item>
  178. <Form.Item label="对象选择">
  179. <ListSelect
  180. ref="formListSelectRef"
  181. selectType="userSelect"
  182. :checkbox="true"
  183. @select="handleListSelectSelect"
  184. />
  185. </Form.Item>
  186. <Form.Item label="上传附件">
  187. <BasicUpload
  188. v-model:value="uploadFormValue"
  189. :api="mockUploadApi"
  190. :apiParams="mockUploadParamsApi"
  191. bizType="uiSchemeDemo"
  192. :maxNumber="3"
  193. :emptyHidePreview="false"
  194. :showPreview="true"
  195. :showPreviewNumber="true"
  196. :helpText="'演示上传弹窗与预览能力,最多 3 个文件'"
  197. @change="handleBasicUploadChange"
  198. />
  199. </Form.Item>
  200. <Form.Item label="备注说明">
  201. <Input.TextArea v-model:value="formModalModel.remark" :rows="4" placeholder="请输入补充说明" />
  202. </Form.Item>
  203. </Form>
  204. <div class="form-actions">
  205. <Button type="primary" @click="openFormModal">弹窗预览</Button>
  206. <Button @click="openUploadModalExample">上传弹窗</Button>
  207. </div>
  208. </Card>
  209. <Card title="表单结果与说明">
  210. <Descriptions bordered size="small" :column="1">
  211. <DescriptionsItem label="适用场景">新增、编辑、选择、上传等表单类交互</DescriptionsItem>
  212. <DescriptionsItem label="使用组件">Form、ListSelect、BasicUpload、BasicModal</DescriptionsItem>
  213. <DescriptionsItem label="所选用户">{{ selectedUsersText }}</DescriptionsItem>
  214. <DescriptionsItem label="上传结果">{{ uploadedFilesText }}</DescriptionsItem>
  215. <DescriptionsItem label="裁剪结果">{{ cropperResult || '暂无' }}</DescriptionsItem>
  216. </Descriptions>
  217. <div class="detail-summary">
  218. 表单页除了展示基础输入组件,也串上了项目里的 `ListSelectModal`、`UploadModal` 这类通用弹窗。
  219. </div>
  220. </Card>
  221. </div>
  222. </div>
  223. </Tabs.TabPane>
  224. </Tabs>
  225. </div>
  226. <Modal
  227. v-model:open="detailModalOpen"
  228. title="详情弹窗示例"
  229. :width="760"
  230. :footer="null"
  231. :centered="true"
  232. destroy-on-close
  233. >
  234. <div v-if="selectedRow" class="detail-modal">
  235. <Descriptions bordered size="small" :column="2">
  236. <DescriptionsItem label="对象">{{ selectedRow.personName }}</DescriptionsItem>
  237. <DescriptionsItem label="对手方">{{ selectedRow.peerName }}</DescriptionsItem>
  238. <DescriptionsItem label="分析场景">{{ selectedRow.scene }}</DescriptionsItem>
  239. <DescriptionsItem label="所属行业">{{ selectedRow.industry }}</DescriptionsItem>
  240. <DescriptionsItem label="所在地区">{{ selectedRow.region }}</DescriptionsItem>
  241. <DescriptionsItem label="联系电话">{{ selectedRow.mobile }}</DescriptionsItem>
  242. <DescriptionsItem label="开户行">{{ selectedRow.bankName }}</DescriptionsItem>
  243. <DescriptionsItem label="最新时间">{{ selectedRow.latestDate }}</DescriptionsItem>
  244. <DescriptionsItem label="风险等级">
  245. <Tag :color="riskColorMap[selectedRow.riskLevel]">{{ selectedRow.riskLevel }}</Tag>
  246. </DescriptionsItem>
  247. <DescriptionsItem label="状态">
  248. <Tag :color="statusColorMap[selectedRow.status]">{{ selectedRow.status }}</Tag>
  249. </DescriptionsItem>
  250. <DescriptionsItem label="近30日命中">{{ selectedRow.togetherCount }} 次</DescriptionsItem>
  251. <DescriptionsItem label="涉及金额">{{ formatAmount(selectedRow.amount) }}</DescriptionsItem>
  252. </Descriptions>
  253. <div class="detail-summary">
  254. {{ selectedRow.summary }}
  255. </div>
  256. <div class="modal-actions">
  257. <Button type="primary" @click="detailModalOpen = false">关闭</Button>
  258. </div>
  259. </div>
  260. </Modal>
  261. <BasicModal
  262. v-model:open="formModalOpen"
  263. title="BasicModal 表单弹窗示例"
  264. :width="720"
  265. :showFooter="true"
  266. :confirmLoading="formModalSubmitting"
  267. wrapClassName="ui-scheme-form-modal"
  268. @ok="handleFormSubmit"
  269. @cancel="formModalOpen = false"
  270. >
  271. <div class="form-modal-body">
  272. <Alert
  273. message="适用场景"
  274. description="用于新增、编辑、配置维护等需要表单提交的弹窗场景。"
  275. type="info"
  276. show-icon
  277. />
  278. <Form layout="vertical" class="mock-form">
  279. <Form.Item label="分析场景">
  280. <Select v-model:value="formModalModel.scene" :options="formSceneOptions" />
  281. </Form.Item>
  282. <Form.Item label="负责人员">
  283. <Input v-model:value="formModalModel.owner" placeholder="请输入负责人员" />
  284. </Form.Item>
  285. <Form.Item label="研判日期">
  286. <DatePicker v-model:value="formModalModel.judgeDate" value-format="YYYY-MM-DD" class="form-date" />
  287. </Form.Item>
  288. <Form.Item label="同步轨迹数据">
  289. <Switch v-model:checked="formModalModel.syncTrack" />
  290. </Form.Item>
  291. <Form.Item label="备注说明">
  292. <Input.TextArea v-model:value="formModalModel.remark" :rows="4" placeholder="请输入补充说明" />
  293. </Form.Item>
  294. </Form>
  295. </div>
  296. </BasicModal>
  297. <BasicDrawer
  298. v-model:open="drawerOpen"
  299. title="BasicDrawer 抽屉示例"
  300. :width="520"
  301. :showFooter="true"
  302. @ok="drawerOpen = false"
  303. @close="drawerOpen = false"
  304. >
  305. <div v-if="selectedRow" class="drawer-panel">
  306. <Alert
  307. message="适用场景"
  308. description="适合侧边详情、轻量编辑、上下文不希望被完全打断的查看场景。"
  309. type="info"
  310. show-icon
  311. />
  312. <Descriptions bordered size="small" :column="1">
  313. <DescriptionsItem label="对象">{{ selectedRow.personName }}</DescriptionsItem>
  314. <DescriptionsItem label="对手方">{{ selectedRow.peerName }}</DescriptionsItem>
  315. <DescriptionsItem label="分析场景">{{ selectedRow.scene }}</DescriptionsItem>
  316. <DescriptionsItem label="所属行业">{{ selectedRow.industry }}</DescriptionsItem>
  317. <DescriptionsItem label="风险等级">
  318. <Tag :color="riskColorMap[selectedRow.riskLevel]">{{ selectedRow.riskLevel }}</Tag>
  319. </DescriptionsItem>
  320. <DescriptionsItem label="状态">
  321. <Tag :color="statusColorMap[selectedRow.status]">{{ selectedRow.status }}</Tag>
  322. </DescriptionsItem>
  323. <DescriptionsItem label="最新时间">{{ selectedRow.latestDate }}</DescriptionsItem>
  324. </Descriptions>
  325. <div class="drawer-summary">
  326. {{ selectedRow.summary }}
  327. </div>
  328. </div>
  329. </BasicDrawer>
  330. <QueryTableModal
  331. v-model:open="queryTableModalOpen"
  332. title="QueryTableModal 示例"
  333. :loading="false"
  334. :filters="queryTableFilters"
  335. :filter-schema="queryTableFilterSchema"
  336. :columns="queryTableColumns"
  337. :data-source="queryTableData"
  338. :pagination="queryTablePagination"
  339. :total-pages="queryTableTotalPages"
  340. :page-size-options="queryTablePageSizeOptions"
  341. :show-table-setting="false"
  342. :table-setting="{ size: true, setting: true, redo: false, fullScreen: false }"
  343. :can-fullscreen="true"
  344. wrap-class-name="ui-scheme-query-table-modal"
  345. @search="handleQueryTableSearch"
  346. @reset="handleQueryTableReset"
  347. @prev-page="handleQueryTablePrevPage"
  348. @next-page="handleQueryTableNextPage"
  349. @page-size-change="handleQueryTablePageSizeChange"
  350. @page-change="handleQueryTablePageChange"
  351. @sort-change="handleQueryTableSortChange"
  352. />
  353. <TransRecordModal
  354. v-model:open="transRecordModalOpen"
  355. title="交易记录"
  356. :mock-mode="true"
  357. :base-query="transRecordBaseQuery"
  358. />
  359. <CallRecordModal v-model:open="callRecordModalOpen" title="通信记录" :base-query="callRecordBaseQuery" />
  360. <AppSearchModal :open="appSearchModalOpen" @close="appSearchModalOpen = false" />
  361. <UploadModal
  362. :api="mockUploadApi"
  363. :apiParams="mockUploadParamsApi"
  364. bizKey="ui-scheme-upload"
  365. bizType="uiSchemeDemo"
  366. :isLazy="true"
  367. :maxNumber="3"
  368. :helpText="'演示上传弹窗,支持选择文件后点击开始上传'"
  369. @register="registerUploadModal"
  370. @change="handleUploadModalChange"
  371. />
  372. <UploadPreviewModal
  373. :value="previewFileList"
  374. :readonly="true"
  375. :emptyHidePreview="false"
  376. :apiDownloadUrl="'/api/mock-download'"
  377. @register="registerUploadPreviewModal"
  378. />
  379. <CopperModal
  380. :value="cropperPreviewSource"
  381. @register="registerCropperModal"
  382. @upload-success="handleCropperSuccess"
  383. />
  384. <ErrorLogDetailModal :info="errorLogDetailInfo" @register="registerErrorLogModal" />
  385. <LockModal @register="registerLockModal" />
  386. </div>
  387. </template>
  388. <script lang="ts" setup name="ViewsUiSchemeIndex">
  389. import { computed, onMounted, reactive, ref, type Ref } from 'vue';
  390. import type { Dayjs } from 'dayjs';
  391. import type { EChartsOption } from 'echarts';
  392. import {
  393. Alert,
  394. Button,
  395. Card,
  396. DatePicker,
  397. Descriptions,
  398. Empty,
  399. Form,
  400. Input,
  401. Modal,
  402. Select,
  403. Statistic,
  404. Switch,
  405. Tag,
  406. Tabs,
  407. } from 'ant-design-vue';
  408. import { ListSelect } from '@jeesite/core/components/ListSelect';
  409. import { BasicDrawer } from '@jeesite/core/components/Drawer';
  410. import { BasicModal, useModal } from '@jeesite/core/components/Modal';
  411. import { BasicTable, type BasicColumn } from '@jeesite/core/components/Table';
  412. import { BasicUpload } from '@jeesite/core/components/Upload';
  413. import AppSearchModal from '@jeesite/core/components/Application/src/search/AppSearchModal.vue';
  414. import CopperModal from '@jeesite/core/components/Cropper/src/CopperModal.vue';
  415. import UploadPreviewModal from '@jeesite/core/components/Upload/src/UploadPreviewModal.vue';
  416. import ErrorLogDetailModal from '@jeesite/core/layouts/views/errorLog/DetailModal.vue';
  417. import { ErrorTypeEnum } from '@jeesite/core/enums/exceptionEnum';
  418. import { useDesign } from '@jeesite/core/hooks/web/useDesign';
  419. import { useECharts } from '@jeesite/core/hooks/web/useECharts';
  420. import { useMessage } from '@jeesite/core/hooks/web/useMessage';
  421. import LockModal from '@jeesite/core/layouts/default/header/components/lock/LockModal.vue';
  422. import componentSetting from '@jeesite/core/settings/componentSetting';
  423. import type { UploadParams } from '@jeesite/core/api/sys/upload';
  424. import type { ErrorLogInfo } from '@jeesite/types/store';
  425. import UploadModal from '@jeesite/core/components/Upload/src/UploadModal.vue';
  426. import CallRecordModal from '@jeesite/call/views/components/CallRecordModal.vue';
  427. import QueryTableModal from '@jeesite/case/views/dm/components/QueryTableModal.vue';
  428. import TransRecordModal from '../components/TransRecordModal.vue';
  429. interface DemoRow {
  430. id: number;
  431. personName: string;
  432. peerName: string;
  433. scene: string;
  434. industry: string;
  435. region: string;
  436. riskLevel: '高' | '中' | '低';
  437. status: '关注中' | '待研判' | '已核查';
  438. togetherCount: number;
  439. amount: number;
  440. latestDate: string;
  441. bankName: string;
  442. mobile: string;
  443. tags: string[];
  444. summary: string;
  445. trend: number[];
  446. }
  447. type RangeValue = [Dayjs, Dayjs] | undefined;
  448. const RangePicker = DatePicker.RangePicker;
  449. const DescriptionsItem = Descriptions.Item;
  450. const { createConfirm, createMessage } = useMessage();
  451. const { prefixCls } = useDesign('ui-scheme');
  452. const activeTab = ref('modal');
  453. const filters = ref({
  454. scene: 'all',
  455. riskLevel: 'all',
  456. keyword: '',
  457. dateRange: undefined as RangeValue,
  458. });
  459. const sceneOptions = [
  460. { label: '全部', value: 'all' },
  461. { label: '同行分析', value: '同行分析' },
  462. { label: '交易分析', value: '交易分析' },
  463. { label: '话单分析', value: '话单分析' },
  464. ];
  465. const riskOptions = [
  466. { label: '全部', value: 'all' },
  467. { label: '高', value: '高' },
  468. { label: '中', value: '中' },
  469. { label: '低', value: '低' },
  470. ];
  471. const riskColorMap: Record<DemoRow['riskLevel'], string> = {
  472. 高: 'red',
  473. 中: 'orange',
  474. 低: 'green',
  475. };
  476. const formSceneOptions = [
  477. { label: '同行分析', value: '同行分析' },
  478. { label: '交易分析', value: '交易分析' },
  479. { label: '话单分析', value: '话单分析' },
  480. ];
  481. const statusColorMap: Record<DemoRow['status'], string> = {
  482. 关注中: 'processing',
  483. 待研判: 'warning',
  484. 已核查: 'default',
  485. };
  486. const monthLabels = ['2025-10', '2025-11', '2025-12', '2026-01', '2026-02', '2026-03'];
  487. const mockSourceData: DemoRow[] = [
  488. {
  489. id: 1,
  490. personName: '张三',
  491. peerName: '杭州云创科技',
  492. scene: '同行分析',
  493. industry: '科技服务',
  494. region: '杭州',
  495. riskLevel: '高',
  496. status: '关注中',
  497. togetherCount: 12,
  498. amount: 128.5,
  499. latestDate: '2026-03-22',
  500. bankName: '中国银行',
  501. mobile: '13800001234',
  502. tags: ['高频同行', '近7日活跃', '需复核'],
  503. summary: '近一个月多次与重点企业共同出行,同时存在资金往来,适合用于展示详情面板和弹窗内容。',
  504. trend: [3, 4, 5, 6, 8, 12],
  505. },
  506. {
  507. id: 2,
  508. personName: '李四',
  509. peerName: '上海远航贸易',
  510. scene: '交易分析',
  511. industry: '贸易流通',
  512. region: '上海',
  513. riskLevel: '中',
  514. status: '待研判',
  515. togetherCount: 8,
  516. amount: 96.2,
  517. latestDate: '2026-03-18',
  518. bankName: '建设银行',
  519. mobile: '13900004567',
  520. tags: ['交易频繁', '跨地区往来'],
  521. summary: '该对象与对手方交易频次较高,金额稳定,适合展示表格、标签和统计卡片的组合效果。',
  522. trend: [2, 3, 4, 4, 6, 8],
  523. },
  524. {
  525. id: 3,
  526. personName: '王五',
  527. peerName: '深圳星河商贸',
  528. scene: '话单分析',
  529. industry: '商贸零售',
  530. region: '深圳',
  531. riskLevel: '中',
  532. status: '关注中',
  533. togetherCount: 10,
  534. amount: 88.7,
  535. latestDate: '2026-03-25',
  536. bankName: '工商银行',
  537. mobile: '13700007890',
  538. tags: ['夜间联系', '跨城轨迹'],
  539. summary: '话单与轨迹存在交叉命中,适合在右侧详情面板中展示基础信息和标签。',
  540. trend: [1, 3, 3, 5, 7, 10],
  541. },
  542. {
  543. id: 4,
  544. personName: '赵六',
  545. peerName: '宁波融信服务',
  546. scene: '同行分析',
  547. industry: '企业服务',
  548. region: '宁波',
  549. riskLevel: '低',
  550. status: '已核查',
  551. togetherCount: 5,
  552. amount: 42.3,
  553. latestDate: '2026-03-09',
  554. bankName: '农业银行',
  555. mobile: '13600002345',
  556. tags: ['历史留痕'],
  557. summary: '该对象已完成初步核查,可用于展示状态标签和较轻量的详情说明。',
  558. trend: [1, 1, 2, 2, 4, 5],
  559. },
  560. {
  561. id: 5,
  562. personName: '张三',
  563. peerName: '上海远航贸易',
  564. scene: '交易分析',
  565. industry: '贸易流通',
  566. region: '上海',
  567. riskLevel: '高',
  568. status: '关注中',
  569. togetherCount: 14,
  570. amount: 166.8,
  571. latestDate: '2026-03-28',
  572. bankName: '中国银行',
  573. mobile: '13800001234',
  574. tags: ['大额转账', '重点对象'],
  575. summary: '同一对象在不同场景下可以复用相同组件,适合模拟列表切换后的详情联动。',
  576. trend: [4, 5, 7, 8, 10, 14],
  577. },
  578. {
  579. id: 6,
  580. personName: '李四',
  581. peerName: '宁波融信服务',
  582. scene: '话单分析',
  583. industry: '企业服务',
  584. region: '宁波',
  585. riskLevel: '低',
  586. status: '待研判',
  587. togetherCount: 4,
  588. amount: 31.4,
  589. latestDate: '2026-02-26',
  590. bankName: '建设银行',
  591. mobile: '13900004567',
  592. tags: ['短时联系'],
  593. summary: '样本较轻,适合用来观察筛选条件变化后表格、图表和详情面板的同步更新。',
  594. trend: [0, 1, 1, 2, 3, 4],
  595. },
  596. ];
  597. const queryTableMockSourceData: DemoRow[] = Array.from({ length: 48 }, (_, index) => {
  598. const base = mockSourceData[index % mockSourceData.length];
  599. const batch = Math.floor(index / mockSourceData.length);
  600. const peerGroup = ['一组', '二组', '三组', '四组'][batch % 4];
  601. const latestDay = String(((index * 2) % 28) + 1).padStart(2, '0');
  602. return {
  603. ...base,
  604. id: index + 1,
  605. peerName: `${base.peerName}-${peerGroup}`,
  606. togetherCount: base.togetherCount + batch * 2 + (index % 3),
  607. amount: Number((base.amount + batch * 15.6 + (index % 4) * 3.2).toFixed(1)),
  608. latestDate: `2026-03-${latestDay}`,
  609. tags: [...base.tags, `批次${batch + 1}`],
  610. summary: `${base.summary} 当前为 QueryTableModal 的第 ${batch + 1} 批 mock 数据。`,
  611. trend: base.trend.map((value, trendIndex) => value + batch + (trendIndex === 5 ? index % 3 : 0)),
  612. };
  613. });
  614. const columns: BasicColumn[] = [
  615. { title: '序号', dataIndex: 'id', key: 'id', width: 70, align: 'center' },
  616. { title: '对象', dataIndex: 'personName', key: 'personName', width: 100, align: 'center' },
  617. { title: '对手方', dataIndex: 'peerName', key: 'peerName', width: 160, align: 'center' },
  618. { title: '分析场景', dataIndex: 'scene', key: 'scene', width: 120, align: 'center' },
  619. { title: '所属行业', dataIndex: 'industry', key: 'industry', width: 120, align: 'center' },
  620. {
  621. title: '风险等级',
  622. dataIndex: 'riskLevel',
  623. key: 'riskLevel',
  624. width: 110,
  625. align: 'center',
  626. slots: { customRender: 'riskLevel' },
  627. },
  628. { title: '近30日命中', dataIndex: 'togetherCount', key: 'togetherCount', width: 120, align: 'center' },
  629. {
  630. title: '涉及金额',
  631. dataIndex: 'amount',
  632. key: 'amount',
  633. width: 120,
  634. align: 'center',
  635. slots: { customRender: 'amount' },
  636. },
  637. { title: '最新时间', dataIndex: 'latestDate', key: 'latestDate', width: 120, align: 'center' },
  638. {
  639. title: '状态',
  640. dataIndex: 'status',
  641. key: 'status',
  642. width: 110,
  643. align: 'center',
  644. slots: { customRender: 'status' },
  645. },
  646. {
  647. title: '操作',
  648. dataIndex: 'action',
  649. key: 'action',
  650. width: 100,
  651. align: 'center',
  652. slots: { customRender: 'action' },
  653. },
  654. ];
  655. const tableData = ref<DemoRow[]>([]);
  656. const selectedRow = ref<DemoRow | null>(null);
  657. const detailModalOpen = ref(false);
  658. const formModalOpen = ref(false);
  659. const formModalSubmitting = ref(false);
  660. const drawerOpen = ref(false);
  661. const queryTableModalOpen = ref(false);
  662. const transRecordModalOpen = ref(false);
  663. const callRecordModalOpen = ref(false);
  664. const appSearchModalOpen = ref(false);
  665. const formListSelectRef = ref<any>(null);
  666. const selectedUsers = ref<Record<string, any>[]>([]);
  667. const uploadFormValue = ref<Record<string, any>>({});
  668. const uploadedFiles = ref<string[]>([]);
  669. const cropperResult = ref('');
  670. const cropperPreviewSource =
  671. 'data:image/svg+xml;charset=UTF-8,' +
  672. encodeURIComponent(
  673. '<svg xmlns="http://www.w3.org/2000/svg" width="480" height="320" viewBox="0 0 480 320"><rect width="480" height="320" fill="#eaf2ff"/><rect x="24" y="24" width="432" height="272" rx="24" fill="#cfe0ff"/><circle cx="160" cy="140" r="42" fill="#7aa2ff"/><rect x="228" y="104" width="120" height="20" rx="10" fill="#5b7cce"/><rect x="228" y="138" width="160" height="16" rx="8" fill="#8da8df"/><rect x="228" y="166" width="96" height="16" rx="8" fill="#8da8df"/><text x="240" y="228" font-size="24" fill="#3a4b7a">UI Scheme</text></svg>',
  674. );
  675. const previewFileList = ref<any[]>([
  676. {
  677. id: 'mock-preview-1',
  678. fileName: 'tonghang-analysis.png',
  679. fileType: 'png',
  680. fileUrl: cropperPreviewSource,
  681. createDate: '2026-04-04 10:30:00',
  682. bizKey: 'ui-scheme-preview',
  683. bizType: 'uiSchemePreview',
  684. bizKeyIsLike: '',
  685. fileEntity: {
  686. fileExtension: 'png',
  687. fileSize: 245760,
  688. },
  689. },
  690. {
  691. id: 'mock-preview-2',
  692. fileName: 'peer-analysis-report.pdf',
  693. fileType: 'pdf',
  694. fileUrl: '',
  695. createDate: '2026-04-04 10:42:00',
  696. bizKey: 'ui-scheme-preview',
  697. bizType: 'uiSchemePreview',
  698. bizKeyIsLike: '',
  699. fileEntity: {
  700. fileExtension: 'pdf',
  701. fileSize: 1048576,
  702. },
  703. },
  704. ]);
  705. const errorLogDetailInfo = ref<ErrorLogInfo>({
  706. type: ErrorTypeEnum.PROMISE,
  707. url: '/trans/uiScheme',
  708. time: '2026-04-04 11:20:36',
  709. file: 'frontend/packages/trans/views/uiScheme/index.vue:512:14',
  710. name: 'MockUiError',
  711. message: 'Cannot read properties of undefined (reading "amount")',
  712. detail: '打开弹窗示例时传入了空数据,当前内容仅用于统一管理时查看异常详情弹窗的结构。',
  713. stack:
  714. 'TypeError: Cannot read properties of undefined (reading "amount") at openModalExample (uiScheme/index.vue:512:14)',
  715. });
  716. const formModalModel = reactive({
  717. scene: '同行分析',
  718. owner: '研判一组',
  719. judgeDate: '2026-04-04',
  720. syncTrack: true,
  721. remark: '该弹窗用于演示项目里基于 BasicModal 的表单录入场景。',
  722. });
  723. const [registerUploadModal, { openModal: openUploadModal }] = useModal();
  724. const [registerUploadPreviewModal, { openModal: openUploadPreviewModal }] = useModal();
  725. const [registerCropperModal, { openModal: openCropperModal }] = useModal();
  726. const [registerErrorLogModal, { openModal: openErrorLogModal }] = useModal();
  727. const [registerLockModal, { openModal: openLockModal }] = useModal();
  728. const modalExamples = [
  729. {
  730. key: 'basic',
  731. title: '基础详情弹窗',
  732. description: '使用 Ant Design Vue `Modal` 展示详情信息,适合详情查看、确认说明等场景。',
  733. sourceFile: '当前页内联示例(ant-design-vue Modal)',
  734. scene: '详情查看 / 结果说明',
  735. component: 'Modal + Descriptions',
  736. dataSource: '本地 mock',
  737. buttonText: '打开弹窗',
  738. buttonType: 'primary' as const,
  739. },
  740. {
  741. key: 'form',
  742. title: 'BasicModal 表单弹窗',
  743. description: '项目基础弹窗包装,适合新增、编辑、配置维护等表单提交场景。',
  744. sourceFile: 'frontend/packages/core/components/Modal/src/BasicModal.vue',
  745. scene: '新增 / 编辑 / 配置维护',
  746. component: 'BasicModal + Form',
  747. dataSource: '本地 mock',
  748. buttonText: '打开弹窗',
  749. buttonType: 'default' as const,
  750. },
  751. {
  752. key: 'drawer',
  753. title: 'BasicDrawer 抽屉',
  754. description: '抽屉适合从右侧承载详情或轻量编辑,不打断当前列表上下文。',
  755. sourceFile: '当前页内联示例(BasicDrawer)',
  756. scene: '侧边详情 / 轻量编辑',
  757. component: 'BasicDrawer + Descriptions',
  758. dataSource: '本地 mock',
  759. buttonText: '打开抽屉',
  760. buttonType: 'default' as const,
  761. },
  762. {
  763. key: 'upload',
  764. title: 'UploadModal',
  765. description: '项目通用上传弹窗,适合附件、证据材料、批量文件上传等场景。',
  766. sourceFile: 'frontend/packages/core/components/Upload/src/UploadModal.vue',
  767. scene: '附件上传 / 材料补充',
  768. component: 'UploadModal',
  769. dataSource: '本地 mock 上传',
  770. buttonText: '打开弹窗',
  771. buttonType: 'default' as const,
  772. },
  773. {
  774. key: 'listSelect',
  775. title: 'ListSelectModal',
  776. description: '通过 ListSelect 触发的数据选择弹窗,常用于人员、机构、字典等对象选择。',
  777. sourceFile: 'frontend/packages/core/components/ListSelect/src/ListSelectModal.vue',
  778. scene: '对象选择 / 关系绑定',
  779. component: 'ListSelect -> ListSelectModal',
  780. dataSource: '真实接口',
  781. buttonText: '打开弹窗',
  782. buttonType: 'default' as const,
  783. },
  784. {
  785. key: 'queryTable',
  786. title: 'QueryTableModal',
  787. description: '项目里常见的“查询区 + 表格 + 分页”复用弹窗,当前用 mock 数据演示。',
  788. sourceFile: 'frontend/packages/case/views/dm/components/QueryTableModal.vue',
  789. scene: '列表查询 / 记录检索',
  790. component: 'QueryTableModal',
  791. dataSource: '本地 mock',
  792. buttonText: '打开弹窗',
  793. buttonType: 'default' as const,
  794. },
  795. {
  796. key: 'transRecord',
  797. title: 'TransRecordModal',
  798. description: '交易分析页面复用的记录弹窗,当前使用组件内置 `mock-mode` 展示交易记录。',
  799. sourceFile: 'frontend/packages/trans/views/components/TransRecordModal.vue',
  800. scene: '交易明细 / 资金记录',
  801. component: 'TransRecordModal',
  802. dataSource: '组件 mock',
  803. buttonText: '打开弹窗',
  804. buttonType: 'default' as const,
  805. },
  806. {
  807. key: 'callRecord',
  808. title: 'CallRecordModal',
  809. description: '话单分析页面复用的通话记录弹窗,展示项目里的真实弹窗组件结构。',
  810. sourceFile: 'frontend/packages/call/views/components/CallRecordModal.vue',
  811. scene: '通话明细 / 话单记录',
  812. component: 'CallRecordModal',
  813. dataSource: '真实接口',
  814. buttonText: '打开弹窗',
  815. buttonType: 'default' as const,
  816. },
  817. {
  818. key: 'confirm',
  819. title: '确认弹窗',
  820. description: '通过 `useMessage().createConfirm` 拉起确认框,适合删除、重算、批量操作前确认。',
  821. sourceFile: '当前页交互示例(useMessage.createConfirm)',
  822. scene: '删除确认 / 重算确认 / 批量动作',
  823. component: 'useMessage.createConfirm',
  824. dataSource: '本地交互',
  825. buttonText: '打开确认框',
  826. buttonType: 'default' as const,
  827. },
  828. {
  829. key: 'appSearch',
  830. title: 'AppSearchModal',
  831. description: '头部全局搜索弹层,适合快速查找菜单、页面入口和系统导航。',
  832. sourceFile: 'frontend/packages/core/components/Application/src/search/AppSearchModal.vue',
  833. scene: '全局菜单搜索 / 快捷跳转',
  834. component: 'AppSearchModal',
  835. dataSource: '当前系统菜单',
  836. buttonText: '打开搜索',
  837. buttonType: 'default' as const,
  838. },
  839. {
  840. key: 'uploadPreview',
  841. title: 'UploadPreviewModal',
  842. description: '上传后的文件预览弹窗,适合附件列表查看、图片缩略图展示和文件清单校验。',
  843. sourceFile: 'frontend/packages/core/components/Upload/src/UploadPreviewModal.vue',
  844. scene: '文件预览 / 附件清单查看',
  845. component: 'UploadPreviewModal',
  846. dataSource: '本地 mock 文件',
  847. buttonText: '打开弹窗',
  848. buttonType: 'default' as const,
  849. },
  850. {
  851. key: 'cropper',
  852. title: 'CopperModal',
  853. description: '头像/图片裁剪弹窗,项目中用于上传前裁剪、旋转、缩放和预览。',
  854. sourceFile: 'frontend/packages/core/components/Cropper/src/CopperModal.vue',
  855. scene: '图片裁剪 / 头像编辑',
  856. component: 'CopperModal',
  857. dataSource: '本地 mock 图片',
  858. buttonText: '打开弹窗',
  859. buttonType: 'default' as const,
  860. },
  861. {
  862. key: 'errorLog',
  863. title: 'ErrorLog DetailModal',
  864. description: '系统错误日志详情弹窗,用于查看异常类型、堆栈、地址和触发时间。',
  865. sourceFile: 'frontend/packages/core/layouts/views/errorLog/DetailModal.vue',
  866. scene: '异常详情 / 问题排查',
  867. component: 'DetailModal',
  868. dataSource: '本地 mock 异常信息',
  869. buttonText: '打开弹窗',
  870. buttonType: 'default' as const,
  871. },
  872. {
  873. key: 'lock',
  874. title: 'LockModal',
  875. description: '系统锁屏弹窗,用于输入锁屏密码并进入锁定状态,适合临时离岗保护场景。',
  876. sourceFile: 'frontend/packages/core/layouts/default/header/components/lock/LockModal.vue',
  877. scene: '锁屏 / 暂离保护',
  878. component: 'LockModal',
  879. dataSource: '当前登录用户 + 本地锁屏状态',
  880. buttonText: '打开弹窗',
  881. buttonType: 'default' as const,
  882. },
  883. ];
  884. const selectedUsersText = computed(() => {
  885. if (!selectedUsers.value.length) return '暂无';
  886. return selectedUsers.value.map((item) => item.userName || item.refName || item.userCode || '-').join('、');
  887. });
  888. const uploadedFilesText = computed(() => {
  889. const names = uploadedFiles.value.length
  890. ? uploadedFiles.value
  891. : String(uploadFormValue.value?.uiSchemeDemo || '')
  892. .split(',')
  893. .filter(Boolean);
  894. return names.length ? names.join('、') : '暂无';
  895. });
  896. const statCards = computed(() => {
  897. const totalCount = tableData.value.length;
  898. const highRiskCount = tableData.value.filter((item) => item.riskLevel === '高').length;
  899. const totalHits = tableData.value.reduce((sum, item) => sum + item.togetherCount, 0);
  900. const totalAmount = Number(tableData.value.reduce((sum, item) => sum + item.amount, 0).toFixed(1));
  901. return [
  902. { title: '当前样本', value: totalCount, precision: 0, suffix: '条' },
  903. { title: '高风险对象', value: highRiskCount, precision: 0, suffix: '条' },
  904. { title: '近30日命中', value: totalHits, precision: 0, suffix: '次' },
  905. { title: '涉及金额', value: totalAmount, precision: 1, suffix: '万' },
  906. ];
  907. });
  908. const transRecordBaseQuery = computed(() => ({
  909. personName: selectedRow.value?.personName || undefined,
  910. otherName: selectedRow.value?.peerName || undefined,
  911. bankName: selectedRow.value?.bankName || undefined,
  912. }));
  913. const callRecordBaseQuery = computed(() => ({
  914. personName: selectedRow.value?.personName || undefined,
  915. otherName: selectedRow.value?.peerName || undefined,
  916. personPhones: selectedRow.value?.mobile ? [selectedRow.value.mobile] : undefined,
  917. }));
  918. const queryTableFilters = reactive({
  919. personName: '',
  920. otherName: '',
  921. scene: '',
  922. riskLevel: '',
  923. startDate: '',
  924. endDate: '',
  925. orderKey: '',
  926. sort: '' as '' | 'ASC' | 'DESC',
  927. });
  928. const queryTablePagination = reactive({
  929. page: 1,
  930. limit: componentSetting.table.defaultPageSize,
  931. total: 0,
  932. });
  933. const queryTablePageSizeOptions = componentSetting.table.pageSizeOptions.map((value) => ({
  934. label: value,
  935. value: Number(value),
  936. }));
  937. const queryTableTotalPages = computed(() => {
  938. if (!queryTablePagination.total) return 1;
  939. return Math.max(1, Math.ceil(queryTablePagination.total / queryTablePagination.limit));
  940. });
  941. const queryTableFilterSchema = [
  942. { field: 'personName', label: '对象', type: 'input', placeholder: '请输入对象', width: 180 },
  943. { field: 'otherName', label: '对手方', type: 'input', placeholder: '请输入对手方', width: 180 },
  944. {
  945. field: 'scene',
  946. label: '场景',
  947. type: 'select',
  948. width: 160,
  949. options: [
  950. { label: '全部', value: '' },
  951. { label: '同行分析', value: '同行分析' },
  952. { label: '交易分析', value: '交易分析' },
  953. { label: '话单分析', value: '话单分析' },
  954. ],
  955. },
  956. {
  957. field: 'riskLevel',
  958. label: '风险等级',
  959. type: 'select',
  960. width: 160,
  961. options: [
  962. { label: '全部', value: '' },
  963. { label: '高', value: '高' },
  964. { label: '中', value: '中' },
  965. { label: '低', value: '低' },
  966. ],
  967. },
  968. { field: 'startDate', label: '开始日期', type: 'date', width: 170 },
  969. { field: 'endDate', label: '结束日期', type: 'date', width: 170 },
  970. ] as const;
  971. const queryTableColumns: BasicColumn[] = [
  972. { title: '排序', dataIndex: 'id', key: 'id', width: 70, align: 'center' },
  973. { title: '对象', dataIndex: 'personName', key: 'personName', width: 120, align: 'center' },
  974. { title: '对手方', dataIndex: 'peerName', key: 'peerName', width: 160, align: 'center' },
  975. { title: '分析场景', dataIndex: 'scene', key: 'scene', width: 120, align: 'center' },
  976. { title: '风险等级', dataIndex: 'riskLevel', key: 'riskLevel', width: 110, align: 'center' },
  977. { title: '命中次数', dataIndex: 'togetherCount', key: 'togetherCount', width: 110, align: 'center', sorter: true },
  978. { title: '涉及金额', dataIndex: 'amountText', key: 'amount', width: 120, align: 'center', sorter: true },
  979. { title: '最新时间', dataIndex: 'latestDate', key: 'latestDate', width: 120, align: 'center', sorter: true },
  980. { title: '状态', dataIndex: 'status', key: 'status', width: 110, align: 'center' },
  981. ];
  982. const queryTableData = ref<any[]>([]);
  983. const trendChartRef = ref<HTMLDivElement | null>(null);
  984. const riskChartRef = ref<HTMLDivElement | null>(null);
  985. const { setOptions: setTrendOptions } = useECharts(trendChartRef as Ref<HTMLDivElement>);
  986. const { setOptions: setRiskOptions } = useECharts(riskChartRef as Ref<HTMLDivElement>);
  987. const formatAmount = (amount: number) => `${amount.toFixed(1)}万`;
  988. const ensureSelectedRow = () => {
  989. if (selectedRow.value) return true;
  990. createMessage.warning('请先在表格中选择一条数据');
  991. return false;
  992. };
  993. const loadQueryTableData = () => {
  994. let filtered = [...queryTableMockSourceData];
  995. if (queryTableFilters.personName) {
  996. filtered = filtered.filter((item) => item.personName.includes(queryTableFilters.personName));
  997. }
  998. if (queryTableFilters.otherName) {
  999. filtered = filtered.filter((item) => item.peerName.includes(queryTableFilters.otherName));
  1000. }
  1001. if (queryTableFilters.scene) {
  1002. filtered = filtered.filter((item) => item.scene === queryTableFilters.scene);
  1003. }
  1004. if (queryTableFilters.riskLevel) {
  1005. filtered = filtered.filter((item) => item.riskLevel === queryTableFilters.riskLevel);
  1006. }
  1007. if (queryTableFilters.startDate) {
  1008. filtered = filtered.filter((item) => item.latestDate >= queryTableFilters.startDate);
  1009. }
  1010. if (queryTableFilters.endDate) {
  1011. filtered = filtered.filter((item) => item.latestDate <= queryTableFilters.endDate);
  1012. }
  1013. if (queryTableFilters.orderKey && queryTableFilters.sort) {
  1014. filtered.sort((left, right) => {
  1015. const leftValue = left[queryTableFilters.orderKey as keyof DemoRow];
  1016. const rightValue = right[queryTableFilters.orderKey as keyof DemoRow];
  1017. const result = String(leftValue ?? '').localeCompare(String(rightValue ?? ''), 'zh-CN', { numeric: true });
  1018. return queryTableFilters.sort === 'ASC' ? result : -result;
  1019. });
  1020. }
  1021. queryTablePagination.total = filtered.length;
  1022. const start = (queryTablePagination.page - 1) * queryTablePagination.limit;
  1023. queryTableData.value = filtered.slice(start, start + queryTablePagination.limit).map((item, index) => ({
  1024. ...item,
  1025. rowKey: item.id,
  1026. id: start + index + 1,
  1027. amountText: formatAmount(item.amount),
  1028. }));
  1029. };
  1030. const applyFilters = () => {
  1031. const keyword = filters.value.keyword.trim();
  1032. const range = filters.value.dateRange;
  1033. tableData.value = mockSourceData.filter((item) => {
  1034. if (filters.value.scene !== 'all' && item.scene !== filters.value.scene) return false;
  1035. if (filters.value.riskLevel !== 'all' && item.riskLevel !== filters.value.riskLevel) return false;
  1036. if (
  1037. keyword &&
  1038. ![item.personName, item.peerName, item.industry, item.region]
  1039. .map((value) => String(value || ''))
  1040. .some((value) => value.includes(keyword))
  1041. ) {
  1042. return false;
  1043. }
  1044. if (range?.[0] && item.latestDate < range[0].format('YYYY-MM-DD')) return false;
  1045. if (range?.[1] && item.latestDate > range[1].format('YYYY-MM-DD')) return false;
  1046. return true;
  1047. });
  1048. if (!tableData.value.length) {
  1049. selectedRow.value = null;
  1050. renderTrendChart([]);
  1051. renderRiskChart([]);
  1052. return;
  1053. }
  1054. const currentId = selectedRow.value?.id;
  1055. selectedRow.value = tableData.value.find((item) => item.id === currentId) || tableData.value[0];
  1056. renderTrendChart(tableData.value);
  1057. renderRiskChart(tableData.value);
  1058. };
  1059. const renderTrendChart = (list: DemoRow[]) => {
  1060. const seriesData = monthLabels.map((_, index) => list.reduce((sum, item) => sum + (item.trend[index] || 0), 0));
  1061. const empty = !list.length;
  1062. const options: EChartsOption = {
  1063. tooltip: { trigger: 'axis' },
  1064. grid: {
  1065. left: '3%',
  1066. right: '3%',
  1067. top: '12%',
  1068. bottom: '8%',
  1069. outerBoundsMode: 'same',
  1070. outerBoundsContain: 'axisLabel',
  1071. },
  1072. xAxis: {
  1073. type: 'category',
  1074. data: empty ? ['暂无数据'] : monthLabels,
  1075. axisTick: { show: false },
  1076. axisLine: { lineStyle: { color: '#d9d9d9' } },
  1077. },
  1078. yAxis: {
  1079. type: 'value',
  1080. axisTick: { show: false },
  1081. splitLine: { lineStyle: { color: '#f0f0f0' } },
  1082. },
  1083. series: [
  1084. {
  1085. name: '命中次数',
  1086. type: 'line',
  1087. smooth: true,
  1088. data: empty ? [0] : seriesData,
  1089. symbol: 'circle',
  1090. symbolSize: 8,
  1091. lineStyle: { color: '#1677ff', width: 2 },
  1092. itemStyle: { color: '#1677ff' },
  1093. areaStyle: { color: 'rgba(22, 119, 255, 0.12)' },
  1094. },
  1095. ],
  1096. };
  1097. setTrendOptions(options);
  1098. };
  1099. const renderRiskChart = (list: DemoRow[]) => {
  1100. const riskData = [
  1101. { name: '高风险', value: list.filter((item) => item.riskLevel === '高').length },
  1102. { name: '中风险', value: list.filter((item) => item.riskLevel === '中').length },
  1103. { name: '低风险', value: list.filter((item) => item.riskLevel === '低').length },
  1104. ].filter((item) => item.value > 0);
  1105. const options: EChartsOption = {
  1106. tooltip: { trigger: 'item' },
  1107. legend: { bottom: 0, icon: 'circle' },
  1108. series: [
  1109. {
  1110. type: 'pie',
  1111. radius: ['42%', '68%'],
  1112. center: ['50%', '45%'],
  1113. label: { formatter: '{b}: {c}' },
  1114. data: riskData.length
  1115. ? riskData
  1116. : [
  1117. {
  1118. name: '暂无数据',
  1119. value: 1,
  1120. itemStyle: { color: '#d9d9d9' },
  1121. },
  1122. ],
  1123. },
  1124. ],
  1125. };
  1126. setRiskOptions(options);
  1127. };
  1128. const handleSearch = () => {
  1129. applyFilters();
  1130. };
  1131. const handleReset = () => {
  1132. filters.value = {
  1133. scene: 'all',
  1134. riskLevel: 'all',
  1135. keyword: '',
  1136. dateRange: undefined,
  1137. };
  1138. applyFilters();
  1139. };
  1140. const handleRowClick = (record: DemoRow) => {
  1141. selectedRow.value = record;
  1142. };
  1143. const rowClassName = (record: any, _index: number): string => {
  1144. const row = record?.record || record;
  1145. return selectedRow.value?.id === row?.id ? 'summary-row-selected' : '';
  1146. };
  1147. const openTransRecordModal = () => {
  1148. if (!ensureSelectedRow()) return;
  1149. transRecordModalOpen.value = true;
  1150. };
  1151. const openDetailModal = () => {
  1152. if (!ensureSelectedRow()) return;
  1153. detailModalOpen.value = true;
  1154. };
  1155. const openFormModal = () => {
  1156. if (!ensureSelectedRow()) return;
  1157. formModalModel.scene = selectedRow.value?.scene || '同行分析';
  1158. formModalModel.owner = '研判一组';
  1159. formModalModel.judgeDate = selectedRow.value?.latestDate || '2026-04-04';
  1160. formModalModel.syncTrack = true;
  1161. formModalModel.remark = `当前对象:${selectedRow.value?.personName || '-'},该表单用于演示项目里的配置或录入类弹窗。`;
  1162. formModalOpen.value = true;
  1163. };
  1164. const openQueryTableModal = () => {
  1165. queryTablePagination.page = 1;
  1166. loadQueryTableData();
  1167. queryTableModalOpen.value = true;
  1168. };
  1169. const openUploadModalExample = () => {
  1170. openUploadModal(true, {});
  1171. };
  1172. const openUploadPreviewExample = () => {
  1173. createMessage.info('预览弹窗当前使用本地 mock 文件,仅用于统一查看组件结构。');
  1174. openUploadPreviewModal(true);
  1175. };
  1176. const openCallRecordModal = () => {
  1177. if (!ensureSelectedRow()) return;
  1178. callRecordModalOpen.value = true;
  1179. };
  1180. const openDrawerExample = () => {
  1181. if (!ensureSelectedRow()) return;
  1182. drawerOpen.value = true;
  1183. };
  1184. const openConfirmExample = () => {
  1185. createConfirm({
  1186. title: '确认弹窗示例',
  1187. content: '是否继续执行这次 mock 研判动作?该确认框用于演示项目里的二次确认交互。',
  1188. iconType: 'warning',
  1189. onOk() {
  1190. createMessage.success('已确认执行');
  1191. },
  1192. });
  1193. };
  1194. const openAppSearchExample = () => {
  1195. appSearchModalOpen.value = true;
  1196. };
  1197. const openCropperExample = () => {
  1198. openCropperModal(true);
  1199. };
  1200. const handleCropperSuccess = (payload: Record<string, any>) => {
  1201. cropperResult.value = payload?.filename || payload?.source || '已完成裁剪';
  1202. createMessage.success('裁剪弹窗已返回结果');
  1203. };
  1204. const openErrorLogExample = () => {
  1205. openErrorLogModal(true);
  1206. };
  1207. const openLockModalExample = () => {
  1208. openLockModal(true);
  1209. };
  1210. const openModalExample = (key: string) => {
  1211. const actionMap: Record<string, () => void> = {
  1212. basic: openDetailModal,
  1213. form: openFormModal,
  1214. drawer: openDrawerExample,
  1215. appSearch: openAppSearchExample,
  1216. upload: openUploadModalExample,
  1217. uploadPreview: openUploadPreviewExample,
  1218. listSelect: () => formListSelectRef.value?.openSelectModal?.(),
  1219. cropper: openCropperExample,
  1220. queryTable: openQueryTableModal,
  1221. transRecord: openTransRecordModal,
  1222. callRecord: openCallRecordModal,
  1223. errorLog: openErrorLogExample,
  1224. lock: openLockModalExample,
  1225. confirm: openConfirmExample,
  1226. };
  1227. actionMap[key]?.();
  1228. };
  1229. const handleFormSubmit = () => {
  1230. formModalSubmitting.value = true;
  1231. window.setTimeout(() => {
  1232. formModalSubmitting.value = false;
  1233. formModalOpen.value = false;
  1234. createMessage.success('表单提交成功(mock)');
  1235. }, 300);
  1236. };
  1237. const handleListSelectSelect = (values: Record<string, any>[]) => {
  1238. selectedUsers.value = values || [];
  1239. };
  1240. const mockUploadParamsApi = async (): Promise<UploadParams> => ({
  1241. maxFileSize: 20 * 1024 * 1024,
  1242. imageAllowSuffixes: 'jpg,jpeg,png,gif,bmp,webp',
  1243. mediaAllowSuffixes: 'mp3,mp4,wav,mov',
  1244. fileAllowSuffixes: 'txt,pdf,doc,docx,xls,xlsx,ppt,pptx,zip,rar,7z',
  1245. });
  1246. const mockUploadApi = (params: any, onUploadProgress?: (event: any) => void) => {
  1247. return new Promise<any>((resolve) => {
  1248. onUploadProgress?.({ loaded: 100, total: 100 });
  1249. window.setTimeout(() => {
  1250. const fileName = params?.fileName || params?.file?.name || 'demo.txt';
  1251. resolve({
  1252. data: {
  1253. fileUpload: {
  1254. id: `mock-${Date.now()}`,
  1255. fileName,
  1256. fileType: String(fileName).split('.').pop() || 'txt',
  1257. fileSort: 0,
  1258. bizKey: String(params?.bizKey || 'ui-scheme-upload'),
  1259. bizType: params?.bizType || 'uiSchemeDemo',
  1260. bizKeyIsLike: '',
  1261. fileEntity: {},
  1262. fileUrl: '',
  1263. },
  1264. },
  1265. });
  1266. }, 240);
  1267. });
  1268. };
  1269. const handleBasicUploadChange = (_value: any, fileList?: Array<Record<string, any>>) => {
  1270. uploadedFiles.value = (fileList || []).map((item) => item?.fileName || '-');
  1271. };
  1272. const handleUploadModalChange = (records: any[]) => {
  1273. uploadedFiles.value = (records || []).map((item) => item?.fileName || '-');
  1274. if (uploadedFiles.value.length) {
  1275. createMessage.success(`已模拟上传 ${uploadedFiles.value.length} 个文件`);
  1276. }
  1277. };
  1278. const handleQueryTableSearch = () => {
  1279. queryTablePagination.page = 1;
  1280. loadQueryTableData();
  1281. };
  1282. const handleQueryTableReset = () => {
  1283. queryTableFilters.personName = '';
  1284. queryTableFilters.otherName = '';
  1285. queryTableFilters.scene = '';
  1286. queryTableFilters.riskLevel = '';
  1287. queryTableFilters.startDate = '';
  1288. queryTableFilters.endDate = '';
  1289. queryTableFilters.orderKey = '';
  1290. queryTableFilters.sort = '';
  1291. queryTablePagination.page = 1;
  1292. loadQueryTableData();
  1293. };
  1294. const handleQueryTablePrevPage = () => {
  1295. if (queryTablePagination.page <= 1) return;
  1296. queryTablePagination.page -= 1;
  1297. loadQueryTableData();
  1298. };
  1299. const handleQueryTableNextPage = () => {
  1300. if (queryTablePagination.page >= queryTableTotalPages.value) return;
  1301. queryTablePagination.page += 1;
  1302. loadQueryTableData();
  1303. };
  1304. const handleQueryTablePageChange = (page: number, pageSize: number) => {
  1305. if (pageSize !== queryTablePagination.limit) {
  1306. handleQueryTablePageSizeChange(pageSize);
  1307. return;
  1308. }
  1309. if (page === queryTablePagination.page - 1) {
  1310. handleQueryTablePrevPage();
  1311. return;
  1312. }
  1313. if (page === queryTablePagination.page + 1) {
  1314. handleQueryTableNextPage();
  1315. return;
  1316. }
  1317. if (page === queryTablePagination.page) return;
  1318. queryTablePagination.page = page;
  1319. loadQueryTableData();
  1320. };
  1321. const handleQueryTablePageSizeChange = (size: number) => {
  1322. queryTablePagination.limit = Number(size);
  1323. queryTablePagination.page = 1;
  1324. loadQueryTableData();
  1325. };
  1326. const handleQueryTableSortChange = (payload: { orderKey: string; sort: 'ASC' | 'DESC' }) => {
  1327. queryTableFilters.orderKey = payload.orderKey;
  1328. queryTableFilters.sort = payload.sort;
  1329. queryTablePagination.page = 1;
  1330. loadQueryTableData();
  1331. };
  1332. onMounted(() => {
  1333. applyFilters();
  1334. loadQueryTableData();
  1335. });
  1336. </script>
  1337. <style lang="less">
  1338. @prefix-cls: ~'jeesite-ui-scheme';
  1339. .@{prefix-cls}.ui-scheme-page {
  1340. background: #fff;
  1341. padding: 12px 16px 24px;
  1342. min-height: 100%;
  1343. height: 100%;
  1344. display: flex;
  1345. flex-direction: column;
  1346. box-sizing: border-box;
  1347. overflow: hidden;
  1348. .page-header {
  1349. margin-bottom: 8px;
  1350. }
  1351. .page-title {
  1352. font-size: 16px;
  1353. font-weight: 600;
  1354. color: #111827;
  1355. padding-bottom: 8px;
  1356. border-bottom: 1px solid #e5e7eb;
  1357. }
  1358. .page-subtitle {
  1359. margin-top: 10px;
  1360. color: #6b7280;
  1361. font-size: 12px;
  1362. line-height: 20px;
  1363. }
  1364. .page-content {
  1365. flex: 1 1 auto;
  1366. min-height: 0;
  1367. display: flex;
  1368. flex-direction: column;
  1369. gap: 12px;
  1370. overflow: auto;
  1371. padding-right: 2px;
  1372. }
  1373. .section-tabs {
  1374. flex: 1 1 auto;
  1375. min-height: 0;
  1376. :deep(.ant-tabs-content-holder) {
  1377. min-height: 0;
  1378. }
  1379. :deep(.ant-tabs-content) {
  1380. height: 100%;
  1381. }
  1382. :deep(.ant-tabs-tabpane) {
  1383. height: 100%;
  1384. }
  1385. }
  1386. .tab-pane {
  1387. display: flex;
  1388. flex-direction: column;
  1389. gap: 12px;
  1390. min-height: 0;
  1391. }
  1392. .filter-bar {
  1393. display: flex;
  1394. flex-wrap: wrap;
  1395. align-items: center;
  1396. gap: 12px;
  1397. padding: 12px 16px;
  1398. border: 1px solid #e5e7eb;
  1399. border-radius: 8px;
  1400. background: #fafafa;
  1401. }
  1402. .filter-item {
  1403. display: flex;
  1404. align-items: center;
  1405. gap: 8px;
  1406. }
  1407. .filter-label {
  1408. color: #6b7280;
  1409. white-space: nowrap;
  1410. font-size: 12px;
  1411. }
  1412. .filter-input {
  1413. width: 160px;
  1414. }
  1415. .filter-input-wide {
  1416. width: 220px;
  1417. }
  1418. .filter-date {
  1419. width: 320px;
  1420. }
  1421. .filter-actions {
  1422. margin-left: auto;
  1423. display: flex;
  1424. gap: 8px;
  1425. }
  1426. .stats-grid {
  1427. display: grid;
  1428. grid-template-columns: repeat(4, minmax(0, 1fr));
  1429. gap: 12px;
  1430. }
  1431. .modal-gallery {
  1432. display: grid;
  1433. grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  1434. gap: 12px;
  1435. }
  1436. .modal-item {
  1437. display: flex;
  1438. flex-direction: column;
  1439. gap: 10px;
  1440. padding: 16px;
  1441. border: 1px solid #e5e7eb;
  1442. border-radius: 8px;
  1443. background: #fafafa;
  1444. }
  1445. .modal-item__title {
  1446. font-size: 14px;
  1447. font-weight: 600;
  1448. color: #111827;
  1449. }
  1450. .modal-item__desc {
  1451. min-height: 44px;
  1452. color: #6b7280;
  1453. font-size: 12px;
  1454. line-height: 20px;
  1455. }
  1456. .modal-item__meta {
  1457. display: flex;
  1458. gap: 8px;
  1459. font-size: 12px;
  1460. line-height: 20px;
  1461. }
  1462. .modal-item__meta-label {
  1463. color: #6b7280;
  1464. flex: 0 0 56px;
  1465. }
  1466. .modal-item__meta-value {
  1467. color: #111827;
  1468. flex: 1 1 auto;
  1469. min-width: 0;
  1470. }
  1471. .stat-card {
  1472. :deep(.ant-card-body) {
  1473. padding: 16px;
  1474. }
  1475. }
  1476. .main-grid {
  1477. display: grid;
  1478. grid-template-columns: minmax(0, 1.6fr) minmax(360px, 1fr);
  1479. gap: 12px;
  1480. min-height: 460px;
  1481. }
  1482. .table-card,
  1483. .detail-card {
  1484. height: 460px;
  1485. :deep(.ant-card-body) {
  1486. height: calc(100% - 57px);
  1487. display: flex;
  1488. flex-direction: column;
  1489. min-height: 0;
  1490. }
  1491. }
  1492. .table-panel {
  1493. flex: 1 1 auto;
  1494. min-height: 0;
  1495. overflow: hidden;
  1496. }
  1497. .detail-panel {
  1498. height: 100%;
  1499. display: flex;
  1500. flex-direction: column;
  1501. gap: 12px;
  1502. min-height: 0;
  1503. }
  1504. .tag-list {
  1505. display: flex;
  1506. flex-wrap: wrap;
  1507. gap: 8px;
  1508. }
  1509. .detail-summary,
  1510. .detail-modal .detail-summary {
  1511. padding: 12px 14px;
  1512. border-radius: 6px;
  1513. background: #fafafa;
  1514. color: #4b5563;
  1515. line-height: 22px;
  1516. }
  1517. .detail-actions,
  1518. .modal-actions {
  1519. display: flex;
  1520. gap: 8px;
  1521. justify-content: flex-end;
  1522. }
  1523. .detail-actions {
  1524. margin-top: auto;
  1525. }
  1526. .chart-grid {
  1527. display: grid;
  1528. grid-template-columns: repeat(2, minmax(0, 1fr));
  1529. gap: 12px;
  1530. }
  1531. .form-grid {
  1532. display: grid;
  1533. grid-template-columns: minmax(0, 1.2fr) minmax(320px, 0.8fr);
  1534. gap: 12px;
  1535. }
  1536. .form-actions {
  1537. display: flex;
  1538. gap: 8px;
  1539. justify-content: flex-end;
  1540. }
  1541. .chart-box {
  1542. height: 280px;
  1543. }
  1544. .detail-modal {
  1545. display: flex;
  1546. flex-direction: column;
  1547. gap: 12px;
  1548. }
  1549. .form-modal-body,
  1550. .drawer-panel {
  1551. display: flex;
  1552. flex-direction: column;
  1553. gap: 12px;
  1554. padding: 16px;
  1555. }
  1556. .mock-form {
  1557. .form-date {
  1558. width: 100%;
  1559. }
  1560. :deep(.ant-form-item) {
  1561. margin-bottom: 14px;
  1562. }
  1563. }
  1564. .drawer-summary {
  1565. padding: 12px 14px;
  1566. border-radius: 6px;
  1567. background: #fafafa;
  1568. color: #4b5563;
  1569. line-height: 22px;
  1570. }
  1571. @media screen and (max-width: 1360px) {
  1572. .stats-grid,
  1573. .modal-gallery,
  1574. .chart-grid,
  1575. .form-grid,
  1576. .main-grid {
  1577. grid-template-columns: 1fr;
  1578. }
  1579. .table-card,
  1580. .detail-card {
  1581. height: auto;
  1582. min-height: 400px;
  1583. }
  1584. }
  1585. @media screen and (max-width: 992px) {
  1586. .filter-actions {
  1587. width: 100%;
  1588. margin-left: 0;
  1589. }
  1590. .filter-input,
  1591. .filter-input-wide,
  1592. .filter-date {
  1593. width: 100%;
  1594. }
  1595. }
  1596. }
  1597. .ui-scheme-query-table-modal {
  1598. .query-table-wrap {
  1599. gap: 14px;
  1600. }
  1601. .query-table-search {
  1602. padding: 14px 16px;
  1603. border: 1px solid #e2e8f0;
  1604. border-radius: 12px;
  1605. background: linear-gradient(180deg, #f8fafc 0%, #ffffff 100%);
  1606. box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.8);
  1607. .label {
  1608. color: #475569;
  1609. font-size: 12px;
  1610. font-weight: 600;
  1611. }
  1612. .ant-input,
  1613. .ant-input-affix-wrapper,
  1614. .ant-picker,
  1615. .ant-select-selector {
  1616. border-radius: 8px !important;
  1617. }
  1618. .ant-btn {
  1619. height: 32px;
  1620. padding: 0 16px;
  1621. border-radius: 8px;
  1622. }
  1623. }
  1624. .ant-modal-body > .scroll-container,
  1625. .ant-modal-body > .scroll-container > .scrollbar__wrap,
  1626. .ant-modal-body > .scroll-container > .scrollbar__wrap > .scrollbar__view,
  1627. .ant-modal-body > .scroll-container > .scrollbar__wrap > .scrollbar__view > div {
  1628. height: 100%;
  1629. min-height: 0;
  1630. }
  1631. .ant-modal-body > .scroll-container > .scrollbar__wrap {
  1632. overflow: hidden;
  1633. }
  1634. .ant-modal-body > .scroll-container > .scrollbar__wrap > .scrollbar__view {
  1635. margin: 0 !important;
  1636. }
  1637. .ant-modal-body > .scroll-container > .scrollbar__bar.is-vertical,
  1638. .ant-modal-body > .scroll-container > .is-vertical {
  1639. display: none !important;
  1640. opacity: 0 !important;
  1641. pointer-events: none;
  1642. }
  1643. }
  1644. @import '../styles/trans-analysis-table-ui.less';
  1645. </style>