feat(hrm): 社保公积金方案表单加「适用部门」与默认缴费基数,修比例输入精度

- 修 8.5% 被四舍五入成 9%:比例格式化按 decimal(5,4) 精度保留 2 位百分比小数
(原 toFixed(step === 0.001 ? 1 : 0) 对 step=0.01 的字段只留整数百分比)
- 表单新增「适用部门」多选树(不选=全局默认)、默认社保/公积金基数
- 列表新增「适用部门」列,未绑定显示「全局默认」标签
- 个税税率配置页同款 toFixed(0) 顺带修正(表单回显 + 列表列)

Co-Authored-By: Claude Code <noreply@anthropic.com>
已修改6个文件
113 ■■■■■ 文件已修改
src/api/hrm/salary/social-security-scheme/index.ts 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/salary/social-security-scheme/data.ts 83 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/salary/social-security-scheme/index.vue 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/salary/social-security-scheme/modules/form.vue 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/salary/tax-rate-config/data.ts 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/salary/tax-rate-config/index.vue 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/hrm/salary/social-security-scheme/index.ts
@@ -18,6 +18,14 @@
    maternityRatio?: number;
    housingFundCompanyRatio?: number;
    housingFundPersonalRatio?: number;
    /** 默认社保缴费基数(不填则用员工基本工资) */
    defaultSocialSecurityBase?: number;
    /** 默认公积金缴费基数(不填或 0 则用社保基数) */
    defaultHousingFundBase?: number;
    /** 适用部门ID列表(为空表示全局默认方案) */
    deptIds?: number[];
    /** 适用部门名称,多个用、分隔(为空表示全局默认) */
    deptNames?: string;
    status?: number;
    remark?: string;
    createTime?: string;
src/views/hrm/salary/social-security-scheme/data.ts
@@ -2,15 +2,26 @@
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { HrmSocialSecuritySchemeApi } from '#/api/hrm/salary/social-security-scheme';
import { getDeptList } from '#/api/system/dept';
import { handleTree } from '#/packages/utils/src';
/** 表单类型 */
export type FormType = 'create' | 'update';
/** 格式化比例输入 */
function createRatioFormatter(step: number = 0.01) {
/**
 * 格式化比例输入:显示值(8.5%)与存储值(0.085)互转
 *
 * 保留 2 位百分比小数,正好对应数据库 decimal(5,4) 的 4 位小数精度,
 * 避免 8.5% 被四舍五入成 9% 后写回 0.09。
 */
function createRatioFormatter() {
  return {
    formatter: (value: number) => value !== undefined && value !== null ? `${(value * 100).toFixed(step === 0.001 ? 1 : 0)}%` : '',
    formatter: (value: number) =>
      value !== undefined && value !== null
        ? `${Number((value * 100).toFixed(2))}%`
        : '',
    parser: (value: string) => {
      const num = parseFloat(value.replace('%', ''));
      const num = parseFloat(String(value).replace('%', ''));
      return isNaN(num) ? 0 : num / 100;
    },
  };
@@ -59,6 +70,27 @@
        ],
      },
      rules: 'required',
    },
    {
      fieldName: 'deptIds',
      label: '适用部门',
      component: 'ApiTreeSelect',
      componentProps: {
        api: async () => {
          const data = await getDeptList();
          return handleTree(data);
        },
        labelField: 'name',
        valueField: 'id',
        childrenField: 'children',
        multiple: true,
        treeCheckable: true,
        showCheckedStrategy: 'SHOW_ALL',
        allowClear: true,
        treeDefaultExpandAll: true,
        placeholder: '不选表示全局默认方案;只对所选部门本级的员工生效',
      },
      formItemClass: 'col-span-2',
    },
    // 养老保险
    {
@@ -148,7 +180,7 @@
        step: 0.001,
        precision: 4,
        style: { width: '100%' },
        ...createRatioFormatter(0.001),
        ...createRatioFormatter(),
      },
    },
    {
@@ -162,7 +194,7 @@
        step: 0.001,
        precision: 4,
        style: { width: '100%' },
        ...createRatioFormatter(0.001),
        ...createRatioFormatter(),
      },
    },
    // 工伤/生育保险
