feat: 模板适配层、易班能力与全局状态

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
zhongyidianandClaude Opus 4.8 committed 2026-08-18 13:39:42 +08:00
1 parent 3995656175
commit d3ae0a8ca7
45 files changed
+6931 -179

No files matched your search

+17
View File
@@ -0,0 +1,17 @@
export * from "./modules";
export type {
AppState,
GuideDialogCharacter,
GuideDialogOption,
GuideDialogPlacement,
GuideDialogState,
GuideDialogStep,
SpotlightPlacement,
SpotlightState,
SpotlightStep,
SpotlightTarget,
StartDialogOptions,
StartSpotlightOptions,
UserState,
YibanConnectionStatus,
} from "./types";
+8
View File
@@ -0,0 +1,8 @@
import { create } from "zustand";
import type { AppState } from "../types/app.types";
export const useAppStore = create<AppState>((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
reset: () => set({ count: 0 }),
}));
+136
View File
@@ -0,0 +1,136 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
import { appConfig } from "@/app.config";
import type { GuideDialogState } from "../types/dialog.types";
const getDialogKey = (courseId: string, dialogId: string) =>
`${courseId}:${dialogId}`;
const getInitialState = () => ({
open: false,
courseId: null,
dialogId: null,
steps: [],
current: 0,
showOnce: true,
});
export const useDialogStore = create<GuideDialogState>()(
persist(
(set, get) => ({
...getInitialState(),
completedDialogMap: {},
startDialog: ({
courseId,
dialogId,
steps,
showOnce = true,
initialStep = 0,
}) => {
if (steps.length === 0) return;
const key = getDialogKey(courseId, dialogId);
if (showOnce && get().completedDialogMap[key]) {
return;
}
set({
open: true,
courseId,
dialogId,
steps,
current: Math.min(Math.max(initialStep, 0), steps.length - 1),
showOnce,
});
},
closeDialog: () => {
set(getInitialState());
},
nextStep: () => {
const { current, steps, finishDialog } = get();
if (current >= steps.length - 1) {
finishDialog();
return;
}
set({ current: current + 1 });
},
prevStep: () => {
const { current } = get();
set({ current: Math.max(current - 1, 0) });
},
setStep: (index) => {
const { steps } = get();
if (steps.length === 0) return;
set({ current: Math.min(Math.max(index, 0), steps.length - 1) });
},
selectOption: (optionId) => {
const { current, steps, setStep, finishDialog, nextStep } = get();
const step = steps[current];
const option = step?.options?.find((item) => item.id === optionId);
if (!option) return;
if (option.finish) {
finishDialog();
return;
}
if (option.nextId) {
const nextIndex = steps.findIndex(
(item) => item.id === option.nextId,
);
if (nextIndex >= 0) {
setStep(nextIndex);
return;
}
}
nextStep();
},
finishDialog: () => {
const { courseId, dialogId, completedDialogMap } = get();
if (!courseId || !dialogId) {
set(getInitialState());
return;
}
const key = getDialogKey(courseId, dialogId);
set({
...getInitialState(),
completedDialogMap: {
...completedDialogMap,
[key]: true,
},
});
},
skipDialog: () => {
get().finishDialog();
},
resetDialog: (courseId, dialogId) => {
const key = getDialogKey(courseId, dialogId);
const { [key]: _removed, ...rest } = get().completedDialogMap;
const isCurrentDialog =
get().courseId === courseId && get().dialogId === dialogId;
set({
...(isCurrentDialog ? getInitialState() : {}),
completedDialogMap: rest,
});
},
}),
{
name: `${appConfig.storage.prefix}-dialog-storage`,
partialize: (state) => ({
completedDialogMap: state.completedDialogMap,
}),
},
),
);
+4
View File
@@ -0,0 +1,4 @@
export * from "./app";
export * from "./dialog";
export * from "./spotlight";
export * from "./user";
+112
View File
@@ -0,0 +1,112 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
import { appConfig } from "@/app.config";
import type { SpotlightState } from "../types/spotlight.types";
const getSpotlightKey = (courseId: string, spotlightId: string) =>
`${courseId}:${spotlightId}`;
const getInitialState = () => ({
open: false,
courseId: null,
spotlightId: null,
steps: [],
current: 0,
showOnce: true,
});
export const useSpotlightStore = create<SpotlightState>()(
persist(
(set, get) => ({
...getInitialState(),
completedSpotlightMap: {},
startSpotlight: ({
courseId,
spotlightId,
steps,
showOnce = true,
initialStep = 0,
}) => {
if (steps.length === 0) return;
const key = getSpotlightKey(courseId, spotlightId);
if (showOnce && get().completedSpotlightMap[key]) {
return;
}
set({
open: true,
courseId,
spotlightId,
steps,
current: Math.min(Math.max(initialStep, 0), steps.length - 1),
showOnce,
});
},
closeSpotlight: () => {
set(getInitialState());
},
nextStep: () => {
const { current, steps, finishSpotlight } = get();
if (current >= steps.length - 1) {
finishSpotlight();
return;
}
set({ current: current + 1 });
},
prevStep: () => {
const { current } = get();
set({ current: Math.max(current - 1, 0) });
},
setStep: (index) => {
const { steps } = get();
if (steps.length === 0) return;
set({ current: Math.min(Math.max(index, 0), steps.length - 1) });
},
finishSpotlight: () => {
const { courseId, spotlightId, completedSpotlightMap } = get();
if (!courseId || !spotlightId) {
set(getInitialState());
return;
}
const key = getSpotlightKey(courseId, spotlightId);
set({
...getInitialState(),
completedSpotlightMap: {
...completedSpotlightMap,
[key]: true,
},
});
},
skipSpotlight: () => {
get().finishSpotlight();
},
resetSpotlight: (courseId, spotlightId) => {
const key = getSpotlightKey(courseId, spotlightId);
const { [key]: _removed, ...rest } = get().completedSpotlightMap;
const isCurrentSpotlight =
get().courseId === courseId && get().spotlightId === spotlightId;
set({
...(isCurrentSpotlight ? getInitialState() : {}),
completedSpotlightMap: rest,
});
},
}),
{
name: `${appConfig.storage.prefix}-spotlight-storage`,
partialize: (state) => ({
completedSpotlightMap: state.completedSpotlightMap,
}),
},
),
);
+29
View File
@@ -0,0 +1,29 @@
import { create } from "zustand";
import type { UserState } from "../types/user.types";
export const useUserStore = create<UserState>((set) => ({
yibanConnectionStatus: "unknown",
setYibanConnectionStatus: (status) => set({ yibanConnectionStatus: status }),
courseId: null,
setCourseId: (courseId) => set({ courseId }),
userId: null,
userName: null,
userAvatar: null,
setUserInfo: (info) =>
set({
userId: info.userId,
userName: info.userName ?? null,
userAvatar: info.userAvatar ?? null,
}),
clearUser: () =>
set({
userId: null,
userName: null,
userAvatar: null,
yibanConnectionStatus: "unknown",
courseId: null,
}),
}));
+5
View File
@@ -0,0 +1,5 @@
export interface AppState {
count: number;
increment: () => void;
reset: () => void;
}
+58
View File
@@ -0,0 +1,58 @@
import type { ReactNode } from "react";
export type GuideDialogPlacement = "top" | "center" | "bottom";
export interface GuideDialogCharacter {
name?: ReactNode;
avatar?: ReactNode;
mood?: string;
}
export interface GuideDialogOption {
id: string;
label: ReactNode;
nextId?: string;
finish?: boolean;
data?: unknown;
}
export interface GuideDialogStep {
id: string;
character?: GuideDialogCharacter;
title?: ReactNode;
description?: ReactNode;
placement?: GuideDialogPlacement;
nextLabel?: string;
prevLabel?: string;
skipLabel?: string;
doneLabel?: string;
options?: GuideDialogOption[];
data?: unknown;
}
export interface StartDialogOptions {
courseId: string;
dialogId: string;
steps: GuideDialogStep[];
showOnce?: boolean;
initialStep?: number;
}
export interface GuideDialogState {
open: boolean;
courseId: string | null;
dialogId: string | null;
steps: GuideDialogStep[];
current: number;
showOnce: boolean;
completedDialogMap: Record<string, boolean>;
startDialog: (options: StartDialogOptions) => void;
closeDialog: () => void;
nextStep: () => void;
prevStep: () => void;
setStep: (index: number) => void;
selectOption: (optionId: string) => void;
finishDialog: () => void;
skipDialog: () => void;
resetDialog: (courseId: string, dialogId: string) => void;
}
+17
View File
@@ -0,0 +1,17 @@
export type { AppState } from "./app.types";
export type {
GuideDialogCharacter,
GuideDialogOption,
GuideDialogPlacement,
GuideDialogState,
GuideDialogStep,
StartDialogOptions,
} from "./dialog.types";
export type {
SpotlightPlacement,
SpotlightState,
SpotlightStep,
SpotlightTarget,
StartSpotlightOptions,
} from "./spotlight.types";
export type { UserState, YibanConnectionStatus } from "./user.types";
+51
View File
@@ -0,0 +1,51 @@
import type { ReactNode } from "react";
export type SpotlightPlacement = "top" | "bottom" | "left" | "right" | "auto";
export interface SpotlightTarget {
selector?: string;
dataGuide?: string;
}
export interface SpotlightStep {
id: string;
target: string | SpotlightTarget;
title?: ReactNode;
description?: ReactNode;
placement?: SpotlightPlacement;
allowTargetClick?: boolean;
padding?: number;
radius?: number;
panelClassName?: string;
nextLabel?: string;
prevLabel?: string;
doneLabel?: string;
skipLabel?: string;
data?: unknown;
}
export interface StartSpotlightOptions {
courseId: string;
spotlightId: string;
steps: SpotlightStep[];
showOnce?: boolean;
initialStep?: number;
}
export interface SpotlightState {
open: boolean;
courseId: string | null;
spotlightId: string | null;
steps: SpotlightStep[];
current: number;
showOnce: boolean;
completedSpotlightMap: Record<string, boolean>;
startSpotlight: (options: StartSpotlightOptions) => void;
closeSpotlight: () => void;
nextStep: () => void;
prevStep: () => void;
setStep: (index: number) => void;
finishSpotlight: () => void;
skipSpotlight: () => void;
resetSpotlight: (courseId: string, spotlightId: string) => void;
}
+24
View File
@@ -0,0 +1,24 @@
export type YibanConnectionStatus = "connected" | "disconnected" | "unknown";
export interface UserState {
// 易班连接状态
yibanConnectionStatus: YibanConnectionStatus;
setYibanConnectionStatus: (status: YibanConnectionStatus) => void;
// 课程ID(用于上报进度)
courseId: string | null;
setCourseId: (courseId: string) => void;
// 易班用户信息
userId: string | null;
userName: string | null;
userAvatar: string | null;
setUserInfo: (info: {
userId: string;
userName?: string;
userAvatar?: string;
}) => void;
// 清空用户信息
clearUser: () => void;
}