forked from zhongyidian/yx-react-template
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
@@ -1,36 +0,0 @@
|
||||
@reference "tailwindcss";
|
||||
|
||||
.button {
|
||||
@apply inline-flex items-center justify-center rounded-md font-medium transition-colors;
|
||||
@apply focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2;
|
||||
@apply disabled:pointer-events-none disabled:opacity-50;
|
||||
}
|
||||
|
||||
.primary {
|
||||
@apply bg-blue-600 text-white hover:bg-blue-700;
|
||||
}
|
||||
|
||||
.secondary {
|
||||
@apply bg-gray-200 text-gray-900 hover:bg-gray-300;
|
||||
}
|
||||
|
||||
.outline {
|
||||
@apply border border-gray-300 bg-transparent hover:bg-gray-100;
|
||||
}
|
||||
|
||||
.ghost {
|
||||
@apply bg-transparent hover:bg-gray-100;
|
||||
}
|
||||
|
||||
.sm {
|
||||
@apply h-8 px-3 text-sm;
|
||||
}
|
||||
|
||||
.md {
|
||||
@apply h-10 px-4 text-base;
|
||||
}
|
||||
|
||||
.lg {
|
||||
@apply h-12 px-6 text-lg;
|
||||
}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
import { type ButtonHTMLAttributes, forwardRef } from "react";
|
||||
import { cn } from "@/utils";
|
||||
import styles from "./Button.module.css";
|
||||
|
||||
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
variant?: "primary" | "secondary" | "outline" | "ghost";
|
||||
size?: "sm" | "md" | "lg";
|
||||
}
|
||||
|
||||
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant = "primary", size = "md", ...props }, ref) => {
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
className={cn(styles.button, styles[variant], styles[size], className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Button.displayName = "Button";
|
||||
@@ -1,2 +0,0 @@
|
||||
export type { ButtonProps } from "./Button.tsx";
|
||||
export { Button } from "./Button.tsx";
|
||||
@@ -0,0 +1,44 @@
|
||||
import { appConfig } from "@/app.config";
|
||||
import { useDialogStore } from "@/stores";
|
||||
import type {
|
||||
GuideDialogStep,
|
||||
StartDialogOptions,
|
||||
} from "@/stores/types/dialog.types";
|
||||
|
||||
export interface DefineDialogOptions {
|
||||
id: string;
|
||||
steps: GuideDialogStep[];
|
||||
showOnce?: boolean;
|
||||
initialStep?: number;
|
||||
}
|
||||
|
||||
export interface GuideDialogController {
|
||||
id: string;
|
||||
steps: GuideDialogStep[];
|
||||
start: (options?: Pick<StartDialogOptions, "initialStep">) => void;
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
export function defineDialog({
|
||||
id,
|
||||
steps,
|
||||
showOnce = true,
|
||||
initialStep = 0,
|
||||
}: DefineDialogOptions): GuideDialogController {
|
||||
return {
|
||||
id,
|
||||
steps,
|
||||
start: (options) => {
|
||||
useDialogStore.getState().startDialog({
|
||||
courseId: appConfig.course.id,
|
||||
dialogId: id,
|
||||
showOnce,
|
||||
steps,
|
||||
initialStep: options?.initialStep ?? initialStep,
|
||||
});
|
||||
},
|
||||
reset: () => {
|
||||
useDialogStore.getState().resetDialog(appConfig.course.id, id);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import { useCallback } from "react";
|
||||
import {
|
||||
type GuideDialogProps,
|
||||
GuideDialog as RegistryGuideDialog,
|
||||
} from "@/components/course-interactions/guide-dialog";
|
||||
import { useCourseCanvas } from "@/course-runtime/canvas";
|
||||
import { useDialogStore } from "@/stores";
|
||||
import { getAppPortalRoot } from "../canvasPortal";
|
||||
|
||||
export function GuideDialog({
|
||||
children,
|
||||
onFinish,
|
||||
onSkip,
|
||||
...props
|
||||
}: Omit<GuideDialogProps, "actions" | "state" | "viewport"> & {
|
||||
onFinish?: () => void;
|
||||
onSkip?: () => void;
|
||||
}) {
|
||||
const {
|
||||
open,
|
||||
steps,
|
||||
current,
|
||||
nextStep,
|
||||
prevStep,
|
||||
selectOption,
|
||||
finishDialog,
|
||||
skipDialog,
|
||||
} = useDialogStore();
|
||||
const { viewport } = useCourseCanvas();
|
||||
|
||||
const finish = useCallback(() => {
|
||||
finishDialog();
|
||||
onFinish?.();
|
||||
}, [finishDialog, onFinish]);
|
||||
|
||||
const skip = useCallback(() => {
|
||||
skipDialog();
|
||||
onSkip?.();
|
||||
}, [onSkip, skipDialog]);
|
||||
|
||||
return (
|
||||
<RegistryGuideDialog
|
||||
state={{
|
||||
open,
|
||||
steps,
|
||||
current,
|
||||
}}
|
||||
actions={{
|
||||
next: nextStep,
|
||||
prev: prevStep,
|
||||
skip,
|
||||
finish,
|
||||
selectOption,
|
||||
}}
|
||||
container={getAppPortalRoot}
|
||||
viewport={viewport}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</RegistryGuideDialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export type {
|
||||
GuideDialogProps,
|
||||
GuideDialogRenderState,
|
||||
GuideDialogTheme,
|
||||
} from "@/components/course-interactions/guide-dialog";
|
||||
@@ -0,0 +1,11 @@
|
||||
export { GuideDialog } from "./GuideDialog";
|
||||
export type {
|
||||
DefineDialogOptions,
|
||||
GuideDialogController,
|
||||
} from "./GuideDialog.helpers";
|
||||
export { defineDialog } from "./GuideDialog.helpers";
|
||||
export type {
|
||||
GuideDialogProps,
|
||||
GuideDialogRenderState,
|
||||
GuideDialogTheme,
|
||||
} from "./GuideDialog.types";
|
||||
@@ -0,0 +1,57 @@
|
||||
import { appConfig } from "@/app.config";
|
||||
import { useSpotlightStore } from "@/stores";
|
||||
import type {
|
||||
SpotlightStep,
|
||||
StartSpotlightOptions,
|
||||
} from "@/stores/types/spotlight.types";
|
||||
|
||||
type DefineSpotlightStep = Omit<SpotlightStep, "target"> & {
|
||||
target?: SpotlightStep["target"];
|
||||
};
|
||||
|
||||
export interface DefineSpotlightOptions {
|
||||
id: string;
|
||||
steps: DefineSpotlightStep[];
|
||||
showOnce?: boolean;
|
||||
initialStep?: number;
|
||||
}
|
||||
|
||||
export interface SpotlightController {
|
||||
id: string;
|
||||
steps: SpotlightStep[];
|
||||
start: (options?: Pick<StartSpotlightOptions, "initialStep">) => void;
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
function normalizeSteps(steps: DefineSpotlightStep[]): SpotlightStep[] {
|
||||
return steps.map((step) => ({
|
||||
...step,
|
||||
target: step.target ?? { dataGuide: step.id },
|
||||
}));
|
||||
}
|
||||
|
||||
export function defineSpotlight({
|
||||
id,
|
||||
steps,
|
||||
showOnce = true,
|
||||
initialStep = 0,
|
||||
}: DefineSpotlightOptions): SpotlightController {
|
||||
const normalizedSteps = normalizeSteps(steps);
|
||||
|
||||
return {
|
||||
id,
|
||||
steps: normalizedSteps,
|
||||
start: (options) => {
|
||||
useSpotlightStore.getState().startSpotlight({
|
||||
courseId: appConfig.course.id,
|
||||
spotlightId: id,
|
||||
showOnce,
|
||||
steps: normalizedSteps,
|
||||
initialStep: options?.initialStep ?? initialStep,
|
||||
});
|
||||
},
|
||||
reset: () => {
|
||||
useSpotlightStore.getState().resetSpotlight(appConfig.course.id, id);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import { useCallback } from "react";
|
||||
import {
|
||||
type GuideSpotlightProps,
|
||||
GuideSpotlight as RegistryGuideSpotlight,
|
||||
} from "@/components/course-interactions/guide-spotlight";
|
||||
import { useCourseCanvas } from "@/course-runtime/canvas";
|
||||
import { useSpotlightStore } from "@/stores";
|
||||
import { getAppPortalRoot } from "../canvasPortal";
|
||||
|
||||
export function GuideSpotlight({
|
||||
children,
|
||||
onFinish,
|
||||
onSkip,
|
||||
container = getAppPortalRoot,
|
||||
viewport,
|
||||
...props
|
||||
}: Omit<GuideSpotlightProps, "actions" | "state"> & {
|
||||
onFinish?: () => void;
|
||||
onSkip?: () => void;
|
||||
}) {
|
||||
const {
|
||||
open,
|
||||
steps,
|
||||
current,
|
||||
nextStep,
|
||||
prevStep,
|
||||
finishSpotlight,
|
||||
skipSpotlight,
|
||||
} = useSpotlightStore();
|
||||
const { viewport: canvasViewport } = useCourseCanvas();
|
||||
|
||||
const finish = useCallback(() => {
|
||||
finishSpotlight();
|
||||
onFinish?.();
|
||||
}, [finishSpotlight, onFinish]);
|
||||
|
||||
const skip = useCallback(() => {
|
||||
skipSpotlight();
|
||||
onSkip?.();
|
||||
}, [onSkip, skipSpotlight]);
|
||||
|
||||
return (
|
||||
<RegistryGuideSpotlight
|
||||
state={{
|
||||
open,
|
||||
steps,
|
||||
current,
|
||||
}}
|
||||
actions={{
|
||||
next: nextStep,
|
||||
prev: prevStep,
|
||||
skip,
|
||||
finish,
|
||||
}}
|
||||
container={container}
|
||||
viewport={viewport ?? canvasViewport}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</RegistryGuideSpotlight>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export type {
|
||||
GuideSpotlightProps,
|
||||
GuideSpotlightRenderState,
|
||||
GuideSpotlightTheme,
|
||||
Rect,
|
||||
SpotlightLayout,
|
||||
} from "@/components/course-interactions/guide-spotlight";
|
||||
@@ -0,0 +1,11 @@
|
||||
export { GuideSpotlight } from "./GuideSpotlight";
|
||||
export type {
|
||||
DefineSpotlightOptions,
|
||||
SpotlightController,
|
||||
} from "./GuideSpotlight.helpers";
|
||||
export { defineSpotlight } from "./GuideSpotlight.helpers";
|
||||
export type {
|
||||
GuideSpotlightProps,
|
||||
GuideSpotlightRenderState,
|
||||
GuideSpotlightTheme,
|
||||
} from "./GuideSpotlight.types";
|
||||
@@ -0,0 +1,25 @@
|
||||
export function LandscapePrompt() {
|
||||
return (
|
||||
<div className="fixed inset-0 z-2000 flex items-center justify-center bg-gray-950/80 px-8 text-white">
|
||||
<div className="text-center">
|
||||
<div className="mx-auto mb-2 flex h-16 w-16 items-center justify-center">
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="h-full w-full text-white"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.5}
|
||||
>
|
||||
<rect x="2" y="5" width="20" height="14" rx="2" />
|
||||
<circle cx="12" cy="12" r="1" fill="currentColor" />
|
||||
</svg>
|
||||
</div>
|
||||
<h1 className="text-2xl font-semibold">请旋转屏幕</h1>
|
||||
<p className="mt-3 text-base text-gray-400">
|
||||
为获得最佳体验,请横屏浏览
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { LandscapePrompt } from "./LandscapePrompt";
|
||||
@@ -0,0 +1,39 @@
|
||||
import { isRouteErrorResponse, Link, useRouteError } from "react-router-dom";
|
||||
|
||||
export function RouteError() {
|
||||
const error = useRouteError();
|
||||
const message = isRouteErrorResponse(error)
|
||||
? `${error.status} ${error.statusText}`
|
||||
: error instanceof Error
|
||||
? error.message
|
||||
: "页面加载失败";
|
||||
|
||||
return (
|
||||
<div className="flex min-h-full items-center justify-center p-8">
|
||||
<div className="w-full max-w-xl rounded-3xl border border-rose-200 bg-white p-8 text-center shadow-xl">
|
||||
<p className="text-sm font-semibold text-rose-600">页面运行异常</p>
|
||||
<h1 className="mt-3 text-2xl font-bold text-gray-950">
|
||||
当前示例无法加载
|
||||
</h1>
|
||||
<p className="mt-3 break-words text-sm leading-6 text-gray-600">
|
||||
{message}
|
||||
</p>
|
||||
<div className="mt-6 flex justify-center gap-3">
|
||||
<Link
|
||||
to="/"
|
||||
className="rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white"
|
||||
>
|
||||
返回首页
|
||||
</Link>
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-lg border border-gray-300 px-4 py-2 text-sm font-medium text-gray-700"
|
||||
onClick={() => window.location.reload()}
|
||||
>
|
||||
重新加载
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export function RouteFallback() {
|
||||
return (
|
||||
<div className="flex min-h-[200px] items-center justify-center text-gray-500">
|
||||
加载中...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { RouteError } from "./RouteError";
|
||||
export { RouteFallback } from "./RouteFallback";
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { RefObject } from "react";
|
||||
|
||||
let portalRoot: RefObject<HTMLDivElement | null> | null = null;
|
||||
|
||||
export function registerAppPortalRoot(
|
||||
container: RefObject<HTMLDivElement | null>,
|
||||
): () => void {
|
||||
portalRoot = container;
|
||||
return () => {
|
||||
if (portalRoot === container) portalRoot = null;
|
||||
};
|
||||
}
|
||||
|
||||
export function getAppPortalRoot(): HTMLDivElement | null {
|
||||
return portalRoot?.current ?? null;
|
||||
}
|
||||
@@ -1,2 +1,35 @@
|
||||
export type { ButtonProps } from "./Button";
|
||||
export { Button } from "./Button";
|
||||
export type { ButtonProps } from "@/components/ui/button";
|
||||
export { Button } from "@/components/ui/button";
|
||||
export type { OverlayProps } from "@/components/ui/overlay";
|
||||
export { Overlay } from "@/components/ui/overlay";
|
||||
export type { PortalContainer, PortalProps } from "@/components/ui/portal";
|
||||
export { Portal } from "@/components/ui/portal";
|
||||
export type {
|
||||
ToastContextValue,
|
||||
ToastItem as ToastItemData,
|
||||
ToastOptions,
|
||||
ToastProviderProps,
|
||||
ToastRenderApi,
|
||||
ToastType,
|
||||
ToastViewportClassNames,
|
||||
ToastViewportProps,
|
||||
} from "@/components/ui/toast";
|
||||
export { ToastProvider, ToastViewport, useToast } from "@/components/ui/toast";
|
||||
export type {
|
||||
DefineDialogOptions,
|
||||
GuideDialogController,
|
||||
GuideDialogProps,
|
||||
GuideDialogRenderState,
|
||||
GuideDialogTheme,
|
||||
} from "./GuideDialog";
|
||||
export { defineDialog, GuideDialog } from "./GuideDialog";
|
||||
export type {
|
||||
DefineSpotlightOptions,
|
||||
GuideSpotlightProps,
|
||||
GuideSpotlightRenderState,
|
||||
GuideSpotlightTheme,
|
||||
SpotlightController,
|
||||
} from "./GuideSpotlight";
|
||||
export { defineSpotlight, GuideSpotlight } from "./GuideSpotlight";
|
||||
export { LandscapePrompt } from "./LandscapePrompt";
|
||||
export { RouteError, RouteFallback } from "./RouteFallback";
|
||||
Reference in new issue
Block a user