更新
This commit is contained in:
@@ -100,6 +100,7 @@ export interface OssCredentialsResponse {
|
||||
host?: string
|
||||
cdn_domain?: string
|
||||
key_prefix?: string
|
||||
object_key?: string
|
||||
max_size?: number
|
||||
duration?: number
|
||||
expired_time?: number
|
||||
@@ -111,8 +112,10 @@ export interface OssCredentialsResponse {
|
||||
}
|
||||
}
|
||||
|
||||
export type OssDirectUploadType = 'video' | 'voice' | 'desktop_package'
|
||||
|
||||
/** 申请 STS 临时凭证 */
|
||||
export function getOssCredentials(params: { type: 'video' }) {
|
||||
export function getOssCredentials(params: { type: OssDirectUploadType; name?: string }) {
|
||||
return request.post({
|
||||
url: '/upload/ossCredentials',
|
||||
params
|
||||
@@ -121,7 +124,7 @@ export function getOssCredentials(params: { type: 'video' }) {
|
||||
|
||||
/** 直传完成回执:写 file 表 + HEAD 校验 */
|
||||
export function confirmOssUpload(params: {
|
||||
type: 'video'
|
||||
type: OssDirectUploadType
|
||||
key: string
|
||||
name: string
|
||||
size: number
|
||||
|
||||
@@ -51,8 +51,10 @@ import useAppStore from '@/stores/modules/app'
|
||||
import useUserStore from '@/stores/modules/user'
|
||||
import feedback from '@/utils/feedback'
|
||||
import {
|
||||
DirectUploadApiError,
|
||||
DirectUploadFallbackError,
|
||||
uploadVideoDirectToCos
|
||||
uploadDirectToCos,
|
||||
type DirectUploadType
|
||||
} from '@/utils/oss-direct-upload'
|
||||
|
||||
export default defineComponent({
|
||||
@@ -83,7 +85,7 @@ export default defineComponent({
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 视频直传到 OSS(绕开服务器中转,仅 type=video 生效)
|
||||
// 直传到对象存储,绕开服务器中转
|
||||
direct: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
@@ -102,8 +104,10 @@ export default defineComponent({
|
||||
const visible = ref(false)
|
||||
const fileList = ref<any[]>([])
|
||||
|
||||
// 仅 video/voice + direct 时才接管 http-request
|
||||
const useDirect = computed(() => props.direct && ['video', 'voice'].includes(props.type))
|
||||
const directTypes: DirectUploadType[] = ['video', 'voice', 'desktop_package']
|
||||
const useDirect = computed(
|
||||
() => props.direct && directTypes.includes(props.type as DirectUploadType)
|
||||
)
|
||||
|
||||
const handleProgress = () => {
|
||||
visible.value = true
|
||||
@@ -131,7 +135,10 @@ export default defineComponent({
|
||||
fileList.value = []
|
||||
emit('allSuccess')
|
||||
}
|
||||
feedback.msgError(`${file.name}文件上传失败`)
|
||||
if (!(event instanceof DirectUploadApiError)) {
|
||||
const message = event instanceof Error ? event.message : ''
|
||||
feedback.msgError(message || `${file.name}文件上传失败`)
|
||||
}
|
||||
uploadRefs.value?.abort(file)
|
||||
visible.value = false
|
||||
emit('change', file)
|
||||
@@ -153,18 +160,20 @@ export default defineComponent({
|
||||
return '.wmv,.avi,.mpg,.mpeg,.3gp,.mov,.mp4,.flv,.rmvb,.mkv'
|
||||
case 'voice':
|
||||
return '.mp3,.wav,.wma,.m4a,.aac,.amr'
|
||||
case 'desktop_package':
|
||||
return '.exe,.zip'
|
||||
default:
|
||||
return '*'
|
||||
}
|
||||
})
|
||||
|
||||
// 走 COS 直传:成功时模拟老接口的响应 envelope,失败/降级时回到默认 XHR
|
||||
// 走 COS 直传:成功时模拟老接口的响应 envelope
|
||||
const httpRequest = async (options: UploadRequestOptions) => {
|
||||
visible.value = true
|
||||
try {
|
||||
const data = await uploadVideoDirectToCos({
|
||||
const data = await uploadDirectToCos({
|
||||
file: options.file,
|
||||
type: props.type as any,
|
||||
type: props.type as DirectUploadType,
|
||||
cid: Number((options.data as any)?.cid ?? 0),
|
||||
onProgress(info) {
|
||||
// 触发 ElUpload 内部进度(保持与默认上传一致的体验)
|
||||
@@ -178,6 +187,12 @@ export default defineComponent({
|
||||
;(options as any).onSuccess?.({ code: RequestCodeEnum.SUCCESS, msg: 'ok', data })
|
||||
} catch (err: any) {
|
||||
if (err instanceof DirectUploadFallbackError) {
|
||||
if (props.type === 'desktop_package') {
|
||||
;(options as any).onError?.(
|
||||
new Error('当前未启用腾讯云 COS,安装包无法直传,请配置 COS 后重试')
|
||||
)
|
||||
return
|
||||
}
|
||||
feedback.msgWarning('当前存储不支持直传,已切换为普通上传')
|
||||
await defaultXhrUpload(options)
|
||||
return
|
||||
|
||||
@@ -3,10 +3,11 @@ import COS from 'cos-js-sdk-v5'
|
||||
import {
|
||||
confirmOssUpload,
|
||||
getOssCredentials,
|
||||
type OssCredentialsResponse
|
||||
type OssCredentialsResponse,
|
||||
type OssDirectUploadType
|
||||
} from '@/api/file'
|
||||
|
||||
export type DirectUploadType = 'video'
|
||||
export type DirectUploadType = OssDirectUploadType
|
||||
|
||||
export interface DirectUploadProgress {
|
||||
/** 0-100 */
|
||||
@@ -37,8 +38,17 @@ export interface DirectUploadOptions {
|
||||
const SLICE_SIZE = 5 * 1024 * 1024 // 5MB
|
||||
const ASYNC_LIMIT = 3
|
||||
|
||||
async function callDirectUploadApi<T>(request: () => Promise<T>): Promise<T> {
|
||||
try {
|
||||
return await request()
|
||||
} catch (error) {
|
||||
// request 拦截器已经展示过接口/网络错误,上传组件只负责收口失败状态
|
||||
throw new DirectUploadApiError(error)
|
||||
}
|
||||
}
|
||||
|
||||
function buildKey(prefix: string, file: File): string {
|
||||
const ext = (file.name.split('.').pop() || 'mp4').toLowerCase()
|
||||
const ext = (file.name.split('.').pop() || 'bin').toLowerCase()
|
||||
const ts = Date.now()
|
||||
const rand = Math.random().toString(36).slice(2, 10)
|
||||
return `${prefix}${ts}-${rand}.${ext}`
|
||||
@@ -48,8 +58,13 @@ function buildKey(prefix: string, file: File): string {
|
||||
* 直传到腾讯云 COS(含 STS 凭证申请、分片上传、回执)
|
||||
* 不支持降级 / fallback=true 时抛错,由调用方决定走老链路。
|
||||
*/
|
||||
export async function uploadVideoDirectToCos(options: DirectUploadOptions): Promise<DirectUploadResult> {
|
||||
const credentials: OssCredentialsResponse = await getOssCredentials({ type: options.type })
|
||||
export async function uploadDirectToCos(options: DirectUploadOptions): Promise<DirectUploadResult> {
|
||||
const credentials: OssCredentialsResponse = await callDirectUploadApi(() =>
|
||||
getOssCredentials({
|
||||
type: options.type,
|
||||
name: options.file.name
|
||||
})
|
||||
)
|
||||
|
||||
if (credentials.fallback) {
|
||||
const handled = options.onFallback?.(credentials.provider) ?? false
|
||||
@@ -66,7 +81,7 @@ export async function uploadVideoDirectToCos(options: DirectUploadOptions): Prom
|
||||
|
||||
if (credentials.max_size && options.file.size > credentials.max_size) {
|
||||
const mb = Math.round(credentials.max_size / 1024 / 1024)
|
||||
throw new Error(`视频体积超出上限(${mb}MB)`)
|
||||
throw new Error(`文件体积超出上限(${mb}MB)`)
|
||||
}
|
||||
|
||||
const cred = credentials.credentials
|
||||
@@ -85,7 +100,7 @@ export async function uploadVideoDirectToCos(options: DirectUploadOptions): Prom
|
||||
}
|
||||
})
|
||||
|
||||
const key = buildKey(credentials.key_prefix, options.file)
|
||||
const key = credentials.object_key || buildKey(credentials.key_prefix, options.file)
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
cos.uploadFile(
|
||||
@@ -117,14 +132,16 @@ export async function uploadVideoDirectToCos(options: DirectUploadOptions): Prom
|
||||
)
|
||||
})
|
||||
|
||||
const confirmed = await confirmOssUpload({
|
||||
type: options.type,
|
||||
key,
|
||||
name: options.file.name,
|
||||
size: options.file.size,
|
||||
content_type: options.file.type || '',
|
||||
cid: options.cid ?? 0
|
||||
})
|
||||
const confirmed = await callDirectUploadApi(() =>
|
||||
confirmOssUpload({
|
||||
type: options.type,
|
||||
key,
|
||||
name: options.file.name,
|
||||
size: options.file.size,
|
||||
content_type: options.file.type || '',
|
||||
cid: options.cid ?? 0
|
||||
})
|
||||
)
|
||||
|
||||
options.onProgress?.({ percent: 100, loaded: options.file.size, total: options.file.size, speed: 0 })
|
||||
|
||||
@@ -140,3 +157,14 @@ export class DirectUploadFallbackError extends Error {
|
||||
this.provider = provider
|
||||
}
|
||||
}
|
||||
|
||||
/** 请求层已经展示过错误,避免 ElUpload 再弹一条通用失败提示。 */
|
||||
export class DirectUploadApiError extends Error {
|
||||
readonly originalError: unknown
|
||||
|
||||
constructor(error: unknown) {
|
||||
super('')
|
||||
this.name = 'DirectUploadApiError'
|
||||
this.originalError = error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,8 @@
|
||||
<code>一键打包</code>
|
||||
产物一致的安装包,并填入打包目录中的 SHA-256。Windows 推荐使用
|
||||
Setup.exe,用户点击“立即更新”后会自动安装并重启;macOS 继续使用 ZIP。
|
||||
安装包通常超过 200MB,优先传到对象存储 / CDN 后粘贴地址。
|
||||
安装包通常超过 200MB,本页上传按钮会直传到已配置的腾讯云 COS;也可以
|
||||
自行上传到其他对象存储 / CDN 后粘贴地址。
|
||||
</div>
|
||||
</el-alert>
|
||||
<div class="text-xl font-medium mb-[20px]">升级策略</div>
|
||||
@@ -125,7 +126,9 @@
|
||||
<el-form-item label="上传安装包">
|
||||
<div>
|
||||
<upload
|
||||
type="file"
|
||||
v-perms="['setting.desktop_workstation/setConfig']"
|
||||
type="desktop_package"
|
||||
direct
|
||||
:limit="1"
|
||||
:multiple="false"
|
||||
:show-progress="true"
|
||||
@@ -136,8 +139,8 @@
|
||||
<el-button type="primary" plain>选择安装包并上传</el-button>
|
||||
</upload>
|
||||
<div class="form-tips">
|
||||
仅建议上传较小的包。大文件请先传到对象存储,再把地址和 SHA-256
|
||||
填到上方。Windows 自动安装程序必须使用 HTTPS 地址并开启证书校验。
|
||||
安装包将分片直传腾讯云 COS,不经过业务服务器(支持 EXE / ZIP,最大
|
||||
2GB)。Windows 自动安装程序必须使用 HTTPS 地址并开启证书校验。
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
Reference in New Issue
Block a user