银川
1.员工管理页面开发、联调
2.薪资发放页面开发联调
3.薪酬管理页面开发联调
4.添加请假扣薪管理页面开发联调
已添加5个文件
已修改11个文件
809 ■■■■ 文件已修改
src/api/hrm/employee/index.ts 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/hrm/leave-type-config/index.ts 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/hrm/salary/calculation/index.ts 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/hrm/salary/payment/index.ts 78 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/employee/data.ts 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/employee/index.vue 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/leave/data.ts 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/leave/index.vue 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/salary/calculation/data.ts 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/salary/calculation/index.vue 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/salary/leave-type-config/data.ts 101 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/salary/leave-type-config/index.vue 84 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/salary/leave-type-config/modules/form.vue 67 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/salary/payment/data.ts 61 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/salary/payment/index.vue 135 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/salary/payment/modules/detail.vue 142 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/hrm/employee/index.ts
@@ -124,4 +124,19 @@
/** å¯¼å‡ºå‘˜å·¥ */
export function exportEmployee(params: any) {
  return requestClient.download('/hrm/employee/export-excel', { params });
}
/** åˆ é™¤æ•™è‚²ç»åކ */
export function deleteEducation(id: number) {
  return requestClient.delete(`/hrm/employee/education/delete?id=${id}`);
}
/** åˆ é™¤å·¥ä½œç»åކ */
export function deleteWorkHistory(id: number) {
  return requestClient.delete(`/hrm/employee/work-history/delete?id=${id}`);
}
/** åˆ é™¤ç´§æ€¥è”系人 */
export function deleteEmergencyContact(id: number) {
  return requestClient.delete(`/hrm/employee/emergency-contact/delete?id=${id}`);
}
src/api/hrm/leave-type-config/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,28 @@
import type { PageResult } from '#/packages/effects/request/src';
import { requestClient } from '#/api/request';
export namespace HrmLeaveTypeConfigApi {
  /** è¯·å‡ç±»åž‹æ‰£æ¬¾é…ç½® */
  export interface LeaveTypeConfig {
    id?: number;
    leaveType?: number;
    name?: string;
    deductionRatio?: number;
    remark?: string;
    status?: number;
    createTime?: string;
  }
}
/** èŽ·å–è¯·å‡ç±»åž‹é…ç½®åˆ—è¡¨ */
export function getLeaveTypeConfigList() {
  return requestClient.get<HrmLeaveTypeConfigApi.LeaveTypeConfig[]>(
    '/hrm/leave-type-config/list',
  );
}
/** æ›´æ–°è¯·å‡ç±»åž‹é…ç½® */
export function updateLeaveTypeConfig(data: HrmLeaveTypeConfigApi.LeaveTypeConfig) {
  return requestClient.put('/hrm/leave-type-config/update', data);
}
src/api/hrm/salary/calculation/index.ts
@@ -22,6 +22,7 @@
    housingFundDeduction?: number;
    taxDeduction?: number;
    otherDeduction?: number;
    leaveDeduction?: number;
    actualSalary?: number;
    workDays?: number;
    overtimeHours?: number;
