feat: 新增更新弹窗,以实现更新的下载

This commit is contained in:
2025-09-10 20:18:11 +08:00
parent d9aa6305da
commit 6138fc47b1
3 changed files with 450 additions and 114 deletions

View File

@@ -0,0 +1,234 @@
<template>
<a-modal
v-model:open="visible"
:title="`发现新版本 ${latestVersion || ''}`"
:width="800"
:footer="null"
:mask-closable="false"
class="update-modal"
>
<div v-if="hasUpdate" class="update-container">
<!-- 更新内容展示 -->
<div class="update-content">
<div
ref="markdownContentRef"
class="markdown-content"
v-html="renderMarkdown(updateContent)"
></div>
</div>
<!-- 操作按钮 -->
<div class="update-footer">
<div class="update-actions">
<a-button v-if="!downloading && !downloaded" @click="visible = false">暂不更新</a-button>
<a-button v-if="!downloading && !downloaded" type="primary" @click="downloadUpdate">
下载更新
</a-button>
<a-button v-if="downloading" type="primary" :loading="true" disabled>
下载中...后端进度
</a-button>
<a-button v-if="downloaded" type="primary" :loading="installing" @click="installUpdate">
{{ installing ? '正在安装...' : '立即安装' }}
</a-button>
</div>
</div>
</div>
</a-modal>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { message } from 'ant-design-vue'
import MarkdownIt from 'markdown-it'
import { Service } from '@/api/services/Service.ts'
const visible = ref(false)
const hasUpdate = ref(false)
const downloading = ref(false)
const downloaded = ref(false)
const installing = ref(false)
const updateContent = ref("")
const latestVersion = ref("")
// markdown 渲染器
const md = new MarkdownIt({ html: true, linkify: true, typographer: true })
const renderMarkdown = (content: string) => md.render(content)
/** 将接口的 update_info 对象转成 Markdown 文本 */
function updateInfoToMarkdown(
info: unknown,
version?: string,
header = '更新内容'
): string {
// 如果后端直接给了字符串,直接返回
if (typeof info === 'string') return info
if (!info || typeof info !== 'object') return ''
const obj = info as Record<string, unknown>
const lines: string[] = []
// 顶部标题
if (version) {
lines.push(`### ${version} ${header}`)
} else {
lines.push(`### ${header}`)
}
lines.push('') // 空行
// 希望按这个顺序展示;其余未知键追加在后
const preferredOrder = ['修复BUG', '程序优化', '新增功能']
const keys = Array.from(
new Set([...preferredOrder, ...Object.keys(obj)])
)
for (const key of keys) {
const val = obj[key]
if (Array.isArray(val) && val.length > 0) {
lines.push(`#### ${key}`)
for (const item of val) {
// 防御:数组里既可能是字符串也可能是对象
if (typeof item === 'string') {
lines.push(`- ${item}`)
} else {
// 兜底:把对象友好地 stringify去掉引号
lines.push(`- ${JSON.stringify(item, null, 0)}`)
}
}
lines.push('') // 每段之间空一行
}
}
return lines.join('\n')
}
// 初始化弹窗流程
const initUpdateCheck = async () => {
try {
const version = import.meta.env.VITE_APP_VERSION || '0.0.0'
const response = await Service.checkUpdateApiUpdateCheckPost({ current_version: version })
if (response.code === 200 && response.if_need_update) {
hasUpdate.value = true
latestVersion.value = response.latest_version || ''
// ✅ 核心修改:把对象转成 Markdown 再给渲染器
updateContent.value = updateInfoToMarkdown(
response.update_info,
response.latest_version,
'更新内容'
)
visible.value = true
}
} catch (err) {
console.error('检查更新失败:', err)
}
}
// 下载更新
const downloadUpdate = async () => {
downloading.value = true
try {
const res = await Service.downloadUpdateApiUpdateDownloadPost()
if (res.code === 200) {
downloaded.value = true
} else {
message.error(res.message || '下载失败')
}
} catch (err) {
console.error('下载更新失败:', err)
message.error('下载更新失败')
} finally {
downloading.value = false
}
}
// 安装更新
const installUpdate = async () => {
installing.value = true
try {
const res = await Service.installUpdateApiUpdateInstallPost()
if (res.code === 200) {
message.success('安装启动')
visible.value = false
} else {
message.error(res.message || '安装失败')
}
} catch (err) {
console.error('安装失败:', err)
message.error('安装失败')
} finally {
installing.value = false
}
}
onMounted(() => {
initUpdateCheck()
})
</script>
<style scoped>
.update-modal :deep(.ant-modal-body) {
padding: 16px 24px;
max-height: 70vh;
overflow: hidden;
}
.update-container {
display: flex;
flex-direction: column;
height: 60vh;
}
.update-content {
flex: 1;
overflow-y: auto;
padding-right: 12px;
}
/* Firefox细滚动条 & 低对比 */
:deep(.update-content) {
scrollbar-width: thin;
scrollbar-color: rgba(255,255,255,0.14) transparent; /* 拇指颜色 / 轨道颜色 */
}
/* WebKitChrome/Edge细、半透明、悬停时稍亮 */
:deep(.update-content::-webkit-scrollbar) {
width: 8px; /* 滚动条更细 */
}
:deep(.update-content::-webkit-scrollbar-track) {
background: transparent; /* 轨道透明,不显眼 */
}
:deep(.update-content::-webkit-scrollbar-thumb) {
background: rgba(255,255,255,0.12); /* 深色模式下更淡 */
border-radius: 8px;
border: 2px solid transparent;
background-clip: padding-box; /* 让边缘更柔和 */
}
/* 悬停时略微提升对比度,便于发现 */
:deep(.update-content:hover::-webkit-scrollbar-thumb) {
background: rgba(255,255,255,0.22);
}
.markdown-content {
line-height: 1.6;
color: var(--ant-color-text);
}
.update-footer {
display: flex;
justify-content: flex-end;
margin-top: 16px;
border-top: 1px solid var(--ant-color-border);
padding-top: 12px;
}
.update-actions {
display: flex;
gap: 10px;
}
</style>

