Feature(custom): add release note page

This commit is contained in:
Kuingsmile
2025-08-29 13:53:22 +08:00
parent e76b84b7a2
commit 692b40241c
5 changed files with 315 additions and 3 deletions

View File

@@ -339,7 +339,20 @@
"newestVersion": "Newest Version: {version}", "newestVersion": "Newest Version: {version}",
"getting": "Getting...", "getting": "Getting...",
"hasNewVersion": "PicList has been updated, please click OK to restart and trigger the update", "hasNewVersion": "PicList has been updated, please click OK to restart and trigger the update",
"networkError": "Network error, please check your network connection" "networkError": "Network error, please check your network connection",
"releaseNotes": "Release Notes",
"releaseNotesDescription": "View the latest changes and updates",
"latestReleaseNotes": "Latest Release Notes",
"refresh": "Refresh",
"retry": "Retry",
"loadingReleaseNotes": "Loading release notes...",
"releaseNotesError": "Failed to load release notes. Please check your network connection.",
"noReleaseNotes": "No release notes available",
"lastUpdated": "Last updated",
"justNow": "just now",
"minutesAgo": "{minutes} minutes ago",
"hoursAgo": "{hours} hours ago",
"daysAgo": "{days} days ago"
} }
}, },
"plugin": { "plugin": {

View File

@@ -334,7 +334,20 @@
"newestVersion": "最新版本", "newestVersion": "最新版本",
"getting": "正在获取中...", "getting": "正在获取中...",
"hasNewVersion": "PicList 更新啦,请点击确定重启触发更新", "hasNewVersion": "PicList 更新啦,请点击确定重启触发更新",
"networkError": "网络错误,请检查网络连接" "networkError": "网络错误,请检查网络连接",
"releaseNotes": "版本说明",
"releaseNotesDescription": "查看最新的更改和更新内容",
"latestReleaseNotes": "最新版本说明",
"refresh": "刷新",
"retry": "重试",
"loadingReleaseNotes": "正在加载版本说明...",
"releaseNotesError": "加载版本说明失败,请检查网络连接。",
"noReleaseNotes": "暂无版本说明",
"lastUpdated": "最后更新",
"justNow": "刚刚",
"minutesAgo": "{minutes} 分钟前",
"hoursAgo": "{hours} 小时前",
"daysAgo": "{days} 天前"
} }
}, },
"plugin": { "plugin": {

View File

@@ -334,7 +334,20 @@
"newestVersion": "最新版本", "newestVersion": "最新版本",
"getting": "正在獲取中...", "getting": "正在獲取中...",
"hasNewVersion": "PicList 更新啦,請點擊確定重啟觸發更新", "hasNewVersion": "PicList 更新啦,請點擊確定重啟觸發更新",
"networkError": "網絡錯誤,請檢查網絡連接" "networkError": "網絡錯誤,請檢查網絡連接",
"releaseNotes": "版本說明",
"releaseNotesDescription": "查看最新的更改和更新內容",
"latestReleaseNotes": "最新版本說明",
"refresh": "刷新",
"retry": "重試",
"loadingReleaseNotes": "正在載入版本說明...",
"releaseNotesError": "載入版本說明失敗,請檢查網絡連接。",
"noReleaseNotes": "暫無版本說明",
"lastUpdated": "最後更新",
"justNow": "剛剛",
"minutesAgo": "{minutes} 分鐘前",
"hoursAgo": "{hours} 小時前",
"daysAgo": "{days} 天前"
} }
}, },
"plugin": { "plugin": {

View File

@@ -650,6 +650,53 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Release Notes Card -->
<div class="settings-section">
<h2>{{ t('pages.settings.update.releaseNotes') }}</h2>
<p>{{ t('pages.settings.update.releaseNotesDescription') }}</p>
<div class="release-notes-card">
<div class="release-notes-header">
<h3>{{ t('pages.settings.update.latestReleaseNotes') }}</h3>
<div class="release-notes-actions">
<button
class="btn btn-secondary btn-sm"
:disabled="fetchingReleaseNotes"
@click="fetchReleaseNotesManually"
>
<RefreshCw :size="14" :class="{ rotate: fetchingReleaseNotes }" />
{{ t('pages.settings.update.refresh') }}
</button>
</div>
</div>
<div class="release-notes-content">
<div v-if="fetchingReleaseNotes" class="release-notes-loading">
<RefreshCw :size="16" class="rotate" />
{{ t('pages.settings.update.loadingReleaseNotes') }}
</div>
<div v-else-if="releaseNotes" class="release-notes-text">
<pre class="release-notes-pre">{{ releaseNotes }}</pre>
</div>
<div v-else-if="releaseNotesError" class="release-notes-error">
{{ releaseNotesError }}
<button class="btn btn-link btn-sm" @click="fetchReleaseNotesManually">
{{ t('pages.settings.update.retry') }}
</button>
</div>
<div v-else class="release-notes-empty">
{{ t('pages.settings.update.noReleaseNotes') }}
</div>
</div>
<div v-if="releaseNotesLastFetch" class="release-notes-footer">
<small>
{{ t('pages.settings.update.lastUpdated') }}: {{ formatLastFetchTime(releaseNotesLastFetch) }}
</small>
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -1401,6 +1448,8 @@ const addWatch = () => {
watch(currentLanguage, newVal => { watch(currentLanguage, newVal => {
if (newVal) { if (newVal) {
handleLanguageChange(newVal) handleLanguageChange(newVal)
// Fetch release notes when language changes
fetchReleaseNotes(true)
} }
}) })
@@ -1531,6 +1580,12 @@ function confirmSyncSetting() {
const version = pkg.version const version = pkg.version
const latestVersion = ref('') const latestVersion = ref('')
const releaseNotes = ref('')
const releaseNotesError = ref('')
const releaseNotesLastFetch = ref<Date | null>(null)
const fetchingReleaseNotes = ref(false)
const RELEASE_NOTES_CACHE_DURATION = 30 * 60 * 1000
const needUpdate = computed(() => { const needUpdate = computed(() => {
if (latestVersion.value) { if (latestVersion.value) {
@@ -1589,6 +1644,9 @@ async function initData() {
formOfSetting.value.logFileSizeLimit = enforceNumber(settings.logFileSizeLimit) || 10 formOfSetting.value.logFileSizeLimit = enforceNumber(settings.logFileSizeLimit) || 10
addProxyWatch() addProxyWatch()
addWatch() addWatch()
// Fetch release notes on initialization
fetchReleaseNotes()
} }
function initArray(arrayT: string | string[], defaultValue: string[]) { function initArray(arrayT: string | string[], defaultValue: string[]) {
@@ -1703,6 +1761,62 @@ function compareVersion2Update(current: string, latest: string): boolean {
return compare(current, latest, '<') return compare(current, latest, '<')
} }
function formatLastFetchTime(date: Date): string {
const now = new Date()
const diffInMinutes = Math.floor((now.getTime() - date.getTime()) / (1000 * 60))
if (diffInMinutes < 1) {
return t('pages.settings.update.justNow')
} else if (diffInMinutes < 60) {
return t('pages.settings.update.minutesAgo', { minutes: diffInMinutes })
} else {
const hours = Math.floor(diffInMinutes / 60)
if (hours < 24) {
return t('pages.settings.update.hoursAgo', { hours })
} else {
const days = Math.floor(hours / 24)
return t('pages.settings.update.daysAgo', { days })
}
}
}
async function fetchReleaseNotes(forceRefresh = false): Promise<void> {
if (!forceRefresh && releaseNotesLastFetch.value) {
const timeSinceLastFetch = Date.now() - releaseNotesLastFetch.value.getTime()
if (timeSinceLastFetch < RELEASE_NOTES_CACHE_DURATION) {
return
}
}
try {
fetchingReleaseNotes.value = true
releaseNotesError.value = ''
const isEnglish = currentLanguage.value === 'en'
const fileName = isEnglish ? 'currentVersion_en.md' : 'currentVersion.md'
const url = `https://raw.githubusercontent.com/Kuingsmile/piclist/dev/${fileName}`
const response = await fetch(url)
if (response.ok) {
const content = await response.text()
releaseNotes.value = content
releaseNotesLastFetch.value = new Date()
releaseNotesError.value = ''
} else {
throw new Error(`HTTP ${response.status}`)
}
} catch (error) {
console.error('Failed to fetch release notes:', error)
releaseNotesError.value = t('pages.settings.update.releaseNotesError')
} finally {
fetchingReleaseNotes.value = false
}
}
async function fetchReleaseNotesManually(): Promise<void> {
await fetchReleaseNotes(true)
}
async function checkUpdate() { async function checkUpdate() {
checkUpdateVisible.value = true checkUpdateVisible.value = true
latestVersion.value = (await getLatestVersion()) || t('pages.settings.update.networkError') latestVersion.value = (await getLatestVersion()) || t('pages.settings.update.networkError')

View File

@@ -809,3 +809,162 @@ small {
background: var(--color-blue-common); background: var(--color-blue-common);
border-color: rgba(255, 255, 255, 0.15); border-color: rgba(255, 255, 255, 0.15);
} }
/* Update info and Release Notes Styles */
.update-info {
margin-bottom: 1.5rem;
padding: 1rem;
background: var(--color-background);
border-radius: 8px;
border: 1px solid var(--color-border);
}
.update-info > div {
margin-bottom: 0.5rem;
font-size: 0.925rem;
}
.update-info > div:last-child {
margin-bottom: 0;
}
.update-notice {
color: var(--color-success) !important;
font-weight: 500;
}
/* Release Notes Card */
.release-notes-card {
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: 12px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.release-notes-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 1.5rem;
background: var(--color-background);
border-bottom: 1px solid var(--color-border);
}
.release-notes-header h3 {
margin: 0;
font-size: 1rem;
font-weight: 600;
color: var(--color-text-primary);
}
.release-notes-actions {
display: flex;
gap: 0.5rem;
}
.btn-sm {
padding: 0.375rem 0.75rem;
font-size: 0.875rem;
min-height: auto;
}
.btn-link {
background: transparent;
border: none;
color: var(--color-accent);
text-decoration: underline;
padding: 0.25rem;
}
.btn-link:hover {
color: var(--color-accent-hover);
background: transparent;
}
.release-notes-content {
background: var(--color-background);
max-height: 400px;
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: var(--color-accent) transparent;
}
.release-notes-content::-webkit-scrollbar {
width: 6px;
}
.release-notes-content::-webkit-scrollbar-track {
background: transparent;
}
.release-notes-content::-webkit-scrollbar-thumb {
background: var(--color-accent);
border-radius: 3px;
}
.release-notes-content::-webkit-scrollbar-thumb:hover {
background: var(--color-accent-hover);
}
.release-notes-loading,
.release-notes-error,
.release-notes-empty {
padding: 2rem;
text-align: center;
color: var(--color-text-secondary);
font-size: 0.925rem;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
}
.release-notes-error {
color: var(--color-error);
flex-direction: column;
gap: 1rem;
}
.release-notes-text {
padding: 0;
}
.release-notes-pre {
margin: 0;
padding: 1.5rem;
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', 'Consolas', monospace;
font-size: 0.875rem;
line-height: 1.6;
white-space: pre-wrap;
word-wrap: break-word;
color: var(--color-text-primary);
background: transparent;
border: none;
}
.release-notes-footer {
padding: 0.75rem 1.5rem;
background: var(--color-background-secondary);
border-top: 1px solid var(--color-border);
text-align: center;
}
.release-notes-footer small {
color: var(--color-text-secondary);
font-size: 0.75rem;
}
/* Rotation animation for loading icons */
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.rotate {
animation: rotate 1s linear infinite;
}