@@ -103,6 +104,16 @@
  return requestClient.post('/hrm/salary/calculation/confirm', { ids });
}
/** æ’¤é”€è–ªé…¬æ ¸ç®—确认 */
export function revokeSalaryCalculation(ids: number[]) {
  return requestClient.post('/hrm/salary/calculation/revoke', { ids });
}
/** åˆ é™¤è–ªé…¬æ ¸ç®—记录 */
export function deleteSalaryCalculation(id: number) {
  return requestClient.delete(`/hrm/salary/calculation/delete?id=${id}`);
}
/** å¯¼å‡ºè–ªé…¬æ ¸ç®—表 */
export function exportSalaryCalculation(params: any) {
  return requestClient.download('/hrm/salary/calculation/export-excel', { params });
src/api/hrm/salary/payment/index.ts
@@ -3,20 +3,44 @@
import { requestClient } from '#/api/request';
export namespace HrmSalaryPaymentApi {
  /** è–ªèµ„发放记录 */
  /** è–ªèµ„发放台账 */
  export interface SalaryPayment {
    id?: number;
    no?: string;
    period?: string;
    totalAmount?: number;
    totalCount?: number;
    paymentMethod?: number;
    paymentTime?: string;
    status?: number;
    fileUrl?: string;
    remark?: string;
    createTime?: string;
  }
  /** è–ªèµ„发放明细 */
  export interface SalaryPaymentDetail {
    id?: number;
    paymentId?: number;
    calculationId?: number;
    userId?: number;
    userName?: string;
    deptId?: number;
    deptName?: string;
    baseSalary?: number;
    performanceSalary?: number;
    overtimePay?: number;
    mealAllowance?: number;
    transportAllowance?: number;
    otherAllowance?: number;
    socialSecurityDeduction?: number;
    housingFundDeduction?: number;
    taxDeduction?: number;
    otherDeduction?: number;
    leaveDeduction?: number;
    actualSalary?: number;
    paymentMethod?: number;
    paymentTime?: string;
    paymentNo?: string;
    status?: number;
    paymentTime?: string;
    remark?: string;
    createTime?: string;
  }
@@ -24,13 +48,19 @@
  /** è–ªèµ„发放查询参数 */
  export interface SalaryPaymentPageParam extends PageParam {
    period?: string;
    status?: number;
  }
  /** è–ªèµ„发放明细查询参数 */
  export interface SalaryPaymentDetailPageParam extends PageParam {
    paymentId?: number;
    userId?: number;
    deptId?: number;
    status?: number;
  }
}
/** æŸ¥è¯¢è–ªèµ„发放分页列表 */
/** æŸ¥è¯¢è–ªèµ„发放台账分页列表 */
export function getSalaryPaymentPage(
  params: HrmSalaryPaymentApi.SalaryPaymentPageParam,
) {
@@ -40,21 +70,45 @@
  );
}
/** æŸ¥è¯¢è–ªèµ„发放详情 */
/** æŸ¥è¯¢è–ªèµ„发放台账详情 */
export function getSalaryPayment(id: number) {
  return requestClient.get<HrmSalaryPaymentApi.SalaryPayment>(
    `/hrm/salary/payment/get?id=${id}`,
  );
}
/** æ‰§è¡Œè–ªèµ„发放 */
export function paySalary(ids: number[]) {
  return requestClient.post('/hrm/salary/payment/pay', { ids });
/** æŸ¥è¯¢è–ªèµ„发放明细列表 */
export function getSalaryPaymentDetailList(paymentId: number) {
  return requestClient.get<HrmSalaryPaymentApi.SalaryPaymentDetail[]>(
    `/hrm/salary/payment/detail/list?paymentId=${paymentId}`,
  );
}
/** æ’¤é”€è–ªèµ„发放 */
export function revokeSalaryPayment(id: number) {
  return requestClient.post(`/hrm/salary/payment/revoke?id=${id}`);
/** ç”Ÿæˆè–ªèµ„发放台账 */
export function generateSalaryPayment(period: string) {
  return requestClient.post<number>(
    `/hrm/salary/payment/generate?period=${period}`,
  );
}
/** å‘放薪资(整个台账) */
export function paySalary(id: number) {
  return requestClient.post(`/hrm/salary/payment/pay?id=${id}`);
}
/** æ‰¹é‡å‘放薪资明细 */
export function paySalaryDetail(detailIds: number[]) {
  return requestClient.post('/hrm/salary/payment/pay-detail', { detailIds });
}
/** æ’¤é”€è–ªèµ„发放明细 */
export function revokeSalaryPaymentDetail(detailIds: number[]) {
  return requestClient.post('/hrm/salary/payment/revoke-detail', { detailIds });
}
/** åˆ é™¤è–ªèµ„发放台账 */
export function deleteSalaryPayment(id: number) {
  return requestClient.delete(`/hrm/salary/payment/delete?id=${id}`);
}
/** å¯¼å‡ºè–ªèµ„发放表 */
src/views/hrm/employee/data.ts
@@ -304,9 +304,8 @@
      field: 'gender',
      title: '性别',
      width: 80,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.HRM_GENDER },
      slots: {
        default: 'gender',
      },
    },
    { field: 'phone', title: '手机号码', minWidth: 120 },
@@ -316,9 +315,8 @@
      field: 'employeeStatus',
      title: '员工状态',
      width: 100,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.HRM_EMPLOYEE_STATUS },
      slots: {
        default: 'employeeStatus',
      },
    },
    { field: 'hireDate', title: '入职日期', width: 110 },
