From 0d38c1f83829bf430330f6f0542a4aba90ba40a6 Mon Sep 17 00:00:00 2001 From: ditiqi Date: Sat, 25 Jan 2025 00:46:59 +0800 Subject: [PATCH] add --- apps/server/src/models/post/utils.ts | 4 +- .../server/src/queue/worker/file.processor.ts | 2 +- apps/server/src/upload/tus.service.ts | 1 + apps/server/src/upload/types.ts | 29 +- apps/server/src/upload/upload.controller.ts | 17 +- apps/server/src/upload/utils.ts | 2 +- apps/web/src/app/main/letter/editor/page.tsx | 2 +- .../src/app/main/letter/list/LetterCard.tsx | 262 ++++++------- .../src/app/main/letter/list/SearchFilter.tsx | 151 ++++---- .../web/src/app/main/letter/list/constants.ts | 106 +++--- apps/web/src/app/main/letter/list/types.ts | 30 +- apps/web/src/app/main/letter/list/utils.ts | 43 ++- .../web/src/app/main/letter/progress/page.tsx | 350 ++++++++++-------- .../common/uploader/FileUploader.tsx | 27 +- .../common/uploader/TusUploader.tsx | 7 + .../models/post/detail/PostCommentCard.tsx | 65 ++-- .../models/post/detail/PostCommentEditor.tsx | 48 +-- .../models/post/detail/PostHeader.tsx | 116 +++--- .../post/detail/PostHeader/InfoBadge.tsx | 17 +- .../post/detail/PostHeader/PostLikeButton.tsx | 54 +++ .../post/detail/PostHeader/StatsSection.tsx | 99 ++--- .../post/detail/PostHeader/TitleSection.tsx | 48 +-- .../models/post/detail/badge/PostBadge.tsx | 62 ++++ .../context/LetterEditorContext.tsx | 0 .../form/LetterBasicForm.tsx | 39 +- .../layout/LetterEditorLayout.tsx | 9 +- .../components/presentation/CustomAvatar.tsx | 33 ++ apps/web/src/hooks/useTusUpload.ts | 49 ++- packages/common/src/enum.ts | 9 +- packages/common/src/select.ts | 8 +- packages/theme/src/tailwind.ts | 207 ++++++----- packages/tus/src/handlers/PatchHandler.ts | 2 +- 32 files changed, 1050 insertions(+), 848 deletions(-) create mode 100644 apps/web/src/components/common/uploader/TusUploader.tsx create mode 100644 apps/web/src/components/models/post/detail/PostHeader/PostLikeButton.tsx create mode 100644 apps/web/src/components/models/post/detail/badge/PostBadge.tsx rename apps/web/src/components/models/post/{LetterEditor => editor}/context/LetterEditorContext.tsx (100%) rename apps/web/src/components/models/post/{LetterEditor => editor}/form/LetterBasicForm.tsx (79%) rename apps/web/src/components/models/post/{LetterEditor => editor}/layout/LetterEditorLayout.tsx (91%) create mode 100644 apps/web/src/components/presentation/CustomAvatar.tsx diff --git a/apps/server/src/models/post/utils.ts b/apps/server/src/models/post/utils.ts index 2f62a69..57a4a2c 100644 --- a/apps/server/src/models/post/utils.ts +++ b/apps/server/src/models/post/utils.ts @@ -99,7 +99,7 @@ export async function updatePostState(id: string) { }, }, }); - if (post?.state === PostState.COMPLETED) { + if (post?.state === PostState.RESOLVED) { return; } const postReceiverIds = post.receivers @@ -132,7 +132,7 @@ export async function updatePostState(id: string) { if (receiverComments > 0) { await db.post.update({ where: { id }, - data: { state: PostState.COMPLETED }, + data: { state: PostState.RESOLVED }, }); } } diff --git a/apps/server/src/queue/worker/file.processor.ts b/apps/server/src/queue/worker/file.processor.ts index a37c266..0fcc3e3 100644 --- a/apps/server/src/queue/worker/file.processor.ts +++ b/apps/server/src/queue/worker/file.processor.ts @@ -10,7 +10,7 @@ const pipeline = new ResourceProcessingPipeline() .addProcessor(new VideoProcessor()); export default async function processJob(job: Job) { if (job.name === QueueJobType.FILE_PROCESS) { - console.log(job); + // console.log(job); const { resource } = job.data; if (!resource) { throw new Error('No resource provided in job data'); diff --git a/apps/server/src/upload/tus.service.ts b/apps/server/src/upload/tus.service.ts index 5b6961f..a9805d7 100644 --- a/apps/server/src/upload/tus.service.ts +++ b/apps/server/src/upload/tus.service.ts @@ -89,6 +89,7 @@ export class TusService implements OnModuleInit { upload: Upload, ) { try { + console.log('upload.id', upload.id); const resource = await this.resourceService.update({ where: { fileId: this.getFileId(upload.id) }, data: { status: ResourceStatus.UPLOADED }, diff --git a/apps/server/src/upload/types.ts b/apps/server/src/upload/types.ts index ef60aaa..2140ebc 100644 --- a/apps/server/src/upload/types.ts +++ b/apps/server/src/upload/types.ts @@ -1,19 +1,24 @@ export interface UploadCompleteEvent { - identifier: string; - filename: string; - size: number; - hash: string; - integrityVerified: boolean; + identifier: string; + filename: string; + size: number; + hash: string; + integrityVerified: boolean; } export type UploadEvent = { - uploadStart: { identifier: string; filename: string; totalSize: number, resuming?: boolean }; - uploadComplete: UploadCompleteEvent - uploadError: { identifier: string; error: string, filename: string }; -} + uploadStart: { + identifier: string; + filename: string; + totalSize: number; + resuming?: boolean; + }; + uploadComplete: UploadCompleteEvent; + uploadError: { identifier: string; error: string; filename: string }; +}; export interface UploadLock { - clientId: string; - timestamp: number; + clientId: string; + timestamp: number; } // 添加重试机制,处理临时网络问题 // 实现定期清理过期的临时文件 @@ -21,4 +26,4 @@ export interface UploadLock { // 实现上传进度持久化,支持服务重启后恢复 // 添加并发限制,防止系统资源耗尽 // 实现文件去重功能,避免重复上传 -// 添加日志记录和监控机制 \ No newline at end of file +// 添加日志记录和监控机制 diff --git a/apps/server/src/upload/upload.controller.ts b/apps/server/src/upload/upload.controller.ts index f014c42..d81dec2 100644 --- a/apps/server/src/upload/upload.controller.ts +++ b/apps/server/src/upload/upload.controller.ts @@ -13,10 +13,25 @@ import { } from '@nestjs/common'; import { Request, Response } from 'express'; import { TusService } from './tus.service'; +import { ResourceService } from '@server/models/resource/resource.service'; @Controller('upload') 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() // async handlePost(@Req() req: Request, @Res() res: Response) { // return this.tusService.handleTus(req, res); diff --git a/apps/server/src/upload/utils.ts b/apps/server/src/upload/utils.ts index a171d7f..a7c189f 100644 --- a/apps/server/src/upload/utils.ts +++ b/apps/server/src/upload/utils.ts @@ -1,4 +1,4 @@ export function extractFileIdFromNginxUrl(url: string) { const match = url.match(/uploads\/(\d{4}\/\d{2}\/\d{2}\/[^/]+)/); return match ? match[1] : ''; -} \ No newline at end of file +} diff --git a/apps/web/src/app/main/letter/editor/page.tsx b/apps/web/src/app/main/letter/editor/page.tsx index bece392..e2ff2f9 100644 --- a/apps/web/src/app/main/letter/editor/page.tsx +++ b/apps/web/src/app/main/letter/editor/page.tsx @@ -1,5 +1,5 @@ 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() { return ( diff --git a/apps/web/src/app/main/letter/list/LetterCard.tsx b/apps/web/src/app/main/letter/list/LetterCard.tsx index b322c5b..e9bdfbc 100644 --- a/apps/web/src/app/main/letter/list/LetterCard.tsx +++ b/apps/web/src/app/main/letter/list/LetterCard.tsx @@ -1,153 +1,161 @@ -import { EyeOutlined, LikeOutlined, LikeFilled, UserOutlined, 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'; +import { + EyeOutlined, + LikeOutlined, + LikeFilled, + UserOutlined, + 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; interface LetterCardProps { - letter: Letter; + letter: Letter; } export function LetterCard({ letter }: LetterCardProps) { - const [likes, setLikes] = useState(0); - const [liked, setLiked] = useState(false); - const [views] = useState(Math.floor(Math.random() * 100)); // 模拟浏览量数据 + const [likes, setLikes] = useState(0); + const [liked, setLiked] = useState(false); + const [views] = useState(Math.floor(Math.random() * 100)); // 模拟浏览量数据 - const handleLike = () => { - if (!liked) { - setLikes(prev => prev + 1); - setLiked(true); - toast.success('已点赞!', { - icon: , - className: 'custom-message', + const handleLike = () => { + if (!liked) { + setLikes((prev) => prev + 1); + setLiked(true); + toast.success("已点赞!", { + icon: , + className: "custom-message", + }); + } else { + setLikes((prev) => prev - 1); + setLiked(false); + toast("已取消点赞", { + className: "custom-message", + }); + } + }; - }); - } else { - setLikes(prev => prev - 1); - setLiked(false); - toast('已取消点赞', { - className: 'custom-message', - - }); - } - }; - - return ( -
-
- {/* Title & Priority */} -
- - <a - href={`/letters/${letter.id}`} - target="_blank" - className="text-primary transition-all duration-300 relative + return ( + <div className="w-full p-4 bg-white transition-all duration-300 ease-in-out group"> + <div className="flex flex-col gap-3"> + {/* Title & Priority */} + <div className="flex justify-between items-start"> + <Title level={4} className="!mb-0 flex-1"> + <a + href={`/letters/${letter.id}`} + target="_blank" + 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 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" - > - {letter.title} - </a> - - {letter.priority && ( - - )} -
+ group-hover:text-primary-600 group-hover:scale-105 group-hover:drop-shadow-md"> + {letter.title} + + + {letter.priority && ( + + )} +
- {/* Meta Info */} -
- - - - {letter.sender} - - | - - - {letter.unit} - - - - - {letter.date} - -
+ {/* Meta Info */} +
+ + + + {letter.sender} + + | + + + {letter.unit} + + + + + {letter.date} + +
- {/* Content Preview */} - {letter.content && ( -
- - - {letter.content} - -
- )} + {/* Content Preview */} + {letter.content && ( +
+ + + {letter.content} + +
+ )} - {/* Badges & Interactions */} -
- - - - + {/* Badges & Interactions */} +
+ + + + -
-
- - {views} -
- - - -
-
-
-
- ); + ${liked ? "bg-blue-500 hover:bg-blue-600" : "hover:border-blue-500 hover:text-blue-500"} + `}> + + {likes} + + + + + + + + ); } -function Badge({ - type, - value, - className = '' +export function Badge({ + type, + value, + className = "", }: { - type: 'priority' | 'category' | 'status'; - value: string; - className?: string; + type: "priority" | "category" | "status"; + value: string; + className?: string; }) { - return ( - - {value.toUpperCase()} - - ); + `}> + + {value?.toUpperCase()} + + ) + ); } diff --git a/apps/web/src/app/main/letter/list/SearchFilter.tsx b/apps/web/src/app/main/letter/list/SearchFilter.tsx index f74cd28..4853c56 100644 --- a/apps/web/src/app/main/letter/list/SearchFilter.tsx +++ b/apps/web/src/app/main/letter/list/SearchFilter.tsx @@ -1,90 +1,87 @@ -import { SearchOutlined } from '@ant-design/icons'; -import { Form, Input, Select, Spin } from 'antd'; -import { useEffect } from 'react'; +import { SearchOutlined } from "@ant-design/icons"; +import { Form, Input, Select, Spin } from "antd"; +import { useEffect } from "react"; interface SearchFiltersProps { - searchTerm: string; - onSearchChange: (value: string) => void; - filterCategory: string; - onCategoryChange: (value: string) => void; - filterStatus: string; - onStatusChange: (value: string) => void; - className?: string; - isLoading?: boolean; + searchTerm: string; + onSearchChange: (value: string) => void; + filterCategory: string; + onCategoryChange: (value: string) => void; + filterStatus: string; + onStatusChange: (value: string) => void; + className?: string; + isLoading?: boolean; } -const LoadingIndicator = () => ( - -); +const LoadingIndicator = () => ; export function SearchFilters({ - searchTerm, - onSearchChange, - filterCategory, - onCategoryChange, - filterStatus, - onStatusChange, - className, - isLoading = false + searchTerm, + onSearchChange, + filterCategory, + onCategoryChange, + filterStatus, + onStatusChange, + className, + isLoading = false, }: SearchFiltersProps) { - const [form] = Form.useForm(); + const [form] = Form.useForm(); - // 统一处理表单初始值 - const initialValues = { - search: searchTerm, - category: filterCategory, - status: filterStatus - }; + // 统一处理表单初始值 + const initialValues = { + search: searchTerm, + category: filterCategory, + status: filterStatus, + }; - useEffect(() => { - form.setFieldsValue(initialValues); - }, [searchTerm, filterCategory, filterStatus, form]); + useEffect(() => { + form.setFieldsValue(initialValues); + }, [searchTerm, filterCategory, filterStatus, form]); - return ( -
-
- - } - placeholder="搜索关键词、发件人或单位..." - onChange={(e) => onSearchChange(e.target.value)} - allowClear - suffix={isLoading ? : null} - /> - + return ( + +
+ + } + placeholder="搜索关键词、发件人或单位..." + onChange={(e) => onSearchChange(e.target.value)} + allowClear + suffix={isLoading ? : null} + /> + - - + - - + +
+ + ); } diff --git a/apps/web/src/app/main/letter/list/constants.ts b/apps/web/src/app/main/letter/list/constants.ts index 7858fc6..df76d37 100644 --- a/apps/web/src/app/main/letter/list/constants.ts +++ b/apps/web/src/app/main/letter/list/constants.ts @@ -1,56 +1,56 @@ import { Letter } from "./types"; export const letters: Letter[] = [ - { - id: '1', - title: 'F-35 Maintenance Schedule Optimization Proposal', - sender: 'John Doe', - rank: 'TSgt', - unit: '33d Fighter Wing', - date: '2025-01-22', - priority: 'high', - status: 'pending', - category: 'suggestion', - isStarred: false, - content: 'Proposal for improving F-35 maintenance efficiency...' - }, - { - id: '2', - title: 'Base Housing Facility Improvement Request', - sender: 'Jane Smith', - rank: 'SSgt', - unit: '96th Test Wing', - date: '2025-01-21', - priority: 'medium', - status: 'in-progress', - category: 'request', - isStarred: true, - content: 'Request for updating base housing facilities...' - }, - { - id: '3', - title: 'Training Program Enhancement Feedback', - sender: 'Robert Johnson', - rank: 'Capt', - unit: '58th Special Operations Wing', - date: '2025-01-20', - priority: 'medium', - status: 'pending', - category: 'feedback', - isStarred: false, - content: 'Feedback regarding current training procedures...' - }, - { - id: '4', - title: 'Cybersecurity Protocol Update Suggestion', - sender: 'Emily Wilson', - rank: 'MSgt', - unit: '67th Cyberspace Wing', - date: '2025-01-19', - priority: 'high', - status: 'pending', - category: 'suggestion', - isStarred: true, - content: 'Suggestions for improving base cybersecurity measures...' - } -]; \ No newline at end of file + { + id: "1", + title: "F-35 Maintenance Schedule Optimization Proposal", + sender: "John Doe", + rank: "TSgt", + unit: "33d Fighter Wing", + date: "2025-01-22", + priority: "high", + status: "pending", + category: "suggestion", + isStarred: false, + content: "Proposal for improving F-35 maintenance efficiency...", + }, + { + id: "2", + title: "Base Housing Facility Improvement Request", + sender: "Jane Smith", + rank: "SSgt", + unit: "96th Test Wing", + date: "2025-01-21", + priority: "medium", + status: "processing", + category: "request", + isStarred: true, + content: "Request for updating base housing facilities...", + }, + { + id: "3", + title: "Training Program Enhancement Feedback", + sender: "Robert Johnson", + rank: "Capt", + unit: "58th Special Operations Wing", + date: "2025-01-20", + priority: "medium", + status: "pending", + category: "feedback", + isStarred: false, + content: "Feedback regarding current training procedures...", + }, + { + id: "4", + title: "Cybersecurity Protocol Update Suggestion", + sender: "Emily Wilson", + rank: "MSgt", + unit: "67th Cyberspace Wing", + date: "2025-01-19", + priority: "high", + status: "pending", + category: "suggestion", + isStarred: true, + content: "Suggestions for improving base cybersecurity measures...", + }, +]; diff --git a/apps/web/src/app/main/letter/list/types.ts b/apps/web/src/app/main/letter/list/types.ts index d6c1a4a..e207683 100644 --- a/apps/web/src/app/main/letter/list/types.ts +++ b/apps/web/src/app/main/letter/list/types.ts @@ -1,16 +1,16 @@ -export type Priority = 'high' | 'medium' | 'low'; -export type Category = 'complaint' | 'suggestion' | 'request' | 'feedback'; -export type Status = 'pending' | 'in-progress' | 'resolved'; +export type Priority = "high" | "medium" | "low"; +export type Category = "complaint" | "suggestion" | "request" | "feedback"; +export type Status = "pending" | "processing" | "resolved"; export interface Letter { - id: string; - title: string; - sender: string; - rank: string; - unit: string; - date: string; - priority: Priority; - category: Category; - status: Status; - content?: string; - isStarred: boolean; -} \ No newline at end of file + id: string; + title: string; + sender: string; + rank: string; + unit: string; + date: string; + priority: Priority; + category: Category; + status: Status; + content?: string; + isStarred: boolean; +} diff --git a/apps/web/src/app/main/letter/list/utils.ts b/apps/web/src/app/main/letter/list/utils.ts index 173cfc5..55548e1 100644 --- a/apps/web/src/app/main/letter/list/utils.ts +++ b/apps/web/src/app/main/letter/list/utils.ts @@ -1,25 +1,30 @@ +import { PostState } from "@nice/common"; + export 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', - }, - status: { - pending: 'bg-yellow-100 text-yellow-800', - 'in-progress': 'bg-blue-100 text-blue-800', - resolved: 'bg-green-100 text-green-800', - }, + 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", + }, + status: { + [PostState.PENDING]: "bg-yellow-100 text-yellow-800", + [PostState.PROCESSING]: "bg-blue-100 text-blue-800", + [PostState.RESOLVED]: "bg-green-100 text-green-800", + }, } as const; export const getBadgeStyle = ( - type: keyof typeof BADGE_STYLES, - value: string + type: keyof typeof BADGE_STYLES, + value: 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" + ); }; diff --git a/apps/web/src/app/main/letter/progress/page.tsx b/apps/web/src/app/main/letter/progress/page.tsx index e8d0145..c9e03ad 100644 --- a/apps/web/src/app/main/letter/progress/page.tsx +++ b/apps/web/src/app/main/letter/progress/page.tsx @@ -1,175 +1,207 @@ -import { useState } from 'react' -import { Input, Button, Card, Steps, Tag, Spin, message } from 'antd' -import { SearchOutlined, SafetyCertificateOutlined } from '@ant-design/icons' +import { useState } from "react"; +import { Input, Button, Card, Steps, Tag, Spin, message } from "antd"; +import { SearchOutlined, SafetyCertificateOutlined } from "@ant-design/icons"; interface FeedbackStatus { - status: 'pending' | 'in-progress' | 'resolved' - ticketId: string - submittedDate: string - lastUpdate: string - title: string + status: "pending" | "processing" | "resolved"; + ticketId: string; + submittedDate: string; + lastUpdate: string; + title: string; } -const { Step } = Steps +const { Step } = Steps; export default function LetterProgressPage() { - const [feedbackId, setFeedbackId] = useState('') - const [status, setStatus] = useState(null) - const [loading, setLoading] = useState(false) - const [error, setError] = useState('') + const [feedbackId, setFeedbackId] = useState(""); + const [status, setStatus] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(""); - const validateInput = () => { - if (!feedbackId.trim()) { - setError('请输入有效的问题编号') - return false - } - if (!/^USAF-\d{4}-\d{4}$/.test(feedbackId)) { - setError('问题编号格式不正确,应为USAF-YYYY-NNNN') - return false - } - setError('') - return true - } + const validateInput = () => { + if (!feedbackId.trim()) { + setError("请输入有效的问题编号"); + return false; + } + if (!/^USAF-\d{4}-\d{4}$/.test(feedbackId)) { + setError("问题编号格式不正确,应为USAF-YYYY-NNNN"); + return false; + } + setError(""); + return true; + }; - const mockLookup = () => { - if (!validateInput()) return + const mockLookup = () => { + if (!validateInput()) return; - setLoading(true) - setTimeout(() => { - setStatus({ - status: 'in-progress', - ticketId: feedbackId, - submittedDate: '2025-01-15', - lastUpdate: '2025-01-21', - title: 'Aircraft Maintenance Schedule Inquiry' - }) - setLoading(false) - }, 1000) - } + setLoading(true); + setTimeout(() => { + setStatus({ + status: "processing", + ticketId: feedbackId, + submittedDate: "2025-01-15", + lastUpdate: "2025-01-21", + title: "Aircraft Maintenance Schedule Inquiry", + }); + setLoading(false); + }, 1000); + }; - const getStatusColor = (status: string) => { - switch (status) { - case 'pending': - return 'orange' - case 'in-progress': - return 'blue' - case 'resolved': - return 'green' - default: - return 'gray' - } - } + const getStatusColor = (status: string) => { + switch (status) { + case "pending": + return "orange"; + case "processing": + return "blue"; + case "resolved": + return "green"; + default: + return "gray"; + } + }; - return ( -
- {/* Header */} -
-
- -
-

