feat(sample): 添加示例应用的核心功能模块

- 集成依赖管理配置文件 libs.versions.toml,统一管理项目依赖版本
- 实现演示 API 接口定义,包含登录、注册、用户管理等 RESTful 端点
- 创建认证仓库 AuthRepository,处理用户会话管理和加密存储
- 开发登录和注册界面,实现用户身份验证流程
- 构建聊天界面 ChatScreen,支持消息收发和历史记录显示
- 实现联系人管理功能,包含好友搜索和添加删除操作
- 添加会话列表界面,展示最近聊天记录和未读消息提示
这个提交包含在:
徐勤民 2026-04-27 19:00:54 +08:00
父节点 173c3cbddd
当前提交 34c02b9832
共有 6 个文件被更改,包括 22 次插入10 次删除

查看文件

@ -16,6 +16,7 @@
| 配置项 | 值 | | 配置项 | 值 |
|--------|-----| |--------|-----|
| API 域名 | `https://dev.xuqinmin.com` | | API 域名 | `https://dev.xuqinmin.com` |
| 登录服务 | `demo-service``/api/demo/auth/*` |
| IM WebSocket | `wss://dev.xuqinmin.com/ws/im` | | IM WebSocket | `wss://dev.xuqinmin.com/ws/im` |
| 演示 AppId | `ak_demo_chat` | | 演示 AppId | `ak_demo_chat` |
| 演示用户 | `demo_alice`Alice、`demo_bob`Bob | | 演示用户 | `demo_alice`Alice、`demo_bob`Bob |
@ -136,6 +137,7 @@ cd XuqmGroup-RNChatDemo
| 路径前缀 | 对应服务 | 端口 | | 路径前缀 | 对应服务 | 端口 |
|----------|---------|-----| |----------|---------|-----|
| `/api/demo/` | demo-service | 8085 |
| `/api/im/` | im-service | 8082 | | `/api/im/` | im-service | 8082 |
| `/ws/im` | im-service (WebSocket) | 8082 | | `/ws/im` | im-service (WebSocket) | 8082 |
| `/api/v1/updates/` | update-service | 8084 | | `/api/v1/updates/` | update-service | 8084 |
@ -144,6 +146,8 @@ cd XuqmGroup-RNChatDemo
所有接口通过 Nginx 反代至 `https://dev.xuqinmin.com` 所有接口通过 Nginx 反代至 `https://dev.xuqinmin.com`
`demo-service` 负责 demo 账号体系的注册、登录、找回密码和资料更新;登录成功后会返回 `demoToken`、`imToken` 和 `profile`。其中 `profile.appId` 用于后续初始化 IM 和更新能力,不走租户服务登录链路。
--- ---
## 注意事项 ## 注意事项

查看文件

@ -2,7 +2,7 @@
set -euo pipefail set -euo pipefail
BASE_URL="${BASE_URL:-https://sentry.xuqinmin.com}" BASE_URL="${BASE_URL:-https://dev.xuqinmin.com}"
APP_ID="${APP_ID:-ak_demo_chat}" APP_ID="${APP_ID:-ak_demo_chat}"
MODULE_ID="${MODULE_ID:-chat-home}" MODULE_ID="${MODULE_ID:-chat-home}"

查看文件

