🎨 添加图库和客服插件(有问题-待修改)

This commit is contained in:
loser 2025-05-14 16:13:20 +08:00
parent e6ceacfae1
commit 0ed234639c
15 changed files with 3332 additions and 0 deletions

View File

@ -79,5 +79,12 @@
"/src/view/user/user/index.vue": "Index",
"/src/plugin/announcement/form/info.vue": "InfoForm",
"/src/plugin/announcement/view/info.vue": "Info",
"/src/plugin/customerservice/view/chat/index.vue": "ServiceMain",
"/src/plugin/customerservice/view/chat/test.vue": "ServiceUserTest",
"/src/plugin/customerservice/view/index.vue": "CustomerService",
"/src/plugin/customerservice/view/login/index.vue": "Index",
"/src/plugin/customerservice/view/reply/index.vue": "ServiceIndex",
"/src/plugin/customerservice/view/script/index.vue": "ServiceIndex",
"/src/plugin/customerservice/view/service/index.vue": "ServiceIndex",
"/src/plugin/email/view/index.vue": "Email"
}

View File

@ -0,0 +1,113 @@
import service from '@/utils/request'
export const saveService = (data) => {
return service({
url: '/service/save_service',
method: 'post',
data
})
}
export const findService = (params) => {
return service({
url: '/service/find_service',
method: 'get',
params
})
}
export const getServiceList = (params) => {
return service({
url: '/service/get_service_list',
method: 'get',
params
})
}
export const deleteService = (params) => {
return service({
url: '/service/delete_service',
method: 'delete',
params
})
}
export const adminServiceLogin = (params) => {
return service({
url: '/service/admin_login',
method: 'get',
params
})
}
export const accountServiceLogin = (data) => {
return service({
url: '/service/account_login',
method: 'POST',
data
})
}
export const saveScript = (data) => {
return service({
url: '/service/save_script',
method: 'post',
data
})
}
export const findScript = (params) => {
return service({
url: '/service/find_script',
method: 'get',
params
})
}
export const getScriptList = (params) => {
return service({
url: '/service/get_script_list',
method: 'get',
params
})
}
export const deleteScript = (params) => {
return service({
url: '/service/delete_script',
method: 'delete',
params
})
}
export const saveReply = (data) => {
return service({
url: '/service/save_reply',
method: 'post',
data
})
}
export const findReply = (params) => {
return service({
url: '/service/find_reply',
method: 'get',
params
})
}
export const autoReplyList = (params) => {
return service({
url: '/service/auto_reply_list',
method: 'get',
params
})
}
export const deleteReply = (params) => {
return service({
url: '/service/delete_reply',
method: 'delete',
params
})
}

View File

@ -0,0 +1,61 @@
import service from '@/utils/request'
import Cookies from 'js-cookie'
// 添加请求拦截器
service.interceptors.request.use(
config => {
config.headers['user-token'] = 'sssssssssssssssssss'
return config
},
error => {
return Promise.reject(error)
}
)
export const getServiceInfo = (params) => {
return service({
url: '/service/get_kf_info',
method: 'get',
params
})
}
export const sendMessageData = (data) => {
return service({
url: '/service/send_msg',
method: 'post',
data
})
}
export const getMsgList = (params) => {
return service({
url: '/service/get_msg_list',
method: 'get',
params
})
}
export const setMsgView = (params) => {
return service({
url: '/service/set_msg_view',
method: 'get',
params
})
}
export const uploadFile = (data) => {
return service({
url: '/service/upload_file',
method: 'POST',
data
})
}
export const getTestUserInfo = (params) => {
return service({
url: '/service/get_user_info',
method: 'get',
params
})
}

View File

@ -0,0 +1,76 @@
import service from '@/utils/request'
import Cookies from 'js-cookie'
// 添加请求拦截器
service.interceptors.request.use(
config => {
config.headers['chat-token'] = Cookies.get('kf_token')
return config
},
error => {
return Promise.reject(error)
}
)
export const getServiceInfo = (params) => {
return service({
url: '/service/get_kf_info',
method: 'get',
params
})
}
export const sendMessageData = (data) => {
return service({
url: '/service/send_msg',
method: 'post',
data
})
}
export const getMsgUser = (params) => {
return service({
url: '/service/get_msg_user',
method: 'get',
params
})
}
export const getMsgList = (params) => {
return service({
url: '/service/get_msg_list',
method: 'get',
params
})
}
export const setMsgView = (params) => {
return service({
url: '/service/set_msg_view',
method: 'get',
params
})
}
export const uploadFile = (data) => {
return service({
url: '/service/upload_file',
method: 'POST',
data
})
}
export const getScriptList = (params) => {
return service({
url: '/service/get_service_script',
method: 'get',
params
})
}
export const getClientUserInfo = (params) => {
return service({
url: '/service/get_user_info',
method: 'get',
params
})
}

View File

