云
2026-10-25 bc59df29155d4c0a3288c8bbae6576e44320301e
feat(hrm): 新增人事管理模块功能

- 添加转正申请功能,包括创建、编辑、审批流程
- 添加薪酬结构管理功能,支持基本工资、绩效占比等配置
- 添加员工薪酬档案管理功能,可单独维护员工个人薪酬
- 实现转正申请的提交、审核、驳回等完整流程
- 添加薪酬相关的API接口和服务
- 集成员工选择组件优化用户体验
- 完善相关表单验证和错误处理机制
已添加12个文件
已修改9个文件
1016 ■■■■■ 文件已修改
dist.zip 补丁 | 查看 | 原始文档 | blame | 历史
src/api/hrm/regularize/index.ts 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/hrm/salary/employee-salary/index.ts 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/hrm/salary/structure/index.ts 60 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/wm/barcode/index.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/aftersales/ticket/detail.vue 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/aftersales/ticket/index.vue 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/regularize/data.ts 65 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/regularize/index.vue 108 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/regularize/modules/form.vue 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/salary/employee-salary/data.ts 86 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/salary/employee-salary/index.vue 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/salary/employee-salary/modules/form.vue 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/salary/structure/data.ts 154 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/salary/structure/index.vue 133 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/hrm/salary/structure/modules/form.vue 95 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/barcode/components/scan-modal.vue 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/barcode/config/index.vue 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/barcode/data.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/barcode/index.vue 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/wls/sn/index.vue 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
dist.zip
Binary files differ
src/api/hrm/regularize/index.ts
@@ -63,3 +63,15 @@
) {
  return requestClient.put('/hrm/regularize/audit', { id, pass, reviewRemark });
}
export function createRegularize(data: HrmRegularizeApi.Regularize) {
  return requestClient.post<number>('/hrm/regularize/create', data);
}
export function updateRegularize(data: HrmRegularizeApi.Regularize) {
  return requestClient.put('/hrm/regularize/update', data);
}
export function deleteRegularize(id: number) {
  return requestClient.delete('/hrm/regularize/delete', { params: { id } });
}
src/api/hrm/salary/employee-salary/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,59 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace HrmEmployeeSalaryApi {
  export interface EmployeeSalary {
    id?: number;
    userId?: number;
    employeeNo?: string;
    employeeName?: string;
    salaryStructureId?: number;
    salaryStructureName?: string;
    baseSalary?: number;
    performanceSalary?: number;
    mealAllowance?: number;
    transportAllowance?: number;
    otherAllowance?: number;
    effectiveDate?: string;
    status?: number;
    remark?: string;
    createTime?: string;
  }
  export interface PageParams extends PageParam {
    userId?: number;
    salaryStructureId?: number;
    status?: number;
  }
}
export function getEmployeeSalaryPage(params: HrmEmployeeSalaryApi.PageParams) {
  return requestClient.get<PageResult<HrmEmployeeSalaryApi.EmployeeSalary>>(
    '/hrm/employee-salary/page', { params },
  );
}
export function getEmployeeSalary(id: number) {
  return requestClient.get<HrmEmployeeSalaryApi.EmployeeSalary>(
    '/hrm/employee-salary/get', { params: { id } },
  );
}
export function getEmployeeSalaryHistory(userId: number) {
  return requestClient.get<HrmEmployeeSalaryApi.EmployeeSalary[]>(
    '/hrm/employee-salary/list-by-user', { params: { userId } },
  );
}
export function createEmployeeSalary(data: HrmEmployeeSalaryApi.EmployeeSalary) {
  return requestClient.post('/hrm/employee-salary/create', data);
}
export function updateEmployeeSalary(data: HrmEmployeeSalaryApi.EmployeeSalary) {
  return requestClient.put('/hrm/employee-salary/update', data);
}
export function deleteEmployeeSalary(id: number) {
  return requestClient.delete('/hrm/employee-salary/delete', { params: { id } });
}
src/api/hrm/salary/structure/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,60 @@
import type { PageParam, PageResult } from '@vben/request';
import { requestClient } from '#/api/request';
export namespace HrmSalaryStructureApi {
  export interface SalaryStructure {
    id?: number;
    name?: string;
    baseSalary?: number;
    performanceRatio?: number;
    overtimePayRatio?: number;
    socialSecurityBase?: number;
    housingFundBase?: number;
    status?: number;
    remark?: string;
    createTime?: string;
  }
  export interface SalaryStructureQueryParam extends Partial<PageParam> {
    name?: string;
    status?: number;
  }
}
export function getSalaryStructurePage(params: HrmSalaryStructureApi.SalaryStructureQueryParam) {
  return requestClient.get<PageResult<HrmSalaryStructureApi.SalaryStructure>>(
    '/hrm/salary/structure/page', { params },
  );
}
export function getSalaryStructureList(
  params?: HrmSalaryStructureApi.SalaryStructureQueryParam,
) {
  return requestClient.get<HrmSalaryStructureApi.SalaryStructure[]>(
    '/hrm/salary/structure/list',
    { params },
  );
}
export function getSalaryStructure(id: number) {
  return requestClient.get<HrmSalaryStructureApi.SalaryStructure>(
    `/hrm/salary/structure/get?id=${id}`,
  );
}
export function createSalaryStructure(
  data: HrmSalaryStructureApi.SalaryStructure,
) {
  return requestClient.post('/hrm/salary/structure/create', data);
}
export function updateSalaryStructure(
  data: HrmSalaryStructureApi.SalaryStructure,
) {
  return requestClient.put('/hrm/salary/structure/update', data);
}
export function deleteSalaryStructure(id: number) {
  return requestClient.delete(`/hrm/salary/structure/delete?id=${id}`);
}
src/api/mes/wm/barcode/index.ts
@@ -73,7 +73,7 @@
  });
}
/** ç”Ÿæˆæ¡ç /二维码图片(服务端 zxing æˆå›¾ï¼Œè¿”回 PNG Base64,不落库) */
/** ç”Ÿæˆæ¡ç /二维码图片(服务端返回 PNG data URL,不落库) */
export function renderBarcodeImage(
  content: string,
  format?: number,
src/views/aftersales/ticket/detail.vue
@@ -17,6 +17,7 @@
  getTicket,
  resolveTicket,
} from '#/api/aftersales/ticket';
import { UserSelect } from '#/views/system/user/components';
defineOptions({ name: 'AftersalesTicketDetail' });
@@ -93,9 +94,13 @@
  assignUserId.value = undefined;
}
async function handleAssignUserChange(value: number | number[] | undefined) {
  assignUserId.value = Array.isArray(value) ? value[0] : value;
}
async function handleAssign() {
  if (!assignUserId.value) {
    message.warning('请输入处理人ID');
    message.warning('请选择处理人');
    return;
  }
  await assignTicket(ticketId.value, assignUserId.value);
@@ -299,12 +304,11 @@
            <Button @click="toggleAssignInput">分配处理人</Button>
          </template>
          <template v-else>
            <Input
              v-model:value="assignUserId"
              placeholder="处理人ID"
              type="number"
              style="width: 120px"
              @pressEnter="handleAssign"
            <UserSelect
              :model-value="assignUserId"
              placeholder="请选择处理人"
              style="width: 200px"
              @update:model-value="handleAssignUserChange"
            />
            <Button size="small" type="primary" @click="handleAssign">确认</Button>
            <Button size="small" @click="toggleAssignInput">取消</Button>
src/views/aftersales/ticket/index.vue
@@ -3,6 +3,7 @@
import type { AftersalesTicketApi } from '#/api/aftersales/ticket';
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { Page, useVbenModal } from '@vben/common-ui';
import { AftersaleTicketStatusEnum } from '@vben/constants';
@@ -17,6 +18,7 @@
import { useTicketGridColumns, useTicketGridFormSchema } from './data';
import Form from './modules/form.vue';
const router = useRouter();
const sceneType = ref('1');
/** å¤„理场景类型的切换 */
@@ -40,9 +42,9 @@
  formModalApi.setData({ formType: 'create' }).open();
}
/** æŸ¥çœ‹å·¥å• */
/** æŸ¥çœ‹å·¥å•:跳转详情页(状态流转操作集中在该页面) */
function handleDetail(row: AftersalesTicketApi.Ticket) {
  formModalApi.setData({ formType: 'detail', id: row.id }).open();
  router.push({ name: 'AftersalesTicketDetail', params: { id: row.id } });
}
/** ç¼–辑工单 */
src/views/hrm/regularize/data.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,65 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { HrmRegularizeApi } from '#/api/hrm/regularize';
import { getSimpleUserList } from '#/api/system/user';
export const statusOptions = [
  { label: '草稿', value: 0 },
  { label: '审批中', value: 10 },
  { label: '已通过', value: 20 },
  { label: '已驳回', value: 30 },
  { label: '已取消', value: 40 },
];
export function useFormSchema(): VbenFormSchema[] {
  return [
    { fieldName: 'id', component: 'Input', dependencies: { triggerFields: [''], show: () => false } },
    {
      fieldName: 'userId', label: '申请人', component: 'ApiSelect',
      componentProps: {
        api: getSimpleUserList, labelField: 'nickname', valueField: 'id',
        allowClear: true, showSearch: true, optionFilterProp: 'label',
        placeholder: '不选则为当前用户',
      },
    },
    {
      fieldName: 'expectedDate', label: '拟转正日期', component: 'DatePicker',
      componentProps: { valueFormat: 'YYYY-MM-DD', style: { width: '100%' } },
      rules: 'required',
    },
    {
      fieldName: 'reason', label: '转正原因', component: 'Textarea',
      componentProps: { rows: 4, maxlength: 1000, showCount: true },
      rules: 'required', formItemClass: 'col-span-2',
    },
    {
      fieldName: 'remark', label: '备注', component: 'Textarea',
      componentProps: { rows: 2, maxlength: 500 }, formItemClass: 'col-span-2',
    },
  ];
}
export function useGridFormSchema(): VbenFormSchema[] {
  return [
    { fieldName: 'no', label: '申请单号', component: 'Input', componentProps: { allowClear: true } },
    {
      fieldName: 'status', label: '审批状态', component: 'Select',
      componentProps: { options: statusOptions, allowClear: true },
    },
  ];
}
export function useGridColumns(): VxeTableGridOptions<HrmRegularizeApi.Regularize>['columns'] {
  return [
    { field: 'no', title: '申请单号', minWidth: 175 },
    { field: 'userName', title: '申请人', minWidth: 110 },
    { field: 'deptName', title: '部门', minWidth: 120 },
    { field: 'expectedDate', title: '拟转正日期', width: 125 },
    { field: 'reason', title: '转正原因', minWidth: 200, showOverflow: true },
    { field: 'status', title: '状态', width: 105, slots: { default: 'status' } },
    { field: 'reviewerName', title: '审核人', width: 110 },
    { field: 'createTime', title: '创建时间', width: 180, formatter: 'formatDateTime' },
    { field: 'actions', title: '操作', fixed: 'right', width: 300, slots: { default: 'actions' } },
  ];
}
src/views/hrm/regularize/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,108 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { HrmRegularizeApi } from '#/api/hrm/regularize';
import { ref } from 'vue';
import { Page, useVbenModal } from '#/packages/effects/common-ui/src';
import { Input, message, Modal, Tag } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
  auditRegularize, deleteRegularize, getRegularizePage, submitRegularize,
} from '#/api/hrm/regularize';
import { statusOptions, useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
defineOptions({ name: 'HrmRegularize' });
const [FormModal, formModalApi] = useVbenModal({ connectedComponent: Form, destroyOnClose: true });
const auditOpen = ref(false);
const auditLoading = ref(false);
const auditId = ref<number>();
const auditPass = ref(true);
const auditRemark = ref('');
function refresh() { gridApi.query(); }
function create() { formModalApi.setData({}).open(); }
function edit(row: HrmRegularizeApi.Regularize) { formModalApi.setData({ id: row.id }).open(); }
async function submit(row: HrmRegularizeApi.Regularize) {
  await submitRegularize(row.id!);
  message.success('已提交审批');
  refresh();
}
async function remove(row: HrmRegularizeApi.Regularize) {
  await deleteRegularize(row.id!);
  message.success('删除成功');
  refresh();
}
function openAudit(row: HrmRegularizeApi.Regularize, pass: boolean) {
  auditId.value = row.id;
  auditPass.value = pass;
  auditRemark.value = '';
  auditOpen.value = true;
}
async function confirmAudit() {
  if (!auditId.value) return;
  if (!auditPass.value && !auditRemark.value.trim()) {
    message.warning('请填写驳回原因');
    return;
  }
  auditLoading.value = true;
  try {
    await auditRegularize(auditId.value, auditPass.value, auditRemark.value.trim());
    auditOpen.value = false;
    message.success('审核完成');
    refresh();
  } finally { auditLoading.value = false; }
}
const [Grid, gridApi] = useVbenVxeGrid({
  formOptions: { schema: useGridFormSchema() },
  gridOptions: {
    columns: useGridColumns(), height: 'auto', keepSource: true,
    proxyConfig: { ajax: { query: async ({ page }, formValues) =>
      getRegularizePage({ pageNo: page.currentPage, pageSize: page.pageSize, ...formValues }) } },
    rowConfig: { keyField: 'id', isHover: true },
    toolbarConfig: { refresh: true, search: true },
  } as VxeTableGridOptions<HrmRegularizeApi.Regularize>,
});
</script>
<template>
  <Page auto-content-height>
    <FormModal @success="refresh" />
    <Modal v-model:open="auditOpen" :confirm-loading="auditLoading"
      :title="auditPass ? '通过转正申请' : '驳回转正申请'" @ok="confirmAudit">
      <p class="mb-2">{{ auditPass ? '确认通过该转正申请?' : '请填写驳回原因。' }}</p>
      <Input.TextArea v-model:value="auditRemark" :rows="3" :maxlength="500" show-count
        :placeholder="auditPass ? '审核意见(选填)' : '驳回原因(必填)'" />
    </Modal>
    <Grid table-title="转正申请">
      <template #toolbar-tools>
        <TableAction :actions="[{ label: '新增申请', type: 'primary', icon: ACTION_ICON.ADD,
          auth: ['hrm:regularize:create'], onClick: create }]" />
      </template>
      <template #status="{ row }">
        <Tag :color="row.status === 20 ? 'success' : row.status === 30 ? 'error' : 'default'">
          {{ row.statusName || statusOptions.find((s) => s.value === row.status)?.label || '-' }}
        </Tag>
      </template>
      <template #actions="{ row }">
        <TableAction :actions="[
          ...(row.status === 0 || row.status === 30 ? [
            { label: '编辑', type: 'link' as const, auth: ['hrm:regularize:update'], onClick: edit.bind(null, row) },
            { label: '提交', type: 'link' as const, auth: ['hrm:regularize:update'],
              popConfirm: { title: '确定提交转正审批吗?', confirm: submit.bind(null, row) } },
            { label: '删除', type: 'link' as const, danger: true, auth: ['hrm:regularize:delete'],
              popConfirm: { title: '确定删除该申请吗?', confirm: remove.bind(null, row) } },
          ] : []),
          ...(row.status === 10 ? [
            { label: '通过', type: 'link' as const, auth: ['hrm:regularize:audit'],
              onClick: openAudit.bind(null, row, true) },
            { label: '驳回', type: 'link' as const, danger: true, auth: ['hrm:regularize:audit'],
              onClick: openAudit.bind(null, row, false) },
          ] : []),
        ]" />
      </template>
    </Grid>
  </Page>
