采购件,半成品,产品页面初步完成
This commit is contained in:
@ -1,11 +1,340 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="product-module">
|
||||
<div class="header-tools">
|
||||
<div class="left-tools">
|
||||
<el-input v-model="queryParams.keyword" placeholder="🔍 搜索物料 / SN / 工单 / 订单号..." class="search-input" clearable @clear="fetchData" @keyup.enter="fetchData">
|
||||
<template #append><el-button :icon="Search" @click="fetchData" /></template>
|
||||
</el-input>
|
||||
</div>
|
||||
<div class="right-tools">
|
||||
<el-button type="primary" :icon="Plus" @click="handleCreate" class="action-btn">成品入库登记</el-button>
|
||||
<el-button :icon="Refresh" @click="fetchData" class="action-btn">刷新</el-button>
|
||||
<el-popover placement="bottom-end" title="列配置" :width="500" trigger="click">
|
||||
<template #reference><el-button :icon="Setting" class="action-btn">表头</el-button></template>
|
||||
<el-checkbox-group v-model="visibleColumnProps" class="column-selector">
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="8" v-for="c in allColumns" :key="c.prop"><el-checkbox :value="c.prop">{{ c.label }}</el-checkbox></el-col>
|
||||
</el-row>
|
||||
</el-checkbox-group>
|
||||
</el-popover>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-table v-loading="loading" :data="tableData" border stripe style="width: 100%" class="modern-table" header-cell-class-name="table-header-gray">
|
||||
<template v-for="col in allColumns" :key="col.prop">
|
||||
<el-table-column v-if="visibleColumnProps.includes(col.prop)" :prop="col.prop" :label="col.label" :min-width="col.minWidth || '120'" show-overflow-tooltip>
|
||||
|
||||
<template #default="scope" v-if="['serial_number'].includes(col.prop)">
|
||||
<span v-if="scope.row[col.prop]" class="tag-sn">{{ scope.row[col.prop] }}</span>
|
||||
<span v-else class="text-placeholder">-</span>
|
||||
</template>
|
||||
|
||||
<template #default="scope" v-else-if="col.prop === 'qty_stock'">
|
||||
<span class="stock-num">{{ scope.row.sum_stock }}</span><el-tag size="small" type="info" effect="plain" class="sum-tag">总</el-tag>
|
||||
</template>
|
||||
|
||||
<template #default="scope" v-else-if="col.prop === 'status'">
|
||||
<el-tag :type="getStatusType(scope.row.status)" effect="light" round>{{ scope.row.status }}</el-tag>
|
||||
</template>
|
||||
|
||||
<template #default="scope" v-else-if="col.prop === 'quality_status'">
|
||||
<el-tag :type="getQualityType(scope.row.quality_status)" effect="dark" size="small">{{ scope.row.quality_status }}</el-tag>
|
||||
</template>
|
||||
|
||||
<template #default="scope" v-else-if="['quality_report_link', 'detail_link', 'inspection_report_link'].includes(col.prop)">
|
||||
<el-link v-if="scope.row[col.prop]" type="primary" :href="scope.row[col.prop]" target="_blank" :underline="false">
|
||||
<el-icon><Link /></el-icon> 查看
|
||||
</el-link>
|
||||
</template>
|
||||
|
||||
<template #default="scope" v-else-if="['sale_price', 'raw_material_cost', 'manual_cost'].includes(col.prop)">
|
||||
<span class="money-text">{{ formatMoney(scope.row[col.prop]) }}</span>
|
||||
</template>
|
||||
|
||||
</el-table-column>
|
||||
</template>
|
||||
|
||||
<el-table-column label="操作" width="160" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="handleUpdate(row)">编辑</el-button>
|
||||
<el-popconfirm title="确定删除?" @confirm="handleDelete(row)"><template #reference><el-button link type="danger">删除</el-button></template></el-popconfirm>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-pagination class="pagination-bar" v-model:current-page="queryParams.page" v-model:page-size="queryParams.pageSize" :total="total" layout="total, sizes, prev, pager, next" background @change="fetchData" />
|
||||
|
||||
<el-dialog v-model="visible" :title="dialogStatus === 'create' ? '成品入库' : '编辑成品'" width="1100px" top="5vh" :close-on-click-modal="false" class="stylish-dialog">
|
||||
<el-form :model="form" label-width="110px" ref="formRef" :rules="rules" size="large" class="stylish-form">
|
||||
|
||||
<div class="form-card basic-card">
|
||||
<div class="card-title"><el-icon class="icon"><Box /></el-icon><span>1. 基础信息</span></div>
|
||||
<div class="card-content">
|
||||
<el-row :gutter="24" v-if="dialogStatus === 'create'" style="margin-bottom: 20px;">
|
||||
<el-col :span="14">
|
||||
<el-form-item label="物料搜索" prop="base_id">
|
||||
<el-select v-model="form.base_id" filterable remote reserve-keyword placeholder="搜名称/规格..." :remote-method="handleSearchMaterial" :loading="searchLoading" style="width: 100%" @change="onMaterialSelected">
|
||||
<el-option v-for="item in materialOptions" :key="item.id" :label="item.name" :value="item.id">
|
||||
<div class="option-item"><span class="opt-name">{{ item.name }}</span><span class="opt-spec">{{ item.spec }}</span></div>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<div class="read-only-grid">
|
||||
<el-row :gutter="24">
|
||||
<el-col :span="8"><el-form-item label="名称"><el-input v-model="form.material_name" disabled class="is-text-view" /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="规格"><el-input v-model="form.spec_model" disabled class="is-text-view" /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="类型"><el-input v-model="form.material_type" disabled class="is-text-view" /></el-form-item></el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-card inbound-card">
|
||||
<div class="card-title"><el-icon class="icon"><House /></el-icon><span>2. 入库详情</span></div>
|
||||
<div class="card-content">
|
||||
<el-row :gutter="24">
|
||||
<el-col :span="6"><el-form-item label="SKU" prop="sku"><el-input v-model="form.sku" /></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="条码" prop="barcode"><el-input v-model="form.barcode" /></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="库位" prop="warehouse_location"><el-input v-model="form.warehouse_location" /></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="入库日期"><el-date-picker v-model="form.in_date" type="date" value-format="YYYY-MM-DD" style="width:100%" disabled /></el-form-item></el-col>
|
||||
</el-row>
|
||||
|
||||
<div class="identity-panel">
|
||||
<el-row :gutter="24">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="序列号(SN)" prop="serial_number">
|
||||
<el-input v-model="form.serial_number" placeholder="必填: 唯一序列号" clearable><template #prefix><span class="prefix-tag sn">SN</span></template></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="入库数量" prop="in_quantity">
|
||||
<el-input-number v-model="form.in_quantity" :min="1" style="width:100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<el-row :gutter="24" style="margin-top:15px">
|
||||
<el-col :span="6">
|
||||
<el-form-item label="质量状态">
|
||||
<el-select v-model="form.quality_status" style="width:100%">
|
||||
<el-option label="合格" value="合格" /><el-option label="不合格" value="不合格" /><el-option label="待检" value="待检" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="9"><el-form-item label="质量报告"><el-input v-model="form.quality_report_link" placeholder="链接" /></el-form-item></el-col>
|
||||
<el-col :span="9"><el-form-item label="检测报告"><el-input v-model="form.inspection_report_link" placeholder="产品检测报告链接" /></el-form-item></el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-card production-card">
|
||||
<div class="card-title"><el-icon class="icon"><Setting /></el-icon><span>3. 生产与销售信息</span></div>
|
||||
<div class="card-content">
|
||||
<el-row :gutter="24">
|
||||
<el-col :span="8"><el-form-item label="BOM编号"><el-input v-model="form.bom_code" /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="BOM版本"><el-input v-model="form.bom_version" /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="工单号"><el-input v-model="form.work_order_code" /></el-form-item></el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="24">
|
||||
<el-col :span="8"><el-form-item label="订单号"><el-input v-model="form.order_id" placeholder="关联销售订单" /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="负责人"><el-input v-model="form.production_manager" /></el-form-item></el-col>
|
||||
<el-col :span="8"><el-form-item label="产品定价"><el-input-number v-model="form.sale_price" :precision="2" style="width:100%"><template #prefix>¥</template></el-input-number></el-form-item></el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="24">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="生产时间">
|
||||
<el-date-picker v-model="form.production_time_range" type="datetimerange" value-format="YYYY-MM-DD HH:mm:ss" style="width:100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6"><el-form-item label="原料成本"><el-input-number v-model="form.raw_material_cost" :precision="2" style="width:100%" /></el-form-item></el-col>
|
||||
<el-col :span="6"><el-form-item label="人工成本"><el-input-number v-model="form.manual_cost" :precision="2" style="width:100%" /></el-form-item></el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="24" style="margin-top:10px">
|
||||
<el-col :span="24"><el-form-item label="详情链接"><el-input v-model="form.detail_link" /></el-form-item></el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="visible = false" size="large">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="submitForm" size="large">提交</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, watch } from 'vue'
|
||||
import { Plus, Setting, Refresh, Search, Box, House, Link } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import dayjs from 'dayjs'
|
||||
// 引用更新后的 product.ts
|
||||
import { getProductList, createProductInbound, updateProductInbound, deleteProductInbound, searchMaterialBase } from '@/api/inbound/product'
|
||||
|
||||
const loading = ref(false)
|
||||
const submitting = ref(false)
|
||||
const visible = ref(false)
|
||||
const searchLoading = ref(false)
|
||||
const dialogStatus = ref<'create' | 'update'>('create')
|
||||
const tableData = ref([])
|
||||
const total = ref(0)
|
||||
const formRef = ref()
|
||||
const queryParams = reactive({ page: 1, pageSize: 15, keyword: '' })
|
||||
const materialOptions = ref<any[]>([])
|
||||
|
||||
const allColumns = [
|
||||
{ prop: 'material_name', label: '名称', minWidth: '120' },
|
||||
{ prop: 'spec_model', label: '规格', minWidth: '120' },
|
||||
{ prop: 'sku', label: 'SKU', minWidth: '100' },
|
||||
{ prop: 'serial_number', label: '序列号', minWidth: '140' },
|
||||
{ prop: 'qty_stock', label: '库存', minWidth: '80' },
|
||||
{ prop: 'status', label: '状态', minWidth: '80' },
|
||||
{ prop: 'quality_status', label: '质量', minWidth: '80' },
|
||||
{ prop: 'sale_price', label: '售价', minWidth: '100' },
|
||||
{ prop: 'order_id', label: '订单号', minWidth: '120' },
|
||||
{ prop: 'work_order_code', label: '工单号', minWidth: '120' },
|
||||
{ prop: 'bom_code', label: 'BOM', minWidth: '100' },
|
||||
{ prop: 'inspection_report_link', label: '检测报告', minWidth: '100' },
|
||||
{ prop: 'quality_report_link', label: '质量报告', minWidth: '100' },
|
||||
{ prop: 'production_manager', label: '负责人', minWidth: '100' },
|
||||
{ prop: 'raw_material_cost', label: '原料成本', minWidth: '100' },
|
||||
{ prop: 'manual_cost', label: '人工成本', minWidth: '100' },
|
||||
{ prop: 'inbound_date', label: '生产日期', minWidth: '120' }
|
||||
]
|
||||
|
||||
const visibleColumnProps = ref(allColumns.map(c => c.prop))
|
||||
|
||||
const form = reactive({
|
||||
id: undefined, base_id: undefined, material_name: '', spec_model: '', material_type: '',
|
||||
sku: '', barcode: '', serial_number: '', in_date: '',
|
||||
in_quantity: 1, stock_quantity: 1, available_quantity: 1,
|
||||
warehouse_location: '', status: '在库', quality_status: '合格',
|
||||
bom_code: '', bom_version: '', work_order_code: '', order_id: '',
|
||||
production_manager: '', production_time_range: [] as string[],
|
||||
raw_material_cost: 0, manual_cost: 0, sale_price: 0,
|
||||
quality_report_link: '', inspection_report_link: '', detail_link: ''
|
||||
})
|
||||
|
||||
const rules = {
|
||||
base_id: [{ required: true, message: '必选', trigger: 'change' }],
|
||||
serial_number: [{ required: true, message: '必填', trigger: 'blur' }],
|
||||
in_quantity: [{ required: true, message: '必填', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res: any = await getProductList(queryParams)
|
||||
tableData.value = res.data.items || []
|
||||
total.value = res.data.total || 0
|
||||
} finally { loading.value = false }
|
||||
}
|
||||
|
||||
const handleSearchMaterial = async (query: string) => {
|
||||
if (query) {
|
||||
searchLoading.value = true
|
||||
try {
|
||||
const res: any = await searchMaterialBase(query)
|
||||
materialOptions.value = res.data || []
|
||||
} finally { searchLoading.value = false }
|
||||
}
|
||||
}
|
||||
|
||||
const onMaterialSelected = (val: number) => {
|
||||
const item = materialOptions.value.find(i => i.id === val)
|
||||
if (item) {
|
||||
form.material_name = item.name
|
||||
form.spec_model = item.spec
|
||||
form.material_type = item.type
|
||||
}
|
||||
}
|
||||
|
||||
const handleCreate = () => {
|
||||
dialogStatus.value = 'create'
|
||||
resetForm()
|
||||
form.in_date = dayjs().format('YYYY-MM-DD')
|
||||
visible.value = true
|
||||
}
|
||||
|
||||
const handleUpdate = (row: any) => {
|
||||
dialogStatus.value = 'update'
|
||||
Object.assign(form, row)
|
||||
// 转换时间格式
|
||||
if(row.production_start_time && row.production_end_time) {
|
||||
form.production_time_range = [row.production_start_time, row.production_end_time]
|
||||
} else {
|
||||
form.production_time_range = []
|
||||
}
|
||||
visible.value = true
|
||||
}
|
||||
|
||||
const submitForm = async () => {
|
||||
await formRef.value.validate(async (valid: boolean) => {
|
||||
if(valid) {
|
||||
submitting.value = true
|
||||
try {
|
||||
const payload = { ...form,
|
||||
production_start_time: form.production_time_range?.[0],
|
||||
production_end_time: form.production_time_range?.[1]
|
||||
}
|
||||
if(dialogStatus.value === 'create') await createProductInbound(payload)
|
||||
else await updateProductInbound(form.id!, payload)
|
||||
ElMessage.success('操作成功')
|
||||
visible.value = false
|
||||
fetchData()
|
||||
} catch(e:any) { ElMessage.error(e.msg || '失败') }
|
||||
finally { submitting.value = false }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleDelete = async (row: any) => {
|
||||
try { await deleteProductInbound(row.id); ElMessage.success('删除成功'); fetchData() }
|
||||
catch(e) { ElMessage.error('删除失败') }
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
materialOptions.value = []
|
||||
Object.assign(form, {
|
||||
id: undefined, base_id: undefined, material_name: '', spec_model: '', material_type: '',
|
||||
sku: '', barcode: '', serial_number: '', in_date: '',
|
||||
in_quantity: 1, stock_quantity: 1, available_quantity: 1,
|
||||
warehouse_location: '', status: '在库', quality_status: '合格',
|
||||
bom_code: '', bom_version: '', work_order_code: '', order_id: '',
|
||||
production_manager: '', production_time_range: [],
|
||||
raw_material_cost: 0, manual_cost: 0, sale_price: 0,
|
||||
quality_report_link: '', inspection_report_link: '', detail_link: ''
|
||||
})
|
||||
}
|
||||
|
||||
const getStatusType = (s:string) => ({'在库':'success','出库':'info','损耗':'danger'}[s]||'warning')
|
||||
const getQualityType = (s:string) => ({'合格':'success','不合格':'danger','待检':'info'}[s]||'info')
|
||||
const formatMoney = (val:any) => isNaN(Number(val)) ? '-' : `¥ ${Number(val).toFixed(2)}`
|
||||
onMounted(() => fetchData())
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.product-module { background: #f5f7fa; padding: 20px; min-height: 100vh; }
|
||||
.header-tools { display: flex; justify-content: space-between; margin-bottom: 20px; background: #fff; padding: 15px; border-radius: 8px; }
|
||||
.modern-table { border-radius: 8px; box-shadow: 0 2px 12px 0 rgba(0,0,0,0.05); }
|
||||
.tag-sn { color: #409EFF; font-weight: bold; font-family: monospace; }
|
||||
.stock-num { font-weight: bold; font-size: 15px; }
|
||||
.form-card { background: #fff; border-radius: 8px; margin-bottom: 20px; border: 1px solid #e4e7ed; overflow: hidden; }
|
||||
.card-title { background: #fcfcfc; padding: 10px 20px; border-bottom: 1px solid #ebeef5; font-weight: 600; display: flex; align-items: center; gap: 8px; }
|
||||
.card-content { padding: 20px; }
|
||||
.basic-card { border-left: 4px solid #409EFF; }
|
||||
.inbound-card { border-left: 4px solid #67C23A; }
|
||||
.production-card { border-left: 4px solid #E6A23C; }
|
||||
.identity-panel { background: #fffbf0; border: 1px dashed #e6a23c; padding: 15px; margin: 10px 0; border-radius: 6px; }
|
||||
.prefix-tag.sn { color: #409EFF; background: #ecf5ff; padding: 0 5px; font-weight: bold; }
|
||||
.option-item { display: flex; justify-content: space-between; width: 100%; }
|
||||
.opt-spec { color: #8492a6; font-size: 12px; }
|
||||
.is-text-view :deep(.el-input__wrapper) { box-shadow: none !important; background: #f5f7fa; border-bottom: 1px solid #dcdfe6; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user