Skip to content

Commit 7fabdee

Browse files
committed
feat: add announcement component and changelog parser
- Introduced a new Announcement component to display product updates and changelog information. - Implemented a changelog parser to structure and retrieve unseen updates. - Enhanced MyHeader.vue to show unseen update count and trigger the announcement modal.
1 parent 2df2842 commit 7fabdee

File tree

7 files changed

+412
-9
lines changed

7 files changed

+412
-9
lines changed

CHANGELOG.zh-CN.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# 更新日志
2+
3+
本文档记录项目的所有重要变更。
4+
5+
## [2.3.1] - 2025-10-27
6+
7+
### 新增功能
8+
9+
- 九宫格拼音键盘,支持T9智能候选词建议
10+
- 语音识别模型自动下载脚本
11+
- T9键盘布局用户自定义选择
12+
13+
### 改进优化
14+
15+
- 支持根据窗口大小自动切换T9键盘布局
16+
- 优化T9侧边栏界面体验
17+
18+
### 其他
19+
20+
- 添加其他优秀输入法的友情推荐

public/announcement.zh-CN.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# 更新日志
2+
3+
本文档记录项目的所有重要变更。
4+
5+
## [2.3.1] - 2025-10-27
6+
7+
### 九键输入、语音输入功能正式上线
8+
9+
为VR设备优化的九宫格拼音输入,让单手输入更加便捷。
10+
11+
#### 新增功能
12+
13+
- 新增类似手机端的九宫格键盘(参考自语燕输入法),支持简单的候选词建议
14+
- 语音识别模型在首次下载后,会缓存到本地,后续无需重新下载
15+
- 新增意见反馈栏,如果有新功能建议或者遇到问题,欢迎用它提交反馈
16+
17+
#### 改进优化
18+
19+
- 支持根据窗口大小自动切换T9键盘布局,并且用户可以在设置里选择使用哪个T9键盘布局
20+
21+
#### 其他
22+
23+
- 在友情推荐里更新其他优秀输入法

