Przeglądaj źródła

Merge branch 'development' of http://8.152.195.41:3000/alien/group_web_merchant into development

李亚非 3 miesięcy temu
rodzic
commit
6b15c61daa

+ 20 - 0
.gitignore

@@ -24,3 +24,23 @@ stats.html
 *.njsproj
 *.sln
 *.sw?
+
+.agent/
+
+.codebuddy/
+
+.cursor/
+
+.opencode/
+
+.qoder/
+
+.windsurf/
+
+openspec/
+
+AGENTS.md
+
+CODEBUDDY.md
+
+QODER.md

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

@@ -0,0 +1,158 @@
+import { PORT_NONE } from "@/api/config/servicePort";
+import http from "@/api";
+import type { ResultData } from "@/api/interface";
+
+/**
+ * 子账号与角色管理相关接口
+ */
+
+// 菜单项接口类型
+export interface MenuItem {
+  menuId: number;
+  menuName: string;
+  parentId: number;
+  menuType: string;
+  menuSort: number;
+  level: number;
+  path: string;
+  component: string;
+  perms: string | null;
+  icon: string;
+  status: string;
+  visible: string;
+  isFrame: string;
+  isCache: string;
+  delFlag: string;
+  createBy: string;
+  createdTime: string;
+  updateBy: string;
+  updatedTime: string;
+  remark: string | null;
+  children?: MenuItem[];
+}
+
+// 菜单树响应接口(后端返回的完整响应结构)
+export interface MenuTreeResponse {
+  code: number;
+  success: boolean;
+  data: MenuItem[];
+  msg: string;
+}
+
+// 创建角色请求参数
+export interface CreateRoleDto {
+  description?: string;
+  menulds?: number[]; // 注意:后端字段名是 menulds
+  remark?: string;
+  roleName: string;
+  roleNameEn?: string;
+  roleSort?: number;
+  roleType?: string;
+  status?: string;
+}
+
+// 通用响应接口
+export interface ApiResponse {
+  code: number;
+  success: boolean;
+  msg: string;
+  data?: any;
+}
+
+// 角色权限项接口(扁平格式)
+export interface RolePermissionItem {
+  level1Permission: string | null;
+  level2Permission: string | null;
+  level3Permission: string | null;
+}
+
+// 角色项接口
+export interface RoleItem {
+  id: number;
+  roleId?: number; // 兼容可能的 roleId 字段
+  roleName: string;
+  description?: string;
+}
+
+/**
+ * 获取菜单列表(树形结构)
+ * @returns Promise<ResultData<MenuItem[]>>
+ */
+export const getMenuTree = (): Promise<ResultData<MenuItem[]>> => {
+  return http.get<MenuItem[]>(PORT_NONE + `/platform/menu/getMenuTree`, {}, { loading: false });
+};
+
+/**
+ * 创建角色(包含权限分配)
+ * @param createRoleDto 创建角色的数据
+ * @returns Promise<ResultData<ApiResponse>>
+ */
+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[]>> => {
+  return http.get<MenuItem[]>(PORT_NONE + `/platform/role/getRolePage`, { page, size, storeId }, { loading: false });
+};
+// 查询所有正常状态的角色列表
+export const getAllNormalRoles = ({ storeId }: { storeId: number }): Promise<ResultData<RoleItem[]>> => {
+  return http.get<RoleItem[]>(PORT_NONE + `/platform/role/getAllNormalRoles`, { storeId }, { loading: false });
+};
+
+// 查询角色权限表
+export const getRolePermissionTable = (params: { roleId: number }): Promise<ResultData<RolePermissionItem[]>> => {
+  return http.get<RolePermissionItem[]>(PORT_NONE + `/platform/role/getRolePermissionTable`, params, { loading: false });
+};
+// 创建账号并分配角色
+export const createAccountAndAssignRole = ({ accountName, phone, roleId, storeId }): Promise<ResultData<ApiResponse>> => {
+  return http.post<ApiResponse>(
+    PORT_NONE + `/platform/user-role/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 }
+  );
+};

+ 106 - 2
src/assets/json/authMenuList.json

@@ -374,7 +374,7 @@
             "icon": "FolderChecked",
             "title": "合同管理",
             "isLink": "",
-            "isHide": false,
+            "isHide": true,
             "isFull": false,
             "isAffix": false,
             "isKeepAlive": false
@@ -388,7 +388,7 @@
             "icon": "Memo",
             "title": "食品经营许可证",
             "isLink": "",
-            "isHide": false,
+            "isHide": true,
             "isFull": false,
             "isAffix": false,
             "isKeepAlive": false
@@ -833,6 +833,110 @@
           }
         }
       ]
+    },
+    {
+      "path": "/accountRoleManagement",
+      "name": "accountRoleManagement",
+      "redirect": "/accountRoleManagement/subAccountManagement",
+      "meta": {
+        "icon": "User",
+        "title": "子账号与角色管理",
+        "isLink": "",
+        "isHide": false,
+        "isFull": false,
+        "isAffix": false,
+        "isKeepAlive": false
+      },
+      "children": [
+        {
+          "path": "/accountRoleManagement/subAccountManagement",
+          "name": "subAccountManagement",
+          "component": "/accountRoleManagement/subAccountManagement/index",
+          "meta": {
+            "icon": "UserFilled",
+            "title": "子账号管理",
+            "isLink": "",
+            "isHide": false,
+            "isFull": false,
+            "isAffix": false,
+            "isKeepAlive": false
+          }
+        },
+        {
+          "path": "/accountRoleManagement/roleManagement",
+          "name": "roleManagement",
+          "component": "/accountRoleManagement/roleManagement/index",
+          "meta": {
+            "icon": "Avatar",
+            "title": "角色管理",
+            "isLink": "",
+            "isHide": false,
+            "isFull": false,
+            "isAffix": false,
+            "isKeepAlive": false
+          }
+        },
+        {
+          "path": "/accountRoleManagement/roleManagement/create",
+          "name": "roleManagementCreate",
+          "component": "/accountRoleManagement/roleManagement/create",
+          "meta": {
+            "icon": "Menu",
+            "title": "创建角色",
+            "activeMenu": "/accountRoleManagement/roleManagement",
+            "isLink": "",
+            "isHide": true,
+            "isFull": false,
+            "isAffix": false,
+            "isKeepAlive": false
+          }
+        },
+        {
+          "path": "/accountRoleManagement/roleManagement/create/:id",
+          "name": "roleManagementEdit",
+          "component": "/accountRoleManagement/roleManagement/create",
+          "meta": {
+            "icon": "Menu",
+            "title": "编辑角色",
+            "activeMenu": "/accountRoleManagement/roleManagement",
+            "isLink": "",
+            "isHide": true,
+            "isFull": false,
+            "isAffix": false,
+            "isKeepAlive": false
+          }
+        },
+        {
+          "path": "/accountRoleManagement/roleManagement/view",
+          "name": "roleManagementView",
+          "component": "/accountRoleManagement/roleManagement/view",
+          "meta": {
+            "icon": "Menu",
+            "title": "查看角色",
+            "activeMenu": "/accountRoleManagement/roleManagement",
+            "isLink": "",
+            "isHide": true,
+            "isFull": false,
+            "isAffix": false,
+            "isKeepAlive": false
+          }
+        },
+        {
+          "path": "/accountRoleManagement/subAccountManagement/create",
+          "name": "subAccountManagementCreate",
+          "component": "/accountRoleManagement/subAccountManagement/create",
+          "meta": {
+            "icon": "Menu",
+            "title": "创建子账号",
+            "activeMenu": "/accountRoleManagement/subAccountManagement",
+            "isLink": "",
+            "isHide": true,
+            "isFull": false,
+            "isAffix": false,
+            "isKeepAlive": false
+          }
+        }
+      ]
     }
   ],
   "msg": "成功"

+ 11 - 10
src/utils/permission.ts

@@ -182,16 +182,17 @@ export async function checkMenuClickPermission(path?: string): Promise<{
     if (!canClickstatus) {
       const messages: string[] = [];
 
-      if (contractManagement) {
-        messages.push("合同已到期,请上传最新合同。");
-      }
-      if (foodBusinessLicense) {
-        console.log(foodBusinessLicense);
-        messages.push("食品经营许可证已到期,请上传最新许可证。");
-      }
-      if (entertainmentBusinessLicense) {
-        messages.push("娱乐经营许可证已到期,请上传最新许可证。");
-      }
+      // 暂时关闭三个权限检查
+      // if (contractManagement) {
+      //   messages.push("合同已到期,请上传最新合同。");
+      // }
+      // if (foodBusinessLicense) {
+      //   console.log(foodBusinessLicense);
+      //   messages.push("食品经营许可证已到期,请上传最新许可证。");
+      // }
+      // if (entertainmentBusinessLicense) {
+      //   messages.push("娱乐经营许可证已到期,请上传最新许可证。");
+      // }
 
       if (messages.length) {
         ElMessage.warning({

+ 493 - 0
src/views/accountRoleManagement/roleManagement/create.vue

@@ -0,0 +1,493 @@
+<template>
+  <div class="role-form-container">
+    <!-- 头部:返回按钮和标题 -->
+    <div class="header-section">
+      <el-button :icon="ArrowLeft" text @click="handleBack"> 返回 </el-button>
+      <h2 class="page-title">
+        {{ isEdit ? "编辑角色" : "创建角色" }}
+      </h2>
+    </div>
+
+    <!-- 表单内容 -->
+    <div class="form-content">
+      <el-form ref="roleFormRef" :model="roleForm" :rules="roleFormRules" label-width="120px">
+        <!-- 角色类型 -->
+        <el-form-item label="角色类型" prop="roleName">
+          <template #label>
+            <span>角色类型</span>
+            <el-tooltip content="请输入角色类型名称" placement="top">
+              <el-icon class="question-icon">
+                <QuestionFilled />
+              </el-icon>
+            </el-tooltip>
+          </template>
+          <el-input v-model="roleForm.roleName" placeholder="请输入" maxlength="50" clearable @blur="checkRoleNameDuplicate" />
+        </el-form-item>
+
+        <!-- 子账号权限 -->
+        <el-form-item label="子账号权限" prop="permissions" required>
+          <template #label>
+            <span>子账号权限</span>
+            <span class="required-star">*</span>
+          </template>
+          <div class="permission-tree-container">
+            <el-tree
+              ref="permissionTreeRef"
+              :data="permissionTreeData"
+              :props="treeProps"
+              show-checkbox
+              node-key="id"
+              :default-expand-all="false"
+              :check-strictly="true"
+              :default-checked-keys="defaultCheckedKeys"
+              :expand-on-click-node="false"
+              @check="handlePermissionCheck"
+            >
+              <template #default="{ node, data }">
+                <span class="tree-node-label">
+                  <span>{{ data.label }}</span>
+                </span>
+              </template>
+            </el-tree>
+          </div>
+        </el-form-item>
+      </el-form>
+    </div>
+
+    <!-- 底部按钮 -->
+    <div class="form-footer">
+      <el-button @click="handleCancel"> 取消 </el-button>
+      <el-button type="primary" @click="handleSave" :loading="saveLoading"> 保存 </el-button>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts" name="roleCreate">
+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,
+  updateRole,
+  getRolePermissionTable,
+  type MenuItem,
+  type RolePermissionItem
+} from "@/api/modules/accountRoleManagement";
+import { localGet } from "@/utils";
+
+// 路由
+const router = useRouter();
+const route = useRoute();
+
+// 判断是编辑还是创建
+const roleId = computed(() => route.query.roleId as string | undefined);
+const isEdit = computed(() => route.query.type === "edit");
+
+// 表单引用
+const roleFormRef = ref<FormInstance>();
+const permissionTreeRef = ref<any>();
+
+// 表单数据
+const roleForm = reactive({
+  roleName: "",
+  description: "",
+  permissions: [] as number[]
+});
+
+// 表单验证规则
+const roleFormRules: FormRules = {
+  roleName: [{ required: true, message: "请输入角色类型", trigger: "blur" }],
+  permissions: [
+    {
+      required: true,
+      validator: (rule, value, callback) => {
+        if (!value || value.length === 0) {
+          callback(new Error("请至少选择一个权限"));
+        } else {
+          callback();
+        }
+      },
+      trigger: "change"
+    }
+  ]
+};
+
+// 树形组件配置
+const treeProps = {
+  children: "children",
+  label: "label"
+};
+
+// 权限树数据
+const permissionTreeData = ref<any[]>([]);
+
+// 转换菜单数据为树形组件需要的格式
+const transformMenuData = (menuList: MenuItem[]): any[] => {
+  return menuList.map(menu => {
+    const node: any = {
+      id: menu.menuId,
+      label: menu.menuName,
+      menuId: menu.menuId,
+      menuName: menu.menuName
+    };
+    if (menu.children && menu.children.length > 0) {
+      node.children = transformMenuData(menu.children);
+    }
+    return node;
+  });
+};
+
+// 默认选中的权限(编辑时使用)
+const defaultCheckedKeys = ref<number[]>([]);
+
+// 保存加载状态
+const saveLoading = ref(false);
+
+// 加载菜单树数据
+const loadMenuTree = async () => {
+  try {
+    const res = await getMenuTree();
+    // res.code 可能是 string 或 number,需要转换比较
+    const code = typeof res.code === "string" ? parseInt(res.code) : res.code;
+    if (code === 200 && res.data) {
+      // 转换数据格式,res.data 是 MenuItem[]
+      permissionTreeData.value = transformMenuData(res.data);
+    } else {
+      ElMessage.error(res.msg || "获取菜单列表失败");
+    }
+  } catch (error) {
+    console.error("获取菜单列表失败:", error);
+    ElMessage.error("获取菜单列表失败,请重试");
+  }
+};
+
+// 初始化数据
+onMounted(async () => {
+  // 先加载菜单树数据
+  await loadMenuTree();
+
+  if (isEdit.value) {
+    // 编辑模式:加载角色数据
+    await loadRoleData();
+  } else {
+    // 创建模式:初始化空数据
+    roleForm.roleName = "";
+    roleForm.permissions = [];
+  }
+});
+
+// 通过权限名称匹配菜单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("加载角色数据失败,请重试");
+  }
+};
+
+// 检查角色名称是否重复
+const checkRoleNameDuplicate = async () => {
+  if (!roleForm.roleName) return;
+
+  // TODO: 调用接口检查名称是否重复
+  // 模拟检查
+  const existingNames = ["管理员", "运营人员", "财务人员", "客服人员"];
+  if (existingNames.includes(roleForm.roleName)) {
+    ElMessage.warning("该角色名称已存在,请使用其他名称");
+  }
+};
+
+// 权限选择变化
+const handlePermissionCheck = (data: any, checked: any) => {
+  const checkedKeys = permissionTreeRef.value?.getCheckedKeys() || [];
+  roleForm.permissions = checkedKeys;
+  // 触发表单验证
+  roleFormRef.value?.validateField("permissions");
+};
+
+// 返回
+const handleBack = () => {
+  router.back();
+};
+
+// 取消
+const handleCancel = () => {
+  router.back();
+};
+// 保存
+const handleSave = async () => {
+  if (!roleFormRef.value) return;
+
+  await roleFormRef.value.validate(async valid => {
+    if (!valid) return;
+
+    saveLoading.value = true;
+    try {
+      const checkedKeys = permissionTreeRef.value?.getCheckedKeys() || [];
+      // 从缓存中获取 createdId,如果没有则从用户信息中获取 storeId
+      const createdId = localGet("createdId") || localGet("geeker-user")?.userInfo?.storeId || "";
+
+      const createRoleDto = {
+        roleName: roleForm.roleName,
+        menuIds: checkedKeys,
+        storeId: createdId
+      };
+
+      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(isEdit.value ? "编辑成功" : "创建成功");
+        // 返回列表页
+        router.push("/accountRoleManagement/roleManagement");
+      } else {
+        ElMessage.error(res.msg || (isEdit.value ? "编辑失败" : "创建失败"));
+      }
+    } catch (error) {
+      ElMessage.error(isEdit.value ? "编辑失败,请重试" : "创建失败,请重试");
+    } finally {
+      saveLoading.value = false;
+    }
+  });
+};
+</script>
+
+<style lang="scss" scoped>
+.role-form-container {
+  min-height: calc(100vh - 84px);
+  padding: 20px;
+  background: #ffffff;
+}
+
+// 头部区域
+.header-section {
+  position: relative;
+  padding-bottom: 16px;
+  margin-bottom: 24px;
+  border-bottom: 1px solid #e4e7ed;
+  .page-title {
+    margin: 0;
+    font-size: 20px;
+    font-weight: 600;
+    color: var(--el-text-color-primary);
+    text-align: center;
+  }
+  .header-tip {
+    position: absolute;
+    top: 0;
+    right: 0;
+    left: 0;
+    margin-top: 40px;
+    .tip-alert {
+      background: #f5f7fa;
+      border: 1px solid #e4e7ed;
+      border-radius: 4px;
+      :deep(.el-alert__content) {
+        .el-alert__title {
+          font-size: 14px;
+          color: var(--el-text-color-regular);
+        }
+      }
+    }
+  }
+}
+
+// 表单内容
+.form-content {
+  max-width: 800px;
+  padding: 24px 0;
+  :deep(.el-form-item) {
+    margin-bottom: 24px;
+    .el-form-item__label {
+      display: flex;
+      align-items: center;
+      font-weight: 500;
+      .question-icon {
+        margin-left: 4px;
+        color: #909399;
+        cursor: help;
+      }
+      .required-star {
+        margin-left: 4px;
+        color: #f56c6c;
+      }
+    }
+  }
+  .permission-tree-container {
+    width: 100%;
+    min-height: 300px;
+    max-height: 500px;
+    padding: 16px;
+    overflow-y: auto;
+    background: #f5f7fa;
+    border: 1px solid #e4e7ed;
+    border-radius: 4px;
+    :deep(.el-tree) {
+      background: transparent;
+      .el-tree-node {
+        .el-tree-node__content {
+          height: 40px;
+          padding: 0 12px;
+          margin-bottom: 2px;
+          border-radius: 4px;
+          transition: background-color 0.2s;
+          &:hover {
+            background-color: #ecf5ff;
+          }
+          .tree-node-label {
+            display: flex;
+            flex: 1;
+            align-items: center;
+            font-size: 14px;
+            color: var(--el-text-color-primary);
+          }
+        }
+        .el-checkbox {
+          margin-right: 8px;
+        }
+
+        // 树节点缩进
+        .el-tree-node__children {
+          padding-left: 20px;
+        }
+      }
+      .el-tree-node__expand-icon {
+        margin-right: 6px;
+        font-size: 14px;
+        color: #909399;
+        transform: translateX(-5px);
+      }
+
+      // 隐藏默认的展开图标,使用自定义样式
+      .el-tree-node__expand-icon.is-leaf {
+        display: none;
+      }
+    }
+  }
+}
+
+// 底部按钮
+.form-footer {
+  display: flex;
+  gap: 12px;
+  justify-content: flex-end;
+  padding: 24px 0;
+  margin-top: 24px;
+  border-top: 1px solid #e4e7ed;
+}
+</style>

+ 301 - 0
src/views/accountRoleManagement/roleManagement/index.vue

@@ -0,0 +1,301 @@
+<template>
+  <div class="role-management-container">
+    <!-- 头部:标题和创建按钮 -->
+    <div class="header-section">
+      <h2 class="page-title">角色管理</h2>
+      <el-button type="primary" :icon="Plus" @click="handleCreateRole"> 创建角色 </el-button>
+    </div>
+
+    <!-- 角色卡片列表 -->
+    <div v-if="roleList.length > 0" class="role-cards">
+      <div
+        v-for="(role, index) in roleList"
+        :key="role.id"
+        class="role-card"
+        :class="{ 'role-card-active': selectedRoleId === role.id }"
+        @click="handleCardClick(role)"
+      >
+        <div class="role-card-header">
+          <h3 class="role-type-title">
+            {{ role.roleName || "角色类型" }}
+          </h3>
+        </div>
+        <div class="role-card-body">
+          <div class="role-info-item">
+            <span class="info-label">关联子账号</span>
+            <span class="info-value">{{ role.subAccountCount || 0 }}个</span>
+          </div>
+          <div class="role-info-item">
+            <span class="info-label">关联权限</span>
+            <span class="info-value">{{ role.permissionCount || 0 }}个</span>
+          </div>
+        </div>
+        <div class="role-card-divider" />
+        <div class="role-card-actions">
+          <el-button link type="primary" @click.stop="handleView(role)"> 查看 </el-button>
+          <el-button link type="primary" @click.stop="handleEdit(role)"> 编辑 </el-button>
+          <el-button link type="primary" @click.stop="handleDelete(role)"> 删除 </el-button>
+        </div>
+      </div>
+    </div>
+
+    <!-- 空状态 -->
+    <div v-else class="empty-state">
+      <el-empty description="暂无角色数据" :image-size="100" />
+    </div>
+
+    <!-- 删除确认对话框 -->
+    <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>
+      </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="roleManagement">
+import { ref, onMounted } from "vue";
+import { useRouter } from "vue-router";
+import { ElMessage } from "element-plus";
+import { Plus } from "@element-plus/icons-vue";
+import { getRolePage, deleteRoleWithCheck } from "@/api/modules/accountRoleManagement";
+import { localGet } from "@/utils";
+// 角色数据类型
+interface RoleItem {
+  id: number;
+  roleName: string;
+  description?: string;
+  subAccountCount: number;
+  permissionCount: number;
+  createTime?: string;
+  updateTime?: string;
+}
+
+// 路由
+const router = useRouter();
+
+// 选中的角色ID
+const selectedRoleId = ref<number | null>(null);
+
+// 角色列表(假数据)
+const roleList = ref<RoleItem[]>([]);
+
+// 删除相关
+const deleteDialogVisible = ref(false);
+const deleteLoading = ref(false);
+const currentRole = ref<RoleItem | null>(null);
+
+// 从缓存中获取 createdId,如果没有则从用户信息中获取 storeId
+const createdId = localGet("createdId") || localGet("geeker-user")?.userInfo?.storeId || "";
+
+const handleGetRolePage = async () => {
+  const res = await getRolePage(1, 10, Number(createdId));
+  const code = typeof res.code === "string" ? parseInt(res.code) : res.code;
+  if (code === 200) {
+    const responseData = res.data as any;
+    roleList.value = responseData.records || [];
+  }
+};
+
+// 初始化:默认选中第一个角色
+onMounted(() => {
+  handleGetRolePage();
+});
+
+// 创建角色
+const handleCreateRole = () => {
+  router.push("/accountRoleManagement/roleManagement/create");
+};
+
+// 查看角色
+const handleView = (role: RoleItem) => {
+  router.push({
+    path: "/accountRoleManagement/roleManagement/view",
+    query: {
+      roleId: role.roleId,
+      roleName: role.roleName
+    }
+  });
+};
+
+// 编辑角色
+const handleEdit = (role: RoleItem) => {
+  router.push({
+    path: "/accountRoleManagement/roleManagement/create",
+    query: {
+      roleId: role.roleId,
+      roleName: role.roleName,
+      type: "edit"
+    }
+  });
+};
+
+// 删除角色
+const handleDelete = (role: RoleItem) => {
+  currentRole.value = role;
+  deleteDialogVisible.value = true;
+};
+
+// 确认删除
+const handleConfirmDelete = async () => {
+  if (!currentRole.value) return;
+
+  deleteLoading.value = true;
+  try {
+    const res = await deleteRoleWithCheck(currentRole.value.roleId);
+    const code = typeof res.code === "string" ? parseInt(res.code) : res.code;
+    if (code === 200) {
+      ElMessage.success("删除成功");
+      handleGetRolePage();
+    }
+    deleteDialogVisible.value = false;
+    currentRole.value = null;
+  } catch (error) {
+    ElMessage.error("删除失败,请重试");
+  } finally {
+    deleteLoading.value = false;
+  }
+};
+
+// 卡片点击
+const handleCardClick = (role: RoleItem) => {
+  selectedRoleId.value = role.id;
+};
+</script>
+
+<style lang="scss" scoped>
+.role-management-container {
+  min-height: calc(100vh - 84px);
+  padding: 20px;
+  background: #ffffff;
+}
+
+// 头部区域
+.header-section {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding-bottom: 16px;
+  margin-bottom: 24px;
+  border-bottom: 1px solid #e4e7ed;
+  .page-title {
+    margin: 0;
+    font-size: 20px;
+    font-weight: 600;
+    color: var(--el-text-color-primary);
+  }
+}
+
+// 角色卡片容器
+.role-cards {
+  display: grid;
+  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
+  gap: 20px;
+  margin-bottom: 20px;
+
+  @media (width <= 768px) {
+    grid-template-columns: repeat(1, 1fr);
+  }
+}
+
+// 角色卡片
+.role-card {
+  padding: 20px;
+  cursor: pointer;
+  background: #ffffff;
+  border: 1px solid #e4e7ed;
+  border-radius: 8px;
+  transition: all 0.3s;
+  &:hover {
+    box-shadow: 0 2px 12px rgb(0 0 0 / 10%);
+    transform: translateY(-2px);
+  }
+  &.role-card-active {
+    border: 2px solid #409eff;
+    box-shadow: 0 2px 12px rgb(64 158 255 / 20%);
+  }
+  .role-card-header {
+    margin-bottom: 16px;
+    .role-type-title {
+      margin: 0;
+      font-size: 16px;
+      font-weight: 600;
+      color: var(--el-text-color-primary);
+    }
+  }
+  .role-card-body {
+    margin-bottom: 16px;
+    .role-info-item {
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      margin-bottom: 12px;
+      &:last-child {
+        margin-bottom: 0;
+      }
+      .info-label {
+        font-size: 14px;
+        color: var(--el-text-color-regular);
+      }
+      .info-value {
+        font-size: 14px;
+        font-weight: 500;
+        color: var(--el-text-color-primary);
+      }
+    }
+  }
+  .role-card-divider {
+    height: 1px;
+    margin: 16px 0;
+    background: #e4e7ed;
+  }
+  .role-card-actions {
+    display: flex;
+    gap: 8px;
+    align-items: center;
+    justify-content: space-around;
+    :deep(.el-button) {
+      flex: 1;
+    }
+  }
+}
+
+// 空状态
+.empty-state {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  min-height: 400px;
+  padding: 40px 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;
+  }
+}
+
+// 对话框底部
+.dialog-footer {
+  display: flex;
+  gap: 12px;
+  justify-content: flex-end;
+}
+</style>

+ 181 - 0
src/views/accountRoleManagement/roleManagement/view.vue

@@ -0,0 +1,181 @@
+<template>
+  <div class="role-view-container">
+    <!-- 头部:返回按钮和标题 -->
+    <div class="header-section">
+      <el-button :icon="ArrowLeft" text @click="handleBack"> 返回 </el-button>
+      <h2 class="page-title">查看角色</h2>
+    </div>
+
+    <!-- 角色信息 -->
+    <div class="role-info-section">
+      <div class="role-name">
+        {{ roleName }}
+      </div>
+      <div class="permission-label">权限</div>
+    </div>
+
+    <!-- 权限表格 -->
+    <div class="permission-table-section">
+      <el-table :data="permissionTableData" border style="width: 100%">
+        <el-table-column prop="level1Permission" label="一级权限" width="200" align="center">
+          <template #default="{ row }">
+            <span>{{ row.level1Permission || "-" }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="level2Permission" label="二级权限" width="200" align="center">
+          <template #default="{ row }">
+            <span>{{ row.level2Permission || "-" }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="level3Permission" label="三级权限" align="center">
+          <template #default="{ row }">
+            <span>{{ row.level3Permission || "-" }}</span>
+          </template>
+        </el-table-column>
+      </el-table>
+    </div>
+
+    <!-- 加载状态 -->
+    <div v-if="loading" class="loading-container">
+      <el-skeleton :rows="5" animated />
+    </div>
+
+    <!-- 空状态 -->
+    <div v-if="!loading && permissionTableData.length === 0" class="empty-state">
+      <el-empty description="暂无权限数据" :image-size="100" />
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts" name="roleView">
+import { ref, onMounted } from "vue";
+import { useRouter, useRoute } from "vue-router";
+import { ElMessage } from "element-plus";
+import { ArrowLeft } from "@element-plus/icons-vue";
+import { getRolePermissionTable, type RolePermissionItem } from "@/api/modules/accountRoleManagement";
+
+// 路由
+const router = useRouter();
+const route = useRoute();
+
+// 角色名称
+const roleName = ref<string>("");
+
+// 权限表格数据
+const permissionTableData = ref<RolePermissionItem[]>([]);
+
+// 加载状态
+const loading = ref(false);
+
+// 获取角色权限数据
+const loadRolePermissionData = async () => {
+  const roleId = route.query.roleId;
+
+  if (!roleId) {
+    ElMessage.error("缺少角色ID参数");
+    router.back();
+    return;
+  }
+
+  loading.value = true;
+  try {
+    const res = await getRolePermissionTable({ roleId: Number(roleId) });
+    const code = typeof res.code === "string" ? parseInt(res.code) : res.code;
+
+    if (code === 200 && res.data) {
+      permissionTableData.value = res.data;
+      // 从路由参数或查询参数中获取角色名称,如果没有则使用默认值
+      roleName.value = (route.query.roleName as string) || "角色";
+    } else {
+      ElMessage.error(res.msg || "获取权限数据失败");
+    }
+  } catch (error) {
+    console.error("获取权限数据失败:", error);
+    ElMessage.error("获取权限数据失败,请重试");
+  } finally {
+    loading.value = false;
+  }
+};
+
+// 返回
+const handleBack = () => {
+  router.back();
+};
+
+// 初始化
+onMounted(() => {
+  loadRolePermissionData();
+});
+</script>
+
+<style lang="scss" scoped>
+.role-view-container {
+  min-height: calc(100vh - 84px);
+  padding: 20px;
+  background: #ffffff;
+}
+
+// 头部区域
+.header-section {
+  position: relative;
+  padding-bottom: 16px;
+  margin-bottom: 24px;
+  border-bottom: 1px solid #e4e7ed;
+  .page-title {
+    margin: 0;
+    font-size: 20px;
+    font-weight: 600;
+    color: var(--el-text-color-primary);
+    text-align: center;
+  }
+}
+
+// 角色信息区域
+.role-info-section {
+  margin-bottom: 24px;
+  .role-name {
+    margin-bottom: 12px;
+    font-size: 18px;
+    font-weight: 600;
+    color: var(--el-text-color-primary);
+  }
+  .permission-label {
+    font-size: 16px;
+    font-weight: 500;
+    color: var(--el-text-color-regular);
+  }
+}
+
+// 权限表格区域
+.permission-table-section {
+  margin-top: 20px;
+  :deep(.el-table) {
+    .el-table__header {
+      th {
+        font-weight: 600;
+        color: var(--el-text-color-primary);
+        background-color: #f5f7fa;
+      }
+    }
+    .el-table__body {
+      td {
+        padding: 12px 0;
+      }
+    }
+  }
+}
+
+// 加载状态
+.loading-container {
+  padding: 40px 20px;
+}
+
+// 空状态
+.empty-state {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  min-height: 400px;
+  padding: 40px 20px;
+}
+</style>

+ 114 - 0
src/views/accountRoleManagement/subAccountManagement/PermissionItem.vue

@@ -0,0 +1,114 @@
+<template>
+  <div class="permission-item" :class="{ expanded: permission.expanded }">
+    <div class="permission-item-header" :style="{ paddingLeft: `${level * 24 + 16}px` }" @click="handleHeaderClick">
+      <span class="permission-name" @click.stop="togglePermission">{{ permission.name }}</span>
+      <el-icon v-if="hasChildren" class="expand-icon" :class="{ rotated: permission.expanded }" @click.stop="togglePermission">
+        <ArrowDown />
+      </el-icon>
+    </div>
+    <div v-if="permission.expanded && hasChildren" class="permission-item-content">
+      <PermissionItem
+        v-for="child in permission.children"
+        :key="child.id"
+        :permission="child"
+        :level="level + 1"
+        @permission-change="(perm, checked) => emit('permissionChange', perm, checked)"
+        @child-permission-change="(parent, child, checked) => emit('childPermissionChange', parent, child, checked)"
+      />
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { computed } from "vue";
+import { ArrowDown } from "@element-plus/icons-vue";
+
+interface PermissionItem {
+  id: number;
+  name: string;
+  checked: boolean;
+  expanded: boolean;
+  children?: PermissionItem[];
+}
+
+interface Props {
+  permission: PermissionItem;
+  level?: number;
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  level: 0
+});
+
+const emit = defineEmits<{
+  permissionChange: [perm: PermissionItem, checked: boolean];
+  childPermissionChange: [parent: PermissionItem, child: PermissionItem, checked: boolean];
+}>();
+
+const hasChildren = computed(() => props.permission.children && props.permission.children.length > 0);
+
+const togglePermission = () => {
+  // 创建新对象而不是直接修改 props
+  const updatedPermission = {
+    ...props.permission,
+    expanded: !props.permission.expanded
+  };
+  // 注意:这里需要父组件处理状态更新,或者使用 emit 通知父组件
+  // 由于这是递归组件,暂时保持原逻辑,但添加注释说明
+  // eslint-disable-next-line vue/no-mutating-props
+  props.permission.expanded = !props.permission.expanded;
+};
+
+const handleHeaderClick = (e: MouseEvent) => {
+  // 如果点击的不是 checkbox、名称或图标,则触发展开/收起
+  const target = e.target as HTMLElement;
+  if (!target.closest(".el-checkbox") && !target.closest(".permission-name") && !target.closest(".expand-icon")) {
+    togglePermission();
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.permission-item {
+  margin-bottom: 12px;
+  overflow: hidden;
+  background: #f5f7fa;
+  border-radius: 4px;
+  transition: all 0.3s;
+  &.expanded {
+    .permission-item-header {
+      background: #ecf5ff;
+    }
+  }
+  .permission-item-header {
+    display: flex;
+    align-items: center;
+    padding: 12px 16px;
+    cursor: pointer;
+    transition: background-color 0.2s;
+    &:hover {
+      background: #ecf5ff;
+    }
+    .permission-name {
+      flex: 1;
+      margin-left: 12px;
+      font-size: 14px;
+      color: var(--el-text-color-primary);
+    }
+    .expand-icon {
+      margin-left: auto;
+      font-size: 14px;
+      color: #909399;
+      transition: transform 0.3s;
+      &.rotated {
+        transform: rotate(180deg);
+      }
+    }
+  }
+  .permission-item-content {
+    padding: 12px 0;
+    background: #ffffff;
+    border-top: 1px solid #e4e7ed;
+  }
+}
+</style>

+ 457 - 0
src/views/accountRoleManagement/subAccountManagement/create.vue

@@ -0,0 +1,457 @@
+<template>
+  <div class="sub-account-form-container">
+    <!-- 头部:返回按钮和标题 -->
+    <div class="header-section">
+      <el-button :icon="ArrowLeft" text @click="handleBack"> 返回 </el-button>
+    </div>
+
+    <!-- 表单内容 -->
+    <div class="form-content">
+      <el-form ref="subAccountFormRef" :model="subAccountForm" :rules="subAccountFormRules" label-width="120px">
+        <!-- 基本信息 -->
+        <div class="form-section">
+          <h3 class="section-title">基本信息</h3>
+          <el-form-item label="账号名称" prop="accountName">
+            <el-input v-model="subAccountForm.accountName" placeholder="请输入" maxlength="50" clearable />
+          </el-form-item>
+          <el-form-item label="手机号" prop="phone">
+            <el-input v-model="subAccountForm.phone" placeholder="请输入" maxlength="11" clearable />
+          </el-form-item>
+        </div>
+
+        <!-- 权限 -->
+        <div class="form-section">
+          <h3 class="section-title">权限</h3>
+          <el-form-item label="选择角色" prop="roleId">
+            <el-select
+              v-model="subAccountForm.roleId"
+              placeholder="请选择角色"
+              clearable
+              style="width: 100%"
+              @change="handleRoleChange"
+            >
+              <el-option
+                v-for="role in roleList"
+                :key="role.roleId || role.id"
+                :label="role.roleName"
+                :value="role.roleId || role.id"
+              />
+            </el-select>
+          </el-form-item>
+
+          <!-- 权限列表 -->
+          <el-form-item label="权限" prop="permissions">
+            <div class="permission-list">
+              <template v-if="permissionList.length > 0">
+                <PermissionItem v-for="permission in permissionList" :key="permission.id" :permission="permission" :level="0" />
+              </template>
+              <div v-else class="permission-empty">
+                <span class="empty-text">暂无权限</span>
+              </div>
+            </div>
+          </el-form-item>
+        </div>
+      </el-form>
+    </div>
+
+    <!-- 底部按钮 -->
+    <div class="form-footer">
+      <el-button @click="handleCancel"> 取消 </el-button>
+      <el-button type="primary" @click="handleSave" :loading="saveLoading"> 确定 </el-button>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts" name="subAccountCreate">
+import { ref, reactive, onMounted, computed } from "vue";
+import { useRouter, useRoute } from "vue-router";
+import { ElMessage, type FormInstance, type FormRules } from "element-plus";
+import { ArrowLeft } from "@element-plus/icons-vue";
+import {
+  getAllNormalRoles,
+  getRolePermissionTable,
+  type RolePermissionItem,
+  type RoleItem,
+  createAccountAndAssignRole
+} from "@/api/modules/accountRoleManagement";
+import { localGet } from "@/utils";
+import PermissionItem from "./PermissionItem.vue";
+// 路由
+const router = useRouter();
+const route = useRoute();
+
+// 判断是编辑还是创建
+const accountId = computed(() => route.params.id as string | undefined);
+const isEdit = computed(() => !!accountId.value);
+
+// 表单引用
+const subAccountFormRef = ref<FormInstance>();
+
+// 表单数据
+const subAccountForm = reactive({
+  accountName: "",
+  phone: "",
+  roleId: undefined as number | undefined,
+  permissions: [] as number[]
+});
+
+// 表单验证规则
+const subAccountFormRules: FormRules = {
+  accountName: [{ required: true, message: "请输入账号名称", trigger: "blur" }],
+  phone: [
+    { required: true, message: "请输入手机号", trigger: "blur" },
+    { pattern: /^1[3-9]\d{9}$/, message: "请输入正确的手机号", trigger: "blur" }
+  ],
+  roleId: [{ required: true, message: "请选择角色", trigger: "change" }]
+};
+
+// 角色列表
+const roleList = ref<RoleItem[]>([]);
+
+// 权限项接口
+interface PermissionItem {
+  id: number;
+  name: string;
+  checked: boolean;
+  expanded: boolean;
+  children?: PermissionItem[];
+}
+
+// 权限列表
+const permissionList = ref<PermissionItem[]>([]);
+
+// 保存加载状态
+const saveLoading = ref(false);
+
+// 加载角色列表
+const loadRoleList = async () => {
+  try {
+    const storeId = localGet("createdId") || "";
+    const res = await getAllNormalRoles({ storeId });
+    const code = typeof res.code === "string" ? parseInt(res.code) : res.code;
+    if (code === 200) {
+      // 处理角色数据,确保有 id 或 roleId 字段
+      const roles = (res.data || []).map((role: any) => ({
+        ...role,
+        // 如果后端返回的是 roleId,也确保 id 字段存在
+        id: role.roleId || role.id,
+        // 如果后端返回的是 id,也确保 roleId 字段存在
+        roleId: role.roleId || role.id
+      }));
+      roleList.value = roles;
+    } else {
+      ElMessage.error(res.msg || "获取角色列表失败");
+    }
+  } catch (error) {
+    console.error("获取角色列表失败:", error);
+    ElMessage.error("获取角色列表失败,请重试");
+  }
+};
+
+// 将扁平格式的权限数据转换为树形结构
+const transformFlatPermissionsToTree = (permissionList: RolePermissionItem[]): PermissionItem[] => {
+  const level1Map = new Map<string, PermissionItem>();
+  const level2Map = new Map<string, PermissionItem>();
+  const level3Map = new Map<string, PermissionItem>();
+  let idCounter = 1;
+
+  // 遍历所有权限项,构建树形结构
+  permissionList.forEach(item => {
+    // 处理一级权限
+    if (item.level1Permission) {
+      let level1 = level1Map.get(item.level1Permission);
+
+      if (!level1) {
+        level1 = {
+          id: idCounter++,
+          name: item.level1Permission,
+          checked: false,
+          expanded: true, // 一级权限默认展开
+          children: []
+        };
+        level1Map.set(item.level1Permission, level1);
+      }
+
+      // 处理二级权限
+      if (item.level2Permission) {
+        const level2Key = `${item.level1Permission}-${item.level2Permission}`;
+        let level2 = level2Map.get(level2Key);
+
+        if (!level2) {
+          level2 = {
+            id: idCounter++,
+            name: item.level2Permission,
+            checked: false,
+            expanded: false,
+            children: []
+          };
+          level2Map.set(level2Key, level2);
+
+          // 确保一级权限的children数组存在并添加二级权限
+          if (!level1.children) {
+            level1.children = [];
+          }
+          // 检查是否已存在,避免重复添加
+          const existingLevel2 = level1.children.find(child => child.id === level2!.id);
+          if (!existingLevel2) {
+            level1.children.push(level2);
+          }
+        }
+
+        // 处理三级权限
+        if (item.level3Permission) {
+          // 重新获取 level2,确保它存在
+          const currentLevel2 = level2Map.get(level2Key);
+
+          if (currentLevel2) {
+            const level3Key = `${level2Key}-${item.level3Permission}`;
+            let level3 = level3Map.get(level3Key);
+
+            if (!level3) {
+              level3 = {
+                id: idCounter++,
+                name: item.level3Permission,
+                checked: false,
+                expanded: false
+              };
+              level3Map.set(level3Key, level3);
+
+              // 确保二级权限的children数组存在并添加三级权限
+              if (!currentLevel2.children) {
+                currentLevel2.children = [];
+              }
+              // 检查是否已存在,避免重复添加
+              const existingLevel3 = currentLevel2.children.find(child => child.id === level3!.id);
+              if (!existingLevel3) {
+                currentLevel2.children.push(level3);
+              }
+            }
+          }
+        }
+      }
+    }
+  });
+
+  // 返回所有一级权限
+  return Array.from(level1Map.values());
+};
+
+// 加载权限列表
+const loadPermissionList = async () => {
+  if (!subAccountForm.roleId) {
+    permissionList.value = [];
+    return;
+  }
+
+  try {
+    const roleId = Number(subAccountForm.roleId);
+    if (!roleId || isNaN(roleId)) {
+      permissionList.value = [];
+      return;
+    }
+
+    const res = await getRolePermissionTable({ roleId });
+    const code = typeof res.code === "string" ? parseInt(res.code) : res.code;
+    if (code === 200 && res.data) {
+      permissionList.value = transformFlatPermissionsToTree(res.data);
+    } else {
+      ElMessage.error(res.msg || "获取权限列表失败");
+      permissionList.value = [];
+    }
+  } catch (error) {
+    console.error("获取权限列表失败:", error);
+    ElMessage.error("获取权限列表失败,请重试");
+    permissionList.value = [];
+  }
+};
+
+// 递归收集所有选中的权限ID
+const collectCheckedPermissions = (permission: PermissionItem): number[] => {
+  const permissions: number[] = [];
+  if (permission.checked) {
+    permissions.push(permission.id);
+  }
+  if (permission.children) {
+    permission.children.forEach(child => {
+      permissions.push(...collectCheckedPermissions(child));
+    });
+  }
+  return permissions;
+};
+
+// 重置权限列表选中状态
+const resetPermissions = () => {
+  const resetItem = (item: PermissionItem) => {
+    item.checked = false;
+    if (item.children) {
+      item.children.forEach(child => resetItem(child));
+    }
+  };
+  permissionList.value.forEach(item => resetItem(item));
+  subAccountForm.permissions = [];
+};
+
+// 初始化数据
+onMounted(() => {
+  loadRoleList();
+  // 不在这里加载权限列表,等用户选择角色后再加载
+
+  if (isEdit.value) {
+    // 编辑模式:加载子账号数据
+    loadSubAccountData();
+  }
+});
+
+// 加载子账号数据(编辑时)
+const loadSubAccountData = () => {
+  // TODO: 调用接口获取子账号详情
+  // 模拟数据
+  const mockAccountData = {
+    id: Number(accountId.value),
+    accountName: "测试账号",
+    phone: "13800138000",
+    roleId: 1,
+    permissions: [1, 11, 111, 2]
+  };
+
+  subAccountForm.accountName = mockAccountData.accountName;
+  subAccountForm.phone = mockAccountData.phone;
+  subAccountForm.roleId = mockAccountData.roleId;
+  subAccountForm.permissions = mockAccountData.permissions;
+
+  // 设置权限列表的选中状态
+  setPermissionsChecked(mockAccountData.permissions);
+};
+
+// 设置权限选中状态
+const setPermissionsChecked = (permissionIds: number[]) => {
+  const setItemChecked = (item: PermissionItem) => {
+    if (permissionIds.includes(item.id)) {
+      item.checked = true;
+    }
+    if (item.children) {
+      item.children.forEach(child => setItemChecked(child));
+    }
+  };
+  permissionList.value.forEach(item => setItemChecked(item));
+};
+
+// 返回
+const handleBack = () => {
+  router.back();
+};
+
+// 取消
+const handleCancel = () => {
+  router.back();
+};
+
+// 保存
+const handleSave = async () => {
+  if (!subAccountFormRef.value) return;
+
+  await subAccountFormRef.value.validate(async valid => {
+    if (!valid) return;
+
+    saveLoading.value = true;
+    try {
+      // TODO: 调用创建/编辑子账号接口
+      const createdId = localGet("createdId") || localGet("geeker-user")?.userInfo?.storeId || "";
+
+      const createAccountDto = {
+        accountName: subAccountForm.accountName,
+        phone: subAccountForm.phone,
+        roleId: subAccountForm.roleId,
+        storeId: createdId
+      };
+      const res = await createAccountAndAssignRole(createAccountDto);
+      if (res.code === 200) {
+        ElMessage.success(isEdit.value ? "编辑成功" : "创建成功");
+        setTimeout(() => {
+          router.push("/accountRoleManagement/subAccountManagement");
+        }, 1000);
+      } else {
+        ElMessage.error(res.msg || "创建失败");
+      }
+    } catch (error) {
+      ElMessage.error(isEdit.value ? "编辑失败,请重试" : "创建失败,请重试");
+    } finally {
+      saveLoading.value = false;
+    }
+  });
+};
+// 处理角色选择变化
+const handleRoleChange = (value: number | null | undefined) => {
+  subAccountForm.roleId = value as number | undefined;
+  permissionList.value = [];
+  if (value) {
+    loadPermissionList();
+  } else {
+    // 如果清空了角色选择,重置权限列表
+    resetPermissions();
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.sub-account-form-container {
+  min-height: calc(100vh - 84px);
+  padding: 20px;
+  background: #ffffff;
+}
+
+// 头部区域
+.header-section {
+  padding-bottom: 16px;
+  margin-bottom: 24px;
+  border-bottom: 1px solid #e4e7ed;
+}
+
+// 表单内容
+.form-content {
+  max-width: 800px;
+  padding: 24px 0;
+  .form-section {
+    margin-bottom: 32px;
+    .section-title {
+      margin: 0 0 24px;
+      font-size: 16px;
+      font-weight: 600;
+      color: var(--el-text-color-primary);
+    }
+  }
+  :deep(.el-form-item) {
+    margin-bottom: 24px;
+    .el-form-item__label {
+      font-weight: 500;
+      color: var(--el-text-color-regular);
+    }
+  }
+
+  // 权限列表
+  .permission-list {
+    width: 100%;
+    .permission-empty {
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      padding: 40px 0;
+      color: var(--el-text-color-placeholder);
+      .empty-text {
+        font-size: 14px;
+        color: var(--el-text-color-secondary);
+      }
+    }
+  }
+}
+
+// 底部按钮
+.form-footer {
+  display: flex;
+  gap: 12px;
+  justify-content: center;
+  padding: 24px 0;
+  margin-top: 24px;
+  border-top: 1px solid #e4e7ed;
+}
+</style>

+ 312 - 0
src/views/accountRoleManagement/subAccountManagement/index.vue

@@ -0,0 +1,312 @@
+<template>
+  <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="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>
+.sub-account-management-container {
+  min-height: calc(100vh - 84px);
+  padding: 20px;
+  background: #ffffff;
+}
+.table-header-btn {
+  display: flex;
+  gap: 12px;
+  align-items: center;
+}
+
+// 删除对话框内容
+.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;
+  }
+}
+
+// 对话框底部
+.dialog-footer {
+  display: flex;
+  gap: 12px;
+  justify-content: flex-end;
+}
+</style>

+ 36 - 27
src/views/home/components/go-flow.vue

@@ -232,7 +232,8 @@
                 </el-upload>
               </el-form-item>
 
-              <el-form-item label="合同图片" prop="contractImageList">
+              <!-- 合同图片上传表单项 - 已隐藏 (2026-01-17) -->
+              <!-- <el-form-item label="合同图片" prop="contractImageList">
                 <el-upload
                   v-model:file-list="step2Form.contractImageList"
                   :http-request="handleHttpUpload"
@@ -247,9 +248,10 @@
                     <div class="el-upload__tip">({{ step2Form.contractImageList.length }}/20)</div>
                   </template>
                 </el-upload>
-              </el-form-item>
+              </el-form-item> -->
 
-              <el-form-item label="食品经营许可证" prop="foodLicenceImgList" v-if="showFoodLicence">
+              <!-- 食品经营许可证上传表单项 - 已隐藏 (2026-01-17) -->
+              <!-- <el-form-item label="食品经营许可证" prop="foodLicenceImgList" v-if="showFoodLicence">
                 <el-upload
                   v-model:file-list="step2Form.foodLicenceImgList"
                   :http-request="handleHttpUpload"
@@ -264,7 +266,7 @@
                     <div class="el-upload__tip">({{ step2Form.foodLicenceImgList.length }}/1)</div>
                   </template>
                 </el-upload>
-              </el-form-item>
+              </el-form-item> -->
 
               <el-form-item label="娱乐经营许可证" prop="disportLicenceImgList" v-if="showDisportLicence">
                 <el-upload
@@ -545,8 +547,8 @@ const step2Rules: FormRules = {
   ],
   address: [{ required: true, message: "请输入经纬度", trigger: "blur" }],
   businessLicenseAddress: [{ required: true, message: "请上传营业执照", trigger: "change" }],
-  contractImageList: [{ required: true, message: "请上传合同图片", trigger: "change" }],
-  foodLicenceImgList: [{ required: true, message: "请上传食品经营许可证", trigger: "change" }],
+  // contractImageList: [{ required: true, message: "请上传合同图片", trigger: "change" }], // 已隐藏 (2026-01-17)
+  // foodLicenceImgList: [{ required: true, message: "请上传食品经营许可证", trigger: "change" }], // 已隐藏 (2026-01-17)
   disportLicenceImgList: [{ required: true, message: "请上传娱乐经营许可证", trigger: "change" }]
 };
 
@@ -1043,30 +1045,35 @@ const autoOcrRecognition = async (ocrType: string, name: string) => {
     if (res && (res.code === 200 || res.code === "200")) {
       // 只有身份证类型才需要保存识别结果到 ocrResult
       if (ocrType === "ID_CARD" && res.data && Array.isArray(res.data) && res.data.length > 0) {
-        // 从正面(第一个元素)获取姓名
-        const frontData = res.data[0]?.face?.data;
-        // 从反面(第二个元素)获取身份证号,如果没有第二个元素则从正面获取
-        const backData = res.data[1]?.face?.data || frontData;
+        // 从第一个元素获取正面或反面数据
+        const firstItem = res.data[0];
+        const secondItem = res.data[1];
+
+        // 根据数据键值判断哪一个是正面(face)或反面(back)
+        const faceData = firstItem?.face?.data || secondItem?.face?.data;
+        const backData = firstItem?.back?.data || secondItem?.back?.data;
 
-        // 提取姓名
-        const name = frontData?.name || "";
+        // 提取姓名(从正面数据中获取)
+        const name = faceData?.name || "";
 
-        // 提取身份证号,优先从反面获取,如果没有则从正面获取
-        let idCard = backData?.idNumber || frontData?.idNumber || "";
+        // 提取身份证号(优先从正面数据中获取,如果没有则从反面获取)
+        let idCard = faceData?.idNumber || backData?.idNumber || "";
 
         // 如果从 data 中获取不到,尝试从 prism_keyValueInfo 中查找
-        if (!idCard && res.data[1]?.face?.prism_keyValueInfo) {
-          const idNumberInfo = res.data[1].face.prism_keyValueInfo.find((item: any) => item.key === "idNumber" && item.value);
-          if (idNumberInfo) {
-            idCard = idNumberInfo.value;
+        if (!idCard) {
+          // 先从正面查找
+          if (firstItem?.face?.prism_keyValueInfo) {
+            const idNumberInfo = firstItem.face.prism_keyValueInfo.find((item: any) => item.key === "idNumber" && item.value);
+            if (idNumberInfo) {
+              idCard = idNumberInfo.value;
+            }
           }
-        }
-
-        // 如果反面没有,尝试从正面查找
-        if (!idCard && res.data[0]?.face?.prism_keyValueInfo) {
-          const idNumberInfo = res.data[0].face.prism_keyValueInfo.find((item: any) => item.key === "idNumber" && item.value);
-          if (idNumberInfo) {
-            idCard = idNumberInfo.value;
+          // 如果正面没有,再从反面查找
+          if (!idCard && secondItem?.face?.prism_keyValueInfo) {
+            const idNumberInfo = secondItem.face.prism_keyValueInfo.find((item: any) => item.key === "idNumber" && item.value);
+            if (idNumberInfo) {
+              idCard = idNumberInfo.value;
+            }
           }
         }
 
@@ -1322,8 +1329,10 @@ const handleSubmit = async () => {
         return;
       }
       const businessLicenseUrls = getFileUrls(step2Form.businessLicenseAddress);
-      const contractImageUrls = getFileUrls(step2Form.contractImageList);
-      const foodLicenceUrls = getFileUrls(step2Form.foodLicenceImgList);
+      // const contractImageUrls = getFileUrls(step2Form.contractImageList); // 已隐藏 (2026-01-17)
+      // const foodLicenceUrls = getFileUrls(step2Form.foodLicenceImgList); // 已隐藏 (2026-01-17)
+      const contractImageUrls: string[] = []; // 空数组替代 (2026-01-17)
+      const foodLicenceUrls: string[] = []; // 空数组替代 (2026-01-17)
       const disportLicenceUrls = getFileUrls(step2Form.disportLicenceImgList);
 
       const whereAddress = await buildWhereAddress(step2Form.region);