From 38eb9e5d23d1cf9e45d7c8d872861ef07258fb86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8D=A1=E4=BB=94?= <1271013637@qq.com> Date: Thu, 30 Oct 2025 19:58:19 +0800 Subject: [PATCH] feat(mobile): :sparkles: added photo album feature on mobile; added blank AI assistant page --- src/layout/index.vue | 67 +++++++++- src/mobile/components/ImagePreview.vue | 7 +- src/mobile/components/MobileLayout.vue | 63 +++++++++- src/mobile/views/chat-room/MediaViewer.vue | 3 +- src/mobile/views/my/AiAssistant.vue | 26 ++++ src/mobile/views/my/MyAlbum.vue | 136 +++++++++++++++++++++ src/mobile/views/my/SimpleBio.vue | 35 +++++- src/router/index.ts | 12 ++ src/stores/file.ts | 92 +++++++++++++- 9 files changed, 426 insertions(+), 15 deletions(-) create mode 100644 src/mobile/views/my/AiAssistant.vue create mode 100644 src/mobile/views/my/MyAlbum.vue diff --git a/src/layout/index.vue b/src/layout/index.vue index 2b8580a9..941760f9 100644 --- a/src/layout/index.vue +++ b/src/layout/index.vue @@ -36,14 +36,15 @@ import { useMitt } from '@/hooks/useMitt.ts' import rustWebSocketClient from '@/services/webSocketRust' import { useContactStore } from '@/stores/contacts.ts' import { useGlobalStore } from '@/stores/global.ts' -import { isWindows } from '@/utils/PlatformConstants' -import { MittEnum, NotificationTypeEnum, TauriCommand } from '@/enums' +import { isMobile, isWindows } from '@/utils/PlatformConstants' +import { MittEnum, MsgEnum, NotificationTypeEnum, TauriCommand } from '@/enums' import { clearListener, initListener, readCountQueue } from '@/utils/ReadCountQueue' import { emitTo, listen } from '@tauri-apps/api/event' import { UserAttentionType } from '@tauri-apps/api/window' import type { MessageType } from '@/services/types.ts' import { WsResponseMessageType } from '@/services/wsType.ts' import { useChatStore } from '@/stores/chat' +import { useFileStore } from '@/stores/file' import { useUserStore } from '@/stores/user' import { useSettingStore } from '@/stores/setting.ts' import { invokeSilently } from '@/utils/TauriInvokeHandler' @@ -53,6 +54,7 @@ import { audioManager } from '@/utils/AudioManager' const route = useRoute() const userStore = useUserStore() const chatStore = useChatStore() +const fileStore = useFileStore() const settingStore = useSettingStore() const userUid = computed(() => userStore.userInfo!.uid) const appWindow = WebviewWindow.getCurrent() @@ -169,6 +171,62 @@ const playMessageSound = async () => { } } +/** + * 从消息中提取文件信息并添加到 file store + */ +const addFileToStore = (data: MessageType) => { + const { message } = data + const { type, body, roomId, id } = message + + // 只处理图片和视频类型 + if (type !== MsgEnum.IMAGE && type !== MsgEnum.VIDEO) { + return + } + + // 提取文件信息 + const fileUrl = body.url + if (!fileUrl) { + return + } + + // 从 URL 中提取文件名 + let fileName = '' + try { + const urlObj = new URL(fileUrl) + const pathname = urlObj.pathname + fileName = pathname.substring(pathname.lastIndexOf('/') + 1) + } catch (e) { + // 如果不是有效的 URL,直接使用消息 ID 作为文件名 + fileName = `${id}.${type === MsgEnum.IMAGE ? 'jpg' : 'mp4'}` + } + + // 从文件名中提取后缀 + const suffix = fileName.includes('.') + ? fileName.substring(fileName.lastIndexOf('.') + 1) + : type === MsgEnum.IMAGE + ? 'jpg' + : 'mp4' + + // 确定 MIME 类型 + let mimeType = '' + if (type === MsgEnum.IMAGE) { + mimeType = `image/${suffix === 'jpg' ? 'jpeg' : suffix}` + } else if (type === MsgEnum.VIDEO) { + mimeType = `video/${suffix}` + } + + // 添加到 file store + fileStore.addFile({ + id, + roomId, + fileName, + type: type === MsgEnum.IMAGE ? 'image' : 'video', + url: fileUrl, + suffix, + mimeType + }) +} + useMitt.on(WsResponseMessageType.RECEIVE_MESSAGE, async (data: MessageType) => { if (chatStore.checkMsgExist(data.message.roomId, data.message.id)) { return @@ -184,6 +242,11 @@ useMitt.on(WsResponseMessageType.RECEIVE_MESSAGE, async (data: MessageType) => { data }) + // 如果是图片或视频消息,添加到 file store(仅移动端需要) + if (isMobile()) { + addFileToStore(data) + } + // 不是自己发的消息才通知 if (data.fromUser.uid !== userUid.value) { // 获取该消息的会话信息 diff --git a/src/mobile/components/ImagePreview.vue b/src/mobile/components/ImagePreview.vue index e78291d5..1cc75299 100644 --- a/src/mobile/components/ImagePreview.vue +++ b/src/mobile/components/ImagePreview.vue @@ -144,16 +144,21 @@ const handleSave = async () => { // 保存文件信息到 file store const roomId = getCurrentRoomId() if (roomId) { + // 获取文件状态,使用相对路径(localPath)而不是绝对路径 + const fileStatus = fileDownloadStore.getFileStatus(imageUrl) + const localPath = fileStatus.localPath || result + // 如果没有消息信息,手动创建文件信息 const fileInfo = { id: props.message!.id, // 生成唯一ID roomId, fileName, type: 'image' as const, - url: result, + url: localPath, // 使用相对路径 suffix: fileName.split('.').pop()?.toLowerCase() } fileStore.addFile(fileInfo) + console.log('🔍 [ImagePreview Debug] 保存文件信息到 fileStore:', fileInfo) } } } catch (e) { diff --git a/src/mobile/components/MobileLayout.vue b/src/mobile/components/MobileLayout.vue index 99398eb8..9895409e 100644 --- a/src/mobile/components/MobileLayout.vue +++ b/src/mobile/components/MobileLayout.vue @@ -22,11 +22,12 @@ + + diff --git a/src/mobile/views/my/MyAlbum.vue b/src/mobile/views/my/MyAlbum.vue new file mode 100644 index 00000000..72120906 --- /dev/null +++ b/src/mobile/views/my/MyAlbum.vue @@ -0,0 +1,136 @@ + + + + + diff --git a/src/mobile/views/my/SimpleBio.vue b/src/mobile/views/my/SimpleBio.vue index 5f9679c6..4fcd3248 100644 --- a/src/mobile/views/my/SimpleBio.vue +++ b/src/mobile/views/my/SimpleBio.vue @@ -40,13 +40,14 @@
+ @click="item.onClick" + class="flex flex-wrap items-center gap-15px p-[15px_10px_5px_10px] cursor-pointer">
{{ item.label }}
- +
@@ -67,19 +68,41 @@ const userStore = useUserStore() const options = ref([ { icon: 'xiangce', - label: '相册' + label: '相册', + onClick: () => { + router.push('/mobile/mobileMy/myAlbum') + } }, { icon: 'shoucang', - label: '收藏' + label: '收藏', + onClick: () => { + // TODO: 跳转到收藏页面 + console.log('收藏') + } }, { icon: 'wenjian', - label: '文件' + label: '文件', + onClick: () => { + // TODO: 跳转到文件页面 + console.log('文件') + } }, { icon: 'gexingzhuangban', - label: '个性装扮' + label: '个性装扮', + onClick: () => { + // TODO: 跳转到个性装扮页面 + console.log('个性装扮') + } + }, + { + icon: 'robot', + label: 'AI助手', + onClick: () => { + router.push('/mobile/mobileMy/aiAssistant') + } } ]) diff --git a/src/router/index.ts b/src/router/index.ts index ed0b4b80..d0474406 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -42,6 +42,8 @@ import MyMessages from '#/views/my/MyMessages.vue' import PublishCommunity from '#/views/my/PublishCommunity.vue' import Share from '#/views/my/Share.vue' import SimpleBio from '#/views/my/SimpleBio.vue' +import AiAssistant from '#/views/my/AiAssistant.vue' +import MyAlbum from '#/views/my/MyAlbum.vue' import { TauriCommand } from '@/enums' import ConfirmQRLogin from '#/views/ConfirmQRLogin.vue' import MyQRCode from '#/views/MyQRCode.vue' @@ -251,6 +253,16 @@ const getMobileRoutes = (): Array => [ path: 'SimpleBio', name: 'mobileSimpleBio', component: SimpleBio + }, + { + path: 'aiAssistant', + name: 'mobileAiAssistant', + component: AiAssistant + }, + { + path: 'myAlbum', + name: 'mobileMyAlbum', + component: MyAlbum } ] }, diff --git a/src/stores/file.ts b/src/stores/file.ts index 439b7907..1ac8269a 100644 --- a/src/stores/file.ts +++ b/src/stores/file.ts @@ -1,8 +1,10 @@ import { defineStore } from 'pinia' import { convertFileSrc } from '@tauri-apps/api/core' -import { appDataDir, join } from '@tauri-apps/api/path' +import { appDataDir, join, resourceDir } from '@tauri-apps/api/path' +import { readDir } from '@tauri-apps/plugin-fs' import { StoresEnum } from '@/enums' import { isMobile } from '@/utils/PlatformConstants' +import { useUserStore } from './user' /** * 文件信息接口 @@ -43,6 +45,10 @@ export const useFileStore = defineStore( const getRoomFilesForDisplay = async (roomId: string) => { const files = roomFilesMap[roomId] ? Object.values(roomFilesMap[roomId]) : [] + if (files.length === 0) { + return [] + } + const processedFiles = await Promise.all( files.map(async (file) => { let displayUrl: string @@ -51,8 +57,9 @@ export const useFileStore = defineStore( // HTTP URL 直接使用 displayUrl = file.url } else { - // 相对路径需要拼接 BaseDirectory.AppData 的绝对路径 - const baseDirPath = isMobile() ? await appDataDir() : await appDataDir() + // 相对路径需要拼接基础目录的绝对路径 + // 移动端使用 AppData,PC 端使用 Resource + const baseDirPath = isMobile() ? await appDataDir() : await resourceDir() const absolutePath = await join(baseDirPath, file.url) displayUrl = convertFileSrc(absolutePath) } @@ -115,6 +122,82 @@ export const useFileStore = defineStore( return roomFilesMap[roomId]?.[fileId] } + /** + * 扫描本地目录并填充 file store + * 用于初始化时加载已存在的文件 + * 注意:此功能仅在移动端可用 + */ + const scanLocalFiles = async (roomId: string) => { + // 仅移动端支持扫描本地文件 + if (!isMobile()) { + console.warn('scanLocalFiles 仅在移动端可用') + return 0 + } + + try { + const userStore = useUserStore() + + // 获取用户数据目录 + const userRoomDir = await userStore.getUserRoomDir() + + // 获取绝对路径(移动端使用 AppData) + const baseDirPath = await appDataDir() + const absolutePath = await join(baseDirPath, userRoomDir) + + // 读取目录内容 + const entries = await readDir(absolutePath) + + // 图片和视频扩展名 + const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp'] + const videoExtensions = ['.mp4', '.mov', '.avi', '.mkv', '.webm'] + + let addedCount = 0 + + for (const entry of entries) { + if (!entry.isFile) continue + + const fileName = entry.name + const ext = fileName.toLowerCase().substring(fileName.lastIndexOf('.')) + + let fileType: 'image' | 'video' | null = null + let mimeType = '' + + if (imageExtensions.includes(ext)) { + fileType = 'image' + mimeType = `image/${ext === '.jpg' ? 'jpeg' : ext.substring(1)}` + } else if (videoExtensions.includes(ext)) { + fileType = 'video' + mimeType = `video/${ext.substring(1)}` + } + + if (fileType) { + // 使用文件名作为 ID(因为我们不知道原始消息 ID) + const fileId = fileName.substring(0, fileName.lastIndexOf('.')) + + // 构建文件 URL(使用相对路径,与 getUserRoomDir 返回的格式一致) + const fileUrl = await join(userRoomDir, fileName) + + addFile({ + id: fileId, + roomId, + fileName, + type: fileType, + url: fileUrl, + suffix: ext.substring(1), + mimeType + }) + + addedCount++ + } + } + + return addedCount + } catch (error) { + console.error('扫描本地文件失败:', error) + return 0 + } + } + return { // 状态 roomFilesMap: readonly(roomFilesMap), @@ -128,7 +211,8 @@ export const useFileStore = defineStore( removeFile, clearRoomFiles, getFile, - getRoomFilesForDisplay + getRoomFilesForDisplay, + scanLocalFiles } }, {