fix(pl): 修复客户品类证件额度选证与回显问题。

优先消耗有到期日的证件,补齐品类产品选择与运输证适用品类回显,避免新规则安排与建档展示失败。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
wuzilongandCursor committed 2026-11-15 07:13:42 +08:00
1 parent b0c1bca683
commit e8cda33a3c
10 files changed
+330 -44

No files matched your search

+8 -1
View File
@@ -428,7 +428,14 @@ export interface CertCategory {
categoryName: string
categoryCode?: string | null
status: string
products?: Array<{ id?: string; productId: string; scopeStatus?: string }>
products?: Array<{
id?: string
productId: string
scopeStatus?: string
productNo?: string | null
productName?: string | null
model?: string | null
}>
}
export const certQuotaRuleApi = {
@@ -1606,7 +1606,7 @@
message.includes('待完善') || message.includes('完善并关闭') || message.includes('证书齐备待完善')
if (title !== '用证没有完成' || pendingFulfill) {
const guide = pendingFulfill
? `${html}<p class="pl-arrange-block-text">新规则下证书齐备只放行安排,不能绕过归档挂证。请到「证书齐备 → 待完善并关闭」上传证件并执行完善并关闭后再归档。</p>`
? `${html}<p class="pl-arrange-block-text">新规则下证书齐备只放行安排,不能绕过归档挂证。请到「证书齐备 → 已标记待完善证书」上传证件并执行完善并关闭后再归档。</p>`
: html
await ElMessageBox.alert(guide, title, {
type: 'warning',
@@ -1618,7 +1618,7 @@
return undefined
}
// 仅旧规则(legacy)允许在归档时标记证书齐备以跳过额度
const readyHtml = `${html}<p class="pl-arrange-block-text">如果购买证、运输证还没上传,可以标记证书齐备。仅旧规则计划标记后这次归档不再校验、不扣这两证额度;新规则计划请走「待完善并关闭」。</p>`
const readyHtml = `${html}<p class="pl-arrange-block-text">如果购买证、运输证还没上传,可以标记证书齐备。仅旧规则计划标记后这次归档不再校验、不扣这两证额度;新规则计划请走「证书齐备 → 已标记待完善证书」。</p>`
try {
await ElMessageBox.confirm(readyHtml, title, {
type: 'warning',
@@ -74,7 +74,7 @@
<section class="pl-plan-form-section pl-plan-form-section--plain">
<h3 class="pl-plan-form-section-title">关联购买证 / 运输证</h3>
<p v-if="plan?.quotaCertPendingFulfill" class="pl-plan-form-empty-hint">
证书齐备(待完善):请到「证书齐备 → 待完善并关闭」挂证
证书齐备(待完善):请到「证书齐备 → 已标记待完善证书」挂证并完善并关闭
</p>
<div v-else-if="linkedCertRows.length" class="pl-plan-linked-certs">
<div v-for="item in linkedCertRows" :key="item.key" class="pl-plan-linked-cert-row">
+151 -22
View File
@@ -43,8 +43,12 @@
</el-table-column>
<el-table-column label="操作" min-width="200" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
<el-button link type="danger" @click="remove(row)">删除</el-button>
<el-button link type="primary" @click="openEdit(row)">
<el-icon><Edit /></el-icon>编辑
</el-button>
<el-button link type="danger" @click="remove(row)">
<el-icon><Delete /></el-icon>删除
</el-button>
</template>
</el-table-column>
</ListTable>
@@ -55,9 +59,10 @@
v-model="visible"
class="dialog-form-layout"
:title="editingId ? '编辑证用品类' : '新增证用品类'"
width="640px"
width="720px"
align-center
destroy-on-close
:close-on-click-modal="false"
>
<el-form label-position="top">
<div class="form-grid-2">
@@ -74,13 +79,41 @@
</el-select>
</el-form-item>
</div>
<el-form-item label="关联系统产品(产品 ID,逗号分隔,保存后按差量写入)">
<el-input
v-model="productIdsText"
type="textarea"
:rows="3"
placeholder="例如:uuid1,uuid2(上线后可改产品选择器)"
/>
<el-form-item label="关联系统产品">
<div class="cert-category-products">
<div class="cert-category-products__toolbar">
<span class="selected-hint">已选 {{ linkedProducts.length }} 项</span>
<el-button type="primary" @click="productSelectorVisible = true">
<el-icon><Plus /></el-icon>添加产品
</el-button>
</div>
<el-table
v-if="linkedProducts.length"
:data="linkedProducts"
stripe
class="list-table"
row-key="productId"
max-height="280"
>
<el-table-column prop="productNo" label="产品编号" min-width="120">
<template #default="{ row }">{{ row.productNo || '—' }}</template>
</el-table-column>
<el-table-column prop="productName" label="产品名称" min-width="160">
<template #default="{ row }">{{ row.productName || row.productId }}</template>
</el-table-column>
<el-table-column prop="model" label="规格型号" min-width="120">
<template #default="{ row }">{{ row.model || '—' }}</template>
</el-table-column>
<el-table-column label="操作" min-width="100" fixed="right">
<template #default="{ $index }">
<el-button link type="danger" @click="removeLinked($index)">
<el-icon><Delete /></el-icon>移除
</el-button>
</template>
</el-table-column>
</el-table>
<p v-else class="form-tip">暂未关联系统产品</p>
</div>
</el-form-item>
</el-form>
<template #footer>
@@ -92,13 +125,30 @@
</el-button>
</template>
</el-dialog>
<ProductSelector
:visible="productSelectorVisible"
@update:visible="productSelectorVisible = $event"
@confirm="onProductsConfirm"
/>
</div>
</template>
<script setup lang="ts">
import { certCategoryApi, type CertCategory } from '@/api/pl/quota-cert'
import { fetchProductDetail } from '@/api/crm/product'
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
import { Check, Close, Plus, Refresh } from '@element-plus/icons-vue'
import ProductSelector from '@/components/business/product-selector/index.vue'
import { Check, Close, Delete, Edit, Plus, Refresh } from '@element-plus/icons-vue'
type LinkedProduct = {
id?: string
productId: string
productNo?: string | null
productName?: string | null
model?: string | null
scopeStatus: 'active' | 'inactive'
}
const keyword = ref('')
const loading = ref(false)
@@ -107,7 +157,8 @@
const visible = ref(false)
const editingId = ref('')
const form = reactive({ categoryName: '', categoryCode: '', status: '1' })
const productIdsText = ref('')
const linkedProducts = ref<LinkedProduct[]>([])
const productSelectorVisible = ref(false)
async function load() {
loading.value = true
@@ -124,36 +175,93 @@
void load()
}
async function enrichMissingNames(items: LinkedProduct[]) {
const missing = items.filter((item) => item.productId && !item.productName)
if (!missing.length) return items
await Promise.all(
missing.map(async (item) => {
try {
const res = (await fetchProductDetail({ id: item.productId })) as Record<string, any>
const data = res?.data || res
item.productNo = data?.productNo || item.productNo
item.productName = data?.productName || item.productName
item.model = data?.model || item.model
} catch {
/* 保留 productId 回显 */
}
})
)
return items
}
async function openEdit(row?: CertCategory) {
editingId.value = row?.id || ''
form.categoryName = row?.categoryName || ''
form.categoryCode = row?.categoryCode || ''
form.status = row?.status || '1'
productIdsText.value = ''
linkedProducts.value = []
if (row?.id) {
const detail = await certCategoryApi.get(row.id)
productIdsText.value = (detail.products || [])
const products = (detail.products || [])
.filter((p) => p.scopeStatus !== 'inactive')
.map((p) => p.productId)
.join(',')
.map((p) => ({
id: p.id,
productId: p.productId,
productNo: p.productNo,
productName: p.productName,
model: p.model,
scopeStatus: 'active' as const
}))
linkedProducts.value = await enrichMissingNames(products)
}
visible.value = true
}
function onProductsConfirm(products: Array<Record<string, any>>) {
const existing = new Map(linkedProducts.value.map((item) => [item.productId, item]))
let added = 0
let skipped = 0
for (const product of products || []) {
const productId = String(product.id || '').trim()
if (!productId) continue
if (existing.has(productId)) {
skipped += 1
continue
}
const row: LinkedProduct = {
productId,
productNo: product.productNo || null,
productName: product.productName || null,
model: product.model || null,
scopeStatus: 'active'
}
existing.set(productId, row)
linkedProducts.value.push(row)
added += 1
}
productSelectorVisible.value = false
if (added) ElMessage.success(`已添加 ${added} 个产品`)
else if (skipped) ElMessage.warning('所选产品均已关联')
}
function removeLinked(index: number) {
linkedProducts.value.splice(index, 1)
}
async function save() {
if (!form.categoryName.trim()) {
ElMessage.warning('请填写品类名称')
return
}
const productIds = productIdsText.value
.split(/[,,\s]+/)
.map((s) => s.trim())
.filter(Boolean)
const body = {
categoryName: form.categoryName.trim(),
categoryCode: form.categoryCode || null,
status: form.status,
products: productIds.map((productId) => ({ productId, scopeStatus: 'active' }))
products: linkedProducts.value.map((item) => ({
...(item.id ? { id: item.id } : {}),
productId: item.productId,
scopeStatus: item.scopeStatus || 'active'
}))
}
saving.value = true
try {
@@ -171,7 +279,9 @@
async function remove(row: CertCategory) {
try {
await ElMessageBox.confirm(`确认删除品类「${row.categoryName}」?`, '删除', { type: 'warning' })
await ElMessageBox.confirm(`确认删除品类「${row.categoryName}」?`, '删除', {
type: 'warning'
})
} catch {
return
}
@@ -188,3 +298,22 @@
void load()
})
</script>
<style scoped lang="scss">
.cert-category-products {
width: 100%;
}
.cert-category-products__toolbar {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 12px;
margin-bottom: 12px;
}
.selected-hint {
color: var(--el-text-color-secondary);
font-size: 13px;
}
</style>
+25 -9
View File
@@ -2,12 +2,12 @@
<div class="page-container">
<section class="panel">
<div class="panel-top"><h2 class="title-bar-heading">证书齐备</h2></div>
<el-tabs v-model="listMode" class="cert-ready-tabs" @tab-change="onModeChange">
<el-tab-pane label="待标记齐备" name="mark" />
<el-tab-pane label="已标记待完善证书" name="fulfill" />
</el-tabs>
<div class="list-toolbar">
<div class="list-toolbar-row">
<el-radio-group v-model="listMode" class="mr-3" @change="onModeChange">
<el-radio-button value="mark">标记齐备</el-radio-button>
<el-radio-button value="fulfill">待完善并关闭</el-radio-button>
</el-radio-group>
<ListQuickSearchInput
v-model="keyword"
placeholder="计划编号"
@@ -117,9 +117,9 @@
:loading="markingId === row.id"
@click="mark(row)"
>
<el-icon><CircleCheck /></el-icon>证书齐备
<el-icon><CircleCheck /></el-icon>标记齐备
</el-button>
<span v-else>已确认</span>
<span v-else>已标记</span>
</template>
</template>
</el-table-column>
@@ -284,6 +284,7 @@
function onModeChange() {
page.value = 1
selectedIds.value = []
void load()
}
@@ -296,9 +297,9 @@
async function mark(row: CertReadyPlan) {
try {
await ElMessageBox.confirm(
'确认后可先安排运输;新规则下归档前仍须在「待完善并关闭」中挂上购买证/运输证。确认人和时间会记在本行。',
'证书齐备',
{ type: 'warning', confirmButtonText: '确认齐备', cancelButtonText: '取消' }
'确认后可先安排运输;新规则下归档前仍须到「已标记待完善证书」页上传证件并执行完善并关闭。确认人和时间会记在本行。',
'标记齐备',
{ type: 'warning', confirmButtonText: '确认标记', cancelButtonText: '取消' }
)
} catch {
return
@@ -365,3 +366,18 @@
void load()
})
</script>
<style scoped lang="scss">
.cert-ready-tabs {
margin: 0 0 4px;
padding: 0 24px;
:deep(> .el-tabs__header) {
margin-bottom: 0;
}
:deep(> .el-tabs__content) {
display: none;
}
}
</style>
+10 -4
View File
@@ -99,8 +99,8 @@
><el-icon><Check /></el-icon
>{{ row.openingConfirmed ? '复核' : '期初确认' }}</el-button
>
<el-button size="small" @click="showContracts(row)"
><el-icon><Connection /></el-icon>关联合同</el-button
<el-button size="small" @click="view(row)"
><el-icon><View /></el-icon>查看</el-button
>
</template></el-table-column
>
@@ -649,7 +649,7 @@
import QuotaHistoryDialog from './QuotaHistoryDialog.vue'
const historyDialog = ref<InstanceType<typeof QuotaHistoryDialog>>()
import { Check, Close, Connection, Delete, Document, Edit, Plus } from '@element-plus/icons-vue'
import { Check, Close, Delete, Document, Edit, Plus, View } from '@element-plus/icons-vue'
import { useUserStore } from '@/store/modules/user'
import { useDictStore } from '@/store/modules/dict'
import { getProductUnitLabel } from '@/utils/crm-product-display'
@@ -1283,6 +1283,10 @@
function applyCertForm(targetKind: CertType, cert: QuotaCert) {
const value = {
...certPayload(targetKind, cert),
// 运输证适用品类为只读展示字段,不进保存 payload
...(targetKind === 'transport'
? { derivedCategoryIds: [...(cert.derivedCategoryIds || [])] }
: {}),
contractBindings: (cert.contractBindings || []).map((binding) => ({
...binding,
products: binding.products.map((product) => ({ ...product }))
@@ -1325,6 +1329,9 @@
form.value = row
? {
...certPayload(targetKind, row),
...(targetKind === 'transport'
? { derivedCategoryIds: [...(row.derivedCategoryIds || [])] }
: {}),
totalQty: inputQty(row.totalQty),
contractBindings: [],
bindingsEditable: row.bindingsEditable !== false,
@@ -1703,7 +1710,6 @@
linkedContracts.value = links
detailVisible.value = true
}
const showContracts = view
async function refreshDetails() {
if (detail.value && detailVisible.value) await view(detail.value)
}