update:app版本控制

This commit is contained in:
ltx 2026-11-05 15:08:10 +08:00
parent b07430d3b2
commit 8c6c137f76
4 changed files with 75 additions and 152 deletions

152
README.md
View File

@ -1,151 +1,3 @@
<div align="center">
<img src="https://i.hd-r.cn/6ce52e5724fae609444b5b48bdc4accb.png">
<p align="center">
<a href="https://v3.vuejs.org/" target="_blank">
<img src="https://img.shields.io/badge/vue.js-vue3.x-green" alt="vue">
</a>
<a href="https://element-plus.gitee.io/#/zh-CN/component/changelog" target="_blank">
<img src="https://img.shields.io/badge/element--plus-%3E1.0.0-blue" alt="element plus">
</a>
<a href="https://www.tslang.cn/" target="_blank">
<img src="https://img.shields.io/badge/typescript-%3E4.0.0-blue" alt="typescript">
</a>
<a href="https://vitejs.dev/" target="_blank">
<img src="https://img.shields.io/badge/vite-%3E2.0.0-yellow" alt="vite">
</a>
<a href="https://gitee.com/lyt-top/vue-next-admin/blob/master/LICENSE" target="_blank">
<img src="https://img.shields.io/badge/license-MIT-success" alt="license">
</a>
</p>
<p>&nbsp;</p>
</div>
website-competition 官网页面管理
#### 💝 长期赞助商
<a href="http://www.ccflow.org/" target="_blank">
<img src="./src/assets/ccflowRightNextAdmin.png" width="50%" height="70px">
</a>
#### 🌈 介绍
基于 vue3.x + CompositionAPI setup 语法糖 + typescript + vite + element plus + vue-router-next + pinia 技术,适配手机、平板、pc 的后台开源免费模板,希望减少工作量,帮助大家实现快速开发。
#### ⛱️ 线上预览
- vue3.x 版本预览(vue-next-admin)<a href="https://lyt-top.gitee.io/vue-next-admin-preview/#/login" target="_blank">https://lyt-top.gitee.io/vue-next-admin-preview/#/login</a>
- vue2.x 版本预览(vue-prev-admin)<a href="https://lyt-top.gitee.io/vue-prev-admin-preview/#/login" target="_blank">https://lyt-top.gitee.io/vue-prev-admin-preview/#/login</a>
- vue3.x + uni-app 商城 H5(vue-next-admin-shop)<a href="https://lyt-top.gitee.io/vue-next-admin-shop-preview" target="_blank">https://lyt-top.gitee.io/vue-next-admin-shop-preview</a>
#### 💒 代码仓库
- vue3.x 版本 <a href="https://gitee.com/lyt-top/vue-next-admin" target="_blank">https://gitee.com/lyt-top/vue-next-admin</a>
- vue2.x 版本 <a href="https://gitee.com/lyt-top/vue-next-admin/tree/vue-prev-admin" target="_blank">https://gitee.com/lyt-top/vue-next-admin/tree/vue-prev-admin</a>
#### 🚧 安装 cnpm、yarn
- 复制代码(桌面 cmd 运行) `npm install -g cnpm --registry=https://registry.npm.taobao.org`
- 复制代码(桌面 cmd 运行) `npm install -g yarn`
#### 🏭 环境支持
| Edge | Firefox | Chrome | Safari |
| --------- | ------------ | ----------- | ----------- |
| Edge ≥ 88 | Firefox ≥ 78 | Chrome ≥ 87 | Safari ≥ 13 |
> 由于 Vue3 不再支持 IE11,故而 ElementPlus 也不支持 IE11 及之前版本。
#### ⚡ 使用说明
建议使用 cnpm,因为 yarn 有时会报错。<a href="http://nodejs.cn/" target="_blank">node 版本 > 14.18+/16+</a>
> Vite 不再支持 Node 12 / 13 / 15,因为上述版本已经进入了 EOL 阶段。现在你必须使用 Node 14.18+ / 16+ 版本。
```bash
# 克隆项目
git clone https://gitee.com/lyt-top/vue-next-admin.git
# 进入项目
cd vue-next-admin
# 安装依赖
cnpm install
# 运行项目
cnpm run dev
# 打包发布
cnpm run build
```
#### 📚 开发文档
- 查看开发文档:<a href="https://lyt-top.gitee.io/vue-next-admin-doc-preview" target="_blank">vue-next-admin-doc</a>
#### 💯 学习交流加 QQ 群
> 1 - 4 交流群已满,请加 vue-next-admin 交流群 5
群号:556254895
其它交流群请查看文档首页 [vueNextAdmin 解疑问](https://lyt-top.gitee.io/vue-next-admin-doc-preview/)
#### 💒 集成后端
- <a target="_blank" href="https://gitee.com/zuohuaijun/Admin.NET">@zuohuaijun Admin.NET</a>
- <a target="_blank" href="https://github.com/PandaGoAdmin/PandaX">@熊猫 PandaGoAdmin</a>
- <a target="_blank" href="https://toscode.gitee.com/GionConnection/gopro_free">@甜蜜蜜 GoPro 平台</a>
- <a target="_blank" href="https://gitee.com/GionConnection/niupi-free">@甜蜜蜜 NiuPi 平台</a>
- <a target="_blank" href="https://gitee.com/tiger1103/gfast/tree/os-v3/">@游子 GFast-V3</a>
- <a target="_blank" href="https://gitee.com/diygw/diygw-ui-php/">@diygw.com gw-ui-php</a>
- <a target="_blank" href="https://gitee.com/zsvg/vboot-net">@zsvg vboot-net</a>
- <a target="_blank" href="https://gitee.com/zsvg/vboot-java">@zsvg vboot-java</a>
- <a target="_blank" href="https://gitee.com/wonderful-code/buildadmin">@青红造了个白 buildadmin</a>
- <a target="_blank" href="https://github.com/xiaodingding/iotfast">@Goodwell iotfast(一个开源的物联网平台)</a>
#### ❤️ 鸣谢列表
- <a href="https://github.com/vuejs/vue" target="_blank">vue</a>
- <a href="https://github.com/vuejs/vue-next" target="_blank">vue-next</a>
- <a href="https://github.com/ElemeFE/element" target="_blank">element-ui</a>
- <a href="https://github.com/element-plus/element-plus" target="_blank">element-plus</a>
- <a href="https://github.com/vuejs/vue-router-next" target="_blank">vue-router-next</a>
- <a href="https://github.com/vuejs/pinia" target="_blank">pinia</a>
- <a href="https://github.com/apache/echarts" target="_blank">echarts</a>
- <a href="https://github.com/axios/axios" target="_blank">axios</a>
- <a href="https://github.com/inorganik/countUp.js" target="_blank">countUp</a>
- <a href="https://github.com/developit/mitt" target="_blank">mitt</a>
- <a href="https://github.com/rstacruz/nprogress" target="_blank">nprogress</a>
- <a href="https://github.com/sindresorhus/screenfull.js" target="_blank">screenfull</a>
- <a href="https://github.com/SortableJS/Sortable" target="_blank">sortablejs</a>
- <a href="https://github.com/sass/sass" target="_blank">sass</a>
- <a href="https://github.com/microsoft/TypeScript" target="_blank">typescript</a>
- <a href="https://github.com/vitejs/vite" target="_blank">vite</a>
- <a href="https://github.com/wangeditor-team/wangEditor" target="_blank">wangeditor</a>
- <a href="https://github.com/fengyuanchen/cropperjs" target="_blank">cropperjs</a>
- <a href="https://github.com/crabbly/Print.js" target="_blank">print-js</a>
- <a href="https://github.com/jbaysolutions/vue-grid-layout" target="_blank">vue-grid-layout</a>
- <a href="https://github.com/antoniandre/splitpanes" target="_blank">splitpanes</a>
- <a href="https://github.com/jsplumb/jsplumb" target="_blank">jsplumb</a>
- <a href="https://github.com/hxj9102/table2excel" target="_blank">js-table2excel</a>
- <a href="https://github.com/mmf-fe/vite-plugin-cdn-import" target="_blank">vite-plugin-cdn-import</a>
- <a href="https://github.com/js-cookie/js-cookie" target="_blank">js-cookie</a>
- <a href="https://github.com/davidshimjs/qrcodejs" target="_blank">qrcodejs2-fixes</a>
- <a href="https://github.com/ljharb/qs" target="_blank">qs</a>
- <a href="https://github.com/JamieCurnow/vue-clipboard3" target="_blank">vue-clipboard3</a>
- <a href="https://github.com/intlify/vue-i18n-next" target="_blank">vue-i18n</a>
- <a href="https://github.com/vbenjs/vite-plugin-compression" target="_blank">vite-plugin-compression</a>
- <a href="https://github.com/chenxch/vite-plugin-vue-setup-extend-plus" target="_blank">vite-plugin-vue-setup-extend-plus</a>
#### 💕 特别感谢
特别感谢老哥们的建议、指导与帮忙。谢谢!
- <a href="https://gitee.com/click33/sa-plus" target="_blank">@省长</a>
- <a href="https://gitee.com/jskz/Jskz-SpringCloud" target="_blank">@唐参</a>
- <a href="https://gitee.com/chuange" target="_blank">@川歌</a>
- @华仔
#### 💌 支持作者
如果觉得框架不错,或者已经在使用了,希望你可以去 <a target="_blank" href="https://github.com/lyt-Top/vue-next-admin">Github</a> 或者
<a target="_blank" href="https://gitee.com/lyt-top/vue-next-admin">Gitee</a> 帮我点个 ⭐ Star,这将是对我极大的鼓励与支持。
app-releases app版本管理页面

View File

@ -50,6 +50,16 @@ export async function initBackEndControlRoutes() {
//静态路由
let staticRoute = [
{
path: '/app-releases',
name: 'appReleases',
component: 'app-releases/index',
meta: {
title: '裁判端版本',
isHide: true,
isKeepAlive: false,
},
},
{
path: '/website-competition',
name: 'websiteCompetition',

View File

@ -0,0 +1,59 @@
<template>
<div class="layout-padding release-page">
<el-card>
<template #header><div class="heading"><div><h2>裁判端版本管理</h2><small>仅超级管理员可发布。当前环境:{{ channelLabel }}</small></div><el-button @click="load">刷新列表</el-button></div></template>
<el-alert title="正式签名首次切换无法覆盖旧调试签名 APK;请先完成逐台迁移检查。发布前必须核对版本号、包名和签名指纹。" type="warning" :closable="false" show-icon />
<el-form label-width="130px" class="upload-form">
<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>
<el-form-item label="更新说明(中文)"><el-input v-model="notesZh" type="textarea" :rows="2" maxlength="2000" show-word-limit /></el-form-item>
<el-form-item label="Release notes"><el-input v-model="notesEn" type="textarea" :rows="2" maxlength="2000" show-word-limit /></el-form-item>
<el-form-item label="紧急强制更新"><el-switch v-model="mandatory" /><small class="hint">评分中不打断;断网仍可继续离线评分。</small></el-form-item>
<el-form-item><el-button type="primary" :loading="uploading" :disabled="!file" @click="upload">上传为草稿</el-button></el-form-item>
</el-form>
<el-table v-loading="loading" :data="releases" border stripe>
<el-table-column prop="versionName" label="版本" width="100" />
<el-table-column prop="versionCode" label="版本号" width="100" />
<el-table-column prop="releaseStatus" label="状态" width="100"><template #default="{row}">{{ ['草稿','已发布','已撤回'][row.releaseStatus] || '未知' }}</template></el-table-column>
<el-table-column prop="isMandatory" label="策略" width="100"><template #default="{row}">{{ row.isMandatory ? '强制' : '可选' }}</template></el-table-column>
<el-table-column prop="notesZh" label="更新说明" min-width="180" show-overflow-tooltip />
<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 />
<el-table-column prop="publishedTime" label="发布时间" width="180" />
<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>
</el-table>
</el-card>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
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 };
const releases = ref<Release[]>([]);
const file = ref<File | null>(null);
const notesZh = ref('');
const notesEn = ref('');
const mandatory = ref(false);
const loading = ref(false);
const uploading = ref(false);
const channelLabel = import.meta.env.VITE_API_URL;
function selectFile(event: Event) { file.value = (event.target as HTMLInputElement).files?.[0] || null; }
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() {
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');
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));
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(); }
catch { ElMessage.error('上传失败,请核对 APK、签名和服务器配置'); }
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 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('撤回失败'); } }
onMounted(load);
</script>
<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}
</style>

