Browse Source

添加指定客服链接复制功能

master
[yxf] 4 weeks ago
parent
commit
76c9732d16
  1. 30
      im-admin-ui/src/api/im/agent/index.ts
  2. 247
      im-admin-ui/src/views/im/code/components/FloatBallSetting.vue
  3. 3
      im-admin-ui/src/views/im/code/components/wangye.vue
  4. 82
      im-admin-ui/src/views/im/customer/index.vue

30
im-admin-ui/src/api/im/agent/index.ts

@ -0,0 +1,30 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
/**
* /
* @param data
*/
export function updateAgentFloatBall(data: {
uniqueToken: string;
pcFloatBall: string;
mobileFloatBall: string;
}): AxiosPromise<any> {
return request({
url: '/im/agent/updateFloatBall',
method: 'post',
data: data
});
}
/**
* token获取代理悬浮球配置
* @param token
*/
export function getAgentFloatBallConfig(token: string): AxiosPromise<any> {
return request({
url: '/im/agent/infoByToken',
method: 'get',
params: { token }
});
}

247
im-admin-ui/src/views/im/code/components/FloatBallSetting.vue

@ -0,0 +1,247 @@
<template>
<div class="float-ball-setting">
<div class="setting-header">
<el-button type="primary" @click="showSettingDialog = true">
<el-icon><Setting /></el-icon>
悬浮球设置
</el-button>
</div>
<!-- 悬浮球设置弹窗 -->
<el-dialog
v-model="showSettingDialog"
title="悬浮球图片设置"
width="500px"
:close-on-click-modal="false"
>
<el-form :model="floatBallConfig" label-width="120px">
<el-form-item label="PC端悬浮球">
<div class="upload-section">
<image-upload
v-model="floatBallConfig.pcImage"
v-model:thumb="floatBallConfig.pcImageThumb"
:width="80"
:height="80"
/>
<div class="upload-tip">
<p>建议尺寸60x60px</p>
</div>
</div>
</el-form-item>
<el-form-item label="移动端悬浮球">
<div class="upload-section">
<image-upload
v-model="floatBallConfig.mobileImage"
v-model:thumb="floatBallConfig.mobileImageThumb"
:width="80"
:height="80"
/>
<div class="upload-tip">
<p>建议尺寸50x50px</p>
</div>
</div>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="showSettingDialog = false">取消</el-button>
<el-button type="primary" @click="saveFloatBallConfig">
保存设置
</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, watch } from 'vue';
import { Setting } from '@element-plus/icons-vue';
import { ElMessage } from 'element-plus';
import ImageUpload from '@/components/ImageUpload/index.vue';
import { updateAgentFloatBall, getAgentFloatBallConfig } from '@/api/im/agent';
//
defineOptions({
name: 'FloatBallSetting'
});
//
const emit = defineEmits<{
(e: 'update:config', config: FloatBallConfig): void;
(e: 'save', config: FloatBallConfig): void;
}>();
// props
interface Props {
initialConfig?: Partial<FloatBallConfig>;
token?: string;
}
const props = withDefaults(defineProps<Props>(), {
initialConfig: () => ({}),
token: ''
});
//
export interface FloatBallConfig {
pcImage: string;
pcImageThumb?: string;
mobileImage: string;
mobileImageThumb?: string;
}
//
const defaultConfig: FloatBallConfig = {
pcImage: '',
pcImageThumb: '',
mobileImage: '',
mobileImageThumb: ''
};
const showSettingDialog = ref(false);
const floatBallConfig = ref<FloatBallConfig>({ ...defaultConfig });
//
async function loadConfigFromServer() {
if (!props.token) return;
try {
const res = await getAgentFloatBallConfig(props.token);
if (res.data) {
const serverConfig = {
pcImage: res.data.pcFloatBall || '',
pcImageThumb: res.data.pcFloatBall || '',
mobileImage: res.data.mobileFloatBall || '',
mobileImageThumb: res.data.mobileFloatBall || ''
};
floatBallConfig.value = { ...defaultConfig, ...serverConfig };
//
const storageKey = `floatBallConfig_${props.token}`;
localStorage.setItem(storageKey, JSON.stringify(floatBallConfig.value));
emit('update:config', floatBallConfig.value);
}
} catch (err) {
console.error('加载服务器配置失败', err);
//
loadFloatBallConfig();
}
}
//
async function saveFloatBallConfig() {
try {
if (!props.token) {
ElMessage.warning('未获取到代理标识,无法保存配置');
return;
}
//
await updateAgentFloatBall({
uniqueToken: props.token,
pcFloatBall: floatBallConfig.value.pcImage,
mobileFloatBall: floatBallConfig.value.mobileImage
});
//
const storageKey = `floatBallConfig_${props.token}`;
localStorage.setItem(storageKey, JSON.stringify(floatBallConfig.value));
emit('save', floatBallConfig.value);
emit('update:config', floatBallConfig.value);
ElMessage.success('悬浮球设置保存成功!');
showSettingDialog.value = false;
} catch (error) {
console.error(error);
ElMessage.error('保存失败,请重试');
}
}
//
function loadFloatBallConfig() {
const storageKey = `floatBallConfig_${props.token || 'default'}`;
const savedConfig = localStorage.getItem(storageKey);
if (savedConfig) {
try {
const parsed = JSON.parse(savedConfig);
floatBallConfig.value = { ...defaultConfig, ...parsed };
} catch (e) {
floatBallConfig.value = { ...defaultConfig };
}
} else if (props.initialConfig && Object.keys(props.initialConfig).length > 0) {
floatBallConfig.value = { ...defaultConfig, ...props.initialConfig };
}
emit('update:config', floatBallConfig.value);
}
//
watch(showSettingDialog, (val) => {
if (val && props.token) {
loadConfigFromServer();
}
});
// token
watch(() => props.token, () => {
if (props.token) {
loadConfigFromServer();
} else {
loadFloatBallConfig();
}
});
onMounted(() => {
if (props.token) {
loadConfigFromServer();
} else {
loadFloatBallConfig();
}
});
defineExpose({
showDialog: () => {
showSettingDialog.value = true;
},
getConfig: () => floatBallConfig.value,
setConfig: (config: Partial<FloatBallConfig>) => {
floatBallConfig.value = { ...floatBallConfig.value, ...config };
}
});
</script>
<style lang="scss" scss>
.float-ball-setting {
.setting-header {
display: flex;
justify-content: flex-end;
margin-bottom: 16px;
padding: 0 4px;
}
.upload-section {
display: flex;
gap: 16px;
align-items: center;
.upload-tip {
p {
margin: 0;
color: #909399;
font-size: 12px;
}
}
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
}
}
</style>

