Skip to content

Commit 606ed46

Browse files
author
Teror Fox
authored
docs: 新增关于反向代理的相关内容并添加复制成功弹窗 (#24)
* docs: 更新反向代理文档,增加图形化界面配置步骤 * docs: 添加 Cloudflare Tunnel 配置步骤和环境变量说明 * feat(copy): 实现自定义复制功能并优化用户体验 - 新增 useCopy 钩子以实现自定义复制逻辑 - 在 Configurator 和 EnvVariableConfig 组件中集成 useCopy - 移除第三方 copy 库,减少项目依赖 - 优化复制按钮文案,统一提示信息 - 在 layout 中添加 ToastProvider 以支持全局提示 * docs: 在中文与英文间添加空格 Signed-off-by: Teror Fox <[email protected]> --------- Signed-off-by: Teror Fox <[email protected]>
1 parent 950b736 commit 606ed46

File tree

8 files changed

+291
-18
lines changed

8 files changed

+291
-18
lines changed

app/components/Configurator.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"use client"
22

33
import { useState } from 'react'
4-
import copy from 'copy-to-clipboard'
4+
import { useCopy } from '@/hooks/use-copy'
55

66
export const Configurator = ({ args, template, env }: {
77
args: string[],
@@ -15,6 +15,7 @@ export const Configurator = ({ args, template, env }: {
1515
}));
1616

1717
const [values, setValues] = useState(envVariables.map(v => v.defaultVal || ''));
18+
const { copyToClipboard } = useCopy();
1819

1920
const handleCopy = () => {
2021
// 处理环境变量
@@ -34,7 +35,7 @@ export const Configurator = ({ args, template, env }: {
3435
);
3536
});
3637

37-
copy(result);
38+
copyToClipboard(result, '配置已复制到剪贴板');
3839
};
3940

4041
return (
@@ -65,4 +66,4 @@ export const Configurator = ({ args, template, env }: {
6566
</button>
6667
</div>
6768
);
68-
};
69+
};

app/components/EnvVariableConfig.tsx

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,19 @@
11
"use client";
22
import { useState } from 'react';
3-
import copy from 'copy-to-clipboard';
3+
import { useCopy } from '@/hooks/use-copy';
44

55
export function EnvVariableConfig({ variableNames, format }: { variableNames: { key: string; name: string; defaultVal?: string }[]; format?: "yaml" | "env" }) {
66
const [values, setValues] = useState(variableNames.map((name) => name.defaultVal || ''));
7-
const [copyButtonText, setCopyButtonText] = useState('复制'); // 新增状态用于控制按钮文本
7+
const { copyToClipboard } = useCopy();
88

99
const handleCopy = () => {
1010
if (format === 'yaml') {
1111
const yamlContent = variableNames.map((name, index) => `- ${name.key}=${values[index]}`).join('\n');
12-
copy(yamlContent);
12+
copyToClipboard(yamlContent, '环境变量配置已复制');
1313
} else {
1414
const envContent = variableNames.map((name, index) => `${name.key}=${values[index]}`).join('\n');
15-
copy(envContent);
15+
copyToClipboard(envContent, '环境变量配置已复制');
1616
}
17-
setCopyButtonText('复制成功');
18-
setTimeout(() => {
19-
setCopyButtonText('复制');
20-
}, 3000);
2117
};
2218

2319
const handleChange = (index: number, value: string) => {
@@ -68,7 +64,7 @@ export function EnvVariableConfig({ variableNames, format }: { variableNames: {
6864
className="border bg-black w-full text-white px-4 py-2 rounded-lg text-sm transform transition-all duration-300 focus:outline-none hover:bg-gray-700 dark:border-gray-700 dark:bg-gray-800"
6965
onClick={handleCopy}
7066
>
71-
{copyButtonText}
67+
复制配置
7268
</button>
7369
</div>
7470
</div>

app/layout.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Provider } from './components/provider';
33
import type { ReactNode } from 'react';
44
import type { Metadata } from 'next'
55
import { env } from 'std-env'
6+
import { ToastProvider } from '@/contexts/toast-context';
67
const baseUrl = env.NEXT_PUBLIC_BASE_URL || 'https://mx-space.js.org'
78
const metaDescription = `Mix Space 是一个小型个人空间站点程序,采用前后端分离设计,适合喜欢写作的你。`
89
const metaTitle = 'Mix Space 文档 - 现代化的个人空间解决方案'
@@ -34,7 +35,9 @@ export default function RootLayout({
3435
</head>
3536
<body>
3637
<Provider>
37-
{children}
38+
<ToastProvider>
39+
{children}
40+
</ToastProvider>
3841
</Provider>
3942
</body>
4043
</html>

components/ui/toast.tsx

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
'use client'
2+
3+
import { useEffect, useState } from 'react'
4+
import { createPortal } from 'react-dom'
5+
import { cn } from '@/utils/cn'
6+
7+
export interface ToastProps {
8+
id: string
9+
message: string
10+
type?: 'success' | 'error' | 'warning' | 'info'
11+
duration?: number
12+
onClose: (id: string) => void
13+
}
14+
15+
export function Toast({ id, message, type = 'success', duration = 3000, onClose }: ToastProps) {
16+
const [isVisible, setIsVisible] = useState(false)
17+
const [isLeaving, setIsLeaving] = useState(false)
18+
const [mounted, setMounted] = useState(false)
19+
20+
useEffect(() => {
21+
setMounted(true)
22+
}, [])
23+
24+
useEffect(() => {
25+
if (!mounted) return
26+
27+
// 进入动画
28+
const timer = setTimeout(() => setIsVisible(true), 50)
29+
30+
// 自动关闭
31+
const closeTimer = setTimeout(() => {
32+
setIsLeaving(true)
33+
setTimeout(() => onClose(id), 300)
34+
}, duration)
35+
36+
return () => {
37+
clearTimeout(timer)
38+
clearTimeout(closeTimer)
39+
}
40+
}, [id, duration, onClose, mounted])
41+
42+
const getTypeStyles = () => {
43+
switch (type) {
44+
case 'success':
45+
return 'bg-green-500 text-white border-green-600'
46+
case 'error':
47+
return 'bg-red-500 text-white border-red-600'
48+
case 'warning':
49+
return 'bg-yellow-500 text-white border-yellow-600'
50+
case 'info':
51+
return 'bg-blue-500 text-white border-blue-600'
52+
default:
53+
return 'bg-green-500 text-white border-green-600'
54+
}
55+
}
56+
57+
const getIcon = () => {
58+
switch (type) {
59+
case 'success':
60+
return '✓'
61+
case 'error':
62+
return '✕'
63+
case 'warning':
64+
return '⚠'
65+
case 'info':
66+
return 'ℹ'
67+
default:
68+
return '✓'
69+
}
70+
}
71+
72+
if (!mounted) return null
73+
74+
return createPortal(
75+
<div
76+
className={cn(
77+
'fixed top-4 right-4 z-50 flex items-center gap-3 px-4 py-3 rounded-lg shadow-lg border',
78+
'transform transition-all duration-300 ease-in-out min-w-[280px] max-w-[400px]',
79+
getTypeStyles(),
80+
isVisible && !isLeaving ? 'translate-x-0 opacity-100' : 'translate-x-full opacity-0'
81+
)}
82+
>
83+
<span className="text-lg font-semibold">{getIcon()}</span>
84+
<span className="flex-1 font-medium text-sm">{message}</span>
85+
<button
86+
onClick={() => {
87+
setIsLeaving(true)
88+
setTimeout(() => onClose(id), 300)
89+
}}
90+
className="ml-2 text-white/80 hover:text-white transition-colors text-lg leading-none"
91+
aria-label="关闭"
92+
>
93+
×
94+
</button>
95+
</div>,
96+
document.body
97+
)
98+
}

content/docs/core/extra.mdx

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,62 @@ icon: Ellipsis
66

77
## 反向代理
88

9-
在这里提供双域名(前端和后端各用一个域名)与单域名(前后端共用一个域名)的配置步骤
9+
在这里提供 **Mix-Space** 的反代配置步骤
1010

11-
当然不管使用哪种方法,都建议用控制面板(如宝塔、1Panel 等)的使用面板提供的反代功能单独粘贴对应的反代配置部分完成配置(需要删掉开头和结尾的 server 块),手写反代配置的大佬随意。
11+
当然不管使用哪种方法,都建议用控制面板(如宝塔、1Panel 等)完成配置,手写反代配置的大佬随意。
1212

1313
另外,不管是前端还是后端的域名,都需要**配置好 HTTPS 证书**以保证网站能正常访问。
1414

15-
### 双域名
15+
### 图形化界面
16+
17+
现代服务器面板(如 `1Panel``宝塔面板`)自带的**反向代理**已足以满足 Mix-Space 所需的反代要求(包括 Websocket),因此我们更建议非高级用户使用图形化界面来操作和维护
18+
19+
#### 宝塔面板
20+
21+
进入`网站`,在`反向代理`栏目下点击`添加反代`
22+
23+
`域名`填入你将要使用的域名,`目标`填写`URL地址` + `http://127.0.0.1:2333`
24+
25+
#### 1Panel
26+
27+
进入`网站 > 网站`,并创建一个新网站,选择`反向代理`
28+
29+
`主域名`填入你将要使用的域名,并勾选`监听 IPV6`,代理类型选择 `http` ,地址填入 `127.0.0.1:2333`
30+
31+
### Cloudflare Tunnel
32+
<Callout type="warn">
33+
除非你在**非完整服务器环境**(如在 Sealos 或 Huggingface Space 上部署),否则我们不推荐在容器内使用该功能,而应在宿主机内配置 **Cloudflare Tunnel** 以避免后期出现管理不方便等问题
34+
</Callout>
35+
36+
启动该功能需要两个环境变量
37+
- `ENABLE_CLOUDFLARED` = **true**
38+
- `CF_ZERO_TRUST_TOKEN` = **Tunnel 给的令牌(删掉 cloudflared.exe service install,只保留令牌部分)**
39+
40+
#### 详细步骤:
41+
1.申请 Cloudflare Zero Trust,关于申请方式请自行查找
42+
43+
2.添加一条隧道,连接方式选择 Cloudflared,名称任意
44+
45+
3.添加一个 Public Hostname,回源选择 HTTP,端口选择 2333
46+
47+
一旦启动成功,你应当在日志中看到如下输出,并在 Cloudflare 后台看到客户端正常上线:
48+
```
49+
============================================
50+
Starting Cloudflared Tunnel
51+
============================================
52+
53+
============================================
54+
2025-06-06T02:22:40Z INF Using SysV
55+
2025-06-06T02:22:41Z INF Linux service for cloudflared installed successfully
56+
```
57+
58+
### 手写配置
59+
60+
<Callout type="warn">
61+
手写配置文件需要较高的**技术功底**,请量力而行
62+
</Callout>
63+
64+
#### 双域名
1665

1766
这里假定前端域名为 `www.example.com`,后端为 `server.example.com`
1867

@@ -80,7 +129,7 @@ server{
80129
- 本地后台为 `https://server.example.com/proxy/qaqdmin`
81130
</Callout>
82131

83-
### 单域名
132+
#### 单域名
84133

85134
以下配置文件以 Nginx 为例,请自行修改 SSL 证书路径以及自己的网站域名。
86135

contexts/toast-context.tsx

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
'use client'
2+
3+
import { createContext, useContext, useState, ReactNode, useCallback } from 'react'
4+
import { Toast, ToastProps } from '@/components/ui/toast'
5+
6+
interface ToastContextType {
7+
showToast: (message: string, type?: ToastProps['type'], duration?: number) => void
8+
showSuccess: (message: string, duration?: number) => void
9+
showError: (message: string, duration?: number) => void
10+
showWarning: (message: string, duration?: number) => void
11+
showInfo: (message: string, duration?: number) => void
12+
}
13+
14+
const ToastContext = createContext<ToastContextType | null>(null)
15+
16+
interface ToastItem extends Omit<ToastProps, 'onClose'> {
17+
id: string
18+
}
19+
20+
export function ToastProvider({ children }: { children: ReactNode }) {
21+
const [toasts, setToasts] = useState<ToastItem[]>([])
22+
23+
const removeToast = useCallback((id: string) => {
24+
setToasts(prev => prev.filter(toast => toast.id !== id))
25+
}, [])
26+
27+
const showToast = useCallback((
28+
message: string,
29+
type: ToastProps['type'] = 'success',
30+
duration = 3000
31+
) => {
32+
const id = Date.now().toString() + Math.random().toString(36).substr(2, 9)
33+
const newToast: ToastItem = {
34+
id,
35+
message,
36+
type,
37+
duration
38+
}
39+
setToasts(prev => [...prev, newToast])
40+
}, [])
41+
42+
const showSuccess = useCallback((message: string, duration?: number) => {
43+
showToast(message, 'success', duration)
44+
}, [showToast])
45+
46+
const showError = useCallback((message: string, duration?: number) => {
47+
showToast(message, 'error', duration)
48+
}, [showToast])
49+
50+
const showWarning = useCallback((message: string, duration?: number) => {
51+
showToast(message, 'warning', duration)
52+
}, [showToast])
53+
54+
const showInfo = useCallback((message: string, duration?: number) => {
55+
showToast(message, 'info', duration)
56+
}, [showToast])
57+
58+
return (
59+
<ToastContext.Provider value={{ showToast, showSuccess, showError, showWarning, showInfo }}>
60+
{children}
61+
{toasts.map(toast => (
62+
<Toast
63+
key={toast.id}
64+
{...toast}
65+
onClose={removeToast}
66+
/>
67+
))}
68+
</ToastContext.Provider>
69+
)
70+
}
71+
72+
export function useToast() {
73+
const context = useContext(ToastContext)
74+
if (!context) {
75+
throw new Error('useToast must be used within a ToastProvider')
76+
}
77+
return context
78+
}

hooks/use-copy.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
'use client'
2+
3+
import { useCallback } from 'react'
4+
import { useToast } from '@/contexts/toast-context'
5+
6+
export function useCopy() {
7+
const { showSuccess, showError } = useToast()
8+
9+
const copyToClipboard = useCallback(async (text: string, successMessage?: string) => {
10+
try {
11+
if (navigator.clipboard && window.isSecureContext) {
12+
// 使用现代 Clipboard API
13+
await navigator.clipboard.writeText(text)
14+
} else {
15+
// 兼容旧版浏览器
16+
const textArea = document.createElement('textarea')
17+
textArea.value = text
18+
textArea.style.position = 'fixed'
19+
textArea.style.left = '-999999px'
20+
textArea.style.top = '-999999px'
21+
document.body.appendChild(textArea)
22+
textArea.focus()
23+
textArea.select()
24+
25+
const successful = document.execCommand('copy')
26+
document.body.removeChild(textArea)
27+
28+
if (!successful) {
29+
throw new Error('复制失败')
30+
}
31+
}
32+
33+
showSuccess(successMessage || '复制成功!')
34+
return true
35+
} catch (error) {
36+
console.error('复制失败:', error)
37+
showError('复制失败,请重试')
38+
return false
39+
}
40+
}, [showSuccess, showError])
41+
42+
return { copyToClipboard }
43+
}

utils/cn.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,6 @@
1-
export { twMerge as cn } from 'tailwind-merge';
1+
import { clsx, type ClassValue } from 'clsx';
2+
import { twMerge } from 'tailwind-merge';
3+
4+
export function cn(...inputs: ClassValue[]) {
5+
return twMerge(clsx(inputs));
6+
}

0 commit comments

Comments
 (0)