fix: 优化资产中心与管理页白曜石视觉
This commit is contained in:
parent
f1f5c2ff80
commit
3cd41b4921
@ -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()
|
||||
})
|
||||
|
||||
|
||||
@ -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 (
|
||||
<>
|
||||
<div className="asset-center-obsidian flex h-full min-h-0 overflow-hidden bg-[#f8f9fa] text-[#2d3335]">
|
||||
<div className="asset-center-obsidian fixed inset-x-0 bottom-0 top-16 flex min-h-0 overflow-hidden bg-[#f8f9fa] text-[#2d3335]">
|
||||
<section className="relative min-w-0 flex-1 bg-[#f1f4f5]">
|
||||
<ModelPreviewPanel
|
||||
modelUrl={selectedVariant?.modelUrl || null}
|
||||
@ -3006,24 +2984,23 @@ export default function AssetCenterClient({
|
||||
/>
|
||||
</section>
|
||||
|
||||
<aside className="flex h-full w-[480px] shrink-0 flex-col bg-white shadow-[-10px_0_40px_rgba(0,0,0,0.03)]">
|
||||
<div className="min-h-0 flex-1 overflow-y-auto border-b border-black/[0.03]">
|
||||
<div className="space-y-4 border-b border-black/[0.03] p-5">
|
||||
<div className="flex items-center justify-between">
|
||||
<Link
|
||||
href="/asset-center/material-library"
|
||||
aria-label="去基础资料库补录"
|
||||
className="flex items-center gap-2 text-[10px] font-black uppercase tracking-[0.12em] text-[#2d3335] transition-opacity hover:opacity-70"
|
||||
>
|
||||
<SwatchBook className="h-3.5 w-3.5" />
|
||||
<span>进入</span>
|
||||
<span>基础资料库</span>
|
||||
<ChevronRight className="h-3 w-3" />
|
||||
</Link>
|
||||
<span className="rounded-[4px] bg-black/[0.03] px-2 py-1 font-mono text-[9px] font-bold text-[#5a6062]/45">
|
||||
V2.6.4
|
||||
</span>
|
||||
</div>
|
||||
<aside className="flex h-full min-h-0 w-[480px] shrink-0 flex-col overflow-hidden bg-white shadow-[-10px_0_40px_rgba(0,0,0,0.03)]">
|
||||
<div className="shrink-0 space-y-4 border-b border-black/[0.03] bg-white p-5">
|
||||
<div className="flex items-center justify-between">
|
||||
<Link
|
||||
href="/asset-center/material-library"
|
||||
aria-label="去基础资料库补录"
|
||||
className="flex items-center gap-2 text-[10px] font-black uppercase tracking-[0.12em] text-[#2d3335] transition-opacity hover:opacity-70"
|
||||
>
|
||||
<SwatchBook className="h-3.5 w-3.5" />
|
||||
<span>进入</span>
|
||||
<span>基础资料库</span>
|
||||
<ChevronRight className="h-3 w-3" />
|
||||
</Link>
|
||||
<span className="rounded-[4px] bg-black/[0.03] px-2 py-1 font-mono text-[9px] font-bold text-[#5a6062]/45">
|
||||
V2.6.4
|
||||
</span>
|
||||
</div>
|
||||
{/* 分类 Tab */}
|
||||
<div className="flex rounded-[6px] bg-black/[0.03] p-0.5 ring-1 ring-black/5">
|
||||
{TYPE_TABS.map((tab) => {
|
||||
@ -3133,10 +3110,10 @@ export default function AssetCenterClient({
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 资产族列表 */}
|
||||
<div className="flex flex-col bg-[#fdfdfd]">
|
||||
<div className="min-h-0 flex-1 overflow-y-auto overscroll-contain bg-[#fdfdfd]">
|
||||
{activeTab === 'PACKAGING' && (
|
||||
<Link
|
||||
href="/asset-center/packaging-materials"
|
||||
@ -3185,7 +3162,7 @@ export default function AssetCenterClient({
|
||||
const isSelectedForAssembly = assemblySelectedFamilyIds.has(family.id)
|
||||
|
||||
return (
|
||||
<div key={family.id} className="flex flex-col overflow-hidden border-b border-black/[0.03] bg-white last:border-b-0">
|
||||
<div key={family.id} className="flex flex-col overflow-hidden border-b border-[#e7eded] bg-white last:border-b-0">
|
||||
{/* 资产族头部 */}
|
||||
<div
|
||||
role="button"
|
||||
@ -3198,23 +3175,23 @@ export default function AssetCenterClient({
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
'group/family flex cursor-pointer items-center gap-3 px-5 py-3 outline-none transition-colors focus-visible:ring-1 focus-visible:ring-[#2d3335]/15',
|
||||
isExpanded ? 'bg-black/[0.02]' : 'hover:bg-black/[0.01]',
|
||||
'group/family flex cursor-pointer items-center gap-3 px-5 py-3.5 outline-none transition-colors focus-visible:ring-1 focus-visible:ring-[#2d3335]/15',
|
||||
isExpanded ? 'bg-[#f8faf9]' : 'hover:bg-[#fafbfb]',
|
||||
)}
|
||||
>
|
||||
<Layers3 className={cn('h-3.5 w-3.5 shrink-0', isExpanded ? 'text-[#2d3335]' : 'text-[#5a6062]/30')} />
|
||||
<Layers3 className={cn('h-4 w-4 shrink-0', isExpanded ? 'text-[#263033]' : 'text-[#8b989b]')} />
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="min-w-0">
|
||||
<p className="min-w-0 truncate text-xs font-bold text-[#2d3335]">
|
||||
<p className="min-w-0 truncate text-[14px] font-black text-[#101719]">
|
||||
{family.name}
|
||||
</p>
|
||||
</div>
|
||||
<p className="truncate font-mono text-[8px] uppercase text-[#5a6062]/35">
|
||||
<p className="mt-0.5 truncate font-mono text-[10px] uppercase text-[#7a878a]">
|
||||
{family.code}
|
||||
{totalFamilyVariants.length > 0 ? ` · ${totalFamilyVariants.length} 个规格款` : ''}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-1">
|
||||
<div className="flex shrink-0 items-center gap-0.5">
|
||||
{renderCompactStatus(family.status, `family-status-${family.id}`)}
|
||||
{(family.status === 'DRAFT' || family.status === 'REJECTED') ? (
|
||||
<button
|
||||
@ -3290,57 +3267,24 @@ export default function AssetCenterClient({
|
||||
<Pencil className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
{isExpanded ? (
|
||||
<ChevronDown className="h-4 w-4 shrink-0 text-[#2d3335] transition-transform duration-200" />
|
||||
<ChevronDown className="h-4 w-4 shrink-0 text-[#263033] transition-transform duration-200" />
|
||||
) : (
|
||||
<ChevronRight className="h-4 w-4 shrink-0 text-[#5a6062]/25 transition-transform duration-200" />
|
||||
<ChevronRight className="h-4 w-4 shrink-0 text-[#9aa5a7] transition-transform duration-200" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isExpanded && (family.status === 'REJECTED' && family.workflow.rejectionReason) ? (
|
||||
<div className="border-t border-black/[0.04] px-4 py-2 text-[11px] text-red-600">
|
||||
<div className="bg-[#f8faf9] px-5 pb-2 text-[11px] text-red-600">
|
||||
驳回原因:{family.workflow.rejectionReason}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{isExpanded && family.applicableSeriesCodes.length > 0 ? (
|
||||
<div className="border-t border-black/[0.04] px-4 py-2 text-[11px] text-[#5a6062]/60">
|
||||
适用品类系列:
|
||||
{family.applicableSeriesCodes
|
||||
.map((code) => seriesTemplateByCode.get(code)?.name ?? code)
|
||||
.join('、')}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* 规格款列表(展开时) */}
|
||||
{isExpanded && (
|
||||
<div className="border-t border-black/[0.04] bg-black/[0.018]">
|
||||
<div className="bg-[#f8faf9]">
|
||||
{familyVariants.length > 0 ? (
|
||||
<div className="py-1">
|
||||
<div className="px-5 py-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openCreateVariantModal(family)}
|
||||
className="flex h-8 min-w-0 flex-1 items-center justify-center gap-2 rounded-[4px] border border-dashed border-black/10 text-[10px] font-bold text-[#5a6062] transition hover:border-[#2d3335] hover:text-[#2d3335]"
|
||||
>
|
||||
<Plus className="h-3 w-3" />
|
||||
<span>新增规格款</span>
|
||||
</button>
|
||||
{isImportableAssetType(family.type) ? (
|
||||
<button
|
||||
type="button"
|
||||
aria-label="CAD 导入到该资产族"
|
||||
title="CAD 导入到该资产族"
|
||||
onClick={() => openCadImportModal(family)}
|
||||
className="flex h-8 shrink-0 items-center gap-1.5 rounded-[4px] bg-black/[0.025] px-2.5 text-[10px] font-bold text-[#5a6062] ring-1 ring-black/[0.04] transition hover:bg-black/[0.05] hover:text-[#2d3335]"
|
||||
>
|
||||
<FileUp className="h-3 w-3" />
|
||||
<span>CAD</span>
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className="pt-1">
|
||||
{familyVariants.map((variant) => {
|
||||
const isSelected = selectedVariantId === variant.id
|
||||
return (
|
||||
@ -3356,22 +3300,22 @@ export default function AssetCenterClient({
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
'flex cursor-pointer items-center gap-2 border-l-2 px-4 py-2 outline-none transition focus-visible:ring-1 focus-visible:ring-[#2d3335]/15',
|
||||
'flex cursor-pointer items-center gap-2 border-l-[3px] px-5 py-2.5 outline-none transition focus-visible:ring-1 focus-visible:ring-[#2d3335]/15',
|
||||
isSelected
|
||||
? 'border-l-[#101719] bg-white shadow-[inset_0_0_0_1px_rgba(17,23,25,0.08)]'
|
||||
: 'border-l-transparent hover:bg-black/[0.035]',
|
||||
? 'border-l-[#101719] bg-white'
|
||||
: 'border-l-transparent hover:bg-white/70',
|
||||
)}
|
||||
>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="min-w-0">
|
||||
<p className={cn(
|
||||
'min-w-0 truncate text-sm',
|
||||
isSelected ? 'font-apple-medium text-[#101719]' : 'text-[#2d3335]',
|
||||
'min-w-0 truncate text-[13px] leading-5',
|
||||
isSelected ? 'font-apple-semibold text-[#101719]' : 'font-apple-medium text-[#3f4a4d]',
|
||||
)}>
|
||||
{variant.name}
|
||||
</p>
|
||||
</div>
|
||||
<p className={cn('truncate text-[11px]', isSelected ? 'text-[#5a6062]/80' : 'text-[#5a6062]/70')}>
|
||||
<p className={cn('truncate text-[10px]', isSelected ? 'text-[#5a6062]' : 'text-[#7a878a]')}>
|
||||
{variant.code}
|
||||
{variant.sizeLabel ? ` · ${variant.sizeLabel}` : ''}
|
||||
{variant.status === 'REJECTED' && variant.workflow.rejectionReason
|
||||
@ -3379,7 +3323,7 @@ export default function AssetCenterClient({
|
||||
: ''}
|
||||
</p>
|
||||
</div>
|
||||
<div className="shrink-0 flex items-center gap-1">
|
||||
<div className="shrink-0 flex items-center gap-0.5">
|
||||
{renderCompactStatus(variant.status, `variant-status-${variant.id}`)}
|
||||
{(variant.status === 'DRAFT' || variant.status === 'REJECTED') ? (
|
||||
<button
|
||||
@ -3437,32 +3381,34 @@ export default function AssetCenterClient({
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="px-5 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openCreateVariantModal(family)}
|
||||
className="flex h-8 min-w-0 flex-1 items-center justify-center gap-2 rounded-[4px] border border-dashed border-black/10 text-[10px] font-bold text-[#5a6062] transition hover:border-[#2d3335] hover:text-[#2d3335]"
|
||||
>
|
||||
<Plus className="h-3 w-3" />
|
||||
<span>新增规格款</span>
|
||||
</button>
|
||||
{isImportableAssetType(family.type) ? (
|
||||
<button
|
||||
type="button"
|
||||
aria-label="CAD 导入到该资产族"
|
||||
title="CAD 导入到该资产族"
|
||||
onClick={() => openCadImportModal(family)}
|
||||
className="flex h-8 shrink-0 items-center gap-1.5 rounded-[4px] bg-black/[0.025] px-2.5 text-[10px] font-bold text-[#5a6062] ring-1 ring-black/[0.04] transition hover:bg-black/[0.05] hover:text-[#2d3335]"
|
||||
>
|
||||
<FileUp className="h-3 w-3" />
|
||||
<span>CAD</span>
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
) : null}
|
||||
<div
|
||||
className={cn(
|
||||
'grid gap-2 px-5 pb-3 pt-2',
|
||||
isImportableAssetType(family.type) ? 'grid-cols-2' : 'grid-cols-1',
|
||||
)}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openCreateVariantModal(family)}
|
||||
className={variantInlineCreateButtonClassName}
|
||||
>
|
||||
<Plus className="h-3.5 w-3.5" />
|
||||
<span>新增规格款</span>
|
||||
</button>
|
||||
{isImportableAssetType(family.type) ? (
|
||||
<button
|
||||
type="button"
|
||||
aria-label="CAD 导入到该资产族"
|
||||
title="CAD 导入到该资产族"
|
||||
onClick={() => openCadImportModal(family)}
|
||||
className={variantInlineImportButtonClassName}
|
||||
>
|
||||
<FileUp className="h-3.5 w-3.5" />
|
||||
<span>CAD 导入</span>
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@ -3471,8 +3417,6 @@ export default function AssetCenterClient({
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
@ -4186,20 +4130,6 @@ export default function AssetCenterClient({
|
||||
<ChevronDown className="h-4 w-4 text-apple-label-tertiary transition group-open:rotate-180" />
|
||||
</summary>
|
||||
<div className="space-y-4 border-t border-apple-separator/60 px-4 py-4">
|
||||
{familyForm.type !== 'PACKAGING' && (
|
||||
<Select
|
||||
label="品类系列"
|
||||
value={familyForm.applicableSeriesCode}
|
||||
onChange={(e) =>
|
||||
setFamilyForm((current) => ({
|
||||
...current,
|
||||
applicableSeriesCode: e.target.value,
|
||||
}))
|
||||
}
|
||||
options={seriesTemplateOptions}
|
||||
placeholder="请选择品类系列"
|
||||
/>
|
||||
)}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<p className="text-xs font-apple-medium text-apple-label-secondary">
|
||||
@ -4230,7 +4160,7 @@ export default function AssetCenterClient({
|
||||
label={definition.label}
|
||||
/>
|
||||
<span className="ml-auto text-[11px] text-apple-label-tertiary">
|
||||
{definition.builtIn ? '系列推荐' : '自定义'}
|
||||
{definition.builtIn ? '内置' : '自定义'}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@ -235,6 +235,7 @@ describe('AssetCenterClient workflow', () => {
|
||||
|
||||
await actor.click(within(actionBar.parentElement ?? actionBar).getByRole('button', { name: '新建资产族' }))
|
||||
expect(screen.getByRole('heading', { name: '新建资产族' })).toBeInTheDocument()
|
||||
expect(screen.queryByLabelText('品类系列')).not.toBeInTheDocument()
|
||||
|
||||
await actor.type(screen.getByPlaceholderText('例如 BOARD-001'), 'BOARD-001')
|
||||
await actor.type(screen.getByPlaceholderText('例如 单板桌桌面板'), '桌面板')
|
||||
|
||||
@ -215,8 +215,8 @@ const mlInputClassName =
|
||||
'h-10 w-full rounded-lg border border-black/10 bg-white px-3 py-2 text-sm font-apple-bold text-[#263033] outline-none transition placeholder:text-[#7a878a]/70 focus:border-[#101719]/30 focus:ring-4 focus:ring-black/[0.04]'
|
||||
const mlSelectClassName =
|
||||
'h-10 w-full rounded-lg border border-black/10 bg-white text-[#263033] focus:border-[#101719]/30 focus:ring-4 focus:ring-black/[0.04]'
|
||||
const mlCardClassName = 'rounded-lg border border-black/10 bg-white p-4'
|
||||
const mlCompactSurfaceClassName = 'rounded-lg border border-black/10 bg-[#f7f9f9] p-2.5'
|
||||
const mlCardClassName = 'bg-white p-4'
|
||||
const mlCompactSurfaceClassName = 'rounded-md bg-[#f7f9f9] p-2.5'
|
||||
const mlUploadButtonClassName =
|
||||
'cursor-pointer rounded-md border border-[#101719]/20 bg-white px-2.5 py-1.5 text-[10px] font-apple-black text-[#101719] ring-1 ring-black/[0.04] transition hover:bg-black/[0.045]'
|
||||
const mlTagClassName =
|
||||
@ -260,7 +260,7 @@ function FinishWorkbenchPreviewCard({ form }: { form: FinishForm }) {
|
||||
return (
|
||||
<div className="ml-preview-surface ml-sphere-preview relative h-[180px] w-full overflow-hidden rounded-lg bg-white p-3 ring-1 ring-black/5">
|
||||
<div
|
||||
className="pointer-events-none absolute inset-0 opacity-70"
|
||||
className="pointer-events-none absolute inset-0 opacity-0"
|
||||
style={{
|
||||
backgroundImage:
|
||||
'linear-gradient(rgba(38,48,51,0.05) 1px, transparent 1px), linear-gradient(90deg, rgba(38,48,51,0.05) 1px, transparent 1px)',
|
||||
@ -311,7 +311,7 @@ function FinishParameterSliderField({
|
||||
id: string, label: string, value: number, min?: number, max?: number, step?: number, helper?: string, disabled?: boolean, onChange: (val: number) => void
|
||||
}) {
|
||||
return (
|
||||
<div className={cn("space-y-2 rounded-lg border p-2.5 transition-all", disabled ? "border-transparent bg-transparent" : "border-black/10 bg-white hover:border-black/20")}>
|
||||
<div className={cn("space-y-2 py-2.5 transition-all", disabled ? "opacity-50" : null)}>
|
||||
<div className="flex items-center justify-between">
|
||||
<label htmlFor={id} className="text-[10px] font-apple-bold text-[#5f6d70]">
|
||||
{label}
|
||||
@ -1725,9 +1725,9 @@ export default function MaterialLibraryClient({
|
||||
<div className="min-h-0 flex-1 overflow-y-auto pr-1">
|
||||
{activeTab === 'finish' ? (
|
||||
<div className="space-y-4">
|
||||
<div className="relative min-h-[430px] overflow-hidden rounded-lg border border-black/10 bg-[#f7f9f9]">
|
||||
<div className="relative min-h-[430px] overflow-hidden bg-[#f7f9f9]">
|
||||
<div
|
||||
className="pointer-events-none absolute inset-0 opacity-50"
|
||||
className="pointer-events-none absolute inset-0 opacity-0"
|
||||
style={{
|
||||
backgroundImage:
|
||||
'linear-gradient(var(--ml-grid-line) 1px, transparent 1px), linear-gradient(90deg, var(--ml-grid-line) 1px, transparent 1px)',
|
||||
@ -1735,11 +1735,11 @@ export default function MaterialLibraryClient({
|
||||
}}
|
||||
/>
|
||||
{activeWorkbenchFinish ? (
|
||||
<div className="absolute inset-6 overflow-hidden rounded-lg border border-black/10 bg-white shadow-[0_24px_70px_var(--ml-shadow)]">
|
||||
<div className="absolute inset-0 overflow-hidden bg-white">
|
||||
<MaterialSurfacePreview item={activeWorkbenchFinish} className="absolute inset-0" />
|
||||
<div className="pointer-events-none absolute inset-0 bg-gradient-to-b from-white/10 via-transparent to-black/10" />
|
||||
|
||||
<div className="absolute bottom-0 right-0 top-0 z-10 w-[238px] max-w-[34%] overflow-hidden bg-white/38 px-4 py-5 text-left backdrop-blur-2xl ring-1 ring-white/45">
|
||||
<div className="absolute bottom-0 right-0 top-0 z-10 w-[238px] max-w-[34%] overflow-hidden bg-white/92 px-4 py-5 text-left ring-1 ring-black/[0.04]">
|
||||
<p className="text-[10px] font-apple-black text-[#4f5b5e]">贴图</p>
|
||||
<h2 className="mt-1 truncate text-xl font-apple-black tracking-tight text-[#101719]">
|
||||
{activeWorkbenchFinish.name}
|
||||
@ -1806,7 +1806,7 @@ export default function MaterialLibraryClient({
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-black/10 bg-white p-4">
|
||||
<div className="bg-white p-4">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-[9px] font-apple-black text-[#66777c]">花色列表</p>
|
||||
@ -1851,7 +1851,7 @@ export default function MaterialLibraryClient({
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<div className="rounded-lg border border-black/10 bg-white p-4">
|
||||
<div className="bg-white p-4">
|
||||
{activeWorkbenchSubstrate ? (
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="min-w-0">
|
||||
@ -1881,7 +1881,7 @@ export default function MaterialLibraryClient({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="rounded-lg border border-black/10 bg-white p-4">
|
||||
<div className="bg-white p-4">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-[9px] font-apple-black text-[#66777c]">基材列表</p>
|
||||
|
||||
@ -519,55 +519,55 @@
|
||||
@layer components {
|
||||
:root,
|
||||
[data-theme='light'] {
|
||||
--ml-bg: #f6f8f9;
|
||||
--ml-rail: #fbfcfc;
|
||||
--ml-canvas: #eef2f3;
|
||||
--ml-workbench: #f7f9f9;
|
||||
--ml-bg: #f6f7f7;
|
||||
--ml-rail: #ffffff;
|
||||
--ml-canvas: #f3f4f4;
|
||||
--ml-workbench: #f7f7f6;
|
||||
--ml-panel: #ffffff;
|
||||
--ml-text: #263033;
|
||||
--ml-muted: #708287;
|
||||
--ml-accent: #263033;
|
||||
--ml-accent-bg: rgba(17, 23, 25, 0.065);
|
||||
--ml-border: rgba(38, 48, 51, 0.10);
|
||||
--ml-text: #111315;
|
||||
--ml-muted: #666b70;
|
||||
--ml-accent: #22262a;
|
||||
--ml-accent-bg: rgba(17, 19, 21, 0.055);
|
||||
--ml-border: rgba(17, 19, 21, 0.10);
|
||||
--ml-glass: rgba(255, 255, 255, 0.74);
|
||||
--ml-grid-line: rgba(38, 48, 51, 0.055);
|
||||
--ml-shadow: rgba(38, 48, 51, 0.14);
|
||||
--ml-grid-line: rgba(17, 19, 21, 0.045);
|
||||
--ml-shadow: rgba(17, 19, 21, 0.12);
|
||||
--ml-row: rgba(255, 255, 255, 0.76);
|
||||
--ml-row-hover: rgba(255, 255, 255, 0.94);
|
||||
--ml-accent-border: rgba(17, 23, 25, 0.18);
|
||||
--ml-accent-ring: rgba(17, 23, 25, 0.08);
|
||||
--ml-accent-border: rgba(17, 19, 21, 0.16);
|
||||
--ml-accent-ring: rgba(17, 19, 21, 0.08);
|
||||
--ml-warning-bg: rgba(205, 77, 70, 0.09);
|
||||
--ml-warning-text: #a44640;
|
||||
--ml-warning-border: rgba(205, 77, 70, 0.18);
|
||||
--ml-ok-bg: rgba(17, 23, 25, 0.06);
|
||||
--ml-ok-text: #263033;
|
||||
--ml-muted-bg: rgba(38, 48, 51, 0.06);
|
||||
--ml-ok-bg: rgba(17, 19, 21, 0.055);
|
||||
--ml-ok-text: #22262a;
|
||||
--ml-muted-bg: rgba(17, 19, 21, 0.055);
|
||||
}
|
||||
|
||||
[data-theme='dark'] {
|
||||
--ml-bg: #f6f8f9;
|
||||
--ml-rail: #fbfcfc;
|
||||
--ml-canvas: #eef2f3;
|
||||
--ml-workbench: #f7f9f9;
|
||||
--ml-bg: #f6f7f7;
|
||||
--ml-rail: #ffffff;
|
||||
--ml-canvas: #f3f4f4;
|
||||
--ml-workbench: #f7f7f6;
|
||||
--ml-panel: #ffffff;
|
||||
--ml-text: #263033;
|
||||
--ml-muted: #708287;
|
||||
--ml-accent: #263033;
|
||||
--ml-accent-bg: rgba(17, 23, 25, 0.065);
|
||||
--ml-border: rgba(38, 48, 51, 0.10);
|
||||
--ml-text: #111315;
|
||||
--ml-muted: #666b70;
|
||||
--ml-accent: #22262a;
|
||||
--ml-accent-bg: rgba(17, 19, 21, 0.055);
|
||||
--ml-border: rgba(17, 19, 21, 0.10);
|
||||
--ml-glass: rgba(255, 255, 255, 0.74);
|
||||
--ml-grid-line: rgba(38, 48, 51, 0.055);
|
||||
--ml-shadow: rgba(38, 48, 51, 0.14);
|
||||
--ml-grid-line: rgba(17, 19, 21, 0.045);
|
||||
--ml-shadow: rgba(17, 19, 21, 0.12);
|
||||
--ml-row: rgba(255, 255, 255, 0.76);
|
||||
--ml-row-hover: rgba(255, 255, 255, 0.94);
|
||||
--ml-accent-border: rgba(17, 23, 25, 0.18);
|
||||
--ml-accent-ring: rgba(17, 23, 25, 0.08);
|
||||
--ml-accent-border: rgba(17, 19, 21, 0.16);
|
||||
--ml-accent-ring: rgba(17, 19, 21, 0.08);
|
||||
--ml-warning-bg: rgba(205, 77, 70, 0.09);
|
||||
--ml-warning-text: #a44640;
|
||||
--ml-warning-border: rgba(205, 77, 70, 0.18);
|
||||
--ml-ok-bg: rgba(17, 23, 25, 0.06);
|
||||
--ml-ok-text: #263033;
|
||||
--ml-muted-bg: rgba(38, 48, 51, 0.06);
|
||||
--ml-ok-bg: rgba(17, 19, 21, 0.055);
|
||||
--ml-ok-text: #22262a;
|
||||
--ml-muted-bg: rgba(17, 19, 21, 0.055);
|
||||
}
|
||||
|
||||
.material-library-obsidian {
|
||||
@ -632,48 +632,48 @@
|
||||
|
||||
:root,
|
||||
[data-theme='light'] {
|
||||
--obs-shell-bg: #eef2f3;
|
||||
--obs-shell-text: #263033;
|
||||
--obs-shell-bg: #f6f7f7;
|
||||
--obs-shell-text: #232629;
|
||||
--obs-shell-panel-bg: #ffffff;
|
||||
--obs-shell-content-bg: #f4f7f7;
|
||||
--obs-shell-header-bg: #f8faf9;
|
||||
--obs-shell-surface-bg: #ffffff;
|
||||
--obs-shell-muted-bg: #edf2f2;
|
||||
--obs-shell-rail-bg: #e5ebeb;
|
||||
--obs-shell-border: rgba(17, 23, 25, 0.12);
|
||||
--obs-shell-title: #101719;
|
||||
--obs-shell-muted: #5f6d70;
|
||||
--obs-shell-subtle: #7a878a;
|
||||
--obs-shell-hover-bg: rgba(17, 23, 25, 0.045);
|
||||
--obs-shell-control-bg: rgba(17, 23, 25, 0.045);
|
||||
--obs-shell-control-hover-bg: rgba(17, 23, 25, 0.08);
|
||||
--obs-shell-control-text: #4f5c5f;
|
||||
--obs-shell-control-hover-text: #101719;
|
||||
--obs-shell-icon-bg: rgba(17, 23, 25, 0.06);
|
||||
--obs-shell-icon-text: #2d393c;
|
||||
--obs-shell-rail-title: #687477;
|
||||
--obs-shell-rail-text: #3f4a4d;
|
||||
--obs-shell-rail-hover-bg: rgba(17, 23, 25, 0.06);
|
||||
--obs-shell-rail-hover-text: #101719;
|
||||
--obs-shell-rail-active-bg: #101719;
|
||||
--obs-shell-rail-active-text: #f7f9f9;
|
||||
--obs-shell-status-text: #566265;
|
||||
--obs-shell-status-muted: #6d787b;
|
||||
--obs-shell-status-strong: #101719;
|
||||
--obs-shell-status-pill-bg: rgba(17, 23, 25, 0.055);
|
||||
--obs-shell-action-bg: #101719;
|
||||
--obs-shell-action-text: #f7f9f9;
|
||||
--obs-shell-action-border: rgba(17, 23, 25, 0.16);
|
||||
--obs-shell-action-hover-bg: #263033;
|
||||
--obs-shell-rail-card-bg: rgba(17, 23, 25, 0.045);
|
||||
--obs-shell-rail-card-text: #101719;
|
||||
--obs-shell-rail-card-muted: #687477;
|
||||
--obs-shell-success: #3f4a4d;
|
||||
--obs-shell-success-bg: rgba(17, 23, 25, 0.045);
|
||||
--obs-shell-success-border: rgba(17, 23, 25, 0.10);
|
||||
--obs-shell-warning: #5a6062;
|
||||
--obs-shell-warning-bg: rgba(17, 23, 25, 0.045);
|
||||
--obs-shell-warning-border: rgba(17, 23, 25, 0.10);
|
||||
--obs-shell-content-bg: #f4f5f5;
|
||||
--obs-shell-header-bg: #fbfbfa;
|
||||
--obs-shell-surface-bg: #f7f7f6;
|
||||
--obs-shell-muted-bg: #f1f2f2;
|
||||
--obs-shell-rail-bg: #fbfbfa;
|
||||
--obs-shell-border: rgba(17, 19, 21, 0.10);
|
||||
--obs-shell-title: #111315;
|
||||
--obs-shell-muted: #666b70;
|
||||
--obs-shell-subtle: #858a8f;
|
||||
--obs-shell-hover-bg: rgba(17, 19, 21, 0.035);
|
||||
--obs-shell-control-bg: rgba(17, 19, 21, 0.045);
|
||||
--obs-shell-control-hover-bg: rgba(17, 19, 21, 0.07);
|
||||
--obs-shell-control-text: #555b60;
|
||||
--obs-shell-control-hover-text: #111315;
|
||||
--obs-shell-icon-bg: rgba(17, 19, 21, 0.055);
|
||||
--obs-shell-icon-text: #40464a;
|
||||
--obs-shell-rail-title: #74797e;
|
||||
--obs-shell-rail-text: #4b5156;
|
||||
--obs-shell-rail-hover-bg: rgba(17, 19, 21, 0.045);
|
||||
--obs-shell-rail-hover-text: #111315;
|
||||
--obs-shell-rail-active-bg: #e8eaeb;
|
||||
--obs-shell-rail-active-text: #111315;
|
||||
--obs-shell-status-text: #555b60;
|
||||
--obs-shell-status-muted: #73787d;
|
||||
--obs-shell-status-strong: #111315;
|
||||
--obs-shell-status-pill-bg: rgba(17, 19, 21, 0.05);
|
||||
--obs-shell-action-bg: #24282c;
|
||||
--obs-shell-action-text: #ffffff;
|
||||
--obs-shell-action-border: rgba(17, 19, 21, 0.18);
|
||||
--obs-shell-action-hover-bg: #171a1d;
|
||||
--obs-shell-rail-card-bg: rgba(17, 19, 21, 0.04);
|
||||
--obs-shell-rail-card-text: #111315;
|
||||
--obs-shell-rail-card-muted: #74797e;
|
||||
--obs-shell-success: #3f4a46;
|
||||
--obs-shell-success-bg: rgba(17, 19, 21, 0.045);
|
||||
--obs-shell-success-border: rgba(17, 19, 21, 0.10);
|
||||
--obs-shell-warning: #6a6260;
|
||||
--obs-shell-warning-bg: rgba(17, 19, 21, 0.045);
|
||||
--obs-shell-warning-border: rgba(17, 19, 21, 0.10);
|
||||
--obs-shell-danger: #b3453d;
|
||||
--obs-shell-danger-bg: rgba(179, 69, 61, 0.08);
|
||||
--obs-shell-danger-border: rgba(179, 69, 61, 0.18);
|
||||
@ -754,6 +754,18 @@
|
||||
background: var(--obs-shell-action-hover-bg) !important;
|
||||
}
|
||||
|
||||
.obsidian-control-shell[data-tone='light'] .obsidian-control-action {
|
||||
border-color: var(--obs-shell-border) !important;
|
||||
background: var(--obs-shell-panel-bg) !important;
|
||||
color: var(--obs-shell-title) !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.obsidian-control-shell[data-tone='light'] .obsidian-control-action:hover {
|
||||
background: var(--obs-shell-hover-bg) !important;
|
||||
color: var(--obs-shell-title) !important;
|
||||
}
|
||||
|
||||
.obsidian-control-rail-card {
|
||||
border-color: var(--obs-shell-border);
|
||||
background: var(--obs-shell-rail-card-bg);
|
||||
|
||||
@ -272,6 +272,7 @@ function UsersPageContent() {
|
||||
icon={ShieldCheck}
|
||||
backHref="/admin"
|
||||
backLabel="后台设置"
|
||||
tone="light"
|
||||
actions={
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
@ -338,15 +339,15 @@ function UsersPageContent() {
|
||||
</div>
|
||||
}
|
||||
inspector={
|
||||
<div className="flex h-full flex-col bg-white text-[#152024]">
|
||||
<div className="flex h-full flex-col bg-[var(--obs-shell-panel-bg)] text-[var(--obs-shell-title)]">
|
||||
{selectedUser ? (
|
||||
<>
|
||||
<div className="border-b border-[#dce3e4] bg-[#f8faf9] px-5 py-5">
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-[6px] border border-[#d8e0e1] bg-white text-[#405054]">
|
||||
<div className="border-b border-[var(--obs-shell-border)] bg-[var(--obs-shell-header-bg)] px-5 py-5">
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-[6px] border border-[var(--obs-shell-border)] bg-[var(--obs-shell-panel-bg)] text-[var(--obs-shell-icon-text)]">
|
||||
<Fingerprint className="h-6 w-6" />
|
||||
</div>
|
||||
<h2 className="mt-4 truncate text-lg font-black text-[#152024]">{selectedUser.name}</h2>
|
||||
<p className="mt-1 text-[11px] font-bold text-[#607073]">
|
||||
<h2 className="mt-4 truncate text-lg font-black text-[var(--obs-shell-title)]">{selectedUser.name}</h2>
|
||||
<p className="mt-1 text-[11px] font-bold text-[var(--obs-shell-muted)]">
|
||||
{roleLabels[selectedUser.role] ?? selectedUser.role}
|
||||
</p>
|
||||
</div>
|
||||
@ -397,24 +398,24 @@ function UsersPageContent() {
|
||||
</>
|
||||
) : (
|
||||
<div className="flex h-full flex-col items-center justify-center px-8 text-center">
|
||||
<Fingerprint className="h-7 w-7 text-[#6b777a]" />
|
||||
<h3 className="mt-4 text-sm font-black text-[#152024]">未选择账号</h3>
|
||||
<p className="mt-2 text-xs font-medium text-[#607073]">点击中间列表中的人员后查看权限和组织信息</p>
|
||||
<Fingerprint className="h-7 w-7 text-[var(--obs-shell-muted)]" />
|
||||
<h3 className="mt-4 text-sm font-black text-[var(--obs-shell-title)]">未选择账号</h3>
|
||||
<p className="mt-2 text-xs font-medium text-[var(--obs-shell-muted)]">点击中间列表中的人员后查看权限和组织信息</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="grid h-full grid-rows-[auto_minmax(0,1fr)] bg-[#f4f7f7]">
|
||||
<header className="border-b border-[#dce3e4] bg-[#f8faf9] px-4 py-3">
|
||||
<div className="grid h-full grid-rows-[auto_minmax(0,1fr)] bg-[var(--obs-shell-content-bg)]">
|
||||
<header className="border-b border-[var(--obs-shell-border)] bg-[var(--obs-shell-header-bg)] px-4 py-3">
|
||||
<div className="relative w-full max-w-xl">
|
||||
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-[#6b777a]" />
|
||||
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-[var(--obs-shell-subtle)]" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="搜索姓名、邮箱或 UID"
|
||||
value={searchQuery}
|
||||
onChange={(event) => 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)]"
|
||||
/>
|
||||
</div>
|
||||
</header>
|
||||
@ -484,11 +485,11 @@ function UsersPageContent() {
|
||||
|
||||
function InspectorItem({ icon: Icon, label, value }: { icon: LucideIcon; label: string; value: string }) {
|
||||
return (
|
||||
<div className="flex items-center gap-3 rounded-[6px] border border-[#dce3e4] bg-white px-3 py-3">
|
||||
<Icon className="h-4 w-4 shrink-0 text-[#6b777a]" />
|
||||
<div className="flex items-center gap-3 px-0 py-2.5">
|
||||
<Icon className="h-4 w-4 shrink-0 text-[var(--obs-shell-muted)]" />
|
||||
<div className="min-w-0">
|
||||
<p className="text-[11px] font-bold text-[#607073]">{label}</p>
|
||||
<p className="truncate text-sm font-black text-[#152024]">{value}</p>
|
||||
<p className="text-[11px] font-bold text-[var(--obs-shell-muted)]">{label}</p>
|
||||
<p className="truncate text-sm font-black text-[var(--obs-shell-title)]">{value}</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -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<ObsidianShellTone>('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 (
|
||||
<div className={cn('obsidian-control-shell h-full min-h-0 bg-[#0d1113] p-3 text-[#e7eeec]', className)}>
|
||||
<ObsidianShellToneContext.Provider value={tone}>
|
||||
<div
|
||||
data-tone={tone}
|
||||
className={cn(
|
||||
'grid h-full min-h-0 gap-3',
|
||||
rail && inspector && 'lg:grid-cols-[224px_minmax(0,1fr)_336px]',
|
||||
rail && !inspector && 'lg:grid-cols-[224px_minmax(0,1fr)]',
|
||||
!rail && inspector && 'lg:grid-cols-[minmax(0,1fr)_336px]',
|
||||
'obsidian-control-shell h-full min-h-0 bg-[var(--obs-shell-bg)] p-3 text-[var(--obs-shell-text)]',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{rail ? (
|
||||
<aside className="flex min-h-0 flex-col rounded-md border border-white/10 bg-[#111719]">
|
||||
<div className="flex h-12 shrink-0 items-center gap-2 border-b border-white/10 px-3">
|
||||
{backHref ? (
|
||||
<Link
|
||||
href={backHref}
|
||||
aria-label={backLabel}
|
||||
className="flex h-8 w-8 items-center justify-center rounded-md border border-white/10 bg-white/[0.03] text-[#9ba8a8] transition hover:bg-white/[0.07] hover:text-white"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
</Link>
|
||||
) : null}
|
||||
<div className="min-w-0">
|
||||
<p className="text-[10px] font-black uppercase tracking-[0.18em] text-[#788586]">{eyebrow}</p>
|
||||
<p className="truncate text-xs font-black text-[#dce7e4]">{backLabel}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="min-h-0 flex-1 overflow-y-auto p-3">{rail}</div>
|
||||
</aside>
|
||||
) : null}
|
||||
|
||||
<section className={cn('flex min-h-0 flex-col overflow-hidden rounded-md border border-white/10 bg-[#101517]', contentClassName)}>
|
||||
<header className="flex h-14 shrink-0 items-center justify-between gap-3 border-b border-white/10 bg-[#111719] px-4">
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
{Icon ? (
|
||||
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md border border-white/10 bg-white/[0.04] text-[#cbd8d5]">
|
||||
<Icon className="h-4 w-4" />
|
||||
<div
|
||||
className={cn(
|
||||
'grid h-full min-h-0 gap-3',
|
||||
rail && inspector && 'lg:grid-cols-[224px_minmax(0,1fr)_336px]',
|
||||
rail && !inspector && 'lg:grid-cols-[224px_minmax(0,1fr)]',
|
||||
!rail && inspector && 'lg:grid-cols-[minmax(0,1fr)_336px]',
|
||||
)}
|
||||
>
|
||||
{rail ? (
|
||||
<aside
|
||||
className={cn(
|
||||
'flex min-h-0 flex-col rounded-md border',
|
||||
isLight ? 'border-[var(--obs-shell-border)] bg-[var(--obs-shell-rail-bg)]' : 'border-white/10 bg-[#111719]',
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'flex h-12 shrink-0 items-center gap-2 border-b px-3',
|
||||
isLight ? 'border-[var(--obs-shell-border)]' : 'border-white/10',
|
||||
)}
|
||||
>
|
||||
{backHref ? (
|
||||
<Link
|
||||
href={backHref}
|
||||
aria-label={backLabel}
|
||||
className={cn(
|
||||
'flex h-8 w-8 items-center justify-center rounded-md border transition',
|
||||
isLight
|
||||
? 'border-[var(--obs-shell-border)] bg-[var(--obs-shell-panel-bg)] text-[var(--obs-shell-muted)] hover:bg-[var(--obs-shell-hover-bg)] hover:text-[var(--obs-shell-title)]'
|
||||
: 'border-white/10 bg-white/[0.03] text-[#9ba8a8] hover:bg-white/[0.07] hover:text-white',
|
||||
)}
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
</Link>
|
||||
) : null}
|
||||
<div className="min-w-0">
|
||||
<p className={cn('text-[10px] font-black uppercase tracking-[0.18em]', isLight ? 'text-[var(--obs-shell-subtle)]' : 'text-[#788586]')}>{eyebrow}</p>
|
||||
<p className={cn('truncate text-xs font-black', isLight ? 'text-[var(--obs-shell-title)]' : 'text-[#dce7e4]')}>{backLabel}</p>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="min-w-0">
|
||||
<p className="text-[10px] font-black uppercase tracking-[0.18em] text-[#788586]">{eyebrow}</p>
|
||||
<h1 className="truncate text-base font-black text-[#f4f7f6]">{title}</h1>
|
||||
</div>
|
||||
</div>
|
||||
{actions ? <div className="flex shrink-0 items-center gap-2">{actions}</div> : null}
|
||||
</header>
|
||||
<div className="min-h-0 flex-1 overflow-hidden">{children}</div>
|
||||
</section>
|
||||
<div className="min-h-0 flex-1 overflow-y-auto p-3">{rail}</div>
|
||||
</aside>
|
||||
) : null}
|
||||
|
||||
{inspector ? (
|
||||
<aside className={cn('min-h-0 overflow-hidden rounded-md border border-white/10 bg-[#111719]', inspectorClassName)}>
|
||||
{inspector}
|
||||
</aside>
|
||||
) : null}
|
||||
<section
|
||||
className={cn(
|
||||
'flex min-h-0 flex-col overflow-hidden rounded-md border',
|
||||
isLight ? 'border-[var(--obs-shell-border)] bg-[var(--obs-shell-panel-bg)]' : 'border-white/10 bg-[#101517]',
|
||||
contentClassName,
|
||||
)}
|
||||
>
|
||||
<header
|
||||
className={cn(
|
||||
'flex h-14 shrink-0 items-center justify-between gap-3 border-b px-4',
|
||||
isLight ? 'border-[var(--obs-shell-border)] bg-[var(--obs-shell-header-bg)]' : 'border-white/10 bg-[#111719]',
|
||||
)}
|
||||
>
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
{Icon ? (
|
||||
<div
|
||||
className={cn(
|
||||
'flex h-8 w-8 shrink-0 items-center justify-center rounded-md border',
|
||||
isLight ? 'border-[var(--obs-shell-border)] bg-[var(--obs-shell-panel-bg)] text-[var(--obs-shell-icon-text)]' : 'border-white/10 bg-white/[0.04] text-[#cbd8d5]',
|
||||
)}
|
||||
>
|
||||
<Icon className="h-4 w-4" />
|
||||
</div>
|
||||
) : null}
|
||||
<div className="min-w-0">
|
||||
<p className={cn('text-[10px] font-black uppercase tracking-[0.18em]', isLight ? 'text-[var(--obs-shell-subtle)]' : 'text-[#788586]')}>{eyebrow}</p>
|
||||
<h1 className={cn('truncate text-base font-black', isLight ? 'text-[var(--obs-shell-title)]' : 'text-[#f4f7f6]')}>{title}</h1>
|
||||
</div>
|
||||
</div>
|
||||
{actions ? <div className="flex shrink-0 items-center gap-2">{actions}</div> : null}
|
||||
</header>
|
||||
<div className="min-h-0 flex-1 overflow-hidden">{children}</div>
|
||||
</section>
|
||||
|
||||
{inspector ? (
|
||||
<aside
|
||||
className={cn(
|
||||
'min-h-0 overflow-hidden rounded-md border',
|
||||
isLight ? 'border-[var(--obs-shell-border)] bg-[var(--obs-shell-panel-bg)]' : 'border-white/10 bg-[#111719]',
|
||||
inspectorClassName,
|
||||
)}
|
||||
>
|
||||
{inspector}
|
||||
</aside>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ObsidianShellToneContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function ObsidianRailGroup({ title, children }: { title: string; children: ReactNode }) {
|
||||
const tone = useContext(ObsidianShellToneContext)
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="px-2 text-[10px] font-black uppercase tracking-[0.18em] text-[#748083]">{title}</div>
|
||||
<div className={cn('px-2 text-[10px] font-black uppercase tracking-[0.18em]', tone === 'light' ? 'text-[var(--obs-shell-rail-title)]' : 'text-[#748083]')}>{title}</div>
|
||||
<div className="space-y-1">{children}</div>
|
||||
</div>
|
||||
)
|
||||
@ -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 (
|
||||
<section className={cn('rounded-md border border-white/10 bg-[#111719]', className)}>
|
||||
<section
|
||||
className={cn(
|
||||
'rounded-md border',
|
||||
tone === 'light' ? 'border-[var(--obs-shell-border)] bg-[var(--obs-shell-panel-bg)]' : 'border-white/10 bg-[#111719]',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{title || eyebrow ? (
|
||||
<header className="border-b border-white/10 px-4 py-3">
|
||||
<header className={cn('border-b px-4 py-3', tone === 'light' ? 'border-[var(--obs-shell-border)]' : 'border-white/10')}>
|
||||
{eyebrow ? (
|
||||
<p className="text-[10px] font-black uppercase tracking-[0.18em] text-[#748083]">{eyebrow}</p>
|
||||
<p className={cn('text-[10px] font-black uppercase tracking-[0.18em]', tone === 'light' ? 'text-[var(--obs-shell-subtle)]' : 'text-[#748083]')}>{eyebrow}</p>
|
||||
) : null}
|
||||
{title ? <h2 className="mt-1 text-sm font-black text-[#edf4f2]">{title}</h2> : null}
|
||||
{title ? <h2 className={cn('mt-1 text-sm font-black', tone === 'light' ? 'text-[var(--obs-shell-title)]' : 'text-[#edf4f2]')}>{title}</h2> : null}
|
||||
</header>
|
||||
) : null}
|
||||
{children}
|
||||
@ -181,12 +250,14 @@ export function ObsidianInspectorSection({
|
||||
title: string
|
||||
children: ReactNode
|
||||
}) {
|
||||
const tone = useContext(ObsidianShellToneContext)
|
||||
|
||||
return (
|
||||
<section className="border-b border-white/10 px-4 py-4 last:border-b-0">
|
||||
<h3 className="mb-3 text-[10px] font-black uppercase tracking-[0.18em] text-[#7f8d8e]">
|
||||
<section className={cn('border-b px-4 py-4 last:border-b-0', tone === 'light' ? 'border-[var(--obs-shell-border)]' : 'border-white/10')}>
|
||||
<h3 className={cn('mb-3 text-[10px] font-black uppercase tracking-[0.18em]', tone === 'light' ? 'text-[var(--obs-shell-subtle)]' : 'text-[#7f8d8e]')}>
|
||||
{title}
|
||||
</h3>
|
||||
<div className="space-y-2">{children}</div>
|
||||
<div className={tone === 'light' ? 'divide-y divide-[var(--obs-shell-border)]' : 'space-y-2'}>{children}</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@ -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 (
|
||||
<div className="flex items-center justify-between gap-4 rounded-md border border-white/10 bg-[#151d1f] px-3 py-2.5">
|
||||
<span className="text-[11px] font-bold text-[#8f9c9d]">{label}</span>
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center justify-between gap-4',
|
||||
toneContext === 'light'
|
||||
? 'px-0 py-2.5'
|
||||
: 'rounded-md border border-white/10 bg-[#151d1f] px-3 py-2.5',
|
||||
)}
|
||||
>
|
||||
<span className={cn('text-[11px] font-bold', toneContext === 'light' ? 'text-[var(--obs-shell-muted)]' : 'text-[#8f9c9d]')}>{label}</span>
|
||||
<span
|
||||
className={cn(
|
||||
'min-w-0 truncate text-right text-xs font-black',
|
||||
tone === 'success' && 'text-[#3f4a4d]',
|
||||
tone === 'warning' && 'text-[#5a6062]',
|
||||
tone === 'default' && 'text-[#edf4f2]',
|
||||
valueToneClassName,
|
||||
)}
|
||||
>
|
||||
{value}
|
||||
|
||||
@ -133,9 +133,9 @@ interface SectionHeaderProps {
|
||||
|
||||
function SummaryBadge({ label, value }: { label: string; value: string }) {
|
||||
return (
|
||||
<span className="inline-flex h-6 items-center gap-1.5 rounded-[5px] border border-[#dce3e4] bg-[#f7f9f9] px-2 text-[10px] font-bold text-[#607073]">
|
||||
<span className="inline-flex h-6 items-center gap-1.5 rounded-[4px] bg-[var(--obs-shell-muted-bg)] px-2 text-[10px] font-bold text-[var(--obs-shell-muted)]">
|
||||
<span>{label}</span>
|
||||
<span className="font-black text-[#152024]">{value}</span>
|
||||
<span className="font-black text-[var(--obs-shell-title)]">{value}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@ -145,17 +145,17 @@ function SectionHeader({ icon, title, count, isOpen, onToggle }: SectionHeaderPr
|
||||
<button
|
||||
type="button"
|
||||
onClick={onToggle}
|
||||
className="flex h-8 w-full items-center gap-2 rounded-[5px] px-2 text-left transition-colors hover:bg-[#f0f4f4] focus:outline-none focus-visible:ring-2 focus-visible:ring-[#b9c8c7]"
|
||||
className="flex h-8 w-full items-center gap-2 rounded-[5px] px-2 text-left transition-colors hover:bg-[var(--obs-shell-hover-bg)] focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--obs-shell-border)]"
|
||||
>
|
||||
<ChevronDown
|
||||
size={14}
|
||||
className={cn('text-[#6b777a] transition-transform', isOpen && 'rotate-180')}
|
||||
className={cn('text-[var(--obs-shell-muted)] transition-transform', isOpen && 'rotate-180')}
|
||||
/>
|
||||
<div className="text-[#6b777a]">{icon}</div>
|
||||
<span className="text-[11px] font-black text-[#384548]">
|
||||
<div className="text-[var(--obs-shell-muted)]">{icon}</div>
|
||||
<span className="text-[11px] font-black text-[var(--obs-shell-title)]">
|
||||
{title}
|
||||
</span>
|
||||
<span className="ml-auto rounded-[4px] bg-[#e9eeee] px-1.5 py-0.5 text-[10px] font-black text-[#607073]">
|
||||
<span className="ml-auto rounded-[4px] bg-[var(--obs-shell-muted-bg)] px-1.5 py-0.5 text-[10px] font-black text-[var(--obs-shell-muted)]">
|
||||
{count} 人
|
||||
</span>
|
||||
</button>
|
||||
@ -164,7 +164,7 @@ function SectionHeader({ icon, title, count, isOpen, onToggle }: SectionHeaderPr
|
||||
|
||||
function UserLedgerHeader() {
|
||||
return (
|
||||
<div className="hidden border-b border-[#e6ecec] bg-[#f7f9f9] px-3 py-2 text-[10px] font-black text-[#6a7679] xl:grid xl:grid-cols-[minmax(220px,1.25fr)_minmax(170px,0.95fr)_minmax(140px,0.8fr)_minmax(112px,0.65fr)_auto] xl:items-center">
|
||||
<div className="hidden border-b border-[var(--obs-shell-border)] bg-[var(--obs-shell-surface-bg)] px-3 py-2 text-[10px] font-black text-[var(--obs-shell-subtle)] xl:grid xl:grid-cols-[minmax(220px,1.25fr)_minmax(170px,0.95fr)_minmax(140px,0.8fr)_minmax(112px,0.65fr)_auto] xl:items-center">
|
||||
<span>账号</span>
|
||||
<span>通讯</span>
|
||||
<span>组织</span>
|
||||
@ -202,43 +202,43 @@ function TeamNodeView({
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="rounded-[6px] border border-[#dce3e4] bg-white">
|
||||
<div className="border-l border-[var(--obs-shell-border)] bg-[var(--obs-shell-panel-bg)] pl-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsOpen((current) => !current)}
|
||||
className="flex min-h-[42px] w-full items-center justify-between gap-3 px-3 py-2 text-left transition-colors hover:bg-[#f8faf9] focus:outline-none focus-visible:ring-2 focus-visible:ring-[#b9c8c7]"
|
||||
className="flex min-h-[42px] w-full items-center justify-between gap-3 px-3 py-2 text-left transition-colors hover:bg-[var(--obs-shell-hover-bg)] focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--obs-shell-border)]"
|
||||
>
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="truncate text-[13px] font-black text-[#152024]">{team.name}</span>
|
||||
<span className="rounded-[4px] border border-[#dce3e4] bg-[#f7f9f9] px-1.5 py-0.5 text-[10px] font-black text-[#607073]">
|
||||
<span className="truncate text-[13px] font-black text-[var(--obs-shell-title)]">{team.name}</span>
|
||||
<span className="rounded-[4px] bg-[var(--obs-shell-muted-bg)] px-1.5 py-0.5 text-[10px] font-black text-[var(--obs-shell-muted)]">
|
||||
{team.summary.memberCount} 人
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-1 flex flex-wrap items-center gap-2 text-[11px] text-[#607073]">
|
||||
<span className="inline-flex items-center gap-1 rounded-[4px] bg-[#eef3f3] px-1.5 py-0.5 font-bold text-[#4e5b5e]">
|
||||
<div className="mt-1 flex flex-wrap items-center gap-2 text-[11px] text-[var(--obs-shell-muted)]">
|
||||
<span className="inline-flex items-center gap-1 rounded-[4px] bg-[var(--obs-shell-muted-bg)] px-1.5 py-0.5 font-bold text-[var(--obs-shell-control-text)]">
|
||||
<ShieldCheck className="h-3 w-3" />
|
||||
主管
|
||||
</span>
|
||||
<span className="font-bold text-[#263033]">{team.leader?.name || '未设置'}</span>
|
||||
{team.description ? <span className="truncate text-[#6b777a]">{team.description}</span> : null}
|
||||
<span className="font-bold text-[var(--obs-shell-title)]">{team.leader?.name || '未设置'}</span>
|
||||
{team.description ? <span className="truncate text-[var(--obs-shell-muted)]">{team.description}</span> : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ChevronDown
|
||||
className={cn(
|
||||
'h-4 w-4 shrink-0 text-[#6b777a] transition-transform duration-150',
|
||||
'h-4 w-4 shrink-0 text-[var(--obs-shell-muted)] transition-transform duration-150',
|
||||
isOpen ? 'rotate-180' : 'rotate-0'
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<div className="border-t border-[#e6ecec] bg-white">
|
||||
<div className="border-t border-[var(--obs-shell-border)] bg-[var(--obs-shell-panel-bg)]">
|
||||
{team.users.length > 0 ? (
|
||||
<div>
|
||||
<UserLedgerHeader />
|
||||
<div className="divide-y divide-[#edf1f1]">
|
||||
<div className="divide-y divide-[var(--obs-shell-border)]">
|
||||
{team.users.map((user) => (
|
||||
<UserListItem
|
||||
key={user.id}
|
||||
@ -258,7 +258,7 @@ function TeamNodeView({
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="m-2 flex items-center gap-2 rounded-[6px] border border-dashed border-[#d6dee0] bg-white px-4 py-3 text-sm text-[#607073]">
|
||||
<div className="m-2 flex items-center gap-2 rounded-[6px] bg-[var(--obs-shell-muted-bg)] px-4 py-3 text-sm text-[var(--obs-shell-muted)]">
|
||||
<Users className="h-4 w-4" />
|
||||
<span>该小组当前没有匹配的成员</span>
|
||||
</div>
|
||||
@ -306,35 +306,35 @@ function DepartmentNodeView({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'rounded-[6px] border border-[#d8e0e1] bg-white',
|
||||
depth > 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'
|
||||
)}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsOpen((current) => !current)}
|
||||
className="flex w-full items-center justify-between gap-3 px-3 py-2.5 text-left transition-colors hover:bg-[#f8faf9] focus:outline-none focus-visible:ring-2 focus-visible:ring-[#b9c8c7]"
|
||||
className="flex w-full items-center justify-between gap-3 px-3 py-2.5 text-left transition-colors hover:bg-[var(--obs-shell-hover-bg)] focus:outline-none focus-visible:ring-2 focus-visible:ring-[var(--obs-shell-border)]"
|
||||
>
|
||||
<div className="flex min-w-0 items-center gap-2.5">
|
||||
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-[6px] border border-[#dce3e4] bg-[#f2f6f6] text-[#526164]">
|
||||
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-[6px] bg-[var(--obs-shell-muted-bg)] text-[var(--obs-shell-icon-text)]">
|
||||
<Building2 className="h-4 w-4" />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="truncate text-[14px] font-black text-[#152024]">
|
||||
<span className="truncate text-[14px] font-black text-[var(--obs-shell-title)]">
|
||||
{department.name}
|
||||
</span>
|
||||
<span className="rounded-[4px] border border-[#dce3e4] bg-[#f7f9f9] px-1.5 py-0.5 text-[10px] font-black text-[#607073]">
|
||||
<span className="rounded-[4px] bg-[var(--obs-shell-muted-bg)] px-1.5 py-0.5 text-[10px] font-black text-[var(--obs-shell-muted)]">
|
||||
{department.summary.totalUserCount} 人
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-1 flex flex-wrap items-center gap-x-3 gap-y-1.5 text-[11px] text-[#607073]">
|
||||
<span className="truncate text-[#6b777a]">{department.fullPath}</span>
|
||||
<div className="inline-flex items-center gap-1 rounded-[4px] bg-[#eef3f3] px-1.5 py-0.5 font-bold text-[#4e5b5e]">
|
||||
<div className="mt-1 flex flex-wrap items-center gap-x-3 gap-y-1.5 text-[11px] text-[var(--obs-shell-muted)]">
|
||||
<span className="truncate text-[var(--obs-shell-muted)]">{department.fullPath}</span>
|
||||
<div className="inline-flex items-center gap-1 rounded-[4px] bg-[var(--obs-shell-muted-bg)] px-1.5 py-0.5 font-bold text-[var(--obs-shell-control-text)]">
|
||||
<ShieldCheck className="h-3 w-3" />
|
||||
<span>主管</span>
|
||||
</div>
|
||||
<span className="font-bold text-[#263033]">
|
||||
<span className="font-bold text-[var(--obs-shell-title)]">
|
||||
{department.manager?.name || '未设置'}
|
||||
</span>
|
||||
</div>
|
||||
@ -349,14 +349,14 @@ function DepartmentNodeView({
|
||||
|
||||
<ChevronDown
|
||||
className={cn(
|
||||
'h-4 w-4 shrink-0 text-[#6b777a] transition-transform duration-150',
|
||||
'h-4 w-4 shrink-0 text-[var(--obs-shell-muted)] transition-transform duration-150',
|
||||
isOpen ? 'rotate-180' : 'rotate-0'
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<div className="border-t border-[#e6ecec] bg-white px-2 py-2">
|
||||
<div className="border-t border-[var(--obs-shell-border)] bg-[var(--obs-shell-panel-bg)] px-2 py-2">
|
||||
{hasDirectUsers && (
|
||||
<div className="mb-3">
|
||||
<SectionHeader
|
||||
@ -367,9 +367,9 @@ function DepartmentNodeView({
|
||||
onToggle={() => setDirectUsersOpen((current) => !current)}
|
||||
/>
|
||||
{directUsersOpen && (
|
||||
<div className="mt-2 overflow-hidden rounded-[5px] border border-[#e8eeee] bg-white">
|
||||
<div className="mt-2 bg-[var(--obs-shell-panel-bg)]">
|
||||
<UserLedgerHeader />
|
||||
<div className="divide-y divide-[#edf1f1]">
|
||||
<div className="divide-y divide-[var(--obs-shell-border)]">
|
||||
{department.users.map((user) => (
|
||||
<UserListItem
|
||||
key={user.id}
|
||||
@ -425,8 +425,8 @@ function DepartmentNodeView({
|
||||
)}
|
||||
|
||||
{hasChildren && (
|
||||
<div className="mt-3 border-l border-[#dce3e4] pl-3">
|
||||
<div className="mb-2 text-[11px] font-black text-[#607073]">
|
||||
<div className="mt-3 border-l border-[var(--obs-shell-border)] pl-3">
|
||||
<div className="mb-2 text-[11px] font-black text-[var(--obs-shell-muted)]">
|
||||
子部门
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
@ -452,7 +452,7 @@ function DepartmentNodeView({
|
||||
)}
|
||||
|
||||
{!hasDirectUsers && !hasTeams && !hasChildren && (
|
||||
<div className="flex items-center gap-2 rounded-[6px] border border-dashed border-[#d6dee0] bg-white px-4 py-3 text-sm text-[#607073]">
|
||||
<div className="flex items-center gap-2 rounded-[6px] bg-[var(--obs-shell-muted-bg)] px-4 py-3 text-sm text-[var(--obs-shell-muted)]">
|
||||
<Users className="h-4 w-4" />
|
||||
<span>该部门当前没有匹配的组织成员</span>
|
||||
</div>
|
||||
@ -550,8 +550,8 @@ export function DepartmentAccessTree({
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center rounded-[6px] border border-[#dce3e4] bg-white py-12">
|
||||
<RefreshCw className="h-5 w-5 animate-spin text-[#6b777a]" />
|
||||
<div className="flex items-center justify-center rounded-[6px] border border-[var(--obs-shell-border)] bg-[var(--obs-shell-panel-bg)] py-12">
|
||||
<RefreshCw className="h-5 w-5 animate-spin text-[var(--obs-shell-muted)]" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -575,9 +575,9 @@ export function DepartmentAccessTree({
|
||||
|
||||
if (!hasDepartments && unassignedUsers.length === 0) {
|
||||
return (
|
||||
<div className="rounded-[6px] border border-dashed border-[#d6dee0] bg-white px-6 py-10 text-center">
|
||||
<Building2 className="mx-auto mb-3 h-8 w-8 text-[#6b777a]" />
|
||||
<p className="text-sm font-medium text-[#607073]">暂无匹配的组织数据</p>
|
||||
<div className="rounded-[6px] border border-dashed border-[var(--obs-shell-border)] bg-[var(--obs-shell-panel-bg)] px-6 py-10 text-center">
|
||||
<Building2 className="mx-auto mb-3 h-8 w-8 text-[var(--obs-shell-muted)]" />
|
||||
<p className="text-sm font-medium text-[var(--obs-shell-muted)]">暂无匹配的组织数据</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -603,28 +603,28 @@ export function DepartmentAccessTree({
|
||||
))}
|
||||
|
||||
{unassignedUsers.length > 0 && (
|
||||
<div className="overflow-hidden rounded-[6px] border border-[#d8e0e1] bg-white">
|
||||
<div className="overflow-hidden rounded-[6px] border border-[var(--obs-shell-border)] bg-[var(--obs-shell-panel-bg)]">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setUnassignedOpen((current) => !current)}
|
||||
className="flex min-h-[42px] w-full items-center gap-3 px-3 py-2 text-left transition-colors hover:bg-[#f8faf9] focus:outline-none focus-visible:ring-2 focus-visible:ring-[#b9c8c7] focus-visible:ring-inset"
|
||||
className="flex min-h-[42px] w-full items-center gap-3 px-3 py-2 text-left transition-colors 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"
|
||||
>
|
||||
<ChevronDown
|
||||
size={14}
|
||||
className={cn('text-[#6b777a] transition-transform', unassignedOpen && 'rotate-180')}
|
||||
className={cn('text-[var(--obs-shell-muted)] transition-transform', unassignedOpen && 'rotate-180')}
|
||||
/>
|
||||
<UserCircle2 size={16} className="text-[#6b777a]" />
|
||||
<span className="text-[13px] font-black text-[#152024]">
|
||||
<UserCircle2 size={16} className="text-[var(--obs-shell-muted)]" />
|
||||
<span className="text-[13px] font-black text-[var(--obs-shell-title)]">
|
||||
未分配 / 外部人员
|
||||
</span>
|
||||
<span className="ml-auto rounded-[4px] bg-[#e9eeee] px-1.5 py-0.5 text-[10px] font-black text-[#607073]">
|
||||
<span className="ml-auto rounded-[4px] bg-[var(--obs-shell-muted-bg)] px-1.5 py-0.5 text-[10px] font-black text-[var(--obs-shell-muted)]">
|
||||
{unassignedUsers.length} 人
|
||||
</span>
|
||||
</button>
|
||||
{unassignedOpen && (
|
||||
<div className="border-t border-[#e6ecec] bg-white">
|
||||
<div className="border-t border-[var(--obs-shell-border)] bg-[var(--obs-shell-panel-bg)]">
|
||||
<UserLedgerHeader />
|
||||
<div className="divide-y divide-[#edf1f1]">
|
||||
<div className="divide-y divide-[var(--obs-shell-border)]">
|
||||
{unassignedUsers.map((user) => (
|
||||
<UserListItem
|
||||
key={user.id}
|
||||
|
||||
@ -84,57 +84,57 @@ interface UserListItemProps {
|
||||
const statusConfig = {
|
||||
ACTIVE: {
|
||||
label: '在职',
|
||||
color: 'border-[#bfe4d4] bg-[#edf8f3] text-[#1f7a56]',
|
||||
color: 'border-[var(--obs-shell-success-border)] bg-[var(--obs-shell-success-bg)] text-[var(--obs-shell-success)]',
|
||||
},
|
||||
PENDING: {
|
||||
label: '待激活',
|
||||
color: 'border-[#ead9aa] bg-[#fff8e8] text-[#8a641b]',
|
||||
color: 'border-[var(--obs-shell-warning-border)] bg-[var(--obs-shell-warning-bg)] text-[var(--obs-shell-warning)]',
|
||||
},
|
||||
INACTIVE: {
|
||||
label: '离职',
|
||||
color: 'border-[#d8dee0] bg-[#f2f5f5] text-[#687477]',
|
||||
color: 'border-[var(--obs-shell-border)] bg-[var(--obs-shell-muted-bg)] text-[var(--obs-shell-muted)]',
|
||||
},
|
||||
SUSPENDED: {
|
||||
label: '锁定',
|
||||
color: 'border-[#efc8c4] bg-[#fff1ef] text-[#a9483f]',
|
||||
color: 'border-[var(--obs-shell-danger-border)] bg-[var(--obs-shell-danger-bg)] text-[var(--obs-shell-danger)]',
|
||||
},
|
||||
};
|
||||
|
||||
const roleConfig: Record<string, { label: string; color: string; avatar: string }> = {
|
||||
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)]'
|
||||
)}
|
||||
>
|
||||
<Icon className="h-3.5 w-3.5" />
|
||||
@ -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 && (
|
||||
<div className="absolute bottom-2 left-0 top-2 w-1 rounded-r-full bg-[#d59c2b]" />
|
||||
<div className="absolute bottom-2 left-0 top-2 w-1 rounded-r-full bg-[var(--obs-shell-warning)]" />
|
||||
)}
|
||||
|
||||
<div className="flex min-w-0 items-center gap-2.5">
|
||||
@ -333,34 +328,34 @@ export const UserListItem = memo(function UserListItem({
|
||||
)}
|
||||
</div>
|
||||
{isCurrentUser && (
|
||||
<div className="absolute -bottom-0.5 -right-0.5 h-3 w-3 rounded-full border-2 border-white bg-[#263033]" />
|
||||
<div className="absolute -bottom-0.5 -right-0.5 h-3 w-3 rounded-full border-2 border-[var(--obs-shell-panel-bg)] bg-[var(--obs-shell-title)]" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1">
|
||||
<h3 className="min-w-0 truncate text-[13px] font-black leading-5 text-[#152024]">
|
||||
<h3 className="min-w-0 truncate text-[13px] font-black leading-5 text-[var(--obs-shell-title)]">
|
||||
{user.name}
|
||||
</h3>
|
||||
<IdentityChip className={roleStyle.color}>{roleStyle.label}</IdentityChip>
|
||||
<IdentityChip className={statusStyle.color}>{statusStyle.label}</IdentityChip>
|
||||
{selected && (
|
||||
<IdentityChip className="border-[#152024] bg-[#152024] text-[#fdfdfc]">当前选择</IdentityChip>
|
||||
<IdentityChip className="border-[var(--obs-shell-title)] bg-[var(--obs-shell-title)] text-[var(--obs-shell-panel-bg)]">当前选择</IdentityChip>
|
||||
)}
|
||||
{isCurrentUser && (
|
||||
<IdentityChip className="border-[#cbd2d4] bg-[#f7f7f6] text-[#405054]">当前账号</IdentityChip>
|
||||
<IdentityChip className="border-[var(--obs-shell-border)] bg-[var(--obs-shell-surface-bg)] text-[var(--obs-shell-control-text)]">当前账号</IdentityChip>
|
||||
)}
|
||||
{user.employeeDirectoryId && (
|
||||
<IdentityChip className="border-[#c9ded8] bg-[#f0f8f5] text-[#2c6654]">已匹配员工库</IdentityChip>
|
||||
<IdentityChip className="border-[var(--obs-shell-success-border)] bg-[var(--obs-shell-success-bg)] text-[var(--obs-shell-success)]">已匹配员工库</IdentityChip>
|
||||
)}
|
||||
{!user.employeeDirectoryId && (
|
||||
<IdentityChip className="border-[#d8dee0] bg-[#f7f9f9] text-[#5d696c]">未匹配员工库</IdentityChip>
|
||||
<IdentityChip className="border-[var(--obs-shell-border)] bg-[var(--obs-shell-surface-bg)] text-[var(--obs-shell-muted)]">未匹配员工库</IdentityChip>
|
||||
)}
|
||||
{user.loginSource === 'DINGTALK' && (
|
||||
<IdentityChip className="border-[#cbdbea] bg-[#eef5fb] text-[#2f5f86]">钉钉已绑定</IdentityChip>
|
||||
<IdentityChip className="border-[var(--obs-shell-border)] bg-[var(--obs-shell-surface-bg)] text-[var(--obs-shell-muted)]">钉钉已绑定</IdentityChip>
|
||||
)}
|
||||
{(user.businessRoles || []).map((role) => (
|
||||
<IdentityChip key={role} className="border-[#d8d2c4] bg-[#fbf7ee] text-[#6b5736]">
|
||||
<IdentityChip key={role} className="border-[var(--obs-shell-border)] bg-[var(--obs-shell-surface-bg)] text-[var(--obs-shell-muted)]">
|
||||
{BUSINESS_ACCOUNT_TYPE_LABELS[role as keyof typeof BUSINESS_ACCOUNT_TYPE_LABELS] ?? role}
|
||||
</IdentityChip>
|
||||
))}
|
||||
@ -368,32 +363,32 @@ export const UserListItem = memo(function UserListItem({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid min-w-0 gap-1 text-[11px] font-medium text-[#607073]">
|
||||
<div className="grid min-w-0 gap-1 text-[11px] font-medium text-[var(--obs-shell-muted)]">
|
||||
<div className="flex min-w-0 items-center gap-1.5">
|
||||
<Mail className="h-3.5 w-3.5 shrink-0 text-[#9aa7aa]" />
|
||||
<Mail className="h-3.5 w-3.5 shrink-0 text-[var(--obs-shell-subtle)]" />
|
||||
<span className="truncate">{user.email}</span>
|
||||
</div>
|
||||
{user.phone ? (
|
||||
<div className="flex min-w-0 items-center gap-1.5">
|
||||
<Phone className="h-3.5 w-3.5 shrink-0 text-[#9aa7aa]" />
|
||||
<Phone className="h-3.5 w-3.5 shrink-0 text-[var(--obs-shell-subtle)]" />
|
||||
<span className="truncate">{user.phone}</span>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="grid min-w-0 gap-1 text-[11px] font-medium text-[#607073]">
|
||||
<div className="grid min-w-0 gap-1 text-[11px] font-medium text-[var(--obs-shell-muted)]">
|
||||
<div className="flex min-w-0 items-center gap-1.5">
|
||||
<Building2 className="h-3.5 w-3.5 shrink-0 text-[#9aa7aa]" />
|
||||
<Building2 className="h-3.5 w-3.5 shrink-0 text-[var(--obs-shell-subtle)]" />
|
||||
<span className="truncate">{user.department?.name || '未归属部门'}</span>
|
||||
</div>
|
||||
<div className="flex min-w-0 items-center gap-1.5">
|
||||
<Users className="h-3.5 w-3.5 shrink-0 text-[#9aa7aa]" />
|
||||
<Users className="h-3.5 w-3.5 shrink-0 text-[var(--obs-shell-subtle)]" />
|
||||
<span className="truncate">{user.team?.name || '未归属小组'}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex min-w-0 items-center gap-1.5 text-[11px] font-medium text-[#607073]">
|
||||
<Clock className="h-3.5 w-3.5 shrink-0 text-[#9aa7aa]" />
|
||||
<div className="flex min-w-0 items-center gap-1.5 text-[11px] font-medium text-[var(--obs-shell-muted)]">
|
||||
<Clock className="h-3.5 w-3.5 shrink-0 text-[var(--obs-shell-subtle)]" />
|
||||
<span className="truncate">{formattedLastLogin}</span>
|
||||
</div>
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user