Merge branch 'main' of http://113.45.157.195:3003/insiinc/re-mooc
This commit is contained in:
commit
4d995a24a9
|
@ -155,6 +155,7 @@ export class UserProfileService {
|
||||||
where: { id },
|
where: { id },
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
|
avatar:true,
|
||||||
deptId: true,
|
deptId: true,
|
||||||
department: true,
|
department: true,
|
||||||
domainId: true,
|
domainId: true,
|
||||||
|
|
|
@ -40,7 +40,11 @@ export class VisitService extends BaseService<Prisma.VisitDelegate> {
|
||||||
id: postId,
|
id: postId,
|
||||||
visitType: args.data.type, // 直接复用传入的类型
|
visitType: args.data.type, // 直接复用传入的类型
|
||||||
});
|
});
|
||||||
|
EventBus.emit('updateTotalCourseViewCount', {
|
||||||
|
visitType: args.data.type, // 直接复用传入的类型
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
async createMany(args: Prisma.VisitCreateManyArgs, staff?: UserProfile) {
|
async createMany(args: Prisma.VisitCreateManyArgs, staff?: UserProfile) {
|
||||||
|
@ -138,6 +142,9 @@ export class VisitService extends BaseService<Prisma.VisitDelegate> {
|
||||||
id: args?.where?.postId as string,
|
id: args?.where?.postId as string,
|
||||||
visitType: args.where.type as any, // 直接复用传入的类型
|
visitType: args.where.type as any, // 直接复用传入的类型
|
||||||
});
|
});
|
||||||
|
EventBus.emit('updateTotalCourseViewCount', {
|
||||||
|
visitType: args.where.type as any, // 直接复用传入的类型
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return superDetele;
|
return superDetele;
|
||||||
|
|
|
@ -7,11 +7,18 @@ import {
|
||||||
VisitType,
|
VisitType,
|
||||||
} from '@nice/common';
|
} from '@nice/common';
|
||||||
export async function updateTotalCourseViewCount(type: VisitType) {
|
export async function updateTotalCourseViewCount(type: VisitType) {
|
||||||
const courses = await db.post.findMany({
|
const posts = await db.post.findMany({
|
||||||
where: { type: PostType.COURSE },
|
where: {
|
||||||
select: { id: true },
|
type: { in: [PostType.COURSE, PostType.LECTURE] },
|
||||||
|
deletedAt: null,
|
||||||
|
},
|
||||||
|
select: { id: true, type: true },
|
||||||
});
|
});
|
||||||
const courseIds = courses.map((course) => course.id);
|
|
||||||
|
const courseIds = posts
|
||||||
|
.filter((post) => post.type === PostType.COURSE)
|
||||||
|
.map((course) => course.id);
|
||||||
|
const lectures = posts.filter((post) => post.type === PostType.LECTURE);
|
||||||
const totalViews = await db.visit.aggregate({
|
const totalViews = await db.visit.aggregate({
|
||||||
_sum: {
|
_sum: {
|
||||||
views: true,
|
views: true,
|
||||||
|
@ -30,6 +37,10 @@ export async function updateTotalCourseViewCount(type: VisitType) {
|
||||||
meta: true,
|
meta: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
const staffs = await db.staff.count({
|
||||||
|
where: { deletedAt: null },
|
||||||
|
});
|
||||||
|
|
||||||
const baseSeting = appConfig.meta as BaseSetting;
|
const baseSeting = appConfig.meta as BaseSetting;
|
||||||
await db.appConfig.update({
|
await db.appConfig.update({
|
||||||
where: {
|
where: {
|
||||||
|
@ -38,7 +49,15 @@ export async function updateTotalCourseViewCount(type: VisitType) {
|
||||||
data: {
|
data: {
|
||||||
meta: {
|
meta: {
|
||||||
...baseSeting,
|
...baseSeting,
|
||||||
reads: totalViews,
|
appConfig: {
|
||||||
|
...(baseSeting?.appConfig || {}),
|
||||||
|
statistics: {
|
||||||
|
reads: totalViews._sum.views || 0,
|
||||||
|
courses: courseIds?.length || 0,
|
||||||
|
staffs: staffs || 0,
|
||||||
|
lectures: lectures?.length || 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -24,7 +24,8 @@ export default function BaseSettingPage() {
|
||||||
const [isFormChanged, setIsFormChanged] = useState(false);
|
const [isFormChanged, setIsFormChanged] = useState(false);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const { user, hasSomePermissions } = useAuth();
|
const { user, hasSomePermissions } = useAuth();
|
||||||
const { pageWidth } = useContext?.(MainLayoutContext);
|
const context = useContext(MainLayoutContext);
|
||||||
|
const pageWidth = context?.pageWidth;
|
||||||
function handleFieldsChange() {
|
function handleFieldsChange() {
|
||||||
setIsFormChanged(true);
|
setIsFormChanged(true);
|
||||||
}
|
}
|
||||||
|
@ -43,7 +44,6 @@ export default function BaseSettingPage() {
|
||||||
}
|
}
|
||||||
async function onSubmit(values: BaseSetting) {
|
async function onSubmit(values: BaseSetting) {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await update.mutateAsync({
|
await update.mutateAsync({
|
||||||
where: {
|
where: {
|
||||||
|
|
|
@ -84,9 +84,6 @@ const CategorySection = () => {
|
||||||
// description: term.description
|
// description: term.description
|
||||||
// })) || [];
|
// })) || [];
|
||||||
// },[data])
|
// },[data])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const handleMouseEnter = useCallback((index: number) => {
|
const handleMouseEnter = useCallback((index: number) => {
|
||||||
setHoveredIndex(index);
|
setHoveredIndex(index);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { useState, useMemo, useEffect } from 'react';
|
import React, { useState, useMemo, useEffect } from 'react';
|
||||||
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
||||||
import { Button, Card, Typography, Tag, Progress, Spin } from 'antd';
|
import { Button, Card, Typography, Tag, Progress, Spin, Empty } from 'antd';
|
||||||
import {
|
import {
|
||||||
PlayCircleOutlined,
|
PlayCircleOutlined,
|
||||||
UserOutlined,
|
UserOutlined,
|
||||||
|
@ -17,7 +17,6 @@ interface GetTaxonomyProps {
|
||||||
categories: string[];
|
categories: string[];
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function useGetTaxonomy({ type }): GetTaxonomyProps {
|
function useGetTaxonomy({ type }): GetTaxonomyProps {
|
||||||
const { data, isLoading }: { data: TermDto[], isLoading: boolean } = api.term.findMany.useQuery({
|
const { data, isLoading }: { data: TermDto[], isLoading: boolean } = api.term.findMany.useQuery({
|
||||||
where: {
|
where: {
|
||||||
|
@ -40,6 +39,7 @@ function useGetTaxonomy({ type }): GetTaxonomyProps {
|
||||||
}, [data]);
|
}, [data]);
|
||||||
return { categories, isLoading }
|
return { categories, isLoading }
|
||||||
}
|
}
|
||||||
|
// 修改useGetName中的查询条件
|
||||||
|
|
||||||
function useFetchCoursesByCategory(category: string) {
|
function useFetchCoursesByCategory(category: string) {
|
||||||
const isAll = category === '全部';
|
const isAll = category === '全部';
|
||||||
|
@ -60,6 +60,8 @@ function useFetchCoursesByCategory(category: string) {
|
||||||
return { data, isLoading};
|
return { data, isLoading};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 不同分类跳转
|
||||||
|
|
||||||
const { Title, Text } = Typography;
|
const { Title, Text } = Typography;
|
||||||
|
|
||||||
interface Course {
|
interface Course {
|
||||||
|
@ -82,6 +84,7 @@ interface CoursesSectionProps {
|
||||||
initialVisibleCoursesCount?: number;
|
initialVisibleCoursesCount?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const CoursesSection: React.FC<CoursesSectionProps> = ({
|
const CoursesSection: React.FC<CoursesSectionProps> = ({
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
|
@ -126,6 +129,9 @@ const CoursesSection: React.FC<CoursesSectionProps> = ({
|
||||||
navigate(`/course?courseId=${course.id}/detail`);
|
navigate(`/course?courseId=${course.id}/detail`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEffect(()=>{
|
||||||
|
console.log('data:', data)
|
||||||
|
})
|
||||||
const filteredCourses = useMemo(() => {
|
const filteredCourses = useMemo(() => {
|
||||||
return selectedCategory === '全部'
|
return selectedCategory === '全部'
|
||||||
? data
|
? data
|
||||||
|
@ -162,7 +168,12 @@ const CoursesSection: React.FC<CoursesSectionProps> = ({
|
||||||
: 'bg-white text-gray-600 hover:bg-gray-100'
|
: 'bg-white text-gray-600 hover:bg-gray-100'
|
||||||
}`}
|
}`}
|
||||||
>全部</Tag>
|
>全部</Tag>
|
||||||
{
|
{gateGory.categories.length === 0 && (
|
||||||
|
<Empty
|
||||||
|
description="暂无课程分类"
|
||||||
|
image={Empty.PRESENTED_IMAGE_DEFAULT}
|
||||||
|
/>
|
||||||
|
)}:{
|
||||||
gateGory.categories.map((category) => (
|
gateGory.categories.map((category) => (
|
||||||
<Tag
|
<Tag
|
||||||
key={category}
|
key={category}
|
||||||
|
@ -181,6 +192,7 @@ const CoursesSection: React.FC<CoursesSectionProps> = ({
|
||||||
</Tag>
|
</Tag>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,16 +45,16 @@ const carouselItems: CarouselItem[] = [
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const HeroSection = () => {
|
||||||
|
const carouselRef = useRef<CarouselRef>(null);
|
||||||
|
const { statistics, baseSetting } = useAppConfig();
|
||||||
|
|
||||||
const platformStats: PlatformStat[] = [
|
const platformStats: PlatformStat[] = [
|
||||||
{ icon: <TeamOutlined />, value: "50,000+", label: "注册学员" },
|
{ icon: <TeamOutlined />, value: "50,000+", label: "注册学员" },
|
||||||
{ icon: <BookOutlined />, value: "1,000+", label: "精品课程" },
|
{ icon: <BookOutlined />, value: "1,000+", label: "精品课程" },
|
||||||
// { icon: <StarOutlined />, value: '98%', label: '好评度' },
|
// { icon: <StarOutlined />, value: '98%', label: '好评度' },
|
||||||
{ icon: <EyeOutlined />, value: "100万+", label: "观看次数" },
|
{ icon: <EyeOutlined />, value: "4552", label: "观看次数" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const HeroSection = () => {
|
|
||||||
const carouselRef = useRef<CarouselRef>(null);
|
|
||||||
|
|
||||||
const handlePrev = useCallback(() => {
|
const handlePrev = useCallback(() => {
|
||||||
carouselRef.current?.prev();
|
carouselRef.current?.prev();
|
||||||
}, []);
|
}, []);
|
||||||
|
@ -97,11 +97,10 @@ const HeroSection = () => {
|
||||||
{/* Content Container */}
|
{/* Content Container */}
|
||||||
<div className="relative h-full max-w-7xl mx-auto px-6 lg:px-8"></div>
|
<div className="relative h-full max-w-7xl mx-auto px-6 lg:px-8"></div>
|
||||||
</div>
|
</div>
|
||||||
)))
|
))
|
||||||
:(
|
) : (
|
||||||
<div></div>
|
<div></div>
|
||||||
)
|
)}
|
||||||
}
|
|
||||||
</Carousel>
|
</Carousel>
|
||||||
|
|
||||||
{/* Navigation Buttons */}
|
{/* Navigation Buttons */}
|
||||||
|
|
|
@ -127,12 +127,10 @@ const HomePage = () => {
|
||||||
});
|
});
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) {
|
if (data) {
|
||||||
console.log('mockCourses data:', data);
|
console.log('Courses data:', data);
|
||||||
}
|
}
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
// 数据处理逻辑
|
|
||||||
// 修正依赖数组
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen">
|
<div className="min-h-screen">
|
||||||
<HeroSection />
|
<HeroSection />
|
||||||
|
|
|
@ -8,7 +8,7 @@ export function MainFooter() {
|
||||||
{/* 开发组织信息 */}
|
{/* 开发组织信息 */}
|
||||||
<div className="text-center md:text-left space-y-2">
|
<div className="text-center md:text-left space-y-2">
|
||||||
<h3 className="text-white font-semibold text-sm flex items-center justify-center md:justify-start">
|
<h3 className="text-white font-semibold text-sm flex items-center justify-center md:justify-start">
|
||||||
创新高地 软件小组
|
软件与数据小组
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-gray-400 text-xs italic">
|
<p className="text-gray-400 text-xs italic">
|
||||||
提供技术支持
|
提供技术支持
|
||||||
|
|
|
@ -84,7 +84,7 @@ export default function StaffForm() {
|
||||||
form.setFieldValue("officerId", data.officerId);
|
form.setFieldValue("officerId", data.officerId);
|
||||||
form.setFieldValue("phoneNumber", data.phoneNumber);
|
form.setFieldValue("phoneNumber", data.phoneNumber);
|
||||||
form.setFieldValue("enabled", data.enabled);
|
form.setFieldValue("enabled", data.enabled);
|
||||||
form.setFieldValue("enabled", data.avatar);
|
form.setFieldValue("avatar", data.avatar);
|
||||||
}
|
}
|
||||||
}, [data]);
|
}, [data]);
|
||||||
// useEffect(() => {
|
// useEffect(() => {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
|
import React, { ReactNode } from "react";
|
||||||
export interface MenuItemType {
|
export interface MenuItemType {
|
||||||
icon: JSX.Element;
|
icon: ReactNode;
|
||||||
label: string;
|
label: string;
|
||||||
action: () => void;
|
action: () => void;
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ export interface TusUploaderProps {
|
||||||
value?: string[];
|
value?: string[];
|
||||||
onChange?: (value: string[]) => void;
|
onChange?: (value: string[]) => void;
|
||||||
multiple?: boolean;
|
multiple?: boolean;
|
||||||
|
allowTypes?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UploadingFile {
|
interface UploadingFile {
|
||||||
|
@ -25,8 +26,8 @@ export const TusUploader = ({
|
||||||
value = [],
|
value = [],
|
||||||
onChange,
|
onChange,
|
||||||
multiple = true,
|
multiple = true,
|
||||||
|
allowTypes = undefined,
|
||||||
}: TusUploaderProps) => {
|
}: TusUploaderProps) => {
|
||||||
|
|
||||||
const { handleFileUpload, uploadProgress } = useTusUpload();
|
const { handleFileUpload, uploadProgress } = useTusUpload();
|
||||||
const [uploadingFiles, setUploadingFiles] = useState<UploadingFile[]>([]);
|
const [uploadingFiles, setUploadingFiles] = useState<UploadingFile[]>([]);
|
||||||
const [completedFiles, setCompletedFiles] = useState<UploadingFile[]>(
|
const [completedFiles, setCompletedFiles] = useState<UploadingFile[]>(
|
||||||
|
@ -61,7 +62,10 @@ export const TusUploader = ({
|
||||||
|
|
||||||
const handleBeforeUpload = useCallback(
|
const handleBeforeUpload = useCallback(
|
||||||
(file: File) => {
|
(file: File) => {
|
||||||
|
if (allowTypes && !allowTypes.includes(file.type)) {
|
||||||
|
toast.error(`文件类型 ${file.type} 不在允许范围内`);
|
||||||
|
return Upload.LIST_IGNORE; // 使用 antd 的官方阻止方式
|
||||||
|
}
|
||||||
const fileKey = `${file.name}-${Date.now()}`;
|
const fileKey = `${file.name}-${Date.now()}`;
|
||||||
|
|
||||||
setUploadingFiles((prev) => [
|
setUploadingFiles((prev) => [
|
||||||
|
@ -136,10 +140,10 @@ export const TusUploader = ({
|
||||||
return (
|
return (
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Upload.Dragger
|
<Upload.Dragger
|
||||||
|
accept={allowTypes?.join(",")}
|
||||||
name="files"
|
name="files"
|
||||||
multiple={multiple}
|
multiple={multiple}
|
||||||
showUploadList={false}
|
showUploadList={false}
|
||||||
|
|
||||||
beforeUpload={handleBeforeUpload}>
|
beforeUpload={handleBeforeUpload}>
|
||||||
<p className="ant-upload-drag-icon">
|
<p className="ant-upload-drag-icon">
|
||||||
<UploadOutlined />
|
<UploadOutlined />
|
||||||
|
@ -149,6 +153,11 @@ export const TusUploader = ({
|
||||||
</p>
|
</p>
|
||||||
<p className="ant-upload-hint">
|
<p className="ant-upload-hint">
|
||||||
{multiple ? "支持单个或批量上传文件" : "仅支持上传单个文件"}
|
{multiple ? "支持单个或批量上传文件" : "仅支持上传单个文件"}
|
||||||
|
{allowTypes && (
|
||||||
|
<span className="block text-xs text-gray-500">
|
||||||
|
允许类型: {allowTypes.join(", ")}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="px-2 py-0 rounded mt-1">
|
<div className="px-2 py-0 rounded mt-1">
|
||||||
|
|
|
@ -47,7 +47,7 @@ export const CourseDetailDescription: React.FC = () => {
|
||||||
<div>{course?.subTitle}</div>
|
<div>{course?.subTitle}</div>
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
<EyeOutlined></EyeOutlined>
|
<EyeOutlined></EyeOutlined>
|
||||||
<div>{course?.meta?.views}</div>
|
<div>{course?.meta?.views || 0}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
<CalendarOutlined></CalendarOutlined>
|
<CalendarOutlined></CalendarOutlined>
|
||||||
|
|
|
@ -21,7 +21,7 @@ export function CourseDetailHeader() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Header className="select-none flex items-center justify-center bg-white shadow-md border-b border-gray-100 fixed w-full z-30">
|
<Header className="select-none flex items-center justify-center bg-white shadow-md border-b border-gray-100 fixed w-full z-30">
|
||||||
<div className="w-full max-w-screen-2xl px-4 md:px-6 mx-auto flex items-center justify-between h-full">
|
<div className="w-full flex items-center justify-between h-full">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<HomeOutlined
|
<HomeOutlined
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|
|
@ -2,6 +2,7 @@ import {
|
||||||
SkeletonItem,
|
SkeletonItem,
|
||||||
SkeletonSection,
|
SkeletonSection,
|
||||||
} from "@web/src/components/presentation/Skeleton";
|
} from "@web/src/components/presentation/Skeleton";
|
||||||
|
import { api } from "packages/client/dist";
|
||||||
|
|
||||||
export const CourseDetailSkeleton = () => {
|
export const CourseDetailSkeleton = () => {
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -10,7 +10,13 @@ import { useSortable } from "@dnd-kit/sortable";
|
||||||
import { CSS } from "@dnd-kit/utilities";
|
import { CSS } from "@dnd-kit/utilities";
|
||||||
import QuillEditor from "@web/src/components/common/editor/quill/QuillEditor";
|
import QuillEditor from "@web/src/components/common/editor/quill/QuillEditor";
|
||||||
import { TusUploader } from "@web/src/components/common/uploader/TusUploader";
|
import { TusUploader } from "@web/src/components/common/uploader/TusUploader";
|
||||||
import { Lecture, LectureType, LessonTypeLabel, PostType } from "@nice/common";
|
import {
|
||||||
|
Lecture,
|
||||||
|
LectureType,
|
||||||
|
LessonTypeLabel,
|
||||||
|
PostType,
|
||||||
|
videoMimeTypes,
|
||||||
|
} from "@nice/common";
|
||||||
import { usePost } from "@nice/client";
|
import { usePost } from "@nice/client";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
|
|
||||||
|
@ -134,7 +140,9 @@ export const SortableLecture: React.FC<SortableLectureProps> = ({
|
||||||
name="title"
|
name="title"
|
||||||
initialValue={field?.title}
|
initialValue={field?.title}
|
||||||
className="mb-0 flex-1"
|
className="mb-0 flex-1"
|
||||||
rules={[{ required: true }]}>
|
rules={[
|
||||||
|
{ required: true, message: "请输入课时标题" },
|
||||||
|
]}>
|
||||||
<Input placeholder="课时标题" />
|
<Input placeholder="课时标题" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
|
@ -158,14 +166,24 @@ export const SortableLecture: React.FC<SortableLectureProps> = ({
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name={["meta", "videoIds"]}
|
name={["meta", "videoIds"]}
|
||||||
className="mb-0 flex-1"
|
className="mb-0 flex-1"
|
||||||
rules={[{ required: true }]}>
|
rules={[
|
||||||
<TusUploader multiple={false} />
|
{
|
||||||
|
required: true,
|
||||||
|
message: "请传入视频",
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
<TusUploader
|
||||||
|
allowTypes={videoMimeTypes}
|
||||||
|
multiple={false}
|
||||||
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
) : (
|
) : (
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="content"
|
name="content"
|
||||||
className="mb-0 flex-1"
|
className="mb-0 flex-1"
|
||||||
rules={[{ required: true }]}>
|
rules={[
|
||||||
|
{ required: true, message: "请输入内容" },
|
||||||
|
]}>
|
||||||
<QuillEditor />
|
<QuillEditor />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -2,9 +2,10 @@ import { Button, Form, Input, Spin, Switch, message } from "antd";
|
||||||
import { useContext, useEffect } from "react";
|
import { useContext, useEffect } from "react";
|
||||||
import { useStaff } from "@nice/client";
|
import { useStaff } from "@nice/client";
|
||||||
import DepartmentSelect from "../department/department-select";
|
import DepartmentSelect from "../department/department-select";
|
||||||
import { api } from "@nice/client"
|
import { api } from "@nice/client";
|
||||||
import { StaffEditorContext } from "./staff-editor";
|
import { StaffEditorContext } from "./staff-editor";
|
||||||
import { useAuth } from "@web/src/providers/auth-provider";
|
import { useAuth } from "@web/src/providers/auth-provider";
|
||||||
|
import AvatarUploader from "../../common/uploader/AvatarUploader";
|
||||||
export default function StaffForm() {
|
export default function StaffForm() {
|
||||||
const { create, update } = useStaff(); // Ensure you have these methods in your hooks
|
const { create, update } = useStaff(); // Ensure you have these methods in your hooks
|
||||||
const {
|
const {
|
||||||
|
@ -21,6 +22,7 @@ export default function StaffForm() {
|
||||||
{ where: { id: editId } },
|
{ where: { id: editId } },
|
||||||
{ enabled: !!editId }
|
{ enabled: !!editId }
|
||||||
);
|
);
|
||||||
|
|
||||||
const { isRoot } = useAuth();
|
const { isRoot } = useAuth();
|
||||||
async function handleFinish(values: any) {
|
async function handleFinish(values: any) {
|
||||||
const {
|
const {
|
||||||
|
@ -31,8 +33,9 @@ export default function StaffForm() {
|
||||||
password,
|
password,
|
||||||
phoneNumber,
|
phoneNumber,
|
||||||
officerId,
|
officerId,
|
||||||
enabled
|
enabled,
|
||||||
} = values
|
avatar,
|
||||||
|
} = values;
|
||||||
setFormLoading(true);
|
setFormLoading(true);
|
||||||
try {
|
try {
|
||||||
if (data && editId) {
|
if (data && editId) {
|
||||||
|
@ -46,8 +49,9 @@ export default function StaffForm() {
|
||||||
password,
|
password,
|
||||||
phoneNumber,
|
phoneNumber,
|
||||||
officerId,
|
officerId,
|
||||||
enabled
|
enabled,
|
||||||
}
|
avatar,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await create.mutateAsync({
|
await create.mutateAsync({
|
||||||
|
@ -58,8 +62,9 @@ export default function StaffForm() {
|
||||||
domainId: fieldDomainId ? fieldDomainId : domainId,
|
domainId: fieldDomainId ? fieldDomainId : domainId,
|
||||||
password,
|
password,
|
||||||
officerId,
|
officerId,
|
||||||
phoneNumber
|
phoneNumber,
|
||||||
}
|
avatar,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
form.resetFields();
|
form.resetFields();
|
||||||
if (deptId) form.setFieldValue("deptId", deptId);
|
if (deptId) form.setFieldValue("deptId", deptId);
|
||||||
|
@ -77,13 +82,14 @@ export default function StaffForm() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
form.resetFields();
|
form.resetFields();
|
||||||
if (data && editId) {
|
if (data && editId) {
|
||||||
form.setFieldValue("username", data.username);
|
form.setFieldValue("username", data?.username);
|
||||||
form.setFieldValue("showname", data.showname);
|
form.setFieldValue("showname", data?.showname);
|
||||||
form.setFieldValue("domainId", data.domainId);
|
form.setFieldValue("domainId", data?.domainId);
|
||||||
form.setFieldValue("deptId", data.deptId);
|
form.setFieldValue("deptId", data?.deptId);
|
||||||
form.setFieldValue("officerId", data.officerId);
|
form.setFieldValue("officerId", data?.officerId);
|
||||||
form.setFieldValue("phoneNumber", data.phoneNumber);
|
form.setFieldValue("phoneNumber", data?.phoneNumber);
|
||||||
form.setFieldValue("enabled", data.enabled)
|
form.setFieldValue("enabled", data?.enabled);
|
||||||
|
form.setFieldValue("avatar", data?.avatar);
|
||||||
}
|
}
|
||||||
}, [data]);
|
}, [data]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -99,6 +105,7 @@ export default function StaffForm() {
|
||||||
<Spin />
|
<Spin />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Form
|
<Form
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
form={form}
|
form={form}
|
||||||
|
@ -106,6 +113,9 @@ export default function StaffForm() {
|
||||||
requiredMark="optional"
|
requiredMark="optional"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
onFinish={handleFinish}>
|
onFinish={handleFinish}>
|
||||||
|
<Form.Item name={"avatar"} label="头像">
|
||||||
|
<AvatarUploader></AvatarUploader>
|
||||||
|
</Form.Item>
|
||||||
{canManageAnyStaff && (
|
{canManageAnyStaff && (
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name={"domainId"}
|
name={"domainId"}
|
||||||
|
@ -127,7 +137,8 @@ export default function StaffForm() {
|
||||||
rules={[{ required: true }]}
|
rules={[{ required: true }]}
|
||||||
name={"username"}
|
name={"username"}
|
||||||
label="帐号">
|
label="帐号">
|
||||||
<Input allowClear
|
<Input
|
||||||
|
allowClear
|
||||||
autoComplete="new-username" // 使用非标准的自动完成值
|
autoComplete="new-username" // 使用非标准的自动完成值
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
/>
|
/>
|
||||||
|
@ -136,7 +147,8 @@ export default function StaffForm() {
|
||||||
rules={[{ required: true }]}
|
rules={[{ required: true }]}
|
||||||
name={"showname"}
|
name={"showname"}
|
||||||
label="姓名">
|
label="姓名">
|
||||||
<Input allowClear
|
<Input
|
||||||
|
allowClear
|
||||||
autoComplete="new-name" // 使用非标准的自动完成值
|
autoComplete="new-name" // 使用非标准的自动完成值
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
/>
|
/>
|
||||||
|
@ -146,8 +158,8 @@ export default function StaffForm() {
|
||||||
{
|
{
|
||||||
required: false,
|
required: false,
|
||||||
pattern: /^\d{5,18}$/,
|
pattern: /^\d{5,18}$/,
|
||||||
message: "请输入正确的证件号(数字)"
|
message: "请输入正确的证件号(数字)",
|
||||||
}
|
},
|
||||||
]}
|
]}
|
||||||
name={"officerId"}
|
name={"officerId"}
|
||||||
label="证件号">
|
label="证件号">
|
||||||
|
@ -158,20 +170,29 @@ export default function StaffForm() {
|
||||||
{
|
{
|
||||||
required: false,
|
required: false,
|
||||||
pattern: /^\d{6,11}$/,
|
pattern: /^\d{6,11}$/,
|
||||||
message: "请输入正确的手机号(数字)"
|
message: "请输入正确的手机号(数字)",
|
||||||
}
|
},
|
||||||
]}
|
]}
|
||||||
name={"phoneNumber"}
|
name={"phoneNumber"}
|
||||||
label="手机号">
|
label="手机号">
|
||||||
<Input autoComplete="new-phone" // 使用非标准的自动完成值
|
<Input
|
||||||
spellCheck={false} allowClear />
|
autoComplete="new-phone" // 使用非标准的自动完成值
|
||||||
|
spellCheck={false}
|
||||||
|
allowClear
|
||||||
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label="密码" name={"password"}>
|
<Form.Item label="密码" name={"password"}>
|
||||||
<Input.Password spellCheck={false} visibilityToggle autoComplete="new-password" />
|
<Input.Password
|
||||||
|
spellCheck={false}
|
||||||
|
visibilityToggle
|
||||||
|
autoComplete="new-password"
|
||||||
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
{editId && <Form.Item label="是否启用" name={"enabled"}>
|
{editId && (
|
||||||
|
<Form.Item label="是否启用" name={"enabled"}>
|
||||||
<Switch></Switch>
|
<Switch></Switch>
|
||||||
</Form.Item>}
|
</Form.Item>
|
||||||
|
)}
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -64,7 +64,7 @@ services:
|
||||||
# extra_hosts:
|
# extra_hosts:
|
||||||
# - "host.docker.internal:host-gateway"
|
# - "host.docker.internal:host-gateway"
|
||||||
nginx:
|
nginx:
|
||||||
image: nice-nginx:latest
|
image: nice-nginx:2.0
|
||||||
ports:
|
ports:
|
||||||
- "80:80"
|
- "80:80"
|
||||||
volumes:
|
volumes:
|
||||||
|
@ -79,6 +79,7 @@ services:
|
||||||
entrypoint: ["/docker-entrypoint.sh"]
|
entrypoint: ["/docker-entrypoint.sh"]
|
||||||
extra_hosts:
|
extra_hosts:
|
||||||
- "host.docker.internal:host-gateway"
|
- "host.docker.internal:host-gateway"
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
image: redis:latest
|
image: redis:latest
|
||||||
ports:
|
ports:
|
||||||
|
|
|
@ -10,6 +10,7 @@ export function useAppConfig() {
|
||||||
api.app_config.findFirst.useQuery({
|
api.app_config.findFirst.useQuery({
|
||||||
where: { slug: AppConfigSlug.BASE_SETTING },
|
where: { slug: AppConfigSlug.BASE_SETTING },
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleMutationSuccess = useCallback(() => {
|
const handleMutationSuccess = useCallback(() => {
|
||||||
utils.app_config.invalidate();
|
utils.app_config.invalidate();
|
||||||
}, [utils]);
|
}, [utils]);
|
||||||
|
@ -26,7 +27,8 @@ export function useAppConfig() {
|
||||||
});
|
});
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data?.meta) {
|
if (data?.meta) {
|
||||||
setBaseSetting(JSON.parse(data?.meta));
|
// console.log(JSON.parse(data?.meta));
|
||||||
|
setBaseSetting(data?.meta);
|
||||||
}
|
}
|
||||||
}, [data, isLoading]);
|
}, [data, isLoading]);
|
||||||
const splashScreen = useMemo(() => {
|
const splashScreen = useMemo(() => {
|
||||||
|
@ -38,6 +40,16 @@ export function useAppConfig() {
|
||||||
const slides = useMemo(() => {
|
const slides = useMemo(() => {
|
||||||
return baseSetting?.appConfig?.slides || [];
|
return baseSetting?.appConfig?.slides || [];
|
||||||
}, [baseSetting]);
|
}, [baseSetting]);
|
||||||
|
const statistics = useMemo(() => {
|
||||||
|
return (
|
||||||
|
baseSetting?.appConfig?.statistics || {
|
||||||
|
reads: 0,
|
||||||
|
staffs: 0,
|
||||||
|
courses: 0,
|
||||||
|
lectures: 0,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}, [baseSetting]);
|
||||||
return {
|
return {
|
||||||
create,
|
create,
|
||||||
deleteMany,
|
deleteMany,
|
||||||
|
@ -47,5 +59,6 @@ export function useAppConfig() {
|
||||||
devDept,
|
devDept,
|
||||||
isLoading,
|
isLoading,
|
||||||
slides,
|
slides,
|
||||||
|
statistics,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -81,3 +81,17 @@ export const InitAppConfigs: Prisma.AppConfigCreateInput[] = [
|
||||||
description: "",
|
description: "",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
export const videoMimeTypes = [
|
||||||
|
"video/*", // 通配符 (部分浏览器可能不支持)
|
||||||
|
"video/mp4", // .mp4
|
||||||
|
"video/quicktime", // .mov
|
||||||
|
"video/x-msvideo", // .avi
|
||||||
|
"video/x-matroska", // .mkv
|
||||||
|
"video/webm", // .webm
|
||||||
|
"video/ogg", // .ogv
|
||||||
|
"video/mpeg", // .mpeg
|
||||||
|
"video/3gpp", // .3gp
|
||||||
|
"video/3gpp2", // .3g2
|
||||||
|
"video/x-flv", // .flv
|
||||||
|
"video/x-ms-wmv", // .wmv
|
||||||
|
];
|
||||||
|
|
|
@ -44,7 +44,12 @@ export interface BaseSetting {
|
||||||
splashScreen?: string;
|
splashScreen?: string;
|
||||||
devDept?: string;
|
devDept?: string;
|
||||||
slides?: [];
|
slides?: [];
|
||||||
|
statistics?: {
|
||||||
reads?: number;
|
reads?: number;
|
||||||
|
courses?: number;
|
||||||
|
lectures?: number;
|
||||||
|
staffs?: number;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
export type RowModelResult = {
|
export type RowModelResult = {
|
||||||
|
|
Loading…
Reference in New Issue