Merge branch 'main' of http://113.45.157.195:3003/insiinc/re-mooc
This commit is contained in:
commit
3027dae3a6
|
@ -352,7 +352,9 @@ export class TermService extends BaseTreeService<Prisma.TermDelegate> {
|
||||||
staff: UserProfile,
|
staff: UserProfile,
|
||||||
data: z.infer<typeof TermMethodSchema.getSimpleTree>,
|
data: z.infer<typeof TermMethodSchema.getSimpleTree>,
|
||||||
) {
|
) {
|
||||||
const { domainId = null, permissions } = staff;
|
// const { domainId = null, permissions } = staff;
|
||||||
|
const permissions = staff?.permissions || [];
|
||||||
|
const domainId = staff?.domainId || null;
|
||||||
const hasAnyPerms =
|
const hasAnyPerms =
|
||||||
permissions.includes(RolePerms.READ_ANY_TERM) ||
|
permissions.includes(RolePerms.READ_ANY_TERM) ||
|
||||||
permissions.includes(RolePerms.MANAGE_ANY_TERM);
|
permissions.includes(RolePerms.MANAGE_ANY_TERM);
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { useContext, useEffect, useState } from "react";
|
||||||
import { Button, Form, Input, message, theme } from "antd";
|
import { Button, Form, Input, message, theme } from "antd";
|
||||||
import { useAppConfig } from "@nice/client";
|
import { useAppConfig } from "@nice/client";
|
||||||
import { useAuth } from "@web/src/providers/auth-provider";
|
import { useAuth } from "@web/src/providers/auth-provider";
|
||||||
import MultiImageUploader from "@web/src/components/common/uploader/MultiImageUploader";
|
import MultiAvatarUploader from "@web/src/components/common/uploader/MultiAvatarUploader";
|
||||||
|
|
||||||
import FixedHeader from "@web/src/components/layout/fix-header";
|
import FixedHeader from "@web/src/components/layout/fix-header";
|
||||||
import { useForm } from "antd/es/form/Form";
|
import { useForm } from "antd/es/form/Form";
|
||||||
|
@ -56,7 +56,7 @@ export default function BaseSettingPage() {
|
||||||
meta: {
|
meta: {
|
||||||
...baseSetting,
|
...baseSetting,
|
||||||
appConfig: {
|
appConfig: {
|
||||||
...baseSetting.appConfig,
|
...(baseSetting?.appConfig || {}),
|
||||||
...appConfig,
|
...appConfig,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -129,9 +129,9 @@ export default function BaseSettingPage() {
|
||||||
</div>
|
</div>
|
||||||
<div className="p-2 grid grid-cols-8 gap-2 border-b">
|
<div className="p-2 grid grid-cols-8 gap-2 border-b">
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label="运维单位"
|
label="首页轮播图"
|
||||||
name={["appConfig", "slides"]}>
|
name={["appConfig", "slides"]}>
|
||||||
<MultiImageUploader></MultiImageUploader>
|
<MultiAvatarUploader></MultiAvatarUploader>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</div>
|
</div>
|
||||||
{/* <div
|
{/* <div
|
||||||
|
@ -174,7 +174,7 @@ export default function BaseSettingPage() {
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
type="primary"
|
type="primary"
|
||||||
ghost>
|
ghost>
|
||||||
清除行模型缓存
|
清除行模型缓存
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -34,6 +34,7 @@ export default function CourseCard({ course }: CourseCardProps) {
|
||||||
backgroundImage: `url(${course?.meta?.thumbnail})`,
|
backgroundImage: `url(${course?.meta?.thumbnail})`,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent opacity-80 group-hover:opacity-60 transition-opacity duration-300" />
|
<div className="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent opacity-80 group-hover:opacity-60 transition-opacity duration-300" />
|
||||||
<PlayCircleOutlined className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-6xl text-white/90 opacity-0 group-hover:opacity-100 transition-all duration-300" />
|
<PlayCircleOutlined className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-6xl text-white/90 opacity-0 group-hover:opacity-100 transition-all duration-300" />
|
||||||
</div>
|
</div>
|
||||||
|
@ -78,6 +79,7 @@ export default function CourseCard({ course }: CourseCardProps) {
|
||||||
{course.terms?.[1].name}
|
{course.terms?.[1].name}
|
||||||
</Tag> */}
|
</Tag> */}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Title
|
<Title
|
||||||
level={4}
|
level={4}
|
||||||
className="mb-4 line-clamp-2 font-bold leading-snug text-gray-800 hover:text-blue-600 transition-colors duration-300 group-hover:scale-[1.02] transform origin-left">
|
className="mb-4 line-clamp-2 font-bold leading-snug text-gray-800 hover:text-blue-600 transition-colors duration-300 group-hover:scale-[1.02] transform origin-left">
|
||||||
|
@ -88,12 +90,17 @@ export default function CourseCard({ course }: CourseCardProps) {
|
||||||
<TeamOutlined className="text-blue-500 text-lg transform group-hover:scale-110 transition-transform duration-300" />
|
<TeamOutlined className="text-blue-500 text-lg transform group-hover:scale-110 transition-transform duration-300" />
|
||||||
<div className="ml-2 flex items-center flex-grow">
|
<div className="ml-2 flex items-center flex-grow">
|
||||||
<Text className="font-medium text-blue-500 hover:text-blue-600 transition-colors duration-300 truncate max-w-[120px]">
|
<Text className="font-medium text-blue-500 hover:text-blue-600 transition-colors duration-300 truncate max-w-[120px]">
|
||||||
{course?.depts?.map((depts) => depts.name)}
|
{course?.depts?.length > 1
|
||||||
{/* {course?.depts?.[0]?.name} */}
|
? `${course.depts[0].name}等`
|
||||||
|
: course?.depts?.[0]?.name}
|
||||||
|
{/* {course?.depts?.map((dept) => {return dept.name.length > 1 ?`${dept.name.slice}等`: dept.name})} */}
|
||||||
|
{/* {course?.depts?.map((dept)=>{return dept.name})} */}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs font-medium text-gray-500">
|
<span className="text-xs font-medium text-gray-500">
|
||||||
观看次数{course?.meta?.views}次
|
{course?.meta?.views
|
||||||
|
? `观看次数 ${course?.meta?.views}`
|
||||||
|
: null}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -52,6 +52,7 @@ const CoursesSection: React.FC<CoursesSectionProps> = ({
|
||||||
className="font-bold text-5xl mb-6 bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent">
|
className="font-bold text-5xl mb-6 bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent">
|
||||||
{title}
|
{title}
|
||||||
</Title>
|
</Title>
|
||||||
|
|
||||||
<Text
|
<Text
|
||||||
type="secondary"
|
type="secondary"
|
||||||
className="text-xl font-light text-gray-600">
|
className="text-xl font-light text-gray-600">
|
||||||
|
|
|
@ -221,21 +221,19 @@ export function UserMenu() {
|
||||||
focus:outline-none
|
focus:outline-none
|
||||||
focus:ring-2 focus:ring-[#00538E]/20
|
focus:ring-2 focus:ring-[#00538E]/20
|
||||||
group relative overflow-hidden
|
group relative overflow-hidden
|
||||||
active:scale-[0.99]
|
active:scale-[0.99]
|
||||||
${
|
${item.label === "注销"
|
||||||
item.label === "注销"
|
|
||||||
? "text-[#B22234] hover:bg-red-50/80 hover:text-red-700"
|
? "text-[#B22234] hover:bg-red-50/80 hover:text-red-700"
|
||||||
: "text-[#00538E] hover:bg-[#E6EEF5] hover:text-[#003F6A]"
|
: "text-[#00538E] hover:bg-[#E6EEF5] hover:text-[#003F6A]"
|
||||||
}`}>
|
}`}>
|
||||||
<span
|
<span
|
||||||
className={`w-5 h-5 flex items-center justify-center
|
className={`w-5 h-5 flex items-center justify-center
|
||||||
transition-all duration-200 ease-in-out
|
transition-all duration-200 ease-in-out
|
||||||
group-hover:scale-110 group-hover:rotate-6
|
group-hover:scale-110 group-hover:rotate-6
|
||||||
group-hover:translate-x-0.5 ${
|
group-hover:translate-x-0.5 ${item.label === "注销"
|
||||||
item.label === "注销"
|
? "group-hover:text-red-600"
|
||||||
? "group-hover:text-red-600"
|
: "group-hover:text-[#003F6A]"
|
||||||
: "group-hover:text-[#003F6A]"
|
}`}>
|
||||||
}`}>
|
|
||||||
{item.icon}
|
{item.icon}
|
||||||
</span>
|
</span>
|
||||||
<span>{item.label}</span>
|
<span>{item.label}</span>
|
||||||
|
|
|
@ -12,6 +12,8 @@ export interface AvatarUploaderProps {
|
||||||
onChange?: (value: string) => void;
|
onChange?: (value: string) => void;
|
||||||
compressed?: boolean;
|
compressed?: boolean;
|
||||||
style?: React.CSSProperties; // 添加style属性
|
style?: React.CSSProperties; // 添加style属性
|
||||||
|
successText?: string;
|
||||||
|
showCover?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UploadingFile {
|
interface UploadingFile {
|
||||||
|
@ -31,6 +33,8 @@ const AvatarUploader: React.FC<AvatarUploaderProps> = ({
|
||||||
className,
|
className,
|
||||||
placeholder = "点击上传",
|
placeholder = "点击上传",
|
||||||
style, // 解构style属性
|
style, // 解构style属性
|
||||||
|
successText = "上传成功",
|
||||||
|
showCover = true,
|
||||||
}) => {
|
}) => {
|
||||||
const { handleFileUpload, uploadProgress } = useTusUpload();
|
const { handleFileUpload, uploadProgress } = useTusUpload();
|
||||||
const [file, setFile] = useState<UploadingFile | null>(null);
|
const [file, setFile] = useState<UploadingFile | null>(null);
|
||||||
|
@ -92,12 +96,12 @@ const AvatarUploader: React.FC<AvatarUploaderProps> = ({
|
||||||
// 使用 resolved 的最新值调用 onChange
|
// 使用 resolved 的最新值调用 onChange
|
||||||
// 强制刷新 Avatar 组件
|
// 强制刷新 Avatar 组件
|
||||||
setAvatarKey((prev) => prev + 1); // 修改 key 强制重新挂载
|
setAvatarKey((prev) => prev + 1); // 修改 key 强制重新挂载
|
||||||
onChange?.(uploadedUrl);
|
|
||||||
console.log(uploadedUrl);
|
console.log(uploadedUrl);
|
||||||
toast.success("头像上传成功");
|
onChange?.(uploadedUrl);
|
||||||
|
toast.success(successText);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("上传错误:", error);
|
console.error("上传错误:", error);
|
||||||
toast.error("头像上传失败");
|
toast.error("上传失败");
|
||||||
setFile((prev) => ({ ...prev!, status: "error" }));
|
setFile((prev) => ({ ...prev!, status: "error" }));
|
||||||
} finally {
|
} finally {
|
||||||
setUploading(false);
|
setUploading(false);
|
||||||
|
@ -124,7 +128,7 @@ const AvatarUploader: React.FC<AvatarUploaderProps> = ({
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
style={{ display: "none" }}
|
style={{ display: "none" }}
|
||||||
/>
|
/>
|
||||||
{previewUrl ? (
|
{(previewUrl && showCover) ? (
|
||||||
<Avatar
|
<Avatar
|
||||||
key={avatarKey}
|
key={avatarKey}
|
||||||
ref={avatarRef}
|
ref={avatarRef}
|
||||||
|
|
|
@ -0,0 +1,83 @@
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
// import UncoverAvatarUploader from "../uploader/UncoverAvatarUploader ";
|
||||||
|
import { Upload, Progress, Button, Image, Form } from "antd";
|
||||||
|
import { DeleteOutlined } from "@ant-design/icons";
|
||||||
|
import AvatarUploader from "./AvatarUploader";
|
||||||
|
import { isEqual } from "lodash";
|
||||||
|
|
||||||
|
interface MultiAvatarUploaderProps {
|
||||||
|
value?: string[];
|
||||||
|
onChange?: (value: string[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MultiAvatarUploader({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
}: MultiAvatarUploaderProps) {
|
||||||
|
const [imageList, setImageList] = useState<string[]>(value || []);
|
||||||
|
const [previewImage, setPreviewImage] = useState<string>("");
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isEqual(value, imageList)) {
|
||||||
|
setImageList(value || []);
|
||||||
|
}
|
||||||
|
}, [value]);
|
||||||
|
useEffect(() => {
|
||||||
|
onChange?.(imageList);
|
||||||
|
}, [imageList]);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex gap-2 mb-2" style={{ width: "1200px" }}>
|
||||||
|
{(imageList || [])?.map((image, index) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="mr-2px relative"
|
||||||
|
key={index}
|
||||||
|
style={{ width: "200px", height: "100px" }}>
|
||||||
|
<Image
|
||||||
|
alt=""
|
||||||
|
style={{ width: "200px", height: "100px" }}
|
||||||
|
src={image}
|
||||||
|
preview={{
|
||||||
|
visible: previewImage === image,
|
||||||
|
onVisibleChange: (visible) =>
|
||||||
|
setPreviewImage(
|
||||||
|
visible ? image || "" : ""
|
||||||
|
),
|
||||||
|
}}></Image>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
danger
|
||||||
|
icon={<DeleteOutlined className="text-red" />}
|
||||||
|
onClick={() =>
|
||||||
|
image &&
|
||||||
|
setImageList(
|
||||||
|
imageList.filter((_, i) => i !== index)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
style={{
|
||||||
|
position: "absolute", // 绝对定位
|
||||||
|
top: "0", // 顶部对齐
|
||||||
|
right: "0", // 右侧对齐
|
||||||
|
zIndex: 1, // 确保按钮在图片上方
|
||||||
|
padding: "4px", // 调整按钮内边距
|
||||||
|
backgroundColor: "rgba(255, 255, 255, 0.2)", // 半透明背景
|
||||||
|
borderRadius: "50%", // 圆形按钮
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div className="flex">
|
||||||
|
<AvatarUploader
|
||||||
|
showCover={false}
|
||||||
|
successText={"轮播图上传成功"}
|
||||||
|
onChange={(value) => {
|
||||||
|
console.log(value);
|
||||||
|
setImageList([...imageList, value]);
|
||||||
|
}}></AvatarUploader>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export default MultiAvatarUploader;
|
|
@ -1,173 +0,0 @@
|
||||||
import React, { useState } from 'react';
|
|
||||||
import { Upload, Modal, message } from 'antd';
|
|
||||||
import { PlusOutlined } from '@ant-design/icons';
|
|
||||||
import { useTusUpload } from "@web/src/hooks/useTusUpload";
|
|
||||||
import toast from "react-hot-toast";
|
|
||||||
|
|
||||||
|
|
||||||
export interface MultiImageUploadProps {
|
|
||||||
value?: string;
|
|
||||||
placeholder?: string;
|
|
||||||
className?: string;
|
|
||||||
onChange?: (value: string) => void;
|
|
||||||
compressed?: boolean;
|
|
||||||
style?: React.CSSProperties; // 添加style属性
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UploadFile {
|
|
||||||
name: string;
|
|
||||||
progress: number;
|
|
||||||
status: "uploading" | "done" | "error";
|
|
||||||
url?: string;
|
|
||||||
fileKey?: string;
|
|
||||||
uid: string;
|
|
||||||
}
|
|
||||||
const MultiImageUpload: React.FC<MultiImageUploadProps> = ({
|
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
compressed = false,
|
|
||||||
className,
|
|
||||||
placeholder = "点击上传",
|
|
||||||
style, // 解构style属性
|
|
||||||
}) => {
|
|
||||||
const [fileList, setFileList] = useState<UploadFile[] | null>(null); // 存储已上传的文件列表
|
|
||||||
const [previewVisible, setPreviewVisible] = useState(false); // 控制预览模态框的显示
|
|
||||||
const [previewImage, setPreviewImage] = useState(''); // 当前预览的图片URL
|
|
||||||
const { handleFileUpload, uploadProgress } = useTusUpload();
|
|
||||||
const [compressedUrl, setCompressedUrl] = useState<string>(value || "");
|
|
||||||
const [url, setUrl] = useState<string>(value || "");
|
|
||||||
const [uploading, setUploading] = useState(false);
|
|
||||||
|
|
||||||
// 处理文件上传前的校验
|
|
||||||
const beforeUpload = (file) => {
|
|
||||||
const isImage = file.type.startsWith('image/');
|
|
||||||
if (!isImage) {
|
|
||||||
message.error('只能上传图片文件!');
|
|
||||||
}
|
|
||||||
const isLt10M = file.size / 1024 / 1024 < 10;
|
|
||||||
if (!isLt10M) {
|
|
||||||
message.error('图片大小不能超过10MB!');
|
|
||||||
}
|
|
||||||
return isImage && isLt10M;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理文件列表变化
|
|
||||||
const handleChange = async ({ fileList }) => {
|
|
||||||
//setFileList(fileList);
|
|
||||||
console.log(fileList);
|
|
||||||
const imageUrls = fileList.map(file => {
|
|
||||||
return URL.createObjectURL(file.originFileObj)
|
|
||||||
});
|
|
||||||
console.log("imageUrls", imageUrls);
|
|
||||||
|
|
||||||
const newFileList = fileList.map(file => {
|
|
||||||
return {
|
|
||||||
name: file.name,
|
|
||||||
progress: 0,
|
|
||||||
status: "uploading",
|
|
||||||
//uid: file.uid,
|
|
||||||
fileKey: `${file.name}-${Date.now()}`,
|
|
||||||
//url: file.url,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
console.log("newFileList", newFileList);
|
|
||||||
setUploading(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const resFileList = newFileList.map(async (file, index) => {
|
|
||||||
const uploadedUrl = await new Promise<string>((resolve, reject) => {
|
|
||||||
handleFileUpload(
|
|
||||||
fileList[index].originFileObj,
|
|
||||||
(result) => {
|
|
||||||
() => {
|
|
||||||
return {
|
|
||||||
...newFileList[index],
|
|
||||||
progress: 100,
|
|
||||||
status: "done",
|
|
||||||
fileId: result.fileId,
|
|
||||||
url: result.url,
|
|
||||||
compressedUrl: result.compressedUrl,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// setFile((prev) => ({
|
|
||||||
// ...prev!,
|
|
||||||
// progress: 100,
|
|
||||||
// status: "done",
|
|
||||||
// fileId: result.fileId,
|
|
||||||
// url: result.url,
|
|
||||||
// compressedUrl: result.compressedUrl,
|
|
||||||
// }));
|
|
||||||
setUrl(result.url);
|
|
||||||
setCompressedUrl(result.compressedUrl);
|
|
||||||
// 直接使用 result 中的最新值
|
|
||||||
resolve(compressed ? result.compressedUrl : result.url);
|
|
||||||
},
|
|
||||||
(error) => {
|
|
||||||
reject(error);
|
|
||||||
},
|
|
||||||
newFileList[index]?.fileKey
|
|
||||||
);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
// await new Promise((resolve) => setTimeout(resolve,4999)); // 方法1:使用 await 暂停执行
|
|
||||||
// 使用 resolved 的最新值调用 onChange
|
|
||||||
// 强制刷新 Avatar 组件
|
|
||||||
// setAvatarKey((prev) => prev + 1); // 修改 key 强制重新挂载
|
|
||||||
onChange?.(resFileList);
|
|
||||||
console.log(resFileList);
|
|
||||||
toast.success("图片上传成功");
|
|
||||||
} catch (error) {
|
|
||||||
console.error("上传错误:", error);
|
|
||||||
toast.error("图片上传失败");
|
|
||||||
// setFile((prev) => ({ ...prev!, status: "error" }));
|
|
||||||
} finally {
|
|
||||||
setUploading(false);
|
|
||||||
}
|
|
||||||
};uploadProgress
|
|
||||||
|
|
||||||
// 处理预览
|
|
||||||
const handlePreview = async (file) => {
|
|
||||||
if (!file.url && !file.preview) {
|
|
||||||
file.preview = await getBase64(file.originFileObj);
|
|
||||||
}
|
|
||||||
setPreviewImage(file.url || file.preview);
|
|
||||||
setPreviewVisible(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 关闭预览模态框
|
|
||||||
const handleCancel = () => setPreviewVisible(false);
|
|
||||||
|
|
||||||
// 将文件转换为Base64格式(用于预览)
|
|
||||||
const getBase64 = (file) => {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.readAsDataURL(file);
|
|
||||||
reader.onload = () => resolve(reader.result);
|
|
||||||
reader.onerror = (error) => reject(error);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Upload.Dragger
|
|
||||||
listType="picture-card" // 卡片样式
|
|
||||||
fileList={fileList} // 已上传的文件列表
|
|
||||||
beforeUpload={beforeUpload} // 上传前的校验
|
|
||||||
onChange={handleChange} // 文件列表变化时的回调
|
|
||||||
onPreview={handlePreview} // 预览回调
|
|
||||||
multiple // 支持多选
|
|
||||||
// style={{ width: '200px' }}
|
|
||||||
>
|
|
||||||
<p className="ant-upload-drag-icon">
|
|
||||||
</p>
|
|
||||||
<p className="ant-upload-text">点击或拖拽文件到此区域上传</p>
|
|
||||||
<p className="ant-upload-hint">支持单个或批量上传</p>
|
|
||||||
</Upload.Dragger>
|
|
||||||
<Modal visible={previewVisible} footer={null} onCancel={handleCancel}>
|
|
||||||
<img alt="预览" style={{ width: '100%' }} src={previewImage} />
|
|
||||||
</Modal>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MultiImageUpload;
|
|
|
@ -1,18 +0,0 @@
|
||||||
import AvatarUploader from "./AvatarUploader"
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
interface TestUploaderProps {
|
|
||||||
value?: string[],
|
|
||||||
onChange?: (value: string[]) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export function TestUploader({
|
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
}: TestUploaderProps) {
|
|
||||||
|
|
||||||
return <>
|
|
||||||
<AvatarUploader></AvatarUploader>
|
|
||||||
</>
|
|
||||||
}
|
|
|
@ -229,20 +229,18 @@ export function UserMenu() {
|
||||||
focus:ring-2 focus:ring-[#00538E]/20
|
focus:ring-2 focus:ring-[#00538E]/20
|
||||||
group relative overflow-hidden
|
group relative overflow-hidden
|
||||||
active:scale-[0.99]
|
active:scale-[0.99]
|
||||||
${
|
${item.label === "注销"
|
||||||
item.label === "注销"
|
|
||||||
? "text-[#B22234] hover:bg-red-50/80 hover:text-red-700"
|
? "text-[#B22234] hover:bg-red-50/80 hover:text-red-700"
|
||||||
: "text-[#00538E] hover:bg-[#E6EEF5] hover:text-[#003F6A]"
|
: "text-[#00538E] hover:bg-[#E6EEF5] hover:text-[#003F6A]"
|
||||||
}`}>
|
}`}>
|
||||||
<span
|
<span
|
||||||
className={`w-5 h-5 flex items-center justify-center
|
className={`w-5 h-5 flex items-center justify-center
|
||||||
transition-all duration-200 ease-in-out
|
transition-all duration-200 ease-in-out
|
||||||
group-hover:scale-110 group-hover:rotate-6
|
group-hover:scale-110 group-hover:rotate-6
|
||||||
group-hover:translate-x-0.5 ${
|
group-hover:translate-x-0.5 ${item.label === "注销"
|
||||||
item.label === "注销"
|
? "group-hover:text-red-600"
|
||||||
? "group-hover:text-red-600"
|
: "group-hover:text-[#003F6A]"
|
||||||
: "group-hover:text-[#003F6A]"
|
}`}>
|
||||||
}`}>
|
|
||||||
{item.icon}
|
{item.icon}
|
||||||
</span>
|
</span>
|
||||||
<span>{item.label}</span>
|
<span>{item.label}</span>
|
||||||
|
|
|
@ -83,6 +83,7 @@ export function CourseFormProvider({
|
||||||
const onSubmit = async (values: any) => {
|
const onSubmit = async (values: any) => {
|
||||||
console.log(values);
|
console.log(values);
|
||||||
const sections = values?.sections || [];
|
const sections = values?.sections || [];
|
||||||
|
const deptIds = values?.deptIds || [];
|
||||||
const termIds = taxonomies
|
const termIds = taxonomies
|
||||||
.map((tax) => values[tax.id]) // 获取每个 taxonomy 对应的选中值
|
.map((tax) => values[tax.id]) // 获取每个 taxonomy 对应的选中值
|
||||||
.filter((id) => id); // 过滤掉空值
|
.filter((id) => id); // 过滤掉空值
|
||||||
|
@ -95,12 +96,16 @@ export function CourseFormProvider({
|
||||||
terms: {
|
terms: {
|
||||||
connect: termIds.map((id) => ({ id })), // 转换成 connect 格式
|
connect: termIds.map((id) => ({ id })), // 转换成 connect 格式
|
||||||
},
|
},
|
||||||
|
depts: {
|
||||||
|
connect: deptIds.map((id) => ({ id })),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
// 删除原始的 taxonomy 字段
|
// 删除原始的 taxonomy 字段
|
||||||
taxonomies.forEach((tax) => {
|
taxonomies.forEach((tax) => {
|
||||||
delete formattedValues[tax.id];
|
delete formattedValues[tax.id];
|
||||||
});
|
});
|
||||||
delete formattedValues.sections;
|
delete formattedValues.sections;
|
||||||
|
delete formattedValues.deptIds;
|
||||||
if (course) {
|
if (course) {
|
||||||
formattedValues.meta = {
|
formattedValues.meta = {
|
||||||
...(course?.meta as CourseMeta),
|
...(course?.meta as CourseMeta),
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { convertToOptions } from "@nice/client";
|
||||||
import TermSelect from "../../../term/term-select";
|
import TermSelect from "../../../term/term-select";
|
||||||
import { useCourseEditor } from "../context/CourseEditorContext";
|
import { useCourseEditor } from "../context/CourseEditorContext";
|
||||||
import AvatarUploader from "@web/src/components/common/uploader/AvatarUploader";
|
import AvatarUploader from "@web/src/components/common/uploader/AvatarUploader";
|
||||||
|
import DepartmentSelect from "../../../department/department-select";
|
||||||
|
|
||||||
const { TextArea } = Input;
|
const { TextArea } = Input;
|
||||||
|
|
||||||
|
@ -48,6 +49,9 @@ export function CourseBasicForm() {
|
||||||
autoSize={{ minRows: 3, maxRows: 6 }}
|
autoSize={{ minRows: 3, maxRows: 6 }}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Form.Item name="deptIds" label="参与单位">
|
||||||
|
<DepartmentSelect multiple />
|
||||||
|
</Form.Item>
|
||||||
{taxonomies &&
|
{taxonomies &&
|
||||||
taxonomies.map((tax, index) => (
|
taxonomies.map((tax, index) => (
|
||||||
<Form.Item
|
<Form.Item
|
||||||
|
|
|
@ -54,6 +54,9 @@ export default function CourseList({
|
||||||
setCurrentPage(page);
|
setCurrentPage(page);
|
||||||
window.scrollTo({ top: 0, behavior: "smooth" });
|
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||||
}
|
}
|
||||||
|
if (isLoading) {
|
||||||
|
return <Skeleton paragraph={{ rows: 10 }}></Skeleton>;
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{courses.length > 0 ? (
|
{courses.length > 0 ? (
|
||||||
|
|
|
@ -15,11 +15,9 @@ interface TermSelectProps {
|
||||||
defaultValue?: string | string[];
|
defaultValue?: string | string[];
|
||||||
value?: string | string[];
|
value?: string | string[];
|
||||||
onChange?: (value: string | string[]) => void;
|
onChange?: (value: string | string[]) => void;
|
||||||
placeholder?: string;
|
|
||||||
multiple?: boolean;
|
multiple?: boolean;
|
||||||
taxonomyId?: string;
|
taxonomyId?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
className?: string;
|
|
||||||
domainId?: string;
|
domainId?: string;
|
||||||
dropdownStyle?: React.CSSProperties;
|
dropdownStyle?: React.CSSProperties;
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
|
@ -34,7 +32,6 @@ export default function TermSelect({
|
||||||
defaultValue,
|
defaultValue,
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
className,
|
|
||||||
placeholder = "选择分类",
|
placeholder = "选择分类",
|
||||||
multiple = false,
|
multiple = false,
|
||||||
taxonomyId,
|
taxonomyId,
|
||||||
|
@ -45,12 +42,13 @@ export default function TermSelect({
|
||||||
style,
|
style,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
dropdownRender,
|
dropdownRender,
|
||||||
}: TermSelectProps) {
|
...treeSelectProps
|
||||||
|
}: TermSelectProps & TreeSelectProps) {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const [listTreeData, setListTreeData] = useState<
|
const [listTreeData, setListTreeData] = useState<
|
||||||
Omit<DefaultOptionType, "label">[]
|
Omit<DefaultOptionType, "label">[]
|
||||||
>([]);
|
>([]);
|
||||||
|
|
||||||
const fetchParentTerms = useCallback(
|
const fetchParentTerms = useCallback(
|
||||||
async (termIds: string | string[], taxonomyId?: string) => {
|
async (termIds: string | string[], taxonomyId?: string) => {
|
||||||
const idsArray = Array.isArray(termIds)
|
const idsArray = Array.isArray(termIds)
|
||||||
|
@ -179,9 +177,6 @@ export default function TermSelect({
|
||||||
<TreeSelect
|
<TreeSelect
|
||||||
treeDataSimpleMode
|
treeDataSimpleMode
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
showSearch={showSearch}
|
|
||||||
allowClear
|
|
||||||
style={style}
|
|
||||||
// ref={selectRef}
|
// ref={selectRef}
|
||||||
dropdownStyle={{
|
dropdownStyle={{
|
||||||
width: "300px", // 固定宽度
|
width: "300px", // 固定宽度
|
||||||
|
@ -189,11 +184,8 @@ export default function TermSelect({
|
||||||
maxWidth: "600px", // 最大宽度
|
maxWidth: "600px", // 最大宽度
|
||||||
...dropdownStyle,
|
...dropdownStyle,
|
||||||
}}
|
}}
|
||||||
dropdownRender={dropdownRender}
|
|
||||||
defaultValue={defaultValue}
|
defaultValue={defaultValue}
|
||||||
value={value}
|
value={value}
|
||||||
open={open}
|
|
||||||
className={className}
|
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
loadData={onLoadData}
|
loadData={onLoadData}
|
||||||
|
@ -204,6 +196,7 @@ export default function TermSelect({
|
||||||
onClear={() => handleChange(multiple ? [] : undefined)}
|
onClear={() => handleChange(multiple ? [] : undefined)}
|
||||||
onTreeExpand={handleExpand}
|
onTreeExpand={handleExpand}
|
||||||
onDropdownVisibleChange={handleDropdownVisibleChange}
|
onDropdownVisibleChange={handleDropdownVisibleChange}
|
||||||
|
{...treeSelectProps}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,199 +0,0 @@
|
||||||
import React, { useEffect, useState, useCallback } from "react";
|
|
||||||
import { Tree } from "antd";
|
|
||||||
import type { DataNode, TreeProps } from "antd/es/tree";
|
|
||||||
import { getUniqueItems } from "@nice/common";
|
|
||||||
import { api } from "@nice/client";
|
|
||||||
|
|
||||||
interface TermData {
|
|
||||||
value?: string;
|
|
||||||
children?: TermData[];
|
|
||||||
key?: string;
|
|
||||||
hasChildren?: boolean;
|
|
||||||
isLeaf?: boolean;
|
|
||||||
pId?: string;
|
|
||||||
title?: React.ReactNode;
|
|
||||||
data?: any;
|
|
||||||
order?: string;
|
|
||||||
id?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TermTreeProps {
|
|
||||||
defaultValue?: string | string[];
|
|
||||||
value?: string | string[];
|
|
||||||
onChange?: (value: string | string[]) => void;
|
|
||||||
multiple?: boolean;
|
|
||||||
taxonomyId?: string;
|
|
||||||
disabled?: boolean;
|
|
||||||
className?: string;
|
|
||||||
domainId?: string;
|
|
||||||
style?: React.CSSProperties;
|
|
||||||
}
|
|
||||||
|
|
||||||
const TermTree: React.FC<TermTreeProps> = ({
|
|
||||||
defaultValue,
|
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
className,
|
|
||||||
multiple = false,
|
|
||||||
taxonomyId,
|
|
||||||
domainId,
|
|
||||||
disabled = false,
|
|
||||||
style,
|
|
||||||
}) => {
|
|
||||||
const utils = api.useUtils();
|
|
||||||
const [treeData, setTreeData] = useState<TermData[]>([]);
|
|
||||||
|
|
||||||
const processTermData = (terms: TermData[]): TermData[] => {
|
|
||||||
return terms.map((term) => ({
|
|
||||||
...term,
|
|
||||||
key: term.key || term.id || "",
|
|
||||||
title: term.title || term.value,
|
|
||||||
children: term.children
|
|
||||||
? processTermData(term.children)
|
|
||||||
: undefined,
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchParentTerms = useCallback(
|
|
||||||
async (termIds: string | string[], taxonomyId?: string) => {
|
|
||||||
const idsArray = Array.isArray(termIds)
|
|
||||||
? termIds
|
|
||||||
: [termIds].filter(Boolean);
|
|
||||||
try {
|
|
||||||
const result = await utils.term.getParentSimpleTree.fetch({
|
|
||||||
termIds: idsArray,
|
|
||||||
taxonomyId,
|
|
||||||
domainId,
|
|
||||||
});
|
|
||||||
return processTermData(result);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(
|
|
||||||
"Error fetching parent terms for termIds",
|
|
||||||
idsArray,
|
|
||||||
":",
|
|
||||||
error
|
|
||||||
);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[utils, domainId]
|
|
||||||
);
|
|
||||||
|
|
||||||
const fetchTerms = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
const rootTerms = await utils.term.getChildSimpleTree.fetch({
|
|
||||||
taxonomyId,
|
|
||||||
domainId,
|
|
||||||
});
|
|
||||||
let combinedTerms = processTermData(rootTerms);
|
|
||||||
if (defaultValue) {
|
|
||||||
const defaultTerms = await fetchParentTerms(
|
|
||||||
defaultValue,
|
|
||||||
taxonomyId
|
|
||||||
);
|
|
||||||
combinedTerms = getUniqueItems(
|
|
||||||
[...treeData, ...combinedTerms, ...defaultTerms],
|
|
||||||
"key"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (value) {
|
|
||||||
const valueTerms = await fetchParentTerms(value, taxonomyId);
|
|
||||||
combinedTerms = getUniqueItems(
|
|
||||||
[...treeData, ...combinedTerms, ...valueTerms],
|
|
||||||
"key"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
setTreeData(combinedTerms);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error fetching terms:", error);
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
defaultValue,
|
|
||||||
value,
|
|
||||||
taxonomyId,
|
|
||||||
utils,
|
|
||||||
fetchParentTerms,
|
|
||||||
domainId,
|
|
||||||
treeData,
|
|
||||||
]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchTerms();
|
|
||||||
}, [fetchTerms]);
|
|
||||||
|
|
||||||
const onLoadData = async ({ key }: any) => {
|
|
||||||
try {
|
|
||||||
const result = await utils.term.getChildSimpleTree.fetch({
|
|
||||||
termIds: [key],
|
|
||||||
taxonomyId,
|
|
||||||
domainId,
|
|
||||||
});
|
|
||||||
const processedResult = processTermData(result);
|
|
||||||
const newItems = getUniqueItems(
|
|
||||||
[...treeData, ...processedResult],
|
|
||||||
"key"
|
|
||||||
);
|
|
||||||
setTreeData(newItems);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(
|
|
||||||
"Error loading data for node with key",
|
|
||||||
key,
|
|
||||||
":",
|
|
||||||
error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCheck: TreeProps["onCheck"] = (checkedKeys, info) => {
|
|
||||||
if (onChange) {
|
|
||||||
if (multiple) {
|
|
||||||
onChange(checkedKeys as string[]);
|
|
||||||
} else {
|
|
||||||
onChange((checkedKeys as string[])[0] || "");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleExpand = async (expandedKeys: React.Key[]) => {
|
|
||||||
try {
|
|
||||||
const allKeyIds = expandedKeys
|
|
||||||
.map((key) => key.toString())
|
|
||||||
.filter(Boolean);
|
|
||||||
const expandedNodes = await utils.term.getChildSimpleTree.fetch({
|
|
||||||
termIds: allKeyIds,
|
|
||||||
taxonomyId,
|
|
||||||
domainId,
|
|
||||||
});
|
|
||||||
const processedNodes = processTermData(expandedNodes);
|
|
||||||
const newItems = getUniqueItems(
|
|
||||||
[...treeData, ...processedNodes],
|
|
||||||
"key"
|
|
||||||
);
|
|
||||||
setTreeData(newItems);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(
|
|
||||||
"Error expanding nodes with keys",
|
|
||||||
expandedKeys,
|
|
||||||
":",
|
|
||||||
error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tree
|
|
||||||
checkable={multiple}
|
|
||||||
disabled={disabled}
|
|
||||||
className={className}
|
|
||||||
style={style}
|
|
||||||
treeData={treeData as DataNode[]}
|
|
||||||
checkedKeys={Array.isArray(value) ? value : value ? [value] : []}
|
|
||||||
onCheck={handleCheck}
|
|
||||||
loadData={onLoadData}
|
|
||||||
onExpand={handleExpand}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TermTree;
|
|
Loading…
Reference in New Issue