style: 优化后台整体UI风格与组件样式

1.  新增并配置全局颜色、阴影、圆角等主题变量
2.  统一各页面卡片、表单、按钮等组件的样式规范
3.  优化登录页、个人中心、仪表盘等页面的布局与视觉效果
4.  添加全局滚动条样式与元素过渡动画
This commit is contained in:
2026-07-02 18:05:46 +08:00
parent 96eaa810f3
commit 718d1c7aac
12 changed files with 242 additions and 97 deletions

View File

@@ -1,4 +1,5 @@
import { Card } from '@arco-design/web-react'; import { Card } from '@arco-design/web-react';
import { IconPlayCircle, IconUser, IconFile, IconCheckCircle } from '@arco-design/web-react/icon';
import { dramaApi, logApi, userApi } from '../api'; import { dramaApi, logApi, userApi } from '../api';
import { usePageData } from '../hooks/usePageData'; import { usePageData } from '../hooks/usePageData';
import { hasPermissions } from '../utils/permission'; import { hasPermissions } from '../utils/permission';
@@ -6,8 +7,32 @@ import { NoAccessPage } from '../components/NoAccessPage';
import { RequestLogTable } from '../components/RequestLogTable'; import { RequestLogTable } from '../components/RequestLogTable';
import type { AppUser, DashboardPageProps, Drama, MetricCardProps, RequestLog } from './interface'; import type { AppUser, DashboardPageProps, Drama, MetricCardProps, RequestLog } from './interface';
function MetricCard({ label, value }: MetricCardProps) { const METRIC_ICONS: Record<string, React.ReactNode> = {
return <Card><div className="mb-2 text-muted">{label}</div><div className="text-[28px] font-bold">{value}</div></Card>; dramas: <IconPlayCircle className="text-2xl text-brand" />,
users: <IconUser className="text-2xl text-success" />,
logs: <IconFile className="text-2xl text-warning" />,
status: <IconCheckCircle className="text-2xl text-success" />,
};
const METRIC_COLORS: Record<string, string> = {
dramas: 'from-brand/5 to-brand/10',
users: 'from-success/5 to-success/10',
logs: 'from-warning/5 to-warning/10',
status: 'from-success/5 to-success/10',
};
function MetricCard({ label, value, icon }: MetricCardProps & { icon: string }) {
return (
<div className={`stat-card bg-gradient-to-br ${METRIC_COLORS[icon]}`}>
<div className="mb-3 flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-white/80 shadow-sm">
{METRIC_ICONS[icon]}
</div>
<span className="text-[13px] font-medium text-muted">{label}</span>
</div>
<div className="text-[28px] font-bold tracking-tight text-ink">{value}</div>
</div>
);
} }
function DashboardPage({ profile }: DashboardPageProps) { function DashboardPage({ profile }: DashboardPageProps) {
@@ -21,11 +46,11 @@ function DashboardPage({ profile }: DashboardPageProps) {
return ( return (
<> <>
<div className="mb-4 grid grid-cols-1 gap-4 sm:grid-cols-2 min-[901px]:grid-cols-4"> <div className="mb-5 grid grid-cols-1 gap-4 sm:grid-cols-2 min-[901px]:grid-cols-4">
{canReadDramas && <MetricCard label="短剧管理" value={dramas.data.pagination.total} />} {canReadDramas && <MetricCard label="短剧管理" value={dramas.data.pagination.total} icon="dramas" />}
{canReadUsers && <MetricCard label="用户管理" value={users.data.pagination.total} />} {canReadUsers && <MetricCard label="用户管理" value={users.data.pagination.total} icon="users" />}
{canReadLogs && <MetricCard label="请求日志" value={logs.data.pagination.total} />} {canReadLogs && <MetricCard label="请求日志" value={logs.data.pagination.total} icon="logs" />}
<MetricCard label="接口状态" value="运行中" /> <MetricCard label="接口状态" value="运行中" icon="status" />
</div> </div>
{canReadLogs ? ( {canReadLogs ? (
<Card className="section-card" title="最近请求"> <Card className="section-card" title="最近请求">

View File

@@ -42,13 +42,16 @@ export function DramaList() {
}; };
return ( return (
<div className=""> <div className="page-stack">
<nav className="flex justify-end border border border-[#e5e5e5] pb-4"> <Card
className="section-card"
title="短剧管理"
extra={
<Button type="primary" icon={<IconPlus />} onClick={() => setVisible(true)}> <Button type="primary" icon={<IconPlus />} onClick={() => setVisible(true)}>
</Button> </Button>
</nav> }
<Card className="section-card" title="短剧管理"> >
<Table <Table
rowKey="id" rowKey="id"
loading={loading} loading={loading}

View File

@@ -2,30 +2,26 @@ import { Button, Form, Input } from "@arco-design/web-react";
import { IconLock, IconUser } from "@arco-design/web-react/icon"; import { IconLock, IconUser } from "@arco-design/web-react/icon";
import type { LoginPanelProps } from "../interface"; import type { LoginPanelProps } from "../interface";
export function LoginPanel({ loading, apiBaseUrl, onSubmit }: LoginPanelProps) { export const LoginPanel = ({ loading, onSubmit }: LoginPanelProps) => {
return ( return (
<div className="flex items-center justify-center bg-[#f7f8fa] bg-[image:linear-gradient(180deg,rgba(255,255,255,0.86),rgba(247,248,250,0.94))] p-12"> <div className="flex items-center justify-center bg-[#f7f8fa] bg-[image:linear-gradient(180deg,rgba(255,255,255,0.86),rgba(247,248,250,0.94))] p-12">
<div className="w-full max-w-[420px] rounded-lg border border-line bg-white p-9 shadow-login"> <div className="w-full max-w-[420px] rounded-2xl border border-line/60 bg-white p-9 shadow-login">
<div className="mb-[30px] flex items-center gap-3.5"> <div className="mb-8 flex items-center flex-col text-center gap-3.5">
<span className="grid h-11 w-11 place-items-center rounded-lg bg-brand font-bold text-white">CT</span> <h2 className="mb-1 w-full mt-0 text-2xl font-semibold text-ink"></h2>
<div> <p className="m-0 w-full text-[13px] text-muted">使</p>
<h2 className="mb-2 mt-0 text-2xl"></h2>
<p className="m-0 text-muted">使</p>
</div>
</div> </div>
<Form layout="vertical" onSubmit={onSubmit} initialValues={{ username: "admin", password: "admin123" }}> <Form layout="vertical" onSubmit={onSubmit} initialValues={{ username: "admin", password: "admin123" }}>
<Form.Item label="用户名" field="username" rules={[{ required: true }]}> <Form.Item label="用户名" field="username" rules={[{ required: true }]}>
<Input prefix={<IconUser />} placeholder="admin" /> <Input prefix={<IconUser className="text-muted" />} placeholder="请输入用户名" size="large" />
</Form.Item> </Form.Item>
<Form.Item label="密码" field="password" rules={[{ required: true }]}> <Form.Item label="密码" field="password" rules={[{ required: true }]}>
<Input.Password prefix={<IconLock />} placeholder="admin123" /> <Input.Password prefix={<IconLock className="text-muted" />} placeholder="请输入密码" size="large" />
</Form.Item> </Form.Item>
<Button type="primary" htmlType="submit" loading={loading} long> <Button type="primary" htmlType="submit" loading={loading} long size="large" className="!h-11 mt-6 !rounded-lg !text-[15px] !font-medium">
</Button> </Button>
</Form> </Form>
<div className="mt-[22px] border-t border-canvas pt-[18px] text-xs text-muted">API {apiBaseUrl}</div>
</div> </div>
</div> </div>
); );
} };

View File

@@ -2,26 +2,26 @@ export function LoginVisual() {
return ( return (
<div className="relative hidden flex-col justify-between overflow-hidden px-14 pb-12 pt-11 text-white after:pointer-events-none after:absolute after:inset-0 after:bg-[linear-gradient(180deg,rgba(255,255,255,0.08),rgba(255,255,255,0))] min-[901px]:flex bg-[linear-gradient(135deg,rgba(15,23,42,0.98)_0%,rgba(22,93,255,0.94)_54%,rgba(15,118,110,0.96)_100%),radial-gradient(circle_at_18%_18%,rgba(255,255,255,0.18),transparent_28%),radial-gradient(circle_at_82%_72%,rgba(255,255,255,0.12),transparent_32%)]"> <div className="relative hidden flex-col justify-between overflow-hidden px-14 pb-12 pt-11 text-white after:pointer-events-none after:absolute after:inset-0 after:bg-[linear-gradient(180deg,rgba(255,255,255,0.08),rgba(255,255,255,0))] min-[901px]:flex bg-[linear-gradient(135deg,rgba(15,23,42,0.98)_0%,rgba(22,93,255,0.94)_54%,rgba(15,118,110,0.96)_100%),radial-gradient(circle_at_18%_18%,rgba(255,255,255,0.18),transparent_28%),radial-gradient(circle_at_82%_72%,rgba(255,255,255,0.12),transparent_32%)]">
<div className="relative z-[1] flex items-center gap-2.5 font-bold"> <div className="relative z-[1] flex items-center gap-2.5 font-bold">
<span className="grid h-7 w-7 place-items-center rounded-md bg-brand text-sm text-white">CT</span> <span className="grid h-8 w-8 place-items-center rounded-lg bg-white/20 text-sm backdrop-blur-sm">CT</span>
<span>cth-tk-admin</span> <span className="text-white/90">cth-tk-admin</span>
</div> </div>
<div className="relative z-[1] max-w-[640px]"> <div className="relative z-[1] max-w-[640px]">
<span className="mb-[18px] inline-flex rounded-full border border-white/20 px-3 py-1.5 text-[13px] text-white/80">TikTok </span> <span className="mb-5 inline-flex rounded-full border border-white/20 bg-white/10 px-4 py-2 text-[13px] text-white/80 backdrop-blur-sm">TikTok </span>
<h1 className="mb-4 mt-0 text-[42px] leading-[1.16]">CTH TikTok </h1> <h1 className="mb-5 mt-0 text-[44px] font-bold leading-[1.16] tracking-tight">CTH TikTok </h1>
<p className="m-0 max-w-[560px] text-base leading-[1.8] text-white/75"></p> <p className="m-0 max-w-[560px] text-base leading-[1.8] text-white/70"></p>
</div> </div>
<div className="relative z-[1] grid max-w-[640px] grid-cols-3 gap-3"> <div className="relative z-[1] grid max-w-[640px] grid-cols-3 gap-4">
<div className="rounded-lg border border-white/20 bg-white/10 p-4 backdrop-blur-[10px]"> <div className="rounded-xl border border-white/15 bg-white/10 p-5 backdrop-blur-md transition-all duration-300 hover:bg-white/15">
<strong className="mb-1.5 block text-xl"></strong> <strong className="mb-2 block text-xl font-semibold"></strong>
<span className="text-white/70"></span> <span className="text-[13px] text-white/65"></span>
</div> </div>
<div className="rounded-lg border border-white/20 bg-white/10 p-4 backdrop-blur-[10px]"> <div className="rounded-xl border border-white/15 bg-white/10 p-5 backdrop-blur-md transition-all duration-300 hover:bg-white/15">
<strong className="mb-1.5 block text-xl"></strong> <strong className="mb-2 block text-xl font-semibold"></strong>
<span className="text-white/70"></span> <span className="text-[13px] text-white/65"></span>
</div> </div>
<div className="rounded-lg border border-white/20 bg-white/10 p-4 backdrop-blur-[10px]"> <div className="rounded-xl border border-white/15 bg-white/10 p-5 backdrop-blur-md transition-all duration-300 hover:bg-white/15">
<strong className="mb-1.5 block text-xl"></strong> <strong className="mb-2 block text-xl font-semibold"></strong>
<span className="text-white/70"></span> <span className="text-[13px] text-white/65"></span>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -13,9 +13,9 @@ export function ProfileInfoList({ profile }: ProfileInfoListProps) {
function ProfileInfoItem({ label, value }: ProfileInfoItemProps) { function ProfileInfoItem({ label, value }: ProfileInfoItemProps) {
return ( return (
<div className="rounded-lg border border-line bg-panel p-3.5"> <div className="rounded-xl border border-line/60 bg-canvas/50 p-4 transition-all duration-200 hover:bg-canvas">
<span className="mb-2 block text-[13px] text-muted">{label}</span> <span className="mb-2 block text-[12px] font-medium uppercase tracking-wider text-muted">{label}</span>
<strong className="break-words font-medium text-ink">{value}</strong> <strong className="break-words text-[15px] font-semibold text-ink">{value}</strong>
</div> </div>
); );
} }

View File

@@ -3,11 +3,13 @@ import type { ProfileSummaryProps } from "../interface";
export function ProfileSummary({ profile }: ProfileSummaryProps) { export function ProfileSummary({ profile }: ProfileSummaryProps) {
return ( return (
<div className="mb-6 flex items-center gap-4"> <div className="mb-6 flex items-center gap-4 rounded-xl bg-gradient-to-br from-brand/5 to-brand/10 p-5">
<Avatar size={56}>{profile?.username?.slice(0, 1).toUpperCase() || "A"}</Avatar> <Avatar size={60} className="!bg-brand !text-white !text-xl !font-bold !shadow-lg !shadow-brand/30">
{profile?.username?.slice(0, 1).toUpperCase() || "A"}
</Avatar>
<div> <div>
<h2 className="mb-1.5 mt-0 text-[22px]">{profile?.nickname || profile?.username || "管理员"}</h2> <h2 className="mb-1 mt-0 text-xl font-semibold text-ink">{profile?.nickname || profile?.username || "管理员"}</h2>
<p className="m-0 text-muted">{profile?.email || "未设置邮箱"}</p> <p className="m-0 text-[13px] text-muted">{profile?.email || "未设置邮箱"}</p>
</div> </div>
</div> </div>
); );

View File

@@ -65,6 +65,7 @@ function ProfilePage() {
}; };
return ( return (
<div className="page-stack">
<div className="grid items-start gap-4 min-[901px]:grid-cols-[minmax(0,1fr)_420px]"> <div className="grid items-start gap-4 min-[901px]:grid-cols-[minmax(0,1fr)_420px]">
<Card className="section-card min-[901px]:row-span-2" title="个人中心" loading={loading}> <Card className="section-card min-[901px]:row-span-2" title="个人中心" loading={loading}>
<ProfileSummary profile={profile} /> <ProfileSummary profile={profile} />
@@ -96,6 +97,7 @@ function ProfilePage() {
</Form> </Form>
</Card> </Card>
</div> </div>
</div>
); );
} }

View File

@@ -117,7 +117,7 @@ export function PermissionTreePicker({ permissions, value, onChange }: Permissio
const checkedKeys = value.map((id) => permissionKey(id)); const checkedKeys = value.map((id) => permissionKey(id));
return ( return (
<div className="mb-[15px] h-[360px] overflow-auto rounded-md border border-line bg-panel px-4 py-3"> <div className="mb-[15px] h-[360px] overflow-auto rounded-xl border border-line/60 bg-canvas/30 px-4 py-3">
<Tree <Tree
blockNode blockNode
checkable checkable

View File

@@ -1,11 +1,17 @@
import { Card, Typography } from "@arco-design/web-react"; import { Card, Typography } from "@arco-design/web-react";
import { IconLock } from "@arco-design/web-react/icon";
const { Text } = Typography; const { Text } = Typography;
export function NoAccessPage() { export function NoAccessPage() {
return ( return (
<Card className="section-card" title="暂无权限"> <Card className="section-card" title="暂无权限">
<Text type="secondary">访</Text> <div className="flex flex-col items-center py-8">
<div className="mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-warning/10">
<IconLock className="text-3xl text-warning" />
</div>
<Text type="secondary" className="text-center">访</Text>
</div>
</Card> </Card>
); );
} }

View File

@@ -2,9 +2,27 @@
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
@layer base {
* {
@apply transition-colors duration-200;
}
::-webkit-scrollbar {
@apply w-1.5 h-1.5;
}
::-webkit-scrollbar-track {
@apply bg-transparent;
}
::-webkit-scrollbar-thumb {
@apply rounded-full bg-line hover:bg-muted;
}
}
@layer components { @layer components {
.section-card { .section-card {
@apply rounded-lg; @apply rounded-xl border border-line/60 bg-white shadow-sm;
} }
.page-stack { .page-stack {
@@ -22,4 +40,82 @@
.form-help { .form-help {
@apply -mt-3 mb-[18px] text-[13px] leading-[1.6] text-muted; @apply -mt-3 mb-[18px] text-[13px] leading-[1.6] text-muted;
} }
.stat-card {
@apply rounded-xl border border-line/60 bg-white p-5 shadow-sm transition-all duration-300 hover:shadow-md hover:-translate-y-0.5;
}
}
.arco-layout-sider {
transition: width 0.2s cubic-bezier(0.2, 0, 0, 1) !important;
}
.arco-layout-sider .arco-menu {
border-right: none !important;
}
.arco-card {
border-radius: 0.75rem !important;
border: 1px solid rgba(229, 230, 235, 0.6) !important;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.04) !important;
}
.arco-table-container {
border-radius: 0.5rem;
overflow: hidden;
}
.arco-table-th {
background-color: #fafbfc !important;
font-weight: 600 !important;
color: #4e5969 !important;
}
.arco-table-tr:hover .arco-table-td {
background-color: #f7f8fa !important;
}
.arco-btn-primary {
box-shadow: 0 2px 6px rgba(22, 93, 251, 0.3) !important;
}
.arco-btn-primary:hover {
box-shadow: 0 4px 12px rgba(22, 93, 251, 0.4) !important;
}
.arco-modal {
border-radius: 0.75rem !important;
overflow: hidden;
}
.arco-modal-header {
border-bottom: 1px solid rgba(229, 230, 235, 0.6) !important;
padding: 16px 24px !important;
}
.arco-modal-body {
padding: 24px !important;
}
.arco-input-wrapper {
border-radius: 0.5rem !important;
transition: all 0.2s !important;
}
.arco-input-wrapper:hover {
border-color: #c9cdd4 !important;
}
.arco-input-wrapper.arco-input-focus {
border-color: #165dff !important;
box-shadow: 0 0 0 2px rgba(22, 93, 251, 0.1) !important;
}
.arco-select-view {
border-radius: 0.5rem !important;
}
.arco-form-item-label {
font-weight: 500 !important;
color: #4e5969 !important;
} }

View File

@@ -48,50 +48,56 @@ export function AdminLayout() {
}; };
return ( return (
<Layout className="min-h-screen"> <Layout className="min-h-screen bg-canvas">
<Sider className="shadow-[1px_0_0_#e5e6eb]" width={220} collapsed={collapsed} collapsible trigger={null}> <Sider className="shadow-[1px_0_0_#e5e6eb] bg-white" width={220} collapsed={collapsed} collapsible trigger={null}>
<div className="flex h-14 items-center gap-2.5 border-b border-line px-[18px] font-bold"> <div className="flex h-14 items-center gap-2.5 border-b border-line/60 px-[18px]">
<span className="grid h-7 w-7 place-items-center rounded-md bg-brand text-sm text-white">TK</span> <span className="grid h-8 w-8 place-items-center rounded-lg bg-brand text-sm font-bold text-white shadow-sm">TK</span>
{!collapsed && <span>TK短剧管理后台</span>} {!collapsed && <span className="font-semibold text-ink">TK短剧管理后台</span>}
</div> </div>
<Menu <Menu
selectedKeys={[current?.item.key || "/dashboard"]} selectedKeys={[current?.item.key || "/dashboard"]}
openKeys={openMenuKeys} openKeys={openMenuKeys}
onClickMenuItem={(key) => navigate(key)} onClickMenuItem={(key) => navigate(key)}
onClickSubMenu={(_, openKeys) => setOpenMenuKeys(openKeys)} onClickSubMenu={(_, openKeys) => setOpenMenuKeys(openKeys)}
className="!border-none"
> >
{visibleMenuItems.map((item) => renderMenuItem(item))} {visibleMenuItems.map((item) => renderMenuItem(item))}
</Menu> </Menu>
</Sider> </Sider>
<Layout> <Layout>
<Header className="flex h-14 items-center justify-between border-b border-line bg-white px-5"> <Header className="flex h-14 items-center justify-between border-b border-line/60 bg-white/80 px-5 backdrop-blur-md">
<Space> <Space>
<Button icon={collapsed ? <IconMenuUnfold /> : <IconMenuFold />} onClick={() => setCollapsed(!collapsed)} /> <Button
type="text"
icon={collapsed ? <IconMenuUnfold /> : <IconMenuFold />}
onClick={() => setCollapsed(!collapsed)}
className="!text-muted hover:!text-ink"
/>
<div className="flex flex-col gap-0.5"> <div className="flex flex-col gap-0.5">
<strong>{current?.item.label || "工作台"}</strong> <strong className="text-[15px] text-ink">{current?.item.label || "工作台"}</strong>
</div> </div>
</Space> </Space>
<Dropdown <Dropdown
position="br" position="br"
trigger="click" trigger="click"
droplist={ droplist={
<Menu> <Menu className="!rounded-lg !border-line/60 !shadow-lg">
<Menu.Item key="profile" onClick={() => navigate("/profile")}> <Menu.Item key="profile" onClick={() => navigate("/profile")} className="!rounded-md">
</Menu.Item> </Menu.Item>
<Menu.Item key="logout" onClick={logout}> <Menu.Item key="logout" onClick={logout} className="!rounded-md">
退 退
</Menu.Item> </Menu.Item>
</Menu> </Menu>
} }
> >
<button className="inline-flex cursor-pointer items-center gap-2 rounded-md border-0 bg-transparent px-2 py-1 text-ink hover:bg-canvas" type="button"> <button className="inline-flex cursor-pointer items-center gap-2 rounded-lg border-0 bg-transparent px-2.5 py-1.5 text-ink transition-all duration-200 hover:bg-canvas" type="button">
<Avatar size={28}>{profile?.username?.slice(0, 1).toUpperCase() || "A"}</Avatar> <Avatar size={30} className="!bg-brand !text-white">{profile?.username?.slice(0, 1).toUpperCase() || "A"}</Avatar>
<Text>{profile?.nickname || profile?.username || "管理员"}</Text> <Text className="!text-[13px]">{profile?.nickname || profile?.username || "管理员"}</Text>
</button> </button>
</Dropdown> </Dropdown>
</Header> </Header>
<Content className="p-3 sm:p-5"> <Content className="p-4 sm:p-6">
<Routes> <Routes>
<Route path="/dashboard" element={<DashboardPage profile={profile} />} /> <Route path="/dashboard" element={<DashboardPage profile={profile} />} />
<Route <Route

View File

@@ -14,6 +14,10 @@ export default {
canvas: '#f2f3f5', canvas: '#f2f3f5',
panel: '#fbfcfd', panel: '#fbfcfd',
brand: '#165dff', brand: '#165dff',
'brand-light': '#e8f3ff',
success: '#00b42a',
warning: '#ff7d00',
danger: '#f53f3f',
}, },
fontFamily: { fontFamily: {
sans: [ sans: [
@@ -28,6 +32,11 @@ export default {
}, },
boxShadow: { boxShadow: {
login: '0 18px 48px rgba(15, 23, 42, 0.08)', login: '0 18px 48px rgba(15, 23, 42, 0.08)',
card: '0 1px 3px rgba(0,0,0,0.04), 0 1px 2px rgba(0,0,0,0.02)',
'card-hover': '0 4px 12px rgba(0,0,0,0.08)',
},
borderRadius: {
xl: '0.75rem',
}, },
}, },
}, },