plugin detail panel header

This commit is contained in:
JzoNg 2024-10-12 14:39:53 +08:00
parent ecd2a1be9f
commit 99f5fea001
8 changed files with 206 additions and 19 deletions

View File

@ -0,0 +1,9 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Icon">
<path id="Vector" fill-rule="evenodd" clip-rule="evenodd" d="M11.3891 2.41987C11.6635 2.58871 11.749 2.94802 11.5802 3.22239L10.3324 5.25H11.0833C11.4055 5.25 11.6667 5.51117 11.6667 5.83334V11.6667C11.6667 12.311 11.1444 12.8333 10.5 12.8333H3.50001C2.85568 12.8333 2.33334 12.311 2.33334 11.6667V5.83334C2.33334 5.51117 2.59451 5.25 2.91668 5.25H8.96252L10.5865 2.61094C10.7554 2.33657 11.1147 2.25102 11.3891 2.41987ZM5.83334 7.58334C5.51118 7.58334 5.25001 7.84449 5.25001 8.16667C5.25001 8.48884 5.51118 8.75 5.83334 8.75H8.16668C8.48885 8.75 8.75001 8.48884 8.75001 8.16667C8.75001 7.84449 8.48885 7.58334 8.16668 7.58334H5.83334Z" fill="#676F83"/>
<g id="Vector_2" opacity="0.5">
<path d="M6.91257 1.79347C6.96898 1.76525 7.01477 1.71948 7.043 1.66303L7.32195 1.10508C7.42946 0.890105 7.73623 0.890105 7.84374 1.10508L8.12269 1.66303C8.15093 1.71948 8.19672 1.76525 8.25313 1.79347L8.81108 2.07245C9.0261 2.17994 9.0261 2.48672 8.81108 2.5942L8.25313 2.87318C8.19672 2.9014 8.15093 2.94717 8.12269 3.00362L7.84374 3.56158C7.73623 3.77655 7.42946 3.77655 7.32195 3.56158L7.043 3.00362C7.01477 2.94717 6.96898 2.9014 6.91257 2.87318L6.35461 2.5942C6.13965 2.48672 6.13965 2.17994 6.35461 2.07245L6.91257 1.79347Z" fill="#676F83"/>
<path d="M3.80145 2.7657C3.85789 2.73748 3.90366 2.69171 3.93189 2.63526L4.11364 2.27174C4.22113 2.05677 4.5279 2.05677 4.63539 2.27174L4.81715 2.63526C4.84537 2.6917 4.89114 2.73748 4.94759 2.7657L5.3111 2.94745C5.52607 3.05494 5.52607 3.36172 5.3111 3.4692L4.94759 3.65096C4.89114 3.67919 4.84537 3.72495 4.81715 3.7814L4.63539 4.14491C4.5279 4.35988 4.22113 4.35988 4.11364 4.14491L3.93189 3.7814C3.90366 3.72495 3.85789 3.67919 3.80145 3.65096L3.43793 3.4692C3.22296 3.36172 3.22296 3.05494 3.43793 2.94745L3.80145 2.7657Z" fill="#676F83"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -0,0 +1,66 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "14",
"height": "14",
"viewBox": "0 0 14 14",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "g",
"attributes": {
"id": "Icon"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"id": "Vector",
"fill-rule": "evenodd",
"clip-rule": "evenodd",
"d": "M11.3891 2.41987C11.6635 2.58871 11.749 2.94802 11.5802 3.22239L10.3324 5.25H11.0833C11.4055 5.25 11.6667 5.51117 11.6667 5.83334V11.6667C11.6667 12.311 11.1444 12.8333 10.5 12.8333H3.50001C2.85568 12.8333 2.33334 12.311 2.33334 11.6667V5.83334C2.33334 5.51117 2.59451 5.25 2.91668 5.25H8.96252L10.5865 2.61094C10.7554 2.33657 11.1147 2.25102 11.3891 2.41987ZM5.83334 7.58334C5.51118 7.58334 5.25001 7.84449 5.25001 8.16667C5.25001 8.48884 5.51118 8.75 5.83334 8.75H8.16668C8.48885 8.75 8.75001 8.48884 8.75001 8.16667C8.75001 7.84449 8.48885 7.58334 8.16668 7.58334H5.83334Z",
"fill": "currentColor"
},
"children": []
},
{
"type": "element",
"name": "g",
"attributes": {
"id": "Vector_2",
"opacity": "0.5"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"d": "M6.91257 1.79347C6.96898 1.76525 7.01477 1.71948 7.043 1.66303L7.32195 1.10508C7.42946 0.890105 7.73623 0.890105 7.84374 1.10508L8.12269 1.66303C8.15093 1.71948 8.19672 1.76525 8.25313 1.79347L8.81108 2.07245C9.0261 2.17994 9.0261 2.48672 8.81108 2.5942L8.25313 2.87318C8.19672 2.9014 8.15093 2.94717 8.12269 3.00362L7.84374 3.56158C7.73623 3.77655 7.42946 3.77655 7.32195 3.56158L7.043 3.00362C7.01477 2.94717 6.96898 2.9014 6.91257 2.87318L6.35461 2.5942C6.13965 2.48672 6.13965 2.17994 6.35461 2.07245L6.91257 1.79347Z",
"fill": "currentColor"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M3.80145 2.7657C3.85789 2.73748 3.90366 2.69171 3.93189 2.63526L4.11364 2.27174C4.22113 2.05677 4.5279 2.05677 4.63539 2.27174L4.81715 2.63526C4.84537 2.6917 4.89114 2.73748 4.94759 2.7657L5.3111 2.94745C5.52607 3.05494 5.52607 3.36172 5.3111 3.4692L4.94759 3.65096C4.89114 3.67919 4.84537 3.72495 4.81715 3.7814L4.63539 4.14491C4.5279 4.35988 4.22113 4.35988 4.11364 4.14491L3.93189 3.7814C3.90366 3.72495 3.85789 3.67919 3.80145 3.65096L3.43793 3.4692C3.22296 3.36172 3.22296 3.05494 3.43793 2.94745L3.80145 2.7657Z",
"fill": "currentColor"
},
"children": []
}
]
}
]
}
]
},
"name": "BoxSparkleFill"
}

