feat(homepage): enhance banner visuals & UX

• Added read-only Base URL input that shows `status.server_address` (fallback `window.location.origin`) and copies value on click.
• Embedded `ScrollList` as input `suffix`; auto-cycles common endpoints every 3 s and allows manual selection.
• Introduced `API_ENDPOINTS` array in `web/src/constants/common.constant.js` for centralized endpoint management.
• Implemented custom CSS to hide ScrollList wheel indicators / scrollbars for a cleaner look.
• Created two blurred colour spheres behind the banner (`blur-ball-indigo`, `blur-ball-teal`) with light-/dark-mode opacity tweaks and lower vertical placement.
• Increased letter-spacing for Chinese heading via conditional `tracking-wide` / `md:tracking-wider` classes to improve readability.
• Misc: updated imports, helper functions, and responsive sizes to keep UI consistent across devices.
This commit is contained in:
t0ng7u
2025-06-25 15:26:51 +08:00
parent 64782027c4
commit 29a44eb7ae
25 changed files with 1076 additions and 957 deletions

View File

@@ -105,7 +105,7 @@ const About = () => {
);
return (
<>
<div className="mt-[64px]">
{aboutLoaded && about === '' ? (
<div className="flex justify-center items-center h-screen p-8">
<Empty
@@ -132,7 +132,7 @@ const About = () => {
)}
</>
)}
</>
</div>
);
};

View File

@@ -3,9 +3,9 @@ import ChannelsTable from '../../components/table/ChannelsTable';
const File = () => {
return (
<>
<div className="mt-[64px]">
<ChannelsTable />
</>
</div>
);
};

View File

