update: replace table component
This commit is contained in:
@@ -0,0 +1,18 @@
|
|||||||
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
|
// Column widths derived from the original CSS-grid fr ratios.
|
||||||
|
// 32+22+15+14+17 = 100; 20+23+14+16+27 = 100.
|
||||||
|
export const USER_PROJECT_COL_WIDTHS = [32, 22, 15, 14, 17] as const;
|
||||||
|
export const ROLE_COL_WIDTHS = [20, 23, 14, 16, 27] as const;
|
||||||
|
|
||||||
|
export type AdminColumnWidths = readonly number[];
|
||||||
|
|
||||||
|
export function AdminColgroup({ widths }: { widths: AdminColumnWidths }): ReactNode {
|
||||||
|
return (
|
||||||
|
<colgroup>
|
||||||
|
{widths.map((width, index) => (
|
||||||
|
<col key={index} style={{ width: `${width}%` }} />
|
||||||
|
))}
|
||||||
|
</colgroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import { X } from "lucide-react";
|
||||||
|
import { type Employee, type Workspace } from "../../services/api";
|
||||||
|
import { Button } from "~/components/ui/button";
|
||||||
|
import {
|
||||||
|
formFieldClass,
|
||||||
|
modalBackdropClass,
|
||||||
|
modalCompactClass,
|
||||||
|
modalEyebrowClass,
|
||||||
|
modalFooterClass,
|
||||||
|
modalFormClass,
|
||||||
|
modalHeaderClass,
|
||||||
|
modalTitleClass,
|
||||||
|
} from "../platform/modalUi";
|
||||||
|
import { UserMultiSelect } from "./UserMultiSelect";
|
||||||
|
|
||||||
|
const CLOSE_BUTTON_CLASS =
|
||||||
|
"size-[34px] rounded-[7px] border border-[#dfe6ed] bg-white hover:bg-[#f7faff] hover:border-[#b9c9da]";
|
||||||
|
const SECONDARY_BUTTON_CLASS =
|
||||||
|
"h-[37px] gap-[7px] rounded-md border-[#d8e1e9] bg-white px-[15px] text-xs font-[650] text-[#56687c]";
|
||||||
|
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]";
|
||||||
|
|
||||||
|
export function ImportMemberDialog({
|
||||||
|
open,
|
||||||
|
selectedProject,
|
||||||
|
availableUsers,
|
||||||
|
selectedUserIds,
|
||||||
|
existingMemberIds,
|
||||||
|
saving,
|
||||||
|
onNotify,
|
||||||
|
onChangeSelectedUserIds,
|
||||||
|
onSubmit,
|
||||||
|
onClose,
|
||||||
|
}: {
|
||||||
|
open: boolean;
|
||||||
|
selectedProject: Workspace | null;
|
||||||
|
availableUsers: Employee[];
|
||||||
|
selectedUserIds: string[];
|
||||||
|
existingMemberIds: string[];
|
||||||
|
saving: boolean;
|
||||||
|
onNotify: (notice: { tone: "success" | "error" | "info"; message: string }) => void;
|
||||||
|
onChangeSelectedUserIds: (ids: string[]) => void;
|
||||||
|
onSubmit: () => Promise<void> | void;
|
||||||
|
onClose: () => void;
|
||||||
|
}) {
|
||||||
|
if (!open || !selectedProject) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={modalBackdropClass} role="presentation">
|
||||||
|
<section className={modalCompactClass} role="dialog" aria-modal="true">
|
||||||
|
<div className={modalHeaderClass}>
|
||||||
|
<div>
|
||||||
|
<span className={modalEyebrowClass}>IMPORT MEMBER</span>
|
||||||
|
<h2 className={modalTitleClass}>导入成员到 {selectedProject?.workspace_name ?? "项目"}</h2>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon-sm"
|
||||||
|
type="button"
|
||||||
|
aria-label="关闭"
|
||||||
|
onClick={() => onClose()}
|
||||||
|
className={CLOSE_BUTTON_CLASS}
|
||||||
|
>
|
||||||
|
<X size={16} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<form className={modalFormClass}>
|
||||||
|
<label className={`${formFieldClass} pb-[120px]`}>
|
||||||
|
<span>选择用户<span className="text-[#e74c3c]">*</span></span>
|
||||||
|
<UserMultiSelect
|
||||||
|
users={availableUsers}
|
||||||
|
selectedUserIds={selectedUserIds}
|
||||||
|
onChange={onChangeSelectedUserIds}
|
||||||
|
existingMemberIds={existingMemberIds}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<div className={modalFooterClass}>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
type="button"
|
||||||
|
onClick={() => onClose()}
|
||||||
|
className={SECONDARY_BUTTON_CLASS}
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
size="sm"
|
||||||
|
type="button"
|
||||||
|
disabled={saving || selectedUserIds.length === 0}
|
||||||
|
onClick={() => void onSubmit()}
|
||||||
|
className={PRIMARY_BUTTON_CLASS}
|
||||||
|
>
|
||||||
|
{saving ? "添加中…" : `添加 (${selectedUserIds.length})`}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
import { X } from "lucide-react";
|
||||||
|
import { type Workspace } from "../../services/api";
|
||||||
|
import { Button } from "~/components/ui/button";
|
||||||
|
import { Input } from "~/components/ui/input";
|
||||||
|
import { Textarea } from "~/components/ui/textarea";
|
||||||
|
import {
|
||||||
|
formFieldClass,
|
||||||
|
modalBackdropClass,
|
||||||
|
modalCompactClass,
|
||||||
|
modalEyebrowClass,
|
||||||
|
modalFooterClass,
|
||||||
|
modalFormClass,
|
||||||
|
modalHeaderClass,
|
||||||
|
modalTitleClass,
|
||||||
|
} from "../platform/modalUi";
|
||||||
|
|
||||||
|
const CLOSE_BUTTON_CLASS =
|
||||||
|
"size-[34px] rounded-[7px] border border-[#dfe6ed] bg-white hover:bg-[#f7faff] hover:border-[#b9c9da]";
|
||||||
|
const SECONDARY_BUTTON_CLASS =
|
||||||
|
"h-[37px] gap-[7px] rounded-md border-[#d8e1e9] bg-white px-[15px] text-xs font-[650] text-[#56687c]";
|
||||||
|
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 FORM_TEXTAREA_CLASS =
|
||||||
|
"min-h-[92px] w-full resize-y rounded-md border border-[#d6e0e9] bg-white px-[11px] py-2.5 text-xs leading-[1.55] text-[#283a4e] outline-none focus:border-[#5b9ddb] focus:shadow-[0_0_0_3px_rgb(38_125_207/8%)]";
|
||||||
|
|
||||||
|
export function ProjectEditDialog({
|
||||||
|
open,
|
||||||
|
editingProject,
|
||||||
|
projectForm,
|
||||||
|
saving,
|
||||||
|
onNotify,
|
||||||
|
onChangeForm,
|
||||||
|
onSubmit,
|
||||||
|
onClose,
|
||||||
|
}: {
|
||||||
|
open: boolean;
|
||||||
|
editingProject: Workspace | null;
|
||||||
|
projectForm: { workspace_code: string; workspace_name: string; quota_bytes: number; description: string };
|
||||||
|
saving: boolean;
|
||||||
|
onNotify: (notice: { tone: "success" | "error" | "info"; message: string }) => void;
|
||||||
|
onChangeForm: (next: { workspace_code: string; workspace_name: string; quota_bytes: number; description: string }) => void;
|
||||||
|
onSubmit: (event: React.FormEvent) => Promise<void> | void;
|
||||||
|
onClose: () => void;
|
||||||
|
}) {
|
||||||
|
if (!open) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={modalBackdropClass} role="presentation">
|
||||||
|
<section className={modalCompactClass} role="dialog" aria-modal="true">
|
||||||
|
<div className={modalHeaderClass}>
|
||||||
|
<div>
|
||||||
|
<span className={modalEyebrowClass}>PROJECT</span>
|
||||||
|
<h2 className={modalTitleClass}>{editingProject ? "编辑项目" : "新建项目"}</h2>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon-sm"
|
||||||
|
type="button"
|
||||||
|
aria-label="关闭"
|
||||||
|
onClick={() => onClose()}
|
||||||
|
className={CLOSE_BUTTON_CLASS}
|
||||||
|
>
|
||||||
|
<X size={16} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<form className={modalFormClass} onSubmit={(event) => void onSubmit(event)}>
|
||||||
|
{!editingProject && (
|
||||||
|
<label className={formFieldClass}>
|
||||||
|
<span>项目编码<span className="text-[#e74c3c]">*</span></span>
|
||||||
|
<Input
|
||||||
|
className={FORM_INPUT_CLASS}
|
||||||
|
autoFocus
|
||||||
|
value={projectForm.workspace_code}
|
||||||
|
onChange={(event) => onChangeForm({ ...projectForm, workspace_code: event.target.value })}
|
||||||
|
placeholder="例如:model-development(小写字母、数字、连字符,3-32 字符)"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
<label className={formFieldClass}>
|
||||||
|
<span>项目名称<span className="text-[#e74c3c]">*</span></span>
|
||||||
|
<Input
|
||||||
|
className={FORM_INPUT_CLASS}
|
||||||
|
value={projectForm.workspace_name}
|
||||||
|
onChange={(event) => onChangeForm({ ...projectForm, workspace_name: event.target.value })}
|
||||||
|
placeholder="请输入项目名称"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label className={formFieldClass}>
|
||||||
|
<span>配额(字节)</span>
|
||||||
|
<Input
|
||||||
|
className={FORM_INPUT_CLASS}
|
||||||
|
type="number"
|
||||||
|
value={projectForm.quota_bytes}
|
||||||
|
onChange={(event) => onChangeForm({ ...projectForm, quota_bytes: parseInt(event.target.value) || 0 })}
|
||||||
|
placeholder="0 表示无配额限制"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label className={formFieldClass}>
|
||||||
|
<span>项目描述</span>
|
||||||
|
<Textarea
|
||||||
|
className={FORM_TEXTAREA_CLASS}
|
||||||
|
value={projectForm.description}
|
||||||
|
onChange={(event) => onChangeForm({ ...projectForm, description: event.target.value })}
|
||||||
|
placeholder="请输入项目描述(可选)"
|
||||||
|
rows={4}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<div className={modalFooterClass}>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
type="button"
|
||||||
|
onClick={() => onClose()}
|
||||||
|
className={SECONDARY_BUTTON_CLASS}
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
size="sm"
|
||||||
|
type="submit"
|
||||||
|
disabled={saving}
|
||||||
|
className={PRIMARY_BUTTON_CLASS}
|
||||||
|
>
|
||||||
|
{saving ? "保存中…" : "保存"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,13 +1,9 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
// TODO: split file (>500 lines per CLAUDE.md 3.2.1 budget)
|
|
||||||
import { X } from "lucide-react";
|
|
||||||
import { ApiRequestError, type Employee, type Workspace, type WorkspaceMember } from "../../services/api";
|
import { ApiRequestError, type Employee, type Workspace, type WorkspaceMember } from "../../services/api";
|
||||||
import { useApi, useAuth } from "../../context/AuthContext";
|
import { useApi, useAuth } from "../../context/AuthContext";
|
||||||
import Icon from "../../components/common/Icon";
|
import Icon from "../../components/common/Icon";
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
import { Input } from "~/components/ui/input";
|
|
||||||
import { Textarea } from "~/components/ui/textarea";
|
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
@@ -16,17 +12,10 @@ import {
|
|||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "~/components/ui/table";
|
} from "~/components/ui/table";
|
||||||
import { UserMultiSelect } from "./UserMultiSelect";
|
import { AdminColgroup, USER_PROJECT_COL_WIDTHS } from "./AdminTable";
|
||||||
import {
|
import { ProjectEditDialog } from "./ProjectEditDialog";
|
||||||
formFieldClass,
|
import { ImportMemberDialog } from "./ImportMemberDialog";
|
||||||
modalBackdropClass,
|
import { ProjectMembersDrawer } from "./ProjectMembersDrawer";
|
||||||
modalCompactClass,
|
|
||||||
modalEyebrowClass,
|
|
||||||
modalFooterClass,
|
|
||||||
modalFormClass,
|
|
||||||
modalHeaderClass,
|
|
||||||
modalTitleClass,
|
|
||||||
} from "../platform/modalUi";
|
|
||||||
|
|
||||||
import "../../styles/admin.css";
|
import "../../styles/admin.css";
|
||||||
|
|
||||||
@@ -37,16 +26,8 @@ const EMPTY_PROJECT_FORM = {
|
|||||||
description: "",
|
description: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
const CLOSE_BUTTON_CLASS =
|
|
||||||
"size-[34px] rounded-[7px] border border-[#dfe6ed] bg-white hover:bg-[#f7faff] hover:border-[#b9c9da]";
|
|
||||||
const SECONDARY_BUTTON_CLASS =
|
|
||||||
"h-[37px] gap-[7px] rounded-md border-[#d8e1e9] bg-white px-[15px] text-xs font-[650] text-[#56687c]";
|
|
||||||
const PRIMARY_BUTTON_CLASS =
|
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]";
|
"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 FORM_TEXTAREA_CLASS =
|
|
||||||
"min-h-[92px] w-full resize-y rounded-md border border-[#d6e0e9] bg-white px-[11px] py-2.5 text-xs leading-[1.55] text-[#283a4e] outline-none focus:border-[#5b9ddb] focus:shadow-[0_0_0_3px_rgb(38_125_207/8%)]";
|
|
||||||
|
|
||||||
export function ProjectManagementPage({
|
export function ProjectManagementPage({
|
||||||
onNotify,
|
onNotify,
|
||||||
@@ -287,13 +268,7 @@ export function ProjectManagementPage({
|
|||||||
|
|
||||||
{!canManage && <div className="admin-readonly">当前为开发人员,只能查看项目列表。</div>}
|
{!canManage && <div className="admin-readonly">当前为开发人员,只能查看项目列表。</div>}
|
||||||
<Table className="rounded-[9px] border border-[#dfe7ef] overflow-hidden text-[11px] text-[#44576a] [--border-color:#dfe7ef] table-fixed bg-white">
|
<Table className="rounded-[9px] border border-[#dfe7ef] overflow-hidden text-[11px] text-[#44576a] [--border-color:#dfe7ef] table-fixed bg-white">
|
||||||
<colgroup>
|
<AdminColgroup widths={USER_PROJECT_COL_WIDTHS} />
|
||||||
<col style={{ width: "32%" }} />
|
|
||||||
<col style={{ width: "22%" }} />
|
|
||||||
<col style={{ width: "15%" }} />
|
|
||||||
<col style={{ width: "14%" }} />
|
|
||||||
<col style={{ width: "17%" }} />
|
|
||||||
</colgroup>
|
|
||||||
<TableHeader className="bg-[#f5f8fb] text-[#748598]">
|
<TableHeader className="bg-[#f5f8fb] text-[#748598]">
|
||||||
<TableRow className="hover:bg-transparent border-b border-[#edf1f5]">
|
<TableRow className="hover:bg-transparent border-b border-[#edf1f5]">
|
||||||
<TableHead className="h-10 px-4 text-[10px] font-bold text-[#748598]">项目名称</TableHead>
|
<TableHead className="h-10 px-4 text-[10px] font-bold text-[#748598]">项目名称</TableHead>
|
||||||
@@ -369,283 +344,44 @@ export function ProjectManagementPage({
|
|||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
|
|
||||||
{projectDialogOpen && (
|
<ProjectEditDialog
|
||||||
<div className={modalBackdropClass} role="presentation">
|
open={projectDialogOpen}
|
||||||
<section className={modalCompactClass} role="dialog" aria-modal="true">
|
editingProject={editingProject}
|
||||||
<div className={modalHeaderClass}>
|
projectForm={projectForm}
|
||||||
<div>
|
saving={saving}
|
||||||
<span className={modalEyebrowClass}>PROJECT</span>
|
onNotify={onNotify}
|
||||||
<h2 className={modalTitleClass}>{editingProject ? "编辑项目" : "新建项目"}</h2>
|
onChangeForm={setProjectForm}
|
||||||
</div>
|
onSubmit={(event) => void submitProject(event)}
|
||||||
<Button
|
onClose={() => setProjectDialogOpen(false)}
|
||||||
variant="ghost"
|
|
||||||
size="icon-sm"
|
|
||||||
type="button"
|
|
||||||
aria-label="关闭"
|
|
||||||
onClick={() => setProjectDialogOpen(false)}
|
|
||||||
className={CLOSE_BUTTON_CLASS}
|
|
||||||
>
|
|
||||||
<X size={16} />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<form className={modalFormClass} onSubmit={(event) => void submitProject(event)}>
|
|
||||||
{!editingProject && (
|
|
||||||
<label className={formFieldClass}>
|
|
||||||
<span>项目编码<span className="text-[#e74c3c]">*</span></span>
|
|
||||||
<Input
|
|
||||||
className={FORM_INPUT_CLASS}
|
|
||||||
autoFocus
|
|
||||||
value={projectForm.workspace_code}
|
|
||||||
onChange={(event) => setProjectForm({ ...projectForm, workspace_code: event.target.value })}
|
|
||||||
placeholder="例如:model-development(小写字母、数字、连字符,3-32 字符)"
|
|
||||||
/>
|
/>
|
||||||
</label>
|
|
||||||
)}
|
|
||||||
<label className={formFieldClass}>
|
|
||||||
<span>项目名称<span className="text-[#e74c3c]">*</span></span>
|
|
||||||
<Input
|
|
||||||
className={FORM_INPUT_CLASS}
|
|
||||||
value={projectForm.workspace_name}
|
|
||||||
onChange={(event) => setProjectForm({ ...projectForm, workspace_name: event.target.value })}
|
|
||||||
placeholder="请输入项目名称"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<label className={formFieldClass}>
|
|
||||||
<span>配额(字节)</span>
|
|
||||||
<Input
|
|
||||||
className={FORM_INPUT_CLASS}
|
|
||||||
type="number"
|
|
||||||
value={projectForm.quota_bytes}
|
|
||||||
onChange={(event) => setProjectForm({ ...projectForm, quota_bytes: parseInt(event.target.value) || 0 })}
|
|
||||||
placeholder="0 表示无配额限制"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<label className={formFieldClass}>
|
|
||||||
<span>项目描述</span>
|
|
||||||
<Textarea
|
|
||||||
className={FORM_TEXTAREA_CLASS}
|
|
||||||
value={projectForm.description}
|
|
||||||
onChange={(event) => setProjectForm({ ...projectForm, description: event.target.value })}
|
|
||||||
placeholder="请输入项目描述(可选)"
|
|
||||||
rows={4}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<div className={modalFooterClass}>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
type="button"
|
|
||||||
onClick={() => setProjectDialogOpen(false)}
|
|
||||||
className={SECONDARY_BUTTON_CLASS}
|
|
||||||
>
|
|
||||||
取消
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="default"
|
|
||||||
size="sm"
|
|
||||||
type="submit"
|
|
||||||
disabled={saving}
|
|
||||||
className={PRIMARY_BUTTON_CLASS}
|
|
||||||
>
|
|
||||||
{saving ? "保存中…" : "保存"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{importMemberDialogOpen && (
|
<ImportMemberDialog
|
||||||
<div className={modalBackdropClass} role="presentation">
|
open={importMemberDialogOpen}
|
||||||
<section className={modalCompactClass} role="dialog" aria-modal="true">
|
selectedProject={selectedProject}
|
||||||
<div className={modalHeaderClass}>
|
availableUsers={availableUsers}
|
||||||
<div>
|
|
||||||
<span className={modalEyebrowClass}>IMPORT MEMBER</span>
|
|
||||||
<h2 className={modalTitleClass}>导入成员到 {selectedProject?.workspace_name ?? "项目"}</h2>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon-sm"
|
|
||||||
type="button"
|
|
||||||
aria-label="关闭"
|
|
||||||
onClick={() => setImportMemberDialogOpen(false)}
|
|
||||||
className={CLOSE_BUTTON_CLASS}
|
|
||||||
>
|
|
||||||
<X size={16} />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<form className={modalFormClass}>
|
|
||||||
<label className={`${formFieldClass} pb-[120px]`}>
|
|
||||||
<span>选择用户<span className="text-[#e74c3c]">*</span></span>
|
|
||||||
<UserMultiSelect
|
|
||||||
users={availableUsers}
|
|
||||||
selectedUserIds={selectedUserIds}
|
selectedUserIds={selectedUserIds}
|
||||||
onChange={setSelectedUserIds}
|
|
||||||
existingMemberIds={currentProjectMembers.map((m) => m.user_id)}
|
existingMemberIds={currentProjectMembers.map((m) => m.user_id)}
|
||||||
|
saving={saving}
|
||||||
|
onNotify={onNotify}
|
||||||
|
onChangeSelectedUserIds={setSelectedUserIds}
|
||||||
|
onSubmit={() => void importMember()}
|
||||||
|
onClose={() => setImportMemberDialogOpen(false)}
|
||||||
/>
|
/>
|
||||||
</label>
|
|
||||||
<div className={modalFooterClass}>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
type="button"
|
|
||||||
onClick={() => setImportMemberDialogOpen(false)}
|
|
||||||
className={SECONDARY_BUTTON_CLASS}
|
|
||||||
>
|
|
||||||
取消
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="default"
|
|
||||||
size="sm"
|
|
||||||
type="button"
|
|
||||||
disabled={saving || selectedUserIds.length === 0}
|
|
||||||
onClick={() => void importMember()}
|
|
||||||
className={PRIMARY_BUTTON_CLASS}
|
|
||||||
>
|
|
||||||
{saving ? "添加中…" : `添加 (${selectedUserIds.length})`}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{membersDrawerOpen && selectedProject && (
|
<ProjectMembersDrawer
|
||||||
<>
|
open={membersDrawerOpen}
|
||||||
<div className="drawer-overlay" onClick={() => setMembersDrawerOpen(false)} style={{
|
selectedProject={selectedProject}
|
||||||
position: "fixed",
|
members={currentProjectMembers}
|
||||||
top: 0,
|
membersLoading={membersLoading}
|
||||||
left: 0,
|
canManage={canManage}
|
||||||
right: 0,
|
onClose={() => setMembersDrawerOpen(false)}
|
||||||
bottom: 0,
|
onAddMembers={() => {
|
||||||
backgroundColor: "rgba(0,0,0,0.3)",
|
|
||||||
zIndex: 99,
|
|
||||||
}} />
|
|
||||||
<aside className="drawer" style={{
|
|
||||||
position: "fixed",
|
|
||||||
right: 0,
|
|
||||||
top: 0,
|
|
||||||
bottom: 0,
|
|
||||||
width: 480,
|
|
||||||
backgroundColor: "#fff",
|
|
||||||
boxShadow: "-2px 0 8px rgba(0,0,0,0.1)",
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
zIndex: 100,
|
|
||||||
}}>
|
|
||||||
<div className="drawer__header" style={{
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
alignItems: "center",
|
|
||||||
padding: "16px 20px",
|
|
||||||
borderBottom: "1px solid #e8e8e8",
|
|
||||||
}}>
|
|
||||||
<div>
|
|
||||||
<span className="modal__eyebrow" style={{ fontSize: 12, color: "#666" }}>MEMBERS</span>
|
|
||||||
<h3 style={{ margin: 0, fontSize: 18 }}>项目成员</h3>
|
|
||||||
<p style={{ margin: "4px 0 0", fontSize: 13, color: "#888" }}>{selectedProject.workspace_name}</p>
|
|
||||||
</div>
|
|
||||||
<button className="icon-button" type="button" onClick={() => setMembersDrawerOpen(false)}>
|
|
||||||
<Icon name="close" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{
|
|
||||||
padding: "12px 20px",
|
|
||||||
borderBottom: "1px solid #f0f0f0",
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
alignItems: "center",
|
|
||||||
}}>
|
|
||||||
<span style={{ fontSize: 14, color: "#666" }}>
|
|
||||||
共 {currentProjectMembers.length} 名成员
|
|
||||||
</span>
|
|
||||||
<button
|
|
||||||
className="primary-button"
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
setMembersDrawerOpen(false);
|
setMembersDrawerOpen(false);
|
||||||
openImportMemberDialog(selectedProject);
|
if (selectedProject) void openImportMemberDialog(selectedProject);
|
||||||
}}
|
}}
|
||||||
style={{ fontSize: 13, padding: "6px 12px" }}
|
onRemoveMember={(userId) => void removeMember(userId)}
|
||||||
>
|
onNotify={onNotify}
|
||||||
<Icon name="plus" size={14} /> 添加成员
|
/>
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ flex: 1, overflow: "auto", padding: "12px 20px" }}>
|
|
||||||
{membersLoading ? (
|
|
||||||
<p style={{ textAlign: "center", color: "#999", padding: "40px 0" }}>加载中…</p>
|
|
||||||
) : currentProjectMembers.length === 0 ? (
|
|
||||||
<p style={{ textAlign: "center", color: "#999", padding: "40px 0" }}>暂无成员</p>
|
|
||||||
) : (
|
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
|
||||||
{currentProjectMembers.map((member) => (
|
|
||||||
<div
|
|
||||||
key={member.user_id}
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
padding: "6px",
|
|
||||||
border: "1px solid #e8e8e8",
|
|
||||||
borderRadius: 6,
|
|
||||||
backgroundColor: "#fafafa",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="avatar"
|
|
||||||
style={{
|
|
||||||
width: 32,
|
|
||||||
height: 32,
|
|
||||||
borderRadius: "50%",
|
|
||||||
backgroundColor: "#1890ff",
|
|
||||||
color: "#fff",
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: 600,
|
|
||||||
marginRight: 10,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{member.display_name.slice(0, 1)}
|
|
||||||
</div>
|
|
||||||
<div style={{ flex: 1, minWidth: 0 }}>
|
|
||||||
<div style={{ fontWeight: 500, marginBottom: 2, fontSize: 13 }}>{member.display_name}</div>
|
|
||||||
<div style={{ fontSize: 11, color: "#888" }}>{member.username}</div>
|
|
||||||
</div>
|
|
||||||
<div style={{ display: "flex", alignItems: "center", gap: 6 }}>
|
|
||||||
{(() => {
|
|
||||||
const isAdmin = member.role_code === "admin";
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => void removeMember(member.user_id)}
|
|
||||||
style={{
|
|
||||||
padding: "3px 8px",
|
|
||||||
fontSize: 11,
|
|
||||||
color: isAdmin ? "#999" : "#ff4d4f",
|
|
||||||
border: "1px solid " + (isAdmin ? "#ddd" : "#ff4d4f"),
|
|
||||||
borderRadius: 4,
|
|
||||||
backgroundColor: "#fff",
|
|
||||||
cursor: isAdmin ? "not-allowed" : canManage ? "pointer" : "not-allowed",
|
|
||||||
}}
|
|
||||||
disabled={!canManage || isAdmin}
|
|
||||||
title={isAdmin ? "管理员不能被移除" : canManage ? "移除成员" : "无权限"}
|
|
||||||
>
|
|
||||||
移除
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,158 @@
|
|||||||
|
import { type Workspace, type WorkspaceMember } from "../../services/api";
|
||||||
|
import Icon from "../../components/common/Icon";
|
||||||
|
|
||||||
|
export function ProjectMembersDrawer({
|
||||||
|
open,
|
||||||
|
selectedProject,
|
||||||
|
members,
|
||||||
|
membersLoading,
|
||||||
|
canManage,
|
||||||
|
onClose,
|
||||||
|
onAddMembers,
|
||||||
|
onRemoveMember,
|
||||||
|
onNotify,
|
||||||
|
}: {
|
||||||
|
open: boolean;
|
||||||
|
selectedProject: Workspace | null;
|
||||||
|
members: WorkspaceMember[];
|
||||||
|
membersLoading: boolean;
|
||||||
|
canManage: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onAddMembers: () => void;
|
||||||
|
onRemoveMember: (userId: string) => void;
|
||||||
|
onNotify: (notice: { tone: "success" | "error" | "info"; message: string }) => void;
|
||||||
|
}) {
|
||||||
|
if (!open || !selectedProject) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="drawer-overlay" onClick={() => onClose()} style={{
|
||||||
|
position: "fixed",
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
backgroundColor: "rgba(0,0,0,0.3)",
|
||||||
|
zIndex: 99,
|
||||||
|
}} />
|
||||||
|
<aside className="drawer" style={{
|
||||||
|
position: "fixed",
|
||||||
|
right: 0,
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
width: 480,
|
||||||
|
backgroundColor: "#fff",
|
||||||
|
boxShadow: "-2px 0 8px rgba(0,0,0,0.1)",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
zIndex: 100,
|
||||||
|
}}>
|
||||||
|
<div className="drawer__header" style={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
padding: "16px 20px",
|
||||||
|
borderBottom: "1px solid #e8e8e8",
|
||||||
|
}}>
|
||||||
|
<div>
|
||||||
|
<span className="modal__eyebrow" style={{ fontSize: 12, color: "#666" }}>MEMBERS</span>
|
||||||
|
<h3 style={{ margin: 0, fontSize: 18 }}>项目成员</h3>
|
||||||
|
<p style={{ margin: "4px 0 0", fontSize: 13, color: "#888" }}>{selectedProject.workspace_name}</p>
|
||||||
|
</div>
|
||||||
|
<button className="icon-button" type="button" onClick={() => onClose()}>
|
||||||
|
<Icon name="close" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{
|
||||||
|
padding: "12px 20px",
|
||||||
|
borderBottom: "1px solid #f0f0f0",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
}}>
|
||||||
|
<span style={{ fontSize: 14, color: "#666" }}>
|
||||||
|
共 {members.length} 名成员
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
className="primary-button"
|
||||||
|
type="button"
|
||||||
|
onClick={() => onAddMembers()}
|
||||||
|
style={{ fontSize: 13, padding: "6px 12px" }}
|
||||||
|
>
|
||||||
|
<Icon name="plus" size={14} /> 添加成员
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ flex: 1, overflow: "auto", padding: "12px 20px" }}>
|
||||||
|
{membersLoading ? (
|
||||||
|
<p style={{ textAlign: "center", color: "#999", padding: "40px 0" }}>加载中…</p>
|
||||||
|
) : members.length === 0 ? (
|
||||||
|
<p style={{ textAlign: "center", color: "#999", padding: "40px 0" }}>暂无成员</p>
|
||||||
|
) : (
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
||||||
|
{members.map((member) => {
|
||||||
|
const isAdmin = member.role_code === "admin";
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={member.user_id}
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
padding: "6px",
|
||||||
|
border: "1px solid #e8e8e8",
|
||||||
|
borderRadius: 6,
|
||||||
|
backgroundColor: "#fafafa",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="avatar"
|
||||||
|
style={{
|
||||||
|
width: 32,
|
||||||
|
height: 32,
|
||||||
|
borderRadius: "50%",
|
||||||
|
backgroundColor: "#1890ff",
|
||||||
|
color: "#fff",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: 600,
|
||||||
|
marginRight: 10,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{member.display_name.slice(0, 1)}
|
||||||
|
</div>
|
||||||
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
|
<div style={{ fontWeight: 500, marginBottom: 2, fontSize: 13 }}>{member.display_name}</div>
|
||||||
|
<div style={{ fontSize: 11, color: "#888" }}>{member.username}</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: 6 }}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => onRemoveMember(member.user_id)}
|
||||||
|
style={{
|
||||||
|
padding: "3px 8px",
|
||||||
|
fontSize: 11,
|
||||||
|
color: isAdmin ? "#999" : "#ff4d4f",
|
||||||
|
border: "1px solid " + (isAdmin ? "#ddd" : "#ff4d4f"),
|
||||||
|
borderRadius: 4,
|
||||||
|
backgroundColor: "#fff",
|
||||||
|
cursor: isAdmin ? "not-allowed" : canManage ? "pointer" : "not-allowed",
|
||||||
|
}}
|
||||||
|
disabled={!canManage || isAdmin}
|
||||||
|
title={isAdmin ? "管理员不能被移除" : canManage ? "移除成员" : "无权限"}
|
||||||
|
>
|
||||||
|
移除
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "~/components/ui/table";
|
} from "~/components/ui/table";
|
||||||
|
import { AdminColgroup, ROLE_COL_WIDTHS } from "./AdminTable";
|
||||||
|
|
||||||
import "../../styles/admin.css";
|
import "../../styles/admin.css";
|
||||||
|
|
||||||
@@ -204,13 +205,7 @@ export function RoleManagementPage({
|
|||||||
{loadError && <div className="admin-readonly">{loadError}</div>}
|
{loadError && <div className="admin-readonly">{loadError}</div>}
|
||||||
|
|
||||||
<Table className="rounded-[9px] border border-[#dfe7ef] overflow-hidden text-[11px] text-[#44576a] [--border-color:#dfe7ef] table-fixed bg-white">
|
<Table className="rounded-[9px] border border-[#dfe7ef] overflow-hidden text-[11px] text-[#44576a] [--border-color:#dfe7ef] table-fixed bg-white">
|
||||||
<colgroup>
|
<AdminColgroup widths={ROLE_COL_WIDTHS} />
|
||||||
<col style={{ width: "20%" }} />
|
|
||||||
<col style={{ width: "23%" }} />
|
|
||||||
<col style={{ width: "14%" }} />
|
|
||||||
<col style={{ width: "16%" }} />
|
|
||||||
<col style={{ width: "27%" }} />
|
|
||||||
</colgroup>
|
|
||||||
<TableHeader className="bg-[#f5f8fb] text-[#748598]">
|
<TableHeader className="bg-[#f5f8fb] text-[#748598]">
|
||||||
<TableRow className="hover:bg-transparent border-b border-[#edf1f5]">
|
<TableRow className="hover:bg-transparent border-b border-[#edf1f5]">
|
||||||
<TableHead className="h-10 px-4 text-[10px] font-bold text-[#748598]">角色编码</TableHead>
|
<TableHead className="h-10 px-4 text-[10px] font-bold text-[#748598]">角色编码</TableHead>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "~/components/ui/table";
|
} from "~/components/ui/table";
|
||||||
|
import { AdminColgroup, USER_PROJECT_COL_WIDTHS } from "./AdminTable";
|
||||||
import {
|
import {
|
||||||
formFieldClass,
|
formFieldClass,
|
||||||
formInputClass,
|
formInputClass,
|
||||||
@@ -221,13 +222,7 @@ export function UserManagementPage({
|
|||||||
|
|
||||||
{!canManage && <div className="admin-readonly">当前为开发人员,只能查看用户列表。</div>}
|
{!canManage && <div className="admin-readonly">当前为开发人员,只能查看用户列表。</div>}
|
||||||
<Table className="rounded-[9px] border border-[#dfe7ef] overflow-hidden text-[11px] text-[#44576a] [--border-color:#dfe7ef] table-fixed bg-white">
|
<Table className="rounded-[9px] border border-[#dfe7ef] overflow-hidden text-[11px] text-[#44576a] [--border-color:#dfe7ef] table-fixed bg-white">
|
||||||
<colgroup>
|
<AdminColgroup widths={USER_PROJECT_COL_WIDTHS} />
|
||||||
<col style={{ width: "32%" }} />
|
|
||||||
<col style={{ width: "22%" }} />
|
|
||||||
<col style={{ width: "15%" }} />
|
|
||||||
<col style={{ width: "14%" }} />
|
|
||||||
<col style={{ width: "17%" }} />
|
|
||||||
</colgroup>
|
|
||||||
<TableHeader className="bg-[#f5f8fb] text-[#748598]">
|
<TableHeader className="bg-[#f5f8fb] text-[#748598]">
|
||||||
<TableRow className="hover:bg-transparent border-b border-[#edf1f5]">
|
<TableRow className="hover:bg-transparent border-b border-[#edf1f5]">
|
||||||
<TableHead className="h-10 px-4 text-[10px] font-bold text-[#748598]">用户</TableHead>
|
<TableHead className="h-10 px-4 text-[10px] font-bold text-[#748598]">用户</TableHead>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
.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-table{overflow:hidden;border:1px solid #dfe7ef;border-radius:9px;background:#fff}.employee-table__head,.employee-row{display:grid;grid-template-columns:1.5fr 1fr .7fr .65fr .8fr;align-items:center;gap:12px;padding:12px 17px}.employee-table__head{color:#748598;background:#f5f8fb;font-size:10px;font-weight:700}.employee-row{min-height:64px;border-top:1px solid #edf1f5;color:#44576a;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}.employee-row code{color:#60758b}.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-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}
|
.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-table{overflow:hidden;border:1px solid #dfe7ef;border-radius:9px;background:#fff}.role-table__head,.role-row{display:grid;grid-template-columns:1fr 1.2fr .7fr .8fr 1.4fr;align-items:center;gap:12px;padding:12px 17px}.role-table__head{color:#748598;background:#f5f8fb;font-size:10px;font-weight:700}.role-row{min-height:60px;border-top:1px solid #edf1f5;color:#44576a;font-size:11px}.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}
|
.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