Merge branch 'main' of http://113.45.157.195:3003/insiinc/re-mooc
This commit is contained in:
commit
010cb6fdab
|
@ -40,7 +40,11 @@ export class VisitService extends BaseService<Prisma.VisitDelegate> {
|
|||
id: postId,
|
||||
visitType: args.data.type, // 直接复用传入的类型
|
||||
});
|
||||
EventBus.emit('updateTotalCourseViewCount', {
|
||||
visitType: args.data.type, // 直接复用传入的类型
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
async createMany(args: Prisma.VisitCreateManyArgs, staff?: UserProfile) {
|
||||
|
@ -138,6 +142,9 @@ export class VisitService extends BaseService<Prisma.VisitDelegate> {
|
|||
id: args?.where?.postId as string,
|
||||
visitType: args.where.type as any, // 直接复用传入的类型
|
||||
});
|
||||
EventBus.emit('updateTotalCourseViewCount', {
|
||||
visitType: args.where.type as any, // 直接复用传入的类型
|
||||
});
|
||||
}
|
||||
}
|
||||
return superDetele;
|
||||
|
|
|
@ -7,11 +7,18 @@ import {
|
|||
VisitType,
|
||||
} from '@nice/common';
|
||||
export async function updateTotalCourseViewCount(type: VisitType) {
|
||||
const courses = await db.post.findMany({
|
||||
where: { type: PostType.COURSE },
|
||||
select: { id: true },
|
||||
const posts = await db.post.findMany({
|
||||
where: {
|
||||
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({
|
||||
_sum: {
|
||||
views: true,
|
||||
|
@ -30,6 +37,10 @@ export async function updateTotalCourseViewCount(type: VisitType) {
|
|||
meta: true,
|
||||
},
|
||||
});
|
||||
const staffs = await db.staff.count({
|
||||
where: { deletedAt: null },
|
||||
});
|
||||
|
||||
const baseSeting = appConfig.meta as BaseSetting;
|
||||
await db.appConfig.update({
|
||||
where: {
|
||||
|
@ -38,7 +49,15 @@ export async function updateTotalCourseViewCount(type: VisitType) {
|
|||
data: {
|
||||
meta: {
|
||||
...baseSeting,
|
||||
reads: totalViews,
|
||||
appConfig: {
|
||||
...(baseSeting?.appConfig || {}),
|
||||
statistics: {
|
||||
reads: totalViews._sum.views || 0,
|
||||
courses: courseIds?.length || 0,
|
||||
staffs: staffs || 0,
|
||||
lectures: lectures?.length || 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -45,16 +45,16 @@ const carouselItems: CarouselItem[] = [
|
|||
},
|
||||
];
|
||||
|
||||
const platformStats: PlatformStat[] = [
|
||||
{ icon: <TeamOutlined />, value: "50,000+", label: "注册学员" },
|
||||
{ icon: <BookOutlined />, value: "1,000+", label: "精品课程" },
|
||||
// { icon: <StarOutlined />, value: '98%', label: '好评度' },
|
||||
{ icon: <EyeOutlined />, value: "100万+", label: "观看次数" },
|
||||
];
|
||||
|
||||
const HeroSection = () => {
|
||||
const carouselRef = useRef<CarouselRef>(null);
|
||||
const { statistics, baseSetting } = useAppConfig();
|
||||
|
||||
const platformStats: PlatformStat[] = [
|
||||
{ icon: <TeamOutlined />, value: "50,000+", label: "注册学员" },
|
||||
{ icon: <BookOutlined />, value: "1,000+", label: "精品课程" },
|
||||
// { icon: <StarOutlined />, value: '98%', label: '好评度' },
|
||||
{ icon: <EyeOutlined />, value: "4552", label: "观看次数" },
|
||||
];
|
||||
const handlePrev = useCallback(() => {
|
||||
carouselRef.current?.prev();
|
||||
}, []);
|
||||
|
@ -74,8 +74,8 @@ const HeroSection = () => {
|
|||
dots={{
|
||||
className: "carousel-dots !bottom-32 !z-20",
|
||||
}}>
|
||||
{Array.isArray(carouselItems)?
|
||||
(carouselItems.map((item, index) => (
|
||||
{Array.isArray(carouselItems) ? (
|
||||
carouselItems.map((item, index) => (
|
||||
<div key={index} className="relative h-[600px]">
|
||||
<div
|
||||
className="absolute inset-0 bg-cover bg-center transform transition-[transform,filter] duration-[2000ms] group-hover:scale-105 group-hover:brightness-110 will-change-[transform,filter]"
|
||||
|
@ -93,11 +93,10 @@ const HeroSection = () => {
|
|||
{/* Content Container */}
|
||||
<div className="relative h-full max-w-7xl mx-auto px-6 lg:px-8"></div>
|
||||
</div>
|
||||
)))
|
||||
:(
|
||||
<div></div>
|
||||
)
|
||||
}
|
||||
))
|
||||
) : (
|
||||
<div></div>
|
||||
)}
|
||||
</Carousel>
|
||||
|
||||
{/* Navigation Buttons */}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import React, { ReactNode } from "react";
|
||||
export interface MenuItemType {
|
||||
icon: JSX.Element;
|
||||
icon: ReactNode;
|
||||
label: string;
|
||||
action: () => void;
|
||||
}
|
||||
|
|
|
@ -47,7 +47,7 @@ export const CourseDetailDescription: React.FC = () => {
|
|||
<div>{course?.subTitle}</div>
|
||||
<div className="flex gap-1">
|
||||
<EyeOutlined></EyeOutlined>
|
||||
<div>{course?.meta?.views}</div>
|
||||
<div>{course?.meta?.views || 0}</div>
|
||||
</div>
|
||||
<div className="flex gap-1">
|
||||
<CalendarOutlined></CalendarOutlined>
|
||||
|
|
|
@ -21,7 +21,7 @@ export function CourseDetailHeader() {
|
|||
|
||||
return (
|
||||
<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">
|
||||
<HomeOutlined
|
||||
onClick={() => {
|
||||
|
|
|
@ -2,6 +2,7 @@ import {
|
|||
SkeletonItem,
|
||||
SkeletonSection,
|
||||
} from "@web/src/components/presentation/Skeleton";
|
||||
import { api } from "packages/client/dist";
|
||||
|
||||
export const CourseDetailSkeleton = () => {
|
||||
return (
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
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 DepartmentSelect from "../department/department-select";
|
||||
import { api } from "@nice/client"
|
||||
import { api } from "@nice/client";
|
||||
import { StaffEditorContext } from "./staff-editor";
|
||||
import { useAuth } from "@web/src/providers/auth-provider";
|
||||
import AvatarUploader from "../../common/uploader/AvatarUploader";
|
||||
export default function StaffForm() {
|
||||
const { create, update } = useStaff(); // Ensure you have these methods in your hooks
|
||||
const {
|
||||
|
@ -21,6 +22,7 @@ export default function StaffForm() {
|
|||
{ where: { id: editId } },
|
||||
{ enabled: !!editId }
|
||||
);
|
||||
|
||||
const { isRoot } = useAuth();
|
||||
async function handleFinish(values: any) {
|
||||
const {
|
||||
|
@ -31,8 +33,9 @@ export default function StaffForm() {
|
|||
password,
|
||||
phoneNumber,
|
||||
officerId,
|
||||
enabled
|
||||
} = values
|
||||
enabled,
|
||||
avatar,
|
||||
} = values;
|
||||
setFormLoading(true);
|
||||
try {
|
||||
if (data && editId) {
|
||||
|
@ -46,8 +49,9 @@ export default function StaffForm() {
|
|||
password,
|
||||
phoneNumber,
|
||||
officerId,
|
||||
enabled
|
||||
}
|
||||
enabled,
|
||||
avatar,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await create.mutateAsync({
|
||||
|
@ -58,8 +62,9 @@ export default function StaffForm() {
|
|||
domainId: fieldDomainId ? fieldDomainId : domainId,
|
||||
password,
|
||||
officerId,
|
||||
phoneNumber
|
||||
}
|
||||
phoneNumber,
|
||||
avatar,
|
||||
},
|
||||
});
|
||||
form.resetFields();
|
||||
if (deptId) form.setFieldValue("deptId", deptId);
|
||||
|
@ -77,13 +82,14 @@ export default function StaffForm() {
|
|||
useEffect(() => {
|
||||
form.resetFields();
|
||||
if (data && editId) {
|
||||
form.setFieldValue("username", data.username);
|
||||
form.setFieldValue("showname", data.showname);
|
||||
form.setFieldValue("domainId", data.domainId);
|
||||
form.setFieldValue("deptId", data.deptId);
|
||||
form.setFieldValue("officerId", data.officerId);
|
||||
form.setFieldValue("phoneNumber", data.phoneNumber);
|
||||
form.setFieldValue("enabled", data.enabled)
|
||||
form.setFieldValue("username", data?.username);
|
||||
form.setFieldValue("showname", data?.showname);
|
||||
form.setFieldValue("domainId", data?.domainId);
|
||||
form.setFieldValue("deptId", data?.deptId);
|
||||
form.setFieldValue("officerId", data?.officerId);
|
||||
form.setFieldValue("phoneNumber", data?.phoneNumber);
|
||||
form.setFieldValue("enabled", data?.enabled);
|
||||
form.setFieldValue("avatar", data?.avatar);
|
||||
}
|
||||
}, [data]);
|
||||
useEffect(() => {
|
||||
|
@ -99,6 +105,7 @@ export default function StaffForm() {
|
|||
<Spin />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Form
|
||||
disabled={isLoading}
|
||||
form={form}
|
||||
|
@ -106,6 +113,9 @@ export default function StaffForm() {
|
|||
requiredMark="optional"
|
||||
autoComplete="off"
|
||||
onFinish={handleFinish}>
|
||||
<Form.Item name={"avatar"} label="头像">
|
||||
<AvatarUploader></AvatarUploader>
|
||||
</Form.Item>
|
||||
{canManageAnyStaff && (
|
||||
<Form.Item
|
||||
name={"domainId"}
|
||||
|
@ -127,7 +137,8 @@ export default function StaffForm() {
|
|||
rules={[{ required: true }]}
|
||||
name={"username"}
|
||||
label="帐号">
|
||||
<Input allowClear
|
||||
<Input
|
||||
allowClear
|
||||
autoComplete="new-username" // 使用非标准的自动完成值
|
||||
spellCheck={false}
|
||||
/>
|
||||
|
@ -136,7 +147,8 @@ export default function StaffForm() {
|
|||
rules={[{ required: true }]}
|
||||
name={"showname"}
|
||||
label="姓名">
|
||||
<Input allowClear
|
||||
<Input
|
||||
allowClear
|
||||
autoComplete="new-name" // 使用非标准的自动完成值
|
||||
spellCheck={false}
|
||||
/>
|
||||
|
@ -146,8 +158,8 @@ export default function StaffForm() {
|
|||
{
|
||||
required: false,
|
||||
pattern: /^\d{5,18}$/,
|
||||
message: "请输入正确的证件号(数字)"
|
||||
}
|
||||
message: "请输入正确的证件号(数字)",
|
||||
},
|
||||
]}
|
||||
name={"officerId"}
|
||||
label="证件号">
|
||||
|
@ -158,20 +170,29 @@ export default function StaffForm() {
|
|||
{
|
||||
required: false,
|
||||
pattern: /^\d{6,11}$/,
|
||||
message: "请输入正确的手机号(数字)"
|
||||
}
|
||||
message: "请输入正确的手机号(数字)",
|
||||
},
|
||||
]}
|
||||
name={"phoneNumber"}
|
||||
label="手机号">
|
||||
<Input autoComplete="new-phone" // 使用非标准的自动完成值
|
||||
spellCheck={false} allowClear />
|
||||
<Input
|
||||
autoComplete="new-phone" // 使用非标准的自动完成值
|
||||
spellCheck={false}
|
||||
allowClear
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="密码" name={"password"}>
|
||||
<Input.Password spellCheck={false} visibilityToggle autoComplete="new-password" />
|
||||
<Input.Password
|
||||
spellCheck={false}
|
||||
visibilityToggle
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</Form.Item>
|
||||
{editId && <Form.Item label="是否启用" name={"enabled"}>
|
||||
<Switch></Switch>
|
||||
</Form.Item>}
|
||||
{editId && (
|
||||
<Form.Item label="是否启用" name={"enabled"}>
|
||||
<Switch></Switch>
|
||||
</Form.Item>
|
||||
)}
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -15,7 +15,7 @@ export function useTusUpload() {
|
|||
>({});
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const [uploadError, setUploadError] = useState<string | null>(null);
|
||||
|
||||
|
||||
const getFileId = (url: string) => {
|
||||
const parts = url.split("/");
|
||||
const uploadIndex = parts.findIndex((part) => part === "upload");
|
||||
|
|
|
@ -10,6 +10,7 @@ export function useAppConfig() {
|
|||
api.app_config.findFirst.useQuery({
|
||||
where: { slug: AppConfigSlug.BASE_SETTING },
|
||||
});
|
||||
|
||||
const handleMutationSuccess = useCallback(() => {
|
||||
utils.app_config.invalidate();
|
||||
}, [utils]);
|
||||
|
@ -26,7 +27,8 @@ export function useAppConfig() {
|
|||
});
|
||||
useEffect(() => {
|
||||
if (data?.meta) {
|
||||
setBaseSetting(JSON.parse(data?.meta));
|
||||
// console.log(JSON.parse(data?.meta));
|
||||
setBaseSetting(data?.meta);
|
||||
}
|
||||
}, [data, isLoading]);
|
||||
const splashScreen = useMemo(() => {
|
||||
|
@ -38,6 +40,16 @@ export function useAppConfig() {
|
|||
const slides = useMemo(() => {
|
||||
return baseSetting?.appConfig?.slides || [];
|
||||
}, [baseSetting]);
|
||||
const statistics = useMemo(() => {
|
||||
return (
|
||||
baseSetting?.appConfig?.statistics || {
|
||||
reads: 0,
|
||||
staffs: 0,
|
||||
courses: 0,
|
||||
lectures: 0,
|
||||
}
|
||||
);
|
||||
}, [baseSetting]);
|
||||
return {
|
||||
create,
|
||||
deleteMany,
|
||||
|
@ -47,5 +59,6 @@ export function useAppConfig() {
|
|||
devDept,
|
||||
isLoading,
|
||||
slides,
|
||||
statistics,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -44,7 +44,12 @@ export interface BaseSetting {
|
|||
splashScreen?: string;
|
||||
devDept?: string;
|
||||
slides?: [];
|
||||
reads?: number;
|
||||
statistics?: {
|
||||
reads?: number;
|
||||
courses?: number;
|
||||
lectures?: number;
|
||||
staffs?: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
export type RowModelResult = {
|
||||
|
|
Loading…
Reference in New Issue