This commit is contained in:
2026-03-02 09:25:09 +08:00
parent d8ae21ced4
commit 74f7b8f602
16 changed files with 832 additions and 127 deletions

4
env/.env vendored
View File

@@ -10,8 +10,8 @@ VITE_WX_APPID = 'wxa023ce905b13a4de'
VITE_APP_PUBLIC_BASE=/
# 后台请求地址localhost
VITE_SERVER_BASEURL = 'http://1.14.158.154:48080/admin-api'
VITE_UPLOAD_BASEURL = 'http://1.14.158.154:48080/upload'
VITE_SERVER_BASEURL = 'https://api.tuanbanlv.com/admin-api'
VITE_UPLOAD_BASEURL = 'https://api.tuanbanlv.com/upload'
# 备注如果后台带统一前缀则也要加到后面eg: https://ukw0y1.laf.run/api
# 注意,如果是微信小程序,还有一套请求地址的配置,根据 develop、trial、release 分别设置上传地址,见 `src/utils/index.ts`。

View File

@@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAqu4smP/F2ib/J2+pJp1Y379U8N18eEdNEIi0+Bw/TZSn90sN
mZvTAu/fB1kkSzjHLhvK8+TMTx/WDvD9u/9WpVlBK9rE6bx8+OcFCk5gePqDZdY0
ybd99D1oicudmO2yU+YMyBD4zM5qvdVyU5EDj6OnV1zB6D5n3pw2thICplYobN5K
osTEBr/c33RX8/3ZbRo0AdyZPcY5I3Uq6vDMCKdmJn4H0klWG3jXYtsen8HVzO3F
UWFFeiZeyBSq5iVBtm6s8w2mQhRbjc6rS0ERalrGL7mV0RjVQauL6TJrIxoI/8v4
aIOgo7gApFlqTf3nfyZw4OTEPQWrTKM2s7/qIwIDAQABAoIBAG0EoW8n2sHrk1tM
rV7ShmeWeY9yRDvWhgFgn8OLCJjrkkF4HgF10ByUbvQZ17seSHNRCJ2LtP9WN8mp
zLtF/LZS+e0FiAfnzvFVLvLG0GL4rCucdmidXnkTXYRdWHO8TruSA17q7DR8Brpy
04sW92V6pHVk1MvSWZ8ylPaFACmjyPjc/gvUdeZG1v0tUg6069r+RZegLotzhcDi
0T2N9W/0nWFIkNNCQrG5ze24NFD+FmhcNQnx/TQMUeU02JxAfA6sM6jdFcRm1j8Z
M2H/VBe1fOZjQceBHgfeexEBNWNqHB9QJIRdk51+EFC7I+CL1wIvJegrUYAQvwNm
njCzhnkCgYEA2SX45T5Zyicp8FGrYH1ufVRnipO/mwAkvlY8tbFd1C4fPB8Hkbx7
ZrM5G3293HJ2TBQO+kCIU1WyT9weVeJ7P9bruDyjNOSu1q7d4xjq49LSLXvo7C4+
T8V7z1fUZt0A0jOdaqfmw+R7ICzQAna8PI/J8elW3Vh7uNk0TRcIXZ0CgYEAyYNI
yrrpabP/sxGYfUpb+Ko6iAcISPTT7rZcN+dVGqqFb8tkvHUSPhhy/H5WYcdFFhzI
S5nMXXmRsgDDRdltukUg4zoA1um5gdI8SeOLzEdcFF0p/Pfc60G+LoY7K/aUlvy2
DeSpSHJgblZVMKw+2/lfvc2M/100BOjObWkWur8CgYEAgNthqVeonKdE4dD065tD
N6ggkUE/0FDzfOdbu033KfP8oQagzUCV0cnEt6WURv69aEP251XoD9uopm8uqTRu
guGcm4WQK9EQV2EJVrvwlyUBh/AhthVy8I91+wJZjnjTBemPHj1oWRJ6ZgtxnCSt
axrAcYdP/qWFNZneyWhDlJkCgYB+7jwuvteB5oidAetcmDcghhGCV3OniNfqGGI0
MHoR5vFQPvzAHLoV9Q6Q7v94ba2dxRmBTWpGQuo8BnD6EYAlgZ+6oXGf7e8U0Bl7
rWIElbpxdVGab4JviaTC53hkM9ja1mnSjIL5CFqnhaf5lbWumADvrIcw30OCCCbn
EffoPwKBgQDU/Q3/lZahWTr4w6NtNmyhi0EnoLv+a9afhYdii1+v5g5PfG0mDvx0
0lmX2+JNqhF5ZMlVACLY1wR0trz4Iz2LstOst1XE3ipy2zbQTF/g6ujudOxrzOuw
jr+1JcbjZfywRQO+2wgegm8Qut7cZe5AraR6cqzIZ6DUO8QtbmsJnw==
-----END RSA PRIVATE KEY-----

View File

