feat(pl): 展示各装送段用证配置与独立占用明细并保留历史计量口径。
This commit is contained in:
1 parent
706260ff7a
commit
b764eb02a7
10 files changed
+497
-40
No files matched your search
@@ -29,3 +29,42 @@ for (const arrange of [false, true]) test(`stale ${arrange ? 'preview' : 'save'}
|
||||
assert.equal(h.state.visible.value, true)
|
||||
assert.equal(h.state.busy.value, true)
|
||||
})
|
||||
|
||||
for (const methodName of ['preview', 'loadContext'])
|
||||
test(`${methodName} ignores a result when configuration changes in flight`, async () => {
|
||||
const script = parse(
|
||||
readFileSync(
|
||||
new URL('../src/components/business/pl-plan-quota-dialog/index.vue', import.meta.url),
|
||||
'utf8'
|
||||
)
|
||||
).descriptor.scriptSetup!.content
|
||||
const ast = ts.createSourceFile('quota.ts', script, ts.ScriptTarget.Latest, true)
|
||||
const method = ast.statements
|
||||
.find((n) => ts.isFunctionDeclaration(n) && n.name?.text === methodName)!
|
||||
.getText(ast)
|
||||
let finish!: (value: any) => void
|
||||
const pending = new Promise<any>((r) => {
|
||||
finish = r
|
||||
})
|
||||
const state: any = {
|
||||
plan: { value: { planId: 'a' } },
|
||||
busy: { value: false },
|
||||
result: { value: undefined },
|
||||
context: { value: undefined },
|
||||
payload: () => ({}),
|
||||
previewVehicle: {},
|
||||
planQuotaApi: { preview: () => pending, context: () => pending }
|
||||
}
|
||||
const js = ts.transpileModule(
|
||||
`let epoch=0,revision=0;${method};return {run:${methodName},change:()=>revision++}`,
|
||||
{ compilerOptions: { target: ts.ScriptTarget.ES2022 } }
|
||||
).outputText
|
||||
const actions = new Function(...Object.keys(state), js)(...Object.values(state))
|
||||
const task = actions.run()
|
||||
actions.change()
|
||||
finish({ status: 'available', sourceFingerprint: 'old' })
|
||||
await task
|
||||
assert.equal(state.result.value, undefined)
|
||||
assert.equal(state.context.value, undefined)
|
||||
assert.equal(state.busy.value, false)
|
||||
})
|
||||
@@ -39,6 +39,7 @@ function pageHarness(kind: 'quota' | 'legal' | 'cert', props: any = { mobile: tr
|
||||
...Object.fromEntries(
|
||||
Object.entries(vue).filter(([key]) => /^[a-zA-Z_$][\w$]*$/.test(key) && key !== 'default')
|
||||
),
|
||||
useElementSize: () => ({ width: vue.ref(900) }),
|
||||
useListTableViewportHeight: () => ({ tableHeight: vue.ref(400) }),
|
||||
useClientColumnFilters,
|
||||
useServerColumnFilters,
|
||||
|
||||
@@ -38,6 +38,7 @@ function pageHarness(kind: 'quota' | 'legal', props: any = { mobile: true }) {
|
||||
...Object.fromEntries(
|
||||
Object.entries(vue).filter(([key]) => /^[a-zA-Z_$][\w$]*$/.test(key) && key !== 'default')
|
||||
),
|
||||
useElementSize: () => ({ width: vue.ref(900) }),
|
||||
useListTableViewportHeight: () => ({ tableHeight: vue.ref(400) }),
|
||||
useServerColumnFilters,
|
||||
usePersistedListColumns,
|
||||
@@ -47,6 +48,7 @@ function pageHarness(kind: 'quota' | 'legal', props: any = { mobile: true }) {
|
||||
onBeforeUnmount: (fn: Function) => unmount.push(fn),
|
||||
request: { get: (url: string, options: any) => queue(lists, { url, ...options.params }) },
|
||||
pickBxData: (value: any) => value,
|
||||
planQuotaApi: { get: (id: string) => queue(gets, id) },
|
||||
legalDocumentApi: {
|
||||
list: (params: any) => queue(lists, params),
|
||||
get: (id: string) => queue(gets, id),
|
||||
@@ -330,3 +332,21 @@ test('legal: read-only permission prevents writes even when detail contains atta
|
||||
assert.equal(h.writes.length, 0)
|
||||
h.stop()
|
||||
})
|
||||
|
||||
test('plan expansion loads on demand, preserves total during refresh and ignores previous tenant', async () => {
|
||||
const h = pageHarness('quota', { mobile: false }),
|
||||
s = h.state
|
||||
s.total.value = 45
|
||||
const refresh = s.load()
|
||||
assert.equal(s.total.value, 45)
|
||||
h.lists[0].resolve({ list: [{ id: 'plan' }], total: 45 })
|
||||
await refresh
|
||||
s.expand({ id: 'plan' }, [{ id: 'plan' }])
|
||||
assert.equal(h.gets.length, 1)
|
||||
assert.deepEqual(s.expandedIds.value, ['plan'])
|
||||
h.user.getUserInfo.tenantNo = 'other'
|
||||
h.gets[0].resolve({ planId: 'plan', sourceOptions: [], usages: [] })
|
||||
await flush()
|
||||
assert.deepEqual(Object.keys(s.details), [])
|
||||
h.stop()
|
||||
})
|
||||
@@ -0,0 +1,109 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
import {
|
||||
measuredSources,
|
||||
configurationPayload,
|
||||
sourceLabel,
|
||||
stopUsageRows
|
||||
} from '../src/components/business/pl-plan-quota-dialog/stop-model'
|
||||
const load: any = {
|
||||
sourceStopType: 'load',
|
||||
sourceStopProductId: 'a',
|
||||
planProductId: 'p',
|
||||
productName: '硫酸',
|
||||
stopOrder: 1,
|
||||
quantity: 100,
|
||||
unit: '吨',
|
||||
contractId: 'c1',
|
||||
contractProductId: 'cp1'
|
||||
}
|
||||
const delivery: any = {
|
||||
...load,
|
||||
sourceStopType: 'delivery',
|
||||
sourceStopProductId: 'b',
|
||||
quantity: 40,
|
||||
contractId: 'c2',
|
||||
contractProductId: 'cp2'
|
||||
}
|
||||
const plan: any = {
|
||||
measurementVersion: 'per_stop_v2',
|
||||
configuration: {},
|
||||
sourceOptions: [load, delivery],
|
||||
usages: []
|
||||
}
|
||||
test('v2 measures every stop and never sends legacy pairs; absent version stays legacy', () => {
|
||||
assert.deepEqual(measuredSources(plan, { p: 'load' }), [load, delivery])
|
||||
assert.equal(
|
||||
configurationPayload(plan, { sourceMapping: [{ old: true }] } as any, [load], {}).sourceMapping,
|
||||
null
|
||||
)
|
||||
assert.deepEqual(measuredSources({ ...plan, measurementVersion: undefined }, { p: 'load' }), [
|
||||
load
|
||||
])
|
||||
})
|
||||
test('source labels use stop and product instead of source UUID', () => {
|
||||
assert.equal(sourceLabel('delivery:b', plan.sourceOptions), '送货段1 · 硫酸')
|
||||
assert.equal(sourceLabel('load:unknown', []), '来源已不可用')
|
||||
})
|
||||
test('all stop products are visible and each certificate keeps its unit and quantity', () => {
|
||||
const data = {
|
||||
...plan,
|
||||
stopProducts: [
|
||||
{ ...load, isToxic: true },
|
||||
{ ...delivery, isToxic: true },
|
||||
{ ...load, sourceStopProductId: 'e', isToxic: false, isExplosive: true }
|
||||
],
|
||||
usages: [
|
||||
{
|
||||
certType: 'purchase',
|
||||
certNoSnapshot: 'BUY',
|
||||
quantityUnit: '吨',
|
||||
items: [
|
||||
{ sourceStopType: 'load', sourceStopProductId: 'a', reservedQty: '100', usedQty: '0' }
|
||||
]
|
||||
},
|
||||
{
|
||||
certType: 'transport',
|
||||
certNoSnapshot: 'MOVE',
|
||||
quantityUnit: '千克',
|
||||
items: [
|
||||
{ sourceStopType: 'load', sourceStopProductId: 'a', reservedQty: '100000', usedQty: '0' }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
const rows = stopUsageRows(data)
|
||||
assert.equal(rows.length, 3)
|
||||
assert.equal(rows[0].purchase, 'BUY · 预占 100 吨 · 已用 0 吨')
|
||||
assert.equal(rows[0].transport, 'MOVE · 预占 100000 千克 · 已用 0 千克')
|
||||
assert.equal(rows[1].purchase, '缺配')
|
||||
assert.equal(rows[2].purchase, '无需两证')
|
||||
})
|
||||
test('draft bindings show zeros in their own units and hidden scope never claims missing', () => {
|
||||
const rows = stopUsageRows({
|
||||
...plan,
|
||||
stopProducts: [
|
||||
{
|
||||
...load,
|
||||
isToxic: true,
|
||||
bindingsReadable: true,
|
||||
certificates: [{ certType: 'purchase', certId: 'cert', certNo: 'BUY', quantityUnit: '桶' }]
|
||||
},
|
||||
{ ...delivery, isToxic: true, bindingsReadable: false }
|
||||
]
|
||||
})
|
||||
assert.equal(rows[0].purchase, 'BUY · 已绑定 · 预占 0 桶 · 已用 0 桶')
|
||||
assert.equal(rows[1].purchase, '合同或证件不可读取')
|
||||
})
|
||||
test('legacy counterpart is traceability only, never a second missing allocation', () => {
|
||||
const legacy = {
|
||||
...plan,
|
||||
measurementVersion: 'legacy_single_side',
|
||||
configuration: {
|
||||
sourceMapping: [
|
||||
{ sourceStopType: 'load', sourceStopProductId: 'a', relatedStopProductIds: ['b'] }
|
||||
]
|
||||
}
|
||||
}
|
||||
assert.equal(stopUsageRows(legacy as any)[1].purchase, '原口径:关联追溯,不另计量')
|
||||
})
|
||||
@@ -167,7 +167,15 @@ export const legalDocumentApi = {
|
||||
request.patch(`${legalBase}/${id}`, { products }).then(pickBxData<LegalDocument>)
|
||||
}
|
||||
|
||||
export type MeasurementVersion = 'per_stop_v2' | 'legacy_single_side'
|
||||
export interface QuotaSourceOption {
|
||||
sourceStopId?: string
|
||||
stopOrder?: number
|
||||
contractNo?: string | null
|
||||
bindingsReadable?: boolean
|
||||
certificates?: { certType: CertType; certId: string; certNo: string; quantityUnit: string }[]
|
||||
isToxic?: boolean
|
||||
isExplosive?: boolean
|
||||
sourceStopType: 'load' | 'delivery'
|
||||
sourceStopProductId: string
|
||||
planProductId: string
|
||||
@@ -202,6 +210,7 @@ export interface QuotaCondition {
|
||||
note: string
|
||||
}
|
||||
export interface QuotaConfiguration {
|
||||
measurementVersion?: MeasurementVersion
|
||||
sourceMapping?: QuotaSourcePair[] | null
|
||||
purchaseCertIds: string[]
|
||||
transportCertIds: string[]
|
||||
@@ -209,6 +218,8 @@ export interface QuotaConfiguration {
|
||||
conditions: QuotaCondition[]
|
||||
}
|
||||
export interface PlanQuota {
|
||||
measurementVersion?: MeasurementVersion
|
||||
stopProducts?: QuotaSourceOption[]
|
||||
planId: string
|
||||
planNo: string
|
||||
licensePlate?: string
|
||||
|
||||
@@ -28,9 +28,23 @@
|
||||
>已安排计划请从调整运输提交用证变更;已归档计划仅查看占用。</p
|
||||
>
|
||||
<section class="view-section">
|
||||
<h3>1. 明确唯一计量侧与装送配对</h3>
|
||||
<h3>{{ isPerStop ? '1. 全部装送段用证需求' : '1. 原口径:唯一计量侧与装送配对' }}</h3>
|
||||
<el-table v-if="isPerStop" :data="measured" stripe>
|
||||
<el-table-column label="段产品" min-width="200"
|
||||
><template #default="{ row }">{{
|
||||
sourceLabel(`${row.sourceStopType}:${row.sourceStopProductId}`, measured)
|
||||
}}</template></el-table-column
|
||||
>
|
||||
<el-table-column prop="contractNo" label="关联合同" min-width="160" />
|
||||
<el-table-column prop="partyName" label="单位" min-width="160" />
|
||||
<el-table-column label="需用量" min-width="120"
|
||||
><template #default="{ row }"
|
||||
>{{ row.quantity }} {{ row.unit }}</template
|
||||
></el-table-column
|
||||
>
|
||||
</el-table>
|
||||
<p v-if="!groups.length">当前计划没有易制毒产品,无需购买证、运输证额度。</p>
|
||||
<div v-for="group in groups" :key="group.id" class="view-section">
|
||||
<div v-for="group in isPerStop ? [] : groups" :key="group.id" class="view-section">
|
||||
<strong>{{ group.name }}</strong>
|
||||
<el-radio-group
|
||||
:model-value="sides[group.id]"
|
||||
@@ -62,7 +76,7 @@
|
||||
v-for="other in opposite(group)"
|
||||
:key="other.sourceStopProductId"
|
||||
:value="other.sourceStopProductId"
|
||||
:label="`${other.partyName || '未填写单位'} · ${other.quantity} ${other.unit} · ${other.sourceStopProductId.slice(-6)}`"
|
||||
:label="`${other.partyName || '未填写单位'} · ${other.quantity} ${other.unit} · ${other.productName}`"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
@@ -215,7 +229,7 @@
|
||||
: 'warning'
|
||||
"
|
||||
>{{ statusLabels[result.status] || result.status }}</el-tag
|
||||
><p v-for="reason in result.reasons" :key="reason">{{ reason }}</p>
|
||||
><p v-for="reason in result.reasons" :key="reason">{{ readableReason(reason) }}</p>
|
||||
<div
|
||||
v-for="row in result.diagnostics || []"
|
||||
:key="row.certType + row.unit"
|
||||
@@ -229,14 +243,17 @@
|
||||
适用余额至多 {{ row.availableUpperBound }} {{ row.unit }};缺口至少
|
||||
{{ row.gapLowerBound }} {{ row.unit }}
|
||||
</p>
|
||||
<p>{{ row.reason }}</p>
|
||||
<p>{{ readableReason(row.reason) }}</p>
|
||||
<p>{{
|
||||
row.sourceIds.map((id) => sourceLabel(id, plan?.sourceOptions || [])).join(';')
|
||||
}}</p>
|
||||
</div>
|
||||
<el-table :data="allocations" stripe
|
||||
><el-table-column prop="kind" label="证种" min-width="90" /><el-table-column
|
||||
prop="certNo"
|
||||
label="证号"
|
||||
min-width="160" /><el-table-column
|
||||
prop="sourceId"
|
||||
prop="sourceLabel"
|
||||
label="计量来源"
|
||||
min-width="190" /><el-table-column
|
||||
prop="sourceQuantity"
|
||||
@@ -258,7 +275,7 @@
|
||||
<section v-if="plan.usages.length" class="view-section"
|
||||
><h3>已有占用与结转</h3
|
||||
><el-table :data="plan.usages" stripe
|
||||
><el-table-column prop="certId" label="证件" min-width="180" /><el-table-column
|
||||
><el-table-column prop="certNoSnapshot" label="证件" min-width="180" /><el-table-column
|
||||
prop="usageStatus"
|
||||
label="状态"
|
||||
min-width="120"
|
||||
@@ -293,6 +310,7 @@
|
||||
</el-dialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { perStop, measuredSources, configurationPayload, sourceLabel } from './stop-model'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { Check, Close, Delete, Plus, Refresh, Search } from '@element-plus/icons-vue'
|
||||
import {
|
||||
@@ -329,6 +347,7 @@
|
||||
const sides = reactive<Record<string, string>>({}),
|
||||
pairs = reactive<Record<string, string[]>>({})
|
||||
let previewVehicle: QuotaPreviewVehicle = {}
|
||||
let revision = 0
|
||||
let epoch = 0,
|
||||
resolve: ((value: QuotaConfiguration | undefined) => void) | undefined
|
||||
const groups = computed(() => {
|
||||
@@ -348,9 +367,8 @@
|
||||
}
|
||||
return [...values.values()]
|
||||
})
|
||||
const measured = computed(() =>
|
||||
groups.value.flatMap((g) => g[sides[g.id] as 'load' | 'delivery'] || [])
|
||||
)
|
||||
const isPerStop = computed(() => perStop(plan.value))
|
||||
const measured = computed(() => measuredSources(plan.value, sides))
|
||||
const selected = computed(() => [
|
||||
...purchases.value
|
||||
.filter((c) => config.value.purchaseCertIds.includes(c.id))
|
||||
@@ -381,10 +399,11 @@
|
||||
result.value?.[kind === 'purchase' ? 'purchaseAllocations' : 'transportAllocations'] || []
|
||||
).map((row) => ({
|
||||
...row,
|
||||
sourceLabel: sourceLabel(row.sourceId, plan.value?.sourceOptions || []),
|
||||
kind: kind === 'purchase' ? '购买证' : '运输证',
|
||||
certNo:
|
||||
selected.value.find((i) => i.kind === kind && i.cert.id === row.certId)?.cert.certNo ||
|
||||
row.certId
|
||||
'证号未提供'
|
||||
}))
|
||||
)
|
||||
)
|
||||
@@ -395,6 +414,7 @@
|
||||
return group[sides[group.id] === 'load' ? 'delivery' : 'load']
|
||||
}
|
||||
function invalidate() {
|
||||
revision++
|
||||
context.value = undefined
|
||||
result.value = undefined
|
||||
config.value.conditions = []
|
||||
@@ -404,14 +424,12 @@
|
||||
invalidate()
|
||||
}
|
||||
function payload() {
|
||||
return {
|
||||
...config.value,
|
||||
sourceMapping: measured.value.map((s) => ({
|
||||
sourceStopType: s.sourceStopType,
|
||||
sourceStopProductId: s.sourceStopProductId,
|
||||
relatedStopProductIds: pairs[s.sourceStopProductId] || []
|
||||
}))
|
||||
}
|
||||
return configurationPayload(plan.value, config.value, measured.value, pairs)
|
||||
}
|
||||
function readableReason(reason: string) {
|
||||
return reason.replace(/(?:load|delivery):[A-Za-z0-9_-]+/g, (id) =>
|
||||
sourceLabel(id, plan.value?.sourceOptions || [])
|
||||
)
|
||||
}
|
||||
function condition(kind: CertType, cert: QuotaCert) {
|
||||
let c = config.value.conditions.find((i) => i.certType === kind && i.certId === cert.id)
|
||||
@@ -437,13 +455,14 @@
|
||||
async function loadContext() {
|
||||
if (!plan.value || busy.value) return
|
||||
busy.value = true
|
||||
const current = epoch
|
||||
const current = epoch,
|
||||
inputRevision = revision
|
||||
try {
|
||||
const value = await planQuotaApi.context(plan.value.planId, {
|
||||
...payload(),
|
||||
...previewVehicle
|
||||
})
|
||||
if (current !== epoch) return
|
||||
if (current !== epoch || inputRevision !== revision) return
|
||||
context.value = value
|
||||
config.value.conditions = config.value.conditions.filter(
|
||||
(c) =>
|
||||
@@ -461,13 +480,14 @@
|
||||
async function preview() {
|
||||
if (!plan.value || busy.value) return
|
||||
busy.value = true
|
||||
const current = epoch
|
||||
const current = epoch,
|
||||
inputRevision = revision
|
||||
try {
|
||||
const value = await planQuotaApi.preview(plan.value.planId, {
|
||||
...payload(),
|
||||
...previewVehicle
|
||||
})
|
||||
if (current === epoch) result.value = value
|
||||
if (current === epoch && inputRevision === revision) result.value = value
|
||||
} finally {
|
||||
if (current === epoch) busy.value = false
|
||||
}
|
||||
@@ -589,10 +609,12 @@
|
||||
}
|
||||
}
|
||||
watch(
|
||||
() => JSON.stringify(config.value.manualAllocations),
|
||||
() => JSON.stringify([config.value.manualAllocations, config.value.conditions]),
|
||||
() => {
|
||||
revision++
|
||||
result.value = undefined
|
||||
}
|
||||
},
|
||||
{ flush: 'sync' }
|
||||
)
|
||||
function invalidateSession() {
|
||||
epoch++
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
import type { PlanQuota, QuotaConfiguration, QuotaSourceOption } from '@/api/pl/quota-cert'
|
||||
|
||||
export const perStop = (plan?: PlanQuota) =>
|
||||
(plan?.measurementVersion || plan?.configuration.measurementVersion) === 'per_stop_v2'
|
||||
|
||||
export function measuredSources(plan: PlanQuota | undefined, sides: Record<string, string>) {
|
||||
return (plan?.sourceOptions || []).filter(
|
||||
(s) => perStop(plan) || sides[s.planProductId] === s.sourceStopType
|
||||
)
|
||||
}
|
||||
|
||||
export function configurationPayload(
|
||||
plan: PlanQuota | undefined,
|
||||
config: QuotaConfiguration,
|
||||
sources: QuotaSourceOption[],
|
||||
pairs: Record<string, string[]>
|
||||
) {
|
||||
return {
|
||||
...config,
|
||||
sourceMapping: perStop(plan)
|
||||
? null
|
||||
: sources.map((s) => ({
|
||||
sourceStopType: s.sourceStopType,
|
||||
sourceStopProductId: s.sourceStopProductId,
|
||||
relatedStopProductIds: pairs[s.sourceStopProductId] || []
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
export function sourceLabel(id: string, sources: QuotaSourceOption[]) {
|
||||
const source = sources.find((s) => `${s.sourceStopType}:${s.sourceStopProductId}` === id)
|
||||
return source
|
||||
? `${source.sourceStopType === 'load' ? '装货' : '送货'}段${source.stopOrder || 1} · ${source.productName}${source.partyName ? ` · ${source.partyName}` : ''}`
|
||||
: '来源已不可用'
|
||||
}
|
||||
|
||||
export function stopUsageRows(plan: PlanQuota) {
|
||||
return (plan.stopProducts || plan.sourceOptions.map((s) => ({ ...s, isToxic: true }))).map(
|
||||
(source) => {
|
||||
const text = (kind: string) => {
|
||||
if (!source.isToxic) return '无需两证'
|
||||
if (
|
||||
!perStop(plan) &&
|
||||
plan.configuration.sourceMapping?.some(
|
||||
(s) =>
|
||||
s.sourceStopType !== source.sourceStopType &&
|
||||
s.relatedStopProductIds.includes(source.sourceStopProductId)
|
||||
)
|
||||
)
|
||||
return '原口径:关联追溯,不另计量'
|
||||
const lines = plan.usages
|
||||
.filter((u) => u.certType === kind)
|
||||
.flatMap((u) =>
|
||||
(u.items || [])
|
||||
.filter(
|
||||
(i: any) =>
|
||||
i.sourceStopType === source.sourceStopType &&
|
||||
i.sourceStopProductId === source.sourceStopProductId
|
||||
)
|
||||
.map((i: any) => {
|
||||
const unit = i.quantityUnit || u.quantityUnit || '—'
|
||||
return `${u.certNoSnapshot || '证号未提供'} · 预占 ${i.reservedQty ?? '—'} ${unit} · 已用 ${i.usedQty ?? '—'} ${unit}`
|
||||
})
|
||||
)
|
||||
const bound = (source.certificates || []).filter(
|
||||
(c) =>
|
||||
c.certType === kind &&
|
||||
!plan.usages.some(
|
||||
(u) =>
|
||||
u.certType === kind &&
|
||||
u.certId === c.certId &&
|
||||
(u.items || []).some(
|
||||
(i: any) =>
|
||||
i.sourceStopType === source.sourceStopType &&
|
||||
i.sourceStopProductId === source.sourceStopProductId
|
||||
)
|
||||
)
|
||||
)
|
||||
lines.push(
|
||||
...bound.map(
|
||||
(c) => `${c.certNo} · 已绑定 · 预占 0 ${c.quantityUnit} · 已用 0 ${c.quantityUnit}`
|
||||
)
|
||||
)
|
||||
if (source.bindingsReadable === false) lines.push('合同或证件不可读取')
|
||||
return lines.join(';') || '缺配'
|
||||
}
|
||||
return {
|
||||
...source,
|
||||
label: sourceLabel(`${source.sourceStopType}:${source.sourceStopProductId}`, [source]),
|
||||
purchase: text('purchase'),
|
||||
transport: text('transport')
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -64,8 +64,64 @@
|
||||
table-key="pl-plan-quota"
|
||||
v-loading="loading"
|
||||
:data="rows"
|
||||
row-key="id"
|
||||
:expand-row-keys="expandedIds"
|
||||
@expand-change="expand"
|
||||
stripe
|
||||
class="list-table"
|
||||
><el-table-column type="expand"
|
||||
><template #default="{ row }">
|
||||
<div
|
||||
class="quota-expansion"
|
||||
:style="expansionStyle"
|
||||
v-loading="details[row.id]?.loading"
|
||||
>
|
||||
<el-alert
|
||||
v-if="details[row.id]?.error"
|
||||
title="段次用证加载失败"
|
||||
type="error"
|
||||
:closable="false"
|
||||
><el-button @click="loadDetail(row.id)"
|
||||
><el-icon><RefreshLeft /></el-icon>重试</el-button
|
||||
></el-alert
|
||||
>
|
||||
<template v-else-if="details[row.id]?.data">
|
||||
<el-tag
|
||||
size="small"
|
||||
:type="perStop(details[row.id].data) ? 'info' : 'warning'"
|
||||
>{{
|
||||
perStop(details[row.id].data) ? '各装送段分别计量' : '原口径:单侧计量'
|
||||
}}</el-tag
|
||||
>
|
||||
<p class="form-tip"
|
||||
>绑定不代表校验通过;购买证与运输证分别显示各自证面单位的预占和已用量。</p
|
||||
>
|
||||
<el-table :data="stopUsageRows(details[row.id].data!)" stripe>
|
||||
<el-table-column prop="label" label="段产品" min-width="210" />
|
||||
<el-table-column label="合同" min-width="150"
|
||||
><template #default="{ row: item }">{{
|
||||
item.contractNo || '未关联或不可读取'
|
||||
}}</template></el-table-column
|
||||
>
|
||||
<el-table-column prop="partyName" label="单位" min-width="160" />
|
||||
<el-table-column label="需用量" min-width="110"
|
||||
><template #default="{ row: item }"
|
||||
>{{ item.quantity }} {{ item.unit }}</template
|
||||
></el-table-column
|
||||
>
|
||||
<el-table-column
|
||||
prop="purchase"
|
||||
label="购买证 · 预占 / 已用"
|
||||
min-width="300"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="transport"
|
||||
label="运输证 · 预占 / 已用"
|
||||
min-width="300"
|
||||
/>
|
||||
</el-table>
|
||||
</template>
|
||||
</div> </template></el-table-column
|
||||
><el-table-column
|
||||
v-for="column in visibleDefs"
|
||||
:key="column.prop"
|
||||
@@ -132,6 +188,8 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { planQuotaApi, type PlanQuota } from '@/api/pl/quota-cert'
|
||||
import { perStop, stopUsageRows } from '@/components/business/pl-plan-quota-dialog/stop-model'
|
||||
import { useListTableViewportHeight } from '@/composables/useListTableViewportHeight'
|
||||
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
|
||||
import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
|
||||
@@ -162,10 +220,38 @@
|
||||
loading = ref(false),
|
||||
rows = ref<(Plan & { quotaStatus: string })[]>([])
|
||||
const dialog = ref<InstanceType<typeof PlPlanQuotaDialog>>()
|
||||
const expandedIds = ref<string[]>([])
|
||||
const details = reactive<Record<string, { loading: boolean; data?: PlanQuota; error?: boolean }>>(
|
||||
{}
|
||||
)
|
||||
function clearDetails() {
|
||||
expandedIds.value = []
|
||||
Object.keys(details).forEach((id) => delete details[id])
|
||||
}
|
||||
async function loadDetail(id: string) {
|
||||
const current = epoch
|
||||
const state = reactive<{ loading: boolean; data?: PlanQuota; error?: boolean }>({
|
||||
loading: true
|
||||
})
|
||||
details[id] = state
|
||||
try {
|
||||
const value = await planQuotaApi.get(id)
|
||||
if (current === epoch && details[id] === state) state.data = value
|
||||
} catch {
|
||||
if (current === epoch && details[id] === state) state.error = true
|
||||
} finally {
|
||||
if (current === epoch && details[id] === state) state.loading = false
|
||||
}
|
||||
}
|
||||
function expand(row: { id: string }, expanded: { id: string }[]) {
|
||||
expandedIds.value = expanded.map((r) => r.id)
|
||||
if (expanded.some((r) => r.id === row.id) && !details[row.id]) void loadDetail(row.id)
|
||||
}
|
||||
let epoch = 0
|
||||
async function load() {
|
||||
const current = ++epoch
|
||||
loading.value = true
|
||||
clearDetails()
|
||||
try {
|
||||
const value = pickBxData<{ list: (Plan & { quotaStatus: string })[]; total: number }>(
|
||||
await request.get('/transport/quota-certs/plans', {
|
||||
@@ -196,6 +282,7 @@
|
||||
() => JSON.stringify([user.accessToken, user.getUserInfo]),
|
||||
() => {
|
||||
epoch++
|
||||
clearDetails()
|
||||
rows.value = []
|
||||
total.value = 0
|
||||
page.value = 1
|
||||
@@ -255,6 +342,10 @@
|
||||
const tableWrapRef = ref<HTMLElement | null>(null)
|
||||
const pagerRef = ref<HTMLElement | null>(null)
|
||||
const tableRef = ref()
|
||||
const { width: tableViewportWidth } = useElementSize(tableWrapRef)
|
||||
const expansionStyle = computed(() => ({
|
||||
width: tableViewportWidth.value > 0 ? `${tableViewportWidth.value}px` : undefined
|
||||
}))
|
||||
const { tableHeight } = useListTableViewportHeight({
|
||||
tableEl: tableWrapRef,
|
||||
pagerEl: pagerRef,
|
||||
@@ -262,3 +353,12 @@
|
||||
enabled: computed(() => !props.mobile)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.quota-expansion {
|
||||
box-sizing: border-box;
|
||||
max-width: 100%;
|
||||
padding: 16px;
|
||||
text-align: left;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user