@ -0,0 +1,807 @@
<template>
<div style="width: 70%;margin-left: auto; margin-right: auto; ">
<el-container style="overflow: hidden;">
<!-- Header Section -->
<el-header class="header">
<el-row type="flex" justify="space-between" align="bottom">
<div class="logo"></div>
<div class="user-info">
<el-avatar :src="kfInfo.avatar" />
<span>{{ kfInfo.nickname }}</span>
<el-badge :value="kfInfo.online ? '在线' : '离线'" class="status-badge" :color="kfInfo.online ? '#ff4d4f' : '#e4e7ed'" ></el-badge>
<el-button type="primary" @click="logout">退出登录</el-button>
</div>
</el-row>
</el-header>
<el-container>
<!-- Sidebar Section -->
<el-aside width="250px" class="sidebar">
<!-- <el-input placeholder="搜索用户名" class="search-input"></el-input>-->
<div class="user-list">
<div class="user-item" :class="{ 'selected': currentUser.id === user.id }" v-for="user in users" :key="user.id" @click="selectUser(user)">
<div class="avatar">
<el-avatar :src="user.avatar" :style="{ filter: user.online ? 'none' : 'grayscale(100%)' }"/>
</div>
<div class="user-info-2">
<div>
{{ user.nickname }}
</div>
<div class="last-message">
{{ truncateMessage(user.message) }}
</div>
</div>
<div class="user-meta">
<div class="message-date">{{ user.add_time_str }}</div>
<div class="unread-count" v-if="user.no_read > 0">
{{ user.no_read > 99 ? '99+' : user.no_read }}
</div>
<div class="unread-placeholder" v-else></div>
</div>
</div>
</div>
</el-aside>
<!-- Main Content Section -->
<el-container>
<el-main class="chat-container">
<div class="chat-box">
<el-scrollbar height="500px" ref="chatBox" @scroll="handleScroll">
<div class="chat-message" v-for="message in messages" :key="message.id" :class="{'message-sent': message.is_kf == 1, 'message-received': message.is_kf != 1}">
<span class="timestamp">{{ formatTimestamp(message.add_time) }}</span>
<div class="message-content">
<el-avatar :src="kfInfo.avatar" class="message-avatar" v-if="message.is_kf===1"/>
<el-avatar :src="currentUser.avatar" class="message-avatar" v-else/>
<el-card class="chat-bubble" :class="{ 'kf-background': message.is_kf === 1, 'user-background': message.is_kf === 0 }">
<el-image
preview-teleported
style="width: 50px; height: 50px"
:src="getUrl(message.content)"
fit="cover"
:preview-src-list="[getUrl(message.content)]"
v-if="message.msg_type ==3"
>
</el-image>
<p v-else>{{ message.content }}</p>
</el-card></div>
</div>
</el-scrollbar>
</div>
<div class="message-options">
<el-popover
placement="top-start"
:width="200"
trigger="hover"
>
<template #reference>
<div style="width: 32px;height: 32px;margin-right: 20px;"><svg t="1721982458168" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1587" width="32" height="32"><path d="M508.544 66.88A448 448 0 0 0 60.992 514.432a448 448 0 0 0 447.552 447.552 448 448 0 0 0 447.552-447.552 448 448 0 0 0-447.552-447.552z m0 831.104a384 384 0 0 1-383.552-383.552 384 384 0 0 1 383.552-383.552 384 384 0 0 1 383.552 383.552 384 384 0 0 1-383.552 383.552z" p-id="1588" fill="#409efc"></path><path d="M367.488 404.48m-48 0a48 48 0 1 0 96 0 48 48 0 1 0-96 0Z" p-id="1589" fill="#409efc"></path><path d="M635.52 404.48m-48 0a48 48 0 1 0 96 0 48 48 0 1 0-96 0Z" p-id="1590" fill="#409efc"></path><path d="M669.248 613.056a31.936 31.936 0 0 0-44.16 9.664c-20.8 32.448-67.968 53.44-120.064 53.44-52.096 0-99.2-20.928-120-53.376a32 32 0 1 0-53.888 34.496c32.768 51.136 99.392 82.88 173.888 82.88 74.56 0 141.184-31.744 173.952-82.944a32 32 0 0 0-9.728-44.16z" p-id="1591" fill="#409efc"></path></svg></div>
</template>
<div class="emoji-picker">
<span v-for="emoji in emojis" :key="emoji" @click="selectEmoji(emoji)">{{ emoji }}</span>
</div>
</el-popover>
<el-upload
class="avatar-uploader"
action="#"
:show-file-list = "false"
:http-request="customUpload"
>
<el-icon color="#409efc" :size="32" style="margin-right: 20px;"><Picture />
</el-icon>
</el-upload>
<el-icon color="#409efc" @click="toggleScriptLibrary" :size="32" style="margin-right: 20px;"><Notebook /></el-icon>
</div>
<el-input type="textarea" placeholder="请输入文字内容" v-model="newMessage" class="message-input" :autosize="{ minRows: 8, maxRows: 8 }"></el-input>
<el-button type="primary" @click="sendMessage" class="send-button">发送</el-button>
</el-main>
<!-- User Info Section with Tabs -->
<el-aside width="300px" class="user-details">
<el-tabs v-model="activeTab" @tab-click="handleTabClick">
<el-tab-pane label="客户信息" name="customer-info">
<el-card v-if="currentUser.uid">
<el-avatar :src="getUrl(clientUserInfo.avatar)" />
<p>手机号码: {{ clientUserInfo.phone }}</p>
<p>用户等级: {{ clientUserInfo.level }}</p>
<p>性别: {{ clientUserInfo.gender }}</p>
<p>余额: </p>
<p>推广员: </p>
</el-card>
<el-card v-else>暂无信息</el-card>
</el-tab-pane>
<el-tab-pane label="交易订单" name="transaction-orders">
<el-tabs tab-position="top" v-model="activeOrderTab">
<el-tab-pane label="全部" name="all-orders">
<p>这里显示全部订单内容</p>
</el-tab-pane>
<el-tab-pane label="未支付" name="unpaid-orders">
<p>这里显示未支付订单内容</p>
</el-tab-pane>
<el-tab-pane label="未发货" name="unshipped-orders">
<p>这里显示未发货订单内容</p>
</el-tab-pane>
<el-tab-pane label="退款中" name="refunding-orders">
<p>这里显示退款中订单内容</p>
</el-tab-pane>
</el-tabs>
</el-tab-pane>
<el-tab-pane label="商品信息" name="product-info">
<el-tabs tab-position="top" v-model="activeProductTab">
<el-tab-pane label="购买" name="purchased-products">
<p>这里显示已购买商品内容</p>
</el-tab-pane>
<el-tab-pane label="足迹" name="footprints">
<p>这里显示浏览足迹内容</p>
</el-tab-pane>
<el-tab-pane label="收藏" name="collected-products">
<p>这里显示收藏商品内容</p>
</el-tab-pane>
</el-tabs>
</el-tab-pane>
</el-tabs>
</el-aside>
</el-container>
</el-container>
<!-- Script Library Dialog -->
<el-dialog title="话术库" v-model="showScriptLibrary" width="50%">
<el-tabs v-model="activeScriptTab" @tab-click="handleClick">
<el-tab-pane label="个人库" name="personal-library">
<div v-for="script in personalScripts" :key="script.id" @click="selectScript(script)" class="script-item">
<span class="script-title">{{ script.title }}</span>
<span class="script-content">{{ script.content }}</span>
</div>
</el-tab-pane>
<el-tab-pane label="公共库" name="public-library">
<div v-for="script in publicScripts" :key="script.id" @click="selectScript(script)" class="script-item">
<span class="script-title">{{ script.title }}</span>
<span class="script-content">{{ script.content }}</span>
</div>
</el-tab-pane>
</el-tabs>
</el-dialog>
</el-container>
</div>
</template>
<script setup>
defineOptions({
name: 'ServiceMain'
})
import { ref ,nextTick, onMounted ,onUnmounted } from 'vue';
import { ElMessage } from 'element-plus';
import { ElScrollbar } from 'element-plus'
import { Picture,Notebook } from '@element-plus/icons-vue'
import Cookies from 'js-cookie'
import {
getServiceInfo,
getMsgList,
getMsgUser,
sendMessageData,
setMsgView,
uploadFile,
getScriptList,
getClientUserInfo
} from "@/plugin/customerservice/api/ws"
import { useRouter } from 'vue-router'
import {getUrl} from "@/utils/image"
const currentUser = ref({})
const token = ref('')
const router = useRouter()
//
const message = ref('')
const kfInfo = ref({
nickname:"",
avatar:"",
online:1,
id:0
})
const fileList = ref([])
const chatBox = ref(null)
const websocket = ref(null)
const currentPage = ref(2)
const clientUserInfo = ref({})
// Function to initialize the WebSocket connection
const initWebSocket = () => {
websocket.value = new WebSocket(`ws://localhost:8888/service/serve_ws?token=${token.value}`) // Replace with your WebSocket URL
websocket.value.onopen = () => {
console.log('WebSocket connection opened')
}
websocket.value.onmessage = async (event) => {
const message = JSON.parse(event.data)
switch (message.type){
case "pong":
break;
case "message":
if(message.data.receiver == kfInfo.value.id){
if(users.value.length === 0){
await getMsgUsers()
}
else{
const user = users.value.find(user => user.uid == message.data.sender)
if (user) {
if(user.uid == currentUser.value.uid){
messages.value.push({
id: Date.now(),
content: message.data.content,
msg_type: message.data.msg_type,
type: message.data.msg_type,
role: 'user',
timestamp: Math.floor(Date.now() / 1000),
add_time: Math.floor(Date.now() / 1000),
is_kf:0
})
}
if( message.data.msg_type == 3){
user.message = '[图片]'
}
else{
user.message = message.data.content
}
user.no_read = (user.no_read || 0 ) + 1
await nextTick()
chatBox.value.wrapRef.scrollTop = chatBox.value.wrapRef.scrollHeight
}else{
await getMsgUsers()
}
}
}
break;
case "offline":
//线
updateUserOnlineStatus(message.data.sender,0)
break;
case "online":
//线
updateUserOnlineStatus(message.data.sender,1)
break;
}
}
// 线
const updateUserOnlineStatus = (userId, isOnline) => {
const user = users.value.find(user => user.uid == userId);
if (user) {
user.online = isOnline ? 1 : 0;
}
}
websocket.value.onerror = (error) => {
console.error('WebSocket error:', error)
}
websocket.value.onclose = () => {
kfInfo.value.online = 0
console.log('WebSocket connection closed')
}
}
const truncateMessage = (message) => {
return message.length > 10 ? message.slice(0, 10) + '...' : message
}
const formatDate = (date) => {
return date.toLocaleDateString() //
}
const sendMessage = async () => {
if(!currentUser.value.uid){
ElMessage.error('先选择聊天用户')
return
}
if (!newMessage.value) {
ElMessage.error('请输入消息内容')
return
}
const message = {
sender: String(kfInfo.value.id),
receiver: String(currentUser.value.uid),
msg_type: '1',
type: '1',
role: 'kf',
timestamp: Math.floor(Date.now() / 1000),
add_time: Math.floor(Date.now() / 1000),
avatar_url: kfInfo.value.avatar,
nickname: kfInfo.value.nickname,
content: newMessage.value,
is_kf:1
}
try {
const response = await sendMessageData(message);
if(response.code !== 0){
ElMessage.error(response.msg)
return
}
currentUser.value.message = newMessage.value
messages.value.push(message)
newMessage.value = ''
await nextTick()
chatBox.value.wrapRef.scrollTop = chatBox.value.wrapRef.scrollHeight
currentUser.value.no_read = 0
setMsgView({uid:currentUser.value.uid})
} catch (error) {
console.error('请求失败:', error);
}
}
const sendPing = () => {
setInterval(function () {
if(websocket.value && websocket.value.readyState === WebSocket.OPEN){
websocket.value.send(JSON.stringify({
type: 'ping',data: {timestamp:Date.now(),}
}))
}
},5000)
}
const getMsgUsers = async () => {
const response = await getMsgUser({kf_id:kfInfo.value.id});
if(response.code === 0){
users.value = response.data
}else{
ElMessage.error(response.msg)
}
}
const getKfInfo = async () => {
const response = await getServiceInfo({});
if(response.code === 0){
kfInfo.value = response.data
}else{
ElMessage.error(response.msg)
}
}
onMounted(async () => {
await getCookie()
})
const customUpload = async (response) => {
if(!currentUser.value.uid){
ElMessage.error('请先选择用户')
return
}
const formData = new FormData();
console.log(response)
formData.append('file', response.file);
const res = await uploadFile(formData)
// 使URLURL
if (res.code===0) {
const message = {
sender: String(kfInfo.value.id),
receiver: String(currentUser.value.uid),
msg_type: '3',
type: '3',
role: 'kf',
timestamp: Math.floor(Date.now() / 1000),
add_time: Math.floor(Date.now() / 1000),
avatar_url: kfInfo.value.avatar,
nickname: kfInfo.value.nickname,
content: res.data,
is_kf:1
}
try {
const response2 = await sendMessageData(message);
if(response2.code !== 0){
ElMessage.error(response2.msg)
return
}
currentUser.value.message = '[图片]'
currentUser.value.add_time_str = formatTimestamp2(Date.now())
messages.value.push(message)
newMessage.value = ''
await nextTick()
chatBox.value.wrapRef.scrollTop = chatBox.value.wrapRef.scrollHeight
} catch (error) {
console.error('请求失败:', error)
}
}
}
const formatTimestamp2 = (timestamp) => {
const now = new Date();
const date = new Date(timestamp);
const todayStart = new Date(now.setHours(0, 0, 0, 0));
const yesterdayStart = new Date(todayStart);
yesterdayStart.setDate(yesterdayStart.getDate() - 1);
if (date >= todayStart) {
//
return date.toTimeString().slice(0, 5); // HH:mm
} else if (date >= yesterdayStart && date < todayStart) {
//
return '昨天';
} else {
//
const month = date.getMonth() + 1;
const day = date.getDate();
return `${month}-${day}`; // MM-DD
}
}
const getCookie = async () => {
token.value = Cookies.get('kf_token')
if(!token.value){
// window.open(window.location.protocol + '//' + window.location.host + '/#/kefu/login')
// return
router.push('/kefu/login')
}
else{
initWebSocket()
await getKfInfo()
await getMsgUsers()
sendPing()
}
console.log('Cookie value: ' + token.value)
}
//
const handleScroll = async (e) => {
if(e.scrollTop === 0){
await loadMessages(currentPage.value)
}
}
//
const loadMessages = async (page = 1) => {
try {
const response2 = await getMsgList({kf_id:kfInfo.value.id,uid:currentUser.value.uid,page:page})
if(response2.code === 0 && response2.data.list.length > 0){
messages.value = [...response2.data.list, ...messages.value]
currentPage.value++;
}
} catch (error) {
console.error('Failed to load messages:', error)
}
}
// WebSocket
onUnmounted(() => {
if (websocket.value) {
websocket.value.close()
}
})
const users = ref([])
const user = ref({
nickname:"",
avatar:""
});
const messages = ref([])
const newMessage = ref('')
const activeTab = ref('customer-info')
const activeOrderTab = ref('all-orders')
const activeProductTab = ref('purchased-products')
const emojis = ref(['😀', '😁', '😂', '🤣', '😃', '😄', '😅', '😆', '😉', '😊'])
const showEmojiPicker = ref(false)
const showScriptLibrary = ref(false)
const activeScriptTab = ref('personal-library')
const personalScripts = ref([
])
const publicScripts = ref([
])
//
const formatTimestamp = (timestamp) => {
const date = new Date(timestamp * 1000); //
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
}
const selectUser = async (user) => {
user.no_read = 0
currentUser.value = user;
currentPage.value = 2
//
await setMsgView({kf_id:kfInfo.value.id,uid:currentUser.value.uid})
const response2 = await getMsgList({kf_id:kfInfo.value.id,uid:currentUser.value.uid})
if(response2.code === 0){
messages.value = response2.data.list
await nextTick()
chatBox.value.wrapRef.scrollTop = chatBox.value.wrapRef.scrollHeight
}else{
ElMessage.error(response2.msg)
}
getClientUser()
}
const handleTabClick = (tab) => {
console.log('Tab clicked:', tab.paneName)
if(tab.paneName === 'customer-info'){
getClientUser()
}else if(tab.paneName === 'transaction-orders'){
}else{
}
}
const getClientUser = async() => {
let res = await getClientUserInfo({uid:currentUser.value.uid})
if(res.code === 0){
clientUserInfo.value = res.data
}
}
const logout = () => {
token.value = Cookies.set('kf_token','')
router.push('/kefu/login');
}
const toggleEmojiPicker = () => {
showEmojiPicker.value = !showEmojiPicker.value;
}
const selectEmoji = (emoji) => {
newMessage.value += emoji;
showEmojiPicker.value = false;
}
const toggleScriptLibrary = async () => {
showScriptLibrary.value = !showScriptLibrary.value;
const res = await getScriptList({type:'1'})
if (res.code === 0) {
}
}
const handleClick = async (tab, event) => {
let cType
if(tab.paneName === 'personal-library'){
cType = '1'
const res = await getScriptList({type:cType})
if(res.code === 0){
personalScripts.value = res.data
console.log(personalScripts.value)
}
}else{
cType = '2'
const res = await getScriptList({type:cType})
if(res.code === 0){
publicScripts.value = res.data
}
}
}
const selectScript = (script) => {
newMessage.value = script.content;
showScriptLibrary.value = false;
};
</script>
<style scoped>
.fade-enter-active, .fade-leave-active {
transition: filter 0.5s;
}
.header {
background-color: #409EFF;
display: flex;
color: #fff;
padding: 0 20px;
justify-content: flex-end; /* 将内容右对齐 */
align-items: center; /* 使文字垂直居中 */
height: 60px; /* 设置头部高度 */
}
.logo {
font-size: 24px;
font-weight: bold;
}
.user-info {
display: flex;
align-items: center;
}
.user-info span {
margin-left: 10px;
}
.status-badge {
margin-left: 10px;
}
.sidebar {
background-color: #f5f5f5;
padding: 10px;
}
.search-input {
margin-bottom: 10px;
}
.chat-container {
display: flex;
flex-direction: column;
padding: 10px;
}
.chat-box {
flex: 1;
overflow-y: auto;
margin-bottom: 10px;
}
.chat-message {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 20px;
}
.timestamp {
font-size: 0.75em;
color: #888;
margin-bottom: 5px;
text-align: center;
width: 100%;
}
.message-content {
display: flex;
align-items: center;
width: 95%;
}
.message-sent .message-content {
justify-content: flex-end;
}
.message-received .message-content {
justify-content: flex-start;
}
.chat-bubble {
max-width: 70%;
word-wrap: break-word;
padding: 10px;
}
.message-avatar {
margin-right: 10px;
}
.message-sent .message-avatar {
order: 2;
margin-left: 10px;
margin-right: 0;
}
.message-options {
display: flex;
justify-content: flex-start; /* Align buttons to the left */
margin-bottom: 10px;
}
.message-input {
margin-bottom: 10px;
}
.send-button {
align-self: flex-end;
}
.user-details {
background-color: #f5f5f5;
padding: 10px;
}
.emoji-picker {
display: flex;
flex-wrap: wrap;
}
.emoji-picker span {
cursor: pointer;
margin: 5px;
font-size: 24px;
}
.user-item {
display: flex;
align-items: center;
padding: 10px;
border-bottom: 1px solid #ddd;
}
.user-item.selected {
background-color: #e4e7ed; /* 高亮显示颜色 */
}
.avatar img {
width: 40px;
height: 40px;
border-radius: 50%;
}
.user-info-2 {
flex-grow: 1;
margin-left: 10px;
}
.last-message {
color: #999;
font-size: 12px;
}
.user-meta {
display: flex;
flex-direction: column;
align-items: flex-end;
}
.message-date {
font-size: 12px;
color: #999;
}
.unread-count {
margin-top: 5px;
background-color: #ff4d4f;
color: #fff;
border-radius: 10px;
font-size: 13px;
padding: 0 5px;
}
.unread-placeholder {
height: 20px;
visibility: hidden; /* 保持布局但不显示内容 */
}
.kf-background {
background-color: #95ec69; /* 类似微信的背景色 */
border-radius: 10px;
}
.user-background {
background-color:#ffffff; /* 用户消息的背景色 */
border-radius: 10px;
}
.script-item {
margin-top: 15px; /* Add space between each item */
}
.script-title {
font-weight: bold;
}
.script-content {
color: #7a7a7a;
margin-left: 10px;
}
</style>

