fix(pl): 修复客户品类证件额度选证与回显问题。
优先消耗有到期日的证件,补齐品类产品选择与运输证适用品类回显,避免新规则安排与建档展示失败。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
b0c1bca683
commit
e8cda33a3c
10 files changed
+330
-44
No files matched your search
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user