ソースを参照

管理端zoomphone与whatsapp页面增添

Zhangbw 2 ヶ月 前
コミット
1d8b4722a5

+ 1 - 1
.env.production

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

+ 2 - 0
src/api/talk/agent/types.ts

@@ -10,6 +10,7 @@ export interface AgentVO {
   ttsPitch: number;
   ttsVolume: number;
   ttsBgs: number;
+  userId?: number;
   createTime?: string;
   updateTime?: string;
 }
@@ -26,6 +27,7 @@ export interface AgentForm {
   ttsPitch?: number;
   ttsVolume?: number;
   ttsBgs?: number;
+  userId?: number;
 }
 
 export interface AgentQuery extends PageQuery {

+ 2 - 0
src/api/talk/session/types.ts

@@ -28,4 +28,6 @@ export interface SessionQuery extends PageQuery {
   userId?: number;
   startTime?: string;
   endTime?: string;
+  orderByColumn?: string;
+  isAsc?: string;
 }

+ 32 - 0
src/api/talk/whatsapp/history/index.ts

@@ -0,0 +1,32 @@
+import request from '@/utils/request';
+import { WhatsAppHistoryQuery } from './types';
+
+export function listWhatsAppHistory(query: WhatsAppHistoryQuery) {
+  return request({
+    url: '/talk/admin/whatsapp/history/list',
+    method: 'get',
+    params: query
+  });
+}
+
+export function getWhatsAppHistory(id: string | number) {
+  return request({
+    url: '/talk/admin/whatsapp/history/' + id,
+    method: 'get'
+  });
+}
+
+export function delWhatsAppHistory(id: string | number | Array<string | number>) {
+  return request({
+    url: '/talk/admin/whatsapp/history/' + id,
+    method: 'delete'
+  });
+}
+
+export function exportWhatsAppHistory(query: WhatsAppHistoryQuery) {
+  return request({
+    url: '/talk/admin/whatsapp/history/export',
+    method: 'post',
+    data: query
+  });
+}

+ 17 - 0
src/api/talk/whatsapp/history/types.ts

@@ -0,0 +1,17 @@
+export interface WhatsAppHistoryVO {
+  id: string | number;
+  sessionId: string;
+  userAccount: string;
+  conversationJson?: string;
+  startTime?: string;
+  endTime?: string;
+  createTime?: string;
+  updateTime?: string;
+}
+
+export interface WhatsAppHistoryQuery extends PageQuery {
+  sessionId?: string;
+  userAccount?: string;
+  startTime?: string;
+  endTime?: string;
+}

+ 48 - 0
src/api/talk/whatsapp/user/index.ts

@@ -0,0 +1,48 @@
+import request from '@/utils/request';
+import { WhatsAppUserQuery, WhatsAppUserForm } from './types';
+
+export function listWhatsAppUser(query: WhatsAppUserQuery) {
+  return request({
+    url: '/talk/admin/whatsapp/user/list',
+    method: 'get',
+    params: query
+  });
+}
+
+export function getWhatsAppUser(id: string | number) {
+  return request({
+    url: '/talk/admin/whatsapp/user/' + id,
+    method: 'get'
+  });
+}
+
+export function addWhatsAppUser(data: WhatsAppUserForm) {
+  return request({
+    url: '/talk/admin/whatsapp/user',
+    method: 'post',
+    data: data
+  });
+}
+
+export function updateWhatsAppUser(data: WhatsAppUserForm) {
+  return request({
+    url: '/talk/admin/whatsapp/user',
+    method: 'put',
+    data: data
+  });
+}
+
+export function delWhatsAppUser(id: string | number | Array<string | number>) {
+  return request({
+    url: '/talk/admin/whatsapp/user/' + id,
+    method: 'delete'
+  });
+}
+
+export function exportWhatsAppUser(query: WhatsAppUserQuery) {
+  return request({
+    url: '/talk/admin/whatsapp/user/export',
+    method: 'post',
+    data: query
+  });
+}

+ 15 - 0
src/api/talk/whatsapp/user/types.ts

@@ -0,0 +1,15 @@
+export interface WhatsAppUserVO {
+  id: string | number;
+  userAccount: string;
+  createTime?: string;
+  updateTime?: string;
+}
+
+export interface WhatsAppUserForm {
+  id?: string | number;
+  userAccount?: string;
+}
+
+export interface WhatsAppUserQuery extends PageQuery {
+  userAccount?: string;
+}

+ 48 - 0
src/api/talk/zoomphone/user/index.ts

@@ -0,0 +1,48 @@
+import request from '@/utils/request';
+import { ZoomPhoneUserQuery, ZoomPhoneUserForm } from './types';
+
+export function listZoomPhoneUser(query: ZoomPhoneUserQuery) {
+  return request({
+    url: '/talk/admin/zoomphone/user/list',
+    method: 'get',
+    params: query
+  });
+}
+
+export function getZoomPhoneUser(id: string | number) {
+  return request({
+    url: '/talk/admin/zoomphone/user/' + id,
+    method: 'get'
+  });
+}
+
+export function addZoomPhoneUser(data: ZoomPhoneUserForm) {
+  return request({
+    url: '/talk/admin/zoomphone/user',
+    method: 'post',
+    data: data
+  });
+}
+
+export function updateZoomPhoneUser(data: ZoomPhoneUserForm) {
+  return request({
+    url: '/talk/admin/zoomphone/user',
+    method: 'put',
+    data: data
+  });
+}
+
+export function delZoomPhoneUser(id: string | number | Array<string | number>) {
+  return request({
+    url: '/talk/admin/zoomphone/user/' + id,
+    method: 'delete'
+  });
+}
+
+export function exportZoomPhoneUser(query: ZoomPhoneUserQuery) {
+  return request({
+    url: '/talk/admin/zoomphone/user/export',
+    method: 'post',
+    data: query
+  });
+}

+ 15 - 0
src/api/talk/zoomphone/user/types.ts

@@ -0,0 +1,15 @@
+export interface ZoomPhoneUserVO {
+  id: string | number;
+  customerPhone: string;
+  createTime?: string;
+  updateTime?: string;
+}
+
+export interface ZoomPhoneUserForm {
+  id?: string | number;
+  customerPhone?: string;
+}
+
+export interface ZoomPhoneUserQuery extends PageQuery {
+  customerPhone?: string;
+}

+ 0 - 6
src/main.ts

@@ -28,9 +28,6 @@ import ElementIcons from '@/plugins/svgicon';
 // permission control
 import './permission';
 
-// 开发者工具保护
-import { initDevToolsProtection } from '@/utils/devtools-protection';
-
 // 国际化
 import i18n from '@/lang/index';
 
@@ -58,6 +55,3 @@ app.use(plugins);
 directive(app);
 
 app.mount('#app');
-
-// 初始化开发者工具保护(仅生产环境)
-initDevToolsProtection();

+ 38 - 0
src/views/talk/agent/index.vue

@@ -63,6 +63,11 @@
         <el-table-column label="语速" align="center" prop="ttsSpeed" />
         <el-table-column label="音调" align="center" prop="ttsPitch" />
         <el-table-column label="音量" align="center" prop="ttsVolume" />
+        <el-table-column label="使用者" align="center" prop="userId">
+          <template #default="scope">
+            <span>{{ getUserNickname(scope.row.userId) }}</span>
+          </template>
+        </el-table-column>
         <el-table-column label="状态" align="center" prop="status">
           <template #default="scope">
             <el-tag :type="scope.row.status === '0' ? 'success' : scope.row.status === '1' ? 'danger' : 'warning'">
@@ -138,6 +143,16 @@
             <el-radio :value="1">有背景音</el-radio>
           </el-radio-group>
         </el-form-item>
+        <el-form-item label="使用者" prop="userId">
+          <el-select v-model="form.userId" placeholder="请选择使用者" clearable filterable>
+            <el-option
+              v-for="user in userList"
+              :key="user.id"
+              :label="user.nickname"
+              :value="user.id"
+            />
+          </el-select>
+        </el-form-item>
         <el-form-item label="状态" prop="status">
           <el-radio-group v-model="form.status">
             <el-radio value="0">空闲中</el-radio>
@@ -159,6 +174,8 @@
 <script setup name="Agent" lang="ts">
 import { listAgent, getAgent, delAgent, addAgent, updateAgent, getTtsVcnDict } from '@/api/talk/agent';
 import { AgentVO, AgentQuery, AgentForm } from '@/api/talk/agent/types';
+import { listTalkUser } from '@/api/talk/user';
+import { TalkUserVO } from '@/api/talk/user/types';
 import { Plus } from '@element-plus/icons-vue';
 import type { UploadProps } from 'element-plus';
 import { getToken } from '@/utils/auth';
@@ -166,6 +183,7 @@ import { getToken } from '@/utils/auth';
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
 
 const agentList = ref<AgentVO[]>([]);
+const userList = ref<TalkUserVO[]>([]);
 const buttonLoading = ref(false);
 const loading = ref(true);
 const showSearch = ref(true);
@@ -233,6 +251,23 @@ const fetchDictData = async () => {
   }
 }
 
