index.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437
  1. <template>
  2. <div class="p-2">
  3. <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
  4. <div v-show="showSearch" class="mb-[10px]">
  5. <el-card shadow="hover">
  6. <el-form ref="queryFormRef" :model="queryParams" :inline="true">
  7. <el-form-item label="客服名称" prop="name">
  8. <el-input v-model="queryParams.name" placeholder="请输入客服名称" clearable @keyup.enter="handleQuery" />
  9. </el-form-item>
  10. <el-form-item label="性别" prop="gender">
  11. <el-select v-model="queryParams.gender" placeholder="请选择性别" clearable>
  12. <el-option label="男" value="0" />
  13. <el-option label="女" value="1" />
  14. </el-select>
  15. </el-form-item>
  16. <el-form-item label="状态" prop="status">
  17. <el-select v-model="queryParams.status" placeholder="请选择状态" clearable>
  18. <el-option label="空闲中" value="0" />
  19. <el-option label="停用" value="1" />
  20. <el-option label="工作中" value="2" />
  21. </el-select>
  22. </el-form-item>
  23. <el-form-item>
  24. <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
  25. <el-button icon="Refresh" @click="resetQuery">重置</el-button>
  26. </el-form-item>
  27. </el-form>
  28. </el-card>
  29. </div>
  30. </transition>
  31. <el-card shadow="never">
  32. <template #header>
  33. <el-row :gutter="10" class="mb8">
  34. <el-col :span="1.5">
  35. <el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['talk:agent:add']">新增</el-button>
  36. </el-col>
  37. <el-col :span="1.5">
  38. <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['talk:agent:edit']">修改</el-button>
  39. </el-col>
  40. <el-col :span="1.5">
  41. <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['talk:agent:remove']">删除</el-button>
  42. </el-col>
  43. <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
  44. </el-row>
  45. </template>
  46. <el-table v-loading="loading" border :data="agentList" @selection-change="handleSelectionChange">
  47. <el-table-column type="selection" width="55" align="center" />
  48. <el-table-column label="客服ID" align="center" prop="id" v-if="true" />
  49. <el-table-column label="客服名称" align="center" prop="name" />
  50. <el-table-column label="性别" align="center" prop="gender">
  51. <template #default="scope">
  52. <span>{{ scope.row.gender === '0' ? '男' : '女' }}</span>
  53. </template>
  54. </el-table-column>
  55. <el-table-column label="欢迎语" align="center" prop="greetingMessage" show-overflow-tooltip />
  56. <el-table-column label="发音人" align="center" prop="ttsVcn">
  57. <template #default="scope">
  58. <span>{{ getDictLabel(ttsVcnOptions, scope.row.ttsVcn) }}</span>
  59. </template>
  60. </el-table-column>
  61. <el-table-column label="语速" align="center" prop="ttsSpeed" />
  62. <el-table-column label="音调" align="center" prop="ttsPitch" />
  63. <el-table-column label="音量" align="center" prop="ttsVolume" />
  64. <el-table-column label="使用者" align="center" prop="userId">
  65. <template #default="scope">
  66. <span>{{ getUserNickname(scope.row.userId) }}</span>
  67. </template>
  68. </el-table-column>
  69. <el-table-column label="状态" align="center" prop="status">
  70. <template #default="scope">
  71. <el-tag :type="scope.row.status === '0' ? 'success' : scope.row.status === '1' ? 'danger' : 'warning'">
  72. {{ scope.row.status === '0' ? '空闲中' : scope.row.status === '1' ? '停用' : '工作中' }}
  73. </el-tag>
  74. </template>
  75. </el-table-column>
  76. <el-table-column label="操作" align="center" fixed="right" class-name="small-padding fixed-width">
  77. <template #default="scope">
  78. <el-tooltip content="修改" placement="top">
  79. <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['talk:agent:edit']"></el-button>
  80. </el-tooltip>
  81. <el-tooltip content="删除" placement="top">
  82. <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['talk:agent:remove']"></el-button>
  83. </el-tooltip>
  84. </template>
  85. </el-table-column>
  86. </el-table>
  87. <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
  88. </el-card>
  89. <!-- 添加或修改客服对话框 -->
  90. <el-dialog :title="dialog.title" v-model="dialog.visible" width="600px" append-to-body>
  91. <el-form ref="agentFormRef" :model="form" :rules="rules" label-width="100px">
  92. <el-form-item label="客服名称" prop="name">
  93. <el-input v-model="form.name" placeholder="请输入客服名称" />
  94. </el-form-item>
  95. <el-form-item label="性别" prop="gender">
  96. <el-radio-group v-model="form.gender">
  97. <el-radio value="0">男</el-radio>
  98. <el-radio value="1">女</el-radio>
  99. </el-radio-group>
  100. </el-form-item>
  101. <el-form-item label="头像URL" prop="avatarUrl">
  102. <el-upload
  103. class="avatar-uploader"
  104. :action="uploadAction"
  105. :headers="uploadHeaders"
  106. :show-file-list="false"
  107. :on-success="handleAvatarSuccess"
  108. :before-upload="beforeAvatarUpload"
  109. >
  110. <img v-if="form.avatarUrl" :src="getAvatarUrl(form.avatarUrl)" class="avatar" />
  111. <el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
  112. </el-upload>
  113. </el-form-item>
  114. <el-form-item label="欢迎语" prop="greetingMessage">
  115. <el-input v-model="form.greetingMessage" type="textarea" placeholder="请输入欢迎语" />
  116. </el-form-item>
  117. <el-form-item label="发音人" prop="ttsVcn">
  118. <el-select v-model="form.ttsVcn" placeholder="请选择发音人">
  119. <el-option
  120. v-for="item in ttsVcnOptions"
  121. :key="item.dictValue"
  122. :label="item.dictLabel"
  123. :value="item.dictValue"
  124. />
  125. </el-select>
  126. </el-form-item>
  127. <el-form-item label="语速" prop="ttsSpeed">
  128. <el-slider v-model="form.ttsSpeed" :min="0" :max="100" show-input />
  129. </el-form-item>
  130. <el-form-item label="音调" prop="ttsPitch">
  131. <el-slider v-model="form.ttsPitch" :min="0" :max="100" show-input />
  132. </el-form-item>
  133. <el-form-item label="音量" prop="ttsVolume">
  134. <el-slider v-model="form.ttsVolume" :min="0" :max="100" show-input />
  135. </el-form-item>
  136. <el-form-item label="背景音" prop="ttsBgs">
  137. <el-radio-group v-model="form.ttsBgs">
  138. <el-radio :value="0">无背景音</el-radio>
  139. <el-radio :value="1">有背景音</el-radio>
  140. </el-radio-group>
  141. </el-form-item>
  142. <el-form-item label="使用者" prop="userId">
  143. <el-select v-model="form.userId" placeholder="请选择使用者" clearable filterable>
  144. <el-option
  145. v-for="user in userList"
  146. :key="user.id"
  147. :label="user.nickname"
  148. :value="user.id"
  149. />
  150. </el-select>
  151. </el-form-item>
  152. <el-form-item label="状态" prop="status">
  153. <el-radio-group v-model="form.status">
  154. <el-radio value="0">空闲中</el-radio>
  155. <el-radio value="1">停用</el-radio>
  156. <el-radio value="2">工作中</el-radio>
  157. </el-radio-group>
  158. </el-form-item>
  159. </el-form>
  160. <template #footer>
  161. <div class="dialog-footer">
  162. <el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
  163. <el-button @click="cancel">取 消</el-button>
  164. </div>
  165. </template>
  166. </el-dialog>
  167. </div>
  168. </template>
  169. <script setup name="Agent" lang="ts">
  170. import { listAgent, getAgent, delAgent, addAgent, updateAgent, getTtsVcnDict } from '@/api/talk/agent';
  171. import { AgentVO, AgentQuery, AgentForm } from '@/api/talk/agent/types';
  172. import { listTalkUser } from '@/api/talk/user';
  173. import { TalkUserVO } from '@/api/talk/user/types';
  174. import { Plus } from '@element-plus/icons-vue';
  175. import type { UploadProps } from 'element-plus';
  176. import { getToken } from '@/utils/auth';
  177. const { proxy } = getCurrentInstance() as ComponentInternalInstance;
  178. const agentList = ref<AgentVO[]>([]);
  179. const userList = ref<TalkUserVO[]>([]);
  180. const buttonLoading = ref(false);
  181. const loading = ref(true);
  182. const showSearch = ref(true);
  183. const ids = ref<Array<string | number>>([]);
  184. const single = ref(true);
  185. const multiple = ref(true);
  186. const total = ref(0);
  187. const ttsVcnOptions = ref<any[]>([]);
  188. const uploadAction = ref(import.meta.env.VITE_APP_BASE_API + '/talk/admin/agent/avatar');
  189. const uploadHeaders = ref({
  190. Authorization: 'Bearer ' + getToken(),
  191. clientid: import.meta.env.VITE_APP_CLIENT_ID
  192. });
  193. const queryFormRef = ref<ElFormInstance>();
  194. const agentFormRef = ref<ElFormInstance>();
  195. const dialog = reactive<DialogOption>({
  196. visible: false,
  197. title: ''
  198. });
  199. const initFormData: AgentForm = {
  200. id: undefined,
  201. name: undefined,
  202. gender: '0',
  203. avatarUrl: undefined,
  204. greetingMessage: undefined,
  205. status: '0',
  206. ttsVcn: 'x4_yezi',
  207. ttsSpeed: 50,
  208. ttsPitch: 50,
  209. ttsVolume: 50,
  210. ttsBgs: 0
  211. }
  212. const data = reactive<PageData<AgentForm, AgentQuery>>({
  213. form: {...initFormData},
  214. queryParams: {
  215. pageNum: 1,
  216. pageSize: 10,
  217. name: undefined,
  218. gender: undefined,
  219. status: undefined
  220. },
  221. rules: {
  222. name: [
  223. { required: true, message: "客服名称不能为空", trigger: "blur" }
  224. ],
  225. gender: [
  226. { required: true, message: "性别不能为空", trigger: "change" }
  227. ]
  228. }
  229. });
  230. const { queryParams, form, rules } = toRefs(data);
  231. /** 获取字典数据 */
  232. const fetchDictData = async () => {
  233. try {
  234. const ttsVcnRes = await getTtsVcnDict();
  235. ttsVcnOptions.value = ttsVcnRes.data || [];
  236. } catch (error) {
  237. console.error('获取字典数据失败:', error);
  238. }
  239. }
  240. /** 获取用户列表 */
  241. const fetchUserList = async () => {
  242. try {
  243. const res = await listTalkUser({ pageNum: 1, pageSize: 1000 });
  244. userList.value = res.rows || [];
  245. } catch (error) {
  246. console.error('获取用户列表失败:', error);
  247. }
  248. }
  249. /** 根据用户ID获取用户昵称 */
  250. const getUserNickname = (userId: number | undefined) => {
  251. if (!userId) return '-';
  252. const user = userList.value.find(u => u.id === userId);
  253. return user ? user.nickname : '-';
  254. }
  255. /** 查询客服列表 */
  256. const getList = async () => {
  257. loading.value = true;
  258. const res = await listAgent(queryParams.value);
  259. agentList.value = res.rows;
  260. total.value = res.total;
  261. loading.value = false;
  262. }
  263. /** 头像上传成功回调 */
  264. const handleAvatarSuccess: UploadProps['onSuccess'] = (response) => {
  265. console.log('上传成功,response:', response);
  266. console.log('response.msg:', response.msg);
  267. form.value.avatarUrl = response.msg;
  268. console.log('设置后 form.avatarUrl:', form.value.avatarUrl);
  269. }
  270. /** 头像上传前校验 */
  271. const beforeAvatarUpload: UploadProps['beforeUpload'] = (rawFile) => {
  272. if (!['image/jpeg', 'image/png', 'image/gif'].includes(rawFile.type)) {
  273. proxy?.$modal.msgError('头像必须是 JPG/PNG/GIF 格式!');
  274. return false;
  275. }
  276. if (rawFile.size / 1024 / 1024 > 2) {
  277. proxy?.$modal.msgError('头像大小不能超过 2MB!');
  278. return false;
  279. }
  280. return true;
  281. }
  282. /** 获取头像完整URL */
  283. const getAvatarUrl = (avatarUrl: string) => {
  284. console.log('getAvatarUrl 被调用, avatarUrl:', avatarUrl);
  285. console.log('VITE_APP_BASE_API:', import.meta.env.VITE_APP_BASE_API);
  286. if (!avatarUrl) {
  287. console.log('avatarUrl 为空,返回空字符串');
  288. return '';
  289. }
  290. if (avatarUrl.startsWith('http://') || avatarUrl.startsWith('https://')) {
  291. console.log('avatarUrl 已经是完整 URL,直接返回');
  292. return avatarUrl;
  293. }
  294. const fullUrl = import.meta.env.VITE_APP_BASE_API + avatarUrl;
  295. console.log('拼接后的完整 URL:', fullUrl);
  296. return fullUrl;
  297. }
  298. /** 根据字典值获取字典标签 */
  299. const getDictLabel = (options: any[], value: string) => {
  300. const item = options.find(opt => opt.dictValue === value);
  301. return item ? item.dictLabel : value;
  302. }
  303. /** 取消按钮 */
  304. const cancel = () => {
  305. reset();
  306. dialog.visible = false;
  307. }
  308. /** 表单重置 */
  309. const reset = () => {
  310. form.value = {...initFormData};
  311. agentFormRef.value?.resetFields();
  312. }
  313. /** 搜索按钮操作 */
  314. const handleQuery = () => {
  315. queryParams.value.pageNum = 1;
  316. getList();
  317. }
  318. /** 重置按钮操作 */
  319. const resetQuery = () => {
  320. queryFormRef.value?.resetFields();
  321. handleQuery();
  322. }
  323. /** 多选框选中数据 */
  324. const handleSelectionChange = (selection: AgentVO[]) => {
  325. ids.value = selection.map(item => item.id);
  326. single.value = selection.length != 1;
  327. multiple.value = !selection.length;
  328. }
  329. /** 新增按钮操作 */
  330. const handleAdd = () => {
  331. reset();
  332. fetchUserList();
  333. dialog.visible = true;
  334. dialog.title = "添加客服";
  335. }
  336. /** 修改按钮操作 */
  337. const handleUpdate = async (row?: AgentVO) => {
  338. reset();
  339. fetchUserList();
  340. const _id = row?.id || ids.value[0]
  341. const res = await getAgent(_id);
  342. Object.assign(form.value, res.data);
  343. dialog.visible = true;
  344. dialog.title = "修改客服";
  345. }
  346. /** 提交按钮 */
  347. const submitForm = () => {
  348. agentFormRef.value?.validate(async (valid: boolean) => {
  349. if (valid) {
  350. buttonLoading.value = true;
  351. if (form.value.id) {
  352. await updateAgent(form.value).finally(() => buttonLoading.value = false);
  353. } else {
  354. await addAgent(form.value).finally(() => buttonLoading.value = false);
  355. }
  356. proxy?.$modal.msgSuccess("操作成功");
  357. dialog.visible = false;
  358. await getList();
  359. }
  360. });
  361. }
  362. /** 删除按钮操作 */
  363. const handleDelete = async (row?: AgentVO) => {
  364. const _ids = row?.id || ids.value;
  365. await proxy?.$modal.confirm('是否确认删除客服编号为"' + _ids + '"的数据项?').finally(() => loading.value = false);
  366. await delAgent(_ids);
  367. proxy?.$modal.msgSuccess("删除成功");
  368. await getList();
  369. }
  370. onMounted(() => {
  371. fetchDictData();
  372. fetchUserList();
  373. getList();
  374. });
  375. </script>
  376. <style scoped lang="scss">
  377. .avatar-uploader {
  378. :deep(.el-upload) {
  379. border: 1px dashed var(--el-border-color);
  380. border-radius: 6px;
  381. cursor: pointer;
  382. position: relative;
  383. overflow: hidden;
  384. transition: var(--el-transition-duration-fast);
  385. }
  386. :deep(.el-upload:hover) {
  387. border-color: var(--el-color-primary);
  388. }
  389. .avatar-uploader-icon {
  390. font-size: 28px;
  391. color: #8c939d;
  392. width: 178px;
  393. height: 178px;
  394. text-align: center;
  395. line-height: 178px;
  396. }
  397. .avatar {
  398. width: 178px;
  399. height: 178px;
  400. display: block;
  401. }
  402. }
  403. </style>