feat: 薪酬核算列表行内编辑、工单处理人下拉搜索、社保比例两位小数、退货客户可编辑

- 薪酬核算列表:金额/出勤/加班列改为文本框直接编辑,工具栏新增「保存修改」(仅待确认可改)
- 售后工单:新增表单与详情「分配处理人」改为可搜索下拉(/system/user/simple-list),修复手输 ID 导致处理人为空
- 社保公积金方案:比例统一按百分比两位小数显示与输入,去掉自动取整
- 退货申请:客户字段改为可编辑文本框,不再强制关联客户档案
已修改7个文件
176 ■■■■■ 文件已修改
src/views/aftersales/return/data.ts 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/aftersales/ticket/data.ts 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/aftersales/ticket/detail.vue 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/salary/calculation/data.ts 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/salary/calculation/index.vue 69 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/salary/social-security-scheme/data.ts 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/salary/social-security-scheme/index.vue 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/aftersales/return/data.ts
@@ -44,9 +44,9 @@
      label: '客户',
      component: 'Input',
      componentProps: {
        disabled: true,
        placeholder: '客户',
        placeholder: '请输入客户名称',
      },
      rules: 'required',
    },
    {
      fieldName: 'saleOrderId',
src/views/aftersales/ticket/data.ts
@@ -2,7 +2,7 @@
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { AftersalesTicketApi } from '#/api/aftersales/ticket';
import { h, markRaw } from 'vue';
import { h } from 'vue';
import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
@@ -10,7 +10,7 @@
import { Button, Tag } from 'ant-design-vue';
import { z } from '#/adapter/form';
import { UserSelect } from '#/views/system/user/components';
import { getSimpleUserList } from '#/api/system/user';
import { getDeptList } from '#/api/system/dept';
import { handleTree } from '#/packages/utils/src';
@@ -85,9 +85,17 @@
    {
      fieldName: 'handlerUserId',
      label: '处理人',
      component: markRaw(UserSelect),
      component: 'ApiSelect',
      componentProps: {
        placeholder: '请选择处理人',
        api: getSimpleUserList,
        labelField: 'nickname',
        valueField: 'id',
        showSearch: true,
        filterOption: (input: string, option: any) =>
          `${option?.label ?? ''}`.toLowerCase().includes(input.toLowerCase()) ||
          `${option?.value ?? ''}`.includes(input),
        allowClear: true,
        placeholder: '请选择处理人(可搜索昵称)',
      },
      rules: 'selectRequired',
    },
src/views/aftersales/ticket/detail.vue
@@ -19,14 +19,8 @@
  Tag,
} from 'ant-design-vue';
import {
  assignTicket,
  cancelTicket,
  classifyTicket,
  closeTicket,
  getTicket,
  resolveTicket,
} from '#/api/aftersales/ticket';
import { assignTicket, cancelTicket, classifyTicket, closeTicket, getTicket, resolveTicket } from '#/api/aftersales/ticket';
import { getSimpleUserList } from '#/api/system/user';
import type { AftersalesVisitApi } from '#/api/aftersales/visit';
import {
  createVisit,
@@ -77,6 +71,25 @@
// 分配处理人
const showAssignInput = ref(false);
const assignUserId = ref<number>();
const userOptions = ref<{ label: string; value: number }[]>([]);
const userLoading = ref(false);
/** 加载可选的系统用户(可搜索昵称) */
async function loadUserOptions() {
  if (userOptions.value.length > 0) {
    return;
  }
  userLoading.value = true;
  try {
    const list = (await getSimpleUserList()) || [];
    userOptions.value = list.map((u: any) => ({
      label: u.nickname || u.username || String(u.id),
      value: u.id,
    }));
  } finally {
    userLoading.value = false;
  }
}
// 问题判定
const showClassifyInput = ref(false);
@@ -156,11 +169,14 @@
function toggleAssignInput() {
  showAssignInput.value = !showAssignInput.value;
  assignUserId.value = undefined;
  if (showAssignInput.value) {
    loadUserOptions();
  }
}
async function handleAssign() {
  if (!assignUserId.value) {
    message.warning('请输入处理人ID');
    message.warning('请选择处理人');
    return;
  }
  await assignTicket(ticketId.value, assignUserId.value);
@@ -468,12 +484,14 @@
            <Button @click="toggleAssignInput">分配处理人</Button>
          </template>
          <template v-else>
            <Input
            <Select
              v-model:value="assignUserId"
              placeholder="处理人ID"
              type="number"
              style="width: 120px"
              @pressEnter="handleAssign"
              :options="userOptions"
              :loading="userLoading"
              show-search
              option-filter-prop="label"
              placeholder="请选择处理人"
              style="width: 200px"
            />
            <Button size="small" type="primary" @click="handleAssign">确认</Button>
            <Button size="small" @click="toggleAssignInput">取消</Button>
src/views/hrm/salary/calculation/data.ts
@@ -75,21 +75,21 @@
    { field: 'period', title: '核算周期', width: 100 },
    { field: 'userName', title: '员工姓名', minWidth: 100 },
    { field: 'deptName', title: '部门', minWidth: 120 },
    { field: 'baseSalary', title: '基本工资', width: 100, formatter: 'formatAmount2' },
    { field: 'performanceSalary', title: '绩效工资', width: 100, formatter: 'formatAmount2' },
    { field: 'overtimePay', title: '加班费', width: 100, formatter: 'formatAmount2' },
    { field: 'mealAllowance', title: '餐补', width: 80, formatter: 'formatAmount2' },
    { field: 'transportAllowance', title: '交通补贴', width: 80, formatter: 'formatAmount2' },
    { field: 'otherAllowance', title: '其他补贴', width: 80, formatter: 'formatAmount2' },
    { field: 'socialSecurityDeduction', title: '社保扣款', width: 100, formatter: 'formatAmount2' },
    { 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 },
    { field: 'totalWorkHours', title: '排班工时(h)', width: 100 },
    { field: 'baseSalary', title: '基本工资', width: 120, slots: { default: 'editableCell' } },
    { field: 'performanceSalary', title: '绩效工资', width: 120, slots: { default: 'editableCell' } },
    { field: 'overtimePay', title: '加班费', width: 120, slots: { default: 'editableCell' } },
    { field: 'mealAllowance', title: '餐补', width: 110, slots: { default: 'editableCell' } },
    { field: 'transportAllowance', title: '交通补贴', width: 110, slots: { default: 'editableCell' } },
    { field: 'otherAllowance', title: '其他补贴', width: 110, slots: { default: 'editableCell' } },
    { field: 'socialSecurityDeduction', title: '社保扣款', width: 120, slots: { default: 'editableCell' } },
    { field: 'housingFundDeduction', title: '公积金扣款', width: 120, slots: { default: 'editableCell' } },
    { field: 'taxDeduction', title: '个税扣款', width: 120, slots: { default: 'editableCell' } },
    { field: 'otherDeduction', title: '其他扣款', width: 110, slots: { default: 'editableCell' } },
    { field: 'leaveDeduction', title: '请假扣款', width: 120, slots: { default: 'editableCell' } },
    { field: 'actualSalary', title: '实发工资', width: 110, formatter: 'formatAmount2' },
    { field: 'workDays', title: '出勤天数', width: 110, slots: { default: 'editableCell' } },
    { field: 'overtimeHours', title: '加班时长(h)', width: 120, slots: { default: 'editableCell' } },
    { field: 'totalWorkHours', title: '排班工时(h)', width: 110 },
    {
      field: 'status',
      title: '状态',
src/views/hrm/salary/calculation/index.vue
@@ -7,7 +7,7 @@
import { Page } from '#/packages/effects/common-ui/src';
import { downloadFileFromBlobPart } from '#/packages/utils/src';
import { message, Modal, Form, FormItem, DatePicker, TreeSelect, Table, InputNumber, Alert, Tag } from 'ant-design-vue';
import { message, Modal, Form, FormItem, DatePicker, TreeSelect, Table, Input, InputNumber, Alert, Tag } from 'ant-design-vue';
import { DICT_TYPE } from '#/packages/constants/src';
import { getDictLabel } from '#/packages/effects/hooks/src';
@@ -22,6 +22,7 @@
  previewSalaryCalculation,
  revokeSalaryCalculation,
  saveSalaryCalculation,
  updateSalaryCalculation,
} from '#/api/hrm/salary/calculation';
import { getDeptList } from '#/api/system/dept';
import { handleTree } from '#/packages/utils/src';
@@ -32,6 +33,7 @@
// 核算弹窗相关
const formModalRef = ref();
const saving = ref(false);
const calculateVisible = ref(false);
const calculatePeriod = ref<string>();
const calculateDeptId = ref<number>();
@@ -226,6 +228,55 @@
async function handleExport() {
  const data = await exportSalaryCalculation(await gridApi.formApi.getValues());
  downloadFileFromBlobPart({ fileName: '薪酬核算表.xls', source: data });
}
/** 列表可编辑的数值字段 */
const EDITABLE_NUMBER_FIELDS = [
  'baseSalary',
  'performanceSalary',
  'overtimePay',
  'mealAllowance',
  'transportAllowance',
  'otherAllowance',
  'socialSecurityDeduction',
  'housingFundDeduction',
  'taxDeduction',
  'otherDeduction',
  'leaveDeduction',
  'workDays',
  'overtimeHours',
];
/** 文本输入转数字(空值按 0) */
function toNumberOrZero(val: any) {
  if (val === '' || val === null || val === undefined) {
    return 0;
  }
  const num = Number(String(val).replace(/,/g, ''));
  return Number.isNaN(num) ? 0 : num;
}
/** 保存列表内联修改(仅待确认状态) */
async function handleSaveEdits() {
  const rows = (gridApi.grid.getData() as any[]).filter((r) => r.status === 0);
  if (rows.length === 0) {
    message.warning('当前页没有待确认的记录可修改');
    return;
  }
  saving.value = true;
  try {
    for (const row of rows) {
      const payload: any = { ...row };
      EDITABLE_NUMBER_FIELDS.forEach((f) => {
        payload[f] = toNumberOrZero(payload[f]);
      });
      await updateSalaryCalculation(payload);
    }
    message.success(`已保存 ${rows.length} 条核算记录`);
    handleRefresh();
  } finally {
    saving.value = false;
  }
}
const [Grid, gridApi] = useVbenVxeGrid({
@@ -496,6 +547,13 @@
              onClick: handleBatchSave,
            },
            {
              label: '保存修改',
              type: 'primary',
              auth: ['hrm:salary-calculation:calculate'],
              loading: saving,
              onClick: handleSaveEdits,
            },
            {
              label: $t('ui.actionTitle.export'),
              type: 'primary',
              icon: ACTION_ICON.DOWNLOAD,
@@ -505,6 +563,15 @@
          ]"
        />
      </template>
      <template #editableCell="{ row, column }">
        <Input
          v-if="row.status === 0"
          v-model:value="row[column.field]"
          size="small"
          :style="{ width: '100%' }"
        />
        <span v-else>{{ row[column.field] ?? '' }}</span>
      </template>
      <template #status="{ row }">
        <Tag :color="row.status === 0 ? 'warning' : row.status === 10 ? 'success' : 'processing'">
          {{ getDictLabel(DICT_TYPE.HRM_SALARY_STATUS, row.status) }}
src/views/hrm/salary/social-security-scheme/data.ts
@@ -5,12 +5,13 @@
/** 表单类型 */
export type FormType = 'create' | 'update';
/** 格式化比例输入 */
/** 格式化比例输入(统一显示两位小数百分比,如 8.25%) */
function createRatioFormatter(step: number = 0.01) {
  return {
    formatter: (value: number) => value !== undefined && value !== null ? `${(value * 100).toFixed(step === 0.001 ? 1 : 0)}%` : '',
    formatter: (value: number) =>
      value !== undefined && value !== null ? `${(value * 100).toFixed(2)}%` : '',
    parser: (value: string) => {
      const num = parseFloat(value.replace('%', ''));
      const num = parseFloat(String(value).replace('%', ''));
      return isNaN(num) ? 0 : num / 100;
    },
  };
src/views/hrm/salary/social-security-scheme/index.vue
@@ -43,10 +43,10 @@
  handleRefresh();
}
/** 格式化比例显示 */
/** 格式化比例显示(保留两位小数) */
function formatRatio(ratio: number | undefined) {
  if (ratio === undefined || ratio === null) return '-';
  return `${(ratio * 100).toFixed(1)}%`;
  return `${(ratio * 100).toFixed(2)}%`;
}
/** 格式化比例显示(保留两位小数) */