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 trpc: TrpcService,
|
||||||
private readonly staffService: StaffService,
|
private readonly staffService: StaffService,
|
||||||
private readonly staffRowService: StaffRowService,
|
private readonly staffRowService: StaffRowService,
|
||||||
) {}
|
) { }
|
||||||
|
|
||||||
router = this.trpc.router({
|
router = this.trpc.router({
|
||||||
create: this.trpc.procedure
|
create: this.trpc.procedure
|
||||||
|
@ -77,5 +77,15 @@ export class StaffRouter {
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
return this.staffService.updateOrder(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-spacing: 6px;
|
||||||
--ag-odd-row-background-color: var(--color-fill-quaternary);
|
--ag-odd-row-background-color: var(--color-fill-quaternary);
|
||||||
--ag-wrapper-border-width: 0px;
|
--ag-wrapper-border-width: 0px;
|
||||||
/* --ag-wrapper-border-color: var(--color-border-secondary); */
|
|
||||||
/* --ag-wrapper-border-radius: 10px; */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.ag-root-wrapper {
|
.ag-root-wrapper {
|
||||||
|
|
|
@ -10,7 +10,7 @@ import dayjs from "dayjs";
|
||||||
import "dayjs/locale/zh-cn";
|
import "dayjs/locale/zh-cn";
|
||||||
import { AuthProvider } from './providers/auth-provider';
|
import { AuthProvider } from './providers/auth-provider';
|
||||||
import { Toaster } from 'react-hot-toast';
|
import { Toaster } from 'react-hot-toast';
|
||||||
import {ThemeProvider} from "@nice/theme"
|
import ThemeProvider from './providers/theme-provider';
|
||||||
dayjs.locale("zh-cn");
|
dayjs.locale("zh-cn");
|
||||||
function App() {
|
function App() {
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@ function App() {
|
||||||
theme={{
|
theme={{
|
||||||
algorithm: theme.defaultAlgorithm,
|
algorithm: theme.defaultAlgorithm,
|
||||||
token: {
|
token: {
|
||||||
colorPrimary: "#2e75b6",
|
colorPrimary: "#00308a",
|
||||||
},
|
},
|
||||||
components: {},
|
components: {},
|
||||||
}}>
|
}}>
|
||||||
|
|
|
@ -13,11 +13,10 @@ import {
|
||||||
} from "antd";
|
} from "antd";
|
||||||
import { useAppConfig } from "@nice/client";
|
import { useAppConfig } from "@nice/client";
|
||||||
import { useAuth } from "@web/src/providers/auth-provider";
|
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 { useForm } from "antd/es/form/Form";
|
||||||
import { api } from "@nice/client"
|
import { api } from "@nice/client"
|
||||||
import { MainLayoutContext } from "../layout";
|
import AdminHeader from "@web/src/components/layout/admin/AdminHeader";
|
||||||
|
|
||||||
|
|
||||||
export default function BaseSettingPage() {
|
export default function BaseSettingPage() {
|
||||||
const { update, baseSetting } = useAppConfig();
|
const { update, baseSetting } = useAppConfig();
|
||||||
|
@ -31,7 +30,6 @@ export default function BaseSettingPage() {
|
||||||
const [isFormChanged, setIsFormChanged] = useState(false);
|
const [isFormChanged, setIsFormChanged] = useState(false);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const { user, hasSomePermissions } = useAuth();
|
const { user, hasSomePermissions } = useAuth();
|
||||||
const { pageWidth } = useContext?.(MainLayoutContext);
|
|
||||||
function handleFieldsChange() {
|
function handleFieldsChange() {
|
||||||
setIsFormChanged(true);
|
setIsFormChanged(true);
|
||||||
}
|
}
|
||||||
|
@ -77,8 +75,8 @@ export default function BaseSettingPage() {
|
||||||
}
|
}
|
||||||
}, [baseSetting, form]);
|
}, [baseSetting, form]);
|
||||||
return (
|
return (
|
||||||
<div style={{ width: pageWidth }}>
|
<div >
|
||||||
<FixedHeader>
|
<AdminHeader>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{isFormChanged &&
|
{isFormChanged &&
|
||||||
hasSomePermissions(RolePerms.MANAGE_BASE_SETTING) && (
|
hasSomePermissions(RolePerms.MANAGE_BASE_SETTING) && (
|
||||||
|
@ -93,7 +91,7 @@ export default function BaseSettingPage() {
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</FixedHeader>
|
</AdminHeader>
|
||||||
<div
|
<div
|
||||||
className="flex flex-col overflow-auto "
|
className="flex flex-col overflow-auto "
|
||||||
style={{ height: "calc(100vh - 48px - 49px)" }}>
|
style={{ height: "calc(100vh - 48px - 49px)" }}>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import DeptEditor from "@web/src/components/models/department/dept-editor";
|
import DeptEditor from "@web/src/components/models/department/dept-editor";
|
||||||
|
|
||||||
export default function DepartmentAdminPage() {
|
export default function DepartmentAdminPage() {
|
||||||
return <div className=" flex-grow bg-white rounded-xl">
|
return <div className=" flex-grow bg-white">
|
||||||
<DeptEditor></DeptEditor>
|
<DeptEditor></DeptEditor>
|
||||||
</div>
|
</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";
|
import RoleEditor from "@web/src/components/models/role/role-editor/role-editor";
|
||||||
|
|
||||||
export default function RoleAdminPage() {
|
export default function RoleAdminPage() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<FixedHeader roomId="role-editor">
|
<AdminHeader roomId="role-editor">
|
||||||
</FixedHeader>
|
</AdminHeader>
|
||||||
<RoleEditor></RoleEditor>
|
<RoleEditor></RoleEditor>
|
||||||
</>
|
</>
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import StaffEditor from "@web/src/components/models/staff/staff-editor"
|
import StaffEditor from "@web/src/components/models/staff/staff-editor"
|
||||||
export default function StaffPage() {
|
export default function StaffPage() {
|
||||||
return (
|
return (
|
||||||
<div className=" bg-white rounded-xl flex-grow">
|
<div className=" bg-white flex-grow">
|
||||||
<StaffEditor></StaffEditor>
|
<StaffEditor></StaffEditor>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
|
import AdminHeader from "@web/src/components/layout/admin/AdminHeader";
|
||||||
import FixedHeader from "@web/src/components/layout/fix-header";
|
|
||||||
import TermEditor from "@web/src/components/models/term/term-editor";
|
import TermEditor from "@web/src/components/models/term/term-editor";
|
||||||
|
|
||||||
export default function TermAdminPage() {
|
export default function TermAdminPage() {
|
||||||
return (<>
|
return (<>
|
||||||
<FixedHeader></FixedHeader>
|
<AdminHeader></AdminHeader>
|
||||||
<TermEditor></TermEditor>
|
<TermEditor></TermEditor>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -47,7 +47,7 @@ const AuthPage: React.FC = () => {
|
||||||
>
|
>
|
||||||
<div className="flex flex-col md:flex-row min-h-[650px]">
|
<div className="flex flex-col md:flex-row min-h-[650px]">
|
||||||
{/* Left Panel - Welcome Section */}
|
{/* 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
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
|
|
@ -69,16 +69,16 @@ export const RegisterForm = ({ onSubmit, isLoading }: RegisterFormProps) => {
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="officerId"
|
name="officerId"
|
||||||
label="工号"
|
label="证件号"
|
||||||
rules={[
|
rules={[
|
||||||
{ required: true, message: "请输入工号" },
|
{ required: true, message: "请输入证件号" },
|
||||||
{
|
{
|
||||||
pattern: /^\d{5,12}$/,
|
pattern: /^\d{5,12}$/,
|
||||||
message: "请输入有效的工号(5-12位数字)"
|
message: "请输入有效的证件号(5-12位数字)"
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Input placeholder="工号" />
|
<Input placeholder="证件号" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
|
|
|
@ -12,7 +12,7 @@ export function Header() {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
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>
|
<h1 className="text-3xl font-bold text-white">公开信件列表</h1>
|
||||||
<div className="mt-4 text-blue-50">
|
<div className="mt-4 text-blue-50">
|
||||||
<p className="text-base opacity-90">
|
<p className="text-base opacity-90">
|
||||||
|
|
|
@ -37,7 +37,7 @@ export function LetterCard({ letter }: LetterCardProps) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<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">
|
<div className="flex flex-col gap-3">
|
||||||
{/* Title & Priority */}
|
{/* Title & Priority */}
|
||||||
|
@ -46,10 +46,10 @@ export function LetterCard({ letter }: LetterCardProps) {
|
||||||
<a
|
<a
|
||||||
href={`/letters/${letter.id}`}
|
href={`/letters/${letter.id}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="text-navy-900 transition-all duration-300 relative
|
className="text-primary transition-all duration-300 relative
|
||||||
before:absolute before:bottom-0 before:left-0 before:w-0 before:h-[2px] before:bg-blue-600
|
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: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}
|
{letter.title}
|
||||||
</a>
|
</a>
|
||||||
|
@ -60,20 +60,20 @@ export function LetterCard({ letter }: LetterCardProps) {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Meta Info */}
|
{/* 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 size="middle">
|
||||||
<Space>
|
<Space>
|
||||||
<UserOutlined className="text-gray-400" />
|
<UserOutlined className="text-secondary-400" />
|
||||||
<Text strong>{letter.sender}</Text>
|
<Text strong>{letter.sender}</Text>
|
||||||
</Space>
|
</Space>
|
||||||
<Text type="secondary">|</Text>
|
<Text type="secondary">|</Text>
|
||||||
<Space>
|
<Space>
|
||||||
<BankOutlined className="text-gray-400" />
|
<BankOutlined className="text-secondary-400" />
|
||||||
<Text>{letter.unit}</Text>
|
<Text>{letter.unit}</Text>
|
||||||
</Space>
|
</Space>
|
||||||
</Space>
|
</Space>
|
||||||
<Space>
|
<Space>
|
||||||
<CalendarOutlined className="text-gray-400" />
|
<CalendarOutlined className="text-secondary-400" />
|
||||||
<Text type="secondary">{letter.date}</Text>
|
<Text type="secondary">{letter.date}</Text>
|
||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -41,7 +41,7 @@ export default function LeaderCard({ leader, isSelected, onSelect }: LeaderCardP
|
||||||
<h3 className="text-xl font-semibold text-gray-900">
|
<h3 className="text-xl font-semibold text-gray-900">
|
||||||
{leader.name}
|
{leader.name}
|
||||||
</h3>
|
</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}
|
{leader.rank}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -76,8 +76,8 @@ export default function LeaderCard({ leader, isSelected, onSelect }: LeaderCardP
|
||||||
<button
|
<button
|
||||||
onClick={onSelect}
|
onClick={onSelect}
|
||||||
className="mt-auto w-full sm:w-auto flex items-center justify-center gap-2
|
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
|
bg-primary text-white py-3 px-6 rounded-lg
|
||||||
hover:bg-[#002070] transition-all duration-300
|
hover:bg-primary-600 transition-all duration-300
|
||||||
focus:outline-none focus:ring-2 focus:ring-[#00308F] focus:ring-opacity-50
|
focus:outline-none focus:ring-2 focus:ring-[#00308F] focus:ring-opacity-50
|
||||||
transform hover:-translate-y-0.5"
|
transform hover:-translate-y-0.5"
|
||||||
>
|
>
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
import { FunnelIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline";
|
import { FunnelIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline";
|
||||||
import { leaders } from "./mock";
|
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { Leader } from "./types";
|
|
||||||
import { Input, Select } from "antd";
|
import { Input, Select } from "antd";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
|
import DepartmentSelect from "@web/src/components/models/department/department-select";
|
||||||
|
|
||||||
const { Search } = Input;
|
const { Search } = Input;
|
||||||
|
|
||||||
|
@ -16,9 +15,6 @@ export default function Filter({ onSearch, onDivisionChange }: FilterProps) {
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [selectedDivision, setSelectedDivision] = useState<string>('all');
|
const [selectedDivision, setSelectedDivision] = useState<string>('all');
|
||||||
|
|
||||||
const divisions = useMemo(() => {
|
|
||||||
return ['all', ...new Set(leaders.map(leader => leader.division))];
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleSearch = (value: string) => {
|
const handleSearch = (value: string) => {
|
||||||
setSearchQuery(value);
|
setSearchQuery(value);
|
||||||
|
@ -54,17 +50,7 @@ export default function Filter({ onSearch, onDivisionChange }: FilterProps) {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Select
|
<DepartmentSelect ></DepartmentSelect>
|
||||||
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,
|
|
||||||
}))}
|
|
||||||
/>
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,70 +1,70 @@
|
||||||
export default function Header() {
|
export default function Header() {
|
||||||
return <header className="bg-gradient-to-r from-primary to-primary-50 text-white p-6">
|
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 className="flex flex-col space-y-6">
|
||||||
{/* 主标题 */}
|
{/* 主标题 */}
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold tracking-wider">
|
<h1 className="text-3xl font-bold tracking-wider">
|
||||||
信件投递入口
|
信件投递入口
|
||||||
</h1>
|
</h1>
|
||||||
<p className="mt-2 text-blue-100 text-lg">
|
<p className="mt-2 text-blue-100 text-lg">
|
||||||
保护您隐私的信件传输平台
|
保护您隐私的信件传输平台
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 隐私保护说明 */}
|
{/* 隐私保护说明 */}
|
||||||
<div className="flex flex-wrap gap-6 text-sm">
|
<div className="flex flex-wrap gap-6 text-sm">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<svg
|
<svg
|
||||||
className="w-5 h-5"
|
className="w-5 h-5"
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
viewBox="0 0 24 24">
|
viewBox="0 0 24 24">
|
||||||
<path
|
<path
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
strokeWidth="2"
|
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"
|
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>
|
</svg>
|
||||||
<span>个人信息严格保密</span>
|
<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>
|
||||||
|
<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">
|
<div className="text-sm text-blue-100 border-t border-blue-400/30 pt-4">
|
||||||
<p>您的个人信息将被严格保密,不向任何第三方透露。您可以选择匿名反映问题,平台会自动过滤可能暴露身份的信息。</p>
|
<p>您的个人信息将被严格保密,不向任何第三方透露。您可以选择匿名反映问题,平台会自动过滤可能暴露身份的信息。</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,18 +5,16 @@ import { leaders } from './mock';
|
||||||
import Header from './header';
|
import Header from './header';
|
||||||
import Filter from './filter';
|
import Filter from './filter';
|
||||||
import LeaderCard from './LeaderCard';
|
import LeaderCard from './LeaderCard';
|
||||||
|
import { Spin, Empty } from 'antd';
|
||||||
|
import { api } from 'packages/client/dist';
|
||||||
|
|
||||||
|
|
||||||
export default function WriteLetterPage() {
|
export default function WriteLetterPage() {
|
||||||
|
|
||||||
const [selectedLeader, setSelectedLeader] = useState<Leader | null>(null);
|
const [selectedLeader, setSelectedLeader] = useState<Leader | null>(null);
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [selectedDivision, setSelectedDivision] = useState<string>('all');
|
const [selectedDivision, setSelectedDivision] = useState<string>('all');
|
||||||
|
|
||||||
const divisions = useMemo(() => {
|
|
||||||
return ['all', ...new Set(leaders.map(leader => leader.division))];
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const filteredLeaders = useMemo(() => {
|
const filteredLeaders = useMemo(() => {
|
||||||
return leaders.filter(leader => {
|
return leaders.filter(leader => {
|
||||||
const matchesSearch = leader.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
const matchesSearch = leader.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
|
@ -57,24 +55,7 @@ export default function WriteLetterPage() {
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
exit={{ opacity: 0 }}
|
exit={{ opacity: 0 }}
|
||||||
>
|
>
|
||||||
<div className="inline-flex flex-col items-center gap-4">
|
<Empty></Empty>
|
||||||
<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>
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
|
|
@ -2,12 +2,13 @@ import { useAuth } from "@web/src/providers/auth-provider";
|
||||||
import { Avatar, Tag, theme, Tooltip } from "antd";
|
import { Avatar, Tag, theme, Tooltip } from "antd";
|
||||||
import React, { ReactNode, useEffect, useState, useRef, CSSProperties } from "react";
|
import React, { ReactNode, useEffect, useState, useRef, CSSProperties } from "react";
|
||||||
import { SyncOutlined } from "@ant-design/icons";
|
import { SyncOutlined } from "@ant-design/icons";
|
||||||
import Breadcrumb from "../layout/breadcrumb";
|
|
||||||
import * as Y from "yjs";
|
import * as Y from "yjs";
|
||||||
import { stringToColor, YWsProvider } from "@nice/common";
|
import { stringToColor, YWsProvider } from "@nice/common";
|
||||||
import { lightenColor } from "@nice/client"
|
import { lightenColor } from "@nice/client"
|
||||||
import { useLocalSettings } from "@web/src/hooks/useLocalSetting";
|
import { useLocalSettings } from "@web/src/hooks/useLocalSetting";
|
||||||
interface FixedHeaderProps {
|
import Breadcrumb from "../element/breadcrumb";
|
||||||
|
|
||||||
|
interface AdminHeaderProps {
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
roomId?: string;
|
roomId?: string;
|
||||||
awarePlaceholder?: string;
|
awarePlaceholder?: string;
|
||||||
|
@ -16,7 +17,7 @@ interface FixedHeaderProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FixedHeader: React.FC<FixedHeaderProps> = ({
|
const AdminHeader: React.FC<AdminHeaderProps> = ({
|
||||||
className,
|
className,
|
||||||
style,
|
style,
|
||||||
borderless = false,
|
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() {
|
export default function Breadcrumb() {
|
||||||
let matches = useMatches();
|
let matches = useMatches();
|
||||||
const { token } = theme.useToken()
|
const { token } = theme.useToken()
|
||||||
|
|
||||||
let crumbs = matches
|
let crumbs = matches
|
||||||
// first get rid of any matches that don't have handle and crumb
|
// first get rid of any matches that don't have handle and crumb
|
||||||
.filter((match) => Boolean((match.handle as any)?.crumb))
|
.filter((match) => Boolean((match.handle as any)?.crumb))
|
|
@ -9,33 +9,9 @@ import {
|
||||||
QuestionCircleOutlined,
|
QuestionCircleOutlined,
|
||||||
LogoutOutlined
|
LogoutOutlined
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import type { MenuItemType } from "./types";
|
|
||||||
import { Spin } from "antd";
|
import { Spin } from "antd";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
// USAF Theme Constants
|
import { MenuItemType } from "./types";
|
||||||
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;
|
|
||||||
|
|
||||||
const menuVariants = {
|
const menuVariants = {
|
||||||
hidden: { opacity: 0, scale: 0.95, y: -10 },
|
hidden: { opacity: 0, scale: 0.95, y: -10 },
|
||||||
visible: {
|
visible: {
|
||||||
|
@ -62,7 +38,7 @@ export function UserMenu() {
|
||||||
const [showMenu, setShowMenu] = useState(false);
|
const [showMenu, setShowMenu] = useState(false);
|
||||||
const menuRef = useRef<HTMLDivElement>(null);
|
const menuRef = useRef<HTMLDivElement>(null);
|
||||||
const { user, logout, isLoading } = useAuth();
|
const { user, logout, isLoading } = useAuth();
|
||||||
|
const navigate = useNavigate()
|
||||||
useClickOutside(menuRef, () => setShowMenu(false));
|
useClickOutside(menuRef, () => setShowMenu(false));
|
||||||
|
|
||||||
const toggleMenu = useCallback(() => {
|
const toggleMenu = useCallback(() => {
|
||||||
|
@ -78,7 +54,9 @@ export function UserMenu() {
|
||||||
{
|
{
|
||||||
icon: <SettingOutlined className="text-lg" />,
|
icon: <SettingOutlined className="text-lg" />,
|
||||||
label: '设置',
|
label: '设置',
|
||||||
action: () => { },
|
action: () => {
|
||||||
|
navigate('/admin/staff')
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <QuestionCircleOutlined className="text-lg" />,
|
icon: <QuestionCircleOutlined className="text-lg" />,
|
||||||
|
@ -156,7 +134,7 @@ export function UserMenu() {
|
||||||
<div
|
<div
|
||||||
className="px-4 py-4 bg-gradient-to-b from-[#F6F9FC] to-white
|
className="px-4 py-4 bg-gradient-to-b from-[#F6F9FC] to-white
|
||||||
border-b border-[#E5EDF5] "
|
border-b border-[#E5EDF5] "
|
||||||
|
|
||||||
>
|
>
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
<Avatar
|
<Avatar
|
|
@ -1,11 +1,11 @@
|
||||||
import { MagnifyingGlassIcon, UserIcon } from "@heroicons/react/24/outline";
|
|
||||||
import { Link, NavLink } from "react-router-dom";
|
import { Link, NavLink } from "react-router-dom";
|
||||||
import { memo } from "react";
|
import { memo } from "react";
|
||||||
import { SearchBar } from "./SearchBar";
|
import { SearchBar } from "./SearchBar";
|
||||||
import { Logo } from "./Logo";
|
|
||||||
import Navigation from "./navigation";
|
import Navigation from "./navigation";
|
||||||
import { UserMenu} from "./usermenu";
|
|
||||||
import { useAuth } from "@web/src/providers/auth-provider";
|
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 {
|
interface HeaderProps {
|
||||||
onSearch?: (query: string) => void;
|
onSearch?: (query: string) => void;
|
||||||
}
|
}
|
||||||
|
@ -35,9 +35,10 @@ focus:ring-[#8EADD4] focus:ring-offset-2
|
||||||
focus:ring-offset-[#13294B]"
|
focus:ring-offset-[#13294B]"
|
||||||
aria-label="Login"
|
aria-label="Login"
|
||||||
>
|
>
|
||||||
<UserIcon className="h-5 w-5 transition-transform
|
<UserOutlined className="h-5 w-5 transition-transform
|
||||||
group-hover:scale-110 group-hover:rotate-12" />
|
group-hover:scale-110 group-hover:rotate-12"></UserOutlined>
|
||||||
<span>Login</span>
|
|
||||||
|
<span>登录</span>
|
||||||
</Link> : <UserMenu />
|
</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 DepartmentList from "./department-list";
|
||||||
import DeptModal from "./dept-modal";
|
import DeptModal from "./dept-modal";
|
||||||
import DeptImportModal from "./dept-import-modal";
|
import DeptImportModal from "./dept-import-modal";
|
||||||
import FixedHeader from "../../layout/fix-header";
|
import AdminHeader from "../../layout/admin/AdminHeader";
|
||||||
export const DeptEditorContext = createContext<{
|
export const DeptEditorContext = createContext<{
|
||||||
parentId: string;
|
parentId: string;
|
||||||
domainId: string;
|
domainId: string;
|
||||||
|
@ -58,7 +58,7 @@ export default function DeptEditor() {
|
||||||
setImportModalOpen,
|
setImportModalOpen,
|
||||||
importModalOpen,
|
importModalOpen,
|
||||||
}}>
|
}}>
|
||||||
<FixedHeader roomId="dept-editor">
|
<AdminHeader roomId="dept-editor">
|
||||||
<div className=" flex items-center gap-4 ">
|
<div className=" flex items-center gap-4 ">
|
||||||
{canManageDept && (
|
{canManageDept && (
|
||||||
<>
|
<>
|
||||||
|
@ -81,7 +81,7 @@ export default function DeptEditor() {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</FixedHeader>
|
</AdminHeader>
|
||||||
<DepartmentList></DepartmentList>
|
<DepartmentList></DepartmentList>
|
||||||
<DeptModal />
|
<DeptModal />
|
||||||
<DeptImportModal />
|
<DeptImportModal />
|
||||||
|
|
|
@ -71,9 +71,9 @@ export default function RoleList() {
|
||||||
setRole(item as any)
|
setRole(item as any)
|
||||||
}}
|
}}
|
||||||
style={{
|
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}>
|
key={item.id}>
|
||||||
<div className=" flex items-center gap-2">
|
<div className=" flex items-center gap-2">
|
||||||
<span className="text-primary"> <UserOutlined></UserOutlined></span>
|
<span className="text-primary"> <UserOutlined></UserOutlined></span>
|
||||||
|
|
|
@ -7,7 +7,8 @@ import { useAuth } from "@web/src/providers/auth-provider";
|
||||||
import { Button } from "antd";
|
import { Button } from "antd";
|
||||||
import DepartmentSelect from "../department/department-select";
|
import DepartmentSelect from "../department/department-select";
|
||||||
import { FormInstance, useForm } from "antd/es/form/Form";
|
import { FormInstance, useForm } from "antd/es/form/Form";
|
||||||
import FixedHeader from "../../layout/fix-header";
|
import AdminHeader from "../../layout/admin/AdminHeader";
|
||||||
|
|
||||||
export const StaffEditorContext = createContext<{
|
export const StaffEditorContext = createContext<{
|
||||||
domainId: string,
|
domainId: string,
|
||||||
modalOpen: boolean,
|
modalOpen: boolean,
|
||||||
|
@ -51,7 +52,7 @@ export default function StaffEditor() {
|
||||||
return hasSomePermissions(RolePerms.MANAGE_ANY_STAFF)
|
return hasSomePermissions(RolePerms.MANAGE_ANY_STAFF)
|
||||||
}, [user])
|
}, [user])
|
||||||
return <StaffEditorContext.Provider value={{ canManageAnyStaff, formLoading, setFormLoading, form, editId, setEditId, domainId, modalOpen, setDomainId, setModalOpen }}>
|
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">
|
<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>
|
<DepartmentSelect rootId={user?.domainId} onChange={(value) => setDomainId(value as string)} disabled={!canManageAnyStaff} value={domainId} className="w-48" domain={true}></DepartmentSelect>
|
||||||
{canManageStaff && <Button
|
{canManageStaff && <Button
|
||||||
|
@ -64,7 +65,7 @@ export default function StaffEditor() {
|
||||||
添加用户
|
添加用户
|
||||||
</Button>}
|
</Button>}
|
||||||
</div>
|
</div>
|
||||||
</FixedHeader>
|
</AdminHeader>
|
||||||
<StaffList domainId={domainId}></StaffList>
|
<StaffList domainId={domainId}></StaffList>
|
||||||
<StaffModal></StaffModal>
|
<StaffModal></StaffModal>
|
||||||
</StaffEditorContext.Provider>
|
</StaffEditorContext.Provider>
|
||||||
|
|
|
@ -25,12 +25,12 @@ const TaxonomyList: React.FC = () => {
|
||||||
{taxonomies?.map((item) => (
|
{taxonomies?.map((item) => (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
background: item.id === taxonomyId ? token.colorPrimaryBg : ""
|
background: item.id === taxonomyId ? token.colorBgTextHover : ""
|
||||||
}}
|
}}
|
||||||
key={item.id} onClick={() => {
|
key={item.id} onClick={() => {
|
||||||
setTaxonomyId(item.id)
|
setTaxonomyId(item.id)
|
||||||
setTaxonomyName(item?.name)
|
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=''>
|
<div className=''>
|
||||||
<span>{item.name}</span>
|
<span>{item.name}</span>
|
||||||
</div>
|
</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">
|
<div style={{ ...gridStyle }} className="ag-theme-alpine">
|
||||||
<AgGridReact
|
<AgGridReact
|
||||||
|
|
||||||
serverSideInitialRowCount={initialRowCount}
|
serverSideInitialRowCount={initialRowCount}
|
||||||
localeText={AG_GRID_LOCALE_CH}
|
localeText={AG_GRID_LOCALE_CH}
|
||||||
defaultColDef={{
|
defaultColDef={{
|
||||||
|
|
|
@ -9,6 +9,7 @@ interface CollapsibleSectionProps {
|
||||||
items: Array<MenuItem>;
|
items: Array<MenuItem>;
|
||||||
className?: string;
|
className?: string;
|
||||||
defaultExpandedKeys?: string[];
|
defaultExpandedKeys?: string[];
|
||||||
|
renderItem?: (item: MenuItem, isActive: boolean, level: number) => React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MenuItem {
|
interface MenuItem {
|
||||||
|
@ -25,6 +26,7 @@ const CollapsibleSection: React.FC<CollapsibleSectionProps> = ({
|
||||||
items,
|
items,
|
||||||
className,
|
className,
|
||||||
defaultExpandedKeys = [],
|
defaultExpandedKeys = [],
|
||||||
|
renderItem,
|
||||||
}) => {
|
}) => {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
@ -59,12 +61,17 @@ const CollapsibleSection: React.FC<CollapsibleSectionProps> = ({
|
||||||
const itemUrl = new URL(item.link, window.location.origin);
|
const itemUrl = new URL(item.link, window.location.origin);
|
||||||
const itemPath = itemUrl.pathname;
|
const itemPath = itemUrl.pathname;
|
||||||
const itemSearchParams = new URLSearchParams(itemUrl.search);
|
const itemSearchParams = new URLSearchParams(itemUrl.search);
|
||||||
|
|
||||||
|
|
||||||
const hasChildren = item.children && item.children.length > 0;
|
const hasChildren = item.children && item.children.length > 0;
|
||||||
const isActive =
|
const isActive =
|
||||||
currentPath === itemPath &&
|
currentPath === itemPath &&
|
||||||
Array.from(itemSearchParams.entries()).every(
|
Array.from(itemSearchParams.entries()).every(
|
||||||
([key, value]) => currentSearchParams.get(key) === value
|
([key, value]) => currentSearchParams.get(key) === value
|
||||||
);
|
);
|
||||||
|
if (renderItem) {
|
||||||
|
return renderItem(item, isActive, level);
|
||||||
|
}
|
||||||
|
|
||||||
const isChildCollapsed = !expandedSections[item.key];
|
const isChildCollapsed = !expandedSections[item.key];
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ export default function Brightness() {
|
||||||
<>
|
<>
|
||||||
{/* 亮度控制 */}
|
{/* 亮度控制 */}
|
||||||
<div className="relative group flex items-center">
|
<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" />
|
<SunIcon className="w-10 h-10" />
|
||||||
</button>
|
</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">
|
<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.play()
|
||||||
: videoRef.current?.pause()
|
: videoRef.current?.pause()
|
||||||
}
|
}
|
||||||
className="text-white hover:text-primaryHover">
|
className="text-white hover:text-primary-400">
|
||||||
{isPlaying ? (
|
{isPlaying ? (
|
||||||
<PauseIcon className="w-10 h-10" />
|
<PauseIcon className="w-10 h-10" />
|
||||||
) : (
|
) : (
|
||||||
|
|
|
@ -17,7 +17,7 @@ export default function Setting() {
|
||||||
<div className="relative flex items-center">
|
<div className="relative flex items-center">
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsSettingsOpen(!isSettingsOpen)}
|
onClick={() => setIsSettingsOpen(!isSettingsOpen)}
|
||||||
className="text-white hover:text-primaryHover">
|
className="text-white hover:text-primary-400">
|
||||||
<Cog6ToothIcon className="w-10 h-10" />
|
<Cog6ToothIcon className="w-10 h-10" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
@ -42,11 +42,10 @@ export default function Setting() {
|
||||||
}}
|
}}
|
||||||
className={`
|
className={`
|
||||||
w-full text-left px-3 py-2 rounded
|
w-full text-left px-3 py-2 rounded
|
||||||
${
|
${resolution === res.id
|
||||||
resolution === res.id
|
? "bg-primary text-white"
|
||||||
? "bg-primary text-white"
|
: "text-white/90 hover:bg-white/20"
|
||||||
: "text-white/90 hover:bg-white/20"
|
}
|
||||||
}
|
|
||||||
transition-colors duration-200
|
transition-colors duration-200
|
||||||
`}>
|
`}>
|
||||||
{res.label || `${res.height}p`}
|
{res.label || `${res.height}p`}
|
||||||
|
|
|
@ -16,7 +16,7 @@ export default function Speed() {
|
||||||
<div className="relative flex items-center">
|
<div className="relative flex items-center">
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsSpeedOpen(!isSpeedOpen)}
|
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">
|
<span className="text-xl font-bold mr-1">
|
||||||
{playbackSpeed === 1.0 ? "倍速" : `${playbackSpeed}x`}
|
{playbackSpeed === 1.0 ? "倍速" : `${playbackSpeed}x`}
|
||||||
</span>
|
</span>
|
||||||
|
@ -40,11 +40,10 @@ export default function Speed() {
|
||||||
}
|
}
|
||||||
setIsSpeedOpen(false);
|
setIsSpeedOpen(false);
|
||||||
}}
|
}}
|
||||||
className={`px-2 py-1 text-lg whitespace-nowrap ${
|
className={`px-2 py-1 text-lg whitespace-nowrap ${playbackSpeed === speed
|
||||||
playbackSpeed === speed
|
? "text-primary-400 font-bold"
|
||||||
? "text-primaryHover font-bold"
|
: "text-white hover:text-primary-400"
|
||||||
: "text-white hover:text-primaryHover"
|
}`}>
|
||||||
}`}>
|
|
||||||
{speed}x
|
{speed}x
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
|
|
|
@ -11,7 +11,7 @@ export default function Volume() {
|
||||||
<div className="group relative flex items-center">
|
<div className="group relative flex items-center">
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsMuted(!isMuted)}
|
onClick={() => setIsMuted(!isMuted)}
|
||||||
className="text-white hover:text-primaryHover">
|
className="text-white hover:text-primary-400">
|
||||||
{isMuted ? (
|
{isMuted ? (
|
||||||
<SpeakerXMarkIcon className="w-10 h-10" />
|
<SpeakerXMarkIcon className="w-10 h-10" />
|
||||||
) : (
|
) : (
|
||||||
|
|
|
@ -45,11 +45,11 @@
|
||||||
background-color: transparent !important;
|
background-color: transparent !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-table-thead > tr > th {
|
.ant-table-thead>tr>th {
|
||||||
background-color: transparent !important;
|
background-color: transparent !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-table-tbody > tr > td {
|
.ant-table-tbody>tr>td {
|
||||||
background-color: transparent !important;
|
background-color: transparent !important;
|
||||||
border-bottom-color: transparent !important;
|
border-bottom-color: transparent !important;
|
||||||
}
|
}
|
||||||
|
@ -85,9 +85,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 覆盖 Ant Design 的默认样式 raido button左侧的按钮*/
|
/* 覆盖 Ant Design 的默认样式 raido button左侧的按钮*/
|
||||||
.ant-radio-button-wrapper-checked:not(
|
.ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled)::before {
|
||||||
.ant-radio-button-wrapper-disabled
|
|
||||||
)::before {
|
|
||||||
background-color: unset !important;
|
background-color: unset !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -100,7 +98,7 @@
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-wrap-header .ant-table-thead > tr > th {
|
.no-wrap-header .ant-table-thead>tr>th {
|
||||||
white-space: nowrap;
|
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;
|
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;
|
border-bottom: none;
|
||||||
/* 去除最后一行的底部边框 */
|
/* 去除最后一行的底部边框 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.quill-editor-container .ql-toolbar.ql-snow,
|
.quill-editor-container .ql-toolbar.ql-snow,
|
||||||
.quill-editor-container .ql-container.ql-snow {
|
.quill-editor-container .ql-container.ql-snow {
|
||||||
border-color: transparent;
|
border-color: transparent;
|
||||||
|
@ -144,21 +143,24 @@
|
||||||
|
|
||||||
.quill-editor-container .ql-editor {
|
.quill-editor-container .ql-editor {
|
||||||
min-height: 120px;
|
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 {
|
.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-editor {
|
||||||
|
|
||||||
/* 代码块容器 */
|
/* 代码块容器 */
|
||||||
.ql-code-block-container {
|
.ql-code-block-container {
|
||||||
background: #1e293b;
|
background: #1e293b;
|
||||||
color: #e2e8f0;
|
color: #e2e8f0;
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
margin: 0; /* 更新 */
|
margin: 0;
|
||||||
|
/* 更新 */
|
||||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
|
||||||
monospace;
|
monospace;
|
||||||
|
|
||||||
|
@ -179,7 +181,8 @@
|
||||||
color: #e2e8f0;
|
color: #e2e8f0;
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
margin: 0; /* 更新 */
|
margin: 0;
|
||||||
|
/* 更新 */
|
||||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
|
||||||
monospace;
|
monospace;
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
|
@ -194,7 +197,8 @@
|
||||||
border-left: 4px solid #3b82f6;
|
border-left: 4px solid #3b82f6;
|
||||||
background: #f8fafc;
|
background: #f8fafc;
|
||||||
padding: 1rem 1.2rem;
|
padding: 1rem 1.2rem;
|
||||||
margin: 0; /* 更新 */
|
margin: 0;
|
||||||
|
/* 更新 */
|
||||||
color: #475569;
|
color: #475569;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
|
|
||||||
|
@ -202,7 +206,8 @@
|
||||||
blockquote {
|
blockquote {
|
||||||
border-left-color: #64748b;
|
border-left-color: #64748b;
|
||||||
background: #f1f5f9;
|
background: #f1f5f9;
|
||||||
margin: 0; /* 更新 */
|
margin: 0;
|
||||||
|
/* 更新 */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -210,12 +215,14 @@
|
||||||
ol {
|
ol {
|
||||||
list-style-type: decimal;
|
list-style-type: decimal;
|
||||||
padding-left: 2rem;
|
padding-left: 2rem;
|
||||||
margin: 0; /* 更新 */
|
margin: 0;
|
||||||
|
/* 更新 */
|
||||||
|
|
||||||
/* 嵌套有序列表 */
|
/* 嵌套有序列表 */
|
||||||
ol {
|
ol {
|
||||||
list-style-type: lower-alpha;
|
list-style-type: lower-alpha;
|
||||||
margin: 0; /* 更新 */
|
margin: 0;
|
||||||
|
/* 更新 */
|
||||||
|
|
||||||
ol {
|
ol {
|
||||||
list-style-type: lower-roman;
|
list-style-type: lower-roman;
|
||||||
|
@ -224,7 +231,8 @@
|
||||||
|
|
||||||
li {
|
li {
|
||||||
padding-left: 0.5rem;
|
padding-left: 0.5rem;
|
||||||
margin-bottom: 0; /* 更新 */
|
margin-bottom: 0;
|
||||||
|
/* 更新 */
|
||||||
|
|
||||||
&::marker {
|
&::marker {
|
||||||
color: #3b82f6;
|
color: #3b82f6;
|
||||||
|
@ -237,12 +245,14 @@
|
||||||
ul {
|
ul {
|
||||||
list-style-type: disc;
|
list-style-type: disc;
|
||||||
padding-left: 2rem;
|
padding-left: 2rem;
|
||||||
margin: 0; /* 更新 */
|
margin: 0;
|
||||||
|
/* 更新 */
|
||||||
|
|
||||||
/* 嵌套无序列表 */
|
/* 嵌套无序列表 */
|
||||||
ul {
|
ul {
|
||||||
list-style-type: circle;
|
list-style-type: circle;
|
||||||
margin: 0; /* 更新 */
|
margin: 0;
|
||||||
|
/* 更新 */
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
list-style-type: square;
|
list-style-type: square;
|
||||||
|
@ -251,7 +261,8 @@
|
||||||
|
|
||||||
li {
|
li {
|
||||||
padding-left: 0.5rem;
|
padding-left: 0.5rem;
|
||||||
margin-bottom: 0; /* 更新 */
|
margin-bottom: 0;
|
||||||
|
/* 更新 */
|
||||||
|
|
||||||
&::marker {
|
&::marker {
|
||||||
color: #3b82f6;
|
color: #3b82f6;
|
||||||
|
@ -266,7 +277,8 @@
|
||||||
color: #1e3a8a;
|
color: #1e3a8a;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
line-height: 1.25;
|
line-height: 1.25;
|
||||||
margin: 0; /* 更新 */
|
margin: 0;
|
||||||
|
/* 更新 */
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
|
@ -287,12 +299,14 @@
|
||||||
hr {
|
hr {
|
||||||
border: 0;
|
border: 0;
|
||||||
border-top: 2px solid #e2e8f0;
|
border-top: 2px solid #e2e8f0;
|
||||||
margin: 0; /* 更新 */
|
margin: 0;
|
||||||
|
/* 更新 */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 段落 */
|
/* 段落 */
|
||||||
p {
|
p {
|
||||||
margin: 0; /* 更新 */
|
margin: 0;
|
||||||
|
/* 更新 */
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -300,7 +314,8 @@
|
||||||
table {
|
table {
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0; /* 更新 */
|
margin: 0;
|
||||||
|
/* 更新 */
|
||||||
|
|
||||||
th,
|
th,
|
||||||
td {
|
td {
|
||||||
|
@ -318,4 +333,4 @@
|
||||||
background: #f8fafc;
|
background: #f8fafc;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -19,19 +19,21 @@ export const useAppTheme = () => useContext(AppThemeContext);
|
||||||
|
|
||||||
export default function ThemeProvider({ children }: { children: ReactNode }) {
|
export default function ThemeProvider({ children }: { children: ReactNode }) {
|
||||||
const { token } = theme.useToken();
|
const { token } = theme.useToken();
|
||||||
|
|
||||||
const applyTheme = (tailwindTheme: TailwindTheme) => {
|
const applyTheme = (tailwindTheme: TailwindTheme) => {
|
||||||
for (let key in tailwindTheme) {
|
for (let key in tailwindTheme) {
|
||||||
document.documentElement.style.setProperty(key, tailwindTheme[key]);
|
document.documentElement.style.setProperty(key, tailwindTheme[key]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const tailwindTheme: TailwindTheme = useMemo(
|
const tailwindTheme: TailwindTheme = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
"--color-primary": token.colorPrimary,
|
"--color-primary": token.colorPrimary,
|
||||||
"--color-primary-active": token.colorPrimaryActive,
|
"--color-primary-active": token.colorPrimaryActive,
|
||||||
"--color-primary-hover": token.colorPrimaryHover,
|
"--color-primary-hover": token.colorPrimaryHover,
|
||||||
"--color-bg-primary-hover": token.colorPrimaryBgHover,
|
"--color-bg-primary-hover": token.colorPrimaryBgHover,
|
||||||
"--color-text-tertiary-400": token.colorTextSecondary,
|
"--color-text-secondary": token.colorTextSecondary,
|
||||||
"--color-text-tertiary-400": token.colorTextTertiary,
|
"--color-text-tertiary": token.colorTextTertiary,
|
||||||
"--color-bg-text-hover": token.colorBgTextHover,
|
"--color-bg-text-hover": token.colorBgTextHover,
|
||||||
"--color-bg-container": token.colorBgContainer,
|
"--color-bg-container": token.colorBgContainer,
|
||||||
"--color-bg-layout": token.colorBgLayout,
|
"--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 LetterProgressPage from "../app/main/letter/progress/page";
|
||||||
import HelpPage from "../app/main/help/page";
|
import HelpPage from "../app/main/help/page";
|
||||||
import AuthPage from "../app/auth/page";
|
import AuthPage from "../app/auth/page";
|
||||||
import React from "react";
|
|
||||||
import EditorLetterPage from "../app/main/letter/editor/page";
|
import EditorLetterPage from "../app/main/letter/editor/page";
|
||||||
import LetterDetailPage from "../app/main/letter/detail/page";
|
import LetterDetailPage from "../app/main/letter/detail/page";
|
||||||
|
import AdminLayout from "../components/layout/admin/AdminLayout";
|
||||||
interface CustomIndexRouteObject extends IndexRouteObject {
|
import { CustomRouteObject } from "./types";
|
||||||
name?: string;
|
import { adminRoute } from "./admin-route";
|
||||||
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;
|
|
||||||
export const routes: CustomRouteObject[] = [
|
export const routes: CustomRouteObject[] = [
|
||||||
{
|
{
|
||||||
path: "/",
|
path: "/",
|
||||||
|
@ -86,120 +63,12 @@ export const routes: CustomRouteObject[] = [
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
adminRoute
|
||||||
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>;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/auth",
|
path: "/auth",
|
||||||
breadcrumb: "登录",
|
|
||||||
element: <AuthPage></AuthPage>,
|
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 { NiceTailwindConfig } from "@nice/theme"
|
||||||
import { createTailwindTheme, defaultTheme } from '@nice/theme';
|
export default NiceTailwindConfig
|
||||||
|
|
||||||
const tailwindTheme = createTailwindTheme(defaultTheme)
|
|
||||||
const config: Config = {
|
|
||||||
content: [
|
|
||||||
"./index.html",
|
|
||||||
"./src/**/*.{js,ts,jsx,tsx}",
|
|
||||||
],
|
|
||||||
theme: tailwindTheme,
|
|
||||||
plugins: [],
|
|
||||||
}
|
|
||||||
export default config
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { generateBorderRadius, generateBoxShadow, generateSpacing, generateTheme, generateTypography, generateZIndex } from "./generator";
|
import { generateTheme} from "./generator";
|
||||||
import { ThemeConfig, ThemeSeed } from "./types";
|
import { ThemeSeed } from "./types";
|
||||||
|
|
||||||
// 添加默认的主题配置
|
// 添加默认的主题配置
|
||||||
export const USAFSeed: ThemeSeed = {
|
export const USAFSeed: ThemeSeed = {
|
||||||
|
@ -13,13 +13,7 @@ export const USAFSeed: ThemeSeed = {
|
||||||
info: '#00538E', // 信息蓝色
|
info: '#00538E', // 信息蓝色
|
||||||
|
|
||||||
},
|
},
|
||||||
config: {
|
|
||||||
borderRadius: generateBorderRadius(),
|
|
||||||
spacing: generateSpacing(),
|
|
||||||
...generateTypography(),
|
|
||||||
boxShadow: generateBoxShadow(),
|
|
||||||
zIndex: generateZIndex(),
|
|
||||||
},
|
|
||||||
isDark: false
|
isDark: false
|
||||||
};
|
};
|
||||||
export const defaultTheme = generateTheme(USAFSeed)
|
export const defaultTheme = generateTheme(USAFSeed)
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { createContext, useContext, useMemo, useState } from 'react';
|
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 { USAFSeed } from './constants';
|
||||||
import { createTailwindTheme, injectThemeVariables } from './styles';
|
import { createTailwindTheme, injectThemeVariables } from './styles';
|
||||||
import { generateTheme } from './generator';
|
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 { withAlpha, generateColorScale } from './colors';
|
||||||
import { darkMode } from './utils';
|
import { darkMode } from './utils';
|
||||||
export function generateThemeColors(seed: ThemeSeed['colors']): ThemeColors {
|
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 {
|
export function generateTheme(seed: ThemeSeed): Theme {
|
||||||
const isDark = seed.isDark ?? false;
|
const isDark = seed.isDark ?? false;
|
||||||
const colors = generateThemeColors(seed.colors);
|
const colors = generateThemeColors(seed.colors);
|
||||||
const semantics = generateSemantics(colors, isDark);
|
const semantics = generateSemantics(colors, isDark);
|
||||||
const config = generateThemeConfig();
|
|
||||||
return {
|
return {
|
||||||
token: {
|
token: {
|
||||||
...colors,
|
...colors,
|
||||||
...semantics,
|
...semantics
|
||||||
...config
|
|
||||||
},
|
},
|
||||||
isDark
|
isDark
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,4 +2,5 @@ export * from "./context"
|
||||||
export * from "./types"
|
export * from "./types"
|
||||||
export * from "./utils"
|
export * from "./utils"
|
||||||
export * from "./styles"
|
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,
|
textColor: themeConfig.textColor,
|
||||||
backgroundColor: themeConfig.backgroundColor,
|
backgroundColor: themeConfig.backgroundColor,
|
||||||
borderColor: themeConfig.border,
|
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)
|
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 {
|
|
||||||
/** 圆角配置 */
|
export type ThemeToken = ThemeSemantics
|
||||||
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 interface Theme {
|
export interface Theme {
|
||||||
token: ThemeToken;
|
token: ThemeToken;
|
||||||
isDark: boolean;
|
isDark: boolean;
|
||||||
|
@ -202,6 +107,6 @@ export interface ThemeSeed {
|
||||||
error?: string;
|
error?: string;
|
||||||
info?: string;
|
info?: string;
|
||||||
}
|
}
|
||||||
config: ThemeConfig
|
|
||||||
isDark?: boolean;
|
isDark?: boolean;
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,14 +2,12 @@
|
||||||
export function darkMode<T>(isDark: boolean, darkValue: T, lightValue: T): T {
|
export function darkMode<T>(isDark: boolean, darkValue: T, lightValue: T): T {
|
||||||
return isDark ? darkValue : lightValue;
|
return isDark ? darkValue : lightValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 将驼峰命名转换为kebab-case
|
* 将驼峰命名转换为kebab-case
|
||||||
*/
|
*/
|
||||||
export function toKebabCase(str: string): string {
|
export function toKebabCase(str: string): string {
|
||||||
return str.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase()
|
return str.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 将嵌套对象扁平化,使用点号连接键名
|
* 将嵌套对象扁平化,使用点号连接键名
|
||||||
*/
|
*/
|
||||||
|
|
Loading…
Reference in New Issue