Bladeren bron

基础框架基本搭建完成

Huanyi 1 maand geleden
bovenliggende
commit
a0d7aed3cf

+ 1 - 0
package.json

@@ -29,6 +29,7 @@
     "axios": "1.13.1",
     "crypto-js": "4.2.0",
     "echarts": "5.6.0",
+    "element-china-area-data": "^6.1.0",
     "element-plus": "2.11.7",
     "file-saver": "2.0.5",
     "highlight.js": "11.11.1",

+ 11 - 0
src/api/service/list/index.ts

@@ -0,0 +1,11 @@
+import request from '@/utils/request';
+import { ServiceListVO } from './types';
+import { AxiosPromise } from 'axios';
+
+// 获取可用服务项目列表
+export function listOnStore(): AxiosPromise<ServiceListVO[]> {
+    return request({
+        url: '/service/list/listOnStore',
+        method: 'get'
+    });
+}

+ 4 - 0
src/api/service/list/types.ts

@@ -0,0 +1,4 @@
+export interface ServiceListVO {
+    id: number | string;
+    name: string;
+}

+ 85 - 0
src/api/system/areaStation/index.ts

@@ -0,0 +1,85 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { AreaStationVO, AreaStationForm, AreaStationQuery, SysAreaStationTypeVo, SysAreaStationOnStoreVo } from '@/api/system/areaStation/types';
+
+/**
+ * 查询区域站点列表
+ * @param query
+ * @returns {*}
+ */
+
+export const listAreaStation = (query?: AreaStationQuery): AxiosPromise<AreaStationVO[]> => {
+  return request({
+    url: '/system/areaStation/list',
+    method: 'get',
+    params: query
+  });
+};
+
+/**
+ * 查询区域站点详细
+ * @param id
+ */
+export const getAreaStation = (id: string | number): AxiosPromise<AreaStationVO> => {
+  return request({
+    url: '/system/areaStation/' + id,
+    method: 'get'
+  });
+};
+
+/**
+ * 新增区域站点
+ * @param data
+ */
+export const addAreaStation = (data: AreaStationForm) => {
+  return request({
+    url: '/system/areaStation',
+    method: 'post',
+    data: data
+  });
+};
+
+/**
+ * 修改区域站点
+ * @param data
+ */
+export const updateAreaStation = (data: AreaStationForm) => {
+  return request({
+    url: '/system/areaStation',
+    method: 'put',
+    data: data
+  });
+};
+
+/**
+ * 删除区域站点
+ * @param id
+ */
+export const delAreaStation = (id: string | number | Array<string | number>) => {
+  return request({
+    url: '/system/areaStation/' + id,
+    method: 'delete'
+  });
+};
+
+/**
+ * 获取区域类型
+ * @Author Huanyi
+ */
+export const listType = (): AxiosPromise<SysAreaStationTypeVo[]> => {
+  return request({
+    url: '/system/areaStation/listType',
+    method: 'GET'
+  });
+};
+
+/**
+ * 获取区域列表
+ * @Author Huanyi
+ */
+export const listOnStore = (): AxiosPromise<SysAreaStationOnStoreVo[]> => {
+  return request({
+    url: '/system/areaStation/listOnStore',
+    method: 'GET'
+  });
+};

+ 145 - 0
src/api/system/areaStation/types.ts

@@ -0,0 +1,145 @@
+export interface AreaStationVO {
+  /**
+   * 序号
+   */
+  id: string | number;
+
+  /**
+   * 区域名称
+   */
+  name: string;
+
+  /**
+   * 省市编码
+   */
+  code: string;
+
+  /**
+   * 排序权重
+   */
+  sort: number;
+
+  /**
+   * 详细地址
+   */
+  address: string;
+
+  /**
+   * 站长姓名
+   */
+  leaderName: string;
+
+  /**
+   * 联系电话
+   */
+  contactPhone: string;
+
+  /**
+   * 经度
+   */
+  longitude: number;
+
+  /**
+   * 纬度
+   */
+  latitude: number;
+
+  /**
+   * 类型
+   */
+  type: number;
+
+  /**
+   * 状态
+   */
+  status: number;
+
+    /**
+     * 子对象
+     */
+    children: AreaStationVO[];
+}
+
+export interface AreaStationForm extends BaseEntity {
+  /**
+   * 序号
+   */
+  id?: string | number;
+
+  /**
+   * 区域名称
+   */
+  name?: string;
+
+  /**
+   * 父级ID
+   */
+  parentId?: string | number;
+
+  /**
+   * 省市编码
+   */
+  code?: string;
+
+  /**
+   * 排序权重
+   */
+  sort?: number;
+
+  /**
+   * 详细地址
+   */
+  address?: string;
+
+  /**
+   * 站长姓名
+   */
+  leaderName?: string;
+
+  /**
+   * 联系电话
+   */
+  contactPhone?: string;
+
+  /**
+   * 经度
+   */
+  longitude?: number;
+
+  /**
+   * 纬度
+   */
+  latitude?: number;
+
+  /**
+   * 类型
+   */
+  type?: number;
+
+  /**
+   * 状态
+   */
+  status?: number;
+
+}
+
+export interface AreaStationQuery {
+
+  /**
+   * 日期范围参数
+   */
+  params?: any;
+}
+
+export interface SysAreaStationTypeVo {
+  value?: number;
+  label?: string;
+  style?: string;
+}
+
+export interface SysAreaStationOnStoreVo {
+  id?: number | string;
+  name?: string;
+  type?: number;
+  parentId?: number | string;
+}

+ 84 - 73
src/api/system/role/index.ts

@@ -1,15 +1,25 @@
 import { UserVO } from '@/api/system/user/types';
 import { UserQuery } from '@/api/system/user/types';
 import { AxiosPromise } from 'axios';
-import { RoleQuery, RoleVO, RoleDeptTree } from './types';
+import { RoleQuery, RoleVO, RoleDeptTree, RoleAllVO } from './types';
 import request from '@/utils/request';
 
+/**
+ * 查询所有角色
+ */
+export const listAllRole = (): AxiosPromise<RoleAllVO[]> => {
+    return request({
+        url: '/system/role/listAll',
+        method: 'get'
+    });
+};
+
 export const listRole = (query: RoleQuery): AxiosPromise<RoleVO[]> => {
-  return request({
-    url: '/system/role/list',
-    method: 'get',
-    params: query
-  });
+    return request({
+        url: '/system/role/list',
+        method: 'get',
+        params: query
+    });
 };
 
 /**
@@ -17,31 +27,31 @@ export const listRole = (query: RoleQuery): AxiosPromise<RoleVO[]> => {
  * @param roleIds
  */
 export const optionSelect = (roleIds: (number | string)[]): AxiosPromise<RoleVO[]> => {
-  return request({
-    url: '/system/role/optionselect?roleIds=' + roleIds,
-    method: 'get'
-  });
+    return request({
+        url: '/system/role/optionselect?roleIds=' + roleIds,
+        method: 'get'
+    });
 };
 
 /**
  * 查询角色详细
  */
 export const getRole = (roleId: string | number): AxiosPromise<RoleVO> => {
-  return request({
-    url: '/system/role/' + roleId,
-    method: 'get'
-  });
+    return request({
+        url: '/system/role/' + roleId,
+        method: 'get'
+    });
 };
 
 /**
  * 新增角色
  */
 export const addRole = (data: any) => {
-  return request({
-    url: '/system/role',
-    method: 'post',
-    data: data
-  });
+    return request({
+        url: '/system/role',
+        method: 'post',
+        data: data
+    });
 };
 
 /**
@@ -49,112 +59,113 @@ export const addRole = (data: any) => {
  * @param data
  */
 export const updateRole = (data: any) => {
-  return request({
-    url: '/system/role',
-    method: 'put',
-    data: data
-  });
+    return request({
+        url: '/system/role',
+        method: 'put',
+        data: data
+    });
 };
 
 /**
  * 角色数据权限
  */
 export const dataScope = (data: any) => {
-  return request({
-    url: '/system/role/dataScope',
-    method: 'put',
-    data: data
-  });
+    return request({
+        url: '/system/role/dataScope',
+        method: 'put',
+        data: data
+    });
 };
 
 /**
  * 角色状态修改
  */
 export const changeRoleStatus = (roleId: string | number, status: string) => {
-  const data = {
-    roleId,
-    status
-  };
-  return request({
-    url: '/system/role/changeStatus',
-    method: 'put',
-    data: data
-  });
+    const data = {
+        roleId,
+        status
+    };
+    return request({
+        url: '/system/role/changeStatus',
+        method: 'put',
+        data: data
+    });
 };
 
 /**
  * 删除角色
  */
 export const delRole = (roleId: Array<string | number> | string | number) => {
-  return request({
-    url: '/system/role/' + roleId,
-    method: 'delete'
-  });
+    return request({
+        url: '/system/role/' + roleId,
+        method: 'delete'
+    });
 };
 
 /**
  * 查询角色已授权用户列表
  */
 export const allocatedUserList = (query: UserQuery): AxiosPromise<UserVO[]> => {
-  return request({
-    url: '/system/role/authUser/allocatedList',
-    method: 'get',
-    params: query
-  });
+    return request({
+        url: '/system/role/authUser/allocatedList',
+        method: 'get',
+        params: query
+    });
 };
 
 /**
  * 查询角色未授权用户列表
  */
 export const unallocatedUserList = (query: UserQuery): AxiosPromise<UserVO[]> => {
-  return request({
-    url: '/system/role/authUser/unallocatedList',
-    method: 'get',
-    params: query
-  });
+    return request({
+        url: '/system/role/authUser/unallocatedList',
+        method: 'get',
+        params: query
+    });
 };
 
 /**
  * 取消用户授权角色
  */
 export const authUserCancel = (data: any) => {
-  return request({
-    url: '/system/role/authUser/cancel',
-    method: 'put',
-    data: data
-  });
+    return request({
+        url: '/system/role/authUser/cancel',
+        method: 'put',
+        data: data
+    });
 };
 
 /**
  * 批量取消用户授权角色
  */
 export const authUserCancelAll = (data: any) => {
-  return request({
-    url: '/system/role/authUser/cancelAll',
-    method: 'put',
-    params: data
-  });
+    return request({
+        url: '/system/role/authUser/cancelAll',
+        method: 'put',
+        params: data
+    });
 };
 
 /**
  * 授权用户选择
  */
 export const authUserSelectAll = (data: any) => {
-  return request({
-    url: '/system/role/authUser/selectAll',
-    method: 'put',
-    params: data
-  });
+    return request({
+        url: '/system/role/authUser/selectAll',
+        method: 'put',
+        params: data
+    });
 };
 // 根据角色ID查询部门树结构
 export const deptTreeSelect = (roleId: string | number): AxiosPromise<RoleDeptTree> => {
-  return request({
-    url: '/system/role/deptTree/' + roleId,
-    method: 'get'
-  });
+    return request({
+        url: '/system/role/deptTree/' + roleId,
+        method: 'get'
+    });
 };
 
 export default {
-  optionSelect,
-  listRole
+    optionSelect,
+    listRole,
+    listAllRole
 };

+ 40 - 35
src/api/system/role/types.ts

@@ -2,51 +2,56 @@
  * 菜单树形结构类型
  */
 export interface DeptTreeOption {
-  id: string;
-  label: string;
-  parentId: string;
-  weight: number;
-  children?: DeptTreeOption[];
+    id: string;
+    label: string;
+    parentId: string;
+    weight: number;
+    children?: DeptTreeOption[];
 }
 
 export interface RoleDeptTree {
-  checkedKeys: string[];
-  depts: DeptTreeOption[];
+    checkedKeys: string[];
+    depts: DeptTreeOption[];
+}
+
+export interface RoleAllVO {
+    id: number | string;
+    name: string;
 }
 
 export interface RoleVO extends BaseEntity {
-  roleId: string | number;
-  roleName: string;
-  roleKey: string;
-  roleSort: number;
-  dataScope: string;
-  menuCheckStrictly: boolean;
-  deptCheckStrictly: boolean;
-  status: string;
-  delFlag: string;
-  remark?: any;
-  flag: boolean;
-  menuIds?: Array<string | number>;
-  deptIds?: Array<string | number>;
-  admin: boolean;
+    roleId: string | number;
+    roleName: string;
+    roleKey: string;
+    roleSort: number;
+    dataScope: string;
+    menuCheckStrictly: boolean;
+    deptCheckStrictly: boolean;
+    status: string;
+    delFlag: string;
+    remark?: any;
+    flag: boolean;
+    menuIds?: Array<string | number>;
+    deptIds?: Array<string | number>;
+    admin: boolean;
 }
 
 export interface RoleQuery extends PageQuery {
-  roleName: string;
-  roleKey: string;
-  status: string;
+    roleName: string;
+    roleKey: string;
+    status: string;
 }
 
 export interface RoleForm {
-  roleName: string;
-  roleKey: string;
-  roleSort: number;
-  status: string;
-  menuCheckStrictly: boolean;
-  deptCheckStrictly: boolean;
-  remark: string;
-  dataScope?: string;
-  roleId: string | undefined;
-  menuIds: Array<string | number>;
-  deptIds: Array<string | number>;
+    roleName: string;
+    roleKey: string;
+    roleSort: number;
+    status: string;
+    menuCheckStrictly: boolean;
+    deptCheckStrictly: boolean;
+    remark: string;
+    dataScope?: string;
+    roleId: string | undefined;
+    menuIds: Array<string | number>;
+    deptIds: Array<string | number>;
 }

+ 100 - 0
src/api/system/store/index.ts

@@ -0,0 +1,100 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { StoreListVO, StoreInfoVO, StoreVO, StoreForm, StoreQuery, StoreStatusVO, SysStorePageBo } from '@/api/system/store/types';
+
+// 查询商户门店列表
+export function listOnMerchantStoreInfo(): AxiosPromise<StoreListVO[]> {
+    return request({
+        url: '/system/store/listOnMerchantStoreInfo',
+        method: 'get'
+    });
+}
+
+// 查询用户关联的门店列表
+export function listOnUser(userId?: string | number): AxiosPromise<any> {
+    return request({
+        url: '/system/store/listOnUser',
+        method: 'get',
+        params: { userId }
+    });
+}
+
+// 查询门店详细信息
+export function getStoreInfo(id: string | number): AxiosPromise<StoreInfoVO> {
+    return request({
+        url: '/system/store/getInfo',
+        method: 'get',
+        params: { id }
+    });
+};
+
+/**
+ * 查询门店管理列表
+ * @param query
+ * @returns {*}
+ */
+
+export const listStore = (query?: SysStorePageBo): AxiosPromise<StoreVO[]> => {
+    return request({
+        url: '/system/store/list',
+        method: 'get',
+        params: query
+    });
+};
+
+/**
+ * 查询门店管理详细
+ * @param id
+ */
+export const getStore = (id: string | number): AxiosPromise<StoreVO> => {
+    return request({
+        url: '/system/store/' + id,
+        method: 'get'
+    });
+};
+
+/**
+ * 新增门店管理
+ * @param data
+ */
+export const addStore = (data: StoreForm) => {
+    return request({
+        url: '/system/store',
+        method: 'post',
+        data: data
+    });
+};
+
+/**
+ * 修改门店管理
+ * @param data
+ */
+export const updateStore = (data: StoreForm) => {
+    return request({
+        url: '/system/store',
+        method: 'put',
+        data: data
+    });
+};
+
+/**
+ * 删除门店管理
+ * @param id
+ */
+export const delStore = (id: string | number | Array<string | number>) => {
+    return request({
+        url: '/system/store/' + id,
+        method: 'delete'
+    });
+};
+
+/**
+ * 获取状态列表
+ */
+export const listStoreStatus = (): AxiosPromise<StoreStatusVO[]> => {
+    return request({
+        url: '/system/store/listStatus',
+        method: 'get'
+    });
+};
+

+ 278 - 0
src/api/system/store/types.ts

@@ -0,0 +1,278 @@
+export interface StoreListVO {
+    id: number | string;
+    name: string;
+}
+
+export interface StoreInfoVO {
+  id: number | string;
+  logo: number | string;
+  logoUrl: string;
+  businessLicense: number | string;
+  businessLicenseUrl: string;
+  name: string;
+  tenantCatergories: number | string;
+  tenantCatergoriesName: string;
+  tenantId: string | number;
+  tenantName: string;
+  startBusinessTime: string;
+  endBusinessTime: string;
+  contact: string;
+  contactNumber: string;
+  validity: string;
+  site: number | string;
+  siteName: string;
+  detailAddress: string;
+  status: number | string;
+  longitude: number;
+  latitude: number;
+  services: (number | string)[];
+  serviceOrder: number | string;
+}
+
+export interface StoreVO {
+  /**
+   * 序号
+   */
+  id: string | number;
+
+  /**
+   * LOGO
+   */
+  logo: number;
+
+  /**
+   * LOGOUrl
+   */
+  logoUrl: string;
+  /**
+   * 营业执照
+   */
+  businessLicense: number;
+
+  /**
+   * 营业执照Url
+   */
+  businessLicenseUrl: string;
+  /**
+   * 门店名称
+   */
+  name: string;
+
+  /**
+   * 商户分类
+   */
+  tenantCatergories: number;
+
+  /**
+   * 开始营业时间
+   */
+  startBusinessTime: string;
+
+  /**
+   * 结束营业时间
+   */
+  endBusinessTime: string;
+
+  /**
+   * 联系人
+   */
+  contact: string;
+
+  /**
+   * 联系电话
+   */
+  contactNumber: string;
+
+  /**
+   * 有效期至
+   */
+  validity: string | number;
+
+  /**
+   * 归属站点
+   */
+  site: number;
+
+  /**
+   * 详细地址
+   */
+  detailAddress: string;
+
+  /**
+   * 状态
+   */
+  status: number;
+
+  /**
+   * 服务项目
+   */
+  services: number[];
+
+  /**
+   * 商户ID
+   */
+  tenantId: string;
+
+  /**
+   * 服务单
+   */
+  serviceOrder: number | string;
+
+  /**
+   * 区域编码
+   */
+  areaCode?: string;
+}
+
+export interface StoreForm extends BaseEntity {
+  /**
+   * 序号
+   */
+  id?: string | number;
+
+  /**
+   * LOGO
+   */
+  logo?: number;
+
+  /**
+   * 营业执照
+   */
+  businessLicense?: number;
+
+  /**
+   * 门店名称
+   */
+  name?: string;
+
+  /**
+   * 商户分类
+   */
+  tenantCatergories?: number;
+
+  /**
+   * 开始营业时间
+   */
+  startBusinessTime?: string;
+
+  /**
+   * 结束营业时间
+   */
+  endBusinessTime?: string;
+
+  /**
+   * 联系人
+   */
+  contact?: string;
+
+  /**
+   * 联系电话
+   */
+  contactNumber?: string;
+
+  /**
+   * 有效期至
+   */
+  validity?: string | number;
+
+  /**
+   * 归属站点
+   */
+  site?: number;
+
+  /**
+   * 详细地址
+   */
+  detailAddress?: string;
+
+  /**
+   * 状态
+   */
+  status?: number;
+
+  /**
+   * 经度
+   */
+  longitude?: number;
+
+  /**
+   * 维度
+   */
+  latitude?: number;
+
+  /**
+   * 服务项目
+   */
+  services?: number[];
+
+  /**
+   * 商户ID
+   */
+  tenantId?: string;
+
+  /**
+   * 区域ID
+   */
+  regionId?: number;
+
+  /**
+   * 区域编码
+   */
+  areaCode?: string;
+}
+
+export interface StoreQuery extends PageQuery {
+  /**
+   * 营业执照
+   */
+  businessLicense?: number;
+
+  /**
+   * 门店名称
+   */
+  name?: string;
+
+  /**
+   * 商户分类
+   */
+  tenantCatergories?: number;
+
+  /**
+   * 联系人
+   */
+  contact?: string;
+
+  /**
+   * 有效期至
+   */
+  validity?: string | number;
+
+  /**
+   * 归属站点
+   */
+  site?: number;
+
+  /**
+   * 状态
+   */
+  status?: number;
+
+  /**
+   * 日期范围参数
+   */
+  params?: any;
+}
+
+export interface SysStorePageBo extends PageQuery {
+  storeOrContact?: string;
+  area?: number | string;
+  station?: number | string;
+  status?: number;
+  params?: any;
+}
+
+export interface StoreStatusVO {
+  value: number;
+  label: string;
+  style: string;
+}

