diff --git a/furniture-3d-system/src/app/asset-center/AssetCenterClient.test.tsx b/furniture-3d-system/src/app/asset-center/AssetCenterClient.test.tsx index c7d42c27..3d5a3b30 100644 --- a/furniture-3d-system/src/app/asset-center/AssetCenterClient.test.tsx +++ b/furniture-3d-system/src/app/asset-center/AssetCenterClient.test.tsx @@ -520,13 +520,14 @@ describe('AssetCenterClient', () => { ) expect(screen.getByTestId('family-status-family-board-available')).toHaveTextContent('可用') - expect(screen.getByTestId('family-status-family-board-available')).toHaveClass('h-7') + expect(screen.getByTestId('family-status-family-board-available')).toHaveClass('h-5') expect(screen.getByTestId('family-status-family-board-available')).not.toHaveClass('h-4') await actor.click(screen.getByText('可用桌面板资产族')) + expect(screen.queryByText(/适用品类系列/)).not.toBeInTheDocument() expect(screen.getByTestId('variant-status-variant-board-rejected')).toHaveTextContent('已驳回') - expect(screen.getByTestId('variant-status-variant-board-rejected')).toHaveClass('h-7') + expect(screen.getByTestId('variant-status-variant-board-rejected')).toHaveClass('h-5') expect(screen.getByRole('button', { name: '提审' })).toBeInTheDocument() }) diff --git a/furniture-3d-system/src/app/asset-center/AssetCenterClient.tsx b/furniture-3d-system/src/app/asset-center/AssetCenterClient.tsx index 39e49474..99f0fe33 100644 --- a/furniture-3d-system/src/app/asset-center/AssetCenterClient.tsx +++ b/furniture-3d-system/src/app/asset-center/AssetCenterClient.tsx @@ -326,21 +326,6 @@ function parseVariantUvEditorFormState( } } -function isFamilyCompatibleWithSeries( - family: AssetFamilyDTO, - seriesCode: string | null | undefined, -): boolean { - if (!seriesCode) { - return true - } - - if (!family.applicableSeriesCodes || family.applicableSeriesCodes.length === 0) { - return true - } - - return family.applicableSeriesCodes.includes(seriesCode) -} - function isEditableElement(target: EventTarget | null) { if (!(target instanceof HTMLElement)) { return false @@ -909,28 +894,34 @@ function canReviewAssets(role: string): boolean { } const compactRowActionClassName = - 'inline-flex h-7 items-center rounded-[4px] border border-black/[0.04] bg-black/[0.03] px-2 text-[11px] text-[#5a6062] transition hover:border-black/10 hover:bg-black/[0.05] hover:text-[#2d3335] disabled:cursor-not-allowed disabled:opacity-50' + 'inline-flex h-6 items-center rounded-[4px] px-1.5 text-[10px] font-apple-bold text-[#687477] transition hover:bg-black/[0.045] hover:text-[#263033] disabled:cursor-not-allowed disabled:opacity-45' const compactRowPrimaryActionClassName = - 'inline-flex h-7 items-center rounded-[4px] border border-[#101719]/15 bg-black/[0.045] px-2 text-[11px] font-bold text-[#101719] transition hover:bg-black/[0.07] disabled:cursor-not-allowed disabled:opacity-50' + 'inline-flex h-6 items-center rounded-[4px] bg-[#edf1f1] px-1.5 text-[10px] font-apple-bold text-[#101719] transition hover:bg-[#e3e9e9] disabled:cursor-not-allowed disabled:opacity-45' const compactRowDangerActionClassName = - 'inline-flex h-7 items-center rounded-[4px] border border-red-500/10 bg-red-500/5 px-2 text-[11px] text-red-600 transition hover:bg-red-500/10 disabled:cursor-not-allowed disabled:opacity-50' + 'inline-flex h-6 items-center rounded-[4px] px-1.5 text-[10px] font-apple-bold text-red-600 transition hover:bg-red-500/10 disabled:cursor-not-allowed disabled:opacity-45' const compactRowNeutralStatusClassName = - 'inline-flex h-7 shrink-0 items-center rounded-[4px] border border-black/[0.04] bg-black/[0.03] px-2 text-[11px] text-[#5a6062]' + 'inline-flex h-5 shrink-0 items-center rounded-[3px] bg-[#eef2f2] px-1.5 text-[10px] font-apple-bold text-[#687477]' const compactRowSuccessStatusClassName = - 'inline-flex h-7 shrink-0 items-center rounded-[4px] border border-[#101719]/10 bg-black/[0.035] px-2 text-[11px] font-bold text-[#3f4a4d]' + 'inline-flex h-5 shrink-0 items-center rounded-[3px] bg-[#e8eeee] px-1.5 text-[10px] font-apple-bold text-[#3f4a4d]' const compactRowWarningStatusClassName = - 'inline-flex h-7 shrink-0 items-center rounded-[4px] border border-[#101719]/10 bg-black/[0.035] px-2 text-[11px] text-[#5a6062]' + 'inline-flex h-5 shrink-0 items-center rounded-[3px] bg-[#f0eeee] px-1.5 text-[10px] font-apple-bold text-[#6a6260]' const compactRowDangerStatusClassName = - 'inline-flex h-7 shrink-0 items-center rounded-[4px] border border-red-500/10 bg-red-500/5 px-2 text-[11px] text-red-600' + 'inline-flex h-5 shrink-0 items-center rounded-[3px] bg-red-500/[0.08] px-1.5 text-[10px] font-apple-bold text-red-600' const compactRowIconActionClassName = - 'inline-flex h-7 w-7 items-center justify-center rounded-[4px] border border-black/[0.04] bg-black/[0.03] text-[#5a6062] transition hover:border-black/10 hover:bg-black/[0.05] hover:text-[#2d3335]' + 'inline-flex h-6 w-6 items-center justify-center rounded-[4px] text-[#7a878a] transition hover:bg-black/[0.045] hover:text-[#263033]' + +const variantInlineCreateButtonClassName = + 'flex h-9 min-w-0 items-center justify-center gap-2 rounded-[5px] border border-black/[0.08] bg-white px-3 text-[11px] font-black text-[#263033] shadow-[0_1px_2px_rgba(16,23,25,0.04)] transition hover:border-black/[0.14] hover:bg-[#f8faf9] hover:text-[#101719] focus-visible:ring-1 focus-visible:ring-[#2d3335]/15' + +const variantInlineImportButtonClassName = + 'flex h-9 min-w-0 items-center justify-center gap-2 rounded-[5px] border border-black/[0.08] bg-white px-3 text-[11px] font-black text-[#263033] shadow-[0_1px_2px_rgba(16,23,25,0.04)] transition hover:border-black/[0.14] hover:bg-[#f8faf9] hover:text-[#101719] focus-visible:ring-1 focus-visible:ring-[#2d3335]/15' type ReviewDialogState = { targetType: 'family' | 'variant' @@ -1078,19 +1069,6 @@ export default function AssetCenterClient({ () => families.find((family) => family.id === editingFamilyId) ?? null, [editingFamilyId, families], ) - const seriesTemplates = initialSeriesTemplates - const seriesTemplateByCode = useMemo( - () => new Map(seriesTemplates.map((template) => [template.code, template])), - [seriesTemplates], - ) - const seriesTemplateOptions = useMemo( - () => - seriesTemplates.map((template) => ({ - value: template.code, - label: `${template.name} · ${template.seriesName}`, - })), - [seriesTemplates], - ) const selectedFinish = useMemo( () => { if (!variantSubstrateDirty && selectedVariant?.finish) { @@ -2917,7 +2895,7 @@ export default function AssetCenterClient({ return ( <> -
+
-
@@ -4186,20 +4130,6 @@ export default function AssetCenterClient({
- {familyForm.type !== 'PACKAGING' && ( - setSearchQuery(event.target.value)} - className="h-10 w-full rounded-[6px] border border-[#d6dee0] bg-white pl-10 pr-4 text-sm font-semibold text-[#152024] outline-none transition placeholder:text-[#7a878a] focus:border-[#152024] focus:bg-white" + className="h-10 w-full rounded-[6px] border border-[var(--obs-shell-border)] bg-[var(--obs-shell-panel-bg)] pl-10 pr-4 text-sm font-semibold text-[var(--obs-shell-title)] outline-none transition placeholder:text-[var(--obs-shell-subtle)] focus:border-[var(--obs-shell-title)] focus:bg-[var(--obs-shell-panel-bg)]" />
@@ -484,11 +485,11 @@ function UsersPageContent() { function InspectorItem({ icon: Icon, label, value }: { icon: LucideIcon; label: string; value: string }) { return ( -
- +
+
-

{label}

-

{value}

+

{label}

+

{value}

) diff --git a/furniture-3d-system/src/components/platform/ObsidianControlShell.tsx b/furniture-3d-system/src/components/platform/ObsidianControlShell.tsx index 87e8558f..1147af30 100644 --- a/furniture-3d-system/src/components/platform/ObsidianControlShell.tsx +++ b/furniture-3d-system/src/components/platform/ObsidianControlShell.tsx @@ -1,10 +1,15 @@ 'use client' import Link from 'next/link' +import { createContext, useContext } from 'react' import type { ComponentType, ReactNode } from 'react' import { ArrowLeft } from 'lucide-react' import { cn } from '@/lib/utils' +type ObsidianShellTone = 'dark' | 'light' + +const ObsidianShellToneContext = createContext('dark') + interface ObsidianControlShellProps { eyebrow: string title: string @@ -20,6 +25,7 @@ interface ObsidianControlShellProps { className?: string contentClassName?: string inspectorClassName?: string + tone?: ObsidianShellTone } export function ObsidianControlShell({ @@ -35,70 +41,120 @@ export function ObsidianControlShell({ className, contentClassName, inspectorClassName, + tone = 'dark', }: ObsidianControlShellProps) { + const isLight = tone === 'light' + return ( -
+
- {rail ? ( - - ) : null} - -
-
-
- {Icon ? ( -
- +
+ {rail ? ( +
-
{children}
-
+
{rail}
+ + ) : null} - {inspector ? ( - - ) : null} +
+
+
+ {Icon ? ( +
+ +
+ ) : null} +
+

{eyebrow}

+

{title}

+
+
+ {actions ?
{actions}
: null} +
+
{children}
+
+ + {inspector ? ( + + ) : null} +
-
+ ) } export function ObsidianRailGroup({ title, children }: { title: string; children: ReactNode }) { + const tone = useContext(ObsidianShellToneContext) + return (
-
{title}
+
{title}
{children}
) @@ -119,11 +175,16 @@ export function ObsidianRailButton({ href?: string onClick?: () => void }) { + const tone = useContext(ObsidianShellToneContext) const className = cn( 'flex w-full items-center gap-3 rounded-md px-3 py-2.5 text-left transition', - active - ? 'bg-[#e7eeec] text-[#101517]' - : 'text-[#c0ccca] hover:bg-white/[0.06] hover:text-white', + tone === 'light' + ? active + ? 'bg-[var(--obs-shell-rail-active-bg)] text-[var(--obs-shell-rail-active-text)]' + : 'text-[var(--obs-shell-rail-text)] hover:bg-[var(--obs-shell-rail-hover-bg)] hover:text-[var(--obs-shell-rail-hover-text)]' + : active + ? 'bg-[#e7eeec] text-[#101517]' + : 'text-[#c0ccca] hover:bg-white/[0.06] hover:text-white', ) const content = ( <> @@ -159,14 +220,22 @@ export function ObsidianPanel({ children: ReactNode className?: string }) { + const tone = useContext(ObsidianShellToneContext) + return ( -
+
{title || eyebrow ? ( -
+
{eyebrow ? ( -

{eyebrow}

+

{eyebrow}

) : null} - {title ?

{title}

: null} + {title ?

{title}

: null}
) : null} {children} @@ -181,12 +250,14 @@ export function ObsidianInspectorSection({ title: string children: ReactNode }) { + const tone = useContext(ObsidianShellToneContext) + return ( -
-

+
+

{title}

-
{children}
+
{children}
) } @@ -200,15 +271,33 @@ export function ObsidianDataRow({ value: ReactNode tone?: 'default' | 'success' | 'warning' }) { + const toneContext = useContext(ObsidianShellToneContext) + const valueToneClassName = toneContext === 'light' + ? cn( + tone === 'success' && 'text-[var(--obs-shell-success)]', + tone === 'warning' && 'text-[#6a6260]', + tone === 'default' && 'text-[var(--obs-shell-title)]', + ) + : cn( + tone === 'success' && 'text-[#3f4a4d]', + tone === 'warning' && 'text-[#5a6062]', + tone === 'default' && 'text-[#edf4f2]', + ) + return ( -
- {label} +
+ {label} {value} diff --git a/furniture-3d-system/src/components/users/DepartmentAccessTree.tsx b/furniture-3d-system/src/components/users/DepartmentAccessTree.tsx index c9a9a381..51e11619 100644 --- a/furniture-3d-system/src/components/users/DepartmentAccessTree.tsx +++ b/furniture-3d-system/src/components/users/DepartmentAccessTree.tsx @@ -133,9 +133,9 @@ interface SectionHeaderProps { function SummaryBadge({ label, value }: { label: string; value: string }) { return ( - + {label} - {value} + {value} ); } @@ -145,17 +145,17 @@ function SectionHeader({ icon, title, count, isOpen, onToggle }: SectionHeaderPr @@ -164,7 +164,7 @@ function SectionHeader({ icon, title, count, isOpen, onToggle }: SectionHeaderPr function UserLedgerHeader() { return ( -
+
账号 通讯 组织 @@ -202,43 +202,43 @@ function TeamNodeView({ const [isOpen, setIsOpen] = useState(false); return ( -
+
{isOpen && ( -
+
{team.users.length > 0 ? (
-
+
{team.users.map((user) => (
) : ( -
+
该小组当前没有匹配的成员
@@ -306,35 +306,35 @@ function DepartmentNodeView({ return (
0 && 'ml-4 mt-2' + 'bg-[var(--obs-shell-panel-bg)]', + depth === 0 ? 'border-b border-[var(--obs-shell-border)]' : 'ml-4 mt-2 border-l border-[var(--obs-shell-border)] pl-3' )} > {isOpen && ( -
+
{hasDirectUsers && (
setDirectUsersOpen((current) => !current)} /> {directUsersOpen && ( -
+
-
+
{department.users.map((user) => ( -
+
+
子部门
@@ -452,7 +452,7 @@ function DepartmentNodeView({ )} {!hasDirectUsers && !hasTeams && !hasChildren && ( -
+
该部门当前没有匹配的组织成员
@@ -550,8 +550,8 @@ export function DepartmentAccessTree({ if (loading) { return ( -
- +
+
); } @@ -575,9 +575,9 @@ export function DepartmentAccessTree({ if (!hasDepartments && unassignedUsers.length === 0) { return ( -
- -

暂无匹配的组织数据

+
+ +

暂无匹配的组织数据

); } @@ -603,28 +603,28 @@ export function DepartmentAccessTree({ ))} {unassignedUsers.length > 0 && ( -
+
{unassignedOpen && ( -
+
-
+
{unassignedUsers.map((user) => ( = { SUPER_ADMIN: { label: '超级管理员', - color: 'border-[#e3d2ad] bg-[#fbf5e7] text-[#7d5a18]', - avatar: 'border-[#e3d2ad] bg-[#fbf5e7] text-[#7d5a18]', + color: 'border-[var(--obs-shell-border)] bg-[var(--obs-shell-control-bg)] text-[var(--obs-shell-title)]', + avatar: 'border-[var(--obs-shell-border)] bg-[var(--obs-shell-control-bg)] text-[var(--obs-shell-title)]', }, ADMIN: { label: '管理员', - color: 'border-[#d7c7b0] bg-[#fff4e7] text-[#985f22]', - avatar: 'border-[#d7c7b0] bg-[#fff4e7] text-[#985f22]', + color: 'border-[var(--obs-shell-border)] bg-[var(--obs-shell-control-bg)] text-[var(--obs-shell-title)]', + avatar: 'border-[var(--obs-shell-border)] bg-[var(--obs-shell-control-bg)] text-[var(--obs-shell-title)]', }, SUPERVISOR: { label: '主管', - color: 'border-[#cfdbde] bg-[#f2f6f6] text-[#405256]', - avatar: 'border-[#cfdbde] bg-[#f2f6f6] text-[#405256]', + color: 'border-[var(--obs-shell-border)] bg-[var(--obs-shell-muted-bg)] text-[var(--obs-shell-control-text)]', + avatar: 'border-[var(--obs-shell-border)] bg-[var(--obs-shell-muted-bg)] text-[var(--obs-shell-control-text)]', }, SUPERVISOR_DEPARTMENT: { label: '部门主管', - color: 'border-[#cfdbde] bg-[#f2f6f6] text-[#405256]', - avatar: 'border-[#cfdbde] bg-[#f2f6f6] text-[#405256]', + color: 'border-[var(--obs-shell-border)] bg-[var(--obs-shell-muted-bg)] text-[var(--obs-shell-control-text)]', + avatar: 'border-[var(--obs-shell-border)] bg-[var(--obs-shell-muted-bg)] text-[var(--obs-shell-control-text)]', }, SUPERVISOR_TEAM: { label: '小组主管', - color: 'border-[#cfdbde] bg-[#f2f6f6] text-[#405256]', - avatar: 'border-[#cfdbde] bg-[#f2f6f6] text-[#405256]', + color: 'border-[var(--obs-shell-border)] bg-[var(--obs-shell-muted-bg)] text-[var(--obs-shell-control-text)]', + avatar: 'border-[var(--obs-shell-border)] bg-[var(--obs-shell-muted-bg)] text-[var(--obs-shell-control-text)]', }, DESIGNER: { label: '设计师', - color: 'border-[#cbdbea] bg-[#eef5fb] text-[#2f5f86]', - avatar: 'border-[#cbdbea] bg-[#eef5fb] text-[#2f5f86]', + color: 'border-[var(--obs-shell-border)] bg-[var(--obs-shell-muted-bg)] text-[var(--obs-shell-control-text)]', + avatar: 'border-[var(--obs-shell-border)] bg-[var(--obs-shell-muted-bg)] text-[var(--obs-shell-control-text)]', }, USER: { label: '普通用户', - color: 'border-[#d8dee0] bg-[#f5f7f7] text-[#4f5b5e]', - avatar: 'border-[#d8dee0] bg-[#f5f7f7] text-[#4f5b5e]', + color: 'border-[var(--obs-shell-border)] bg-[var(--obs-shell-muted-bg)] text-[var(--obs-shell-muted)]', + avatar: 'border-[var(--obs-shell-border)] bg-[var(--obs-shell-muted-bg)] text-[var(--obs-shell-muted)]', }, }; @@ -182,15 +182,15 @@ function RowActionButton({ onClick={onClick} className={cn( 'inline-flex h-8 shrink-0 items-center justify-center gap-1.5 rounded-[5px] border px-2.5 text-[12px] font-bold transition-colors', - 'focus:outline-none focus-visible:ring-2 focus-visible:ring-[#b9c8c7] focus-visible:ring-offset-1', + 'focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--obs-shell-border)] focus-visible:ring-offset-1', tone === 'neutral' && - 'border-[#d6dee0] bg-[#f7f9f9] text-[#324044] hover:border-[#b8c4c7] hover:bg-[#eef3f3]', + 'border-[var(--obs-shell-border)] bg-[var(--obs-shell-surface-bg)] text-[var(--obs-shell-control-text)] hover:border-[var(--obs-shell-border)] hover:bg-[var(--obs-shell-hover-bg)]', tone === 'warning' && - 'border-[#ead9aa] bg-[#fff9eb] text-[#8a641b] hover:border-[#d8bd72] hover:bg-[#fff3d3]', + 'border-[var(--obs-shell-warning-border)] bg-[var(--obs-shell-warning-bg)] text-[var(--obs-shell-warning)] hover:bg-[var(--obs-shell-control-hover-bg)]', tone === 'danger' && - 'border-transparent bg-transparent px-2 text-[#9d5750] hover:border-[#efc8c4] hover:bg-[#fff1ef]', + 'border-transparent bg-transparent px-2 text-[var(--obs-shell-danger)] hover:border-[var(--obs-shell-danger-border)] hover:bg-[var(--obs-shell-danger-bg)]', tone === 'success' && - 'border-[#bfe4d4] bg-[#eff9f4] text-[#1f7a56] hover:border-[#95d5b9] hover:bg-[#e5f5ee]' + 'border-[var(--obs-shell-success-border)] bg-[var(--obs-shell-success-bg)] text-[var(--obs-shell-success)] hover:bg-[var(--obs-shell-control-hover-bg)]' )} > @@ -295,21 +295,16 @@ export const UserListItem = memo(function UserListItem({ data-selected={selected ? 'true' : 'false'} onClick={handleSelect} onKeyDown={handleRowKeyDown} - style={ - selected - ? { boxShadow: 'inset 3px 0 0 #152024' } - : undefined - } className={cn( - 'group relative grid cursor-pointer grid-cols-1 gap-2 bg-white px-3 py-2.5 text-left transition-colors duration-150', - 'hover:bg-[#f8faf9] focus:outline-none focus-visible:ring-2 focus-visible:ring-[#b9c8c7] focus-visible:ring-inset', + 'group relative grid cursor-pointer grid-cols-1 gap-2 border-l-[3px] bg-[var(--obs-shell-panel-bg)] px-3 py-2.5 text-left transition-colors duration-150', + 'hover:bg-[var(--obs-shell-hover-bg)] focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--obs-shell-border)] focus-visible:ring-inset', 'xl:grid-cols-[minmax(220px,1.25fr)_minmax(170px,0.95fr)_minmax(140px,0.8fr)_minmax(112px,0.65fr)_auto] xl:items-center', - selected && 'bg-[#eef5f4]', - isPending && !selected && 'bg-[#fffaf0]' + selected ? 'border-l-[var(--obs-shell-title)] bg-[var(--obs-shell-control-bg)]' : 'border-l-transparent', + isPending && !selected && 'bg-[var(--obs-shell-warning-bg)]' )} > {isPending && ( -
+
)}
@@ -333,34 +328,34 @@ export const UserListItem = memo(function UserListItem({ )}
{isCurrentUser && ( -
+
)}
-

+

{user.name}

{roleStyle.label} {statusStyle.label} {selected && ( - 当前选择 + 当前选择 )} {isCurrentUser && ( - 当前账号 + 当前账号 )} {user.employeeDirectoryId && ( - 已匹配员工库 + 已匹配员工库 )} {!user.employeeDirectoryId && ( - 未匹配员工库 + 未匹配员工库 )} {user.loginSource === 'DINGTALK' && ( - 钉钉已绑定 + 钉钉已绑定 )} {(user.businessRoles || []).map((role) => ( - + {BUSINESS_ACCOUNT_TYPE_LABELS[role as keyof typeof BUSINESS_ACCOUNT_TYPE_LABELS] ?? role} ))} @@ -368,32 +363,32 @@ export const UserListItem = memo(function UserListItem({
-
+
- + {user.email}
{user.phone ? (
- + {user.phone}
) : null}
-
+
- + {user.department?.name || '未归属部门'}
- + {user.team?.name || '未归属小组'}
-
- +
+ {formattedLastLogin}