|
|
@@ -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>
|