feat(common): ✨ mobile image review

This commit is contained in:
wanwanruwoxin
2025-10-20 19:44:14 +08:00
committed by Dawn
parent c0526f235b
commit fa2cab3e7e
13 changed files with 325 additions and 96 deletions
+52 -40
View File
@@ -1,52 +1,59 @@
<template>
<n-image
v-if="body?.url"
class="select-none cursor-pointer"
:img-props="{
style: {
...imageStyle
}
}"
object-fit="cover"
show-toolbar-tooltip
preview-disabled
style="border-radius: 8px; cursor: pointer !important"
:src="body?.url"
@dblclick="handleOpenImageViewer"
@click="handleOpenImage"
@error="handleImageError">
<template #placeholder>
<n-flex
v-if="!isError"
align="center"
justify="center"
:style="{
width: `${imageStyle.width}`,
height: `${imageStyle.height}`,
backgroundColor: '#c8c8c833'
}"
class="rounded-10px">
<img class="size-24px select-none" src="@/assets/img/loading.svg" alt="loading" />
</n-flex>
</template>
<template #error>
<n-flex v-if="isError" align="center" justify="center" class="w-200px h-150px bg-#c8c8c833 rounded-10px">
<svg class="size-34px color-[--chat-text-color]"><use href="#error-picture"></use></svg>
</n-flex>
</template>
</n-image>
<div>
<n-image
v-if="body?.url"
class="select-none cursor-pointer"
:img-props="{
style: {
...imageStyle
}
}"
object-fit="cover"
show-toolbar-tooltip
preview-disabled
style="border-radius: 8px; cursor: pointer !important"
:src="body?.url"
@dblclick="handleOpenImageViewer"
@click="handleOpenImage"
@error="handleImageError">
<template #placeholder>
<n-flex
v-if="!isError"
align="center"
justify="center"
:style="{
width: `${imageStyle.width}`,
height: `${imageStyle.height}`,
backgroundColor: '#c8c8c833'
}"
class="rounded-10px">
<img class="size-24px select-none" src="@/assets/img/loading.svg" alt="loading" />
</n-flex>
</template>
<template #error>
<n-flex v-if="isError" align="center" justify="center" class="w-200px h-150px bg-#c8c8c833 rounded-10px">
<svg class="size-34px color-[--chat-text-color]">
<use href="#error-picture"></use>
</svg>
</n-flex>
</template>
</n-image>
<!-- 图片预览组件 -->
<ImagePreview v-model:visible="showImagePreviewRef" :image-url="body?.url || ''" :message="message" />
</div>
</template>
<script setup lang="ts">
import { MsgEnum } from '@/enums/index'
import { MsgEnum } from '@/enums'
import { useImageViewer } from '@/hooks/useImageViewer'
import type { ImageBody } from '@/services/types'
import type { ImageBody, MsgType } from '@/services/types'
import { isMobile } from '@/utils/PlatformConstants'
import { showImagePreview } from 'vant/es'
const props = defineProps<{
body: ImageBody
onImageClick?: (url: string) => void
message?: MsgType
}>()
// 图片显示相关常量
const MAX_WIDTH = isMobile() ? 240 : 320
@@ -57,6 +64,8 @@ const MIN_HEIGHT = 60
const isError = ref(false)
// 使用图片查看器hook
const { openImageViewer } = useImageViewer()
const showImagePreviewRef = ref(false)
const imagesRef = ref<string[]>([])
// 处理图片加载错误
const handleImageError = () => {
@@ -67,7 +76,8 @@ const handleOpenImage = () => {
if (!isMobile()) return // 非移动端直接返回
if (props.body?.url) {
showImagePreview([props.body.url])
imagesRef.value = [props.body.url]
showImagePreviewRef.value = true
}
}
@@ -129,3 +139,5 @@ const imageStyle = computed(() => {
}
})
</script>
<style scoped></style>
@@ -121,7 +121,7 @@
</template>
<script setup lang="ts">
import { join, resourceDir } from '@tauri-apps/api/path'
import { appDataDir, join, resourceDir } from '@tauri-apps/api/path'
import { BaseDirectory } from '@tauri-apps/plugin-fs'
import { MessageStatusEnum } from '@/enums'
import { MittEnum, MsgEnum } from '@/enums/index'
@@ -131,6 +131,7 @@ import { useVideoViewer } from '@/hooks/useVideoViewer'
import type { VideoBody } from '@/services/types'
import { useVideoViewer as useVideoViewerStore } from '@/stores/videoViewer'
import { formatBytes } from '@/utils/Formatting.ts'
import { isMobile } from '@/utils/PlatformConstants'
const { openVideoViewer, getLocalVideoPath, checkVideoDownloaded, getVideoFilenameEllipsis } = useVideoViewer()
const videoViewerStore = useVideoViewerStore()
@@ -223,13 +224,14 @@ const downloadVideo = async () => {
try {
const localPath = await getLocalVideoPath(props.body?.url)
if (localPath) {
await downloadFile(props.body.url, localPath, BaseDirectory.Resource)
const baseDir = isMobile() ? BaseDirectory.AppData : BaseDirectory.Resource
await downloadFile(props.body.url, localPath, baseDir)
isVideoDownloaded.value = await checkVideoDownloaded(props.body.url)
// 下载完成后,更新videoViewer store中的视频路径
if (isVideoDownloaded.value) {
const resourceDirPath = await resourceDir()
const path = await join(resourceDirPath, localPath)
const baseDirPath = isMobile() ? await appDataDir() : await resourceDir()
const path = await join(baseDirPath, localPath)
videoViewerStore.updateVideoPath(props.body.url, path)
}
}
+10 -7
View File
@@ -3,7 +3,7 @@ import { BaseDirectory, create, exists, mkdir, readFile } from '@tauri-apps/plug
import type { Ref } from 'vue'
import type { FilesMeta } from '@/services/types'
import { getFilesMeta, getImageCache } from '@/utils/PathUtil'
import { isMac } from '@/utils/PlatformConstants'
import { isMac, isMobile } from '@/utils/PlatformConstants'
/**
* 单个文件元数据接口
@@ -87,7 +87,8 @@ export const useAudioFileManager = (userId: string): AudioFileManagerReturn => {
const fullPath = await join(cachePath, fileName)
// 检查文件是否存在于本地缓存文件夹中
const fileExists = await exists(fullPath, { baseDir: BaseDirectory.AppCache })
const baseDir = isMobile() ? BaseDirectory.AppData : BaseDirectory.AppCache
const fileExists = await exists(fullPath, { baseDir })
if (!fileExists) {
return {
cachePath,
@@ -98,7 +99,7 @@ export const useAudioFileManager = (userId: string): AudioFileManagerReturn => {
}
// 读取音频文件内容
const fileBuffer = await readFile(fullPath, { baseDir: BaseDirectory.AppCache })
const fileBuffer = await readFile(fullPath, { baseDir })
// 如果是 Uint8Array,手动转成ArrayBuffer
const arrayBuffer =
@@ -124,16 +125,17 @@ export const useAudioFileManager = (userId: string): AudioFileManagerReturn => {
const fetchAndDownloadAudioFile = async (cachePath: string, fileName: string, url: string): Promise<ArrayBuffer> => {
const response = await fetch(url)
const arrayBuffer = await response.arrayBuffer()
const dirExists = await exists(cachePath, { baseDir: BaseDirectory.AppCache })
const baseDir = isMobile() ? BaseDirectory.AppData : BaseDirectory.AppCache
const dirExists = await exists(cachePath, { baseDir })
// 若目录不存在,则创建缓存目录
if (!dirExists) {
await mkdir(cachePath, { baseDir: BaseDirectory.AppCache, recursive: true })
await mkdir(cachePath, { baseDir, recursive: true })
}
// 拼接完整路径并保存文件
const fullPath = await join(cachePath, fileName)
const file = await create(fullPath, { baseDir: BaseDirectory.AppCache })
const file = await create(fullPath, { baseDir })
await file.write(new Uint8Array(arrayBuffer))
await file.close()
@@ -151,7 +153,8 @@ export const useAudioFileManager = (userId: string): AudioFileManagerReturn => {
const cachePath = getImageCache(audioFolder, userId)
const fullPath = await join(cachePath, fileMeta.name)
const fileExists = await exists(fullPath, { baseDir: BaseDirectory.AppCache })
const baseDir = isMobile() ? BaseDirectory.AppData : BaseDirectory.AppCache
const fileExists = await exists(fullPath, { baseDir })
return {
exists: fileExists,
+5 -4
View File
@@ -1,4 +1,4 @@
import { join, resourceDir } from '@tauri-apps/api/path'
import { appDataDir, join, resourceDir } from '@tauri-apps/api/path'
import { writeImage, writeText } from '@tauri-apps/plugin-clipboard-manager'
import { save } from '@tauri-apps/plugin-dialog'
import { BaseDirectory } from '@tauri-apps/plugin-fs'
@@ -297,14 +297,15 @@ export const useChatMain = (isHistoryMode = false, options: UseChatMainOptions =
if (!isDownloaded) {
// 如果未下载,先下载视频
await downloadFile(item.message.body.url, localPath, BaseDirectory.Resource)
const baseDir = isMobile() ? BaseDirectory.AppData : BaseDirectory.Resource
await downloadFile(item.message.body.url, localPath, baseDir)
// 通知相关组件更新视频下载状态
useMitt.emit(MittEnum.VIDEO_DOWNLOAD_STATUS_UPDATED, { url: item.message.body.url, downloaded: true })
}
// 获取视频的绝对路径
const resourceDirPath = await resourceDir()
const absolutePath = await join(resourceDirPath, localPath)
const baseDirPath = isMobile() ? await appDataDir() : await resourceDir()
const absolutePath = await join(baseDirPath, localPath)
// 在文件管理器中显示视频
await revealItemInDir(absolutePath)
} catch (error) {
+5 -3
View File
@@ -14,6 +14,7 @@ import { AvatarUtils } from '@/utils/AvatarUtils'
import { removeTag } from '@/utils/Formatting'
import { getSessionDetailWithFriends } from '@/utils/ImRequestUtils'
import { getImageCache } from '@/utils/PathUtil.ts'
import { isMobile } from '@/utils/PlatformConstants'
import { invokeWithErrorHandler } from '../utils/TauriInvokeHandler'
export interface SelectionRange {
@@ -37,11 +38,12 @@ const saveCacheFile = async (file: any, subFolder: string): Promise<string> => {
const cacheReader = new FileReader()
cacheReader.onload = async (e: any) => {
try {
const isExists = await exists(tempPath, { baseDir: BaseDirectory.AppCache })
const baseDir = isMobile() ? BaseDirectory.AppData : BaseDirectory.AppCache
const isExists = await exists(tempPath, { baseDir })
if (!isExists) {
await mkdir(tempPath, { baseDir: BaseDirectory.AppCache, recursive: true })
await mkdir(tempPath, { baseDir, recursive: true })
}
const tempFile = await create(fullPath, { baseDir: BaseDirectory.AppCache })
const tempFile = await create(fullPath, { baseDir })
await tempFile.write(e.target.result)
await tempFile.close()
+6 -1
View File
@@ -1,12 +1,17 @@
import { BaseDirectory, exists, mkdir, writeFile } from '@tauri-apps/plugin-fs'
import { createEventHook } from '@vueuse/core'
import { isMobile } from '@/utils/PlatformConstants'
export const useDownload = () => {
const process = ref(0)
const isDownloading = ref(false)
const { on: onLoaded, trigger } = createEventHook()
const downloadFile = async (url: string, savePath: string, baseDir: BaseDirectory = BaseDirectory.AppCache) => {
const downloadFile = async (
url: string,
savePath: string,
baseDir: BaseDirectory = isMobile() ? BaseDirectory.AppData : BaseDirectory.AppCache
) => {
try {
isDownloading.value = true
process.value = 0
+5 -2
View File
@@ -8,6 +8,7 @@ import { useUserStore } from '@/stores/user'
import { extractFileName, getMimeTypeFromExtension } from '@/utils/Formatting'
import { getImageDimensions } from '@/utils/ImageUtils'
import { getQiniuToken } from '@/utils/ImRequestUtils'
import { isMobile } from '@/utils/PlatformConstants'
/** 文件信息类型 */
export type FileInfoType = {
@@ -623,7 +624,8 @@ export const useUpload = () => {
}
try {
const file = await readFile(path, { baseDir: BaseDirectory.AppCache })
const baseDir = isMobile() ? BaseDirectory.AppData : BaseDirectory.AppCache
const file = await readFile(path, { baseDir })
console.log(`📁 读取文件: ${path}, 大小: ${file.length} bytes`)
// 创建File对象
@@ -814,7 +816,8 @@ export const useUpload = () => {
// 使用默认上传方式
console.log('执行文件上传:', path)
try {
const file = await readFile(path, { baseDir: BaseDirectory.AppCache })
const baseDir = isMobile() ? BaseDirectory.AppData : BaseDirectory.AppCache
const file = await readFile(path, { baseDir })
// 添加文件大小检查
if (file.length > MAX_FILE_SIZE) {
+2 -1
View File
@@ -69,7 +69,8 @@ export const useVideoViewer = () => {
try {
const localPath = await getLocalVideoPath(url)
if (localPath) {
return await exists(localPath, { baseDir: BaseDirectory.Resource })
const baseDir = isMobile() ? BaseDirectory.AppData : BaseDirectory.Resource
return await exists(localPath, { baseDir })
}
} catch (error) {
console.error('检查视频下载状态失败:', error)
+5 -3
View File
@@ -3,6 +3,7 @@ import { startRecording, stopRecording } from 'tauri-plugin-mic-recorder-api'
import { useUserStore } from '@/stores/user'
import { calculateCompressionRatio, compressAudioToMp3, getAudioInfo } from '@/utils/AudioCompression'
import { getImageCache } from '@/utils/PathUtil.ts'
import { isMobile } from '@/utils/PlatformConstants'
import { UploadSceneEnum } from '../enums'
import { useUpload } from './useUpload'
@@ -247,16 +248,17 @@ export const useVoiceRecordRust = (options: VoiceRecordRustOptions = {}) => {
const fullPath = cachePath + fileHashName
// 确保目录存在
const dirExists = await exists(cachePath, { baseDir: BaseDirectory.AppCache })
const baseDir = isMobile() ? BaseDirectory.AppData : BaseDirectory.AppCache
const dirExists = await exists(cachePath, { baseDir })
if (!dirExists) {
await mkdir(cachePath, { baseDir: BaseDirectory.AppCache, recursive: true })
await mkdir(cachePath, { baseDir, recursive: true })
}
// 将Blob转换为ArrayBuffer
const arrayBuffer = await audioBlob.arrayBuffer()
// 保存到本地文件
const file = await create(fullPath, { baseDir: BaseDirectory.AppCache })
const file = await create(fullPath, { baseDir })
await file.write(new Uint8Array(arrayBuffer))
await file.close()
+182
View File
@@ -0,0 +1,182 @@
<template>
<Teleport to="body">
<Transition name="image-preview" appear>
<div v-if="visible" class="fixed top-0 left-0 w-100vw h-100vh bg-black z-3000" @click="handleClose">
<div class="w-full h-full flex justify-center items-center relative">
<Transition name="image-zoom" appear>
<img class="w-full" :src="imageUrl" alt="preview" />
</Transition>
<div class="absolute right-3 bottom-10 flex gap-3 p-1.5 rounded-5 backdrop-blur-sm">
<n-button
v-if="showForward"
circle
quaternary
size="medium"
@click.stop="handleForward"
aria-label="转发"
class="bg-white/20">
<template #icon>
<svg class="size-22px text-white">
<use href="#share"></use>
</svg>
</template>
</n-button>
<n-button
v-if="showSave"
circle
quaternary
size="medium"
@click.stop="handleSave"
aria-label="保存"
class="bg-white/20">
<template #icon>
<svg class="size-22px text-white">
<use href="#Importing"></use>
</svg>
</template>
</n-button>
<n-button
v-if="showMore"
circle
quaternary
size="medium"
@click.stop="handleMore"
aria-label="更多"
class="bg-white/20">
<template #icon>
<svg class="size-22px text-white">
<use href="#more"></use>
</svg>
</template>
</n-button>
</div>
</div>
</div>
</Transition>
</Teleport>
</template>
<script setup lang="ts">
import { MergeMessageType, MittEnum } from '@/enums'
import { useChatStore } from '@/stores/chat'
import { useFileDownloadStore } from '@/stores/fileDownload'
import { useMitt } from '@/hooks/useMitt'
import { extractFileName } from '@/utils/Formatting'
import type { MsgType } from '@/services/types'
interface Props {
visible: boolean
imageUrl: string
message?: MsgType
showForward?: boolean
showSave?: boolean
showMore?: boolean
}
interface Emits {
(e: 'update:visible', value: boolean): void
(e: 'forward'): void
(e: 'save'): void
(e: 'more'): void
}
const props = withDefaults(defineProps<Props>(), {
showForward: true,
showSave: true,
showMore: true
})
const emit = defineEmits<Emits>()
const chatStore = useChatStore()
const fileDownloadStore = useFileDownloadStore()
const handleClose = () => {
emit('update:visible', false)
}
const handleForward = () => {
const msgId = props.message?.id
if (!msgId) {
if (window.$message) {
window.$message.warning('无法转发:消息ID缺失')
}
return
}
const target = chatStore.chatMessageList.find((m: any) => m.message.id === msgId)
if (!target) {
if (window.$message) {
window.$message.warning('未找到可转发的消息')
}
return
}
chatStore.clearMsgCheck()
target.isCheck = true
chatStore.setMsgMultiChoose(false)
useMitt.emit(MittEnum.MSG_MULTI_CHOOSE, { action: 'open-forward', mergeType: MergeMessageType.SINGLE })
emit('forward')
}
const handleSave = async () => {
const imageUrl = props.imageUrl
if (!imageUrl) {
if (window.$message) {
window.$message.warning('无法保存:图片地址缺失')
}
return
}
try {
const fileName = extractFileName(imageUrl) || 'image.png'
const result = await fileDownloadStore.downloadFile(imageUrl, fileName)
if (result && window.$message) {
console.log('图片保存路径:', result)
window.$message.success('图片已保存')
}
} catch (e) {
console.error('保存图片失败:', e)
if (window.$message) {
window.$message.error('保存失败')
}
}
emit('save')
}
const handleMore = () => {
if (window.$message) {
window.$message.warning('更多功能暂未实现')
}
emit('more')
}
</script>
<style scoped>
/* 图片预览背景淡入淡出动画 */
.image-preview-enter-active,
.image-preview-leave-active {
transition: opacity 0.3s ease;
}
.image-preview-enter-from,
.image-preview-leave-to {
opacity: 0;
}
/* 图片缩放动画 */
.image-zoom-enter-active,
.image-zoom-leave-active {
transition: all 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
.image-zoom-enter-from {
opacity: 0;
transform: scale(0.8);
}
.image-zoom-leave-to {
opacity: 0;
transform: scale(1.1);
}
</style>
+9 -8
View File
@@ -1,4 +1,4 @@
import { join, resourceDir } from '@tauri-apps/api/path'
import { appDataDir, join, resourceDir } from '@tauri-apps/api/path'
import { BaseDirectory, exists, writeFile } from '@tauri-apps/plugin-fs'
import { defineStore } from 'pinia'
import { StoresEnum } from '@/enums'
@@ -153,17 +153,17 @@ export const useFileDownloadStore = defineStore(
* @param fileName 文件名
*/
const checkFileExists = async (fileUrl: string, fileName: string): Promise<boolean> => {
if (isMobile()) return false
try {
const downloadsDir = await userStore.getUserRoomDir()
const filePath = await join(downloadsDir, fileName)
const fileExists = await exists(filePath, { baseDir: BaseDirectory.Resource })
const baseDir = isMobile() ? BaseDirectory.AppData : BaseDirectory.Resource
const fileExists = await exists(filePath, { baseDir })
if (fileExists) {
// 文件存在,构建绝对路径并更新状态
const resourceDirPath = await resourceDir()
const absolutePath = await join(resourceDirPath, filePath)
const baseDirPath = isMobile() ? await appDataDir() : await resourceDir()
const absolutePath = await join(baseDirPath, filePath)
// 保持原生路径格式用于文件操作,规范化路径用于显示
const normalizedPath = absolutePath.replace(/\\/g, '/')
@@ -254,11 +254,12 @@ export const useFileDownloadStore = defineStore(
}
// 写入文件
await writeFile(filePath, fileData, { baseDir: BaseDirectory.Resource })
const baseDir = isMobile() ? BaseDirectory.AppData : BaseDirectory.Resource
await writeFile(filePath, fileData, { baseDir })
// 构建绝对路径
const resourceDirPath = await resourceDir()
const absolutePath = await join(resourceDirPath, filePath)
const baseDirPath = isMobile() ? await appDataDir() : await resourceDir()
const absolutePath = await join(baseDirPath, filePath)
// 保持原生路径格式用于文件操作,规范化路径用于显示
const normalizedPath = absolutePath.replace(/\\/g, '/')
+20 -10
View File
@@ -11,6 +11,7 @@ import type { MessageType } from '@/services/types.ts'
import { fixFileMimeType, isVideoUrl } from '@/utils/FileType'
import { getMimeTypeFromExtension, removeTag } from '@/utils/Formatting'
import { getImageDimensions } from '@/utils/ImageUtils'
import { isMobile } from '@/utils/PlatformConstants'
import { useGroupStore } from '../stores/group'
interface MessageStrategy {
@@ -261,7 +262,8 @@ class ImageMessageStrategyImpl extends AbstractMessageStrategy {
const tempPath = `temp-image-${Date.now()}-${file.name}`
const arrayBuffer = await file.arrayBuffer()
const uint8Array = new Uint8Array(arrayBuffer)
await writeFile(tempPath, uint8Array, { baseDir: BaseDirectory.AppCache })
const baseDir = isMobile() ? BaseDirectory.AppData : BaseDirectory.AppCache
await writeFile(tempPath, uint8Array, { baseDir })
return {
type: this.msgType,
@@ -329,7 +331,8 @@ class ImageMessageStrategyImpl extends AbstractMessageStrategy {
console.log('标准化路径:', normalizedPath)
try {
const fileData = await readFile(normalizedPath, { baseDir: BaseDirectory.AppCache })
const baseDir = isMobile() ? BaseDirectory.AppData : BaseDirectory.AppCache
const fileData = await readFile(normalizedPath, { baseDir })
const fileName = path.split('/').pop() || 'image.png'
const fileType = getMimeTypeFromExtension(fileName)
@@ -598,7 +601,8 @@ class FileMessageStrategyImpl extends AbstractMessageStrategy {
private async getFileFromPath(path: string): Promise<File> {
try {
const normalizedPath = path.replace(/\\/g, '/')
const fileData = await readFile(normalizedPath, { baseDir: BaseDirectory.AppCache })
const baseDir = isMobile() ? BaseDirectory.AppData : BaseDirectory.AppCache
const fileData = await readFile(normalizedPath, { baseDir })
const fileName = normalizedPath.split('/').pop() || 'unknown'
const fileType = getMimeTypeFromExtension(fileName)
@@ -636,7 +640,8 @@ class FileMessageStrategyImpl extends AbstractMessageStrategy {
// 将文件保存到临时位置
const arrayBuffer = await validatedFile.arrayBuffer()
const uint8Array = new Uint8Array(arrayBuffer)
await writeFile(tempPath, uint8Array, { baseDir: BaseDirectory.AppCache })
const baseDir = isMobile() ? BaseDirectory.AppData : BaseDirectory.AppCache
await writeFile(tempPath, uint8Array, { baseDir })
return {
type: this.msgType,
@@ -923,7 +928,8 @@ class VideoMessageStrategyImpl extends AbstractMessageStrategy {
const uint8Array = new Uint8Array(arrayBuffer)
// 写入临时文件
await writeFile(tempPath, uint8Array, { baseDir: BaseDirectory.AppCache })
const baseDir = isMobile() ? BaseDirectory.AppData : BaseDirectory.AppCache
await writeFile(tempPath, uint8Array, { baseDir })
// 构建完整的文件路径
const fullPath = await join(await appCacheDir(), tempPath)
@@ -952,7 +958,8 @@ class VideoMessageStrategyImpl extends AbstractMessageStrategy {
// 清理临时文件
try {
await remove(tempPath, { baseDir: BaseDirectory.AppCache })
const baseDir = isMobile() ? BaseDirectory.AppData : BaseDirectory.AppCache
await remove(tempPath, { baseDir })
} catch (cleanupError) {
console.warn('清理临时文件失败:', cleanupError)
}
@@ -979,7 +986,8 @@ class VideoMessageStrategyImpl extends AbstractMessageStrategy {
const tempPath = `temp-video-${Date.now()}-${file.name}`
const arrayBuffer = await file.arrayBuffer()
const uint8Array = new Uint8Array(arrayBuffer)
await writeFile(tempPath, uint8Array, { baseDir: BaseDirectory.AppCache })
const baseDir = isMobile() ? BaseDirectory.AppData : BaseDirectory.AppCache
await writeFile(tempPath, uint8Array, { baseDir })
return {
type: this.msgType,
@@ -1054,8 +1062,9 @@ class VideoMessageStrategyImpl extends AbstractMessageStrategy {
// 5. 处理合法字符串路径的情况
try {
const normalizedPath = videoFile.replace(/\\/g, '/')
const baseDir = isMobile() ? BaseDirectory.AppData : BaseDirectory.AppCache
const fileData = await readFile(normalizedPath, {
baseDir: BaseDirectory.AppCache
baseDir
})
const fileName = normalizedPath.split('/').pop() || 'video.mp4'
@@ -1135,14 +1144,15 @@ class VideoMessageStrategyImpl extends AbstractMessageStrategy {
const uint8Array = new Uint8Array(arrayBuffer)
// 写入临时文件
await writeFile(tempPath, uint8Array, { baseDir: BaseDirectory.AppCache })
const baseDir = isMobile() ? BaseDirectory.AppData : BaseDirectory.AppCache
await writeFile(tempPath, uint8Array, { baseDir })
// enableDeduplication启用文件去重,使用哈希值计算
const result = await this.uploadHook.doUpload(tempPath, uploadUrl, { ...options, enableDeduplication: true })
// 清理临时文件
try {
await remove(tempPath, { baseDir: BaseDirectory.AppCache })
await remove(tempPath, { baseDir })
} catch (cleanupError) {
console.warn('清理临时文件失败:', cleanupError)
}
+18 -13
View File
@@ -1,8 +1,9 @@
import { invoke } from '@tauri-apps/api/core'
import { appCacheDir, join, resourceDir } from '@tauri-apps/api/path'
import { appCacheDir, appDataDir, join, resourceDir } from '@tauri-apps/api/path'
import { BaseDirectory, exists, mkdir, readFile, writeFile } from '@tauri-apps/plugin-fs'
import { type FileTypeResult, fileTypeFromBuffer } from 'file-type'
import type { FilesMeta } from '@/services/types'
import { isMobile } from './PlatformConstants'
// Tauri 资源目录下存放用户数据的根目录名
const USER_DATA = 'userData'
@@ -14,10 +15,11 @@ const MODELS_DIR = 'models'
* Tauri 在构建后默认不会创建该目录,需要在第一次使用前主动创建。
*/
const ensureUserDataRoot = async (): Promise<void> => {
const dirExists = await exists(USER_DATA, { baseDir: BaseDirectory.Resource })
const baseDir = isMobile() ? BaseDirectory.AppData : BaseDirectory.Resource
const dirExists = await exists(USER_DATA, { baseDir })
if (!dirExists) {
await mkdir(USER_DATA, {
baseDir: BaseDirectory.Resource,
baseDir,
recursive: true
})
}
@@ -42,10 +44,11 @@ const getUserVideosDir = async (userUid: string, roomId: string): Promise<string
await ensureUserDataRoot()
// 确保用户ID和房间ID的子目录也存在
const userRoomDir = await join(USER_DATA, userUid, roomId)
const userRoomDirExists = await exists(userRoomDir, { baseDir: BaseDirectory.Resource })
const baseDir = isMobile() ? BaseDirectory.AppData : BaseDirectory.Resource
const userRoomDirExists = await exists(userRoomDir, { baseDir })
if (!userRoomDirExists) {
await mkdir(userRoomDir, {
baseDir: BaseDirectory.Resource,
baseDir,
recursive: true
})
}
@@ -56,8 +59,8 @@ export const getUserAbsoluteVideosDir = async (userUid: string, roomId: string)
const userResourceDirectory = await getUserVideosDir(userUid, roomId)
const filePath = await join(userResourceDirectory)
const resourceDirPath = await resourceDir()
const absoluteDir = await join(resourceDirPath, filePath)
const baseDirPath = isMobile() ? await appDataDir() : await resourceDir()
const absoluteDir = await join(baseDirPath, filePath)
return absoluteDir
}
@@ -71,10 +74,11 @@ const getImageCache = (subFolder: string, userUid: string): string => {
const ensureModelsDir = async (): Promise<string> => {
await ensureUserDataRoot()
const modelsPath = await join(USER_DATA, MODELS_DIR)
const hasModelsDir = await exists(modelsPath, { baseDir: BaseDirectory.Resource })
const baseDir = isMobile() ? BaseDirectory.AppData : BaseDirectory.Resource
const hasModelsDir = await exists(modelsPath, { baseDir })
if (!hasModelsDir) {
await mkdir(modelsPath, {
baseDir: BaseDirectory.Resource,
baseDir,
recursive: true
})
}
@@ -91,7 +95,8 @@ const ensureModelsDir = async (): Promise<string> => {
export const ensureModelFile = async (fileName: string, remoteUrl: string): Promise<string> => {
const modelsDir = await ensureModelsDir()
const modelRelativePath = await join(modelsDir, fileName)
const modelExists = await exists(modelRelativePath, { baseDir: BaseDirectory.Resource })
const baseDir = isMobile() ? BaseDirectory.AppData : BaseDirectory.Resource
const modelExists = await exists(modelRelativePath, { baseDir })
if (!modelExists) {
const response = await fetch(remoteUrl)
@@ -100,12 +105,12 @@ export const ensureModelFile = async (fileName: string, remoteUrl: string): Prom
}
const buffer = await response.arrayBuffer()
await writeFile(modelRelativePath, new Uint8Array(buffer), {
baseDir: BaseDirectory.Resource
baseDir
})
}
const resourceDirPath = await resourceDir()
return await join(resourceDirPath, modelRelativePath)
const baseDirPath = isMobile() ? await appDataDir() : await resourceDir()
return await join(baseDirPath, modelRelativePath)
}
/**