Commit 0bd826c4 by t0ng7u

šŸš€ feat(detail): enhance API Info list with jump button & responsive layout

* Added an ā€œJumpā€ (`ExternalLink`) tag to each API entry that opens the URL in a new tab
* Placed ā€œSpeed Testā€ and ā€œJumpā€ tags on the same line as the route
  * Route is left-aligned; tags are right-aligned and wrap to next line when space is insufficient
* Inserted `<Divider />` between API items to improve visual separation
* Tweaked flex gaps and utility classes for consistent spacing and readability
parent 33f4e404
import React, { useContext, useEffect, useRef, useState, useMemo, useCallback } from 'react'; import React, { useContext, useEffect, useRef, useState, useMemo, useCallback } from 'react';
import { initVChartSemiTheme } from '@visactor/vchart-semi-theme'; import { initVChartSemiTheme } from '@visactor/vchart-semi-theme';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { Wallet, Activity, Zap, Gauge, PieChart, Server, Bell, HelpCircle } from 'lucide-react'; import { Wallet, Activity, Zap, Gauge, PieChart, Server, Bell, HelpCircle, ExternalLink } from 'lucide-react';
import { marked } from 'marked'; import { marked } from 'marked';
import { import {
Card, Card,
Form, Form,
Spin, Spin,
IconButton, Button,
Modal, Modal,
Avatar, Avatar,
Tabs, Tabs,
...@@ -614,7 +614,7 @@ const Detail = (props) => { ...@@ -614,7 +614,7 @@ const Detail = (props) => {
const handleSpeedTest = useCallback((apiUrl) => { const handleSpeedTest = useCallback((apiUrl) => {
const encodedUrl = encodeURIComponent(apiUrl); const encodedUrl = encodeURIComponent(apiUrl);
const speedTestUrl = `https://www.tcptest.cn/http/${encodedUrl}`; const speedTestUrl = `https://www.tcptest.cn/http/${encodedUrl}`;
window.open(speedTestUrl, '_blank'); window.open(speedTestUrl, '_blank', 'noopener,noreferrer');
}, []); }, []);
const handleInputChange = useCallback((value, name) => { const handleInputChange = useCallback((value, name) => {
...@@ -1108,12 +1108,14 @@ const Detail = (props) => { ...@@ -1108,12 +1108,14 @@ const Detail = (props) => {
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<h2 className="text-2xl font-semibold text-gray-800">{getGreeting}</h2> <h2 className="text-2xl font-semibold text-gray-800">{getGreeting}</h2>
<div className="flex gap-3"> <div className="flex gap-3">
<IconButton <Button
type='tertiary'
icon={<IconSearch />} icon={<IconSearch />}
onClick={showSearchModal} onClick={showSearchModal}
className={`bg-green-500 hover:bg-green-600 ${ICON_BUTTON_CLASS}`} className={`bg-green-500 hover:bg-green-600 ${ICON_BUTTON_CLASS}`}
/> />
<IconButton <Button
type='tertiary'
icon={<IconRefresh />} icon={<IconRefresh />}
onClick={refresh} onClick={refresh}
loading={loading} loading={loading}
...@@ -1311,40 +1313,57 @@ const Detail = (props) => { ...@@ -1311,40 +1313,57 @@ const Detail = (props) => {
> >
{apiInfoData.length > 0 ? ( {apiInfoData.length > 0 ? (
apiInfoData.map((api) => ( apiInfoData.map((api) => (
<div key={api.id} className="flex p-2 hover:bg-white rounded-lg transition-colors cursor-pointer"> <>
<div className="flex-shrink-0 mr-3"> <div key={api.id} className="flex p-2 hover:bg-white rounded-lg transition-colors cursor-pointer">
<Avatar <div className="flex-shrink-0 mr-3">
size="extra-small" <Avatar
color={api.color} size="extra-small"
> color={api.color}
{api.route.substring(0, 2)}
</Avatar>
</div>
<div className="flex-1">
<div className="text-sm font-medium text-gray-900 mb-1 !font-bold flex items-center gap-2">
<Tag
prefixIcon={<Gauge size={12} />}
size="small"
color="white"
shape='circle'
onClick={() => handleSpeedTest(api.url)}
className="cursor-pointer hover:opacity-80 text-xs"
> >
{t('ęµ‹é€Ÿ')} {api.route.substring(0, 2)}
</Tag> </Avatar>
{api.route}
</div>
<div
className="!text-semi-color-primary break-all cursor-pointer hover:underline mb-1"
onClick={() => handleCopyUrl(api.url)}
>
{api.url}
</div> </div>
<div className="text-gray-500"> <div className="flex-1">
{api.description} <div className="flex flex-wrap items-center justify-between mb-1 w-full gap-2">
<span className="text-sm font-medium text-gray-900 !font-bold break-all">
{api.route}
</span>
<div className="flex items-center gap-1 mt-1 lg:mt-0">
<Tag
prefixIcon={<Gauge size={12} />}
size="small"
color="white"
shape='circle'
onClick={() => handleSpeedTest(api.url)}
className="cursor-pointer hover:opacity-80 text-xs"
>
{t('ęµ‹é€Ÿ')}
</Tag>
<Tag
prefixIcon={<ExternalLink size={12} />}
size="small"
color="white"
shape='circle'
onClick={() => window.open(api.url, '_blank', 'noopener,noreferrer')}
className="cursor-pointer hover:opacity-80 text-xs"
>
{t('跳转')}
</Tag>
</div>
</div>
<div
className="!text-semi-color-primary break-all cursor-pointer hover:underline mb-1"
onClick={() => handleCopyUrl(api.url)}
>
{api.url}
</div>
<div className="text-gray-500">
{api.description}
</div>
</div> </div>
</div> </div>
</div> <Divider />
</>
)) ))
) : ( ) : (
<div className="flex justify-center items-center py-8"> <div className="flex justify-center items-center py-8">
...@@ -1368,274 +1387,277 @@ const Detail = (props) => { ...@@ -1368,274 +1387,277 @@ const Detail = (props) => {
</div> </div>
{/* ē³»ē»Ÿå…¬å‘Šå’Œåøøč§é—®ē­”å”ē‰‡ */} {/* ē³»ē»Ÿå…¬å‘Šå’Œåøøč§é—®ē­”å”ē‰‡ */}
{hasInfoPanels && ( {
<div className="mb-4"> hasInfoPanels && (
<div className="grid grid-cols-1 lg:grid-cols-4 gap-4"> <div className="mb-4">
{/* å…¬å‘Šå”ē‰‡ */} <div className="grid grid-cols-1 lg:grid-cols-4 gap-4">
{announcementsEnabled && ( {/* å…¬å‘Šå”ē‰‡ */}
<Card {announcementsEnabled && (
{...CARD_PROPS} <Card
className="shadow-sm !rounded-2xl lg:col-span-2" {...CARD_PROPS}
title={ className="shadow-sm !rounded-2xl lg:col-span-2"
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-2 w-full"> title={
<div className="flex items-center gap-2"> <div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-2 w-full">
<Bell size={16} /> <div className="flex items-center gap-2">
{t('ē³»ē»Ÿå…¬å‘Š')} <Bell size={16} />
<Tag color="grey" shape="circle"> {t('ē³»ē»Ÿå…¬å‘Š')}
{t('ę˜¾ē¤ŗęœ€ę–°20ę”')} <Tag color="grey" shape="circle">
</Tag> {t('ę˜¾ē¤ŗęœ€ę–°20ę”')}
</Tag>
</div>
{/* 图例 */}
<div className="flex flex-wrap gap-3 text-xs">
{announcementLegendData.map((legend, index) => (
<div key={index} className="flex items-center gap-1">
<div
className="w-2 h-2 rounded-full"
style={{
backgroundColor: legend.color === 'grey' ? '#8b9aa7' :
legend.color === 'blue' ? '#3b82f6' :
legend.color === 'green' ? '#10b981' :
legend.color === 'orange' ? '#f59e0b' :
legend.color === 'red' ? '#ef4444' : '#8b9aa7'
}}
/>
<span className="text-gray-600">{legend.label}</span>
</div>
))}
</div>
</div> </div>
{/* 图例 */} }
<div className="flex flex-wrap gap-3 text-xs"> >
{announcementLegendData.map((legend, index) => ( <div className="card-content-container">
<div key={index} className="flex items-center gap-1"> <div
<div ref={announcementScrollRef}
className="w-2 h-2 rounded-full" className="p-2 max-h-96 overflow-y-auto card-content-scroll"
style={{ onScroll={() => handleCardScroll(announcementScrollRef, setShowAnnouncementScrollHint)}
backgroundColor: legend.color === 'grey' ? '#8b9aa7' : >
legend.color === 'blue' ? '#3b82f6' : {announcementData.length > 0 ? (
legend.color === 'green' ? '#10b981' : <Timeline mode="alternate">
legend.color === 'orange' ? '#f59e0b' : {announcementData.map((item, idx) => (
legend.color === 'red' ? '#ef4444' : '#8b9aa7' <Timeline.Item
}} key={idx}
type={item.type || 'default'}
time={item.time}
>
<div>
<div
dangerouslySetInnerHTML={{ __html: marked.parse(item.content || '') }}
/>
{item.extra && (
<div
className="text-xs text-gray-500"
dangerouslySetInnerHTML={{ __html: marked.parse(item.extra) }}
/>
)}
</div>
</Timeline.Item>
))}
</Timeline>
) : (
<div className="flex justify-center items-center py-8">
<Empty
image={<IllustrationConstruction style={ILLUSTRATION_SIZE} />}
darkModeImage={<IllustrationConstructionDark style={ILLUSTRATION_SIZE} />}
title={t('ęš‚ę— ē³»ē»Ÿå…¬å‘Š')}
description={t('čÆ·č”ē³»ē®”ē†å‘˜åœØē³»ē»Ÿč®¾ē½®äø­é…ē½®å…¬å‘Šäæ”ęÆ')}
/> />
<span className="text-gray-600">{legend.label}</span>
</div> </div>
))} )}
</div> </div>
<div
className="card-content-fade-indicator"
style={{ opacity: showAnnouncementScrollHint ? 1 : 0 }}
/>
</div> </div>
} </Card>
> )}
<div className="card-content-container">
<div {/* åøøč§é—®ē­”å”ē‰‡ */}
ref={announcementScrollRef} {faqEnabled && (
className="p-2 max-h-96 overflow-y-auto card-content-scroll" <Card
onScroll={() => handleCardScroll(announcementScrollRef, setShowAnnouncementScrollHint)} {...CARD_PROPS}
> className="shadow-sm !rounded-2xl lg:col-span-1"
{announcementData.length > 0 ? ( title={
<Timeline mode="alternate"> <div className={FLEX_CENTER_GAP2}>
{announcementData.map((item, idx) => ( <HelpCircle size={16} />
<Timeline.Item {t('常见问答')}
key={idx} </div>
type={item.type || 'default'} }
time={item.time} bodyStyle={{ padding: 0 }}
> >
<div> <div className="card-content-container">
<div
ref={faqScrollRef}
className="p-2 max-h-96 overflow-y-auto card-content-scroll"
onScroll={() => handleCardScroll(faqScrollRef, setShowFaqScrollHint)}
>
{faqData.length > 0 ? (
<Collapse
accordion
expandIcon={<IconPlus />}
collapseIcon={<IconMinus />}
>
{faqData.map((item, index) => (
<Collapse.Panel
key={index}
header={item.question}
itemKey={index.toString()}
>
<div <div
dangerouslySetInnerHTML={{ __html: marked.parse(item.content || '') }} dangerouslySetInnerHTML={{ __html: marked.parse(item.answer || '') }}
/> />
{item.extra && ( </Collapse.Panel>
<div ))}
className="text-xs text-gray-500" </Collapse>
dangerouslySetInnerHTML={{ __html: marked.parse(item.extra) }} ) : (
/> <div className="flex justify-center items-center py-8">
)} <Empty
</div> image={<IllustrationConstruction style={ILLUSTRATION_SIZE} />}
</Timeline.Item> darkModeImage={<IllustrationConstructionDark style={ILLUSTRATION_SIZE} />}
))} title={t('ęš‚ę— åøøč§é—®ē­”')}
</Timeline> description={t('čÆ·č”ē³»ē®”ē†å‘˜åœØē³»ē»Ÿč®¾ē½®äø­é…ē½®åøøč§é—®ē­”')}
) : ( />
<div className="flex justify-center items-center py-8"> </div>
<Empty )}
image={<IllustrationConstruction style={ILLUSTRATION_SIZE} />}
darkModeImage={<IllustrationConstructionDark style={ILLUSTRATION_SIZE} />}
title={t('ęš‚ę— ē³»ē»Ÿå…¬å‘Š')}
description={t('čÆ·č”ē³»ē®”ē†å‘˜åœØē³»ē»Ÿč®¾ē½®äø­é…ē½®å…¬å‘Šäæ”ęÆ')}
/>
</div>
)}
</div>
<div
className="card-content-fade-indicator"
style={{ opacity: showAnnouncementScrollHint ? 1 : 0 }}
/>
</div>
</Card>
)}
{/* åøøč§é—®ē­”å”ē‰‡ */}
{faqEnabled && (
<Card
{...CARD_PROPS}
className="shadow-sm !rounded-2xl lg:col-span-1"
title={
<div className={FLEX_CENTER_GAP2}>
<HelpCircle size={16} />
{t('常见问答')}
</div>
}
bodyStyle={{ padding: 0 }}
>
<div className="card-content-container">
<div
ref={faqScrollRef}
className="p-2 max-h-96 overflow-y-auto card-content-scroll"
onScroll={() => handleCardScroll(faqScrollRef, setShowFaqScrollHint)}
>
{faqData.length > 0 ? (
<Collapse
accordion
expandIcon={<IconPlus />}
collapseIcon={<IconMinus />}
>
{faqData.map((item, index) => (
<Collapse.Panel
key={index}
header={item.question}
itemKey={index.toString()}
>
<div
dangerouslySetInnerHTML={{ __html: marked.parse(item.answer || '') }}
/>
</Collapse.Panel>
))}
</Collapse>
) : (
<div className="flex justify-center items-center py-8">
<Empty
image={<IllustrationConstruction style={ILLUSTRATION_SIZE} />}
darkModeImage={<IllustrationConstructionDark style={ILLUSTRATION_SIZE} />}
title={t('ęš‚ę— åøøč§é—®ē­”')}
description={t('čÆ·č”ē³»ē®”ē†å‘˜åœØē³»ē»Ÿč®¾ē½®äø­é…ē½®åøøč§é—®ē­”')}
/>
</div>
)}
</div>
<div
className="card-content-fade-indicator"
style={{ opacity: showFaqScrollHint ? 1 : 0 }}
/>
</div>
</Card>
)}
{/* ęœåŠ”åÆē”Øę€§å”ē‰‡ */}
{uptimeEnabled && (
<Card
{...CARD_PROPS}
className="shadow-sm !rounded-2xl lg:col-span-1 flex flex-col"
title={
<div className="flex items-center justify-between w-full gap-2">
<div className="flex items-center gap-2">
<Gauge size={16} />
{t('ęœåŠ”åÆē”Øę€§')}
</div> </div>
<IconButton <div
icon={<IconRefresh />} className="card-content-fade-indicator"
onClick={loadUptimeData} style={{ opacity: showFaqScrollHint ? 1 : 0 }}
loading={uptimeLoading}
size="small"
theme="borderless"
className="text-gray-500 hover:text-blue-500 hover:bg-blue-50 !rounded-full"
/> />
</div> </div>
} </Card>
bodyStyle={{ padding: 0 }} )}
>
{/* å†…å®¹åŒŗåŸŸ */} {/* ęœåŠ”åÆē”Øę€§å”ē‰‡ */}
<div className="flex-1 relative"> {uptimeEnabled && (
<Spin spinning={uptimeLoading}> <Card
{uptimeData.length > 0 ? ( {...CARD_PROPS}
uptimeData.length === 1 ? ( className="shadow-sm !rounded-2xl lg:col-span-1 flex flex-col"
<div className="card-content-container"> title={
<div <div className="flex items-center justify-between w-full gap-2">
ref={uptimeScrollRef} <div className="flex items-center gap-2">
className="p-2 max-h-[24rem] overflow-y-auto card-content-scroll" <Gauge size={16} />
onScroll={() => handleCardScroll(uptimeScrollRef, setShowUptimeScrollHint)} {t('ęœåŠ”åÆē”Øę€§')}
> </div>
{renderMonitorList(uptimeData[0].monitors)} <Button
icon={<IconRefresh />}
onClick={loadUptimeData}
loading={uptimeLoading}
size="small"
theme="borderless"
type='tertiary'
className="text-gray-500 hover:text-blue-500 hover:bg-blue-50 !rounded-full"
/>
</div>
}
bodyStyle={{ padding: 0 }}
>
{/* å†…å®¹åŒŗåŸŸ */}
<div className="flex-1 relative">
<Spin spinning={uptimeLoading}>
{uptimeData.length > 0 ? (
uptimeData.length === 1 ? (
<div className="card-content-container">
<div
ref={uptimeScrollRef}
className="p-2 max-h-[24rem] overflow-y-auto card-content-scroll"
onScroll={() => handleCardScroll(uptimeScrollRef, setShowUptimeScrollHint)}
>
{renderMonitorList(uptimeData[0].monitors)}
</div>
<div
className="card-content-fade-indicator"
style={{ opacity: showUptimeScrollHint ? 1 : 0 }}
/>
</div> </div>
<div ) : (
className="card-content-fade-indicator" <Tabs
style={{ opacity: showUptimeScrollHint ? 1 : 0 }} type="card"
/> collapsible
</div> activeKey={activeUptimeTab}
) : ( onChange={setActiveUptimeTab}
<Tabs size="small"
type="card" >
collapsible {uptimeData.map((group, groupIdx) => {
activeKey={activeUptimeTab} if (!uptimeTabScrollRefs.current[group.categoryName]) {
onChange={setActiveUptimeTab} uptimeTabScrollRefs.current[group.categoryName] = React.createRef();
size="small" }
> const tabScrollRef = uptimeTabScrollRefs.current[group.categoryName];
{uptimeData.map((group, groupIdx) => {
if (!uptimeTabScrollRefs.current[group.categoryName]) { return (
uptimeTabScrollRefs.current[group.categoryName] = React.createRef(); <TabPane
} tab={
const tabScrollRef = uptimeTabScrollRefs.current[group.categoryName]; <span className="flex items-center gap-2">
<Gauge size={14} />
return ( {group.categoryName}
<TabPane <Tag
tab={ color={activeUptimeTab === group.categoryName ? 'red' : 'grey'}
<span className="flex items-center gap-2"> size='small'
<Gauge size={14} /> shape='circle'
{group.categoryName} >
<Tag {group.monitors ? group.monitors.length : 0}
color={activeUptimeTab === group.categoryName ? 'red' : 'grey'} </Tag>
size='small' </span>
shape='circle' }
itemKey={group.categoryName}
key={groupIdx}
>
<div className="card-content-container">
<div
ref={tabScrollRef}
className="p-2 max-h-[21.5rem] overflow-y-auto card-content-scroll"
onScroll={() => handleCardScroll(tabScrollRef, setShowUptimeScrollHint)}
> >
{group.monitors ? group.monitors.length : 0} {renderMonitorList(group.monitors)}
</Tag> </div>
</span> <div
} className="card-content-fade-indicator"
itemKey={group.categoryName} style={{ opacity: activeUptimeTab === group.categoryName ? showUptimeScrollHint ? 1 : 0 : 0 }}
key={groupIdx} />
>
<div className="card-content-container">
<div
ref={tabScrollRef}
className="p-2 max-h-[21.5rem] overflow-y-auto card-content-scroll"
onScroll={() => handleCardScroll(tabScrollRef, setShowUptimeScrollHint)}
>
{renderMonitorList(group.monitors)}
</div> </div>
<div </TabPane>
className="card-content-fade-indicator" );
style={{ opacity: activeUptimeTab === group.categoryName ? showUptimeScrollHint ? 1 : 0 : 0 }} })}
/> </Tabs>
</div> )
</TabPane> ) : (
); <div className="flex justify-center items-center py-8">
})} <Empty
</Tabs> image={<IllustrationConstruction style={ILLUSTRATION_SIZE} />}
) darkModeImage={<IllustrationConstructionDark style={ILLUSTRATION_SIZE} />}
) : ( title={t('ęš‚ę— ē›‘ęŽ§ę•°ę®')}
<div className="flex justify-center items-center py-8"> description={t('čÆ·č”ē³»ē®”ē†å‘˜åœØē³»ē»Ÿč®¾ē½®äø­é…ē½®Uptime')}
<Empty
image={<IllustrationConstruction style={ILLUSTRATION_SIZE} />}
darkModeImage={<IllustrationConstructionDark style={ILLUSTRATION_SIZE} />}
title={t('ęš‚ę— ē›‘ęŽ§ę•°ę®')}
description={t('čÆ·č”ē³»ē®”ē†å‘˜åœØē³»ē»Ÿč®¾ē½®äø­é…ē½®Uptime')}
/>
</div>
)}
</Spin>
</div>
{/* å›ŗå®šåœØåŗ•éƒØēš„å›¾ä¾‹ */}
{uptimeData.length > 0 && (
<div className="p-3 mt-auto bg-gray-50 rounded-b-2xl">
<div className="flex flex-wrap gap-3 text-xs justify-center">
{uptimeLegendData.map((legend, index) => (
<div key={index} className="flex items-center gap-1">
<div
className="w-2 h-2 rounded-full"
style={{ backgroundColor: legend.color }}
/> />
<span className="text-gray-600">{legend.label}</span>
</div> </div>
))} )}
</div> </Spin>
</div> </div>
)}
</Card> {/* å›ŗå®šåœØåŗ•éƒØēš„å›¾ä¾‹ */}
)} {uptimeData.length > 0 && (
<div className="p-3 mt-auto bg-gray-50 rounded-b-2xl">
<div className="flex flex-wrap gap-3 text-xs justify-center">
{uptimeLegendData.map((legend, index) => (
<div key={index} className="flex items-center gap-1">
<div
className="w-2 h-2 rounded-full"
style={{ backgroundColor: legend.color }}
/>
<span className="text-gray-600">{legend.label}</span>
</div>
))}
</div>
</div>
)}
</Card>
)}
</div>
</div> </div>
</div> )
)} }
</Spin> </Spin >
</div> </div >
); );
}; };
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment