feat: MiniProgram IM Demo - login + chat pages

这个提交包含在:
徐勤民 2026-04-30 19:35:55 +08:00
当前提交 5ec928da81
共有 14 个文件被更改,包括 527 次插入0 次删除

15
app.js 普通文件
查看文件

@ -0,0 +1,15 @@
const localIP = '10.222.233.79'
App({
globalData: {
sdk: null,
userId: '',
token: '',
baseUrl: `http://${localIP}:8082`,
wsUrl: `ws://${localIP}:8082/ws/im`,
},
onLaunch() {
console.log('MiniProgram Demo launched')
},
})

14
app.json 普通文件
查看文件

@ -0,0 +1,14 @@
{
"pages": [
"pages/login/login",
"pages/chat/chat"
],
"window": {
"backgroundTextStyle": "light",
"navigationBarBackgroundColor": "#fff",
"navigationBarTitleText": "Xuqm IM Demo",
"navigationBarTextStyle": "black"
},
"style": "v2",
"sitemapLocation": "sitemap.json"
}

9
package.json 普通文件
查看文件

@ -0,0 +1,9 @@
{
"name": "xuqm-im-miniprogram-demo",
"version": "0.1.0",
"description": "XuqmGroup WeChat MiniProgram IM Demo",
"private": true,
"dependencies": {
"xuqm-group-wechat-mini-program-sdk": "file:../XuqmGroup-WeChatMiniProgramSDK"
}
}

104
pages/chat/chat.js 普通文件
查看文件

@ -0,0 +1,104 @@
const app = getApp()
Page({
data: {
userId: '',
connected: false,
activeTab: 'conv',
conversations: [],
friends: [],
messages: [],
currentTarget: '',
currentChatType: 'SINGLE',
inputText: '',
},
onLoad() {
const sdk = app.globalData.sdk
const userId = app.globalData.userId
this.setData({ userId })
sdk.on('connected', () => this.setData({ connected: true }))
sdk.on('disconnected', () => this.setData({ connected: false }))
sdk.on('message', (msg) => this.handleMessage(msg))
sdk.on('revoke', ({ msgId }) => this.handleRevoke(msgId))
this.loadConversations()
this.loadFriends()
},
onUnload() {
app.globalData.sdk?.disconnect()
},
handleMessage(msg) {
const messages = [...this.data.messages, msg]
this.setData({ messages })
},
handleRevoke(msgId) {
const messages = this.data.messages.map(m =>
m.id === msgId ? { ...m, revoked: true, content: '' } : m
)
this.setData({ messages })
},
async loadConversations() {
try {
const conversations = await app.globalData.sdk.listConversations()
this.setData({ conversations })
} catch (err) {
console.error('loadConversations failed', err)
}
},
async loadFriends() {
try {
const friends = await app.globalData.sdk.listFriends()
this.setData({ friends })
} catch (err) {
console.error('loadFriends failed', err)
}
},
switchTab(e) {
this.setData({ activeTab: e.currentTarget.dataset.tab })
},
selectConv(e) {
const { id, type } = e.currentTarget.dataset
this.setData({ currentTarget: id, currentChatType: type, messages: [] })
this.loadHistory(id, type)
},
startChat(e) {
const id = e.currentTarget.dataset.id
this.setData({ currentTarget: id, currentChatType: 'SINGLE', messages: [] })
this.loadHistory(id, 'SINGLE')
},
async loadHistory(targetId, chatType) {
try {
const result = await app.globalData.sdk.fetchHistory(targetId, { page: 0, size: 50 })
this.setData({ messages: result.content || [] })
} catch (err) {
console.error('loadHistory failed', err)
}
},
onInput(e) {
this.setData({ inputText: e.detail.value })
},
sendText() {
const { inputText, currentTarget, currentChatType } = this.data
if (!inputText.trim() || !currentTarget) return
app.globalData.sdk.send({
toId: currentTarget,
chatType: currentChatType,
msgType: 'TEXT',
content: inputText.trim(),
})
this.setData({ inputText: '' })
},
})

4
pages/chat/chat.json 普通文件
查看文件

@ -0,0 +1,4 @@
{
"usingComponents": {},
"navigationBarTitleText": "聊天"
}

53
pages/chat/chat.wxml 普通文件
查看文件

