From d8ae565b4ae8c536396d74e9473e3726083c3c84 Mon Sep 17 00:00:00 2001
From: dzq <dzq@ys.com>
Date: Wed, 2 Apr 2025 09:34:17 +0800
Subject: [PATCH] =?UTF-8?q?=E4=BC=81=E4=B8=9A=E5=BE=AE=E4=BF=A1=E7=94=A8?=
 =?UTF-8?q?=E6=88=B7=E7=AE=A1=E7=90=86?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/api/qy/qyUser.ts                          |  12 +-
 src/api/system/dept.ts                        |   1 +
 src/views/system/qyUser/hook.tsx              | 114 -------------
 src/views/user/qy/BalanceEditModal.vue        |  78 +++++++++
 .../{system/qyUser => user/qy}/UserDetail.vue |   0
 src/views/user/qy/hook.tsx                    | 151 ++++++++++++++++++
 .../{system/qyUser => user/qy}/index.vue      |  62 +++----
 src/views/{system/qyUser => user/qy}/tree.vue |   0
 8 files changed, 273 insertions(+), 145 deletions(-)
 delete mode 100644 src/views/system/qyUser/hook.tsx
 create mode 100644 src/views/user/qy/BalanceEditModal.vue
 rename src/views/{system/qyUser => user/qy}/UserDetail.vue (100%)
 create mode 100644 src/views/user/qy/hook.tsx
 rename src/views/{system/qyUser => user/qy}/index.vue (73%)
 rename src/views/{system/qyUser => user/qy}/tree.vue (100%)