src/views/hrm/employee/index.vue
@@ -5,7 +5,10 @@
import { Page, useVbenModal } from '#/packages/effects/common-ui/src';
import { downloadFileFromBlobPart } from '#/packages/utils/src';
import { Button, message } from 'ant-design-vue';
import { Button, message, Tag } from 'ant-design-vue';
import { DICT_TYPE } from '#/packages/constants/src';
import { getDictLabel } from '#/packages/effects/hooks/src';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { deleteEmployee, exportEmployee, getEmployeePage } from '#/api/hrm/employee';
@@ -117,6 +120,16 @@
      <template #employeeNo="{ row }">
        <Button type="link" @click="handleDetail(row)">{{ row.employeeNo }}</Button>
      </template>
      <template #gender="{ row }">
        <Tag :color="row.gender === 1 ? 'blue' : 'pink'">
          {{ getDictLabel(DICT_TYPE.HRM_GENDER, row.gender) }}
        </Tag>
      </template>
      <template #employeeStatus="{ row }">
        <Tag :color="row.employeeStatus === 1 ? 'success' : row.employeeStatus === 2 ? 'processing' : 'default'">
          {{ getDictLabel(DICT_TYPE.HRM_EMPLOYEE_STATUS, row.employeeStatus) }}
        </Tag>
      </template>
      <template #actions="{ row }">
        <TableAction
          :actions="[
src/views/hrm/leave/data.ts
@@ -138,9 +138,8 @@
      field: 'leaveType',
      title: '请假类型',
      width: 100,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.HRM_LEAVE_TYPE },
      slots: {
        default: 'leaveType',
      },
    },
    { field: 'startTime', title: '开始时间', width: 160 },
src/views/hrm/leave/index.vue
@@ -4,7 +4,10 @@
import { Page, useVbenModal } from '#/packages/effects/common-ui/src';
import { message } from 'ant-design-vue';
import { message, Tag } from 'ant-design-vue';
import { DICT_TYPE } from '#/packages/constants/src';
import { getDictLabel } from '#/packages/effects/hooks/src';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
@@ -72,6 +75,20 @@
  handleRefresh();
}
/** è¯·å‡ç±»åž‹é¢œè‰² */
function getLeaveTypeColor(type: number): string {
  switch (type) {
    case 1: return 'green';      // å¹´å‡
    case 2: return 'orange';     // äº‹å‡
    case 3: return 'red';        // ç—…假
    case 4: return 'purple';     // å©šå‡
    case 5: return 'cyan';       // äº§å‡
    case 6: return 'magenta';    // ä¸§å‡
    case 7: return 'geekblue';   // è°ƒä¼‘
    default: return 'default';
  }
}
const [Grid, gridApi] = useVbenVxeGrid({
  formOptions: {
    schema: useGridFormSchema(),
@@ -119,6 +136,11 @@
          ]"
        />
      </template>
      <template #leaveType="{ row }">
        <Tag :color="getLeaveTypeColor(row.leaveType)">
          {{ getDictLabel(DICT_TYPE.HRM_LEAVE_TYPE, row.leaveType) }}
        </Tag>
      </template>
      <template #actions="{ row }">
        <TableAction
          :actions="[
src/views/hrm/salary/calculation/data.ts
@@ -85,6 +85,7 @@
    { field: 'housingFundDeduction', title: '公积金扣款', width: 100, formatter: 'formatAmount2' },
    { field: 'taxDeduction', title: '个税扣款', width: 100, formatter: 'formatAmount2' },
    { field: 'otherDeduction', title: '其他扣款', width: 80, formatter: 'formatAmount2' },
    { field: 'leaveDeduction', title: '请假扣款', width: 100, formatter: 'formatAmount2' },
    { field: 'actualSalary', title: '实发工资', width: 100, formatter: 'formatAmount2' },
    { field: 'workDays', title: '出勤天数', width: 80 },
    { field: 'overtimeHours', title: '加班时长(h)', width: 100 },
@@ -92,9 +93,9 @@
      field: 'status',
      title: '状态',
      width: 100,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.HRM_SALARY_STATUS },
      fixed: 'right',
      slots: {
        default: 'status',
      },
    },
    {
@@ -123,8 +124,9 @@
    { title: '公积金扣款', dataIndex: 'housingFundDeduction', width: 110 },
    { title: '个税扣款', dataIndex: 'taxDeduction', width: 110 },
    { title: '其他扣款', dataIndex: 'otherDeduction', width: 100 },
    { title: '实发工资', dataIndex: 'actualSalary', width: 100, fixed: 'right' },
    { title: '请假扣款', dataIndex: 'leaveDeduction', width: 110 },
    { title: '出勤天数', dataIndex: 'workDays', width: 90 },
    { title: '加班时长', dataIndex: 'overtimeHours', width: 100 },
    { title: '实发工资', dataIndex: 'actualSalary', width: 100, fixed: 'right' },
  ];
}
src/views/hrm/salary/calculation/index.vue
@@ -7,7 +7,10 @@
import { Page } from '#/packages/effects/common-ui/src';
import { downloadFileFromBlobPart } from '#/packages/utils/src';
import { message, Modal, Form, FormItem, DatePicker, TreeSelect, Table, InputNumber, Alert } from 'ant-design-vue';
import { message, Modal, Form, FormItem, DatePicker, TreeSelect, Table, InputNumber, Alert, Tag } from 'ant-design-vue';
import { DICT_TYPE } from '#/packages/constants/src';
import { getDictLabel } from '#/packages/effects/hooks/src';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
@@ -102,7 +105,8 @@
  const deduction = (record.socialSecurityDeduction || 0) +
    (record.housingFundDeduction || 0) +
    (record.taxDeduction || 0) +
    (record.otherDeduction || 0);
    (record.otherDeduction || 0) +
    (record.leaveDeduction || 0);
  return income - deduction;
}
@@ -365,6 +369,16 @@
              @change="(val: number) => updateFieldValue(record, 'otherDeduction', val)"
            />
          </template>
          <template v-else-if="column.dataIndex === 'leaveDeduction'">
            <InputNumber
              v-model:value="record.leaveDeduction"
              :min="0"
              :precision="2"
              size="small"
              style="width: 90px"
              @change="(val: number) => updateFieldValue(record, 'leaveDeduction', val)"
            />
          </template>
          <template v-else-if="column.dataIndex === 'actualSalary'">
            <span class="font-bold text-green-600">{{ record.actualSalary?.toFixed(2) }}</span>
          </template>
