From 76db4ffcf25059987cfdad8d9b139da32d399365 Mon Sep 17 00:00:00 2001 From: longdayi <13477510+longdayilongdayi@user.noreply.gitee.com> Date: Wed, 22 Jan 2025 23:19:58 +0800 Subject: [PATCH] 01222319 --- apps/web/src/app/auth/login.tsx | 68 ++ apps/web/src/app/auth/page.tsx | 145 +++++ apps/web/src/app/auth/register.tsx | 203 ++++++ apps/web/src/app/auth/types.ts | 10 + apps/web/src/app/login.tsx | 602 +++++++++++------- apps/web/src/app/main/help/page.tsx | 3 + apps/web/src/app/main/letter/list/Header.tsx | 20 + .../src/app/main/letter/list/LetterCard.tsx | 63 ++ .../src/app/main/letter/list/Pagination.tsx | 167 +++++ .../src/app/main/letter/list/SearchFilter.tsx | 84 +++ .../web/src/app/main/letter/list/constants.ts | 56 ++ apps/web/src/app/main/letter/list/page.tsx | 139 +--- apps/web/src/app/main/letter/list/types.ts | 16 + .../app/main/letter/list/useLetterFilters.ts | 33 + apps/web/src/app/main/letter/list/utils.ts | 25 + .../web/src/app/main/letter/progress/page.tsx | 120 ++++ apps/web/src/app/main/letter/write/filter.tsx | 41 ++ apps/web/src/app/main/letter/write/header.tsx | 46 ++ apps/web/src/app/main/letter/write/page.tsx | 80 +-- .../web/src/components/layout/main/Header.tsx | 51 +- .../src/components/layout/main/constants.tsx | 8 +- .../src/components/layout/main/navigation.tsx | 41 ++ .../src/components/layout/main/useNavItem.ts | 38 ++ apps/web/src/providers/theme-provider.tsx | 56 -- apps/web/src/routes/index.tsx | 19 +- packages/client/src/api/hooks/useTerm.ts | 6 +- packages/common/src/constants.ts | 10 +- 27 files changed, 1590 insertions(+), 560 deletions(-) create mode 100644 apps/web/src/app/auth/login.tsx create mode 100644 apps/web/src/app/auth/page.tsx create mode 100644 apps/web/src/app/auth/register.tsx create mode 100644 apps/web/src/app/auth/types.ts create mode 100644 apps/web/src/app/main/help/page.tsx create mode 100644 apps/web/src/app/main/letter/list/Header.tsx create mode 100644 apps/web/src/app/main/letter/list/LetterCard.tsx create mode 100644 apps/web/src/app/main/letter/list/Pagination.tsx create mode 100644 apps/web/src/app/main/letter/list/SearchFilter.tsx create mode 100644 apps/web/src/app/main/letter/list/constants.ts create mode 100644 apps/web/src/app/main/letter/list/types.ts create mode 100644 apps/web/src/app/main/letter/list/useLetterFilters.ts create mode 100644 apps/web/src/app/main/letter/list/utils.ts create mode 100644 apps/web/src/app/main/letter/progress/page.tsx create mode 100644 apps/web/src/app/main/letter/write/filter.tsx create mode 100644 apps/web/src/app/main/letter/write/header.tsx create mode 100644 apps/web/src/components/layout/main/navigation.tsx create mode 100644 apps/web/src/components/layout/main/useNavItem.ts diff --git a/apps/web/src/app/auth/login.tsx b/apps/web/src/app/auth/login.tsx new file mode 100644 index 0000000..fb481d3 --- /dev/null +++ b/apps/web/src/app/auth/login.tsx @@ -0,0 +1,68 @@ +import { motion } from "framer-motion"; + +export const LoginForm = ({ form, onSubmit, isLoading }) => { + const { register, handleSubmit, formState: { errors } } = form; + return ( + +

+ Sign In +

+
+
+ + {errors.username && ( + + {errors.username.message} + + )} +
+ +
+ + {errors.password && ( + + {errors.password.message} + + )} +
+
+ + +
+ ); +}; \ No newline at end of file diff --git a/apps/web/src/app/auth/page.tsx b/apps/web/src/app/auth/page.tsx new file mode 100644 index 0000000..2cc7f86 --- /dev/null +++ b/apps/web/src/app/auth/page.tsx @@ -0,0 +1,145 @@ +import React, { useState, useRef, useEffect } from "react"; +import { useLocation, useNavigate } from "react-router-dom"; +import { toast } from "react-hot-toast"; +import { AnimatePresence, motion } from "framer-motion"; +import { useForm } from "react-hook-form"; +import { useAuth } from "@web/src/providers/auth-provider"; +import { RegisterForm } from "./register"; +import { LoginForm } from "./login"; +import { LoginFormInputs, RegisterFormInputs } from "./types"; +import { Button } from "@web/src/components/common/element/Button"; + +const AuthPage: React.FC = () => { + const [showLogin, setShowLogin] = useState(true); + const [isLoading, setIsLoading] = useState(false); + const { login, isAuthenticated, signup } = useAuth(); + const location = useLocation(); + const navigate = useNavigate(); + const loginForm = useForm({ + mode: "onChange" + }); + const registerForm = useForm({ + mode: "onChange" + }); + + const onSubmitLogin = async (data: LoginFormInputs) => { + try { + setIsLoading(true); + console.log(data) + await login(data.username, data.password); + toast.success("Welcome back!"); + } catch (err: any) { + toast.error(err?.response?.data?.message || "Invalid credentials"); + } finally { + setIsLoading(false); + } + }; + + const onSubmitRegister = async (data: RegisterFormInputs) => { + try { + setIsLoading(true); + await signup(data); + toast.success("Registration successful!"); + setShowLogin(true); + } catch (err: any) { + toast.error(err?.response?.data?.message); + } finally { + setIsLoading(false); + } + }; + + useEffect(() => { + if (isAuthenticated) { + const params = new URLSearchParams(location.search); + const redirectUrl = params.get("redirect_url") || "/"; + navigate(redirectUrl, { replace: true }); + } + }, [isAuthenticated, location]); + + return ( +
+ +
+ {/* Left Panel */} + + + {/* Logo Section */} +
+
+ United States Air Force Logo +
+
+
+ + {/* Title Section */} +
+

+ + USAF Leadership Portal + +

+

+ {showLogin + ? "Access your secure USAF portal" + : "Create your authorized account"} +

+
+ + {/* Switch Form Button */} + + + + + {/* Right Panel - Forms */} +
+ + {showLogin ? ( + + ) : ( + + )} + +
+
+
+
+ ); +}; + +export default AuthPage; diff --git a/apps/web/src/app/auth/register.tsx b/apps/web/src/app/auth/register.tsx new file mode 100644 index 0000000..75c2343 --- /dev/null +++ b/apps/web/src/app/auth/register.tsx @@ -0,0 +1,203 @@ +import { motion } from "framer-motion"; + +// RegisterForm.tsx +export const RegisterForm = ({ form, onSubmit, isLoading }) => { + const { register, handleSubmit, formState: { errors }, watch } = form; + const password = watch("password"); + + return ( + +

