Compare commits

..

36 Commits

Author SHA1 Message Date
147c34f4f9 feat(credit): 新增信用管理小程序客户多步骤页面
- 添加 step1、step2、step3 页面实现多步骤客户管理功能
- 集成 ele-pro-table 组件实现客户数据表格展示
- 实现用户信息、拖欠金额、步骤状态等关键字段显示
- 添加文件附件预览和图片缩略图展示功能
- 实现搜索、编辑、删除、批量操作等交互功能
- 集成导出功能支持客户数据导出为 Excel
- 添加自定义列过滤和默认可见列配置
- 实现步骤标签颜色分类和状态标记功能
2026-03-20 21:02:44 +08:00
ce3f22f4ae feat(credit): 添加客户列表跟进人字段显示
- 在表格列配置中增加跟进人列定义
- 设置跟进人列的数据索引为 realName
- 配置跟进人列宽度为 120px 并居中对齐
- 将 realName 字段添加到列表查询参数中
2026-03-19 17:35:57 +08:00
54827a9876 fix(file): 解决OSS图片处理参数导致的文件访问问题
- 在SelectFile组件中为文件链接添加stripOssImageProcess处理
- 从common工具库导入并使用stripOssImageProcess函数
- 在creditMpCustomer编辑页面对文件URL进行图片处理参数清理
- 在creditMpCustomer列表页面添加sanitizeFileUrl函数处理文件URL
- 修改文件规范化逻辑以确保非图片文件移除OSS处理参数
2026-03-19 17:30:21 +08:00
a0a4cc7a8d feat(credit): 更新客户信息编辑组件功能
- 替换城市输入框为省市区级联选择组件
- 新增步骤下拉选择框用于流程状态管理
- 集成文件选择组件替代原有文件路径输入框
- 实现文件上传和删除功能
- 添加文件列表管理和同步逻辑
- 优化搜索组件中关键词搜索框位置
- 完善查询参数过滤逻辑支持数组和单一值处理
2026-03-19 17:19:03 +08:00
56063e9bcd feat(credit): 更新客户信息编辑组件功能
- 替换城市输入框为省市区级联选择组件
- 新增步骤下拉选择框用于流程状态管理
- 集成文件选择组件替代原有文件路径输入框
- 实现文件上传和删除功能
- 添加文件列表管理和同步逻辑
- 优化搜索组件中关键词搜索框位置
- 完善查询参数过滤逻辑支持数组和单一值处理
2026-03-19 17:08:20 +08:00
72c431967d feat(creditMpCustomer): 添加步骤筛选功能并优化文件展示
- 在模型中新增 step 字段用于标识处理进度
- 添加步骤筛选下拉框支持按状态搜索
- 重构文件展示逻辑支持图片预览和文件链接
- 添加步骤标签颜色区分不同处理阶段
- 优化表格列宽度布局提升显示效果
- 实现文件路径解析和缓存机制
2026-03-19 16:18:23 +08:00
1b115edabe feat(credit): 添加小程序客户用户信息展示功能
- 在模型中增加昵称、头像、手机号字段
- 实现用户信息表格列,展示头像、昵称和手机号
- 添加用户信息样式布局
- 更新导出数据配置,包含新的用户信息字段
- 调整表格列宽度和对齐方式以优化显示效果
2026-03-16 23:37:23 +08:00
41a1fcb4ed feat(creditMpCustomer): 添加导出和批量删除功能
- 在搜索组件中添加导出按钮和批量删除按钮
- 实现导出数据功能,支持导出小程序端客户列表
- 添加关键词搜索功能,支持按关键词过滤数据
- 实现批量删除功能,支持选择多个客户进行删除
- 更新表格配置,添加行选择和搜索参数传递
- 修改数据源查询逻辑,支持关键词和状态筛选
- 添加文件下载图标和删除图标依赖
2026-03-16 21:39:57 +08:00
c79c68e7a1 refactor(creditMpCustomer): 移除不必要的表单项并优化列表配置
- 移除了链接、状态、企业ID、所在省份、所在辖区、是否有数据、是否推荐等表单项
- 将文件上传字段从image重命名为files以保持一致性
- 更新了表格列配置,移除了对应的显示列
- 修改了删除功能中的主键引用从creditMpCustomerId改为id
- 调整了默认显示的核心列配置
- 移除了修改时间字段的显示与相关逻辑
2026-03-16 21:26:39 +08:00
5182112b72 feat(credit): 添加小程序端客户管理功能
- 创建小程序端客户数据模型和API接口
- 实现分页查询、新增、修改、删除等基础CRUD操作
- 添加小程序端客户管理页面和编辑弹窗组件
- 集成表格展示、搜索、批量操作等功能
- 配置开发环境API地址为http://127.0.0.1:9200/api
2026-03-16 21:08:51 +08:00
a9d008fb90 feat(credit): 添加客户导入功能并优化信用模块界面
- 在客户导入API中添加isCustomer参数标识
- 为信用相关页面集成公司ID刷新按钮组件
- 在竞争者、客户、外部、风险关系和供应商模块中添加刷新功能
- 配置开发环境变量注释API地址配置
- 统一信用模块工具栏组件导入和使用方式
2026-03-15 13:01:41 +08:00
6827855b78 feat(credit): 添加企业客户导入功能
- 在CreditCompanyParam接口中新增type和isCustomer字段
- 实现importCustomer API方法用于导入客户Excel文件
- 创建credit-company-import组件实现企业批量导入功能
- 开发credit-company-related-import组件用于企业详情子表导入
- 添加creditCompanyEdit组件提供企业信息编辑功能
- 集成导入功能到客户管理页面并更新搜索组件
- 配置表格列展示企业详细信息并设置滚动宽度
- 实现导入模板下载和文件类型验证功能
2026-03-12 22:24:58 +08:00
3f2ab2cc40 feat(credit): 添加涉案金额字段显示并调整开发环境配置
- 在信用判决债务人页面添加 involvedAmount 字段模板
- 当 involvedAmount 为空时回退到 amount 字段显示
- 注释掉开发环境中的 VITE_API_URL 配置项以避免冲突
2026-03-05 09:01:13 +08:00
a0170f820e feat(components): 添加社区和门店选择组件
- 新增 SelectCommunity 组件用于选择社区数据
- 新增 SelectShopStore 组件用于选择门店数据
- 实现社区选择的数据表格和搜索功能
- 实现门店选择的数据表格和搜索功能
- 集成弹窗模式的选择交互界面
- 添加相应的 API 接口调用支持
2026-03-04 14:10:54 +08:00
a9d513fca4 feat(shop): 添加小区、门店、配送员、店员、仓库管理功能及相关API
- 新增小区管理模块,包括查询、添加、修改、删除等功能
- 新增门店管理模块,包含门店基础信息管理功能
- 新增配送员管理模块,支持骑手信息配置及工作状态管理
- 新增店员管理模块,用于门店人员信息维护
- 新增仓库管理模块,支持不同类型的仓库信息管理
- 添加电子围栏功能,支持圆形和方形围栏设置
- 更新经销商用户模型,增加类型、头像、店铺名称等字段
- 更新订单模型,添加物流单号、昵称等字段
- 更新商品模型,调整分销佣金相关字段设计
- 修复信用模块中部分字段映射错误问题
- 优化邀请链接生成逻辑,增加租户ID参数传递
- 移除部分不必要部门字段显示,简化管理员界面
2026-03-04 14:04:57 +08:00
fe20f0f0b3 feat(credit): 新增附近企业批量导入功能并优化数据表结构
- 新增 importCreditNearbyCompanyMulti 函数支持多文件同时导入
- 提取 buildImportFormData 工具函数统一处理文件上传逻辑
- 在信用公司页面新增批量导入按钮和上传图标组件
- 将限制高消费表格中的发生时间字段从 occurrenceTime 改为 releaseDate
- 调整股权冻结页面的数据状态列顺序和显示逻辑
- 移除附近企业页面中重复和不必要的表格列配置
- 修改附近企业页面通信地址邮箱列为正确的通信地址邮编
- 更新开发环境配置文件启用本地 API 地址并
2026-03-03 16:32:16 +08:00
b1147f7a3d feat(credit): 添加附近企业批量导入功能
- 新增 ImportBatchCreditNearbyCompanyItem 和 ImportBatchCreditNearbyCompanyResult 接口定义
- 实现 importBatchCreditNearbyCompany 函数支持多文件循环导入
- 添加 stopOnError 选项控制遇到失败时是否停止导入
- 返回详细的导入结果统计(总数、成功数、失败数及明细)
- 确保即使有失败也不 reject,便于上层展示部分成功/失败明细
2026-03-02 15:17:21 +08:00
c29daa5293 feat(credit): 添加历史司法数据导入功能
- 新增立案信息历史数据导入API和组件
- 新增送达公告历史数据导入API和组件
- 新增诉前调解历史数据导入API和组件
- 在企业详情页面添加各类型历史数据导入按钮
- 实现Excel文件上传和模板下载功能
- 添加文件类型和大小验证机制
2026-03-02 14:47:10 +08:00
b4f3dcbb18 feat(credit): 更新表格选择模式并添加新的列模板
- 将多个信用模块表格的选择属性从 :selection 改为 v-model:selection
- 在信用判决债务人页面添加 appellee 列的模板显示
- 在信用执行信息页面添加 plaintiffAppellant 列的模板显示
- 优化表格组件的数据绑定方式
2026-03-02 14:25:32 +08:00
ddb311b82f fix(credit): 修正信用违约和公司信息页面字段映射错误
- 修复信用违约页面中原告/上诉人与被告/被上诉人的字段映射颠倒问题
- 修正信用公司信息组件中当事人的字段配置错误
- 注释开发环境中的API URL配置以避免冲突
2026-03-02 14:08:20 +08:00
6a9be80811 refactor(setting): 更新设置组件以支持基于键值的更新
- 将 updateSetting 替换为 updateSettingByKey 方法调用
- 为所有设置组件初始化 settingKey 和 settingId 字段
- 重构 watch 监听逻辑以支持多类型数据格式处理
- 实现对 content 字段的字符串和对象格式解析
- 添加对数组和对象数据类型的兼容性支持
- 优化表单数据赋值和重置逻辑
- 统一各组件中的数据处理流程
2026-02-27 22:59:51 +08:00
75d31dd09e fix(credit): 修复被执行人的appellee字段显示问题
- 在customRender函数中为appellee字段添加默认值'-'
- 解决当name1和appellee都为空时显示undefined的问题
- 确保被执行人的appellee字段始终有正确的显示值
2026-02-14 18:09:57 +08:00
d3345d14d6 style(credit): 格式化代码并优化表格渲染逻辑
- 统一API请求参数格式化,在多个信用模块中添加对象参数换行
- 移除多余空行和尾随逗号,保持代码整洁
- 优化信用公司组件中的表格列配置,修复被执行人字段显示逻辑
- 调整信用案件导入模板下载链接的布局格式
- 优化表格数据分页计算,改进总数判断逻辑
- 调整搜索组件中注释按钮的缩进格式
- 修复信用司法页面的导入按钮组件路径引用格式
2026-02-14 17:37:48 +08:00
ff186910da feat(credit): 更新信用 judgment debtor 模型并添加排序功能
- 在 creditJudgmentDebtor 模型中添加原告/上诉人、被告/被上诉人、其他当事人/第三人字段
- 修复模型中 name 字段注释并添加 name1 备用字段
- 在 creditCompanyInfo 组件中调整字段映射,将 name1 移至正确位置
- 修复 freezeDateEnd 字段映射错误
- 为 creditCustomer、creditJudgmentDebtor 和 creditSupplier 页面的表格列添加排序功能
- 更新相关表格列配置以支持 sorter 属性
2026-02-14 17:27:46 +08:00
338a2b9d72 feat(credit): 更新司法文书相关字段和表格列配置
- 在信用司法文书模型中添加文书类型字段 documentType
- 将导入按钮文本从"导入(多)"更改为"导入(单)"
- 统一将 plaintiffUser 字段重命名为 plaintiffAppellant,defendantUser 重命名为 appellee
- 更新多个表格列配置中的字段映射关系
- 修改文书类型列的数据索引从 type 到 documentType
- 移除重复的冻结日期结束列并优化股权冻结表格结构
2026-02-14 11:09:57 +08:00
36806bac58 feat(credit): 更新信用模块表格字段配置
- 在信用公司信息组件中为对外投资和案件相关表格添加创建时间字段
- 调整被执行人表格的列顺序并重命名相关字段如原告/上诉人、被告/被上诉人等
- 修改涉案金额字段名为involvedAmount并在判决债务人页面添加创建时间列
- 重新排列信用用户页面中标的金额和操作人列的顺序
- 更新限制高消费页面的涉案金额和法院标题文案
2026-02-14 09:55:21 +08:00
952a082752 style(table): 调整表格列宽和对齐样式
- 调整信用公司页面表格列宽:原文件导入名称和系统匹配企业名称从240调整为280
- 调整信用公司页面表格列宽:登记状态从120调整为180
- 调整信用公司页面表格列宽:参保人数所属年报从140调整为180
- 移除信用公司页面更多邮箱、企业类型和纳税人识别号的固定宽度设置
- 为信用公司页面曾用名和英文名添加省略号显示
- 为信用客户页面状态、销售金额、公开日期和数据来源列添加居中对齐和固定宽度
- 修复信用客户页面重复的排序器配置
- 在信用用户页面启用并配置创建时间列的显示和格式化
2026-02-12 15:48:47 +08:00
9febd8016c feat(credit): 信用模块历史数据导入功能优化
- 为所有信用模块的导入组件添加companyId参数支持
- 在企业详情页面为各信用分类标签页添加历史数据导入按钮
- 移除独立的历史导入按钮,整合到企业详情页的对应标签页中
- 更新导入功能的API调用以传递关联企业ID
- 修复法院公告相关组件的命名一致性问题
- 重构导入完成后的数据刷新逻辑以匹配新的标签页结构
2026-02-11 17:55:23 +08:00
f01e3359ec style(credit): 格式化代码并移除未使用的方法
- 移除了 CreditSearchToolbar 组件中的未使用 add 方法
- 格式化了多个信用模块中的组件标签换行
- 格式化了 Ant Design 图标导入语句的换行
- 为表格列添加了排序功能 (sorter: true)
- 格式化了 creditGqdj 模块中的长对象属性换行
- 格式化了 creditNearbyCompany 模块中的注释和表格配置
- 移除了重复的 sorter 属性配置
2026-02-06 18:53:54 +08:00
8d1d9d9de9 feat(credit): 更新信用判决债务人页面表格字段
- 新增原告/上诉人、被告/被上诉人、其他当事人/第三人字段
- 新增发生时间字段并调整宽度为120
- 将立案日期字段重命名为发生时间
- 将执行标的字段重命名为涉案金额
- 移除证件号/组织机构代码字段
- 调整表格列配置顺序和结构
2026-02-06 16:54:19 +08:00
c16b857505 refactor(credit): 更新搜索工具栏组件以支持动态模块配置
- 修改 CreditSearchToolbar 组件以接收 module 属性并传递给 RefreshCompanyIdButton
- 在所有信用相关页面中添加对应的 module 属性配置
- 优化组件代码格式化和导入语句
- 添加 reload 方法以在刷新 companyId 后保持搜索条件
- 修复图标组件标签闭合格式问题
2026-02-06 13:20:52 +08:00
d97d279670 refactor(credit): 移除行政许可模型中的无效注解
- 移除了 licenseContent 字段上的 TableField 注解
- 保留了许可内容字段的其他功能实现
- 清理了相关的元数据映射配置
2026-02-06 12:52:16 +08:00
c18120fe81 fix(credit): 修复信用评估页面状态字段映射错误
- 将表格列的dataIndex从'status'更正为'dataType'
- 将表格列的key从'status'更正为'dataType'
- 确保数据绑定与实际数据结构一致
2026-01-31 13:22:46 +08:00
2d7e44e0ad refactor(credit): 更新信用模块表格字段映射
- 将 plaintiffUser 字段统一重命名为 plaintiffAppellant
- 将被告字段从 plaintiffUser 更改为 appellee
- 在最终版本页面中修正 dataType 字段映射为正确的当事人字段
- 在股权冻结页面添加 appellee2 回退显示逻辑
- 修正信用修复页面的被告字段映射为 defendantUser
2026-01-31 02:17:27 +08:00
c5908f4376 refactor(credit): 移除后端标记字段并优化数据统计逻辑
- 删除了不再使用的 hasData 字段及其相关注释
- 移除了 normalizeHasData 工具函数
- 将总数统计逻辑改为基于后端返回的 count 字段或列表长度
- 更新了标签页数据检测逻辑为基于记录数判断
- 修正了标签页高亮显示的注释说明
2026-01-28 23:53:10 +08:00
f78aa97bd1 feat(credit): 为企业信用模块添加数据统计和标签高亮功能
- 在CreditCompany模型中新增多个信用相关记录数字段用于数据统计
- 实现标签页数据高亮显示功能,通过记录数判断标签是否有数据
- 添加tabCountFieldMap映射配置不同标签对应的记录数字段
- 实现syncTabsHasDataFromCounts方法同步标签数据状态
- 新增normalizeHasData方法规范化数据存在性判断逻辑
- 添加CSS样式实现有数据标签的红色高亮显示效果
- 更新开发环境API地址配置并移除注释标记
2026-01-28 23:45:27 +08:00
174 changed files with 20751 additions and 3122 deletions

View File

@@ -1,6 +1,7 @@
VITE_APP_NAME=后台管理(开发环境)
#VITE_API_URL=http://127.0.0.1:9200/api
VITE_API_URL=http://127.0.0.1:9200/api
#VITE_SERVER_API_URL=http://127.0.0.1:8000/api
#VITE_API_URL=https://cms-api.s209.websoft.top/api
#VITE_API_URL=https://ysb-api.websoft.top/api
#VITE_SERVER_API_URL=https://server.websoft.top/api

62
a.html Normal file
View File

@@ -0,0 +1,62 @@
<p><strong>桂乐淘后台管理手册</strong></p>
<p><strong>后台登录信息:</strong></p>
<p><strong>地址:</strong><a href="login"><strong>https://glt.websoft.top/login</strong></a><strong><br /></strong><strong>账号13800010584</strong><strong><br /></strong><strong>验证码183911</strong></p>
<p><strong>一、创建管理员账号</strong></p>
<p>(一)创建流程</p>
<p>主页-用户管理-设置手机号-角色设置(将该手机号设置为对应的角色权限)-打开管理员开关,即可创建管理员账号-搜索到对应管理员手机号-勾选该账号-重置密码-复制密码给管理员-打开后台登录地址-使用手机号+登录密码+核验码进行登录-登录成功后再自行修改登录密码。</p>
<p><strong>&nbsp;<img src="https://oss.wsdns.cn/20260306/4ce09bd61eb549a3b955490ba2b489d8.png" /></strong></p>
<p><img src="https://oss.wsdns.cn/20260306/a2814cc44e5c41909a513b53b13e2918.png" /></p>
<p><img src="https://oss.wsdns.cn/20260306/205744c621a045b5a2557456dabd25de.png" /></p>
<p><img src="https://oss.wsdns.cn/20260306/f151c0a2571540f1bb54f73e0009db07.png" /></p>
<p><img src="https://oss.wsdns.cn/20260306/fcc150c5e8294b9d990c0eb7db51eed4.png" /></p>
<p>&nbsp;</p>
<p>(二)角色管理</p>
<p>新建或修改角色-分配权限(按角色需要勾选权限)</p>
<p>1.角色设置</p>
<p><img src="https://oss.wsdns.cn/20260306/44dcfdd7e424434788c52a5e3b129b05.png" /></p>
<p>2.角色名称</p>
<p><img src="https://oss.wsdns.cn/20260306/d7405372b30647f99042a1667fba1713.png" /></p>
<p><img src="https://oss.wsdns.cn/20260306/406cb1aab12c45d4ac6aa550bb76b0c2.png" /></p>
<p><strong>&nbsp;</strong></p>
<p><strong>二、业务管理(创建商品-创建水票-配送)</strong></p>
<p>(一)商品创建流程</p>
<p>新增或编辑商品,按字段填写或上传商品信息,保存后即可(可自主管理上下架及商品排序),在商品管理模块添加的是普通的快递商品,不属于水票套票</p>
<p><img src="https://oss.wsdns.cn/20260306/9547293d358c4de0834f39301a68967a.png" /></p>
<p><img src="https://oss.wsdns.cn/20260306/8f7fd971e3bf48c8bf57ee8ff88dc6b0.png" /></p>
<p>&nbsp;</p>
<p>(二)水票套票创建及配送</p>
<p>基于商品管理里的商品信息,进入水票-模板管理-填写模板规则</p>
<p><img src="https://oss.wsdns.cn/20260306/72f6691aacab43d5accacc3d585b2c57.png" /></p>
<p><img src="https://oss.wsdns.cn/20260306/62a4c06e5d7c4f35980eeb4f661fb7a2.png" /></p>
<p><img src="https://oss.wsdns.cn/20260306/4e6a9764ffe24079ae90701254d695ea.png" /></p>
<p><img src="https://oss.wsdns.cn/20260306/0528a0acf7a9483b973a521f8255e6a9.png" /></p>
<p>(三)配送</p>
<p>用户购买水票后,系统自动释放水票模板的首期水票,并自动生成配送订单到用户指定地址(须在电子围栏内)</p>
<p>配送员添加:接单-配送员管理-新增/修改配送员信息</p>
<p><img src="https://oss.wsdns.cn/20260306/853717ca88a046b7bd11f82a7dd1a17d.png" /></p>
<p><img src="https://oss.wsdns.cn/20260306/9d5b41ccbc7243629b98bd1b5e96d000.png" /></p>
<p><img src="https://oss.wsdns.cn/20260306/5bdaadaa39e14046bf1d08c34d745bc3.png" /></p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p><strong>&nbsp;</strong></p>
<p><strong>三、分销</strong></p>
<p>(一)分销用户</p>
<p><img src="https://oss.wsdns.cn/20260306/964092d296074f0a9df2b9f46d7ef9f4.png" /></p>
<p>(二)推荐关系</p>
<p><img src="https://oss.wsdns.cn/20260306/f299eb173590496c84cd545425c82438.png" /></p>
<p>(三)分销订单</p>
<p><strong><img src="https://oss.wsdns.cn/20260306/ddf51b18abee477eabc3335d5535e9c4.png" /></strong></p>
<p><strong>用户操作手册</strong></p>
<p><strong>一、水票购买</strong></p>
<p>用户可按指引购买水票,选择地址时,如不在电子围栏范围内,无法付款配送。</p>
<p><img src="https://oss.wsdns.cn/20260306/880543fbd60a44d98f79445df0f45115.png" /> &nbsp;<img src="https://oss.wsdns.cn/20260306/f7f628046e384e51a3df99caa320a8a8.png" /><img src="https://oss.wsdns.cn/20260306/515f0893e8d24e0c8ba4a2288f3b67f9.png" /></p>
<p><strong>二、我的水票</strong></p>
<p>用户查看自己购买过的水票明细及使用、释放情况等</p>
<p><img src="https://oss.wsdns.cn/20260306/a482517619654972a713a628109f2cf7.png" /></p>
<p><strong>&nbsp;</strong></p>
<p><strong></strong><strong></strong><strong>立即送水</strong></p>
<p>用户查看自己购买过的水票明细及使用、释放情况等</p>
<p>&nbsp; &nbsp;<img src="https://oss.wsdns.cn/20260306/66a9645153d94ca6b598ae9813516179.png" /><img src="https://oss.wsdns.cn/20260306/06ecf42a53f8411a88ade40e797ed9e9.png" /></p>
<p><strong>三、</strong><strong>分享中心</strong></p>
<p>用户查看自己的推广佣金情况、推广的用户列表,申请提现等</p>
<p>&nbsp; <img src="https://oss.wsdns.cn/20260306/eb6d0d4240374549945a14f6c6b3e179.png" /><img src="https://oss.wsdns.cn/20260306/6036ca3fe4a0438baa3a680a62d2da7b.png" /></p>

BIN
dist.zip

Binary file not shown.

View File

@@ -26,4 +26,3 @@ export async function refreshCreditCompanyId(
}
return Promise.reject(new Error(res.data.message));
}

View File

