From 0e9563993ac594b9d89ae5537ebfe60e62a07242 Mon Sep 17 00:00:00 2001
From: tuqin <tuqin@tuqin.cn>
Date: 星期五, 23 十月 2026 12:09:13 +0800
Subject: [PATCH] feat: 薪酬核算列表行内编辑、工单处理人下拉搜索、社保比例两位小数、退货客户可编辑

---
 src/views/aftersales/ticket/data.ts                   |   16 ++++-
 src/views/aftersales/return/data.ts                   |    4 
 src/views/hrm/salary/social-security-scheme/index.vue |    4 
 src/views/hrm/salary/calculation/data.ts              |   30 +++++-----
 src/views/hrm/salary/social-security-scheme/data.ts   |    7 +-
 src/views/hrm/salary/calculation/index.vue            |   69 ++++++++++++++++++++++
 src/views/aftersales/ticket/detail.vue                |   46 ++++++++++----
 7 files changed, 135 insertions(+), 41 deletions(-)

diff --git a/src/views/aftersales/return/data.ts b/src/views/aftersales/return/data.ts
index 5acd874..3174429 100644
--- a/src/views/aftersales/return/data.ts
+++ b/src/views/aftersales/return/data.ts
@@ -44,9 +44,9 @@
       label: '瀹㈡埛',
       component: 'Input',
       componentProps: {
-        disabled: true,
-        placeholder: '瀹㈡埛',
+        placeholder: '璇疯緭鍏ュ鎴峰悕绉�',
       },
+      rules: 'required',
     },
     {
       fieldName: 'saleOrderId',
diff --git a/src/views/aftersales/ticket/data.ts b/src/views/aftersales/ticket/data.ts
index 9b0d34d..aefa638 100644
--- a/src/views/aftersales/ticket/data.ts
+++ b/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',
     },
diff --git a/src/views/aftersales/ticket/detail.vue b/src/views/aftersales/ticket/detail.vue
index 092ebc2..d87527d 100644
--- a/src/views/aftersales/ticket/detail.vue
+++ b/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="澶勭悊浜篒D"
-              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>
diff --git a/src/views/hrm/salary/calculation/data.ts b/src/views/hrm/salary/calculation/data.ts
index c02d031..d52945d 100644
--- a/src/views/hrm/salary/calculation/data.ts
+++ b/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: '绀句繚鎵f', width: 100, formatter: 'formatAmount2' },
-    { field: 'housingFundDeduction', title: '鍏Н閲戞墸娆�', width: 100, formatter: 'formatAmount2' },
-    { field: 'taxDeduction', title: '涓◣鎵f', width: 100, formatter: 'formatAmount2' },
-    { field: 'otherDeduction', title: '鍏朵粬鎵f', width: 80, formatter: 'formatAmount2' },
-    { field: 'leaveDeduction', title: '璇峰亣鎵f', 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: '绀句繚鎵f', width: 120, slots: { default: 'editableCell' } },
+    { field: 'housingFundDeduction', title: '鍏Н閲戞墸娆�', width: 120, slots: { default: 'editableCell' } },
+    { field: 'taxDeduction', title: '涓◣鎵f', width: 120, slots: { default: 'editableCell' } },
+    { field: 'otherDeduction', title: '鍏朵粬鎵f', width: 110, slots: { default: 'editableCell' } },
+    { field: 'leaveDeduction', title: '璇峰亣鎵f', 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: '鐘舵��',
diff --git a/src/views/hrm/salary/calculation/index.vue b/src/views/hrm/salary/calculation/index.vue
index d07d3ea..77e4204 100644
--- a/src/views/hrm/salary/calculation/index.vue
+++ b/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) }}
diff --git a/src/views/hrm/salary/social-security-scheme/data.ts b/src/views/hrm/salary/social-security-scheme/data.ts
index 9a14da2..e05ed44 100644
--- a/src/views/hrm/salary/social-security-scheme/data.ts
+++ b/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;
     },
   };
diff --git a/src/views/hrm/salary/social-security-scheme/index.vue b/src/views/hrm/salary/social-security-scheme/index.vue
index c5dd5c7..45fbaa8 100644
--- a/src/views/hrm/salary/social-security-scheme/index.vue
+++ b/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)}%`;
 }
 
 /** 鏍煎紡鍖栨瘮渚嬫樉绀猴紙淇濈暀涓や綅灏忔暟锛� */

--
Gitblit v1.9.3