+ Create Account +

+ +
+ {/* Department Selection */} +
+ + + {errors.deptId && ( + + {errors.deptId.message} + + )} +
+ + {/* User Information Row */} +
+
+ + + {errors.username && ( + + {errors.username.message} + + )} +
+ +
+ + + {errors.showname && ( + + {errors.showname.message} + + )} +
+
+ + {/* Service ID */} +
+ + + {errors.officerId && ( + + {errors.officerId.message} + + )} +
+ + {/* Password Fields */} +
+
+ + + {errors.password && ( + + {errors.password.message} + + )} +
+ +
+ + value === password || "Passwords do not match" + })} + className="w-full px-4 py-2 bg-white/10 border border-gray-600 + rounded-lg focus:ring-2 focus:ring-blue-500 + text-white placeholder-gray-400" + placeholder="Confirm Password" + /> + {errors.repeatPass && ( + + {errors.repeatPass.message} + + )} +
+
+
+ {/* Submit Button */} + + +
+ ); +}; \ No newline at end of file diff --git a/apps/web/src/app/auth/types.ts b/apps/web/src/app/auth/types.ts new file mode 100644 index 0000000..6d6b894 --- /dev/null +++ b/apps/web/src/app/auth/types.ts @@ -0,0 +1,10 @@ +export interface LoginFormInputs { + username: string; + password: string; +} +export interface RegisterFormInputs extends LoginFormInputs { + deptId: string; + officerId: string; + showname: string; + repeatPass: string; +} \ No newline at end of file diff --git a/apps/web/src/app/login.tsx b/apps/web/src/app/login.tsx index f2a9f1b..a48bfc8 100644 --- a/apps/web/src/app/login.tsx +++ b/apps/web/src/app/login.tsx @@ -1,46 +1,54 @@ import React, { useState, useRef, useEffect } from "react"; -import { Form, Input, Button, message, Row, Col } from "antd"; import { useLocation, useNavigate } from "react-router-dom"; import { useAuth } from "../providers/auth-provider"; -import DepartmentSelect from "../components/models/department/department-select"; -import SineWave from "../components/animation/sine-wave"; +import { toast } from "react-hot-toast"; +import { AnimatePresence, motion } from "framer-motion"; +import { useForm } from "react-hook-form"; +interface LoginFormInputs { + username: string; + password: string; +} +interface RegisterFormInputs extends LoginFormInputs { + deptId: string; + officerId: string; + showname: string; + repeatPass: string; +} const LoginPage: React.FC = () => { const [showLogin, setShowLogin] = useState(true); - const [registerLoading, setRegisterLoading] = useState(false); - const { - login, - isAuthenticated, - signup - } = useAuth() - const loginFormRef = useRef(null); - const registerFormRef = useRef(null); + const [isLoading, setIsLoading] = useState(false); + const { login, isAuthenticated, signup } = useAuth(); const location = useLocation(); const navigate = useNavigate(); - const onFinishLogin = async (values: any) => { + const loginForm = useForm({ + mode: "onChange" + }); + const registerForm = useForm({ + mode: "onChange" + }); + const onSubmitLogin = async (data: LoginFormInputs) => { try { - const { username, password } = values; - await login(username, password); + setIsLoading(true); + await login(data.username, data.password); + toast.success("Welcome back!"); } catch (err: any) { - message.error(err?.response?.data?.message || "帐号或密码错误!"); - console.error(err); - } - }; - - const onFinishRegister = async (values: any) => { - setRegisterLoading(true); - const { username, password, deptId, officerId, showname } = values; - try { - await signup({ username, password, deptId, officerId, showname }); - message.success("注册成功!"); - setShowLogin(true); - // loginFormRef.current.submit(); - } catch (err: any) { - message.error(err?.response?.data?.message); + toast.error(err?.response?.data?.message || "Invalid credentials"); } finally { - setRegisterLoading(false); + setIsLoading(false); + } + }; + const onSubmitRegister = async (data: RegisterFormInputs) => { + try { + setIsLoading(true); + await signup(data); + toast.success("Registration successful!"); + setShowLogin(true); + } catch (err: any) { + toast.error(err?.response?.data?.message); + } finally { + setIsLoading(false); } }; - useEffect(() => { if (isAuthenticated) { const params = new URLSearchParams(location.search); @@ -50,224 +58,330 @@ const LoginPage: React.FC = () => { }, [isAuthenticated, location]); return ( -
-
-
- {showLogin ? ( -
- -
没有账号?
-
- 点击注册一个属于你自己的账号吧! -
-
setShowLogin(false)} - className="hover:translate-y-1 my-8 p-2 text-center rounded-xl border-white border hover:bg-white hover:text-primary hover:shadow-xl hover:cursor-pointer transition-all"> - 注册 -
+
+ +
+ +
+ USAF Logo +

+ USAF Leadership Portal +

+

+ {showLogin ? "Need an account?" : "Already registered?"} +

+
- ) : ( -
-
注册小贴士
-
- 请认真填写用户信息哦! -
-
setShowLogin(true)} - className="hover:translate-y-1 my-8 rounded-xl text-center border-white border p-2 hover:bg-white hover:text-primary hover:shadow-xl hover:cursor-pointer transition-all"> - 返回登录 -
- -
- )} -
-
- {showLogin ? ( - <> -
- 登录 -
-
- - - + - - - -
- -
-
- - ) : ( -
-
- 注册 -
-
- - - - - - - - - - - - - - - - - - - - - - ({ - validator(_, value) { - if ( - !value || - getFieldValue( - "password" - ) === value - ) { - return Promise.resolve(); - } - return Promise.reject( - new Error( - "两次输入的密码不一致" - ) - ); - }, - }), - ]}> - - - -
- -
-
-
- )} + {/* Right Panel */} +
+ + {showLogin ? ( + + ) : ( + + )} + +
-
+
); }; +// RegisterForm.tsx +const RegisterForm = ({ form, onSubmit, isLoading }) => { + const { register, handleSubmit, formState: { errors }, watch } = form; + const password = watch("password"); + + return ( + +

