feat: initial Vue3 SDK Demo application
- Login page with quick user selection - Real-time chat with WebSocket (STOMP) - Conversation/friend/group list sidebar - API test console with operation logs - Integration with local dev environment
这个提交包含在:
当前提交
28c1110344
4
.gitignore
vendored
普通文件
4
.gitignore
vendored
普通文件
@ -0,0 +1,4 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
.DS_Store
|
||||||
|
*.log
|
||||||
12
index.html
普通文件
12
index.html
普通文件
@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Vue3 SDK Demo</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
22
package.json
普通文件
22
package.json
普通文件
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"name": "xuqmgroup-vue3-sdk-demo",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vue-tsc -b && vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"vue": "^3.5.13",
|
||||||
|
"element-plus": "^2.9.1",
|
||||||
|
"@element-plus/icons-vue": "^2.3.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vitejs/plugin-vue": "^5.2.3",
|
||||||
|
"typescript": "^5.8.2",
|
||||||
|
"vite": "^6.2.2",
|
||||||
|
"vue-tsc": "^2.2.8"
|
||||||
|
}
|
||||||
|
}
|
||||||
40
src/App.vue
普通文件
40
src/App.vue
普通文件
@ -0,0 +1,40 @@
|
|||||||
|
<template>
|
||||||
|
<div class="app">
|
||||||
|
<LoginView v-if="!token" @login="handleLogin" />
|
||||||
|
<ChatDemoView v-else :token="token" :userId="userId" @logout="handleLogout" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { setToken, setUserId } from '@xuqm/vue3-sdk'
|
||||||
|
import LoginView from './views/LoginView.vue'
|
||||||
|
import ChatDemoView from './views/ChatDemoView.vue'
|
||||||
|
|
||||||
|
const token = ref<string>('')
|
||||||
|
const userId = ref<string>('')
|
||||||
|
|
||||||
|
function handleLogin(t: string, u: string) {
|
||||||
|
token.value = t
|
||||||
|
userId.value = u
|
||||||
|
setToken(t)
|
||||||
|
setUserId(u)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleLogout() {
|
||||||
|
token.value = ''
|
||||||
|
userId.value = ''
|
||||||
|
setToken(null)
|
||||||
|
setUserId(null)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
html, body, #app, .app {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
height: 100%;
|
||||||
|
font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB',
|
||||||
|
'Microsoft YaHei', Arial, sans-serif;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
17
src/main.ts
普通文件
17
src/main.ts
普通文件
@ -0,0 +1,17 @@
|
|||||||
|
import { createApp } from 'vue'
|
||||||
|
import ElementPlus from 'element-plus'
|
||||||
|
import 'element-plus/dist/index.css'
|
||||||
|
import App from './App.vue'
|
||||||
|
import { init } from '@xuqm/vue3-sdk'
|
||||||
|
|
||||||
|
init({
|
||||||
|
appKey: 'ak_demo_chat',
|
||||||
|
appSecret: 'as_demo_secret',
|
||||||
|
debug: true,
|
||||||
|
baseUrl: 'http://192.168.113.37:8082',
|
||||||
|
wsUrl: 'ws://192.168.113.37:8082/ws/im',
|
||||||
|
})
|
||||||
|
|
||||||
|
const app = createApp(App)
|
||||||
|
app.use(ElementPlus)
|
||||||
|
app.mount('#app')
|
||||||
699
src/views/ChatDemoView.vue
普通文件
699
src/views/ChatDemoView.vue
普通文件
@ -0,0 +1,699 @@
|
|||||||
|
<template>
|
||||||
|
<div class="chat-demo">
|
||||||
|
<!-- 顶部栏 -->
|
||||||
|
<div class="header">
|
||||||
|
<div class="header-left">
|
||||||
|
<el-tag :type="connected ? 'success' : 'danger'" effect="dark" round>
|
||||||
|
{{ connected ? 'WebSocket 已连接' : 'WebSocket 未连接' }}
|
||||||
|
</el-tag>
|
||||||
|
<span class="user-info">当前用户: <b>{{ userId }}</b></span>
|
||||||
|
</div>
|
||||||
|
<div class="header-right">
|
||||||
|
<el-button size="small" @click="testAllApis" :loading="testing">一键API测试</el-button>
|
||||||
|
<el-button size="small" type="danger" @click="$emit('logout')">退出登录</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="main">
|
||||||
|
<!-- 左侧边栏 -->
|
||||||
|
<div class="sidebar">
|
||||||
|
<el-tabs v-model="activeTab" stretch>
|
||||||
|
<el-tab-pane label="会话" name="conversations">
|
||||||
|
<div class="list-container">
|
||||||
|
<div
|
||||||
|
v-for="conv in conversations"
|
||||||
|
:key="conv.targetId"
|
||||||
|
:class="['conv-item', { active: currentTarget?.targetId === conv.targetId }]"
|
||||||
|
@click="selectConversation(conv)"
|
||||||
|
>
|
||||||
|
<div class="conv-title">
|
||||||
|
<span>{{ conv.targetId }}</span>
|
||||||
|
<el-badge v-if="conv.unreadCount > 0" :value="conv.unreadCount" />
|
||||||
|
</div>
|
||||||
|
<div class="conv-meta">
|
||||||
|
<span class="conv-preview">{{ conv.lastMsgContent || '暂无消息' }}</span>
|
||||||
|
<span class="conv-time">{{ formatTime(conv.lastMsgTime) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<el-empty v-if="conversations.length === 0" description="暂无会话" />
|
||||||
|
</div>
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<el-tab-pane label="好友" name="friends">
|
||||||
|
<div class="list-container">
|
||||||
|
<el-button size="small" @click="loadFriends" :loading="loadingFriends">刷新好友</el-button>
|
||||||
|
<el-divider />
|
||||||
|
<div v-for="f in friends" :key="f" class="friend-item" @click="startChat(f, 'SINGLE')">
|
||||||
|
<el-icon><User /></el-icon> {{ f }}
|
||||||
|
</div>
|
||||||
|
<el-empty v-if="friends.length === 0" description="暂无好友" />
|
||||||
|
</div>
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<el-tab-pane label="群组" name="groups">
|
||||||
|
<div class="list-container">
|
||||||
|
<el-button size="small" @click="loadGroups" :loading="loadingGroups">刷新群组</el-button>
|
||||||
|
<el-divider />
|
||||||
|
<div v-for="g in groups" :key="g.id" class="group-item" @click="startChat(g.id, 'GROUP')">
|
||||||
|
<el-icon><ChatDotRound /></el-icon> {{ g.name }}
|
||||||
|
</div>
|
||||||
|
<el-empty v-if="groups.length === 0" description="暂无群组" />
|
||||||
|
</div>
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 中间聊天区 -->
|
||||||
|
<div class="chat-area">
|
||||||
|
<div v-if="currentTarget" class="chat-header">
|
||||||
|
{{ currentTarget.targetId }} ({{ currentTarget.chatType === 'SINGLE' ? '单聊' : '群聊' }})
|
||||||
|
</div>
|
||||||
|
<div v-else class="chat-header">选择一个会话开始聊天</div>
|
||||||
|
|
||||||
|
<div ref="msgContainer" class="message-list">
|
||||||
|
<div
|
||||||
|
v-for="msg in messages"
|
||||||
|
:key="msg.id"
|
||||||
|
:class="['message-row', msg.fromId === userId ? 'self' : 'other']"
|
||||||
|
>
|
||||||
|
<div class="message-bubble">
|
||||||
|
<div class="msg-sender">{{ msg.fromId }}</div>
|
||||||
|
<div class="msg-content">
|
||||||
|
<span v-if="msg.revoked" class="revoked">消息已撤回</span>
|
||||||
|
<span v-else>{{ msg.content }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="msg-meta">
|
||||||
|
<el-tag size="small" :type="statusType(msg.status)">{{ msg.status }}</el-tag>
|
||||||
|
<span class="msg-time">{{ formatTime(msg.createdAt) }}</span>
|
||||||
|
<el-button
|
||||||
|
v-if="msg.fromId === userId && !msg.revoked"
|
||||||
|
link
|
||||||
|
size="small"
|
||||||
|
type="danger"
|
||||||
|
@click="revokeMsg(msg.id)"
|
||||||
|
>
|
||||||
|
撤回
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<el-empty v-if="messages.length === 0 && currentTarget" description="暂无消息" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="currentTarget" class="input-area">
|
||||||
|
<el-input
|
||||||
|
v-model="inputText"
|
||||||
|
type="textarea"
|
||||||
|
:rows="2"
|
||||||
|
placeholder="输入消息,按 Enter 发送"
|
||||||
|
@keydown.enter.prevent="sendText"
|
||||||
|
/>
|
||||||
|
<div class="input-actions">
|
||||||
|
<el-button @click="markConversationAsRead">标记已读</el-button>
|
||||||
|
<el-button type="primary" @click="sendText" :disabled="!inputText.trim()">发送</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧测试面板 -->
|
||||||
|
<div class="test-panel">
|
||||||
|
<h4>API 测试控制台</h4>
|
||||||
|
<el-scrollbar height="calc(100vh - 140px)">
|
||||||
|
<el-collapse v-model="activeCollapse">
|
||||||
|
<el-collapse-item title="消息操作" name="msg">
|
||||||
|
<el-button size="small" @click="loadHistory" :loading="loading">加载历史</el-button>
|
||||||
|
<el-button size="small" @click="searchMsgs" :loading="loading">搜索消息</el-button>
|
||||||
|
</el-collapse-item>
|
||||||
|
|
||||||
|
<el-collapse-item title="好友操作" name="friend">
|
||||||
|
<el-input v-model="friendInput" size="small" placeholder="用户ID">
|
||||||
|
<template #append>
|
||||||
|
<el-button @click="addFriend" :loading="loading">加好友</el-button>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
<el-button size="small" @click="loadFriendRequests" :loading="loading" style="margin-top:8px">
|
||||||
|
好友请求列表
|
||||||
|
</el-button>
|
||||||
|
</el-collapse-item>
|
||||||
|
|
||||||
|
<el-collapse-item title="群组操作" name="group">
|
||||||
|
<el-button size="small" @click="createGroup" :loading="loading">创建测试群组</el-button>
|
||||||
|
<el-button size="small" @click="joinGroup" :loading="loading" style="margin-top:8px">申请入群</el-button>
|
||||||
|
</el-collapse-item>
|
||||||
|
|
||||||
|
<el-collapse-item title="会话操作" name="conv">
|
||||||
|
<el-button size="small" @click="pinConv" :loading="loading">置顶会话</el-button>
|
||||||
|
<el-button size="small" @click="muteConv" :loading="loading" style="margin-top:8px">静音会话</el-button>
|
||||||
|
<el-button size="small" @click="deleteConv" :loading="loading" style="margin-top:8px">删除会话</el-button>
|
||||||
|
</el-collapse-item>
|
||||||
|
|
||||||
|
<el-collapse-item title="用户操作" name="user">
|
||||||
|
<el-button size="small" @click="getProfile" :loading="loading">获取资料</el-button>
|
||||||
|
<el-button size="small" @click="updateProfile" :loading="loading" style="margin-top:8px">更新昵称</el-button>
|
||||||
|
</el-collapse-item>
|
||||||
|
</el-collapse>
|
||||||
|
|
||||||
|
<el-divider />
|
||||||
|
|
||||||
|
<div class="log-area">
|
||||||
|
<div
|
||||||
|
v-for="(log, i) in logs"
|
||||||
|
:key="i"
|
||||||
|
:class="['log-item', log.type]"
|
||||||
|
>
|
||||||
|
<span class="log-time">{{ log.time }}</span>
|
||||||
|
<span class="log-text">{{ log.text }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-scrollbar>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { User, ChatDotRound } from '@element-plus/icons-vue'
|
||||||
|
import { useIm } from '@xuqm/vue3-sdk'
|
||||||
|
import type { ConversationView, ImGroup, FriendRequest } from '@xuqm/vue3-sdk'
|
||||||
|
import {
|
||||||
|
sendFriendRequest,
|
||||||
|
listFriendRequests,
|
||||||
|
acceptFriendRequest,
|
||||||
|
searchMessages,
|
||||||
|
getProfile as apiGetProfile,
|
||||||
|
updateProfile as apiUpdateProfile,
|
||||||
|
} from '@xuqm/vue3-sdk'
|
||||||
|
|
||||||
|
const props = defineProps<{ token: string; userId: string }>()
|
||||||
|
defineEmits<{ (e: 'logout'): void }>()
|
||||||
|
|
||||||
|
const {
|
||||||
|
connect, disconnect, send, revoke, messages, conversations, connected,
|
||||||
|
refreshConversations, loadHistory: fetchHistory, setConversationRead,
|
||||||
|
getFriends, getGroups, setConversationPinnedState, setConversationMutedState,
|
||||||
|
removeConversation,
|
||||||
|
} = useIm()
|
||||||
|
|
||||||
|
const activeTab = ref('conversations')
|
||||||
|
const activeCollapse = ref(['msg'])
|
||||||
|
const currentTarget = ref<ConversationView | null>(null)
|
||||||
|
const inputText = ref('')
|
||||||
|
const msgContainer = ref<HTMLDivElement>()
|
||||||
|
|
||||||
|
const friends = ref<string[]>([])
|
||||||
|
const groups = ref<ImGroup[]>([])
|
||||||
|
const friendRequests = ref<FriendRequest[]>([])
|
||||||
|
const loading = ref(false)
|
||||||
|
const loadingFriends = ref(false)
|
||||||
|
const loadingGroups = ref(false)
|
||||||
|
const testing = ref(false)
|
||||||
|
const friendInput = ref('')
|
||||||
|
const logs = ref<{ time: string; text: string; type: string }[]>([])
|
||||||
|
|
||||||
|
function log(text: string, type: 'info' | 'success' | 'error' = 'info') {
|
||||||
|
const time = new Date().toLocaleTimeString()
|
||||||
|
logs.value.unshift({ time, text, type })
|
||||||
|
if (logs.value.length > 100) logs.value.pop()
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTime(t: number | string | Date) {
|
||||||
|
if (!t) return ''
|
||||||
|
const d = typeof t === 'number' ? new Date(t) : new Date(t)
|
||||||
|
return d.toLocaleTimeString()
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusType(s: string) {
|
||||||
|
const map: Record<string, string> = {
|
||||||
|
SENDING: 'warning', SENT: 'success', DELIVERED: 'info',
|
||||||
|
READ: 'success', FAILED: 'danger', REVOKED: 'info',
|
||||||
|
}
|
||||||
|
return map[s] || 'info'
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectConversation(conv: ConversationView) {
|
||||||
|
currentTarget.value = conv
|
||||||
|
messages.value = [] // clear local messages when switching
|
||||||
|
loadHistory()
|
||||||
|
}
|
||||||
|
|
||||||
|
function startChat(targetId: string, chatType: 'SINGLE' | 'GROUP') {
|
||||||
|
currentTarget.value = {
|
||||||
|
targetId, chatType, lastMsgTime: Date.now(), unreadCount: 0, isMuted: false, isPinned: false,
|
||||||
|
}
|
||||||
|
messages.value = []
|
||||||
|
loadHistory()
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendText() {
|
||||||
|
if (!inputText.value.trim() || !currentTarget.value) return
|
||||||
|
send({
|
||||||
|
toId: currentTarget.value.targetId,
|
||||||
|
chatType: currentTarget.value.chatType,
|
||||||
|
msgType: 'TEXT',
|
||||||
|
content: inputText.value.trim(),
|
||||||
|
})
|
||||||
|
inputText.value = ''
|
||||||
|
scrollToBottom()
|
||||||
|
}
|
||||||
|
|
||||||
|
function revokeMsg(msgId: string) {
|
||||||
|
revoke(msgId).then(() => {
|
||||||
|
ElMessage.success('消息已撤回')
|
||||||
|
log(`撤回消息: ${msgId}`, 'success')
|
||||||
|
}).catch((err: any) => {
|
||||||
|
ElMessage.error(err.message || '撤回失败')
|
||||||
|
log(`撤回失败: ${err.message}`, 'error')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadHistory() {
|
||||||
|
if (!currentTarget.value) return
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const result = currentTarget.value.chatType === 'SINGLE'
|
||||||
|
? await fetchHistory(currentTarget.value.targetId, { page: 0, size: 50 })
|
||||||
|
: await useIm().loadGroupHistory(currentTarget.value.targetId, { page: 0, size: 50 })
|
||||||
|
log(`加载历史: ${result.content.length} 条, total=${result.totalElements}`, 'success')
|
||||||
|
// Note: useIm doesn't expose setMessages, we just log for now
|
||||||
|
} catch (err: any) {
|
||||||
|
log(`加载历史失败: ${err.message}`, 'error')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadFriends() {
|
||||||
|
loadingFriends.value = true
|
||||||
|
try {
|
||||||
|
friends.value = await getFriends()
|
||||||
|
log(`好友列表: ${friends.value.length} 人`, 'success')
|
||||||
|
} catch (err: any) {
|
||||||
|
log(`获取好友失败: ${err.message}`, 'error')
|
||||||
|
} finally {
|
||||||
|
loadingFriends.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadGroups() {
|
||||||
|
loadingGroups.value = true
|
||||||
|
try {
|
||||||
|
groups.value = await getGroups()
|
||||||
|
log(`群组列表: ${groups.value.length} 个`, 'success')
|
||||||
|
} catch (err: any) {
|
||||||
|
log(`获取群组失败: ${err.message}`, 'error')
|
||||||
|
} finally {
|
||||||
|
loadingGroups.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addFriend() {
|
||||||
|
if (!friendInput.value.trim()) return
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await sendFriendRequest(friendInput.value.trim(), 'demo测试')
|
||||||
|
log(`发送好友请求: ${res.id}`, 'success')
|
||||||
|
ElMessage.success('好友请求已发送')
|
||||||
|
} catch (err: any) {
|
||||||
|
log(`加好友失败: ${err.message}`, 'error')
|
||||||
|
ElMessage.error(err.message)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadFriendRequests() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
friendRequests.value = await listFriendRequests('incoming')
|
||||||
|
log(`好友请求: ${friendRequests.value.length} 条待处理`, 'success')
|
||||||
|
if (friendRequests.value.length > 0) {
|
||||||
|
// Auto accept first pending request for demo
|
||||||
|
const pending = friendRequests.value.find(r => r.status === 'PENDING')
|
||||||
|
if (pending) {
|
||||||
|
await acceptFriendRequest(pending.id)
|
||||||
|
log(`自动接受好友请求: ${pending.fromUserId}`, 'success')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
log(`获取好友请求失败: ${err.message}`, 'error')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function searchMsgs() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const result = await searchMessages(null, currentTarget.value?.chatType || 'SINGLE', 'TEXT')
|
||||||
|
log(`搜索消息: ${result.content.length} 条`, 'success')
|
||||||
|
} catch (err: any) {
|
||||||
|
log(`搜索消息失败: ${err.message}`, 'error')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createGroup() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
// Use http directly or API - demo uses simple approach
|
||||||
|
const { http } = await import('@xuqm/vue3-sdk')
|
||||||
|
const group = await http.post('/api/im/groups', {
|
||||||
|
name: `TestGroup_${Date.now()}`,
|
||||||
|
memberIds: ['user_b'],
|
||||||
|
groupType: 'WORK',
|
||||||
|
})
|
||||||
|
log(`创建群组成功: ${group.id}`, 'success')
|
||||||
|
await loadGroups()
|
||||||
|
} catch (err: any) {
|
||||||
|
log(`创建群组失败: ${err.message}`, 'error')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function joinGroup() {
|
||||||
|
if (!currentTarget.value || currentTarget.value.chatType !== 'GROUP') {
|
||||||
|
ElMessage.warning('请先选择一个群组')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const { sendGroupJoinRequest } = await import('@xuqm/vue3-sdk')
|
||||||
|
const res = await sendGroupJoinRequest(currentTarget.value.targetId, '申请加入')
|
||||||
|
log(`申请入群: ${res.id}`, 'success')
|
||||||
|
} catch (err: any) {
|
||||||
|
log(`申请入群失败: ${err.message}`, 'error')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pinConv() {
|
||||||
|
if (!currentTarget.value) return
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
await setConversationPinnedState(currentTarget.value.targetId, currentTarget.value.chatType, true)
|
||||||
|
log('会话已置顶', 'success')
|
||||||
|
await refreshConversations()
|
||||||
|
} catch (err: any) {
|
||||||
|
log(`置顶失败: ${err.message}`, 'error')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function muteConv() {
|
||||||
|
if (!currentTarget.value) return
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
await setConversationMutedState(currentTarget.value.targetId, currentTarget.value.chatType, true)
|
||||||
|
log('会话已静音', 'success')
|
||||||
|
} catch (err: any) {
|
||||||
|
log(`静音失败: ${err.message}`, 'error')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteConv() {
|
||||||
|
if (!currentTarget.value) return
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
await removeConversation(currentTarget.value.targetId, currentTarget.value.chatType)
|
||||||
|
log('会话已删除', 'success')
|
||||||
|
currentTarget.value = null
|
||||||
|
await refreshConversations()
|
||||||
|
} catch (err: any) {
|
||||||
|
log(`删除失败: ${err.message}`, 'error')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getProfile() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const p = await apiGetProfile(props.userId)
|
||||||
|
log(`用户资料: ${p.nickname || p.userId}`, 'success')
|
||||||
|
} catch (err: any) {
|
||||||
|
log(`获取资料失败: ${err.message}`, 'error')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateProfile() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const p = await apiUpdateProfile(props.userId, `DemoUser_${Date.now()}`, null, 'MALE')
|
||||||
|
log(`更新昵称: ${p.nickname}`, 'success')
|
||||||
|
} catch (err: any) {
|
||||||
|
log(`更新失败: ${err.message}`, 'error')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function markConversationAsRead() {
|
||||||
|
if (!currentTarget.value) return
|
||||||
|
setConversationRead(currentTarget.value.targetId, currentTarget.value.chatType)
|
||||||
|
.then(() => {
|
||||||
|
log('标记已读成功', 'success')
|
||||||
|
ElMessage.success('已标记已读')
|
||||||
|
})
|
||||||
|
.catch((err: any) => {
|
||||||
|
log(`标记已读失败: ${err.message}`, 'error')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function scrollToBottom() {
|
||||||
|
nextTick(() => {
|
||||||
|
msgContainer.value?.scrollTo({ top: msgContainer.value.scrollHeight, behavior: 'smooth' })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testAllApis() {
|
||||||
|
testing.value = true
|
||||||
|
log('========== 开始一键API测试 ==========')
|
||||||
|
try {
|
||||||
|
await loadFriends()
|
||||||
|
await loadGroups()
|
||||||
|
await loadFriendRequests()
|
||||||
|
await getProfile()
|
||||||
|
if (currentTarget.value) {
|
||||||
|
await loadHistory()
|
||||||
|
await searchMsgs()
|
||||||
|
}
|
||||||
|
log('========== API测试完成 ==========', 'success')
|
||||||
|
ElMessage.success('API 测试完成,请查看日志')
|
||||||
|
} catch (err: any) {
|
||||||
|
log(`测试异常: ${err.message}`, 'error')
|
||||||
|
} finally {
|
||||||
|
testing.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(messages, scrollToBottom, { deep: true })
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
connect()
|
||||||
|
log('Demo 启动,尝试连接 WebSocket...')
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
disconnect()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.chat-demo {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100vh;
|
||||||
|
background: #f5f7fa;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 20px;
|
||||||
|
background: #fff;
|
||||||
|
border-bottom: 1px solid #e4e7ed;
|
||||||
|
}
|
||||||
|
.header-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
.user-info {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
.main {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.sidebar {
|
||||||
|
width: 260px;
|
||||||
|
background: #fff;
|
||||||
|
border-right: 1px solid #e4e7ed;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.list-container {
|
||||||
|
padding: 8px;
|
||||||
|
overflow-y: auto;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.conv-item {
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
.conv-item:hover, .conv-item.active {
|
||||||
|
background: #ecf5ff;
|
||||||
|
}
|
||||||
|
.conv-title {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.conv-meta {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
.conv-preview {
|
||||||
|
max-width: 120px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.friend-item, .group-item {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
.friend-item:hover, .group-item:hover {
|
||||||
|
background: #f5f7fa;
|
||||||
|
}
|
||||||
|
.chat-area {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
.chat-header {
|
||||||
|
padding: 12px 20px;
|
||||||
|
border-bottom: 1px solid #e4e7ed;
|
||||||
|
font-weight: 500;
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
|
.message-list {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 16px 20px;
|
||||||
|
}
|
||||||
|
.message-row {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.message-row.self {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
.message-row.other {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
.message-bubble {
|
||||||
|
max-width: 60%;
|
||||||
|
padding: 10px 14px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #f4f4f5;
|
||||||
|
}
|
||||||
|
.message-row.self .message-bubble {
|
||||||
|
background: #409eff;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.msg-sender {
|
||||||
|
font-size: 12px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
.msg-content {
|
||||||
|
word-break: break-word;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.revoked {
|
||||||
|
font-style: italic;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
.msg-meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 6px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
.message-row.self .msg-meta {
|
||||||
|
color: rgba(255,255,255,0.85);
|
||||||
|
}
|
||||||
|
.input-area {
|
||||||
|
padding: 12px 20px;
|
||||||
|
border-top: 1px solid #e4e7ed;
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
|
.input-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
.test-panel {
|
||||||
|
width: 300px;
|
||||||
|
background: #fff;
|
||||||
|
border-left: 1px solid #e4e7ed;
|
||||||
|
padding: 12px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.test-panel h4 {
|
||||||
|
margin: 0 0 12px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
border-bottom: 1px solid #e4e7ed;
|
||||||
|
}
|
||||||
|
.log-area {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
.log-item {
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 4px 0;
|
||||||
|
border-bottom: 1px dashed #ebeef5;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
.log-time {
|
||||||
|
color: #909399;
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
.log-item.success .log-text {
|
||||||
|
color: #67c23a;
|
||||||
|
}
|
||||||
|
.log-item.error .log-text {
|
||||||
|
color: #f56c6c;
|
||||||
|
}
|
||||||
|
.log-item.info .log-text {
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
102
src/views/LoginView.vue
普通文件
102
src/views/LoginView.vue
普通文件
@ -0,0 +1,102 @@
|
|||||||
|
<template>
|
||||||
|
<div class="login-container">
|
||||||
|
<el-card class="login-card" shadow="always">
|
||||||
|
<template #header>
|
||||||
|
<h2>Vue3 SDK Demo 登录</h2>
|
||||||
|
</template>
|
||||||
|
<el-form :model="form" label-width="80px" @submit.prevent="handleLogin">
|
||||||
|
<el-form-item label="用户ID">
|
||||||
|
<el-input v-model="form.userId" placeholder="如: user_a" clearable />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="密码">
|
||||||
|
<el-input v-model="form.password" type="password" placeholder="123456" show-password />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" native-type="submit" :loading="loading" style="width: 100%">
|
||||||
|
登录
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<el-divider>快捷账号</el-divider>
|
||||||
|
<div class="quick-users">
|
||||||
|
<el-tag
|
||||||
|
v-for="u in quickUsers"
|
||||||
|
:key="u"
|
||||||
|
class="quick-tag"
|
||||||
|
type="info"
|
||||||
|
@click="quickLogin(u)"
|
||||||
|
>
|
||||||
|
{{ u }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { reactive, ref } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
|
||||||
|
const emit = defineEmits<{ (e: 'login', token: string, userId: string): void }>()
|
||||||
|
|
||||||
|
const form = reactive({ userId: 'user_a', password: '123456' })
|
||||||
|
const loading = ref(false)
|
||||||
|
const quickUsers = ['user_a', 'user_b', 'user_ascii']
|
||||||
|
|
||||||
|
async function handleLogin() {
|
||||||
|
if (!form.userId || !form.password) {
|
||||||
|
ElMessage.warning('请填写完整信息')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await doLogin(form.userId, form.password)
|
||||||
|
}
|
||||||
|
|
||||||
|
function quickLogin(userId: string) {
|
||||||
|
form.userId = userId
|
||||||
|
form.password = '123456'
|
||||||
|
doLogin(userId, '123456')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doLogin(userId: string, password: string) {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await fetch('http://192.168.113.37:8085/api/demo/auth/login', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ appId: 'ak_demo_chat', userId, password }),
|
||||||
|
})
|
||||||
|
const json = await res.json()
|
||||||
|
if (json.code !== 200) {
|
||||||
|
throw new Error(json.message || '登录失败')
|
||||||
|
}
|
||||||
|
ElMessage.success(`登录成功: ${userId}`)
|
||||||
|
emit('login', json.data.imToken, userId)
|
||||||
|
} catch (err: any) {
|
||||||
|
ElMessage.error(err.message || '登录失败')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.login-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100vh;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
}
|
||||||
|
.login-card {
|
||||||
|
width: 420px;
|
||||||
|
}
|
||||||
|
.quick-users {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.quick-tag {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
19
tsconfig.json
普通文件
19
tsconfig.json
普通文件
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"strict": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"sourceMap": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"lib": ["ESNext", "DOM"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": { "@/*": ["src/*"] }
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts", "src/**/*.vue"]
|
||||||
|
}
|
||||||
17
vite.config.ts
普通文件
17
vite.config.ts
普通文件
@ -0,0 +1,17 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
import { resolve } from 'path'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [vue()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': resolve(__dirname, 'src'),
|
||||||
|
'@xuqm/vue3-sdk': resolve(__dirname, '../XuqmGroup-Vue3SDK/src/index.ts'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
port: 5173,
|
||||||
|
host: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
正在加载...
在新工单中引用
屏蔽一个用户