feat(release): bump version to 0.1.2

## 详细信息
- 升级项目版本号到 0.1.2
- 增强刷课稳定性(失败重试、心跳检测、状态自动纠正)
- 优化账号页与工作台紧凑布局,增加已学/未学区分与记录筛选
- 新增首次进入更新检查、更新日志弹窗与在线下载/回退 Release 跳转
This commit is contained in:
2026-04-02 22:33:04 +08:00
parent a061123e36
commit 58555c5043
12 changed files with 1569 additions and 413 deletions

View File

@@ -19,6 +19,7 @@ const withLogTimestamp = (message: string) => {
return `[${timestamp}] ${message}`;
};
const MAX_STUDY_LOGS_PER_ACCOUNT = 1000;
export type AccountAuth = {
password: string;
@@ -38,6 +39,45 @@ export type AccountItem = {
export type RecordCacheMap = Record<string, RecordItem[]>;
type PersistPreferences = {
persistAccounts: boolean;
persistRecords: boolean;
persistLogs: boolean;
};
const settingsStorageKey = "settings-storage";
const defaultPersistPreferences: PersistPreferences = {
persistAccounts: true,
persistRecords: true,
persistLogs: true,
};
const resolvePersistPreferences = (): PersistPreferences => {
try {
const rawValue = localStorage.getItem(settingsStorageKey);
if (!rawValue) {
return defaultPersistPreferences;
}
const parsedValue = JSON.parse(rawValue) as {
state?: Partial<PersistPreferences>;
};
const persistedState = parsedValue?.state;
return {
persistAccounts:
persistedState?.persistAccounts ??
defaultPersistPreferences.persistAccounts,
persistRecords:
persistedState?.persistRecords ?? defaultPersistPreferences.persistRecords,
persistLogs:
persistedState?.persistLogs ?? defaultPersistPreferences.persistLogs,
};
} catch {
return defaultPersistPreferences;
}
};
type AccountState = {
accounts: AccountItem[];
selectedAccountId: string;
@@ -49,6 +89,7 @@ type AccountState = {
recordCacheMap: RecordCacheMap;
studyLogsMap: Record<string, string[]>;
runningStudyMap: Record<string, boolean>;
studyHeartbeatMap: Record<string, number>;
setSelectedAccountId: (accountId: string) => void;
setExpandedAccountId: (accountId: string) => void;
setSelectedCourseId: (courseId: number | null) => void;
@@ -57,6 +98,9 @@ type AccountState = {
setRecords: (records: RecordItem[]) => void;
setRecordCache: (cacheKey: string, records: RecordItem[]) => void;
setAccountRunningStudy: (accountId: string, value: boolean) => void;
touchStudyHeartbeat: (accountId: string, timestamp?: number) => void;
clearStudyHeartbeat: (accountId: string) => void;
clearAllStudyHeartbeat: () => void;
appendStudyLog: (accountId: string, message: string) => void;
clearStudyLogs: (accountId: string) => void;
clearAllStudyLogs: () => void;
@@ -78,6 +122,7 @@ export const accountStore = createStore<AccountState>()(
recordCacheMap: {},
studyLogsMap: {},
runningStudyMap: {},
studyHeartbeatMap: {},
setSelectedAccountId: (accountId) =>
set({ selectedAccountId: accountId }),
setExpandedAccountId: (accountId) =>
@@ -100,16 +145,39 @@ export const accountStore = createStore<AccountState>()(
[accountId]: value,
},
})),
appendStudyLog: (accountId, message) =>
touchStudyHeartbeat: (accountId, timestamp) =>
set((state) => ({
studyLogsMap: {
...state.studyLogsMap,
[accountId]: [
...(state.studyLogsMap[accountId] ?? []),
withLogTimestamp(message),
],
studyHeartbeatMap: {
...state.studyHeartbeatMap,
[accountId]: timestamp ?? Date.now(),
},
})),
clearStudyHeartbeat: (accountId) =>
set((state) => ({
studyHeartbeatMap: Object.fromEntries(
Object.entries(state.studyHeartbeatMap).filter(
([key]) => key !== accountId,
),
),
})),
clearAllStudyHeartbeat: () =>
set({
studyHeartbeatMap: {},
}),
appendStudyLog: (accountId, message) =>
set((state) => {
const nextLogs = [
...(state.studyLogsMap[accountId] ?? []),
withLogTimestamp(message),
].slice(-MAX_STUDY_LOGS_PER_ACCOUNT);
return {
studyLogsMap: {
...state.studyLogsMap,
[accountId]: nextLogs,
},
};
}),
clearStudyLogs: (accountId) =>
set((state) => ({
studyLogsMap: {
@@ -171,24 +239,42 @@ export const accountStore = createStore<AccountState>()(
([key]) => key !== accountId,
),
),
studyHeartbeatMap: Object.fromEntries(
Object.entries(state.studyHeartbeatMap).filter(
([key]) => key !== accountId,
),
),
};
}),
}),
{
name: "account-storage",
storage: createJSONStorage(() => localStorage),
partialize: (state) => ({
accounts: state.accounts,
selectedAccountId: state.selectedAccountId,
expandedAccountId: state.expandedAccountId,
selectedCourseId: state.selectedCourseId,
courseKind: state.courseKind,
recordType: state.recordType,
records: state.records,
recordCacheMap: state.recordCacheMap,
studyLogsMap: state.studyLogsMap,
runningStudyMap: state.runningStudyMap,
}),
partialize: (state) => {
const preferences = resolvePersistPreferences();
const persistedState: Partial<AccountState> = {
courseKind: state.courseKind,
recordType: state.recordType,
};
if (preferences.persistAccounts) {
persistedState.accounts = state.accounts;
persistedState.selectedAccountId = state.selectedAccountId;
persistedState.expandedAccountId = state.expandedAccountId;
persistedState.selectedCourseId = state.selectedCourseId;
}
if (preferences.persistRecords) {
persistedState.records = state.records;
persistedState.recordCacheMap = state.recordCacheMap;
}
if (preferences.persistLogs) {
persistedState.studyLogsMap = state.studyLogsMap;
}
return persistedState;
},
},
),
);

