后端新增 OssUtils/OssTokenVo/OssCorsInitRunner,通过 STS 临时凭证实现客户端直传 OSS; 前端 upload API 适配直传流程,赛事创建/作品提交/作业/富文本编辑器均已切换; 多环境(dev/test/prod) OSS 配置补全;新增 oss-direct-upload-demo 示例项目及 E2E 测试。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
211 lines
4.4 KiB
Vue
211 lines
4.4 KiB
Vue
<!--
|
||
OSS 直传上传组件 Demo
|
||
|
||
最小可运行示例,展示如何使用 file.ts 和 env.ts 实现文件直传阿里云 OSS。
|
||
|
||
使用方式:
|
||
1. 将 file.ts 和 env.ts 复制到你的项目中
|
||
2. 安装 axios:npm install axios
|
||
3. 在页面中引入此组件即可使用
|
||
-->
|
||
<template>
|
||
<div class="upload-demo">
|
||
<h2>阿里云 OSS 直传上传 Demo</h2>
|
||
|
||
<!-- 文件选择 -->
|
||
<div class="upload-area">
|
||
<input
|
||
type="file"
|
||
ref="fileInput"
|
||
@change="handleFileChange"
|
||
accept="image/*,.pdf,.doc,.docx,.mp4,.mp3"
|
||
/>
|
||
<button @click="handleUpload" :disabled="!selectedFile || uploading">
|
||
{{ uploading ? "上传中..." : "上传文件" }}
|
||
</button>
|
||
<button
|
||
v-if="uploading"
|
||
@click="handleCancel"
|
||
style="margin-left: 8px; color: red"
|
||
>
|
||
取消上传
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 进度条 -->
|
||
<div v-if="uploading" class="progress-area">
|
||
<div class="progress-bar">
|
||
<div class="progress-fill" :style="{ width: progress + '%' }"></div>
|
||
</div>
|
||
<span>{{ progress }}%</span>
|
||
</div>
|
||
|
||
<!-- 上传结果 -->
|
||
<div v-if="result" class="result-area">
|
||
<p>上传成功!</p>
|
||
<p>文件路径:{{ result.filePath }}</p>
|
||
<p>文件大小:{{ (result.fileSize / 1024).toFixed(1) }} KB</p>
|
||
<img
|
||
v-if="result.filePath && isImage(result.fileName)"
|
||
:src="result.filePath"
|
||
alt="预览"
|
||
style="max-width: 300px; margin-top: 8px"
|
||
/>
|
||
</div>
|
||
|
||
<!-- 错误信息 -->
|
||
<div v-if="error" class="error-area">
|
||
<p style="color: red">上传失败:{{ error }}</p>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref } from "vue";
|
||
import { uploadFile } from "./file";
|
||
|
||
const fileInput = ref<HTMLInputElement>();
|
||
const selectedFile = ref<File | null>(null);
|
||
const uploading = ref(false);
|
||
const progress = ref(0);
|
||
const result = ref<{
|
||
filePath: string;
|
||
fileName: string;
|
||
fileSize: number;
|
||
} | null>(null);
|
||
const error = ref<string>("");
|
||
|
||
// 取消控制器
|
||
let abortController: AbortController | null = null;
|
||
|
||
/** 判断是否为图片 */
|
||
function isImage(fileName: string): boolean {
|
||
return /\.(jpg|jpeg|png|gif|webp|bmp)$/i.test(fileName);
|
||
}
|
||
|
||
/** 文件选择事件 */
|
||
function handleFileChange(event: Event) {
|
||
const target = event.target as HTMLInputElement;
|
||
if (target.files && target.files.length > 0) {
|
||
selectedFile.value = target.files[0];
|
||
error.value = "";
|
||
result.value = null;
|
||
}
|
||
}
|
||
|
||
/** 开始上传 */
|
||
async function handleUpload() {
|
||
if (!selectedFile.value) return;
|
||
|
||
uploading.value = true;
|
||
progress.value = 0;
|
||
error.value = "";
|
||
result.value = null;
|
||
|
||
// 创建取消控制器
|
||
abortController = new AbortController();
|
||
|
||
try {
|
||
const uploadResult = await uploadFile(selectedFile.value, "demo", {
|
||
onProgress: (percent) => {
|
||
progress.value = percent;
|
||
},
|
||
signal: abortController.signal,
|
||
});
|
||
|
||
result.value = {
|
||
filePath: uploadResult.filePath,
|
||
fileName: uploadResult.fileName,
|
||
fileSize: uploadResult.fileSize,
|
||
};
|
||
} catch (err: any) {
|
||
if (err.name === "CanceledError" || err.name === "AbortError") {
|
||
error.value = "上传已取消";
|
||
} else {
|
||
error.value = err.message || "未知错误";
|
||
}
|
||
} finally {
|
||
uploading.value = false;
|
||
abortController = null;
|
||
}
|
||
}
|
||
|
||
/** 取消上传 */
|
||
function handleCancel() {
|
||
if (abortController) {
|
||
abortController.abort();
|
||
abortController = null;
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.upload-demo {
|
||
max-width: 500px;
|
||
margin: 20px auto;
|
||
padding: 20px;
|
||
border: 1px solid #e0e0e0;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.upload-area {
|
||
margin: 16px 0;
|
||
}
|
||
|
||
.progress-area {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin: 8px 0;
|
||
}
|
||
|
||
.progress-bar {
|
||
flex: 1;
|
||
height: 8px;
|
||
background: #e0e0e0;
|
||
border-radius: 4px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.progress-fill {
|
||
height: 100%;
|
||
background: #1890ff;
|
||
transition: width 0.3s;
|
||
}
|
||
|
||
.result-area,
|
||
.error-area {
|
||
margin-top: 16px;
|
||
padding: 12px;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.result-area {
|
||
background: #f6ffed;
|
||
border: 1px solid #b7eb8f;
|
||
}
|
||
|
||
.error-area {
|
||
background: #fff2f0;
|
||
border: 1px solid #ffccc7;
|
||
}
|
||
|
||
button {
|
||
padding: 6px 16px;
|
||
border: 1px solid #d9d9d9;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
background: #fff;
|
||
}
|
||
|
||
button:hover:not(:disabled) {
|
||
border-color: #1890ff;
|
||
color: #1890ff;
|
||
}
|
||
|
||
button:disabled {
|
||
opacity: 0.5;
|
||
cursor: not-allowed;
|
||
}
|
||
</style>
|