Merge branch 'main' of http://113.45.157.195:3003/insiinc/leader-mail
This commit is contained in:
commit
fe5750199a
|
@ -18,7 +18,7 @@ export class StaffRouter {
|
|||
private readonly trpc: TrpcService,
|
||||
private readonly staffService: StaffService,
|
||||
private readonly staffRowService: StaffRowService,
|
||||
) {}
|
||||
) { }
|
||||
|
||||
router = this.trpc.router({
|
||||
create: this.trpc.procedure
|
||||
|
@ -77,5 +77,15 @@ export class StaffRouter {
|
|||
.mutation(async ({ input }) => {
|
||||
return this.staffService.updateOrder(input);
|
||||
}),
|
||||
findManyWithPagination: this.trpc.procedure
|
||||
.input(z.object({
|
||||
page: z.number(),
|
||||
pageSize: z.number().optional(),
|
||||
where: StaffWhereInputSchema.optional(),
|
||||
select: StaffSelectSchema.optional()
|
||||
})) // Assuming StaffMethodSchema.findMany is the Zod schema for finding staffs by keyword
|
||||
.query(async ({ input }) => {
|
||||
return await this.staffService.findManyWithPagination(input);
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
|
|
@ -33,8 +33,7 @@
|
|||
--ag-spacing: 6px;
|
||||
--ag-odd-row-background-color: var(--color-fill-quaternary);
|
||||
--ag-wrapper-border-width: 0px;
|
||||
/* --ag-wrapper-border-color: var(--color-border-secondary); */
|
||||
/* --ag-wrapper-border-radius: 10px; */
|
||||
|
||||
}
|
||||
|
||||
.ag-root-wrapper {
|
||||
|
|
|
@ -10,7 +10,7 @@ import dayjs from "dayjs";
|
|||
import "dayjs/locale/zh-cn";
|
||||
import { AuthProvider } from './providers/auth-provider';
|
||||
import { Toaster } from 'react-hot-toast';
|
||||
import {ThemeProvider} from "@nice/theme"
|
||||
import ThemeProvider from './providers/theme-provider';
|
||||
dayjs.locale("zh-cn");
|
||||
function App() {
|
||||
|
||||
|
@ -23,7 +23,7 @@ function App() {
|
|||
theme={{
|
||||
algorithm: theme.defaultAlgorithm,
|
||||
token: {
|
||||
colorPrimary: "#2e75b6",
|
||||
colorPrimary: "#00308a",
|
||||
},
|
||||
components: {},
|
||||
}}>
|
||||
|
|
|
@ -13,11 +13,10 @@ import {
|
|||
} from "antd";
|
||||
import { useAppConfig } from "@nice/client";
|
||||
import { useAuth } from "@web/src/providers/auth-provider";
|
||||
|
||||
import FixedHeader from "@web/src/components/layout/fix-header";
|
||||
import { useForm } from "antd/es/form/Form";
|
||||
import { api } from "@nice/client"
|
||||
import { MainLayoutContext } from "../layout";
|
||||
import AdminHeader from "@web/src/components/layout/admin/AdminHeader";
|
||||
|
||||
|
||||
export default function BaseSettingPage() {
|
||||
const { update, baseSetting } = useAppConfig();
|
||||
|
@ -31,7 +30,6 @@ export default function BaseSettingPage() {
|
|||
const [isFormChanged, setIsFormChanged] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { user, hasSomePermissions } = useAuth();
|
||||
const { pageWidth } = useContext?.(MainLayoutContext);
|
||||
function handleFieldsChange() {
|
||||
setIsFormChanged(true);
|
||||
}
|
||||
|
@ -77,8 +75,8 @@ export default function BaseSettingPage() {
|
|||
}
|
||||
}, [baseSetting, form]);
|
||||
return (
|
||||
<div style={{ width: pageWidth }}>
|
||||
<FixedHeader>
|
||||
<div >
|
||||
<AdminHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
{isFormChanged &&
|
||||
hasSomePermissions(RolePerms.MANAGE_BASE_SETTING) && (
|
||||
|
@ -93,7 +91,7 @@ export default function BaseSettingPage() {
|
|||
</>
|
||||
)}
|
||||
</div>
|
||||
</FixedHeader>
|
||||
</AdminHeader>
|
||||
<div
|
||||
className="flex flex-col overflow-auto "
|
||||
style={{ height: "calc(100vh - 48px - 49px)" }}>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import DeptEditor from "@web/src/components/models/department/dept-editor";
|
||||
|
||||
export default function DepartmentAdminPage() {
|
||||
return <div className=" flex-grow bg-white rounded-xl">
|
||||
return <div className=" flex-grow bg-white">
|
||||
<DeptEditor></DeptEditor>
|
||||
</div>
|
||||
}
|
|
@ -1,115 +0,0 @@
|
|||
import React, {
|
||||
createContext,
|
||||
CSSProperties,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import { Outlet, useLocation } from "react-router-dom";
|
||||
import "react-resizable/css/styles.css";
|
||||
import { theme } from "antd";
|
||||
import ResizableSidebar from "@web/src/components/layout/resizable-sidebar";
|
||||
import SidebarContent from "@web/src/components/layout/sidebar-content";
|
||||
import UserHeader from "@web/src/components/layout/user-header";
|
||||
import { Icon } from "@nice/iconer";
|
||||
import { env } from "@web/src/env";
|
||||
import RoundedClip from "@web/src/components/svg/rounded-clip";
|
||||
import {useTerm} from "@nice/client"
|
||||
|
||||
export const MainLayoutContext = createContext<{
|
||||
pageWidth?: number;
|
||||
}>({
|
||||
pageWidth: undefined,
|
||||
});
|
||||
const ParallelogramTag = () => {
|
||||
const { token } = theme.useToken();
|
||||
const parallelogramStyle: CSSProperties = {
|
||||
display: "inline-flex",
|
||||
alignItems: "center", // 垂直居中
|
||||
transform: "skew(-20deg)",
|
||||
height: "25px", // 调整高度以适应文本
|
||||
padding: "0 20px",
|
||||
backgroundColor: token.colorPrimaryBg,
|
||||
// margin: '0 0 0 10px',
|
||||
};
|
||||
|
||||
const contentStyle: CSSProperties = {
|
||||
transform: "skew(20deg)",
|
||||
fontSize: token.fontSize,
|
||||
fontWeight: "bold",
|
||||
color: token.colorPrimary,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="shadow" style={parallelogramStyle}>
|
||||
<span style={contentStyle}>{env.VERSION}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const MainLayoutPage: React.FC = () => {
|
||||
const { token } = theme.useToken();
|
||||
const [sidebarWidth, setSidebarWidth] = useState<number>();
|
||||
const [pageWidth, setPageWidth] = useState<number>();
|
||||
useTerm();
|
||||
const updateWidth = () => {
|
||||
const remainingWidth =
|
||||
window.innerWidth - Math.max(sidebarWidth || 0, 200);
|
||||
setPageWidth(remainingWidth);
|
||||
};
|
||||
useEffect(() => {
|
||||
window.addEventListener("resize", updateWidth);
|
||||
return () => window.removeEventListener("resize", updateWidth);
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
updateWidth();
|
||||
}, [sidebarWidth]);
|
||||
useEffect(() => {
|
||||
document.title = `${env.APP_NAME}`;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<MainLayoutContext.Provider value={{ pageWidth }}>
|
||||
<div>
|
||||
<div className=" absolute top-1 left-5 text-white flex items-center gap-4 ">
|
||||
<Icon
|
||||
size={36}
|
||||
className=" text-blue-200"
|
||||
name="loop"></Icon>
|
||||
<div
|
||||
className=" flex justify-center items-center font-extrabold "
|
||||
style={{
|
||||
fontSize: token.fontSizeHeading4,
|
||||
lineHeight: token.lineHeightHeading4,
|
||||
}}>
|
||||
{env.APP_NAME || "loop sys"}
|
||||
</div>
|
||||
<ParallelogramTag></ParallelogramTag>
|
||||
</div>
|
||||
<div
|
||||
className=" bg-primary"
|
||||
style={{
|
||||
display: "flex",
|
||||
height: "calc(100vh)",
|
||||
}}>
|
||||
<ResizableSidebar
|
||||
onWidthChange={setSidebarWidth}
|
||||
className="py-2 px-4 ">
|
||||
<SidebarContent></SidebarContent>
|
||||
</ResizableSidebar>
|
||||
<div
|
||||
className=" flex-grow"
|
||||
style={{ backgroundColor: token.colorBgContainer }}>
|
||||
<UserHeader></UserHeader>
|
||||
<div
|
||||
className="relative"
|
||||
style={{ height: "calc(100vh - 48px)" }}>
|
||||
<RoundedClip></RoundedClip>
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</MainLayoutContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default MainLayoutPage;
|
|
@ -1,11 +1,11 @@
|
|||
import FixedHeader from "@web/src/components/layout/fix-header";
|
||||
import AdminHeader from "@web/src/components/layout/admin/AdminHeader";
|
||||
import RoleEditor from "@web/src/components/models/role/role-editor/role-editor";
|
||||
|
||||
export default function RoleAdminPage() {
|
||||
return (
|
||||
<>
|
||||
<FixedHeader roomId="role-editor">
|
||||
</FixedHeader>
|
||||
<AdminHeader roomId="role-editor">
|
||||
</AdminHeader>
|
||||
<RoleEditor></RoleEditor>
|
||||
</>
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import StaffEditor from "@web/src/components/models/staff/staff-editor"
|
||||
export default function StaffPage() {
|
||||
return (
|
||||
<div className=" bg-white rounded-xl flex-grow">
|
||||
<div className=" bg-white flex-grow">
|
||||
<StaffEditor></StaffEditor>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
|
||||
import FixedHeader from "@web/src/components/layout/fix-header";
|
||||
import AdminHeader from "@web/src/components/layout/admin/AdminHeader";
|
||||
import TermEditor from "@web/src/components/models/term/term-editor";
|
||||
|
||||
export default function TermAdminPage() {
|
||||
return (<>
|
||||
<FixedHeader></FixedHeader>
|
||||
<AdminHeader></AdminHeader>
|
||||
<TermEditor></TermEditor>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -47,7 +47,7 @@ const AuthPage: React.FC = () => {
|
|||
>
|
||||
<div className="flex flex-col md:flex-row min-h-[650px]">
|
||||
{/* Left Panel - Welcome Section */}
|
||||
<div className="w-full md:w-1/2 p-12 bg-gradient-to-br from-primary-500 to-primary-100 text-white flex flex-col justify-center relative overflow-hidden">
|
||||
<div className="w-full md:w-1/2 p-12 bg-gradient-to-br from-primary to-primary-400 text-white flex flex-col justify-center relative overflow-hidden">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
|
|
|
@ -69,16 +69,16 @@ export const RegisterForm = ({ onSubmit, isLoading }: RegisterFormProps) => {
|
|||
|
||||
<Form.Item
|
||||
name="officerId"
|
||||
label="工号"
|
||||
label="证件号"
|
||||
rules={[
|
||||
{ required: true, message: "请输入工号" },
|
||||
{ required: true, message: "请输入证件号" },
|
||||
{
|
||||
pattern: /^\d{5,12}$/,
|
||||
message: "请输入有效的工号(5-12位数字)"
|
||||
message: "请输入有效的证件号(5-12位数字)"
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Input placeholder="工号" />
|
||||
<Input placeholder="证件号" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
|
|
|
@ -12,7 +12,7 @@ export function Header() {
|
|||
};
|
||||
|
||||
return (
|
||||
<header className="bg-gradient-to-r from-primary to-primary-50 p-6">
|
||||
<header className="bg-gradient-to-r from-primary to-primary-400 p-6">
|
||||
<h1 className="text-3xl font-bold text-white">公开信件列表</h1>
|
||||
<div className="mt-4 text-blue-50">
|
||||
<p className="text-base opacity-90">
|
||||
|
|
|
@ -37,7 +37,7 @@ export function LetterCard({ letter }: LetterCardProps) {
|
|||
|
||||
return (
|
||||
<div
|
||||
className="w-full p-4 bg-white transition-all duration-300 ease-in-out hover:shadow-lg group"
|
||||
className="w-full p-4 bg-white transition-all duration-300 ease-in-out group"
|
||||
>
|
||||
<div className="flex flex-col gap-3">
|
||||
{/* Title & Priority */}
|
||||
|
@ -46,10 +46,10 @@ export function LetterCard({ letter }: LetterCardProps) {
|
|||
<a
|
||||
href={`/letters/${letter.id}`}
|
||||
target="_blank"
|
||||
className="text-navy-900 transition-all duration-300 relative
|
||||
before:absolute before:bottom-0 before:left-0 before:w-0 before:h-[2px] before:bg-blue-600
|
||||
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-blue-600 group-hover:scale-105 group-hover:drop-shadow-md"
|
||||
group-hover:text-primary-600 group-hover:scale-105 group-hover:drop-shadow-md"
|
||||
>
|
||||
{letter.title}
|
||||
</a>
|
||||
|
@ -60,20 +60,20 @@ export function LetterCard({ letter }: LetterCardProps) {
|
|||
</div>
|
||||
|
||||
{/* Meta Info */}
|
||||
<div className="flex justify-between items-center text-sm text-gray-600">
|
||||
<div className="flex justify-between items-center text-sm text-secondary">
|
||||
<Space size="middle">
|
||||
<Space>
|
||||
<UserOutlined className="text-gray-400" />
|
||||
<UserOutlined className="text-secondary-400" />
|
||||
<Text strong>{letter.sender}</Text>
|
||||
</Space>
|
||||
<Text type="secondary">|</Text>
|
||||
<Space>
|
||||
<BankOutlined className="text-gray-400" />
|
||||
<BankOutlined className="text-secondary-400" />
|
||||
<Text>{letter.unit}</Text>
|
||||
</Space>
|
||||
</Space>
|
||||
<Space>
|
||||
<CalendarOutlined className="text-gray-400" />
|
||||
<CalendarOutlined className="text-secondary-400" />
|
||||
<Text type="secondary">{letter.date}</Text>
|
||||
</Space>
|
||||
</div>
|
||||
|
|
|
@ -41,7 +41,7 @@ export default function LeaderCard({ leader, isSelected, onSelect }: LeaderCardP
|
|||
<h3 className="text-xl font-semibold text-gray-900">
|
||||
{leader.name}
|
||||
</h3>
|
||||
<span className="px-3 py-1 text-sm font-medium bg-blue-50 text-[#00308F] rounded-full">
|
||||
<span className="px-3 py-1 text-sm font-medium bg-blue-50 text-primary rounded-full">
|
||||
{leader.rank}
|
||||
</span>
|
||||
</div>
|
||||
|
@ -76,8 +76,8 @@ export default function LeaderCard({ leader, isSelected, onSelect }: LeaderCardP
|
|||
<button
|
||||
onClick={onSelect}
|
||||
className="mt-auto w-full sm:w-auto flex items-center justify-center gap-2
|
||||
bg-[#00308F] text-white py-3 px-6 rounded-lg
|
||||
hover:bg-[#002070] transition-all duration-300
|
||||
bg-primary text-white py-3 px-6 rounded-lg
|
||||
hover:bg-primary-600 transition-all duration-300
|
||||
focus:outline-none focus:ring-2 focus:ring-[#00308F] focus:ring-opacity-50
|
||||
transform hover:-translate-y-0.5"
|
||||
>
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import { FunnelIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline";
|
||||
import { leaders } from "./mock";
|
||||
import { useMemo, useState } from "react";
|
||||
import { Leader } from "./types";
|
||||
import { Input, Select } from "antd";
|
||||
import { motion } from "framer-motion";
|
||||
import DepartmentSelect from "@web/src/components/models/department/department-select";
|
||||
|
||||
const { Search } = Input;
|
||||
|
||||
|
@ -16,9 +15,6 @@ export default function Filter({ onSearch, onDivisionChange }: FilterProps) {
|
|||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [selectedDivision, setSelectedDivision] = useState<string>('all');
|
||||
|
||||
const divisions = useMemo(() => {
|
||||
return ['all', ...new Set(leaders.map(leader => leader.division))];
|
||||
}, []);
|
||||
|
||||
const handleSearch = (value: string) => {
|
||||
setSearchQuery(value);
|
||||
|
@ -54,17 +50,7 @@ export default function Filter({ onSearch, onDivisionChange }: FilterProps) {
|
|||
/>
|
||||
</div>
|
||||
|
||||
<Select
|
||||
size="large"
|
||||
value={selectedDivision}
|
||||
onChange={handleDivisionChange}
|
||||
suffixIcon={<FunnelIcon className="w-5 h-5 text-gray-400" />}
|
||||
className="w-full md:w-64"
|
||||
options={divisions.map(division => ({
|
||||
value: division,
|
||||
label: division === 'all' ? 'All Divisions' : division,
|
||||
}))}
|
||||
/>
|
||||
<DepartmentSelect ></DepartmentSelect>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,70 +1,70 @@
|
|||
export default function Header() {
|
||||
return <header className="bg-gradient-to-r from-primary to-primary-50 text-white p-6">
|
||||
<div className="flex flex-col space-y-6">
|
||||
{/* 主标题 */}
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-wider">
|
||||
信件投递入口
|
||||
</h1>
|
||||
<p className="mt-2 text-blue-100 text-lg">
|
||||
保护您隐私的信件传输平台
|
||||
</p>
|
||||
</div>
|
||||
return <header className="bg-gradient-to-r from-primary to-primary-400 text-white p-6">
|
||||
<div className="flex flex-col space-y-6">
|
||||
{/* 主标题 */}
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-wider">
|
||||
信件投递入口
|
||||
</h1>
|
||||
<p className="mt-2 text-blue-100 text-lg">
|
||||
保护您隐私的信件传输平台
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 隐私保护说明 */}
|
||||
<div className="flex flex-wrap gap-6 text-sm">
|
||||
<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="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
|
||||
/>
|
||||
</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="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
||||
/>
|
||||
</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 11V7a4 4 0 118 0m-4 8v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
<span>网络数据加密存储</span>
|
||||
</div>
|
||||
{/* 隐私保护说明 */}
|
||||
<div className="flex flex-wrap gap-6 text-sm">
|
||||
<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="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
|
||||
/>
|
||||
</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="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
||||
/>
|
||||
</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 11V7a4 4 0 118 0m-4 8v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
<span>网络数据加密存储</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 隐私承诺 */}
|
||||
<div className="text-sm text-blue-100 border-t border-blue-400/30 pt-4">
|
||||
<p>您的个人信息将被严格保密,不向任何第三方透露。您可以选择匿名反映问题,平台会自动过滤可能暴露身份的信息。</p>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
{/* 隐私承诺 */}
|
||||
<div className="text-sm text-blue-100 border-t border-blue-400/30 pt-4">
|
||||
<p>您的个人信息将被严格保密,不向任何第三方透露。您可以选择匿名反映问题,平台会自动过滤可能暴露身份的信息。</p>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
}
|
||||
|
|
|
@ -5,18 +5,16 @@ import { leaders } from './mock';
|
|||
import Header from './header';
|
||||
import Filter from './filter';
|
||||
import LeaderCard from './LeaderCard';
|
||||
|
||||
import { Spin, Empty } from 'antd';
|
||||
import { api } from 'packages/client/dist';
|
||||
|
||||
|
||||
export default function WriteLetterPage() {
|
||||
|
||||
const [selectedLeader, setSelectedLeader] = useState<Leader | null>(null);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [selectedDivision, setSelectedDivision] = useState<string>('all');
|
||||
|
||||
const divisions = useMemo(() => {
|
||||
return ['all', ...new Set(leaders.map(leader => leader.division))];
|
||||
}, []);
|
||||
|
||||
const filteredLeaders = useMemo(() => {
|
||||
return leaders.filter(leader => {
|
||||
const matchesSearch = leader.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
|
@ -57,24 +55,7 @@ export default function WriteLetterPage() {
|
|||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
>
|
||||
<div className="inline-flex flex-col items-center gap-4">
|
||||
<svg
|
||||
className="w-16 h-16 text-gray-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
<p className="text-gray-600 text-lg">
|
||||
No leaders found matching your search criteria
|
||||
</p>
|
||||
</div>
|
||||
<Empty></Empty>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
|
|
@ -2,12 +2,13 @@ import { useAuth } from "@web/src/providers/auth-provider";
|
|||
import { Avatar, Tag, theme, Tooltip } from "antd";
|
||||
import React, { ReactNode, useEffect, useState, useRef, CSSProperties } from "react";
|
||||
import { SyncOutlined } from "@ant-design/icons";
|
||||
import Breadcrumb from "../layout/breadcrumb";
|
||||
import * as Y from "yjs";
|
||||
import { stringToColor, YWsProvider } from "@nice/common";
|
||||
import { lightenColor } from "@nice/client"
|
||||
import { useLocalSettings } from "@web/src/hooks/useLocalSetting";
|
||||
interface FixedHeaderProps {
|
||||
import Breadcrumb from "../element/breadcrumb";
|
||||
|
||||
interface AdminHeaderProps {
|
||||
children?: ReactNode;
|
||||
roomId?: string;
|
||||
awarePlaceholder?: string;
|
||||
|
@ -16,7 +17,7 @@ interface FixedHeaderProps {
|
|||
className?: string;
|
||||
}
|
||||
|
||||
const FixedHeader: React.FC<FixedHeaderProps> = ({
|
||||
const AdminHeader: React.FC<AdminHeaderProps> = ({
|
||||
className,
|
||||
style,
|
||||
borderless = false,
|
||||
|
@ -173,4 +174,4 @@ const FixedHeader: React.FC<FixedHeaderProps> = ({
|
|||
);
|
||||
};
|
||||
|
||||
export default FixedHeader;
|
||||
export default AdminHeader;
|
|
@ -0,0 +1,20 @@
|
|||
import { Outlet } from "react-router-dom";
|
||||
import { Layout } from "antd";
|
||||
|
||||
import { adminRoute } from "@web/src/routes/admin-route";
|
||||
import AdminSidebar from "./AdminSidebar";
|
||||
|
||||
const { Content } = Layout;
|
||||
|
||||
export default function AdminLayout() {
|
||||
return (
|
||||
<Layout className="min-h-screen">
|
||||
<AdminSidebar routes={adminRoute.children || []} />
|
||||
<Layout>
|
||||
<Content >
|
||||
<Outlet />
|
||||
</Content>
|
||||
</Layout>
|
||||
</Layout>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
import { useState, useMemo } from 'react';
|
||||
import { NavLink, matchPath, useLocation, useMatches } from 'react-router-dom';
|
||||
import { Layout, Menu, theme } from 'antd';
|
||||
import type { MenuProps } from 'antd';
|
||||
import { CustomRouteObject } from '@web/src/routes/types';
|
||||
|
||||
const { Sider } = Layout;
|
||||
const { useToken } = theme;
|
||||
|
||||
type SidebarProps = {
|
||||
routes: CustomRouteObject[];
|
||||
};
|
||||
|
||||
export default function AdminSidebar({ routes }: SidebarProps) {
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
const { token } = useToken();
|
||||
let matches = useMatches();
|
||||
console.log(matches)
|
||||
const menuItems: MenuProps['items'] = useMemo(() =>
|
||||
routes.map(route => ({
|
||||
key: route.path,
|
||||
icon: route.icon,
|
||||
label: (
|
||||
<NavLink
|
||||
to={route.path}
|
||||
|
||||
>
|
||||
{route.name}
|
||||
</NavLink>
|
||||
),
|
||||
}))
|
||||
, [routes]);
|
||||
|
||||
return (
|
||||
<Sider
|
||||
collapsible
|
||||
collapsed={collapsed}
|
||||
onCollapse={(value) => setCollapsed(value)}
|
||||
width={150}
|
||||
className="h-screen sticky top-0"
|
||||
style={{
|
||||
backgroundColor: token.colorBgContainer,
|
||||
borderRight: `1px solid ${token.colorBorderSecondary}`
|
||||
}}
|
||||
>
|
||||
<Menu
|
||||
theme="light"
|
||||
mode="inline"
|
||||
selectedKeys={routes
|
||||
.filter(route => matches.some(match => match.pathname.includes(route.path)))
|
||||
.map(route => route.path)}
|
||||
|
||||
items={menuItems}
|
||||
className="border-r-0"
|
||||
style={{
|
||||
borderRight: 0
|
||||
}}
|
||||
/>
|
||||
</Sider>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
import { Link } from "react-router-dom";
|
||||
|
||||
export const Logo = () => (
|
||||
<Link to="/" className="flex items-center space-x-3 group rounded-lg focus:outline-none focus:ring-2 focus:ring-[#8EADD4]" aria-label="Go to homepage">
|
||||
<div className="relative h-12 w-12 transform transition-transform group-hover:scale-105">
|
||||
<svg
|
||||
viewBox="0 0 100 100"
|
||||
className="h-full w-full transition-transform duration-300"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
{/* Mail envelope base */}
|
||||
<rect
|
||||
x="10"
|
||||
y="25"
|
||||
width="80"
|
||||
height="50"
|
||||
rx="4"
|
||||
className="fill-[#8EADD4] transition-colors duration-300 group-hover:fill-[#6B8CB3] rounded-lg"
|
||||
/>
|
||||
|
||||
{/* Envelope flap */}
|
||||
<path
|
||||
d="M10 29L50 55L90 29"
|
||||
strokeWidth="3"
|
||||
strokeLinecap="round"
|
||||
className="stroke-white"
|
||||
/>
|
||||
|
||||
{/* People silhouette */}
|
||||
<path
|
||||
d="M40 45C40 45 35 50 30 50C25 50 20 45 20 45C20 45 25 55 30 55C35 55 40 45 40 45Z"
|
||||
className="fill-white"
|
||||
/>
|
||||
<circle cx="30" cy="42" r="5" className="fill-white" />
|
||||
|
||||
{/* Leadership star */}
|
||||
<path
|
||||
d="M70 42L72.5 47L78 48L74 52L75 57L70 54.5L65 57L66 52L62 48L67.5 47L70 42Z"
|
||||
className="fill-white transition-transform origin-center group-hover:rotate-45 duration-500"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
</Link>
|
||||
);
|
|
@ -6,7 +6,6 @@ import { RightOutlined } from '@ant-design/icons';
|
|||
export default function Breadcrumb() {
|
||||
let matches = useMatches();
|
||||
const { token } = theme.useToken()
|
||||
|
||||
let crumbs = matches
|
||||
// first get rid of any matches that don't have handle and crumb
|
||||
.filter((match) => Boolean((match.handle as any)?.crumb))
|
|
@ -9,33 +9,9 @@ import {
|
|||
QuestionCircleOutlined,
|
||||
LogoutOutlined
|
||||
} from "@ant-design/icons";
|
||||
import type { MenuItemType } from "./types";
|
||||
import { Spin } from "antd";
|
||||
|
||||
// USAF Theme Constants
|
||||
const USAF_THEME = {
|
||||
colors: {
|
||||
primary: '#00538E', // Air Force Blue
|
||||
secondary: '#003F6A', // Darker Blue
|
||||
accent: '#B22234', // Air Force Red
|
||||
background: '#F6F9FC', // Light Blue tint
|
||||
hover: '#E6EEF5', // Lighter hover state
|
||||
border: '#E5EDF5', // Light Border
|
||||
text: {
|
||||
primary: '#00538E',
|
||||
secondary: '#4A5568',
|
||||
light: '#718096',
|
||||
danger: '#B22234'
|
||||
}
|
||||
},
|
||||
shadows: {
|
||||
sm: '0 2px 4px 0 rgba(0, 83, 142, 0.08)',
|
||||
md: '0 4px 8px -1px rgba(0, 83, 142, 0.15), 0 2px 4px -1px rgba(0, 83, 142, 0.08)',
|
||||
lg: '0 12px 20px -3px rgba(0, 83, 142, 0.15), 0 4px 8px -2px rgba(0, 83, 142, 0.1)',
|
||||
hover: '0 6px 12px -2px rgba(0, 83, 142, 0.12), 0 3px 6px -1px rgba(0, 83, 142, 0.07)'
|
||||
}
|
||||
} as const;
|
||||
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { MenuItemType } from "./types";
|
||||
const menuVariants = {
|
||||
hidden: { opacity: 0, scale: 0.95, y: -10 },
|
||||
visible: {
|
||||
|
@ -62,7 +38,7 @@ export function UserMenu() {
|
|||
const [showMenu, setShowMenu] = useState(false);
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
const { user, logout, isLoading } = useAuth();
|
||||
|
||||
const navigate = useNavigate()
|
||||
useClickOutside(menuRef, () => setShowMenu(false));
|
||||
|
||||
const toggleMenu = useCallback(() => {
|
||||
|
@ -78,7 +54,9 @@ export function UserMenu() {
|
|||
{
|
||||
icon: <SettingOutlined className="text-lg" />,
|
||||
label: '设置',
|
||||
action: () => { },
|
||||
action: () => {
|
||||
navigate('/admin/staff')
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: <QuestionCircleOutlined className="text-lg" />,
|
||||
|
@ -156,7 +134,7 @@ export function UserMenu() {
|
|||
<div
|
||||
className="px-4 py-4 bg-gradient-to-b from-[#F6F9FC] to-white
|
||||
border-b border-[#E5EDF5] "
|
||||
|
||||
|
||||
>
|
||||
<div className="flex items-center space-x-4">
|
||||
<Avatar
|
|
@ -1,11 +1,11 @@
|
|||
import { MagnifyingGlassIcon, UserIcon } from "@heroicons/react/24/outline";
|
||||
import { Link, NavLink } from "react-router-dom";
|
||||
import { memo } from "react";
|
||||
import { SearchBar } from "./SearchBar";
|
||||
import { Logo } from "./Logo";
|
||||
import Navigation from "./navigation";
|
||||
import { UserMenu} from "./usermenu";
|
||||
import { useAuth } from "@web/src/providers/auth-provider";
|
||||
import { UserOutlined } from "@ant-design/icons";
|
||||
import { UserMenu } from "../element/usermenu";
|
||||
import { Logo } from "../element/Logo";
|
||||
interface HeaderProps {
|
||||
onSearch?: (query: string) => void;
|
||||
}
|
||||
|
@ -35,9 +35,10 @@ focus:ring-[#8EADD4] focus:ring-offset-2
|
|||
focus:ring-offset-[#13294B]"
|
||||
aria-label="Login"
|
||||
>
|
||||
<UserIcon className="h-5 w-5 transition-transform
|
||||
group-hover:scale-110 group-hover:rotate-12" />
|
||||
<span>Login</span>
|
||||
<UserOutlined className="h-5 w-5 transition-transform
|
||||
group-hover:scale-110 group-hover:rotate-12"></UserOutlined>
|
||||
|
||||
<span>登录</span>
|
||||
</Link> : <UserMenu />
|
||||
|
||||
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
import { Link } from "react-router-dom";
|
||||
|
||||
export const Logo = () => (
|
||||
<Link to="/" className="flex items-center space-x-3 group focus:outline-none focus:ring-2 focus:ring-[#8EADD4] rounded-lg">
|
||||
<img
|
||||
src="/logo.svg"
|
||||
alt="USAF Logo"
|
||||
className="h-12 w-auto transform transition-transform group-hover:scale-105"
|
||||
loading="eager"
|
||||
/>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold tracking-wider">Leadership Mailbox</h1>
|
||||
<p className="text-sm text-[#8EADD4]">United States Air Force</p>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
|
@ -1,40 +0,0 @@
|
|||
import { useState, useRef, useEffect } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { NotificationsPanel } from './notifications-panel';
|
||||
import { BellIcon } from '@heroicons/react/24/outline';
|
||||
import { useClickOutside } from '@web/src/hooks/useClickOutside';
|
||||
|
||||
interface NotificationsDropdownProps {
|
||||
notifications: number;
|
||||
notificationItems: Array<any>;
|
||||
}
|
||||
|
||||
export function NotificationsDropdown({ notifications, notificationItems }: NotificationsDropdownProps) {
|
||||
const [showNotifications, setShowNotifications] = useState(false);
|
||||
const notificationRef = useRef<HTMLDivElement>(null);
|
||||
useClickOutside(notificationRef, () => setShowNotifications(false));
|
||||
return (
|
||||
<div ref={notificationRef} className="relative">
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
className="p-2 rounded-lg hover:bg-gray-100 transition-colors"
|
||||
onClick={() => setShowNotifications(!showNotifications)}
|
||||
>
|
||||
<BellIcon className='w-6 h-6' ></BellIcon>
|
||||
|
||||
{notifications > 0 && (
|
||||
<span className="absolute top-0 right-0 w-5 h-5 bg-red-500 text-white rounded-full text-xs flex items-center justify-center">
|
||||
{notifications}
|
||||
</span>
|
||||
)}
|
||||
</motion.button>
|
||||
|
||||
<AnimatePresence>
|
||||
{showNotifications && (
|
||||
<NotificationsPanel notificationItems={notificationItems} />
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,64 +0,0 @@
|
|||
import { ClockIcon } from '@heroicons/react/24/outline';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
interface NotificationsPanelProps {
|
||||
notificationItems: Array<{
|
||||
icon: React.ReactNode;
|
||||
title: string;
|
||||
description: string;
|
||||
time: string;
|
||||
isUnread: boolean;
|
||||
}>;
|
||||
}
|
||||
|
||||
export function NotificationsPanel({ notificationItems }: NotificationsPanelProps) {
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -10 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="absolute right-0 mt-2 w-80 bg-white rounded-xl shadow-lg border border-gray-200 overflow-hidden"
|
||||
>
|
||||
<div className="p-4 border-b border-gray-100">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-lg font-semibold text-gray-900">Notifications</h3>
|
||||
<span className="text-sm text-primary-600 hover:text-blue-700 cursor-pointer">
|
||||
Mark all as read
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="max-h-[400px] overflow-y-auto overflow-x-hidden">
|
||||
{notificationItems.map((item, index) => (
|
||||
<motion.div
|
||||
key={index}
|
||||
whileHover={{ x: 4 }}
|
||||
className={`p-4 border-b border-gray-100 cursor-pointer hover:bg-gray-50 transition-colors ${item.isUnread ? 'bg-blue-50/50' : ''
|
||||
}`}
|
||||
>
|
||||
<div className="flex gap-3">
|
||||
<div className="flex-shrink-0 w-8 h-8 rounded-full bg-gray-100 flex items-center justify-center">
|
||||
{item.icon}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h4 className="text-sm font-medium text-gray-900">{item.title}</h4>
|
||||
<p className="text-sm text-gray-600 mt-1">{item.description}</p>
|
||||
<div className="flex items-center gap-1 mt-2 text-xs text-tertiary-300">
|
||||
<ClockIcon className='h-4 w-4'></ClockIcon>
|
||||
<span>{item.time}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="p-4 border-t border-gray-100 bg-gray-50">
|
||||
<button className="w-full text-sm text-center text-primary-600 hover:text-blue-700">
|
||||
View all notifications
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
|
@ -1,55 +0,0 @@
|
|||
import { useState, useRef, useEffect } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { SearchDropdown } from './search-dropdown';
|
||||
import { MagnifyingGlassIcon, XMarkIcon } from '@heroicons/react/24/outline';
|
||||
import { useClickOutside } from '@web/src/hooks/useClickOutside';
|
||||
|
||||
interface SearchBarProps {
|
||||
recentSearches: string[];
|
||||
}
|
||||
|
||||
export function SearchBar({ recentSearches }: SearchBarProps) {
|
||||
const [searchFocused, setSearchFocused] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const searchRef = useRef<HTMLDivElement>(null);
|
||||
useClickOutside(searchRef, () => setSearchFocused(false))
|
||||
return (
|
||||
<div ref={searchRef} className="relative max-w-xl w-full px-4">
|
||||
<div className={`
|
||||
relative flex items-center w-full h-10 rounded-full
|
||||
transition-all duration-300 ease-in-out
|
||||
${searchFocused
|
||||
? 'bg-white shadow-md ring-2 ring-blue-500'
|
||||
: 'bg-gray-100 hover:bg-gray-200'
|
||||
}
|
||||
`}>
|
||||
<MagnifyingGlassIcon className="h-5 w-5 ml-3 text-tertiary-300" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search for courses, topics, or instructors..."
|
||||
className="w-full h-full bg-transparent px-3 outline-none text-sm"
|
||||
onFocus={() => setSearchFocused(true)}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
{searchQuery && (
|
||||
<motion.button
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.8 }}
|
||||
className="p-1.5 mr-2 rounded-full hover:bg-gray-200"
|
||||
onClick={() => setSearchQuery('')}
|
||||
>
|
||||
<XMarkIcon className="h-4 w-4 text-tertiary-300" />
|
||||
</motion.button>
|
||||
)}
|
||||
</div>
|
||||
<SearchDropdown
|
||||
searchFocused={searchFocused}
|
||||
searchQuery={searchQuery}
|
||||
recentSearches={recentSearches}
|
||||
setSearchQuery={setSearchQuery}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,58 +0,0 @@
|
|||
import { MagnifyingGlassIcon } from '@heroicons/react/24/outline';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
interface SearchDropdownProps {
|
||||
searchFocused: boolean;
|
||||
searchQuery: string;
|
||||
recentSearches: string[];
|
||||
setSearchQuery: (query: string) => void;
|
||||
}
|
||||
|
||||
export function SearchDropdown({
|
||||
searchFocused,
|
||||
searchQuery,
|
||||
recentSearches,
|
||||
setSearchQuery
|
||||
}: SearchDropdownProps) {
|
||||
if (!searchFocused) return null;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -10 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="absolute top-12 left-4 right-4 bg-white rounded-xl shadow-lg border border-gray-200 overflow-hidden"
|
||||
>
|
||||
<div className="p-3">
|
||||
<h3 className="text-xs font-medium text-tertiary-300 mb-2">Recent Searches</h3>
|
||||
<div className="space-y-1">
|
||||
{recentSearches.map((search, index) => (
|
||||
<motion.button
|
||||
key={index}
|
||||
whileHover={{ x: 4 }}
|
||||
className="flex items-center gap-2 w-full p-2 rounded-lg hover:bg-gray-100 text-left"
|
||||
onClick={() => setSearchQuery(search)}
|
||||
>
|
||||
<MagnifyingGlassIcon className="h-4 w-4 text-tertiary-300" />
|
||||
<span className="text-sm text-gray-700">{search}</span>
|
||||
</motion.button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{searchQuery && (
|
||||
<div className="border-t border-gray-100 p-3">
|
||||
<motion.button
|
||||
whileHover={{ x: 4 }}
|
||||
className="flex items-center gap-2 w-full p-2 rounded-lg hover:bg-gray-100"
|
||||
>
|
||||
<MagnifyingGlassIcon className="h-4 w-4 text-blue-500" />
|
||||
<span className="text-sm text-blue-500">
|
||||
Search for "{searchQuery}"
|
||||
</span>
|
||||
</motion.button>
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
|
@ -1,95 +0,0 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { ResizableBox } from 'react-resizable';
|
||||
import 'react-resizable/css/styles.css';
|
||||
import { theme } from 'antd';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
type SidebarProps = {
|
||||
children: ReactNode;
|
||||
handlePosition?: 'left' | 'right';
|
||||
className?: string;
|
||||
minWidth?: number;
|
||||
maxWidth?: number;
|
||||
defaultWidth?: number;
|
||||
onWidthChange?: (width: number) => void; // New prop for handling width change
|
||||
};
|
||||
|
||||
export default function ResizableSidebar({
|
||||
children,
|
||||
handlePosition = 'right',
|
||||
className = '',
|
||||
minWidth = 200,
|
||||
maxWidth = 400,
|
||||
defaultWidth = 200,
|
||||
onWidthChange
|
||||
}: SidebarProps) {
|
||||
const [width, setWidth] = useState(defaultWidth);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [isHoveringHandle, setIsHoveringHandle] = useState(false);
|
||||
const { token } = theme.useToken();
|
||||
|
||||
useEffect(() => {
|
||||
if (isDragging) {
|
||||
document.body.style.cursor = 'col-resize';
|
||||
} else {
|
||||
document.body.style.cursor = '';
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.body.style.cursor = ''; // Cleanup on unmount
|
||||
};
|
||||
}, [isDragging]);
|
||||
|
||||
const handleResizeStop = (e, data) => {
|
||||
const newWidth = data.size.width;
|
||||
setWidth(newWidth);
|
||||
setIsDragging(false);
|
||||
|
||||
if (onWidthChange) {
|
||||
onWidthChange(newWidth); // Call the callback with new width
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ResizableBox
|
||||
width={width}
|
||||
height={Infinity}
|
||||
axis="x"
|
||||
resizeHandles={handlePosition === 'left' ? ['w'] : ['e']}
|
||||
minConstraints={[minWidth, Infinity]}
|
||||
maxConstraints={[maxWidth, Infinity]}
|
||||
onResizeStart={() => setIsDragging(true)}
|
||||
onResizeStop={handleResizeStop}
|
||||
handle={
|
||||
<span
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
[handlePosition]: -7,
|
||||
width: '14px',
|
||||
height: '100%',
|
||||
cursor: (isHoveringHandle || isDragging) ? 'col-resize' : "default",
|
||||
zIndex: 1,
|
||||
backgroundColor: 'transparent'
|
||||
}}
|
||||
onMouseEnter={() => setIsHoveringHandle(true)}
|
||||
onMouseLeave={() => setIsHoveringHandle(false)}
|
||||
/>
|
||||
}
|
||||
className={className}
|
||||
style={{
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
...(handlePosition === 'right' && {
|
||||
borderRight: (isDragging || isHoveringHandle) ? `2px solid ${token.colorPrimaryBorder}` : ``,
|
||||
}),
|
||||
...(handlePosition === 'left' && {
|
||||
borderLeft: (isDragging || isHoveringHandle) ? `2px solid ${token.colorPrimaryBorder}` : ``,
|
||||
}),
|
||||
transition: 'border-color 0.3s',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ResizableBox>
|
||||
);
|
||||
}
|
|
@ -1,85 +0,0 @@
|
|||
import { Avatar, Divider, Dropdown, theme } from "antd";
|
||||
import { Icon } from "@nice/iconer";
|
||||
|
||||
import CollapsibleSection from "../presentation/collapse-section";
|
||||
import { useAuth } from "@web/src/providers/auth-provider";
|
||||
import { RolePerms } from "@nice/common";
|
||||
|
||||
export default function SidebarContent() {
|
||||
const { logout, user, isAuthenticated, hasSomePermissions } = useAuth();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="mt-12">
|
||||
<CollapsibleSection
|
||||
defaultExpandedKeys={["1", "2", "3"]}
|
||||
items={[
|
||||
// {
|
||||
// key: "home",
|
||||
// label: "首页",
|
||||
// icon: <Icon name={"home"}></Icon>,
|
||||
// link: "/",
|
||||
// },
|
||||
|
||||
hasSomePermissions(
|
||||
RolePerms.MANAGE_ANY_DEPT,
|
||||
RolePerms.MANAGE_ANY_STAFF,
|
||||
RolePerms.MANAGE_ANY_ROLE,
|
||||
RolePerms.MANAGE_DOM_STAFF,
|
||||
RolePerms.MANAGE_BASE_SETTING
|
||||
) && {
|
||||
key: "4",
|
||||
label: "系统设置",
|
||||
icon: <Icon name="setting"></Icon>,
|
||||
children: [
|
||||
hasSomePermissions(
|
||||
RolePerms.MANAGE_BASE_SETTING
|
||||
) && {
|
||||
key: "4-0",
|
||||
icon: <Icon name="config"></Icon>,
|
||||
label: "参数配置",
|
||||
link: "/admin/base-setting",
|
||||
},
|
||||
|
||||
hasSomePermissions(
|
||||
RolePerms.MANAGE_ANY_TERM,
|
||||
// RolePerms.MANAGE_DOM_TERM
|
||||
) && {
|
||||
key: "4-1",
|
||||
icon: <Icon name="category-outline"></Icon>,
|
||||
label: "分类配置",
|
||||
link: "/admin/term",
|
||||
},
|
||||
hasSomePermissions(
|
||||
RolePerms.MANAGE_ANY_DEPT
|
||||
) && {
|
||||
key: "4-5",
|
||||
icon: <Icon name="org"></Icon>,
|
||||
label: "组织架构",
|
||||
link: "/admin/department",
|
||||
},
|
||||
hasSomePermissions(
|
||||
RolePerms.MANAGE_ANY_STAFF,
|
||||
RolePerms.MANAGE_DOM_STAFF
|
||||
) && {
|
||||
key: "4-6",
|
||||
icon: <Icon name="people-group"></Icon>,
|
||||
label: "用户管理",
|
||||
link: "/admin/staff",
|
||||
},
|
||||
hasSomePermissions(
|
||||
RolePerms.MANAGE_ANY_ROLE,
|
||||
RolePerms.MANAGE_DOM_ROLE
|
||||
) && {
|
||||
key: "4-7",
|
||||
icon: <Icon name="admin-outlined"></Icon>,
|
||||
label: "角色管理",
|
||||
link: "/admin/role",
|
||||
},
|
||||
].filter(Boolean),
|
||||
},
|
||||
].filter(Boolean)}></CollapsibleSection>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,50 +0,0 @@
|
|||
import { Avatar, Button, Dropdown, theme } from "antd";
|
||||
import { useAuth } from "@web/src/providers/auth-provider";
|
||||
import { Icon } from "@nice/iconer";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
export default function UserHeader() {
|
||||
const { logout, user, isAuthenticated } = useAuth();
|
||||
const { token } = theme.useToken();
|
||||
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
<div className="p-2 flex items-center justify-end bg-gradient-to-r from-primary to-primaryActive">
|
||||
<div className=" flex items-center gap-4">
|
||||
<div
|
||||
style={{ color: token.colorTextLightSolid }}
|
||||
className="rounded flex items-center select-none justify-between">
|
||||
<div
|
||||
className="flex hover:bg-blue-200 rounded px-2 items-center gap-4 hover:cursor-pointer"
|
||||
onClick={() => {
|
||||
// if (user?.pilot?.id) {
|
||||
// navigate(`/pilots/${user?.pilot.id}`);
|
||||
// }
|
||||
}}>
|
||||
|
||||
<Avatar
|
||||
shape={"circle"}
|
||||
// src={user?.pilot?.photo}
|
||||
style={{ background: token.colorPrimary }}>
|
||||
{(user?.showname || user?.username)
|
||||
?.slice(0, 1)
|
||||
.toUpperCase()}
|
||||
</Avatar>
|
||||
<span>{user?.showname || user?.username}</span>
|
||||
{user?.department && <>
|
||||
<Icon name="org"></Icon>
|
||||
<span className=" font-bold">{user?.department?.name}</span></>}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
onClick={async () => {
|
||||
await logout()
|
||||
}}
|
||||
className="active:bg-gray-100/60 flex items-center gap-2 text-white hover:bg-gray-100/30 px-2 rounded py-1 cursor-pointer">
|
||||
<Icon name="logout" />
|
||||
注销
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -7,7 +7,7 @@ import { useForm } from "antd/es/form/Form";
|
|||
import DepartmentList from "./department-list";
|
||||
import DeptModal from "./dept-modal";
|
||||
import DeptImportModal from "./dept-import-modal";
|
||||
import FixedHeader from "../../layout/fix-header";
|
||||
import AdminHeader from "../../layout/admin/AdminHeader";
|
||||
export const DeptEditorContext = createContext<{
|
||||
parentId: string;
|
||||
domainId: string;
|
||||
|
@ -58,7 +58,7 @@ export default function DeptEditor() {
|
|||
setImportModalOpen,
|
||||
importModalOpen,
|
||||
}}>
|
||||
<FixedHeader roomId="dept-editor">
|
||||
<AdminHeader roomId="dept-editor">
|
||||
<div className=" flex items-center gap-4 ">
|
||||
{canManageDept && (
|
||||
<>
|
||||
|
@ -81,7 +81,7 @@ export default function DeptEditor() {
|
|||
)}
|
||||
</div>
|
||||
|
||||
</FixedHeader>
|
||||
</AdminHeader>
|
||||
<DepartmentList></DepartmentList>
|
||||
<DeptModal />
|
||||
<DeptImportModal />
|
||||
|
|
|
@ -71,9 +71,9 @@ export default function RoleList() {
|
|||
setRole(item as any)
|
||||
}}
|
||||
style={{
|
||||
background: item.id === role?.id ? token.colorPrimaryBg : ""
|
||||
background: item.id === role?.id ? token.colorBgTextHover : ""
|
||||
}}
|
||||
className={`p-2 hover:bg-textHover text-tertiary-300 ${item.id === role?.id ? " text-primary border-l-4 border-primaryHover" : ""} transition-all ease-in-out flex items-center justify-between `}
|
||||
className={`p-2 hover:bg-textHover text-tertiary-300 ${item.id === role?.id ? " text-primary border-l-4 border-primary" : ""} transition-all ease-in-out flex items-center justify-between `}
|
||||
key={item.id}>
|
||||
<div className=" flex items-center gap-2">
|
||||
<span className="text-primary"> <UserOutlined></UserOutlined></span>
|
||||
|
|
|
@ -7,7 +7,8 @@ import { useAuth } from "@web/src/providers/auth-provider";
|
|||
import { Button } from "antd";
|
||||
import DepartmentSelect from "../department/department-select";
|
||||
import { FormInstance, useForm } from "antd/es/form/Form";
|
||||
import FixedHeader from "../../layout/fix-header";
|
||||
import AdminHeader from "../../layout/admin/AdminHeader";
|
||||
|
||||
export const StaffEditorContext = createContext<{
|
||||
domainId: string,
|
||||
modalOpen: boolean,
|
||||
|
@ -51,7 +52,7 @@ export default function StaffEditor() {
|
|||
return hasSomePermissions(RolePerms.MANAGE_ANY_STAFF)
|
||||
}, [user])
|
||||
return <StaffEditorContext.Provider value={{ canManageAnyStaff, formLoading, setFormLoading, form, editId, setEditId, domainId, modalOpen, setDomainId, setModalOpen }}>
|
||||
<FixedHeader roomId="staff-editor">
|
||||
<AdminHeader roomId="staff-editor">
|
||||
<div className="flex items-center gap-4">
|
||||
<DepartmentSelect rootId={user?.domainId} onChange={(value) => setDomainId(value as string)} disabled={!canManageAnyStaff} value={domainId} className="w-48" domain={true}></DepartmentSelect>
|
||||
{canManageStaff && <Button
|
||||
|
@ -64,7 +65,7 @@ export default function StaffEditor() {
|
|||
添加用户
|
||||
</Button>}
|
||||
</div>
|
||||
</FixedHeader>
|
||||
</AdminHeader>
|
||||
<StaffList domainId={domainId}></StaffList>
|
||||
<StaffModal></StaffModal>
|
||||
</StaffEditorContext.Provider>
|
||||
|
|
|
@ -25,12 +25,12 @@ const TaxonomyList: React.FC = () => {
|
|||
{taxonomies?.map((item) => (
|
||||
<div
|
||||
style={{
|
||||
background: item.id === taxonomyId ? token.colorPrimaryBg : ""
|
||||
background: item.id === taxonomyId ? token.colorBgTextHover : ""
|
||||
}}
|
||||
key={item.id} onClick={() => {
|
||||
setTaxonomyId(item.id)
|
||||
setTaxonomyName(item?.name)
|
||||
}} className={`flex items-center ${item.id === taxonomyId ? " text-primary border-l-4 border-primaryHover" : ""} gap-4 p-2 hover:bg-textHover transition-all ease-in-out`}>
|
||||
}} className={`flex items-center ${item.id === taxonomyId ? " text-primary border-l-4 border-primary" : ""} gap-4 p-2 hover:bg-textHover transition-all ease-in-out`}>
|
||||
<div className=''>
|
||||
<span>{item.name}</span>
|
||||
</div>
|
||||
|
|
|
@ -1,58 +0,0 @@
|
|||
// components/NavBar.tsx
|
||||
import { motion } from "framer-motion";
|
||||
import React, { useState } from "react";
|
||||
|
||||
interface NavItem {
|
||||
id: string;
|
||||
icon?: React.ReactNode;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface NavBarProps {
|
||||
items: NavItem[];
|
||||
defaultSelected?: string;
|
||||
onSelect?: (id: string) => void;
|
||||
}
|
||||
|
||||
export const NavBar = ({ items, defaultSelected, onSelect }: NavBarProps) => {
|
||||
const [selected, setSelected] = useState(defaultSelected || items[0]?.id);
|
||||
|
||||
const handleSelect = (id: string) => {
|
||||
setSelected(id);
|
||||
onSelect?.(id);
|
||||
};
|
||||
|
||||
return (
|
||||
<nav className="bg-white px-4 py-2 shadow-sm">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<ul className="flex items-center space-x-8">
|
||||
{items.map((item) => (
|
||||
<li key={item.id} className="relative">
|
||||
<button
|
||||
onClick={() => handleSelect(item.id)}
|
||||
className={`flex items-center space-x-2 px-2 py-4 text-sm font-medium transition-colors
|
||||
${selected === item.id ? "text-black" : "text-tertiary-300 hover:text-gray-800"}`}>
|
||||
{item.icon && (
|
||||
<span className="w-4 h-4">{item.icon}</span>
|
||||
)}
|
||||
<span>{item.label}</span>
|
||||
</button>
|
||||
{selected === item.id && (
|
||||
<motion.div
|
||||
className="absolute bottom-0 left-0 right-0 h-0.5 bg-black"
|
||||
layoutId="underline"
|
||||
initial={false}
|
||||
transition={{
|
||||
type: "spring",
|
||||
stiffness: 500,
|
||||
damping: 30,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
};
|
|
@ -368,7 +368,6 @@ const AgServerTable: React.FC<AgTableProps> = ({
|
|||
}}>
|
||||
<div style={{ ...gridStyle }} className="ag-theme-alpine">
|
||||
<AgGridReact
|
||||
|
||||
serverSideInitialRowCount={initialRowCount}
|
||||
localeText={AG_GRID_LOCALE_CH}
|
||||
defaultColDef={{
|
||||
|
|
|
@ -9,6 +9,7 @@ interface CollapsibleSectionProps {
|
|||
items: Array<MenuItem>;
|
||||
className?: string;
|
||||
defaultExpandedKeys?: string[];
|
||||
renderItem?: (item: MenuItem, isActive: boolean, level: number) => React.ReactNode;
|
||||
}
|
||||
|
||||
interface MenuItem {
|
||||
|
@ -25,6 +26,7 @@ const CollapsibleSection: React.FC<CollapsibleSectionProps> = ({
|
|||
items,
|
||||
className,
|
||||
defaultExpandedKeys = [],
|
||||
renderItem,
|
||||
}) => {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
@ -59,12 +61,17 @@ const CollapsibleSection: React.FC<CollapsibleSectionProps> = ({
|
|||
const itemUrl = new URL(item.link, window.location.origin);
|
||||
const itemPath = itemUrl.pathname;
|
||||
const itemSearchParams = new URLSearchParams(itemUrl.search);
|
||||
|
||||
|
||||
const hasChildren = item.children && item.children.length > 0;
|
||||
const isActive =
|
||||
currentPath === itemPath &&
|
||||
Array.from(itemSearchParams.entries()).every(
|
||||
([key, value]) => currentSearchParams.get(key) === value
|
||||
);
|
||||
if (renderItem) {
|
||||
return renderItem(item, isActive, level);
|
||||
}
|
||||
|
||||
const isChildCollapsed = !expandedSections[item.key];
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ export default function Brightness() {
|
|||
<>
|
||||
{/* 亮度控制 */}
|
||||
<div className="relative group flex items-center">
|
||||
<button className="text-white hover:text-primaryHover">
|
||||
<button className="text-white hover:text-primary-400">
|
||||
<SunIcon className="w-10 h-10" />
|
||||
</button>
|
||||
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200">
|
||||
|
|
|
@ -13,7 +13,7 @@ export default function Play() {
|
|||
? videoRef.current.play()
|
||||
: videoRef.current?.pause()
|
||||
}
|
||||
className="text-white hover:text-primaryHover">
|
||||
className="text-white hover:text-primary-400">
|
||||
{isPlaying ? (
|
||||
<PauseIcon className="w-10 h-10" />
|
||||
) : (
|
||||
|
|
|
@ -17,7 +17,7 @@ export default function Setting() {
|
|||
<div className="relative flex items-center">
|
||||
<button
|
||||
onClick={() => setIsSettingsOpen(!isSettingsOpen)}
|
||||
className="text-white hover:text-primaryHover">
|
||||
className="text-white hover:text-primary-400">
|
||||
<Cog6ToothIcon className="w-10 h-10" />
|
||||
</button>
|
||||
|
||||
|
@ -42,11 +42,10 @@ export default function Setting() {
|
|||
}}
|
||||
className={`
|
||||
w-full text-left px-3 py-2 rounded
|
||||
${
|
||||
resolution === res.id
|
||||
? "bg-primary text-white"
|
||||
: "text-white/90 hover:bg-white/20"
|
||||
}
|
||||
${resolution === res.id
|
||||
? "bg-primary text-white"
|
||||
: "text-white/90 hover:bg-white/20"
|
||||
}
|
||||
transition-colors duration-200
|
||||
`}>
|
||||
{res.label || `${res.height}p`}
|
||||
|
|
|
@ -16,7 +16,7 @@ export default function Speed() {
|
|||
<div className="relative flex items-center">
|
||||
<button
|
||||
onClick={() => setIsSpeedOpen(!isSpeedOpen)}
|
||||
className="text-white hover:text-primaryHover flex items-center">
|
||||
className="text-white hover:text-primary-400 flex items-center">
|
||||
<span className="text-xl font-bold mr-1">
|
||||
{playbackSpeed === 1.0 ? "倍速" : `${playbackSpeed}x`}
|
||||
</span>
|
||||
|
@ -40,11 +40,10 @@ export default function Speed() {
|
|||
}
|
||||
setIsSpeedOpen(false);
|
||||
}}
|
||||
className={`px-2 py-1 text-lg whitespace-nowrap ${
|
||||
playbackSpeed === speed
|
||||
? "text-primaryHover font-bold"
|
||||
: "text-white hover:text-primaryHover"
|
||||
}`}>
|
||||
className={`px-2 py-1 text-lg whitespace-nowrap ${playbackSpeed === speed
|
||||
? "text-primary-400 font-bold"
|
||||
: "text-white hover:text-primary-400"
|
||||
}`}>
|
||||
{speed}x
|
||||
</button>
|
||||
)
|
||||
|
|
|
@ -11,7 +11,7 @@ export default function Volume() {
|
|||
<div className="group relative flex items-center">
|
||||
<button
|
||||
onClick={() => setIsMuted(!isMuted)}
|
||||
className="text-white hover:text-primaryHover">
|
||||
className="text-white hover:text-primary-400">
|
||||
{isMuted ? (
|
||||
<SpeakerXMarkIcon className="w-10 h-10" />
|
||||
) : (
|
||||
|
|
|
@ -45,11 +45,11 @@
|
|||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.ant-table-thead > tr > th {
|
||||
.ant-table-thead>tr>th {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.ant-table-tbody > tr > td {
|
||||
.ant-table-tbody>tr>td {
|
||||
background-color: transparent !important;
|
||||
border-bottom-color: transparent !important;
|
||||
}
|
||||
|
@ -85,9 +85,7 @@
|
|||
}
|
||||
|
||||
/* 覆盖 Ant Design 的默认样式 raido button左侧的按钮*/
|
||||
.ant-radio-button-wrapper-checked:not(
|
||||
.ant-radio-button-wrapper-disabled
|
||||
)::before {
|
||||
.ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled)::before {
|
||||
background-color: unset !important;
|
||||
}
|
||||
|
||||
|
@ -100,7 +98,7 @@
|
|||
display: none !important;
|
||||
}
|
||||
|
||||
.no-wrap-header .ant-table-thead > tr > th {
|
||||
.no-wrap-header .ant-table-thead>tr>th {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
|
@ -116,15 +114,16 @@
|
|||
/* 设置单元格边框 */
|
||||
}
|
||||
|
||||
.custom-table .ant-table-tbody > tr > td {
|
||||
.custom-table .ant-table-tbody>tr>td {
|
||||
border-bottom: 1px solid #ddd;
|
||||
/* 设置表格行底部边框 */
|
||||
}
|
||||
|
||||
.custom-table .ant-table-tbody > tr:last-child > td {
|
||||
.custom-table .ant-table-tbody>tr:last-child>td {
|
||||
border-bottom: none;
|
||||
/* 去除最后一行的底部边框 */
|
||||
}
|
||||
|
||||
.quill-editor-container .ql-toolbar.ql-snow,
|
||||
.quill-editor-container .ql-container.ql-snow {
|
||||
border-color: transparent;
|
||||
|
@ -144,21 +143,24 @@
|
|||
|
||||
.quill-editor-container .ql-editor {
|
||||
min-height: 120px;
|
||||
color: rgb(30, 41, 59); /* slate-800 */
|
||||
color: rgb(30, 41, 59);
|
||||
/* slate-800 */
|
||||
}
|
||||
|
||||
.quill-editor-container .ql-editor.ql-blank::before {
|
||||
color: rgb(100, 116, 139); /* slate-500 */
|
||||
color: rgb(100, 116, 139);
|
||||
/* slate-500 */
|
||||
}
|
||||
|
||||
.ql-editor {
|
||||
|
||||
|
||||
/* 代码块容器 */
|
||||
.ql-code-block-container {
|
||||
background: #1e293b;
|
||||
color: #e2e8f0;
|
||||
border-radius: 0.5rem;
|
||||
margin: 0; /* 更新 */
|
||||
margin: 0;
|
||||
/* 更新 */
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
|
||||
monospace;
|
||||
|
||||
|
@ -179,7 +181,8 @@
|
|||
color: #e2e8f0;
|
||||
border-radius: 0.5rem;
|
||||
padding: 1rem;
|
||||
margin: 0; /* 更新 */
|
||||
margin: 0;
|
||||
/* 更新 */
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
|
||||
monospace;
|
||||
font-size: 0.875rem;
|
||||
|
@ -194,7 +197,8 @@
|
|||
border-left: 4px solid #3b82f6;
|
||||
background: #f8fafc;
|
||||
padding: 1rem 1.2rem;
|
||||
margin: 0; /* 更新 */
|
||||
margin: 0;
|
||||
/* 更新 */
|
||||
color: #475569;
|
||||
font-style: italic;
|
||||
|
||||
|
@ -202,7 +206,8 @@
|
|||
blockquote {
|
||||
border-left-color: #64748b;
|
||||
background: #f1f5f9;
|
||||
margin: 0; /* 更新 */
|
||||
margin: 0;
|
||||
/* 更新 */
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -210,12 +215,14 @@
|
|||
ol {
|
||||
list-style-type: decimal;
|
||||
padding-left: 2rem;
|
||||
margin: 0; /* 更新 */
|
||||
margin: 0;
|
||||
/* 更新 */
|
||||
|
||||
/* 嵌套有序列表 */
|
||||
ol {
|
||||
list-style-type: lower-alpha;
|
||||
margin: 0; /* 更新 */
|
||||
margin: 0;
|
||||
/* 更新 */
|
||||
|
||||
ol {
|
||||
list-style-type: lower-roman;
|
||||
|
@ -224,7 +231,8 @@
|
|||
|
||||
li {
|
||||
padding-left: 0.5rem;
|
||||
margin-bottom: 0; /* 更新 */
|
||||
margin-bottom: 0;
|
||||
/* 更新 */
|
||||
|
||||
&::marker {
|
||||
color: #3b82f6;
|
||||
|
@ -237,12 +245,14 @@
|
|||
ul {
|
||||
list-style-type: disc;
|
||||
padding-left: 2rem;
|
||||
margin: 0; /* 更新 */
|
||||
margin: 0;
|
||||
/* 更新 */
|
||||
|
||||
/* 嵌套无序列表 */
|
||||
ul {
|
||||
list-style-type: circle;
|
||||
margin: 0; /* 更新 */
|
||||
margin: 0;
|
||||
/* 更新 */
|
||||
|
||||
ul {
|
||||
list-style-type: square;
|
||||
|
@ -251,7 +261,8 @@
|
|||
|
||||
li {
|
||||
padding-left: 0.5rem;
|
||||
margin-bottom: 0; /* 更新 */
|
||||
margin-bottom: 0;
|
||||
/* 更新 */
|
||||
|
||||
&::marker {
|
||||
color: #3b82f6;
|
||||
|
@ -266,7 +277,8 @@
|
|||
color: #1e3a8a;
|
||||
font-weight: 600;
|
||||
line-height: 1.25;
|
||||
margin: 0; /* 更新 */
|
||||
margin: 0;
|
||||
/* 更新 */
|
||||
}
|
||||
|
||||
h1 {
|
||||
|
@ -287,12 +299,14 @@
|
|||
hr {
|
||||
border: 0;
|
||||
border-top: 2px solid #e2e8f0;
|
||||
margin: 0; /* 更新 */
|
||||
margin: 0;
|
||||
/* 更新 */
|
||||
}
|
||||
|
||||
/* 段落 */
|
||||
p {
|
||||
margin: 0; /* 更新 */
|
||||
margin: 0;
|
||||
/* 更新 */
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
|
@ -300,7 +314,8 @@
|
|||
table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin: 0; /* 更新 */
|
||||
margin: 0;
|
||||
/* 更新 */
|
||||
|
||||
th,
|
||||
td {
|
||||
|
@ -318,4 +333,4 @@
|
|||
background: #f8fafc;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -19,19 +19,21 @@ 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 tailwindTheme: TailwindTheme = useMemo(
|
||||
() => ({
|
||||
"--color-primary": token.colorPrimary,
|
||||
"--color-primary-active": token.colorPrimaryActive,
|
||||
"--color-primary-hover": token.colorPrimaryHover,
|
||||
"--color-bg-primary-hover": token.colorPrimaryBgHover,
|
||||
"--color-text-tertiary-400": token.colorTextSecondary,
|
||||
"--color-text-tertiary-400": token.colorTextTertiary,
|
||||
"--color-text-secondary": token.colorTextSecondary,
|
||||
"--color-text-tertiary": token.colorTextTertiary,
|
||||
"--color-bg-text-hover": token.colorBgTextHover,
|
||||
"--color-bg-container": token.colorBgContainer,
|
||||
"--color-bg-layout": token.colorBgLayout,
|
||||
|
|
|
@ -0,0 +1,134 @@
|
|||
import { RolePerms } from "@nice/common";
|
||||
import { Link } from "react-router-dom";
|
||||
import {
|
||||
SettingOutlined,
|
||||
TeamOutlined,
|
||||
UserOutlined,
|
||||
TagsOutlined,
|
||||
SafetyOutlined
|
||||
} from '@ant-design/icons';
|
||||
import BaseSettingPage from "../app/admin/base-setting/page";
|
||||
import DepartmentAdminPage from "../app/admin/department/page";
|
||||
import RoleAdminPage from "../app/admin/role/page";
|
||||
import TermAdminPage from "../app/admin/term/page";
|
||||
import WithAuth from "../components/utils/with-auth";
|
||||
import { CustomRouteObject } from "./types";
|
||||
import StaffPage from "../app/admin/staff/page";
|
||||
import AdminLayout from "../components/layout/admin/AdminLayout";
|
||||
|
||||
export const adminRoute: CustomRouteObject = {
|
||||
path: "admin",
|
||||
name: "系统设置",
|
||||
element: <AdminLayout></AdminLayout>,
|
||||
children: [
|
||||
{
|
||||
path: "base-setting",
|
||||
name: '基本设置',
|
||||
icon: <SettingOutlined />,
|
||||
element: (
|
||||
<WithAuth
|
||||
options={{
|
||||
orPermissions: [RolePerms.MANAGE_BASE_SETTING],
|
||||
}}>
|
||||
<BaseSettingPage></BaseSettingPage>
|
||||
</WithAuth>
|
||||
),
|
||||
handle: {
|
||||
crumb() {
|
||||
return (
|
||||
<Link to={"/admin/base-setting"}>
|
||||
基本设置
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "department",
|
||||
name: '组织架构',
|
||||
icon: <TeamOutlined />,
|
||||
element: (
|
||||
<WithAuth
|
||||
options={{
|
||||
orPermissions: [RolePerms.MANAGE_ANY_DEPT],
|
||||
}}>
|
||||
<DepartmentAdminPage></DepartmentAdminPage>
|
||||
</WithAuth>
|
||||
),
|
||||
handle: {
|
||||
crumb() {
|
||||
return (
|
||||
<Link to={"/admin/department"}>
|
||||
组织架构
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "staff",
|
||||
name: '用户管理',
|
||||
icon: <UserOutlined />,
|
||||
element: (
|
||||
<WithAuth
|
||||
options={{
|
||||
orPermissions: [
|
||||
RolePerms.MANAGE_ANY_STAFF,
|
||||
RolePerms.MANAGE_DOM_STAFF,
|
||||
],
|
||||
}}>
|
||||
<StaffPage></StaffPage>
|
||||
</WithAuth>
|
||||
),
|
||||
handle: {
|
||||
crumb() {
|
||||
return (
|
||||
<Link to={"/admin/staff"}>用户管理</Link>
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "term",
|
||||
name: '分类配置',
|
||||
icon: <TagsOutlined />,
|
||||
element: (
|
||||
<WithAuth
|
||||
options={{
|
||||
orPermissions: [
|
||||
RolePerms.MANAGE_ANY_TERM,
|
||||
// RolePerms.MANAGE_DOM_TERM
|
||||
],
|
||||
}}>
|
||||
<TermAdminPage></TermAdminPage>
|
||||
</WithAuth>
|
||||
),
|
||||
handle: {
|
||||
crumb() {
|
||||
return <Link to={"/admin/term"}>分类配置</Link>;
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "role",
|
||||
name: '角色管理',
|
||||
icon: <SafetyOutlined />,
|
||||
element: (
|
||||
<WithAuth
|
||||
options={{
|
||||
orPermissions: [
|
||||
RolePerms.MANAGE_ANY_ROLE,
|
||||
RolePerms.MANAGE_DOM_ROLE,
|
||||
],
|
||||
}}>
|
||||
<RoleAdminPage></RoleAdminPage>
|
||||
</WithAuth>
|
||||
),
|
||||
handle: {
|
||||
crumb() {
|
||||
return <Link to={"/admin/role"}>角色管理</Link>;
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
|
@ -18,34 +18,11 @@ 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";
|
||||
import React from "react";
|
||||
import EditorLetterPage from "../app/main/letter/editor/page";
|
||||
import LetterDetailPage from "../app/main/letter/detail/page";
|
||||
|
||||
interface CustomIndexRouteObject extends IndexRouteObject {
|
||||
name?: string;
|
||||
breadcrumb?: string;
|
||||
}
|
||||
interface CustomIndexRouteObject extends IndexRouteObject {
|
||||
name?: string;
|
||||
breadcrumb?: string;
|
||||
}
|
||||
export interface NavItem {
|
||||
icon?: React.ReactNode;
|
||||
label: string;
|
||||
path: string;
|
||||
}
|
||||
export interface CustomNonIndexRouteObject extends NonIndexRouteObject {
|
||||
name?: string;
|
||||
children?: CustomRouteObject[];
|
||||
breadcrumb?: string;
|
||||
handle?: {
|
||||
crumb: (data?: any) => void;
|
||||
};
|
||||
}
|
||||
export type CustomRouteObject =
|
||||
| CustomIndexRouteObject
|
||||
| CustomNonIndexRouteObject;
|
||||
import AdminLayout from "../components/layout/admin/AdminLayout";
|
||||
import { CustomRouteObject } from "./types";
|
||||
import { adminRoute } from "./admin-route";
|
||||
export const routes: CustomRouteObject[] = [
|
||||
{
|
||||
path: "/",
|
||||
|
@ -86,120 +63,12 @@ export const routes: CustomRouteObject[] = [
|
|||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "admin",
|
||||
children: [
|
||||
{
|
||||
path: "base-setting",
|
||||
element: (
|
||||
<WithAuth
|
||||
options={{
|
||||
orPermissions: [
|
||||
RolePerms.MANAGE_BASE_SETTING,
|
||||
],
|
||||
}}>
|
||||
<BaseSettingPage></BaseSettingPage>
|
||||
</WithAuth>
|
||||
),
|
||||
handle: {
|
||||
crumb() {
|
||||
return (
|
||||
<Link to={"/admin/base-setting"}>
|
||||
基本设置
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "department",
|
||||
breadcrumb: "单位管理",
|
||||
element: (
|
||||
<WithAuth
|
||||
options={{
|
||||
orPermissions: [RolePerms.MANAGE_ANY_DEPT],
|
||||
}}>
|
||||
<DepartmentAdminPage></DepartmentAdminPage>
|
||||
</WithAuth>
|
||||
),
|
||||
handle: {
|
||||
crumb() {
|
||||
return (
|
||||
<Link to={"/admin/department"}>
|
||||
组织架构
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "staff",
|
||||
element: (
|
||||
<WithAuth
|
||||
options={{
|
||||
orPermissions: [
|
||||
RolePerms.MANAGE_ANY_STAFF,
|
||||
RolePerms.MANAGE_DOM_STAFF,
|
||||
],
|
||||
}}>
|
||||
<StaffAdminPage></StaffAdminPage>
|
||||
</WithAuth>
|
||||
),
|
||||
handle: {
|
||||
crumb() {
|
||||
return (
|
||||
<Link to={"/admin/staff"}>用户管理</Link>
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "term",
|
||||
breadcrumb: "分类配置",
|
||||
element: (
|
||||
<WithAuth
|
||||
options={{
|
||||
orPermissions: [
|
||||
RolePerms.MANAGE_ANY_TERM,
|
||||
// RolePerms.MANAGE_DOM_TERM
|
||||
],
|
||||
}}>
|
||||
<TermAdminPage></TermAdminPage>
|
||||
</WithAuth>
|
||||
),
|
||||
handle: {
|
||||
crumb() {
|
||||
return <Link to={"/admin/term"}>分类配置</Link>;
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "role",
|
||||
breadcrumb: "角色管理",
|
||||
element: (
|
||||
<WithAuth
|
||||
options={{
|
||||
orPermissions: [
|
||||
RolePerms.MANAGE_ANY_ROLE,
|
||||
RolePerms.MANAGE_DOM_ROLE,
|
||||
],
|
||||
}}>
|
||||
<RoleAdminPage></RoleAdminPage>
|
||||
</WithAuth>
|
||||
),
|
||||
handle: {
|
||||
crumb() {
|
||||
return <Link to={"/admin/role"}>角色管理</Link>;
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
adminRoute
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "/auth",
|
||||
breadcrumb: "登录",
|
||||
|
||||
element: <AuthPage></AuthPage>,
|
||||
},
|
||||
];
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
import { ReactNode } from "react";
|
||||
import { IndexRouteObject, NonIndexRouteObject } from "react-router-dom";
|
||||
|
||||
export interface CustomIndexRouteObject extends IndexRouteObject {
|
||||
name?: string;
|
||||
icon?: ReactNode
|
||||
}
|
||||
export interface CustomNonIndexRouteObject extends NonIndexRouteObject {
|
||||
name?: string;
|
||||
children?: CustomRouteObject[];
|
||||
icon?: ReactNode
|
||||
handle?: {
|
||||
crumb: (data?: any) => void;
|
||||
};
|
||||
}
|
||||
export type CustomRouteObject =
|
||||
| CustomIndexRouteObject
|
||||
| CustomNonIndexRouteObject;
|
|
@ -1,13 +1,2 @@
|
|||
import type { Config } from 'tailwindcss'
|
||||
import { createTailwindTheme, defaultTheme } from '@nice/theme';
|
||||
|
||||
const tailwindTheme = createTailwindTheme(defaultTheme)
|
||||
const config: Config = {
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{js,ts,jsx,tsx}",
|
||||
],
|
||||
theme: tailwindTheme,
|
||||
plugins: [],
|
||||
}
|
||||
export default config
|
||||
import { NiceTailwindConfig } from "@nice/theme"
|
||||
export default NiceTailwindConfig
|
|
@ -1,5 +1,5 @@
|
|||
import { generateBorderRadius, generateBoxShadow, generateSpacing, generateTheme, generateTypography, generateZIndex } from "./generator";
|
||||
import { ThemeConfig, ThemeSeed } from "./types";
|
||||
import { generateTheme} from "./generator";
|
||||
import { ThemeSeed } from "./types";
|
||||
|
||||
// 添加默认的主题配置
|
||||
export const USAFSeed: ThemeSeed = {
|
||||
|
@ -13,13 +13,7 @@ export const USAFSeed: ThemeSeed = {
|
|||
info: '#00538E', // 信息蓝色
|
||||
|
||||
},
|
||||
config: {
|
||||
borderRadius: generateBorderRadius(),
|
||||
spacing: generateSpacing(),
|
||||
...generateTypography(),
|
||||
boxShadow: generateBoxShadow(),
|
||||
zIndex: generateZIndex(),
|
||||
},
|
||||
|
||||
isDark: false
|
||||
};
|
||||
export const defaultTheme = generateTheme(USAFSeed)
|
|
@ -1,5 +1,5 @@
|
|||
import React, { createContext, useContext, useMemo, useState } from 'react';
|
||||
import type { Theme, ThemeConfig, ThemeSeed, ThemeToken } from './types';
|
||||
import type { ThemeSeed, ThemeToken } from './types';
|
||||
import { USAFSeed } from './constants';
|
||||
import { createTailwindTheme, injectThemeVariables } from './styles';
|
||||
import { generateTheme } from './generator';
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Theme, ThemeConfig, ThemeColors, ThemeSemantics, ThemeSeed, ThemeToken } from './types';
|
||||
import { Theme, ThemeColors, ThemeSemantics, ThemeSeed, ThemeToken } from './types';
|
||||
import { withAlpha, generateColorScale } from './colors';
|
||||
import { darkMode } from './utils';
|
||||
export function generateThemeColors(seed: ThemeSeed['colors']): ThemeColors {
|
||||
|
@ -83,129 +83,16 @@ export function generateSemantics(colors: ThemeColors, isDark: boolean): ThemeSe
|
|||
};
|
||||
}
|
||||
|
||||
export function generateSpacing(): ThemeConfig['spacing'] {
|
||||
return {
|
||||
0: '0',
|
||||
xs: '0.25rem',
|
||||
sm: '0.5rem',
|
||||
DEFAULT: '1rem',
|
||||
lg: '1.5rem',
|
||||
xl: '2rem',
|
||||
'2xl': '3rem'
|
||||
};
|
||||
}
|
||||
|
||||
export function generateBorderRadius(): ThemeConfig['borderRadius'] {
|
||||
return {
|
||||
none: '0',
|
||||
xs: '0.125rem',
|
||||
sm: '0.25rem',
|
||||
DEFAULT: '0.375rem',
|
||||
lg: '0.5rem',
|
||||
xl: '0.75rem',
|
||||
full: '9999px'
|
||||
};
|
||||
}
|
||||
|
||||
export function generateTypography(): Pick<ThemeConfig, 'fontFamily' | 'fontSize' | 'lineHeight' | 'letterSpacing' | 'fontWeight'> {
|
||||
return {
|
||||
fontFamily: {
|
||||
sans: ['-apple-system', 'BlinkMacSystemFont', '"Segoe UI"', 'Roboto', '"Helvetica Neue"', 'Arial', 'sans-serif'],
|
||||
serif: ['Georgia', 'Cambria', '"Times New Roman"', 'Times', 'serif'],
|
||||
mono: ['Menlo', 'Monaco', 'Consolas', '"Liberation Mono"', '"Courier New"', 'monospace'],
|
||||
DEFAULT: ['-apple-system', 'BlinkMacSystemFont', '"Segoe UI"', 'Roboto', '"Helvetica Neue"', 'Arial', 'sans-serif']
|
||||
},
|
||||
fontSize: {
|
||||
xs: '0.75rem',
|
||||
sm: '0.875rem',
|
||||
base: '1rem',
|
||||
lg: '1.125rem',
|
||||
xl: '1.25rem',
|
||||
'2xl': '1.5rem',
|
||||
display: '2rem',
|
||||
DEFAULT: '1rem'
|
||||
},
|
||||
lineHeight: {
|
||||
xs: '1rem',
|
||||
sm: '1.25rem',
|
||||
base: '1.5rem',
|
||||
lg: '1.75rem',
|
||||
xl: '1.75rem',
|
||||
'2xl': '2rem',
|
||||
display: '2.5rem',
|
||||
DEFAULT: '1.5rem'
|
||||
},
|
||||
letterSpacing: {
|
||||
xs: '-0.05em',
|
||||
sm: '-0.025em',
|
||||
base: '0',
|
||||
lg: '0.025em',
|
||||
xl: '0.025em',
|
||||
'2xl': '0.025em',
|
||||
display: '0',
|
||||
DEFAULT: '0'
|
||||
},
|
||||
fontWeight: {
|
||||
xs: 400,
|
||||
sm: 400,
|
||||
base: 400,
|
||||
lg: 500,
|
||||
xl: 500,
|
||||
'2xl': 600,
|
||||
display: 600,
|
||||
DEFAULT: 400
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export function generateBoxShadow(): ThemeConfig['boxShadow'] {
|
||||
return {
|
||||
none: 'none',
|
||||
sm: '0 1px 2px 0 rgb(0 0 0 / 0.05)',
|
||||
DEFAULT: '0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)',
|
||||
lg: '0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)',
|
||||
xl: '0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)',
|
||||
inner: 'inset 0 2px 4px 0 rgb(0 0 0 / 0.05)'
|
||||
};
|
||||
}
|
||||
|
||||
export function generateZIndex(): ThemeConfig['zIndex'] {
|
||||
return {
|
||||
negative: '-1',
|
||||
0: '0',
|
||||
10: '10',
|
||||
20: '20',
|
||||
30: '30',
|
||||
40: '40',
|
||||
50: '50',
|
||||
modal: '1000',
|
||||
popover: '1100',
|
||||
tooltip: '1200',
|
||||
DEFAULT: '0'
|
||||
};
|
||||
}
|
||||
|
||||
export function generateThemeConfig(): ThemeConfig {
|
||||
return {
|
||||
borderRadius: generateBorderRadius(),
|
||||
spacing: generateSpacing(),
|
||||
...generateTypography(),
|
||||
boxShadow: generateBoxShadow(),
|
||||
zIndex: generateZIndex()
|
||||
};
|
||||
}
|
||||
|
||||
export function generateTheme(seed: ThemeSeed): Theme {
|
||||
const isDark = seed.isDark ?? false;
|
||||
const colors = generateThemeColors(seed.colors);
|
||||
const semantics = generateSemantics(colors, isDark);
|
||||
const config = generateThemeConfig();
|
||||
|
||||
return {
|
||||
token: {
|
||||
...colors,
|
||||
...semantics,
|
||||
...config
|
||||
...semantics
|
||||
},
|
||||
isDark
|
||||
};
|
||||
|
|
|
@ -2,4 +2,5 @@ export * from "./context"
|
|||
export * from "./types"
|
||||
export * from "./utils"
|
||||
export * from "./styles"
|
||||
export * from "./constants"
|
||||
export * from "./constants"
|
||||
export * from "./tailwind"
|
|
@ -82,18 +82,6 @@ export function createTailwindTheme(theme: Theme): Partial<Config["theme"]> {
|
|||
textColor: themeConfig.textColor,
|
||||
backgroundColor: themeConfig.backgroundColor,
|
||||
borderColor: themeConfig.border,
|
||||
borderRadius: themeConfig.borderRadius,
|
||||
spacing: themeConfig.spacing,
|
||||
fontFamily: Object.entries(themeConfig.fontFamily || {}).reduce((acc, [key, value]) => ({
|
||||
...acc,
|
||||
[key]: (value as string).split(',')
|
||||
}), {}),
|
||||
fontSize: themeConfig.fontSize,
|
||||
lineHeight: themeConfig.lineHeight,
|
||||
letterSpacing: themeConfig.letterSpacing,
|
||||
fontWeight: themeConfig.fontWeight,
|
||||
boxShadow: themeConfig.boxShadow,
|
||||
zIndex: themeConfig.zIndex
|
||||
}
|
||||
}
|
||||
console.log(result)
|
||||
|
|
|
@ -0,0 +1,106 @@
|
|||
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: [
|
||||
|
||||
],
|
||||
}
|
|
@ -91,103 +91,8 @@ export type ThemeSemantics = {
|
|||
};
|
||||
}
|
||||
|
||||
export interface ThemeConfig {
|
||||
/** 圆角配置 */
|
||||
borderRadius: {
|
||||
none: string;
|
||||
xs: string;
|
||||
sm: string;
|
||||
DEFAULT: string;
|
||||
lg: string;
|
||||
xl: string;
|
||||
full: string;
|
||||
};
|
||||
/** 间距配置 */
|
||||
spacing: {
|
||||
0: string;
|
||||
xs: string;
|
||||
sm: string;
|
||||
DEFAULT: string;
|
||||
lg: string;
|
||||
xl: string;
|
||||
'2xl': string;
|
||||
};
|
||||
/** 字体族配置 */
|
||||
fontFamily: {
|
||||
sans: string[];
|
||||
serif: string[];
|
||||
mono: string[];
|
||||
DEFAULT: string[];
|
||||
};
|
||||
/** 字体大小配置 */
|
||||
fontSize: {
|
||||
xs: string;
|
||||
sm: string;
|
||||
base: string;
|
||||
lg: string;
|
||||
xl: string;
|
||||
'2xl': string;
|
||||
display: string;
|
||||
DEFAULT: string;
|
||||
};
|
||||
/** 行高配置 */
|
||||
lineHeight: {
|
||||
xs: string;
|
||||
sm: string;
|
||||
base: string;
|
||||
lg: string;
|
||||
xl: string;
|
||||
'2xl': string;
|
||||
display: string;
|
||||
DEFAULT: string;
|
||||
};
|
||||
/** 字母间距配置 */
|
||||
letterSpacing: {
|
||||
xs?: string;
|
||||
sm?: string;
|
||||
base?: string;
|
||||
lg?: string;
|
||||
xl?: string;
|
||||
'2xl'?: string;
|
||||
display?: string;
|
||||
DEFAULT: string;
|
||||
};
|
||||
/** 字重配置 */
|
||||
fontWeight: {
|
||||
xs?: string | number;
|
||||
sm?: string | number;
|
||||
base?: string | number;
|
||||
lg?: string | number;
|
||||
xl?: string | number;
|
||||
'2xl'?: string | number;
|
||||
display?: string | number;
|
||||
DEFAULT?: string | number;
|
||||
};
|
||||
/** 阴影配置 */
|
||||
boxShadow: {
|
||||
none: string;
|
||||
sm: string;
|
||||
DEFAULT: string;
|
||||
lg: string;
|
||||
xl: string;
|
||||
inner: string;
|
||||
};
|
||||
/** Z轴层级配置 */
|
||||
zIndex: {
|
||||
negative: string;
|
||||
0: string;
|
||||
10: string;
|
||||
20: string;
|
||||
30: string;
|
||||
40: string;
|
||||
50: string;
|
||||
modal: string;
|
||||
popover: string;
|
||||
tooltip: string;
|
||||
DEFAULT: string;
|
||||
};
|
||||
}
|
||||
export type ThemeToken = ThemeSemantics & ThemeConfig
|
||||
|
||||
export type ThemeToken = ThemeSemantics
|
||||
export interface Theme {
|
||||
token: ThemeToken;
|
||||
isDark: boolean;
|
||||
|
@ -202,6 +107,6 @@ export interface ThemeSeed {
|
|||
error?: string;
|
||||
info?: string;
|
||||
}
|
||||
config: ThemeConfig
|
||||
|
||||
isDark?: boolean;
|
||||
}
|
||||
|
|
|
@ -2,14 +2,12 @@
|
|||
export function darkMode<T>(isDark: boolean, darkValue: T, lightValue: T): T {
|
||||
return isDark ? darkValue : lightValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将驼峰命名转换为kebab-case
|
||||
*/
|
||||
export function toKebabCase(str: string): string {
|
||||
return str.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase()
|
||||
}
|
||||
|
||||
/**
|
||||
* 将嵌套对象扁平化,使用点号连接键名
|
||||
*/
|
||||
|
|
Loading…
Reference in New Issue