diff --git a/apps/server/src/auth/utils.ts b/apps/server/src/auth/utils.ts index 5e8cd9a..b1b2a88 100755 --- a/apps/server/src/auth/utils.ts +++ b/apps/server/src/auth/utils.ts @@ -155,6 +155,7 @@ export class UserProfileService { where: { id }, select: { id: true, + avatar:true, deptId: true, department: true, domainId: true, diff --git a/apps/server/src/models/visit/visit.service.ts b/apps/server/src/models/visit/visit.service.ts index 4742c61..20607e3 100755 --- a/apps/server/src/models/visit/visit.service.ts +++ b/apps/server/src/models/visit/visit.service.ts @@ -40,7 +40,11 @@ export class VisitService extends BaseService { 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 { id: args?.where?.postId as string, visitType: args.where.type as any, // 直接复用传入的类型 }); + EventBus.emit('updateTotalCourseViewCount', { + visitType: args.where.type as any, // 直接复用传入的类型 + }); } } return superDetele; diff --git a/apps/server/src/queue/models/post/post.queue.service.ts b/apps/server/src/queue/models/post/post.queue.service.ts index f7370df..8e49a81 100644 --- a/apps/server/src/queue/models/post/post.queue.service.ts +++ b/apps/server/src/queue/models/post/post.queue.service.ts @@ -22,6 +22,12 @@ export class PostQueueService implements OnModuleInit { EventBus.on('updatePostState', ({ id }) => { this.addUpdatePostState({ id }); }); + EventBus.on('updatePostState', ({ id }) => { + this.addUpdatePostState({ id }); + }); + EventBus.on('updateTotalCourseViewCount', ({ visitType }) => { + this.addUpdateTotalCourseViewCount({ visitType }); + }); } async addUpdateVisitCountJob(data: updateVisitCountJobData) { this.logger.log(`update post view count ${data.id}`); @@ -37,4 +43,14 @@ export class PostQueueService implements OnModuleInit { debounce: { id: `${QueueJobType.UPDATE_POST_STATE}_${data.id}` }, }); } + async addUpdateTotalCourseViewCount({ visitType }) { + this.logger.log(`update post state ${visitType}`); + await this.generalQueue.add( + QueueJobType.UPDATE_TOTAL_COURSE_VIEW_COUNT, + { type: visitType }, + { + debounce: { id: `${QueueJobType.UPDATE_POST_STATE}_${visitType}` }, + }, + ); + } } diff --git a/apps/server/src/queue/models/post/utils.ts b/apps/server/src/queue/models/post/utils.ts index 98eb43c..0b25170 100644 --- a/apps/server/src/queue/models/post/utils.ts +++ b/apps/server/src/queue/models/post/utils.ts @@ -1,4 +1,67 @@ -import { db, VisitType } from '@nice/common'; +import { + AppConfigSlug, + BaseSetting, + db, + PostType, + TaxonomySlug, + VisitType, +} from '@nice/common'; +export async function updateTotalCourseViewCount(type: VisitType) { + const posts = await db.post.findMany({ + where: { + type: { in: [PostType.COURSE, PostType.LECTURE] }, + deletedAt: null, + }, + select: { id: true, type: true }, + }); + + 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, + }, + where: { + postId: { in: courseIds }, + type: type, + }, + }); + const appConfig = await db.appConfig.findFirst({ + where: { + slug: AppConfigSlug.BASE_SETTING, + }, + select: { + id: true, + meta: true, + }, + }); + const staffs = await db.staff.count({ + where: { deletedAt: null }, + }); + + const baseSeting = appConfig.meta as BaseSetting; + await db.appConfig.update({ + where: { + slug: AppConfigSlug.BASE_SETTING, + }, + data: { + meta: { + ...baseSeting, + appConfig: { + ...(baseSeting?.appConfig || {}), + statistics: { + reads: totalViews._sum.views || 0, + courses: courseIds?.length || 0, + staffs: staffs || 0, + lectures: lectures?.length || 0, + }, + }, + }, + }, + }); +} export async function updatePostViewCount(id: string, type: VisitType) { const post = await db.post.findFirst({ where: { id }, diff --git a/apps/server/src/queue/types.ts b/apps/server/src/queue/types.ts index 7e0f308..11119ee 100755 --- a/apps/server/src/queue/types.ts +++ b/apps/server/src/queue/types.ts @@ -4,6 +4,7 @@ export enum QueueJobType { FILE_PROCESS = 'file_process', UPDATE_POST_VISIT_COUNT = 'updatePostVisitCount', UPDATE_POST_STATE = 'updatePostState', + UPDATE_TOTAL_COURSE_VIEW_COUNT = 'updateTotalCourseViewCount', } export type updateVisitCountJobData = { id: string; diff --git a/apps/server/src/queue/worker/processor.ts b/apps/server/src/queue/worker/processor.ts index a968179..86d428b 100755 --- a/apps/server/src/queue/worker/processor.ts +++ b/apps/server/src/queue/worker/processor.ts @@ -11,7 +11,10 @@ import { updateCourseReviewStats, updateParentLectureStats, } from '@server/models/post/utils'; -import { updatePostViewCount } from '../models/post/utils'; +import { + updatePostViewCount, + updateTotalCourseViewCount, +} from '../models/post/utils'; const logger = new Logger('QueueWorker'); export default async function processJob(job: Job) { try { @@ -51,6 +54,9 @@ export default async function processJob(job: Job) { if (job.name === QueueJobType.UPDATE_POST_STATE) { await updatePostViewCount(job.data.id, job.data.type); } + if (job.name === QueueJobType.UPDATE_TOTAL_COURSE_VIEW_COUNT) { + await updateTotalCourseViewCount(job.data.type); + } } catch (error: any) { logger.error( `Error processing stats update job: ${error.message}`, diff --git a/apps/server/src/utils/event-bus.ts b/apps/server/src/utils/event-bus.ts index dfb3409..3f96688 100755 --- a/apps/server/src/utils/event-bus.ts +++ b/apps/server/src/utils/event-bus.ts @@ -21,6 +21,9 @@ type Events = { updatePostState: { id: string; }; + updateTotalCourseViewCount: { + visitType: VisitType | string; + }; onMessageCreated: { data: Partial }; dataChanged: { type: string; operation: CrudOperation; data: any }; }; diff --git a/apps/web/src/app/admin/base-setting/page.tsx b/apps/web/src/app/admin/base-setting/page.tsx index 82601ee..1f8d42f 100755 --- a/apps/web/src/app/admin/base-setting/page.tsx +++ b/apps/web/src/app/admin/base-setting/page.tsx @@ -24,7 +24,8 @@ export default function BaseSettingPage() { const [isFormChanged, setIsFormChanged] = useState(false); const [loading, setLoading] = useState(false); const { user, hasSomePermissions } = useAuth(); - const { pageWidth } = useContext?.(MainLayoutContext); + const context = useContext(MainLayoutContext); + const pageWidth = context?.pageWidth; function handleFieldsChange() { setIsFormChanged(true); } @@ -43,7 +44,6 @@ export default function BaseSettingPage() { } async function onSubmit(values: BaseSetting) { setLoading(true); - try { await update.mutateAsync({ where: { @@ -116,6 +116,13 @@ export default function BaseSettingPage() { +
+ + + +
{/*
= ({ color={selectedCategory === category ? 'blue' : 'default'} onClick={() => { setSelectedCategory(category) - // console.log(gateGory) - } - } - + console.log(category) + }} className={`px-6 py-2 text-base cursor-pointer rounded-full transition-all duration-300 ${selectedCategory === category ? 'bg-blue-600 text-white shadow-lg' : 'bg-white text-gray-600 hover:bg-gray-100' }`} + > {category} diff --git a/apps/web/src/app/main/home/components/HeroSection.tsx b/apps/web/src/app/main/home/components/HeroSection.tsx index 9dae829..0399c32 100755 --- a/apps/web/src/app/main/home/components/HeroSection.tsx +++ b/apps/web/src/app/main/home/components/HeroSection.tsx @@ -10,6 +10,7 @@ import { EyeOutlined, } from "@ant-design/icons"; import type { CarouselRef } from "antd/es/carousel"; +import { useAppConfig } from "@nice/client"; const { Title, Text } = Typography; @@ -44,16 +45,16 @@ const carouselItems: CarouselItem[] = [ }, ]; -const platformStats: PlatformStat[] = [ - { icon: , value: "50,000+", label: "注册学员" }, - { icon: , value: "1,000+", label: "精品课程" }, - // { icon: , value: '98%', label: '好评度' }, - { icon: , value: "100万+", label: "观看次数" }, -]; - const HeroSection = () => { const carouselRef = useRef(null); - + const { statistics, baseSetting } = useAppConfig(); + + const platformStats: PlatformStat[] = [ + { icon: , value: "50,000+", label: "注册学员" }, + { icon: , value: "1,000+", label: "精品课程" }, + // { icon: , value: '98%', label: '好评度' }, + { icon: , value: "4552", label: "观看次数" }, + ]; const handlePrev = useCallback(() => { carouselRef.current?.prev(); }, []); @@ -61,7 +62,7 @@ const HeroSection = () => { const handleNext = useCallback(() => { carouselRef.current?.next(); }, []); - + //const {slides:carouselItems} = useAppConfig() return (
@@ -73,24 +74,29 @@ const HeroSection = () => { dots={{ className: "carousel-dots !bottom-32 !z-20", }}> - {carouselItems.map((item, index) => ( -
-
-
-
+ {Array.isArray(carouselItems) ? ( + carouselItems.map((item, index) => ( +
+
+
+
- {/* Content Container */} -
-
- ))} + {/* Content Container */} +
+
+ )) + ) : ( +
+ )} {/* Navigation Buttons */} diff --git a/apps/web/src/app/main/layout/MainFooter.tsx b/apps/web/src/app/main/layout/MainFooter.tsx index 1356335..e37d149 100755 --- a/apps/web/src/app/main/layout/MainFooter.tsx +++ b/apps/web/src/app/main/layout/MainFooter.tsx @@ -8,7 +8,7 @@ export function MainFooter() { {/* 开发组织信息 */}

- 创新高地 软件小组 + 软件与数据小组

提供技术支持 diff --git a/apps/web/src/app/main/layout/MainHeader.tsx b/apps/web/src/app/main/layout/MainHeader.tsx index c8e0d2a..76a15b4 100755 --- a/apps/web/src/app/main/layout/MainHeader.tsx +++ b/apps/web/src/app/main/layout/MainHeader.tsx @@ -3,7 +3,7 @@ import { Input, Layout, Avatar, Button, Dropdown } from "antd"; import { EditFilled, SearchOutlined, UserOutlined } from "@ant-design/icons"; import { useAuth } from "@web/src/providers/auth-provider"; import { useNavigate } from "react-router-dom"; -import { UserMenu } from "./UserMenu"; +import { UserMenu } from "./UserMenu/UserMenu"; import { NavigationMenu } from "./NavigationMenu"; const { Header } = Layout; @@ -35,10 +35,12 @@ export function MainHeader() { className="w-72 rounded-full" value={searchValue} onChange={(e) => setSearchValue(e.target.value)} - onPressEnter={(e)=>{ + onPressEnter={(e) => { //console.log(e) - setSearchValue('') - navigate(`/courses/?searchValue=${searchValue}`) + setSearchValue(""); + navigate( + `/courses/?searchValue=${searchValue}` + ); window.scrollTo({ top: 0, behavior: "smooth" }); }} /> @@ -54,18 +56,7 @@ export function MainHeader() { )} {isAuthenticated ? ( - } - trigger={["click"]} - placement="bottomRight"> - - {(user?.showname || - user?.username || - "")[0]?.toUpperCase()} - - + ) : ( + ))} +

+ + )} + +
+ + + ); +} diff --git a/apps/web/src/app/main/layout/UserMenu/types.ts b/apps/web/src/app/main/layout/UserMenu/types.ts new file mode 100644 index 0000000..dfe4b00 --- /dev/null +++ b/apps/web/src/app/main/layout/UserMenu/types.ts @@ -0,0 +1,6 @@ +import React, { ReactNode } from "react"; +export interface MenuItemType { + icon: ReactNode; + label: string; + action: () => void; +} diff --git a/apps/web/src/components/common/uploader/TusUploader.tsx b/apps/web/src/components/common/uploader/TusUploader.tsx index e9fa45b..1811135 100755 --- a/apps/web/src/components/common/uploader/TusUploader.tsx +++ b/apps/web/src/components/common/uploader/TusUploader.tsx @@ -11,6 +11,7 @@ export interface TusUploaderProps { value?: string[]; onChange?: (value: string[]) => void; multiple?: boolean; + allowTypes?: string[]; } interface UploadingFile { @@ -25,8 +26,8 @@ export const TusUploader = ({ value = [], onChange, multiple = true, + allowTypes = undefined, }: TusUploaderProps) => { - const { handleFileUpload, uploadProgress } = useTusUpload(); const [uploadingFiles, setUploadingFiles] = useState([]); const [completedFiles, setCompletedFiles] = useState( @@ -61,7 +62,10 @@ export const TusUploader = ({ const handleBeforeUpload = useCallback( (file: File) => { - + if (allowTypes && !allowTypes.includes(file.type)) { + toast.error(`文件类型 ${file.type} 不在允许范围内`); + return Upload.LIST_IGNORE; // 使用 antd 的官方阻止方式 + } const fileKey = `${file.name}-${Date.now()}`; setUploadingFiles((prev) => [ @@ -136,10 +140,10 @@ export const TusUploader = ({ return (

@@ -149,6 +153,11 @@ export const TusUploader = ({

{multiple ? "支持单个或批量上传文件" : "仅支持上传单个文件"} + {allowTypes && ( + + 允许类型: {allowTypes.join(", ")} + + )}

@@ -165,10 +174,10 @@ export const TusUploader = ({ file.status === "done" ? 100 : Math.round( - uploadProgress?.[ - file.fileKey! - ] || 0 - ) + uploadProgress?.[ + file.fileKey! + ] || 0 + ) } status={ file.status === "error" diff --git a/apps/web/src/components/models/course/detail/CourseDetailContext.tsx b/apps/web/src/components/models/course/detail/CourseDetailContext.tsx index 890a331..8702d19 100755 --- a/apps/web/src/components/models/course/detail/CourseDetailContext.tsx +++ b/apps/web/src/components/models/course/detail/CourseDetailContext.tsx @@ -58,6 +58,7 @@ export function CourseDetailProvider({ ); useEffect(() => { if (course) { + console.log("read"); read.mutateAsync({ data: { visitorId: user?.id || null, diff --git a/apps/web/src/components/models/course/detail/CourseDetailDescription.tsx b/apps/web/src/components/models/course/detail/CourseDetailDescription.tsx index e33e445..feba41d 100755 --- a/apps/web/src/components/models/course/detail/CourseDetailDescription.tsx +++ b/apps/web/src/components/models/course/detail/CourseDetailDescription.tsx @@ -47,7 +47,7 @@ export const CourseDetailDescription: React.FC = () => {
{course?.subTitle}
-
{course?.meta?.views}
+
{course?.meta?.views || 0}
diff --git a/apps/web/src/components/models/course/detail/CourseDetailHeader/CourseDetailHeader.tsx b/apps/web/src/components/models/course/detail/CourseDetailHeader/CourseDetailHeader.tsx index 71ec0e0..7e708e1 100755 --- a/apps/web/src/components/models/course/detail/CourseDetailHeader/CourseDetailHeader.tsx +++ b/apps/web/src/components/models/course/detail/CourseDetailHeader/CourseDetailHeader.tsx @@ -8,7 +8,7 @@ import { } from "@ant-design/icons"; import { useAuth } from "@web/src/providers/auth-provider"; import { useNavigate } from "react-router-dom"; -import { UserMenu } from "@web/src/app/main/layout/UserMenu"; +import { UserMenu } from "@web/src/app/main/layout/UserMenu/UserMenu"; import { CourseDetailContext } from "../CourseDetailContext"; const { Header } = Layout; @@ -21,7 +21,7 @@ export function CourseDetailHeader() { return (
-
+
{ diff --git a/apps/web/src/components/models/course/detail/CourseDetailSkeleton.tsx b/apps/web/src/components/models/course/detail/CourseDetailSkeleton.tsx index 95b1049..ad58cb8 100755 --- a/apps/web/src/components/models/course/detail/CourseDetailSkeleton.tsx +++ b/apps/web/src/components/models/course/detail/CourseDetailSkeleton.tsx @@ -2,6 +2,7 @@ import { SkeletonItem, SkeletonSection, } from "@web/src/components/presentation/Skeleton"; +import { api } from "packages/client/dist"; export const CourseDetailSkeleton = () => { return ( diff --git a/apps/web/src/components/models/course/editor/form/CourseContentForm/SortableLecture.tsx b/apps/web/src/components/models/course/editor/form/CourseContentForm/SortableLecture.tsx index 7a23629..d421ba2 100755 --- a/apps/web/src/components/models/course/editor/form/CourseContentForm/SortableLecture.tsx +++ b/apps/web/src/components/models/course/editor/form/CourseContentForm/SortableLecture.tsx @@ -10,7 +10,13 @@ import { useSortable } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; import QuillEditor from "@web/src/components/common/editor/quill/QuillEditor"; 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 toast from "react-hot-toast"; @@ -134,7 +140,9 @@ export const SortableLecture: React.FC = ({ name="title" initialValue={field?.title} className="mb-0 flex-1" - rules={[{ required: true }]}> + rules={[ + { required: true, message: "请输入课时标题" }, + ]}> = ({ - + rules={[ + { + required: true, + message: "请传入视频", + }, + ]}> + ) : ( + rules={[ + { required: true, message: "请输入内容" }, + ]}> )} diff --git a/apps/web/src/components/models/staff/staff-form.tsx b/apps/web/src/components/models/staff/staff-form.tsx index 1dc4920..513f08c 100755 --- a/apps/web/src/components/models/staff/staff-form.tsx +++ b/apps/web/src/components/models/staff/staff-form.tsx @@ -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() {
)} +
+ + + {canManageAnyStaff && ( - @@ -136,7 +147,8 @@ export default function StaffForm() { rules={[{ required: true }]} name={"showname"} label="姓名"> - @@ -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="手机号"> - + - + - {editId && - - } + {editId && ( + + + + )}
); diff --git a/apps/web/src/hooks/useTusUpload.ts b/apps/web/src/hooks/useTusUpload.ts index a4589a5..3f6d038 100755 --- a/apps/web/src/hooks/useTusUpload.ts +++ b/apps/web/src/hooks/useTusUpload.ts @@ -15,7 +15,7 @@ export function useTusUpload() { >({}); const [isUploading, setIsUploading] = useState(false); const [uploadError, setUploadError] = useState(null); - + const getFileId = (url: string) => { const parts = url.split("/"); const uploadIndex = parts.findIndex((part) => part === "upload"); diff --git a/docker-compose.example.yml b/docker-compose.example.yml index ee99507..ab857dc 100755 --- a/docker-compose.example.yml +++ b/docker-compose.example.yml @@ -1,135 +1,136 @@ version: "3.8" services: - db: - image: postgres:latest - ports: - - "5432:5432" - environment: - - POSTGRES_DB=app - - POSTGRES_USER=root - - POSTGRES_PASSWORD=Letusdoit000 - volumes: - - ./volumes/postgres:/var/lib/postgresql/data - # minio: - # image: minio/minio - # ports: - # - "9000:9000" - # - "9001:9001" - # volumes: - # - ./volumes/minio:/minio_data - # environment: - # - MINIO_ACCESS_KEY=minioadmin - # - MINIO_SECRET_KEY=minioadmin - # command: minio server /minio_data --console-address ":9001" -address ":9000" - # healthcheck: - # test: - # [ - # "CMD", - # "curl", - # "-f", - # "http://192.168.2.1:9001/minio/health/live" - # ] - # interval: 30s - # timeout: 20s - # retries: 3 - pgadmin: - image: dpage/pgadmin4 - ports: - - "8082:80" - environment: - - PGADMIN_DEFAULT_EMAIL=insiinc@outlook.com - - PGADMIN_DEFAULT_PASSWORD=Letusdoit000 - # tusd: - # image: tusproject/tusd - # ports: - # - "8080:8080" - # environment: - # - AWS_REGION=cn-north-1 - # - AWS_ACCESS_KEY_ID=minioadmin - # - AWS_SECRET_ACCESS_KEY=minioadmin - # command: -verbose -s3-bucket app -s3-endpoint http://minio:9000 -hooks-http http://host.docker.internal:3000/upload/hook - # volumes: - # - ./volumes/tusd:/data - # extra_hosts: - # - "host.docker.internal:host-gateway" - # depends_on: - # - minio - # tusd: - # image: tusproject/tusd - # ports: - # - "8080:8080" - # command: -verbose -upload-dir /data -hooks-http http://host.docker.internal:3000/upload/hook - # volumes: - # - ./uploads:/data - # extra_hosts: - # - "host.docker.internal:host-gateway" - nginx: - image: nice-nginx:latest - ports: - - "80:80" - volumes: - - ./config/nginx/conf.d:/etc/nginx/conf.d - - ./config/nginx/nginx.conf:/etc/nginx/nginx.conf - - ./uploads:/data/uploads # tusd 上传目录 - - ./web-dist:/usr/share/nginx/html # 添加前端构建文件的挂载 - - ./config/nginx/entrypoint.sh:/docker-entrypoint.sh - environment: - - SERVER_IP=host.docker.internal - - SERVER_PORT=3000 - entrypoint: ["/docker-entrypoint.sh"] - extra_hosts: - - "host.docker.internal:host-gateway" - redis: - image: redis:latest - ports: - - "6379:6379" - volumes: - - ./config/redis.conf:/usr/local/etc/redis/redis.conf - - ./volumes/redis:/data - command: ["redis-server", "/usr/local/etc/redis/redis.conf"] - # restic: - # image: restic/restic:latest - # environment: - # - RESTIC_REPOSITORY=/backup - # - RESTIC_PASSWORD=Letusdoit000 - # volumes: - # - ./volumes/postgres:/data - # - ./volumes/restic-cache:/root/.cache/restic - # - ./backup:/backup # 本地目录挂载到容器内的 /backup - # - ./config/backup.sh:/usr/local/bin/backup.sh # Mount your script inside the container - # entrypoint: /usr/local/bin/backup.sh - # depends_on: - # - db - # web: - # image: td-web:latest - # ports: - # - "80:80" - # environment: - # - VITE_APP_SERVER_IP=192.168.79.77 - # - VITE_APP_VERSION=0.3.0 - # - VITE_APP_APP_NAME=两道防线管理后台 - # server: - # image: td-server:latest - # ports: - # - "3000:3000" - # - "3001:3001" - # environment: - # - DATABASE_URL=postgresql://root:Letusdoit000@db:5432/app?schema=public - # - REDIS_HOST=redis - # - REDIS_PORT=6379 - # - REDIS_PASSWORD=Letusdoit000 - # - TUS_URL=http://192.168.2.1:8080 - # - JWT_SECRET=/yT9MnLm/r6NY7ee2Fby6ihCHZl+nFx4OQFKupivrhA= - # - PUSH_URL=http://dns:9092 - # - PUSH_APPID=123 - # - PUSH_APPSECRET=123 - # - MINIO_HOST=minio - # - ADMIN_PHONE_NUMBER=13258117304 - # - DEADLINE_CRON=0 0 8 * * * - # depends_on: - # - db - # - redis + db: + image: postgres:latest + ports: + - "5432:5432" + environment: + - POSTGRES_DB=app + - POSTGRES_USER=root + - POSTGRES_PASSWORD=Letusdoit000 + volumes: + - ./volumes/postgres:/var/lib/postgresql/data + # minio: + # image: minio/minio + # ports: + # - "9000:9000" + # - "9001:9001" + # volumes: + # - ./volumes/minio:/minio_data + # environment: + # - MINIO_ACCESS_KEY=minioadmin + # - MINIO_SECRET_KEY=minioadmin + # command: minio server /minio_data --console-address ":9001" -address ":9000" + # healthcheck: + # test: + # [ + # "CMD", + # "curl", + # "-f", + # "http://192.168.2.1:9001/minio/health/live" + # ] + # interval: 30s + # timeout: 20s + # retries: 3 + pgadmin: + image: dpage/pgadmin4 + ports: + - "8082:80" + environment: + - PGADMIN_DEFAULT_EMAIL=insiinc@outlook.com + - PGADMIN_DEFAULT_PASSWORD=Letusdoit000 + # tusd: + # image: tusproject/tusd + # ports: + # - "8080:8080" + # environment: + # - AWS_REGION=cn-north-1 + # - AWS_ACCESS_KEY_ID=minioadmin + # - AWS_SECRET_ACCESS_KEY=minioadmin + # command: -verbose -s3-bucket app -s3-endpoint http://minio:9000 -hooks-http http://host.docker.internal:3000/upload/hook + # volumes: + # - ./volumes/tusd:/data + # extra_hosts: + # - "host.docker.internal:host-gateway" + # depends_on: + # - minio + # tusd: + # image: tusproject/tusd + # ports: + # - "8080:8080" + # command: -verbose -upload-dir /data -hooks-http http://host.docker.internal:3000/upload/hook + # volumes: + # - ./uploads:/data + # extra_hosts: + # - "host.docker.internal:host-gateway" + nginx: + image: nice-nginx:2.0 + ports: + - "80:80" + volumes: + - ./config/nginx/conf.d:/etc/nginx/conf.d + - ./config/nginx/nginx.conf:/etc/nginx/nginx.conf + - ./uploads:/data/uploads # tusd 上传目录 + - ./web-dist:/usr/share/nginx/html # 添加前端构建文件的挂载 + - ./config/nginx/entrypoint.sh:/docker-entrypoint.sh + environment: + - SERVER_IP=host.docker.internal + - SERVER_PORT=3000 + entrypoint: ["/docker-entrypoint.sh"] + extra_hosts: + - "host.docker.internal:host-gateway" + + redis: + image: redis:latest + ports: + - "6379:6379" + volumes: + - ./config/redis.conf:/usr/local/etc/redis/redis.conf + - ./volumes/redis:/data + command: ["redis-server", "/usr/local/etc/redis/redis.conf"] + # restic: + # image: restic/restic:latest + # environment: + # - RESTIC_REPOSITORY=/backup + # - RESTIC_PASSWORD=Letusdoit000 + # volumes: + # - ./volumes/postgres:/data + # - ./volumes/restic-cache:/root/.cache/restic + # - ./backup:/backup # 本地目录挂载到容器内的 /backup + # - ./config/backup.sh:/usr/local/bin/backup.sh # Mount your script inside the container + # entrypoint: /usr/local/bin/backup.sh + # depends_on: + # - db + # web: + # image: td-web:latest + # ports: + # - "80:80" + # environment: + # - VITE_APP_SERVER_IP=192.168.79.77 + # - VITE_APP_VERSION=0.3.0 + # - VITE_APP_APP_NAME=两道防线管理后台 + # server: + # image: td-server:latest + # ports: + # - "3000:3000" + # - "3001:3001" + # environment: + # - DATABASE_URL=postgresql://root:Letusdoit000@db:5432/app?schema=public + # - REDIS_HOST=redis + # - REDIS_PORT=6379 + # - REDIS_PASSWORD=Letusdoit000 + # - TUS_URL=http://192.168.2.1:8080 + # - JWT_SECRET=/yT9MnLm/r6NY7ee2Fby6ihCHZl+nFx4OQFKupivrhA= + # - PUSH_URL=http://dns:9092 + # - PUSH_APPID=123 + # - PUSH_APPSECRET=123 + # - MINIO_HOST=minio + # - ADMIN_PHONE_NUMBER=13258117304 + # - DEADLINE_CRON=0 0 8 * * * + # depends_on: + # - db + # - redis networks: - default: - name: remooc + default: + name: remooc diff --git a/packages/client/src/api/hooks/useAppConfig.ts b/packages/client/src/api/hooks/useAppConfig.ts index 0b00ffd..f38b6de 100755 --- a/packages/client/src/api/hooks/useAppConfig.ts +++ b/packages/client/src/api/hooks/useAppConfig.ts @@ -3,15 +3,16 @@ import { AppConfigSlug, BaseSetting } from "@nice/common"; import { useCallback, useEffect, useMemo, useState } from "react"; export function useAppConfig() { - const utils = api.useUtils() + const utils = api.useUtils(); const [baseSetting, setBaseSetting] = useState(); const { data, isLoading }: { data: any; isLoading: boolean } = api.app_config.findFirst.useQuery({ - where: { slug: AppConfigSlug.BASE_SETTING } + where: { slug: AppConfigSlug.BASE_SETTING }, }); + const handleMutationSuccess = useCallback(() => { - utils.app_config.invalidate() + utils.app_config.invalidate(); }, [utils]); // Use the generic success handler in mutations @@ -26,9 +27,9 @@ 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(() => { return baseSetting?.appConfig?.splashScreen; @@ -36,8 +37,20 @@ export function useAppConfig() { const devDept = useMemo(() => { return baseSetting?.appConfig?.devDept; }, [baseSetting]); + 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, update, @@ -45,5 +58,7 @@ export function useAppConfig() { splashScreen, devDept, isLoading, + slides, + statistics, }; } diff --git a/packages/common/src/constants.ts b/packages/common/src/constants.ts index 7e9174f..9a36b03 100755 --- a/packages/common/src/constants.ts +++ b/packages/common/src/constants.ts @@ -81,3 +81,17 @@ export const InitAppConfigs: Prisma.AppConfigCreateInput[] = [ 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 +]; diff --git a/packages/common/src/models/select.ts b/packages/common/src/models/select.ts index 36e072d..a591134 100755 --- a/packages/common/src/models/select.ts +++ b/packages/common/src/models/select.ts @@ -70,21 +70,22 @@ export const courseDetailSelect: Prisma.PostSelect = { title: true, subTitle: true, content: true, + depts: true, // isFeatured: true, createdAt: true, updatedAt: true, // 关联表选择 - terms:{ - select:{ - id:true, - name:true, - taxonomy:{ - select:{ - id:true, - slug:true - } - } - } + terms: { + select: { + id: true, + name: true, + taxonomy: { + select: { + id: true, + slug: true, + }, + }, + }, }, enrollments: { select: { diff --git a/packages/common/src/models/staff.ts b/packages/common/src/models/staff.ts index a6ace02..565f847 100755 --- a/packages/common/src/models/staff.ts +++ b/packages/common/src/models/staff.ts @@ -2,37 +2,37 @@ import { Staff, Department } from "@prisma/client"; import { RolePerms } from "../enum"; export type StaffRowModel = { - avatar: string; - dept_name: string; - officer_id: string; - phone_number: string; - showname: string; - username: string; + avatar: string; + dept_name: string; + officer_id: string; + phone_number: string; + showname: string; + username: string; }; export type UserProfile = Staff & { - permissions: RolePerms[]; - deptIds: string[]; - parentDeptIds: string[]; - domain: Department; - department: Department; + permissions: RolePerms[]; + deptIds: string[]; + parentDeptIds: string[]; + domain: Department; + department: Department; }; export type StaffDto = Staff & { - domain?: Department; - department?: Department; + domain?: Department; + department?: Department; }; export interface AuthDto { - token: string; - staff: StaffDto; - refreshToken: string; - perms: string[]; + token: string; + staff: StaffDto; + refreshToken: string; + perms: string[]; } export interface JwtPayload { - sub: string; - username: string; + sub: string; + username: string; } export interface TokenPayload { - id: string; - phoneNumber: string; - name: string; -} \ No newline at end of file + id: string; + phoneNumber: string; + name: string; +} diff --git a/packages/common/src/types.ts b/packages/common/src/types.ts index 084316a..0207fb7 100755 --- a/packages/common/src/types.ts +++ b/packages/common/src/types.ts @@ -43,6 +43,13 @@ export interface BaseSetting { appConfig?: { splashScreen?: string; devDept?: string; + slides?: []; + statistics?: { + reads?: number; + courses?: number; + lectures?: number; + staffs?: number; + }; }; } export type RowModelResult = {