View File

@ -0,0 +1,497 @@
<template>
<div style="width: 55%;margin-left: auto; margin-right: auto; ">
<el-container style="overflow: hidden;">
<el-container>
<el-container>
<el-main class="chat-container">
<div class="chat-box">
<el-scrollbar height="500px" ref="chatBox" @scroll="handleScroll">
<div class="chat-message" v-for="message in messages" :key="message.id" :class="{'message-sent': message.is_kf == 0, 'message-received': message.is_kf != 0}">
<span class="timestamp">{{ formatTimestamp(message.add_time) }}</span>
<div class="message-content">
<el-avatar :src="kfInfo.avatar" class="message-avatar" v-if="message.is_kf===1"/>
<el-avatar :src="currentUser.avatar" class="message-avatar" v-else/>
<el-card class="chat-bubble" :class="{ 'kf-background': message.is_kf === 0, 'user-background': message.is_kf === 1 }">
<el-image
preview-teleported
style="width: 50px; height: 50px"
:src="message.content"
fit="cover"
:preview-src-list="[message.content]"
v-if="message.msg_type ==3"
>
</el-image>
<p v-else>{{ message.content }}</p>
</el-card></div>
</div>
</el-scrollbar>
</div>
<div class="message-options">
<el-popover
placement="top-start"
:width="200"
trigger="hover"
>
<template #reference>
<div style="width: 32px;height: 32px;margin-right: 20px;"><svg t="1721982458168" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1587" width="32" height="32"><path d="M508.544 66.88A448 448 0 0 0 60.992 514.432a448 448 0 0 0 447.552 447.552 448 448 0 0 0 447.552-447.552 448 448 0 0 0-447.552-447.552z m0 831.104a384 384 0 0 1-383.552-383.552 384 384 0 0 1 383.552-383.552 384 384 0 0 1 383.552 383.552 384 384 0 0 1-383.552 383.552z" p-id="1588" fill="#409efc"></path><path d="M367.488 404.48m-48 0a48 48 0 1 0 96 0 48 48 0 1 0-96 0Z" p-id="1589" fill="#409efc"></path><path d="M635.52 404.48m-48 0a48 48 0 1 0 96 0 48 48 0 1 0-96 0Z" p-id="1590" fill="#409efc"></path><path d="M669.248 613.056a31.936 31.936 0 0 0-44.16 9.664c-20.8 32.448-67.968 53.44-120.064 53.44-52.096 0-99.2-20.928-120-53.376a32 32 0 1 0-53.888 34.496c32.768 51.136 99.392 82.88 173.888 82.88 74.56 0 141.184-31.744 173.952-82.944a32 32 0 0 0-9.728-44.16z" p-id="1591" fill="#409efc"></path></svg></div>
</template>
<div class="emoji-picker">
<span v-for="emoji in emojis" :key="emoji" @click="selectEmoji(emoji)">{{ emoji }}</span>
</div>
</el-popover>
<el-upload
class="avatar-uploader"
action="#"
:show-file-list = "false"
:http-request="customUpload"
>
<el-icon color="#409efc" :size="32" style="margin-right: 20px;"><Picture />
</el-icon>
</el-upload>
</div>
<el-input type="textarea" placeholder="请输入文字内容" v-model="newMessage" class="message-input" :autosize="{ minRows: 8, maxRows: 8 }"></el-input>
<el-button type="primary" @click="sendMessage" class="send-button">发送</el-button>
</el-main>
</el-container>
</el-container>
</el-container>
</div>
</template>
<script setup>
defineOptions({
name: 'ServiceUserTest'
})
import { ref ,nextTick, onMounted ,onUnmounted } from 'vue';
import { ElMessage } from 'element-plus';
import { ElScrollbar } from 'element-plus'
import { Picture,Notebook } from '@element-plus/icons-vue'
import Cookies from 'js-cookie'
import {getServiceInfo,getMsgList,sendMessageData, setMsgView,uploadFile,getTestUserInfo} from "@/plugin/customerservice/api/test"
import { useRouter } from 'vue-router'
const uid = ref(0)
const currentUser = ref({})
const token = ref('')
const router = useRouter()
console.log(uid.value)
//
const message = ref('')
const kfInfo = ref({
nickname:"",
avatar:"",
online:0,
id:0
})
const fileList = ref([])
const chatBox = ref(null)
const websocket = ref(null)
const currentPage = ref(2)
// Function to initialize the WebSocket connection
const initWebSocket = () => {
websocket.value = new WebSocket(`ws://localhost:8888/service/ws?user_id=${uid.value}`) // Replace with your WebSocket URL
websocket.value.onopen = () => {
console.log('WebSocket connection opened')
}
websocket.value.onmessage = async (event) => {
const message = JSON.parse(event.data)
switch (message.type){
case "pong":
break;
case "message":
if(message.data.sender == kfInfo.value.id && message.data.receiver == currentUser.value.id ){
messages.value.push({
id: Date.now(),
content: message.data.content,
msg_type: message.data.msg_type,
type: message.data.msg_type,
role: 'kf',
timestamp: Math.floor(Date.now() / 1000),
add_time: Math.floor(Date.now() / 1000),
is_kf:1
});
await nextTick()
chatBox.value.wrapRef.scrollTop = chatBox.value.wrapRef.scrollHeight;
}
break;
case "offline":
//线
updateUserOnlineStatus(message.data.sender,0)
break;
case "online":
//线
updateUserOnlineStatus(message.data.sender,1)
break;
}
}
// 线
const updateUserOnlineStatus = (userId, isOnline) => {
const user = users.value.find(user => user.uid == userId);
if (user) {
user.online = isOnline ? 1 : 0;
}
}
websocket.value.onerror = (error) => {
console.error('WebSocket error:', error)
}
websocket.value.onclose = () => {
kfInfo.value.online = 0
console.log('WebSocket connection closed')
}
}
const formatDate = (date) => {
return date.toLocaleDateString() //
}
const sendMessage = async () => {
currentUser.value.id = 1
if(!currentUser.value.id){
ElMessage.error('用户不存在')
return
}
if (!newMessage.value) {
ElMessage.error('请输入消息内容')
return
}
const message = {
sender: String(currentUser.value.id),
receiver: String(kfInfo.value.id),
msg_type: '1',
type: '1',
role: 'user',
timestamp: Math.floor(Date.now() / 1000),
add_time: Math.floor(Date.now() / 1000),
avatar_url: kfInfo.value.avatar,
nickname: kfInfo.value.nickname,
content: newMessage.value,
is_kf:0
}
try {
messages.value.push(message)
const response = await sendMessageData(message);
if(response.code !== 0){
ElMessage.error(response.msg)
return
}
newMessage.value = ''
await nextTick()
chatBox.value.wrapRef.scrollTop = chatBox.value.wrapRef.scrollHeight;
} catch (error) {
console.error('请求失败:', error);
}
}
const sendPing = () => {
setInterval(function () {
if(websocket.value && websocket.value.readyState === WebSocket.OPEN){
websocket.value.send(JSON.stringify({
type: 'ping',data: {timestamp:Date.now(),}
}))
}
},5000)
}
const getKfInfo = async () => {
const response = await getServiceInfo({uid:uid.value});
if(response.code === 0){
kfInfo.value = response.data
}else{
ElMessage.error(response.msg)
}
}
const getUserInfo = async () => {
const response = await getTestUserInfo({uid:uid.value});
if(response.code === 0){
currentUser.value = response.data
}else{
ElMessage.error(response.msg)
}
currentUser.value.id = 1
}
onMounted(async () => {
getQueryParams()
await getKfInfo()
await getUserInfo()
const res = await getMsgList({kf_id:kfInfo.value.id,uid:currentUser.value.id})
if(res.code === 0 && res.data.list.length > 0){
messages.value = [...res.data.list, ...messages.value]
}
initWebSocket()
sendPing()
})
const getQueryParams = () => {
const hash = window.location.hash
const queryString = hash.includes('?') ? hash.split('?')[1] : ''
const params = new URLSearchParams(queryString)
uid.value = params.get('uid')
}
const customUpload = async (response) => {
if(!currentUser.value.id){
ElMessage.error('请先选择用户')
return
}
const formData = new FormData();
console.log(response)
formData.append('file', response.file);
const res = await uploadFile(formData)
// 使URLURL
if (res.code===0) {
const message = {
sender: String(currentUser.value.id),
receiver: String(kfInfo.value.id),
msg_type: '3',
type: '3',
role: 'user',
timestamp: Math.floor(Date.now() / 1000),
add_time: Math.floor(Date.now() / 1000),
avatar_url: kfInfo.value.avatar,
nickname: kfInfo.value.nickname,
content: res.data,
is_kf:0
}
try {
messages.value.push(message)
const response2 = await sendMessageData(message);
if(response2.code !== 0){
ElMessage.error(response2.msg)
return
}
newMessage.value = ''
await nextTick()
chatBox.value.wrapRef.scrollTop = chatBox.value.wrapRef.scrollHeight
} catch (error) {
console.error('请求失败:', error)
}
}
}
const formatTimestamp2 = (timestamp) => {
const now = new Date();
const date = new Date(timestamp);
const todayStart = new Date(now.setHours(0, 0, 0, 0));
const yesterdayStart = new Date(todayStart);
yesterdayStart.setDate(yesterdayStart.getDate() - 1);
if (date >= todayStart) {
//
return date.toTimeString().slice(0, 5); // HH:mm
} else if (date >= yesterdayStart && date < todayStart) {
//
return '昨天';
} else {
//
const month = date.getMonth() + 1;
const day = date.getDate();
return `${month}-${day}`; // MM-DD
}
}
const getCookie = async () => {
token.value = Cookies.get('kf_token')
if(!token.value){
// window.open(window.location.protocol + '//' + window.location.host + '/#/kefu/login')
// return
router.push('/kefu/login')
}
else{
await getKfInfo()
await getMsgUsers()
initWebSocket()
sendPing()
}
console.log('Cookie value: ' + token.value)
}
//
const handleScroll = async (e) => {
if(e.scrollTop === 0){
await loadMessages(currentPage.value)
}
}
//
const loadMessages = async (page = 1) => {
try {
const response2 = await getMsgList({kf_id:kfInfo.value.id,uid:currentUser.value.uid,page:page})
if(response2.code === 0 && response2.data.list.length > 0){
messages.value = [...response2.data.list, ...messages.value]
currentPage.value++;
}
} catch (error) {
console.error('Failed to load messages:', error)
}
}
// WebSocket
onUnmounted(() => {
if (websocket.value) {
websocket.value.close()
}
})
const users = ref([])
const user = ref({
nickname:"",
avatar:""
});
const messages = ref([])
const newMessage = ref('')
const activeTab = ref('customer-info')
const emojis = ref(['😀', '😁', '😂', '🤣', '😃', '😄', '😅', '😆', '😉', '😊'])
const showEmojiPicker = ref(false)
//
const formatTimestamp = (timestamp) => {
const date = new Date(timestamp * 1000); //
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
}
const selectEmoji = (emoji) => {
newMessage.value += emoji;
showEmojiPicker.value = false;
}
</script>
<style scoped>
.fade-enter-active, .fade-leave-active {
transition: filter 0.5s;
}
.user-info span {
margin-left: 10px;
}
.chat-container {
display: flex;
flex-direction: column;
padding: 10px;
background: #f5f5f5;
}
.chat-box {
flex: 1;
overflow-y: auto;
margin-bottom: 10px;
}
.chat-message {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 20px;
}
.timestamp {
font-size: 0.75em;
color: #888;
margin-bottom: 5px;
text-align: center;
width: 100%;
}
.message-content {
display: flex;
align-items: center;
width: 95%;
}
.message-sent .message-content {
justify-content: flex-end;
}
.message-received .message-content {
justify-content: flex-start;
}
.chat-bubble {
max-width: 70%;
word-wrap: break-word;
padding: 10px;
}
.message-avatar {
margin-right: 10px;
}
.message-sent .message-avatar {
order: 2;
margin-left: 10px;
margin-right: 0;
}
.message-options {
display: flex;
justify-content: flex-start; /* Align buttons to the left */
margin-bottom: 10px;
}
.message-input {
margin-bottom: 10px;
}
.send-button {
align-self: flex-end;
}
.emoji-picker {
display: flex;
flex-wrap: wrap;
}
.emoji-picker span {
cursor: pointer;
margin: 5px;
font-size: 24px;
}
.avatar img {
width: 40px;
height: 40px;
border-radius: 50%;
}
.kf-background {
background-color: #95ec69;
border-radius: 10px;
}
.user-background {
background-color:#ffffff;
border-radius: 10px;
}
</style>

