feat: 前端更新改为定时拉取
This commit is contained in:
@@ -1 +1,2 @@
|
|||||||
VITE_APP_ENV='prod'
|
VITE_APP_ENV='prod'
|
||||||
|
VITE_APP_VERSION='1.0.1'
|
||||||
@@ -1 +1,2 @@
|
|||||||
VITE_APP_ENV='dev'
|
VITE_APP_ENV='dev'
|
||||||
|
VITE_APP_VERSION='0.9.0'
|
||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -7,5 +7,9 @@ export type UpdateCheckIn = {
|
|||||||
* 当前前端版本号
|
* 当前前端版本号
|
||||||
*/
|
*/
|
||||||
current_version: string;
|
current_version: string;
|
||||||
|
/**
|
||||||
|
* 是否强制拉取更新信息
|
||||||
|
*/
|
||||||
|
if_force?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
117
frontend/src/composables/useUpdateChecker.ts
Normal file
117
frontend/src/composables/useUpdateChecker.ts
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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('获取更新失败!')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user