@ -0,0 +1,53 @@
<view class="chat-container">
<view class="header">
<text class="status {{connected ? 'online' : 'offline'}}">{{connected ? '已连接' : '未连接'}}</text>
<text class="user">当前用户: {{userId}}</text>
</view>
<view class="sidebar">
<view class="tab-bar">
<view class="tab {{activeTab==='conv'?'active':''}}" bindtap="switchTab" data-tab="conv">会话</view>
<view class="tab {{activeTab==='friend'?'active':''}}" bindtap="switchTab" data-tab="friend">好友</view>
</view>
<scroll-view scroll-y class="list">
<block wx:if="{{activeTab==='conv'}}">
<view wx:for="{{conversations}}" wx:key="targetId" class="item" bindtap="selectConv" data-id="{{item.targetId}}" data-type="{{item.chatType}}">
<view class="item-title">{{item.targetId}}</view>
<view class="item-meta">{{item.lastMsgContent||'暂无消息'}}</view>
</view>
<view wx:if="{{conversations.length===0}}" class="empty">暂无会话</view>
</block>
<block wx:if="{{activeTab==='friend'}}">
<view wx:for="{{friends}}" wx:key="*this" class="item" bindtap="startChat" data-id="{{item}}">
<view class="item-title">{{item}}</view>
</view>
<view wx:if="{{friends.length===0}}" class="empty">暂无好友</view>
</block>
</scroll-view>
</view>
<view class="chat-area" wx:if="{{currentTarget}}">
<view class="chat-header">{{currentTarget}} ({{currentChatType==='SINGLE'?'单聊':'群聊'}})</view>
<scroll-view scroll-y class="message-list" scroll-into-view="msg-{{messages.length-1}}">
<view wx:for="{{messages}}" wx:key="id" id="msg-{{index}}" class="msg-row {{item.fromId===userId?'self':'other'}}">
<view class="msg-bubble">
<view class="msg-sender">{{item.fromId||item.fromUserId}}</view>
<view class="msg-content">{{item.revoked?'消息已撤回':item.content}}</view>
<view class="msg-meta">
<text class="tag">{{item.status}}</text>
<text class="time">{{item.createdAt}}</text>
</view>
</view>
</view>
</scroll-view>
<view class="input-area">
<input class="msg-input" placeholder="输入消息" value="{{inputText}}" bindinput="onInput" confirm-type="send" bindconfirm="sendText" />
<button class="send-btn" type="primary" size="mini" bindtap="sendText">发送</button>
</view>
</view>
<view wx:else class="empty-chat">选择一个会话开始聊天</view>
</view>

152
pages/chat/chat.wxss 普通文件
查看文件