+ Create Account +

+ +
+ {/* Department Selection */} +
+ + + {errors.deptId && ( + + {errors.deptId.message} + + )} +
+ + {/* User Information Row */} +
+
+ + + {errors.username && ( + + {errors.username.message} + + )} +
+ +
+ + + {errors.showname && ( + + {errors.showname.message} + + )} +
+
+ + {/* Service ID */} +
+ + + {errors.officerId && ( + + {errors.officerId.message} + + )} +
+ + {/* Password Fields */} +
+
+ + + {errors.password && ( + + {errors.password.message} + + )} +
+ +
+ + value === password || "Passwords do not match" + })} + className="w-full px-4 py-2 bg-white/10 border border-gray-600 + rounded-lg focus:ring-2 focus:ring-blue-500 + text-white placeholder-gray-400" + placeholder="Confirm Password" + /> + {errors.repeatPass && ( + + {errors.repeatPass.message} + + )} +
+
+
+ {/* Submit Button */} + + +
+ ); +}; +const LoginForm = ({ form, onSubmit, isLoading }) => { + const { register, handleSubmit, formState: { errors } } = form; + + return ( + +

+ Sign In +

+ +
+
+ + {errors.username && ( + + {errors.username.message} + + )} +
+ +
+ + {errors.password && ( + + {errors.password.message} + + )} +
+
+ + +
+ ); +}; export default LoginPage; diff --git a/apps/web/src/app/main/help/page.tsx b/apps/web/src/app/main/help/page.tsx new file mode 100644 index 0000000..d860461 --- /dev/null +++ b/apps/web/src/app/main/help/page.tsx @@ -0,0 +1,3 @@ +export default function HelpPage() { + return <>help +} \ No newline at end of file diff --git a/apps/web/src/app/main/letter/list/Header.tsx b/apps/web/src/app/main/letter/list/Header.tsx new file mode 100644 index 0000000..0764ee9 --- /dev/null +++ b/apps/web/src/app/main/letter/list/Header.tsx @@ -0,0 +1,20 @@ +import { SearchFilters } from "./SearchFilter"; + +export function Header() { + return ( +
+

公开信件列表

+
+

+ 服务宗旨:畅通官兵诉求渠道 • 促进部队建设发展 • 提升单位战斗力 +

+
+ 实时跟踪反馈进度 + 保障信息传递安全 + 高效解决实际问题 +
+
+ +
+ ); +} diff --git a/apps/web/src/app/main/letter/list/LetterCard.tsx b/apps/web/src/app/main/letter/list/LetterCard.tsx new file mode 100644 index 0000000..b6b78ec --- /dev/null +++ b/apps/web/src/app/main/letter/list/LetterCard.tsx @@ -0,0 +1,63 @@ +import { StarIcon } from '@heroicons/react/24/outline'; +import { Letter } from './types'; +import { getBadgeStyle } from './utils'; + +interface LetterCardProps { + letter: Letter; +} + +export function LetterCard({ letter }: LetterCardProps) { + return ( +
+
+ {/* Header Section */} + + + {/* Meta Information */} +
+ {letter.sender} | {letter.unit} + {letter.date} +
+ + {/* Badges Section */} +
+ + + {letter.priority && } +
+ + {/* Content Preview */} + {letter.content && ( +

+ {letter.content} +

+ )} +
+
+ ); +} + +function Badge({ type, value }: { type: 'priority' | 'category' | 'status'; value: string }) { + return ( + + {value.toUpperCase()} + + ); +} diff --git a/apps/web/src/app/main/letter/list/Pagination.tsx b/apps/web/src/app/main/letter/list/Pagination.tsx new file mode 100644 index 0000000..f6df121 --- /dev/null +++ b/apps/web/src/app/main/letter/list/Pagination.tsx @@ -0,0 +1,167 @@ +import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/24/outline'; +import { useMemo } from 'react'; + +interface PaginationProps { + totalItems: number; + itemsPerPage: number; + currentPage?: number; + onPageChange?: (page: number) => void; +} +export function Pagination({ + totalItems, + itemsPerPage, + currentPage = 1, + onPageChange = () => { }, +}: PaginationProps) { + const STYLE_CONFIG = { + colors: { + primary: 'bg-[#003875]', // USAF Blue + hover: 'hover:bg-[#00264d]', // Darker USAF Blue + disabled: 'bg-[#e6e6e6]', + text: { + primary: 'text-[#003875]', + light: 'text-white', + secondary: 'text-[#4a4a4a]' + } + }, + components: { + container: ` + flex items-center justify-between + px-4 py-3 sm:px-6 + `, + pagination: ` + inline-flex shadow-sm rounded-md + divide-x divide-gray-200 + `, + button: ` + relative inline-flex items-center justify-center + min-w-[2.5rem] h-10 + text-sm font-medium + transition-colors duration-200 + focus:outline-none focus:ring-2 focus:ring-[#003875] focus:ring-offset-2 + ` + } + }; + // Memoized calculations + const totalPages = useMemo(() => Math.ceil(totalItems / itemsPerPage), [totalItems, itemsPerPage]); + const startItem = (currentPage - 1) * itemsPerPage + 1; + const endItem = Math.min(currentPage * itemsPerPage, totalItems); + + // Navigation handlers + const handlePrevious = () => currentPage > 1 && onPageChange(currentPage - 1); + const handleNext = () => currentPage < totalPages && onPageChange(currentPage + 1); + + // Generate page numbers with improved logic + const getPageNumbers = () => { + const maxVisiblePages = 5; + if (totalPages <= maxVisiblePages) { + return Array.from({ length: totalPages }, (_, i) => i + 1); + } + if (currentPage <= 3) { + return [1, 2, 3, '...', totalPages]; + } + if (currentPage >= totalPages - 2) { + return [1, '...', totalPages - 2, totalPages - 1, totalPages]; + } + return [ + 1, + '...', + currentPage - 1, + currentPage, + currentPage + 1, + '...', + totalPages + ]; + }; + + const renderPageButton = (pageNum: number | string, index: number) => { + if (pageNum === '...') { + return ( + + … + + ); + } + + const isCurrentPage = currentPage === pageNum; + const buttonStyle = isCurrentPage + ? ` + ${STYLE_CONFIG.components.button} + ${STYLE_CONFIG.colors.primary} + ${STYLE_CONFIG.colors.text.light} + ${STYLE_CONFIG.colors.hover} + ` + : ` + ${STYLE_CONFIG.components.button} + bg-white + ${STYLE_CONFIG.colors.text.primary} + hover:bg-gray-50 + `; + + return ( + + ); + }; + return ( +
+
+
+

+ Showing {startItem} to{' '} + {endItem} of{' '} + {totalItems} results +

+
+ + +
+
+ ); +} \ No newline at end of file diff --git a/apps/web/src/app/main/letter/list/SearchFilter.tsx b/apps/web/src/app/main/letter/list/SearchFilter.tsx new file mode 100644 index 0000000..5825439 --- /dev/null +++ b/apps/web/src/app/main/letter/list/SearchFilter.tsx @@ -0,0 +1,84 @@ +import { MagnifyingGlassIcon } from '@heroicons/react/24/outline'; + +interface SearchFiltersProps { + searchTerm: string; + onSearchChange: (value: string) => void; + filterCategory: string; + onCategoryChange: (value: string) => void; + filterStatus: string; + onStatusChange: (value: string) => void; + className?: string +} +export function SearchFilters({ + searchTerm, + onSearchChange, + filterCategory, + onCategoryChange, + filterStatus, + onStatusChange, + className +}: SearchFiltersProps) { + return ( +
+
+
+ + onSearchChange(e.target.value)} + /> +
+
+ +
+ ); +} + +function FilterDropdowns({ + filterCategory, + onCategoryChange, + filterStatus, + onStatusChange, +}: Pick) { + const selectClassName = `min-w-[160px] h-[46px] px-4 rounded-lg + bg-white shadow-sm transition-all duration-200 + text-[#041E42] font-medium + focus:outline-none focus:border-[#00308F] focus:ring-2 focus:ring-[#00308F]/20 + hover:border-[#00308F]`; + return ( +
+ + +
+ ); +} \ No newline at end of file diff --git a/apps/web/src/app/main/letter/list/constants.ts b/apps/web/src/app/main/letter/list/constants.ts new file mode 100644 index 0000000..7858fc6 --- /dev/null +++ b/apps/web/src/app/main/letter/list/constants.ts @@ -0,0 +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 diff --git a/apps/web/src/app/main/letter/list/page.tsx b/apps/web/src/app/main/letter/list/page.tsx index f114d34..3a68fa1 100644 --- a/apps/web/src/app/main/letter/list/page.tsx +++ b/apps/web/src/app/main/letter/list/page.tsx @@ -1,118 +1,37 @@ -import { useState } from 'react'; -import { - MagnifyingGlassIcon, - StarIcon, - TrashIcon, - EnvelopeIcon, - FunnelIcon, -} from '@heroicons/react/24/outline'; -import { StarIcon as StarIconSolid } from '@heroicons/react/24/solid'; -interface Letter { - id: string; - sender: string; - subject: string; - date: string; - priority: 'high' | 'medium' | 'low'; - isRead: boolean; - isStarred: boolean; -} +import { letters } from "./constants"; +import { Header } from "./Header"; +import { LetterCard } from "./LetterCard"; +import { Pagination } from "./Pagination"; +import { SearchFilters } from "./SearchFilter"; +import { useLetterFilters } from "./useLetterFilters"; export default function LetterListPage() { - const [letters, setLetters] = useState([ - { - id: '1', - sender: 'Gen. Charles Q. Brown Jr.', - subject: 'Strategic Force Posture Update', - date: '2024-01-22', - priority: 'high', - isRead: false, - isStarred: true, - }, - // ... existing code ... - ]); - - const [searchTerm, setSearchTerm] = useState(''); - const [selectedFilter, setSelectedFilter] = useState('all'); + const { + searchTerm, + setSearchTerm, + filterCategory, + setFilterCategory, + filterStatus, + setFilterStatus, + filteredLetters, + } = useLetterFilters(letters); return ( -
- {/* Header */} -
-
- -

USAF Leadership Mailbox

-
-
- - {/* Main Content */} -
- {/* Search and Filter Bar */} -
-
- - setSearchTerm(e.target.value)} - /> -
-
- - -
-
- - {/* Letters List */} -
- {letters.map((letter) => ( -
-
- -
-
- {letter.sender} - {letter.priority === 'high' && ( - - High Priority - - )} -
-
{letter.subject}
-
-
{letter.date}
- -
-
- ))} -
-
+ // 添加 flex flex-col 使其成为弹性布局容器 +
+
+ {/* 添加 flex-grow 使内容区域自动填充剩余空间 */} +
+ {filteredLetters.map((letter) => ( + + ))} +
+ {/* Pagination 会自然固定在底部 */} +
); } diff --git a/apps/web/src/app/main/letter/list/types.ts b/apps/web/src/app/main/letter/list/types.ts new file mode 100644 index 0000000..d6c1a4a --- /dev/null +++ b/apps/web/src/app/main/letter/list/types.ts @@ -0,0 +1,16 @@ +export type Priority = 'high' | 'medium' | 'low'; +export type Category = 'complaint' | 'suggestion' | 'request' | 'feedback'; +export type Status = 'pending' | 'in-progress' | '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 diff --git a/apps/web/src/app/main/letter/list/useLetterFilters.ts b/apps/web/src/app/main/letter/list/useLetterFilters.ts new file mode 100644 index 0000000..3dd7f74 --- /dev/null +++ b/apps/web/src/app/main/letter/list/useLetterFilters.ts @@ -0,0 +1,33 @@ +import { useState, useMemo } from 'react'; +import { Letter } from './types'; + +export function useLetterFilters(letters: Letter[]) { + const [currentPage, setCurrentPage] = useState(1); + const [searchTerm, setSearchTerm] = useState(''); + const [filterStatus, setFilterStatus] = useState('all'); + const [filterCategory, setFilterCategory] = useState('all'); + + const filteredLetters = useMemo(() => { + return letters.filter(letter => { + const matchesSearch = [letter.title, letter.sender, letter.unit] + .some(field => field.toLowerCase().includes(searchTerm.toLowerCase())); + + const matchesCategory = filterCategory === 'all' || letter.category === filterCategory; + const matchesStatus = filterStatus === 'all' || letter.status === filterStatus; + + return matchesSearch && matchesCategory && matchesStatus; + }); + }, [letters, searchTerm, filterCategory, filterStatus]); + + return { + currentPage, + setCurrentPage, + searchTerm, + setSearchTerm, + filterStatus, + setFilterStatus, + filterCategory, + setFilterCategory, + filteredLetters, + }; +} diff --git a/apps/web/src/app/main/letter/list/utils.ts b/apps/web/src/app/main/letter/list/utils.ts new file mode 100644 index 0000000..173cfc5 --- /dev/null +++ b/apps/web/src/app/main/letter/list/utils.ts @@ -0,0 +1,25 @@ +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', + }, +} as const; + +export const getBadgeStyle = ( + 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'; +}; diff --git a/apps/web/src/app/main/letter/progress/page.tsx b/apps/web/src/app/main/letter/progress/page.tsx new file mode 100644 index 0000000..95722c5 --- /dev/null +++ b/apps/web/src/app/main/letter/progress/page.tsx @@ -0,0 +1,120 @@ +import { useState } from 'react' + +interface FeedbackStatus { + status: 'pending' | 'in-progress' | 'resolved' + ticketId: string + submittedDate: string + lastUpdate: string + title: string +} + +export default function LetterProgressPage() { + const [feedbackId, setFeedbackId] = useState('') + const [status, setStatus] = useState(null) + + // Mock data - In production this would come from an API + const mockLookup = () => { + setStatus({ + status: 'in-progress', + ticketId: 'USAF-2025-0123', + submittedDate: '2025-01-15', + lastUpdate: '2025-01-21', + title: 'Aircraft Maintenance Schedule Inquiry' + }) + } + + return ( +
+ {/* Header */} +
+

+ USAF Feedback Progress Tracking +

+
+

请输入您的问题编号以查询处理进度

+
+
+ + {/* Main Content */} +
+ {/* Search Section */} +
+ +
+ setFeedbackId(e.target.value)} + placeholder="e.g. USAF-2025-0123" + className="flex-1 p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-[#00538B] focus:border-transparent" + /> + +
+
+ {/* Results Section */} + {status && ( +
+
+
+

+ Feedback Details +

+ + {status.status.toUpperCase()} + +
+ +
+
+

Ticket ID

+

{status.ticketId}

+
+
+

Submitted Date

+

{status.submittedDate}

+
+
+

Last Update

+

{status.lastUpdate}

+
+
+

Title

+

{status.title}

+
+
+ + {/* Progress Timeline */} +
+
+
+
+ {['Submitted', 'In Review', 'Resolved'].map((step, index) => ( +
+
+ {index + 1} +
+
{step}
+
+ ))} +
+
+
+
+
+ )} +
+
+ ) +} diff --git a/apps/web/src/app/main/letter/write/filter.tsx b/apps/web/src/app/main/letter/write/filter.tsx new file mode 100644 index 0000000..ffede5b --- /dev/null +++ b/apps/web/src/app/main/letter/write/filter.tsx @@ -0,0 +1,41 @@ +import { FunnelIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline"; +import { leaders } from "./mock"; +import { useMemo, useState } from "react"; +import { Leader } from "./types"; + +export default function Filter() { + const [selectedLeader, setSelectedLeader] = useState(null); + const [searchQuery, setSearchQuery] = useState(''); + const [selectedDivision, setSelectedDivision] = useState('all'); + + const divisions = useMemo(() => { + return ['all', ...new Set(leaders.map(leader => leader.division))]; + }, []); + + return
+
+ + setSearchQuery(e.target.value)} + /> +
+
+ + +
+
+} \ No newline at end of file diff --git a/apps/web/src/app/main/letter/write/header.tsx b/apps/web/src/app/main/letter/write/header.tsx new file mode 100644 index 0000000..0b1b6bd --- /dev/null +++ b/apps/web/src/app/main/letter/write/header.tsx @@ -0,0 +1,46 @@ +export default function Header() { + return
+
+ {/* 主标题 */} +
+

+ 信件投递入口 +

+

+ 保护您隐私的信件传输平台 +

+
+ + {/* 隐私保护说明 */} +
+
+ + + + 个人信息严格保密 +
+
+ + + + 支持匿名反映问题 +
+
+ + + + 网络信息加密存储 +
+
+ + {/* 隐私承诺 */} +
+

我们承诺:您的个人信息将被严格保密,不向任何第三方透露。您可以选择匿名反映问题,平台会自动过滤可能暴露身份的信息。

+
+
+
+ +} \ No newline at end of file diff --git a/apps/web/src/app/main/letter/write/page.tsx b/apps/web/src/app/main/letter/write/page.tsx index dcef8ca..ecf8934 100644 --- a/apps/web/src/app/main/letter/write/page.tsx +++ b/apps/web/src/app/main/letter/write/page.tsx @@ -3,6 +3,8 @@ import { motion } from 'framer-motion'; import { FunnelIcon, MagnifyingGlassIcon, PaperAirplaneIcon } from '@heroicons/react/24/outline'; import { Leader } from './types'; import { leaders } from './mock'; +import Header from './header'; +import Filter from './filter'; export default function WriteLetterPage() { @@ -25,83 +27,11 @@ export default function WriteLetterPage() { return (
- {/* Header Banner */} -
-
-
- {/* 主标题 */} -
-

- 信件投递入口 -

-

- 保护您隐私的信件传输平台 -

-
- - {/* 隐私保护说明 */} -
-
- - - - 个人信息严格保密 -
-
- - - - 支持匿名反映问题 -
-
- - - - 网络信息加密存储 -
-
- - {/* 隐私承诺 */} -
-

我们承诺:您的个人信息将被严格保密,不向任何第三方透露。您可以选择匿名反映问题,平台会自动过滤可能暴露身份的信息。

-
-
-
-
- - +
{/* 搜索和筛选区域 */} -
-
-
- - setSearchQuery(e.target.value)} - /> -
-
- - -
-
+
+ {/* Modified Leader Cards Grid */}
{filteredLeaders.map((leader) => ( diff --git a/apps/web/src/components/layout/main/Header.tsx b/apps/web/src/components/layout/main/Header.tsx index 973b47b..a8cb668 100644 --- a/apps/web/src/components/layout/main/Header.tsx +++ b/apps/web/src/components/layout/main/Header.tsx @@ -1,25 +1,20 @@ import { MagnifyingGlassIcon, UserIcon } from "@heroicons/react/24/outline"; import { Link, NavLink } from "react-router-dom"; import { memo } from "react"; -import { NAV_ITEMS } from "./constants"; import { SearchBar } from "./SearchBar"; import { Logo } from "./Logo"; - +import Navigation from "./navigation"; interface HeaderProps { onSearch?: (query: string) => void; } - export const Header = memo(function Header({ onSearch }: HeaderProps) { return (
- {/* Main Content */}
- - {/* User Actions */}
- - {/* Navigation */} - +
); diff --git a/apps/web/src/components/layout/main/constants.tsx b/apps/web/src/components/layout/main/constants.tsx index f075f25..3deda80 100644 --- a/apps/web/src/components/layout/main/constants.tsx +++ b/apps/web/src/components/layout/main/constants.tsx @@ -1,7 +1 @@ -export const NAV_ITEMS = [ - { to: "/write-letter", label: "Write Letter" }, - { to: "/search-letter", label: "Search Letters" }, - { to: "/public-letters", label: "Public Letters" }, - { to: "/announcements", label: "Announcements" }, - { to: "/guidelines", label: "Guidelines" }, -] as const; +export { } \ No newline at end of file diff --git a/apps/web/src/components/layout/main/navigation.tsx b/apps/web/src/components/layout/main/navigation.tsx new file mode 100644 index 0000000..e883965 --- /dev/null +++ b/apps/web/src/components/layout/main/navigation.tsx @@ -0,0 +1,41 @@ +import { NavLink } from "react-router-dom"; +import { useNavItem } from "./useNavItem"; + +export default function Navigation() { + const { navItems } = useNavItem() + return +} \ No newline at end of file diff --git a/apps/web/src/components/layout/main/useNavItem.ts b/apps/web/src/components/layout/main/useNavItem.ts new file mode 100644 index 0000000..959c568 --- /dev/null +++ b/apps/web/src/components/layout/main/useNavItem.ts @@ -0,0 +1,38 @@ +import { api } from "@nice/client"; +import { TaxonomySlug } from "@nice/common"; +import { useMemo } from "react"; +// export const NAV_ITEMS = [ + +// { to: "/write-letter", label: "咨询求助" }, +// { to: "/write-letter", label: "解难帮困" }, +// { to: "/write-letter", label: "需求提报" }, +// { to: "/write-letter", label: "意见建议" }, +// { to: "/write-letter", label: "问题反映" }, +// { to: "/write-letter", label: "举报投诉" }, + +// ] as const; + +export function useNavItem() { + const { data } = api.term.findMany.useQuery({ + where: { + taxonomy: { slug: TaxonomySlug.CATEGORY } + } + }); + + const navItems = useMemo(() => { + const defaultItems = [ + { to: "/letter-list", label: "公开信件" }, + { to: "/letter-progress", label: "进度查询" }, + { to: "/help", label: "使用帮助" }, + ]; + + if (!data) return defaultItems; + + return data.reduce((items, term) => { + items.push({ to: `/write-letter?category=${term.id}`, label: term.name }); + return items; + }, [{ to: "/letter-list", label: "公开信件" }].concat(defaultItems.slice(1))); + }, [data]); + + return { navItems }; +} \ No newline at end of file diff --git a/apps/web/src/providers/theme-provider.tsx b/apps/web/src/providers/theme-provider.tsx index 226a47c..e352aac 100644 --- a/apps/web/src/providers/theme-provider.tsx +++ b/apps/web/src/providers/theme-provider.tsx @@ -19,67 +19,11 @@ export const useAppTheme = () => useContext(AppThemeContext); export default function ThemeProvider({ children }: { children: ReactNode }) { const { token } = theme.useToken(); - const applyTheme = (tailwindTheme: TailwindTheme) => { for (let key in tailwindTheme) { document.documentElement.style.setProperty(key, tailwindTheme[key]); } }; - - // const agTheme = useMemo( - // () => - // themeQuartz.withPart(iconSetQuartzLight).withParams({ - // accentColor: token.colorPrimary, - // backgroundColor: token.colorBgContainer, - // borderColor: token.colorBorderSecondary, - // // borderRadius: 2, - // browserColorScheme: "light", - // cellHorizontalPaddingScale: 0.7, - - // fontSize: token.fontSize, - // foregroundColor: token.colorText, - // headerBackgroundColor: token.colorFillQuaternary, - // headerFontSize: token.fontSize, - // headerFontWeight: 600, - // headerTextColor: token.colorPrimary, - // rowBorder: true, - // rowVerticalPaddingScale: 0.9, - // sidePanelBorder: true, - // spacing: 6, - // oddRowBackgroundColor: token.colorFillQuaternary, - // wrapperBorder: true, - // wrapperBorderRadius: 0, - - // // headerRowBorder: true, - // // columnBorder: true, - // // headerRowBorder: true, - // pinnedRowBorder: true - // }), - // [token] - // ); - // const subTableTheme = useMemo( - // () => - // themeQuartz.withPart(iconSetQuartzLight).withParams({ - // accentColor: token.colorTextSecondary, // 可以使用不同的强调色 - // backgroundColor: token.colorBgLayout, - // borderColor: token.colorBorderSecondary, - // fontSize: token.fontSizeSM, // 可以使用不同的字体大小 - // foregroundColor: token.colorTextSecondary, - // headerBackgroundColor: token.colorFillSecondary, - // headerFontSize: token.fontSizeSM, - // headerFontWeight: 500, // 可以使用不同的字体粗细 - // headerTextColor: token.colorTextTertiary, - // rowBorder: false, // 可以选择不显示行边框 - // rowVerticalPaddingScale: 0.6, - // sidePanelBorder: false, - // spacing: 4, - // oddRowBackgroundColor: token.colorFillQuaternary, - // wrapperBorder: false, - // wrapperBorderRadius: 0, - // columnBorder: false, - // }), - // [token] - // ); const tailwindTheme: TailwindTheme = useMemo( () => ({ "--color-primary": token.colorPrimary, diff --git a/apps/web/src/routes/index.tsx b/apps/web/src/routes/index.tsx index 02fa67b..67c5c6b 100755 --- a/apps/web/src/routes/index.tsx +++ b/apps/web/src/routes/index.tsx @@ -11,7 +11,6 @@ import TermAdminPage from "../app/admin/term/page"; import StaffAdminPage from "../app/admin/staff/page"; import RoleAdminPage from "../app/admin/role/page"; import WithAuth from "../components/utils/with-auth"; -import LoginPage from "../app/login"; import BaseSettingPage from "../app/admin/base-setting/page"; import { MainLayout } from "../components/layout/main/MainLayout"; import HomePage from "../app/main/home/page"; @@ -22,6 +21,9 @@ import CourseSettingForm from "../components/models/course/editor/form/CourseSet import CourseEditorLayout from "../components/models/course/editor/layout/CourseEditorLayout"; import WriteLetterPage from "../app/main/letter/write/page"; import LetterListPage from "../app/main/letter/list/page"; +import LetterProgressPage from "../app/main/letter/progress/page"; +import HelpPage from "../app/main/help/page"; +import AuthPage from "../app/auth/page"; interface CustomIndexRouteObject extends IndexRouteObject { name?: string; @@ -69,10 +71,17 @@ export const routes: CustomRouteObject[] = [ element: , }, { - path: "list", + path: "letter-list", element: } - + , { + path: 'letter-progress', + element: + }, + { + path: 'help', + element: + } ], }, { @@ -213,9 +222,9 @@ export const routes: CustomRouteObject[] = [ ], }, { - path: "/login", + path: "/auth", breadcrumb: "登录", - element: , + element: , }, ]; diff --git a/packages/client/src/api/hooks/useTerm.ts b/packages/client/src/api/hooks/useTerm.ts index b32447c..23a3f96 100755 --- a/packages/client/src/api/hooks/useTerm.ts +++ b/packages/client/src/api/hooks/useTerm.ts @@ -20,8 +20,6 @@ export function useTerm() { }, }); - - const update = api.term.update.useMutation({ onSuccess: (result) => { queryClient.invalidateQueries({ queryKey }); @@ -61,11 +59,9 @@ export function useTerm() { }; return { create, - update, softDeleteByIds, getTerm, - upsertTags, - + upsertTags }; } diff --git a/packages/common/src/constants.ts b/packages/common/src/constants.ts index d6c8e08..a57be2b 100755 --- a/packages/common/src/constants.ts +++ b/packages/common/src/constants.ts @@ -60,15 +60,7 @@ export const InitTaxonomies: { name: string; slug: string }[] = [ { name: "分类", slug: TaxonomySlug.CATEGORY, - }, - { - name: "研判单元", - slug: TaxonomySlug.UNIT, - }, - { - name: "标签", - slug: TaxonomySlug.TAG, - }, + } ]; export const InitAppConfigs: Prisma.AppConfigCreateInput[] = [ {