diff --git a/src/api/qy/qyUser.ts b/src/api/qy/qyUser.ts
index 99c41a1..b952f9e 100644
--- a/src/api/qy/qyUser.ts
+++ b/src/api/qy/qyUser.ts
@@ -50,6 +50,8 @@ export interface QyUserDTO {
   corpid?: string;
   /** 应用ID(导出列:应用ID) */
   appid?: string;
+  /** 用户余额 */
+  balance?: number;
 }
 
 export interface QyUserQuery extends BasePageQuery {
@@ -62,10 +64,12 @@ export interface QyUserQuery extends BasePageQuery {
 }
 
 export interface AddQyUserCommand {
-  name: string;
-  mobile: string;
-  department: string;
-  corpid: string;
+  name?: string;
+  mobile?: string;
+  department?: string;
+  corpid?: string;
+  /** 用户余额 */
+  balance?: number;
 }
 
 export interface UpdateQyUserCommand extends AddQyUserCommand {
diff --git a/src/api/system/dept.ts b/src/api/system/dept.ts
index 52b370a..54c8509 100644
--- a/src/api/system/dept.ts
+++ b/src/api/system/dept.ts
@@ -12,6 +12,7 @@ export interface DeptQuery extends BaseQuery {
 export interface DeptDTO {
   createTime?: Date;
   id?: number;
+  departmentId?: string;
   deptName?: string;
   email?: string;
   leaderName?: string;
diff --git a/src/views/system/qyUser/hook.tsx b/src/views/system/qyUser/hook.tsx
deleted file mode 100644
index fe95bc5..0000000
--- a/src/views/system/qyUser/hook.tsx
+++ /dev/null
@@ -1,114 +0,0 @@
-import dayjs from "dayjs";
-import { message } from "@/utils/message";
-import {
-  QyUserQuery,
-  getQyUserListApi
-} from "@/api/qy/qyUser";
-import { ElMessageBox } from "element-plus";
-import { type PaginationProps } from "@pureadmin/table";
-import { reactive, ref, computed, onMounted, toRaw, h } from "vue";
-import { CommonUtils } from "@/utils/common";
-import { addDialog } from "@/components/ReDialog";
-import userDetail from "./UserDetail.vue";
-import { handleTree, setDisabledForTreeOptions } from "@/utils/tree";
-import { getQyDeptListApi } from "@/api/system/dept";
-import { getPostListApi } from "@/api/system/post";
-import { getRoleListApi } from "@/api/system/role";
-import { useWxStore } from "@/store/modules/wx";
-
-export function useHook() {
-  const wxStore = useWxStore();
-
-  const searchFormParams = reactive<QyUserQuery>({
-    /** 姓名(导出列:姓名) */
-    name: undefined,
-    /** 手机号(导出列:联系方式) */
-    mobile: undefined,
-    corpid: wxStore.corpid,
-    mainDepartment: undefined,
-  });
-
-  const formRef = ref();
-  const timeRange = ref<[string, string]>();
-
-  const dataList = ref([]);
-  const pageLoading = ref(true);
-  const switchLoadMap = ref({});
-  const pagination = reactive<PaginationProps>({
-    total: 0,
-    pageSize: 8,
-    currentPage: 1,
-    background: true
-  });
-
-  const deptTreeList = ref([]);
-  const postOptions = ref([]);
-  const roleOptions = ref([]);
-  const buttonClass = computed(() => {
-    return [
-      "!h-[20px]",
-      "reset-margin",
-      "!text-gray-500",
-      "dark:!text-white",
-      "dark:hover:!text-primary"
-    ];
-  });
-
-
-  async function onSearch() {
-    // 点击搜索的时候 需要重置分页
-    pagination.currentPage = 1;
-    getList();
-  }
-
-  async function getList() {
-    CommonUtils.fillPaginationParams(searchFormParams, pagination);
-    CommonUtils.fillTimeRangeParams(searchFormParams, timeRange.value);
-
-    pageLoading.value = true;
-    const { data } = await getQyUserListApi(toRaw(searchFormParams)).finally(
-      () => {
-        pageLoading.value = false;
-      }
-    );
-
-    dataList.value = data.rows;
-    pagination.total = data.total;
-  }
-
-  const resetForm = formEl => {
-    if (!formEl) return;
-    formEl.resetFields();
-    onSearch();
-  };
-
-  onMounted(async () => {
-    onSearch();
-    const deptResponse = await getQyDeptListApi(wxStore.corpid);
-    deptTreeList.value = await setDisabledForTreeOptions(
-      handleTree(deptResponse.data),
-      "status"
-    );
-
-    const postResponse = await getPostListApi({});
-    postOptions.value = postResponse.data.rows;
-
-    const roleResponse = await getRoleListApi({});
-    roleOptions.value = roleResponse.data.rows;
-  });
-
-  const handleViewDetail = (row: any) => {
-  };
-
-  return {
-    searchFormParams,
-    pageLoading,
-    dataList,
-    pagination,
-    buttonClass,
-    onSearch,
-    resetForm,
-    getList,
-    handleViewDetail
-  };
-}
diff --git a/src/views/user/qy/BalanceEditModal.vue b/src/views/user/qy/BalanceEditModal.vue
new file mode 100644
index 0000000..3c36495
--- /dev/null
+++ b/src/views/user/qy/BalanceEditModal.vue
@@ -0,0 +1,78 @@
+<script setup lang="ts">
+import { ref, reactive, watch } from "vue";
+import { ElMessage } from "element-plus";
+import { useRenderIcon } from "@/components/ReIcon/src/hooks";
+import { updateQyUserApi } from "@/api/qy/qyUser";
+import Confirm from "@iconify-icons/ep/check";
+import type { FormRules } from 'element-plus';
+
+const props = defineProps({
+  visible: {
+    type: Boolean,
+    default: false
+  },
+  row: {
+    type: Object,
+    default: null
+  }
+});
+
+const emit = defineEmits(["update:visible", "refresh"]);
+
+const formRef = ref();
+const formData = reactive({
+  balance: 0
+});
+
+const rules = reactive<FormRules>({
+  balance: [
+    { required: true, message: "请输入金额", trigger: "blur" },
+    { type: 'number', message: '必须为数字类型' },
+    { validator: (_, v, cb) => v >= 0 ? cb() : cb(new Error('金额不能小于0')), trigger: 'blur' }
+  ]
+});
+
+const handleConfirm = async () => {
+  try {
+    await formRef.value.validate();
+    await updateQyUserApi(props.row.id, {
+      id: props.row.id,
+      balance: formData.balance
+    });
+    ElMessage.success("余额修改成功");
+    emit("refresh");
+    closeDialog();
+  } catch (error) {
+    console.error("表单提交失败", error);
+  }
+};
+
+const closeDialog = () => {
+  formRef.value.resetFields();
+  emit("update:visible", false);
+};
+
+watch(() => props.row, (val) => {
+  if (val) {
+    formData.balance = val.balance || 0;
+  }
+}, { immediate: true });
+</script>
+
+<template>
+  <el-dialog title="修改余额" :model-value="visible" width="500px" @close="closeDialog">
+    <el-form ref="formRef" :model="formData" :rules="rules" label-width="80px">
+      <el-form-item label="金额" prop="balance">
+        <el-input-number v-model="formData.balance" :precision="2" :step="0.1" :min="0" controls-position="right"
+          class="!w-[200px]" />
+      </el-form-item>
+    </el-form>
+
+    <template #footer>
+      <el-button @click="closeDialog">取消</el-button>
+      <el-button type="primary" :icon="useRenderIcon(Confirm)" @click="handleConfirm">
+        确认
+      </el-button>
+    </template>
+  </el-dialog>
+</template>
\ No newline at end of file
diff --git a/src/views/system/qyUser/UserDetail.vue b/src/views/user/qy/UserDetail.vue
similarity index 100%
rename from src/views/system/qyUser/UserDetail.vue
rename to src/views/user/qy/UserDetail.vue
diff --git a/src/views/user/qy/hook.tsx b/src/views/user/qy/hook.tsx
new file mode 100644
index 0000000..8201bf8
--- /dev/null
+++ b/src/views/user/qy/hook.tsx
@@ -0,0 +1,151 @@
+import {
+  QyUserDTO,
+  QyUserQuery,
+  getQyUserListApi,
+  updateQyUserApi
+} from "@/api/qy/qyUser";
+import { ElMessage } from "element-plus";
+import { type PaginationProps } from "@pureadmin/table";
+import { reactive, ref, onMounted, toRaw, h, onBeforeUnmount } from "vue";
+import { CommonUtils } from "@/utils/common";
+import { handleTree, setDisabledForTreeOptions } from "@/utils/tree";
+import { getQyDeptListApi } from "@/api/system/dept";
+import { getPostListApi } from "@/api/system/post";
+import { getRoleListApi } from "@/api/system/role";
+import { useWxStore } from "@/store/modules/wx";
+
+/**
+ * 企业用户管理页面的组合式函数
+ * 提供用户列表查询、分页、部门/岗位/角色数据获取、余额修改等功能
+ */
+export function useHook() {
+  const wxStore = useWxStore(); // 微信相关状态管理
+
+  // 搜索表单参数
+  const searchFormParams = reactive<QyUserQuery>({
+    /** 姓名(导出列:姓名) */
+    name: undefined,
+    /** 手机号(导出列:联系方式) */
+    mobile: undefined,
+    corpid: wxStore.corpid, // 企业ID
+    mainDepartment: undefined, // 主部门
+  });
+
+  const timeRange = ref<[string, string]>(); // 时间范围选择
+
+  // 列表相关状态
+  const dataList = ref([]); // 用户列表数据
+  const pageLoading = ref(true); // 加载状态
+  const pagination = reactive<PaginationProps>({ // 分页配置
+    total: 0,
+    pageSize: 8,
+    currentPage: 1,
+    background: true
+  });
+
+  // 余额修改相关状态
+  const BalanceEditModal = ref(null); // 余额编辑模态框引用
+  const balanceVisible = ref(false); // 余额弹窗可见性
+  const currentBalance = ref(0); // 当前编辑的余额值
+  const selectedUserId = ref<number>(); // 当前选中的用户ID
+  const selectedUser = ref<QyUserDTO>(); // 当前选中的用户对象
+
+  // 下拉选项数据
+  const deptTreeList = ref([]); // 部门树形数据
+  const postOptions = ref([]); // 岗位选项
+  const roleOptions = ref([]); // 角色选项
+
+  /**
+   * 执行搜索操作
+   * 重置分页后重新获取列表数据
+   */
+  async function onSearch() {
+    pagination.currentPage = 1;
+    getList();
+  }
+
+  /**
+   * 获取用户列表数据
+   * 处理分页参数和时间范围参数后调用API
+   */
+  async function getList() {
+    CommonUtils.fillPaginationParams(searchFormParams, pagination);
+    CommonUtils.fillTimeRangeParams(searchFormParams, timeRange.value);
+
+    pageLoading.value = true;
+    const { data } = await getQyUserListApi(toRaw(searchFormParams)).finally(
+      () => {
+        pageLoading.value = false;
+      }
+    );
+
+    dataList.value = data.rows;
+    pagination.total = data.total;
+  }
+
+  /**
+   * 重置搜索表单
+   * @param formEl 表单元素引用
+   */
+  const resetForm = formEl => {
+    if (!formEl) return;
+    formEl.resetFields();
+    onSearch();
+  };
+
+  // 组件挂载时执行
+  onMounted(async () => {
+    onSearch(); // 初始加载列表数据
+
+    // 获取部门树形数据
+    const deptResponse = await getQyDeptListApi(wxStore.corpid);
+    deptTreeList.value = await setDisabledForTreeOptions(
+      handleTree(deptResponse.data),
+      "status"
+    );
+
+    // 获取岗位选项
+    const postResponse = await getPostListApi({});
+    postOptions.value = postResponse.data.rows;
+
+    // 获取角色选项
+    const roleResponse = await getRoleListApi({});
+    roleOptions.value = roleResponse.data.rows;
+  });
+
+  /**
+   * 查看用户详情
+   * @param row 用户数据行
+   */
+  const handleViewDetail = (row: any) => {
+    // TODO: 实现查看详情逻辑
+  };
+
+  /**
+   * 打开修改余额弹窗
+   * @param row 用户数据行
+   */
+  const handleModifyBalance = (row: QyUserDTO) => {
+    selectedUserId.value = row.id;
+    currentBalance.value = row.balance || 0;
+    selectedUser.value = row;
+    balanceVisible.value = true;
+  };
+
+
+  // 暴露给模板使用的属性和方法
+  return {
+    BalanceEditModal,
+    searchFormParams,
+    pageLoading,
+    dataList,
+    pagination,
+    onSearch,
+    resetForm,
+    getList,
+    handleViewDetail,
+    handleModifyBalance,
+    balanceVisible,
+    selectedUser
+  };
+}
diff --git a/src/views/system/qyUser/index.vue b/src/views/user/qy/index.vue
similarity index 73%
rename from src/views/system/qyUser/index.vue
rename to src/views/user/qy/index.vue
index ad20865..9a32b6a 100644
--- a/src/views/system/qyUser/index.vue
+++ b/src/views/user/qy/index.vue
@@ -2,19 +2,11 @@
 import { ref, watch } from "vue";
 import tree from "./tree.vue";
 import { useHook } from "./hook";
-import { PureTableBar } from "@/components/RePureTableBar";
 import { useRenderIcon } from "@/components/ReIcon/src/hooks";
+import BalanceEditModal from "./BalanceEditModal.vue";
 
-import Password from "@iconify-icons/ri/lock-password-line";
-import More from "@iconify-icons/ep/more-filled";
-import Delete from "@iconify-icons/ep/delete";
-import EditPen from "@iconify-icons/ep/edit-pen";
-import Download from "@iconify-icons/ep/download";
-import Upload from "@iconify-icons/ep/upload";
 import Search from "@iconify-icons/ep/search";
 import Refresh from "@iconify-icons/ep/refresh";
-import AddFill from "@iconify-icons/ri/add-circle-line";
-import { useUserStoreHook } from "@/store/modules/user";
 
 defineOptions({
   name: "QyUser"
@@ -23,14 +15,16 @@ defineOptions({
 const formRef = ref();
 const {
   searchFormParams,
+  onSearch,
   pageLoading,
   dataList,
   pagination,
-  buttonClass,
-  onSearch,
-  resetForm,
   getList,
-  handleViewDetail
+  resetForm,
+  handleViewDetail,
+  handleModifyBalance,
+  balanceVisible,
+  selectedUser
 } = useHook();
 
 watch(
@@ -45,6 +39,7 @@ watch(
   <div class="main">
     <tree class="w-[17%] float-left" v-model="searchFormParams.mainDepartment" />
     <div class="float-right w-[82%]">
+      <BalanceEditModal v-model:visible="balanceVisible" :row="selectedUser" @refresh="getList" />
       <el-form ref="formRef" :inline="true" :model="searchFormParams"
         class="search-form bg-bg_color w-[99/100] pl-8 pt-[12px]">
         <el-form-item label="姓名:" prop="name">
@@ -62,19 +57,29 @@ watch(
 
       <div class="grid-container">
         <el-row :gutter="20">
-          <el-col v-for="(item, index) in dataList" :key="index" :xs="24" :sm="12" :md="8" :lg="6">
+          <el-col v-for="(item, index) in dataList" :key="item.id" :xs="24" :sm="12" :md="8" :lg="6">
             <el-card class="user-card">
               <div class="card-content">
                 <el-avatar :size="60" :src="item.avatar" class="avatar" />
                 <div class="user-info">
                   <div class="name">{{ item.name }}</div>
                   <div class="gender">性别:{{ item.gender === '1' ? '男' : item.gender === '2' ? '女' : '' }}</div>
-                  <div class="create-time">创建时间:{{ item.createTime }}</div>
+                  <div class="create-time">创建时间:{{ item.createTimeStr }}</div>
+                  <div class="balance">余额:¥{{ item.balance?.toFixed(2) }}</div>
                 </div>
               </div>
-              <el-button type="primary" size="small" class="detail-btn" @click="handleViewDetail(item)">
-                查看详情
-              </el-button>
+              <el-row :gutter="10">
+                <el-col :span="12">
+                  <el-button type="primary" size="small" class="detail-btn" @click="handleViewDetail(item)">
+                    查看详情
+                  </el-button>
+                </el-col>
+                <el-col :span="12">
+                  <el-button type="warning" size="small" class="detail-btn" @click="handleModifyBalance(item)">
+                    修改余额
+                  </el-button>
+                </el-col>
+              </el-row>
             </el-card>
           </el-col>
         </el-row>
@@ -117,8 +122,6 @@ watch(
     }
 
     .user-info {
-      flex: 1;
-
       .name {
         font-size: 16px;
         font-weight: 500;
@@ -131,6 +134,14 @@ watch(
         color: #909399;
         line-height: 1.5;
       }
+
+      .balance {
+        font-family: monospace;
+        font-size: 14px;
+        color: #67c23a;
+        margin-bottom: 6px;
+        line-height: 1.5;
+      }
     }
   }
 
@@ -151,14 +162,11 @@ watch(
 }
 
 .pagination-wrapper {
-  position: fixed;
-  bottom: 0;
-  left: 0;
-  right: 0;
+  position: relative;
   background: var(--el-bg-color);
-  padding: 12px 20px;
-  box-shadow: 0 -2px 12px rgba(0, 0, 0, 0.05);
-  z-index: 10;
+  padding: 12px 12px;
+  margin-top: 20px;
+  text-align: center;
 
   :deep(.el-pagination) {
     margin: 0;
diff --git a/src/views/system/qyUser/tree.vue b/src/views/user/qy/tree.vue
similarity index 100%
rename from src/views/system/qyUser/tree.vue
rename to src/views/user/qy/tree.vue