</template>
src/views/hrm/regularize/modules/form.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,49 @@
<script lang="ts" setup>
import type { HrmRegularizeApi } from '#/api/hrm/regularize';
import { computed, ref } from 'vue';
import { useVbenModal } from '#/packages/effects/common-ui/src';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { createRegularize, getRegularize, updateRegularize } from '#/api/hrm/regularize';
import { useFormSchema } from '../data';
const emit = defineEmits(['success']);
const isEdit = ref(false);
const title = computed(() => isEdit.value ? '修改转正申请' : '新增转正申请');
const [Form, formApi] = useVbenForm({
  commonConfig: { componentProps: { class: 'w-full' }, formItemClass: 'col-span-1', labelWidth: 110 },
  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();
    try {
      const data = await formApi.getValues() as HrmRegularizeApi.Regularize;
      if (isEdit.value) await updateRegularize(data);
      else await createRegularize(data);
      await modalApi.close();
      message.success('保存成功');
      emit('success');
    } finally { modalApi.unlock(); }
  },
  async onOpenChange(open: boolean) {
    if (!open) return;
    const { id } = modalApi.getData<{ id?: number }>();
    isEdit.value = !!id;
    await formApi.resetForm();
    if (id) {
      modalApi.lock();
      try { await formApi.setValues(await getRegularize(id)); }
      finally { modalApi.unlock(); }
    }
  },
});
</script>
<template>
  <Modal :title="title" class="w-[680px]"><Form class="mx-4" /></Modal>
