This commit is contained in:
zlwu committed 2026-06-10 22:59:34 +08:00
1 parent 725774ef0c
commit c3be9d1dce
176 files changed
+41925 -6342

No files matched your search

@@ -121,16 +121,14 @@ const props = withDefaults(
accept?: string
hint?: string
disabled?: boolean
maxSizeMb?: number
}>(),
{
modelValue: '',
folder: 'crm/customer',
limit: 15,
accept: 'image/*,video/*,.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt,.zip,.rar',
hint: '支持图片、视频、PDF、Word、Excel等文件,单文件不超过 20MB',
disabled: false,
maxSizeMb: 20
hint: '支持图片、视频、PDF、Word、Excel等文件',
disabled: false
}
)
@@ -215,13 +213,6 @@ async function handleAttachmentChange(uploadFile: UploadFile) {
return
}
const maxBytes = props.maxSizeMb * 1024 * 1024
if (rawFile.size > maxBytes) {
ElMessage.error(`单文件不能超过 ${props.maxSizeMb}MB`)
uploadRef.value?.clearFiles()
return
}
const item: AttachmentItem = {
id: `upload-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
fileName: rawFile.name,
@@ -0,0 +1,117 @@
<template>
<el-select
v-model="model"
:placeholder="placeholder"
filterable
remote
:remote-method="searchRemote"
:loading="loading"
:allow-create="allowCreate"
default-first-option
clearable
style="width: 100%"
@focus="handleFocus"
>
<el-option
v-for="item in options"
:key="item.id"
:label="item.customerName"
:value="item.customerName"
>
<div class="db-crm-option">
<span>{{ item.customerName }}</span>
<span v-if="item.customerNo" class="db-crm-option__meta">{{ item.customerNo }}</span>
</div>
</el-option>
</el-select>
</template>
<script setup lang="ts">
import { fetchCustomerList } from '@/api/crm/customer'
defineOptions({ name: 'DbCrmCustomerSelect' })
const model = defineModel<string>({ default: '' })
withDefaults(
defineProps<{
placeholder?: string
allowCreate?: boolean
}>(),
{
placeholder: '请输入客户名称搜索',
allowCreate: true
}
)
interface CustomerOption {
id: string
customerName: string
customerNo?: string
}
const loading = ref(false)
const options = ref<CustomerOption[]>([])
const extractList = (res: unknown): CustomerOption[] => {
const data = (res as { data?: { list?: CustomerOption[]; rows?: CustomerOption[] } })?.data ?? res ?? {}
const payload = data as { list?: CustomerOption[]; rows?: CustomerOption[] }
return payload.list || payload.rows || []
}
const loadDefault = async () => {
loading.value = true
try {
const res = await fetchCustomerList({ page: 1, pageSize: 20 })
options.value = extractList(res)
} catch (error) {
console.error('加载 CRM 客户列表失败', error)
options.value = []
} finally {
loading.value = false
}
}
const searchRemote = async (query: string) => {
if (!query.trim()) {
await loadDefault()
return
}
loading.value = true
try {
const res = await fetchCustomerList({
customerName: query.trim(),
page: 1,
pageSize: 50
})
options.value = extractList(res)
} catch (error) {
console.error('搜索 CRM 客户失败', error)
options.value = []
} finally {
loading.value = false
}
}
const handleFocus = () => {
if (options.value.length === 0) {
loadDefault()
}
}
</script>
<style scoped lang="scss">
.db-crm-option {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
min-width: 0;
}
.db-crm-option__meta {
flex-shrink: 0;
font-size: 12px;
color: var(--text-muted);
}
</style>
@@ -0,0 +1,184 @@
<template>
<div class="db-crm-product-select">
<el-select
v-model="productId"
:placeholder="placeholder"
filterable
remote
:remote-method="searchRemote"
:loading="loading"
clearable
style="width: 100%"
@focus="handleFocus"
@change="handleProductChange"
>
<el-option
v-for="item in options"
:key="item.id"
:label="item.productName"
:value="item.id"
>
<div class="db-crm-option">
<span>{{ item.productName }}</span>
<span v-if="formatProductMeta(item)" class="db-crm-option__meta">{{
formatProductMeta(item)
}}</span>
</div>
</el-option>
</el-select>
</div>
</template>
<script setup lang="ts">
import { fetchProductDetail, fetchProductList } from '@/api/crm/product'
import { useDictStore } from '@/store/modules/dict'
defineOptions({ name: 'DbCrmProductSelect' })
const productId = defineModel<string>('productId', { default: '' })
const productName = defineModel<string>('productName', { default: '' })
const specification = defineModel<string>('specification', { default: '' })
withDefaults(
defineProps<{
placeholder?: string
}>(),
{
placeholder: '请搜索并选择 CRM 产品'
}
)
interface ProductOption {
id: string
productName: string
model?: string
unit?: string
}
const dictStore = useDictStore()
const loading = ref(false)
const options = ref<ProductOption[]>([])
const formatProductMeta = (item: ProductOption): 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 || ''
}
const extractList = (res: unknown): ProductOption[] => {
const data =
(res as { data?: { list?: ProductOption[]; rows?: ProductOption[] } })?.data ?? res ?? {}
const payload = data as { list?: ProductOption[]; rows?: ProductOption[] }
return payload.list || payload.rows || []
}
const syncFromOption = (item: ProductOption | undefined) => {
if (!item) {
productName.value = ''
specification.value = ''
return
}
productName.value = item.productName
specification.value = item.model?.trim() || ''
}
const ensureCurrentOption = async () => {
const id = productId.value?.trim()
if (!id) return
if (options.value.some((item) => item.id === id)) return
try {
const res = await fetchProductDetail({ id })
const detail = (res as { data?: ProductOption })?.data ?? (res as ProductOption)
if (detail?.id) {
options.value = [detail, ...options.value]
syncFromOption(detail)
}
} catch (error) {
console.error('加载 CRM 产品详情失败', error)
}
}
const loadDefault = async () => {
loading.value = true
try {
const res = await fetchProductList({ page: 1, pageSize: 20 })
options.value = extractList(res)
await ensureCurrentOption()
} catch (error) {
console.error('加载 CRM 产品列表失败', error)
options.value = []
} finally {
loading.value = false
}
}
const searchRemote = async (query: string) => {
if (!query.trim()) {
await loadDefault()
return
}
loading.value = true
try {
const res = await fetchProductList({
keyword: query.trim(),
page: 1,
pageSize: 50
})
options.value = extractList(res)
await ensureCurrentOption()
} catch (error) {
console.error('搜索 CRM 产品失败', error)
options.value = []
} finally {
loading.value = false
}
}
const handleFocus = () => {
if (options.value.length === 0) {
loadDefault()
}
}
const handleProductChange = (value: string) => {
const item = options.value.find((option) => option.id === value)
syncFromOption(item)
}
watch(productId, async (value) => {
if (!value) {
syncFromOption(undefined)
return
}
await ensureCurrentOption()
handleProductChange(value)
})
onMounted(() => {
if (productId.value) {
ensureCurrentOption()
}
})
</script>
<style scoped lang="scss">
.db-crm-product-select {
width: 100%;
}
.db-crm-option {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
min-width: 0;
}
.db-crm-option__meta {
flex-shrink: 0;
font-size: 12px;
color: var(--text-muted);
}
</style>
@@ -0,0 +1,32 @@
<template>
<div class="db-quick-dates" role="group" aria-label="日期快捷筛选">
<el-button
v-for="item in options"
:key="item.value"
:class="{ 'is-active': modelValue === item.value }"
@click="emit('update:modelValue', item.value)"
>
{{ item.label }}
</el-button>
</div>
</template>
<script setup lang="ts">
export type DbDateFilterType = 'all' | 'today' | 'tomorrow' | 'yesterday' | 'week'
defineProps<{
modelValue: DbDateFilterType | ''
}>()
const emit = defineEmits<{
'update:modelValue': [value: DbDateFilterType]
}>()
const options: { label: string; value: DbDateFilterType }[] = [
{ label: '全部', value: 'all' },
{ label: '今日', value: 'today' },
{ label: '明日', value: 'tomorrow' },
{ label: '昨日', value: 'yesterday' },
{ label: '本周', value: 'week' }
]
</script>
@@ -0,0 +1,178 @@
<template>
<el-dialog
v-model="innerVisible"
title="查看关联数据"
width="1200px"
align-center
destroy-on-close
class="dialog-form-layout db-related-table-dialog"
modal-class="order-module-dialog-mask"
>
<div v-loading="loading" class="db-related-body">
<el-empty v-if="!hasData" description="暂无关联数据" />
<template v-else>
<section v-if="data.plan" class="db-related-table-section">
<h3 class="db-related-table-title">计划</h3>
<div class="view-subtable-wrap">
<el-table :data="planRows" class="view-subtable list-table" stripe>
<el-table-column prop="planNo" label="计划号" min-width="120" align="center" />
<el-table-column prop="businessType" label="业务类型" min-width="100" align="center">
<template #default="{ row }">
<el-tag :type="businessTypeTagType(row.businessType)" size="small">
{{ row.businessType }}
</el-tag>
</template>
</el-table-column>
<el-table-column
prop="customerName"
label="往来单位"
min-width="150"
align="center"
show-overflow-tooltip
/>
<el-table-column
prop="product"
label="产品"
min-width="120"
align="center"
show-overflow-tooltip
/>
<el-table-column prop="planDate" label="计划日期" min-width="120" align="center" />
<el-table-column prop="startDate" label="起始日期" min-width="120" align="center" />
<el-table-column prop="endDate" label="截止日期" min-width="120" align="center" />
<el-table-column prop="executionStatus" label="执行状态" min-width="110" align="center">
<template #default="{ row }">
<el-tag :type="planExecutionTagType(row.executionStatus)" size="small">
{{ row.executionStatus || '—' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="plannedQuantity" label="计划量(吨)" min-width="110" align="right" />
<el-table-column label="已执行量(吨)" min-width="110" align="right">
<template #default="{ row }">
{{ formatKgToTon(row.executedQuantity) }}
</template>
</el-table-column>
<el-table-column
prop="remarks"
label="备注"
min-width="150"
align="center"
show-overflow-tooltip
/>
</el-table>
</div>
</section>
<section v-if="data.weighing" class="db-related-table-section">
<h3 class="db-related-table-title">地磅单</h3>
<div class="view-subtable-wrap">
<el-table :data="weighingRows" class="view-subtable list-table" stripe>
<el-table-column
prop="batchNo"
label="出入库单号"
min-width="130"
align="center"
show-overflow-tooltip
/>
<el-table-column prop="vehicleNo" label="车牌号" min-width="110" align="center" />
<el-table-column
prop="cargoName"
label="货物名称"
min-width="120"
align="center"
show-overflow-tooltip
/>
<el-table-column prop="businessType" label="业务类型" min-width="100" align="center">
<template #default="{ row }">
<el-tag :type="businessTypeTagType(row.businessType)" size="small">
{{ row.businessType }}
</el-tag>
</template>
</el-table-column>
<el-table-column
prop="supplier"
label="供货单位"
min-width="150"
align="center"
show-overflow-tooltip
/>
<el-table-column
prop="consignee"
label="收货单位"
min-width="150"
align="center"
show-overflow-tooltip
/>
<el-table-column prop="grossWeight" label="毛重(kg)" min-width="110" align="right" />
<el-table-column
prop="grossWeightTime"
label="毛重时间"
min-width="160"
align="center"
/>
<el-table-column prop="tareWeight" label="皮重(kg)" min-width="110" align="right" />
<el-table-column
prop="tareWeightTime"
label="皮重时间"
min-width="160"
align="center"
/>
<el-table-column prop="deduction" label="扣杂(kg)" min-width="110" align="right">
<template #default="{ row }">
{{ row.deduction ?? 0 }}
</template>
</el-table-column>
<el-table-column prop="netWeight" label="净重(kg)" min-width="110" align="right" />
<el-table-column
prop="remarks"
label="备注"
min-width="150"
align="center"
show-overflow-tooltip
/>
</el-table>
</div>
</section>
</template>
</div>
<template #footer>
<el-button @click="innerVisible = false">
<el-icon><Close /></el-icon>
关闭
</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { Close } from '@element-plus/icons-vue'
import type { InventoryRelatedData } from '@/api/db/inventory'
import {
businessTypeTagType,
formatKgToTon,
planExecutionTagType
} from '@/components/business/db/db-related-view'
const props = defineProps<{
visible: boolean
loading?: boolean
data: InventoryRelatedData
}>()
const emit = defineEmits<{
(e: 'update:visible', val: boolean): void
}>()
const innerVisible = computed({
get: () => props.visible,
set: (val) => emit('update:visible', val)
})
const hasData = computed(() => Boolean(props.data.plan || props.data.weighing))
const planRows = computed(() => (props.data.plan ? [props.data.plan] : []))
const weighingRows = computed(() => (props.data.weighing ? [props.data.weighing] : []))
</script>
@@ -0,0 +1,148 @@
<template>
<el-dialog
v-model="innerVisible"
title="查看关联数据"
width="1200px"
align-center
destroy-on-close
class="dialog-form-layout db-related-table-dialog"
modal-class="order-module-dialog-mask"
>
<div v-loading="loading" class="db-related-body">
<el-empty v-if="!hasData" description="暂无关联数据" />
<template v-else>
<section v-if="data.inventories.length" class="db-related-table-section">
<h3 class="db-related-table-title">出入库单</h3>
<div class="view-subtable-wrap">
<el-table :data="data.inventories" class="view-subtable list-table" stripe>
<el-table-column type="index" label="序号" min-width="60" align="center" />
<el-table-column
prop="batchNo"
label="出入库单号"
min-width="130"
align="center"
show-overflow-tooltip
/>
<el-table-column prop="date" label="日期" min-width="120" align="center" />
<el-table-column
prop="partnerCompany"
label="往来单位"
min-width="150"
align="center"
show-overflow-tooltip
/>
<el-table-column prop="businessType" label="业务类型" min-width="100" align="center">
<template #default="{ row }">
<el-tag :type="businessTypeTagType(row.businessType)" size="small">
{{ row.businessType }}
</el-tag>
</template>
</el-table-column>
<el-table-column
prop="product"
label="产品"
min-width="120"
align="center"
show-overflow-tooltip
/>
<el-table-column prop="vehicleNo" label="车牌号" min-width="110" align="center" />
<el-table-column prop="quantity" label="数量(吨)" min-width="110" align="right" />
<el-table-column prop="status" label="状态" min-width="100" align="center">
<template #default="{ row }">
<el-tag :type="inventoryStatusTagType(row.status)" size="small">
{{ row.status || '待处理' }}
</el-tag>
</template>
</el-table-column>
<el-table-column
prop="remarks"
label="备注"
min-width="150"
align="center"
show-overflow-tooltip
/>
</el-table>
</div>
</section>
<section v-if="data.weighings.length" class="db-related-table-section">
<h3 class="db-related-table-title">地磅单</h3>
<div class="view-subtable-wrap">
<el-table :data="data.weighings" class="view-subtable list-table" stripe>
<el-table-column type="index" label="序号" min-width="60" align="center" />
<el-table-column
prop="batchNo"
label="出入库单号"
min-width="130"
align="center"
show-overflow-tooltip
/>
<el-table-column prop="vehicleNo" label="车牌号" min-width="110" align="center" />
<el-table-column
prop="cargoName"
label="货物名称"
min-width="120"
align="center"
show-overflow-tooltip
/>
<el-table-column prop="businessType" label="业务类型" min-width="100" align="center">
<template #default="{ row }">
<el-tag :type="businessTypeTagType(row.businessType)" size="small">
{{ row.businessType }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="grossWeight" label="毛重(kg)" min-width="110" align="right" />
<el-table-column
prop="grossWeightTime"
label="毛重时间"
min-width="160"
align="center"
/>
<el-table-column prop="tareWeight" label="皮重(kg)" min-width="110" align="right" />
<el-table-column
prop="tareWeightTime"
label="皮重时间"
min-width="160"
align="center"
/>
<el-table-column prop="netWeight" label="净重(kg)" min-width="110" align="right" />
</el-table>
</div>
</section>
</template>
</div>
<template #footer>
<el-button @click="innerVisible = false">
<el-icon><Close /></el-icon>
关闭
</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { Close } from '@element-plus/icons-vue'
import type { PlanRelatedData } from '@/api/db/plans'
import { businessTypeTagType, inventoryStatusTagType } from '@/components/business/db/db-related-view'
const props = defineProps<{
visible: boolean
loading?: boolean
data: PlanRelatedData
}>()
const emit = defineEmits<{
(e: 'update:visible', val: boolean): void
}>()
const innerVisible = computed({
get: () => props.visible,
set: (val) => emit('update:visible', val)
})
const hasData = computed(
() => props.data.inventories.length > 0 || props.data.weighings.length > 0
)
</script>
@@ -0,0 +1,37 @@
<template>
<SystemViewDialog
v-model:visible="innerVisible"
title="查看关联数据"
:icon="Link"
:sections="sections"
:loading="loading"
width="1200px"
modal-class="order-module-dialog-mask"
/>
</template>
<script setup lang="ts">
/**
* UI 模式 P09 · 标准查看弹窗
* 参考:SystemViewDialog · src/views/system/express/index.vue
*/
import { computed } from 'vue'
import { Link } from '@element-plus/icons-vue'
import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
import type { ViewSection } from '@/components/business/system-view-dialog/index.vue'
const props = defineProps<{
visible: boolean
loading?: boolean
sections: ViewSection[]
}>()
const emit = defineEmits<{
(e: 'update:visible', val: boolean): void
}>()
const innerVisible = computed({
get: () => props.visible,
set: (val) => emit('update:visible', val)
})
</script>
@@ -0,0 +1,95 @@
<template>
<el-select
v-model="model"
:placeholder="placeholder"
filterable
remote
:remote-method="loadOptions"
:loading="loading"
:allow-create="allowCreate"
default-first-option
clearable
style="width: 100%"
@focus="handleFocus"
>
<el-option
v-for="item in options"
:key="item.id"
:label="item.vehicleNo"
:value="item.vehicleNo"
/>
</el-select>
</template>
<script setup lang="ts">
import { vehicleApi, type Vehicle } from '@/api/db/vehicles'
defineOptions({ name: 'DbVehicleSelect' })
const model = defineModel<string>({ default: '' })
withDefaults(
defineProps<{
placeholder?: string
allowCreate?: boolean
}>(),
{
placeholder: '请选择或输入车牌号',
allowCreate: true
}
)
const loading = ref(false)
const options = ref<Vehicle[]>([])
const loadOptions = async (keyword: string) => {
loading.value = true
try {
options.value = await vehicleApi.list(keyword || undefined)
} catch (error) {
console.error('加载车牌号列表失败', error)
options.value = []
} finally {
loading.value = false
}
}
const handleFocus = () => {
if (options.value.length === 0) {
loadOptions('')
}
}
const isSameVehicleNo = (left: string, right: string) =>
left.trim().toLowerCase() === right.trim().toLowerCase()
const vehicleExists = async (vehicleNo: string) => {
const trimmed = vehicleNo.trim()
if (options.value.some((item) => isSameVehicleNo(item.vehicleNo, trimmed))) {
return true
}
const list = await vehicleApi.list(trimmed)
const matched = list.find((item) => isSameVehicleNo(item.vehicleNo, trimmed))
if (matched && !options.value.some((item) => item.id === matched.id)) {
options.value = [...options.value, matched]
}
return Boolean(matched)
}
/** 保存前确保车牌号已写入基础资料(已存在则跳过,与原地磅系统一致) */
const ensureVehicleExists = async () => {
const vehicleNo = model.value?.trim()
if (!vehicleNo) return
if (await vehicleExists(vehicleNo)) return
try {
await vehicleApi.create({ vehicleNo, remarks: null }, { showErrorMessage: false })
} catch (err: unknown) {
const status = (err as { response?: { status?: number } })?.response?.status
if (status !== 409) throw err
}
}
defineExpose({ ensureVehicleExists })
</script>
@@ -0,0 +1,67 @@
<template>
<div class="db-chart-card">
<h3 class="db-chart-title">{{ title }}</h3>
<div class="db-dual-bar__body">
<el-empty v-if="!data.length" description="今日无数据" />
<template v-else>
<div class="legend">
<span class="legend-item">
<span class="legend-color planned" />
计划量
</span>
<span class="legend-item">
<span class="legend-color actual" />
实际量
</span>
</div>
<div v-for="item in data" :key="item.label" class="chart-item">
<span class="chart-label">{{ item.label }}</span>
<div class="chart-bar-group">
<div class="chart-bar-wrapper">
<div
class="chart-bar planned"
:style="{
width: maxValue ? (item.planned / maxValue) * 100 + '%' : '0%',
minWidth: item.planned > 0 ? '4px' : '0'
}"
/>
</div>
<div class="chart-bar-wrapper">
<div
class="chart-bar actual"
:style="{
width: maxValue ? (item.actual / maxValue) * 100 + '%' : '0%',
minWidth: item.actual > 0 ? '4px' : '0'
}"
/>
</div>
</div>
<div class="chart-values">
<span class="value planned">{{ item.planned.toFixed(2) }}</span>
<span class="value actual">{{ item.actual.toFixed(2) }}</span>
</div>
</div>
</template>
</div>
</div>
</template>
<script setup lang="ts">
export interface DualBarItem {
label: string
planned: number
actual: number
}
const props = defineProps<{
title: string
data: DualBarItem[]
}>()
const maxValue = computed(() => {
if (!props.data.length) return 0
const maxPlanned = Math.max(...props.data.map((item) => item.planned))
const maxActual = Math.max(...props.data.map((item) => item.actual))
return Math.max(maxPlanned, maxActual) || 0
})
</script>
@@ -0,0 +1,201 @@
import type { Plan, PlanInventoryRecord, PlanRelatedData, PlanWeighingRecord } from '@/api/db/plans'
import type { InventoryRecord, InventoryRelatedData, InventoryWeighingRecord } from '@/api/db/inventory'
import type { WeighingRelatedData } from '@/api/db/weighing'
import type { TagType, ViewField, ViewSection } from '@/components/business/system-view-dialog/index.vue'
export function businessTypeTagType(businessType: string): TagType {
return businessType === '入库' ? 'success' : 'warning'
}
export function inventoryStatusTagType(status?: string): TagType {
if (status === '已完成') return 'success'
if (status === '待入库' || status === '待出库') return 'warning'
return 'info'
}
export function planExecutionTagType(status?: string): TagType {
if (status === '已入库' || status === '已出库') return 'success'
return 'warning'
}
export function formatKgToTon(kg: number | undefined | null): string {
if (kg === undefined || kg === null) return '—'
return (kg / 1000).toFixed(2)
}
function sectionTitle(base: string, index: number, total: number): string {
return total > 1 ? `${base}(${index + 1}/${total})` : base
}
export function buildPlanViewSection(plan: Plan): ViewSection {
return {
title: '计划',
fields: [
{ label: '计划号', value: plan.planNo },
{ label: '业务类型', value: plan.businessType, tagType: businessTypeTagType(plan.businessType) },
{ label: '往来单位', value: plan.customerName },
{ label: '产品', value: plan.product },
{ label: '规格', value: plan.specification },
{ label: '计划日期', value: plan.planDate },
{ label: '起始日期', value: plan.startDate },
{ label: '截止日期', value: plan.endDate },
{
label: '执行状态',
value: plan.executionStatus,
tagType: planExecutionTagType(plan.executionStatus)
},
{ label: '计划量(吨)', value: plan.plannedQuantity },
{ label: '已执行量(吨)', value: formatKgToTon(plan.executedQuantity) },
{ label: '备注', value: plan.remarks, span: true }
]
}
}
export function buildInventoryViewSection(
record: PlanInventoryRecord | InventoryRecord,
title = '出入库单'
): ViewSection {
return {
title,
fields: [
{ label: '出入库单号', value: record.batchNo },
{ label: '日期', value: record.date },
{ label: '往来单位', value: record.partnerCompany },
{
label: '业务类型',
value: record.businessType,
tagType: businessTypeTagType(record.businessType)
},
{ label: '产品', value: record.product },
{ label: '规格', value: record.specification },
{ label: '车牌号', value: record.vehicleNo },
{ label: '数量(吨)', value: record.quantity },
{
label: '状态',
value: record.status || '待处理',
tagType: inventoryStatusTagType(record.status)
},
{ label: '备注', value: record.remarks, span: true }
]
}
}
export function buildWeighingViewSection(
record: PlanWeighingRecord | InventoryWeighingRecord,
title = '地磅单'
): ViewSection {
const fields: ViewField[] = [
{ label: '出入库单号', value: record.batchNo },
{ label: '车牌号', value: record.vehicleNo },
{ label: '货物名称', value: record.cargoName },
{ label: '规格', value: 'specification' in record ? record.specification : undefined },
{
label: '业务类型',
value: record.businessType,
tagType: businessTypeTagType(record.businessType)
}
]
if ('supplier' in record) {
fields.push(
{ label: '供货单位', value: record.supplier },
{ label: '收货单位', value: record.consignee }
)
}
fields.push(
{ label: '毛重(kg)', value: record.grossWeight },
{ label: '毛重时间', value: record.grossWeightTime },
{ label: '皮重(kg)', value: record.tareWeight },
{ label: '皮重时间', value: record.tareWeightTime }
)
if ('deduction' in record) {
fields.push({ label: '扣杂(kg)', value: record.deduction || 0 })
}
fields.push({ label: '净重(kg)', value: record.netWeight })
if ('remarks' in record) {
fields.push({ label: '备注', value: record.remarks, span: true })
}
return { title, fields }
}
type MergedPlanRelatedItem = (PlanInventoryRecord | PlanWeighingRecord) & {
type: 'inventory' | 'weighing'
sortTime: string
}
function mergePlanRelatedItems(data: PlanRelatedData): MergedPlanRelatedItem[] {
const items: MergedPlanRelatedItem[] = []
data.inventories.forEach((inv) => {
items.push({
...inv,
type: 'inventory',
sortTime: inv.createdAt || inv.date || ''
})
})
data.weighings.forEach((w) => {
items.push({
...w,
type: 'weighing',
sortTime: w.grossWeightTime || w.createdAt || ''
})
})
return items.sort((a, b) => {
const timeA = a.sortTime ? new Date(a.sortTime).getTime() : 0
const timeB = b.sortTime ? new Date(b.sortTime).getTime() : 0
if (timeA === 0 && timeB === 0) return 0
if (timeA === 0) return 1
if (timeB === 0) return -1
return timeB - timeA
})
}
export function buildPlanRelatedSections(data: PlanRelatedData): ViewSection[] {
const items = mergePlanRelatedItems(data)
if (!items.length) {
return [{ title: '关联数据', fields: [{ label: '说明', value: '暂无关联数据', span: true }] }]
}
const inventoryCount = items.filter((item) => item.type === 'inventory').length
const weighingCount = items.filter((item) => item.type === 'weighing').length
let inventoryIndex = 0
let weighingIndex = 0
return items.map((item) => {
if (item.type === 'inventory') {
const title = sectionTitle('出入库单', inventoryIndex, inventoryCount)
inventoryIndex += 1
return buildInventoryViewSection(item, title)
}
const title = sectionTitle('地磅单', weighingIndex, weighingCount)
weighingIndex += 1
return buildWeighingViewSection(item, title)
})
}
export function buildInventoryRelatedSections(data: InventoryRelatedData): ViewSection[] {
const sections: ViewSection[] = []
if (data.plan) sections.push(buildPlanViewSection(data.plan))
if (data.weighing) sections.push(buildWeighingViewSection(data.weighing))
if (!sections.length) {
return [{ title: '关联数据', fields: [{ label: '说明', value: '暂无关联数据', span: true }] }]
}
return sections
}
export function buildWeighingRelatedSections(data: WeighingRelatedData): ViewSection[] {
const sections: ViewSection[] = []
if (data.plan) sections.push(buildPlanViewSection(data.plan))
if (data.inventory) sections.push(buildInventoryViewSection(data.inventory))
if (!sections.length) {
return [{ title: '关联数据', fields: [{ label: '说明', value: '暂无关联数据', span: true }] }]
}
return sections
}
@@ -116,14 +116,12 @@ const props = withDefaults(
hint?: string
showHint?: boolean
disabled?: boolean
maxSizeMb?: number
}>(),
{
bizId: 0,
hint: '支持图片、视频、PDF、Word、Excel等文件,单文件不超过 20MB',
hint: '支持图片、视频、PDF、Word、Excel等文件',
showHint: true,
disabled: false,
maxSizeMb: 20
disabled: false
}
)
@@ -151,13 +149,6 @@ async function handleAttachmentChange(uploadFile: UploadFile) {
const rawFile = uploadFile.raw
if (!rawFile) return
const maxBytes = props.maxSizeMb * 1024 * 1024
if (rawFile.size > maxBytes) {
ElMessage.error(`单文件不能超过 ${props.maxSizeMb}MB`)
uploadRef.value?.clearFiles()
return
}
const item: BxAttachmentItem = {
id: `upload-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
fileName: rawFile.name,