@@ -15,7 +15,9 @@ export async function pageCreditAdministrativeLicense(
) {
const res = await request.get<
ApiResult<PageResult<CreditAdministrativeLicense>>
>('/credit/credit-administrative-license/page', { params: withCreditUserScope(params) });
>('/credit/credit-administrative-license/page', {
params: withCreditUserScope(params)
});
if (res.data.code === 0) {
return res.data.data;
}

View File

@@ -22,7 +22,6 @@ export interface CreditAdministrativeLicense {
validityEnd?: string;
// 许可机关
licensingAuthority?: string;
@TableField("License_content")
// 许可内容
licenseContent?: string;
// 数据来源单位

View File

@@ -127,3 +127,30 @@ export async function importCreditCaseFiling(file: File, companyId?: number) {
}
return Promise.reject(new Error(res.data.message));
}
/**
* 导入历史立案信息司法大数据
*/
export async function importCreditCaseFilingHistory(
file: File,
companyId?: number
) {
const formData = new FormData();
formData.append('file', file);
if (companyId != null) {
formData.append('companyId', String(companyId));
}
const res = await request.post<ApiResult<unknown>>(
'/credit/credit-case-filing/import/history',
formData,
{
headers: {
'Content-Type': 'multipart/form-data'
}
}
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}

View File

@@ -123,6 +123,28 @@ export async function importCreditCompany(file: File) {
return Promise.reject(new Error(res.data.message));
}
/**
* 导入客户
*/
export async function importCustomer(file: File) {
const formData = new FormData();
formData.append('file', file);
formData.append('isCustomer', '1');
const res = await request.post<ApiResult<unknown>>(
'/credit/credit-company/import',
formData,
{
headers: {
'Content-Type': 'multipart/form-data'
}
}
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 根据企业匹配名称查询关联信息
*/

View File

@@ -108,6 +108,56 @@ export interface CreditCompany {
cechnologyLevel?: string;
// 是否小微企业
smallEnterprise?: string;
// 记录数
creditAdministrativeLicense?: number;
// 记录数
creditBankruptcy?: number;
// 记录数
creditBranch?: number;
// 记录数
creditBreachOfTrust?: number;
// 记录数
creditCaseFiling?: number;
// 记录数
creditCompetitor?: number;
// 记录数
creditCourtAnnouncement?: number;
// 记录数
creditCourtSession?: number;
// 记录数
creditCustomer?: number;
// 记录数
creditDeliveryNotice?: number;
// 记录数
creditExternal?: number;
// 记录数
creditFinalVersion?: number;
// 记录数
creditGqdj?: number;
// 记录数
creditHistoricalLegalPerson?: number;
// 记录数
creditJudgmentDebtor?: number;
// 记录数
creditJudicialDocument?: number;
// 记录数
creditJudiciary?: number;
// 记录数
creditMediation?: number;
// 记录数
creditNearbyCompany?: number;
// 记录数
creditPatent?: number;
// 记录数
creditRiskRelation?: number;
// 记录数
creditSupplier?: number;
// 记录数
creditSuspectedRelationship?: number;
// 记录数
creditUser?: number;
// 记录数
creditXgxf?: number;
// 备注
comments?: string;
// 是否推荐
@@ -133,7 +183,8 @@ export interface CreditCompany {
*/
export interface CreditCompanyParam extends PageParam {
userId?: number;
type?: number;
isCustomer?: number;
id?: number;
keywords?: string;
}

View File

@@ -134,3 +134,30 @@ export async function importCreditDeliveryNotice(
}
return Promise.reject(new Error(res.data.message));
}
/**
* 导入历史送达公告司法大数据
*/
export async function importCreditDeliveryNoticeHistory(
file: File,
companyId?: number
) {
const formData = new FormData();
formData.append('file', file);
if (companyId != null) {
formData.append('companyId', String(companyId));
}
const res = await request.post<ApiResult<unknown>>(
'/credit/credit-delivery-notice/import/history',
formData,
{
headers: {
'Content-Type': 'multipart/form-data'
}
}
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}

View File

@@ -15,7 +15,9 @@ export async function pageCreditHistoricalLegalPerson(
) {
const res = await request.get<
ApiResult<PageResult<CreditHistoricalLegalPerson>>
>('/credit/credit-historical-legal-person/page', { params: withCreditUserScope(params) });
>('/credit/credit-historical-legal-person/page', {
params: withCreditUserScope(params)
});
if (res.data.code === 0) {
return res.data.data;
}

View File

@@ -8,7 +8,7 @@ export interface CreditJudgmentDebtor {
id?: number;
// 案号
caseNumber?: string;
// 被执行人名称
//
name?: string;
// 证件号/组织机构代码
code?: string;
@@ -44,6 +44,14 @@ export interface CreditJudgmentDebtor {
updateTime?: string;
// 历史ID
historyId?: number;
// 原告/上诉人
plaintiffAppellant?: string;
// 被告/被上诉人
appellee?: string;
// 其他当事人/第三人
otherPartiesThirdParty?: string;
// 被执行人名称
name1?: string;
}
/**

View File

@@ -8,6 +8,8 @@ export interface CreditJudicialDocument {
id?: number;
// 文书标题
title?: string;
// 文书类型
documentType?: string;
// 数据类型
dataType?: string;
// 原告/上诉人

View File

@@ -125,3 +125,30 @@ export async function importCreditMediation(file: File, companyId?: number) {
}
return Promise.reject(new Error(res.data.message));
}
/**
* 导入历史诉前调解司法大数据
*/
export async function importCreditMediationHistory(
file: File,
companyId?: number
) {
const formData = new FormData();
formData.append('file', file);
if (companyId != null) {
formData.append('companyId', String(companyId));
}
const res = await request.post<ApiResult<unknown>>(
'/credit/credit-mediation/import/history',
formData,
{
headers: {
'Content-Type': 'multipart/form-data'
}
}
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}

View File

@@ -0,0 +1,105 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { CreditMpCustomer, CreditMpCustomerParam } from './model';
/**
* 分页查询小程序端客户
*/
export async function pageCreditMpCustomer(params: CreditMpCustomerParam) {
const res = await request.get<ApiResult<PageResult<CreditMpCustomer>>>(
'/credit/credit-mp-customer/page',
{
params
}
);
if (res.data.code === 0) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 查询小程序端客户列表
*/
export async function listCreditMpCustomer(params?: CreditMpCustomerParam) {
const res = await request.get<ApiResult<CreditMpCustomer[]>>(
'/credit/credit-mp-customer',
{
params
}
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 添加小程序端客户
*/
export async function addCreditMpCustomer(data: CreditMpCustomer) {
const res = await request.post<ApiResult<unknown>>(
'/credit/credit-mp-customer',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 修改小程序端客户
*/
export async function updateCreditMpCustomer(data: CreditMpCustomer) {
const res = await request.put<ApiResult<unknown>>(
'/credit/credit-mp-customer',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 删除小程序端客户
*/
export async function removeCreditMpCustomer(id?: number) {
const res = await request.delete<ApiResult<unknown>>(
'/credit/credit-mp-customer/' + id
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 批量删除小程序端客户
*/
export async function removeBatchCreditMpCustomer(data: (number | undefined)[]) {
const res = await request.delete<ApiResult<unknown>>(
'/credit/credit-mp-customer/batch',
{
data
}
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 根据id查询小程序端客户
*/
export async function getCreditMpCustomer(id: number) {
const res = await request.get<ApiResult<CreditMpCustomer>>(
'/credit/credit-mp-customer/' + id
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}

View File

@@ -0,0 +1,66 @@
import type { PageParam } from '@/api';
/**
* 小程序端客户
*/
export interface CreditMpCustomer {
// ID
id?: number;
// 拖欠方
toUser?: string;
// 拖欠金额
price?: string;
// 拖欠年数
years?: string;
// 链接
url?: string;
// 状态
statusTxt?: string;
// 企业ID
companyId?: number;
// 所在省份
province?: string;
// 所在城市
city?: string;
// 所在辖区
region?: string;
// 文件路径
files?: string;
// 步骤, 0未受理1已受理2材料提交3合同签订4执行回款5完结
step?: number;
// 是否有数据
hasData?: string;
// 备注
comments?: string;
// 是否推荐
recommend?: number;
// 排序(数字越小越靠前)
sortNumber?: number;
// 状态, 0正常, 1冻结
status?: number;
// 是否删除, 0否, 1是
deleted?: number;
// 用户ID
userId?: number;
// 昵称
nickname?: string;
// 头像
avatar?: string;
// 手机号
phone?: string;
// 租户id
tenantId?: number;
// 创建时间
createTime?: string;
// 修改时间
updateTime?: string;
}
/**
* 小程序端客户搜索条件
*/
export interface CreditMpCustomerParam extends PageParam {
id?: number;
keywords?: string;
step?: number;
}

View File

@@ -115,11 +115,7 @@ export async function importCreditNearbyCompany(
file: File,
companyId?: number
) {
const formData = new FormData();
formData.append('file', file);
if (companyId != null) {
formData.append('companyId', String(companyId));
}
const formData = buildImportFormData(file, companyId);
const res = await request.post<ApiResult<unknown>>(
'/credit/credit-nearby-company/import',
formData,
@@ -134,3 +130,104 @@ export async function importCreditNearbyCompany(
}
return Promise.reject(new Error(res.data.message));
}
/**
* 导入附近企业(多文件,同一次请求)
*
* 约定:后端需支持 multipart 同名字段 `file` 多次出现(常见于 MultipartFile[] file
*/
export async function importCreditNearbyCompanyMulti(
files: File[] | FileList,
companyId?: number
) {
const formData = buildImportFormData(files, companyId);
const res = await request.post<ApiResult<unknown>>(
'/credit/credit-nearby-company/import',
formData,
{
headers: {
'Content-Type': 'multipart/form-data'
}
}
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
function buildImportFormData(
fileOrFiles: File | File[] | FileList,
companyId?: number
) {
const formData = new FormData();
if (Array.isArray(fileOrFiles)) {
fileOrFiles.forEach((f) => formData.append('file', f));
} else if (fileOrFiles instanceof FileList) {
Array.from(fileOrFiles).forEach((f) => formData.append('file', f));
} else {
formData.append('file', fileOrFiles);
}
if (companyId != null) {
formData.append('companyId', String(companyId));
}
return formData;
}
export interface ImportBatchCreditNearbyCompanyItem {
fileName: string;
success: boolean;
message: string;
}
export interface ImportBatchCreditNearbyCompanyResult {
total: number;
success: number;
failure: number;
items: ImportBatchCreditNearbyCompanyItem[];
}
/**
* 导入附近企业(多文件)
*
* 说明:
* - 目前复用单文件导入接口,前端循环调用实现“多文件导入”
* - 不会在有失败时 reject便于上层展示部分成功/失败明细)
*/
export async function importBatchCreditNearbyCompany(
files: File[] | FileList,
companyId?: number,
options?: {
/** 遇到失败是否立即停止,默认 false继续导入后续文件 */
stopOnError?: boolean;
}
): Promise<ImportBatchCreditNearbyCompanyResult> {
const list = Array.isArray(files) ? files : Array.from(files);
const items: ImportBatchCreditNearbyCompanyItem[] = [];
const stopOnError = options?.stopOnError ?? false;
for (const file of list) {
try {
const msg = await importCreditNearbyCompany(file, companyId);
items.push({ fileName: file.name, success: true, message: msg });
} catch (e: unknown) {
items.push({
fileName: file.name,
success: false,
message: e instanceof Error ? e.message : String(e)
});
if (stopOnError) {
break;
}
}
}
const success = items.filter((d) => d.success).length;
const failure = items.length - success;
return {
total: list.length,
success,
failure,
items
};
}

View File

@@ -15,7 +15,9 @@ export async function pageCreditSuspectedRelationship(
) {
const res = await request.get<
ApiResult<PageResult<CreditSuspectedRelationship>>
>('/credit/credit-suspected-relationship/page', { params: withCreditUserScope(params) });
>('/credit/credit-suspected-relationship/page', {
params: withCreditUserScope(params)
});
if (res.data.code === 0) {
return res.data.data;
}

View File

@@ -0,0 +1,105 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { ShopCommunity, ShopCommunityParam } from './model';
/**
* 分页查询小区
*/
export async function pageShopCommunity(params: ShopCommunityParam) {
const res = await request.get<ApiResult<PageResult<ShopCommunity>>>(
'/shop/shop-community/page',
{
params
}
);
if (res.data.code === 0) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 查询小区列表
*/
export async function listShopCommunity(params?: ShopCommunityParam) {
const res = await request.get<ApiResult<ShopCommunity[]>>(
'/shop/shop-community',
{
params
}
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 添加小区
*/
export async function addShopCommunity(data: ShopCommunity) {
const res = await request.post<ApiResult<unknown>>(
'/shop/shop-community',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 修改小区
*/
export async function updateShopCommunity(data: ShopCommunity) {
const res = await request.put<ApiResult<unknown>>(
'/shop/shop-community',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 删除小区
*/
export async function removeShopCommunity(id?: number) {
const res = await request.delete<ApiResult<unknown>>(
'/shop/shop-community/' + id
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 批量删除小区
*/
export async function removeBatchShopCommunity(data: (number | undefined)[]) {
const res = await request.delete<ApiResult<unknown>>(
'/shop/shop-community/batch',
{
data
}
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 根据id查询小区
*/
export async function getShopCommunity(id: number) {
const res = await request.get<ApiResult<ShopCommunity>>(
'/shop/shop-community/' + id
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}

View File

@@ -0,0 +1,35 @@
import type { PageParam } from '@/api';
/**
* 小区
*/
export interface ShopCommunity {
// ID
id?: number;
// 小区名称
name?: string;
// 小区编号
code?: string;
// 详细地址
address?: string;
// 排序(数字越小越靠前)
sortNumber?: number;
// 备注
comments?: string;
// 状态, 0正常, 1冻结
status?: number;
// 租户id
tenantId?: number;
// 创建时间
createTime?: string;
}
/**
* 小区搜索条件
*/
export interface ShopCommunityParam extends PageParam {
id?: number;
name?: string;
code?: string;
keywords?: string;
}

View File

@@ -8,6 +8,8 @@ export interface ShopDealerCapital {
id?: number;
// 分销商用户ID
userId?: number;
// 分销商昵称
nickName?: string;
// 订单ID
orderId?: number;
// 订单编号

View File

@@ -12,6 +12,8 @@ export interface ShopDealerOrder {
title?: string;
// 买家用户昵称
nickname?: string;
// 真实姓名
realName?: string;
// 订单编号
orderNo?: string;
// 订单总金额(不含运费)
@@ -52,6 +54,10 @@ export interface ShopDealerOrder {
isSettled?: number;
// 结算时间
settleTime?: number;
// 是否解冻(0否 1是)
isUnfreeze?: number;
// 解冻时间
unfreezeTime?: number;
// 订单备注
comments?: string;
// 商城ID

View File

@@ -6,8 +6,16 @@ import type { PageParam } from '@/api';
export interface ShopDealerUser {
// 主键ID
id?: number;
// 类型 0经销商 1企业 2集团
type?: number;
// 自增ID
userId?: number;
// 头像
avatar?: string;
// 店铺名称
dealerName?: string;
// 小区名称
community?: string;
// 姓名
realName?: string;
// 手机号
@@ -34,6 +42,8 @@ export interface ShopDealerUser {
thirdNum?: number;
// 专属二维码
qrcode?: string;
// 配送员所属门店
shopName?: string;
// 是否删除
isDelete?: number;
// 租户id
@@ -56,5 +66,7 @@ export interface ShopDealerUser {
*/
export interface ShopDealerUserParam extends PageParam {
id?: number;
realName?: string;
mobile?: string;
keywords?: string;
}

View File

@@ -94,7 +94,7 @@ export interface ShopGoods {
supplierMerchantId?: number;
supplierName?: string;
// 状态0未上架1上架
isShow?: number;
isShow?: boolean;
// 状态, 0上架 1待上架 2待审核 3审核不通过
status?: number;
// 备注
@@ -124,6 +124,21 @@ export interface ShopGoods {
canUseDate?: string;
ensureTag?: string;
expiredDay?: number;
// --- 分销/佣金(新字段,后端保持 snake_case---
// 是否开启分销佣金0关闭 1开启
isOpenCommission?: number;
// 分佣类型10固定金额 20百分比
commissionType?: number;
// 一级/二级/三级分销佣金(单位以服务端为准)
firstMoney?: number;
secondMoney?: number;
thirdMoney?: number;
// 一级/二级分红(单位以服务端为准)
firstDividend?: number;
secondDividend?: number;
// 配送奖金
deliveryMoney?: number;
}
export interface BathSet {

View File

@@ -135,3 +135,18 @@ export async function shopOrderTotal(params?: ShopOrderParam) {
}
return Promise.reject(new Error(res.data.message));
}
/**
* 申请|同意退款
*/
export async function refundShopOrder(data: ShopOrder) {
const res = await request.put<ApiResult<unknown>>(
MODULES_API_URL + '/shop/shop-order/refund',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}

View File

@@ -37,6 +37,10 @@ export interface ShopOrder {
icCard?: string;
// 头像
avatar?: string;
// 昵称(部分接口会返回)
nickname?: string;
// 兼容字段:部分接口可能返回 name
name?: string;
// 真实姓名
realName?: string;
// 手机号码
@@ -99,6 +103,8 @@ export interface ShopOrder {
expressId?: number;
// 快递公司名称
expressName?: string;
// 物流单号
expressNo?: string;
// 发货人
sendName?: string;
// 发货人联系方式

View File

@@ -0,0 +1,105 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { ShopStore, ShopStoreParam } from './model';
/**
* 分页查询门店
*/
export async function pageShopStore(params: ShopStoreParam) {
const res = await request.get<ApiResult<PageResult<ShopStore>>>(
'/shop/shop-store/page',
{
params
}
);
if (res.data.code === 0) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 查询门店列表
*/
export async function listShopStore(params?: ShopStoreParam) {
const res = await request.get<ApiResult<ShopStore[]>>(
'/shop/shop-store',
{
params
}
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 添加门店
*/
export async function addShopStore(data: ShopStore) {
const res = await request.post<ApiResult<unknown>>(
'/shop/shop-store',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 修改门店
*/
export async function updateShopStore(data: ShopStore) {
const res = await request.put<ApiResult<unknown>>(
'/shop/shop-store',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 删除门店
*/
export async function removeShopStore(id?: number) {
const res = await request.delete<ApiResult<unknown>>(
'/shop/shop-store/' + id
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 批量删除门店
*/
export async function removeBatchShopStore(data: (number | undefined)[]) {
const res = await request.delete<ApiResult<unknown>>(
'/shop/shop-store/batch',
{
data
}
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 根据id查询门店
*/
export async function getShopStore(id: number) {
const res = await request.get<ApiResult<ShopStore>>(
'/shop/shop-store/' + id
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}

View File

@@ -0,0 +1,59 @@
import type { PageParam } from '@/api';
/**
* 门店
*/
export interface ShopStore {
// 自增ID
id?: number;
// 店铺名称
name?: string;
// 门店地址
address?: string;
// 手机号码
phone?: string;
// 邮箱
email?: string;
// 门店经理
managerName?: string;
// 门店banner
shopBanner?: string;
// 所在省份
province?: string;
// 所在城市
city?: string;
// 所在辖区
region?: string;
// 经度和纬度
lngAndLat?: string;
// 位置
location?:string;
// 区域
district?: string;
// 轮廓
points?: string;
// 用户ID
userId?: number;
// 状态
status?: number;
// 备注
comments?: string;
// 排序号
sortNumber?: number;
// 是否删除
isDelete?: number;
// 租户id
tenantId?: number;
// 创建时间
createTime?: string;
// 修改时间
updateTime?: string;
}
/**
* 门店搜索条件
*/
export interface ShopStoreParam extends PageParam {
id?: number;
keywords?: string;
}

View File

@@ -0,0 +1,105 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { ShopStoreFence, ShopStoreFenceParam } from './model';
/**
* 分页查询黄家明_电子围栏
*/
export async function pageShopStoreFence(params: ShopStoreFenceParam) {
const res = await request.get<ApiResult<PageResult<ShopStoreFence>>>(
'/shop/shop-store-fence/page',
{
params
}
);
if (res.data.code === 0) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 查询黄家明_电子围栏列表
*/
export async function listShopStoreFence(params?: ShopStoreFenceParam) {
const res = await request.get<ApiResult<ShopStoreFence[]>>(
'/shop/shop-store-fence',
{
params
}
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 添加黄家明_电子围栏
*/
export async function addShopStoreFence(data: ShopStoreFence) {
const res = await request.post<ApiResult<unknown>>(
'/shop/shop-store-fence',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 修改黄家明_电子围栏
*/
export async function updateShopStoreFence(data: ShopStoreFence) {
const res = await request.put<ApiResult<unknown>>(
'/shop/shop-store-fence',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 删除黄家明_电子围栏
*/
export async function removeShopStoreFence(id?: number) {
const res = await request.delete<ApiResult<unknown>>(
'/shop/shop-store-fence/' + id
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 批量删除黄家明_电子围栏
*/
export async function removeBatchShopStoreFence(data: (number | undefined)[]) {
const res = await request.delete<ApiResult<unknown>>(
'/shop/shop-store-fence/batch',
{
data
}
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 根据id查询黄家明_电子围栏
*/
export async function getShopStoreFence(id: number) {
const res = await request.get<ApiResult<ShopStoreFence>>(
'/shop/shop-store-fence/' + id
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}

View File

@@ -0,0 +1,43 @@
import type { PageParam } from '@/api';
/**
* 黄家明_电子围栏
*/
export interface ShopStoreFence {
// 自增ID
id?: number;
// 围栏名称
name?: string;
// 类型 0圆形 1方形
type?: number;
// 定位
location?: string;
// 经度
longitude?: string;
// 纬度
latitude?: string;
// 区域
district?: string;
// 电子围栏轮廓
points?: string;
// 排序(数字越小越靠前)
sortNumber?: number;
// 备注
comments?: string;
// 状态, 0正常, 1冻结
status?: number;
// 租户id
tenantId?: number;
// 创建时间
createTime?: string;
// 修改时间
updateTime?: string;
}
/**
* 黄家明_电子围栏搜索条件
*/
export interface ShopStoreFenceParam extends PageParam {
id?: number;
keywords?: string;
}

View File

@@ -0,0 +1,105 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { ShopStoreRider, ShopStoreRiderParam } from './model';
/**
* 分页查询配送员
*/
export async function pageShopStoreRider(params: ShopStoreRiderParam) {
const res = await request.get<ApiResult<PageResult<ShopStoreRider>>>(
'/shop/shop-store-rider/page',
{
params
}
);
if (res.data.code === 0) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 查询配送员列表
*/
export async function listShopStoreRider(params?: ShopStoreRiderParam) {
const res = await request.get<ApiResult<ShopStoreRider[]>>(
'/shop/shop-store-rider',
{
params
}
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 添加配送员
*/
export async function addShopStoreRider(data: ShopStoreRider) {
const res = await request.post<ApiResult<unknown>>(
'/shop/shop-store-rider',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 修改配送员
*/
export async function updateShopStoreRider(data: ShopStoreRider) {
const res = await request.put<ApiResult<unknown>>(
'/shop/shop-store-rider',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 删除配送员
*/
export async function removeShopStoreRider(id?: number) {
const res = await request.delete<ApiResult<unknown>>(
'/shop/shop-store-rider/' + id
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 批量删除配送员
*/
export async function removeBatchShopStoreRider(data: (number | undefined)[]) {
const res = await request.delete<ApiResult<unknown>>(
'/shop/shop-store-rider/batch',
{
data
}
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 根据id查询配送员
*/
export async function getShopStoreRider(id: number) {
const res = await request.get<ApiResult<ShopStoreRider>>(
'/shop/shop-store-rider/' + id
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}

View File

@@ -0,0 +1,65 @@
import type { PageParam } from '@/api';
/**
* 配送员
*/
export interface ShopStoreRider {
// 主键ID
id?: string;
// 门店IDshop_store.id
storeId?: number;
// 门店名称(后端联表返回,提交时可不传)
storeName?: string;
// 配送点IDshop_dealer.id
dealerId?: number;
// 骑手编号(可选)
riderNo?: string;
// 姓名
realName?: string;
// 手机号
mobile?: string;
// 头像
avatar?: string;
// 身份证号(可选)
idCardNo?: string;
// 状态1启用0禁用
status?: number;
// 接单状态0休息/下线1在线2忙碌
workStatus?: number;
// 是否开启自动派单1是0否
autoDispatchEnabled?: number;
// 派单优先级(同小区多骑手时可用,值越大越优先)
dispatchPriority?: number;
// 最大同时配送单数0表示不限制
maxOnhandOrders?: number;
// 是否计算工资(提成)1计算0不计算如三方配送点可设0
commissionCalcEnabled?: number;
// 水每桶提成金额(元/桶)
waterBucketUnitFee?: string;
// 其他商品提成方式1按订单固定金额2按订单金额比例3按商品规则(另表)
otherGoodsCommissionType?: number;
// 其他商品提成值:固定金额(元)或比例(%)
otherGoodsCommissionValue?: string;
// 用户ID
userId?: number;
// 备注
comments?: string;
// 排序号
sortNumber?: number;
// 是否删除
isDelete?: number;
// 租户id
tenantId?: number;
// 创建时间
createTime?: string;
// 修改时间
updateTime?: string;
}
/**
* 配送员搜索条件
*/
export interface ShopStoreRiderParam extends PageParam {
id?: number;
keywords?: string;
}

View File

@@ -0,0 +1,105 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { ShopStoreUser, ShopStoreUserParam } from './model';
/**
* 分页查询店员
*/
export async function pageShopStoreUser(params: ShopStoreUserParam) {
const res = await request.get<ApiResult<PageResult<ShopStoreUser>>>(
'/shop/shop-store-user/page',
{
params
}
);
if (res.data.code === 0) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 查询店员列表
*/
export async function listShopStoreUser(params?: ShopStoreUserParam) {
const res = await request.get<ApiResult<ShopStoreUser[]>>(
'/shop/shop-store-user',
{
params
}
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 添加店员
*/
export async function addShopStoreUser(data: ShopStoreUser) {
const res = await request.post<ApiResult<unknown>>(
'/shop/shop-store-user',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 修改店员
*/
export async function updateShopStoreUser(data: ShopStoreUser) {
const res = await request.put<ApiResult<unknown>>(
'/shop/shop-store-user',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 删除店员
*/
export async function removeShopStoreUser(id?: number) {
const res = await request.delete<ApiResult<unknown>>(
'/shop/shop-store-user/' + id
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 批量删除店员
*/
export async function removeBatchShopStoreUser(data: (number | undefined)[]) {
const res = await request.delete<ApiResult<unknown>>(
'/shop/shop-store-user/batch',
{
data
}
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 根据id查询店员
*/
export async function getShopStoreUser(id: number) {
const res = await request.get<ApiResult<ShopStoreUser>>(
'/shop/shop-store-user/' + id
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}

View File

@@ -0,0 +1,33 @@
import type { PageParam } from '@/api';
/**
* 店员
*/
export interface ShopStoreUser {
// 主键ID
id?: number;
// 配送点IDshop_dealer.id
storeId?: number;
// 用户ID
userId?: number;
// 备注
comments?: string;
// 排序号
sortNumber?: number;
// 是否删除
isDelete?: number;
// 租户id
tenantId?: number;
// 创建时间
createTime?: string;
// 修改时间
updateTime?: string;
}
/**
* 店员搜索条件
*/
export interface ShopStoreUserParam extends PageParam {
id?: number;
keywords?: string;
}

View File

@@ -0,0 +1,105 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { ShopStoreWarehouse, ShopStoreWarehouseParam } from './model';
/**
* 分页查询仓库
*/
export async function pageShopStoreWarehouse(params: ShopStoreWarehouseParam) {
const res = await request.get<ApiResult<PageResult<ShopStoreWarehouse>>>(
'/shop/shop-store-warehouse/page',
{
params
}
);
if (res.data.code === 0) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 查询仓库列表
*/
export async function listShopStoreWarehouse(params?: ShopStoreWarehouseParam) {
const res = await request.get<ApiResult<ShopStoreWarehouse[]>>(
'/shop/shop-store-warehouse',
{
params
}
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 添加仓库
*/
export async function addShopStoreWarehouse(data: ShopStoreWarehouse) {
const res = await request.post<ApiResult<unknown>>(
'/shop/shop-store-warehouse',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 修改仓库
*/
export async function updateShopStoreWarehouse(data: ShopStoreWarehouse) {
const res = await request.put<ApiResult<unknown>>(
'/shop/shop-store-warehouse',
data
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 删除仓库
*/
export async function removeShopStoreWarehouse(id?: number) {
const res = await request.delete<ApiResult<unknown>>(
'/shop/shop-store-warehouse/' + id
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 批量删除仓库
*/
export async function removeBatchShopStoreWarehouse(data: (number | undefined)[]) {
const res = await request.delete<ApiResult<unknown>>(
'/shop/shop-store-warehouse/batch',
{
data
}
);
if (res.data.code === 0) {
return res.data.message;
}
return Promise.reject(new Error(res.data.message));
}
/**
* 根据id查询仓库
*/
export async function getShopStoreWarehouse(id: number) {
const res = await request.get<ApiResult<ShopStoreWarehouse>>(
'/shop/shop-store-warehouse/' + id
);
if (res.data.code === 0 && res.data.data) {
return res.data.data;
}
return Promise.reject(new Error(res.data.message));
}

View File

@@ -0,0 +1,51 @@
import type { PageParam } from '@/api';
/**
* 仓库
*/
export interface ShopStoreWarehouse {
// 自增ID
id?: number;
// 仓库名称
name?: string;
// 唯一标识
code?: string;
// 类型 中心仓,区域仓,门店仓
type?: string;
// 仓库地址
address?: string;
// 真实姓名
realName?: string;
// 联系电话
phone?: string;
// 所在省份
province?: string;
// 所在城市
city?: string;
// 所在辖区
region?: string;
// 经纬度
lngAndLat?: string;
// 用户ID
userId?: number;
// 备注
comments?: string;
// 排序号
sortNumber?: number;
// 是否删除
isDelete?: number;
// 租户id
tenantId?: number;
// 创建时间
createTime?: string;
// 修改时间
updateTime?: string;
}
/**
* 仓库搜索条件
*/
export interface ShopStoreWarehouseParam extends PageParam {
id?: number;
keywords?: string;
}

View File

@@ -0,0 +1,126 @@
<template>
<ele-modal
:width="750"
:visible="visible"
:maskClosable="false"
:title="title"
:footer="null"
:body-style="{ paddingBottom: '28px' }"
@update:visible="updateVisible"
>
<ele-pro-table
ref="tableRef"
row-key="userId"
:datasource="datasource"
:columns="columns"
:pagination="false"
>
<template #toolbar>
<a-space>
<a-input-search
allow-clear
v-model:value="where.keywords"
placeholder="请输入搜索关键词"
style="width: 200px"
@search="reload"
@pressEnter="reload"
/>
</a-space>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'roles'">
<a-tag v-for="(item, index) in record.roles" :key="index">{{
item.roleName
}}</a-tag>
</template>
<template v-if="column.key === 'action'">
<a-space>
<a-button type="primary" @click="done(record)">选择</a-button>
</a-space>
</template>
</template>
</ele-pro-table>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import {
ColumnItem,
DatasourceFunction
} from 'ele-admin-pro/es/ele-pro-table/types';
import { EleProTable } from 'ele-admin-pro';
import useSearch from '@/utils/use-search';
import { pageShopCommunity } from "@/api/shop/shopCommunity";
import { ShopDealerUser } from "@/api/shop/shopDealerUser/model";
import {ShopCommunityParam} from "@/api/shop/shopCommunity/model";
defineProps<{
// 弹窗是否打开
visible: boolean;
title?: string;
// 修改回显的数据
data?: ShopDealerUser | null;
type?: string;
}>();
const emit = defineEmits<{
(e: 'done', data: ShopDealerUser): void;
(e: 'update:visible', visible: boolean): void;
}>();
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 表单数据
const { where } = useSearch<ShopCommunityParam>({
userId: undefined,
name: undefined,
keywords: ''
});
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格配置
const columns = ref<ColumnItem[]>([
{
title: 'ID',
dataIndex: 'id'
},
{
title: '小区名称',
dataIndex: 'name'
},
{
title: '操作',
key: 'action',
align: 'center',
hideInSetting: true
}
]);
// 表格数据源
const datasource: DatasourceFunction = ({ page, limit, where, orders }) => {
where.type = 1;
return pageShopCommunity({
...where,
...orders,
page,
limit
});
};
/* 搜索 */
const reload = () => {
tableRef?.value?.reload({ page: 1, where });
};
const done = (record: ShopDealerUser) => {
updateVisible(false);
emit('done', record);
};
</script>
<style lang="less"></style>

View File

@@ -0,0 +1,71 @@
<template>
<div>
<a-input-group compact>
<a-input
disabled
style="width: calc(100% - 32px)"
v-model:value="content"
:placeholder="placeholder"
/>
<a-button @click="openEdit">
<template #icon><BulbOutlined class="ele-text-warning" /></template>
</a-button>
</a-input-group>
<!-- 选择弹窗 -->
<select-data
v-model:visible="showEdit"
:data="current"
:title="placeholder"
:type="type"
@done="onChange"
/>
</div>
</template>
<script lang="ts" setup>
import { BulbOutlined } from '@ant-design/icons-vue';
import { ref } from 'vue';
import SelectData from './components/select-data.vue';
import { User } from '@/api/system/user/model';
const props = withDefaults(
defineProps<{
value?: any;
placeholder?: string;
index?: number;
type?: string;
}>(),
{
placeholder: '请选择'
}
);
const emit = defineEmits<{
(e: 'done', User): void;
(e: 'clear'): void;
}>();
// 是否显示编辑弹窗
const showEdit = ref(false);
// 当前编辑数据
const current = ref<User | null>(null);
const content = ref<any>();
/* 打开编辑弹窗 */
const openEdit = (row?: User) => {
current.value = row ?? null;
showEdit.value = true;
};
const onChange = (row) => {
console.log(row,'sdfsd111')
row.index = Number(props.index);
emit('done', row);
};
if (props.value) {
content.value = props.value;
}
// 查询租户列表
// const appList = ref<App[] | undefined>([]);
</script>

View File

@@ -34,9 +34,9 @@
</a-button>
</a-upload>
<a-upload
v-else
v-else-if="type == 'image'"
:show-upload-list="false"
:accept="'image/*,application/*'"
:accept="'image/*'"
:customRequest="onUpload"
>
<a-button type="primary" class="ele-btn-icon">
@@ -46,6 +46,19 @@
<span>上传图片</span>
</a-button>
</a-upload>
<a-upload
v-else
:show-upload-list="false"
:accept="'image/*,application/*,text/*'"
:customRequest="onUpload"
>
<a-button type="primary" class="ele-btn-icon">
<template #icon>
<UploadOutlined />
</template>
<span>上传文件</span>
</a-button>
</a-upload>
<a-select
show-search
allow-clear
@@ -274,16 +287,36 @@
// 上传文件
const onUpload = (item) => {
const { file } = item;
if (!file.type.startsWith('image') && props.type != 'video') {
message.error('只能选择图片');
return;
}
if (props.type == 'video') {
if (!file.type.startsWith('video')) {
message.error('只能选择视频');
return;
}
if (file.size / 1024 / 1024 > 100) {
message.error('大小不能超过 100MB');
return;
}
} else if (props.type == 'image') {
if (!file.type.startsWith('image')) {
message.error('只能选择图片');
return;
}
if (file.size / 1024 / 1024 > 10) {
message.error('大小不能超过 10MB');
return;
}
} else {
// 默认:允许图片/文档等附件
const isAllowed =
file.type.startsWith('image') ||
file.type.startsWith('application') ||
file.type.startsWith('text') ||
file.type === 'application/octet-stream' ||
file.type === '';
if (!isAllowed) {
message.error('只能选择图片或文件');
return;
}
if (file.size / 1024 / 1024 > 10) {
message.error('大小不能超过 10MB');
return;

View File

@@ -16,7 +16,16 @@
</a>
</div>
<div v-else class="image-upload-item">
<YoutubeOutlined />
<a
class="file-item"
:href="stripOssImageProcess(item.url)"
target="_blank"
rel="noopener noreferrer"
:style="{ width: width + 'px', height: height + 'px' }"
>
<FileOutlined />
<span class="file-name">{{ item.name || guessNameFromUrl(item.url) }}</span>
</a>
<a class="image-upload-close" @click="onDeleteItem(index)">
<CloseOutlined />
</a>
@@ -47,12 +56,12 @@
import {
PlusOutlined,
CloseOutlined,
YoutubeOutlined
FileOutlined
} from '@ant-design/icons-vue';
import { ref } from 'vue';
import SelectData from './components/select-data.vue';
import { FileRecord } from '@/api/system/file/model';
import { isImage } from '@/utils/common';
import { isImage, stripOssImageProcess } from '@/utils/common';
const props = withDefaults(
defineProps<{
@@ -98,6 +107,16 @@
const onDeleteItem = (index: number) => {
emit('del', index);
};
const guessNameFromUrl = (url: string) => {
const cleanUrl = (url?.split('?')[0] ?? '').trim();
const last = cleanUrl.split('/').pop() || '';
try {
return decodeURIComponent(last) || url;
} catch {
return last || url;
}
};
</script>
<style lang="less" scoped>
.select-picture-btn {
@@ -112,6 +131,21 @@
.image-upload-item {
position: relative;
}
.file-item {
display: inline-flex;
align-items: center;
gap: 6px;
border: 1px dashed var(--grey-7);
padding: 0 10px;
border-radius: 4px;
max-width: 240px;
color: inherit;
}
.file-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.image-upload-close {
width: 18px;
height: 18px;

View File

@@ -0,0 +1,106 @@
<template>
<ele-modal
:width="800"
:visible="visible"
:maskClosable="false"
:title="title"
:footer="null"
:body-style="{ paddingBottom: '28px' }"
@update:visible="updateVisible"
>
<ele-pro-table
ref="tableRef"
row-key="id"
:datasource="datasource"
:columns="columns"
:pagination="false"
>
<template #toolbar>
<a-space>
<a-input-search
allow-clear
v-model:value="where.keywords"
placeholder="请输入门店关键词"
style="width: 240px"
@search="reload"
@pressEnter="reload"
/>
</a-space>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'action'">
<a-space>
<a-button type="primary" @click="done(record)">选择</a-button>
</a-space>
</template>
</template>
</ele-pro-table>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import type {
ColumnItem,
DatasourceFunction
} from 'ele-admin-pro/es/ele-pro-table/types';
import { EleProTable } from 'ele-admin-pro';
import useSearch from '@/utils/use-search';
import { pageShopStore } from '@/api/shop/shopStore';
import type { ShopStoreParam } from '@/api/shop/shopStore/model';
import type { ShopStore } from '@/api/shop/shopStore/model';
defineProps<{
visible: boolean;
title?: string;
}>();
const emit = defineEmits<{
(e: 'done', data: ShopStore): void;
(e: 'update:visible', visible: boolean): void;
}>();
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
const { where } = useSearch<ShopStoreParam>({
id: undefined,
keywords: ''
});
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
const columns = ref<ColumnItem[]>([
{ title: 'ID', dataIndex: 'id', width: 90 },
{ title: '门店名称', dataIndex: 'name' },
{ title: '电话', dataIndex: 'phone', width: 160 },
{ title: '地址', dataIndex: 'address' },
{
title: '操作',
key: 'action',
width: 120,
align: 'center',
hideInSetting: true
}
]);
const datasource: DatasourceFunction = ({ page, limit, where, orders }) => {
return pageShopStore({
...where,
...orders,
page,
limit
});
};
const reload = () => {
tableRef?.value?.reload({ page: 1, where: where as ShopStoreParam });
};
const done = (record: ShopStore) => {
updateVisible(false);
emit('done', record);
};
</script>

View File

@@ -0,0 +1,63 @@
<template>
<div>
<a-input-group compact>
<a-input
disabled
style="width: calc(100% - 32px)"
v-model:value="content"
:placeholder="placeholder"
/>
<a-button @click="openEdit">
<template #icon><BulbOutlined class="ele-text-warning" /></template>
</a-button>
</a-input-group>
<!-- 选择弹窗 -->
<SelectData
v-model:visible="showEdit"
:title="placeholder"
@done="onChange"
/>
</div>
</template>
<script lang="ts" setup>
import { BulbOutlined } from '@ant-design/icons-vue';
import { ref, watch } from 'vue';
import SelectData from './components/select-data.vue';
import type { ShopStore } from '@/api/shop/shopStore/model';
const props = withDefaults(
defineProps<{
value?: string;
placeholder?: string;
}>(),
{
placeholder: '选择门店'
}
);
const emit = defineEmits<{
(e: 'done', data?: ShopStore): void;
(e: 'clear'): void;
}>();
const showEdit = ref(false);
const content = ref<string>(props.value ?? '');
const openEdit = () => {
showEdit.value = true;
};
const onChange = (row?: ShopStore) => {
showEdit.value = false;
emit('done', row);
};
watch(
() => props.value,
(v) => {
content.value = v ?? '';
}
);
</script>

View File

@@ -444,6 +444,24 @@ export const isImage = (fileName) => {
});
};
// 移除 OSS 图片处理参数(非图片文件拼接该参数会导致访问失败)
export const stripOssImageProcess = (url?: string) => {
if (!url) return url;
const hashIndex = url.indexOf('#');
const withNoHash = hashIndex >= 0 ? url.slice(0, hashIndex) : url;
const hash = hashIndex >= 0 ? url.slice(hashIndex) : '';
const queryIndex = withNoHash.indexOf('?');
if (queryIndex < 0) return url;
const base = withNoHash.slice(0, queryIndex);
const query = withNoHash.slice(queryIndex + 1);
const kept = query
.split('&')
.filter(Boolean)
.filter((p) => p !== 'x-oss-process' && !p.startsWith('x-oss-process='));
const rebuilt = kept.length ? `${base}?${kept.join('&')}` : base;
return `${rebuilt}${hash}`;
};
export const getWeek = (text) => {
const week = [
'星期日',

View File

@@ -9,17 +9,17 @@
<!-- </a-button>-->
<a-button class="ele-btn-icon" @click="openImport">
<template #icon>
<CloudUploadOutlined/>
<CloudUploadOutlined />
</template>
<span>导入()</span>
</a-button>
<a-button class="ele-btn-icon" @click="exportData">
<template #icon>
<CloudDownloadOutlined/>
<CloudDownloadOutlined />
</template>
<span>导出</span>
</a-button>
<RefreshCompanyIdButton module="credit-case-filing" @done="reload"/>
<RefreshCompanyIdButton :module="props.module" @done="reload" />
<a-button
danger
class="ele-btn-icon"
@@ -27,7 +27,7 @@
@click="remove"
>
<template #icon>
<DeleteOutlined/>
<DeleteOutlined />
</template>
<span>批量删除</span>
</a-button>
@@ -43,57 +43,59 @@
</template>
<script lang="ts" setup>
import {computed, ref} from 'vue';
import {
CloudUploadOutlined,
CloudDownloadOutlined,
DeleteOutlined
} from '@ant-design/icons-vue';
import RefreshCompanyIdButton from "@/views/credit/components/RefreshCompanyIdButton.vue";
import { computed, ref } from 'vue';
import {
CloudUploadOutlined,
CloudDownloadOutlined,
DeleteOutlined
} from '@ant-design/icons-vue';
import RefreshCompanyIdButton from '@/views/credit/components/RefreshCompanyIdButton.vue';
const props = withDefaults(
defineProps<{
selection?: any[];
}>(),
{
selection: () => []
}
);
const props = withDefaults(
defineProps<{
/** 后端模块路径片段,如: credit-breach-of-trust */
module: string;
selection?: any[];
}>(),
{
selection: () => []
}
);
const emit = defineEmits<{
(e: 'search', where?: { keywords?: string }): void;
(e: 'add'): void;
(e: 'remove'): void;
(e: 'batchMove'): void;
(e: 'importData'): void;
(e: 'exportData'): void;
}>();
const emit = defineEmits<{
(e: 'search', where?: { keywords?: string }): void;
(e: 'add'): void;
(e: 'remove'): void;
(e: 'batchMove'): void;
(e: 'importData'): void;
(e: 'exportData'): void;
}>();
const keywords = ref('');
const selection = computed(() => props.selection || []);
const keywords = ref('');
const selection = computed(() => props.selection || []);
// 新增
const add = () => {
emit('add');
};
// 搜索
const handleSearch = () => {
emit('search', { keywords: keywords.value });
};
// 搜索
const handleSearch = () => {
emit('search', {keywords: keywords.value});
};
// 刷新 companyId 后,保持当前条件并触发父级 reload
const reload = () => {
emit('search', { keywords: keywords.value });
};
// 导入
const openImport = () => {
emit('importData');
};
// 导入
const openImport = () => {
emit('importData');
};
// 导出
const exportData = () => {
emit('exportData');
};
// 导出
const exportData = () => {
emit('exportData');
};
// 批量删除
const remove = () => {
emit('remove');
};
// 批量删除
const remove = () => {
emit('remove');
};
</script>

View File

@@ -37,6 +37,7 @@
CloudDownloadOutlined,
DeleteOutlined
} from '@ant-design/icons-vue';
import RefreshCompanyIdButton from "@/views/credit/components/RefreshCompanyIdButton.vue";
const props = withDefaults(
defineProps<{

View File

@@ -57,4 +57,3 @@
});
};
</script>

View File

@@ -7,7 +7,7 @@
:columns="columns"
:datasource="datasource"
:customRow="customRow"
:selection="selection"
v-model:selection="selection"
:scroll="{ x: 2400 }"
tool-class="ele-toolbar-form"
class="sys-org-table"
@@ -221,7 +221,7 @@
sorter: true,
ellipsis: true,
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd HH:mm:ss')
},
}
// {
// title: '操作',
// key: 'action',

View File

@@ -7,7 +7,7 @@
:columns="columns"
:datasource="datasource"
:customRow="customRow"
:selection="selection"
v-model:selection="selection"
tool-class="ele-toolbar-form"
class="sys-org-table"
>
@@ -24,7 +24,6 @@
@exportData="exportData"
/>
</a-space>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'companyName'">
@@ -194,7 +193,7 @@
sorter: true,
ellipsis: true,
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd HH:mm:ss')
},
}
// {
// title: '操作',
// key: 'action',

View File

@@ -7,7 +7,7 @@
:columns="columns"
:datasource="datasource"
:customRow="customRow"
:selection="selection"
v-model:selection="selection"
tool-class="ele-toolbar-form"
class="sys-org-table"
>
@@ -197,7 +197,7 @@
sorter: true,
ellipsis: true,
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd HH:mm:ss')
},
}
// {
// title: '操作',
// key: 'action',

View File

@@ -41,9 +41,11 @@
(e: 'update:visible', visible: boolean): void;
}>();
defineProps<{
const props = defineProps<{
// 是否打开弹窗
visible: boolean;
// 关联企业ID企业详情下导入时需要
companyId?: number;
}>();
// 导入请求状态
@@ -74,7 +76,7 @@
return false;
}
loading.value = true;
importCreditBreachOfTrustHistory(file)
importCreditBreachOfTrustHistory(file, props.companyId)
.then((msg) => {
loading.value = false;
message.success(msg);
@@ -93,4 +95,3 @@
emit('update:visible', value);
};
</script>

View File

@@ -15,6 +15,7 @@
<a-space class="flex">
<search
@search="reload"
module="credit-breach-of-trust"
:selection="selection"
@add="openEdit"
@remove="removeBatch"
@@ -22,12 +23,6 @@
@importData="openImport"
@exportData="exportData"
/>
<a-button type="dashed" class="ele-btn-icon" @click="openImport2">
<template #icon>
<CloudUploadOutlined />
</template>
<span class="text-red-500">导入历史失信被执行人</span>
</a-button>
</a-space>
</template>
<template #bodyCell="{ column, record }">
@@ -73,15 +68,13 @@
/>
<!-- 导入弹窗 -->
<CreditBreachOfTrustImport v-model:visible="showImport" @done="reload" />
<!-- 历史导入弹窗 -->
<CreditBreachOfTrustHistoryImport v-model:visible="showImport2" @done="reload" />
</a-page-header>
</template>
<script lang="ts" setup>
import { createVNode, ref } from 'vue';
import { message, Modal } from 'ant-design-vue';
import { ExclamationCircleOutlined, CloudUploadOutlined } from '@ant-design/icons-vue';
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
import type { EleProTable } from 'ele-admin-pro';
import { toDateString } from 'ele-admin-pro';
import type {
@@ -93,7 +86,6 @@
import { getPageTitle } from '@/utils/common';
import CreditBreachOfTrustEdit from './components/creditBreachOfTrustEdit.vue';
import CreditBreachOfTrustImport from './components/credit-breach-of-trust-import.vue';
import CreditBreachOfTrustHistoryImport from './components/credit-breach-of-trust-history-import.vue';
import {
pageCreditBreachOfTrust,
listCreditBreachOfTrust,
@@ -116,8 +108,6 @@
const showEdit = ref(false);
// 是否显示导入弹窗
const showImport = ref(false);
// 是否显示历史导入弹窗
const showImport2 = ref(false);
// 是否显示批量移动弹窗
const showMove = ref(false);
// 加载状态
@@ -163,13 +153,13 @@
},
{
title: '原告/上诉人',
dataIndex: 'plaintiffUser',
key: 'plaintiffUser'
dataIndex: 'appellee',
key: 'appellee'
},
{
title: '被告/被上诉人',
dataIndex: 'plaintiffUser',
key: 'plaintiffUser'
dataIndex: 'plaintiffAppellant',
key: 'plaintiffAppellant'
},
{
title: '其他当事人/第三人',
@@ -178,8 +168,8 @@
},
{
title: '发生时间',
dataIndex: 'occurrenceTime',
key: 'occurrenceTime',
dataIndex: 'releaseDate',
key: 'releaseDate',
width: 120
},
{
@@ -215,9 +205,10 @@
key: 'createTime',
width: 180,
align: 'center',
sorter: true,
ellipsis: true,
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd HH:mm:ss')
},
}
// {
// title: '操作',
// key: 'action',
@@ -254,11 +245,6 @@
showImport.value = true;
};
/* 打开历史导入弹窗 */
const openImport2 = () => {
showImport2.value = true;
};
/* 导出 */
const exportData = () => {
exportCreditData<CreditBreachOfTrust>({

View File

@@ -0,0 +1,98 @@
<!-- 历史立案信息导入弹窗 -->
<template>
<ele-modal
:width="520"
:footer="null"
title="历史立案信息批量导入"
:visible="visible"
@update:visible="updateVisible"
>
<a-spin :spinning="loading">
<a-upload-dragger
accept=".xls,.xlsx"
:show-upload-list="false"
:customRequest="doUpload"
style="padding: 24px 0; margin-bottom: 16px"
>
<p class="ant-upload-drag-icon">
<cloud-upload-outlined />
</p>
<p class="ant-upload-hint">将文件拖到此处或点击上传</p>
</a-upload-dragger>
</a-spin>
<div class="ele-text-center">
<span>只能上传xlsxlsx文件</span>
<a :href="templateUrl" download="历史立案信息导入模板.xlsx">
下载导入模板
</a>
</div>
</ele-modal>
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue';
import { message } from 'ant-design-vue/es';
import { CloudUploadOutlined } from '@ant-design/icons-vue';
import { importCreditCaseFilingHistory } from '@/api/credit/creditCaseFiling';
import { API_BASE_URL } from '@/config/setting';
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
const props = defineProps<{
// 是否打开弹窗
visible: boolean;
// 关联企业ID企业详情下导入时需要
companyId?: number;
}>();
// 导入请求状态
const loading = ref(false);
// 模板下载地址,保持与当前接口域名一致
const templateUrl = computed(() => {
const base = (localStorage.getItem('ApiUrl') || API_BASE_URL || '').replace(
/\/$/,
''
);
return `${base}/credit/credit-case-filing/import/history/template`;
});
/* 上传 */
const doUpload = ({ file }) => {
if (
![
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
].includes(file.type)
) {
message.error('只能选择 excel 文件');
return false;
}
if (file.size / 1024 / 1024 > 10) {
message.error('大小不能超过 10MB');
return false;
}
loading.value = true;
importCreditCaseFilingHistory(file, props.companyId)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
return false;
};
/* 更新 visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
</script>

View File

@@ -22,9 +22,7 @@
</a-spin>
<div class="ele-text-center">
<span>只能上传xlsxlsx文件</span>
<a :href="templateUrl" download="立案信息导入模板.xlsx">
下载导入模板
</a>
<a :href="templateUrl" download="立案信息导入模板.xlsx"> 下载导入模板 </a>
</div>
</ele-modal>
</template>

View File

@@ -15,6 +15,7 @@
<a-space class="flex">
<search
@search="reload"
module="credit-case-filing"
:selection="selection"
@add="openEdit"
@remove="removeBatch"
@@ -153,13 +154,13 @@
},
{
title: '原告/上诉人',
dataIndex: 'plaintiffUser',
key: 'plaintiffUser'
dataIndex: 'plaintiffAppellant',
key: 'plaintiffAppellant'
},
{
title: '被告/被上诉人',
dataIndex: 'plaintiffUser',
key: 'plaintiffUser'
dataIndex: 'appellee',
key: 'appellee'
},
{
title: '其他当事人/第三人',
@@ -215,8 +216,9 @@
width: 180,
align: 'center',
ellipsis: true,
sorter: true,
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd HH:mm:ss')
},
}
// {
// title: '操作',
// key: 'action',

View File

@@ -1,12 +1,12 @@
<!-- 搜索表单 -->
<template>
<a-space :size="10" style="flex-wrap: wrap">
<!-- <a-button type="primary" class="ele-btn-icon" @click="add">-->
<!-- <template #icon>-->
<!-- <PlusOutlined />-->
<!-- </template>-->
<!-- <span>添加</span>-->
<!-- </a-button>-->
<!-- <a-button type="primary" class="ele-btn-icon" @click="add">-->
<!-- <template #icon>-->
<!-- <PlusOutlined />-->
<!-- </template>-->
<!-- <span>添加</span>-->
<!-- </a-button>-->
<a-button type="primary" class="ele-btn-icon" @click="openImport">
<template #icon>
<CloudUploadOutlined />

View File

@@ -178,19 +178,19 @@
title: '原文件导入名称',
dataIndex: 'name',
key: 'name',
width: 240
width: 280
},
{
title: '系统匹配企业名称',
dataIndex: 'matchName',
key: 'matchName',
width: 240
width: 280
},
{
title: '登记状态',
dataIndex: 'registrationStatus',
key: 'registrationStatus',
width: 120
width: 180
},
{
title: '法定代表人',
@@ -272,20 +272,17 @@
title: '更多邮箱',
dataIndex: 'moreEmail',
key: 'moreEmail',
width: 120,
ellipsis: true
},
{
title: '企业(机构)类型',
dataIndex: 'institutionType',
key: 'institutionType',
width: 140
key: 'institutionType'
},
{
title: '纳税人识别号',
dataIndex: 'taxpayerCode',
key: 'taxpayerCode',
width: 120
key: 'taxpayerCode'
},
{
title: '注册号',
@@ -308,18 +305,18 @@
title: '参保人数所属年报',
dataIndex: 'annualReport',
key: 'annualReport',
width: 140
width: 180
},
{
title: '营业期限',
dataIndex: 'businessTerm',
key: 'businessTerm',
width: 120
ellipsis: true
},
{
title: '国标行业门类',
dataIndex: 'nationalStandardIndustryCategories',
key: 'nationalStandardIndustryCategories',
key: 'nationalStandardIndustryCategories'
},
{
title: '国标行业大类',
@@ -364,12 +361,14 @@
{
title: '曾用名',
dataIndex: 'formerName',
key: 'formerName'
key: 'formerName',
ellipsis: true
},
{
title: '英文名',
dataIndex: 'englishName',
key: 'englishName'
key: 'englishName',
ellipsis: true
},
{
title: '官网',
@@ -455,6 +454,7 @@
width: 180,
align: 'center',
ellipsis: true,
sorter: true,
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd HH:mm:ss')
}
// {

View File

@@ -13,6 +13,7 @@
>
<template #toolbar>
<a-space class="flex">
<RefreshCompanyIdButton module="credit-competitor" @done="reload" />
<search
@search="reload"
:selection="selection"
@@ -81,6 +82,7 @@
ColumnItem
} from 'ele-admin-pro/es/ele-pro-table/types';
import Search from '@/views/credit/components/CreditSearchToolbar2.vue';
import RefreshCompanyIdButton from '@/views/credit/components/RefreshCompanyIdButton.vue';
import { exportCreditData } from '../utils/export';
import { getPageTitle } from '@/utils/common';
import CreditCompetitorEdit from './components/creditCompetitorEdit.vue';
@@ -194,8 +196,9 @@
width: 180,
align: 'center',
ellipsis: true,
sorter: true,
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd HH:mm:ss')
},
}
// {
// title: '操作',
// key: 'action',

View File

@@ -1,9 +1,9 @@
<!-- 历史开庭公告导入弹窗 -->
<!-- 历史法院公告导入弹窗 -->
<template>
<ele-modal
:width="520"
:footer="null"
title="历史开庭公告批量导入"
title="历史法院公告批量导入"
:visible="visible"
@update:visible="updateVisible"
>
@@ -22,7 +22,7 @@
</a-spin>
<div class="ele-text-center">
<span>只能上传xlsxlsx文件</span>
<a :href="templateUrl" download="历史开庭公告导入模板.xlsx">
<a :href="templateUrl" download="历史法院公告导入模板.xlsx">
下载导入模板
</a>
</div>
@@ -41,9 +41,11 @@
(e: 'update:visible', visible: boolean): void;
}>();
defineProps<{
const props = defineProps<{
// 是否打开弹窗
visible: boolean;
// 关联企业ID企业详情下导入时需要
companyId?: number;
}>();
// 导入请求状态
@@ -74,7 +76,7 @@
return false;
}
loading.value = true;
importCreditCourtAnnouncementHistory(file)
importCreditCourtAnnouncementHistory(file, props.companyId)
.then((msg) => {
loading.value = false;
message.success(msg);
@@ -93,4 +95,3 @@
emit('update:visible', value);
};
</script>

View File

@@ -15,6 +15,7 @@
<a-space class="flex">
<search
@search="reload"
module="credit-court-announcement"
:selection="selection"
@add="openEdit"
@remove="removeBatch"
@@ -22,12 +23,6 @@
@importData="openImport"
@exportData="exportData"
/>
<a-button type="dashed" class="ele-btn-icon" @click="openImport2">
<template #icon>
<CloudUploadOutlined />
</template>
<span class="text-red-500">导入历史开庭公告</span>
</a-button>
</a-space>
</template>
<template #bodyCell="{ column, record }">
@@ -76,18 +71,13 @@
v-model:visible="showImport"
@done="reload"
/>
<!-- 历史导入弹窗 -->
<CreditCourtAnnouncementHistoryImport
v-model:visible="showImport2"
@done="reload"
/>
</a-page-header>
</template>
<script lang="ts" setup>
import { createVNode, ref } from 'vue';
import { message, Modal } from 'ant-design-vue';
import { ExclamationCircleOutlined, CloudUploadOutlined } from '@ant-design/icons-vue';
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
import type { EleProTable } from 'ele-admin-pro';
import { toDateString } from 'ele-admin-pro';
import type {
@@ -99,7 +89,6 @@
import { getPageTitle } from '@/utils/common';
import CreditCourtAnnouncementEdit from './components/creditCourtAnnouncementEdit.vue';
import CreditCourtAnnouncementImport from './components/credit-court-announcement-import.vue';
import CreditCourtAnnouncementHistoryImport from './components/credit-court-announcement-history-import.vue';
import {
pageCreditCourtAnnouncement,
listCreditCourtAnnouncement,
@@ -122,8 +111,6 @@
const showEdit = ref(false);
// 是否显示导入弹窗
const showImport = ref(false);
// 是否显示历史导入弹窗
const showImport2 = ref(false);
// 是否显示批量移动弹窗
const showMove = ref(false);
// 加载状态
@@ -169,13 +156,13 @@
},
{
title: '原告/上诉人',
dataIndex: 'plaintiffUser',
key: 'plaintiffUser'
dataIndex: 'plaintiffAppellant',
key: 'plaintiffAppellant'
},
{
title: '被告/被上诉人',
dataIndex: 'plaintiffUser',
key: 'plaintiffUser'
dataIndex: 'appellee',
key: 'appellee'
},
{
title: '其他当事人/第三人',
@@ -227,8 +214,9 @@
width: 180,
align: 'center',
ellipsis: true,
sorter: true,
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd HH:mm:ss')
},
}
// {
// title: '操作',
// key: 'action',
@@ -265,11 +253,6 @@
showImport.value = true;
};
/* 打开历史导入弹窗 */
const openImport2 = () => {
showImport2.value = true;
};
/* 导出 */
const exportData = () => {
exportCreditData<CreditCourtAnnouncement>({

View File

@@ -41,9 +41,11 @@
(e: 'update:visible', visible: boolean): void;
}>();
defineProps<{
const props = defineProps<{
// 是否打开弹窗
visible: boolean;
// 关联企业ID企业详情下导入时需要
companyId?: number;
}>();
// 导入请求状态
@@ -74,7 +76,7 @@
return false;
}
loading.value = true;
importCreditCourtSessionHistory(file)
importCreditCourtSessionHistory(file, props.companyId)
.then((msg) => {
loading.value = false;
message.success(msg);
@@ -93,4 +95,3 @@
emit('update:visible', value);
};
</script>

View File

@@ -15,6 +15,7 @@
<a-space class="flex">
<search
@search="reload"
module="credit-court-session"
:selection="selection"
@add="openEdit"
@remove="removeBatch"
@@ -22,12 +23,6 @@
@importData="openImport"
@exportData="exportData"
/>
<a-button type="dashed" class="ele-btn-icon" @click="openImport2">
<template #icon>
<CloudUploadOutlined />
</template>
<span class="text-red-500">导入历史开庭公告</span>
</a-button>
</a-space>
</template>
<template #bodyCell="{ column, record }">
@@ -65,15 +60,13 @@
/>
<!-- 导入弹窗 -->
<CreditCourtSessionImport v-model:visible="showImport" @done="reload" />
<!-- 历史导入弹窗 -->
<CreditCourtSessionHistoryImport v-model:visible="showImport2" @done="reload" />
</a-page-header>
</template>
<script lang="ts" setup>
import { createVNode, ref } from 'vue';
import { message, Modal } from 'ant-design-vue';
import { ExclamationCircleOutlined, CloudUploadOutlined } from '@ant-design/icons-vue';
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
import type { EleProTable } from 'ele-admin-pro';
import { toDateString } from 'ele-admin-pro';
import type {
@@ -85,7 +78,6 @@
import { getPageTitle } from '@/utils/common';
import CreditCourtSessionEdit from './components/creditCourtSessionEdit.vue';
import CreditCourtSessionImport from './components/credit-court-session-import.vue';
import CreditCourtSessionHistoryImport from './components/credit-court-session-history-import.vue';
import {
pageCreditCourtSession,
listCreditCourtSession,
@@ -96,7 +88,7 @@
CreditCourtSession,
CreditCourtSessionParam
} from '@/api/credit/creditCourtSession/model';
import type {CreditMediation} from "@/api/credit/creditMediation/model";
import type { CreditMediation } from '@/api/credit/creditMediation/model';
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
@@ -109,8 +101,6 @@
const showEdit = ref(false);
// 是否显示导入弹窗
const showImport = ref(false);
// 是否显示历史导入弹窗
const showImport2 = ref(false);
// 是否显示批量移动弹窗
const showMove = ref(false);
// 加载状态
@@ -156,13 +146,13 @@
},
{
title: '原告/上诉人',
dataIndex: 'plaintiffUser',
key: 'plaintiffUser'
dataIndex: 'plaintiffAppellant',
key: 'plaintiffAppellant'
},
{
title: '被告/被上诉人',
dataIndex: 'plaintiffUser',
key: 'plaintiffUser'
dataIndex: 'appellee',
key: 'appellee'
},
{
title: '其他当事人/第三人',
@@ -213,8 +203,9 @@
width: 180,
align: 'center',
ellipsis: true,
sorter: true,
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd HH:mm:ss')
},
}
// {
// title: '操作',
// key: 'action',
@@ -251,11 +242,6 @@
showImport.value = true;
};
/* 打开历史导入弹窗 */
const openImport2 = () => {
showImport2.value = true;
};
/* 导出 */
const exportData = () => {
exportCreditData<CreditCourtSession>({

View File

@@ -1,18 +1,18 @@
<!-- 搜索表单 -->
<template>
<a-space :size="10" style="flex-wrap: wrap">
<!-- <a-button type="primary" class="ele-btn-icon" @click="add">-->
<!-- <template #icon>-->
<!-- <PlusOutlined />-->
<!-- </template>-->
<!-- <span>添加</span>-->
<!-- </a-button>-->
<!-- <a-button class="ele-btn-icon" @click="openImport">-->
<!-- <template #icon>-->
<!-- <CloudUploadOutlined />-->
<!-- </template>-->
<!-- <span>导入()</span>-->
<!-- </a-button>-->
<!-- <a-button type="primary" class="ele-btn-icon" @click="add">-->
<!-- <template #icon>-->
<!-- <PlusOutlined />-->
<!-- </template>-->
<!-- <span>添加</span>-->
<!-- </a-button>-->
<!-- <a-button class="ele-btn-icon" @click="openImport">-->
<!-- <template #icon>-->
<!-- <CloudUploadOutlined />-->
<!-- </template>-->
<!-- <span>导入()</span>-->
<!-- </a-button>-->
<a-button class="ele-btn-icon" @click="exportData">
<template #icon>
<CloudDownloadOutlined />

View File

@@ -13,6 +13,7 @@
>
<template #toolbar>
<a-space class="flex">
<RefreshCompanyIdButton module="credit-customer" @done="reload" />
<search
@search="reload"
:selection="selection"
@@ -81,6 +82,7 @@
ColumnItem
} from 'ele-admin-pro/es/ele-pro-table/types';
import Search from './components/search.vue';
import RefreshCompanyIdButton from '@/views/credit/components/RefreshCompanyIdButton.vue';
import { getPageTitle } from '@/utils/common';
import CreditCustomerEdit from './components/creditCustomerEdit.vue';
import {
@@ -155,22 +157,31 @@
{
title: '状态',
dataIndex: 'statusTxt',
key: 'statusTxt'
key: 'statusTxt',
width: 120,
align: 'center'
},
{
title: '销售金额(万元)',
dataIndex: 'price',
key: 'price'
key: 'price',
width: 180,
sorter: true,
align: 'center'
},
{
title: '公开日期',
dataIndex: 'publicDate',
key: 'publicDate'
key: 'publicDate',
align: 'center',
width: 120
},
{
title: '数据来源',
dataIndex: 'dataSource',
key: 'dataSource'
key: 'dataSource',
align: 'center',
width: 90
},
{
title: '操作人',
@@ -188,7 +199,7 @@
sorter: true,
ellipsis: true,
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd HH:mm:ss')
},
}
// {
// title: '操作',
// key: 'action',

View File

@@ -0,0 +1,98 @@
<!-- 历史送达公告导入弹窗 -->
<template>
<ele-modal
:width="520"
:footer="null"
title="历史送达公告批量导入"
:visible="visible"
@update:visible="updateVisible"
>
<a-spin :spinning="loading">
<a-upload-dragger
accept=".xls,.xlsx"
:show-upload-list="false"
:customRequest="doUpload"
style="padding: 24px 0; margin-bottom: 16px"
>
<p class="ant-upload-drag-icon">
<cloud-upload-outlined />
</p>
<p class="ant-upload-hint">将文件拖到此处或点击上传</p>
</a-upload-dragger>
</a-spin>
<div class="ele-text-center">
<span>只能上传xlsxlsx文件</span>
<a :href="templateUrl" download="历史送达公告导入模板.xlsx">
下载导入模板
</a>
</div>
</ele-modal>
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue';
import { message } from 'ant-design-vue/es';
import { CloudUploadOutlined } from '@ant-design/icons-vue';
import { importCreditDeliveryNoticeHistory } from '@/api/credit/creditDeliveryNotice';
import { API_BASE_URL } from '@/config/setting';
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
const props = defineProps<{
// 是否打开弹窗
visible: boolean;
// 关联企业ID企业详情下导入时需要
companyId?: number;
}>();
// 导入请求状态
const loading = ref(false);
// 模板下载地址,保持与当前接口域名一致
const templateUrl = computed(() => {
const base = (localStorage.getItem('ApiUrl') || API_BASE_URL || '').replace(
/\/$/,
''
);
return `${base}/credit/credit-delivery-notice/import/history/template`;
});
/* 上传 */
const doUpload = ({ file }) => {
if (
![
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
].includes(file.type)
) {
message.error('只能选择 excel 文件');
return false;
}
if (file.size / 1024 / 1024 > 10) {
message.error('大小不能超过 10MB');
return false;
}
loading.value = true;
importCreditDeliveryNoticeHistory(file, props.companyId)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
return false;
};
/* 更新 visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
</script>

View File

@@ -15,6 +15,7 @@
<a-space class="flex">
<search
@search="reload"
module="credit-delivery-notice"
:selection="selection"
@add="openEdit"
@remove="removeBatch"
@@ -152,13 +153,13 @@
},
{
title: '原告/上诉人',
dataIndex: 'plaintiffUser',
key: 'plaintiffUser'
dataIndex: 'plaintiffAppellant',
key: 'plaintiffAppellant'
},
{
title: '被告/被上诉人',
dataIndex: 'plaintiffUser',
key: 'plaintiffUser'
dataIndex: 'appellee',
key: 'appellee'
},
{
title: '其他当事人/第三人',
@@ -209,8 +210,9 @@
width: 180,
align: 'center',
ellipsis: true,
sorter: true,
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd HH:mm:ss')
},
}
// {
// title: '操作',
// key: 'action',

View File

@@ -14,6 +14,7 @@
>
<template #toolbar>
<a-space class="flex">
<RefreshCompanyIdButton module="credit-external" @done="reload" />
<search
@search="reload"
:selection="selection"
@@ -82,6 +83,7 @@
ColumnItem
} from 'ele-admin-pro/es/ele-pro-table/types';
import Search from '@/views/credit/components/CreditSearchToolbar2.vue';
import RefreshCompanyIdButton from '@/views/credit/components/RefreshCompanyIdButton.vue';
import { exportCreditData } from '../utils/export';
import { getPageTitle } from '@/utils/common';
import CreditExternalEdit from './components/creditExternalEdit.vue';
@@ -225,6 +227,7 @@
width: 180,
align: 'center',
ellipsis: true,
sorter: true,
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd HH:mm:ss')
}
// {

View File

@@ -41,9 +41,11 @@
(e: 'update:visible', visible: boolean): void;
}>();
defineProps<{
const props = defineProps<{
// 是否打开弹窗
visible: boolean;
// 关联企业ID企业详情下导入时需要
companyId?: number;
}>();
// 导入请求状态
@@ -74,7 +76,7 @@
return false;
}
loading.value = true;
importCreditFinalVersionHistory(file)
importCreditFinalVersionHistory(file, props.companyId)
.then((msg) => {
loading.value = false;
message.success(msg);
@@ -93,4 +95,3 @@
emit('update:visible', value);
};
</script>

View File

@@ -15,6 +15,7 @@
<a-space class="flex">
<search
@search="reload"
module="credit-final-version"
:selection="selection"
@add="openEdit"
@remove="removeBatch"
@@ -22,12 +23,6 @@
@importData="openImport"
@exportData="exportData"
/>
<a-button type="dashed" class="ele-btn-icon" @click="openImport2">
<template #icon>
<CloudUploadOutlined />
</template>
<span class="text-red-500">导入历史终本案件</span>
</a-button>
</a-space>
</template>
<template #bodyCell="{ column, record }">
@@ -65,15 +60,13 @@
/>
<!-- 导入弹窗 -->
<CreditFinalVersionImport v-model:visible="showImport" @done="reload" />
<!-- 历史导入弹窗 -->
<CreditFinalVersionHistoryImport v-model:visible="showImport2" @done="reload" />
</a-page-header>
</template>
<script lang="ts" setup>
import { createVNode, ref } from 'vue';
import { message, Modal } from 'ant-design-vue';
import { ExclamationCircleOutlined, CloudUploadOutlined } from '@ant-design/icons-vue';
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
import type { EleProTable } from 'ele-admin-pro';
import { toDateString } from 'ele-admin-pro';
import type {
@@ -85,7 +78,6 @@
import { getPageTitle } from '@/utils/common';
import CreditFinalVersionEdit from './components/creditFinalVersionEdit.vue';
import CreditFinalVersionImport from './components/credit-final-version-import.vue';
import CreditFinalVersionHistoryImport from './components/credit-final-version-history-import.vue';
import {
pageCreditFinalVersion,
listCreditFinalVersion,
@@ -108,8 +100,6 @@
const showEdit = ref(false);
// 是否显示导入弹窗
const showImport = ref(false);
// 是否显示历史导入弹窗
const showImport2 = ref(false);
// 是否显示批量移动弹窗
const showMove = ref(false);
// 加载状态
@@ -155,23 +145,23 @@
},
{
title: '原告/上诉人',
dataIndex: 'dataType',
key: 'dataType'
dataIndex: 'plaintiffAppellant',
key: 'plaintiffAppellant'
},
{
title: '被告/被上诉人',
dataIndex: 'dataType',
key: 'dataType'
dataIndex: 'appellee',
key: 'appellee'
},
{
title: '其他当事人/第三人',
dataIndex: 'dataType',
key: 'dataType'
dataIndex: 'otherPartiesThirdParty',
key: 'otherPartiesThirdParty'
},
{
title: '发生时间',
dataIndex: 'occurrenceTime',
key: 'occurrenceTime',
dataIndex: 'finalDate',
key: 'finalDate',
width: 120
},
{
@@ -242,8 +232,9 @@
width: 180,
align: 'center',
ellipsis: true,
sorter: true,
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd HH:mm:ss')
},
}
// {
// title: '操作',
// key: 'action',
@@ -280,11 +271,6 @@
showImport.value = true;
};
/* 打开历史导入弹窗 */
const openImport2 = () => {
showImport2.value = true;
};
/* 导出 */
const exportData = () => {
exportCreditData<CreditFinalVersion>({

View File

@@ -41,9 +41,11 @@
(e: 'update:visible', visible: boolean): void;
}>();
defineProps<{
const props = defineProps<{
// 是否打开弹窗
visible: boolean;
// 关联企业ID企业详情下导入时需要
companyId?: number;
}>();
// 导入请求状态
@@ -74,7 +76,7 @@
return false;
}
loading.value = true;
importCreditGqdjHistory(file)
importCreditGqdjHistory(file, props.companyId)
.then((msg) => {
loading.value = false;
message.success(msg);
@@ -93,4 +95,3 @@
emit('update:visible', value);
};
</script>

View File

@@ -16,6 +16,7 @@
<a-space class="flex">
<search
@search="reload"
module="credit-gqdj"
:selection="selection"
@add="openEdit"
@remove="removeBatch"
@@ -23,12 +24,6 @@
@importData="openImport"
@exportData="exportData"
/>
<a-button type="dashed" class="ele-btn-icon" @click="openImport2">
<template #icon>
<CloudUploadOutlined />
</template>
<span class="text-red-500">导入历史股权冻结</span>
</a-button>
</a-space>
</template>
<template #bodyCell="{ column, record }">
@@ -38,10 +33,16 @@
<template v-if="column.key === 'image'">
<a-image :src="record.image" :width="50" />
</template>
<template v-if="column.key === 'appellee'">
{{ record.appellee2 ? record.appellee2 : record.appellee }}
</template>
<template v-if="column.key === 'status'">
<a-tag v-if="record.status === 0" color="green">显示</a-tag>
<a-tag v-if="record.status === 1" color="red">隐藏</a-tag>
</template>
<template v-if="column.key === 'dataType'">
{{ record.dataType }}
</template>
<template v-if="column.key === 'action'">
<a-space>
<a @click="openEdit(record)">修改</a>
@@ -62,15 +63,13 @@
<CreditGqdjEdit v-model:visible="showEdit" :data="current" @done="reload" />
<!-- 导入弹窗 -->
<CreditGqdjImport v-model:visible="showImport" @done="reload" />
<!-- 历史导入弹窗 -->
<CreditGqdjHistoryImport v-model:visible="showImport2" @done="reload" />
</a-page-header>
</template>
<script lang="ts" setup>
import { createVNode, ref } from 'vue';
import { message, Modal } from 'ant-design-vue';
import { ExclamationCircleOutlined, CloudUploadOutlined } from '@ant-design/icons-vue';
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
import type { EleProTable } from 'ele-admin-pro';
import { toDateString } from 'ele-admin-pro';
import type {
@@ -82,7 +81,6 @@
import { getPageTitle } from '@/utils/common';
import CreditGqdjEdit from './components/creditGqdjEdit.vue';
import CreditGqdjImport from './components/credit-gqdj-import.vue';
import CreditGqdjHistoryImport from './components/credit-gqdj-history-import.vue';
import {
pageCreditGqdj,
listCreditGqdj,
@@ -105,8 +103,6 @@
const showEdit = ref(false);
// 是否显示导入弹窗
const showImport = ref(false);
// 是否显示历史导入弹窗
const showImport2 = ref(false);
// 是否显示批量移动弹窗
const showMove = ref(false);
// 加载状态
@@ -157,13 +153,13 @@
},
{
title: '冻结股权标的企业',
dataIndex: 'plaintiffUser',
key: 'plaintiffUser'
dataIndex: 'plaintiffAppellant',
key: 'plaintiffAppellant'
},
{
title: '被执行人',
dataIndex: 'plaintiffUser',
key: 'plaintiffUser'
dataIndex: 'appellee',
key: 'appellee'
},
{
title: '执行法院',
@@ -187,18 +183,18 @@
},
{
title: '冻结结束日期',
dataIndex: 'freezeDateStart',
key: 'freezeDateStart'
dataIndex: 'freezeDateEnd',
key: 'freezeDateEnd'
},
{
title: '状态',
dataIndex: 'status',
key: 'status'
dataIndex: 'dataStatus',
key: 'dataStatus'
},
{
title: '数据状态',
dataIndex: 'dataStatus',
key: 'dataStatus'
dataIndex: 'dataType',
key: 'dataType'
},
{
title: '操作人',
@@ -214,8 +210,9 @@
width: 180,
align: 'center',
ellipsis: true,
sorter: true,
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd HH:mm:ss')
},
}
// {
// title: '操作',
// key: 'action',
@@ -252,11 +249,6 @@
showImport.value = true;
};
/* 打开历史导入弹窗 */
const openImport2 = () => {
showImport2.value = true;
};
/* 导出 */
const exportData = () => {
exportCreditData<CreditGqdj>({
@@ -266,7 +258,10 @@
{ title: '执行通知文书号', dataIndex: '执行通知文书号' },
{ title: '被执行人', dataIndex: 'appellee' },
{ title: '冻结股权标的企业', dataIndex: 'plaintiffAppellant' },
{ title: '被执行人持有股权、其他投资权益数额', dataIndex: 'involvedAmount' },
{
title: '被执行人持有股权、其他投资权益数额',
dataIndex: 'involvedAmount'
},
{ title: '执行法院', dataIndex: 'courtName' },
{ title: '类型', dataIndex: 'dataType' },
{ title: '状态', dataIndex: 'dataStatus' },

View File

@@ -188,8 +188,9 @@
align: 'center',
sorter: true,
ellipsis: true,
sorter: true,
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd HH:mm:ss')
},
}
// {
// title: '操作',
// key: 'action',

View File

@@ -41,9 +41,11 @@
(e: 'update:visible', visible: boolean): void;
}>();
defineProps<{
const props = defineProps<{
// 是否打开弹窗
visible: boolean;
// 关联企业ID企业详情下导入时需要
companyId?: number;
}>();
// 导入请求状态
@@ -74,7 +76,7 @@
return false;
}
loading.value = true;
importCreditJudgmentDebtorHistory(file)
importCreditJudgmentDebtorHistory(file, props.companyId)
.then((msg) => {
loading.value = false;
message.success(msg);

View File

@@ -15,6 +15,7 @@
<a-space class="flex">
<search
@search="reload"
module="credit-judgment-debtor"
:selection="selection"
@add="openEdit"
@remove="removeBatch"
@@ -22,12 +23,6 @@
@importData="openImport"
@exportData="exportData"
/>
<a-button type="dashed" class="ele-btn-icon" @click="openImport2">
<template #icon>
<CloudUploadOutlined />
</template>
<span class="text-red-500">导入历史被执行人</span>
</a-button>
</a-space>
</template>
<template #bodyCell="{ column, record }">
@@ -37,6 +32,12 @@
<template v-if="column.key === 'image'">
<a-image :src="record.image" :width="50" />
</template>
<template v-if="column.key === 'appellee'">
{{ record.appellee || record.name }}
</template>
<template v-if="column.key === 'involvedAmount'">
{{ record.involvedAmount || record.amount }}
</template>
<template v-if="column.key === 'caseNumber'">
<template v-if="record.historyId">
<span class="text-red-500 font-bold">{{
@@ -65,7 +66,7 @@
<a-tag v-if="record.status === 1" color="red">隐藏</a-tag>
</template>
<template v-if="column.key === 'updateTime'">
<div>{{ record.updateTime }}</div>
<div>{{ record.updateTime }}</div>
</template>
<template v-if="column.key === 'action'">
<a-space>
@@ -91,21 +92,13 @@
/>
<!-- 导入弹窗 -->
<CreditJudgmentDebtorImport v-model:visible="showImport" @done="reload" />
<!-- 历史被执行人导入弹窗 -->
<CreditJudgmentDebtorHistoryImport
v-model:visible="showImport2"
@done="reload"
/>
</a-page-header>
</template>
<script lang="ts" setup>
import { createVNode, ref } from 'vue';
import { message, Modal } from 'ant-design-vue';
import {
CloudUploadOutlined,
ExclamationCircleOutlined
} from '@ant-design/icons-vue';
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
import type { EleProTable } from 'ele-admin-pro';
import { toDateString } from 'ele-admin-pro';
import type {
@@ -117,7 +110,6 @@
import { getPageTitle } from '@/utils/common';
import CreditJudgmentDebtorEdit from './components/creditJudgmentDebtorEdit.vue';
import CreditJudgmentDebtorImport from './components/credit-judgment-debtor-import.vue';
import CreditJudgmentDebtorHistoryImport from './components/credit-judgment-debtor-history-import.vue';
import {
pageCreditJudgmentDebtor,
listCreditJudgmentDebtor,
@@ -141,8 +133,6 @@
const showEdit = ref(false);
// 是否显示导入弹窗
const showImport = ref(false);
// 是否显示历史被执行人导入弹窗
const showImport2 = ref(false);
// 是否显示批量移动弹窗
const showMove = ref(false);
// 加载状态
@@ -186,25 +176,39 @@
dataIndex: 'companyName',
key: 'companyName'
},
{
title: '原告/上诉人',
dataIndex: 'plaintiffAppellant',
key: 'plaintiffAppellant',
sorter: true
},
{
title: '被告/被上诉人',
dataIndex: 'appellee',
key: 'appellee',
sorter: true
},
{
title: '其他当事人/第三人',
dataIndex: 'otherPartiesThirdParty',
key: 'otherPartiesThirdParty',
sorter: true
},
{
title: '发生时间',
dataIndex: 'occurrenceTime',
key: 'occurrenceTime',
width: 120
},
{
title: '案号',
dataIndex: 'caseNumber',
key: 'caseNumber'
},
{
title: '被执行人名称',
dataIndex: 'name',
key: 'name'
},
{
title: '证件号/组织机构代码',
dataIndex: 'code',
key: 'code'
},
{
title: '立案日期',
dataIndex: 'occurrenceTime',
key: 'occurrenceTime',
title: '涉案金额',
dataIndex: 'involvedAmount',
key: 'involvedAmount',
width: 120
},
{
@@ -212,12 +216,6 @@
dataIndex: 'courtName',
key: 'courtName'
},
{
title: '执行标的(元)',
dataIndex: 'amount',
key: 'amount',
width: 120
},
{
title: '数据状态',
dataIndex: 'dataStatus',
@@ -239,6 +237,7 @@
width: 180,
align: 'center',
ellipsis: true,
sorter: true,
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd HH:mm:ss')
}
// {
@@ -277,11 +276,6 @@
showImport.value = true;
};
/* 打开历史被执行人导入弹窗 */
const openImport2 = () => {
showImport2.value = true;
};
/* 导出 */
const exportData = () => {
exportCreditData<CreditJudgmentDebtor>({
@@ -290,7 +284,6 @@
{ title: 'ID', dataIndex: 'id' },
{ title: '案号', dataIndex: 'caseNumber' },
{ title: '被执行人名称', dataIndex: 'name' },
{ title: '证件号/组织机构代码', dataIndex: 'code' },
{ title: '立案日期', dataIndex: 'occurrenceTime' },
{ title: '执行标的(元)', dataIndex: 'amount' },
{ title: '法院', dataIndex: 'courtName' },

View File

@@ -41,9 +41,11 @@
(e: 'update:visible', visible: boolean): void;
}>();
defineProps<{
const props = defineProps<{
// 是否打开弹窗
visible: boolean;
// 关联企业ID企业详情下导入时需要
companyId?: number;
}>();
// 导入请求状态
@@ -74,7 +76,7 @@
return false;
}
loading.value = true;
importCreditJudicialDocumentHistory(file)
importCreditJudicialDocumentHistory(file, props.companyId)
.then((msg) => {
loading.value = false;
message.success(msg);
@@ -93,4 +95,3 @@
emit('update:visible', value);
};
</script>

View File

@@ -16,6 +16,7 @@
<a-space class="flex">
<search
@search="reload"
module="credit-judicial-document"
:selection="selection"
@add="openEdit"
@remove="removeBatch"
@@ -23,12 +24,6 @@
@importData="openImport"
@exportData="exportData"
/>
<a-button type="dashed" class="ele-btn-icon" @click="openImport2">
<template #icon>
<CloudUploadOutlined />
</template>
<span class="text-red-500">导入历史裁判文书</span>
</a-button>
</a-space>
</template>
<template #bodyCell="{ column, record }">
@@ -66,18 +61,13 @@
/>
<!-- 导入弹窗 -->
<CreditJudicialDocumentImport v-model:visible="showImport" @done="reload" />
<!-- 历史导入弹窗 -->
<CreditJudicialDocumentHistoryImport
v-model:visible="showImport2"
@done="reload"
/>
</a-page-header>
</template>
<script lang="ts" setup>
import { createVNode, ref } from 'vue';
import { message, Modal } from 'ant-design-vue';
import { ExclamationCircleOutlined, CloudUploadOutlined } from '@ant-design/icons-vue';
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
import type { EleProTable } from 'ele-admin-pro';
import { toDateString } from 'ele-admin-pro';
import type {
@@ -89,7 +79,6 @@
import { getPageTitle } from '@/utils/common';
import CreditJudicialDocumentEdit from './components/creditJudicialDocumentEdit.vue';
import CreditJudicialDocumentImport from './components/credit-judicial-document-import.vue';
import CreditJudicialDocumentHistoryImport from './components/credit-judicial-document-history-import.vue';
import {
pageCreditJudicialDocument,
listCreditJudicialDocument,
@@ -112,8 +101,6 @@
const showEdit = ref(false);
// 是否显示导入弹窗
const showImport = ref(false);
// 是否显示历史导入弹窗
const showImport2 = ref(false);
// 是否显示批量移动弹窗
const showMove = ref(false);
// 加载状态
@@ -164,8 +151,8 @@
},
{
title: '文书类型',
dataIndex: 'type',
key: 'type'
dataIndex: 'documentType',
key: 'documentType'
},
{
title: '当事人',
@@ -229,8 +216,9 @@
width: 180,
align: 'center',
ellipsis: true,
sorter: true,
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd HH:mm:ss')
},
}
// {
// title: '操作',
// key: 'action',
@@ -267,11 +255,6 @@
showImport.value = true;
};
/* 打开历史导入弹窗 */
const openImport2 = () => {
showImport2.value = true;
};
/* 导出 */
const exportData = () => {
exportCreditData<CreditJudicialDocument>({

View File

@@ -1,18 +1,18 @@
<!-- 搜索表单 -->
<template>
<a-space :size="10" style="flex-wrap: wrap">
<!-- <a-button type="primary" class="ele-btn-icon" @click="add">-->
<!-- <template #icon>-->
<!-- <PlusOutlined />-->
<!-- </template>-->
<!-- <span>添加</span>-->
<!-- </a-button>-->
<!-- <a-button class="ele-btn-icon" @click="openImport">-->
<!-- <template #icon>-->
<!-- <CloudUploadOutlined />-->
<!-- </template>-->
<!-- <span>导入()</span>-->
<!-- </a-button>-->
<!-- <a-button type="primary" class="ele-btn-icon" @click="add">-->
<!-- <template #icon>-->
<!-- <PlusOutlined />-->
<!-- </template>-->
<!-- <span>添加</span>-->
<!-- </a-button>-->
<!-- <a-button class="ele-btn-icon" @click="openImport">-->
<!-- <template #icon>-->
<!-- <CloudUploadOutlined />-->
<!-- </template>-->
<!-- <span>导入()</span>-->
<!-- </a-button>-->
<a-button class="ele-btn-icon" @click="exportData">
<template #icon>
<CloudDownloadOutlined />
@@ -53,7 +53,7 @@
CreditJudiciary,
CreditJudiciaryParam
} from '@/api/credit/creditJudiciary/model';
import RefreshCompanyIdButton from "@/views/credit/components/RefreshCompanyIdButton.vue";
import RefreshCompanyIdButton from '@/views/credit/components/RefreshCompanyIdButton.vue';
const props = withDefaults(
defineProps<{

View File

@@ -82,6 +82,7 @@
ColumnItem
} from 'ele-admin-pro/es/ele-pro-table/types';
import Search from './components/search.vue';
import { toDateString } from 'ele-admin-pro';
import RefreshCompanyIdButton from '@/views/credit/components/RefreshCompanyIdButton.vue';
import { getPageTitle } from '@/utils/common';
import CreditJudiciaryEdit from './components/creditJudiciaryEdit.vue';
@@ -206,6 +207,16 @@
key: 'realName',
width: 90,
align: 'center'
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
width: 200,
align: 'center',
sorter: true,
ellipsis: true,
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd HH:mm:ss')
}
// {
// title: '操作',

View File

@@ -0,0 +1,98 @@
<!-- 历史诉前调解导入弹窗 -->
<template>
<ele-modal
:width="520"
:footer="null"
title="历史诉前调解批量导入"
:visible="visible"
@update:visible="updateVisible"
>
<a-spin :spinning="loading">
<a-upload-dragger
accept=".xls,.xlsx"
:show-upload-list="false"
:customRequest="doUpload"
style="padding: 24px 0; margin-bottom: 16px"
>
<p class="ant-upload-drag-icon">
<cloud-upload-outlined />
</p>
<p class="ant-upload-hint">将文件拖到此处或点击上传</p>
</a-upload-dragger>
</a-spin>
<div class="ele-text-center">
<span>只能上传xlsxlsx文件</span>
<a :href="templateUrl" download="历史诉前调解导入模板.xlsx">
下载导入模板
</a>
</div>
</ele-modal>
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue';
import { message } from 'ant-design-vue/es';
import { CloudUploadOutlined } from '@ant-design/icons-vue';
import { importCreditMediationHistory } from '@/api/credit/creditMediation';
import { API_BASE_URL } from '@/config/setting';
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
const props = defineProps<{
// 是否打开弹窗
visible: boolean;
// 关联企业ID企业详情下导入时需要
companyId?: number;
}>();
// 导入请求状态
const loading = ref(false);
// 模板下载地址,保持与当前接口域名一致
const templateUrl = computed(() => {
const base = (localStorage.getItem('ApiUrl') || API_BASE_URL || '').replace(
/\/$/,
''
);
return `${base}/credit/credit-mediation/import/history/template`;
});
/* 上传 */
const doUpload = ({ file }) => {
if (
![
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
].includes(file.type)
) {
message.error('只能选择 excel 文件');
return false;
}
if (file.size / 1024 / 1024 > 10) {
message.error('大小不能超过 10MB');
return false;
}
loading.value = true;
importCreditMediationHistory(file, props.companyId)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
return false;
};
/* 更新 visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
</script>

View File

@@ -15,6 +15,7 @@
<a-space class="flex">
<search
@search="reload"
module="credit-mediation"
:selection="selection"
@add="openEdit"
@remove="removeBatch"
@@ -144,13 +145,13 @@
},
{
title: '原告/上诉人',
dataIndex: 'plaintiffUser',
key: 'plaintiffUser'
dataIndex: 'plaintiffAppellant',
key: 'plaintiffAppellant'
},
{
title: '被告/被上诉人',
dataIndex: 'plaintiffUser',
key: 'plaintiffUser'
dataIndex: 'appellee',
key: 'appellee'
},
{
title: '其他当事人/第三人',
@@ -201,8 +202,9 @@
width: 180,
align: 'center',
ellipsis: true,
sorter: true,
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd HH:mm:ss')
},
}
// {
// title: '操作',
// key: 'action',

View File

@@ -0,0 +1,391 @@
<!-- 编辑弹窗 -->
<template>
<ele-modal
:width="800"
:visible="visible"
:maskClosable="false"
:maxable="maxable"
:title="isUpdate ? '编辑小程序端客户' : '添加小程序端客户'"
:body-style="{ paddingBottom: '28px' }"
@update:visible="updateVisible"
@ok="save"
>
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col="styleResponsive ? { md: 4, sm: 5, xs: 24 } : { flex: '90px' }"
:wrapper-col="
styleResponsive ? { md: 19, sm: 19, xs: 24 } : { flex: '1' }
"
>
<a-form-item label="拖欠方" name="toUser">
<a-input
allow-clear
placeholder="请输入拖欠方"
v-model:value="form.toUser"
/>
</a-form-item>
<a-form-item label="拖欠金额" name="price">
<a-input
allow-clear
placeholder="请输入拖欠金额"
v-model:value="form.price"
/>
</a-form-item>
<a-form-item label="拖欠年数" name="years">
<a-input
allow-clear
placeholder="请输入拖欠年数"
v-model:value="form.years"
/>
</a-form-item>
<!-- <a-form-item label="状态" name="statusTxt">-->
<!-- <a-input-->
<!-- allow-clear-->
<!-- placeholder="请输入状态"-->
<!-- v-model:value="form.statusTxt"-->
<!-- />-->
<!-- </a-form-item>-->
<!-- <a-form-item label="企业ID" name="companyId">-->
<!-- <a-input-->
<!-- allow-clear-->
<!-- placeholder="请输入企业ID"-->
<!-- v-model:value="form.companyId"-->
<!-- />-->
<!-- </a-form-item>-->
<!-- <a-form-item label="所在省份" name="province">-->
<!-- <a-input-->
<!-- allow-clear-->
<!-- placeholder="请输入所在省份"-->
<!-- v-model:value="form.province"-->
<!-- />-->
<!-- </a-form-item>-->
<a-form-item label="所在城市" name="city">
<RegionsSelect
v-model:value="regions"
type="provinceCity"
valueField="label"
placeholder="请选择省/市"
/>
</a-form-item>
<a-form-item label="步骤" name="step">
<a-select v-model:value="form.step" placeholder="请选择步骤" allow-clear>
<a-select-option
v-for="item in stepOptions"
:key="item.value"
:value="item.value"
>
{{ item.text }}
</a-select-option>
</a-select>
</a-form-item>
<!-- <a-form-item label="所在辖区" name="region">-->
<!-- <a-input-->
<!-- allow-clear-->
<!-- placeholder="请输入所在辖区"-->
<!-- v-model:value="form.region"-->
<!-- />-->
<!-- </a-form-item>-->
<a-form-item label="文件路径" name="files">
<SelectFile
:data="fileList"
:limit="9"
placeholder="请选择附件"
@done="onFileChoose"
@del="onFileDelete"
/>
</a-form-item>
<!-- <a-form-item label="是否有数据" name="hasData">-->
<!-- <a-input-->
<!-- allow-clear-->
<!-- placeholder="请输入是否有数据"-->
<!-- v-model:value="form.hasData"-->
<!-- />-->
<!-- </a-form-item>-->
<a-form-item label="备注" name="comments">
<a-textarea
:rows="4"
:maxlength="200"
placeholder="请输入描述"
v-model:value="form.comments"
/>
</a-form-item>
<!-- <a-form-item label="是否推荐" name="recommend">-->
<!-- <a-input-->
<!-- allow-clear-->
<!-- placeholder="请输入是否推荐"-->
<!-- v-model:value="form.recommend"-->
<!-- />-->
<!-- </a-form-item>-->
<a-form-item label="排序" name="sortNumber">
<a-input-number
:min="0"
:max="9999"
class="ele-fluid"
placeholder="请输入排序号"
v-model:value="form.sortNumber"
/>
</a-form-item>
<!-- <a-form-item label="状态, 0正常, 1冻结" name="status">-->
<!-- <a-radio-group v-model:value="form.status">-->
<!-- <a-radio :value="0">显示</a-radio>-->
<!-- <a-radio :value="1">隐藏</a-radio>-->
<!-- </a-radio-group>-->
<!-- </a-form-item>-->
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import { Form, message } from 'ant-design-vue';
import { assignObject, uuid } from 'ele-admin-pro';
import { addCreditMpCustomer, updateCreditMpCustomer } from '@/api/credit/creditMpCustomer';
import { CreditMpCustomer } from '@/api/credit/creditMpCustomer/model';
import { useThemeStore } from '@/store/modules/theme';
import { storeToRefs } from 'pinia';
import { FormInstance } from 'ant-design-vue/es/form';
import { FileRecord } from '@/api/system/file/model';
import RegionsSelect from '@/components/RegionsSelect/index.vue';
import SelectFile from '@/components/SelectFile/index.vue';
import { isImage, stripOssImageProcess } from '@/utils/common';
// 是否是修改
const isUpdate = ref(false);
const useForm = Form.useForm;
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: CreditMpCustomer | null;
}>();
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
// 提交状态
const loading = ref(false);
// 是否显示最大化切换按钮
const maxable = ref(true);
// 表格选中数据
const formRef = ref<FormInstance | null>(null);
const regions = ref<string[]>();
const fileList = ref<any[]>([]);
const stepOptions = [
{ value: 0, text: '未受理' },
{ value: 1, text: '已受理' },
{ value: 2, text: '材料提交' },
{ value: 3, text: '合同签订' },
{ value: 4, text: '执行回款' },
{ value: 5, text: '完结' }
];
// 用户信息
const form = reactive<CreditMpCustomer>({
id: undefined,
toUser: undefined,
price: undefined,
years: undefined,
url: undefined,
statusTxt: undefined,
companyId: undefined,
province: undefined,
city: undefined,
region: undefined,
files: undefined,
step: 0,
hasData: undefined,
recommend: undefined,
status: undefined,
deleted: undefined,
userId: undefined,
tenantId: undefined,
createTime: undefined,
updateTime: undefined,
comments: '',
sortNumber: 100
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 表单验证规则
const rules = reactive({
creditMpCustomerName: [
{
required: true,
type: 'string',
message: '请填写小程序端客户名称',
trigger: 'blur'
}
]
});
const guessNameFromUrl = (url: string) => {
const cleanUrl = (url.split('?')[0] ?? '').trim();
const last = cleanUrl.split('/').pop() || '';
try {
return decodeURIComponent(last) || url;
} catch {
return last || url;
}
};
const normalizeFiles = (raw: unknown) => {
if (!raw) return [];
if (Array.isArray(raw)) {
return raw
.map((item: any) => {
if (!item) return null;
if (typeof item === 'string') {
const url = isImage(item) ? item : stripOssImageProcess(item);
return {
url,
name: guessNameFromUrl(url),
thumbnail: url,
isImage: isImage(url)
};
}
const rawUrl = item.url || item.path || item.href;
const url =
rawUrl && typeof rawUrl === 'string'
? isImage(rawUrl)
? rawUrl
: stripOssImageProcess(rawUrl)
: undefined;
if (!url) return null;
return {
url,
name: typeof item.name === 'string' ? item.name : guessNameFromUrl(url),
thumbnail: typeof item.thumbnail === 'string' ? item.thumbnail : undefined,
isImage: typeof item.isImage === 'boolean' ? item.isImage : isImage(url)
};
})
.filter(Boolean);
}
if (typeof raw === 'string') {
const text = raw.trim();
if (!text) return [];
try {
let parsed: any = JSON.parse(text);
if (typeof parsed === 'string') parsed = JSON.parse(parsed);
if (Array.isArray(parsed)) return normalizeFiles(parsed);
} catch {
// ignore
}
return normalizeFiles(text.includes(',') ? text.split(',') : [text]);
}
return [];
};
const syncFilesToForm = () => {
const payload = (fileList.value ?? [])
.map((f: any) => {
const url = f?.url;
if (!url) return null;
return {
name: f?.name || guessNameFromUrl(url),
url,
thumbnail: f?.thumbnail,
isImage: typeof f?.isImage === 'boolean' ? f.isImage : isImage(url)
};
})
.filter(Boolean);
form.files = payload.length ? JSON.stringify(payload) : '';
};
const onFileChoose = (data: FileRecord) => {
const rawUrl = data.url || data.downloadUrl || data.path;
const url = rawUrl ? (isImage(rawUrl) ? rawUrl : stripOssImageProcess(rawUrl)) : undefined;
if (!url) return;
const exists = fileList.value.some((d: any) => d?.url === url);
if (exists) return;
fileList.value.push({
url,
name: data.name || guessNameFromUrl(url),
thumbnail: data.thumbnail,
isImage: isImage(url)
});
syncFilesToForm();
};
const onFileDelete = (index: number) => {
fileList.value.splice(index, 1);
syncFilesToForm();
};
const { resetFields } = useForm(form, rules);
// 级联选择回填到表单字段
watch(
regions,
(val) => {
form.province = val?.[0];
form.city = val?.[1];
},
{ immediate: true }
);
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
const formData = {
...form
};
const saveOrUpdate = isUpdate.value ? updateCreditMpCustomer : addCreditMpCustomer;
saveOrUpdate(formData)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {});
};
watch(
() => props.visible,
(visible) => {
if (visible) {
regions.value = undefined;
fileList.value = [];
if (props.data) {
assignObject(form, props.data);
regions.value = form.province && form.city ? [form.province, form.city] : undefined;
fileList.value = normalizeFiles(props.data.files);
syncFilesToForm();
isUpdate.value = true;
} else {
isUpdate.value = false;
form.step = 0;
}
} else {
resetFields();
regions.value = undefined;
fileList.value = [];
}
},
{ immediate: true }
);
</script>

View File

@@ -0,0 +1,120 @@
<!-- 搜索表单 -->
<template>
<a-space :size="10" style="flex-wrap: wrap">
<!-- <a-button type="primary" class="ele-btn-icon" @click="add">-->
<!-- <template #icon>-->
<!-- <PlusOutlined />-->
<!-- </template>-->
<!-- <span>添加</span>-->
<!-- </a-button>-->
<a-button class="ele-btn-icon" @click="exportData">
<template #icon>
<CloudDownloadOutlined />
</template>
<span>导出</span>
</a-button>
<a-button
danger
class="ele-btn-icon"
:disabled="!selection?.length"
@click="remove"
>
<template #icon>
<DeleteOutlined />
</template>
<span>批量删除</span>
</a-button>
<a-select
v-model:value="step"
allow-clear
placeholder="步骤"
style="width: 140px"
@change="handleSearch"
>
<a-select-option
v-for="item in stepOptions"
:key="item.value"
:value="item.value"
>
{{ item.text }}
</a-select-option>
</a-select>
<a-input-search
allow-clear
v-model:value="keywords"
placeholder="请输入关键词"
style="width: 220px"
@search="handleSearch"
@pressEnter="handleSearch"
/>
</a-space>
</template>
<script lang="ts" setup>
import { computed, ref, watch } from 'vue';
import {
PlusOutlined,
CloudDownloadOutlined,
DeleteOutlined
} from '@ant-design/icons-vue';
import type {
CreditMpCustomer,
CreditMpCustomerParam
} from '@/api/credit/creditMpCustomer/model';
const props = withDefaults(
defineProps<{
// 选中的角色
selection?: CreditMpCustomer[];
}>(),
{
selection: () => []
}
);
const emit = defineEmits<{
(e: 'search', where?: CreditMpCustomerParam): void;
(e: 'add'): void;
(e: 'remove'): void;
(e: 'batchMove'): void;
(e: 'exportData'): void;
}>();
const keywords = ref('');
const step = ref<number | undefined>(undefined);
const selection = computed(() => props.selection || []);
const stepOptions = [
{ value: 0, text: '未受理' },
{ value: 1, text: '已受理' },
{ value: 2, text: '材料提交' },
{ value: 3, text: '合同签订' },
{ value: 4, text: '执行回款' },
{ value: 5, text: '完结' }
];
// 新增
const add = () => {
emit('add');
};
// 搜索
const handleSearch = () => {
emit('search', { keywords: keywords.value || undefined, step: step.value });
};
// 导出
const exportData = () => {
emit('exportData');
};
// 批量删除
const remove = () => {
emit('remove');
};
watch(
() => props.selection,
() => {}
);
</script>

View File

@@ -0,0 +1,589 @@
<template>
<a-page-header :title="getPageTitle()" @back="() => $router.go(-1)">
<a-card :bordered="false" :body-style="{ padding: '16px' }">
<ele-pro-table
ref="tableRef"
row-key="id"
:columns="columns"
:datasource="datasource"
:customRow="customRow"
v-model:selection="selection"
tool-class="ele-toolbar-form"
class="sys-org-table"
>
<template #toolbar>
<search
@search="reload"
:selection="selection"
@add="openEdit"
@remove="removeBatch"
@batchMove="openMove"
@exportData="exportData"
/>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'userInfo'">
<div class="user-info">
<a-avatar :size="42" :src="record.avatar">
<template v-if="!record.avatar" #icon>
<UserOutlined/>
</template>
</a-avatar>
<div class="user-details">
<h4 class="username">{{ record.nickname }}</h4>
<p class="user-phone">{{ record.phone }}</p>
</div>
</div>
</template>
<template v-if="column.key === 'image'">
<a-image :src="record.image" :width="50"/>
</template>
<template v-if="column.key === 'files'">
<a-space :size="8" style="flex-wrap: wrap">
<template v-for="(file, index) in normalizeFiles(record.files)" :key="`${file.url}-${index}`">
<a-image
v-if="file.isImage"
:src="file.thumbnail || file.url"
:width="50"
:preview="{ src: file.url }"
/>
<a
v-else
:href="sanitizeFileUrl(file.url, file.isImage)"
target="_blank"
rel="noopener noreferrer"
>
{{ file.name || `附件${index + 1}` }}
</a>
</template>
<span v-if="!normalizeFiles(record.files).length">-</span>
</a-space>
</template>
<template v-if="column.key === 'step'">
<a-tag :color="getStepColor(record.step)">
{{ getStepText(record.step) }}
</a-tag>
</template>
<template v-if="column.key === 'status'">
<a-tag v-if="record.status === 0" color="green">显示</a-tag>
<a-tag v-if="record.status === 1" color="red">隐藏</a-tag>
</template>
<template v-if="column.key === 'action'">
<a @click="openEdit(record)">修改</a>
<a-divider type="vertical"/>
<a-popconfirm
title="确定要删除此记录吗?"
@confirm="remove(record)"
>
<a class="ele-text-danger">删除</a>
</a-popconfirm>
</template>
</template>
</ele-pro-table>
</a-card>
<!-- 编辑弹窗 -->
<CreditMpCustomerEdit v-model:visible="showEdit" :data="current" @done="reload"/>
</a-page-header>
</template>
<script lang="ts" setup>
import {createVNode, ref, computed} from 'vue';
import {message, Modal} from 'ant-design-vue';
import {ExclamationCircleOutlined, UserOutlined} from '@ant-design/icons-vue';
import type {EleProTable} from 'ele-admin-pro';
import {toDateString} from 'ele-admin-pro';
import type {
DatasourceFunction,
ColumnItem
} from 'ele-admin-pro/es/ele-pro-table/types';
import Search from './components/search.vue';
import {getPageTitle} from '@/utils/common';
import CreditMpCustomerEdit from './components/creditMpCustomerEdit.vue';
import {
pageCreditMpCustomer,
listCreditMpCustomer,
removeCreditMpCustomer,
removeBatchCreditMpCustomer
} from '@/api/credit/creditMpCustomer';
import type {CreditMpCustomer, CreditMpCustomerParam} from '@/api/credit/creditMpCustomer/model';
import {exportCreditData} from '../utils/export';
import { stripOssImageProcess } from '@/utils/common';
type NormalizedFile = {
name?: string;
url: string;
thumbnail?: string;
isImage: boolean;
};
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格选中数据
const selection = ref<CreditMpCustomer[]>([]);
// 当前编辑数据
const current = ref<CreditMpCustomer | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 是否显示批量移动弹窗
const showMove = ref(false);
// 加载状态
const loading = ref(true);
// 搜索关键词
const searchText = ref('');
const searchStep = ref<number | undefined>(undefined);
// 表格数据源
const datasource: DatasourceFunction = ({
page,
limit,
where = {},
orders,
filters
}) => {
const params: CreditMpCustomerParam = {...(where as CreditMpCustomerParam)};
if (filters) {
const filterStatus = (filters as any).status;
if (Array.isArray(filterStatus)) {
if (filterStatus.length) {
(params as any).status = filterStatus[0];
}
} else if (filterStatus !== undefined && filterStatus !== null) {
(params as any).status = filterStatus;
}
const stepFilter = (filters as any).step;
if (Array.isArray(stepFilter)) {
if (stepFilter.length) {
(params as any).step = stepFilter[0];
}
} else if (stepFilter !== undefined && stepFilter !== null) {
(params as any).step = stepFilter;
}
}
return pageCreditMpCustomer({
...params,
...orders,
page,
limit
});
};
const stepOptions = [
{ value: 0, text: '未受理', color: 'default' },
{ value: 1, text: '已受理', color: 'blue' },
{ value: 2, text: '材料提交', color: 'cyan' },
{ value: 3, text: '合同签订', color: 'purple' },
{ value: 4, text: '执行回款', color: 'orange' },
{ value: 5, text: '完结', color: 'green' }
] as const;
const getStepText = (step: unknown) => {
const value = typeof step === 'string' ? Number(step) : (step as number | undefined);
const match = stepOptions.find((d) => d.value === value);
return match?.text ?? '-';
};
const getStepColor = (step: unknown) => {
const value = typeof step === 'string' ? Number(step) : (step as number | undefined);
const match = stepOptions.find((d) => d.value === value);
return match?.color ?? 'default';
};
const isImageUrl = (url: string) => {
const cleanUrl = url.split('?')[0] ?? '';
return /\.(png|jpe?g|gif|webp|bmp|svg)$/i.test(cleanUrl);
};
const guessNameFromUrl = (url: string) => {
const cleanUrl = (url.split('?')[0] ?? '').trim();
const last = cleanUrl.split('/').pop() || '';
try {
return decodeURIComponent(last) || url;
} catch {
return last || url;
}
};
const tryParseJson = (value: string) => {
try {
return JSON.parse(value);
} catch {
return undefined;
}
};
const sanitizeFileUrl = (url: string, isImage: boolean) => {
return isImage ? url : (stripOssImageProcess(url) as string);
};
const filesCache = new Map<string, NormalizedFile[]>();
const normalizeFiles = (raw: unknown): NormalizedFile[] => {
if (!raw) return [];
if (Array.isArray(raw)) {
return raw
.map((item: any) => {
if (!item) return null;
if (typeof item === 'string') {
const url = sanitizeFileUrl(item, isImageUrl(item));
return {
url,
thumbnail: url,
name: guessNameFromUrl(url),
isImage: isImageUrl(url)
} satisfies NormalizedFile;
}
const rawUrl = item.url || item.path || item.href;
const url = typeof rawUrl === 'string' ? rawUrl : undefined;
if (!url) return null;
const thumbnail = typeof item.thumbnail === 'string' ? item.thumbnail : undefined;
const name = typeof item.name === 'string' ? item.name : guessNameFromUrl(url);
const isImage = typeof item.isImage === 'boolean' ? item.isImage : isImageUrl(url);
return {url: sanitizeFileUrl(url, isImage), thumbnail, name, isImage} satisfies NormalizedFile;
})
.filter(Boolean) as NormalizedFile[];
}
if (typeof raw === 'string') {
const text = raw.trim();
if (!text) return [];
const cached = filesCache.get(text);
if (cached) return cached;
// 兼容:后端返回 JSON 数组字符串(示例:"[{\"name\":\"...\",\"url\":\"...\"}]")
let parsed: any = tryParseJson(text);
if (typeof parsed === 'string') {
parsed = tryParseJson(parsed) ?? parsed;
}
if (Array.isArray(parsed)) {
const result = normalizeFiles(parsed);
if (filesCache.size > 500) filesCache.clear();
filesCache.set(text, result);
return result;
}
// 兜底:单个 url 或逗号分隔 url
const parts = text.includes(',') ? text.split(',') : [text];
const result = normalizeFiles(parts.map((p) => p.trim()).filter(Boolean));
if (filesCache.size > 500) filesCache.clear();
filesCache.set(text, result);
return result;
}
return [];
};
// 完整的列配置(包含所有字段)
const allColumns = ref<ColumnItem[]>([
{
title: 'ID',
dataIndex: 'id',
key: 'id',
width: 90,
},
{
title: '用户信息',
dataIndex: 'userInfo',
key: 'userInfo',
width: 240,
},
{
title: '拖欠方',
dataIndex: 'toUser',
key: 'toUser'
},
{
title: '拖欠金额',
dataIndex: 'price',
key: 'price',
width: 120,
align: 'center',
customRender: ({ text }) => '¥' + text
},
{
title: '拖欠年数',
dataIndex: 'years',
key: 'years',
align: 'center'
},
// {
// title: '链接',
// dataIndex: 'url',
// key: 'url',
// ellipsis: true
// },
// {
// title: '状态',
// dataIndex: 'statusTxt',
// key: 'statusTxt'
// },
// {
// title: '所在省份',
// dataIndex: 'province',
// key: 'province',
// ellipsis: true
// },
{
title: '所在城市',
dataIndex: 'city',
key: 'city',
align: 'center'
},
{
title: '步骤',
dataIndex: 'step',
key: 'step',
width: 120,
align: 'center',
filters: stepOptions.map((d) => ({ text: d.text, value: d.value }))
},
// {
// title: '所在辖区',
// dataIndex: 'region',
// key: 'region',
// ellipsis: true
// },
{
title: '附件',
dataIndex: 'files',
key: 'files',
align: 'center'
},
// {
// title: '是否有数据',
// dataIndex: 'hasData',
// key: 'hasData',
// width: 120
// },
// {
// title: '备注',
// dataIndex: 'comments',
// key: 'comments',
// ellipsis: true
// },
// {
// title: '是否推荐',
// dataIndex: 'recommend',
// key: 'recommend',
// width: 120
// },
{
title: '排序',
dataIndex: 'sortNumber',
key: 'sortNumber',
width: 120
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 120
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
width: 200,
align: 'center',
sorter: true,
ellipsis: true,
customRender: ({text}) => toDateString(text, 'yyyy-MM-dd HH:mm:ss')
},
{
title: '跟进人',
dataIndex: 'realName',
key: 'realName',
width: 120,
align: 'center'
},
{
title: '操作',
key: 'action',
width: 120,
fixed: 'right',
align: 'center',
hideInSetting: true
}
]);
// 默认显示的核心列最多5个主要字段
const defaultVisibleColumns = [
'id',
'userInfo',
'toUser',
'price',
'years',
'city',
'realName',
'step',
'files',
// 'status',
'createTime',
'action'
];
// 根据默认可见列过滤显示的列
const columns = computed(() => {
return allColumns.value.filter(col =>
defaultVisibleColumns.includes(col.dataIndex) || col.key === 'action'
);
});
/* 搜索 */
const reload = (where?: CreditMpCustomerParam) => {
if (where && Object.prototype.hasOwnProperty.call(where, 'keywords')) {
searchText.value = where.keywords ?? '';
}
if (where && Object.prototype.hasOwnProperty.call(where, 'step')) {
searchStep.value = where.step;
}
const targetWhere = where ?? {keywords: searchText.value || undefined, step: searchStep.value};
selection.value = [];
tableRef?.value?.reload({where: targetWhere});
};
/* 打开编辑弹窗 */
const openEdit = (row?: CreditMpCustomer) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开批量移动弹窗 */
const openMove = () => {
showMove.value = true;
};
/* 导出 */
const exportData = () => {
exportCreditData<CreditMpCustomer>({
filename: '小程序端客户列表',
includeCompanyName: false,
columns: [
{title: 'ID', dataIndex: 'id'},
{title: '拖欠方', dataIndex: 'toUser'},
{title: '拖欠金额', dataIndex: 'price'},
{title: '拖欠年数', dataIndex: 'years'},
{title: '链接', dataIndex: 'url'},
{title: '状态', dataIndex: 'statusTxt'},
{title: '所在省份', dataIndex: 'province'},
{title: '所在城市', dataIndex: 'city'},
{title: '所在辖区', dataIndex: 'region'},
{title: '步骤', dataIndex: 'step'},
{title: '文件路径', dataIndex: 'files'},
{title: '是否有数据', dataIndex: 'hasData'},
{title: '备注', dataIndex: 'comments'},
{title: '是否推荐', dataIndex: 'recommend'},
{title: '排序(数字越小越靠前)', dataIndex: 'sortNumber'},
{title: '状态, 0正常, 1冻结', dataIndex: 'status'},
{title: '是否删除, 0否, 1是', dataIndex: 'deleted'},
{title: '用户ID', dataIndex: 'userId'},
{title: '创建时间', dataIndex: 'createTime'},
{title: '修改时间', dataIndex: 'updateTime'}
],
fetchData: () =>
listCreditMpCustomer({
keywords: searchText.value || undefined,
step: searchStep.value
})
});
};
/* 删除单个 */
const remove = (row: CreditMpCustomer) => {
const hide = message.loading('请求中..', 0);
removeCreditMpCustomer(row.id)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
};
/* 批量删除 */
const removeBatch = () => {
if (!selection.value.length) {
message.error('请至少选择一条数据');
return;
}
Modal.confirm({
title: '提示',
content: '确定要删除选中的记录吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
const hide = message.loading('请求中..', 0);
removeBatchCreditMpCustomer(selection.value.map((d) => d.id))
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
/* 查询 */
const query = () => {
loading.value = true;
};
/* 自定义行属性 */
const customRow = (record: CreditMpCustomer) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
};
query();
</script>
<script lang="ts">
export default {
name: 'CreditMpCustomer'
};
</script>
<style lang="less" scoped>
.user-info {
display: flex;
align-items: center;
padding: 6px;
.user-details {
margin-left: 6px;
flex: 1;
.username {
margin: 0;
color: #333;
font-size: 16px;
font-weight: 500;
}
.user-phone {
margin: 0;
color: #666;
font-size: 14px;
}
}
}
</style>

View File

@@ -0,0 +1,589 @@
<template>
<a-page-header :title="getPageTitle()" @back="() => $router.go(-1)">
<a-card :bordered="false" :body-style="{ padding: '16px' }">
<ele-pro-table
ref="tableRef"
row-key="id"
:columns="columns"
:datasource="datasource"
:customRow="customRow"
v-model:selection="selection"
tool-class="ele-toolbar-form"
class="sys-org-table"
>
<template #toolbar>
<search
@search="reload"
:selection="selection"
@add="openEdit"
@remove="removeBatch"
@batchMove="openMove"
@exportData="exportData"
/>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'userInfo'">
<div class="user-info">
<a-avatar :size="42" :src="record.avatar">
<template v-if="!record.avatar" #icon>
<UserOutlined/>
</template>
</a-avatar>
<div class="user-details">
<h4 class="username">{{ record.nickname }}</h4>
<p class="user-phone">{{ record.phone }}</p>
</div>
</div>
</template>
<template v-if="column.key === 'image'">
<a-image :src="record.image" :width="50"/>
</template>
<template v-if="column.key === 'files'">
<a-space :size="8" style="flex-wrap: wrap">
<template v-for="(file, index) in normalizeFiles(record.files)" :key="`${file.url}-${index}`">
<a-image
v-if="file.isImage"
:src="file.thumbnail || file.url"
:width="50"
:preview="{ src: file.url }"
/>
<a
v-else
:href="sanitizeFileUrl(file.url, file.isImage)"
target="_blank"
rel="noopener noreferrer"
>
{{ file.name || `附件${index + 1}` }}
</a>
</template>
<span v-if="!normalizeFiles(record.files).length">-</span>
</a-space>
</template>
<template v-if="column.key === 'step'">
<a-tag :color="getStepColor(record.step)">
{{ getStepText(record.step) }}
</a-tag>
</template>
<template v-if="column.key === 'status'">
<a-tag v-if="record.status === 0" color="green">显示</a-tag>
<a-tag v-if="record.status === 1" color="red">隐藏</a-tag>
</template>
<template v-if="column.key === 'action'">
<a @click="openEdit(record)">修改</a>
<a-divider type="vertical"/>
<a-popconfirm
title="确定要删除此记录吗?"
@confirm="remove(record)"
>
<a class="ele-text-danger">删除</a>
</a-popconfirm>
</template>
</template>
</ele-pro-table>
</a-card>
<!-- 编辑弹窗 -->
<CreditMpCustomerEdit v-model:visible="showEdit" :data="current" @done="reload"/>
</a-page-header>
</template>
<script lang="ts" setup>
import {createVNode, ref, computed} from 'vue';
import {message, Modal} from 'ant-design-vue';
import {ExclamationCircleOutlined, UserOutlined} from '@ant-design/icons-vue';
import type {EleProTable} from 'ele-admin-pro';
import {toDateString} from 'ele-admin-pro';
import type {
DatasourceFunction,
ColumnItem
} from 'ele-admin-pro/es/ele-pro-table/types';
import Search from './../components/search.vue';
import {getPageTitle} from '@/utils/common';
import CreditMpCustomerEdit from './../components/creditMpCustomerEdit.vue';
import {
pageCreditMpCustomer,
listCreditMpCustomer,
removeCreditMpCustomer,
removeBatchCreditMpCustomer
} from '@/api/credit/creditMpCustomer';
import type {CreditMpCustomer, CreditMpCustomerParam} from '@/api/credit/creditMpCustomer/model';
import {exportCreditData} from '../../utils/export';
import { stripOssImageProcess } from '@/utils/common';
type NormalizedFile = {
name?: string;
url: string;
thumbnail?: string;
isImage: boolean;
};
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格选中数据
const selection = ref<CreditMpCustomer[]>([]);
// 当前编辑数据
const current = ref<CreditMpCustomer | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 是否显示批量移动弹窗
const showMove = ref(false);
// 加载状态
const loading = ref(true);
// 搜索关键词
const searchText = ref('');
const searchStep = ref<number | undefined>(undefined);
// 表格数据源
const datasource: DatasourceFunction = ({
page,
limit,
where = {},
orders,
filters
}) => {
const params: CreditMpCustomerParam = {...(where as CreditMpCustomerParam)};
if (filters) {
const filterStatus = (filters as any).status;
if (Array.isArray(filterStatus)) {
if (filterStatus.length) {
(params as any).status = filterStatus[0];
}
} else if (filterStatus !== undefined && filterStatus !== null) {
(params as any).status = filterStatus;
}
const stepFilter = (filters as any).step;
if (Array.isArray(stepFilter)) {
if (stepFilter.length) {
(params as any).step = stepFilter[0];
}
} else if (stepFilter !== undefined && stepFilter !== null) {
(params as any).step = stepFilter;
}
}
return pageCreditMpCustomer({
...params,
...orders,
page,
limit
});
};
const stepOptions = [
{ value: 0, text: '未受理', color: 'default' },
{ value: 1, text: '已受理', color: 'blue' },
{ value: 2, text: '材料提交', color: 'cyan' },
{ value: 3, text: '合同签订', color: 'purple' },
{ value: 4, text: '执行回款', color: 'orange' },
{ value: 5, text: '完结', color: 'green' }
] as const;
const getStepText = (step: unknown) => {
const value = typeof step === 'string' ? Number(step) : (step as number | undefined);
const match = stepOptions.find((d) => d.value === value);
return match?.text ?? '-';
};
const getStepColor = (step: unknown) => {
const value = typeof step === 'string' ? Number(step) : (step as number | undefined);
const match = stepOptions.find((d) => d.value === value);
return match?.color ?? 'default';
};
const isImageUrl = (url: string) => {
const cleanUrl = url.split('?')[0] ?? '';
return /\.(png|jpe?g|gif|webp|bmp|svg)$/i.test(cleanUrl);
};
const guessNameFromUrl = (url: string) => {
const cleanUrl = (url.split('?')[0] ?? '').trim();
const last = cleanUrl.split('/').pop() || '';
try {
return decodeURIComponent(last) || url;
} catch {
return last || url;
}
};
const tryParseJson = (value: string) => {
try {
return JSON.parse(value);
} catch {
return undefined;
}
};
const sanitizeFileUrl = (url: string, isImage: boolean) => {
return isImage ? url : (stripOssImageProcess(url) as string);
};
const filesCache = new Map<string, NormalizedFile[]>();
const normalizeFiles = (raw: unknown): NormalizedFile[] => {
if (!raw) return [];
if (Array.isArray(raw)) {
return raw
.map((item: any) => {
if (!item) return null;
if (typeof item === 'string') {
const url = sanitizeFileUrl(item, isImageUrl(item));
return {
url,
thumbnail: url,
name: guessNameFromUrl(url),
isImage: isImageUrl(url)
} satisfies NormalizedFile;
}
const rawUrl = item.url || item.path || item.href;
const url = typeof rawUrl === 'string' ? rawUrl : undefined;
if (!url) return null;
const thumbnail = typeof item.thumbnail === 'string' ? item.thumbnail : undefined;
const name = typeof item.name === 'string' ? item.name : guessNameFromUrl(url);
const isImage = typeof item.isImage === 'boolean' ? item.isImage : isImageUrl(url);
return {url: sanitizeFileUrl(url, isImage), thumbnail, name, isImage} satisfies NormalizedFile;
})
.filter(Boolean) as NormalizedFile[];
}
if (typeof raw === 'string') {
const text = raw.trim();
if (!text) return [];
const cached = filesCache.get(text);
if (cached) return cached;
// 兼容:后端返回 JSON 数组字符串(示例:"[{\"name\":\"...\",\"url\":\"...\"}]")
let parsed: any = tryParseJson(text);
if (typeof parsed === 'string') {
parsed = tryParseJson(parsed) ?? parsed;
}
if (Array.isArray(parsed)) {
const result = normalizeFiles(parsed);
if (filesCache.size > 500) filesCache.clear();
filesCache.set(text, result);
return result;
}
// 兜底:单个 url 或逗号分隔 url
const parts = text.includes(',') ? text.split(',') : [text];
const result = normalizeFiles(parts.map((p) => p.trim()).filter(Boolean));
if (filesCache.size > 500) filesCache.clear();
filesCache.set(text, result);
return result;
}
return [];
};
// 完整的列配置(包含所有字段)
const allColumns = ref<ColumnItem[]>([
{
title: 'ID',
dataIndex: 'id',
key: 'id',
width: 90,
},
{
title: '用户信息',
dataIndex: 'userInfo',
key: 'userInfo',
width: 240,
},
{
title: '拖欠方',
dataIndex: 'toUser',
key: 'toUser'
},
{
title: '拖欠金额',
dataIndex: 'price',
key: 'price',
width: 120,
align: 'center',
customRender: ({ text }) => '¥' + text
},
{
title: '拖欠年数',
dataIndex: 'years',
key: 'years',
align: 'center'
},
// {
// title: '链接',
// dataIndex: 'url',
// key: 'url',
// ellipsis: true
// },
// {
// title: '状态',
// dataIndex: 'statusTxt',
// key: 'statusTxt'
// },
// {
// title: '所在省份',
// dataIndex: 'province',
// key: 'province',
// ellipsis: true
// },
{
title: '所在城市',
dataIndex: 'city',
key: 'city',
align: 'center'
},
{
title: '步骤',
dataIndex: 'step',
key: 'step',
width: 120,
align: 'center',
filters: stepOptions.map((d) => ({ text: d.text, value: d.value }))
},
// {
// title: '所在辖区',
// dataIndex: 'region',
// key: 'region',
// ellipsis: true
// },
{
title: '附件',
dataIndex: 'files',
key: 'files',
align: 'center'
},
// {
// title: '是否有数据',
// dataIndex: 'hasData',
// key: 'hasData',
// width: 120
// },
// {
// title: '备注',
// dataIndex: 'comments',
// key: 'comments',
// ellipsis: true
// },
// {
// title: '是否推荐',
// dataIndex: 'recommend',
// key: 'recommend',
// width: 120
// },
{
title: '排序',
dataIndex: 'sortNumber',
key: 'sortNumber',
width: 120
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 120
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
width: 200,
align: 'center',
sorter: true,
ellipsis: true,
customRender: ({text}) => toDateString(text, 'yyyy-MM-dd HH:mm:ss')
},
{
title: '跟进人',
dataIndex: 'realName',
key: 'realName',
width: 120,
align: 'center'
},
{
title: '操作',
key: 'action',
width: 120,
fixed: 'right',
align: 'center',
hideInSetting: true
}
]);
// 默认显示的核心列最多5个主要字段
const defaultVisibleColumns = [
'id',
'userInfo',
'toUser',
'price',
'years',
'city',
'realName',
'step',
'files',
// 'status',
'createTime',
'action'
];
// 根据默认可见列过滤显示的列
const columns = computed(() => {
return allColumns.value.filter(col =>
defaultVisibleColumns.includes(col.dataIndex) || col.key === 'action'
);
});
/* 搜索 */
const reload = (where?: CreditMpCustomerParam) => {
if (where && Object.prototype.hasOwnProperty.call(where, 'keywords')) {
searchText.value = where.keywords ?? '';
}
if (where && Object.prototype.hasOwnProperty.call(where, 'step')) {
searchStep.value = where.step;
}
const targetWhere = where ?? {keywords: searchText.value || undefined, step: searchStep.value};
selection.value = [];
tableRef?.value?.reload({where: targetWhere});
};
/* 打开编辑弹窗 */
const openEdit = (row?: CreditMpCustomer) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开批量移动弹窗 */
const openMove = () => {
showMove.value = true;
};
/* 导出 */
const exportData = () => {
exportCreditData<CreditMpCustomer>({
filename: '小程序端客户列表',
includeCompanyName: false,
columns: [
{title: 'ID', dataIndex: 'id'},
{title: '拖欠方', dataIndex: 'toUser'},
{title: '拖欠金额', dataIndex: 'price'},
{title: '拖欠年数', dataIndex: 'years'},
{title: '链接', dataIndex: 'url'},
{title: '状态', dataIndex: 'statusTxt'},
{title: '所在省份', dataIndex: 'province'},
{title: '所在城市', dataIndex: 'city'},
{title: '所在辖区', dataIndex: 'region'},
{title: '步骤', dataIndex: 'step'},
{title: '文件路径', dataIndex: 'files'},
{title: '是否有数据', dataIndex: 'hasData'},
{title: '备注', dataIndex: 'comments'},
{title: '是否推荐', dataIndex: 'recommend'},
{title: '排序(数字越小越靠前)', dataIndex: 'sortNumber'},
{title: '状态, 0正常, 1冻结', dataIndex: 'status'},
{title: '是否删除, 0否, 1是', dataIndex: 'deleted'},
{title: '用户ID', dataIndex: 'userId'},
{title: '创建时间', dataIndex: 'createTime'},
{title: '修改时间', dataIndex: 'updateTime'}
],
fetchData: () =>
listCreditMpCustomer({
keywords: searchText.value || undefined,
step: searchStep.value
})
});
};
/* 删除单个 */
const remove = (row: CreditMpCustomer) => {
const hide = message.loading('请求中..', 0);
removeCreditMpCustomer(row.id)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
};
/* 批量删除 */
const removeBatch = () => {
if (!selection.value.length) {
message.error('请至少选择一条数据');
return;
}
Modal.confirm({
title: '提示',
content: '确定要删除选中的记录吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
const hide = message.loading('请求中..', 0);
removeBatchCreditMpCustomer(selection.value.map((d) => d.id))
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
/* 查询 */
const query = () => {
loading.value = true;
};
/* 自定义行属性 */
const customRow = (record: CreditMpCustomer) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
};
query();
</script>
<script lang="ts">
export default {
name: 'CreditMpCustomer'
};
</script>
<style lang="less" scoped>
.user-info {
display: flex;
align-items: center;
padding: 6px;
.user-details {
margin-left: 6px;
flex: 1;
.username {
margin: 0;
color: #333;
font-size: 16px;
font-weight: 500;
}
.user-phone {
margin: 0;
color: #666;
font-size: 14px;
}
}
}
</style>

View File

@@ -0,0 +1,589 @@
<template>
<a-page-header :title="getPageTitle()" @back="() => $router.go(-1)">
<a-card :bordered="false" :body-style="{ padding: '16px' }">
<ele-pro-table
ref="tableRef"
row-key="id"
:columns="columns"
:datasource="datasource"
:customRow="customRow"
v-model:selection="selection"
tool-class="ele-toolbar-form"
class="sys-org-table"
>
<template #toolbar>
<search
@search="reload"
:selection="selection"
@add="openEdit"
@remove="removeBatch"
@batchMove="openMove"
@exportData="exportData"
/>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'userInfo'">
<div class="user-info">
<a-avatar :size="42" :src="record.avatar">
<template v-if="!record.avatar" #icon>
<UserOutlined/>
</template>
</a-avatar>
<div class="user-details">
<h4 class="username">{{ record.nickname }}</h4>
<p class="user-phone">{{ record.phone }}</p>
</div>
</div>
</template>
<template v-if="column.key === 'image'">
<a-image :src="record.image" :width="50"/>
</template>
<template v-if="column.key === 'files'">
<a-space :size="8" style="flex-wrap: wrap">
<template v-for="(file, index) in normalizeFiles(record.files)" :key="`${file.url}-${index}`">
<a-image
v-if="file.isImage"
:src="file.thumbnail || file.url"
:width="50"
:preview="{ src: file.url }"
/>
<a
v-else
:href="sanitizeFileUrl(file.url, file.isImage)"
target="_blank"
rel="noopener noreferrer"
>
{{ file.name || `附件${index + 1}` }}
</a>
</template>
<span v-if="!normalizeFiles(record.files).length">-</span>
</a-space>
</template>
<template v-if="column.key === 'step'">
<a-tag :color="getStepColor(record.step)">
{{ getStepText(record.step) }}
</a-tag>
</template>
<template v-if="column.key === 'status'">
<a-tag v-if="record.status === 0" color="green">显示</a-tag>
<a-tag v-if="record.status === 1" color="red">隐藏</a-tag>
</template>
<template v-if="column.key === 'action'">
<a @click="openEdit(record)">修改</a>
<a-divider type="vertical"/>
<a-popconfirm
title="确定要删除此记录吗?"
@confirm="remove(record)"
>
<a class="ele-text-danger">删除</a>
</a-popconfirm>
</template>
</template>
</ele-pro-table>
</a-card>
<!-- 编辑弹窗 -->
<CreditMpCustomerEdit v-model:visible="showEdit" :data="current" @done="reload"/>
</a-page-header>
</template>
<script lang="ts" setup>
import {createVNode, ref, computed} from 'vue';
import {message, Modal} from 'ant-design-vue';
import {ExclamationCircleOutlined, UserOutlined} from '@ant-design/icons-vue';
import type {EleProTable} from 'ele-admin-pro';
import {toDateString} from 'ele-admin-pro';
import type {
DatasourceFunction,
ColumnItem
} from 'ele-admin-pro/es/ele-pro-table/types';
import Search from './../components/search.vue';
import {getPageTitle} from '@/utils/common';
import CreditMpCustomerEdit from './../components/creditMpCustomerEdit.vue';
import {
pageCreditMpCustomer,
listCreditMpCustomer,
removeCreditMpCustomer,
removeBatchCreditMpCustomer
} from '@/api/credit/creditMpCustomer';
import type {CreditMpCustomer, CreditMpCustomerParam} from '@/api/credit/creditMpCustomer/model';
import {exportCreditData} from '../../utils/export';
import { stripOssImageProcess } from '@/utils/common';
type NormalizedFile = {
name?: string;
url: string;
thumbnail?: string;
isImage: boolean;
};
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格选中数据
const selection = ref<CreditMpCustomer[]>([]);
// 当前编辑数据
const current = ref<CreditMpCustomer | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 是否显示批量移动弹窗
const showMove = ref(false);
// 加载状态
const loading = ref(true);
// 搜索关键词
const searchText = ref('');
const searchStep = ref<number | undefined>(undefined);
// 表格数据源
const datasource: DatasourceFunction = ({
page,
limit,
where = {},
orders,
filters
}) => {
const params: CreditMpCustomerParam = {...(where as CreditMpCustomerParam)};
if (filters) {
const filterStatus = (filters as any).status;
if (Array.isArray(filterStatus)) {
if (filterStatus.length) {
(params as any).status = filterStatus[0];
}
} else if (filterStatus !== undefined && filterStatus !== null) {
(params as any).status = filterStatus;
}
const stepFilter = (filters as any).step;
if (Array.isArray(stepFilter)) {
if (stepFilter.length) {
(params as any).step = stepFilter[0];
}
} else if (stepFilter !== undefined && stepFilter !== null) {
(params as any).step = stepFilter;
}
}
return pageCreditMpCustomer({
...params,
...orders,
page,
limit
});
};
const stepOptions = [
{ value: 0, text: '未受理', color: 'default' },
{ value: 1, text: '已受理', color: 'blue' },
{ value: 2, text: '材料提交', color: 'cyan' },
{ value: 3, text: '合同签订', color: 'purple' },
{ value: 4, text: '执行回款', color: 'orange' },
{ value: 5, text: '完结', color: 'green' }
] as const;
const getStepText = (step: unknown) => {
const value = typeof step === 'string' ? Number(step) : (step as number | undefined);
const match = stepOptions.find((d) => d.value === value);
return match?.text ?? '-';
};
const getStepColor = (step: unknown) => {
const value = typeof step === 'string' ? Number(step) : (step as number | undefined);
const match = stepOptions.find((d) => d.value === value);
return match?.color ?? 'default';
};
const isImageUrl = (url: string) => {
const cleanUrl = url.split('?')[0] ?? '';
return /\.(png|jpe?g|gif|webp|bmp|svg)$/i.test(cleanUrl);
};
const guessNameFromUrl = (url: string) => {
const cleanUrl = (url.split('?')[0] ?? '').trim();
const last = cleanUrl.split('/').pop() || '';
try {
return decodeURIComponent(last) || url;
} catch {
return last || url;
}
};
const tryParseJson = (value: string) => {
try {
return JSON.parse(value);
} catch {
return undefined;
}
};
const sanitizeFileUrl = (url: string, isImage: boolean) => {
return isImage ? url : (stripOssImageProcess(url) as string);
};
const filesCache = new Map<string, NormalizedFile[]>();
const normalizeFiles = (raw: unknown): NormalizedFile[] => {
if (!raw) return [];
if (Array.isArray(raw)) {
return raw
.map((item: any) => {
if (!item) return null;
if (typeof item === 'string') {
const url = sanitizeFileUrl(item, isImageUrl(item));
return {
url,
thumbnail: url,
name: guessNameFromUrl(url),
isImage: isImageUrl(url)
} satisfies NormalizedFile;
}
const rawUrl = item.url || item.path || item.href;
const url = typeof rawUrl === 'string' ? rawUrl : undefined;
if (!url) return null;
const thumbnail = typeof item.thumbnail === 'string' ? item.thumbnail : undefined;
const name = typeof item.name === 'string' ? item.name : guessNameFromUrl(url);
const isImage = typeof item.isImage === 'boolean' ? item.isImage : isImageUrl(url);
return {url: sanitizeFileUrl(url, isImage), thumbnail, name, isImage} satisfies NormalizedFile;
})
.filter(Boolean) as NormalizedFile[];
}
if (typeof raw === 'string') {
const text = raw.trim();
if (!text) return [];
const cached = filesCache.get(text);
if (cached) return cached;
// 兼容:后端返回 JSON 数组字符串(示例:"[{\"name\":\"...\",\"url\":\"...\"}]")
let parsed: any = tryParseJson(text);
if (typeof parsed === 'string') {
parsed = tryParseJson(parsed) ?? parsed;
}
if (Array.isArray(parsed)) {
const result = normalizeFiles(parsed);
if (filesCache.size > 500) filesCache.clear();
filesCache.set(text, result);
return result;
}
// 兜底:单个 url 或逗号分隔 url
const parts = text.includes(',') ? text.split(',') : [text];
const result = normalizeFiles(parts.map((p) => p.trim()).filter(Boolean));
if (filesCache.size > 500) filesCache.clear();
filesCache.set(text, result);
return result;
}
return [];
};
// 完整的列配置(包含所有字段)
const allColumns = ref<ColumnItem[]>([
{
title: 'ID',
dataIndex: 'id',
key: 'id',
width: 90,
},
{
title: '用户信息',
dataIndex: 'userInfo',
key: 'userInfo',
width: 240,
},
{
title: '拖欠方',
dataIndex: 'toUser',
key: 'toUser'
},
{
title: '拖欠金额',
dataIndex: 'price',
key: 'price',
width: 120,
align: 'center',
customRender: ({ text }) => '¥' + text
},
{
title: '拖欠年数',
dataIndex: 'years',
key: 'years',
align: 'center'
},
// {
// title: '链接',
// dataIndex: 'url',
// key: 'url',
// ellipsis: true
// },
// {
// title: '状态',
// dataIndex: 'statusTxt',
// key: 'statusTxt'
// },
// {
// title: '所在省份',
// dataIndex: 'province',
// key: 'province',
// ellipsis: true
// },
{
title: '所在城市',
dataIndex: 'city',
key: 'city',
align: 'center'
},
{
title: '步骤',
dataIndex: 'step',
key: 'step',
width: 120,
align: 'center',
filters: stepOptions.map((d) => ({ text: d.text, value: d.value }))
},
// {
// title: '所在辖区',
// dataIndex: 'region',
// key: 'region',
// ellipsis: true
// },
{
title: '附件',
dataIndex: 'files',
key: 'files',
align: 'center'
},
// {
// title: '是否有数据',
// dataIndex: 'hasData',
// key: 'hasData',
// width: 120
// },
// {
// title: '备注',
// dataIndex: 'comments',
// key: 'comments',
// ellipsis: true
// },
// {
// title: '是否推荐',
// dataIndex: 'recommend',
// key: 'recommend',
// width: 120
// },
{
title: '排序',
dataIndex: 'sortNumber',
key: 'sortNumber',
width: 120
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 120
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
width: 200,
align: 'center',
sorter: true,
ellipsis: true,
customRender: ({text}) => toDateString(text, 'yyyy-MM-dd HH:mm:ss')
},
{
title: '跟进人',
dataIndex: 'realName',
key: 'realName',
width: 120,
align: 'center'
},
{
title: '操作',
key: 'action',
width: 120,
fixed: 'right',
align: 'center',
hideInSetting: true
}
]);
// 默认显示的核心列最多5个主要字段
const defaultVisibleColumns = [
'id',
'userInfo',
'toUser',
'price',
'years',
'city',
'realName',
'step',
'files',
// 'status',
'createTime',
'action'
];
// 根据默认可见列过滤显示的列
const columns = computed(() => {
return allColumns.value.filter(col =>
defaultVisibleColumns.includes(col.dataIndex) || col.key === 'action'
);
});
/* 搜索 */
const reload = (where?: CreditMpCustomerParam) => {
if (where && Object.prototype.hasOwnProperty.call(where, 'keywords')) {
searchText.value = where.keywords ?? '';
}
if (where && Object.prototype.hasOwnProperty.call(where, 'step')) {
searchStep.value = where.step;
}
const targetWhere = where ?? {keywords: searchText.value || undefined, step: searchStep.value};
selection.value = [];
tableRef?.value?.reload({where: targetWhere});
};
/* 打开编辑弹窗 */
const openEdit = (row?: CreditMpCustomer) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开批量移动弹窗 */
const openMove = () => {
showMove.value = true;
};
/* 导出 */
const exportData = () => {
exportCreditData<CreditMpCustomer>({
filename: '小程序端客户列表',
includeCompanyName: false,
columns: [
{title: 'ID', dataIndex: 'id'},
{title: '拖欠方', dataIndex: 'toUser'},
{title: '拖欠金额', dataIndex: 'price'},
{title: '拖欠年数', dataIndex: 'years'},
{title: '链接', dataIndex: 'url'},
{title: '状态', dataIndex: 'statusTxt'},
{title: '所在省份', dataIndex: 'province'},
{title: '所在城市', dataIndex: 'city'},
{title: '所在辖区', dataIndex: 'region'},
{title: '步骤', dataIndex: 'step'},
{title: '文件路径', dataIndex: 'files'},
{title: '是否有数据', dataIndex: 'hasData'},
{title: '备注', dataIndex: 'comments'},
{title: '是否推荐', dataIndex: 'recommend'},
{title: '排序(数字越小越靠前)', dataIndex: 'sortNumber'},
{title: '状态, 0正常, 1冻结', dataIndex: 'status'},
{title: '是否删除, 0否, 1是', dataIndex: 'deleted'},
{title: '用户ID', dataIndex: 'userId'},
{title: '创建时间', dataIndex: 'createTime'},
{title: '修改时间', dataIndex: 'updateTime'}
],
fetchData: () =>
listCreditMpCustomer({
keywords: searchText.value || undefined,
step: searchStep.value
})
});
};
/* 删除单个 */
const remove = (row: CreditMpCustomer) => {
const hide = message.loading('请求中..', 0);
removeCreditMpCustomer(row.id)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
};
/* 批量删除 */
const removeBatch = () => {
if (!selection.value.length) {
message.error('请至少选择一条数据');
return;
}
Modal.confirm({
title: '提示',
content: '确定要删除选中的记录吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
const hide = message.loading('请求中..', 0);
removeBatchCreditMpCustomer(selection.value.map((d) => d.id))
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
/* 查询 */
const query = () => {
loading.value = true;
};
/* 自定义行属性 */
const customRow = (record: CreditMpCustomer) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
};
query();
</script>
<script lang="ts">
export default {
name: 'CreditMpCustomer'
};
</script>
<style lang="less" scoped>
.user-info {
display: flex;
align-items: center;
padding: 6px;
.user-details {
margin-left: 6px;
flex: 1;
.username {
margin: 0;
color: #333;
font-size: 16px;
font-weight: 500;
}
.user-phone {
margin: 0;
color: #666;
font-size: 14px;
}
}
}
</style>

View File

@@ -0,0 +1,589 @@
<template>
<a-page-header :title="getPageTitle()" @back="() => $router.go(-1)">
<a-card :bordered="false" :body-style="{ padding: '16px' }">
<ele-pro-table
ref="tableRef"
row-key="id"
:columns="columns"
:datasource="datasource"
:customRow="customRow"
v-model:selection="selection"
tool-class="ele-toolbar-form"
class="sys-org-table"
>
<template #toolbar>
<search
@search="reload"
:selection="selection"
@add="openEdit"
@remove="removeBatch"
@batchMove="openMove"
@exportData="exportData"
/>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'userInfo'">
<div class="user-info">
<a-avatar :size="42" :src="record.avatar">
<template v-if="!record.avatar" #icon>
<UserOutlined/>
</template>
</a-avatar>
<div class="user-details">
<h4 class="username">{{ record.nickname }}</h4>
<p class="user-phone">{{ record.phone }}</p>
</div>
</div>
</template>
<template v-if="column.key === 'image'">
<a-image :src="record.image" :width="50"/>
</template>
<template v-if="column.key === 'files'">
<a-space :size="8" style="flex-wrap: wrap">
<template v-for="(file, index) in normalizeFiles(record.files)" :key="`${file.url}-${index}`">
<a-image
v-if="file.isImage"
:src="file.thumbnail || file.url"
:width="50"
:preview="{ src: file.url }"
/>
<a
v-else
:href="sanitizeFileUrl(file.url, file.isImage)"
target="_blank"
rel="noopener noreferrer"
>
{{ file.name || `附件${index + 1}` }}
</a>
</template>
<span v-if="!normalizeFiles(record.files).length">-</span>
</a-space>
</template>
<template v-if="column.key === 'step'">
<a-tag :color="getStepColor(record.step)">
{{ getStepText(record.step) }}
</a-tag>
</template>
<template v-if="column.key === 'status'">
<a-tag v-if="record.status === 0" color="green">显示</a-tag>
<a-tag v-if="record.status === 1" color="red">隐藏</a-tag>
</template>
<template v-if="column.key === 'action'">
<a @click="openEdit(record)">修改</a>
<a-divider type="vertical"/>
<a-popconfirm
title="确定要删除此记录吗?"
@confirm="remove(record)"
>
<a class="ele-text-danger">删除</a>
</a-popconfirm>
</template>
</template>
</ele-pro-table>
</a-card>
<!-- 编辑弹窗 -->
<CreditMpCustomerEdit v-model:visible="showEdit" :data="current" @done="reload"/>
</a-page-header>
</template>
<script lang="ts" setup>
import {createVNode, ref, computed} from 'vue';
import {message, Modal} from 'ant-design-vue';
import {ExclamationCircleOutlined, UserOutlined} from '@ant-design/icons-vue';
import type {EleProTable} from 'ele-admin-pro';
import {toDateString} from 'ele-admin-pro';
import type {
DatasourceFunction,
ColumnItem
} from 'ele-admin-pro/es/ele-pro-table/types';
import Search from './../components/search.vue';
import {getPageTitle} from '@/utils/common';
import CreditMpCustomerEdit from './../components/creditMpCustomerEdit.vue';
import {
pageCreditMpCustomer,
listCreditMpCustomer,
removeCreditMpCustomer,
removeBatchCreditMpCustomer
} from '@/api/credit/creditMpCustomer';
import type {CreditMpCustomer, CreditMpCustomerParam} from '@/api/credit/creditMpCustomer/model';
import {exportCreditData} from '../../utils/export';
import { stripOssImageProcess } from '@/utils/common';
type NormalizedFile = {
name?: string;
url: string;
thumbnail?: string;
isImage: boolean;
};
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格选中数据
const selection = ref<CreditMpCustomer[]>([]);
// 当前编辑数据
const current = ref<CreditMpCustomer | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 是否显示批量移动弹窗
const showMove = ref(false);
// 加载状态
const loading = ref(true);
// 搜索关键词
const searchText = ref('');
const searchStep = ref<number | undefined>(undefined);
// 表格数据源
const datasource: DatasourceFunction = ({
page,
limit,
where = {},
orders,
filters
}) => {
const params: CreditMpCustomerParam = {...(where as CreditMpCustomerParam)};
if (filters) {
const filterStatus = (filters as any).status;
if (Array.isArray(filterStatus)) {
if (filterStatus.length) {
(params as any).status = filterStatus[0];
}
} else if (filterStatus !== undefined && filterStatus !== null) {
(params as any).status = filterStatus;
}
const stepFilter = (filters as any).step;
if (Array.isArray(stepFilter)) {
if (stepFilter.length) {
(params as any).step = stepFilter[0];
}
} else if (stepFilter !== undefined && stepFilter !== null) {
(params as any).step = stepFilter;
}
}
return pageCreditMpCustomer({
...params,
...orders,
page,
limit
});
};
const stepOptions = [
{ value: 0, text: '未受理', color: 'default' },
{ value: 1, text: '已受理', color: 'blue' },
{ value: 2, text: '材料提交', color: 'cyan' },
{ value: 3, text: '合同签订', color: 'purple' },
{ value: 4, text: '执行回款', color: 'orange' },
{ value: 5, text: '完结', color: 'green' }
] as const;
const getStepText = (step: unknown) => {
const value = typeof step === 'string' ? Number(step) : (step as number | undefined);
const match = stepOptions.find((d) => d.value === value);
return match?.text ?? '-';
};
const getStepColor = (step: unknown) => {
const value = typeof step === 'string' ? Number(step) : (step as number | undefined);
const match = stepOptions.find((d) => d.value === value);
return match?.color ?? 'default';
};
const isImageUrl = (url: string) => {
const cleanUrl = url.split('?')[0] ?? '';
return /\.(png|jpe?g|gif|webp|bmp|svg)$/i.test(cleanUrl);
};
const guessNameFromUrl = (url: string) => {
const cleanUrl = (url.split('?')[0] ?? '').trim();
const last = cleanUrl.split('/').pop() || '';
try {
return decodeURIComponent(last) || url;
} catch {
return last || url;
}
};
const tryParseJson = (value: string) => {
try {
return JSON.parse(value);
} catch {
return undefined;
}
};
const sanitizeFileUrl = (url: string, isImage: boolean) => {
return isImage ? url : (stripOssImageProcess(url) as string);
};
const filesCache = new Map<string, NormalizedFile[]>();
const normalizeFiles = (raw: unknown): NormalizedFile[] => {
if (!raw) return [];
if (Array.isArray(raw)) {
return raw
.map((item: any) => {
if (!item) return null;
if (typeof item === 'string') {
const url = sanitizeFileUrl(item, isImageUrl(item));
return {
url,
thumbnail: url,
name: guessNameFromUrl(url),
isImage: isImageUrl(url)
} satisfies NormalizedFile;
}
const rawUrl = item.url || item.path || item.href;
const url = typeof rawUrl === 'string' ? rawUrl : undefined;
if (!url) return null;
const thumbnail = typeof item.thumbnail === 'string' ? item.thumbnail : undefined;
const name = typeof item.name === 'string' ? item.name : guessNameFromUrl(url);
const isImage = typeof item.isImage === 'boolean' ? item.isImage : isImageUrl(url);
return {url: sanitizeFileUrl(url, isImage), thumbnail, name, isImage} satisfies NormalizedFile;
})
.filter(Boolean) as NormalizedFile[];
}
if (typeof raw === 'string') {
const text = raw.trim();
if (!text) return [];
const cached = filesCache.get(text);
if (cached) return cached;
// 兼容:后端返回 JSON 数组字符串(示例:"[{\"name\":\"...\",\"url\":\"...\"}]")
let parsed: any = tryParseJson(text);
if (typeof parsed === 'string') {
parsed = tryParseJson(parsed) ?? parsed;
}
if (Array.isArray(parsed)) {
const result = normalizeFiles(parsed);
if (filesCache.size > 500) filesCache.clear();
filesCache.set(text, result);
return result;
}
// 兜底:单个 url 或逗号分隔 url
const parts = text.includes(',') ? text.split(',') : [text];
const result = normalizeFiles(parts.map((p) => p.trim()).filter(Boolean));
if (filesCache.size > 500) filesCache.clear();
filesCache.set(text, result);
return result;
}
return [];
};
// 完整的列配置(包含所有字段)
const allColumns = ref<ColumnItem[]>([
{
title: 'ID',
dataIndex: 'id',
key: 'id',
width: 90,
},
{
title: '用户信息',
dataIndex: 'userInfo',
key: 'userInfo',
width: 240,
},
{
title: '拖欠方',
dataIndex: 'toUser',
key: 'toUser'
},
{
title: '拖欠金额',
dataIndex: 'price',
key: 'price',
width: 120,
align: 'center',
customRender: ({ text }) => '¥' + text
},
{
title: '拖欠年数',
dataIndex: 'years',
key: 'years',
align: 'center'
},
// {
// title: '链接',
// dataIndex: 'url',
// key: 'url',
// ellipsis: true
// },
// {
// title: '状态',
// dataIndex: 'statusTxt',
// key: 'statusTxt'
// },
// {
// title: '所在省份',
// dataIndex: 'province',
// key: 'province',
// ellipsis: true
// },
{
title: '所在城市',
dataIndex: 'city',
key: 'city',
align: 'center'
},
{
title: '步骤',
dataIndex: 'step',
key: 'step',
width: 120,
align: 'center',
filters: stepOptions.map((d) => ({ text: d.text, value: d.value }))
},
// {
// title: '所在辖区',
// dataIndex: 'region',
// key: 'region',
// ellipsis: true
// },
{
title: '附件',
dataIndex: 'files',
key: 'files',
align: 'center'
},
// {
// title: '是否有数据',
// dataIndex: 'hasData',
// key: 'hasData',
// width: 120
// },
// {
// title: '备注',
// dataIndex: 'comments',
// key: 'comments',
// ellipsis: true
// },
// {
// title: '是否推荐',
// dataIndex: 'recommend',
// key: 'recommend',
// width: 120
// },
{
title: '排序',
dataIndex: 'sortNumber',
key: 'sortNumber',
width: 120
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 120
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
width: 200,
align: 'center',
sorter: true,
ellipsis: true,
customRender: ({text}) => toDateString(text, 'yyyy-MM-dd HH:mm:ss')
},
{
title: '跟进人',
dataIndex: 'realName',
key: 'realName',
width: 120,
align: 'center'
},
{
title: '操作',
key: 'action',
width: 120,
fixed: 'right',
align: 'center',
hideInSetting: true
}
]);
// 默认显示的核心列最多5个主要字段
const defaultVisibleColumns = [
'id',
'userInfo',
'toUser',
'price',
'years',
'city',
'realName',
'step',
'files',
// 'status',
'createTime',
'action'
];
// 根据默认可见列过滤显示的列
const columns = computed(() => {
return allColumns.value.filter(col =>
defaultVisibleColumns.includes(col.dataIndex) || col.key === 'action'
);
});
/* 搜索 */
const reload = (where?: CreditMpCustomerParam) => {
if (where && Object.prototype.hasOwnProperty.call(where, 'keywords')) {
searchText.value = where.keywords ?? '';
}
if (where && Object.prototype.hasOwnProperty.call(where, 'step')) {
searchStep.value = where.step;
}
const targetWhere = where ?? {keywords: searchText.value || undefined, step: searchStep.value};
selection.value = [];
tableRef?.value?.reload({where: targetWhere});
};
/* 打开编辑弹窗 */
const openEdit = (row?: CreditMpCustomer) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开批量移动弹窗 */
const openMove = () => {
showMove.value = true;
};
/* 导出 */
const exportData = () => {
exportCreditData<CreditMpCustomer>({
filename: '小程序端客户列表',
includeCompanyName: false,
columns: [
{title: 'ID', dataIndex: 'id'},
{title: '拖欠方', dataIndex: 'toUser'},
{title: '拖欠金额', dataIndex: 'price'},
{title: '拖欠年数', dataIndex: 'years'},
{title: '链接', dataIndex: 'url'},
{title: '状态', dataIndex: 'statusTxt'},
{title: '所在省份', dataIndex: 'province'},
{title: '所在城市', dataIndex: 'city'},
{title: '所在辖区', dataIndex: 'region'},
{title: '步骤', dataIndex: 'step'},
{title: '文件路径', dataIndex: 'files'},
{title: '是否有数据', dataIndex: 'hasData'},
{title: '备注', dataIndex: 'comments'},
{title: '是否推荐', dataIndex: 'recommend'},
{title: '排序(数字越小越靠前)', dataIndex: 'sortNumber'},
{title: '状态, 0正常, 1冻结', dataIndex: 'status'},
{title: '是否删除, 0否, 1是', dataIndex: 'deleted'},
{title: '用户ID', dataIndex: 'userId'},
{title: '创建时间', dataIndex: 'createTime'},
{title: '修改时间', dataIndex: 'updateTime'}
],
fetchData: () =>
listCreditMpCustomer({
keywords: searchText.value || undefined,
step: searchStep.value
})
});
};
/* 删除单个 */
const remove = (row: CreditMpCustomer) => {
const hide = message.loading('请求中..', 0);
removeCreditMpCustomer(row.id)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
};
/* 批量删除 */
const removeBatch = () => {
if (!selection.value.length) {
message.error('请至少选择一条数据');
return;
}
Modal.confirm({
title: '提示',
content: '确定要删除选中的记录吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
const hide = message.loading('请求中..', 0);
removeBatchCreditMpCustomer(selection.value.map((d) => d.id))
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
/* 查询 */
const query = () => {
loading.value = true;
};
/* 自定义行属性 */
const customRow = (record: CreditMpCustomer) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
};
query();
</script>
<script lang="ts">
export default {
name: 'CreditMpCustomer'
};
</script>
<style lang="less" scoped>
.user-info {
display: flex;
align-items: center;
padding: 6px;
.user-details {
margin-left: 6px;
flex: 1;
.username {
margin: 0;
color: #333;
font-size: 16px;
font-weight: 500;
}
.user-phone {
margin: 0;
color: #666;
font-size: 14px;
}
}
}
</style>

View File

@@ -0,0 +1,589 @@
<template>
<a-page-header :title="getPageTitle()" @back="() => $router.go(-1)">
<a-card :bordered="false" :body-style="{ padding: '16px' }">
<ele-pro-table
ref="tableRef"
row-key="id"
:columns="columns"
:datasource="datasource"
:customRow="customRow"
v-model:selection="selection"
tool-class="ele-toolbar-form"
class="sys-org-table"
>
<template #toolbar>
<search
@search="reload"
:selection="selection"
@add="openEdit"
@remove="removeBatch"
@batchMove="openMove"
@exportData="exportData"
/>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'userInfo'">
<div class="user-info">
<a-avatar :size="42" :src="record.avatar">
<template v-if="!record.avatar" #icon>
<UserOutlined/>
</template>
</a-avatar>
<div class="user-details">
<h4 class="username">{{ record.nickname }}</h4>
<p class="user-phone">{{ record.phone }}</p>
</div>
</div>
</template>
<template v-if="column.key === 'image'">
<a-image :src="record.image" :width="50"/>
</template>
<template v-if="column.key === 'files'">
<a-space :size="8" style="flex-wrap: wrap">
<template v-for="(file, index) in normalizeFiles(record.files)" :key="`${file.url}-${index}`">
<a-image
v-if="file.isImage"
:src="file.thumbnail || file.url"
:width="50"
:preview="{ src: file.url }"
/>
<a
v-else
:href="sanitizeFileUrl(file.url, file.isImage)"
target="_blank"
rel="noopener noreferrer"
>
{{ file.name || `附件${index + 1}` }}
</a>
</template>
<span v-if="!normalizeFiles(record.files).length">-</span>
</a-space>
</template>
<template v-if="column.key === 'step'">
<a-tag :color="getStepColor(record.step)">
{{ getStepText(record.step) }}
</a-tag>
</template>
<template v-if="column.key === 'status'">
<a-tag v-if="record.status === 0" color="green">显示</a-tag>
<a-tag v-if="record.status === 1" color="red">隐藏</a-tag>
</template>
<template v-if="column.key === 'action'">
<a @click="openEdit(record)">修改</a>
<a-divider type="vertical"/>
<a-popconfirm
title="确定要删除此记录吗?"
@confirm="remove(record)"
>
<a class="ele-text-danger">删除</a>
</a-popconfirm>
</template>
</template>
</ele-pro-table>
</a-card>
<!-- 编辑弹窗 -->
<CreditMpCustomerEdit v-model:visible="showEdit" :data="current" @done="reload"/>
</a-page-header>
</template>
<script lang="ts" setup>
import {createVNode, ref, computed} from 'vue';
import {message, Modal} from 'ant-design-vue';
import {ExclamationCircleOutlined, UserOutlined} from '@ant-design/icons-vue';
import type {EleProTable} from 'ele-admin-pro';
import {toDateString} from 'ele-admin-pro';
import type {
DatasourceFunction,
ColumnItem
} from 'ele-admin-pro/es/ele-pro-table/types';
import Search from './../components/search.vue';
import {getPageTitle} from '@/utils/common';
import CreditMpCustomerEdit from './../components/creditMpCustomerEdit.vue';
import {
pageCreditMpCustomer,
listCreditMpCustomer,
removeCreditMpCustomer,
removeBatchCreditMpCustomer
} from '@/api/credit/creditMpCustomer';
import type {CreditMpCustomer, CreditMpCustomerParam} from '@/api/credit/creditMpCustomer/model';
import {exportCreditData} from '../../utils/export';
import { stripOssImageProcess } from '@/utils/common';
type NormalizedFile = {
name?: string;
url: string;
thumbnail?: string;
isImage: boolean;
};
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格选中数据
const selection = ref<CreditMpCustomer[]>([]);
// 当前编辑数据
const current = ref<CreditMpCustomer | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 是否显示批量移动弹窗
const showMove = ref(false);
// 加载状态
const loading = ref(true);
// 搜索关键词
const searchText = ref('');
const searchStep = ref<number | undefined>(undefined);
// 表格数据源
const datasource: DatasourceFunction = ({
page,
limit,
where = {},
orders,
filters
}) => {
const params: CreditMpCustomerParam = {...(where as CreditMpCustomerParam)};
if (filters) {
const filterStatus = (filters as any).status;
if (Array.isArray(filterStatus)) {
if (filterStatus.length) {
(params as any).status = filterStatus[0];
}
} else if (filterStatus !== undefined && filterStatus !== null) {
(params as any).status = filterStatus;
}
const stepFilter = (filters as any).step;
if (Array.isArray(stepFilter)) {
if (stepFilter.length) {
(params as any).step = stepFilter[0];
}
} else if (stepFilter !== undefined && stepFilter !== null) {
(params as any).step = stepFilter;
}
}
return pageCreditMpCustomer({
...params,
...orders,
page,
limit
});
};
const stepOptions = [
{ value: 0, text: '未受理', color: 'default' },
{ value: 1, text: '已受理', color: 'blue' },
{ value: 2, text: '材料提交', color: 'cyan' },
{ value: 3, text: '合同签订', color: 'purple' },
{ value: 4, text: '执行回款', color: 'orange' },
{ value: 5, text: '完结', color: 'green' }
] as const;
const getStepText = (step: unknown) => {
const value = typeof step === 'string' ? Number(step) : (step as number | undefined);
const match = stepOptions.find((d) => d.value === value);
return match?.text ?? '-';
};
const getStepColor = (step: unknown) => {
const value = typeof step === 'string' ? Number(step) : (step as number | undefined);
const match = stepOptions.find((d) => d.value === value);
return match?.color ?? 'default';
};
const isImageUrl = (url: string) => {
const cleanUrl = url.split('?')[0] ?? '';
return /\.(png|jpe?g|gif|webp|bmp|svg)$/i.test(cleanUrl);
};
const guessNameFromUrl = (url: string) => {
const cleanUrl = (url.split('?')[0] ?? '').trim();
const last = cleanUrl.split('/').pop() || '';
try {
return decodeURIComponent(last) || url;
} catch {
return last || url;
}
};
const tryParseJson = (value: string) => {
try {
return JSON.parse(value);
} catch {
return undefined;
}
};
const sanitizeFileUrl = (url: string, isImage: boolean) => {
return isImage ? url : (stripOssImageProcess(url) as string);
};
const filesCache = new Map<string, NormalizedFile[]>();
const normalizeFiles = (raw: unknown): NormalizedFile[] => {
if (!raw) return [];
if (Array.isArray(raw)) {
return raw
.map((item: any) => {
if (!item) return null;
if (typeof item === 'string') {
const url = sanitizeFileUrl(item, isImageUrl(item));
return {
url,
thumbnail: url,
name: guessNameFromUrl(url),
isImage: isImageUrl(url)
} satisfies NormalizedFile;
}
const rawUrl = item.url || item.path || item.href;
const url = typeof rawUrl === 'string' ? rawUrl : undefined;
if (!url) return null;
const thumbnail = typeof item.thumbnail === 'string' ? item.thumbnail : undefined;
const name = typeof item.name === 'string' ? item.name : guessNameFromUrl(url);
const isImage = typeof item.isImage === 'boolean' ? item.isImage : isImageUrl(url);
return {url: sanitizeFileUrl(url, isImage), thumbnail, name, isImage} satisfies NormalizedFile;
})
.filter(Boolean) as NormalizedFile[];
}
if (typeof raw === 'string') {
const text = raw.trim();
if (!text) return [];
const cached = filesCache.get(text);
if (cached) return cached;
// 兼容:后端返回 JSON 数组字符串(示例:"[{\"name\":\"...\",\"url\":\"...\"}]")
let parsed: any = tryParseJson(text);
if (typeof parsed === 'string') {
parsed = tryParseJson(parsed) ?? parsed;
}
if (Array.isArray(parsed)) {
const result = normalizeFiles(parsed);
if (filesCache.size > 500) filesCache.clear();
filesCache.set(text, result);
return result;
}
// 兜底:单个 url 或逗号分隔 url
const parts = text.includes(',') ? text.split(',') : [text];
const result = normalizeFiles(parts.map((p) => p.trim()).filter(Boolean));
if (filesCache.size > 500) filesCache.clear();
filesCache.set(text, result);
return result;
}
return [];
};
// 完整的列配置(包含所有字段)
const allColumns = ref<ColumnItem[]>([
{
title: 'ID',
dataIndex: 'id',
key: 'id',
width: 90,
},
{
title: '用户信息',
dataIndex: 'userInfo',
key: 'userInfo',
width: 240,
},
{
title: '拖欠方',
dataIndex: 'toUser',
key: 'toUser'
},
{
title: '拖欠金额',
dataIndex: 'price',
key: 'price',
width: 120,
align: 'center',
customRender: ({ text }) => '¥' + text
},
{
title: '拖欠年数',
dataIndex: 'years',
key: 'years',
align: 'center'
},
// {
// title: '链接',
// dataIndex: 'url',
// key: 'url',
// ellipsis: true
// },
// {
// title: '状态',
// dataIndex: 'statusTxt',
// key: 'statusTxt'
// },
// {
// title: '所在省份',
// dataIndex: 'province',
// key: 'province',
// ellipsis: true
// },
{
title: '所在城市',
dataIndex: 'city',
key: 'city',
align: 'center'
},
{
title: '步骤',
dataIndex: 'step',
key: 'step',
width: 120,
align: 'center',
filters: stepOptions.map((d) => ({ text: d.text, value: d.value }))
},
// {
// title: '所在辖区',
// dataIndex: 'region',
// key: 'region',
// ellipsis: true
// },
{
title: '附件',
dataIndex: 'files',
key: 'files',
align: 'center'
},
// {
// title: '是否有数据',
// dataIndex: 'hasData',
// key: 'hasData',
// width: 120
// },
// {
// title: '备注',
// dataIndex: 'comments',
// key: 'comments',
// ellipsis: true
// },
// {
// title: '是否推荐',
// dataIndex: 'recommend',
// key: 'recommend',
// width: 120
// },
{
title: '排序',
dataIndex: 'sortNumber',
key: 'sortNumber',
width: 120
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 120
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
width: 200,
align: 'center',
sorter: true,
ellipsis: true,
customRender: ({text}) => toDateString(text, 'yyyy-MM-dd HH:mm:ss')
},
{
title: '跟进人',
dataIndex: 'realName',
key: 'realName',
width: 120,
align: 'center'
},
{
title: '操作',
key: 'action',
width: 120,
fixed: 'right',
align: 'center',
hideInSetting: true
}
]);
// 默认显示的核心列最多5个主要字段
const defaultVisibleColumns = [
'id',
'userInfo',
'toUser',
'price',
'years',
'city',
'realName',
'step',
'files',
// 'status',
'createTime',
'action'
];
// 根据默认可见列过滤显示的列
const columns = computed(() => {
return allColumns.value.filter(col =>
defaultVisibleColumns.includes(col.dataIndex) || col.key === 'action'
);
});
/* 搜索 */
const reload = (where?: CreditMpCustomerParam) => {
if (where && Object.prototype.hasOwnProperty.call(where, 'keywords')) {
searchText.value = where.keywords ?? '';
}
if (where && Object.prototype.hasOwnProperty.call(where, 'step')) {
searchStep.value = where.step;
}
const targetWhere = where ?? {keywords: searchText.value || undefined, step: searchStep.value};
selection.value = [];
tableRef?.value?.reload({where: targetWhere});
};
/* 打开编辑弹窗 */
const openEdit = (row?: CreditMpCustomer) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开批量移动弹窗 */
const openMove = () => {
showMove.value = true;
};
/* 导出 */
const exportData = () => {
exportCreditData<CreditMpCustomer>({
filename: '小程序端客户列表',
includeCompanyName: false,
columns: [
{title: 'ID', dataIndex: 'id'},
{title: '拖欠方', dataIndex: 'toUser'},
{title: '拖欠金额', dataIndex: 'price'},
{title: '拖欠年数', dataIndex: 'years'},
{title: '链接', dataIndex: 'url'},
{title: '状态', dataIndex: 'statusTxt'},
{title: '所在省份', dataIndex: 'province'},
{title: '所在城市', dataIndex: 'city'},
{title: '所在辖区', dataIndex: 'region'},
{title: '步骤', dataIndex: 'step'},
{title: '文件路径', dataIndex: 'files'},
{title: '是否有数据', dataIndex: 'hasData'},
{title: '备注', dataIndex: 'comments'},
{title: '是否推荐', dataIndex: 'recommend'},
{title: '排序(数字越小越靠前)', dataIndex: 'sortNumber'},
{title: '状态, 0正常, 1冻结', dataIndex: 'status'},
{title: '是否删除, 0否, 1是', dataIndex: 'deleted'},
{title: '用户ID', dataIndex: 'userId'},
{title: '创建时间', dataIndex: 'createTime'},
{title: '修改时间', dataIndex: 'updateTime'}
],
fetchData: () =>
listCreditMpCustomer({
keywords: searchText.value || undefined,
step: searchStep.value
})
});
};
/* 删除单个 */
const remove = (row: CreditMpCustomer) => {
const hide = message.loading('请求中..', 0);
removeCreditMpCustomer(row.id)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
};
/* 批量删除 */
const removeBatch = () => {
if (!selection.value.length) {
message.error('请至少选择一条数据');
return;
}
Modal.confirm({
title: '提示',
content: '确定要删除选中的记录吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
const hide = message.loading('请求中..', 0);
removeBatchCreditMpCustomer(selection.value.map((d) => d.id))
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
/* 查询 */
const query = () => {
loading.value = true;
};
/* 自定义行属性 */
const customRow = (record: CreditMpCustomer) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
};
query();
</script>
<script lang="ts">
export default {
name: 'CreditMpCustomer'
};
</script>
<style lang="less" scoped>
.user-info {
display: flex;
align-items: center;
padding: 6px;
.user-details {
margin-left: 6px;
flex: 1;
.username {
margin: 0;
color: #333;
font-size: 16px;
font-weight: 500;
}
.user-phone {
margin: 0;
color: #666;
font-size: 14px;
}
}
}
</style>

View File

@@ -0,0 +1,589 @@
<template>
<a-page-header :title="getPageTitle()" @back="() => $router.go(-1)">
<a-card :bordered="false" :body-style="{ padding: '16px' }">
<ele-pro-table
ref="tableRef"
row-key="id"
:columns="columns"
:datasource="datasource"
:customRow="customRow"
v-model:selection="selection"
tool-class="ele-toolbar-form"
class="sys-org-table"
>
<template #toolbar>
<search
@search="reload"
:selection="selection"
@add="openEdit"
@remove="removeBatch"
@batchMove="openMove"
@exportData="exportData"
/>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'userInfo'">
<div class="user-info">
<a-avatar :size="42" :src="record.avatar">
<template v-if="!record.avatar" #icon>
<UserOutlined/>
</template>
</a-avatar>
<div class="user-details">
<h4 class="username">{{ record.nickname }}</h4>
<p class="user-phone">{{ record.phone }}</p>
</div>
</div>
</template>
<template v-if="column.key === 'image'">
<a-image :src="record.image" :width="50"/>
</template>
<template v-if="column.key === 'files'">
<a-space :size="8" style="flex-wrap: wrap">
<template v-for="(file, index) in normalizeFiles(record.files)" :key="`${file.url}-${index}`">
<a-image
v-if="file.isImage"
:src="file.thumbnail || file.url"
:width="50"
:preview="{ src: file.url }"
/>
<a
v-else
:href="sanitizeFileUrl(file.url, file.isImage)"
target="_blank"
rel="noopener noreferrer"
>
{{ file.name || `附件${index + 1}` }}
</a>
</template>
<span v-if="!normalizeFiles(record.files).length">-</span>
</a-space>
</template>
<template v-if="column.key === 'step'">
<a-tag :color="getStepColor(record.step)">
{{ getStepText(record.step) }}
</a-tag>
</template>
<template v-if="column.key === 'status'">
<a-tag v-if="record.status === 0" color="green">显示</a-tag>
<a-tag v-if="record.status === 1" color="red">隐藏</a-tag>
</template>
<template v-if="column.key === 'action'">
<a @click="openEdit(record)">修改</a>
<a-divider type="vertical"/>
<a-popconfirm
title="确定要删除此记录吗?"
@confirm="remove(record)"
>
<a class="ele-text-danger">删除</a>
</a-popconfirm>
</template>
</template>
</ele-pro-table>
</a-card>
<!-- 编辑弹窗 -->
<CreditMpCustomerEdit v-model:visible="showEdit" :data="current" @done="reload"/>
</a-page-header>
</template>
<script lang="ts" setup>
import {createVNode, ref, computed} from 'vue';
import {message, Modal} from 'ant-design-vue';
import {ExclamationCircleOutlined, UserOutlined} from '@ant-design/icons-vue';
import type {EleProTable} from 'ele-admin-pro';
import {toDateString} from 'ele-admin-pro';
import type {
DatasourceFunction,
ColumnItem
} from 'ele-admin-pro/es/ele-pro-table/types';
import Search from './../components/search.vue';
import {getPageTitle} from '@/utils/common';
import CreditMpCustomerEdit from './../components/creditMpCustomerEdit.vue';
import {
pageCreditMpCustomer,
listCreditMpCustomer,
removeCreditMpCustomer,
removeBatchCreditMpCustomer
} from '@/api/credit/creditMpCustomer';
import type {CreditMpCustomer, CreditMpCustomerParam} from '@/api/credit/creditMpCustomer/model';
import {exportCreditData} from '../../utils/export';
import { stripOssImageProcess } from '@/utils/common';
type NormalizedFile = {
name?: string;
url: string;
thumbnail?: string;
isImage: boolean;
};
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格选中数据
const selection = ref<CreditMpCustomer[]>([]);
// 当前编辑数据
const current = ref<CreditMpCustomer | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 是否显示批量移动弹窗
const showMove = ref(false);
// 加载状态
const loading = ref(true);
// 搜索关键词
const searchText = ref('');
const searchStep = ref<number | undefined>(undefined);
// 表格数据源
const datasource: DatasourceFunction = ({
page,
limit,
where = {},
orders,
filters
}) => {
const params: CreditMpCustomerParam = {...(where as CreditMpCustomerParam)};
if (filters) {
const filterStatus = (filters as any).status;
if (Array.isArray(filterStatus)) {
if (filterStatus.length) {
(params as any).status = filterStatus[0];
}
} else if (filterStatus !== undefined && filterStatus !== null) {
(params as any).status = filterStatus;
}
const stepFilter = (filters as any).step;
if (Array.isArray(stepFilter)) {
if (stepFilter.length) {
(params as any).step = stepFilter[0];
}
} else if (stepFilter !== undefined && stepFilter !== null) {
(params as any).step = stepFilter;
}
}
return pageCreditMpCustomer({
...params,
...orders,
page,
limit
});
};
const stepOptions = [
{ value: 0, text: '未受理', color: 'default' },
{ value: 1, text: '已受理', color: 'blue' },
{ value: 2, text: '材料提交', color: 'cyan' },
{ value: 3, text: '合同签订', color: 'purple' },
{ value: 4, text: '执行回款', color: 'orange' },
{ value: 5, text: '完结', color: 'green' }
] as const;
const getStepText = (step: unknown) => {
const value = typeof step === 'string' ? Number(step) : (step as number | undefined);
const match = stepOptions.find((d) => d.value === value);
return match?.text ?? '-';
};
const getStepColor = (step: unknown) => {
const value = typeof step === 'string' ? Number(step) : (step as number | undefined);
const match = stepOptions.find((d) => d.value === value);
return match?.color ?? 'default';
};
const isImageUrl = (url: string) => {
const cleanUrl = url.split('?')[0] ?? '';
return /\.(png|jpe?g|gif|webp|bmp|svg)$/i.test(cleanUrl);
};
const guessNameFromUrl = (url: string) => {
const cleanUrl = (url.split('?')[0] ?? '').trim();
const last = cleanUrl.split('/').pop() || '';
try {
return decodeURIComponent(last) || url;
} catch {
return last || url;
}
};
const tryParseJson = (value: string) => {
try {
return JSON.parse(value);
} catch {
return undefined;
}
};
const sanitizeFileUrl = (url: string, isImage: boolean) => {
return isImage ? url : (stripOssImageProcess(url) as string);
};
const filesCache = new Map<string, NormalizedFile[]>();
const normalizeFiles = (raw: unknown): NormalizedFile[] => {
if (!raw) return [];
if (Array.isArray(raw)) {
return raw
.map((item: any) => {
if (!item) return null;
if (typeof item === 'string') {
const url = sanitizeFileUrl(item, isImageUrl(item));
return {
url,
thumbnail: url,
name: guessNameFromUrl(url),
isImage: isImageUrl(url)
} satisfies NormalizedFile;
}
const rawUrl = item.url || item.path || item.href;
const url = typeof rawUrl === 'string' ? rawUrl : undefined;
if (!url) return null;
const thumbnail = typeof item.thumbnail === 'string' ? item.thumbnail : undefined;
const name = typeof item.name === 'string' ? item.name : guessNameFromUrl(url);
const isImage = typeof item.isImage === 'boolean' ? item.isImage : isImageUrl(url);
return {url: sanitizeFileUrl(url, isImage), thumbnail, name, isImage} satisfies NormalizedFile;
})
.filter(Boolean) as NormalizedFile[];
}
if (typeof raw === 'string') {
const text = raw.trim();
if (!text) return [];
const cached = filesCache.get(text);
if (cached) return cached;
// 兼容:后端返回 JSON 数组字符串(示例:"[{\"name\":\"...\",\"url\":\"...\"}]")
let parsed: any = tryParseJson(text);
if (typeof parsed === 'string') {
parsed = tryParseJson(parsed) ?? parsed;
}
if (Array.isArray(parsed)) {
const result = normalizeFiles(parsed);
if (filesCache.size > 500) filesCache.clear();
filesCache.set(text, result);
return result;
}
// 兜底:单个 url 或逗号分隔 url
const parts = text.includes(',') ? text.split(',') : [text];
const result = normalizeFiles(parts.map((p) => p.trim()).filter(Boolean));
if (filesCache.size > 500) filesCache.clear();
filesCache.set(text, result);
return result;
}
return [];
};
// 完整的列配置(包含所有字段)
const allColumns = ref<ColumnItem[]>([
{
title: 'ID',
dataIndex: 'id',
key: 'id',
width: 90,
},
{
title: '用户信息',
dataIndex: 'userInfo',
key: 'userInfo',
width: 240,
},
{
title: '拖欠方',
dataIndex: 'toUser',
key: 'toUser'
},
{
title: '拖欠金额',
dataIndex: 'price',
key: 'price',
width: 120,
align: 'center',
customRender: ({ text }) => '¥' + text
},
{
title: '拖欠年数',
dataIndex: 'years',
key: 'years',
align: 'center'
},
// {
// title: '链接',
// dataIndex: 'url',
// key: 'url',
// ellipsis: true
// },
// {
// title: '状态',
// dataIndex: 'statusTxt',
// key: 'statusTxt'
// },
// {
// title: '所在省份',
// dataIndex: 'province',
// key: 'province',
// ellipsis: true
// },
{
title: '所在城市',
dataIndex: 'city',
key: 'city',
align: 'center'
},
{
title: '步骤',
dataIndex: 'step',
key: 'step',
width: 120,
align: 'center',
filters: stepOptions.map((d) => ({ text: d.text, value: d.value }))
},
// {
// title: '所在辖区',
// dataIndex: 'region',
// key: 'region',
// ellipsis: true
// },
{
title: '附件',
dataIndex: 'files',
key: 'files',
align: 'center'
},
// {
// title: '是否有数据',
// dataIndex: 'hasData',
// key: 'hasData',
// width: 120
// },
// {
// title: '备注',
// dataIndex: 'comments',
// key: 'comments',
// ellipsis: true
// },
// {
// title: '是否推荐',
// dataIndex: 'recommend',
// key: 'recommend',
// width: 120
// },
{
title: '排序',
dataIndex: 'sortNumber',
key: 'sortNumber',
width: 120
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 120
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
width: 200,
align: 'center',
sorter: true,
ellipsis: true,
customRender: ({text}) => toDateString(text, 'yyyy-MM-dd HH:mm:ss')
},
{
title: '跟进人',
dataIndex: 'realName',
key: 'realName',
width: 120,
align: 'center'
},
{
title: '操作',
key: 'action',
width: 120,
fixed: 'right',
align: 'center',
hideInSetting: true
}
]);
// 默认显示的核心列最多5个主要字段
const defaultVisibleColumns = [
'id',
'userInfo',
'toUser',
'price',
'years',
'city',
'realName',
'step',
'files',
// 'status',
'createTime',
'action'
];
// 根据默认可见列过滤显示的列
const columns = computed(() => {
return allColumns.value.filter(col =>
defaultVisibleColumns.includes(col.dataIndex) || col.key === 'action'
);
});
/* 搜索 */
const reload = (where?: CreditMpCustomerParam) => {
if (where && Object.prototype.hasOwnProperty.call(where, 'keywords')) {
searchText.value = where.keywords ?? '';
}
if (where && Object.prototype.hasOwnProperty.call(where, 'step')) {
searchStep.value = where.step;
}
const targetWhere = where ?? {keywords: searchText.value || undefined, step: searchStep.value};
selection.value = [];
tableRef?.value?.reload({where: targetWhere});
};
/* 打开编辑弹窗 */
const openEdit = (row?: CreditMpCustomer) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开批量移动弹窗 */
const openMove = () => {
showMove.value = true;
};
/* 导出 */
const exportData = () => {
exportCreditData<CreditMpCustomer>({
filename: '小程序端客户列表',
includeCompanyName: false,
columns: [
{title: 'ID', dataIndex: 'id'},
{title: '拖欠方', dataIndex: 'toUser'},
{title: '拖欠金额', dataIndex: 'price'},
{title: '拖欠年数', dataIndex: 'years'},
{title: '链接', dataIndex: 'url'},
{title: '状态', dataIndex: 'statusTxt'},
{title: '所在省份', dataIndex: 'province'},
{title: '所在城市', dataIndex: 'city'},
{title: '所在辖区', dataIndex: 'region'},
{title: '步骤', dataIndex: 'step'},
{title: '文件路径', dataIndex: 'files'},
{title: '是否有数据', dataIndex: 'hasData'},
{title: '备注', dataIndex: 'comments'},
{title: '是否推荐', dataIndex: 'recommend'},
{title: '排序(数字越小越靠前)', dataIndex: 'sortNumber'},
{title: '状态, 0正常, 1冻结', dataIndex: 'status'},
{title: '是否删除, 0否, 1是', dataIndex: 'deleted'},
{title: '用户ID', dataIndex: 'userId'},
{title: '创建时间', dataIndex: 'createTime'},
{title: '修改时间', dataIndex: 'updateTime'}
],
fetchData: () =>
listCreditMpCustomer({
keywords: searchText.value || undefined,
step: searchStep.value
})
});
};
/* 删除单个 */
const remove = (row: CreditMpCustomer) => {
const hide = message.loading('请求中..', 0);
removeCreditMpCustomer(row.id)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
};
/* 批量删除 */
const removeBatch = () => {
if (!selection.value.length) {
message.error('请至少选择一条数据');
return;
}
Modal.confirm({
title: '提示',
content: '确定要删除选中的记录吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
const hide = message.loading('请求中..', 0);
removeBatchCreditMpCustomer(selection.value.map((d) => d.id))
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
/* 查询 */
const query = () => {
loading.value = true;
};
/* 自定义行属性 */
const customRow = (record: CreditMpCustomer) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
};
query();
</script>
<script lang="ts">
export default {
name: 'CreditMpCustomer'
};
</script>
<style lang="less" scoped>
.user-info {
display: flex;
align-items: center;
padding: 6px;
.user-details {
margin-left: 6px;
flex: 1;
.username {
margin: 0;
color: #333;
font-size: 16px;
font-weight: 500;
}
.user-phone {
margin: 0;
color: #666;
font-size: 14px;
}
}
}
</style>

View File

@@ -0,0 +1,589 @@
<template>
<a-page-header :title="getPageTitle()" @back="() => $router.go(-1)">
<a-card :bordered="false" :body-style="{ padding: '16px' }">
<ele-pro-table
ref="tableRef"
row-key="id"
:columns="columns"
:datasource="datasource"
:customRow="customRow"
v-model:selection="selection"
tool-class="ele-toolbar-form"
class="sys-org-table"
>
<template #toolbar>
<search
@search="reload"
:selection="selection"
@add="openEdit"
@remove="removeBatch"
@batchMove="openMove"
@exportData="exportData"
/>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'userInfo'">
<div class="user-info">
<a-avatar :size="42" :src="record.avatar">
<template v-if="!record.avatar" #icon>
<UserOutlined/>
</template>
</a-avatar>
<div class="user-details">
<h4 class="username">{{ record.nickname }}</h4>
<p class="user-phone">{{ record.phone }}</p>
</div>
</div>
</template>
<template v-if="column.key === 'image'">
<a-image :src="record.image" :width="50"/>
</template>
<template v-if="column.key === 'files'">
<a-space :size="8" style="flex-wrap: wrap">
<template v-for="(file, index) in normalizeFiles(record.files)" :key="`${file.url}-${index}`">
<a-image
v-if="file.isImage"
:src="file.thumbnail || file.url"
:width="50"
:preview="{ src: file.url }"
/>
<a
v-else
:href="sanitizeFileUrl(file.url, file.isImage)"
target="_blank"
rel="noopener noreferrer"
>
{{ file.name || `附件${index + 1}` }}
</a>
</template>
<span v-if="!normalizeFiles(record.files).length">-</span>
</a-space>
</template>
<template v-if="column.key === 'step'">
<a-tag :color="getStepColor(record.step)">
{{ getStepText(record.step) }}
</a-tag>
</template>
<template v-if="column.key === 'status'">
<a-tag v-if="record.status === 0" color="green">显示</a-tag>
<a-tag v-if="record.status === 1" color="red">隐藏</a-tag>
</template>
<template v-if="column.key === 'action'">
<a @click="openEdit(record)">修改</a>
<a-divider type="vertical"/>
<a-popconfirm
title="确定要删除此记录吗?"
@confirm="remove(record)"
>
<a class="ele-text-danger">删除</a>
</a-popconfirm>
</template>
</template>
</ele-pro-table>
</a-card>
<!-- 编辑弹窗 -->
<CreditMpCustomerEdit v-model:visible="showEdit" :data="current" @done="reload"/>
</a-page-header>
</template>
<script lang="ts" setup>
import {createVNode, ref, computed} from 'vue';
import {message, Modal} from 'ant-design-vue';
import {ExclamationCircleOutlined, UserOutlined} from '@ant-design/icons-vue';
import type {EleProTable} from 'ele-admin-pro';
import {toDateString} from 'ele-admin-pro';
import type {
DatasourceFunction,
ColumnItem
} from 'ele-admin-pro/es/ele-pro-table/types';
import Search from './../components/search.vue';
import {getPageTitle} from '@/utils/common';
import CreditMpCustomerEdit from './../components/creditMpCustomerEdit.vue';
import {
pageCreditMpCustomer,
listCreditMpCustomer,
removeCreditMpCustomer,
removeBatchCreditMpCustomer
} from '@/api/credit/creditMpCustomer';
import type {CreditMpCustomer, CreditMpCustomerParam} from '@/api/credit/creditMpCustomer/model';
import {exportCreditData} from '../../utils/export';
import { stripOssImageProcess } from '@/utils/common';
type NormalizedFile = {
name?: string;
url: string;
thumbnail?: string;
isImage: boolean;
};
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格选中数据
const selection = ref<CreditMpCustomer[]>([]);
// 当前编辑数据
const current = ref<CreditMpCustomer | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 是否显示批量移动弹窗
const showMove = ref(false);
// 加载状态
const loading = ref(true);
// 搜索关键词
const searchText = ref('');
const searchStep = ref<number | undefined>(undefined);
// 表格数据源
const datasource: DatasourceFunction = ({
page,
limit,
where = {},
orders,
filters
}) => {
const params: CreditMpCustomerParam = {...(where as CreditMpCustomerParam)};
if (filters) {
const filterStatus = (filters as any).status;
if (Array.isArray(filterStatus)) {
if (filterStatus.length) {
(params as any).status = filterStatus[0];
}
} else if (filterStatus !== undefined && filterStatus !== null) {
(params as any).status = filterStatus;
}
const stepFilter = (filters as any).step;
if (Array.isArray(stepFilter)) {
if (stepFilter.length) {
(params as any).step = stepFilter[0];
}
} else if (stepFilter !== undefined && stepFilter !== null) {
(params as any).step = stepFilter;
}
}
return pageCreditMpCustomer({
...params,
...orders,
page,
limit
});
};
const stepOptions = [
{ value: 0, text: '未受理', color: 'default' },
{ value: 1, text: '已受理', color: 'blue' },
{ value: 2, text: '材料提交', color: 'cyan' },
{ value: 3, text: '合同签订', color: 'purple' },
{ value: 4, text: '执行回款', color: 'orange' },
{ value: 5, text: '完结', color: 'green' }
] as const;
const getStepText = (step: unknown) => {
const value = typeof step === 'string' ? Number(step) : (step as number | undefined);
const match = stepOptions.find((d) => d.value === value);
return match?.text ?? '-';
};
const getStepColor = (step: unknown) => {
const value = typeof step === 'string' ? Number(step) : (step as number | undefined);
const match = stepOptions.find((d) => d.value === value);
return match?.color ?? 'default';
};
const isImageUrl = (url: string) => {
const cleanUrl = url.split('?')[0] ?? '';
return /\.(png|jpe?g|gif|webp|bmp|svg)$/i.test(cleanUrl);
};
const guessNameFromUrl = (url: string) => {
const cleanUrl = (url.split('?')[0] ?? '').trim();
const last = cleanUrl.split('/').pop() || '';
try {
return decodeURIComponent(last) || url;
} catch {
return last || url;
}
};
const tryParseJson = (value: string) => {
try {
return JSON.parse(value);
} catch {
return undefined;
}
};
const sanitizeFileUrl = (url: string, isImage: boolean) => {
return isImage ? url : (stripOssImageProcess(url) as string);
};
const filesCache = new Map<string, NormalizedFile[]>();
const normalizeFiles = (raw: unknown): NormalizedFile[] => {
if (!raw) return [];
if (Array.isArray(raw)) {
return raw
.map((item: any) => {
if (!item) return null;
if (typeof item === 'string') {
const url = sanitizeFileUrl(item, isImageUrl(item));
return {
url,
thumbnail: url,
name: guessNameFromUrl(url),
isImage: isImageUrl(url)
} satisfies NormalizedFile;
}
const rawUrl = item.url || item.path || item.href;
const url = typeof rawUrl === 'string' ? rawUrl : undefined;
if (!url) return null;
const thumbnail = typeof item.thumbnail === 'string' ? item.thumbnail : undefined;
const name = typeof item.name === 'string' ? item.name : guessNameFromUrl(url);
const isImage = typeof item.isImage === 'boolean' ? item.isImage : isImageUrl(url);
return {url: sanitizeFileUrl(url, isImage), thumbnail, name, isImage} satisfies NormalizedFile;
})
.filter(Boolean) as NormalizedFile[];
}
if (typeof raw === 'string') {
const text = raw.trim();
if (!text) return [];
const cached = filesCache.get(text);
if (cached) return cached;
// 兼容:后端返回 JSON 数组字符串(示例:"[{\"name\":\"...\",\"url\":\"...\"}]")
let parsed: any = tryParseJson(text);
if (typeof parsed === 'string') {
parsed = tryParseJson(parsed) ?? parsed;
}
if (Array.isArray(parsed)) {
const result = normalizeFiles(parsed);
if (filesCache.size > 500) filesCache.clear();
filesCache.set(text, result);
return result;
}
// 兜底:单个 url 或逗号分隔 url
const parts = text.includes(',') ? text.split(',') : [text];
const result = normalizeFiles(parts.map((p) => p.trim()).filter(Boolean));
if (filesCache.size > 500) filesCache.clear();
filesCache.set(text, result);
return result;
}
return [];
};
// 完整的列配置(包含所有字段)
const allColumns = ref<ColumnItem[]>([
{
title: 'ID',
dataIndex: 'id',
key: 'id',
width: 90,
},
{
title: '用户信息',
dataIndex: 'userInfo',
key: 'userInfo',
width: 240,
},
{
title: '拖欠方',
dataIndex: 'toUser',
key: 'toUser'
},
{
title: '拖欠金额',
dataIndex: 'price',
key: 'price',
width: 120,
align: 'center',
customRender: ({ text }) => '¥' + text
},
{
title: '拖欠年数',
dataIndex: 'years',
key: 'years',
align: 'center'
},
// {
// title: '链接',
// dataIndex: 'url',
// key: 'url',
// ellipsis: true
// },
// {
// title: '状态',
// dataIndex: 'statusTxt',
// key: 'statusTxt'
// },
// {
// title: '所在省份',
// dataIndex: 'province',
// key: 'province',
// ellipsis: true
// },
{
title: '所在城市',
dataIndex: 'city',
key: 'city',
align: 'center'
},
{
title: '步骤',
dataIndex: 'step',
key: 'step',
width: 120,
align: 'center',
filters: stepOptions.map((d) => ({ text: d.text, value: d.value }))
},
// {
// title: '所在辖区',
// dataIndex: 'region',
// key: 'region',
// ellipsis: true
// },
{
title: '附件',
dataIndex: 'files',
key: 'files',
align: 'center'
},
// {
// title: '是否有数据',
// dataIndex: 'hasData',
// key: 'hasData',
// width: 120
// },
// {
// title: '备注',
// dataIndex: 'comments',
// key: 'comments',
// ellipsis: true
// },
// {
// title: '是否推荐',
// dataIndex: 'recommend',
// key: 'recommend',
// width: 120
// },
{
title: '排序',
dataIndex: 'sortNumber',
key: 'sortNumber',
width: 120
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 120
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
width: 200,
align: 'center',
sorter: true,
ellipsis: true,
customRender: ({text}) => toDateString(text, 'yyyy-MM-dd HH:mm:ss')
},
{
title: '跟进人',
dataIndex: 'realName',
key: 'realName',
width: 120,
align: 'center'
},
{
title: '操作',
key: 'action',
width: 120,
fixed: 'right',
align: 'center',
hideInSetting: true
}
]);
// 默认显示的核心列最多5个主要字段
const defaultVisibleColumns = [
'id',
'userInfo',
'toUser',
'price',
'years',
'city',
'realName',
'step',
'files',
// 'status',
'createTime',
'action'
];
// 根据默认可见列过滤显示的列
const columns = computed(() => {
return allColumns.value.filter(col =>
defaultVisibleColumns.includes(col.dataIndex) || col.key === 'action'
);
});
/* 搜索 */
const reload = (where?: CreditMpCustomerParam) => {
if (where && Object.prototype.hasOwnProperty.call(where, 'keywords')) {
searchText.value = where.keywords ?? '';
}
if (where && Object.prototype.hasOwnProperty.call(where, 'step')) {
searchStep.value = where.step;
}
const targetWhere = where ?? {keywords: searchText.value || undefined, step: searchStep.value};
selection.value = [];
tableRef?.value?.reload({where: targetWhere});
};
/* 打开编辑弹窗 */
const openEdit = (row?: CreditMpCustomer) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开批量移动弹窗 */
const openMove = () => {
showMove.value = true;
};
/* 导出 */
const exportData = () => {
exportCreditData<CreditMpCustomer>({
filename: '小程序端客户列表',
includeCompanyName: false,
columns: [
{title: 'ID', dataIndex: 'id'},
{title: '拖欠方', dataIndex: 'toUser'},
{title: '拖欠金额', dataIndex: 'price'},
{title: '拖欠年数', dataIndex: 'years'},
{title: '链接', dataIndex: 'url'},
{title: '状态', dataIndex: 'statusTxt'},
{title: '所在省份', dataIndex: 'province'},
{title: '所在城市', dataIndex: 'city'},
{title: '所在辖区', dataIndex: 'region'},
{title: '步骤', dataIndex: 'step'},
{title: '文件路径', dataIndex: 'files'},
{title: '是否有数据', dataIndex: 'hasData'},
{title: '备注', dataIndex: 'comments'},
{title: '是否推荐', dataIndex: 'recommend'},
{title: '排序(数字越小越靠前)', dataIndex: 'sortNumber'},
{title: '状态, 0正常, 1冻结', dataIndex: 'status'},
{title: '是否删除, 0否, 1是', dataIndex: 'deleted'},
{title: '用户ID', dataIndex: 'userId'},
{title: '创建时间', dataIndex: 'createTime'},
{title: '修改时间', dataIndex: 'updateTime'}
],
fetchData: () =>
listCreditMpCustomer({
keywords: searchText.value || undefined,
step: searchStep.value
})
});
};
/* 删除单个 */
const remove = (row: CreditMpCustomer) => {
const hide = message.loading('请求中..', 0);
removeCreditMpCustomer(row.id)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
};
/* 批量删除 */
const removeBatch = () => {
if (!selection.value.length) {
message.error('请至少选择一条数据');
return;
}
Modal.confirm({
title: '提示',
content: '确定要删除选中的记录吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
const hide = message.loading('请求中..', 0);
removeBatchCreditMpCustomer(selection.value.map((d) => d.id))
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
/* 查询 */
const query = () => {
loading.value = true;
};
/* 自定义行属性 */
const customRow = (record: CreditMpCustomer) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
};
query();
</script>
<script lang="ts">
export default {
name: 'CreditMpCustomer'
};
</script>
<style lang="less" scoped>
.user-info {
display: flex;
align-items: center;
padding: 6px;
.user-details {
margin-left: 6px;
flex: 1;
.username {
margin: 0;
color: #333;
font-size: 16px;
font-weight: 500;
}
.user-phone {
margin: 0;
color: #666;
font-size: 14px;
}
}
}
</style>

View File

@@ -8,12 +8,18 @@
:datasource="datasource"
:customRow="customRow"
:scroll="{ x: 9000 }"
:selection="selection"
v-model:selection="selection"
tool-class="ele-toolbar-form"
class="sys-org-table"
>
<template #toolbar>
<a-space class="flex">
<a-button class="ele-btn-icon" @click="openImport">
<template #icon>
<CloudUploadOutlined />
</template>
<span>导入()</span>
</a-button>
<RefreshCompanyIdButton
module="credit-nearby-company"
@done="reload"
@@ -51,14 +57,14 @@
<template v-if="column.key === 'action'">
<a-space>
<a @click="openEdit(record)">详情</a>
<!-- <a @click="openEdit(record)">修改</a>-->
<!-- <a-divider type="vertical" />-->
<!-- <a-popconfirm-->
<!-- title="确定要删除此记录吗?"-->
<!-- @confirm="remove(record)"-->
<!-- >-->
<!-- <a class="ele-text-danger">删除</a>-->
<!-- </a-popconfirm>-->
<!-- <a @click="openEdit(record)">修改</a>-->
<!-- <a-divider type="vertical" />-->
<!-- <a-popconfirm-->
<!-- title="确定要删除此记录吗?"-->
<!-- @confirm="remove(record)"-->
<!-- >-->
<!-- <a class="ele-text-danger">删除</a>-->
<!-- </a-popconfirm>-->
</a-space>
</template>
</template>
@@ -90,6 +96,7 @@
import RefreshCompanyIdButton from '@/views/credit/components/RefreshCompanyIdButton.vue';
import { exportCreditData } from '../utils/export';
import { getPageTitle } from '@/utils/common';
import { CloudUploadOutlined } from '@ant-design/icons-vue';
import CreditNearbyCompanyEdit from './components/creditNearbyCompanyEdit.vue';
import CreditNearbyCompanyImport from './components/credit-nearby-company-import.vue';
import {
@@ -213,12 +220,6 @@
key: 'email',
ellipsis: true
},
{
title: '邮箱',
dataIndex: 'moreEmail',
key: 'moreEmail',
ellipsis: true
},
{
title: '所在国家',
dataIndex: 'country',
@@ -249,98 +250,82 @@
{
title: '注册号',
dataIndex: 'registrationNumber',
key: 'registrationNumber',
key: 'registrationNumber'
},
{
title: '组织机构代码',
dataIndex: 'organizationalCode',
key: 'organizationalCode',
key: 'organizationalCode'
},
{
title: '参保人数',
dataIndex: 'numberOfInsuredPersons',
key: 'numberOfInsuredPersons',
key: 'numberOfInsuredPersons'
},
{
title: '参保人数所属年报',
dataIndex: 'annualReport',
key: 'annualReport',
key: 'annualReport'
},
{
title: '企业(机构)类型',
dataIndex: 'institutionType',
key: 'institutionType',
key: 'institutionType'
},
{
title: '企业规模',
dataIndex: 'companySize',
key: 'companySize',
key: 'companySize'
},
{
title: '营业期限',
dataIndex: 'businessTerm',
key: 'businessTerm',
key: 'businessTerm'
},
{
title: '国标行业门类',
dataIndex: 'nationalStandardIndustryCategories',
key: 'nationalStandardIndustryCategories',
key: 'nationalStandardIndustryCategories'
},
{
title: '国标行业大类',
dataIndex: 'nationalStandardIndustryCategories2',
key: 'nationalStandardIndustryCategories2',
key: 'nationalStandardIndustryCategories2'
},
{
title: '国标行业中类',
dataIndex: 'nationalStandardIndustryCategories3',
key: 'nationalStandardIndustryCategories3',
key: 'nationalStandardIndustryCategories3'
},
{
title: '国标行业小类',
dataIndex: 'nationalStandardIndustryCategories4',
key: 'nationalStandardIndustryCategories4',
key: 'nationalStandardIndustryCategories4'
},
{
title: '曾用名',
dataIndex: 'formerName',
key: 'formerName',
key: 'formerName'
},
{
title: '英文名',
dataIndex: 'englishName',
key: 'englishName',
key: 'englishName'
},
{
title: '官网网址',
dataIndex: 'domain',
key: 'domain',
key: 'domain'
},
{
title: '通信地址',
dataIndex: 'mailingAddress',
key: 'mailingAddress',
key: 'mailingAddress'
},
{
title: '通信地址邮',
title: '通信地址邮',
dataIndex: 'mailingEmail',
key: 'mailingEmail',
key: 'mailingEmail'
},
{
title: '企业简介',
@@ -354,102 +339,53 @@
key: 'natureOfBusiness',
ellipsis: true
},
{
title: '电话',
dataIndex: 'tel',
key: 'tel',
},
{
title: '企查查行业门类',
dataIndex: 'nationalStandardIndustryCategories5',
key: 'nationalStandardIndustryCategories5',
key: 'nationalStandardIndustryCategories5'
},
{
title: '企查查行业大类',
dataIndex: 'nationalStandardIndustryCategories6',
key: 'nationalStandardIndustryCategories6',
key: 'nationalStandardIndustryCategories6'
},
{
title: '企查查行业中类',
dataIndex: 'nationalStandardIndustryCategories7',
key: 'nationalStandardIndustryCategories7',
key: 'nationalStandardIndustryCategories7'
},
{
title: '企查查行业小类',
dataIndex: 'nationalStandardIndustryCategories8',
key: 'nationalStandardIndustryCategories8',
key: 'nationalStandardIndustryCategories8'
},
// {
// title: '实缴资本',
// dataIndex: 'paidinCapital',
// key: 'paidinCapital'
// },
// {
// title: '登记机关',
// dataIndex: 'registrationAuthority',
// key: 'registrationAuthority'
// },
// {
// title: '纳税人资质',
// dataIndex: 'taxpayerQualification',
// key: 'taxpayerQualification'
// },
// {
// title: '最新年报年份',
// dataIndex: 'latestAnnualReportYear',
// key: 'latestAnnualReportYear'
// },
// {
// title: '最新年报营业收入',
// dataIndex: 'latestAnnualReportOnOperatingRevenue',
// key: 'latestAnnualReportOnOperatingRevenue'
// },
},
{
title: '类型',
dataIndex: 'type',
key: 'type',
width: 120
},
{
title: '实缴资本',
dataIndex: 'paidinCapital',
key: 'paidinCapital',
},
{
title: '登记机关',
dataIndex: 'registrationAuthority',
key: 'registrationAuthority',
},
{
title: '纳税人资质',
dataIndex: 'taxpayerQualification',
key: 'taxpayerQualification',
},
{
title: '最新年报年份',
dataIndex: 'latestAnnualReportYear',
key: 'latestAnnualReportYear',
},
{
title: '最新年报营业收入',
dataIndex: 'latestAnnualReportOnOperatingRevenue',
key: 'latestAnnualReportOnOperatingRevenue',
},
{
title: '企查分',
dataIndex: 'enterpriseScoreCheck',
key: 'enterpriseScoreCheck',
},
{
title: '信用等级',
dataIndex: 'creditRating',
key: 'creditRating',
},
{
title: '科创分',
dataIndex: 'cechnologyScore',
key: 'cechnologyScore',
},
{
title: '科创等级',
dataIndex: 'cechnologyLevel',
key: 'cechnologyLevel',
},
{
title: '是否小微企业',
dataIndex: 'smallEnterprise',
key: 'smallEnterprise',
},
// {
// title: '备注',
// dataIndex: 'comments',
@@ -489,6 +425,7 @@
align: 'center',
sorter: true,
ellipsis: true,
sorter: true,
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd HH:mm:ss')
},
{
@@ -559,10 +496,22 @@
{ title: '企业(机构)类型', dataIndex: 'institutionType' },
{ title: '企业规模', dataIndex: 'companySize' },
{ title: '营业期限', dataIndex: 'businessTerm' },
{ title: '国标行业门类', dataIndex: 'nationalStandardIndustryCategories' },
{ title: '国标行业类', dataIndex: 'nationalStandardIndustryCategories2' },
{ title: '国标行业中类', dataIndex: 'nationalStandardIndustryCategories3' },
{ title: '国标行业小类', dataIndex: 'nationalStandardIndustryCategories4' },
{
title: '国标行业类',
dataIndex: 'nationalStandardIndustryCategories'
},
{
title: '国标行业大类',
dataIndex: 'nationalStandardIndustryCategories2'
},
{
title: '国标行业中类',
dataIndex: 'nationalStandardIndustryCategories3'
},
{
title: '国标行业小类',
dataIndex: 'nationalStandardIndustryCategories4'
},
{ title: '曾用名', dataIndex: 'formerName' },
{ title: '英文名', dataIndex: 'englishName' },
{ title: '官网网址', dataIndex: 'domain' },
@@ -570,11 +519,22 @@
{ title: '通信地址邮箱', dataIndex: 'mailingEmail' },
{ title: '企业简介', dataIndex: 'companyProfile' },
{ title: '经营范围', dataIndex: 'natureOfBusiness' },
{ title: '电话', dataIndex: 'tel' },
{ title: '企查查行业门类', dataIndex: 'nationalStandardIndustryCategories5' },
{ title: '企查查行业大类', dataIndex: 'nationalStandardIndustryCategories6' },
{ title: '企查查行业中类', dataIndex: 'nationalStandardIndustryCategories7' },
{ title: '企查查行业小类', dataIndex: 'nationalStandardIndustryCategories8' },
{
title: '企查查行业门类',
dataIndex: 'nationalStandardIndustryCategories5'
},
{
title: '企查查行业大类',
dataIndex: 'nationalStandardIndustryCategories6'
},
{
title: '企查查行业中类',
dataIndex: 'nationalStandardIndustryCategories7'
},
{
title: '企查查行业小类',
dataIndex: 'nationalStandardIndustryCategories8'
},
{ title: '链接', dataIndex: 'url' },
{ title: '类型', dataIndex: 'type' },
{ title: '上级ID', dataIndex: 'parentId' },
@@ -582,7 +542,10 @@
{ title: '登记机关', dataIndex: 'registrationAuthority' },
{ title: '纳税人资质', dataIndex: 'taxpayerQualification' },
{ title: '最新年报年份', dataIndex: 'latestAnnualReportYear' },
{ title: '最新年报营业收入', dataIndex: 'latestAnnualReportOnOperatingRevenue' },
{
title: '最新年报营业收入',
dataIndex: 'latestAnnualReportOnOperatingRevenue'
},
{ title: '企查分', dataIndex: 'enterpriseScoreCheck' },
{ title: '信用等级', dataIndex: 'creditRating' },
{ title: '科创分', dataIndex: 'cechnologyScore' },

View File

@@ -212,8 +212,9 @@
align: 'center',
sorter: true,
ellipsis: true,
sorter: true,
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd HH:mm:ss')
},
}
// {
// title: '操作',
// key: 'action',

View File

@@ -13,6 +13,7 @@
<template #toolbar>
<search
@search="reload"
module="credit-project"
:selection="selection"
@add="openEdit"
@remove="removeBatch"
@@ -206,6 +207,7 @@
width: 180,
align: 'center',
ellipsis: true,
sorter: true,
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd HH:mm:ss')
},
{

View File

@@ -13,6 +13,7 @@
>
<template #toolbar>
<a-space class="flex">
<RefreshCompanyIdButton module="credit-risk-relation" @done="reload" />
<search
@search="reload"
:selection="selection"
@@ -73,6 +74,7 @@
ColumnItem
} from 'ele-admin-pro/es/ele-pro-table/types';
import Search from '@/views/credit/components/CreditSearchToolbar2.vue';
import RefreshCompanyIdButton from '@/views/credit/components/RefreshCompanyIdButton.vue';
import { exportCreditData } from '../utils/export';
import { getPageTitle } from '@/utils/common';
import CreditRiskRelationEdit from './components/creditRiskRelationEdit.vue';
@@ -186,6 +188,7 @@
width: 180,
align: 'center',
ellipsis: true,
sorter: true,
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd HH:mm:ss')
}
// {

View File

@@ -13,6 +13,7 @@
>
<template #toolbar>
<a-space class="flex">
<RefreshCompanyIdButton module="credit-supplier" @done="reload" />
<search
@search="reload"
:selection="selection"
@@ -73,6 +74,7 @@
ColumnItem
} from 'ele-admin-pro/es/ele-pro-table/types';
import Search from '@/views/credit/components/CreditSearchToolbar2.vue';
import RefreshCompanyIdButton from '@/views/credit/components/RefreshCompanyIdButton.vue';
import { exportCreditData } from '../utils/export';
import { getPageTitle } from '@/utils/common';
import CreditSupplierEdit from './components/creditSupplierEdit.vue';
@@ -155,7 +157,8 @@
{
title: '采购金额(万元)',
dataIndex: 'purchaseAmount',
key: 'purchaseAmount'
key: 'purchaseAmount',
sorter: true
},
{
title: '公开日期',
@@ -181,8 +184,9 @@
width: 180,
align: 'center',
ellipsis: true,
sorter: true,
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd HH:mm:ss')
},
}
// {
// title: '操作',
// key: 'action',

View File

@@ -214,7 +214,7 @@
sorter: true,
ellipsis: true,
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd HH:mm:ss')
},
}
// {
// title: '操作',
// key: 'action',

View File

@@ -1,18 +1,18 @@
<!-- 搜索表单 -->
<template>
<a-space :size="10" style="flex-wrap: wrap">
<!-- <a-button type="primary" class="ele-btn-icon" @click="add">-->
<!-- <template #icon>-->
<!-- <PlusOutlined />-->
<!-- </template>-->
<!-- <span>添加</span>-->
<!-- </a-button>-->
<!-- <a-button class="ele-btn-icon" @click="openImport">-->
<!-- <template #icon>-->
<!-- <CloudUploadOutlined />-->
<!-- </template>-->
<!-- <span>导入()</span>-->
<!-- </a-button>-->
<!-- <a-button type="primary" class="ele-btn-icon" @click="add">-->
<!-- <template #icon>-->
<!-- <PlusOutlined />-->
<!-- </template>-->
<!-- <span>添加</span>-->
<!-- </a-button>-->
<!-- <a-button class="ele-btn-icon" @click="openImport">-->
<!-- <template #icon>-->
<!-- <CloudUploadOutlined />-->
<!-- </template>-->
<!-- <span>导入()</span>-->
<!-- </a-button>-->
<a-button class="ele-btn-icon" @click="exportData">
<template #icon>
<CloudDownloadOutlined />

View File

@@ -14,6 +14,7 @@
>
<template #toolbar>
<a-space class="flex">
<RefreshCompanyIdButton module="credit-user" @done="reload" />
<search
@search="reload"
:selection="selection"
@@ -76,7 +77,9 @@
DatasourceFunction,
ColumnItem
} from 'ele-admin-pro/es/ele-pro-table/types';
import { toDateString } from 'ele-admin-pro';
import Search from './components/search.vue';
import RefreshCompanyIdButton from '@/views/credit/components/RefreshCompanyIdButton.vue';
import { getPageTitle } from '@/utils/common';
import CreditUserEdit from './components/creditUserEdit.vue';
import CreditUserImport from './components/credit-user-import.vue';
@@ -210,6 +213,11 @@
dataIndex: 'winningName',
key: 'winningName'
},
{
title: '中标金额',
dataIndex: 'winningPrice',
key: 'winningPrice'
},
{
title: '操作人',
dataIndex: 'realName',
@@ -217,11 +225,6 @@
width: 90,
align: 'center'
},
{
title: '中标金额',
dataIndex: 'winningPrice',
key: 'winningPrice'
},
// {
// title: '排序',
// dataIndex: 'sortNumber',
@@ -236,16 +239,16 @@
// width: 120,
// align: 'center'
// },
// {
// title: '创建时间',
// dataIndex: 'createTime',
// key: 'createTime',
// width: 200,
// align: 'center',
// sorter: true,
// ellipsis: true,
// customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd HH:mm:ss')
// },
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
width: 200,
align: 'center',
sorter: true,
ellipsis: true,
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd HH:mm:ss')
}
// {
// title: '操作',
// key: 'action',

View File

@@ -41,9 +41,11 @@
(e: 'update:visible', visible: boolean): void;
}>();
defineProps<{
const props = defineProps<{
// 是否打开弹窗
visible: boolean;
// 关联企业ID企业详情下导入时需要
companyId?: number;
}>();
// 导入请求状态
@@ -74,7 +76,7 @@
return false;
}
loading.value = true;
importCreditXgxfHistory(file)
importCreditXgxfHistory(file, props.companyId)
.then((msg) => {
loading.value = false;
message.success(msg);
@@ -93,4 +95,3 @@
emit('update:visible', value);
};
</script>

View File

@@ -15,6 +15,7 @@
<a-space class="flex">
<search
@search="reload"
module="credit-xgxf"
:selection="selection"
@add="openEdit"
@remove="removeBatch"
@@ -22,12 +23,6 @@
@importData="openImport"
@exportData="exportData"
/>
<a-button type="dashed" class="ele-btn-icon" @click="openImport2">
<template #icon>
<CloudUploadOutlined />
</template>
<span class="text-red-500">历史限制高消费</span>
</a-button>
</a-space>
</template>
<template #bodyCell="{ column, record }">
@@ -37,6 +32,9 @@
<template v-if="column.key === 'image'">
<a-image :src="record.image" :width="50" />
</template>
<template v-if="column.key === 'plaintiffAppellant'">
{{ record.plaintiffAppellant || record.dataType }}
</template>
<template v-if="column.key === 'status'">
<a-tag v-if="record.status === 0" color="green">显示</a-tag>
<a-tag v-if="record.status === 1" color="red">隐藏</a-tag>
@@ -61,15 +59,13 @@
<CreditXgxfEdit v-model:visible="showEdit" :data="current" @done="reload" />
<!-- 导入弹窗 -->
<CreditXgxfImport v-model:visible="showImport" @done="reload" />
<!-- 历史导入弹窗 -->
<CreditXgxfHistoryImport v-model:visible="showImport2" @done="reload" />
</a-page-header>
</template>
<script lang="ts" setup>
import { createVNode, ref } from 'vue';
import { message, Modal } from 'ant-design-vue';
import { ExclamationCircleOutlined, CloudUploadOutlined } from '@ant-design/icons-vue';
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
import type { EleProTable } from 'ele-admin-pro';
import { toDateString } from 'ele-admin-pro';
import type {
@@ -81,7 +77,6 @@
import { getPageTitle } from '@/utils/common';
import CreditXgxfEdit from './components/creditXgxfEdit.vue';
import CreditXgxfImport from './components/credit-xgxf-import.vue';
import CreditXgxfHistoryImport from './components/credit-xgxf-history-import.vue';
import {
pageCreditXgxf,
listCreditXgxf,
@@ -104,8 +99,6 @@
const showEdit = ref(false);
// 是否显示导入弹窗
const showImport = ref(false);
// 是否显示历史导入弹窗
const showImport2 = ref(false);
// 是否显示批量移动弹窗
const showMove = ref(false);
// 加载状态
@@ -151,13 +144,13 @@
},
{
title: '原告/上诉人',
dataIndex: 'plaintiffUser',
key: 'plaintiffUser'
dataIndex: 'plaintiffAppellant',
key: 'plaintiffAppellant'
},
{
title: '被告/被上诉人',
dataIndex: 'plaintiffUser',
key: 'plaintiffUser'
dataIndex: 'appellee',
key: 'appellee'
},
{
title: '其他当事人/第三人',
@@ -166,8 +159,8 @@
},
{
title: '发生时间',
dataIndex: 'occurrenceTime',
key: 'occurrenceTime'
dataIndex: 'releaseDate',
key: 'releaseDate'
},
{
title: '案号',
@@ -175,12 +168,12 @@
key: 'caseNumber'
},
{
title: '涉案金额(元)',
title: '涉案金额',
dataIndex: 'involvedAmount',
key: 'involvedAmount'
},
{
title: '执行法院',
title: '法院',
dataIndex: 'courtName',
key: 'courtName'
},
@@ -230,8 +223,9 @@
width: 180,
align: 'center',
ellipsis: true,
sorter: true,
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd HH:mm:ss')
},
}
// {
// title: '操作',
// key: 'action',
@@ -268,11 +262,6 @@
showImport.value = true;
};
/* 打开历史导入弹窗 */
const openImport2 = () => {
showImport2.value = true;
};
/* 导出 */
const exportData = () => {
exportCreditData<CreditXgxf>({

View File

@@ -0,0 +1,93 @@
<!-- 企业导入弹窗 -->
<template>
<ele-modal
:width="520"
:footer="null"
title="企业批量导入"
:visible="visible"
@update:visible="updateVisible"
>
<a-spin :spinning="loading">
<a-upload-dragger
accept=".xls,.xlsx"
:show-upload-list="false"
:customRequest="doUpload"
style="padding: 24px 0; margin-bottom: 16px"
>
<p class="ant-upload-drag-icon">
<cloud-upload-outlined />
</p>
<p class="ant-upload-hint">将文件拖到此处或点击上传</p>
</a-upload-dragger>
</a-spin>
<div class="ele-text-center">
<span>只能上传xlsxlsx文件</span>
<a :href="templateUrl" download="企业导入模板.xlsx"> 下载导入模板 </a>
</div>
</ele-modal>
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue';
import { message } from 'ant-design-vue/es';
import { CloudUploadOutlined } from '@ant-design/icons-vue';
import {importCustomer} from '@/api/credit/creditCompany';
import { API_BASE_URL } from '@/config/setting';
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
defineProps<{
// 是否打开弹窗
visible: boolean;
}>();
// 导入请求状态
const loading = ref(false);
// 模板下载地址,保持与当前接口域名一致
const templateUrl = computed(() => {
const base = (localStorage.getItem('ApiUrl') || API_BASE_URL || '').replace(
/\/$/,
''
);
return `${base}/credit/credit-company/import/template`;
});
/* 上传 */
const doUpload = ({ file }) => {
if (
![
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
].includes(file.type)
) {
message.error('只能选择 excel 文件');
return false;
}
if (file.size / 1024 / 1024 > 10) {
message.error('大小不能超过 10MB');
return false;
}
loading.value = true;
importCustomer(file)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
return false;
};
/* 更新 visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
</script>

Some files were not shown because too many files have changed in this diff Show More