src/App.vue

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
<script setup lang="ts">
2+
import { ref, provide } from 'vue'
23
import {
34
NConfigProvider,
45
NDialogProvider,
@@ -11,13 +12,20 @@ import { homepage } from '../package.json'
1112
import MyHeader from './components/MyHeader.vue'
1213
import MyLayout from './components/MyLayout.vue'
1314
import MyPwa from './components/MyPwa.vue'
15+
import Announcement from './components/Announcement.vue'
1416
import { currentTheme } from './util'
17+
18+
const announcementRef = ref<InstanceType<typeof Announcement>>()
19+
20+
// Provide the announcement ref to child components
21+
provide('announcementRef', announcementRef)
1522
</script>
1623

1724
<template>
1825
<n-config-provider :theme="currentTheme === 'dark' ? darkTheme : null" :locale="zhCN">
1926
<n-message-provider>
2027
<MyPwa />
28+
<Announcement ref="announcementRef" />
2129
<my-layout>
2230
<template #header>
2331
<my-header icon="./icon.svg" :homepage="homepage" />

src/changelog-parser.ts

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
// Parse CHANGELOG.zh-CN.md structure
2+
// Format:
3+
// ## [version] - date
4+
// ### Update Title
5+
// Description
6+
// #### Section Name
7+
// - item
8+
// - item
9+
10+
export interface ChangelogItem {
11+
text: string
12+
}
13+
14+
export interface ChangelogSection {
15+
name: string // e.g., "新增功能", "改进优化"
16+
items: ChangelogItem[]
17+
}
18+
19+
export interface ProductUpdate {
20+
version: string
21+
date: string
22+
releasedAt: number // Unix timestamp
23+
title: string // e.g., "九键输入法、语音输入法正式上线"
24+
description: string
25+
sections: ChangelogSection[]
26+
}
27+
28+
export function parseChangelog(markdown: string): ProductUpdate[] {
29+
const lines = markdown.split('\n')
30+
const updates: ProductUpdate[] = []
31+
let currentUpdate: Partial<ProductUpdate> | null = null
32+
let currentSection: Partial<ChangelogSection> | null = null
33+
let expectingTitle = false
34+
let expectingDescription = false
35+
36+
for (let i = 0; i < lines.length; i++) {
37+
const line = lines[i].trim()
38+
39+
// Skip empty lines and main title
40+
if (!line || line.startsWith('# 更新日志') || line.startsWith('本文档记录')) {
41+
continue
42+
}
43+
44+
// Match version line: ## [2.3.1] - 2025-10-27
45+
const versionMatch = line.match(/^##\s+\[([^\]]+)\]\s+-\s+(.+)/)
46+
if (versionMatch) {
47+
// Save previous update if exists
48+
if (currentUpdate && currentUpdate.version) {
49+
if (currentSection && currentSection.name) {
50+
if (!currentUpdate.sections) currentUpdate.sections = []
51+
currentUpdate.sections.push(currentSection as ChangelogSection)
52+
}
53+
updates.push(currentUpdate as ProductUpdate)
54+
}
55+
56+
// Start new update
57+
const [, version, dateStr] = versionMatch
58+
currentUpdate = {
59+
version,
60+
date: dateStr,
61+
releasedAt: Date.parse(dateStr),
62+
title: '',
63+
description: '',
64+
sections: []
65+
}
66+
currentSection = null
67+
expectingTitle = true
68+
expectingDescription = false
69+
continue
70+
}
71+
72+
// Match title: ### Update Title
73+
const titleMatch = line.match(/^###\s+(.+)/)
74+
if (titleMatch && expectingTitle) {
75+
currentUpdate!.title = titleMatch[1]
76+
expectingTitle = false
77+
expectingDescription = true
78+
continue
79+
}
80+
81+
// Match section: #### 新增功能
82+
const sectionMatch = line.match(/^####\s+(.+)/)
83+
if (sectionMatch) {
84+
// Save previous section
85+
if (currentSection && currentSection.name) {
86+
currentUpdate!.sections!.push(currentSection as ChangelogSection)
87+
}
88+
89+
// Start new section
90+
currentSection = {
91+
name: sectionMatch[1],
92+
items: []
93+
}
94+
expectingDescription = false
95+
continue
96+
}
97+
98+
// Match item: - 九宫格拼音键盘
99+
const itemMatch = line.match(/^-\s+(.+)/)
100+
if (itemMatch && currentSection) {
101+
currentSection.items!.push({ text: itemMatch[1] })
102+
continue
103+
}
104+
105+
// Description line (plain text after title)
106+
if (expectingDescription && line && !line.startsWith('#')) {
107+
currentUpdate!.description = line
108+
expectingDescription = false
109+
continue
110+
}
111+
}
112+
113+
// Save last update
114+
if (currentUpdate && currentUpdate.version) {
115+
if (currentSection && currentSection.name) {
116+
currentUpdate.sections!.push(currentSection as ChangelogSection)
117+
}
118+
updates.push(currentUpdate as ProductUpdate)
119+
}
120+
121+
return updates
122+
}
123+
124+
// Storage key for last viewed timestamp
125+
export const LAST_VIEWED_KEY = 'vrime_changelog_last_viewed'
126+
127+
// Get last viewed timestamp from localStorage
128+
export function getLastViewedTimestamp(): number {
129+
const stored = localStorage.getItem(LAST_VIEWED_KEY)
130+
return stored ? parseInt(stored, 10) : 0
131+
}
132+
133+
// Update last viewed timestamp
134+
export function markChangelogAsViewed(): void {
135+
localStorage.setItem(LAST_VIEWED_KEY, Date.now().toString())
136+
}
137+
138+
// Get updates released after a certain timestamp
139+
export function getUnseenUpdates(updates: ProductUpdate[]): ProductUpdate[] {
140+
const lastViewed = getLastViewedTimestamp()
141+
return updates.filter(update => update.releasedAt > lastViewed)
142+
}
143+
144+
// Get count of unseen updates
145+
export function getUnseenCount(updates: ProductUpdate[]): number {
146+
return getUnseenUpdates(updates).length
147+
}
148+
149+
// Check if update is new (released after last viewed)
150+
export function isNewUpdate(update: ProductUpdate): boolean {
151+
return update.releasedAt > getLastViewedTimestamp()
152+
}

0 commit comments

Comments
 (0)