Skip to content

Commit 8cb8173

Browse files
committed
feat: 更新日志
1 parent 2b178cd commit 8cb8173

9 files changed

Lines changed: 263 additions & 74 deletions

File tree

MaiChartManager/Front/AGENTS.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ Front/
1818
│ ├── icons/ # 图标组件
1919
│ ├── locales/ # i18n 翻译文件
2020
│ ├── plugins/ # Vue 插件(posthog, sentry, i18n)
21-
│ ├── store/ # 状态管理:refs.ts 单文件
21+
│ ├── store/ # 状态管理:refs.ts(核心)+ appUpdate.ts(更新/更新日志相关)
2222
│ ├── utils/ # 工具函数
2323
│ ├── views/ # 页面视图:BatchAction/, Charts/, GenreVersionManager/, ModManager/, Tools/
2424
│ └── assets/ # 静态资源
@@ -34,7 +34,7 @@ Front/
3434
|------|------|
3535
| 调用后端 API | `src/client/api.ts`(手写封装)|
3636
| 添加新页面 | `src/views/` 对应子目录 |
37-
| 全局状态 | `src/store/refs.ts` |
37+
| 全局状态 | `src/store/refs.ts`(核心状态)、`src/store/appUpdate.ts`(更新相关)|
3838
| 复用组件 | `src/components/` |
3939
| 国际化文本 | `src/locales/` |
4040
| 重新生成 API client | 运行 `npx ts-node genClient.ts` |
@@ -45,8 +45,9 @@ Front/
4545
- UI 组件库:Naive UI
4646
- 样式方案:UnoCSS(原子化 CSS)
4747
- 路径别名:`@``./src`
48-
- 状态管理极简,所有全局 ref 集中在 `src/store/refs.ts`
48+
- 状态管理极简,核心全局 ref 集中在 `src/store/refs.ts`,更新/更新日志相关状态在 `src/store/appUpdate.ts`
4949
- API client 由 `genClient.ts` 读取后端控制器自动生成,输出到 `apiGen.ts`
50+
- 代码注释使用中文
5051

5152
## ANTI-PATTERNS
5253

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { defineComponent, watch } from "vue";
2+
import { Modal } from "@munet/ui";
3+
import { useI18n } from "vue-i18n";
4+
import { VueMarkdownIt } from "@f3ve/vue-markdown-it";
5+
import { version } from "@/store/refs";
6+
import {
7+
changelogAutoPopupDone,
8+
changelogContent,
9+
changelogTargetVersion,
10+
getCleanVersion,
11+
lastShownChangelogVersion,
12+
openChangelog,
13+
showChangelogModal,
14+
} from "@/store/appUpdate";
15+
import style from "@/components/VersionInfo/style.module.sass";
16+
17+
export default defineComponent({
18+
props: {
19+
ready: {
20+
type: Boolean,
21+
required: true,
22+
},
23+
},
24+
setup(props) {
25+
const { t } = useI18n();
26+
27+
watch(
28+
() => [props.ready, version.value?.version] as const,
29+
async ([ready, ver]) => {
30+
if (!ready || !ver) return;
31+
if (changelogAutoPopupDone.value) return;
32+
changelogAutoPopupDone.value = true;
33+
34+
const cleanVer = getCleanVersion(ver);
35+
if (cleanVer === lastShownChangelogVersion.value) return;
36+
37+
await new Promise(resolve => setTimeout(resolve, 200));
38+
const shown = await openChangelog(ver, {
39+
showAfterLoaded: true,
40+
skipIfEmpty: true,
41+
});
42+
if (shown) {
43+
lastShownChangelogVersion.value = cleanVer;
44+
}
45+
},
46+
{ immediate: true },
47+
);
48+
49+
return () => <Modal
50+
width="min(85vw,50em)"
51+
title={`${t('about.changelogTitle')} - v${changelogTargetVersion.value}`}
52+
v-model:show={showChangelogModal.value}
53+
>
54+
<div class={style.mdContent}>
55+
{changelogContent.value
56+
? <VueMarkdownIt source={changelogContent.value} />
57+
: <div class="text-center py-4 op-60">{t('common.loading')}</div>
58+
}
59+
</div>
60+
</Modal>;
61+
},
62+
})

MaiChartManager/Front/src/components/VersionInfo/index.tsx

Lines changed: 31 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
import { computed, defineComponent, ref } from "vue";
22
import AppIcon from '@/components/AppIcon';
33
import '@fontsource/nerko-one'
4-
import { appUpdateInfo, updateVersion, version } from "@/store/refs";
4+
import { updateVersion, version } from "@/store/refs";
5+
import { appUpdateInfo, openChangelog } from "@/store/appUpdate";
56
import { compareVersions } from "@/views/ModManager/shouldShowUpdateController";
6-
import { locale } from "@/locales";
7-
import { VueMarkdownIt } from '@f3ve/vue-markdown-it';
8-
import style from './style.module.sass';
97
import StorePurchaseButton from "@/components/StorePurchaseButton";
108
import AfdianIcon from "@/icons/afdian.svg";
119
import { HardwareAccelerationStatus, LicenseStatus } from "@/client/apiGen";
@@ -14,25 +12,19 @@ import { Modal, TextInput, Button, addToast, theme } from "@munet/ui";
1412
import api from "@/client/api";
1513

