提交
This commit is contained in:
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,
|
||||
|
||||
Reference in new issue
Block a user