3
im-admin-ui/src/views/im/code/components/wangye.vue

@ -1,7 +1,7 @@
<template> <template>
<div class="content"> <div class="content">
<p class="font-w">使用简介</p> <p class="font-w">使用简介</p>
<p class="text-i">网页内快速接入客服让网页拥有客服窗口请把一下代码复制到网页最底部</p> <p class="text-i">网页内快速接入客服让网页拥有客服窗口请把一下代码复制到网页最底部, 如果需要指定用户ID, 则在token后参数中添加kefuid=用户ID</p>
<el-divider /> <el-divider />
<p class="typetitle">获取代码</p> <p class="typetitle">获取代码</p>
@ -19,6 +19,7 @@
openUrl: '{{ siteUrl }}', openUrl: '{{ siteUrl }}',
token: '{{ uniqueToken }}', token: '{{ uniqueToken }}',
isShowTip: true, isShowTip: true,
kefuid: '',
mobileIcon: '{{ floatConfig?.mobileImage || "" }}', mobileIcon: '{{ floatConfig?.mobileImage || "" }}',
pcIcon: '{{ floatConfig?.pcImage || "" }}', pcIcon: '{{ floatConfig?.pcImage || "" }}',
windowStyle:'', windowStyle:'',

82
im-admin-ui/src/views/im/customer/index.vue

