Jelajahi Sumber

子账号与角色管理

lxr 2 bulan lalu
induk
melakukan
b878b7034b

+ 44 - 0
src/api/modules/accountRoleManagement.ts

@@ -90,6 +90,10 @@ export const getMenuTree = (): Promise<ResultData<MenuItem[]>> => {
 export const createRole = (createRoleDto: CreateRoleDto): Promise<ResultData<ApiResponse>> => {
   return http.post<ApiResponse>(PORT_NONE + `/platform/role/createRole`, createRoleDto, { loading: false });
 };
+// 编辑角色
+export const updateRole = (updateRoleDto: updateRoleDto): Promise<ResultData<ApiResponse>> => {
+  return http.post<ApiResponse>(PORT_NONE + `/platform/role/updateRole`, updateRoleDto, { loading: false });
+};
 
 // 角色管理列表
 export const getRolePage = (page: number, size: number, storeId: number): Promise<ResultData<MenuItem[]>> => {
@@ -112,3 +116,43 @@ export const createAccountAndAssignRole = ({ accountName, phone, roleId, storeId
     { loading: false }
   );
 };
+// 查询当前店铺下的子账号列表
+export const querySubAccounts = (
+  storeId: number,
+  accountName: string,
+  phone: string,
+  roleName: string
+): Promise<ResultData<ApiResponse>> => {
+  return http.get<ApiResponse>(
+    PORT_NONE + `/platform/user-role/querySubAccounts`,
+    { storeId, accountName, phone, roleName },
+    { loading: false }
+  );
+};
+
+// 删除角色
+export const deleteRoleWithCheck = (roleId: number): Promise<ResultData<ApiResponse>> => {
+  return http.delete<ApiResponse>(PORT_NONE + `/platform/role/deleteRoleWithCheck`, { roleId }, { loading: false });
+};
+// 分页查询操作记录
+export const uoperationLogPage = ({
+  page,
+  size,
+  account,
+  endTime,
+  module,
+  startTime
+}: {
+  page: number;
+  size: number;
+  operationType: string;
+  operationName: string;
+  operationTime: string;
+  operationContent: string;
+}): Promise<ResultData<ApiResponse>> => {
+  return http.post<ApiResponse>(
+    PORT_NONE + `/platform/operationLog/page`,
+    { page, size, account, endTime, module, startTime },
+    { loading: false }
+  );
+};

+ 135 - 28
src/views/accountRoleManagement/roleManagement/create.vue

@@ -38,7 +38,7 @@
               show-checkbox
               node-key="id"
               :default-expand-all="false"
-              :check-strictly="false"
+              :check-strictly="true"
               :default-checked-keys="defaultCheckedKeys"
               :expand-on-click-node="false"
               @check="handlePermissionCheck"
@@ -67,7 +67,14 @@ import { ref, reactive, onMounted, computed, nextTick } from "vue";
 import { useRouter, useRoute } from "vue-router";
 import { ElMessage, type FormInstance, type FormRules } from "element-plus";
 import { ArrowLeft, QuestionFilled } from "@element-plus/icons-vue";
-import { getMenuTree, createRole, type MenuItem } from "@/api/modules/accountRoleManagement";
+import {
+  getMenuTree,
+  createRole,
+  updateRole,
+  getRolePermissionTable,
+  type MenuItem,
+  type RolePermissionItem
+} from "@/api/modules/accountRoleManagement";
 import { localGet } from "@/utils";
 
 // 路由
@@ -75,8 +82,8 @@ const router = useRouter();
 const route = useRoute();
 
 // 判断是编辑还是创建
-const roleId = computed(() => route.params.id as string | undefined);
-const isEdit = computed(() => !!roleId.value);
+const roleId = computed(() => route.query.roleId as string | undefined);
+const isEdit = computed(() => route.query.type === "edit");
 
 // 表单引用
 const roleFormRef = ref<FormInstance>();
@@ -157,13 +164,13 @@ const loadMenuTree = async () => {
 };
 
 // 初始化数据
-onMounted(() => {
+onMounted(async () => {
   // 先加载菜单树数据
-  loadMenuTree();
+  await loadMenuTree();
 
   if (isEdit.value) {
     // 编辑模式:加载角色数据
-    loadRoleData();
+    await loadRoleData();
   } else {
     // 创建模式:初始化空数据
     roleForm.roleName = "";
@@ -171,26 +178,109 @@ onMounted(() => {
   }
 });
 
-// 加载角色数据(编辑时)
-const loadRoleData = () => {
-  // TODO: 调用接口获取角色详情
-  // 模拟数据
-  const mockRoleData = {
-    id: Number(roleId.value),
-    roleName: "店长",
-    permissions: [1, 11, 111, 2] // 选中的权限ID
-  };
-
-  roleForm.roleName = mockRoleData.roleName;
-  roleForm.permissions = mockRoleData.permissions;
-  defaultCheckedKeys.value = mockRoleData.permissions;
-
-  // 设置树形组件的选中状态
-  nextTick(() => {
-    if (permissionTreeRef.value) {
-      permissionTreeRef.value.setCheckedKeys(defaultCheckedKeys.value);
+// 通过权限名称匹配菜单ID(递归查找,支持转换后的树形数据)
+const findMenuIdByName = (menuList: any[], permissionName: string): number | null => {
+  for (const menu of menuList) {
+    if (menu.menuName === permissionName || menu.label === permissionName) {
+      return menu.menuId || menu.id;
+    }
+    if (menu.children && menu.children.length > 0) {
+      const found = findMenuIdByName(menu.children, permissionName);
+      if (found !== null) {
+        return found;
+      }
+    }
+  }
+  return null;
+};
+
+// 从权限详情数据中提取所有菜单ID
+const extractMenuIdsFromPermissions = (permissionList: RolePermissionItem[], menuTree: MenuItem[]): number[] => {
+  const menuIds: number[] = [];
+  const addedIds = new Set<number>();
+
+  permissionList.forEach(permission => {
+    // 处理一级权限
+    if (permission.level1Permission) {
+      const menuId = findMenuIdByName(menuTree, permission.level1Permission);
+      if (menuId !== null && !addedIds.has(menuId)) {
+        menuIds.push(menuId);
+        addedIds.add(menuId);
+      }
+    }
+
+    // 处理二级权限
+    // 注意:如果存在三级权限,不要单独添加二级权限ID,避免因为父子关联导致所有子节点被选中
+    if (permission.level2Permission && !permission.level3Permission) {
+      const menuId = findMenuIdByName(menuTree, permission.level2Permission);
+      if (menuId !== null && !addedIds.has(menuId)) {
+        menuIds.push(menuId);
+        addedIds.add(menuId);
+      }
+    }
+
+    // 处理三级权限
+    // 注意:level3Permission 可能包含多个权限名称(用空格分隔),需要拆分处理
+    if (permission.level3Permission) {
+      // 将 level3Permission 按空格拆分为多个权限名称
+      const level3PermissionNames = permission.level3Permission
+        .split(/\s+/) // 按一个或多个空白字符拆分
+        .map(name => name.trim()) // 去除首尾空格
+        .filter(name => name.length > 0); // 过滤空字符串
+
+      // 对每个权限名称分别进行精确匹配
+      level3PermissionNames.forEach(permissionName => {
+        const menuId = findMenuIdByName(menuTree, permissionName);
+        // 只有精确匹配到菜单项时才添加,避免误匹配
+        if (menuId !== null && !addedIds.has(menuId)) {
+          menuIds.push(menuId);
+          addedIds.add(menuId);
+        }
+      });
     }
   });
+
+  return menuIds;
+};
+
+// 加载角色数据(编辑时)
+const loadRoleData = async () => {
+  if (!roleId.value) {
+    ElMessage.error("缺少角色ID参数");
+    router.back();
+    return;
+  }
+
+  try {
+    // 从query中获取角色名称
+    const queryRoleName = route.query.roleName as string;
+    if (queryRoleName) {
+      roleForm.roleName = queryRoleName;
+    }
+
+    // 调用接口获取权限详情
+    const res = await getRolePermissionTable({ roleId: Number(roleId.value) });
+    const code = typeof res.code === "string" ? parseInt(res.code) : res.code;
+
+    if (code === 200 && res.data) {
+      // 从权限详情中提取菜单ID
+      const menuIds = extractMenuIdsFromPermissions(res.data, permissionTreeData.value as any);
+      roleForm.permissions = menuIds;
+      defaultCheckedKeys.value = menuIds;
+
+      // 设置树形组件的选中状态
+      nextTick(() => {
+        if (permissionTreeRef.value) {
+          permissionTreeRef.value.setCheckedKeys(defaultCheckedKeys.value);
+        }
+      });
+    } else {
+      ElMessage.error(res.msg || "获取角色权限详情失败");
+    }
+  } catch (error) {
+    console.error("加载角色数据失败:", error);
+    ElMessage.error("加载角色数据失败,请重试");
+  }
 };
 
 // 检查角色名称是否重复
@@ -241,15 +331,32 @@ const handleSave = async () => {
         storeId: createdId
       };
 
-      const res = await createRole(createRoleDto);
+      const updateRoleDto = {
+        roleName: roleForm.roleName,
+        menuIds: checkedKeys,
+        storeId: createdId
+      };
+
+      let res;
+      if (isEdit.value && roleId.value) {
+        // 编辑模式:调用更新接口
+        res = await updateRole({
+          ...updateRoleDto,
+          roleId: Number(roleId.value)
+        } as any);
+      } else {
+        // 创建模式:调用创建接口
+        res = await createRole(createRoleDto);
+      }
+
       // res.code 可能是 string 或 number,使用 == 进行宽松比较
       const code = typeof res.code === "string" ? parseInt(res.code) : res.code;
       if (code === 200) {
-        ElMessage.success("创建成功");
+        ElMessage.success(isEdit.value ? "编辑成功" : "创建成功");
         // 返回列表页
         router.push("/accountRoleManagement/roleManagement");
       } else {
-        ElMessage.error(res.msg || "创建失败");
+        ElMessage.error(res.msg || (isEdit.value ? "编辑失败" : "创建失败"));
       }
     } catch (error) {
       ElMessage.error(isEdit.value ? "编辑失败,请重试" : "创建失败,请重试");

+ 14 - 17
src/views/accountRoleManagement/roleManagement/index.vue

@@ -48,7 +48,7 @@
     <el-dialog v-model="deleteDialogVisible" title="删除确认" width="400px" :close-on-click-modal="false">
       <div class="delete-content">
         <p>确定要删除角色"{{ currentRole?.roleName }}"吗?</p>
-        <p class="delete-warning">删除后,该角色关联的子账号将失去该角色权限,此操作不可恢复!</p>
+        <p class="delete-warning">如果角色关联的子账号 就无法删除!</p>
       </div>
       <template #footer>
         <div class="dialog-footer">
@@ -65,7 +65,7 @@ import { ref, onMounted } from "vue";
 import { useRouter } from "vue-router";
 import { ElMessage } from "element-plus";
 import { Plus } from "@element-plus/icons-vue";
-import { getRolePage } from "@/api/modules/accountRoleManagement";
+import { getRolePage, deleteRoleWithCheck } from "@/api/modules/accountRoleManagement";
 import { localGet } from "@/utils";
 // 角色数据类型
 interface RoleItem {
@@ -127,7 +127,14 @@ const handleView = (role: RoleItem) => {
 
 // 编辑角色
 const handleEdit = (role: RoleItem) => {
-  router.push(`/accountRoleManagement/roleManagement/create/${role.id}`);
+  router.push({
+    path: "/accountRoleManagement/roleManagement/create",
+    query: {
+      roleId: role.roleId,
+      roleName: role.roleName,
+      type: "edit"
+    }
+  });
 };
 
 // 删除角色
@@ -142,22 +149,12 @@ const handleConfirmDelete = async () => {
 
   deleteLoading.value = true;
   try {
-    // TODO: 调用删除接口
-    // 模拟接口调用
-    await new Promise(resolve => setTimeout(resolve, 500));
-
-    // 从列表中删除
-    const index = roleList.value.findIndex(item => item.id === currentRole.value!.id);
-    if (index > -1) {
-      roleList.value.splice(index, 1);
+    const res = await deleteRoleWithCheck(currentRole.value.roleId);
+    const code = typeof res.code === "string" ? parseInt(res.code) : res.code;
+    if (code === 200) {
       ElMessage.success("删除成功");
-
-      // 如果删除的是当前选中的角色,重新选择第一个
-      if (selectedRoleId.value === currentRole.value.id) {
-        selectedRoleId.value = roleList.value.length > 0 ? roleList.value[0].id : null;
-      }
+      handleGetRolePage();
     }
-
     deleteDialogVisible.value = false;
     currentRole.value = null;
   } catch (error) {

+ 294 - 15
src/views/accountRoleManagement/subAccountManagement/index.vue

@@ -1,33 +1,312 @@
 <template>
-  <div class="card content-box">
-    <div class="content-section">
-      <h2 class="page-title">子账号管理</h2>
-      <el-button type="primary" @click="handleCreateSubAccount"> 创建子账号 </el-button>
-    </div>
+  <div class="sub-account-management-container">
+    <ProTable
+      ref="proTable"
+      :columns="columns"
+      :request-api="getTableList"
+      :init-param="initParam"
+      :data-callback="dataCallback"
+      row-key="userId"
+    >
+      <!-- 表格 header 按钮 -->
+      <template #tableHeader="scope">
+        <div class="table-header-btn">
+          <el-button type="primary" :icon="Plus" @click="handleCreateSubAccount"> 新建账号 </el-button>
+          <el-button :icon="Delete" :disabled="!scope.isSelected" @click="handleBatchDelete"> 批量删除 </el-button>
+        </div>
+      </template>
+      <template #tableHeaderRight>
+        <el-link type="primary" :underline="false"> 历史操作记录 </el-link>
+      </template>
+      <!-- 可操作权限列 -->
+      <template #permissionCount="scope">
+        <span>{{ scope.row.permissionCount || 0 }}个</span>
+      </template>
+      <!-- 操作列 -->
+      <template #operation="scope">
+        <el-button link type="primary" @click="handleView(scope.row)"> 查看 </el-button>
+        <el-button link type="primary" @click="handleEdit(scope.row)"> 编辑 </el-button>
+        <el-button link type="danger" @click="handleDelete(scope.row)"> 删除 </el-button>
+      </template>
+    </ProTable>
+
+    <!-- 删除确认对话框 -->
+    <el-dialog v-model="deleteDialogVisible" title="删除确认" width="400px" :close-on-click-modal="false">
+      <div class="delete-content">
+        <p v-if="deleteType === 'single'">确定要删除子账号"{{ currentAccount?.accountName }}"吗?</p>
+        <p v-else>确定要删除选中的 {{ selectedAccounts.length }} 个子账号吗?</p>
+        <p class="delete-warning">此操作不可恢复!</p>
+      </div>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="deleteDialogVisible = false"> 取消 </el-button>
+          <el-button type="danger" @click="handleConfirmDelete" :loading="deleteLoading"> 确定删除 </el-button>
+        </div>
+      </template>
+    </el-dialog>
   </div>
 </template>
 
-<script setup lang="ts" name="subAccountManagement">
+<script setup lang="tsx" name="subAccountManagement">
+import { ref, reactive, onMounted, computed } from "vue";
 import { useRouter } from "vue-router";
+import { ElMessage } from "element-plus";
+import { Plus, Delete } from "@element-plus/icons-vue";
+import ProTable from "@/components/ProTable/index.vue";
+import { ColumnProps, ProTableInstance } from "@/components/ProTable/interface";
+import { querySubAccounts, getAllNormalRoles, type RoleItem } from "@/api/modules/accountRoleManagement";
+import { localGet } from "@/utils";
+
+// 子账号数据类型
+interface SubAccountItem {
+  userId: number;
+  accountName: string;
+  phone: string;
+  roleId: number;
+  roleName: string;
+  permissionCount: number;
+}
+
+// 路由
 const router = useRouter();
 
-// 子账号管理页面
+// ProTable 实例
+const proTable = ref<ProTableInstance>();
+
+// 角色列表(用于搜索下拉框)
+const roleList = ref<RoleItem[]>([]);
+
+// 选中的账号
+const selectedAccounts = ref<SubAccountItem[]>([]);
+
+// 删除相关
+const deleteDialogVisible = ref(false);
+const deleteLoading = ref(false);
+const deleteType = ref<"single" | "batch">("single");
+const currentAccount = ref<SubAccountItem | null>(null);
+
+// 获取 storeId
+const getStoreId = (): string => {
+  return localGet("createdId") || localGet("geeker-user")?.userInfo?.storeId || "";
+};
+
+// 加载角色列表(用于搜索下拉框)
+const loadRoleList = async () => {
+  try {
+    const storeId = getStoreId();
+    if (!storeId) return;
+
+    const res = await getAllNormalRoles({ storeId: Number(storeId) });
+    const code = typeof res.code === "string" ? parseInt(res.code) : res.code;
+    if (code === 200) {
+      roleList.value = (res.data || []).map((role: any) => ({
+        ...role,
+        id: role.roleId || role.id,
+        roleId: role.roleId || role.id
+      }));
+    }
+  } catch (error) {
+    console.error("获取角色列表失败:", error);
+  }
+};
+
+// 表格列配置
+const columns = reactive<ColumnProps<SubAccountItem>[]>([
+  {
+    type: "selection",
+    fixed: "left",
+    width: 55
+  },
+  {
+    prop: "accountName",
+    label: "账号名称",
+    search: {
+      el: "input",
+      props: { placeholder: "请输入账号名称" }
+    }
+  },
+  {
+    prop: "phone",
+    label: "手机号",
+    search: {
+      el: "input",
+      props: { placeholder: "请输入手机号" }
+    }
+  },
+  {
+    prop: "roleName",
+    label: "角色",
+    search: {
+      el: "select",
+      props: { placeholder: "请选择角色", clearable: true },
+      key: "roleName"
+    },
+    enum: computed(() => {
+      return roleList.value.map(role => ({
+        label: role.roleName,
+        value: role.roleName
+      }));
+    }),
+    fieldNames: { label: "label", value: "value" }
+  },
+  {
+    prop: "permissionCount",
+    label: "可操作权限"
+  },
+  {
+    prop: "operation",
+    label: "操作",
+    fixed: "right",
+    width: 180
+  }
+]);
+
+// 初始化参数
+const initParam = reactive({
+  storeId: getStoreId()
+});
+
+// 数据回调处理
+const dataCallback = (data: any) => {
+  // 如果返回的是数组,转换为分页格式
+  if (Array.isArray(data)) {
+    return {
+      list: data,
+      total: data.length
+    };
+  }
+  // 如果返回的是分页对象
+  if (data.records) {
+    return {
+      list: data.records || [],
+      total: data.total || 0
+    };
+  }
+  // 默认返回
+  return {
+    list: [],
+    total: 0
+  };
+};
+
+// 请求接口
+const getTableList = (params: any) => {
+  const storeId = getStoreId();
+  if (!storeId) {
+    ElMessage.warning("缺少店铺ID");
+    return Promise.resolve({ code: 200, data: [], msg: "缺少店铺ID" });
+  }
+
+  return querySubAccounts(Number(storeId), params.accountName || "", params.phone || "", params.roleName || "");
+};
+
+// 创建子账号
 const handleCreateSubAccount = () => {
   router.push("/accountRoleManagement/subAccountManagement/create");
 };
+
+// 查看
+const handleView = (row: SubAccountItem) => {
+  // TODO: 实现查看功能
+  ElMessage.info("查看功能待实现");
+};
+
+// 编辑
+const handleEdit = (row: SubAccountItem) => {
+  router.push({
+    path: "/accountRoleManagement/subAccountManagement/create",
+    query: {
+      id: row.userId.toString(),
+      accountName: row.accountName
+    }
+  });
+};
+
+// 删除单个账号
+const handleDelete = (row: SubAccountItem) => {
+  currentAccount.value = row;
+  deleteType.value = "single";
+  deleteDialogVisible.value = true;
+};
+
+// 批量删除
+const handleBatchDelete = () => {
+  const selectedList = proTable.value?.selectedList || [];
+  if (selectedList.length === 0) {
+    ElMessage.warning("请选择要删除的子账号");
+    return;
+  }
+  selectedAccounts.value = selectedList as SubAccountItem[];
+  deleteType.value = "batch";
+  deleteDialogVisible.value = true;
+};
+
+// 确认删除
+const handleConfirmDelete = async () => {
+  deleteLoading.value = true;
+  try {
+    // TODO: 调用删除接口
+    // 模拟接口调用
+    await new Promise(resolve => setTimeout(resolve, 500));
+
+    if (deleteType.value === "single" && currentAccount.value) {
+      // 单个删除
+      ElMessage.success("删除成功");
+    } else {
+      // 批量删除
+      ElMessage.success(`成功删除 ${selectedAccounts.value.length} 个子账号`);
+    }
+
+    // 刷新表格
+    proTable.value?.getTableList();
+    deleteDialogVisible.value = false;
+    currentAccount.value = null;
+    selectedAccounts.value = [];
+  } catch (error) {
+    ElMessage.error("删除失败,请重试");
+  } finally {
+    deleteLoading.value = false;
+  }
+};
+
+// 初始化
+onMounted(() => {
+  loadRoleList();
+});
 </script>
 
 <style lang="scss" scoped>
-.content-box {
+.sub-account-management-container {
+  min-height: calc(100vh - 84px);
   padding: 20px;
+  background: #ffffff;
+}
+.table-header-btn {
+  display: flex;
+  gap: 12px;
+  align-items: center;
 }
-.content-section {
-  margin-bottom: 20px;
+
+// 删除对话框内容
+.delete-content {
+  p {
+    margin: 0 0 12px;
+    font-size: 14px;
+    color: var(--el-text-color-primary);
+    &:last-child {
+      margin-bottom: 0;
+    }
+  }
+  .delete-warning {
+    font-weight: 500;
+    color: #e6a23c;
+  }
 }
-.page-title {
-  margin: 0;
-  font-size: 20px;
-  font-weight: 600;
-  color: var(--el-text-color-primary);
+
+// 对话框底部
+.dialog-footer {
+  display: flex;
+  gap: 12px;
+  justify-content: flex-end;
 }
 </style>