View File

@@ -35,6 +35,38 @@ type SettingsState = {
};
const accountStorageKey = "account-storage";
type PersistedStorage = {
state?: Record<string, unknown>;
version?: number;
};
const patchAccountStorage = (
patcher: (state: Record<string, unknown>) => Record<string, unknown>,
) => {
const rawValue = localStorage.getItem(accountStorageKey);
if (!rawValue) {
return;
}
try {
const parsedValue = JSON.parse(rawValue) as PersistedStorage;
const currentState =
parsedValue.state && typeof parsedValue.state === "object"
? parsedValue.state
: {};
const nextState = patcher(currentState);
localStorage.setItem(
accountStorageKey,
JSON.stringify({
...parsedValue,
state: nextState,
}),
);
} catch {
localStorage.removeItem(accountStorageKey);
}
};
const uniqueHosts = (hosts: HostOption[]) => {
const map = new Map<string, HostOption>();
@@ -74,26 +106,59 @@ export const settingsStore = createStore<SettingsState>()(
if (section === "accounts") set({ persistAccounts: value });
if (section === "records") set({ persistRecords: value });
if (section === "logs") set({ persistLogs: value });
if (!value) {
queueMicrotask(() => get().clearPersistedSection(section));
}
},
clearPersistedSection: (section) => {
if (section === "accounts") {
localStorage.removeItem(accountStorageKey);
patchAccountStorage((state) => {
const {
accounts,
selectedAccountId,
expandedAccountId,
selectedCourseId,
records,
recordCacheMap,
studyLogsMap,
runningStudyMap,
...rest
} = state;
void accounts;
void selectedAccountId;
void expandedAccountId;
void selectedCourseId;
void records;
void recordCacheMap;
void studyLogsMap;
void runningStudyMap;
return rest;
});
}
if (section === "records") {
set({ persistRecords: false });
queueMicrotask(() => set({ persistRecords: true }));
patchAccountStorage((state) => {
const { records, recordCacheMap, selectedCourseId, ...rest } =
state;
void records;
void recordCacheMap;
void selectedCourseId;
return rest;
});
}
if (section === "logs") {
set({ persistLogs: false });
queueMicrotask(() => set({ persistLogs: true }));
patchAccountStorage((state) => {
const { studyLogsMap, runningStudyMap, ...rest } = state;
void studyLogsMap;
void runningStudyMap;
return rest;
});
}
},
clearAllPersistedData: () => {
localStorage.removeItem(accountStorageKey);
get().clearPersistedSection("records");
get().clearPersistedSection("logs");
},
setAutoScrollLogs: (value) => set({ autoScrollLogs: value }),
setShowLogTimestamps: (value) => set({ showLogTimestamps: value }),