topfans/frontend/oss-upload-test(1).html
2026-04-07 23:08:49 +08:00

646 lines
24 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>TopFans - OSS 文件上传测试</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.container {
background: white;
border-radius: 16px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
max-width: 600px;
width: 100%;
padding: 40px;
}
h1 {
color: #333;
margin-bottom: 10px;
font-size: 28px;
}
.subtitle {
color: #666;
margin-bottom: 30px;
font-size: 14px;
}
.form-group {
margin-bottom: 25px;
}
label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #444;
font-size: 14px;
}
input[type="text"],
input[type="file"],
select {
width: 100%;
padding: 12px 16px;
border: 2px solid #e1e8ed;
border-radius: 8px;
font-size: 14px;
transition: all 0.3s ease;
font-family: inherit;
}
input[type="text"]:focus,
select:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
input[type="file"] {
padding: 10px;
cursor: pointer;
}
button {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 14px 28px;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 16px;
font-weight: 600;
width: 100%;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
button:hover {
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3);
}
button:active {
transform: translateY(0);
}
button:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.status {
margin-top: 25px;
padding: 16px;
border-radius: 8px;
font-size: 14px;
line-height: 1.6;
display: none;
animation: slideIn 0.3s ease;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.status.show {
display: block;
}
.status.info {
background-color: #e3f2fd;
color: #1565c0;
border-left: 4px solid #1565c0;
}
.status.success {
background-color: #e8f5e9;
color: #2e7d32;
border-left: 4px solid #2e7d32;
}
.status.error {
background-color: #ffebee;
color: #c62828;
border-left: 4px solid #c62828;
}
.status a {
color: inherit;
word-break: break-all;
text-decoration: underline;
}
.help-text {
font-size: 12px;
color: #888;
margin-top: 5px;
}
.loading {
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
border-top-color: white;
animation: spin 1s linear infinite;
margin-right: 8px;
vertical-align: middle;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.divider {
margin: 40px 0;
border-top: 2px solid #e1e8ed;
position: relative;
}
.divider::after {
content: 'OR';
position: absolute;
top: -12px;
left: 50%;
transform: translateX(-50%);
background: white;
padding: 0 15px;
color: #888;
font-size: 12px;
font-weight: 600;
}
.image-preview {
margin-top: 20px;
border-radius: 8px;
overflow: hidden;
display: none;
animation: slideIn 0.3s ease;
}
.image-preview.show {
display: block;
}
.image-preview img {
width: 100%;
height: auto;
display: block;
border: 2px solid #e1e8ed;
border-radius: 8px;
}
.image-info {
margin-top: 10px;
padding: 12px;
background-color: #f5f5f5;
border-radius: 8px;
font-size: 13px;
color: #666;
}
.section-title {
font-size: 20px;
color: #333;
margin-bottom: 20px;
font-weight: 600;
}
</style>
</head>
<body>
<div class="container">
<h1>🚀 TopFans OSS 上传测试</h1>
<p class="subtitle">测试头像和资产文件上传到阿里云 OSS</p>
<!-- 上传文件部分 -->
<div class="section-title">📤 上传文件</div>
<form id="uploadForm">
<div class="form-group">
<label for="jwtToken">JWT Token <span style="color: #c62828;">*</span></label>
<input type="text" id="jwtToken" name="jwtToken" placeholder="请输入您的 JWT Token" required />
<div class="help-text">从登录接口获取的 JWT Token</div>
</div>
<div class="form-group">
<label for="uploadType">上传类型 <span style="color: #c62828;">*</span></label>
<select id="uploadType" name="uploadType">
<option value="avatar">👤 头像 (avatar)</option>
<option value="asset">🎨 资产 (asset)</option>
</select>
<div class="help-text">选择文件上传的目标目录</div>
</div>
<div class="form-group">
<label for="file">选择文件 <span style="color: #c62828;">*</span></label>
<input type="file" id="file" name="file" required />
<div class="help-text">支持的文件类型:图片、视频等</div>
</div>
<button type="submit" id="submitBtn">
<span id="btnText">上传文件</span>
</button>
</form>
<div id="statusDiv" class="status"></div>
<!-- 分隔线 -->
<div class="divider"></div>
<!-- 查看图片部分 -->
<div class="section-title">🖼️ 查看上传的图片</div>
<form id="viewForm">
<div class="form-group">
<label for="viewJwtToken">JWT Token <span style="color: #c62828;">*</span></label>
<input type="text" id="viewJwtToken" name="viewJwtToken" placeholder="请输入您的 JWT Token" required />
<div class="help-text">从登录接口获取的 JWT Token</div>
</div>
<div class="form-group">
<label for="viewType">文件类型 <span style="color: #c62828;">*</span></label>
<select id="viewType" name="viewType">
<option value="avatar">👤 头像 (avatar)</option>
<option value="asset">🎨 资产 (asset)</option>
</select>
<div class="help-text">选择文件所在的目录类型(与上传时一致)</div>
</div>
<div class="form-group">
<label for="imageUrl">图片 URL <span style="color: #c62828;">*</span></label>
<input type="text" id="imageUrl" name="imageUrl"
placeholder="例如: https://top-fans-test.oss-cn-shanghai.aliyuncs.com/avatar/7/87/WechatIMG325.jpg"
required />
<div class="help-text">输入从上传接口返回的图片 URL</div>
</div>
<button type="submit" id="viewBtn">
<span id="viewBtnText">查看图片</span>
</button>
</form>
<div id="viewStatusDiv" class="status"></div>
<!-- 图片预览区域 -->
<div id="imagePreview" class="image-preview">
<img id="previewImg" src="" alt="预览图片" />
<div id="imageInfo" class="image-info"></div>
</div>
</div>
<script type="text/javascript">
document.addEventListener('DOMContentLoaded', function () {
const form = document.querySelector("#uploadForm");
const fileInput = document.querySelector("#file");
const jwtTokenInput = document.querySelector("#jwtToken");
const uploadTypeInput = document.querySelector("#uploadType");
const statusDiv = document.querySelector("#statusDiv");
const submitBtn = document.querySelector("#submitBtn");
const btnText = document.querySelector("#btnText");
form.addEventListener("submit", async (event) => {
event.preventDefault();
const file = fileInput.files[0];
const jwtToken = jwtTokenInput.value.trim();
const uploadType = uploadTypeInput.value;
if (!file) {
showStatus('❌ 请选择一个文件再上传。', 'error');
return;
}
if (!jwtToken) {
showStatus('❌ 请输入 JWT Token。', 'error');
return;
}
// 禁用提交按钮
submitBtn.disabled = true;
btnText.innerHTML = '<span class="loading"></span>上传中...';
try {
showStatus('⏳ 正在获取上传签名...', 'info');
// 1. 获取签名
const signResponse = await fetch(
`http://192.168.1.185:8080/api/v1/assets/oss/signature?type=${uploadType}`,
{
method: "GET",
headers: {
'Authorization': `Bearer ${jwtToken}`
}
}
);
if (!signResponse.ok) {
throw new Error(`获取签名失败: HTTP ${signResponse.status}`);
}
const signData = await signResponse.json();
if (signData.code !== 200) {
throw new Error(signData.message || '获取签名失败');
}
console.log('✅ 签名获取成功:', signData);
// 2. 构建表单数据
showStatus('📤 正在上传文件到 OSS...', 'info');
const formData = new FormData();
// 注意:字段顺序很重要!
formData.append("key", signData.data.dir + file.name); // 文件路径
formData.append("policy", signData.data.policy);
formData.append("success_action_status", "200");
formData.append("x-oss-credential", signData.data.x_oss_credential);
formData.append("x-oss-date", signData.data.x_oss_date);
formData.append("x-oss-security-token", signData.data.security_token);
formData.append("x-oss-signature", signData.data.signature); // 正确的字段名
formData.append("x-oss-signature-version", signData.data.x_oss_signature_version);
formData.append("file", file); // file 必须为最后一个表单域
console.log('📦 上传信息:', {
host: signData.data.host,
dir: signData.data.dir,
filename: file.name,
fullPath: signData.data.dir + file.name
});
// 3. 上传到 OSS
const uploadResponse = await fetch(signData.data.host, {
method: "POST",
body: formData
});
if (uploadResponse.ok || uploadResponse.status === 204) {
const fileUrl = `${signData.data.host}/${signData.data.dir}${file.name}`;
showStatus(
`✅ <strong>文件上传成功!</strong><br><br>
<strong>文件类型:</strong> ${uploadType}<br>
<strong>文件名:</strong> ${file.name}<br>
<strong>文件大小:</strong> ${(file.size / 1024).toFixed(2)} KB<br><br>
<strong>文件 URL:</strong><br>
<a href="${fileUrl}" target="_blank">${fileUrl}</a>`,
'success'
);
console.log('✅ 上传成功:', fileUrl);
// 重置表单
fileInput.value = '';
} else {
const errorText = await uploadResponse.text();
console.error("❌ 上传失败", uploadResponse, errorText);
showStatus(`❌ <strong>上传失败</strong><br>${errorText || '未知错误'}`, 'error');
}
} catch (error) {
console.error("❌ 发生错误:", error);
showStatus(`❌ <strong>发生错误:</strong><br>${error.message}`, 'error');
} finally {
// 恢复提交按钮
submitBtn.disabled = false;
btnText.textContent = '上传文件';
}
});
function showStatus(message, type) {
statusDiv.innerHTML = message;
statusDiv.className = `status ${type} show`;
}
// ========== 查看图片功能 ==========
const viewForm = document.querySelector("#viewForm");
const viewJwtTokenInput = document.querySelector("#viewJwtToken");
const viewTypeInput = document.querySelector("#viewType");
const imageUrlInput = document.querySelector("#imageUrl");
const viewStatusDiv = document.querySelector("#viewStatusDiv");
const viewBtn = document.querySelector("#viewBtn");
const viewBtnText = document.querySelector("#viewBtnText");
const imagePreview = document.querySelector("#imagePreview");
const previewImg = document.querySelector("#previewImg");
const imageInfo = document.querySelector("#imageInfo");
viewForm.addEventListener("submit", async (event) => {
event.preventDefault();
const viewJwtToken = viewJwtTokenInput.value.trim();
const viewType = viewTypeInput.value;
const imageUrl = imageUrlInput.value.trim();
if (!viewJwtToken) {
showViewStatus('❌ 请输入 JWT Token。', 'error');
return;
}
if (!imageUrl) {
showViewStatus('❌ 请输入图片 URL。', 'error');
return;
}
// 验证 URL 格式
try {
new URL(imageUrl);
} catch (e) {
showViewStatus('❌ 请输入有效的 URL 格式。', 'error');
return;
}
// 禁用按钮
viewBtn.disabled = true;
viewBtnText.innerHTML = '<span class="loading"></span>加载中...';
// 隐藏之前的预览
imagePreview.classList.remove('show');
// 清除之前的图片源,避免触发旧的加载事件
previewImg.src = '';
previewImg.onload = null;
previewImg.onerror = null;
try {
// 1. 从 URL 中提取文件路径
const urlObj = new URL(imageUrl);
const filePath = urlObj.pathname.substring(1); // 去掉开头的 '/'
console.log('📝 解析图片信息:', {
url: imageUrl,
filePath: filePath,
type: viewType
});
showViewStatus('⏳ 正在获取 OSS 签名...', 'info');
// 2. 使用与上传相同的接口获取签名(与上传时保持完全一致)
const signResponse = await fetch(
`http://192.168.1.185:8080/api/v1/assets/oss/signature?type=${viewType}`,
{
method: "GET",
headers: {
'Authorization': `Bearer ${viewJwtToken}`
}
}
);
if (!signResponse.ok) {
throw new Error(`获取签名失败: HTTP ${signResponse.status}`);
}
const signData = await signResponse.json();
console.log('📦 后端返回的签名数据:', signData);
if (signData.code !== 200) {
throw new Error(signData.message || '获取签名失败');
}
// 检查返回的数据结构(与上传时相同)
if (!signData.data) {
console.error('❌ 签名数据结构异常:', signData);
throw new Error('后端返回的签名数据格式不正确');
}
console.log('✅ OSS 签名获取成功:', signData);
// 3. 构建带签名的请求头
showViewStatus('⏳ 正在加载图片...', 'info');
// 构建请求头(按照 OSS 标准格式)
const headers = {};
// 如果后端返回了签名信息,添加到请求头
if (signData.data.signature) {
headers['Authorization'] = signData.data.signature;
console.log('🔐 使用签名认证:', signData.data.signature);
}
// 如果后端返回了日期,添加到请求头
if (signData.data.date) {
headers['Date'] = signData.data.date;
console.log('📅 请求日期:', signData.data.date);
}
console.log('🔗 请求 URL:', imageUrl);
console.log('📋 请求头:', headers);
// 使用 fetch 获取图片,带上 Authorization 签名
const imageResponse = await fetch(imageUrl, {
method: 'GET',
headers: headers,
mode: 'cors'
});
if (!imageResponse.ok) {
throw new Error(`获取图片失败: HTTP ${imageResponse.status} ${imageResponse.statusText}`);
}
// 获取图片的 Blob 数据
const blob = await imageResponse.blob();
const contentType = imageResponse.headers.get('Content-Type');
console.log('✅ 图片数据获取成功:', {
size: blob.size,
type: contentType
});
// 验证是否为图片
if (!contentType || !contentType.startsWith('image/')) {
throw new Error('URL 返回的不是图片文件');
}
// 创建本地 URL 用于显示
const objectUrl = URL.createObjectURL(blob);
// 设置图片加载事件
previewImg.onload = () => {
const img = previewImg;
const fileSizeKB = (blob.size / 1024).toFixed(2);
const fileSizeMB = (blob.size / 1024 / 1024).toFixed(2);
const sizeText = blob.size > 1024 * 1024
? `${fileSizeMB} MB`
: `${fileSizeKB} KB`;
console.log('✅ 图片显示成功:', {
filePath: filePath,
dimensions: `${img.naturalWidth}x${img.naturalHeight}`,
size: sizeText
});
imageInfo.innerHTML = `
<strong>图片信息:</strong><br>
文件路径: ${filePath}<br>
文件类型: ${viewType}<br>
尺寸: ${img.naturalWidth} × ${img.naturalHeight} px<br>
大小: ${sizeText}<br>
类型: ${contentType}
`;
imagePreview.classList.add('show');
showViewStatus('✅ 图片加载成功!', 'success');
// 恢复按钮
viewBtn.disabled = false;
viewBtnText.textContent = '查看图片';
};
previewImg.onerror = () => {
URL.revokeObjectURL(objectUrl);
imagePreview.classList.remove('show');
showViewStatus('❌ <strong>图片显示失败</strong>', 'error');
// 恢复按钮
viewBtn.disabled = false;
viewBtnText.textContent = '查看图片';
};
// 设置图片源
previewImg.src = objectUrl;
} catch (error) {
console.error("❌ 获取签名时发生错误:", error);
imagePreview.classList.remove('show');
showViewStatus(`❌ <strong>获取签名失败:</strong><br>${error.message}`, 'error');
// 恢复按钮
viewBtn.disabled = false;
viewBtnText.textContent = '查看图片';
}
});
function showViewStatus(message, type) {
viewStatusDiv.innerHTML = message;
viewStatusDiv.className = `status ${type} show`;
}
});
</script>
</body>
</html>