1614
export default defineComponent({
17-
setup(props) {
15+
setup() {
1816
const show = ref(false);
1917
const showOfflineActivation = ref(false);
2018
const activationCode = ref('');
2119
const activating = ref(false);
2220
const displayVersion = computed(() => version.value?.version?.split('+')[0]);
23-
const showChangelog = ref(false);
2421

2522
const hasAppUpdate = computed(() => {
2623
if (!appUpdateInfo.value?.version || !version.value?.version) return false;
2724
const currentVersion = version.value.version.split('+')[0];
2825
return compareVersions(appUpdateInfo.value.version, currentVersion) > 0;
2926
});
3027

31-
const changelogNotes = computed(() => {
32-
if (!appUpdateInfo.value?.notes) return '';
33-
return appUpdateInfo.value.notes[locale.value] || appUpdateInfo.value.notes['en'] || '';
34-
});
35-
3628
const { t } = useI18n();
3729

3830
const onVersionClick = (e: MouseEvent) => {
@@ -63,6 +55,24 @@ export default defineComponent({
6355
}
6456
};
6557

58+
const openCurrentVersionChangelog = async () => {
59+
const currentVer = version.value?.version;
60+
if (!currentVer) return;
61+
await openChangelog(currentVer, {
62+
showAfterLoaded: true,
63+
skipIfEmpty: true,
64+
});
65+
};
66+
67+
const openLatestVersionChangelog = async () => {
68+
const latestVer = appUpdateInfo.value?.version;
69+
if (!latestVer) return;
70+
await openChangelog(latestVer, {
71+
showAfterLoaded: true,
72+
skipIfEmpty: true,
73+
});
74+
};
75+
6676
return () => version.value && <div class={'w-15 py-1 flex items-center justify-center rounded-md cursor-pointer transition-all duration-200 bg-avatarMenuButton text-3.5 shrink-0 relative'} onClick={onVersionClick}>
6777
v{displayVersion.value}
6878
{hasAppUpdate.value && <div class="absolute top-0.5 right-0.5 w-2 h-2 rounded-full bg-red-500 pointer-events-none" />}
@@ -78,16 +88,22 @@ export default defineComponent({
7888
<a class="i-mdi-github hover:c-[var(--text-color)] transition-300" href="https://github.com/clansty/MaiChartManager" target="_blank"/>
7989
<a class="i-ri-qq-fill hover:c-[var(--text-color)] transition-300" href="https://qm.qq.com/q/U3gT7CDuy6" target="_blank" />
8090
</div>
81-
<div class="flex items-center gap-2">
91+
<div class="flex items-center gap-2 flex-wrap">
8292
{t('about.version')}: v{version.value.version}
93+
<span
94+
class={[theme.value.lc, 'fl cursor-pointer']}
95+
onClick={openCurrentVersionChangelog}
96+
>
97+
{t('about.viewChangelog')}
98+
</span>
8399
{hasAppUpdate.value && <>
84100
<span class="c-red-500 font-bold">{t('about.updateAvailable', { version: appUpdateInfo.value!.version })}</span>
85-
<a
101+
<span
86102
class={[theme.value.lc, 'fl cursor-pointer']}
87-
onClick={() => { showChangelog.value = true; }}
103+
onClick={openLatestVersionChangelog}
88104
>
89105
{t('about.viewChangelog')}
90-
</a>
106+
</span>
91107
<Button onClick={() => window.open('ms-windows-store://pdp/?ProductId=9P1JDKQ60G4G')}>
92108
{t('about.updateHint')}
93109
</Button>
@@ -155,15 +171,6 @@ export default defineComponent({
155171
</Button>,
156172
}}</Modal>
157173

158-
<Modal
159-
width="min(85vw,50em)"
160-
title={t('about.changelogTitle')}
161-
v-model:show={showChangelog.value}
162-
>
163-
<div class={style.mdContent}>
164-
<VueMarkdownIt source={changelogNotes.value} />
165-
</div>
166-
</Modal>
167174
</div>;
168175
}
169176
})
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import { ref } from "vue";
2+
import { useStorage } from "@vueuse/core";
3+
import { locale } from "@/locales";
4+
import { aquaMaiVersionConfig } from "@/client/api";
5+
import { GetGetConfigTypeEnum } from "@/client/aquaMaiVersionConfigApiGen";
6+
7+
const CHANGELOG_BUCKET_BASE = 'https://munet-version-config-1251600285.cos.ap-shanghai.myqcloud.com/mcm-changelog';
8+
9+
// --- Mod 更新 ---
10+
11+
export const modUpdateInfo = ref<Awaited<ReturnType<typeof aquaMaiVersionConfig.getGetConfig>>['data']>([{
12+
type: GetGetConfigTypeEnum.Builtin,
13+
}])
14+
15+
export const updateModUpdateInfo = async () => {
16+
try {
17+
modUpdateInfo.value = await Promise.any([
18+
(async () => {
19+
const res = await aquaMaiVersionConfig.getGetConfig({
20+
cache: 'no-cache',
21+
});
22+
return res.data;
23+
})(),
24+
(async () => {
25+
const res = await fetch('https://munet-version-config-1251600285.cos.ap-shanghai.myqcloud.com/aquamai.json', {
26+
cache: 'no-cache',
27+
});
28+
if (!res.ok) {
29+
throw new Error(`Failed to fetch mod update info: ${res.status} ${res.statusText}`);
30+
}
31+
return await res.json();
32+
})(),
33+
]);
34+
} catch (e) {
35+
console.error('Failed to get mod update info:', e);
36+
}
37+
}
38+
39+
// --- 应用更新 ---
40+
41+
export const appUpdateInfo = ref<{ version: string } | null>(null);
42+
43+
export const updateAppUpdateInfo = async () => {
44+
try {
45+
const res = await fetch('https://munet-version-config-1251600285.cos.ap-shanghai.myqcloud.com/mcm.json', {
46+
cache: 'no-cache',
47+
});
48+
if (!res.ok) return;
49+
appUpdateInfo.value = await res.json();
50+
// 预加载新版本的更新日志
51+
if (appUpdateInfo.value?.version) {
52+
eagerFetchChangelog(appUpdateInfo.value.version);
53+
}
54+
} catch (e) {
55+
console.error('Failed to get app update info:', e);
56+
}
57+
}
58+
59+
// --- 更新日志 ---
60+
61+
export const showChangelogModal = ref(false);
62+
export const changelogContent = ref('');
63+
export const changelogTargetVersion = ref('');
64+
export const changelogAutoPopupDone = ref(false);
65+
export const lastShownChangelogVersion = useStorage('lastShownChangelogVersion', '');
66+
67+
export const getCleanVersion = (v: string) => v.split('+')[0];
68+
69+
/**
70+
* 构建 locale 回退链:当前语言 → 基础语言 → en
71+
* 例如 zh-TW → zh → en
72+
*/
73+
function getLocaleFallbackChain(): string[] {
74+
const current = locale.value;
75+
const chain = [current];
76+
if (current.includes('-')) {
77+
chain.push(current.split('-')[0]);
78+
}
79+
if (!chain.includes('en')) {
80+
chain.push('en');
81+
}
82+
return chain;
83+
}
84+
85+
async function fetchChangelog(ver: string): Promise<string> {
86+
const cleanVer = getCleanVersion(ver);
87+
for (const loc of getLocaleFallbackChain()) {
88+
try {
89+
const res = await fetch(`${CHANGELOG_BUCKET_BASE}/${cleanVer}.${loc}.md`, { cache: 'no-cache' });
90+
if (res.ok) return await res.text();
91+
} catch {
92+
// 网络错误,尝试下一个 locale
93+
}
94+
}
95+
return '';
96+
}
97+
98+
// 更新日志缓存:版本号 + 语言链 → fetch promise
99+
const changelogCache = new Map<string, Promise<string>>();
100+
let openChangelogRequestId = 0;
101+
102+
function getChangelogCacheKey(ver: string) {
103+
const cleanVer = getCleanVersion(ver);
104+
return `${cleanVer}|${getLocaleFallbackChain().join('>')}`;
105+
}
106+
107+
export function eagerFetchChangelog(ver: string) {
108+
const key = getChangelogCacheKey(ver);
109+
if (!changelogCache.has(key)) {
110+
changelogCache.set(key, fetchChangelog(ver));
111+
}
112+
}
113+
114+
async function getChangelogCached(ver: string): Promise<string> {
115+
const key = getChangelogCacheKey(ver);
116+
const cached = changelogCache.get(key);
117+
if (cached) return cached;
118+
const promise = fetchChangelog(ver);
119+
changelogCache.set(key, promise);
120+
return promise;
121+
}
122+
123+
export async function openChangelog(ver: string, options?: {
124+
showAfterLoaded?: boolean;
125+
skipIfEmpty?: boolean;
126+
}) {
127+
const requestId = ++openChangelogRequestId;
128+
const cleanVer = getCleanVersion(ver);
129+
const showAfterLoaded = !!options?.showAfterLoaded;
130+
const skipIfEmpty = !!options?.skipIfEmpty;
131+
132+
changelogTargetVersion.value = cleanVer;
133+
changelogContent.value = '';
134+
135+
if (!showAfterLoaded) {
136+
showChangelogModal.value = true;
137+
}
138+
139+
const content = await getChangelogCached(ver);
140+
if (requestId !== openChangelogRequestId) {
141+
return false;
142+
}
143+
if (skipIfEmpty && !content) {
144+
showChangelogModal.value = false;
145+
return false;
146+
}
147+
148+
changelogContent.value = content;
149+
if (showAfterLoaded) {
150+
showChangelogModal.value = true;
151+
}
152+
return true;
153+
}

0 commit comments

Comments
 (0)