@ -33,6 +33,7 @@
</el-row> </el-row>
</template> </template>
<el-table v-loading="loading" :data="userList" @selection-change="handleSelectionChange"> <el-table v-loading="loading" :data="userList" @selection-change="handleSelectionChange">
<el-table-column label="ID" align="center" prop="id" />
<el-table-column label="用户名" align="center" prop="userName" /> <el-table-column label="用户名" align="center" prop="userName" />
<el-table-column label="用户昵称" align="center" prop="nickName" /> <el-table-column label="用户昵称" align="center" prop="nickName" />
<el-table-column label="用户头像" align="center" prop="headImageThumb" width="100"> <el-table-column label="用户头像" align="center" prop="headImageThumb" width="100">
@ -52,12 +53,12 @@
</template> </template>
</el-table-column> </el-table-column>
--> -->
<el-table-column label="注册时间" align="center" prop="createdTime" width="180"> <el-table-column label="注册时间" align="center" prop="createdTime" width="160">
<template #default="scope"> <template #default="scope">
<span>{{ parseTime(scope.row.createdTime, '{y}-{m}-{d}') }}</span> <span>{{ parseTime(scope.row.createdTime, '{y}-{m}-{d}') }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="最后登录时间" align="center" prop="lastLoginTime" width="180"> <el-table-column label="最后登录时间" align="center" prop="lastLoginTime" width="160">
<template #default="scope"> <template #default="scope">
<span>{{ parseTime(scope.row.lastLoginTime, '{y}-{m}-{d}') }}</span> <span>{{ parseTime(scope.row.lastLoginTime, '{y}-{m}-{d}') }}</span>
</template> </template>
@ -73,6 +74,9 @@
<el-tooltip content="删除" placement="top"> <el-tooltip content="删除" placement="top">
<el-button v-hasPermi="['im:user:removeCustomer']" link type="primary" icon="Delete" @click="handleDelete(scope.row)"></el-button> <el-button v-hasPermi="['im:user:removeCustomer']" link type="primary" icon="Delete" @click="handleDelete(scope.row)"></el-button>
</el-tooltip> </el-tooltip>
<el-tooltip content="复制链接" placement="top">
<el-button v-hasPermi="['im:user:editCustomer']" link type="primary" icon="CopyDocument" @click="handleCopyLink(scope.row)"></el-button>
</el-tooltip>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
@ -121,7 +125,10 @@
import { listUser, getUser, delUser, addUser, updateUser, resetUserPwd } from '@/api/im/user/customer'; import { listUser, getUser, delUser, addUser, updateUser, resetUserPwd } from '@/api/im/user/customer';
import { UserVO, UserQuery, UserForm } from '@/api/im/user/types'; import { UserVO, UserQuery, UserForm } from '@/api/im/user/types';
import { to } from 'await-to-js'; import { to } from 'await-to-js';
import { getInfo } from '@/api/login';
import { getCurrentInstance, ComponentInternalInstance } from 'vue';
const { proxy } = getCurrentInstance() as ComponentInternalInstance; const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const uniqueToken = ref('');
const userList = ref<UserVO[]>([]); const userList = ref<UserVO[]>([]);
const loading = ref(true); const loading = ref(true);
@ -217,6 +224,76 @@ const handleQuery = () => {
console.log('handleQuery'); console.log('handleQuery');
}; };
/**
* 获取当前登录用户的 uniqueToken
*/
const fetchUniqueToken = async () => {
try {
const res = await getInfo();
if (res.data?.tokenInfo?.uniqueToken) {
uniqueToken.value = res.data.tokenInfo.uniqueToken;
} else {
console.warn('未获取到 uniqueToken');
}
} catch (error) {
console.error('获取 uniqueToken 失败:', error);
}
};
/**
* 复制客服链接
* @param row 客服信息
*/
const handleCopyLink = async (row: UserVO) => {
// kefuid
const baseUrl = `${location.origin}/h5`;
const linkUrl = `${baseUrl}?token=${uniqueToken.value}&kefuid=${row.id}`;
try {
// 使 Clipboard API
await navigator.clipboard.writeText(linkUrl);
ElMessage.success('链接已复制到剪贴板');
} catch (err) {
// 使
fallbackCopyTextToClipboard(linkUrl);
}
};
/**
* 降级复制方案兼容旧浏览器
*/
const fallbackCopyTextToClipboard = (text: string) => {
const textArea = document.createElement('textarea');
textArea.value = text;
//
// textArea.style.position = 'fixed';
// textArea.style.top = '0';
// textArea.style.left = '0';
// textArea.style.width = '2em';
// textArea.style.height = '2em';
// textArea.style.padding = '0';
// textArea.style.border = 'none';
// textArea.style.outline = 'none';
// textArea.style.boxShadow = 'none';
// textArea.style.background = 'transparent';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
const successful = document.execCommand('copy');
if (successful) {
ElMessage.success('链接已复制到剪贴板');
} else {
ElMessage.error('复制失败,请手动复制');
}
} catch (err) {
console.error('Fallback: Oops, unable to copy', err);
ElMessage.error('复制失败,请手动复制');
}
document.body.removeChild(textArea);
};
/** 重置按钮操作 */ /** 重置按钮操作 */
const resetQuery = () => { const resetQuery = () => {
queryFormRef.value?.resetFields(); queryFormRef.value?.resetFields();
@ -344,5 +421,6 @@ const handleExport = () => {
onMounted(() => { onMounted(() => {
getList(); getList();
fetchUniqueToken();
}); });
</script> </script>

Loading…
Cancel
Save