重构:Wails v3 迁移 + 前端目录规范化 + Sidebar滚动优化

- web/ → frontend/ 目录重命名(Wails v3 标准结构)
- main.go: Middleware 修复 custom.js 404 + DevTools 延迟启动
- Sidebar: 收藏夹内部独立滚动 + 帮助区块固定底部
- useFavorites.ts: longPressTimer const→let 修复 TypeError
- App.vue: Arco Tabs padding-top 覆盖
- build: config.yml / Taskfile.yml 对齐官方模板,devtools build tag
- 新增 v3 bindings、vite.config.js、跨平台构建配置

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
lxy
2026-05-01 11:03:53 +08:00
co-authored by Claude Opus 4.7
parent 44847e0d40
commit f54bf1c28d
185 changed files with 7768 additions and 914 deletions
@@ -0,0 +1,324 @@
<template>
<!-- 升级提示弹窗 -->
</template>
<script setup>
import { computed, watch, onMounted, onUnmounted, h, nextTick } from 'vue'
import { Modal, Message, Progress } from '@arco-design/web-vue'
import { useUpdateStore } from '../stores/update'
import { marked } from '../utils/markedExtensions'
import { sanitizeHtml } from '@/utils/fileUtils'
import { DownloadUpdate } from '../wailsjs/v3-bindings/u-desk/app'
import { On, Off } from '@wailsio/events'
const props = defineProps({
modelValue: {
type: Boolean,
default: false
},
updateInfo: {
type: Object,
default: () => ({})
}
})
const emit = defineEmits(['update:modelValue'])
// 使用更新管理 store
const updateStore = useUpdateStore()
// 模态框实例
let confirmModalInstance = null
let progressModalInstance = null
// Computed
const currentVersion = computed(() => props.updateInfo?.current_version || '0.1.0')
const latestVersion = computed(() => props.updateInfo?.latest_version || '')
const changelog = computed(() => props.updateInfo?.changelog || '')
const forceUpdate = computed(() => props.updateInfo?.force_update || false)
// Watch
watch(() => props.modelValue, (val) => {
if (val) {
showUpdateModal()
} else {
closeModals()
}
})
// Utility functions
const formatDate = (dateStr) => {
if (!dateStr) return ''
try {
const date = new Date(dateStr)
if (!isNaN(date.getTime())) {
return date.toLocaleDateString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit'
}).replace(/\//g, '-')
}
} catch {}
return dateStr
}
// 显示更新确认弹窗
const showUpdateModal = () => {
if (confirmModalInstance) return
confirmModalInstance = Modal.confirm({
title: forceUpdate.value ? '重要更新' : '发现新版本',
content: () => {
const elements = [
h('div', { style: { marginBottom: '8px' } }, [
h('span', { style: { fontSize: '13px', color: 'var(--color-text-2)' } }, '版本:'),
h('span', { style: { fontSize: '14px', color: 'var(--color-text-1)', marginLeft: '8px' } }, currentVersion.value),
h('span', { style: { fontSize: '14px', color: 'var(--color-text-2)', marginLeft: '12px', marginRight: '12px' } }, '→'),
h('span', { style: { fontSize: '16px', color: 'rgb(var(--primary-6))', fontWeight: '500' } }, latestVersion.value)
])
]
// 更新日志
if (changelog.value) {
const changelogHtml = (() => { try { return sanitizeHtml(String(marked.parse(changelog.value))) } catch { return changelog.value } })()
elements.push(
h('div', { style: { marginBottom: '8px' } }, [
h('div', { style: { fontSize: '12px', color: 'var(--color-text-2)', marginBottom: '4px' } }, '更新内容:'),
h('div', {
style: {
fontSize: '12px',
color: 'var(--color-text-2)',
lineHeight: '1.6',
padding: '10px 12px',
background: 'var(--color-fill-1)',
borderRadius: '4px',
maxHeight: '240px',
overflowY: 'auto'
},
innerHTML: changelogHtml
})
])
)
}
// 发布日期和文件大小
const metadata = []
if (props.updateInfo?.release_date) {
metadata.push(formatDate(props.updateInfo.release_date))
}
if (props.updateInfo?.file_size) {
metadata.push(updateStore.formatFileSize(props.updateInfo.file_size))
}
if (metadata.length > 0) {
elements.push(
h('div', { style: { marginBottom: '4px', fontSize: '12px', color: 'var(--color-text-3)' } }, metadata.join(' · '))
)
}
// 强制更新提示
if (forceUpdate.value) {
elements.push(
h('div', {
style: {
marginTop: '12px',
padding: '12px',
background: 'var(--color-danger-light-1)',
border: '1px solid var(--color-danger-light-3)',
borderRadius: '4px',
color: 'rgb(var(--danger-6))',
fontSize: '13px'
}
}, '⚠️ 此版本包含重要的安全更新和问题修复,为保障正常使用,请完成更新后再继续。')
)
}
return elements
},
okText: '立即更新',
cancelText: '稍后提醒',
closable: !forceUpdate.value,
maskClosable: !forceUpdate.value,
onOk: async () => {
confirmModalInstance = null
await handleDownload()
},
onCancel: () => {
confirmModalInstance = null
emit('update:modelValue', false)
},
onBeforeCancel: () => {
if (forceUpdate.value) {
Message.warning('此版本为强制更新,无法跳过')
return false
}
return true
}
})
}
// 生成进度弹窗内容
const getProgressModalContent = () => {
// 下载中状态
if (updateStore.downloading) {
const progressValue = Math.min(100, Math.max(0, updateStore.downloadProgress || 0))
const finalProgress = progressValue / 100
const { downloaded, total, speed } = updateStore.progressInfo
const sizeText = total > 0
? `${updateStore.formatFileSize(downloaded)} / ${updateStore.formatFileSize(total)}`
: updateStore.downloadProgress > 0 ? '计算文件大小...' : '准备下载...'
const speedElement = speed > 0
? h('div', { style: { fontSize: '12px', color: 'var(--color-text-3)', marginTop: '4px' } },
`下载速度: ${updateStore.formatSpeed(speed)}`
)
: null
return [
h('div', { style: { marginBottom: '16px' } }, [
h('div', { style: { marginBottom: '8px', fontSize: '14px', color: 'var(--color-text-2)' } }, '正在下载更新包...')
]),
h('div', { style: { marginBottom: '8px' } }, [
h(Progress, { percent: finalProgress, showText: true })
]),
h('div', { style: { fontSize: '12px', color: 'var(--color-text-3)', marginTop: '8px' } }, sizeText),
speedElement
]
}
// 安装中状态
if (updateStore.installing) {
return [
h('div', { style: { marginBottom: '16px' } }, [
h('div', { style: { marginBottom: '8px', fontSize: '14px', color: 'var(--color-text-2)' } }, '正在安装更新...')
]),
h('div', { style: { fontSize: '12px', color: 'var(--color-text-3)' } }, '请稍候,应用将在安装完成后自动重启...')
]
}
// 完成状态
return [
h('div', { style: { marginBottom: '16px', textAlign: 'center' } }, [
h('div', { style: { fontSize: '16px', color: 'rgb(var(--success-6))', marginBottom: '8px' } }, '✓ 更新完成')
]),
h('div', { style: { fontSize: '14px', color: 'var(--color-text-2)' } }, '应用将在几秒后自动重启...')
]
}
// 更新进度弹窗内容
const updateProgressModal = async () => {
if (!progressModalInstance) return
await nextTick()
progressModalInstance.update({
content: () => getProgressModalContent()
})
}
// 显示进度弹窗
const showProgressModal = async () => {
if (progressModalInstance) {
progressModalInstance.close()
progressModalInstance = null
}
progressModalInstance = Modal.info({
title: '更新进度',
content: () => getProgressModalContent(),
closable: false,
maskClosable: false,
footer: false
})
await nextTick()
// 监听 store 状态变化
const stopWatcher = watch(
[
() => updateStore.downloadProgress,
() => updateStore.downloading,
() => updateStore.installing,
() => updateStore.progressInfo.total,
() => updateStore.progressInfo.downloaded,
() => updateStore.progressInfo.speed
],
async () => {
await nextTick(updateProgressModal)
},
{ immediate: true, flush: 'post' }
)
if (progressModalInstance) {
progressModalInstance._stopWatcher = stopWatcher
}
}
// 关闭进度弹窗
const closeProgressModal = () => {
if (progressModalInstance) {
if (progressModalInstance._stopWatcher) {
progressModalInstance._stopWatcher()
delete progressModalInstance._stopWatcher
}
progressModalInstance.close()
progressModalInstance = null
}
}
// 关闭所有弹窗
const closeModals = () => {
if (confirmModalInstance) {
confirmModalInstance.close()
confirmModalInstance = null
}
closeProgressModal()
}
// 下载更新
const handleDownload = async () => {
await showProgressModal()
try {
const result = await DownloadUpdate(props.updateInfo.download_url)
if (result.success) return
closeProgressModal()
Message.error(result.message || '下载启动失败')
} catch (error) {
console.error('下载失败:', error)
closeProgressModal()
Message.error('下载失败:' + (error.message || error))
}
}
// 下载完成处理(本地覆盖:关闭弹窗)
const onDownloadComplete = async (event) => {
const data = typeof event === 'string' ? JSON.parse(event) : event
if (data.error) {
closeProgressModal()
Message.error('下载失败:' + data.error)
return
}
if (!data.success || !data.file_path) {
closeProgressModal()
Message.error('下载完成但数据不完整')
return
}
// 等待安装完成
await new Promise(resolve => setTimeout(resolve, 3000))
closeProgressModal()
emit('update:modelValue', false)
}
// 生命周期
onMounted(() => {
On('download-complete', onDownloadComplete)
})
onUnmounted(() => {
Off('download-complete')
closeModals()
})
</script>