feat: 模板适配层、易班能力与全局状态
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
3995656175
commit
d3ae0a8ca7
45 files changed
+6931
-179
No files matched your search
@@ -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";
|
||||
@@ -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 }),
|
||||
}));
|
||||
@@ -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,
|
||||
}),
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from "./app";
|
||||
export * from "./dialog";
|
||||
export * from "./spotlight";
|
||||
export * from "./user";
|
||||
@@ -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,
|
||||
}),
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -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,
|
||||
}),
|
||||
}));
|
||||
@@ -0,0 +1,5 @@
|
||||
export interface AppState {
|
||||
count: number;
|
||||
increment: () => void;
|
||||
reset: () => void;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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";
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user