View File

@ -0,0 +1,16 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './BoxSparkleFill.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
props,
ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />)
Icon.displayName = 'BoxSparkleFill'
export default Icon

View File

@ -1 +1,2 @@
export { default as BoxSparkleFill } from './BoxSparkleFill'
export { default as LeftCorner } from './LeftCorner'

View File

@ -1,16 +1,20 @@
'use client'
import React, { useEffect, useState } from 'react'
import React, { useEffect, useMemo, useState } from 'react'
import type { FC } from 'react'
import { useTranslation } from 'react-i18next'
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
import { RiVerifiedBadgeLine } from '@remixicon/react'
import Badge from '../../base/badge'
import { RiCloseLine, RiVerifiedBadgeLine } from '@remixicon/react'
import type { Plugin } from '../types'
import Badge from '../../base/badge'
import Description from '../card/base/description'
import Icon from '../card/base/card-icon'
import Title from '../card/base/title'
import DownloadCount from '../card/base/download-count'
import OperationDropdown from './operation-dropdown'
import type { Locale } from '@/i18n'
import { fetchPluginDetail } from '@/app/(commonLayout)/plugins/test/card/actions'
import { BoxSparkleFill } from '@/app/components/base/icons/src/vender/plugin'
import Button from '@/app/components/base/button'
import ActionButton from '@/app/components/base/action-button'
import Drawer from '@/app/components/base/drawer'
import Loading from '@/app/components/base/loading'
import cn from '@/utils/classnames'
@ -27,6 +31,7 @@ type Props = {
const PluginDetailPanel: FC<Props> = ({
locale,
}) => {
const { t } = useTranslation()
const searchParams = useSearchParams()
const pluginID = searchParams.get('pluginID')
const router = useRouter()
@ -34,6 +39,12 @@ const PluginDetailPanel: FC<Props> = ({
const [loading, setLoading] = useState(true)
const [pluginDetail, setPluginDetail] = useState<Plugin>()
const hasNewVersion = useMemo(() => {
if (!pluginDetail)
return false
return pluginDetail.latest_version !== pluginDetail.version
}, [pluginDetail])
const getPluginDetail = async (pluginID: string) => {
setLoading(true)
const detail = await fetchPluginDetail(pluginID)
@ -49,6 +60,8 @@ const PluginDetailPanel: FC<Props> = ({
router.replace(pathname)
}
const handleUpdate = () => {}
useEffect(() => {
if (pluginID)
getPluginDetail(pluginID)
@ -65,40 +78,43 @@ const PluginDetailPanel: FC<Props> = ({
footer={null}
mask={false}
positionCenter={false}
panelClassname={cn('mt-[65px] !w-[405px] !max-w-[405px]')}
panelClassname={cn('mt-[64px] mr-2 mb-2 !w-[420px] !max-w-[420px] !p-0 !bg-components-panel-bg rounded-2xl border-[0.5px] border-components-panel-border shadow-xl')}
>
{loading && <Loading type='area' />}
{!loading && pluginDetail && (
<div
className={cn('w-full flex flex-col bg-white border-[0.5px] border-gray-200 rounded-xl shadow-xl')}
style={{
height: 'calc(100vh - 65px)',
}}
>
<div className={cn('group relative p-4 pb-3 border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg hover-bg-components-panel-on-panel-item-bg rounded-xl shadow-xs')}>
{/* Header */}
<div className={cn('w-full flex flex-col')}>
<div className={cn('shrink-0 p-4 pb-3 border-b border-divider-subtle bg-components-panel-bg')}>
<div className="flex">
<Icon src={pluginDetail.icon} />
<div className="ml-3 w-0 grow">
<div className="flex items-center h-5">
<Title title={pluginDetail.label[locale]} />
<RiVerifiedBadgeLine className="shrink-0 ml-0.5 w-4 h-4 text-text-accent" />
<Badge
className='mx-1'
text={pluginDetail.version}
hasRedCornerMark={hasNewVersion}
/>
{hasNewVersion && (
<Button variant='secondary-accent' size='small' className='!h-5' onClick={handleUpdate}>{t('plugin.detailPanel.operation.update')}</Button>
)}
</div>
<div className='mb-1 flex justify-between items-center h-4'>
<div className='flex items-center'>
<div className='text-text-tertiary system-xs-regular'>{pluginDetail.org}</div>
<div className='mx-2 text-text-quaternary system-xs-regular'>·</div>
<DownloadCount downloadCount={pluginDetail.install_count || 0} />
<BoxSparkleFill className='w-3.5 h-3.5 text-text-tertiary' />
</div>
</div>
</div>
<div className='flex gap-1'>
<OperationDropdown />
<ActionButton onClick={handleClose}>
<RiCloseLine className='w-4 h-4' />
</ActionButton>
</div>
</div>
<Description className='mt-3' text={pluginDetail.brief[locale]} descriptionLineRows={2}></Description>
<div className='mt-3 flex space-x-0.5'>
{['LLM', 'text embedding', 'speech2text'].map(tag => (
<Badge key={tag} text={tag} />
))}
</div>
</div>
</div>
)}

View File

@ -0,0 +1,61 @@
'use client'
import type { FC } from 'react'
import React, { useCallback, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { RiArrowRightUpLine, RiMoreFill } from '@remixicon/react'
import ActionButton from '@/app/components/base/action-button'
// import Button from '@/app/components/base/button'
import {
PortalToFollowElem,
PortalToFollowElemContent,
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import cn from '@/utils/classnames'
type Props = {
}
const OperationDropdown: FC<Props> = () => {
const { t } = useTranslation()
const [open, doSetOpen] = useState(false)
const openRef = useRef(open)
const setOpen = useCallback((v: boolean) => {
doSetOpen(v)
openRef.current = v
}, [doSetOpen])
const handleTrigger = useCallback(() => {
setOpen(!openRef.current)
}, [setOpen])
return (
<PortalToFollowElem
open={open}
onOpenChange={setOpen}
placement='bottom-end'
offset={{
mainAxis: -12,
crossAxis: 36,
}}
>
<PortalToFollowElemTrigger onClick={handleTrigger}>
<ActionButton className={cn(open && 'bg-state-base-hover')}>
<RiMoreFill className='w-4 h-4' />
</ActionButton>
</PortalToFollowElemTrigger>
<PortalToFollowElemContent className='z-50'>
<div className='w-[160px] p-1 bg-components-panel-bg-blur rounded-xl border-[0.5px] border-components-panel-border shadow-lg'>
<div className='px-3 py-1.5 rounded-lg text-text-secondary system-md-regular cursor-pointer hover:bg-state-base-hover'>{t('plugin.detailPanel.operation.info')}</div>
<div className='px-3 py-1.5 rounded-lg text-text-secondary system-md-regular cursor-pointer hover:bg-state-base-hover'>{t('plugin.detailPanel.operation.checkUpdate')}</div>
<div className='flex items-center px-3 py-1.5 rounded-lg text-text-secondary system-md-regular cursor-pointer hover:bg-state-base-hover'>
<div className='grow'>{t('plugin.detailPanel.operation.viewDetail')}</div>
<RiArrowRightUpLine className='shrink-0 w-3.5 h-3.5 text-text-tertiary' />
</div>
<div className='my-1 h-px bg-divider-subtle'></div>
<div className='px-3 py-1.5 rounded-lg text-text-secondary system-md-regular cursor-pointer hover:bg-state-base-hover'>{t('plugin.detailPanel.operation.remove')}</div>
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
)
}
export default React.memo(OperationDropdown)

View File

@ -1,6 +1,15 @@
const translation = {
from: 'From',
endpointsEnabled: '{{num}} sets of endpoints enabled',
detailPanel: {
operation: {
update: 'Update',
info: 'Plugin Info',
checkUpdate: 'Check Update',
viewDetail: 'View Detail',
remove: 'Remove',
},
},
}
export default translation

View File

@ -1,6 +1,15 @@
const translation = {
from: '来自',
endpointsEnabled: '{{num}} 组端点已启用',
detailPanel: {
operation: {
update: '更新',
info: '插件信息',
checkUpdate: '检查更新',
viewDetail: '查看详情',
remove: '移除',
},
},
}
export default translation