Ver código fonte

整改完成

Huanyi 1 semana atrás
pai
commit
c90a65b081

+ 1 - 1
.env.production

@@ -15,7 +15,7 @@ VITE_APP_MONITOR_ADMIN = '/admin/applications'
 VITE_APP_SNAILJOB_ADMIN = '/snail-job'
 
 # 生产环境
-VITE_APP_BASE_API = 'http://192.168.1.78/api'
+VITE_APP_BASE_API = 'https://app.jxhsal.com/api'
 
 # 是否在打包时开启压缩,支持 gzip 和 brotli
 VITE_BUILD_COMPRESS = gzip

BIN
dist.zip


+ 26 - 1
src/api/system/complaint/index.ts

@@ -1,9 +1,10 @@
 import request from '@/utils/request';
 import { AxiosPromise } from 'axios';
-import { ComplaintVO, ComplaintQuery } from '@/api/system/complaint/types';
+import { ComplaintVO, ComplaintQuery, ComplaintDealForm } from '@/api/system/complaint/types';
 
 /**
  * 查询投诉建议列表
+ * @Author: Antigravity
  */
 export const listComplaint = (query?: ComplaintQuery): AxiosPromise<ComplaintVO[]> => {
   return request({
@@ -13,8 +14,32 @@ export const listComplaint = (query?: ComplaintQuery): AxiosPromise<ComplaintVO[
   });
 };
 
+/**
+ * 查询投诉建议详情
+ * @Author: Antigravity
+ */
+export const getComplaintDetail = (id: string | number): AxiosPromise<ComplaintVO> => {
+  return request({
+    url: '/system/complaint/detail/' + id,
+    method: 'get'
+  });
+};
+
+/**
+ * 处理投诉建议
+ * @Author: Antigravity
+ */
+export const dealComplaint = (data: ComplaintDealForm) => {
+  return request({
+    url: '/system/complaint/deal',
+    method: 'put',
+    data: data
+  });
+};
+
 /**
  * 删除投诉建议
+ * @Author: Antigravity
  */
 export const delComplaint = (id: string | number | Array<string | number>) => {
   return request({

+ 15 - 0
src/api/system/complaint/types.ts

@@ -6,6 +6,11 @@ export interface ComplaintVO {
   imageUrls: string;
   customerId: string | number;
   customerNickname: string;
+  customerPhone: string;
+  status: string;
+  dealResult: string;
+  dealImages: string;
+  dealImageUrls: string;
   createTime: string;
 }
 
@@ -13,3 +18,13 @@ export interface ComplaintQuery extends PageQuery {
   feedbackType?: string;
   customerName?: string;
 }
+
+/**
+ * 处理投诉表单
+ * @Author: Antigravity
+ */
+export interface ComplaintDealForm {
+  id: string | number;
+  dealResult: string;
+  dealImages: string;
+}

+ 27 - 4
src/api/system/customer/index.ts

@@ -25,6 +25,18 @@ export const getCustomer = (id: string | number): AxiosPromise<CustomerVO> => {
   });
 };
 
+/**
+ * 查询客户详情(含授权客户完整信息列表)
+ * @param id
+ * @Author: Antigravity
+ */
+export const getCustomerDetail = (id: string | number): AxiosPromise<CustomerVO> => {
+  return request({
+    url: '/system/customer/detail/' + id,
+    method: 'get'
+  });
+};
+
 /**
  * 新增客户
  * @param data
@@ -38,19 +50,30 @@ export const addCustomer = (data: CustomerForm) => {
 };
 
 /**
- * 授权客户
+ * 授权客户(支持多个,逗号分隔)
  * @param id 客户ID
- * @param authClientFRowID ERP客户 RowID
+ * @param authClientFRowIDs 授权客户 RowID 列表(逗号分隔)
  * @Author: Antigravity
  */
-export const authCustomer = (id: string | number, authClientFRowID: string) => {
+export const authCustomer = (id: string | number, authClientFRowIDs: string) => {
   return request({
     url: '/system/customer/auth',
     method: 'put',
-    params: { id, authClientFRowID }
+    params: { id, authClientFRowIDs }
   });
 };
 
+/**
+ * 修改客户状态(启用/禁用)
+ * @Author: Antigravity
+ */
+export const changeCustomerStatus = (id: string | number, status: string) => {
+  return request({
+    url: '/system/customer/changeStatus',
+    method: 'put',
+    params: { id, status }
+  });
+};
 
 /**
  * 导出客户列表

+ 38 - 4
src/api/system/customer/types.ts

@@ -1,3 +1,22 @@
+export interface ErpClientBriefVO {
+  /**
+   * ERP客户 RowID
+   */
+  rowId: string;
+  /**
+   * 客户名称
+   */
+  name: string;
+  /**
+   * 客户类型
+   */
+  clientClass: string;
+  /**
+   * 加入时间
+   */
+  enterDate: string;
+}
+
 export interface CustomerVO {
   /**
    * 客户ID
@@ -25,30 +44,45 @@ export interface CustomerVO {
   wechatUnionid: string;
 
   /**
-   * 授权客户 RowID
+   * 授权客户 RowID(多个逗号分隔)
    */
   authClientFRowID: string;
 
   /**
-   * 授权客户名称
+   * 授权客户名称(多个 / 分隔)
    */
   authClientName?: string;
 
   /**
-   * 授权客户类型
+   * 授权客户类型(多个 / 分隔)
    */
   authClientClass?: string;
 
   /**
-   * 授权加入时间
+   * 授权加入时间(多个 / 分隔)
    */
   authClientEnterDate?: string;
 
+  /**
+   * 授权客户详情列表(仅详情接口返回)
+   */
+  authClientList?: ErpClientBriefVO[];
+
   /**
    * 头像地址
    */
   avatar: string;
 
+  /**
+   * 头像URL
+   */
+  avatarUrl?: string;
+
+  /**
+   * 账号状态(0-禁用 1-启用)
+   */
+  status: string;
+
   /**
    * 创建时间
    */

+ 358 - 93
src/views/complaint/index.vue

@@ -1,76 +1,165 @@
+<!-- @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="feedbackType">
-                        <el-select v-model="queryParams.feedbackType" placeholder="请选择反馈类型" clearable>
-                            <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>
-                    <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 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="feedbackType">
+            <el-select v-model="queryParams.feedbackType" placeholder="请选择反馈类型" clearable>
+              <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>
+          <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="['system:complaint:remove']" type="danger" plain icon="Delete" :disabled="multiple"
+              @click="handleDelete()">删除</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="complaintList" 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="feedbackType" width="100">
+          <template #default="scope">
+            <dict-tag :options="sys_complaint_type" :value="scope.row.feedbackType" />
+          </template>
+        </el-table-column>
+        <el-table-column label="投诉内容" align="center" prop="content" min-width="160" :show-overflow-tooltip="true" />
+        <el-table-column label="图片" align="center" width="130">
+          <template #default="scope">
+            <div v-if="scope.row.imageUrls" class="image-cell">
+              <el-image v-for="(url, idx) in getShowImages(scope.row.imageUrls)" :key="idx" :src="url"
+                :preview-src-list="scope.row.imageUrls.split(',')" :initial-index="idx" fit="cover"
+                style="width:36px;height:36px;border-radius:4px" preview-teleported />
+              <span v-if="getImageCount(scope.row.imageUrls) > 2" class="image-ellipsis">…</span>
+            </div>
+            <span v-else>-</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="状态" align="center" width="90">
+          <template #default="scope">
+            <el-tag :type="scope.row.status === '0' ? 'warning' : 'success'">
+              {{ scope.row.status === '0' ? '待处理' : '已处理' }}
+            </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="createTime" width="170" />
+        <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="200">
+          <template #default="scope">
+            <div class="action-btns">
+              <el-button v-hasPermi="['complaint:complaint:query']" link type="primary" icon="View"
+                @click="handleDetail(scope.row)">详情</el-button>
+              <el-button v-if="scope.row.status !== '1'" v-hasPermi="['complaint:complaint:deal']" link type="warning"
+                icon="Finished" @click="handleDeal(scope.row)">处理</el-button>
+              <el-button v-hasPermi="['system:complaint:remove']" link type="danger" icon="Delete"
+                @click="handleDelete(scope.row)">删除</el-button>
+            </div>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <pagination v-show="total > 0" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize"
+        :total="total" @pagination="getList" />
+    </el-card>
+
+    <!-- 投诉详情对话框 -->
+    <el-dialog v-model="detailDialog.visible" title="投诉详情" width="680px" append-to-body>
+      <template v-if="detailDialog.data">
+        <div class="detail-header">
+          <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>
-        </transition>
-
-        <el-card shadow="never">
-            <template #header>
-                <el-row :gutter="10" class="mb8">
-                    <el-col :span="1.5">
-                        <el-button v-hasPermi="['system:complaint:remove']" type="danger" plain icon="Delete"
-                            :disabled="multiple" @click="handleDelete()">删除</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="complaintList" border @selection-change="handleSelectionChange">
-                <el-table-column type="selection" width="55" align="center" />
-                <el-table-column label="反馈类型" align="center" prop="feedbackType" width="120">
-                    <template #default="scope">
-                        <dict-tag :options="sys_complaint_type" :value="scope.row.feedbackType" />
-                    </template>
-                </el-table-column>
-                <el-table-column label="反馈内容" align="center" prop="content" :show-overflow-tooltip="true" />
-                <el-table-column label="图片" align="center" width="160">
-                    <template #default="scope">
-                        <div v-if="scope.row.imageUrls"
-                            style="display:flex;gap:4px;justify-content:center;flex-wrap:wrap">
-                            <el-image v-for="(url, idx) in scope.row.imageUrls.split(',')" :key="idx" :src="url"
-                                :preview-src-list="scope.row.imageUrls.split(',')" :initial-index="(idx as number)"
-                                fit="cover" style="width:40px;height:40px;border-radius:4px" preview-teleported />
-                        </div>
-                        <span v-else>-</span>
-                    </template>
-                </el-table-column>
-                <el-table-column label="客户昵称" align="center" prop="customerNickname" width="120" />
-                <el-table-column label="提交时间" align="center" prop="createTime" width="160" />
-                <el-table-column label="操作" align="center" width="80">
-                    <template #default="scope">
-                        <el-button v-hasPermi="['system:complaint:remove']" link type="danger" icon="Delete"
-                            @click="handleDelete(scope.row)">删除</el-button>
-                    </template>
-                </el-table-column>
-            </el-table>
-
-            <pagination v-show="total > 0" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize"
-                :total="total" @pagination="getList" />
-        </el-card>
-    </div>
+          </div>
+          <el-tag :type="detailDialog.data.status === '0' ? 'warning' : 'success'" size="small">
+            {{ detailDialog.data.status === '0' ? '待处理' : '已处理' }}
+          </el-tag>
+        </div>
+        <div class="detail-divider"></div>
+        <el-descriptions :column="2" border size="small" class="detail-descriptions">
+          <el-descriptions-item label="类型">
+            <dict-tag :options="sys_complaint_type" :value="detailDialog.data.feedbackType" />
+          </el-descriptions-item>
+          <el-descriptions-item label="投诉ID">{{ detailDialog.data.id }}</el-descriptions-item>
+          <el-descriptions-item label="投诉内容" :span="2">{{ detailDialog.data.content }}</el-descriptions-item>
+          <el-descriptions-item label="投诉图片" :span="2">
+            <div v-if="detailDialog.data.imageUrls" class="descriptions-images">
+              <el-image v-for="(url, idx) in detailDialog.data.imageUrls.split(',')" :key="idx" :src="url"
+                :preview-src-list="detailDialog.data.imageUrls.split(',')" :initial-index="(idx as number)" fit="cover"
+                style="width:60px;height:60px;border-radius:4px;margin-right:6px" preview-teleported />
+            </div>
+            <span v-else>-</span>
+          </el-descriptions-item>
+          <el-descriptions-item label="提交时间">{{ detailDialog.data.createTime }}</el-descriptions-item>
+        </el-descriptions>
+
+        <!-- 已处理时展示处理结果和凭证 -->
+        <template v-if="detailDialog.data.status === '1'">
+          <div class="section-title">处理结果</div>
+          <el-descriptions :column="1" border size="small">
+            <el-descriptions-item label="处理结果">{{ detailDialog.data.dealResult }}</el-descriptions-item>
+            <el-descriptions-item label="处理凭证">
+              <div v-if="detailDialog.data.dealImageUrls" class="descriptions-images">
+                <el-image v-for="(url, idx) in detailDialog.data.dealImageUrls.split(',')" :key="idx" :src="url"
+                  :preview-src-list="detailDialog.data.dealImageUrls.split(',')" :initial-index="(idx as number)"
+                  fit="cover" style="width:60px;height:60px;border-radius:4px;margin-right:6px" preview-teleported />
+              </div>
+              <span v-else>-</span>
+            </el-descriptions-item>
+          </el-descriptions>
+        </template>
+      </template>
+    </el-dialog>
+
+    <!-- 处理投诉对话框 -->
+    <el-dialog v-model="dealDialog.visible" title="处理投诉" width="600px" append-to-body @close="resetDealForm">
+      <el-form ref="dealFormRef" :model="dealForm" :rules="dealRules" label-width="80px">
+        <el-form-item label="投诉内容">
+          <div class="deal-content-text">{{ dealDialog.complaintContent }}</div>
+        </el-form-item>
+        <el-form-item label="处理结果" prop="dealResult">
+          <el-input v-model="dealForm.dealResult" type="textarea" :rows="4" placeholder="请输入处理结果" maxlength="2000"
+            show-word-limit />
+        </el-form-item>
+        <el-form-item label="上传凭证">
+          <ImageUpload v-model="dealForm.dealImages" :limit="9" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button :loading="dealLoading" type="primary" icon="Check" @click="submitDeal">确 定</el-button>
+          <el-button icon="Close" @click="dealDialog.visible = false">取 消</el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
 </template>
 
 <script setup name="Complaint" lang="ts">
-import { listComplaint, delComplaint } from '@/api/system/complaint';
-import { ComplaintVO, ComplaintQuery } from '@/api/system/complaint/types';
+import { listComplaint, getComplaintDetail, dealComplaint, delComplaint } from '@/api/system/complaint';
+import { ComplaintVO, ComplaintQuery, ComplaintDealForm } from '@/api/system/complaint/types';
+
+/** @Author: Antigravity */
 
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
 const { sys_complaint_type } = toRefs<any>(proxy?.useDict('sys_complaint_type'));
@@ -85,50 +174,226 @@ const total = ref(0);
 const queryFormRef = ref<ElFormInstance>();
 
 const data = reactive<PageData<object, ComplaintQuery>>({
-    form: {},
-    queryParams: {
-        pageNum: 1,
-        pageSize: 10,
-        feedbackType: undefined,
-        customerName: undefined
-    },
-    rules: {}
+  form: {},
+  queryParams: {
+    pageNum: 1,
+    pageSize: 10,
+    feedbackType: undefined,
+    customerName: undefined
+  },
+  rules: {}
 });
 
 const { queryParams } = toRefs(data);
 
+// 详情对话框
+const detailDialog = reactive({
+  visible: false,
+  data: null as ComplaintVO | null
+});
+
+// 处理对话框
+const dealDialog = reactive({
+  visible: false,
+  complaintId: undefined as string | number | undefined,
+  complaintContent: ''
+});
+const dealLoading = ref(false);
+const dealFormRef = ref<ElFormInstance>();
+const dealForm = reactive<ComplaintDealForm>({
+  id: '',
+  dealResult: '',
+  dealImages: ''
+});
+const dealRules = {
+  dealResult: [{ required: true, message: '请输入处理结果', trigger: 'blur' }]
+};
+
+/**
+ * 获取展示的图片列表(最多2张)
+ */
+const getShowImages = (imageUrls: string) => {
+  const urls = imageUrls.split(',');
+  return urls.slice(0, 2);
+};
+
+/**
+ * 获取图片总数
+ */
+const getImageCount = (imageUrls: string) => {
+  return imageUrls.split(',').length;
+};
+
 const getList = async () => {
-    loading.value = true;
-    const res = await listComplaint(queryParams.value);
-    complaintList.value = res.rows;
-    total.value = res.total;
-    loading.value = false;
+  loading.value = true;
+  const res = await listComplaint(queryParams.value);
+  complaintList.value = (res as any).rows || [];
+  total.value = (res as any).total || 0;
+  loading.value = false;
 };
 
 const handleQuery = () => {
-    queryParams.value.pageNum = 1;
-    getList();
+  queryParams.value.pageNum = 1;
+  getList();
 };
 
 const resetQuery = () => {
-    queryFormRef.value?.resetFields();
-    handleQuery();
+  queryFormRef.value?.resetFields();
+  handleQuery();
 };
 
 const handleSelectionChange = (selection: ComplaintVO[]) => {
-    ids.value = selection.map((item) => item.id);
-    multiple.value = !selection.length;
+  ids.value = selection.map((item) => item.id);
+  multiple.value = !selection.length;
 };
 
+/** 查看详情 */
+const handleDetail = async (row: ComplaintVO) => {
+  detailDialog.visible = true;
+  detailDialog.data = null;
+  try {
+    const res = await getComplaintDetail(row.id);
+    detailDialog.data = res.data as ComplaintVO;
+  } catch (e) {
+    proxy?.$modal.msgError('获取投诉详情失败');
+    detailDialog.visible = false;
+  }
+};
+
+/** 打开处理弹窗 */
+const handleDeal = (row: ComplaintVO) => {
+  dealDialog.visible = true;
+  dealDialog.complaintId = row.id;
+  dealDialog.complaintContent = row.content;
+  dealForm.id = row.id;
+  dealForm.dealResult = '';
+  dealForm.dealImages = '';
+};
+
+/** 重置处理表单 */
+const resetDealForm = () => {
+  dealFormRef.value?.resetFields();
+  dealForm.dealImages = '';
+};
+
+/** 提交处理 */
+const submitDeal = async () => {
+  const valid = await dealFormRef.value?.validate().catch(() => false);
+  if (!valid) return;
+  dealLoading.value = true;
+  try {
+    await dealComplaint({ ...dealForm });
+    proxy?.$modal.msgSuccess('处理成功');
+    dealDialog.visible = false;
+    getList();
+  } catch (e: any) {
+    proxy?.$modal.msgError(e?.msg || '处理失败');
+  } finally {
+    dealLoading.value = false;
+  }
+};
+
+/** 删除 */
 const handleDelete = async (row?: ComplaintVO) => {
-    const _ids = row?.id || ids.value;
-    await proxy?.$modal.confirm('是否确认删除所选投诉建议数据?').finally(() => (loading.value = false));
-    await delComplaint(_ids);
-    proxy?.$modal.msgSuccess('删除成功');
-    await getList();
+  const _ids = row?.id || ids.value;
+  await proxy?.$modal.confirm('是否确认删除所选投诉建议数据?');
+  loading.value = true;
+  await delComplaint(_ids);
+  proxy?.$modal.msgSuccess('删除成功');
+  await getList();
 };
 
 onMounted(() => {
-    getList();
+  getList();
 });
 </script>
+
+<style scoped>
+/* 操作按钮一行显示 */
+.action-btns {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  flex-wrap: nowrap;
+  white-space: nowrap;
+}
+
+/* 列表中图片展示 */
+.image-cell {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 4px;
+}
+
+.image-ellipsis {
+  font-size: 16px;
+  font-weight: 600;
+  color: #999;
+  margin-left: 2px;
+}
+
+/* 详情弹窗样式 */
+.detail-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding-bottom: 20px;
+}
+
+.detail-user-info {
+  display: flex;
+  align-items: center;
+  gap: 14px;
+}
+
+.detail-nickname {
+  font-size: 16px;
+  font-weight: 600;
+  color: #1d1d1f;
+}
+
+.detail-phone {
+  font-size: 13px;
+  color: #999;
+  margin-top: 2px;
+}
+
+.detail-divider {
+  height: 1px;
+  background: linear-gradient(90deg, #eee 0%, transparent 100%);
+  margin-bottom: 20px;
+}
+
+.detail-descriptions {
+  margin-bottom: 8px;
+}
+
+.descriptions-images {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 6px;
+}
+
+/* 处理结果、授权客户 分区标题 */
+.section-title {
+  font-size: 14px;
+  font-weight: 600;
+  color: #303133;
+  margin: 20px 0 12px 0;
+  padding-left: 10px;
+  border-left: 3px solid #e6a23c;
+}
+
+/* 处理弹窗 */
+.deal-content-text {
+  padding: 10px 12px;
+  background: #f5f7fa;
+  border-radius: 6px;
+  color: #606266;
+  font-size: 13px;
+  line-height: 1.6;
+  max-height: 120px;
+  overflow-y: auto;
+}
+</style>

+ 352 - 150
src/views/customer/index.vue

@@ -1,101 +1,150 @@
 <!-- @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 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="900px" append-to-body>
+      <el-form :model="erpQueryParams" ref="erpQueryFormRef" :inline="true" label-width="68px">
+        <el-form-item label="客户名称" prop="name">
+          <el-input v-model="erpQueryParams.name" placeholder="请输入客户名称" clearable style="width: 240px"
+            @keyup.enter="getErpList" />
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" icon="Search" @click="getErpList">搜索</el-button>
+          <el-button icon="Refresh" @click="resetErpQuery">重置</el-button>
+        </el-form-item>
+      </el-form>
+      <el-table v-loading="erpLoading" :data="erpList" @selection-change="handleErpSelectionChange" ref="erpTableRef">
+        <el-table-column type="selection" width="50" align="center" />
+        <el-table-column label="代码" align="center" prop="num" width="100"
+          :formatter="(row: ErpClientVO) => row.num || '-'" />
+        <el-table-column label="名称" align="left" prop="name" min-width="180" :show-overflow-tooltip="true"
+          :formatter="(row: ErpClientVO) => row.name || '-'" />
+        <el-table-column label="客户类型" align="center" prop="clientClass" width="100"
+          :formatter="(row: ErpClientVO) => row.clientClass || '-'" />
+        <el-table-column label="加入名称" align="center" prop="enterName" width="100"
+          :formatter="(row: ErpClientVO) => row.enterName || '-'" />
+        <el-table-column label="加入时间" align="center" prop="enterDate" width="170"
+          :formatter="(row: ErpClientVO) => row.enterDate || '-'" />
+      </el-table>
+      <pagination v-show="erpTotal > 0" v-model:page="erpQueryParams.pageNum" v-model:limit="erpQueryParams.pageSize"
+        :total="erpTotal" layout="total, prev, pager, next" @pagination="getErpList" />
+      <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>
-        </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="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="userName" />
-                <el-table-column label="手机号" align="center" prop="phone" />
-                <el-table-column label="注册时间" align="center" prop="createTime" width="160" />
-                <el-table-column label="授权客户名称" align="center" prop="authClientName" :show-overflow-tooltip="true" />
-                <el-table-column label="授权客户类型" align="center" prop="authClientClass" width="120" />
-                <el-table-column label="授权加入时间" align="center" prop="authClientEnterDate" width="160" />
-                <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="100">
-                    <template #default="scope">
-                        <el-button v-hasPermi="['customer:customer:auth']" link type="success" icon="User"
-                            @click="handleAuth(scope.row)">授权</el-button>
-                    </template>
-                </el-table-column>
-            </el-table>
-
-            <pagination v-show="total > 0" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize"
-                :total="total" @pagination="getList" />
-        </el-card>
-
-        <!-- 授权客户对话框 -->
-        <el-dialog v-model="authDialog.visible" title="授权客户" width="900px" append-to-body>
-            <el-form :model="erpQueryParams" ref="erpQueryFormRef" :inline="true" label-width="68px">
-                <el-form-item label="客户名称" prop="name">
-                    <el-input v-model="erpQueryParams.name" placeholder="请输入客户名称" clearable style="width: 240px"
-                        @keyup.enter="getErpList" />
-                </el-form-item>
-                <el-form-item>
-                    <el-button type="primary" icon="Search" @click="getErpList">搜索</el-button>
-                    <el-button icon="Refresh" @click="resetErpQuery">重置</el-button>
-                </el-form-item>
-            </el-form>
-            <el-table v-loading="erpLoading" :data="erpList" highlight-current-row
-                @current-change="handleErpCurrentChange">
-                <el-table-column label="代码" align="center" prop="num" width="100"
-                    :formatter="(row: ErpClientVO) => row.num || '-'" />
-                <el-table-column label="名称" align="left" prop="name" min-width="180" :show-overflow-tooltip="true"
-                    :formatter="(row: ErpClientVO) => row.name || '-'" />
-                <el-table-column label="客户类型" align="center" prop="clientClass" width="100"
-                    :formatter="(row: ErpClientVO) => row.clientClass || '-'" />
-                <el-table-column label="加入名称" align="center" prop="enterName" width="100"
-                    :formatter="(row: ErpClientVO) => row.enterName || '-'" />
-                <el-table-column label="加入时间" align="center" prop="enterDate" width="170"
-                    :formatter="(row: ErpClientVO) => row.enterDate || '-'" />
-            </el-table>
-            <pagination v-show="erpTotal > 0" v-model:page="erpQueryParams.pageNum"
-                v-model:limit="erpQueryParams.pageSize" :total="erpTotal" layout="total, prev, pager, next"
-                @pagination="getErpList" />
-            <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>
-    </div>
+          </div>
+        </div>
+        <el-empty v-else description="暂无授权客户" :image-size="60" />
+      </template>
+    </el-dialog>
+  </div>
 </template>
 
 <script setup name="Customer" lang="ts">
-import { listCustomer, getCustomer, authCustomer } from '@/api/system/customer';
-import { CustomerVO, CustomerQuery, CustomerForm } from '@/api/system/customer/types';
+import { listCustomer, getCustomerDetail, authCustomer, changeCustomerStatus } from '@/api/system/customer';
+import { CustomerVO, CustomerQuery, CustomerForm, ErpClientBriefVO } from '@/api/system/customer/types';
 import { listErpClient } from '@/api/erp/client';
 import { ErpClientVO, ErpClientQuery } from '@/api/erp/client/types';
+import { checkPermi } from '@/utils/permission';
+
+/** @Author: Antigravity */
 
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
 
@@ -110,118 +159,271 @@ const queryFormRef = ref<ElFormInstance>();
 
 // 授权客户对话框相关
 const authDialog = reactive({
-    visible: false,
-    customerId: undefined as string | number | undefined
+  visible: false,
+  customerId: undefined as string | number | undefined
 });
 const erpLoading = ref(false);
 const erpList = ref<ErpClientVO[]>([]);
 const erpTotal = ref(0);
 const erpQueryParams = reactive<ErpClientQuery>({
-    pageNum: 1,
-    pageSize: 10,
-    name: undefined
+  pageNum: 1,
+  pageSize: 10,
+  name: undefined
 });
-const selectedErpClient = ref<ErpClientVO | null>(null);
+const selectedErpClients = ref<ErpClientVO[]>([]);
+const erpTableRef = ref();
 const erpQueryFormRef = ref<ElFormInstance>();
 
+// 详情对话框
+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: {}
+  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);
-    customerList.value = res.rows;
-    total.value = res.total;
-    loading.value = false;
+  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();
+  queryParams.value.pageNum = 1;
+  getList();
 };
 
 /** 重置按钮操作 */
 const resetQuery = () => {
-    queryFormRef.value?.resetFields();
-    handleQuery();
+  queryFormRef.value?.resetFields();
+  handleQuery();
 };
 
 /** 多选框选中数据 */
 const handleSelectionChange = (selection: CustomerVO[]) => {
-    ids.value = selection.map((item) => item.id);
+  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;
-    selectedErpClient.value = null;
-    resetErpQuery();
+  authDialog.customerId = row.id;
+  authDialog.visible = true;
+  selectedErpClients.value = [];
+  resetErpQuery();
 };
 
 /** 获取ERP客户列表 */
 const getErpList = async () => {
-    erpLoading.value = true;
-    const res = await listErpClient(erpQueryParams);
-    erpList.value = res.rows;
-    erpTotal.value = res.total;
-    erpLoading.value = false;
+  erpLoading.value = true;
+  const res = await listErpClient(erpQueryParams);
+  erpList.value = res.rows;
+  erpTotal.value = res.total;
+  erpLoading.value = false;
 };
 
 /** 重置ERP查询 */
 const resetErpQuery = () => {
-    erpQueryFormRef.value?.resetFields();
-    erpQueryParams.pageNum = 1;
-    getErpList();
+  erpQueryFormRef.value?.resetFields();
+  erpQueryParams.pageNum = 1;
+  getErpList();
 };
 
-/** ERP客户选中 */
-const handleErpCurrentChange = (val: ErpClientVO | null) => {
-    selectedErpClient.value = val;
+/** ERP客户多选选中 */
+const handleErpSelectionChange = (val: ErpClientVO[]) => {
+  selectedErpClients.value = val;
 };
 
 /** 确认授权 */
 const confirmAuth = async () => {
-    if (!selectedErpClient.value) {
-        proxy?.$modal.msgError('请选择一个ERP客户');
-        return;
-    }
-    buttonLoading.value = true;
-    try {
-        await authCustomer(authDialog.customerId!, selectedErpClient.value.rowId);
-        proxy?.$modal.msgSuccess('授权成功');
-        authDialog.visible = false;
-        getList();
-    } finally {
-        buttonLoading.value = false;
-    }
+  if (!selectedErpClients.value || selectedErpClients.value.length === 0) {
+    proxy?.$modal.msgError('请至少选择一个ERP客户');
+    return;
+  }
+  buttonLoading.value = true;
+  try {
+    const authClientFRowIDs = selectedErpClients.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();
+  getList();
 });
 </script>
 
 <style scoped>
-/** @Author: Antigravity - 优化弹窗分页居中 */
+.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;
+  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>