+ 17 - 0
src/api/system/tenant/index.ts

@@ -11,6 +11,14 @@ export function listTenant(query: TenantQuery): AxiosPromise<TenantVO[]> {
   });
 }
 
+// 查询当前登录租户信息
+export function getTenantInfo(): AxiosPromise<TenantVO> {
+  return request({
+    url: '/system/tenant/getInfo',
+    method: 'get'
+  });
+}
+
 // 查询租户详细
 export function getTenant(id: string | number): AxiosPromise<TenantVO> {
   return request({
@@ -107,3 +115,12 @@ export function syncTenantConfig() {
     method: 'get'
   });
 }
+
+// 查询门店品牌列表
+export function listOnStore(query?: any) {
+  return request({
+    url: '/system/tenant/listOnStore',
+    method: 'get',
+    params: query
+  });
+}

+ 3 - 0
src/api/system/tenant/types.ts

@@ -14,6 +14,9 @@ export interface TenantVO extends BaseEntity {
   expireTime: string;
   accountCount: number;
   status: string;
+  admin?: number;
+  logo?: number | string;
+  logoUrl?: string;
 }
 
 export interface TenantQuery extends PageQuery {

+ 75 - 0
src/api/system/tenantCategories/index.ts

@@ -0,0 +1,75 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { TenantCategoriesVO, TenantCategoriesForm, TenantCategoriesQuery, SysTenantCategoriesOnStoreVo } from '@/api/system/tenantCategories/types';
+
+/**
+ * 查询商户分类列表
+ * @param query
+ * @returns {*}
+ */
+
+export const listTenantCategories = (query?: TenantCategoriesQuery): AxiosPromise<TenantCategoriesVO[]> => {
+  return request({
+    url: '/system/tenantCategories/list',
+    method: 'get',
+    params: query
+  });
+};
+
+/**
+ * 查询商户分类详细
+ * @param id
+ */
+export const getTenantCategories = (id: string | number): AxiosPromise<TenantCategoriesVO> => {
+  return request({
+    url: '/system/tenantCategories/' + id,
+    method: 'get'
+  });
+};
+
+/**
+ * 新增商户分类
+ * @param data
+ */
+export const addTenantCategories = (data: TenantCategoriesForm) => {
+  return request({
+    url: '/system/tenantCategories',
+    method: 'post',
+    data: data
+  });
+};
+
+/**
+ * 修改商户分类
+ * @param data
+ */
+export const updateTenantCategories = (data: TenantCategoriesForm) => {
+  return request({
+    url: '/system/tenantCategories',
+    method: 'put',
+    data: data
+  });
+};
+
+/**
+ * 删除商户分类
+ * @param id
+ */
+export const delTenantCategories = (id: string | number | Array<string | number>) => {
+  return request({
+    url: '/system/tenantCategories/' + id,
+    method: 'delete'
+  });
+};
+
+/**
+ * 获取商户分类的列表
+ * @Author Huanyi
+ */
+export const listOnStore = (query: PageQuery): AxiosPromise<SysTenantCategoriesOnStoreVo[]> => {
+  return request({
+    url: '/system/tenantCategories/listOnStore',
+    method: 'GET',
+    params: query
+  });
+};

+ 113 - 0
src/api/system/tenantCategories/types.ts

@@ -0,0 +1,113 @@
+export interface TenantCategoriesVO {
+  /**
+   * 序号
+   */
+  id: string | number;
+
+  /**
+   * 分类名称
+   */
+  name: string;
+
+  /**
+   * 排序
+   */
+  sort: number;
+
+  /**
+   * 图标
+   */
+  icon: number;
+
+  /**
+   * 图标Url
+   */
+  iconUrl: string;
+  /**
+   * 状态
+   */
+  status: number;
+
+  /**
+   * 创建者
+   */
+  createBy: number;
+
+  /**
+   * 创建时间
+   */
+  createTime: string;
+
+  /**
+   * 更新者
+   */
+  updateBy: number;
+
+  /**
+   * 更新时间
+   */
+  updateTime: string;
+
+}
+
+export interface TenantCategoriesForm extends BaseEntity {
+  /**
+   * 序号
+   */
+  id?: string | number;
+
+  /**
+   * 分类名称
+   */
+  name?: string;
+
+  /**
+   * 排序
+   */
+  sort?: number;
+
+  /**
+   * 图标
+   */
+  icon?: number;
+
+  /**
+   * 状态
+   */
+  status?: number;
+
+}
+
+export interface TenantCategoriesQuery extends PageQuery {
+
+  /**
+   * 分类名称
+   */
+  name?: string;
+
+  /**
+   * 状态
+   */
+  status?: number;
+
+  /**
+   * 创建时间
+   */
+  createTime?: string;
+
+  /**
+   * 更新时间
+   */
+  updateTime?: string;
+
+  /**
+   * 日期范围参数
+   */
+  params?: any;
+}
+
+export interface SysTenantCategoriesOnStoreVo {
+  id?: number | string;
+  name?: string;
+}
+

+ 1 - 1
src/api/system/user/index.ts

@@ -11,7 +11,7 @@ import { parseStrEmpty } from '@/utils/ruoyi';
  */
 export const listUser = (query: UserQuery): AxiosPromise<UserVO[]> => {
   return request({
-    url: '/system/user/list',
+    url: '/system/user/listOnMerchant',
     method: 'get',
     params: query
   });

+ 3 - 1
src/api/system/user/types.ts

@@ -20,7 +20,7 @@ export interface UserQuery extends PageQuery {
   status?: string;
   deptId?: string | number;
   roleId?: string | number;
-  userIds?:  string | number | (string | number)[] | undefined;
+  userIds?: string | number | (string | number)[] | undefined;
 }
 
 /**
@@ -47,6 +47,7 @@ export interface UserVO extends BaseEntity {
   roleIds: any;
   postIds: any;
   roleId: any;
+  storeIds?: any[];
   admin: boolean;
 }
 
@@ -67,6 +68,7 @@ export interface UserForm {
   remark?: string;
   postIds: string[];
   roleIds: string[];
+  storeIds?: number[];
 }
 
 export interface UserInfoVO {

+ 170 - 0
src/components/PageSelect/index.vue

@@ -0,0 +1,170 @@
+<template>
+  <div class="page-select">
+    <el-select v-bind="{ ...$attrs, value: undefined }" v-model="localValue" :filterable="true" :remote="false"
+      @visible-change="handleVisibleChange" @change="handleInput">
+      <el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" />
+      <!-- 分页组件 -->
+      <div v-if="total > 0" class="brand-pagination">
+        <div class="custom-pagination">
+          <span class="page-arrow" :class="{ disabled: currentPage <= 1 }"
+            @click="currentPage > 1 && handlePageChange(currentPage - 1)">
+            &lt;
+          </span>
+          <template v-for="page in pageNumbers" :key="page">
+            <span class="page-number" :class="{ active: page === currentPage }" @click="handlePageChange(page)">
+              {{ page }}
+            </span>
+          </template>
+          <span class="page-arrow" :class="{ disabled: currentPage >= totalPages }"
+            @click="currentPage < totalPages && handlePageChange(currentPage + 1)">
+            &gt;
+          </span>
+          <span class="total-text">共{{ total }}条</span>
+        </div>
+      </div>
+    </el-select>
+  </div>
+</template>
+
+<script setup name="PageSelect" lang="ts">
+import { ref, computed, watch } from 'vue';
+
+const props = defineProps({
+  modelValue: {
+    type: [String, Number],
+    default: undefined
+  },
+  options: {
+    type: Array,
+    default: () => []
+  },
+  total: {
+    type: Number,
+    default: 0
+  },
+  pageSize: {
+    type: Number,
+    default: 10
+  }
+});
+
+const emit = defineEmits(['update:modelValue', 'page-change', 'visible-change']);
+
+const localValue = ref(props.modelValue);
+const currentPage = ref(1);
+
+// 监听modelValue变化
+watch(
+  () => props.modelValue,
+  (newValue) => {
+    localValue.value = newValue;
+  },
+  { immediate: true }
+);
+
+// 计算属性:总页数
+const totalPages = computed(() => {
+  return Math.ceil(props.total / props.pageSize);
+});
+
+// 计算属性:页码数组
+const pageNumbers = computed(() => {
+  const pages = [];
+  const total = totalPages.value;
+  const current = currentPage.value;
+
+  // 生成页码,最多显示5个页码
+  let start = Math.max(1, current - 2);
+  let end = Math.min(total, start + 4);
+
+  // 调整起始页码,确保显示5个页码
+  if (end - start < 4) {
+    start = Math.max(1, end - 4);
+  }
+
+  for (let i = start; i <= end; i++) {
+    pages.push(i);
+  }
+
+  return pages;
+});
+
+// 处理输入变化
+const handleInput = (value: any) => {
+  emit('update:modelValue', value);
+};
+
+// 处理可见性变化
+const handleVisibleChange = (visible: boolean) => {
+  if (visible) {
+    currentPage.value = 1;
+  }
+  emit('visible-change', visible);
+};
+
+// 处理页面切换
+const handlePageChange = (page: number) => {
+  currentPage.value = page;
+  emit('page-change', page);
+};
+</script>
+
+<style scoped>
+.page-select {
+  width: 100%;
+}
+
+.brand-pagination {
+  margin-top: 10px;
+  padding-top: 10px;
+  border-top: 1px solid #ebeef5;
+  text-align: center;
+}
+
+.custom-pagination {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 10px;
+  font-size: 14px;
+}
+
+.page-arrow {
+  cursor: pointer;
+  color: #606266;
+  user-select: none;
+  padding: 2px 8px;
+  transition: color 0.3s;
+}
+
+.page-arrow:hover:not(.disabled) {
+  color: #409eff;
+}
+
+.page-arrow.disabled {
+  color: #c0c4cc;
+  cursor: not-allowed;
+}
+
+.page-number {
+  cursor: pointer;
+  color: #606266;
+  padding: 2px 8px;
+  transition: all 0.3s;
+}
+
+.page-number:hover {
+  color: #409eff;
+}
+
+.page-number.active {
+  color: #409eff;
+  font-weight: bold;
+}
+
+.total-text {
+  margin-left: 15px;
+  font-size: 12px;
+  color: #909399;
+}
+</style>

+ 1017 - 0
src/views/archieves/customer/index.vue

@@ -0,0 +1,1017 @@
+<template>
+  <div class="page-container">
+    <el-card shadow="never">
+      <template #header>
+        <div class="card-header">
+          <span class="title">用户管理</span>
+          <div class="header-actions">
+            <el-select v-model="searchForm.store" placeholder="录入门店" style="width: 150px; margin-right: 10px" clearable>
+              <el-option label="三里屯店" value="三里屯店" />
+              <el-option label="国贸店" value="国贸店" />
+            </el-select>
+            <el-input v-model="searchForm.keyword" placeholder="搜索姓名/手机号" style="width: 200px; margin-right: 10px" clearable />
+            <el-button type="primary" icon="Plus" @click="handleAdd">新增用户</el-button>
+          </div>
+        </div>
+      </template>
+
+      <el-table :data="filteredTableData" style="width: 100%" :header-cell-style="{ background: '#f5f7fa' }">
+        <el-table-column label="用户基本信息" width="250">
+          <template #default="scope">
+            <div style="display: flex; align-items: center">
+              <el-avatar :size="40" :src="scope.row.avatar" style="margin-right: 10px" />
+              <div>
+                <div style="font-weight: bold">
+                  {{ scope.row.name }}
+                  <el-icon v-if="scope.row.gender === '女'" color="#F56C6C"><Female /></el-icon>
+                  <el-icon v-else color="#409EFF"><Male /></el-icon>
+                </div>
+                <div style="font-size: 12px; color: #999">{{ scope.row.phone }}</div>
+              </div>
+            </div>
+          </template>
+        </el-table-column>
+        <el-table-column prop="address" label="住址" show-overflow-tooltip min-width="150" />
+        <el-table-column label="用户标签" width="200">
+          <template #default="scope">
+            <el-tag v-for="tag in scope.row.tags" :key="tag.name" :type="tag.type" effect="light" size="small" style="margin-right: 5px">{{
+              tag.name
+            }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="录入信息" width="200">
+          <template #default="scope">
+            <div>
+              <el-tag size="small" effect="plain" :type="scope.row.source.includes('平台') ? '' : 'warning'">{{ scope.row.source }}</el-tag>
+            </div>
+            <div style="font-size: 12px; color: #999; margin-top: 4px">创建时间: {{ scope.row.entryTime }}</div>
+          </template>
+        </el-table-column>
+        <el-table-column label="订单数量" width="120" align="center" sortable prop="orderCount">
+          <template #default="scope">
+            <div>{{ scope.row.orderCount }}单</div>
+          </template>
+        </el-table-column>
+        <el-table-column prop="petCount" label="关联宠物" width="100" align="center">
+          <template #default="scope">
+            <el-tag size="small" round>{{ scope.row.petCount }}只</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="状态" width="100" align="center">
+          <template #default="scope">
+            <el-switch v-model="scope.row.status" inline-prompt active-text="正常" inactive-text="停用" @change="handleStatusChange(scope.row)" />
+          </template>
+        </el-table-column>
+        <el-table-column prop="remark" label="备注" show-overflow-tooltip />
+        <el-table-column label="操作" width="200" align="center">
+          <template #default="scope">
+            <el-button link type="primary" size="small" @click="handleDetail(scope.row)">详情</el-button>
+            <el-button link type="primary" size="small" @click="handleEdit(scope.row)">编辑</el-button>
+            <el-dropdown trigger="click" @command="(cmd) => handleCommand(cmd, scope.row)" style="margin-left: 10px; vertical-align: middle">
+              <el-button link type="primary" size="small">
+                更多<el-icon class="el-icon--right"><arrow-down /></el-icon>
+              </el-button>
+              <template #dropdown>
+                <el-dropdown-menu>
+                  <el-dropdown-item command="remark">添加备注</el-dropdown-item>
+                  <el-dropdown-item :command="scope.row.status ? 'disable' : 'enable'">
+                    {{ scope.row.status ? '停用用户' : '启用用户' }}
+                  </el-dropdown-item>
+                  <el-dropdown-item command="delete" style="color: #f56c6c">删除用户</el-dropdown-item>
+                </el-dropdown-menu>
+              </template>
+            </el-dropdown>
+          </template>
+        </el-table-column>
+      </el-table>
+      <div class="pagination-container">
+        <el-pagination
+          v-model:current-page="currentPage"
+          v-model:page-size="pageSize"
+          :page-sizes="[10, 20, 50, 100]"
+          layout="total, sizes, prev, pager, next, jumper"
+          :total="total"
+          @size-change="handleSizeChange"
+          @current-change="handleCurrentChange"
+        />
+      </div>
+    </el-card>
+
+    <!-- User Detail Drawer -->
+    <el-drawer v-model="drawerVisible" title="用户档案详情" size="60%" destroy-on-close>
+      <div class="profile-header">
+        <el-avatar :size="80" :src="currentUser.avatar" />
+        <div class="profile-basic">
+          <div class="name-row">
+            <span class="name">{{ currentUser.name }}</span>
+            <el-tag size="small" :type="currentUser.gender === '公' ? '' : 'danger'" effect="dark" style="margin-left: 10px">
+              {{ currentUser.gender }}
+            </el-tag>
+            <span class="phone">{{ currentUser.phone }}</span>
+          </div>
+          <div class="tags-row" style="margin-top: 8px">
+            <el-tag v-for="tag in currentUser.tags" :key="tag.name" :type="tag.type" effect="light" size="small" style="margin-right: 5px">
+              {{ tag.name }}
+            </el-tag>
+          </div>
+        </div>
+      </div>
+
+      <el-tabs v-model="detailActiveTab" class="profile-tabs">
+        <el-tab-pane label="档案信息" name="info">
+          <div class="section-title">基本信息</div>
+          <el-descriptions :column="2" border>
+            <el-descriptions-item label="姓名">{{ currentUser.name }}</el-descriptions-item>
+            <el-descriptions-item label="电话">{{ currentUser.phone }}</el-descriptions-item>
+            <el-descriptions-item label="关联门店">{{ currentUser.store || '-' }}</el-descriptions-item>
+            <el-descriptions-item label="录入来源">{{ currentUser.source }}</el-descriptions-item>
+            <el-descriptions-item label="录入时间">{{ currentUser.entryTime }}</el-descriptions-item>
+          </el-descriptions>
+
+          <div class="section-title" style="margin-top: 20px">居住信息</div>
+          <el-descriptions :column="2" border>
+            <el-descriptions-item label="详细住址" :span="2">{{ currentUser.address }}</el-descriptions-item>
+            <el-descriptions-item label="房屋类型">
+              {{ currentUser.houseType === 'stairs' ? '楼梯' : '电梯' }}
+            </el-descriptions-item>
+            <el-descriptions-item label="入门方式">
+              {{ currentUser.entryMethod === 'password' ? '密码开门' : '钥匙开门' }}
+            </el-descriptions-item>
+            <el-descriptions-item label="开门详情" :span="2">
+              {{ currentUser.entryMethod === 'password' ? currentUser.entryPassword : currentUser.keyLocation }}
+            </el-descriptions-item>
+          </el-descriptions>
+        </el-tab-pane>
+
+        <el-tab-pane label="宠物列表" name="pets">
+          <div style="margin-bottom: 15px">
+            <el-button type="primary" size="small" icon="Plus" @click="openAddPet">新增宠物</el-button>
+          </div>
+          <el-table :data="currentPets" border style="width: 100%">
+            <el-table-column label="宠物信息" width="200">
+              <template #default="scope">
+                <div style="display: flex; align-items: center">
+                  <el-avatar :size="30" src="https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png" style="margin-right: 8px" />
+                  {{ scope.row.name }}
+                </div>
+              </template>
+            </el-table-column>
+            <el-table-column prop="breed" label="品种" />
+            <el-table-column prop="gender" label="性别" width="60" />
+            <el-table-column prop="age" label="年龄" width="60" />
+            <el-table-column prop="status" label="健康状态">
+              <template #default="scope">
+                <el-tag :type="scope.row.status === '健康' ? 'success' : 'warning'" size="small">{{ scope.row.status }}</el-tag>
+              </template>
+            </el-table-column>
+            <el-table-column label="疫苗接种" width="120" align="center">
+              <template #default="scope">
+                {{ scope.row.vaccine || '-' }}
+              </template>
+            </el-table-column>
+            <el-table-column label="操作" width="200" align="center">
+              <template #default="scope">
+                <el-button link type="primary" @click="handlePetDetail(scope.row)">详情</el-button>
+                <el-button link type="primary" @click="handlePetEdit(scope.row)">编辑</el-button>
+                <el-button link type="primary" @click="handlePetRemark(scope.row)">备注</el-button>
+                <el-button link type="danger" @click="handlePetDelete(scope.row)">删除</el-button>
+              </template>
+            </el-table-column>
+          </el-table>
+        </el-tab-pane>
+
+        <el-tab-pane label="历史订单" name="orders">
+          <el-table :data="mockOrders" border style="width: 100%">
+            <el-table-column prop="orderNo" label="订单编号" width="180" />
+            <el-table-column prop="service" label="服务项目" />
+            <el-table-column prop="pets" label="服务宠物" />
+            <el-table-column prop="time" label="服务时间" width="180" />
+            <el-table-column prop="status" label="状态" width="100">
+              <template #default="scope">
+                <el-tag type="success" size="small">完成</el-tag>
+              </template>
+            </el-table-column>
+          </el-table>
+        </el-tab-pane>
+
+        <el-tab-pane label="档案日志" name="logs">
+          <el-timeline style="margin-top: 10px; padding-left: 5px">
+            <el-timeline-item v-for="(log, index) in mockLogs" :key="index" :timestamp="log.timestamp" :type="log.type">
+              {{ log.content }}
+              <div style="font-size: 12px; color: #999; margin-top: 4px">操作人: {{ log.operator }}</div>
+            </el-timeline-item>
+          </el-timeline>
+        </el-tab-pane>
+      </el-tabs>
+    </el-drawer>
+
+    <!-- Add/Edit User Dialog -->
+    <el-dialog v-model="dialogVisible" :title="isEdit ? '编辑用户' : '新增用户'" width="700px" destroy-on-close>
+      <el-form :model="form" label-width="90px" class="user-form">
+        <el-row :gutter="20">
+          <el-col :span="24" style="text-align: center; margin-bottom: 25px">
+            <el-upload action="#" :show-file-list="false" :auto-upload="false">
+              <el-avatar
+                :size="80"
+                :src="form.avatar || 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png'"
+                class="upload-avatar"
+              />
+              <div style="margin-top: 8px; font-size: 12px; color: #409eff">点击修改头像</div>
+            </el-upload>
+          </el-col>
+
+          <el-col :span="24"><div class="form-section-header">基本资料</div></el-col>
+          <el-col :span="12">
+            <el-form-item label="录入来源">
+              <el-select v-model="form.source" style="width: 100%" filterable allow-create default-first-option placeholder="请选择或输入">
+                <el-option label="客户自助下单" value="客户自助下单" />
+                <el-option label="三里屯店录入" value="三里屯店录入" />
+                <el-option label="国贸店录入" value="国贸店录入" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="姓名" required><el-input v-model="form.name" placeholder="请输入姓名" /></el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="电话" required><el-input v-model="form.phone" placeholder="请输入电话" /></el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="性别">
+              <el-radio-group v-model="form.gender">
+                <el-radio label="男">男</el-radio>
+                <el-radio label="女">女</el-radio>
+              </el-radio-group>
+            </el-form-item>
+          </el-col>
+
+          <el-col :span="24"><div class="form-section-header">居住信息</div></el-col>
+          <el-col :span="24">
+            <el-form-item label="所在地区">
+              <el-cascader v-model="form.region" :options="pcaOptions" placeholder="请选择省/市/区" style="width: 100%" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="24">
+            <el-form-item label="详细住址"><el-input v-model="form.detailAddress" placeholder="请输入街道/门牌号" /></el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="房屋类型">
+              <el-radio-group v-model="form.houseType">
+                <el-radio label="stairs">楼梯</el-radio>
+                <el-radio label="elevator">电梯</el-radio>
+              </el-radio-group>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="入门方式">
+              <el-radio-group v-model="form.entryMethod">
+                <el-radio label="password">密码开门</el-radio>
+                <el-radio label="key">钥匙开门</el-radio>
+              </el-radio-group>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12" v-if="form.entryMethod === 'password'">
+            <el-form-item label="开门密码">
+              <el-input v-model="form.entryPassword" placeholder="请输入密码" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12" v-if="form.entryMethod === 'key'">
+            <el-form-item label="钥匙位置">
+              <el-input v-model="form.keyLocation" placeholder="如:地毯下" />
+            </el-form-item>
+          </el-col>
+
+          <el-col :span="24"><div class="form-section-header">其他</div></el-col>
+          <el-col :span="24">
+            <el-form-item label="用户标签">
+              <el-select v-model="selectedTagIds" multiple placeholder="选择标签" style="width: 100%">
+                <el-option v-for="tag in allUserTags" :key="tag.id" :label="tag.name" :value="tag.id">
+                  <el-tag :type="tag.type" effect="light" size="small">{{ tag.name }}</el-tag>
+                </el-option>
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="24">
+            <el-form-item label="备注说明"><el-input type="textarea" v-model="form.remark" rows="3" /></el-form-item>
+          </el-col>
+        </el-row>
+      </el-form>
+      <div style="text-align: center; margin-top: 20px">
+        <el-button @click="dialogVisible = false" size="large" style="width: 120px">取消</el-button>
+        <el-button type="primary" @click="saveUser" size="large" style="width: 120px">保存</el-button>
+      </div>
+    </el-dialog>
+
+    <!-- Remark Dialog -->
+    <el-dialog v-model="remarkDialogVisible" title="添加备注" width="400px">
+      <el-input v-model="remarkForm.content" type="textarea" :rows="3" placeholder="请输入备注内容..." />
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button @click="remarkDialogVisible = false">取消</el-button>
+          <el-button type="primary" @click="saveRemark">保存</el-button>
+        </span>
+      </template>
+    </el-dialog>
+
+    <!-- Full Add/Edit Pet Dialog -->
+    <el-dialog v-model="petDialogVisible" :title="petForm.id ? '编辑宠物' : '新增宠物'" width="800px">
+      <el-tabs v-model="petDialogActiveTab">
+        <el-tab-pane label="基本信息" name="basic">
+          <el-form :model="petForm" label-width="100px">
+            <el-row>
+              <el-col :span="24" style="display: flex; justify-content: center; margin-bottom: 20px">
+                <el-upload class="avatar-uploader" action="#" :show-file-list="false" :auto-upload="false" :on-change="handlePetUploadFile">
+                  <el-avatar v-if="petForm.avatar" :src="petForm.avatar" :size="80" />
+                  <el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
+                </el-upload>
+              </el-col>
+              <el-col :span="12">
+                <el-form-item label="宠物姓名" required><el-input v-model="petForm.name" /></el-form-item>
+              </el-col>
+              <el-col :span="12">
+                <el-form-item label="性别">
+                  <el-radio-group v-model="petForm.gender">
+                    <el-radio label="公">公</el-radio>
+                    <el-radio label="母">母</el-radio>
+                  </el-radio-group>
+                </el-form-item>
+              </el-col>
+              <el-col :span="12">
+                <el-form-item label="品种">
+                  <el-select v-model="petForm.breed" placeholder="请选择品种" style="width: 100%" filterable allow-create default-first-option>
+                    <el-option v-for="breed in petBreeds" :key="breed" :label="breed" :value="breed" />
+                  </el-select>
+                </el-form-item>
+              </el-col>
+              <el-col :span="12">
+                <el-form-item label="体型">
+                  <el-select v-model="petForm.size" style="width: 100%">
+                    <el-option label="小型" value="small" />
+                    <el-option label="中型" value="medium" />
+                    <el-option label="大型" value="large" />
+                  </el-select>
+                </el-form-item>
+              </el-col>
+              <el-col :span="12">
+                <el-form-item label="体重(kg)"><el-input-number v-model="petForm.weight" :min="0" :precision="1" style="width: 100%" /></el-form-item>
+              </el-col>
+              <el-col :span="12">
+                <el-form-item label="年龄(岁)"><el-input-number v-model="petForm.age" :min="0" style="width: 100%" /></el-form-item>
+              </el-col>
+              <el-col :span="24">
+                <el-form-item label="性格关键词"><el-input v-model="petForm.personality" placeholder="如:活泼、粘人" /></el-form-item>
+              </el-col>
+              <el-col :span="24">
+                <el-form-item label="萌宠性格"><el-input v-model="petForm.cutePersonality" type="textarea" placeholder="详细描述" /></el-form-item>
+              </el-col>
+              <el-col :span="24">
+                <el-form-item label="宠物标签">
+                  <el-select v-model="petForm.tags" multiple placeholder="选择标签" style="width: 100%">
+                    <el-option v-for="tag in allPetTags" :key="tag.name" :label="tag.name" :value="tag.name">
+                      <el-tag :type="tag.type" effect="light" size="small">{{ tag.name }}</el-tag>
+                    </el-option>
+                  </el-select>
+                </el-form-item>
+              </el-col>
+            </el-row>
+          </el-form>
+        </el-tab-pane>
+        <el-tab-pane label="家庭信息" name="family">
+          <el-form :model="petForm" label-width="120px">
+            <el-form-item label="新来家庭时间">
+              <el-date-picker v-model="petForm.arrivalTime" type="date" placeholder="选择日期" style="width: 100%" />
+            </el-form-item>
+            <el-form-item label="家庭房屋类型">
+              <el-radio-group v-model="petForm.houseType">
+                <el-radio label="stairs">楼梯</el-radio>
+                <el-radio label="elevator">电梯</el-radio>
+              </el-radio-group>
+            </el-form-item>
+            <el-form-item label="入门方式">
+              <el-radio-group v-model="petForm.entryMethod">
+                <el-radio label="password">密码开门</el-radio>
+                <el-radio label="key">钥匙开门</el-radio>
+              </el-radio-group>
+            </el-form-item>
+            <el-form-item label="密码" v-if="petForm.entryMethod === 'password'">
+              <el-input v-model="petForm.entryPassword" placeholder="请输入门锁密码" />
+            </el-form-item>
+            <el-form-item label="钥匙位置" v-if="petForm.entryMethod === 'key'">
+              <el-input v-model="petForm.keyLocation" placeholder="请输入钥匙存放位置" />
+            </el-form-item>
+          </el-form>
+        </el-tab-pane>
+        <el-tab-pane label="健康状况" name="health">
+          <el-form :model="petForm" label-width="120px">
+            <el-form-item label="健康状态">
+              <el-radio-group v-model="petForm.healthStatus">
+                <el-radio label="健康">健康</el-radio>
+                <el-radio label="亚健康">亚健康</el-radio>
+                <el-radio label="疾病">疾病</el-radio>
+              </el-radio-group>
+            </el-form-item>
+            <el-form-item label="是否有攻击倾向">
+              <el-switch v-model="petForm.aggression" active-text="是" inactive-text="否" />
+            </el-form-item>
+            <el-form-item label="疫苗情况">
+              <el-radio-group v-model="petForm.vaccine">
+                <el-radio label="无">无</el-radio>
+                <el-radio label="已打1次">已打1次</el-radio>
+                <el-radio label="已打2次">已打2次</el-radio>
+                <el-radio label="已打3次">已打3次</el-radio>
+              </el-radio-group>
+            </el-form-item>
+            <el-form-item label="疫苗凭证">
+              <el-upload class="avatar-uploader" action="#" :show-file-list="false" :auto-upload="false" :on-change="handlePetUploadVaccineCert">
+                <img v-if="petForm.vaccineCert" :src="petForm.vaccineCert" class="avatar" style="width: 100px; height: 100px; object-fit: cover" />
+                <el-icon v-else class="avatar-uploader-icon" style="width: 100px; height: 100px; line-height: 100px"><Plus /></el-icon>
+              </el-upload>
+            </el-form-item>
+            <el-form-item label="既往病史">
+              <el-input v-model="petForm.medicalHistory" type="textarea" placeholder="如有病史请记录" />
+            </el-form-item>
+            <el-form-item label="过敏史">
+              <el-input v-model="petForm.allergies" type="textarea" placeholder="如有过敏源请记录" />
+            </el-form-item>
+          </el-form>
+        </el-tab-pane>
+      </el-tabs>
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button @click="petDialogVisible = false">取消</el-button>
+          <el-button type="primary" @click="savePet">保存</el-button>
+        </span>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, computed } from 'vue';
+import { ElMessage, ElMessageBox } from 'element-plus';
+
+const currentPage = ref(1);
+const pageSize = ref(10);
+const total = ref(100);
+
+const handleSizeChange = (val) => {
+  console.log(`每页 ${val} 条`);
+};
+const handleCurrentChange = (val) => {
+  console.log(`当前页: ${val}`);
+};
+
+const searchForm = reactive({
+  keyword: '',
+  store: ''
+});
+
+const dialogVisible = ref(false);
+const drawerVisible = ref(false);
+const remarkDialogVisible = ref(false);
+const petDialogVisible = ref(false);
+const isEdit = ref(false);
+const detailActiveTab = ref('info');
+const petDialogActiveTab = ref('basic');
+
+const selectedTagIds = ref([]);
+const currentUser = ref({});
+const currentPets = ref([]);
+
+// Mock Data
+const allUserTags = [
+  { id: 1, name: '优质客户', type: 'success' },
+  { id: 2, name: '潜在流失', type: 'warning' },
+  { id: 3, name: '黑名单', type: 'danger' }
+];
+
+const petBreeds = [
+  '金毛',
+  '拉布拉多',
+  '柴犬',
+  '柯基',
+  '哈士奇',
+  '阿拉斯加',
+  '萨摩耶',
+  '边境牧羊犬',
+  '德国牧羊犬',
+  '贵宾犬/泰迪',
+  '比熊',
+  '博美',
+  '雪纳瑞',
+  '法斗',
+  '中华田园犬',
+  '英短',
+  '美短',
+  '布偶猫',
+  '加菲猫',
+  '暹罗猫',
+  '波斯猫',
+  '缅因猫',
+  '中华田园猫'
+];
+
+const allPetTags = [
+  { name: '易过敏', type: 'danger' },
+  { name: '胆小', type: 'warning' },
+  { name: '攻击性', type: 'info' },
+  { name: '粘人', type: 'success' }
+];
+
+const tableData = ref([
+  {
+    id: 101,
+    avatar: 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png',
+    name: '张先生',
+    phone: '13800138000',
+    gender: '男',
+    address: '北京市朝阳区三里屯',
+    houseType: 'elevator',
+    entryMethod: 'password',
+    entryPassword: '456',
+    keyLocation: '',
+    remark: '经常周末来',
+    tags: [{ name: '优质客户', type: 'success' }],
+    petCount: 2,
+    entryTime: '2025-01-15 10:00:00',
+    source: '三里屯店录入',
+    orderCount: 12,
+    totalAmount: 3580.0,
+    store: '爱宠生活馆 (三里屯店)',
+    status: true
+  },
+  {
+    id: 102,
+    avatar: 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png',
+    name: '李小姐',
+    phone: '13900139000',
+    gender: '女',
+    address: '上海市浦东新区',
+    houseType: 'stairs',
+    entryMethod: 'key',
+    entryPassword: '',
+    keyLocation: '门口地垫下',
+    remark: '',
+    tags: [],
+    petCount: 0,
+    entryTime: '2025-02-01 14:30:00',
+    source: '爱宠生活馆 (三里屯店)录入',
+    orderCount: 0,
+    totalAmount: 0.0,
+    store: '爱宠生活馆 (国贸店)',
+    status: true
+  }
+]);
+
+const mockOrders = ref([
+  { orderNo: 'DD20231001001', service: '上门喂养 (标准版)', pets: '旺财', time: '2023-10-01 10:00', amount: '88.00', status: 'completed' },
+  { orderNo: 'DD20230915002', service: '深度洗护套餐', pets: '旺财, 咪咪', time: '2023-09-15 14:00', amount: '158.00', status: 'completed' }
+]);
+
+const mockLogs = ref([
+  { content: '用户注册成功', timestamp: '2025-01-15 10:00:00', operator: '系统', type: 'success' },
+  { content: '新增宠物档案 [旺财]', timestamp: '2025-01-15 10:05:00', operator: '张先生', type: 'primary' }
+]);
+
+const form = reactive({
+  id: null,
+  avatar: '',
+  name: '',
+  phone: '',
+  gender: '男',
+  address: '',
+  detailAddress: '',
+  region: [],
+  houseType: 'elevator',
+  entryMethod: 'password',
+  entryPassword: '',
+  keyLocation: '',
+  remark: '',
+  source: '三里屯店录入',
+  entryTime: '',
+  store: '',
+  status: true
+});
+
+const petForm = reactive({
+  id: null,
+  avatar: '',
+  name: '',
+  gender: '公',
+  breed: '',
+  age: 1,
+  size: 'small',
+  weight: 5,
+  personality: '',
+  cutePersonality: '',
+  tags: [],
+
+  arrivalTime: '',
+  houseType: 'stairs',
+  entryMethod: 'key',
+  entryPassword: '',
+  keyLocation: '',
+
+  healthStatus: '健康',
+  aggression: false,
+  vaccine: '无',
+  vaccineCert: '',
+  medicalHistory: '',
+  allergies: ''
+});
+
+const remarkForm = reactive({ content: '' });
+
+const filteredTableData = computed(() => {
+  return tableData.value.filter((item) => {
+    const matchKey = !searchForm.keyword || item.name.includes(searchForm.keyword) || item.phone.includes(searchForm.keyword);
+    const matchStore = !searchForm.store || item.store === searchForm.store;
+    return matchKey && matchStore;
+  });
+});
+
+const handleAdd = () => {
+  isEdit.value = false;
+  selectedTagIds.value = [];
+  Object.assign(form, {
+    id: null,
+    avatar: '',
+    name: '',
+    phone: '',
+    gender: '男',
+    address: '',
+    detailAddress: '',
+    region: [],
+    remark: '',
+    houseType: 'elevator',
+    entryMethod: 'password',
+    entryPassword: '',
+    keyLocation: '',
+    source: '三里屯店录入',
+    entryTime: new Date().toLocaleString().replace(/\//g, '-'),
+    store: '',
+    status: true
+  });
+  dialogVisible.value = true;
+};
+
+const handleEdit = (row) => {
+  isEdit.value = true;
+  Object.assign(form, row);
+  // Mock parsing address to region? For now just keep address string
+  form.detailAddress = row.address; // Simplify: edit mode just show full string in detail
+  form.region = [];
+
+  selectedTagIds.value = row.tags.map((t) => allUserTags.find((at) => at.name === t.name)?.id).filter((id) => id);
+  dialogVisible.value = true;
+};
+
+// Mock PCA Data
+const pcaOptions = [
+  {
+    value: '北京市',
+    label: '北京市',
+    children: [
+      {
+        value: '市辖区',
+        label: '市辖区',
+        children: [
+          { value: '朝阳区', label: '朝阳区' },
+          { value: '海淀区', label: '海淀区' }
+        ]
+      }
+    ]
+  },
+  {
+    value: '上海市',
+    label: '上海市',
+    children: [
+      {
+        value: '市辖区',
+        label: '市辖区',
+        children: [
+          { value: '浦东新区', label: '浦东新区' },
+          { value: '徐汇区', label: '徐汇区' }
+        ]
+      }
+    ]
+  }
+];
+
+const handleDetail = (row) => {
+  currentUser.value = { ...row };
+  detailActiveTab.value = 'info';
+  // Mock Load Pets
+  if (row.petCount > 0) {
+    currentPets.value = [
+      { id: 1, name: '旺财', breed: '金毛', gender: '公', age: 3, status: '健康', vaccine: '已打3次' },
+      { id: 2, name: '咪咪', breed: '加菲猫', gender: '母', age: 2, status: '健康', vaccine: '无' }
+    ].slice(0, row.petCount);
+  } else {
+    currentPets.value = [];
+  }
+  drawerVisible.value = true;
+};
+
+const handleRemark = (row) => {
+  currentUser.value = row;
+  remarkForm.content = '';
+  remarkDialogVisible.value = true;
+};
+
+const saveRemark = () => {
+  if (!remarkForm.content) return ElMessage.warning('请输入内容');
+  mockLogs.value.unshift({
+    content: remarkForm.content,
+    timestamp: new Date().toLocaleString(),
+    operator: '管理员',
+    type: 'warning'
+  });
+  ElMessage.success('备注添加成功');
+  remarkDialogVisible.value = false;
+};
+
+const saveUser = () => {
+  if (!form.name) return ElMessage.warning('请输入姓名');
+
+  const newTags = selectedTagIds.value.map((id) => allUserTags.find((t) => t.id === id));
+
+  const fullAddress = (form.region ? form.region.join('') : '') + form.detailAddress;
+  const saveForm = { ...form, address: fullAddress };
+
+  if (isEdit.value) {
+    const idx = tableData.value.findIndex((item) => item.id === form.id);
+    if (idx !== -1) Object.assign(tableData.value[idx], { ...saveForm, tags: newTags });
+  } else {
+    tableData.value.push({
+      id: Date.now(),
+      ...saveForm,
+      // entryTime is auto set in handleAdd, or here if we want current time on save
+      entryTime: new Date().toLocaleString().replace(/\//g, '-'),
+      tags: newTags,
+      avatar: 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png',
+      petCount: 0,
+      orderCount: 0,
+      totalAmount: 0.0
+    });
+  }
+  ElMessage.success('保存成功');
+  dialogVisible.value = false;
+};
+
+const handleDelete = (row) => {
+  ElMessageBox.confirm('确认删除该用户档案吗?', '提示', { type: 'warning' }).then(() => {
+    tableData.value = tableData.value.filter((item) => item.id !== row.id);
+    ElMessage.success('删除成功');
+  });
+};
+
+const handlePetUploadFile = (file) => {
+  petForm.avatar = URL.createObjectURL(file.raw);
+};
+const handlePetUploadVaccineCert = (file) => {
+  petForm.vaccineCert = URL.createObjectURL(file.raw);
+};
+
+const openAddPet = () => {
+  petDialogActiveTab.value = 'basic';
+  Object.assign(petForm, {
+    id: null,
+    avatar: '',
+    name: '',
+    gender: '公',
+    breed: '',
+    age: 1,
+    size: 'small',
+    weight: 5,
+    ownerId: null,
+    personality: '',
+    cutePersonality: '',
+    tags: [],
+    arrivalTime: '',
+    houseType: 'stairs',
+    entryMethod: 'key',
+    entryPassword: '',
+    keyLocation: '',
+    healthStatus: '健康',
+    aggression: false,
+    vaccine: '无',
+    vaccineCert: '',
+    medicalHistory: '',
+    allergies: ''
+  });
+  petDialogVisible.value = true;
+};
+
+const handlePetDetail = (row) => {
+  // Show simple details or full? The dialog is editable, so detail view can just be the grid or a text overview.
+  // For now, let's just make it show the edit dialog but read-only?
+  // Or just alert like before? User didn't specify, but "功能复刻" implies full detail.
+  // The "Pet Archive" has a Drawer for details. I should probably use that or just use the Edit Dialog for now as "Detailed View".
+  // But since "handlePetEdit" exists, maybe "handlePetDetail" isn't fully implemented in UserList yet.
+  // I will stick to existing alert or simple functionality unless asked.
+  ElMessage.info(`查看宠物 [${row.name}] 详情`);
+};
+
+const handlePetEdit = (row) => {
+  petDialogActiveTab.value = 'basic';
+  const defaults = {
+    avatar: '',
+    name: '',
+    gender: '公',
+    breed: '',
+    age: 1,
+    size: 'small',
+    weight: 5,
+    personality: '',
+    cutePersonality: '',
+    tags: [],
+    arrivalTime: '',
+    houseType: 'stairs',
+    entryMethod: 'key',
+    entryPassword: '',
+    keyLocation: '',
+    healthStatus: '健康',
+    aggression: false,
+    vaccine: '无',
+    vaccineCert: '',
+    medicalHistory: '',
+    allergies: ''
+  };
+  Object.assign(petForm, { ...defaults, ...row });
+  petDialogVisible.value = true;
+};
+
+const handlePetRemark = (row) => {
+  // Reuse main remark dialog but maybe prefix content?
+  remarkForm.content = `[宠物:${row.name}] `;
+  remarkDialogVisible.value = true;
+};
+
+const handlePetDelete = (row) => {
+  ElMessageBox.confirm(`确认删除宠物 [${row.name}] 吗?`, '提示', { type: 'warning' }).then(() => {
+    currentPets.value = currentPets.value.filter((p) => p.id !== row.id);
+    // Update counts
+    if (currentUser.value.id) {
+      const idx = tableData.value.findIndex((item) => item.id === currentUser.value.id);
+      if (idx !== -1) {
+        tableData.value[idx].petCount = currentPets.value.length;
+        currentUser.value.petCount = currentPets.value.length;
+      }
+    }
+    ElMessage.success('宠物删除成功');
+  });
+};
+
+const handleCommand = (command, row) => {
+  if (command === 'remark') {
+    handleRemark(row);
+  } else if (command === 'delete') {
+    handleDelete(row);
+  } else if (command === 'enable' || command === 'disable') {
+    // Toggle status status relies on row.status being updated or we update it manually here if not using v-model in switch
+    // But since we use switch v-model in table, this command might be redundant if switch is there.
+    // However, user asked for 'Operations can modify status'.
+    // If we strictly follow 'Operations bar has Modify Status', then the Switch column is optional but good UX.
+    // Let's assume the switch is the primary way, butdropdown item toggles it too.
+    row.status = command === 'enable';
+    handleStatusChange(row);
+  }
+};
+
+const handleStatusChange = (row) => {
+  ElMessage.success(`${row.name} 已${row.status ? '启用' : '停用'}`);
+};
+
+const savePet = () => {
+  if (!petForm.name) return ElMessage.warning('请输入宠物昵称');
+
+  if (petForm.id) {
+    // Edit existing
+    const idx = currentPets.value.findIndex((p) => p.id === petForm.id);
+    if (idx !== -1) Object.assign(currentPets.value[idx], petForm);
+  } else {
+    // Add new
+    currentPets.value.push({
+      id: Date.now(),
+      ...petForm
+    });
+  }
+
+  // Update main count
+  if (currentUser.value.id) {
+    const idx = tableData.value.findIndex((item) => item.id === currentUser.value.id);
+    if (idx !== -1) {
+      tableData.value[idx].petCount = currentPets.value.length;
+      currentUser.value.petCount = currentPets.value.length;
+    }
+  }
+  ElMessage.success('宠物档案保存成功');
+  petDialogVisible.value = false;
+};
+</script>
+
+<style scoped>
+.page-container {
+  padding: 20px;
+}
+.card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+.title {
+  font-weight: bold;
+}
+
+.profile-header {
+  display: flex;
+  align-items: center;
+  margin-bottom: 20px;
+  padding-bottom: 20px;
+  border-bottom: 1px solid #f0f0f0;
+}
+.profile-basic {
+  margin-left: 20px;
+}
+.name-row {
+  display: flex;
+  align-items: center;
+}
+.name {
+  font-size: 20px;
+  font-weight: bold;
+  color: #303133;
+}
+.phone {
+  margin-left: 10px;
+  color: #666;
+}
+.section-title {
+  font-size: 16px;
+  font-weight: bold;
+  margin-bottom: 15px;
+  border-left: 4px solid #409eff;
+  padding-left: 10px;
+  line-height: 1.2;
+}
+
+.form-section-header {
+  font-weight: bold;
+  margin-bottom: 15px;
+  margin-top: 10px;
+  padding-bottom: 5px;
+  border-bottom: 1px dashed #eee;
+  color: #303133;
+}
+.upload-avatar:hover {
+  cursor: pointer;
+  opacity: 0.8;
+}
+.pagination-container {
+  margin-top: 20px;
+  display: flex;
+  justify-content: flex-end;
+}
+
+/* Add Upload Styles */
+.avatar-uploader .el-upload {
+  cursor: pointer;
+  position: relative;
+  overflow: hidden;
+  display: inline-block;
+}
+.avatar-uploader-icon {
+  font-size: 28px;
+  color: #8c939d;
+  width: 100px;
+  height: 100px;
+  text-align: center;
+  border: 1px dashed #dcdfe6;
+  border-radius: 4px;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  transition: 0.2s;
+}
+.avatar-uploader-icon:hover {
+  border-color: #409eff;
+  color: #409eff;
+}
+.avatar {
+  width: 100px;
+  height: 100px;
+  display: block;
+  border-radius: 4px;
+}
+
+/* Adjust for Pet Avatar (should be round and smaller?) - The template uses the same class, so it will inherit.
+   In logic, pet avatar uses <el-avatar> if exists. If not, it shows icon.
+   I can force roundness for the one in the specific row if I target it, but generic square is usually fine for "upload area".
+   However, user might want it to match PetList which was round for avatar.
+   Let's check PetList styles again.
+   PetList: .avatar-uploader-icon { width: 80px; height: 80px; border-radius: 50%; }
+
+   I'll stick to a generic nice looking square for now as it fits both.
+*/
+</style>

+ 633 - 0
src/views/archieves/pet/index.vue

@@ -0,0 +1,633 @@
+<template>
+  <div class="page-container">
+    <el-card shadow="never">
+      <template #header>
+        <div class="card-header">
+          <span class="title">宠物档案</span>
+          <div class="header-actions">
+            <el-input v-model="searchKey" placeholder="搜索宠物名/主人" style="width: 200px; margin-right: 10px" clearable />
+            <el-button type="primary" icon="Plus" @click="handleAdd">新增档案</el-button>
+          </div>
+        </div>
+      </template>
+
+      <el-table :data="filteredTableData" style="width: 100%">
+        <el-table-column label="宠物信息" width="220">
+          <template #default="scope">
+            <div style="display: flex; align-items: center">
+              <el-avatar :size="50" :src="scope.row.avatar" style="margin-right: 10px" />
+              <div>
+                <div style="font-weight: bold">{{ scope.row.name }}</div>
+                <div style="font-size: 12px; color: #999">{{ scope.row.breed }} | {{ scope.row.age }}岁</div>
+              </div>
+            </div>
+          </template>
+        </el-table-column>
+        <el-table-column prop="gender" label="性别" width="80" align="center">
+          <template #default="scope">
+            <el-tag :type="scope.row.gender === '公' ? '' : 'danger'" effect="plain" size="small">{{ scope.row.gender }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="所属主人" width="180">
+          <template #default="scope">
+            <div>{{ scope.row.ownerName }}</div>
+            <div style="font-size: 12px; color: #666">{{ scope.row.ownerPhone }}</div>
+          </template>
+        </el-table-column>
+        <el-table-column label="标签" min-width="150">
+          <template #default="scope">
+            <el-tag v-for="tag in scope.row.tags" :key="tag" :type="getTagType(tag)" effect="light" size="small" style="margin-right: 5px">{{
+              tag
+            }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="健康状态" width="100" align="center">
+          <template #default="scope">
+            <el-tag :type="scope.row.healthStatus === '健康' ? 'success' : 'warning'" effect="dark" size="small">{{ scope.row.healthStatus }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="疫苗接种" width="120" align="center">
+          <template #default="scope">
+            {{ scope.row.vaccine || '-' }}
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" width="200" align="center">
+          <template #default="scope">
+            <el-button link type="primary" @click="handleDetail(scope.row)">详情</el-button>
+            <el-button link type="primary" @click="handleEdit(scope.row)">编辑</el-button>
+            <el-button link type="primary" @click="handleRemark(scope.row)">备注</el-button>
+            <el-button link type="danger" @click="handleDelete(scope.row)">删除</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+      <div class="pagination-container">
+        <el-pagination
+          v-model:current-page="currentPage"
+          v-model:page-size="pageSize"
+          :page-sizes="[10, 20, 50, 100]"
+          layout="total, sizes, prev, pager, next, jumper"
+          :total="total"
+          @size-change="handleSizeChange"
+          @current-change="handleCurrentChange"
+        />
+      </div>
+    </el-card>
+
+    <el-dialog v-model="dialogVisible" title="宠物档案详情" width="800px">
+      <el-tabs v-model="activeTab">
+        <el-tab-pane label="基本信息" name="basic">
+          <el-form :model="form" label-width="100px">
+            <el-row>
+              <el-col :span="24" style="display: flex; justify-content: center; margin-bottom: 20px">
+                <el-upload class="avatar-uploader" action="#" :show-file-list="false" :auto-upload="false" :on-change="handleUploadFile">
+                  <el-avatar v-if="form.avatar" :src="form.avatar" :size="80" />
+                  <el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
+                </el-upload>
+              </el-col>
+              <el-col :span="12">
+                <el-form-item label="宠物姓名" required><el-input v-model="form.name" /></el-form-item>
+              </el-col>
+              <el-col :span="12">
+                <el-form-item label="所属主人" required>
+                  <el-select v-model="form.ownerId" placeholder="选择主人" style="width: 100%" filterable>
+                    <el-option v-for="user in userList" :key="user.id" :label="user.name + ' - ' + user.phone" :value="user.id" />
+                  </el-select>
+                </el-form-item>
+              </el-col>
+              <el-col :span="12">
+                <el-form-item label="性别">
+                  <el-radio-group v-model="form.gender">
+                    <el-radio label="公">公</el-radio>
+                    <el-radio label="母">母</el-radio>
+                  </el-radio-group>
+                </el-form-item>
+              </el-col>
+              <el-col :span="12">
+                <el-form-item label="品种">
+                  <el-select v-model="form.breed" placeholder="请选择品种" style="width: 100%" filterable allow-create default-first-option>
+                    <el-option v-for="breed in petBreeds" :key="breed" :label="breed" :value="breed" />
+                  </el-select>
+                </el-form-item>
+              </el-col>
+              <el-col :span="12">
+                <el-form-item label="体型">
+                  <el-select v-model="form.size" style="width: 100%">
+                    <el-option label="小型" value="small" />
+                    <el-option label="中型" value="medium" />
+                    <el-option label="大型" value="large" />
+                  </el-select>
+                </el-form-item>
+              </el-col>
+              <el-col :span="12">
+                <el-form-item label="体重(kg)"><el-input-number v-model="form.weight" :min="0" :precision="1" style="width: 100%" /></el-form-item>
+              </el-col>
+              <el-col :span="12">
+                <el-form-item label="年龄(岁)"><el-input-number v-model="form.age" :min="0" style="width: 100%" /></el-form-item>
+              </el-col>
+              <el-col :span="24">
+                <el-form-item label="性格关键词"><el-input v-model="form.personality" placeholder="如:活泼、粘人" /></el-form-item>
+              </el-col>
+              <el-col :span="24">
+                <el-form-item label="萌宠性格"><el-input v-model="form.cutePersonality" type="textarea" placeholder="详细描述" /></el-form-item>
+              </el-col>
+              <el-col :span="24">
+                <el-form-item label="宠物标签">
+                  <el-select v-model="form.tags" multiple placeholder="选择标签" style="width: 100%">
+                    <el-option v-for="tag in allPetTags" :key="tag.name" :label="tag.name" :value="tag.name">
+                      <el-tag :type="tag.type" effect="light" size="small">{{ tag.name }}</el-tag>
+                    </el-option>
+                  </el-select>
+                </el-form-item>
+              </el-col>
+            </el-row>
+          </el-form>
+        </el-tab-pane>
+        <el-tab-pane label="家庭信息" name="family">
+          <el-form :model="form" label-width="120px">
+            <el-form-item label="新来家庭时间">
+              <el-date-picker v-model="form.arrivalTime" type="date" placeholder="选择日期" style="width: 100%" />
+            </el-form-item>
+            <el-form-item label="家庭房屋类型">
+              <el-radio-group v-model="form.houseType">
+                <el-radio label="stairs">楼梯</el-radio>
+                <el-radio label="elevator">电梯</el-radio>
+              </el-radio-group>
+            </el-form-item>
+            <el-form-item label="入门方式">
+              <el-radio-group v-model="form.entryMethod">
+                <el-radio label="password">密码开门</el-radio>
+                <el-radio label="key">钥匙开门</el-radio>
+              </el-radio-group>
+            </el-form-item>
+            <el-form-item label="密码" v-if="form.entryMethod === 'password'">
+              <el-input v-model="form.entryPassword" placeholder="请输入门锁密码" />
+            </el-form-item>
+            <el-form-item label="钥匙位置" v-if="form.entryMethod === 'key'">
+              <el-input v-model="form.keyLocation" placeholder="请输入钥匙存放位置" />
+            </el-form-item>
+          </el-form>
+        </el-tab-pane>
+        <el-tab-pane label="健康状况" name="health">
+          <el-form :model="form" label-width="120px">
+            <el-form-item label="健康状态">
+              <el-radio-group v-model="form.healthStatus">
+                <el-radio label="健康">健康</el-radio>
+                <el-radio label="亚健康">亚健康</el-radio>
+                <el-radio label="疾病">疾病</el-radio>
+              </el-radio-group>
+            </el-form-item>
+            <el-form-item label="是否有攻击倾向">
+              <el-switch v-model="form.aggression" active-text="是" inactive-text="否" />
+            </el-form-item>
+            <el-form-item label="疫苗情况">
+              <el-radio-group v-model="form.vaccine">
+                <el-radio label="无">无</el-radio>
+                <el-radio label="已打1次">已打1次</el-radio>
+                <el-radio label="已打2次">已打2次</el-radio>
+                <el-radio label="已打3次">已打3次</el-radio>
+              </el-radio-group>
+            </el-form-item>
+            <el-form-item label="疫苗凭证">
+              <el-upload class="avatar-uploader" action="#" :show-file-list="false" :auto-upload="false" :on-change="handleUploadVaccineCert">
+                <img v-if="form.vaccineCert" :src="form.vaccineCert" class="avatar" style="width: 100px; height: 100px; object-fit: cover" />
+                <el-icon v-else class="avatar-uploader-icon" style="width: 100px; height: 100px; line-height: 100px"><Plus /></el-icon>
+              </el-upload>
+            </el-form-item>
+            <el-form-item label="既往病史">
+              <el-input v-model="form.medicalHistory" type="textarea" placeholder="如有病史请记录" />
+            </el-form-item>
+            <el-form-item label="过敏史">
+              <el-input v-model="form.allergies" type="textarea" placeholder="如有过敏源请记录" />
+            </el-form-item>
+          </el-form>
+        </el-tab-pane>
+      </el-tabs>
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button @click="dialogVisible = false">取消</el-button>
+          <el-button type="primary" @click="saveData">保存</el-button>
+        </span>
+      </template>
+    </el-dialog>
+
+    <!-- Pet Profile Drawer -->
+    <el-drawer v-model="drawerVisible" title="宠物档案详情" size="60%" destroy-on-close>
+      <div class="profile-header">
+        <el-avatar :size="80" :src="currentPet.avatar" />
+        <div class="profile-basic">
+          <div class="name-row">
+            <span class="name">{{ currentPet.name }}</span>
+            <el-tag size="small" :type="currentPet.gender === '公' ? '' : 'danger'" effect="dark" style="margin-left: 10px">
+              {{ currentPet.gender }}
+            </el-tag>
+            <el-tag size="small" effect="plain" type="info" style="margin-left: 5px">{{ currentPet.age }}岁</el-tag>
+          </div>
+          <div class="tags-row" style="margin-top: 8px">
+            <el-tag v-for="tag in currentPet.tags" :key="tag" :type="getTagType(tag)" effect="light" size="small" style="margin-right: 5px">
+              {{ tag }}
+            </el-tag>
+          </div>
+        </div>
+        <div style="margin-left: auto">
+          <el-button type="primary" size="small" plain @click="handleRemark(currentPet)">添加备注</el-button>
+        </div>
+      </div>
+
+      <el-tabs v-model="detailActiveTab" class="profile-tabs">
+        <el-tab-pane label="档案信息" name="info">
+          <div class="section-title">基本信息</div>
+          <el-descriptions :column="2" border>
+            <el-descriptions-item label="品种">{{ currentPet.breed }}</el-descriptions-item>
+            <el-descriptions-item label="体型">
+              {{ currentPet.size === 'small' ? '小型' : currentPet.size === 'medium' ? '中型' : '大型' }}
+            </el-descriptions-item>
+            <el-descriptions-item label="体重">{{ currentPet.weight }} kg</el-descriptions-item>
+            <el-descriptions-item label="所属主人">{{ currentPet.ownerName }} ({{ currentPet.ownerPhone }})</el-descriptions-item>
+            <el-descriptions-item label="性格关键词">{{ currentPet.personality || '-' }}</el-descriptions-item>
+            <el-descriptions-item label="萌宠性格" :span="2">{{ currentPet.cutePersonality || '-' }}</el-descriptions-item>
+          </el-descriptions>
+
+          <div class="section-title" style="margin-top: 20px">家庭信息</div>
+          <el-descriptions :column="2" border>
+            <el-descriptions-item label="到家时间">{{ currentPet.arrivalTime || '-' }}</el-descriptions-item>
+            <el-descriptions-item label="房屋类型">
+              {{ currentPet.houseType === 'stairs' ? '楼梯' : '电梯' }}
+            </el-descriptions-item>
+            <el-descriptions-item label="入门方式">
+              {{ currentPet.entryMethod === 'password' ? '密码开门' : '钥匙开门' }}
+            </el-descriptions-item>
+            <el-descriptions-item label="开门详情">
+              {{ currentPet.entryMethod === 'password' ? currentPet.entryPassword : currentPet.keyLocation }}
+            </el-descriptions-item>
+          </el-descriptions>
+
+          <div class="section-title" style="margin-top: 20px">健康状况</div>
+          <el-descriptions :column="2" border>
+            <el-descriptions-item label="健康状态">
+              <el-tag :type="currentPet.healthStatus === '健康' ? 'success' : 'warning'" size="small">{{ currentPet.healthStatus }}</el-tag>
+            </el-descriptions-item>
+            <el-descriptions-item label="攻击倾向">
+              <el-tag :type="currentPet.aggression ? 'danger' : 'success'" size="small">{{ currentPet.aggression ? '有' : '无' }}</el-tag>
+            </el-descriptions-item>
+            <el-descriptions-item label="疫苗情况" :span="2">
+              <div>{{ currentPet.vaccine || '-' }}</div>
+              <div v-if="currentPet.vaccineCert" style="margin-top: 10px">
+                <el-image
+                  style="width: 100px; height: 100px; border-radius: 4px"
+                  :src="currentPet.vaccineCert"
+                  :preview-src-list="[currentPet.vaccineCert]"
+                  fit="cover"
+                />
+              </div>
+            </el-descriptions-item>
+            <el-descriptions-item label="既往病史" :span="2">{{ currentPet.medicalHistory || '-' }}</el-descriptions-item>
+            <el-descriptions-item label="过敏史" :span="2">{{ currentPet.allergies || '-' }}</el-descriptions-item>
+          </el-descriptions>
+        </el-tab-pane>
+
+        <el-tab-pane label="历史订单" name="orders">
+          <el-table :data="mockOrders" border style="width: 100%">
+            <el-table-column prop="orderNo" label="订单编号" width="180" />
+            <el-table-column prop="service" label="服务项目" />
+            <el-table-column prop="time" label="服务时间" width="180" />
+            <el-table-column prop="amount" label="金额" width="100" />
+            <el-table-column prop="status" label="状态" width="100">
+              <template #default="scope">
+                <el-tag type="success" size="small">完成</el-tag>
+              </template>
+            </el-table-column>
+          </el-table>
+        </el-tab-pane>
+
+        <el-tab-pane label="备注日志" name="logs">
+          <el-timeline style="margin-top: 10px; padding-left: 5px">
+            <el-timeline-item v-for="(activity, index) in mockLogs" :key="index" :timestamp="activity.timestamp" :type="activity.type">
+              {{ activity.content }}
+              <div style="font-size: 12px; color: #999; margin-top: 4px">操作人: {{ activity.operator }}</div>
+            </el-timeline-item>
+          </el-timeline>
+        </el-tab-pane>
+      </el-tabs>
+    </el-drawer>
+
+    <!-- Remark Dialog -->
+    <el-dialog v-model="remarkDialogVisible" title="添加备注" width="400px">
+      <el-input v-model="remarkForm.content" type="textarea" :rows="3" placeholder="请输入备注内容..." />
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button @click="remarkDialogVisible = false">取消</el-button>
+          <el-button type="primary" @click="saveRemark">保存</el-button>
+        </span>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, computed } from 'vue';
+import { ElMessage, ElMessageBox } from 'element-plus';
+
+const searchKey = ref('');
+const currentPage = ref(1);
+const pageSize = ref(10);
+const total = ref(100);
+
+const handleSizeChange = (val) => {
+  console.log(`每页 ${val} 条`);
+};
+const handleCurrentChange = (val) => {
+  console.log(`当前页: ${val}`);
+};
+
+const dialogVisible = ref(false);
+const drawerVisible = ref(false);
+const remarkDialogVisible = ref(false);
+const isEdit = ref(false);
+const activeTab = ref('basic');
+const detailActiveTab = ref('info');
+const currentPet = ref({});
+
+const mockOrders = ref([
+  { orderNo: 'DD20231001001', service: '上门喂养 (标准版)', time: '2023-10-01 10:00', amount: '88.00', status: 'completed' },
+  { orderNo: 'DD20230915002', service: '深度洗护套餐', time: '2023-09-15 14:00', amount: '158.00', status: 'completed' }
+]);
+
+const mockLogs = ref([
+  { content: '因宠物胆小,建议安排资深服务师', timestamp: '2023-10-01 09:00', operator: '客服小美', type: 'warning' },
+  { content: '更新了疫苗接种记录', timestamp: '2023-09-10 11:30', operator: '管理员', type: 'primary' },
+  { content: '创建宠物档案', timestamp: '2023-01-01 10:00', operator: '系统', type: 'info' }
+]);
+
+const remarkForm = reactive({ content: '' });
+
+// Mock Users for selection
+const userList = [
+  { id: 101, name: '张先生', phone: '13800138000' },
+  { id: 102, name: '李小姐', phone: '13900139000' }
+];
+
+const petBreeds = [
+  '金毛',
+  '拉布拉多',
+  '柴犬',
+  '柯基',
+  '哈士奇',
+  '阿拉斯加',
+  '萨摩耶',
+  '边境牧羊犬',
+  '德国牧羊犬',
+  '贵宾犬/泰迪',
+  '比熊',
+  '博美',
+  '雪纳瑞',
+  '法斗',
+  '中华田园犬',
+  '英短',
+  '美短',
+  '布偶猫',
+  '加菲猫',
+  '暹罗猫',
+  '波斯猫',
+  '缅因猫',
+  '中华田园猫'
+];
+
+const allPetTags = [
+  { name: '易过敏', type: 'danger' },
+  { name: '胆小', type: 'warning' },
+  { name: '攻击性', type: 'info' },
+  { name: '粘人', type: 'success' }
+];
+
+const getTagType = (name) => {
+  const tag = allPetTags.find((t) => t.name === name);
+  return tag ? tag.type : 'info';
+};
+
+const tableData = ref([
+  {
+    id: 1,
+    avatar: 'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png',
+    name: '旺财',
+    gender: '公',
+    breed: '金毛',
+    age: 3,
+    size: 'large',
+    weight: 30,
+    ownerId: 101,
+    ownerName: '张先生',
+    ownerPhone: '13800138000',
+    tags: ['易过敏', '胆小'],
+    healthStatus: '健康',
+    personality: '活泼',
+    cutePersonality: '超级粘人,喜欢玩球',
+    arrivalTime: '2023-01-01',
+    houseType: 'elevator',
+    entryMethod: 'password',
+    entryPassword: '456',
+    aggression: false,
+    vaccine: '已打3次',
+    vaccineCert: 'https://cube.elemecdn.com/6/94/4d3ea53c084bad6931a56d5158a48jpeg.jpeg',
+    medicalHistory: '',
+    allergies: '海鲜'
+  }
+]);
+
+const filteredTableData = computed(() => {
+  return tableData.value.filter((item) => !searchKey.value || item.name.includes(searchKey.value) || item.ownerName.includes(searchKey.value));
+});
+
+const form = reactive({
+  id: null,
+  avatar: '',
+  name: '',
+  gender: '公',
+  breed: '',
+  age: 1,
+  size: 'small',
+  weight: 5,
+  ownerId: null,
+  personality: '',
+  cutePersonality: '',
+  tags: [],
+
+  arrivalTime: '',
+  houseType: 'stairs',
+  entryMethod: 'key',
+  entryPassword: '',
+  keyLocation: '',
+
+  healthStatus: '健康',
+  aggression: false,
+  vaccine: '无',
+  vaccineCert: '',
+  medicalHistory: '',
+  allergies: ''
+});
+
+const handleAdd = () => {
+  isEdit.value = false;
+  activeTab.value = 'basic';
+  Object.assign(form, {
+    id: null,
+    avatar: '',
+    name: '',
+    gender: '公',
+    breed: '',
+    age: 1,
+    size: 'small',
+    weight: 5,
+    ownerId: null,
+    personality: '',
+    cutePersonality: '',
+    tags: [],
+    arrivalTime: '',
+    houseType: 'stairs',
+    entryMethod: 'key',
+    entryPassword: '',
+    keyLocation: '',
+    healthStatus: '健康',
+    aggression: false,
+    vaccine: '无',
+    vaccineCert: '',
+    medicalHistory: '',
+    allergies: ''
+  });
+  dialogVisible.value = true;
+};
+
+const handleEdit = (row) => {
+  isEdit.value = true;
+  activeTab.value = 'basic';
+  Object.assign(form, row);
+  dialogVisible.value = true;
+};
+
+const handleDetail = (row) => {
+  currentPet.value = { ...row };
+  drawerVisible.value = true;
+};
+
+const handleRemark = (row) => {
+  currentPet.value = row;
+  remarkForm.content = '';
+  remarkDialogVisible.value = true;
+};
+
+const saveRemark = () => {
+  if (!remarkForm.content) return ElMessage.warning('请输入内容');
+  mockLogs.value.unshift({
+    content: remarkForm.content,
+    timestamp: new Date().toLocaleString(),
+    operator: '当前用户',
+    type: 'primary'
+  });
+  ElMessage.success('备注添加成功');
+  remarkDialogVisible.value = false;
+};
+
+const handleDelete = (row) => {
+  ElMessageBox.confirm('确认删除该宠物档案吗?', '提示', { type: 'warning' }).then(() => {
+    tableData.value = tableData.value.filter((item) => item.id !== row.id);
+    ElMessage.success('删除成功');
+  });
+};
+
+const handleUploadFile = (file) => {
+  form.avatar = URL.createObjectURL(file.raw);
+};
+
+const handleUploadVaccineCert = (file) => {
+  form.vaccineCert = URL.createObjectURL(file.raw);
+};
+
+const saveData = () => {
+  if (!form.name) return ElMessage.warning('请输入宠物姓名');
+  if (!form.ownerId) return ElMessage.warning('请选择所属主人');
+
+  const owner = userList.find((u) => u.id === form.ownerId);
+  const saveData = {
+    ...form,
+    ownerName: owner ? owner.name : 'Unknown',
+    ownerPhone: owner ? owner.phone : ''
+  };
+
+  if (isEdit.value) {
+    const idx = tableData.value.findIndex((item) => item.id === form.id);
+    if (idx !== -1) Object.assign(tableData.value[idx], saveData);
+  } else {
+    tableData.value.push({
+      id: Date.now(),
+      ...saveData
+    });
+  }
+  ElMessage.success('保存成功');
+  dialogVisible.value = false;
+};
+</script>
+
+<script>
+// Separate setup logic needed for function declarations not hoisted? No, script setup handles it.
+// Additional functions
+</script>
+
+<style scoped>
+.page-container {
+  padding: 20px;
+}
+.card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+.title {
+  font-weight: bold;
+}
+.avatar-uploader-icon {
+  font-size: 28px;
+  color: #8c939d;
+  width: 80px;
+  height: 80px;
+  text-align: center;
+  border: 1px dashed #dcdfe6;
+  border-radius: 50%;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+.avatar-uploader-icon:hover {
+  border-color: var(--el-color-primary);
+}
+.profile-header {
+  display: flex;
+  align-items: center;
+  margin-bottom: 20px;
+  padding-bottom: 20px;
+  border-bottom: 1px solid #f0f0f0;
+}
+.profile-basic {
+  margin-left: 20px;
+}
+.name-row {
+  display: flex;
+  align-items: center;
+}
+.name {
+  font-size: 20px;
+  font-weight: bold;
+  color: #303133;
+}
+.section-title {
+  font-size: 16px;
+  font-weight: bold;
+  margin-bottom: 15px;
+  border-left: 4px solid #409eff;
+  padding-left: 10px;
+  line-height: 1.2;
+}
+.pagination-container {
+  margin-top: 20px;
+  display: flex;
+  justify-content: flex-end;
+}
+</style>

+ 130 - 0
src/views/merchant/brand/index.vue

@@ -0,0 +1,130 @@
+<template>
+  <div class="page-container">
+    <el-card shadow="never">
+      <template #header>
+        <div class="card-header">
+          <span>品牌信息设置</span>
+        </div>
+      </template>
+      <el-form :model="brandForm" label-width="120px" class="brand-form">
+        <el-form-item label="品牌Logo">
+          <el-avatar v-if="brandForm.logo" :size="100" shape="square" :src="brandForm.logo" />
+        </el-form-item>
+
+        <el-form-item label="品牌名称" required>
+          <el-input v-model="brandForm.name" style="width: 400px" disabled />
+        </el-form-item>
+
+        <el-form-item label="商户账号" required>
+          <el-input v-model="brandForm.account" placeholder="用于商户后台登录" style="width: 400px" disabled />
+        </el-form-item>
+
+<!--        <el-form-item label="登录密码" required>-->
+<!--          <el-input v-model="brandForm.password" type="password" show-password placeholder="若留空则不修改密码" style="width: 400px" />-->
+<!--        </el-form-item>-->
+
+        <el-form-item label="联系人">
+          <el-input v-model="brandForm.contactPerson" style="width: 400px" />
+        </el-form-item>
+
+        <el-form-item label="联系电话">
+          <el-input v-model="brandForm.contactPhone" style="width: 400px" />
+        </el-form-item>
+
+        <el-form-item label="总部地址">
+          <el-input v-model="brandForm.address" style="width: 600px" />
+        </el-form-item>
+
+        <el-form-item label="简介">
+          <el-input v-model="brandForm.description" type="textarea" :rows="4" style="width: 600px" />
+        </el-form-item>
+
+        <!-- Keep this original requirement from before -->
+<!--        <el-form-item label="合同有效期至">-->
+<!--          <el-input :model-value="brandForm.validUntil" disabled style="width: 300px" />-->
+<!--        </el-form-item>-->
+
+        <el-form-item>
+          <el-button type="primary" @click="handleSave">保存修改</el-button>
+        </el-form-item>
+      </el-form>
+    </el-card>
+  </div>
+</template>
+
+<script setup>
+import { reactive, onMounted } from 'vue';
+import { ElMessage } from 'element-plus';
+import { getTenantInfo } from '@/api/system/tenant';
+
+const brandForm = reactive({
+  logo: '',
+  name: '',
+  account: '',
+  password: '',
+  contactPerson: '',
+  contactPhone: '',
+  address: '',
+  status: true,
+  description: '',
+  validUntil: ''
+});
+
+const getInfo = async () => {
+  try {
+    const res = await getTenantInfo();
+    const data = res.data;
+    if (data) {
+      brandForm.logo = data.logoUrl || '';
+      brandForm.name = data.companyName || '';
+      brandForm.account = data.username || '';
+      brandForm.contactPerson = data.contactUserName || '';
+      brandForm.contactPhone = data.contactPhone || '';
+      brandForm.address = data.address || '';
+      brandForm.description = data.intro || '';
+      brandForm.validUntil = data.expireTime || '';
+    }
+  } catch (error) {
+    console.error('获取品牌信息失败:', error);
+  }
+};
+
+onMounted(() => {
+  getInfo();
+});
+
+const handleLogoChange = (file) => {
+  if (file && file.raw) {
+    brandForm.logo = URL.createObjectURL(file.raw);
+  }
+};
+
+const handleSave = () => {
+  ElMessage.success('品牌信息已更新');
+};
+</script>
+
+<style scoped>
+.page-container {
+  padding: 20px;
+}
+.brand-form {
+  margin-top: 20px;
+}
+.avatar-uploader-icon {
+  font-size: 28px;
+  color: #8c939d;
+  width: 100px;
+  height: 100px;
+  text-align: center;
+  border: 1px dashed #d9d9d9;
+  border-radius: 8px;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  cursor: pointer;
+}
+.avatar-uploader-icon:hover {
+  border-color: #409eff;
+}
+</style>

+ 297 - 0
src/views/merchant/storeInfo/index.vue

@@ -0,0 +1,297 @@
+<template>
+  <div class="page-container">
+    <el-card shadow="never">
+      <template #header>
+        <div class="card-header">
+          <span>门店信息设置</span>
+          <el-select v-model="currentStoreId" placeholder="选择门店" @change="handleStoreChange" style="width: 250px">
+            <el-option v-for="store in stores" :key="store.id" :label="store.name" :value="store.id" />
+          </el-select>
+        </div>
+      </template>
+
+      <el-form :model="storeForm" label-width="120px" class="store-form">
+        <!-- Logo -->
+        <el-form-item label="门店Logo">
+          <el-upload class="avatar-uploader" action="#" :show-file-list="false" :auto-upload="false" :on-change="handleLogoChange">
+            <el-avatar v-if="storeForm.logo" :size="100" shape="square" :src="storeForm.logo" />
+            <el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
+          </el-upload>
+        </el-form-item>
+
+        <!-- License -->
+        <el-form-item label="营业执照" required>
+          <el-upload action="#" :show-file-list="false" :auto-upload="false" :on-change="handleLicenseChange">
+            <el-image style="width: 150px; height: 100px; border-radius: 4px; border: 1px solid #dcdfe6" :src="storeForm.license" fit="cover">
+              <template #error>
+                <div
+                  style="width: 100%; height: 100%; background: #f5f7fa; display: flex; justify-content: center; align-items: center; color: #dedfe0"
+                >
+                  <el-icon size="24"><Picture /></el-icon>
+                </div>
+              </template>
+            </el-image>
+          </el-upload>
+        </el-form-item>
+
+        <!-- Name -->
+        <el-form-item label="门店名称" required>
+          <el-input v-model="storeForm.name" style="width: 400px" disabled />
+        </el-form-item>
+
+        <!-- Services -->
+        <el-form-item label="服务项目" required>
+          <el-checkbox-group v-model="storeForm.services" disabled>
+            <el-checkbox v-for="service in serviceOptions" :key="service.id" :label="service.id">
+              {{ service.name }}
+            </el-checkbox>
+          </el-checkbox-group>
+        </el-form-item>
+
+        <!-- Category -->
+        <el-form-item label="商户分类" required>
+          <el-input v-model="storeForm.category" style="width: 400px" disabled />
+        </el-form-item>
+
+        <!--        &lt;!&ndash; Account &ndash;&gt;-->
+        <!--        <el-form-item label="管理账号" required>-->
+        <!--          <el-input v-model="storeForm.account" placeholder="用于后台登录" style="width: 400px" disabled />-->
+        <!--        </el-form-item>-->
+
+        <!--        &lt;!&ndash; Password &ndash;&gt;-->
+        <!--        <el-form-item label="登录密码">-->
+        <!--          <el-input v-model="storeForm.password" type="password" show-password placeholder="若留空则不修改密码" style="width: 400px" />-->
+        <!--        </el-form-item>-->
+
+        <!-- Business hours -->
+        <el-form-item label="营业时间" required>
+          <div style="width: 400px">
+            <el-time-picker
+              v-model="storeForm.businessHours"
+              is-range
+              range-separator="至"
+              start-placeholder="开始时间"
+              end-placeholder="结束时间"
+              format="HH:mm"
+              value-format="HH:mm"
+              style="width: 100%"
+            />
+          </div>
+        </el-form-item>
+
+        <el-form-item label="联系人" required>
+          <el-input v-model="storeForm.contactPerson" style="width: 400px" />
+        </el-form-item>
+
+        <el-form-item label="联系电话" required>
+          <el-input v-model="storeForm.contactPhone" style="width: 400px" />
+        </el-form-item>
+
+        <!-- Valid Until -->
+        <el-form-item label="有效期至">
+          <el-input :model-value="storeForm.validUntil" disabled style="width: 400px" />
+        </el-form-item>
+
+        <!-- Address -->
+        <el-form-item label="详细地址" required>
+          <div style="width: 600px">
+            <el-cascader
+              v-model="storeForm.selectedArea"
+              :options="regionData"
+              placeholder="请选择省/市/区"
+              style="width: 100%; margin-bottom: 10px"
+              @change="handleAreaChange"
+            />
+            <el-input v-model="storeForm.address" type="textarea" placeholder="输入详细地址" />
+            <div style="margin-top: 10px; text-align: right">
+              <el-button type="warning" size="small" @click="handleGetLocation" icon="Location">获取经纬度</el-button>
+            </div>
+          </div>
+        </el-form-item>
+
+        <!-- Lat Lng -->
+        <el-form-item label="经纬度">
+          <div style="display: flex; gap: 10px; width: 600px">
+            <el-input v-model="storeForm.longitude" placeholder="经度" readonly />
+            <el-input v-model="storeForm.latitude" placeholder="纬度" readonly />
+          </div>
+        </el-form-item>
+
+        <el-form-item>
+          <el-button type="primary" @click="handleSave" size="large">保存修改</el-button>
+        </el-form-item>
+      </el-form>
+    </el-card>
+  </div>
+</template>
+
+<script setup>
+import { reactive, ref, onMounted } from 'vue';
+import { ElMessage } from 'element-plus';
+import { Plus, Picture, Location } from '@element-plus/icons-vue';
+import { listOnMerchantStoreInfo, getStoreInfo } from '@/api/system/store';
+import { listOnStore } from '@/api/service/list';
+import { regionData } from 'element-china-area-data';
+
+const currentStoreId = ref('');
+
+const stores = ref([]);
+const serviceOptions = ref([]);
+
+const storeForm = reactive({
+  id: '',
+  logo: '',
+  license: '',
+  name: '',
+  services: [],
+  category: '',
+  account: '',
+  password: '',
+  contactPerson: '',
+  contactPhone: '',
+  businessHours: ['', ''],
+  validUntil: '',
+  address: '',
+  longitude: '',
+  latitude: '',
+  areaCode: '',
+  selectedArea: []
+});
+
+const getStoreList = async () => {
+  try {
+    const res = await listOnMerchantStoreInfo();
+    const data = res.data;
+    if (data && data.length > 0) {
+      stores.value = data;
+      currentStoreId.value = data[0].id;
+      handleStoreChange(currentStoreId.value);
+    }
+  } catch (error) {
+    console.error('获取门店列表失败:', error);
+  }
+};
+
+const getServiceOptions = async () => {
+  try {
+    const res = await listOnStore();
+    if (res.data) {
+      serviceOptions.value = res.data;
+    }
+  } catch (error) {
+    console.error('获取服务项目失败:', error);
+  }
+};
+
+const handleStoreChange = async (val) => {
+  if (!val) return;
+  try {
+    const res = await getStoreInfo(val);
+    const data = res.data;
+    if (data) {
+      storeForm.id = data.id;
+      storeForm.logo = data.logoUrl || '';
+      storeForm.license = data.businessLicenseUrl || '';
+      storeForm.name = data.name || '';
+      storeForm.services = data.services || [];
+      storeForm.category = data.tenantCatergoriesName || '';
+      storeForm.contactPerson = data.contact || '';
+      storeForm.contactPhone = data.contactNumber || '';
+
+      const start = data.startBusinessTime ? data.startBusinessTime.split(' ')[1]?.substring(0, 5) : '';
+      const end = data.endBusinessTime ? data.endBusinessTime.split(' ')[1]?.substring(0, 5) : '';
+      storeForm.businessHours = [start, end];
+
+      storeForm.validUntil = data.validity ? data.validity.split(' ')[0] : '';
+
+      storeForm.address = data.detailAddress || '';
+      storeForm.longitude = data.longitude ? String(data.longitude) : '';
+      storeForm.latitude = data.latitude ? String(data.latitude) : '';
+      storeForm.areaCode = data.areaCode || '';
+      if (data.areaCode) {
+        if (Array.isArray(data.areaCode)) {
+          storeForm.selectedArea = data.areaCode;
+        } else if (typeof data.areaCode === 'string') {
+          storeForm.selectedArea = data.areaCode.split(',');
+        }
+      } else {
+        storeForm.selectedArea = [];
+      }
+    }
+  } catch (error) {
+    console.error('获取详细信息失败:', error);
+  }
+};
+
+onMounted(() => {
+  getServiceOptions();
+  getStoreList();
+});
+
+const handleLogoChange = (file) => {
+  if (file && file.raw) {
+    storeForm.logo = URL.createObjectURL(file.raw);
+  }
+};
+
+const handleLicenseChange = (file) => {
+  if (file && file.raw) {
+    storeForm.license = URL.createObjectURL(file.raw);
+  }
+};
+
+const handleAreaChange = (val) => {
+  if (val && val.length > 0) {
+    storeForm.areaCode = val.join(',');
+  } else {
+    storeForm.areaCode = '';
+  }
+};
+
+const handleGetLocation = () => {
+  if (!storeForm.selectedArea || storeForm.selectedArea.length === 0) {
+    return ElMessage.warning('请先选择省市区');
+  }
+  if (!storeForm.address) {
+    return ElMessage.warning('请先填写详细地址');
+  }
+
+  ElMessage.success('根据地址获取经纬度成功');
+  storeForm.longitude = '116.452562';
+  storeForm.latitude = '39.936404';
+};
+
+const handleSave = () => {
+  ElMessage.success(`门店【${storeForm.name}】信息已更新`);
+};
+</script>
+
+<style scoped>
+.page-container {
+  padding: 20px;
+}
+.card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+.store-form {
+  margin-top: 20px;
+}
+.avatar-uploader-icon {
+  font-size: 28px;
+  color: #8c939d;
+  width: 100px;
+  height: 100px;
+  text-align: center;
+  border: 1px dashed #d9d9d9;
+  border-radius: 8px;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  cursor: pointer;
+}
+.avatar-uploader-icon:hover {
+  border-color: #409eff;
+}
+</style>

+ 839 - 0
src/views/merchant/storeManagement/index.vue

@@ -0,0 +1,839 @@
+<template>
+  <div class="p-2">
+    <transition :enter-active-class="proxy?.animate.searchAnimate.enter"
+                :leave-active-class="proxy?.animate.searchAnimate.leave">
+      <div v-show="showSearch" class="mb-[10px] bg-white p-[20px] rounded-[4px] flex justify-between items-center shadow-sm" style="background-color: #fff; padding: 20px; border-radius: 4px; box-shadow: 0 1px 4px rgba(0,21,41,.08);">
+        <div class="text-[18px] font-bold text-[#303133]" style="font-size: 18px; font-weight: bold; color: #303133;">门店管理</div>
+        <div class="flex items-center gap-[10px]" style="display: flex; gap: 10px; align-items: center;">
+          <el-input v-model="queryParams.storeOrContact" placeholder="搜索门店名称/联系人" prefix-icon="Search"
+                    style="width: 250px" clearable @keyup.enter="handleQuery" @clear="handleQuery" />
+          <el-cascader v-model="searchRegionValue" :options="areaOptions" placeholder="所属城市"
+                       style="width: 150px" clearable @change="handleSearchAreaChange" />
+          <el-select v-model="queryParams.station" placeholder="所属站点" style="width: 150px" clearable @change="handleQuery">
+            <el-option v-for="site in searchSiteOptions" :key="site.value" :label="site.label" :value="site.value" />
+          </el-select>
+          <el-select v-model="queryParams.status" placeholder="状态" style="width: 120px" clearable @change="handleQuery">
+            <el-option v-for="item in statusList" :key="item.value" :label="item.label" :value="item.value" />
+          </el-select>
+          <el-button type="primary" icon="Plus" @click="handleAdd" v-hasPermi="['system:store:add']">新增门店</el-button>
+        </div>
+      </div>
+    </transition>
+
+    <el-card shadow="never">
+
+      <el-table v-loading="loading" border :data="storeList" @selection-change="handleSelectionChange">
+        <el-table-column type="selection" width="55" align="center" />
+        <el-table-column label="门店信息" align="left" width="300">
+          <template #default="scope">
+            <div class="store-info" style="display: flex; align-items: center; gap: 10px;">
+              <div class="store-logo">
+                <image-preview :src="scope.row.logoUrl" :width="50" :height="50" />
+              </div>
+              <div class="store-details" style="display: flex; flex-direction: column; gap: 6px;">
+                <div class="store-name" style="font-size: 14px; font-weight: 500; color: #303133;">{{ scope.row.name }}</div>
+                <div class="store-categories" style="display: flex; gap: 6px;">
+                  <el-tag size="small" type="warning" effect="plain" v-if="scope.row.tenantName">{{ scope.row.tenantName }}</el-tag>
+                  <el-tag size="small" type="success" effect="plain" v-if="scope.row.tenantCatergoriesName">{{ scope.row.tenantCatergoriesName }}</el-tag>
+                </div>
+              </div>
+            </div>
+          </template>
+        </el-table-column>
+        <el-table-column label="资质认证" align="center" width="100">
+          <template #default="scope">
+            <image-preview v-if="scope.row.businessLicenseUrl" :src="scope.row.businessLicenseUrl" :width="40" :height="40" />
+            <span v-else>-</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="服务项目" align="center" width="200">
+          <template #default="scope">
+            <div class="services">
+              <el-tag v-for="service in scope.row.services" :key="service" size="small"
+                      style="margin-right: 5px; margin-bottom: 5px">
+                {{ getServiceName(service) }}
+              </el-tag>
+            </div>
+          </template>
+        </el-table-column>
+        <el-table-column label="归属站点" align="center" width="150">
+          <template #default="scope">
+            <div>{{ scope.row.siteName }}</div>
+          </template>
+        </el-table-column>
+        <el-table-column label="服务单" align="center" width="150">
+          <template #default="scope">
+            <div>{{ scope.row.serviceOrder }}</div>
+          </template>
+        </el-table-column>
+        <el-table-column label="营业时间" align="center" width="150">
+          <template #default="scope">
+            <div>{{ formatTime(scope.row.startBusinessTime) }}-{{ formatTime(scope.row.endBusinessTime) }}</div>
+          </template>
+        </el-table-column>
+        <el-table-column label="门店地址" align="center" width="300">
+          <template #default="scope">
+            <div>{{ scope.row.detailAddress }}</div>
+          </template>
+        </el-table-column>
+        <el-table-column label="联系方式" align="left" width="180">
+          <template #default="scope">
+            <div style="display: flex; flex-direction: column; gap: 6px;">
+              <div style="display: flex; align-items: center; gap: 6px; color: #606266; font-size: 14px;">
+                <el-icon size="16"><User /></el-icon>
+                <span>{{ scope.row.contact }}</span>
+              </div>
+              <div style="display: flex; align-items: center; gap: 6px; color: #409eff; font-size: 14px;">
+                <el-icon size="16"><Phone /></el-icon>
+                <span>{{ scope.row.contactNumber }}</span>
+              </div>
+            </div>
+          </template>
+        </el-table-column>
+        <el-table-column label="有效期至" align="center" width="120">
+          <template #default="scope">
+            <div>{{ parseTime(scope.row.validity, '{y}-{m}-{d}') }}</div>
+          </template>
+        </el-table-column>
+        <el-table-column label="状态" align="center" width="100">
+          <template #default="scope">
+            <template v-for="item in statusList" :key="item.value">
+              <el-tag v-if="scope.row.status === item.value" :type="item.style">{{ item.label }}</el-tag>
+            </template>
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" align="center" fixed="right" class-name="small-padding fixed-width">
+          <template #default="scope">
+            <el-tooltip content="详情" placement="top">
+              <el-button link type="primary" icon="View" @click="handleDetail(scope.row)"></el-button>
+            </el-tooltip>
+            <el-tooltip content="修改" placement="top">
+              <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)"
+                         v-hasPermi="['system:store:edit']"></el-button>
+            </el-tooltip>
+            <el-tooltip content="更多" placement="top">
+              <el-button link type="primary" icon="More"></el-button>
+            </el-tooltip>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum"
+                  v-model:limit="queryParams.pageSize" @pagination="getList" />
+    </el-card>
+    <!-- 添加或修改门店管理对话框 -->
+    <el-dialog :title="dialog.title" v-model="dialog.visible" width="600px" append-to-body>
+      <el-form ref="storeFormRef" :model="form" :rules="rules" label-width="120px">
+        <el-form-item label="门店Logo" prop="logo">
+          <image-upload v-model="form.logo" :limit="1" />
+        </el-form-item>
+        <el-form-item label="营业执照" prop="businessLicense">
+          <image-upload v-model="form.businessLicense" :limit="1" />
+        </el-form-item>
+        <el-form-item label="门店名称" prop="name">
+          <el-input v-model="form.name" placeholder="请输入门店名称" />
+        </el-form-item>
+        <el-form-item label="服务项目" prop="services">
+          <el-checkbox-group v-model="form.services">
+            <el-checkbox v-for="service in serviceList" :key="service.id" :label="service.id" border>
+              {{ service.name }}
+            </el-checkbox>
+          </el-checkbox-group>
+        </el-form-item>
+        <el-form-item label="商户分类" prop="tenantCatergories">
+          <PageSelect v-model="form.tenantCatergories"
+                      :options="tenantCategoriesList.map(item => ({ value: item.id, label: item.name }))"
+                      :total="tenantCategoriesTotal" :pageSize="10" placeholder="请选择商户分类"
+                      @page-change="handleTenantCategoriesPageChange" @visible-change="handleTenantCategoriesVisibleChange" />
+        </el-form-item>
+        <el-form-item label="所属品牌" prop="tenantId">
+          <PageSelect v-model="form.tenantId"
+                      :options="brandList.map(item => ({ value: item.tenantId, label: item.name }))" :total="brandTotal"
+                      :pageSize="10" placeholder="请选择所属品牌" @page-change="handleBrandPageChange"
+                      @visible-change="handleBrandSelectVisibleChange" />
+        </el-form-item>
+        <el-form-item label="营业时间" prop="startBusinessTime">
+          <el-row :gutter="10">
+            <el-col :span="10">
+              <el-time-picker clearable v-model="form.startBusinessTime" value-format="HH:mm" placeholder="开始时间"
+                              style="width: 100%">
+              </el-time-picker>
+            </el-col>
+            <el-col :span="4" style="text-align: center; line-height: 40px">
+              至
+            </el-col>
+            <el-col :span="10">
+              <el-time-picker clearable v-model="form.endBusinessTime" value-format="HH:mm" placeholder="结束时间"
+                              style="width: 100%">
+              </el-time-picker>
+            </el-col>
+          </el-row>
+        </el-form-item>
+        <el-form-item label="联系人" prop="contact">
+          <el-input v-model="form.contact" placeholder="请输入联系人" />
+        </el-form-item>
+        <el-form-item label="联系电话" prop="contactNumber">
+          <el-input v-model="form.contactNumber" placeholder="请输入联系电话" />
+        </el-form-item>
+        <el-form-item label="有效期至" prop="validity">
+          <el-date-picker clearable v-model="form.validity" type="date" value-format="YYYY-MM-DD" placeholder="请选择有效期至"
+                          style="width: 100%">
+          </el-date-picker>
+        </el-form-item>
+        <el-row :gutter="10">
+          <el-col :span="12">
+            <el-form-item label="所在区域" prop="regionId">
+              <el-cascader v-model="regionValue" :options="areaOptions" placeholder="选择区域" style="width: 100%"
+                           @change="handleAreaChange" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="归属站点" prop="site">
+              <el-select v-model="form.site" placeholder="选择站点" :disabled="!form.regionId">
+                <el-option v-for="site in siteOptions" :key="site.value" :label="site.label" :value="site.value" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-form-item label="详细地址">
+          <el-row :gutter="10" style="margin-bottom: 10px">
+            <el-col :span="24">
+              <el-cascader v-model="addressCascaderValue" :options="regionData" placeholder="选择省市区"
+                           style="width: 100%" />
+            </el-col>
+          </el-row>
+          <el-input v-model="form.detailAddress" type="textarea" placeholder="输入详细地址" rows="3" style="width: 100%" />
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" style="width: 100%" @click="getGeolocation">获取经纬度</el-button>
+        </el-form-item>
+        <el-row :gutter="10">
+          <el-col :span="12">
+            <el-form-item label="经度" prop="longitude">
+              <el-input v-model="form.longitude" placeholder="请获取/输入位置经度" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="纬度" prop="latitude">
+              <el-input v-model="form.latitude" placeholder="请获取/输入位置纬度" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
+          <el-button @click="cancel">取 消</el-button>
+        </div>
+      </template>
+    </el-dialog>
+
+    <!-- 门店详情对话框 -->
+    <el-dialog :title="detailDialog.title" v-model="detailDialog.visible" width="800px" append-to-body>
+      <el-tabs v-model="activeTab" style="padding: 0 10px;">
+        <el-tab-pane label="基础信息" name="basic">
+          <el-descriptions :column="2" border>
+            <el-descriptions-item label="门店名称">{{ detailData.name }}</el-descriptions-item>
+            <el-descriptions-item label="商户分类">{{ detailData.tenantCatergoriesName || '-' }}</el-descriptions-item>
+            <el-descriptions-item label="所属品牌">{{ detailData.tenantName || '-' }}</el-descriptions-item>
+            <el-descriptions-item label="营业时间">{{ formatTime(detailData.startBusinessTime) }} - {{ formatTime(detailData.endBusinessTime) }}</el-descriptions-item>
+            <el-descriptions-item label="有效期至">{{ parseTime(detailData.validity, '{y}-{m}-{d}') }}</el-descriptions-item>
+            <el-descriptions-item label="联系人">{{ detailData.contact }}</el-descriptions-item>
+            <el-descriptions-item label="联系电话">{{ detailData.contactNumber }}</el-descriptions-item>
+            <el-descriptions-item label="所在区域">{{ detailData.regionName || '北京市朝阳区' }}</el-descriptions-item>
+            <el-descriptions-item label="详细地址">{{ detailData.detailAddress }}</el-descriptions-item>
+            <el-descriptions-item label="营业执照">
+              <image-preview v-if="detailData.businessLicenseUrl" :src="detailData.businessLicenseUrl" :width="80" :height="60" />
+              <span v-else>-</span>
+            </el-descriptions-item>
+          </el-descriptions>
+        </el-tab-pane>
+        <el-tab-pane label="服务订单记录" name="orders">
+          <el-table :data="orderList" border style="width: 100%">
+            <el-table-column label="订单号" prop="orderNo" min-width="150" />
+            <el-table-column label="服务项目" prop="service" min-width="120" />
+            <el-table-column label="客户" prop="customer" min-width="100" />
+            <el-table-column label="金额" prop="amount" min-width="100" />
+            <el-table-column label="下单时间" prop="time" min-width="160" />
+            <el-table-column label="状态" align="center" width="100">
+              <template #default="scope">
+                <el-tag :type="scope.row.statusType" effect="plain" size="small">{{ scope.row.status }}</el-tag>
+              </template>
+            </el-table-column>
+          </el-table>
+        </el-tab-pane>
+      </el-tabs>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="detailDialog.visible = false">关 闭</el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup name="Store" lang="ts">
+import { listStore, getStore, delStore, addStore, updateStore, listStoreStatus } from '@/api/system/store';
+import { StoreVO, StoreQuery, StoreForm, StoreStatusVO, SysStorePageBo } from '@/api/system/store/types';
+import { listOnStore } from '@/api/system/tenant';
+import { listOnStore as listTenantCategoriesOnStore } from '@/api/system/tenantCategories';
+import { listOnStore as listServiceOnStore } from '@/api/service/list';
+import { listOnStore as listAreaStationOnStore } from '@/api/system/areaStation';
+import { SysAreaStationOnStoreVo } from '@/api/system/areaStation/types';
+import { regionData, codeToText, textToCode } from 'element-china-area-data';
+import PageSelect from '@/components/PageSelect/index.vue';
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+
+const storeList = ref<StoreVO[]>([]);
+const buttonLoading = ref(false);
+const loading = ref(true);
+const showSearch = ref(true);
+const ids = ref<Array<string | number>>([]);
+const single = ref(true);
+const multiple = ref(true);
+const total = ref(0);
+
+const queryFormRef = ref<ElFormInstance>();
+const storeFormRef = ref<ElFormInstance>();
+const brandSelectRef = ref<any>(null);
+
+const searchRegionValue = ref<any[]>([]); // 搜索的区域值
+const searchSiteOptions = ref<any[]>([]); // 搜索的站点选项
+
+/** 处理搜索区域选择变化 */
+const handleSearchAreaChange = (value: any[]) => {
+  // 清空级联站点
+  queryParams.value.station = undefined;
+
+  if (value && value.length > 0) {
+    const areaId = value[value.length - 1];
+    queryParams.value.area = areaId;
+    searchSiteOptions.value = areaStationList.value
+      .filter((item: any) => item.type === 2 && String(item.parentId) === String(areaId))
+      .map((item: any) => ({
+        value: item.id,
+        label: item.name
+      }));
+  } else {
+    queryParams.value.area = undefined;
+    searchSiteOptions.value = [];
+  }
+  handleQuery();
+};
+
+// 新增的响应式变量
+const regionValue = ref<any[]>([]);
+const province = ref('');
+const city = ref('');
+const district = ref('');
+const addressCascaderValue = ref<any[]>([]); // 省市区级联选择器值
+const brandList = ref<any[]>([]); // 品牌列表
+const brandLoading = ref(false); // 品牌加载状态
+const currentPage = ref(1); // 当前页码
+const brandKeyword = ref(''); // 搜索关键词
+const brandSelectVisible = ref(false); // 品牌选择框可见状态
+const brandTotal = ref(0); // 品牌总数
+const serviceList = ref<any[]>([]); // 服务项目列表
+const statusList = ref<StoreStatusVO[]>([]); // 状态列表
+const tenantCategoriesList = ref<any[]>([]); // 商户分类列表
+const tenantCategoriesTotal = ref(0); // 商户分类总数
+const areaStationList = ref<SysAreaStationOnStoreVo[]>([]); // 区域站点列表
+const areaOptions = ref<any[]>([]); // 所在区域树形选项
+const siteOptions = ref<any[]>([]); // 归属站点选项
+
+
+
+const dialog = reactive<DialogOption>({
+  visible: false,
+  title: ''
+});
+
+const detailDialog = reactive({
+  visible: false,
+  title: '门店详情'
+});
+const activeTab = ref('basic');
+const detailData = ref<any>({});
+const orderList = ref([
+  { orderNo: 'ORD202402040001', service: '洗澡美容', customer: '张三', amount: '¥128', time: '2024-02-04 10:00', status: '已完成', statusType: 'success' },
+  { orderNo: 'ORD202402040002', service: '寄养服务', customer: '李四', amount: '¥500', time: '2024-02-03 14:30', status: '已完成', statusType: 'success' },
+  { orderNo: 'ORD202402030005', service: '疫苗注射', customer: '王五', amount: '¥80', time: '2024-02-01 09:00', status: '已取消', statusType: 'info' }
+]);
+
+/** 详情按钮操作 */
+const handleDetail = async (row: StoreVO) => {
+  const res = await getStore(row.id);
+  // 合并列表里的关联数据,以便能够展示名称等额外字段
+  detailData.value = { ...row, ...res.data };
+  activeTab.value = 'basic';
+  detailDialog.visible = true;
+};
+
+const initFormData: StoreForm = {
+  id: undefined,
+  logo: undefined,
+  businessLicense: undefined,
+  name: undefined,
+  tenantCatergories: undefined,
+  startBusinessTime: undefined,
+  endBusinessTime: undefined,
+  contact: undefined,
+  contactNumber: undefined,
+  validity: undefined,
+  site: undefined,
+  detailAddress: undefined,
+  status: undefined,
+  longitude: undefined,
+  latitude: undefined,
+  tenantId: undefined,
+  services: [],
+  regionId: undefined,
+  areaCode: undefined,
+}
+const data = reactive<PageData<StoreForm, SysStorePageBo>>({
+  form: { ...initFormData },
+  queryParams: {
+    pageNum: 1,
+    pageSize: 10,
+    storeOrContact: undefined,
+    area: undefined,
+    station: undefined,
+    status: undefined,
+    params: {
+    }
+  },
+  rules: {
+    id: [
+      { required: true, message: "序号不能为空", trigger: "blur" }
+    ],
+    businessLicense: [
+      { required: true, message: "营业执照不能为空", trigger: "blur" }
+    ],
+    name: [
+      { required: true, message: "门店名称不能为空", trigger: "blur" }
+    ],
+    tenantCatergories: [
+      { required: true, message: "商户分类不能为空", trigger: "change" }
+    ],
+    startBusinessTime: [
+      { required: true, message: "开始营业时间不能为空", trigger: "blur" }
+    ],
+    endBusinessTime: [
+      { required: true, message: "结束营业时间不能为空", trigger: "blur" }
+    ],
+    contact: [
+      { required: true, message: "联系人不能为空", trigger: "blur" }
+    ],
+    contactNumber: [
+      { required: true, message: "联系电话不能为空", trigger: "blur" }
+    ],
+    validity: [
+      { required: true, message: "有效期至不能为空", trigger: "blur" }
+    ],
+    tenantId: [
+      { required: true, message: "租户编号不能为空", trigger: "change" }
+    ],
+    regionId: [
+      { required: true, message: "所在区域不能为空", trigger: "change" }
+    ],
+    site: [
+      { required: true, message: "归属站点不能为空", trigger: "change" }
+    ],
+  }
+});
+
+const { queryParams, form, rules } = toRefs(data);
+
+/** 查询门店管理列表 */
+const getList = async () => {
+  loading.value = true;
+  const res = await listStore(queryParams.value);
+  storeList.value = res.rows;
+  total.value = res.total;
+  loading.value = false;
+}
+
+/** 取消按钮 */
+const cancel = () => {
+  reset();
+  dialog.visible = false;
+}
+
+/** 表单重置 */
+const reset = () => {
+  form.value = { ...initFormData };
+  // 重置新增的变量
+  regionValue.value = [];
+  province.value = '';
+  city.value = '';
+  district.value = '';
+  addressCascaderValue.value = [];
+  storeFormRef.value?.resetFields();
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.value.pageNum = 1;
+  getList();
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  searchRegionValue.value = [];
+  searchSiteOptions.value = [];
+  queryParams.value.storeOrContact = undefined;
+  queryParams.value.area = undefined;
+  queryParams.value.station = undefined;
+  queryParams.value.status = undefined;
+  handleQuery();
+}
+
+/** 多选框选中数据 */
+const handleSelectionChange = (selection: StoreVO[]) => {
+  ids.value = selection.map(item => item.id);
+  single.value = selection.length != 1;
+  multiple.value = !selection.length;
+}
+
+/** 新增按钮操作 */
+const handleAdd = () => {
+  reset();
+  dialog.visible = true;
+  dialog.title = "添加门店管理";
+}
+
+/** 修改按钮操作 */
+const handleUpdate = async (row?: StoreVO) => {
+  reset();
+  const _id = row?.id || ids.value[0]
+  const res = await getStore(_id);
+  Object.assign(form.value, res.data);
+
+  if (res.data.areaCode) {
+    if (Array.isArray(res.data.areaCode)) {
+      addressCascaderValue.value = res.data.areaCode;
+    } else if (typeof res.data.areaCode === 'string') {
+      addressCascaderValue.value = res.data.areaCode.split(',');
+    }
+  }
+
+  dialog.visible = true;
+  dialog.title = "修改门店管理";
+}
+
+/** 提交按钮 */
+const submitForm = () => {
+  storeFormRef.value?.validate(async (valid: boolean) => {
+    if (valid) {
+      buttonLoading.value = true;
+      if (form.value.id) {
+        await updateStore(form.value).finally(() => buttonLoading.value = false);
+      } else {
+        await addStore(form.value).finally(() => buttonLoading.value = false);
+      }
+      proxy?.$modal.msgSuccess("操作成功");
+      dialog.visible = false;
+      await getList();
+    }
+  });
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (row?: StoreVO) => {
+  const _ids = row?.id || ids.value;
+  await proxy?.$modal.confirm('是否确认删除门店管理编号为"' + _ids + '"的数据项?').finally(() => loading.value = false);
+  await delStore(_ids);
+  proxy?.$modal.msgSuccess("删除成功");
+  await getList();
+}
+
+/** 导出按钮操作 */
+const handleExport = () => {
+  proxy?.download('system/store/export', {
+    ...queryParams.value
+  }, `store_${new Date().getTime()}.xlsx`)
+}
+
+/** 获取经纬度 */
+const getGeolocation = () => {
+  if ('geolocation' in navigator) {
+    navigator.geolocation.getCurrentPosition(
+      (position) => {
+        form.value.longitude = position.coords.longitude.toFixed(6);
+        form.value.latitude = position.coords.latitude.toFixed(6);
+        proxy?.$modal.msgSuccess('获取经纬度成功');
+      },
+      (error) => {
+        let errorMessage = '获取位置失败';
+        switch (error.code) {
+          case error.PERMISSION_DENIED:
+            errorMessage = '用户拒绝了地理定位请求';
+            break;
+          case error.POSITION_UNAVAILABLE:
+            errorMessage = '位置信息不可用';
+            break;
+          case error.TIMEOUT:
+            errorMessage = '获取位置超时';
+            break;
+          case error.UNKNOWN_ERROR:
+            errorMessage = '未知错误';
+            break;
+        }
+        proxy?.$modal.msgError(errorMessage);
+      }
+    );
+  } else {
+    proxy?.$modal.msgError('您的浏览器不支持地理定位');
+  }
+};
+
+/** 获取品牌列表 */
+const getBrandList = async (pageNum = 1, keyword = '', append = false) => {
+  brandLoading.value = true;
+  // 确保参数格式正确,直接传递数字类型的pageNum
+  const res = await listOnStore({ pageNum: pageNum, pageSize: 10 });
+  if (res.code === 200) {
+    if (append) {
+      // 追加模式,用于分页加载
+      brandList.value = [...brandList.value, ...res.rows];
+    } else {
+      // 替换模式,用于初始加载或搜索
+      brandList.value = res.rows;
+    }
+    // 存储总数
+    brandTotal.value = res.total || 0;
+    console.log('总数', brandTotal.value);
+  }
+  brandLoading.value = false;
+};
+
+/** 获取服务项目列表 */
+const getServiceList = async () => {
+  try {
+    const res = await listServiceOnStore();
+    // 转换数据格式,适配checkbox组件
+    serviceList.value = res.data || res;
+  } catch (error) {
+    console.error('获取服务项目列表失败:', error);
+  }
+};
+
+/** 获取区域站点列表 */
+const getAreaStationList = async () => {
+  try {
+    const res = await listAreaStationOnStore();
+    const data = res.data || res;
+    areaStationList.value = data;
+
+    // 分离所在区域数据(type为0或1)
+    const areaData = data.filter((item: any) => item.type === 0 || item.type === 1);
+    // 构建树形结构
+    areaOptions.value = buildTree(areaData, 0);
+
+    // 初始化站点数据为空
+    siteOptions.value = [];
+  } catch (error) {
+    console.error('获取区域站点列表失败:', error);
+  }
+};
+
+/** 构建树形结构 */
+const buildTree = (data: any[], parentId: any): any[] => {
+  return data
+    .filter(item => String(item.parentId) === String(parentId))
+    .map(item => ({
+      value: item.id,
+      label: item.name,
+      children: buildTree(data, item.id)
+    }));
+};
+
+/** 处理所在区域选择变化 */
+const handleAreaChange = (value: any[]) => {
+  // 清空归属站点选择
+  form.value.site = undefined;
+
+  if (value && value.length > 0) {
+    // 获取最后一级的id
+    const areaId = value[value.length - 1];
+    // 更新regionId
+    form.value.regionId = areaId;
+    // 过滤出parentId等于areaId的站点
+    siteOptions.value = areaStationList.value
+      .filter((item: any) => item.type === 2 && String(item.parentId) === String(areaId))
+      .map((item: any) => ({
+        value: item.id,
+        label: item.name
+      }));
+  } else {
+    // 如果没有选择区域,清空站点选项和regionId
+    form.value.regionId = undefined;
+    siteOptions.value = [];
+  }
+};
+
+/** 获取商户分类列表 */
+const getTenantCategoriesList = async (pageNum = 1) => {
+  try {
+    const res = await listTenantCategoriesOnStore({ pageNum, pageSize: 10 });
+    if (res.code === 200) {
+      tenantCategoriesList.value = res.rows;
+      tenantCategoriesTotal.value = res.total || 0;
+    }
+  } catch (error) {
+    console.error('获取商户分类列表失败:', error);
+  }
+};
+
+/** 处理品牌页面切换 */
+const handleBrandPageChange = (page: number) => {
+  // 确保page是数字类型
+  const pageNum = Number(page);
+  currentPage.value = pageNum;
+  getBrandList(pageNum, brandKeyword.value, false);
+};
+
+/** 处理商户分类分页 */
+const handleTenantCategoriesPageChange = (page: number) => {
+  // 确保page是数字类型
+  const pageNum = Number(page);
+  getTenantCategoriesList(pageNum);
+};
+
+/** 处理商户分类选择框可见性变化 */
+const handleTenantCategoriesVisibleChange = (visible: boolean) => {
+  if (visible) {
+    getTenantCategoriesList(1);
+  }
+};
+
+/** 远程搜索方法 */
+const remoteMethod = (query: string) => {
+  brandKeyword.value = query;
+  currentPage.value = 1;
+  getBrandList(1, query, false);
+};
+
+/** 处理品牌选择框显示状态变化 */
+const handleBrandSelectVisibleChange = (visible: boolean) => {
+  brandSelectVisible.value = visible;
+  if (visible) {
+    // 选择框显示时,重置页码并重新加载数据
+    currentPage.value = 1;
+    getBrandList(1, brandKeyword.value, false);
+  }
+};
+
+// 监听省市区选择变化,不追加到详细地址,直接存储到区域编码中
+watch(
+  addressCascaderValue,
+  (newValue) => {
+    if (newValue && newValue.length > 0) {
+      form.value.areaCode = newValue.join(',');
+    } else {
+      form.value.areaCode = undefined;
+    }
+  },
+  { deep: true }
+);
+
+/** 获取服务项目名称 */
+const getServiceName = (serviceId: number): string => {
+  const service = serviceList.value.find(item => item.id === serviceId);
+  return service ? service.name : String(serviceId);
+};
+
+/** 获取状态列表 */
+const getStatusList = async () => {
+  try {
+    const res: any = await listStoreStatus();
+    // 兼容可能的不同响应体结构
+    statusList.value = res.data || res.rows || res;
+  } catch (error) {
+    console.error('获取状态列表失败:', error);
+  }
+};
+
+/** 格式化时间为时分 */
+const formatTime = (time: string | number): string => {
+  if (!time) return '';
+
+  // 处理时间戳或日期字符串
+  const date = new Date(time);
+
+  // 检查是否是有效日期
+  if (isNaN(date.getTime())) return '';
+
+  // 格式化为 HH:mm
+  const hours = date.getHours().toString().padStart(2, '0');
+  const minutes = date.getMinutes().toString().padStart(2, '0');
+
+  return `${hours}:${minutes}`;
+};
+
+onMounted(() => {
+  getList();
+  getBrandList();
+  getServiceList();
+  getAreaStationList();
+  getStatusList();
+});
+</script>
+
+<style scoped>
+.brand-pagination {
+  margin-top: 10px;
+  padding-top: 10px;
+  border-top: 1px solid #ebeef5;
+  text-align: center;
+}
+
+.custom-pagination {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 10px;
+  font-size: 14px;
+}
+
+.page-arrow {
+  cursor: pointer;
+  color: #606266;
+  user-select: none;
+  padding: 2px 8px;
+  transition: color 0.3s;
+}
+
+.page-arrow:hover:not(.disabled) {
+  color: #409eff;
+}
+
+.page-arrow.disabled {
+  color: #c0c4cc;
+  cursor: not-allowed;
+}
+
+.page-number {
+  cursor: pointer;
+  color: #606266;
+  padding: 2px 8px;
+  transition: all 0.3s;
+}
+
+.page-number:hover {
+  color: #409eff;
+}
+
+.page-number.active {
+  color: #409eff;
+  font-weight: bold;
+}
+
+.total-text {
+  margin-left: 15px;
+  font-size: 12px;
+  color: #909399;
+}
+</style>
+

+ 354 - 0
src/views/system/account/index.vue

@@ -0,0 +1,354 @@
+<template>
+  <div class="page-container">
+    <el-card shadow="hover">
+      <template #header>
+        <div class="card-header">
+          <span style="font-weight: bold;">账号管理</span>
+          <div class="header-actions" style="display: flex; gap: 10px;">
+            <el-input v-model="queryParams.userName" placeholder="搜索用户名" style="width: 150px" clearable @keyup.enter="getList" />
+            <el-input v-model="queryParams.phonenumber" placeholder="搜索手机号" style="width: 150px" clearable @keyup.enter="getList" />
+            <el-button type="primary" icon="Search" @click="getList">搜索</el-button>
+            <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+            <el-button type="warning" icon="Plus" @click="handleAdd" v-hasPermi="['system:user:add']" style="background-color: #e6a23c; border-color: #e6a23c; color: #fff;">新增账号</el-button>
+          </div>
+        </div>
+      </template>
+
+      <el-table :data="tableData" style="width: 100%" v-loading="loading" :header-cell-style="{ color: '#909399', fontWeight: 'normal', backgroundColor: '#fff', borderBottom: '1px solid #ebeef5' }">
+        <el-table-column prop="avatar" label="头像" width="80" align="center">
+          <template #default="scope">
+            <el-avatar :size="40" :src="scope.row.avatar || 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png'" />
+          </template>
+        </el-table-column>
+        <el-table-column prop="userName" label="用户名" width="120" />
+        <el-table-column prop="nickName" label="姓名" width="120" />
+        <el-table-column prop="phonenumber" label="手机号" width="150" />
+        <el-table-column prop="roles" label="角色" min-width="150">
+          <template #default="scope">
+            <template v-if="scope.row.roles && scope.row.roles.length > 0">
+              <el-tag v-for="role in scope.row.roles" :key="role.roleId" style="margin-right: 5px; margin-bottom: 5px;" size="small">
+                {{ roleOptions.find(item => item.id === role.roleId)?.name || role.roleName }}
+              </el-tag>
+            </template>
+            <template v-else-if="scope.row.roleIds && scope.row.roleIds.length > 0">
+              <el-tag v-for="roleId in scope.row.roleIds" :key="roleId" style="margin-right: 5px; margin-bottom: 5px;" size="small">
+                {{ roleOptions.find(item => item.id == roleId)?.name || '未知角色' }}
+              </el-tag>
+            </template>
+            <span v-else style="color: #909399;">-</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="storeIds" label="管理门店" min-width="200">
+          <template #default="scope">
+            <span v-if="scope.row.storeIds && scope.row.storeIds.length > 0">
+              {{ scope.row.storeIds.map(id => storeOptions.find(item => item.id === id)?.name || '未知门店(' + id + ')').join(',') }}
+            </span>
+            <span v-else style="color: #909399;">全部门店</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="status" label="状态" width="100">
+          <template #default="scope">
+            <el-switch v-model="scope.row.status" active-value="0" inactive-value="1" @change="handleStatusChange(scope.row)" style="--el-switch-on-color: #f3d19e; --el-switch-off-color: #dcdfe6" />
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" width="200" fixed="right">
+          <template #default="scope">
+            <div style="display: flex; gap: 15px;" v-if="scope.row.userId !== 1">
+              <el-button link style="color: #e6a23c; padding: 0;" @click="handleEdit(scope.row)" v-hasPermi="['system:user:edit']">编辑</el-button>
+              <el-button link style="color: #67c23a; padding: 0;" @click="handleResetPwd(scope.row)" v-hasPermi="['system:user:resetPwd']">重置密码</el-button>
+              <el-button link style="color: #f56c6c; padding: 0;" @click="handleDelete(scope.row)" v-hasPermi="['system:user:remove']">删除</el-button>
+            </div>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <div class="pagination-container">
+        <el-pagination
+          v-if="total > 0"
+          v-model:current-page="queryParams.pageNum"
+          v-model:page-size="queryParams.pageSize"
+          :page-sizes="[10, 20, 50, 100]"
+          layout="total, sizes, prev, pager, next, jumper"
+          :total="total"
+          @size-change="handleSizeChange"
+          @current-change="handleCurrentChange"
+        />
+      </div>
+    </el-card>
+
+    <!-- 账号表单 -->
+    <el-dialog v-model="dialogVisible" :title="isEdit ? '编辑账号' : '新增账号'" width="550px" append-to-body>
+      <el-form ref="userFormRef" :model="form" :rules="rules" label-width="90px">
+        <el-row>
+          <el-col :span="24">
+            <el-form-item label="用户名" prop="userName">
+              <el-input v-model="form.userName" placeholder="用于登录" :disabled="isEdit" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="24">
+            <el-form-item label="姓名" prop="nickName">
+              <el-input v-model="form.nickName" placeholder="人员姓名" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="24">
+            <el-form-item label="手机号" prop="phonenumber">
+              <el-input v-model="form.phonenumber" placeholder="联系电话" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="24" v-if="!isEdit">
+            <el-form-item label="登录密码" prop="password">
+              <el-input v-model="form.password" type="password" show-password placeholder="密码" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="24" v-if="form.userId == null || form.userId != useUserStore().userId">
+            <el-form-item label="分配角色" prop="roleIds">
+              <el-select v-model="form.roleIds" multiple placeholder="请选择角色" style="width: 100%">
+                <el-option
+                  v-for="item in roleOptions"
+                  :key="item.id"
+                  :label="item.name"
+                  :value="item.id"
+                />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="24" v-if="form.userId == null || form.userId != useUserStore().userId">
+            <el-form-item label="管理门店">
+              <el-select
+                v-model="form.storeIds"
+                multiple
+                collapse-tags
+                placeholder="请选择管理门店"
+                style="width: 100%"
+              >
+                <el-option
+                  v-for="store in storeOptions"
+                  :key="store.id"
+                  :label="store.name"
+                  :value="store.id"
+                />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="24">
+            <el-form-item label="状态">
+              <el-radio-group v-model="form.status">
+                <el-radio value="0">启用</el-radio>
+                <el-radio value="1">禁用</el-radio>
+              </el-radio-group>
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </el-form>
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button @click="dialogVisible = false">取消</el-button>
+          <el-button type="primary" @click="saveAccount" :loading="buttonLoading">保存</el-button>
+        </span>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts" name="Account">
+import { ref, reactive, onMounted, getCurrentInstance, ComponentInternalInstance } from 'vue';
+import { ElMessage, ElMessageBox, FormInstance, FormRules } from 'element-plus';
+import userApi from '@/api/system/user';
+import { listOnUser } from '@/api/system/store';
+import { listAllRole } from '@/api/system/role';
+import { useUserStore } from '@/store/modules/user';
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+
+// State
+const queryParams = reactive({
+  pageNum: 1,
+  pageSize: 10,
+  userName: '',
+  phonenumber: ''
+});
+const total = ref(0);
+const tableData = ref([]);
+const loading = ref(false);
+const buttonLoading = ref(false);
+
+const dialogVisible = ref(false);
+const isEdit = ref(false);
+
+const userFormRef = ref<FormInstance>();
+const form = ref<any>({
+  userId: undefined,
+  userName: '',
+  nickName: '',
+  phonenumber: '',
+  password: '',
+  roleIds: [],
+  storeIds: [],
+  postIds: [],
+  status: '0'
+});
+
+const rules = reactive<FormRules>({
+  userName: [{ required: true, message: '用户名不能为空', trigger: 'blur' }],
+  nickName: [{ required: true, message: '姓名不能为空', trigger: 'blur' }],
+  password: [{ required: true, message: '密码不能为空', trigger: 'blur' }],
+  roleIds: [{ type: 'array', required: true, message: '请至少选择一个角色', trigger: 'change' }]
+});
+
+const storeOptions = ref<any[]>([]);
+const roleOptions = ref<any[]>([]);
+const initPassword = ref('123456');
+
+// Fetch data
+const getList = async () => {
+  loading.value = true;
+  try {
+    const res = await userApi.listUser(queryParams);
+    tableData.value = res.rows as any;
+    total.value = res.total;
+  } finally {
+    loading.value = false;
+  }
+};
+
+const resetQuery = () => {
+  queryParams.userName = '';
+  queryParams.phonenumber = '';
+  queryParams.pageNum = 1;
+  getList();
+};
+
+const getStoreData = async () => {
+  const res = await listOnUser();
+  storeOptions.value = res.data;
+};
+
+const getRoleData = async () => {
+  const res = await listAllRole();
+  roleOptions.value = res.data;
+};
+
+const handleSizeChange = (val: number) => {
+  queryParams.pageSize = val;
+  getList();
+};
+const handleCurrentChange = (val: number) => {
+  queryParams.pageNum = val;
+  getList();
+};
+
+const resetForm = () => {
+  form.value = {
+    userId: undefined,
+    userName: '',
+    nickName: '',
+    phonenumber: '',
+    password: '',
+    roleIds: [],
+    storeIds: [],
+    postIds: [],
+    status: '0'
+  };
+  userFormRef.value?.resetFields();
+  userFormRef.value?.clearValidate();
+};
+
+const handleAdd = async () => {
+  resetForm();
+  isEdit.value = false;
+  await userApi.getUser();
+  form.value.password = initPassword.value;
+  dialogVisible.value = true;
+};
+
+const handleEdit = async (row: any) => {
+  resetForm();
+  isEdit.value = true;
+  const { data } = await userApi.getUser(row.userId);
+  Object.assign(form.value, data.user);
+
+  form.value.roleIds = data.roleIds || [];
+  form.value.storeIds = data.storeIds || (data.user && data.user.storeIds) || [];
+  form.value.password = '';
+  dialogVisible.value = true;
+};
+
+const handleStatusChange = async (row: any) => {
+  const text = row.status === '0' ? '启用' : '废弃';
+  try {
+    await ElMessageBox.confirm(`确认要${text} ${row.userName} 的账号吗?`, '提示', { type: 'warning' });
+    await userApi.changeUserStatus(row.userId, row.status);
+    ElMessage.success(`${text}成功`);
+  } catch {
+    row.status = row.status === '0' ? '1' : '0';
+  }
+};
+
+const handleResetPwd = async (row: any) => {
+  try {
+    await ElMessageBox.confirm(`确认重置 ${row.userName} 的密码为 ${initPassword.value} 吗?`, '提示', { type: 'warning' });
+    await userApi.resetUserPwd(row.userId, initPassword.value);
+    ElMessage.success('重置成功');
+  } catch {}
+};
+
+const handleDelete = async (row: any) => {
+  try {
+    await ElMessageBox.confirm('是否确认删除该账号?', '提示', { type: 'warning' });
+    await userApi.delUser(row.userId);
+    ElMessage.success('删除成功');
+    getList();
+  } catch {}
+};
+
+const saveAccount = () => {
+  userFormRef.value?.validate(async (valid) => {
+    if (valid) {
+      buttonLoading.value = true;
+      try {
+        if (form.value.userId) {
+          if (form.value.userId === useUserStore().userId) {
+            form.value.roleIds = null as any;
+            form.value.storeIds = null as any;
+          }
+          await userApi.updateUser(form.value);
+        } else {
+          await userApi.addUser(form.value);
+        }
+        ElMessage.success('保存成功');
+        dialogVisible.value = false;
+        getList();
+      } finally {
+        buttonLoading.value = false;
+      }
+    }
+  });
+};
+
+onMounted(() => {
+  getList();
+  getStoreData();
+  getRoleData();
+  proxy?.getConfigKey('sys.user.initPassword').then((res: any) => {
+    if (res && res.data) {
+      initPassword.value = res.data;
+    }
+  });
+});
+</script>
+
+<style scoped>
+.page-container {
+  padding: 8px;
+}
+.card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+.pagination-container {
+  margin-top: 20px;
+  display: flex;
+  justify-content: flex-end;
+}
+</style>

+ 41 - 74
src/views/system/role/index.vue

@@ -1,91 +1,58 @@
 <template>
   <div class="p-2">
     <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
-      <div v-show="showSearch" class="mb-[10px]">
-        <el-card shadow="hover">
-          <el-form ref="queryFormRef" :model="queryParams" :inline="true">
-            <el-form-item label="角色名称" prop="roleName">
-              <el-input v-model="queryParams.roleName" placeholder="请输入角色名称" clearable @keyup.enter="handleQuery" />
-            </el-form-item>
-            <el-form-item label="权限字符" prop="roleKey">
-              <el-input v-model="queryParams.roleKey" placeholder="请输入权限字符" clearable @keyup.enter="handleQuery" />
-            </el-form-item>
-            <el-form-item label="状态" prop="status">
-              <el-select v-model="queryParams.status" placeholder="角色状态" clearable>
-                <el-option v-for="dict in sys_normal_disable" :key="dict.value" :label="dict.label" :value="dict.value" />
-              </el-select>
-            </el-form-item>
-            <el-form-item label="创建时间" style="width: 308px">
-              <el-date-picker
-                v-model="dateRange"
-                value-format="YYYY-MM-DD HH:mm:ss"
-                type="daterange"
-                range-separator="-"
-                start-placeholder="开始日期"
-                end-placeholder="结束日期"
-                :default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
-              ></el-date-picker>
-            </el-form-item>
-
-            <el-form-item>
-              <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
-              <el-button icon="Refresh" @click="resetQuery">重置</el-button>
-            </el-form-item>
-          </el-form>
-        </el-card>
+      <div v-show="showSearch" class="mb-[10px] bg-white p-[20px] rounded-[4px] shadow-sm" style="box-shadow: 0 1px 4px rgba(0,21,41,.08);">
+        <el-form ref="queryFormRef" :model="queryParams" :inline="true" style="margin-bottom: -18px;">
+          <el-form-item label="角色名称" prop="roleName">
+            <el-input v-model="queryParams.roleName" placeholder="请输入角色名称" clearable @keyup.enter="handleQuery" />
+          </el-form-item>
+<!--          <el-form-item label="权限字符" prop="roleKey">-->
+<!--            <el-input v-model="queryParams.roleKey" placeholder="请输入权限字符" clearable @keyup.enter="handleQuery" />-->
+<!--          </el-form-item>-->
+          <el-form-item label="状态" prop="status">
+            <el-select v-model="queryParams.status" placeholder="角色状态" clearable style="width: 150px">
+              <el-option v-for="dict in sys_normal_disable" :key="dict.value" :label="dict.label" :value="dict.value" />
+            </el-select>
+          </el-form-item>
+          <el-form-item>
+            <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+            <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+            <el-button v-hasPermi="['system:role:export']" icon="Download" @click="handleExport">导出</el-button>
+            <el-button v-hasPermi="['system:role:remove']" type="danger" plain icon="Delete" :disabled="ids.length === 0" @click="handleDelete()">批量删除</el-button>
+          </el-form-item>
+        </el-form>
       </div>
     </transition>
 
-    <el-card shadow="hover">
-      <template #header>
-        <el-row :gutter="10">
-          <el-col :span="1.5">
-            <el-button v-hasPermi="['system:role:add']" type="primary" plain icon="Plus" @click="handleAdd()">新增</el-button>
-          </el-col>
-          <el-col :span="1.5">
-            <el-button v-hasPermi="['system:role:edit']" type="success" plain :disabled="single" icon="Edit" @click="handleUpdate()">修改</el-button>
-          </el-col>
-          <el-col :span="1.5">
-            <el-button v-hasPermi="['system:role:remove']" type="danger" plain :disabled="ids.length === 0" @click="handleDelete()">删除</el-button>
-          </el-col>
-          <el-col :span="1.5">
-            <el-button v-hasPermi="['system:role:export']" type="warning" plain icon="Download" @click="handleExport">导出</el-button>
-          </el-col>
-          <right-toolbar v-model:show-search="showSearch" @query-table="getList"></right-toolbar>
-        </el-row>
-      </template>
+    <div class="bg-white rounded-[4px] shadow-sm" style="box-shadow: 0 1px 4px rgba(0,21,41,.08); padding: 20px;">
+      <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
+        <div style="font-size: 16px; font-weight: bold; color: #303133;">角色与权限管理</div>
+        <el-button type="warning" @click="handleAdd" v-hasPermi="['system:role:add']" style="background-color: #e6a23c; border-color: #e6a23c; color: #fff;">+ 新增角色</el-button>
+      </div>
 
-      <el-table ref="roleTableRef" border v-loading="loading" :data="roleList" @selection-change="handleSelectionChange">
+      <el-table ref="roleTableRef" v-loading="loading" :data="roleList" @selection-change="handleSelectionChange" :header-cell-style="{ color: '#909399', fontWeight: 'normal', backgroundColor: '#fff', borderBottom: '1px solid #ebeef5' }">
         <el-table-column type="selection" width="55" align="center" />
-        <el-table-column v-if="false" label="角色编号" prop="roleId" width="120" />
-        <el-table-column label="角色名称" prop="roleName" :show-overflow-tooltip="true" width="150" />
-        <el-table-column label="权限字符" prop="roleKey" :show-overflow-tooltip="true" width="200" />
-        <el-table-column label="显示顺序" prop="roleSort" width="100" />
-        <el-table-column label="状态" align="center" width="100">
+        <el-table-column label="角色名称" prop="roleName" width="200" />
+        <el-table-column label="描述" prop="remark" min-width="300">
           <template #default="scope">
-            <el-switch v-model="scope.row.status" active-value="0" inactive-value="1" @change="handleStatusChange(scope.row)"></el-switch>
+            {{ scope.row.remark || '-' }}
           </template>
         </el-table-column>
-        <el-table-column label="创建时间" align="center" prop="createTime">
+        <el-table-column label="状态" align="center" width="100">
           <template #default="scope">
-            <span>{{ proxy.parseTime(scope.row.createTime) }}</span>
+            <el-switch v-model="scope.row.status" active-value="0" inactive-value="1" @change="handleStatusChange(scope.row)" style="--el-switch-on-color: #f3d19e; --el-switch-off-color: #dcdfe6"></el-switch>
           </template>
         </el-table-column>
-
-        <el-table-column fixed="right" label="操作" width="180">
+        <el-table-column label="操作" width="250" align="left">
           <template #default="scope">
-            <el-tooltip v-if="scope.row.roleId !== 1" content="修改" placement="top">
-              <el-button v-hasPermi="['system:role:edit']" link type="primary" icon="Edit" @click="handleUpdate(scope.row)"></el-button>
-            </el-tooltip>
-            <el-tooltip v-if="scope.row.roleId !== 1" content="删除" placement="top">
-              <el-button v-hasPermi="['system:role:remove']" link type="primary" icon="Delete" @click="handleDelete(scope.row)"></el-button>
-            </el-tooltip>
-            <el-tooltip v-if="scope.row.roleId !== 1" content="数据权限" placement="top">
-              <el-button v-hasPermi="['system:role:edit']" link type="primary" icon="CircleCheck" @click="handleDataScope(scope.row)"></el-button>
-            </el-tooltip>
-<!--            <el-tooltip v-if="scope.row.roleId !== 1" content="分配用户" placement="top">-->
-<!--              <el-button v-hasPermi="['system:role:edit']" link type="primary" icon="User" @click="handleAuthUser(scope.row)"></el-button>-->
-<!--            </el-tooltip>-->
+            <div style="display: flex; gap: 15px;" v-if="scope.row.roleId !== 1">
+              <el-button link style="color: #e6a23c; padding: 0;" @click="handleUpdate(scope.row)" v-hasPermi="['system:role:edit']">编辑</el-button>
+              <el-button link style="color: #67c23a; padding: 0;" @click="handleDataScope(scope.row)" v-hasPermi="['system:role:edit']">分配权限</el-button>
+              <el-button link style="color: #f56c6c; padding: 0;" @click="handleDelete(scope.row)" v-hasPermi="['system:role:remove']">删除</el-button>
+            </div>
+            <div v-else>
+              <span style="color: #909399; font-size: 13px;">-</span>
+            </div>
           </template>
         </el-table-column>
       </el-table>
@@ -97,7 +64,7 @@
         v-model:limit="queryParams.pageSize"
         @pagination="getList"
       />
-    </el-card>
+    </div>
 
     <el-dialog v-model="dialog.visible" :title="dialog.title" width="500px" append-to-body>
       <el-form ref="roleFormRef" :model="form" :rules="rules" label-width="100px">