mirror of
https://github.com/HuLaSpark/HuLa.git
synced 2026-10-24 11:53:36 +08:00
feat(common): ✨ mobile image review
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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, '/')
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user