+/** 获取用户列表 */
+const fetchUserList = async () => {
+  try {
+    const res = await listTalkUser({ pageNum: 1, pageSize: 1000 });
+    userList.value = res.rows || [];
+  } catch (error) {
+    console.error('获取用户列表失败:', error);
+  }
+}
+
+/** 根据用户ID获取用户昵称 */
+const getUserNickname = (userId: number | undefined) => {
+  if (!userId) return '-';
+  const user = userList.value.find(u => u.id === userId);
+  return user ? user.nickname : '-';
+}
+
 /** 查询客服列表 */
 const getList = async () => {
   loading.value = true;
@@ -320,6 +355,7 @@ const handleSelectionChange = (selection: AgentVO[]) => {
 /** 新增按钮操作 */
 const handleAdd = () => {
   reset();
+  fetchUserList();
   dialog.visible = true;
   dialog.title = "添加客服";
 }
@@ -327,6 +363,7 @@ const handleAdd = () => {
 /** 修改按钮操作 */
 const handleUpdate = async (row?: AgentVO) => {
   reset();
+  fetchUserList();
   const _id = row?.id || ids.value[0]
   const res = await getAgent(_id);
   Object.assign(form.value, res.data);
@@ -362,6 +399,7 @@ const handleDelete = async (row?: AgentVO) => {
 
 onMounted(() => {
   fetchDictData();
+  fetchUserList();
   getList();
 });
 </script>

+ 3 - 1
src/views/talk/session/index.vue

@@ -152,7 +152,9 @@ const data = reactive<PageData<any, SessionQuery>>({
     customerPhone: undefined,
     userId: undefined,
     startTime: undefined,
-    endTime: undefined
+    endTime: undefined,
+    orderByColumn: 'createTime',
+    isAsc: 'desc'
   },
   rules: {}
 });

+ 301 - 0
src/views/talk/whatsapp/history/index.vue

@@ -0,0 +1,301 @@
+<template>
+  <div class="p-2">
+    <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
+      <div v-show="showSearch" class="mb-[10px]">
+        <el-card shadow="hover">
+          <el-form ref="queryFormRef" :model="queryParams" :inline="true">
+            <el-form-item label="会话标识" prop="sessionId">
+              <el-input v-model="queryParams.sessionId" placeholder="请输入会话标识" clearable @keyup.enter="handleQuery" />
+            </el-form-item>
+            <el-form-item label="用户账号" prop="userAccount">
+              <el-input v-model="queryParams.userAccount" placeholder="请输入用户账号" clearable @keyup.enter="handleQuery" />
+            </el-form-item>
+            <div style="width: 100%"></div>
+            <el-form-item label="开始时间" prop="startTime">
+              <el-date-picker clearable
+                v-model="queryParams.startTime"
+                type="date"
+                value-format="YYYY-MM-DD"
+                placeholder="请选择开始时间"
+              />
+            </el-form-item>
+            <el-form-item label="结束时间" prop="endTime">
+              <el-date-picker clearable
+                v-model="queryParams.endTime"
+                type="date"
+                value-format="YYYY-MM-DD"
+                placeholder="请选择结束时间"
+              />
+            </el-form-item>
+            <el-form-item>
+              <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+              <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+            </el-form-item>
+          </el-form>
+        </el-card>
+      </div>
+    </transition>
+
+    <el-card shadow="never">
+      <template #header>
+        <el-row :gutter="10" class="mb8">
+          <el-col :span="1.5">
+            <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['talk:whatsapp:history:remove']">删除</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['talk:whatsapp:history:export']">导出</el-button>
+          </el-col>
+          <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
+        </el-row>
+      </template>
+
+      <el-table v-loading="loading" border :data="historyList" @selection-change="handleSelectionChange">
+        <el-table-column type="selection" width="55" align="center" />
+        <el-table-column label="会话ID" align="center" prop="id" width="80" />
+        <el-table-column label="会话标识" align="center" prop="sessionId" width="200" show-overflow-tooltip />
+        <el-table-column label="用户账号" align="center" prop="userAccount" />
+        <el-table-column label="开始时间" align="center" prop="startTime" width="180">
+          <template #default="scope">
+            <span>{{ parseTime(scope.row.startTime, '{y}-{m}-{d} {h}:{i}') }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="结束时间" align="center" prop="endTime" width="180">
+          <template #default="scope">
+            <span>{{ parseTime(scope.row.endTime, '{y}-{m}-{d} {h}:{i}') }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" align="center" fixed="right" class-name="small-padding fixed-width">
+          <template #default="scope">
+            <el-tooltip content="查看详情" placement="top">
+              <el-button link type="primary" icon="View" @click="handleView(scope.row)" v-hasPermi="['talk:whatsapp:history:query']"></el-button>
+            </el-tooltip>
+            <el-tooltip content="删除" placement="top">
+              <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['talk:whatsapp:history:remove']"></el-button>
+            </el-tooltip>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
+    </el-card>
+
+    <!-- 查看会话详情对话框 -->
+    <el-dialog title="会话详情" v-model="dialog.visible" width="800px" append-to-body>
+      <el-descriptions :column="2" border>
+        <el-descriptions-item label="会话标识">{{ historyDetail.sessionId }}</el-descriptions-item>
+        <el-descriptions-item label="用户账号">{{ historyDetail.userAccount }}</el-descriptions-item>
+        <el-descriptions-item label="开始时间">{{ parseTime(historyDetail.startTime) }}</el-descriptions-item>
+        <el-descriptions-item label="结束时间">{{ parseTime(historyDetail.endTime) }}</el-descriptions-item>
+      </el-descriptions>
+      <el-divider content-position="left">对话内容</el-divider>
+      <div class="conversation-content">
+        <div v-if="parsedConversation.length > 0" class="message-list">
+          <div v-for="(msg, index) in parsedConversation" :key="index" class="message-item">
+            <div class="message-header">
+              <span class="message-role" :class="msg.role === 'user' ? 'user-role' : 'assistant-role'">
+                {{ msg.role === 'user' ? 'User' : 'AI' }}
+              </span>
+              <span class="message-time">{{ formatTimestamp(msg.timestamp) }}</span>
+            </div>
+            <div class="message-content">{{ msg.content }}</div>
+          </div>
+        </div>
+        <div v-else class="empty-conversation">暂无对话内容</div>
+      </div>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="dialog.visible = false">关 闭</el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup name="WhatsAppHistory" lang="ts">
+import { listWhatsAppHistory, getWhatsAppHistory, delWhatsAppHistory } from '@/api/talk/whatsapp/history';
+import { WhatsAppHistoryVO, WhatsAppHistoryQuery } from '@/api/talk/whatsapp/history/types';
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+
+const historyList = ref<WhatsAppHistoryVO[]>([]);
+const loading = ref(true);
+const showSearch = ref(true);
+const ids = ref<Array<string | number>>([]);
+const multiple = ref(true);
+const total = ref(0);
+const historyDetail = ref<WhatsAppHistoryVO>({} as WhatsAppHistoryVO);
+
+const queryFormRef = ref<ElFormInstance>();
+
+const dialog = reactive<DialogOption>({
+  visible: false,
+  title: ''
+});
+
+const data = reactive<PageData<any, WhatsAppHistoryQuery>>({
+  form: {},
+  queryParams: {
+    pageNum: 1,
+    pageSize: 10,
+    sessionId: undefined,
+    userAccount: undefined,
+    startTime: undefined,
+    endTime: undefined,
+    orderByColumn: 'createTime',
+    isAsc: 'desc'
+  },
+  rules: {}
+});
+
+const { queryParams } = toRefs(data);
+
+/** 解析对话内容 */
+const parsedConversation = computed(() => {
+  if (!historyDetail.value.conversationJson) {
+    return [];
+  }
+  try {
+    return JSON.parse(historyDetail.value.conversationJson);
+  } catch (e) {
+    console.error('解析对话内容失败:', e);
+    return [];
+  }
+});
+
+/** 格式化时间戳 */
+const formatTimestamp = (timestamp: string) => {
+  if (!timestamp) return '';
+  const date = new Date(parseInt(timestamp));
+  return date.toLocaleString('zh-CN', {
+    year: 'numeric',
+    month: '2-digit',
+    day: '2-digit',
+    hour: '2-digit',
+    minute: '2-digit',
+    second: '2-digit'
+  });
+};
+
+/** 查询会话列表 */
+const getList = async () => {
+  loading.value = true;
+  const res = await listWhatsAppHistory(queryParams.value);
+  historyList.value = res.rows;
+  total.value = res.total;
+  loading.value = false;
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.value.pageNum = 1;
+  getList();
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value?.resetFields();
+  handleQuery();
+}
+
+/** 多选框选中数据 */
+const handleSelectionChange = (selection: WhatsAppHistoryVO[]) => {
+  ids.value = selection.map(item => item.id);
+  multiple.value = !selection.length;
+}
+
+/** 查看详情 */
+const handleView = async (row: WhatsAppHistoryVO) => {
+  const res = await getWhatsAppHistory(row.id);
+  historyDetail.value = res.data;
+  dialog.visible = true;
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (row?: WhatsAppHistoryVO) => {
+  const _ids = row?.id || ids.value;
+  await proxy?.$modal.confirm('是否确认删除会话编号为"' + _ids + '"的数据项?').finally(() => loading.value = false);
+  await delWhatsAppHistory(_ids);
+  proxy?.$modal.msgSuccess("删除成功");
+  await getList();
+}
+
+/** 导出按钮操作 */
+const handleExport = () => {
+  proxy?.download('talk/admin/whatsapp/history/export', {
+    ...queryParams.value
+  }, `whatsapp_history_${new Date().getTime()}.xlsx`)
+}
+
+onMounted(() => {
+  getList();
+});
+</script>
+
+<style scoped>
+.conversation-content {
+  max-height: 400px;
+  overflow-y: auto;
+  background-color: #f5f5f5;
+  padding: 10px;
+  border-radius: 4px;
+}
+
+.message-list {
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+}
+
+.message-item {
+  background: white;
+  padding: 12px;
+  border-radius: 8px;
+  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
+}
+
+.message-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 8px;
+  padding-bottom: 6px;
+  border-bottom: 1px solid #e5e7eb;
+}
+
+.message-role {
+  font-weight: 600;
+  font-size: 14px;
+  padding: 2px 8px;
+  border-radius: 4px;
+}
+
+.user-role {
+  background-color: #dbeafe;
+  color: #1e40af;
+}
+
+.assistant-role {
+  background-color: #dcfce7;
+  color: #166534;
+}
+
+.message-time {
+  font-size: 12px;
+  color: #6b7280;
+}
+
+.message-content {
+  font-size: 14px;
+  line-height: 1.6;
+  color: #374151;
+  white-space: pre-wrap;
+  word-wrap: break-word;
+}
+
+.empty-conversation {
+  text-align: center;
+  color: #9ca3af;
+  padding: 20px;
+  font-size: 14px;
+}
+</style>

+ 214 - 0
src/views/talk/whatsapp/user/index.vue

@@ -0,0 +1,214 @@
+<template>
+  <div class="p-2">
+    <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
+      <div v-show="showSearch" class="mb-[10px]">
+        <el-card shadow="hover">
+          <el-form ref="queryFormRef" :model="queryParams" :inline="true">
+            <el-form-item label="用户账号" prop="userAccount">
+              <el-input v-model="queryParams.userAccount" 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>
+        </el-card>
+      </div>
+    </transition>
+
+    <el-card shadow="never">
+      <template #header>
+        <el-row :gutter="10" class="mb8">
+          <el-col :span="1.5">
+            <el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['talk:whatsapp:user:add']">新增</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['talk:whatsapp:user:edit']">修改</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['talk:whatsapp:user:remove']">删除</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['talk:whatsapp:user:export']">导出</el-button>
+          </el-col>
+          <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
+        </el-row>
+      </template>
+
+      <el-table v-loading="loading" border :data="userList" @selection-change="handleSelectionChange">
+        <el-table-column type="selection" width="55" align="center" />
+        <el-table-column label="用户ID" align="center" prop="id" />
+        <el-table-column label="用户账号" align="center" prop="userAccount" />
+        <el-table-column label="创建时间" align="center" prop="createTime" width="180" />
+        <el-table-column label="操作" align="center" fixed="right" class-name="small-padding fixed-width" width="150">
+          <template #default="scope">
+            <el-tooltip content="修改" placement="top">
+              <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['talk:whatsapp:user:edit']"></el-button>
+            </el-tooltip>
+            <el-tooltip content="删除" placement="top">
+              <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['talk:whatsapp:user:remove']"></el-button>
+            </el-tooltip>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
+    </el-card>
+
+    <!-- 添加或修改WhatsApp用户对话框 -->
+    <el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
+      <el-form ref="userFormRef" :model="form" :rules="rules" label-width="80px">
+        <el-form-item label="用户账号" prop="userAccount">
+          <el-input v-model="form.userAccount" placeholder="请输入用户账号" :disabled="!!form.id" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
+          <el-button @click="cancel">取 消</el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup name="WhatsAppUser" lang="ts">
+import { listWhatsAppUser, getWhatsAppUser, delWhatsAppUser, addWhatsAppUser, updateWhatsAppUser, exportWhatsAppUser } from '@/api/talk/whatsapp/user';
+import { WhatsAppUserVO, WhatsAppUserQuery, WhatsAppUserForm } from '@/api/talk/whatsapp/user/types';
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+
+const userList = ref<WhatsAppUserVO[]>([]);
+const buttonLoading = ref(false);
+const loading = ref(true);
+const showSearch = ref(true);
+const ids = ref<Array<string | number>>([]);
+const single = ref(true);
+const multiple = ref(true);
+const total = ref(0);
+
+const queryFormRef = ref<ElFormInstance>();
+const userFormRef = ref<ElFormInstance>();
+
+const dialog = reactive<DialogOption>({
+  visible: false,
+  title: ''
+});
+
+const initFormData: WhatsAppUserForm = {
+  id: undefined,
+  userAccount: undefined
+}
+
+const data = reactive<PageData<WhatsAppUserForm, WhatsAppUserQuery>>({
+  form: {...initFormData},
+  queryParams: {
+    pageNum: 1,
+    pageSize: 10,
+    userAccount: undefined
+  },
+  rules: {
+    userAccount: [
+      { required: true, message: "用户账号不能为空", trigger: "blur" }
+    ]
+  }
+});
+
+const { queryParams, form, rules } = toRefs(data);
+
+/** 查询WhatsApp用户列表 */
+const getList = async () => {
+  loading.value = true;
+  const res = await listWhatsAppUser(queryParams.value);
+  userList.value = res.rows;
+  total.value = res.total;
+  loading.value = false;
+}
+
+/** 取消按钮 */
+const cancel = () => {
+  reset();
+  dialog.visible = false;
+}
+
+/** 表单重置 */
+const reset = () => {
+  form.value = {...initFormData};
+  userFormRef.value?.resetFields();
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.value.pageNum = 1;
+  getList();
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value?.resetFields();
+  handleQuery();
+}
+
+/** 多选框选中数据 */
+const handleSelectionChange = (selection: WhatsAppUserVO[]) => {
+  ids.value = selection.map(item => item.id);
+  single.value = selection.length != 1;
+  multiple.value = !selection.length;
+}
+
+/** 新增按钮操作 */
+const handleAdd = () => {
+  reset();
+  dialog.visible = true;
+  dialog.title = "添加WhatsApp用户";
+}
+
+/** 修改按钮操作 */
+const handleUpdate = async (row?: WhatsAppUserVO) => {
+  reset();
+  const _id = row?.id || ids.value[0]
+  const res = await getWhatsAppUser(_id);
+  Object.assign(form.value, res.data);
+  dialog.visible = true;
+  dialog.title = "修改WhatsApp用户";
+}
+
+/** 提交按钮 */
+const submitForm = () => {
+  userFormRef.value?.validate(async (valid: boolean) => {
+    if (valid) {
+      buttonLoading.value = true;
+      if (form.value.id) {
+        await updateWhatsAppUser(form.value).finally(() => buttonLoading.value = false);
+      } else {
+        await addWhatsAppUser(form.value).finally(() => buttonLoading.value = false);
+      }
+      proxy?.$modal.msgSuccess("操作成功");
+      dialog.visible = false;
+      await getList();
+    }
+  });
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (row?: WhatsAppUserVO) => {
+  const _ids = row?.id || ids.value;
+  await proxy?.$modal.confirm('是否确认删除WhatsApp用户编号为"' + _ids + '"的数据项?').finally(() => loading.value = false);
+  await delWhatsAppUser(_ids);
+  proxy?.$modal.msgSuccess("删除成功");
+  await getList();
+}
+
+/** 导出按钮操作 */
+const handleExport = () => {
+  proxy?.$modal.confirm('是否确认导出所有WhatsApp用户数据项?').then(() => {
+    return exportWhatsAppUser(queryParams.value);
+  }).then(() => {
+    proxy?.$modal.msgSuccess('导出成功');
+  }).catch(() => {});
+}
+
+onMounted(() => {
+  getList();
+});
+</script>

+ 209 - 0
src/views/talk/zoomphone/user/index.vue

@@ -0,0 +1,209 @@
+<template>
+  <div class="p-2">
+    <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
+      <div v-show="showSearch" class="mb-[10px]">
+        <el-card shadow="hover">
+          <el-form ref="queryFormRef" :model="queryParams" :inline="true">
+            <el-form-item label="客户电话" prop="customerPhone">
+              <el-input v-model="queryParams.customerPhone" 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>
+        </el-card>
+      </div>
+    </transition>
+
+    <el-card shadow="never">
+      <template #header>
+        <el-row :gutter="10" class="mb8">
+          <el-col :span="1.5">
+            <el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['zoomphone:user:add']">新增</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['zoomphone:user:edit']">修改</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['zoomphone:user:remove']">删除</el-button>
+          </el-col>
+          <el-col :span="1.5">
+            <el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['zoomphone:user:export']">导出</el-button>
+          </el-col>
+          <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
+        </el-row>
+      </template>
+
+      <el-table v-loading="loading" border :data="userList" @selection-change="handleSelectionChange">
+        <el-table-column type="selection" width="55" align="center" />
+        <el-table-column label="用户ID" align="center" prop="id" />
+        <el-table-column label="客户电话" align="center" prop="customerPhone" />
+        <el-table-column label="创建时间" align="center" prop="createTime" width="180" />
+        <el-table-column label="操作" align="center" fixed="right" class-name="small-padding fixed-width" width="150">
+          <template #default="scope">
+            <el-tooltip content="修改" placement="top">
+              <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['zoomphone:user:edit']"></el-button>
+            </el-tooltip>
+            <el-tooltip content="删除" placement="top">
+              <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['zoomphone:user:remove']"></el-button>
+            </el-tooltip>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
+    </el-card>
+
+    <!-- 添加或修改ZoomPhone用户对话框 -->
+    <el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
+      <el-form ref="userFormRef" :model="form" :rules="rules" label-width="80px">
+        <el-form-item label="客户电话" prop="customerPhone">
+          <el-input v-model="form.customerPhone" placeholder="请输入客户电话" :disabled="!!form.id" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
+          <el-button @click="cancel">取 消</el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup name="ZoomPhoneUser" lang="ts">
+import { listZoomPhoneUser, getZoomPhoneUser, delZoomPhoneUser, addZoomPhoneUser, updateZoomPhoneUser, exportZoomPhoneUser } from '@/api/talk/zoomphone/user';
+import { ZoomPhoneUserVO, ZoomPhoneUserQuery, ZoomPhoneUserForm } from '@/api/talk/zoomphone/user/types';
+
+const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+
+const userList = ref<ZoomPhoneUserVO[]>([]);
+const buttonLoading = ref(false);
+const loading = ref(true);
+const showSearch = ref(true);
+const ids = ref<Array<string | number>>([]);
+const single = ref(true);
+const multiple = ref(true);
+const total = ref(0);
+
+const queryFormRef = ref<ElFormInstance>();
+const userFormRef = ref<ElFormInstance>();
+
+const dialog = reactive<DialogOption>({
+  visible: false,
+  title: ''
+});
+
+const initFormData: ZoomPhoneUserForm = {
+  id: undefined,
+  customerPhone: undefined
+};
+const data = reactive<PageData<ZoomPhoneUserVO, ZoomPhoneUserForm>>({
+  form: { ...initFormData },
+  queryParams: {
+    pageNum: 1,
+    pageSize: 10,
+    customerPhone: undefined
+  },
+  rules: {
+    customerPhone: [
+      { required: true, message: '客户电话不能为空', trigger: 'blur' }
+    ]
+  }
+});
+
+const { queryParams, form, rules } = toRefs(data);
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true;
+  const res = await listZoomPhoneUser(queryParams.value);
+  userList.value = res.rows;
+  total.value = res.total;
+  loading.value = false;
+}
+
+/** 取消按钮 */
+const cancel = () => {
+  reset();
+  dialog.visible = false;
+}
+
+/** 表单重置 */
+const reset = () => {
+  form.value = { ...initFormData };
+  userFormRef.value?.resetFields();
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.value.pageNum = 1;
+  getList();
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value?.resetFields();
+  handleQuery();
+}
+
+/** 多选框选中数据 */
+const handleSelectionChange = (selection: ZoomPhoneUserVO[]) => {
+  ids.value = selection.map(item => item.id);
+  single.value = selection.length != 1;
+  multiple.value = !selection.length;
+}
+
+/** 新增按钮操作 */
+const handleAdd = () => {
+  reset();
+  dialog.visible = true;
+  dialog.title = "添加ZoomPhone用户";
+}
+
+/** 修改按钮操作 */
+const handleUpdate = async (row?: ZoomPhoneUserVO) => {
+  reset();
+  const _id = row?.id || ids.value[0]
+  const res = await getZoomPhoneUser(_id);
+  Object.assign(form.value, res.data);
+  dialog.visible = true;
+  dialog.title = "修改ZoomPhone用户";
+}
+
+/** 提交按钮 */
+const submitForm = () => {
+  userFormRef.value?.validate(async (valid: boolean) => {
+    if (valid) {
+      buttonLoading.value = true;
+      if (form.value.id) {
+        await updateZoomPhoneUser(form.value).finally(() => buttonLoading.value = false);
+      } else {
+        await addZoomPhoneUser(form.value).finally(() => buttonLoading.value = false);
+      }
+      proxy?.$modal.msgSuccess("操作成功");
+      dialog.visible = false;
+      await getList();
+    }
+  });
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (row?: ZoomPhoneUserVO) => {
+  const _ids = row?.id || ids.value;
+  await proxy?.$modal.confirm('是否确认删除用户编号为"' + _ids + '"的数据项?').finally(() => loading.value = false);
+  await delZoomPhoneUser(_ids);
+  proxy?.$modal.msgSuccess("删除成功");
+  await getList();
+}
+
+/** 导出按钮操作 */
+const handleExport = () => {
+  proxy?.$download.post('/talk/admin/zoomphone/user/export', queryParams.value, `zoomphone_user_${new Date().getTime()}.xlsx`);
+}
+
+onMounted(() => {
+  getList();
+});
+</script>