@@ -183,7 +215,7 @@
        step: 0.001,
        precision: 4,
        style: { width: '100%' },
        ...createRatioFormatter(0.001),
        ...createRatioFormatter(),
      },
    },
    {
@@ -197,7 +229,7 @@
        step: 0.001,
        precision: 4,
        style: { width: '100%' },
        ...createRatioFormatter(0.001),
        ...createRatioFormatter(),
      },
    },
    // 公积金
@@ -233,6 +265,35 @@
        precision: 4,
        style: { width: '100%' },
        ...createRatioFormatter(),
      },
    },
    // 缴费基数
    {
      fieldName: 'divider_base',
      label: '缴费基数(未单独维护员工社保档案的员工按此计算)',
      component: 'Divider',
      formItemClass: 'col-span-2',
    },
    {
      fieldName: 'defaultSocialSecurityBase',
      label: '默认社保基数',
      component: 'InputNumber',
      componentProps: {
        placeholder: '留空则用员工基本工资',
        min: 0,
        precision: 2,
        style: { width: '100%' },
      },
    },
    {
      fieldName: 'defaultHousingFundBase',
      label: '默认公积金基数',
      component: 'InputNumber',
      componentProps: {
        placeholder: '留空或 0 则用社保基数',
        min: 0,
        precision: 2,
        style: { width: '100%' },
      },
    },
    // 备注
@@ -281,6 +342,12 @@
export function useGridColumns(): VxeTableGridOptions<HrmSocialSecuritySchemeApi.SocialSecurityScheme>['columns'] {
  return [
    { field: 'name', title: '方案名称', minWidth: 180 },
    {
      field: 'deptNames',
      title: '适用部门',
      minWidth: 160,
      slots: { default: 'deptNames' },
    },
    { field: 'effectiveDate', title: '生效日期', width: 110 },
    {
      field: 'pensionCompanyRatio',
src/views/hrm/salary/social-security-scheme/index.vue
@@ -95,6 +95,10 @@
          ]"
        />
      </template>
      <template #deptNames="{ row }">
        <span v-if="row.deptNames">{{ row.deptNames }}</span>
        <Tag v-else color="blue">全局默认</Tag>
      </template>
      <template #pensionCompanyRatio="{ row }">
        {{ formatRatio(row.pensionCompanyRatio) }}
      </template>
src/views/hrm/salary/social-security-scheme/modules/form.vue
@@ -81,6 +81,7 @@
        maternityRatio: 0.005,
        housingFundCompanyRatio: 0.12,
        housingFundPersonalRatio: 0.12,
        deptIds: [],
        status: 0,
      });
      return;
src/views/hrm/salary/tax-rate-config/data.ts
@@ -60,8 +60,15 @@
        max: 1,
        step: 0.01,
        precision: 4,
        formatter: (value: number) => `${(value * 100).toFixed(0)}%`,
        parser: (value: string) => parseFloat(value.replace('%', '')) / 100,
        // 保留 2 位百分比小数(对应 decimal(5,4) 的 4 位小数),避免 3.5% 被四舍五入成 4%
        formatter: (value: number) =>
          value === undefined || value === null
            ? ''
            : `${Number((value * 100).toFixed(2))}%`,
        parser: (value: string) => {
          const num = parseFloat(String(value).replace('%', ''));
          return isNaN(num) ? 0 : num / 100;
        },
      },
      rules: 'required',
    },
src/views/hrm/salary/tax-rate-config/index.vue
@@ -88,7 +88,11 @@
        {{ row.minAmount }} ~ {{ row.maxAmount === 0 ? '∞' : row.maxAmount }}
      </template>
      <template #taxRate="{ row }">
        {{ (row.taxRate * 100).toFixed(0) }}%
        {{
          row.taxRate === undefined || row.taxRate === null
            ? '-'
            : `${Number((row.taxRate * 100).toFixed(2))}%`
        }}
      </template>
      <template #actions="{ row }">
        <TableAction