@@ -425,6 +439,11 @@
          ]"
        />
      </template>
      <template #status="{ row }">
        <Tag :color="row.status === 0 ? 'warning' : row.status === 10 ? 'success' : 'processing'">
          {{ getDictLabel(DICT_TYPE.HRM_SALARY_STATUS, row.status) }}
        </Tag>
      </template>
      <template #actions="{ row }">
        <TableAction
          :actions="[
src/views/hrm/salary/leave-type-config/data.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,101 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { HrmLeaveTypeConfigApi } from '#/api/hrm/leave-type-config';
/** è¡¨å•类型 */
export type FormType = 'update';
/** æ–°å¢ž/修改请假扣款配置的表单 */
export function useFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'id',
      component: 'Input',
      dependencies: {
        triggerFields: [''],
        show: () => false,
      },
    },
    {
      fieldName: 'name',
      label: '请假类型',
      component: 'Input',
      componentProps: {
        disabled: true,
        placeholder: '',
      },
      formItemClass: 'col-span-2',
    },
    {
      fieldName: 'deductionRatio',
      label: '扣款比例',
      component: 'InputNumber',
      componentProps: {
        placeholder: '请输入扣款比例',
        min: 0,
        max: 100,
        step: 10,
        precision: 0,
        formatter: (value: number) => `${value}%`,
        parser: (value: string) => parseFloat(value.replace('%', '')),
        style: { width: '100%' },
      },
      rules: 'required',
      help: '扣款比例,如50%表示按一半扣款',
    },
    {
      fieldName: 'status',
      label: '状态',
      component: 'RadioGroup',
      componentProps: {
        options: [
          { label: '启用', value: 0 },
          { label: '禁用', value: 1 },
        ],
      },
      rules: 'required',
    },
    {
      fieldName: 'remark',
      label: '备注',
      component: 'Textarea',
      componentProps: {
        placeholder: '请输入备注',
        rows: 2,
      },
      formItemClass: 'col-span-2',
    },
  ];
}
/** åˆ—表的字段 */
export function useGridColumns(): VxeTableGridOptions<HrmLeaveTypeConfigApi.LeaveTypeConfig>['columns'] {
  return [
    { field: 'name', title: '请假类型', minWidth: 120 },
    {
      field: 'deductionRatio',
      title: '扣款比例',
      width: 120,
      slots: { default: 'deductionRatio' },
    },
    { field: 'remark', title: '备注', minWidth: 200 },
    {
      field: 'status',
      title: '状态',
      width: 80,
      slots: { default: 'status' },
    },
    {
      field: 'createTime',
      title: '创建时间',
      width: 180,
      formatter: 'formatDateTime',
    },
    {
      title: '操作',
      width: 100,
      fixed: 'right',
      slots: { default: 'actions' },
    },
  ];
}
src/views/hrm/salary/leave-type-config/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,84 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { HrmLeaveTypeConfigApi } from '#/api/hrm/leave-type-config';
import { Page, useVbenModal } from '#/packages/effects/common-ui/src';
import { message, Tag } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import { getLeaveTypeConfigList } from '#/api/hrm/leave-type-config';
import { $t } from '#/locales';
import { useGridColumns } from './data';
import Form from './modules/form.vue';
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: Form,
  destroyOnClose: true,
});
/** åˆ·æ–°è¡¨æ ¼ */
function handleRefresh() {
  gridApi.query();
}
/** ç¼–辑配置 */
function handleEdit(row: HrmLeaveTypeConfigApi.LeaveTypeConfig) {
  formModalApi.setData({ id: row.id, row }).open();
}
const [Grid, gridApi] = useVbenVxeGrid({
  gridOptions: {
    columns: useGridColumns(),
    height: 'auto',
    keepSource: true,
    proxyConfig: {
      ajax: {
        query: async () => await getLeaveTypeConfigList(),
      },
    },
    rowConfig: {
      keyField: 'id',
      isHover: true,
    },
    pagerConfig: {
      enabled: false,
    },
    toolbarConfig: {
      refresh: true,
    },
  } as VxeTableGridOptions<HrmLeaveTypeConfigApi.LeaveTypeConfig>,
});
</script>
<template>
  <Page auto-content-height>
    <FormModal @success="handleRefresh" />
    <Grid table-title="请假扣款配置列表">
      <template #deductionRatio="{ row }">
        <Tag :color="row.deductionRatio === 0 ? 'success' : row.deductionRatio === 1 ? 'error' : 'warning'">
          {{ row.deductionRatio === 0 ? '不扣款' : `${(row.deductionRatio * 100).toFixed(0)}%` }}
        </Tag>
      </template>
      <template #status="{ row }">
        <Tag :color="row.status === 0 ? 'success' : 'default'">
          {{ row.status === 0 ? '启用' : '禁用' }}
        </Tag>
      </template>
      <template #actions="{ row }">
        <TableAction
          :actions="[
            {
              label: $t('common.edit'),
              type: 'link',
              icon: ACTION_ICON.EDIT,
              auth: ['hrm:leave-type-config:update'],
              onClick: handleEdit.bind(null, row),
            },
          ]"
        />
      </template>
    </Grid>
  </Page>
</template>
src/views/hrm/salary/leave-type-config/modules/form.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,67 @@
<script lang="ts" setup>
import type { HrmLeaveTypeConfigApi } from '#/api/hrm/leave-type-config';
import { ref } from 'vue';
import { useVbenModal } from '#/packages/effects/common-ui/src';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { updateLeaveTypeConfig } from '#/api/hrm/leave-type-config';
import { $t } from '#/locales';
import { useFormSchema } from '../data';
const emit = defineEmits(['success']);
const [Form, formApi] = useVbenForm({
  commonConfig: {
    componentProps: {
      class: 'w-full',
    },
    formItemClass: 'col-span-1',
    labelWidth: 100,
  },
  wrapperClass: 'grid-cols-2',
  layout: 'horizontal',
  schema: useFormSchema(),
  showDefaultActions: false,
});
const [Modal, modalApi] = useVbenModal({
  async onConfirm() {
    const { valid } = await formApi.validate();
    if (!valid) {
      return;
    }
    modalApi.lock();
    const data = await formApi.getValues();
    // è½¬æ¢æ¯”例为小数
    data.deductionRatio = data.deductionRatio / 100;
    try {
      await updateLeaveTypeConfig(data);
      await modalApi.close();
      emit('success');
      message.success($t('ui.actionMessage.operationSuccess'));
    } finally {
      modalApi.unlock();
    }
  },
  async onOpenChange(isOpen: boolean) {
    if (!isOpen) {
      return;
    }
    const data = modalApi.getData<{ id: number; row: HrmLeaveTypeConfigApi.LeaveTypeConfig }>();
    // è½¬æ¢æ¯”例为百分比显示
    const row = { ...data.row, deductionRatio: (data.row.deductionRatio || 0) * 100 };
    await formApi.setValues(row);
  },
});
</script>
<template>
  <Modal title="编辑扣款比例" class="w-[600px]">
    <Form class="mx-4" />
  </Modal>
</template>
src/views/hrm/salary/payment/data.ts
@@ -2,12 +2,8 @@
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { HrmSalaryPaymentApi } from '#/api/hrm/salary/payment';
import { DICT_TYPE } from '#/packages/constants/src';
import { getDictOptions } from '#/packages/effects/hooks/src';
import { getDeptList } from '#/api/system/dept';
import { getEmployeeSimpleList } from '#/api/hrm/employee';
import { handleTree } from '#/packages/utils/src';
import { DICT_TYPE } from '#/packages/constants/src';
/** åˆ—表的搜索表单 */
export function useGridFormSchema(): VbenFormSchema[] {
@@ -26,44 +22,14 @@
      },
    },
    {
      fieldName: 'userId',
      label: '员工',
      component: 'ApiSelect',
      componentProps: {
        api: getEmployeeSimpleList,
        labelField: 'name',
        valueField: 'id',
        allowClear: true,
        placeholder: '请选择员工',
      },
    },
    {
      fieldName: 'deptId',
      label: '部门',
      component: 'ApiTreeSelect',
      componentProps: {
        api: async () => {
          const data = await getDeptList();
          return handleTree(data);
        },
        labelField: 'name',
        valueField: 'id',
        childrenField: 'children',
        allowClear: true,
        placeholder: '请选择部门',
        treeDefaultExpandAll: true,
      },
    },
    {
      fieldName: 'status',
      label: '发放状态',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: [
          { label: '待发放', value: 10 },
          { label: '已发放', value: 20 },
          { label: '已撤销', value: 30 },
          { label: '待发放', value: 0 },
          { label: '已发放', value: 10 },
        ],
        placeholder: '请选择发放状态',
      },