View File

@ -2,7 +2,7 @@
<div class="launcher-page">
<main class="launcher-shell">
<header><p>SCORING CONSOLE</p><h1>{{ step === 'mode' ? '请选择计分模式' : '请选择赛事' }}</h1><span>{{ step === 'mode' ? '进入系统前,请先选择本次比赛的计分模式' : '选择数据来源和赛事后,各业务菜单将使用同一赛事上下文' }}</span></header>
<header><p>SCORING CONSOLE</p><h1>{{ step === 'mode' ? '请选择计分模式' : '请选择赛事' }}</h1><span>{{ step === 'mode' ? '进入系统前,请先选择本次比赛的计分模式' : '选择数据来源和赛事后,各业务菜单将使用同一赛事上下文' }}</span><div v-if="isAdmin" style="margin-top:14px"><el-button plain type="primary" @click="router.push('/app-releases')">裁判端版本管理</el-button></div></header>
<div v-if="step === 'mode'" class="mode-grid">
<button class="mode-card active" type="button" @click="selectFreestyle"><span class="mode-icon">✦</span><b>花样打分</b><em>Freestyle scoring</em><i>选择赛事 →</i></button>
<button class="mode-card disabled" type="button" disabled><span class="mode-icon">⌁</span><b>速度打分</b><em>Speed scoring</em><i>暂未开放</i></button>
@ -22,12 +22,14 @@
</template>
<script setup>
import { ref } from 'vue';
import { computed, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { workbenchEventsGet } from '/@/api/match/workbench';
import { saveScoringContext } from '/@/utils/scoringContext';
import { useUserInfo } from '/@/stores/userInfo';
const route=useRoute(), router=useRouter(), step=ref('mode'), sourceMode=ref('registration'), keyword=ref(''), events=ref([]), loading=ref(false), pageNo=ref(1), total=ref(0);
const isAdmin=computed(() => useUserInfo().userInfo?.account==='admin');
const pageSize=8;
const requestedSourceMode=String(route.query.sourceMode||'');
if(requestedSourceMode==='orderbook'||requestedSourceMode==='registration')sourceMode.value=requestedSourceMode;