123
This commit is contained in:
parent
ad76e0dac7
commit
13d79ee235
|
@ -9,6 +9,7 @@ import {
|
|||
UpdateOrderArgs,
|
||||
TransactionType,
|
||||
SelectArgs,
|
||||
OrderByArgs,
|
||||
} from './base.type';
|
||||
import {
|
||||
NotFoundException,
|
||||
|
@ -451,8 +452,9 @@ export class BaseService<
|
|||
pageSize?: number;
|
||||
where?: WhereArgs<A['findMany']>;
|
||||
select?: SelectArgs<A['findMany']>;
|
||||
orderBy?: OrderByArgs<A['findMany']>
|
||||
}): Promise<{ items: R['findMany']; totalPages: number, totalCount: number }> {
|
||||
const { page = 1, pageSize = 10, where, select } = args;
|
||||
const { page = 1, pageSize = 10, where, select, orderBy } = args;
|
||||
|
||||
try {
|
||||
// 获取总记录数
|
||||
|
@ -462,6 +464,7 @@ export class BaseService<
|
|||
const items = (await this.getModel().findMany({
|
||||
where,
|
||||
select,
|
||||
orderBy,
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
} as any)) as R['findMany'];
|
||||
|
|
|
@ -12,6 +12,7 @@ const PostDeleteManyArgsSchema: ZodType<Prisma.PostDeleteManyArgs> = z.any();
|
|||
const PostWhereInputSchema: ZodType<Prisma.PostWhereInput> = z.any();
|
||||
const PostSelectSchema: ZodType<Prisma.PostSelect> = z.any();
|
||||
const PostUpdateInputSchema: ZodType<Prisma.PostUpdateInput> = z.any();
|
||||
const PostOrderBySchema: ZodType<Prisma.PostOrderByWithRelationInput> = z.any()
|
||||
@Injectable()
|
||||
export class PostRouter {
|
||||
constructor(
|
||||
|
@ -103,6 +104,7 @@ export class PostRouter {
|
|||
pageSize: z.number().optional(),
|
||||
where: PostWhereInputSchema.optional(),
|
||||
select: PostSelectSchema.optional(),
|
||||
orderBy: PostOrderBySchema.optional()
|
||||
}),
|
||||
) // Assuming StaffMethodSchema.findMany is the Zod schema for finding staffs by keyword
|
||||
.query(async ({ input, ctx }) => {
|
||||
|
|
|
@ -71,8 +71,11 @@ export class PostService extends BaseService<Prisma.PostDelegate> {
|
|||
const transDto = await this.wrapResult(
|
||||
super.findFirst(args),
|
||||
async (result) => {
|
||||
await setPostRelation({ data: result, staff, clientIp });
|
||||
await this.setPerms(result, staff);
|
||||
if (result) {
|
||||
await setPostRelation({ data: result, staff, clientIp });
|
||||
await this.setPerms(result, staff);
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
);
|
||||
|
@ -98,13 +101,13 @@ export class PostService extends BaseService<Prisma.PostDelegate> {
|
|||
});
|
||||
}
|
||||
async findManyWithPagination(
|
||||
args: { page?: number; pageSize?: number; where?: Prisma.PostWhereInput; select?: Prisma.PostSelect<DefaultArgs>; },
|
||||
args: { page?: number; pageSize?: number; where?: Prisma.PostWhereInput; select?: Prisma.PostSelect<DefaultArgs>; orderBy?: Prisma.PostOrderByWithRelationInput },
|
||||
staff?: UserProfile,
|
||||
clientIp?: string,
|
||||
) {
|
||||
if (!args.where) args.where = {};
|
||||
args.where.OR = await this.preFilter(args.where.OR, staff);
|
||||
return this.wrapResult(super.findManyWithPagination(args), async (result) => {
|
||||
return this.wrapResult(super.findManyWithPagination(args as any), async (result) => {
|
||||
const { items } = result;
|
||||
await Promise.all(
|
||||
items.map(async (item) => {
|
||||
|
|
|
@ -4,5 +4,8 @@ import { useParams } from "react-router-dom";
|
|||
export default function LetterDetailPage() {
|
||||
const { id } = useParams();
|
||||
|
||||
return <PostDetail id={id}></PostDetail>;
|
||||
return <div className="shadow-elegant border-2 border-white rounded-xl bg-gradient-to-b from-slate-100 to-slate-50">
|
||||
|
||||
<PostDetail id={id}></PostDetail>
|
||||
</div>;
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ export function Header() {
|
|||
<div className="flex flex-col space-y-6">
|
||||
{/* 主标题区域 */}
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-wider text-white">公开信件列表</h1>
|
||||
<h1 className="text-3xl font-bold tracking-wider text-white">公开信件</h1>
|
||||
<p className="mt-2 text-blue-100 text-lg">
|
||||
透明公开 • 及时反馈 • 有效解决
|
||||
</p>
|
||||
|
|
|
@ -5,7 +5,7 @@ export default function LetterListPage() {
|
|||
|
||||
return (
|
||||
// 添加 flex flex-col 使其成为弹性布局容器
|
||||
<div className="min-h-screen shadow-elegant border-2 border-white rounded-xl overflow-hidden bg-gradient-to-b from-slate-100 to-slate-50 flex flex-col">
|
||||
<div className="min-h-screen shadow-elegant border-2 border-white rounded-xl overflow-hidden bg-slate-200 flex flex-col">
|
||||
<Header />
|
||||
{/* 添加 flex-grow 使内容区域自动填充剩余空间 */}
|
||||
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import { Input, Button, Card, Steps, Tag, Spin, message, Empty } from "antd";
|
||||
import { SearchOutlined, SafetyCertificateOutlined } from "@ant-design/icons";
|
||||
import { SearchOutlined, SafetyCertificateOutlined, SendOutlined, MailOutlined, SyncOutlined, CheckCircleOutlined } from "@ant-design/icons";
|
||||
import ProgressHeader from "./ProgressHeader";
|
||||
import { api } from "@nice/client";
|
||||
import { LetterBadge } from "@web/src/components/models/post/LetterBadge";
|
||||
import dayjs from "dayjs";
|
||||
import { LetterCard } from "@web/src/components/models/post/LetterCard";
|
||||
import { postDetailSelect } from "@nice/common";
|
||||
import { postDetailSelect, PostState } from "@nice/common";
|
||||
|
||||
|
||||
|
||||
export default function LetterProgressPage() {
|
||||
const [letterId, setLetterId] = useState<string>();
|
||||
const { data, isFetching, error } = api.post.findFirst.useQuery({
|
||||
const { data, isLoading, error } = api.post.findFirst.useQuery({
|
||||
where: {
|
||||
id: letterId
|
||||
},
|
||||
|
@ -28,11 +28,12 @@ export default function LetterProgressPage() {
|
|||
|
||||
return (
|
||||
<div
|
||||
className="min-h-screen shadow-elegant border-2 border-white rounded-xl bg-gradient-to-b from-slate-100 to-slate-50">
|
||||
className=" shadow-elegant border-2 border-white rounded-xl bg-slate-200">
|
||||
<ProgressHeader></ProgressHeader>
|
||||
<main className="container mx-auto p-6 flex flex-col gap-4">
|
||||
<div className="flex gap-4">
|
||||
<Input
|
||||
variant="filled"
|
||||
prefix={
|
||||
<SearchOutlined className=" text-secondary-300" />
|
||||
}
|
||||
|
@ -43,91 +44,45 @@ export default function LetterProgressPage() {
|
|||
|
||||
/>
|
||||
</div>
|
||||
{isLoading && <div className="flex justify-center items-center pt-6">
|
||||
<Spin size="large" ></Spin>
|
||||
</div>}
|
||||
{!isLoading && letterId && !data && (
|
||||
<div className=" p-6">
|
||||
<Empty
|
||||
description={`未找到编码为 ${letterId} 的信件`}
|
||||
></Empty>
|
||||
</div>
|
||||
)}
|
||||
{data && (
|
||||
<>
|
||||
<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">
|
||||
|
||||
{/* Results Section */}
|
||||
<Spin spinning={isFetching} tip="查询中...">
|
||||
{data ? (
|
||||
<>
|
||||
<div className="space-y-6 p-4 rounded-xl bg-white">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between border-b pb-4 space-y-2">
|
||||
<h2 className="text-xl font-semibold text-primary">
|
||||
信件处理进度追踪
|
||||
</h2>
|
||||
<LetterBadge type="state" value={data?.state} className="text-sm" />
|
||||
</div>
|
||||
<Steps
|
||||
current={[PostState.PENDING, PostState.PROCESSING, PostState.RESOLVED].indexOf(data.state as PostState)}
|
||||
|
||||
{/* Details Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">
|
||||
信件编号
|
||||
</p>
|
||||
<p className="font-medium text-[#003366]">
|
||||
{data.id}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">
|
||||
提交日期
|
||||
</p>
|
||||
<p className="font-medium text-[#003366]">
|
||||
{dayjs(data.createdAt).format('YYYY-MM-DD HH:mm:ss')}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">
|
||||
最后更新
|
||||
</p>
|
||||
<p className="font-medium text-[#003366]">
|
||||
{dayjs(data.updatedAt).format('YYYY-MM-DD HH:mm:ss')}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">
|
||||
标题
|
||||
</p>
|
||||
<p className="font-medium text-[#003366]">
|
||||
{data.title}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Progress Timeline */}
|
||||
<div className="mt-8">
|
||||
<Steps
|
||||
current={['pending', 'processing', 'completed'].indexOf(data.state)}
|
||||
|
||||
items={[
|
||||
{
|
||||
title: '已提交',
|
||||
description: '信件已成功接收',
|
||||
icon: <SafetyCertificateOutlined />,
|
||||
},
|
||||
{
|
||||
title: '处理中',
|
||||
description: '正在审核处理',
|
||||
icon: <SafetyCertificateOutlined />,
|
||||
},
|
||||
{
|
||||
title: '已办结',
|
||||
description: '信件处理完成',
|
||||
icon: <SafetyCertificateOutlined />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<LetterCard letter={data as any} /></>
|
||||
) : !isFetching && letterId && (
|
||||
<div className=" p-6">
|
||||
|
||||
<Empty
|
||||
description={`未找到编码为 ${letterId} 的信件`}
|
||||
></Empty>
|
||||
items={[
|
||||
{
|
||||
title: '已提交',
|
||||
description: '信件已成功接收',
|
||||
icon: <MailOutlined></MailOutlined>
|
||||
},
|
||||
{
|
||||
title: '处理中',
|
||||
description: '正在审核处理',
|
||||
icon: <SyncOutlined></SyncOutlined>
|
||||
},
|
||||
{
|
||||
title: '已办结',
|
||||
description: '信件处理完成',
|
||||
icon: <CheckCircleOutlined></CheckCircleOutlined>
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Spin>
|
||||
</>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { motion } from 'framer-motion';
|
||||
import { StaffDto } from "@nice/common";
|
||||
import { Button, Tooltip, Badge } from 'antd';
|
||||
import { BankFilled, SendOutlined } from '@ant-design/icons';
|
||||
import { BankFilled, SendOutlined, UserOutlined, MailOutlined, PhoneOutlined, HomeOutlined } from '@ant-design/icons';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
export interface SendCardProps {
|
||||
|
@ -10,19 +10,17 @@ export interface SendCardProps {
|
|||
}
|
||||
|
||||
export function SendCard({ staff, termId }: SendCardProps) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleSendLetter = () => {
|
||||
window.open(`/editor?termId=${termId || ''}&receiverId=${staff.id}`, '_blank');
|
||||
};
|
||||
return (
|
||||
<div
|
||||
|
||||
className="bg-white rounded-xl overflow-hidden border-2 hover:border-primary transition-all duration-300"
|
||||
onClick={handleSendLetter}
|
||||
className="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="flex flex-col sm:flex-row">
|
||||
{/* Image Container */}
|
||||
<div className="sm:w-56 h-72 sm:h-auto flex-shrink-0 bg-gradient-to-br from-gray-50 to-gray-100 flex items-center justify-center relative">
|
||||
<div className="sm:w-56 h-72 sm:h-auto flex-shrink-0 border-r flex items-center justify-center relative">
|
||||
{staff.meta?.photoUrl ? (
|
||||
<img
|
||||
src={staff.meta.photoUrl}
|
||||
|
@ -31,20 +29,7 @@ export function SendCard({ staff, termId }: SendCardProps) {
|
|||
/>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center text-gray-400">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-20 w-20 mb-3 text-gray-300"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1}
|
||||
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<span className="text-sm">暂未上传照片</span>
|
||||
</div>
|
||||
)}
|
||||
|
@ -57,15 +42,11 @@ export function SendCard({ staff, termId }: SendCardProps) {
|
|||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 mb-4">
|
||||
<div>
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<h3 className="text-2xl font-semibold text-gray-900">
|
||||
<h3 className="text-2xl font-semibold text-gray-700">
|
||||
{staff?.showname || staff?.username}
|
||||
</h3>
|
||||
|
||||
</div>
|
||||
<p className="text-gray-600 text-lg font-medium flex items-center gap-2">
|
||||
|
||||
<BankFilled></BankFilled>
|
||||
{staff.department?.name || '未设置部门'}</p>
|
||||
</div>
|
||||
<Tooltip title="职级">
|
||||
<span className="inline-flex items-center px-4 py-1.5 text-sm font-medium bg-gradient-to-r from-blue-50 to-blue-100 text-primary rounded-full hover:from-blue-100 hover:to-blue-200 transition-colors shadow-sm">
|
||||
|
@ -76,39 +57,26 @@ export function SendCard({ staff, termId }: SendCardProps) {
|
|||
|
||||
{/* Contact Information */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm text-gray-600 mb-6">
|
||||
<div className="flex items-center bg-gray-50 rounded-lg p-3 hover:bg-gray-100 transition-all duration-200 hover:shadow-sm">
|
||||
<svg className="w-4 h-4 mr-2 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2"
|
||||
d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<div className="flex items-center bg-gray-50 rounded-lg p-3 transition-all duration-200 hover:shadow-sm">
|
||||
<BankFilled className="w-4 h-4 mr-2 text-gray-500" rev={undefined} />
|
||||
<span className="truncate">{staff.department?.name || '未设置部门'}</span>
|
||||
</div>
|
||||
<div className="flex items-center bg-gray-50 rounded-lg p-3 transition-all duration-200 hover:shadow-sm">
|
||||
<MailOutlined className="w-4 h-4 mr-2 text-gray-500" rev={undefined} />
|
||||
<span className="truncate">{staff.meta?.email || '未设置邮箱'}</span>
|
||||
</div>
|
||||
<div className="flex items-center bg-gray-50 rounded-lg p-3 hover:bg-gray-100 transition-all duration-200 hover:shadow-sm">
|
||||
<svg className="w-4 h-4 mr-2 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2"
|
||||
d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
|
||||
</svg>
|
||||
<div className="flex items-center bg-gray-50 rounded-lg p-3 transition-all duration-200 hover:shadow-sm">
|
||||
<PhoneOutlined className="w-4 h-4 mr-2 text-gray-500" rev={undefined} />
|
||||
<span className="truncate">{staff.phoneNumber || '未设置电话'}</span>
|
||||
</div>
|
||||
<div className="flex items-center bg-gray-50 rounded-lg p-3 hover:bg-gray-100 transition-all duration-200 hover:shadow-sm md:col-span-2">
|
||||
<svg className="w-4 h-4 mr-2 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2"
|
||||
d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
|
||||
</svg>
|
||||
<div className="flex items-center bg-gray-50 rounded-lg p-3 transition-all duration-200 hover:shadow-sm">
|
||||
<HomeOutlined className="w-4 h-4 mr-2 text-gray-500" rev={undefined} />
|
||||
<span className="truncate">{staff.meta?.office || '未设置办公室'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
icon={<SendOutlined />}
|
||||
onClick={handleSendLetter}
|
||||
|
||||
>
|
||||
发送信件
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -46,27 +46,29 @@ export default function WriteLetterPage() {
|
|||
}, [searchQuery, selectedDept, resetPage]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen shadow-elegant border-2 border-white rounded-xl bg-gradient-to-b from-slate-100 to-slate-50">
|
||||
<div className="min-h-screen shadow-elegant border-2 border-white rounded-xl bg-slate-200">
|
||||
<WriteHeader term={getTerm(termId)} />
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="mb-8 space-y-4">
|
||||
{/* Search and Filter Section */}
|
||||
<div className="flex flex-col md:flex-row gap-4 items-center">
|
||||
<div className="w-full md:w-96">
|
||||
<Input
|
||||
prefix={<SearchOutlined className="text-gray-400" />}
|
||||
placeholder="搜索领导姓名或职级..."
|
||||
onChange={debounce((e) => setSearchQuery(e.target.value), 300)}
|
||||
className="w-full"
|
||||
size="large"
|
||||
/>
|
||||
</div>
|
||||
<DepartmentSelect
|
||||
variant='filled'
|
||||
size="large"
|
||||
value={selectedDept}
|
||||
onChange={setSelectedDept as any}
|
||||
className="w-full md:w-64"
|
||||
className="w-1/2"
|
||||
/>
|
||||
<Input
|
||||
variant='filled'
|
||||
className={'w-1/2'}
|
||||
prefix={<SearchOutlined className="text-gray-400" />}
|
||||
placeholder="搜索领导姓名或职级..."
|
||||
onChange={debounce((e) => setSearchQuery(e.target.value), 300)}
|
||||
|
||||
size="large"
|
||||
/>
|
||||
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
|
|
|
@ -11,8 +11,8 @@ export function Footer() {
|
|||
<h3 className="text-white font-semibold text-sm flex items-center justify-center md:justify-start">
|
||||
创新高地 软件小组
|
||||
</h3>
|
||||
<p className="text-gray-400 text-xs">
|
||||
提供系统开发与技术支持
|
||||
<p className="text-gray-400 text-xs italic">
|
||||
提供技术支持
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
@ -20,11 +20,11 @@ export function Footer() {
|
|||
<div className="text-center space-y-2">
|
||||
<div className="flex items-center justify-center space-x-2">
|
||||
<PhoneOutlined className="text-gray-400" />
|
||||
<span className="text-gray-300 text-xs">1-800-XXX-XXXX</span>
|
||||
<span className="text-gray-300 text-xs">628118</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-center space-x-2">
|
||||
<MailOutlined className="text-gray-400" />
|
||||
<span className="text-gray-300 text-xs">support@example.com</span>
|
||||
<span className="text-gray-300 text-xs">gcsjs6@tx3l.nb.kj</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -32,14 +32,14 @@ export function Footer() {
|
|||
<div className="text-center md:text-right space-y-2">
|
||||
<div className="flex items-center justify-center md:justify-end space-x-4">
|
||||
<a
|
||||
href="https://portal.example.com"
|
||||
href="https://27.57.72.21"
|
||||
className="text-gray-400 hover:text-white transition-colors"
|
||||
title="访问门户网站"
|
||||
>
|
||||
<HomeOutlined className="text-lg" />
|
||||
</a>
|
||||
<a
|
||||
href="https://nextcloud.example.com"
|
||||
href="https://27.57.72.14"
|
||||
className="text-gray-400 hover:text-white transition-colors"
|
||||
title="访问烽火青云"
|
||||
>
|
||||
|
@ -47,7 +47,7 @@ export function Footer() {
|
|||
</a>
|
||||
|
||||
<a
|
||||
href="https://regulation.example.com"
|
||||
href="http://27.57.72.38"
|
||||
className="text-gray-400 hover:text-white transition-colors"
|
||||
title="访问烽火律询"
|
||||
>
|
||||
|
|
|
@ -15,6 +15,7 @@ interface DepartmentSelectProps {
|
|||
disabled?: boolean;
|
||||
className?: string;
|
||||
size?: "small" | "middle" | "large";
|
||||
variant?: "outlined" | "borderless" | "filled"
|
||||
}
|
||||
|
||||
export default function DepartmentSelect({
|
||||
|
@ -28,6 +29,7 @@ export default function DepartmentSelect({
|
|||
size,
|
||||
disabled = false,
|
||||
domain = undefined,
|
||||
variant
|
||||
}: DepartmentSelectProps) {
|
||||
const utils = api.useUtils();
|
||||
const [listTreeData, setListTreeData] = useState<
|
||||
|
@ -148,6 +150,7 @@ export default function DepartmentSelect({
|
|||
|
||||
return (
|
||||
<TreeSelect
|
||||
variant={variant}
|
||||
treeDataSimpleMode
|
||||
disabled={disabled}
|
||||
showSearch
|
||||
|
|
|
@ -13,64 +13,72 @@ import {
|
|||
export const BADGE_STYLES = {
|
||||
category: {
|
||||
complaint: {
|
||||
bg: "bg-gradient-to-r from-orange-50 to-orange-100",
|
||||
text: "text-orange-800",
|
||||
border: "border-orange-200",
|
||||
icon: <ExclamationCircleOutlined className="text-orange-500" />,
|
||||
hover: "hover:from-orange-100 hover:to-orange-200",
|
||||
bg: "bg-orange-50",
|
||||
text: "text-orange-700",
|
||||
border: "border border-orange-100",
|
||||
|
||||
icon: <ExclamationCircleOutlined className="text-orange-400 text-sm" />,
|
||||
|
||||
},
|
||||
suggestion: {
|
||||
bg: "bg-gradient-to-r from-blue-50 to-blue-100",
|
||||
text: "text-blue-800",
|
||||
border: "border-blue-200",
|
||||
icon: <BulbOutlined className="text-blue-500" />,
|
||||
hover: "hover:from-blue-100 hover:to-blue-200",
|
||||
bg: "bg-blue-50",
|
||||
text: "text-blue-700",
|
||||
border: "border border-blue-100",
|
||||
|
||||
icon: <BulbOutlined className="text-blue-400 text-sm" />,
|
||||
|
||||
},
|
||||
request: {
|
||||
bg: "bg-gradient-to-r from-purple-50 to-purple-100",
|
||||
text: "text-purple-800",
|
||||
border: "border-purple-200",
|
||||
icon: <QuestionCircleOutlined className="text-purple-500" />,
|
||||
hover: "hover:from-purple-100 hover:to-purple-200",
|
||||
bg: "bg-purple-50",
|
||||
text: "text-purple-700",
|
||||
border: "border border-purple-100",
|
||||
|
||||
icon: <QuestionCircleOutlined className="text-purple-400 text-sm" />,
|
||||
|
||||
},
|
||||
feedback: {
|
||||
bg: "bg-gradient-to-r from-teal-50 to-teal-100",
|
||||
text: "text-teal-800",
|
||||
border: "border-teal-200",
|
||||
icon: <CommentOutlined className="text-teal-500" />,
|
||||
hover: "hover:from-teal-100 hover:to-teal-200",
|
||||
bg: "bg-teal-50",
|
||||
text: "text-teal-700",
|
||||
border: "border border-teal-100",
|
||||
|
||||
icon: <CommentOutlined className="text-teal-400 text-sm" />,
|
||||
|
||||
},
|
||||
},
|
||||
state: {
|
||||
[PostState.PENDING]: {
|
||||
bg: "bg-gradient-to-r from-yellow-50 to-yellow-100",
|
||||
text: "text-yellow-800",
|
||||
border: "border-yellow-200",
|
||||
icon: <ClockCircleOutlined className="text-yellow-500" />,
|
||||
hover: "hover:from-yellow-100 hover:to-yellow-200",
|
||||
bg: "bg-yellow-50",
|
||||
text: "text-yellow-700",
|
||||
border: "border border-yellow-100",
|
||||
|
||||
icon: <ClockCircleOutlined className="text-yellow-400 text-sm" />,
|
||||
|
||||
},
|
||||
[PostState.PROCESSING]: {
|
||||
bg: "bg-gradient-to-r from-blue-50 to-blue-100",
|
||||
text: "text-blue-800",
|
||||
border: "border-blue-200",
|
||||
icon: <SyncOutlined className="text-blue-500" spin />,
|
||||
hover: "hover:from-blue-100 hover:to-blue-200",
|
||||
bg: "bg-blue-50",
|
||||
text: "text-blue-700",
|
||||
border: "border border-blue-100",
|
||||
|
||||
icon: <SyncOutlined className="text-blue-400 text-sm" spin />,
|
||||
|
||||
},
|
||||
[PostState.RESOLVED]: {
|
||||
bg: "bg-gradient-to-r from-green-50 to-green-100",
|
||||
text: "text-green-800",
|
||||
border: "border-green-200",
|
||||
icon: <CheckCircleOutlined className="text-green-500" />,
|
||||
hover: "hover:from-green-100 hover:to-green-200",
|
||||
bg: "bg-green-50",
|
||||
text: "text-green-700",
|
||||
border: "border border-green-100",
|
||||
|
||||
icon: <CheckCircleOutlined className="text-green-400 text-sm" />,
|
||||
|
||||
},
|
||||
},
|
||||
tag: {
|
||||
default: {
|
||||
bg: "bg-gradient-to-r from-gray-50 to-gray-100",
|
||||
text: "text-gray-800",
|
||||
border: "border-gray-200",
|
||||
icon: <TagOutlined className="text-gray-500" />,
|
||||
hover: "hover:from-gray-100 hover:to-gray-200",
|
||||
bg: "bg-gray-50",
|
||||
text: "text-gray-700",
|
||||
border: "border border-gray-100",
|
||||
|
||||
icon: <TagOutlined className="text-gray-400 text-sm" />,
|
||||
|
||||
}
|
||||
},
|
||||
} as const;
|
||||
|
@ -82,11 +90,9 @@ export const getBadgeStyle = (
|
|||
const style = type === 'tag'
|
||||
? BADGE_STYLES.tag.default
|
||||
: BADGE_STYLES[type][value as keyof (typeof BADGE_STYLES)[typeof type]] || {
|
||||
bg: "bg-gradient-to-r from-gray-50 to-gray-100",
|
||||
text: "text-gray-800",
|
||||
border: "border-gray-200",
|
||||
icon: <TagOutlined className="text-gray-500" />,
|
||||
hover: "hover:from-gray-100 hover:to-gray-200",
|
||||
bg: "bg-gray-50",
|
||||
text: "text-gray-700",
|
||||
icon: <TagOutlined className="text-gray-400 text-sm" />,
|
||||
};
|
||||
|
||||
return style;
|
||||
|
@ -108,12 +114,9 @@ export function LetterBadge({
|
|||
return (
|
||||
<span
|
||||
className={`
|
||||
inline-flex items-center gap-1 px-2.5 py-1 rounded-full text-xs font-medium
|
||||
${style.bg} ${style.text} ${style.hover}
|
||||
border ${style.border}
|
||||
shadow-sm backdrop-blur-sm
|
||||
transition-all duration-300 ease-out
|
||||
hover:shadow-md hover:-translate-y-0.5
|
||||
inline-flex items-center gap-2 px-2 py-1 rounded-full
|
||||
text-xs transition-all
|
||||
${style.bg} ${style.text} ${style.border}
|
||||
${className}
|
||||
`}>
|
||||
{style.icon}
|
||||
|
|
|
@ -7,6 +7,7 @@ import {
|
|||
CalendarOutlined,
|
||||
FileTextOutlined,
|
||||
SendOutlined,
|
||||
MailOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { Button, Typography, Space, Tooltip } from "antd";
|
||||
import { PostDto, PostStateLabels } from "@nice/common";
|
||||
|
@ -21,54 +22,42 @@ interface LetterCardProps {
|
|||
|
||||
export function LetterCard({ letter }: LetterCardProps) {
|
||||
return (
|
||||
<div onClick={() => {
|
||||
window.open(`/${letter.id}/detail`)
|
||||
}}
|
||||
className=" cursor-pointer w-full p-6 bg-white rounded-xl group relative overflow-hidden duration-300 hover:-translate-y-1 transition-all ease-in-out "
|
||||
<div
|
||||
onClick={() => {
|
||||
window.open(`/${letter.id}/detail`)
|
||||
}}
|
||||
className="cursor-pointer p-6 bg-slate-100/80 rounded-xl hover:ring-white hover:ring-1 transition-all
|
||||
duration-300 ease-in-out hover:-translate-y-0.5
|
||||
active:scale-[0.98] border border-white
|
||||
group relative overflow-hidden"
|
||||
>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex justify-between items-start">
|
||||
<Title level={4} className="!mb-0 flex-1 font-serif tracking-tight text-gray-800">
|
||||
<a
|
||||
href={`/${letter.id}/detail`}
|
||||
target="_blank"
|
||||
|
||||
className="text-primary hover:text-primary-600 transition-colors duration-200 hover:underline">
|
||||
{letter.title}
|
||||
</a>
|
||||
</Title>
|
||||
<div className=" text-xl text-primary font-bold">
|
||||
{letter.title}
|
||||
</div>
|
||||
{/* Meta Info */}
|
||||
<div className="flex justify-between items-center text-sm text-gray-500">
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="flex justify-between items-center text-sm text-gray-600 gap-4 flex-wrap">
|
||||
<div className="flex items-center gap-4 flex-1 min-w-[300px]">
|
||||
{letter.author?.department?.name && (
|
||||
<div className="flex items-center gap-2">
|
||||
<BankOutlined className="text-secondary-400 text-base" />
|
||||
<Text className="text-gray-600 font-medium">
|
||||
{letter.author?.department?.name}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
<UserOutlined className="text-secondary-400 text-base" />
|
||||
<Text className="text-gray-600 font-medium">
|
||||
<UserOutlined className="text-primary text-base" />
|
||||
<Text className="text-primary font-medium">
|
||||
{letter.author?.showname || '匿名用户'}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{letter.receivers.some(item => item.department?.name) && (
|
||||
<div className="flex items-center gap-2">
|
||||
<BankOutlined className="text-secondary-400 text-base" />
|
||||
<Tooltip title={letter.receivers.map(item => item.department?.name).filter(Boolean).join(', ')}>
|
||||
<Text className="text-gray-600">
|
||||
{letter.receivers
|
||||
.map(item => item.department?.name)
|
||||
.filter(Boolean)
|
||||
.slice(0, 2)
|
||||
.join('、')}
|
||||
{letter.receivers.filter(item => item.department?.name).length > 2 && ' 等'}
|
||||
</Text>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{letter.receivers.some(item => item.showname) && (
|
||||
<div className="flex items-center gap-2">
|
||||
<SendOutlined className="text-secondary-400 text-base" />
|
||||
<MailOutlined className="text-primary-400 text-base" />
|
||||
<Tooltip title={letter.receivers.map(item => item.showname).filter(Boolean).join(', ')}>
|
||||
<Text className="text-gray-600">
|
||||
<Text className="text-primary-400">
|
||||
{letter.receivers
|
||||
.map(item => item.showname)
|
||||
.filter(Boolean)
|
||||
|
@ -90,21 +79,21 @@ export function LetterCard({ letter }: LetterCardProps) {
|
|||
|
||||
{/* Content Preview */}
|
||||
{letter.content && (
|
||||
<Paragraph
|
||||
ellipsis={{ rows: 2 }}
|
||||
className="!mb-4 text-gray-600 flex-1 leading-relaxed text-sm font-sans">
|
||||
{letter.content}
|
||||
</Paragraph>
|
||||
<div className="flex-1 leading-relaxed text-sm">
|
||||
<div
|
||||
dangerouslySetInnerHTML={{ __html: letter.content }}
|
||||
className="line-clamp-2"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<LetterBadge type="state" value={letter.state} />
|
||||
{letter.meta.tags.map(tag => (
|
||||
<LetterBadge key={tag} type="tag" value={tag} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Badges & Interactions */}
|
||||
<div className="flex justify-between items-center ">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<LetterBadge type="state" value={letter.state} />
|
||||
{letter.meta.tags.map(tag => (
|
||||
<LetterBadge key={tag} type="tag" value={tag} />
|
||||
))}
|
||||
{letter.terms.map(term => (
|
||||
<LetterBadge key={term.name} type="category" value={term.name} />
|
||||
))}
|
||||
|
|
|
@ -6,7 +6,7 @@ import { Avatar } from "antd";
|
|||
import { useVisitor } from "@nice/client";
|
||||
import { useContext, useEffect, useRef, useState } from "react";
|
||||
import { PostDetailContext } from "./context/PostDetailContext";
|
||||
import { LikeFilled, LikeOutlined } from "@ant-design/icons";
|
||||
import { CheckCircleOutlined, LikeFilled, LikeOutlined } from "@ant-design/icons";
|
||||
import PostLikeButton from "./PostHeader/PostLikeButton";
|
||||
import { CustomAvatar } from "@web/src/components/presentation/CustomAvatar";
|
||||
import PostResources from "./PostResources";
|
||||
|
@ -22,7 +22,7 @@ export default function PostCommentCard({
|
|||
}) {
|
||||
return (
|
||||
<motion.div
|
||||
className="bg-white rounded-lg shadow-sm border border-slate-200 p-4"
|
||||
className="bg-white rounded-lg border p-4"
|
||||
layout>
|
||||
<div className="flex items-start space-x-2 gap-2">
|
||||
<div className="flex-shrink-0">
|
||||
|
@ -35,7 +35,7 @@ export default function PostCommentCard({
|
|||
<div className="flex-1">
|
||||
<div className={`flex-1 min-w-0 `}>
|
||||
<div className="flex flex-1 justify-between ">
|
||||
<div className="flex space-x-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="flex font-medium text-slate-900">
|
||||
{post.author?.showname || "匿名用户"}
|
||||
</span>
|
||||
|
@ -45,10 +45,9 @@ export default function PostCommentCard({
|
|||
)}
|
||||
</span>
|
||||
{isReceiverComment && (
|
||||
<div className=" ">
|
||||
<span className=" py-0.5 px-2 text-xs rounded-full bg-blue-100 text-blue-800">
|
||||
官方回复
|
||||
</span>
|
||||
<div className=" py-1 px-4 rounded-full bg-primary-50 text-primary-500">
|
||||
|
||||
官方回复
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -52,19 +52,19 @@ export default function PostCommentEditor() {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="w-full mx-auto mt-1">
|
||||
<div className="p-6">
|
||||
<form onSubmit={handleSubmit} className="space-y-2">
|
||||
<Tabs defaultActiveKey="1">
|
||||
<TabPane tab="回复" key="1">
|
||||
<div className="relative rounded-lg border border-slate-200 bg-white shadow-sm">
|
||||
<div className="relative rounded-lg border bg-white">
|
||||
<QuillEditor
|
||||
value={content}
|
||||
onChange={setContent}
|
||||
placeholder="写下你的回复..."
|
||||
|
||||
className="bg-transparent"
|
||||
theme="snow"
|
||||
minRows={6}
|
||||
maxRows={12}
|
||||
|
||||
modules={{
|
||||
toolbar: [
|
||||
["bold", "italic", "strike"],
|
||||
|
@ -77,12 +77,7 @@ export default function PostCommentEditor() {
|
|||
["clean"],
|
||||
],
|
||||
}}
|
||||
style={
|
||||
{
|
||||
"--ql-border-color": "transparent",
|
||||
"--ql-toolbar-bg": "rgb(248, 250, 252)", // slate-50
|
||||
} as React.CSSProperties
|
||||
}
|
||||
|
||||
/>
|
||||
</div>
|
||||
</TabPane>
|
||||
|
@ -101,6 +96,7 @@ export default function PostCommentEditor() {
|
|||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
size="large"
|
||||
disabled={isContentEmpty(content)}
|
||||
className="flex items-center space-x-2 bg-primary"
|
||||
icon={<SendOutlined />}>
|
||||
|
|
|
@ -6,6 +6,7 @@ import { motion, AnimatePresence } from "framer-motion";
|
|||
import PostCommentCard from "./PostCommentCard";
|
||||
import { useInView } from "react-intersection-observer";
|
||||
import { LoadingCard } from "@web/src/components/models/post/detail/LoadingCard";
|
||||
import { Spin } from "antd";
|
||||
export default function PostCommentList() {
|
||||
const { post } = useContext(PostDetailContext);
|
||||
const { ref: loadMoreRef, inView } = useInView();
|
||||
|
@ -136,18 +137,11 @@ export default function PostCommentList() {
|
|||
}
|
||||
|
||||
if (!items.length) {
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="text-center py-12 text-slate-500">
|
||||
暂无回复,来发表第一条回复吧
|
||||
</motion.div>
|
||||
);
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-2 mt-2">
|
||||
<div className=" space-y-2 p-6">
|
||||
<AnimatePresence mode="popLayout">
|
||||
{items.map((comment, index) => (
|
||||
<motion.div
|
||||
|
@ -173,9 +167,7 @@ export default function PostCommentList() {
|
|||
{/* 加载更多触发器 */}
|
||||
<div ref={loadMoreRef} className="h-20">
|
||||
{isFetchingNextOfficialPage || isFetchingNextNonOfficialPage ? (
|
||||
<div className="flex justify-center py-4">
|
||||
<div className="w-6 h-6 border-2 border-[#00308F] border-t-transparent rounded-full animate-spin" />
|
||||
</div>
|
||||
<Spin></Spin>
|
||||
) : (
|
||||
items.length > 0 &&
|
||||
!hasNextOfficialPage &&
|
||||
|
@ -185,21 +177,11 @@ export default function PostCommentList() {
|
|||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className="flex flex-col items-center py-4 space-y-2">
|
||||
<div className="h-px w-16 bg-gradient-to-r from-transparent via-[#00308F]/30 to-transparent" />
|
||||
|
||||
<span className="text-sm text-gray-500 font-medium">
|
||||
已加载全部回复
|
||||
</span>
|
||||
<motion.div
|
||||
className="h-px w-16 bg-gradient-to-r from-transparent via-[#00308F]/30 to-transparent"
|
||||
initial={{ width: "4rem" }}
|
||||
animate={{ width: "12rem" }}
|
||||
transition={{
|
||||
duration: 1.5,
|
||||
repeat: Infinity,
|
||||
repeatType: "reverse",
|
||||
ease: "easeInOut",
|
||||
}}
|
||||
/>
|
||||
|
||||
</motion.div>
|
||||
)
|
||||
)}
|
||||
|
|
|
@ -1,16 +1,9 @@
|
|||
import { useEffect } from "react";
|
||||
import { PostDetailProvider } from "./context/PostDetailContext";
|
||||
import PostDetailLayout from "./layout/PostDetailLayout";
|
||||
|
||||
export default function PostDetail({ id }: { id?: string }) {
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<PostDetailProvider editId={id}>
|
||||
<PostDetailLayout></PostDetailLayout>
|
||||
</PostDetailProvider>
|
||||
</>
|
||||
);
|
||||
return <PostDetailProvider editId={id}>
|
||||
<PostDetailLayout></PostDetailLayout>
|
||||
</PostDetailProvider>
|
||||
}
|
||||
|
|
|
@ -19,16 +19,15 @@ export default function Content() {
|
|||
}, [post?.content]);
|
||||
|
||||
return (
|
||||
<div className="relative bg-white rounded-b-xl p-4 pt-2 border border-[#97A9C4]/30">
|
||||
<div className="text-secondary-700">
|
||||
<div className="p-6 text-base " >
|
||||
<div className="text-secondary-700 bg-white rounded-lg p-6 ">
|
||||
{/* 包装整个内容区域的容器 */}
|
||||
<div
|
||||
ref={contentWrapperRef}
|
||||
className={`duration-300 ${
|
||||
shouldCollapse && !isExpanded
|
||||
? `max-h-[150px] overflow-hidden relative`
|
||||
: ""
|
||||
}`}>
|
||||
className={`duration-300 ${shouldCollapse && !isExpanded
|
||||
? `max-h-[150px] overflow-hidden relative`
|
||||
: ""
|
||||
}`}>
|
||||
{/* 内容区域 */}
|
||||
<div
|
||||
className="ql-editor p-0 space-y-1 leading-relaxed"
|
||||
|
@ -37,8 +36,7 @@ export default function Content() {
|
|||
}}
|
||||
/>
|
||||
|
||||
{/* PostResources 组件 */}
|
||||
<PostResources post={post} />
|
||||
|
||||
|
||||
{/* 渐变遮罩 */}
|
||||
{shouldCollapse && !isExpanded && (
|
||||
|
@ -55,8 +53,6 @@ export default function Content() {
|
|||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Stats Section */}
|
||||
<StatsSection />
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -12,16 +12,12 @@ import {
|
|||
UserOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import dayjs from "dayjs";
|
||||
import { CornerBadge } from "../badge/CornerBadeg";
|
||||
import { LetterBadge } from "../../LetterBadge";
|
||||
const { Title, Paragraph, Text } = Typography;
|
||||
export default function Header() {
|
||||
const { post, user } = useContext(PostDetailContext);
|
||||
return (
|
||||
<header className="rounded-t-xl bg-gradient-to-r from-primary to-primary-400 text-white p-4 relative">
|
||||
{/* 右上角标签 */}
|
||||
{/* <CornerBadge type="state" value={post?.state}></CornerBadge> */}
|
||||
|
||||
<header className="rounded-t-xl bg-gradient-to-r from-primary to-primary-400 text-white p-6 relative">
|
||||
<div className="flex flex-col space-b-1">
|
||||
{/* 主标题 */}
|
||||
<div>
|
||||
|
@ -29,42 +25,42 @@ export default function Header() {
|
|||
{post?.title}
|
||||
</h1>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
{/* 收件人信息行 */}
|
||||
<div className="space-y-2">
|
||||
|
||||
<Space className="mr-4">
|
||||
<MailOutlined className="text-white" />
|
||||
<span className="text-white">收件人:</span>
|
||||
|
||||
<span className="text-white">发件人</span>
|
||||
<Text className="text-white" strong>
|
||||
{post?.author?.showname || "匿名用户"}
|
||||
</Text>
|
||||
</Space>
|
||||
<Space className="mr-4">
|
||||
|
||||
<span className="text-white">收件人 </span>
|
||||
|
||||
{post?.receivers?.map((receiver, index) => (
|
||||
<Text
|
||||
strong
|
||||
className="text-white"
|
||||
key={`${index}`}>
|
||||
{receiver?.showname}
|
||||
{receiver?.meta?.rank} {receiver?.showname || '匿名用户'}
|
||||
</Text>
|
||||
))}
|
||||
</Space>
|
||||
<Space className="mr-4">
|
||||
<SendOutlined className="text-white" />
|
||||
<span className="text-white">发件人:</span>
|
||||
<Text className="text-white" strong>
|
||||
{post?.author?.showname || "匿名用户"}
|
||||
</Text>
|
||||
</Space>
|
||||
{/* Date Info Badge */}
|
||||
<Space className="mr-4">
|
||||
<CalendarOutlined className="text-white" />
|
||||
|
||||
<span className="text-white">创建于</span>
|
||||
<Text className="text-white">
|
||||
创建于:
|
||||
|
||||
{dayjs(post?.createdAt).format("YYYY-MM-DD")}
|
||||
</Text>
|
||||
</Space>
|
||||
{/* Last Updated Badge */}
|
||||
<Space className="mr-4">
|
||||
<ClockCircleOutlined className="text-white" />
|
||||
|
||||
<span className="text-white">最后更新于</span>
|
||||
<Text className="text-white">
|
||||
更新于:
|
||||
|
||||
{dayjs(post?.updatedAt).format("YYYY-MM-DD")}
|
||||
</Text>
|
||||
</Space>
|
||||
|
@ -76,7 +72,7 @@ export default function Header() {
|
|||
<LockOutlined className="text-white" />
|
||||
)}
|
||||
<Text className="text-white">
|
||||
{post?.isPublic ? "公开" : "私信"}
|
||||
{post?.isPublic ? "公开" : "保密"}
|
||||
</Text>
|
||||
</Space>
|
||||
|
||||
|
@ -86,7 +82,7 @@ export default function Header() {
|
|||
</div>
|
||||
{/* Second Row - Term and Tags */}
|
||||
{post?.meta?.tags?.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{/* Tags Badges */}
|
||||
|
||||
<LetterBadge type="state" value={post?.state} />
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
import { useContext } from "react";
|
||||
import { PostDetailContext } from "../context/PostDetailContext";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
import { StatsSection } from "./StatsSection";
|
||||
|
||||
import PostResources from "../PostResources";
|
||||
import Header from "./Header";
|
||||
import Content from "./Content";
|
||||
|
||||
export default function PostHeader() {
|
||||
const { post, user } = useContext(PostDetailContext);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header></Header>
|
||||
<Content></Content>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -31,19 +31,19 @@ export default function PostLikeButton({ post }: { post: PostDto }) {
|
|||
}
|
||||
}
|
||||
return (
|
||||
<Tooltip title={post?.liked ? "取消点赞" : "点赞"} placement="top">
|
||||
<Button
|
||||
type={post?.liked ? "primary" : "default"}
|
||||
shape="round"
|
||||
ghost={post?.liked}
|
||||
icon={post?.liked ? <LikeFilled /> : <LikeOutlined />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
likeThisPost()
|
||||
}}
|
||||
>
|
||||
{post?.likes}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Button
|
||||
title={post?.liked ? "取消点赞" : "点赞"}
|
||||
type={post?.liked ? "primary" : "default"}
|
||||
shape="round"
|
||||
|
||||
icon={post?.liked ? <LikeFilled /> : <LikeOutlined />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
likeThisPost()
|
||||
}}
|
||||
>
|
||||
{post?.likes}
|
||||
</Button>
|
||||
|
||||
);
|
||||
}
|
||||
|
|
|
@ -8,27 +8,27 @@ import {
|
|||
} from "@ant-design/icons";
|
||||
import { Button, Tooltip } from "antd/lib";
|
||||
import { PostDetailContext } from "../context/PostDetailContext";
|
||||
import { useVisitor } from "@nice/client";
|
||||
import { VisitType } from "packages/common/dist";
|
||||
import PostLikeButton from "./PostLikeButton";
|
||||
import PostResources from "../PostResources";
|
||||
export function StatsSection() {
|
||||
const { post } = useContext(PostDetailContext);
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.7 }}
|
||||
className="mt-6 flex flex-wrap gap-4 justify-between items-center">
|
||||
<div
|
||||
|
||||
className="mt-6 flex flex-wrap gap-4 justify-between items-center">
|
||||
{/* PostResources 组件 */}
|
||||
<PostResources post={post} />
|
||||
<div className=" flex gap-2">
|
||||
<Button type="default" shape="round" icon={<EyeOutlined />}>
|
||||
<Button title="浏览量" type="default" shape="round" icon={<EyeOutlined />}>
|
||||
{post?.views}
|
||||
</Button>
|
||||
<Button type="default" shape="round" icon={<CommentOutlined />}>
|
||||
<Button type="default" title="评论数" shape="round" icon={<CommentOutlined />}>
|
||||
{post?.commentsCount}
|
||||
</Button>
|
||||
<PostLikeButton post={post}></PostLikeButton>
|
||||
</div>
|
||||
<PostLikeButton post={post}></PostLikeButton>
|
||||
</motion.div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
import { motion } from "framer-motion";
|
||||
import { useContext, useEffect } from "react";
|
||||
import { PostDetailContext } from "../context/PostDetailContext";
|
||||
import PostHeader from "../PostHeader/PostHeader";
|
||||
import WriteHeader from "../PostHeader/Header";
|
||||
import PostCommentEditor from "../PostCommentEditor";
|
||||
import PostCommentList from "../PostCommentList";
|
||||
import { useVisitor } from "@nice/client";
|
||||
import { useAuth } from "@web/src/providers/auth-provider";
|
||||
import { VisitType } from "@nice/common";
|
||||
import Header from "../PostHeader/Header";
|
||||
import Content from "../PostHeader/Content";
|
||||
|
||||
export default function PostDetailLayout() {
|
||||
const { post, user } = useContext(PostDetailContext);
|
||||
|
@ -15,7 +13,6 @@ export default function PostDetailLayout() {
|
|||
|
||||
useEffect(() => {
|
||||
if (post) {
|
||||
console.log("read");
|
||||
read.mutateAsync({
|
||||
data: {
|
||||
visitorId: user?.id || null,
|
||||
|
@ -25,11 +22,10 @@ export default function PostDetailLayout() {
|
|||
});
|
||||
}
|
||||
}, [post]);
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-slate-50 to-slate-100 flex flex-col">
|
||||
<PostHeader></PostHeader>
|
||||
<PostCommentEditor></PostCommentEditor>
|
||||
<PostCommentList></PostCommentList>
|
||||
</div>
|
||||
);
|
||||
return <>
|
||||
<Header></Header>
|
||||
<Content></Content>
|
||||
<PostCommentEditor></PostCommentEditor>
|
||||
<PostCommentList></PostCommentList>
|
||||
</>
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { Input, Pagination, Empty } from 'antd';
|
||||
import { Input, Pagination, Empty, Spin } from 'antd';
|
||||
import { api, RouterInputs } from "@nice/client";
|
||||
import { LetterCard } from "../LetterCard";
|
||||
import { NonVoid } from "@nice/utils";
|
||||
|
@ -21,15 +21,15 @@ export default function LetterList({ params }: { params: NonVoid<RouterInputs["p
|
|||
}],
|
||||
...params?.where
|
||||
},
|
||||
orderBy: {
|
||||
updatedAt: "desc"
|
||||
},
|
||||
select: {
|
||||
...postDetailSelect,
|
||||
...params.select
|
||||
}
|
||||
});
|
||||
useEffect(() => {
|
||||
console.log(data)
|
||||
}, [data])
|
||||
// Debounced search function
|
||||
|
||||
const debouncedSearch = useMemo(
|
||||
() =>
|
||||
debounce((value: string) => {
|
||||
|
@ -57,29 +57,27 @@ export default function LetterList({ params }: { params: NonVoid<RouterInputs["p
|
|||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
{/* Search Bar */}
|
||||
<div className="p-6 mb-6 transition-all ">
|
||||
<div className="max-w-2xl ">
|
||||
<Input
|
||||
placeholder="搜索信件标题..."
|
||||
allowClear
|
||||
size="large"
|
||||
onChange={(e) => handleSearch(e.target.value)}
|
||||
prefix={<SearchOutlined className="text-gray-400" />}
|
||||
/>
|
||||
</div>
|
||||
<div className="p-6 transition-all ">
|
||||
<Input
|
||||
variant="filled"
|
||||
className='w-full'
|
||||
placeholder="搜索信件标题..."
|
||||
allowClear
|
||||
size="large"
|
||||
onChange={(e) => handleSearch(e.target.value)}
|
||||
prefix={<SearchOutlined className="text-gray-400" />}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Content Area */}
|
||||
<div className="flex-grow overflow-auto px-4">
|
||||
<div className="flex-grow px-6">
|
||||
{isLoading ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 animate-pulse">
|
||||
{[...Array(6)].map((_, index) => (
|
||||
<div key={index} className="h-48 bg-gray-100 rounded-lg"></div>
|
||||
))}
|
||||
<div className='flex justify-center items-center pt-6'>
|
||||
<Spin size='large'></Spin>
|
||||
</div>
|
||||
) : data?.items.length ? (
|
||||
<>
|
||||
<div className="grid grid-cols-1 md:grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-1 lg:grid-cols-1 gap-4 mb-6">
|
||||
{data.items.map((letter: any) => (
|
||||
<LetterCard key={letter.id} letter={letter} />
|
||||
))}
|
||||
|
@ -96,16 +94,14 @@ export default function LetterList({ params }: { params: NonVoid<RouterInputs["p
|
|||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex flex-col justify-center items-center h-96">
|
||||
<div className="flex flex-col justify-center items-center pt-6">
|
||||
<Empty
|
||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||
|
||||
description={
|
||||
<span className="text-gray-600">
|
||||
{searchText ? "未找到相关信件" : "暂无信件"}
|
||||
</span>
|
||||
searchText ? "未找到相关信件" : "暂无信件"
|
||||
}
|
||||
|
||||
className="flex flex-col items-center"
|
||||
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
border-bottom-left-radius: 8px;
|
||||
border-bottom-right-radius: 8px;
|
||||
border: none;
|
||||
@apply text-base
|
||||
}
|
||||
|
||||
.ag-custom-dragging-class {
|
||||
|
|
|
@ -198,5 +198,5 @@ export enum PostState {
|
|||
export const PostStateLabels = {
|
||||
[PostState.PENDING]: "待处理",
|
||||
[PostState.PROCESSING]: "处理中",
|
||||
[PostState.RESOLVED]: "已解答",
|
||||
[PostState.RESOLVED]: "已完成",
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue