feat: ops租户筛选 + 服务去平台化 + 补全Flutter/H5/小程序文档

- AppListView: 添加租户模糊搜索筛选,显示租户名称
- AppDetailView: 功能服务表去掉平台列,按服务类型展示
- ops.ts: listApps 支持 tenantId 参数,AppItem 添加 tenantName
- 新增 Flutter/H5/小程序 setup.md 和 im.md 文档
- config.ts: sidebar 添加 Flutter/H5/小程序子页面

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
这个提交包含在:
XuqmGroup 2026-05-16 00:26:53 +08:00
父节点 ceb22c6286
当前提交 65914b0ec2
共有 10 个文件被更改,包括 702 次插入9 次删除

查看文件

@ -69,6 +69,8 @@ export default defineConfig({
],
'/flutter/': [
{ text: '概览', link: '/flutter/' },
{ text: '安装配置', link: '/flutter/setup' },
{ text: 'IM 接入', link: '/flutter/im' },
],
'/harmony/': [
{ text: '概览', link: '/harmony/' },
@ -79,9 +81,13 @@ export default defineConfig({
],
'/miniprogram/': [
{ text: '概览', link: '/miniprogram/' },
{ text: '安装配置', link: '/miniprogram/setup' },
{ text: 'IM 接入', link: '/miniprogram/im' },
],
'/h5/': [
{ text: '概览', link: '/h5/' },
{ text: '安装配置', link: '/h5/setup' },
{ text: 'IM 接入', link: '/h5/im' },
],
'/server/': [
{ text: 'API 速查', link: '/server/api' },

183
docs-site/docs/flutter/im.md 普通文件
查看文件

@ -0,0 +1,183 @@
# Flutter IM 接入
**模块**`xuqm_flutter_im` · **最低版本**Dart 3.0+
---
## 登录
```dart
import 'package:xuqm_flutter_sdk/xuqm_flutter_sdk.dart';
await XuqmImSdk().login('user_001', 'your_user_sig_jwt');
```
登录成功后会自动建立 IM 实时连接。
---
## 监听消息
```dart
final im = XuqmImSdk();
im.ws.onConnected = () {
print('IM 已连接');
};
im.ws.onMessage = (XuqmImMessage msg) {
print('收到消息: ${msg.msgType} - ${msg.content}');
};
im.ws.onDisconnected = (String? reason) {
print('IM 断开: $reason');
};
```
---
## 发送消息
```dart
// 发送文本消息
final msg = await im.sendTextMessage(
'user_002',
'SINGLE',
'Hello from Flutter!',
);
// 发送图片消息
final imgMsg = await im.sendMessage(
'user_002',
'SINGLE',
'IMAGE',
jsonEncode({'url': 'https://cdn.example.com/img.jpg', 'width': 800, 'height': 600}),
);
// 撤回消息
await im.revokeMessage(msg.id);
// 编辑消息
await im.editMessage(msg.id, '新内容');
```
---
## 会话管理
```dart
// 会话列表
final conversations = await im.listConversations();
// 置顶会话
await im.setConversationPinned('user_002', 'SINGLE', true);
// 免打扰
await im.setConversationMuted('group_xxx', 'GROUP', true);
// 标记已读
await im.markRead('user_002');
// 设置草稿
await im.setDraft('user_002', 'SINGLE', '未完成的消息');
```
---
## 好友与群组
```dart
// 好友列表
final friends = await im.listFriends();
// 添加好友
await im.addFriend('user_002');
// 移除好友
await im.removeFriend('user_002');
// 创建群组
final group = await im.createGroup('Flutter 群', ['user_001', 'user_002']);
// 群组列表
final groups = await im.listGroups();
// 添加群成员
await im.addGroupMember(group.id, 'user_003');
// 退出群聊
await im.leaveGroup(group.id);
```
---
## 历史消息
```dart
// 单聊历史
final history = await im.fetchHistory('user_002', page: 0, size: 20);
// 群聊历史
final groupHistory = await im.fetchGroupHistory('group_xxx', page: 0, size: 50);
// 定位消息所在页
final page = await im.locateHistoryPage(
'user_002',
messageId: 'msg_xxx',
pageSize: 20,
);
```
---
## 完整示例
```dart
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:xuqm_flutter_sdk/xuqm_flutter_sdk.dart';
class ChatPage extends StatefulWidget {
@override
_ChatPageState createState() => _ChatPageState();
}
class _ChatPageState extends State<ChatPage> {
final im = XuqmImSdk();
final List<XuqmImMessage> messages = [];
@override
void initState() {
super.initState();
_init();
}
Future<void> _init() async {
await XuqmSDK.initialize(XuqmInitOptions(appKey: 'your_app_key'));
await im.login('user_001', 'your_user_sig_jwt');
im.ws.onMessage = (msg) {
setState(() => messages.add(msg));
};
}
Future<void> _send(String text) async {
final msg = await im.sendTextMessage('user_002', 'SINGLE', text);
setState(() => messages.add(msg));
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Flutter IM')),
body: ListView.builder(
itemCount: messages.length,
itemBuilder: (context, index) {
final msg = messages[index];
return ListTile(title: Text('${msg.fromId}: ${msg.content}'));
},
),
);
}
}
```

查看文件

@ -0,0 +1,68 @@
# Flutter 安装配置
**包名**`xuqm_flutter_sdk` · **版本**0.2.x · **语言**Dart
---
## 安装
SDK 通过 Gitea Git 仓库发布,在 `pubspec.yaml` 中配置:
```yaml
dependencies:
xuqm_flutter_sdk:
git:
url: https://xuqinmin.com/xuqinmin12/XuqmGroup-FlutterSDK.git
ref: v0.2.0
```
> Gitea Package Registry 暂不支持 Dart/Flutter 包格式,因此通过 Git Tag 方式发布。
---
## 模块说明
| 模块 | 包 | 功能 |
|------|-----|------|
| `xuqm_flutter_common` | `packages/common` | 初始化、网络、配置管理 |
| `xuqm_flutter_im` | `packages/im` | 单聊、群聊、消息收发、会话、好友、群组 |
| `xuqm_flutter_push` | `packages/push` | 设备 Token 注册、厂商检测Android/ APNsiOS|
| `xuqm_flutter_update` | `packages/update` | App 版本检查、商店跳转、APK 下载Android|
---
## 初始化
```dart
import 'package:xuqm_flutter_sdk/xuqm_flutter_sdk.dart';
await XuqmSDK.initialize(XuqmInitOptions(
appKey: 'your_app_key',
debug: true,
));
```
初始化时会自动向服务端请求远程配置IM API 地址等),若网络异常则回退到内置默认值。
---
## 多模块统一登录
无论集成了哪些模块IM、Push、Update,**初始化和登录永远只做一次**
```dart
// 初始化
await XuqmSDK.initialize(XuqmInitOptions(appKey: 'your_app_key'));
// 登录(业务登录成功后调用一次)
await XuqmImSdk().login('user_001', 'your_user_sig_jwt');
// 登出
await XuqmImSdk().logout();
```
---
## 下一步
- [Flutter IM 接入 →](./im)

156
docs-site/docs/h5/im.md 普通文件
查看文件

@ -0,0 +1,156 @@
# H5 IM 接入
**模块**`@xuqm/h5-sdk` · **功能范围**:消息收发、会话管理、好友、群组
---
## IM 管理器
```typescript
import { ImManager } from '@xuqm/h5-sdk'
const im = new ImManager()
// 监听连接状态
im.on('connected', () => console.log('IM 已连接'))
im.on('disconnected', () => console.log('IM 已断开'))
// 监听实时消息
im.on('messages', (msgs) => {
console.log('收到消息', msgs)
})
// 监听会话列表变化
im.on('conversations', (convs) => {
console.log('会话列表更新', convs)
})
// 开始连接
im.connect()
```
---
## 发送消息
```typescript
// 发送文本
im.send({
toId: 'user_002',
chatType: 'SINGLE',
msgType: 'TEXT',
content: 'Hello H5!',
})
```
### 发送文件消息(自动上传)
SDK 内部自动上传文件到文件服务器,无需调用方手动上传。
```typescript
import { sendImageMessage, sendFileMessage } from '@xuqm/h5-sdk'
// 发送图片(从 input 元素获取)
const file = document.querySelector('input[type="file"]').files[0]
await sendImageMessage('user_002', 'SINGLE', file, 800, 600)
// 发送文件
await sendFileMessage('user_002', 'SINGLE', file)
```
---
## 核心 API
### 事件订阅
| 事件 | 回调签名 | 说明 |
|------|---------|------|
| `connected` | `() => void` | IM 实时连接已建立 |
| `disconnected` | `() => void` | IM 实时连接已断开 |
| `messages` | `(msgs: ImMessage[]) => void` | 收到新消息 |
| `conversations` | `(convs: ConversationData[]) => void` | 会话列表变化 |
| `error` | `(err: Error) => void` | 发生错误 |
```typescript
im.on('conversations', (convs) => {
// 更新 UI
})
im.off('conversations', handler) // 取消订阅
```
### 状态属性
```typescript
im.connected // boolean,是否已连接
im.messages // ImMessage[],当前消息列表
im.conversations // ConversationData[],当前会话列表
im.error // Error | null,最近错误
```
### HTTP API
所有 IM 功能均通过纯函数暴露:
```typescript
import {
sendMessage,
sendTextMessage,
sendImageMessage,
sendVideoMessage,
sendFileMessage,
sendAudioMessage,
fetchHistory,
fetchGroupHistory,
listConversations,
markRead,
listFriends,
addFriend,
listGroups,
createGroup,
// ...
} from '@xuqm/h5-sdk'
```
---
## 与框架集成
### React
```tsx
import { useEffect, useState } from 'react'
import { ImManager } from '@xuqm/h5-sdk'
function useIm() {
const [im] = useState(() => new ImManager())
const [messages, setMessages] = useState([])
const [conversations, setConversations] = useState([])
useEffect(() => {
im.on('messages', setMessages)
im.on('conversations', setConversations)
im.connect()
return () => {
im.off('messages', setMessages)
im.off('conversations', setConversations)
im.disconnect()
}
}, [im])
return { im, messages, conversations }
}
```
### 纯 HTML/JS
```html
<script src="https://dev.xuqinmin.com/docs/h5-sdk/index.umd.js"></script>
<script>
XuqmH5SDK.init({ appKey: 'your_app_key' })
XuqmH5SDK.login('user_001', 'userSig')
const im = new XuqmH5SDK.ImManager()
im.on('messages', function(msgs) { console.log(msgs) })
im.connect()
</script>
```

54
docs-site/docs/h5/setup.md 普通文件
查看文件

@ -0,0 +1,54 @@
# H5 安装配置
**包名**`@xuqm/h5-sdk` · **语言**TypeScript / JavaScript · **格式**ESM + UMD
`@xuqm/h5-sdk` 是从 Vue3 SDK 提取的纯 JavaScript/TypeScript 核心库,**不依赖任何前端框架**Vue/React/Angular 均可使用),可在任何支持 ES6 的浏览器或 H5 环境中运行。
> **功能范围**:仅包含 **IM** 功能(消息收发、会话管理、好友、群组),**不包含 Push 和 Update**。
---
## 安装
```bash
npm install @xuqm/h5-sdk
```
或 CDN 引入:
```html
<script src="https://dev.xuqinmin.com/docs/h5-sdk/index.umd.js"></script>
<script>
const { init, ImManager, login } = XuqmH5SDK
</script>
```
---
## 初始化
只需传入 `appKey`,服务器地址由 SDK 内置。
```typescript
import { init } from '@xuqm/h5-sdk'
init({
appKey: 'your_app_key', // 在租户平台创建应用后获得
})
```
---
## 登录
```typescript
import { login } from '@xuqm/h5-sdk'
await login('user_001', 'your_user_sig_jwt')
```
---
## 下一步
- [H5 IM 接入 →](./im)

查看文件

@ -0,0 +1,137 @@
# 微信小程序 IM 接入
**模块**`xuqm-group-wechat-mini-program-sdk`
---
## 登录
```ts
import { XuqmMiniProgramSDK } from 'xuqm-group-wechat-mini-program-sdk'
const sdk = new XuqmMiniProgramSDK()
sdk.init({ appKey: 'your_app_key' })
// 使用 UserSig 登录
await sdk.login('user_001', 'your_user_sig_jwt')
```
---
## 监听消息
```ts
sdk.on('connected', () => {
console.log('IM 已连接')
})
sdk.on('message', (msg) => {
console.log('收到消息:', msg.msgType, msg.content)
})
sdk.on('read', (msg) => {
console.log('已读回执:', msg.id)
})
sdk.on('revoke', (data) => {
console.log('消息被撤回:', data.msgId)
})
sdk.on('disconnected', (reason) => {
console.log('断开连接:', reason)
})
sdk.on('error', (error) => {
console.error('IM 错误:', error)
})
```
---
## 发送消息
```ts
const msg = await sdk.send({
toId: 'user_002',
chatType: 'SINGLE',
msgType: 'TEXT',
content: 'Hello!',
})
```
### 发送文本消息(快捷方法)
```ts
await sdk.sendTextMessage('user_002', 'SINGLE', 'Hello!')
```
---
## 历史消息
```ts
const history = await sdk.fetchHistory('user_002')
const groupHistory = await sdk.fetchGroupHistory('group_xxx')
```
---
## 会话列表
```ts
const conversations = await sdk.listConversations()
await sdk.markRead('user_002')
await sdk.setConversationPinned('user_002', 'SINGLE', true)
await sdk.setConversationMuted('user_002', 'SINGLE', true)
```
---
## 群聊
```ts
const groups = await sdk.listGroups()
const group = await sdk.getGroupInfo('group_xxx')
const members = await sdk.listGroupMembers('group_xxx')
```
---
## 好友管理
```ts
const friends = await sdk.listFriends()
await sdk.addFriend('user_002')
await sdk.removeFriend('user_002')
```
---
## 离线消息同步
```ts
const count = await sdk.offlineMessageCount()
const messages = await sdk.syncOfflineMessages()
```
---
## 消息类型
| MsgType | 说明 |
|---------|------|
| `TEXT` | 纯文本 |
| `IMAGE` | 图片 |
| `VIDEO` | 视频 |
| `AUDIO` | 语音 |
| `FILE` | 文件 |
| `LOCATION` | 位置 |
| `CUSTOM` | 自定义 |
| `NOTIFY` | 系统通知 |
| `RICH_TEXT` | 富文本 |
| `CALL_AUDIO` | 语音通话信令 |
| `CALL_VIDEO` | 视频通话信令 |
| `QUOTE` | 引用 |
| `MERGE` | 合并转发 |
| `FORWARD` | 转发 |
| `REVOKED` | 撤回 |

查看文件

@ -0,0 +1,45 @@
# 微信小程序安装配置
**包名**`xuqm-group-wechat-mini-program-sdk` · **版本**0.1.0
---
## npm 安装
在项目根目录创建 `.npmrc`
```
registry=https://nexus.xuqinmin.com/repository/npm/
```
然后安装:
```bash
npm install xuqm-group-wechat-mini-program-sdk
```
或在微信开发者工具中:
1. 打开「工具」→「构建 npm」
2. 在小程序 `package.json` 中添加上述依赖
---
## 初始化
```ts
import { XuqmMiniProgramSDK } from 'xuqm-group-wechat-mini-program-sdk'
const sdk = new XuqmMiniProgramSDK()
sdk.init({
appKey: 'your_app_key',
debug: true, // 可选
})
```
---
## 下一步
- [小程序 IM 接入 →](./im)

查看文件

@ -91,6 +91,7 @@ export interface AppItem {
name: string
packageName: string
tenantId: string
tenantName?: string
createdAt: string
}
@ -241,8 +242,8 @@ export const opsApi = {
rejectRequest: (requestId: string, reviewNote = '') =>
client.post<{ data: ServiceRequest }>(`/ops/service-requests/${requestId}/reject`, { reviewNote }),
listApps: (keyword = '', page = 0, size = 20) =>
client.get<{ data: AppPage }>('/ops/apps', { params: { keyword, page, size } }),
listApps: (keyword = '', tenantId = '', page = 0, size = 20) =>
client.get<{ data: AppPage }>('/ops/apps', { params: { keyword, tenantId, page, size } }),
getApp: (id: string) =>
client.get<{ data: AppDetail }>(`/ops/apps/${id}`),

查看文件

@ -34,7 +34,6 @@
<el-card style="margin-bottom: 16px">
<template #header>功能服务</template>
<el-table :data="detail.services" border stripe>
<el-table-column prop="platform" label="平台" width="120" />
<el-table-column prop="serviceType" label="服务类型" width="140" />
<el-table-column label="状态" width="120">
<template #default="{ row }">

查看文件

@ -2,16 +2,38 @@
<div>
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">
<h2>应用管理</h2>
<el-input v-model="search" placeholder="搜索应用名称 / AppKey" style="width:280px" clearable @clear="loadApps" @keyup.enter="loadApps">
<template #prefix><el-icon><Search /></el-icon></template>
</el-input>
<div style="display:flex;gap:12px;align-items:center">
<el-select
v-model="tenantFilter"
filterable
remote
clearable
placeholder="筛选租户"
:remote-method="searchTenants"
:loading="tenantLoading"
style="width:200px"
@change="onTenantChange"
>
<el-option
v-for="t in tenantOptions"
:key="t.id"
:label="t.nickname || t.username"
:value="t.id"
/>
</el-select>
<el-input v-model="search" placeholder="搜索应用名称 / AppKey" style="width:280px" clearable @clear="loadApps" @keyup.enter="loadApps">
<template #prefix><el-icon><Search /></el-icon></template>
</el-input>
</div>
</div>
<el-table :data="apps" v-loading="loading" border stripe>
<el-table-column prop="appKey" label="AppKey" width="220" />
<el-table-column prop="name" label="应用名称" min-width="160" />
<el-table-column prop="packageName" label="包名" min-width="160" />
<el-table-column prop="tenantId" label="租户ID" width="220" show-overflow-tooltip />
<el-table-column label="租户" width="180" show-overflow-tooltip>
<template #default="{ row }">{{ row.tenantName || row.tenantId }}</template>
</el-table-column>
<el-table-column prop="createdAt" label="创建时间" width="180">
<template #default="{ row }">{{ new Date(row.createdAt).toLocaleString('zh-CN') }}</template>
</el-table-column>
@ -33,19 +55,41 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { Search } from '@element-plus/icons-vue'
import { opsApi, type AppItem } from '@/api/ops'
import { opsApi, type AppItem, type TenantItem } from '@/api/ops'
const apps = ref<AppItem[]>([])
const loading = ref(false)
const search = ref('')
const tenantFilter = ref('')
const page = ref(0)
const size = ref(20)
const total = ref(0)
const tenantOptions = ref<TenantItem[]>([])
const tenantLoading = ref(false)
async function searchTenants(keyword: string) {
if (!keyword) { tenantOptions.value = []; return }
tenantLoading.value = true
try {
const res = await opsApi.listTenants(keyword, 0, 20)
tenantOptions.value = res.data.data.content
} catch {
tenantOptions.value = []
} finally {
tenantLoading.value = false
}
}
function onTenantChange() {
page.value = 0
loadApps()
}
async function loadApps() {
loading.value = true
try {
const res = await opsApi.listApps(search.value, page.value, size.value)
const res = await opsApi.listApps(search.value, tenantFilter.value, page.value, size.value)
apps.value = res.data.data.content
total.value = res.data.data.total
} catch {