View File

@ -0,0 +1,30 @@
<template>
<div class="customerservice">
在此处书写页面代码
</div>
</template>
<script setup>
import { ref } from 'vue'
import { routerName } from '@/plugin/customerservice/api/api.js'
const data = ref({})
defineOptions({
name: 'CustomerService'
})
const useApi = async() =>{
const res = await routerName(data.value)
if(res.code === 0){
console.log(res.data)
}
}
</script>
<style lang="scss" scoped>
.customerservice {
}
</style>

View File

@ -0,0 +1,107 @@
<template>
<div class="sign-in">
<div class="header">
<h2>客服系统</h2>
</div>
<el-form :model="loginForm" class="login-form" :rules="formRule" ref="kfFormRef">
<el-form-item prop="account">
<el-input v-model="loginForm.account" placeholder="登录账户" size="large"></el-input>
</el-form-item>
<el-form-item prop="password">
<el-input v-model="loginForm.password" type="password" placeholder="密码" size="large"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleLogin" size="large">登录</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script setup>
import { reactive ,ref,onMounted} from 'vue'
import {accountServiceLogin} from "@/plugin/customerservice/api/api";
import Cookies from "js-cookie";
import { useRouter } from 'vue-router'
const kfFormRef = ref()
const router = useRouter()
const loginForm = ref({
account: '',
password: '',
})
//
const formRule = reactive({
account: [
{ required: true, message: '账户不能为空', trigger: 'change' }
],
password: [
{ required: true, message: '密码不能为空', trigger: 'change' }
],
})
const handleLogin = () => {
console.log('登录信息:', loginForm);
kfFormRef.value?.validate( async (valid) => {
if (!valid) return
const res = await accountServiceLogin(loginForm.value)
if (res.code === 0 && res.data.token) {
Cookies.set('kf_token', res.data.token, { expires: 3 })
router.push('/kefu/main')
}
})
}
onMounted(async () => {
let token = Cookies.get('kf_token')
if(token){
router.push('/kefu/main')
}
})
</script>
<style scoped>
.header {
text-align: center;
margin-bottom: 20px;
}
.login-form {
display: flex;
flex-direction: column;
}
.sign-in {
width: 380px;
padding: 20px;
margin:100px auto;
background: #fff;
-webkit-box-shadow: 0 1px 2px 0 rgba(101,129,156,.08);
box-shadow: 0 1px 2px 0 rgba(101,129,156,.08);
}
.sign-in h1,.sign-in h2,.sign-in .copyright{
font-weight: normal;
color: #4d627b;
text-align: center;
}
.sign-in .loginTitle{
font-size: 24px;
}
.sign-in .loginDesc{
font-size: 14px;
margin-bottom: 15px;
color: #409EFF;
text-decoration: none;
cursor: pointer;
}
.sign-in .copyright{
font-size: 12px;
}
@media (max-width: 768px) {
.sign-in{
width: 90%;
margin:40px auto;
background-color: #f5f5f5;
box-shadow:none;
}
}
</style>