</template>
src/views/hrm/salary/employee-salary/data.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,86 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { HrmEmployeeSalaryApi } from '#/api/hrm/salary/employee-salary';
import { getEmployeeSimpleList } from '#/api/hrm/employee';
import { getSalaryStructureList } from '#/api/hrm/salary/structure';
const statusOptions = [
  { label: '启用', value: 0 },
  { label: '禁用', value: 1 },
];
export function useFormSchema(): VbenFormSchema[] {
  return [
    { fieldName: 'id', component: 'Input', dependencies: { triggerFields: [''], show: () => false } },
    {
      fieldName: 'userId', label: '员工', component: 'ApiSelect',
      componentProps: {
        api: getEmployeeSimpleList, labelField: 'name', valueField: 'id',
        showSearch: true, optionFilterProp: 'label', placeholder: '请选择员工',
      },
      rules: 'selectRequired', formItemClass: 'col-span-2',
    },
    {
      fieldName: 'salaryStructureId', label: '薪酬结构', component: 'ApiSelect',
      componentProps: {
        api: () => getSalaryStructureList({ status: 0 }),
        labelField: 'name', valueField: 'id',
        placeholder: '请选择已启用的薪酬结构',
      },
      rules: 'selectRequired', formItemClass: 'col-span-2',
    },
    ...([
      ['baseSalary', '基本工资'], ['performanceSalary', '绩效工资'],
      ['mealAllowance', '餐补'], ['transportAllowance', '交通补贴'],
      ['otherAllowance', '其他补贴'],
    ] as const).map(([fieldName, label]) => ({
      fieldName, label, component: 'InputNumber' as const,
      componentProps: { min: 0, precision: 2, style: { width: '100%' } },
      ...(fieldName === 'baseSalary' ? { rules: 'required' as const } : {}),
    })),
    {
      fieldName: 'effectiveDate', label: '生效日期', component: 'DatePicker',
      componentProps: { valueFormat: 'YYYY-MM-DD', style: { width: '100%' } },
      rules: 'required',
    },
    {
      fieldName: 'status', label: '状态', component: 'RadioGroup',
      componentProps: { options: statusOptions }, rules: 'required',
    },
    {
      fieldName: 'remark', label: '备注', component: 'Textarea',
      componentProps: { rows: 2, maxlength: 500 }, formItemClass: 'col-span-2',
    },
  ];
}
export function useGridFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'userId', label: '员工', component: 'ApiSelect',
      componentProps: {
        api: getEmployeeSimpleList, labelField: 'name', valueField: 'id',
        allowClear: true, showSearch: true, optionFilterProp: 'label',
      },
    },
    {
      fieldName: 'status', label: '状态', component: 'Select',
      componentProps: { options: statusOptions, allowClear: true },
    },
  ];
}
export function useGridColumns(): VxeTableGridOptions<HrmEmployeeSalaryApi.EmployeeSalary>['columns'] {
  return [
    { field: 'employeeNo', title: '员工编号', minWidth: 120 },
    { field: 'employeeName', title: '员工姓名', minWidth: 100 },
    { field: 'salaryStructureName', title: '薪酬结构', minWidth: 140 },
    { field: 'baseSalary', title: '基本工资', width: 115, formatter: 'formatAmount2' },
    { field: 'performanceSalary', title: '绩效工资', width: 115, formatter: 'formatAmount2' },
    { field: 'effectiveDate', title: '生效日期', width: 120 },
    { field: 'status', title: '状态', width: 90, slots: { default: 'status' } },
    { field: 'createTime', title: '创建时间', width: 180, formatter: 'formatDateTime' },
    { field: 'actions', title: '操作', fixed: 'right', width: 180, slots: { default: 'actions' } },
  ];
}
src/views/hrm/salary/employee-salary/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,61 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { HrmEmployeeSalaryApi } from '#/api/hrm/salary/employee-salary';
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 { deleteEmployeeSalary, getEmployeeSalaryPage } from '#/api/hrm/salary/employee-salary';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
defineOptions({ name: 'HrmEmployeeSalary' });
const [FormModal, formModalApi] = useVbenModal({ connectedComponent: Form, destroyOnClose: true });
function refresh() { gridApi.query(); }
function create() { formModalApi.setData({}).open(); }
function edit(row: HrmEmployeeSalaryApi.EmployeeSalary) { formModalApi.setData({ id: row.id }).open(); }
async function remove(row: HrmEmployeeSalaryApi.EmployeeSalary) {
  await deleteEmployeeSalary(row.id!);
  message.success('删除成功');
  refresh();
}
const [Grid, gridApi] = useVbenVxeGrid({
  formOptions: { schema: useGridFormSchema() },
  gridOptions: {
    columns: useGridColumns(), height: 'auto', keepSource: true,
    proxyConfig: { ajax: { query: async ({ page }, formValues) =>
      getEmployeeSalaryPage({ pageNo: page.currentPage, pageSize: page.pageSize, ...formValues }) } },
    rowConfig: { keyField: 'id', isHover: true },
    toolbarConfig: { refresh: true, search: true },
  } as VxeTableGridOptions<HrmEmployeeSalaryApi.EmployeeSalary>,
});
</script>
<template>
  <Page auto-content-height>
    <FormModal @success="refresh" />
    <Grid table-title="员工薪酬档案">
      <template #toolbar-tools>
        <TableAction :actions="[{ label: '新增档案', type: 'primary', icon: ACTION_ICON.ADD,
          auth: ['hrm:salary:create'], onClick: create }]" />
      </template>
      <template #status="{ row }">
        <Tag :color="row.status === 0 ? 'success' : 'default'">
          {{ row.status === 0 ? '启用' : '禁用' }}
        </Tag>
      </template>
      <template #actions="{ row }">
        <TableAction :actions="[
          { label: '编辑', type: 'link', icon: ACTION_ICON.EDIT,
            auth: ['hrm:salary:update'], onClick: edit.bind(null, row) },
          { label: '删除', type: 'link', danger: true, icon: ACTION_ICON.DELETE,
            auth: ['hrm:salary:delete'], popConfirm: {
              title: '确定删除该薪酬档案吗?', confirm: remove.bind(null, row),
            } },
        ]" />
      </template>
    </Grid>
  </Page>
