style: 完成后台样式统一优化
本次提交完成全后台页面的样式统一整改: 1. 新增Tailwind语义化颜色token,替换所有硬编码颜色值 2. 统一全站悬浮交互效果与阴影样式 3. 修正头像、菜单的品牌渐变配色 4. 统一登录页背景色为语义化token 5. 补全缺失的页面容器包裹 6. 统一弹窗宽度与表格分页配置 7. 标准化状态标签颜色与新增按钮样式 8. 对齐表单控件宽度规范
This commit is contained in:
125
.mimocode/plans/1783060688305-neon-meadow.md
Normal file
125
.mimocode/plans/1783060688305-neon-meadow.md
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
# 后台样式统一优化计划
|
||||||
|
|
||||||
|
## 问题总结
|
||||||
|
|
||||||
|
经过全面审查,发现以下风格不一致和突兀的地方:
|
||||||
|
|
||||||
|
| # | 问题 | 严重度 | 涉及文件 |
|
||||||
|
|---|------|--------|----------|
|
||||||
|
| 1 | CSS 中大量使用硬编码十六进制颜色,未引用 Tailwind 语义 token | 高 | `index.css` |
|
||||||
|
| 2 | `stat-card` 悬浮效果(上移+大阴影)与 `section-card`(仅阴影)风格差异大 | 中 | `index.css` |
|
||||||
|
| 3 | 主按钮悬浮时 `translateY(-1px)` 在管理后台中显得突兀 | 中 | `index.css` |
|
||||||
|
| 4 | `blue-600` 应为 `brand` 的渐变色在布局和头像中混用 | 中 | `layout/index.tsx`, `ProfileSummary.tsx` |
|
||||||
|
| 5 | 登录页使用 `#f7f8fa` 而非 `canvas` token | 低 | `Login/index.tsx`, `LoginPanel.tsx` |
|
||||||
|
| 6 | Role/Personnel 页面缺少 `page-stack` 包裹 | 中 | `Role/index.tsx`, `Personnel/index.tsx` |
|
||||||
|
| 7 | Modal 宽度不一致(640/720/默认520) | 中 | `Channel`, `DramaList`, `DramaDetail`, `Role`, `Personnel` |
|
||||||
|
| 8 | 分页配置不一致(部分有 sizeCanChange/showTotal,部分没有) | 中 | 多个页面 |
|
||||||
|
| 9 | 状态标签颜色不一致(禁用:红色 vs 灰色) | 低 | `User`, `Personnel` vs `Channel`, `Drama` |
|
||||||
|
| 10 | DramaList 的"新增短剧"按钮用绿色,其他页面用蓝色 | 低 | `DramaList.tsx` |
|
||||||
|
| 11 | InputNumber 宽度不一致(固定180px vs 100%) | 低 | `Channel` vs `Drama` |
|
||||||
|
|
||||||
|
## 实施方案
|
||||||
|
|
||||||
|
### 步骤 1:统一 CSS 中的颜色引用 (`index.css`)
|
||||||
|
|
||||||
|
将所有硬编码颜色替换为 Tailwind 语义化写法:
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* 之前 */
|
||||||
|
background-color: #f2f3f5 !important; /* canvas */
|
||||||
|
color: #165dff !important; /* brand */
|
||||||
|
background-color: #e8f3ff !important; /* brand-light */
|
||||||
|
color: #4e5969 !important; /* 无对应token,新增 secondary */
|
||||||
|
background-color: #fafbfc !important; /* 无对应token,新增 header-bg */
|
||||||
|
color: #1d2129 !important; /* ink */
|
||||||
|
color: #86909c !important; /* muted */
|
||||||
|
border-color: #c9cdd4 !important; /* 无对应token,新增 border-hover */
|
||||||
|
background-color: #f7f8fa !important; /* 无对应token,新增 row-hover */
|
||||||
|
color: #fff !important; /* white */
|
||||||
|
```
|
||||||
|
|
||||||
|
新增 tailwind.config.js token:
|
||||||
|
- `secondary: '#4e5969'` — 表头、表单标签等次级文字
|
||||||
|
- `header-bg: '#fafbfc'` — 表头、描述标签背景
|
||||||
|
- `row-hover: '#f7f8fa'` — 表格行悬浮背景
|
||||||
|
- `border-hover: '#c9cdd4'` — 输入框悬浮边框
|
||||||
|
|
||||||
|
CSS 改用 `theme()` 函数引用:
|
||||||
|
```css
|
||||||
|
background-color: theme('colors.canvas') !important;
|
||||||
|
color: theme('colors.brand') !important;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 步骤 2:统一悬浮效果 (`index.css`)
|
||||||
|
|
||||||
|
- 移除 `stat-card` 的 `hover:-translate-y-0.5`,改为与 `section-card` 一致的仅阴影变化
|
||||||
|
- 移除 `.arco-btn-primary:hover` 的 `transform: translateY(-1px)`
|
||||||
|
- 统一所有悬浮阴影为同一级别
|
||||||
|
|
||||||
|
### 步骤 3:修复渐变色 (`layout/index.tsx`, `ProfileSummary.tsx`)
|
||||||
|
|
||||||
|
将 `to-blue-600` 改为 `to-brand`:
|
||||||
|
- `layout/index.tsx` 行 61:Logo 渐变
|
||||||
|
- `layout/index.tsx` 行 103:Avatar 渐变
|
||||||
|
- `ProfileSummary.tsx` 行 7:Avatar 渐变
|
||||||
|
|
||||||
|
### 步骤 4:修复登录页背景色 (`Login/index.tsx`, `LoginPanel.tsx`)
|
||||||
|
|
||||||
|
将 `bg-[#f7f8fa]` 改为 `bg-canvas`
|
||||||
|
|
||||||
|
### 步骤 5:统一页面结构 (`Role/index.tsx`, `Personnel/index.tsx`)
|
||||||
|
|
||||||
|
为 Role 和 Personnel 页面添加 `page-stack` 包裹:
|
||||||
|
```tsx
|
||||||
|
return (
|
||||||
|
<div className="page-stack">
|
||||||
|
<Panel ...>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 步骤 6:统一 Modal 宽度
|
||||||
|
|
||||||
|
所有包含表单的 Modal 统一使用 `style={{ width: 640 }}`:
|
||||||
|
- `Role/index.tsx`:新增角色 Modal 添加宽度
|
||||||
|
- `Personnel/index.tsx`:新增人员 Modal、分配角色 Modal 添加宽度
|
||||||
|
|
||||||
|
### 步骤 7:统一分页配置
|
||||||
|
|
||||||
|
所有 Table 的 pagination 统一添加 `sizeCanChange: true` 和 `showTotal: true`:
|
||||||
|
- `User/index.tsx`
|
||||||
|
- `Role/index.tsx`
|
||||||
|
- `Personnel/index.tsx`
|
||||||
|
- `Data/index.tsx`(3个表格)
|
||||||
|
|
||||||
|
### 步骤 8:统一状态标签颜色
|
||||||
|
|
||||||
|
禁用/停用状态统一使用 `gray` 而非 `red`:
|
||||||
|
- `User/index.tsx`:将 `'red'` 改为 `'gray'`
|
||||||
|
- `Personnel/index.tsx`:将 `'red'` 改为 `'gray'`
|
||||||
|
|
||||||
|
### 步骤 9:统一"新增"按钮颜色
|
||||||
|
|
||||||
|
`DramaList.tsx` 的"新增短剧"按钮移除 `status="success"`,使用默认蓝色主按钮。
|
||||||
|
|
||||||
|
### 步骤 10:统一 InputNumber 宽度
|
||||||
|
|
||||||
|
`Channel/index.tsx` 的 InputNumber 从 `style={{ width: 180 }}` 改为 `style={{ width: 200 }}`,与 Select 保持一致。
|
||||||
|
|
||||||
|
## 涉及文件清单
|
||||||
|
|
||||||
|
1. `tailwind.config.js` — 新增 secondary/header-bg/row-hover/border-hover token
|
||||||
|
2. `src/index.css` — 统一颜色引用、统一悬浮效果
|
||||||
|
3. `src/layout/index.tsx` — 修复渐变色 blue-600 → brand
|
||||||
|
4. `src/Profile/components/ProfileSummary.tsx` — 修复渐变色
|
||||||
|
5. `src/Login/index.tsx` — 背景色 canvas
|
||||||
|
6. `src/Login/components/LoginPanel.tsx` — 背景色 canvas
|
||||||
|
7. `src/Role/index.tsx` — 添加 page-stack、Modal 宽度、分页配置
|
||||||
|
8. `src/Personnel/index.tsx` — 添加 page-stack、Modal 宽度、分页配置、状态标签颜色
|
||||||
|
9. `src/User/index.tsx` — 分页配置、状态标签颜色
|
||||||
|
10. `src/Data/index.tsx` — 分页配置(3个表格)
|
||||||
|
11. `src/Drama/components/DramaList.tsx` — 按钮颜色
|
||||||
|
|
||||||
|
## 验证方式
|
||||||
|
|
||||||
|
1. `pnpm lint` — TypeScript 类型检查通过
|
||||||
|
2. `pnpm build` — 构建成功
|
||||||
|
3. 视觉检查:各页面悬浮效果、颜色、间距保持一致
|
||||||
@@ -84,10 +84,10 @@ export default function ChannelPage() {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Space size="large" align="start">
|
<Space size="large" align="start">
|
||||||
<Form.Item label="排序权重" field="sortOrder">
|
<Form.Item label="排序权重" field="sortOrder">
|
||||||
<InputNumber min={0} style={{ width: 180 }} />
|
<InputNumber min={0} style={{ width: 200 }} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label="状态" field="status">
|
<Form.Item label="状态" field="status">
|
||||||
<Select style={{ width: 180 }} disabled={Boolean(editingChannel?.isDefault)}>
|
<Select style={{ width: 200 }} disabled={Boolean(editingChannel?.isDefault)}>
|
||||||
<Select.Option value="active">启用</Select.Option>
|
<Select.Option value="active">启用</Select.Option>
|
||||||
<Select.Option value="disabled">停用</Select.Option>
|
<Select.Option value="disabled">停用</Select.Option>
|
||||||
</Select>
|
</Select>
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ function DataPage() {
|
|||||||
</Panel>
|
</Panel>
|
||||||
|
|
||||||
<Panel title="短剧数据" noPadding>
|
<Panel title="短剧数据" noPadding>
|
||||||
<Table rowKey="dramaId" loading={dramas.loading} data={dramas.data.list} pagination={{ current: dramas.data.pagination.page, total: dramas.data.pagination.total, pageSize: dramas.data.pagination.pageSize, onChange: dramas.changePage }} columns={[
|
<Table rowKey="dramaId" loading={dramas.loading} data={dramas.data.list} pagination={{ current: dramas.data.pagination.page, total: dramas.data.pagination.total, pageSize: dramas.data.pagination.pageSize, sizeCanChange: true, showTotal: true, onChange: dramas.changePage }} columns={[
|
||||||
{ title: '短剧ID', dataIndex: 'dramaId', width: 100 },
|
{ title: '短剧ID', dataIndex: 'dramaId', width: 100 },
|
||||||
{ title: '短剧名称', dataIndex: 'title' },
|
{ title: '短剧名称', dataIndex: 'title' },
|
||||||
{ title: '播放量', dataIndex: 'plays', width: 120 },
|
{ title: '播放量', dataIndex: 'plays', width: 120 },
|
||||||
@@ -68,7 +68,7 @@ function DataPage() {
|
|||||||
</Panel>
|
</Panel>
|
||||||
|
|
||||||
<Panel title="剧集数据" noPadding>
|
<Panel title="剧集数据" noPadding>
|
||||||
<Table rowKey="episodeId" loading={episodes.loading} data={episodes.data.list} pagination={{ current: episodes.data.pagination.page, total: episodes.data.pagination.total, pageSize: episodes.data.pagination.pageSize, onChange: episodes.changePage }} columns={[
|
<Table rowKey="episodeId" loading={episodes.loading} data={episodes.data.list} pagination={{ current: episodes.data.pagination.page, total: episodes.data.pagination.total, pageSize: episodes.data.pagination.pageSize, sizeCanChange: true, showTotal: true, onChange: episodes.changePage }} columns={[
|
||||||
{ title: '剧集ID', dataIndex: 'episodeId', width: 100 },
|
{ title: '剧集ID', dataIndex: 'episodeId', width: 100 },
|
||||||
{ title: '短剧ID', dataIndex: 'dramaId', width: 100 },
|
{ title: '短剧ID', dataIndex: 'dramaId', width: 100 },
|
||||||
{ title: '集数', dataIndex: 'episodeNo', width: 90 },
|
{ title: '集数', dataIndex: 'episodeNo', width: 90 },
|
||||||
@@ -81,7 +81,7 @@ function DataPage() {
|
|||||||
</Panel>
|
</Panel>
|
||||||
|
|
||||||
<Panel title="同步记录" noPadding>
|
<Panel title="同步记录" noPadding>
|
||||||
<Table rowKey="id" loading={jobs.loading} data={jobs.data.list} pagination={{ current: jobs.data.pagination.page, total: jobs.data.pagination.total, pageSize: jobs.data.pagination.pageSize, onChange: jobs.changePage }} columns={[
|
<Table rowKey="id" loading={jobs.loading} data={jobs.data.list} pagination={{ current: jobs.data.pagination.page, total: jobs.data.pagination.total, pageSize: jobs.data.pagination.pageSize, sizeCanChange: true, showTotal: true, onChange: jobs.changePage }} columns={[
|
||||||
{ title: '任务ID', dataIndex: 'jobId' },
|
{ title: '任务ID', dataIndex: 'jobId' },
|
||||||
{ title: '状态', dataIndex: 'status', width: 120 },
|
{ title: '状态', dataIndex: 'status', width: 120 },
|
||||||
{ title: '短剧数', dataIndex: 'dramaCount', width: 120 },
|
{ title: '短剧数', dataIndex: 'dramaCount', width: 120 },
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ export function DramaList() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="page-stack">
|
<div className="page-stack">
|
||||||
<Panel title="短剧管理" extra={<Button type="primary" status="success" icon={<IconPlus />} onClick={() => setVisible(true)}>新增短剧</Button>}>
|
<Panel title="短剧管理" extra={<Button type="primary" icon={<IconPlus />} onClick={() => setVisible(true)}>新增短剧</Button>}>
|
||||||
<Row gutter={16} align="center">
|
<Row gutter={16} align="center">
|
||||||
<Col span={6}>
|
<Col span={6}>
|
||||||
<div className="form-label">短剧标题</div>
|
<div className="form-label">短剧标题</div>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import type { LoginPanelProps } from "../interface";
|
|||||||
|
|
||||||
export const LoginPanel = ({ loading, 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-canvas bg-[image:linear-gradient(180deg,rgba(255,255,255,0.86),rgba(242,243,245,0.94))] p-12">
|
||||||
<div className="w-full max-w-[420px] rounded-2xl border border-line/40 bg-white p-10 shadow-login">
|
<div className="w-full max-w-[420px] rounded-2xl border border-line/40 bg-white p-10 shadow-login">
|
||||||
<div className="mb-8 flex flex-col items-center text-center">
|
<div className="mb-8 flex flex-col items-center text-center">
|
||||||
<h2 className="mb-2 w-full mt-0 text-[22px] font-bold text-ink">后台登录</h2>
|
<h2 className="mb-2 w-full mt-0 text-[22px] font-bold text-ink">后台登录</h2>
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ function LoginPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid min-h-screen grid-cols-1 bg-[#f7f8fa] min-[901px]:grid-cols-[minmax(520px,0.95fr)_minmax(420px,1fr)]">
|
<div className="grid min-h-screen grid-cols-1 bg-canvas min-[901px]:grid-cols-[minmax(520px,0.95fr)_minmax(420px,1fr)]">
|
||||||
<LoginVisual />
|
<LoginVisual />
|
||||||
<LoginPanel loading={loading} apiBaseUrl={authApi.getApiBaseUrl()} onSubmit={onSubmit} />
|
<LoginPanel loading={loading} apiBaseUrl={authApi.getApiBaseUrl()} onSubmit={onSubmit} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -66,14 +66,15 @@ function PersonnelPage() {
|
|||||||
));
|
));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div className="page-stack">
|
||||||
<Panel title="人员管理" extra={<Button type="primary" icon={<IconPlus />} onClick={openCreate}>新增人员</Button>} noPadding>
|
<Panel title="人员管理" extra={<Button type="primary" icon={<IconPlus />} onClick={openCreate}>新增人员</Button>} noPadding>
|
||||||
<Table rowKey="id" loading={loading} data={data.list} pagination={{ current: data.pagination.page, total: data.pagination.total, pageSize: data.pagination.pageSize, onChange: changePage }} columns={[
|
<Table rowKey="id" loading={loading} data={data.list} pagination={{ current: data.pagination.page, total: data.pagination.total, pageSize: data.pagination.pageSize, sizeCanChange: true, showTotal: true, onChange: changePage }} columns={[
|
||||||
{ title: 'ID', dataIndex: 'id', width: 80 },
|
{ title: 'ID', dataIndex: 'id', width: 80 },
|
||||||
{ title: '用户名', dataIndex: 'username' },
|
{ title: '用户名', dataIndex: 'username' },
|
||||||
{ title: '姓名', dataIndex: 'nickname' },
|
{ title: '姓名', dataIndex: 'nickname' },
|
||||||
{ title: '邮箱', dataIndex: 'email' },
|
{ title: '邮箱', dataIndex: 'email' },
|
||||||
{ title: '角色', render: (_, record: AdminUser) => <Space wrap>{record.isSuperAdmin ? <Tag color="gold">超级管理员</Tag> : record.roles.map((role) => <Tag key={role.id}>{role.name}</Tag>)}</Space> },
|
{ title: '角色', render: (_, record: AdminUser) => <Space wrap>{record.isSuperAdmin ? <Tag color="gold">超级管理员</Tag> : record.roles.map((role) => <Tag key={role.id}>{role.name}</Tag>)}</Space> },
|
||||||
{ title: '状态', dataIndex: 'status', width: 120, render: (status) => <Tag color={status === 'active' ? 'green' : 'red'}>{status === 'active' ? '启用' : '禁用'}</Tag> },
|
{ title: '状态', dataIndex: 'status', width: 120, render: (status) => <Tag color={status === 'active' ? 'green' : 'gray'}>{status === 'active' ? '启用' : '禁用'}</Tag> },
|
||||||
{ title: '创建时间', dataIndex: 'createdAt', width: 190 },
|
{ title: '创建时间', dataIndex: 'createdAt', width: 190 },
|
||||||
{
|
{
|
||||||
title: '操作',
|
title: '操作',
|
||||||
@@ -88,7 +89,7 @@ function PersonnelPage() {
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
]} />
|
]} />
|
||||||
<Modal title="新增人员" visible={createVisible} onCancel={() => setCreateVisible(false)} footer={null}>
|
<Modal title="新增人员" visible={createVisible} onCancel={() => setCreateVisible(false)} footer={null} style={{ width: 640 }}>
|
||||||
<Form form={createForm} layout="vertical" onSubmit={createPersonnel}>
|
<Form form={createForm} layout="vertical" onSubmit={createPersonnel}>
|
||||||
<Form.Item label="用户名" field="username" rules={[{ required: true }]}>
|
<Form.Item label="用户名" field="username" rules={[{ required: true }]}>
|
||||||
<Input placeholder="请输入登录用户名" />
|
<Input placeholder="请输入登录用户名" />
|
||||||
@@ -119,6 +120,7 @@ function PersonnelPage() {
|
|||||||
confirmLoading={assigning}
|
confirmLoading={assigning}
|
||||||
onOk={saveAssignedRoles}
|
onOk={saveAssignedRoles}
|
||||||
onCancel={() => setAssignVisible(false)}
|
onCancel={() => setAssignVisible(false)}
|
||||||
|
style={{ width: 640 }}
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
mode="multiple"
|
mode="multiple"
|
||||||
@@ -131,6 +133,7 @@ function PersonnelPage() {
|
|||||||
</Select>
|
</Select>
|
||||||
</Modal>
|
</Modal>
|
||||||
</Panel>
|
</Panel>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ 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-5 rounded-2xl bg-gradient-to-br from-brand/5 to-brand/10 p-6 ring-1 ring-brand/10">
|
<div className="mb-6 flex items-center gap-5 rounded-2xl bg-gradient-to-br from-brand/5 to-brand/10 p-6 ring-1 ring-brand/10">
|
||||||
<Avatar size={64} className="!bg-gradient-to-br !from-brand !to-blue-600 !text-white !text-xl !font-bold !shadow-lg !shadow-brand/30">
|
<Avatar size={64} className="!bg-brand !text-white !text-xl !font-bold !shadow-lg !shadow-brand/30">
|
||||||
{profile?.username?.slice(0, 1).toUpperCase() || "A"}
|
{profile?.username?.slice(0, 1).toUpperCase() || "A"}
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ function RolesPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div className="page-stack">
|
||||||
<Panel
|
<Panel
|
||||||
title="角色管理"
|
title="角色管理"
|
||||||
extra={
|
extra={
|
||||||
@@ -79,6 +80,8 @@ function RolesPage() {
|
|||||||
current: data.pagination.page,
|
current: data.pagination.page,
|
||||||
total: data.pagination.total,
|
total: data.pagination.total,
|
||||||
pageSize: data.pagination.pageSize,
|
pageSize: data.pagination.pageSize,
|
||||||
|
sizeCanChange: true,
|
||||||
|
showTotal: true,
|
||||||
onChange: changePage,
|
onChange: changePage,
|
||||||
}}
|
}}
|
||||||
columns={[
|
columns={[
|
||||||
@@ -106,7 +109,7 @@ function RolesPage() {
|
|||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<Modal title="新增角色" visible={createVisible} onCancel={() => setCreateVisible(false)} footer={null}>
|
<Modal title="新增角色" visible={createVisible} onCancel={() => setCreateVisible(false)} footer={null} style={{ width: 640 }}>
|
||||||
<Form form={createForm} layout="vertical" onSubmit={createRole}>
|
<Form form={createForm} layout="vertical" onSubmit={createRole}>
|
||||||
<Form.Item label="角色编码" field="code" rules={[{ required: true }]}>
|
<Form.Item label="角色编码" field="code" rules={[{ required: true }]}>
|
||||||
<Input placeholder="请输入角色编码,例如 drama_operator" />
|
<Input placeholder="请输入角色编码,例如 drama_operator" />
|
||||||
@@ -135,10 +138,12 @@ function RolesPage() {
|
|||||||
confirmLoading={savingPermissions}
|
confirmLoading={savingPermissions}
|
||||||
onOk={saveRolePermissions}
|
onOk={saveRolePermissions}
|
||||||
onCancel={() => setAssignmentVisible(false)}
|
onCancel={() => setAssignmentVisible(false)}
|
||||||
|
style={{ width: 640 }}
|
||||||
>
|
>
|
||||||
<PermissionTreePicker permissions={permissions.data.list} value={selectedPermissionIds} onChange={setSelectedPermissionIds} />
|
<PermissionTreePicker permissions={permissions.data.list} value={selectedPermissionIds} onChange={setSelectedPermissionIds} />
|
||||||
</Modal>
|
</Modal>
|
||||||
</Panel>
|
</Panel>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,11 +15,11 @@ function UsersPage() {
|
|||||||
return (
|
return (
|
||||||
<div className="page-stack">
|
<div className="page-stack">
|
||||||
<Panel title="小程序用户管理" noPadding>
|
<Panel title="小程序用户管理" noPadding>
|
||||||
<Table rowKey="id" loading={loading} data={data.list} pagination={{ current: data.pagination.page, total: data.pagination.total, pageSize: data.pagination.pageSize, onChange: changePage }} columns={[
|
<Table rowKey="id" loading={loading} data={data.list} pagination={{ current: data.pagination.page, total: data.pagination.total, pageSize: data.pagination.pageSize, sizeCanChange: true, showTotal: true, onChange: changePage }} columns={[
|
||||||
{ title: 'ID', dataIndex: 'id', width: 80 },
|
{ title: 'ID', dataIndex: 'id', width: 80 },
|
||||||
{ title: 'TikTok Open ID', dataIndex: 'tiktokOpenId' },
|
{ title: 'TikTok Open ID', dataIndex: 'tiktokOpenId' },
|
||||||
{ title: '昵称', dataIndex: 'nickname' },
|
{ title: '昵称', dataIndex: 'nickname' },
|
||||||
{ title: '状态', dataIndex: 'status', width: 120, render: (status) => <Tag color={status === 'active' ? 'green' : 'red'}>{status === 'active' ? '启用' : '禁用'}</Tag> },
|
{ title: '状态', dataIndex: 'status', width: 120, render: (status) => <Tag color={status === 'active' ? 'green' : 'gray'}>{status === 'active' ? '启用' : '禁用'}</Tag> },
|
||||||
{ title: '操作', width: 160, render: (_, record: AppUser) => <Button size="small" onClick={() => update状态(record.id, record.status === 'active' ? 'disabled' : 'active')}>{record.status === 'active' ? '禁用' : '启用'}</Button> },
|
{ title: '操作', width: 160, render: (_, record: AppUser) => <Button size="small" onClick={() => update状态(record.id, record.status === 'active' ? 'disabled' : 'active')}>{record.status === 'active' ? '禁用' : '启用'}</Button> },
|
||||||
]} />
|
]} />
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.section-card:hover {
|
.section-card:hover {
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
box-shadow: theme('boxShadow.card-hover');
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-stack {
|
.page-stack {
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.form-label {
|
.form-label {
|
||||||
@apply mb-2 text-sm font-medium text-ink;
|
@apply mb-2 text-sm font-medium;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-help {
|
.form-help {
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.stat-card {
|
.stat-card {
|
||||||
@apply rounded-xl border border-line/60 bg-white p-5 shadow-sm transition-all duration-300 hover:shadow-lg hover:-translate-y-0.5 cursor-default;
|
@apply rounded-xl border border-line/60 bg-white p-5 shadow-sm transition-shadow duration-300 hover:shadow-card-hover cursor-default;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,12 +66,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.arco-menu-item:hover {
|
.arco-menu-item:hover {
|
||||||
background-color: #f2f3f5 !important;
|
background-color: theme('colors.canvas') !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.arco-menu-item.arco-menu-selected {
|
.arco-menu-item.arco-menu-selected {
|
||||||
background-color: #e8f3ff !important;
|
background-color: theme('colors.brand-light') !important;
|
||||||
color: #165dff !important;
|
color: theme('colors.brand') !important;
|
||||||
font-weight: 600 !important;
|
font-weight: 600 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,29 +80,29 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.arco-menu-inline-header:hover {
|
.arco-menu-inline-header:hover {
|
||||||
background-color: #f2f3f5 !important;
|
background-color: theme('colors.canvas') !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Card ───────────────────────────────────────────── */
|
/* ── Card ───────────────────────────────────────────── */
|
||||||
.arco-card {
|
.arco-card {
|
||||||
border: 1px solid rgba(229, 230, 235, 0.6) !important;
|
border: 1px solid theme('colors.line / 60%') !important;
|
||||||
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.04) !important;
|
box-shadow: theme('boxShadow.card') !important;
|
||||||
transition: box-shadow 0.3s ease !important;
|
transition: box-shadow 0.3s ease !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.arco-card:hover {
|
.arco-card:hover {
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06) !important;
|
box-shadow: theme('boxShadow.card-hover') !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.arco-card-header {
|
.arco-card-header {
|
||||||
border-bottom: 1px solid rgba(229, 230, 235, 0.5) !important;
|
border-bottom: 1px solid theme('colors.line / 50%') !important;
|
||||||
padding: 14px 20px !important;
|
padding: 14px 20px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.arco-card-header-title {
|
.arco-card-header-title {
|
||||||
font-weight: 600 !important;
|
font-weight: 600 !important;
|
||||||
font-size: 15px !important;
|
font-size: 15px !important;
|
||||||
color: #1d2129 !important;
|
color: theme('colors.ink') !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Table ──────────────────────────────────────────── */
|
/* ── Table ──────────────────────────────────────────── */
|
||||||
@@ -111,12 +111,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.arco-table-th {
|
.arco-table-th {
|
||||||
background-color: #fafbfc !important;
|
background-color: theme('colors.header-bg') !important;
|
||||||
font-weight: 600 !important;
|
font-weight: 600 !important;
|
||||||
color: #4e5969 !important;
|
color: theme('colors.secondary') !important;
|
||||||
font-size: 13px !important;
|
font-size: 13px !important;
|
||||||
letter-spacing: 0.01em;
|
letter-spacing: 0.01em;
|
||||||
border-bottom: 1px solid rgba(229, 230, 235, 0.8) !important;
|
border-bottom: 1px solid theme('colors.line / 80%') !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.arco-table-td {
|
.arco-table-td {
|
||||||
@@ -124,7 +124,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.arco-table-tr:hover .arco-table-td {
|
.arco-table-tr:hover .arco-table-td {
|
||||||
background-color: #f7f8fa !important;
|
background-color: theme('colors.row-hover') !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.arco-table-sticky-header .arco-table-container {
|
.arco-table-sticky-header .arco-table-container {
|
||||||
@@ -142,16 +142,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.arco-pagination-item:hover {
|
.arco-pagination-item:hover {
|
||||||
background-color: #e8f3ff !important;
|
background-color: theme('colors.brand-light') !important;
|
||||||
color: #165dff !important;
|
color: theme('colors.brand') !important;
|
||||||
border-color: #bedaff !important;
|
border-color: theme('colors.brand / 40%') !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.arco-pagination-item-active {
|
.arco-pagination-item-active {
|
||||||
background-color: #165dff !important;
|
background-color: theme('colors.brand') !important;
|
||||||
border-color: #165dff !important;
|
border-color: theme('colors.brand') !important;
|
||||||
color: #fff !important;
|
color: #fff !important;
|
||||||
box-shadow: 0 2px 6px rgba(22, 93, 251, 0.3) !important;
|
box-shadow: 0 2px 6px theme('colors.brand / 30%') !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Button ─────────────────────────────────────────── */
|
/* ── Button ─────────────────────────────────────────── */
|
||||||
@@ -161,25 +161,24 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.arco-btn-primary {
|
.arco-btn-primary {
|
||||||
box-shadow: 0 2px 6px rgba(22, 93, 251, 0.3) !important;
|
box-shadow: 0 2px 6px theme('colors.brand / 30%') !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.arco-btn-primary:hover {
|
.arco-btn-primary:hover {
|
||||||
box-shadow: 0 4px 12px rgba(22, 93, 251, 0.4) !important;
|
box-shadow: 0 4px 12px theme('colors.brand / 40%') !important;
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.arco-btn-secondary:not(.arco-btn-disabled):hover {
|
.arco-btn-secondary:not(.arco-btn-disabled):hover {
|
||||||
background-color: #f2f3f5 !important;
|
background-color: theme('colors.canvas') !important;
|
||||||
border-color: #c9cdd4 !important;
|
border-color: theme('colors.line-hover') !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.arco-btn-text:not(.arco-btn-disabled):hover {
|
.arco-btn-text:not(.arco-btn-disabled):hover {
|
||||||
background-color: #f2f3f5 !important;
|
background-color: theme('colors.canvas') !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.arco-btn-text-danger:not(.arco-btn-disabled):hover {
|
.arco-btn-text-danger:not(.arco-btn-disabled):hover {
|
||||||
background-color: #fff1f0 !important;
|
background-color: theme('colors.danger-light') !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Modal ──────────────────────────────────────────── */
|
/* ── Modal ──────────────────────────────────────────── */
|
||||||
@@ -188,7 +187,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.arco-modal-header {
|
.arco-modal-header {
|
||||||
border-bottom: 1px solid rgba(229, 230, 235, 0.6) !important;
|
border-bottom: 1px solid theme('colors.line / 60%') !important;
|
||||||
padding: 16px 24px !important;
|
padding: 16px 24px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,7 +201,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.arco-modal-footer {
|
.arco-modal-footer {
|
||||||
border-top: 1px solid rgba(229, 230, 235, 0.6) !important;
|
border-top: 1px solid theme('colors.line / 60%') !important;
|
||||||
padding: 12px 24px !important;
|
padding: 12px 24px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,12 +211,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.arco-input-wrapper:hover {
|
.arco-input-wrapper:hover {
|
||||||
border-color: #c9cdd4 !important;
|
border-color: theme('colors.line-hover') !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.arco-input-wrapper.arco-input-focus {
|
.arco-input-wrapper.arco-input-focus {
|
||||||
border-color: #165dff !important;
|
border-color: theme('colors.brand') !important;
|
||||||
box-shadow: 0 0 0 2px rgba(22, 93, 251, 0.1) !important;
|
box-shadow: 0 0 0 2px theme('colors.brand / 10%') !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.arco-select-view {
|
.arco-select-view {
|
||||||
@@ -225,12 +224,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.arco-select-view:hover {
|
.arco-select-view:hover {
|
||||||
border-color: #c9cdd4 !important;
|
border-color: theme('colors.line-hover') !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.arco-select-view.arco-select-view-focus {
|
.arco-select-view.arco-select-view-focus {
|
||||||
border-color: #165dff !important;
|
border-color: theme('colors.brand') !important;
|
||||||
box-shadow: 0 0 0 2px rgba(22, 93, 251, 0.1) !important;
|
box-shadow: 0 0 0 2px theme('colors.brand / 10%') !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.arco-textarea-wrapper {
|
.arco-textarea-wrapper {
|
||||||
@@ -238,18 +237,18 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.arco-textarea-wrapper:hover {
|
.arco-textarea-wrapper:hover {
|
||||||
border-color: #c9cdd4 !important;
|
border-color: theme('colors.line-hover') !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.arco-textarea-focus {
|
.arco-textarea-focus {
|
||||||
border-color: #165dff !important;
|
border-color: theme('colors.brand') !important;
|
||||||
box-shadow: 0 0 0 2px rgba(22, 93, 251, 0.1) !important;
|
box-shadow: 0 0 0 2px theme('colors.brand / 10%') !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Form ───────────────────────────────────────────── */
|
/* ── Form ───────────────────────────────────────────── */
|
||||||
.arco-form-item-label {
|
.arco-form-item-label {
|
||||||
font-weight: 500 !important;
|
font-weight: 500 !important;
|
||||||
color: #4e5969 !important;
|
color: theme('colors.secondary') !important;
|
||||||
font-size: 13px !important;
|
font-size: 13px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -262,12 +261,12 @@
|
|||||||
/* ── Descriptions ───────────────────────────────────── */
|
/* ── Descriptions ───────────────────────────────────── */
|
||||||
.arco-descriptions-item-label {
|
.arco-descriptions-item-label {
|
||||||
font-weight: 500 !important;
|
font-weight: 500 !important;
|
||||||
color: #86909c !important;
|
color: theme('colors.muted') !important;
|
||||||
background-color: #fafbfc !important;
|
background-color: theme('colors.header-bg') !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.arco-descriptions-item-value {
|
.arco-descriptions-item-value {
|
||||||
color: #1d2129 !important;
|
color: theme('colors.ink') !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Dropdown ───────────────────────────────────────── */
|
/* ── Dropdown ───────────────────────────────────────── */
|
||||||
@@ -280,7 +279,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.arco-dropdown-menu-item:hover {
|
.arco-dropdown-menu-item:hover {
|
||||||
background-color: #f2f3f5 !important;
|
background-color: theme('colors.canvas') !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Switch ─────────────────────────────────────────── */
|
/* ── Switch ─────────────────────────────────────────── */
|
||||||
@@ -289,5 +288,5 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.arco-switch-checked {
|
.arco-switch-checked {
|
||||||
box-shadow: 0 2px 4px rgba(22, 93, 251, 0.3) !important;
|
box-shadow: 0 2px 4px theme('colors.brand / 30%') !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export function AdminLayout() {
|
|||||||
trigger={null}
|
trigger={null}
|
||||||
>
|
>
|
||||||
<div className="flex h-[60px] items-center gap-2.5 px-5">
|
<div className="flex h-[60px] items-center gap-2.5 px-5">
|
||||||
<span className="grid h-9 w-9 place-items-center rounded-xl bg-gradient-to-br from-brand to-blue-600 text-sm font-bold text-white shadow-md shadow-brand/25">TK</span>
|
<span className="grid h-9 w-9 place-items-center rounded-xl bg-brand text-sm font-bold text-white shadow-md shadow-brand/25">TK</span>
|
||||||
{!collapsed && <span className="text-[15px] font-bold tracking-tight text-ink">TK短剧管理后台</span>}
|
{!collapsed && <span className="text-[15px] font-bold tracking-tight text-ink">TK短剧管理后台</span>}
|
||||||
</div>
|
</div>
|
||||||
<div className="mx-3 mb-2 h-px bg-line/50" />
|
<div className="mx-3 mb-2 h-px bg-line/50" />
|
||||||
@@ -99,8 +99,8 @@ export function AdminLayout() {
|
|||||||
</Menu>
|
</Menu>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<button className="inline-flex cursor-pointer items-center gap-2.5 rounded-xl border border-transparent bg-transparent px-3 py-2 text-ink transition-all duration-200 hover:border-line/60 hover:bg-canvas/80" type="button">
|
<button className="inline-flex cursor-pointer items-center gap-2.5 rounded-xl border border-transparent bg-transparent px-3 py-2 text-ink transition-all duration-200 hover:bg-canvas/80" type="button">
|
||||||
<Avatar size={32} className="!bg-gradient-to-br !from-brand !to-blue-600 !text-white !text-[13px] !font-semibold !shadow-sm !shadow-brand/20">{profile?.username?.slice(0, 1).toUpperCase() || "A"}</Avatar>
|
<Avatar size={32} className="!bg-brand !text-white !text-[13px] !font-semibold !shadow-sm !shadow-brand/20">{profile?.username?.slice(0, 1).toUpperCase() || "A"}</Avatar>
|
||||||
<Text className="!text-[13px] !font-medium">{profile?.nickname || profile?.username || "管理员"}</Text>
|
<Text className="!text-[13px] !font-medium">{profile?.nickname || profile?.username || "管理员"}</Text>
|
||||||
</button>
|
</button>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
|||||||
@@ -9,9 +9,13 @@ export default {
|
|||||||
extend: {
|
extend: {
|
||||||
colors: {
|
colors: {
|
||||||
ink: '#1d2129',
|
ink: '#1d2129',
|
||||||
|
secondary: '#4e5969',
|
||||||
muted: '#86909c',
|
muted: '#86909c',
|
||||||
line: '#e5e6eb',
|
line: '#e5e6eb',
|
||||||
|
'line-hover': '#c9cdd4',
|
||||||
canvas: '#f2f3f5',
|
canvas: '#f2f3f5',
|
||||||
|
'row-hover': '#f7f8fa',
|
||||||
|
'header-bg': '#fafbfc',
|
||||||
panel: '#fbfcfd',
|
panel: '#fbfcfd',
|
||||||
brand: '#165dff',
|
brand: '#165dff',
|
||||||
'brand-light': '#e8f3ff',
|
'brand-light': '#e8f3ff',
|
||||||
|
|||||||
Reference in New Issue
Block a user