This commit is contained in:
zlwu committed 2026-07-12 12:04:43 +08:00
1 parent 7b1d4c8729
commit ffc89d9b57
75 files changed
+12263 -7950

No files matched your search

+140 -6
View File
@@ -2,7 +2,7 @@
<el-dialog
title="选择客户"
v-model="dialogVisible"
width="1280px"
width="1500px"
align-center
class="dialog-form-layout"
modal-class="order-module-dialog-mask"
@@ -39,6 +39,7 @@
<div class="panel dialog-list-panel">
<div class="list-body list-wrap dialog-list-body-only">
<el-table
ref="tableRef"
v-loading="loading"
:data="customerList"
class="list-table"
@@ -48,7 +49,13 @@
:max-height="400"
>
<el-table-column type="index" label="序号" min-width="60" align="center" header-align="center" />
<el-table-column label="客户名称" prop="customerName" min-width="230" align="center" header-align="center" :show-overflow-tooltip="true" />
<el-table-column label="客户名称" prop="customerName" min-width="200" align="center" header-align="center" :show-overflow-tooltip="true" />
<el-table-column label="客户简称" prop="shortName" min-width="120" align="center" header-align="center" :show-overflow-tooltip="true">
<template #default="scope">
<span v-if="hasShortName(scope.row)">{{ scope.row.shortName }}</span>
<el-tag v-else type="warning" size="small">未填写</el-tag>
</template>
</el-table-column>
<el-table-column label="客户类型" prop="customerType" min-width="150" align="center" header-align="center">
<template #default="scope">
<DictTag dict-type="crm_customer_type" :value="scope.row.customerType" />
@@ -72,6 +79,13 @@
</template>
</el-table-column>
<el-table-column label="负责人" prop="ownerUserName" min-width="100" align="center" header-align="center" :show-overflow-tooltip="true" />
<el-table-column label="操作" fixed="right" min-width="120" align="center" header-align="center" class-name="col-actions">
<template #default="scope">
<el-button link type="primary" @click.stop="handleEditShortName(scope.row)">
<el-icon><Edit /></el-icon>{{ hasShortName(scope.row) ? '编辑简称' : '填写简称' }}
</el-button>
</template>
</el-table-column>
</el-table>
<ListPagination
@@ -90,6 +104,7 @@
<div class="selected-customer">
<el-descriptions :column="2" border>
<el-descriptions-item label="客户名称">{{ currentCustomer.customerName }}</el-descriptions-item>
<el-descriptions-item label="客户简称">{{ currentCustomer.shortName }}</el-descriptions-item>
<el-descriptions-item label="客户类型">
<DictTag dict-type="crm_customer_type" :value="currentCustomer.customerType" />
</el-descriptions-item>
@@ -110,7 +125,55 @@
<el-button class="btn-ghost" @click="handleClose">
<el-icon><Close /></el-icon>取消
</el-button>
<el-button class="btn-main" type="primary" @click="handleConfirm" :disabled="!currentCustomer">
<el-button
class="btn-main"
type="primary"
@click="handleConfirm"
:disabled="!currentCustomer || !hasShortName(currentCustomer)"
>
<el-icon><Check /></el-icon>确定
</el-button>
</div>
</template>
</el-dialog>
<!-- 填写/编辑客户简称 -->
<el-dialog
v-model="shortNameDialogVisible"
:title="hasShortName(shortNameForm) ? '编辑客户简称' : '填写客户简称'"
width="480px"
align-center
class="dialog-form-layout"
modal-class="order-module-dialog-mask"
:close-on-click-modal="false"
append-to-body
>
<el-form
ref="shortNameFormRef"
:model="shortNameForm"
:rules="shortNameRules"
label-position="top"
class="crm-dialog-form"
>
<el-form-item label="客户名称">
<el-input :model-value="shortNameForm.customerName" disabled />
</el-form-item>
<el-form-item label="客户简称" prop="shortName">
<el-input
v-model="shortNameForm.shortName"
placeholder="请输入客户简称"
maxlength="100"
show-word-limit
clearable
/>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button class="btn-ghost" @click="shortNameDialogVisible = false">
<el-icon><Close /></el-icon>取消
</el-button>
<el-button class="btn-main" type="primary" :loading="shortNameSaving" @click="handleSaveShortName">
<el-icon><Check /></el-icon>确定
</el-button>
</div>
@@ -122,10 +185,10 @@
import { useDictStore } from '@/store/modules/dict'
import DictTag from '@/components/DictTag/index.vue'
import ListPagination from '@/components/ListPagination/index.vue'
import { Check, Close, Refresh, Search } from '@element-plus/icons-vue'
import { Check, Close, Edit, Refresh, Search } from '@element-plus/icons-vue'
import { ref, watch } from 'vue'
import type { ElFormInstance } from 'element-plus'
import { fetchCustomerList } from '@/api/crm/customer'
import type { ElFormInstance, FormItemRule } from 'element-plus'
import { fetchCustomerList, fetchUpdateCustomer } from '@/api/crm/customer'
const dictStore = useDictStore()
@@ -167,6 +230,21 @@ const loading = ref(false)
const total = ref(0)
const currentCustomer = ref<any | null>(null)
const queryFormRef = ref<ElFormInstance>()
const tableRef = ref<any>()
const shortNameDialogVisible = ref(false)
const shortNameSaving = ref(false)
const shortNameFormRef = ref<ElFormInstance>()
const shortNameForm = ref({
id: '' as string | number,
customerName: '',
shortName: ''
})
const shortNameRules: Record<string, FormItemRule[]> = {
shortName: [{ required: true, message: '客户简称不能为空', trigger: 'blur' }]
}
const hasShortName = (row?: { shortName?: string } | null) => Boolean(String(row?.shortName || '').trim())
const getList = async () => {
loading.value = true
@@ -213,10 +291,66 @@ watchDebounced(
)
const handleCurrentChange = (row: any) => {
if (!row) {
currentCustomer.value = null
return
}
if (!hasShortName(row)) {
ElMessage.warning('该客户尚未填写简称,请先填写客户简称后再选择')
tableRef.value?.setCurrentRow(undefined)
currentCustomer.value = null
return
}
currentCustomer.value = row
}
const handleEditShortName = (row: any) => {
shortNameForm.value = {
id: row.id,
customerName: row.customerName || '',
shortName: row.shortName || ''
}
shortNameDialogVisible.value = true
nextTick(() => shortNameFormRef.value?.clearValidate())
}
const handleSaveShortName = async () => {
const valid = await shortNameFormRef.value?.validate().catch(() => false)
if (!valid) return
shortNameSaving.value = true
try {
const trimmedShortName = shortNameForm.value.shortName.trim()
await fetchUpdateCustomer({
id: shortNameForm.value.id,
shortName: trimmedShortName
})
ElMessage.success('客户简称保存成功')
const targetId = shortNameForm.value.id
const updatedRow = customerList.value.find((item) => item.id === targetId)
if (updatedRow) {
updatedRow.shortName = trimmedShortName
}
if (currentCustomer.value?.id === targetId) {
currentCustomer.value = { ...currentCustomer.value, shortName: trimmedShortName }
}
shortNameDialogVisible.value = false
} catch (error) {
console.error('保存客户简称失败:', error)
ElMessage.error('保存客户简称失败')
} finally {
shortNameSaving.value = false
}
}
const handleConfirm = () => {
if (!currentCustomer.value) return
if (!hasShortName(currentCustomer.value)) {
ElMessage.warning('该客户尚未填写简称,请先填写客户简称后再选择')
return
}
emit('confirm', currentCustomer.value)
handleClose()
}
@@ -54,6 +54,9 @@
</span>
</div>
</template>
<span v-if="getCategoryLabel(file)" class="attachment-preview__category">
{{ getCategoryLabel(file) }}
</span>
</div>
<button
v-if="compact && !disabled"
@@ -165,6 +168,8 @@ const props = withDefaults(
disabled?: boolean
/** 紧凑模式:小缩略图,只读时隐藏文件名与操作栏 */
compact?: boolean
/** 附件 URL → 分类名称,用于查看态在缩略图上展示分类标签 */
categoryLabels?: Record<string, string>
}>(),
{
modelValue: '',
@@ -173,7 +178,8 @@ const props = withDefaults(
accept: 'image/*,video/*,.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt,.zip,.rar',
hint: '支持图片、视频、PDF、Word、Excel等文件',
disabled: false,
compact: false
compact: false,
categoryLabels: () => ({})
}
)
@@ -269,6 +275,11 @@ function getFileExtension(fileName: string) {
return fileName.substring(lastDotIndex + 1).toUpperCase()
}
function getCategoryLabel(file: AttachmentItem) {
const label = props.categoryLabels?.[file.fileUrl]
return label ? String(label).trim() : ''
}
function canPreview(file: AttachmentItem) {
return Boolean(file.fileUrl) && (isImageFile(file) || isVideoFile(file) || isPdfFile(file))
}
@@ -420,6 +431,7 @@ defineExpose({ hasUploading })
}
.attachment-item {
flex: 0 0 auto;
width: 140px;
border: 1px solid #e2e8f0;
border-radius: 8px;
@@ -438,6 +450,7 @@ defineExpose({ hasUploading })
}
.attachment-preview {
position: relative;
width: 100%;
height: 96px;
display: flex;
@@ -490,6 +503,21 @@ defineExpose({ hasUploading })
color: #1e293b;
}
.attachment-preview__category {
position: absolute;
right: 0;
bottom: 0;
left: 0;
z-index: 1;
padding: 2px 4px;
background: rgba(15, 23, 42, 0.62);
color: #fff;
font-size: 11px;
line-height: 1.3;
text-align: center;
word-break: keep-all;
}
.attachment-footer {
display: flex;
flex-direction: column;
@@ -565,7 +593,8 @@ defineExpose({ hasUploading })
}
:deep(.el-upload) {
display: block;
display: inline-flex;
flex: 0 0 auto;
}
.attachment-upload--compact {
@@ -204,7 +204,7 @@
调整运输时现场链接将轮换更新,旧现场链接将失效。
</template>
<template v-else-if="showDibangHint">
勾选后将按产品进行地磅单生成(当月已有则复用),并在地磅出入库管理中产生对应记录。
勾选后将按产品进行地磅单生成(当月已有则复用);入库单日期取送达时间,出库单日期取取货时间,未填时回退计划日期。
</template>
</p>
</div>
@@ -1,12 +1,12 @@
<template>
<div class="view-kv-table-cell">
<el-tag v-if="field.tagType" :type="field.tagType" size="small">{{
<el-tag v-if="field.tagType" :type="field.tagType" size="small" class="view-kv-tag">{{
formatViewValue(field.value)
}}</el-tag>
<ul v-else-if="field.items?.length" class="view-plain-list">
<li v-for="(item, idx) in field.items" :key="idx">{{ item }}</li>
</ul>
<span v-else :class="{ 'money-display': field.money, 'muted-display': isMuted }">{{
<span v-else :class="{ 'money-display': field.money, 'danger-display': field.danger, 'muted-display': isMuted }">{{
formatViewValue(field.value)
}}</span>
<el-button
@@ -49,7 +49,7 @@
const onFieldView = inject<((field: ViewField) => void) | undefined>(VIEW_KV_FIELD_VIEW_KEY)
const isMuted = computed(
() => !props.field.money && formatViewValue(props.field.value) === '—'
() => !props.field.money && !props.field.danger && formatViewValue(props.field.value) === '—'
)
const showViewBtn = computed(() => {
@@ -91,6 +91,10 @@
gap: 6px;
}
.view-kv-tag {
font-weight: 600;
}
.view-kv-field-link {
margin: 0;
padding: 0 4px;
@@ -112,4 +116,9 @@
color: var(--text-muted, #64748b);
font-weight: 600;
}
.danger-display {
color: #dc2626;
font-weight: 600;
}
</style>
@@ -18,6 +18,8 @@ export interface ViewField {
fullSpan?: boolean
tagType?: TagType
money?: boolean
/** 红色加粗强调展示(如结算方式等关键业务字段) */
danger?: boolean
/** 值后展示「查看」按钮,点击时 ViewKvTable 抛出 field-view */
viewTarget?: 'customer' | 'opportunity' | 'quote'
viewId?: string | number | null
@@ -141,14 +141,16 @@ export function isHazardCertReady(plan: { hazardCertReady?: number } | null | un
return Number(plan?.hazardCertReady ?? 0) === 1
}
/** 含危化品且尚未人工确认证书齐备时,阻断安排运输 */
/** 含危化品且尚未确认证书齐备(人工确认或全部所需证书已上传)时,阻断安排运输 */
export function shouldBlockTransportForHazardCert(
plan: { hazardCertReady?: number; products?: PlanProduct[] } | null | undefined,
products?: PlanProduct[]
) {
const prods = products ?? plan?.products
if (!planHasHazardProducts(prods)) return false
return !isHazardCertReady(plan)
if (isHazardCertReady(plan)) return false
if (!findMissingHazardCerts(prods ?? []).length) return false
return true
}
export function buildMissingHazardCertReadyMessage(
@@ -156,7 +158,10 @@ export function buildMissingHazardCertReadyMessage(
products?: PlanProduct[]
) {
if (!shouldBlockTransportForHazardCert(plan, products)) return ''
return '该计划危化品证书尚未确认齐备'
const prods = products ?? plan?.products ?? []
const missingCertsMsg = buildMissingHazardCertMessage(prods)
if (missingCertsMsg) return missingCertsMsg
return '该计划危化品证书尚未确认齐备,请点击「证书齐备」或补全所需证书'
}
function formatMissingCertLabels(product: PlanProduct): string {
+22 -2
View File
@@ -280,9 +280,29 @@ async function handleDynamicRoutes(
// 2. 获取菜单数据
const menuList = await menuProcessor.getMenuList()
// 3. 验证菜单数据
// 3. 无菜单权限(如未绑定角色):允许空菜单并完成初始化,跳转 403
if (!menuProcessor.validateMenuList(menuList)) {
throw new Error('获取菜单列表失败,请重新登录')
routeRegistry?.register([])
const menuStore = useMenuStore()
menuStore.setMenuList([])
menuStore.addRemoveRouteFns(routeRegistry?.getRemoveRouteFns() || [])
IframeRouteManager.getInstance().save()
useWorktabStore().validateWorktabs(router)
routeInitInProgress = false
closeLoading()
if (isStaticRoute(to.path)) {
next({
path: to.path,
query: to.query,
hash: to.hash,
replace: true
})
return
}
next({ path: '/403', replace: true })
return
}
// 4. 注册动态路由
@@ -193,8 +193,9 @@
if (c.mobile) parts.push(c.mobile)
if (c.status) {
parts.push(dictStore.getLabel('crm_clue_status', c.status))
} else if (c.isConverted === 1) {
parts.push('已转化')
}
if (c.isConverted === 1) parts.push('已转化')
return parts.join(' · ')
})
@@ -338,7 +339,6 @@
}
const handleInvalidSuccess = async () => {
ElMessage.success('标记无效成功')
await loadDetail()
emit('success')
}
@@ -350,7 +350,6 @@
}
const handleConvertSuccess = async () => {
ElMessage.success('转化成功')
await loadDetail()
emit('success')
}
+1 -1
View File
@@ -56,7 +56,7 @@
v-model="attachmentUrls"
folder="crm/clue/follow"
:limit="15"
hint="支持图片、视频、PDF、Word 等文件,单文件不超过 20MB,最多 15 个"
hint="支持图片、视频、PDF、Word 等文件,最多 15 个"
/>
</el-form-item>
<el-form-item label="备注" prop="remark">
@@ -14,8 +14,7 @@
ref="formRef"
:model="form"
:rules="rules"
label-position="right"
label-width="100px"
label-position="top"
class="crm-dialog-form"
scroll-to-error
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
@@ -1381,7 +1381,11 @@ const detailBasicViewFields = computed<ViewField[]>(() => {
? []
: [{ label: '合同金额(元)', value: `¥${formatDetailAmount(c.amount)}`, money: true }]),
{ label: '交易主体', value: c.supplierName },
detailTagField('结算方式', 'crm_settlement_method', c.settlementMethod)
{
label: '结算方式',
value: c.settlementMethod ? dictStore.getLabel('crm_settlement_method', c.settlementMethod) : '—',
danger: true
}
];
});
+122 -20
View File
@@ -279,7 +279,7 @@
</el-table-column>
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="合同编号" align="center" prop="contractNo" min-width="170" show-overflow-tooltip />
<el-table-column label="合同名称" prop="contractName" show-overflow-tooltip min-width="200">
<el-table-column label="合同名称" prop="contractName" show-overflow-tooltip min-width="280">
<template #default="scope">
<el-link type="primary" @click="handleView(scope.row)">{{ scope.row.contractName }}</el-link>
</template>
@@ -349,14 +349,6 @@
>
</template>
</el-table-column>
<el-table-column v-if="isColumnVisible('receivedAmount')" label="已回款" align="right" prop="receivedAmount" min-width="130">
<template #default="scope">
<span v-if="scope.row.receivedAmount && scope.row.receivedAmount > 0" class="text-success"
>¥{{ parseFloat(scope.row.receivedAmount).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) }}</span
>
<span v-else class="text-muted">¥0.00</span>
</template>
</el-table-column>
<el-table-column v-if="isColumnVisible('unreceivedAmount')" label="未回款" align="right" prop="unreceivedAmount" min-width="130">
<template #default="scope">
<span
@@ -366,14 +358,6 @@
>
</template>
</el-table-column>
<el-table-column v-if="isColumnVisible('receivedRate')" label="回款进度" align="center" min-width="120">
<template #default="scope">
<el-progress
:percentage="getReceivedRate(scope.row.contractAmount, scope.row.receivedAmount)"
:color="getProgressColor(getReceivedRate(scope.row.contractAmount, scope.row.receivedAmount))"
/>
</template>
</el-table-column>
<el-table-column v-if="isColumnVisible('invoiceAmount')" label="已开票金额" align="right" prop="invoiceAmount" min-width="130">
<template #default="scope">
<span
@@ -421,6 +405,22 @@
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column v-if="isColumnVisible('receivedAmount')" label="已回款" align="right" prop="receivedAmount" min-width="130">
<template #default="scope">
<span v-if="scope.row.receivedAmount && scope.row.receivedAmount > 0" class="text-success"
>¥{{ parseFloat(scope.row.receivedAmount).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) }}</span
>
<span v-else class="text-muted">¥0.00</span>
</template>
</el-table-column>
<el-table-column v-if="isColumnVisible('receivedRate')" label="回款进度" align="center" min-width="120">
<template #default="scope">
<el-progress
:percentage="getReceivedRate(scope.row.contractAmount, scope.row.receivedAmount)"
:color="getProgressColor(getReceivedRate(scope.row.contractAmount, scope.row.receivedAmount))"
/>
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('ownerUserName')"
label="负责人"
@@ -438,7 +438,7 @@
show-overflow-tooltip
/>
<el-table-column v-if="isColumnVisible('createByName')" label="创建人" align="center" prop="createByName" min-width="100" />
<el-table-column v-if="isColumnVisible('createTime')" label="创建时间" align="center" prop="createTime" min-width="160" show-overflow-tooltip>
<el-table-column v-if="isColumnVisible('createTime')" label="创建时间" align="center" prop="createTime" min-width="140" show-overflow-tooltip>
<template #default="scope">
<span>{{ parseTime(scope.row.createTime) }}</span>
</template>
@@ -581,9 +581,7 @@ const listColumnDefs = [
{ prop: 'renewalCount', label: '续签次数' },
{ prop: 'attachmentFiles', label: '合同附件' },
{ prop: 'amount', label: '合同金额(元)' },
{ prop: 'receivedAmount', label: '已回款' },
{ prop: 'unreceivedAmount', label: '未回款' },
{ prop: 'receivedRate', label: '回款进度' },
{ prop: 'invoiceAmount', label: '已开票金额' },
{ prop: 'uninvoiceAmount', label: '未开票金额' },
{ prop: 'status', label: '合同状态' },
@@ -595,6 +593,8 @@ const listColumnDefs = [
{ prop: 'partyA', label: '甲方' },
{ prop: 'partyB', label: '乙方' },
{ prop: 'settlementMethod', label: '结算方式' },
{ prop: 'receivedAmount', label: '已回款' },
{ prop: 'receivedRate', label: '回款进度' },
{ prop: 'ownerUserName', label: '负责人' },
{ prop: 'createDeptName', label: '创建部门' },
{ prop: 'createByName', label: '创建人' },
@@ -1342,4 +1342,106 @@ onMounted(async () => {
color: var(--mint-primary, #1ca851);
}
.attachment-list-cell {
display: flex;
flex-wrap: nowrap;
gap: 8px;
justify-content: flex-start;
align-items: flex-start;
overflow-x: auto;
overflow-y: hidden;
white-space: nowrap;
padding: 4px 0;
scrollbar-width: none;
-ms-overflow-style: none;
&::-webkit-scrollbar {
display: none;
}
.attachment-thumb-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
flex-shrink: 0;
width: 58px;
}
.attachment-preview-wrap {
position: relative;
width: 50px;
height: 50px;
flex-shrink: 0;
}
.attachment-thumb-image {
width: 50px;
height: 50px;
border-radius: 4px;
border: 1px solid #e2e8f0;
object-fit: cover;
display: block;
}
.attachment-file-icon {
width: 50px;
height: 50px;
border-radius: 4px;
border: 1px solid #e2e8f0;
background: #f8fafc;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
transition: all 0.2s;
.el-icon {
font-size: 22px;
color: #1e293b;
}
&:hover {
border-color: #1ca851;
background: #e6f8ed;
.el-icon {
color: #1ca851;
}
}
}
.attachment-download-btn {
position: absolute;
right: -4px;
bottom: -4px;
width: 18px;
height: 18px;
border-radius: 50%;
background: #fff;
border: 1px solid #e2e8f0;
color: #1e293b;
cursor: pointer;
z-index: 2;
&:hover {
color: #1ca851;
border-color: #1ca851;
background: #e6f8ed;
}
}
.attachment-category-label {
display: block;
width: 100%;
font-size: 11px;
line-height: 1.3;
color: #1e293b;
text-align: center;
white-space: normal;
word-break: break-all;
}
}
</style>
@@ -15,20 +15,11 @@
append-to-body
>
<template #certificates>
<div class="cert-list">
<div v-if="customerInfo.businessLicenseImages" class="cert-block">
<div class="view-field-label">营业执照</div>
<AttachmentUploadField :model-value="customerInfo.businessLicenseImages" disabled />
</div>
<div v-if="customerInfo.businessPermitImages" class="cert-block">
<div class="view-field-label">经营许可证</div>
<AttachmentUploadField :model-value="customerInfo.businessPermitImages" disabled />
</div>
<div v-if="customerInfo.otherCertImages" class="cert-block">
<div class="view-field-label">其他证件</div>
<AttachmentUploadField :model-value="customerInfo.otherCertImages" disabled />
</div>
</div>
<AttachmentUploadField
:model-value="certificateDisplay.modelValue"
:category-labels="certificateDisplay.categoryLabels"
disabled
/>
</template>
<template #hero-actions>
@@ -547,6 +538,32 @@ const isToday = computed(() => {
return today === customerInfo.value.nextTime.split(' ')[0]
})
function appendCertUrls(raw: string | undefined, label: string, urls: string[], labels: Record<string, string>) {
if (!raw) return
String(raw)
.split(',')
.map((item) => item.trim())
.filter(Boolean)
.forEach((url) => {
urls.push(url)
labels[url] = label
})
}
function buildCertDisplay(customer: CustomerVO) {
const urls: string[] = []
const labels: Record<string, string> = {}
appendCertUrls(customer.businessLicenseImages, '营业执照', urls, labels)
appendCertUrls(customer.businessPermitImages, '经营许可证', urls, labels)
appendCertUrls(customer.otherCertImages, '其他证件', urls, labels)
return {
modelValue: urls.join(','),
categoryLabels: labels
}
}
const certificateDisplay = computed(() => buildCertDisplay(customerInfo.value))
const viewHeroMeta = computed(() => {
const c = customerInfo.value
const parts: string[] = []
@@ -993,18 +1010,6 @@ defineExpose({ open })
}
}
.cert-list {
display: flex;
flex-direction: column;
gap: 16px;
}
.cert-block {
.view-field-label {
margin-bottom: 8px;
}
}
:deep(.el-timeline-item__timestamp) {
color: #1e293b;
font-size: 12px;
@@ -152,25 +152,25 @@
<el-divider content-position="left">证件信息</el-divider>
<div class="form-grid-4">
<el-form-item label="营业执照" prop="businessLicenseImages">
<el-form-item label="营业执照" prop="businessLicenseImages" class="form-field-span-2">
<AttachmentUploadField
ref="businessLicenseUploadRef"
v-model="form.businessLicenseImages"
:limit="5"
accept="image/*,.pdf"
folder="crm/customer/license"
hint="支持 JPG、PNG、PDF,单文件不超过 20MB,最多 5 个"
hint="支持 JPG、PNG、PDF,最多 5 个"
@update:model-value="handleCertFieldChange('businessLicenseImages')"
/>
</el-form-item>
<el-form-item label="经营许可证" prop="businessPermitImages">
<el-form-item label="经营许可证" prop="businessPermitImages" class="form-field-span-2">
<AttachmentUploadField
ref="businessPermitUploadRef"
v-model="form.businessPermitImages"
:limit="5"
accept="image/*,.pdf"
folder="crm/customer/permit"
hint="支持 JPG、PNG、PDF,单文件不超过 20MB,最多 5 个"
hint="支持 JPG、PNG、PDF,最多 5 个"
@update:model-value="handleCertFieldChange('businessPermitImages')"
/>
</el-form-item>
@@ -181,7 +181,7 @@
:limit="10"
accept="image/*,.pdf"
folder="crm/customer/cert"
hint="支持 JPG、PNG、PDF,单文件不超过 20MB,最多 10 个"
hint="支持 JPG、PNG、PDF,最多 10 个"
/>
</el-form-item>
</div>
@@ -519,6 +519,7 @@ const open = async (id?: number | string) => {
Object.assign(form.value, data)
form.value.businessLicenseImages =
(data as any)?.businessLicenseImages || (data as any)?.businessLicenseImg || undefined
delete (form.value as Record<string, unknown>).businessLicenseImg
form.value.businessPermitImages = (data as any)?.businessPermitImages || undefined
form.value.otherCertImages = (data as any)?.otherCertImages || undefined
if ((data as any)?.ownerUserId) {
@@ -536,6 +537,8 @@ const open = async (id?: number | string) => {
/** 构建提交 payload(字段名与后端 Schema 对齐) */
function buildSubmitPayload() {
const payload: Record<string, any> = { ...form.value }
// 详情接口会同时返回 businessLicenseImg(旧字段),避免覆盖新上传的 businessLicenseImages
delete payload.businessLicenseImg
return payload
}
@@ -82,7 +82,7 @@
v-model="attachmentUrls"
folder="crm/customer/follow"
:limit="15"
hint="支持图片、视频、PDF、Word、Excel等文件,单文件不超过 20MB,最多 15 个"
hint="支持图片、视频、PDF、Word、Excel等文件,最多 15 个"
/>
</el-form-item>
<el-form-item label="备注" prop="remark">
@@ -75,7 +75,7 @@
v-model="attachmentUrls"
folder="crm/opportunity/follow"
:limit="15"
hint="支持图片、视频、PDF、Word 等文件,单文件不超过 20MB,最多 15 个"
hint="支持图片、视频、PDF、Word 等文件,最多 15 个"
/>
</el-form-item>
</el-form>
+2 -32
View File
@@ -201,18 +201,6 @@
header-align="center"
show-overflow-tooltip
/>
<el-table-column
v-if="isColumnVisible('unit')"
prop="unit"
label="单位"
min-width="80"
align="center"
header-align="center"
>
<template #default="{ row }">
{{ formatProductUnit(row.unit) }}
</template>
</el-table-column>
<el-table-column
v-if="isColumnVisible('price')"
prop="price"
@@ -293,7 +281,7 @@
>
<div class="form-grid-3">
<el-form-item label="产品编号">
<el-input v-model="formData.productNo" placeholder="留空自动生成" />
<el-input v-model="formData.productNo" placeholder="留空自动生成(如 SXWL000001)" />
</el-form-item>
<el-form-item label="产品名称" prop="productName">
<el-input v-model="formData.productName" placeholder="请输入产品名称" />
@@ -367,7 +355,7 @@
:limit="5"
accept="image/*"
folder="crm/product"
hint="支持 JPG、PNG、WebP 等图片,单文件不超过 20MB,最多 5 张"
hint="支持 JPG、PNG、WebP 等图片,最多 5 张"
/>
</el-form-item>
<el-form-item label="产品描述" style="grid-column: 1 / -1">
@@ -548,11 +536,6 @@ function getHazardFlagTagType(value: unknown) {
return 'danger'
}
function formatProductUnit(unit: string | null | undefined) {
if (!unit) return '-'
return dictStore.getLabel('crm_product_unit', unit) || unit
}
function formatProductPrice(value: unknown) {
if (value === null || value === undefined || value === '') return '-'
const num = Number(value)
@@ -568,7 +551,6 @@ const listColumnDefs = [
{ prop: 'productNo', label: '产品编号' },
{ prop: 'categoryName', label: '产品分类' },
{ prop: 'model', label: '规格型号' },
{ prop: 'unit', label: '单位' },
{ prop: 'price', label: '标准价(元)' },
{ prop: 'cost', label: '成本价(元)', defaultVisible: false },
{ prop: 'status', label: '状态' }
@@ -648,22 +630,10 @@ const productViewSections = computed<ViewSection[]>(() => {
if (!d) return []
const fields: ViewField[] = [
{
label: '标准价(元)',
value: formatProductPrice(d.price),
money: true
},
{
label: '成本价(元)',
value: formatProductPrice(d.costPrice ?? d.cost),
money: true
},
crmField('库存数量', `${d.stock ?? 0} ${formatProductUnit(d.unit)}`),
crmDictField('状态', 'crm_product_status', d.status, dictStore),
crmField('产品分类', d.categoryName),
crmField('规格型号', d.model),
crmField('条码', d.barcode),
crmField('库存预警值', d.lowStock ?? 10),
crmField('销售次数', d.saleCount ?? 0),
crmField('税率', `${d.taxRate ?? 13}%`),
crmTagField('是否易制毒', formatHazardFlag(d.isToxic), getHazardFlagTagType(d.isToxic)),
@@ -203,47 +203,50 @@
<div class="quote-approval-flow-scroll">
<p v-if="approvalList.length === 0" class="view-empty-hint">暂无审批记录</p>
<el-timeline v-else>
<el-timeline-item
v-for="item in approvalList"
:key="item.id"
:timestamp="`第${item.step}步 · ${item.createTime || '—'}`"
placement="top"
:color="getApprovalTimelineColor(item.approvalResult)"
:hollow="item.approvalResult === 'pending'"
>
<div class="quote-approval-card">
<div class="quote-approval-card-head">
<div class="quote-approval-card-user">
<el-avatar :size="28">{{ item.approverName?.substring(0, 1) || '—' }}</el-avatar>
<span>{{ item.approverName || '—' }}</span>
<div v-else class="quote-approval-flow-track">
<template v-for="(item, index) in approvalList" :key="item.id">
<div class="quote-approval-flow-step">
<p class="quote-approval-step-meta">
第{{ item.step }}步 · {{ item.createTime || '—' }}
</p>
<div class="quote-approval-card">
<div class="quote-approval-card-head">
<div class="quote-approval-card-user">
<el-avatar :size="28">{{ item.approverName?.substring(0, 1) || '—' }}</el-avatar>
<span>{{ item.approverName || '—' }}</span>
</div>
<el-tag
v-if="item.approvalResult === 'approved'"
type="success"
size="small"
>
已通过
</el-tag>
<el-tag
v-else-if="item.approvalResult === 'rejected'"
type="danger"
size="small"
>
已拒绝
</el-tag>
<el-tag v-else type="info" size="small">待审批</el-tag>
</div>
<el-tag
v-if="item.approvalResult === 'approved'"
type="success"
size="small"
>
已通过
</el-tag>
<el-tag
v-else-if="item.approvalResult === 'rejected'"
type="danger"
size="small"
>
已拒绝
</el-tag>
<el-tag v-else type="info" size="small">待审批</el-tag>
<p v-if="item.approvalOpinion" class="quote-approval-opinion">
审批意见:{{ item.approvalOpinion }}
</p>
<p v-if="item.approvalTime" class="quote-approval-meta">
审批时间:{{ item.approvalTime }}
</p>
<p v-if="item.remark" class="quote-approval-meta">备注:{{ item.remark }}</p>
</div>
<p v-if="item.approvalOpinion" class="quote-approval-opinion">
审批意见:{{ item.approvalOpinion }}
</p>
<p v-if="item.approvalTime" class="quote-approval-meta">
审批时间:{{ item.approvalTime }}
</p>
<p v-if="item.remark" class="quote-approval-meta">备注:{{ item.remark }}</p>
</div>
</el-timeline-item>
</el-timeline>
<div
v-if="index < approvalList.length - 1"
class="quote-approval-flow-connector"
:class="{ 'quote-approval-flow-connector--done': item.approvalResult === 'approved' }"
/>
</template>
</div>
</div>
<div v-if="approvalList.length > 0" class="quote-approval-stats">
@@ -386,12 +389,6 @@ function getDaysRemaining(expireDate?: string) {
return `剩余${diff}天`
}
function getApprovalTimelineColor(result?: string) {
if (result === 'approved') return '#1ca851'
if (result === 'rejected') return '#ef4444'
return '#94a3b8'
}
/** 8 列表格区块:禁用长字段独占行,保证每行尽量排满 4 组 */
function viewQuadTableSection(title: string, fields: ViewField[]): ViewSection {
return {
@@ -622,12 +619,62 @@ defineExpose({ open, refresh })
}
.quote-approval-flow-scroll {
max-height: 420px;
overflow-y: auto;
padding-right: 4px;
overflow-x: auto;
overflow-y: hidden;
padding: 4px 4px 12px;
}
.quote-approval-flow-track {
display: flex;
align-items: stretch;
gap: 0;
min-width: min-content;
padding: 4px 0;
}
.quote-approval-flow-step {
display: flex;
flex-direction: column;
gap: 8px;
width: 240px;
flex: 0 0 auto;
}
.quote-approval-step-meta {
margin: 0;
font-size: 12px;
line-height: 1.4;
color: var(--mint-text-muted, #64748b);
}
.quote-approval-flow-connector {
width: 48px;
height: 2px;
margin-top: 52px;
flex-shrink: 0;
align-self: flex-start;
background: #e2e8f0;
position: relative;
}
.quote-approval-flow-connector--done {
background: linear-gradient(90deg, #1ca851, #49be78);
}
.quote-approval-flow-connector--done::after {
content: '';
position: absolute;
right: -3px;
top: -3px;
width: 8px;
height: 8px;
border-top: 2px solid #1ca851;
border-right: 2px solid #1ca851;
transform: rotate(45deg);
}
.quote-approval-card {
flex: 1;
padding: 12px 14px;
border: 1px solid var(--border, #d9e8e2);
border-radius: 8px;
@@ -5,14 +5,17 @@
:model="form"
:rules="rules"
label-position="right"
label-width="100px"
label-width="120px"
class="crm-dialog-form quote-form-body"
scroll-to-error
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
>
<el-divider content-position="left">基本信息</el-divider>
<el-divider content-position="left">
<span class="quote-section-title">基本信息</span>
</el-divider>
<div class="form-grid-4">
<el-form-item class="quote-type-item form-col-span-4" label="报价类型" prop="quoteType" required>
<!-- 第一行:报价类型、报价单编号、报价主题、关联商机 -->
<el-form-item class="quote-type-item" label="报价类型" prop="quoteType" required>
<el-radio-group v-model="form.quoteType" @change="handleQuoteTypeChange">
<el-radio-button v-for="item in quoteTypeOptions" :key="item.value" :label="item.value">
{{ item.label }}
@@ -25,6 +28,27 @@
<el-form-item :label="basicLabels.quoteTitle" prop="quoteTitle">
<el-input v-model="form.quoteTitle" placeholder="可不填,系统将自动生成" maxlength="100" show-word-limit />
</el-form-item>
<el-form-item :label="basicLabels.opportunity" prop="opportunityId">
<el-input v-model="opportunityDisplayName" placeholder="请选择商机(可选)" readonly clearable>
<template #append>
<el-button text @click="handleSelectOpportunity">
<el-icon><Search /></el-icon>
</el-button>
</template>
</el-input>
</el-form-item>
<!-- 第二行:采购主体/报价主体、客户/供货商名称、联系人、负责人 -->
<el-form-item :label="basicLabels.supplier" prop="supplierId" required>
<el-select
v-model="form.supplierId"
:placeholder="`请选择${basicLabels.supplier}`"
clearable
style="width: 100%"
@change="handleSupplierChange"
>
<el-option v-for="supplier in supplierList" :key="supplier.id" :label="supplier.supplierName" :value="supplier.id" />
</el-select>
</el-form-item>
<el-form-item :label="basicLabels.customerName" prop="customerId" required>
<el-input v-model="form.customerName" placeholder="请选择客户" readonly>
<template #append>
@@ -43,15 +67,6 @@
</template>
</el-input>
</el-form-item>
<el-form-item :label="basicLabels.opportunity" prop="opportunityId">
<el-input v-model="opportunityDisplayName" placeholder="请选择商机(可选)" readonly clearable>
<template #append>
<el-button text @click="handleSelectOpportunity">
<el-icon><Search /></el-icon>
</el-button>
</template>
</el-input>
</el-form-item>
<el-form-item :label="basicLabels.ownerUser" prop="ownerUserId" required>
<el-select
v-model="form.ownerUserId"
@@ -64,6 +79,10 @@
<el-option v-for="user in userList" :key="user.userId" :label="user.nickName" :value="user.userId" />
</el-select>
</el-form-item>
<!-- 第三行:运费、询价/报价日期、有效期(天) -->
<el-form-item label="运费(元)" prop="freight">
<el-input-number v-model="form.freight" :min="0" :step="0.01" style="width: 100%" />
</el-form-item>
<el-form-item :label="basicLabels.quoteDate" prop="quoteDate" required>
<el-date-picker
v-model="form.quoteDate"
@@ -76,25 +95,11 @@
<el-form-item :label="basicLabels.validityDays" prop="validityDays" required>
<el-input-number v-model="form.validityDays" :min="1" :max="365" style="width: 100%" />
</el-form-item>
<el-form-item :label="basicLabels.supplier" prop="supplierId" required>
<el-select
v-model="form.supplierId"
:placeholder="`请选择${basicLabels.supplier}`"
clearable
style="width: 100%"
@change="handleSupplierChange"
>
<el-option v-for="supplier in supplierList" :key="supplier.id" :label="supplier.supplierName" :value="supplier.id" />
</el-select>
</el-form-item>
<el-form-item label="运费(元)" prop="freight">
<el-input-number v-model="form.freight" :min="0" :step="0.01" style="width: 100%" />
</el-form-item>
</div>
<!-- 产品明细 -->
<el-divider content-position="left">
<span style="font-size: 16px; font-weight: bold; color: #67c23a">
<span class="quote-section-title">
<el-icon><GoodsFilled /></el-icon> 产品明细
</span>
</el-divider>
@@ -236,7 +241,7 @@
<!-- 报价条款 -->
<el-divider content-position="left">
<span style="font-size: 16px; font-weight: bold; color: #67c23a">
<span class="quote-section-title">
<el-icon><List /></el-icon> 报价条款
</span>
</el-divider>
@@ -891,8 +896,8 @@ const handleCustomerConfirm = async (customer: CustomerVO) => {
if (customer) {
form.value.customerId = customer.id;
form.value.customerName = customer.customerName;
// 保存客户简称
customerShortName.value = customer.shortName || customer.customerName || '';
// 保存客户简称(报价主题依赖简称,不可回退为客户全称)
customerShortName.value = customer.shortName?.trim() || '';
// 清空联系人和商机
form.value.contactId = undefined;
form.value.contactName = '';
@@ -963,6 +968,39 @@ const handleOwnerChange = (userId: string | number | undefined) => {
form.value.ownerUserName = user?.nickName || ''
}
/** 当前登录用户 ID(后端 current-user 返回 id,兼容历史 userId 字段) */
const resolveCurrentUserId = (): string => {
const info = userStore.info || ({} as Api.Auth.UserInfo & Record<string, unknown>)
const fromInfo = String(info.id || info.userId || '')
if (fromInfo) return fromInfo
const token = userStore.accessToken
if (!token || token.split('.').length < 2) return ''
try {
const payload = token.split('.')[1]
const b64 = payload.replace(/-/g, '+').replace(/_/g, '/')
const pad = '='.repeat((4 - (b64.length % 4)) % 4)
const obj = JSON.parse(atob(b64 + pad))
return String(obj?.sub || '')
} catch {
return ''
}
}
/** 新增时默认负责人为当前登录人 */
const applyDefaultOwnerUser = () => {
const currentUserId = resolveCurrentUserId()
if (!currentUserId) return
const matched = userList.value.find((item) => String(item.userId) === currentUserId)
if (matched) {
form.value.ownerUserId = matched.userId as QuoteForm['ownerUserId']
form.value.ownerUserName = matched.nickName || ''
return
}
form.value.ownerUserId = currentUserId as QuoteForm['ownerUserId']
const info = userStore.info as Record<string, string | undefined>
form.value.ownerUserName = info?.realName || info?.userName || info?.username || ''
}
const formatApproverOptionLabel = (user: { nickName?: string; userName?: string }) => {
const nick = (user.nickName || '').trim()
const account = (user.userName || '').trim()
@@ -1610,6 +1648,7 @@ const initNew = async () => {
form.value.quoteDate = getTodayDate();
await dictStore.loadAllDict();
await Promise.all([loadSupplierList(), loadUserList()]);
applyDefaultOwnerUser();
};
const initEdit = async (id: string | number) => {
@@ -1674,11 +1713,27 @@ onMounted(async () => {
getQuoteInfoForCopy(copyId);
} else {
form.value.quoteDate = getTodayDate();
applyDefaultOwnerUser();
}
});
</script>
<style scoped lang="scss">
.quote-form-body {
:deep(.el-form-item__label) {
white-space: nowrap;
}
}
.quote-section-title {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 16px;
font-weight: 400;
color: var(--primary-dark, #059669);
}
.box-card {
margin-top: 10px;
}
@@ -1708,17 +1763,19 @@ onMounted(async () => {
.quote-type-item :deep(.el-form-item__content) {
display: flex;
align-items: center;
justify-content: center;
justify-content: flex-start;
}
.quote-type-item :deep(.el-radio-group) {
display: inline-flex;
justify-content: center;
justify-content: flex-start;
flex-wrap: wrap;
gap: 0;
}
.quote-type-item :deep(.el-radio-button__inner) {
padding: 14px 26px;
font-size: 16px;
padding: 8px 16px;
font-size: 14px;
}
.quote-type-wrapper {
@@ -2027,11 +2084,6 @@ onMounted(async () => {
gap: 8px 12px;
}
.quote-type-item :deep(.el-radio-group) {
display: inline-flex;
justify-content: center;
}
.approval-dialog-actions {
display: flex;
flex-wrap: wrap;
+42 -20
View File
@@ -438,8 +438,7 @@
ref="submitFormRef"
:model="submitForm"
:rules="submitRules"
label-position="right"
label-width="100px"
label-position="top"
class="crm-dialog-form"
scroll-to-error
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
@@ -1772,8 +1771,34 @@ const DEFAULT_QUOTE_TABLE_REMARK_ROW = `<tr>
<td colspan="10" style="padding: 8px; border: 1px solid #000; background-color: #f9f9f9; text-align: center; font-size: 13px;"> </td>
</tr>`;
/** 生成可替换的产品明细区块(产品行 + 合计行,每次从数据库数据渲染) */
const buildQuoteDynamicProductSection = (quote: QuoteVO): string => {
const QUOTE_TABLE_REMARK_ROW_PATTERN =
/<!--\s*备注行\s*-->\s*(<tr[\s\S]*?colspan=["']10["'][\s\S]*?<\/tr>)/i;
/** 从已保存模板中提取备注行(兼容备注行在合计上方或下方的旧结构) */
const extractQuoteTableRemarkRow = (html: string): string => {
const withComment = html.match(QUOTE_TABLE_REMARK_ROW_PATTERN);
if (withComment?.[1]) return withComment[1];
const colspan10Rows = [...html.matchAll(/<tr[\s\S]*?colspan=["']10["'][\s\S]*?<\/tr>/gi)].map((m) => m[0]);
for (let i = colspan10Rows.length - 1; i >= 0; i--) {
const row = colspan10Rows[i];
if (!row.includes('合计') && !row.includes('暂无产品信息')) {
return row;
}
}
return DEFAULT_QUOTE_TABLE_REMARK_ROW;
};
/** 移除动态区块外重复的备注行(旧模板中备注行在合计下方时) */
const removeOrphanRemarkRowAfterDynamicSection = (html: string): string => {
return html.replace(
/(<!--\s*quote-dynamic-products-end\s*-->)\s*(?:<!--\s*备注行\s*-->\s*)?<tr[\s\S]*?colspan=["']10["'][\s\S]*?<\/tr>/i,
'$1'
);
};
/** 生成可替换的产品明细区块(产品行 + 备注行 + 合计行,每次从数据库数据渲染) */
const buildQuoteDynamicProductSection = (quote: QuoteVO, remarkRow: string = DEFAULT_QUOTE_TABLE_REMARK_ROW): string => {
const productRows =
quote.productList
?.map(
@@ -1805,6 +1830,8 @@ const buildQuoteDynamicProductSection = (quote: QuoteVO): string => {
return `<!-- quote-dynamic-products-start -->
${productRows}
<!-- 备注行 -->
${remarkRow}
${totalRow}
<!-- quote-dynamic-products-end -->`;
};
@@ -1815,24 +1842,21 @@ const applyQuoteProductDataToTemplate = (
quote: QuoteVO,
supplierInfo?: SupplierInfoVO
): string => {
const dynamicSection = buildQuoteDynamicProductSection(quote);
const remarkRow = extractQuoteTableRemarkRow(savedTemplate);
const dynamicSection = buildQuoteDynamicProductSection(quote, remarkRow);
let result = savedTemplate;
if (TEMPLATE_DYNAMIC_PRODUCTS_PATTERN.test(savedTemplate)) {
return savedTemplate.replace(TEMPLATE_DYNAMIC_PRODUCTS_PATTERN, dynamicSection);
}
if (!/<tbody[\s\S]*?<\/tbody>/i.test(savedTemplate)) {
result = savedTemplate.replace(TEMPLATE_DYNAMIC_PRODUCTS_PATTERN, dynamicSection);
} else if (!/<tbody[\s\S]*?<\/tbody>/i.test(savedTemplate)) {
return generateQuoteTemplate(quote, supplierInfo);
}
return savedTemplate.replace(/<tbody>([\s\S]*?)<\/tbody>/i, (_match, body: string) => {
const remarkRow =
body.match(/<tr>[\s\S]*?colspan=["']10["'][\s\S]*?<\/tr>/i)?.[0] || DEFAULT_QUOTE_TABLE_REMARK_ROW;
return `<tbody>
} else {
result = savedTemplate.replace(/<tbody>([\s\S]*?)<\/tbody>/i, () => {
return `<tbody>
${dynamicSection}
<!-- 备注行 -->
${remarkRow}
</tbody>`;
});
});
}
return removeOrphanRemarkRowAfterDynamicSection(result);
};
/** 生成报价单HTML模版 */
@@ -1939,8 +1963,6 @@ const generateQuoteTemplate = (quote: QuoteVO, supplierInfo?: SupplierInfoVO): s
</thead>
<tbody>
${dynamicProductSection}
<!-- 备注行 -->
${DEFAULT_QUOTE_TABLE_REMARK_ROW}
</tbody>
</table>
+3 -3
View File
@@ -264,7 +264,7 @@
:limit="5"
accept="image/*,.pdf"
folder="pl/carrier/license"
hint="支持 JPG、PNG、PDF,单文件不超过 20MB,最多 5 个"
hint="支持 JPG、PNG、PDF,最多 5 个"
@update:model-value="handleCertFieldChange('businessLicenseUrls')"
/>
</el-form-item>
@@ -275,7 +275,7 @@
:limit="5"
accept="image/*,.pdf"
folder="pl/carrier/permit"
hint="支持 JPG、PNG、PDF,单文件不超过 20MB,最多 5 个"
hint="支持 JPG、PNG、PDF,最多 5 个"
@update:model-value="handleCertFieldChange('roadTransportLicenseUrls')"
/>
</el-form-item>
@@ -286,7 +286,7 @@
:limit="5"
accept="image/*,.pdf"
folder="pl/carrier/agreement"
hint="支持 JPG、PNG、PDF,单文件不超过 20MB,最多 5 个"
hint="支持 JPG、PNG、PDF,最多 5 个"
@update:model-value="handleCertFieldChange('transportAgreementUrls')"
/>
</el-form-item>