add
This commit is contained in:
parent
fe5750199a
commit
0d38c1f838
|
@ -99,7 +99,7 @@ export async function updatePostState(id: string) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (post?.state === PostState.COMPLETED) {
|
if (post?.state === PostState.RESOLVED) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const postReceiverIds = post.receivers
|
const postReceiverIds = post.receivers
|
||||||
|
@ -132,7 +132,7 @@ export async function updatePostState(id: string) {
|
||||||
if (receiverComments > 0) {
|
if (receiverComments > 0) {
|
||||||
await db.post.update({
|
await db.post.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data: { state: PostState.COMPLETED },
|
data: { state: PostState.RESOLVED },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ const pipeline = new ResourceProcessingPipeline()
|
||||||
.addProcessor(new VideoProcessor());
|
.addProcessor(new VideoProcessor());
|
||||||
export default async function processJob(job: Job<any, any, QueueJobType>) {
|
export default async function processJob(job: Job<any, any, QueueJobType>) {
|
||||||
if (job.name === QueueJobType.FILE_PROCESS) {
|
if (job.name === QueueJobType.FILE_PROCESS) {
|
||||||
console.log(job);
|
// console.log(job);
|
||||||
const { resource } = job.data;
|
const { resource } = job.data;
|
||||||
if (!resource) {
|
if (!resource) {
|
||||||
throw new Error('No resource provided in job data');
|
throw new Error('No resource provided in job data');
|
||||||
|
|
|
@ -89,6 +89,7 @@ export class TusService implements OnModuleInit {
|
||||||
upload: Upload,
|
upload: Upload,
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
|
console.log('upload.id', upload.id);
|
||||||
const resource = await this.resourceService.update({
|
const resource = await this.resourceService.update({
|
||||||
where: { fileId: this.getFileId(upload.id) },
|
where: { fileId: this.getFileId(upload.id) },
|
||||||
data: { status: ResourceStatus.UPLOADED },
|
data: { status: ResourceStatus.UPLOADED },
|
||||||
|
|
|
@ -7,10 +7,15 @@ export interface UploadCompleteEvent {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UploadEvent = {
|
export type UploadEvent = {
|
||||||
uploadStart: { identifier: string; filename: string; totalSize: number, resuming?: boolean };
|
uploadStart: {
|
||||||
uploadComplete: UploadCompleteEvent
|
identifier: string;
|
||||||
uploadError: { identifier: string; error: string, filename: string };
|
filename: string;
|
||||||
}
|
totalSize: number;
|
||||||
|
resuming?: boolean;
|
||||||
|
};
|
||||||
|
uploadComplete: UploadCompleteEvent;
|
||||||
|
uploadError: { identifier: string; error: string; filename: string };
|
||||||
|
};
|
||||||
export interface UploadLock {
|
export interface UploadLock {
|
||||||
clientId: string;
|
clientId: string;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
|
|
|
@ -13,10 +13,25 @@ import {
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { TusService } from './tus.service';
|
import { TusService } from './tus.service';
|
||||||
|
import { ResourceService } from '@server/models/resource/resource.service';
|
||||||
|
|
||||||
@Controller('upload')
|
@Controller('upload')
|
||||||
export class UploadController {
|
export class UploadController {
|
||||||
constructor(private readonly tusService: TusService) {}
|
constructor(
|
||||||
|
private readonly tusService: TusService,
|
||||||
|
private readonly resourceService: ResourceService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Get('resource/:fileId')
|
||||||
|
async getResourceByFileId(@Param('fileId') fileId: string) {
|
||||||
|
const resource = await this.resourceService.findFirst({
|
||||||
|
where: { fileId },
|
||||||
|
});
|
||||||
|
if (!resource) {
|
||||||
|
return { status: 'pending' };
|
||||||
|
}
|
||||||
|
return { status: 'ready', resource };
|
||||||
|
}
|
||||||
// @Post()
|
// @Post()
|
||||||
// async handlePost(@Req() req: Request, @Res() res: Response) {
|
// async handlePost(@Req() req: Request, @Res() res: Response) {
|
||||||
// return this.tusService.handleTus(req, res);
|
// return this.tusService.handleTus(req, res);
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import LetterEditorLayout from "@web/src/components/models/post/LetterEditor/layout/LetterEditorLayout";
|
import LetterEditorLayout from "@web/src/components/models/post/editor/layout/LetterEditorLayout";
|
||||||
|
|
||||||
export default function EditorLetterPage() {
|
export default function EditorLetterPage() {
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,9 +1,17 @@
|
||||||
import { EyeOutlined, LikeOutlined, LikeFilled, UserOutlined, BankOutlined, CalendarOutlined, FileTextOutlined } from '@ant-design/icons';
|
import {
|
||||||
import { Button, Typography, Space, Tooltip } from 'antd';
|
EyeOutlined,
|
||||||
import toast from 'react-hot-toast';
|
LikeOutlined,
|
||||||
import { Letter } from './types';
|
LikeFilled,
|
||||||
import { getBadgeStyle } from './utils';
|
UserOutlined,
|
||||||
import { useState } from 'react';
|
BankOutlined,
|
||||||
|
CalendarOutlined,
|
||||||
|
FileTextOutlined,
|
||||||
|
} from "@ant-design/icons";
|
||||||
|
import { Button, Typography, Space, Tooltip } from "antd";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
|
import { Letter } from "./types";
|
||||||
|
import { getBadgeStyle } from "./utils";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
const { Title, Paragraph, Text } = Typography;
|
const { Title, Paragraph, Text } = Typography;
|
||||||
|
|
||||||
|
@ -18,27 +26,23 @@ export function LetterCard({ letter }: LetterCardProps) {
|
||||||
|
|
||||||
const handleLike = () => {
|
const handleLike = () => {
|
||||||
if (!liked) {
|
if (!liked) {
|
||||||
setLikes(prev => prev + 1);
|
setLikes((prev) => prev + 1);
|
||||||
setLiked(true);
|
setLiked(true);
|
||||||
toast.success('已点赞!', {
|
toast.success("已点赞!", {
|
||||||
icon: <LikeFilled className="text-blue-500" />,
|
icon: <LikeFilled className="text-blue-500" />,
|
||||||
className: 'custom-message',
|
className: "custom-message",
|
||||||
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setLikes(prev => prev - 1);
|
setLikes((prev) => prev - 1);
|
||||||
setLiked(false);
|
setLiked(false);
|
||||||
toast('已取消点赞', {
|
toast("已取消点赞", {
|
||||||
className: 'custom-message',
|
className: "custom-message",
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="w-full p-4 bg-white transition-all duration-300 ease-in-out group">
|
||||||
className="w-full p-4 bg-white transition-all duration-300 ease-in-out group"
|
|
||||||
>
|
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
{/* Title & Priority */}
|
{/* Title & Priority */}
|
||||||
<div className="flex justify-between items-start">
|
<div className="flex justify-between items-start">
|
||||||
|
@ -49,13 +53,16 @@ export function LetterCard({ letter }: LetterCardProps) {
|
||||||
className="text-primary transition-all duration-300 relative
|
className="text-primary transition-all duration-300 relative
|
||||||
before:absolute before:bottom-0 before:left-0 before:w-0 before:h-[2px] before:bg-primary-600
|
before:absolute before:bottom-0 before:left-0 before:w-0 before:h-[2px] before:bg-primary-600
|
||||||
group-hover:before:w-full before:transition-all before:duration-300
|
group-hover:before:w-full before:transition-all before:duration-300
|
||||||
group-hover:text-primary-600 group-hover:scale-105 group-hover:drop-shadow-md"
|
group-hover:text-primary-600 group-hover:scale-105 group-hover:drop-shadow-md">
|
||||||
>
|
|
||||||
{letter.title}
|
{letter.title}
|
||||||
</a>
|
</a>
|
||||||
</Title>
|
</Title>
|
||||||
{letter.priority && (
|
{letter.priority && (
|
||||||
<Badge type="priority" value={letter.priority} className="ml-2" />
|
<Badge
|
||||||
|
type="priority"
|
||||||
|
value={letter.priority}
|
||||||
|
className="ml-2"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -84,8 +91,7 @@ export function LetterCard({ letter }: LetterCardProps) {
|
||||||
<FileTextOutlined className="text-gray-400 mt-1" />
|
<FileTextOutlined className="text-gray-400 mt-1" />
|
||||||
<Paragraph
|
<Paragraph
|
||||||
ellipsis={{ rows: 2 }}
|
ellipsis={{ rows: 2 }}
|
||||||
className="!mb-3 text-gray-600 flex-1"
|
className="!mb-3 text-gray-600 flex-1">
|
||||||
>
|
|
||||||
{letter.content}
|
{letter.content}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
</div>
|
</div>
|
||||||
|
@ -104,11 +110,10 @@ export function LetterCard({ letter }: LetterCardProps) {
|
||||||
<span className="text-sm">{views}</span>
|
<span className="text-sm">{views}</span>
|
||||||
</div>
|
</div>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
title={liked ? '取消点赞' : '点赞'}
|
title={liked ? "取消点赞" : "点赞"}
|
||||||
placement="top"
|
placement="top">
|
||||||
>
|
|
||||||
<Button
|
<Button
|
||||||
type={liked ? 'primary' : 'default'}
|
type={liked ? "primary" : "default"}
|
||||||
shape="round"
|
shape="round"
|
||||||
size="small"
|
size="small"
|
||||||
icon={liked ? <LikeFilled /> : <LikeOutlined />}
|
icon={liked ? <LikeFilled /> : <LikeOutlined />}
|
||||||
|
@ -116,10 +121,11 @@ export function LetterCard({ letter }: LetterCardProps) {
|
||||||
className={`
|
className={`
|
||||||
flex items-center gap-1 px-3 transform transition-all duration-300
|
flex items-center gap-1 px-3 transform transition-all duration-300
|
||||||
hover:scale-105 hover:shadow-md
|
hover:scale-105 hover:shadow-md
|
||||||
${liked ? 'bg-blue-500 hover:bg-blue-600' : 'hover:border-blue-500 hover:text-blue-500'}
|
${liked ? "bg-blue-500 hover:bg-blue-600" : "hover:border-blue-500 hover:text-blue-500"}
|
||||||
`}
|
`}>
|
||||||
>
|
<span className={liked ? "text-white" : ""}>
|
||||||
<span className={liked ? 'text-white' : ''}>{likes}</span>
|
{likes}
|
||||||
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
@ -129,25 +135,27 @@ export function LetterCard({ letter }: LetterCardProps) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function Badge({
|
export function Badge({
|
||||||
type,
|
type,
|
||||||
value,
|
value,
|
||||||
className = ''
|
className = "",
|
||||||
}: {
|
}: {
|
||||||
type: 'priority' | 'category' | 'status';
|
type: "priority" | "category" | "status";
|
||||||
value: string;
|
value: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
|
value && (
|
||||||
<span
|
<span
|
||||||
className={`
|
className={`
|
||||||
inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
|
inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
|
||||||
${getBadgeStyle(type, value)}
|
${getBadgeStyle(type, value)}
|
||||||
transition-all duration-200 ease-in-out transform hover:scale-105
|
transition-all duration-200 ease-in-out transform hover:scale-105
|
||||||
${className}
|
${className}
|
||||||
`}
|
`}>
|
||||||
>
|
|
||||||
{value.toUpperCase()}
|
{value?.toUpperCase()}
|
||||||
</span>
|
</span>
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { SearchOutlined } from '@ant-design/icons';
|
import { SearchOutlined } from "@ant-design/icons";
|
||||||
import { Form, Input, Select, Spin } from 'antd';
|
import { Form, Input, Select, Spin } from "antd";
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from "react";
|
||||||
|
|
||||||
interface SearchFiltersProps {
|
interface SearchFiltersProps {
|
||||||
searchTerm: string;
|
searchTerm: string;
|
||||||
|
@ -13,9 +13,7 @@ interface SearchFiltersProps {
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const LoadingIndicator = () => (
|
const LoadingIndicator = () => <Spin size="small" className="ml-2" />;
|
||||||
<Spin size="small" className="ml-2" />
|
|
||||||
);
|
|
||||||
|
|
||||||
export function SearchFilters({
|
export function SearchFilters({
|
||||||
searchTerm,
|
searchTerm,
|
||||||
|
@ -25,7 +23,7 @@ export function SearchFilters({
|
||||||
filterStatus,
|
filterStatus,
|
||||||
onStatusChange,
|
onStatusChange,
|
||||||
className,
|
className,
|
||||||
isLoading = false
|
isLoading = false,
|
||||||
}: SearchFiltersProps) {
|
}: SearchFiltersProps) {
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
|
@ -33,7 +31,7 @@ export function SearchFilters({
|
||||||
const initialValues = {
|
const initialValues = {
|
||||||
search: searchTerm,
|
search: searchTerm,
|
||||||
category: filterCategory,
|
category: filterCategory,
|
||||||
status: filterStatus
|
status: filterStatus,
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -45,8 +43,7 @@ export function SearchFilters({
|
||||||
form={form}
|
form={form}
|
||||||
layout="vertical"
|
layout="vertical"
|
||||||
className={className}
|
className={className}
|
||||||
initialValues={initialValues}
|
initialValues={initialValues}>
|
||||||
>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
<Form.Item name="search" noStyle>
|
<Form.Item name="search" noStyle>
|
||||||
<Input
|
<Input
|
||||||
|
@ -63,11 +60,11 @@ export function SearchFilters({
|
||||||
className="w-full"
|
className="w-full"
|
||||||
onChange={onCategoryChange}
|
onChange={onCategoryChange}
|
||||||
options={[
|
options={[
|
||||||
{ value: 'all', label: '所有分类' },
|
{ value: "all", label: "所有分类" },
|
||||||
{ value: 'complaint', label: '投诉' },
|
{ value: "complaint", label: "投诉" },
|
||||||
{ value: 'suggestion', label: '建议' },
|
{ value: "suggestion", label: "建议" },
|
||||||
{ value: 'request', label: '请求' },
|
{ value: "request", label: "请求" },
|
||||||
{ value: 'feedback', label: '反馈' }
|
{ value: "feedback", label: "反馈" },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
@ -77,10 +74,10 @@ export function SearchFilters({
|
||||||
className="w-full"
|
className="w-full"
|
||||||
onChange={onStatusChange}
|
onChange={onStatusChange}
|
||||||
options={[
|
options={[
|
||||||
{ value: 'all', label: '所有状态' },
|
{ value: "all", label: "所有状态" },
|
||||||
{ value: 'pending', label: '待处理' },
|
{ value: "pending", label: "待处理" },
|
||||||
{ value: 'in-progress', label: '处理中' },
|
{ value: "processing", label: "处理中" },
|
||||||
{ value: 'resolved', label: '已解决' }
|
{ value: "resolved", label: "已解决" },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
|
@ -2,55 +2,55 @@ import { Letter } from "./types";
|
||||||
|
|
||||||
export const letters: Letter[] = [
|
export const letters: Letter[] = [
|
||||||
{
|
{
|
||||||
id: '1',
|
id: "1",
|
||||||
title: 'F-35 Maintenance Schedule Optimization Proposal',
|
title: "F-35 Maintenance Schedule Optimization Proposal",
|
||||||
sender: 'John Doe',
|
sender: "John Doe",
|
||||||
rank: 'TSgt',
|
rank: "TSgt",
|
||||||
unit: '33d Fighter Wing',
|
unit: "33d Fighter Wing",
|
||||||
date: '2025-01-22',
|
date: "2025-01-22",
|
||||||
priority: 'high',
|
priority: "high",
|
||||||
status: 'pending',
|
status: "pending",
|
||||||
category: 'suggestion',
|
category: "suggestion",
|
||||||
isStarred: false,
|
isStarred: false,
|
||||||
content: 'Proposal for improving F-35 maintenance efficiency...'
|
content: "Proposal for improving F-35 maintenance efficiency...",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '2',
|
id: "2",
|
||||||
title: 'Base Housing Facility Improvement Request',
|
title: "Base Housing Facility Improvement Request",
|
||||||
sender: 'Jane Smith',
|
sender: "Jane Smith",
|
||||||
rank: 'SSgt',
|
rank: "SSgt",
|
||||||
unit: '96th Test Wing',
|
unit: "96th Test Wing",
|
||||||
date: '2025-01-21',
|
date: "2025-01-21",
|
||||||
priority: 'medium',
|
priority: "medium",
|
||||||
status: 'in-progress',
|
status: "processing",
|
||||||
category: 'request',
|
category: "request",
|
||||||
isStarred: true,
|
isStarred: true,
|
||||||
content: 'Request for updating base housing facilities...'
|
content: "Request for updating base housing facilities...",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '3',
|
id: "3",
|
||||||
title: 'Training Program Enhancement Feedback',
|
title: "Training Program Enhancement Feedback",
|
||||||
sender: 'Robert Johnson',
|
sender: "Robert Johnson",
|
||||||
rank: 'Capt',
|
rank: "Capt",
|
||||||
unit: '58th Special Operations Wing',
|
unit: "58th Special Operations Wing",
|
||||||
date: '2025-01-20',
|
date: "2025-01-20",
|
||||||
priority: 'medium',
|
priority: "medium",
|
||||||
status: 'pending',
|
status: "pending",
|
||||||
category: 'feedback',
|
category: "feedback",
|
||||||
isStarred: false,
|
isStarred: false,
|
||||||
content: 'Feedback regarding current training procedures...'
|
content: "Feedback regarding current training procedures...",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '4',
|
id: "4",
|
||||||
title: 'Cybersecurity Protocol Update Suggestion',
|
title: "Cybersecurity Protocol Update Suggestion",
|
||||||
sender: 'Emily Wilson',
|
sender: "Emily Wilson",
|
||||||
rank: 'MSgt',
|
rank: "MSgt",
|
||||||
unit: '67th Cyberspace Wing',
|
unit: "67th Cyberspace Wing",
|
||||||
date: '2025-01-19',
|
date: "2025-01-19",
|
||||||
priority: 'high',
|
priority: "high",
|
||||||
status: 'pending',
|
status: "pending",
|
||||||
category: 'suggestion',
|
category: "suggestion",
|
||||||
isStarred: true,
|
isStarred: true,
|
||||||
content: 'Suggestions for improving base cybersecurity measures...'
|
content: "Suggestions for improving base cybersecurity measures...",
|
||||||
}
|
},
|
||||||
];
|
];
|
|
@ -1,6 +1,6 @@
|
||||||
export type Priority = 'high' | 'medium' | 'low';
|
export type Priority = "high" | "medium" | "low";
|
||||||
export type Category = 'complaint' | 'suggestion' | 'request' | 'feedback';
|
export type Category = "complaint" | "suggestion" | "request" | "feedback";
|
||||||
export type Status = 'pending' | 'in-progress' | 'resolved';
|
export type Status = "pending" | "processing" | "resolved";
|
||||||
export interface Letter {
|
export interface Letter {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
|
|
|
@ -1,19 +1,21 @@
|
||||||
|
import { PostState } from "@nice/common";
|
||||||
|
|
||||||
export const BADGE_STYLES = {
|
export const BADGE_STYLES = {
|
||||||
priority: {
|
priority: {
|
||||||
high: 'bg-red-100 text-red-800',
|
high: "bg-red-100 text-red-800",
|
||||||
medium: 'bg-yellow-100 text-yellow-800',
|
medium: "bg-yellow-100 text-yellow-800",
|
||||||
low: 'bg-green-100 text-green-800',
|
low: "bg-green-100 text-green-800",
|
||||||
},
|
},
|
||||||
category: {
|
category: {
|
||||||
complaint: 'bg-orange-100 text-orange-800',
|
complaint: "bg-orange-100 text-orange-800",
|
||||||
suggestion: 'bg-blue-100 text-blue-800',
|
suggestion: "bg-blue-100 text-blue-800",
|
||||||
request: 'bg-purple-100 text-purple-800',
|
request: "bg-purple-100 text-purple-800",
|
||||||
feedback: 'bg-teal-100 text-teal-800',
|
feedback: "bg-teal-100 text-teal-800",
|
||||||
},
|
},
|
||||||
status: {
|
status: {
|
||||||
pending: 'bg-yellow-100 text-yellow-800',
|
[PostState.PENDING]: "bg-yellow-100 text-yellow-800",
|
||||||
'in-progress': 'bg-blue-100 text-blue-800',
|
[PostState.PROCESSING]: "bg-blue-100 text-blue-800",
|
||||||
resolved: 'bg-green-100 text-green-800',
|
[PostState.RESOLVED]: "bg-green-100 text-green-800",
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
@ -21,5 +23,8 @@ export const getBadgeStyle = (
|
||||||
type: keyof typeof BADGE_STYLES,
|
type: keyof typeof BADGE_STYLES,
|
||||||
value: string
|
value: string
|
||||||
): string => {
|
): string => {
|
||||||
return BADGE_STYLES[type][value as keyof (typeof BADGE_STYLES)[typeof type]] || 'bg-gray-100 text-gray-800';
|
return (
|
||||||
|
BADGE_STYLES[type][value as keyof (typeof BADGE_STYLES)[typeof type]] ||
|
||||||
|
"bg-gray-100 text-gray-800"
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,74 +1,80 @@
|
||||||
import { useState } from 'react'
|
import { useState } from "react";
|
||||||
import { Input, Button, Card, Steps, Tag, Spin, message } from 'antd'
|
import { Input, Button, Card, Steps, Tag, Spin, message } from "antd";
|
||||||
import { SearchOutlined, SafetyCertificateOutlined } from '@ant-design/icons'
|
import { SearchOutlined, SafetyCertificateOutlined } from "@ant-design/icons";
|
||||||
|
|
||||||
interface FeedbackStatus {
|
interface FeedbackStatus {
|
||||||
status: 'pending' | 'in-progress' | 'resolved'
|
status: "pending" | "processing" | "resolved";
|
||||||
ticketId: string
|
ticketId: string;
|
||||||
submittedDate: string
|
submittedDate: string;
|
||||||
lastUpdate: string
|
lastUpdate: string;
|
||||||
title: string
|
title: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { Step } = Steps
|
const { Step } = Steps;
|
||||||
|
|
||||||
export default function LetterProgressPage() {
|
export default function LetterProgressPage() {
|
||||||
const [feedbackId, setFeedbackId] = useState('')
|
const [feedbackId, setFeedbackId] = useState("");
|
||||||
const [status, setStatus] = useState<FeedbackStatus | null>(null)
|
const [status, setStatus] = useState<FeedbackStatus | null>(null);
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState("");
|
||||||
|
|
||||||
const validateInput = () => {
|
const validateInput = () => {
|
||||||
if (!feedbackId.trim()) {
|
if (!feedbackId.trim()) {
|
||||||
setError('请输入有效的问题编号')
|
setError("请输入有效的问题编号");
|
||||||
return false
|
return false;
|
||||||
}
|
}
|
||||||
if (!/^USAF-\d{4}-\d{4}$/.test(feedbackId)) {
|
if (!/^USAF-\d{4}-\d{4}$/.test(feedbackId)) {
|
||||||
setError('问题编号格式不正确,应为USAF-YYYY-NNNN')
|
setError("问题编号格式不正确,应为USAF-YYYY-NNNN");
|
||||||
return false
|
return false;
|
||||||
}
|
|
||||||
setError('')
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
setError("");
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
const mockLookup = () => {
|
const mockLookup = () => {
|
||||||
if (!validateInput()) return
|
if (!validateInput()) return;
|
||||||
|
|
||||||
setLoading(true)
|
setLoading(true);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setStatus({
|
setStatus({
|
||||||
status: 'in-progress',
|
status: "processing",
|
||||||
ticketId: feedbackId,
|
ticketId: feedbackId,
|
||||||
submittedDate: '2025-01-15',
|
submittedDate: "2025-01-15",
|
||||||
lastUpdate: '2025-01-21',
|
lastUpdate: "2025-01-21",
|
||||||
title: 'Aircraft Maintenance Schedule Inquiry'
|
title: "Aircraft Maintenance Schedule Inquiry",
|
||||||
})
|
});
|
||||||
setLoading(false)
|
setLoading(false);
|
||||||
}, 1000)
|
}, 1000);
|
||||||
}
|
};
|
||||||
|
|
||||||
const getStatusColor = (status: string) => {
|
const getStatusColor = (status: string) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'pending':
|
case "pending":
|
||||||
return 'orange'
|
return "orange";
|
||||||
case 'in-progress':
|
case "processing":
|
||||||
return 'blue'
|
return "blue";
|
||||||
case 'resolved':
|
case "resolved":
|
||||||
return 'green'
|
return "green";
|
||||||
default:
|
default:
|
||||||
return 'gray'
|
return "gray";
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-white" style={{ fontFamily: 'Arial, sans-serif' }}>
|
<div
|
||||||
|
className="min-h-screen bg-white"
|
||||||
|
style={{ fontFamily: "Arial, sans-serif" }}>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<header className="bg-[#003366] p-8 text-white">
|
<header className="bg-[#003366] p-8 text-white">
|
||||||
<div className="container mx-auto flex items-center">
|
<div className="container mx-auto flex items-center">
|
||||||
<SafetyCertificateOutlined className="text-4xl mr-4" />
|
<SafetyCertificateOutlined className="text-4xl mr-4" />
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold mb-2">USAF Feedback Tracking System</h1>
|
<h1 className="text-3xl font-bold mb-2">
|
||||||
<p className="text-lg">Enter your ticket ID to track progress</p>
|
USAF Feedback Tracking System
|
||||||
|
</h1>
|
||||||
|
<p className="text-lg">
|
||||||
|
Enter your ticket ID to track progress
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
@ -83,12 +89,14 @@ export default function LetterProgressPage() {
|
||||||
</label>
|
</label>
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
<Input
|
<Input
|
||||||
prefix={<SearchOutlined className="text-[#003366]" />}
|
prefix={
|
||||||
|
<SearchOutlined className="text-[#003366]" />
|
||||||
|
}
|
||||||
size="large"
|
size="large"
|
||||||
value={feedbackId}
|
value={feedbackId}
|
||||||
onChange={(e) => setFeedbackId(e.target.value)}
|
onChange={(e) => setFeedbackId(e.target.value)}
|
||||||
placeholder="e.g. USAF-2025-0123"
|
placeholder="e.g. USAF-2025-0123"
|
||||||
status={error ? 'error' : ''}
|
status={error ? "error" : ""}
|
||||||
className="border border-gray-300"
|
className="border border-gray-300"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
|
@ -97,12 +105,16 @@ export default function LetterProgressPage() {
|
||||||
icon={<SearchOutlined />}
|
icon={<SearchOutlined />}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
onClick={mockLookup}
|
onClick={mockLookup}
|
||||||
style={{ backgroundColor: '#003366', borderColor: '#003366' }}
|
style={{
|
||||||
>
|
backgroundColor: "#003366",
|
||||||
|
borderColor: "#003366",
|
||||||
|
}}>
|
||||||
Track
|
Track
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{error && <p className="text-red-600 text-sm">{error}</p>}
|
{error && (
|
||||||
|
<p className="text-red-600 text-sm">{error}</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
@ -117,8 +129,7 @@ export default function LetterProgressPage() {
|
||||||
</h2>
|
</h2>
|
||||||
<Tag
|
<Tag
|
||||||
color={getStatusColor(status.status)}
|
color={getStatusColor(status.status)}
|
||||||
className="font-bold uppercase"
|
className="font-bold uppercase">
|
||||||
>
|
|
||||||
{status.status}
|
{status.status}
|
||||||
</Tag>
|
</Tag>
|
||||||
</div>
|
</div>
|
||||||
|
@ -126,29 +137,50 @@ export default function LetterProgressPage() {
|
||||||
{/* Details Grid */}
|
{/* Details Grid */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-gray-600">Ticket ID</p>
|
<p className="text-sm text-gray-600">
|
||||||
<p className="font-medium text-[#003366]">{status.ticketId}</p>
|
Ticket ID
|
||||||
|
</p>
|
||||||
|
<p className="font-medium text-[#003366]">
|
||||||
|
{status.ticketId}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-gray-600">Submitted Date</p>
|
<p className="text-sm text-gray-600">
|
||||||
<p className="font-medium text-[#003366]">{status.submittedDate}</p>
|
Submitted Date
|
||||||
|
</p>
|
||||||
|
<p className="font-medium text-[#003366]">
|
||||||
|
{status.submittedDate}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-gray-600">Last Update</p>
|
<p className="text-sm text-gray-600">
|
||||||
<p className="font-medium text-[#003366]">{status.lastUpdate}</p>
|
Last Update
|
||||||
|
</p>
|
||||||
|
<p className="font-medium text-[#003366]">
|
||||||
|
{status.lastUpdate}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-gray-600">Subject</p>
|
<p className="text-sm text-gray-600">
|
||||||
<p className="font-medium text-[#003366]">{status.title}</p>
|
Subject
|
||||||
|
</p>
|
||||||
|
<p className="font-medium text-[#003366]">
|
||||||
|
{status.title}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Progress Timeline */}
|
{/* Progress Timeline */}
|
||||||
<div className="mt-8">
|
<div className="mt-8">
|
||||||
<Steps
|
<Steps
|
||||||
current={status.status === 'pending' ? 0 : status.status === 'in-progress' ? 1 : 2}
|
current={
|
||||||
className="usa-progress"
|
status.status === "pending"
|
||||||
>
|
? 0
|
||||||
|
: status.status === "processing"
|
||||||
|
? 1
|
||||||
|
: 2
|
||||||
|
}
|
||||||
|
className="usa-progress">
|
||||||
<Step
|
<Step
|
||||||
title="Submitted"
|
title="Submitted"
|
||||||
description="Ticket received"
|
description="Ticket received"
|
||||||
|
@ -171,5 +203,5 @@ export default function LetterProgressPage() {
|
||||||
)}
|
)}
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@ import { useTusUpload } from "@web/src/hooks/useTusUpload";
|
||||||
|
|
||||||
interface FileUploaderProps {
|
interface FileUploaderProps {
|
||||||
endpoint?: string;
|
endpoint?: string;
|
||||||
onSuccess?: (url: string) => void;
|
onSuccess?: (result: { url: string; fileId: string }) => void;
|
||||||
onError?: (error: Error) => void;
|
onError?: (error: Error) => void;
|
||||||
maxSize?: number;
|
maxSize?: number;
|
||||||
allowedTypes?: string[];
|
allowedTypes?: string[];
|
||||||
|
@ -44,7 +44,7 @@ const FileItem: React.FC<FileItemProps> = memo(
|
||||||
onClick={() => onRemove(file.name)}
|
onClick={() => onRemove(file.name)}
|
||||||
className="opacity-0 group-hover:opacity-100 transition-opacity p-1 hover:bg-gray-100 rounded-full"
|
className="opacity-0 group-hover:opacity-100 transition-opacity p-1 hover:bg-gray-100 rounded-full"
|
||||||
aria-label={`Remove ${file.name}`}>
|
aria-label={`Remove ${file.name}`}>
|
||||||
<XMarkIcon className="w-5 h-5 text-tertiary-300" />
|
<XMarkIcon className="w-5 h-5 text-gray-500" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{!isUploaded && progress !== undefined && (
|
{!isUploaded && progress !== undefined && (
|
||||||
|
@ -57,7 +57,7 @@ const FileItem: React.FC<FileItemProps> = memo(
|
||||||
transition={{ duration: 0.3 }}
|
transition={{ duration: 0.3 }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs text-tertiary-300 mt-1">
|
<span className="text-xs text-gray-500 mt-1">
|
||||||
{progress}%
|
{progress}%
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -74,7 +74,6 @@ const FileItem: React.FC<FileItemProps> = memo(
|
||||||
);
|
);
|
||||||
|
|
||||||
const FileUploader: React.FC<FileUploaderProps> = ({
|
const FileUploader: React.FC<FileUploaderProps> = ({
|
||||||
endpoint = "",
|
|
||||||
onSuccess,
|
onSuccess,
|
||||||
onError,
|
onError,
|
||||||
maxSize = 100,
|
maxSize = 100,
|
||||||
|
@ -125,7 +124,12 @@ const FileUploader: React.FC<FileUploaderProps> = ({
|
||||||
handleFileUpload(
|
handleFileUpload(
|
||||||
file,
|
file,
|
||||||
(upload) => {
|
(upload) => {
|
||||||
onSuccess?.(upload.url || "");
|
console.log("Upload complete:", {
|
||||||
|
url: upload.url,
|
||||||
|
fileId: upload.fileId,
|
||||||
|
// resource: upload.resource
|
||||||
|
});
|
||||||
|
onSuccess?.(upload);
|
||||||
setFiles((prev) =>
|
setFiles((prev) =>
|
||||||
prev.map((f) =>
|
prev.map((f) =>
|
||||||
f.file.name === file.name
|
f.file.name === file.name
|
||||||
|
@ -185,7 +189,8 @@ const FileUploader: React.FC<FileUploaderProps> = ({
|
||||||
relative flex flex-col items-center justify-center w-full h-32
|
relative flex flex-col items-center justify-center w-full h-32
|
||||||
border-2 border-dashed rounded-lg cursor-pointer
|
border-2 border-dashed rounded-lg cursor-pointer
|
||||||
transition-colors duration-200 ease-in-out
|
transition-colors duration-200 ease-in-out
|
||||||
${isDragging
|
${
|
||||||
|
isDragging
|
||||||
? "border-blue-500 bg-blue-50"
|
? "border-blue-500 bg-blue-50"
|
||||||
: "border-gray-300 hover:border-blue-500"
|
: "border-gray-300 hover:border-blue-500"
|
||||||
}
|
}
|
||||||
|
@ -198,8 +203,8 @@ const FileUploader: React.FC<FileUploaderProps> = ({
|
||||||
accept={allowedTypes.join(",")}
|
accept={allowedTypes.join(",")}
|
||||||
className="hidden"
|
className="hidden"
|
||||||
/>
|
/>
|
||||||
<CloudArrowUpIcon className="w-10 h-10 text-tertiary-300" />
|
<CloudArrowUpIcon className="w-10 h-10 text-gray-400" />
|
||||||
<p className="mt-2 text-sm text-tertiary-300">{placeholder}</p>
|
<p className="mt-2 text-sm text-gray-500">{placeholder}</p>
|
||||||
{isDragging && (
|
{isDragging && (
|
||||||
<div className="absolute inset-0 flex items-center justify-center bg-blue-100/50 rounded-lg">
|
<div className="absolute inset-0 flex items-center justify-center bg-blue-100/50 rounded-lg">
|
||||||
<p className="text-blue-500 font-medium">
|
<p className="text-blue-500 font-medium">
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { UploadOutlined } from "@ant-design/icons";
|
||||||
|
import { Form, Upload, message } from "antd";
|
||||||
|
|
||||||
|
export const TusUploader = ({ value = [], onChange }) => {
|
||||||
|
return <Upload.Dragger></Upload.Dragger>;
|
||||||
|
};
|
|
@ -7,6 +7,8 @@ import { useVisitor } from "@nice/client";
|
||||||
import { useContext, useState } from "react";
|
import { useContext, useState } from "react";
|
||||||
import { PostDetailContext } from "./context/PostDetailContext";
|
import { PostDetailContext } from "./context/PostDetailContext";
|
||||||
import { LikeFilled, LikeOutlined } from "@ant-design/icons";
|
import { LikeFilled, LikeOutlined } from "@ant-design/icons";
|
||||||
|
import PostLikeButton from "./PostHeader/PostLikeButton";
|
||||||
|
import { CustomAvatar } from "@web/src/components/presentation/CustomAvatar";
|
||||||
|
|
||||||
export default function PostCommentCard({
|
export default function PostCommentCard({
|
||||||
post,
|
post,
|
||||||
|
@ -55,27 +57,25 @@ export default function PostCommentCard({
|
||||||
<motion.div
|
<motion.div
|
||||||
className="bg-white rounded-lg shadow-sm border border-slate-200 p-4"
|
className="bg-white rounded-lg shadow-sm border border-slate-200 p-4"
|
||||||
layout>
|
layout>
|
||||||
<div className="flex items-start space-x-3">
|
<div className="flex items-start space-x-3 gap-4">
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
<Avatar
|
<CustomAvatar
|
||||||
className="ring-2 ring-white hover:ring-[#00538E]/90
|
|
||||||
transition-all duration-200 ease-in-out shadow-md
|
|
||||||
hover:shadow-lg"
|
|
||||||
src={post.author?.avatar}
|
src={post.author?.avatar}
|
||||||
size={40}>
|
size={40}
|
||||||
{!post.author?.avatar &&
|
name={
|
||||||
(post.author?.showname || "匿名用户")}
|
!post.author?.avatar && post.author?.showname
|
||||||
</Avatar>
|
}></CustomAvatar>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div
|
<div className="flex flex-1 justify-between">
|
||||||
className="flex items-center space-x-2"
|
<div className="flex space-x-2" style={{ height: 40 }}>
|
||||||
style={{ height: 40 }}>
|
|
||||||
<span className="font-medium text-slate-900">
|
<span className="font-medium text-slate-900">
|
||||||
{post.author?.showname || "匿名用户"}
|
{post.author?.showname || "匿名用户"}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-sm text-slate-500">
|
<span className="text-sm text-slate-500">
|
||||||
{dayjs(post?.createdAt).format("YYYY-MM-DD HH:mm")}
|
{dayjs(post?.createdAt).format(
|
||||||
|
"YYYY-MM-DD HH:mm"
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
{isReceiverComment && (
|
{isReceiverComment && (
|
||||||
<span className="inline-flex items-center px-2 py-1 text-xs font-medium rounded-full bg-blue-100 text-blue-800">
|
<span className="inline-flex items-center px-2 py-1 text-xs font-medium rounded-full bg-blue-100 text-blue-800">
|
||||||
|
@ -83,6 +83,10 @@ export default function PostCommentCard({
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{/* 添加有帮助按钮 */}
|
||||||
|
<PostLikeButton post={post}></PostLikeButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="ql-editor text-slate-800"
|
className="ql-editor text-slate-800"
|
||||||
style={{
|
style={{
|
||||||
|
@ -90,23 +94,6 @@ export default function PostCommentCard({
|
||||||
}}
|
}}
|
||||||
dangerouslySetInnerHTML={{ __html: post.content || "" }}
|
dangerouslySetInnerHTML={{ __html: post.content || "" }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 添加有帮助按钮 */}
|
|
||||||
<div className="mt-3 flex items-center">
|
|
||||||
<motion.button
|
|
||||||
onClick={likeThisPost}
|
|
||||||
whileHover={{ scale: 1.05 }}
|
|
||||||
whileTap={{ scale: 0.95 }}
|
|
||||||
className={`inline-flex items-center space-x-1.5 px-3 py-1.5 rounded-full text-sm
|
|
||||||
${
|
|
||||||
liked
|
|
||||||
? "bg-blue-50 text-blue-600"
|
|
||||||
: "hover:bg-slate-50 text-slate-600"
|
|
||||||
} transition-colors duration-200`}>
|
|
||||||
{liked ? <LikeFilled /> : <LikeOutlined />}
|
|
||||||
<span>{likeCount} 有帮助</span>
|
|
||||||
</motion.button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
|
@ -1,14 +1,15 @@
|
||||||
import React, { useContext, useState } from "react";
|
import React, { useContext, useState } from "react";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import { PaperAirplaneIcon } from "@heroicons/react/24/solid";
|
|
||||||
import { CommandLineIcon } from "@heroicons/react/24/outline";
|
import { Button } from "antd";
|
||||||
import QuillEditor from "@web/src/components/common/editor/quill/QuillEditor";
|
import QuillEditor from "@web/src/components/common/editor/quill/QuillEditor";
|
||||||
import { PostDetailContext } from "./context/PostDetailContext";
|
import { PostDetailContext } from "./context/PostDetailContext";
|
||||||
import { usePost } from "@nice/client";
|
import { usePost } from "@nice/client";
|
||||||
import { PostType } from "@nice/common";
|
import { PostType } from "@nice/common";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import { isContentEmpty } from "./utils";
|
import { isContentEmpty } from "./utils";
|
||||||
|
import { SendOutlined } from "@ant-design/icons";
|
||||||
|
import { TusUploader } from "@web/src/components/common/uploader/TusUploader";
|
||||||
export default function PostCommentEditor() {
|
export default function PostCommentEditor() {
|
||||||
const { post } = useContext(PostDetailContext);
|
const { post } = useContext(PostDetailContext);
|
||||||
const [content, setContent] = useState("");
|
const [content, setContent] = useState("");
|
||||||
|
@ -82,36 +83,21 @@ export default function PostCommentEditor() {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-end">
|
<TusUploader></TusUploader>
|
||||||
{/* <motion.button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setIsPreview(!isPreview)}
|
|
||||||
whileHover={{ scale: 1.02 }}
|
|
||||||
whileTap={{ scale: 0.98 }}
|
|
||||||
className={`flex items-center space-x-1 px-3 py-1.5 rounded-md
|
|
||||||
transition-colors ${
|
|
||||||
isPreview
|
|
||||||
? "bg-blue-600 text-white"
|
|
||||||
: "bg-slate-100 text-slate-600 hover:bg-slate-200"
|
|
||||||
}`}>
|
|
||||||
<CommandLineIcon className="w-5 h-5" />
|
|
||||||
<span>{isPreview ? "编辑" : "预览"}</span>
|
|
||||||
</motion.button> */}
|
|
||||||
|
|
||||||
<motion.button
|
<div className="flex items-center justify-end">
|
||||||
type="submit"
|
<motion.div
|
||||||
disabled={isContentEmpty(content)}
|
|
||||||
whileHover={{ scale: 1.02 }}
|
whileHover={{ scale: 1.02 }}
|
||||||
whileTap={{ scale: 0.98 }}
|
whileTap={{ scale: 0.98 }}>
|
||||||
className={`flex items-center space-x-2 px-4 py-2 rounded-md
|
<Button
|
||||||
${
|
type="primary"
|
||||||
!isContentEmpty(content)
|
htmlType="submit"
|
||||||
? "bg-blue-600 text-white hover:bg-blue-700"
|
disabled={isContentEmpty(content)}
|
||||||
: "bg-slate-100 text-slate-400 cursor-not-allowed"
|
className="flex items-center space-x-2 bg-primary"
|
||||||
} transition-colors`}>
|
icon={<SendOutlined />}>
|
||||||
<PaperAirplaneIcon className="w-4 h-4" />
|
提交
|
||||||
<span>提交</span>
|
</Button>
|
||||||
</motion.button>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
|
@ -11,14 +11,21 @@ import {
|
||||||
EyeIcon,
|
EyeIcon,
|
||||||
ChatBubbleLeftIcon,
|
ChatBubbleLeftIcon,
|
||||||
} from "@heroicons/react/24/outline";
|
} from "@heroicons/react/24/outline";
|
||||||
|
import { Button, Typography, Space, Tooltip } from "antd";
|
||||||
import { useVisitor } from "@nice/client";
|
import { useVisitor } from "@nice/client";
|
||||||
import { PostState, VisitType } from "@nice/common";
|
import { PostState, VisitType } from "@nice/common";
|
||||||
import {
|
import {
|
||||||
|
CalendarOutlined,
|
||||||
|
ClockCircleOutlined,
|
||||||
CommentOutlined,
|
CommentOutlined,
|
||||||
EyeOutlined,
|
EyeOutlined,
|
||||||
|
FileTextOutlined,
|
||||||
|
FolderOutlined,
|
||||||
LikeFilled,
|
LikeFilled,
|
||||||
LikeOutlined,
|
LikeOutlined,
|
||||||
|
LockOutlined,
|
||||||
|
UnlockOutlined,
|
||||||
|
UserOutlined,
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { TitleSection } from "./PostHeader/TitleSection";
|
import { TitleSection } from "./PostHeader/TitleSection";
|
||||||
|
@ -30,7 +37,9 @@ import {
|
||||||
VisibilityBadge,
|
VisibilityBadge,
|
||||||
} from "./PostHeader/InfoBadge";
|
} from "./PostHeader/InfoBadge";
|
||||||
import { StatsSection } from "./PostHeader/StatsSection";
|
import { StatsSection } from "./PostHeader/StatsSection";
|
||||||
|
import { PostBadge } from "./badge/PostBadge";
|
||||||
|
|
||||||
|
const { Title, Paragraph, Text } = Typography;
|
||||||
export default function PostHeader() {
|
export default function PostHeader() {
|
||||||
const { post, user } = useContext(PostDetailContext);
|
const { post, user } = useContext(PostDetailContext);
|
||||||
const { like, unLike } = useVisitor();
|
const { like, unLike } = useVisitor();
|
||||||
|
@ -64,62 +73,76 @@ export default function PostHeader() {
|
||||||
initial={{ opacity: 0, y: -20 }}
|
initial={{ opacity: 0, y: -20 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ duration: 0.5 }}
|
transition={{ duration: 0.5 }}
|
||||||
className="relative bg-gradient-to-br from-[#E6E9F0] via-[#EDF0F8] to-[#D8E2EF] rounded-lg p-6 shadow-lg border border-[#97A9C4]/30">
|
className="relative bg-gradient-to-br from-primary-250 via-primary-150 to--primary-350 rounded-lg p-6 shadow-lg border border-[#97A9C4]/30">
|
||||||
{/* Corner Decorations */}
|
{/* Corner Decorations */}
|
||||||
<div className="absolute top-0 left-0 w-5 h-5 border-t-2 border-l-2 border-[#97A9C4] rounded-tl-lg" />
|
<div className="absolute top-0 left-0 w-5 h-5 border-t-4 border-l-4 border-primary rounded-tl-lg" />
|
||||||
<div className="absolute bottom-0 right-0 w-5 h-5 border-b-2 border-r-2 border-[#97A9C4] rounded-br-lg" />
|
<div className="absolute bottom-0 right-0 w-5 h-5 border-b-4 border-r-4 border-primary rounded-br-lg" />
|
||||||
|
|
||||||
{/* Title Section */}
|
{/* Title Section */}
|
||||||
|
|
||||||
<TitleSection
|
<TitleSection></TitleSection>
|
||||||
title={post?.title}
|
|
||||||
state={post?.state as PostState}></TitleSection>
|
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* First Row - Basic Info */}
|
{/* 收件人信息行 */}
|
||||||
<div className="flex flex-wrap gap-4">
|
<Space>
|
||||||
{/* Author Info Badge */}
|
<UserOutlined className="text-secondary-400" />
|
||||||
<AuthorBadge
|
<span className="text-secondary-400">收件人:</span>
|
||||||
name={
|
<Text strong>
|
||||||
post?.author?.showname || "匿名用户"
|
{post?.receivers?.map((receiver) => receiver?.showname)}
|
||||||
}></AuthorBadge>
|
</Text>
|
||||||
|
</Space>
|
||||||
|
|
||||||
|
{/* First Row - Basic Info */}
|
||||||
|
<div className="flex flex-wrap items-center gap-1">
|
||||||
|
{/* Author Info Badge */}
|
||||||
|
<Space>
|
||||||
|
<UserOutlined className="text-secondary-400" />
|
||||||
|
<span className="text-secondary-400">发件人:</span>
|
||||||
|
<Text strong>
|
||||||
|
{post?.author?.showname || "匿名用户"}
|
||||||
|
</Text>
|
||||||
|
</Space>
|
||||||
|
<Text type="secondary">|</Text>
|
||||||
{/* Date Info Badge */}
|
{/* Date Info Badge */}
|
||||||
{post?.createdAt && (
|
<Space>
|
||||||
<DateBadge
|
<CalendarOutlined className="text-secondary-400" />
|
||||||
date={dayjs(post?.createdAt).format("YYYY-MM-DD")}
|
|
||||||
label="创建于:"></DateBadge>
|
<Text>
|
||||||
)}
|
创建于:
|
||||||
|
{dayjs(post?.createdAt).format("YYYY-MM-DD")}
|
||||||
|
</Text>
|
||||||
|
</Space>
|
||||||
|
<Text type="secondary">|</Text>
|
||||||
{/* Last Updated Badge */}
|
{/* Last Updated Badge */}
|
||||||
{post?.updatedAt && post.updatedAt !== post.createdAt && (
|
<Space>
|
||||||
<UpdatedBadge
|
<ClockCircleOutlined className="text-secondary-400" />
|
||||||
date={dayjs(post?.updatedAt).format(
|
<Text>
|
||||||
"YYYY-MM-DD"
|
更新于:
|
||||||
)}></UpdatedBadge>
|
{dayjs(post?.updatedAt).format("YYYY-MM-DD")}
|
||||||
)}
|
</Text>
|
||||||
|
</Space>
|
||||||
|
<Text type="secondary">|</Text>
|
||||||
{/* Visibility Status Badge */}
|
{/* Visibility Status Badge */}
|
||||||
<VisibilityBadge
|
<Space>
|
||||||
isPublic={post?.isPublic}></VisibilityBadge>
|
{post?.isPublic ? (
|
||||||
|
<UnlockOutlined className="text-secondary-400" />
|
||||||
|
) : (
|
||||||
|
<LockOutlined className="text-secondary-400" />
|
||||||
|
)}
|
||||||
|
<Text>{post?.isPublic ? "公开" : "私信"}</Text>
|
||||||
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
{/* Second Row - Term and Tags */}
|
{/* Second Row - Term and Tags */}
|
||||||
<div className="flex flex-wrap gap-4">
|
<div className="flex flex-wrap gap-1">
|
||||||
{/* Term Badge */}
|
|
||||||
{post?.term?.name && (
|
|
||||||
<TermBadge term={post.term.name}></TermBadge>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Tags Badges */}
|
{/* Tags Badges */}
|
||||||
{post?.meta?.tags &&
|
{post?.meta?.tags &&
|
||||||
post.meta.tags.length > 0 &&
|
post.meta.tags.length > 0 &&
|
||||||
post.meta.tags.map((tag, index) => (
|
post.meta.tags.map((tag, index) => (
|
||||||
<motion.span
|
<Space key={index}>
|
||||||
key={index}
|
<PostBadge
|
||||||
whileHover={{ scale: 1.05 }}
|
type="tag"
|
||||||
className="inline-flex items-center bg-[#507AAF]/10 px-3 py-1.5 rounded border border-[#97A9C4]/50 shadow-md hover:bg-[#507AAF]/20">
|
value={`#${tag}`}></PostBadge>
|
||||||
<span className="text-sm text-[#2B4C7E]">
|
</Space>
|
||||||
#{tag}
|
|
||||||
</span>
|
|
||||||
</motion.span>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -129,20 +152,15 @@ export default function PostHeader() {
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
transition={{ delay: 0.6 }}
|
transition={{ delay: 0.6 }}
|
||||||
className="mt-6 text-[#2B4C7E]">
|
className="mt-6 text-secondary-700">
|
||||||
<div
|
<div
|
||||||
className="ql-editor space-y-4 leading-relaxed bg-white/60 p-4 rounded-md border border-[#97A9C4]/30 shadow-inner hover:bg-white/80 transition-colors duration-300"
|
className="ql-editor p-0 space-y-4 leading-relaxed duration-300"
|
||||||
dangerouslySetInnerHTML={{ __html: post?.content || "" }}
|
dangerouslySetInnerHTML={{ __html: post?.content || "" }}
|
||||||
/>
|
/>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{/* Stats Section */}
|
{/* Stats Section */}
|
||||||
<StatsSection
|
<StatsSection></StatsSection>
|
||||||
likes={post?.likes}
|
|
||||||
views={post?.views}
|
|
||||||
commentsCount={post?.commentsCount}
|
|
||||||
liked={post?.liked}
|
|
||||||
onLikeClick={likeThisPost}></StatsSection>
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,11 +21,10 @@ export function InfoBadge({ icon, text, delay = 0 }: InfoBadgeProps) {
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, x: -20 }}
|
initial={{ opacity: 0, x: -20 }}
|
||||||
animate={{ opacity: 1, x: 0 }}
|
animate={{ opacity: 1, x: 0 }}
|
||||||
transition={{ delay }}
|
|
||||||
whileHover={{ scale: 1.05 }}
|
whileHover={{ scale: 1.05 }}
|
||||||
className="flex items-center gap-2 bg-white px-3 py-1.5 rounded-md border border-[#97A9C4]/50 shadow-md hover:bg-[#F8FAFC] transition-colors duration-300">
|
className="flex items-center gap-2 bg-white px-3 py-1.5 rounded-md border border-secondary-350/50 shadow-md hover:bg-secondary-50 transition-colors duration-300">
|
||||||
{icon}
|
{icon}
|
||||||
<span className="text-[#2B4C7E]">{text}</span>
|
<span className="text-primary">{text}</span>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -33,7 +32,7 @@ export function InfoBadge({ icon, text, delay = 0 }: InfoBadgeProps) {
|
||||||
export function AuthorBadge({ name }: { name: string }) {
|
export function AuthorBadge({ name }: { name: string }) {
|
||||||
return (
|
return (
|
||||||
<InfoBadge
|
<InfoBadge
|
||||||
icon={<UserCircleIcon className="h-5 w-5 text-[#2B4C7E]" />}
|
icon={<UserCircleIcon className="h-5 w-5 text-primary" />}
|
||||||
text={name}
|
text={name}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -42,7 +41,7 @@ export function AuthorBadge({ name }: { name: string }) {
|
||||||
export function DateBadge({ date, label }: { date: string; label: string }) {
|
export function DateBadge({ date, label }: { date: string; label: string }) {
|
||||||
return (
|
return (
|
||||||
<InfoBadge
|
<InfoBadge
|
||||||
icon={<CalendarIcon className="h-5 w-5 text-[#2B4C7E]" />}
|
icon={<CalendarIcon className="h-5 w-5 text-primary" />}
|
||||||
text={`${label}: ${dayjs(date).format("YYYY-MM-DD")}`}
|
text={`${label}: ${dayjs(date).format("YYYY-MM-DD")}`}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -51,7 +50,7 @@ export function DateBadge({ date, label }: { date: string; label: string }) {
|
||||||
export function UpdatedBadge({ date }: { date: string }) {
|
export function UpdatedBadge({ date }: { date: string }) {
|
||||||
return (
|
return (
|
||||||
<InfoBadge
|
<InfoBadge
|
||||||
icon={<ClockIcon className="h-5 w-5 text-[#2B4C7E]" />}
|
icon={<ClockIcon className="h-5 w-5 text-primary" />}
|
||||||
text={`更新于: ${dayjs(date).format("YYYY-MM-DD")}`}
|
text={`更新于: ${dayjs(date).format("YYYY-MM-DD")}`}
|
||||||
delay={0.45}
|
delay={0.45}
|
||||||
/>
|
/>
|
||||||
|
@ -63,9 +62,9 @@ export function VisibilityBadge({ isPublic }: { isPublic: boolean }) {
|
||||||
<InfoBadge
|
<InfoBadge
|
||||||
icon={
|
icon={
|
||||||
isPublic ? (
|
isPublic ? (
|
||||||
<LockOpenIcon className="h-5 w-5 text-[#2B4C7E]" />
|
<LockOpenIcon className="h-5 w-5 text-primary" />
|
||||||
) : (
|
) : (
|
||||||
<LockClosedIcon className="h-5 w-5 text-[#2B4C7E]" />
|
<LockClosedIcon className="h-5 w-5 text-primary" />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
text={isPublic ? "公开" : "私信"}
|
text={isPublic ? "公开" : "私信"}
|
||||||
|
@ -77,7 +76,7 @@ export function VisibilityBadge({ isPublic }: { isPublic: boolean }) {
|
||||||
export function TermBadge({ term }: { term: string }) {
|
export function TermBadge({ term }: { term: string }) {
|
||||||
return (
|
return (
|
||||||
<InfoBadge
|
<InfoBadge
|
||||||
icon={<StarIcon className="h-5 w-5 text-[#2B4C7E]" />}
|
icon={<StarIcon className="h-5 w-5 text-primary" />}
|
||||||
text={term}
|
text={term}
|
||||||
delay={0.55}
|
delay={0.55}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
import { PostDto, VisitType } from "@nice/common";
|
||||||
|
import { useVisitor } from "@nice/client";
|
||||||
|
import { useContext } from "react";
|
||||||
|
import { PostDetailContext } from "../context/PostDetailContext";
|
||||||
|
import { Button, Tooltip } from "antd";
|
||||||
|
import { LikeFilled, LikeOutlined } from "@ant-design/icons";
|
||||||
|
|
||||||
|
export default function PostLikeButton({ post }: { post: PostDto }) {
|
||||||
|
const { user } = useContext(PostDetailContext);
|
||||||
|
const { like, unLike } = useVisitor();
|
||||||
|
|
||||||
|
function likeThisPost() {
|
||||||
|
if (!post?.liked) {
|
||||||
|
post.likes += 1;
|
||||||
|
post.liked = true;
|
||||||
|
like.mutateAsync({
|
||||||
|
data: {
|
||||||
|
visitorId: user?.id || null,
|
||||||
|
postId: post.id,
|
||||||
|
type: VisitType.LIKE,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
post.likes -= 1;
|
||||||
|
post.liked = false;
|
||||||
|
unLike.mutateAsync({
|
||||||
|
where: {
|
||||||
|
visitorId: user?.id || null,
|
||||||
|
postId: post.id,
|
||||||
|
type: VisitType.LIKE,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Tooltip title={post?.liked ? "取消点赞" : "点赞"} placement="top">
|
||||||
|
<Button
|
||||||
|
type={post?.liked ? "primary" : "default"}
|
||||||
|
shape="round"
|
||||||
|
size="small"
|
||||||
|
icon={post?.liked ? <LikeFilled /> : <LikeOutlined />}
|
||||||
|
onClick={likeThisPost}
|
||||||
|
className={`
|
||||||
|
flex items-center gap-1 px-3 transform transition-all duration-300
|
||||||
|
hover:scale-105 hover:shadow-md
|
||||||
|
${post?.liked ? "bg-blue-500 hover:bg-blue-600" : "hover:border-blue-500 hover:text-blue-500"}
|
||||||
|
`}>
|
||||||
|
<span className={post?.liked ? "text-white" : ""}>
|
||||||
|
{post?.likes}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,68 +1,37 @@
|
||||||
import React from "react";
|
import React, { useContext } from "react";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import { LikeFilled, LikeOutlined, EyeOutlined, CommentOutlined } from "@ant-design/icons";
|
import {
|
||||||
|
LikeFilled,
|
||||||
|
LikeOutlined,
|
||||||
|
EyeOutlined,
|
||||||
|
CommentOutlined,
|
||||||
|
} 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";
|
||||||
|
export function StatsSection() {
|
||||||
|
const { post, user } = useContext(PostDetailContext);
|
||||||
|
const { like, unLike } = useVisitor();
|
||||||
|
|
||||||
interface StatsButtonProps {
|
|
||||||
icon: React.ReactNode;
|
|
||||||
text: string;
|
|
||||||
onClick?: () => void;
|
|
||||||
isActive?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function StatsButton({ icon, text, onClick, isActive }: StatsButtonProps) {
|
|
||||||
return (
|
|
||||||
<motion.button
|
|
||||||
whileHover={{ scale: 1.05 }}
|
|
||||||
whileTap={{ scale: 0.95 }}
|
|
||||||
onClick={onClick}
|
|
||||||
className={`flex items-center gap-2 px-4 py-2 rounded-md ${
|
|
||||||
isActive
|
|
||||||
? "bg-[#507AAF] text-white"
|
|
||||||
: "bg-white text-[#2B4C7E] hover:bg-[#507AAF] hover:text-white"
|
|
||||||
} transition-all duration-300 shadow-md border border-[#97A9C4]/30`}
|
|
||||||
>
|
|
||||||
{icon}
|
|
||||||
<span className="font-medium">{text}</span>
|
|
||||||
</motion.button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface StatsSectionProps {
|
|
||||||
likes: number;
|
|
||||||
views: number;
|
|
||||||
commentsCount: number;
|
|
||||||
liked: boolean;
|
|
||||||
onLikeClick: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function StatsSection({
|
|
||||||
likes,
|
|
||||||
views,
|
|
||||||
commentsCount,
|
|
||||||
liked,
|
|
||||||
onLikeClick,
|
|
||||||
}: StatsSectionProps) {
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ delay: 0.7 }}
|
transition={{ delay: 0.7 }}
|
||||||
className="mt-6 flex flex-wrap gap-4 justify-start items-center"
|
className="mt-6 flex flex-wrap gap-4 justify-between items-center">
|
||||||
>
|
<div className=" flex gap-2">
|
||||||
<StatsButton
|
<div className="flex items-center gap-1 text-gray-500">
|
||||||
icon={liked ? <LikeFilled style={{ fontSize: 18 }} /> : <LikeOutlined style={{ fontSize: 18 }} />}
|
<EyeOutlined className="text-lg" />
|
||||||
text={`${likes} 有帮助`}
|
<span className="text-sm">{post?.views}</span>
|
||||||
onClick={onLikeClick}
|
</div>
|
||||||
isActive={liked}
|
<div className="flex items-center gap-1 text-gray-500">
|
||||||
/>
|
<CommentOutlined className="text-lg" />
|
||||||
<StatsButton
|
<span className="text-sm">{post?.commentsCount}</span>
|
||||||
icon={<EyeOutlined style={{ fontSize: 18 }} />}
|
</div>
|
||||||
text={`${views} 浏览`}
|
</div>
|
||||||
/>
|
<PostLikeButton post={post}></PostLikeButton>
|
||||||
<StatsButton
|
|
||||||
icon={<CommentOutlined style={{ fontSize: 18 }} />}
|
|
||||||
text={`${commentsCount} 回复`}
|
|
||||||
/>
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,30 +1,29 @@
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import { Tag } from "antd";
|
import { Space, Tag } from "antd";
|
||||||
import { PostState } from "@nice/common";
|
import { PostState } from "@nice/common";
|
||||||
import {
|
import {
|
||||||
ClockIcon,
|
ClockIcon,
|
||||||
CheckCircleIcon,
|
CheckCircleIcon,
|
||||||
ExclamationCircleIcon,
|
ExclamationCircleIcon,
|
||||||
} from "@heroicons/react/24/outline";
|
} from "@heroicons/react/24/outline";
|
||||||
|
import { Badge } from "@web/src/app/main/letter/list/LetterCard";
|
||||||
|
import { useContext } from "react";
|
||||||
|
import { PostDetailContext } from "../context/PostDetailContext";
|
||||||
|
import { PostBadge } from "../badge/PostBadge";
|
||||||
|
|
||||||
interface TitleSectionProps {
|
interface TitleSectionProps {
|
||||||
title: string;
|
title: string;
|
||||||
state: PostState;
|
state: PostState;
|
||||||
}
|
}
|
||||||
|
|
||||||
const stateColors = {
|
|
||||||
[PostState.PENDING]: "orange",
|
|
||||||
[PostState.PROCESSING]: "blue",
|
|
||||||
[PostState.COMPLETED]: "green",
|
|
||||||
};
|
|
||||||
|
|
||||||
const stateLabels = {
|
const stateLabels = {
|
||||||
[PostState.PENDING]: "待处理",
|
[PostState.PENDING]: "待处理",
|
||||||
[PostState.PROCESSING]: "处理中",
|
[PostState.PROCESSING]: "处理中",
|
||||||
[PostState.COMPLETED]: "已完成",
|
[PostState.RESOLVED]: "已完成",
|
||||||
};
|
};
|
||||||
|
|
||||||
export function TitleSection({ title, state }: TitleSectionProps) {
|
export function TitleSection() {
|
||||||
|
const { post, user } = useContext(PostDetailContext);
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
|
@ -32,34 +31,17 @@ export function TitleSection({ title, state }: TitleSectionProps) {
|
||||||
transition={{ delay: 0.2 }}
|
transition={{ delay: 0.2 }}
|
||||||
className="relative mb-6 flex items-center gap-4">
|
className="relative mb-6 flex items-center gap-4">
|
||||||
{/* Decorative Line */}
|
{/* Decorative Line */}
|
||||||
<div className="absolute -left-2 top-1/2 -translate-y-1/2 w-1 h-8 bg-[#97A9C4]" />
|
{/* <div className="absolute -left-2 top-1/2 -translate-y-1/2 w-1 h-8 bg-primary" /> */}
|
||||||
|
|
||||||
{/* Title */}
|
{/* Title */}
|
||||||
<h1 className="text-2xl font-bold text-[#2B4C7E] pl-4 tracking-wider uppercase">
|
<h1 className="text-xl font-bold text-primary tracking-wider uppercase">
|
||||||
{title}
|
{post?.title}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
{/* State Tag */}
|
<Space size="small" wrap className="flex-1">
|
||||||
{/* <motion.div
|
<PostBadge type="category" value={post?.term?.name} />
|
||||||
initial={{ opacity: 0, x: -20 }}
|
<PostBadge type="state" value={post?.state} />
|
||||||
animate={{ opacity: 1, x: 0 }}
|
</Space>
|
||||||
// transition={{ delay: 0.45 }}
|
|
||||||
whileHover={{ scale: 1.05 }}
|
|
||||||
className="flex items-center gap-2 bg-white px-3 py-1.5 rounded-md border border-[#97A9C4]/50 shadow-md hover:bg-[#F8FAFC] transition-colors duration-300"> */}
|
|
||||||
<Tag
|
|
||||||
color={stateColors[state]}
|
|
||||||
className="flex items-center gap-1.5 px-3 py-1 shadow-md rounded-md text-sm font-medium border-none">
|
|
||||||
{state === PostState.PENDING && (
|
|
||||||
<ExclamationCircleIcon className="h-4 w-4" />
|
|
||||||
)}
|
|
||||||
{state === PostState.PROCESSING && (
|
|
||||||
<ClockIcon className="h-4 w-4" />
|
|
||||||
)}
|
|
||||||
{state === PostState.COMPLETED && (
|
|
||||||
<CheckCircleIcon className="h-4 w-4" />
|
|
||||||
)}
|
|
||||||
{stateLabels[state]}
|
|
||||||
</Tag>
|
|
||||||
{/* </motion.div> */}
|
{/* </motion.div> */}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -0,0 +1,62 @@
|
||||||
|
import { PostState ,PostStateLabels} from "@nice/common";
|
||||||
|
|
||||||
|
export function PostBadge({
|
||||||
|
type,
|
||||||
|
value,
|
||||||
|
className = "",
|
||||||
|
}: {
|
||||||
|
type: "priority" | "category" | "state" | "tag";
|
||||||
|
value: string;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
value && (
|
||||||
|
<span
|
||||||
|
className={`
|
||||||
|
inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
|
||||||
|
${getBadgeStyle(type, value)}
|
||||||
|
transition-all duration-200 ease-in-out transform hover:scale-105
|
||||||
|
${className}
|
||||||
|
`}>
|
||||||
|
{type === "state" ? PostStateLabels?.[value] : value?.toUpperCase()}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const BADGE_STYLES = {
|
||||||
|
priority: {
|
||||||
|
high: "bg-red-100 text-red-800",
|
||||||
|
medium: "bg-yellow-100 text-yellow-800",
|
||||||
|
low: "bg-green-100 text-green-800",
|
||||||
|
},
|
||||||
|
category: {
|
||||||
|
complaint: "bg-orange-100 text-orange-800",
|
||||||
|
suggestion: "bg-blue-100 text-blue-800",
|
||||||
|
request: "bg-purple-100 text-purple-800",
|
||||||
|
feedback: "bg-teal-100 text-teal-800",
|
||||||
|
},
|
||||||
|
state: {
|
||||||
|
[PostState.PENDING]: "bg-yellow-100 text-yellow-800",
|
||||||
|
[PostState.PROCESSING]: "bg-blue-100 text-blue-800",
|
||||||
|
[PostState.RESOLVED]: "bg-green-100 text-green-800",
|
||||||
|
},
|
||||||
|
tag: {
|
||||||
|
_: "bg-primary-100 text-primary-800",
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const getBadgeStyle = (
|
||||||
|
type: keyof typeof BADGE_STYLES,
|
||||||
|
value: string
|
||||||
|
): string => {
|
||||||
|
if (type === "tag") {
|
||||||
|
return "bg-primary-100 text-primary";
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
BADGE_STYLES[type][value as keyof (typeof BADGE_STYLES)[typeof type]] ||
|
||||||
|
"bg-gray-100 text-gray-800"
|
||||||
|
);
|
||||||
|
};
|
|
@ -8,8 +8,9 @@ import {
|
||||||
TagOutlined,
|
TagOutlined,
|
||||||
FileTextOutlined,
|
FileTextOutlined,
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import FileUploader from "@web/src/components/common/uploader/FileUploader";
|
|
||||||
import QuillEditor from "@web/src/components/common/editor/quill/QuillEditor";
|
import QuillEditor from "@web/src/components/common/editor/quill/QuillEditor";
|
||||||
|
import { PostBadge } from "../../detail/badge/PostBadge";
|
||||||
|
|
||||||
export function LetterBasicForm() {
|
export function LetterBasicForm() {
|
||||||
const { onSubmit, receiverId, termId, form } = useLetterEditor();
|
const { onSubmit, receiverId, termId, form } = useLetterEditor();
|
||||||
|
@ -42,15 +43,11 @@ export function LetterBasicForm() {
|
||||||
initialValues={{ meta: { tags: [] }, isPublic: true }}>
|
initialValues={{ meta: { tags: [] }, isPublic: true }}>
|
||||||
{/* 收件人和板块信息行 */}
|
{/* 收件人和板块信息行 */}
|
||||||
<div className="flex justify-start items-center gap-8 ">
|
<div className="flex justify-start items-center gap-8 ">
|
||||||
<div className="flex items-center font-semibold text-[#00308F]">
|
<div className="flex items-center font-semibold text-primary">
|
||||||
<UserOutlined className="w-5 h-5 mr-2 text-[#00308F]" />
|
<UserOutlined className="w-5 h-5 mr-2 text-primary" />
|
||||||
<div>收件人:{receiver?.showname}</div>
|
<div>收件人:{receiver?.showname}</div>
|
||||||
</div>
|
</div>
|
||||||
|
<PostBadge type="category" value={term?.name}></PostBadge>
|
||||||
<div className="flex items-center font-semibold text-[#00308F]">
|
|
||||||
<FolderOutlined className="w-5 h-5 mr-2 text-[#00308F]" />
|
|
||||||
<div>板块:{term?.name}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 主题输入框 */}
|
{/* 主题输入框 */}
|
||||||
|
@ -58,11 +55,13 @@ export function LetterBasicForm() {
|
||||||
<Form.Item
|
<Form.Item
|
||||||
required={false} //不显示星号
|
required={false} //不显示星号
|
||||||
label={
|
label={
|
||||||
<span className="block mb-1">
|
<div className="mb-1 items-center">
|
||||||
<TagOutlined className="w-5 h-5 inline mr-2 text-[#00308F]" />
|
<TagOutlined className="mr-2 text-primary" />
|
||||||
标题
|
标题
|
||||||
<span className="text-gray-400">(必选)</span>
|
<span className="text-secondary-400">
|
||||||
|
(必选)
|
||||||
</span>
|
</span>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
name="title"
|
name="title"
|
||||||
rules={[{ required: true, message: "请输入标题" }]}
|
rules={[{ required: true, message: "请输入标题" }]}
|
||||||
|
@ -77,7 +76,7 @@ export function LetterBasicForm() {
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={
|
label={
|
||||||
<span className="block mb-1">
|
<span className="block mb-1">
|
||||||
<TagOutlined className="w-5 h-5 inline mr-2 text-[#00308F]" />
|
<TagOutlined className=" mr-2 text-primary" />
|
||||||
标签
|
标签
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
|
@ -108,9 +107,11 @@ export function LetterBasicForm() {
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={
|
label={
|
||||||
<span className="block mb-1">
|
<span className="block mb-1">
|
||||||
<FileTextOutlined className="w-5 h-5 inline mr-2 text-[#00308F]" />
|
<FileTextOutlined className=" mr-2 text-primary" />
|
||||||
内容
|
内容
|
||||||
<span className="text-gray-400">(必选)</span>
|
<span className="text-secondary-400">
|
||||||
|
(必选)
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
name="content"
|
name="content"
|
||||||
|
@ -120,7 +121,7 @@ export function LetterBasicForm() {
|
||||||
wrapperCol={{ span: 24 }}>
|
wrapperCol={{ span: 24 }}>
|
||||||
<div className="relative rounded-lg border border-slate-200 bg-white shadow-sm">
|
<div className="relative rounded-lg border border-slate-200 bg-white shadow-sm">
|
||||||
<QuillEditor
|
<QuillEditor
|
||||||
maxLength={400}
|
maxLength={1000}
|
||||||
placeholder="请输入内容"
|
placeholder="请输入内容"
|
||||||
minRows={6}
|
minRows={6}
|
||||||
maxRows={12}
|
maxRows={12}
|
||||||
|
@ -132,9 +133,9 @@ export function LetterBasicForm() {
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* <FileUploader /> */}
|
|
||||||
|
|
||||||
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-end gap-4 border-t border-gray-100 pt-4">
|
|
||||||
|
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-end gap-4 border-t border-secondary-100 pt-4">
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="isPublic"
|
name="isPublic"
|
||||||
valuePropName="checked"
|
valuePropName="checked"
|
||||||
|
@ -146,7 +147,7 @@ export function LetterBasicForm() {
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
onClick={() => form.submit()}
|
onClick={() => form.submit()}
|
||||||
className="bg-[#00308F] hover:bg-[#041E42] w-full sm:w-auto"
|
className="bg-primary hover:bg-primary-600 w-full sm:w-auto"
|
||||||
style={{
|
style={{
|
||||||
transform: "scale(1)",
|
transform: "scale(1)",
|
||||||
transition: "all 0.2s",
|
transition: "all 0.2s",
|
|
@ -3,21 +3,22 @@ import { motion } from "framer-motion";
|
||||||
import { PaperAirplaneIcon } from "@heroicons/react/24/outline";
|
import { PaperAirplaneIcon } from "@heroicons/react/24/outline";
|
||||||
import { LetterFormProvider } from "../context/LetterEditorContext";
|
import { LetterFormProvider } from "../context/LetterEditorContext";
|
||||||
import { LetterBasicForm } from "../form/LetterBasicForm";
|
import { LetterBasicForm } from "../form/LetterBasicForm";
|
||||||
|
import { useTheme } from "@nice/theme";
|
||||||
|
|
||||||
export default function LetterEditorLayout() {
|
export default function LetterEditorLayout() {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const params = new URLSearchParams(location.search);
|
const params = new URLSearchParams(location.search);
|
||||||
|
// const {} = useTheme();
|
||||||
const receiverId = params.get("receiverId");
|
const receiverId = params.get("receiverId");
|
||||||
const termId = params.get("termId");
|
const termId = params.get("termId");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
className="min-h-screen rounded-xl overflow-hidden border border-gray-200 shadow-lg" // 添加圆角和溢出隐藏
|
className="min-h-screen overflow-hidden border border-gray-200 shadow-lg" // 添加圆角和溢出隐藏
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
transition={{ duration: 0.8 }}>
|
transition={{ duration: 0.8 }}>
|
||||||
<div className="bg-gradient-to-r from-[#00308F] to-[#0353A4] text-white py-8">
|
<div className="bg-gradient-to-r from-primary to-primary-400 text-white py-8">
|
||||||
<div className="w-full px-4 max-w-7xl mx-auto">
|
<div className="w-full px-4 max-w-7xl mx-auto">
|
||||||
<motion.div
|
<motion.div
|
||||||
className="flex items-center justify-center mb-6"
|
className="flex items-center justify-center mb-6"
|
||||||
|
@ -28,7 +29,7 @@ export default function LetterEditorLayout() {
|
||||||
type: "spring",
|
type: "spring",
|
||||||
stiffness: 100,
|
stiffness: 100,
|
||||||
}}>
|
}}>
|
||||||
<PaperAirplaneIcon className="h-12 w-12" />
|
{/* <PaperAirplaneIcon className="h-12 w-12" /> */}
|
||||||
<h1 className="text-3xl font-bold ml-4">撰写信件</h1>
|
<h1 className="text-3xl font-bold ml-4">撰写信件</h1>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { Avatar } from "antd";
|
||||||
|
import { AvatarProps } from "antd/lib/avatar";
|
||||||
|
|
||||||
|
interface CustomAvatarProps extends Omit<AvatarProps, "children"> {
|
||||||
|
src?: string;
|
||||||
|
name?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CustomAvatar({
|
||||||
|
src,
|
||||||
|
name,
|
||||||
|
className = "",
|
||||||
|
...props
|
||||||
|
}: CustomAvatarProps) {
|
||||||
|
// 获取名字的第一个字符,如果没有名字则显示"匿"
|
||||||
|
const firstChar = name ? name.charAt(0) : "匿";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Avatar
|
||||||
|
className={`ring-2 ring-primary/50
|
||||||
|
bg-primary-300
|
||||||
|
text-white
|
||||||
|
transition-all duration-200 ease-in-out shadow-md
|
||||||
|
hover:shadow-lg
|
||||||
|
${className}`}
|
||||||
|
shape="square"
|
||||||
|
src={src}
|
||||||
|
size={40}
|
||||||
|
{...props}>
|
||||||
|
{!src && firstChar}
|
||||||
|
</Avatar>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,17 +1,27 @@
|
||||||
// useTusUpload.ts
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import * as tus from "tus-js-client";
|
import * as tus from "tus-js-client";
|
||||||
|
|
||||||
interface UploadResult {
|
interface UploadResult {
|
||||||
url?: string;
|
url: string;
|
||||||
|
fileId: string;
|
||||||
|
// resource: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useTusUpload() {
|
export function useTusUpload() {
|
||||||
const [progress, setProgress] = useState(0);
|
const [progress, setProgress] = useState(0);
|
||||||
const [isUploading, setIsUploading] = useState(false);
|
const [isUploading, setIsUploading] = useState(false);
|
||||||
const [uploadError, setUploadError] = useState<string | null>(null);
|
const [uploadError, setUploadError] = useState<string | null>(null);
|
||||||
|
const getFileId = (url: string) => {
|
||||||
const handleFileUpload = (
|
const parts = url.split("/");
|
||||||
|
// Find the index of the 'upload' segment
|
||||||
|
const uploadIndex = parts.findIndex((part) => part === "upload");
|
||||||
|
if (uploadIndex === -1 || uploadIndex + 4 >= parts.length) {
|
||||||
|
throw new Error("Invalid upload URL format");
|
||||||
|
}
|
||||||
|
// Get the date parts and file ID (4 segments after 'upload')
|
||||||
|
return parts.slice(uploadIndex + 1, uploadIndex + 5).join("/");
|
||||||
|
};
|
||||||
|
const handleFileUpload = async (
|
||||||
file: File,
|
file: File,
|
||||||
onSuccess: (result: UploadResult) => void,
|
onSuccess: (result: UploadResult) => void,
|
||||||
onError: (error: Error) => void
|
onError: (error: Error) => void
|
||||||
|
@ -19,9 +29,8 @@ export function useTusUpload() {
|
||||||
setIsUploading(true);
|
setIsUploading(true);
|
||||||
setProgress(0);
|
setProgress(0);
|
||||||
setUploadError(null);
|
setUploadError(null);
|
||||||
|
|
||||||
const upload = new tus.Upload(file, {
|
const upload = new tus.Upload(file, {
|
||||||
endpoint: "http://localhost:3000/upload", // 替换为实际的上传端点
|
endpoint: "http://localhost:3000/upload",
|
||||||
retryDelays: [0, 1000, 3000, 5000],
|
retryDelays: [0, 1000, 3000, 5000],
|
||||||
metadata: {
|
metadata: {
|
||||||
filename: file.name,
|
filename: file.name,
|
||||||
|
@ -34,10 +43,28 @@ export function useTusUpload() {
|
||||||
).toFixed(2);
|
).toFixed(2);
|
||||||
setProgress(Number(uploadProgress));
|
setProgress(Number(uploadProgress));
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: async () => {
|
||||||
|
try {
|
||||||
|
if (upload.url) {
|
||||||
|
const fileId = getFileId(upload.url);
|
||||||
|
// const resource = await pollResourceStatus(fileId);
|
||||||
setIsUploading(false);
|
setIsUploading(false);
|
||||||
setProgress(100);
|
setProgress(100);
|
||||||
onSuccess({ url: upload.url });
|
onSuccess({
|
||||||
|
url: upload.url,
|
||||||
|
fileId,
|
||||||
|
// resource,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const err =
|
||||||
|
error instanceof Error
|
||||||
|
? error
|
||||||
|
: new Error("Unknown error");
|
||||||
|
setIsUploading(false);
|
||||||
|
setUploadError(err.message);
|
||||||
|
onError(err);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
setIsUploading(false);
|
setIsUploading(false);
|
||||||
|
@ -45,10 +72,8 @@ export function useTusUpload() {
|
||||||
onError(error);
|
onError(error);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
upload.start();
|
upload.start();
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
progress,
|
progress,
|
||||||
isUploading,
|
isUploading,
|
||||||
|
|
|
@ -8,7 +8,7 @@ export enum PostType {
|
||||||
}
|
}
|
||||||
export enum TaxonomySlug {
|
export enum TaxonomySlug {
|
||||||
CATEGORY = "category",
|
CATEGORY = "category",
|
||||||
TAG = "tag"
|
TAG = "tag",
|
||||||
}
|
}
|
||||||
export enum VisitType {
|
export enum VisitType {
|
||||||
STAR = "star",
|
STAR = "star",
|
||||||
|
@ -193,5 +193,10 @@ export const LessonTypeLabel = {
|
||||||
export enum PostState {
|
export enum PostState {
|
||||||
PENDING = "pending",
|
PENDING = "pending",
|
||||||
PROCESSING = "processing",
|
PROCESSING = "processing",
|
||||||
COMPLETED = "completed",
|
RESOLVED = "resolved",
|
||||||
}
|
}
|
||||||
|
export const PostStateLabels = {
|
||||||
|
[PostState.PENDING]: "待处理",
|
||||||
|
[PostState.PROCESSING]: "处理中",
|
||||||
|
[PostState.RESOLVED]: "已解答",
|
||||||
|
};
|
||||||
|
|
|
@ -15,7 +15,13 @@ export const postDetailSelect: Prisma.PostSelect = {
|
||||||
termId: true,
|
termId: true,
|
||||||
term: {
|
term: {
|
||||||
include: {
|
include: {
|
||||||
taxonomy: true,
|
taxonomy: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
slug: true,
|
||||||
|
name: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
authorId: true,
|
authorId: true,
|
||||||
|
|
|
@ -1,106 +1,105 @@
|
||||||
import type { Config } from 'tailwindcss'
|
import type { Config } from "tailwindcss";
|
||||||
|
|
||||||
export const NiceTailwindConfig: Config = {
|
export const NiceTailwindConfig: Config = {
|
||||||
content: [
|
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
|
||||||
"./index.html",
|
|
||||||
"./src/**/*.{js,ts,jsx,tsx}",
|
|
||||||
],
|
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
colors: {
|
colors: {
|
||||||
// 主色调 - 空军蓝
|
// 主色调 - 空军蓝
|
||||||
primary: {
|
primary: {
|
||||||
50: '#e8f2ff',
|
50: "#e8f2ff",
|
||||||
100: '#c5d9f7',
|
100: "#c5d9f7",
|
||||||
200: '#9fc0ef',
|
150: "#EDF0F8", // via color
|
||||||
300: '#78a7e7',
|
200: "#9fc0ef",
|
||||||
400: '#528edf',
|
250: "#E6E9F0", // from color
|
||||||
500: '#00308a',
|
300: "#78a7e7",
|
||||||
600: '#00256b',
|
350: "#D8E2EF", // to color
|
||||||
700: '#001a4c',
|
400: "#528edf",
|
||||||
800: '#000f2d',
|
500: "#00308a",
|
||||||
900: '#00040e',
|
600: "#00256b",
|
||||||
DEFAULT: '#00308a',
|
700: "#001a4c",
|
||||||
|
800: "#000f2d",
|
||||||
|
900: "#00040e",
|
||||||
|
DEFAULT: "#00308a",
|
||||||
},
|
},
|
||||||
// 辅助色 - 军事灰
|
// 辅助色 - 军事灰
|
||||||
secondary: {
|
secondary: {
|
||||||
50: '#f5f5f5',
|
50: "#f5f5f5",
|
||||||
100: '#e0e0e0',
|
100: "#e0e0e0",
|
||||||
200: '#c2c2c2',
|
200: "#c2c2c2",
|
||||||
300: '#a3a3a3',
|
300: "#a3a3a3",
|
||||||
400: '#858585',
|
350: "#97A9C4", // New color inserted
|
||||||
500: '#666666',
|
400: "#858585",
|
||||||
600: '#4d4d4d',
|
500: "#666666",
|
||||||
700: '#333333',
|
600: "#4d4d4d",
|
||||||
800: '#1a1a1a',
|
700: "#333333",
|
||||||
900: '#0d0d0d',
|
800: "#1a1a1a",
|
||||||
DEFAULT: '#4d4d4d',
|
900: "#0d0d0d",
|
||||||
|
DEFAULT: "#4d4d4d",
|
||||||
},
|
},
|
||||||
// 强调色 - 军徽金
|
// 强调色 - 军徽金
|
||||||
accent: {
|
accent: {
|
||||||
50: '#fff8e5',
|
50: "#fff8e5",
|
||||||
100: '#ffecb3',
|
100: "#ffecb3",
|
||||||
200: '#ffe080',
|
200: "#ffe080",
|
||||||
300: '#ffd44d',
|
300: "#ffd44d",
|
||||||
400: '#ffc81a',
|
400: "#ffc81a",
|
||||||
500: '#e6b400',
|
500: "#e6b400",
|
||||||
600: '#b38f00',
|
600: "#b38f00",
|
||||||
700: '#806a00',
|
700: "#806a00",
|
||||||
800: '#4d4000',
|
800: "#4d4000",
|
||||||
900: '#1a1500',
|
900: "#1a1500",
|
||||||
DEFAULT: '#e6b400',
|
DEFAULT: "#e6b400",
|
||||||
},
|
},
|
||||||
// 功能色
|
// 功能色
|
||||||
success: '#28a745',
|
success: "#28a745",
|
||||||
warning: '#ffc107',
|
warning: "#ffc107",
|
||||||
danger: '#dc3545',
|
danger: "#dc3545",
|
||||||
info: '#17a2b8',
|
info: "#17a2b8",
|
||||||
},
|
},
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
sans: ['Inter', 'sans-serif'],
|
sans: ["Inter", "sans-serif"],
|
||||||
heading: ['Bebas Neue', 'sans-serif'],
|
heading: ["Bebas Neue", "sans-serif"],
|
||||||
mono: ['Source Code Pro', 'monospace'],
|
mono: ["Source Code Pro", "monospace"],
|
||||||
},
|
},
|
||||||
spacing: {
|
spacing: {
|
||||||
'72': '18rem',
|
"72": "18rem",
|
||||||
'84': '21rem',
|
"84": "21rem",
|
||||||
'96': '24rem',
|
"96": "24rem",
|
||||||
},
|
},
|
||||||
borderRadius: {
|
borderRadius: {
|
||||||
'xl': '1rem',
|
xl: "1rem",
|
||||||
'2xl': '2rem',
|
"2xl": "2rem",
|
||||||
'3xl': '3rem',
|
"3xl": "3rem",
|
||||||
},
|
},
|
||||||
boxShadow: {
|
boxShadow: {
|
||||||
'outline': '0 0 0 3px rgba(0, 48, 138, 0.5)',
|
outline: "0 0 0 3px rgba(0, 48, 138, 0.5)",
|
||||||
'solid': '2px 2px 0 0 rgba(0, 0, 0, 0.2)',
|
solid: "2px 2px 0 0 rgba(0, 0, 0, 0.2)",
|
||||||
'glow': '0 0 8px rgba(230, 180, 0, 0.8)',
|
glow: "0 0 8px rgba(230, 180, 0, 0.8)",
|
||||||
'inset': 'inset 0 2px 4px 0 rgba(0, 0, 0, 0.15)',
|
inset: "inset 0 2px 4px 0 rgba(0, 0, 0, 0.15)",
|
||||||
'elevation-1': '0 1px 2px 0 rgba(0, 0, 0, 0.1)',
|
"elevation-1": "0 1px 2px 0 rgba(0, 0, 0, 0.1)",
|
||||||
'elevation-2': '0 2px 4px 0 rgba(0, 0, 0, 0.15)',
|
"elevation-2": "0 2px 4px 0 rgba(0, 0, 0, 0.15)",
|
||||||
'elevation-3': '0 4px 8px 0 rgba(0, 0, 0, 0.2)',
|
"elevation-3": "0 4px 8px 0 rgba(0, 0, 0, 0.2)",
|
||||||
'elevation-4': '0 8px 16px 0 rgba(0, 0, 0, 0.25)',
|
"elevation-4": "0 8px 16px 0 rgba(0, 0, 0, 0.25)",
|
||||||
'elevation-5': '0 16px 32px 0 rgba(0, 0, 0, 0.3)',
|
"elevation-5": "0 16px 32px 0 rgba(0, 0, 0, 0.3)",
|
||||||
'panel': '0 4px 6px -1px rgba(0, 48, 138, 0.1), 0 2px 4px -2px rgba(0, 48, 138, 0.1)',
|
panel: "0 4px 6px -1px rgba(0, 48, 138, 0.1), 0 2px 4px -2px rgba(0, 48, 138, 0.1)",
|
||||||
'button': '0 2px 4px rgba(0, 48, 138, 0.2), inset 0 1px 2px rgba(255, 255, 255, 0.1)',
|
button: "0 2px 4px rgba(0, 48, 138, 0.2), inset 0 1px 2px rgba(255, 255, 255, 0.1)",
|
||||||
'card': '0 4px 6px rgba(0, 0, 0, 0.1), 0 1px 3px rgba(0, 0, 0, 0.08)',
|
card: "0 4px 6px rgba(0, 0, 0, 0.1), 0 1px 3px rgba(0, 0, 0, 0.08)",
|
||||||
'modal': '0 8px 32px rgba(0, 0, 0, 0.2), 0 4px 16px rgba(0, 0, 0, 0.15)',
|
modal: "0 8px 32px rgba(0, 0, 0, 0.2), 0 4px 16px rgba(0, 0, 0, 0.15)",
|
||||||
},
|
},
|
||||||
animation: {
|
animation: {
|
||||||
'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite',
|
"pulse-slow": "pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite",
|
||||||
'spin-slow': 'spin 3s linear infinite',
|
"spin-slow": "spin 3s linear infinite",
|
||||||
},
|
},
|
||||||
transitionDuration: {
|
transitionDuration: {
|
||||||
'2000': '2000ms',
|
"2000": "2000ms",
|
||||||
'3000': '3000ms',
|
"3000": "3000ms",
|
||||||
},
|
},
|
||||||
screens: {
|
screens: {
|
||||||
'3xl': '1920px',
|
"3xl": "1920px",
|
||||||
'4xl': '2560px',
|
"4xl": "2560px",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [],
|
||||||
|
};
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
|
@ -62,7 +62,7 @@ export class PatchHandler extends BaseHandler {
|
||||||
try {
|
try {
|
||||||
// 从请求中获取文件ID
|
// 从请求中获取文件ID
|
||||||
const id = this.getFileIdFromRequest(req)
|
const id = this.getFileIdFromRequest(req)
|
||||||
console.log('id', id)
|
// console.log('id', id)
|
||||||
if (!id) {
|
if (!id) {
|
||||||
throw ERRORS.FILE_NOT_FOUND
|
throw ERRORS.FILE_NOT_FOUND
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue