540 lines
17 KiB
Vue
540 lines
17 KiB
Vue
<template>
|
|
<div class="page-container">
|
|
<n-card>
|
|
<div class="table-toolbar">
|
|
<n-space>
|
|
<n-button type="primary" @click="handleGoCreate">
|
|
<template #icon><n-icon><AddOutline /></n-icon></template>
|
|
新建工序委外
|
|
</n-button>
|
|
<n-button disabled title="待后端接口支持">下推</n-button>
|
|
<n-button type="error" :disabled="selectedIds.length === 0" @click="handleBatchDelete">
|
|
<template #icon><n-icon><TrashOutline /></n-icon></template>
|
|
批量删除
|
|
</n-button>
|
|
</n-space>
|
|
<n-space>
|
|
<n-button quaternary circle @click="loadData">
|
|
<template #icon><n-icon><RefreshOutline /></n-icon></template>
|
|
</n-button>
|
|
</n-space>
|
|
</div>
|
|
|
|
<div class="search-form">
|
|
<n-form inline :model="searchForm" label-placement="left" size="small">
|
|
<n-form-item label="委外编号">
|
|
<n-input v-model:value="searchForm.outsourceCode" placeholder="工单委外编号" clearable style="width: 160px" />
|
|
</n-form-item>
|
|
<n-form-item label="供应商">
|
|
<n-input v-model:value="searchForm.supplierName" placeholder="委外供应商" clearable style="width: 140px" />
|
|
</n-form-item>
|
|
<n-form-item label="工序">
|
|
<n-input v-model:value="searchForm.name" placeholder="委外工序" clearable style="width: 140px" />
|
|
</n-form-item>
|
|
<n-form-item label="状态">
|
|
<n-select
|
|
v-model:value="searchForm.status"
|
|
placeholder="当前阶段"
|
|
clearable
|
|
style="width: 120px"
|
|
:options="statusOptions"
|
|
/>
|
|
</n-form-item>
|
|
<n-form-item>
|
|
<n-space>
|
|
<n-button type="primary" size="small" @click="handleSearch">
|
|
<template #icon><n-icon><SearchOutline /></n-icon></template>
|
|
搜索
|
|
</n-button>
|
|
<n-button size="small" @click="handleReset">重置</n-button>
|
|
</n-space>
|
|
</n-form-item>
|
|
</n-form>
|
|
</div>
|
|
|
|
<n-data-table
|
|
:columns="columns"
|
|
:data="tableData"
|
|
:loading="loading"
|
|
:pagination="pagination"
|
|
:row-key="(row) => row.id!"
|
|
:scroll-x="1520"
|
|
@update:page="handlePageChange"
|
|
@update:page-size="handlePageSizeChange"
|
|
@update:checked-row-keys="handleCheck"
|
|
/>
|
|
</n-card>
|
|
|
|
<!-- 详情 -->
|
|
<n-modal v-model:show="detailVisible" preset="card" title="委外工单详情" style="width: 760px">
|
|
<n-descriptions v-if="detailData" :column="2" bordered size="small" label-placement="left">
|
|
<n-descriptions-item label="工单委外编号">{{ detailData.outsourceCode || '-' }}</n-descriptions-item>
|
|
<n-descriptions-item label="当前阶段">{{ statusLabel(detailData.status) }}</n-descriptions-item>
|
|
<n-descriptions-item label="委外工序">{{ detailData.name || '-' }}</n-descriptions-item>
|
|
<n-descriptions-item label="工序工单号">{{ detailData.businessCode || '-' }}</n-descriptions-item>
|
|
<n-descriptions-item label="委外供应商">{{ detailData.supplierName || '-' }}</n-descriptions-item>
|
|
<n-descriptions-item label="优先级">{{ priorityLabel(detailData.priority) }}</n-descriptions-item>
|
|
<n-descriptions-item label="委外数量">{{ formatQty(detailData.quantity) }}</n-descriptions-item>
|
|
<n-descriptions-item label="委外单价">{{ formatMoney(detailData.unitPrice) }}</n-descriptions-item>
|
|
<n-descriptions-item label="委外总金额">{{ formatMoney(detailData.totalAmount) }}</n-descriptions-item>
|
|
<n-descriptions-item label="已发料数量">{{ formatDecimal(detailData.issuedQuantity) }}</n-descriptions-item>
|
|
<n-descriptions-item label="已收货数量">{{ formatDecimal(detailData.receivedQuantity) }}</n-descriptions-item>
|
|
<n-descriptions-item label="合格数量">{{ formatDecimal(detailData.qualifiedQuantity) }}</n-descriptions-item>
|
|
<n-descriptions-item label="预计开始">{{ detailData.beginPreparationTime || '-' }}</n-descriptions-item>
|
|
<n-descriptions-item label="预计结束">{{ detailData.endPreparationTime || '-' }}</n-descriptions-item>
|
|
<n-descriptions-item label="备注" :span="2">{{ detailData.remark || '-' }}</n-descriptions-item>
|
|
</n-descriptions>
|
|
</n-modal>
|
|
|
|
<!-- 编辑 -->
|
|
<n-modal v-model:show="modalVisible" preset="card" :title="modalTitle" style="width: 720px">
|
|
<n-form ref="formRef" :model="formData" :rules="formRules" label-placement="left" label-width="110px">
|
|
<n-grid :cols="2" :x-gap="16">
|
|
<n-form-item-gi label="工单委外编号">
|
|
<n-input :value="formData.outsourceCode" disabled />
|
|
</n-form-item-gi>
|
|
<n-form-item-gi label="委外工序">
|
|
<n-input :value="formData.name" disabled />
|
|
</n-form-item-gi>
|
|
<n-form-item-gi label="供应商" path="supplierName">
|
|
<n-input v-model:value="formData.supplierName" placeholder="请输入供应商" />
|
|
</n-form-item-gi>
|
|
<n-form-item-gi label="委外数量" path="quantity">
|
|
<n-input-number v-model:value="formData.quantity" :min="1" :precision="0" style="width: 100%" />
|
|
</n-form-item-gi>
|
|
<n-form-item-gi label="单价" path="unitPrice">
|
|
<n-input-number v-model:value="formData.unitPrice" :min="0" :precision="2" style="width: 100%" />
|
|
</n-form-item-gi>
|
|
<n-form-item-gi label="优先级" path="priority">
|
|
<n-select v-model:value="formData.priority" :options="priorityOptions" />
|
|
</n-form-item-gi>
|
|
<n-form-item-gi label="当前阶段" path="status">
|
|
<n-select v-model:value="formData.status" :options="statusOptions" />
|
|
</n-form-item-gi>
|
|
<n-form-item-gi label="预计开始" path="beginPreparationTime">
|
|
<n-date-picker v-model:value="formData.beginPreparationTime" type="datetime" clearable style="width: 100%" />
|
|
</n-form-item-gi>
|
|
<n-form-item-gi label="预计结束" path="endPreparationTime">
|
|
<n-date-picker v-model:value="formData.endPreparationTime" type="datetime" clearable style="width: 100%" />
|
|
</n-form-item-gi>
|
|
<n-form-item-gi :span="2" label="备注" path="remark">
|
|
<n-input v-model:value="formData.remark" type="textarea" :autosize="{ minRows: 2, maxRows: 4 }" />
|
|
</n-form-item-gi>
|
|
</n-grid>
|
|
</n-form>
|
|
<template #footer>
|
|
<n-space justify="end">
|
|
<n-button @click="modalVisible = false">取消</n-button>
|
|
<n-button type="primary" :loading="submitLoading" @click="handleSubmit">保存</n-button>
|
|
</n-space>
|
|
</template>
|
|
</n-modal>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, reactive, h, onMounted } from 'vue'
|
|
import { useRouter } from 'vue-router'
|
|
import {
|
|
NButton, NSpace, NIcon, NTag, NModal, NForm, NFormItem, NFormItemGi, NGrid,
|
|
NInput, NInputNumber, NSelect, NDatePicker, NDescriptions, NDescriptionsItem,
|
|
useMessage, useDialog, type DataTableColumns, type FormRules,
|
|
} from 'naive-ui'
|
|
import {
|
|
SearchOutline, RefreshOutline, AddOutline, TrashOutline,
|
|
CopyOutline, EllipseOutline, TimeOutline,
|
|
} from '@vicons/ionicons5'
|
|
import {
|
|
outsourcingApi,
|
|
OUTSOURCE_STATUS_MAP,
|
|
OUTSOURCE_PRIORITY_MAP,
|
|
type Outsourcing,
|
|
} from '@/api/outsourcing'
|
|
|
|
const message = useMessage()
|
|
const dialog = useDialog()
|
|
const router = useRouter()
|
|
|
|
const searchForm = reactive({
|
|
outsourceCode: '',
|
|
supplierName: '',
|
|
name: '',
|
|
status: null as number | null,
|
|
})
|
|
|
|
const statusOptions = Object.entries(OUTSOURCE_STATUS_MAP).map(([value, item]) => ({
|
|
label: item.label,
|
|
value: Number(value),
|
|
}))
|
|
|
|
const priorityOptions = Object.entries(OUTSOURCE_PRIORITY_MAP).map(([value, item]) => ({
|
|
label: item.label,
|
|
value: Number(value),
|
|
}))
|
|
|
|
const tableData = ref<Outsourcing[]>([])
|
|
const loading = ref(false)
|
|
const submitLoading = ref(false)
|
|
const selectedIds = ref<number[]>([])
|
|
const pagination = reactive({
|
|
page: 1,
|
|
pageSize: 10,
|
|
itemCount: 0,
|
|
showSizePicker: true,
|
|
pageSizes: [10, 20, 50],
|
|
})
|
|
|
|
const detailVisible = ref(false)
|
|
const detailData = ref<Outsourcing | null>(null)
|
|
const modalVisible = ref(false)
|
|
const modalTitle = ref('')
|
|
const formRef = ref()
|
|
|
|
const defaultFormData = (): Record<string, unknown> => ({
|
|
id: undefined,
|
|
outsourceCode: '',
|
|
name: '',
|
|
supplierName: '',
|
|
quantity: null,
|
|
unitPrice: null,
|
|
priority: 1,
|
|
status: 1,
|
|
beginPreparationTime: null,
|
|
endPreparationTime: null,
|
|
remark: '',
|
|
})
|
|
const formData = reactive<Record<string, unknown>>(defaultFormData())
|
|
|
|
const formRules: FormRules = {
|
|
supplierName: [{ required: true, message: '请输入供应商', trigger: 'blur' }],
|
|
quantity: [{ required: true, type: 'number', message: '请输入委外数量', trigger: ['blur', 'change'] }],
|
|
}
|
|
|
|
function formatQty(val?: number | null) {
|
|
if (val == null) return '0.00'
|
|
return Number(val).toFixed(2)
|
|
}
|
|
|
|
function formatDecimal(val?: number | string | null) {
|
|
if (val == null || val === '') return '0.00'
|
|
return Number(val).toFixed(2)
|
|
}
|
|
|
|
function formatMoney(val?: number | string | null, mask = false) {
|
|
if (mask) return '¥***'
|
|
if (val == null || val === '') return '-'
|
|
return `¥${Number(val).toFixed(2)}`
|
|
}
|
|
|
|
function statusLabel(status?: number) {
|
|
if (status == null) return '-'
|
|
return OUTSOURCE_STATUS_MAP[status]?.label ?? String(status)
|
|
}
|
|
|
|
function priorityLabel(priority?: number) {
|
|
if (priority == null) return '-'
|
|
return OUTSOURCE_PRIORITY_MAP[priority]?.label ?? String(priority)
|
|
}
|
|
|
|
async function copyText(text: string) {
|
|
try {
|
|
await navigator.clipboard.writeText(text)
|
|
message.success('已复制')
|
|
} catch {
|
|
message.warning('复制失败')
|
|
}
|
|
}
|
|
|
|
const columns: DataTableColumns<Outsourcing> = [
|
|
{ type: 'selection', width: 48, align: 'center', fixed: 'left' },
|
|
{
|
|
title: '工单委外编号',
|
|
key: 'outsourceCode',
|
|
width: 200,
|
|
align: 'center',
|
|
fixed: 'left',
|
|
ellipsis: { tooltip: true },
|
|
render(row) {
|
|
const code = row.outsourceCode || (row.id ? `WW${row.id}` : '-')
|
|
return h('div', { class: 'cell-center code-cell' }, [
|
|
h(NButton, {
|
|
text: true,
|
|
type: 'primary',
|
|
size: 'small',
|
|
onClick: () => openDetail(row),
|
|
}, { default: () => code }),
|
|
h(NButton, {
|
|
text: true,
|
|
size: 'tiny',
|
|
onClick: () => copyText(code),
|
|
}, {
|
|
default: () => h(NIcon, { size: 14 }, { default: () => h(CopyOutline) }),
|
|
}),
|
|
])
|
|
},
|
|
},
|
|
{
|
|
title: '委外数量',
|
|
key: 'quantity',
|
|
width: 96,
|
|
align: 'center',
|
|
render: (row) => formatQty(row.quantity),
|
|
},
|
|
{
|
|
title: '委外供应商',
|
|
key: 'supplierName',
|
|
width: 140,
|
|
align: 'center',
|
|
ellipsis: { tooltip: true },
|
|
},
|
|
{
|
|
title: '委外工序',
|
|
key: 'name',
|
|
width: 120,
|
|
align: 'center',
|
|
ellipsis: { tooltip: true },
|
|
render: (row) => row.name || '-',
|
|
},
|
|
{
|
|
title: '委外单价',
|
|
key: 'unitPrice',
|
|
width: 96,
|
|
align: 'center',
|
|
render: (row) => formatMoney(row.unitPrice, true),
|
|
},
|
|
{
|
|
title: '委外总金额',
|
|
key: 'totalAmount',
|
|
width: 108,
|
|
align: 'center',
|
|
render: (row) => formatMoney(row.totalAmount, true),
|
|
},
|
|
{
|
|
title: '优先级',
|
|
key: 'priority',
|
|
width: 88,
|
|
align: 'center',
|
|
render(row) {
|
|
const item = OUTSOURCE_PRIORITY_MAP[row.priority ?? 1] ?? OUTSOURCE_PRIORITY_MAP[1]
|
|
return h('div', { class: 'cell-center' }, [
|
|
h(NTag, { size: 'small', type: item.type, bordered: false }, { default: () => item.label }),
|
|
])
|
|
},
|
|
},
|
|
{
|
|
title: '已发料数量',
|
|
key: 'issuedQuantity',
|
|
width: 104,
|
|
align: 'center',
|
|
render: (row) => formatDecimal(row.issuedQuantity),
|
|
},
|
|
{
|
|
title: '已收货数量',
|
|
key: 'receivedQuantity',
|
|
width: 104,
|
|
align: 'center',
|
|
render: (row) => formatDecimal(row.receivedQuantity),
|
|
},
|
|
{
|
|
title: '合格数量',
|
|
key: 'qualifiedQuantity',
|
|
width: 96,
|
|
align: 'center',
|
|
render: (row) => formatDecimal(row.qualifiedQuantity),
|
|
},
|
|
{
|
|
title: '当前阶段',
|
|
key: 'status',
|
|
width: 100,
|
|
align: 'center',
|
|
render(row) {
|
|
const status = row.status ?? 1
|
|
const item = OUTSOURCE_STATUS_MAP[status] ?? OUTSOURCE_STATUS_MAP[1]
|
|
const icon = status === 1 ? TimeOutline : EllipseOutline
|
|
const color = status === 1 ? '#2080f0' : '#bfbfbf'
|
|
return h('div', { class: 'cell-center status-cell' }, [
|
|
h(NIcon, { color, size: 16 }, { default: () => h(icon) }),
|
|
h('span', item.label),
|
|
])
|
|
},
|
|
},
|
|
{
|
|
title: '操作',
|
|
key: 'actions',
|
|
width: 120,
|
|
align: 'center',
|
|
fixed: 'right',
|
|
render(row) {
|
|
return h('div', { class: 'cell-center' }, [
|
|
h(NSpace, { size: 4, justify: 'center' }, {
|
|
default: () => [
|
|
h(NButton, { text: true, type: 'primary', size: 'small', onClick: () => openDetail(row) }, { default: () => '详情' }),
|
|
h(NButton, { text: true, type: 'primary', size: 'small', onClick: () => handleEdit(row) }, { default: () => '编辑' }),
|
|
],
|
|
}),
|
|
])
|
|
},
|
|
},
|
|
]
|
|
|
|
async function loadData() {
|
|
loading.value = true
|
|
try {
|
|
const res = await outsourcingApi.page({
|
|
page: pagination.page,
|
|
pageSize: pagination.pageSize,
|
|
name: searchForm.name || undefined,
|
|
outsourceCode: searchForm.outsourceCode || undefined,
|
|
supplierName: searchForm.supplierName || undefined,
|
|
status: searchForm.status ?? undefined,
|
|
})
|
|
tableData.value = res.list ?? []
|
|
pagination.itemCount = res.total ?? 0
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
function handleSearch() {
|
|
pagination.page = 1
|
|
loadData()
|
|
}
|
|
|
|
function handleReset() {
|
|
searchForm.outsourceCode = ''
|
|
searchForm.supplierName = ''
|
|
searchForm.name = ''
|
|
searchForm.status = null
|
|
handleSearch()
|
|
}
|
|
|
|
function handlePageChange(page: number) {
|
|
pagination.page = page
|
|
loadData()
|
|
}
|
|
|
|
function handlePageSizeChange(pageSize: number) {
|
|
pagination.pageSize = pageSize
|
|
pagination.page = 1
|
|
loadData()
|
|
}
|
|
|
|
function handleCheck(keys: Array<string | number>) {
|
|
selectedIds.value = keys as number[]
|
|
}
|
|
|
|
function handleGoCreate() {
|
|
router.push('/biz/orderProcessPlan/board')
|
|
message.info('请在看板中选择工序后创建委外')
|
|
}
|
|
|
|
function parseDateTime(val?: string | null) {
|
|
if (!val) return null
|
|
const ts = new Date(val.replace(' ', 'T')).getTime()
|
|
return Number.isNaN(ts) ? null : ts
|
|
}
|
|
|
|
async function openDetail(row: Outsourcing) {
|
|
if (!row.id) return
|
|
detailData.value = await outsourcingApi.detail(row.id)
|
|
detailVisible.value = true
|
|
}
|
|
|
|
function handleEdit(row: Outsourcing) {
|
|
modalTitle.value = '编辑委外工单'
|
|
Object.assign(formData, defaultFormData(), row, {
|
|
quantity: row.quantity ?? null,
|
|
unitPrice: row.unitPrice != null ? Number(row.unitPrice) : null,
|
|
beginPreparationTime: parseDateTime(row.beginPreparationTime),
|
|
endPreparationTime: parseDateTime(row.endPreparationTime),
|
|
})
|
|
modalVisible.value = true
|
|
}
|
|
|
|
function formatSubmitDateTime(ts: number | null) {
|
|
if (!ts) return undefined
|
|
return new Date(ts).toISOString().slice(0, 19).replace('T', ' ')
|
|
}
|
|
|
|
async function handleSubmit() {
|
|
await formRef.value?.validate()
|
|
submitLoading.value = true
|
|
try {
|
|
const qty = formData.quantity as number
|
|
const price = formData.unitPrice as number | null
|
|
const payload: Outsourcing = {
|
|
id: formData.id as number,
|
|
supplierName: formData.supplierName as string,
|
|
quantity: qty,
|
|
unitPrice: price ?? undefined,
|
|
totalAmount: price != null ? Number((qty * price).toFixed(2)) : undefined,
|
|
priority: formData.priority as number,
|
|
status: formData.status as number,
|
|
beginPreparationTime: formatSubmitDateTime(formData.beginPreparationTime as number | null),
|
|
endPreparationTime: formatSubmitDateTime(formData.endPreparationTime as number | null),
|
|
remark: (formData.remark as string) || undefined,
|
|
}
|
|
await outsourcingApi.update(payload)
|
|
message.success('保存成功')
|
|
modalVisible.value = false
|
|
loadData()
|
|
} finally {
|
|
submitLoading.value = false
|
|
}
|
|
}
|
|
|
|
function handleBatchDelete() {
|
|
dialog.warning({
|
|
title: '提示',
|
|
content: `确定删除选中的 ${selectedIds.value.length} 条委外工单吗?`,
|
|
positiveText: '确定',
|
|
negativeText: '取消',
|
|
onPositiveClick: async () => {
|
|
await outsourcingApi.delete(selectedIds.value)
|
|
message.success('删除成功')
|
|
selectedIds.value = []
|
|
loadData()
|
|
},
|
|
})
|
|
}
|
|
|
|
onMounted(loadData)
|
|
</script>
|
|
|
|
<style scoped>
|
|
.search-form {
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.table-toolbar {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.cell-center {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 100%;
|
|
}
|
|
|
|
.code-cell {
|
|
gap: 4px;
|
|
}
|
|
|
|
.status-cell {
|
|
gap: 6px;
|
|
font-size: 13px;
|
|
}
|
|
|
|
:deep(.n-data-table-th) {
|
|
text-align: center !important;
|
|
}
|
|
|
|
:deep(.n-data-table-td) {
|
|
text-align: center;
|
|
}
|
|
</style>
|