|
@@ -0,0 +1,381 @@
|
|
|
|
+<template>
|
|
|
|
+ <div class="p-6">
|
|
|
|
+ <!-- 头部操作区 -->
|
|
|
|
+ <div class="bg-white rounded-lg shadow p-6 mb-6">
|
|
|
|
+ <div class="flex justify-between items-center">
|
|
|
|
+ <h2 class="text-2xl font-bold text-gray-800">原料品类管理</h2>
|
|
|
|
+ <div class="flex space-x-4">
|
|
|
|
+ <button
|
|
|
|
+ @click="showAddDialog = true"
|
|
|
|
+ class="flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
|
|
|
+ >
|
|
|
|
+ <PlusIcon class="w-5 h-5 mr-2" />
|
|
|
|
+ 新增原料品类
|
|
|
|
+ </button>
|
|
|
|
+ <button
|
|
|
|
+ @click="handleExport"
|
|
|
|
+ class="flex items-center px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors"
|
|
|
|
+ >
|
|
|
|
+ <DownloadIcon class="w-5 h-5 mr-2" />
|
|
|
|
+ 导出数据
|
|
|
|
+ </button>
|
|
|
|
+ <div class="relative">
|
|
|
|
+ <input
|
|
|
|
+ type="file"
|
|
|
|
+ class="hidden"
|
|
|
|
+ ref="fileInput"
|
|
|
|
+ @change="handleFileChange"
|
|
|
|
+ />
|
|
|
|
+ <button
|
|
|
|
+ @click="$refs.fileInput.click()"
|
|
|
|
+ class="flex items-center px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"
|
|
|
|
+ >
|
|
|
|
+ <UploadIcon class="w-5 h-5 mr-2" />
|
|
|
|
+ 导入数据
|
|
|
|
+ </button>
|
|
|
|
+ </div>
|
|
|
|
+ </div>
|
|
|
|
+ </div>
|
|
|
|
+ </div>
|
|
|
|
+
|
|
|
|
+ <!-- 筛选区 -->
|
|
|
|
+ <div class="bg-white rounded-lg shadow p-6 mb-6">
|
|
|
|
+ <div class="flex flex-wrap gap-4">
|
|
|
|
+ <div class="relative flex-1 min-w-[200px]">
|
|
|
|
+ <SearchIcon class="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400" />
|
|
|
|
+ <input
|
|
|
|
+ v-model="searchQuery"
|
|
|
|
+ type="text"
|
|
|
|
+ placeholder="根据物料编码/物料组/类别一/类别二/原料大类进行模糊查询"
|
|
|
|
+ class="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
|
|
+ @input="handleSearch"
|
|
|
|
+ />
|
|
|
|
+ </div>
|
|
|
|
+ </div>
|
|
|
|
+ </div>
|
|
|
|
+
|
|
|
|
+ <!-- 原料品类列表 -->
|
|
|
|
+ <div class="bg-white rounded-lg shadow overflow-hidden">
|
|
|
|
+ <div class="overflow-x-auto">
|
|
|
|
+ <table class="min-w-full divide-y divide-gray-200">
|
|
|
|
+ <thead class="bg-gray-50">
|
|
|
|
+ <tr>
|
|
|
|
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
|
|
+ <input
|
|
|
|
+ type="checkbox"
|
|
|
|
+ v-model="selectAll"
|
|
|
|
+ class="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
|
|
|
+ />
|
|
|
|
+ </th>
|
|
|
|
+ <th
|
|
|
|
+ v-for="column in columns"
|
|
|
|
+ :key="column.prop"
|
|
|
|
+ class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider cursor-pointer"
|
|
|
|
+ >
|
|
|
|
+ <div class="flex items-center">
|
|
|
|
+ {{ column.label }}
|
|
|
|
+ </div>
|
|
|
|
+ </th>
|
|
|
|
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
|
|
+ 操作
|
|
|
|
+ </th>
|
|
|
|
+ </tr>
|
|
|
|
+ </thead>
|
|
|
|
+ <tbody class="bg-white divide-y divide-gray-200">
|
|
|
|
+ <tr v-for="material in filteredMaterials" :key="material.id" class="hover:bg-gray-50">
|
|
|
|
+ <td class="px-6 py-4 whitespace-nowrap">
|
|
|
|
+ <input
|
|
|
|
+ type="checkbox"
|
|
|
|
+ v-model="selectedItems"
|
|
|
|
+ :value="material.id"
|
|
|
|
+ class="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
|
|
|
+ />
|
|
|
|
+ </td>
|
|
|
|
+ <td class="px-6 py-4 whitespace-nowrap">{{ material.materialCode }}</td>
|
|
|
|
+ <td class="px-6 py-4 whitespace-nowrap">{{ material.materialGroup }}</td>
|
|
|
|
+ <td class="px-6 py-4 whitespace-nowrap">{{ material.categoryOne }}</td>
|
|
|
|
+ <td class="px-6 py-4 whitespace-nowrap">{{ material.categoryTwo }}</td>
|
|
|
|
+ <td class="px-6 py-4 whitespace-nowrap">{{ material.rawMaterialCategory }}</td>
|
|
|
|
+ <td class="px-6 py-4 whitespace-nowrap">
|
|
|
|
+ <div class="flex space-x-2">
|
|
|
|
+ <button
|
|
|
|
+ @click="handleEdit(material)"
|
|
|
|
+ class="px-3 py-1 text-sm text-green-600 hover:text-green-800"
|
|
|
|
+ >
|
|
|
|
+ 修改
|
|
|
|
+ </button>
|
|
|
|
+ <button
|
|
|
|
+ @click="handleDelete(material)"
|
|
|
|
+ class="px-3 py-1 text-sm text-red-600 hover:text-red-800"
|
|
|
|
+ >
|
|
|
|
+ 删除
|
|
|
|
+ </button>
|
|
|
|
+ </div>
|
|
|
|
+ </td>
|
|
|
|
+ </tr>
|
|
|
|
+ </tbody>
|
|
|
|
+ </table>
|
|
|
|
+ </div>
|
|
|
|
+
|
|
|
|
+ <!-- 批量操作 -->
|
|
|
|
+ <div v-if="selectedItems.length > 0" class="px-6 py-4 bg-gray-50 border-t border-gray-200">
|
|
|
|
+ <div class="flex items-center justify-between">
|
|
|
|
+ <span class="text-sm text-gray-600">已选择 {{ selectedItems.length }} 项</span>
|
|
|
|
+ <div class="flex space-x-4">
|
|
|
|
+ <button
|
|
|
|
+ @click="handleBatchEdit"
|
|
|
|
+ class="flex items-center px-4 py-2 text-sm text-blue-600 hover:text-blue-800"
|
|
|
|
+ >
|
|
|
|
+ <EditIcon class="w-4 h-4 mr-1" />
|
|
|
|
+ 批量编辑
|
|
|
|
+ </button>
|
|
|
|
+ <button
|
|
|
|
+ @click="handleBatchDelete"
|
|
|
|
+ class="flex items-center px-4 py-2 text-sm text-red-600 hover:text-red-800"
|
|
|
|
+ >
|
|
|
|
+ <TrashIcon class="w-4 h-4 mr-1" />
|
|
|
|
+ 批量删除
|
|
|
|
+ </button>
|
|
|
|
+ </div>
|
|
|
|
+ </div>
|
|
|
|
+ </div>
|
|
|
|
+
|
|
|
|
+ <!-- 分页 -->
|
|
|
|
+ <div class="px-6 py-4 bg-gray-50 border-t border-gray-200">
|
|
|
|
+ <div class="flex items-center justify-between">
|
|
|
|
+ <div class="flex items-center">
|
|
|
|
+ <span class="text-sm text-gray-700 mr-4">每页显示</span>
|
|
|
|
+ <select
|
|
|
|
+ v-model="pageSize"
|
|
|
|
+ class="px-2 py-1 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
|
|
+ @change="handleSizeChange"
|
|
|
|
+ >
|
|
|
|
+ <option v-for="size in [10, 20, 50, 100]" :key="size" :value="size">
|
|
|
|
+ {{ size }}
|
|
|
|
+ </option>
|
|
|
|
+ </select>
|
|
|
|
+ </div>
|
|
|
|
+ <div class="flex items-center space-x-2">
|
|
|
|
+ <button
|
|
|
|
+ @click="currentPage--"
|
|
|
|
+ :disabled="currentPage === 1"
|
|
|
|
+ class="px-3 py-1 border border-gray-300 rounded-md disabled:opacity-50"
|
|
|
|
+ >
|
|
|
|
+ <ChevronLeftIcon class="w-4 h-4" />
|
|
|
|
+ </button>
|
|
|
|
+ <span class="text-sm text-gray-700">
|
|
|
|
+ 第 {{ currentPage }} 页 / 共 {{ totalPages }} 页
|
|
|
|
+ </span>
|
|
|
|
+ <button
|
|
|
|
+ @click="currentPage++"
|
|
|
|
+ :disabled="currentPage === totalPages"
|
|
|
|
+ class="px-3 py-1 border border-gray-300 rounded-md disabled:opacity-50"
|
|
|
|
+ >
|
|
|
|
+ <ChevronRightIcon class="w-4 h-4" />
|
|
|
|
+ </button>
|
|
|
|
+ </div>
|
|
|
|
+ </div>
|
|
|
|
+ </div>
|
|
|
|
+ </div>
|
|
|
|
+ </div>
|
|
|
|
+</template>
|
|
|
|
+
|
|
|
|
+<script setup>
|
|
|
|
+import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue'
|
|
|
|
+import {
|
|
|
|
+ PlusIcon,
|
|
|
|
+ DownloadIcon,
|
|
|
|
+ UploadIcon,
|
|
|
|
+ SearchIcon,
|
|
|
|
+ EditIcon,
|
|
|
|
+ TrashIcon,
|
|
|
|
+ ChevronLeftIcon,
|
|
|
|
+ ChevronRightIcon,
|
|
|
|
+ XIcon
|
|
|
|
+} from 'lucide-vue-next'
|
|
|
|
+import { useRouter } from 'vue-router'
|
|
|
|
+
|
|
|
|
+const router = useRouter()
|
|
|
|
+
|
|
|
|
+// Mock 数据
|
|
|
|
+const mockData = {
|
|
|
|
+ materials: {
|
|
|
|
+ list: [
|
|
|
|
+ {
|
|
|
|
+ id: 1,
|
|
|
|
+ materialCode: 'M001',
|
|
|
|
+ materialGroup: 'MG01',
|
|
|
|
+ categoryOne: 'C1',
|
|
|
|
+ categoryTwo: 'C2',
|
|
|
|
+ rawMaterialCategory: 'RMC01'
|
|
|
|
+ },
|
|
|
|
+ {
|
|
|
|
+ id: 2,
|
|
|
|
+ materialCode: 'M002',
|
|
|
|
+ materialGroup: 'MG02',
|
|
|
|
+ categoryOne: 'C3',
|
|
|
|
+ categoryTwo: 'C4',
|
|
|
|
+ rawMaterialCategory: 'RMC02'
|
|
|
|
+ }
|
|
|
|
+ ],
|
|
|
|
+ total: 2
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// 状态管理
|
|
|
|
+const loading = ref(false)
|
|
|
|
+const searchQuery = ref('')
|
|
|
|
+const currentPage = ref(1)
|
|
|
|
+const pageSize = ref(10)
|
|
|
|
+const total = ref(0)
|
|
|
|
+const selectedItems = ref([])
|
|
|
|
+const showAddDialog = ref(false)
|
|
|
|
+const isEdit = ref(false)
|
|
|
|
+const sortKey = ref('')
|
|
|
|
+const sortOrder = ref('asc')
|
|
|
|
+
|
|
|
|
+// 常量定义
|
|
|
|
+const columns = [
|
|
|
|
+ { prop: 'materialCode', label: '物料编码' },
|
|
|
|
+ { prop: 'materialGroup', label: '物料组' },
|
|
|
|
+ { prop: 'categoryOne', label: '类别一' },
|
|
|
|
+ { prop: 'categoryTwo', label: '类别二' },
|
|
|
|
+ { prop: 'rawMaterialCategory', label: '原料大类' }
|
|
|
|
+]
|
|
|
|
+
|
|
|
|
+const addForm = ref({
|
|
|
|
+ materialCode: '',
|
|
|
|
+ materialGroup: '',
|
|
|
|
+ categoryOne: '',
|
|
|
|
+ categoryTwo: '',
|
|
|
|
+ rawMaterialCategory: ''
|
|
|
|
+})
|
|
|
|
+
|
|
|
|
+// 计算属性
|
|
|
|
+const filteredMaterials = computed(() => {
|
|
|
|
+ return mockData.materials.list.filter(material =>
|
|
|
|
+ material.materialCode.includes(searchQuery.value) ||
|
|
|
|
+ material.materialGroup.includes(searchQuery.value) ||
|
|
|
|
+ material.categoryOne.includes(searchQuery.value) ||
|
|
|
|
+ material.categoryTwo.includes(searchQuery.value) ||
|
|
|
|
+ material.rawMaterialCategory.includes(searchQuery.value)
|
|
|
|
+ )
|
|
|
|
+})
|
|
|
|
+
|
|
|
|
+const totalPages = computed(() => {
|
|
|
|
+ return Math.ceil(total.value / pageSize.value)
|
|
|
|
+})
|
|
|
|
+
|
|
|
|
+const selectAll = computed({
|
|
|
|
+ get: () => selectedItems.value.length === filteredMaterials.value.length,
|
|
|
|
+ set: (value) => {
|
|
|
|
+ selectedItems.value = value ? filteredMaterials.value.map(m => m.id) : []
|
|
|
|
+ }
|
|
|
|
+})
|
|
|
|
+
|
|
|
|
+// 方法
|
|
|
|
+const handleSearch = () => {
|
|
|
|
+ currentPage.value = 1
|
|
|
|
+ fetchMaterials()
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+const handleSort = (prop) => {
|
|
|
|
+ if (sortKey.value === prop) {
|
|
|
|
+ sortOrder.value = sortOrder.value === 'asc' ? 'desc' : 'asc'
|
|
|
|
+ } else {
|
|
|
|
+ sortKey.value = prop
|
|
|
|
+ sortOrder.value = 'asc'
|
|
|
|
+ }
|
|
|
|
+ const sortedMaterials = [...filteredMaterials.value]
|
|
|
|
+ sortedMaterials.sort((a, b) => {
|
|
|
|
+ const aValue = a[prop]
|
|
|
|
+ const bValue = b[prop]
|
|
|
|
+ if (sortOrder.value === 'asc') {
|
|
|
|
+ return aValue > bValue ? 1 : -1
|
|
|
|
+ } else {
|
|
|
|
+ return aValue < bValue ? 1 : -1
|
|
|
|
+ }
|
|
|
|
+ })
|
|
|
|
+ filteredMaterials.value = sortedMaterials
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+const handleSizeChange = () => {
|
|
|
|
+ currentPage.value = 1
|
|
|
|
+ fetchMaterials()
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+const handleCurrentChange = (page) => {
|
|
|
|
+ currentPage.value = page
|
|
|
|
+ fetchMaterials()
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+const handleFileChange = (event) => {
|
|
|
|
+ const file = event.target.files[0]
|
|
|
|
+ if (file) {
|
|
|
|
+ // 处理文件上传
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+const handleExport = () => {
|
|
|
|
+ // 实现导出逻辑
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+const handleBatchEdit = () => {
|
|
|
|
+ // 实现批量编辑逻辑
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+const handleBatchDelete = () => {
|
|
|
|
+ // 实现批量删除逻辑
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+const handleEdit = (material) => {
|
|
|
|
+ isEdit.value = true
|
|
|
|
+ addForm.value = { ...material }
|
|
|
|
+ showAddDialog.value = true
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+const handleDelete = (material) => {
|
|
|
|
+ // 实现删除逻辑
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+const handleSubmit = () => {
|
|
|
|
+ // 实现表单提交逻辑
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// 数据获取
|
|
|
|
+const fetchMaterials = async () => {
|
|
|
|
+ try {
|
|
|
|
+ loading.value = true
|
|
|
|
+ // 模拟网络延迟
|
|
|
|
+ await new Promise(resolve => setTimeout(resolve, 500))
|
|
|
|
+ total.value = mockData.materials.total
|
|
|
|
+ } catch (error) {
|
|
|
|
+ console.error('获取数据失败:', error)
|
|
|
|
+ } finally {
|
|
|
|
+ loading.value = false
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// 添加 watch 监听对话框显示状态
|
|
|
|
+watch(showAddDialog, (newVal) => {
|
|
|
|
+ if (!newVal) {
|
|
|
|
+ isEdit.value = false
|
|
|
|
+ addForm.value = {
|
|
|
|
+ materialCode: '',
|
|
|
|
+ materialGroup: '',
|
|
|
|
+ categoryOne: '',
|
|
|
|
+ categoryTwo: '',
|
|
|
|
+ rawMaterialCategory: ''
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+})
|
|
|
|
+
|
|
|
|
+// 生命周期钩子
|
|
|
|
+onMounted(() => {
|
|
|
|
+ fetchMaterials()
|
|
|
|
+})
|
|
|
|
+
|
|
|
|
+onUnmounted(() => {
|
|
|
|
+ // 清理操作(如果有需要)
|
|
|
|
+})
|
|
|
|
+</script>
|