SalaryManagementView.vue 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504
  1. <template>
  2. <div class="min-h-screen bg-gray-50 dark:bg-gray-900 p-6">
  3. <!-- 顶部导航栏 -->
  4. <div class="flex justify-between items-center mb-6">
  5. <h1 class="text-2xl font-bold text-gray-900 dark:text-white">薪资管理</h1>
  6. <div class="flex space-x-4">
  7. <button
  8. @click="showSalaryAdjustment = true"
  9. 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"
  10. >
  11. <PlusIcon class="h-5 w-5 inline-block mr-2" />
  12. 薪资调整
  13. </button>
  14. <button
  15. @click="handleSalaryPayment"
  16. 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"
  17. >
  18. <DownloadIcon class="h-5 w-5 inline-block mr-2" />
  19. 发放薪资
  20. </button>
  21. </div>
  22. </div>
  23. <!-- 薪资统计卡片 -->
  24. <div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-6">
  25. <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
  26. <h3 class="text-lg font-medium text-gray-900 dark:text-white">本月总薪资</h3>
  27. <p class="mt-2 text-3xl font-bold text-indigo-600">¥{{ totalSalary }}</p>
  28. <p class="mt-1 text-sm text-gray-500 dark:text-gray-400">本月</p>
  29. </div>
  30. <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
  31. <h3 class="text-lg font-medium text-gray-900 dark:text-white">平均薪资</h3>
  32. <p class="mt-2 text-3xl font-bold text-green-600">¥{{ averageSalary }}</p>
  33. <p class="mt-1 text-sm text-gray-500 dark:text-gray-400">本月</p>
  34. </div>
  35. <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
  36. <h3 class="text-lg font-medium text-gray-900 dark:text-white">最高薪资</h3>
  37. <p class="mt-2 text-3xl font-bold text-yellow-600">¥{{ maxSalary }}</p>
  38. <p class="mt-1 text-sm text-gray-500 dark:text-gray-400">本月</p>
  39. </div>
  40. <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
  41. <h3 class="text-lg font-medium text-gray-900 dark:text-white">最低薪资</h3>
  42. <p class="mt-2 text-3xl font-bold text-red-600">¥{{ minSalary }}</p>
  43. <p class="mt-1 text-sm text-gray-500 dark:text-gray-400">本月</p>
  44. </div>
  45. </div>
  46. <!-- 搜索和筛选 -->
  47. <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 mb-6">
  48. <div class="grid grid-cols-1 md:grid-cols-4 gap-4">
  49. <div>
  50. <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">月份</label>
  51. <input
  52. type="month"
  53. v-model="filters.month"
  54. 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"
  55. />
  56. </div>
  57. <div>
  58. <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">部门</label>
  59. <select
  60. v-model="filters.department"
  61. 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"
  62. >
  63. <option value="">所有部门</option>
  64. <option v-for="dept in departments" :key="dept.id" :value="dept.id">
  65. {{ dept.name }}
  66. </option>
  67. </select>
  68. </div>
  69. <div>
  70. <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">薪资范围</label>
  71. <div class="flex space-x-2">
  72. <input
  73. type="number"
  74. v-model="filters.minSalary"
  75. placeholder="最低"
  76. 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"
  77. />
  78. <input
  79. type="number"
  80. v-model="filters.maxSalary"
  81. placeholder="最高"
  82. 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"
  83. />
  84. </div>
  85. </div>
  86. <div>
  87. <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">搜索</label>
  88. <div class="mt-1 relative rounded-md shadow-sm">
  89. <input
  90. type="text"
  91. v-model="searchText"
  92. placeholder="搜索员工姓名..."
  93. 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"
  94. />
  95. <div class="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
  96. <SearchIcon class="h-5 w-5 text-gray-400" />
  97. </div>
  98. </div>
  99. </div>
  100. </div>
  101. </div>
  102. <!-- 薪资列表 -->
  103. <div class="bg-white dark:bg-gray-800 rounded-lg shadow overflow-hidden">
  104. <table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
  105. <thead class="bg-gray-50 dark:bg-gray-700">
  106. <tr>
  107. <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">员工信息</th>
  108. <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">基本工资</th>
  109. <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">绩效奖金</th>
  110. <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">加班工资</th>
  111. <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">补贴</th>
  112. <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">扣款</th>
  113. <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">实发工资</th>
  114. <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">状态</th>
  115. <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">操作</th>
  116. </tr>
  117. </thead>
  118. <tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
  119. <tr v-for="salary in filteredSalaries" :key="salary.id">
  120. <td class="px-6 py-4 whitespace-nowrap">
  121. <div class="flex items-center">
  122. <div class="flex-shrink-0 h-10 w-10">
  123. <div class="h-10 w-10 rounded-full bg-gray-200 flex items-center justify-center">
  124. <UserIcon class="h-6 w-6 text-gray-500" />
  125. </div>
  126. </div>
  127. <div class="ml-4">
  128. <div class="text-sm font-medium text-gray-900 dark:text-white">{{ salary.employee.name }}</div>
  129. <div class="text-sm text-gray-500 dark:text-gray-400">{{ getDepartmentName(salary.employee.departmentId) }}</div>
  130. </div>
  131. </div>
  132. </td>
  133. <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
  134. ¥{{ salary.baseSalary }}
  135. </td>
  136. <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
  137. ¥{{ salary.bonus }}
  138. </td>
  139. <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
  140. ¥{{ salary.overtimePay }}
  141. </td>
  142. <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
  143. ¥{{ salary.allowance }}
  144. </td>
  145. <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
  146. ¥{{ salary.deduction }}
  147. </td>
  148. <td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-white">
  149. ¥{{ salary.netSalary }}
  150. </td>
  151. <td class="px-6 py-4 whitespace-nowrap">
  152. <span :class="getStatusClass(salary.status)" class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full">
  153. {{ getStatusText(salary.status) }}
  154. </span>
  155. </td>
  156. <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
  157. <button
  158. @click="handleView(salary)"
  159. class="text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300 mr-3"
  160. >
  161. 查看
  162. </button>
  163. <button
  164. @click="handleAdjust(salary)"
  165. class="text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300"
  166. >
  167. 调整
  168. </button>
  169. </td>
  170. </tr>
  171. </tbody>
  172. </table>
  173. </div>
  174. <!-- 薪资调整弹窗 -->
  175. <div v-if="showSalaryAdjustment" class="fixed inset-0 bg-gray-500 bg-opacity-75 flex items-center justify-center z-50">
  176. <div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-2xl w-full">
  177. <div class="p-6">
  178. <div class="flex justify-between items-center mb-6">
  179. <h3 class="text-lg font-medium text-gray-900 dark:text-white">薪资调整</h3>
  180. <button
  181. @click="showSalaryAdjustment = false"
  182. class="text-gray-400 hover:text-gray-500 dark:hover:text-gray-300"
  183. >
  184. <XIcon class="h-6 w-6" />
  185. </button>
  186. </div>
  187. <form @submit.prevent="handleSubmitAdjustment" class="space-y-6">
  188. <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
  189. <div>
  190. <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">员工</label>
  191. <select
  192. v-model="adjustmentForm.employeeId"
  193. 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"
  194. >
  195. <option v-for="emp in employees" :key="emp.id" :value="emp.id">
  196. {{ emp.name }}
  197. </option>
  198. </select>
  199. </div>
  200. <div>
  201. <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">调整类型</label>
  202. <select
  203. v-model="adjustmentForm.type"
  204. 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"
  205. >
  206. <option value="base">基本工资</option>
  207. <option value="bonus">绩效奖金</option>
  208. <option value="allowance">补贴</option>
  209. </select>
  210. </div>
  211. <div>
  212. <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">调整金额</label>
  213. <input
  214. type="number"
  215. v-model="adjustmentForm.amount"
  216. 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"
  217. />
  218. </div>
  219. <div>
  220. <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">生效日期</label>
  221. <input
  222. type="date"
  223. v-model="adjustmentForm.effectiveDate"
  224. 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"
  225. />
  226. </div>
  227. <div class="md:col-span-2">
  228. <label class="block text-sm font-medium text-gray-700 dark:text-gray-300">调整原因</label>
  229. <textarea
  230. v-model="adjustmentForm.reason"
  231. rows="3"
  232. 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"
  233. ></textarea>
  234. </div>
  235. </div>
  236. <div class="flex justify-end space-x-3">
  237. <button
  238. type="button"
  239. @click="showSalaryAdjustment = false"
  240. 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"
  241. >
  242. 取消
  243. </button>
  244. <button
  245. type="submit"
  246. 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"
  247. >
  248. 提交
  249. </button>
  250. </div>
  251. </form>
  252. </div>
  253. </div>
  254. </div>
  255. </div>
  256. </template>
  257. <script setup>
  258. import { ref, computed } from 'vue'
  259. import {
  260. SearchIcon,
  261. PlusIcon,
  262. DownloadIcon,
  263. XIcon,
  264. UserIcon
  265. } from 'lucide-vue-next'
  266. // 薪资数据
  267. const salaries = ref([
  268. {
  269. id: 'S202401010001',
  270. employee: {
  271. id: 'E001',
  272. name: '张三',
  273. departmentId: 'D001'
  274. },
  275. baseSalary: 10000,
  276. bonus: 2000,
  277. overtimePay: 1000,
  278. allowance: 500,
  279. deduction: 300,
  280. netSalary: 13200,
  281. status: 'paid',
  282. month: '2024-01'
  283. },
  284. {
  285. id: 'S202401010002',
  286. employee: {
  287. id: 'E002',
  288. name: '李四',
  289. departmentId: 'D002'
  290. },
  291. baseSalary: 8000,
  292. bonus: 1500,
  293. overtimePay: 800,
  294. allowance: 400,
  295. deduction: 200,
  296. netSalary: 10500,
  297. status: 'pending',
  298. month: '2024-01'
  299. },
  300. {
  301. id: 'S202401010003',
  302. employee: {
  303. id: 'E003',
  304. name: '王五',
  305. departmentId: 'D003'
  306. },
  307. baseSalary: 12000,
  308. bonus: 3000,
  309. overtimePay: 1500,
  310. allowance: 600,
  311. deduction: 400,
  312. netSalary: 16700,
  313. status: 'paid',
  314. month: '2024-01'
  315. },
  316. {
  317. id: 'S202401010004',
  318. employee: {
  319. id: 'E004',
  320. name: '赵六',
  321. departmentId: 'D001'
  322. },
  323. baseSalary: 9000,
  324. bonus: 1800,
  325. overtimePay: 900,
  326. allowance: 450,
  327. deduction: 250,
  328. netSalary: 11900,
  329. status: 'cancelled',
  330. month: '2024-01'
  331. }
  332. ])
  333. // 员工数据
  334. const employees = ref([
  335. {
  336. id: 'E001',
  337. name: '张三',
  338. departmentId: 'D001',
  339. position: '高级工程师',
  340. baseSalary: 10000
  341. },
  342. {
  343. id: 'E002',
  344. name: '李四',
  345. departmentId: 'D002',
  346. position: '市场经理',
  347. baseSalary: 8000
  348. },
  349. {
  350. id: 'E003',
  351. name: '王五',
  352. departmentId: 'D003',
  353. position: '人事总监',
  354. baseSalary: 12000
  355. },
  356. {
  357. id: 'E004',
  358. name: '赵六',
  359. departmentId: 'D001',
  360. position: '前端工程师',
  361. baseSalary: 9000
  362. }
  363. ])
  364. // 部门数据
  365. const departments = ref([
  366. { id: 'D001', name: '技术部' },
  367. { id: 'D002', name: '市场部' },
  368. { id: 'D003', name: '人事部' },
  369. { id: 'D004', name: '财务部' }
  370. ])
  371. // 搜索和筛选
  372. const searchText = ref('')
  373. const filters = ref({
  374. month: '2024-01',
  375. department: '',
  376. minSalary: '',
  377. maxSalary: ''
  378. })
  379. // 薪资调整表单
  380. const showSalaryAdjustment = ref(false)
  381. const adjustmentForm = ref({
  382. employeeId: '',
  383. type: 'base',
  384. amount: 0,
  385. effectiveDate: '',
  386. reason: ''
  387. })
  388. // 计算属性
  389. const filteredSalaries = computed(() => {
  390. return salaries.value.filter(salary => {
  391. const matchesSearch = !searchText.value ||
  392. salary.employee.name.toLowerCase().includes(searchText.value.toLowerCase())
  393. const matchesMonth = !filters.value.month || salary.month === filters.value.month
  394. const matchesDepartment = !filters.value.department || salary.employee.departmentId === filters.value.department
  395. const matchesSalary = (!filters.value.minSalary || salary.netSalary >= Number(filters.value.minSalary)) &&
  396. (!filters.value.maxSalary || salary.netSalary <= Number(filters.value.maxSalary))
  397. return matchesSearch && matchesMonth && matchesDepartment && matchesSalary
  398. })
  399. })
  400. const totalSalary = computed(() => {
  401. return filteredSalaries.value.reduce((sum, salary) => sum + salary.netSalary, 0)
  402. })
  403. const averageSalary = computed(() => {
  404. return filteredSalaries.value.length > 0
  405. ? Math.round(totalSalary.value / filteredSalaries.value.length)
  406. : 0
  407. })
  408. const maxSalary = computed(() => {
  409. return filteredSalaries.value.length > 0
  410. ? Math.max(...filteredSalaries.value.map(salary => salary.netSalary))
  411. : 0
  412. })
  413. const minSalary = computed(() => {
  414. return filteredSalaries.value.length > 0
  415. ? Math.min(...filteredSalaries.value.map(salary => salary.netSalary))
  416. : 0
  417. })
  418. // 方法
  419. const getDepartmentName = (id) => {
  420. const dept = departments.value.find(d => d.id === id)
  421. return dept ? dept.name : '未知部门'
  422. }
  423. const getStatusClass = (status) => {
  424. const classes = {
  425. paid: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200',
  426. pending: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200',
  427. cancelled: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200'
  428. }
  429. return classes[status] || 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200'
  430. }
  431. const getStatusText = (status) => {
  432. const texts = {
  433. paid: '已发放',
  434. pending: '待发放',
  435. cancelled: '已取消'
  436. }
  437. return texts[status] || '未知'
  438. }
  439. const handleView = (salary) => {
  440. // 实现查看薪资详情的逻辑
  441. console.log('查看薪资:', salary)
  442. // 这里可以实现查看薪资详情的逻辑,例如打开一个详情弹窗
  443. }
  444. const handleAdjust = (salary) => {
  445. // 设置调整表单的初始值
  446. adjustmentForm.value.employeeId = salary.employee.id
  447. // 打开薪资调整弹窗
  448. showSalaryAdjustment.value = true
  449. }
  450. const handleSubmitAdjustment = () => {
  451. // 实现提交薪资调整的逻辑
  452. console.log('提交薪资调整:', adjustmentForm.value)
  453. // 这里可以实现提交薪资调整的逻辑,例如发送请求到后端API
  454. // 关闭弹窗
  455. showSalaryAdjustment.value = false
  456. // 重置表单
  457. adjustmentForm.value = {
  458. employeeId: '',
  459. type: 'base',
  460. amount: 0,
  461. effectiveDate: '',
  462. reason: ''
  463. }
  464. }
  465. const handleSalaryPayment = () => {
  466. // 实现发放薪资的逻辑
  467. console.log('发放薪资')
  468. // 这里可以实现发放薪资的逻辑,例如将待发放的薪资状态更新为已发放
  469. // 模拟更新薪资状态
  470. salaries.value = salaries.value.map(salary => {
  471. if (salary.status === 'pending') {
  472. return { ...salary, status: 'paid' }
  473. }
  474. return salary
  475. })
  476. }
  477. </script>