@@ -74,12 +40,10 @@
/** åˆ—表的字段 */
export function useGridColumns(): VxeTableGridOptions<HrmSalaryPaymentApi.SalaryPayment>['columns'] {
  return [
    { type: 'checkbox', width: 50 },
    { field: 'no', title: '发放单号', minWidth: 140 },
    { field: 'period', title: '发放周期', width: 100 },
    { field: 'userName', title: '员工姓名', minWidth: 100 },
    { field: 'deptName', title: '部门', minWidth: 120 },
    { field: 'actualSalary', title: '实发工资', width: 100, formatter: 'formatAmount2' },
    { field: 'totalAmount', title: '发放总金额', width: 120, formatter: 'formatAmount2' },
    { field: 'totalCount', title: '发放人数', width: 100 },
    {
      field: 'paymentMethod',
      title: '发放方式',
@@ -90,15 +54,12 @@
      },
    },
    { field: 'paymentTime', title: '发放时间', width: 160 },
    { field: 'paymentNo', title: '发放流水号', minWidth: 140 },
    {
      field: 'status',
      title: '发放状态',
      title: '状态',
      width: 100,
      cellRender: {
        name: 'CellDict',
        props: { type: DICT_TYPE.HRM_SALARY_STATUS },
      },
      fixed: 'right',
      slots: { default: 'status' },
    },
    {
      field: 'createTime',
@@ -108,11 +69,9 @@
    },
    {
      title: '操作',
      width: 150,
      width: 180,
      fixed: 'right',
      slots: {
        default: 'actions',
      },
      slots: { default: 'actions' },
    },
  ];
}
src/views/hrm/salary/payment/index.vue
@@ -2,56 +2,79 @@
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { HrmSalaryPaymentApi } from '#/api/hrm/salary/payment';
import { Page } from '#/packages/effects/common-ui/src';
import { ref } from 'vue';
import { Page, useVbenModal } from '#/packages/effects/common-ui/src';
import { downloadFileFromBlobPart } from '#/packages/utils/src';
import { message } from 'ant-design-vue';
import { message, Tag, DatePicker } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
  exportSalaryPayment,
  generateSalaryPayment,
  getSalaryPaymentPage,
  paySalary,
  revokeSalaryPayment,
} from '#/api/hrm/salary/payment';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import DetailModal from './modules/detail.vue';
const [DetailModalComp, detailModalApi] = useVbenModal({
  connectedComponent: DetailModal,
  destroyOnClose: true,
});
// ç”Ÿæˆå°è´¦å¼¹çª—
const generateVisible = ref(false);
const generatePeriod = ref<string>();
const generateLoading = ref(false);
/** åˆ·æ–°è¡¨æ ¼ */
function handleRefresh() {
  gridApi.query();
}
/** æ‰§è¡Œè–ªèµ„发放 */
async function handlePay() {
  const records = gridApi.grid.getCheckboxRecords() as HrmSalaryPaymentApi.SalaryPayment[];
  if (records.length === 0) {
    message.warning('请选择要发放的记录');
    return;
  }
  const unpaidRecords = records.filter((r) => r.status === 10);
  if (unpaidRecords.length === 0) {
    message.warning('所选记录中没有待发放状态的记录');
    return;
  }
  const ids = unpaidRecords.map((r) => r.id!);
  await paySalary(ids);
  message.success('发放成功');
  handleRefresh();
/** æ‰“开生成台账弹窗 */
function openGenerateModal() {
  generatePeriod.value = undefined;
  generateVisible.value = true;
}
/** æ’¤é”€è–ªèµ„发放 */
async function handleRevoke(row: HrmSalaryPaymentApi.SalaryPayment) {
  await revokeSalaryPayment(row.id!);
  message.success('撤销成功');
/** ç”Ÿæˆå‘放台账 */
async function handleGenerate() {
  if (!generatePeriod.value) {
    message.warning('请选择发放周期');
    return;
  }
  generateLoading.value = true;
  try {
    await generateSalaryPayment(generatePeriod.value);
    message.success('生成发放台账成功');
    generateVisible.value = false;
    handleRefresh();
  } finally {
    generateLoading.value = false;
  }
}
/** æŸ¥çœ‹æ˜Žç»† */
function handleViewDetail(row: HrmSalaryPaymentApi.SalaryPayment) {
  detailModalApi.setData({ paymentId: row.id, period: row.period, status: row.status }).open();
}
/** å‘放薪资 */
async function handlePay(row: HrmSalaryPaymentApi.SalaryPayment) {
  await paySalary(row.id!);
  message.success('发放成功');
  handleRefresh();
}
/** å¯¼å‡ºè–ªèµ„发放 */
async function handleExport() {
  const data = await exportSalaryPayment(await gridApi.formApi.getValues());
  downloadFileFromBlobPart({ fileName: '薪资发放表.xls', source: data });
  downloadFileFromBlobPart({ fileName: '薪资发放台账.xls', source: data });
}
const [Grid, gridApi] = useVbenVxeGrid({
@@ -62,10 +85,6 @@
    columns: useGridColumns(),
    height: 'auto',
    keepSource: true,
    checkboxConfig: {
      highlight: true,
      reserve: true,
    },
    proxyConfig: {
      ajax: {
        query: async ({ page }, formValues) =>
@@ -90,16 +109,45 @@
<template>
  <Page auto-content-height>
    <Grid table-title="薪资发放列表">
    <DetailModalComp @success="handleRefresh" />
    <!-- ç”Ÿæˆå°è´¦å¼¹çª— -->
    <Modal
      v-model:open="generateVisible"
      title="生成发放台账"
      width="400px"
      :maskClosable="false"
    >
      <Form layout="vertical">
        <FormItem label="发放周期" required>
          <DatePicker
            v-model:value="generatePeriod"
            picker="month"
            format="YYYY-MM"
            valueFormat="YYYY-MM"
            placeholder="请选择发放周期"
            style="width: 100%"
          />
        </FormItem>
      </Form>
      <template #footer>
        <a-button @click="generateVisible = false">取消</a-button>
        <a-button type="primary" :loading="generateLoading" @click="handleGenerate">
          ç¡®å®š
        </a-button>
      </template>
    </Modal>
    <Grid table-title="薪资发放台账">
      <template #toolbar-tools>
        <TableAction
          :actions="[
            {
              label: '发放',
              label: '生成台账',
              type: 'primary',
              icon: ACTION_ICON.EDIT,
              auth: ['hrm:salary-payment:pay'],
              onClick: handlePay,
              icon: ACTION_ICON.ADD,
              auth: ['hrm:salary-payment:generate'],
              onClick: openGenerateModal,
            },
            {
              label: $t('ui.actionTitle.export'),
@@ -111,17 +159,28 @@
          ]"
        />
      </template>
      <template #status="{ row }">
        <Tag :color="row.status === 0 ? 'warning' : 'success'">
          {{ row.status === 0 ? '待发放' : '已发放' }}
        </Tag>
      </template>
      <template #actions="{ row }">
        <TableAction
          :actions="[
            {
              label: '撤销',
              label: '查看明细',
              type: 'link',
              auth: ['hrm:salary-payment:revoke'],
              ifShow: row.status === 20,
              onClick: handleViewDetail.bind(null, row),
            },
            {
              label: '发放',
              type: 'link',
              icon: ACTION_ICON.EDIT,
              auth: ['hrm:salary-payment:pay'],
              ifShow: row.status === 0,
              popConfirm: {
                title: '确认撤销该发放记录吗?',
                confirm: handleRevoke.bind(null, row),
                title: '确认发放该周期的薪资吗?',
                confirm: handlePay.bind(null, row),
              },
            },
          ]"
src/views/hrm/salary/payment/modules/detail.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,142 @@
<script lang="ts" setup>
import type { HrmSalaryPaymentApi } from '#/api/hrm/salary/payment';
import { ref, computed } from 'vue';
import { useVbenModal } from '#/packages/effects/common-ui/src';
import { message, Table, Tag, InputNumber, Alert } from 'ant-design-vue';
import { ACTION_ICON, TableAction } from '#/adapter/vxe-table';
import { getSalaryPaymentDetailList, paySalaryDetail } from '#/api/hrm/salary/payment';
import { $t } from '#/locales';
const emit = defineEmits(['success']);
const paymentId = ref<number>();
const period = ref<string>();
const status = ref<number>();
const detailList = ref<HrmSalaryPaymentApi.SalaryPaymentDetail[]>([]);
const loading = ref(false);
const selectedIds = ref<number[]>([]);
const payLoading = ref(false);
const isPaid = computed(() => status.value === 10);
const columns = [
  { title: '员工姓名', dataIndex: 'userName', width: 100, fixed: 'left' },
  { title: '部门', dataIndex: 'deptName', width: 120 },
  { title: '基本工资', dataIndex: 'baseSalary', width: 100 },
  { title: '绩效工资', dataIndex: 'performanceSalary', width: 100 },
  { title: '社保扣款', dataIndex: 'socialSecurityDeduction', width: 100 },
  { title: '公积金扣款', dataIndex: 'housingFundDeduction', width: 100 },
  { title: '个税扣款', dataIndex: 'taxDeduction', width: 100 },
  { title: '请假扣款', dataIndex: 'leaveDeduction', width: 100 },
  { title: '发放时间', dataIndex: 'paymentTime', width: 160 },
  { title: '实发工资', dataIndex: 'actualSalary', width: 100, fixed: 'right' },
  {
    title: '状态',
    dataIndex: 'status',
    width: 80,
    fixed: 'right',
    key: 'status',
  },
];
/** æ‰¹é‡å‘放 */
async function handleBatchPay() {
  if (selectedIds.value.length === 0) {
    message.warning('请选择要发放的明细');
    return;
  }
  payLoading.value = true;
  try {
    await paySalaryDetail(selectedIds.value);
    message.success('发放成功');
    emit('success');
    // åˆ·æ–°æ˜Žç»†åˆ—表
    detailList.value = await getSalaryPaymentDetailList(paymentId.value!);
    selectedIds.value = [];
  } finally {
    payLoading.value = false;
  }
}
/** å•条发放 */
async function handlePayDetail(row: HrmSalaryPaymentApi.SalaryPaymentDetail) {
  await paySalaryDetail([row.id!]);
  message.success('发放成功');
  emit('success');
  // åˆ·æ–°æ˜Žç»†åˆ—表
  detailList.value = await getSalaryPaymentDetailList(paymentId.value!);
}
/** é€‰æ‹©è¡Œ */
function onSelectChange(selectedRowKeys: number[]) {
  selectedIds.value = selectedRowKeys;
}
const [Modal, modalApi] = useVbenModal({
  async onOpenChange(isOpen: boolean) {
    if (!isOpen) {
      detailList.value = [];
      selectedIds.value = [];
      return;
    }
    const data = modalApi.getData<{ paymentId: number; period: string; status: number }>();
    paymentId.value = data.paymentId;
    period.value = data.period;
    status.value = data.status;
    loading.value = true;
    try {
      detailList.value = await getSalaryPaymentDetailList(data.paymentId);
    } finally {
      loading.value = false;
    }
  },
});
</script>
<template>
  <Modal :title="`${period} - è–ªèµ„发放明细`" class="w-[1100px]" :footer="null">
    <Alert
      v-if="!isPaid"
      message="勾选需要发放的员工明细,点击批量发放按钮进行发放"
      type="info"
      show-icon
      style="margin-bottom: 16px"
    />
    <div v-if="!isPaid" class="mb-3">
      <a-button type="primary" :loading="payLoading" :disabled="selectedIds.length === 0" @click="handleBatchPay">
        æ‰¹é‡å‘放 (已选 {{ selectedIds.length }} æ¡)
      </a-button>
    </div>
    <Table
      :columns="columns"
      :data-source="detailList"
      :loading="loading"
      :pagination="false"
      :scroll="{ x: 1200, y: 400 }"
      :row-selection="!isPaid ? { selectedRowKeys: selectedIds, onChange: onSelectChange } : undefined"
      size="small"
      row-key="id"
      bordered
    >
      <template #bodyCell="{ column, record }">
        <template v-if="column.key === 'status'">
          <Tag :color="record.status === 0 ? 'warning' : 'success'">
            {{ record.status === 0 ? '待发放' : '已发放' }}
          </Tag>
        </template>
        <template v-else-if="column.dataIndex === 'actualSalary'">
          <span class="font-bold text-green-600">{{ record.actualSalary?.toFixed(2) }}</span>
        </template>
        <template v-else-if="column.dataIndex && ['baseSalary', 'performanceSalary', 'socialSecurityDeduction', 'housingFundDeduction', 'taxDeduction', 'leaveDeduction'].includes(column.dataIndex)">
          {{ (record[column.dataIndex] || 0).toFixed(2) }}
        </template>
      </template>
    </Table>
  </Modal>
</template>