@ -0,0 +1,152 @@
.chat-container {
display: flex;
flex-direction: column;
height: 100vh;
background: #f5f7fa;
}
.header {
display: flex;
justify-content: space-between;
padding: 16rpx 24rpx;
background: #fff;
border-bottom: 1rpx solid #eee;
}
.status {
font-size: 28rpx;
}
.online { color: #07c160; }
.offline { color: #e64340; }
.user { font-size: 28rpx; color: #666; }
.sidebar {
display: flex;
flex-direction: row;
flex: 1;
overflow: hidden;
}
.tab-bar {
width: 160rpx;
background: #fff;
border-right: 1rpx solid #eee;
}
.tab {
padding: 24rpx 0;
text-align: center;
font-size: 28rpx;
color: #666;
border-bottom: 1rpx solid #f0f0f0;
}
.tab.active {
color: #1989fa;
background: #eef5ff;
}
.list {
flex: 1;
padding: 16rpx;
}
.item {
padding: 20rpx;
background: #fff;
border-radius: 8rpx;
margin-bottom: 16rpx;
}
.item-title {
font-size: 30rpx;
font-weight: 500;
color: #333;
}
.item-meta {
font-size: 24rpx;
color: #999;
margin-top: 8rpx;
}
.empty {
text-align: center;
color: #999;
padding: 40rpx;
}
.chat-area {
flex: 1;
display: flex;
flex-direction: column;
}
.chat-header {
padding: 20rpx;
background: #fff;
text-align: center;
font-size: 30rpx;
border-bottom: 1rpx solid #eee;
}
.message-list {
flex: 1;
padding: 20rpx;
}
.msg-row {
display: flex;
margin-bottom: 20rpx;
}
.msg-row.self {
justify-content: flex-end;
}
.msg-row.other {
justify-content: flex-start;
}
.msg-bubble {
max-width: 70%;
padding: 16rpx 20rpx;
border-radius: 12rpx;
background: #fff;
}
.msg-row.self .msg-bubble {
background: #95ec69;
}
.msg-sender {
font-size: 22rpx;
color: #999;
margin-bottom: 4rpx;
}
.msg-content {
font-size: 30rpx;
color: #333;
word-break: break-all;
}
.msg-meta {
display: flex;
justify-content: space-between;
margin-top: 8rpx;
}
.tag {
font-size: 20rpx;
color: #1989fa;
}
.time {
font-size: 20rpx;
color: #999;
}
.input-area {
display: flex;
padding: 16rpx;
background: #fff;
border-top: 1rpx solid #eee;
}
.msg-input {
flex: 1;
height: 72rpx;
border: 1rpx solid #ddd;
border-radius: 8rpx;
padding: 0 16rpx;
font-size: 30rpx;
}
.send-btn {
margin-left: 16rpx;
}
.empty-chat {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
color: #999;
font-size: 32rpx;
}

45
pages/login/login.js 普通文件
查看文件

@ -0,0 +1,45 @@
const app = getApp()
// 引入 SDK实际使用时通过 npm 包或复制 dist 文件)
const { XuqmMiniProgramSDK } = require('../../utils/sdk')
Page({
data: {
userId: 'user_a',
password: '123456',
loading: false,
error: '',
},
onLoad() {
const sdk = new XuqmMiniProgramSDK()
const { baseUrl, wsUrl } = app.globalData
sdk.init({ appKey: 'ak_demo_chat', appSecret: 'secret', debug: true, baseUrl, wsUrl })
app.globalData.sdk = sdk
},
onUserIdInput(e) {
this.setData({ userId: e.detail.value })
},
onPasswordInput(e) {
this.setData({ password: e.detail.value })
},
async onLogin() {
const { userId, password } = this.data
if (!userId || !password) {
this.setData({ error: '请输入用户ID和密码' })
return
}
this.setData({ loading: true, error: '' })
try {
const sdk = app.globalData.sdk
await sdk.loginWithDemo(userId, password)
app.globalData.userId = userId
wx.navigateTo({ url: '/pages/chat/chat' })
} catch (err) {
this.setData({ error: err.message || '登录失败', loading: false })
}
},
})

4
pages/login/login.json 普通文件
查看文件

@ -0,0 +1,4 @@
{
"usingComponents": {},
"navigationBarTitleText": "登录"
}

7
pages/login/login.wxml 普通文件
查看文件

@ -0,0 +1,7 @@
<view class="login-container">
<view class="title">Xuqm IM Demo</view>
<input class="input" placeholder="请输入用户ID" value="{{userId}}" bindinput="onUserIdInput" />
<input class="input" placeholder="请输入密码" password value="{{password}}" bindinput="onPasswordInput" />
<button class="btn" type="primary" bindtap="onLogin" loading="{{loading}}">登录</button>
<view wx:if="{{error}}" class="error">{{error}}</view>
</view>

31
pages/login/login.wxss 普通文件
查看文件

@ -0,0 +1,31 @@
.login-container {
padding: 80rpx 40rpx;
display: flex;
flex-direction: column;
align-items: center;
}
.title {
font-size: 48rpx;
font-weight: bold;
margin-bottom: 60rpx;
color: #333;
}
.input {
width: 100%;
height: 88rpx;
border: 1rpx solid #ddd;
border-radius: 8rpx;
padding: 0 24rpx;
margin-bottom: 24rpx;
font-size: 32rpx;
box-sizing: border-box;
}
.btn {
width: 100%;
margin-top: 24rpx;
}
.error {
color: #e64340;
margin-top: 24rpx;
font-size: 28rpx;
}

77
project.config.json 普通文件
查看文件

@ -0,0 +1,77 @@
{
"description": "XuqmGroup WeChat MiniProgram IM Demo",
"packOptions": {
"ignore": []
},
"setting": {
"urlCheck": false,
"es6": true,
"enhance": true,
"postcss": true,
"preloadBackgroundData": false,
"minified": true,
"newFeature": false,
"coverView": true,
"nodeModules": false,
"autoAudits": false,
"showShadowRootInWxmlPanel": true,
"scopeDataCheck": false,
"uglifyFileName": false,
"checkInvalidKey": true,
"checkSiteMap": true,
"uploadWithSourceMap": true,
"compileHotReLoad": false,
"lazyloadPlaceholderEnable": false,
"useMultiFrameRuntime": true,
"useApiHook": true,
"useApiHostProcess": true,
"babelSetting": {
"ignore": [],
"disablePlugins": [],
"outputPath": ""
},
"enableEngineNative": false,
"useIsolateContext": true,
"userConfirmedBundleSwitch": false,
"packNpmManually": false,
"packNpmRelationList": [],
"minifyWXSS": true,
"disableUseStrict": false,
"minifyWXML": true,
"showES6CompileOption": false,
"useCompilerPlugins": false
},
"compileType": "miniprogram",
"libVersion": "2.32.0",
"appid": "touristappid",
"projectname": "XuqmIMDemo",
"debugOptions": {
"hidedInDevtools": []
},
"scripts": {},
"staticServerOptions": {
"baseURL": "",
"servePath": ""
},
"isGameTourist": false,
"condition": {
"search": {
"list": []
},
"conversation": {
"list": []
},
"game": {
"list": []
},
"plugin": {
"list": []
},
"gamePlugin": {
"list": []
},
"miniprogram": {
"list": []
}
}
}

7
sitemap.json 普通文件
查看文件

@ -0,0 +1,7 @@
{
"desc": "关于本小程序的索引",
"rules": [{
"action": "allow",
"page": "*"
}]
}

5
utils/sdk.js 普通文件
查看文件

@ -0,0 +1,5 @@
// 微信小程序 Demo 的 SDK 入口
// 实际使用时通过 npm install 引入,这里直接引用 SDK 构建产物
const { XuqmMiniProgramSDK } = require('../miniprogram_npm/xuqm-group-wechat-mini-program-sdk/index.js')
module.exports = { XuqmMiniProgramSDK }