Files
tiantian-system/app/pages/admin/supply/purchase.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

465 lines
14 KiB
Vue
Raw 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 purchaseStats = ref([
{ label: '采购单总数', value: 156, icon: 'fa-file-alt', gradient: 'from-blue-500 to-purple-500', change: '+23%', up: true },
{ label: '待审批', value: 12, icon: 'fa-clock', gradient: 'from-orange-500 to-yellow-500', change: '+3', up: false },
{ label: '已完成', value: 138, icon: 'fa-check-circle', gradient: 'from-green-500 to-teal-500', change: '+18%', up: true },
{ label: '采购总额', value: '89.5', unit: '万', icon: 'fa-wallet', gradient: 'from-pink-500 to-rose-500', change: '+15%', up: true },
])
// 采购单列表
const purchaseOrders = ref([
{ id: 'PO-2026040901', supplier: '深圳市精密机械有限公司', material: '轴承组件 A型', quantity: 500, unit: '套', amount: 25000, status: 'pending', applicant: '张经理', date: '2026-04-09' },
{ id: 'PO-2026040802', supplier: '上海五金工具厂', material: '数控刀具套装', quantity: 20, unit: '套', amount: 36000, status: 'approved', applicant: '李主管', date: '2026-04-08' },
{ id: 'PO-2026040801', supplier: '东莞市金属材料公司', material: '铝合金板材', quantity: 200, unit: '张', amount: 48000, status: 'processing', applicant: '王经理', date: '2026-04-08' },
{ id: 'PO-2026040703', supplier: '苏州液压设备厂', material: '液压缸体 B型', quantity: 50, unit: '件', amount: 75000, status: 'completed', applicant: '赵主管', date: '2026-04-07' },
{ id: 'PO-2026040702', supplier: '广州润滑油脂公司', material: '工业润滑油', quantity: 100, unit: '桶', amount: 15000, status: 'completed', applicant: '张经理', date: '2026-04-07' },
{ id: 'PO-2026040601', supplier: '深圳市精密机械有限公司', material: '密封圈组件', quantity: 1000, unit: '个', amount: 8000, status: 'completed', applicant: '李主管', date: '2026-04-06' },
])
const statusMap: Record<string, { label: string; color: string; bg: string }> = {
pending: { label: '待审批', color: 'text-orange-600', bg: 'bg-orange-100' },
approved: { label: '已审批', color: 'text-blue-600', bg: 'bg-blue-100' },
processing: { label: '执行中', color: 'text-purple-600', bg: 'bg-purple-100' },
completed: { label: '已完成', color: 'text-green-600', bg: 'bg-green-100' },
rejected: { label: '已驳回', color: 'text-red-600', bg: 'bg-red-100' },
}
const statusFilter = ref('all')
const searchKeyword = ref('')
const filteredList = computed(() => {
return purchaseOrders.value.filter((item) => {
const matchStatus = statusFilter.value === 'all' || item.status === statusFilter.value
const matchSearch = !searchKeyword.value ||
item.id.toLowerCase().includes(searchKeyword.value.toLowerCase()) ||
item.supplier.toLowerCase().includes(searchKeyword.value.toLowerCase()) ||
item.material.toLowerCase().includes(searchKeyword.value.toLowerCase())
return matchStatus && matchSearch
})
})
// 供应商列表
const suppliers = ref([
{ name: '深圳市精密机械有限公司', contact: '陈经理', phone: '138****1234', items: 45, total: '120万', rating: 4.8 },
{ name: '上海五金工具厂', contact: '李总', phone: '139****5678', items: 32, total: '85万', rating: 4.6 },
{ name: '东莞市金属材料公司', contact: '王经理', phone: '137****9012', items: 28, total: '200万', rating: 4.9 },
{ name: '苏州液压设备厂', contact: '张工', phone: '136****3456', items: 15, total: '95万', rating: 4.7 },
])
const addFormVisible = ref(false)
const addForm = reactive({
supplier: '',
material: '',
quantity: '',
unit: '',
estimatedAmount: '',
deliveryDate: '',
remark: '',
})
function handleAdd() {
addFormVisible.value = false
message.success('采购单创建成功')
}
</script>
<template>
<div class="purchase-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>
<a-button type="primary" @click="addFormVisible = true">
<template #icon><i class="fas fa-plus mr-1"></i></template>
新建采购单
</a-button>
</div>
<!-- 统计卡片 -->
<div class="grid grid-cols-4 gap-6 mb-6">
<div
v-for="stat in purchaseStats"
: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>
<!-- 筛选栏 -->
<div class="glass rounded-2xl p-4 mb-6">
<div class="flex items-center justify-between gap-4">
<div class="flex items-center gap-3">
<span class="text-gray-600 font-medium">状态筛选</span>
<a-radio-group v-model:value="statusFilter" button-style="solid">
<a-radio-button value="all">全部</a-radio-button>
<a-radio-button value="pending">待审批</a-radio-button>
<a-radio-button value="approved">已审批</a-radio-button>
<a-radio-button value="processing">执行中</a-radio-button>
<a-radio-button value="completed">已完成</a-radio-button>
</a-radio-group>
</div>
<a-input-search
v-model:value="searchKeyword"
placeholder="搜索采购单号、供应商、物料..."
style="width: 300px"
allow-clear
/>
</div>
</div>
<!-- 采购单列表 -->
<div 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-file-alt text-blue-500 mr-2"></i>
采购单列表
</h3>
<span class="text-sm text-gray-500"> {{ filteredList.length }} 条记录</span>
</div>
<a-table
:dataSource="filteredList"
:pagination="{ pageSize: 10 }"
rowKey="id"
:scroll="{ x: 1200 }"
>
<a-table-column title="采购单号" dataIndex="id" width="140" />
<a-table-column title="供应商" dataIndex="supplier" width="200">
<template #default="{ text }">
<span class="font-medium">{{ text }}</span>
</template>
</a-table-column>
<a-table-column title="采购物料" dataIndex="material" width="150" />
<a-table-column title="数量" dataIndex="quantity" width="100" align="center">
<template #default="{ record }">
{{ record.quantity }} {{ record.unit }}
</template>
</a-table-column>
<a-table-column title="金额(元)" dataIndex="amount" width="120" align="right">
<template #default="{ text }">
<span class="font-medium text-orange-600">¥{{ text.toLocaleString() }}</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"
:class="[statusMap[text]?.color, statusMap[text]?.bg]"
>
{{ statusMap[text]?.label }}
</span>
</template>
</a-table-column>
<a-table-column title="申请人" dataIndex="applicant" width="100" align="center" />
<a-table-column title="申请日期" dataIndex="date" width="120" />
<a-table-column title="操作" width="160" 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="审批" v-if="statusFilter === 'pending'">
<i class="fas fa-check"></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 class="glass rounded-2xl p-6 mt-6">
<div class="flex items-center justify-between mb-4">
<h3 class="font-bold text-lg text-gray-800">
<i class="fas fa-building text-green-500 mr-2"></i>
供应商列表
</h3>
<a-button type="link">
<i class="fas fa-plus mr-1"></i>添加供应商
</a-button>
</div>
<a-row :gutter="[16, 16]">
<a-col :xs="24" :sm="12" :lg="6" v-for="supplier in suppliers" :key="supplier.name">
<div class="supplier-card glass rounded-xl p-4 card-hover">
<div class="flex items-start justify-between mb-3">
<div class="w-10 h-10 rounded-lg bg-gradient-to-br from-blue-500 to-purple-500 flex items-center justify-center text-white">
<i class="fas fa-building"></i>
</div>
<a-rate :default-value="supplier.rating" disabled size="small" />
</div>
<h4 class="font-bold text-gray-800 mb-1">{{ supplier.name }}</h4>
<p class="text-sm text-gray-500 mb-2">{{ supplier.contact }} | {{ supplier.phone }}</p>
<div class="flex justify-between text-sm">
<span class="text-gray-500">合作物料<span class="text-blue-600 font-medium">{{ supplier.items }}</span> </span>
<span class="text-gray-500">累计<span class="text-green-600 font-medium">{{ supplier.total }}</span></span>
</div>
</div>
</a-col>
</a-row>
</div>
<!-- 新建采购单弹窗 -->
<a-modal
v-model:open="addFormVisible"
title="新建采购单"
@ok="handleAdd"
ok-text="确认创建"
cancel-text="取消"
width="600px"
>
<a-form :model="addForm" layout="vertical">
<a-form-item label="供应商" required>
<a-select v-model:value="addForm.supplier" placeholder="请选择供应商">
<a-select-option v-for="s in suppliers" :key="s.name" :value="s.name">{{ s.name }}</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="采购物料" required>
<a-input v-model:value="addForm.material" placeholder="请输入物料名称" />
</a-form-item>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="数量" required>
<a-input-number v-model:value="addForm.quantity" style="width: 100%" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="单位">
<a-input v-model:value="addForm.unit" placeholder="如:套、件、个" />
</a-form-item>
</a-col>
</a-row>
<a-form-item label="预估金额(元)">
<a-input-number v-model:value="addForm.estimatedAmount" style="width: 100%" :min="0" />
</a-form-item>
<a-form-item label="期望交货日期">
<a-date-picker v-model:value="addForm.deliveryDate" style="width: 100%" />
</a-form-item>
<a-form-item label="备注">
<a-textarea v-model:value="addForm.remark" :rows="3" placeholder="请输入备注信息" />
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<style scoped>
.purchase-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;
}
.mb-3 {
margin-bottom: 12px;
}
.mb-2 {
margin-bottom: 8px;
}
.mb-1 {
margin-bottom: 4px;
}
.mt-1 {
margin-top: 4px;
}
.mt-6 {
margin-top: 24px;
}
.ml-1 {
margin-left: 4px;
}
.mr-2 {
margin-right: 8px;
}
.mr-1 {
margin-right: 4px;
}
.text-2xl {
font-size: 24px;
}
.text-3xl {
font-size: 30px;
}
.text-lg {
font-size: 18px;
}
.text-sm {
font-size: 14px;
}
.text-xs {
font-size: 12px;
}
.text-base {
font-size: 16px;
}
.rounded-2xl {
border-radius: 16px;
}
.rounded-xl {
border-radius: 12px;
}
.p-6 {
padding: 24px;
}
.p-4 {
padding: 16px;
}
.gap-6 {
gap: 24px;
}
.gap-4 {
gap: 16px;
}
.gap-3 {
gap: 12px;
}
.gap-2 {
gap: 8px;
}
.flex {
display: flex;
}
.items-center {
align-items: center;
}
.items-start {
align-items: flex-start;
}
.justify-between {
justify-content: space-between;
}
.justify-end {
justify-content: flex-end;
}
.font-bold {
font-weight: 700;
}
.font-medium {
font-weight: 500;
}
.text-gray-800 {
color: #1f2937;
}
.text-gray-600 {
color: #4b5563;
}
.text-gray-500 {
color: #6b7280;
}
.text-orange-600 {
color: #ea580c;
}
.text-blue-600 {
color: #2563eb;
}
.text-green-600 {
color: #16a34a;
}
.bg-gradient-to-br {
background-image: linear-gradient(to bottom right, var(--tw-gradient-stops));
}
</style>