USAF Feedback Tracking System

-

Enter your ticket ID to track progress

-
-
-
+ return ( +
+ {/* Header */} +
+
+ +
+

+ USAF Feedback Tracking System +

+

+ Enter your ticket ID to track progress +

+
+
+
- {/* Main Content */} -
- {/* Search Section */} - -
- -
- } - size="large" - value={feedbackId} - onChange={(e) => setFeedbackId(e.target.value)} - placeholder="e.g. USAF-2025-0123" - status={error ? 'error' : ''} - className="border border-gray-300" - /> - -
- {error &&

{error}

} -
-
+ {/* Main Content */} +
+ {/* Search Section */} + +
+ +
+ + } + size="large" + value={feedbackId} + onChange={(e) => setFeedbackId(e.target.value)} + placeholder="e.g. USAF-2025-0123" + status={error ? "error" : ""} + className="border border-gray-300" + /> + +
+ {error && ( +

{error}

+ )} +
+
- {/* Results Section */} - {status && ( - -
- {/* Header */} -
-

- Ticket Details -

- - {status.status} - -
+ {/* Results Section */} + {status && ( + +
+ {/* Header */} +
+

+ Ticket Details +

+ + {status.status} + +
- {/* Details Grid */} -
-
-

Ticket ID

-

{status.ticketId}

-
-
-

Submitted Date

-

{status.submittedDate}

-
-
-

Last Update

-

{status.lastUpdate}

-
-
-

Subject

-

{status.title}

-
-
+ {/* Details Grid */} +
+
+

+ Ticket ID +

+

+ {status.ticketId} +

+
+
+

+ Submitted Date +

+

+ {status.submittedDate} +

+
+
+

+ Last Update +

+

+ {status.lastUpdate} +

+
+
+

+ Subject +

+

+ {status.title} +

+
+
- {/* Progress Timeline */} -
- - } - /> - } - /> - } - /> - -
-
-
- )} -
-
- ) + {/* Progress Timeline */} +
+ + } + /> + } + /> + } + /> + +
+
+ + )} + +
+ ); } diff --git a/apps/web/src/components/common/uploader/FileUploader.tsx b/apps/web/src/components/common/uploader/FileUploader.tsx index 4bbf236..99bb2af 100644 --- a/apps/web/src/components/common/uploader/FileUploader.tsx +++ b/apps/web/src/components/common/uploader/FileUploader.tsx @@ -13,7 +13,7 @@ import { useTusUpload } from "@web/src/hooks/useTusUpload"; interface FileUploaderProps { endpoint?: string; - onSuccess?: (url: string) => void; + onSuccess?: (result: { url: string; fileId: string }) => void; onError?: (error: Error) => void; maxSize?: number; allowedTypes?: string[]; @@ -44,7 +44,7 @@ const FileItem: React.FC = memo( onClick={() => onRemove(file.name)} className="opacity-0 group-hover:opacity-100 transition-opacity p-1 hover:bg-gray-100 rounded-full" aria-label={`Remove ${file.name}`}> - + {!isUploaded && progress !== undefined && ( @@ -57,7 +57,7 @@ const FileItem: React.FC = memo( transition={{ duration: 0.3 }} /> - + {progress}% @@ -74,7 +74,6 @@ const FileItem: React.FC = memo( ); const FileUploader: React.FC = ({ - endpoint = "", onSuccess, onError, maxSize = 100, @@ -125,7 +124,12 @@ const FileUploader: React.FC = ({ handleFileUpload( file, (upload) => { - onSuccess?.(upload.url || ""); + console.log("Upload complete:", { + url: upload.url, + fileId: upload.fileId, + // resource: upload.resource + }); + onSuccess?.(upload); setFiles((prev) => prev.map((f) => f.file.name === file.name @@ -185,10 +189,11 @@ const FileUploader: React.FC = ({ relative flex flex-col items-center justify-center w-full h-32 border-2 border-dashed rounded-lg cursor-pointer transition-colors duration-200 ease-in-out - ${isDragging - ? "border-blue-500 bg-blue-50" - : "border-gray-300 hover:border-blue-500" - } + ${ + isDragging + ? "border-blue-500 bg-blue-50" + : "border-gray-300 hover:border-blue-500" + } `}> = ({ accept={allowedTypes.join(",")} className="hidden" /> - -

{placeholder}

+ +

{placeholder}

{isDragging && (

diff --git a/apps/web/src/components/common/uploader/TusUploader.tsx b/apps/web/src/components/common/uploader/TusUploader.tsx new file mode 100644 index 0000000..4211322 --- /dev/null +++ b/apps/web/src/components/common/uploader/TusUploader.tsx @@ -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 ; +}; diff --git a/apps/web/src/components/models/post/detail/PostCommentCard.tsx b/apps/web/src/components/models/post/detail/PostCommentCard.tsx index 3cd2422..828cbaf 100644 --- a/apps/web/src/components/models/post/detail/PostCommentCard.tsx +++ b/apps/web/src/components/models/post/detail/PostCommentCard.tsx @@ -7,6 +7,8 @@ import { useVisitor } from "@nice/client"; import { useContext, useState } from "react"; import { PostDetailContext } from "./context/PostDetailContext"; import { LikeFilled, LikeOutlined } from "@ant-design/icons"; +import PostLikeButton from "./PostHeader/PostLikeButton"; +import { CustomAvatar } from "@web/src/components/presentation/CustomAvatar"; export default function PostCommentCard({ post, @@ -55,34 +57,36 @@ export default function PostCommentCard({ -

+
- - {!post.author?.avatar && - (post.author?.showname || "匿名用户")} - + size={40} + name={ + !post.author?.avatar && post.author?.showname + }>
-
- - {post.author?.showname || "匿名用户"} - - - {dayjs(post?.createdAt).format("YYYY-MM-DD HH:mm")} - - {isReceiverComment && ( - - 官方回答 +
+
+ + {post.author?.showname || "匿名用户"} - )} + + {dayjs(post?.createdAt).format( + "YYYY-MM-DD HH:mm" + )} + + {isReceiverComment && ( + + 官方回答 + + )} +
+ {/* 添加有帮助按钮 */} +
+
- - {/* 添加有帮助按钮 */} -
- - {liked ? : } - {likeCount} 有帮助 - -
diff --git a/apps/web/src/components/models/post/detail/PostCommentEditor.tsx b/apps/web/src/components/models/post/detail/PostCommentEditor.tsx index 0361702..292a883 100644 --- a/apps/web/src/components/models/post/detail/PostCommentEditor.tsx +++ b/apps/web/src/components/models/post/detail/PostCommentEditor.tsx @@ -1,14 +1,15 @@ import React, { useContext, useState } from "react"; 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 { PostDetailContext } from "./context/PostDetailContext"; import { usePost } from "@nice/client"; import { PostType } from "@nice/common"; import toast from "react-hot-toast"; import { isContentEmpty } from "./utils"; - +import { SendOutlined } from "@ant-design/icons"; +import { TusUploader } from "@web/src/components/common/uploader/TusUploader"; export default function PostCommentEditor() { const { post } = useContext(PostDetailContext); const [content, setContent] = useState(""); @@ -82,36 +83,21 @@ export default function PostCommentEditor() { )}
-
- {/* 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" - }`}> - - {isPreview ? "编辑" : "预览"} - */} + - + - - 提交 - + whileTap={{ scale: 0.98 }}> + +
diff --git a/apps/web/src/components/models/post/detail/PostHeader.tsx b/apps/web/src/components/models/post/detail/PostHeader.tsx index 55b1f21..8daf65d 100644 --- a/apps/web/src/components/models/post/detail/PostHeader.tsx +++ b/apps/web/src/components/models/post/detail/PostHeader.tsx @@ -11,14 +11,21 @@ import { EyeIcon, ChatBubbleLeftIcon, } from "@heroicons/react/24/outline"; - +import { Button, Typography, Space, Tooltip } from "antd"; import { useVisitor } from "@nice/client"; import { PostState, VisitType } from "@nice/common"; import { + CalendarOutlined, + ClockCircleOutlined, CommentOutlined, EyeOutlined, + FileTextOutlined, + FolderOutlined, LikeFilled, LikeOutlined, + LockOutlined, + UnlockOutlined, + UserOutlined, } from "@ant-design/icons"; import dayjs from "dayjs"; import { TitleSection } from "./PostHeader/TitleSection"; @@ -30,7 +37,9 @@ import { VisibilityBadge, } from "./PostHeader/InfoBadge"; import { StatsSection } from "./PostHeader/StatsSection"; +import { PostBadge } from "./badge/PostBadge"; +const { Title, Paragraph, Text } = Typography; export default function PostHeader() { const { post, user } = useContext(PostDetailContext); const { like, unLike } = useVisitor(); @@ -64,62 +73,76 @@ export default function PostHeader() { initial={{ opacity: 0, y: -20 }} animate={{ opacity: 1, y: 0 }} 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 */} -
-
+
+
{/* Title Section */} - +
- {/* First Row - Basic Info */} -
- {/* Author Info Badge */} - + {/* 收件人信息行 */} + + + 收件人: + + {post?.receivers?.map((receiver) => receiver?.showname)} + + + {/* First Row - Basic Info */} +
+ {/* Author Info Badge */} + + + 发件人: + + {post?.author?.showname || "匿名用户"} + + + | {/* Date Info Badge */} - {post?.createdAt && ( - - )} + + + + + 创建于: + {dayjs(post?.createdAt).format("YYYY-MM-DD")} + + + | {/* Last Updated Badge */} - {post?.updatedAt && post.updatedAt !== post.createdAt && ( - - )} + + + + 更新于: + {dayjs(post?.updatedAt).format("YYYY-MM-DD")} + + + | {/* Visibility Status Badge */} - + + {post?.isPublic ? ( + + ) : ( + + )} + {post?.isPublic ? "公开" : "私信"} +
{/* Second Row - Term and Tags */} -
- {/* Term Badge */} - {post?.term?.name && ( - - )} - +
{/* Tags Badges */} {post?.meta?.tags && post.meta.tags.length > 0 && post.meta.tags.map((tag, index) => ( - - - #{tag} - - + + + ))}
@@ -129,20 +152,15 @@ export default function PostHeader() { initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ delay: 0.6 }} - className="mt-6 text-[#2B4C7E]"> + className="mt-6 text-secondary-700">
{/* Stats Section */} - + ); } diff --git a/apps/web/src/components/models/post/detail/PostHeader/InfoBadge.tsx b/apps/web/src/components/models/post/detail/PostHeader/InfoBadge.tsx index 6a1931d..41d95a3 100644 --- a/apps/web/src/components/models/post/detail/PostHeader/InfoBadge.tsx +++ b/apps/web/src/components/models/post/detail/PostHeader/InfoBadge.tsx @@ -21,11 +21,10 @@ export function InfoBadge({ icon, text, delay = 0 }: InfoBadgeProps) { + 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} - {text} + {text} ); } @@ -33,7 +32,7 @@ export function InfoBadge({ icon, text, delay = 0 }: InfoBadgeProps) { export function AuthorBadge({ name }: { name: string }) { return ( } + icon={} text={name} /> ); @@ -42,7 +41,7 @@ export function AuthorBadge({ name }: { name: string }) { export function DateBadge({ date, label }: { date: string; label: string }) { return ( } + icon={} 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 }) { return ( } + icon={} text={`更新于: ${dayjs(date).format("YYYY-MM-DD")}`} delay={0.45} /> @@ -63,9 +62,9 @@ export function VisibilityBadge({ isPublic }: { isPublic: boolean }) { + ) : ( - + ) } text={isPublic ? "公开" : "私信"} @@ -77,7 +76,7 @@ export function VisibilityBadge({ isPublic }: { isPublic: boolean }) { export function TermBadge({ term }: { term: string }) { return ( } + icon={} text={term} delay={0.55} /> diff --git a/apps/web/src/components/models/post/detail/PostHeader/PostLikeButton.tsx b/apps/web/src/components/models/post/detail/PostHeader/PostLikeButton.tsx new file mode 100644 index 0000000..419d7be --- /dev/null +++ b/apps/web/src/components/models/post/detail/PostHeader/PostLikeButton.tsx @@ -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 ( + + + + ); +} diff --git a/apps/web/src/components/models/post/detail/PostHeader/StatsSection.tsx b/apps/web/src/components/models/post/detail/PostHeader/StatsSection.tsx index ecf9232..176dd77 100644 --- a/apps/web/src/components/models/post/detail/PostHeader/StatsSection.tsx +++ b/apps/web/src/components/models/post/detail/PostHeader/StatsSection.tsx @@ -1,68 +1,37 @@ -import React from "react"; +import React, { useContext } from "react"; 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 ( - - {icon} - {text} - - ); -} - -interface StatsSectionProps { - likes: number; - views: number; - commentsCount: number; - liked: boolean; - onLikeClick: () => void; -} - -export function StatsSection({ - likes, - views, - commentsCount, - liked, - onLikeClick, -}: StatsSectionProps) { - return ( - - : } - text={`${likes} 有帮助`} - onClick={onLikeClick} - isActive={liked} - /> - } - text={`${views} 浏览`} - /> - } - text={`${commentsCount} 回复`} - /> - - ); + return ( + +
+
+ + {post?.views} +
+
+ + {post?.commentsCount} +
+
+ +
+ ); } diff --git a/apps/web/src/components/models/post/detail/PostHeader/TitleSection.tsx b/apps/web/src/components/models/post/detail/PostHeader/TitleSection.tsx index e8d5d70..4de55f6 100644 --- a/apps/web/src/components/models/post/detail/PostHeader/TitleSection.tsx +++ b/apps/web/src/components/models/post/detail/PostHeader/TitleSection.tsx @@ -1,30 +1,29 @@ import { motion } from "framer-motion"; -import { Tag } from "antd"; +import { Space, Tag } from "antd"; import { PostState } from "@nice/common"; import { ClockIcon, CheckCircleIcon, ExclamationCircleIcon, } 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 { title: string; state: PostState; } -const stateColors = { - [PostState.PENDING]: "orange", - [PostState.PROCESSING]: "blue", - [PostState.COMPLETED]: "green", -}; - const stateLabels = { [PostState.PENDING]: "待处理", [PostState.PROCESSING]: "处理中", - [PostState.COMPLETED]: "已完成", + [PostState.RESOLVED]: "已完成", }; -export function TitleSection({ title, state }: TitleSectionProps) { +export function TitleSection() { + const { post, user } = useContext(PostDetailContext); return ( {/* Decorative Line */} -
+ {/*
*/} {/* Title */} -

- {title} +

+ {post?.title}

- {/* State Tag */} - {/* */} - - {state === PostState.PENDING && ( - - )} - {state === PostState.PROCESSING && ( - - )} - {state === PostState.COMPLETED && ( - - )} - {stateLabels[state]} - + + + + {/* */} ); diff --git a/apps/web/src/components/models/post/detail/badge/PostBadge.tsx b/apps/web/src/components/models/post/detail/badge/PostBadge.tsx new file mode 100644 index 0000000..fb9565b --- /dev/null +++ b/apps/web/src/components/models/post/detail/badge/PostBadge.tsx @@ -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 && ( + + {type === "state" ? PostStateLabels?.[value] : value?.toUpperCase()} + + ) + ); +} + +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" + ); +}; diff --git a/apps/web/src/components/models/post/LetterEditor/context/LetterEditorContext.tsx b/apps/web/src/components/models/post/editor/context/LetterEditorContext.tsx similarity index 100% rename from apps/web/src/components/models/post/LetterEditor/context/LetterEditorContext.tsx rename to apps/web/src/components/models/post/editor/context/LetterEditorContext.tsx diff --git a/apps/web/src/components/models/post/LetterEditor/form/LetterBasicForm.tsx b/apps/web/src/components/models/post/editor/form/LetterBasicForm.tsx similarity index 79% rename from apps/web/src/components/models/post/LetterEditor/form/LetterBasicForm.tsx rename to apps/web/src/components/models/post/editor/form/LetterBasicForm.tsx index d6fc1c0..f5327b7 100644 --- a/apps/web/src/components/models/post/LetterEditor/form/LetterBasicForm.tsx +++ b/apps/web/src/components/models/post/editor/form/LetterBasicForm.tsx @@ -8,8 +8,9 @@ import { TagOutlined, FileTextOutlined, } from "@ant-design/icons"; -import FileUploader from "@web/src/components/common/uploader/FileUploader"; + import QuillEditor from "@web/src/components/common/editor/quill/QuillEditor"; +import { PostBadge } from "../../detail/badge/PostBadge"; export function LetterBasicForm() { const { onSubmit, receiverId, termId, form } = useLetterEditor(); @@ -42,15 +43,11 @@ export function LetterBasicForm() { initialValues={{ meta: { tags: [] }, isPublic: true }}> {/* 收件人和板块信息行 */}
-
- +
+
收件人:{receiver?.showname}
- -
- -
板块:{term?.name}
-
+
{/* 主题输入框 */} @@ -58,11 +55,13 @@ export function LetterBasicForm() { - +
+ 标题 - (必选) - + + (必选) + +
} name="title" rules={[{ required: true, message: "请输入标题" }]} @@ -77,7 +76,7 @@ export function LetterBasicForm() { - + 标签 } @@ -108,9 +107,11 @@ export function LetterBasicForm() { - + 内容 - (必选) + + (必选) + } name="content" @@ -120,7 +121,7 @@ export function LetterBasicForm() { wrapperCol={{ span: 24 }}>
- {/* */} + -
+
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={{ transform: "scale(1)", transition: "all 0.2s", diff --git a/apps/web/src/components/models/post/LetterEditor/layout/LetterEditorLayout.tsx b/apps/web/src/components/models/post/editor/layout/LetterEditorLayout.tsx similarity index 91% rename from apps/web/src/components/models/post/LetterEditor/layout/LetterEditorLayout.tsx rename to apps/web/src/components/models/post/editor/layout/LetterEditorLayout.tsx index f3a5c38..9f083b6 100644 --- a/apps/web/src/components/models/post/LetterEditor/layout/LetterEditorLayout.tsx +++ b/apps/web/src/components/models/post/editor/layout/LetterEditorLayout.tsx @@ -3,21 +3,22 @@ import { motion } from "framer-motion"; import { PaperAirplaneIcon } from "@heroicons/react/24/outline"; import { LetterFormProvider } from "../context/LetterEditorContext"; import { LetterBasicForm } from "../form/LetterBasicForm"; +import { useTheme } from "@nice/theme"; export default function LetterEditorLayout() { const location = useLocation(); const params = new URLSearchParams(location.search); - + // const {} = useTheme(); const receiverId = params.get("receiverId"); const termId = params.get("termId"); return ( -
+
- + {/* */}

撰写信件

diff --git a/apps/web/src/components/presentation/CustomAvatar.tsx b/apps/web/src/components/presentation/CustomAvatar.tsx new file mode 100644 index 0000000..5d92b4b --- /dev/null +++ b/apps/web/src/components/presentation/CustomAvatar.tsx @@ -0,0 +1,33 @@ +import { Avatar } from "antd"; +import { AvatarProps } from "antd/lib/avatar"; + +interface CustomAvatarProps extends Omit { + src?: string; + name?: string; +} + +export function CustomAvatar({ + src, + name, + className = "", + ...props +}: CustomAvatarProps) { + // 获取名字的第一个字符,如果没有名字则显示"匿" + const firstChar = name ? name.charAt(0) : "匿"; + + return ( + + {!src && firstChar} + + ); +} diff --git a/apps/web/src/hooks/useTusUpload.ts b/apps/web/src/hooks/useTusUpload.ts index c9afb1f..6b3924b 100644 --- a/apps/web/src/hooks/useTusUpload.ts +++ b/apps/web/src/hooks/useTusUpload.ts @@ -1,17 +1,27 @@ -// useTusUpload.ts import { useState } from "react"; import * as tus from "tus-js-client"; interface UploadResult { - url?: string; + url: string; + fileId: string; + // resource: any; } export function useTusUpload() { const [progress, setProgress] = useState(0); const [isUploading, setIsUploading] = useState(false); const [uploadError, setUploadError] = useState(null); - - const handleFileUpload = ( + const getFileId = (url: string) => { + 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, onSuccess: (result: UploadResult) => void, onError: (error: Error) => void @@ -19,9 +29,8 @@ export function useTusUpload() { setIsUploading(true); setProgress(0); setUploadError(null); - const upload = new tus.Upload(file, { - endpoint: "http://localhost:3000/upload", // 替换为实际的上传端点 + endpoint: "http://localhost:3000/upload", retryDelays: [0, 1000, 3000, 5000], metadata: { filename: file.name, @@ -34,10 +43,28 @@ export function useTusUpload() { ).toFixed(2); setProgress(Number(uploadProgress)); }, - onSuccess: () => { - setIsUploading(false); - setProgress(100); - onSuccess({ url: upload.url }); + onSuccess: async () => { + try { + if (upload.url) { + const fileId = getFileId(upload.url); + // const resource = await pollResourceStatus(fileId); + setIsUploading(false); + setProgress(100); + 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) => { setIsUploading(false); @@ -45,10 +72,8 @@ export function useTusUpload() { onError(error); }, }); - upload.start(); }; - return { progress, isUploading, diff --git a/packages/common/src/enum.ts b/packages/common/src/enum.ts index 851f64b..40733e5 100755 --- a/packages/common/src/enum.ts +++ b/packages/common/src/enum.ts @@ -8,7 +8,7 @@ export enum PostType { } export enum TaxonomySlug { CATEGORY = "category", - TAG = "tag" + TAG = "tag", } export enum VisitType { STAR = "star", @@ -193,5 +193,10 @@ export const LessonTypeLabel = { export enum PostState { PENDING = "pending", PROCESSING = "processing", - COMPLETED = "completed", + RESOLVED = "resolved", } +export const PostStateLabels = { + [PostState.PENDING]: "待处理", + [PostState.PROCESSING]: "处理中", + [PostState.RESOLVED]: "已解答", +}; diff --git a/packages/common/src/select.ts b/packages/common/src/select.ts index b9d2045..8208654 100644 --- a/packages/common/src/select.ts +++ b/packages/common/src/select.ts @@ -15,7 +15,13 @@ export const postDetailSelect: Prisma.PostSelect = { termId: true, term: { include: { - taxonomy: true, + taxonomy: { + select: { + id: true, + slug: true, + name: true, + }, + }, }, }, authorId: true, diff --git a/packages/theme/src/tailwind.ts b/packages/theme/src/tailwind.ts index 7feafd8..28d0afa 100644 --- a/packages/theme/src/tailwind.ts +++ b/packages/theme/src/tailwind.ts @@ -1,106 +1,105 @@ -import type { Config } from 'tailwindcss' +import type { Config } from "tailwindcss"; export const NiceTailwindConfig: Config = { - content: [ - "./index.html", - "./src/**/*.{js,ts,jsx,tsx}", - ], - theme: { - extend: { - colors: { - // 主色调 - 空军蓝 - primary: { - 50: '#e8f2ff', - 100: '#c5d9f7', - 200: '#9fc0ef', - 300: '#78a7e7', - 400: '#528edf', - 500: '#00308a', - 600: '#00256b', - 700: '#001a4c', - 800: '#000f2d', - 900: '#00040e', - DEFAULT: '#00308a', - }, - // 辅助色 - 军事灰 - secondary: { - 50: '#f5f5f5', - 100: '#e0e0e0', - 200: '#c2c2c2', - 300: '#a3a3a3', - 400: '#858585', - 500: '#666666', - 600: '#4d4d4d', - 700: '#333333', - 800: '#1a1a1a', - 900: '#0d0d0d', - DEFAULT: '#4d4d4d', - }, - // 强调色 - 军徽金 - accent: { - 50: '#fff8e5', - 100: '#ffecb3', - 200: '#ffe080', - 300: '#ffd44d', - 400: '#ffc81a', - 500: '#e6b400', - 600: '#b38f00', - 700: '#806a00', - 800: '#4d4000', - 900: '#1a1500', - DEFAULT: '#e6b400', - }, - // 功能色 - success: '#28a745', - warning: '#ffc107', - danger: '#dc3545', - info: '#17a2b8', - }, - fontFamily: { - sans: ['Inter', 'sans-serif'], - heading: ['Bebas Neue', 'sans-serif'], - mono: ['Source Code Pro', 'monospace'], - }, - spacing: { - '72': '18rem', - '84': '21rem', - '96': '24rem', - }, - borderRadius: { - 'xl': '1rem', - '2xl': '2rem', - '3xl': '3rem', - }, - boxShadow: { - 'outline': '0 0 0 3px rgba(0, 48, 138, 0.5)', - 'solid': '2px 2px 0 0 rgba(0, 0, 0, 0.2)', - 'glow': '0 0 8px rgba(230, 180, 0, 0.8)', - '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-2': '0 2px 4px 0 rgba(0, 0, 0, 0.15)', - '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-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)', - '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)', - 'modal': '0 8px 32px rgba(0, 0, 0, 0.2), 0 4px 16px rgba(0, 0, 0, 0.15)', - }, - animation: { - 'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite', - 'spin-slow': 'spin 3s linear infinite', - }, - transitionDuration: { - '2000': '2000ms', - '3000': '3000ms', - }, - screens: { - '3xl': '1920px', - '4xl': '2560px', - }, - }, - }, - plugins: [ - - ], -} + content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], + theme: { + extend: { + colors: { + // 主色调 - 空军蓝 + primary: { + 50: "#e8f2ff", + 100: "#c5d9f7", + 150: "#EDF0F8", // via color + 200: "#9fc0ef", + 250: "#E6E9F0", // from color + 300: "#78a7e7", + 350: "#D8E2EF", // to color + 400: "#528edf", + 500: "#00308a", + 600: "#00256b", + 700: "#001a4c", + 800: "#000f2d", + 900: "#00040e", + DEFAULT: "#00308a", + }, + // 辅助色 - 军事灰 + secondary: { + 50: "#f5f5f5", + 100: "#e0e0e0", + 200: "#c2c2c2", + 300: "#a3a3a3", + 350: "#97A9C4", // New color inserted + 400: "#858585", + 500: "#666666", + 600: "#4d4d4d", + 700: "#333333", + 800: "#1a1a1a", + 900: "#0d0d0d", + DEFAULT: "#4d4d4d", + }, + // 强调色 - 军徽金 + accent: { + 50: "#fff8e5", + 100: "#ffecb3", + 200: "#ffe080", + 300: "#ffd44d", + 400: "#ffc81a", + 500: "#e6b400", + 600: "#b38f00", + 700: "#806a00", + 800: "#4d4000", + 900: "#1a1500", + DEFAULT: "#e6b400", + }, + // 功能色 + success: "#28a745", + warning: "#ffc107", + danger: "#dc3545", + info: "#17a2b8", + }, + fontFamily: { + sans: ["Inter", "sans-serif"], + heading: ["Bebas Neue", "sans-serif"], + mono: ["Source Code Pro", "monospace"], + }, + spacing: { + "72": "18rem", + "84": "21rem", + "96": "24rem", + }, + borderRadius: { + xl: "1rem", + "2xl": "2rem", + "3xl": "3rem", + }, + boxShadow: { + outline: "0 0 0 3px rgba(0, 48, 138, 0.5)", + solid: "2px 2px 0 0 rgba(0, 0, 0, 0.2)", + glow: "0 0 8px rgba(230, 180, 0, 0.8)", + 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-2": "0 2px 4px 0 rgba(0, 0, 0, 0.15)", + "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-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)", + 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)", + modal: "0 8px 32px rgba(0, 0, 0, 0.2), 0 4px 16px rgba(0, 0, 0, 0.15)", + }, + animation: { + "pulse-slow": "pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite", + "spin-slow": "spin 3s linear infinite", + }, + transitionDuration: { + "2000": "2000ms", + "3000": "3000ms", + }, + screens: { + "3xl": "1920px", + "4xl": "2560px", + }, + }, + }, + plugins: [], +}; diff --git a/packages/tus/src/handlers/PatchHandler.ts b/packages/tus/src/handlers/PatchHandler.ts index 4197007..72da508 100644 --- a/packages/tus/src/handlers/PatchHandler.ts +++ b/packages/tus/src/handlers/PatchHandler.ts @@ -62,7 +62,7 @@ export class PatchHandler extends BaseHandler { try { // 从请求中获取文件ID const id = this.getFileIdFromRequest(req) - console.log('id', id) + // console.log('id', id) if (!id) { throw ERRORS.FILE_NOT_FOUND }