View File

@ -0,0 +1,364 @@
<template>
<div>
<div class="gva-table-box">
<div class="gva-btn-list">
<el-button type="primary" icon="plus" @click="openDialog">新增</el-button>
</div>
<el-table
ref="multipleTable"
style="width: 100%"
tooltip-effect="dark"
:data="tableData"
row-key="uid"
>
<el-table-column align="left" label="ID" prop="id" width="120" />
<el-table-column align="left" label="回复类型" prop="reply_type" width="100" >
<template #default="scope">
{{replyType[scope.row.reply_type]}}
</template>
</el-table-column>
<el-table-column align="left" label="关键字" prop="keyword" width="120" />
<el-table-column align="left" label="回复内容" prop="content" width="150">
<template #default="scope">
<el-image
preview-teleported
style="width: 50px; height: 50px"
:src="scope.row.content"
fit="cover"
:preview-src-list="[scope.row.content]"
v-if="scope.row.reply_type === 2"
>
</el-image>
<span v-else>
{{scope.row.content}}
</span>
</template>
</el-table-column>
<el-table-column align="left" label="状态" prop="status" width="120">
<template #default="scope">
<el-tag type="success" v-if="scope.row.status === 1">正常</el-tag>
<el-tag type="danger" v-else>禁用</el-tag>
</template>
</el-table-column>
<el-table-column align="left" label="添加时间" prop="add_time_str" width="180">
</el-table-column>
<el-table-column align="left" label="操作" fixed="right" min-width="240">
<template #default="scope">
<el-button type="primary" link icon="edit" class="table-button" @click="updateScriptFunc(scope.row)">编辑</el-button>
<el-button type="primary" link icon="delete" @click="deleteRow(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="gva-pagination">
<el-pagination
layout="total, sizes, prev, pager, next, jumper"
:current-page="page"
:page-size="pageSize"
:page-sizes="[10, 30, 50, 100]"
:total="total"
@current-change="handleCurrentChange"
@size-change="handleSizeChange"
/>
</div>
</div>
<el-drawer destroy-on-close size="800" v-model="dialogFormVisible" :show-close="false" :before-close="closeDialog">
<template #header>
<div class="flex justify-between items-center">
<span class="text-lg">{{type==='create'?'添加':'修改'}}</span>
<div>
<el-button type="primary" @click="enterDialog"> </el-button>
<el-button @click="closeDialog"> </el-button>
</div>
</div>
</template>
<el-form :model="formData" label-position="top" ref="elFormRef" :rules="formRule" label-width="80px">
<el-form-item label="是否完全匹配:" prop="reply_type" >
<el-switch v-model="formData.is_complete" active-color="#13ce66" active-value="1" inactive-value="0" inactive-color="#ff4949" active-text="" inactive-text="" clearable ></el-switch>
</el-form-item>
<el-form-item label="关键字:" prop="keyword" >
<el-input v-model="formData.keyword" :clearable="true" placeholder="请输入关键字" />
</el-form-item>
<el-form-item label="回复类型:" prop="reply_type" >
<el-radio-group v-model="formData.reply_type">
<el-radio value="1" border>文本</el-radio>
<el-radio value="2" border>图片</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="回复内容:" prop="content" >
<template #default="scope">
<el-input v-model="formData.content" :clearable="true" placeholder="请输入内容" v-if="formData.reply_type==1"/>
<div class="selected-images" v-else>
<div class="selected-image" v-for="image in selectedImages" :key="image">
<el-image v-if="fileTypeList.includes(image.tag) === true" :src="image.url" style="width: 100%; height: 100%; object-fit: cover;margin-right: 10px;"></el-image>
<span class="remove-icon" @click="removeSelectedImage(image)"><el-icon><circle-close></circle-close></el-icon></span>
</div>
<el-icon v-if="isMultiple || selectedImages.length === 0" class="avatar-uploader-icon" @click="openImageLibrary"><Plus /></el-icon>
</div>
</template>
</el-form-item>
<el-form-item label="是否启用:" prop="status" >
<el-switch v-model="formData.status" active-color="#13ce66" active-value="1" inactive-value="0" inactive-color="#ff4949" active-text="" inactive-text="" clearable ></el-switch>
</el-form-item>
</el-form>
</el-drawer>
<el-dialog v-model="isDialogVisible" title="媒体库" width="950px" destroy-on-close>
<ImageLibrary @select="handleImageSelect" :multiple="isMultiple"/>
</el-dialog>
</div>
</template>
<script setup>
import {CircleClose, Plus} from '@element-plus/icons-vue'
//
import { getDictFunc, formatDate, formatBoolean, filterDict ,filterDataSource, ReturnArrImg, onDownloadFile } from '@/utils/format'
import { ElMessage, ElMessageBox } from 'element-plus'
import { ref, reactive } from 'vue'
import ImageLibrary from "@/plugin/picturelibrary/view/components/imageLibrary.vue";
import {
autoReplyList,
deleteReply,
findReply,
saveReply
} from "@/plugin/customerservice/api/api";
defineOptions({
name: 'ServiceIndex'
})
const elFormRef = ref()
const elSearchFormRef = ref()
// /
const showAllQuery = ref(false)
const isDialogVisible = ref(false)
const isMultiple = ref(false) //
const selectedImages = ref([])
const openImageLibrary = () => {
isDialogVisible.value = true
}
const fileTypeList = ['png', 'jpg', 'jpeg', 'gif']
const replyType = ref(['未知','文本','图片'])
const handleImageSelect = (images) => {
if (isMultiple.value) {
selectedImages.value = [...selectedImages.value, ...images]
} else {
selectedImages.value = Array.isArray(images) ? images : [images]
}
formData.value.content = selectedImages.value[0]['url']
isDialogVisible.value = false
}
const removeSelectedImage = (image) => {
const index = selectedImages.value.indexOf(image)
if (index !== -1) {
selectedImages.value.splice(index, 1)
}
}
//
const formData = ref({
id: 0,
reply_type: '1',
is_complete: '1',
Keyword: '',
content: '',
status: '1',
})
//
const formRule = reactive({
keyword: [
{ required: true, message: '话术关键字必须填写', trigger: 'change' }
],
content: [
{ required: true, message: '内容必须填写', trigger: 'change' }
],
})
// =========== ===========
const page = ref(1)
const total = ref(0)
const pageSize = ref(10)
const tableData = ref([])
//
const handleSizeChange = (val) => {
pageSize.value = val
getTableData()
}
//
const handleCurrentChange = (val) => {
page.value = val
getTableData()
}
//
const getTableData = async() => {
const table = await autoReplyList({ page: page.value, limit: pageSize.value, keyword:'' })
if (table.code === 0) {
tableData.value = table.data.list
total.value = table.data.total
page.value = table.data.page
pageSize.value = table.data.pageSize
}
}
getTableData()
// ============== ===============
//
const setOptions = async () =>{
}
//
setOptions()
//
const deleteRow = (row) => {
ElMessageBox.confirm('确定要删除吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
deleteSysMemberFunc(row)
})
}
//
const type = ref('')
//
const updateScriptFunc = async(row) => {
const res = await findReply({ id: row.id })
type.value = 'update'
if (res.code === 0) {
formData.value = res.data
formData.value.status = formData.value.status.toString()
formData.value.reply_type = formData.value.reply_type.toString()
formData.value.is_complete = formData.value.is_complete.toString()
dialogFormVisible.value = true
}
}
//
const deleteSysMemberFunc = async (row) => {
const res = await deleteReply({ id: row.id })
if (res.code === 0) {
ElMessage({
type: 'success',
message: '删除成功'
})
if (tableData.value.length === 1 && page.value > 1) {
page.value--
}
getTableData()
}
}
//
const dialogFormVisible = ref(false)
//
const openDialog = () => {
type.value = 'create'
dialogFormVisible.value = true
}
//
const closeDialog = () => {
dialogFormVisible.value = false
formData.value = {
id: 0,
reply_type: '1',
is_complete: '1',
Keyword: '',
content: '',
status: 0,
}
selectedImages.value = []
}
//
const enterDialog = async () => {
elFormRef.value?.validate( async (valid) => {
if (!valid) return
let res
let title
switch (type.value) {
case 'create':
title = '创建'
break
case 'update':
title = '更新'
break
default:
title = '创建'
break
}
formData.value.status = parseInt(formData.value.status,10)
formData.value.reply_type = parseInt(formData.value.reply_type,10)
formData.value.is_complete = parseInt(formData.value.is_complete,10)
res = await saveReply(formData.value)
if (res.code === 0) {
ElMessage({
type: 'success',
message: title
})
closeDialog()
getTableData()
}
})
}
</script>
<style>
.selected-images {
position: relative;
display: flex;
flex-wrap: wrap;
}
.selected-image {
position: relative;
margin-right: 10px;
margin-bottom:10px;
width: 100px;
height: 100px;
}
.selected-image .remove-icon {
position: absolute;
top: 0; /* 微调位置 */
right: 0; /* 微调位置 */
color: black;
padding: 5px;
cursor: pointer;
font-size: 22px;
line-height: 22px;
text-align: center;
}
.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 100px;
height: 100px;
text-align: center;
border: 1px dashed #d4d9e1;
}
</style>

View File

