This commit is contained in:
longdayi 2025-01-24 17:37:51 +08:00
parent d28793a15d
commit c1268b1a8f
59 changed files with 612 additions and 1258 deletions

View File

@ -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);
}),
});
}

View File

@ -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 {

View File

@ -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: {},
}}>

View File

@ -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)" }}>

View File

@ -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>
}

View File

@ -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;

View File

@ -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>
</>

View File

@ -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>
);

View File

@ -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>
</>
);

View File

@ -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 }}

View File

@ -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

View File

@ -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">

View File

@ -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>

View File

@ -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"
>

View File

@ -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>
);
}

View File

@ -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>
}

View File

@ -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>

View File

@ -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;

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);

View File

@ -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))

View File

@ -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

View File

@ -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 />

View File

@ -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>
);

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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 />

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>
);
};

View File

@ -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={{

View File

@ -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];

View File

@ -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">

View File

@ -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" />
) : (

View File

@ -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`}

View File

@ -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>
)

View File

@ -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" />
) : (

View File

@ -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;
}
}
}
}

View File

@ -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,

View File

@ -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>;
},
},
},
]
}

View File

@ -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>,
},
];

View File

@ -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;

View File

@ -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

View File

@ -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)

View File

@ -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';

View File

@ -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
};

View File

@ -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"

View File

@ -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)

View File

@ -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: [
],
}

View File

@ -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;
}

View File

@ -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()
}
/**
* 使
*/