</template>
src/views/hrm/salary/employee-salary/modules/form.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,59 @@
<script lang="ts" setup>
import type { HrmEmployeeSalaryApi } from '#/api/hrm/salary/employee-salary';
import { computed, ref } from 'vue';
import { useVbenModal } from '#/packages/effects/common-ui/src';
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
  createEmployeeSalary, getEmployeeSalary, updateEmployeeSalary,
} from '#/api/hrm/salary/employee-salary';
import { useFormSchema } from '../data';
const emit = defineEmits(['success']);
const isEdit = ref(false);
const title = computed(() => isEdit.value ? '修改员工薪酬档案' : '新增员工薪酬档案');
const [Form, formApi] = useVbenForm({
  commonConfig: { componentProps: { class: 'w-full' }, formItemClass: 'col-span-1', labelWidth: 110 },
  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();
    try {
      const data = await formApi.getValues() as HrmEmployeeSalaryApi.EmployeeSalary;
      if (isEdit.value) await updateEmployeeSalary(data);
      else await createEmployeeSalary(data);
      await modalApi.close();
      message.success('保存成功');
      emit('success');
    } finally {
      modalApi.unlock();
    }
  },
  async onOpenChange(open: boolean) {
    if (!open) return;
    const { id } = modalApi.getData<{ id?: number }>();
    isEdit.value = !!id;
    await formApi.resetForm();
    if (id) {
      modalApi.lock();
      try { await formApi.setValues(await getEmployeeSalary(id)); }
      finally { modalApi.unlock(); }
    } else {
      await formApi.setValues({ baseSalary: 0, status: 0 });
    }
  },
});
</script>
<template>
  <Modal :title="title" class="w-[760px]">
    <Form class="mx-4" />
  </Modal>
