This commit is contained in:
ditiqi 2025-01-26 09:28:38 +08:00
parent daae6a9018
commit a0447eab2f
11 changed files with 552 additions and 294 deletions

View File

@ -451,7 +451,11 @@ export class BaseService<
pageSize?: number; pageSize?: number;
where?: WhereArgs<A['findMany']>; where?: WhereArgs<A['findMany']>;
select?: SelectArgs<A['findMany']>; select?: SelectArgs<A['findMany']>;
}): Promise<{ items: R['findMany']; totalPages: number, totalCount: number }> { }): Promise<{
items: R['findMany'];
totalPages: number;
totalCount: number;
}> {
const { page = 1, pageSize = 10, where, select } = args; const { page = 1, pageSize = 10, where, select } = args;
try { try {
@ -470,7 +474,7 @@ export class BaseService<
return { return {
items, items,
totalPages, totalPages,
totalCount: total totalCount: total,
}; };
} catch (error) { } catch (error) {
this.handleError(error, 'read'); this.handleError(error, 'read');

View File

@ -0,0 +1,74 @@
export function Header() {
return (
<header className="bg-gradient-to-r from-primary to-primary-400 p-6 rounded-t-xl">
<div className="flex flex-col space-y-6">
{/* 主标题区域 */}
<div>
<h1 className="text-3xl font-bold tracking-wider text-white">
</h1>
<p className="mt-2 text-blue-100 text-lg">
</p>
</div>
{/* 服务特点说明 */}
<div className="flex flex-wrap gap-6 text-sm text-white">
<div className="flex items-center gap-2">
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"
/>
</svg>
<span></span>
</div>
<div className="flex items-center gap-2">
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M12 6v6m0 0v6m0-6h6m-6 0H6"
/>
</svg>
<span></span>
</div>
<div className="flex items-center gap-2">
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z"
/>
</svg>
<span></span>
</div>
</div>
{/* 服务宗旨说明 */}
<div className="text-sm text-blue-100 border-t border-blue-400/30 pt-4">
<p className="leading-relaxed">
</p>
</div>
</div>
</header>
);
}

View File

@ -0,0 +1,24 @@
import LetterList from "@web/src/components/models/post/list/LetterList";
import { Header } from "./Header";
import { useAuth } from "@web/src/providers/auth-provider";
export default function InboxPage() {
const { user } = useAuth();
return (
// 添加 flex flex-col 使其成为弹性布局容器
<div className="min-h-screen shadow-elegant border-2 border-white rounded-xl overflow-hidden bg-gradient-to-b from-slate-100 to-slate-50 flex flex-col">
<Header />
{/* 添加 flex-grow 使内容区域自动填充剩余空间 */}
<LetterList
params={{
where: {
receivers: {
some: {
id: user?.id,
},
},
},
}}></LetterList>
</div>
);
}

View File

@ -0,0 +1,11 @@
import { useAuth } from "@web/src/providers/auth-provider";
import InboxPage from "../inbox/page";
import LetterListPage from "../list/page";
export default function IndexPage() {
const { user } = useAuth();
if (user) {
return <InboxPage></InboxPage>;
}
return <LetterListPage></LetterListPage>;
}

View File

@ -0,0 +1,74 @@
export function Header() {
return (
<header className="bg-gradient-to-r from-primary to-primary-400 p-6 rounded-t-xl">
<div className="flex flex-col space-y-6">
{/* 主标题区域 */}
<div>
<h1 className="text-3xl font-bold tracking-wider text-white">
</h1>
<p className="mt-2 text-blue-100 text-lg">
</p>
</div>
{/* 服务特点说明 */}
<div className="flex flex-wrap gap-6 text-sm text-white">
<div className="flex items-center gap-2">
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"
/>
</svg>
<span></span>
</div>
<div className="flex items-center gap-2">
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<span></span>
</div>
<div className="flex items-center gap-2">
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z"
/>
</svg>
<span></span>
</div>
</div>
{/* 服务宗旨说明 */}
<div className="text-sm text-blue-100 border-t border-blue-400/30 pt-4">
<p className="leading-relaxed">
</p>
</div>
</div>
</header>
);
}

View File

@ -0,0 +1,20 @@
import LetterList from "@web/src/components/models/post/list/LetterList";
import { Header } from "./Header";
import { useAuth } from "@web/src/providers/auth-provider";
export default function OutboxPage() {
const { user } = useAuth();
return (
// 添加 flex flex-col 使其成为弹性布局容器
<div className="min-h-screen shadow-elegant border-2 border-white rounded-xl overflow-hidden bg-gradient-to-b from-slate-100 to-slate-50 flex flex-col">
<Header />
{/* 添加 flex-grow 使内容区域自动填充剩余空间 */}
<LetterList
params={{
where: {
authorId: user?.id,
},
}}></LetterList>
</div>
);
}

View File

@ -22,6 +22,7 @@ export default function WriteLetterPage() {
const { data, isLoading, error } = api.staff.findManyWithPagination.useQuery({ const { data, isLoading, error } = api.staff.findManyWithPagination.useQuery({
page: currentPage, page: currentPage,
pageSize, pageSize,
where: { where: {
deptId: selectedDept, deptId: selectedDept,
OR: [{ OR: [{

View File

@ -7,7 +7,7 @@ import {
UserOutlined, UserOutlined,
SettingOutlined, SettingOutlined,
QuestionCircleOutlined, QuestionCircleOutlined,
LogoutOutlined LogoutOutlined,
} from "@ant-design/icons"; } from "@ant-design/icons";
import { Spin } from "antd"; import { Spin } from "antd";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
@ -21,54 +21,57 @@ const menuVariants = {
transition: { transition: {
type: "spring", type: "spring",
stiffness: 300, stiffness: 300,
damping: 30 damping: 30,
} },
}, },
exit: { exit: {
opacity: 0, opacity: 0,
scale: 0.95, scale: 0.95,
y: -10, y: -10,
transition: { transition: {
duration: 0.2 duration: 0.2,
} },
} },
}; };
export function UserMenu() { export function UserMenu() {
const [showMenu, setShowMenu] = useState(false); const [showMenu, setShowMenu] = useState(false);
const menuRef = useRef<HTMLDivElement>(null); const menuRef = useRef<HTMLDivElement>(null);
const { user, logout, isLoading } = useAuth(); const { user, logout, isLoading } = useAuth();
const navigate = useNavigate() const navigate = useNavigate();
useClickOutside(menuRef, () => setShowMenu(false)); useClickOutside(menuRef, () => setShowMenu(false));
const toggleMenu = useCallback(() => { const toggleMenu = useCallback(() => {
setShowMenu(prev => !prev); setShowMenu((prev) => !prev);
}, []); }, []);
const menuItems: MenuItemType[] = useMemo(() => [ const menuItems: MenuItemType[] = useMemo(
() => [
{ {
icon: <UserOutlined className="text-lg" />, icon: <UserOutlined className="text-lg" />,
label: '个人信息', label: "个人信息",
action: () => {}, action: () => {},
}, },
{ {
icon: <SettingOutlined className="text-lg" />, icon: <SettingOutlined className="text-lg" />,
label: '设置', label: "设置",
action: () => { action: () => {
navigate('/admin/staff') navigate("/admin/staff");
}, },
}, },
{ // {
icon: <QuestionCircleOutlined className="text-lg" />, // icon: <QuestionCircleOutlined className="text-lg" />,
label: '帮助', // label: '帮助',
action: () => { }, // action: () => { },
}, // },
{ {
icon: <LogoutOutlined className="text-lg" />, icon: <LogoutOutlined className="text-lg" />,
label: '注销', label: "注销",
action: () => logout(), action: () => logout(),
}, },
], [logout]); ],
[logout]
);
const handleMenuItemClick = useCallback((action: () => void) => { const handleMenuItemClick = useCallback((action: () => void) => {
action(); action();
@ -95,8 +98,7 @@ export function UserMenu() {
onClick={toggleMenu} onClick={toggleMenu}
className="relative rounded-full focus:outline-none className="relative rounded-full focus:outline-none
focus:ring-2 focus:ring-[#00538E]/80 focus:ring-offset-2 focus:ring-2 focus:ring-[#00538E]/80 focus:ring-offset-2
focus:ring-offset-white transition-all duration-200 ease-in-out" focus:ring-offset-white transition-all duration-200 ease-in-out">
>
<Avatar <Avatar
src={user?.avatar} src={user?.avatar}
name={user?.showname || user?.username} name={user?.showname || user?.username}
@ -128,14 +130,11 @@ export function UserMenu() {
style={{ zIndex: 100 }} style={{ zIndex: 100 }}
className="absolute right-0 mt-3 w-64 origin-top-right className="absolute right-0 mt-3 w-64 origin-top-right
bg-white rounded-xl overflow-hidden shadow-lg bg-white rounded-xl overflow-hidden shadow-lg
border border-[#E5EDF5]" border border-[#E5EDF5]">
>
{/* User Profile Section */} {/* User Profile Section */}
<div <div
className="px-4 py-4 bg-gradient-to-b from-[#F6F9FC] to-white className="px-4 py-4 bg-gradient-to-b from-[#F6F9FC] to-white
border-b border-[#E5EDF5] " border-b border-[#E5EDF5] ">
>
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
<Avatar <Avatar
src={user?.avatar} src={user?.avatar}
@ -172,17 +171,20 @@ export function UserMenu() {
focus:ring-2 focus:ring-[#00538E]/20 focus:ring-2 focus:ring-[#00538E]/20
group relative overflow-hidden group relative overflow-hidden
active:scale-[0.99] active:scale-[0.99]
${item.label === '注销' ${
? 'text-[#B22234] hover:bg-red-50/80 hover:text-red-700' item.label === "注销"
: 'text-[#00538E] hover:bg-[#E6EEF5] hover:text-[#003F6A]' ? "text-[#B22234] hover:bg-red-50/80 hover:text-red-700"
}`} : "text-[#00538E] hover:bg-[#E6EEF5] hover:text-[#003F6A]"
> }`}>
<span className={`w-5 h-5 flex items-center justify-center <span
className={`w-5 h-5 flex items-center justify-center
transition-all duration-200 ease-in-out transition-all duration-200 ease-in-out
group-hover:scale-110 group-hover:rotate-6 group-hover:scale-110 group-hover:rotate-6
group-hover:translate-x-0.5 ${item.label === '注销' group-hover:translate-x-0.5 ${
? 'group-hover:text-red-600' item.label === "注销"
: 'group-hover:text-[#003F6A]'}`}> ? "group-hover:text-red-600"
: "group-hover:text-[#003F6A]"
}`}>
{item.icon} {item.icon}
</span> </span>
<span>{item.label}</span> <span>{item.label}</span>

View File

@ -1,6 +1,7 @@
import { NavLink, useLocation } from "react-router-dom"; import { NavLink, useLocation } from "react-router-dom";
import { useNavItem } from "./useNavItem"; import { useNavItem } from "./useNavItem";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import React from "react";
interface NavItem { interface NavItem {
to: string; to: string;
@ -17,13 +18,16 @@ export default function Navigation({ className }: NavigationProps) {
const location = useLocation(); const location = useLocation();
const isActive = (to: string) => { const isActive = (to: string) => {
const [pathname, search] = to.split('?'); const [pathname, search] = to.split("?");
return location.pathname === pathname && return (
(!search ? !location.search : location.search === `?${search}`); location.pathname === pathname &&
(!search ? !location.search : location.search === `?${search}`)
);
}; };
return ( return (
<nav className={twMerge( <nav
className={twMerge(
"mt-4 rounded-xl bg-gradient-to-r from-primary-500 to-primary-600 shadow-lg", "mt-4 rounded-xl bg-gradient-to-r from-primary-500 to-primary-600 shadow-lg",
className className
)}> )}>
@ -35,34 +39,41 @@ export default function Navigation({ className }: NavigationProps) {
<NavLink <NavLink
key={item.to} key={item.to}
to={item.to} to={item.to}
className={({ isActive: active }) => twMerge( className={({ isActive: active }) =>
twMerge(
"relative px-4 py-2.5 text-sm font-medium", "relative px-4 py-2.5 text-sm font-medium",
"text-gray-300 hover:text-white", "text-gray-300 hover:text-white",
"transition-all duration-200 ease-out group", "transition-all duration-200 ease-out group",
active && "text-white" active && "text-white"
)} )
> }>
<span className="relative z-10 flex items-center gap-2 transition-transform group-hover:translate-y-[-1px]"> <span className="relative z-10 flex items-center gap-2 transition-transform group-hover:translate-y-[-1px]">
{item.icon} {item.icon}
<span className="tracking-wide">{item.label}</span> <span className="tracking-wide">
{item.label}
</span>
</span> </span>
{/* Active Indicator */} {/* Active Indicator */}
<span className={twMerge( <span
className={twMerge(
"absolute bottom-0 left-1/2 h-[2px] bg-blue-400", "absolute bottom-0 left-1/2 h-[2px] bg-blue-400",
"transition-all duration-300 ease-out", "transition-all duration-300 ease-out",
"transform -translate-x-1/2", "transform -translate-x-1/2",
isActive(item.to) isActive(item.to)
? "w-full opacity-100" ? "w-full opacity-100"
: "w-0 opacity-0 group-hover:w-1/2 group-hover:opacity-40" : "w-0 opacity-0 group-hover:w-1/2 group-hover:opacity-40"
)} /> )}
/>
{/* Hover Glow Effect */} {/* Hover Glow Effect */}
<span className={twMerge( <span
className={twMerge(
"absolute inset-0 rounded-lg bg-blue-400/0", "absolute inset-0 rounded-lg bg-blue-400/0",
"transition-all duration-300", "transition-all duration-300",
"group-hover:bg-blue-400/5" "group-hover:bg-blue-400/5"
)} /> )}
/>
</NavLink> </NavLink>
))} ))}
</div> </div>
@ -75,13 +86,14 @@ export default function Navigation({ className }: NavigationProps) {
<NavLink <NavLink
key={item.to} key={item.to}
to={item.to} to={item.to}
className={({ isActive: active }) => twMerge( className={({ isActive: active }) =>
twMerge(
"px-3 py-1.5 text-sm font-medium rounded-full", "px-3 py-1.5 text-sm font-medium rounded-full",
"transition-colors duration-200", "transition-colors duration-200",
"text-gray-300 hover:text-white", "text-gray-300 hover:text-white",
active && "bg-blue-500/20 text-white" active && "bg-blue-500/20 text-white"
)} )
> }>
<span className="flex items-center gap-1.5"> <span className="flex items-center gap-1.5">
{item.icon} {item.icon}
<span>{item.label}</span> <span>{item.label}</span>

View File

@ -1,13 +1,15 @@
import { api } from "@nice/client"; import { api } from "@nice/client";
import { TaxonomySlug } from "@nice/common"; import { TaxonomySlug } from "@nice/common";
import { useMemo } from "react"; import React, { useMemo } from "react";
import { MailOutlined, SendOutlined } from "@ant-design/icons";
import { import {
FileTextOutlined, FileTextOutlined,
ScheduleOutlined, ScheduleOutlined,
QuestionCircleOutlined, QuestionCircleOutlined,
FolderOutlined, FolderOutlined,
TagsOutlined TagsOutlined,
} from "@ant-design/icons"; } from "@ant-design/icons";
import { useAuth } from "@web/src/providers/auth-provider";
export interface NavItem { export interface NavItem {
to: string; to: string;
@ -16,50 +18,69 @@ export interface NavItem {
} }
export function useNavItem() { export function useNavItem() {
const { user } = useAuth();
const { data } = api.term.findMany.useQuery({ const { data } = api.term.findMany.useQuery({
where: { where: {
taxonomy: { slug: TaxonomySlug.CATEGORY } taxonomy: { slug: TaxonomySlug.CATEGORY },
} },
}); });
const navItems = useMemo(() => { const navItems = useMemo(() => {
// 定义固定的导航项 // 定义固定的导航项
const staticItems = { const staticItems = {
inbox: {
to: user ? "/" : "/inbox",
label: "我收到的",
icon: <MailOutlined className="text-base" />,
},
outbox: {
to: "/outbox",
label: "我发出的",
icon: <SendOutlined className="text-base" />,
},
letterList: { letterList: {
to: "/", to: !user ? "/" : "/letter-list",
label: "公开信件", label: "公开信件",
icon: <FileTextOutlined className="text-base" /> icon: <FileTextOutlined className="text-base" />,
}, },
letterProgress: { letterProgress: {
to: "/letter-progress", to: "/letter-progress",
label: "进度查询", label: "进度查询",
icon: <ScheduleOutlined className="text-base" /> icon: <ScheduleOutlined className="text-base" />,
}, },
help: { // help: {
to: "/help", // to: "/help",
label: "使用帮助", // label: "使用帮助",
icon: <QuestionCircleOutlined className="text-base" /> // icon: <QuestionCircleOutlined className="text-base" />
} // }
}; };
if (!data) { if (!data) {
return [staticItems.letterList, staticItems.letterProgress, staticItems.help]; return [
user && staticItems.inbox,
user && staticItems.outbox,
staticItems.letterList,
staticItems.letterProgress,
// staticItems.help,
].filter(Boolean);
} }
// 构建分类导航项 // 构建分类导航项
const categoryItems = data.map(term => ({ const categoryItems = data.map((term) => ({
to: `/write-letter?termId=${term.id}`, to: `/write-letter?termId=${term.id}`,
label: term.name, label: term.name,
icon: <TagsOutlined className="text-base"></TagsOutlined> icon: <TagsOutlined className="text-base"></TagsOutlined>,
})); }));
// 按照指定顺序返回导航项 // 按照指定顺序返回导航项
return [ return [
user && staticItems.inbox,
user && staticItems.outbox,
staticItems.letterList, staticItems.letterList,
staticItems.letterProgress, staticItems.letterProgress,
...categoryItems, ...categoryItems,
staticItems.help // staticItems.help,
]; ].filter(Boolean);
}, [data]); }, [data]);
return { navItems }; return { navItems };

View File

@ -23,6 +23,9 @@ import LetterDetailPage from "../app/main/letter/detail/page";
import AdminLayout from "../components/layout/admin/AdminLayout"; import AdminLayout from "../components/layout/admin/AdminLayout";
import { CustomRouteObject } from "./types"; import { CustomRouteObject } from "./types";
import { adminRoute } from "./admin-route"; import { adminRoute } from "./admin-route";
import InboxPage from "../app/main/letter/inbox/page";
import OutboxPage from "../app/main/letter/outbox/page";
import IndexPage from "../app/main/letter/index/page";
export const routes: CustomRouteObject[] = [ export const routes: CustomRouteObject[] = [
{ {
path: "/", path: "/",
@ -36,8 +39,20 @@ export const routes: CustomRouteObject[] = [
{ {
element: <MainLayout></MainLayout>, element: <MainLayout></MainLayout>,
children: [ children: [
{
path: "inbox",
element: <InboxPage></InboxPage>,
},
{
path: "outbox",
element: <OutboxPage></OutboxPage>,
},
{ {
index: true, index: true,
element: <IndexPage></IndexPage>,
},
{
path: "letter-list",
element: <LetterListPage></LetterListPage>, element: <LetterListPage></LetterListPage>,
}, },
{ {