@ -0,0 +1,306 @@
<template>
<div>
<div class="gva-table-box">
<div class="gva-btn-list">
<el-button type="primary" icon="plus" @click="openDialog">新增</el-button>
</div>
<el-table
ref="multipleTable"
style="width: 100%"
tooltip-effect="dark"
:data="tableData"
row-key="uid"
>
<el-table-column align="left" label="ID" prop="id" width="120" />
<el-table-column align="left" label="标题" prop="title" width="120" />
<el-table-column align="left" label="详情" prop="content" width="200" />
<el-table-column align="left" label="排序" prop="sort" width="120" />
<el-table-column align="left" label="添加时间" prop="add_time_str" width="180">
</el-table-column>
<el-table-column align="left" label="操作" fixed="right" min-width="240">
<template #default="scope">
<el-button type="primary" link icon="edit" class="table-button" @click="updateScriptFunc(scope.row)">编辑</el-button>
<el-button type="primary" link icon="delete" @click="deleteRow(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="gva-pagination">
<el-pagination
layout="total, sizes, prev, pager, next, jumper"
:current-page="page"
:page-size="pageSize"
:page-sizes="[10, 30, 50, 100]"
:total="total"
@current-change="handleCurrentChange"
@size-change="handleSizeChange"
/>
</div>
</div>
<el-drawer destroy-on-close size="800" v-model="dialogFormVisible" :show-close="false" :before-close="closeDialog">
<template #header>
<div class="flex justify-between items-center">
<span class="text-lg">{{type==='create'?'添加':'修改'}}</span>
<div>
<el-button type="primary" @click="enterDialog"> </el-button>
<el-button @click="closeDialog"> </el-button>
</div>
</div>
</template>
<el-form :model="formData" label-position="top" ref="elFormRef" :rules="formRule" label-width="80px">
<el-form-item label="话术标题:" prop="title" >
<el-input v-model="formData.title" :clearable="true" placeholder="请输入标题" />
</el-form-item>
<el-form-item label="话术内容:" prop="content" >
<el-input v-model="formData.content" :clearable="true" placeholder="请输入内容" />
</el-form-item>
<el-form-item label="排序:" prop="sort" >
<el-input-number v-model="formData.sort" :clearable="true" placeholder="请输入排序"/>
</el-form-item>
</el-form>
</el-drawer>
<el-dialog v-model="isDialogVisible" title="媒体库" width="950px" destroy-on-close>
<ImageLibrary @select="handleImageSelect" :multiple="isMultiple"/>
</el-dialog>
</div>
</template>
<script setup>
import {CircleClose, Plus} from '@element-plus/icons-vue'
//
import { getDictFunc, formatDate, formatBoolean, filterDict ,filterDataSource, ReturnArrImg, onDownloadFile } from '@/utils/format'
import { ElMessage, ElMessageBox } from 'element-plus'
import { ref, reactive } from 'vue'
import ImageLibrary from "@/plugin/picturelibrary/view/components/imageLibrary.vue";
import {deleteScript, findScript, getScriptList, saveScript} from "@/plugin/customerservice/api/api";
defineOptions({
name: 'ServiceIndex'
})
const elFormRef = ref()
const elSearchFormRef = ref()
// /
const showAllQuery = ref(false)
const isDialogVisible = ref(false)
const isMultiple = ref(false) //
const selectedImages = ref([])
const openImageLibrary = () => {
isDialogVisible.value = true
}
const fileTypeList = ['png', 'jpg', 'jpeg', 'gif']
const handleImageSelect = (images) => {
if (isMultiple.value) {
selectedImages.value = [...selectedImages.value, ...images]
} else {
selectedImages.value = Array.isArray(images) ? images : [images]
}
formData.value.avatar = selectedImages.value[0]['url']
isDialogVisible.value = false
}
const removeSelectedImage = (image) => {
const index = selectedImages.value.indexOf(image)
if (index !== -1) {
selectedImages.value.splice(index, 1)
}
}
//
const formData = ref({
id: 0,
service_id: 0,
title: '',
content: '',
sort: 0,
})
//
const formRule = reactive({
title: [
{ required: true, message: '话术标题必须填写', trigger: 'change' }
],
content: [
{ required: true, message: '话术内容必须填写', trigger: 'change' }
],
})
// =========== ===========
const page = ref(1)
const total = ref(0)
const pageSize = ref(10)
const tableData = ref([])
//
const handleSizeChange = (val) => {
pageSize.value = val
getTableData()
}
//
const handleCurrentChange = (val) => {
page.value = val
getTableData()
}
//
const getTableData = async() => {
const table = await getScriptList({ page: page.value, limit: pageSize.value, keyword:'' })
if (table.code === 0) {
tableData.value = table.data.list
total.value = table.data.total
page.value = table.data.page
pageSize.value = table.data.pageSize
}
}
getTableData()
// ============== ===============
//
const setOptions = async () =>{
}
//
setOptions()
//
const deleteRow = (row) => {
ElMessageBox.confirm('确定要删除吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
deleteSysMemberFunc(row)
})
}
//
const type = ref('')
//
const updateScriptFunc = async(row) => {
const res = await findScript({ id: row.id })
type.value = 'update'
if (res.code === 0) {
formData.value = res.data
dialogFormVisible.value = true
}
}
//
const deleteSysMemberFunc = async (row) => {
const res = await deleteScript({ id: row.id })
if (res.code === 0) {
ElMessage({
type: 'success',
message: '删除成功'
})
if (tableData.value.length === 1 && page.value > 1) {
page.value--
}
getTableData()
}
}
//
const dialogFormVisible = ref(false)
//
const openDialog = () => {
type.value = 'create'
dialogFormVisible.value = true
}
//
const closeDialog = () => {
dialogFormVisible.value = false
formData.value = {
id: 0,
service_id: 0,
title: '',
content: '',
sort: 0,
}
selectedImages.value = []
}
//
const enterDialog = async () => {
elFormRef.value?.validate( async (valid) => {
if (!valid) return
let res
let title
switch (type.value) {
case 'create':
title = '创建'
break
case 'update':
title = '更新'
break
default:
title = '创建'
break
}
res = await saveScript(formData.value)
if (res.code === 0) {
ElMessage({
type: 'success',
message: title
})
closeDialog()
getTableData()
}
})
}
</script>
<style>
.selected-images {
position: relative;
display: flex;
flex-wrap: wrap;
}
.selected-image {
position: relative;
margin-right: 10px;
margin-bottom:10px;
width: 100px;
height: 100px;
}
.selected-image .remove-icon {
position: absolute;
top: 0; /* 微调位置 */
right: 0; /* 微调位置 */
color: black;
padding: 5px;
cursor: pointer;
font-size: 22px;
line-height: 22px;
text-align: center;
}
.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 100px;
height: 100px;
text-align: center;
border: 1px dashed #d4d9e1;
}
</style>

View File

