feat: 接入腾讯云cos上传
This commit is contained in:
parent
3e3525aee1
commit
dd9000bf48
@ -28,6 +28,7 @@
|
|||||||
"axios": "^1.7.2",
|
"axios": "^1.7.2",
|
||||||
"chart.js": "^4.4.3",
|
"chart.js": "^4.4.3",
|
||||||
"compare-versions": "^6.1.1",
|
"compare-versions": "^6.1.1",
|
||||||
|
"cos-js-sdk-v5": "^1.10.1",
|
||||||
"dayjs": "^1.11.12",
|
"dayjs": "^1.11.12",
|
||||||
"echarts": "^5.5.1",
|
"echarts": "^5.5.1",
|
||||||
"element-plus": "^2.7.8",
|
"element-plus": "^2.7.8",
|
||||||
|
|||||||
23
pnpm-lock.yaml
generated
23
pnpm-lock.yaml
generated
@ -47,6 +47,9 @@ importers:
|
|||||||
compare-versions:
|
compare-versions:
|
||||||
specifier: ^6.1.1
|
specifier: ^6.1.1
|
||||||
version: 6.1.1
|
version: 6.1.1
|
||||||
|
cos-js-sdk-v5:
|
||||||
|
specifier: ^1.10.1
|
||||||
|
version: 1.10.1
|
||||||
dayjs:
|
dayjs:
|
||||||
specifier: ^1.11.12
|
specifier: ^1.11.12
|
||||||
version: 1.11.13
|
version: 1.11.13
|
||||||
@ -1517,6 +1520,9 @@ packages:
|
|||||||
core-js@3.41.0:
|
core-js@3.41.0:
|
||||||
resolution: {integrity: sha512-SJ4/EHwS36QMJd6h/Rg+GyR4A5xE0FSI3eZ+iBVpfqf1x0eTSg1smWLHrA+2jQThZSh97fmSgFSU8B61nxosxA==}
|
resolution: {integrity: sha512-SJ4/EHwS36QMJd6h/Rg+GyR4A5xE0FSI3eZ+iBVpfqf1x0eTSg1smWLHrA+2jQThZSh97fmSgFSU8B61nxosxA==}
|
||||||
|
|
||||||
|
cos-js-sdk-v5@1.10.1:
|
||||||
|
resolution: {integrity: sha512-a4SRfCY5g6Z35C7OWe9te/S1zk77rVQzfpvZ33gmTdJQzKxbNbEG7Aw/v453XwVMsQB352FIf7KRMm5Ya/wlZQ==}
|
||||||
|
|
||||||
cose-base@1.0.3:
|
cose-base@1.0.3:
|
||||||
resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==}
|
resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==}
|
||||||
|
|
||||||
@ -2098,6 +2104,10 @@ packages:
|
|||||||
fast-uri@3.0.6:
|
fast-uri@3.0.6:
|
||||||
resolution: {integrity: sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==}
|
resolution: {integrity: sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==}
|
||||||
|
|
||||||
|
fast-xml-parser@4.5.0:
|
||||||
|
resolution: {integrity: sha512-/PlTQCI96+fZMAOLMZK4CWG1ItCbfZ/0jx7UIJFChPNrx7tcEgerUgWbeieCM9MfHInUDyK8DWYZ+YrywDJuTg==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
fastq@1.19.1:
|
fastq@1.19.1:
|
||||||
resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==}
|
resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==}
|
||||||
|
|
||||||
@ -3347,6 +3357,9 @@ packages:
|
|||||||
strip-literal@2.1.1:
|
strip-literal@2.1.1:
|
||||||
resolution: {integrity: sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==}
|
resolution: {integrity: sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==}
|
||||||
|
|
||||||
|
strnum@1.1.2:
|
||||||
|
resolution: {integrity: sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==}
|
||||||
|
|
||||||
stylis@4.3.6:
|
stylis@4.3.6:
|
||||||
resolution: {integrity: sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==}
|
resolution: {integrity: sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==}
|
||||||
|
|
||||||
@ -5087,6 +5100,10 @@ snapshots:
|
|||||||
|
|
||||||
core-js@3.41.0: {}
|
core-js@3.41.0: {}
|
||||||
|
|
||||||
|
cos-js-sdk-v5@1.10.1:
|
||||||
|
dependencies:
|
||||||
|
fast-xml-parser: 4.5.0
|
||||||
|
|
||||||
cose-base@1.0.3:
|
cose-base@1.0.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
layout-base: 1.0.2
|
layout-base: 1.0.2
|
||||||
@ -5806,6 +5823,10 @@ snapshots:
|
|||||||
|
|
||||||
fast-uri@3.0.6: {}
|
fast-uri@3.0.6: {}
|
||||||
|
|
||||||
|
fast-xml-parser@4.5.0:
|
||||||
|
dependencies:
|
||||||
|
strnum: 1.1.2
|
||||||
|
|
||||||
fastq@1.19.1:
|
fastq@1.19.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
reusify: 1.1.0
|
reusify: 1.1.0
|
||||||
@ -7172,6 +7193,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
js-tokens: 9.0.1
|
js-tokens: 9.0.1
|
||||||
|
|
||||||
|
strnum@1.1.2: {}
|
||||||
|
|
||||||
stylis@4.3.6: {}
|
stylis@4.3.6: {}
|
||||||
|
|
||||||
supports-color@7.2.0:
|
supports-color@7.2.0:
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
allowBuilds:
|
allowBuilds:
|
||||||
'@parcel/watcher': true
|
'@parcel/watcher': true
|
||||||
core-js: false
|
core-js: false
|
||||||
|
cos-js-sdk-v5: false
|
||||||
es5-ext: false
|
es5-ext: false
|
||||||
esbuild: true
|
esbuild: true
|
||||||
vue-demi: true
|
vue-demi: true
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import str from '@/utils/str'
|
import str from '@/utils/str'
|
||||||
import { http } from './http'
|
import { http } from './http'
|
||||||
import { get } from './request'
|
import { get } from './request'
|
||||||
|
import { toCosUploadResponse, uploadFileToCos } from '@/utils/cosUpload'
|
||||||
|
|
||||||
export type Option = {
|
export type Option = {
|
||||||
value: any
|
value: any
|
||||||
@ -199,16 +200,19 @@ const globalApi = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// 上传文件
|
// 上传文件
|
||||||
uploadFile: (data: any) =>
|
uploadFile: async (data: any) => {
|
||||||
http.request({
|
const file = data instanceof FormData ? data.get('file') : data?.file
|
||||||
url: '/sysFileInfo/tenUploadAll',
|
const fileType =
|
||||||
method: 'post',
|
Number(data instanceof FormData ? data.get('fileType') : data?.fileType) === 1 ? 1 : 2
|
||||||
timeout: 15000,
|
if (!(file instanceof Blob)) {
|
||||||
headers: {
|
throw new Error('请选择要上传的文件')
|
||||||
'content-type': 'multipart/form-data',
|
}
|
||||||
},
|
const result = await uploadFileToCos(file, {
|
||||||
data,
|
fileType,
|
||||||
}),
|
fileName: file instanceof File ? file.name : undefined,
|
||||||
|
})
|
||||||
|
return toCosUploadResponse(result)
|
||||||
|
},
|
||||||
|
|
||||||
// 上报课堂监控截图
|
// 上报课堂监控截图
|
||||||
reportSnapshot: (data: { sessionId: string; url: string; userId: string }) =>
|
reportSnapshot: (data: { sessionId: string; url: string; userId: string }) =>
|
||||||
|
|||||||
@ -103,7 +103,11 @@ instance.interceptors.response.use(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 文件流不拦截
|
// 文件流不拦截
|
||||||
if (response.headers['content-type']?.indexOf('octet-stream') !== -1 || code == 200)
|
if (
|
||||||
|
response.headers['content-type']?.indexOf('octet-stream') !== -1 ||
|
||||||
|
code == 200 ||
|
||||||
|
code == 0
|
||||||
|
)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
return Promise.reject(buildError(response))
|
return Promise.reject(buildError(response))
|
||||||
|
|||||||
@ -167,7 +167,7 @@ function handleDel(url: string) {
|
|||||||
<el-upload
|
<el-upload
|
||||||
v-if="!reportFlag && canAddMore"
|
v-if="!reportFlag && canAddMore"
|
||||||
class="start-btn"
|
class="start-btn"
|
||||||
action="/api/main/sysFileInfo/tenUploadAll"
|
action="#"
|
||||||
:data="{ type: 1 }"
|
:data="{ type: 1 }"
|
||||||
:headers="{
|
:headers="{
|
||||||
Authorization: token,
|
Authorization: token,
|
||||||
|
|||||||
@ -66,7 +66,7 @@ function handleShow(url: string, isTeacher = false) {
|
|||||||
:style="{
|
:style="{
|
||||||
'background-image': `url(${OSS_URL}/urm/homeWork/add-image-btn.png)`,
|
'background-image': `url(${OSS_URL}/urm/homeWork/add-image-btn.png)`,
|
||||||
}"
|
}"
|
||||||
action="/api/main/sysFileInfo/tenUploadAll"
|
action="#"
|
||||||
:data="{ type: 1 }"
|
:data="{ type: 1 }"
|
||||||
:headers="{
|
:headers="{
|
||||||
Authorization: token,
|
Authorization: token,
|
||||||
@ -100,7 +100,7 @@ function handleShow(url: string, isTeacher = false) {
|
|||||||
<!-- 答题 -->
|
<!-- 答题 -->
|
||||||
<el-upload
|
<el-upload
|
||||||
class="upload-demo"
|
class="upload-demo"
|
||||||
action="/api/main/sysFileInfo/tenUploadAll"
|
action="#"
|
||||||
:data="{ type: 1 }"
|
:data="{ type: 1 }"
|
||||||
:headers="{
|
:headers="{
|
||||||
Authorization: token,
|
Authorization: token,
|
||||||
|
|||||||
@ -3,8 +3,8 @@ import { storeToRefs } from 'pinia'
|
|||||||
import globalApi from '@/api/global'
|
import globalApi from '@/api/global'
|
||||||
import { userStore } from '@/stores'
|
import { userStore } from '@/stores'
|
||||||
import { $XXL, getNativeApi } from '@/utils'
|
import { $XXL, getNativeApi } from '@/utils'
|
||||||
import sass from '@/utils/sass'
|
|
||||||
import { useTips } from './useTips'
|
import { useTips } from './useTips'
|
||||||
|
import { uploadFileToCos, uploadNativeFilePathToCos } from '@/utils/cosUpload'
|
||||||
|
|
||||||
let monitorTimer: ReturnType<typeof setInterval> | null = null
|
let monitorTimer: ReturnType<typeof setInterval> | null = null
|
||||||
let monitorSessionId = ''
|
let monitorSessionId = ''
|
||||||
@ -76,7 +76,6 @@ const parseNativeScreenshotPath = (raw: unknown): string => {
|
|||||||
|
|
||||||
const uploadNativeScreenshot = async () => {
|
const uploadNativeScreenshot = async () => {
|
||||||
try {
|
try {
|
||||||
const { token } = storeToRefs(userStore())
|
|
||||||
const raw = $XXL.getScreenshot() || {}
|
const raw = $XXL.getScreenshot() || {}
|
||||||
monitorLog('native getScreenshot result', raw)
|
monitorLog('native getScreenshot result', raw)
|
||||||
const filePath = parseNativeScreenshotPath(raw)
|
const filePath = parseNativeScreenshotPath(raw)
|
||||||
@ -86,14 +85,8 @@ const uploadNativeScreenshot = async () => {
|
|||||||
}
|
}
|
||||||
monitorLog('native screenshot path', filePath)
|
monitorLog('native screenshot path', filePath)
|
||||||
|
|
||||||
const res = await $XXL.uploadFile({
|
const res = await uploadNativeFilePathToCos(filePath, config => $XXL.uploadFile(config), {
|
||||||
url: `${import.meta.env.VITE_SERVER_URL}/sysFileInfo/tenUploadAll`,
|
fileType: 1,
|
||||||
header: {
|
|
||||||
Authorization: token.value,
|
|
||||||
'X-Tenant-Id': sass.platform.tenantId,
|
|
||||||
},
|
|
||||||
filePath,
|
|
||||||
fileKey: 'file',
|
|
||||||
})
|
})
|
||||||
const url = normalizeUploadPath(res)
|
const url = normalizeUploadPath(res)
|
||||||
monitorLog('native upload result', { raw: res, url })
|
monitorLog('native upload result', { raw: res, url })
|
||||||
@ -234,9 +227,7 @@ const uploadWebScreenshot = async () => {
|
|||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
const formData = new FormData()
|
const res = await uploadFileToCos(file, { fileType: 1 })
|
||||||
formData.append('file', file)
|
|
||||||
const { data: res } = await globalApi.uploadFile(formData)
|
|
||||||
const url = normalizeUploadPath(res)
|
const url = normalizeUploadPath(res)
|
||||||
monitorLog('web upload result', { raw: res, url })
|
monitorLog('web upload result', { raw: res, url })
|
||||||
return url
|
return url
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import {
|
|||||||
resolveMonitorWsType,
|
resolveMonitorWsType,
|
||||||
stopClassMonitor,
|
stopClassMonitor,
|
||||||
} from './useClassMonitor'
|
} from './useClassMonitor'
|
||||||
|
import { uploadNativeFilePathToCos } from '@/utils/cosUpload'
|
||||||
|
|
||||||
let SOCKET: UseWebSocketReturn<any>
|
let SOCKET: UseWebSocketReturn<any>
|
||||||
|
|
||||||
@ -192,13 +193,8 @@ export const useSocket = () => {
|
|||||||
simSerialNumber: SN,
|
simSerialNumber: SN,
|
||||||
msg: '',
|
msg: '',
|
||||||
})
|
})
|
||||||
const res = await $XXL.uploadFile({
|
const res = await uploadNativeFilePathToCos(filePath, config => $XXL.uploadFile(config), {
|
||||||
url: `${import.meta.env.VITE_SERVER_URL}/sysFileInfo/tenUploadAll`,
|
fileType: 1,
|
||||||
header: {
|
|
||||||
Authorization: token,
|
|
||||||
},
|
|
||||||
filePath,
|
|
||||||
fileKey: 'file',
|
|
||||||
})
|
})
|
||||||
sendMsg({
|
sendMsg({
|
||||||
type: 'desktopScreenshotImage',
|
type: 'desktopScreenshotImage',
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { userStore } from '@/stores'
|
|||||||
import { XMessage } from '@/hooks'
|
import { XMessage } from '@/hooks'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import sass from '@/utils/sass'
|
import sass from '@/utils/sass'
|
||||||
|
import { cosUploadRequest } from '@/utils/cosUpload'
|
||||||
|
|
||||||
const { token } = userStore()
|
const { token } = userStore()
|
||||||
const OSS_URL = import.meta.env.VITE_OSS_URL
|
const OSS_URL = import.meta.env.VITE_OSS_URL
|
||||||
@ -122,7 +123,7 @@ onActivated(() => {
|
|||||||
<div v-else class="upload-btn">
|
<div v-else class="upload-btn">
|
||||||
<el-upload
|
<el-upload
|
||||||
class="add-image-btn pointer"
|
class="add-image-btn pointer"
|
||||||
action="/api/main/sysFileInfo/tenUploadAll"
|
action="#"
|
||||||
:data="{ type: 1 }"
|
:data="{ type: 1 }"
|
||||||
:headers="{
|
:headers="{
|
||||||
Authorization: token,
|
Authorization: token,
|
||||||
@ -130,6 +131,7 @@ onActivated(() => {
|
|||||||
}"
|
}"
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
:before-upload="handleBeforeUpload"
|
:before-upload="handleBeforeUpload"
|
||||||
|
:http-request="cosUploadRequest"
|
||||||
:on-success="handleUploadSuccess"
|
:on-success="handleUploadSuccess"
|
||||||
:show-file-list="false"
|
:show-file-list="false"
|
||||||
:disabled="uploadLoading"
|
:disabled="uploadLoading"
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import { useRoute, useRouter, onBeforeRouteLeave } from 'vue-router'
|
|||||||
|
|
||||||
import { $XXL, getDurationText } from '@/utils'
|
import { $XXL, getDurationText } from '@/utils'
|
||||||
|
|
||||||
import { userStore, groupPaperStore } from '@/stores'
|
import { groupPaperStore } from '@/stores'
|
||||||
import {
|
import {
|
||||||
resource_type_ai,
|
resource_type_ai,
|
||||||
resource_type_analogy,
|
resource_type_analogy,
|
||||||
@ -25,8 +25,8 @@ import type { IQuestion } from '@/types/question'
|
|||||||
import { globalBack } from '@/router'
|
import { globalBack } from '@/router'
|
||||||
import { dialog } from '@/components/mj/mj-dialog'
|
import { dialog } from '@/components/mj/mj-dialog'
|
||||||
import hud from '@/utils/hud'
|
import hud from '@/utils/hud'
|
||||||
import sass from '@/utils/sass'
|
|
||||||
import homeworkApi, { saveRecordAnswer, submitExam } from '@/api/subject/homework'
|
import homeworkApi, { saveRecordAnswer, submitExam } from '@/api/subject/homework'
|
||||||
|
import { uploadNativeFilePathToCos } from '@/utils/cosUpload'
|
||||||
|
|
||||||
const emit = defineEmits(['urgeSubmit'])
|
const emit = defineEmits(['urgeSubmit'])
|
||||||
|
|
||||||
@ -34,7 +34,6 @@ defineOptions({
|
|||||||
name: 'DoWork',
|
name: 'DoWork',
|
||||||
})
|
})
|
||||||
|
|
||||||
const { token } = userStore()
|
|
||||||
const { groupPaperParams } = toRefs(groupPaperStore())
|
const { groupPaperParams } = toRefs(groupPaperStore())
|
||||||
|
|
||||||
const resourceTypeConfig: Record<string, string> = {
|
const resourceTypeConfig: Record<string, string> = {
|
||||||
@ -247,15 +246,7 @@ async function uploadFile(options: anyObj) {
|
|||||||
if (options.isAndroid) {
|
if (options.isAndroid) {
|
||||||
if (options.submitAnswerPicOriginal) {
|
if (options.submitAnswerPicOriginal) {
|
||||||
const res = await withUploadTimeout(
|
const res = await withUploadTimeout(
|
||||||
$XXL.uploadFile({
|
uploadNativeFilePathToCos(options.file, config => $XXL.uploadFile(config)),
|
||||||
url: `${import.meta.env.VITE_SERVER_URL}/sysFileInfo/tenUploadAll`,
|
|
||||||
header: {
|
|
||||||
Authorization: token,
|
|
||||||
'X-Tenant-Id': sass.platform.tenantId,
|
|
||||||
},
|
|
||||||
filePath: options.file,
|
|
||||||
fileKey: 'file',
|
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
path = extractUploadPath(res)
|
path = extractUploadPath(res)
|
||||||
}
|
}
|
||||||
@ -986,7 +977,7 @@ defineExpose({
|
|||||||
$filter: brightness(0) saturate(0) invert(0) sepia(0) hue-rotate(30deg);
|
$filter: brightness(0) saturate(0) invert(0) sepia(0) hue-rotate(30deg);
|
||||||
|
|
||||||
.dowork {
|
.dowork {
|
||||||
// background: rgb(163, 175, 199);
|
background: #fff;
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { XMessage, debounce } from '@/hooks'
|
|||||||
import homeWorkApi from '@/api/system/homeWork'
|
import homeWorkApi from '@/api/system/homeWork'
|
||||||
import LoadIcon from '@/page/system/mailbox/components/LoadIcon.vue'
|
import LoadIcon from '@/page/system/mailbox/components/LoadIcon.vue'
|
||||||
import sass from '@/utils/sass'
|
import sass from '@/utils/sass'
|
||||||
|
import { cosUploadRequest } from '@/utils/cosUpload'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { token } = userStore()
|
const { token } = userStore()
|
||||||
@ -143,7 +144,7 @@ onMounted(async () => {
|
|||||||
<div>
|
<div>
|
||||||
<el-upload
|
<el-upload
|
||||||
class="pointer"
|
class="pointer"
|
||||||
action="/api/main/sysFileInfo/tenUploadAll"
|
action="#"
|
||||||
:data="{ type: 1 }"
|
:data="{ type: 1 }"
|
||||||
:headers="{
|
:headers="{
|
||||||
Authorization: token,
|
Authorization: token,
|
||||||
@ -151,6 +152,7 @@ onMounted(async () => {
|
|||||||
}"
|
}"
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
:before-upload="handleBeforeUpload"
|
:before-upload="handleBeforeUpload"
|
||||||
|
:http-request="cosUploadRequest"
|
||||||
:on-success="handleUploadSuccess"
|
:on-success="handleUploadSuccess"
|
||||||
:on-error="handleUploadError"
|
:on-error="handleUploadError"
|
||||||
:show-file-list="false"
|
:show-file-list="false"
|
||||||
|
|||||||
@ -7,10 +7,10 @@ import dayjs from 'dayjs'
|
|||||||
import { useRoute, useRouter, onBeforeRouteLeave } from 'vue-router'
|
import { useRoute, useRouter, onBeforeRouteLeave } from 'vue-router'
|
||||||
import { $XXL, getDurationText } from '@/utils'
|
import { $XXL, getDurationText } from '@/utils'
|
||||||
import { loading } from '@/components/mj/mj-loading'
|
import { loading } from '@/components/mj/mj-loading'
|
||||||
import { userStore } from '@/stores'
|
|
||||||
import type { IQuestion } from '@/types/question'
|
import type { IQuestion } from '@/types/question'
|
||||||
import { globalBack } from '@/router'
|
import { globalBack } from '@/router'
|
||||||
import { dialog } from '@/components/mj/mj-dialog'
|
import { dialog } from '@/components/mj/mj-dialog'
|
||||||
|
import { uploadNativeFilePathToCos } from '@/utils/cosUpload'
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'HomeWorkDoWork',
|
name: 'HomeWorkDoWork',
|
||||||
})
|
})
|
||||||
@ -21,8 +21,6 @@ type PaperInfo = {
|
|||||||
currentTitleId: string
|
currentTitleId: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const { token } = userStore()
|
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
@ -123,14 +121,7 @@ async function uploadFile(options: anyObj) {
|
|||||||
let path = ''
|
let path = ''
|
||||||
if (options.isAndroid) {
|
if (options.isAndroid) {
|
||||||
if (options.submitAnswerPicOriginal) {
|
if (options.submitAnswerPicOriginal) {
|
||||||
const res = await $XXL.uploadFile({
|
const res = await uploadNativeFilePathToCos(options.file, config => $XXL.uploadFile(config))
|
||||||
url: `${import.meta.env.VITE_SERVER_URL}/sysFileInfo/tenUploadAll`,
|
|
||||||
header: {
|
|
||||||
Authorization: token,
|
|
||||||
},
|
|
||||||
filePath: options.file,
|
|
||||||
fileKey: 'file',
|
|
||||||
})
|
|
||||||
path = res.url
|
path = res.url
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { XMessage, debounce } from '@/hooks'
|
|||||||
import homeWorkApi from '@/api/system/homeWork'
|
import homeWorkApi from '@/api/system/homeWork'
|
||||||
import LoadIcon from '@/page/system/mailbox/components/LoadIcon.vue'
|
import LoadIcon from '@/page/system/mailbox/components/LoadIcon.vue'
|
||||||
import sass from '@/utils/sass'
|
import sass from '@/utils/sass'
|
||||||
|
import { cosUploadRequest } from '@/utils/cosUpload'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { token } = userStore()
|
const { token } = userStore()
|
||||||
@ -143,7 +144,7 @@ onMounted(async () => {
|
|||||||
<div>
|
<div>
|
||||||
<el-upload
|
<el-upload
|
||||||
class="pointer"
|
class="pointer"
|
||||||
action="/api/main/sysFileInfo/tenUploadAll"
|
action="#"
|
||||||
:data="{ type: 1 }"
|
:data="{ type: 1 }"
|
||||||
:headers="{
|
:headers="{
|
||||||
Authorization: token,
|
Authorization: token,
|
||||||
@ -151,6 +152,7 @@ onMounted(async () => {
|
|||||||
}"
|
}"
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
:before-upload="handleBeforeUpload"
|
:before-upload="handleBeforeUpload"
|
||||||
|
:http-request="cosUploadRequest"
|
||||||
:on-success="handleUploadSuccess"
|
:on-success="handleUploadSuccess"
|
||||||
:on-error="handleUploadError"
|
:on-error="handleUploadError"
|
||||||
:show-file-list="false"
|
:show-file-list="false"
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import { compareVersions } from 'compare-versions'
|
|||||||
import { userStore } from '@/stores'
|
import { userStore } from '@/stores'
|
||||||
import VConsole from 'vconsole'
|
import VConsole from 'vconsole'
|
||||||
import { XMessage } from '@/hooks'
|
import { XMessage } from '@/hooks'
|
||||||
|
import { uploadNativeFilePathToCos } from '@/utils/cosUpload'
|
||||||
import hud from '@/utils/hud'
|
import hud from '@/utils/hud'
|
||||||
|
|
||||||
interface App {
|
interface App {
|
||||||
@ -169,17 +170,11 @@ export const globalStore = defineStore('global', () => {
|
|||||||
})
|
})
|
||||||
if ((res?.appInfoList || []).length === 0) return
|
if ((res?.appInfoList || []).length === 0) return
|
||||||
const unApps = res.appInfoList
|
const unApps = res.appInfoList
|
||||||
const { token } = userStore()
|
|
||||||
for (const i of unApps) {
|
for (const i of unApps) {
|
||||||
const app = apps.find((a: App) => a.pkName === i.pkName)
|
const app = apps.find((a: App) => a.pkName === i.pkName)
|
||||||
const logoRes = await $XXL.uploadFile({
|
const logoRes = await uploadNativeFilePathToCos(app.iconPath, config =>
|
||||||
url: `${import.meta.env.VITE_SERVER_URL}/sysFileInfo/tenUploadAll`,
|
$XXL.uploadFile(config),
|
||||||
header: {
|
)
|
||||||
Authorization: token,
|
|
||||||
},
|
|
||||||
filePath: app.iconPath,
|
|
||||||
fileKey: 'file',
|
|
||||||
})
|
|
||||||
globalApi.updateAppInfo([
|
globalApi.updateAppInfo([
|
||||||
{
|
{
|
||||||
pkName: i.pkName,
|
pkName: i.pkName,
|
||||||
|
|||||||
246
src/utils/cosUpload.ts
Normal file
246
src/utils/cosUpload.ts
Normal file
@ -0,0 +1,246 @@
|
|||||||
|
import COS from 'cos-js-sdk-v5'
|
||||||
|
import { post } from '@/api/request'
|
||||||
|
|
||||||
|
export interface CosStsData {
|
||||||
|
bucket: string
|
||||||
|
durationSeconds: number
|
||||||
|
expireAt: number
|
||||||
|
region: string
|
||||||
|
sessionToken: string
|
||||||
|
tmpSecretId: string
|
||||||
|
tmpSecretKey: string
|
||||||
|
uploadPathPrefix: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CosUploadResult {
|
||||||
|
bucket: string
|
||||||
|
filePath: string
|
||||||
|
key: string
|
||||||
|
region: string
|
||||||
|
url: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type UploadProgress = (event: { percent: number }) => void
|
||||||
|
type NativeUpload = (config: anyObj) => Promise<any>
|
||||||
|
export type CosFileType = 1 | 2
|
||||||
|
|
||||||
|
const stsCache: Partial<Record<CosFileType, CosStsData>> = {}
|
||||||
|
const stsPromise: Partial<Record<CosFileType, Promise<CosStsData>>> = {}
|
||||||
|
|
||||||
|
const normalizePrefix = (prefix = '') => prefix.replace(/^\/+/, '').replace(/\/?$/, '/')
|
||||||
|
|
||||||
|
const getFileExt = (fileName = '', type = '') => {
|
||||||
|
const ext = fileName.match(/\.([a-zA-Z0-9]+)$/)?.[1]
|
||||||
|
if (ext) return ext.toLowerCase()
|
||||||
|
if (type.includes('/')) return type.split('/').pop() || 'png'
|
||||||
|
return 'png'
|
||||||
|
}
|
||||||
|
|
||||||
|
const getFileName = (file: File | Blob, fileName?: string) => {
|
||||||
|
const rawName = fileName || (file instanceof File ? file.name : '')
|
||||||
|
const ext = getFileExt(rawName, file.type)
|
||||||
|
return `${Date.now()}-${Math.random().toString(36).slice(2, 10)}.${ext}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildKey = (sts: CosStsData, file: File | Blob, fileName?: string) => {
|
||||||
|
const date = new Date()
|
||||||
|
const yyyy = date.getFullYear()
|
||||||
|
const mm = String(date.getMonth() + 1).padStart(2, '0')
|
||||||
|
const dd = String(date.getDate()).padStart(2, '0')
|
||||||
|
return `${normalizePrefix(sts.uploadPathPrefix)}${yyyy}${mm}${dd}/${getFileName(file, fileName)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const getCosHost = (sts: CosStsData) => `${sts.bucket}.cos.${sts.region}.myqcloud.com`
|
||||||
|
|
||||||
|
const normalizeCosUrl = (location: string, sts: CosStsData, key: string) => {
|
||||||
|
const url = location || `${getCosHost(sts)}/${key}`
|
||||||
|
return /^https?:\/\//i.test(url) ? url.replace(/^http:\/\//i, 'https://') : `https://${url}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizeStsTime = (value: number) => (value > 9999999999 ? Math.floor(value / 1000) : value)
|
||||||
|
|
||||||
|
const normalizeSts = (sts: CosStsData): CosStsData => ({
|
||||||
|
...sts,
|
||||||
|
expireAt: normalizeStsTime(Number(sts.expireAt) || 0),
|
||||||
|
})
|
||||||
|
|
||||||
|
const isValidSts = (sts: CosStsData | undefined) => {
|
||||||
|
if (!sts) return false
|
||||||
|
const now = Math.floor(Date.now() / 1000)
|
||||||
|
return (
|
||||||
|
!!sts.bucket && !!sts.region && !!sts.tmpSecretId && !!sts.tmpSecretKey && sts.expireAt > now
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getCosSts = async (fileType: CosFileType = 2) => {
|
||||||
|
if (isValidSts(stsCache[fileType])) return stsCache[fileType] as CosStsData
|
||||||
|
if (!stsPromise[fileType]) {
|
||||||
|
stsPromise[fileType] = post<CosStsData>('/common/cos/sts', { fileType }).then(res => {
|
||||||
|
const normalized = normalizeSts(res)
|
||||||
|
stsCache[fileType] = normalized
|
||||||
|
return normalized
|
||||||
|
})
|
||||||
|
stsPromise[fileType]?.finally(() => {
|
||||||
|
stsPromise[fileType] = undefined
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return stsPromise[fileType] as Promise<CosStsData>
|
||||||
|
}
|
||||||
|
|
||||||
|
const createCos = (fileType: CosFileType) =>
|
||||||
|
new COS({
|
||||||
|
Protocol: 'https:',
|
||||||
|
getAuthorization: async (_options, callback) => {
|
||||||
|
const sts = await getCosSts(fileType)
|
||||||
|
const now = Math.floor(Date.now() / 1000)
|
||||||
|
callback({
|
||||||
|
TmpSecretId: sts.tmpSecretId,
|
||||||
|
TmpSecretKey: sts.tmpSecretKey,
|
||||||
|
SecurityToken: sts.sessionToken,
|
||||||
|
StartTime: Math.max(now - 60, sts.expireAt - sts.durationSeconds),
|
||||||
|
ExpiredTime: sts.expireAt,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const uploadFileToCos = async (
|
||||||
|
file: File | Blob,
|
||||||
|
options: { fileName?: string; fileType?: CosFileType; onProgress?: UploadProgress } = {},
|
||||||
|
): Promise<CosUploadResult> => {
|
||||||
|
const fileType = options.fileType || 2
|
||||||
|
const sts = await getCosSts(fileType)
|
||||||
|
const key = buildKey(sts, file, options.fileName)
|
||||||
|
const result = await createCos(fileType).putObject({
|
||||||
|
Bucket: sts.bucket,
|
||||||
|
Region: sts.region,
|
||||||
|
Key: key,
|
||||||
|
Body: file,
|
||||||
|
ContentType: file.type || undefined,
|
||||||
|
onProgress: progress => {
|
||||||
|
options.onProgress?.({ percent: Math.round((progress.percent || 0) * 100) })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const url = normalizeCosUrl(result.Location, sts, key)
|
||||||
|
return {
|
||||||
|
bucket: sts.bucket,
|
||||||
|
filePath: url,
|
||||||
|
key,
|
||||||
|
region: sts.region,
|
||||||
|
url,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const toHex = (buffer: ArrayBuffer) =>
|
||||||
|
Array.from(new Uint8Array(buffer))
|
||||||
|
.map(byte => byte.toString(16).padStart(2, '0'))
|
||||||
|
.join('')
|
||||||
|
|
||||||
|
const sha1 = async (value: string) => {
|
||||||
|
const data = new TextEncoder().encode(value)
|
||||||
|
return toHex(await crypto.subtle.digest('SHA-1', data))
|
||||||
|
}
|
||||||
|
|
||||||
|
const hmacSha1 = async (key: string, value: string) => {
|
||||||
|
const cryptoKey = await crypto.subtle.importKey(
|
||||||
|
'raw',
|
||||||
|
new TextEncoder().encode(key),
|
||||||
|
{ name: 'HMAC', hash: 'SHA-1' },
|
||||||
|
false,
|
||||||
|
['sign'],
|
||||||
|
)
|
||||||
|
return toHex(await crypto.subtle.sign('HMAC', cryptoKey, new TextEncoder().encode(value)))
|
||||||
|
}
|
||||||
|
|
||||||
|
const encodeBase64 = (value: string) => {
|
||||||
|
const bytes = new TextEncoder().encode(value)
|
||||||
|
let binary = ''
|
||||||
|
bytes.forEach(byte => {
|
||||||
|
binary += String.fromCharCode(byte)
|
||||||
|
})
|
||||||
|
return btoa(binary)
|
||||||
|
}
|
||||||
|
|
||||||
|
const createPostObjectFields = async (sts: CosStsData, key: string) => {
|
||||||
|
const now = Math.floor(Date.now() / 1000)
|
||||||
|
const start = Math.max(now - 60, sts.expireAt - sts.durationSeconds)
|
||||||
|
const keyTime = `${start};${sts.expireAt}`
|
||||||
|
const policy = encodeBase64(
|
||||||
|
JSON.stringify({
|
||||||
|
expiration: new Date(sts.expireAt * 1000).toISOString(),
|
||||||
|
conditions: [
|
||||||
|
{ bucket: sts.bucket },
|
||||||
|
{ key },
|
||||||
|
{ 'q-sign-algorithm': 'sha1' },
|
||||||
|
{ 'q-ak': sts.tmpSecretId },
|
||||||
|
{ 'q-sign-time': keyTime },
|
||||||
|
{ 'x-cos-security-token': sts.sessionToken },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
const signKey = await hmacSha1(sts.tmpSecretKey, keyTime)
|
||||||
|
const signature = await hmacSha1(signKey, await sha1(policy))
|
||||||
|
return {
|
||||||
|
key,
|
||||||
|
policy,
|
||||||
|
'q-sign-algorithm': 'sha1',
|
||||||
|
'q-ak': sts.tmpSecretId,
|
||||||
|
'q-key-time': keyTime,
|
||||||
|
'q-signature': signature,
|
||||||
|
'x-cos-security-token': sts.sessionToken,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const uploadNativeFilePathToCos = async (
|
||||||
|
filePath: string,
|
||||||
|
nativeUpload: NativeUpload,
|
||||||
|
options: { fileName?: string; fileType?: CosFileType; mimeType?: string } = {},
|
||||||
|
): Promise<CosUploadResult> => {
|
||||||
|
const sts = await getCosSts(options.fileType || 2)
|
||||||
|
const fileName = options.fileName || filePath.split(/[\\/]/).pop() || ''
|
||||||
|
const key = buildKey(sts, new Blob([], { type: options.mimeType || 'image/png' }), fileName)
|
||||||
|
const formData = await createPostObjectFields(sts, key)
|
||||||
|
await nativeUpload({
|
||||||
|
url: `https://${getCosHost(sts)}`,
|
||||||
|
header: {},
|
||||||
|
data: formData,
|
||||||
|
filePath,
|
||||||
|
fileKey: 'file',
|
||||||
|
formData,
|
||||||
|
})
|
||||||
|
const url = normalizeCosUrl('', sts, key)
|
||||||
|
return {
|
||||||
|
bucket: sts.bucket,
|
||||||
|
filePath: url,
|
||||||
|
key,
|
||||||
|
region: sts.region,
|
||||||
|
url,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const toCosUploadResponse = (result: CosUploadResult) => ({
|
||||||
|
data: {
|
||||||
|
code: 200,
|
||||||
|
data: result,
|
||||||
|
message: '',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const cosUploadRequest = async (options: anyObj) => {
|
||||||
|
try {
|
||||||
|
const result = await uploadFileToCos(options.file, {
|
||||||
|
fileType: options.data?.fileType || 2,
|
||||||
|
fileName: options.file?.name,
|
||||||
|
onProgress: event => options.onProgress?.(event),
|
||||||
|
})
|
||||||
|
const response = {
|
||||||
|
code: 200,
|
||||||
|
data: result,
|
||||||
|
message: '',
|
||||||
|
}
|
||||||
|
options.onSuccess?.(response)
|
||||||
|
return response
|
||||||
|
} catch (error) {
|
||||||
|
options.onError?.(error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user