feat(release): bump version to 0.1.2
## 详细信息 - 升级项目版本号到 0.1.2 - 增强刷课稳定性(失败重试、心跳检测、状态自动纠正) - 优化账号页与工作台紧凑布局,增加已学/未学区分与记录筛选 - 新增首次进入更新检查、更新日志弹窗与在线下载/回退 Release 跳转
This commit is contained in:
@@ -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;
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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 }),
|
||||
|
||||
Reference in New Issue
Block a user