add
This commit is contained in:
parent
daae6a9018
commit
a0447eab2f
|
@ -38,7 +38,7 @@ export class BaseService<
|
||||||
protected prisma: PrismaClient,
|
protected prisma: PrismaClient,
|
||||||
protected objectType: string,
|
protected objectType: string,
|
||||||
protected enableOrder: boolean = false,
|
protected enableOrder: boolean = false,
|
||||||
) { }
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the name of the model dynamically.
|
* Retrieves the name of the model dynamically.
|
||||||
|
@ -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');
|
||||||
|
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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>;
|
||||||
|
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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: [{
|
||||||
|
|
|
@ -4,194 +4,196 @@ import { motion, AnimatePresence } from "framer-motion";
|
||||||
import { useState, useRef, useCallback, useMemo } from "react";
|
import { useState, useRef, useCallback, useMemo } from "react";
|
||||||
import { Avatar } from "../../common/element/Avatar";
|
import { Avatar } from "../../common/element/Avatar";
|
||||||
import {
|
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";
|
||||||
import { MenuItemType } from "./types";
|
import { MenuItemType } from "./types";
|
||||||
const menuVariants = {
|
const menuVariants = {
|
||||||
hidden: { opacity: 0, scale: 0.95, y: -10 },
|
hidden: { opacity: 0, scale: 0.95, y: -10 },
|
||||||
visible: {
|
visible: {
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
scale: 1,
|
scale: 1,
|
||||||
y: 0,
|
y: 0,
|
||||||
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" />,
|
{
|
||||||
label: '个人信息',
|
icon: <UserOutlined className="text-lg" />,
|
||||||
action: () => { },
|
label: "个人信息",
|
||||||
},
|
action: () => {},
|
||||||
{
|
},
|
||||||
icon: <SettingOutlined className="text-lg" />,
|
{
|
||||||
label: '设置',
|
icon: <SettingOutlined className="text-lg" />,
|
||||||
action: () => {
|
label: "设置",
|
||||||
navigate('/admin/staff')
|
action: () => {
|
||||||
},
|
navigate("/admin/staff");
|
||||||
},
|
},
|
||||||
{
|
},
|
||||||
icon: <QuestionCircleOutlined className="text-lg" />,
|
// {
|
||||||
label: '帮助',
|
// icon: <QuestionCircleOutlined className="text-lg" />,
|
||||||
action: () => { },
|
// label: '帮助',
|
||||||
},
|
// action: () => { },
|
||||||
{
|
// },
|
||||||
icon: <LogoutOutlined className="text-lg" />,
|
{
|
||||||
label: '注销',
|
icon: <LogoutOutlined className="text-lg" />,
|
||||||
action: () => logout(),
|
label: "注销",
|
||||||
},
|
action: () => logout(),
|
||||||
], [logout]);
|
},
|
||||||
|
],
|
||||||
|
[logout]
|
||||||
|
);
|
||||||
|
|
||||||
const handleMenuItemClick = useCallback((action: () => void) => {
|
const handleMenuItemClick = useCallback((action: () => void) => {
|
||||||
action();
|
action();
|
||||||
setShowMenu(false);
|
setShowMenu(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center w-10 h-10">
|
<div className="flex items-center justify-center w-10 h-10">
|
||||||
<Spin size="small" />
|
<Spin size="small" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={menuRef} className="relative">
|
<div ref={menuRef} className="relative">
|
||||||
<motion.button
|
<motion.button
|
||||||
aria-label="用户菜单"
|
aria-label="用户菜单"
|
||||||
aria-haspopup="true"
|
aria-haspopup="true"
|
||||||
aria-expanded={showMenu}
|
aria-expanded={showMenu}
|
||||||
aria-controls="user-menu"
|
aria-controls="user-menu"
|
||||||
whileHover={{ scale: 1.02 }}
|
whileHover={{ scale: 1.02 }}
|
||||||
whileTap={{ scale: 0.98 }}
|
whileTap={{ scale: 0.98 }}
|
||||||
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}
|
size={40}
|
||||||
size={40}
|
className="ring-2 ring-white hover:ring-[#00538E]/90
|
||||||
className="ring-2 ring-white hover:ring-[#00538E]/90
|
|
||||||
transition-all duration-200 ease-in-out shadow-md
|
transition-all duration-200 ease-in-out shadow-md
|
||||||
hover:shadow-lg"
|
hover:shadow-lg"
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
className="absolute bottom-0 right-0 h-3 w-3
|
className="absolute bottom-0 right-0 h-3 w-3
|
||||||
rounded-full bg-emerald-500 ring-2 ring-white
|
rounded-full bg-emerald-500 ring-2 ring-white
|
||||||
shadow-sm transition-transform duration-200
|
shadow-sm transition-transform duration-200
|
||||||
ease-in-out hover:scale-110"
|
ease-in-out hover:scale-110"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
</motion.button>
|
</motion.button>
|
||||||
|
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{showMenu && (
|
{showMenu && (
|
||||||
<motion.div
|
<motion.div
|
||||||
initial="hidden"
|
initial="hidden"
|
||||||
animate="visible"
|
animate="visible"
|
||||||
exit="exit"
|
exit="exit"
|
||||||
variants={menuVariants}
|
variants={menuVariants}
|
||||||
role="menu"
|
role="menu"
|
||||||
id="user-menu"
|
id="user-menu"
|
||||||
aria-orientation="vertical"
|
aria-orientation="vertical"
|
||||||
aria-labelledby="user-menu-button"
|
aria-labelledby="user-menu-button"
|
||||||
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">
|
||||||
|
<Avatar
|
||||||
|
src={user?.avatar}
|
||||||
|
name={user?.showname || user?.username}
|
||||||
|
size={40}
|
||||||
|
className="ring-2 ring-white shadow-sm"
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col space-y-0.5">
|
||||||
|
<span className="text-sm font-semibold text-[#00538E]">
|
||||||
|
{user?.showname || user?.username}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-[#718096] flex items-center gap-1.5">
|
||||||
|
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse"></span>
|
||||||
|
在线
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
>
|
{/* Menu Items */}
|
||||||
<div className="flex items-center space-x-4">
|
<div className="p-2">
|
||||||
<Avatar
|
{menuItems.map((item, index) => (
|
||||||
src={user?.avatar}
|
<button
|
||||||
name={user?.showname || user?.username}
|
key={index}
|
||||||
size={40}
|
role="menuitem"
|
||||||
className="ring-2 ring-white shadow-sm"
|
tabIndex={showMenu ? 0 : -1}
|
||||||
/>
|
onClick={(e) => {
|
||||||
<div className="flex flex-col space-y-0.5">
|
e.stopPropagation();
|
||||||
<span className="text-sm font-semibold text-[#00538E]">
|
handleMenuItemClick(item.action);
|
||||||
{user?.showname || user?.username}
|
}}
|
||||||
</span>
|
className={`flex items-center gap-3 w-full px-4 py-3
|
||||||
<span className="text-xs text-[#718096] flex items-center gap-1.5">
|
|
||||||
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse"></span>
|
|
||||||
在线
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Menu Items */}
|
|
||||||
<div className="p-2">
|
|
||||||
{menuItems.map((item, index) => (
|
|
||||||
<button
|
|
||||||
key={index}
|
|
||||||
role="menuitem"
|
|
||||||
tabIndex={showMenu ? 0 : -1}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
handleMenuItemClick(item.action);
|
|
||||||
}}
|
|
||||||
className={`flex items-center gap-3 w-full px-4 py-3
|
|
||||||
text-sm font-medium rounded-lg transition-all
|
text-sm font-medium rounded-lg transition-all
|
||||||
focus:outline-none
|
focus:outline-none
|
||||||
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"
|
||||||
{item.icon}
|
: "group-hover:text-[#003F6A]"
|
||||||
</span>
|
}`}>
|
||||||
<span>{item.label}</span>
|
{item.icon}
|
||||||
</button>
|
</span>
|
||||||
))}
|
<span>{item.label}</span>
|
||||||
</div>
|
</button>
|
||||||
</motion.div>
|
))}
|
||||||
)}
|
</div>
|
||||||
</AnimatePresence>
|
</motion.div>
|
||||||
</div>
|
)}
|
||||||
);
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,96 +1,108 @@
|
||||||
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;
|
||||||
label: string;
|
label: string;
|
||||||
icon?: React.ReactNode;
|
icon?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface NavigationProps {
|
interface NavigationProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Navigation({ className }: NavigationProps) {
|
export default function Navigation({ className }: NavigationProps) {
|
||||||
const { navItems } = useNavItem();
|
const { navItems } = useNavItem();
|
||||||
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
|
||||||
"mt-4 rounded-xl bg-gradient-to-r from-primary-500 to-primary-600 shadow-lg",
|
className={twMerge(
|
||||||
className
|
"mt-4 rounded-xl bg-gradient-to-r from-primary-500 to-primary-600 shadow-lg",
|
||||||
)}>
|
className
|
||||||
<div className="flex flex-col md:flex-row items-stretch">
|
)}>
|
||||||
{/* Desktop Navigation */}
|
<div className="flex flex-col md:flex-row items-stretch">
|
||||||
<div className="hidden md:flex items-center px-6 py-2 w-full overflow-x-auto">
|
{/* Desktop Navigation */}
|
||||||
<div className="flex space-x-6 min-w-max">
|
<div className="hidden md:flex items-center px-6 py-2 w-full overflow-x-auto">
|
||||||
{navItems.map((item) => (
|
<div className="flex space-x-6 min-w-max">
|
||||||
<NavLink
|
{navItems.map((item) => (
|
||||||
key={item.to}
|
<NavLink
|
||||||
to={item.to}
|
key={item.to}
|
||||||
className={({ isActive: active }) => twMerge(
|
to={item.to}
|
||||||
"relative px-4 py-2.5 text-sm font-medium",
|
className={({ isActive: active }) =>
|
||||||
"text-gray-300 hover:text-white",
|
twMerge(
|
||||||
"transition-all duration-200 ease-out group",
|
"relative px-4 py-2.5 text-sm font-medium",
|
||||||
active && "text-white"
|
"text-gray-300 hover:text-white",
|
||||||
)}
|
"transition-all duration-200 ease-out group",
|
||||||
>
|
active && "text-white"
|
||||||
<span className="relative z-10 flex items-center gap-2 transition-transform group-hover:translate-y-[-1px]">
|
)
|
||||||
{item.icon}
|
}>
|
||||||
<span className="tracking-wide">{item.label}</span>
|
<span className="relative z-10 flex items-center gap-2 transition-transform group-hover:translate-y-[-1px]">
|
||||||
</span>
|
{item.icon}
|
||||||
|
<span className="tracking-wide">
|
||||||
|
{item.label}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
{/* Active Indicator */}
|
{/* Active Indicator */}
|
||||||
<span className={twMerge(
|
<span
|
||||||
"absolute bottom-0 left-1/2 h-[2px] bg-blue-400",
|
className={twMerge(
|
||||||
"transition-all duration-300 ease-out",
|
"absolute bottom-0 left-1/2 h-[2px] bg-blue-400",
|
||||||
"transform -translate-x-1/2",
|
"transition-all duration-300 ease-out",
|
||||||
isActive(item.to)
|
"transform -translate-x-1/2",
|
||||||
? "w-full opacity-100"
|
isActive(item.to)
|
||||||
: "w-0 opacity-0 group-hover:w-1/2 group-hover:opacity-40"
|
? "w-full opacity-100"
|
||||||
)} />
|
: "w-0 opacity-0 group-hover:w-1/2 group-hover:opacity-40"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Hover Glow Effect */}
|
{/* Hover Glow Effect */}
|
||||||
<span className={twMerge(
|
<span
|
||||||
"absolute inset-0 rounded-lg bg-blue-400/0",
|
className={twMerge(
|
||||||
"transition-all duration-300",
|
"absolute inset-0 rounded-lg bg-blue-400/0",
|
||||||
"group-hover:bg-blue-400/5"
|
"transition-all duration-300",
|
||||||
)} />
|
"group-hover:bg-blue-400/5"
|
||||||
</NavLink>
|
)}
|
||||||
))}
|
/>
|
||||||
</div>
|
</NavLink>
|
||||||
</div>
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Mobile Navigation */}
|
{/* Mobile Navigation */}
|
||||||
<div className="md:hidden flex overflow-x-auto scrollbar-none px-4 py-2">
|
<div className="md:hidden flex overflow-x-auto scrollbar-none px-4 py-2">
|
||||||
<div className="flex space-x-4 min-w-max">
|
<div className="flex space-x-4 min-w-max">
|
||||||
{navItems.map((item) => (
|
{navItems.map((item) => (
|
||||||
<NavLink
|
<NavLink
|
||||||
key={item.to}
|
key={item.to}
|
||||||
to={item.to}
|
to={item.to}
|
||||||
className={({ isActive: active }) => twMerge(
|
className={({ isActive: active }) =>
|
||||||
"px-3 py-1.5 text-sm font-medium rounded-full",
|
twMerge(
|
||||||
"transition-colors duration-200",
|
"px-3 py-1.5 text-sm font-medium rounded-full",
|
||||||
"text-gray-300 hover:text-white",
|
"transition-colors duration-200",
|
||||||
active && "bg-blue-500/20 text-white"
|
"text-gray-300 hover:text-white",
|
||||||
)}
|
active && "bg-blue-500/20 text-white"
|
||||||
>
|
)
|
||||||
<span className="flex items-center gap-1.5">
|
}>
|
||||||
{item.icon}
|
<span className="flex items-center gap-1.5">
|
||||||
<span>{item.label}</span>
|
{item.icon}
|
||||||
</span>
|
<span>{item.label}</span>
|
||||||
</NavLink>
|
</span>
|
||||||
))}
|
</NavLink>
|
||||||
</div>
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</div>
|
||||||
);
|
</nav>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,66 +1,87 @@
|
||||||
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;
|
||||||
label: string;
|
label: string;
|
||||||
icon?: React.ReactNode;
|
icon?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useNavItem() {
|
export function useNavItem() {
|
||||||
const { data } = api.term.findMany.useQuery({
|
const { user } = useAuth();
|
||||||
where: {
|
const { data } = api.term.findMany.useQuery({
|
||||||
taxonomy: { slug: TaxonomySlug.CATEGORY }
|
where: {
|
||||||
}
|
taxonomy: { slug: TaxonomySlug.CATEGORY },
|
||||||
});
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const navItems = useMemo(() => {
|
const navItems = useMemo(() => {
|
||||||
// 定义固定的导航项
|
// 定义固定的导航项
|
||||||
const staticItems = {
|
const staticItems = {
|
||||||
letterList: {
|
inbox: {
|
||||||
to: "/",
|
to: user ? "/" : "/inbox",
|
||||||
label: "公开信件",
|
label: "我收到的",
|
||||||
icon: <FileTextOutlined className="text-base" />
|
icon: <MailOutlined className="text-base" />,
|
||||||
},
|
},
|
||||||
letterProgress: {
|
outbox: {
|
||||||
to: "/letter-progress",
|
to: "/outbox",
|
||||||
label: "进度查询",
|
label: "我发出的",
|
||||||
icon: <ScheduleOutlined className="text-base" />
|
icon: <SendOutlined className="text-base" />,
|
||||||
},
|
},
|
||||||
help: {
|
letterList: {
|
||||||
to: "/help",
|
to: !user ? "/" : "/letter-list",
|
||||||
label: "使用帮助",
|
label: "公开信件",
|
||||||
icon: <QuestionCircleOutlined className="text-base" />
|
icon: <FileTextOutlined className="text-base" />,
|
||||||
}
|
},
|
||||||
};
|
letterProgress: {
|
||||||
|
to: "/letter-progress",
|
||||||
|
label: "进度查询",
|
||||||
|
icon: <ScheduleOutlined className="text-base" />,
|
||||||
|
},
|
||||||
|
// help: {
|
||||||
|
// to: "/help",
|
||||||
|
// label: "使用帮助",
|
||||||
|
// 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 [
|
||||||
staticItems.letterList,
|
user && staticItems.inbox,
|
||||||
staticItems.letterProgress,
|
user && staticItems.outbox,
|
||||||
...categoryItems,
|
staticItems.letterList,
|
||||||
staticItems.help
|
staticItems.letterProgress,
|
||||||
];
|
...categoryItems,
|
||||||
}, [data]);
|
// staticItems.help,
|
||||||
|
].filter(Boolean);
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
return { navItems };
|
return { navItems };
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
Loading…
Reference in New Issue