@ -0,0 +1,399 @@
<template>
<div>
<div class="gva-table-box">
<div class="gva-btn-list">
<el-button type="primary" icon="plus" @click="openDialog">新增</el-button>
</div>
<el-table
ref="multipleTable"
style="width: 100%"
tooltip-effect="dark"
:data="tableData"
row-key="uid"
>
<el-table-column align="left" label="ID" prop="id" width="120" />
<el-table-column label="客服头像" width="100">
<template #default="scope">
<el-image
preview-teleported
style="width: 50px; height: 50px"
:src="scope.row.avatar"
fit="cover"
:preview-src-list="[scope.row.avatar]"
>
</el-image>
</template>
</el-table-column>
<el-table-column align="left" label="关联用户id" prop="uid" width="120" />
<el-table-column align="left" label="客服名称" prop="nickname" width="120" />
<el-table-column align="left" label="客服账户" prop="account" width="120" />
<el-table-column align="left" label="客服状态" prop="status" width="120">
<template #default="scope">
<el-tag type="success" v-if="scope.row.status === 1">正常</el-tag>
<el-tag type="danger" v-else>禁用</el-tag>
</template>
</el-table-column>
<el-table-column align="left" label="操作" fixed="right" min-width="240">
<template #default="scope">
<el-button type="primary" link icon="edit" class="table-button" @click="updateServiceFunc(scope.row)">编辑</el-button>
<el-button type="primary" link icon="delete" @click="deleteRow(scope.row)">删除</el-button>
<el-button type="primary" link icon="edit" v-if="scope.row.status === 1" class="table-button" @click="goChat(scope.row)">进入工作台</el-button>
</template>
</el-table-column>
</el-table>
<div class="gva-pagination">
<el-pagination
layout="total, sizes, prev, pager, next, jumper"
:current-page="page"
:page-size="pageSize"
:page-sizes="[10, 30, 50, 100]"
:total="total"
@current-change="handleCurrentChange"
@size-change="handleSizeChange"
/>
</div>
</div>
<el-drawer destroy-on-close size="800" v-model="dialogFormVisible" :show-close="false" :before-close="closeDialog">
<template #header>
<div class="flex justify-between items-center">
<span class="text-lg">{{type==='create'?'添加':'修改'}}</span>
<div>
<el-button type="primary" @click="enterDialog"> </el-button>
<el-button @click="closeDialog"> </el-button>
</div>
</div>
</template>
<el-form :model="formData" label-position="top" ref="elFormRef" :rules="formRule" label-width="80px">
<el-form-item label="客服头像:" prop="avatar">
<div class="selected-images">
<div class="selected-image" v-for="image in selectedImages" :key="image">
<el-image v-if="fileTypeList.includes(image.tag) === true" :src="image.url" style="width: 100%; height: 100%; object-fit: cover;margin-right: 10px;"></el-image>
<video v-else controls style="width: 100%; height: 100%;">
<source :src="image.url" />
</video>
<span class="remove-icon" @click="removeSelectedImage(image)"><el-icon><circle-close></circle-close></el-icon></span>
</div>
<el-icon v-if="isMultiple || selectedImages.length === 0" class="avatar-uploader-icon" @click="openImageLibrary"><Plus /></el-icon>
</div>
</el-form-item>
<el-form-item label="关联UID:" prop="uid" >
<el-input-number v-model="formData.uid" :clearable="true" placeholder="请输入UID" />
</el-form-item>
<el-form-item label="客服名称:" prop="nickname" >
<el-input v-model="formData.nickname" :clearable="true" placeholder="请输入客服名称" />
</el-form-item>
<el-form-item label="客服账户:" prop="account" >
<el-input v-model="formData.account" :clearable="true" placeholder="请输入客服账户" />
</el-form-item>
<el-form-item label="账户密码:" prop="password" >
<el-input v-model="formData.password" :clearable="true" :placeholder="type==='create'?'请输入账户密码':'不修改请留空'"/>
</el-form-item>
<el-form-item label="确认密码:" prop="password2" >
<el-input v-model="formData.password2" :clearable="true" :placeholder="type==='create'?'请输入确认密码':'不修改请留空'" />
</el-form-item>
<el-form-item label="是否启用:" prop="status" >
<el-switch v-model="formData.status" active-color="#13ce66" active-value="1" inactive-value="0" inactive-color="#ff4949" active-text="" inactive-text="" clearable ></el-switch>
</el-form-item>
</el-form>
</el-drawer>
<el-dialog v-model="isDialogVisible" title="媒体库" width="950px" destroy-on-close>
<ImageLibrary @select="handleImageSelect" :multiple="isMultiple"/>
</el-dialog>
</div>
</template>
<script setup>
import {CircleClose, Plus} from '@element-plus/icons-vue'
//
import { getDictFunc, formatDate, formatBoolean, filterDict ,filterDataSource, ReturnArrImg, onDownloadFile } from '@/utils/format'
import { ElMessage, ElMessageBox } from 'element-plus'
import { ref, reactive } from 'vue'
import Cookies from 'js-cookie'
import ImageLibrary from "@/plugin/picturelibrary/view/components/imageLibrary.vue";
import {deleteService, findService, getServiceList, saveService,adminServiceLogin} from "@/plugin/customerservice/api/api";
defineOptions({
name: 'ServiceIndex'
})
const elFormRef = ref()
const elSearchFormRef = ref()
// /
const showAllQuery = ref(false)
const isDialogVisible = ref(false)
const isMultiple = ref(false) //
const selectedImages = ref([])
const openImageLibrary = () => {
isDialogVisible.value = true
}
const fileTypeList = ['png', 'jpg', 'jpeg', 'gif']
const handleImageSelect = (images) => {
if (isMultiple.value) {
selectedImages.value = [...selectedImages.value, ...images]
} else {
selectedImages.value = Array.isArray(images) ? images : [images]
}
formData.value.avatar = selectedImages.value[0]['url']
isDialogVisible.value = false
}
const removeSelectedImage = (image) => {
const index = selectedImages.value.indexOf(image)
if (index !== -1) {
selectedImages.value.splice(index, 1)
}
}
//
const formData = ref({
id: 0,
uid: 0,
account: '',
password: '',
password2: '',
nickname: '',
avatar: '',
status: 0,
})
//
const formRule = reactive({
avatar: [
{ required: true, message: '客服头像必须选择', trigger: 'change' }
],
nickname: [
{ required: true, message: '客服账户必须填写', trigger: 'change' }
],
uid: [
{ required: true, message: '关联用户id必须填写', trigger: 'change' }
],
account: [
{ required: true, message: '客服账户必须填写', trigger: 'change' }
],
password: [
{ validator: (rule, value, callback) => {
if (!formData.value.password && type.value !== 'update') {
callback(new Error('密码必须填写'))
} else {
callback()
}
}, trigger: 'change' }
],
password2: [
{ validator: (rule, value, callback) => {
if (formData.value.password2 !== formData.value.password && type.value !== 'update') {
callback(new Error('两次密码输入不一致'))
} else {
callback()
}
}, trigger: 'change' }
],
})
// =========== ===========
const page = ref(1)
const total = ref(0)
const pageSize = ref(10)
const tableData = ref([])
//
const onSubmit = () => {
elSearchFormRef.value?.validate(async(valid) => {
if (!valid) return
page.value = 1
pageSize.value = 10
getTableData()
})
}
//
const handleSizeChange = (val) => {
pageSize.value = val
getTableData()
}
//
const handleCurrentChange = (val) => {
page.value = val
getTableData()
}
//
const getTableData = async() => {
const table = await getServiceList({ page: page.value, limit: pageSize.value, keyword:'' })
if (table.code === 0) {
tableData.value = table.data.list
total.value = table.data.total
page.value = table.data.page
pageSize.value = table.data.pageSize
}
}
getTableData()
// ============== ===============
//
const setOptions = async () =>{
}
//
setOptions()
//
const deleteRow = (row) => {
ElMessageBox.confirm('确定要删除吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
deleteSysMemberFunc(row)
})
}
//
const type = ref('')
//
const updateServiceFunc = async(row) => {
const res = await findService({ id: row.id })
type.value = 'update'
if (res.code === 0) {
formData.value = res.data
selectedImages.value[0] = { url: formData.value.avatar, tag: 'jpg' }
formData.value.status = formData.value.status.toString()
dialogFormVisible.value = true
}
}
const goChat = async (row) => {
const res = await adminServiceLogin({id:row.id})
if (res.code === 0 && res.data.token) {
Cookies.set('kf_token', res.data.token, { expires: 3 });
window.open(window.location.protocol + '//' + window.location.host + '/#/kefu/main', '_blank');
}
}
//
const deleteSysMemberFunc = async (row) => {
const res = await deleteService({ id: row.id })
if (res.code === 0) {
ElMessage({
type: 'success',
message: '删除成功'
})
if (tableData.value.length === 1 && page.value > 1) {
page.value--
}
getTableData()
}
}
//
const dialogFormVisible = ref(false)
//
const openDialog = () => {
type.value = 'create'
dialogFormVisible.value = true
}
//
const closeDialog = () => {
dialogFormVisible.value = false
formData.value = {
id: 0,
uid: 0,
account: '',
password: '',
password2: '',
nickname: '',
avatar: '',
status: 0,
}
selectedImages.value = []
}
//
const enterDialog = async () => {
elFormRef.value?.validate( async (valid) => {
if (!valid) return
let res
let title
switch (type.value) {
case 'create':
title = '创建'
break
case 'update':
title = '更新'
break
default:
title = '创建'
break
}
formData.value.status = parseInt(formData.value.status,10)
res = await saveService(formData.value)
if (res.code === 0) {
ElMessage({
type: 'success',
message: title
})
closeDialog()
getTableData()
}
})
}
</script>
<style>
.selected-images {
position: relative;
display: flex;
flex-wrap: wrap;
}
.selected-image {
position: relative;
margin-right: 10px;
margin-bottom:10px;
width: 100px;
height: 100px;
}
.selected-image .remove-icon {
position: absolute;
top: 0; /* 微调位置 */
right: 0; /* 微调位置 */
color: black;
padding: 5px;
cursor: pointer;
font-size: 22px;
line-height: 22px;
text-align: center;
}
.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 100px;
height: 100px;
text-align: center;
border: 1px dashed #d4d9e1;
}
</style>

View File

@ -0,0 +1,55 @@
import service from '@/utils/request'
// @Router /pic_library/list
export const getFileListApi = (data) => {
return service({
url: '/pic/pic_library/list',
method: 'post',
data
})
}
// @Router /pic_library/cat_list
export const getCategoryListApi = (data) => {
return service({
url: '/pic/pic_library/cat_list',
method: 'post',
data
})
}
// @Router /pic_library/add_cat
export const addCategoryApi = (data) => {
return service({
url: '/pic/pic_library/add_cat',
method: 'post',
data
})
}
// @Router /pic_library/upload_handler
export const uploadHandlerApi = (data) => {
return service({
url: '/pic/pic_library/upload_handler',
method: 'post',
data
})
}
// @Router /pic_library/delete_file
export const deleteFilesApi = (data) => {
return service({
url: '/pic/pic_library/delete_file',
method: 'post',
data
})
}
// @Router /pic_library/delete_cat
export const deleteCategoryApi = (data) => {
return service({
url: '/pic/pic_library/delete_cat',
method: 'post',
data
})
}

View File

