Commit 5a3fb30d authored by David Yang's avatar David Yang

feat: 应用切换

parent 48eaa28c
......@@ -20,6 +20,8 @@ export default defineConfig({
react: resolvePackageRoot('react'),
'react-dom': resolvePackageRoot('react-dom'),
},
base: '/model_analysis/',
publicPath: '/model_analysis/',
routes: [
{
path: ROUTE_PATHS.login,
......
{
"name": "model_analysis",
"version": "1.0.1",
"build": "abc",
"description": "merge master",
"date": "2025-07-14 19:27:20",
"author": "yang"
}
......@@ -100,7 +100,7 @@ export function Combobox({
// Anchor positioning
'[--anchor-gap:--spacing(2)] [--anchor-padding:--spacing(4)] sm:data-[anchor~=start]:[--anchor-offset:-4px]',
// Base styles,
'isolate min-w-[calc(var(--input-width)+8px)] scroll-py-1 rounded-xl p-1 select-none empty:invisible',
'isolate z-[160] min-w-[calc(var(--input-width)+8px)] scroll-py-1 rounded-xl p-1 select-none empty:invisible',
// Invisible border that is only visible in `forced-colors` mode for accessibility purposes
'outline outline-transparent focus:outline-hidden',
// Handle scrolling when menu won't fit in viewport
......
import { UserContext } from '@/wrapper/Auth';
import {
ChartCard,
HeroBanner,
MetricCard,
NoticeBanner,
Panel,
SectionHeader,
Tag,
} from './AnalysisShared';
import {
ClockIcon,
ShareIcon,
} from '@heroicons/react/24/outline';
import {
buildOverviewCharts,
buildMetricCards,
......@@ -19,6 +19,58 @@ import {
} from './analysisService';
import { useContext, useEffect, useMemo, useState } from 'react';
function OverviewStatusItem({ icon: Icon, label, value, hint }) {
return (
<div className="flex min-w-0 items-start gap-3 rounded-lg border border-zinc-200 bg-white px-4 py-3 shadow-sm">
<div className="flex size-9 shrink-0 items-center justify-center rounded-lg bg-rose-50 text-rose-600">
<Icon className="size-[18px]" />
</div>
<div className="min-w-0">
<div className="text-xs font-medium text-zinc-500">{label}</div>
<div className="mt-1 truncate text-lg font-semibold leading-6 text-zinc-950">
{value}
</div>
<div className="mt-0.5 truncate text-xs text-zinc-500">{hint}</div>
</div>
</div>
);
}
function OverviewHeader({ welcomeName, lastUpdatedAt, sharedViewCount }) {
return (
<section className="overflow-hidden rounded-lg border border-zinc-200/80 bg-white shadow-sm">
<div className="grid gap-5 bg-[linear-gradient(135deg,rgba(255,241,242,0.78),rgba(255,255,255,0.96)_42%,rgba(248,250,252,0.98))] p-5 lg:grid-cols-[minmax(0,1fr)_minmax(360px,480px)] lg:items-center lg:p-6">
<div className="min-w-0">
<div className="inline-flex rounded-full border border-rose-200 bg-white/80 px-3 py-1 text-[11px] font-semibold tracking-[0.16em] text-rose-700">
ANALYSIS OVERVIEW
</div>
<h1 className="mt-4 max-w-4xl text-2xl font-semibold leading-tight text-zinc-950 sm:text-3xl">
{welcomeName},这里是新分析工具的统一驾驶舱
</h1>
<p className="mt-3 max-w-4xl text-sm leading-6 text-zinc-600">
聚合视图配置、转发审批和图表看板三条主链路,先判断配置成熟度,再进入具体分析任务。
</p>
</div>
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-1 xl:grid-cols-2">
<OverviewStatusItem
icon={ClockIcon}
label="最近更新时间"
value={lastUpdatedAt}
hint="配置或审批变更"
/>
<OverviewStatusItem
icon={ShareIcon}
label="共享视图数"
value={sharedViewCount}
hint="团队开放配置"
/>
</div>
</div>
</section>
);
}
export default function AnalysisOverview() {
const userInfo = useContext(UserContext);
const [subjects, setSubjects] = useState([]);
......@@ -76,6 +128,20 @@ export default function AnalysisOverview() {
);
const welcomeName =
userInfo?.display_name || userInfo?.displayName || userInfo?.username || '业务同事';
const lastUpdatedAt = useMemo(
() =>
formatDateTime(
[...views, ...forwards, ...boards]
.map((item) => item.updatedAt || item.createdAt)
.sort()
.pop(),
),
[boards, forwards, views],
);
const sharedViewCount = useMemo(
() => views.filter((item) => item.scope === 'shared').length,
[views],
);
const viewChartOption = useMemo(
() => ({
......@@ -159,41 +225,10 @@ export default function AnalysisOverview() {
return (
<div className="space-y-6">
<HeroBanner
eyebrow="ANALYSIS OVERVIEW"
title={`${welcomeName},这里是新分析工具的统一驾驶舱`}
description="总览页聚合对象覆盖、视图配置、转发审批和图表看板四条主链路,方便业务先看配置成熟度,再进入视图或审批页处理具体分析任务。"
aside={
<div className="grid gap-3 sm:grid-cols-2">
<div className="rounded-[24px] border border-white/70 bg-white/80 p-4 shadow-sm">
<div className="text-xs font-semibold tracking-[0.18em] text-zinc-500">
最近更新时间
</div>
<div className="mt-2 text-2xl font-semibold text-zinc-950">
{formatDateTime(
[...views, ...forwards, ...boards]
.map((item) => item.updatedAt || item.createdAt)
.sort()
.pop(),
)}
</div>
<div className="mt-1 text-sm text-zinc-500">
最新配置或审批变更时间
</div>
</div>
<div className="rounded-[24px] border border-white/70 bg-white/80 p-4 shadow-sm">
<div className="text-xs font-semibold tracking-[0.18em] text-zinc-500">
共享视图数
</div>
<div className="mt-2 text-2xl font-semibold text-zinc-950">
{views.filter((item) => item.scope === 'shared').length}
</div>
<div className="mt-1 text-sm text-zinc-500">
已对团队开放的分析配置
</div>
</div>
</div>
}
<OverviewHeader
welcomeName={welcomeName}
lastUpdatedAt={lastUpdatedAt}
sharedViewCount={sharedViewCount}
/>
{loading ? <NoticeBanner>正在加载分析总览...</NoticeBanner> : null}
......@@ -230,83 +265,6 @@ export default function AnalysisOverview() {
emptyText="还没有图表看板"
/>
</div>
<div className="grid gap-6 xl:grid-cols-[1.1fr_0.9fr]">
<Panel className="p-6">
<SectionHeader
title="对象覆盖"
description="后端对象定义是整个分析工具的底座,新增对象后可以直接在视图页配置。"
/>
<div className="grid gap-3">
{subjects.length > 0 ? (
subjects.map((subject) => (
<div
key={subject.name}
className="rounded-[22px] border border-zinc-200 bg-zinc-50/80 px-4 py-4"
>
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div>
<div className="text-base font-semibold text-zinc-950">
{subject.title || subject.name}
</div>
<div className="mt-1 text-sm text-zinc-500">
{subject.description || '当前对象已可用于配置分析视图。'}
</div>
</div>
<Tag tone="sky">{subject.fields.length} 个字段</Tag>
</div>
</div>
))
) : (
<div className="text-sm text-zinc-400">暂无可用对象定义</div>
)}
</div>
</Panel>
<Panel className="p-6">
<SectionHeader
title="最近活动"
description="把最近新增或更新的视图、审批和看板放在一起,方便判断业务关注点。"
/>
<div className="space-y-3">
{[...views, ...forwards, ...boards]
.sort((left, right) =>
String(right.updatedAt || right.createdAt).localeCompare(
String(left.updatedAt || left.createdAt),
),
)
.slice(0, 6)
.map((item) => (
<div
key={`${item.id}_${item.updatedAt || item.createdAt}`}
className="rounded-[22px] border border-zinc-200 bg-white px-4 py-4"
>
<div className="flex items-center justify-between gap-3">
<div className="min-w-0">
<div className="truncate text-sm font-semibold text-zinc-950">
{item.title}
</div>
<div className="mt-1 text-xs text-zinc-500">
更新于 {formatDateTime(item.updatedAt || item.createdAt)}
</div>
</div>
{'approvalStatus' in item ? (
<Tag tone="amber">审批记录</Tag>
) : 'widgets' in item ? (
<Tag tone="emerald">图表看板</Tag>
) : (
<Tag tone="sky">分析视图</Tag>
)}
</div>
</div>
))}
{views.length + forwards.length + boards.length === 0 ? (
<div className="text-sm text-zinc-400">当前还没有配置活动</div>
) : null}
</div>
</Panel>
</div>
</div>
);
}
......@@ -38,6 +38,8 @@ import {
formatDateTime,
formatFieldValue,
getApprovalStatusLabel,
getForwardRowStatusLabel,
getForwardRowStatusTone,
getFieldInputKind,
getViewScopeLabel,
loadAnalysisSubjectDefinition,
......@@ -153,6 +155,10 @@ export function ApprovalStatusTag({ status }) {
return <Tag tone={tone}>{getApprovalStatusLabel(status)}</Tag>;
}
export function WorkflowStatusTag({ status }) {
return <Tag tone={getForwardRowStatusTone(status)}>{getForwardRowStatusLabel(status)}</Tag>;
}
export function EmptyCollection({ title, description }) {
return (
<div className="rounded-[24px] border border-dashed border-zinc-200 bg-zinc-50 px-5 py-10 text-center">
......@@ -258,6 +264,7 @@ export function UserSelectField({
required = false,
placeholder = '搜索用户',
}) {
const [query, setQuery] = useState('');
const [options, setOptions] = useState([]);
const [loading, setLoading] = useState(false);
......@@ -266,17 +273,22 @@ export function UserSelectField({
(async () => {
setLoading(true);
const result = await searchUsers('');
try {
const result = await searchUsers(query);
if (!cancelled) {
setOptions(result);
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
})();
return () => {
cancelled = true;
};
}, []);
}, [query]);
return (
<div>
......@@ -291,6 +303,7 @@ export function UserSelectField({
.toLowerCase()
.includes(String(query || '').toLowerCase())
}
onQueryChange={setQuery}
placeholder={placeholder}
aria-label={label}
>
......@@ -645,6 +658,9 @@ export function RecordsResultTable({
records,
columns,
selectedIds,
rowStatusByRecordId = {},
forwardItemByRecordId = new Map(),
onOpenForwardItem,
onToggleRow,
onToggleAll,
}) {
......@@ -663,6 +679,7 @@ export function RecordsResultTable({
/>
</TableHeader>
<TableHeader>标题</TableHeader>
<TableHeader>当前状态</TableHeader>
{columns.map((column) => (
<TableHeader key={column.alias}>{column.label}</TableHeader>
))}
......@@ -670,7 +687,11 @@ export function RecordsResultTable({
</TableRow>
</TableHead>
<TableBody>
{records.map((record) => (
{records.map((record) => {
const recordKey = record.recordName || record.id;
const forwardItem = forwardItemByRecordId.get(String(recordKey));
return (
<TableRow key={record.id}>
<TableCell>
<Checkbox
......@@ -681,7 +702,17 @@ export function RecordsResultTable({
/>
</TableCell>
<TableCell className="font-medium text-zinc-950">
<button
type="button"
onClick={() => onOpenForwardItem?.(record, forwardItem || null)}
className="max-w-72 truncate text-left font-medium text-sky-700 underline-offset-4 hover:text-sky-900 hover:underline"
aria-label={`查看 ${record.title || record.id} 的回填详情`}
>
{record.title || '-'}
</button>
</TableCell>
<TableCell>
<WorkflowStatusTag status={rowStatusByRecordId[recordKey] || 'pending_send'} />
</TableCell>
{columns.map((column) => (
<TableCell key={column.alias} className="text-zinc-600">
......@@ -692,7 +723,8 @@ export function RecordsResultTable({
{formatDateTime(record.updatedAt)}
</TableCell>
</TableRow>
))}
);
})}
</TableBody>
</Table>
);
......@@ -705,4 +737,3 @@ export function ForwardStatusOptionsHint() {
</div>
);
}
This diff is collapsed.
import ApplicationSwitcherDialog from '@/pages/project_manager_xy/ApplicationSwitcherDialog';
import xyLogo from '@/assets/xy-logo.png';
import xyLogoSmall from '@/assets/xy-logo_small.png';
import { UserContext } from '@/wrapper/Auth';
import { SiteContext } from '@/wrapper/Site';
import {
ArrowsRightLeftIcon,
Bars3Icon,
ChartBarSquareIcon,
ChartPieIcon,
ClipboardDocumentListIcon,
Squares2X2Icon,
XMarkIcon,
} from '@heroicons/react/24/outline';
import clsx from 'clsx';
import { useEffect, useMemo, useState } from 'react';
import { useContext, useEffect, useMemo, useState } from 'react';
import { Link, Outlet, useLocation } from 'umi';
const APP_NAME = '数据分析工具';
......@@ -185,7 +189,10 @@ function MobileSidebar({ open, close, children }) {
export default function ProjectManagerLayout() {
const location = useLocation();
const userInfo = useContext(UserContext);
const siteInfo = useContext(SiteContext);
const [isMobile, setIsMobile] = useState(false);
const [isApplicationDialogOpen, setIsApplicationDialogOpen] = useState(false);
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);
const [isMobileSidebarOpen, setIsMobileSidebarOpen] = useState(false);
......@@ -229,6 +236,23 @@ export default function ProjectManagerLayout() {
);
}, [location.pathname]);
const applicationSwitcherButton = (
<button
type="button"
onClick={() => setIsApplicationDialogOpen(true)}
aria-label="Open application switcher"
title="应用切换"
className={clsx(
'inline-flex items-center justify-center rounded-xl border border-zinc-700 bg-zinc-800 text-zinc-300 shadow-sm transition duration-200 hover:border-[#fb923c]/70 hover:bg-zinc-800/90 hover:text-[#fdba74] focus:outline-none focus-visible:ring-2 focus-visible:ring-[#fb923c]/40',
sidebarCollapsed ? 'size-9' : 'size-10',
)}
>
<Squares2X2Icon
className={clsx(sidebarCollapsed ? 'size-[18px]' : 'size-5')}
/>
</button>
);
return (
<div className="flex h-screen overflow-hidden bg-zinc-50">
<aside
......@@ -239,22 +263,34 @@ export default function ProjectManagerLayout() {
>
<div
className={clsx(
'mb-7 flex items-center',
sidebarCollapsed ? 'justify-center' : 'gap-3 px-1',
'mb-7',
sidebarCollapsed
? 'flex flex-col items-center gap-3'
: 'flex items-start justify-between gap-4 px-1',
)}
>
{sidebarCollapsed ? (
<>
<img
src={xyLogoSmall}
alt="XY Logo"
className="h-9 w-auto max-w-[52px] shrink-0 object-contain"
className="h-8 w-auto max-w-8 shrink-0 object-contain"
/>
) : null}
{!sidebarCollapsed ? (
{applicationSwitcherButton}
</>
) : (
<>
<div className="min-w-0 flex-1">
<h1 className="truncate text-sm font-semibold tracking-[0.02em] text-white/95">
{APP_NAME}
</h1>
) : null}
<div className="mt-1 text-xs text-zinc-400">
配置驱动的数据分析工具
</div>
</div>
{applicationSwitcherButton}
</>
)}
</div>
<nav className="flex-1 space-y-5 overflow-y-auto">
......@@ -280,6 +316,17 @@ export default function ProjectManagerLayout() {
</div>
))}
</nav>
{!sidebarCollapsed ? (
<div className="mt-6 rounded-2xl border border-zinc-800 bg-zinc-950/70 p-4 text-sm text-zinc-300">
<div className="font-medium text-white/90">
{userInfo?.display_name || userInfo?.username || '当前用户'}
</div>
<div className="mt-1 text-xs text-zinc-500">
当前正在查看数据分析工具
</div>
</div>
) : null}
</aside>
<div className="relative z-30 hidden lg:flex">
......@@ -307,6 +354,7 @@ export default function ProjectManagerLayout() {
close={() => setIsMobileSidebarOpen(false)}
>
<div className="px-5 pt-6">
<div>
<div className="flex items-center gap-2">
<img
src={xyLogo}
......@@ -317,6 +365,10 @@ export default function ProjectManagerLayout() {
{APP_NAME}
</h1>
</div>
<div className="mt-1 text-xs text-zinc-500">
配置驱动分析工作区
</div>
</div>
</div>
<nav className="space-y-5 overflow-y-auto px-5 pb-6 pt-6">
......@@ -351,12 +403,26 @@ export default function ProjectManagerLayout() {
<Bars3Icon className="size-5" />
</NavbarItem>
<div className="text-sm text-zinc-500">{currentGroupTitle}</div>
<NavbarItem
onClick={() => setIsApplicationDialogOpen(true)}
aria-label="Open application switcher"
className="border border-rose-200 bg-white text-rose-500 shadow-sm hover:bg-rose-50 hover:text-rose-600"
>
<Squares2X2Icon className="size-5" />
</NavbarItem>
</div>
<div className="flex-1 overflow-y-auto rounded-[32px] border border-white/70 bg-white/75 p-4 shadow-[0_24px_80px_rgba(15,23,42,0.06)] backdrop-blur sm:p-6">
<Outlet />
</div>
</main>
<ApplicationSwitcherDialog
open={isApplicationDialogOpen}
onClose={() => setIsApplicationDialogOpen(false)}
userInfo={userInfo}
siteInfo={siteInfo}
/>
</div>
);
}
......@@ -317,7 +317,7 @@ export async function searchSystemUsers(keyword = '') {
: '',
null,
);
return mergeUserRecords(response?.records || []);
return mergeUserRecords(response?.users || response?.records || []);
}
export async function fetchSystemUsersByIds(ids = []) {
......
......@@ -603,7 +603,7 @@ function parseDocument(formData) {
}
// user
function queryUsers(page, page_size, filter = {}, status = null) {
function queryUsers(page, page_size, filter = {}, status = null, groups = false) {
return request('/user/query', {
method: 'post',
data: {
......@@ -611,6 +611,7 @@ function queryUsers(page, page_size, filter = {}, status = null) {
page_size,
filter,
status,
groups,
},
});
}
......
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment