481 lines
22 KiB
TypeScript
481 lines
22 KiB
TypeScript
'use client'
|
||
|
||
import Image from 'next/image'
|
||
import Link from 'next/link'
|
||
import { signOut } from 'next-auth/react'
|
||
import { useRouter } from 'next/navigation'
|
||
import { Session } from 'next-auth'
|
||
import { useState, useRef, useEffect } from 'react'
|
||
import { initializeNetwork, setupNetworkChangeDetection, addNetworkChangeListener } from '@/lib/network'
|
||
import UpdateLogModal, { useUpdateLog, showUpdateLog } from '@/components/UpdateLogModal'
|
||
import MaintenanceBanner from '@/components/MaintenanceBanner'
|
||
import HeaderTaobaoSearch from '@/components/HeaderTaobaoSearch'
|
||
import BrandLogo from '@/components/BrandLogo'
|
||
import SystemPerformanceCard, { type SystemPerformanceData } from '@/components/board/SystemPerformanceCard'
|
||
import { DEFAULT_LOGIN_LOGO_URL } from '@/lib/brand-assets'
|
||
|
||
/*
|
||
* MaterialsHeader —— 创意资产库板块顶栏(看板语言 · Zen Minimal)
|
||
* 移植自 components/Header.tsx 的全部功能(淘宝搜索/内外网状态/管理员性能监控/用户菜单/更新日志),
|
||
* 视觉统一到品牌中心白皮书语言:浅底、发丝线、墨字。
|
||
* 用户菜单是“个人工作台”的统一入口;链接保留原有权限与业务行为,只迁移其入口体验。
|
||
*/
|
||
|
||
interface MaterialsHeaderProps {
|
||
session: Session | null
|
||
/** 板块模式:传入板块名显示面包屑;不传则首页模式(显示五大板块导航) */
|
||
sectionLabel?: string
|
||
}
|
||
|
||
const BOARD_NAV: Array<{ key: string; label: string; href?: string; planned?: boolean; adminOnly?: boolean }> = [
|
||
{ key: 'dashboard', label: '中心仪表盘', href: '/' },
|
||
{ key: 'assets', label: '创意资产库', href: '/materials' },
|
||
{ key: 'organization', label: '组织管理', href: '/organization', adminOnly: true },
|
||
{ key: 'business', label: '业务经营', planned: true },
|
||
{ key: 'governance', label: '治理与设置', planned: true },
|
||
]
|
||
|
||
export default function MaterialsHeader({ session, sectionLabel }: MaterialsHeaderProps) {
|
||
const router = useRouter()
|
||
const role = session ? String((session.user as any)?.role || '') : ''
|
||
const isAdmin = ['ADMIN', 'SYSTEM_ADMIN', 'DEPT_ADMIN'].includes(role)
|
||
const canViewPerformance = ['ADMIN', 'SYSTEM_ADMIN'].includes(role) || Boolean((session?.user as any)?.isSuperAdmin)
|
||
const [showDropdown, setShowDropdown] = useState(false)
|
||
const [isInternalNetwork, setIsInternalNetwork] = useState(false)
|
||
const [networkChecked, setNetworkChecked] = useState(false)
|
||
const dropdownRef = useRef<HTMLDivElement>(null)
|
||
|
||
// 性能监控悬停卡片状态
|
||
const [showPerfCard, setShowPerfCard] = useState(false)
|
||
const [perfLoading, setPerfLoading] = useState(false)
|
||
const [perfError, setPerfError] = useState<string | null>(null)
|
||
const [perfData, setPerfData] = useState<SystemPerformanceData | null>(null)
|
||
const perfHoverTimerRef = useRef<number | null>(null)
|
||
const perfHideTimerRef = useRef<number | null>(null)
|
||
const perfIntervalRef = useRef<number | null>(null)
|
||
|
||
const { showModal: showUpdateLogModal, currentLog, showAllHistory, handleClose: handleUpdateLogClose } = useUpdateLog()
|
||
|
||
// 品牌设置(logo):浅底表面用浅底适配版
|
||
const [brandLogoUrl, setBrandLogoUrl] = useState(DEFAULT_LOGIN_LOGO_URL)
|
||
useEffect(() => {
|
||
const loadSettings = async () => {
|
||
try {
|
||
const response = await fetch('/api/system-settings')
|
||
if (response.ok) {
|
||
const settings = await response.json()
|
||
setBrandLogoUrl(settings.loginLogoUrl || DEFAULT_LOGIN_LOGO_URL)
|
||
}
|
||
} catch {
|
||
// 设置加载失败时保留默认 logo
|
||
}
|
||
}
|
||
void loadSettings()
|
||
}, [])
|
||
|
||
// 更新浏览器标题(板块身份)
|
||
useEffect(() => {
|
||
document.title = sectionLabel ? `由壹点·品牌中心 · ${sectionLabel}` : '由壹点·品牌中心'
|
||
}, [sectionLabel])
|
||
|
||
// 权威档案身份行(菜单头:花名下的职位·部门;无绑定时回退邮箱)
|
||
const [employeeLine, setEmployeeLine] = useState<string | null>(null)
|
||
useEffect(() => {
|
||
const load = async () => {
|
||
try {
|
||
const res = await fetch('/api/user/profile')
|
||
if (!res.ok) return
|
||
const data = await res.json()
|
||
const employee = data?.employee
|
||
if (employee) {
|
||
const dept = employee.departmentPath ? employee.departmentPath.split('/').filter(Boolean).slice(-2).join(' / ') : ''
|
||
const line = [employee.positionTitle, dept].filter(Boolean).join(' · ')
|
||
setEmployeeLine(line || employee.jobNumber || null)
|
||
}
|
||
} catch {
|
||
// 档案行加载失败时回退邮箱展示
|
||
}
|
||
}
|
||
void load()
|
||
}, [])
|
||
|
||
// 检测网络状态
|
||
useEffect(() => {
|
||
const detectNetwork = async () => {
|
||
try {
|
||
const networkStatus = await initializeNetwork()
|
||
setIsInternalNetwork(networkStatus.isInternal)
|
||
setNetworkChecked(true)
|
||
} catch (error) {
|
||
console.error('网络检测失败:', error)
|
||
setIsInternalNetwork(false)
|
||
setNetworkChecked(true)
|
||
}
|
||
}
|
||
detectNetwork()
|
||
setupNetworkChangeDetection()
|
||
const unsubscribe = addNetworkChangeListener((status) => {
|
||
setIsInternalNetwork(status.isInternal)
|
||
})
|
||
return () => {
|
||
unsubscribe()
|
||
}
|
||
}, [])
|
||
|
||
// 点击外部或按 Escape 关闭用户菜单
|
||
useEffect(() => {
|
||
const handleClickOutside = (event: MouseEvent) => {
|
||
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
||
setShowDropdown(false)
|
||
}
|
||
}
|
||
const handleKeyDown = (event: KeyboardEvent) => {
|
||
if (event.key === 'Escape') setShowDropdown(false)
|
||
}
|
||
document.addEventListener('mousedown', handleClickOutside)
|
||
document.addEventListener('keydown', handleKeyDown)
|
||
return () => {
|
||
document.removeEventListener('mousedown', handleClickOutside)
|
||
document.removeEventListener('keydown', handleKeyDown)
|
||
}
|
||
}, [])
|
||
|
||
// 性能监控:根据 showPerfCard 控制轮询(2 秒)
|
||
useEffect(() => {
|
||
const clearIntervalSafe = () => {
|
||
if (perfIntervalRef.current !== null) {
|
||
window.clearInterval(perfIntervalRef.current)
|
||
perfIntervalRef.current = null
|
||
}
|
||
}
|
||
if (!canViewPerformance || !showPerfCard) {
|
||
clearIntervalSafe()
|
||
return
|
||
}
|
||
const fetchPerf = async (isInitial: boolean) => {
|
||
try {
|
||
if (isInitial) setPerfLoading(true)
|
||
setPerfError(null)
|
||
const res = await fetch('/api/performance/quick')
|
||
if (!res.ok) {
|
||
throw new Error(`HTTP ${res.status}`)
|
||
}
|
||
const data = await res.json()
|
||
setPerfData(data)
|
||
} catch (err) {
|
||
console.error('加载性能数据失败:', err)
|
||
setPerfError('刚才没读到')
|
||
} finally {
|
||
setPerfLoading(false)
|
||
}
|
||
}
|
||
void fetchPerf(true)
|
||
perfIntervalRef.current = window.setInterval(() => {
|
||
void fetchPerf(false)
|
||
}, 2000)
|
||
return () => {
|
||
clearIntervalSafe()
|
||
}
|
||
}, [canViewPerformance, showPerfCard])
|
||
|
||
// 悬停事件处理:延迟显示 / 隐藏性能卡片
|
||
const handlePerfMouseEnter = () => {
|
||
if (!canViewPerformance) return
|
||
if (perfHideTimerRef.current !== null) {
|
||
window.clearTimeout(perfHideTimerRef.current)
|
||
perfHideTimerRef.current = null
|
||
}
|
||
if (perfHoverTimerRef.current === null) {
|
||
perfHoverTimerRef.current = window.setTimeout(() => {
|
||
setShowPerfCard(true)
|
||
perfHoverTimerRef.current = null
|
||
}, 300)
|
||
}
|
||
}
|
||
|
||
const handlePerfMouseLeave = () => {
|
||
if (!canViewPerformance) return
|
||
if (perfHoverTimerRef.current !== null) {
|
||
window.clearTimeout(perfHoverTimerRef.current)
|
||
perfHoverTimerRef.current = null
|
||
}
|
||
if (perfHideTimerRef.current !== null) {
|
||
window.clearTimeout(perfHideTimerRef.current)
|
||
perfHideTimerRef.current = null
|
||
}
|
||
perfHideTimerRef.current = window.setTimeout(() => {
|
||
setShowPerfCard(false)
|
||
perfHideTimerRef.current = null
|
||
}, 100)
|
||
}
|
||
|
||
const getInitials = (name: string) => {
|
||
return name?.charAt(0).toUpperCase() || 'U'
|
||
}
|
||
|
||
const menuItemCls =
|
||
'group flex w-full items-start gap-3 rounded-md px-3 py-3 text-left text-[#1a1c1d] transition-colors hover:bg-[#f1f2f4] focus:outline-none focus-visible:ring-2 focus-visible:ring-[#1a1c1d]/70 focus-visible:ring-offset-2 focus-visible:ring-offset-white'
|
||
const menuIconCls =
|
||
'mt-0.5 h-4 w-4 shrink-0 text-[#6b7075] transition-colors group-hover:text-[#1a1c1d]'
|
||
|
||
return (
|
||
<>
|
||
<MaintenanceBanner />
|
||
|
||
<header className="sticky top-0 z-50 border-b border-[#e7e8ea] bg-[#fafafa]">
|
||
<div className="mx-auto flex h-14 max-w-[1280px] items-center gap-3 px-4 sm:px-6">
|
||
{/* 品牌 */}
|
||
<Link
|
||
href="/"
|
||
className="flex min-w-0 items-center gap-2.5 rounded-sm text-[14px] font-semibold tracking-tight text-[#1a1c1d] transition-opacity hover:opacity-70"
|
||
>
|
||
<BrandLogo
|
||
src={brandLogoUrl}
|
||
fallbackSrc={DEFAULT_LOGIN_LOGO_URL}
|
||
alt=""
|
||
width={28}
|
||
height={28}
|
||
unoptimized
|
||
className="h-7 w-7 shrink-0 rounded-md object-cover"
|
||
/>
|
||
<span className="hidden sm:inline">由壹点·品牌中心</span>
|
||
<span className="sr-only sm:hidden">由壹点·品牌中心</span>
|
||
</Link>
|
||
|
||
{sectionLabel ? (
|
||
/* 板块模式:面包屑 */
|
||
<>
|
||
<span aria-hidden="true" className="text-[12px] text-[#d4d6da]">/</span>
|
||
<span className="text-[13px] font-medium text-[#6b7075]">{sectionLabel}</span>
|
||
</>
|
||
) : (
|
||
/* 首页模式:五大板块导航 */
|
||
<nav className="ml-auto hidden items-center gap-6 md:flex" aria-label="看板板块导航">
|
||
{BOARD_NAV.filter((i) => !i.adminOnly || isAdmin).map((item) =>
|
||
item.planned ? (
|
||
<span key={item.key} className="text-[13px] text-[#86898e]" title="规划中,尚未开放">
|
||
{item.label}
|
||
</span>
|
||
) : (
|
||
<Link
|
||
key={item.key}
|
||
href={item.href ?? '/'}
|
||
aria-current={item.key === 'dashboard' ? 'page' : undefined}
|
||
className={`rounded-sm text-[13px] transition-opacity hover:opacity-60 ${
|
||
item.key === 'dashboard' ? 'font-semibold text-[#1a1c1d]' : 'text-[#6b7075]'
|
||
}`}
|
||
>
|
||
{item.label}
|
||
</Link>
|
||
),
|
||
)}
|
||
</nav>
|
||
)}
|
||
|
||
<nav className={`flex items-center gap-3 sm:gap-5 ${sectionLabel ? 'ml-auto' : 'md:ml-0'}`} aria-label="页面导航">
|
||
{session && (
|
||
<div className="hidden sm:block">
|
||
<HeaderTaobaoSearch />
|
||
</div>
|
||
)}
|
||
|
||
{/* 网络状态 + 性能监控悬停卡片 */}
|
||
{networkChecked && (
|
||
<div
|
||
className="relative hidden sm:block"
|
||
onMouseEnter={canViewPerformance ? handlePerfMouseEnter : undefined}
|
||
onMouseLeave={canViewPerformance ? handlePerfMouseLeave : undefined}
|
||
>
|
||
{canViewPerformance ? (
|
||
<button
|
||
type="button"
|
||
aria-expanded={showPerfCard}
|
||
aria-controls="system-performance-panel"
|
||
onClick={() => setShowPerfCard((open) => !open)}
|
||
className={`flex items-center gap-1.5 rounded-full border px-3 py-1.5 text-xs font-medium transition-opacity hover:opacity-80 focus:outline-none focus-visible:ring-2 focus-visible:ring-[#1a1c1d]/70 focus-visible:ring-offset-2 focus-visible:ring-offset-[#fafafa] ${
|
||
isInternalNetwork
|
||
? 'border-emerald-600/20 bg-emerald-600/10 text-emerald-700'
|
||
: 'border-sky-600/20 bg-sky-600/10 text-sky-700'
|
||
}`}
|
||
>
|
||
<svg className="h-3 w-3" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true">
|
||
<circle cx="10" cy="10" r="4" />
|
||
</svg>
|
||
<span>{isInternalNetwork ? '内网' : '公网'}</span>
|
||
</button>
|
||
) : (
|
||
<div
|
||
className={`flex items-center gap-1.5 rounded-full border px-3 py-1.5 text-xs font-medium ${
|
||
isInternalNetwork
|
||
? 'border-emerald-600/20 bg-emerald-600/10 text-emerald-700'
|
||
: 'border-sky-600/20 bg-sky-600/10 text-sky-700'
|
||
}`}
|
||
>
|
||
<svg className="h-3 w-3" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true">
|
||
<circle cx="10" cy="10" r="4" />
|
||
</svg>
|
||
<span>{isInternalNetwork ? '内网' : '公网'}</span>
|
||
</div>
|
||
)}
|
||
|
||
{canViewPerformance && showPerfCard && (
|
||
<SystemPerformanceCard data={perfData} loading={perfLoading} error={perfError} />
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* 用户菜单 */}
|
||
{session && (
|
||
<div className="relative" ref={dropdownRef}>
|
||
<button
|
||
onClick={() => setShowDropdown(!showDropdown)}
|
||
aria-label={showDropdown ? '关闭个人工作台菜单' : '打开个人工作台菜单'}
|
||
aria-expanded={showDropdown}
|
||
className="flex items-center rounded-md p-1.5 transition-colors hover:bg-[#f1f2f4] focus:outline-none focus-visible:ring-2 focus-visible:ring-[#1a1c1d]/70 focus-visible:ring-offset-2 focus-visible:ring-offset-[#fafafa]"
|
||
>
|
||
{(session.user as any)?.avatar ? (
|
||
<Image
|
||
src={(session.user as any).avatar}
|
||
alt={session.user?.name || '用户'}
|
||
width={32}
|
||
height={32}
|
||
className="h-8 w-8 rounded-full border border-[#e7e8ea] object-cover transition-colors hover:border-[#9aa0a8]"
|
||
unoptimized
|
||
/>
|
||
) : (
|
||
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-[#1a1c1d] text-base font-semibold text-white">
|
||
{getInitials(session.user?.name || '')}
|
||
</div>
|
||
)}
|
||
</button>
|
||
|
||
{showDropdown && (
|
||
<div
|
||
role="menu"
|
||
aria-label="个人工作台"
|
||
className="absolute right-0 z-50 mt-2 max-h-[calc(100dvh-4.5rem)] w-[min(21rem,calc(100vw-2rem))] overflow-y-auto border border-[#dfe1e4] bg-white p-2 [scrollbar-color:#c9ccd1_transparent] [scrollbar-width:thin]"
|
||
>
|
||
<div className="border-b border-[#e7e8ea] px-3 pb-4 pt-3">
|
||
<p className="font-display text-[17px] font-semibold tracking-[-0.02em] text-[#1a1c1d]">{session.user?.name}</p>
|
||
<p className="mt-1 truncate text-[12px] leading-relaxed text-[#6b7075]">
|
||
{employeeLine ?? session.user?.email}
|
||
</p>
|
||
<p className="mt-3 font-en text-[10px] uppercase tracking-[0.14em] text-[#86898e]">个人工作台</p>
|
||
</div>
|
||
|
||
{/* 个人档案 */}
|
||
<Link href="/profile" onClick={() => setShowDropdown(false)} className={menuItemCls} role="menuitem">
|
||
<svg className={menuIconCls} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||
</svg>
|
||
<span className="min-w-0">
|
||
<span className="block text-[13px] font-semibold">我的档案</span>
|
||
<span className="mt-0.5 block text-[12px] leading-snug text-[#6b7075]">账号资料、组织归属与我的素材</span>
|
||
</span>
|
||
</Link>
|
||
|
||
<div aria-hidden="true" className="my-1.5 border-t border-[#e7e8ea]" />
|
||
|
||
{/* 工具 */}
|
||
<button
|
||
onClick={() => {
|
||
setShowDropdown(false)
|
||
showUpdateLog('all')
|
||
}}
|
||
className={menuItemCls}
|
||
role="menuitem"
|
||
>
|
||
<svg className={menuIconCls} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
|
||
</svg>
|
||
<span className="min-w-0">
|
||
<span className="block text-[13px] font-semibold">更新记录</span>
|
||
<span className="mt-0.5 block text-[12px] leading-snug text-[#6b7075]">查看已发布的功能与体验改进</span>
|
||
</span>
|
||
</button>
|
||
|
||
<a
|
||
href="https://alidocs.dingtalk.com/i/nodes/NZQYprEoWoeEabj3srv6RwDkJ1waOeDk?utm_scene=person_space"
|
||
target="_blank"
|
||
rel="noopener noreferrer"
|
||
onClick={() => setShowDropdown(false)}
|
||
className={menuItemCls}
|
||
role="menuitem"
|
||
>
|
||
<svg className={menuIconCls} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z" />
|
||
</svg>
|
||
<span className="min-w-0">
|
||
<span className="flex items-center gap-1 text-[13px] font-semibold">提交反馈 <span aria-hidden="true" className="text-[#86898e]">↗</span></span>
|
||
<span className="mt-0.5 block text-[12px] leading-snug text-[#6b7075]">在钉钉文档中留下问题或建议</span>
|
||
</span>
|
||
</a>
|
||
|
||
{isAdmin && (
|
||
<>
|
||
<div aria-hidden="true" className="my-1.5 border-t border-[#e7e8ea]" />
|
||
<Link href="/admin" onClick={() => setShowDropdown(false)} className={menuItemCls} role="menuitem">
|
||
<svg className={menuIconCls} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path
|
||
strokeLinecap="round"
|
||
strokeLinejoin="round"
|
||
strokeWidth={2}
|
||
d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
|
||
/>
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||
</svg>
|
||
<span className="min-w-0">
|
||
<span className="block text-[13px] font-semibold">素材库后台</span>
|
||
<span className="mt-0.5 block text-[12px] leading-snug text-[#6b7075]">素材管理、统计与系统配置</span>
|
||
</span>
|
||
</Link>
|
||
</>
|
||
)}
|
||
|
||
<div aria-hidden="true" className="my-1.5 border-t border-[#e7e8ea]" />
|
||
|
||
<button
|
||
onClick={async () => {
|
||
setShowDropdown(false)
|
||
try {
|
||
await signOut({ redirect: false })
|
||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||
router.push('/login')
|
||
router.refresh()
|
||
} catch (error) {
|
||
console.error('退出登录失败:', error)
|
||
router.push('/login')
|
||
router.refresh()
|
||
}
|
||
}}
|
||
className="group flex w-full items-center gap-3 rounded-md px-3 py-3 text-left text-[13px] font-semibold text-[#6b7075] transition-colors hover:bg-[#f5f0f0] hover:text-[#a23c36] focus:outline-none focus-visible:ring-2 focus-visible:ring-[#1a1c1d]/70 focus-visible:ring-offset-2 focus-visible:ring-offset-white"
|
||
role="menuitem"
|
||
>
|
||
<svg className="h-4 w-4 shrink-0 transition-colors group-hover:text-[#a23c36]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path
|
||
strokeLinecap="round"
|
||
strokeLinejoin="round"
|
||
strokeWidth={2}
|
||
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
|
||
/>
|
||
</svg>
|
||
<span>退出登录</span>
|
||
</button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
</nav>
|
||
</div>
|
||
</header>
|
||
|
||
<UpdateLogModal
|
||
isOpen={showUpdateLogModal}
|
||
onClose={handleUpdateLogClose}
|
||
updateLog={currentLog || undefined}
|
||
showAllHistory={showAllHistory}
|
||
/>
|
||
</>
|
||
)
|
||
}
|