123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504 |
- <template>
- <div class="min-h-screen bg-gray-50 dark:bg-gray-900 p-6">
- <!-- 顶部导航栏 -->
- <div class="flex justify-between items-center mb-6">
- <h1 class="text-2xl font-bold text-gray-900 dark:text-white">薪资管理</h1>
- <div class="flex space-x-4">
- <button
- @click="showSalaryAdjustment = true"
- class="px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
- >
- <PlusIcon class="h-5 w-5 inline-block mr-2" />
- 薪资调整
- </button>
- <button
- @click="handleSalaryPayment"
- class="px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
- >
- <DownloadIcon class="h-5 w-5 inline-block mr-2" />
- 发放薪资
- </button>
- </div>
- </div>
- <!-- 薪资统计卡片 -->
- <div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-6">
- <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
- <h3 class="text-lg font-medium text-gray-900 dark:text-white">本月总薪资</h3>
- <p class="mt-2 text-3xl font-bold text-indigo-600">¥{{ totalSalary }}</p>
- <p class="mt-1 text-sm text-gray-500 dark:text-gray-400">本月</p>
- </div>
- <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
- <h3 class="text-lg font-medium text-gray-900 dark:text-white">平均薪资</h3>
- <p class="mt-2 text-3xl font-bold text-green-600">¥{{ averageSalary }}</p>
- <p class="mt-1 text-sm text-gray-500 dark:text-gray-400">本月</p>
- </div>
- <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
- <h3 class="text-lg font-medium text-gray-900 dark:text-white">最高薪资</h3>
- <p class="mt-2 text-3xl font-bold text-yellow-600">¥{{ maxSalary }}</p>
- <p class="mt-1 text-sm text-gray-500 dark:text-gray-400">本月</p>
- </div>
- <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
- <h3 class="text-lg font-medium text-gray-900 dark:text-white">最低薪资</h3>
- <p class="mt-2 text-3xl font-bold text-red-600">¥{{ minSalary }}</p>
- <p class="mt-1 text-sm text-gray-500 dark:text-gray-400">本月</p>
- </div>
- </div>
- <!-- 搜索和筛选 -->
- <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 mb-6">
- <div class="grid grid-cols-1 md:grid-cols-4 gap-4">
- <div>
- <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">月份</label>
- <input
- type="month"
- v-model="filters.month"
- class="mt-1 block w-full border border-gray-300 dark:border-gray-600 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm dark:bg-gray-700 dark:text-white"
- />
- </div>
- <div>
- <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">部门</label>
- <select
- v-model="filters.department"
- class="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 dark:border-gray-600 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md dark:bg-gray-700 dark:text-white"
- >
- <option value="">所有部门</option>
- <option v-for="dept in departments" :key="dept.id" :value="dept.id">
- {{ dept.name }}
- </option>
- </select>
- </div>
- <div>
- <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">薪资范围</label>
- <div class="flex space-x-2">
- <input
- type="number"
- v-model="filters.minSalary"
- placeholder="最低"
- class="mt-1 block w-full border border-gray-300 dark:border-gray-600 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm dark:bg-gray-700 dark:text-white"
- />
- <input
- type="number"
- v-model="filters.maxSalary"
- placeholder="最高"
- class="mt-1 block w-full border border-gray-300 dark:border-gray-600 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm dark:bg-gray-700 dark:text-white"
- />
- </div>
- </div>
- <div>
- <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">搜索</label>
- <div class="mt-1 relative rounded-md shadow-sm">
- <input
- type="text"
- v-model="searchText"
- placeholder="搜索员工姓名..."
- class="block w-full pr-10 border-gray-300 dark:border-gray-600 rounded-md focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm dark:bg-gray-700 dark:text-white"
- />
- <div class="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
- <SearchIcon class="h-5 w-5 text-gray-400" />
- </div>
- </div>
- </div>
- </div>
- </div>
- <!-- 薪资列表 -->
- <div class="bg-white dark:bg-gray-800 rounded-lg shadow overflow-hidden">
- <table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
- <thead class="bg-gray-50 dark:bg-gray-700">
- <tr>
- <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">员工信息</th>
- <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">基本工资</th>
- <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">绩效奖金</th>
- <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">加班工资</th>
- <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">补贴</th>
- <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">扣款</th>
- <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">实发工资</th>
- <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">状态</th>
- <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">操作</th>
- </tr>
- </thead>
- <tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
- <tr v-for="salary in filteredSalaries" :key="salary.id">
- <td class="px-6 py-4 whitespace-nowrap">
- <div class="flex items-center">
- <div class="flex-shrink-0 h-10 w-10">
- <div class="h-10 w-10 rounded-full bg-gray-200 flex items-center justify-center">
- <UserIcon class="h-6 w-6 text-gray-500" />
- </div>
- </div>
- <div class="ml-4">
- <div class="text-sm font-medium text-gray-900 dark:text-white">{{ salary.employee.name }}</div>
- <div class="text-sm text-gray-500 dark:text-gray-400">{{ getDepartmentName(salary.employee.departmentId) }}</div>
- </div>
- </div>
- </td>
- <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
- ¥{{ salary.baseSalary }}
- </td>
- <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
- ¥{{ salary.bonus }}
- </td>
- <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
- ¥{{ salary.overtimePay }}
- </td>
- <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
- ¥{{ salary.allowance }}
- </td>
- <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
- ¥{{ salary.deduction }}
- </td>
- <td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-white">
- ¥{{ salary.netSalary }}
- </td>
- <td class="px-6 py-4 whitespace-nowrap">
- <span :class="getStatusClass(salary.status)" class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full">
- {{ getStatusText(salary.status) }}
- </span>
- </td>
- <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
- <button
- @click="handleView(salary)"
- class="text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300 mr-3"
- >
- 查看
- </button>
- <button
- @click="handleAdjust(salary)"
- class="text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300"
- >
- 调整
- </button>
- </td>
- </tr>
- </tbody>
- </table>
- </div>
- <!-- 薪资调整弹窗 -->
- <div v-if="showSalaryAdjustment" class="fixed inset-0 bg-gray-500 bg-opacity-75 flex items-center justify-center z-50">
- <div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-2xl w-full">
- <div class="p-6">
- <div class="flex justify-between items-center mb-6">
- <h3 class="text-lg font-medium text-gray-900 dark:text-white">薪资调整</h3>
- <button
- @click="showSalaryAdjustment = false"
- class="text-gray-400 hover:text-gray-500 dark:hover:text-gray-300"
- >
- <XIcon class="h-6 w-6" />
- </button>
- </div>
- <form @submit.prevent="handleSubmitAdjustment" class="space-y-6">
- <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
- <div>
- <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">员工</label>
- <select
- v-model="adjustmentForm.employeeId"
- class="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 dark:border-gray-600 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md dark:bg-gray-700 dark:text-white"
- >
- <option v-for="emp in employees" :key="emp.id" :value="emp.id">
- {{ emp.name }}
- </option>
- </select>
- </div>
- <div>
- <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">调整类型</label>
- <select
- v-model="adjustmentForm.type"
- class="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 dark:border-gray-600 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md dark:bg-gray-700 dark:text-white"
- >
- <option value="base">基本工资</option>
- <option value="bonus">绩效奖金</option>
- <option value="allowance">补贴</option>
- </select>
- </div>
- <div>
- <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">调整金额</label>
- <input
- type="number"
- v-model="adjustmentForm.amount"
- class="mt-1 block w-full border border-gray-300 dark:border-gray-600 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm dark:bg-gray-700 dark:text-white"
- />
- </div>
- <div>
- <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">生效日期</label>
- <input
- type="date"
- v-model="adjustmentForm.effectiveDate"
- class="mt-1 block w-full border border-gray-300 dark:border-gray-600 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm dark:bg-gray-700 dark:text-white"
- />
- </div>
- <div class="md:col-span-2">
- <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">调整原因</label>
- <textarea
- v-model="adjustmentForm.reason"
- rows="3"
- class="mt-1 block w-full border border-gray-300 dark:border-gray-600 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm dark:bg-gray-700 dark:text-white"
- ></textarea>
- </div>
- </div>
- <div class="flex justify-end space-x-3">
- <button
- type="button"
- @click="showSalaryAdjustment = false"
- class="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
- >
- 取消
- </button>
- <button
- type="submit"
- class="px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
- >
- 提交
- </button>
- </div>
- </form>
- </div>
- </div>
- </div>
- </div>
- </template>
- <script setup>
- import { ref, computed } from 'vue'
- import {
- SearchIcon,
- PlusIcon,
- DownloadIcon,
- XIcon,
- UserIcon
- } from 'lucide-vue-next'
- // 薪资数据
- const salaries = ref([
- {
- id: 'S202401010001',
- employee: {
- id: 'E001',
- name: '张三',
- departmentId: 'D001'
- },
- baseSalary: 10000,
- bonus: 2000,
- overtimePay: 1000,
- allowance: 500,
- deduction: 300,
- netSalary: 13200,
- status: 'paid',
- month: '2024-01'
- },
- {
- id: 'S202401010002',
- employee: {
- id: 'E002',
- name: '李四',
- departmentId: 'D002'
- },
- baseSalary: 8000,
- bonus: 1500,
- overtimePay: 800,
- allowance: 400,
- deduction: 200,
- netSalary: 10500,
- status: 'pending',
- month: '2024-01'
- },
- {
- id: 'S202401010003',
- employee: {
- id: 'E003',
- name: '王五',
- departmentId: 'D003'
- },
- baseSalary: 12000,
- bonus: 3000,
- overtimePay: 1500,
- allowance: 600,
- deduction: 400,
- netSalary: 16700,
- status: 'paid',
- month: '2024-01'
- },
- {
- id: 'S202401010004',
- employee: {
- id: 'E004',
- name: '赵六',
- departmentId: 'D001'
- },
- baseSalary: 9000,
- bonus: 1800,
- overtimePay: 900,
- allowance: 450,
- deduction: 250,
- netSalary: 11900,
- status: 'cancelled',
- month: '2024-01'
- }
- ])
- // 员工数据
- const employees = ref([
- {
- id: 'E001',
- name: '张三',
- departmentId: 'D001',
- position: '高级工程师',
- baseSalary: 10000
- },
- {
- id: 'E002',
- name: '李四',
- departmentId: 'D002',
- position: '市场经理',
- baseSalary: 8000
- },
- {
- id: 'E003',
- name: '王五',
- departmentId: 'D003',
- position: '人事总监',
- baseSalary: 12000
- },
- {
- id: 'E004',
- name: '赵六',
- departmentId: 'D001',
- position: '前端工程师',
- baseSalary: 9000
- }
- ])
- // 部门数据
- const departments = ref([
- { id: 'D001', name: '技术部' },
- { id: 'D002', name: '市场部' },
- { id: 'D003', name: '人事部' },
- { id: 'D004', name: '财务部' }
- ])
- // 搜索和筛选
- const searchText = ref('')
- const filters = ref({
- month: '2024-01',
- department: '',
- minSalary: '',
- maxSalary: ''
- })
- // 薪资调整表单
- const showSalaryAdjustment = ref(false)
- const adjustmentForm = ref({
- employeeId: '',
- type: 'base',
- amount: 0,
- effectiveDate: '',
- reason: ''
- })
- // 计算属性
- const filteredSalaries = computed(() => {
- return salaries.value.filter(salary => {
- const matchesSearch = !searchText.value ||
- salary.employee.name.toLowerCase().includes(searchText.value.toLowerCase())
- const matchesMonth = !filters.value.month || salary.month === filters.value.month
- const matchesDepartment = !filters.value.department || salary.employee.departmentId === filters.value.department
- const matchesSalary = (!filters.value.minSalary || salary.netSalary >= Number(filters.value.minSalary)) &&
- (!filters.value.maxSalary || salary.netSalary <= Number(filters.value.maxSalary))
- return matchesSearch && matchesMonth && matchesDepartment && matchesSalary
- })
- })
- const totalSalary = computed(() => {
- return filteredSalaries.value.reduce((sum, salary) => sum + salary.netSalary, 0)
- })
- const averageSalary = computed(() => {
- return filteredSalaries.value.length > 0
- ? Math.round(totalSalary.value / filteredSalaries.value.length)
- : 0
- })
- const maxSalary = computed(() => {
- return filteredSalaries.value.length > 0
- ? Math.max(...filteredSalaries.value.map(salary => salary.netSalary))
- : 0
- })
- const minSalary = computed(() => {
- return filteredSalaries.value.length > 0
- ? Math.min(...filteredSalaries.value.map(salary => salary.netSalary))
- : 0
- })
- // 方法
- const getDepartmentName = (id) => {
- const dept = departments.value.find(d => d.id === id)
- return dept ? dept.name : '未知部门'
- }
- const getStatusClass = (status) => {
- const classes = {
- paid: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200',
- pending: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200',
- cancelled: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200'
- }
- return classes[status] || 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200'
- }
- const getStatusText = (status) => {
- const texts = {
- paid: '已发放',
- pending: '待发放',
- cancelled: '已取消'
- }
- return texts[status] || '未知'
- }
- const handleView = (salary) => {
- // 实现查看薪资详情的逻辑
- console.log('查看薪资:', salary)
- // 这里可以实现查看薪资详情的逻辑,例如打开一个详情弹窗
- }
- const handleAdjust = (salary) => {
- // 设置调整表单的初始值
- adjustmentForm.value.employeeId = salary.employee.id
- // 打开薪资调整弹窗
- showSalaryAdjustment.value = true
- }
- const handleSubmitAdjustment = () => {
- // 实现提交薪资调整的逻辑
- console.log('提交薪资调整:', adjustmentForm.value)
- // 这里可以实现提交薪资调整的逻辑,例如发送请求到后端API
-
- // 关闭弹窗
- showSalaryAdjustment.value = false
-
- // 重置表单
- adjustmentForm.value = {
- employeeId: '',
- type: 'base',
- amount: 0,
- effectiveDate: '',
- reason: ''
- }
- }
- const handleSalaryPayment = () => {
- // 实现发放薪资的逻辑
- console.log('发放薪资')
- // 这里可以实现发放薪资的逻辑,例如将待发放的薪资状态更新为已发放
-
- // 模拟更新薪资状态
- salaries.value = salaries.value.map(salary => {
- if (salary.status === 'pending') {
- return { ...salary, status: 'paid' }
- }
- return salary
- })
- }
- </script>
|