Commit 0ff9c35e by 同語 Committed by GitHub

feat(web): support classic Rsbuild dev and build

Merge pull request #5232 from QuantumNous/feat/classic-rsbuild-dev-workflow
parents 9a2e60df 0bbcaa89
...@@ -33,16 +33,18 @@ jobs: ...@@ -33,16 +33,18 @@ jobs:
env: env:
CI: "" CI: ""
run: | run: |
cd web/default cd web
bun install bun install --frozen-lockfile
cd default
DISABLE_ESLINT_PLUGIN='true' VITE_REACT_APP_VERSION=$VERSION bun run build DISABLE_ESLINT_PLUGIN='true' VITE_REACT_APP_VERSION=$VERSION bun run build
cd ../.. cd ../..
- name: Build Frontend (classic) - name: Build Frontend (classic)
env: env:
CI: "" CI: ""
run: | run: |
cd web/classic cd web
bun install bun install --frozen-lockfile
cd classic
VITE_REACT_APP_VERSION=$VERSION bun run build VITE_REACT_APP_VERSION=$VERSION bun run build
cd ../.. cd ../..
- name: Set up Go - name: Set up Go
...@@ -91,16 +93,18 @@ jobs: ...@@ -91,16 +93,18 @@ jobs:
CI: "" CI: ""
NODE_OPTIONS: "--max-old-space-size=4096" NODE_OPTIONS: "--max-old-space-size=4096"
run: | run: |
cd web/default cd web
bun install bun install --frozen-lockfile
cd default
DISABLE_ESLINT_PLUGIN='true' VITE_REACT_APP_VERSION=$VERSION bun run build DISABLE_ESLINT_PLUGIN='true' VITE_REACT_APP_VERSION=$VERSION bun run build
cd ../.. cd ../..
- name: Build Frontend (classic) - name: Build Frontend (classic)
env: env:
CI: "" CI: ""
run: | run: |
cd web/classic cd web
bun install bun install --frozen-lockfile
cd classic
VITE_REACT_APP_VERSION=$VERSION bun run build VITE_REACT_APP_VERSION=$VERSION bun run build
cd ../.. cd ../..
- name: Set up Go - name: Set up Go
...@@ -146,16 +150,18 @@ jobs: ...@@ -146,16 +150,18 @@ jobs:
env: env:
CI: "" CI: ""
run: | run: |
cd web/default cd web
bun install bun install --frozen-lockfile
cd default
DISABLE_ESLINT_PLUGIN='true' VITE_REACT_APP_VERSION=$VERSION bun run build DISABLE_ESLINT_PLUGIN='true' VITE_REACT_APP_VERSION=$VERSION bun run build
cd ../.. cd ../..
- name: Build Frontend (classic) - name: Build Frontend (classic)
env: env:
CI: "" CI: ""
run: | run: |
cd web/classic cd web
bun install bun install --frozen-lockfile
cd classic
VITE_REACT_APP_VERSION=$VERSION bun run build VITE_REACT_APP_VERSION=$VERSION bun run build
cd ../.. cd ../..
- name: Set up Go - name: Set up Go
......
FROM oven/bun:1@sha256:0733e50325078969732ebe3b15ce4c4be5082f18c4ac1a0f0ca4839c2e4e42a7 AS builder FROM oven/bun:1@sha256:0733e50325078969732ebe3b15ce4c4be5082f18c4ac1a0f0ca4839c2e4e42a7 AS builder
WORKDIR /build WORKDIR /build/web
COPY web/default/package.json . COPY web/package.json web/bun.lock ./
COPY web/default/bun.lock . COPY web/default/package.json ./default/package.json
RUN bun install COPY web/classic/package.json ./classic/package.json
COPY ./web/default . RUN bun install --frozen-lockfile
COPY ./VERSION . COPY ./web/default ./default
RUN DISABLE_ESLINT_PLUGIN='true' VITE_REACT_APP_VERSION=$(cat VERSION) bun run build COPY ./VERSION /build/VERSION
RUN cd default && DISABLE_ESLINT_PLUGIN='true' VITE_REACT_APP_VERSION=$(cat /build/VERSION) bun run build
FROM oven/bun:1@sha256:0733e50325078969732ebe3b15ce4c4be5082f18c4ac1a0f0ca4839c2e4e42a7 AS builder-classic FROM oven/bun:1@sha256:0733e50325078969732ebe3b15ce4c4be5082f18c4ac1a0f0ca4839c2e4e42a7 AS builder-classic
WORKDIR /build WORKDIR /build/web
COPY web/classic/package.json . COPY web/package.json web/bun.lock ./
COPY web/classic/bun.lock . COPY web/default/package.json ./default/package.json
RUN bun install COPY web/classic/package.json ./classic/package.json
COPY ./web/classic . RUN bun install --frozen-lockfile
COPY ./VERSION . COPY ./web/classic ./classic
RUN VITE_REACT_APP_VERSION=$(cat VERSION) bun run build COPY ./VERSION /build/VERSION
RUN cd classic && VITE_REACT_APP_VERSION=$(cat /build/VERSION) bun run build
FROM golang:1.26.1-alpine@sha256:2389ebfa5b7f43eeafbd6be0c3700cc46690ef842ad962f6c5bd6be49ed82039 AS builder2 FROM golang:1.26.1-alpine@sha256:2389ebfa5b7f43eeafbd6be0c3700cc46690ef842ad962f6c5bd6be49ed82039 AS builder2
ENV GO111MODULE=on CGO_ENABLED=0 ENV GO111MODULE=on CGO_ENABLED=0
...@@ -32,8 +34,8 @@ ADD go.mod go.sum ./ ...@@ -32,8 +34,8 @@ ADD go.mod go.sum ./
RUN go mod download RUN go mod download
COPY . . COPY . .
COPY --from=builder /build/dist ./web/default/dist COPY --from=builder /build/web/default/dist ./web/default/dist
COPY --from=builder-classic /build/dist ./web/classic/dist COPY --from=builder-classic /build/web/classic/dist ./web/classic/dist
RUN go build -ldflags "-s -w -X 'github.com/QuantumNous/new-api/common.Version=$(cat VERSION)'" -o new-api RUN go build -ldflags "-s -w -X 'github.com/QuantumNous/new-api/common.Version=$(cat VERSION)'" -o new-api
FROM debian:bookworm-slim@sha256:f06537653ac770703bc45b4b113475bd402f451e85223f0f2837acbf89ab020a FROM debian:bookworm-slim@sha256:f06537653ac770703bc45b4b113475bd402f451e85223f0f2837acbf89ab020a
......
FRONTEND_DIR = ./web/default FRONTEND_DIR = ./web/default
FRONTEND_CLASSIC_DIR = ./web/classic FRONTEND_CLASSIC_DIR = ./web/classic
BACKEND_DIR = . BACKEND_DIR = .
DEV_FRONTEND_DEFAULT_PORT ?= 5173
DEV_FRONTEND_CLASSIC_PORT ?= 5174
DEV_COMPOSE_FILE = docker-compose.dev.yml DEV_COMPOSE_FILE = docker-compose.dev.yml
DEV_POSTGRES_SERVICE = postgres DEV_POSTGRES_SERVICE = postgres
DEV_BACKEND_SERVICE = new-api DEV_BACKEND_SERVICE = new-api
...@@ -14,11 +16,13 @@ all: build-all-frontends start-backend ...@@ -14,11 +16,13 @@ all: build-all-frontends start-backend
build-frontend: build-frontend:
@echo "Building default frontend..." @echo "Building default frontend..."
@cd $(FRONTEND_DIR) && bun install && DISABLE_ESLINT_PLUGIN='true' VITE_REACT_APP_VERSION=$(cat ../../VERSION) bun run build @cd ./web && bun install --frozen-lockfile
@cd $(FRONTEND_DIR) && DISABLE_ESLINT_PLUGIN='true' VITE_REACT_APP_VERSION=$(cat ../../VERSION) bun run build
build-frontend-classic: build-frontend-classic:
@echo "Building classic frontend..." @echo "Building classic frontend..."
@cd $(FRONTEND_CLASSIC_DIR) && bun install && VITE_REACT_APP_VERSION=$(cat ../../VERSION) bun run build @cd ./web && bun install --frozen-lockfile
@cd $(FRONTEND_CLASSIC_DIR) && VITE_REACT_APP_VERSION=$(cat ../../VERSION) bun run build
build-all-frontends: build-frontend build-frontend-classic build-all-frontends: build-frontend build-frontend-classic
...@@ -35,12 +39,35 @@ dev-api-rebuild: ...@@ -35,12 +39,35 @@ dev-api-rebuild:
@docker compose -f $(DEV_COMPOSE_FILE) up -d --build $(DEV_BACKEND_SERVICE) @docker compose -f $(DEV_COMPOSE_FILE) up -d --build $(DEV_BACKEND_SERVICE)
dev-web: dev-web:
@echo "Starting frontend dev server..." @echo "Starting both frontend dev servers..."
@cd $(FRONTEND_DIR) && bun install && bun run dev @echo "Default frontend: http://localhost:$(DEV_FRONTEND_DEFAULT_PORT)"
@echo "Classic frontend: http://localhost:$(DEV_FRONTEND_CLASSIC_PORT)"
@cd ./web && bun install
@(cd $(FRONTEND_DIR) && bun run dev -- --host 0.0.0.0 --port $(DEV_FRONTEND_DEFAULT_PORT)) & \
default_pid=$$!; \
(cd $(FRONTEND_CLASSIC_DIR) && bun run dev -- --host 0.0.0.0 --port $(DEV_FRONTEND_CLASSIC_PORT)) & \
classic_pid=$$!; \
trap 'kill $$default_pid $$classic_pid 2>/dev/null; wait $$default_pid $$classic_pid 2>/dev/null; exit 130' INT TERM; \
while kill -0 $$default_pid 2>/dev/null && kill -0 $$classic_pid 2>/dev/null; do \
sleep 1; \
done; \
if ! kill -0 $$default_pid 2>/dev/null; then \
wait $$default_pid; \
status=$$?; \
kill $$classic_pid 2>/dev/null; \
wait $$classic_pid 2>/dev/null; \
exit $$status; \
fi; \
wait $$classic_pid; \
status=$$?; \
kill $$default_pid 2>/dev/null; \
wait $$default_pid 2>/dev/null; \
exit $$status
dev-web-classic: dev-web-classic:
@echo "Starting classic frontend dev server..." @echo "Starting classic frontend dev server..."
@cd $(FRONTEND_CLASSIC_DIR) && bun install && bun run dev @cd ./web && bun install
@cd $(FRONTEND_CLASSIC_DIR) && bun run dev -- --host 0.0.0.0 --port $(DEV_FRONTEND_CLASSIC_PORT)
dev: dev-api dev-web dev: dev-api dev-web
......
...@@ -24,6 +24,5 @@ ...@@ -24,6 +24,5 @@
<body> <body>
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div> <div id="root"></div>
<script type="module" src="/src/index.jsx"></script>
</body> </body>
</html> </html>
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
"private": true, "private": true,
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@douyinfe/semi-illustrations": "^2.69.1",
"@douyinfe/semi-icons": "^2.63.1", "@douyinfe/semi-icons": "^2.63.1",
"@douyinfe/semi-ui": "^2.69.1", "@douyinfe/semi-ui": "^2.69.1",
"@lobehub/icons": "catalog:", "@lobehub/icons": "catalog:",
...@@ -14,6 +15,7 @@ ...@@ -14,6 +15,7 @@
"clsx": "catalog:", "clsx": "catalog:",
"dayjs": "catalog:", "dayjs": "catalog:",
"history": "^5.3.0", "history": "^5.3.0",
"highlight.js": "^11.11.1",
"i18next": "^23.16.8", "i18next": "^23.16.8",
"i18next-browser-languagedetector": "^7.2.0", "i18next-browser-languagedetector": "^7.2.0",
"katex": "^0.16.22", "katex": "^0.16.22",
...@@ -21,8 +23,8 @@ ...@@ -21,8 +23,8 @@
"marked": "^4.1.1", "marked": "^4.1.1",
"mermaid": "^11.6.0", "mermaid": "^11.6.0",
"qrcode.react": "catalog:", "qrcode.react": "catalog:",
"react": "^18.2.0", "react": "^19.2.6",
"react-dom": "^18.2.0", "react-dom": "^19.2.6",
"react-dropzone": "^14.2.3", "react-dropzone": "^14.2.3",
"react-fireworks": "^1.0.4", "react-fireworks": "^1.0.4",
"react-i18next": "^13.0.0", "react-i18next": "^13.0.0",
...@@ -42,13 +44,13 @@ ...@@ -42,13 +44,13 @@
"use-debounce": "^10.0.4" "use-debounce": "^10.0.4"
}, },
"scripts": { "scripts": {
"dev": "vite", "dev": "rsbuild dev",
"build": "vite build", "build": "rsbuild build",
"lint": "prettier . --check", "lint": "prettier . --check",
"lint:fix": "prettier . --write", "lint:fix": "prettier . --write",
"eslint": "bunx eslint \"**/*.{js,jsx}\" --cache", "eslint": "bunx eslint \"**/*.{js,jsx}\" --cache",
"eslint:fix": "bunx eslint \"**/*.{js,jsx}\" --fix --cache", "eslint:fix": "bunx eslint \"**/*.{js,jsx}\" --fix --cache",
"preview": "vite preview", "preview": "rsbuild preview",
"i18n:extract": "bunx i18next-cli extract", "i18n:extract": "bunx i18next-cli extract",
"i18n:status": "bunx i18next-cli status", "i18n:status": "bunx i18next-cli status",
"i18n:sync": "bunx i18next-cli sync", "i18n:sync": "bunx i18next-cli sync",
...@@ -73,20 +75,19 @@ ...@@ -73,20 +75,19 @@
] ]
}, },
"devDependencies": { "devDependencies": {
"@douyinfe/vite-plugin-semi": "^2.74.0-alpha.6", "@rsbuild/core": "^2.0.7",
"@rsbuild/plugin-react": "^2.0.0",
"@so1ve/prettier-config": "^3.1.0", "@so1ve/prettier-config": "^3.1.0",
"@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.4.21", "autoprefixer": "^10.4.21",
"code-inspector-plugin": "^1.3.3",
"eslint": "8.57.0", "eslint": "8.57.0",
"eslint-plugin-header": "^3.1.1", "eslint-plugin-header": "^3.1.1",
"eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-hooks": "^5.2.0",
"i18next-cli": "^1.10.3", "i18next-cli": "^1.10.3",
"postcss": "^8.5.3", "postcss": "^8.5.3",
"prop-types": "^15.8.1",
"prettier": "catalog:", "prettier": "catalog:",
"tailwindcss": "^3", "tailwindcss": "^3",
"typescript": "4.4.2", "typescript": "4.4.2"
"vite": "^5.2.0"
}, },
"prettier": { "prettier": {
"singleQuote": true, "singleQuote": true,
......
import path from 'path'
import { createRequire } from 'module'
import { fileURLToPath } from 'url'
import { defineConfig, loadEnv } from '@rsbuild/core'
import { pluginReact } from '@rsbuild/plugin-react'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const require = createRequire(import.meta.url)
const semiUiDir = path.resolve(
path.dirname(require.resolve('@douyinfe/semi-ui')),
'../..',
)
export default defineConfig(({ envMode }) => {
const env = loadEnv({ mode: envMode, prefixes: ['VITE_'] })
const clientServerUrl =
process.env.VITE_REACT_APP_SERVER_URL ||
env.rawPublicVars.VITE_REACT_APP_SERVER_URL ||
''
const proxyServerUrl =
clientServerUrl ||
'http://localhost:3000'
const isProd = envMode === 'production'
const devProxy = Object.fromEntries(
(['/api', '/mj', '/pg'] as const).map((key) => [
key,
{ target: proxyServerUrl, changeOrigin: true },
]),
) as Record<string, { target: string; changeOrigin: boolean }>
return {
plugins: [pluginReact()],
source: {
entry: {
index: './src/index.jsx',
},
define: {
'import.meta.env.VITE_REACT_APP_SERVER_URL': JSON.stringify(
clientServerUrl,
),
},
},
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
'@douyinfe/semi-ui/dist/css/semi.css': path.resolve(
semiUiDir,
'dist/css/semi.css',
),
},
},
html: {
template: './index.html',
},
server: {
host: '0.0.0.0',
strictPort: true,
proxy: devProxy,
},
output: {
minify: isProd,
target: 'web',
distPath: {
root: 'dist',
},
},
performance: {
removeConsole: isProd ? ['log'] : false,
buildCache: {
cacheDigest: [process.env.VITE_REACT_APP_VERSION],
},
},
tools: {
rspack: {
module: {
rules: [
{
test: /src[\\/].*\.js$/,
type: 'javascript/auto',
use: [
{
loader: 'builtin:swc-loader',
options: {
jsc: {
parser: {
syntax: 'ecmascript',
jsx: true,
},
transform: {
react: {
runtime: 'automatic',
development: !isProd,
refresh: !isProd,
},
},
},
},
},
],
},
],
},
},
},
}
})
...@@ -947,7 +947,7 @@ const LoginForm = () => { ...@@ -947,7 +947,7 @@ const LoginForm = () => {
}; };
return ( return (
<div className='relative overflow-hidden bg-gray-100 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8'> <div className='classic-page-fill relative overflow-hidden bg-gray-100 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8'>
{/* 背景模糊晕染球 */} {/* 背景模糊晕染球 */}
<div <div
className='blur-ball blur-ball-indigo' className='blur-ball blur-ball-indigo'
......
...@@ -104,7 +104,7 @@ const PasswordResetConfirm = () => { ...@@ -104,7 +104,7 @@ const PasswordResetConfirm = () => {
} }
return ( return (
<div className='relative overflow-hidden bg-gray-100 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8'> <div className='classic-page-fill relative overflow-hidden bg-gray-100 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8'>
{/* 背景模糊晕染球 */} {/* 背景模糊晕染球 */}
<div <div
className='blur-ball blur-ball-indigo' className='blur-ball blur-ball-indigo'
......
...@@ -104,7 +104,7 @@ const PasswordResetForm = () => { ...@@ -104,7 +104,7 @@ const PasswordResetForm = () => {
} }
return ( return (
<div className='relative overflow-hidden bg-gray-100 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8'> <div className='classic-page-fill relative overflow-hidden bg-gray-100 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8'>
{/* 背景模糊晕染球 */} {/* 背景模糊晕染球 */}
<div <div
className='blur-ball blur-ball-indigo' className='blur-ball blur-ball-indigo'
......
...@@ -770,7 +770,7 @@ const RegisterForm = () => { ...@@ -770,7 +770,7 @@ const RegisterForm = () => {
}; };
return ( return (
<div className='relative overflow-hidden bg-gray-100 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8'> <div className='classic-page-fill relative overflow-hidden bg-gray-100 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8'>
{/* 背景模糊晕染球 */} {/* 背景模糊晕染球 */}
<div <div
className='blur-ball blur-ball-indigo' className='blur-ball blur-ball-indigo'
......
...@@ -141,7 +141,7 @@ const DocumentRenderer = ({ apiEndpoint, title, cacheKey, emptyMessage }) => { ...@@ -141,7 +141,7 @@ const DocumentRenderer = ({ apiEndpoint, title, cacheKey, emptyMessage }) => {
// 显示加载状态 // 显示加载状态
if (loading) { if (loading) {
return ( return (
<div className='flex justify-center items-center min-h-screen'> <div className='classic-page-fill flex justify-center items-center'>
<Spin size='large' /> <Spin size='large' />
</div> </div>
); );
...@@ -150,7 +150,7 @@ const DocumentRenderer = ({ apiEndpoint, title, cacheKey, emptyMessage }) => { ...@@ -150,7 +150,7 @@ const DocumentRenderer = ({ apiEndpoint, title, cacheKey, emptyMessage }) => {
// 如果没有内容,显示空状态 // 如果没有内容,显示空状态
if (!content || content.trim() === '') { if (!content || content.trim() === '') {
return ( return (
<div className='flex justify-center items-center min-h-screen bg-gray-50'> <div className='classic-page-fill flex justify-center items-center bg-gray-50'>
<Empty <Empty
title={t('管理员未设置' + title + '内容')} title={t('管理员未设置' + title + '内容')}
image={ image={
...@@ -168,7 +168,7 @@ const DocumentRenderer = ({ apiEndpoint, title, cacheKey, emptyMessage }) => { ...@@ -168,7 +168,7 @@ const DocumentRenderer = ({ apiEndpoint, title, cacheKey, emptyMessage }) => {
// 如果是 URL,显示链接卡片 // 如果是 URL,显示链接卡片
if (isUrl(content)) { if (isUrl(content)) {
return ( return (
<div className='flex justify-center items-center min-h-screen bg-gray-50 p-4'> <div className='classic-page-fill flex justify-center items-center bg-gray-50 p-4'>
<Card className='max-w-md w-full'> <Card className='max-w-md w-full'>
<div className='text-center'> <div className='text-center'>
<Title heading={4} className='mb-4'> <Title heading={4} className='mb-4'>
...@@ -196,7 +196,7 @@ const DocumentRenderer = ({ apiEndpoint, title, cacheKey, emptyMessage }) => { ...@@ -196,7 +196,7 @@ const DocumentRenderer = ({ apiEndpoint, title, cacheKey, emptyMessage }) => {
// 如果是 HTML 内容,直接渲染 // 如果是 HTML 内容,直接渲染
if (isHtmlContent(content)) { if (isHtmlContent(content)) {
return ( return (
<div className='min-h-screen bg-gray-50'> <div className='classic-page-fill bg-gray-50'>
<div className='max-w-4xl mx-auto py-12 px-4 sm:px-6 lg:px-8'> <div className='max-w-4xl mx-auto py-12 px-4 sm:px-6 lg:px-8'>
<div className='bg-white rounded-lg shadow-sm p-8'> <div className='bg-white rounded-lg shadow-sm p-8'>
<Title heading={2} className='text-center mb-8'> <Title heading={2} className='text-center mb-8'>
...@@ -214,7 +214,7 @@ const DocumentRenderer = ({ apiEndpoint, title, cacheKey, emptyMessage }) => { ...@@ -214,7 +214,7 @@ const DocumentRenderer = ({ apiEndpoint, title, cacheKey, emptyMessage }) => {
// 其他内容统一使用 Markdown 渲染器 // 其他内容统一使用 Markdown 渲染器
return ( return (
<div className='min-h-screen bg-gray-50'> <div className='classic-page-fill bg-gray-50'>
<div className='max-w-4xl mx-auto py-12 px-4 sm:px-6 lg:px-8'> <div className='max-w-4xl mx-auto py-12 px-4 sm:px-6 lg:px-8'>
<div className='bg-white rounded-lg shadow-sm p-8'> <div className='bg-white rounded-lg shadow-sm p-8'>
<Title heading={2} className='text-center mb-8'> <Title heading={2} className='text-center mb-8'>
......
...@@ -71,6 +71,7 @@ const PageLayout = () => { ...@@ -71,6 +71,7 @@ const PageLayout = () => {
const isConsoleRoute = location.pathname.startsWith('/console'); const isConsoleRoute = location.pathname.startsWith('/console');
const showSider = isConsoleRoute && (!isMobile || drawerOpen); const showSider = isConsoleRoute && (!isMobile || drawerOpen);
const isFixedLayout = isConsoleRoute || location.pathname === '/pricing';
useEffect(() => { useEffect(() => {
if (isMobile && drawerOpen && collapsed) { if (isMobile && drawerOpen && collapsed) {
...@@ -146,11 +147,11 @@ const PageLayout = () => { ...@@ -146,11 +147,11 @@ const PageLayout = () => {
return ( return (
<Layout <Layout
className='app-layout' className={`app-layout${isFixedLayout ? ' app-layout-fixed' : ''}`}
style={{ style={{
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
overflow: isMobile ? 'visible' : 'hidden', overflow: isFixedLayout && !isMobile ? 'hidden' : 'visible',
}} }}
> >
<Header <Header
...@@ -171,9 +172,10 @@ const PageLayout = () => { ...@@ -171,9 +172,10 @@ const PageLayout = () => {
</Header> </Header>
<Layout <Layout
style={{ style={{
overflow: isMobile ? 'visible' : 'auto', overflow: isFixedLayout && !isMobile ? 'auto' : 'visible',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
flex: '1 1 auto',
}} }}
> >
{showSider && ( {showSider && (
...@@ -206,15 +208,18 @@ const PageLayout = () => { ...@@ -206,15 +208,18 @@ const PageLayout = () => {
flex: '1 1 auto', flex: '1 1 auto',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
minHeight: 0,
}} }}
> >
<Content <Content
className={isFixedLayout ? undefined : 'public-page-content'}
style={{ style={{
flex: '1 0 auto', flex: isFixedLayout ? '1 0 auto' : '1 1 auto',
overflowY: isMobile ? 'visible' : 'hidden', overflowY: isFixedLayout && !isMobile ? 'hidden' : 'visible',
WebkitOverflowScrolling: 'touch', WebkitOverflowScrolling: 'touch',
padding: shouldInnerPadding ? (isMobile ? '5px' : '24px') : '0', padding: shouldInnerPadding ? (isMobile ? '5px' : '24px') : '0',
position: 'relative', position: 'relative',
minHeight: 0,
}} }}
> >
<ErrorBoundary> <ErrorBoundary>
......
...@@ -17,12 +17,46 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. ...@@ -17,12 +17,46 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com For commercial licensing, please contact support@quantumnous.com
*/ */
export * from './channel.constants'; export {
export * from './user.constants'; CHANNEL_OPTIONS,
export * from './toast.constants'; MODEL_FETCHABLE_CHANNEL_TYPES,
export * from './common.constant'; MODEL_TABLE_PAGE_SIZE,
export * from './dashboard.constants'; } from './channel.constants';
export * from './playground.constants'; export { userConstants } from './user.constants';
export * from './redemption.constants'; export { toastConstants } from './toast.constants';
export * from './channel-affinity-template.constants'; export {
export * from './billing.constants'; ITEMS_PER_PAGE,
DEFAULT_ENDPOINT,
TABLE_COMPACT_MODES_KEY,
API_ENDPOINTS,
TASK_ACTION_GENERATE,
TASK_ACTION_TEXT_GENERATE,
TASK_ACTION_FIRST_TAIL_GENERATE,
TASK_ACTION_REFERENCE_GENERATE,
TASK_ACTION_REMIX_GENERATE,
} from './common.constant';
export {
REDEMPTION_STATUS,
REDEMPTION_STATUS_MAP,
REDEMPTION_ACTIONS,
} from './redemption.constants';
export {
CODEX_CLI_HEADER_PASSTHROUGH_HEADERS,
CLAUDE_CLI_HEADER_PASSTHROUGH_HEADERS,
CODEX_CLI_HEADER_PASSTHROUGH_TEMPLATE,
CLAUDE_CLI_HEADER_PASSTHROUGH_TEMPLATE,
CHANNEL_AFFINITY_RULE_TEMPLATES,
cloneChannelAffinityTemplate,
} from './channel-affinity-template.constants';
export {
BILLING_VARS,
BILLING_VAR_KEYS,
BILLING_PRICING_VARS,
BILLING_EXTRA_VARS,
BILLING_VAR_KEY_TO_FIELD,
BILLING_VAR_FIELD_TO_LABEL,
BILLING_VAR_FIELD_TO_SHORT_LABEL,
BILLING_CACHE_VAR_MAP,
BILLING_VAR_REGEX,
BILLING_CONDITION_VARS,
} from './billing.constants';
...@@ -94,7 +94,6 @@ import { ...@@ -94,7 +94,6 @@ import {
SiGitlab, SiGitlab,
SiGoogle, SiGoogle,
SiKeycloak, SiKeycloak,
SiLinkedin,
SiNextcloud, SiNextcloud,
SiNotion, SiNotion,
SiOkta, SiOkta,
...@@ -106,6 +105,7 @@ import { ...@@ -106,6 +105,7 @@ import {
SiWechat, SiWechat,
SiX, SiX,
} from 'react-icons/si'; } from 'react-icons/si';
import { FaLinkedin } from 'react-icons/fa';
// 获取侧边栏Lucide图标组件 // 获取侧边栏Lucide图标组件
export function getLucideIcon(key, selected = false) { export function getLucideIcon(key, selected = false) {
...@@ -509,7 +509,7 @@ const oauthProviderIconMap = { ...@@ -509,7 +509,7 @@ const oauthProviderIconMap = {
google: SiGoogle, google: SiGoogle,
discord: SiDiscord, discord: SiDiscord,
facebook: SiFacebook, facebook: SiFacebook,
linkedin: SiLinkedin, linkedin: FaLinkedin,
x: SiX, x: SiX,
twitter: SiX, twitter: SiX,
slack: SiSlack, slack: SiSlack,
......
...@@ -123,7 +123,7 @@ export function showError(error) { ...@@ -123,7 +123,7 @@ export function showError(error) {
console.error(error); console.error(error);
if (error.message) { if (error.message) {
if (error.name === 'AxiosError') { if (error.name === 'AxiosError') {
switch (error.response.status) { switch (error.response?.status) {
case 401: case 401:
// 清除用户状态 // 清除用户状态
localStorage.removeItem('user'); localStorage.removeItem('user');
......
...@@ -31,18 +31,40 @@ body { ...@@ -31,18 +31,40 @@ body {
background-color: var(--semi-color-bg-0); background-color: var(--semi-color-bg-0);
} }
/* 桌面端禁止 body 纵向滚动 - 防止 VChart tooltip 触发页面滚动条 */ .app-layout {
@media (min-width: 768px) { min-height: 100vh;
body { min-height: 100dvh;
overflow-y: hidden;
}
} }
.app-layout { .app-layout-fixed {
height: 100vh; height: 100vh;
height: 100dvh; height: 100dvh;
} }
.public-page-content {
display: flex;
flex-direction: column;
}
.classic-page-fill {
flex: 1 0 auto;
min-height: 100%;
}
.classic-home-page,
.classic-home-default {
display: flex;
flex-direction: column;
}
.classic-home-default {
flex: 1 0 auto;
}
.classic-home-hero {
flex: 1 0 auto;
}
.app-sider { .app-sider {
height: calc(100vh - 64px); height: calc(100vh - 64px);
height: calc(100dvh - 64px); height: calc(100dvh - 64px);
......
import '@douyinfe/semi-ui/react19-adapter';
/* /*
Copyright (C) 2025 QuantumNous Copyright (C) 2025 QuantumNous
......
...@@ -133,9 +133,9 @@ const About = () => { ...@@ -133,9 +133,9 @@ const About = () => {
); );
return ( return (
<div className='mt-[60px] px-2'> <div className='classic-page-fill flex flex-col pt-[60px] px-2'>
{aboutLoaded && about === '' ? ( {aboutLoaded && about === '' ? (
<div className='flex justify-center items-center h-screen p-8'> <div className='flex flex-1 justify-center items-center p-8'>
<Empty <Empty
image={ image={
<IllustrationConstruction style={{ width: 150, height: 150 }} /> <IllustrationConstruction style={{ width: 150, height: 150 }} />
...@@ -156,7 +156,12 @@ const About = () => { ...@@ -156,7 +156,12 @@ const About = () => {
{about.startsWith('https://') ? ( {about.startsWith('https://') ? (
<iframe <iframe
src={about} src={about}
style={{ width: '100%', height: '100vh', border: 'none' }} style={{
width: '100%',
flex: '1 1 auto',
minHeight: 0,
border: 'none',
}}
/> />
) : ( ) : (
<div <div
......
...@@ -28,7 +28,7 @@ import { useTranslation } from 'react-i18next'; ...@@ -28,7 +28,7 @@ import { useTranslation } from 'react-i18next';
const Forbidden = () => { const Forbidden = () => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<div className='flex justify-center items-center h-screen p-8'> <div className='classic-page-fill flex justify-center items-center p-8'>
<Empty <Empty
image={<IllustrationNoAccess style={{ width: 250, height: 250 }} />} image={<IllustrationNoAccess style={{ width: 250, height: 250 }} />}
darkModeImage={ darkModeImage={
......
...@@ -149,20 +149,20 @@ const Home = () => { ...@@ -149,20 +149,20 @@ const Home = () => {
}, [endpointItems.length]); }, [endpointItems.length]);
return ( return (
<div className='w-full overflow-x-hidden'> <div className='classic-page-fill classic-home-page w-full overflow-x-hidden'>
<NoticeModal <NoticeModal
visible={noticeVisible} visible={noticeVisible}
onClose={() => setNoticeVisible(false)} onClose={() => setNoticeVisible(false)}
isMobile={isMobile} isMobile={isMobile}
/> />
{homePageContentLoaded && homePageContent === '' ? ( {homePageContentLoaded && homePageContent === '' ? (
<div className='w-full overflow-x-hidden'> <div className='classic-home-default w-full overflow-x-hidden'>
{/* Banner 部分 */} {/* 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='classic-home-hero w-full border-b border-semi-color-border relative overflow-x-hidden'>
{/* 背景模糊晕染球 */} {/* 背景模糊晕染球 */}
<div className='blur-ball blur-ball-indigo' /> <div className='blur-ball blur-ball-indigo' />
<div className='blur-ball blur-ball-teal' /> <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 items-center justify-center px-4 pt-24 pb-8'>
{/* 居中内容区 */} {/* 居中内容区 */}
<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 text-center max-w-4xl mx-auto'>
<div className='flex flex-col items-center justify-center mb-6 md:mb-8'> <div className='flex flex-col items-center justify-center mb-6 md:mb-8'>
...@@ -335,11 +335,11 @@ const Home = () => { ...@@ -335,11 +335,11 @@ const Home = () => {
</div> </div>
</div> </div>
) : ( ) : (
<div className='overflow-x-hidden w-full'> <div className='classic-page-fill overflow-x-hidden w-full'>
{homePageContent.startsWith('https://') ? ( {homePageContent.startsWith('https://') ? (
<iframe <iframe
src={homePageContent} src={homePageContent}
className='w-full h-screen border-none' className='w-full h-full border-none'
/> />
) : ( ) : (
<div <div
......
...@@ -28,7 +28,7 @@ import { useTranslation } from 'react-i18next'; ...@@ -28,7 +28,7 @@ import { useTranslation } from 'react-i18next';
const NotFound = () => { const NotFound = () => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<div className='flex justify-center items-center h-screen p-8'> <div className='classic-page-fill flex justify-center items-center p-8'>
<Empty <Empty
image={<IllustrationNotFound style={{ width: 250, height: 250 }} />} image={<IllustrationNotFound style={{ width: 250, height: 250 }} />}
darkModeImage={ darkModeImage={
......
/*
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import react from '@vitejs/plugin-react';
import { defineConfig, transformWithEsbuild } from 'vite';
import pkg from '@douyinfe/vite-plugin-semi';
import path from 'path';
import { codeInspectorPlugin } from 'code-inspector-plugin';
const { vitePluginSemi } = pkg;
// https://vitejs.dev/config/
export default defineConfig({
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
plugins: [
codeInspectorPlugin({
bundler: 'vite',
}),
{
name: 'treat-js-files-as-jsx',
async transform(code, id) {
if (!/src\/.*\.js$/.test(id)) {
return null;
}
// Use the exposed transform from vite, instead of directly
// transforming with esbuild
return transformWithEsbuild(code, id, {
loader: 'jsx',
jsx: 'automatic',
});
},
},
react(),
vitePluginSemi({
cssLayer: true,
}),
],
optimizeDeps: {
force: true,
esbuildOptions: {
loader: {
'.js': 'jsx',
'.json': 'json',
},
},
},
build: {
rollupOptions: {
output: {
manualChunks: {
'react-core': ['react', 'react-dom', 'react-router-dom'],
'semi-ui': ['@douyinfe/semi-icons', '@douyinfe/semi-ui'],
tools: ['axios', 'history', 'marked'],
'react-components': [
'react-dropzone',
'react-fireworks',
'react-telegram-login',
'react-toastify',
'react-turnstile',
],
i18n: [
'i18next',
'react-i18next',
'i18next-browser-languagedetector',
],
},
},
},
},
server: {
host: '0.0.0.0',
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
},
'/mj': {
target: 'http://localhost:3000',
changeOrigin: true,
},
'/pg': {
target: 'http://localhost:3000',
changeOrigin: true,
},
},
},
});
...@@ -65,6 +65,7 @@ export default defineConfig(({ envMode }) => { ...@@ -65,6 +65,7 @@ export default defineConfig(({ envMode }) => {
}, },
server: { server: {
host: '0.0.0.0', host: '0.0.0.0',
strictPort: true,
proxy: devProxy, proxy: devProxy,
}, },
output: { output: {
......
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