@ -0,0 +1,456 @@
<template>
<div class="image-library">
<div class="image-library-2" style="width: 180px;;border-right: solid 1px var(--el-border-color);">
<el-tree
:data="categories"
node-key="id"
:props="defaultProps"
@node-click="handleNodeClick"
>
<template #default="{ node, data }">
<span>{{ data.name }}</span>
<span style="float: right; margin-left: 25px">
<el-dropdown @command="(command) => handleCommand(data, command)">
<span class="el-dropdown-link"><b>...</b></span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="add">添加分类</el-dropdown-item>
<el-dropdown-item command="edit" v-if="data.id > 0">编辑分类</el-dropdown-item>
<el-dropdown-item command="delete" v-if="data.id > 0">删除分类</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</span>
</template>
</el-tree>
</div>
<div>
<div class="top-buttons" style="margin-left: 15px;">
<el-button @click="useSelectedImages" type="primary">使用选中的文件</el-button>
<el-button @click="openUploadDialog" type="primary">上传文件</el-button>
<el-button @click="confirmDeleteImages">删除文件</el-button>
<el-input
v-model="keyword"
class="keyword"
placeholder="请输入文件名"
style="margin-left: 10px;"
/>
<el-button
type="primary"
icon="search"
@click="getSearchData"
>查询</el-button>
</div>
<div class="flex flex-wrap gap-4">
<div v-for="image in images" :key="image.ID" class="block">
<el-image v-if="image.tag !== 'mp4'" :src="image.url" @click="toggleImageSelection(image)" :class="{ selected: isSelected(image) }" style="width: 96px;height: 96px;"></el-image>
<video v-else controls style="width: 96%; height: 96px;" @click="toggleImageSelection(image)" :class="{ selected: isSelected(image) }">
<source :src="image.url" />
</video>
<div class="overflow-hidden text-nowrap overflow-ellipsis text-center w-full">{{image.name}}</div>
</div>
</div>
<el-pagination @current-change="pageChange" background layout="prev, pager, next" :page-size="18" :total="totalPage" style="float: right"/>
</div>
</div>
<!-- 添加分类弹窗 -->
<el-dialog v-model="isAddCategoryDialogVisible" @close="closeAddCategoryDialog" title="添加分类">
<el-form>
<el-form-item label="上级分类">
<el-tree-select
v-model="cat_id"
:data="categories"
check-strictly
:props="defaultProps"
:render-after-expand="false"
style="width: 240px"
/>
</el-form-item>
<el-form-item label="分类名称">
<el-input v-model="cat_name" placeholder="分类名称"></el-input>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="closeAddCategoryDialog">取消</el-button>
<el-button type="primary" @click="confirmAddCategory">确定</el-button>
</template>
</el-dialog>
<!-- 删除确认弹窗 -->
<el-dialog v-model="isDeleteImagesDialogVisible" @close="closeDeleteImagesDialog" title="删除文件" width="350">
<span>确定要删除选中的文件吗</span>
<template #footer>
<el-button @click="closeDeleteImagesDialog">取消</el-button>
<el-button type="danger" @click="deleteImages">确定</el-button>
</template>
</el-dialog>
<!-- 上传图片弹窗 -->
<el-dialog v-model="isUploadDialogVisible" title="上传文件">
<el-form>
<el-form-item label="上传分类">
<el-tree-select
v-model="selectedCategory"
check-strictly
:data="categories"
node-key="id"
:props="defaultProps"
placeholder="请选择分类"
>
<template #default="{ node, data }">
<span>{{ data.name }}</span>
</template>
</el-tree-select>
</el-form-item>
<el-form-item label="选择文件">
<el-upload
ref="uploadRef"
action=""
multiple
:on-change="handleChange"
:on-remove="handleRemove"
:file-list="fileList"
list-type="picture-card"
:auto-upload="false">
<el-icon><Plus /></el-icon>
</el-upload>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="closeUploadDialog">取消</el-button>
<el-button type="primary" @click="submitUpload">确定</el-button>
</template>
</el-dialog>
</template>
<script setup>
import { ref, defineEmits, onMounted } from 'vue';
import { ElMessage } from 'element-plus';
import { Plus } from '@element-plus/icons-vue'
import { getCategoryListApi,getFileListApi,addCategoryApi,deleteCategoryApi,deleteFilesApi,uploadHandlerApi } from '@/plugin/picturelibrary/api/api.js'
import {getUrl, isVideoExt} from "@/utils/image";
const emit = defineEmits(['select']);
const cat_id = ref(0)
const selectedImage = ref(null);
const selectedImages = ref([]);
const images = ref([])
const categories = ref([])
const isAddCategoryDialogVisible = ref(false)
const cat_name = ref('')
const isDeleteImagesDialogVisible = ref(false)
const isUploadDialogVisible = ref(false)
const selectedCategory = ref(null)
const fileList = ref([])
const selectedParentCategory = ref(null)
const totalPage = ref(0)
const editId = ref(0)
const searchCatId = ref(0)
const page = ref(1)
const pageSize = ref(18)
const keyword = ref('')
const props = defineProps({
multiple: Boolean, // multiple
});
const defaultProps = {
children: 'children',
label: 'name',
value: 'id'
}
const pageChange = (index) => {
page.value = index
getPicList()
}
const getSearchData = () => {
getPicList()
}
const getPicList = async () => {
try {
const response = await getFileListApi({page:page.value,limit:pageSize.value,cid:searchCatId.value,keyword:keyword.value})
if(response.code === 0){
if(response.data.list){
images.value = response.data.list
totalPage.value = response.data.total
}
}
else{
ElMessage.error(response.msg);
}
} catch (error) {
ElMessage.error('请求失败');
}
}
const fetchCategories = async (name = '顶级分类') => {
let data = {
name: name,
id: 0,
pid: 0,
children:[]
}
try {
const response = await getCategoryListApi({})
if(response.code === 0){
if(response.data.list){
response.data.list.unshift(data)
categories.value = response.data.list
}
else{
categories.value = [data]
}
}
else{
ElMessage.error('分类数据获取失败');
}
} catch (error) {
console.error('Error fetching categories:', error)
}
}
onMounted(() => {
fetchCategories()
getPicList()
})
const toggleImageSelection = (image) => {
const index = selectedImages.value.findIndex(img => img.ID === image.ID);
if (index > -1) {
selectedImages.value.splice(index, 1);
} else {
selectedImages.value.push(image);
}
}
const isSelected = (image) => {
return selectedImages.value.some(img => img.ID === image.ID);
}
const useSelectedImages = () => {
if (selectedImages.value.length > 0) {
if(!props.multiple && selectedImages.value.length > 1){
ElMessage.error('只能选择一个文件');
return
}
const urls = selectedImages.value.map(img => img);
emit('select', urls);
selectedImages.value = []
} else {
ElMessage.error('请选择一个或多个文件');
}
}
const openUploadDialog = () => {
isUploadDialogVisible.value = true
}
const closeUploadDialog = () => {
isUploadDialogVisible.value = false
selectedCategory.value = null
fileList.value = []
}
const fileTypeList = ref(['image/gif', 'image/jpeg', 'image/png', 'video/mp4', 'video/x-msvideo', 'video/x-ms-wmv','video/mpeg','video/x-ms-asf'])
const submitUpload = async () => {
if (selectedCategory.value !== null && fileList.value.length > 0) {
try {
const formData = new FormData();
fileList.value.forEach(file => {
formData.append('files', file.raw);
});
formData.append('cat_id', selectedCategory.value);
try {
const response = await uploadHandlerApi(formData);
if(response.code === 0){
ElMessage.success('文件上传成功');
closeUploadDialog();
selectedCategory.value = null
}
else{
ElMessage.error('文件上传失败');
}
} catch (error) {
ElMessage.error('文件上传失败');
}
} catch (error) {
ElMessage.error('上传失败,请重试');
}
} else {
ElMessage.error('请选择分类和文件');
}
}
const deleteImages = async () => {
if (selectedImages.value.length > 0) {
const response = await deleteFilesApi(selectedImages.value)
if(response.code === 0){
ElMessage.success('删除成功');
selectedImages.value = [];
isDeleteImagesDialogVisible.value = false;
getPicList()
}
else{
ElMessage.error('文件删除失败');
isDeleteImagesDialogVisible.value = false;
}
} else {
ElMessage.error('请选择一张或多张图片');
}
}
const confirmDeleteImages = () => {
if (selectedImages.value.length > 0) {
isDeleteImagesDialogVisible.value = true;
} else {
ElMessage.error('请选择一个或多个文件');
}
}
const addCategory = (cat) => {
isAddCategoryDialogVisible.value = true;
cat_id.value = cat.id
}
const editCategory = (category) => {
cat_id.value = category.pid
cat_name.value = category.name
editId.value = category.id
isAddCategoryDialogVisible.value = true;
}
const deleteCategory = async (category) => {
try {
const response = await deleteCategoryApi({cat_id:category.id})
if(response.code === 0){
ElMessage.success('删除成功');
fetchCategories()
getPicList()
}
else{
ElMessage.error(response.data.msg);
}
} catch (error) {
ElMessage.error('删除分类失败,请重试');
}
}
const closeAddCategoryDialog = () => {
isAddCategoryDialogVisible.value = false
cat_name.value = ''
selectedParentCategory.value = null
}
const confirmAddCategory = async () => {
if (cat_name.value.trim()) {
const newCategory = {
name: cat_name.value.trim(),
pid: cat_id.value,
id:editId.value
};
try {
const response = await addCategoryApi(newCategory);
if(response.code === 0){
ElMessage.success('添加成功');
fetchCategories()
getPicList()
closeAddCategoryDialog();
}
else{
ElMessage.error(response.msg);
}
} catch (error) {
ElMessage.error('分类添加失败,请重试');
}
} else {
ElMessage.error('分类名称不能为空');
}
}
const handleNodeClick = (node) => {
searchCatId.value = node.id
getPicList()
}
const handleCommand = (category,command) => {
if (command === 'add') {
addCategory(category);
} else if (command === 'edit') {
editCategory(category);
} else if (command === 'delete') {
deleteCategory(category);
}
}
const handleChange = (uploadFile, uploadFiles) => {
uploadFiles.forEach(file => {
if(fileTypeList.value.includes(file.raw.type) === false){
ElMessage.error('图片或视频格式有误');
fileList.value = []
}
else{
fileList.value = uploadFiles;
console.log('fileList:', fileList.value.length);
}
});
}
const handleRemove = (file, fileList) => {
// fileList.value = fileList;
console.log('Remove:', file, fileList);
}
const closeDeleteImagesDialog = () => {
isDeleteImagesDialogVisible.value = false
}
const dummyRequest = ({ onSuccess }) => {
setTimeout(() => {
onSuccess('ok');
}, 1000);
}
const uploadRef = ref(null)
</script>
<style scoped>
.image-library {
display: flex;
}
.image-library-2 {
flex: 0 0 auto;
}
.el-dropdown-link {
cursor: pointer;
}
.top-buttons {
display: flex;
justify-content: flex-start;
margin-bottom: 20px;
}
.selected {
border: 3px solid #409eff;
}
.block {
display: flex;
flex-wrap: wrap;
box-sizing: border-box;
vertical-align: top;
padding: 2px;
width: 100px;
height: 100px;
margin-left: 15px;
}
</style>

View File

@ -0,0 +1,30 @@
<template>
<div class="picturelibrary">
在此处书写页面代码
</div>
</template>
<script setup>
import { ref } from 'vue'
import { routerName } from '@/plugin/picturelibrary/api/api.js'
const data = ref({})
defineOptions({
name: 'PictureLibrary'
})
const useApi = async() =>{
const res = await routerName(data.value)
if(res.code === 0){
console.log(res.data)
}
}
</script>
<style lang="scss" scoped>
.picturelibrary {
}
</style>

View File

@ -31,6 +31,30 @@ const routes = [
},
component: () => import('@/view/error/index.vue')
},
{
path: '/kefu/login',
name: 'ServiceLogin',
component: () => import('@/plugin/customerservice/view/login/index.vue'),
meta:{
client:true
}
},
{
path: '/kefu/main',
name: 'ServiceMain',
component: () => import('@/plugin/customerservice/view/chat/index.vue'),
meta:{
client:true
}
},
{
path: '/kefu/test',
name: 'ServiceUserTest',
component: () => import('@/plugin/customerservice/view/chat/test.vue'),
meta:{
client:true
}
},
]
const router = createRouter({