feat(web-admin): 出入库列表与编辑弹窗展示取送货及完整产品列

列表增加取送货地址;编辑弹窗主信息与产品八列对齐设计。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-07-19 16:47:02 +08:00
1 parent 8b2ebe28e6
commit 4d88ceb65e
1 file changed
+374
@@ -0,0 +1,374 @@
<template>
<div class="page-container">
<section class="panel">
<div class="panel-top">
<h2 class="title-bar-heading">{{ title }}</h2>
</div>
<div class="filter-block">
<el-form :model="searchForm" class="filter-form" @submit.prevent>
<div class="filter-grid">
<el-form-item label="单号">
<el-input v-model="searchForm.orderNo" clearable placeholder="单号" @keyup.enter="handleSearch" />
</el-form-item>
<el-form-item label="计划号">
<el-input v-model="searchForm.planNo" clearable placeholder="计划号" @keyup.enter="handleSearch" />
</el-form-item>
<el-form-item label="状态">
<el-select v-model="searchForm.status" clearable placeholder="全部">
<el-option label="进行中" value="in_progress" />
<el-option label="已完成" value="completed" />
<el-option label="已作废" value="voided" />
</el-select>
</el-form-item>
<el-form-item label="往来单位">
<el-input
v-model="searchForm.partnerCompany"
clearable
placeholder="往来单位"
@keyup.enter="handleSearch"
/>
</el-form-item>
</div>
<div class="filter-actions">
<div class="filter-actions-end">
<el-button class="btn-ghost" :loading="loading" @click="handleReset">
<el-icon><RefreshLeft /></el-icon>重置
</el-button>
<el-button class="btn-main" type="primary" :loading="loading" @click="handleSearch">
<el-icon><Search /></el-icon>搜索
</el-button>
</div>
</div>
</el-form>
</div>
<div class="list-toolbar">
<div class="list-toolbar-row">
<div class="list-toolbar-selected">
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
</div>
<el-button
class="btn-main"
type="primary"
:disabled="selectedRows.length !== 1"
@click="handleCopySiteLink"
>
<el-icon><Link /></el-icon>复制现场链接
</el-button>
<el-button class="btn-ghost" :disabled="selectedRows.length !== 1" @click="openEdit(selectedRows[0])">
<el-icon><Edit /></el-icon>编辑
</el-button>
</div>
</div>
<div class="list-body list-wrap">
<el-table
class="list-table"
:data="rows"
v-loading="loading"
stripe
row-key="id"
@selection-change="(val: PlIoOrder[]) => (selectedRows = val)"
@row-dblclick="openEdit"
>
<el-table-column type="selection" min-width="55" align="center" />
<el-table-column
type="index"
label="序号"
min-width="68"
align="center"
:index="(i: number) => (pagination.page - 1) * pagination.pageSize + i + 1"
/>
<el-table-column prop="orderNo" label="单号" min-width="150" show-overflow-tooltip>
<template #default="{ row }">
<el-link type="primary" @click="openEdit(row)">{{ row.orderNo }}</el-link>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" min-width="100" align="center">
<template #default="{ row }">
<el-tag :type="statusTagType(row.status)" size="small">{{ statusLabel(row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="planNo" label="计划号" min-width="140" show-overflow-tooltip />
<el-table-column prop="partnerCompany" label="往来单位" min-width="160" show-overflow-tooltip />
<el-table-column prop="vehicleNo" label="车牌" min-width="110" align="center" />
<el-table-column
prop="pickupAddress"
label="取货地址"
min-width="160"
show-overflow-tooltip
/>
<el-table-column label="送货地址" min-width="160" show-overflow-tooltip>
<template #default="{ row }">
{{ row.destinationAddress || row.destinationName || '—' }}
</template>
</el-table-column>
<el-table-column prop="productSummary" label="产品" min-width="180" show-overflow-tooltip />
<el-table-column prop="actualQtyTotal" label="实际数量合计" min-width="120" align="right" />
<el-table-column prop="siteFeedbackTime" label="最近提交" min-width="160" show-overflow-tooltip />
</el-table>
<div class="pagination list-pager-bar">
<el-pagination
v-model:current-page="pagination.page"
v-model:page-size="pagination.pageSize"
:total="pagination.total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
@current-change="fetchData"
@size-change="fetchData"
/>
</div>
</div>
</section>
<el-dialog
v-model="dialogVisible"
:title="kind === 'inbound' ? '编辑入库单' : '编辑出库单'"
width="1080px"
align-center
destroy-on-close
class="dialog-form-layout"
:close-on-click-modal="false"
>
<el-form v-if="editForm" label-position="top" class="form-grid-2">
<el-form-item label="单号">
<el-input :model-value="editForm.orderNo" disabled />
</el-form-item>
<el-form-item label="状态">
<el-select v-model="editForm.status" :disabled="editForm.status === 'voided'">
<el-option label="进行中" value="in_progress" />
<el-option label="已完成" value="completed" />
<el-option label="已作废" value="voided" />
</el-select>
</el-form-item>
<el-form-item label="计划号">
<el-input :model-value="editForm.planNo" disabled />
</el-form-item>
<el-form-item label="车牌">
<el-input :model-value="editForm.vehicleNo" disabled />
</el-form-item>
<el-form-item label="往来单位" class="form-col-span-2">
<el-input :model-value="editForm.partnerCompany" disabled />
</el-form-item>
<el-form-item label="取货地址" class="form-col-span-2">
<el-input :model-value="editForm.pickupAddress || '—'" disabled />
</el-form-item>
<el-form-item label="取货时间">
<el-input :model-value="formatDisplayTime(editForm.pickupTime)" disabled />
</el-form-item>
<el-form-item label="取货注意事项" class="form-col-span-2">
<el-input
type="textarea"
:rows="2"
:model-value="editForm.pickupNotes || '—'"
disabled
/>
</el-form-item>
<el-form-item label="送货地址" class="form-col-span-2">
<el-input
:model-value="editForm.destinationAddress || editForm.destinationName || '—'"
disabled
/>
</el-form-item>
<el-form-item label="送达时间">
<el-input :model-value="formatDisplayTime(editForm.deliveryTime)" disabled />
</el-form-item>
<el-form-item label="送货注意事项" class="form-col-span-2">
<el-input
type="textarea"
:rows="2"
:model-value="editForm.destinationNotes || '—'"
disabled
/>
</el-form-item>
<el-form-item label="产品明细" class="form-col-span-2">
<el-table :data="editForm.items || []" size="small" border>
<el-table-column prop="productNo" label="产品编号" min-width="110" show-overflow-tooltip />
<el-table-column prop="productName" label="产品名称" min-width="120" show-overflow-tooltip />
<el-table-column prop="specification" label="型号/规格" min-width="100" show-overflow-tooltip />
<el-table-column label="单位" min-width="72" align="center">
<template #default="{ row }">{{ row.unitLabel || row.unit || '—' }}</template>
</el-table-column>
<el-table-column prop="planQty" label="计划数量" min-width="100" align="right" />
<el-table-column label="实际数量" min-width="120">
<template #default="{ row }">
<el-input-number
v-model="row.actualQty"
:min="0"
:precision="3"
:controls="false"
:disabled="editForm.status === 'voided'"
/>
</template>
</el-table-column>
<el-table-column prop="packageSpec" label="包装规格" min-width="110" show-overflow-tooltip />
<el-table-column prop="remark" label="备注" min-width="120" show-overflow-tooltip />
</el-table>
</el-form-item>
<el-form-item label="现场备注" class="form-col-span-2">
<el-input
v-model="editForm.siteFeedbackRemark"
type="textarea"
:rows="3"
maxlength="500"
show-word-limit
:disabled="editForm.status === 'voided'"
/>
</el-form-item>
<el-form-item label="现场附件" class="form-col-span-2">
<AttachmentUploadField
v-model="editImagesCsv"
:disabled="editForm.status === 'voided'"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">
<el-icon><Close /></el-icon>取消
</el-button>
<el-button type="primary" :loading="saving" :disabled="editForm?.status === 'voided'" @click="handleSave">
<el-icon><Check /></el-icon>确定
</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { onMounted, reactive, ref } from 'vue'
import { Check, Close, Edit, Link, RefreshLeft, Search } from '@element-plus/icons-vue'
import AttachmentUploadField from '@/components/business/attachment-upload-field/index.vue'
import {
inboundOrderApi,
outboundOrderApi,
type PlIoOrder,
type PlIoOrderQuery
} from '@/api/pl'
const props = defineProps<{
kind: 'inbound' | 'outbound'
title: string
}>()
const api = props.kind === 'inbound' ? inboundOrderApi : outboundOrderApi
const loading = ref(false)
const saving = ref(false)
const rows = ref<PlIoOrder[]>([])
const selectedRows = ref<PlIoOrder[]>([])
const dialogVisible = ref(false)
const editForm = ref<PlIoOrder | null>(null)
const editImagesCsv = ref('')
const pagination = reactive({ page: 1, pageSize: 20, total: 0 })
const searchForm = reactive<PlIoOrderQuery>({
orderNo: '',
planNo: '',
status: '',
partnerCompany: ''
})
function statusLabel(s: string) {
const map: Record<string, string> = {
in_progress: '进行中',
completed: '已完成',
voided: '已作废'
}
return map[s] || s
}
function statusTagType(s: string) {
if (s === 'completed') return 'success'
if (s === 'voided') return 'info'
return 'warning'
}
function formatDisplayTime(value?: string) {
if (!value) return '—'
const normalized = String(value).replace('T', ' ')
return normalized.length > 16 ? normalized.slice(0, 16) : normalized
}
async function fetchData() {
loading.value = true
try {
const res = await api.list({
...searchForm,
page: pagination.page,
pageSize: pagination.pageSize
})
rows.value = res.list || []
pagination.total = res.total || 0
} finally {
loading.value = false
}
}
function handleSearch() {
pagination.page = 1
fetchData()
}
function handleReset() {
searchForm.orderNo = ''
searchForm.planNo = ''
searchForm.status = ''
searchForm.partnerCompany = ''
handleSearch()
}
async function openEdit(row?: PlIoOrder) {
if (!row?.id) return
const detail = await api.get(row.id)
editForm.value = {
...detail,
items: (detail.items || []).map(it => ({ ...it }))
}
editImagesCsv.value = Array.isArray(detail.siteFeedbackImages)
? detail.siteFeedbackImages.filter(Boolean).join(',')
: ''
dialogVisible.value = true
}
async function handleSave() {
if (!editForm.value) return
saving.value = true
try {
const images = editImagesCsv.value
? editImagesCsv.value.split(',').map(s => s.trim()).filter(Boolean)
: []
await api.patch(editForm.value.id, {
status: editForm.value.status,
siteFeedbackRemark: editForm.value.siteFeedbackRemark,
siteFeedbackImages: images,
items: (editForm.value.items || []).map(it => ({
id: it.id,
actualQty: it.actualQty
}))
})
ElMessage.success('保存成功')
dialogVisible.value = false
fetchData()
} catch (e) {
ElMessage.error(e instanceof Error ? e.message : '保存失败')
} finally {
saving.value = false
}
}
async function handleCopySiteLink() {
const row = selectedRows.value[0]
if (!row) return
const data = await api.siteUrl(row.id)
const url = data.siteUrl || row.siteUrl
if (!url) {
ElMessage.warning('暂无现场链接')
return
}
await navigator.clipboard.writeText(url)
ElMessage.success('已复制现场链接')
}
onMounted(fetchData)
</script>