update:css样式拆分

This commit is contained in:
xiaozhu
2026-08-25 16:52:10 +08:00
parent 76aa2dcb27
commit 12111de788
27 changed files with 1404 additions and 3059 deletions
+205 -48
View File
@@ -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,
}),
),
);