Explorar el Código

将 客户管理 换成 员工管理; 修改登录

Huanyi hace 2 días
padre
commit
99fda81abc

+ 12 - 0
src/api/erp/client/index.ts

@@ -26,3 +26,15 @@ export const searchErpClient = (name: string): AxiosPromise<ErpClientVO[]> => {
     params: { name }
   });
 };
+
+/**
+ * 根据 RowID 列表批量查询客户
+ * @param rowIds 逗号分隔的 rowId 字符串
+ */
+export const getErpClientByIds = (rowIds: string): AxiosPromise<ErpClientVO[]> => {
+  return request({
+    url: '/erp/client/listByIds',
+    method: 'get',
+    params: { rowIds }
+  });
+};

+ 2 - 1
src/api/login.ts

@@ -14,7 +14,8 @@ export function login(data: LoginData): AxiosPromise<LoginResult> {
   const params = {
     ...data,
     clientId: data.clientId || clientId,
-    grantType: data.grantType || 'password'
+    grantType: data.grantType || 'password',
+    userSource: data.userSource || 'sys_user'
   };
   return request({
     url: '/auth/login',

+ 4 - 4
src/api/system/complaint/types.ts

@@ -4,9 +4,9 @@ export interface ComplaintVO {
   content: string;
   images: string;
   imageUrls: string;
-  customerId: string | number;
-  customerNickname: string;
-  customerPhone: string;
+  employeeId: string | number;
+  employeeName: string;
+  employeePhone: string;
   status: string;
   dealResult: string;
   dealImages: string;
@@ -16,7 +16,7 @@ export interface ComplaintVO {
 
 export interface ComplaintQuery extends PageQuery {
   feedbackType?: string;
-  customerName?: string;
+  employeeName?: string;
 }
 
 /**

+ 0 - 89
src/api/system/customer/index.ts

@@ -1,89 +0,0 @@
-import request from '@/utils/request';
-import { AxiosPromise } from 'axios';
-import { CustomerVO, CustomerForm, CustomerQuery } from '@/api/system/customer/types';
-
-/**
- * 查询客户列表
- * @param query
- */
-export const listCustomer = (query?: CustomerQuery): AxiosPromise<CustomerVO[]> => {
-  return request({
-    url: '/system/customer/list',
-    method: 'get',
-    params: query
-  });
-};
-
-/**
- * 查询客户详细
- * @param id
- */
-export const getCustomer = (id: string | number): AxiosPromise<CustomerVO> => {
-  return request({
-    url: '/system/customer/' + id,
-    method: 'get'
-  });
-};
-
-/**
- * 查询客户详情(含授权客户完整信息列表)
- * @param id
- * @Author: Antigravity
- */
-export const getCustomerDetail = (id: string | number): AxiosPromise<CustomerVO> => {
-  return request({
-    url: '/system/customer/detail/' + id,
-    method: 'get'
-  });
-};
-
-/**
- * 新增客户
- * @param data
- */
-export const addCustomer = (data: CustomerForm) => {
-  return request({
-    url: '/system/customer',
-    method: 'post',
-    data: data
-  });
-};
-
-/**
- * 授权客户(支持多个,逗号分隔)
- * @param id 客户ID
- * @param authClientFRowIDs 授权客户 RowID 列表(逗号分隔)
- * @Author: Antigravity
- */
-export const authCustomer = (id: string | number, authClientFRowIDs: string) => {
-  return request({
-    url: '/system/customer/auth',
-    method: 'put',
-    params: { id, authClientFRowIDs }
-  });
-};
-
-/**
- * 修改客户状态(启用/禁用)
- * @Author: Antigravity
- */
-export const changeCustomerStatus = (id: string | number, status: string) => {
-  return request({
-    url: '/system/customer/changeStatus',
-    method: 'put',
-    params: { id, status }
-  });
-};
-
-/**
- * 导出客户列表
- * @param query
- */
-export const exportCustomer = (query: CustomerQuery) => {
-  return request({
-    url: '/system/customer/export',
-    method: 'post',
-    params: query,
-    responseType: 'blob'
-  });
-};

+ 86 - 0
src/api/system/employee/index.ts

@@ -0,0 +1,86 @@
+import request from '@/utils/request';
+import { AxiosPromise } from 'axios';
+import { EmployeeVO, EmployeeForm, EmployeeQuery } from '@/api/system/employee/types';
+
+/**
+ * 查询员工列表
+ * @param query
+ */
+export const listEmployee = (query?: EmployeeQuery): AxiosPromise<EmployeeVO[]> => {
+  return request({
+    url: '/system/employee/list',
+    method: 'get',
+    params: query
+  });
+};
+
+/**
+ * 查询员工详细
+ * @param id
+ */
+export const getEmployee = (id: string | number): AxiosPromise<EmployeeVO> => {
+  return request({
+    url: '/system/employee/' + id,
+    method: 'get'
+  });
+};
+
+/**
+ * 查询员工详情(含授权客户完整信息列表)
+ * @param id
+ */
+export const getEmployeeDetail = (id: string | number): AxiosPromise<EmployeeVO> => {
+  return request({
+    url: '/system/employee/detail/' + id,
+    method: 'get'
+  });
+};
+
+/**
+ * 新增员工
+ * @param data
+ */
+export const addEmployee = (data: EmployeeForm) => {
+  return request({
+    url: '/system/employee',
+    method: 'post',
+    data: data
+  });
+};
+
+/**
+ * 授权客户(支持多个,逗号分隔)
+ * @param id 员工ID
+ * @param authClientFRowIDs 授权客户 RowID 列表(逗号分隔)
+ */
+export const authEmployee = (id: string | number, authClientFRowIDs: string) => {
+  return request({
+    url: '/system/employee/auth',
+    method: 'put',
+    params: { id, authClientFRowIDs }
+  });
+};
+
+/**
+ * 修改员工状态(启用/禁用)
+ */
+export const changeEmployeeStatus = (id: string | number, status: string) => {
+  return request({
+    url: '/system/employee/changeStatus',
+    method: 'put',
+    params: { id, status }
+  });
+};
+
+/**
+ * 导出员工列表
+ * @param query
+ */
+export const exportEmployee = (query: EmployeeQuery) => {
+  return request({
+    url: '/system/employee/export',
+    method: 'post',
+    params: query,
+    responseType: 'blob'
+  });
+};

+ 20 - 15
src/api/system/customer/types.ts → src/api/system/employee/types.ts

@@ -17,16 +17,16 @@ export interface ErpClientBriefVO {
   enterDate: string;
 }
 
-export interface CustomerVO {
+export interface EmployeeVO {
   /**
-   * 客户ID
+   * 员工ID
    */
   id: string | number;
 
   /**
-   * 用户
+   * 
    */
-  userName: string;
+  name: string;
 
   /**
    * 手机号
@@ -46,7 +46,7 @@ export interface CustomerVO {
   /**
    * 授权客户 RowID(多个逗号分隔)
    */
-  authClientFRowID: string;
+  authClientFRowIDs: string;
 
   /**
    * 授权客户名称(多个 / 分隔)
@@ -69,7 +69,7 @@ export interface CustomerVO {
   authClientList?: ErpClientBriefVO[];
 
   /**
-   * 头像地址
+   * 头像OSS ID
    */
   avatar: string;
 
@@ -89,16 +89,16 @@ export interface CustomerVO {
   createTime: string;
 }
 
-export interface CustomerForm extends BaseEntity {
+export interface EmployeeForm extends BaseEntity {
   /**
-   * 客户ID
+   * 员工ID
    */
   id?: string | number;
 
   /**
-   * 用户
+   * 
    */
-  userName?: string;
+  name?: string;
 
   /**
    * 手机号
@@ -118,19 +118,24 @@ export interface CustomerForm extends BaseEntity {
   /**
    * 授权客户 RowID
    */
-  authClientFRowID?: string;
+  authClientFRowIDs?: string;
 
   /**
-   * 头像地址
+   * 头像OSS ID
    */
   avatar?: string;
+
+  /**
+   * 密码
+   */
+  password?: string;
 }
 
-export interface CustomerQuery extends PageQuery {
+export interface EmployeeQuery extends PageQuery {
   /**
-   * 用户名
+   * 
    */
-  userName?: string;
+  name?: string;
 
   /**
    * 手机号

+ 1 - 0
src/api/types.ts

@@ -26,6 +26,7 @@ export interface LoginData {
   uuid?: string;
   clientId: string;
   grantType: string;
+  userSource?: string;
 }
 
 /**

+ 7 - 7
src/views/complaint/index.vue

@@ -10,8 +10,8 @@
               <el-option v-for="dict in sys_complaint_type" :key="dict.value" :label="dict.label" :value="dict.value" />
             </el-select>
           </el-form-item>
-          <el-form-item label="客户姓名" prop="customerName">
-            <el-input v-model="queryParams.customerName" placeholder="请输入客户姓名" clearable @keyup.enter="handleQuery" />
+          <el-form-item label="员工姓名" prop="employeeName">
+            <el-input v-model="queryParams.employeeName" placeholder="请输入员工姓名" clearable @keyup.enter="handleQuery" />
           </el-form-item>
           <el-form-item>
             <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
@@ -59,8 +59,8 @@
             </el-tag>
           </template>
         </el-table-column>
-        <el-table-column label="用户昵称" align="center" prop="customerNickname" width="120" />
-        <el-table-column label="用户手机" align="center" prop="customerPhone" width="130" />
+        <el-table-column label="用户姓名" align="center" prop="employeeName" width="120" />
+        <el-table-column label="用户手机" align="center" prop="employeePhone" width="130" />
         <el-table-column label="提交时间" align="center" prop="createTime" width="170" />
         <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="200">
           <template #default="scope">
@@ -87,8 +87,8 @@
           <div class="detail-user-info">
             <el-avatar :size="48" icon="UserFilled" />
             <div>
-              <div class="detail-nickname">{{ detailDialog.data.customerNickname }}</div>
-              <div class="detail-phone">{{ detailDialog.data.customerPhone || '-' }}</div>
+              <div class="detail-nickname">{{ detailDialog.data.employeeName }}</div>
+              <div class="detail-phone">{{ detailDialog.data.employeePhone || '-' }}</div>
             </div>
           </div>
           <el-tag :type="detailDialog.data.status === '0' ? 'warning' : 'success'" size="small">
@@ -179,7 +179,7 @@ const data = reactive<PageData<object, ComplaintQuery>>({
     pageNum: 1,
     pageSize: 10,
     feedbackType: undefined,
-    customerName: undefined
+    employeeName: undefined
   },
   rules: {}
 });

+ 0 - 611
src/views/customer/index.vue

@@ -1,611 +0,0 @@
-<!-- @Author: Antigravity -->
-<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="search">
-        <el-form ref="queryFormRef" :model="queryParams" :inline="true" label-width="85px">
-          <el-form-item label="用户名" prop="userName">
-            <el-input v-model="queryParams.userName" placeholder="请输入用户名" clearable @keyup.enter="handleQuery" />
-          </el-form-item>
-          <el-form-item label="手机号" prop="phone">
-            <el-input v-model="queryParams.phone" placeholder="请输入手机号" clearable @keyup.enter="handleQuery" />
-          </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>
-      </div>
-    </transition>
-
-    <el-card shadow="never">
-      <template #header>
-        <el-row :gutter="10" class="mb8">
-          <right-toolbar v-model:show-search="showSearch" @query-table="getList"></right-toolbar>
-        </el-row>
-      </template>
-
-      <el-table v-loading="loading" :data="customerList" border @selection-change="handleSelectionChange">
-        <el-table-column type="selection" width="55" align="center" />
-        <el-table-column label="序号" align="center" width="70" type="index" />
-        <el-table-column label="昵称" align="center" prop="userName" min-width="120" />
-        <el-table-column label="头像" align="center" width="80">
-          <template #default="scope">
-            <el-avatar :size="36" :src="scope.row.avatarUrl" icon="UserFilled" />
-          </template>
-        </el-table-column>
-        <el-table-column label="手机" align="center" prop="phone" width="140" />
-        <el-table-column label="注册时间" align="center" prop="createTime" width="170" />
-        <el-table-column label="状态" align="center" width="100">
-          <template #default="scope">
-            <el-switch v-if="checkPermi(['customer:customer:changeStatus'])" v-model="scope.row._statusActive"
-              :loading="scope.row._statusLoading" inline-prompt @change="handleStatusChange(scope.row)" />
-            <el-tag v-else :type="scope.row.status === '0' ? 'danger' : 'success'">
-              {{ scope.row.status === '0' ? '禁用' : '启用' }}
-            </el-tag>
-          </template>
-        </el-table-column>
-        <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="160">
-          <template #default="scope">
-            <el-button v-hasPermi="['customer:customer:auth']" link type="success" icon="User"
-              @click="handleAuth(scope.row)">授权</el-button>
-            <el-button v-hasPermi="['customer:customer:query']" link type="primary" icon="View"
-              @click="handleDetail(scope.row)">详情</el-button>
-          </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 v-model="authDialog.visible" title="授权客户" width="700px" append-to-body @opened="initAuthDialog">
-      <!-- 已选中客户卡片 -->
-      <div class="selected-header">
-        <span class="section-label">已授权客户</span>
-        <span class="selected-count">共 {{ selectedAuthClients.length }} 家</span>
-      </div>
-      <div v-if="selectedAuthClients.length > 0" class="selected-card-grid">
-        <div v-for="(client, idx) in selectedAuthClients" :key="idx" class="selected-client-card">
-          <span class="card-index">{{ idx + 1 }}</span>
-          <span class="card-name">{{ client.name }}</span>
-          <el-icon class="card-remove" @click="removeAuthClient(idx)">
-            <Close />
-          </el-icon>
-        </div>
-      </div>
-      <el-empty v-else description="暂无已授权客户" :image-size="40" />
-
-      <el-divider />
-
-      <!-- 搜索区 -->
-      <div class="search-row">
-        <el-input v-model="authSearchKeyword" placeholder="输入客户名称搜索" clearable @keyup.enter="handleAuthSearch"
-          style="flex: 1">
-          <template #append>
-            <el-button :loading="authSearchLoading" icon="Search" @click="handleAuthSearch">搜索</el-button>
-          </template>
-        </el-input>
-      </div>
-
-      <!-- 搜索结果 -->
-      <div v-if="authSearchResults.length > 0" class="search-results">
-        <div class="section-label mb-2">搜索结果</div>
-        <div v-for="item in authSearchResults" :key="item.rowId" class="search-result-item"
-          :class="{ disabled: isAuthClientSelected(item.rowId) }" @click="addAuthClient(item)">
-          <div class="result-info">
-            <span class="result-name">{{ item.name }}</span>
-            <span class="result-meta">{{ item.num || '-' }} | {{ item.clientClass || '-' }}</span>
-          </div>
-          <el-icon v-if="isAuthClientSelected(item.rowId)" class="result-check">
-            <CircleCheckFilled />
-          </el-icon>
-          <el-icon v-else class="result-add">
-            <CirclePlus />
-          </el-icon>
-        </div>
-      </div>
-      <el-empty v-else-if="authSearched" description="未找到匹配的客户" :image-size="40" />
-
-      <template #footer>
-        <div class="dialog-footer">
-          <el-button :loading="buttonLoading" type="primary" icon="Check" @click="confirmAuth">确 定</el-button>
-          <el-button icon="Close" @click="authDialog.visible = false">取 消</el-button>
-        </div>
-      </template>
-    </el-dialog>
-
-    <!-- 客户详情对话框 -->
-    <el-dialog v-model="detailDialog.visible" title="客户详情" width="680px" append-to-body>
-      <template v-if="detailDialog.data">
-        <div class="detail-header">
-          <el-avatar :size="72" :src="detailDialog.data.avatarUrl" icon="UserFilled" class="detail-avatar" />
-          <div class="detail-name-box">
-            <text class="detail-name">{{ detailDialog.data.userName }}</text>
-            <el-tag :type="detailDialog.data.status === '0' ? 'danger' : 'success'" size="small"
-              class="detail-status-tag">
-              {{ detailDialog.data.status === '0' ? '已禁用' : '已启用' }}
-            </el-tag>
-          </div>
-        </div>
-        <div class="detail-divider"></div>
-        <el-descriptions :column="2" border size="small" class="detail-descriptions">
-          <el-descriptions-item label="客户ID">{{ detailDialog.data.id }}</el-descriptions-item>
-          <el-descriptions-item label="手机号">{{ detailDialog.data.phone || '-' }}</el-descriptions-item>
-          <el-descriptions-item label="微信OpenID">{{ detailDialog.data.wechatOpenid || '-' }}</el-descriptions-item>
-          <el-descriptions-item label="微信UnionID">{{ detailDialog.data.wechatUnionid || '-' }}</el-descriptions-item>
-          <el-descriptions-item label="注册时间">{{ detailDialog.data.createTime }}</el-descriptions-item>
-        </el-descriptions>
-
-        <div class="section-title">授权客户</div>
-        <div v-if="detailDialog.data.authClientList && detailDialog.data.authClientList.length > 0">
-          <div class="auth-client-card" v-for="(client, idx) in detailDialog.data.authClientList" :key="idx">
-            <span class="client-index">{{ idx + 1 }}</span>
-            <div class="client-info">
-              <div class="client-row"><span class="client-label">名称</span><span class="client-value">{{ client.name
-                  }}</span></div>
-              <div class="client-row"><span class="client-label">类型</span><span class="client-value">{{
-                client.clientClass || '-' }}</span></div>
-              <div class="client-row"><span class="client-label">加入时间</span><span class="client-value">{{
-                client.enterDate || '-' }}</span></div>
-            </div>
-          </div>
-        </div>
-        <el-empty v-else description="暂无授权客户" :image-size="60" />
-      </template>
-    </el-dialog>
-  </div>
-</template>
-
-<script setup name="Customer" lang="ts">
-import { listCustomer, getCustomerDetail, authCustomer, changeCustomerStatus } from '@/api/system/customer';
-import { CustomerVO, CustomerQuery, CustomerForm, ErpClientBriefVO } from '@/api/system/customer/types';
-import { searchErpClient } from '@/api/erp/client';
-import { ErpClientVO } from '@/api/erp/client/types';
-import { checkPermi } from '@/utils/permission';
-
-/** @Author: Antigravity */
-
-const { proxy } = getCurrentInstance() as ComponentInternalInstance;
-
-const customerList = ref<CustomerVO[]>([]);
-const buttonLoading = ref(false);
-const loading = ref(true);
-const showSearch = ref(true);
-const ids = ref<Array<string | number>>([]);
-const total = ref(0);
-
-const queryFormRef = ref<ElFormInstance>();
-
-// 授权客户对话框相关
-const authDialog = reactive({
-  visible: false,
-  customerId: undefined as string | number | undefined
-});
-const authSearchKeyword = ref('');
-const authSearchLoading = ref(false);
-const authSearchResults = ref<ErpClientVO[]>([]);
-const authSearched = ref(false);
-const selectedAuthClients = ref<ErpClientVO[]>([]);
-
-// 详情对话框
-const detailDialog = reactive({
-  visible: false,
-  data: null as CustomerVO | null
-});
-
-const data = reactive<PageData<CustomerForm, CustomerQuery>>({
-  form: {} as any,
-  queryParams: {
-    pageNum: 1,
-    pageSize: 10,
-    userName: undefined,
-    phone: undefined,
-    wechatOpenid: undefined
-  },
-  rules: {}
-});
-
-const { queryParams } = toRefs(data);
-
-/**
- * 处理列表数据,附加状态展示字段
- */
-const processList = (list: CustomerVO[]) => {
-  list.forEach(item => {
-    (item as any)._statusActive = item.status !== '0';
-    (item as any)._statusLoading = false;
-  });
-};
-
-/** 查询客户列表 */
-const getList = async () => {
-  loading.value = true;
-  const res = await listCustomer(queryParams.value);
-  const rows = res.rows as CustomerVO[] || [];
-  processList(rows);
-  customerList.value = rows;
-  total.value = res.total;
-  loading.value = false;
-};
-
-/** 搜索按钮操作 */
-const handleQuery = () => {
-  queryParams.value.pageNum = 1;
-  getList();
-};
-
-/** 重置按钮操作 */
-const resetQuery = () => {
-  queryFormRef.value?.resetFields();
-  handleQuery();
-};
-
-/** 多选框选中数据 */
-const handleSelectionChange = (selection: CustomerVO[]) => {
-  ids.value = selection.map((item) => item.id);
-};
-
-/** 状态切换 */
-const handleStatusChange = async (row: CustomerVO & { _statusActive: boolean; _statusLoading: boolean }) => {
-  const newStatus = row._statusActive ? '1' : '0';
-  try {
-    row._statusLoading = true;
-    await changeCustomerStatus(row.id, newStatus);
-    proxy?.$modal.msgSuccess(newStatus === '1' ? '已启用' : '已禁用');
-  } catch (e: any) {
-    // 失败回滚状态
-    row._statusActive = !row._statusActive;
-    proxy?.$modal.msgError(e?.msg || '操作失败');
-  } finally {
-    row._statusLoading = false;
-  }
-};
-
-/** 授权客户按钮操作 */
-const handleAuth = (row: CustomerVO) => {
-  authDialog.customerId = row.id;
-  authDialog.visible = true;
-};
-
-/** 弹窗打开时初始化 */
-const initAuthDialog = async () => {
-  authSearchKeyword.value = '';
-  authSearchResults.value = [];
-  authSearched.value = false;
-  selectedAuthClients.value = [];
-  if (!authDialog.customerId) return;
-  try {
-    const res = await getCustomerDetail(authDialog.customerId);
-    if (res.data?.authClientList) {
-      selectedAuthClients.value = res.data.authClientList.map(c => ({
-        rowId: c.rowId,
-        name: c.name,
-        num: c.rowId,
-        clientClass: c.clientClass
-      } as ErpClientVO));
-    }
-  } catch (e) {
-    console.error('加载已授权客户失败', e);
-  }
-};
-
-/** 搜索ERP客户 */
-const handleAuthSearch = async () => {
-  const keyword = authSearchKeyword.value?.trim();
-  if (!keyword) {
-    proxy?.$modal.msgWarning('请输入客户名称');
-    return;
-  }
-  authSearchLoading.value = true;
-  authSearched.value = true;
-  try {
-    const res = await searchErpClient(keyword);
-    authSearchResults.value = res.data || [];
-  } catch (e) {
-    authSearchResults.value = [];
-  } finally {
-    authSearchLoading.value = false;
-  }
-};
-
-/** 判断客户是否已选中 */
-const isAuthClientSelected = (rowId: string) => {
-  return selectedAuthClients.value.some(c => c.rowId === rowId);
-};
-
-/** 添加客户到已选列表 */
-const addAuthClient = (client: ErpClientVO) => {
-  if (isAuthClientSelected(client.rowId)) return;
-  selectedAuthClients.value.push({ ...client });
-};
-
-/** 从已选列表移除客户 */
-const removeAuthClient = (idx: number) => {
-  selectedAuthClients.value.splice(idx, 1);
-};
-
-/** 确认授权 */
-const confirmAuth = async () => {
-  if (!selectedAuthClients.value || selectedAuthClients.value.length === 0) {
-    proxy?.$modal.msgError('请至少选择一个ERP客户');
-    return;
-  }
-  buttonLoading.value = true;
-  try {
-    const authClientFRowIDs = selectedAuthClients.value.map(c => c.rowId).join(',');
-    await authCustomer(authDialog.customerId!, authClientFRowIDs);
-    proxy?.$modal.msgSuccess('授权成功');
-    authDialog.visible = false;
-    getList();
-  } finally {
-    buttonLoading.value = false;
-  }
-};
-
-/** 查看客户详情 */
-const handleDetail = async (row: CustomerVO) => {
-  detailDialog.visible = true;
-  detailDialog.data = null;
-  try {
-    const res = await getCustomerDetail(row.id);
-    detailDialog.data = res.data;
-  } catch (e) {
-    proxy?.$modal.msgError('获取客户详情失败');
-    detailDialog.visible = false;
-  }
-};
-
-onMounted(() => {
-  getList();
-});
-</script>
-
-<style scoped>
-/* ========== 授权对话框 ========== */
-.selected-header {
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-  margin-bottom: 12px;
-}
-
-.section-label {
-  font-size: 14px;
-  font-weight: 600;
-  color: #303133;
-}
-
-.selected-count {
-  font-size: 12px;
-  color: #909399;
-}
-
-.selected-card-grid {
-  display: grid;
-  grid-template-columns: repeat(4, 1fr);
-  gap: 10px;
-}
-
-.selected-client-card {
-  display: flex;
-  align-items: center;
-  gap: 6px;
-  background: #f0f9eb;
-  border: 1px solid #c2e7b0;
-  border-radius: 6px;
-  padding: 6px 10px;
-  font-size: 12px;
-  overflow: hidden;
-}
-
-.card-index {
-  width: 18px;
-  height: 18px;
-  background: #67C23A;
-  color: #fff;
-  border-radius: 50%;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  font-size: 11px;
-  font-weight: 600;
-  flex-shrink: 0;
-}
-
-.card-name {
-  flex: 1;
-  color: #303133;
-  overflow: hidden;
-  text-overflow: ellipsis;
-  white-space: nowrap;
-}
-
-.card-remove {
-  color: #909399;
-  cursor: pointer;
-  flex-shrink: 0;
-  font-size: 14px;
-}
-
-.card-remove:hover {
-  color: #F56C6C;
-}
-
-.search-row {
-  display: flex;
-}
-
-.search-results {
-  margin-top: 12px;
-  max-height: 260px;
-  overflow-y: auto;
-}
-
-.search-result-item {
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-  padding: 10px 14px;
-  border: 1px solid #ebeef5;
-  border-radius: 6px;
-  margin-bottom: 8px;
-  cursor: pointer;
-  transition: all 0.2s;
-}
-
-.search-result-item:hover {
-  border-color: #409EFF;
-  background: #ecf5ff;
-}
-
-.search-result-item.disabled {
-  opacity: 0.5;
-  cursor: not-allowed;
-  border-color: #c2e7b0;
-  background: #f0f9eb;
-}
-
-.result-info {
-  display: flex;
-  flex-direction: column;
-  gap: 2px;
-}
-
-.result-name {
-  font-size: 14px;
-  font-weight: 500;
-  color: #303133;
-}
-
-.result-meta {
-  font-size: 12px;
-  color: #909399;
-}
-
-.result-add {
-  color: #409EFF;
-  font-size: 20px;
-}
-
-.result-check {
-  color: #67C23A;
-  font-size: 20px;
-}
-
-/* ========== 详情弹窗 ========== */
-.section-title {
-  font-size: 14px;
-  font-weight: 600;
-  color: #303133;
-  margin: 20px 0 12px 0;
-  padding-left: 10px;
-  border-left: 3px solid #C1001C;
-}
-
-:deep(.el-dialog .pagination-container) {
-  display: flex !important;
-  justify-content: center !important;
-  background-color: transparent !important;
-  padding: 20px 0 10px 0 !important;
-  margin-top: 0 !important;
-}
-
-/* 详情弹窗样式 */
-.detail-header {
-  display: flex;
-  align-items: center;
-  gap: 24px;
-  padding-bottom: 20px;
-}
-
-.detail-avatar {
-  flex-shrink: 0;
-  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
-}
-
-.detail-name-box {
-  display: flex;
-  align-items: center;
-  gap: 12px;
-}
-
-.detail-name {
-  font-size: 22px;
-  font-weight: 600;
-  color: #1d1d1f;
-  letter-spacing: 0.5px;
-}
-
-.detail-status-tag {
-  flex-shrink: 0;
-}
-
-.detail-divider {
-  height: 1px;
-  background: linear-gradient(90deg, #eee 0%, transparent 100%);
-  margin-bottom: 20px;
-}
-
-.detail-descriptions {
-  margin-bottom: 8px;
-}
-
-/* 授权客户卡片 */
-.auth-client-card {
-  display: flex;
-  align-items: flex-start;
-  gap: 16px;
-  background: #fafafa;
-  border-radius: 12px;
-  padding: 16px 20px;
-  margin-bottom: 12px;
-  border: 1px solid #f0f0f0;
-  transition: box-shadow 0.2s;
-}
-
-.auth-client-card:hover {
-  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
-}
-
-.client-index {
-  width: 28px;
-  height: 28px;
-  background: #C1001C;
-  color: #fff;
-  border-radius: 50%;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  font-size: 13px;
-  font-weight: 600;
-  flex-shrink: 0;
-  margin-top: 2px;
-}
-
-.client-info {
-  flex: 1;
-  display: flex;
-  flex-direction: column;
-  gap: 6px;
-}
-
-.client-row {
-  display: flex;
-  gap: 12px;
-  font-size: 13px;
-}
-
-.client-label {
-  color: #999;
-  min-width: 60px;
-  flex-shrink: 0;
-}
-
-.client-value {
-  color: #333;
-  font-weight: 500;
-}
-</style>

+ 351 - 0
src/views/employee/components/SelectClientDialog.vue

@@ -0,0 +1,351 @@
+<template>
+  <el-dialog
+    :model-value="modelValue"
+    @update:model-value="$emit('update:modelValue', $event)"
+    :title="title"
+    width="560px"
+    append-to-body
+    class="select-client-dialog"
+    @opened="onOpened"
+  >
+    <div class="select-client-body">
+      <div class="sc-selected-bar" v-if="localSelected.length > 0">
+        <span class="sc-label">{{ selectedLabel }}</span>
+        <el-tag
+          v-for="(client, idx) in localSelected"
+          :key="idx"
+          closable
+          size="default"
+          @close="removeClient(idx)"
+          class="sc-tag"
+        >
+          {{ client.name }}
+        </el-tag>
+      </div>
+
+      <div class="sc-search">
+        <el-input
+          v-model="keyword"
+          :placeholder="searchPlaceholder"
+          clearable
+          size="large"
+          class="sc-search-input"
+          @keyup.enter="handleSearch"
+        >
+          <template #prefix>
+            <el-icon><Search /></el-icon>
+          </template>
+        </el-input>
+        <el-button type="primary" size="large" :loading="searchLoading" @click="handleSearch">
+          搜索
+        </el-button>
+      </div>
+
+      <div class="sc-results" v-if="searchResults.length > 0">
+        <div
+          v-for="item in searchResults"
+          :key="item.rowId"
+          class="sc-result-card"
+          :class="{ 'is-selected': isSelected(item.rowId) }"
+          @click="addClient(item)"
+        >
+          <div class="sc-result-left">
+            <div class="sc-result-avatar">{{ item.name?.charAt(0) }}</div>
+            <div class="sc-result-info">
+              <span class="sc-result-name">{{ item.name }}</span>
+              <span class="sc-result-extra">{{ item.clientClass || '未分类' }}</span>
+            </div>
+          </div>
+          <el-icon v-if="isSelected(item.rowId)" class="sc-result-selected">
+            <CircleCheckFilled />
+          </el-icon>
+          <el-icon v-else class="sc-result-add">
+            <CirclePlus />
+          </el-icon>
+        </div>
+      </div>
+      <el-empty v-else-if="searched" description="未找到匹配的客户" :image-size="60" />
+
+      <div v-if="!searched && searchResults.length === 0" class="sc-placeholder">
+        <el-icon class="sc-placeholder-icon"><Search /></el-icon>
+        <p>{{ placeholderText }}</p>
+      </div>
+    </div>
+    <template #footer>
+      <div class="add-dialog-footer">
+        <el-button size="large" @click="$emit('update:modelValue', false)">取 消</el-button>
+        <el-button size="large" type="primary" :loading="confirmLoading" @click="$emit('confirm')">
+          {{ confirmText }}({{ localSelected.length }})
+        </el-button>
+      </div>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+import { ref, watch } from 'vue';
+import { ErpClientVO } from '@/api/erp/client/types';
+
+const props = withDefaults(defineProps<{
+  modelValue: boolean;
+  title?: string;
+  selectedLabel?: string;
+  searchPlaceholder?: string;
+  placeholderText?: string;
+  confirmText?: string;
+  selectedClients: ErpClientVO[];
+  searchResults: ErpClientVO[];
+  searchLoading?: boolean;
+  confirmLoading?: boolean;
+  searched?: boolean;
+}>(), {
+  title: '选择授权客户',
+  selectedLabel: '已选',
+  searchPlaceholder: '搜索客户名称',
+  placeholderText: '输入客户名称进行搜索',
+  confirmText: '确认选择',
+  searchLoading: false,
+  confirmLoading: false,
+  searched: false
+});
+
+const emit = defineEmits<{
+  'update:modelValue': [value: boolean];
+  'update:selectedClients': [value: ErpClientVO[]];
+  'search': [keyword: string];
+  'confirm': [];
+}>();
+
+const keyword = ref('');
+const localSelected = ref<ErpClientVO[]>([...props.selectedClients]);
+
+watch(() => props.selectedClients, (val) => {
+  localSelected.value = [...val];
+}, { deep: true });
+
+watch(() => props.modelValue, (val) => {
+  if (!val) {
+    keyword.value = '';
+  }
+});
+
+const onOpened = () => {
+  keyword.value = '';
+  localSelected.value = [...props.selectedClients];
+};
+
+const handleSearch = () => {
+  const kw = keyword.value?.trim();
+  if (!kw) return;
+  emit('search', kw);
+};
+
+const isSelected = (rowId: string) => {
+  return localSelected.value.some(c => c.rowId === rowId);
+};
+
+const addClient = (client: ErpClientVO) => {
+  if (isSelected(client.rowId)) return;
+  localSelected.value.push({ ...client });
+  emit('update:selectedClients', [...localSelected.value]);
+};
+
+const removeClient = (idx: number) => {
+  localSelected.value.splice(idx, 1);
+  emit('update:selectedClients', [...localSelected.value]);
+};
+</script>
+
+<style scoped>
+.select-client-dialog :deep(.el-dialog__header) {
+  padding: 22px 24px 0;
+  margin: 0;
+}
+
+.select-client-dialog :deep(.el-dialog__title) {
+  font-size: 17px;
+  font-weight: 600;
+  color: #1a1a1a;
+}
+
+.select-client-dialog :deep(.el-dialog__body) {
+  padding: 16px 24px 8px;
+}
+
+.select-client-body {
+  display: flex;
+  flex-direction: column;
+  gap: 16px;
+}
+
+.sc-selected-bar {
+  display: flex;
+  align-items: center;
+  flex-wrap: wrap;
+  gap: 8px;
+  padding: 10px 14px;
+  background: #f9fafb;
+  border-radius: 10px;
+  min-height: 42px;
+}
+
+.sc-label {
+  font-size: 12px;
+  color: #909399;
+  font-weight: 500;
+  flex-shrink: 0;
+}
+
+.sc-tag {
+  border-radius: 16px;
+  padding: 4px 14px;
+  font-size: 13px;
+}
+
+.sc-search {
+  display: flex;
+  gap: 10px;
+}
+
+.sc-search-input {
+  flex: 1;
+}
+
+.sc-search-input :deep(.el-input__wrapper) {
+  border-radius: 8px;
+  box-shadow: 0 0 0 1px #e4e7ed inset;
+}
+
+.sc-search-input :deep(.el-input--large.is-focus .el-input__wrapper) {
+  box-shadow: 0 0 0 1px #409eff inset, 0 0 0 3px rgba(64, 158, 255, 0.08);
+}
+
+.sc-search .el-button {
+  border-radius: 8px;
+  padding: 0 24px;
+}
+
+.sc-results {
+  display: flex;
+  flex-direction: column;
+  gap: 6px;
+  max-height: 300px;
+  overflow-y: auto;
+}
+
+.sc-result-card {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 12px 16px;
+  border-radius: 10px;
+  border: 1px solid #f0f0f0;
+  cursor: pointer;
+  transition: all 0.2s;
+}
+
+.sc-result-card:hover {
+  background: #f5f8ff;
+  border-color: #d4e2ff;
+}
+
+.sc-result-card.is-selected {
+  background: #f0f5ff;
+  border-color: #bdd3ff;
+}
+
+.sc-result-left {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+}
+
+.sc-result-avatar {
+  width: 40px;
+  height: 40px;
+  border-radius: 10px;
+  background: linear-gradient(135deg, #409eff, #3370ff);
+  color: #fff;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 16px;
+  font-weight: 600;
+  flex-shrink: 0;
+}
+
+.sc-result-info {
+  display: flex;
+  flex-direction: column;
+  gap: 2px;
+}
+
+.sc-result-name {
+  font-size: 14px;
+  font-weight: 500;
+  color: #1a1a1a;
+}
+
+.sc-result-extra {
+  font-size: 12px;
+  color: #909399;
+}
+
+.sc-result-add {
+  font-size: 20px;
+  color: #c0c4cc;
+  transition: color 0.2s;
+}
+
+.sc-result-card:hover .sc-result-add {
+  color: #409eff;
+}
+
+.sc-result-selected {
+  font-size: 20px;
+  color: #409eff;
+}
+
+.sc-placeholder {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  padding: 48px 0;
+  color: #c0c4cc;
+}
+
+.sc-placeholder-icon {
+  font-size: 40px;
+  margin-bottom: 12px;
+}
+
+.sc-placeholder p {
+  margin: 0;
+  font-size: 13px;
+  color: #b0b5be;
+}
+
+.add-dialog-footer {
+  display: flex;
+  justify-content: flex-end;
+  gap: 12px;
+  padding-top: 4px;
+}
+
+.add-dialog-footer .el-button {
+  border-radius: 8px;
+  padding: 10px 28px;
+  font-size: 14px;
+  font-weight: 500;
+  letter-spacing: 0.5px;
+}
+
+.add-dialog-footer .el-button--primary {
+  box-shadow: 0 2px 8px rgba(64, 158, 255, 0.25);
+}
+
+.add-dialog-footer .el-button--primary:hover {
+  box-shadow: 0 4px 16px rgba(64, 158, 255, 0.35);
+}
+</style>

+ 816 - 0
src/views/employee/index.vue

@@ -0,0 +1,816 @@
+<!-- @Author: Antigravity -->
+<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="search">
+        <el-form ref="queryFormRef" :model="queryParams" :inline="true" label-width="85px">
+          <el-form-item label="姓名" prop="name">
+            <el-input v-model="queryParams.name" placeholder="请输入姓名" clearable @keyup.enter="handleQuery" />
+          </el-form-item>
+          <el-form-item label="手机号" prop="phone">
+            <el-input v-model="queryParams.phone" placeholder="请输入手机号" clearable @keyup.enter="handleQuery" />
+          </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>
+      </div>
+    </transition>
+
+    <el-card shadow="never">
+      <template #header>
+        <el-row :gutter="10" class="mb8">
+          <el-col :span="1.5">
+            <el-button v-hasPermi="['employee:employee:add']" type="primary" icon="Plus"
+              @click="handleAdd">新增</el-button>
+          </el-col>
+          <right-toolbar v-model:show-search="showSearch" @query-table="getList"></right-toolbar>
+        </el-row>
+      </template>
+
+      <el-table v-loading="loading" :data="employeeList" border @selection-change="handleSelectionChange">
+        <el-table-column type="selection" width="55" align="center" />
+        <el-table-column label="序号" align="center" width="70" type="index" />
+        <el-table-column label="姓名" align="center" prop="name" min-width="120" />
+        <el-table-column label="头像" align="center" width="80">
+          <template #default="scope">
+            <el-avatar :size="36" :src="scope.row.avatarUrl" icon="UserFilled" />
+          </template>
+        </el-table-column>
+        <el-table-column label="手机" align="center" prop="phone" width="140" />
+        <el-table-column label="注册时间" align="center" prop="createTime" width="170" />
+        <el-table-column label="状态" align="center" width="100">
+          <template #default="scope">
+            <el-switch v-if="checkPermi(['employee:employee:changeStatus'])" v-model="scope.row._statusActive"
+              :loading="scope.row._statusLoading" inline-prompt @change="handleStatusChange(scope.row)" />
+            <el-tag v-else :type="scope.row.status === '0' ? 'danger' : 'success'">
+              {{ scope.row.status === '0' ? '禁用' : '启用' }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="160">
+          <template #default="scope">
+            <el-button v-hasPermi="['employee:employee:auth']" link type="success" icon="User"
+              @click="handleAuth(scope.row)">授权</el-button>
+            <el-button v-hasPermi="['employee:employee:query']" link type="primary" icon="View"
+              @click="handleDetail(scope.row)">详情</el-button>
+          </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>
+
+    <!-- 授权客户对话框 -->
+    <SelectClientDialog v-model="authDialog.visible" v-model:selected-clients="selectedAuthClients" title="授权客户"
+      selected-label="已授权" placeholder-text="输入客户名称搜索并添加授权客户" confirm-text="确认授权" :search-results="authSearchResults"
+      :search-loading="authSearchLoading" :confirm-loading="buttonLoading" :searched="authSearched"
+      @search="handleAuthSearch" @confirm="confirmAuth" />
+
+    <!-- 新增员工对话框 -->
+    <el-dialog v-model="addDialog.visible" title="新增员工" width="520px" append-to-body @close="resetAddForm"
+      class="add-employee-dialog">
+      <div class="add-form-wrapper">
+        <!-- 头像区 -->
+        <div class="add-avatar-area">
+          <el-upload class="add-avatar-uploader" :action="uploadUrl" :headers="uploadHeaders" :show-file-list="false"
+            :on-success="handleAddAvatarSuccess" :before-upload="beforeAddAvatarUpload">
+            <div class="add-avatar-mask">
+              <img v-if="addForm.avatarUrl" :src="addForm.avatarUrl" class="add-avatar-img" />
+              <el-icon v-else class="add-avatar-placeholder">
+                <UserFilled />
+              </el-icon>
+              <div class="add-avatar-overlay">
+                <el-icon>
+                  <Camera />
+                </el-icon>
+                <span>更换头像</span>
+              </div>
+            </div>
+          </el-upload>
+          <p class="add-avatar-tip">点击上传员工头像</p>
+        </div>
+
+        <el-form ref="addFormRef" :model="addForm" :rules="addFormRules" label-position="top" class="add-form">
+          <el-form-item label="姓名" prop="name">
+            <el-input v-model="addForm.name" placeholder="请输入员工姓名" clearable maxlength="30" size="large" />
+          </el-form-item>
+          <el-form-item label="电话(登录账号)" prop="phone">
+            <el-input v-model="addForm.phone" placeholder="请输入手机号作为登录账号" clearable maxlength="11" size="large" />
+          </el-form-item>
+          <el-form-item label="授权客户">
+            <div class="add-auth-wrap">
+              <div v-if="addSelectedClients.length > 0" class="add-auth-tags">
+                <el-tag v-for="(client, idx) in addSelectedClients" :key="idx" closable size="large"
+                  @close="removeAddClient(idx)" class="add-auth-tag">
+                  <span class="add-auth-tag-index">{{ idx + 1 }}</span>
+                  {{ client.name }}
+                </el-tag>
+              </div>
+              <el-button class="add-auth-btn" @click="openAddSelectClient">
+                <el-icon>
+                  <Plus />
+                </el-icon>
+                {{ addSelectedClients.length > 0 ? '继续添加' : '选择客户' }}
+              </el-button>
+            </div>
+          </el-form-item>
+        </el-form>
+      </div>
+      <template #footer>
+        <div class="add-dialog-footer">
+          <el-button size="large" @click="addDialog.visible = false">取 消</el-button>
+          <el-button size="large" type="primary" :loading="addBtnLoading" @click="confirmAdd">确认新增</el-button>
+        </div>
+      </template>
+    </el-dialog>
+
+    <!-- 新增员工-选择授权客户子对话框 -->
+    <SelectClientDialog v-model="selectClientVisible" v-model:selected-clients="addSelectedClients"
+      :search-results="selectClientResults" :search-loading="selectClientLoading" :searched="selectClientSearched"
+      @search="handleSelectClientSearch" @confirm="selectClientVisible = false" />
+
+    <!-- 员工详情对话框 -->
+    <el-dialog v-model="detailDialog.visible" title="员工详情" width="680px" append-to-body>
+      <template v-if="detailDialog.data">
+        <div class="detail-header">
+          <el-avatar :size="72" :src="detailDialog.data.avatarUrl" icon="UserFilled" class="detail-avatar" />
+          <div class="detail-name-box">
+            <text class="detail-name">{{ detailDialog.data.name }}</text>
+            <el-tag :type="detailDialog.data.status === '0' ? 'danger' : 'success'" size="small"
+              class="detail-status-tag">
+              {{ detailDialog.data.status === '0' ? '已禁用' : '已启用' }}
+            </el-tag>
+          </div>
+        </div>
+        <div class="detail-divider"></div>
+        <el-descriptions :column="2" border size="small" class="detail-descriptions">
+          <el-descriptions-item label="员工ID">{{ detailDialog.data.id }}</el-descriptions-item>
+          <el-descriptions-item label="手机号">{{ detailDialog.data.phone || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="注册时间">{{ detailDialog.data.createTime }}</el-descriptions-item>
+        </el-descriptions>
+
+        <div class="section-title">授权客户</div>
+        <div v-if="detailDialog.data.authClientList && detailDialog.data.authClientList.length > 0">
+          <div class="auth-client-card" v-for="(client, idx) in detailDialog.data.authClientList" :key="idx">
+            <span class="client-index">{{ idx + 1 }}</span>
+            <div class="client-info">
+              <div class="client-row"><span class="client-label">名称</span><span class="client-value">{{ client.name
+              }}</span></div>
+              <div class="client-row"><span class="client-label">类型</span><span class="client-value">{{
+                client.clientClass || '-' }}</span></div>
+              <div class="client-row"><span class="client-label">加入时间</span><span class="client-value">{{
+                client.enterDate || '-' }}</span></div>
+            </div>
+          </div>
+        </div>
+        <el-empty v-else description="暂无授权客户" :image-size="60" />
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup name="Customer" lang="ts">
+import { listEmployee, addEmployee, getEmployeeDetail, authEmployee, changeEmployeeStatus } from '@/api/system/employee';
+import { EmployeeVO, EmployeeQuery, EmployeeForm, ErpClientBriefVO } from '@/api/system/employee/types';
+import { searchErpClient, getErpClientByIds } from '@/api/erp/client';
+import { ErpClientVO } from '@/api/erp/client/types';
+import { checkPermi } from '@/utils/permission';
+import { globalHeaders } from '@/utils/request';
+import SelectClientDialog from './components/SelectClientDialog.vue';
+
+/** @Author: Antigravity */
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+
+const employeeList = ref<EmployeeVO[]>([]);
+const buttonLoading = ref(false);
+const loading = ref(true);
+const showSearch = ref(true);
+const ids = ref<Array<string | number>>([]);
+const total = ref(0);
+
+const queryFormRef = ref<ElFormInstance>();
+
+// 授权客户对话框相关
+const authDialog = reactive({
+  visible: false,
+  employeeId: undefined as string | number | undefined
+});
+const authSearchLoading = ref(false);
+const authSearchResults = ref<ErpClientVO[]>([]);
+const authSearched = ref(false);
+const selectedAuthClients = ref<ErpClientVO[]>([]);
+
+// 详情对话框
+const detailDialog = reactive({
+  visible: false,
+  data: null as EmployeeVO | null
+});
+
+// 新增员工对话框
+const addDialog = reactive({
+  visible: false
+});
+const addFormRef = ref<ElFormInstance>();
+const addForm = reactive({
+  name: '',
+  phone: '',
+  avatar: undefined as string | undefined,
+  avatarUrl: ''
+});
+const addFormRules = reactive({
+  name: [{ required: true, message: '姓名不能为空', trigger: 'blur' }],
+  phone: [{ required: true, message: '手机号不能为空', trigger: 'blur' }]
+});
+const addBtnLoading = ref(false);
+const addSelectedClients = ref<ErpClientVO[]>([]);
+
+// 新增员工-选择授权客户子对话框
+const selectClientVisible = ref(false);
+const selectClientLoading = ref(false);
+const selectClientResults = ref<ErpClientVO[]>([]);
+const selectClientSearched = ref(false);
+
+const uploadUrl = import.meta.env.VITE_APP_BASE_API + '/resource/oss/upload';
+const uploadHeaders = ref(globalHeaders());
+
+watch(() => authDialog.visible, (val) => {
+  if (val) initAuthDialog();
+});
+
+const data = reactive<PageData<EmployeeForm, EmployeeQuery>>({
+  form: {} as any,
+  queryParams: {
+    pageNum: 1,
+    pageSize: 10,
+    name: undefined,
+    phone: undefined,
+    wechatOpenid: undefined
+  },
+  rules: {}
+});
+
+const { queryParams } = toRefs(data);
+
+/**
+ * 处理列表数据,附加状态展示字段
+ */
+const processList = (list: EmployeeVO[]) => {
+  list.forEach(item => {
+    (item as any)._statusActive = item.status !== '0';
+    (item as any)._statusLoading = false;
+  });
+};
+
+/** 查询员工列表 */
+const getList = async () => {
+  loading.value = true;
+  const res = await listEmployee(queryParams.value);
+  const rows = res.rows as EmployeeVO[] || [];
+  processList(rows);
+  employeeList.value = rows;
+  total.value = res.total;
+  loading.value = false;
+};
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.value.pageNum = 1;
+  getList();
+};
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value?.resetFields();
+  handleQuery();
+};
+
+/** 多选框选中数据 */
+const handleSelectionChange = (selection: EmployeeVO[]) => {
+  ids.value = selection.map((item) => item.id);
+};
+
+/** 状态切换 */
+const handleStatusChange = async (row: EmployeeVO & { _statusActive: boolean; _statusLoading: boolean }) => {
+  const newStatus = row._statusActive ? '1' : '0';
+  try {
+    row._statusLoading = true;
+    await changeEmployeeStatus(row.id, newStatus);
+    proxy?.$modal.msgSuccess(newStatus === '1' ? '已启用' : '已禁用');
+  } catch (e: any) {
+    // 失败回滚状态
+    row._statusActive = !row._statusActive;
+    proxy?.$modal.msgError(e?.msg || '操作失败');
+  } finally {
+    row._statusLoading = false;
+  }
+};
+
+/** 授权员工按钮操作 */
+const handleAuth = (row: EmployeeVO) => {
+  authDialog.employeeId = row.id;
+  authDialog.visible = true;
+};
+
+/** 弹窗打开时初始化 */
+const initAuthDialog = async () => {
+  authSearchResults.value = [];
+  authSearched.value = false;
+  selectedAuthClients.value = [];
+  if (!authDialog.employeeId) return;
+  try {
+    const res = await getEmployeeDetail(authDialog.employeeId);
+    const ids = res.data?.authClientFRowIDs;
+    if (ids) {
+      selectedAuthClients.value = await fetchClientsByIds(ids);
+    }
+  } catch (e) {
+    console.error('加载已授权客户失败', e);
+  }
+};
+
+/** 搜索ERP客户 */
+const handleAuthSearch = async (keyword: string) => {
+  if (!keyword) {
+    proxy?.$modal.msgWarning('请输入客户名称');
+    return;
+  }
+  authSearchLoading.value = true;
+  authSearched.value = true;
+  try {
+    const res = await searchErpClient(keyword);
+    authSearchResults.value = res.data || [];
+  } catch (e) {
+    authSearchResults.value = [];
+  } finally {
+    authSearchLoading.value = false;
+  }
+};
+
+/** 确认授权 */
+const confirmAuth = async () => {
+  if (!selectedAuthClients.value || selectedAuthClients.value.length === 0) {
+    proxy?.$modal.msgError('请至少选择一个ERP客户');
+    return;
+  }
+  buttonLoading.value = true;
+  try {
+    const authClientFRowIDs = selectedAuthClients.value.map(c => c.rowId).join(',');
+    await authEmployee(authDialog.employeeId!, authClientFRowIDs);
+    proxy?.$modal.msgSuccess('授权成功');
+    authDialog.visible = false;
+    getList();
+  } finally {
+    buttonLoading.value = false;
+  }
+};
+
+/** 查看员工详情 */
+const handleDetail = async (row: EmployeeVO) => {
+  detailDialog.visible = true;
+  detailDialog.data = null;
+  try {
+    const res = await getEmployeeDetail(row.id);
+    const data = res.data;
+    if (data && data.authClientFRowIDs) {
+      data.authClientList = await fetchClientsByIds(data.authClientFRowIDs) as any;
+    }
+    detailDialog.data = data;
+  } catch (e) {
+    proxy?.$modal.msgError('获取员工详情失败');
+    detailDialog.visible = false;
+  }
+};
+
+/** 新增按钮操作 */
+const handleAdd = () => {
+  resetAddForm();
+  addDialog.visible = true;
+};
+
+/** 重置新增表单 */
+const resetAddForm = () => {
+  addForm.name = '';
+  addForm.phone = '';
+  addForm.avatar = undefined;
+  addForm.avatarUrl = '';
+  addSelectedClients.value = [];
+  addFormRef.value?.resetFields();
+};
+
+/** 打开选择授权客户子对话框 */
+const openAddSelectClient = () => {
+  selectClientVisible.value = true;
+};
+
+/** 搜索客户 */
+const handleSelectClientSearch = async (keyword: string) => {
+  if (!keyword) {
+    proxy?.$modal.msgWarning('请输入客户名称');
+    return;
+  }
+  selectClientLoading.value = true;
+  selectClientSearched.value = true;
+  try {
+    const res = await searchErpClient(keyword);
+    selectClientResults.value = res.data || [];
+  } catch (e) {
+    selectClientResults.value = [];
+  } finally {
+    selectClientLoading.value = false;
+  }
+};
+
+/** 头像上传成功回调 */
+const handleAddAvatarSuccess = (res: any) => {
+  if (res.code === 200) {
+    addForm.avatar = res.data.ossId;
+    addForm.avatarUrl = res.data.url;
+  } else {
+    proxy?.$modal.msgError(res.msg || '上传失败');
+  }
+};
+
+/** 头像上传前校验 */
+const beforeAddAvatarUpload = (file: any) => {
+  const isImg = file.type.indexOf('image/') > -1;
+  if (!isImg) {
+    proxy?.$modal.msgError('请上传图片格式文件');
+    return false;
+  }
+  const isLt5M = file.size / 1024 / 1024 < 5;
+  if (!isLt5M) {
+    proxy?.$modal.msgError('上传头像图片大小不能超过 5MB!');
+    return false;
+  }
+  return true;
+};
+
+/** 确认新增员工 */
+const confirmAdd = async () => {
+  const valid = await addFormRef.value?.validate().catch(() => false);
+  if (!valid) return;
+  addBtnLoading.value = true;
+  try {
+    const data: EmployeeForm = {
+      name: addForm.name,
+      phone: addForm.phone,
+      password: '123456',
+      authClientFRowIDs: addSelectedClients.value.map(c => c.rowId).join(',')
+    };
+    if (addForm.avatar) {
+      data.avatar = addForm.avatar;
+    }
+    await addEmployee(data);
+    proxy?.$modal.msgSuccess('新增成功');
+    addDialog.visible = false;
+    getList();
+  } finally {
+    addBtnLoading.value = false;
+  }
+};
+
+/**
+ * 根据 authClientFRowIDs 逗号分隔字符串,调用 ERP 批量接口反查客户列表
+ */
+const fetchClientsByIds = async (authClientFRowIDs: string): Promise<ErpClientVO[]> => {
+  const ids = authClientFRowIDs.split(',').map(s => s.trim()).filter(Boolean);
+  if (ids.length === 0) return [];
+  try {
+    const res = await getErpClientByIds(ids.join(','));
+    return (res.data || []).map(c => ({
+      ...c,
+      num: (c as any).num || c.rowId
+    }));
+  } catch (e) {
+    console.error('批量查询ERP客户失败', e);
+    return [];
+  }
+};
+
+onMounted(() => {
+  getList();
+});
+</script>
+
+<style scoped>
+/* ========== 详情弹窗 ========== */
+.section-title {
+  font-size: 14px;
+  font-weight: 600;
+  color: #303133;
+  margin: 20px 0 12px 0;
+  padding-left: 10px;
+  border-left: 3px solid #C1001C;
+}
+
+:deep(.el-dialog .pagination-container) {
+  display: flex !important;
+  justify-content: center !important;
+  background-color: transparent !important;
+  padding: 20px 0 10px 0 !important;
+  margin-top: 0 !important;
+}
+
+.detail-header {
+  display: flex;
+  align-items: center;
+  gap: 24px;
+  padding-bottom: 20px;
+}
+
+.detail-avatar {
+  flex-shrink: 0;
+  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
+}
+
+.detail-name-box {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+}
+
+.detail-name {
+  font-size: 22px;
+  font-weight: 600;
+  color: #1d1d1f;
+  letter-spacing: 0.5px;
+}
+
+.detail-status-tag {
+  flex-shrink: 0;
+}
+
+.detail-divider {
+  height: 1px;
+  background: linear-gradient(90deg, #eee 0%, transparent 100%);
+  margin-bottom: 20px;
+}
+
+.detail-descriptions {
+  margin-bottom: 8px;
+}
+
+.auth-client-card {
+  display: flex;
+  align-items: flex-start;
+  gap: 16px;
+  background: #fafafa;
+  border-radius: 12px;
+  padding: 16px 20px;
+  margin-bottom: 12px;
+  border: 1px solid #f0f0f0;
+  transition: box-shadow 0.2s;
+}
+
+.auth-client-card:hover {
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
+}
+
+.client-index {
+  width: 28px;
+  height: 28px;
+  background: #C1001C;
+  color: #fff;
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 13px;
+  font-weight: 600;
+  flex-shrink: 0;
+  margin-top: 2px;
+}
+
+.client-info {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  gap: 6px;
+}
+
+.client-row {
+  display: flex;
+  gap: 12px;
+  font-size: 13px;
+}
+
+.client-label {
+  color: #999;
+  min-width: 60px;
+  flex-shrink: 0;
+}
+
+.client-value {
+  color: #333;
+  font-weight: 500;
+}
+
+/* ========== 新增员工对话框 ========== */
+.add-employee-dialog :deep(.el-dialog__header) {
+  padding: 24px 28px 0;
+  margin: 0;
+}
+
+.add-employee-dialog :deep(.el-dialog__title) {
+  font-size: 18px;
+  font-weight: 600;
+  color: #1a1a1a;
+  letter-spacing: 0.3px;
+}
+
+.add-employee-dialog :deep(.el-dialog__body) {
+  padding: 12px 28px 20px;
+}
+
+.add-form-wrapper {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+
+.add-avatar-area {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  margin-bottom: 28px;
+}
+
+.add-avatar-uploader {
+  cursor: pointer;
+}
+
+.add-avatar-mask {
+  position: relative;
+  width: 88px;
+  height: 88px;
+  border-radius: 50%;
+  overflow: hidden;
+  background: #f5f7fa;
+  border: 2px solid #e8ecf1;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  transition: border-color 0.3s, box-shadow 0.3s;
+}
+
+.add-avatar-mask:hover {
+  border-color: #409eff;
+  box-shadow: 0 0 0 4px rgba(64, 158, 255, 0.1);
+}
+
+.add-avatar-mask:hover .add-avatar-overlay {
+  opacity: 1;
+}
+
+.add-avatar-img {
+  width: 100%;
+  height: 100%;
+  object-fit: cover;
+}
+
+.add-avatar-placeholder {
+  font-size: 36px;
+  color: #c0c4cc;
+}
+
+.add-avatar-overlay {
+  position: absolute;
+  inset: 0;
+  background: rgba(0, 0, 0, 0.45);
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  gap: 2px;
+  opacity: 0;
+  transition: opacity 0.3s;
+  color: #fff;
+  font-size: 11px;
+}
+
+.add-avatar-overlay .el-icon {
+  font-size: 18px;
+}
+
+.add-avatar-tip {
+  margin: 10px 0 0;
+  font-size: 12px;
+  color: #b0b5be;
+  letter-spacing: 0.2px;
+}
+
+.add-form {
+  width: 100%;
+}
+
+.add-form :deep(.el-form-item__label) {
+  font-size: 13px;
+  font-weight: 500;
+  color: #4a4f5a;
+  padding-bottom: 6px;
+}
+
+.add-form :deep(.el-input--large .el-input__wrapper) {
+  border-radius: 8px;
+  box-shadow: 0 0 0 1px #e4e7ed inset;
+  transition: box-shadow 0.25s;
+}
+
+.add-form :deep(.el-input--large .el-input__wrapper:hover) {
+  box-shadow: 0 0 0 1px #c6cacf inset;
+}
+
+.add-form :deep(.el-input--large.is-focus .el-input__wrapper) {
+  box-shadow: 0 0 0 1px #409eff inset, 0 0 0 3px rgba(64, 158, 255, 0.08);
+}
+
+.add-auth-wrap {
+  display: flex;
+  flex-wrap: wrap;
+  align-items: center;
+  gap: 8px;
+}
+
+.add-auth-tags {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 6px;
+}
+
+.add-auth-tag {
+  font-size: 13px;
+  border-radius: 6px;
+  padding: 6px 12px;
+  border: none;
+  background: #f0f5ff;
+  color: #3370ff;
+}
+
+.add-auth-tag :deep(.el-tag__close) {
+  color: #8fa8e0;
+}
+
+.add-auth-tag :deep(.el-tag__close:hover) {
+  background: rgba(51, 112, 255, 0.12);
+  color: #3370ff;
+}
+
+.add-auth-tag-index {
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  width: 18px;
+  height: 18px;
+  border-radius: 50%;
+  background: #3370ff;
+  color: #fff;
+  font-size: 11px;
+  font-weight: 600;
+  margin-right: 6px;
+}
+
+.add-auth-btn {
+  flex-shrink: 0;
+  border-radius: 6px;
+  border: 1px dashed #c0c4cc;
+  background: #fff;
+  color: #606266;
+  font-size: 13px;
+  padding: 8px 16px;
+  transition: border-color 0.25s, color 0.25s, background 0.25s;
+}
+
+.add-auth-btn:hover {
+  border-color: #409eff;
+  color: #409eff;
+  background: #f0f5ff;
+}
+
+.add-dialog-footer {
+  display: flex;
+  justify-content: flex-end;
+  gap: 12px;
+  padding-top: 4px;
+}
+
+.add-dialog-footer .el-button {
+  border-radius: 8px;
+  padding: 10px 28px;
+  font-size: 14px;
+  font-weight: 500;
+  letter-spacing: 0.5px;
+}
+
+.add-dialog-footer .el-button--primary {
+  box-shadow: 0 2px 8px rgba(64, 158, 255, 0.25);
+}
+
+.add-dialog-footer .el-button--primary:hover {
+  box-shadow: 0 4px 16px rgba(64, 158, 255, 0.35);
+}
+</style>