@@ -81,3 +81,13 @@ export function generateContract(id: number) {
export function generateContractHtml(id: number) {
return http.get<string>(`/car/renewal-order/generate-contract-html?id=${id}`)
}
/** 创建线上签名令牌(用于分享签名) */
export function createSignToken(id: number) {
return http.post<{ uuid: string; signUrl: string }>('/car/renewal-order/create-sign-token', undefined, { id })
}
/** 清空订单合同与客户签名(重新生成合同时先清空再扫码签名) */
export function clearContractAndSignature(id: number) {
return http.post<boolean>('/car/renewal-order/clear-contract-sign', undefined, { id })
}

View File

@@ -7,6 +7,7 @@ export interface RenewalProductVO {
productName?: string // 产品名称
productContent?: string // 产品内容
productType?: string // 产品类别
effectiveYear?: string // 生效年限(仅产品类别为无忧时有值)
remark?: string // 备注
}

View File

@@ -17,6 +17,9 @@
<wd-cell title="产品类别">
<dict-tag :type="DICT_TYPE.CAR_RENEWAL_PRODUCT_TYPE" :value="productData?.productType" />
</wd-cell>
<wd-cell v-if="showEffectiveYear" title="生效年限">
<dict-tag :type="DICT_TYPE.CAR_RENEWAL_YEAR" :value="productData?.effectiveYear" />
</wd-cell>
<wd-cell title="产品内容" :value="productData?.productContent || '-'" />
<wd-cell title="备注" :value="productData?.remark || '-'" />
<wd-cell title="创建时间" :value="formatDateTime(productData?.createTime) || '-'" />
@@ -54,6 +57,7 @@ import { onLoad, onShow } from '@dcloudio/uni-app'
import { computed, onMounted, ref } from 'vue'
import { useToast } from 'wot-design-uni'
import { deleteRenewalProduct, getRenewalProduct } from '@/api/car/renewalproduct'
import { getDictLabel } from '@/hooks/useDict'
import { useAccess } from '@/hooks/useAccess'
import { navigateBackPlus } from '@/utils'
import { DICT_TYPE } from '@/utils/constants'
@@ -73,6 +77,12 @@ const deleting = ref(false)
const productId = ref<number>()
const productData = ref<RenewalProductVO | null>(null)
/** 仅当产品类别为「无忧」或「无忧延保」时显示生效年限 */
const showEffectiveYear = computed(() => {
const label = getDictLabel(DICT_TYPE.CAR_RENEWAL_PRODUCT_TYPE, productData.value?.productType) || ''
return label === '无忧' || label === '无忧延保'
})
/** 返回上一页 */
function handleBack() {
navigateBackPlus()
@@ -101,6 +111,8 @@ function handleDelete() {
try {
await deleteRenewalProduct(Number(productId.value))
toast.success('删除成功')
// 标记列表页需要刷新,返回后列表会重新拉取数据
uni.setStorageSync('renewalproduct_list_need_refresh', true)
setTimeout(() => {
handleBack()
}, 500)

View File

@@ -36,6 +36,17 @@
label-key="label"
value-key="value"
/>
<wd-picker
v-if="showEffectiveYear"
v-model="formData.effectiveYear"
:columns="effectiveYearOptions"
label="生效年限"
label-width="180rpx"
prop="effectiveYear"
placeholder="请选择生效年限"
label-key="label"
value-key="value"
/>
<wd-textarea
v-model="formData.productContent"
label="产品内容"
@@ -77,10 +88,10 @@
import type { FormInstance } from 'wot-design-uni/components/wd-form/types'
import type { RenewalProductVO } from '@/api/car/renewalproduct'
import { onLoad } from '@dcloudio/uni-app'
import { computed, onMounted, ref } from 'vue'
import { computed, onMounted, ref, watch } from 'vue'
import { useToast } from 'wot-design-uni'
import { createRenewalProduct, getRenewalProduct, updateRenewalProduct } from '@/api/car/renewalproduct'
import { getStrDictOptions } from '@/hooks/useDict'
import { getDictLabel, getStrDictOptions } from '@/hooks/useDict'
import { navigateBackPlus } from '@/utils'
import { DICT_TYPE } from '@/utils/constants'
@@ -107,10 +118,41 @@ const productTypeOptions = computed(() => {
}))
})
// 生效年限选项(仅产品类别为「无忧」时展示)
const effectiveYearOptions = computed(() => {
return getStrDictOptions(DICT_TYPE.CAR_RENEWAL_YEAR).map(item => ({
label: item.label,
value: item.value,
}))
})
/** 仅当产品类别为「无忧」或「无忧延保」时显示生效年限 */
const showEffectiveYear = computed(() => {
const label = getDictLabel(DICT_TYPE.CAR_RENEWAL_PRODUCT_TYPE, formData.value.productType) || ''
return label === '无忧' || label === '无忧延保'
})
// 产品类别切换为「无忧」或「无忧延保」时设置生效年限默认值,切换为其他时清空
watch(
() => formData.value.productType,
(type) => {
const label = getDictLabel(DICT_TYPE.CAR_RENEWAL_PRODUCT_TYPE, type) || ''
if (label === '无忧' || label === '无忧延保') {
const opts = getStrDictOptions(DICT_TYPE.CAR_RENEWAL_YEAR)
if (opts.length > 0 && !formData.value.effectiveYear) {
formData.value.effectiveYear = opts[0].value
}
} else {
formData.value.effectiveYear = undefined
}
},
)
const formData = ref<RenewalProductVO>({
productName: undefined,
productContent: undefined,
productType: undefined,
effectiveYear: undefined,
remark: undefined,
})
@@ -161,6 +203,8 @@ async function handleSubmit() {
await createRenewalProduct(formData.value)
toast.success('新增成功')
}
// 标记列表页需要刷新,返回后列表会重新拉取数据
uni.setStorageSync('renewalproduct_list_need_refresh', true)
setTimeout(() => {
navigateBackPlus()
}, 500)

View File

@@ -170,16 +170,24 @@ onReachBottom(() => {
loadMore()
})
/** 页面显示时刷新(从编辑页面返回时 */
/** 是否首次显示(首次不依赖 onShow 刷新,由 onMounted 拉取;从子页返回时 onShow 会再次触发并刷新 */
const isFirstShow = ref(true)
/** 页面显示时刷新(与订单列表一致:从新增/编辑/详情页返回后刷新列表) */
onShow(() => {
// 检查页面栈,如果上一个页面是编辑页面,则刷新
const pages = getCurrentPages()
if (pages.length > 1) {
const prevPage = pages[pages.length - 2]
if (prevPage?.route?.includes('/renewalproduct/form/index')) {
// 检查存储中的刷新标志(新增、编辑、删除成功后子页会设置)
const needRefresh = uni.getStorageSync('renewalproduct_list_need_refresh')
if (needRefresh) {
uni.removeStorageSync('renewalproduct_list_need_refresh')
getList(true)
isFirstShow.value = false
return
}
// 非首次显示时也刷新一次(从表单页或详情页返回,确保列表是最新)
if (!isFirstShow.value) {
getList(true)
}
}
isFirstShow.value = false
})
/** 初始化 */

View File

@@ -82,6 +82,7 @@
</view>
</picker>
</view>
<wd-input v-model="formData.invoiceDate" prop="invoiceDate" v-show="false" />
<wd-input
v-model="formData.invoiceAmount"
label="发票金额"
@@ -100,7 +101,7 @@
clearable
placeholder="请输入购买时公里数"
/>
<wd-cell title="行驶证" title-width="180rpx" />
<wd-cell title="行驶证" title-width="180rpx" required />
<view class="px-24rpx py-16rpx">
<wd-upload
v-model:file-list="drivingLicenseFileList"
@@ -140,7 +141,7 @@
:source-type="['album', 'camera']"
/>
</view>
<wd-cell title="购车发票" title-width="180rpx" />
<wd-cell title="购车发票" title-width="180rpx" required />
<view class="px-24rpx py-16rpx">
<wd-upload
v-model:file-list="carInvoiceFileList"
@@ -150,7 +151,7 @@
:source-type="['album', 'camera']"
/>
</view>
<wd-cell title="购置税发票" title-width="180rpx" />
<wd-cell title="购置税凭证" title-width="180rpx" required />
<view class="px-24rpx py-16rpx">
<wd-upload
v-model:file-list="purchaseTaxInvoiceFileList"
@@ -180,6 +181,7 @@
v-model="formData.serviceBuyer"
label="服务购买方"
label-width="180rpx"
prop="serviceBuyer"
clearable
placeholder="请输入服务购买方"
/>
@@ -187,6 +189,7 @@
v-model="formData.carBuyer"
label="车辆购买方"
label-width="180rpx"
prop="carBuyer"
clearable
placeholder="请输入车辆购买方"
/>
@@ -195,6 +198,7 @@
:columns="certTypeOptions"
label="证件类型"
label-width="180rpx"
prop="certType"
placeholder="请选择证件类型"
value-key="value"
label-key="label"
@@ -203,6 +207,7 @@
v-model="formData.certNo"
label="证件号码"
label-width="180rpx"
prop="certNo"
clearable
placeholder="请输入证件号码"
/>
@@ -210,6 +215,7 @@
v-model="formData.mobile"
label="联系电话"
label-width="180rpx"
prop="mobile"
type="number"
clearable
placeholder="请输入联系电话"
@@ -234,12 +240,12 @@
:columns="storeOptions"
label="门店"
label-width="180rpx"
prop="storeId"
placeholder="请选择门店"
value-key="id"
label-key="storeName"
disabled
/>
<wd-cell title="身份证正面" title-width="180rpx" />
<wd-cell title="身份证正面" title-width="180rpx" required />
<view class="px-24rpx py-16rpx">
<wd-upload
v-model:file-list="idCardFrontFileList"
@@ -249,7 +255,7 @@
:source-type="['album', 'camera']"
/>
</view>
<wd-cell title="身份证反面" title-width="180rpx" />
<wd-cell title="身份证反面" title-width="180rpx" required />
<view class="px-24rpx py-16rpx">
<wd-upload
v-model:file-list="idCardBackFileList"
@@ -280,6 +286,7 @@
v-model="formData.serviceProduct"
label="服务产品"
label-width="180rpx"
prop="serviceProduct"
readonly
placeholder="选择产品后自动填充"
/>
@@ -287,6 +294,7 @@
v-model="formData.productValidity"
label="产品时效"
label-width="180rpx"
prop="productValidity"
readonly
placeholder="选择产品后自动填充"
/>
@@ -294,6 +302,7 @@
:model-value="productTypeLabel"
label="产品类别"
label-width="180rpx"
prop="productType"
readonly
placeholder="选择产品后自动填充"
/>
@@ -301,16 +310,18 @@
v-model="formData.productFee"
label="产品费用"
label-width="180rpx"
prop="productFee"
type="digit"
clearable
placeholder="请输入产品费用"
/>
<wd-input
v-model="formData.originalWarrantyYears"
label="原厂质保时长"
label="产品年限"
label-width="180rpx"
required
clearable
placeholder="请输入原厂质保时长"
placeholder="请输入产品年限3"
/>
<wd-input
v-model="formData.originalWarrantyMileage"
@@ -325,27 +336,31 @@
<!-- 选项卡4: 其他信息 -->
<view v-show="tabIndex === 3">
<wd-cell-group border title="其他信息">
<wd-input
<wd-picker
v-model="formData.settlementMethod"
:columns="settlementMethodOptions"
label="结算方式"
label-width="180rpx"
clearable
placeholder="请输入结算方式"
prop="settlementMethod"
placeholder="请选择结算方式"
value-key="value"
label-key="label"
/>
<wd-input
v-model="formData.inputUser"
label="录单人"
label-width="180rpx"
prop="inputUser"
clearable
placeholder="请输入录单人"
/>
<wd-cell v-if="formData.productType === '00'" title="合同路径" title-width="180rpx">
<wd-cell v-if="showContractComponents" title="合同路径" title-width="180rpx">
<template #value>
<text class="text-24rpx text-[#999]">保存订单后可生成在线合同</text>
</template>
</wd-cell>
<wd-textarea
v-if="formData.productType === '00'"
v-if="showContractComponents"
v-model="formData.contractRemark"
label="合同备注"
label-width="180rpx"
@@ -408,7 +423,7 @@ import type { StoreVO } from '@/api/tire/store'
import dayjs from 'dayjs'
import { computed, onMounted, ref } from 'vue'
import { useMessage, useToast } from 'wot-design-uni'
import { createRenewalOrder } from '@/api/car/renewalorder'
import { createRenewalOrder, createSignToken } from '@/api/car/renewalorder'
import { findByProductType, getRenewalProduct } from '@/api/car/renewalproduct'
import { findByProductType as findStoreByProductType } from '@/api/tire/store'
import { getDictLabel, getStrDictOptions } from '@/hooks/useDict'
@@ -428,7 +443,7 @@ const toast = useToast()
const message = useMessage()
const tokenStore = useTokenStore()
const userStore = useUserStore()
const getTitle = computed(() => '新增续保订单')
const getTitle = computed(() => '新增订单')
const formLoading = ref(false)
const tabIndex = ref(0) // 当前选项卡索引
const invoiceFileList = ref<UploadFile[]>([]) // 发票图片文件列表
@@ -447,6 +462,11 @@ const productList = ref<RenewalProductVO[]>([]) // 续保产品列表
const storeOptions = computed(() => storeList.value.map(item => ({ id: item.id, storeName: item.storeName })))
const productOptions = computed(() => productList.value.map(item => ({ id: item.id, productName: item.productName })))
const certTypeOptions = computed(() => getStrDictOptions('car_renewal_identity_type'))
const settlementMethodOptions = computed(() => getStrDictOptions(DICT_TYPE.CAR_RENEWAL_PAY_METHOD))
// 判断是否显示合同相关组件仅当产品类别为00或02时显示
const showContractComponents = computed(() => {
return formData.value.productType === '00' || formData.value.productType === '02'
})
const formData = ref<RenewalOrderVO>({
id: undefined,
@@ -472,10 +492,10 @@ const formData = ref<RenewalOrderVO>({
serviceProduct: undefined,
productType: undefined,
productValidity: undefined,
originalWarrantyYears: undefined,
originalWarrantyYears: '3',
originalWarrantyMileage: undefined,
productFee: undefined,
settlementMethod: undefined,
settlementMethod: '00',
remark: undefined,
inputUser: undefined,
contractUrl: undefined,
@@ -497,9 +517,23 @@ const formRules = {
carModel: [{ required: true, message: '车型不能为空' }],
vin: [{ required: true, message: '车架号不能为空' }],
engineNo: [{ required: true, message: '发动机号不能为空' }],
invoiceDate: [{ required: true, message: '发票日期不能为空' }],
invoiceAmount: [{ required: true, message: '发票金额不能为空' }],
purchaseMileage: [{ required: true, message: '购买时公里数不能为空' }],
serviceBuyer: [{ required: true, message: '服务购买方不能为空' }],
carBuyer: [{ required: true, message: '车辆购买方不能为空' }],
certType: [{ required: true, message: '证件类型不能为空' }],
certNo: [{ required: true, message: '证件号码不能为空' }],
mobile: [{ required: true, message: '联系电话不能为空' }],
storeId: [{ required: true, message: '门店不能为空' }],
productId: [{ required: true, message: '续保产品不能为空' }],
serviceProduct: [{ required: true, message: '服务产品不能为空' }],
productType: [{ required: true, message: '产品类别不能为空' }],
productValidity: [{ required: true, message: '产品时效不能为空' }],
productFee: [{ required: true, message: '产品费用不能为空' }],
settlementMethod: [{ required: true, message: '结算方式不能为空' }],
inputUser: [{ required: true, message: '录单人不能为空' }],
originalWarrantyYears: [{ required: true, message: '产品年限不能为空' }],
}
// 根据 productType 值获取字典 label
@@ -713,7 +747,7 @@ function createUploadMethod(fieldName: keyof RenewalOrderVO): UploadMethod {
}
}
/** 多张图片上传(购车发票、购置税发票、商业险保单) */
/** 多张图片上传(购车发票、购置税凭证、商业险保单) */
function createMultiUploadMethod(fieldName: 'carInvoiceUrls' | 'purchaseTaxInvoiceUrls' | 'businessInsurancePolicyUrls'): UploadMethod {
return (file, uploadFormData, options) => {
const uploadTask = uni.uploadFile({
@@ -756,14 +790,36 @@ function createMultiUploadMethod(fieldName: 'carInvoiceUrls' | 'purchaseTaxInvoi
// 订单录入页面不需要加载详情,只允许新增
// ========== 新增完成后合同预览逻辑(产品类别 00 ==========
// 流程:提交 → 创建订单 → 若 productType===00 则跳转合同预览页 → 客户签名 → 确认生成合同 → 再跳转订单详情
// ========== 新增完成后合同预览逻辑(产品类别 00 或 02 ==========
// 流程:提交 → 创建订单 → 若 productType===00 或 02 则跳转合同预览页 → 客户签名 → 确认生成合同 → 再跳转订单详情
// 否则:创建成功 → 提示 → 跳转订单详情
async function handleSubmit() {
const { valid } = await formRef.value!.validate()
if (!valid) return
// 必传文件校验:行驶证、购车发票、购置税凭证、身份证正反面
if (!formData.value.drivingLicenseUrl) {
toast.error('请上传行驶证')
return
}
if (!formData.value.carInvoiceUrls?.length) {
toast.error('请上传购车发票')
return
}
if (!formData.value.purchaseTaxInvoiceUrls?.length) {
toast.error('请上传购置税凭证')
return
}
if (!formData.value.idCardFrontUrl) {
toast.error('请上传身份证正面')
return
}
if (!formData.value.idCardBackUrl) {
toast.error('请上传身份证反面')
return
}
formLoading.value = true
try {
const data = { ...formData.value }
@@ -777,17 +833,49 @@ async function handleSubmit() {
data.invoiceDate = dayjs(data.invoiceDate).format('YYYY-MM-DD')
}
}
// 确保 productType 被正确传递
if (!data.productType && formData.value.productType) {
data.productType = formData.value.productType
}
const raw = await createRenewalOrder(data) as unknown
const id = (raw as { data?: number })?.data ?? (raw as number)
const isProduct00 = formData.value.productType === '00'
// 检查产品类别是否为 00 或 02
const productType = data.productType || formData.value.productType
const needContract = (productType === '00' || productType === '02') && id
if (isProduct00 && id) {
// 新增完成 → 跳转合同预览 → 客户签名 → 确认生成合同 → 合同预览页内再跳转详情
console.log('新增订单提交 - productType:', productType, 'needContract:', needContract, 'id:', id, 'data:', data)
if (needContract) {
// 新增完成 → 弹出选项:直接签名 / 分享签名
uni.setStorageSync('renewalorder_list_need_refresh', true)
toast.show('订单已创建,请选择签名方式')
uni.showActionSheet({
itemList: ['直接签名', '分享签名'],
success: async (res) => {
if (res.tapIndex === 0) {
uni.navigateTo({
url: `/pages/car/renewalorder/contract-preview/index?id=${id}&from=add`,
})
toast.show('订单已创建,请完成客户签名后生成合同')
toast.show('请完成客户签名后生成合同')
} else if (res.tapIndex === 1) {
try {
const tokenRes = await createSignToken(id) as { data?: { signUrl?: string }; signUrl?: string }
const signUrl = tokenRes?.data?.signUrl ?? tokenRes?.signUrl
if (!signUrl) {
toast.show('生成签名链接失败')
return
}
uni.navigateTo({
url: `/pages/car/renewalorder/sign-share/index?signUrl=${encodeURIComponent(signUrl)}`,
})
toast.show('请分享链接给客户签名,签名成功后合同将自动生成')
} catch (e) {
console.error(e)
toast.error('操作失败')
}
}
},
})
} else {
// 非 00 产品:直接提示成功并跳转详情
toast.success('新增成功')
@@ -806,14 +894,24 @@ async function handleSubmit() {
/** 初始化 */
onMounted(async () => {
await Promise.all([getStoreList(), getProductList()])
// 新增:默认选择门店第一项
if (!formData.value.storeId && storeList.value.length > 0) {
formData.value.storeId = storeList.value[0].id
// 新增:默认选择当前用户所在门店;车辆购买方默认为当前用户门店名称,均可修改
if (userStore.storeId != null && storeList.value.some(s => s.id === userStore.storeId)) {
formData.value.storeId = userStore.storeId
formData.value.serviceBuyer = userStore.storeName || storeList.value.find(s => s.id === userStore.storeId)?.storeName || ''
} else if (storeList.value.length > 0) {
if (!formData.value.storeId) formData.value.storeId = storeList.value[0].id
if (!formData.value.serviceBuyer) formData.value.serviceBuyer = userStore.storeName || storeList.value[0].storeName || ''
}
// 新增:默认选择证件类型字典第一项
if (!formData.value.certType && certTypeOptions.value.length > 0) {
formData.value.certType = certTypeOptions.value[0].value
}
// 新增:默认选择结算方式为 '00'
if (!formData.value.settlementMethod) {
formData.value.settlementMethod = '00'
}
// 新增:录单人默认当前登录用户名称,可修改
formData.value.inputUser = userStore.userInfo?.nickname || userStore.userInfo?.username || ''
})
</script>

View File

@@ -115,6 +115,12 @@ async function load() {
previewLoading.value = true
try {
await Promise.all([fetchOrder(), fetchHtml()])
// 如果是新增订单后跳转过来的,提示用户可以签名
if (fromAdd.value === 'add' || fromAdd.value === 'form_create') {
setTimeout(() => {
toast.show('请预览合同内容,确认无误后点击"点击签名"按钮进行签名')
}, 500)
}
} catch (e) {
console.error('加载合同预览失败', e)
toast.error('加载失败')
@@ -172,9 +178,13 @@ onLoad((options) => {
})
/** 从签名页返回时刷新订单,以更新“已签名”状态 */
onShow(() => {
onShow(async () => {
if (orderId.value && !previewLoading.value) {
fetchOrder()
await fetchOrder()
// 如果是新增订单后跳转过来的add 或 form_create签名完成后自动提示可以生成合同
if ((fromAdd.value === 'add' || fromAdd.value === 'form_create') && order.value?.customerSignatureUrl) {
toast.show('签名已完成,请点击"确认生成合同"按钮生成合同')
}
}
})
</script>

View File

@@ -153,7 +153,7 @@
/>
</view>
</wd-cell>
<wd-cell title="购置税发票" title-width="180rpx">
<wd-cell title="购置税凭证" title-width="180rpx">
<view class="flex items-center justify-end">
<wd-upload
v-model:file-list="purchaseTaxInvoiceFileList"
@@ -326,11 +326,10 @@
/>
<wd-input
v-model="formData.originalWarrantyYears"
label="原厂质保时长"
label="产品年限"
label-width="180rpx"
clearable
placeholder="请输入原厂质保时长"
readonly
placeholder="-"
/>
<wd-input
v-model="formData.originalWarrantyMileage"
@@ -362,7 +361,7 @@
placeholder="请输入录单人"
readonly
/>
<wd-cell v-if="formData.productType === '00'" title="合同路径" title-width="180rpx">
<wd-cell v-if="formData.productType === '00' || formData.productType === '02'" title="合同路径" title-width="180rpx">
<view class="contract-area">
<view class="contract-row contract-row-top">
<view
@@ -388,7 +387,7 @@
</view>
</wd-cell>
<wd-textarea
v-if="formData.productType === '00'"
v-if="formData.productType === '00' || formData.productType === '02'"
v-model="formData.contractRemark"
label="合同备注"
label-width="180rpx"
@@ -424,7 +423,7 @@ import dayjs from 'dayjs'
import { onLoad, onShow } from '@dcloudio/uni-app'
import { computed, onMounted, ref } from 'vue'
import { useToast } from 'wot-design-uni'
import { getRenewalOrder } from '@/api/car/renewalorder'
import { clearContractAndSignature, createSignToken, getRenewalOrder } from '@/api/car/renewalorder'
import { findByProductType, getRenewalProduct } from '@/api/car/renewalproduct'
import { findByProductType as findStoreByProductType } from '@/api/tire/store'
import { getEnvBaseUrl, navigateBackPlus } from '@/utils'
@@ -454,7 +453,7 @@ const certificateOfConformityFileList = ref<UploadFile[]>([]) // 合格证文件
const odometerPhotoFileList = ref<UploadFile[]>([]) // 里程表照片文件列表
const nameplatePhotoFileList = ref<UploadFile[]>([]) // 车名牌照片文件列表
const carInvoiceFileList = ref<UploadFile[]>([]) // 购车发票文件列表
const purchaseTaxInvoiceFileList = ref<UploadFile[]>([]) // 购置税发票文件列表
const purchaseTaxInvoiceFileList = ref<UploadFile[]>([]) // 购置税凭证文件列表
const businessInsurancePolicyFileList = ref<UploadFile[]>([]) // 商业险保单文件列表
const storeOptions = computed(() => storeList.value.map(item => ({ id: item.id, storeName: item.storeName })))
@@ -488,7 +487,7 @@ const formData = ref<RenewalOrderVO>({
productId: undefined,
serviceProduct: undefined,
productValidity: undefined,
originalWarrantyYears: undefined,
originalWarrantyYears: '3',
originalWarrantyMileage: undefined,
productFee: undefined,
settlementMethod: undefined,
@@ -718,19 +717,55 @@ async function getDetail() {
}
}
/** 预览并生成合同:跳转合同预览页,签名后可确认生成 */
/** 生成/重新生成合同:弹出选项(直接签名 / 分享签名) */
function handlePreviewContract() {
if (!orderId.value) {
toast.show('订单编号为空')
return
}
if (formData.value.productType !== '00') {
toast.show('当前产品类别不支持生成合同')
if (formData.value.productType !== '00' && formData.value.productType !== '02') {
toast.show('当前产品类别不支持生成合同,仅产品类别为 00 或 02 时可生成合同')
return
}
uni.showActionSheet({
itemList: ['直接签名', '分享签名'],
success: (res) => {
if (res.tapIndex === 0) {
// 直接签名:跳转合同预览页
uni.navigateTo({
url: `/pages/car/renewalorder/contract-preview/index?id=${orderId.value}`,
})
} else if (res.tapIndex === 1) {
// 分享签名:创建令牌后跳转分享页
doOpenShareSign(Number(orderId.value))
}
},
})
}
/** 分享签名:若有合同则先清空,再创建令牌并跳转分享页 */
async function doOpenShareSign(id: number) {
try {
const hadContract = !!formData.value.contractUrl
if (hadContract) {
await clearContractAndSignature(id)
formData.value.contractUrl = undefined
formData.value.customerSignatureUrl = undefined
toast.show('已清空合同与签名,请分享链接给客户签名')
}
const res = await createSignToken(id) as { data?: { signUrl?: string }; signUrl?: string }
const signUrl = res?.data?.signUrl ?? res?.signUrl
if (!signUrl) {
toast.show('生成签名链接失败')
return
}
uni.navigateTo({
url: `/pages/car/renewalorder/sign-share/index?signUrl=${encodeURIComponent(signUrl)}`,
})
} catch (e) {
console.error(e)
toast.error('操作失败')
}
}
/** 页面加载时获取路由参数 */

View File

@@ -69,13 +69,20 @@
clearable
placeholder="请输入发动机号"
/>
<wd-datetime-picker
v-model="formData.invoiceDate"
label="发票日期"
label-width="180rpx"
type="date"
placeholder="请选择发票日期"
/>
<wd-cell title="发票日期" title-width="180rpx">
<template #value>
<picker
mode="date"
:value="formData.invoiceDate || getDefaultDate()"
@change="onInvoiceDateChange"
>
<view class="invoice-date-picker-value">
{{ formData.invoiceDate || getDefaultDate() || '请选择发票日期' }}
</view>
</picker>
</template>
</wd-cell>
<wd-input v-model="formData.invoiceDate" prop="invoiceDate" v-show="false" />
<wd-input
v-model="formData.invoiceAmount"
label="发票金额"
@@ -94,7 +101,7 @@
clearable
placeholder="请输入购买时公里数"
/>
<wd-cell title="行驶证" title-width="180rpx">
<wd-cell title="行驶证" title-width="180rpx" required>
<template #value>
<view class="flex items-center justify-end">
<wd-upload
@@ -146,7 +153,7 @@
</view>
</template>
</wd-cell>
<wd-cell title="购车发票" title-width="180rpx">
<wd-cell title="购车发票" title-width="180rpx" required>
<template #value>
<view class="flex items-center justify-end">
<wd-upload
@@ -159,7 +166,7 @@
</view>
</template>
</wd-cell>
<wd-cell title="购置税发票" title-width="180rpx">
<wd-cell title="购置税凭证" title-width="180rpx" required>
<template #value>
<view class="flex items-center justify-end">
<wd-upload
@@ -195,6 +202,7 @@
v-model="formData.serviceBuyer"
label="服务购买方"
label-width="180rpx"
prop="serviceBuyer"
clearable
placeholder="请输入服务购买方"
/>
@@ -202,6 +210,7 @@
v-model="formData.carBuyer"
label="车辆购买方"
label-width="180rpx"
prop="carBuyer"
clearable
placeholder="请输入车辆购买方"
/>
@@ -210,6 +219,7 @@
:columns="certTypeOptions"
label="证件类型"
label-width="180rpx"
prop="certType"
placeholder="请选择证件类型"
value-key="value"
label-key="label"
@@ -218,6 +228,7 @@
v-model="formData.certNo"
label="证件号码"
label-width="180rpx"
prop="certNo"
clearable
placeholder="请输入证件号码"
/>
@@ -225,6 +236,7 @@
v-model="formData.mobile"
label="联系电话"
label-width="180rpx"
prop="mobile"
type="number"
clearable
placeholder="请输入联系电话"
@@ -249,6 +261,7 @@
:columns="storeOptions"
label="门店"
label-width="180rpx"
prop="storeId"
placeholder="请选择门店"
value-key="id"
label-key="storeName"
@@ -266,7 +279,7 @@
</view>
</template>
</wd-cell>
<wd-cell title="身份证正面" title-width="180rpx">
<wd-cell title="身份证正面" title-width="180rpx" required>
<template #value>
<view class="flex items-center justify-end">
<wd-upload
@@ -279,7 +292,7 @@
</view>
</template>
</wd-cell>
<wd-cell title="身份证反面" title-width="180rpx">
<wd-cell title="身份证反面" title-width="180rpx" required>
<template #value>
<view class="flex items-center justify-end">
<wd-upload
@@ -313,6 +326,7 @@
v-model="formData.serviceProduct"
label="服务产品"
label-width="180rpx"
prop="serviceProduct"
readonly
placeholder="选择产品后自动填充"
/>
@@ -320,6 +334,7 @@
v-model="formData.productValidity"
label="产品时效"
label-width="180rpx"
prop="productValidity"
readonly
placeholder="选择产品后自动填充"
/>
@@ -327,6 +342,7 @@
:model-value="productTypeLabel"
label="产品类别"
label-width="180rpx"
prop="productType"
readonly
placeholder="选择产品后自动填充"
/>
@@ -334,16 +350,18 @@
v-model="formData.productFee"
label="产品费用"
label-width="180rpx"
prop="productFee"
type="digit"
clearable
placeholder="请输入产品费用"
/>
<wd-input
v-model="formData.originalWarrantyYears"
label="原厂质保时长"
label="产品年限"
label-width="180rpx"
required
clearable
placeholder="请输入原厂质保时长"
placeholder="请输入产品年限3"
/>
<wd-input
v-model="formData.originalWarrantyMileage"
@@ -358,21 +376,25 @@
<!-- 选项卡4: 其他信息 -->
<view v-show="tabIndex === 3">
<wd-cell-group border title="其他信息">
<wd-input
<wd-picker
v-model="formData.settlementMethod"
:columns="settlementMethodOptions"
label="结算方式"
label-width="180rpx"
clearable
placeholder="请输入结算方式"
prop="settlementMethod"
placeholder="请选择结算方式"
value-key="value"
label-key="label"
/>
<wd-input
v-model="formData.inputUser"
label="录单人"
label-width="180rpx"
prop="inputUser"
clearable
placeholder="请输入录单人"
/>
<wd-cell v-if="formData.productType === '00'" title="合同路径" title-width="180rpx">
<wd-cell v-if="showContractComponents" title="合同路径" title-width="180rpx">
<template #value>
<view class="contract-area">
<view class="contract-row contract-row-top">
@@ -397,7 +419,7 @@
</template>
</wd-cell>
<wd-textarea
v-if="formData.productType === '00'"
v-if="showContractComponents"
v-model="formData.contractRemark"
label="合同备注"
label-width="180rpx"
@@ -457,11 +479,18 @@ import type { RenewalProductVO } from '@/api/car/renewalproduct'
import type { StoreVO } from '@/api/tire/store'
import type { UploadFile, UploadMethod } from 'wot-design-uni/components/wd-upload/types'
import { onLoad, onShow } from '@dcloudio/uni-app'
import { computed, onMounted, ref } from 'vue'
import { computed, onMounted, ref, watch, nextTick } from 'vue'
import { useToast, useMessage } from 'wot-design-uni'
import { createRenewalOrder, getRenewalOrder, updateRenewalOrder } from '@/api/car/renewalorder'
import {
clearContractAndSignature,
createRenewalOrder,
createSignToken,
getRenewalOrder,
updateRenewalOrder,
} from '@/api/car/renewalorder'
import { findByProductType, getRenewalProduct } from '@/api/car/renewalproduct'
import { findByProductType as findStoreByProductType } from '@/api/tire/store'
import { useUserStore } from '@/store/user'
import { getEnvBaseUrl, navigateBackPlus } from '@/utils'
import { getDictLabel, getStrDictOptions } from '@/hooks/useDict'
import { DICT_TYPE } from '@/utils/constants'
@@ -482,7 +511,8 @@ definePage({
const toast = useToast()
const message = useMessage()
const getTitle = computed(() => props.id ? '编辑续保订单' : '新增续保订单')
const userStore = useUserStore()
const getTitle = computed(() => props.id ? '编辑订单' : '新增订单')
const formLoading = ref(false)
const tabIndex = ref(0) // 当前选项卡索引
const storeList = ref<StoreVO[]>([]) // 门店列表
@@ -496,17 +526,23 @@ const certificateOfConformityFileList = ref<UploadFile[]>([]) // 合格证文件
const odometerPhotoFileList = ref<UploadFile[]>([]) // 里程表照片文件列表
const nameplatePhotoFileList = ref<UploadFile[]>([]) // 车名牌照片文件列表
const carInvoiceFileList = ref<UploadFile[]>([]) // 购车发票文件列表
const purchaseTaxInvoiceFileList = ref<UploadFile[]>([]) // 购置税发票文件列表
const purchaseTaxInvoiceFileList = ref<UploadFile[]>([]) // 购置税凭证文件列表
const businessInsurancePolicyFileList = ref<UploadFile[]>([]) // 商业险保单文件列表
const storeOptions = computed(() => storeList.value.map(item => ({ id: item.id, storeName: item.storeName })))
const productOptions = computed(() => productList.value.map(item => ({ id: item.id, productName: item.productName })))
const certTypeOptions = computed(() => getStrDictOptions('car_renewal_identity_type'))
const settlementMethodOptions = computed(() => getStrDictOptions(DICT_TYPE.CAR_RENEWAL_PAY_METHOD))
// 根据 productType 值获取字典 label
const productTypeLabel = computed(() => {
if (!formData.value.productType) return ''
return getDictLabel(DICT_TYPE.CAR_RENEWAL_PRODUCT_TYPE, formData.value.productType) || formData.value.productType
})
// 判断是否显示合同相关组件仅当产品类别为00或02时显示
const showContractComponents = computed(() => {
return formData.value.productType === '00' || formData.value.productType === '02'
})
const formData = ref<RenewalOrderVO>({
id: undefined,
@@ -518,7 +554,14 @@ const formData = ref<RenewalOrderVO>({
purchaseMileage: undefined,
engineNo: undefined,
vin: undefined,
invoiceDate: undefined,
invoiceDate: (() => {
// 设置默认值为当前日期,避免 wd-datetime-picker 收到 undefined 报错
const today = new Date()
const year = today.getFullYear()
const month = String(today.getMonth() + 1).padStart(2, '0')
const day = String(today.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
})(),
invoiceUrl: undefined,
serviceBuyer: undefined,
carBuyer: undefined,
@@ -532,10 +575,10 @@ const formData = ref<RenewalOrderVO>({
serviceProduct: undefined,
productType: undefined,
productValidity: undefined,
originalWarrantyYears: undefined,
originalWarrantyYears: '3',
originalWarrantyMileage: undefined,
productFee: undefined,
settlementMethod: undefined,
settlementMethod: '00',
remark: undefined,
inputUser: undefined,
contractRemark: undefined,
@@ -558,9 +601,23 @@ const formRules = {
carModel: [{ required: true, message: '车型不能为空' }],
vin: [{ required: true, message: '车架号不能为空' }],
engineNo: [{ required: true, message: '发动机号不能为空' }],
invoiceDate: [{ required: true, message: '发票日期不能为空' }],
invoiceAmount: [{ required: true, message: '发票金额不能为空' }],
purchaseMileage: [{ required: true, message: '购买时公里数不能为空' }],
serviceBuyer: [{ required: true, message: '服务购买方不能为空' }],
carBuyer: [{ required: true, message: '车辆购买方不能为空' }],
certType: [{ required: true, message: '证件类型不能为空' }],
certNo: [{ required: true, message: '证件号码不能为空' }],
mobile: [{ required: true, message: '联系电话不能为空' }],
storeId: [{ required: true, message: '门店不能为空' }],
productId: [{ required: true, message: '续保产品不能为空' }],
serviceProduct: [{ required: true, message: '服务产品不能为空' }],
productType: [{ required: true, message: '产品类别不能为空' }],
productValidity: [{ required: true, message: '产品时效不能为空' }],
productFee: [{ required: true, message: '产品费用不能为空' }],
settlementMethod: [{ required: true, message: '结算方式不能为空' }],
inputUser: [{ required: true, message: '录单人不能为空' }],
originalWarrantyYears: [{ required: true, message: '产品年限不能为空' }],
}
const formRef = ref<FormInstance>()
@@ -595,13 +652,16 @@ async function handleNextTab() {
let validFields: string[] = []
if (tabIndex.value === 0) {
// 验证车辆信息
validFields = ['licensePlate', 'carBrand', 'carModel', 'vin', 'engineNo', 'invoiceAmount', 'purchaseMileage']
validFields = ['licensePlate', 'carBrand', 'carModel', 'vin', 'engineNo', 'invoiceDate', 'invoiceAmount', 'purchaseMileage']
} else if (tabIndex.value === 1) {
// 购买方信息无必填项
validFields = []
// 验证购买方信息
validFields = ['serviceBuyer', 'carBuyer', 'certType', 'certNo', 'mobile', 'storeId']
} else if (tabIndex.value === 2) {
// 验证产品信息
validFields = ['productId']
validFields = ['productId', 'serviceProduct', 'productType', 'productValidity', 'productFee', 'originalWarrantyYears']
} else if (tabIndex.value === 3) {
// 验证其他信息
validFields = ['settlementMethod', 'inputUser']
}
if (validFields.length > 0) {
@@ -636,6 +696,25 @@ async function getProductList() {
}
}
/** 获取默认日期 */
function getDefaultDate() {
const today = new Date()
const year = today.getFullYear()
const month = String(today.getMonth() + 1).padStart(2, '0')
const day = String(today.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
}
/** 发票日期选择(原生 picker避免 wd-datetime-picker Invalid array length 错误) */
function onInvoiceDateChange(e: { detail: { value: string } }) {
const v = e.detail?.value
if (v && /^\d{4}-\d{2}-\d{2}$/.test(v)) {
formData.value.invoiceDate = v
} else {
formData.value.invoiceDate = getDefaultDate()
}
}
/** 处理产品选择变化 */
async function handleProductChange({ value }: { value: number }) {
if (!value) {
@@ -751,20 +830,54 @@ const contractFileName = computed(() => {
return formData.value.contractUrl ? getFileNameFromUrl(formData.value.contractUrl) : '合同.pdf'
})
/** 预览并生成合同:跳转合同预览页,签名后可确认生成 */
/** 生成/重新生成合同:弹出选项(直接签名 / 分享签名) */
function handlePreviewContract() {
const id = props.id || formData.value.id
if (!id) {
toast.show('请先保存订单后再生成合同')
return
}
if (formData.value.productType !== '00') {
toast.show('当前产品类别不支持生成合同')
if (formData.value.productType !== '00' && formData.value.productType !== '02') {
toast.show('当前产品类别不支持生成合同,仅产品类别为 00 或 02 时可生成合同')
return
}
uni.showActionSheet({
itemList: ['直接签名', '分享签名'],
success: (res) => {
if (res.tapIndex === 0) {
uni.navigateTo({
url: `/pages/car/renewalorder/contract-preview/index?id=${id}`,
})
} else if (res.tapIndex === 1) {
doOpenShareSign(Number(id))
}
},
})
}
/** 分享签名:若有合同则先清空,再创建令牌并跳转分享页 */
async function doOpenShareSign(id: number) {
try {
const hadContract = !!formData.value.contractUrl
if (hadContract) {
await clearContractAndSignature(id)
formData.value.contractUrl = undefined
formData.value.customerSignatureUrl = undefined
toast.show('已清空合同与签名,请分享链接给客户签名')
}
const res = await createSignToken(id) as { data?: { signUrl?: string }; signUrl?: string }
const signUrl = res?.data?.signUrl ?? res?.signUrl
if (!signUrl) {
toast.show('生成签名链接失败')
return
}
uni.navigateTo({
url: `/pages/car/renewalorder/sign-share/index?signUrl=${encodeURIComponent(signUrl)}`,
})
} catch (e) {
console.error(e)
toast.error('操作失败')
}
}
/** 处理合同下载并打开 */
@@ -817,15 +930,37 @@ async function getDetail() {
try {
const orderId = copyId.value || Number(props.id)
const data = await getRenewalOrder(orderId)
// 处理发票日期
// 处理发票日期 - 先设置一个默认值,避免组件初始化时收到 undefined
const getDefaultDate = () => {
const today = new Date()
const year = today.getFullYear()
const month = String(today.getMonth() + 1).padStart(2, '0')
const day = String(today.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
}
// 先设置默认值
formData.value.invoiceDate = getDefaultDate()
// 然后处理实际日期
if (data.invoiceDate) {
let processedDate: string | null = null
if (typeof data.invoiceDate === 'string') {
formData.value.invoiceDate = data.invoiceDate.split(' ')[0] // 只取日期部分
const dateStr = data.invoiceDate.split(' ')[0] // 只取日期部分
// 验证日期格式,确保符合 YYYY-MM-DD
if (/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) {
processedDate = dateStr
}
} else if (data.invoiceDate instanceof Date) {
const year = data.invoiceDate.getFullYear()
const month = String(data.invoiceDate.getMonth() + 1).padStart(2, '0')
const day = String(data.invoiceDate.getDate()).padStart(2, '0')
formData.value.invoiceDate = `${year}-${month}-${day}`
processedDate = `${year}-${month}-${day}`
}
// 如果处理后的日期有效,使用它;否则保持默认值
if (processedDate && /^\d{4}-\d{2}-\d{2}$/.test(processedDate)) {
formData.value.invoiceDate = processedDate
}
}
// 处理多张图片的 JSON 数组字段:如果后端返回的是字符串,需要解析为数组
@@ -850,18 +985,103 @@ async function getDetail() {
data.businessInsurancePolicyUrls = []
}
}
// 如果是复制创建,清除主键合同路径等与原订单绑定的信息
// 如果是复制创建,清除主键合同路径和所有图片相关字段
if (copyId.value) {
// 确保发票日期格式正确,如果为空或无效则设置为当前日期
let invoiceDate = data.invoiceDate
if (!invoiceDate || invoiceDate === 'Invalid Date' || invoiceDate === 'null' || invoiceDate === 'undefined') {
const today = new Date()
const year = today.getFullYear()
const month = String(today.getMonth() + 1).padStart(2, '0')
const day = String(today.getDate()).padStart(2, '0')
invoiceDate = `${year}-${month}-${day}`
} else if (typeof invoiceDate === 'string') {
// 确保日期格式为 YYYY-MM-DD
invoiceDate = invoiceDate.split(' ')[0]
// 验证日期格式
if (!/^\d{4}-\d{2}-\d{2}$/.test(invoiceDate)) {
const today = new Date()
const year = today.getFullYear()
const month = String(today.getMonth() + 1).padStart(2, '0')
const day = String(today.getDate()).padStart(2, '0')
invoiceDate = `${year}-${month}-${day}`
}
}
const copyData: any = {
...data,
id: undefined,
contractUrl: '',
invoiceDate: invoiceDate || (() => {
// 如果发票日期仍然无效,设置为当前日期
const today = new Date()
const year = today.getFullYear()
const month = String(today.getMonth() + 1).padStart(2, '0')
const day = String(today.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
})(),
// 清空所有图片相关字段
customerSignatureUrl: undefined,
idCardFrontUrl: undefined,
idCardBackUrl: undefined,
drivingLicenseUrl: undefined,
certificateOfConformityUrl: undefined,
odometerPhotoUrl: undefined,
nameplatePhotoUrl: undefined,
carInvoiceUrls: [],
purchaseTaxInvoiceUrls: [],
businessInsurancePolicyUrls: [],
}
// 确保 invoiceDate 格式正确
if (!copyData.invoiceDate || !/^\d{4}-\d{2}-\d{2}$/.test(copyData.invoiceDate)) {
const today = new Date()
const year = today.getFullYear()
const month = String(today.getMonth() + 1).padStart(2, '0')
const day = String(today.getDate()).padStart(2, '0')
copyData.invoiceDate = `${year}-${month}-${day}`
}
// 确保 invoiceDate 是有效的字符串
if (!copyData.invoiceDate || typeof copyData.invoiceDate !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(copyData.invoiceDate)) {
const today = new Date()
const year = today.getFullYear()
const month = String(today.getMonth() + 1).padStart(2, '0')
const day = String(today.getDate()).padStart(2, '0')
copyData.invoiceDate = `${year}-${month}-${day}`
}
formData.value = copyData
} else {
formData.value = data
// 使用 nextTick 确保组件正确更新,然后再次验证日期值
await nextTick()
// 强制确保日期值有效
if (!formData.value.invoiceDate || typeof formData.value.invoiceDate !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(formData.value.invoiceDate)) {
const today = new Date()
const year = today.getFullYear()
const month = String(today.getMonth() + 1).padStart(2, '0')
const day = String(today.getDate()).padStart(2, '0')
formData.value.invoiceDate = `${year}-${month}-${day}`
}
// 初始化文件列表
// 复制创建模式下,清空所有文件列表
customerSignatureFileList.value = []
idCardFrontFileList.value = []
idCardBackFileList.value = []
drivingLicenseFileList.value = []
certificateOfConformityFileList.value = []
odometerPhotoFileList.value = []
nameplatePhotoFileList.value = []
carInvoiceFileList.value = []
purchaseTaxInvoiceFileList.value = []
businessInsurancePolicyFileList.value = []
} else {
// 编辑模式下,确保发票日期有效
if (!data.invoiceDate || !/^\d{4}-\d{2}-\d{2}$/.test(data.invoiceDate)) {
const today = new Date()
const year = today.getFullYear()
const month = String(today.getMonth() + 1).padStart(2, '0')
const day = String(today.getDate()).padStart(2, '0')
data.invoiceDate = `${year}-${month}-${day}`
}
formData.value = data
// 编辑模式下,初始化文件列表
customerSignatureFileList.value = urlToFileList(data.customerSignatureUrl)
idCardFrontFileList.value = urlToFileList(data.idCardFrontUrl)
idCardBackFileList.value = urlToFileList(data.idCardBackUrl)
@@ -872,6 +1092,7 @@ async function getDetail() {
carInvoiceFileList.value = urlToFileList(data.carInvoiceUrls)
purchaseTaxInvoiceFileList.value = urlToFileList(data.purchaseTaxInvoiceUrls)
businessInsurancePolicyFileList.value = urlToFileList(data.businessInsurancePolicyUrls)
}
} catch (error) {
console.error('获取订单详情失败:', error)
toast.show('获取订单详情失败')
@@ -889,9 +1110,36 @@ async function handleSubmit() {
return
}
// 必传文件校验:行驶证、购车发票、购置税凭证、身份证正反面
if (!formData.value.drivingLicenseUrl) {
toast.show('请上传行驶证')
return
}
if (!formData.value.carInvoiceUrls?.length) {
toast.show('请上传购车发票')
return
}
if (!formData.value.purchaseTaxInvoiceUrls?.length) {
toast.show('请上传购置税凭证')
return
}
if (!formData.value.idCardFrontUrl) {
toast.show('请上传身份证正面')
return
}
if (!formData.value.idCardBackUrl) {
toast.show('请上传身份证反面')
return
}
formLoading.value = true
try {
const data = { ...formData.value }
// 确保 productType 被正确传递
if (!data.productType && formData.value.productType) {
data.productType = formData.value.productType
}
// 复制创建和新增都走新增接口
if (props.id && !copyId.value) {
await updateRenewalOrder(data)
@@ -911,13 +1159,43 @@ async function handleSubmit() {
} else {
const res = (await createRenewalOrder(data)) as unknown
const newId = (res as { data?: number })?.data ?? (res as number)
const needContract = formData.value.productType === '00' && newId
// 检查产品类别是否为 00 或 02
const productType = data.productType || formData.value.productType
const needContract = (productType === '00' || productType === '02') && newId
console.log('表单提交 - productType:', productType, 'needContract:', needContract, 'id:', newId, 'data:', data)
if (needContract) {
// 新增完成 → 弹出选项:直接签名 / 分享签名
uni.setStorageSync('renewalorder_list_need_refresh', true)
toast.show('订单已创建,请选择签名方式')
uni.showActionSheet({
itemList: ['直接签名', '分享签名'],
success: async (res) => {
if (res.tapIndex === 0) {
uni.navigateTo({
url: `/pages/car/renewalorder/contract-preview/index?id=${newId}&from=form_create`,
})
toast.show('订单已创建,请完成客户签名后生成合同')
toast.show('请完成客户签名后生成合同')
} else if (res.tapIndex === 1) {
try {
const tokenRes = await createSignToken(newId) as { data?: { signUrl?: string }; signUrl?: string }
const signUrl = tokenRes?.data?.signUrl ?? tokenRes?.signUrl
if (!signUrl) {
toast.show('生成签名链接失败')
return
}
uni.navigateTo({
url: `/pages/car/renewalorder/sign-share/index?signUrl=${encodeURIComponent(signUrl)}`,
})
toast.show('请分享链接给客户签名,签名成功后合同将自动生成')
} catch (e) {
console.error(e)
toast.error('操作失败')
}
}
},
})
} else {
toast.success(copyId.value ? '复制创建成功' : '新增成功')
uni.setStorageSync('renewalorder_list_need_refresh', true)
@@ -959,17 +1237,32 @@ onShow(() => {
}
})
/** 初始化 */
onMounted(async () => {
await Promise.all([getStoreList(), getProductList()])
// 新增:默认选择门店第一项(编辑不覆盖)
if ((!props.id && !copyId.value) && !formData.value.storeId && storeList.value.length > 0) {
formData.value.storeId = storeList.value[0].id
// 新增:默认选择当前用户所在门店;车辆购买方默认为当前用户门店名称,均可修改
if (!props.id && !copyId.value && storeList.value.length > 0) {
if (userStore.storeId != null && storeList.value.some((s: StoreVO) => s.id === userStore.storeId)) {
formData.value.storeId = userStore.storeId
formData.value.serviceBuyer = userStore.storeName || storeList.value.find((s: StoreVO) => s.id === userStore.storeId)?.storeName || ''
} else {
if (!formData.value.storeId) formData.value.storeId = storeList.value[0].id
if (!formData.value.serviceBuyer) formData.value.serviceBuyer = userStore.storeName || storeList.value[0].storeName || ''
}
}
// 新增:默认选择证件类型字典第一项(编辑不覆盖)
if ((!props.id && !copyId.value) && !formData.value.certType && certTypeOptions.value.length > 0) {
formData.value.certType = certTypeOptions.value[0].value
}
// 新增:默认选择结算方式为 '00'(编辑不覆盖)
if ((!props.id && !copyId.value) && !formData.value.settlementMethod) {
formData.value.settlementMethod = '00'
}
// 新增:录单人默认当前登录用户名称,可修改
if (!props.id && !copyId.value) {
formData.value.inputUser = userStore.userInfo?.nickname || userStore.userInfo?.username || ''
}
// 如果是编辑模式或复制创建模式,加载详情
if (props.id || copyId.value) {
await getDetail()
@@ -978,6 +1271,10 @@ onMounted(async () => {
</script>
<style lang="scss" scoped>
.invoice-date-picker-value {
font-size: 28rpx;
color: #333;
}
.contract-area {
display: flex;
flex-direction: column;

View File

@@ -20,20 +20,24 @@
<view class="relative p-24rpx" @click="handleDetail(item)">
<view class="flex items-center justify-between mb-16rpx">
<view class="text-32rpx text-[#333] font-semibold">
{{ item.licensePlate || '-' }}
{{ item.serviceBuyer || '-' }}
</view>
<view class="text-24rpx text-[#999]">
{{ item.createTime ? formatDate(item.createTime) : '-' }}
</view>
</view>
<view class="space-y-12rpx text-26rpx text-[#666]">
<view class="flex items-center justify-between">
<text>车牌号</text>
<text>{{ item.licensePlate || '-' }}</text>
</view>
<view class="flex items-center justify-between">
<text>品牌车型</text>
<text>{{ item.carBrand }} {{ item.carModel }}</text>
</view>
<view class="flex items-center justify-between">
<view class="flex items-center justify-between" @click.stop @click="handleEdit(item)">
<text>服务购买方</text>
<text>{{ item.serviceBuyer || '-' }}</text>
<text class="text-[#409eff] underline">{{ item.serviceBuyer || '-' }}</text>
</view>
<view class="flex items-center justify-between">
<text>联系电话</text>
@@ -43,10 +47,18 @@
<text>门店</text>
<text>{{ item.storeName || '-' }}</text>
</view>
<view class="flex items-center justify-between">
<text>产品年限</text>
<text>{{ item.originalWarrantyYears || '-' }}</text>
</view>
<view class="flex items-center justify-between">
<text>产品费用</text>
<text class="text-[#1890ff]">¥{{ item.productFee || 0 }}</text>
</view>
<view class="flex items-center justify-between">
<text>结算方式</text>
<text>{{ getSettlementMethodLabel(item.settlementMethod) }}</text>
</view>
</view>
</view>
<!-- 底部操作按钮 -->
@@ -100,6 +112,8 @@ import { useToast } from 'wot-design-uni'
import { navigateBackPlus } from '@/utils'
import { formatDate } from '@/utils/date'
import SearchForm from './components/search-form.vue'
import { getDictLabel } from '@/hooks/useDict'
import { DICT_TYPE } from '@/utils/constants'
definePage({
style: {
@@ -177,6 +191,19 @@ function handleDetail(item: RenewalOrderVO) {
})
}
/** 编辑订单 */
function handleEdit(item: RenewalOrderVO) {
uni.navigateTo({
url: `/pages/car/renewalorder/form/index?id=${item.id}`,
})
}
/** 获取结算方式字典标签 */
function getSettlementMethodLabel(value?: string) {
if (!value) return '-'
return getDictLabel(DICT_TYPE.CAR_RENEWAL_PAY_METHOD, value) || value
}
/** 复制创建订单:基于原订单预填表单,进入新增模式 */
function handleCopy(item: RenewalOrderVO) {
uni.navigateTo({

View File

@@ -0,0 +1,133 @@
<template>
<view class="yd-page-container sign-share-page">
<view v-if="fullUrl" class="sign-share-content">
<view class="tip-box">
<text class="tip-text">请将下方链接或二维码分享给客户客户打开链接完成签名后合同将自动生成</text>
</view>
<view class="link-box">
<text class="link-label">签名链接</text>
<text class="link-value" selectable>{{ fullUrl }}</text>
</view>
<view class="tip-small">链接有效期 12 小时</view>
<wd-button type="primary" block class="copy-btn" @click="handleCopy">
复制链接
</wd-button>
</view>
<view v-else class="loading-wrap">
<view class="loading-dot" />
<text class="text-28rpx text-[#999] mt-24rpx">加载中...</text>
</view>
</view>
</template>
<script lang="ts" setup>
import { onLoad } from '@dcloudio/uni-app'
import { ref } from 'vue'
import { useToast } from 'wot-design-uni'
import { getEnvBaseUrlRoot } from '@/utils'
definePage({
style: {
navigationBarTitleText: '分享签名',
navigationStyle: 'default',
},
})
const toast = useToast()
const fullUrl = ref('')
function handleCopy() {
if (!fullUrl.value) return
uni.setClipboardData({
data: fullUrl.value,
success: () => {
toast.success('链接已复制')
setTimeout(() => {
// 关闭分享页 + 上一页(新增/编辑/详情),直接回到列表
uni.navigateBack({ delta: 2 })
}, 500)
},
fail: () => {
toast.error('复制失败')
},
})
}
onLoad((options) => {
let signUrl = options?.signUrl || ''
if (signUrl) {
try {
signUrl = decodeURIComponent(signUrl)
} catch {
// 已为明文路径则忽略
}
const base = getEnvBaseUrlRoot()
fullUrl.value = base + (signUrl.startsWith('/') ? signUrl : `/${signUrl}`)
}
})
</script>
<style lang="scss" scoped>
.sign-share-page {
min-height: 100vh;
}
.sign-share-content {
padding: 32rpx;
}
.tip-box {
margin-bottom: 32rpx;
padding: 24rpx;
background: #f0f9ff;
border-radius: 12rpx;
}
.tip-text {
font-size: 28rpx;
color: #333;
line-height: 1.6;
}
.link-box {
margin-bottom: 16rpx;
padding: 24rpx;
background: #f5f5f5;
border-radius: 12rpx;
}
.link-label {
display: block;
font-size: 24rpx;
color: #999;
margin-bottom: 12rpx;
}
.link-value {
font-size: 24rpx;
color: #333;
word-break: break-all;
}
.tip-small {
font-size: 24rpx;
color: #999;
margin-bottom: 32rpx;
}
.copy-btn {
margin-top: 24rpx;
}
.loading-wrap {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 80rpx 0;
}
.loading-dot {
width: 48rpx;
height: 48rpx;
border: 4rpx solid #e5e5e5;
border-top-color: #3b82f6;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
</style>

View File

@@ -55,6 +55,8 @@ const BPM_DICT = {
/** ========== CAR - 车辆模块 ========== */
const CAR_DICT = {
CAR_RENEWAL_PRODUCT_TYPE: 'car_renewal_product_type', // 续保产品类别
CAR_RENEWAL_PAY_METHOD: 'car_renewal_pay_method', // 续保结算方式
CAR_RENEWAL_YEAR: 'car_renewal_year', // 续保生效年限
} as const
/** 字典类型枚举 - 统一导出 */

View File

@@ -123,9 +123,9 @@ export function getEnvBaseUrl() {
// # 有些同学可能需要在微信小程序里面根据 develop、trial、release 分别设置上传地址,参考代码如下。
// TODO @芋艿:这个后续也要调整。
const VITE_SERVER_BASEURL__WEIXIN_DEVELOP = 'http://1.14.158.154:48080/admin-api'
const VITE_SERVER_BASEURL__WEIXIN_TRIAL = 'http://1.14.158.154:48080/admin-api'
const VITE_SERVER_BASEURL__WEIXIN_RELEASE = 'http://1.14.158.154:48080/admin-api'
const VITE_SERVER_BASEURL__WEIXIN_DEVELOP = 'https://api.tuanbanlv.com/admin-api'
const VITE_SERVER_BASEURL__WEIXIN_TRIAL = 'https://api.tuanbanlv.com/admin-api'
const VITE_SERVER_BASEURL__WEIXIN_RELEASE = 'https://api.tuanbanlv.com/admin-api'
// 微信小程序端环境区分
if (isMpWeixin) {
@@ -150,17 +150,18 @@ export function getEnvBaseUrl() {
}
/**
* 根据环境变量,获取基础路径的根路径,比如 http://1.14.158.154:48080
* 根据环境变量,获取基础路径的根路径,比如 https://api.tuanbanlv.com
*
* add by 芋艿:用户类似 websocket 这种需要根路径的场景
* 注意:小程序环境无 URL 构造函数,使用正则解析
*
* @return 根路径
*/
export function getEnvBaseUrlRoot() {
const baseUrl = getEnvBaseUrl()
// 提取根路径
const urlObj = new URL(baseUrl)
return urlObj.origin
// 小程序环境 URL 可能不可用,用正则提取 origin
const match = baseUrl.match(/^(https?:\/\/[^/]+)/)
return match ? match[1] : baseUrl
}
/**

View File

@@ -191,7 +191,7 @@ pnpm build:mp:prod
```env
# 服务端地址
VITE_SERVER_BASEURL=http://1.14.158.154:48080
VITE_SERVER_BASEURL=https://api.tuanbanlv.com
# 应用标题
VITE_APP_TITLE=芋道管理后台