wew
This commit is contained in:
parent
4e87f437e1
commit
66fabbf475
@ -41,3 +41,14 @@ export const send_template_message = (data) => {
|
|||||||
data
|
data
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 查看小程序状态
|
||||||
|
|
||||||
|
export const checkMiniProgramStatus = (params) => {
|
||||||
|
return request({
|
||||||
|
url: `admin/app/check_status`,
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -2,7 +2,7 @@
|
|||||||
<div style="width: 100%;">
|
<div style="width: 100%;">
|
||||||
<!-- <el-input v-model="centent" suffix-icon="Search" @input="inputSearch" placeholder="搜索"></el-input> -->
|
<!-- <el-input v-model="centent" suffix-icon="Search" @input="inputSearch" placeholder="搜索"></el-input> -->
|
||||||
<ul v-infinite-scroll="load" class="infinite-list" style="overflow: auto" :style="'height:' + height" :infinite-scroll-immediate="false" infinite-scroll-distance="5">
|
<ul v-infinite-scroll="load" class="infinite-list" style="overflow: auto" :style="'height:' + height" :infinite-scroll-immediate="false" infinite-scroll-distance="5">
|
||||||
<li v-for="item in cardlist" :key="item.sender_id" class="infinite-list-item" :class="{ active: item.active }"
|
<li v-for="item in cardlist" :key="item.sender_id" class="infinite-list-item" :class="{ active: item.sender_id == activeId }"
|
||||||
@click="handleItem(item)">
|
@click="handleItem(item)">
|
||||||
<div class="avatar-container">
|
<div class="avatar-container">
|
||||||
<el-image class="user-avatar" :src="item.sender_avatar"></el-image>
|
<el-image class="user-avatar" :src="item.sender_avatar"></el-image>
|
||||||
@ -65,8 +65,9 @@ const inputSearch = (e) => {
|
|||||||
emits('input', e)
|
emits('input', e)
|
||||||
}, 500)
|
}, 500)
|
||||||
}
|
}
|
||||||
|
const activeId = ref(null)
|
||||||
const handleItem = (row) => {
|
const handleItem = (row) => {
|
||||||
|
activeId.value = row.sender_id
|
||||||
if (props.multiple) {
|
if (props.multiple) {
|
||||||
if (!row.active) {
|
if (!row.active) {
|
||||||
row.active = true
|
row.active = true
|
||||||
@ -272,7 +273,7 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.active {
|
.active {
|
||||||
background-color: var(--el-color-primary-light-7);
|
background-color: var(--el-color-primary-light-9);
|
||||||
}
|
}
|
||||||
|
|
||||||
.infinite-list .infinite-list-item+.list-item {
|
.infinite-list .infinite-list-item+.list-item {
|
||||||
|
|||||||
@ -1,16 +1,33 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue'
|
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
import Setting from '@/layout/tabbar/setting/index.vue'
|
import Setting from '@/layout/tabbar/setting/index.vue'
|
||||||
|
import appDetail from '@/store/modules/appDetail'
|
||||||
|
const appInfoStore = appDetail()
|
||||||
|
|
||||||
|
// ensure route/router are defined
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
|
console.log('appInfoStore', appInfoStore);
|
||||||
|
const appStatus = computed(() => {
|
||||||
|
// guard access and provide fallback
|
||||||
|
try {
|
||||||
|
return appInfoStore.appDetail?.check_status_text || ''
|
||||||
|
} catch (e) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const newPath = computed(() => {
|
const newPath = computed(() => {
|
||||||
return route.path
|
return route.path
|
||||||
})
|
})
|
||||||
const toLink = (path) => {
|
const toLink = (path) => {
|
||||||
router.push(path)
|
router.push(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -25,7 +42,8 @@ const toLink = (path) => {
|
|||||||
小程序聊天系统
|
小程序聊天系统
|
||||||
</div>
|
</div>
|
||||||
<div class="right">
|
<div class="right">
|
||||||
<div class="nav">
|
<div class="nav" v-if="newPath == '/chat'">
|
||||||
|
状态:{{ appStatus }}
|
||||||
<!-- <ul><li @click="toLink('/dashboard')" :class="{ active: newPath == '/dashboard' }">首页</li></ul> -->
|
<!-- <ul><li @click="toLink('/dashboard')" :class="{ active: newPath == '/dashboard' }">首页</li></ul> -->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -70,6 +88,7 @@ const toLink = (path) => {
|
|||||||
padding-left: 10px;
|
padding-left: 10px;
|
||||||
width: calc(100% - 300px);
|
width: calc(100% - 300px);
|
||||||
.nav{
|
.nav{
|
||||||
|
padding-left: 10px;
|
||||||
li{
|
li{
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 0 12px;
|
padding: 0 12px;
|
||||||
|
|||||||
24
src/store/modules/appDetail.ts
Normal file
24
src/store/modules/appDetail.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
// 机器人相关仓库
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
|
||||||
|
const appDetail = defineStore(
|
||||||
|
'appDetail',
|
||||||
|
() => {
|
||||||
|
|
||||||
|
const appDetail = ref({
|
||||||
|
check_status_text: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
return {
|
||||||
|
appDetail
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
persist: true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export default appDetail
|
||||||
@ -429,3 +429,8 @@ span[class=el-tooltip__trigger] {
|
|||||||
padding-top: 0!important;
|
padding-top: 0!important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.el-image__error{
|
||||||
|
font-size: 12px!important;
|
||||||
|
line-height: 14px;
|
||||||
|
}
|
||||||
@ -1,10 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="chat-page">
|
<div class="chat-page">
|
||||||
<div class="left-panel">
|
<div class="left-panel">
|
||||||
<WxUserCard @load="onLoadUserList" :cardlist="userList" height="calc(100vh - 120px)" @change="onSelectUser" />
|
<WxUserCard @load="onLoadUserList" :cardlist="userList" height="calc(100vh - 120px)"
|
||||||
|
@change="onSelectUser" />
|
||||||
</div>
|
</div>
|
||||||
<div class="right-panel">
|
<div class="right-panel">
|
||||||
<div :class="['chat-body', { 'chat-body--loading': isLoadingMessages }]" ref="chatBody" @scroll="handleScroll">
|
<div :class="['chat-body', { 'chat-body--loading': isLoadingMessages }]" ref="chatBody"
|
||||||
|
@scroll="handleScroll">
|
||||||
<!-- 加载更多提示 -->
|
<!-- 加载更多提示 -->
|
||||||
<div v-if="loadingMore" class="loading-more">加载中...</div>
|
<div v-if="loadingMore" class="loading-more">加载中...</div>
|
||||||
|
|
||||||
@ -13,7 +15,8 @@
|
|||||||
|
|
||||||
<div class="chat-footer">
|
<div class="chat-footer">
|
||||||
<div class="footer-left" style="">
|
<div class="footer-left" style="">
|
||||||
<div class="upload-row" style="display:flex; align-items:center; gap:12px;margin-left: 6px;position: relative; top: 10px;">
|
<div class="upload-row"
|
||||||
|
style="display:flex; align-items:center; gap:12px;margin-left: 6px;position: relative; top: 10px;">
|
||||||
<el-icon style="font-size:24px;cursor:pointer" @click="triggerImageInput">
|
<el-icon style="font-size:24px;cursor:pointer" @click="triggerImageInput">
|
||||||
<svg class="icon" aria-hidden="true">
|
<svg class="icon" aria-hidden="true">
|
||||||
<use xlink:href="#icon-tupian1"></use>
|
<use xlink:href="#icon-tupian1"></use>
|
||||||
@ -28,21 +31,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<!-- 发送按钮 -->
|
<!-- 发送按钮 -->
|
||||||
<div class="send-button-container" style="margin-left: 12px;">
|
<div class="send-button-container" style="margin-left: 12px;">
|
||||||
<el-button
|
<el-button type="primary" @click="send" :disabled="!draft.trim() || isSending || isUploading"
|
||||||
type="primary"
|
:loading="isSending" style="height: 60px;">
|
||||||
@click="send"
|
|
||||||
:disabled="!draft.trim() || isSending || isUploading"
|
|
||||||
:loading="isSending"
|
|
||||||
style="height: 60px;"
|
|
||||||
>
|
|
||||||
{{ isSending ? '发送中...' : '发送' }}
|
{{ isSending ? '发送中...' : '发送' }}
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button
|
<el-button type="primary" @click="sendTemplateMessage" :loading="isSending2" style="height: 60px;">
|
||||||
type="primary"
|
|
||||||
@click="sendTemplateMessage"
|
|
||||||
:loading="isSending2"
|
|
||||||
style="height: 60px;"
|
|
||||||
>
|
|
||||||
{{ isSending2 ? '发送中...' : '发送模板消息' }}
|
{{ isSending2 ? '发送中...' : '发送模板消息' }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
@ -58,10 +51,12 @@ import WxUserCard from '@/components/UserCard/index.vue'
|
|||||||
import UserMessage from '@/components/UserMessage/index.vue'
|
import UserMessage from '@/components/UserMessage/index.vue'
|
||||||
import mihoutai from '@/assets/images/mihoutai.png'
|
import mihoutai from '@/assets/images/mihoutai.png'
|
||||||
import V3Emoji from "vue3-emoji";
|
import V3Emoji from "vue3-emoji";
|
||||||
import { getUserList, send_message, get_messages, send_template_message } from '@/api/chat'
|
import { getUserList, send_message, get_messages, send_template_message, checkMiniProgramStatus } from '@/api/chat'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { uploadFile } from '@/api/upload'
|
import { uploadFile } from '@/api/upload'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
|
import appDetail from '@/store/modules/appDetail'
|
||||||
|
const appInfoStore = appDetail()
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
@ -101,6 +96,8 @@ const messageTimer = ref(null) // 定时器实例
|
|||||||
const isSending = ref(false) // 防止重复发送
|
const isSending = ref(false) // 防止重复发送
|
||||||
const lastSendTime = ref(0) // 最后发送时间,用于防抖
|
const lastSendTime = ref(0) // 最后发送时间,用于防抖
|
||||||
const isUploading = ref(false) // 防止重复上传
|
const isUploading = ref(false) // 防止重复上传
|
||||||
|
// 小程序状态轮询定时器
|
||||||
|
const appStatusTimer = ref(null)
|
||||||
|
|
||||||
// 选择用户
|
// 选择用户
|
||||||
const onSelectUser = async (id) => {
|
const onSelectUser = async (id) => {
|
||||||
@ -855,22 +852,65 @@ const sendTemplateMessage = async (templateId) => {
|
|||||||
"app_secret": route.query.app_secret,
|
"app_secret": route.query.app_secret,
|
||||||
"template_id": route.query.template_id,
|
"template_id": route.query.template_id,
|
||||||
"touser": activeUser.value.sender_id
|
"touser": activeUser.value.sender_id
|
||||||
})
|
}).then((res) => {
|
||||||
ElMessage({ type: 'success', message: '模板消息发送成功' })
|
if (res.succes == true) {
|
||||||
|
ElMessage({ type: 'success', message: '模板消息发送成功' })
|
||||||
|
// 发送成功后,立即轮询获取最新消息以获取真实ID
|
||||||
|
setTimeout(() => {
|
||||||
|
getMessages(false, 1) // 轮询最新消息
|
||||||
|
}, 500)
|
||||||
|
} else {
|
||||||
|
ElMessage({ type: 'error', message: res.message || '模板消息发送失败' })
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
// 发送成功后,立即轮询获取最新消息以获取真实ID
|
|
||||||
setTimeout(() => {
|
|
||||||
getMessages(false, 1) // 轮询最新消息
|
|
||||||
}, 500)
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('模板消息发送失败:', error)
|
console.error('模板消息发送失败:', error)
|
||||||
ElMessage({ type: 'error', message: '模板消息发送失败' })
|
ElMessage({ type: 'error', message: '模板消息发送失败' })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const getAppStaus = async () => {
|
||||||
|
try {
|
||||||
|
const res = await checkMiniProgramStatus({
|
||||||
|
app_id: route.query.app_id
|
||||||
|
})
|
||||||
|
// appInfoStore.appDetail 是一个 ref,使用 .value 同步
|
||||||
|
if (appInfoStore && appInfoStore.appDetail) {
|
||||||
|
appInfoStore.appDetail.check_status_text = res.check_status_text || ''
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
} catch (error) {
|
||||||
|
if (appInfoStore && appInfoStore.appDetail) {
|
||||||
|
appInfoStore.appDetail.check_status_text = ''
|
||||||
|
}
|
||||||
|
console.error('获取应用状态失败:', error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const startAppStatusTimer = () => {
|
||||||
|
if (appStatusTimer.value) clearInterval(appStatusTimer.value)
|
||||||
|
// 立即触发一次,然后每60秒触发
|
||||||
|
getAppStaus()
|
||||||
|
appStatusTimer.value = setInterval(() => {
|
||||||
|
if (!route.query.app_id) return
|
||||||
|
getAppStaus()
|
||||||
|
}, 60 * 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
const stopAppStatusTimer = () => {
|
||||||
|
if (appStatusTimer.value) {
|
||||||
|
clearInterval(appStatusTimer.value)
|
||||||
|
appStatusTimer.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
// 组件挂载时
|
// 组件挂载时
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
getUsers()
|
getUsers()
|
||||||
|
getAppStaus()
|
||||||
// 启动用户列表轮询
|
// 启动用户列表轮询
|
||||||
startUserListTimer()
|
startUserListTimer()
|
||||||
|
|
||||||
@ -984,24 +1024,30 @@ watch(messages, async (newVal, oldVal) => {
|
|||||||
/* For WebKit-based browsers (Chrome, Safari, Edge Chromium) */
|
/* For WebKit-based browsers (Chrome, Safari, Edge Chromium) */
|
||||||
-webkit-overflow-scrolling: touch;
|
-webkit-overflow-scrolling: touch;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-body::-webkit-scrollbar {
|
.chat-body::-webkit-scrollbar {
|
||||||
width: 0px;
|
width: 0px;
|
||||||
height: 0px;
|
height: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* For Firefox */
|
/* For Firefox */
|
||||||
.chat-body {
|
.chat-body {
|
||||||
scrollbar-width: none; /* Firefox */
|
scrollbar-width: none;
|
||||||
|
/* Firefox */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* For IE and older Edge */
|
/* For IE and older Edge */
|
||||||
.chat-body {
|
.chat-body {
|
||||||
-ms-overflow-style: none; /* IE 10+ */
|
-ms-overflow-style: none;
|
||||||
|
/* IE 10+ */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* When loading messages during user switch, hide visual content to avoid jump/flicker */
|
/* When loading messages during user switch, hide visual content to avoid jump/flicker */
|
||||||
.chat-body--loading {
|
.chat-body--loading {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 180ms ease-in-out;
|
transition: opacity 180ms ease-in-out;
|
||||||
pointer-events: none; /* prevent interaction during transient loading */
|
pointer-events: none;
|
||||||
|
/* prevent interaction during transient loading */
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载更多样式
|
// 加载更多样式
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user