feat: enhance modal functionality and integrate new components
- Added keyframes and animation for a smooth scale-in effect in the Tailwind configuration. - Updated Image component to improve quality handling and style definitions. - Introduced WordViewer and WordViewerModal components for document viewing capabilities. - Enhanced Modal component to support embedded mode with customizable position and dimensions. - Integrated IframeModal across various pages for user assistance and content display. - Updated Content and Menu pages to utilize new components and improve user interaction. - Added utility function to retrieve public paths for resources.
No files matched your search
|
Before Width: | Height: | Size: 231 KiB After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 231 KiB |
|
After Width: | Height: | Size: 270 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 255 KiB After Width: | Height: | Size: 185 KiB |
@@ -0,0 +1,147 @@
|
|||||||
|
# Alert 组件使用说明
|
||||||
|
|
||||||
|
## 简介
|
||||||
|
|
||||||
|
`Alert` 是一个通用的提示框组件,类似于 Ant Design 的 Alert,用于向用户显示重要的提示信息。
|
||||||
|
|
||||||
|
## 特性
|
||||||
|
|
||||||
|
- 🎨 支持 4 种类型:`warning`、`info`、`success`、`error`
|
||||||
|
- 🎭 可选显示/隐藏图标
|
||||||
|
- ❌ 可选是否可关闭
|
||||||
|
- 🎯 居中模态显示
|
||||||
|
- ✨ 优雅的动画效果
|
||||||
|
|
||||||
|
## 基本用法
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { Alert } from "@/components";
|
||||||
|
|
||||||
|
function MyComponent() {
|
||||||
|
const [showAlert, setShowAlert] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Alert
|
||||||
|
isOpen={showAlert}
|
||||||
|
onClose={() => setShowAlert(false)}
|
||||||
|
message="这是一条提示信息"
|
||||||
|
type="warning"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
### Props
|
||||||
|
|
||||||
|
| 参数 | 说明 | 类型 | 默认值 | 必填 |
|
||||||
|
| -------- | -------------- | --------------------------------------------- | ----------- | ---- |
|
||||||
|
| isOpen | 是否显示 Alert | `boolean` | - | ✅ |
|
||||||
|
| onClose | 关闭回调函数 | `() => void` | - | ✅ |
|
||||||
|
| message | 提示内容 | `string \| ReactNode` | - | ✅ |
|
||||||
|
| type | 提示类型 | `'warning' \| 'info' \| 'success' \| 'error'` | `'warning'` | ❌ |
|
||||||
|
| showIcon | 是否显示图标 | `boolean` | `true` | ❌ |
|
||||||
|
| closable | 是否可关闭 | `boolean` | `true` | ❌ |
|
||||||
|
|
||||||
|
## 示例
|
||||||
|
|
||||||
|
### 警告提示
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Alert
|
||||||
|
isOpen={showWarning}
|
||||||
|
onClose={() => setShowWarning(false)}
|
||||||
|
message="该名师工作室数字睿教正在筹划中"
|
||||||
|
type="warning"
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 成功提示
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Alert
|
||||||
|
isOpen={showSuccess}
|
||||||
|
onClose={() => setShowSuccess(false)}
|
||||||
|
message="操作成功!"
|
||||||
|
type="success"
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 错误提示
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Alert
|
||||||
|
isOpen={showError}
|
||||||
|
onClose={() => setShowError(false)}
|
||||||
|
message="操作失败,请稍后重试"
|
||||||
|
type="error"
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 信息提示
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Alert
|
||||||
|
isOpen={showInfo}
|
||||||
|
onClose={() => setShowInfo(false)}
|
||||||
|
message="这是一条普通信息"
|
||||||
|
type="info"
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 自定义内容
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Alert
|
||||||
|
isOpen={showCustom}
|
||||||
|
onClose={() => setShowCustom(false)}
|
||||||
|
message={
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-bold">自定义标题</h3>
|
||||||
|
<p className="text-sm mt-1">这是自定义的内容</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
type="info"
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 不可关闭的提示
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Alert
|
||||||
|
isOpen={showPermanent}
|
||||||
|
onClose={() => {}}
|
||||||
|
message="这是一条不可关闭的提示"
|
||||||
|
type="info"
|
||||||
|
closable={false}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 无图标提示
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Alert
|
||||||
|
isOpen={showNoIcon}
|
||||||
|
onClose={() => setShowNoIcon(false)}
|
||||||
|
message="这是一条没有图标的提示"
|
||||||
|
type="info"
|
||||||
|
showIcon={false}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 样式说明
|
||||||
|
|
||||||
|
每种类型都有对应的颜色主题:
|
||||||
|
|
||||||
|
- **warning**: 黄色系(适用于警告信息)
|
||||||
|
- **info**: 蓝色系(适用于一般信息)
|
||||||
|
- **success**: 绿色系(适用于成功提示)
|
||||||
|
- **error**: 红色系(适用于错误提示)
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. Alert 组件会创建一个全屏半透明遮罩层
|
||||||
|
2. 点击遮罩层可以关闭 Alert(当 `closable` 为 `true` 时)
|
||||||
|
3. Alert 的 z-index 为 50,确保在大多数元素之上
|
||||||
|
4. 支持动画效果,通过 `animate-scale-in` 类实现
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
import { ReactNode } from "react";
|
||||||
|
|
||||||
|
type AlertType = "warning" | "info" | "success" | "error";
|
||||||
|
|
||||||
|
interface AlertProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
message: string | ReactNode;
|
||||||
|
type?: AlertType;
|
||||||
|
showIcon?: boolean;
|
||||||
|
closable?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const alertStyles: Record<
|
||||||
|
AlertType,
|
||||||
|
{
|
||||||
|
bg: string;
|
||||||
|
border: string;
|
||||||
|
text: string;
|
||||||
|
icon: string;
|
||||||
|
}
|
||||||
|
> = {
|
||||||
|
warning: {
|
||||||
|
bg: "bg-yellow-50",
|
||||||
|
border: "border-yellow-400",
|
||||||
|
text: "text-yellow-800",
|
||||||
|
icon: "text-yellow-400",
|
||||||
|
},
|
||||||
|
info: {
|
||||||
|
bg: "bg-blue-50",
|
||||||
|
border: "border-blue-400",
|
||||||
|
text: "text-blue-800",
|
||||||
|
icon: "text-blue-400",
|
||||||
|
},
|
||||||
|
success: {
|
||||||
|
bg: "bg-green-50",
|
||||||
|
border: "border-green-400",
|
||||||
|
text: "text-green-800",
|
||||||
|
icon: "text-green-400",
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
bg: "bg-red-50",
|
||||||
|
border: "border-red-400",
|
||||||
|
text: "text-red-800",
|
||||||
|
icon: "text-red-400",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const icons = {
|
||||||
|
warning: (
|
||||||
|
<svg className="h-6 w-6" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
info: (
|
||||||
|
<svg className="h-6 w-6" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
success: (
|
||||||
|
<svg className="h-6 w-6" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
error: (
|
||||||
|
<svg className="h-6 w-6" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Alert = ({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
message,
|
||||||
|
type = "warning",
|
||||||
|
showIcon = true,
|
||||||
|
closable = true,
|
||||||
|
}: AlertProps) => {
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
const styles = alertStyles[type];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||||
|
{/* 半透明背景 */}
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 bg-black/50"
|
||||||
|
onClick={closable ? onClose : undefined}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Alert 提示框 */}
|
||||||
|
<div
|
||||||
|
className={`relative ${styles.bg} border-l-4 ${styles.border} px-6 py-4 rounded-md shadow-lg max-w-md mx-4 animate-scale-in`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center">
|
||||||
|
{/* 图标 */}
|
||||||
|
{showIcon && (
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<div className={styles.icon}>{icons[type]}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 内容 */}
|
||||||
|
<div className={`${showIcon ? "ml-3" : ""} flex-1`}>
|
||||||
|
{typeof message === "string" ? (
|
||||||
|
<p className={`text-lg font-medium ${styles.text}`}>{message}</p>
|
||||||
|
) : (
|
||||||
|
message
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 关闭按钮 */}
|
||||||
|
{closable && (
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className={`ml-4 flex-shrink-0 ${styles.icon
|
||||||
|
.replace("text-", "text-")
|
||||||
|
.replace("400", "600")} hover:${styles.icon
|
||||||
|
.replace("text-", "text-")
|
||||||
|
.replace("400", "800")} transition-colors`}
|
||||||
|
>
|
||||||
|
<svg className="h-5 w-5" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -40,7 +40,7 @@ export default function Image({
|
|||||||
onError,
|
onError,
|
||||||
onClick,
|
onClick,
|
||||||
draggable = false,
|
draggable = false,
|
||||||
quality = 80,
|
quality: _quality = 80,
|
||||||
blur = false,
|
blur = false,
|
||||||
button = false,
|
button = false,
|
||||||
}: ImageProps) {
|
}: ImageProps) {
|
||||||
@@ -98,7 +98,7 @@ export default function Image({
|
|||||||
}
|
}
|
||||||
}, [loading, src]);
|
}, [loading, src]);
|
||||||
|
|
||||||
const imageStyle = {
|
const imageStyle: React.CSSProperties = {
|
||||||
width: width,
|
width: width,
|
||||||
height: height,
|
height: height,
|
||||||
objectFit,
|
objectFit,
|
||||||
@@ -110,7 +110,7 @@ export default function Image({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"inline-block",
|
width === "100%" ? "block" : "inline-block",
|
||||||
button &&
|
button &&
|
||||||
"cursor-pointer transition-all duration-300 hover:scale-105 hover:brightness-110",
|
"cursor-pointer transition-all duration-300 hover:scale-105 hover:brightness-110",
|
||||||
className
|
className
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -1,27 +1,110 @@
|
|||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
|
type DisplayMode = "modal" | "embedded";
|
||||||
|
type EmbeddedPosition =
|
||||||
|
| "bottom-right"
|
||||||
|
| "bottom-left"
|
||||||
|
| "top-right"
|
||||||
|
| "top-left";
|
||||||
|
|
||||||
interface ModalProps {
|
interface ModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
mode?: DisplayMode;
|
||||||
|
position?: EmbeddedPosition;
|
||||||
|
width?: string | number;
|
||||||
|
height?: string | number;
|
||||||
|
showMask?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Modal = ({ isOpen, onClose, children, className }: ModalProps) => {
|
export const Modal = ({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
mode = "modal",
|
||||||
|
position = "bottom-right",
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
showMask = true,
|
||||||
|
}: ModalProps) => {
|
||||||
if (!isOpen) return null;
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
// 嵌入式客服对话框模式
|
||||||
|
if (mode === "embedded") {
|
||||||
|
const getPosition = () => {
|
||||||
|
switch (position) {
|
||||||
|
case "bottom-right":
|
||||||
|
return "bottom-6 right-6";
|
||||||
|
case "bottom-left":
|
||||||
|
return "bottom-6 left-6";
|
||||||
|
case "top-right":
|
||||||
|
return "top-6 right-6";
|
||||||
|
case "top-left":
|
||||||
|
return "top-6 left-6";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultWidth = width || "400px";
|
||||||
|
const defaultHeight = height || "600px";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
"fixed z-[9999] bg-transparent overflow-hidden border border-transparent",
|
||||||
|
"transition-all duration-300 ease-out",
|
||||||
|
"animate-scale-in",
|
||||||
|
getPosition(),
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
width: defaultWidth,
|
||||||
|
height: defaultHeight,
|
||||||
|
maxHeight: "calc(100vh - 3rem)",
|
||||||
|
maxWidth: "calc(100vw - 3rem)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* 关闭/最小化按钮 */}
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="absolute top-3 right-3 z-10 w-7 h-7 flex items-center justify-center bg-white hover:bg-gray-100 rounded-full transition-colors shadow-sm border border-gray-200"
|
||||||
|
title="最小化"
|
||||||
|
>
|
||||||
|
<span className="text-gray-500 text-lg font-bold leading-none">
|
||||||
|
−
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* 内容 */}
|
||||||
|
<div className="h-full w-full">{children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 传统弹窗模式
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||||
{/* 背景遮罩 */}
|
{/* 背景遮罩 */}
|
||||||
<div className="absolute inset-0 bg-black/80 backdrop-blur-sm" />
|
{showMask && (
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 bg-black/80 backdrop-blur-sm"
|
||||||
|
onClick={onClose}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 弹窗内容 */}
|
{/* 弹窗内容 */}
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"relative bg-white rounded-lg shadow-xl max-w-7xl max-h-[100vh] w-full mx-4",
|
"relative bg-white rounded-lg shadow-xl max-w-7xl max-h-[100vh] w-full mx-4",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
|
style={{
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{/* 关闭按钮 */}
|
{/* 关闭按钮 */}
|
||||||
<button
|
<button
|
||||||
@@ -43,6 +126,11 @@ interface IframeModalProps {
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
url: string;
|
url: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
|
mode?: DisplayMode;
|
||||||
|
position?: EmbeddedPosition;
|
||||||
|
width?: string | number;
|
||||||
|
height?: string | number;
|
||||||
|
showMask?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const IframeModal = ({
|
export const IframeModal = ({
|
||||||
@@ -50,6 +138,11 @@ export const IframeModal = ({
|
|||||||
onClose,
|
onClose,
|
||||||
url,
|
url,
|
||||||
title,
|
title,
|
||||||
|
mode = "modal",
|
||||||
|
position = "bottom-right",
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
showMask = true,
|
||||||
}: IframeModalProps) => {
|
}: IframeModalProps) => {
|
||||||
const [hasError, setHasError] = useState(false);
|
const [hasError, setHasError] = useState(false);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
@@ -88,16 +181,25 @@ export const IframeModal = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={isOpen} onClose={onClose} className="max-w-5xl">
|
<Modal
|
||||||
<div className="h-[90vh] flex flex-col">
|
isOpen={isOpen}
|
||||||
{title && (
|
onClose={onClose}
|
||||||
<div className="px-6 py-4 border-b border-gray-200">
|
mode={mode}
|
||||||
|
position={position}
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
showMask={showMask}
|
||||||
|
className={mode === "modal" ? "max-w-5xl" : ""}
|
||||||
|
>
|
||||||
|
<div className="h-full flex flex-col">
|
||||||
|
{title && mode === "modal" && (
|
||||||
|
<div className="px-6 py-4 border-b border-transparent">
|
||||||
<h3 className="text-lg font-semibold text-gray-800">{title}</h3>
|
<h3 className="text-lg font-semibold text-gray-800">{title}</h3>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex-1 relative">
|
<div className="flex-1 relative">
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<div className="absolute inset-0 flex items-center justify-center bg-gray-50">
|
<div className="absolute inset-0 flex items-center justify-center bg-transparent">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500 mx-auto mb-4"></div>
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500 mx-auto mb-4"></div>
|
||||||
<p className="text-gray-600">正在加载...</p>
|
<p className="text-gray-600">正在加载...</p>
|
||||||
@@ -106,7 +208,7 @@ export const IframeModal = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{hasError ? (
|
{hasError ? (
|
||||||
<div className="absolute inset-0 flex items-center justify-center bg-gray-50">
|
<div className="absolute inset-0 flex items-center justify-center bg-transparent">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="text-red-500 text-6xl mb-4">⚠️</div>
|
<div className="text-red-500 text-6xl mb-4">⚠️</div>
|
||||||
<h3 className="text-lg font-semibold text-gray-800 mb-2">
|
<h3 className="text-lg font-semibold text-gray-800 mb-2">
|
||||||
@@ -127,7 +229,7 @@ export const IframeModal = ({
|
|||||||
) : (
|
) : (
|
||||||
<iframe
|
<iframe
|
||||||
src={url}
|
src={url}
|
||||||
className="w-full h-full border-0"
|
className="w-full h-full border-0 bg-transparent"
|
||||||
title={title || "嵌入页面"}
|
title={title || "嵌入页面"}
|
||||||
allow="microphone; camera; autoplay; fullscreen; encrypted-media; picture-in-picture; web-share; accelerometer; gyroscope; magnetometer; payment; usb; serial; bluetooth; xr-spatial-tracking"
|
allow="microphone; camera; autoplay; fullscreen; encrypted-media; picture-in-picture; web-share; accelerometer; gyroscope; magnetometer; payment; usb; serial; bluetooth; xr-spatial-tracking"
|
||||||
referrerPolicy="no-referrer-when-downgrade"
|
referrerPolicy="no-referrer-when-downgrade"
|
||||||
|
|||||||
@@ -0,0 +1,215 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import mammoth from "mammoth";
|
||||||
|
|
||||||
|
interface WordViewerProps {
|
||||||
|
/** Word 文档的路径 */
|
||||||
|
docPath: string;
|
||||||
|
/** 容器的类名 */
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WordViewer = ({ docPath, className = "" }: WordViewerProps) => {
|
||||||
|
const [htmlContent, setHtmlContent] = useState<string>("");
|
||||||
|
const [loading, setLoading] = useState<boolean>(true);
|
||||||
|
const [error, setError] = useState<string>("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const loadDocument = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setError("");
|
||||||
|
|
||||||
|
// 获取 Word 文档
|
||||||
|
const response = await fetch(docPath);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("无法加载文档");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换为 ArrayBuffer
|
||||||
|
const arrayBuffer = await response.arrayBuffer();
|
||||||
|
|
||||||
|
// 使用 mammoth 转换为 HTML
|
||||||
|
const result = await mammoth.convertToHtml({
|
||||||
|
arrayBuffer: arrayBuffer,
|
||||||
|
});
|
||||||
|
|
||||||
|
setHtmlContent(result.value);
|
||||||
|
|
||||||
|
// 如果有警告信息,可以在控制台输出
|
||||||
|
if (result.messages.length > 0) {
|
||||||
|
console.warn("文档转换警告:", result.messages);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("加载文档失败:", err);
|
||||||
|
setError(err instanceof Error ? err.message : "加载文档失败");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadDocument();
|
||||||
|
}, [docPath]);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className={`flex justify-center items-center ${className}`}>
|
||||||
|
<div className="text-[#82B2D6] text-2xl font-bold">加载中...</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className={`flex justify-center items-center ${className}`}>
|
||||||
|
<div className="text-red-500 text-2xl font-bold">加载失败: {error}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`h-full flex flex-col p-4 ${className}`}>
|
||||||
|
{/* 头部工具栏 */}
|
||||||
|
{/* <div className="flex justify-between items-center mb-4">
|
||||||
|
<h3 className="text-[#82B2D6] text-2xl font-bold">工作室活动文稿</h3>
|
||||||
|
<a
|
||||||
|
href={docPath}
|
||||||
|
download="工作室活动文稿.docx"
|
||||||
|
className="px-6 py-2 bg-[#82B2D6] text-white rounded-lg hover:bg-[#6a9bc4] transition-colors text-sm font-medium"
|
||||||
|
>
|
||||||
|
下载文档
|
||||||
|
</a>
|
||||||
|
</div> */}
|
||||||
|
|
||||||
|
{/* 文档内容区域 custom-scrollbar*/}
|
||||||
|
<div className="flex-1 overflow-y-auto bg-white rounded-lg px-4 py-0 ">
|
||||||
|
<div
|
||||||
|
className="word-content"
|
||||||
|
dangerouslySetInnerHTML={{ __html: htmlContent }}
|
||||||
|
style={{
|
||||||
|
maxWidth: "100%",
|
||||||
|
color: "#333",
|
||||||
|
lineHeight: "1.8",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 内联样式 - 美化文档内容 */}
|
||||||
|
<style>{`
|
||||||
|
.word-content h1 {
|
||||||
|
font-size: 2em;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 1em 0 0.5em;
|
||||||
|
color: #1a1a1a;
|
||||||
|
border-bottom: 2px solid #82B2D6;
|
||||||
|
padding-bottom: 0.3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.word-content h2 {
|
||||||
|
font-size: 1.5em;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0.8em 0 0.4em;
|
||||||
|
color: #2a2a2a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.word-content h3 {
|
||||||
|
font-size: 1.3em;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0.6em 0 0.3em;
|
||||||
|
color: #3a3a3a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.word-content p {
|
||||||
|
margin: 0.8em 0;
|
||||||
|
text-align: justify;
|
||||||
|
text-indent: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表格内的段落不需要缩进 */
|
||||||
|
.word-content table p {
|
||||||
|
text-indent: 0;
|
||||||
|
margin: 0.4em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.word-content img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
display: block;
|
||||||
|
margin: 1.5em auto;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.word-content ul,
|
||||||
|
.word-content ol {
|
||||||
|
margin: 0.8em 0;
|
||||||
|
padding-left: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.word-content li {
|
||||||
|
margin: 0.4em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.word-content table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin: 1em 0;
|
||||||
|
table-layout: fixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.word-content table th,
|
||||||
|
.word-content table td {
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
padding: 0.8em;
|
||||||
|
text-align: left;
|
||||||
|
vertical-align: top;
|
||||||
|
word-wrap: break-word;
|
||||||
|
word-break: break-word;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.word-content table th {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表格第一列(标题列)固定宽度 */
|
||||||
|
.word-content table tr td:first-child,
|
||||||
|
.word-content table tr th:first-child {
|
||||||
|
width: 200px;
|
||||||
|
min-width: 200px;
|
||||||
|
white-space: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.word-content strong,
|
||||||
|
.word-content b {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #1a1a1a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.word-content em,
|
||||||
|
.word-content i {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.word-content a {
|
||||||
|
color: #82B2D6;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.word-content a:hover {
|
||||||
|
color: #6a9bc4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.word-content blockquote {
|
||||||
|
border-left: 4px solid #82B2D6;
|
||||||
|
padding-left: 1em;
|
||||||
|
margin: 1em 0;
|
||||||
|
color: #666;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import { X } from "lucide-react";
|
||||||
|
import { WordViewer } from "./WordViewer";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
interface WordViewerModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
docPath: string;
|
||||||
|
title?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WordViewerModal = ({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
docPath,
|
||||||
|
title = "活动文稿",
|
||||||
|
}: WordViewerModalProps) => {
|
||||||
|
// 监听 ESC 键关闭弹窗
|
||||||
|
useEffect(() => {
|
||||||
|
const handleEscape = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === "Escape" && isOpen) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("keydown", handleEscape);
|
||||||
|
return () => document.removeEventListener("keydown", handleEscape);
|
||||||
|
}, [isOpen, onClose]);
|
||||||
|
|
||||||
|
// 阻止背景滚动
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
document.body.style.overflow = "hidden";
|
||||||
|
} else {
|
||||||
|
document.body.style.overflow = "unset";
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.body.style.overflow = "unset";
|
||||||
|
};
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||||
|
{/* 背景遮罩 */}
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 bg-black/50 backdrop-blur-sm"
|
||||||
|
onClick={onClose}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 弹窗内容 */}
|
||||||
|
<div className="relative w-[90vw] h-[90vh] bg-white rounded-2xl shadow-2xl overflow-hidden">
|
||||||
|
{/* 头部 */}
|
||||||
|
<div className="flex items-center justify-between px-6 py-4 bg-gradient-to-r from-[#82B2D6] to-[#6a9bc4] text-white">
|
||||||
|
<h2 className="text-2xl font-bold">{title}</h2>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="p-2 hover:bg-white/20 rounded-lg transition-colors"
|
||||||
|
aria-label="关闭"
|
||||||
|
>
|
||||||
|
<X size={24} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 文档内容区域 */}
|
||||||
|
<div className="h-[calc(100%-64px)] overflow-hidden">
|
||||||
|
<WordViewer docPath={docPath} className="w-full h-full" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,2 +1,5 @@
|
|||||||
export { default as Image } from "./Image";
|
export { default as Image } from "./Image";
|
||||||
export { default as BackgroundImage } from "./BackgroundImage";
|
export { default as BackgroundImage } from "./BackgroundImage";
|
||||||
|
export { WordViewer } from "./WordViewer";
|
||||||
|
export { WordViewerModal } from "./WordViewerModal";
|
||||||
|
export { Alert } from "./Alert";
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
export interface Activity {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
date: string;
|
||||||
|
docPath: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
//技能专项培训
|
||||||
|
export const skillTrainingActivities: Activity[] = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: "曹君名师工作室启动会",
|
||||||
|
date: "2024年7月2日",
|
||||||
|
docPath: "/word/【7月2日】工作室活动情况记录.docx",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: "个人发展规划及专业技能竞赛讨论",
|
||||||
|
date: "2024年10月8日",
|
||||||
|
docPath: "/word/【10月8日】工作室活动情况记录.docx",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: '"南湖杯"学生技能大赛直播电商专业比赛培训',
|
||||||
|
date: "2024年10月16日",
|
||||||
|
docPath: "/word/【10月16日】工作室活动情况记录.docx",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
name: "职业生涯规划讲座",
|
||||||
|
date: "2024年10月18日",
|
||||||
|
docPath: "/word/【10月18日】工作室活动情况记录.docx",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
name: '"南湖杯"直播电商专业比赛复赛(学生赛、教师赛)培训',
|
||||||
|
date: "2024年11月6日",
|
||||||
|
docPath: "/word/【11月6日】工作室活动情况记录.docx",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
name: '"南湖杯"直播销售教师赛项',
|
||||||
|
date: "2024年11月14日",
|
||||||
|
docPath: "/word/【11月14日】工作室活动情况记录.docx",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 7,
|
||||||
|
name: "院级规划教材建设研讨会",
|
||||||
|
date: "2024年11月14日",
|
||||||
|
docPath: "/word/【11月15日】工作室活动情况记录.docx",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
//交流与展示活动
|
||||||
|
export const exchangeAndShowActivities: Activity[] = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: "南湖师资助力乡村振兴新发展",
|
||||||
|
date: "2024年08月30日",
|
||||||
|
docPath: "/word/南湖师资助力乡村振兴新发展.docx",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: "从黄浦江畔到雪域高原 沪青协作赋能果洛“土特产”出圈——对口支援新实践:从“输血”到“造血”的数智帮扶",
|
||||||
|
date: "2025年09月01日",
|
||||||
|
docPath:
|
||||||
|
"/word/从黄浦江畔到雪域高原 沪青协作赋能果洛“土特产”出圈——对口支援新实践:从“输血”到“造血”的数智帮扶.docx",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: "与桂同心育新农,山海携手促共富",
|
||||||
|
date: "2025年",
|
||||||
|
docPath: "/word/与桂同心育新农,山海携手促共富.docx",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
name: "产教融合育新苗 直播赋能助振兴 ——智慧商旅系学子赴浦江边检站开展助农直播实践",
|
||||||
|
date: "2025年09月26日",
|
||||||
|
docPath:
|
||||||
|
"/word/产教融合育新苗 直播赋能助振兴 ——智慧商旅系学子赴浦江边检站开展助农直播实践.docx",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
name: "政校携手,口岸直播助侗乡——南湖学子浦江口岸助农实践出新彩",
|
||||||
|
date: "2025年10月20日",
|
||||||
|
docPath:
|
||||||
|
"/word/政校携手,口岸直播助侗乡——南湖学子浦江口岸助农实践出新彩.docx",
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -1,9 +1,14 @@
|
|||||||
import { BackgroundImage, Image } from "@/components";
|
import { BackgroundImage, Image, WordViewerModal } from "@/components";
|
||||||
import { IframeModal } from "@/components/Modal";
|
import { IframeModal } from "@/components/Modal";
|
||||||
import { getStaticImage } from "@/utils";
|
import { getStaticImage, getPublicPath } from "@/utils";
|
||||||
|
import {
|
||||||
|
skillTrainingActivities,
|
||||||
|
exchangeAndShowActivities,
|
||||||
|
Activity,
|
||||||
|
} from "@/data/activities";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* tab列表
|
* tab列表
|
||||||
@@ -50,26 +55,28 @@ const tabList = [
|
|||||||
const sideBarList = [
|
const sideBarList = [
|
||||||
{
|
{
|
||||||
name: "理论与技能转向培训",
|
name: "理论与技能转向培训",
|
||||||
|
key: 0,
|
||||||
image: getStaticImage("content/理论与技能专项培训-未选中.png"),
|
image: getStaticImage("content/理论与技能专项培训-未选中.png"),
|
||||||
selectedImage: getStaticImage("content/理论与技能专项培训-选中.png"),
|
selectedImage: getStaticImage("content/理论与技能专项培训-选中.png"),
|
||||||
},
|
},
|
||||||
{
|
// {
|
||||||
name: "教学实践与反思",
|
// name: "教学实践与反思",
|
||||||
image: getStaticImage("content/教学实践与反思-未选中.png"),
|
// image: getStaticImage("content/教学实践与反思-未选中.png"),
|
||||||
selectedImage: getStaticImage("content/教学实践与反思-选中.png"),
|
// selectedImage: getStaticImage("content/教学实践与反思-选中.png"),
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
name: "项目与课题研究",
|
// name: "项目与课题研究",
|
||||||
image: getStaticImage("content/项目与课题研究-未选中.png"),
|
// image: getStaticImage("content/项目与课题研究-未选中.png"),
|
||||||
selectedImage: getStaticImage("content/项目与课题研究-选中.png"),
|
// selectedImage: getStaticImage("content/项目与课题研究-选中.png"),
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
name: "经验传承与资源共享",
|
// name: "经验传承与资源共享",
|
||||||
image: getStaticImage("content/经验传承与资源共享-未选中.png"),
|
// image: getStaticImage("content/经验传承与资源共享-未选中.png"),
|
||||||
selectedImage: getStaticImage("content/经验传承与资源共享-选中.png"),
|
// selectedImage: getStaticImage("content/经验传承与资源共享-选中.png"),
|
||||||
},
|
// },
|
||||||
{
|
{
|
||||||
name: "交流与展示活动",
|
name: "交流与展示活动",
|
||||||
|
key: 4,
|
||||||
image: getStaticImage("content/交流与展示活动-未选中.png"),
|
image: getStaticImage("content/交流与展示活动-未选中.png"),
|
||||||
selectedImage: getStaticImage("content/交流与展示活动-选中.png"),
|
selectedImage: getStaticImage("content/交流与展示活动-选中.png"),
|
||||||
},
|
},
|
||||||
@@ -78,11 +85,13 @@ const sideBarList = [
|
|||||||
export const Tab = ({
|
export const Tab = ({
|
||||||
changeTab,
|
changeTab,
|
||||||
className,
|
className,
|
||||||
|
defaultTabId = 1,
|
||||||
}: {
|
}: {
|
||||||
changeTab?: (id: number) => void;
|
changeTab?: (id: number) => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
defaultTabId?: number;
|
||||||
}) => {
|
}) => {
|
||||||
const [selectedTabId, setSelectedTabId] = useState(1);
|
const [selectedTabId, setSelectedTabId] = useState(defaultTabId);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* tab */}
|
{/* tab */}
|
||||||
@@ -102,7 +111,7 @@ export const Tab = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<TabContent
|
<TabContent
|
||||||
className="h-[calc(100%-200px)]"
|
className="h-[calc(100%-180px)]"
|
||||||
selectedTabId={selectedTabId}
|
selectedTabId={selectedTabId}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
@@ -117,33 +126,67 @@ const TabContent = ({
|
|||||||
selectedTabId: number;
|
selectedTabId: number;
|
||||||
}) => {
|
}) => {
|
||||||
const [selectedSideBarId, setSelectedSideBarId] = useState(0);
|
const [selectedSideBarId, setSelectedSideBarId] = useState(0);
|
||||||
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
const [isActivitiesModalOpen, setIsActivitiesModalOpen] = useState(false);
|
||||||
|
const [selectedActivity, setSelectedActivity] = useState<Activity | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
const [selectedActivities, setSelectedActivities] = useState<Activity | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleActivityClick = (activity: Activity) => {
|
||||||
|
setSelectedActivity(activity);
|
||||||
|
setIsModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleActivitiesClick = (activities: Activity) => {
|
||||||
|
setSelectedActivities(activities);
|
||||||
|
setIsActivitiesModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseModal = () => {
|
||||||
|
setIsModalOpen(false);
|
||||||
|
setSelectedActivity(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleActivitiesCloseModal = () => {
|
||||||
|
setIsActivitiesModalOpen(false);
|
||||||
|
setSelectedActivities(null);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={clsx("overflow-y-auto custom-scrollbar", className)}>
|
<div className={clsx("overflow-y-auto custom-scrollbar", className)}>
|
||||||
{/* 通过tab切换内容 */}
|
{/* 通过tab切换内容 */}
|
||||||
{selectedTabId === 1 && (
|
{selectedTabId === 1 && (
|
||||||
<Image
|
<div className="w-full">
|
||||||
height={942}
|
<Image
|
||||||
src={getStaticImage("content/图文.png")}
|
width={"100%"}
|
||||||
alt="工作室介绍"
|
height={"auto"}
|
||||||
/>
|
src={getStaticImage("content/图文.png")}
|
||||||
|
alt="工作室介绍"
|
||||||
|
className="w-full h-auto"
|
||||||
|
objectFit="contain"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
{selectedTabId === 2 && (
|
{selectedTabId === 2 && (
|
||||||
<div className="flex gap-10 pt-8">
|
<div className="flex gap-10 pt-8">
|
||||||
{/* 侧边栏 */}
|
{/* 侧边栏 */}
|
||||||
<div className="flex flex-col gap-10">
|
<div className="flex flex-col gap-10">
|
||||||
{sideBarList.map((item, index) => (
|
{sideBarList.map((item) => (
|
||||||
<Image
|
<Image
|
||||||
key={item.name}
|
key={item.key}
|
||||||
src={
|
src={
|
||||||
selectedSideBarId === index
|
selectedSideBarId === item.key
|
||||||
? item.selectedImage
|
? item.selectedImage
|
||||||
: item.image
|
: item.image
|
||||||
}
|
}
|
||||||
alt={item.name}
|
alt={item.name}
|
||||||
button
|
button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedSideBarId(index);
|
setSelectedSideBarId(item.key);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@@ -154,17 +197,115 @@ const TabContent = ({
|
|||||||
height={766}
|
height={766}
|
||||||
>
|
>
|
||||||
<div className="h-full flex justify-center items-center">
|
<div className="h-full flex justify-center items-center">
|
||||||
<span className="text-[#82B2D6] text-4xl font-bold">
|
{selectedSideBarId === 0 ? (
|
||||||
图文、视频展示理论与技能专项培训内容
|
<>
|
||||||
</span>
|
{/* 技能专项培训列表 */}
|
||||||
|
<div className="w-full h-full p-8 overflow-y-auto custom-scrollbar">
|
||||||
|
{/* 活动列表表格 */}
|
||||||
|
<div className="bg-white rounded-lg shadow-lg overflow-hidden">
|
||||||
|
<table className="w-full">
|
||||||
|
<thead>
|
||||||
|
<tr className="bg-gradient-to-r from-[#82B2D6] to-[#6a9bc4] text-white">
|
||||||
|
<th className="px-6 py-4 text-left text-lg font-semibold w-[70%]">
|
||||||
|
活动名称
|
||||||
|
</th>
|
||||||
|
<th className="px-6 py-4 text-left text-lg font-semibold w-[30%]">
|
||||||
|
时间
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{skillTrainingActivities.map((activity, index) => (
|
||||||
|
<tr
|
||||||
|
key={activity.id}
|
||||||
|
onClick={() => handleActivityClick(activity)}
|
||||||
|
className={clsx(
|
||||||
|
"border-b border-gray-200 cursor-pointer transition-all duration-200",
|
||||||
|
"hover:bg-[#82B2D6]/10 hover:shadow-md",
|
||||||
|
index % 2 === 0 ? "bg-white" : "bg-gray-50"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<td className="px-6 py-4 text-gray-800 font-medium">
|
||||||
|
{activity.name}
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 text-gray-600">
|
||||||
|
{activity.date}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : selectedSideBarId === 1 ? (
|
||||||
|
<span className="text-[#82B2D6] text-4xl font-bold">
|
||||||
|
图文、视频展示教学实践与反思内容
|
||||||
|
</span>
|
||||||
|
) : selectedSideBarId === 2 ? (
|
||||||
|
<span className="text-[#82B2D6] text-4xl font-bold">
|
||||||
|
图文、视频展示项目与课题研究内容
|
||||||
|
</span>
|
||||||
|
) : selectedSideBarId === 3 ? (
|
||||||
|
<span className="text-[#82B2D6] text-4xl font-bold">
|
||||||
|
图文、视频展示经验传承与资源共享内容
|
||||||
|
</span>
|
||||||
|
) : selectedSideBarId === 4 ? (
|
||||||
|
<div className="w-full h-full pt-4 relative">
|
||||||
|
{/* 历史活动列表 */}
|
||||||
|
<div className="w-full h-full p-8 overflow-y-auto custom-scrollbar">
|
||||||
|
{/* 活动列表表格 */}
|
||||||
|
<div className="bg-white rounded-lg shadow-lg overflow-hidden">
|
||||||
|
<table className="w-full">
|
||||||
|
<thead>
|
||||||
|
<tr className="bg-gradient-to-r from-[#82B2D6] to-[#6a9bc4] text-white">
|
||||||
|
<th className="px-6 py-4 text-left text-lg font-semibold w-[70%]">
|
||||||
|
活动名称
|
||||||
|
</th>
|
||||||
|
<th className="px-6 py-4 text-left text-lg font-semibold w-[30%]">
|
||||||
|
时间
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{exchangeAndShowActivities.map(
|
||||||
|
(activity, index) => (
|
||||||
|
<tr
|
||||||
|
key={activity.id}
|
||||||
|
onClick={() =>
|
||||||
|
handleActivitiesClick(activity)
|
||||||
|
}
|
||||||
|
className={clsx(
|
||||||
|
"border-b border-gray-200 cursor-pointer transition-all duration-200",
|
||||||
|
"hover:bg-[#82B2D6]/10 hover:shadow-md",
|
||||||
|
index % 2 === 0 ? "bg-white" : "bg-gray-50"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<td className="px-6 py-4 text-gray-800 font-medium">
|
||||||
|
{activity.name}
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 text-gray-600">
|
||||||
|
{activity.date}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</BackgroundImage>
|
</BackgroundImage>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{selectedTabId === 3 && (
|
{selectedTabId === 3 && (
|
||||||
<div className="text-center text-[#82B2D6] text-4xl font-bold">
|
<Image
|
||||||
暂无内容
|
height={1415}
|
||||||
</div>
|
src={getStaticImage("content/图文3.png")}
|
||||||
|
alt="工作室介绍"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{selectedTabId === 4 && (
|
{selectedTabId === 4 && (
|
||||||
<div className="text-center text-[#82B2D6] text-4xl font-bold">
|
<div className="text-center text-[#82B2D6] text-4xl font-bold">
|
||||||
@@ -172,14 +313,36 @@ const TabContent = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 活动文档弹窗 */}
|
||||||
|
{selectedActivity && (
|
||||||
|
<WordViewerModal
|
||||||
|
isOpen={isModalOpen}
|
||||||
|
onClose={handleCloseModal}
|
||||||
|
docPath={getPublicPath(selectedActivity.docPath)}
|
||||||
|
title={selectedActivity.name}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{selectedActivities && (
|
||||||
|
<WordViewerModal
|
||||||
|
isOpen={isActivitiesModalOpen}
|
||||||
|
onClose={handleActivitiesCloseModal}
|
||||||
|
docPath={getPublicPath(selectedActivities.docPath)}
|
||||||
|
title={selectedActivities.name}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function PublicityPage() {
|
export default function PublicityPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const { id } = useParams<{ id: string }>();
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
|
||||||
|
// 将路由参数 id 转换为数字,如果无效则默认为 1
|
||||||
|
const defaultTabId = id ? parseInt(id, 10) : 1;
|
||||||
|
|
||||||
const changeTab = (id: number) => {
|
const changeTab = (id: number) => {
|
||||||
console.log(id);
|
console.log(id);
|
||||||
};
|
};
|
||||||
@@ -211,28 +374,35 @@ export default function PublicityPage() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 数字人头像 */}
|
{/* 数字人头像 */}
|
||||||
<div
|
{!isModalOpen && (
|
||||||
className="flex flex-col items-center absolute right-4 bottom-2 cursor-pointer hover:scale-105 transition-all duration-300 z-10"
|
<>
|
||||||
onClick={handleAvatarClick}
|
<div
|
||||||
>
|
className="flex flex-col items-center absolute right-4 bottom-2 cursor-pointer hover:scale-105 transition-all duration-300 z-10"
|
||||||
<Image
|
onClick={handleAvatarClick}
|
||||||
src={getStaticImage("publicity/数字人-头像框.png")}
|
>
|
||||||
alt="数字人头像"
|
<Image
|
||||||
/>
|
src={getStaticImage("publicity/数字人-头像框.png")}
|
||||||
<span className="text-white text-2xl font-bold tracking-wide">
|
alt="数字人头像"
|
||||||
点击我随时为你解答
|
/>
|
||||||
</span>
|
<span className="text-white text-2xl font-bold tracking-wide">
|
||||||
</div>
|
点击我随时为你解答
|
||||||
|
</span>
|
||||||
<Tab changeTab={changeTab} />
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<Tab changeTab={changeTab} defaultTabId={defaultTabId} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 弹窗 */}
|
{/* 嵌入式客服对话框 */}
|
||||||
<IframeModal
|
<IframeModal
|
||||||
isOpen={isModalOpen}
|
isOpen={isModalOpen}
|
||||||
onClose={handleCloseModal}
|
onClose={handleCloseModal}
|
||||||
url="https://human-train.lkz.fit/ad905c35-886a-4abb-98c0-e7f894d86b4c/"
|
url="https://www.edutech.host/digitalHuman/"
|
||||||
title="数字人助手"
|
title="数字人助手"
|
||||||
|
mode="embedded"
|
||||||
|
position="bottom-right"
|
||||||
|
width="420px"
|
||||||
|
height="650px"
|
||||||
/>
|
/>
|
||||||
</BackgroundImage>
|
</BackgroundImage>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { BackgroundImage, Image } from "@/components";
|
import { BackgroundImage, Image, Alert } from "@/components";
|
||||||
import { getStaticImage } from "@/utils";
|
import { getStaticImage } from "@/utils";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { IframeModal } from "@/components/Modal";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 卢航-未选中.png
|
* 卢航-未选中.png
|
||||||
@@ -33,7 +34,7 @@ const teachers = [
|
|||||||
id: 3,
|
id: 3,
|
||||||
name: "龚丹",
|
name: "龚丹",
|
||||||
avatar: getStaticImage("menu/龚丹-未选中.png"),
|
avatar: getStaticImage("menu/龚丹-未选中.png"),
|
||||||
selectedAvatar: getStaticImage("menu/曹君-选中.png"),
|
selectedAvatar: getStaticImage("menu/龚丹-选中.png"),
|
||||||
position: {
|
position: {
|
||||||
top: "20%",
|
top: "20%",
|
||||||
left: "65%",
|
left: "65%",
|
||||||
@@ -44,8 +45,15 @@ const teachers = [
|
|||||||
export default function Home() {
|
export default function Home() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [selectedTeacherId, setSelectedTeacherId] = useState(0);
|
const [selectedTeacherId, setSelectedTeacherId] = useState(0);
|
||||||
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
const [showTipModal, setShowTipModal] = useState(false);
|
||||||
|
|
||||||
const handleSelectTeacher = (teacherId: number) => {
|
const handleSelectTeacher = (teacherId: number) => {
|
||||||
|
if (teacherId !== 2) {
|
||||||
|
// 点击卢航或龚丹时显示提示
|
||||||
|
setShowTipModal(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
setSelectedTeacherId(teacherId);
|
setSelectedTeacherId(teacherId);
|
||||||
navigate("/Publicity");
|
navigate("/Publicity");
|
||||||
};
|
};
|
||||||
@@ -55,6 +63,14 @@ export default function Home() {
|
|||||||
// navigate("/experiment");
|
// navigate("/experiment");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleAvatarClick = () => {
|
||||||
|
setIsModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseModal = () => {
|
||||||
|
setIsModalOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BackgroundImage
|
<BackgroundImage
|
||||||
src={getStaticImage("menu/首页背景.png")}
|
src={getStaticImage("menu/首页背景.png")}
|
||||||
@@ -76,6 +92,44 @@ export default function Home() {
|
|||||||
className="absolute bottom-0 left-1/2 transform -translate-x-1/2"
|
className="absolute bottom-0 left-1/2 transform -translate-x-1/2"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* 数字人头像 */}
|
||||||
|
{!isModalOpen && (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className="flex flex-col items-center absolute right-4 bottom-2 cursor-pointer hover:scale-105 transition-all duration-300 z-20"
|
||||||
|
onClick={handleAvatarClick}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src={getStaticImage("publicity/数字人-头像框.png")}
|
||||||
|
alt="数字人头像"
|
||||||
|
/>
|
||||||
|
<span className="text-white text-2xl font-bold tracking-wide">
|
||||||
|
点击我随时为你解答
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 嵌入式客服对话框 */}
|
||||||
|
<IframeModal
|
||||||
|
isOpen={isModalOpen}
|
||||||
|
onClose={handleCloseModal}
|
||||||
|
url="https://www.edutech.host/digitalHuman/"
|
||||||
|
title="数字人助手"
|
||||||
|
mode="embedded"
|
||||||
|
position="bottom-right"
|
||||||
|
width="420px"
|
||||||
|
height="650px"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 提示弹窗 */}
|
||||||
|
<Alert
|
||||||
|
isOpen={showTipModal}
|
||||||
|
onClose={() => setShowTipModal(false)}
|
||||||
|
message="该名师工作室数字睿教正在筹划中"
|
||||||
|
type="warning"
|
||||||
|
/>
|
||||||
|
|
||||||
<BackgroundImage
|
<BackgroundImage
|
||||||
src={getStaticImage("menu/转盘合.png")}
|
src={getStaticImage("menu/转盘合.png")}
|
||||||
className="absolute w-full h-500 -bottom-1/5 left-0"
|
className="absolute w-full h-500 -bottom-1/5 left-0"
|
||||||
@@ -83,27 +137,38 @@ export default function Home() {
|
|||||||
backgroundPosition="center"
|
backgroundPosition="center"
|
||||||
>
|
>
|
||||||
{teachers.map((teacher) => (
|
{teachers.map((teacher) => (
|
||||||
<Image
|
<>
|
||||||
key={teacher.id}
|
<Image
|
||||||
src={
|
key={teacher.id}
|
||||||
selectedTeacherId === teacher.id
|
src={
|
||||||
? teacher.selectedAvatar
|
|
||||||
: teacher.avatar
|
|
||||||
}
|
|
||||||
alt={teacher.name}
|
|
||||||
onClick={() => handleSelectTeacher(teacher.id)}
|
|
||||||
className="absolute"
|
|
||||||
style={{
|
|
||||||
top:
|
|
||||||
selectedTeacherId === teacher.id ? "4%" : teacher.position.top,
|
|
||||||
left:
|
|
||||||
selectedTeacherId === teacher.id
|
selectedTeacherId === teacher.id
|
||||||
? "36%"
|
? teacher.selectedAvatar
|
||||||
: teacher.position.left,
|
: teacher.avatar
|
||||||
transition: "transform 0.2s ease, z-index 0.2s ease",
|
}
|
||||||
}}
|
alt={teacher.name}
|
||||||
button={selectedTeacherId !== teacher.id}
|
onClick={() => handleSelectTeacher(teacher.id)}
|
||||||
/>
|
className="absolute"
|
||||||
|
style={{
|
||||||
|
top:
|
||||||
|
selectedTeacherId === teacher.id
|
||||||
|
? "4%"
|
||||||
|
: teacher.position.top,
|
||||||
|
left:
|
||||||
|
selectedTeacherId === teacher.id
|
||||||
|
? "36%"
|
||||||
|
: teacher.position.left,
|
||||||
|
transition: "transform 0.2s ease, z-index 0.2s ease",
|
||||||
|
}}
|
||||||
|
button={selectedTeacherId !== teacher.id}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* <div className="absolute top-[25%] left-[12%] text-gray-500 text-3xl font-bold cursor-not-allowed">
|
||||||
|
该名师工作室数字睿教正在筹划中
|
||||||
|
</div>
|
||||||
|
<div className="absolute top-[25%] left-[65%] text-gray-500 text-3xl font-bold cursor-not-allowed">
|
||||||
|
该名师工作室数字睿教正在筹划中
|
||||||
|
</div> */}
|
||||||
|
</>
|
||||||
))}
|
))}
|
||||||
</BackgroundImage>
|
</BackgroundImage>
|
||||||
</BackgroundImage>
|
</BackgroundImage>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { BackgroundImage, Image } from "@/components";
|
import { BackgroundImage, Image } from "@/components";
|
||||||
|
import { IframeModal } from "@/components/Modal";
|
||||||
import { getStaticImage } from "@/utils";
|
import { getStaticImage } from "@/utils";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
@@ -113,6 +114,7 @@ export default function PublicityPage() {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||||
const [selectedFunctionId, setSelectedFunctionId] = useState(0);
|
const [selectedFunctionId, setSelectedFunctionId] = useState(0);
|
||||||
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
|
||||||
const handleOpenDialog = () => {
|
const handleOpenDialog = () => {
|
||||||
setIsDialogOpen(true);
|
setIsDialogOpen(true);
|
||||||
@@ -121,6 +123,13 @@ export default function PublicityPage() {
|
|||||||
const handleCloseDialog = () => {
|
const handleCloseDialog = () => {
|
||||||
setIsDialogOpen(false);
|
setIsDialogOpen(false);
|
||||||
};
|
};
|
||||||
|
const handleAvatarClick = () => {
|
||||||
|
setIsModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseModal = () => {
|
||||||
|
setIsModalOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BackgroundImage
|
<BackgroundImage
|
||||||
@@ -168,17 +177,46 @@ export default function PublicityPage() {
|
|||||||
className="absolute left-0 -bottom-20"
|
className="absolute left-0 -bottom-20"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* 数字人头像 */}
|
||||||
|
{!isModalOpen && (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className="flex flex-col items-center absolute right-4 bottom-2 cursor-pointer hover:scale-105 transition-all duration-300 z-10"
|
||||||
|
onClick={handleAvatarClick}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src={getStaticImage("publicity/数字人-头像框.png")}
|
||||||
|
alt="数字人头像"
|
||||||
|
/>
|
||||||
|
<span className="text-white text-2xl font-bold tracking-wide">
|
||||||
|
点击我随时为你解答
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
{/* 提示 */}
|
{/* 提示 */}
|
||||||
<Image
|
{/* <Image
|
||||||
src={getStaticImage("publicity/点击提示.png")}
|
src={getStaticImage("publicity/点击提示.png")}
|
||||||
alt="提示"
|
alt="提示"
|
||||||
className="absolute left-0 -bottom-10"
|
className="absolute left-0 -bottom-10"
|
||||||
button
|
button
|
||||||
onClick={handleOpenDialog}
|
onClick={handleOpenDialog}
|
||||||
/>
|
/> */}
|
||||||
|
|
||||||
{/* 解答弹窗 */}
|
{/* 解答弹窗 */}
|
||||||
<DialogBox isOpen={isDialogOpen} onClose={handleCloseDialog} />
|
<DialogBox isOpen={isDialogOpen} onClose={handleCloseDialog} />
|
||||||
|
|
||||||
|
{/* 嵌入式客服对话框 */}
|
||||||
|
<IframeModal
|
||||||
|
isOpen={isModalOpen}
|
||||||
|
onClose={handleCloseModal}
|
||||||
|
url="https://www.edutech.host/digitalHuman/"
|
||||||
|
title="数字人助手"
|
||||||
|
mode="embedded"
|
||||||
|
position="bottom-right"
|
||||||
|
width="420px"
|
||||||
|
height="650px"
|
||||||
|
/>
|
||||||
</BackgroundImage>
|
</BackgroundImage>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -29,14 +29,4 @@ declare module "*.webp" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Vite 类型声明
|
// Vite 类型声明
|
||||||
interface ImportMeta {
|
/// <reference types="vite/client" />
|
||||||
glob: (
|
|
||||||
pattern: string,
|
|
||||||
options?: {
|
|
||||||
eager?: boolean;
|
|
||||||
import?: string;
|
|
||||||
as?: string;
|
|
||||||
}
|
|
||||||
) => Record<string, any>;
|
|
||||||
url: string;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
interface ImportMeta {
|
||||||
|
glob: (
|
||||||
|
pattern: string,
|
||||||
|
options?: {
|
||||||
|
eager?: boolean;
|
||||||
|
import?: string;
|
||||||
|
as?: string;
|
||||||
|
}
|
||||||
|
) => Record<string, any>;
|
||||||
|
url: string;
|
||||||
|
env: {
|
||||||
|
BASE_URL: string;
|
||||||
|
MODE: string;
|
||||||
|
DEV: boolean;
|
||||||
|
PROD: boolean;
|
||||||
|
SSR: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
declare module "mammoth" {
|
||||||
|
interface ConvertToHtmlOptions {
|
||||||
|
arrayBuffer: ArrayBuffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ConvertToHtmlResult {
|
||||||
|
value: string;
|
||||||
|
messages: Array<{
|
||||||
|
type: string;
|
||||||
|
message: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertToHtml(
|
||||||
|
options: ConvertToHtmlOptions
|
||||||
|
): Promise<ConvertToHtmlResult>;
|
||||||
|
|
||||||
|
export default {
|
||||||
|
convertToHtml,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -39,3 +39,11 @@ export const getBackgroundImage = (path: string) => {
|
|||||||
const imageUrl = getStaticImage(path);
|
const imageUrl = getStaticImage(path);
|
||||||
return imageUrl ? `url(${imageUrl})` : "none";
|
return imageUrl ? `url(${imageUrl})` : "none";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 获取 public 文件夹中的资源路径(用于 Word 文档等)
|
||||||
|
export const getPublicPath = (path: string) => {
|
||||||
|
// 移除开头的斜杠(如果有)
|
||||||
|
const cleanPath = path.startsWith("/") ? path.slice(1) : path;
|
||||||
|
// 使用 Vite 的 BASE_URL 环境变量来处理基础路径
|
||||||
|
return `${import.meta.env.BASE_URL}${cleanPath}`;
|
||||||
|
};
|
||||||
@@ -3,7 +3,23 @@ import type { Config } from "tailwindcss";
|
|||||||
export default {
|
export default {
|
||||||
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
|
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
|
||||||
theme: {
|
theme: {
|
||||||
extend: {},
|
extend: {
|
||||||
|
keyframes: {
|
||||||
|
"scale-in": {
|
||||||
|
"0%": {
|
||||||
|
opacity: "0",
|
||||||
|
transform: "scale(0.9) translateY(10px)",
|
||||||
|
},
|
||||||
|
"100%": {
|
||||||
|
opacity: "1",
|
||||||
|
transform: "scale(1) translateY(0)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
"scale-in": "scale-in 0.2s ease-out",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
plugins: [],
|
plugins: [],
|
||||||
} satisfies Config;
|
} satisfies Config;
|
||||||