feat: 前端更新改为定时拉取

This commit is contained in:
DLmaster361
2025-09-13 14:54:11 +08:00
parent 79bd982383
commit 6c7a0226fd
8 changed files with 199 additions and 73 deletions

View File

@@ -1 +1,2 @@
VITE_APP_ENV='prod' VITE_APP_ENV='prod'
VITE_APP_VERSION='1.0.1'

View File

@@ -1 +1,2 @@
VITE_APP_ENV='dev' VITE_APP_ENV='dev'
VITE_APP_VERSION='0.9.0'

View File

@@ -3,13 +3,16 @@ import { onMounted, computed } from 'vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import { ConfigProvider } from 'ant-design-vue' import { ConfigProvider } from 'ant-design-vue'
import { useTheme } from './composables/useTheme.ts' import { useTheme } from './composables/useTheme.ts'
import { useUpdateChecker } from './composables/useUpdateChecker.ts'
import AppLayout from './components/AppLayout.vue' import AppLayout from './components/AppLayout.vue'
import TitleBar from './components/TitleBar.vue' import TitleBar from './components/TitleBar.vue'
import UpdateModal from './components/UpdateModal.vue'
import zhCN from 'ant-design-vue/es/locale/zh_CN' import zhCN from 'ant-design-vue/es/locale/zh_CN'
import { logger } from '@/utils/logger' import { logger } from '@/utils/logger'
const route = useRoute() const route = useRoute()
const { antdTheme, initTheme } = useTheme() const { antdTheme, initTheme } = useTheme()
const { updateVisible, updateData, onUpdateConfirmed } = useUpdateChecker()
// 判断是否为初始化页面 // 判断是否为初始化页面
const isInitializationPage = computed(() => route.name === 'Initialization') const isInitializationPage = computed(() => route.name === 'Initialization')
@@ -35,6 +38,13 @@ onMounted(() => {
<TitleBar /> <TitleBar />
<AppLayout /> <AppLayout />
</div> </div>
<!-- 全局更新模态框 -->
<UpdateModal
v-model:visible="updateVisible"
:update-data="updateData"
@confirmed="onUpdateConfirmed"
/>
</ConfigProvider> </ConfigProvider>
</template> </template>

View File

@@ -7,5 +7,9 @@ export type UpdateCheckIn = {
* 当前前端版本号 * 当前前端版本号
*/ */
current_version: string; current_version: string;
/**
* 是否强制拉取更新信息
*/
if_force?: boolean;
}; };

View File

@@ -5,10 +5,11 @@
:width="800" :width="800"
:footer="null" :footer="null"
:mask-closable="false" :mask-closable="false"
:z-index="9999"
class="update-modal" class="update-modal"
> >
<div v-if="hasUpdate" class="update-container"> <div class="update-container">
<!-- 更新内容展示 --> <!-- 更新内容展示 -->
<div class="update-content"> <div class="update-content">
<div <div
@@ -21,7 +22,7 @@
<!-- 操作按钮 --> <!-- 操作按钮 -->
<div class="update-footer"> <div class="update-footer">
<div class="update-actions"> <div class="update-actions">
<a-button v-if="!downloading && !downloaded" @click="visible = false">暂不更新</a-button> <a-button v-if="!downloading && !downloaded" @click="handleCancel">暂不更新</a-button>
<a-button v-if="!downloading && !downloaded" type="primary" @click="downloadUpdate"> <a-button v-if="!downloading && !downloaded" type="primary" @click="downloadUpdate">
下载更新 下载更新
</a-button> </a-button>
@@ -40,19 +41,43 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, ref } from 'vue' import { computed, ref, watch } from 'vue'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import MarkdownIt from 'markdown-it' import MarkdownIt from 'markdown-it'
import { Service } from '@/api/services/Service.ts' import { Service } from '@/api/services/Service.ts'
const visible = ref(false) // Props 定义
interface Props {
visible: boolean
updateData: Record<string, string[]>
}
const props = defineProps<Props>()
// Emits 定义
const emit = defineEmits<{
confirmed: []
'update:visible': [value: boolean]
}>()
// 内部状态
const hasUpdate = ref(false) const hasUpdate = ref(false)
const downloading = ref(false) const downloading = ref(false)
const downloaded = ref(false) const downloaded = ref(false)
const installing = ref(false) const installing = ref(false)
const updateContent = ref("")
const latestVersion = ref("") const latestVersion = ref("")
// 计算属性 - 响应式地接收外部 visible 状态
const visible = computed({
get: () => props.visible,
set: (value: boolean) => emit('update:visible', value)
})
// 计算属性 - 转换 updateData 为 markdown
const updateContent = computed(() => {
return updateInfoToMarkdown(props.updateData, latestVersion.value, '更新内容')
})
// markdown 渲染器 // markdown 渲染器
const md = new MarkdownIt({ html: true, linkify: true, typographer: true }) const md = new MarkdownIt({ html: true, linkify: true, typographer: true })
const renderMarkdown = (content: string) => md.render(content) const renderMarkdown = (content: string) => md.render(content)
@@ -105,28 +130,35 @@ function updateInfoToMarkdown(
return lines.join('\n') return lines.join('\n')
} }
// 初始化弹窗流程 // 初始化时设置 hasUpdate
const initUpdateCheck = async () => { const initializeModal = () => {
try { if (props.updateData && Object.keys(props.updateData).length > 0) {
const version = import.meta.env.VITE_APP_VERSION || '0.0.0' hasUpdate.value = true
const response = await Service.checkUpdateApiUpdateCheckPost({ current_version: version }) // 从 updateData 中提取版本信息(如果有的话)
const versionInfo = Object.values(props.updateData).flat().find(item =>
if (response.code === 200 && response.if_need_update) { typeof item === 'string' && item.includes('版本')
hasUpdate.value = true )
latestVersion.value = response.latest_version || '' if (versionInfo) {
// ✅ 核心修改:把对象转成 Markdown 再给渲染器 const versionMatch = versionInfo.match(/v?(\d+\.\d+\.\d+)/)
updateContent.value = updateInfoToMarkdown( if (versionMatch) {
response.update_info, latestVersion.value = versionMatch[1]
response.latest_version, }
'更新内容'
)
visible.value = true
} }
} catch (err) {
console.error('检查更新失败:', err)
} }
} }
// 监听 props 变化
watch(() => props.updateData, () => {
initializeModal()
}, { immediate: true })
// 监听 visible 变化
watch(() => props.visible, (newVisible) => {
if (newVisible && props.updateData && Object.keys(props.updateData).length > 0) {
hasUpdate.value = true
}
}, { immediate: true })
// 下载更新 // 下载更新
const downloadUpdate = async () => { const downloadUpdate = async () => {
downloading.value = true downloading.value = true
@@ -134,6 +166,7 @@ const downloadUpdate = async () => {
const res = await Service.downloadUpdateApiUpdateDownloadPost() const res = await Service.downloadUpdateApiUpdateDownloadPost()
if (res.code === 200) { if (res.code === 200) {
downloaded.value = true downloaded.value = true
message.success('下载完成')
} else { } else {
message.error(res.message || '下载失败') message.error(res.message || '下载失败')
} }
@@ -153,6 +186,7 @@ const installUpdate = async () => {
if (res.code === 200) { if (res.code === 200) {
message.success('安装启动') message.success('安装启动')
visible.value = false visible.value = false
emit('confirmed')
} else { } else {
message.error(res.message || '安装失败') message.error(res.message || '安装失败')
} }
@@ -164,9 +198,11 @@ const installUpdate = async () => {
} }
} }
onMounted(() => { // 关闭弹窗
initUpdateCheck() const handleCancel = () => {
}) visible.value = false
emit('confirmed')
}
</script> </script>

View File

@@ -0,0 +1,117 @@
import { ref, onMounted, onUnmounted } from 'vue'
import { Service } from '@/api'
import { message } from 'ant-design-vue'
// 获取版本号,优先使用环境变量,否则使用一个测试版本
const version = (import.meta as any).env.VITE_APP_VERSION || '1.0.0'
// 全局状态 - 在所有组件间共享
const updateVisible = ref(false)
const updateData = ref<Record<string, string[]>>({})
// 定时器相关 - 参考顶栏TitleBar.vue的实现
const POLL_MS = 4 * 60 * 60 * 1000 // 4小时
let updateCheckTimer: NodeJS.Timeout | null = null
const isPolling = ref(false)
// 防止重复弹出的状态
let lastShownVersion: string | null = null
export function useUpdateChecker() {
// 执行一次更新检查 - 完全参考顶栏的 pollOnce 逻辑
const pollOnce = async () => {
if (isPolling.value) return
isPolling.value = true
try {
const response = await Service.checkUpdateApiUpdateCheckPost({
current_version: version,
if_force: false, // 定时检查不强制获取,和顶栏一致
})
if (response.code === 200) {
if (response.if_need_update) {
// 检查是否已经有更新弹窗在显示,避免重复弹出
if (updateVisible.value) {
return
}
// 检查是否为同一版本,避免同一版本重复弹出
if (lastShownVersion === response.latest_version) {
return
}
updateData.value = response.update_info
updateVisible.value = true
lastShownVersion = response.latest_version // 记录已显示的版本
}
}
} catch (error: any) {
console.error('[useUpdateChecker] 定时更新检查失败:', error?.message)
} finally {
isPolling.value = false
}
}
// 手动检查更新(用于设置页面按钮)
const checkUpdate = async (silent = false, forceCheck = false) => {
try {
const response = await Service.checkUpdateApiUpdateCheckPost({
current_version: version,
if_force: forceCheck,
})
if (response.code === 200) {
if (response.if_need_update) {
updateData.value = response.update_info
updateVisible.value = true
} else {
if (!silent) {
message.success('暂无更新~')
}
}
} else {
if (!silent) {
message.error(response.message || '获取更新失败')
}
}
} catch (error: any) {
console.error('[useUpdateChecker] 手动更新检查失败:', error?.message)
if (!silent) {
message.error('获取更新失败!')
}
}
}
// 确认回调
const onUpdateConfirmed = () => {
updateVisible.value = false
}
// 组件挂载时启动检查器 - 参考顶栏的 onMounted 逻辑
onMounted(async () => {
// 延迟3秒后再执行首次检查确保后端已经完全启动
setTimeout(async () => {
await pollOnce()
}, 3000)
// 每 4 小时检查一次更新
updateCheckTimer = setInterval(pollOnce, POLL_MS)
})
// 组件卸载时清理定时器 - 参考顶栏的 onBeforeUnmount 逻辑
onUnmounted(() => {
if (updateCheckTimer) {
clearInterval(updateCheckTimer)
updateCheckTimer = null
}
})
return {
updateVisible,
updateData,
checkUpdate,
onUpdateConfirmed
}
}

View File

@@ -25,13 +25,6 @@
@confirmed="onNoticeConfirmed" @confirmed="onNoticeConfirmed"
/> />
<!-- 更新模态框 -->
<UpdateModal
v-model:visible="updateVisible"
:update-data="updateData"
@confirmed="onUpdateConfirmed"
/>
<div class="content"> <div class="content">
<!-- 当期活动关卡 --> <!-- 当期活动关卡 -->
<a-card <a-card
@@ -253,7 +246,6 @@ import { Service } from '@/api/services/Service'
import NoticeModal from '@/components/NoticeModal.vue' import NoticeModal from '@/components/NoticeModal.vue'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { API_ENDPOINTS } from '@/config/mirrors.ts' import { API_ENDPOINTS } from '@/config/mirrors.ts'
import UpdateModal from '@/components/UpdateModal.vue'
interface ActivityInfo { interface ActivityInfo {
Tip: string Tip: string
@@ -308,11 +300,6 @@ const noticeVisible = ref(false)
const noticeData = ref<Record<string, string>>({}) const noticeData = ref<Record<string, string>>({})
const noticeLoading = ref(false) 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(() => { const currentActivity = computed(() => {
if (!activityData.value.length) return null if (!activityData.value.length) return null
@@ -492,35 +479,9 @@ 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(() => { onMounted(() => {
fetchActivityData() fetchActivityData()
fetchNoticeData() fetchNoticeData()
checkUpdate()
}) })
</script> </script>
@@ -549,11 +510,6 @@ onMounted(() => {
min-width: 120px; min-width: 120px;
} }
/* 公告相关样式 */
.notice-modal {
/* 自定义公告模态框样式 */
}
.activity-card { .activity-card {
margin-bottom: 24px; margin-bottom: 24px;
} }

View File

@@ -221,6 +221,7 @@ const checkUpdate = async () => {
try { try {
const response = await Service.checkUpdateApiUpdateCheckPost({ const response = await Service.checkUpdateApiUpdateCheckPost({
current_version: version, current_version: version,
if_force: true, // 手动检查强制获取最新信息
}) })
if (response.code === 200) { if (response.code === 200) {
if (response.if_need_update) { if (response.if_need_update) {
@@ -234,7 +235,7 @@ const checkUpdate = async () => {
} }
} catch (error) { } catch (error) {
console.error('获取更新失败:', error) console.error('获取更新失败:', error)
return '获取更新失败!' message.error('获取更新失败!')
} }
} }