update:版本控制

This commit is contained in:
ltx 2026-11-06 11:23:14 +08:00
parent f37c98a55c
commit 655ede8d3e

View File

@ -49,7 +49,7 @@
</el-upload> </el-upload>
</div> </div>
</div> </div>
<div class="upload-tip"><el-icon><CircleCheck /></el-icon><span>建议从同一个打包输出目录选取两个文件;APK 将在服务端计算 SHA-256。</span></div> <div class="upload-tip"><el-icon><CircleCheck /></el-icon><span>建议从同一个打包输出目录选取两个文件;APK 由浏览器直传 OSS,后台校验文件并计算 SHA-256。</span></div>
</section> </section>
<section class="panel details-panel"> <section class="panel details-panel">
@ -74,6 +74,10 @@
<span>先保存为草稿,核对无误后再发布</span> <span>先保存为草稿,核对无误后再发布</span>
<el-button type="primary" class="upload-button" :loading="uploading" :disabled="!file" @click="upload">上传为草稿</el-button> <el-button type="primary" class="upload-button" :loading="uploading" :disabled="!file" @click="upload">上传为草稿</el-button>
</div> </div>
<div v-if="uploading" class="upload-progress">
<span>{{ uploadPhase }}</span>
<el-progress :percentage="uploadProgress" :stroke-width="8" />
</div>
</section> </section>
</div> </div>
@ -99,6 +103,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, ref } from 'vue'; import { computed, onMounted, ref } from 'vue';
import axios from 'axios';
import { ElMessage, ElMessageBox } from 'element-plus'; import { ElMessage, ElMessageBox } from 'element-plus';
import type { UploadFile } from 'element-plus'; import type { UploadFile } from 'element-plus';
import { CircleCheck, Document, Refresh, UploadFilled, WarningFilled } from '@element-plus/icons-vue'; import { CircleCheck, Document, Refresh, UploadFilled, WarningFilled } from '@element-plus/icons-vue';
@ -128,6 +133,8 @@ const notesEn = ref('');
const mandatory = ref(false); const mandatory = ref(false);
const loading = ref(false); const loading = ref(false);
const uploading = ref(false); const uploading = ref(false);
const uploadProgress = ref(0);
const uploadPhase = ref('');
const channelLabel = import.meta.env.VITE_API_URL || '未配置'; const channelLabel = import.meta.env.VITE_API_URL || '未配置';
const publishedCount = computed(() => releases.value.filter((item) => item.releaseStatus === 1).length); const publishedCount = computed(() => releases.value.filter((item) => item.releaseStatus === 1).length);
const draftCount = computed(() => releases.value.filter((item) => item.releaseStatus === 0).length); const draftCount = computed(() => releases.value.filter((item) => item.releaseStatus === 0).length);
@ -153,6 +160,7 @@ function selectApk(uploadFile: UploadFile) {
if (!selected) return; if (!selected) return;
if (!selected.name.toLowerCase().endsWith('.apk')) return ElMessage.warning('请选择 .apk 安装包'); if (!selected.name.toLowerCase().endsWith('.apk')) return ElMessage.warning('请选择 .apk 安装包');
file.value = selected; file.value = selected;
uploadProgress.value = 0;
} }
async function selectBuildInfo(uploadFile: UploadFile) { async function selectBuildInfo(uploadFile: UploadFile) {
const selected = uploadFile.raw; const selected = uploadFile.raw;
@ -179,18 +187,59 @@ async function upload() {
if (!versionName.value.trim() || !Number.isInteger(versionCode.value) || (versionCode.value ?? 0) <= 0 || !/^[A-F0-9]{64}$/.test(fingerprint)) { if (!versionName.value.trim() || !Number.isInteger(versionCode.value) || (versionCode.value ?? 0) <= 0 || !/^[A-F0-9]{64}$/.test(fingerprint)) {
return ElMessage.warning('请核对版本名、整数版本号和 64 位公开签名指纹'); return ElMessage.warning('请核对版本名、整数版本号和 64 位公开签名指纹');
} }
const form = new FormData(); const selected = file.value;
form.append('file', file.value);
form.append('versionName', versionName.value.trim());
form.append('versionCode', String(versionCode.value));
form.append('certificateSha256', fingerprint);
form.append('notesZh', notesZh.value);
form.append('notesEn', notesEn.value);
form.append('isMandatory', String(mandatory.value));
uploading.value = true; uploading.value = true;
try { const response = await http.post('/app-updates/admin/upload', form, { headers: { 'Content-Type': 'multipart/form-data' }, timeout: 300000 }); if (response.data?.statusCode !== 200) throw new Error('上传失败'); ElMessage.success('已上传为草稿,请核对后发布'); file.value = null; buildInfoName.value = ''; await load(); } uploadProgress.value = 0;
catch { ElMessage.error('上传失败,请查看服务器返回的具体错误'); } uploadPhase.value = '正在获取临时上传地址…';
finally { uploading.value = false; } try {
const ticketResponse = await http.post('/app-updates/admin/upload-ticket', {
versionCode: versionCode.value,
fileSize: selected.size,
});
if (ticketResponse.data?.statusCode !== 200) throw new Error('获取 OSS 上传地址失败');
const ticket = ticketResponse.data?.data;
if (!ticket?.uploadUrl || !ticket?.objectKey || ticket?.contentType !== 'application/octet-stream') {
throw new Error('后台返回的上传地址无效');
}
uploadPhase.value = '正在直传阿里云 OSS…';
// Use a standalone Axios request: the signed OSS URL must not carry the admin JWT.
await axios.put(ticket.uploadUrl, selected, {
headers: { 'Content-Type': ticket.contentType },
timeout: 0,
onUploadProgress: (event) => {
uploadProgress.value = Math.min(99, Math.round((event.loaded / selected.size) * 100));
},
});
uploadProgress.value = 100;
uploadPhase.value = 'OSS 上传完成,后台正在校验并生成草稿…';
const response = await http.post('/app-updates/admin/upload-complete', {
objectKey: ticket.objectKey,
fileSize: selected.size,
versionName: versionName.value.trim(),
versionCode: versionCode.value,
certificateSha256: fingerprint,
notesZh: notesZh.value.trim(),
notesEn: notesEn.value.trim(),
isMandatory: mandatory.value,
}, { timeout: 300000 });
if (response.data?.statusCode !== 200) throw new Error('OSS 文件校验或草稿创建失败');
ElMessage.success('已上传为草稿,请核对后发布');
file.value = null;
buildInfoName.value = '';
await load();
} catch (error) {
if (axios.isAxiosError(error)) {
const status = error.response?.status;
ElMessage.error(status === 403
? 'OSS 拒绝上传,请检查临时签名、Bucket 权限和系统时间'
: '无法直传 OSS,请检查网络及 Bucket CORS 配置');
} else {
ElMessage.error(error instanceof Error ? error.message : '上传失败,请查看后台错误');
}
} finally {
uploading.value = false;
uploadPhase.value = '';
}
} }
async function publish(row: Release) { async function publish(row: Release) {
const confirmed = await ElMessageBox.confirm(`请确认安装包与构建信息匹配。发布 ${row.versionName}(${row.versionCode})后,裁判端将可检查到此版本。`, '发布确认', { type: 'warning' }).then(() => true, () => false); const confirmed = await ElMessageBox.confirm(`请确认安装包与构建信息匹配。发布 ${row.versionName}(${row.versionCode})后,裁判端将可检查到此版本。`, '发布确认', { type: 'warning' }).then(() => true, () => false);
@ -267,6 +316,8 @@ onMounted(load);
.form-footer span { color: #8b9cb0; font-size: 11px; } .form-footer span { color: #8b9cb0; font-size: 11px; }
.upload-button { min-width: 130px; height: 38px; border: 0; border-radius: 9px; background: linear-gradient(110deg, #1686ee, #1265ce); box-shadow: 0 7px 15px rgba(27, 115, 214, .19); } .upload-button { min-width: 130px; height: 38px; border: 0; border-radius: 9px; background: linear-gradient(110deg, #1686ee, #1265ce); box-shadow: 0 7px 15px rgba(27, 115, 214, .19); }
.upload-button:hover { background: linear-gradient(110deg, #3198f1, #1d74da); } .upload-button:hover { background: linear-gradient(110deg, #3198f1, #1d74da); }
.upload-progress { display: grid; gap: 8px; margin-top: 15px; color: #54708f; font-size: 12px; }
.upload-progress :deep(.el-progress-bar__inner) { background: linear-gradient(110deg, #1686ee, #43a5f4); }
.history-panel { margin-top: 18px; padding: 23px 24px 12px; } .history-panel { margin-top: 18px; padding: 23px 24px 12px; }
.history-heading { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 17px; } .history-heading { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 17px; }
.record-count { padding: 7px 10px; border-radius: 7px; background: #f3f7fc; color: #7890aa; font-size: 11px; } .record-count { padding: 7px 10px; border-radius: 7px; background: #f3f7fc; color: #7890aa; font-size: 11px; }