update:文件地址
This commit is contained in:
parent
8c6c137f76
commit
f37c98a55c
@ -1,59 +1,287 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="layout-padding release-page">
|
<div class="release-page">
|
||||||
<el-card>
|
<section class="hero">
|
||||||
<template #header><div class="heading"><div><h2>裁判端版本管理</h2><small>仅超级管理员可发布。当前环境:{{ channelLabel }}</small></div><el-button @click="load">刷新列表</el-button></div></template>
|
<div class="hero-pattern" aria-hidden="true"></div>
|
||||||
<el-alert title="正式签名首次切换无法覆盖旧调试签名 APK;请先完成逐台迁移检查。发布前必须核对版本号、包名和签名指纹。" type="warning" :closable="false" show-icon />
|
<div class="hero-copy">
|
||||||
<el-form label-width="130px" class="upload-form">
|
<div class="eyebrow"><span class="eyebrow-dot"></span> APP RELEASE CENTER <span class="eyebrow-divider">/</span> 内部分发</div>
|
||||||
<el-form-item label="APK 文件"><input type="file" accept=".apk,application/vnd.android.package-archive" @change="selectFile" /><span v-if="file">{{ (file.size / 1024 / 1024).toFixed(1) }} MB</span></el-form-item>
|
<h1>裁判端版本管理</h1>
|
||||||
<el-form-item label="更新说明(中文)"><el-input v-model="notesZh" type="textarea" :rows="2" maxlength="2000" show-word-limit /></el-form-item>
|
<p>从安装包上传到版本发布,让每一次升级都清晰可追溯。</p>
|
||||||
<el-form-item label="Release notes"><el-input v-model="notesEn" type="textarea" :rows="2" maxlength="2000" show-word-limit /></el-form-item>
|
<div class="hero-endpoint"><span>当前接口</span><strong>{{ channelLabel }}</strong></div>
|
||||||
<el-form-item label="紧急强制更新"><el-switch v-model="mandatory" /><small class="hint">评分中不打断;断网仍可继续离线评分。</small></el-form-item>
|
</div>
|
||||||
<el-form-item><el-button type="primary" :loading="uploading" :disabled="!file" @click="upload">上传为草稿</el-button></el-form-item>
|
<div class="hero-side">
|
||||||
|
<el-button class="refresh-button" :icon="Refresh" :loading="loading" @click="load">刷新记录</el-button>
|
||||||
|
<div class="hero-stats">
|
||||||
|
<div><strong>{{ publishedCount }}</strong><span>已发布</span></div>
|
||||||
|
<div><strong>{{ draftCount }}</strong><span>待发布</span></div>
|
||||||
|
<div><strong>{{ latestVersion }}</strong><span>最新版本</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="safety-note">
|
||||||
|
<el-icon><WarningFilled /></el-icon>
|
||||||
|
<span>当前服务端暂不解析 APK 内的包名、版本和签名。请选用同一次打包生成的 APK 与 <code>build-info.json</code>,人工核对后发布;平板安装前仍会校验实际安装包。</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="workspace">
|
||||||
|
<section class="panel upload-panel">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<span class="step-number">01</span>
|
||||||
|
<div><h2>上传构建文件</h2><p>拖拽文件到对应区域,或点击区域浏览本地文件</p></div>
|
||||||
|
</div>
|
||||||
|
<div class="dropzones">
|
||||||
|
<div class="dropzone-wrap">
|
||||||
|
<div class="dropzone-label">Android 安装包 <span class="required">必选</span></div>
|
||||||
|
<el-upload class="dropzone" :class="{ selected: !!file }" drag :auto-upload="false" :show-file-list="false" accept=".apk,application/vnd.android.package-archive" :on-change="selectApk">
|
||||||
|
<span class="drop-icon"><el-icon><UploadFilled /></el-icon></span>
|
||||||
|
<strong>{{ file ? '安装包已就绪' : '拖拽 APK 到这里' }}</strong>
|
||||||
|
<span class="drop-hint">{{ file ? file.name : '或点击选择 .apk 文件' }}</span>
|
||||||
|
<span class="drop-meta">{{ file ? formatBytes(file.size) + ' · 点击可更换' : '支持最大 200 MB' }}</span>
|
||||||
|
</el-upload>
|
||||||
|
</div>
|
||||||
|
<div class="dropzone-wrap">
|
||||||
|
<div class="dropzone-label">构建信息 <span class="optional">推荐</span></div>
|
||||||
|
<el-upload class="dropzone metadata-dropzone" :class="{ selected: !!buildInfoName }" drag :auto-upload="false" :show-file-list="false" accept=".json,application/json" :on-change="selectBuildInfo">
|
||||||
|
<span class="drop-icon"><el-icon><Document /></el-icon></span>
|
||||||
|
<strong>{{ buildInfoName ? '构建信息已读取' : '拖拽构建信息到这里' }}</strong>
|
||||||
|
<span class="drop-hint">{{ buildInfoName || '或点击选择 build-info.json' }}</span>
|
||||||
|
<span class="drop-meta">只在浏览器本地读取,不上传 JSON</span>
|
||||||
|
</el-upload>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="upload-tip"><el-icon><CircleCheck /></el-icon><span>建议从同一个打包输出目录选取两个文件;APK 将在服务端计算 SHA-256。</span></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="panel details-panel">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<span class="step-number">02</span>
|
||||||
|
<div><h2>填写发布信息</h2><p>构建信息会自动带入,发布前请再次核对</p></div>
|
||||||
|
</div>
|
||||||
|
<el-form label-position="top" class="release-form">
|
||||||
|
<div class="form-row">
|
||||||
|
<el-form-item label="版本名"><el-input v-model="versionName" placeholder="例如 1.0.0" maxlength="64" /></el-form-item>
|
||||||
|
<el-form-item label="版本号"><el-input-number v-model="versionCode" :min="1" :max="999999999" :precision="0" controls-position="right" /><span class="field-help">需高于设备已安装版本</span></el-form-item>
|
||||||
|
</div>
|
||||||
|
<el-form-item label="公开签名证书 SHA-256"><el-input v-model="certificateSha256" placeholder="64 位十六进制指纹,不是密钥密码" /><span class="field-help">仅用于 App 对照签名;不要填写或上传私钥密码</span></el-form-item>
|
||||||
|
<el-form-item label="更新说明"><el-input v-model="notesZh" type="textarea" :rows="3" maxlength="2000" show-word-limit placeholder="简要说明本次更新内容" /></el-form-item>
|
||||||
|
<el-form-item label="Release notes(选填)"><el-input v-model="notesEn" type="textarea" :rows="2" maxlength="2000" show-word-limit placeholder="English release notes" /></el-form-item>
|
||||||
|
<div class="policy-box">
|
||||||
|
<div><strong>紧急强制更新</strong><p>评分中不打断;待提交内容会先同步。普通更新可由裁判稍后处理。</p></div>
|
||||||
|
<el-switch v-model="mandatory" />
|
||||||
|
</div>
|
||||||
</el-form>
|
</el-form>
|
||||||
<el-table v-loading="loading" :data="releases" border stripe>
|
<div class="form-footer">
|
||||||
<el-table-column prop="versionName" label="版本" width="100" />
|
<span>先保存为草稿,核对无误后再发布</span>
|
||||||
<el-table-column prop="versionCode" label="版本号" width="100" />
|
<el-button type="primary" class="upload-button" :loading="uploading" :disabled="!file" @click="upload">上传为草稿</el-button>
|
||||||
<el-table-column prop="releaseStatus" label="状态" width="100"><template #default="{row}">{{ ['草稿','已发布','已撤回'][row.releaseStatus] || '未知' }}</template></el-table-column>
|
</div>
|
||||||
<el-table-column prop="isMandatory" label="策略" width="100"><template #default="{row}">{{ row.isMandatory ? '强制' : '可选' }}</template></el-table-column>
|
</section>
|
||||||
<el-table-column prop="notesZh" label="更新说明" min-width="180" show-overflow-tooltip />
|
</div>
|
||||||
<el-table-column prop="sha256" label="文件 SHA-256" min-width="210" show-overflow-tooltip />
|
|
||||||
<el-table-column prop="certificateSha256" label="签名指纹" min-width="210" show-overflow-tooltip />
|
<section class="panel history-panel">
|
||||||
<el-table-column prop="publishedTime" label="发布时间" width="180" />
|
<div class="history-heading">
|
||||||
<el-table-column label="操作" width="165" fixed="right"><template #default="{row}"><el-button v-if="row.releaseStatus === 0" text type="primary" @click="publish(row)">发布</el-button><el-button v-if="row.releaseStatus === 1" text type="danger" @click="withdraw(row)">撤回</el-button></template></el-table-column>
|
<div class="panel-heading"><span class="step-number">03</span><div><h2>发布记录</h2><p>查看草稿、已发布及撤回的历史版本</p></div></div>
|
||||||
|
<span class="record-count">共 {{ releases.length }} 条记录</span>
|
||||||
|
</div>
|
||||||
|
<el-table v-loading="loading" :data="releases" class="release-table" empty-text="暂无版本记录" table-layout="fixed">
|
||||||
|
<el-table-column label="版本" min-width="150"><template #default="{ row }"><div class="version-cell"><strong>{{ row.versionName }}</strong><small>Build {{ row.versionCode }}</small></div></template></el-table-column>
|
||||||
|
<el-table-column label="状态" width="112"><template #default="{ row }"><el-tag round :type="statusType(row.releaseStatus)" effect="light">{{ statusLabel(row.releaseStatus) }}</el-tag></template></el-table-column>
|
||||||
|
<el-table-column label="策略" width="112"><template #default="{ row }"><span :class="['strategy', row.isMandatory ? 'mandatory' : 'optional']">{{ row.isMandatory ? '强制更新' : '可选更新' }}</span></template></el-table-column>
|
||||||
|
<el-table-column prop="notesZh" label="更新说明" min-width="190" show-overflow-tooltip />
|
||||||
|
<el-table-column label="文件大小" width="112"><template #default="{ row }">{{ formatBytes(row.fileSize) }}</template></el-table-column>
|
||||||
|
<el-table-column label="文件 SHA-256" min-width="145"><template #default="{ row }"><el-tooltip :content="row.sha256" placement="top"><span class="hash-text">{{ shortHash(row.sha256) }}</span></el-tooltip></template></el-table-column>
|
||||||
|
<el-table-column label="签名指纹" min-width="145"><template #default="{ row }"><el-tooltip :content="row.certificateSha256" placement="top"><span class="hash-text">{{ shortHash(row.certificateSha256) }}</span></el-tooltip></template></el-table-column>
|
||||||
|
<el-table-column label="发布时间" min-width="155"><template #default="{ row }">{{ formatTime(row.publishedTime) }}</template></el-table-column>
|
||||||
|
<el-table-column label="操作" width="132" fixed="right"><template #default="{ row }"><el-button v-if="row.releaseStatus === 0" text type="primary" @click="publish(row)">发布</el-button><el-button v-else-if="row.releaseStatus === 1" text type="danger" @click="withdraw(row)">撤回</el-button><span v-else class="muted">—</span></template></el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
</el-card>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, ref } from 'vue';
|
import { computed, onMounted, ref } from 'vue';
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||||
|
import type { UploadFile } from 'element-plus';
|
||||||
|
import { CircleCheck, Document, Refresh, UploadFilled, WarningFilled } from '@element-plus/icons-vue';
|
||||||
import http from '/@/utils/request';
|
import http from '/@/utils/request';
|
||||||
type Release = { id: string; versionName: string; versionCode: number; releaseStatus: number; isMandatory: boolean; notesZh: string; sha256: string; certificateSha256: string; publishedTime?: string };
|
|
||||||
|
type Release = {
|
||||||
|
id: string;
|
||||||
|
versionName: string;
|
||||||
|
versionCode: number;
|
||||||
|
releaseStatus: number;
|
||||||
|
isMandatory: boolean;
|
||||||
|
notesZh: string;
|
||||||
|
fileSize: number;
|
||||||
|
sha256: string;
|
||||||
|
certificateSha256: string;
|
||||||
|
publishedTime?: string;
|
||||||
|
};
|
||||||
|
|
||||||
const releases = ref<Release[]>([]);
|
const releases = ref<Release[]>([]);
|
||||||
const file = ref<File | null>(null);
|
const file = ref<File | null>(null);
|
||||||
|
const buildInfoName = ref('');
|
||||||
|
const versionName = ref('');
|
||||||
|
const versionCode = ref<number | undefined>(undefined);
|
||||||
|
const certificateSha256 = ref('');
|
||||||
const notesZh = ref('');
|
const notesZh = ref('');
|
||||||
const notesEn = ref('');
|
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 channelLabel = import.meta.env.VITE_API_URL;
|
const channelLabel = import.meta.env.VITE_API_URL || '未配置';
|
||||||
function selectFile(event: Event) { file.value = (event.target as HTMLInputElement).files?.[0] || null; }
|
const publishedCount = computed(() => releases.value.filter((item) => item.releaseStatus === 1).length);
|
||||||
|
const draftCount = computed(() => releases.value.filter((item) => item.releaseStatus === 0).length);
|
||||||
|
const latestVersion = computed(() => {
|
||||||
|
const latest = releases.value.filter((item) => item.releaseStatus === 1).sort((a, b) => b.versionCode - a.versionCode)[0];
|
||||||
|
return latest?.versionName || '—';
|
||||||
|
});
|
||||||
|
|
||||||
|
function formatBytes(size?: number) {
|
||||||
|
if (!size || size < 0) return '—';
|
||||||
|
return size >= 1024 * 1024 ? `${(size / 1024 / 1024).toFixed(1)} MB` : `${(size / 1024).toFixed(1)} KB`;
|
||||||
|
}
|
||||||
|
function shortHash(value?: string) { return value ? `${value.slice(0, 8)}…${value.slice(-6)}` : '—'; }
|
||||||
|
function formatTime(value?: string) {
|
||||||
|
if (!value) return '—';
|
||||||
|
const date = new Date(value);
|
||||||
|
return Number.isNaN(date.getTime()) ? '—' : date.toLocaleString('zh-CN', { hour12: false });
|
||||||
|
}
|
||||||
|
function statusLabel(status: number) { return ['草稿', '已发布', '已撤回'][status] || '未知'; }
|
||||||
|
function statusType(status: number): 'warning' | 'success' | 'info' { return status === 1 ? 'success' : status === 0 ? 'warning' : 'info'; }
|
||||||
|
function selectApk(uploadFile: UploadFile) {
|
||||||
|
const selected = uploadFile.raw;
|
||||||
|
if (!selected) return;
|
||||||
|
if (!selected.name.toLowerCase().endsWith('.apk')) return ElMessage.warning('请选择 .apk 安装包');
|
||||||
|
file.value = selected;
|
||||||
|
}
|
||||||
|
async function selectBuildInfo(uploadFile: UploadFile) {
|
||||||
|
const selected = uploadFile.raw;
|
||||||
|
if (!selected) return;
|
||||||
|
buildInfoName.value = '';
|
||||||
|
try {
|
||||||
|
const info = JSON.parse(await selected.text());
|
||||||
|
if (info.mode !== 'release' || info.packageName !== 'com.yd.event.yd_pattern_scoring_system' ||
|
||||||
|
!Number.isInteger(info.versionCode) || !/^[A-Fa-f0-9]{64}$/.test(info.certificateSha256 || '')) {
|
||||||
|
throw new Error('构建信息不是有效的正式版记录');
|
||||||
|
}
|
||||||
|
versionName.value = String(info.versionName || '');
|
||||||
|
versionCode.value = info.versionCode;
|
||||||
|
certificateSha256.value = info.certificateSha256.toUpperCase();
|
||||||
|
buildInfoName.value = selected.name;
|
||||||
|
ElMessage.success('已读取构建信息,请确认与所选 APK 来自同一次打包');
|
||||||
|
} catch { ElMessage.error('无法读取正式版 build-info.json,请手动填写并核对'); }
|
||||||
|
}
|
||||||
async function load() { loading.value = true; try { const response = await http.get('/app-updates/admin/releases'); if (response.data?.statusCode !== 200) throw new Error('加载失败'); releases.value = Array.isArray(response.data?.data) ? response.data.data : []; } catch { ElMessage.error('无法加载版本列表'); } finally { loading.value = false; } }
|
async function load() { loading.value = true; try { const response = await http.get('/app-updates/admin/releases'); if (response.data?.statusCode !== 200) throw new Error('加载失败'); releases.value = Array.isArray(response.data?.data) ? response.data.data : []; } catch { ElMessage.error('无法加载版本列表'); } finally { loading.value = false; } }
|
||||||
async function upload() {
|
async function upload() {
|
||||||
if (!file.value || !notesZh.value.trim()) return ElMessage.warning('请选择 APK 并填写中文更新说明');
|
if (!file.value || !notesZh.value.trim()) return ElMessage.warning('请选择 APK 并填写中文更新说明');
|
||||||
if (!file.value.name.toLowerCase().endsWith('.apk') || file.value.size > 200 * 1024 * 1024) return ElMessage.warning('仅支持不超过 200 MB 的 APK');
|
if (!file.value.name.toLowerCase().endsWith('.apk') || file.value.size > 200 * 1024 * 1024) return ElMessage.warning('仅支持不超过 200 MB 的 APK');
|
||||||
const form = new FormData(); form.append('file', file.value); form.append('notesZh', notesZh.value); form.append('notesEn', notesEn.value); form.append('isMandatory', String(mandatory.value));
|
const fingerprint = certificateSha256.value.replace(/:/g, '').trim().toUpperCase();
|
||||||
|
if (!versionName.value.trim() || !Number.isInteger(versionCode.value) || (versionCode.value ?? 0) <= 0 || !/^[A-F0-9]{64}$/.test(fingerprint)) {
|
||||||
|
return ElMessage.warning('请核对版本名、整数版本号和 64 位公开签名指纹');
|
||||||
|
}
|
||||||
|
const form = new FormData();
|
||||||
|
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; await load(); }
|
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(); }
|
||||||
catch { ElMessage.error('上传失败,请核对 APK、签名和服务器配置'); }
|
catch { ElMessage.error('上传失败,请查看服务器返回的具体错误'); }
|
||||||
finally { uploading.value = false; }
|
finally { uploading.value = false; }
|
||||||
}
|
}
|
||||||
async function publish(row: Release) { await ElMessageBox.confirm(`确认发布 ${row.versionName}(${row.versionCode})?`, '发布确认'); try { const response = await http.post(`/app-updates/admin/${row.id}/publish`, {}); if (response.data?.statusCode !== 200) throw new Error('发布失败'); ElMessage.success('发布成功'); await load(); } catch { ElMessage.error('发布失败'); } }
|
async function publish(row: Release) {
|
||||||
async function withdraw(row: Release) { await ElMessageBox.confirm(`确认撤回 ${row.versionName}?`, '撤回确认'); try { const response = await http.post(`/app-updates/admin/${row.id}/withdraw`, {}); if (response.data?.statusCode !== 200) throw new Error('撤回失败'); ElMessage.success('已撤回'); await load(); } catch { ElMessage.error('撤回失败'); } }
|
const confirmed = await ElMessageBox.confirm(`请确认安装包与构建信息匹配。发布 ${row.versionName}(${row.versionCode})后,裁判端将可检查到此版本。`, '发布确认', { type: 'warning' }).then(() => true, () => false);
|
||||||
|
if (!confirmed) return;
|
||||||
|
try { const response = await http.post(`/app-updates/admin/${row.id}/publish`, {}); if (response.data?.statusCode !== 200) throw new Error('发布失败'); ElMessage.success('发布成功'); await load(); } catch { ElMessage.error('发布失败'); }
|
||||||
|
}
|
||||||
|
async function withdraw(row: Release) {
|
||||||
|
const confirmed = await ElMessageBox.confirm(`确认撤回 ${row.versionName}?`, '撤回确认', { type: 'warning' }).then(() => true, () => false);
|
||||||
|
if (!confirmed) return;
|
||||||
|
try { const response = await http.post(`/app-updates/admin/${row.id}/withdraw`, {}); if (response.data?.statusCode !== 200) throw new Error('撤回失败'); ElMessage.success('已撤回'); await load(); } catch { ElMessage.error('撤回失败'); }
|
||||||
|
}
|
||||||
onMounted(load);
|
onMounted(load);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.release-page{padding:22px}.heading{display:flex;justify-content:space-between;align-items:center}.heading h2{margin:0 0 5px}.heading small,.hint{color:#8193a7}.upload-form{max-width:760px;margin:22px 0}.hint{margin-left:10px}
|
.release-page { min-height: 100%; padding: 24px clamp(16px, 2.6vw, 36px) 42px; color: #203553; background: radial-gradient(circle at 92% 2%, #e7f1ff 0, transparent 27%), #f5f8fd; }
|
||||||
|
.hero { position: relative; display: flex; justify-content: space-between; gap: 32px; min-height: 210px; overflow: hidden; padding: 30px 38px; border-radius: 24px; color: #fff; background: linear-gradient(112deg, #103b83 0%, #1668cf 57%, #438fe9 100%); box-shadow: 0 16px 36px rgba(17, 80, 159, .16); }
|
||||||
|
.hero::after { position: absolute; right: -90px; bottom: -166px; width: 480px; height: 480px; border: 1px solid rgba(255, 255, 255, .2); border-radius: 50%; content: ''; }
|
||||||
|
.hero-pattern { position: absolute; right: 72px; top: -115px; width: 390px; height: 390px; border: 1px solid rgba(255, 255, 255, .14); border-radius: 50%; box-shadow: 0 0 0 45px rgba(255, 255, 255, .035), 0 0 0 92px rgba(255, 255, 255, .025); }
|
||||||
|
.hero-copy, .hero-side { position: relative; z-index: 1; }
|
||||||
|
.eyebrow { display: flex; align-items: center; gap: 9px; font-size: 11px; font-weight: 700; letter-spacing: .14em; color: #d5eaff; }
|
||||||
|
.eyebrow-dot { width: 7px; height: 7px; border-radius: 50%; background: #7de1ed; box-shadow: 0 0 0 4px rgba(125, 225, 237, .15); }
|
||||||
|
.eyebrow-divider { color: rgba(255, 255, 255, .45); }
|
||||||
|
.hero h1 { margin: 20px 0 8px; font-size: clamp(24px, 2.3vw, 34px); font-weight: 700; letter-spacing: .02em; }
|
||||||
|
.hero p { margin: 0; font-size: 14px; color: #d8eaff; }
|
||||||
|
.hero-endpoint { display: flex; align-items: center; gap: 10px; margin-top: 25px; font-size: 12px; color: #bad9ff; }
|
||||||
|
.hero-endpoint strong { max-width: 520px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500; color: #fff; }
|
||||||
|
.hero-side { display: flex; flex-direction: column; align-items: flex-end; justify-content: space-between; min-width: 350px; }
|
||||||
|
.refresh-button { border-color: rgba(255, 255, 255, .36); color: #fff; background: rgba(255, 255, 255, .12); }
|
||||||
|
.refresh-button:hover { border-color: #fff; color: #1555a8; background: #fff; }
|
||||||
|
.hero-stats { display: flex; gap: 10px; }
|
||||||
|
.hero-stats > div { display: flex; flex-direction: column; gap: 6px; min-width: 98px; padding: 13px 16px; border: 1px solid rgba(255, 255, 255, .2); border-radius: 13px; background: rgba(255, 255, 255, .1); backdrop-filter: blur(4px); }
|
||||||
|
.hero-stats strong { font-size: 20px; line-height: 1; }
|
||||||
|
.hero-stats span { font-size: 11px; color: #d3e7ff; }
|
||||||
|
.safety-note { display: flex; align-items: flex-start; gap: 10px; margin: 18px 0; padding: 13px 16px; border: 1px solid #f6dfad; border-radius: 13px; background: #fffaf0; color: #825e23; font-size: 12px; line-height: 1.7; }
|
||||||
|
.safety-note .el-icon { flex: none; margin-top: 2px; font-size: 17px; color: #d99a27; }
|
||||||
|
.safety-note code { padding: 2px 5px; border-radius: 4px; background: #fff0cd; font-size: 11px; }
|
||||||
|
.workspace { display: grid; grid-template-columns: minmax(0, 1.03fr) minmax(390px, .97fr); gap: 18px; align-items: stretch; }
|
||||||
|
.panel { min-width: 0; border: 1px solid #e5ecf6; border-radius: 19px; background: #fff; box-shadow: 0 8px 26px rgba(35, 77, 128, .045); }
|
||||||
|
.upload-panel, .details-panel { padding: 24px; }
|
||||||
|
.panel-heading { display: flex; align-items: flex-start; gap: 13px; }
|
||||||
|
.step-number { display: inline-flex; align-items: center; justify-content: center; flex: none; width: 35px; height: 35px; border-radius: 11px; background: #eaf3ff; color: #1676d8; font-size: 13px; font-weight: 800; }
|
||||||
|
.panel-heading h2 { margin: 1px 0 5px; font-size: 17px; line-height: 1.2; color: #17345d; }
|
||||||
|
.panel-heading p { margin: 0; font-size: 12px; line-height: 1.5; color: #8293aa; }
|
||||||
|
.dropzones { display: grid; gap: 18px; margin-top: 25px; }
|
||||||
|
.dropzone-label { display: flex; align-items: center; gap: 8px; margin-bottom: 9px; font-size: 12px; font-weight: 700; color: #334c6e; }
|
||||||
|
.required, .optional { padding: 2px 7px; border-radius: 5px; font-size: 10px; font-weight: 600; }
|
||||||
|
.required { color: #e56762; background: #fff0ef; }
|
||||||
|
.optional { color: #5482b8; background: #ecf4ff; }
|
||||||
|
.dropzone { display: block; width: 100%; }
|
||||||
|
.dropzone :deep(.el-upload), .dropzone :deep(.el-upload-dragger) { width: 100%; }
|
||||||
|
.dropzone :deep(.el-upload-dragger) { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 182px; padding: 18px; border: 1.5px dashed #b8cfec; border-radius: 15px; background: #f8fbff; transition: border-color .2s, background .2s, box-shadow .2s; }
|
||||||
|
.dropzone :deep(.el-upload-dragger:hover), .dropzone :deep(.el-upload-dragger.is-dragover) { border-color: #2888ed; background: #f0f7ff; box-shadow: inset 0 0 0 1px rgba(40, 136, 237, .13); }
|
||||||
|
.dropzone.selected :deep(.el-upload-dragger) { border-style: solid; border-color: #7bb7f4; background: #f1f8ff; }
|
||||||
|
.metadata-dropzone :deep(.el-upload-dragger) { height: 158px; }
|
||||||
|
.drop-icon { display: inline-flex; align-items: center; justify-content: center; width: 46px; height: 46px; margin-bottom: 11px; border-radius: 14px; background: #e6f1ff; color: #1882e8; font-size: 23px; }
|
||||||
|
.metadata-dropzone .drop-icon { background: #ecf7f9; color: #2d9cab; }
|
||||||
|
.dropzone strong { color: #28527e; font-size: 14px; }
|
||||||
|
.drop-hint { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin-top: 6px; color: #66819f; font-size: 12px; }
|
||||||
|
.drop-meta { margin-top: 7px; color: #9cabc0; font-size: 11px; }
|
||||||
|
.upload-tip { display: flex; align-items: flex-start; gap: 8px; margin-top: 21px; padding: 13px 15px; border-radius: 10px; background: #f2f8ff; color: #5d7b9f; font-size: 12px; line-height: 1.6; }
|
||||||
|
.upload-tip .el-icon { flex: none; margin-top: 2px; color: #2185e5; font-size: 15px; }
|
||||||
|
.release-form { margin-top: 24px; }
|
||||||
|
.release-form :deep(.el-form-item) { margin-bottom: 18px; }
|
||||||
|
.release-form :deep(.el-form-item__label) { padding-bottom: 7px; color: #344e70; font-size: 12px; font-weight: 700; }
|
||||||
|
.release-form :deep(.el-input__wrapper), .release-form :deep(.el-textarea__inner) { border-radius: 9px; }
|
||||||
|
.release-form :deep(.el-input-number) { width: 100%; }
|
||||||
|
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 15px; }
|
||||||
|
.field-help { display: block; margin-top: 5px; color: #91a0b4; font-size: 11px; line-height: 1.4; }
|
||||||
|
.policy-box { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 13px 15px; border: 1px solid #e8eff8; border-radius: 11px; background: #f8fbff; }
|
||||||
|
.policy-box strong { font-size: 12px; color: #294663; }
|
||||||
|
.policy-box p { margin: 5px 0 0; font-size: 11px; line-height: 1.5; color: #899bb2; }
|
||||||
|
.form-footer { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-top: 20px; padding-top: 18px; border-top: 1px solid #eef2f7; }
|
||||||
|
.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:hover { background: linear-gradient(110deg, #3198f1, #1d74da); }
|
||||||
|
.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; }
|
||||||
|
.record-count { padding: 7px 10px; border-radius: 7px; background: #f3f7fc; color: #7890aa; font-size: 11px; }
|
||||||
|
.release-table { width: 100%; }
|
||||||
|
.release-table :deep(th.el-table__cell) { height: 44px; background: #f6f9fe; color: #607794; font-size: 12px; font-weight: 700; }
|
||||||
|
.release-table :deep(td.el-table__cell) { height: 60px; color: #526b88; font-size: 12px; }
|
||||||
|
.release-table :deep(.el-table__inner-wrapper::before) { display: none; }
|
||||||
|
.version-cell { display: flex; flex-direction: column; gap: 4px; }
|
||||||
|
.version-cell strong { color: #1c416f; font-size: 13px; }
|
||||||
|
.version-cell small { color: #96a7b9; font-size: 11px; }
|
||||||
|
.strategy { font-size: 11px; font-weight: 600; }
|
||||||
|
.strategy.mandatory { color: #e17646; }
|
||||||
|
.strategy.optional { color: #6084ad; }
|
||||||
|
.hash-text { color: #4176af; cursor: help; font-family: Consolas, monospace; font-size: 11px; }
|
||||||
|
.muted { color: #adbac9; }
|
||||||
|
@media (max-width: 1180px) { .workspace { grid-template-columns: 1fr; } .dropzones { grid-template-columns: 1fr 1fr; } .metadata-dropzone :deep(.el-upload-dragger) { height: 182px; } }
|
||||||
|
@media (max-width: 740px) { .release-page { padding: 14px 12px 28px; } .hero { flex-direction: column; gap: 24px; min-height: 0; padding: 24px; } .hero-side { align-items: stretch; min-width: 0; gap: 15px; } .hero-stats > div { flex: 1; min-width: 0; } .hero-endpoint strong { max-width: 250px; } .dropzones, .form-row { grid-template-columns: 1fr; } .upload-panel, .details-panel, .history-panel { padding: 18px; } .form-footer { flex-direction: column; align-items: stretch; } }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user