Files
tiantian-system/app/pages/hr.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

536 lines
20 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.

<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">DEMO演示系统</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>
<NuxtLink to="/device" class="sidebar-item flex items-center gap-3 px-4 py-3 rounded-xl cursor-pointer">
<SettingOutlined class="text-base" />
<span>设备管理</span>
</NuxtLink>
<NuxtLink to="/procurement" class="sidebar-item flex items-center gap-3 px-4 py-3 rounded-xl cursor-pointer">
<ShoppingCartOutlined class="text-base" />
<span>采购管理</span>
</NuxtLink>
<NuxtLink to="/warehouse" class="sidebar-item flex items-center gap-3 px-4 py-3 rounded-xl cursor-pointer">
<InboxOutlined class="text-base" />
<span>仓储物流</span>
</NuxtLink>
<NuxtLink to="/finance" class="sidebar-item flex items-center gap-3 px-4 py-3 rounded-xl cursor-pointer">
<WalletOutlined class="text-base" />
<span>财务管理</span>
</NuxtLink>
<NuxtLink to="/hr" class="sidebar-item active flex items-center gap-3 px-4 py-3 rounded-xl cursor-pointer">
<TeamOutlined class="text-base" />
<span>人力资源</span>
</NuxtLink>
<NuxtLink to="/office" class="sidebar-item flex items-center gap-3 px-4 py-3 rounded-xl cursor-pointer">
<ProjectOutlined class="text-base" />
<span>协同办公</span>
</NuxtLink>
</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="5" :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">
<TeamOutlined class="text-blue-600 text-xl" />
</div>
<span class="text-green-500 text-sm font-medium flex items-center gap-1"><ArrowUpOutlined /> 5%</span>
</div>
<h3 class="text-3xl font-bold text-gray-800 mb-1">186</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">
<UserAddOutlined 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">8</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">12</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 /> 8%</span>
</div>
<h3 class="text-3xl font-bold text-gray-800 mb-1">¥156<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">186</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="employeeData" :columns="columns" :pagination="false">
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'employee'">
<div class="flex items-center gap-3">
<a-avatar :style="{ background: record.avatarBg }">{{ record.name[0] }}</a-avatar>
<div>
<p class="font-medium text-gray-800">{{ record.name }}</p>
<p class="text-xs text-gray-500">{{ record.employeeId }}</p>
</div>
</div>
</template>
<template v-if="column.key === 'department'">
<p class="font-medium text-gray-800">{{ record.department }}</p>
<p class="text-xs text-gray-500">{{ record.position }}</p>
</template>
<template v-if="column.key === 'status'">
<a-tag :color="getStatusColor(record.status)">
<template v-if="record.status === 'active'"><CheckCircleOutlined /> 在职</template>
<template v-else-if="record.status === 'probation'"><ClockCircleOutlined /> 试用期</template>
<template v-else-if="record.status === 'leave'"><PauseCircleOutlined /> 休假中</template>
<template v-else-if="record.status === 'resigned'"><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>
<a-button type="text" size="small" class="text-green-600">
<EditOutlined />
</a-button>
<a-button type="text" size="small" class="text-red-600">
<DeleteOutlined />
</a-button>
</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 186 </p>
<a-pagination v-model:current="currentPage" :total="186" :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">
<PieChartOutlined 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="dept in departments" :key="dept.name">
<div class="flex items-center justify-between mb-2">
<span class="text-sm text-gray-600">{{ dept.name }}</span>
<span class="text-sm font-medium text-gray-800">{{ dept.count }} ({{ dept.percent }}%)</span>
</div>
<a-progress :percent="dept.percent" :stroke-color="dept.color" :show-info="false" />
</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">
<BellOutlined class="text-orange-500" />
待办事项
</h3>
<a-badge count="5" />
</div>
<div class="space-y-3">
<div
v-for="todo in todos"
:key="todo.id"
class="todo-item flex items-center gap-4 p-4 bg-gray-50 rounded-xl cursor-pointer hover:shadow-md transition-all"
>
<div class="w-10 h-10 rounded-xl flex items-center justify-center flex-shrink-0" :class="todo.iconBg">
<component :is="todo.icon" :class="todo.iconColor" />
</div>
<div class="flex-1">
<h4 class="font-medium text-gray-800">{{ todo.title }}</h4>
<p class="text-xs text-gray-500">{{ todo.desc }}</p>
</div>
<a-tag :color="todo.urgency === 'high' ? 'red' : todo.urgency === 'medium' ? 'orange' : 'blue'">
{{ todo.urgencyText }}
</a-tag>
</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,
CheckCircleOutlined,
ClockCircleOutlined,
DollarOutlined,
UserAddOutlined,
PauseCircleOutlined,
CloseCircleOutlined,
EyeOutlined,
EditOutlined,
DeleteOutlined,
PieChartOutlined,
BlockOutlined,
FileTextOutlined,
CheckOutlined,
} from '@ant-design/icons-vue'
definePageMeta({ layout: 'blank' })
// 搜索关键词
const searchKeyword = ref('')
const listSearchKeyword = ref('')
const currentPage = ref(1)
// 标签页
const activeTab = ref('员工管理')
const tabOptions = ['员工管理', '组织架构', '考勤管理', '薪资福利', '招聘管理']
// 表格列定义
const columns = [
{ title: '员工信息', key: 'employee' },
{ title: '部门职位', key: 'department' },
{ title: '入职日期', dataIndex: 'joinDate', key: 'joinDate' },
{ title: '联系方式', dataIndex: 'phone', key: 'phone' },
{ title: '状态', key: 'status' },
{ title: '操作', key: 'action', align: 'center' },
]
// 员工数据
const employeeData = [
{
key: '1',
name: '张三',
employeeId: 'TT2024001',
department: '技术部',
position: '高级前端工程师',
joinDate: '2024-03-15',
phone: '138****1234',
status: 'active',
avatarBg: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
},
{
key: '2',
name: '李四',
employeeId: 'TT2024002',
department: '产品部',
position: '产品经理',
joinDate: '2024-02-20',
phone: '139****5678',
status: 'active',
avatarBg: 'linear-gradient(135deg, #11998e 0%, #38ef7d 100%)',
},
{
key: '3',
name: '王五',
employeeId: 'TT2025001',
department: '销售部',
position: '销售经理',
joinDate: '2025-01-10',
phone: '137****9012',
status: 'probation',
avatarBg: 'linear-gradient(135deg, #f5576c 0%, #f093fb 100%)',
},
{
key: '4',
name: '赵六',
employeeId: 'TT2023005',
department: '人事部',
position: 'HR专员',
joinDate: '2023-08-15',
phone: '136****3456',
status: 'leave',
avatarBg: 'linear-gradient(135deg, #ffecd2 0%, #fcb69f 100%)',
},
{
key: '5',
name: '孙七',
employeeId: 'TT2022008',
department: '财务部',
position: '财务主管',
joinDate: '2022-06-01',
phone: '135****7890',
status: 'active',
avatarBg: 'linear-gradient(135deg, #a8edea 0%, #fed6e3 100%)',
},
]
// 部门数据
const departments = [
{ name: '技术部', count: 45, percent: 24, color: { from: '#667eea', to: '#764ba2' } },
{ name: '销售部', count: 38, percent: 20, color: { from: '#11998e', to: '#38ef7d' } },
{ name: '产品部', count: 25, percent: 13, color: { from: '#f5576c', to: '#f093fb' } },
{ name: '运营部', count: 32, percent: 17, color: { from: '#ffecd2', to: '#fcb69f' } },
{ name: '人事行政部', count: 18, percent: 10, color: { from: '#a8edea', to: '#fed6e3' } },
{ name: '财务部', count: 15, percent: 8, color: { from: '#667eea', to: '#764ba2' } },
{ name: '其他', count: 13, percent: 7, color: { from: '#11998e', to: '#38ef7d' } },
]
// 待办事项
const todos = [
{ id: 1, title: '审批请假申请', desc: '技术部 - 张三3天病假', urgency: 'high', urgencyText: '紧急', icon: FileTextOutlined, iconBg: 'bg-red-100', iconColor: 'text-red-600' },
{ id: 2, title: '试用期转正评估', desc: '销售部 - 王五入职3个月', urgency: 'medium', urgencyText: '今日', icon: CheckOutlined, iconBg: 'bg-orange-100', iconColor: 'text-orange-600' },
{ id: 3, title: '薪资调整审批', desc: '产品部 - 李四,晋升调薪', urgency: 'medium', urgencyText: '本周', icon: DollarOutlined, iconBg: 'bg-blue-100', iconColor: 'text-blue-600' },
{ id: 4, title: '新员工入职办理', desc: '明天有2名新员工报到', urgency: 'low', urgencyText: '明日', icon: UserAddOutlined, iconBg: 'bg-green-100', iconColor: 'text-green-600' },
]
// 获取状态颜色
const getStatusColor = (status: string) => {
const colorMap: Record<string, string> = {
active: 'success',
probation: 'warning',
leave: 'processing',
resigned: 'default',
}
return colorMap[status] || 'default'
}
</script>
<style scoped>
.sidebar {
background: linear-gradient(180deg, #667eea 0%, #764ba2 100%);
}
.sidebar-item {
color: white;
}
.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); }
.todo-item {
transition: all 0.3s ease;
}
</style>