View File

@@ -25,6 +25,13 @@
@confirmed="onNoticeConfirmed"
/>
<!-- 更新模态框 -->
<UpdateModal
v-model:visible="updateVisible"
:update-data="updateData"
@confirmed="onUpdateConfirmed"
/>
<div class="content">
<!-- 当期活动关卡 -->
<a-card
@@ -33,7 +40,6 @@
class="activity-card"
:loading="loading"
>
<div v-if="error" class="error-message">
<a-alert :message="error" type="error" show-icon closable @close="error = ''" />
</div>
@@ -242,15 +248,12 @@
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
import { message } from 'ant-design-vue'
import {
ClockCircleOutlined,
UserOutlined,
BellOutlined,
} from '@ant-design/icons-vue'
import { ClockCircleOutlined, UserOutlined, BellOutlined } from '@ant-design/icons-vue'
import { Service } from '@/api/services/Service'
import NoticeModal from '@/components/NoticeModal.vue'
import dayjs from 'dayjs'
import { API_ENDPOINTS } from '@/config/mirrors.ts'
import UpdateModal from '@/components/UpdateModal.vue'
interface ActivityInfo {
Tip: string
@@ -305,6 +308,11 @@ const noticeVisible = ref(false)
const noticeData = ref<Record<string, string>>({})
const noticeLoading = ref(false)
// 更新相关
const version = import.meta.env.VITE_APP_VERSION || '获取版本失败!'
const updateVisible = ref(false)
const updateData = ref<Record<string, string[]>>({})
// 获取当前活动信息
const currentActivity = computed(() => {
if (!activityData.value.length) return null
@@ -358,35 +366,6 @@ const getActivityTimeStatus = (expireTime: string): 'normal' | 'warning' | 'ende
}
// 获取倒计时样式 - 如果剩余时间小于2天则显示红色
const getCountdownStyle = (expireTime: string) => {
try {
const expire = new Date(expireTime)
const now = new Date()
const remaining = expire.getTime() - now.getTime()
const twoDaysInMs = 2 * 24 * 60 * 60 * 1000
if (remaining <= twoDaysInMs) {
return {
color: '#ff4d4f',
fontWeight: 'bold',
fontSize: '18px',
}
}
return {
color: 'var(--ant-color-text)',
fontWeight: '600',
fontSize: '20px',
}
} catch {
return {
color: 'var(--ant-color-text)',
fontWeight: '600',
fontSize: '20px',
}
}
}
const getProxyTimestamp = (dateStr: string) => {
if (!dateStr) return Date.now()
@@ -513,9 +492,35 @@ const showNotice = async () => {
}
}
const checkUpdate = async () => {
try {
const response = await Service.checkUpdateApiUpdateCheckPost({
current_version: version,
})
if (response.code === 200) {
if (response.if_need_update) {
updateData.value = response.update_info
updateVisible.value = true
} else {
}
} else {
message.error(response.message || '获取更新失败')
}
} catch (error) {
console.error('获取更新失败:', error)
return '获取更新失败!'
}
}
// 确认回调
const onUpdateConfirmed = () => {
updateVisible.value = false
}
onMounted(() => {
fetchActivityData()
fetchNoticeData()
checkUpdate()
})
</script>

View File

@@ -5,7 +5,7 @@ import {
QuestionCircleOutlined,
HomeOutlined,
GithubOutlined,
QqOutlined
QqOutlined,
} from '@ant-design/icons-vue'
import { message } from 'ant-design-vue'
import type { ThemeColor, ThemeMode } from '../composables/useTheme'
@@ -13,6 +13,9 @@ import { useTheme } from '../composables/useTheme.ts'
import { useSettingsApi } from '../composables/useSettingsApi'
import type { SelectValue } from 'ant-design-vue/es/select'
import type { SettingsData } from '../types/settings'
import { Service, type VersionOut } from '@/api'
const app_version = import.meta.env.VITE_APP_VERSION || '获取版本失败!'
const router = useRouter()
const { themeMode, themeColor, themeColors, setThemeMode, setThemeColor } = useTheme()
@@ -20,6 +23,8 @@ const { loading, getSettings, updateSettings } = useSettingsApi()
const activeKey = ref('basic')
const backendUpdateInfo = ref<VersionOut | null>(null)
const settings = reactive<SettingsData>({
UI: {
IfShowTray: false,
@@ -64,7 +69,7 @@ const settings = reactive<SettingsData>({
Source: 'GitHub',
ProxyAddress: '',
MirrorChyanCDK: '',
}
},
})
// 选项配置
@@ -176,6 +181,15 @@ const handleSettingChange = async (category: keyof SettingsData, key: string, va
}
}
const getBackendVersion = async () => {
try {
backendUpdateInfo.value = await Service.getGitVersionApiInfoVersionPost()
} catch (error) {
console.error('Failed to get backend version:', error)
return '获取后端版本失败!'
}
}
const handleThemeModeChange = (e: any) => {
setThemeMode(e.target.value as ThemeMode)
}
@@ -198,6 +212,7 @@ const openDevTools = () => {
onMounted(() => {
loadSettings()
getBackendVersion()
})
</script>
@@ -336,7 +351,10 @@ onMounted(() => {
</div>
<a-select
v-model:value="settings.Function.HistoryRetentionTime"
@change="(value: any) => handleSettingChange('Function', 'HistoryRetentionTime', value)"
@change="
(value: any) =>
handleSettingChange('Function', 'HistoryRetentionTime', value)
"
:options="historyRetentionOptions"
size="large"
style="width: 100%"
@@ -353,7 +371,9 @@ onMounted(() => {
</div>
<a-select
v-model:value="settings.Function.IfAllowSleep"
@change="(checked: any) => handleSettingChange('Function', 'IfAllowSleep', checked)"
@change="
(checked: any) => handleSettingChange('Function', 'IfAllowSleep', checked)
"
size="large"
style="width: 100%"
>
@@ -374,7 +394,9 @@ onMounted(() => {
</div>
<a-select
v-model:value="settings.Function.IfSilence"
@change="(checked: any) => handleSettingChange('Function', 'IfSilence', checked)"
@change="
(checked: any) => handleSettingChange('Function', 'IfSilence', checked)
"
size="large"
style="width: 100%"
>
@@ -387,7 +409,9 @@ onMounted(() => {
<div class="form-item-vertical">
<div class="form-label-wrapper">
<span class="form-label">模拟器老板键</span>
<a-tooltip title="程序依靠模拟器老板键隐藏模拟器窗口,需要开启静默模式后才能填写,请直接输入文字,多个键位之间请用「+」隔开">
<a-tooltip
title="程序依靠模拟器老板键隐藏模拟器窗口,需要开启静默模式后才能填写,请直接输入文字,多个键位之间请用「+」隔开"
>
<QuestionCircleOutlined class="help-icon" />
</a-tooltip>
</div>
@@ -408,9 +432,11 @@ onMounted(() => {
<span class="form-label">托管Bilibili游戏隐私政策</span>
<a-tooltip>
<template #title>
<div style="max-width: 300px;">
<p>开启本项即代表您已完整阅读并同意以下协议并授权本程序在其认定需要时以其认定合适的方法替您处理相关弹窗</p>
<ul style="margin: 8px 0; padding-left: 16px;">
<div style="max-width: 300px">
<p>
开启本项即代表您已完整阅读并同意以下协议并授权本程序在其认定需要时以其认定合适的方法替您处理相关弹窗
</p>
<ul style="margin: 8px 0; padding-left: 16px">
<li>
<a
href="https://www.bilibili.com/protocal/licence.html"
@@ -449,7 +475,10 @@ onMounted(() => {
</div>
<a-select
v-model:value="settings.Function.IfAgreeBilibili"
@change="(checked: any) => handleSettingChange('Function', 'IfAgreeBilibili', checked)"
@change="
(checked: any) =>
handleSettingChange('Function', 'IfAgreeBilibili', checked)
"
size="large"
style="width: 100%"
>
@@ -468,7 +497,10 @@ onMounted(() => {
</div>
<a-select
v-model:value="settings.Function.IfSkipMumuSplashAds"
@change="(checked: any) => handleSettingChange('Function', 'IfSkipMumuSplashAds', checked)"
@change="
(checked: any) =>
handleSettingChange('Function', 'IfSkipMumuSplashAds', checked)
"
size="large"
style="width: 100%"
>
@@ -500,7 +532,9 @@ onMounted(() => {
</div>
<a-select
v-model:value="settings.Notify.SendTaskResultTime"
@change="(value: any) => handleSettingChange('Notify', 'SendTaskResultTime', value)"
@change="
(value: any) => handleSettingChange('Notify', 'SendTaskResultTime', value)
"
:options="sendTaskResultTimeOptions"
size="large"
style="width: 100%"
@@ -517,7 +551,9 @@ onMounted(() => {
</div>
<a-select
v-model:value="settings.Notify.IfSendStatistic"
@change="(checked: any) => handleSettingChange('Notify', 'IfSendStatistic', checked)"
@change="
(checked: any) => handleSettingChange('Notify', 'IfSendStatistic', checked)
"
size="large"
style="width: 100%"
>
@@ -536,7 +572,9 @@ onMounted(() => {
</div>
<a-select
v-model:value="settings.Notify.IfSendSixStar"
@change="(checked: any) => handleSettingChange('Notify', 'IfSendSixStar', checked)"
@change="
(checked: any) => handleSettingChange('Notify', 'IfSendSixStar', checked)
"
size="large"
style="width: 100%"
>
@@ -563,7 +601,9 @@ onMounted(() => {
</div>
<a-select
v-model:value="settings.Notify.IfPushPlyer"
@change="(checked: any) => handleSettingChange('Notify', 'IfPushPlyer', checked)"
@change="
(checked: any) => handleSettingChange('Notify', 'IfPushPlyer', checked)
"
size="large"
style="width: 100%"
>
@@ -598,7 +638,9 @@ onMounted(() => {
</div>
<a-select
v-model:value="settings.Notify.IfSendMail"
@change="(checked: any) => handleSettingChange('Notify', 'IfSendMail', checked)"
@change="
(checked: any) => handleSettingChange('Notify', 'IfSendMail', checked)
"
size="large"
style="width: 100%"
>
@@ -617,7 +659,13 @@ onMounted(() => {
</div>
<a-input
v-model:value="settings.Notify.SMTPServerAddress"
@blur="handleSettingChange('Notify', 'SMTPServerAddress', settings.Notify.SMTPServerAddress)"
@blur="
handleSettingChange(
'Notify',
'SMTPServerAddress',
settings.Notify.SMTPServerAddress
)
"
:disabled="!settings.Notify.IfSendMail"
placeholder="请输入发信邮箱SMTP服务器地址"
size="large"
@@ -636,7 +684,9 @@ onMounted(() => {
</div>
<a-input
v-model:value="settings.Notify.FromAddress"
@blur="handleSettingChange('Notify', 'FromAddress', settings.Notify.FromAddress)"
@blur="
handleSettingChange('Notify', 'FromAddress', settings.Notify.FromAddress)
"
:disabled="!settings.Notify.IfSendMail"
placeholder="请输入发信邮箱地址"
size="large"
@@ -653,7 +703,13 @@ onMounted(() => {
</div>
<a-input-password
v-model:value="settings.Notify.AuthorizationCode"
@blur="handleSettingChange('Notify', 'AuthorizationCode', settings.Notify.AuthorizationCode)"
@blur="
handleSettingChange(
'Notify',
'AuthorizationCode',
settings.Notify.AuthorizationCode
)
"
:disabled="!settings.Notify.IfSendMail"
placeholder="请输入发信邮箱授权码"
size="large"
@@ -682,7 +738,6 @@ onMounted(() => {
</a-row>
</div>
<div class="form-section">
<div class="section-header">
<h3>Server酱通知</h3>
@@ -702,16 +757,16 @@ onMounted(() => {
<span class="form-label">启用Server酱通知</span>
<a-tooltip>
<template #title>
<div>
使用Server酱推送通知
</div>
<div>使用Server酱推送通知</div>
</template>
<QuestionCircleOutlined class="help-icon" />
</a-tooltip>
</div>
<a-select
v-model:value="settings.Notify.IfServerChan"
@change="(checked: any) => handleSettingChange('Notify', 'IfServerChan', checked)"
@change="
(checked: any) => handleSettingChange('Notify', 'IfServerChan', checked)
"
size="large"
style="width: 100%"
>
@@ -733,7 +788,13 @@ onMounted(() => {
</div>
<a-input
v-model:value="settings.Notify.ServerChanKey"
@blur="handleSettingChange('Notify', 'ServerChanKey', settings.Notify.ServerChanKey)"
@blur="
handleSettingChange(
'Notify',
'ServerChanKey',
settings.Notify.ServerChanKey
)
"
:disabled="!settings.Notify.IfServerChan"
placeholder="请输入Server酱SendKey"
size="large"
@@ -766,7 +827,10 @@ onMounted(() => {
</div>
<a-select
v-model:value="settings.Notify.IfCompanyWebHookBot"
@change="(checked: any) => handleSettingChange('Notify', 'IfCompanyWebHookBot', checked)"
@change="
(checked: any) =>
handleSettingChange('Notify', 'IfCompanyWebHookBot', checked)
"
size="large"
style="width: 100%"
>
@@ -785,7 +849,13 @@ onMounted(() => {
</div>
<a-input
v-model:value="settings.Notify.CompanyWebHookBotUrl"
@blur="handleSettingChange('Notify', 'CompanyWebHookBotUrl', settings.Notify.CompanyWebHookBotUrl)"
@blur="
handleSettingChange(
'Notify',
'CompanyWebHookBotUrl',
settings.Notify.CompanyWebHookBotUrl
)
"
:disabled="!settings.Notify.IfCompanyWebHookBot"
placeholder="请输入Webhook URL"
size="large"
@@ -815,7 +885,9 @@ onMounted(() => {
</div>
<a-select
v-model:value="settings.Update.IfAutoUpdate"
@change="(checked: any) => handleSettingChange('Update', 'IfAutoUpdate', checked)"
@change="
(checked: any) => handleSettingChange('Update', 'IfAutoUpdate', checked)
"
size="large"
style="width: 100%"
>
@@ -864,13 +936,17 @@ onMounted(() => {
<div class="form-item-vertical">
<div class="form-label-wrapper">
<span class="form-label">网络代理地址</span>
<a-tooltip title="使用网络代理软件时,若出现网络连接问题,请尝试设置代理地址,此设置全局生效">
<a-tooltip
title="使用网络代理软件时,若出现网络连接问题,请尝试设置代理地址,此设置全局生效"
>
<QuestionCircleOutlined class="help-icon" />
</a-tooltip>
</div>
<a-input
v-model:value="settings.Update.ProxyAddress"
@blur="handleSettingChange('Update', 'ProxyAddress', settings.Update.ProxyAddress)"
@blur="
handleSettingChange('Update', 'ProxyAddress', settings.Update.ProxyAddress)
"
placeholder="请输入网络代理地址"
size="large"
/>
@@ -900,7 +976,13 @@ onMounted(() => {
</div>
<a-input
v-model:value="settings.Update.MirrorChyanCDK"
@blur="handleSettingChange('Update', 'MirrorChyanCDK', settings.Update.MirrorChyanCDK)"
@blur="
handleSettingChange(
'Update',
'MirrorChyanCDK',
settings.Update.MirrorChyanCDK
)
"
:disabled="settings.Update.Source !== 'MirrorChyan'"
placeholder="使用Mirror源时请输入Mirror酱CDK"
size="large"
@@ -930,7 +1012,9 @@ onMounted(() => {
</div>
<a-select
v-model:value="settings.Start.IfSelfStart"
@change="(checked: any) => handleSettingChange('Start', 'IfSelfStart', checked)"
@change="
(checked: any) => handleSettingChange('Start', 'IfSelfStart', checked)
"
size="large"
style="width: 100%"
>
@@ -949,7 +1033,10 @@ onMounted(() => {
</div>
<a-select
v-model:value="settings.Start.IfMinimizeDirectly"
@change="(checked: any) => handleSettingChange('Start', 'IfMinimizeDirectly', checked)"
@change="
(checked: any) =>
handleSettingChange('Start', 'IfMinimizeDirectly', checked)
"
size="large"
style="width: 100%"
>
@@ -1023,12 +1110,8 @@ onMounted(() => {
<a-row :gutter="24">
<a-col :span="24">
<a-space size="large">
<a-button type="primary" @click="goToLogs" size="large">
查看日志
</a-button>
<a-button @click="openDevTools" size="large">
打开开发者工具
</a-button>
<a-button type="primary" @click="goToLogs" size="large"> 查看日志 </a-button>
<a-button @click="openDevTools" size="large"> 打开开发者工具 </a-button>
</a-space>
</a-col>
</a-row>
@@ -1052,11 +1135,7 @@ onMounted(() => {
<div class="link-content">
<h4>软件官网</h4>
<p>查看最新版本和功能介绍</p>
<a
href="https://auto-mas.top"
target="_blank"
class="link-button"
>
<a href="https://auto-mas.top" target="_blank" class="link-button">
访问官网
</a>
</div>
@@ -1088,11 +1167,7 @@ onMounted(() => {
<div class="link-content">
<h4>用户QQ群</h4>
<p>加入社区获取帮助和交流</p>
<a
href="https://qm.qq.com/q/bd9fISNoME"
target="_blank"
class="link-button"
>
<a href="https://qm.qq.com/q/bd9fISNoME" target="_blank" class="link-button">
加入群聊
</a>
</div>
@@ -1113,7 +1188,7 @@ onMounted(() => {
</div>
<div class="info-item">
<span class="info-label">当前版本</span>
<span class="info-value">v1.0.0</span>
<span class="info-value">{{app_version}}</span>
</div>
</a-col>
<a-col :span="12">
@@ -1127,6 +1202,28 @@ onMounted(() => {
</div>
</a-col>
</a-row>
<a-row :gutter="24">
<a-col :span="12">
<div class="info-item">
<span class="info-label">当前后端版本</span>
<span class="info-value">{{backendUpdateInfo?.current_version}}</span>
</div>
<div class="info-item">
<span class="info-label">当前后端哈希值</span>
<span class="info-value">{{backendUpdateInfo?.current_hash}}</span>
</div>
</a-col>
<a-col :span="12">
<div class="info-item">
<span class="info-label">当前时间</span>
<span class="info-value">{{backendUpdateInfo?.current_time}}</span>
</div>
<div class="info-item">
<span class="info-label">是否需要更新</span>
<span class="info-value">{{backendUpdateInfo?.if_need_update}}</span>
</div>
</a-col>
</a-row>
</div>
</div>
</a-tab-pane>