SalaryView.vue 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. <template>
  2. <div>
  3. <a-card>
  4. <a-space>
  5. <a-button type="primary" @click="showGenerateModal">
  6. 生成工资
  7. </a-button>
  8. <a-button @click="loadSalaryList">刷新</a-button>
  9. </a-space>
  10. </a-card>
  11. <a-card class="mt-4" title="工资周期列表">
  12. <a-table
  13. :columns="columns"
  14. :dataSource="salaryPeriods"
  15. :loading="loading"
  16. rowKey="period"
  17. >
  18. <template #status="{ record }">
  19. <a-tag :color="record.status === 'locked' ? 'red' : 'green'">
  20. {{ record.status === 'locked' ? '已锁定' : '待锁定' }}
  21. </a-tag>
  22. </template>
  23. <template #action="{ record }">
  24. <a-space>
  25. <a-button size="small" @click="showDetailModal(record.period)">
  26. 查看明细
  27. </a-button>
  28. <a-button
  29. v-if="record.status === 'pending'"
  30. size="small"
  31. type="primary"
  32. @click="lockSalary(record.period)"
  33. >
  34. 锁定
  35. </a-button>
  36. <a-button
  37. v-else
  38. size="small"
  39. @click="unlockSalary(record.period)"
  40. >
  41. 解锁
  42. </a-button>
  43. <a-popconfirm
  44. title="确定删除此工资周期吗?"
  45. @confirm="deleteSalary(record.period)"
  46. >
  47. <a-button size="small" danger>删除</a-button>
  48. </a-popconfirm>
  49. </a-space>
  50. </template>
  51. </a-table>
  52. </a-card>
  53. <!-- 生成工资模态框 -->
  54. <a-modal
  55. v-model:open="generateModalVisible"
  56. title="生成工资"
  57. @ok="handleGenerate"
  58. :confirmLoading="generating"
  59. >
  60. <a-form layout="vertical">
  61. <a-form-item label="工资周期" required>
  62. <a-month-picker
  63. v-model:value="generatePeriod"
  64. placeholder="请选择工资周期"
  65. style="width: 100%"
  66. />
  67. </a-form-item>
  68. </a-form>
  69. </a-modal>
  70. <!-- 工资明细模态框 -->
  71. <a-modal
  72. v-model:open="detailModalVisible"
  73. :title="`工资明细 - ${currentPeriod}`"
  74. width="800px"
  75. :footer="null"
  76. >
  77. <a-table
  78. :columns="detailColumns"
  79. :dataSource="salaryDetails"
  80. :loading="detailLoading"
  81. rowKey="user_id"
  82. :pagination="false"
  83. >
  84. <template #amount="{ record }">
  85. ¥{{ record.total_amount.toFixed(2) }}
  86. </template>
  87. </a-table>
  88. </a-modal>
  89. </div>
  90. </template>
  91. <script setup lang="ts">
  92. import { ref, onMounted } from 'vue';
  93. import { message } from 'ant-design-vue';
  94. import dayjs from 'dayjs';
  95. import { SalaryApi, createApiClient } from '@smartcut/api-client';
  96. import { STORAGE_KEYS } from '@smartcut/shared-utils';
  97. import { getApiBaseUrl } from '@/apiConfig';
  98. import type { SalaryPeriod, SalaryDetailItem } from '@smartcut/types';
  99. const apiClient = createApiClient({
  100. baseURL: getApiBaseUrl(),
  101. tokenKey: STORAGE_KEYS.FACTORY_TOKEN
  102. });
  103. const salaryApi = new SalaryApi(apiClient);
  104. const salaryPeriods = ref<SalaryPeriod[]>([]);
  105. const loading = ref(false);
  106. const columns = [
  107. { title: '工资周期', dataIndex: 'period', key: 'period', width: 150 },
  108. { title: '状态', dataIndex: 'status', key: 'status', width: 120, slots: { customRender: 'status' } },
  109. { title: '创建时间', dataIndex: 'created_at', key: 'created_at', width: 200 },
  110. { title: '操作', key: 'action', width: 300, slots: { customRender: 'action' } }
  111. ];
  112. const detailColumns = [
  113. { title: '员工', dataIndex: 'user_name', key: 'user_name', width: 150 },
  114. { title: '总数量', dataIndex: 'total_quantity', key: 'total_quantity', width: 120 },
  115. { title: '记录数', dataIndex: 'records_count', key: 'records_count', width: 100 },
  116. { title: '工资金额', key: 'amount', width: 150, slots: { customRender: 'amount' } }
  117. ];
  118. const generateModalVisible = ref(false);
  119. const generating = ref(false);
  120. const generatePeriod = ref<dayjs.Dayjs | null>(null);
  121. const detailModalVisible = ref(false);
  122. const currentPeriod = ref('');
  123. const salaryDetails = ref<SalaryDetailItem[]>([]);
  124. const detailLoading = ref(false);
  125. async function loadSalaryList() {
  126. loading.value = true;
  127. try {
  128. salaryPeriods.value = await salaryApi.getSalaryList();
  129. } catch (error) {
  130. message.error('加载工资列表失败');
  131. } finally {
  132. loading.value = false;
  133. }
  134. }
  135. function showGenerateModal() {
  136. generateModalVisible.value = true;
  137. generatePeriod.value = dayjs();
  138. }
  139. async function handleGenerate() {
  140. if (!generatePeriod.value) {
  141. message.error('请选择工资周期');
  142. return;
  143. }
  144. generating.value = true;
  145. try {
  146. const period = generatePeriod.value.format('YYYY-MM');
  147. await salaryApi.generateSalary({ period });
  148. message.success('工资已生成');
  149. generateModalVisible.value = false;
  150. loadSalaryList();
  151. } catch (error: any) {
  152. message.error(error.response?.data?.msg || '生成工资失败');
  153. } finally {
  154. generating.value = false;
  155. }
  156. }
  157. async function showDetailModal(period: string) {
  158. currentPeriod.value = period;
  159. detailModalVisible.value = true;
  160. detailLoading.value = true;
  161. try {
  162. salaryDetails.value = await salaryApi.getSalaryDetail(period);
  163. } catch (error) {
  164. message.error('加载工资明细失败');
  165. } finally {
  166. detailLoading.value = false;
  167. }
  168. }
  169. async function lockSalary(period: string) {
  170. try {
  171. await salaryApi.lockSalary(period);
  172. message.success('工资已锁定');
  173. loadSalaryList();
  174. } catch (error) {
  175. message.error('锁定失败');
  176. }
  177. }
  178. async function unlockSalary(period: string) {
  179. try {
  180. await salaryApi.unlockSalary(period);
  181. message.success('工资已解锁');
  182. loadSalaryList();
  183. } catch (error) {
  184. message.error('解锁失败');
  185. }
  186. }
  187. async function deleteSalary(period: string) {
  188. try {
  189. await salaryApi.deleteSalary(period);
  190. message.success('工资周期已删除');
  191. loadSalaryList();
  192. } catch (error) {
  193. message.error('删除失败');
  194. }
  195. }
  196. onMounted(() => {
  197. loadSalaryList();
  198. });
  199. </script>