diff --git a/apps/server/src/models/term/term.service.ts b/apps/server/src/models/term/term.service.ts index 676d45e..7cbcc5f 100755 --- a/apps/server/src/models/term/term.service.ts +++ b/apps/server/src/models/term/term.service.ts @@ -352,7 +352,9 @@ export class TermService extends BaseTreeService { staff: UserProfile, data: z.infer, ) { - const { domainId = null, permissions } = staff; + // const { domainId = null, permissions } = staff; + const permissions = staff?.permissions || []; + const domainId = staff?.domainId || null; const hasAnyPerms = permissions.includes(RolePerms.READ_ANY_TERM) || permissions.includes(RolePerms.MANAGE_ANY_TERM); diff --git a/apps/web/src/app/admin/base-setting/page.tsx b/apps/web/src/app/admin/base-setting/page.tsx index c0aed70..d4a4efa 100755 --- a/apps/web/src/app/admin/base-setting/page.tsx +++ b/apps/web/src/app/admin/base-setting/page.tsx @@ -3,7 +3,7 @@ import { useContext, useEffect, useState } from "react"; import { Button, Form, Input, message, theme } from "antd"; import { useAppConfig } from "@nice/client"; 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 { useForm } from "antd/es/form/Form"; @@ -56,7 +56,7 @@ export default function BaseSettingPage() { meta: { ...baseSetting, appConfig: { - ...baseSetting.appConfig, + ...(baseSetting?.appConfig || {}), ...appConfig, }, }, @@ -129,9 +129,9 @@ export default function BaseSettingPage() {
- +
{/*
+ ghost> 清除行模型缓存
diff --git a/apps/web/src/app/main/courses/components/CourseCard.tsx b/apps/web/src/app/main/courses/components/CourseCard.tsx index 7b6cae1..fc3db64 100755 --- a/apps/web/src/app/main/courses/components/CourseCard.tsx +++ b/apps/web/src/app/main/courses/components/CourseCard.tsx @@ -34,6 +34,7 @@ export default function CourseCard({ course }: CourseCardProps) { backgroundImage: `url(${course?.meta?.thumbnail})`, }} /> +
@@ -78,6 +79,7 @@ export default function CourseCard({ course }: CourseCardProps) { {course.terms?.[1].name} */} + @@ -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" /> <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]"> - {course?.depts?.map((depts) => depts.name)} - {/* {course?.depts?.[0]?.name} */} + {course?.depts?.length > 1 + ? `${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> </div> <span className="text-xs font-medium text-gray-500"> - 观看次数{course?.meta?.views}次 + {course?.meta?.views + ? `观看次数 ${course?.meta?.views}` + : null} </span> </div> diff --git a/apps/web/src/app/main/home/components/CoursesSection.tsx b/apps/web/src/app/main/home/components/CoursesSection.tsx index aa2bf07..9f45863 100755 --- a/apps/web/src/app/main/home/components/CoursesSection.tsx +++ b/apps/web/src/app/main/home/components/CoursesSection.tsx @@ -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"> {title} + diff --git a/apps/web/src/app/main/layout/UserMenu/UserMenu.tsx b/apps/web/src/app/main/layout/UserMenu/UserMenu.tsx index 508fdaa..ea23902 100755 --- a/apps/web/src/app/main/layout/UserMenu/UserMenu.tsx +++ b/apps/web/src/app/main/layout/UserMenu/UserMenu.tsx @@ -221,21 +221,19 @@ export function UserMenu() { focus:outline-none focus:ring-2 focus:ring-[#00538E]/20 group relative overflow-hidden - active:scale-[0.99] - ${ - item.label === "注销" + active:scale-[0.99] + ${item.label === "注销" ? "text-[#B22234] hover:bg-red-50/80 hover:text-red-700" : "text-[#00538E] hover:bg-[#E6EEF5] hover:text-[#003F6A]" - }`}> + }`}> + group-hover:translate-x-0.5 ${item.label === "注销" + ? "group-hover:text-red-600" + : "group-hover:text-[#003F6A]" + }`}> {item.icon} {item.label} diff --git a/apps/web/src/components/common/uploader/AvatarUploader.tsx b/apps/web/src/components/common/uploader/AvatarUploader.tsx index abbb355..33246c8 100755 --- a/apps/web/src/components/common/uploader/AvatarUploader.tsx +++ b/apps/web/src/components/common/uploader/AvatarUploader.tsx @@ -12,6 +12,8 @@ export interface AvatarUploaderProps { onChange?: (value: string) => void; compressed?: boolean; style?: React.CSSProperties; // 添加style属性 + successText?: string; + showCover?: boolean; } interface UploadingFile { @@ -31,6 +33,8 @@ const AvatarUploader: React.FC = ({ className, placeholder = "点击上传", style, // 解构style属性 + successText = "上传成功", + showCover = true, }) => { const { handleFileUpload, uploadProgress } = useTusUpload(); const [file, setFile] = useState(null); @@ -92,12 +96,12 @@ const AvatarUploader: React.FC = ({ // 使用 resolved 的最新值调用 onChange // 强制刷新 Avatar 组件 setAvatarKey((prev) => prev + 1); // 修改 key 强制重新挂载 - onChange?.(uploadedUrl); console.log(uploadedUrl); - toast.success("头像上传成功"); + onChange?.(uploadedUrl); + toast.success(successText); } catch (error) { console.error("上传错误:", error); - toast.error("头像上传失败"); + toast.error("上传失败"); setFile((prev) => ({ ...prev!, status: "error" })); } finally { setUploading(false); @@ -124,7 +128,7 @@ const AvatarUploader: React.FC = ({ accept="image/*" style={{ display: "none" }} /> - {previewUrl ? ( + {(previewUrl && showCover) ? ( void; +} + +export function MultiAvatarUploader({ + value, + onChange, +}: MultiAvatarUploaderProps) { + const [imageList, setImageList] = useState(value || []); + const [previewImage, setPreviewImage] = useState(""); + useEffect(() => { + if (!isEqual(value, imageList)) { + setImageList(value || []); + } + }, [value]); + useEffect(() => { + onChange?.(imageList); + }, [imageList]); + return ( + <> +
+ {(imageList || [])?.map((image, index) => { + return ( +
+ + setPreviewImage( + visible ? image || "" : "" + ), + }}> +
+ ); + })} +
+
+ { + console.log(value); + setImageList([...imageList, value]); + }}> +
+ + ); +} +export default MultiAvatarUploader; diff --git a/apps/web/src/components/common/uploader/MultiImageUploader.tsx b/apps/web/src/components/common/uploader/MultiImageUploader.tsx deleted file mode 100644 index 5f6d026..0000000 --- a/apps/web/src/components/common/uploader/MultiImageUploader.tsx +++ /dev/null @@ -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 = ({ - value, - onChange, - compressed = false, - className, - placeholder = "点击上传", - style, // 解构style属性 -}) => { - const [fileList, setFileList] = useState(null); // 存储已上传的文件列表 - const [previewVisible, setPreviewVisible] = useState(false); // 控制预览模态框的显示 - const [previewImage, setPreviewImage] = useState(''); // 当前预览的图片URL - const { handleFileUpload, uploadProgress } = useTusUpload(); - const [compressedUrl, setCompressedUrl] = useState(value || ""); - const [url, setUrl] = useState(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((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 ( -
- -

-

-

点击或拖拽文件到此区域上传

-

支持单个或批量上传

-
- - 预览 - -
- ); -}; - -export default MultiImageUpload; diff --git a/apps/web/src/components/common/uploader/TestUploader.tsx b/apps/web/src/components/common/uploader/TestUploader.tsx deleted file mode 100644 index d4c196e..0000000 --- a/apps/web/src/components/common/uploader/TestUploader.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import AvatarUploader from "./AvatarUploader" - - - -interface TestUploaderProps { - value?: string[], - onChange?: (value: string[]) => void -} - -export function TestUploader({ - value, - onChange, -}: TestUploaderProps) { - - return <> - - -} \ No newline at end of file diff --git a/apps/web/src/components/layout/element/usermenu/usermenu.tsx b/apps/web/src/components/layout/element/usermenu/usermenu.tsx index d2611dd..31460cd 100755 --- a/apps/web/src/components/layout/element/usermenu/usermenu.tsx +++ b/apps/web/src/components/layout/element/usermenu/usermenu.tsx @@ -229,20 +229,18 @@ export function UserMenu() { focus:ring-2 focus:ring-[#00538E]/20 group relative overflow-hidden active:scale-[0.99] - ${ - item.label === "注销" + ${item.label === "注销" ? "text-[#B22234] hover:bg-red-50/80 hover:text-red-700" : "text-[#00538E] hover:bg-[#E6EEF5] hover:text-[#003F6A]" - }`}> + }`}> + group-hover:translate-x-0.5 ${item.label === "注销" + ? "group-hover:text-red-600" + : "group-hover:text-[#003F6A]" + }`}> {item.icon} {item.label} diff --git a/apps/web/src/components/models/course/editor/context/CourseEditorContext.tsx b/apps/web/src/components/models/course/editor/context/CourseEditorContext.tsx index 6a12e2c..6b61854 100755 --- a/apps/web/src/components/models/course/editor/context/CourseEditorContext.tsx +++ b/apps/web/src/components/models/course/editor/context/CourseEditorContext.tsx @@ -83,6 +83,7 @@ export function CourseFormProvider({ const onSubmit = async (values: any) => { console.log(values); const sections = values?.sections || []; + const deptIds = values?.deptIds || []; const termIds = taxonomies .map((tax) => values[tax.id]) // 获取每个 taxonomy 对应的选中值 .filter((id) => id); // 过滤掉空值 @@ -95,12 +96,16 @@ export function CourseFormProvider({ terms: { connect: termIds.map((id) => ({ id })), // 转换成 connect 格式 }, + depts: { + connect: deptIds.map((id) => ({ id })), + }, }; // 删除原始的 taxonomy 字段 taxonomies.forEach((tax) => { delete formattedValues[tax.id]; }); delete formattedValues.sections; + delete formattedValues.deptIds; if (course) { formattedValues.meta = { ...(course?.meta as CourseMeta), diff --git a/apps/web/src/components/models/course/editor/form/CourseBasicForm.tsx b/apps/web/src/components/models/course/editor/form/CourseBasicForm.tsx index b05579b..cf683f1 100755 --- a/apps/web/src/components/models/course/editor/form/CourseBasicForm.tsx +++ b/apps/web/src/components/models/course/editor/form/CourseBasicForm.tsx @@ -4,6 +4,7 @@ import { convertToOptions } from "@nice/client"; import TermSelect from "../../../term/term-select"; import { useCourseEditor } from "../context/CourseEditorContext"; import AvatarUploader from "@web/src/components/common/uploader/AvatarUploader"; +import DepartmentSelect from "../../../department/department-select"; const { TextArea } = Input; @@ -48,6 +49,9 @@ export function CourseBasicForm() { autoSize={{ minRows: 3, maxRows: 6 }} /> + + + {taxonomies && taxonomies.map((tax, index) => ( ; + } return (
{courses.length > 0 ? ( diff --git a/apps/web/src/components/models/term/term-select.tsx b/apps/web/src/components/models/term/term-select.tsx index 8f76f07..95990dd 100755 --- a/apps/web/src/components/models/term/term-select.tsx +++ b/apps/web/src/components/models/term/term-select.tsx @@ -15,11 +15,9 @@ interface TermSelectProps { defaultValue?: string | string[]; value?: string | string[]; onChange?: (value: string | string[]) => void; - placeholder?: string; multiple?: boolean; taxonomyId?: string; disabled?: boolean; - className?: string; domainId?: string; dropdownStyle?: React.CSSProperties; style?: React.CSSProperties; @@ -34,7 +32,6 @@ export default function TermSelect({ defaultValue, value, onChange, - className, placeholder = "选择分类", multiple = false, taxonomyId, @@ -45,12 +42,13 @@ export default function TermSelect({ style, disabled = false, dropdownRender, -}: TermSelectProps) { + ...treeSelectProps +}: TermSelectProps & TreeSelectProps) { const utils = api.useUtils(); const [listTreeData, setListTreeData] = useState< Omit[] >([]); - + const fetchParentTerms = useCallback( async (termIds: string | string[], taxonomyId?: string) => { const idsArray = Array.isArray(termIds) @@ -179,9 +177,6 @@ export default function TermSelect({ handleChange(multiple ? [] : undefined)} onTreeExpand={handleExpand} onDropdownVisibleChange={handleDropdownVisibleChange} + {...treeSelectProps} /> ); } diff --git a/apps/web/src/components/models/term/term-tree.tsx b/apps/web/src/components/models/term/term-tree.tsx deleted file mode 100644 index ae88a68..0000000 --- a/apps/web/src/components/models/term/term-tree.tsx +++ /dev/null @@ -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 = ({ - defaultValue, - value, - onChange, - className, - multiple = false, - taxonomyId, - domainId, - disabled = false, - style, -}) => { - const utils = api.useUtils(); - const [treeData, setTreeData] = useState([]); - - 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 ( - - ); -}; - -export default TermTree;