</template>
src/views/hrm/salary/structure/data.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,154 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { HrmSalaryStructureApi } from '#/api/hrm/salary/structure';
export type FormType = 'create' | 'update';
const statusOptions = [
  { label: '启用', value: 0 },
  { label: '禁用', value: 1 },
];
export function useFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'id',
      component: 'Input',
      dependencies: { triggerFields: [''], show: () => false },
    },
    {
      fieldName: 'name',
      label: '结构名称',
      component: 'Input',
      componentProps: {
        maxlength: 100,
        placeholder: '例如:标准员工薪酬、管理层薪酬',
        showCount: true,
      },
      rules: 'required',
      formItemClass: 'col-span-2',
    },
    {
      fieldName: 'baseSalary',
      label: '基本工资',
      component: 'InputNumber',
      componentProps: {
        addonAfter: '元',
        min: 0,
        precision: 2,
        style: { width: '100%' },
      },
      rules: 'required',
    },
    {
      fieldName: 'performanceRatio',
      label: '绩效占比',
      component: 'InputNumber',
      componentProps: {
        addonAfter: '%',
        min: 0,
        max: 100,
        precision: 2,
        style: { width: '100%' },
      },
    },
    {
      fieldName: 'overtimePayRatio',
      label: '加班倍率',
      component: 'InputNumber',
      componentProps: {
        addonAfter: '倍',
        max: 10,
        min: 0,
        precision: 2,
        step: 0.1,
        style: { width: '100%' },
      },
    },
    {
      fieldName: 'socialSecurityBase',
      label: '社保基数',
      component: 'InputNumber',
      componentProps: { addonAfter: '元', min: 0, precision: 2, style: { width: '100%' } },
    },
    {
      fieldName: 'housingFundBase',
      label: '公积金基数',
      component: 'InputNumber',
      componentProps: { addonAfter: '元', min: 0, precision: 2, style: { width: '100%' } },
    },
    {
      fieldName: 'status',
      label: '状态',
      component: 'RadioGroup',
      componentProps: { options: statusOptions },
      rules: 'required',
    },
    {
      fieldName: 'remark',
      label: '备注',
      component: 'Textarea',
      componentProps: {
        maxlength: 500,
        placeholder: '填写适用岗位、职级或特殊计薪说明',
        rows: 3,
        showCount: true,
      },
      formItemClass: 'col-span-2',
    },
  ];
}
export function useGridFormSchema(): VbenFormSchema[] {
  return [
    {
      fieldName: 'name',
      label: '结构名称',
      component: 'Input',
      componentProps: {
        allowClear: true,
        placeholder: '请输入结构名称',
      },
    },
    {
      fieldName: 'status',
      label: '状态',
      component: 'Select',
      componentProps: {
        allowClear: true,
        options: statusOptions,
        placeholder: '请选择状态',
      },
    },
  ];
}
export function useGridColumns(): VxeTableGridOptions<HrmSalaryStructureApi.SalaryStructure>['columns'] {
  return [
    { field: 'name', title: '结构名称', minWidth: 180 },
    { field: 'baseSalary', title: '基本工资', width: 125, formatter: 'formatAmount2' },
    { field: 'performanceRatio', title: '绩效占比', width: 110, slots: { default: 'performanceRatio' } },
    { field: 'overtimePayRatio', title: '加班倍率', width: 110, slots: { default: 'overtimePayRatio' } },
    { field: 'socialSecurityBase', title: '社保基数', width: 125, formatter: 'formatAmount2' },
    { field: 'housingFundBase', title: '公积金基数', width: 125, formatter: 'formatAmount2' },
    {
      field: 'status',
      title: '状态',
      width: 90,
      slots: { default: 'status' },
    },
    {
      field: 'createTime',
      title: '创建时间',
      width: 180,
      formatter: 'formatDate',
    },
    {
      field: 'actions',
      title: '操作',
      fixed: 'right',
      slots: { default: 'actions' },
      width: 150,
    },
  ];
}
src/views/hrm/salary/structure/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,133 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { HrmSalaryStructureApi } from '#/api/hrm/salary/structure';
import { Page, useVbenModal } from '#/packages/effects/common-ui/src';
import { Alert, message, Tag } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
  deleteSalaryStructure,
  getSalaryStructurePage,
} from '#/api/hrm/salary/structure';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
defineOptions({ name: 'HrmSalaryStructure' });
const [FormModal, formModalApi] = useVbenModal({
  connectedComponent: Form,
  destroyOnClose: true,
});
function handleRefresh() {
  gridApi.query();
}
function handleCreate() {
  formModalApi.setData({ formType: 'create' }).open();
}
function handleEdit(row: HrmSalaryStructureApi.SalaryStructure) {
  formModalApi.setData({ id: row.id, formType: 'update' }).open();
}
async function handleDelete(row: HrmSalaryStructureApi.SalaryStructure) {
  await deleteSalaryStructure(row.id!);
  message.success($t('ui.actionMessage.deleteSuccess', ['薪酬结构']));
  handleRefresh();
}
function formatRatio(value?: number, suffix = '倍') {
  return value === undefined || value === null ? '-' : `${Number(value).toFixed(2)} ${suffix}`;
}
const [Grid, gridApi] = useVbenVxeGrid({
  formOptions: { schema: useGridFormSchema() },
  gridOptions: {
    columns: useGridColumns(),
    height: 'auto',
    keepSource: true,
    proxyConfig: {
      ajax: {
        query: async ({ page }, formValues) =>
          await getSalaryStructurePage({
            pageNo: page.currentPage,
            pageSize: page.pageSize,
            ...formValues,
          }),
      },
    },
    rowConfig: { keyField: 'id', isHover: true },
    toolbarConfig: { refresh: true, search: true },
  } as VxeTableGridOptions<HrmSalaryStructureApi.SalaryStructure>,
});
</script>
<template>
  <Page auto-content-height>
    <FormModal @success="handleRefresh" />
    <Alert
      class="mb-4"
      description="按当前工程的薪酬模型维护结构标准;员工个人金额在员工薪酬档案中维护。"
      message="薪酬结构"
      show-icon
      type="info"
    />
    <Grid table-title="薪酬结构列表">
      <template #toolbar-tools>
        <TableAction
          :actions="[
            {
              label: $t('ui.actionTitle.create', ['薪酬结构']),
              type: 'primary',
              icon: ACTION_ICON.ADD,
              auth: ['hrm:salary:create'],
              onClick: handleCreate,
            },
          ]"
        />
      </template>
      <template #performanceRatio="{ row }">
        {{ formatRatio(row.performanceRatio, '%') }}
      </template>
      <template #overtimePayRatio="{ row }">
        <Tag :color="(row.overtimePayRatio ?? 0) > 0 ? 'blue' : 'default'">
          {{ (row.overtimePayRatio ?? 0) > 0 ? formatRatio(row.overtimePayRatio) : '不计加班工资' }}
        </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:salary:update'],
              onClick: handleEdit.bind(null, row),
            },
            {
              label: $t('common.delete'),
              type: 'link',
              danger: true,
              icon: ACTION_ICON.DELETE,
              auth: ['hrm:salary:delete'],
              popConfirm: {
                title: '确认删除该薪酬结构吗?已被员工使用的结构不可删除。',
                confirm: handleDelete.bind(null, row),
              },
            },
          ]"
        />
      </template>
    </Grid>
  </Page>
