This commit is contained in:
zlwu committed 2026-08-03 12:48:52 +08:00
1 parent 594e4ce38e
commit e233269abf
144 files changed
+228178 -111974

No files matched your search

@@ -59,7 +59,7 @@
<div class="panel dialog-list-panel">
<div class="list-body list-wrap dialog-list-body-only">
<el-table
<ListTable table-key="business-contract-selector"
ref="tableRef"
v-loading="loading"
:data="contractList"
@@ -138,7 +138,7 @@
{{ planCountMap[String(row.id)] ?? 0 }}
</template>
</el-table-column>
</el-table>
</ListTable>
<ListPagination
v-show="total > 0"
@@ -7,13 +7,13 @@
:remote-method="searchRemote"
:loading="loading"
:allow-create="allowCreate"
default-first-option
:default-first-option="allowCreate"
clearable
style="width: 100%"
@focus="handleFocus"
>
<el-option
v-for="item in options"
v-for="item in displayOptions"
:key="item.id"
:label="item.customerName"
:value="item.customerName"
@@ -98,6 +98,22 @@
loadDefault()
}
}
/** 编辑回显:历史往来单位若不在客户库,仍展示只读选项(禁止 allow-create 时) */
const displayOptions = computed(() => {
const current = model.value?.trim()
if (!current) return options.value
if (options.value.some((item) => item.customerName === current)) {
return options.value
}
return [
{
id: `legacy-${current}`,
customerName: current
},
...options.value
]
})
</script>
<style scoped lang="scss">
@@ -53,6 +53,7 @@
productName: string
model?: string
unit?: string
status?: string
}
const dictStore = useDictStore()
@@ -60,11 +61,14 @@
const options = ref<ProductOption[]>([])
const formatProductMeta = (item: ProductOption): string => {
const parts: string[] = []
const model = item.model?.trim() || ''
const unitRaw = item.unit?.trim() || ''
const unit = unitRaw ? dictStore.getLabel('crm_product_unit', unitRaw) : ''
if (model && unit) return `${model} / ${unit}`
return model || unit || ''
if (model && unit) parts.push(`${model} / ${unit}`)
else if (model || unit) parts.push(model || unit)
if (item.status === 'off') parts.push('已下架')
return parts.join(' · ')
}
const extractList = (res: unknown): ProductOption[] => {
@@ -84,6 +88,7 @@
specification.value = item.model?.trim() || ''
}
/** 编辑回显:当前产品即使已下架也保留在选项中,新建/重选仅上架产品 */
const ensureCurrentOption = async () => {
const id = productId.value?.trim()
if (!id) return
@@ -103,7 +108,7 @@
const loadDefault = async () => {
loading.value = true
try {
const res = await fetchProductList({ page: 1, pageSize: 20 })
const res = await fetchProductList({ page: 1, pageSize: 20, status: 'on' })
options.value = extractList(res)
await ensureCurrentOption()
} catch (error) {
@@ -124,7 +129,8 @@
const res = await fetchProductList({
keyword: query.trim(),
page: 1,
pageSize: 50
pageSize: 50,
status: 'on'
})
options.value = extractList(res)
await ensureCurrentOption()
@@ -0,0 +1,156 @@
<!--
主列表薄封装:在 el-table 上启用列宽拖拽,并按 table-key 持久化到 localStorage。
用法:将 <el-table class="list-table" …> 换成 <ListTable table-key="crm-customer" class="list-table" …>
-->
<template>
<el-table
ref="tableRef"
v-bind="tableAttrs"
:border="resolvedBorder"
:allow-drag-last-column="true"
:class="tableClass"
@header-dragend="handleHeaderDragEnd"
>
<slot />
</el-table>
</template>
<script setup lang="ts">
import { computed, nextTick, onMounted, ref, unref, useAttrs, watch } from 'vue'
import {
resolveListColumnKey,
usePersistedListColumnWidths
} from '@/composables/usePersistedListColumnWidths'
defineOptions({ name: 'ListTable', inheritAttrs: false })
const props = withDefaults(
defineProps<{
/** 表格唯一标识,用于 localStorage 分区(同页多表必须不同) */
tableKey: string
/** 是否持久化列宽,默认 true */
persistColumnWidth?: boolean
/** 是否开启 border(列宽拖拽依赖);默认 true */
border?: boolean
}>(),
{
persistColumnWidth: true,
border: true
}
)
const attrs = useAttrs()
const tableRef = ref<any>(null)
const { setWidth, applyToTable, resetWidths, getWidths } = usePersistedListColumnWidths(
computed(() => props.tableKey)
)
const resolvedBorder = computed(() => props.border !== false)
const tableClass = computed(() => {
const extra = attrs.class
return ['list-table', extra]
})
const tableAttrs = computed(() => {
const raw = { ...attrs } as Record<string, unknown>
delete raw.class
delete raw.border
// 避免与内部 handler 重复绑定(父级若仍监听,下面会手动转发)
delete raw.onHeaderDragend
delete raw.onHeaderDragEnd
return raw
})
const parentHeaderDragEnd = computed(() => {
const a = attrs as Record<string, unknown>
return (a.onHeaderDragend || a.onHeaderDragEnd) as
| ((...args: any[]) => void)
| Array<(...args: any[]) => void>
| undefined
})
let applyTimer: ReturnType<typeof setTimeout> | null = null
const scheduleApply = () => {
if (!props.persistColumnWidth) return
if (!Object.keys(getWidths()).length) return
if (applyTimer) clearTimeout(applyTimer)
applyTimer = setTimeout(() => {
nextTick(() => {
applyToTable(tableRef.value)
})
}, 16)
}
const handleHeaderDragEnd = (
newWidth: number,
oldWidth: number,
column: any,
event: MouseEvent
) => {
if (props.persistColumnWidth) {
const key = resolveListColumnKey(column)
if (key) setWidth(key, newWidth)
}
const parent = parentHeaderDragEnd.value
if (typeof parent === 'function') {
parent(newWidth, oldWidth, column, event)
} else if (Array.isArray(parent)) {
parent.forEach((fn) => fn?.(newWidth, oldWidth, column, event))
}
}
onMounted(() => {
scheduleApply()
// 首屏列尚未完全注册时再补一次
setTimeout(() => scheduleApply(), 100)
})
watch(
() => (attrs as any).data,
() => scheduleApply(),
{ deep: false }
)
watch(
() => props.tableKey,
() => scheduleApply()
)
// 列显隐导致列数量变化时写回(避免在 drag 过程中用 onUpdated 反复 apply)
watch(
() => {
const cols = tableRef.value?.store?.states?.columns
return unref(cols)?.length ?? 0
},
() => scheduleApply()
)
/**
* 代理底层 el-table,保证父组件 ref 上的 clearSelection / doLayout 等仍可用
*/
const exposed = new Proxy(
{},
{
get(_target, prop) {
if (prop === 'elTableRef') return tableRef
if (prop === 'applyColumnWidths') {
return () => applyToTable(tableRef.value)
}
if (prop === 'resetColumnWidths') {
return () => {
resetWidths()
}
}
const table = tableRef.value
if (!table) return undefined
const value = table[prop as keyof typeof table]
return typeof value === 'function' ? value.bind(table) : value
}
}
)
defineExpose(exposed)
</script>
@@ -9,7 +9,7 @@
<div v-if="!certList.length" class="cert-empty">{{ emptyHint }}</div>
<el-table
<ListTable table-key="business-pl-cert-form-list"
v-else-if="layout === 'table'"
:data="certList"
stripe
@@ -113,7 +113,7 @@
</el-button>
</template>
</el-table-column>
</el-table>
</ListTable>
<div v-else class="cert-card-list">
<div v-for="(cert, index) in certList" :key="cert._key" class="cert-card">
@@ -4,7 +4,7 @@
<div v-else-if="layout === 'table'" class="panel dialog-list-panel">
<div class="list-body list-wrap dialog-list-body-only">
<el-table
<ListTable table-key="business-pl-cert-view-list"
:data="certList"
class="list-table"
stripe
@@ -77,7 +77,7 @@
<span v-else>—</span>
</template>
</el-table-column>
</el-table>
</ListTable>
</div>
</div>
@@ -113,7 +113,7 @@
<h3 class="dialog-section-title">关联运输信息</h3>
<div class="panel dialog-list-panel">
<div class="list-body list-wrap dialog-list-body-only">
<el-table
<ListTable table-key="business-pl-freight-disburse-dialog"
:data="settlement.transports || []"
class="list-table"
stripe
@@ -159,7 +159,7 @@
<el-table-column prop="freightAmount" label="运费(元)" min-width="110" align="right">
<template #default="{ row }">{{ formatAmount(row.freightAmount) }}</template>
</el-table-column>
</el-table>
</ListTable>
</div>
</div>
</section>
@@ -87,7 +87,7 @@
</div>
<div class="panel dialog-list-panel">
<div class="list-body list-wrap dialog-list-body-only">
<el-table
<ListTable table-key="business-pl-freight-settlement-form-dialog"
:data="editableTransports"
class="list-table"
stripe
@@ -152,7 +152,7 @@
</el-button>
</template>
</el-table-column>
</el-table>
</ListTable>
</div>
</div>
<p v-if="canEditTransports" class="dialog-section-tip">至少保留一条关联运输;删除后可在「添加运输」中重新加入</p>
@@ -222,7 +222,7 @@
</div>
</div>
<div class="list-body list-wrap dialog-list-body-only">
<el-table
<ListTable table-key="business-pl-freight-settlement-form-dialog-2"
:data="removedTransports"
class="list-table"
stripe
@@ -309,7 +309,7 @@
>
<template #default="{ row }">{{ formatAmount(row.freightAmount) }}</template>
</el-table-column>
</el-table>
</ListTable>
</div>
</div>
@@ -322,7 +322,7 @@
</div>
</div>
<div class="list-body list-wrap dialog-list-body-only">
<el-table
<ListTable table-key="business-pl-freight-settlement-form-dialog-3"
v-loading="addLoading"
:data="availablePlans"
class="list-table"
@@ -412,7 +412,7 @@
row.archiveTime ? formatDateTime(row.archiveTime) : '—'
}}</template>
</el-table-column>
</el-table>
</ListTable>
<div class="pagination list-pager-bar">
<div class="list-pager-selected">
@@ -32,7 +32,7 @@
<h4 class="view-section-title">发票信息</h4>
<div class="panel dialog-list-panel">
<div class="list-body list-wrap dialog-list-body-only">
<el-table
<ListTable table-key="business-pl-freight-settlement-view-dialog"
:data="invoiceRows"
class="list-table"
stripe
@@ -61,7 +61,7 @@
<span v-else class="view-kv-value">—</span>
</template>
</el-table-column>
</el-table>
</ListTable>
</div>
</div>
<div class="view-kv-grid view-kv-grid--cols-2 view-kv-grid--mt">
@@ -80,7 +80,7 @@
<h4 class="view-section-title">关联运输信息</h4>
<div class="panel dialog-list-panel">
<div class="list-body list-wrap dialog-list-body-only">
<el-table
<ListTable table-key="business-pl-freight-settlement-view-dialog-2"
:data="settlement.transports || []"
class="list-table"
stripe
@@ -126,7 +126,7 @@
<el-table-column prop="freightAmount" label="运费(元)" min-width="110" align="right">
<template #default="{ row }">{{ formatAmount(row.freightAmount) }}</template>
</el-table-column>
</el-table>
</ListTable>
</div>
</div>
</section>
@@ -1,7 +1,7 @@
<template>
<div class="panel dialog-list-panel pl-plan-product-table-wrap">
<div class="list-body list-wrap dialog-list-body-only">
<el-table :data="products" stripe class="list-table pl-plan-product-table" empty-text="暂无产品">
<ListTable table-key="business-pl-plan-form-dialog-pl-plan-form-product-table" :data="products" stripe class="list-table pl-plan-product-table" empty-text="暂无产品">
<el-table-column label="序号" type="index" min-width="60" align="center" header-align="center" />
<el-table-column label="产品名称" min-width="140" show-overflow-tooltip>
<template #default="{ row }">
@@ -140,7 +140,7 @@
</el-button>
</template>
</el-table-column>
</el-table>
</ListTable>
</div>
</div>
</template>
@@ -23,7 +23,7 @@
<p v-if="!hasDriverFeedback" class="pl-plan-form-empty-hint">暂无司机回传</p>
<template v-else>
<div class="view-subtable-wrap">
<el-table :data="driverFeedbackTableRows" class="view-subtable list-table" stripe>
<ListTable table-key="business-pl-plan-transport-feedback-blocks" :data="driverFeedbackTableRows" class="view-subtable list-table" stripe>
<el-table-column type="index" label="序号" min-width="68" align="center" />
<el-table-column
prop="productNo"
@@ -98,7 +98,7 @@
{{ row.remark || '—' }}
</template>
</el-table-column>
</el-table>
</ListTable>
</div>
<div class="pl-plan-io-site-meta">
<div class="pl-plan-io-site-meta__item">
@@ -148,7 +148,7 @@
<template v-if="ioOrder">
<ViewKvTable :fields="ioOrderViewFields" mode="quint" />
<div class="view-subtable-wrap" style="margin-top: 12px">
<el-table :data="resolvedIoOrderItems" class="view-subtable list-table" stripe>
<ListTable table-key="business-pl-plan-transport-feedback-blocks-2" :data="resolvedIoOrderItems" class="view-subtable list-table" stripe>
<el-table-column type="index" label="序号" min-width="68" align="center" />
<el-table-column
prop="productNo"
@@ -215,7 +215,7 @@
{{ row.remark || '—' }}
</template>
</el-table-column>
</el-table>
</ListTable>
</div>
<div class="pl-plan-io-site-meta">
<div class="pl-plan-io-site-meta__item">
@@ -238,7 +238,7 @@
暂无现场链接
</p>
<div v-else class="view-subtable-wrap">
<el-table :data="siteFeedbackTableRows" class="view-subtable list-table" stripe>
<ListTable table-key="business-pl-plan-transport-feedback-blocks-3" :data="siteFeedbackTableRows" class="view-subtable list-table" stripe>
<el-table-column type="index" label="序号" min-width="68" align="center" />
<el-table-column
prop="productName"
@@ -294,7 +294,7 @@
</el-button>
</template>
</el-table-column>
</el-table>
</ListTable>
</div>
</template>
</section>
@@ -329,7 +329,7 @@
<template v-if="row.weighing">
<h5 class="pl-plan-dibang-weighing-title">地磅单</h5>
<div class="view-subtable-wrap">
<el-table :data="[row.weighing]" class="view-subtable list-table" stripe>
<ListTable table-key="business-pl-plan-transport-feedback-blocks-4" :data="[row.weighing]" class="view-subtable list-table" stripe>
<el-table-column
type="index"
label="序号"
@@ -402,7 +402,7 @@
align="center"
show-overflow-tooltip
/>
</el-table>
</ListTable>
</div>
</template>
<p v-else class="pl-plan-form-empty-hint pl-plan-dibang-pending">尚未关联地磅单</p>
@@ -48,7 +48,7 @@
</div>
<div class="list-body list-wrap dialog-list-body-only">
<el-table
<ListTable table-key="business-pl-transport-address-select-dialog"
class="list-table pl-transport-address-select-table"
:data="rows"
v-loading="loading"
@@ -107,7 +107,7 @@
</el-button>
</template>
</el-table-column>
</el-table>
</ListTable>
<ListPagination
v-show="pagination.total > 0"
@@ -15,7 +15,14 @@
:key="item.id"
:label="item.licensePlate"
:value="item.licensePlate"
/>
>
<div class="pl-vehicle-option">
<span>{{ item.licensePlate }}</span>
<span v-if="vehicleTypeLabel(item)" class="pl-vehicle-option__meta">{{
vehicleTypeLabel(item)
}}</span>
</div>
</el-option>
</el-select>
</template>
@@ -32,17 +39,29 @@
placeholder?: string
}>(),
{
placeholder: '请选择车牌号'
placeholder: '请选择车牌号(三方/自有车辆)'
}
)
const loading = ref(false)
const options = ref<Vehicle[]>([])
const vehicleTypeLabel = (item: Vehicle): string => {
if (item.id?.startsWith('legacy-')) return '历史'
if (item.vehicleType === 'self') return '自有'
if (item.vehicleType === 'carrier') return '三方'
return ''
}
/** 三方车辆 + 山峡自有车辆,禁止手填新建 */
const loadOptions = async (keyword: string) => {
loading.value = true
try {
const res = await vehicleApi.list(keyword.trim() ? { q: keyword.trim() } : undefined)
const res = await vehicleApi.list({
...(keyword.trim() ? { q: keyword.trim() } : {}),
page: 1,
pageSize: 100
})
options.value = pickBxList<Vehicle>(res)
} catch (error) {
console.error('加载运输车辆列表失败', error)
@@ -58,7 +77,7 @@
}
}
/** 编辑态回显:历史出入库单中的车牌若不在运输车辆库,仍展示只读选项 */
/** 编辑态回显:历史出入库单中的车牌若不在车辆库,仍展示只读选项 */
const displayOptions = computed(() => {
const current = model.value?.trim()
if (!current) return options.value
@@ -77,3 +96,19 @@
]
})
</script>
<style scoped lang="scss">
.pl-vehicle-option {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
min-width: 0;
}
.pl-vehicle-option__meta {
flex-shrink: 0;
font-size: 12px;
color: var(--text-muted);
}
</style>