提交 749e555b 作者: Hao

add

父级 e861a5d9
<template>
<div>
<header>
<div class="operation">
<el-button @click="changeLang('zhcn')" class="mr10">中文</el-button>
<el-button @click="changeLang('en')" class="mr10">英文</el-button>
<el-button @click="changeLang('ko')" class="mr10">韩文</el-button>
<el-button @click="changeLang('ja')" class="mr10">日文</el-button>
</div>
</header>
<router-view />
</div>
</template>
<script setup lang="ts">
const changeLang = (value:any)=> {
localStorage.setItem('lang', value)
location.reload();
}
const changeLang = (value: any) => {
localStorage.setItem('lang', value)
location.reload()
}
</script>
<style lang="scss">
* {
......
// axios.js
import axios from "axios";
const instance = axios.create({
baseURL: "http://192.168.31.228:8080", // 设置基础 URL
baseURL: "http://192.168.31.123:8080", // 设置基础 URL
// baseURL:'/api',
timeout: 1000, // 设置请求超时时间
});
......
......@@ -27,7 +27,7 @@ const Kjiehuifu = (query: any) => {
//获取用户列表
const getUserList = (query: any) => {
return http({
url: "/getUserList",
url: "/kf/chat/getChatList",
method: "get",
data: query,
});
......@@ -35,9 +35,9 @@ const getUserList = (query: any) => {
//查看未读消息
const checkMesssages = (query: any) => {
return http({
url: "/checkMesssages",
url: "/kf/chat/getChatHisList",
method: "get",
data: query,
params: query,
});
};
//上传接口
......
<template>
<div>
<div class="auto-prompt">
<div class="chat-textarea">
<div class="chat-bar">
<div>
......@@ -31,36 +31,19 @@
快捷回复
</div>
</div>
<!-- <el-autocomplete
class="autocomplete"
v-model="state"
:fetch-suggestions="querySearchAsync"
placeholder="请输入问题"
type="textarg"
@select="handleSelect"
ref="automaticPromptRef"
/> -->
<!-- <el-input
v-model="state"
ref="automaticPromptRef"
class="el-autocomplete"
placeholder="输入信息"
type="textarea"
resize="none"
@change="handleSelect"
/> -->
<div
class="el-autocomplete"
class="el-autocomplete textarea"
id="el-autocomplete"
ref="el-autocomplete"
ref="elAutocomplete"
placeholder="输入信息"
contenteditable="true"
style="outline: none;height: 150px;"
@paste="onPaste"
style="outline: none; height: 100%;"
@paste.prevent="handlePaste"
@focus="removeDefaultContent"
@input="handleSelect"
>
</div>
@blur="addDefaultContent"
></div>
</div>
<emotion
v-if="showEmotion"
style="position: absolute; top: calc(100% - 465px); background: #fff;"
......@@ -78,229 +61,180 @@
</template>
<script lang="ts" setup>
import Emotion from './IndexComponent.vue'
import emotion from './IndexComponent.vue'
import { ElLoading } from 'element-plus'
import { ref, watch, defineEmits, defineExpose } from 'vue'
import { upLoadFilesHander } from '../minxins/UploadMixin'
import axios from 'axios'
const state = ref('')
const elAutocomplete = ref(null)
const file = ref(null)
interface LinkItem {
value: string
link: string
}
// uploadMixin()
const inputVal = ref('')
const elAutocomplete = ref<any>(null)
const file = ref<any>(null)
const imgShowWidth = ref<number>(50)
const imgShowHeight = ref<number>(50)
const showEmotion = ref<boolean>(false)
const automaticPromptRef = ref(null)
const links = ref<LinkItem[]>([])
const handleEmotion = (i) => {
state.value += i
const handleEmotion = (i: any) => {
elAutocomplete.value.innerHTML += i
inputVal.value = elAutocomplete.value.innerHTML
showEmotion.value = false
automaticPromptRef.value.focus()
elAutocomplete.value.focus()
}
const upfile = () => {
file.value.click()
}
//上传的数据
const fileChange = () => {
var e = window.event || event
var e: any = window.event || event
var oFile = e.target.files[0]
upLoadFilesHander(oFile).then((ress) => {
const { success, result } = ress
console.log(result)
const { success, result }: any = ress
})
const loading = ElLoading.service({
const loading: any = ElLoading?.service({
lock: true,
text: '上传中...',
spinner: 'el-icon-loading',
})
loading.close()
}
const showWord = () => {
console.log(3)
}
// const loadFromBackend = async (value: string) => {
// try {
// //输入时候请求后端根据输入值得到提示。 后端返回集合,集合里面对象属性为value和link都是string类型
// const response = await axios.get(
// `http://localhost:8080/getAutoMsg/${value}`,
// )
// links.value = response.data
// } catch (error) {
// console.error(error)
// }
// }
const querySearchAsync = (queryString: string, cb: (arg: any) => void) => {
const results = queryString
? links.value.filter(createFilter(queryString))
: links.value
cb(results)
}
const createFilter = (queryString: string) => {
return (link: LinkItem) => {
return link.value.toLowerCase().indexOf(queryString.toLowerCase()) === 0
}
}
const handleSelect = (value: string) => {
state.value = value.target.innerHTML
const handleSelect = (value: any) => {
inputVal.value = value.target.innerHTML
}
const emit = defineEmits(['updateState'])
watch(state, async (newValue) => {
if(!newValue) document.getElementById('el-autocomplete').innerHTML = ''
watch(inputVal, async (newValue) => {
if (!newValue) elAutocomplete.value.innerHTML = ''
emit('updateState', newValue)
})
const getPaste = (e: any) => {
console.log(e, '粘贴')
// 如果剪贴板没有数据则直接返回
// 用Promise封装便于将来使用
return new Promise((resolve, reject) => {
if (!(e.clipboardData && e.clipboardData.items)) {
reject(new Error('Not allow to paste this type!'))
} else {
// 复制的内容在剪贴板里位置不确定,所以通过遍历来保证数据准确
for (let i = 0, len = e.clipboardData.items.length; i < len; i++) {
const item = e.clipboardData.items[i]
// 文本格式内容处理
if (item.kind === 'string') {
item.getAsString((str: any) => {
resolve(str)
})
// 图片格式内容处理
} else if (item.kind === 'file') {
const pasteFile = item.getAsFile()
// 处理pasteFile
const imgEvent = {
target: {
files: [pasteFile],
},
}
chooseImg(imgEvent, (url: any) => {
resolve(url)
})
} else {
reject(new Error('Not allow to paste this type!'))
}
}
}
})
// 去文本粘贴
const handlePaste = async (event: any) => {
event.preventDefault()
const pasteResult = handlePastePlainText(event.clipboardData)
if (pasteResult) return
await handlePasteImageFile(event.clipboardData)
}
const onPaste = (event: any) => {
// navigator.permissions.query({name: 'clipboard-read'}).then(function(result) {
// if (result.state === 'granted') {
// // 用户已授权访问剪贴板
// } else if (result.state === 'prompt') {
// // 用户尚未授权访问剪贴板,需要提示用户授权
// result.onchange = function() {
// if (result.state === 'granted') {
// // 用户已授权访问剪贴板
// consoel
// } else if (result.state === 'denied') {
// // 用户拒绝了访问剪贴板的请求
// }
// };
// } else if (result.state === 'denied') {
// // 用户拒绝了访问剪贴板的请求
// }
// });
const items = (event.clipboardData || window.clipboardData).items
console.log(items)
for (let i = 0; i < items.length; i++) {
if (items[i].type.indexOf('image') !== -1) {
const file = items[i].getAsFile()
const reader = new FileReader()
reader.onload = function (e) {
const img = document.createElement('img')
img.src = e.target.result
img.style.width = '200px' // 设置图片宽度
img.style.height = '300px' // 设置图片高度
event.target.appendChild(img)
}
reader.readAsDataURL(file)
}
// 去格式粘贴 文本
const handlePastePlainText = (clipboardData: any) => {
const text = clipboardData.getData('text/plain')
if (text) {
const textNode = document.createTextNode(text)
cursorInsert(textNode)
return true
}
// e.stopPropagation()
// e.preventDefault()
// var text = '',
// event = e.originalEvent || e
// if (event.clipboardData && event.clipboardData.getData) {
// text = event.clipboardData.getData('text/plain')
// } else if (window.clipboardData && window.clipboardData.getData) {
// text = window.clipboardData.getData('Text')
// }
// if (document.queryCommandSupported('insertText')) {
// document.execCommand('insertText', false, text)
// } else {
// document.execCommand('paste', false, text)
// }
// getPaste(e).then((res: any) => {
// console.log(res, 'res')
// })
return false
}
const toPreviewer = (dataUrl, cb) => {
cb && cb(dataUrl)
//改变光标位置
const cursorInsert = (node: any) => {
// 获取光标范围
const selObj: any = window.getSelection()
const range = selObj.getRangeAt(0)
// 光标处插入节点
range.insertNode(node)
// 取消insert node 后的选中状态,将光标恢复到 insert node 后面
range.collapse(false)
inputVal.value = elAutocomplete.value.innerHTML
}
const compress = (img, fileType, maxWidth) => {
let canvas = document.createElement('canvas')
let ctx = canvas.getContext('2d')
const proportion = img.width / img.height
const width = maxWidth
const height = maxWidth / proportion
console.log(width, height, '打印宽高')
canvas.width = width
canvas.height = height
ctx.fillStyle = '#fff'
ctx.fillRect(0, 0, canvas.width, canvas.height)
ctx.drawImage(img, 0, 0, width, height)
const base64data = canvas.toDataURL(fileType, 0.75)
canvas = ctx = null
return base64data
//粘贴图片
const handlePasteImageFile = async (clipboardData: any) => {
const img = getPasteImageFile(clipboardData.files)
if (!img) {
return
}
const uploadRes = await fileToBase64(img)
if (!uploadRes) {
return
}
const oImage = await getImageObject(
uploadRes,
imgShowWidth.value,
imgShowHeight.value,
)
cursorInsert(oImage)
inputVal.value = elAutocomplete.value.innerHTML
}
const chooseImg = (e, cb, maxsize = 200 * 1024) => {
const file = e.target.files[0]
if (!file || !/\/(?:jpeg|jpg|png)/i.test(file.type)) {
return
// 获取一个 image object
const getImageObject = (uploadRes: any, showWidth: any, showHeight: any) => {
const oImage = new Image(showWidth, showHeight)
const datasetFields = ['width', 'height']
const datasetSizes: any = {
width: '200',
height: '200',
}
const reader = new FileReader()
reader.onload = function () {
const result = this.result
let img = new Image()
if (result.length <= maxsize) {
toPreviewer(result, cb)
datasetFields.forEach((field) => {
oImage.setAttribute(`data-${field}`, datasetSizes[field])
})
oImage.src = uploadRes
return oImage
}
const getPasteImageFile = (clipboardDataFiles: any) => {
if (!clipboardDataFiles.length) {
return null
}
// 剪切版中选择的(用户第一个点的在尾)第一张图片
const clipboardDataFileList = Array.from(clipboardDataFiles || [])
let firstSelectedImage = null
clipboardDataFileList.forEach((file: any) => {
if (!file.type.match(/image\//i)) {
return
}
img.onload = function () {
const compressedDataUrl = compress(img, file.type, maxsize / 1024)
toPreviewer(compressedDataUrl, cb)
img = null
firstSelectedImage = file
})
return firstSelectedImage
}
//将文件流转base64
const fileToBase64 = function (file: any) {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = (event: any) => {
resolve(event.target.result)
}
img.style.width = '100px'
img.style.height = '100px'
img.src = result
reader.onerror = (error) => {
reject(error)
}
reader.readAsDataURL(file)
})
}
const removeDefaultContent = function (event: any) {
elAutocomplete.value.classList.remove('textarea')
cursorIn()
}
const addDefaultContent = function (event: any) {
if (!event.target.innerHTML && event.target.innerText.length == 0) {
elAutocomplete.value.classList.add('textarea')
}
reader.readAsDataURL(file)
}
const cursorIn = function () {
var divElement: any = document.querySelector('div[contenteditable="true"]')
var range = document.createRange()
range.selectNodeContents(divElement)
range.collapse(false)
var selection: any = window.getSelection()
selection.removeAllRanges()
selection.addRange(range)
}
defineExpose({
setState(res: any) {
state.value = res
inputVal.value = res
},
getState() {
return state.value
return inputVal.value
},
})
</script>
<style lang="scss" scoped>
.auto-prompt {
padding: 20px;
}
.textarea {
&:after {
content: attr(placeholder);
display: inline-block;
color: #999;
font-size: 13px;
vertical-align: middle;
line-height: 16px;
}
}
.icon {
width: 24px;
height: 24px;
......
......@@ -16,12 +16,14 @@
import { ref, watch, defineEmits, defineExpose, defineProps } from 'vue'
const list = ref([])
import * as emoji from 'emoji.json'
console.log(emoji,'emoji')
const getList = () => {
list.value = emoji.slice(0, 50)
list.value.push(emoji[55])
list.value.push(emoji[56])
list.value.push(emoji[57])
list.value.push(emoji[60])
let info = emoji.default
list.value = info.slice(0, 50)
list.value.push(info[55])
list.value.push(info[56])
list.value.push(info[57])
list.value.push(info[60])
}
const emits = defineEmits(['emotion'])
const clickHandler = (i: number | string) => {
......
......@@ -48,7 +48,6 @@ function initWebSocket(wsUrl: string) {
// 定义重连函数
const reConnect = (wsUrl: string) => {
console.log("尝试重新连接");
if (useUserStore().isConnected) return; // 如果已经连上就不在重连了
rec && clearTimeout(rec);
rec = setTimeout(function () {
......@@ -59,12 +58,12 @@ const reConnect = (wsUrl: string) => {
// 创建连接
function websocketOpen() {
console.log("连接成功");
useUserStore().connect();
}
// 数据接收
function websocketonmessage(e: MessageEvent<any>) {
const res = JSON.parse(e.data); // 解析JSON格式的数据
console.log(res, "接受到的数据");
// 下面的判断则是后台返回的接收到的数据 如何处理自己决定
if (res.command == 11) {
//将数据放在store中
......@@ -124,5 +123,4 @@ const closeWebSocket = () => {
}
};
export { initWebSocket, sendWebSocket, creatWebSocket, closeWebSocket };
<template>
<div class="common-layout">
<el-container>
<el-aside width="280px">
<el-header class="el-header-left">
<el-input placeholder="搜索" />
</el-header>
<div
active-text-color="#000"
background-color="#fff"
default-active="1"
>
<div
class="el-menu-item"
:class="getMenuItem(item)"
:index="item?.userId"
:key="item?.userId"
v-for="item in userList"
@click="handleMenuClick(item)"
>
<div class="user-info-box">
<div class="user-img-box">
<img :src="item.userImg" alt="" />
</div>
<div class="user-info-right">
<div class="user-box-right-nbs">
<div class="user-name-box">
<span class="user-name">{{ item?.username }}</span>
<div class="user-time">
05-19 16:38
</div>
</div>
<div class="user-reply-box">
<span class="user-reply">
{{
item?.messages[
item?.messages.length - 1
]?.content.includes('http')
? '[图片]'
: item.messages[item.messages.length - 1]?.content
}}
</span>
<div class="count">1</div>
</div>
</div>
</div>
</div>
</div>
</div>
</el-aside>
<el-container class="el-container-center" v-if="customerInfo.username">
<el-header class="el-header-center">
<p class="name">{{ customerInfo.username }}</p>
<p class="url">US|https://www.ibeautytop.com</p>
</el-header>
<el-main
id="srollId"
ref="srollId"
:style="{
height: '600px',
width: '100%',
border: '1px solid #ccc',
}"
>
<div
class="message-container"
v-for="(message, index) in messages"
:key="index"
:class="getMessageClass(message?.isSent)"
>
<div class="time">{{ message.time }}</div>
<div v-if="message.isSent" class="message-container">
<div class="bubble">
<div
v-if="message?.msgType == 0"
class="message"
v-html="message?.content"
@click.prevent="handleMessageClick($event)"
></div>
<div v-else class="img-wraper">
<el-image
:src="message?.content"
:preview-src-list="[message?.content]"
/>
</div>
</div>
<div class="avatar">
<img
src="../assets/logo.png"
alt="Avatar"
class="avatar-image"
/>
</div>
</div>
<div v-if="!message.isSent" class="message-container">
<div class="avatar">
<img :src="message?.userImg" class="avatar-image" />
</div>
<div class="bubble">
<div
v-if="message?.msgType == 0"
class="message"
v-html="message?.content"
@click.prevent="handleMessageClick($event)"
></div>
<div v-else class="img-wraper">
<el-image
:src="message?.content"
:preview-src-list="[message?.content]"
/>
</div>
</div>
</div>
</div>
</el-main>
<el-footer class="el-footer">
<AutomaticPrompt
@keydown.enter="handleButtonClick"
@updateState="getState"
ref="automaticPromptRef"
></AutomaticPrompt>
<el-button
type="primary"
plain
class="btn-send"
style="width: 50px;"
@click.stop="handleButtonClick"
>
发送
</el-button>
</el-footer>
</el-container>
<el-container class="el-container-center" v-else></el-container>
<el-aside width="400px">
<div>
<el-header class="el-header-right">
<img src="../assets/logo.png" alt="Avatar" />
<div class="name">询价单</div>
</el-header>
<div class="customer-info user-wrapper">
<div class="customer-info-right">
<div class="customer-info-box">
<div class="avatar-box">
<img src="../assets/shop.jpg" alt="Avatar" />
</div>
<div class="avatar-right">
<div class="avatar-right-name">
{{ inquiryInfo.name }}
</div>
<div class="avatar-right-price">
<div>{{ inquiryInfo.price }}</div>
<div>{{ inquiryInfo.unit }}</div>
</div>
<div class="avatar-input-box">
<div>
<el-input-number
class="avatar-number-input"
:min="1"
@change="handleChange"
v-model="ruleForm.count"
:max="10"
/>
</div>
<div class="avatar-input-right">
<div class="change">更换</div>
<div class="delete">删除</div>
</div>
</div>
</div>
</div>
<div class="add">
<img src="../assets/add-line.png" />
<div>添加</div>
</div>
</div>
<div class="form">
<el-form
ref="ruleFormRef"
style="max-width: 600px;"
:model="ruleForm"
:rules="rules"
label-position="top"
label-width="auto"
>
<el-form-item label="客户名称" prop="customerName">
<el-input
v-model="ruleForm.customerName"
type="password"
placeholder="默认展示"
autocomplete="off"
/>
</el-form-item>
<el-form-item label="国家/地区" prop="countriesRegions">
<el-input
v-model="ruleForm.countriesRegions"
type="password"
placeholder="默认展示"
autocomplete="off"
/>
</el-form-item>
<el-form-item label="Email" prop="Email">
<el-input
placeholder="默认展示"
v-model.number="ruleForm.Email"
/>
</el-form-item>
<el-form-item label="WatchApp" prop="WatchApp">
<el-input
placeholder="默认展示"
v-model.number="ruleForm.WatchApp"
/>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input
placeholder="默认展示"
v-model.number="ruleForm.remark"
/>
</el-form-item>
<el-form-item
label="备注"
prop="content"
style="margin-bottom: 3px;"
>
<el-input
placeholder="文本内容"
v-model="ruleForm.content"
type="textarea"
:rows="8"
/>
</el-form-item>
<el-button
type="primary"
plain
style="width: 50px;"
@click.stop="sumbitFuleForm(ruleFormRef)"
>
发送
</el-button>
</el-form>
</div>
</div>
</div>
</el-aside>
</el-container>
</div>
</template>
<script lang="ts" setup>
import '../assets/font/iconfont.css'
import { ElMessage } from 'element-plus'
import { ref, onMounted, watch, nextTick, reactive } from 'vue'
import { getShortDate } from '../utils/index'
import { useUserStore } from '../store/modules/user'
import AutomaticPrompt from '../components/AutomaticPrompt.vue'
import ImageViewer from '@luohc92/vue3-image-viewer'
import { sendWebSocket } from '../utils/websocket'
import type { FormInstance } from 'element-plus'
import { initWebSocket } from "../utils/websocket";
import '@luohc92/vue3-image-viewer/dist/style.css'
import { v4 as uuidv4 } from "uuid"
import { getUserList, InquiryAdd, checkMesssages } from '../axios/model/user'
const ruleForm = ref({
count: 1,
})
const connectMsg = () => {
const useUser = useUserStore();
const toIp = `ws://192.168.31.228:8081?type=yk&code=${uuidv4()}&kf=10086`;
console.log(toIp)
useUser.connect();
initWebSocket(toIp);
};
connectMsg();
const ruleFormRef = ref<FormInstance>()
const customerInfo = ref({})
const messages = ref([])
const count = ref(0)
const store = useUserStore()
const userList = ref([])
const automaticPromptRef = ref('')
const inquiryInfo = ref({
price: '20.00',
unit: 'USD',
name: 'Industrial 5V 12V RS232 RS-232 Parallel PCI PC',
})
const rules = reactive({
customerName: [
{
required: true,
message: '请输入客户名称',
trigger: 'blur',
},
],
countriesRegions: [
{
required: true,
message: '请输入国家/地区',
trigger: 'blur',
},
],
Email: [
{
required: true,
message: '请输入Email',
trigger: 'blur',
},
],
WatchApp: [
{
required: true,
message: '请输入WatchApp',
trigger: 'blur',
},
],
remark: [
{
required: true,
message: '请输入描述',
trigger: 'blur',
},
],
content: [
{
required: true,
message: '请输入描述',
trigger: 'blur',
},
],
})
let msg = ''
//进入页面直接发送请求从后端获取热点数据
onMounted(async () => {
count.value = store.count
if (store.userList.length > 0) {
userList.value = store.userList
} else {
getList()
}
setMessage()
})
//设置message
const setMessage = () => {
if (store.customerInfo.username) {
customerInfo.value = store.customerInfo
messages.value = customerInfo.value.messages || []
}
setSrollHeight()
}
//获取列表的数据
const getList = async () => {
let query = {}
try {
const { success, result, message }: any = await getUserList(query)
if (success) {
let list = result.map((item: any) => {
item.messages = []
return item
})
userList.value = list
store.setUserList(list)
if (customerInfo.value.username) handleMenuClick(customerInfo.value)
}
} catch (error) {
console.log(error)
}
}
//获取子组件中state的值,这个好像是写多余了,可以直接使用automaticPromptRef.value.setState('');获取state值
const getState = (v: string) => {
msg = v
}
const handleChange = (val: any) => {
store.setCount(val)
}
//监听聊天数据的改变
watch(
userList,
(newVal, oldVal) => {
if (newVal && newVal.length > 0 && customerInfo.value.username) {
const obj =
newVal.find((item: any) => {
return item.username == customerInfo.value.username
}) || {}
messages.value = obj.messages || []
}
},
{ deep: true },
)
//监听聊天框数据的改变
watch(
messages,
(newVal, oldVal) => {
if (newVal?.length > 0) setSrollHeight()
},
{ deep: true },
)
//让聊天滑动窗口滑到底部
const setSrollHeight = () => {
nextTick(() => {
const div = document.getElementById('srollId')
if (div) div.scrollTop = div?.scrollHeight
})
}
//切换聊天人
const handleMenuClick = (item: object) => {
customerInfo.value = item
store.setCustomerInfo(item)
messages.value = item?.messages || []
const data = {
cmd: '19',
type: '1',
fromUserId: item?.username,
group_id: '',
userId: store.userInfo.username,
}
sendWebSocket(data)
getCheckMesssages(data)
}
//发送按钮
const handleButtonClick = () => {
if (!msg) {
return ElMessage({
message: '请输入内容',
type: 'error',
})
}
let data = {
content: msg,
isSent: true,
cmd: '11',
msgType: 0,
chatType: '2',
group_id: '',
time: getShortDate(),
to: customerInfo.value.username,
form: store.userInfo.username,
}
messages.value?.push(data)
sendWebSocket(data)
automaticPromptRef.value.setState('')
}
const handleMessageClick = (event: any) => {
const target = event.target
if (target.tagName === 'A') {
// 点击的是超链接
// 执行相应的操作
if (target.innerHTML === '解决') {
alert('感谢您的使用')
} else if (target.innerHTML === '未解决') {
alert('很抱歉未能解决你的问题')
} else {
handleLinkClick(target.innerHTML)
}
} else if (target.tagName === 'IMG') {
// 点击的图片进行放大操作
ImageViewer({
//切记额images这个参数是数组,我的target.valueof().src值是一个http的图片地址
images: [target.valueOf().src],
curIndex: 0,
zIndex: 2000,
showDownload: true,
showThumbnail: true,
handlePosition: 'bottom',
maskBgColor: 'rgba(0,0,0,0.7)',
onClose: () => {
console.log('close')
},
})
}
}
//查看消息将其改为已读
const getCheckMesssages = async (res: any) => {
try {
const { success, result, message }: any = await checkMesssages({
username: res.username,
})
} catch (error) {
console.log(error)
}
}
//提交询价单
const sumbitFuleForm = (formEl: FormInstance | undefined) => {
let query = {}
if (!formEl) return
formEl.validate(async (valid: boolean) => {
if (valid) {
try {
const { success, result, message }: any = await InquiryAdd(
ruleForm.value,
)
if (success) {
ElMessage({
message,
type: 'success',
})
}
} catch (error) {
console.log(error)
}
}
})
}
const handleLinkClick = (msg: string) => {
messages.value.push({ content: msg, isSent: true })
}
//消息框样式动态选择
const getMessageClass = (isSent: boolean) => {
return isSent ? 'message-container-right' : 'message-container-left'
}
//左边按钮操作
const getMenuItem = (item: any) => {
return item.username == customerInfo.value?.username ? 'is-active' : ''
}
</script>
<style lang="scss" scoped>
.el-header-left {
height: 76px;
width: 100%;
border: 1px solid #e6e8ed;
text-align: center;
padding: 16px;
.el-input {
:deep(.el-input__wrapper) {
background: var(--Fill-Grey-Fill-1, #f8f8fa);
}
height: 44px;
font-family: 'PingFang SC';
font-style: normal;
font-weight: 400;
width: 100%;
.el-input__inner {
font-size: 14px;
}
}
}
.el-container-center {
background: #e6e8ed;
}
.el-header-center {
height: 76px;
width: 100%;
border: 1px solid #e6e8ed;
padding: 16px 24px;
display: flex;
justify-content: space-between;
flex-direction: column;
.name {
color: #010914;
font-family: 'Inter';
font-size: 18px;
font-style: normal;
font-weight: 600;
}
.url {
color: #798494;
}
}
.el-header-right {
height: 76px;
width: 100%;
border: 1px solid #e6e8ed;
display: flex;
align-items: center;
img {
width: 15.42px;
height: 17.92px;
margin-right: 14px;
}
.name {
color: #010914;
font-family: 'PingFang SC';
font-size: 16px;
font-style: normal;
font-weight: 500;
line-height: 20px;
}
}
.el-footer {
width: 100%;
border: 1px solid #ccc;
border-top: 0px;
padding: 10px;
height: calc(100vh - 600px - 76px);
position: relative;
.btn-send {
position: absolute;
bottom: 20px;
right: 20px;
}
}
.el-menu-item {
line-height: initial;
padding: 0px !important;
height: 86px;
border-left: 3px solid transparent;
&.is-active {
background: #eff0f1;
border-left: 3px solid #1890ff;
}
}
.user-info-box {
display: flex;
width: 100%;
height: 100%;
padding: 0px 10px;
box-sizing: border-box;
justify-content: space-between;
.user-img-box {
margin-right: 10px;
display: flex;
align-items: center;
img {
width: 40px;
height: 40px;
}
}
.user-info-right {
display: flex;
justify-content: space-between;
align-items: center;
height: 100%;
width: 100%;
.user-box-right-nbs {
width: 100%;
}
.user-reply-box {
position: relative;
display: flex;
}
.count {
right: 16px;
top: 4px;
position: absolute;
display: flex;
width: var(--Edges-xl, 16px);
height: var(--Edges-xl, 16px);
min-width: var(--Edges-xl, 16px);
padding: var(--Spacing-none, 0);
flex-direction: column;
justify-content: center;
color: #fff;
align-items: center;
border-radius: var(--Radius-full, 1000px);
background: red;
}
.label {
margin-left: 5px;
color: #3875ea;
font-size: 12px;
background: #d8e5ff;
border-radius: 2px;
padding: 1px 5px;
&.pc {
background: rgba(100, 64, 194, 0.16);
color: #6440c2;
}
}
}
.user-name-box {
display: flex;
align-items: center;
justify-content: space-between;
.user-name {
word-break: break-all;
text-overflow: ellipsis;
overflow: hidden;
max-width: 120px;
font-size: 16px;
color: rgba(0, 0, 0, 0.65);
margin-right: 10px;
font-family: 'Inter';
font-style: normal;
font-weight: 600;
}
}
.user-reply {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
overflow: hidden;
color: #798494;
font-feature-settings: 'clig' off, 'liga' off;
text-overflow: ellipsis;
font-family: 'Inter';
margin-top: 3px;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px;
}
.user-time {
color: #999;
font-size: 12px;
font-style: normal;
font-family: 'Inter';
font-weight: 400;
}
}
.customer-info-right {
.customer-info-box {
display: flex;
.avatar-box {
width: 88px;
height: 88px;
border-radius: 8px;
margin-right: 12px;
overflow: hidden;
img {
width: 100%;
height: 100%;
}
}
.avatar-input-box {
display: flex;
align-items: center;
margin-top: 8px;
justify-content: space-between;
.change {
margin-right: 27px;
color: #010914;
text-align: right;
font-family: 'PingFang SC';
font-size: 12px;
font-style: normal;
font-weight: 400;
}
.delete {
color: #ff6600;
text-align: right;
font-family: 'PingFang SC';
font-size: 12px;
font-style: normal;
font-weight: 400;
}
}
.avatar-input-right {
display: flex;
}
.avatar-right {
flex: 1;
.avatar-right-name {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
flex: 1 0 0;
overflow: hidden;
color: #010914;
font-feature-settings: 'clig' off, 'liga' off;
text-overflow: ellipsis;
font-family: 'Inter';
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px;
}
.avatar-right-price {
display: flex;
color: #010914;
font-feature-settings: 'clig' off, 'liga' off;
font-family: 'Inter';
font-size: 16px;
font-style: normal;
line-height: 24px;
div {
&:nth-of-type(1) {
margin-right: 4px;
font-weight: 600;
}
&:nth-of-type(2) {
font-weight: 400;
}
}
}
}
}
}
.el-input__wrapper {
width: 48px;
}
.user-wrapper {
padding: 16px;
}
.user-info {
padding-top: 15px;
padding-bottom: 10px;
}
.chat-bar {
height: 50px;
width: 100%;
line-height: 50px;
text-align: left;
}
.send-btn {
position: absolute;
right: 0;
bottom: 10px;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-pack: end;
-ms-flex-pack: end;
justify-content: flex-end;
margin-top: 10px;
margin-right: 10px;
}
.underline-link {
text-decoration: underline;
}
.message-container {
display: flex;
align-items: center;
margin-bottom: 10px;
position: relative;
}
.message-container .time {
text-align: center;
color: #999;
font-size: 14px;
position: absolute;
width: 100%;
}
.avatar {
margin-left: 10px; /* 修改这里将头像放在消息框的右边 */
}
.avatar-image {
width: 40px;
height: 40px;
border-radius: 50%;
object-fit: cover;
}
.bubble {
background-color: #e8e8e8;
color: #000;
padding: 10px;
border-radius: 5px;
max-width: 320px;
background: #f5f5f5;
border-radius: 10px;
color: #000;
font-size: 14px;
overflow: hidden;
.img-wraper img {
max-width: 100%;
height: auto;
display: block;
}
}
.message {
text-align: left;
margin: 0;
}
.message-container-right {
justify-content: flex-end;
}
.message-container-left {
justify-content: flex-start;
}
.avatar-number-input :deep(.el-input) {
width: 102px !important;
}
.avatar-number-input {
width: 102px !important;
}
.add {
display: flex;
height: var(--Layout-lg, 32px);
padding: 0 var(--Spacing-lg, 12px) 0 var(--Spacing-md, 8px);
justify-content: center;
align-items: center;
gap: 4px;
border-radius: var(--Radius-xs, 4px);
border: var(--Edges-zero, 1px) solid var(--color-Stroke-Weak, #e6e8ed);
background: var(--color-bg-Program-White, #fff);
width: 72px;
height: 32px;
margin-top: 16px;
img {
width: 20px;
height: 20px;
}
}
.form {
margin-top: 32px;
:deep(.el-form-item__label) {
color: #010914;
font-family: 'PingFang SC';
font-size: 14px;
font-style: normal;
font-weight: bolder;
line-height: 20px;
}
:deep(.el-input__wrapper) {
padding: 0px;
}
:deep(.el-input__inner) {
padding: 8px;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
flex: 1 0 0;
overflow: hidden;
color: #798494;
font-feature-settings: 'clig' off, 'liga' off;
text-overflow: ellipsis;
font-family: 'PingFang SC';
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px;
background: #f2f3f5;
}
:deep(.el-textarea__inner) {
background: #f2f3f5;
}
}
</style>
......@@ -3,18 +3,18 @@
<el-container>
<el-container class="el-container-center" v-if="customerInfo.username">
<el-header class="el-header-center">
<p class="name">{{ customerInfo.username }}</p>
<p class="url">US|https://www.ibeautytop.com</p>
<p class="onlineCustomer">您正在与 在线客服 对话</p>
</el-header>
<el-main
id="srollId"
ref="srollId"
:style="{
height: '600px',
height: '60vh',
width: '100%',
border: '1px solid #ccc',
}"
>
<div class="title">您好,系统正在为您接入在线客服</div>
<div
class="message-container"
v-for="(message, index) in messages"
......@@ -67,20 +67,36 @@
</div>
</el-main>
<el-footer class="el-footer">
<div class="static-box">
<div
v-for="(item, index) in staticList"
:key="index"
class="staticList"
>
<div>{{ item }}</div>
</div>
</div>
<AutomaticPrompt
@keydown.enter="handleButtonClick"
@updateState="getState"
ref="automaticPromptRef"
></AutomaticPrompt>
<el-button
type="primary"
plain
class="btn-send"
style="width: 50px;"
@click.stop="handleButtonClick"
>
发送
</el-button>
<div class="btn-send">
<el-button
plain
style="width: 50px; background: #f8f8fa; color: #000;"
@click.stop="handleButtonClick"
>
Finish
</el-button>
<el-button
style="width: 50px; color: #fff; background: #000;"
plain
@click.stop="handleButtonClick"
>
send
</el-button>
</div>
</el-footer>
</el-container>
<el-container class="el-container-center" v-else></el-container>
......@@ -90,95 +106,41 @@
<script lang="ts" setup>
import '../assets/font/iconfont.css'
import { ElMessage } from 'element-plus'
import {
ref,
onMounted,
watch,
nextTick,
reactive,
getCurrentInstance,
} from 'vue'
import { ref, onMounted, watch, nextTick, getCurrentInstance } from 'vue'
import { getShortDate } from '../utils/index'
import { useUserStore } from '../store/modules/user'
import AutomaticPrompt from '../components/AutomaticPrompt.vue'
import ImageViewer from '@luohc92/vue3-image-viewer'
import { sendWebSocket } from '../utils/websocket'
import type { FormInstance } from 'element-plus'
import { initWebSocket } from '../utils/websocket'
import '@luohc92/vue3-image-viewer/dist/style.css'
import { v4 as uuidv4 } from 'uuid'
import { getUserList, InquiryAdd, checkMesssages } from '../axios/model/user'
const ruleForm = ref({
count: 1,
})
const { appContext } = getCurrentInstance()
const ruleFormRef = ref<FormInstance>()
import { checkMesssages } from '../axios/model/user'
const customerInfo = ref({})
const messages = ref([])
const count = ref(0)
const staticList = ref([
'Login account',
'Forgot password',
'Free entry',
'Add/manage products',
'improve',
])
const store = useUserStore()
const automaticPromptRef = ref('')
const rules = reactive({
customerName: [
{
required: true,
message: '请输入客户名称',
trigger: 'blur',
},
],
countriesRegions: [
{
required: true,
message: '请输入国家/地区',
trigger: 'blur',
},
],
Email: [
{
required: true,
message: '请输入Email',
trigger: 'blur',
},
],
WatchApp: [
{
required: true,
message: '请输入WatchApp',
trigger: 'blur',
},
],
remark: [
{
required: true,
message: '请输入描述',
trigger: 'blur',
},
],
content: [
{
required: true,
message: '请输入描述',
trigger: 'blur',
},
],
})
let msg = ''
//进入页面直接发送请求从后端获取热点数据
onMounted(async () => {
connectMsg().then((res) => {
// const translate = appContext.config.globalProperties.$translate
getHistoryMessage()
setMessage()
getHistoryMessage()
setMessage()
})
})
const connectMsg = () => {
return new Promise((resolve, reject) => {
return new Promise((resolve: any, reject: any) => {
const useUser = useUserStore()
useUser.setUserInfo({
username: Date.now(),
})
const toIp = `ws://192.168.31.228:8081?type=yk&code=${useUser.userInfo.username}&kf=${store.customerInfo.username}`
console.log(toIp)
const toIp = `ws://192.168.31.123:8081?type=yk&code=${useUser.userInfo.username}&kf=${store.customerInfo.username}`
useUser.connect()
initWebSocket(toIp)
setTimeout(() => {
......@@ -195,8 +157,9 @@ const getHistoryMessage = () => {
group_id: '',
userId: store.userInfo.username,
}
console.log(data)
sendWebSocket(data)
getCheckMesssages(data)
// getCheckMesssages(data)
}
//设置message
......@@ -248,7 +211,6 @@ const handleButtonClick = () => {
to: customerInfo.value.username,
form: store.userInfo.username,
}
console.log('打印用户发送的内容', data)
messages.value?.push(data)
sendWebSocket(data)
automaticPromptRef.value.setState('')
......@@ -301,6 +263,32 @@ const getMessageClass = (isSent: boolean) => {
}
</script>
<style lang="scss" scoped>
.title {
display: flex;
padding: 8px;
justify-content: center;
align-items: center;
border-radius: 4px;
width: 196px;
background: #f8f8fa;
font-size: 12px;
font-family: 'PingFang SC';
font-weight: 400;
margin: 0px auto;
color: #798494;
}
.onlineCustomer {
height: 44px;
padding: 8px;
line-height: 44px;
align-items: center;
gap: 8px;
background: rgba(248, 248, 250, 1);
font-family: 'PingFang SC';
font-size: 14px;
font-style: normal;
font-weight: 600;
}
.el-header-left {
height: 76px;
width: 100%;
......@@ -322,13 +310,13 @@ const getMessageClass = (isSent: boolean) => {
}
}
.el-container-center {
background: #e6e8ed;
background: #fff;
}
.el-header {
padding: 0px;
}
.el-header-center {
height: 76px;
width: 100%;
border: 1px solid #e6e8ed;
padding: 16px 24px;
display: flex;
justify-content: space-between;
flex-direction: column;
......@@ -367,14 +355,35 @@ const getMessageClass = (isSent: boolean) => {
width: 100%;
border: 1px solid #ccc;
border-top: 0px;
padding: 10px;
height: calc(100vh - 600px - 76px);
padding: 0px;
height: calc(100vh - 60vh - 60px);
position: relative;
.btn-send {
position: absolute;
bottom: 20px;
right: 20px;
}
.static-box {
display: flex;
background: #e6e8ed;
padding: 8px 16px;
}
.staticList {
height: 36px;
border: 1px solid #e6e8ed;
border-radius: 36px;
background: #fff;
width: max-width;
color: #000000;
padding: 0px 16px;
font-family: 'Inter';
font-size: 14px;
font-weight: 600;
line-height: 36px;
text-align: center;
margin-right: 8px;
}
}
.el-menu-item {
line-height: initial;
......@@ -452,7 +461,7 @@ const getMessageClass = (isSent: boolean) => {
word-break: break-all;
text-overflow: ellipsis;
overflow: hidden;
max-width: 120px;
max-width: 80px;
font-size: 16px;
color: rgba(0, 0, 0, 0.65);
margin-right: 10px;
......@@ -463,10 +472,9 @@ const getMessageClass = (isSent: boolean) => {
}
.user-reply {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
word-break: break-all;
text-overflow: ellipsis;
max-width: 80px;
overflow: hidden;
color: #798494;
font-feature-settings: 'clig' off, 'liga' off;
......@@ -638,6 +646,8 @@ const getMessageClass = (isSent: boolean) => {
color: #000;
font-size: 14px;
overflow: hidden;
word-break: break-all;
text-overflow: ellipsis;
.img-wraper img {
max-width: 100%;
height: auto;
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论