material_manage_system/frontend/src/views/Materials.vue
2026-09-15 16:08:18 +08:00

342 lines
11 KiB
Vue

<template>
<div class="materials-page">
<div class="page-header">
<h2>物料管理</h2>
<el-button type="primary" @click="showAddModal">新增物料</el-button>
</div>
<div class="search-bar">
<input
v-model="searchKeyword"
type="text"
class="search-native-input"
placeholder="搜索物料编码或名称"
@keyup.enter="handleSearch"
/>
<el-button type="primary" @click="handleSearch">搜索</el-button>
</div>
<BaseTable
:data="materialsStore.materials"
:loading="materialsStore.loading"
:current-page="materialsStore.pagination.currentPage"
:page-size="materialsStore.pagination.pageSize"
:total="materialsStore.pagination.total"
@page-change="handlePageChange"
>
<el-table-column type="index" label="ID" width="60" />
<el-table-column prop="material_code" label="物料编码" />
<el-table-column prop="name" label="名称" />
<el-table-column prop="material_image" label="物料图片" width="80">
<template #default="{ row }">
<el-image
v-if="row.material_image"
:src="row.material_image"
style="width: 40px; height: 40px; object-fit: contain"
:preview-src-list="[row.material_image]"
fit="contain"
/>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column prop="spec" label="规格" />
<el-table-column prop="category" label="分类">
<template #default="{ row }">
{{ materialsStore.getCategoryDisplay(row.category) }}
</template>
</el-table-column>
<el-table-column prop="unit" label="单位" width="60" />
<el-table-column prop="warning_stock" label="预警库存" width="100" />
<el-table-column prop="created_at" label="创建时间" width="160">
<template #default="{ row }">
{{ formatDate(row.created_at) }}
</template>
</el-table-column>
<el-table-column label="操作" width="240" fixed="right">
<template #default="{ row }">
<el-button size="small" @click="showDetail(row)">详情</el-button>
<el-button v-if="authStore.isAdmin" size="small" type="primary" @click="showEditModal(row)">编辑</el-button>
<el-button v-if="authStore.isAdmin" size="small" type="danger" @click="handleDelete(row.id)">删除</el-button>
</template>
</el-table-column>
</BaseTable>
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="600px">
<el-form :model="form" label-width="100px">
<el-form-item label="物料编码">
<el-input v-model="form.material_code" :readonly="dialogTitle !== '新增物料'" />
</el-form-item>
<el-form-item label="名称">
<el-input v-model="form.name" />
</el-form-item>
<el-form-item label="规格">
<el-input v-model="form.spec" />
</el-form-item>
<el-form-item label="分类">
<el-select v-model="form.category" style="width: 100%">
<el-option label="标准件" value="A01" />
<el-option label="成品件" value="A02" />
<el-option label="简易加工件" value="A03" />
<el-option label="精密加工件" value="A04" />
<el-option label="钣金件" value="A05" />
<el-option label="塑胶件" value="A06" />
<el-option label="其他" value="A07" />
<el-option label="成品件" value="B01" />
<el-option label="简易加工件" value="B02" />
<el-option label="PCB硬板" value="B03" />
<el-option label="FPC软板" value="B04" />
<el-option label="线束" value="B05" />
<el-option label="组装组件" value="B06" />
<el-option label="其他" value="B07" />
<el-option label="连接耗材" value="C01" />
<el-option label="密封耗材" value="C02" />
<el-option label="其他" value="C03" />
<el-option label="自研程序固件" value="D01" />
<el-option label="外购软件授权" value="D02" />
<el-option label="授权介质" value="D03" />
<el-option label="其他" value="D04" />
<el-option label="成品" value="E01" />
<el-option label="半成品" value="E02" />
<el-option label="资料文档" value="F01" />
<el-option label="标识标牌" value="F02" />
<el-option label="包装物料" value="F03" />
<el-option label="随机工具配件" value="F04" />
<el-option label="其他" value="F05" />
</el-select>
</el-form-item>
<el-form-item label="单位">
<el-input v-model="form.unit" />
</el-form-item>
<el-form-item label="预警库存">
<el-input-number v-model="form.warning_stock" :min="0" />
</el-form-item>
<el-form-item label="图片URL">
<el-input v-model="form.material_image" placeholder="图片URL" />
</el-form-item>
<el-form-item label="图片上传">
<input type="file" accept="image/*" @change="handleImageUpload" ref="imageInput" />
<img v-if="imagePreview" :src="imagePreview" style="max-width:200px;margin-top:10px;" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSave" :loading="saveLoading">保存</el-button>
</template>
</el-dialog>
<el-dialog v-model="detailVisible" title="物料详情" width="600px">
<el-descriptions :column="2" border v-if="detailRow">
<el-descriptions-item label="物料编码">{{ detailRow.material_code }}</el-descriptions-item>
<el-descriptions-item label="名称">{{ detailRow.name }}</el-descriptions-item>
<el-descriptions-item label="规格">{{ detailRow.spec || '-' }}</el-descriptions-item>
<el-descriptions-item label="分类">{{ materialsStore.getCategoryDisplay(detailRow.category) }}</el-descriptions-item>
<el-descriptions-item label="单位">{{ detailRow.unit }}</el-descriptions-item>
<el-descriptions-item label="预警库存">{{ detailRow.warning_stock }}</el-descriptions-item>
<el-descriptions-item label="当前库存">{{ detailRow.current_stock }}</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ formatDate(detailRow.created_at) }}</el-descriptions-item>
<el-descriptions-item label="更新时间" :span="2">{{ formatDate(detailRow.updated_at) }}</el-descriptions-item>
<el-descriptions-item label="物料图片" :span="2">
<el-image
v-if="detailRow.material_image"
:src="detailRow.material_image"
style="width: 100px; height: 100px; object-fit: contain"
:preview-src-list="[detailRow.material_image]"
fit="contain"
/>
<span v-else>-</span>
</el-descriptions-item>
</el-descriptions>
</el-dialog>
<DeleteConfirm
v-model="deleteDialogVisible"
@confirm="confirmDelete"
/>
<AlertTip :message="alertMessage" :type="alertType" />
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { useMaterialsStore } from '../stores/materials'
import { useAuthStore } from '../stores/auth'
import BaseTable from '../components/business/BaseTable.vue'
import DeleteConfirm from '../components/business/DeleteConfirm.vue'
import AlertTip from '../components/business/AlertTip.vue'
import { ElMessage } from 'element-plus'
const materialsStore = useMaterialsStore()
const authStore = useAuthStore()
const searchKeyword = ref('')
const dialogVisible = ref(false)
const detailVisible = ref(false)
const deleteDialogVisible = ref(false)
const dialogTitle = ref('新增物料')
const saveLoading = ref(false)
const editingId = ref(null)
const deleteId = ref(null)
const detailRow = ref(null)
const alertMessage = ref('')
const alertType = ref('success')
const imagePreview = ref('')
const imageInput = ref(null)
function handleImageUpload(e) {
const file = e.target.files[0]
if (file) {
imagePreview.value = URL.createObjectURL(file)
}
}
const form = reactive({
material_code: '',
name: '',
spec: '',
category: '',
unit: 'pcs',
warning_stock: 0,
material_image: ''
})
onMounted(() => {
materialsStore.fetchMaterials()
})
function handleSearch() {
materialsStore.fetchMaterials(1, searchKeyword.value)
}
function handlePageChange(page) {
materialsStore.fetchMaterials(page, searchKeyword.value)
}
function showAddModal() {
editingId.value = null
dialogTitle.value = '新增物料'
Object.assign(form, {
material_code: '',
name: '',
spec: '',
category: '',
unit: 'pcs',
warning_stock: 0,
material_image: ''
})
imagePreview.value = ''
if (imageInput.value) imageInput.value.value = ''
dialogVisible.value = true
}
function showDetail(row) {
detailRow.value = row
detailVisible.value = true
}
function showEditModal(row) {
editingId.value = row.id
dialogTitle.value = '编辑物料'
Object.assign(form, {
material_code: row.material_code,
name: row.name,
spec: row.spec || '',
category: row.category || '',
unit: row.unit,
warning_stock: row.warning_stock,
material_image: row.material_image || ''
})
imagePreview.value = ''
if (imageInput.value) imageInput.value.value = ''
dialogVisible.value = true
}
async function handleSave() {
saveLoading.value = true
try {
const formData = { ...form }
const file = imageInput.value?.files[0]
if (file) {
const uploadForm = new FormData()
uploadForm.append('image', file)
const res = await fetch('/api/upload', {
method: 'POST',
headers: { 'Authorization': `Bearer ${authStore.token}` },
body: uploadForm
})
if (!res.ok) throw new Error('图片上传失败')
const data = await res.json()
formData.material_image = (data.data && data.data.url) ? data.data.url : ''
}
if (editingId.value) {
await materialsStore.updateMaterial(editingId.value, formData)
ElMessage.success('物料更新成功')
} else {
await materialsStore.createMaterial(formData)
ElMessage.success('物料创建成功')
}
dialogVisible.value = false
} catch (e) {
ElMessage.error(e.message || '保存失败')
} finally {
saveLoading.value = false
}
}
function handleDelete(id) {
deleteId.value = id
deleteDialogVisible.value = true
}
async function confirmDelete() {
try {
await materialsStore.deleteMaterial(deleteId.value)
ElMessage.success('物料删除成功')
} catch (e) {
// 错误已由 API 层处理
}
}
function formatDate(dateStr) {
if (!dateStr) return '-'
return new Date(dateStr).toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
})
}
</script>
<style scoped>
.materials-page {
padding: 20px;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.search-bar {
display: flex;
width: 100%;
gap: 10px;
margin-bottom: 15px;
}
.search-native-input {
width: 250px;
padding: 8px 12px;
border: 1px solid #dcdfe6;
border-radius: 4px;
font-size: 14px;
outline: none;
background: #fff;
box-sizing: border-box;
}
.search-native-input:focus {
border-color: #409eff;
}
</style>