</template>
src/views/hrm/salary/structure/modules/form.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,95 @@
<script lang="ts" setup>
import type { HrmSalaryStructureApi } from '#/api/hrm/salary/structure';
import type { FormType } from '../data';
import { computed, ref } from 'vue';
import { useVbenModal } from '#/packages/effects/common-ui/src';
import { Alert, message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import {
  createSalaryStructure,
  getSalaryStructure,
  updateSalaryStructure,
} from '#/api/hrm/salary/structure';
import { $t } from '#/locales';
import { useFormSchema } from '../data';
const emit = defineEmits(['success']);
const formType = ref<FormType>('create');
const getTitle = computed(() =>
  formType.value === 'update' ? '修改薪酬结构' : '新增薪酬结构',
);
const [Form, formApi] = useVbenForm({
  commonConfig: {
    componentProps: { class: 'w-full' },
    formItemClass: 'col-span-1',
    labelWidth: 110,
  },
  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();
    try {
      const data =
        (await formApi.getValues()) as HrmSalaryStructureApi.SalaryStructure;
      if (data.id) {
        await updateSalaryStructure(data);
      } else {
        await createSalaryStructure(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<{ formType: FormType; id?: number }>();
    formType.value = data.formType;
    await formApi.resetForm();
    if (!data.id) {
      await formApi.setValues({
        baseSalary: 0,
        overtimePayRatio: 1.5,
        performanceRatio: 0,
        status: 0,
      });
      return;
    }
    modalApi.lock();
    try {
      await formApi.setValues(await getSalaryStructure(data.id));
    } finally {
      modalApi.unlock();
    }
  },
});
</script>
<template>
  <Modal :title="getTitle" class="w-[680px]">
    <Alert
      class="mx-4 mb-4"
      description="维护本工程支持的基本工资、绩效占比、加班倍率及社保和公积金基数。员工个人薪酬可在员工薪酬档案中单独维护。"
      message="薪酬结构说明"
      show-icon
      type="info"
    />
    <Form class="mx-4" />
  </Modal>
</template>
src/views/wls/barcode/components/scan-modal.vue
@@ -67,7 +67,9 @@
      content.value.trim(),
      parseResult.value?.format,
    );
    imageBase64.value = base64 ? `data:image/png;base64,${base64}` : '';
    imageBase64.value = base64
      ? (base64.startsWith('data:image/') ? base64 : `data:image/png;base64,${base64}`)
      : '';
    if (!base64) {
      message.warning('成图失败,请检查条码内容是否符合所选格式');
    }
src/views/wls/barcode/config/index.vue
@@ -33,15 +33,20 @@
/** è‡ªåŠ¨ç”Ÿæˆå¼€å…³å˜æ›´ */
async function handleAutoGenerateChange(
  newValue: boolean,
  row: MesWmBarcodeConfigApi.BarcodeConfig,
): Promise<boolean> {
  const text = row.autoGenerateFlag ? '启用' : '停用';
  if (!row.id) {
    message.error('条码配置编号缺失,请刷新后重试');
    return false;
  }
  const text = newValue ? '启用' : '停用';
  try {
    await confirm(`确认要${text}自动生成吗?`);
  } catch {
    return false;
  }
  await updateBarcodeConfig(row);
  await updateBarcodeConfig({ ...row, autoGenerateFlag: newValue });
  message.success(`${text}成功`);
  return true;
}
src/views/wls/barcode/data.ts
@@ -614,6 +614,7 @@
/** é…ç½®åˆ—表的字段 */
export function useConfigGridColumns(
  onAutoGenerateChange: (
    newValue: boolean,
    row: MesWmBarcodeConfigApi.BarcodeConfig,
  ) => Promise<boolean>,
): VxeTableGridOptions<MesWmBarcodeConfigApi.BarcodeConfig>['columns'] {
@@ -663,6 +664,7 @@
      cellRender: {
        name: 'CellSwitch',
        attrs: { beforeChange: onAutoGenerateChange },
        props: { checkedValue: true, unCheckedValue: false },
      },
    },
    {
src/views/wls/barcode/index.vue
@@ -149,12 +149,12 @@
/** è·³è½¬æ¡ç é…ç½® */
function handleConfig() {
  router.push({ path: '/mes/wm/barcode/config' });
  router.push({ path: '/wls/barcode/config' });
}
/** æ‰“开完整批次追溯清单 */
function handleTraceCenter() {
  router.push({ path: '/mes/qc/batch-trace' });
  router.push({ path: '/qc/batch-trace' });
}
/** å¯¼å‡ºè¡¨æ ¼ */
src/views/wls/sn/index.vue
@@ -208,8 +208,14 @@
    <OutStockModalRef @success="handleRefresh" />
    <SnDetailModal ref="snDetailModalRef" />
    <Tabs v-model:active-key="activeKey" class="px-4 pt-1" type="card">
      <Tabs.TabPane key="group" tab="SN ç åˆ†ç»„">
    <div class="sn-page flex h-full min-h-0 flex-col">
      <Tabs
        v-model:active-key="activeKey"
        class="sn-tabs min-h-0 flex-1 px-4 pt-1"
        type="card"
      >
        <Tabs.TabPane key="group" class="min-h-0" tab="SN ç åˆ†ç»„">
          <div class="h-full min-h-0">
        <Grid table-title="SN ç åˆ—表">
          <template #toolbar-tools>
            <TableAction
@@ -274,8 +280,10 @@
            />
          </template>
        </Grid>
          </div>
      </Tabs.TabPane>
      <Tabs.TabPane key="in-stock" tab="在库单件">
        <Tabs.TabPane key="in-stock" class="min-h-0" tab="在库单件">
          <div class="h-full min-h-0">
        <InStockGrid table-title="SN å•件状态列表">
          <template #toolbar-tools>
            <TableAction
@@ -329,7 +337,23 @@
            />
          </template>
        </InStockGrid>
          </div>
      </Tabs.TabPane>
    </Tabs>
    </div>
  </Page>
</template>
<style scoped>
.sn-tabs :deep(.ant-tabs-content-holder) {
  min-height: 0;
  flex: 1;
  overflow: hidden;
}
.sn-tabs :deep(.ant-tabs-content),
.sn-tabs :deep(.ant-tabs-tabpane) {
  height: 100%;
  min-height: 0;
}
</style>