🎨 添加图库和客服插件(有问题-待修改)
This commit is contained in:
parent
e6ceacfae1
commit
0ed234639c
@ -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"
|
||||
}
|
113
src/plugin/customerservice/api/api.js
Normal file
113
src/plugin/customerservice/api/api.js
Normal 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
|
||||
})
|
||||
}
|
61
src/plugin/customerservice/api/test.js
Normal file
61
src/plugin/customerservice/api/test.js
Normal 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
|
||||
})
|
||||
}
|
76
src/plugin/customerservice/api/ws.js
Normal file
76
src/plugin/customerservice/api/ws.js
Normal 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
|
||||
})
|
||||
}
|
807
src/plugin/customerservice/view/chat/index.vue
Normal file
807
src/plugin/customerservice/view/chat/index.vue
Normal 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)
|
||||
// 使用上传后的文件URL(例如,显示图片或保存URL)
|
||||
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>
|
497
src/plugin/customerservice/view/chat/test.vue
Normal file
497
src/plugin/customerservice/view/chat/test.vue
Normal 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)
|
||||
// 使用上传后的文件URL(例如,显示图片或保存URL)
|
||||
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>
|
30
src/plugin/customerservice/view/index.vue
Normal file
30
src/plugin/customerservice/view/index.vue
Normal 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>
|
107
src/plugin/customerservice/view/login/index.vue
Normal file
107
src/plugin/customerservice/view/login/index.vue
Normal 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>
|
364
src/plugin/customerservice/view/reply/index.vue
Normal file
364
src/plugin/customerservice/view/reply/index.vue
Normal 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>
|
306
src/plugin/customerservice/view/script/index.vue
Normal file
306
src/plugin/customerservice/view/script/index.vue
Normal 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>
|
399
src/plugin/customerservice/view/service/index.vue
Normal file
399
src/plugin/customerservice/view/service/index.vue
Normal 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>
|
55
src/plugin/picturelibrary/api/api.js
Normal file
55
src/plugin/picturelibrary/api/api.js
Normal 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
|
||||
})
|
||||
}
|
456
src/plugin/picturelibrary/view/components/imageLibrary.vue
Normal file
456
src/plugin/picturelibrary/view/components/imageLibrary.vue
Normal 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>
|
30
src/plugin/picturelibrary/view/index.vue
Normal file
30
src/plugin/picturelibrary/view/index.vue
Normal 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>
|
@ -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({
|
||||
|
Loading…
Reference in New Issue
Block a user