@ -33,6 +33,7 @@ async function request<T>(
} }
export interface UserProfile { export interface UserProfile {
appId: string
userId: string userId: string
nickname: string nickname: string
avatar: string avatar: string

查看文件

@ -8,6 +8,10 @@ import pluginMeta from '../../plugin.json'
const APP_ID = 'ak_demo_chat' const APP_ID = 'ak_demo_chat'
const SERVER_URL = 'https://dev.xuqinmin.com' const SERVER_URL = 'https://dev.xuqinmin.com'
function buildImDbName(appId: string, userId: string): string {
return `xuqm_im_${appId}_${userId}`
}
interface AuthState { interface AuthState {
ready: boolean ready: boolean
userId: string | null userId: string | null
@ -27,10 +31,11 @@ const AuthContext = createContext<AuthContextValue | null>(null)
export function AuthProvider({ children }: { children: React.ReactNode }) { export function AuthProvider({ children }: { children: React.ReactNode }) {
const [state, setState] = useState<AuthState>({ ready: false, userId: null, profile: null }) const [state, setState] = useState<AuthState>({ ready: false, userId: null, profile: null })
const initSDK = useCallback(async (userId: string, imToken: string, profile: UserProfile) => { const initSDK = useCallback(async (profile: UserProfile, imToken: string) => {
await XuqmSDK.initialize({ appId: APP_ID, serverUrl: SERVER_URL }) const appId = profile.appId || APP_ID
await ImSDK.loginWithToken(userId, imToken, 'xuqm_im') await XuqmSDK.initialize({ appId, serverUrl: SERVER_URL })
setState({ ready: true, userId, profile }) await ImSDK.loginWithToken(profile.userId, imToken, buildImDbName(appId, profile.userId))
setState({ ready: true, userId: profile.userId, profile })
UpdateSDK.checkAppUpdate() UpdateSDK.checkAppUpdate()
.then(update => { .then(update => {
@ -57,7 +62,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
const imToken = await load<string>(K.IM_TOKEN) const imToken = await load<string>(K.IM_TOKEN)
const profile = await load<UserProfile>(K.PROFILE) const profile = await load<UserProfile>(K.PROFILE)
if (demoToken && imToken && profile) { if (demoToken && imToken && profile) {
await initSDK(profile.userId, imToken, profile) await initSDK(profile, imToken)
return return
} }
} catch { } catch {
@ -72,7 +77,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
await save(K.DEMO_TOKEN, result.demoToken) await save(K.DEMO_TOKEN, result.demoToken)
await save(K.IM_TOKEN, result.imToken) await save(K.IM_TOKEN, result.imToken)
await save(K.PROFILE, result.profile) await save(K.PROFILE, result.profile)
await initSDK(result.profile.userId, result.imToken, result.profile) await initSDK(result.profile, result.imToken)
}, [initSDK]) }, [initSDK])
const login = useCallback(async (userId: string, password: string) => { const login = useCallback(async (userId: string, password: string) => {

查看文件

@ -18,6 +18,7 @@ interface ConvWithMeta extends ConversationData {
} }
export default function ConversationListScreen() { export default function ConversationListScreen() {
const appId = 'ak_demo_chat'
const navigation = useNavigation<Nav>() const navigation = useNavigation<Nav>()
const [conversations, setConversations] = useState<ConvWithMeta[]>([]) const [conversations, setConversations] = useState<ConvWithMeta[]>([])
const profileCache = useRef<Record<string, UserProfile>>({}) const profileCache = useRef<Record<string, UserProfile>>({})
@ -27,10 +28,10 @@ export default function ConversationListScreen() {
if (!profile) { if (!profile) {
try { try {
const results = await demoApi.searchUsers(conv.targetId) const results = await demoApi.searchUsers(conv.targetId)
profile = results.find(u => u.userId === conv.targetId) ?? { userId: conv.targetId, nickname: conv.targetId, avatar: '', gender: 'UNKNOWN', status: '' } profile = results.find(u => u.userId === conv.targetId) ?? { appId, userId: conv.targetId, nickname: conv.targetId, avatar: '', gender: 'UNKNOWN', status: '' }
profileCache.current[conv.targetId] = profile profileCache.current[conv.targetId] = profile
} catch { } catch {
profile = { userId: conv.targetId, nickname: conv.targetId, avatar: '', gender: 'UNKNOWN', status: '' } profile = { appId, userId: conv.targetId, nickname: conv.targetId, avatar: '', gender: 'UNKNOWN', status: '' }
} }
} }
return { ...conv, targetName: profile.nickname, targetAvatar: profile.avatar } return { ...conv, targetName: profile.nickname, targetAvatar: profile.avatar }

查看文件

@ -12,6 +12,7 @@ type Props = NativeStackScreenProps<RootStackParams, 'GroupMembers'>
export default function GroupMembersScreen({ route }: Props) { export default function GroupMembersScreen({ route }: Props) {
const { groupId, groupName } = route.params const { groupId, groupName } = route.params
const appId = 'ak_demo_chat'
const navigation = useNavigation() const navigation = useNavigation()
const [members, setMembers] = useState<UserProfile[]>([]) const [members, setMembers] = useState<UserProfile[]>([])
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
@ -28,7 +29,7 @@ export default function GroupMembersScreen({ route }: Props) {
const profiles = await Promise.all( const profiles = await Promise.all(
ids.map(async id => { ids.map(async id => {
const res = await demoApi.searchUsers(id) const res = await demoApi.searchUsers(id)
return res.find(u => u.userId === id) ?? { userId: id, nickname: id, avatar: '', gender: 'UNKNOWN' as const, status: '' } return res.find(u => u.userId === id) ?? { appId, userId: id, nickname: id, avatar: '', gender: 'UNKNOWN' as const, status: '' }
}) })
) )
setMembers(profiles) setMembers(profiles)