@@ -37,12 +37,12 @@ const ChatPage = () => {
return !isLoading && iframeSrc ? (
<iframe
src={iframeSrc}
style={{ width: '100%', height: '100%', border: 'none' }}
style={{ width: '100%', height: 'calc(100vh - 64px)', border: 'none', marginTop: '64px' }}
title='Token Frame'
allow='camera;microphone'
/>
) : (
<div className="fixed inset-0 w-screen h-screen flex items-center justify-center bg-white/80 z-[1000]">
<div className="fixed inset-0 w-screen h-screen flex items-center justify-center bg-white/80 z-[1000] mt-[64px]">
<div className="flex flex-col items-center">
<Spin
size="large"

View File

@@ -17,7 +17,7 @@ const chat2page = () => {
}
return (
<div>
<div className="mt-[64px]">
<h3>正在加载请稍候...</h3>
</div>
);

View File

@@ -984,7 +984,7 @@ const Detail = (props) => {
}, []);
return (
<div className="bg-gray-50 h-full">
<div className="bg-gray-50 h-full mt-[64px]">
<div className="flex items-center justify-between mb-4">
<h2 className="text-2xl font-semibold text-gray-800">{getGreeting}</h2>
<div className="flex gap-3">

View File

@@ -1,10 +1,11 @@
import React, { useContext, useEffect, useState } from 'react';
import { Button, Typography, Tag } from '@douyinfe/semi-ui';
import { API, showError, isMobile } from '../../helpers';
import { Button, Typography, Tag, Input, ScrollList, ScrollItem } from '@douyinfe/semi-ui';
import { API, showError, isMobile, copy, showSuccess } from '../../helpers';
import { API_ENDPOINTS } from '../../constants/common.constant';
import { StatusContext } from '../../context/Status';
import { marked } from 'marked';
import { useTranslation } from 'react-i18next';
import { IconGithubLogo, IconPlay, IconFile } from '@douyinfe/semi-icons';
import { IconGithubLogo, IconPlay, IconFile, IconCopy } from '@douyinfe/semi-icons';
import { Link } from 'react-router-dom';
import NoticeModal from '../../components/layout/NoticeModal';
import { Moonshot, OpenAI, XAI, Zhipu, Volcengine, Cohere, Claude, Gemini, Suno, Minimax, Wenxin, Spark, Qingyan, DeepSeek, Qwen, Midjourney, Grok, AzureAI, Hunyuan, Xinference } from '@lobehub/icons';
@@ -17,29 +18,12 @@ const Home = () => {
const [homePageContentLoaded, setHomePageContentLoaded] = useState(false);
const [homePageContent, setHomePageContent] = useState('');
const [noticeVisible, setNoticeVisible] = useState(false);
const isDemoSiteMode = statusState?.status?.demo_site_enabled || false;
const docsLink = statusState?.status?.docs_link || '';
useEffect(() => {
const checkNoticeAndShow = async () => {
const lastCloseDate = localStorage.getItem('notice_close_date');
const today = new Date().toDateString();
if (lastCloseDate !== today) {
try {
const res = await API.get('/api/notice');
const { success, data } = res.data;
if (success && data && data.trim() !== '') {
setNoticeVisible(true);
}
} catch (error) {
console.error('获取公告失败:', error);
}
}
};
checkNoticeAndShow();
}, []);
const serverAddress = statusState?.status?.server_address || window.location.origin;
const endpointItems = API_ENDPOINTS.map((e) => ({ value: e }));
const [endpointIndex, setEndpointIndex] = useState(0);
const isChinese = i18n.language.startsWith('zh');
const displayHomePageContent = async () => {
setHomePageContent(localStorage.getItem('home_page_content') || '');
@@ -71,10 +55,44 @@ const Home = () => {
setHomePageContentLoaded(true);
};
const handleCopyBaseURL = async () => {
const ok = await copy(serverAddress);
if (ok) {
showSuccess(t('已复制到剪切板'));
}
};
useEffect(() => {
const checkNoticeAndShow = async () => {
const lastCloseDate = localStorage.getItem('notice_close_date');
const today = new Date().toDateString();
if (lastCloseDate !== today) {
try {
const res = await API.get('/api/notice');
const { success, data } = res.data;
if (success && data && data.trim() !== '') {
setNoticeVisible(true);
}
} catch (error) {
console.error('获取公告失败:', error);
}
}
};
checkNoticeAndShow();
}, []);
useEffect(() => {
displayHomePageContent().then();
}, []);
useEffect(() => {
const timer = setInterval(() => {
setEndpointIndex((prev) => (prev + 1) % endpointItems.length);
}, 3000);
return () => clearInterval(timer);
}, [endpointItems.length]);
return (
<div className="w-full overflow-x-hidden">
<NoticeModal
@@ -86,30 +104,63 @@ const Home = () => {
<div className="w-full overflow-x-hidden">
{/* Banner 部分 */}
<div className="w-full border-b border-semi-color-border min-h-[500px] md:min-h-[600px] lg:min-h-[700px] relative overflow-x-hidden">
<div className="flex items-center justify-center h-full px-4 py-20 md:py-24 lg:py-32">
{/* 背景模糊晕染球*/}
<div className="blur-ball blur-ball-indigo" />
<div className="blur-ball blur-ball-teal" />
<div className="flex items-center justify-center h-full px-4 py-20 md:py-24 lg:py-32 mt-10">
{/* 居中内容区 */}
<div className="flex flex-col items-center justify-center text-center max-w-4xl mx-auto">
<div className="flex flex-col items-center justify-center mb-6 md:mb-8">
<h1 className="text-4xl md:text-5xl lg:text-6xl xl:text-7xl font-bold text-semi-color-text-0 leading-tight">
<h1 className={`text-4xl md:text-5xl lg:text-6xl xl:text-7xl font-bold text-semi-color-text-0 leading-tight ${isChinese ? 'tracking-wide md:tracking-wider' : ''}`}>
{i18n.language === 'en' ? (
<>
The Unified<br />
LLMs API Gateway
<span className="shine-text">LLMs API Gateway</span>
</>
) : (
t('统一的大模型接口网关')
<>
统一的<br />
<span className="shine-text">大模型接口网关</span>
</>
)}
</h1>
<p className="text-lg md:text-xl lg:text-2xl text-semi-color-text-1 mt-4 md:mt-6">
{t('更好的价格,更好的稳定性,无需订阅')}
<p className="text-base md:text-lg lg:text-xl text-semi-color-text-1 mt-4 md:mt-6 max-w-xl">
{t('更好的价格,更好的稳定性,只需要将模型基址替换为:')}
</p>
{/* BASE URL 与端点选择 */}
<div className="flex flex-col md:flex-row items-center justify-center gap-4 w-full mt-4 md:mt-6 max-w-md">
<Input
readOnly
value={serverAddress}
className="flex-1 !rounded-full"
size={isMobile() ? 'default' : 'large'}
suffix={
<div className="flex items-center gap-2">
<ScrollList bodyHeight={32} style={{ border: 'unset', boxShadow: 'unset' }}>
<ScrollItem
mode="wheel"
cycled={true}
list={endpointItems}
selectedIndex={endpointIndex}
onSelect={({ index }) => setEndpointIndex(index)}
/>
</ScrollList>
<Button
type="primary"
onClick={handleCopyBaseURL}
icon={<IconCopy />}
/>
</div>
}
/>
</div>
</div>
{/* 操作按钮 */}
<div className="flex flex-row gap-4 justify-center items-center">
<Link to="/console">
<Button theme="solid" type="primary" size={isMobile() ? "default" : "large"} className="!rounded-3xl px-8 py-2" icon={<IconPlay />}>
{t('开始使用')}
{t('获取密钥')}
</Button>
</Link>
{isDemoSiteMode && statusState?.status?.version ? (

View File

@@ -2,9 +2,9 @@ import React from 'react';
import LogsTable from '../../components/table/LogsTable';
const Token = () => (
<>
<div className="mt-[64px]">
<LogsTable />
</>
</div>
);
export default Token;

View File

@@ -2,9 +2,9 @@ import React from 'react';
import MjLogsTable from '../../components/table/MjLogsTable';
const Midjourney = () => (
<>
<div className="mt-[64px]">
<MjLogsTable />
</>
</div>
);
export default Midjourney;

View File

@@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next';
const NotFound = () => {
const { t } = useTranslation();
return (
<div className="flex justify-center items-center h-screen p-8">
<div className="flex justify-center items-center h-screen p-8 mt-[64px]">
<Empty
image={<IllustrationNotFound style={{ width: 250, height: 250 }} />}
darkModeImage={<IllustrationNotFoundDark style={{ width: 250, height: 250 }} />}

View File

@@ -363,7 +363,7 @@ const Playground = () => {
}, [setMessage, saveMessagesImmediately]);
return (
<div className="h-full bg-gray-50">
<div className="h-full bg-gray-50 mt-[64px]">
<Layout style={{ height: '100%', background: 'transparent' }} className="flex flex-col md:flex-row">
{(showSettings || !styleState.isMobile) && (
<Layout.Sider

View File

@@ -2,9 +2,9 @@ import React from 'react';
import ModelPricing from '../../components/table/ModelPricing.js';
const Pricing = () => (
<>
<div className="mt-[64px]">
<ModelPricing />
</>
</div>
);
export default Pricing;

View File

@@ -3,9 +3,9 @@ import RedemptionsTable from '../../components/table/RedemptionsTable';
const Redemption = () => {
return (
<>
<div className="mt-[64px]">
<RedemptionsTable />
</>
</div>
);
};

View File

@@ -150,7 +150,7 @@ const Setting = () => {
}
}, [location.search]);
return (
<div>
<div className="mt-[64px]">
<Layout>
<Layout.Content>
<Tabs

View File

@@ -133,7 +133,7 @@ const Setup = () => {
};
return (
<div className="bg-gray-50">
<div className="bg-gray-50 mt-[64px]">
<Layout>
<Layout.Content>
<div className="flex justify-center px-4 py-8">

View File

@@ -2,9 +2,9 @@ import React from 'react';
import TaskLogsTable from '../../components/table/TaskLogsTable.js';
const Task = () => (
<>
<div className="mt-[64px]">
<TaskLogsTable />
</>
</div>
);
export default Task;

View File

@@ -3,9 +3,9 @@ import TokensTable from '../../components/table/TokensTable';
const Token = () => {
return (
<>
<div className="mt-[64px]">
<TokensTable />
</>
</div>
);
};

View File

@@ -382,7 +382,7 @@ const TopUp = () => {
};
return (
<div className='mx-auto relative min-h-screen lg:min-h-0'>
<div className='mx-auto relative min-h-screen lg:min-h-0 mt-[64px]'>
{/* 划转模态框 */}
<Modal
title={

View File

@@ -3,9 +3,9 @@ import UsersTable from '../../components/table/UsersTable';
const User = () => {
return (
<>
<div className="mt-[64px]">
<UsersTable />
</>
</div>
);
};