Files
tiantian-system/app/pages/admin/supply/warehouse.vue
赵忠林 a9da04fbb8 fix(oa): 修复多处 Duplicate attribute 错误问题
- 修改 app/components/oa/TaskForm.vue 中 a-input 类型冲突为 a-input-number
- 合并 admin/supply/warehouse.vue 和 production/equipment.vue 中多个 :class 绑定,避免重复属性
- 统一改为数组方式绑定静态和动态 class,防止 Vue 编译器 Duplicate attribute 警告
- 清理缓存并验证构建通过,确保无重复属性错误
- 通过扫描确认 app/ 目录下 Vue 文件不再存在重复属性问题
- 添加 OaTaskForm 组件类型声明及懒加载声明
- 将 ERP 演示独立 HTML 页面整合至 /app/pages,统一布局与导航
- 升级制造业管理后台页面风格,采用玻璃态和渐变设计
- 修订规划文档相关内容,更新 DEMO 系统名称及功能模块描述
- 修改 ecosystem.config.cjs 中运行端口为 10591
2026-04-09 12:08:55 +08:00

461 lines
16 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup lang="ts">
definePageMeta({ layout: 'admin' })
// 仓储统计
const warehouseStats = ref([
{ label: '物料种类', value: 5230, icon: 'fa-boxes', gradient: 'from-blue-500 to-cyan-500', change: '+126', up: true },
{ label: '库存总量', value: '8.5', unit: '万', icon: 'fa-warehouse', gradient: 'from-green-500 to-teal-500', change: '+1.2万', up: true },
{ label: '待入库', value: 45, icon: 'fa-arrow-down', gradient: 'from-orange-500 to-yellow-500', change: '+12', up: false },
{ label: '待出库', value: 28, icon: 'fa-truck', gradient: 'from-purple-500 to-pink-500', change: '-8', up: true },
])
// 库存列表
const inventoryList = ref([
{ code: 'MAT-001', name: '轴承组件 A型', category: '标准件', warehouse: 'A区', location: 'A-01-03', stock: 1500, safeStock: 500, unit: '套', status: 'normal' },
{ code: 'MAT-002', name: '铝合金板材', category: '原材料', warehouse: 'B区', location: 'B-02-05', stock: 320, safeStock: 200, unit: '张', status: 'normal' },
{ code: 'MAT-003', name: '液压缸体 B型', category: '半成品', warehouse: 'C区', location: 'C-01-02', stock: 80, safeStock: 100, unit: '件', status: 'low' },
{ code: 'MAT-004', name: '数控刀具套装', category: '工装', warehouse: 'D区', location: 'D-03-01', stock: 45, safeStock: 20, unit: '套', status: 'normal' },
{ code: 'MAT-005', name: '密封圈组件', category: '标准件', warehouse: 'A区', location: 'A-02-01', stock: 2800, safeStock: 1000, unit: '个', status: 'normal' },
{ code: 'MAT-006', name: '传动齿轮组 C型', category: '零部件', warehouse: 'C区', location: 'C-02-03', stock: 150, safeStock: 200, unit: '组', status: 'low' },
])
const statusMap: Record<string, { label: string; color: string; bg: string }> = {
normal: { label: '正常', color: 'text-green-600', bg: 'bg-green-100' },
low: { label: '偏低', color: 'text-orange-600', bg: 'bg-orange-100' },
out: { label: '缺货', color: 'text-red-600', bg: 'bg-red-100' },
over: { label: '超储', color: 'text-blue-600', bg: 'bg-blue-100' },
}
// 入库记录
const inboundRecords = ref([
{ id: 'IN-2026040901', material: '轴承组件 A型', quantity: 500, unit: '套', type: '采购入库', operator: '仓管员-张三', date: '2026-04-09 09:30', status: 'completed' },
{ id: 'IN-2026040802', material: '铝合金板材', quantity: 200, unit: '张', type: '采购入库', operator: '仓管员-李四', date: '2026-04-08 14:20', status: 'completed' },
{ id: 'IN-2026040801', material: '数控刀具套装', quantity: 20, unit: '套', type: '采购入库', operator: '仓管员-张三', date: '2026-04-08 10:15', status: 'completed' },
{ id: 'IN-2026040703', material: '工业润滑油', quantity: 50, unit: '桶', type: '采购入库', operator: '仓管员-王五', date: '2026-04-07 16:45', status: 'completed' },
])
// 出库记录
const outboundRecords = ref([
{ id: 'OUT-2026040901', material: '轴承组件 A型', quantity: 100, unit: '套', type: '生产领料', recipient: '1号车间', operator: '仓管员-张三', date: '2026-04-09 08:30', status: 'completed' },
{ id: 'OUT-2026040902', material: '密封圈组件', quantity: 200, unit: '个', type: '生产领料', recipient: '2号车间', operator: '仓管员-李四', date: '2026-04-09 10:20', status: 'completed' },
{ id: 'OUT-2026040801', material: '铝合金板材', quantity: 50, unit: '张', type: '生产领料', recipient: '3号车间', operator: '仓管员-王五', date: '2026-04-08 15:30', status: 'completed' },
])
const activeTab = ref('inventory')
const searchKeyword = ref('')
const filteredInventory = computed(() => {
return inventoryList.value.filter((item) => {
return !searchKeyword.value ||
item.name.toLowerCase().includes(searchKeyword.value.toLowerCase()) ||
item.code.toLowerCase().includes(searchKeyword.value.toLowerCase()) ||
item.category.toLowerCase().includes(searchKeyword.value.toLowerCase())
})
})
// 入库表单
const inboundVisible = ref(false)
const inboundForm = reactive({
material: '',
quantity: '',
unit: '',
type: 'purchase',
supplier: '',
remark: '',
})
function handleInbound() {
inboundVisible.value = false
message.success('入库登记成功')
}
</script>
<template>
<div class="warehouse-page">
<!-- 页面标题 -->
<div class="page-header mb-6">
<div>
<h2 class="text-2xl font-bold text-gray-800">仓储物流</h2>
<p class="text-gray-500 mt-1">管理库存物料跟踪出入库记录</p>
</div>
<div class="flex gap-3">
<a-button @click="inboundVisible = true">
<template #icon><i class="fas fa-arrow-down mr-1"></i></template>
入库登记
</a-button>
<a-button type="primary">
<template #icon><i class="fas fa-arrow-up mr-1"></i></template>
出库登记
</a-button>
</div>
</div>
<!-- 统计卡片 -->
<div class="grid grid-cols-4 gap-6 mb-6">
<div
v-for="stat in warehouseStats"
:key="stat.label"
class="glass rounded-2xl p-6 card-hover cursor-pointer"
>
<div class="flex items-center justify-between mb-4">
<div
class="w-12 h-12 rounded-xl flex items-center justify-center text-white"
:class="`bg-gradient-to-br ${stat.gradient}`"
>
<i :class="`fas ${stat.icon}`"></i>
</div>
<span
class="text-sm font-medium"
:class="stat.up ? 'text-green-500' : 'text-red-500'"
>
<i :class="stat.up ? 'fas fa-arrow-up' : 'fas fa-arrow-down'"></i>
{{ stat.change }}
</span>
</div>
<h3 class="text-3xl font-bold text-gray-800 mb-1">
{{ stat.value }}<span v-if="stat.unit" class="text-base text-gray-500 ml-1">{{ stat.unit }}</span>
</h3>
<p class="text-gray-500 text-sm">{{ stat.label }}</p>
</div>
</div>
<!-- Tab切换 -->
<div class="glass rounded-2xl p-4 mb-6">
<a-radio-group v-model:value="activeTab" button-style="solid">
<a-radio-button value="inventory">
<i class="fas fa-boxes mr-1"></i>库存查询
</a-radio-button>
<a-radio-button value="inbound">
<i class="fas fa-arrow-down mr-1"></i>入库记录
</a-radio-button>
<a-radio-button value="outbound">
<i class="fas fa-arrow-up mr-1"></i>出库记录
</a-radio-button>
</a-radio-group>
<a-input-search
v-model:value="searchKeyword"
placeholder="搜索物料名称、编号..."
style="width: 280px; float: right"
allow-clear
/>
</div>
<!-- 库存列表 -->
<div v-show="activeTab === 'inventory'" class="glass rounded-2xl p-6">
<div class="flex items-center justify-between mb-4">
<h3 class="font-bold text-lg text-gray-800">
<i class="fas fa-boxes text-blue-500 mr-2"></i>
库存列表
</h3>
<span class="text-sm text-gray-500"> {{ filteredInventory.length }} 种物料</span>
</div>
<a-table
:dataSource="filteredInventory"
:pagination="{ pageSize: 10 }"
rowKey="code"
:scroll="{ x: 1100 }"
>
<a-table-column title="物料编码" dataIndex="code" width="110" />
<a-table-column title="物料名称" dataIndex="name" width="160">
<template #default="{ text }">
<span class="font-medium">{{ text }}</span>
</template>
</a-table-column>
<a-table-column title="分类" dataIndex="category" width="100" />
<a-table-column title="仓库" dataIndex="warehouse" width="80" align="center" />
<a-table-column title="库位" dataIndex="location" width="100" align="center" />
<a-table-column title="库存量" dataIndex="stock" width="120" align="right">
<template #default="{ record }">
<span class="font-medium">{{ record.stock.toLocaleString() }} {{ record.unit }}</span>
</template>
</a-table-column>
<a-table-column title="安全库存" dataIndex="safeStock" width="100" align="right">
<template #default="{ record }">
<span class="text-gray-500">{{ record.safeStock }} {{ record.unit }}</span>
</template>
</a-table-column>
<a-table-column title="状态" dataIndex="status" width="100" align="center">
<template #default="{ text }">
<span
:class="['px-2 py-1 rounded-lg text-sm font-medium', statusMap[text]?.color, statusMap[text]?.bg]"
>
{{ statusMap[text]?.label }}
</span>
</template>
</a-table-column>
<a-table-column title="操作" width="120" align="center" fixed="right">
<template #default>
<div class="flex items-center gap-2 justify-center">
<a-button type="link" size="small" title="详情">
<i class="fas fa-eye"></i>
</a-button>
<a-button type="link" size="small" title="调整">
<i class="fas fa-edit"></i>
</a-button>
</div>
</template>
</a-table-column>
</a-table>
</div>
<!-- 入库记录 -->
<div v-show="activeTab === 'inbound'" class="glass rounded-2xl p-6">
<div class="flex items-center justify-between mb-4">
<h3 class="font-bold text-lg text-gray-800">
<i class="fas fa-arrow-down text-green-500 mr-2"></i>
入库记录
</h3>
</div>
<a-table
:dataSource="inboundRecords"
:pagination="{ pageSize: 10 }"
rowKey="id"
>
<a-table-column title="入库单号" dataIndex="id" width="140" />
<a-table-column title="物料名称" dataIndex="material" width="160">
<template #default="{ text }">
<span class="font-medium">{{ text }}</span>
</template>
</a-table-column>
<a-table-column title="数量" width="120" align="center">
<template #default="{ record }">
<span class="text-green-600 font-medium">+{{ record.quantity }} {{ record.unit }}</span>
</template>
</a-table-column>
<a-table-column title="入库类型" dataIndex="type" width="120">
<template #default="{ text }">
<a-tag :color="text === '采购入库' ? 'blue' : 'purple'">{{ text }}</a-tag>
</template>
</a-table-column>
<a-table-column title="操作员" dataIndex="operator" width="120" />
<a-table-column title="入库时间" dataIndex="date" width="160" />
<a-table-column title="状态" width="100" align="center">
<template #default>
<a-tag color="success">已完成</a-tag>
</template>
</a-table-column>
</a-table>
</div>
<!-- 出库记录 -->
<div v-show="activeTab === 'outbound'" class="glass rounded-2xl p-6">
<div class="flex items-center justify-between mb-4">
<h3 class="font-bold text-lg text-gray-800">
<i class="fas fa-arrow-up text-orange-500 mr-2"></i>
出库记录
</h3>
</div>
<a-table
:dataSource="outboundRecords"
:pagination="{ pageSize: 10 }"
rowKey="id"
>
<a-table-column title="出库单号" dataIndex="id" width="140" />
<a-table-column title="物料名称" dataIndex="material" width="160">
<template #default="{ text }">
<span class="font-medium">{{ text }}</span>
</template>
</a-table-column>
<a-table-column title="数量" width="120" align="center">
<template #default="{ record }">
<span class="text-orange-600 font-medium">-{{ record.quantity }} {{ record.unit }}</span>
</template>
</a-table-column>
<a-table-column title="出库类型" dataIndex="type" width="120">
<template #default="{ text }">
<a-tag :color="text === '生产领料' ? 'orange' : 'cyan'">{{ text }}</a-tag>
</template>
</a-table-column>
<a-table-column title="领用部门" dataIndex="recipient" width="120" />
<a-table-column title="操作员" dataIndex="operator" width="120" />
<a-table-column title="出库时间" dataIndex="date" width="160" />
<a-table-column title="状态" width="100" align="center">
<template #default>
<a-tag color="success">已完成</a-tag>
</template>
</a-table-column>
</a-table>
</div>
<!-- 入库登记弹窗 -->
<a-modal
v-model:open="inboundVisible"
title="入库登记"
@ok="handleInbound"
ok-text="确认入库"
cancel-text="取消"
>
<a-form :model="inboundForm" layout="vertical">
<a-form-item label="物料" required>
<a-select v-model:value="inboundForm.material" placeholder="请选择物料">
<a-select-option v-for="item in inventoryList" :key="item.code" :value="item.name">
{{ item.name }} ({{ item.code }})
</a-select-option>
</a-select>
</a-form-item>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="数量" required>
<a-input-number v-model:value="inboundForm.quantity" style="width: 100%" :min="1" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="单位">
<a-input v-model:value="inboundForm.unit" placeholder="如:套、件、个" />
</a-form-item>
</a-col>
</a-row>
<a-form-item label="入库类型">
<a-radio-group v-model:value="inboundForm.type">
<a-radio value="purchase">采购入库</a-radio>
<a-radio value="return">退货入库</a-radio>
<a-radio value="transfer">调拨入库</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="供应商" v-if="inboundForm.type === 'purchase'">
<a-input v-model:value="inboundForm.supplier" placeholder="请输入供应商名称" />
</a-form-item>
<a-form-item label="备注">
<a-textarea v-model:value="inboundForm.remark" :rows="2" placeholder="请输入备注信息" />
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<style scoped>
.warehouse-page {
padding: 0;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.glass {
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.3);
}
.card-hover {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.card-hover:hover {
transform: translateY(-4px);
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
}
.grid {
display: grid;
}
.grid-cols-4 {
grid-template-columns: repeat(4, 1fr);
}
@media (max-width: 1200px) {
.grid-cols-4 {
grid-template-columns: repeat(2, 1fr);
}
}
.mb-6 {
margin-bottom: 24px;
}
.mb-4 {
margin-bottom: 16px;
}
.mr-1 {
margin-right: 4px;
}
.mr-2 {
margin-right: 8px;
}
.ml-1 {
margin-left: 4px;
}
.text-2xl {
font-size: 24px;
}
.text-3xl {
font-size: 30px;
}
.text-lg {
font-size: 18px;
}
.text-sm {
font-size: 14px;
}
.text-base {
font-size: 16px;
}
.rounded-2xl {
border-radius: 16px;
}
.p-6 {
padding: 24px;
}
.p-4 {
padding: 16px;
}
.gap-6 {
gap: 24px;
}
.flex {
display: flex;
}
.items-center {
align-items: center;
}
.justify-between {
justify-content: space-between;
}
.font-bold {
font-weight: 700;
}
.font-medium {
font-weight: 500;
}
.text-gray-800 {
color: #1f2937;
}
.text-gray-500 {
color: #6b7280;
}
.text-green-600 {
color: #16a34a;
}
.text-orange-600 {
color: #ea580c;
}
</style>