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
}
},
{