From f84cd516208115ee2bffe7e365944cb8c2359e89 Mon Sep 17 00:00:00 2001 From: lstmxx <740719284@qq.com> Date: Fri, 26 Sep 2025 18:32:51 +0800 Subject: [PATCH 1/8] feat: init upload-image component and update changeNavActive --- .gitignore | 1 + pnpm-lock.yaml | 54 +++++---- src/common/constant/init.ts | 1 + src/common/model/index.ts | 1 + src/common/model/video.ts | 57 +++++++++ src/components.d.ts | 2 +- .../nav-content/nav-content.data.ts | 8 ++ src/components/nav-content/nav-content.vue | 3 +- src/locales/zh-CN.json | 5 +- src/router/index.ts | 8 ++ src/utils/upload-utils.ts | 112 +++++++++++++++++- src/views/upload-video/upload-video.styl | 0 src/views/upload-video/upload-video.vue | 7 ++ 13 files changed, 228 insertions(+), 31 deletions(-) create mode 100644 src/common/model/video.ts create mode 100644 src/views/upload-video/upload-video.styl create mode 100644 src/views/upload-video/upload-video.vue diff --git a/.gitignore b/.gitignore index 6c5616fb..8e877c3c 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,4 @@ Thumbs.db # Other *.local +GEMINI.md \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 84fb63d8..e43f0263 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,7 +1,7 @@ lockfileVersion: '6.0' settings: - autoInstallPeers: true + autoInstallPeers: false excludeLinksFromLockfile: false dependencies: @@ -99,7 +99,7 @@ devDependencies: version: 0.1.2 stylelint-stylus: specifier: ^0.18.0 - version: 0.18.0(postcss-syntax@0.36.2)(stylelint@15.10.2) + version: 0.18.0(stylelint@15.10.2) stylus: specifier: ^0.59.0 version: 0.59.0 @@ -111,19 +111,19 @@ devDependencies: version: 4.9.5 unplugin-auto-import: specifier: ^0.15.2 - version: 0.15.3(rollup@2.79.1) + version: 0.15.3 unplugin-icons: specifier: ^0.16.1 version: 0.16.5(@vue/compiler-sfc@3.3.4) unplugin-vue-components: specifier: ^0.24.1 - version: 0.24.1(rollup@2.79.1)(vue@3.3.4) + version: 0.24.1(vue@3.3.4) vite: specifier: ~2.7.13 version: 2.7.13(stylus@0.59.0) vite-plugin-pwa: specifier: ^0.12.8 - version: 0.12.8(vite@2.7.13)(workbox-build@6.6.0)(workbox-window@6.6.0) + version: 0.12.8(vite@2.7.13) packages: @@ -1835,7 +1835,7 @@ packages: rollup: 2.79.1 dev: true - /@rollup/pluginutils@5.0.2(rollup@2.79.1): + /@rollup/pluginutils@5.0.2: resolution: {integrity: sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==} engines: {node: '>=14.0.0'} peerDependencies: @@ -1847,7 +1847,6 @@ packages: '@types/estree': 1.0.1 estree-walker: 2.0.2 picomatch: 2.3.1 - rollup: 2.79.1 dev: true /@surma/rollup-plugin-off-main-thread@2.2.3: @@ -6204,7 +6203,7 @@ packages: dependencies: htmlparser2: 3.10.1 postcss: 7.0.39 - postcss-syntax: 0.36.2(postcss-html@1.5.0)(postcss@8.4.27) + postcss-syntax: 0.36.2(postcss-html@0.36.0)(postcss-jsx@0.36.4)(postcss-less@3.1.4)(postcss-markdown@0.36.0)(postcss-scss@2.1.1)(postcss@7.0.39) dev: true /postcss-html@1.5.0: @@ -6225,7 +6224,7 @@ packages: dependencies: '@babel/core': 7.22.9 postcss: 7.0.39 - postcss-syntax: 0.36.2(postcss-html@1.5.0)(postcss@8.4.27) + postcss-syntax: 0.36.2(postcss-html@0.36.0)(postcss-jsx@0.36.4)(postcss-less@3.1.4)(postcss-markdown@0.36.0)(postcss-scss@2.1.1)(postcss@7.0.39) transitivePeerDependencies: - supports-color dev: true @@ -6244,7 +6243,7 @@ packages: postcss-syntax: '>=0.36.0' dependencies: postcss: 7.0.39 - postcss-syntax: 0.36.2(postcss-html@1.5.0)(postcss@8.4.27) + postcss-syntax: 0.36.2(postcss-html@0.36.0)(postcss-jsx@0.36.4)(postcss-less@3.1.4)(postcss-markdown@0.36.0)(postcss-scss@2.1.1)(postcss@7.0.39) remark: 10.0.1 unist-util-find-all-after: 1.0.5 dev: true @@ -6335,7 +6334,7 @@ packages: - supports-color dev: true - /postcss-syntax@0.36.2(postcss-html@1.5.0)(postcss@8.4.27): + /postcss-syntax@0.36.2(postcss-html@0.36.0)(postcss-jsx@0.36.4)(postcss-less@3.1.4)(postcss-markdown@0.36.0)(postcss-scss@2.1.1)(postcss@7.0.39): resolution: {integrity: sha512-nBRg/i7E3SOHWxF3PpF5WnJM/jQ1YpY9000OaVXlAQj6Zp/kIqJxEDWIZ67tAd7NLuk7zqN4yqe9nc0oNAOs1w==} peerDependencies: postcss: '>=5.0.0' @@ -6356,8 +6355,12 @@ packages: postcss-scss: optional: true dependencies: - postcss: 8.4.27 - postcss-html: 1.5.0 + postcss: 7.0.39 + postcss-html: 0.36.0(postcss-syntax@0.36.2)(postcss@7.0.39) + postcss-jsx: 0.36.4(postcss-syntax@0.36.2)(postcss@7.0.39) + postcss-less: 3.1.4 + postcss-markdown: 0.36.0(postcss-syntax@0.36.2)(postcss@7.0.39) + postcss-scss: 2.1.1 dev: true /postcss-value-parser@3.3.1: @@ -7033,6 +7036,7 @@ packages: /source-map@0.8.0-beta.0: resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} engines: {node: '>= 8'} + deprecated: The work that was done in this beta branch won't be included in future versions dependencies: whatwg-url: 7.1.0 dev: true @@ -7283,7 +7287,7 @@ packages: stylelint: 9.10.1 dev: true - /stylelint-stylus@0.18.0(postcss-syntax@0.36.2)(stylelint@15.10.2): + /stylelint-stylus@0.18.0(stylelint@15.10.2): resolution: {integrity: sha512-n3zjLFLonPOUYY3UIUtSKzZzPB9GQo+BvtmcEQfI+4QHiNsHpfr9QrznkWT8sUB+S11dr3JRCzZ45K9lRQviSg==} engines: {node: ^12 || >=14} peerDependencies: @@ -7300,7 +7304,6 @@ packages: postcss-media-query-parser: 0.2.3 postcss-selector-parser: 6.0.13 postcss-styl: 0.12.3 - postcss-syntax: 0.36.2(postcss-html@1.5.0)(postcss@8.4.27) style-search: 0.1.0 stylelint: 15.10.2 stylelint-config-html: 1.1.0(postcss-html@1.5.0)(stylelint@15.10.2) @@ -7399,7 +7402,7 @@ packages: postcss-sass: 0.3.5 postcss-scss: 2.1.1 postcss-selector-parser: 3.1.2 - postcss-syntax: 0.36.2(postcss-html@1.5.0)(postcss@8.4.27) + postcss-syntax: 0.36.2(postcss-html@0.36.0)(postcss-jsx@0.36.4)(postcss-less@3.1.4)(postcss-markdown@0.36.0)(postcss-scss@2.1.1)(postcss@7.0.39) postcss-value-parser: 3.3.1 resolve-from: 4.0.0 signal-exit: 3.0.7 @@ -7829,10 +7832,10 @@ packages: x-is-string: 0.1.0 dev: true - /unimport@3.1.0(rollup@2.79.1): + /unimport@3.1.0: resolution: {integrity: sha512-ybK3NVWh30MdiqSyqakrrQOeiXyu5507tDA0tUf7VJHrsq4DM6S43gR7oAsZaFojM32hzX982Lqw02D3yf2aiA==} dependencies: - '@rollup/pluginutils': 5.0.2(rollup@2.79.1) + '@rollup/pluginutils': 5.0.2 escape-string-regexp: 5.0.0 fast-glob: 3.3.1 local-pkg: 0.4.3 @@ -7911,7 +7914,7 @@ packages: engines: {node: '>= 10.0.0'} dev: true - /unplugin-auto-import@0.15.3(rollup@2.79.1): + /unplugin-auto-import@0.15.3: resolution: {integrity: sha512-RLT8SqbPn4bT7yBshZId0uPSofKWnwr66RyDaxWaFb/+f7OTDOWAsVNz+hOQLBWSjvbekr2xZY9ccS8TDHJbCQ==} engines: {node: '>=14'} peerDependencies: @@ -7924,11 +7927,11 @@ packages: optional: true dependencies: '@antfu/utils': 0.7.5 - '@rollup/pluginutils': 5.0.2(rollup@2.79.1) + '@rollup/pluginutils': 5.0.2 local-pkg: 0.4.3 magic-string: 0.30.1 minimatch: 9.0.3 - unimport: 3.1.0(rollup@2.79.1) + unimport: 3.1.0 unplugin: 1.4.0 transitivePeerDependencies: - rollup @@ -7966,7 +7969,7 @@ packages: - supports-color dev: true - /unplugin-vue-components@0.24.1(rollup@2.79.1)(vue@3.3.4): + /unplugin-vue-components@0.24.1(vue@3.3.4): resolution: {integrity: sha512-T3A8HkZoIE1Cja95xNqolwza0yD5IVlgZZ1PVAGvVCx8xthmjsv38xWRCtHtwl+rvZyL9uif42SRkDGw9aCfMA==} engines: {node: '>=14'} peerDependencies: @@ -7980,7 +7983,7 @@ packages: optional: true dependencies: '@antfu/utils': 0.7.5 - '@rollup/pluginutils': 5.0.2(rollup@2.79.1) + '@rollup/pluginutils': 5.0.2 chokidar: 3.5.3 debug: 4.3.4 fast-glob: 3.3.1 @@ -8096,12 +8099,10 @@ packages: vfile-message: 1.1.1 dev: true - /vite-plugin-pwa@0.12.8(vite@2.7.13)(workbox-build@6.6.0)(workbox-window@6.6.0): + /vite-plugin-pwa@0.12.8(vite@2.7.13): resolution: {integrity: sha512-pSiFHmnJGMQJJL8aJzQ8SaraZBSBPMGvGUkCNzheIq9UQCEk/eP3UmANNmS9eupuhIpTK8AdxTOHcaMcAqAbCA==} peerDependencies: vite: ^2.0.0 || ^3.0.0-0 - workbox-build: ^6.4.0 - workbox-window: ^6.4.0 dependencies: debug: 4.3.4 fast-glob: 3.3.1 @@ -8111,6 +8112,7 @@ packages: workbox-build: 6.6.0 workbox-window: 6.6.0 transitivePeerDependencies: + - '@types/babel__core' - supports-color dev: true diff --git a/src/common/constant/init.ts b/src/common/constant/init.ts index a7380dac..7bd68520 100644 --- a/src/common/constant/init.ts +++ b/src/common/constant/init.ts @@ -5,6 +5,7 @@ export const INIT_REPO_BARNCH = 'master' export const GH_PAGES = 'gh-pages' export const PICX_UPLOAD_IMG_DESC = 'Upload image via PicX (https://github.com/XPoet/picx)' export const PICX_UPLOAD_IMGS_DESC = 'Upload images via PicX (https://github.com/XPoet/picx)' +export const PICX_UPLOAD_VIDEO_DESC = 'Upload video via PicX (https://github.com/XPoet/picx)' export const PICX_DEL_IMG_DESC = 'Delete image via PicX (https://github.com/XPoet/picx)' export const PICX_INIT_SETTINGS_MSG = 'Init settings via PicX (https://github.com/XPoet/picx)' export const PICX_UPDATE_SETTINGS_MSG = 'Update settings via PicX (https://github.com/XPoet/picx)' diff --git a/src/common/model/index.ts b/src/common/model/index.ts index d93b49c5..972d6599 100644 --- a/src/common/model/index.ts +++ b/src/common/model/index.ts @@ -3,3 +3,4 @@ export * from './user-config' export * from './user-settings' export * from './vite-config' export * from './tool' +export * from './video' diff --git a/src/common/model/video.ts b/src/common/model/video.ts new file mode 100644 index 00000000..3432bc5c --- /dev/null +++ b/src/common/model/video.ts @@ -0,0 +1,57 @@ +/** + * Uploaded video object Model + */ +export interface UploadedVideoModel { + type: string + uuid: string + sha: string + dir: string + path: string + name: string + size: number + deleting: boolean + checked: boolean + active?: boolean + deployed?: boolean +} + +/** + * Upload list video object Model + */ +export interface UploadVideoModel { + uuid: string + + base64: { + originalBase64: string + } + + fileInfo: { + originalFile: File | null + } + + filename: { + name: string + initName: string // initial name + final: string // final name + suffix: string // suffix + isRename: boolean // whether to rename + newName: string // new name + isAddHash: boolean // whether to add hash + hash: string // hash + isAddPrefix: boolean // whether to add prefix + prefix: string // prefix + } + + uploadStatus: { + progress: 0 | 100 + uploading: boolean + } + + uploadedVideo?: UploadedVideoModel + + reUploadInfo?: { + isReUpload: boolean + path: string + dir: string + } +} diff --git a/src/components.d.ts b/src/components.d.ts index a5f6cc50..b8bb94f6 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -121,4 +121,4 @@ declare module '@vue/runtime-core' { export interface ComponentCustomProperties { vLoading: typeof import('element-plus/es')['ElLoadingDirective'] } -} \ No newline at end of file +} diff --git a/src/components/nav-content/nav-content.data.ts b/src/components/nav-content/nav-content.data.ts index d013af7a..dfc967f5 100644 --- a/src/components/nav-content/nav-content.data.ts +++ b/src/components/nav-content/nav-content.data.ts @@ -18,6 +18,14 @@ export const navInfoList = shallowRef([ path: '/upload', isShow: true }, + { + uuid: getUuid(), + name: 'nav.video_upload', + icon: IEpUpload, + isActive: false, + path: '/upload-video', + isShow: true + }, { uuid: getUuid(), name: 'nav.management', diff --git a/src/components/nav-content/nav-content.vue b/src/components/nav-content/nav-content.vue index 140916bf..740c2727 100644 --- a/src/components/nav-content/nav-content.vue +++ b/src/components/nav-content/nav-content.vue @@ -87,7 +87,8 @@ const onNavClick = (e: any) => { const changeNavActive = (currentPath: string) => { navInfoList.value.forEach((v) => { const temp = v - temp.isActive = v.path === currentPath || currentPath.includes(v.path) + const rootPath = `/${currentPath.split('/')[1]}` + temp.isActive = v.path === currentPath || rootPath === v.path return temp }) diff --git a/src/locales/zh-CN.json b/src/locales/zh-CN.json index 56a4f55c..8ee90f3a 100644 --- a/src/locales/zh-CN.json +++ b/src/locales/zh-CN.json @@ -40,7 +40,8 @@ "settings": "图床设置", "toolbox": "工具箱", "feedback": "帮助反馈", - "actions": "快捷操作" + "actions": "快捷操作", + "video_upload": "上传视频" }, "actions": { "watermark": "图片水印", @@ -276,4 +277,4 @@ "text_8": "复制其他仓库图片", "loading_1": "复制中" } -} +} \ No newline at end of file diff --git a/src/router/index.ts b/src/router/index.ts index 78cd0f20..43a7ec61 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -35,6 +35,14 @@ const routes: Array = [ title: 'nav.upload' } }, + { + path: '/upload-video', + name: 'upload-video', + component: () => import('@/views/upload-video/upload-video.vue'), + meta: { + title: 'nav.video_upload' + } + }, { path: '/management', name: 'Management', diff --git a/src/utils/upload-utils.ts b/src/utils/upload-utils.ts index 61987e2f..47b2a453 100644 --- a/src/utils/upload-utils.ts +++ b/src/utils/upload-utils.ts @@ -1,4 +1,5 @@ import { UploadedImageModel, UserConfigInfoModel, UploadImageModel } from '@/common/model' +import { UploadedVideoModel, UploadVideoModel } from '@/common/model/video' import { store } from '@/stores' import { createCommit, @@ -8,7 +9,7 @@ import { getFileBlob, getBranchInfo } from '@/common/api' -import { PICX_UPLOAD_IMG_DESC } from '@/common/constant' +import { PICX_UPLOAD_IMG_DESC, PICX_UPLOAD_VIDEO_DESC } from '@/common/constant' import i18n from '@/plugins/vue/i18n' /** @@ -195,3 +196,112 @@ export function uploadImageToGitHub( } }) } + +/** + * 视频上传成功之后的处理 + * @param res + * @param video + * @param userConfigInfo + */ +const videoUploadedHandle = ( + res: { name: string; sha: string; path: string; size: number }, + video: UploadVideoModel, + userConfigInfo: UserConfigInfoModel +) => { + let dir = userConfigInfo.selectedDir + + if (video?.reUploadInfo?.isReUpload) { + dir = video.reUploadInfo.dir + } + + // 上传状态处理 + video.uploadStatus.progress = 100 + video.uploadStatus.uploading = false + + const uploadedVideo: UploadedVideoModel = { + checked: false, + type: 'video', + uuid: video.uuid, + dir, + name: res.name, + sha: res.sha, + path: res.path, + deleting: false, + size: res.size, + deployed: true + } + + video.uploadedVideo = uploadedVideo + + // dirImageList 增加目录 + store.dispatch('DIR_IMAGE_LIST_ADD_DIR', dir) + + // dirImageList 增加视频 + store.dispatch('DIR_IMAGE_LIST_ADD_IMAGE', uploadedVideo) +} + +/** + * 上传视频的 URL 处理 + * @param config + * @param videoObj + */ +export const uploadVideoUrlHandle = ( + config: UserConfigInfoModel, + videoObj: UploadVideoModel +): string => { + const { owner, repo, selectedDir: dir } = config + const filename: string = videoObj.filename.final + + let path = filename + + if (dir !== '/') { + path = `${dir}/${filename}` + } + + if (videoObj?.reUploadInfo?.isReUpload) { + path = videoObj.reUploadInfo.path + } + + return `/repos/${owner}/${repo}/contents/${path}` +} + +/** + * 上传一个视频到 GitHub 仓库 + * @param userConfigInfo + * @param video + */ +export function uploadVideoToGitHub( + userConfigInfo: UserConfigInfoModel, + video: UploadVideoModel +): Promise { + const { branch, email, owner } = userConfigInfo + + const data: any = { + message: PICX_UPLOAD_VIDEO_DESC, + branch, + content: video.base64.originalBase64.split(',')[1] + } + + if (email) { + data.committer = { + name: owner, + email + } + } + + video.uploadStatus.uploading = true + + // eslint-disable-next-line no-async-promise-executor + return new Promise(async (resolve) => { + const uploadRes = await uploadSingleImage(uploadVideoUrlHandle(userConfigInfo, video), data) + console.log('uploadSingleVideo >> ', uploadRes) + video.uploadStatus.uploading = false + if (uploadRes) { + const { name, sha, path, size } = uploadRes.content + videoUploadedHandle({ name, sha, path, size }, video, userConfigInfo) + resolve(true) + } else { + resolve(false) + } + }) +} diff --git a/src/views/upload-video/upload-video.styl b/src/views/upload-video/upload-video.styl new file mode 100644 index 00000000..e69de29b diff --git a/src/views/upload-video/upload-video.vue b/src/views/upload-video/upload-video.vue new file mode 100644 index 00000000..a9df869d --- /dev/null +++ b/src/views/upload-video/upload-video.vue @@ -0,0 +1,7 @@ + + + + + From d5c37448cc02ea42ec77c550cfb18cdbc4bb0760 Mon Sep 17 00:00:00 2001 From: lstmxx <740719284@qq.com> Date: Sun, 28 Sep 2025 11:33:19 +0800 Subject: [PATCH 2/8] feat: add getting-video component --- src/auto-imports.d.ts | 1 - src/common/model/tool.ts | 6 ++ src/common/model/video.ts | 4 +- src/locales/en.json | 3 +- src/locales/zh-CN.json | 1 + src/locales/zh-TW.json | 3 +- src/stores/index.ts | 4 +- src/stores/modules/upload-video-list/index.ts | 40 +++++++ src/stores/modules/upload-video-list/types.ts | 5 + src/utils/file-utils.ts | 38 ++++++- .../getting-video/getting-video.styl | 61 +++++++++++ .../getting-video/getting-video.vue | 48 +++++++++ .../getting-video/hooks/use-getting-vidoe.ts | 69 ++++++++++++ src/views/upload-video/upload-video.styl | 102 ++++++++++++++++++ src/views/upload-video/upload-video.vue | 100 ++++++++++++++++- 15 files changed, 474 insertions(+), 11 deletions(-) create mode 100644 src/stores/modules/upload-video-list/index.ts create mode 100644 src/stores/modules/upload-video-list/types.ts create mode 100644 src/views/upload-video/components/getting-video/getting-video.styl create mode 100644 src/views/upload-video/components/getting-video/getting-video.vue create mode 100644 src/views/upload-video/components/getting-video/hooks/use-getting-vidoe.ts diff --git a/src/auto-imports.d.ts b/src/auto-imports.d.ts index cee8c010..001d94e0 100644 --- a/src/auto-imports.d.ts +++ b/src/auto-imports.d.ts @@ -18,6 +18,5 @@ declare global { const IEpPicture: typeof import('~icons/ep/picture')['default'] const IEpPostcard: typeof import('~icons/ep/postcard')['default'] const IEpSetting: typeof import('~icons/ep/setting')['default'] - const IEpSwitch: typeof import('~icons/ep/switch')['default'] const IEpUpload: typeof import('~icons/ep/upload')['default'] } diff --git a/src/common/model/tool.ts b/src/common/model/tool.ts index a4bc59af..d8e4100c 100644 --- a/src/common/model/tool.ts +++ b/src/common/model/tool.ts @@ -12,6 +12,12 @@ export interface ImageHandleResult { file: File } +export interface VideoHandleResult { + uuid: string + objectURL: string + file: File +} + export interface ImgProcessStateModel { uuid: string originalName: string diff --git a/src/common/model/video.ts b/src/common/model/video.ts index 3432bc5c..4c03e471 100644 --- a/src/common/model/video.ts +++ b/src/common/model/video.ts @@ -21,9 +21,7 @@ export interface UploadedVideoModel { export interface UploadVideoModel { uuid: string - base64: { - originalBase64: string - } + objectURL: string fileInfo: { originalFile: File | null diff --git a/src/locales/en.json b/src/locales/en.json index a3837ad5..2dd40f60 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -163,6 +163,7 @@ }, "upload_page": { "upload_area_text": "Drag / Paste / Click here to select images", + "upload_video_area_text": "Drag / Paste / Click here to select videos", "message1": "Please complete image hosting configuration first", "message2": "Please select a repository", "message3": "Directory cannot be empty", @@ -276,4 +277,4 @@ "text_8": "Copy other repository images", "loading_1": "Copying" } -} +} \ No newline at end of file diff --git a/src/locales/zh-CN.json b/src/locales/zh-CN.json index 8ee90f3a..21ae431a 100644 --- a/src/locales/zh-CN.json +++ b/src/locales/zh-CN.json @@ -164,6 +164,7 @@ }, "upload_page": { "upload_area_text": "拖拽 / 粘贴 / 点击此处选择图片", + "upload_video_area_text": "拖拽 / 粘贴 / 点击此处选择视频", "message1": "请先完成图床配置", "message2": "请选择一个仓库", "message3": "目录不能为空", diff --git a/src/locales/zh-TW.json b/src/locales/zh-TW.json index a0573561..dbd60140 100644 --- a/src/locales/zh-TW.json +++ b/src/locales/zh-TW.json @@ -163,6 +163,7 @@ }, "upload_page": { "upload_area_text": "拖曳 / 貼上 / 點擊此處選擇圖片", + "upload_video_area_text": "拖曳 / 貼上 / 點擊此處選擇影片", "message1": "請先完成圖床配置", "message2": "請選擇一個倉庫", "message3": "目錄不能為空", @@ -276,4 +277,4 @@ "text_8": "複製其他倉庫圖片", "loading_1": "複製中" } -} +} \ No newline at end of file diff --git a/src/stores/index.ts b/src/stores/index.ts index a9c58f2c..f8b40774 100644 --- a/src/stores/index.ts +++ b/src/stores/index.ts @@ -10,6 +10,7 @@ import toolboxImageListModule from './modules/toolbox-image-list' import uploadImageListModule from './modules/upload-image-list' import githubAuthorizeModule from './modules/github-authorize' import deployStatusModule from './modules/deploy-status' +import uploadVideoListModule from './modules/upload-video-list' // Create a new store instance export const store = createStore({ @@ -22,7 +23,8 @@ export const store = createStore({ toolboxImageListModule, uploadImageListModule, githubAuthorizeModule, - deployStatusModule + deployStatusModule, + uploadVideoListModule }, state: { rootName: 'root' diff --git a/src/stores/modules/upload-video-list/index.ts b/src/stores/modules/upload-video-list/index.ts new file mode 100644 index 00000000..1f24b8ba --- /dev/null +++ b/src/stores/modules/upload-video-list/index.ts @@ -0,0 +1,40 @@ +import { Module } from 'vuex' +import RootStateTypes from '@/stores/types' +import UploadVideoListStateTypes from './types' +import { UploadVideoModel } from '@/common/model' + +const uploadVideoListModule: Module = { + state: { + uploadVideoList: [] + }, + + mutations: {}, + + actions: { + // 上传处理的图片列表 - 增加 + UPLOAD_VIDEO_LIST_ADD({ state }, item: UploadVideoModel) { + state.uploadVideoList.unshift(item) + }, + + // 上传处理的图片列表 - 删除 + UPLOAD_VIDEO_LIST_REMOVE({ state }, uuid: string) { + if (state.uploadVideoList.length > 0) { + const rmIdx = state.uploadVideoList.findIndex((v) => v.uuid === uuid) + if (rmIdx !== -1 && state.uploadVideoList[rmIdx].uploadStatus.progress === 0) { + state.uploadVideoList.splice(rmIdx, 1) + } + } + }, + + // 上传处理的图片列表 - 重置 + UPLOAD_VIDEO_LIST_RESET({ state }) { + state.uploadVideoList = [] + } + }, + + getters: { + getUploadVideoList: (state): UploadVideoModel[] => state.uploadVideoList + } +} + +export default uploadVideoListModule diff --git a/src/stores/modules/upload-video-list/types.ts b/src/stores/modules/upload-video-list/types.ts new file mode 100644 index 00000000..bd54971f --- /dev/null +++ b/src/stores/modules/upload-video-list/types.ts @@ -0,0 +1,5 @@ +import { UploadVideoModel } from '@/common/model' + +export default interface UploadImageListStateTypes { + uploadVideoList: UploadVideoModel[] +} diff --git a/src/utils/file-utils.ts b/src/utils/file-utils.ts index 77cd3b30..0c50791c 100644 --- a/src/utils/file-utils.ts +++ b/src/utils/file-utils.ts @@ -1,5 +1,5 @@ import { ElMessage } from 'element-plus' -import { ImageHandleResult } from '@/common/model' +import { ImageHandleResult, VideoHandleResult } from '@/common/model' import { getUuid } from '@/utils/common-utils' import { imgFileToBase64 } from '@/utils/image-utils' import { IMG_UPLOAD_MAX_SIZE } from '@/common/constant' @@ -95,3 +95,39 @@ export const gettingFilesHandle = (file: File): Promise { + fileType = fileType.toLowerCase() + return /(mp4|mov|avi|mkv|flv|wmv|webm)$/.test(fileType) +} + +/** + * 处理获取的视频文件 + * @param file + */ +export const gettingVideoFilesHandle = (file: File): Promise => { + return new Promise((resolve) => { + if (!file) { + resolve(null) + } + + const fileType = file.name.split('.').pop() || '' + + if (!isVideo(fileType)) { + ElMessage.error(i18n.global.t('upload_page.tip_9', { name: file.name })) + resolve(null) + } + + const objectURL = URL.createObjectURL(file) + + resolve({ + uuid: getUuid(), + objectURL, + file + }) + }) +} diff --git a/src/views/upload-video/components/getting-video/getting-video.styl b/src/views/upload-video/components/getting-video/getting-video.styl new file mode 100644 index 00000000..23ebebc2 --- /dev/null +++ b/src/views/upload-video/components/getting-video/getting-video.styl @@ -0,0 +1,61 @@ +.getting-images-container { + position relative + z-index 999 + display flex + align-items center + justify-content center + box-sizing border-box + width 100% + height 300rem + border 4rem dashed var(--text-color-4) + border-radius 8rem + + &.focus { + border-color var(--el-color-primary) + } + + &.disabled { + pointer-events none + } + + &:hover { + border-color var(--el-color-primary) + } + + label { + position absolute + z-index 1000 + display block + width 100% + height 100% + cursor pointer + } + + input[type="file"] { + position absolute + top -9999rem + left -9999rem + } + + .upload-area-tips { + color #aaa + text-align center + user-select none + + .icon { + font-size 100rem + } + + .text { + font-size 20rem + cursor default + } + } + + + .preview-video { + width 100% + height 100% + background-color #000 + } +} diff --git a/src/views/upload-video/components/getting-video/getting-video.vue b/src/views/upload-video/components/getting-video/getting-video.vue new file mode 100644 index 00000000..faaa44c3 --- /dev/null +++ b/src/views/upload-video/components/getting-video/getting-video.vue @@ -0,0 +1,48 @@ + + + + + + diff --git a/src/views/upload-video/components/getting-video/hooks/use-getting-vidoe.ts b/src/views/upload-video/components/getting-video/hooks/use-getting-vidoe.ts new file mode 100644 index 00000000..31dfbd56 --- /dev/null +++ b/src/views/upload-video/components/getting-video/hooks/use-getting-vidoe.ts @@ -0,0 +1,69 @@ +/* eslint-disable no-unused-vars */ + +import { ref } from 'vue' +import { gettingVideoFilesHandle } from '@/utils/file-utils' +import { VideoHandleResult } from '@/common/model' + +export const useGettingVideo = (onSelectSuccess: (result: VideoHandleResult[]) => void) => { + const curShowVideo = ref<{ + uuid: string + objectURL: string + }>({ + uuid: '', + objectURL: '' + }) + + const handleVideoFiles = async ( + files: FileList | undefined | null + ): Promise => { + console.log('files', files) + if (!files || files.length === 0) return [] + + const uploadResult: VideoHandleResult[] = [] + + await Promise.all( + Array.from(files).map(async (file) => { + const result = await gettingVideoFilesHandle(file) + if (result) { + uploadResult.push(result) + } + }) + ) + + if (uploadResult.length > 0) { + const lastIndex = uploadResult.length - 1 + const { uuid, objectURL } = uploadResult[lastIndex] + curShowVideo.value = { + uuid, + objectURL + } + } + + return uploadResult + } + + const onSelect = async (e: Event) => { + const target = e.target as HTMLInputElement + + onSelectSuccess(await handleVideoFiles(target.files)) + + target.value = '' + target.value = target.defaultValue + } + + const onDrop = async (e: DragEvent) => { + e.preventDefault() + onSelectSuccess(await handleVideoFiles(e.dataTransfer?.files)) + } + + const onPaste = async (e: ClipboardEvent) => { + onSelectSuccess(await handleVideoFiles(e.clipboardData?.files)) + } + + return { + curShowVideo, + onSelect, + onDrop, + onPaste + } +} diff --git a/src/views/upload-video/upload-video.styl b/src/views/upload-video/upload-video.styl index e69de29b..c3eb614f 100644 --- a/src/views/upload-video/upload-video.styl +++ b/src/views/upload-video/upload-video.styl @@ -0,0 +1,102 @@ +.upload-page-container { + display flex + justify-content space-between + width 100% + height 100% + + .upload-page-left { + flex-shrink 0 + box-sizing border-box + width 300rem + height 100% + margin-right 10rem + border-top-right-radius 0 + border-bottom-right-radius 0 + + .uploaded-item { + width 100% + margin-bottom 20rem + + &:last-child { + margin-bottom 0 + } + } + } + + + .upload-page-right { + box-sizing border-box + width 100% + height 100% + overflow-y auto + + &.has-left { + border-top-left-radius 0 + border-bottom-left-radius 0 + } + + .row-item { + display flex + justify-content center + box-sizing border-box + width 100% + margin-bottom 16rem + + &:last-child { + margin-bottom 0 + } + + .content-box { + box-sizing border-box + width 100% + max-width $content-max-width + margin 0 auto + + &.upload-area-status { + display flex + align-items center + justify-content space-between + margin-bottom 10rem + font-size 14rem + } + + + &.operation-btn { + display flex + justify-content flex-end + } + + + .shortcut-key { + margin-left 5rem + padding 4rem 5rem + font-size 12rem + letter-spacing 1rem + border-radius 4rem + box-shadow 1rem 2rem 3rem var(--shadow-color) + + +picx-tablet() { + display none + } + } + } + } + + .upload-tools { + width 100% + + .repos-dir-info { + margin-bottom 20rem + font-size 12rem + + .repos-dir-info-item { + margin-right 10rem + + &:last-child { + margin-right 0 + } + } + } + } + } +} diff --git a/src/views/upload-video/upload-video.vue b/src/views/upload-video/upload-video.vue index a9df869d..55d709f8 100644 --- a/src/views/upload-video/upload-video.vue +++ b/src/views/upload-video/upload-video.vue @@ -1,7 +1,101 @@ - + - + From 16c21cfd78465dfae1a5a53cdfc61fcca94f0eff Mon Sep 17 00:00:00 2001 From: lstmxx <740719284@qq.com> Date: Tue, 30 Sep 2025 10:11:44 +0800 Subject: [PATCH 3/8] feat: add video selection feature --- src/common/constant/settings.ts | 5 + src/common/model/tool.ts | 1 + src/common/model/video.ts | 8 + src/locales/en.json | 1 + src/locales/zh-CN.json | 1 + src/locales/zh-TW.json | 1 + src/utils/file-utils.ts | 18 +- src/utils/video-utils.ts | 172 ++++++++++++++ .../getting-video/hooks/use-getting-vidoe.ts | 1 - .../upload-video-card/hooks/use-filename.ts | 66 ++++++ .../components/upload-video-card/type.ts | 5 + .../upload-video-card/upload-video-card.styl | 210 ++++++++++++++++++ .../upload-video-card/upload-video-card.vue | 200 +++++++++++++++++ src/views/upload-video/upload-video.vue | 31 ++- src/views/upload-video/utils/generate.ts | 36 +++ 15 files changed, 747 insertions(+), 9 deletions(-) create mode 100644 src/utils/video-utils.ts create mode 100644 src/views/upload-video/components/upload-video-card/hooks/use-filename.ts create mode 100644 src/views/upload-video/components/upload-video-card/type.ts create mode 100644 src/views/upload-video/components/upload-video-card/upload-video-card.styl create mode 100644 src/views/upload-video/components/upload-video-card/upload-video-card.vue create mode 100644 src/views/upload-video/utils/generate.ts diff --git a/src/common/constant/settings.ts b/src/common/constant/settings.ts index c08a8816..f749efef 100644 --- a/src/common/constant/settings.ts +++ b/src/common/constant/settings.ts @@ -8,6 +8,11 @@ export const NEW_DIR_COUNT_MAX: number = 5 */ export const IMG_UPLOAD_MAX_SIZE: number = 30 // MB +/** + * 允许上传视频的最大尺寸 + */ +export const VIDEO_UPLOAD_MAX_SIZE: number = 100 // MB + /** * 图片重命名最大长度 */ diff --git a/src/common/model/tool.ts b/src/common/model/tool.ts index d8e4100c..8de13868 100644 --- a/src/common/model/tool.ts +++ b/src/common/model/tool.ts @@ -16,6 +16,7 @@ export interface VideoHandleResult { uuid: string objectURL: string file: File + base64: string } export interface ImgProcessStateModel { diff --git a/src/common/model/video.ts b/src/common/model/video.ts index 4c03e471..c1a56139 100644 --- a/src/common/model/video.ts +++ b/src/common/model/video.ts @@ -21,10 +21,18 @@ export interface UploadedVideoModel { export interface UploadVideoModel { uuid: string + base64: { + originalBase64: string + watermarkBase64: string | null + compressBase64: string | null + } + objectURL: string fileInfo: { originalFile: File | null + compressFile: File | null + watermarkFile: File | null } filename: { diff --git a/src/locales/en.json b/src/locales/en.json index 2dd40f60..8e868dde 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -19,6 +19,7 @@ "upload": "Upload", "rename": "Rename", "copy_link": "Copy Image Link", + "copy_video_link": "Copy Video Link", "paste_image": "Paste images", "copy_success_1": "Image link has been automatically copied to the system clipboard", "copy_success_2": "Image link copied successfully", diff --git a/src/locales/zh-CN.json b/src/locales/zh-CN.json index 21ae431a..26b04aca 100644 --- a/src/locales/zh-CN.json +++ b/src/locales/zh-CN.json @@ -19,6 +19,7 @@ "upload": "上传", "rename": "重命名", "copy_link": "复制图片链接", + "copy_video_link": "复制视频链接", "paste_image": "粘贴图片", "copy_success_1": "图片链接已自动复制到系统剪贴板", "copy_success_2": "图片链接复制成功", diff --git a/src/locales/zh-TW.json b/src/locales/zh-TW.json index dbd60140..2780c535 100644 --- a/src/locales/zh-TW.json +++ b/src/locales/zh-TW.json @@ -19,6 +19,7 @@ "upload": "上傳", "rename": "重新命名", "copy_link": "複製圖片連結", + "copy_video_link": "複製影片連結", "paste_image": "貼上圖片", "copy_success_1": "圖片連結已自動複製到系統剪貼簿", "copy_success_2": "圖片連結複製成功", diff --git a/src/utils/file-utils.ts b/src/utils/file-utils.ts index 0c50791c..c17c537b 100644 --- a/src/utils/file-utils.ts +++ b/src/utils/file-utils.ts @@ -2,8 +2,9 @@ import { ElMessage } from 'element-plus' import { ImageHandleResult, VideoHandleResult } from '@/common/model' import { getUuid } from '@/utils/common-utils' import { imgFileToBase64 } from '@/utils/image-utils' -import { IMG_UPLOAD_MAX_SIZE } from '@/common/constant' +import { IMG_UPLOAD_MAX_SIZE, VIDEO_UPLOAD_MAX_SIZE } from '@/common/constant' import i18n from '@/plugins/vue/i18n' +import { videoFileToBase64 } from './video-utils' /** * 获取文件名 @@ -110,7 +111,8 @@ export const isVideo = (fileType: string): boolean => { * @param file */ export const gettingVideoFilesHandle = (file: File): Promise => { - return new Promise((resolve) => { + // eslint-disable-next-line no-async-promise-executor + return new Promise(async (resolve) => { if (!file) { resolve(null) } @@ -124,10 +126,20 @@ export const gettingVideoFilesHandle = (file: File): Promise= VIDEO_UPLOAD_MAX_SIZE * 1024) { + ElMessage.error( + i18n.global.t('upload_page.tip_10', { name: file.name, size: IMG_UPLOAD_MAX_SIZE }) + ) + resolve(null) + } + resolve({ uuid: getUuid(), objectURL, - file + file, + base64 }) }) } diff --git a/src/utils/video-utils.ts b/src/utils/video-utils.ts new file mode 100644 index 00000000..e10cb8c0 --- /dev/null +++ b/src/utils/video-utils.ts @@ -0,0 +1,172 @@ +import { computed } from 'vue' +import { UploadedVideoModel, UploadVideoModel } from '@/common/model' +import { store } from '@/stores' +import { copyText } from './common-utils' +import i18n from '@/plugins/vue/i18n' + +/** + * 视频 File 格式转 Base64 格式 + * @param file + */ +export function videoFileToBase64(file: File): Promise { + return new Promise((resolve) => { + const reader = new FileReader() + reader.readAsDataURL(file) + reader.onload = () => { + const base64 = reader.result as string + resolve(base64) + } + reader.onerror = () => resolve(null) + }) +} + +/** + * 生成一个上传的视频对象 + */ +export const createUploadVideoObject = (): UploadVideoModel => { + return { + uuid: '', + objectURL: '', + base64: { + originalBase64: '', + watermarkBase64: null, + compressBase64: null + }, + fileInfo: { + originalFile: null, + compressFile: null, + watermarkFile: null + }, + filename: { + hash: '', + suffix: '', + name: '', + prefix: '', + final: '', + initName: '', + newName: '', + isAddHash: true, + isRename: false, + isAddPrefix: false + }, + uploadStatus: { + progress: 0, + uploading: false + }, + reUploadInfo: { + dir: '', + path: '', + isReUpload: false + } + } +} + +/** + * 文件名称添加前缀的处理 + * @param filename + * @param isAddPrefix + */ +export const addPrefixHandle = ( + filenameObj: UploadVideoModel['filename'], + isAddPrefix: boolean +) => { + filenameObj.isAddPrefix = isAddPrefix + if (isAddPrefix) { + filenameObj.name = `${filenameObj.prefix}${filenameObj.initName}` + } else { + filenameObj.name = `${filenameObj.initName}` + } + if (filenameObj.isAddHash) { + filenameObj.final = `${filenameObj.name}.${filenameObj.hash}.${filenameObj.suffix}` + } else { + filenameObj.final = `${filenameObj.name}.${filenameObj.suffix}` + } +} + +/** + * 文件名称添加哈希值的处理 + * @param filenameObj + * @param isAddHash + */ +export const addHashHandle = (filenameObj: UploadVideoModel['filename'], isAddHash: boolean) => { + filenameObj.isAddHash = isAddHash + if (isAddHash) { + filenameObj.final = `${filenameObj.name}.${filenameObj.hash}.${filenameObj.suffix}` + } else { + filenameObj.final = `${filenameObj.name}.${filenameObj.suffix}` + } +} + +/** + * 重命名 + * @param filenameObj + * @param isRename + */ +export const rename = ( + filenameObj: UploadVideoModel['filename'], + isRename: boolean, + isAddPrefix: boolean +) => { + filenameObj.isRename = isRename + + if (isRename) { + filenameObj.name = filenameObj.newName.trim().replace(/\s+/g, '-') + } else { + addPrefixHandle(filenameObj, isAddPrefix) // 恢复列表 prefix 选项 + } + + if (filenameObj.isAddHash) { + filenameObj.final = `${filenameObj.name}.${filenameObj.hash}.${filenameObj.suffix}` + } else { + filenameObj.final = `${filenameObj.name}.${filenameObj.suffix}` + } +} + +/** + * 生成一个视频链接 + * @param videoObj + */ +export const generateVideoLink = (videoObj: UploadedVideoModel): string | null => { + const userConfigInfo = computed(() => store.getters.getUserConfigInfo).value + const userSettings = computed(() => store.getters.getUserSettings).value + + const { selected } = userSettings.imageLinkType + const { rule } = userSettings.imageLinkType.presetList[selected] + if (rule) { + const { owner, repo, branch } = userConfigInfo + return rule + .replaceAll('{{owner}}', owner) + .replaceAll('{{repo}}', repo) + .replaceAll('{{branch}}', branch) + .replaceAll('{{path}}', videoObj.path) + } + return null +} + +const copyMessage = (autoCopy = false) => { + const message: string = autoCopy + ? i18n.global.t('copy_success_1') + : i18n.global.t('copy_success_2') + + ElMessage({ + type: autoCopy ? 'info' : 'success', + message, + duration: autoCopy ? 6000 : 4000 + }) +} + +/** + * 复制单个视频链接 + * @param videoObj + * @param autoCopy + */ +export const copyVideoLink = (videoObj: UploadedVideoModel, autoCopy: boolean = false) => { + const link = generateVideoLink(videoObj) + if (link) { + copyText(link, () => { + copyMessage(autoCopy) + }) + } else { + ElMessage.error({ message: i18n.global.t('copy_fail_1') }) + } +} diff --git a/src/views/upload-video/components/getting-video/hooks/use-getting-vidoe.ts b/src/views/upload-video/components/getting-video/hooks/use-getting-vidoe.ts index 31dfbd56..9d34b791 100644 --- a/src/views/upload-video/components/getting-video/hooks/use-getting-vidoe.ts +++ b/src/views/upload-video/components/getting-video/hooks/use-getting-vidoe.ts @@ -52,7 +52,6 @@ export const useGettingVideo = (onSelectSuccess: (result: VideoHandleResult[]) = } const onDrop = async (e: DragEvent) => { - e.preventDefault() onSelectSuccess(await handleVideoFiles(e.dataTransfer?.files)) } diff --git a/src/views/upload-video/components/upload-video-card/hooks/use-filename.ts b/src/views/upload-video/components/upload-video-card/hooks/use-filename.ts new file mode 100644 index 00000000..4f406512 --- /dev/null +++ b/src/views/upload-video/components/upload-video-card/hooks/use-filename.ts @@ -0,0 +1,66 @@ +import { computed, ref } from 'vue' +import { Props } from '../type' +import { store } from '@/stores' +import { addHashHandle, addPrefixHandle, rename } from '@/utils/video-utils' + +export const useFilename = (props: Readonly) => { + const renameInputRef = ref(null) + + const userSettings = computed(() => store.getters.getUserSettings).value + + const fileNameOperateData = ref({ + isAddHash: false, + isAddPrefix: false, + isRename: false, + newName: '' + }) + + const onHashRename = (e: boolean) => { + addHashHandle(props.videoItem.filename, e) + } + + const onPrefixNaming = (e: boolean) => { + addPrefixHandle(props.videoItem.filename, e) + } + + const onRename = () => { + props.videoItem!.filename.newName = fileNameOperateData.value.newName + setTimeout(() => { + renameInputRef.value?.focus() + }, 100) + rename( + props.videoItem!.filename, + fileNameOperateData.value.isRename, + fileNameOperateData.value.isAddPrefix + ) + } + + const initFilename = () => { + const { imageName } = userSettings + if (props.videoItem!.uploadStatus.progress === 0) { + props.videoItem!.filename.isAddHash = imageName.enableHash + props.videoItem!.filename.isAddPrefix = imageName.addPrefix.enable + props.videoItem!.filename.prefix = imageName.addPrefix.prefix + + // 添加前缀处理 + addPrefixHandle(props.videoItem.filename, imageName.addPrefix.enable) + + // 添加哈希值处理 + addHashHandle(props.videoItem.filename, imageName.enableHash) + + fileNameOperateData.value.isAddHash = props.videoItem!.filename.isAddHash + fileNameOperateData.value.isAddPrefix = props.videoItem!.filename.isAddPrefix + fileNameOperateData.value.isRename = props.videoItem!.filename.isRename + fileNameOperateData.value.newName = props.videoItem!.filename.newName + } + } + + return { + renameInputRef, + fileNameOperateData, + initFilename, + onHashRename, + onPrefixNaming, + onRename + } +} diff --git a/src/views/upload-video/components/upload-video-card/type.ts b/src/views/upload-video/components/upload-video-card/type.ts new file mode 100644 index 00000000..8742c303 --- /dev/null +++ b/src/views/upload-video/components/upload-video-card/type.ts @@ -0,0 +1,5 @@ +import { UploadVideoModel } from '@/common/model' + +export type Props = { + videoItem: UploadVideoModel +} diff --git a/src/views/upload-video/components/upload-video-card/upload-video-card.styl b/src/views/upload-video/components/upload-video-card/upload-video-card.styl new file mode 100644 index 00000000..da0ee78b --- /dev/null +++ b/src/views/upload-video/components/upload-video-card/upload-video-card.styl @@ -0,0 +1,210 @@ +.upload-video-card-container { + position relative + display flex + flex-direction column + justify-content flex-start + box-sizing border-box + width 100% + margin-bottom 20rem + overflow hidden + border 1rem solid var(--border-color) + border-radius 6rem + + &.wait-upload { + border-color var(--await-upload-color) + } + + &.uploading { + border-color var(--uploading-color) + } + + &.uploaded { + border-color var(--uploaded-color) + } + + &:last-child { + margin-bottom 0 + } + + + &:hover { + .del-video-btn { + display block + } + } + + + .video-show-container { + position relative + box-sizing border-box + width 100% + height 140rem + + .preview-video { + width 100% + height 100% + background #000 + cursor pointer + } + } + + + .before-upload-handle-container { + position relative + box-sizing border-box + width 100% + border-top 1rem solid var(--border-color) + + .video-name-box { + position relative + display flex + align-items center + justify-content space-between + box-sizing border-box + width 100% + padding 2rem 5rem + font-size 13rem + border-bottom 1rem solid var(--border-color) + + &.no-border { + border-bottom none + } + + .video-name { + position relative + box-sizing border-box + width calc(100% - 20rem) + } + + .fold-btn { + position relative + display flex + align-items center + justify-content end + box-sizing border-box + width 20rem + font-size 15rem + cursor pointer + } + } + + .video-name-operate-box { + position relative + display flex + flex-direction column + justify-content center + box-sizing border-box + padding 2rem 5rem + border-bottom 1rem solid var(--border-color) + + &.folded { + display none + } + + + .operate-item { + display flex + align-items center + height 28rem + + .rename-input { + margin-left 10rem + } + } + } + + .video-info-box { + display flex + align-items center + justify-content space-between + padding 5rem + font-size 12rem + user-select none + + .file-size-box { + transform scale(0.9) + + .file-size-item { + padding 2rem 3rem + background var(--background-color-3) + border-radius 3rem + } + + + .original-file-size { + margin-left -4rem + + &.del-line { + text-decoration line-through + } + } + + + .finial-file-size { + margin-left 6rem + color var(--el-color-primary) + } + } + } + } + + + .after-upload-handle-container { + position relative + box-sizing border-box + width 100% + height 30rem + color var(--el-color-primary) + font-size 13rem + background var(--el-color-primary-light-9) + border-top 1rem solid var(--border-color) + cursor pointer + + &:hover { + color var(--el-color-white) + background var(--el-color-primary) + } + } + + + .del-video-btn { + position absolute + top 6rem + right 6rem + display none + color var(--background-color) + font-size 22rem + cursor pointer + } + + .upload-status-box { + position absolute + top -8rem + left -16rem + box-sizing border-box + width 46rem + height 26rem + color #fff + text-align center + box-shadow 0 1rem 1rem var(--border-color) + transform rotate(315deg) + + &.wait-upload { + background var(--await-upload-color) + } + + &.uploaded { + background var(--uploaded-color) + + .el-icon { + margin-top 12rem + } + } + + .el-icon { + margin-top 10rem + font-size 12rem + transform rotate(45deg) + } + } +} diff --git a/src/views/upload-video/components/upload-video-card/upload-video-card.vue b/src/views/upload-video/components/upload-video-card/upload-video-card.vue new file mode 100644 index 00000000..dae8fd40 --- /dev/null +++ b/src/views/upload-video/components/upload-video-card/upload-video-card.vue @@ -0,0 +1,200 @@ + + + + + + diff --git a/src/views/upload-video/upload-video.vue b/src/views/upload-video/upload-video.vue index 55d709f8..4472b060 100644 --- a/src/views/upload-video/upload-video.vue +++ b/src/views/upload-video/upload-video.vue @@ -1,9 +1,11 @@ diff --git a/src/views/upload-video/components/upload-video-card/upload-video-card.vue b/src/views/upload-video/components/upload-video-card/upload-video-card.vue index dae8fd40..0a0538be 100644 --- a/src/views/upload-video/components/upload-video-card/upload-video-card.vue +++ b/src/views/upload-video/components/upload-video-card/upload-video-card.vue @@ -11,6 +11,7 @@ import { copyVideoLink } from '@/utils/video-utils' const emits = defineEmits<{ (e: 'remove', uuid: string): void + (e: 'preview', videoItem: Props['videoItem']): void }>() const props = defineProps() @@ -54,6 +55,10 @@ const remove = (uuid: string) => { emits('remove', uuid) } +const handlePreview = () => { + emits('preview', props.videoItem) +} + onMounted(async () => { await initFilename() }) @@ -68,7 +73,12 @@ onMounted(async () => { uploaded: !videoItem.uploadStatus.uploading && videoItem.uploadStatus.progress === 100 }" > -
+
diff --git a/src/views/upload-video/components/video-preview/video-preview.vue b/src/views/upload-video/components/video-preview/video-preview.vue new file mode 100644 index 00000000..92008c38 --- /dev/null +++ b/src/views/upload-video/components/video-preview/video-preview.vue @@ -0,0 +1,38 @@ + + + + + diff --git a/src/views/upload-video/upload-video.vue b/src/views/upload-video/upload-video.vue index 4472b060..afb5f92c 100644 --- a/src/views/upload-video/upload-video.vue +++ b/src/views/upload-video/upload-video.vue @@ -3,9 +3,10 @@ import { computed, ref, watch } from 'vue' import { UploadVideoModel, ElementPlusSizeEnum, VideoHandleResult } from '@/common/model' import { store } from '@/stores' import { getOSName } from '@/utils' +import { generateUploadVideoObject } from './utils/generate' import GettingVideo from './components/getting-video/getting-video.vue' import UploadVideoCard from './components/upload-video-card/upload-video-card.vue' -import { generateUploadVideoObject } from './utils/generate' +import VideoPreview from './components/video-preview/video-preview.vue' const userConfigInfo = computed(() => store.getters.getUserConfigInfo).value const globalSettings = computed(() => store.getters.getGlobalSettings).value @@ -29,6 +30,11 @@ const remove = (uuid: string) => { store.dispatch('UPLOAD_VIDEO_LIST_REMOVE', uuid) } +const videoPreviewRef = ref | null>(null) +const handlePreview = (videoItem: UploadVideoModel) => { + videoPreviewRef.value?.handleOpen(videoItem) +} + const uploadImage = () => { // todo } @@ -68,7 +74,7 @@ watch( v-if="uploadVideoList.length && globalSettings!.elementPlusSize !== ElementPlusSizeEnum.small" >
- +
@@ -114,6 +120,7 @@ watch( + From 3fcb4eb647655ea8d6fc0173e0fabe1e1a29e51a Mon Sep 17 00:00:00 2001 From: lstmxx <740719284@qq.com> Date: Tue, 30 Sep 2025 18:09:21 +0800 Subject: [PATCH 5/8] feat: add single video upload --- src/auto-imports.d.ts | 1 + src/utils/upload-utils.ts | 47 +++++++++ src/utils/video-utils.ts | 21 ++++ .../getting-video/getting-video.vue | 14 +++ .../upload-video/hooks/use-upload-video.ts | 95 +++++++++++++++++++ src/views/upload-video/upload-video.vue | 44 +++++++-- 6 files changed, 212 insertions(+), 10 deletions(-) create mode 100644 src/views/upload-video/hooks/use-upload-video.ts diff --git a/src/auto-imports.d.ts b/src/auto-imports.d.ts index 001d94e0..cee8c010 100644 --- a/src/auto-imports.d.ts +++ b/src/auto-imports.d.ts @@ -18,5 +18,6 @@ declare global { const IEpPicture: typeof import('~icons/ep/picture')['default'] const IEpPostcard: typeof import('~icons/ep/postcard')['default'] const IEpSetting: typeof import('~icons/ep/setting')['default'] + const IEpSwitch: typeof import('~icons/ep/switch')['default'] const IEpUpload: typeof import('~icons/ep/upload')['default'] } diff --git a/src/utils/upload-utils.ts b/src/utils/upload-utils.ts index 47b2a453..f1a5a7c7 100644 --- a/src/utils/upload-utils.ts +++ b/src/utils/upload-utils.ts @@ -11,6 +11,7 @@ import { } from '@/common/api' import { PICX_UPLOAD_IMG_DESC, PICX_UPLOAD_VIDEO_DESC } from '@/common/constant' import i18n from '@/plugins/vue/i18n' +import router from '@/router' /** * 图片上传成功之后的处理 @@ -305,3 +306,49 @@ export function uploadVideoToGitHub( } }) } + +/** + * 校验用户配置信息 + * @param userConfigInfo 用户配置信息 + */ +async function validateConfig(userConfigInfo: UserConfigInfoModel) { + const { token, repo, selectedDir } = userConfigInfo + + if (!token) { + ElMessage.error({ message: i18n.global.t('upload_page.message1') }) + await router.push('/config') + return false + } + + if (!repo) { + ElMessage.error({ message: i18n.global.t('upload_page.message2') }) + await router.push('/config') + return false + } + + if (!selectedDir) { + ElMessage.error({ message: i18n.global.t('upload_page.message3') }) + await router.push('/config') + return false + } + + return true +} + +export async function beforeUpload( + userConfigInfo: UserConfigInfoModel, + fileList: T[] +) { + if (!validateConfig(userConfigInfo)) { + return [] + } + + const notYetUploadList = fileList.filter((x) => x.uploadStatus.progress === 0) + + if (notYetUploadList.length === 0) { + ElMessage.error({ message: i18n.global.t('upload_page.message4') }) + return [] + } + + return notYetUploadList +} diff --git a/src/utils/video-utils.ts b/src/utils/video-utils.ts index e10cb8c0..341449ea 100644 --- a/src/utils/video-utils.ts +++ b/src/utils/video-utils.ts @@ -170,3 +170,24 @@ export const copyVideoLink = (videoObj: UploadedVideoModel, autoCopy: boolean = ElMessage.error({ message: i18n.global.t('copy_fail_1') }) } } + +/** + * 批量复制视频链接 + * @param uploadedVideoList 视频对象列表 + * @param autoCopy + */ +export const batchCopyVideoLinks = ( + uploadedVideoList: UploadedVideoModel[], + autoCopy: boolean = false +) => { + if (uploadedVideoList?.length > 0) { + let linksTxt = '' + uploadedVideoList.forEach((video, index) => { + const link = generateVideoLink(video) + linksTxt += `${link}${index < uploadedVideoList.length - 1 ? '\n' : ''}` + }) + copyText(linksTxt, () => { + copyMessage(autoCopy) + }) + } +} diff --git a/src/views/upload-video/components/getting-video/getting-video.vue b/src/views/upload-video/components/getting-video/getting-video.vue index c590f40d..fdb5511f 100644 --- a/src/views/upload-video/components/getting-video/getting-video.vue +++ b/src/views/upload-video/components/getting-video/getting-video.vue @@ -1,5 +1,6 @@ From 02fe9af9c20d74e8d16aedc75ea5fc0915f4b710 Mon Sep 17 00:00:00 2001 From: lstmxx <740719284@qq.com> Date: Sat, 11 Oct 2025 11:27:57 +0800 Subject: [PATCH 8/8] feat: add batch upload video --- src/auto-imports.d.ts | 1 + src/common/constant/init.ts | 1 + src/utils/upload-utils.ts | 89 ++++++++++++++++++- .../upload-video/hooks/use-upload-video.ts | 20 ++--- 4 files changed, 99 insertions(+), 12 deletions(-) diff --git a/src/auto-imports.d.ts b/src/auto-imports.d.ts index 001d94e0..cee8c010 100644 --- a/src/auto-imports.d.ts +++ b/src/auto-imports.d.ts @@ -18,5 +18,6 @@ declare global { const IEpPicture: typeof import('~icons/ep/picture')['default'] const IEpPostcard: typeof import('~icons/ep/postcard')['default'] const IEpSetting: typeof import('~icons/ep/setting')['default'] + const IEpSwitch: typeof import('~icons/ep/switch')['default'] const IEpUpload: typeof import('~icons/ep/upload')['default'] } diff --git a/src/common/constant/init.ts b/src/common/constant/init.ts index 7bd68520..81f6cc69 100644 --- a/src/common/constant/init.ts +++ b/src/common/constant/init.ts @@ -6,6 +6,7 @@ export const GH_PAGES = 'gh-pages' export const PICX_UPLOAD_IMG_DESC = 'Upload image via PicX (https://github.com/XPoet/picx)' export const PICX_UPLOAD_IMGS_DESC = 'Upload images via PicX (https://github.com/XPoet/picx)' export const PICX_UPLOAD_VIDEO_DESC = 'Upload video via PicX (https://github.com/XPoet/picx)' +export const PICX_UPLOAD_VIDEOS_DESC = 'Upload videos via PicX (https://github.com/XPoet/picx)' export const PICX_DEL_IMG_DESC = 'Delete image via PicX (https://github.com/XPoet/picx)' export const PICX_INIT_SETTINGS_MSG = 'Init settings via PicX (https://github.com/XPoet/picx)' export const PICX_UPDATE_SETTINGS_MSG = 'Update settings via PicX (https://github.com/XPoet/picx)' diff --git a/src/utils/upload-utils.ts b/src/utils/upload-utils.ts index f1a5a7c7..1dfb5d7d 100644 --- a/src/utils/upload-utils.ts +++ b/src/utils/upload-utils.ts @@ -9,7 +9,11 @@ import { getFileBlob, getBranchInfo } from '@/common/api' -import { PICX_UPLOAD_IMG_DESC, PICX_UPLOAD_VIDEO_DESC } from '@/common/constant' +import { + PICX_UPLOAD_IMG_DESC, + PICX_UPLOAD_VIDEO_DESC, + PICX_UPLOAD_VIDEOS_DESC +} from '@/common/constant' import i18n from '@/plugins/vue/i18n' import router from '@/router' @@ -238,7 +242,7 @@ const videoUploadedHandle = ( store.dispatch('DIR_IMAGE_LIST_ADD_DIR', dir) // dirImageList 增加视频 - store.dispatch('DIR_IMAGE_LIST_ADD_IMAGE', uploadedVideo) + store.dispatch('DIR_IMAGE_LIST_ADD_VIDEO', uploadedVideo) } /** @@ -352,3 +356,84 @@ export async function beforeUpload { + const { branch, repo, selectedDir, owner } = userConfigInfo + + const blobs = [] + // eslint-disable-next-line no-restricted-syntax + for (const video of videos) { + video.uploadStatus.uploading = true + const tempBase64 = ( + video.base64.compressBase64 || + video.base64.watermarkBase64 || + video.base64.originalBase64 + ).split(',')[1] + // 上传图片文件,为仓库创建 blobs + const blobRes = await getFileBlob(tempBase64, owner, repo) + if (blobRes) { + blobs.push({ video, ...blobRes }) + } else { + video.uploadStatus.uploading = false + ElMessage.error(i18n.global.t('upload_page.tip_11', { name: video.filename.final })) + } + } + + // 获取 head,用于获取当前分支信息(根目录的 tree sha 以及 head commit sha) + const branchRes: any = await getBranchInfo(owner, repo, branch) + if (!branchRes) { + return Promise.resolve(false) + } + + const finalPath = selectedDir === '/' ? '' : `${selectedDir}/` + + // 创建 tree + const treeRes = await createTree( + owner, + repo, + blobs.map((x: any) => ({ + sha: x.sha, + path: `${finalPath}${x.video.filename.final}` + })), + branchRes + ) + if (!treeRes) { + return Promise.resolve(false) + } + + // 创建 commit 节点 + const commitRes: any = await createCommit( + owner, + repo, + treeRes, + branchRes, + PICX_UPLOAD_VIDEOS_DESC + ) + if (!commitRes) { + return Promise.resolve(false) + } + + // 将当前分支 ref 指向新创建的 commit + const refRes = await createRef(owner, repo, branch, commitRes.sha) + if (!refRes) { + return Promise.resolve(false) + } + + blobs.forEach((blob: any) => { + const name = blob.video.filename.final + videoUploadedHandle( + { name, sha: blob.sha, path: `${finalPath}${name}`, size: 0 }, + blob.video, + userConfigInfo + ) + }) + return Promise.resolve(true) +} diff --git a/src/views/upload-video/hooks/use-upload-video.ts b/src/views/upload-video/hooks/use-upload-video.ts index 3ecc7f55..7b3e44fe 100644 --- a/src/views/upload-video/hooks/use-upload-video.ts +++ b/src/views/upload-video/hooks/use-upload-video.ts @@ -1,7 +1,7 @@ import { computed, ref, Ref } from 'vue' import { UploadedVideoModel, UploadStatusEnum, UploadVideoModel } from '@/common/model' import { store } from '@/stores' -import { beforeUpload, uploadVideoToGitHub } from '@/utils/upload-utils' +import { beforeUpload, uploadVideosToGitHub, uploadVideoToGitHub } from '@/utils/upload-utils' import i18n from '@/plugins/vue/i18n' import { batchCopyVideoLinks, copyVideoLink } from '@/utils/video-utils' @@ -14,7 +14,7 @@ export const useUploadVideo = ( const userConfigInfo = computed(() => store.getters.getUserConfigInfo) const doUploadVideos = async (videoList: UploadVideoModel[]) => { - // 单张图片 + // 单个视频 if (videoList.length === 1) { if (await uploadVideoToGitHub(userConfigInfo.value, videoList[0])) { return UploadStatusEnum.uploaded @@ -22,12 +22,12 @@ export const useUploadVideo = ( return UploadStatusEnum.uploadFail } - // 多张图片 + // 多个视频 if (videoList.length > 1) { - // if (await uploadImagesToGitHub(userConfigInfo, imgList)) { - // } - return UploadStatusEnum.allUploaded - // return UploadStatusEnum.uploadFail + if (await uploadVideosToGitHub(userConfigInfo.value, videoList)) { + return UploadStatusEnum.allUploaded + } + return UploadStatusEnum.uploadFail } return UploadStatusEnum.uploadFail @@ -38,7 +38,7 @@ export const useUploadVideo = ( uploadedVideo: UploadedVideoModel[], isBatch: boolean = false ) => { - // 自动复制图片链接到系统剪贴板 + // 自动复制链接到系统剪贴板 if (isBatch) { batchCopyVideoLinks(uploadedVideo, true) } else { @@ -67,13 +67,13 @@ export const useUploadVideo = ( .map((x) => x.uploadedVideo!) switch (result) { - // 单张图片上传成功 + // 单视频上传成功 case UploadStatusEnum.uploaded: ElMessage.success({ message: i18n.global.t('upload_page.message5') }) await afterUploadSuccess(uploadedVideo) break - // 多张图片上传成功 + // 多视频上传成功 case UploadStatusEnum.allUploaded: ElMessage.success({ message: i18n.global.t('upload_page.message6') }) await afterUploadSuccess(uploadedVideo, true)