updata:admin.css替用
This commit is contained in:
@@ -1,6 +1,13 @@
|
||||
import type { PlatformPermission } from "../../services/api";
|
||||
|
||||
import { Checkbox } from "~/components/ui/checkbox";
|
||||
import {
|
||||
adminEmptyClass,
|
||||
permissionGroupClass,
|
||||
permissionGroupTitleClass,
|
||||
permissionGroupsClass,
|
||||
permissionItemClassName,
|
||||
} from "./adminUi";
|
||||
|
||||
/** 模块编码 → 展示名(用于权限分组标题)。 */
|
||||
const MODULE_LABELS: Record<string, string> = {
|
||||
@@ -10,7 +17,7 @@ const MODULE_LABELS: Record<string, string> = {
|
||||
system: "系统管理",
|
||||
};
|
||||
|
||||
/** 勾选框样式:复刻原生 checkbox 外观(白底、灰边、蓝色勾选),并对齐 `.permission-item input` 的 3px 顶部偏移。 */
|
||||
/** 勾选框样式:复刻原生 checkbox 外观(白底、灰边、蓝色勾选),并对齐 permission item 的 3px 顶部偏移。 */
|
||||
const CHECKBOX_CLASS =
|
||||
"mt-[3px] size-[16px] rounded-[3px] border-[#cdd9e4] bg-white disabled:opacity-100 data-checked:border-[#1277d3] data-checked:bg-[#1277d3] data-checked:text-white";
|
||||
|
||||
@@ -41,16 +48,16 @@ export function PermissionCheckboxList({
|
||||
);
|
||||
|
||||
if (permissions.length === 0) {
|
||||
return <p className="admin-empty">暂无可用权限</p>;
|
||||
return <p className={adminEmptyClass}>暂无可用权限</p>;
|
||||
}
|
||||
|
||||
const isAdmin = targetRoleCode === "admin";
|
||||
|
||||
return (
|
||||
<div className="permission-groups">
|
||||
<div className={permissionGroupsClass}>
|
||||
{Object.entries(groups).map(([module, items]) => (
|
||||
<div className="permission-group" key={module}>
|
||||
<div className="permission-group__title">
|
||||
<div className={permissionGroupClass} key={module}>
|
||||
<div className={permissionGroupTitleClass}>
|
||||
{MODULE_LABELS[module] ?? module}
|
||||
</div>
|
||||
{items.map((permission) => {
|
||||
@@ -61,7 +68,7 @@ export function PermissionCheckboxList({
|
||||
const checked = selected.includes(code);
|
||||
return (
|
||||
<label
|
||||
className={`permission-item${locked ? " is-locked" : ""}`}
|
||||
className={permissionItemClassName(locked)}
|
||||
key={code}
|
||||
>
|
||||
<Checkbox
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { modalFormClass } from "../platform/modalUi";
|
||||
import { PermissionCheckboxList } from "./PermissionCheckboxList";
|
||||
import { roleErrorClass } from "./adminUi";
|
||||
|
||||
export function PermissionEditDialog({
|
||||
open,
|
||||
@@ -91,7 +92,7 @@ export function PermissionEditDialog({
|
||||
>
|
||||
<div className={modalFormClass}>
|
||||
{target?.role_code === "admin" && (
|
||||
<p className="role-error">admin 角色必须保留 system:view 权限。</p>
|
||||
<p className={roleErrorClass}>admin 角色必须保留 system:view 权限。</p>
|
||||
)}
|
||||
{target ? (
|
||||
<PermissionCheckboxList
|
||||
|
||||
@@ -17,8 +17,20 @@ import { AdminColgroup, USER_PROJECT_COL_WIDTHS } from "./AdminTable";
|
||||
import { ProjectEditDialog } from "./ProjectEditDialog";
|
||||
import { ImportMemberDialog } from "./ImportMemberDialog";
|
||||
import { ProjectMembersDrawer } from "./ProjectMembersDrawer";
|
||||
|
||||
import "../../styles/admin.css";
|
||||
import {
|
||||
adminEmptyClass,
|
||||
adminPageClass,
|
||||
adminReadonlyClass,
|
||||
adminToolbarClass,
|
||||
adminToolbarInputClass,
|
||||
adminToolbarSearchClass,
|
||||
employeeActionsClass,
|
||||
employeeAvatarClass,
|
||||
employeeNameClass,
|
||||
rowButtonClass,
|
||||
rowDangerButtonClass,
|
||||
statusPillClass,
|
||||
} from "./adminUi";
|
||||
|
||||
const EMPTY_PROJECT_FORM = {
|
||||
workspace_code: "",
|
||||
@@ -242,14 +254,14 @@ export function ProjectManagementPage({
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="admin-page">
|
||||
<div className="admin-toolbar">
|
||||
<div className="admin-toolbar__search">
|
||||
<section className={adminPageClass}>
|
||||
<div className={adminToolbarClass}>
|
||||
<div className={adminToolbarSearchClass}>
|
||||
<Search size={14} />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="请输入项目名称"
|
||||
className="admin-toolbar__input"
|
||||
className={adminToolbarInputClass}
|
||||
value={projectSearchTerm}
|
||||
onChange={(event) => setProjectSearchTerm(event.target.value)}
|
||||
/>
|
||||
@@ -267,7 +279,7 @@ export function ProjectManagementPage({
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{!canManage && <div className="admin-readonly">当前为开发人员,只能查看项目列表。</div>}
|
||||
{!canManage && <div className={adminReadonlyClass}>当前为开发人员,只能查看项目列表。</div>}
|
||||
<Table className="rounded-[9px] border border-[#dfe7ef] overflow-hidden text-[11px] text-[#44576a] [--border-color:#dfe7ef] table-fixed bg-white">
|
||||
<AdminColgroup widths={USER_PROJECT_COL_WIDTHS} />
|
||||
<TableHeader className="bg-[#f5f8fb] text-[#748598]">
|
||||
@@ -283,13 +295,13 @@ export function ProjectManagementPage({
|
||||
{projectLoading ? (
|
||||
<TableRow className="min-h-[64px] border-t border-[#edf1f5] hover:bg-[#f7fafc] data-[state=selected]:bg-transparent">
|
||||
<TableCell colSpan={5} className="p-0">
|
||||
<p className="admin-empty">正在加载项目…</p>
|
||||
<p className={adminEmptyClass}>正在加载项目…</p>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : projects.length === 0 ? (
|
||||
<TableRow className="min-h-[64px] border-t border-[#edf1f5] hover:bg-[#f7fafc] data-[state=selected]:bg-transparent">
|
||||
<TableCell colSpan={5} className="p-0">
|
||||
<p className="admin-empty">暂无项目</p>
|
||||
<p className={adminEmptyClass}>暂无项目</p>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
@@ -306,8 +318,8 @@ export function ProjectManagementPage({
|
||||
.map((project) => (
|
||||
<TableRow key={project.workspace_id} className="min-h-[64px] border-t border-[#edf1f5] hover:bg-[#f7fafc] data-[state=selected]:bg-transparent">
|
||||
<TableCell className="p-3 px-4 align-middle">
|
||||
<span className="employee-name">
|
||||
<b className="avatar">{project.workspace_name.slice(0, 1)}</b>
|
||||
<span className={employeeNameClass}>
|
||||
<b className={employeeAvatarClass}>{project.workspace_name.slice(0, 1)}</b>
|
||||
<span>
|
||||
<strong>{project.workspace_name}</strong>
|
||||
<small>{project.description ?? "无描述"}</small>
|
||||
@@ -317,26 +329,45 @@ export function ProjectManagementPage({
|
||||
<TableCell className="p-3 px-4 align-middle"><code>{project.workspace_code}</code></TableCell>
|
||||
<TableCell className="p-3 px-4 align-middle">
|
||||
<span>
|
||||
<span className={`status-pill is-${project.status}`}>
|
||||
<span className={statusPillClass(project.status)}>
|
||||
{project.status === "active" ? "正常" : project.status === "archived" ? "已归档" : "已删除"}
|
||||
</span>
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="p-3 px-4 align-middle"><span>{project.quota_bytes > 0 ? `${(project.quota_bytes / 1024 / 1024 / 1024).toFixed(1)} GB` : "无限制"}</span></TableCell>
|
||||
<TableCell className="p-3 px-4 align-middle">
|
||||
<span className="employee-actions">
|
||||
<button type="button" disabled={!canManage} onClick={() => openMembersDrawer(project)}>成员</button>
|
||||
<button type="button" disabled={!canManage} onClick={() => openEditProject(project)}>编辑</button>
|
||||
{/* <button type="button" disabled={!canManage} onClick={() => openImportMemberDialog(project)}>导入成员</button> */}
|
||||
<button
|
||||
<span className={employeeActionsClass}>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="xs"
|
||||
type="button"
|
||||
disabled={!canManage}
|
||||
onClick={() => openMembersDrawer(project)}
|
||||
className={rowButtonClass}
|
||||
>
|
||||
成员
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="xs"
|
||||
type="button"
|
||||
disabled={!canManage}
|
||||
onClick={() => openEditProject(project)}
|
||||
className={rowButtonClass}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="xs"
|
||||
type="button"
|
||||
className="is-danger"
|
||||
disabled={!canManage || project.status === "disabled"}
|
||||
title={project.status === "disabled" ? "已删除的项目不能操作" : "删除项目"}
|
||||
onClick={() => setDeleteTarget(project)}
|
||||
className={rowDangerButtonClass}
|
||||
>
|
||||
删除
|
||||
</button>
|
||||
</Button>
|
||||
</span>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
@@ -19,6 +19,12 @@ import {
|
||||
modalFormClass,
|
||||
} from "../platform/modalUi";
|
||||
import { PermissionCheckboxList } from "./PermissionCheckboxList";
|
||||
import {
|
||||
formHintClass,
|
||||
permissionGroupTitleClass,
|
||||
permissionGroupsClass,
|
||||
roleErrorClass,
|
||||
} from "./adminUi";
|
||||
|
||||
const RESERVED_ROLE_CODES = new Set(["admin", "developer"]);
|
||||
const ROLE_CODE_PATTERN = /^[a-z][a-z0-9_-]{1,63}$/;
|
||||
@@ -153,7 +159,7 @@ export function RoleEditDialog({
|
||||
eyebrow="ROLE"
|
||||
title={editing ? "编辑角色" : "新建角色"}
|
||||
>
|
||||
<form className={`${modalFormClass} role-create-form`} onSubmit={(event) => void handleSubmit(event)}>
|
||||
<form className={modalFormClass} onSubmit={(event) => void handleSubmit(event)}>
|
||||
{editing ? (
|
||||
<label className={formFieldClass}>
|
||||
<span>角色编码</span>
|
||||
@@ -169,10 +175,10 @@ export function RoleEditDialog({
|
||||
onChange={handleRoleCodeChange}
|
||||
placeholder="例如:qa_engineer"
|
||||
/>
|
||||
<span className="form-hint">
|
||||
<span className={formHintClass}>
|
||||
以小写字母开头,仅含小写字母/数字/下划线/连字符(2~64 字符)
|
||||
</span>
|
||||
{roleCodeError && <span className="role-error">{roleCodeError}</span>}
|
||||
{roleCodeError && <span className={roleErrorClass}>{roleCodeError}</span>}
|
||||
</label>
|
||||
)}
|
||||
<label className={formFieldClass}>
|
||||
@@ -185,7 +191,7 @@ export function RoleEditDialog({
|
||||
placeholder="请输入角色名称"
|
||||
/>
|
||||
{editing !== null && editing.is_builtin && (
|
||||
<span className="form-hint">内置角色名称不可修改,描述可编辑</span>
|
||||
<span className={formHintClass}>内置角色名称不可修改,描述可编辑</span>
|
||||
)}
|
||||
</label>
|
||||
<label className={formFieldClass}>
|
||||
@@ -198,8 +204,8 @@ export function RoleEditDialog({
|
||||
/>
|
||||
</label>
|
||||
{!editing && (
|
||||
<div className="permission-groups">
|
||||
<div className="permission-group__title">初始权限</div>
|
||||
<div className={permissionGroupsClass}>
|
||||
<div className={permissionGroupTitleClass}>初始权限</div>
|
||||
<PermissionCheckboxList
|
||||
permissions={permissions}
|
||||
selected={form.permission_codes}
|
||||
|
||||
@@ -22,8 +22,21 @@ import {
|
||||
TableRow,
|
||||
} from "~/components/ui/table";
|
||||
import { AdminColgroup, ROLE_COL_WIDTHS } from "./AdminTable";
|
||||
|
||||
import "../../styles/admin.css";
|
||||
import {
|
||||
adminEmptyClass,
|
||||
adminPageClass,
|
||||
adminReadonlyClass,
|
||||
adminToolbarClass,
|
||||
adminToolbarInputClass,
|
||||
adminToolbarSearchClass,
|
||||
builtinBadgeClass,
|
||||
permissionCountClass,
|
||||
roleActionsClass,
|
||||
roleCodeClass,
|
||||
roleNameClass,
|
||||
rowButtonClass,
|
||||
rowDangerButtonClass,
|
||||
} from "./adminUi";
|
||||
|
||||
const PRIMARY_BUTTON_CLASS =
|
||||
"h-[37px] gap-[7px] rounded-md border border-[#126ac3] bg-gradient-to-br from-[#1881e7] to-[#1268c9] px-[15px] text-xs font-[650] text-white shadow-[0_6px_15px_rgb(20_111_202/18%)] enabled:hover:from-[#1175d8] enabled:hover:to-[#0e5bad]";
|
||||
@@ -177,14 +190,14 @@ export function RoleManagementPage({
|
||||
});
|
||||
|
||||
return (
|
||||
<section className="admin-page">
|
||||
<div className="admin-toolbar">
|
||||
<div className="admin-toolbar__search">
|
||||
<section className={adminPageClass}>
|
||||
<div className={adminToolbarClass}>
|
||||
<div className={adminToolbarSearchClass}>
|
||||
<Search size={14} />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="搜索角色编码 / 名称"
|
||||
className="admin-toolbar__input"
|
||||
className={adminToolbarInputClass}
|
||||
value={searchTerm}
|
||||
onChange={(event) => setSearchTerm(event.target.value)}
|
||||
/>
|
||||
@@ -203,9 +216,9 @@ export function RoleManagementPage({
|
||||
</div>
|
||||
|
||||
{!canManage && (
|
||||
<div className="admin-readonly">当前为开发人员,只能查看角色列表。</div>
|
||||
<div className={adminReadonlyClass}>当前为开发人员,只能查看角色列表。</div>
|
||||
)}
|
||||
{loadError && <div className="admin-readonly">{loadError}</div>}
|
||||
{loadError && <div className={adminReadonlyClass}>{loadError}</div>}
|
||||
|
||||
<Table className="rounded-[9px] border border-[#dfe7ef] overflow-hidden text-[11px] text-[#44576a] [--border-color:#dfe7ef] table-fixed bg-white">
|
||||
<AdminColgroup widths={ROLE_COL_WIDTHS} />
|
||||
@@ -222,13 +235,13 @@ export function RoleManagementPage({
|
||||
{loading ? (
|
||||
<TableRow className="min-h-[60px] border-t border-[#edf1f5] hover:bg-[#f7fafc] data-[state=selected]:bg-transparent">
|
||||
<TableCell colSpan={5} className="p-0">
|
||||
<p className="admin-empty">正在加载角色…</p>
|
||||
<p className={adminEmptyClass}>正在加载角色…</p>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : filteredRoles.length === 0 ? (
|
||||
<TableRow className="min-h-[60px] border-t border-[#edf1f5] hover:bg-[#f7fafc] data-[state=selected]:bg-transparent">
|
||||
<TableCell colSpan={5} className="p-0">
|
||||
<p className="admin-empty">
|
||||
<p className={adminEmptyClass}>
|
||||
{roles.length === 0 ? "暂无角色" : "没有匹配的角色"}
|
||||
</p>
|
||||
</TableCell>
|
||||
@@ -236,22 +249,22 @@ export function RoleManagementPage({
|
||||
) : (
|
||||
filteredRoles.map((role) => (
|
||||
<TableRow key={role.role_id} className="min-h-[60px] border-t border-[#edf1f5] hover:bg-[#f7fafc] data-[state=selected]:bg-transparent">
|
||||
<TableCell className="p-3 px-4 align-middle"><code className="role-code">{role.role_code}</code></TableCell>
|
||||
<TableCell className="p-3 px-4 align-middle"><span className="role-name">{role.role_name}</span></TableCell>
|
||||
<TableCell className="p-3 px-4 align-middle"><span className={`builtin-badge${role.is_builtin ? "" : " is-custom"}`}>
|
||||
<TableCell className="p-3 px-4 align-middle"><code className={roleCodeClass}>{role.role_code}</code></TableCell>
|
||||
<TableCell className="p-3 px-4 align-middle"><span className={roleNameClass}>{role.role_name}</span></TableCell>
|
||||
<TableCell className="p-3 px-4 align-middle"><span className={builtinBadgeClass(role.is_builtin)}>
|
||||
{role.is_builtin ? "内置" : "自定义"}
|
||||
</span></TableCell>
|
||||
<TableCell className="p-3 px-4 align-middle"><span className="permission-count">
|
||||
<TableCell className="p-3 px-4 align-middle"><span className={permissionCountClass}>
|
||||
{role.permission_codes.length} 项权限
|
||||
</span></TableCell>
|
||||
<TableCell className="p-3 px-4 align-middle"><span className="role-actions">
|
||||
<TableCell className="p-3 px-4 align-middle"><span className={roleActionsClass}>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="xs"
|
||||
type="button"
|
||||
disabled={!canManage}
|
||||
onClick={() => openEdit(role)}
|
||||
className="h-6 gap-1 rounded-[4px] border-[#d9e3ec] bg-white px-[9px] text-[11px] text-[#4c6c88] hover:bg-[#f7fafc] disabled:cursor-not-allowed disabled:opacity-[0.45]"
|
||||
className={rowButtonClass}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
@@ -261,7 +274,7 @@ export function RoleManagementPage({
|
||||
type="button"
|
||||
disabled={!canManage}
|
||||
onClick={() => openPermissions(role)}
|
||||
className="h-6 gap-1 rounded-[4px] border-[#d9e3ec] bg-white px-[9px] text-[11px] text-[#4c6c88] hover:bg-[#f7fafc] disabled:cursor-not-allowed disabled:opacity-[0.45]"
|
||||
className={rowButtonClass}
|
||||
>
|
||||
权限
|
||||
</Button>
|
||||
@@ -272,7 +285,7 @@ export function RoleManagementPage({
|
||||
disabled={!canManage || role.is_builtin}
|
||||
title={role.is_builtin ? "内置角色不可删除" : "删除角色"}
|
||||
onClick={() => setDeleteTarget(role)}
|
||||
className="h-6 gap-1 rounded-[4px] border-red-200 bg-white px-[9px] text-[11px] text-red-700 hover:bg-red-50 disabled:cursor-not-allowed disabled:opacity-[0.45]"
|
||||
className={rowDangerButtonClass}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
|
||||
@@ -25,8 +25,21 @@ import {
|
||||
formInputClass,
|
||||
modalFormClass,
|
||||
} from "../platform/modalUi";
|
||||
|
||||
import "../../styles/admin.css";
|
||||
import {
|
||||
adminEmptyClass,
|
||||
adminPageClass,
|
||||
adminReadonlyClass,
|
||||
adminToolbarClass,
|
||||
adminToolbarInputClass,
|
||||
adminToolbarSearchClass,
|
||||
employeeActionsClass,
|
||||
employeeAvatarClass,
|
||||
employeeNameClass,
|
||||
rolePillClass,
|
||||
rowButtonClass,
|
||||
rowDangerButtonClass,
|
||||
statusPillClass,
|
||||
} from "./adminUi";
|
||||
|
||||
const EMPTY_FORM = {
|
||||
username: "",
|
||||
@@ -41,11 +54,6 @@ const PRIMARY_BUTTON_CLASS =
|
||||
"h-[37px] gap-[7px] rounded-md border border-[#126ac3] bg-gradient-to-br from-[#1881e7] to-[#1268c9] px-[15px] text-xs font-[650] text-white shadow-[0_6px_15px_rgb(20_111_202/18%)] enabled:hover:from-[#1175d8] enabled:hover:to-[#0e5bad]";
|
||||
const FORM_INPUT_CLASS =
|
||||
"h-[39px] w-full rounded-md border border-[#d6e0e9] bg-white px-[11px] text-xs text-[#283a4e] outline-none focus:border-[#5b9ddb] focus:shadow-[0_0_0_3px_rgb(38_125_207/8%)] disabled:cursor-not-allowed disabled:opacity-50";
|
||||
const ROW_BUTTON_CLASS =
|
||||
"h-6 gap-1 rounded-[4px] border-[#d9e3ec] bg-white px-[9px] text-[11px] text-[#4c6c88] hover:bg-[#f7fafc] disabled:cursor-not-allowed disabled:opacity-[0.45]";
|
||||
const ROW_DANGER_BUTTON_CLASS =
|
||||
"h-6 gap-1 rounded-[4px] border-[#d9e3ec] bg-white px-[9px] text-[11px] text-[#c14a4a] hover:bg-[#fdf2f2] disabled:cursor-not-allowed disabled:opacity-[0.45]";
|
||||
|
||||
export function UserManagementPage({
|
||||
onNotify,
|
||||
onConnectionChange,
|
||||
@@ -190,14 +198,14 @@ export function UserManagementPage({
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="admin-page">
|
||||
<div className="admin-toolbar">
|
||||
<div className="admin-toolbar__search">
|
||||
<section className={adminPageClass}>
|
||||
<div className={adminToolbarClass}>
|
||||
<div className={adminToolbarSearchClass}>
|
||||
<Search size={14} />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="请输入用户名"
|
||||
className="admin-toolbar__input"
|
||||
className={adminToolbarInputClass}
|
||||
value={userSearchTerm}
|
||||
onChange={(event) => setUserSearchTerm(event.target.value)}
|
||||
/>
|
||||
@@ -215,7 +223,7 @@ export function UserManagementPage({
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{!canManage && <div className="admin-readonly">当前为开发人员,只能查看用户列表。</div>}
|
||||
{!canManage && <div className={adminReadonlyClass}>当前为开发人员,只能查看用户列表。</div>}
|
||||
<Table className="rounded-[9px] border border-[#dfe7ef] overflow-hidden text-[11px] text-[#44576a] [--border-color:#dfe7ef] table-fixed bg-white">
|
||||
<AdminColgroup widths={USER_PROJECT_COL_WIDTHS} />
|
||||
<TableHeader className="bg-[#f5f8fb] text-[#748598]">
|
||||
@@ -231,7 +239,7 @@ export function UserManagementPage({
|
||||
{loading ? (
|
||||
<TableRow className="min-h-[64px] border-t border-[#edf1f5] hover:bg-[#f7fafc] data-[state=selected]:bg-transparent">
|
||||
<TableCell colSpan={5} className="p-0">
|
||||
<p className="admin-empty">正在加载用户…</p>
|
||||
<p className={adminEmptyClass}>正在加载用户…</p>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
@@ -249,18 +257,18 @@ export function UserManagementPage({
|
||||
const isProtectedAdmin = employee.role_code === "admin";
|
||||
return (
|
||||
<TableRow key={employee.user_id} className="min-h-[64px] border-t border-[#edf1f5] hover:bg-[#f7fafc] data-[state=selected]:bg-transparent">
|
||||
<TableCell className="p-3 px-4 align-middle"><span className="employee-name"><b className="avatar">{employee.display_name.slice(0, 1)}</b><span><strong>{employee.display_name}</strong><small>{employee.email ?? "未设置邮箱"}</small></span></span></TableCell>
|
||||
<TableCell className="p-3 px-4 align-middle"><span className={employeeNameClass}><b className={employeeAvatarClass}>{employee.display_name.slice(0, 1)}</b><span><strong>{employee.display_name}</strong><small>{employee.email ?? "未设置邮箱"}</small></span></span></TableCell>
|
||||
<TableCell className="p-3 px-4 align-middle"><code>{employee.username}</code></TableCell>
|
||||
<TableCell className="p-3 px-4 align-middle">{employee.role_code ? (<span className={`role-pill is-${employee.role_code}`}>{employee.role_name}</span>) : (<span>-</span>)}</TableCell>
|
||||
<TableCell className="p-3 px-4 align-middle"><span className={`status-pill is-${employee.status}`}>{employee.status === "active" ? "正常" : employee.status === "disabled" ? "已停用" : "已锁定"}</span></TableCell>
|
||||
<TableCell className="p-3 px-4 align-middle"><span className="employee-actions">
|
||||
<TableCell className="p-3 px-4 align-middle">{employee.role_code ? (<span className={rolePillClass(employee.role_code)}>{employee.role_name}</span>) : (<span>-</span>)}</TableCell>
|
||||
<TableCell className="p-3 px-4 align-middle"><span className={statusPillClass(employee.status)}>{employee.status === "active" ? "正常" : employee.status === "disabled" ? "已停用" : "已锁定"}</span></TableCell>
|
||||
<TableCell className="p-3 px-4 align-middle"><span className={employeeActionsClass}>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="xs"
|
||||
type="button"
|
||||
disabled={!canManage}
|
||||
onClick={() => openEdit(employee)}
|
||||
className={ROW_BUTTON_CLASS}
|
||||
className={rowButtonClass}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
@@ -271,7 +279,7 @@ export function UserManagementPage({
|
||||
disabled={!canManage || isProtectedAdmin}
|
||||
title={isProtectedAdmin ? "管理员账号不能删除" : "删除"}
|
||||
onClick={() => setDeleteTarget(employee)}
|
||||
className={ROW_DANGER_BUTTON_CLASS}
|
||||
className={rowDangerButtonClass}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
|
||||
import { type Employee } from "../../services/api";
|
||||
import { adminToolbarInputClass } from "./adminUi";
|
||||
|
||||
// 多选用户下拉框组件 - 带 tag 显示
|
||||
export function UserMultiSelect({
|
||||
@@ -47,7 +48,7 @@ export function UserMultiSelect({
|
||||
<div ref={containerRef} style={{ position: "relative" }}>
|
||||
{/* 选择框 - 显示 tag */}
|
||||
<div
|
||||
className="admin-toolbar__input"
|
||||
className={adminToolbarInputClass}
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
style={{
|
||||
display: "flex",
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import { cn } from "~/lib/utils";
|
||||
|
||||
/** Shared Tailwind class strings for admin management pages. */
|
||||
|
||||
export const adminPageClass = "h-full overflow-auto bg-[#f3f6f9] p-6";
|
||||
|
||||
export const adminToolbarClass =
|
||||
"mb-3.5 flex items-center justify-between rounded-lg border border-[#dfe7ef] bg-white px-3.5 py-3";
|
||||
|
||||
export const adminToolbarSearchClass =
|
||||
"flex w-[280px] items-center gap-2 rounded-md border border-[#d9e2eb] bg-[#f8fafb] px-3 py-2 [&_svg]:text-[#8a99a8]";
|
||||
|
||||
export const adminToolbarInputClass =
|
||||
"w-full border-none bg-transparent text-xs text-[#20364c] outline-none placeholder:text-[#a0b0bf]";
|
||||
|
||||
export const adminReadonlyClass =
|
||||
"mb-3 rounded-md border border-[#f1d79c] bg-[#fff8e8] px-3.5 py-2.5 text-[11px] text-[#8a6416]";
|
||||
|
||||
export const adminEmptyClass = "p-[25px] text-center text-[#8796a6]";
|
||||
|
||||
export const employeeNameClass =
|
||||
"flex items-center gap-2.5 [&_small]:mt-[3px] [&_small]:text-[#93a0ae] [&_strong]:text-[#23384e] [&>span]:flex [&>span]:min-w-0 [&>span]:flex-col";
|
||||
|
||||
export const employeeAvatarClass =
|
||||
"grid size-8 place-items-center rounded-full bg-gradient-to-br from-[#3b92ed] to-[#1869c9] text-sm font-bold text-white";
|
||||
|
||||
export const employeeActionsClass = "flex gap-1.5";
|
||||
|
||||
export const roleActionsClass = "flex gap-1.5";
|
||||
|
||||
export const rowButtonClass =
|
||||
"h-6 gap-1 rounded-[4px] border-[#d9e3ec] bg-white px-[9px] text-[11px] text-[#4c6c88] hover:bg-[#f7fafc] disabled:cursor-not-allowed disabled:opacity-[0.45]";
|
||||
|
||||
export const rowDangerButtonClass =
|
||||
"h-6 gap-1 rounded-[4px] border-[#d9e3ec] bg-white px-[9px] text-[11px] text-[#c14a4a] hover:bg-[#fdf2f2] disabled:cursor-not-allowed disabled:opacity-[0.45]";
|
||||
|
||||
export const roleCodeClass = "font-mono text-[11px] text-[#60758b]";
|
||||
|
||||
export const roleNameClass = "font-semibold text-[#23384e]";
|
||||
|
||||
export const permissionCountClass = "text-[#5d7488]";
|
||||
|
||||
export const formHintClass =
|
||||
"mt-1.5 block text-[10px] font-normal leading-normal text-[#98a5b3]";
|
||||
|
||||
export const roleErrorClass = "mt-1.5 text-[10px] font-medium text-[#c0392b]";
|
||||
|
||||
export const permissionGroupsClass = "flex flex-col gap-3.5";
|
||||
|
||||
export const permissionGroupClass =
|
||||
"rounded-[7px] border border-[#e3eaf1] bg-[#fafbfc] px-3 py-2.5";
|
||||
|
||||
export const permissionGroupTitleClass =
|
||||
"mb-2 text-[10px] font-bold tracking-wide text-[#2e86de]";
|
||||
|
||||
export const permissionItemClass =
|
||||
"flex cursor-pointer items-start gap-[9px] rounded-md px-2 py-[7px] hover:bg-[#f0f6fc] [&_small]:mt-0.5 [&_small]:text-[10px] [&_small]:text-[#8a99a8] [&_span]:flex [&_span]:min-w-0 [&_span]:flex-col [&_strong]:text-[11px] [&_strong]:text-[#33475c]";
|
||||
|
||||
export const permissionItemLockedClass =
|
||||
"cursor-not-allowed bg-[#f1f3f5] opacity-80 hover:bg-[#f1f3f5]";
|
||||
|
||||
export function rolePillClass(roleCode: string): string {
|
||||
return cn(
|
||||
"w-max rounded-xl px-2 py-1",
|
||||
roleCode === "admin"
|
||||
? "bg-[#fff2d6] text-[#87580f]"
|
||||
: "bg-[#eaf4ff] text-[#1d6ab3]",
|
||||
);
|
||||
}
|
||||
|
||||
export function statusPillClass(status: string): string {
|
||||
return cn(
|
||||
"w-max rounded-xl px-2 py-1",
|
||||
status === "disabled" || status === "locked"
|
||||
? "bg-[#ffeded] text-[#a64b4b]"
|
||||
: "bg-[#e8f8f1] text-[#138657]",
|
||||
);
|
||||
}
|
||||
|
||||
export function builtinBadgeClass(isBuiltin: boolean): string {
|
||||
return cn(
|
||||
"w-max rounded-xl px-2 py-1 text-[10px]",
|
||||
isBuiltin ? "bg-[#fff2d6] text-[#87580f]" : "bg-[#eaf4ff] text-[#1d6ab3]",
|
||||
);
|
||||
}
|
||||
|
||||
export function permissionItemClassName(locked: boolean): string {
|
||||
return cn(permissionItemClass, locked && permissionItemLockedClass);
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
.admin-page{height:100%;padding:24px;overflow:auto;background:#f3f6f9}.admin-page__header{display:flex;align-items:center;justify-content:space-between;margin-bottom:15px;padding:20px 22px;border:1px solid #dee7ef;border-radius:9px;background:#fff}.admin-page__header span{color:#1978d4;font-size:10px;font-weight:800}.admin-page__header h2{margin:4px 0;color:#20364c}.admin-page__header p{margin:0;color:#8a99a8;font-size:11px}.admin-readonly{margin-bottom:12px;padding:10px 13px;border:1px solid #f1d79c;border-radius:6px;color:#8a6416;background:#fff8e8;font-size:11px}.employee-name{display:flex;align-items:center;gap:10px}.employee-name .avatar{display:grid;width:32px;height:32px;place-items:center}.employee-name>span{display:flex;min-width:0;flex-direction:column}.employee-name strong{color:#23384e}.employee-name small{margin-top:3px;color:#93a0ae}.role-pill,.status-pill{width:max-content;padding:4px 8px;border-radius:12px}.role-pill{color:#1d6ab3;background:#eaf4ff}.role-pill.is-admin{color:#87580f;background:#fff2d6}.status-pill{color:#138657;background:#e8f8f1}.status-pill.is-disabled,.status-pill.is-locked{color:#a64b4b;background:#ffeded}.employee-actions{display:flex;gap:6px}.employee-actions button{padding:5px 9px;border:1px solid #d9e3ec;border-radius:4px;color:#4c6c88;background:#fff;cursor:pointer}.employee-actions button.is-danger{color:#c14a4a}.employee-actions button:disabled{cursor:not-allowed;opacity:.45}.admin-empty{padding:25px;text-align:center;color:#8796a6}
|
||||
|
||||
.admin-tabs{display:flex;gap:10px;margin-bottom:16px}.admin-tab{display:flex;align-items:center;gap:7px;height:42px;padding:0 16px;border:1px solid #d9e2eb;border-radius:8px;color:#54687e;background:#fff;cursor:pointer;font-size:12px;font-weight:600;transition:all .15s ease}.admin-tab:hover{border-color:#b8c9d9;background:#f7fafc}.admin-tab.is-active{border-color:#2e86de;color:#fff;background:linear-gradient(135deg,#2e86de,#1978d4);box-shadow:0 6px 18px rgb(28 119 222/22%)}.admin-toolbar{display:flex;align-items:center;justify-content:space-between;margin-bottom:14px;padding:12px 14px;border:1px solid #dfe7ef;border-radius:8px;background:#fff}.admin-toolbar__search{display:flex;align-items:center;gap:8px;padding:8px 12px;border:1px solid #d9e2eb;border-radius:6px;background:#f8fafb;width:280px}.admin-toolbar__search svg{color:#8a99a8}.admin-toolbar__input{border:none;outline:none;background:transparent;color:#20364c;font-size:12px;width:100%}.admin-toolbar__input::placeholder{color:#a0b0bf}.admin-toolbar__button{display:flex;align-items:center;gap:6px;height:36px;padding:0 14px;border:none;border-radius:6px;color:#fff;background:linear-gradient(135deg,#2e86de,#1978d4);cursor:pointer;font-size:12px;font-weight:600;box-shadow:0 4px 12px rgb(28 119 222/18%)}.admin-toolbar__button:disabled{cursor:not-allowed;opacity:.5}.project-list-placeholder{display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:400px;border:1px solid #e0e7ee;border-radius:9px;background:#fff;color:#7a8fa3;text-align:center}.project-list-placeholder svg{margin-bottom:16px;opacity:.6}.project-list-placeholder h3{margin:12px 0 6px;color:#34475d;font-size:16px}.project-list-placeholder p{margin:0;font-size:12px}.form-field .required{color:#e53935;margin-left:4px;font-size:12px}
|
||||
|
||||
|
||||
.role-code{color:#60758b;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11px}.role-name{color:#23384e;font-weight:600}.builtin-badge{width:max-content;padding:4px 8px;border-radius:12px;color:#87580f;background:#fff2d6;font-size:10px}.builtin-badge.is-custom{color:#1d6ab3;background:#eaf4ff}.permission-count{color:#5d7488}.role-actions{display:flex;gap:6px}.role-actions button{padding:5px 9px;border:1px solid #d9e3ec;border-radius:4px;color:#4c6c88;background:#fff;cursor:pointer}.role-actions button.is-danger{color:#c14a4a}.role-actions button:disabled{cursor:not-allowed;opacity:.45}.permission-groups{display:flex;flex-direction:column;gap:14px}.permission-groups>.permission-group__title{color:#2e86de;font-size:10px;font-weight:700}.permission-group{border:1px solid #e3eaf1;border-radius:7px;background:#fafbfc;padding:10px 12px}.permission-group__title{margin-bottom:8px;color:#2e86de;font-size:10px;font-weight:700;letter-spacing:.04em}.permission-item{display:flex;align-items:flex-start;gap:9px;padding:7px 8px;border-radius:6px;cursor:pointer}.permission-item:hover{background:#f0f6fc}.permission-item input{margin-top:3px}.permission-item span{display:flex;flex-direction:column;min-width:0}.permission-item strong{color:#33475c;font-size:11px}.permission-item small{color:#8a99a8;font-size:10px;margin-top:2px}.permission-item.is-locked{background:#f1f3f5;cursor:not-allowed;opacity:.8}.role-create-form .form-hint{display:block;margin-top:6px;color:#98a5b3;font-size:10px;font-weight:400;line-height:1.5}.role-error{margin-top:6px;color:#c0392b;font-size:10px;font-weight:500}
|
||||
Reference in New Issue
Block a user