系统管理页面部分更改
This commit is contained in:
@@ -8,6 +8,7 @@ import { useApi, useAuth } from "../../context/AuthContext";
|
||||
import Icon from "../../components/Icon";
|
||||
import "../../styles/admin.css";
|
||||
import "../../styles/dashboard.css";
|
||||
import "../../styles/platform.css";
|
||||
|
||||
|
||||
type Notice = {
|
||||
@@ -41,13 +42,13 @@ export function DashboardPage({
|
||||
<div className="dashboard-metrics">
|
||||
<article><Icon name="script" /><span><b>{scriptCount}</b><small>工作副本</small></span></article>
|
||||
<article><Icon name="workspace" /><span><b>2</b><small>Workspace</small></span></article>
|
||||
<article><Icon name="settings" /><span><b>4</b><small>平台员工</small></span></article>
|
||||
<article><Icon name="settings" /><span><b>4</b><small>平台用户</small></span></article>
|
||||
<article><Icon name="check" /><span><b>{online ? "正常" : "检查中"}</b><small>平台状态</small></span></article>
|
||||
</div>
|
||||
<div className="dashboard-actions">
|
||||
<button type="button" onClick={() => onNavigate("scripts")}><Icon name="script" />构建脚本</button>
|
||||
<button type="button" onClick={() => onNavigate("schedules")}><Icon name="schedule" />调度配置</button>
|
||||
<button type="button" onClick={() => onNavigate("system")}><Icon name="settings" />员工管理</button>
|
||||
<button type="button" onClick={() => onNavigate("system")}><Icon name="settings" />系统管理</button>
|
||||
</div>
|
||||
<div className="dashboard-grid">
|
||||
<section className="dashboard-panel dashboard-panel--trend">
|
||||
@@ -105,6 +106,11 @@ const EMPTY_FORM = {
|
||||
status: "active" as "active" | "disabled" | "locked",
|
||||
};
|
||||
|
||||
const EMPTY_PROJECT_FORM = {
|
||||
project_name: "",
|
||||
description: "",
|
||||
};
|
||||
|
||||
export function SystemAdminPage({
|
||||
onNotify,
|
||||
onConnectionChange,
|
||||
@@ -112,6 +118,7 @@ export function SystemAdminPage({
|
||||
onNotify: (notice: Notice) => void;
|
||||
onConnectionChange: (online: boolean) => void;
|
||||
}) {
|
||||
const [activeTab, setActiveTab] = useState<"users" | "projects">("users");
|
||||
const api = useApi();
|
||||
const { user, currentWorkspace } = useAuth();
|
||||
const [employees, setEmployees] = useState<Employee[]>([]);
|
||||
@@ -120,6 +127,8 @@ export function SystemAdminPage({
|
||||
const [editing, setEditing] = useState<Employee | null>(null);
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [form, setForm] = useState(EMPTY_FORM);
|
||||
const [projectDialogOpen, setProjectDialogOpen] = useState(false);
|
||||
const [projectForm, setProjectForm] = useState(EMPTY_PROJECT_FORM);
|
||||
const canManage = user?.role_code === "admin";
|
||||
|
||||
const load = async (): Promise<void> => {
|
||||
@@ -131,7 +140,7 @@ export function SystemAdminPage({
|
||||
onConnectionChange(false);
|
||||
onNotify({
|
||||
tone: "error",
|
||||
message: error instanceof Error ? error.message : "员工列表加载失败",
|
||||
message: error instanceof Error ? error.message : "用户列表加载失败",
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
@@ -160,7 +169,7 @@ export function SystemAdminPage({
|
||||
setDialogOpen(true);
|
||||
};
|
||||
|
||||
const submit = async (event: FormEvent): Promise<void> => {
|
||||
const submit = async (event: React.FormEvent): Promise<void> => {
|
||||
event.preventDefault();
|
||||
if (!form.display_name.trim() || (!editing && !form.username.trim())) return;
|
||||
setSaving(true);
|
||||
@@ -185,11 +194,11 @@ export function SystemAdminPage({
|
||||
setEmployees((current) => [...current, created]);
|
||||
}
|
||||
setDialogOpen(false);
|
||||
onNotify({ tone: "success", message: editing ? "员工信息已更新" : "员工已添加" });
|
||||
onNotify({ tone: "success", message: editing ? "用户信息已更新" : "用户已添加" });
|
||||
} catch (error) {
|
||||
onNotify({
|
||||
tone: "error",
|
||||
message: error instanceof ApiRequestError ? error.message : "保存员工失败",
|
||||
message: error instanceof ApiRequestError ? error.message : "保存用户失败",
|
||||
});
|
||||
} finally {
|
||||
setSaving(false);
|
||||
@@ -197,62 +206,178 @@ export function SystemAdminPage({
|
||||
};
|
||||
|
||||
const remove = async (employee: Employee): Promise<void> => {
|
||||
if (!window.confirm(`确定从当前 Workspace 删除员工“${employee.display_name}”吗?`)) return;
|
||||
if (!window.confirm(`确定从当前 Workspace 删除用户"${employee.display_name}"吗?`)) return;
|
||||
try {
|
||||
await api.deleteEmployee(employee.user_id);
|
||||
setEmployees((current) => current.filter((item) => item.user_id !== employee.user_id));
|
||||
onNotify({ tone: "success", message: "员工已删除" });
|
||||
onNotify({ tone: "success", message: "用户已删除" });
|
||||
} catch (error) {
|
||||
onNotify({
|
||||
tone: "error",
|
||||
message: error instanceof Error ? error.message : "删除员工失败",
|
||||
message: error instanceof Error ? error.message : "删除用户失败",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const openCreateProject = (): void => {
|
||||
setProjectForm(EMPTY_PROJECT_FORM);
|
||||
setProjectDialogOpen(true);
|
||||
};
|
||||
|
||||
const submitProject = async (event: React.FormEvent): Promise<void> => {
|
||||
event.preventDefault();
|
||||
if (!projectForm.project_name.trim()) return;
|
||||
setSaving(true);
|
||||
try {
|
||||
// TODO: 调用实际的项目创建 API
|
||||
// await api.createProject({ ... })
|
||||
setProjectDialogOpen(false);
|
||||
onNotify({ tone: "success", message: "项目已创建" });
|
||||
} catch (error) {
|
||||
onNotify({
|
||||
tone: "error",
|
||||
message: error instanceof ApiRequestError ? error.message : "创建项目失败",
|
||||
});
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="admin-page">
|
||||
<header className="admin-page__header">
|
||||
{/* <header className="admin-page__header">
|
||||
<div>
|
||||
<span>系统管理</span>
|
||||
<h2>员工管理</h2>
|
||||
<p>{currentWorkspace?.workspace_name ?? "(未选择 Workspace)"} · {employees.length} 名员工</p>
|
||||
<h3>{activeTab === "users" ? "用户管理" : "项目管理"}</h3>
|
||||
<p>
|
||||
{currentWorkspace?.workspace_name ?? "(未选择 Workspace)"}
|
||||
{activeTab === "users" ? ` · ${employees.length} 名用户` : ""}
|
||||
</p>
|
||||
</div>
|
||||
<button className="primary-button" type="button" disabled={!canManage} onClick={openCreate}>
|
||||
<Icon name="plus" size={15} />添加员工
|
||||
{activeTab === "users" && (
|
||||
<button className="primary-button" type="button" disabled={!canManage} onClick={openCreate}>
|
||||
<Icon name="plus" size={15} />添加用户
|
||||
</button>
|
||||
)}
|
||||
</header> */}
|
||||
|
||||
<div className="admin-tabs">
|
||||
<button
|
||||
className={`admin-tab${activeTab === "users" ? " is-active" : ""}`}
|
||||
type="button"
|
||||
onClick={() => setActiveTab("users")}
|
||||
>
|
||||
<Icon name="workspace" size={16} />
|
||||
用户管理
|
||||
</button>
|
||||
<button
|
||||
className={`admin-tab${activeTab === "projects" ? " is-active" : ""}`}
|
||||
type="button"
|
||||
onClick={() => setActiveTab("projects")}
|
||||
>
|
||||
<Icon name="folder" size={16} />
|
||||
项目管理
|
||||
</button>
|
||||
</header>
|
||||
{!canManage && <div className="admin-readonly">当前为开发人员,只能查看员工列表。</div>}
|
||||
<div className="employee-table">
|
||||
<div className="employee-table__head"><span>员工</span><span>账号</span><span>角色</span><span>状态</span><span>操作</span></div>
|
||||
{loading ? <p className="admin-empty">正在加载员工…</p> : employees.map((employee) => {
|
||||
const isProtectedAdmin = employee.role_code === "admin";
|
||||
return (
|
||||
<div className="employee-row" key={employee.user_id}>
|
||||
<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>
|
||||
<code>{employee.username}</code>
|
||||
<span className={`role-pill is-${employee.role_code}`}>{employee.role_name}</span>
|
||||
<span className={`status-pill is-${employee.status}`}>{employee.status === "active" ? "正常" : employee.status === "disabled" ? "已停用" : "已锁定"}</span>
|
||||
<span className="employee-actions">
|
||||
<button type="button" disabled={!canManage} onClick={() => openEdit(employee)}>编辑</button>
|
||||
<button type="button" className="is-danger" disabled={!canManage || isProtectedAdmin} title={isProtectedAdmin ? "管理员账号不能删除" : "删除"} onClick={() => void remove(employee)}>删除</button>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{dialogOpen && (
|
||||
{activeTab === "users" && (
|
||||
<div className="admin-toolbar">
|
||||
<div className="admin-toolbar__search">
|
||||
<Icon name="search" size={14} />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="请输入用户名"
|
||||
className="admin-toolbar__input"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
className="primary-button admin-toolbar__button"
|
||||
type="button"
|
||||
disabled={!canManage}
|
||||
onClick={openCreate}
|
||||
>
|
||||
<Icon name="plus" size={15} />
|
||||
新建用户
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === "projects" && (
|
||||
<div className="admin-toolbar">
|
||||
<div className="admin-toolbar__search">
|
||||
<Icon name="search" size={14} />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="请输入项目名称"
|
||||
className="admin-toolbar__input"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
className="primary-button admin-toolbar__button"
|
||||
type="button"
|
||||
disabled={!canManage}
|
||||
onClick={openCreateProject}
|
||||
>
|
||||
<Icon name="plus" size={15} />
|
||||
新建项目
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === "users" ? (
|
||||
<>
|
||||
{!canManage && <div className="admin-readonly">当前为开发人员,只能查看用户列表。</div>}
|
||||
<div className="employee-table">
|
||||
<div className="employee-table__head"><span>用户</span><span>账号</span><span>角色</span><span>状态</span><span>操作</span></div>
|
||||
{loading ? <p className="admin-empty">正在加载用户…</p> : employees.map((employee) => {
|
||||
const isProtectedAdmin = employee.role_code === "admin";
|
||||
return (
|
||||
<div className="employee-row" key={employee.user_id}>
|
||||
<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>
|
||||
<code>{employee.username}</code>
|
||||
<span className={`role-pill is-${employee.role_code}`}>{employee.role_name}</span>
|
||||
<span className={`status-pill is-${employee.status}`}>{employee.status === "active" ? "正常" : employee.status === "disabled" ? "已停用" : "已锁定"}</span>
|
||||
<span className="employee-actions">
|
||||
<button type="button" disabled={!canManage} onClick={() => openEdit(employee)}>编辑</button>
|
||||
<button type="button" className="is-danger" disabled={!canManage || isProtectedAdmin} title={isProtectedAdmin ? "管理员账号不能删除" : "删除"} onClick={() => void remove(employee)}>删除</button>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{dialogOpen && (
|
||||
<div className="modal-backdrop">
|
||||
<section className="modal modal--compact" role="dialog" aria-modal="true">
|
||||
<div className="modal__header"><div><span className="modal__eyebrow">EMPLOYEE</span><h2>{editing ? "编辑用户" : "添加用户"}</h2></div><button className="icon-button" type="button" onClick={() => setDialogOpen(false)}><Icon name="close" /></button></div>
|
||||
<form onSubmit={(event) => void submit(event)}>
|
||||
<label className="form-field"><span>姓名<span className="required">*</span></span><input autoFocus value={form.display_name} onChange={(event) => setForm({ ...form, display_name: event.target.value })} /></label>
|
||||
<label className="form-field"><span>登录账号<span className="required">*</span></span><input disabled={Boolean(editing)} value={form.username} onChange={(event) => setForm({ ...form, username: event.target.value })} /></label>
|
||||
<label className="form-field"><span>邮箱</span><input type="email" value={form.email} onChange={(event) => setForm({ ...form, email: event.target.value })} /></label>
|
||||
<label className="form-field"><span>角色</span><select value={form.role_code} onChange={(event) => setForm({ ...form, role_code: event.target.value as "admin" | "developer" })}><option value="developer">开发人员</option><option value="admin">管理员</option></select></label>
|
||||
{editing && <label className="form-field"><span>状态</span><select value={form.status} onChange={(event) => setForm({ ...form, status: event.target.value as typeof form.status })}><option value="active">正常</option><option value="disabled">停用</option><option value="locked">锁定</option></select></label>}
|
||||
<div className="modal__footer"><button className="secondary-button" type="button" onClick={() => setDialogOpen(false)}>取消</button><button className="primary-button" type="submit" disabled={saving}>{saving ? "保存中…" : "保存"}</button></div>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="project-list-placeholder">
|
||||
<Icon name="folder" size={42} />
|
||||
<h3>项目管理</h3>
|
||||
<p>项目管理功能开发中…</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{projectDialogOpen && (
|
||||
<div className="modal-backdrop">
|
||||
<section className="modal modal--compact" role="dialog" aria-modal="true">
|
||||
<div className="modal__header"><div><span className="modal__eyebrow">EMPLOYEE</span><h2>{editing ? "编辑员工" : "添加员工"}</h2></div><button className="icon-button" type="button" onClick={() => setDialogOpen(false)}><Icon name="close" /></button></div>
|
||||
<form onSubmit={(event) => void submit(event)}>
|
||||
<label className="form-field"><span>姓名</span><input autoFocus value={form.display_name} onChange={(event) => setForm({ ...form, display_name: event.target.value })} /></label>
|
||||
<label className="form-field"><span>登录账号</span><input disabled={Boolean(editing)} value={form.username} onChange={(event) => setForm({ ...form, username: event.target.value })} /></label>
|
||||
<label className="form-field"><span>邮箱</span><input type="email" value={form.email} onChange={(event) => setForm({ ...form, email: event.target.value })} /></label>
|
||||
<label className="form-field"><span>角色</span><select value={form.role_code} onChange={(event) => setForm({ ...form, role_code: event.target.value as "admin" | "developer" })}><option value="developer">开发人员</option><option value="admin">管理员</option></select></label>
|
||||
{editing && <label className="form-field"><span>状态</span><select value={form.status} onChange={(event) => setForm({ ...form, status: event.target.value as typeof form.status })}><option value="active">正常</option><option value="disabled">停用</option><option value="locked">锁定</option></select></label>}
|
||||
<div className="modal__footer"><button className="secondary-button" type="button" onClick={() => setDialogOpen(false)}>取消</button><button className="primary-button" type="submit" disabled={saving}>{saving ? "保存中…" : "保存"}</button></div>
|
||||
<div className="modal__header"><div><span className="modal__eyebrow">PROJECT</span><h2>新建项目</h2></div><button className="icon-button" type="button" onClick={() => setProjectDialogOpen(false)}><Icon name="close" /></button></div>
|
||||
<form onSubmit={(event) => void submitProject(event)}>
|
||||
<label className="form-field"><span>项目名称<span className="required">*</span></span><input autoFocus value={projectForm.project_name} onChange={(event) => setProjectForm({ ...projectForm, project_name: event.target.value })} placeholder="请输入项目名称" /></label>
|
||||
<label className="form-field"><span>项目描述</span><textarea value={projectForm.description} onChange={(event) => setProjectForm({ ...projectForm, description: event.target.value })} placeholder="请输入项目描述(可选)" rows={4} /></label>
|
||||
<div className="modal__footer"><button className="secondary-button" type="button" onClick={() => setProjectDialogOpen(false)}>取消</button><button className="primary-button" type="submit" disabled={saving}>{saving ? "创建中…" : "创建"}</button></div>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -173,6 +173,7 @@ function AuthenticatedModelPlatformApp() {
|
||||
const [publishing, setPublishing] = useState(false);
|
||||
const [publishedVersion, setPublishedVersion] =
|
||||
useState<StableVersion | null>(null);
|
||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
||||
|
||||
const load = async (silent = false) => {
|
||||
if (!silent) setLoading(true);
|
||||
@@ -800,12 +801,13 @@ function AuthenticatedModelPlatformApp() {
|
||||
|
||||
return (
|
||||
<div className="app-shell">
|
||||
<aside className="sidebar">
|
||||
<aside className={`sidebar${sidebarCollapsed ? " is-collapsed" : ""}`}>
|
||||
<div className="brand">
|
||||
<span className="brand__mark"><Icon name="brand" size={31} /></span>
|
||||
<span className="brand__name">模型实验开发平台</span>
|
||||
{!sidebarCollapsed && <span className="brand__name">模型实验开发平台</span>}
|
||||
</div>
|
||||
|
||||
{!sidebarCollapsed && (
|
||||
<nav className="navigation" aria-label="主导航">
|
||||
{navigation.map((item) => (
|
||||
<button
|
||||
@@ -830,10 +832,11 @@ function AuthenticatedModelPlatformApp() {
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
)}
|
||||
|
||||
<button className="sidebar-footer" type="button">
|
||||
<button className="sidebar-footer" type="button" onClick={() => setSidebarCollapsed((v) => !v)}>
|
||||
<Icon name="menu" size={19} />
|
||||
<span>收起菜单</span>
|
||||
<span>{sidebarCollapsed ? "展开菜单" : "收起菜单"}</span>
|
||||
</button>
|
||||
</aside>
|
||||
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
.dashboard-page,.admin-page{height:100%;padding:24px;overflow:auto;background:#f3f6f9}.dashboard-hero{display:flex;align-items:center;justify-content:space-between;padding:34px;border-radius:12px;color:#fff;background:linear-gradient(125deg,#0b3d69,#1978d4);box-shadow:0 12px 30px rgb(19 80 137/18%)}.dashboard-hero span{font-size:10px;letter-spacing:.12em;opacity:.82}.dashboard-hero h2{margin:8px 0 5px;font-size:25px}.dashboard-hero p{margin:0;font-size:12px;opacity:.88}.dashboard-hero__badge{padding:8px 12px;border-radius:20px;background:rgb(255 255 255/16%)}.dashboard-metrics{display:grid;grid-template-columns:repeat(4,1fr);gap:14px;margin-top:18px}.dashboard-metrics article{display:flex;align-items:center;gap:14px;padding:20px;border:1px solid #e0e8ef;border-radius:9px;background:#fff}.dashboard-metrics svg{color:#1978d4}.dashboard-metrics span{display:flex;flex-direction:column}.dashboard-metrics b{color:#20364c;font-size:21px}.dashboard-metrics small{color:#8796a6}.dashboard-actions{display:flex;gap:12px;margin-top:18px}.dashboard-actions button{display:flex;align-items:center;gap:8px;padding:13px 18px;border:1px solid #d8e4ef;border-radius:7px;color:#346587;background:#fff;cursor:pointer}.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 #dfe7ee;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}
|
||||
.dashboard-page,.admin-page{height:100%;padding:24px;overflow:auto;background:#f3f6f9}.dashboard-hero{display:flex;align-items:center;justify-content:space-between;padding:34px;border-radius:12px;color:#fff;background:linear-gradient(125deg,#0b3d69,#1978d4);box-shadow:0 12px 30px rgb(19 80 137/18%)}.dashboard-hero span{font-size:10px;letter-spacing:.12em;opacity:.82}.dashboard-hero h2{margin:8px 0 5px;font-size:25px}.dashboard-hero p{margin:0;font-size:12px;opacity:.88}.dashboard-hero__badge{padding:8px 12px;border-radius:20px;background:rgb(255 255 255/16%)}.dashboard-metrics{display:grid;grid-template-columns:repeat(4,1fr);gap:14px;margin-top:18px}.dashboard-metrics article{display:flex;align-items:center;gap:14px;padding:20px;border:1px solid #e0e8ef;border-radius:9px;background:#fff}.dashboard-metrics svg{color:#1978d4}.dashboard-metrics span{display:flex;flex-direction:column}.dashboard-metrics b{color:#20364c;font-size:21px}.dashboard-metrics small{color:#8796a6}.dashboard-actions{display:flex;gap:12px;margin-top:18px}.dashboard-actions button{display:flex;align-items:center;gap:8px;padding:13px 18px;border:1px solid #d8e4ef;border-radius:7px;color:#346587;background:#fff;cursor:pointer}.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-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}
|
||||
|
||||
@@ -48,6 +48,37 @@ button {
|
||||
radial-gradient(circle at 10% 1%, rgb(28 105 186 / 25%), transparent 27%),
|
||||
#09233f;
|
||||
box-shadow: 5px 0 18px rgb(19 47 77 / 9%);
|
||||
transition: width 0.2s ease, min-width 0.2s ease;
|
||||
}
|
||||
|
||||
.sidebar.is-collapsed {
|
||||
width: 70px;
|
||||
min-width: 70px;
|
||||
}
|
||||
|
||||
.sidebar.is-collapsed .brand__name,
|
||||
.sidebar.is-collapsed .sidebar-footer span,
|
||||
.sidebar.is-collapsed .nav-item span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sidebar.is-collapsed .brand {
|
||||
justify-content: center;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.sidebar.is-collapsed .sidebar-footer {
|
||||
width: calc(100% - 18px);
|
||||
justify-content: center;
|
||||
padding-left: 17px;
|
||||
padding-right: 17px;
|
||||
}
|
||||
|
||||
.sidebar.is-collapsed .nav-item {
|
||||
justify-content: center;
|
||||
padding-left: 17px;
|
||||
padding-right: 17px;
|
||||
}
|
||||
|
||||
.brand {
|
||||
@@ -194,6 +225,9 @@ button {
|
||||
|
||||
.topbar-menu-wrap {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
}
|
||||
|
||||
.topbar-dropdown {
|
||||
@@ -328,7 +362,7 @@ button {
|
||||
}
|
||||
|
||||
.user-menu__copy {
|
||||
min-width: 55px;
|
||||
min-width: 50px;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
|
||||
Reference in New Issue
Block a user