From 5e3ab5b9013f98fa1957c9fca6d3c750b1579629 Mon Sep 17 00:00:00 2001 From: ouhaolan Date: Thu, 26 Mar 2026 08:56:03 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E6=94=B9bug=EF=BC=8C=E9=A6=96?= =?UTF-8?q?=E9=A1=B5=E9=97=A8=E5=BA=97=E6=B2=A1=E8=BF=87=E6=BB=A4=E5=88=A0?= =?UTF-8?q?=E9=99=A4=E7=9A=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.dev | 6 +- ruoyi-vue-pro | 2 +- src/api/car/renewalorder/index.ts | 21 ++ src/api/system/permission/index.ts | 2 + src/api/system/role/index.ts | 2 + src/config/axios/service.ts | 2 +- src/utils/constants.ts | 1 + src/utils/dict.ts | 1 + .../car/renewalorder/RenewalOrderForm.vue | 47 +++- src/views/car/renewalorder/index.vue | 206 ++++++++++++++++++ src/views/system/post/PostForm.vue | 13 +- .../system/role/RoleDataPermissionForm.vue | 97 +++++++-- src/views/system/role/RoleForm.vue | 12 +- src/views/tire/store/index.vue | 28 ++- 14 files changed, 397 insertions(+), 43 deletions(-) diff --git a/.env.dev b/.env.dev index 1eb217a..4956a85 100644 --- a/.env.dev +++ b/.env.dev @@ -4,14 +4,14 @@ NODE_ENV=production VITE_DEV=true # 请求路径 -VITE_BASE_URL='http://localhost:48080' -#VITE_BASE_URL='https://test.zmingzhikeji.cn' +#VITE_BASE_URL='http://localhost:48080' +VITE_BASE_URL='https://api.tuanbanlv.com' # 文件上传类型:server - 后端上传, client - 前端直连上传,仅支持S3服务 VITE_UPLOAD_TYPE=client # 上传路径 -VITE_UPLOAD_URL='https://www.zmingzhikeji.cn/admin-api/infra/file/upload' +VITE_UPLOAD_URL='https://api.tuanbanlv.com/admin-api/infra/file/upload' # 接口地址 VITE_API_URL=/admin-api diff --git a/ruoyi-vue-pro b/ruoyi-vue-pro index 1be18e2..39edcaa 160000 --- a/ruoyi-vue-pro +++ b/ruoyi-vue-pro @@ -1 +1 @@ -Subproject commit 1be18e2ff5c9b58e2de3a496798083f9fb727873 +Subproject commit 39edcaadf438f7aa3dd802f7c18b4f34778848ff diff --git a/src/api/car/renewalorder/index.ts b/src/api/car/renewalorder/index.ts index 762b1d4..d745ddb 100644 --- a/src/api/car/renewalorder/index.ts +++ b/src/api/car/renewalorder/index.ts @@ -95,5 +95,26 @@ export const RenewalOrderApi = { // 清空订单合同与客户签名(重新生成合同时先清空再扫码签名) clearContractAndSignature: async (id: number) => { return await request.post({ url: `/car/renewal-order/clear-contract-sign`, params: { id } }) + }, + + // 身份证识别(用于图片识别新增) + recognizeIdcard: async (file: File, idCardSide: 'front' | 'back' = 'front') => { + const formData = new FormData() + formData.append('file', file) + formData.append('idCardSide', idCardSide) + return await request.upload({ + url: '/car/renewal-order/recognize-idcard', + data: formData + }) + }, + + // 机动车销售发票识别(用于图片识别新增) + recognizeVehicleInvoice: async (file: File) => { + const formData = new FormData() + formData.append('file', file) + return await request.upload({ + url: '/car/renewal-order/recognize-vehicle-invoice', + data: formData + }) } } diff --git a/src/api/system/permission/index.ts b/src/api/system/permission/index.ts index b3c7696..2344790 100644 --- a/src/api/system/permission/index.ts +++ b/src/api/system/permission/index.ts @@ -14,6 +14,8 @@ export interface PermissionAssignRoleDataScopeReqVO { roleId: number dataScope: number dataScopeDeptIds: number[] + storeDataScope?: number // 门店数据范围(1:全部门店 2:本门店及以下 3:仅本人 4:指定门店) + storeDataScopeStoreIds?: number[] // 指定门店ID列表(storeDataScope=4 时使用) } // 查询角色拥有的菜单权限 diff --git a/src/api/system/role/index.ts b/src/api/system/role/index.ts index 3325dde..e0fc3cf 100644 --- a/src/api/system/role/index.ts +++ b/src/api/system/role/index.ts @@ -9,6 +9,8 @@ export interface RoleVO { type: number dataScope: number dataScopeDeptIds: number[] + storeDataScope?: number // 门店数据范围(1:全部门店 2:本门店及以下 3:仅本人 4:指定门店) + storeDataScopeStoreIds?: number[] // 指定门店ID列表(storeDataScope=4 时使用) createTime: Date } diff --git a/src/config/axios/service.ts b/src/config/axios/service.ts index aff542b..5a29a67 100644 --- a/src/config/axios/service.ts +++ b/src/config/axios/service.ts @@ -178,7 +178,7 @@ service.interceptors.response.use( } else { ElNotification.error({ title: msg }) } - return Promise.reject('error') + return Promise.reject(new Error(msg)) } else { return data } diff --git a/src/utils/constants.ts b/src/utils/constants.ts index cfa785b..e205f05 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -46,6 +46,7 @@ export const SystemDataScopeEnum = { DEPT_SELF: 5 // 仅本人数据权限 } + /** * 用户的社交平台的类型枚举 */ diff --git a/src/utils/dict.ts b/src/utils/dict.ts index c2a031d..550e299 100644 --- a/src/utils/dict.ts +++ b/src/utils/dict.ts @@ -252,5 +252,6 @@ export enum DICT_TYPE { CAR_RENEWAL_PRODUCT_TYPE= 'car_renewal_product_type', CAR_RENEWAL_PAY_METHOD= 'car_renewal_pay_method', CAR_RENEWAL_YEAR= 'car_renewal_year', // 续保生效年限 + CAR_STORE_DATA_SCOPE = 'car_store_data_scope' // 门店数据权限(1 全部门店 2 本门店及以下) } diff --git a/src/views/car/renewalorder/RenewalOrderForm.vue b/src/views/car/renewalorder/RenewalOrderForm.vue index 36f8a30..a496b82 100644 --- a/src/views/car/renewalorder/RenewalOrderForm.vue +++ b/src/views/car/renewalorder/RenewalOrderForm.vue @@ -109,7 +109,7 @@ - + @@ -182,6 +182,7 @@ placeholder="请选择门店" filterable style="width: 100%" + @change="handleStoreChange" > void) => { - if (!val || !Array.isArray(val) || val.length === 0) cb(new Error('请上传购置税凭证')) - else cb() - }, - trigger: 'change' - }], idCardFrontUrl: [{ required: true, message: '请上传身份证正面', trigger: 'change' }], idCardBackUrl: [{ required: true, message: '请上传身份证反面', trigger: 'change' }] }) @@ -562,6 +555,14 @@ const getStoreList = async () => { } } +/** 选择门店时,服务购买方自动填充为门店名称 */ +const handleStoreChange = (storeId: number) => { + if (storeId) { + const store = storeList.value.find(s => s.id === storeId) + if (store?.storeName) formData.value.serviceBuyer = store.storeName + } +} + /** 获取续保产品列表 */ const getProductList = async () => { try { @@ -605,7 +606,7 @@ const open = async (type: string, id?: number) => { resetForm() // 加载门店列表和产品列表 await Promise.all([getStoreList(), getProductList()]) - // 新增:默认选择门店第一项,车辆购买方默认为该门店名称(编辑不覆盖,均可修改) + // 新增:默认选择门店第一项,服务购买方默认为门店名称(编辑不覆盖,均可修改) if (!id && storeList.value.length > 0) { if (!formData.value.storeId) { formData.value.storeId = storeList.value[0].id @@ -748,7 +749,31 @@ const openCopy = async (id: number) => { formLoading.value = false } } -defineExpose({ open, openCopy }) // 提供 open / openCopy 方法,用于打开弹窗 +/** 打开弹窗并预填识别结果(身份证 / 车辆购置发票) */ +const openWithRecognizedData = async (recognizedData: Record) => { + await open('create') + if (!recognizedData) return + // 服务购买方 = 门店名字,不由识别结果填充;车辆购买方 = 身份证上的人名 + const idCardName = recognizedData.carBuyer || recognizedData.serviceBuyer || recognizedData.name + if (idCardName) formData.value.carBuyer = String(idCardName) + if (recognizedData.certNo) formData.value.certNo = String(recognizedData.certNo) + if (recognizedData.contactAddress) formData.value.contactAddress = String(recognizedData.contactAddress) + // 车辆购置发票识别字段 + if (recognizedData.vin) formData.value.vin = String(recognizedData.vin) + if (recognizedData.engineNo) formData.value.engineNo = String(recognizedData.engineNo) + if (recognizedData.factoryModel) formData.value.factoryModel = String(recognizedData.factoryModel) + if (recognizedData.invoiceDate) formData.value.invoiceDate = String(recognizedData.invoiceDate) + if (recognizedData.invoiceAmount != null) formData.value.invoiceAmount = Number(recognizedData.invoiceAmount) + if (recognizedData.carModel) formData.value.carModel = String(recognizedData.carModel) + // 图片 URL:身份证正反面、购车发票 + if (recognizedData.idCardFrontUrl) formData.value.idCardFrontUrl = String(recognizedData.idCardFrontUrl) + if (recognizedData.idCardBackUrl) formData.value.idCardBackUrl = String(recognizedData.idCardBackUrl) + if (recognizedData.carInvoiceUrls && Array.isArray(recognizedData.carInvoiceUrls) && recognizedData.carInvoiceUrls.length > 0) { + formData.value.carInvoiceUrls = recognizedData.carInvoiceUrls.map((u: any) => String(u)) + } +} + +defineExpose({ open, openCopy, openWithRecognizedData }) // 提供 open / openCopy / openWithRecognizedData 方法 /** 提交表单 */ const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调 diff --git a/src/views/car/renewalorder/index.vue b/src/views/car/renewalorder/index.vue index 9e81c57..8284ae2 100644 --- a/src/views/car/renewalorder/index.vue +++ b/src/views/car/renewalorder/index.vue @@ -83,6 +83,14 @@ > 新增 + + 图片识别新增 + + + + + +
+
+
身份证正面(人像面)*
+ + +
将身份证正面拖到此处,或点击选择
+
+
已选择:{{ recognizeSelectedFile.name }}
+
+
+
身份证反面(国徽面,可选)
+ + +
将身份证反面拖到此处,或点击选择
+
+
已选择:{{ recognizeBackSelectedFile.name }}
+
+
+
+ +
+ + +
将机动车销售发票照片拖到此处,或点击选择
+ +
+
+ 已选择:{{ invoiceSelectedFile.name }} +
+
+
+
+ +
+ { formRef.value.open(type, id) } +/** 图片识别新增 */ +const recognizeDialogVisible = ref(false) +const recognizeActiveTab = ref<'idcard' | 'invoice'>('idcard') +const recognizeUploadRef = ref() +const recognizeSelectedFile = ref(null) +const recognizeBackUploadRef = ref() +const recognizeBackSelectedFile = ref(null) +const recognizeLoading = ref(false) +const invoiceUploadRef = ref() +const invoiceSelectedFile = ref(null) +const invoiceRecognizeLoading = ref(false) + +/** 提取识别/上传失败原因,用于提示用户 */ +const getRecognizeErrorMessage = (e: any, fallback: string): string => { + return e?.response?.data?.msg ?? e?.message ?? e?.msg ?? (typeof e === 'string' ? e : fallback) +} + +/** 上传文件获取 URL */ +const uploadFileGetUrl = async (file: File): Promise => { + const res = await FileApi.updateFile({ file }) as any + return res?.data ?? res?.url ?? '' +} + +const openRecognizeDialog = () => { + recognizeActiveTab.value = 'idcard' + recognizeSelectedFile.value = null + recognizeBackSelectedFile.value = null + invoiceSelectedFile.value = null + recognizeUploadRef.value?.clearFiles() + recognizeBackUploadRef.value?.clearFiles() + invoiceUploadRef.value?.clearFiles() + recognizeDialogVisible.value = true +} +const handleRecognizeFileChange = (uploadFile: { raw?: File }) => { + recognizeSelectedFile.value = uploadFile?.raw || null +} +const handleRecognizeBackFileChange = (uploadFile: { raw?: File }) => { + recognizeBackSelectedFile.value = uploadFile?.raw || null +} +const handleInvoiceFileChange = (uploadFile: { raw?: File }) => { + invoiceSelectedFile.value = uploadFile?.raw || null +} +const handleRecognizeSubmit = async () => { + if (!recognizeSelectedFile.value) return + recognizeLoading.value = true + try { + const res = await RenewalOrderApi.recognizeIdcard(recognizeSelectedFile.value, 'front') as any + const data = res?.data ?? res + if (!data || typeof data !== 'object') { + const errMsg = res?.msg || '识别失败,未获取到有效数据,请重新上传' + message.error(errMsg) + return + } + // 上传身份证正反面获取 URL + try { + const idCardFrontUrl = await uploadFileGetUrl(recognizeSelectedFile.value) + if (idCardFrontUrl) data.idCardFrontUrl = idCardFrontUrl + if (recognizeBackSelectedFile.value) { + const idCardBackUrl = await uploadFileGetUrl(recognizeBackSelectedFile.value) + if (idCardBackUrl) data.idCardBackUrl = idCardBackUrl + } + } catch (uploadErr: any) { + const errMsg = getRecognizeErrorMessage(uploadErr, '图片上传失败,请重试') + message.error(`图片上传失败:${errMsg}`) + return + } + recognizeDialogVisible.value = false + formRef.value.openWithRecognizedData(data) + message.success('识别成功,请补充其他信息后保存') + } catch (e: any) { + const errMsg = getRecognizeErrorMessage(e, '识别失败,请重新上传正确的身份证照片') + message.error(errMsg.includes('请重新上传') ? errMsg : `${errMsg},请重新上传`) + } finally { + recognizeLoading.value = false + } +} +const handleInvoiceRecognizeSubmit = async () => { + if (!invoiceSelectedFile.value) return + invoiceRecognizeLoading.value = true + try { + const res = await RenewalOrderApi.recognizeVehicleInvoice(invoiceSelectedFile.value) as any + const data = res?.data ?? res + if (!data || typeof data !== 'object') { + const errMsg = res?.msg || '识别失败,未获取到有效数据,请重新上传' + message.error(errMsg) + return + } + // 上传发票图片获取 URL,初始化到购车发票 + try { + const invoiceUrl = await uploadFileGetUrl(invoiceSelectedFile.value) + if (invoiceUrl) data.carInvoiceUrls = [invoiceUrl] + } catch (uploadErr: any) { + const errMsg = getRecognizeErrorMessage(uploadErr, '图片上传失败,请重试') + message.error(`图片上传失败:${errMsg}`) + return + } + recognizeDialogVisible.value = false + formRef.value.openWithRecognizedData(data) + message.success('识别成功,请补充其他信息后保存') + } catch (e: any) { + const errMsg = getRecognizeErrorMessage(e, '识别失败,请重新上传正确的机动车销售发票照片') + message.error(errMsg.includes('请重新上传') ? errMsg : `${errMsg},请重新上传`) + } finally { + invoiceRecognizeLoading.value = false + } +} + /** 复制创建订单:基于原订单预填表单,进入新增模式 */ const handleCopy = (id: number) => { formRef.value.openCopy(id) diff --git a/src/views/system/post/PostForm.vue b/src/views/system/post/PostForm.vue index 1894e0c..1f95ffa 100644 --- a/src/views/system/post/PostForm.vue +++ b/src/views/system/post/PostForm.vue @@ -14,7 +14,13 @@ - + @@ -61,6 +67,7 @@ const formData = ref({ const formRules = reactive({ name: [{ required: true, message: '岗位标题不能为空', trigger: 'blur' }], code: [{ required: true, message: '岗位编码不能为空', trigger: 'change' }], + sort: [{ required: true, message: '岗位顺序不能为空', trigger: 'change' }], status: [{ required: true, message: '岗位状态不能为空', trigger: 'change' }], remark: [{ required: false, message: '岗位内容不能为空', trigger: 'blur' }] }) @@ -116,10 +123,10 @@ const resetForm = () => { id: undefined, name: '', code: '', - sort: undefined, + sort: 0, status: CommonStatusEnum.ENABLE, remark: '' - } as any + } formRef.value?.resetFields() } diff --git a/src/views/system/role/RoleDataPermissionForm.vue b/src/views/system/role/RoleDataPermissionForm.vue index 476a623..668fd7a 100644 --- a/src/views/system/role/RoleDataPermissionForm.vue +++ b/src/views/system/role/RoleDataPermissionForm.vue @@ -7,7 +7,7 @@ {{ formData.code }} - + + + + + +
全部门店/指定门店:查看全部或所选门店;本门店及以下:仅关联门店;仅本人:仅本人创建的数据
+
+ + + + + + ([]) // 门店列表(用于指定门店) const deptOptions = ref([]) // 部门树形结构 const deptExpand = ref(true) // 展开/折叠 const treeRef = ref() // 菜单树组件 Ref @@ -95,18 +130,36 @@ const checkStrictly = ref(true) // 是否严格模式,即父子不关联 const open = async (row: RoleApi.RoleVO) => { dialogVisible.value = true resetForm() - // 加载 Dept 列表。注意,必须放在前面,不然下面 setChecked 没数据节点 - deptOptions.value = handleTree(await DeptApi.getSimpleDeptList()) - // 设置数据 - formData.id = row.id - formData.name = row.name - formData.code = row.code - formData.dataScope = row.dataScope - await nextTick() - // 需要在 DOM 渲染完成后,再设置选中状态 - row.dataScopeDeptIds?.forEach((deptId: number): void => { - treeRef.value.setChecked(deptId, true, false) - }) + formLoading.value = true + try { + // 加载 Dept 列表。注意,必须放在前面,不然下面 setChecked 没数据节点 + deptOptions.value = handleTree(await DeptApi.getSimpleDeptList()) + // 加载门店列表(指定门店时使用) + const storeList = await StoreApi.findByProductType() + storeOptions.value = Array.isArray(storeList) ? storeList : (storeList?.data ?? storeList?.list ?? []) + // 从接口拉取最新角色数据,确保 storeDataScope 等字段正确回显 + const role = await RoleApi.getRole(row.id) + formData.id = role.id + formData.name = role.name + formData.code = role.code + formData.dataScope = role.dataScope + const ADMIN_ROLES = ['super_admin', 'tenant_admin', 'crm_admin'] + formData.storeDataScope = role.storeDataScope ?? (ADMIN_ROLES.includes(role.code ?? '') ? 1 : undefined) + formData.storeDataScopeStoreIds = role.storeDataScopeStoreIds ? Array.from(role.storeDataScopeStoreIds) : [] + await nextTick() + const deptIds = role.dataScopeDeptIds ? Array.from(role.dataScopeDeptIds) : [] + deptIds.forEach((deptId: number): void => { + treeRef.value?.setChecked(deptId, true, false) + }) + // 设置指定门店勾选 + if (formData.storeDataScope === 4 && formData.storeDataScopeStoreIds?.length) { + storeTreeRef.value?.setCheckedKeys(formData.storeDataScopeStoreIds) + } else { + storeTreeRef.value?.setCheckedKeys([]) + } + } finally { + formLoading.value = false + } } defineExpose({ open }) // 提供 open 方法,用于打开弹窗 @@ -121,7 +174,12 @@ const submitForm = async () => { dataScopeDeptIds: formData.dataScope !== SystemDataScopeEnum.DEPT_CUSTOM ? [] - : treeRef.value.getCheckedKeys(false) + : treeRef.value.getCheckedKeys(false), + storeDataScope: formData.storeDataScope, + storeDataScopeStoreIds: + formData.storeDataScope === 4 + ? (storeTreeRef.value?.getCheckedKeys(false) ?? formData.storeDataScopeStoreIds ?? []) + : [] } await PermissionApi.assignRoleDataScope(data) message.success(t('common.updateSuccess')) @@ -140,14 +198,17 @@ const resetForm = () => { deptExpand.value = true checkStrictly.value = true // 重置表单 - formData.value = { + Object.assign(formData, { id: undefined, name: '', code: '', dataScope: undefined, - dataScopeDeptIds: [] - } + dataScopeDeptIds: [], + storeDataScope: undefined, + storeDataScopeStoreIds: [] + }) treeRef.value?.setCheckedNodes([]) + storeTreeRef.value?.setCheckedKeys([]) formRef.value?.resetFields() } diff --git a/src/views/system/role/RoleForm.vue b/src/views/system/role/RoleForm.vue index 161b757..16910f2 100644 --- a/src/views/system/role/RoleForm.vue +++ b/src/views/system/role/RoleForm.vue @@ -14,7 +14,13 @@ - + @@ -54,7 +60,7 @@ const formData = ref({ id: undefined, name: '', code: '', - sort: undefined, + sort: 0, status: CommonStatusEnum.ENABLE, remark: '' }) @@ -90,7 +96,7 @@ const resetForm = () => { id: undefined, name: '', code: '', - sort: undefined, + sort: 0, status: CommonStatusEnum.ENABLE, remark: '' } diff --git a/src/views/tire/store/index.vue b/src/views/tire/store/index.vue index 28cba3a..f6d17e7 100644 --- a/src/views/tire/store/index.vue +++ b/src/views/tire/store/index.vue @@ -109,6 +109,26 @@ :formatter="dateFormatter" width="180px" /> + + + { } /** 删除按钮操作 */ -const handleDelete = async (id: number) => { +const handleDelete = async (row: StoreVO) => { try { // 删除的二次确认 - await message.delConfirm() + await message.delConfirm( + row?.storeName ? `是否确认删除门店「${row.storeName}」?` : '是否确认删除该门店?' + ) // 发起删除 - await StoreApi.deleteStore(id) + await StoreApi.deleteStore(row.id) message.success(t('common.delSuccess')) // 刷新列表 await getList()