1253 lines
47 KiB
Vue
1253 lines
47 KiB
Vue
<template>
|
||
<Dialog :title="dialogTitle" v-model="dialogVisible" width="1200px">
|
||
<el-form
|
||
ref="formRef"
|
||
:model="formData"
|
||
:rules="formRules"
|
||
label-width="120px"
|
||
v-loading="formLoading"
|
||
>
|
||
<el-collapse v-model="activeNames">
|
||
<el-collapse-item title="车辆信息" name="car">
|
||
<el-row :gutter="20">
|
||
<el-col :span="12">
|
||
<el-form-item label="汽车品牌" prop="carBrand">
|
||
<el-input v-model="formData.carBrand" placeholder="请输入汽车品牌" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="车型" prop="carModel">
|
||
<el-input v-model="formData.carModel" placeholder="请输入车型" />
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
<el-row :gutter="20">
|
||
<el-col :span="12">
|
||
<el-form-item label="车牌号" prop="licensePlate">
|
||
<el-input v-model="formData.licensePlate" placeholder="请输入车牌号" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="厂牌型号" prop="factoryModel">
|
||
<el-input v-model="formData.factoryModel" placeholder="请输入厂牌型号" />
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
<el-row :gutter="20">
|
||
<el-col :span="12">
|
||
<el-form-item label="车架号" prop="vin">
|
||
<el-input v-model="formData.vin" placeholder="请输入车架号" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="发动机号" prop="engineNo">
|
||
<el-input v-model="formData.engineNo" placeholder="请输入发动机号" />
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
<el-row :gutter="20">
|
||
<el-col :span="12">
|
||
<el-form-item label="发票日期" prop="invoiceDate">
|
||
<el-date-picker
|
||
v-model="formData.invoiceDate"
|
||
type="date"
|
||
value-format="YYYY-MM-DD"
|
||
placeholder="选择发票日期"
|
||
style="width: 100%"
|
||
/>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="发票金额" prop="invoiceAmount">
|
||
<el-input-number
|
||
v-model="formData.invoiceAmount"
|
||
:min="0"
|
||
:precision="2"
|
||
:step="0.01"
|
||
controls-position="right"
|
||
placeholder="请输入发票金额"
|
||
style="width: 100%"
|
||
/>
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
<el-row :gutter="20">
|
||
<el-col :span="12">
|
||
<el-form-item label="购买时公里数" prop="purchaseMileage">
|
||
<el-input v-model="formData.purchaseMileage" placeholder="请输入购买时公里数" />
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
<el-row :gutter="20">
|
||
<el-col :span="12">
|
||
<el-form-item label="行驶证" prop="drivingLicenseUrl" required>
|
||
<UploadImg v-model="formData.drivingLicenseUrl" :file-size="10" :file-type="['image/jpeg', 'image/png', 'image/jpg', 'image/gif']" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="合格证" prop="certificateOfConformityUrl">
|
||
<UploadImg v-model="formData.certificateOfConformityUrl" :file-size="10" :file-type="['image/jpeg', 'image/png', 'image/jpg', 'image/gif']" />
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
<el-row :gutter="20">
|
||
<el-col :span="12">
|
||
<el-form-item label="里程表照片" prop="odometerPhotoUrl">
|
||
<UploadImg v-model="formData.odometerPhotoUrl" :file-size="10" :file-type="['image/jpeg', 'image/png', 'image/jpg', 'image/gif']" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="车名牌照片" prop="nameplatePhotoUrl">
|
||
<UploadImg v-model="formData.nameplatePhotoUrl" :file-size="10" :file-type="['image/jpeg', 'image/png', 'image/jpg', 'image/gif']" />
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
<el-row :gutter="20">
|
||
<el-col :span="12">
|
||
<el-form-item label="购车发票" prop="carInvoiceUrls" required>
|
||
<UploadImgs v-model="formData.carInvoiceUrls" :limit="10" :file-size="10" :file-type="['image/jpeg', 'image/png', 'image/jpg', 'image/gif']" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="购置税凭证" prop="purchaseTaxInvoiceUrls">
|
||
<UploadImgs v-model="formData.purchaseTaxInvoiceUrls" :limit="10" :file-size="10" :file-type="['image/jpeg', 'image/png', 'image/jpg', 'image/gif']" />
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
<el-row :gutter="20">
|
||
<el-col :span="12">
|
||
<el-form-item label="商业险保单" prop="businessInsurancePolicyUrls">
|
||
<UploadImgs v-model="formData.businessInsurancePolicyUrls" :limit="10" :file-size="10" :file-type="['image/jpeg', 'image/png', 'image/jpg', 'image/gif']" />
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
</el-collapse-item>
|
||
|
||
<el-collapse-item title="购买方信息" name="buyer">
|
||
<el-row :gutter="20">
|
||
<el-col :span="12">
|
||
<el-form-item label="服务购买方" prop="serviceBuyer">
|
||
<el-input v-model="formData.serviceBuyer" placeholder="请输入服务购买方" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="车辆购买方" prop="carBuyer">
|
||
<el-input v-model="formData.carBuyer" placeholder="请输入车辆购买方" />
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
<el-row :gutter="20">
|
||
<el-col :span="12">
|
||
<el-form-item label="证件类型" prop="certType">
|
||
<el-select v-model="formData.certType" placeholder="请选择证件类型" style="width: 100%">
|
||
<el-option
|
||
v-for="dict in certTypeOptions"
|
||
:key="dict.value"
|
||
:label="dict.label"
|
||
:value="dict.value"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="证件号码" prop="certNo">
|
||
<el-input v-model="formData.certNo" placeholder="请输入证件号码" />
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
<el-row :gutter="20">
|
||
<el-col :span="12">
|
||
<el-form-item label="联系电话" prop="mobile">
|
||
<el-input v-model="formData.mobile" placeholder="请输入联系电话" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="会员邮箱" prop="memberEmail">
|
||
<el-input v-model="formData.memberEmail" placeholder="请输入会员邮箱" />
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
<el-row :gutter="20">
|
||
<el-col :span="24">
|
||
<el-form-item label="联系地址" prop="contactAddress">
|
||
<el-input v-model="formData.contactAddress" placeholder="请输入联系地址" />
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
<el-row :gutter="20">
|
||
<el-col :span="12">
|
||
<el-form-item label="门店" prop="storeId">
|
||
<el-select
|
||
v-model="formData.storeId"
|
||
placeholder="请选择门店"
|
||
filterable
|
||
style="width: 100%"
|
||
@change="handleStoreChange"
|
||
>
|
||
<el-option
|
||
v-for="store in storeList"
|
||
:key="store.id"
|
||
:label="store.storeName"
|
||
:value="store.id"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
<el-row :gutter="20">
|
||
<el-col :span="12">
|
||
<el-form-item label="身份证正面" prop="idCardFrontUrl" required>
|
||
<UploadImg v-model="formData.idCardFrontUrl" :file-size="10" :file-type="['image/jpeg', 'image/png', 'image/jpg', 'image/gif']" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="身份证反面" prop="idCardBackUrl" required>
|
||
<UploadImg v-model="formData.idCardBackUrl" :file-size="10" :file-type="['image/jpeg', 'image/png', 'image/jpg', 'image/gif']" />
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
</el-collapse-item>
|
||
|
||
<el-collapse-item title="产品信息" name="product">
|
||
<el-row :gutter="20">
|
||
<el-col :span="12">
|
||
<el-form-item label="续保产品" prop="productId">
|
||
<el-select
|
||
v-model="formData.productId"
|
||
placeholder="请选择续保产品"
|
||
filterable
|
||
style="width: 100%"
|
||
@change="handleProductChange"
|
||
>
|
||
<el-option
|
||
v-for="product in productList"
|
||
:key="product.id"
|
||
:label="product.productName"
|
||
:value="product.id"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="服务产品" prop="serviceProduct">
|
||
<el-input v-model="formData.serviceProduct" placeholder="选择产品后自动填充" readonly />
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
<el-row :gutter="20">
|
||
<el-col :span="12">
|
||
<el-form-item label="产品类别" prop="productType">
|
||
<el-input :model-value="productTypeLabel" placeholder="选择产品后自动填充" readonly />
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
<el-row :gutter="20">
|
||
<el-col :span="12">
|
||
<el-form-item label="产品时效" prop="productValidity">
|
||
<el-input v-model="formData.productValidity" placeholder="选择产品后自动填充" readonly />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="产品费用" prop="productFee">
|
||
<el-input-number
|
||
v-model="formData.productFee"
|
||
:min="0"
|
||
:precision="2"
|
||
:step="0.01"
|
||
controls-position="right"
|
||
placeholder="请输入产品费用"
|
||
style="width: 100%"
|
||
/>
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
<el-row :gutter="20">
|
||
<el-col v-if="showOriginalWarrantyYears" :span="12">
|
||
<el-form-item label="产品年限" prop="originalWarrantyYears">
|
||
<el-input v-model="formData.originalWarrantyYears" placeholder="选择续保产品后自动带出产品生效年限,可修改" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="showOriginalWarrantyYears ? 12 : 24">
|
||
<el-form-item label="原厂质保里程" prop="originalWarrantyMileage">
|
||
<el-input v-model="formData.originalWarrantyMileage" placeholder="请输入原厂质保里程" />
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
<el-row :gutter="20">
|
||
<el-col :span="12">
|
||
<el-form-item label="结算方式" prop="settlementMethod">
|
||
<el-select
|
||
v-model="formData.settlementMethod"
|
||
placeholder="请选择结算方式"
|
||
style="width: 100%"
|
||
>
|
||
<el-option
|
||
v-for="dict in settlementMethodOptions"
|
||
:key="dict.value"
|
||
:label="dict.label"
|
||
:value="dict.value"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="录单人" prop="inputUser">
|
||
<el-input v-model="formData.inputUser" placeholder="请输入录单人" />
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
</el-collapse-item>
|
||
|
||
<el-collapse-item title="其他信息" name="other">
|
||
<el-row :gutter="20">
|
||
<el-col :span="12">
|
||
<el-form-item
|
||
v-if="showContractComponents"
|
||
label="合同路径"
|
||
prop="contractUrl"
|
||
>
|
||
<div class="flex flex-col gap-10px">
|
||
<div class="flex gap-10px">
|
||
<el-button
|
||
type="primary"
|
||
:loading="onlineSignLoading"
|
||
:disabled="!formData.id"
|
||
@click="handleOpenOnlineSignForContract"
|
||
>
|
||
<Icon icon="ep:view" class="mr-5px" />
|
||
{{ formData.contractUrl ? '重新生成合同' : '生成在线合同' }}
|
||
</el-button>
|
||
</div>
|
||
<div
|
||
v-if="formData.contractUrl"
|
||
class="flex items-center gap-6px text-primary underline cursor-pointer select-none"
|
||
@click="handleContractDownloadClick"
|
||
>
|
||
<span>{{ contractFileName }}</span>
|
||
<Icon icon="ep:download" />
|
||
</div>
|
||
</div>
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item
|
||
v-if="showContractComponents"
|
||
label="合同备注"
|
||
prop="contractRemark"
|
||
>
|
||
<el-input v-model="formData.contractRemark" type="textarea" :rows="3" placeholder="请输入合同备注" />
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
<el-row :gutter="20">
|
||
<el-col :span="24">
|
||
<el-form-item label="备注" prop="remark">
|
||
<el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="请输入备注" />
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
</el-collapse-item>
|
||
</el-collapse>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
|
||
<el-button @click="dialogVisible = false">取 消</el-button>
|
||
</template>
|
||
|
||
<!-- 合同预览弹窗 -->
|
||
<Dialog
|
||
v-model="contractPreviewVisible"
|
||
title="合同预览"
|
||
width="90%"
|
||
:close-on-click-modal="false"
|
||
class="contract-preview-dialog"
|
||
>
|
||
<div v-loading="previewLoading" class="contract-preview-container">
|
||
<div class="contract-preview-content" v-if="contractHtml">
|
||
<div class="contract-html-wrapper" v-html="contractHtmlWithSignature"></div>
|
||
</div>
|
||
<div v-else class="empty-state">
|
||
<el-empty description="暂无合同内容" />
|
||
</div>
|
||
</div>
|
||
<template #footer>
|
||
<el-button @click="handleOpenSignature" :disabled="hasSignature">
|
||
{{ hasSignature ? '已签名' : '客户签名' }}
|
||
</el-button>
|
||
<el-button @click="contractPreviewVisible = false">取消</el-button>
|
||
<el-button
|
||
v-if="hasSignature"
|
||
type="primary"
|
||
@click="handleConfirmGenerateContract"
|
||
>
|
||
确认生成合同
|
||
</el-button>
|
||
<el-button v-else type="info" disabled>
|
||
请先完成客户签名
|
||
</el-button>
|
||
</template>
|
||
</Dialog>
|
||
|
||
<!-- 签名画板 -->
|
||
<SignaturePad v-model="signaturePadVisible" @confirm="handleSignatureConfirm" />
|
||
|
||
<!-- 客户线上签名:二维码 + 复制链接 -->
|
||
<Dialog
|
||
v-model="onlineSignDialogVisible"
|
||
title="客户线上签名"
|
||
width="420px"
|
||
>
|
||
<div v-loading="onlineSignLoading" class="flex flex-col items-center gap-16px">
|
||
<template v-if="onlineSignFullUrl">
|
||
<Qrcode :text="onlineSignFullUrl" :width="220" />
|
||
<p class="text-sm text-gray-600">客户扫码打开签名页,完成签名后将跳转签名成功页</p>
|
||
<div class="w-full flex gap-8px">
|
||
<el-input v-model="onlineSignFullUrl" readonly size="default" />
|
||
</div>
|
||
<p class="text-xs text-gray-400">链接有效期 12 小时</p>
|
||
</template>
|
||
</div>
|
||
<template #footer>
|
||
<el-button type="primary" @click="copyOnlineSignUrlAndClose" :disabled="!onlineSignFullUrl">
|
||
复制
|
||
</el-button>
|
||
</template>
|
||
</Dialog>
|
||
</Dialog>
|
||
</template>
|
||
<script setup lang="ts">
|
||
import { RenewalOrderApi, RenewalOrderVO } from '@/api/car/renewalorder'
|
||
import { RenewalProductApi, RenewalProductVO } from '@/api/car/renewalproduct'
|
||
import { StoreApi, StoreVO } from '@/api/tire/store'
|
||
import UploadImg from '@/components/UploadFile/src/UploadImg.vue'
|
||
import UploadImgs from '@/components/UploadFile/src/UploadImgs.vue'
|
||
import { useUserStore } from '@/store/modules/user'
|
||
import { DICT_TYPE, getStrDictOptions, getDictLabel } from '@/utils/dict'
|
||
import SignaturePad from '@/components/SignaturePad'
|
||
import { Dialog } from '@/components/Dialog'
|
||
import { Qrcode } from '@/components/Qrcode'
|
||
import * as FileApi from '@/api/infra/file'
|
||
import { ElMessageBox } from 'element-plus'
|
||
|
||
/** 车辆订单管理 表单 */
|
||
defineOptions({ name: 'RenewalOrderForm' })
|
||
|
||
const { t } = useI18n() // 国际化
|
||
const message = useMessage() // 消息弹窗
|
||
|
||
const dialogVisible = ref(false) // 弹窗的是否展示
|
||
const dialogTitle = ref('') // 弹窗的标题
|
||
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
|
||
const formType = ref('') // 表单的类型:create - 新增;update - 修改
|
||
const productList = ref<RenewalProductVO[]>([]) // 续保产品列表
|
||
const storeList = ref<StoreVO[]>([]) // 门店列表
|
||
const activeNames = ref(['car', 'buyer', 'product', 'other']) // 折叠面板默认展开项
|
||
const generatingContract = ref(false) // 正在生成合同(保留用于兼容)
|
||
const certTypeOptions = computed(() => getStrDictOptions('car_renewal_identity_type'))
|
||
const settlementMethodOptions = computed(() => getStrDictOptions(DICT_TYPE.CAR_RENEWAL_PAY_METHOD))
|
||
|
||
// 合同预览相关
|
||
const contractPreviewVisible = ref(false)
|
||
const previewLoading = ref(false)
|
||
const contractHtml = ref('')
|
||
const customerSignature = ref('') // 客户签名 base64 或 URL
|
||
const signaturePadVisible = ref(false)
|
||
const hasSignature = ref(false) // 是否已有签名(从订单中读取或新签名)
|
||
/** 客户线上签名弹窗:二维码 + 链接 */
|
||
const onlineSignDialogVisible = ref(false)
|
||
const onlineSignFullUrl = ref('')
|
||
const onlineSignLoading = ref(false)
|
||
const productTypeOptions = computed(() => getStrDictOptions(DICT_TYPE.CAR_RENEWAL_PRODUCT_TYPE))
|
||
// 根据 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'
|
||
})
|
||
// 产品年限:仅当产品类别为 00、02(与合同一致的规定品类)时展示并校验
|
||
const showOriginalWarrantyYears = computed(() => showContractComponents.value)
|
||
const formData = ref({
|
||
id: undefined,
|
||
carBrand: undefined,
|
||
carModel: undefined,
|
||
licensePlate: undefined,
|
||
factoryModel: undefined,
|
||
invoiceAmount: undefined,
|
||
purchaseMileage: undefined,
|
||
engineNo: undefined,
|
||
vin: undefined,
|
||
invoiceDate: undefined,
|
||
invoiceUrl: undefined,
|
||
serviceBuyer: undefined,
|
||
carBuyer: undefined,
|
||
certType: undefined,
|
||
mobile: undefined,
|
||
certNo: undefined,
|
||
contactAddress: undefined,
|
||
memberEmail: undefined,
|
||
storeId: undefined,
|
||
productId: undefined,
|
||
serviceProduct: undefined,
|
||
productType: undefined,
|
||
productValidity: undefined,
|
||
originalWarrantyYears: undefined,
|
||
originalWarrantyMileage: undefined,
|
||
productFee: undefined,
|
||
settlementMethod: '00',
|
||
remark: undefined,
|
||
inputUser: undefined,
|
||
contractRemark: undefined,
|
||
contractUrl: '',
|
||
customerSignatureUrl: undefined,
|
||
idCardFrontUrl: undefined,
|
||
idCardBackUrl: undefined,
|
||
drivingLicenseUrl: undefined,
|
||
carInvoiceUrls: [],
|
||
purchaseTaxInvoiceUrls: [],
|
||
businessInsurancePolicyUrls: [],
|
||
certificateOfConformityUrl: undefined,
|
||
odometerPhotoUrl: undefined,
|
||
nameplatePhotoUrl: undefined
|
||
})
|
||
const formRules = reactive({
|
||
licensePlate: [{ required: true, message: '车牌号不能为空', trigger: 'blur' }],
|
||
vin: [{ required: true, message: '车架号不能为空', trigger: 'blur' }],
|
||
engineNo: [{ required: true, message: '发动机号不能为空', trigger: 'blur' }],
|
||
invoiceDate: [{ required: true, message: '发票日期不能为空', trigger: 'change' }],
|
||
invoiceAmount: [{ required: true, message: '发票金额不能为空', trigger: 'blur' }],
|
||
serviceBuyer: [{ required: true, message: '服务购买方不能为空', trigger: 'blur' }],
|
||
carBuyer: [{ required: true, message: '车辆购买方不能为空', trigger: 'blur' }],
|
||
certType: [{ required: true, message: '证件类型不能为空', trigger: 'change' }],
|
||
certNo: [{ required: true, message: '证件号码不能为空', trigger: 'blur' }],
|
||
mobile: [{ required: true, message: '联系电话不能为空', trigger: 'blur' }],
|
||
storeId: [{ required: true, message: '门店不能为空', trigger: 'change' }],
|
||
productId: [{ required: true, message: '续保产品不能为空', trigger: 'change' }],
|
||
serviceProduct: [{ required: true, message: '服务产品不能为空', trigger: 'blur' }],
|
||
productType: [{ required: true, message: '产品类别不能为空', trigger: 'change' }],
|
||
productValidity: [{ required: true, message: '产品时效不能为空', trigger: 'blur' }],
|
||
productFee: [{ required: true, message: '产品费用不能为空', trigger: 'blur' }],
|
||
settlementMethod: [{ required: true, message: '结算方式不能为空', trigger: 'change' }],
|
||
inputUser: [{ required: true, message: '录单人不能为空', trigger: 'blur' }],
|
||
originalWarrantyYears: [
|
||
{
|
||
validator: (_rule: any, value: any, callback: (e?: Error) => void) => {
|
||
const pt = formData.value.productType
|
||
const need = pt === '00' || pt === '02'
|
||
if (need && (value == null || String(value).trim() === '')) {
|
||
callback(new Error('产品年限不能为空'))
|
||
} else {
|
||
callback()
|
||
}
|
||
},
|
||
trigger: 'blur'
|
||
}
|
||
],
|
||
drivingLicenseUrl: [{ required: true, message: '请上传行驶证', trigger: 'change' }],
|
||
carInvoiceUrls: [{
|
||
required: true,
|
||
validator: (_: any, val: string[] | undefined, cb: (e?: Error) => 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' }]
|
||
})
|
||
const formRef = ref() // 表单 Ref
|
||
|
||
/** 获取门店列表 */
|
||
const getStoreList = async () => {
|
||
try {
|
||
const list = await StoreApi.findByProductType()
|
||
// request 封装通常会返回 data;这里兜底两种情况
|
||
storeList.value = (list as any)?.data ?? (list as any) ?? []
|
||
} catch (error) {
|
||
console.error('获取门店列表失败:', error)
|
||
}
|
||
}
|
||
|
||
/** 选择门店时,服务购买方自动填充为门店名称 */
|
||
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 {
|
||
const list = await RenewalProductApi.findByProductType()
|
||
// 后端返回 CommonResult<List>,request 封装通常会返回 data;这里兜底两种情况
|
||
productList.value = (list as any)?.data ?? (list as any) ?? []
|
||
} catch (error) {
|
||
console.error('获取续保产品列表失败:', error)
|
||
}
|
||
}
|
||
|
||
/** 处理产品选择变化 */
|
||
const handleProductChange = async (productId: number) => {
|
||
if (!productId) {
|
||
formData.value.serviceProduct = undefined
|
||
formData.value.productType = undefined
|
||
formData.value.productValidity = undefined
|
||
formData.value.originalWarrantyYears = undefined
|
||
return
|
||
}
|
||
try {
|
||
const product = await RenewalProductApi.getRenewalProduct(productId)
|
||
// 回显服务产品和产品时效(产品时效 = 产品内容)
|
||
formData.value.serviceProduct = product.productName || ''
|
||
// 回显产品类别
|
||
// 这里直接读取返回的 productType,如果后端是包在 data 里,axios 封装会已解包
|
||
// 为了兼容性,再做一层兜底
|
||
const p: any = product as any
|
||
formData.value.productType = p.productType || (p.data && p.data.productType) || ''
|
||
formData.value.productValidity = product.productContent || ''
|
||
const pt = formData.value.productType
|
||
const needWarrantyYears = pt === '00' || pt === '02'
|
||
if (needWarrantyYears) {
|
||
const eff = p.effectiveYear ?? p.data?.effectiveYear
|
||
if (eff != null && eff !== '') {
|
||
formData.value.originalWarrantyYears = String(eff)
|
||
}
|
||
} else {
|
||
formData.value.originalWarrantyYears = undefined
|
||
}
|
||
} catch (error) {
|
||
console.error('获取产品详情失败:', error)
|
||
message.error('获取产品详情失败')
|
||
}
|
||
}
|
||
|
||
/** 打开弹窗(新增 / 编辑) */
|
||
const open = async (type: string, id?: number) => {
|
||
dialogVisible.value = true
|
||
dialogTitle.value = t('action.' + type)
|
||
formType.value = type
|
||
resetForm()
|
||
// 加载门店列表和产品列表
|
||
await Promise.all([getStoreList(), getProductList()])
|
||
// 新增:默认选择门店第一项,服务购买方默认为门店名称(编辑不覆盖,均可修改)
|
||
if (!id && storeList.value.length > 0) {
|
||
if (!formData.value.storeId) {
|
||
formData.value.storeId = storeList.value[0].id
|
||
}
|
||
if (!formData.value.serviceBuyer && storeList.value[0].storeName) {
|
||
formData.value.serviceBuyer = storeList.value[0].storeName
|
||
}
|
||
}
|
||
// 新增:默认选择证件类型字典第一项(编辑不覆盖)
|
||
if (!id && !formData.value.certType && certTypeOptions.value.length > 0) {
|
||
formData.value.certType = certTypeOptions.value[0].value
|
||
}
|
||
// 新增:默认选择结算方式为 '00'(编辑不覆盖)
|
||
if (!id && !formData.value.settlementMethod) {
|
||
formData.value.settlementMethod = '00'
|
||
}
|
||
// 新增:录单人默认当前登录用户名称,可修改
|
||
if (!id) {
|
||
const userStore = useUserStore()
|
||
formData.value.inputUser = userStore.getUser?.nickname ?? ''
|
||
}
|
||
// 修改时,设置数据
|
||
if (id) {
|
||
formLoading.value = true
|
||
try {
|
||
const data = await RenewalOrderApi.getRenewalOrder(id)
|
||
// 处理发票日期:如果是Date对象或时间戳,转换为字符串格式 YYYY-MM-DD
|
||
if (data.invoiceDate) {
|
||
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')
|
||
data.invoiceDate = `${year}-${month}-${day}`
|
||
} else if (typeof data.invoiceDate === 'number' || typeof data.invoiceDate === 'string') {
|
||
// 如果是时间戳或字符串,尝试转换
|
||
const date = new Date(data.invoiceDate)
|
||
if (!isNaN(date.getTime())) {
|
||
const year = date.getFullYear()
|
||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||
const day = String(date.getDate()).padStart(2, '0')
|
||
data.invoiceDate = `${year}-${month}-${day}`
|
||
}
|
||
}
|
||
}
|
||
// 处理多张图片的 JSON 数组字段:如果后端返回的是字符串,需要解析为数组
|
||
if (data.carInvoiceUrls && typeof data.carInvoiceUrls === 'string') {
|
||
try {
|
||
data.carInvoiceUrls = JSON.parse(data.carInvoiceUrls)
|
||
} catch {
|
||
data.carInvoiceUrls = []
|
||
}
|
||
}
|
||
if (data.purchaseTaxInvoiceUrls && typeof data.purchaseTaxInvoiceUrls === 'string') {
|
||
try {
|
||
data.purchaseTaxInvoiceUrls = JSON.parse(data.purchaseTaxInvoiceUrls)
|
||
} catch {
|
||
data.purchaseTaxInvoiceUrls = []
|
||
}
|
||
}
|
||
if (data.businessInsurancePolicyUrls && typeof data.businessInsurancePolicyUrls === 'string') {
|
||
try {
|
||
data.businessInsurancePolicyUrls = JSON.parse(data.businessInsurancePolicyUrls)
|
||
} catch {
|
||
data.businessInsurancePolicyUrls = []
|
||
}
|
||
}
|
||
formData.value = data
|
||
|
||
// 编辑模式下,如果已有客户签名,初始化签名状态
|
||
if (data.customerSignatureUrl) {
|
||
customerSignature.value = data.customerSignatureUrl
|
||
hasSignature.value = true
|
||
} else {
|
||
customerSignature.value = ''
|
||
hasSignature.value = false
|
||
}
|
||
} finally {
|
||
formLoading.value = false
|
||
}
|
||
}
|
||
}
|
||
/** 复制创建:基于已有订单数据预填,但以“新增”方式保存 */
|
||
const openCopy = async (id: number) => {
|
||
dialogVisible.value = true
|
||
dialogTitle.value = '复制创建订单'
|
||
formType.value = 'create'
|
||
resetForm()
|
||
// 先加载门店和产品列表,保证下拉可用
|
||
await Promise.all([getStoreList(), getProductList()])
|
||
formLoading.value = true
|
||
try {
|
||
const data = await RenewalOrderApi.getRenewalOrder(id)
|
||
// 处理发票日期:如果是Date对象或时间戳,转换为字符串格式 YYYY-MM-DD
|
||
if (data.invoiceDate) {
|
||
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')
|
||
data.invoiceDate = `${year}-${month}-${day}`
|
||
} else if (typeof data.invoiceDate === 'number' || typeof data.invoiceDate === 'string') {
|
||
const date = new Date(data.invoiceDate)
|
||
if (!isNaN(date.getTime())) {
|
||
const year = date.getFullYear()
|
||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||
const day = String(date.getDate()).padStart(2, '0')
|
||
data.invoiceDate = `${year}-${month}-${day}`
|
||
}
|
||
}
|
||
}
|
||
// 处理多张图片的 JSON 数组字段:如果后端返回的是字符串,需要解析为数组
|
||
if (data.carInvoiceUrls && typeof data.carInvoiceUrls === 'string') {
|
||
try {
|
||
data.carInvoiceUrls = JSON.parse(data.carInvoiceUrls)
|
||
} catch {
|
||
data.carInvoiceUrls = []
|
||
}
|
||
}
|
||
if (data.purchaseTaxInvoiceUrls && typeof data.purchaseTaxInvoiceUrls === 'string') {
|
||
try {
|
||
data.purchaseTaxInvoiceUrls = JSON.parse(data.purchaseTaxInvoiceUrls)
|
||
} catch {
|
||
data.purchaseTaxInvoiceUrls = []
|
||
}
|
||
}
|
||
if (data.businessInsurancePolicyUrls && typeof data.businessInsurancePolicyUrls === 'string') {
|
||
try {
|
||
data.businessInsurancePolicyUrls = JSON.parse(data.businessInsurancePolicyUrls)
|
||
} catch {
|
||
data.businessInsurancePolicyUrls = []
|
||
}
|
||
}
|
||
// 清除主键和合同路径等与原订单绑定的信息
|
||
const copyData: any = {
|
||
...data,
|
||
id: undefined,
|
||
contractUrl: '',
|
||
}
|
||
formData.value = copyData
|
||
} finally {
|
||
formLoading.value = false
|
||
}
|
||
}
|
||
/** 打开弹窗并预填识别结果(身份证 / 车辆购置发票) */
|
||
const openWithRecognizedData = async (recognizedData: Record<string, any>) => {
|
||
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 事件,用于操作成功后的回调
|
||
const submitForm = async () => {
|
||
await formRef.value.validate()
|
||
formLoading.value = true
|
||
try {
|
||
const payload = { ...formData.value } as unknown as RenewalOrderVO
|
||
if (payload.productType !== '00' && payload.productType !== '02') {
|
||
payload.originalWarrantyYears = undefined as any
|
||
}
|
||
if (formType.value === 'create') {
|
||
const res = await RenewalOrderApi.createRenewalOrder(payload) as any
|
||
const newId = res?.data ?? res
|
||
if (newId) formData.value.id = newId
|
||
if ((payload.productType === '00' || payload.productType === '02') && newId) {
|
||
message.success('订单已创建,请让客户扫码完成签名后自动生成合同')
|
||
await openOnlineSignForOrder(newId)
|
||
return
|
||
}
|
||
message.success(t('common.createSuccess'))
|
||
} else {
|
||
await RenewalOrderApi.updateRenewalOrder(payload)
|
||
message.success(t('common.updateSuccess'))
|
||
}
|
||
dialogVisible.value = false
|
||
emit('success')
|
||
} finally {
|
||
formLoading.value = false
|
||
}
|
||
}
|
||
|
||
/** 为指定订单打开线上签名弹窗(二维码 + 链接),用于创建订单后或生成/重新生成合同 */
|
||
const openOnlineSignForOrder = async (orderId: number) => {
|
||
onlineSignFullUrl.value = ''
|
||
onlineSignDialogVisible.value = true
|
||
onlineSignLoading.value = true
|
||
try {
|
||
const res = await RenewalOrderApi.createSignToken(orderId) as any
|
||
const signUrl = res?.data?.signUrl ?? res?.signUrl
|
||
const baseUrl = import.meta.env.VITE_BASE_URL || (typeof window !== 'undefined' ? window.location.origin : '')
|
||
const fullUrl = (baseUrl.replace(/\/$/, '') + (signUrl?.startsWith('/') ? '' : '/') + signUrl) || ''
|
||
if (fullUrl) {
|
||
onlineSignFullUrl.value = fullUrl
|
||
message.success('请让客户扫码或复制链接完成签名,签名成功后自动生成合同并跳转成功页')
|
||
} else {
|
||
message.warning('生成签名链接失败')
|
||
onlineSignDialogVisible.value = false
|
||
}
|
||
} catch (e) {
|
||
message.error('创建线上签名失败')
|
||
onlineSignDialogVisible.value = false
|
||
console.error(e)
|
||
} finally {
|
||
onlineSignLoading.value = false
|
||
}
|
||
}
|
||
|
||
/** 生成/重新生成合同:直接弹出二维码;二维码显示时若有合同则清空合同与签名 */
|
||
const handleOpenOnlineSignForContract = async () => {
|
||
if (formData.value.productType !== '00' && formData.value.productType !== '02') {
|
||
message.warning('当前产品类别不支持生成合同,仅产品类别为 00 或 02 时可生成合同')
|
||
return
|
||
}
|
||
const id = formData.value.id as number
|
||
if (!id) {
|
||
message.warning('请先保存订单后再生成合同')
|
||
return
|
||
}
|
||
try {
|
||
const hadContract = !!formData.value.contractUrl
|
||
await openOnlineSignForOrder(id)
|
||
// 在二维码显示的时候清空合同路径与签名
|
||
if (hadContract) {
|
||
await RenewalOrderApi.clearContractAndSignature(id)
|
||
formData.value.contractUrl = ''
|
||
formData.value.customerSignatureUrl = undefined
|
||
message.success('已清空合同与签名,请让客户扫码重新签名')
|
||
}
|
||
} catch (e) {
|
||
message.error('操作失败')
|
||
console.error(e)
|
||
}
|
||
}
|
||
|
||
/** 重置表单 */
|
||
const resetForm = () => {
|
||
formData.value = {
|
||
id: undefined,
|
||
carBrand: undefined,
|
||
carModel: undefined,
|
||
licensePlate: undefined,
|
||
factoryModel: undefined,
|
||
invoiceAmount: undefined,
|
||
purchaseMileage: undefined,
|
||
engineNo: undefined,
|
||
vin: undefined,
|
||
invoiceDate: undefined,
|
||
invoiceUrl: undefined,
|
||
serviceBuyer: undefined,
|
||
carBuyer: undefined,
|
||
certType: undefined,
|
||
mobile: undefined,
|
||
certNo: undefined,
|
||
contactAddress: undefined,
|
||
memberEmail: undefined,
|
||
storeId: undefined,
|
||
productId: undefined,
|
||
serviceProduct: undefined,
|
||
productType: undefined,
|
||
productValidity: undefined,
|
||
originalWarrantyYears: undefined,
|
||
originalWarrantyMileage: undefined,
|
||
productFee: undefined,
|
||
settlementMethod: '00',
|
||
remark: undefined,
|
||
inputUser: undefined,
|
||
contractRemark: undefined,
|
||
contractUrl: '',
|
||
customerSignatureUrl: undefined,
|
||
idCardFrontUrl: undefined,
|
||
idCardBackUrl: undefined,
|
||
drivingLicenseUrl: undefined,
|
||
carInvoiceUrls: [],
|
||
purchaseTaxInvoiceUrls: [],
|
||
businessInsurancePolicyUrls: [],
|
||
certificateOfConformityUrl: undefined,
|
||
odometerPhotoUrl: undefined,
|
||
nameplatePhotoUrl: undefined
|
||
}
|
||
formRef.value?.resetFields()
|
||
// 重置合同预览相关状态(但保留已加载的签名状态,如果是编辑模式)
|
||
if (formType.value === 'create') {
|
||
contractHtml.value = ''
|
||
customerSignature.value = ''
|
||
hasSignature.value = false
|
||
contractPreviewVisible.value = false
|
||
}
|
||
}
|
||
|
||
/** 处理合同下载并打开 */
|
||
const getFileNameFromUrl = (url: string) => {
|
||
try {
|
||
const pathname = new URL(url).pathname
|
||
const raw = pathname.split('/').pop() || '合同.pdf'
|
||
return decodeURIComponent(raw)
|
||
} catch {
|
||
const raw = url.split('/').pop() || '合同.pdf'
|
||
try {
|
||
return decodeURIComponent(raw)
|
||
} catch {
|
||
return raw
|
||
}
|
||
}
|
||
}
|
||
|
||
const contractFileName = computed(() => {
|
||
return formData.value.contractUrl ? getFileNameFromUrl(formData.value.contractUrl) : '合同.pdf'
|
||
})
|
||
|
||
const handleContractDownloadClick = async () => {
|
||
if (!formData.value.contractUrl) {
|
||
message.warning('合同路径为空')
|
||
return
|
||
}
|
||
try {
|
||
const url = formData.value.contractUrl
|
||
const filename = contractFileName.value || '合同.pdf'
|
||
|
||
// 1) 先尝试 fetch 下载为 blob,触发浏览器下载(文件名更可控)
|
||
try {
|
||
const res = await fetch(url)
|
||
const blob = await res.blob()
|
||
const objUrl = URL.createObjectURL(blob)
|
||
const a = document.createElement('a')
|
||
a.href = objUrl
|
||
a.download = filename
|
||
document.body.appendChild(a)
|
||
a.click()
|
||
a.remove()
|
||
setTimeout(() => URL.revokeObjectURL(objUrl), 60_000)
|
||
} catch (e) {
|
||
console.warn('fetch 下载失败,降级为直接打开链接', e)
|
||
}
|
||
|
||
// 2) 再打开新窗口预览 PDF
|
||
window.open(url, '_blank')
|
||
} catch (e) {
|
||
message.error('下载合同失败')
|
||
console.error('下载合同失败', e)
|
||
}
|
||
}
|
||
|
||
// 计算带签名的合同HTML(仅在有签名时替换后端整块签章区,避免重复渲染)
|
||
const contractHtmlWithSignature = computed(() => {
|
||
if (!contractHtml.value) return ''
|
||
|
||
let html = contractHtml.value
|
||
|
||
// 如果已有签名,用前端签章区整体替换后端的签章区,避免出现两份
|
||
if (customerSignature.value) {
|
||
const signatureHtml = `
|
||
<div class="sign-area" style="margin-top: 10px; padding: 20px 0; border-top: 1px solid #e4e7ed;">
|
||
<table style="width: 100%; border: none; border-collapse: collapse;">
|
||
<tr>
|
||
<td style="width: 52%; text-align: right; padding-left: 24px; vertical-align: middle; border: none;">
|
||
<div style="display: inline-block; text-align: left; min-width: 240px;">
|
||
<div style="position: relative; margin-top: 6px; font-size: 11pt;">
|
||
<span style="display: inline-block; width: 64px; font-weight: 700;">服务商:</span>
|
||
<span style="display: inline-block; width: 172px; border-bottom: 1px solid #111; height: 14px; vertical-align: bottom;"></span>
|
||
</div>
|
||
<div style="position: relative; margin-top: 6px; font-size: 11pt;">
|
||
<span style="display: inline-block; width: 64px; font-weight: 700;">签章:</span>
|
||
<span style="display: inline-block; width: 172px; border-bottom: 1px solid #111; height: 14px; vertical-align: bottom;"></span>
|
||
</div>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
</table>
|
||
</div>
|
||
`
|
||
// 匹配整块签章区:<div class="sign-area">...<table>...</table></div>,整块替换避免残留导致两份
|
||
const signAreaRegex = /<div\s+class="sign-area"[^>]*>\s*<table[\s\S]*?<\/table>\s*<\/div>/i
|
||
html = html.replace(signAreaRegex, signatureHtml)
|
||
}
|
||
|
||
return html
|
||
})
|
||
|
||
/** 复制线上签名链接(仅复制) */
|
||
const copyOnlineSignUrl = async () => {
|
||
if (!onlineSignFullUrl.value) return
|
||
try {
|
||
await navigator.clipboard.writeText(onlineSignFullUrl.value)
|
||
message.success('链接已复制到剪贴板')
|
||
} catch {
|
||
message.error('复制失败')
|
||
}
|
||
}
|
||
|
||
/** 复制链接并关闭二维码弹窗和详情页 */
|
||
const copyOnlineSignUrlAndClose = async () => {
|
||
if (!onlineSignFullUrl.value) return
|
||
try {
|
||
await navigator.clipboard.writeText(onlineSignFullUrl.value)
|
||
message.success('链接已复制到剪贴板')
|
||
onlineSignDialogVisible.value = false
|
||
dialogVisible.value = false
|
||
emit('success')
|
||
} catch {
|
||
message.error('复制失败')
|
||
}
|
||
}
|
||
|
||
/** 合同预览(本机签名流程使用,仅当从其他入口打开预览时使用) */
|
||
const doPreviewContract = async () => {
|
||
contractPreviewVisible.value = true
|
||
previewLoading.value = true
|
||
try {
|
||
// 获取合同HTML
|
||
const html = await RenewalOrderApi.generateContractHtml(formData.value.id as unknown as number)
|
||
// 后端返回的可能是 CommonResult<string>,需要提取 data
|
||
contractHtml.value = (html as any)?.data ?? (html as any) ?? ''
|
||
|
||
// 获取订单信息,检查是否已有客户签名
|
||
try {
|
||
const orderData = await RenewalOrderApi.getRenewalOrder(formData.value.id as unknown as number)
|
||
if (orderData.customerSignatureUrl) {
|
||
// 订单中已有签名,加载显示
|
||
customerSignature.value = orderData.customerSignatureUrl
|
||
hasSignature.value = true
|
||
} else {
|
||
// 订单中没有签名,重置状态
|
||
customerSignature.value = ''
|
||
hasSignature.value = false
|
||
}
|
||
} catch (error) {
|
||
console.warn('获取订单签名信息失败:', error)
|
||
hasSignature.value = false
|
||
}
|
||
} catch (error) {
|
||
console.error('获取合同预览失败:', error)
|
||
message.error('获取合同预览失败')
|
||
} finally {
|
||
previewLoading.value = false
|
||
}
|
||
}
|
||
|
||
/** 打开签名画板 */
|
||
const handleOpenSignature = () => {
|
||
signaturePadVisible.value = true
|
||
}
|
||
|
||
/** 签名确认 */
|
||
const handleSignatureConfirm = async (signatureDataUrl: string) => {
|
||
// 先保存 base64 用于预览显示
|
||
customerSignature.value = signatureDataUrl
|
||
|
||
// 将 base64 转换为文件并上传
|
||
try {
|
||
const signatureUrl = await uploadSignatureImage(signatureDataUrl)
|
||
|
||
// 更新订单的客户签名
|
||
if (formData.value.id && signatureUrl) {
|
||
try {
|
||
const orderData = await RenewalOrderApi.getRenewalOrder(formData.value.id as unknown as number)
|
||
const updateData: RenewalOrderVO = {
|
||
...orderData,
|
||
customerSignatureUrl: signatureUrl
|
||
}
|
||
await RenewalOrderApi.updateRenewalOrder(updateData)
|
||
|
||
// 签名保存成功后,更新为URL(用于后续预览)
|
||
customerSignature.value = signatureUrl
|
||
// 标记已有签名,允许生成合同
|
||
hasSignature.value = true
|
||
// 更新表单数据中的签名URL
|
||
formData.value.customerSignatureUrl = signatureUrl
|
||
|
||
message.success('签名已保存并更新到订单,现在可以生成合同了')
|
||
} catch (error) {
|
||
console.error('更新订单签名失败:', error)
|
||
message.warning('签名已保存,但更新订单失败,请手动更新')
|
||
// 即使更新失败,也标记为已有签名(因为已经上传了)
|
||
hasSignature.value = true
|
||
}
|
||
} else {
|
||
message.success('签名已保存')
|
||
// 标记为已有签名
|
||
hasSignature.value = true
|
||
}
|
||
} catch (error) {
|
||
console.error('上传签名失败:', error)
|
||
message.error('上传签名失败')
|
||
// 上传失败,重置状态
|
||
customerSignature.value = ''
|
||
hasSignature.value = false
|
||
}
|
||
}
|
||
|
||
/** 上传签名图片 */
|
||
const uploadSignatureImage = async (base64DataUrl: string): Promise<string> => {
|
||
// 将 base64 转换为 Blob
|
||
const base64Data = base64DataUrl.split(',')[1]
|
||
const byteCharacters = atob(base64Data)
|
||
const byteNumbers = new Array(byteCharacters.length)
|
||
for (let i = 0; i < byteCharacters.length; i++) {
|
||
byteNumbers[i] = byteCharacters.charCodeAt(i)
|
||
}
|
||
const byteArray = new Uint8Array(byteNumbers)
|
||
const blob = new Blob([byteArray], { type: 'image/png' })
|
||
|
||
// 创建 File 对象
|
||
const fileName = `signature_${Date.now()}.png`
|
||
const file = new File([blob], fileName, { type: 'image/png' })
|
||
|
||
// 上传文件
|
||
const res = await FileApi.updateFile({ file })
|
||
// 返回文件URL
|
||
return (res as any)?.data ?? (res as any)?.url ?? ''
|
||
}
|
||
|
||
/** 确认生成合同 */
|
||
const handleConfirmGenerateContract = async () => {
|
||
if (!formData.value.id) {
|
||
message.warning('订单ID不存在')
|
||
return
|
||
}
|
||
|
||
// 再次检查是否有签名(双重保险)
|
||
if (!hasSignature.value || !customerSignature.value) {
|
||
message.warning('请先完成客户签名')
|
||
return
|
||
}
|
||
|
||
try {
|
||
// 确认弹窗
|
||
await ElMessageBox.confirm('确定要生成合同吗?生成后将无法修改。', '提示', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning'
|
||
})
|
||
|
||
// 生成合同(签名已经在 handleSignatureConfirm 中保存到订单了)
|
||
previewLoading.value = true
|
||
try {
|
||
const url = await RenewalOrderApi.generateContract(formData.value.id as unknown as number)
|
||
const contractUrl = (url as any)?.data ?? (url as any)
|
||
|
||
// 更新表单数据中的合同URL
|
||
formData.value.contractUrl = contractUrl
|
||
|
||
message.success('合同已生成')
|
||
contractPreviewVisible.value = false
|
||
contractHtml.value = ''
|
||
customerSignature.value = ''
|
||
hasSignature.value = false
|
||
dialogVisible.value = false
|
||
emit('success')
|
||
} catch (error) {
|
||
console.error('生成合同失败:', error)
|
||
message.error('生成合同失败')
|
||
} finally {
|
||
previewLoading.value = false
|
||
}
|
||
} catch {
|
||
// 用户取消
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.contract-preview-container {
|
||
min-height: 400px;
|
||
max-height: 70vh;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.contract-preview-content {
|
||
background: #fff;
|
||
padding: 20px;
|
||
border: 1px solid #e4e7ed;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.contract-html-wrapper {
|
||
:deep(img) {
|
||
max-width: 100%;
|
||
height: auto;
|
||
}
|
||
|
||
:deep(table) {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
}
|
||
|
||
:deep(table td),
|
||
:deep(table th) {
|
||
border: 1px solid #ddd;
|
||
padding: 8px;
|
||
}
|
||
}
|
||
|
||
.empty-state {
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
min-height: 400px;
|
||
}
|
||
</style>
|