add
This commit is contained in:
parent
8293f6389d
commit
7723891358
|
|
@ -24,10 +24,12 @@ export class ResourceRouter {
|
||||||
router = this.trpc.router({
|
router = this.trpc.router({
|
||||||
create: this.trpc.protectProcedure
|
create: this.trpc.protectProcedure
|
||||||
.input(ResourceCreateArgsSchema)
|
.input(ResourceCreateArgsSchema)
|
||||||
|
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const { staff } = ctx;
|
const { staff } = ctx;
|
||||||
return await this.resourceService.create(input, { staff });
|
return await this.resourceService.create(input, { staff });
|
||||||
}),
|
}),
|
||||||
|
|
||||||
createMany: this.trpc.protectProcedure
|
createMany: this.trpc.protectProcedure
|
||||||
.input(z.array(ResourceCreateManyInputSchema))
|
.input(z.array(ResourceCreateManyInputSchema))
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
|
@ -35,7 +37,7 @@ export class ResourceRouter {
|
||||||
|
|
||||||
return await this.resourceService.createMany({ data: input }, staff);
|
return await this.resourceService.createMany({ data: input }, staff);
|
||||||
}),
|
}),
|
||||||
deleteMany: this.trpc.procedure
|
deleteMany: this.trpc.protectProcedure
|
||||||
.input(ResourceDeleteManyArgsSchema)
|
.input(ResourceDeleteManyArgsSchema)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
return await this.resourceService.deleteMany(input);
|
return await this.resourceService.deleteMany(input);
|
||||||
|
|
@ -46,9 +48,12 @@ export class ResourceRouter {
|
||||||
return await this.resourceService.findFirst(input);
|
return await this.resourceService.findFirst(input);
|
||||||
}),
|
}),
|
||||||
softDeleteByIds: this.trpc.protectProcedure
|
softDeleteByIds: this.trpc.protectProcedure
|
||||||
.input(z.object({ ids: z.array(z.string()) })) // expect input according to the schema
|
.input(z.object({
|
||||||
.mutation(async ({ input }) => {
|
ids: z.array(z.string())
|
||||||
return this.resourceService.softDeleteByIds(input.ids);
|
}))
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
const result = await this.resourceService.softDeleteByIds(input.ids);
|
||||||
|
return result;
|
||||||
}),
|
}),
|
||||||
updateOrder: this.trpc.protectProcedure
|
updateOrder: this.trpc.protectProcedure
|
||||||
.input(UpdateOrderSchema)
|
.input(UpdateOrderSchema)
|
||||||
|
|
@ -60,7 +65,7 @@ export class ResourceRouter {
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input }) => {
|
||||||
return await this.resourceService.findMany(input);
|
return await this.resourceService.findMany(input);
|
||||||
}),
|
}),
|
||||||
findManyWithCursor: this.trpc.protectProcedure
|
findManyWithCursor: this.trpc.procedure
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
cursor: z.any().nullish(),
|
cursor: z.any().nullish(),
|
||||||
|
|
@ -73,5 +78,24 @@ export class ResourceRouter {
|
||||||
const { staff } = ctx;
|
const { staff } = ctx;
|
||||||
return await this.resourceService.findManyWithCursor(input);
|
return await this.resourceService.findManyWithCursor(input);
|
||||||
}),
|
}),
|
||||||
|
count: this.trpc.procedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
where: z.object({
|
||||||
|
AND: z.object({
|
||||||
|
title: z.object({
|
||||||
|
not: z.null()
|
||||||
|
}),
|
||||||
|
description: z.object({
|
||||||
|
not: z.null()
|
||||||
|
})
|
||||||
|
}).optional(),
|
||||||
|
deletedAt: z.date().nullable().optional(),
|
||||||
|
}).optional(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.query(async ({ input }) => {
|
||||||
|
return await this.resourceService.count(input);
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,22 +6,69 @@ import {
|
||||||
ObjectType,
|
ObjectType,
|
||||||
Prisma,
|
Prisma,
|
||||||
Resource,
|
Resource,
|
||||||
ResourceStatus,
|
PrismaClient,
|
||||||
} from '@nice/common';
|
} from '@nice/common';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ResourceService extends BaseService<Prisma.ResourceDelegate> {
|
export class ResourceService extends BaseService<Prisma.ResourceDelegate> {
|
||||||
|
protected db: PrismaClient;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super(db, ObjectType.RESOURCE);
|
super(db, ObjectType.RESOURCE);
|
||||||
|
this.db = db;
|
||||||
}
|
}
|
||||||
async create(
|
async create(
|
||||||
args: Prisma.ResourceCreateArgs,
|
args: Prisma.ResourceCreateArgs,
|
||||||
params?: { staff?: UserProfile },
|
params?: { staff?: UserProfile },
|
||||||
): Promise<Resource> {
|
): Promise<Resource> {
|
||||||
if (params?.staff) {
|
try {
|
||||||
args.data.ownerId = params?.staff?.id;
|
// 检查文件是否已存在
|
||||||
|
if (args.data.fileId) {
|
||||||
|
const existingResource = await this.db.resource.findUnique({
|
||||||
|
where: {
|
||||||
|
fileId: args.data.fileId,
|
||||||
|
deletedAt: null, // 只检查未删除的资源
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 如果文件已存在但已被"删除",可以更新而不是创建
|
||||||
|
if (existingResource && existingResource.deletedAt) {
|
||||||
|
return this.db.resource.update({
|
||||||
|
where: { id: existingResource.id },
|
||||||
|
data: {
|
||||||
|
...args.data,
|
||||||
|
deletedAt: null,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果文件已存在且未删除,可以返回现有记录或抛出更友好的错误
|
||||||
|
if (existingResource) {
|
||||||
|
return existingResource; // 或者抛出自定义错误
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置所有者
|
||||||
|
if (params?.staff) {
|
||||||
|
args.data.ownerId = params.staff.id;
|
||||||
|
}
|
||||||
|
// 确保将 description 写入数据库
|
||||||
|
// if (args.data.description) {
|
||||||
|
// args.data.description = args.data.description.trim();
|
||||||
|
// }
|
||||||
|
// 设置其他必要字段
|
||||||
|
args.data = {
|
||||||
|
...args.data,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
};
|
||||||
|
console.log('创建资源参数:', args.data); // 调试日志
|
||||||
|
return super.create(args);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('创建资源失败:', error);
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
return super.create(args);
|
|
||||||
}
|
}
|
||||||
async softDeleteByFileId(fileId: string) {
|
async softDeleteByFileId(fileId: string) {
|
||||||
return this.update({
|
return this.update({
|
||||||
|
|
@ -33,4 +80,26 @@ export class ResourceService extends BaseService<Prisma.ResourceDelegate> {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
async softDeleteMany(ids: string[]) {
|
||||||
|
try {
|
||||||
|
console.log('softDeleteByIds called with ids:', ids);
|
||||||
|
const result = await this.db.resource.updateMany({
|
||||||
|
where: {
|
||||||
|
id: {
|
||||||
|
in: ids,
|
||||||
|
},
|
||||||
|
deletedAt: null,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
deletedAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
console.log('softDeleteByIds result:', result);
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Service delete error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import AdminHeader from "@web/src/components/layout/admin/AdminHeader";
|
||||||
import AvatarUploader from "@web/src/components/common/uploader/AvatarUploader";
|
import AvatarUploader from "@web/src/components/common/uploader/AvatarUploader";
|
||||||
import MultiAvatarUploader from "@web/src/components/common/uploader/MultiAvatarUploader";
|
import MultiAvatarUploader from "@web/src/components/common/uploader/MultiAvatarUploader";
|
||||||
import CarouselUrlInput from "@web/src/components/common/uploader/CarouselUrlInput";
|
import CarouselUrlInput from "@web/src/components/common/uploader/CarouselUrlInput";
|
||||||
|
import { TusUploader } from "@web/src/components/common/uploader/TusUploader";
|
||||||
|
|
||||||
export default function BaseSettingPage() {
|
export default function BaseSettingPage() {
|
||||||
const { update, baseSetting } = useAppConfig();
|
const { update, baseSetting } = useAppConfig();
|
||||||
|
|
|
||||||
|
|
@ -40,11 +40,11 @@ const AuthPage: React.FC = () => {
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ delay: 0.2, duration: 0.5 }}>
|
transition={{ delay: 0.2, duration: 0.5 }}>
|
||||||
<div className="text-4xl text-white mb-4 font-serif">
|
<div className="text-4xl text-white mb-4 font-serif">
|
||||||
{env.APP_NAME || "信箱"}
|
{env.APP_NAME || "心灵树洞"}
|
||||||
</div>
|
</div>
|
||||||
<Paragraph className="text-lg mb-8 text-blue-100 leading-relaxed text-justify">
|
<Paragraph className="text-lg mb-8 text-blue-100 leading-relaxed text-justify">
|
||||||
聆音于微 润心以答 <br></br>
|
聆听细语 润泽心灵 <br></br>
|
||||||
纾难化雨 解忧惟勤
|
勤勉解困 化雨无声
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
{showLogin && (
|
{showLogin && (
|
||||||
<Button
|
<Button
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
export function CourseContent() {
|
||||||
|
return (
|
||||||
|
<div className="min-h-[400px]">
|
||||||
|
<h2 className="text-xl font-bold mb-4">心理小课件</h2>
|
||||||
|
{/* 这里放课件列表内容 */}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
export function MoreContent() {
|
||||||
|
return (
|
||||||
|
<div className="min-h-[400px]">
|
||||||
|
<h2 className="text-xl font-bold mb-4">更多</h2>
|
||||||
|
{/* 这里放视频列表内容 */}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,273 @@
|
||||||
|
import React, { useState, useMemo } from 'react';
|
||||||
|
import { Tabs, Button, message, Image, Row, Col, Modal, Input } from 'antd';
|
||||||
|
import { TusUploader } from "@web/src/components/common/uploader/TusUploader";
|
||||||
|
import { SendOutlined, DeleteOutlined } from "@ant-design/icons";
|
||||||
|
import { api } from "@nice/client";
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { env } from "@web/src/env";
|
||||||
|
import { getFileIcon } from "@web/src/components/models/post/detail/utils";
|
||||||
|
import { formatFileSize, getCompressedImageUrl } from "@nice/utils";
|
||||||
|
import { ResourceDto } from 'packages/common/dist';
|
||||||
|
const { TabPane } = Tabs;
|
||||||
|
|
||||||
|
export function MusicContent() {
|
||||||
|
const [fileIds, setFileIds] = useState<string[]>([]);
|
||||||
|
const [uploaderKey, setUploaderKey] = useState<number>(0);
|
||||||
|
// const [description, setDescription] = useState<string>('');
|
||||||
|
// 获取资源列表
|
||||||
|
const { data: resources,refetch } :{data:ResourceDto[],refetch:()=>void} = api.resource.findMany.useQuery({
|
||||||
|
where: {
|
||||||
|
deletedAt: null,
|
||||||
|
postId: null,
|
||||||
|
},
|
||||||
|
orderBy: {
|
||||||
|
createdAt: 'desc'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// 处理资源数据
|
||||||
|
const { imageResources, fileResources } = useMemo(() => {
|
||||||
|
if (!resources) return { imageResources: [], fileResources: [] };
|
||||||
|
const isImage = (url: string) => /\.(png|jpg|jpeg|gif|webp)$/i.test(url);
|
||||||
|
|
||||||
|
const processedResources = resources.map(resource => {
|
||||||
|
if (!resource?.url) return null;
|
||||||
|
const original = `http://${env.SERVER_IP}:${env.UPLOAD_PORT}/uploads/${resource.url}`;
|
||||||
|
const isImg = isImage(resource.url);
|
||||||
|
return {
|
||||||
|
...resource,
|
||||||
|
url: isImg ? getCompressedImageUrl(original) : original,
|
||||||
|
originalUrl: original,
|
||||||
|
isImage: isImg,
|
||||||
|
};
|
||||||
|
}).filter(Boolean);
|
||||||
|
|
||||||
|
return {
|
||||||
|
imageResources: processedResources.filter(res => res.isImage),
|
||||||
|
fileResources: processedResources.filter(res => !res.isImage)
|
||||||
|
};
|
||||||
|
}, [resources]);
|
||||||
|
|
||||||
|
const createMutation = api.resource.create.useMutation({});
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!fileIds.length) {
|
||||||
|
message.error("请先上传文件");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if (!description.trim()) {
|
||||||
|
// message.error("请输入文件描述");
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 逐个上传文件,而不是使用 Promise.all
|
||||||
|
for (const fileId of fileIds) {
|
||||||
|
try {
|
||||||
|
await createMutation.mutateAsync({
|
||||||
|
data: {
|
||||||
|
fileId,
|
||||||
|
// description: description.trim(),
|
||||||
|
isPublic: true,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error && error.message.includes('Unique constraint failed')) {
|
||||||
|
console.warn(`文件 ${fileId} 已存在,跳过`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
message.success("上传成功!");
|
||||||
|
setFileIds([]);
|
||||||
|
// setDescription('');
|
||||||
|
setUploaderKey(prev => prev + 1);
|
||||||
|
refetch(); // 刷新列表
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error uploading:", error);
|
||||||
|
message.error("上传失败,请稍后重试");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// 删除资源
|
||||||
|
const deleteMutation = api.resource.softDeleteByIds.useMutation();
|
||||||
|
|
||||||
|
const handleDelete = async (id: string) => {
|
||||||
|
console.log("Delete resource id:", id);
|
||||||
|
try {
|
||||||
|
const confirmed = await new Promise(resolve => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: '确认删除',
|
||||||
|
content: '确定要删除这个文件吗?此操作不可恢复。',
|
||||||
|
okText: '确认',
|
||||||
|
cancelText: '取消',
|
||||||
|
onOk: () => resolve(true),
|
||||||
|
onCancel: () => resolve(false),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!confirmed) return;
|
||||||
|
|
||||||
|
await deleteMutation.mutateAsync({
|
||||||
|
ids: [id]
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Delete error:', error);
|
||||||
|
message.error('删除失败,请重试');
|
||||||
|
}
|
||||||
|
|
||||||
|
refetch();
|
||||||
|
message.success('删除成功');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-6">
|
||||||
|
<header className="rounded-t-xl bg-gradient-to-r from-primary to-primary-400 text-white p-6 relative mb-4">
|
||||||
|
<div className="flex flex-col space-b-1">
|
||||||
|
<h1>锦囊资源上传</h1>
|
||||||
|
<p>支持视频、图片、文档、PPT等多种格式文件</p>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
{/* 上传区域 */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<Tabs defaultActiveKey="1">
|
||||||
|
<TabPane >
|
||||||
|
<div className="relative rounded-xl border border-white hover:ring-1 ring-white transition-all duration-300 ease-in-out bg-slate-100">
|
||||||
|
<TusUploader
|
||||||
|
key={uploaderKey}
|
||||||
|
value={fileIds}
|
||||||
|
onChange={(value) => {
|
||||||
|
console.log('文件IDs:', value);
|
||||||
|
setFileIds(value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</TabPane>
|
||||||
|
{/* <TabPane tab="描述" key="2">
|
||||||
|
<div className="relative rounded-xl border border-white hover:ring-1 ring-white transition-all duration-300 ease-in-out bg-slate-100 p-4">
|
||||||
|
<Input.TextArea
|
||||||
|
value={description}
|
||||||
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
|
placeholder="请输入文件描述信息"
|
||||||
|
autoSize={{ minRows: 3, maxRows: 6 }}
|
||||||
|
className="bg-transparent border-none focus:ring-0"
|
||||||
|
/>
|
||||||
|
<div className="mt-2 text-sm text-gray-500">
|
||||||
|
<p>请添加文件描述,方便后续管理和识别。</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabPane> */}
|
||||||
|
</Tabs>
|
||||||
|
{fileIds.length > 0 && (
|
||||||
|
<div className="flex items-center justify-end gap-2">
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
onClick={handleSubmit}
|
||||||
|
className="flex items-center space-x-2 bg-primary"
|
||||||
|
icon={<SendOutlined />}
|
||||||
|
>
|
||||||
|
上传
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 文件展示区域 */}
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* 图片资源展示 */}
|
||||||
|
{/* {imageResources?.length > 0 && (
|
||||||
|
<div className="rounded-xl border p-4 bg-white">
|
||||||
|
<h3 className="text-lg font-medium mb-4">图片列表</h3>
|
||||||
|
<Row gutter={[16, 16]}>
|
||||||
|
<Image.PreviewGroup>
|
||||||
|
{imageResources.map((resource) => (
|
||||||
|
<Col key={resource.url} xs={12} sm={8} md={6} lg={4}>
|
||||||
|
<div className="relative aspect-square rounded-lg overflow-hidden flex items-center justify-center bg-gray-100">
|
||||||
|
<Image
|
||||||
|
src={resource.url}
|
||||||
|
alt={resource.title}
|
||||||
|
preview={{
|
||||||
|
src: resource.originalUrl,
|
||||||
|
}}
|
||||||
|
className="object-contain w-full h-full"
|
||||||
|
/>
|
||||||
|
<div className="absolute bottom-0 left-0 right-0 p-2 bg-gradient-to-t from-black/60 to-transparent">
|
||||||
|
<div className="flex items-center justify-between text-white">
|
||||||
|
<span className="text-sm truncate">{resource.title}</span>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
className="text-white hover:text-red-500"
|
||||||
|
icon={<DeleteOutlined />}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation(); // 防止触发图片预览
|
||||||
|
handleDelete(resource.id);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
))}
|
||||||
|
</Image.PreviewGroup>
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
)} */}
|
||||||
|
|
||||||
|
{/* 其他文件资源展示 */}
|
||||||
|
{fileResources?.length > 0 && (
|
||||||
|
<div className="rounded-xl border p-4 bg-white">
|
||||||
|
<h3 className="text-lg font-medium mb-4">文件列表</h3>
|
||||||
|
<div className="grid gap-4 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{fileResources.map((resource) => (
|
||||||
|
<div
|
||||||
|
key={resource.url}
|
||||||
|
className="flex items-center p-3 rounded-lg border hover:shadow-md transition-shadow"
|
||||||
|
>
|
||||||
|
<div className="text-primary-600 text-2xl mr-3">
|
||||||
|
{getFileIcon(resource.url)}
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="font-medium truncate">
|
||||||
|
{resource.title || '未命名文件'}
|
||||||
|
</div>
|
||||||
|
{resource.description && (
|
||||||
|
<div className="text-xs text-gray-500 mt-1">
|
||||||
|
描述: {resource.description}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="text-xs text-gray-500 flex items-center gap-2">
|
||||||
|
<span>{dayjs(resource.createdAt).format('YYYY-MM-DD')}</span>
|
||||||
|
<span>{resource.meta?.size && formatFileSize(resource.meta.size)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
onClick={() => window.open(resource.url)}
|
||||||
|
>
|
||||||
|
下载
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
danger
|
||||||
|
icon={<DeleteOutlined />}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation(); // 防止触发下载
|
||||||
|
handleDelete(resource.id);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,88 @@
|
||||||
|
import { Tabs } from 'antd';
|
||||||
|
import {
|
||||||
|
VideoCameraOutlined,
|
||||||
|
FileTextOutlined,
|
||||||
|
CustomerServiceOutlined,
|
||||||
|
ReadOutlined,
|
||||||
|
BookOutlined
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
import { VideoContent } from './VideoContent';
|
||||||
|
import { CourseContent } from './CourseContent';
|
||||||
|
import { MusicContent } from './MusicContent';
|
||||||
|
import { ScienceContent } from './ScienceContent';
|
||||||
|
import { MoreContent } from './MoreContent';
|
||||||
|
import './pt.css';
|
||||||
|
|
||||||
|
export function PsychologyNav() {
|
||||||
|
const items = [
|
||||||
|
// {
|
||||||
|
// key: 'more',
|
||||||
|
// label: (
|
||||||
|
// <span className="flex items-center gap-2 text-gray-700 hover:text-primary transition-colors">
|
||||||
|
// <BookOutlined className="text-lg" />
|
||||||
|
// <span>宣传报道</span>
|
||||||
|
// </span>
|
||||||
|
// ),
|
||||||
|
// children: <VideoContent />
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// key: 'music',
|
||||||
|
// label: (
|
||||||
|
// <span className="flex items-center gap-2 text-gray-700 hover:text-primary transition-colors">
|
||||||
|
// < ReadOutlined className="text-lg" />
|
||||||
|
// <span>常识科普</span>
|
||||||
|
// </span>
|
||||||
|
// ),
|
||||||
|
// children: <VideoContent />
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// key: 'courses',
|
||||||
|
// label: (
|
||||||
|
// <span className="flex items-center gap-2 text-gray-700 hover:text-primary transition-colors">
|
||||||
|
// <VideoCameraOutlined className="text-lg" />
|
||||||
|
// <span>案例分析</span>
|
||||||
|
// </span>
|
||||||
|
// ),
|
||||||
|
// children: <VideoContent />
|
||||||
|
// },
|
||||||
|
{
|
||||||
|
key: 'science',
|
||||||
|
label: (
|
||||||
|
<span className="flex items-center gap-2 text-gray-700 hover:text-primary transition-colors">
|
||||||
|
< FileTextOutlined className="text-lg" />
|
||||||
|
<span>心理课件</span>
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
children: <VideoContent />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'vedio',
|
||||||
|
label: (
|
||||||
|
<span className="flex items-center gap-2 text-gray-700 hover:text-primary transition-colors">
|
||||||
|
<CustomerServiceOutlined className="text-lg" />
|
||||||
|
<span>音视频</span>
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
children: <MusicContent />
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
];
|
||||||
|
return (
|
||||||
|
<div className="w-full from bg-white rounded-lg shadow-md">
|
||||||
|
<Tabs
|
||||||
|
defaultActiveKey="videos"
|
||||||
|
items={items}
|
||||||
|
className="psychology-tabs"
|
||||||
|
tabBarStyle={{
|
||||||
|
margin: 0,
|
||||||
|
padding: '12px 16px 0',
|
||||||
|
borderBottom: '1px solid #f0f0f0'
|
||||||
|
}}
|
||||||
|
tabBarGutter={200}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
export default PsychologyNav;
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
export function ScienceContent() {
|
||||||
|
return (
|
||||||
|
<div className="min-h-[400px]">
|
||||||
|
<h2 className="text-xl font-bold mb-4">科普</h2>
|
||||||
|
{/* 这里放视频列表内容 */}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,343 @@
|
||||||
|
import React, { useState, useMemo } from "react";
|
||||||
|
import {
|
||||||
|
Tabs,
|
||||||
|
Button,
|
||||||
|
message,
|
||||||
|
Image,
|
||||||
|
Row,
|
||||||
|
Col,
|
||||||
|
Modal,
|
||||||
|
Input,
|
||||||
|
Alert,
|
||||||
|
} from "antd";
|
||||||
|
import { TusUploader } from "@web/src/components/common/uploader/TusUploader";
|
||||||
|
import { SendOutlined, DeleteOutlined, LoginOutlined } from "@ant-design/icons";
|
||||||
|
import { api } from "@nice/client";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import { env } from "@web/src/env";
|
||||||
|
import { getFileIcon } from "@web/src/components/models/post/detail/utils";
|
||||||
|
import { formatFileSize, getCompressedImageUrl } from "@nice/utils";
|
||||||
|
import { ResourceDto, RoleName } from "packages/common/dist";
|
||||||
|
import { useAuth } from "@web/src/providers/auth-provider";
|
||||||
|
const { TabPane } = Tabs;
|
||||||
|
|
||||||
|
export function VideoContent() {
|
||||||
|
const { isAuthenticated, user, hasSomePermissions } = useAuth();
|
||||||
|
const [fileIds, setFileIds] = useState<string[]>([]);
|
||||||
|
const [uploaderKey, setUploaderKey] = useState<number>(0);
|
||||||
|
// const [description, setDescription] = useState<string>('');
|
||||||
|
|
||||||
|
// 检查是否为域管理员
|
||||||
|
const isDomainAdmin = useMemo(() => {
|
||||||
|
return hasSomePermissions("MANAGE_DOM_STAFF", "MANAGE_ANY_STAFF"); // 使用权限检查而不是角色名称
|
||||||
|
}, [hasSomePermissions]);
|
||||||
|
|
||||||
|
// 获取资源列表
|
||||||
|
const {
|
||||||
|
data: resources,
|
||||||
|
refetch,
|
||||||
|
}: { data: ResourceDto[]; refetch: () => void } =
|
||||||
|
api.resource.findMany.useQuery({
|
||||||
|
where: {
|
||||||
|
deletedAt: null,
|
||||||
|
postId: null,
|
||||||
|
},
|
||||||
|
orderBy: {
|
||||||
|
createdAt: "desc",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
// 处理资源数据
|
||||||
|
const { imageResources, fileResources } = useMemo(() => {
|
||||||
|
if (!resources) return { imageResources: [], fileResources: [] };
|
||||||
|
const isImage = (url: string) => /\.(png|jpg|jpeg|gif|webp)$/i.test(url);
|
||||||
|
|
||||||
|
const processedResources = resources
|
||||||
|
.map((resource) => {
|
||||||
|
if (!resource?.url) return null;
|
||||||
|
const original = `http://${env.SERVER_IP}:${env.UPLOAD_PORT}/uploads/${resource.url}`;
|
||||||
|
const isImg = isImage(resource.url);
|
||||||
|
return {
|
||||||
|
...resource,
|
||||||
|
url: isImg ? getCompressedImageUrl(original) : original,
|
||||||
|
originalUrl: original,
|
||||||
|
isImage: isImg,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
return {
|
||||||
|
imageResources: processedResources.filter((res) => res.isImage),
|
||||||
|
fileResources: processedResources.filter((res) => !res.isImage),
|
||||||
|
};
|
||||||
|
}, [resources]);
|
||||||
|
|
||||||
|
const createMutation = api.resource.create.useMutation({});
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
message.error("请先登录");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isDomainAdmin) {
|
||||||
|
message.error("只有管理员才能上传文件");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fileIds.length) {
|
||||||
|
message.error("请先上传文件");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 逐个上传文件,而不是使用 Promise.all
|
||||||
|
for (const fileId of fileIds) {
|
||||||
|
try {
|
||||||
|
await createMutation.mutateAsync({
|
||||||
|
data: {
|
||||||
|
fileId,
|
||||||
|
// description: description.trim(),
|
||||||
|
isPublic: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (
|
||||||
|
error instanceof Error &&
|
||||||
|
error.message.includes("Unique constraint failed")
|
||||||
|
) {
|
||||||
|
console.warn(`文件 ${fileId} 已存在,跳过`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
message.success("上传成功!");
|
||||||
|
setFileIds([]);
|
||||||
|
// setDescription('');
|
||||||
|
setUploaderKey((prev) => prev + 1);
|
||||||
|
refetch(); // 刷新列表
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error uploading:", error);
|
||||||
|
message.error("上传失败,请稍后重试");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 删除资源
|
||||||
|
const deleteMutation = api.resource.softDeleteByIds.useMutation();
|
||||||
|
|
||||||
|
const handleDelete = async (id: string) => {
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
message.error("请先登录");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isDomainAdmin) {
|
||||||
|
message.error("只有域管理员才能删除文件");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Delete resource id:", id);
|
||||||
|
try {
|
||||||
|
const confirmed = await new Promise((resolve) => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: "确认删除",
|
||||||
|
content: "确定要删除这个文件吗?此操作不可恢复。",
|
||||||
|
okText: "确认",
|
||||||
|
cancelText: "取消",
|
||||||
|
onOk: () => resolve(true),
|
||||||
|
onCancel: () => resolve(false),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!confirmed) return;
|
||||||
|
|
||||||
|
await deleteMutation.mutateAsync({
|
||||||
|
ids: [id],
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Delete error:", error);
|
||||||
|
message.error("删除失败,请重试");
|
||||||
|
}
|
||||||
|
|
||||||
|
refetch();
|
||||||
|
message.success("删除成功");
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-6">
|
||||||
|
<header className="rounded-t-xl bg-gradient-to-r from-primary to-primary-400 text-white p-6 relative mb-4">
|
||||||
|
<div className="flex flex-col space-b-1">
|
||||||
|
<h1>锦囊资源上传</h1>
|
||||||
|
<p>支持视频、图片、文档、PPT等多种格式文件</p>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{!isAuthenticated && (
|
||||||
|
<Alert
|
||||||
|
message="请先登录"
|
||||||
|
description="您需要登录后才能查看和管理文件资源"
|
||||||
|
type="warning"
|
||||||
|
showIcon
|
||||||
|
className="mb-4"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isAuthenticated && !isDomainAdmin && (
|
||||||
|
<Alert
|
||||||
|
message="权限不足"
|
||||||
|
description="只有管理员才能上传和管理文件资源"
|
||||||
|
type="warning"
|
||||||
|
showIcon
|
||||||
|
className="mb-4"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 上传区域 */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<Tabs defaultActiveKey="1">
|
||||||
|
<TabPane>
|
||||||
|
<div
|
||||||
|
className={`relative rounded-xl border hover:ring-1 ring-white transition-all duration-300 ease-in-out bg-slate-100 ${!isDomainAdmin ? " pointer-events-none" : ""}`}
|
||||||
|
>
|
||||||
|
<TusUploader
|
||||||
|
key={uploaderKey}
|
||||||
|
value={fileIds}
|
||||||
|
onChange={(value) => {
|
||||||
|
if (!isDomainAdmin) {
|
||||||
|
message.error("只有管理员才能上传文件");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setFileIds(value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/* {!isDomainAdmin && (
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center bg-black bg-opacity-20 rounded-xl">
|
||||||
|
<span className="text-white font-medium">
|
||||||
|
只有管理员才能使用上传功能
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)} */}
|
||||||
|
</div>
|
||||||
|
</TabPane>
|
||||||
|
</Tabs>
|
||||||
|
{isDomainAdmin && fileIds.length > 0 && (
|
||||||
|
<div className="flex items-center justify-end gap-2">
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
onClick={handleSubmit}
|
||||||
|
className="flex items-center space-x-2 bg-primary"
|
||||||
|
icon={<SendOutlined />}
|
||||||
|
>
|
||||||
|
上传
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 文件展示区域 */}
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* 图片资源展示 */}
|
||||||
|
{imageResources?.length > 0 && (
|
||||||
|
<div className="rounded-xl border p-4 bg-white">
|
||||||
|
<h3 className="text-lg font-medium mb-4">图片列表</h3>
|
||||||
|
<Row gutter={[16, 16]}>
|
||||||
|
<Image.PreviewGroup>
|
||||||
|
{imageResources.map((resource) => (
|
||||||
|
<Col key={resource.url} xs={12} sm={8} md={6} lg={4}>
|
||||||
|
<div className="relative aspect-square rounded-lg overflow-hidden flex items-center justify-center bg-gray-100">
|
||||||
|
<Image
|
||||||
|
src={resource.url}
|
||||||
|
alt={resource.title}
|
||||||
|
preview={{
|
||||||
|
src: resource.originalUrl,
|
||||||
|
}}
|
||||||
|
className="object-contain w-full h-full"
|
||||||
|
/>
|
||||||
|
<div className="absolute bottom-0 left-0 right-0 p-2 bg-gradient-to-t from-black/60 to-transparent">
|
||||||
|
<div className="flex items-center justify-between text-white">
|
||||||
|
<span className="text-sm truncate">
|
||||||
|
{resource.title}
|
||||||
|
</span>
|
||||||
|
{isDomainAdmin && (
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
className="text-white hover:text-red-500"
|
||||||
|
icon={<DeleteOutlined />}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleDelete(resource.id);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
))}
|
||||||
|
</Image.PreviewGroup>
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 其他文件资源展示 */}
|
||||||
|
{fileResources?.length > 0 && (
|
||||||
|
<div className="rounded-xl border p-4 bg-white">
|
||||||
|
<h3 className="text-lg font-medium mb-4">文件列表</h3>
|
||||||
|
<div className="grid gap-4 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{fileResources.map((resource) => (
|
||||||
|
<div
|
||||||
|
key={resource.url}
|
||||||
|
className="flex items-center p-3 rounded-lg border hover:shadow-md transition-shadow"
|
||||||
|
>
|
||||||
|
<div className="text-primary-600 text-2xl mr-3">
|
||||||
|
{getFileIcon(resource.url)}
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="font-medium truncate">
|
||||||
|
{resource.title || "未命名文件"}
|
||||||
|
</div>
|
||||||
|
{resource.description && (
|
||||||
|
<div className="text-xs text-gray-500 mt-1">
|
||||||
|
描述: {resource.description}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="text-xs text-gray-500 flex items-center gap-2">
|
||||||
|
<span>
|
||||||
|
{dayjs(resource.createdAt).format("YYYY-MM-DD")}
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
{resource.meta?.size &&
|
||||||
|
formatFileSize(resource.meta.size)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
onClick={() => window.open(resource.url)}
|
||||||
|
>
|
||||||
|
下载
|
||||||
|
</Button>
|
||||||
|
{isDomainAdmin && (
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
size="small"
|
||||||
|
danger
|
||||||
|
icon={<DeleteOutlined />}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleDelete(resource.id);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,8 @@
|
||||||
|
import PsychologyNav from "./PsychologyNav";
|
||||||
|
|
||||||
export default function HelpPage() {
|
export default function HelpPage() {
|
||||||
return <>help</>
|
|
||||||
|
return <div >
|
||||||
|
<PsychologyNav />
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
.psychology-tabs {
|
||||||
|
/* 标签页样式 */
|
||||||
|
.ant-tabs-nav {
|
||||||
|
margin: 0 !important;
|
||||||
|
background: #fff;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-tabs-tab {
|
||||||
|
padding: 12px 0 !important;
|
||||||
|
font-size: 15px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #1890ff; /* 修改悬停颜色为Ant Design默认蓝色 */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-tabs-tab-active {
|
||||||
|
.ant-tabs-tab-btn {
|
||||||
|
color: #1890ff !important; /* 修改激活状态文字颜色 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 修改图标和文字颜色 */
|
||||||
|
.anticon, span {
|
||||||
|
color: #1890ff !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 激活标签的下划线样式 */
|
||||||
|
.ant-tabs-ink-bar {
|
||||||
|
height: 2px !important;
|
||||||
|
background: #1890ff; /* 修改下划线颜色 */
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.overflow-auto::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overflow-auto::-webkit-scrollbar-track {
|
||||||
|
background: #f1f1f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overflow-auto::-webkit-scrollbar-thumb {
|
||||||
|
background: #888;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overflow-auto::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #555;
|
||||||
|
}
|
||||||
|
|
@ -73,7 +73,7 @@ export default function LetterProgressPage() {
|
||||||
{data && (
|
{data && (
|
||||||
<>
|
<>
|
||||||
<LetterCard letter={data as any} />
|
<LetterCard letter={data as any} />
|
||||||
<div className=" p-6 bg-slate-100 border hover:ring-1 ring-white ease-in-out hover:-translate-y-0.5 border-white rounded-xl cursor-pointer overflow-hidden transition-all duration-300">
|
<div className=" p-6 bg-slate-100 border hover:ring-1 ring-white ease-in-out hover:-translate-y-0.5 border-white rounded-xl cursor-pointer overflow-hidden transition-all duration-300">
|
||||||
<Steps
|
<Steps
|
||||||
current={[
|
current={[
|
||||||
PostState.PENDING,
|
PostState.PENDING,
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,8 @@ interface UploadingFile {
|
||||||
fileKey?: string;
|
fileKey?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TusUploader = ({ value = [], onChange }: TusUploaderProps) => {
|
export const TusUploader = ({ value = [], onChange,
|
||||||
|
}: 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[]>(
|
||||||
|
|
@ -48,7 +49,6 @@ export const TusUploader = ({ value = [], onChange }: TusUploaderProps) => {
|
||||||
},
|
},
|
||||||
[onChange]
|
[onChange]
|
||||||
);
|
);
|
||||||
|
|
||||||
// 新增:处理删除上传中的失败文件
|
// 新增:处理删除上传中的失败文件
|
||||||
const handleRemoveUploadingFile = useCallback((fileKey: string) => {
|
const handleRemoveUploadingFile = useCallback((fileKey: string) => {
|
||||||
setUploadingFiles((prev) => prev.filter((f) => f.fileKey !== fileKey));
|
setUploadingFiles((prev) => prev.filter((f) => f.fileKey !== fileKey));
|
||||||
|
|
@ -57,7 +57,6 @@ export const TusUploader = ({ value = [], onChange }: TusUploaderProps) => {
|
||||||
const handleBeforeUpload = useCallback(
|
const handleBeforeUpload = useCallback(
|
||||||
(file: File) => {
|
(file: File) => {
|
||||||
const fileKey = `${file.name}-${Date.now()}`;
|
const fileKey = `${file.name}-${Date.now()}`;
|
||||||
|
|
||||||
setUploadingFiles((prev) => [
|
setUploadingFiles((prev) => [
|
||||||
...prev,
|
...prev,
|
||||||
{
|
{
|
||||||
|
|
@ -107,7 +106,7 @@ export const TusUploader = ({ value = [], onChange }: TusUploaderProps) => {
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
[handleFileUpload, onChange]
|
[handleFileUpload, onChange, ]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -123,7 +122,9 @@ export const TusUploader = ({ value = [], onChange }: TusUploaderProps) => {
|
||||||
<UploadOutlined />
|
<UploadOutlined />
|
||||||
</p>
|
</p>
|
||||||
<p className="ant-upload-text">点击或拖拽文件到此区域进行上传</p>
|
<p className="ant-upload-text">点击或拖拽文件到此区域进行上传</p>
|
||||||
<p className="ant-upload-hint">支持单个或批量上传文件</p>
|
<p className="ant-upload-hint">支持单个或批量上传文件
|
||||||
|
|
||||||
|
</p>
|
||||||
|
|
||||||
<div className="px-2 py-0 rounded mt-1">
|
<div className="px-2 py-0 rounded mt-1">
|
||||||
{uploadingFiles.map((file) => (
|
{uploadingFiles.map((file) => (
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,12 @@ import React, {
|
||||||
createContext,
|
createContext,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { Avatar } from "../../../common/element/Avatar";
|
import { Avatar } from "../../../common/element/Avatar";
|
||||||
|
import {api} from "@nice/client";
|
||||||
import {
|
import {
|
||||||
UserOutlined,
|
UserOutlined,
|
||||||
SettingOutlined,
|
SettingOutlined,
|
||||||
LogoutOutlined,
|
LogoutOutlined,
|
||||||
|
VideoCameraOutlined,
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import { FormInstance, Spin } from "antd";
|
import { FormInstance, Spin } from "antd";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
@ -76,6 +78,8 @@ export function UserMenu() {
|
||||||
const canManageAnyStaff = useMemo(() => {
|
const canManageAnyStaff = useMemo(() => {
|
||||||
return hasSomePermissions(RolePerms.MANAGE_ANY_STAFF);
|
return hasSomePermissions(RolePerms.MANAGE_ANY_STAFF);
|
||||||
}, [user]);
|
}, [user]);
|
||||||
|
const [showUploadModal, setShowUploadModal] = useState(false);
|
||||||
|
const utils = api.useUtils();
|
||||||
const menuItems: MenuItemType[] = useMemo(
|
const menuItems: MenuItemType[] = useMemo(
|
||||||
() =>
|
() =>
|
||||||
[
|
[
|
||||||
|
|
@ -226,20 +230,18 @@ export function UserMenu() {
|
||||||
focus:ring-2 focus:ring-[#00538E]/20
|
focus:ring-2 focus:ring-[#00538E]/20
|
||||||
group relative overflow-hidden
|
group relative overflow-hidden
|
||||||
active:scale-[0.99]
|
active:scale-[0.99]
|
||||||
${
|
${item.label === "注销"
|
||||||
item.label === "注销"
|
|
||||||
? "text-[#B22234] hover:bg-red-50/80 hover:text-red-700"
|
? "text-[#B22234] hover:bg-red-50/80 hover:text-red-700"
|
||||||
: "text-[#00538E] hover:bg-[#E6EEF5] hover:text-[#003F6A]"
|
: "text-[#00538E] hover:bg-[#E6EEF5] hover:text-[#003F6A]"
|
||||||
}`}>
|
}`}>
|
||||||
<span
|
<span
|
||||||
className={`w-5 h-5 flex items-center justify-center
|
className={`w-5 h-5 flex items-center justify-center
|
||||||
transition-all duration-200 ease-in-out
|
transition-all duration-200 ease-in-out
|
||||||
group-hover:scale-110 group-hover:rotate-6
|
group-hover:scale-110 group-hover:rotate-6
|
||||||
group-hover:translate-x-0.5 ${
|
group-hover:translate-x-0.5 ${item.label === "注销"
|
||||||
item.label === "注销"
|
? "group-hover:text-red-600"
|
||||||
? "group-hover:text-red-600"
|
: "group-hover:text-[#003F6A]"
|
||||||
: "group-hover:text-[#003F6A]"
|
}`}>
|
||||||
}`}>
|
|
||||||
{item.icon}
|
{item.icon}
|
||||||
</span>
|
</span>
|
||||||
<span>{item.label}</span>
|
<span>{item.label}</span>
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import Logo from "../../common/element/Logo";
|
||||||
|
|
||||||
export function Footer() {
|
export function Footer() {
|
||||||
return (
|
return (
|
||||||
<footer className="bg-gradient-to-b from-slate-800 to-slate-900 relative z-10 text-secondary-200 mt-1">
|
<footer className="bg-gradient-to-b from-blue-800 to-blue-900 relative z-10 text-secondary-200 py-6 px-8 flex flex-col md:flex-row justify-between items-center">
|
||||||
<div className="container mx-auto px-4 py-6">
|
<div className="container mx-auto px-4 py-6">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
{/* 开发组织信息 */}
|
{/* 开发组织信息 */}
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ export const Header = memo(function Header() {
|
||||||
{!isAuthenticated
|
{!isAuthenticated
|
||||||
? !previewMode && (
|
? !previewMode && (
|
||||||
<Button
|
<Button
|
||||||
className="text-lg bg-primary-500/80"
|
className="text-lg bg-green-500/80"
|
||||||
style={{
|
style={{
|
||||||
boxShadow: "none",
|
boxShadow: "none",
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,6 @@ export function useNavItem() {
|
||||||
icon: <CommentOutlined className="text-base" />,
|
icon: <CommentOutlined className="text-base" />,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// 按照指定顺序返回导航项
|
// 按照指定顺序返回导航项
|
||||||
return [
|
return [
|
||||||
!previewMode && staticItems.inbox,
|
!previewMode && staticItems.inbox,
|
||||||
|
|
|
||||||
|
|
@ -34,9 +34,9 @@ export default function PostCommentCard({
|
||||||
layout>
|
layout>
|
||||||
<div className="flex items-start space-x-2 gap-2">
|
<div className="flex items-start space-x-2 gap-2">
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
{/* <a href={post.author?.meta?.photoUrl}>
|
<a href={post.author?.meta?.photoUrl}>
|
||||||
{post.author?.meta?.photoUrl}
|
{post.author?.meta?.photoUrl}
|
||||||
</a> */}
|
</a>
|
||||||
<CustomAvatar
|
<CustomAvatar
|
||||||
src={post.author?.meta?.photoUrl}
|
src={post.author?.meta?.photoUrl}
|
||||||
size={50}
|
size={50}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,3 @@
|
||||||
// This is your Prisma schema file,
|
|
||||||
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
|
||||||
|
|
||||||
generator client {
|
generator client {
|
||||||
provider = "prisma-client-js"
|
provider = "prisma-client-js"
|
||||||
}
|
}
|
||||||
|
|
@ -267,26 +264,25 @@ model Visit {
|
||||||
|
|
||||||
model Resource {
|
model Resource {
|
||||||
id String @id @default(cuid()) @map("id")
|
id String @id @default(cuid()) @map("id")
|
||||||
title String? @map("title")
|
title String? @map("title") // 视频标题
|
||||||
description String? @map("description")
|
description String? @map("description") // 视频描述
|
||||||
type String? @map("type")
|
type String? @map("type") // 文件类型,可以设置为 "video"
|
||||||
fileId String? @unique
|
fileId String? @unique // 文件唯一标识
|
||||||
url String?
|
url String?
|
||||||
meta Json? @map("meta")
|
coverUrl String? @map("cover_url")
|
||||||
status String?
|
meta Json? @map("meta") // 可以存储额外信息,如封面图url等
|
||||||
createdAt DateTime? @default(now()) @map("created_at")
|
status String? // 视频状态
|
||||||
|
createdAt DateTime? @default(now()) @map("created_at") // 上传时间
|
||||||
updatedAt DateTime? @updatedAt @map("updated_at")
|
updatedAt DateTime? @updatedAt @map("updated_at")
|
||||||
createdBy String? @map("created_by")
|
createdBy String? @map("created_by") // 上传者ID
|
||||||
updatedBy String? @map("updated_by")
|
updatedBy String? @map("updated_by")
|
||||||
deletedAt DateTime? @map("deleted_at")
|
deletedAt DateTime? @map("deleted_at")
|
||||||
|
|
||||||
isPublic Boolean? @default(true) @map("is_public")
|
isPublic Boolean? @default(true) @map("is_public")
|
||||||
owner Staff? @relation(fields: [ownerId], references: [id])
|
owner Staff? @relation(fields: [ownerId], references: [id])
|
||||||
ownerId String? @map("owner_id")
|
ownerId String? @map("owner_id")
|
||||||
post Post? @relation(fields: [postId], references: [id])
|
post Post? @relation(fields: [postId], references: [id])
|
||||||
postId String? @map("post_id")
|
postId String? @map("post_id")
|
||||||
|
|
||||||
// 索引
|
|
||||||
@@index([type])
|
@@index([type])
|
||||||
@@index([createdAt])
|
@@index([createdAt])
|
||||||
@@map("resource")
|
@@map("resource")
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue