Files
tiantian-system/app/pages/procurement.vue
赵忠林 f9e1286ab1 refactor(developer-config): 移除开发者配置页面相关代码和文档
- 删除应用配置页面及相关组件,重构路由为 /developer/config/[id].vue
- 移除开发者文档页面及其导航与样式实现
- 清理开发者侧功能完善工作日志文件
- 删除全局.gitignore配置文件,清理无用忽略规则
- 优化应用配置页面的参数读取和路由结构,解决刷新404问题
- 解决数据库配置唯一键冲突,调整保存逻辑避免重复插入
- 移除对后端配置加密字段的 secret 标记,修正加密异常问题
2026-04-09 07:35:34 +08:00

581 lines
22 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.

<template>
<div class="flex min-h-screen bg-gradient-to-br from-gray-50 to-gray-100">
<!-- 左侧导航 -->
<aside class="w-64 fixed h-full text-white flex flex-col sidebar">
<!-- Logo -->
<div class="p-6 border-b border-white/10">
<div class="flex items-center gap-3">
<div class="w-10 h-10 bg-white/20 rounded-xl flex items-center justify-center">
<BlockOutlined class="text-xl" />
</div>
<div>
<h1 class="font-bold text-lg">天天系统</h1>
<p class="text-xs text-white/70">ERP 管理平台</p>
</div>
</div>
</div>
<!-- 导航菜单 -->
<nav class="flex-1 py-6 px-3">
<div class="space-y-1">
<NuxtLink to="/" class="sidebar-item flex items-center gap-3 px-4 py-3 rounded-xl cursor-pointer">
<HomeOutlined class="text-base" />
<span>工作台</span>
</NuxtLink>
<a href="#" class="sidebar-item flex items-center gap-3 px-4 py-3 rounded-xl cursor-pointer">
<SettingOutlined class="text-base" />
<span>设备管理</span>
</a>
<NuxtLink to="/procurement" class="sidebar-item active flex items-center gap-3 px-4 py-3 rounded-xl cursor-pointer">
<ShoppingCartOutlined class="text-base" />
<span>采购管理</span>
</NuxtLink>
<a href="#" class="sidebar-item flex items-center gap-3 px-4 py-3 rounded-xl cursor-pointer">
<InboxOutlined class="text-base" />
<span>仓储物流</span>
</a>
<a href="#" class="sidebar-item flex items-center gap-3 px-4 py-3 rounded-xl cursor-pointer">
<WalletOutlined class="text-base" />
<span>财务管理</span>
</a>
<NuxtLink to="/hr" class="sidebar-item flex items-center gap-3 px-4 py-3 rounded-xl cursor-pointer">
<TeamOutlined class="text-base" />
<span>人力资源</span>
</NuxtLink>
<a href="#" class="sidebar-item flex items-center gap-3 px-4 py-3 rounded-xl cursor-pointer">
<ProjectOutlined class="text-base" />
<span>协同办公</span>
</a>
</div>
<div class="mt-8 pt-6 border-t border-white/10">
<p class="px-4 text-xs text-white/50 mb-3">个人</p>
<a href="#" class="sidebar-item flex items-center gap-3 px-4 py-3 rounded-xl cursor-pointer">
<UserOutlined class="text-base" />
<span>个人信息</span>
</a>
<a href="#" class="sidebar-item flex items-center gap-3 px-4 py-3 rounded-xl cursor-pointer">
<SettingOutlined class="text-base" />
<span>系统设置</span>
</a>
</div>
</nav>
<!-- 用户信息 -->
<div class="p-4 border-t border-white/10">
<div class="flex items-center gap-3">
<div class="w-10 h-10 bg-white/20 rounded-full flex items-center justify-center">
<UserOutlined />
</div>
<div class="flex-1">
<p class="font-medium">管理员</p>
<p class="text-xs text-white/70">超级管理员</p>
</div>
<button class="text-white/70 hover:text-white">
<LogoutOutlined />
</button>
</div>
</div>
</aside>
<!-- 主内容区 -->
<main class="flex-1 ml-64">
<!-- 顶部栏 -->
<header class="bg-white/85 backdrop-blur-xl sticky top-0 z-50 px-8 py-4 border-b border-white/30">
<div class="flex items-center justify-between">
<!-- 搜索 -->
<div class="relative w-96">
<a-input
v-model:value="searchKeyword"
placeholder="搜索采购单、供应商、物料..."
class="w-full"
size="large"
>
<template #prefix>
<SearchOutlined class="text-gray-400" />
</template>
</a-input>
</div>
<!-- 右侧 -->
<div class="flex items-center gap-6">
<button class="text-gray-500 hover:text-purple-600 transition-colors">
<FullscreenOutlined class="text-lg" />
</button>
<a-badge count="3" :offset="[-2, 2]">
<BellOutlined class="text-gray-500 hover:text-purple-600 transition-colors text-lg cursor-pointer" />
</a-badge>
<button class="text-gray-500 hover:text-purple-600 transition-colors">
<GlobalOutlined class="text-lg" />
</button>
<div class="flex items-center gap-3 pl-6 border-l border-gray-200">
<a-avatar class="bg-gradient-to-br from-purple-500 to-pink-500">A</a-avatar>
<div>
<p class="font-medium text-gray-800">Admin</p>
<p class="text-xs text-gray-500">超级管理员</p>
</div>
</div>
</div>
</div>
</header>
<!-- 内容区域 -->
<div class="p-8">
<!-- 页面标题 -->
<div class="mb-8">
<div class="flex items-center gap-3 mb-2">
<NuxtLink to="/" class="text-gray-500 hover:text-purple-600 transition-colors">
<ArrowLeftOutlined />
</NuxtLink>
<h2 class="text-3xl font-bold text-gray-800">采购管理</h2>
</div>
<p class="text-gray-500">管理采购申请订单跟踪供应商信息及采购统计</p>
</div>
<!-- 数据概览 -->
<div class="grid grid-cols-4 gap-6 mb-8">
<div class="stat-card blue bg-white rounded-2xl p-6 card-hover shadow-sm">
<div class="flex items-center justify-between mb-4">
<div class="w-12 h-12 bg-blue-100 rounded-xl flex items-center justify-center">
<FileTextOutlined class="text-blue-600 text-xl" />
</div>
<span class="text-green-500 text-sm font-medium flex items-center gap-1"><ArrowUpOutlined /> 8%</span>
</div>
<h3 class="text-3xl font-bold text-gray-800 mb-1">23</h3>
<p class="text-gray-500 text-sm">本月采购单</p>
</div>
<div class="stat-card green bg-white rounded-2xl p-6 card-hover shadow-sm">
<div class="flex items-center justify-between mb-4">
<div class="w-12 h-12 bg-green-100 rounded-xl flex items-center justify-center">
<CheckCircleOutlined class="text-green-600 text-xl" />
</div>
<span class="text-green-500 text-sm font-medium flex items-center gap-1"><ArrowUpOutlined /> 12%</span>
</div>
<h3 class="text-3xl font-bold text-gray-800 mb-1">18</h3>
<p class="text-gray-500 text-sm">已完成</p>
</div>
<div class="stat-card orange bg-white rounded-2xl p-6 card-hover shadow-sm">
<div class="flex items-center justify-between mb-4">
<div class="w-12 h-12 bg-orange-100 rounded-xl flex items-center justify-center">
<ClockCircleOutlined class="text-orange-600 text-xl" />
</div>
<span class="text-red-500 text-sm font-medium flex items-center gap-1"><ArrowUpOutlined /> 3</span>
</div>
<h3 class="text-3xl font-bold text-gray-800 mb-1">5</h3>
<p class="text-gray-500 text-sm">待审批</p>
</div>
<div class="stat-card purple bg-white rounded-2xl p-6 card-hover shadow-sm">
<div class="flex items-center justify-between mb-4">
<div class="w-12 h-12 bg-purple-100 rounded-xl flex items-center justify-center">
<DollarOutlined class="text-purple-600 text-xl" />
</div>
<span class="text-green-500 text-sm font-medium flex items-center gap-1"><ArrowUpOutlined /> 15%</span>
</div>
<h3 class="text-3xl font-bold text-gray-800 mb-1">¥45.8<span class="text-lg"></span></h3>
<p class="text-gray-500 text-sm">本月采购额</p>
</div>
</div>
<!-- 快捷操作栏 -->
<div class="bg-white/85 backdrop-blur-xl rounded-2xl p-4 mb-8 shadow-sm">
<div class="flex items-center justify-between">
<div class="flex gap-3">
<a-button type="primary" class="bg-gradient-to-r from-purple-600 to-purple-700 border-0">
<PlusOutlined />
新建采购单
</a-button>
<a-button>
<ImportOutlined class="text-blue-500" />
批量导入
</a-button>
<a-button>
<ExportOutlined class="text-green-500" />
导出报表
</a-button>
</div>
<div class="flex gap-3">
<a-button>
<FilterOutlined class="text-gray-500" />
筛选
</a-button>
<a-button>
<CalendarOutlined class="text-gray-500" />
2026年4月
</a-button>
</div>
</div>
</div>
<!-- 标签页 -->
<div class="bg-white/85 backdrop-blur-xl rounded-2xl p-2 mb-8 inline-flex shadow-sm">
<a-segmented v-model:value="activeTab" :options="tabOptions" />
</div>
<!-- 采购申请列表 -->
<div class="bg-white/85 backdrop-blur-xl rounded-2xl overflow-hidden mb-8 shadow-sm">
<div class="p-6 border-b border-gray-100">
<div class="flex items-center justify-between">
<h3 class="font-bold text-lg text-gray-800 flex items-center gap-2">
<UnorderedListOutlined class="text-purple-500" />
采购申请列表
<a-tag color="purple">23</a-tag>
</h3>
<div class="flex items-center gap-3">
<a-input-search
v-model:value="listSearchKeyword"
placeholder="搜索采购单号、申请人..."
class="w-64"
/>
</div>
</div>
</div>
<a-table :dataSource="purchaseData" :columns="columns" :pagination="false">
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'applicant'">
<div class="flex items-center gap-3">
<a-avatar :style="{ background: record.avatarBg }">{{ record.applicantName[0] }}</a-avatar>
<div>
<p class="font-medium text-gray-800">{{ record.applicantName }}</p>
<p class="text-xs text-gray-500">{{ record.department }}</p>
</div>
</div>
</template>
<template v-if="column.key === 'items'">
<p class="font-medium text-gray-800">{{ record.itemName }}</p>
<p class="text-xs text-gray-500">{{ record.itemSpec }}</p>
</template>
<template v-if="column.key === 'status'">
<a-tag :color="getStatusColor(record.status)">
<template v-if="record.status === 'pending'"><ClockCircleOutlined /> 待审批</template>
<template v-else-if="record.status === 'approved'"><CheckCircleOutlined /> 已批准</template>
<template v-else-if="record.status === 'processing'"><LoadingOutlined /> 采购中</template>
<template v-else-if="record.status === 'completed'"><CheckCircleOutlined /> 已完成</template>
<template v-else-if="record.status === 'rejected'"><CloseCircleOutlined /> 已驳回</template>
</a-tag>
</template>
<template v-if="column.key === 'action'">
<div class="flex items-center justify-center gap-2">
<a-button type="text" size="small" class="text-blue-600">
<EyeOutlined />
</a-button>
<template v-if="record.status === 'pending'">
<a-button type="text" size="small" class="text-green-600">
<CheckOutlined />
</a-button>
<a-button type="text" size="small" class="text-red-600">
<CloseOutlined />
</a-button>
</template>
<template v-else-if="record.status === 'approved'">
<a-button type="text" size="small" class="text-purple-600">
<FileTextOutlined />
</a-button>
</template>
<template v-else-if="record.status === 'processing'">
<a-button type="text" size="small" class="text-orange-600">
<CarOutlined />
</a-button>
</template>
<template v-else-if="record.status === 'rejected'">
<a-button type="text" size="small" class="text-yellow-600">
<RedoOutlined />
</a-button>
</template>
</div>
</template>
</template>
</a-table>
<!-- 分页 -->
<div class="p-6 border-t border-gray-100">
<div class="flex items-center justify-between">
<p class="text-sm text-gray-500">显示 1-5 23 </p>
<a-pagination v-model:current="currentPage" :total="23" :pageSize="5" show-less-items />
</div>
</div>
</div>
<!-- 底部区域 -->
<div class="grid grid-cols-2 gap-6">
<!-- 供应商概览 -->
<div class="bg-white/85 backdrop-blur-xl rounded-2xl p-6 card-hover shadow-sm">
<div class="flex items-center justify-between mb-6">
<h3 class="font-bold text-lg text-gray-800 flex items-center gap-2">
<BankOutlined class="text-blue-500" />
主要供应商
</h3>
<button class="text-purple-600 text-sm font-medium hover:underline">查看全部</button>
</div>
<div class="space-y-4">
<div
v-for="supplier in suppliers"
:key="supplier.name"
class="supplier-card flex items-center gap-4 p-4 bg-gray-50 rounded-xl cursor-pointer hover:shadow-md transition-all"
>
<div class="w-12 h-12 rounded-xl flex items-center justify-center flex-shrink-0" :class="supplier.iconBg">
<component :is="supplier.icon" :class="supplier.iconColor" />
</div>
<div class="flex-1">
<h4 class="font-medium text-gray-800">{{ supplier.name }}</h4>
<p class="text-xs text-gray-500">{{ supplier.category }} | 合作 {{ supplier.years }} </p>
</div>
<div class="text-right">
<p class="font-semibold text-gray-800">{{ supplier.amount }}</p>
<p class="text-xs text-green-500">本年采购额</p>
</div>
</div>
</div>
</div>
<!-- 采购趋势 -->
<div class="bg-white/85 backdrop-blur-xl rounded-2xl p-6 card-hover shadow-sm">
<div class="flex items-center justify-between mb-6">
<h3 class="font-bold text-lg text-gray-800 flex items-center gap-2">
<LineChartOutlined class="text-purple-500" />
采购趋势
</h3>
<div class="flex gap-2">
<button
v-for="period in ['本月', '本季', '本年']"
:key="period"
:class="['px-3 py-1 text-xs rounded-lg font-medium', trendPeriod === period ? 'bg-purple-100 text-purple-600' : 'bg-gray-100 text-gray-600']"
@click="trendPeriod = period"
>
{{ period }}
</button>
</div>
</div>
<div class="space-y-4">
<div v-for="trend in trends" :key="trend.name">
<div class="flex items-center justify-between mb-2">
<span class="text-sm text-gray-600">{{ trend.name }}</span>
<span class="text-sm font-medium text-gray-800">{{ trend.amount }} ({{ trend.percent }}%)</span>
</div>
<a-progress :percent="trend.percent" :stroke-color="trend.color" :show-info="false" />
</div>
</div>
<div class="mt-6 pt-4 border-t border-gray-100">
<div class="flex items-center justify-between">
<span class="text-sm text-gray-500">本月采购总额</span>
<span class="text-xl font-bold gradient-text">¥502</span>
</div>
</div>
</div>
</div>
</div>
</main>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import {
HomeOutlined,
SettingOutlined,
ShoppingCartOutlined,
InboxOutlined,
WalletOutlined,
TeamOutlined,
ProjectOutlined,
UserOutlined,
LogoutOutlined,
SearchOutlined,
FullscreenOutlined,
BellOutlined,
GlobalOutlined,
ArrowLeftOutlined,
ArrowUpOutlined,
PlusOutlined,
ImportOutlined,
ExportOutlined,
FilterOutlined,
CalendarOutlined,
UnorderedListOutlined,
FileTextOutlined,
CheckCircleOutlined,
ClockCircleOutlined,
DollarOutlined,
LoadingOutlined,
CloseCircleOutlined,
EyeOutlined,
CheckOutlined,
CloseOutlined,
RedoOutlined,
CarOutlined,
BankOutlined,
LineChartOutlined,
BlockOutlined,
BuildOutlined,
LaptopOutlined,
} from '@ant-design/icons-vue'
definePageMeta({ layout: 'blank' })
// 搜索关键词
const searchKeyword = ref('')
const listSearchKeyword = ref('')
const currentPage = ref(1)
// 标签页
const activeTab = ref('采购申请')
const tabOptions = ['采购申请', '采购订单', '供应商管理', '采购统计']
// 趋势周期
const trendPeriod = ref('本月')
// 表格列定义
const columns = [
{ title: '采购单号', dataIndex: 'code', key: 'code' },
{ title: '申请信息', key: 'applicant' },
{ title: '采购物品', key: 'items' },
{ title: '金额', dataIndex: 'amount', key: 'amount' },
{ title: '状态', key: 'status' },
{ title: '申请时间', dataIndex: 'time', key: 'time' },
{ title: '操作', key: 'action', align: 'center' },
]
// 采购数据
const purchaseData = [
{
key: '1',
code: 'CG-2026-0409-001',
applicantName: '张三',
department: '技术部',
avatarBg: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
itemName: '办公电脑 x5',
itemSpec: '联想 ThinkPad T14',
amount: '¥45,000',
status: 'pending',
time: '2026-04-09 09:30',
},
{
key: '2',
code: 'CG-2026-0408-003',
applicantName: '李四',
department: '生产部',
avatarBg: 'linear-gradient(135deg, #11998e 0%, #38ef7d 100%)',
itemName: '原材料-钢材 x2000kg',
itemSpec: '304不锈钢板',
amount: '¥128,000',
status: 'approved',
time: '2026-04-08 14:20',
},
{
key: '3',
code: 'CG-2026-0408-002',
applicantName: '王五',
department: '行政部',
avatarBg: 'linear-gradient(135deg, #f5576c 0%, #f093fb 100%)',
itemName: '办公用品一批',
itemSpec: '打印纸、墨盒、文具',
amount: '¥3,580',
status: 'processing',
time: '2026-04-08 10:15',
},
{
key: '4',
code: 'CG-2026-0407-005',
applicantName: '赵六',
department: '研发部',
avatarBg: 'linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%)',
itemName: '实验设备 x2',
itemSpec: '示波器、万用表',
amount: '¥25,600',
status: 'completed',
time: '2026-04-07 16:45',
},
{
key: '5',
code: 'CG-2026-0407-004',
applicantName: '孙七',
department: '质量部',
avatarBg: 'linear-gradient(135deg, #a8edea 0%, #fed6e3 100%)',
itemName: '检测设备校准服务',
itemSpec: '年度校准合同',
amount: '¥18,000',
status: 'rejected',
time: '2026-04-07 09:00',
},
]
// 供应商数据
const suppliers = [
{ name: '深圳市华强电子有限公司', category: '电子元器件', years: 3, amount: '¥156万', icon: BuildOutlined, iconBg: 'bg-blue-100', iconColor: 'text-blue-600' },
{ name: '上海宝钢贸易有限公司', category: '钢材原料', years: 5, amount: '¥328万', icon: BuildOutlined, iconBg: 'bg-green-100', iconColor: 'text-green-600' },
{ name: '联想集团(深圳)有限公司', category: '办公设备', years: 2, amount: '¥89万', icon: LaptopOutlined, iconBg: 'bg-orange-100', iconColor: 'text-orange-600' },
]
// 趋势数据
const trends = [
{ name: '原材料采购', amount: '¥328万', percent: 65, color: { from: '#667eea', to: '#764ba2' } },
{ name: '设备采购', amount: '¥89万', percent: 18, color: { from: '#11998e', to: '#38ef7d' } },
{ name: '办公用品', amount: '¥45万', percent: 9, color: { from: '#f5576c', to: '#f093fb' } },
{ name: '服务采购', amount: '¥40万', percent: 8, color: { from: '#ffecd2', to: '#fcb69f' } },
]
// 获取状态颜色
const getStatusColor = (status: string) => {
const colorMap: Record<string, string> = {
pending: 'warning',
approved: 'success',
processing: 'processing',
completed: 'blue',
rejected: 'error',
}
return colorMap[status] || 'default'
}
</script>
<style scoped>
.sidebar {
background: linear-gradient(180deg, #667eea 0%, #764ba2 100%);
}
.sidebar-item:hover,
.sidebar-item.active {
background: rgba(255, 255, 255, 0.2);
}
.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);
}
.stat-card {
position: relative;
overflow: hidden;
}
.stat-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
}
.stat-card.blue::before { background: linear-gradient(90deg, #667eea, #764ba2); }
.stat-card.green::before { background: linear-gradient(90deg, #11998e, #38ef7d); }
.stat-card.orange::before { background: linear-gradient(90deg, #f093fb, #f5576c); }
.stat-card.purple::before { background: linear-gradient(90deg, #a8edea, #fed6e3); }
.gradient-text {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.supplier-card {
transition: all 0.3s ease;
}
</style>