update:css样式拆分
This commit is contained in:
@@ -1,8 +1,19 @@
|
||||
import Icon from "../../components/common/Icon";
|
||||
import { useAuth } from "../../context/AuthContext";
|
||||
|
||||
import "../../styles/admin.css";
|
||||
import "../../styles/dashboard.css";
|
||||
const TREND_VALUES = [38, 55, 44, 73, 61, 86, 78] as const;
|
||||
const TREND_LABELS = ["周一", "周二", "周三", "周四", "周五", "周六", "今天"] as const;
|
||||
|
||||
const ACTIVITY_ROWS = [
|
||||
["数据探索.ipynb 发布稳定版本 v3.0", "张三", "成功", "16:42"],
|
||||
["每日模型训练流程完成调度运行", "Scheduler", "成功", "15:25"],
|
||||
["批量预测.py 更新工作副本", "王五", "已同步", "14:18"],
|
||||
["风险验证流程完成 DAG 校验", "李四", "成功", "11:06"],
|
||||
] as const;
|
||||
|
||||
function LegendDot({ className }: { className: string }) {
|
||||
return <i className={`inline-block size-[7px] rounded-full ${className}`} />;
|
||||
}
|
||||
|
||||
export function DashboardPage({
|
||||
scriptCount,
|
||||
@@ -15,71 +26,217 @@ export function DashboardPage({
|
||||
}) {
|
||||
const { user, currentWorkspace } = useAuth();
|
||||
const isSystemAdmin = user?.is_system_admin ?? false;
|
||||
const notebookCount = Math.max(1, Math.round(scriptCount * 0.67));
|
||||
const pythonCount = Math.max(0, scriptCount - Math.round(scriptCount * 0.67));
|
||||
|
||||
return (
|
||||
<section className="dashboard-page">
|
||||
<div className="dashboard-hero">
|
||||
<section className="h-full overflow-auto bg-bg p-6">
|
||||
<div className="dashboard-hero-shell flex items-center justify-between rounded-xl px-[34px] py-[34px] text-white shadow-[0_12px_30px_rgb(19_80_137/18%)]">
|
||||
<div>
|
||||
<span>MODEL DEVELOPMENT PLATFORM</span>
|
||||
<h2>下午好,{user?.display_name ?? "用户"}</h2>
|
||||
<p>
|
||||
<span className="text-[10px] tracking-[0.12em] opacity-80">
|
||||
MODEL DEVELOPMENT PLATFORM
|
||||
</span>
|
||||
<h2 className="my-2 mb-[5px] text-[25px]">
|
||||
下午好,{user?.display_name ?? "用户"}
|
||||
</h2>
|
||||
<p className="m-0 text-xs opacity-90">
|
||||
当前位于 {currentWorkspace?.workspace_name ?? "(未选择 Workspace)"}
|
||||
,可以继续构建脚本或配置调度。
|
||||
</p>
|
||||
</div>
|
||||
<span className="dashboard-hero__badge">{online ? "服务正常" : "服务连接中"}</span>
|
||||
<span className="rounded-[20px] bg-white/16 px-3 py-2 text-sm">
|
||||
{online ? "服务正常" : "服务连接中"}
|
||||
</span>
|
||||
</div>
|
||||
<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="check" /><span><b>{online ? "正常" : "检查中"}</b><small>平台状态</small></span></article>
|
||||
|
||||
<div className="mt-[18px] grid grid-cols-4 gap-3.5">
|
||||
{[
|
||||
{ icon: "script" as const, value: scriptCount, label: "工作副本" },
|
||||
{ icon: "workspace" as const, value: 2, label: "Workspace" },
|
||||
{ icon: "settings" as const, value: 4, label: "平台用户" },
|
||||
{
|
||||
icon: "check" as const,
|
||||
value: online ? "正常" : "检查中",
|
||||
label: "平台状态",
|
||||
},
|
||||
].map((item) => (
|
||||
<article
|
||||
key={item.label}
|
||||
className="flex items-center gap-3.5 rounded-[9px] border border-[#e0e8ef] bg-white p-5"
|
||||
>
|
||||
<span className="text-brand">
|
||||
<Icon name={item.icon} />
|
||||
</span>
|
||||
<span className="flex flex-col">
|
||||
<b className="text-[21px] text-[#20364c]">{item.value}</b>
|
||||
<small className="text-[#8796a6]">{item.label}</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>
|
||||
|
||||
<div className="mt-[18px] flex gap-3">
|
||||
<button
|
||||
type="button"
|
||||
className="flex cursor-pointer items-center gap-2 rounded-[7px] border border-[#d8e4ef] bg-white px-[18px] py-[13px] text-[#346587]"
|
||||
onClick={() => onNavigate("scripts")}
|
||||
>
|
||||
<Icon name="script" />
|
||||
构建脚本
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="flex cursor-pointer items-center gap-2 rounded-[7px] border border-[#d8e4ef] bg-white px-[18px] py-[13px] text-[#346587]"
|
||||
onClick={() => onNavigate("schedules")}
|
||||
>
|
||||
<Icon name="schedule" />
|
||||
调度配置
|
||||
</button>
|
||||
{isSystemAdmin && (
|
||||
<button type="button" onClick={() => onNavigate("system")}><Icon name="settings" />系统管理</button>
|
||||
<button
|
||||
type="button"
|
||||
className="flex cursor-pointer items-center gap-2 rounded-[7px] border border-[#d8e4ef] bg-white px-[18px] py-[13px] text-[#346587]"
|
||||
onClick={() => onNavigate("system")}
|
||||
>
|
||||
<Icon name="settings" />
|
||||
系统管理
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="dashboard-grid">
|
||||
<section className="dashboard-panel dashboard-panel--trend">
|
||||
<header><div><span>运行趋势</span><h3>近 7 天调度执行</h3></div><b>成功率 92.6%</b></header>
|
||||
<div className="trend-chart">
|
||||
{[38, 55, 44, 73, 61, 86, 78].map((value, index) => (
|
||||
<div className="trend-column" key={index}>
|
||||
<span className="trend-column__value">{Math.round(value / 7)}</span>
|
||||
<i style={{ height: `${value}%` }} />
|
||||
<small>{["周一", "周二", "周三", "周四", "周五", "周六", "今天"][index]}</small>
|
||||
|
||||
<div className="mt-[18px] grid gap-3.5 pb-[25px] max-[1200px]:grid-cols-1 min-[1201px]:grid-cols-[minmax(0,1.6fr)_minmax(310px,0.9fr)]">
|
||||
<section className="rounded-[10px] border border-[#dfe7ee] bg-white shadow-[0_5px_16px_rgb(33_65_97/5%)]">
|
||||
<header className="flex items-center justify-between border-b border-line-soft px-5 py-[17px]">
|
||||
<div>
|
||||
<span className="text-[9px] font-extrabold tracking-[0.09em] text-[#7890a6]">
|
||||
运行趋势
|
||||
</span>
|
||||
<h3 className="mt-1 text-sm text-[#263b50]">近 7 天调度执行</h3>
|
||||
</div>
|
||||
<b className="rounded-[13px] bg-[#e8f8f1] px-[9px] py-[5px] text-[10px] text-[#16855a]">
|
||||
成功率 92.6%
|
||||
</b>
|
||||
</header>
|
||||
<div className="trend-chart-bg flex h-[190px] items-end gap-[18px] px-7 pb-[17px] pt-[26px]">
|
||||
{TREND_VALUES.map((value, index) => (
|
||||
<div
|
||||
className="flex h-full flex-1 flex-col items-center justify-end gap-[5px]"
|
||||
key={TREND_LABELS[index]}
|
||||
>
|
||||
<span className="text-[9px] text-[#7890a3]">
|
||||
{Math.round(value / 7)}
|
||||
</span>
|
||||
<i
|
||||
className="block min-h-3 w-[min(36px,72%)] rounded-t-[5px] rounded-b-[2px] bg-gradient-to-b from-[#49a0eb] to-[#207bd1] shadow-[0_4px_9px_rgb(32_123_209/18%)]"
|
||||
style={{ height: `${value}%` }}
|
||||
/>
|
||||
<small className="text-[9px] text-[#7890a3]">
|
||||
{TREND_LABELS[index]}
|
||||
</small>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<footer><span><i className="legend-dot is-success" />成功 75</span><span><i className="legend-dot is-failed" />失败 6</span></footer>
|
||||
<footer className="flex justify-center gap-[22px] p-2.5 text-[9px] text-[#708397]">
|
||||
<span className="flex items-center gap-[5px]">
|
||||
<LegendDot className="bg-[#27b87c]" />
|
||||
成功 75
|
||||
</span>
|
||||
<span className="flex items-center gap-[5px]">
|
||||
<LegendDot className="bg-[#ed7777]" />
|
||||
失败 6
|
||||
</span>
|
||||
</footer>
|
||||
</section>
|
||||
<section className="dashboard-panel dashboard-panel--donut">
|
||||
<header><div><span>脚本资产</span><h3>类型分布</h3></div></header>
|
||||
<div className="donut-layout">
|
||||
<div className="donut-chart"><span><b>{scriptCount}</b><small>全部脚本</small></span></div>
|
||||
<div className="donut-legend">
|
||||
<span><i className="legend-dot is-notebook" /><b>Notebook</b><small>{Math.max(1, Math.round(scriptCount * .67))} 个 · 67%</small></span>
|
||||
<span><i className="legend-dot is-python" /><b>Python</b><small>{Math.max(0, scriptCount - Math.round(scriptCount * .67))} 个 · 33%</small></span>
|
||||
<span><i className="legend-dot is-version" /><b>稳定版本</b><small>3 个已发布</small></span>
|
||||
|
||||
<section className="rounded-[10px] border border-[#dfe7ee] bg-white shadow-[0_5px_16px_rgb(33_65_97/5%)]">
|
||||
<header className="flex items-center justify-between border-b border-line-soft px-5 py-[17px]">
|
||||
<div>
|
||||
<span className="text-[9px] font-extrabold tracking-[0.09em] text-[#7890a6]">
|
||||
脚本资产
|
||||
</span>
|
||||
<h3 className="mt-1 text-sm text-[#263b50]">类型分布</h3>
|
||||
</div>
|
||||
</header>
|
||||
<div className="flex min-h-[226px] items-center justify-center gap-7 p-5">
|
||||
<div className="dashboard-donut relative grid size-32 shrink-0 place-items-center rounded-full">
|
||||
<span className="z-[1] flex flex-col items-center">
|
||||
<b className="text-xl text-[#273d53]">{scriptCount}</b>
|
||||
<small className="text-[8px] text-[#8a9aaa]">全部脚本</small>
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col gap-[15px]">
|
||||
{[
|
||||
{
|
||||
color: "bg-[#318de0]",
|
||||
title: "Notebook",
|
||||
detail: `${notebookCount} 个 · 67%`,
|
||||
},
|
||||
{
|
||||
color: "bg-[#72c9b0]",
|
||||
title: "Python",
|
||||
detail: `${pythonCount} 个 · 33%`,
|
||||
},
|
||||
{
|
||||
color: "bg-[#f3b75c]",
|
||||
title: "稳定版本",
|
||||
detail: "3 个已发布",
|
||||
},
|
||||
].map((item) => (
|
||||
<span
|
||||
key={item.title}
|
||||
className="grid grid-cols-[8px_1fr] items-center gap-x-[7px]"
|
||||
>
|
||||
<LegendDot className={item.color} />
|
||||
<b className="text-[10px] text-[#40566c]">{item.title}</b>
|
||||
<small className="col-start-2 text-[8px] text-[#8a9baa]">
|
||||
{item.detail}
|
||||
</small>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section className="dashboard-panel dashboard-panel--activity">
|
||||
<header><div><span>ACTIVITY</span><h3>最近平台活动</h3></div><button type="button">查看全部</button></header>
|
||||
<div className="activity-table">
|
||||
<div className="activity-table__head"><span>操作内容</span><span>执行人</span><span>状态</span><span>时间</span></div>
|
||||
{[
|
||||
["数据探索.ipynb 发布稳定版本 v3.0", "张三", "成功", "16:42"],
|
||||
["每日模型训练流程完成调度运行", "Scheduler", "成功", "15:25"],
|
||||
["批量预测.py 更新工作副本", "王五", "已同步", "14:18"],
|
||||
["风险验证流程完成 DAG 校验", "李四", "成功", "11:06"],
|
||||
].map((row) => (
|
||||
<div className="activity-row" key={row[0]}>
|
||||
<span><i className="activity-icon"><Icon name="check" size={13} /></i>{row[0]}</span>
|
||||
<span>{row[1]}</span><span><b>{row[2]}</b></span><span>{row[3]}</span>
|
||||
|
||||
<section className="col-span-full rounded-[10px] border border-[#dfe7ee] bg-white shadow-[0_5px_16px_rgb(33_65_97/5%)] max-[1200px]:col-auto">
|
||||
<header className="flex items-center justify-between border-b border-line-soft px-5 py-[17px]">
|
||||
<div>
|
||||
<span className="text-[9px] font-extrabold tracking-[0.09em] text-[#7890a6]">
|
||||
ACTIVITY
|
||||
</span>
|
||||
<h3 className="mt-1 text-sm text-[#263b50]">最近平台活动</h3>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="border-0 bg-transparent text-[10px] text-[#257bcb]"
|
||||
>
|
||||
查看全部
|
||||
</button>
|
||||
</header>
|
||||
<div>
|
||||
<div className="grid grid-cols-[2fr_0.7fr_0.55fr_0.5fr] items-center gap-[15px] bg-[#f7f9fb] px-5 py-[11px] text-[9px] font-bold text-[#8595a5]">
|
||||
<span>操作内容</span>
|
||||
<span>执行人</span>
|
||||
<span>状态</span>
|
||||
<span>时间</span>
|
||||
</div>
|
||||
{ACTIVITY_ROWS.map((row) => (
|
||||
<div
|
||||
className="grid grid-cols-[2fr_0.7fr_0.55fr_0.5fr] items-center gap-[15px] border-t border-line-soft px-5 py-[11px] text-[10px] text-[#607488]"
|
||||
key={row[0]}
|
||||
>
|
||||
<span className="flex items-center gap-[9px] text-[#344c63]">
|
||||
<i className="grid size-[25px] place-items-center rounded-md bg-[#eaf4ff] text-[#277fcc]">
|
||||
<Icon name="check" size={13} />
|
||||
</i>
|
||||
{row[0]}
|
||||
</span>
|
||||
<span>{row[1]}</span>
|
||||
<span>
|
||||
<b className="rounded-[10px] bg-[#e9f8f2] px-[7px] py-[3px] text-[9px] text-[#16855a]">
|
||||
{row[2]}
|
||||
</b>
|
||||
</span>
|
||||
<span>{row[3]}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,21 @@ import { ApiRequestError, type Employee, type Workspace, type WorkspaceMember }
|
||||
import { useApi, useAuth } from "../../context/AuthContext";
|
||||
import Icon from "../../components/common/Icon";
|
||||
import { UserMultiSelect } from "./UserMultiSelect";
|
||||
import {
|
||||
formFieldClass,
|
||||
formInputClass,
|
||||
formTextareaClass,
|
||||
iconButtonClass,
|
||||
modalBackdropClass,
|
||||
modalCompactClass,
|
||||
modalEyebrowClass,
|
||||
modalFooterClass,
|
||||
modalFormClass,
|
||||
modalHeaderClass,
|
||||
modalTitleClass,
|
||||
primaryButtonClass,
|
||||
secondaryButtonClass,
|
||||
} from "../platform/modalUi";
|
||||
|
||||
import "../../styles/admin.css";
|
||||
|
||||
@@ -199,24 +214,6 @@ export function ProjectManagementPage({
|
||||
}
|
||||
};
|
||||
|
||||
const updateMemberRole = async (userId: string, roleCode: "admin" | "developer"): Promise<void> => {
|
||||
if (!selectedProject) return;
|
||||
try {
|
||||
const updated = await api.updateWorkspaceMember(selectedProject.workspace_id, userId, {
|
||||
role_code: roleCode,
|
||||
});
|
||||
setCurrentProjectMembers((current) =>
|
||||
current.map((m) => (m.user_id === userId ? { ...m, role_code: roleCode, role_name: roleCode === "admin" ? "管理员" : "开发人员" } : m))
|
||||
);
|
||||
onNotify({ tone: "success", message: "成员角色已更新" });
|
||||
} catch (error) {
|
||||
onNotify({
|
||||
tone: "error",
|
||||
message: error instanceof ApiRequestError ? error.message : "更新成员角色失败",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const importMember = async (): Promise<void> => {
|
||||
if (!selectedProject || selectedUserIds.length === 0) {
|
||||
onNotify({ tone: "error", message: "请选择要添加的用户" });
|
||||
@@ -327,20 +324,23 @@ export function ProjectManagementPage({
|
||||
</div>
|
||||
|
||||
{projectDialogOpen && (
|
||||
<div className="modal-backdrop">
|
||||
<section className="modal modal--compact" role="dialog" aria-modal="true">
|
||||
<div className="modal__header">
|
||||
<div className={modalBackdropClass} role="presentation">
|
||||
<section className={modalCompactClass} role="dialog" aria-modal="true">
|
||||
<div className={modalHeaderClass}>
|
||||
<div>
|
||||
<span className="modal__eyebrow">PROJECT</span>
|
||||
<h2>{editingProject ? "编辑项目" : "新建项目"}</h2>
|
||||
<span className={modalEyebrowClass}>PROJECT</span>
|
||||
<h2 className={modalTitleClass}>{editingProject ? "编辑项目" : "新建项目"}</h2>
|
||||
</div>
|
||||
<button className="icon-button" type="button" onClick={() => setProjectDialogOpen(false)}><Icon name="close" /></button>
|
||||
<button className={iconButtonClass} type="button" aria-label="关闭" onClick={() => setProjectDialogOpen(false)}>
|
||||
<Icon name="close" />
|
||||
</button>
|
||||
</div>
|
||||
<form onSubmit={(event) => void submitProject(event)}>
|
||||
<form className={modalFormClass} onSubmit={(event) => void submitProject(event)}>
|
||||
{!editingProject && (
|
||||
<label className="form-field">
|
||||
<span>项目编码<span className="required">*</span></span>
|
||||
<label className={formFieldClass}>
|
||||
<span>项目编码<span className="text-[#e74c3c]">*</span></span>
|
||||
<input
|
||||
className={formInputClass}
|
||||
autoFocus
|
||||
value={projectForm.workspace_code}
|
||||
onChange={(event) => setProjectForm({ ...projectForm, workspace_code: event.target.value })}
|
||||
@@ -348,35 +348,38 @@ export function ProjectManagementPage({
|
||||
/>
|
||||
</label>
|
||||
)}
|
||||
<label className="form-field">
|
||||
<span>项目名称<span className="required">*</span></span>
|
||||
<label className={formFieldClass}>
|
||||
<span>项目名称<span className="text-[#e74c3c]">*</span></span>
|
||||
<input
|
||||
className={formInputClass}
|
||||
value={projectForm.workspace_name}
|
||||
onChange={(event) => setProjectForm({ ...projectForm, workspace_name: event.target.value })}
|
||||
placeholder="请输入项目名称"
|
||||
/>
|
||||
</label>
|
||||
<label className="form-field">
|
||||
<label className={formFieldClass}>
|
||||
<span>配额(字节)</span>
|
||||
<input
|
||||
className={formInputClass}
|
||||
type="number"
|
||||
value={projectForm.quota_bytes}
|
||||
onChange={(event) => setProjectForm({ ...projectForm, quota_bytes: parseInt(event.target.value) || 0 })}
|
||||
placeholder="0 表示无配额限制"
|
||||
/>
|
||||
</label>
|
||||
<label className="form-field">
|
||||
<label className={formFieldClass}>
|
||||
<span>项目描述</span>
|
||||
<textarea
|
||||
className={formTextareaClass}
|
||||
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 className={modalFooterClass}>
|
||||
<button className={secondaryButtonClass} type="button" onClick={() => setProjectDialogOpen(false)}>取消</button>
|
||||
<button className={primaryButtonClass} type="submit" disabled={saving}>{saving ? "保存中…" : "保存"}</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
@@ -384,18 +387,20 @@ export function ProjectManagementPage({
|
||||
)}
|
||||
|
||||
{importMemberDialogOpen && (
|
||||
<div className="modal-backdrop">
|
||||
<section className="modal modal--compact" role="dialog" aria-modal="true">
|
||||
<div className="modal__header">
|
||||
<div className={modalBackdropClass} role="presentation">
|
||||
<section className={modalCompactClass} role="dialog" aria-modal="true">
|
||||
<div className={modalHeaderClass}>
|
||||
<div>
|
||||
<span className="modal__eyebrow">IMPORT MEMBER</span>
|
||||
<h2>导入成员到 {selectedProject?.workspace_name ?? "项目"}</h2>
|
||||
<span className={modalEyebrowClass}>IMPORT MEMBER</span>
|
||||
<h2 className={modalTitleClass}>导入成员到 {selectedProject?.workspace_name ?? "项目"}</h2>
|
||||
</div>
|
||||
<button className="icon-button" type="button" onClick={() => setImportMemberDialogOpen(false)}><Icon name="close" /></button>
|
||||
<button className={iconButtonClass} type="button" aria-label="关闭" onClick={() => setImportMemberDialogOpen(false)}>
|
||||
<Icon name="close" />
|
||||
</button>
|
||||
</div>
|
||||
<form >
|
||||
<label className="form-field" style={{ paddingBottom: 120 }}>
|
||||
<span>选择用户<span className="required">*</span></span>
|
||||
<form className={modalFormClass}>
|
||||
<label className={`${formFieldClass} pb-[120px]`}>
|
||||
<span>选择用户<span className="text-[#e74c3c]">*</span></span>
|
||||
<UserMultiSelect
|
||||
users={availableUsers}
|
||||
selectedUserIds={selectedUserIds}
|
||||
@@ -403,9 +408,9 @@ export function ProjectManagementPage({
|
||||
existingMemberIds={currentProjectMembers.map((m) => m.user_id)}
|
||||
/>
|
||||
</label>
|
||||
<div className="modal__footer">
|
||||
<button className="secondary-button" type="button" onClick={() => setImportMemberDialogOpen(false)}>取消</button>
|
||||
<button className="primary-button" type="button" disabled={saving || selectedUserIds.length === 0} onClick={() => void importMember()}>
|
||||
<div className={modalFooterClass}>
|
||||
<button className={secondaryButtonClass} type="button" onClick={() => setImportMemberDialogOpen(false)}>取消</button>
|
||||
<button className={primaryButtonClass} type="button" disabled={saving || selectedUserIds.length === 0} onClick={() => void importMember()}>
|
||||
{saving ? "添加中…" : `添加 (${selectedUserIds.length})`}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,20 @@ import { useEffect, useState } from "react";
|
||||
import { ApiRequestError, type Employee } from "../../services/api";
|
||||
import { useApi, useAuth } from "../../context/AuthContext";
|
||||
import Icon from "../../components/common/Icon";
|
||||
import {
|
||||
formFieldClass,
|
||||
formInputClass,
|
||||
iconButtonClass,
|
||||
modalBackdropClass,
|
||||
modalCompactClass,
|
||||
modalEyebrowClass,
|
||||
modalFooterClass,
|
||||
modalFormClass,
|
||||
modalHeaderClass,
|
||||
modalTitleClass,
|
||||
primaryButtonClass,
|
||||
secondaryButtonClass,
|
||||
} from "../platform/modalUi";
|
||||
|
||||
import "../../styles/admin.css";
|
||||
|
||||
@@ -215,28 +229,32 @@ export function UserManagementPage({
|
||||
</div>
|
||||
|
||||
{dialogOpen && (
|
||||
<div className="modal-backdrop">
|
||||
<section className="modal modal--compact" role="dialog" aria-modal="true">
|
||||
<div className="modal__header">
|
||||
<div className={modalBackdropClass} role="presentation">
|
||||
<section className={modalCompactClass} role="dialog" aria-modal="true">
|
||||
<div className={modalHeaderClass}>
|
||||
<div>
|
||||
<span className="modal__eyebrow">EMPLOYEE</span>
|
||||
<h2>{editing ? "编辑用户" : "添加用户"}</h2>
|
||||
<span className={modalEyebrowClass}>EMPLOYEE</span>
|
||||
<h2 className={modalTitleClass}>{editing ? "编辑用户" : "添加用户"}</h2>
|
||||
</div>
|
||||
<button className="icon-button" type="button" onClick={() => setDialogOpen(false)}><Icon name="close" /></button>
|
||||
<button className={iconButtonClass} type="button" aria-label="关闭" onClick={() => setDialogOpen(false)}>
|
||||
<Icon name="close" />
|
||||
</button>
|
||||
</div>
|
||||
<form onSubmit={(event) => void submit(event)}>
|
||||
<label className="form-field">
|
||||
<span>姓名<span className="required">*</span></span>
|
||||
<form className={modalFormClass} onSubmit={(event) => void submit(event)}>
|
||||
<label className={formFieldClass}>
|
||||
<span>姓名<span className="text-[#e74c3c]">*</span></span>
|
||||
<input
|
||||
className={formInputClass}
|
||||
autoFocus={!editing}
|
||||
value={form.display_name}
|
||||
onChange={(event) => setForm({ ...form, display_name: event.target.value })}
|
||||
placeholder="请输入姓名"
|
||||
/>
|
||||
</label>
|
||||
<label className="form-field">
|
||||
<span>登录账号<span className="required">*</span></span>
|
||||
<label className={formFieldClass}>
|
||||
<span>登录账号<span className="text-[#e74c3c]">*</span></span>
|
||||
<input
|
||||
className={formInputClass}
|
||||
disabled={Boolean(editing)}
|
||||
value={form.username}
|
||||
onChange={(event) => setForm({ ...form, username: event.target.value })}
|
||||
@@ -245,9 +263,10 @@ export function UserManagementPage({
|
||||
/>
|
||||
</label>
|
||||
{!editing && (
|
||||
<label className="form-field">
|
||||
<span>密码<span className="required">*</span></span>
|
||||
<label className={formFieldClass}>
|
||||
<span>密码<span className="text-[#e74c3c]">*</span></span>
|
||||
<input
|
||||
className={formInputClass}
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
value={form.password}
|
||||
@@ -256,9 +275,10 @@ export function UserManagementPage({
|
||||
/>
|
||||
</label>
|
||||
)}
|
||||
<label className="form-field">
|
||||
<label className={formFieldClass}>
|
||||
<span>邮箱</span>
|
||||
<input
|
||||
className={formInputClass}
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
value={form.email}
|
||||
@@ -266,26 +286,24 @@ export function UserManagementPage({
|
||||
placeholder="请输入邮箱(可选)"
|
||||
/>
|
||||
</label>
|
||||
{/* <label className="form-field">
|
||||
<span>角色</span>
|
||||
<select value={form.role_code} disabled={Boolean(editing) && ((editing as Employee).user_id === user?.user_id || (editing as Employee).role_code === "admin")} 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">
|
||||
<label className={formFieldClass}>
|
||||
<span>状态</span>
|
||||
<select value={form.status} disabled={Boolean(editing) && ((editing as Employee).user_id === user?.user_id || (editing as Employee).role_code === "admin")} onChange={(event) => setForm({ ...form, status: event.target.value as typeof form.status })}>
|
||||
<select
|
||||
className={formInputClass}
|
||||
value={form.status}
|
||||
disabled={Boolean(editing) && (editing.user_id === user?.user_id || editing.role_code === "admin")}
|
||||
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 className={modalFooterClass}>
|
||||
<button className={secondaryButtonClass} type="button" onClick={() => setDialogOpen(false)}>取消</button>
|
||||
<button className={primaryButtonClass} type="submit" disabled={saving}>{saving ? "保存中…" : "保存"}</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
@@ -194,7 +194,6 @@ type Actions = {
|
||||
importMembers: (
|
||||
workspace: Workspace,
|
||||
user_ids: string[],
|
||||
role_code: "admin" | "developer",
|
||||
onNotify: (n: Notice) => void,
|
||||
) => Promise<void>;
|
||||
};
|
||||
@@ -453,14 +452,13 @@ export const useAdminStore = create<State & Actions>((set, get) => ({
|
||||
}
|
||||
},
|
||||
|
||||
importMembers: async (workspace, user_ids, role_code, onNotify) => {
|
||||
importMembers: async (workspace, user_ids, onNotify) => {
|
||||
const api = requireApi();
|
||||
try {
|
||||
await Promise.all(
|
||||
user_ids.map((uid) =>
|
||||
api.addWorkspaceMember(workspace.workspace_id, {
|
||||
user_id: uid,
|
||||
role_code,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user