├── .github └── ISSUE_TEMPLATE │ ├── bug_report_zh.yml │ └── feature_request_zh.yml ├── .gitignore ├── README.md ├── app ├── [[...mdxPath]] │ └── page.tsx ├── api │ ├── oauth2 │ │ ├── callback │ │ │ └── route.ts │ │ └── route.ts │ ├── topics │ │ └── [topicId] │ │ │ └── route.ts │ └── users │ │ └── [username] │ │ └── route.ts ├── globals.css └── layout.tsx ├── components.json ├── components ├── animate-ui │ ├── backgrounds │ │ └── fireworks.tsx │ ├── buttons │ │ ├── copy.tsx │ │ └── flip.tsx │ ├── components │ │ ├── avatar-group.tsx │ │ ├── code-tabs.tsx │ │ ├── scroll-progress.tsx │ │ ├── tabs.tsx │ │ └── tooltip.tsx │ ├── effects │ │ └── motion-highlight.tsx │ ├── icons │ │ ├── gavel.tsx │ │ ├── icon.tsx │ │ ├── refresh-ccw.tsx │ │ ├── search.tsx │ │ ├── send.tsx │ │ └── thumbs-up.tsx │ ├── radix │ │ └── hover-card.tsx │ ├── text │ │ └── sliding-number.tsx │ └── ui-elements │ │ └── management-bar.tsx ├── common │ ├── CardGrid.tsx │ ├── NavbarOAuthButton.tsx │ ├── ThemeWrapper.tsx │ ├── TopicHoverCard.tsx │ ├── UserGroupTooltip.tsx │ ├── UserHoverCard.tsx │ └── card │ │ ├── Gold.tsx │ │ ├── Niello.tsx │ │ └── User.tsx ├── pandora │ ├── HTML.tsx │ └── MemoryCard.tsx └── ui │ ├── avatar.tsx │ ├── button.tsx │ └── card.tsx ├── content ├── AI │ ├── Fuclaude │ │ ├── EveryFuclaude.mdx │ │ ├── Fuclaude.mdx │ │ ├── SessionToken.mdx │ │ └── _meta.ts │ ├── OAIFree │ │ ├── AccessToken.mdx │ │ ├── Chat2APIOAIFree.mdx │ │ ├── DemoOAIFree.mdx │ │ ├── EveryOAIFree.mdx │ │ ├── NewOAIFree.mdx │ │ ├── RefreshToken.mdx │ │ ├── ShareOAIFree.mdx │ │ ├── ShareToken.mdx │ │ └── _meta.ts │ └── _meta.ts ├── Community │ ├── LinuxDoConnect.mdx │ ├── LinuxDoLottery.mdx │ ├── LinuxDoMetaverse.mdx │ ├── LinuxDoWebMail.mdx │ ├── OAIPro.mdx │ └── _meta.ts ├── Encyclopedia │ ├── Cant │ │ ├── AFF.mdx │ │ ├── C.mdx │ │ ├── Che.mdx │ │ ├── Gao7Nian3.mdx │ │ ├── Lao.mdx │ │ ├── VPS.mdx │ │ └── _meta.ts │ ├── User │ │ ├── 6512345.mdx │ │ ├── _meta.ts │ │ ├── user3.mdx │ │ └── vux1jpmal5t41lg.mdx │ └── _meta.ts ├── HttpRW │ ├── ApiHttpRW.mdx │ ├── HttpRW.mdx │ ├── TLSHttpRW.mdx │ └── _meta.ts ├── LinuxDo │ ├── _meta.ts │ ├── administrator.mdx │ ├── culture.mdx │ ├── rules.mdx │ └── trustlevel.mdx ├── _meta.ts ├── honor.mdx ├── index.mdx └── tools.mdx ├── eslint.config.mjs ├── hooks └── useDataCache.ts ├── lib ├── data-cache.ts └── utils.ts ├── mdx-components.js ├── next.config.mjs ├── package.json ├── pnpm-lock.yaml ├── postcss.config.mjs ├── public ├── 2048.png ├── Farrington-7B.ttf ├── demo.png ├── favicon.ico ├── fuclaude.png ├── linuxdo_dark.png ├── linuxdo_light.png ├── linuxdoconnect_1.png ├── linuxdoconnect_2.png ├── linuxdoconnect_3.png ├── logo.png ├── neo.jpg └── oaifree.png ├── tailwind.config.js └── tsconfig.json /.github/ISSUE_TEMPLATE/bug_report_zh.yml: -------------------------------------------------------------------------------- 1 | name: 错误反馈 2 | description: '提交 LINUX DO WIKI 漏洞' 3 | title: '[错误] ' 4 | body: 5 | - type: checkboxes 6 | id: ensure 7 | attributes: 8 | label: Verify steps 9 | description: 为避免重复提交,在提交之前,请勾选以下所有选项以证明您已经阅读并理解了以下要求,否则该 issue 将被关闭。 10 | options: 11 | - label: 我已在标题简短的描述了我所遇到的问题 12 | - label: 我已在 [Issue Tracker](./?q=is%3Aissue) 中寻找过我要提出的问题,但未找到相同的问题 13 | 14 | - type: dropdown 15 | attributes: 16 | label: 操作系统 17 | description: 请提供操作系统类型 18 | multiple: true 19 | options: 20 | - MacOS 21 | - Windows 22 | - Linux 23 | validations: 24 | required: true 25 | - type: dropdown 26 | attributes: 27 | label: 浏览器版本 28 | description: 请提供使用的浏览器版本 29 | multiple: true 30 | options: 31 | - Edge 32 | - Chorme 33 | - Firefox 34 | - 手机系统默认浏览器 35 | - type: input 36 | attributes: 37 | label: 系统及浏览器版本 38 | description: 请提供出现问题的操作系统及使用的浏览器版本 39 | validations: 40 | required: true 41 | - type: textarea 42 | attributes: 43 | label: 描述 44 | description: 请提供错误的详细描述。 45 | validations: 46 | required: true 47 | - type: textarea 48 | attributes: 49 | label: 重现方式 50 | description: 请提供重现错误的步骤 51 | validations: 52 | required: true 53 | - type: textarea 54 | attributes: 55 | label: 社区个人主页 56 | description: 请提供社区个人主页 57 | validations: 58 | required: true 59 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request_zh.yml: -------------------------------------------------------------------------------- 1 | name: 功能请求 2 | description: '请求 LINUX DO WIKI 功能' 3 | title: '[建议] ' 4 | body: 5 | - type: checkboxes 6 | id: ensure 7 | attributes: 8 | label: Verify steps 9 | description: 为避免重复提交,在提交之前,请勾选以下所有选项以证明您已经阅读并理解了以下要求,否则该 issue 将被关闭。 10 | options: 11 | - label: 我已在标题简短的描述了我所需的功能 12 | - label: 我已在 [Issue Tracker](./?q=is%3Aissue) 中寻找过,但未找到我所需的功能 13 | - label: 我未在当前版本找到我所需的功能 14 | - type: textarea 15 | attributes: 16 | label: 描述 17 | description: 请提供所需功能的详细描述 18 | validations: 19 | required: true 20 | - type: textarea 21 | attributes: 22 | label: 社区个人主页 23 | description: 请提供社区个人主页 24 | validations: 25 | required: true 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # env files (can opt-in for committing if needed) 34 | .env* 35 | 36 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | next-env.d.ts 42 | 43 | _pagefind/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 | 8 | 9 | ## Linux Do Wiki | [ wiki.linux.do](wiki.linux.do) 10 | 11 |
12 | 13 |
14 | 15 | ### Wiki 技术栈 16 | 17 | - Nextjs 15 18 | - Tailwind CSS v4 19 | - Nextera 20 | - ShadcnUI、AnimateUI 21 | 22 | #### 诚邀各位共同参与维护🔗 23 | 24 | 为营造并鼓励共建环境,秉着**真诚、友善、团结、专业,共建你我引以为荣之社区的精神**。现对所有提交的 Issue/PR 的社区用户,在成功采纳后按照相应规则向各位积极参与的佬友表示感谢! 25 | 26 | ### 感谢我们的共建者💗 27 | 28 | [![][github-contrib-shield]][github-contrib-link] 29 | 30 | 31 | 32 | [github-contrib-shield]: https://contrib.rocks/image?repo=Chenyme/Linux-Do-Wiki 33 | [github-contrib-link]: https://github.com/chenyme/LinuxDoWiki/graphs/contributors 34 | 35 | 36 | ### 反馈格式和说明 37 | 38 | 为了方便更好的管理和维护 Wiki 站,希望大家在提交 Issue/PR 时遵循一定的格式。 39 | 40 | - PR 时请遵循项目框架 Nextjs、Nextra 的编写规范 41 | - 除紧急情况外,所有 Merge 会在每周定时更新 42 | - Issue/PR 提交时的格式和说明: 43 | 44 | |标题格式|Issue/PR 内容| 45 | |------|------| 46 | | 【建议】xxx页面的xxx内容 | 想要建议的内容 + 理由 + 社区个人主页链接 | 47 | | 【错误】xxx页面的xxxBUG | 报告xxx页面内的BUG + 具体情况 + 社区个人主页链接 | 48 | | 【修复】xxx页面的xxxBUG | xxx页面内的 xxx BUG + 社区个人主页链接 | 49 | | 【更正】xxx页面的xxx内容 | xxx页面内需要更新的内容 + 社区个人主页链接 | 50 | | 【新增】xxx页面的xxx内容 | xxx页面内需要新增的内容 + 理由 + 社区个人主页链接 | 51 | | 【改进】xxx页面的xxx内容 | xxx页面内需要改进的内容 + 理由 + 社区个人主页链接 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /app/[[...mdxPath]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { generateStaticParamsFor, importPage } from 'nextra/pages' 2 | import { useMDXComponents as getMDXComponents } from '../../mdx-components' 3 | 4 | export const generateStaticParams = generateStaticParamsFor('mdxPath') 5 | 6 | export async function generateMetadata(props: { params: Promise<{ mdxPath: string[] }> }) { 7 | const params = await props.params 8 | const { mdxPath } = params 9 | const { metadata } = await importPage(mdxPath) 10 | return metadata 11 | } 12 | 13 | const Wrapper = getMDXComponents().wrapper 14 | 15 | type PageProps = { 16 | params: Promise<{ 17 | mdxPath: string[] 18 | }> 19 | } 20 | 21 | export default async function Page({ params }: PageProps) { 22 | const resolvedParams = await params 23 | const { mdxPath } = resolvedParams 24 | const result = await importPage(mdxPath) 25 | const { default: MDXContent, toc, metadata } = result 26 | return ( 27 | 28 | 29 | 30 | ) 31 | } -------------------------------------------------------------------------------- /app/api/oauth2/callback/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server'; 2 | import { cookies } from 'next/headers'; 3 | 4 | // OAuth2配置 5 | const OAUTH2_CONFIG = { 6 | clientId: process.env.OAUTH_CLIENT_ID || 'bVcJaAhPOpbzS7tJe33qOiRbaffg4hf7', 7 | clientSecret: process.env.OAUTH_CLIENT_SECRET || '2rT42kRTsCajielGqysihZpilBZAxkqe', 8 | tokenEndpoint: 'https://connect.linux.do/oauth2/token', 9 | userInfoEndpoint: 'https://connect.linux.do/api/user' 10 | }; 11 | 12 | export async function GET(request: NextRequest) { 13 | try { 14 | const searchParams = request.nextUrl.searchParams; 15 | const code = searchParams.get('code'); 16 | const state = searchParams.get('state'); 17 | const cookieStore = await cookies(); 18 | 19 | // 验证状态参数以防止CSRF攻击 20 | const storedState = cookieStore.get('oauth_state')?.value; 21 | 22 | if (!storedState || state !== storedState) { 23 | const errorBaseUrl = process.env.NEXT_PUBLIC_BASE_URL || request.nextUrl.origin; 24 | return NextResponse.redirect(new URL('/tools?error=invalid_state', errorBaseUrl)); 25 | } 26 | 27 | // 清除状态cookie 28 | cookieStore.set('oauth_state', '', { expires: new Date(0) }); 29 | 30 | if (!code) { 31 | const errorBaseUrl = process.env.NEXT_PUBLIC_BASE_URL || request.nextUrl.origin; 32 | return NextResponse.redirect(new URL('/tools?error=no_code', errorBaseUrl)); 33 | } 34 | 35 | // 交换授权码获取访问令牌 36 | const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || request.nextUrl.origin; 37 | const redirectUri = `${baseUrl}/api/oauth2/callback`; 38 | const tokenResponse = await fetch(OAUTH2_CONFIG.tokenEndpoint, { 39 | method: 'POST', 40 | headers: { 41 | 'Content-Type': 'application/x-www-form-urlencoded', 42 | 'Accept': 'application/json' 43 | }, 44 | body: new URLSearchParams({ 45 | grant_type: 'authorization_code', 46 | client_id: OAUTH2_CONFIG.clientId, 47 | client_secret: OAUTH2_CONFIG.clientSecret, 48 | code, 49 | redirect_uri: redirectUri 50 | }) 51 | }); 52 | 53 | if (!tokenResponse.ok) { 54 | console.error('Token exchange failed:', await tokenResponse.text()); 55 | const errorBaseUrl = process.env.NEXT_PUBLIC_BASE_URL || request.nextUrl.origin; 56 | return NextResponse.redirect(new URL('/tools?error=token_exchange_failed', errorBaseUrl)); 57 | } 58 | 59 | const tokenData = await tokenResponse.json(); 60 | const { access_token, refresh_token, expires_in } = tokenData; 61 | 62 | // 获取用户信息 63 | const userInfoResponse = await fetch(OAUTH2_CONFIG.userInfoEndpoint, { 64 | headers: { 65 | 'Authorization': `Bearer ${access_token}` 66 | } 67 | }); 68 | 69 | if (!userInfoResponse.ok) { 70 | console.error('User info fetch failed:', await userInfoResponse.text()); 71 | const errorBaseUrl = process.env.NEXT_PUBLIC_BASE_URL || request.nextUrl.origin; 72 | return NextResponse.redirect(new URL('/tools?error=user_info_failed', errorBaseUrl)); 73 | } 74 | 75 | const userData = await userInfoResponse.json(); 76 | 77 | // 设置用户信息和令牌到cookie 78 | cookieStore.set('oauth_token', JSON.stringify({ 79 | access_token, 80 | refresh_token, 81 | expires_at: Date.now() + expires_in * 1000 82 | }), { 83 | httpOnly: true, 84 | secure: process.env.NODE_ENV === 'production', 85 | maxAge: expires_in, 86 | path: '/' 87 | }); 88 | 89 | // 用户公开信息存储在非httpOnly cookie中,以便客户端JavaScript访问 90 | cookieStore.set('oauth_user', JSON.stringify({ 91 | id: userData.id, 92 | name: userData.name || userData.username, 93 | username: userData.username, 94 | avatar: userData.avatar_url || userData.avatar 95 | }), { 96 | httpOnly: false, 97 | secure: process.env.NODE_ENV === 'production', 98 | maxAge: expires_in, 99 | path: '/' 100 | }); 101 | 102 | // 获取登录后重定向URL 103 | const redirectAfterLogin = cookieStore.get('redirect_after_login')?.value || '/tools'; 104 | cookieStore.set('redirect_after_login', '', { expires: new Date(0) }); 105 | 106 | // 重定向到原始页面 107 | const redirectBaseUrl = process.env.NEXT_PUBLIC_BASE_URL || request.nextUrl.origin; 108 | return NextResponse.redirect(new URL(redirectAfterLogin, redirectBaseUrl)); 109 | 110 | } catch (error) { 111 | console.error('OAuth callback error:', error); 112 | const errorBaseUrl = process.env.NEXT_PUBLIC_BASE_URL || request.nextUrl.origin; 113 | return NextResponse.redirect(new URL('/tools?error=server_error', errorBaseUrl)); 114 | } 115 | } -------------------------------------------------------------------------------- /app/api/oauth2/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server'; 2 | import { cookies } from 'next/headers'; 3 | 4 | // OAuth2配置 5 | const OAUTH2_CONFIG = { 6 | clientId: process.env.OAUTH_CLIENT_ID || 'bVcJaAhPOpbzS7tJe33qOiRbaffg4hf7', 7 | clientSecret: process.env.OAUTH_CLIENT_SECRET || '2rT42kRTsCajielGqysihZpilBZAxkqe', 8 | authorizeEndpoint: 'https://connect.linux.do/oauth2/authorize', 9 | tokenEndpoint: 'https://connect.linux.do/oauth2/token', 10 | userInfoEndpoint: 'https://connect.linux.do/api/user', 11 | redirectUri: '', 12 | scope: 'profile' 13 | }; 14 | 15 | // 生成随机状态值以防止CSRF攻击 16 | function generateState(): string { 17 | return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15); 18 | } 19 | 20 | // 授权请求处理 21 | export async function GET(request: NextRequest) { 22 | const searchParams = request.nextUrl.searchParams; 23 | const action = searchParams.get('action'); 24 | 25 | // 处理登出请求 26 | if (action === 'logout') { 27 | const cookieStore = await cookies(); 28 | cookieStore.set('oauth_user', '', { expires: new Date(0) }); 29 | cookieStore.set('oauth_token', '', { expires: new Date(0) }); 30 | 31 | // 获取重定向URL,默认为/tools 32 | const redirectTo = searchParams.get('redirect') || '/tools'; 33 | const logoutBaseUrl = process.env.NEXT_PUBLIC_BASE_URL || request.nextUrl.origin; 34 | return NextResponse.redirect(new URL(redirectTo, logoutBaseUrl)); 35 | } 36 | 37 | // 生成状态值并存储在cookie中 38 | const state = generateState(); 39 | const cookieStore = await cookies(); 40 | cookieStore.set('oauth_state', state, { 41 | httpOnly: true, 42 | secure: process.env.NODE_ENV === 'production', 43 | maxAge: 60 * 10, // 10分钟有效期 44 | path: '/' 45 | }); 46 | 47 | // 获取重定向URL,用于登录后返回 48 | const redirectAfterLogin = searchParams.get('redirect') || '/tools'; 49 | cookieStore.set('redirect_after_login', redirectAfterLogin, { 50 | httpOnly: true, 51 | secure: process.env.NODE_ENV === 'production', 52 | maxAge: 60 * 10, // 10分钟有效期 53 | path: '/' 54 | }); 55 | 56 | // 构建授权URL 57 | const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || request.nextUrl.origin; 58 | const redirectUri = `${baseUrl}/api/oauth2/callback`; 59 | const authUrl = new URL(OAUTH2_CONFIG.authorizeEndpoint); 60 | authUrl.searchParams.append('response_type', 'code'); 61 | authUrl.searchParams.append('client_id', OAUTH2_CONFIG.clientId); 62 | authUrl.searchParams.append('redirect_uri', redirectUri); 63 | authUrl.searchParams.append('state', state); 64 | authUrl.searchParams.append('scope', OAUTH2_CONFIG.scope); 65 | 66 | // 重定向到授权服务器 67 | return NextResponse.redirect(authUrl); 68 | } 69 | 70 | // 处理授权回调 71 | export async function POST(request: NextRequest) { 72 | try { 73 | const body = await request.json(); 74 | const { code } = body; 75 | 76 | if (!code) { 77 | return NextResponse.json({ error: 'Authorization code is required' }, { status: 400 }); 78 | } 79 | 80 | // 交换授权码获取访问令牌 81 | const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || request.nextUrl.origin; 82 | const redirectUri = `${baseUrl}/api/oauth2/callback`; 83 | const tokenResponse = await fetch(OAUTH2_CONFIG.tokenEndpoint, { 84 | method: 'POST', 85 | headers: { 86 | 'Content-Type': 'application/x-www-form-urlencoded', 87 | 'Accept': 'application/json' 88 | }, 89 | body: new URLSearchParams({ 90 | grant_type: 'authorization_code', 91 | client_id: OAUTH2_CONFIG.clientId, 92 | client_secret: OAUTH2_CONFIG.clientSecret, 93 | code, 94 | redirect_uri: redirectUri 95 | }) 96 | }); 97 | 98 | if (!tokenResponse.ok) { 99 | const errorData = await tokenResponse.json(); 100 | return NextResponse.json({ error: 'Failed to exchange token', details: errorData }, { status: 400 }); 101 | } 102 | 103 | const tokenData = await tokenResponse.json(); 104 | const { access_token, refresh_token, expires_in } = tokenData; 105 | 106 | // 获取用户信息 107 | const userInfoResponse = await fetch(OAUTH2_CONFIG.userInfoEndpoint, { 108 | headers: { 109 | 'Authorization': `Bearer ${access_token}` 110 | } 111 | }); 112 | 113 | if (!userInfoResponse.ok) { 114 | return NextResponse.json({ error: 'Failed to fetch user info' }, { status: 400 }); 115 | } 116 | 117 | const userData = await userInfoResponse.json(); 118 | 119 | const cookieStore = await cookies(); 120 | // 设置用户信息和令牌到cookie 121 | cookieStore.set('oauth_token', JSON.stringify({ 122 | access_token, 123 | refresh_token, 124 | expires_at: Date.now() + expires_in * 1000 125 | }), { 126 | httpOnly: true, 127 | secure: process.env.NODE_ENV === 'production', 128 | maxAge: expires_in, 129 | path: '/' 130 | }); 131 | 132 | // 用户公开信息存储在非httpOnly cookie中,以便客户端JavaScript访问 133 | cookieStore.set('oauth_user', JSON.stringify({ 134 | id: userData.id, 135 | name: userData.name || userData.username, 136 | username: userData.username, 137 | avatar: userData.avatar_url || userData.avatar 138 | }), { 139 | httpOnly: false, 140 | secure: process.env.NODE_ENV === 'production', 141 | maxAge: expires_in, 142 | path: '/' 143 | }); 144 | 145 | return NextResponse.json({ success: true, user: userData }); 146 | 147 | } catch (error) { 148 | console.error('OAuth error:', error); 149 | return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); 150 | } 151 | } -------------------------------------------------------------------------------- /app/api/topics/[topicId]/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server'; 2 | 3 | interface TopicResponse { 4 | id: number; 5 | title: string; 6 | created_at: string; 7 | user_id: number; 8 | category_id: number; 9 | tags: string[]; 10 | posts_count: number; 11 | views: number; 12 | like_count: number; 13 | last_posted_at: string; 14 | word_count: number; 15 | participant_count: number; 16 | details: { 17 | created_by: { 18 | username: string; 19 | avatar_template: string; 20 | }; 21 | }; 22 | } 23 | 24 | export async function GET( 25 | request: Request, 26 | { params }: { params: Promise<{ topicId: string }> } 27 | ): Promise { 28 | const { topicId } = await params; 29 | 30 | try { 31 | const response = await fetch(`https://linux.do/t/${topicId}.json`); 32 | 33 | if (!response.ok) { 34 | throw new Error(`HTTP error! status: ${response.status}`); 35 | } 36 | 37 | const data: TopicResponse = await response.json(); 38 | 39 | return NextResponse.json(data); 40 | } catch { 41 | return NextResponse.json( 42 | { error: 'Failed to fetch topic data' }, 43 | { status: 500 } 44 | ); 45 | } 46 | } -------------------------------------------------------------------------------- /app/api/users/[username]/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server'; 2 | 3 | interface UserResponse { 4 | user: { 5 | id: number; 6 | username: string; 7 | name: string; 8 | avatar_template: string; 9 | bio_excerpt: string; 10 | email?: string; 11 | trust_level: number; 12 | created_at: string; 13 | gamification_score?: number; 14 | title?: string; 15 | }; 16 | } 17 | 18 | export async function GET( 19 | request: NextRequest, 20 | { params }: { params: Promise<{ username: string }> } 21 | ): Promise { 22 | const { username } = await params; 23 | 24 | try { 25 | const response = await fetch(`https://linux.do/users/${username}.json`); 26 | const data: UserResponse = await response.json(); 27 | 28 | return NextResponse.json(data); 29 | } catch { 30 | return NextResponse.json( 31 | { error: 'Failed to fetch user data' }, 32 | { status: 500 } 33 | ); 34 | } 35 | } -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @import 'tailwindcss'; 2 | 3 | /* Optional: import Nextra theme styles */ 4 | @import 'nextra-theme-docs/style.css'; 5 | @import "tw-animate-css"; 6 | 7 | @custom-variant dark (&:is(.dark *)); /* or nextra-theme-blog/style.css */ 8 | 9 | @variant dark (&:where(.dark *)); 10 | 11 | @theme inline { 12 | 13 | --font-sans: var(--font-inter), var(--font-noto-sans-sc), system-ui, sans-serif; 14 | 15 | --font-mono: var(--font-inter), var(--font-noto-sans-sc), monospace; 16 | 17 | --radius-sm: calc(var(--radius) - 4px); 18 | 19 | --radius-md: calc(var(--radius) - 2px); 20 | 21 | --radius-lg: var(--radius); 22 | 23 | --radius-xl: calc(var(--radius) + 4px); 24 | 25 | --color-background: var(--background); 26 | 27 | --color-foreground: var(--foreground); 28 | 29 | --color-card: var(--card); 30 | 31 | --color-card-foreground: var(--card-foreground); 32 | 33 | --color-popover: var(--popover); 34 | 35 | --color-popover-foreground: var(--popover-foreground); 36 | 37 | --color-primary: var(--primary); 38 | 39 | --color-primary-foreground: var(--primary-foreground); 40 | 41 | --color-secondary: var(--secondary); 42 | 43 | --color-secondary-foreground: var(--secondary-foreground); 44 | 45 | --color-muted: var(--muted); 46 | 47 | --color-muted-foreground: var(--muted-foreground); 48 | 49 | --color-accent: var(--accent); 50 | 51 | --color-accent-foreground: var(--accent-foreground); 52 | 53 | --color-destructive: var(--destructive); 54 | 55 | --color-border: var(--border); 56 | 57 | --color-input: var(--input); 58 | 59 | --color-ring: var(--ring); 60 | 61 | --color-chart-1: var(--chart-1); 62 | 63 | --color-chart-2: var(--chart-2); 64 | 65 | --color-chart-3: var(--chart-3); 66 | 67 | --color-chart-4: var(--chart-4); 68 | 69 | --color-chart-5: var(--chart-5); 70 | 71 | --color-sidebar: var(--sidebar); 72 | 73 | --color-sidebar-foreground: var(--sidebar-foreground); 74 | 75 | --color-sidebar-primary: var(--sidebar-primary); 76 | 77 | --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); 78 | 79 | --color-sidebar-accent: var(--sidebar-accent); 80 | 81 | --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); 82 | 83 | --color-sidebar-border: var(--sidebar-border); 84 | 85 | --color-sidebar-ring: var(--sidebar-ring); 86 | } 87 | 88 | :root { 89 | 90 | --radius: 0.625rem; 91 | 92 | --background: oklch(1 0 0); 93 | 94 | --foreground: oklch(0.141 0.005 285.823); 95 | 96 | --card: oklch(1 0 0); 97 | 98 | --card-foreground: oklch(0.141 0.005 285.823); 99 | 100 | --popover: oklch(1 0 0); 101 | 102 | --popover-foreground: oklch(0.141 0.005 285.823); 103 | 104 | --primary: oklch(0.21 0.006 285.885); 105 | 106 | --primary-foreground: oklch(0.985 0 0); 107 | 108 | --secondary: oklch(0.967 0.001 286.375); 109 | 110 | --secondary-foreground: oklch(0.21 0.006 285.885); 111 | 112 | --muted: oklch(0.967 0.001 286.375); 113 | 114 | --muted-foreground: oklch(0.552 0.016 285.938); 115 | 116 | --accent: oklch(0.967 0.001 286.375); 117 | 118 | --accent-foreground: oklch(0.21 0.006 285.885); 119 | 120 | --destructive: oklch(0.577 0.245 27.325); 121 | 122 | --border: oklch(0.92 0.004 286.32); 123 | 124 | --input: oklch(0.92 0.004 286.32); 125 | 126 | --ring: oklch(0.705 0.015 286.067); 127 | 128 | --chart-1: oklch(0.646 0.222 41.116); 129 | 130 | --chart-2: oklch(0.6 0.118 184.704); 131 | 132 | --chart-3: oklch(0.398 0.07 227.392); 133 | 134 | --chart-4: oklch(0.828 0.189 84.429); 135 | 136 | --chart-5: oklch(0.769 0.188 70.08); 137 | 138 | --sidebar: oklch(0.985 0 0); 139 | 140 | --sidebar-foreground: oklch(0.141 0.005 285.823); 141 | 142 | --sidebar-primary: oklch(0.21 0.006 285.885); 143 | 144 | --sidebar-primary-foreground: oklch(0.985 0 0); 145 | 146 | --sidebar-accent: oklch(0.967 0.001 286.375); 147 | 148 | --sidebar-accent-foreground: oklch(0.21 0.006 285.885); 149 | 150 | --sidebar-border: oklch(0.92 0.004 286.32); 151 | 152 | --sidebar-ring: oklch(0.705 0.015 286.067); 153 | } 154 | 155 | .dark { 156 | 157 | --background: oklch(0.141 0.005 285.823); 158 | 159 | --foreground: oklch(0.985 0 0); 160 | 161 | --card: oklch(0.21 0.006 285.885); 162 | 163 | --card-foreground: oklch(0.985 0 0); 164 | 165 | --popover: oklch(0.21 0.006 285.885); 166 | 167 | --popover-foreground: oklch(0.985 0 0); 168 | 169 | --primary: oklch(0.92 0.004 286.32); 170 | 171 | --primary-foreground: oklch(0.21 0.006 285.885); 172 | 173 | --secondary: oklch(0.274 0.006 286.033); 174 | 175 | --secondary-foreground: oklch(0.985 0 0); 176 | 177 | --muted: oklch(0.274 0.006 286.033); 178 | 179 | --muted-foreground: oklch(0.705 0.015 286.067); 180 | 181 | --accent: oklch(0.274 0.006 286.033); 182 | 183 | --accent-foreground: oklch(0.985 0 0); 184 | 185 | --destructive: oklch(0.704 0.191 22.216); 186 | 187 | --border: oklch(1 0 0 / 10%); 188 | 189 | --input: oklch(1 0 0 / 15%); 190 | 191 | --ring: oklch(0.552 0.016 285.938); 192 | 193 | --chart-1: oklch(0.488 0.243 264.376); 194 | 195 | --chart-2: oklch(0.696 0.17 162.48); 196 | 197 | --chart-3: oklch(0.769 0.188 70.08); 198 | 199 | --chart-4: oklch(0.627 0.265 303.9); 200 | 201 | --chart-5: oklch(0.645 0.246 16.439); 202 | 203 | --sidebar: oklch(0.21 0.006 285.885); 204 | 205 | --sidebar-foreground: oklch(0.985 0 0); 206 | 207 | --sidebar-primary: oklch(0.488 0.243 264.376); 208 | 209 | --sidebar-primary-foreground: oklch(0.985 0 0); 210 | 211 | --sidebar-accent: oklch(0.274 0.006 286.033); 212 | 213 | --sidebar-accent-foreground: oklch(0.985 0 0); 214 | 215 | --sidebar-border: oklch(1 0 0 / 10%); 216 | 217 | --sidebar-ring: oklch(0.552 0.016 285.938); 218 | } 219 | 220 | @layer base { 221 | * { 222 | @apply border-border outline-ring/50; 223 | } 224 | body { 225 | @apply text-foreground; 226 | } 227 | } -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { getPageMap } from 'nextra/page-map' 2 | import { Head, Search } from 'nextra/components' 3 | import { LastUpdated } from 'nextra-theme-docs' 4 | import { Footer, Layout, Navbar } from 'nextra-theme-docs' 5 | 6 | import './globals.css' 7 | import Link from 'next/link' 8 | import Image from 'next/image' 9 | import {Inter, Noto_Sans_SC} from 'next/font/google'; 10 | import { ScrollProgress } from '@/components/animate-ui/components/scroll-progress'; 11 | import NavbarOAuthButton from '@/components/common/NavbarOAuthButton'; 12 | 13 | export const metadata = { 14 | title: 'Linux Do Wiki', 15 | description: 'Linux Do Wiki', 16 | icons: { 17 | icon: '/favicon.ico', 18 | }, 19 | } 20 | 21 | const inter = Inter({ 22 | variable: '--font-inter', 23 | subsets: ['latin'], 24 | display: 'swap', 25 | }); 26 | 27 | const notoSansSC = Noto_Sans_SC({ 28 | variable: '--font-noto-sans-sc', 29 | subsets: ['latin'], 30 | display: 'swap', 31 | weight: ['300', '400', '500', '600', '700'], 32 | }); 33 | 34 | // 导航栏 35 | const navbar = ( 36 | 39 | Linux Do Wiki 40 | LINUX DO WIKI 41 | 42 | } 43 | > 44 | 45 | 46 | ) 47 | 48 | // 搜索 49 | const search = ( 50 | 56 | ) 57 | 58 | // 页脚 59 | const footer =
60 |
61 |
Powered by 62 | @Chenyme 63 |
64 |
{new Date().getFullYear()} © Linux Do Wiki.
65 |
66 |
67 | 68 | export default async function RootLayout({ children }: { children: React.ReactNode }) { 69 | return ( 70 | 76 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 90 | {/* 全局滚动进度条 */} 91 | 92 | 最近更新时间:} 107 | themeSwitch={{ 108 | dark: '深色模式', 109 | light: '浅色模式', 110 | system: '跟随系统', 111 | }} 112 | toc={{ 113 | backToTop: '返回顶部', 114 | title: '在此页中', 115 | }} 116 | > 117 | {children} 118 | 119 | 120 | 121 | ) 122 | } -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "", 8 | "css": "app/globals.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } -------------------------------------------------------------------------------- /components/animate-ui/buttons/copy.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import { AnimatePresence, HTMLMotionProps, motion } from 'motion/react'; 5 | import { CheckIcon, CopyIcon } from 'lucide-react'; 6 | import { cva, type VariantProps } from 'class-variance-authority'; 7 | 8 | import { cn } from '@/lib/utils'; 9 | 10 | const buttonVariants = cva( 11 | 'inline-flex items-center justify-center cursor-pointer rounded-md transition-colors disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive', 12 | { 13 | variants: { 14 | variant: { 15 | default: 16 | 'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90', 17 | muted: 'bg-muted text-muted-foreground', 18 | destructive: 19 | 'bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60', 20 | outline: 21 | 'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50', 22 | secondary: 23 | 'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80', 24 | ghost: 25 | 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50', 26 | }, 27 | size: { 28 | default: 'size-8 rounded-lg [&_svg]:size-4', 29 | sm: 'size-6 [&_svg]:size-3', 30 | md: 'size-10 rounded-lg [&_svg]:size-5', 31 | lg: 'size-12 rounded-xl [&_svg]:size-6', 32 | }, 33 | }, 34 | defaultVariants: { 35 | variant: 'default', 36 | size: 'default', 37 | }, 38 | }, 39 | ); 40 | 41 | type CopyButtonProps = Omit, 'children' | 'onCopy'> & 42 | VariantProps & { 43 | content?: string; 44 | delay?: number; 45 | onCopy?: (content: string) => void; 46 | isCopied?: boolean; 47 | onCopyChange?: (isCopied: boolean) => void; 48 | }; 49 | 50 | function CopyButton({ 51 | content, 52 | className, 53 | size, 54 | variant, 55 | delay = 3000, 56 | onClick, 57 | onCopy, 58 | isCopied, 59 | onCopyChange, 60 | ...props 61 | }: CopyButtonProps) { 62 | const [localIsCopied, setLocalIsCopied] = React.useState(isCopied ?? false); 63 | const Icon = localIsCopied ? CheckIcon : CopyIcon; 64 | 65 | React.useEffect(() => { 66 | setLocalIsCopied(isCopied ?? false); 67 | }, [isCopied]); 68 | 69 | const handleIsCopied = React.useCallback( 70 | (isCopied: boolean) => { 71 | setLocalIsCopied(isCopied); 72 | onCopyChange?.(isCopied); 73 | }, 74 | [onCopyChange], 75 | ); 76 | 77 | const handleCopy = React.useCallback( 78 | (e: React.MouseEvent) => { 79 | if (isCopied) return; 80 | if (content) { 81 | navigator.clipboard 82 | .writeText(content) 83 | .then(() => { 84 | handleIsCopied(true); 85 | setTimeout(() => handleIsCopied(false), delay); 86 | onCopy?.(content); 87 | }) 88 | .catch((error) => { 89 | console.error('Error copying command', error); 90 | }); 91 | } 92 | onClick?.(e); 93 | }, 94 | [isCopied, content, delay, onClick, onCopy, handleIsCopied], 95 | ); 96 | 97 | return ( 98 | 106 | 107 | 115 | 116 | 117 | 118 | 119 | ); 120 | } 121 | 122 | export { CopyButton, buttonVariants, type CopyButtonProps }; 123 | -------------------------------------------------------------------------------- /components/animate-ui/buttons/flip.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import { 5 | type HTMLMotionProps, 6 | type Transition, 7 | type Variant, 8 | motion, 9 | } from 'motion/react'; 10 | 11 | import { cn } from '@/lib/utils'; 12 | 13 | type FlipDirection = 'top' | 'bottom' | 'left' | 'right'; 14 | 15 | type FlipButtonProps = HTMLMotionProps<'button'> & { 16 | frontText: string; 17 | backText: string; 18 | transition?: Transition; 19 | frontClassName?: string; 20 | backClassName?: string; 21 | from?: FlipDirection; 22 | }; 23 | 24 | const DEFAULT_SPAN_CLASS_NAME = 25 | 'absolute inset-0 flex items-center justify-center rounded-lg'; 26 | 27 | function FlipButton({ 28 | frontText, 29 | backText, 30 | transition = { type: 'spring', stiffness: 280, damping: 20 }, 31 | className, 32 | frontClassName, 33 | backClassName, 34 | from = 'top', 35 | ...props 36 | }: FlipButtonProps) { 37 | const isVertical = from === 'top' || from === 'bottom'; 38 | const rotateAxis = isVertical ? 'rotateX' : 'rotateY'; 39 | 40 | const frontOffset = from === 'top' || from === 'left' ? '50%' : '-50%'; 41 | const backOffset = from === 'top' || from === 'left' ? '-50%' : '50%'; 42 | 43 | const buildVariant = ( 44 | opacity: number, 45 | rotation: number, 46 | offset: string | null = null, 47 | ): Variant => ({ 48 | opacity, 49 | [rotateAxis]: rotation, 50 | ...(isVertical && offset !== null ? { y: offset } : {}), 51 | ...(!isVertical && offset !== null ? { x: offset } : {}), 52 | }); 53 | 54 | const frontVariants = { 55 | initial: buildVariant(1, 0, '0%'), 56 | hover: buildVariant(0, 90, frontOffset), 57 | }; 58 | 59 | const backVariants = { 60 | initial: buildVariant(0, 90, backOffset), 61 | hover: buildVariant(1, 0, '0%'), 62 | }; 63 | 64 | return ( 65 | 76 | 86 | {frontText} 87 | 88 | 98 | {backText} 99 | 100 | {frontText} 101 | 102 | ); 103 | } 104 | 105 | export { FlipButton, type FlipButtonProps, type FlipDirection }; 106 | -------------------------------------------------------------------------------- /components/animate-ui/components/avatar-group.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import { motion, type Transition } from 'motion/react'; 5 | 6 | import { cn } from '@/lib/utils'; 7 | import { 8 | Tooltip, 9 | TooltipContent, 10 | TooltipProvider, 11 | TooltipTrigger, 12 | type TooltipProps, 13 | type TooltipContentProps, 14 | } from '@/components/animate-ui/components/tooltip'; 15 | 16 | type AvatarProps = TooltipProps & { 17 | children: React.ReactNode; 18 | zIndex: number; 19 | transition: Transition; 20 | translate: string | number; 21 | }; 22 | 23 | function AvatarContainer({ 24 | children, 25 | zIndex, 26 | transition, 27 | translate, 28 | ...props 29 | }: AvatarProps) { 30 | return ( 31 | 32 | 33 | 41 | 48 | {children} 49 | 50 | 51 | 52 | 53 | ); 54 | } 55 | 56 | type AvatarGroupTooltipProps = TooltipContentProps; 57 | 58 | function AvatarGroupTooltip(props: AvatarGroupTooltipProps) { 59 | return ; 60 | } 61 | 62 | type AvatarGroupProps = Omit, 'translate'> & { 63 | children: React.ReactElement[]; 64 | transition?: Transition; 65 | invertOverlap?: boolean; 66 | translate?: string | number; 67 | tooltipProps?: Omit; 68 | }; 69 | 70 | function AvatarGroup({ 71 | ref, 72 | children, 73 | className, 74 | transition = { type: 'spring', stiffness: 300, damping: 17 }, 75 | invertOverlap = false, 76 | translate = '-30%', 77 | tooltipProps = { side: 'top', sideOffset: 24 }, 78 | ...props 79 | }: AvatarGroupProps) { 80 | return ( 81 | 82 |
88 | {children?.map((child, index) => ( 89 | 98 | {child} 99 | 100 | ))} 101 |
102 |
103 | ); 104 | } 105 | 106 | export { 107 | AvatarGroup, 108 | AvatarGroupTooltip, 109 | type AvatarGroupProps, 110 | type AvatarGroupTooltipProps, 111 | }; 112 | -------------------------------------------------------------------------------- /components/animate-ui/components/code-tabs.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import { useTheme } from 'next-themes'; 5 | 6 | import { cn } from '@/lib/utils'; 7 | import { 8 | Tabs, 9 | TabsContent, 10 | TabsList, 11 | TabsTrigger, 12 | TabsContents, 13 | type TabsProps, 14 | } from '@/components/animate-ui/components/tabs'; 15 | import { CopyButton } from '@/components/animate-ui/buttons/copy'; 16 | 17 | type CodeTabsProps = { 18 | codes: Record; 19 | lang?: string; 20 | themes?: { 21 | light: string; 22 | dark: string; 23 | }; 24 | copyButton?: boolean; 25 | onCopy?: (content: string) => void; 26 | } & Omit; 27 | 28 | function CodeTabs({ 29 | codes, 30 | lang = 'bash', 31 | themes = { 32 | light: 'github-light', 33 | dark: 'github-dark', 34 | }, 35 | className, 36 | defaultValue, 37 | value, 38 | onValueChange, 39 | copyButton = true, 40 | onCopy, 41 | ...props 42 | }: CodeTabsProps) { 43 | const { resolvedTheme } = useTheme(); 44 | 45 | const [highlightedCodes, setHighlightedCodes] = React.useState | null>(null); 49 | const [selectedCode, setSelectedCode] = React.useState( 50 | value ?? defaultValue ?? Object.keys(codes)[0] ?? '', 51 | ); 52 | 53 | React.useEffect(() => { 54 | async function loadHighlightedCode() { 55 | try { 56 | const { codeToHtml } = await import('shiki'); 57 | const newHighlightedCodes: Record = {}; 58 | 59 | for (const [command, val] of Object.entries(codes)) { 60 | const highlighted = await codeToHtml(val, { 61 | lang, 62 | themes: { 63 | light: themes.light, 64 | dark: themes.dark, 65 | }, 66 | defaultColor: resolvedTheme === 'dark' ? 'dark' : 'light', 67 | }); 68 | 69 | newHighlightedCodes[command] = highlighted; 70 | } 71 | 72 | setHighlightedCodes(newHighlightedCodes); 73 | } catch (error) { 74 | console.error('Error highlighting codes', error); 75 | setHighlightedCodes(codes); 76 | } 77 | } 78 | loadHighlightedCode(); 79 | }, [resolvedTheme, lang, themes.light, themes.dark, codes]); 80 | 81 | return ( 82 | { 91 | setSelectedCode(val); 92 | onValueChange?.(val); 93 | }} 94 | > 95 | 100 |
101 | {highlightedCodes && 102 | Object.keys(highlightedCodes).map((code) => ( 103 | 108 | {code} 109 | 110 | ))} 111 |
112 | 113 | {copyButton && highlightedCodes && ( 114 | 121 | )} 122 |
123 | 124 | {highlightedCodes && 125 | Object.entries(highlightedCodes).map(([code, val]) => ( 126 | 132 |
136 | 137 | ))} 138 | 139 | 140 | ); 141 | } 142 | 143 | export { CodeTabs, type CodeTabsProps }; 144 | -------------------------------------------------------------------------------- /components/animate-ui/components/scroll-progress.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import { 5 | motion, 6 | useScroll, 7 | useSpring, 8 | type HTMLMotionProps, 9 | } from 'motion/react'; 10 | 11 | import { cn } from '@/lib/utils'; 12 | 13 | type ScrollProgressProps = React.ComponentProps<'div'> & { 14 | progressProps?: HTMLMotionProps<'div'>; 15 | }; 16 | 17 | function ScrollProgress({ 18 | ref, 19 | className, 20 | children, 21 | progressProps, 22 | ...props 23 | }: ScrollProgressProps) { 24 | const containerRef = React.useRef(null); 25 | React.useImperativeHandle(ref, () => containerRef.current as HTMLDivElement); 26 | 27 | const { scrollYProgress } = useScroll( 28 | children ? { container: containerRef } : undefined, 29 | ); 30 | 31 | const scaleX = useSpring(scrollYProgress, { 32 | stiffness: 250, 33 | damping: 40, 34 | bounce: 0, 35 | }); 36 | 37 | return ( 38 | <> 39 | 48 | {containerRef && ( 49 |
55 | {children} 56 |
57 | )} 58 | 59 | ); 60 | } 61 | 62 | export { ScrollProgress, type ScrollProgressProps }; 63 | -------------------------------------------------------------------------------- /components/animate-ui/components/tabs.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import { motion, type Transition, type HTMLMotionProps } from 'motion/react'; 5 | 6 | import { cn } from '@/lib/utils'; 7 | import { 8 | MotionHighlight, 9 | MotionHighlightItem, 10 | } from '@/components/animate-ui/effects/motion-highlight'; 11 | 12 | type TabsContextType = { 13 | activeValue: T; 14 | handleValueChange: (value: T) => void; 15 | registerTrigger: (value: T, node: HTMLElement | null) => void; 16 | }; 17 | 18 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 19 | const TabsContext = React.createContext | undefined>( 20 | undefined, 21 | ); 22 | 23 | function useTabs(): TabsContextType { 24 | const context = React.useContext(TabsContext); 25 | if (!context) { 26 | throw new Error('useTabs must be used within a TabsProvider'); 27 | } 28 | return context; 29 | } 30 | 31 | type BaseTabsProps = React.ComponentProps<'div'> & { 32 | children: React.ReactNode; 33 | }; 34 | 35 | type UnControlledTabsProps = BaseTabsProps & { 36 | defaultValue?: T; 37 | value?: never; 38 | onValueChange?: never; 39 | }; 40 | 41 | type ControlledTabsProps = BaseTabsProps & { 42 | value: T; 43 | onValueChange?: (value: T) => void; 44 | defaultValue?: never; 45 | }; 46 | 47 | type TabsProps = 48 | | UnControlledTabsProps 49 | | ControlledTabsProps; 50 | 51 | function Tabs({ 52 | defaultValue, 53 | value, 54 | onValueChange, 55 | children, 56 | className, 57 | ...props 58 | }: TabsProps) { 59 | const [activeValue, setActiveValue] = React.useState( 60 | defaultValue ?? undefined, 61 | ); 62 | const triggersRef = React.useRef(new Map()); 63 | const initialSet = React.useRef(false); 64 | const isControlled = value !== undefined; 65 | 66 | React.useEffect(() => { 67 | if ( 68 | !isControlled && 69 | activeValue === undefined && 70 | triggersRef.current.size > 0 && 71 | !initialSet.current 72 | ) { 73 | const firstTab = Array.from(triggersRef.current.keys())[0]; 74 | setActiveValue(firstTab as T); 75 | initialSet.current = true; 76 | } 77 | }, [activeValue, isControlled]); 78 | 79 | const registerTrigger = (value: string, node: HTMLElement | null) => { 80 | if (node) { 81 | triggersRef.current.set(value, node); 82 | if (!isControlled && activeValue === undefined && !initialSet.current) { 83 | setActiveValue(value as T); 84 | initialSet.current = true; 85 | } 86 | } else { 87 | triggersRef.current.delete(value); 88 | } 89 | }; 90 | 91 | const handleValueChange = (val: T) => { 92 | if (!isControlled) setActiveValue(val); 93 | else onValueChange?.(val); 94 | }; 95 | 96 | return ( 97 | 104 |
109 | {children} 110 |
111 |
112 | ); 113 | } 114 | 115 | type TabsListProps = React.ComponentProps<'div'> & { 116 | children: React.ReactNode; 117 | activeClassName?: string; 118 | transition?: Transition; 119 | }; 120 | 121 | function TabsList({ 122 | children, 123 | className, 124 | activeClassName, 125 | transition = { 126 | type: 'spring', 127 | stiffness: 200, 128 | damping: 25, 129 | }, 130 | ...props 131 | }: TabsListProps) { 132 | const { activeValue } = useTabs(); 133 | 134 | return ( 135 | 141 |
150 | {children} 151 |
152 |
153 | ); 154 | } 155 | 156 | type TabsTriggerProps = HTMLMotionProps<'button'> & { 157 | value: string; 158 | children: React.ReactNode; 159 | }; 160 | 161 | function TabsTrigger({ 162 | ref, 163 | value, 164 | children, 165 | className, 166 | ...props 167 | }: TabsTriggerProps) { 168 | const { activeValue, handleValueChange, registerTrigger } = useTabs(); 169 | 170 | const localRef = React.useRef(null); 171 | React.useImperativeHandle(ref, () => localRef.current as HTMLButtonElement); 172 | 173 | React.useEffect(() => { 174 | registerTrigger(value, localRef.current); 175 | return () => registerTrigger(value, null); 176 | }, [value, registerTrigger]); 177 | 178 | return ( 179 | 180 | handleValueChange(value)} 186 | data-state={activeValue === value ? 'active' : 'inactive'} 187 | className={cn( 188 | 'inline-flex cursor-pointer items-center size-full justify-center whitespace-nowrap rounded-sm px-2 py-1 text-sm font-medium ring-offset-background transition-transform focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:text-foreground z-[1]', 189 | className, 190 | )} 191 | {...props} 192 | > 193 | {children} 194 | 195 | 196 | ); 197 | } 198 | 199 | type TabsContentsProps = React.ComponentProps<'div'> & { 200 | children: React.ReactNode; 201 | transition?: Transition; 202 | }; 203 | 204 | function TabsContents({ 205 | children, 206 | className, 207 | transition = { 208 | type: 'spring', 209 | stiffness: 300, 210 | damping: 30, 211 | bounce: 0, 212 | restDelta: 0.01, 213 | }, 214 | ...props 215 | }: TabsContentsProps) { 216 | const { activeValue } = useTabs(); 217 | const childrenArray = React.Children.toArray(children); 218 | const activeIndex = childrenArray.findIndex( 219 | (child): child is React.ReactElement<{ value: string }> => 220 | React.isValidElement(child) && 221 | typeof child.props === 'object' && 222 | child.props !== null && 223 | 'value' in child.props && 224 | child.props.value === activeValue, 225 | ); 226 | 227 | return ( 228 |
233 | 238 | {childrenArray.map((child, index) => ( 239 |
240 | {child} 241 |
242 | ))} 243 |
244 |
245 | ); 246 | } 247 | 248 | type TabsContentProps = HTMLMotionProps<'div'> & { 249 | value: string; 250 | children: React.ReactNode; 251 | }; 252 | 253 | function TabsContent({ 254 | children, 255 | value, 256 | className, 257 | ...props 258 | }: TabsContentProps) { 259 | const { activeValue } = useTabs(); 260 | const isActive = activeValue === value; 261 | return ( 262 | 272 | {children} 273 | 274 | ); 275 | } 276 | 277 | export { 278 | Tabs, 279 | TabsList, 280 | TabsTrigger, 281 | TabsContents, 282 | TabsContent, 283 | useTabs, 284 | type TabsContextType, 285 | type TabsProps, 286 | type TabsListProps, 287 | type TabsTriggerProps, 288 | type TabsContentsProps, 289 | type TabsContentProps, 290 | }; 291 | -------------------------------------------------------------------------------- /components/animate-ui/icons/gavel.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import { motion, type Variants } from 'motion/react'; 5 | 6 | import { 7 | getVariants, 8 | useAnimateIconContext, 9 | IconWrapper, 10 | type IconProps, 11 | } from '@/components/animate-ui/icons/icon'; 12 | 13 | type GavelProps = IconProps; 14 | 15 | const animations = { 16 | default: { 17 | group: { 18 | initial: { 19 | rotate: 0, 20 | }, 21 | animate: { 22 | transformOrigin: 'bottom left', 23 | rotate: [0, 30, -5, 0], 24 | }, 25 | }, 26 | path1: {}, 27 | path2: {}, 28 | path3: {}, 29 | } satisfies Record, 30 | } as const; 31 | 32 | function IconComponent({ size, ...props }: GavelProps) { 33 | const { controls } = useAnimateIconContext(); 34 | const variants = getVariants(animations); 35 | 36 | return ( 37 | 52 | 58 | 64 | 70 | 76 | 82 | 83 | ); 84 | } 85 | 86 | function Gavel(props: GavelProps) { 87 | return ; 88 | } 89 | 90 | export { 91 | animations, 92 | Gavel, 93 | Gavel as GavelIcon, 94 | type GavelProps, 95 | type GavelProps as GavelIconProps, 96 | }; 97 | -------------------------------------------------------------------------------- /components/animate-ui/icons/icon.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import { 5 | SVGMotionProps, 6 | useAnimation, 7 | type AnimationControls, 8 | type Variants, 9 | } from 'motion/react'; 10 | 11 | import { cn } from '@/lib/utils'; 12 | 13 | const staticAnimations = { 14 | path: { 15 | initial: { pathLength: 1, opacity: 1 }, 16 | animate: { 17 | pathLength: [0.05, 1], 18 | opacity: [0, 1], 19 | transition: { 20 | duration: 0.8, 21 | ease: 'easeInOut', 22 | opacity: { duration: 0.01 }, 23 | }, 24 | }, 25 | } as Variants, 26 | 'path-loop': { 27 | initial: { pathLength: 1, opacity: 1 }, 28 | animate: { 29 | pathLength: [1, 0.05, 1], 30 | opacity: [1, 0, 1], 31 | transition: { 32 | duration: 1.6, 33 | ease: 'easeInOut', 34 | opacity: { duration: 0.01 }, 35 | }, 36 | }, 37 | } as Variants, 38 | } as const; 39 | 40 | type StaticAnimations = keyof typeof staticAnimations; 41 | type TriggerProp = boolean | StaticAnimations | T; 42 | 43 | interface AnimateIconContextValue { 44 | controls: AnimationControls | undefined; 45 | animation: StaticAnimations | string; 46 | loop: boolean; 47 | loopDelay: number; 48 | } 49 | 50 | interface DefaultIconProps { 51 | animate?: TriggerProp; 52 | onAnimateChange?: ( 53 | value: boolean, 54 | animation: StaticAnimations | string, 55 | ) => void; 56 | animateOnHover?: TriggerProp; 57 | animateOnTap?: TriggerProp; 58 | animation?: T | StaticAnimations; 59 | loop?: boolean; 60 | loopDelay?: number; 61 | onAnimateStart?: () => void; 62 | onAnimateEnd?: () => void; 63 | } 64 | 65 | interface AnimateIconProps extends DefaultIconProps { 66 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 67 | children: React.ReactElement; 68 | } 69 | 70 | interface IconProps 71 | extends DefaultIconProps, 72 | Omit< 73 | SVGMotionProps, 74 | 'animate' | 'onAnimationStart' | 'onAnimationEnd' 75 | > { 76 | size?: number; 77 | } 78 | 79 | interface IconWrapperProps extends IconProps { 80 | icon: React.ComponentType>; 81 | } 82 | 83 | const AnimateIconContext = React.createContext( 84 | null, 85 | ); 86 | 87 | function useAnimateIconContext() { 88 | const context = React.useContext(AnimateIconContext); 89 | if (!context) 90 | return { 91 | controls: undefined, 92 | animation: 'default', 93 | loop: false, 94 | loopDelay: 0, 95 | }; 96 | return context; 97 | } 98 | 99 | function AnimateIcon({ 100 | animate, 101 | onAnimateChange, 102 | animateOnHover, 103 | animateOnTap, 104 | animation = 'default', 105 | loop = false, 106 | loopDelay = 0, 107 | onAnimateStart, 108 | onAnimateEnd, 109 | children, 110 | }: AnimateIconProps) { 111 | const controls = useAnimation(); 112 | const [localAnimate, setLocalAnimate] = React.useState(!!animate); 113 | const currentAnimation = React.useRef(animation); 114 | 115 | const startAnimation = React.useCallback( 116 | (trigger: TriggerProp) => { 117 | currentAnimation.current = 118 | typeof trigger === 'string' ? trigger : animation; 119 | setLocalAnimate(true); 120 | }, 121 | [animation], 122 | ); 123 | 124 | const stopAnimation = React.useCallback(() => { 125 | setLocalAnimate(false); 126 | }, []); 127 | 128 | React.useEffect(() => { 129 | currentAnimation.current = 130 | typeof animate === 'string' ? animate : animation; 131 | setLocalAnimate(!!animate); 132 | // eslint-disable-next-line react-hooks/exhaustive-deps 133 | }, [animate]); 134 | 135 | React.useEffect( 136 | () => onAnimateChange?.(localAnimate, currentAnimation.current), 137 | [localAnimate, onAnimateChange], 138 | ); 139 | 140 | React.useEffect(() => { 141 | if (localAnimate) onAnimateStart?.(); 142 | controls.start(localAnimate ? 'animate' : 'initial').then(() => { 143 | if (localAnimate) onAnimateEnd?.(); 144 | }); 145 | }, [localAnimate, controls, onAnimateStart, onAnimateEnd]); 146 | 147 | const handleMouseEnter = (e: MouseEvent) => { 148 | if (animateOnHover) startAnimation(animateOnHover); 149 | children.props?.onMouseEnter?.(e); 150 | }; 151 | const handleMouseLeave = (e: MouseEvent) => { 152 | if (animateOnHover || animateOnTap) stopAnimation(); 153 | children.props?.onMouseLeave?.(e); 154 | }; 155 | const handlePointerDown = (e: PointerEvent) => { 156 | if (animateOnTap) startAnimation(animateOnTap); 157 | children.props?.onPointerDown?.(e); 158 | }; 159 | const handlePointerUp = (e: PointerEvent) => { 160 | if (animateOnTap) stopAnimation(); 161 | children.props?.onPointerUp?.(e); 162 | }; 163 | 164 | const child = React.Children.only(children); 165 | const cloned = React.cloneElement(child, { 166 | onMouseEnter: handleMouseEnter, 167 | onMouseLeave: handleMouseLeave, 168 | onPointerDown: handlePointerDown, 169 | onPointerUp: handlePointerUp, 170 | }); 171 | 172 | return ( 173 | 181 | {cloned} 182 | 183 | ); 184 | } 185 | 186 | const pathClassName = 187 | "[&_[stroke-dasharray='1px_1px']]:![stroke-dasharray:1px_0px]"; 188 | 189 | function IconWrapper({ 190 | size = 28, 191 | animation: animationProp, 192 | animate, 193 | onAnimateChange, 194 | animateOnHover = false, 195 | animateOnTap = false, 196 | icon: IconComponent, 197 | loop = false, 198 | loopDelay = 0, 199 | onAnimateStart, 200 | onAnimateEnd, 201 | className, 202 | ...props 203 | }: IconWrapperProps) { 204 | const context = React.useContext(AnimateIconContext); 205 | 206 | if (context) { 207 | const { 208 | controls, 209 | animation: parentAnimation, 210 | loop: parentLoop, 211 | loopDelay: parentLoopDelay, 212 | } = context; 213 | const animationToUse = animationProp ?? parentAnimation; 214 | const loopToUse = loop || parentLoop; 215 | const loopDelayToUse = loopDelay || parentLoopDelay; 216 | 217 | return ( 218 | 226 | 235 | 236 | ); 237 | } 238 | 239 | if ( 240 | animate !== undefined || 241 | onAnimateChange !== undefined || 242 | animateOnHover || 243 | animateOnTap || 244 | animationProp 245 | ) { 246 | return ( 247 | 258 | 267 | 268 | ); 269 | } 270 | 271 | return ( 272 | 281 | ); 282 | } 283 | 284 | function getVariants< 285 | V extends { default: T; [key: string]: T }, 286 | T extends Record, 287 | >(animations: V): T { 288 | // eslint-disable-next-line react-hooks/rules-of-hooks 289 | const { animation: animationType, loop, loopDelay } = useAnimateIconContext(); 290 | 291 | let result: T; 292 | 293 | if (animationType in staticAnimations) { 294 | const variant = staticAnimations[animationType as StaticAnimations]; 295 | result = {} as T; 296 | for (const key in animations.default) { 297 | if ( 298 | (animationType === 'path' || animationType === 'path-loop') && 299 | key.includes('group') 300 | ) 301 | continue; 302 | result[key] = variant as T[Extract]; 303 | } 304 | } else { 305 | result = (animations[animationType as keyof V] as T) ?? animations.default; 306 | } 307 | 308 | if (loop) { 309 | for (const key in result) { 310 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 311 | const state = result[key] as any; 312 | const transition = state.animate?.transition; 313 | if (!transition) continue; 314 | 315 | const hasNestedKeys = Object.values(transition).some( 316 | (v) => 317 | typeof v === 'object' && 318 | v !== null && 319 | ('ease' in v || 'duration' in v || 'times' in v), 320 | ); 321 | 322 | if (hasNestedKeys) { 323 | for (const prop in transition) { 324 | const subTrans = transition[prop]; 325 | if (typeof subTrans === 'object' && subTrans !== null) { 326 | transition[prop] = { 327 | ...subTrans, 328 | repeat: Infinity, 329 | repeatType: 'loop', 330 | repeatDelay: loopDelay, 331 | }; 332 | } 333 | } 334 | } else { 335 | state.animate.transition = { 336 | ...transition, 337 | repeat: Infinity, 338 | repeatType: 'loop', 339 | repeatDelay: loopDelay, 340 | }; 341 | } 342 | } 343 | } 344 | 345 | return result; 346 | } 347 | 348 | export { 349 | pathClassName, 350 | staticAnimations, 351 | AnimateIcon, 352 | IconWrapper, 353 | useAnimateIconContext, 354 | getVariants, 355 | type IconProps, 356 | type IconWrapperProps, 357 | type AnimateIconProps, 358 | type AnimateIconContextValue, 359 | }; 360 | -------------------------------------------------------------------------------- /components/animate-ui/icons/refresh-ccw.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import { motion, type Variants } from 'motion/react'; 5 | 6 | import { 7 | getVariants, 8 | useAnimateIconContext, 9 | IconWrapper, 10 | type IconProps, 11 | } from '@/components/animate-ui/icons/icon'; 12 | 13 | type RefreshCcwProps = IconProps; 14 | 15 | const animations = { 16 | default: { 17 | group: { 18 | initial: { 19 | rotate: 0, 20 | transition: { type: 'spring', stiffness: 150, damping: 25 }, 21 | }, 22 | animate: { 23 | rotate: -45, 24 | transition: { type: 'spring', stiffness: 150, damping: 25 }, 25 | }, 26 | }, 27 | path1: {}, 28 | path2: {}, 29 | path3: {}, 30 | path4: {}, 31 | } satisfies Record, 32 | rotate: { 33 | group: { 34 | initial: { 35 | rotate: 0, 36 | transition: { type: 'spring', stiffness: 100, damping: 25 }, 37 | }, 38 | animate: { 39 | rotate: -360, 40 | transition: { type: 'spring', stiffness: 100, damping: 25 }, 41 | }, 42 | }, 43 | path1: {}, 44 | path2: {}, 45 | path3: {}, 46 | path4: {}, 47 | } satisfies Record, 48 | } as const; 49 | 50 | function IconComponent({ size, ...props }: RefreshCcwProps) { 51 | const { controls } = useAnimateIconContext(); 52 | const variants = getVariants(animations); 53 | 54 | return ( 55 | 70 | 76 | 82 | 88 | 94 | 95 | ); 96 | } 97 | 98 | function RefreshCcw(props: RefreshCcwProps) { 99 | return ; 100 | } 101 | 102 | export { 103 | animations, 104 | RefreshCcw, 105 | RefreshCcw as RefreshCcwIcon, 106 | type RefreshCcwProps, 107 | type RefreshCcwProps as RefreshCcwIconProps, 108 | }; 109 | -------------------------------------------------------------------------------- /components/animate-ui/icons/search.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import { motion, type Variants } from 'motion/react'; 5 | 6 | import { 7 | getVariants, 8 | useAnimateIconContext, 9 | IconWrapper, 10 | type IconProps, 11 | } from '@/components/animate-ui/icons/icon'; 12 | 13 | type SearchProps = IconProps; 14 | 15 | const animations = { 16 | default: { 17 | group: { 18 | initial: { 19 | rotate: 0, 20 | }, 21 | animate: { 22 | transformOrigin: 'bottom right', 23 | rotate: [0, 17, -10, 5, -1, 0], 24 | transition: { duration: 0.8, ease: 'easeInOut' }, 25 | }, 26 | }, 27 | path: {}, 28 | circle: {}, 29 | } satisfies Record, 30 | find: { 31 | group: { 32 | initial: { 33 | x: 0, 34 | y: 0, 35 | }, 36 | animate: { 37 | x: [0, '-15%', 0, 0], 38 | y: [0, 0, '-15%', 0], 39 | transition: { duration: 1, ease: 'easeInOut' }, 40 | }, 41 | }, 42 | path: {}, 43 | circle: {}, 44 | } satisfies Record, 45 | } as const; 46 | 47 | function IconComponent({ size, ...props }: SearchProps) { 48 | const { controls } = useAnimateIconContext(); 49 | const variants = getVariants(animations); 50 | 51 | return ( 52 | 67 | 73 | 81 | 82 | ); 83 | } 84 | 85 | function Search(props: SearchProps) { 86 | return ; 87 | } 88 | 89 | export { 90 | animations, 91 | Search, 92 | Search as SearchIcon, 93 | type SearchProps, 94 | type SearchProps as SearchIconProps, 95 | }; 96 | -------------------------------------------------------------------------------- /components/animate-ui/icons/send.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import { motion, type Variants } from 'motion/react'; 5 | 6 | import { 7 | getVariants, 8 | useAnimateIconContext, 9 | IconWrapper, 10 | type IconProps, 11 | } from '@/components/animate-ui/icons/icon'; 12 | 13 | type SendProps = IconProps; 14 | 15 | const animations = { 16 | default: { 17 | group: { 18 | initial: { 19 | scale: 1, 20 | x: 0, 21 | y: 0, 22 | }, 23 | animate: { 24 | scale: [1, 0.8, 1, 1, 1], 25 | x: [0, '-10%', '100%', '-125%', 0], 26 | y: [0, '10%', '-100%', '125%', 0], 27 | transition: { 28 | default: { ease: 'easeInOut', duration: 1.2 }, 29 | x: { 30 | ease: 'easeInOut', 31 | duration: 1.2, 32 | times: [0, 0.25, 0.5, 0.5, 1], 33 | }, 34 | y: { 35 | ease: 'easeInOut', 36 | duration: 1.2, 37 | times: [0, 0.25, 0.5, 0.5, 1], 38 | }, 39 | }, 40 | }, 41 | }, 42 | path1: {}, 43 | path2: {}, 44 | } satisfies Record, 45 | } as const; 46 | 47 | function IconComponent({ size, ...props }: SendProps) { 48 | const { controls } = useAnimateIconContext(); 49 | const variants = getVariants(animations); 50 | 51 | return ( 52 | 64 | 65 | 71 | 77 | 78 | 79 | ); 80 | } 81 | 82 | function Send(props: SendProps) { 83 | return ; 84 | } 85 | 86 | export { 87 | animations, 88 | Send, 89 | Send as SendIcon, 90 | type SendProps, 91 | type SendProps as SendIconProps, 92 | }; 93 | -------------------------------------------------------------------------------- /components/animate-ui/icons/thumbs-up.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import { motion, type Variants } from 'motion/react'; 5 | 6 | import { 7 | getVariants, 8 | useAnimateIconContext, 9 | IconWrapper, 10 | type IconProps, 11 | } from '@/components/animate-ui/icons/icon'; 12 | 13 | type ThumbsUpProps = IconProps; 14 | 15 | const animations = { 16 | default: { 17 | group: { 18 | initial: { 19 | rotate: 0, 20 | }, 21 | animate: { 22 | rotate: [0, -20, -12], 23 | transformOrigin: 'bottom left', 24 | transition: { 25 | duration: 0.4, 26 | ease: 'easeInOut', 27 | }, 28 | }, 29 | }, 30 | path1: {}, 31 | path2: {}, 32 | } satisfies Record, 33 | 'default-loop': { 34 | group: { 35 | initial: { 36 | rotate: 0, 37 | }, 38 | animate: { 39 | rotate: [0, -20, 5, 0], 40 | transformOrigin: 'bottom left', 41 | transition: { 42 | duration: 0.8, 43 | ease: 'easeInOut', 44 | }, 45 | }, 46 | }, 47 | path1: {}, 48 | path2: {}, 49 | } satisfies Record, 50 | } as const; 51 | 52 | function IconComponent({ size, ...props }: ThumbsUpProps) { 53 | const { controls } = useAnimateIconContext(); 54 | const variants = getVariants(animations); 55 | 56 | return ( 57 | 72 | 78 | 84 | 85 | ); 86 | } 87 | 88 | function ThumbsUp(props: ThumbsUpProps) { 89 | return ; 90 | } 91 | 92 | export { 93 | animations, 94 | ThumbsUp, 95 | ThumbsUp as ThumbsUpIcon, 96 | type ThumbsUpProps, 97 | type ThumbsUpProps as ThumbsUpIconProps, 98 | }; 99 | -------------------------------------------------------------------------------- /components/animate-ui/radix/hover-card.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import { HoverCard as HoverCardPrimitive } from 'radix-ui'; 5 | import { 6 | AnimatePresence, 7 | motion, 8 | type HTMLMotionProps, 9 | type Transition, 10 | } from 'motion/react'; 11 | 12 | import { cn } from '@/lib/utils'; 13 | 14 | type HoverCardContextType = { 15 | isOpen: boolean; 16 | }; 17 | 18 | const HoverCardContext = React.createContext( 19 | undefined, 20 | ); 21 | 22 | const useHoverCard = (): HoverCardContextType => { 23 | const context = React.useContext(HoverCardContext); 24 | if (!context) { 25 | throw new Error('useHoverCard must be used within a HoverCard'); 26 | } 27 | return context; 28 | }; 29 | 30 | type Side = 'top' | 'bottom' | 'left' | 'right'; 31 | 32 | const getInitialPosition = (side: Side) => { 33 | switch (side) { 34 | case 'top': 35 | return { y: 15 }; 36 | case 'bottom': 37 | return { y: -15 }; 38 | case 'left': 39 | return { x: 15 }; 40 | case 'right': 41 | return { x: -15 }; 42 | } 43 | }; 44 | 45 | type HoverCardProps = React.ComponentProps; 46 | 47 | function HoverCard({ children, ...props }: HoverCardProps) { 48 | const [isOpen, setIsOpen] = React.useState( 49 | props?.open ?? props?.defaultOpen ?? false, 50 | ); 51 | 52 | React.useEffect(() => { 53 | if (props?.open !== undefined) setIsOpen(props.open); 54 | }, [props?.open]); 55 | 56 | const handleOpenChange = React.useCallback( 57 | (open: boolean) => { 58 | setIsOpen(open); 59 | props.onOpenChange?.(open); 60 | }, 61 | [props], 62 | ); 63 | 64 | return ( 65 | 66 | 71 | {children} 72 | 73 | 74 | ); 75 | } 76 | 77 | type HoverCardTriggerProps = React.ComponentProps< 78 | typeof HoverCardPrimitive.Trigger 79 | >; 80 | 81 | function HoverCardTrigger(props: HoverCardTriggerProps) { 82 | return ( 83 | 84 | ); 85 | } 86 | 87 | type HoverCardContentProps = React.ComponentProps< 88 | typeof HoverCardPrimitive.Content 89 | > & 90 | HTMLMotionProps<'div'> & { 91 | transition?: Transition; 92 | }; 93 | 94 | function HoverCardContent({ 95 | className, 96 | align = 'center', 97 | side = 'bottom', 98 | sideOffset = 4, 99 | transition = { type: 'spring', stiffness: 300, damping: 25 }, 100 | children, 101 | ...props 102 | }: HoverCardContentProps) { 103 | const { isOpen } = useHoverCard(); 104 | const initialPosition = getInitialPosition(side); 105 | 106 | return ( 107 | 108 | {isOpen && ( 109 | 110 | 117 | 130 | {children} 131 | 132 | 133 | 134 | )} 135 | 136 | ); 137 | } 138 | 139 | export { 140 | HoverCard, 141 | HoverCardTrigger, 142 | HoverCardContent, 143 | useHoverCard, 144 | type HoverCardContextType, 145 | type HoverCardProps, 146 | type HoverCardTriggerProps, 147 | type HoverCardContentProps, 148 | }; 149 | -------------------------------------------------------------------------------- /components/animate-ui/text/sliding-number.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import { 5 | useSpring, 6 | useTransform, 7 | motion, 8 | useInView, 9 | type MotionValue, 10 | type SpringOptions, 11 | type UseInViewOptions, 12 | } from 'motion/react'; 13 | import useMeasure from 'react-use-measure'; 14 | 15 | import { cn } from '@/lib/utils'; 16 | 17 | type SlidingNumberRollerProps = { 18 | prevValue: number; 19 | value: number; 20 | place: number; 21 | transition: SpringOptions; 22 | }; 23 | 24 | function SlidingNumberRoller({ 25 | prevValue, 26 | value, 27 | place, 28 | transition, 29 | }: SlidingNumberRollerProps) { 30 | const startNumber = Math.floor(prevValue / place) % 10; 31 | const targetNumber = Math.floor(value / place) % 10; 32 | const animatedValue = useSpring(startNumber, transition); 33 | 34 | React.useEffect(() => { 35 | animatedValue.set(targetNumber); 36 | }, [targetNumber, animatedValue]); 37 | 38 | const [measureRef, { height }] = useMeasure(); 39 | 40 | return ( 41 | 46 | 0 47 | {Array.from({ length: 10 }, (_, i) => ( 48 | 55 | ))} 56 | 57 | ); 58 | } 59 | 60 | type SlidingNumberDisplayProps = { 61 | motionValue: MotionValue; 62 | number: number; 63 | height: number; 64 | transition: SpringOptions; 65 | }; 66 | 67 | function SlidingNumberDisplay({ 68 | motionValue, 69 | number, 70 | height, 71 | transition, 72 | }: SlidingNumberDisplayProps) { 73 | const y = useTransform(motionValue, (latest) => { 74 | if (!height) return 0; 75 | const currentNumber = latest % 10; 76 | const offset = (10 + number - currentNumber) % 10; 77 | let translateY = offset * height; 78 | if (offset > 5) translateY -= 10 * height; 79 | return translateY; 80 | }); 81 | 82 | if (!height) { 83 | return {number}; 84 | } 85 | 86 | return ( 87 | 93 | {number} 94 | 95 | ); 96 | } 97 | 98 | type SlidingNumberProps = React.ComponentProps<'span'> & { 99 | number: number | string; 100 | inView?: boolean; 101 | inViewMargin?: UseInViewOptions['margin']; 102 | inViewOnce?: boolean; 103 | padStart?: boolean; 104 | decimalSeparator?: string; 105 | decimalPlaces?: number; 106 | transition?: SpringOptions; 107 | }; 108 | 109 | function SlidingNumber({ 110 | ref, 111 | number, 112 | className, 113 | inView = false, 114 | inViewMargin = '0px', 115 | inViewOnce = true, 116 | padStart = false, 117 | decimalSeparator = '.', 118 | decimalPlaces = 0, 119 | transition = { 120 | stiffness: 200, 121 | damping: 20, 122 | mass: 0.4, 123 | }, 124 | ...props 125 | }: SlidingNumberProps) { 126 | const localRef = React.useRef(null); 127 | React.useImperativeHandle(ref, () => localRef.current!); 128 | 129 | const inViewResult = useInView(localRef, { 130 | once: inViewOnce, 131 | margin: inViewMargin, 132 | }); 133 | const isInView = !inView || inViewResult; 134 | 135 | const prevNumberRef = React.useRef(0); 136 | 137 | const effectiveNumber = React.useMemo( 138 | () => (!isInView ? 0 : Math.abs(Number(number))), 139 | [number, isInView], 140 | ); 141 | 142 | const formatNumber = React.useCallback( 143 | (num: number) => 144 | decimalPlaces != null ? num.toFixed(decimalPlaces) : num.toString(), 145 | [decimalPlaces], 146 | ); 147 | 148 | const numberStr = formatNumber(effectiveNumber); 149 | const [newIntStrRaw, newDecStrRaw = ''] = numberStr.split('.'); 150 | const newIntStr = 151 | padStart && newIntStrRaw?.length === 1 ? '0' + newIntStrRaw : newIntStrRaw; 152 | 153 | const prevFormatted = formatNumber(prevNumberRef.current); 154 | const [prevIntStrRaw = '', prevDecStrRaw = ''] = prevFormatted.split('.'); 155 | const prevIntStr = 156 | padStart && prevIntStrRaw.length === 1 157 | ? '0' + prevIntStrRaw 158 | : prevIntStrRaw; 159 | 160 | const adjustedPrevInt = React.useMemo(() => { 161 | return prevIntStr.length > (newIntStr?.length ?? 0) 162 | ? prevIntStr.slice(-(newIntStr?.length ?? 0)) 163 | : prevIntStr.padStart(newIntStr?.length ?? 0, '0'); 164 | }, [prevIntStr, newIntStr]); 165 | 166 | const adjustedPrevDec = React.useMemo(() => { 167 | if (!newDecStrRaw) return ''; 168 | return prevDecStrRaw.length > newDecStrRaw.length 169 | ? prevDecStrRaw.slice(0, newDecStrRaw.length) 170 | : prevDecStrRaw.padEnd(newDecStrRaw.length, '0'); 171 | }, [prevDecStrRaw, newDecStrRaw]); 172 | 173 | React.useEffect(() => { 174 | if (isInView) prevNumberRef.current = effectiveNumber; 175 | }, [effectiveNumber, isInView]); 176 | 177 | const intDigitCount = newIntStr?.length ?? 0; 178 | const intPlaces = React.useMemo( 179 | () => 180 | Array.from({ length: intDigitCount }, (_, i) => 181 | Math.pow(10, intDigitCount - i - 1), 182 | ), 183 | [intDigitCount], 184 | ); 185 | const decPlaces = React.useMemo( 186 | () => 187 | newDecStrRaw 188 | ? Array.from({ length: newDecStrRaw.length }, (_, i) => 189 | Math.pow(10, newDecStrRaw.length - i - 1), 190 | ) 191 | : [], 192 | [newDecStrRaw], 193 | ); 194 | 195 | const newDecValue = newDecStrRaw ? parseInt(newDecStrRaw, 10) : 0; 196 | const prevDecValue = adjustedPrevDec ? parseInt(adjustedPrevDec, 10) : 0; 197 | 198 | return ( 199 | 205 | {isInView && Number(number) < 0 && -} 206 | 207 | {intPlaces.map((place) => ( 208 | 215 | ))} 216 | 217 | {newDecStrRaw && ( 218 | <> 219 | {decimalSeparator} 220 | {decPlaces.map((place) => ( 221 | 228 | ))} 229 | 230 | )} 231 | 232 | ); 233 | } 234 | 235 | export { SlidingNumber, type SlidingNumberProps }; 236 | -------------------------------------------------------------------------------- /components/animate-ui/ui-elements/management-bar.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import * as React from 'react'; 4 | import { 5 | ChevronLeft, 6 | ChevronRight, 7 | Ban, 8 | X, 9 | Command, 10 | IdCard, 11 | } from 'lucide-react'; 12 | import { SlidingNumber } from '@/components/animate-ui/text/sliding-number'; 13 | import { motion, Variants, Transition } from 'motion/react'; 14 | 15 | const TOTAL_PAGES = 10; 16 | 17 | const BUTTON_MOTION_CONFIG = { 18 | initial: 'rest', 19 | whileHover: 'hover', 20 | whileTap: 'tap', 21 | variants: { 22 | rest: { maxWidth: '40px' }, 23 | hover: { 24 | maxWidth: '140px', 25 | transition: { type: 'spring', stiffness: 200, damping: 35, delay: 0.15 }, 26 | }, 27 | tap: { scale: 0.95 }, 28 | }, 29 | transition: { type: 'spring', stiffness: 250, damping: 25 }, 30 | }; 31 | 32 | const LABEL_VARIANTS: Variants = { 33 | rest: { opacity: 0, x: 4 }, 34 | hover: { opacity: 1, x: 0, visibility: 'visible' }, 35 | tap: { opacity: 1, x: 0, visibility: 'visible' }, 36 | }; 37 | 38 | const LABEL_TRANSITION: Transition = { 39 | type: 'spring', 40 | stiffness: 200, 41 | damping: 25, 42 | }; 43 | 44 | function ManagementBar() { 45 | const [currentPage, setCurrentPage] = React.useState(1); 46 | 47 | const handlePrevPage = React.useCallback(() => { 48 | if (currentPage > 1) setCurrentPage(currentPage - 1); 49 | }, [currentPage]); 50 | 51 | const handleNextPage = React.useCallback(() => { 52 | if (currentPage < TOTAL_PAGES) setCurrentPage(currentPage + 1); 53 | }, [currentPage]); 54 | 55 | return ( 56 |
57 |
58 | 65 |
66 | 71 | / {TOTAL_PAGES} 72 |
73 | 80 |
81 | 82 |
83 | 84 | 89 | 94 | 95 | 100 | Blacklist 101 | 102 | 103 | 104 | 109 | 110 | 115 | Reject 116 | 117 | 118 | 119 | 124 | 125 | 130 | Hire 131 | 132 | 133 | 134 | 135 |
136 | 137 | 141 | Move to: 142 | Interview I 143 |
144 |
145 | E 146 |
147 | 148 |
149 | ); 150 | } 151 | 152 | export { ManagementBar }; 153 | -------------------------------------------------------------------------------- /components/common/CardGrid.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from 'react'; 2 | 3 | interface CardGridProps { 4 | children: ReactNode; 5 | className?: string; 6 | } 7 | 8 | /** 9 | * 自适应卡片网格组件 10 | * 默认每行显示3个卡片,在小屏幕上自动调整为1个或2个 11 | */ 12 | const CardGrid: React.FC = ({ children, className = '' }) => { 13 | return ( 14 |
15 | {children} 16 |
17 | ); 18 | }; 19 | 20 | export default CardGrid; -------------------------------------------------------------------------------- /components/common/NavbarOAuthButton.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import React, { useEffect, useState, useCallback } from 'react'; 4 | import { FlipButton } from '@/components/animate-ui/buttons/flip'; 5 | import { Loader } from 'lucide-react'; 6 | 7 | interface NavbarOAuthButtonProps { 8 | className?: string; 9 | } 10 | 11 | const NavbarOAuthButton: React.FC = ({ 12 | className = "ml-2" 13 | }) => { 14 | const [isLoggedIn, setIsLoggedIn] = useState(false); 15 | const [userName, setUserName] = useState(''); 16 | const [isLoading, setIsLoading] = useState(true); 17 | 18 | // 获取cookie辅助函数 19 | const getCookie = (name: string): string | undefined => { 20 | const value = `; ${document.cookie}`; 21 | const parts = value.split(`; ${name}=`); 22 | if (parts.length === 2) return parts.pop()?.split(';').shift(); 23 | return undefined; 24 | }; 25 | 26 | // 检查用户登录状态 27 | const checkUserLoginStatus = useCallback(() => { 28 | setIsLoading(true); 29 | 30 | try { 31 | // 尝试从cookie中获取用户信息 32 | const userCookie = getCookie('oauth_user'); 33 | 34 | if (userCookie) { 35 | try { 36 | // 先解码URL编码的cookie值,再解析JSON 37 | const decodedCookie = decodeURIComponent(userCookie); 38 | const userData = JSON.parse(decodedCookie); 39 | setIsLoggedIn(true); 40 | setUserName(userData.name || 'User'); 41 | } catch (e) { 42 | console.error('Failed to parse user cookie', e); 43 | } 44 | } 45 | } catch (error) { 46 | console.error('Error checking login status:', error); 47 | } finally { 48 | setIsLoading(false); 49 | } 50 | }, []); 51 | 52 | useEffect(() => { 53 | // 客户端渲染时才执行 54 | if (typeof window === 'undefined') return; 55 | 56 | // 检查URL中是否有错误参数 57 | const urlParams = new URLSearchParams(window.location.search); 58 | const error = urlParams.get('error'); 59 | 60 | if (error) { 61 | console.error('OAuth error:', error); 62 | // 清除错误参数 63 | window.history.replaceState({}, document.title, window.location.pathname); 64 | } 65 | 66 | // 检查cookie中是否有用户信息 67 | checkUserLoginStatus(); 68 | }, [checkUserLoginStatus]); 69 | 70 | // 处理登录点击 71 | const handleLogin = () => { 72 | const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || window.location.origin; 73 | 74 | if (isLoggedIn) { 75 | // 如果已登录,则登出 76 | window.location.href = `${baseUrl}/api/oauth2?action=logout&redirect=${encodeURIComponent(window.location.pathname)}`; 77 | } else { 78 | // 如果未登录,则重定向到OAuth授权 79 | window.location.href = `${baseUrl}/api/oauth2?redirect=${encodeURIComponent(window.location.pathname)}`; 80 | } 81 | }; 82 | 83 | if (isLoading) { 84 | return ( 85 |
86 | 87 |
88 | ); 89 | } 90 | 91 | return ( 92 |
93 | 98 |
99 | ); 100 | }; 101 | 102 | export default NavbarOAuthButton; -------------------------------------------------------------------------------- /components/common/ThemeWrapper.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import {useEffect, useState} from "react"; 4 | import {useTheme} from "nextra-theme-docs"; 5 | 6 | export const ThemeWrapper = ({ childrenInLightTheme, childrenInDarkTheme }: { childrenInLightTheme: React.ReactNode, childrenInDarkTheme: React.ReactNode }) => { 7 | const { theme } = useTheme(); 8 | const [currentTheme, setCurrentTheme] = useState(theme); 9 | 10 | useEffect(() => { 11 | if (theme === 'system') { 12 | const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); 13 | const updateTheme = () => setCurrentTheme(darkModeMediaQuery.matches ? 'dark' : 'light'); 14 | updateTheme(); 15 | darkModeMediaQuery.addEventListener('change', updateTheme); 16 | 17 | return () => darkModeMediaQuery.removeEventListener('change', updateTheme); 18 | } else { 19 | setCurrentTheme(theme); 20 | } 21 | }, [theme]); 22 | 23 | const children = currentTheme === 'light' 24 | ? childrenInLightTheme 25 | : childrenInDarkTheme; 26 | 27 | return ( 28 | <> 29 | {children} 30 | 31 | ); 32 | } 33 | 34 | // 同时保留默认导出以兼容可能的其他导入方式 35 | export default ThemeWrapper; -------------------------------------------------------------------------------- /components/common/UserGroupTooltip.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { UserHoverCard } from './UserHoverCard'; 4 | 5 | /** 6 | * 用户组项目接口 7 | */ 8 | interface UserGroupItem { 9 | username: string; 10 | title?: string; 11 | description?: string; 12 | } 13 | 14 | /** 15 | * 用户组接口 16 | */ 17 | interface UserGroupTooltipProps { 18 | users: UserGroupItem[]; 19 | maxDisplay?: number; 20 | className?: string; 21 | title?: string; 22 | size?: 'sm' | 'md' | 'lg'; 23 | } 24 | 25 | /** 26 | * 用户组头像工具提示组件 27 | */ 28 | export const UserGroupTooltip = ({ 29 | users, 30 | maxDisplay = 5, 31 | className = '', 32 | title, 33 | size = 'md', 34 | }: UserGroupTooltipProps) => { 35 | // 分割用户:显示的和剩余的 36 | const displayUsers = users.slice(0, maxDisplay); 37 | const remainingCount = users.length - maxDisplay; 38 | 39 | // 根据尺寸获取样式 40 | const getSizeStyles = () => { 41 | switch (size) { 42 | case 'sm': 43 | return { 44 | avatarSize: 'size-8', // 外层容器 32px 45 | overlap: '-ml-2', 46 | height: 'h-8', 47 | hoverCardSize: 'md' as const // UserHoverCard md = size-8 = 32px ✓ 48 | }; 49 | case 'lg': 50 | return { 51 | avatarSize: 'size-14', // 外层容器 56px 52 | overlap: '-ml-3', 53 | height: 'h-14', 54 | hoverCardSize: '2xl' as const // UserHoverCard 2xl = size-14 = 56px ✓ 55 | }; 56 | case 'md': 57 | default: 58 | return { 59 | avatarSize: 'size-12', // 外层容器 48px 60 | overlap: '-ml-3', 61 | height: 'h-12', 62 | hoverCardSize: 'xl' as const // UserHoverCard xl = size-12 = 48px ✓ 63 | }; 64 | } 65 | }; 66 | 67 | const styles = getSizeStyles(); 68 | 69 | return ( 70 |
71 | {title &&

{title}

} 72 | 73 | {/* 用户头像重叠列表 */} 74 |
75 | {displayUsers.map((user, index) => ( 76 |
0 ? styles.overlap : ''} hover:z-10 transition-all duration-200 hover:scale-110`} 79 | style={{ zIndex: displayUsers.length - index }} 80 | > 81 |
82 | 83 |
84 |
85 | ))} 86 | 87 | {/* 显示剩余用户数量的圆圈 */} 88 | {remainingCount > 0 && ( 89 |
93 | +{remainingCount} 94 |
95 | )} 96 |
97 |
98 | ); 99 | }; -------------------------------------------------------------------------------- /components/common/UserHoverCard.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import Link from 'next/link'; 4 | import { 5 | HoverCard, 6 | HoverCardTrigger, 7 | HoverCardContent, 8 | } from '@/components/animate-ui/radix/hover-card'; 9 | import { useUserData } from '@/hooks/useDataCache'; 10 | import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'; 11 | 12 | /** 13 | * UserHoverCard 组件属性接口 14 | */ 15 | interface UserHoverCardProps { 16 | username: string; 17 | showUsername?: boolean; // 是否显示 @username 文本,默认为 true 18 | size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl'; // 头像尺寸,默认为 xs 19 | } 20 | 21 | /** 22 | * 默认配置常量 23 | */ 24 | const DEFAULT_CONFIG = { 25 | AVATAR_URL: '/logo.png', 26 | AVATAR_SIZE: '288', 27 | PROFILE_BASE_URL: 'https://linux.do/u/', 28 | DEFAULT_BIO: '这个用户还没有填写个人简介', 29 | } as const; 30 | 31 | /** 32 | * 清理HTML标签并处理特殊格式 33 | */ 34 | const cleanHtmlText = (htmlText: string): string => { 35 | if (!htmlText) return ''; 36 | 37 | // 替换
标签为换行符 38 | let cleanText = htmlText.replace(//gi, '\n'); 39 | 40 | // 处理链接标签 41 | cleanText = cleanText.replace(/]*href="([^"]*)"[^>]*>(.*?)<\/a>/gi, (match, href, text) => { 42 | // 清理链接文本中的HTML标签 43 | const cleanLinkText = text.replace(/<[^>]*>/g, '').trim(); 44 | 45 | // 如果链接文本为空,只显示URL 46 | if (!cleanLinkText) { 47 | return href; 48 | } 49 | 50 | // 如果链接文本就是URL,只显示一次 51 | if (cleanLinkText === href) { 52 | return href; 53 | } 54 | 55 | // 显示格式:文本(URL) 56 | return `${cleanLinkText}(${href})`; 57 | }); 58 | 59 | // 处理图片标签 60 | cleanText = cleanText.replace(/]*>/gi, (match) => { 61 | // 提取title属性(emoji名称) 62 | const titleMatch = match.match(/title="([^"]*)"/i); 63 | if (titleMatch) { 64 | return titleMatch[1]; // 返回emoji名称,如 :wave: 65 | } 66 | 67 | // 提取alt属性作为备选 68 | const altMatch = match.match(/alt="([^"]*)"/i); 69 | if (altMatch) { 70 | return altMatch[1]; 71 | } 72 | 73 | // 如果是emoji路径,尝试提取emoji名称 74 | const srcMatch = match.match(/src="[^"]*\/([^\/]*?)\.(png|gif|jpg|jpeg)/i); 75 | if (srcMatch && match.includes('emoji')) { 76 | return `:${srcMatch[1]}:`; // 返回 :emoji_name: 格式 77 | } 78 | 79 | // 其他图片显示占位符 80 | return '[图片]'; 81 | }); 82 | 83 | // 移除所有其他HTML标签,但保留内容 84 | cleanText = cleanText.replace(/<[^>]*>/g, ''); 85 | 86 | // 解码HTML实体 87 | cleanText = cleanText 88 | .replace(/&/g, '&') 89 | .replace(/</g, '<') 90 | .replace(/>/g, '>') 91 | .replace(/"/g, '"') 92 | .replace(/'/g, "'") 93 | .replace(/ /g, ' '); 94 | 95 | // 清理多余的空行和空格 96 | cleanText = cleanText 97 | .split('\n') 98 | .map(line => line.trim()) 99 | .filter(line => line.length > 0) 100 | .join('\n') 101 | .trim(); 102 | 103 | return cleanText; 104 | }; 105 | 106 | /** 107 | * 获取头像尺寸类名 108 | */ 109 | const getAvatarSizeClass = (size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl'): string => { 110 | switch (size) { 111 | case 'xs': return 'size-4'; 112 | case 'sm': return 'size-6'; 113 | case 'md': return 'size-8'; 114 | case 'lg': return 'size-10'; 115 | case 'xl': return 'size-12'; 116 | case '2xl': return 'size-14'; 117 | default: return 'size-5'; // 默认尺寸 118 | } 119 | }; 120 | 121 | /** 122 | * 用户悬停卡片组件 123 | * 显示用户头像和用户名,鼠标悬停时显示详细信息 124 | * 125 | * @param username - 用户名 126 | * @param showUsername - 是否显示 @username 文本,默认为 true 127 | * @param size - 头像尺寸,默认为 xs 128 | * @returns React 组件 129 | */ 130 | export const UserHoverCard = ({ username, showUsername = true, size = 'xs' }: UserHoverCardProps) => { 131 | // 使用优化的数据缓存Hook 132 | const { data: userData, loading } = useUserData(username); 133 | 134 | // 获取头像尺寸类名 135 | const avatarSizeClass = getAvatarSizeClass(size); 136 | 137 | /** 138 | * 获取头像 URL 139 | */ 140 | const getAvatarUrl = (size: string = DEFAULT_CONFIG.AVATAR_SIZE): string => { 141 | if (!userData?.avatar_template) { 142 | return DEFAULT_CONFIG.AVATAR_URL; 143 | } 144 | return `https://linux.do${userData.avatar_template.replace('{size}', size)}`; 145 | }; 146 | 147 | /** 148 | * 获取用户显示名称 149 | */ 150 | const getDisplayName = (): string => { 151 | return userData?.name || username; 152 | }; 153 | 154 | /** 155 | * 获取用户简介 156 | */ 157 | const getUserBio = (): string => { 158 | const rawBio = userData?.bio_excerpt || DEFAULT_CONFIG.DEFAULT_BIO; 159 | return cleanHtmlText(rawBio); 160 | }; 161 | 162 | return ( 163 | 164 | 165 | 171 | 172 | 177 | {username.substring(0, 2).toUpperCase()} 178 | 179 | {showUsername && ( 180 | @{username} 181 | )} 182 | 183 | 184 | 185 | 186 |
187 | {/* 用户头像 */} 188 | 189 | 194 | {username.substring(0, 2).toUpperCase()} 195 | 196 | 197 | {/* 用户信息 */} 198 |
199 | {/* 基本信息 */} 200 |
201 |
202 | {loading ? '加载中...' : getDisplayName()} 203 |
204 |
205 | @{username} 206 |
207 |
208 | 209 | {/* 用户简介 */} 210 |
211 | {loading ? '加载中...' : getUserBio()} 212 |
213 | 214 | {/* 统计信息 */} 215 |
216 |
217 |
218 | {loading ? '-' : (userData?.trust_level ?? 0)} 219 |
220 |
信任等级
221 |
222 |
223 |
224 | {loading ? '-' : (userData?.gamification_score ?? 0)} 225 |
226 |
社区点数
227 |
228 |
229 |
230 |
231 |
232 |
233 | ); 234 | }; -------------------------------------------------------------------------------- /components/common/card/Gold.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface GoldCardProps { 4 | heading?: string; 5 | number?: string; 6 | date?: string; 7 | name?: string; 8 | code?: string; 9 | } 10 | 11 | const Gold: React.FC = ({ 12 | heading = "GOLD", 13 | number = "4356 7891 2345 6789", 14 | date = "09/26", 15 | name = "LINUX DO METAVERSE", 16 | code = "123" 17 | }) => { 18 | return ( 19 |
20 |
21 | {/* 正面 */} 22 |
23 |

24 | {heading} 25 |

26 | 27 | {/* 三色圆圈标志 - 使用Niello中的SVG样式 */} 28 | 35 | {/* 顶部黑色圆圈 */} 36 | 37 | {/* 左下白色圆圈 */} 38 | 39 | {/* 右下黄色圆圈 */} 40 | 41 | {/* 中心重叠混合效果 */} 42 | 43 | 44 | 45 | {/* 芯片 */} 46 |
47 | 48 | 49 | 50 |
51 | 52 | {/* 持卡人姓名 */} 53 |

54 | {name} 55 |

56 |
57 | 58 | {/* 反面 */} 59 |
60 | {/* 磁条 */} 61 |
74 | 75 | {/* 非接触式支付标志 */} 76 |
77 | 78 | 79 | 80 |
81 | 82 | {/* 卡号 */} 83 |

84 | {number} 85 |

86 | 87 | {/* 有效期和CVC的容器 */} 88 |
89 | {/* 有效期 */} 90 |
91 |

EXP

92 |

93 | {date} 94 |

95 |
96 | 97 | {/* CVC号码 */} 98 |
99 |

CVC

100 |

101 | {code} 102 |

103 |
104 |
105 |
106 |
107 |
108 | ); 109 | }; 110 | 111 | export default Gold; -------------------------------------------------------------------------------- /components/common/card/Niello.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React from 'react'; 4 | 5 | interface NielloProps { 6 | heading?: string; 7 | number?: string; 8 | validThru?: string; 9 | date?: string; 10 | name?: string; 11 | code?: string; 12 | } 13 | 14 | const Niello: React.FC = ({ 15 | number = "4962 1162 7845 1269", 16 | date = "12/28", 17 | name = "Chenyme", 18 | code = "127" 19 | }) => { 20 | return ( 21 |
22 |
23 | {/* 卡片正面 */} 24 |
25 | {/* 姓名 */} 26 |

27 | {name} 28 |

29 | 30 | {/* 芯片 */} 31 | 40 | 47 | 48 | 49 | {/* 非接触式支付图标 */} 50 | 59 | 66 | 67 | 68 | {/* 卡号 */} 69 |

70 | {number} 71 |

72 | 73 | {/* 有效期标签和日期 */} 74 |
75 |

VALID THRU

76 |

77 | {date} 78 |

79 |
80 | 81 | {/* 卡片名称 */} 82 |

83 | Linux Do Metaverse 84 |

85 | 86 | {/* 支付标志 (三角形三色圆圈) */} 87 | 94 | {/* 顶部黑色圆圈 */} 95 | 96 | {/* 左下白色圆圈 */} 97 | 98 | {/* 右下黄色圆圈 */} 99 | 100 | {/* 中心重叠混合效果 */} 101 | 102 | 103 |
104 | 105 | {/* 卡片背面 */} 106 |
107 | {/* 磁条 */} 108 |
121 | 122 | {/* 签名栏 */} 123 |
124 | 125 | {/* CVV栏 */} 126 |
127 |

128 | {code} 129 |

130 |
131 |
132 |
133 |
134 | ); 135 | }; 136 | 137 | export default Niello; -------------------------------------------------------------------------------- /components/pandora/HTML.tsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenyme/LinuxDoWiki/b0f28127c613852d088ddb805ac0e2f074d6eee0/components/pandora/HTML.tsx -------------------------------------------------------------------------------- /components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as AvatarPrimitive from "@radix-ui/react-avatar" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | function Avatar({ 9 | className, 10 | ...props 11 | }: React.ComponentProps) { 12 | return ( 13 | 21 | ) 22 | } 23 | 24 | function AvatarImage({ 25 | className, 26 | ...props 27 | }: React.ComponentProps) { 28 | return ( 29 | 34 | ) 35 | } 36 | 37 | function AvatarFallback({ 38 | className, 39 | ...props 40 | }: React.ComponentProps) { 41 | return ( 42 | 50 | ) 51 | } 52 | 53 | export { Avatar, AvatarImage, AvatarFallback } 54 | -------------------------------------------------------------------------------- /components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", 16 | outline: 17 | "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", 18 | secondary: 19 | "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", 20 | ghost: 21 | "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", 22 | link: "text-primary underline-offset-4 hover:underline", 23 | }, 24 | size: { 25 | default: "h-9 px-4 py-2 has-[>svg]:px-3", 26 | sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", 27 | lg: "h-10 rounded-md px-6 has-[>svg]:px-4", 28 | icon: "size-9", 29 | }, 30 | }, 31 | defaultVariants: { 32 | variant: "default", 33 | size: "default", 34 | }, 35 | } 36 | ) 37 | 38 | function Button({ 39 | className, 40 | variant, 41 | size, 42 | asChild = false, 43 | ...props 44 | }: React.ComponentProps<"button"> & 45 | VariantProps & { 46 | asChild?: boolean 47 | }) { 48 | const Comp = asChild ? Slot : "button" 49 | 50 | return ( 51 | 56 | ) 57 | } 58 | 59 | export { Button, buttonVariants } 60 | -------------------------------------------------------------------------------- /components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | function Card({ className, ...props }: React.ComponentProps<"div">) { 6 | return ( 7 |
15 | ) 16 | } 17 | 18 | function CardHeader({ className, ...props }: React.ComponentProps<"div">) { 19 | return ( 20 |
28 | ) 29 | } 30 | 31 | function CardTitle({ className, ...props }: React.ComponentProps<"div">) { 32 | return ( 33 |
38 | ) 39 | } 40 | 41 | function CardDescription({ className, ...props }: React.ComponentProps<"div">) { 42 | return ( 43 |
48 | ) 49 | } 50 | 51 | function CardAction({ className, ...props }: React.ComponentProps<"div">) { 52 | return ( 53 |
61 | ) 62 | } 63 | 64 | function CardContent({ className, ...props }: React.ComponentProps<"div">) { 65 | return ( 66 |
71 | ) 72 | } 73 | 74 | function CardFooter({ className, ...props }: React.ComponentProps<"div">) { 75 | return ( 76 |
81 | ) 82 | } 83 | 84 | export { 85 | Card, 86 | CardHeader, 87 | CardFooter, 88 | CardTitle, 89 | CardAction, 90 | CardDescription, 91 | CardContent, 92 | } 93 | -------------------------------------------------------------------------------- /content/AI/Fuclaude/EveryFuclaude.mdx: -------------------------------------------------------------------------------- 1 |

2 | 3 | import Link from 'next/link' 4 | 5 | # 通配子域 Fuclaude 6 | 7 | 与 OAIFree 的通配子域名功能类似,`*.fuclaude.oaifree.com` 现已支持通配子域名访问登录 Fuclaude 镜像(注:**非 `fuclaude.oaifree.com`**)。您可以使用 linuxdo.fuclaude.oaifree.com、oaifree.fuclaude.oaifree.com 等子域名来访问镜像服务。 8 | 9 | 实际上,`*.fuclaude.oaifree.com` 中的 `*` 可以是任何符合域名规则的字符。由于不同域名的 Cookie 相互独立,这在一定程度上可以防止服务被阻断。 10 | -------------------------------------------------------------------------------- /content/AI/Fuclaude/Fuclaude.mdx: -------------------------------------------------------------------------------- 1 |

2 | 3 | import Link from 'next/link' 4 | import Image from 'next/image' 5 | import { Callout } from 'nextra/components' 6 | import { TopicHoverCard } from '@/components/common/TopicHoverCard' 7 | import { CodeTabs } from '@/components/animate-ui/components/code-tabs' 8 | 9 | # 新版镜像 Fuclaude 10 | 11 | 由于原定域名被抢注,因此改名为 Fuclaude(demo.fuclaude.com)。联想上一节所提的 OAIFree 镜像,Fuclaude 是一个 Claude 的镜像站点。目前镜像支持 OAuth2 登录,这与 OAIFree 用法相似,避免直接泄露 Token,同时支持会话隔离、过期时间等诸多功能。 12 | 13 | ## 功能介绍 14 | 15 | - 支持 FreeBSD 系统(Serv00)部署 16 | - 支持全流程代理,接码、注册均已完成(注册需要国外手机号) 17 | - 支持 OAuth2 登录,与 OAIFree 用法相似,避免直接泄露 Token 18 | - 完美支持会话隔离,通过 OAuth2 登录实现,与 OAIFree 的 Share Token 类似 19 | - 支持接入内容审核,默认使用模型 text-moderation-latest 20 | - 支持 sessionKey 获取,接口 /api/auth/session 21 | - 支持配置站点密码,原生保护镜像站点 22 | - 支持配置过期时间 expire 23 | 24 |

25 | 26 | fucluade 27 | 28 | ## 登录使用 29 | 30 | ### 邮箱登录 31 | 32 | 点击 前往 Fuclaude,使用您 Claude 的注册邮箱直接登录。或者,使用您自行部署的地址进行邮箱登录。 33 | 34 | - 目前 Fuclaude 已代理 Claude 接码,无需担心获取不到验证码 35 | 36 | ### Session Token 登录 37 | 38 | 首先,按上一节 Session Token 的步骤获取 Session Token。或者,使用您自行部署的地址获取 Session Token。再 前往 Fuclaude 使用 Session Token 登录镜像服务。 39 | 40 | ### OAuth 获取链接 41 | 42 | - 请确保替换 `YOURDOMAIN` 为你的域名,`SESSION_KEY` 为你的 Session Token,`UNIQUE_NAME` 为你的独一无二的名称 43 | 44 |

45 | 46 | response.json()) 88 | .then(data => { 89 | const loginUrl = data.login_url; 90 | // 你可以在这里处理 loginUrl 91 | console.log(loginUrl); 92 | }) 93 | .catch(error => { 94 | console.error('Error:', error); 95 | });` 96 | }} 97 | /> 98 | 99 | ## 部署使用 100 | 101 | ### 手动部署 102 | 103 | 前往 【Fuclaude】- Github 查看最新版本的 Fuclaude。 104 | 105 | 在仓库的 Release 页面下载最新版本的 Fuclaude,解压后即可使用。 106 | 107 | ### Docker-compose 部署 108 | 109 |

110 | 111 | -------------------------------------------------------------------------------- /content/AI/Fuclaude/SessionToken.mdx: -------------------------------------------------------------------------------- 1 |

2 | 3 | import Link from 'next/link' 4 | 5 | # Session Token 6 | 7 | Claude 的 Session Token 是一个用于身份认证的令牌,主要用于 Claude 网页端交互时的身份验证。这个令牌与其他 API 服务的 API 密钥或会话令牌类似,在与 Claude 的交互过程中扮演着验证身份、保障安全以及维持对话连续性的重要角色。 8 | 9 | 与前文提到的 Access Token 相似,Session Token 也是一个动态令牌,会在**重新登录账号后**失效。 10 | 11 | ## 获取方法 12 | 13 | - 点击 前往 Fuclaude,使用邮箱登录 14 | 15 | - 登录后访问 此链接获取 Session Token 16 | 17 | 请注意:Session Token 将在**重新登录账号后**失效! 18 | 19 | 为确保 Session Token 的有效期充足,建议使用 Fuclaude 的 Session Token 进行登录。 -------------------------------------------------------------------------------- /content/AI/Fuclaude/_meta.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | SessionToken: "Session Token", 3 | Fuclaude: "新版镜像 Fuclaude", 4 | } -------------------------------------------------------------------------------- /content/AI/OAIFree/AccessToken.mdx: -------------------------------------------------------------------------------- 1 |

2 | 3 | import Link from 'next/link' 4 | import { Cards, Callout } from 'nextra/components' 5 | import { ThumbsUp } from '@/components/animate-ui/icons/thumbs-up' 6 | import { CodeTabs } from '@/components/animate-ui/components/code-tabs' 7 | 8 | # Access Token 9 | 10 | 当你在官网登录 ChatGPT 后,系统会自动发放一个许可令牌,这个令牌就是 Access Token。它是一种会过期的动态令牌,相当于另一种登录方式,但安全性更高,目前 OpenAI 的 Access Token 有效期为10天。 11 | 12 | 在这期间,你可以使用 Access Token 登录 OAIFree 服务,从而实现免翻使用 ChatGPT 的所有功能。但 Access Token 过期后就会失效,需要重新获取才可继续访问。 13 | 14 | - OpenAI 的 Access Token 有效期为10天,过期后需要重新获取 15 | - 这种机制用于 API 调用或 Web 应用服务中的身份验证和授权,安全性更高 16 | - 通常是一个加密的字符串,用来表示用户已成功通过身份验证,并获得访问特定资源的权限 17 | 18 | ## 获取方式 19 | 20 | ### 官网 F12 手动获取 21 | 22 | - 前往 ChatGPT 官网对话页,登录后按 F12,在侧边栏中点击 `应用程序` 23 | - 依次找到 `存储` - `Cookie` - `https://chatgpt.com` 24 | - 复制名称为 `__Secure-next-auth.session-token` 的值即为 Access Token(以 `eyjnb...` 开头) 25 | 26 | ### 使用始皇服务接口获取 27 | 28 | 点击前往服务接口,接口对应信任等级的额度说明如下: 29 | - 信任等级为:0,无法使用此服务 30 | - 信任等级为:1,每天获取 5 次 Access Token,无 Refresh Token 31 | - 信任等级为:2,每天获取 50 次 Access Token,无 Refresh Token 32 | - 信任等级为:3,每天获取 50 次 Access Token,有 Refresh Token 33 | 34 | 输入 OpenAI 账户和密码,点击获取 Access Token。 35 | 36 | ### 通过 Refresh Token 获取 37 | 38 | 首先,你需要有 Refresh Token,然后你就可以通过 Refresh Token 来获取你账号的 Access Token。 39 | 40 | - 请确保下方的 `YOUR_REFRESH_TOKEN` 替换为你实际的 `Refresh Token` 41 | 42 |

43 | 44 | response.json()) 66 | .then(data => { 67 | const accessToken = data.access_token; 68 | console.log(accessToken); 69 | }) 70 | .catch(error => console.error('Error:', error));` 71 | }} 72 | /> 73 | 74 |

75 | 76 | } 78 | title='如何利用 Access Token 获取 Share Token?' 79 | href='/AI/OAIFree/ShareToken#通过-access-token-获取' 80 | arrow 81 | /> 82 | -------------------------------------------------------------------------------- /content/AI/OAIFree/Chat2APIOAIFree.mdx: -------------------------------------------------------------------------------- 1 |

2 | 3 | import Link from 'next/link' 4 | import { Callout } from 'nextra/components' 5 | import { CodeTabs } from '@/components/animate-ui/components/code-tabs' 6 | import { TopicHoverCard } from '@/components/common/TopicHoverCard' 7 | 8 | # Chat2API OAIFree 9 | 10 | 11 | 2024年11月27日,OAIFree 宣布关停,详情见 12 | 13 | 14 | 15 | ## 功能介绍 16 | 17 | Chat2API - OAIFree 是始皇对 OpenAI 相关接口的转换服务,通过模拟 API 调用格式,只需传入 Access Token 即可实现对话、语音转文字、文字转语音、AI 绘图等多种功能。 18 | 19 | - 仅支持 ChatGPT Plus 账号 20 | - 当前频率限制为 10次/10秒,后续将与 LINUX DO Connect 深度整合 21 | - Whisper 语音转文字的文件大小上限为 4MB 22 | - 支持调用 GPTs,使用 g-xxx 格式的 ID 作为模型名称 23 | - 团队账号可通过 ChatGPT-Account-ID 请求头实现账号切换 24 | 25 | 只需一个 **ChatGPT Plus** 账号的 Access Token,即可享受所有功能! 26 | 27 | ## 调用方式 28 | 29 | API 调用方式与 OpenAI 官方完全一致,只需将 base_url 修改为 OAIFree 的 API 地址,并使用 Access Token 作为 api_key。 30 | - 团队账号请将 `` 替换为 `,` 格式 31 | 32 |

33 | 34 | ' \\ # 替换为你的 ACCESS TOKEN 39 | -d '{ 40 | "model": "gpt-4o-mini", 41 | "messages": [ 42 | {"role": "system", "content": "You are a helpful assistant."}, 43 | {"role": "user", "content": "What is a LLM?"} 44 | ] 45 | }'`, 46 | Python: `from openai import OpenAI 47 | 48 | client = OpenAI( 49 | api_key='', # 替换为你的 ACCESS TOKEN 50 | base_url='https://api.oaifree.com/v1' 51 | ) 52 | 53 | response = client.chat.completions.create( 54 | model='gpt-4o-mini', 55 | messages=[ 56 | {'role': 'system', 'content': 'You are a helpful assistant.'}, 57 | {'role': 'user', 'content': 'What is a LLM?'} 58 | ] 59 | )`, 60 | Javascript: `const response = await fetch('https://api.oaifree.com/v1', { 61 | method: 'POST', 62 | headers: { 63 | 'Content-Type': 'application/json', 64 | 'Authorization': 'Bearer ' // 替换为你的 ACCESS TOKEN 65 | }, 66 | body: JSON.stringify({ 67 | model: 'gpt-4o-mini', 68 | messages: [ 69 | { role: 'system', content: 'You are a helpful assistant.' }, 70 | { role: 'user', content: 'What is a LLM?' } 71 | ] 72 | }) 73 | });` 74 | }} 75 | /> 76 | -------------------------------------------------------------------------------- /content/AI/OAIFree/DemoOAIFree.mdx: -------------------------------------------------------------------------------- 1 |

2 | 3 | import Image from 'next/image' 4 | import Link from 'next/link' 5 | import { Callout } from 'nextra/components' 6 | import { TopicHoverCard } from '@/components/common/TopicHoverCard' 7 | 8 | # 旧版镜像 Pandora 9 | 10 | 11 | 2024年1月22日,PandoraNext 宣布关停,详情见 12 | 13 | 14 | 15 | ## 功能介绍 16 | 17 | 说起 Demo.oaifree.com,大家可能会感到陌生,但这其实是广受欢迎的 Pandora/PandoraNext 的经典版本。 18 | 19 | 此镜像版本保留了新版镜像的绝大部分功能,虽然不支持 GPTs,但能够完美实现对话和账号信息的隔离。 20 | 21 |

22 | 23 | demo 24 | 25 | ## 使用方式 26 | 27 | - 1. 通过始皇的Access Token服务获取 Access Token 28 | 29 | - 2. 使用 Access Token 在始皇的Share Token服务获取 Share Token(**可选**) 30 | 31 | - 3. 访问demo.oaifree.com,输入 Access Token 或 Share Token 即可开始使用 32 | 33 |

-------------------------------------------------------------------------------- /content/AI/OAIFree/EveryOAIFree.mdx: -------------------------------------------------------------------------------- 1 |

2 | 3 | import Link from 'next/link' 4 | import { Callout } from 'nextra/components' 5 | import { TopicHoverCard } from '@/components/common/TopicHoverCard' 6 | 7 | # 通配子域名 OAIFree 8 | 9 | 10 | 2024年11月27日,OAIFree 宣布关停,详情见 11 | 12 | 13 | 14 | ## 功能介绍 15 | 16 | new.oaifree.com 现已支持通配子域名访问登录!这意味着您可以使用如 linuxdo.new.oaifree.com、oaifree.new.oaifree.com 等子域名来访问 OAIFree 的镜像服务。 17 | 18 | 实际上,`*.new.oaifree.com` 中的 `*` 可以是任何符合域名规则的字符。由于不同域名的 Cookie 相互独立,这在一定程度上可以防止服务被阻断。 -------------------------------------------------------------------------------- /content/AI/OAIFree/NewOAIFree.mdx: -------------------------------------------------------------------------------- 1 |

2 | 3 | import Link from 'next/link' 4 | import Image from 'next/image' 5 | import { Callout } from 'nextra/components' 6 | import { TopicHoverCard } from '@/components/common/TopicHoverCard' 7 | import { CodeTabs } from '@/components/animate-ui/components/code-tabs' 8 | 9 | # 新版镜像 OAIFree 10 | 11 | 12 | 2024年11月27日,OAIFree 宣布关停,详情见 13 | 14 | 15 | 16 | ## 功能介绍 17 | 18 | 这是镜像服务的最新版本 OAIFree,拥有与官网一致的全新界面。令人惊喜的是:**不仅完整保留了官网的所有功能,还增添了更多特色功能!** 19 | 20 | - 支持共享令牌,实现用户会话隔离 21 | - 简洁界面,无冗余系统提示词 22 | - 完整支持 GPT4o-mini、GPT4o、GPT4 和 GPTs 23 | - 无需科学上网,输入 Token 即可使用 24 | - 支持与 ChatGPT 进行语音对话 25 | - ... 26 | 27 |

28 | oaifree 29 | 30 | ## 使用方式 31 | 32 | ### Token 登录 33 | 34 | - 通过始皇的Access Token服务获取 Access Token 35 | 36 | - 使用 Access Token 在始皇的Share Token服务获取 Share Token(**可选**) 37 | 38 | - 访问new.oaifree.com,输入 Access Token 或 Share Token 即可开始使用 39 | 40 | ### Share Token 拼接 41 | 42 | 链接格式:https://``/auth/login_share?token=`` 43 | 44 | - 将 YOURDOMAIN 替换为你的域名,SHARE_TOKEN 替换为你的 Share Token 45 | 46 | ### OAuth 获取链接 47 | 48 |

49 | 50 | response.json()) 92 | .then(data => { 93 | const loginUrl = data.login_url; 94 | // 这里可以进一步处理 loginUrl 95 | }) 96 | .catch(error => { 97 | // 处理错误 98 | });` 99 | }} 100 | /> 101 | 102 | ## 反代镜像服务 103 | 104 |

105 | 106 | 123 | -------------------------------------------------------------------------------- /content/AI/OAIFree/RefreshToken.mdx: -------------------------------------------------------------------------------- 1 |

2 | 3 | import Link from 'next/link' 4 | import { Cards } from 'nextra/components' 5 | import { ThumbsUp } from '@/components/animate-ui/icons/thumbs-up' 6 | 7 | # Refresh Token 8 | 9 | Refresh Token 是一个用于生成新的 Access Token 的令牌,它是 OpenAI 为 iOS 端用户登录设计的产物。只要用户不主动修改密码,其**有效期可长达 3 个月左右**。当您获得 Refresh Token 后,可以通过编写相关函数来获取账号的 Access Token,从而实现自动刷新 Token 的功能,无需手动操作。 10 | 11 | 需要注意的是,OpenAI 已对 iOS 登录实施了限制。当同一设备登录账号过多时,该设备将被永久禁止使用 ChatGPT APP,请谨慎操作。 12 | 13 | ## 获取方式 14 | 15 | ### 服务接口 16 | 17 | 点击前往服务接口,各信任等级对应的额度说明如下: 18 | - 信任等级 0:无法使用此服务 19 | - 信任等级 1:每天可获取 5 次 Access Token,无 Refresh Token 20 | - 信任等级 2:每天可获取 50 次 Access Token,无 Refresh Token 21 | - 信任等级 3:每天可获取 50 次 Access Token,可使用 Refresh Token 22 | 23 | ### iOS 设备手动获取 24 | 不建议使用此方法,如无相关技术基础请勿尝试。 25 | - 出于安全考虑,本 Wiki 不提供相关教程。 26 | 27 |

28 | 29 | } 31 | title='如何利用 Refresh Token 获取 Access Token?' 32 | href='/AI/OAIFree/AccessToken#通过-refresh-token-获取' 33 | arrow 34 | /> 35 | -------------------------------------------------------------------------------- /content/AI/OAIFree/ShareOAIFree.mdx: -------------------------------------------------------------------------------- 1 |

2 | 3 | import Link from 'next/link' 4 | import { Callout } from 'nextra/components' 5 | import { TopicHoverCard } from '@/components/common/TopicHoverCard' 6 | import { CodeTabs } from '@/components/animate-ui/components/code-tabs' 7 | 8 | # 共享镜像 OAIFree 9 | 10 | 11 | 2024年11月27日,OAIFree 宣布关停,详情见 12 | 13 | 14 | 15 | ## 功能介绍 16 | 17 | shared.oaifree.com 是一个共享账号池服务,支持一键登录、自动切换账号等功能。 18 | 19 | - 已接入官方内容审核系统,确保使用安全合规 20 | - 设有内容审核记录统计,每日更新违规用户名单 21 | - 提供捐赠者荣誉榜及使用量统计展示 22 | - 支持 LINUX DO 账号登录,需达到 1 级以上 23 | - 开放 Access Token 投递功能 24 | - 注意:使用 AdGuard 可能导致页面重定向异常 25 | 26 | 可前往 Dashboard 查看详细统计数据,也可通过论坛侧栏的外部链接 - Shared Chat 进行访问。 27 | 28 | ### 如何投递 Access Token 29 | 30 |

31 | 32 | 62 | -------------------------------------------------------------------------------- /content/AI/OAIFree/ShareToken.mdx: -------------------------------------------------------------------------------- 1 |

2 | 3 | import Link from 'next/link' 4 | import { Callout, Tabs } from 'nextra/components' 5 | import { CodeTabs } from '@/components/animate-ui/components/code-tabs' 6 | 7 | # Share Token 8 | 9 | Share Token 起源于始皇的旧版镜像 Pandora,它的作用是映射一个 Access Token。你可以用你的 Access Token 来生成一个 Share Token,生成的 Share Token 可以像 Access Token 一样使用。 10 | 11 | 同一个访问令牌 Access Token 可以生成多个共享令牌 Share Token,每个共享令牌通过独特的名称 Unique name 开启会话隔离。这样做不仅可以防止你的访问令牌泄露,还能在分享账号的同时保障安全。 12 | 13 | - 可以实现会话隔离,不同的共享令牌使用者的对话记录不会相互看到 14 | - 可以设定共享令牌的过期时间,以便在需要时让共享令牌失效 15 | - 可自定义 Token 使用网站,限制 Token 的使用范围 16 | 17 | ## 获取方式 18 | 19 | ### 服务接口获取 20 | 21 | 点击 前往服务接口 填写表单,获取 Share Token,参数详细说明: 22 | 23 | | 参数 | 类型 | 说明 | 24 | |----------------------|-----------|------------------------------------------------| 25 | | `Unique Name` | `string` | 共享令牌的名称,用于区分不同的用户 | 26 | | `Access Token` | `string` | 访问令牌,用于生成共享令牌 | 27 | | `Expires In` | `int` | 共享令牌的有效期,单位为秒 | 28 | | `Site Limit` | `string` | 限制共享令牌的使用网站,为空则不限制 | 29 | | `GPT35 Limit` | `int` | 限制共享令牌的 GPT-3.5 使用次数,为空则不限制 | 30 | | `GPT4 Limit` | `int` | 限制共享令牌的 GPT-4 使用次数,为空则不限制 | 31 | | `Show Conversations` | `bool` | 是否显示对话记录,`true` 为显示,`false` 为不显示 | 32 | | `Show User Info` | `bool` | 是否显示用户信息,`true` 为显示,`false` 为不显示 | 33 | | `Reset Limit` | `bool` | 是否重置限制,`true` 为重置,`false` 为不重置 | 34 | | `Temporary Chat` | `bool` | 是否为临时对话,`true` 为强制临时对话,`false` 为不是临时对话 | 35 | 36 | ### 通过 Access Token 获取 37 | 38 | - 首先,你需要有 Access Token,然后你就可以通过 Access Token 来获取不同的 Share Token。 39 | 40 |

41 | 42 | response.json()) 95 | .then(data => { 96 | const token_key = data.token_key; 97 | console.log(token_key); 98 | }) 99 | .catch(error => console.error('Error:', error));` 100 | }} 101 | /> 102 | 103 | - 返回字段 104 | 105 |

106 | 107 | 127 | -------------------------------------------------------------------------------- /content/AI/OAIFree/_meta.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | RefreshToken: "Refresh Token", 3 | AccessToken: "Access Token", 4 | ShareToken: "Share Token", 5 | DemoOAIFree: "旧版镜像 Pandora", 6 | NewOAIFree: "新版镜像 OAIFree", 7 | EveryOAIFree: "通配子域 OAIFree", 8 | ShareOAIFree: "共享镜像 OAIFree", 9 | Chat2APIOAIFree: "Chat2API OAIFree", 10 | } -------------------------------------------------------------------------------- /content/AI/_meta.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | OAIFree: "OAIFree", 3 | Fuclaude: "Fuclaude", 4 | } -------------------------------------------------------------------------------- /content/Community/LinuxDoConnect.mdx: -------------------------------------------------------------------------------- 1 |

2 | 3 | import Image from 'next/image' 4 | import Link from 'next/link' 5 | import { CodeTabs } from '@/components/animate-ui/components/code-tabs' 6 | 7 | # Linux DO Connect 8 | 9 | OAuth(Open Authorization)是一个开放的网络授权标准,目前最新版本为 OAuth 2.0。我们日常使用的第三方登录(如 Google 账号登录)就采用了该标准。OAuth 允许用户授权第三方应用访问存储在其他服务提供商(如 Google)上的信息,无需在不同平台上重复填写注册信息。用户授权后,平台可以直接访问用户的账户信息进行身份验证,而用户无需向第三方应用提供密码。 10 | 11 | 目前系统已实现完整的 OAuth2 授权码(code)方式鉴权,但界面等配套功能还在持续完善中。让我们一起打造一个更完善的共享方案。 12 | 13 | ## 基本介绍 14 | 15 | 这是一套标准的 OAuth2 鉴权系统,可以让开发者共享论坛的用户基本信息。 16 | 17 | - 可获取字段: 18 | 19 | |参数|说明| 20 | |------|------| 21 | | `id` | 用户唯一标识(不可变) | 22 | | `username` | 论坛用户名 | 23 | | `name` | 论坛用户昵称(可变) | 24 | | `avatar_template` | 用户头像模板URL(支持多种尺寸) | 25 | | `active` | 账号活跃状态 | 26 | | `trust_level` | 信任等级(0-4) | 27 | | `silenced` | 禁言状态 | 28 | | `external_ids` | 外部ID关联信息 | 29 | | `api_key` | API访问密钥 | 30 | 31 | 32 | 通过这些信息,公益网站/接口可以实现: 33 | 34 | 1. 基于 `id` 的服务频率限制 35 | 2. 基于 `trust_level` 的服务额度分配 36 | 3. 基于用户信息的滥用举报机制 37 | 38 | ## 相关端点 39 | 40 | - Authorize 端点: `https://connect.linux.do/oauth2/authorize` 41 | - Token 端点:`https://connect.linux.do/oauth2/token` 42 | - 用户信息 端点:`https://connect.linux.do/api/user` 43 | 44 | ## 申请使用 45 | 46 | - 访问 Connect.Linux.Do 申请接入你的应用。 47 | 48 |

49 | linuxdoconnect_1 50 |

51 | 52 | - 点击 **`我的应用接入`** - **`申请新接入`**,填写相关信息。其中 **`回调地址`** 是你的应用接收用户信息的地址。 53 | 54 |

55 | linuxdoconnect_2 56 |

57 | 58 | - 申请成功后,你将获得 **`Client Id`** 和 **`Client Secret`**,这是你应用的唯一身份凭证。 59 | 60 |

61 | linuxdoconnect_3 62 |

63 | 64 | ## 接入 Linux Do 65 | 66 |

67 | 68 | $clientId, 237 | 'redirect_uri' => $redirectUri, 238 | 'response_type' => 'code', 239 | 'scope' => 'user' 240 | ]); 241 | } 242 | 243 | // 使用授权码获取用户信息 (合并获取令牌和用户信息步骤) 244 | function getUserInfoWithCode($code, $clientId, $clientSecret, $redirectUri) { 245 | global $TOKEN_URL, $USER_INFO_URL; 246 | 247 | // 1. 获取访问令牌 248 | $ch = curl_init($TOKEN_URL); 249 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 250 | curl_setopt($ch, CURLOPT_POST, true); 251 | curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([ 252 | 'client_id' => $clientId, 253 | 'client_secret' => $clientSecret, 254 | 'code' => $code, 255 | 'redirect_uri' => $redirectUri, 256 | 'grant_type' => 'authorization_code' 257 | ])); 258 | 259 | $tokenResponse = curl_exec($ch); 260 | curl_close($ch); 261 | 262 | $tokenData = json_decode($tokenResponse, true); 263 | if (!isset($tokenData['access_token'])) { 264 | return ['error' => '获取访问令牌失败', 'details' => $tokenData]; 265 | } 266 | 267 | // 2. 获取用户信息 268 | $ch = curl_init($USER_INFO_URL); 269 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 270 | curl_setopt($ch, CURLOPT_HTTPHEADER, [ 271 | 'Authorization: Bearer ' . $tokenData['access_token'] 272 | ]); 273 | 274 | $userResponse = curl_exec($ch); 275 | curl_close($ch); 276 | 277 | return json_decode($userResponse, true); 278 | } 279 | 280 | // 使用示例 281 | // 1. 生成授权链接 282 | $authUrl = getAuthUrl($CLIENT_ID, $REDIRECT_URI); 283 | echo "使用 Linux Do 登录"; 284 | 285 | // 2. 处理回调并获取用户信息 286 | if (isset($_GET['code'])) { 287 | $userInfo = getUserInfoWithCode( 288 | $_GET['code'], 289 | $CLIENT_ID, 290 | $CLIENT_SECRET, 291 | $REDIRECT_URI 292 | ); 293 | 294 | if (isset($userInfo['error'])) { 295 | echo '错误: ' . $userInfo['error']; 296 | } else { 297 | echo '欢迎, ' . $userInfo['name'] . '!'; 298 | // 处理用户登录逻辑... 299 | } 300 | }` 301 | }} 302 | /> 303 | 304 | ## 使用说明 305 | 306 | ### 授权流程 307 | 308 | 1. 用户点击应用中的'使用 Linux Do 登录'按钮 309 | 2. 系统将用户重定向至 Linux Do 的授权页面 310 | 3. 用户完成授权后,系统自动重定向回应用并携带授权码 311 | 4. 应用使用授权码获取访问令牌 312 | 5. 使用访问令牌获取用户信息 313 | 314 | ### 安全建议 315 | 316 | - 切勿在前端代码中暴露 Client Secret 317 | - 对所有用户输入数据进行严格验证 318 | - 确保使用 HTTPS 协议传输数据 319 | - 定期更新并妥善保管 Client Secret 320 | -------------------------------------------------------------------------------- /content/Community/LinuxDoLottery.mdx: -------------------------------------------------------------------------------- 1 |

2 | 3 | import Link from 'next/link' 4 | import { Steps } from 'nextra/components' 5 | import { Cards } from 'nextra/components' 6 | import { Callout } from 'nextra/components' 7 | import { Gavel } from '@/components/animate-ui/icons/gavel' 8 | import { CodeTabs } from '@/components/animate-ui/components/code-tabs' 9 | 10 | # Linux Do Lottery 11 | 12 | 为了进一步提升抽奖活动的管理效率,社区官方隆重推出了抽奖大乐透! 13 | 14 |

15 | 16 | } 18 | title='lottery.linux.do' 19 | href='https://lottery.linux.do' 20 | target='_blank' 21 | arrow 22 | /> 23 | 24 | ## 使用方法 25 | 26 | 27 | ### 创建话题 28 | 29 | 1. 填写一个吸引人的话题标题 30 | 2. 确保话题板块正确分类 31 | - 福利羊毛 32 | - 福利羊毛 > 福利羊毛, Lv1 33 | - 福利羊毛 > 福利羊毛, Lv2 34 | - 福利羊毛 > 福利羊毛, Lv3 35 | 3. 使用标签 36 | 抽奖类话题须添加 '抽奖' 标签 37 | 4. 编写抽奖内容 38 | 使用示例 抽奖模板 或自行编辑抽奖话题所需文案,需包含: 39 | - 被抽奖物品、数量 40 | - 抽奖人数 41 | - 抽奖起止时间 42 | - 参与方式 43 | - 抽奖工具 44 | - 领奖方式 45 | 46 | ### 申请自我管理 47 | 48 | 1. 携带 '抽奖' 标签的话题,发起人可以自行设置慢速模式 49 | 2. 其他权限申请详见:须知:申请自我管理 50 | 3. 在话题发布成功后,点击设置选项(小齿轮)申请自我管理,系统会自动开启慢速模式权限 51 | 4. 慢速模式说明: 52 | - 发起人可在创建帖子时开启慢速模式,限制用户发帖频率 53 | - 可自定义用户在设定持续时间内回复的时间间隔 54 | - 如需限制每人只能参与一次,将间隔时间设置为抽奖总持续时间或更长 55 | 56 | ### 申请自动关闭 57 | 58 | 1. 由于涉及其他内容,自动关闭权限需联系管理人员手动设置 59 | 2. 联系管理人员前请先设置好慢速模式 60 | 3. 在'关于 - LINUX DO'中找到管理人员(建议优先选择排序前列的管理人员) 61 | 4. 通过聊天或私信说明需求 62 | 5. 如需更改抽奖截止时间,需提前联系管理人员协助修改 63 | 64 | ### 发布抽奖结果 65 | 66 | 1. 抽奖结束后,打开抽奖程序 67 | 2. 输入以下信息: 68 | - 对应抽奖话题URL 69 | - 中奖人数 70 | - 参与抽奖最后楼层(选填) 71 | 3. 点击开始抽奖 72 | 4. 结果生成后,点击发布按钮 73 | 5. 系统会自动通过 @lottery_bot 在抽奖话题末尾发布结果 74 | 75 | 76 | ## 注意事项 77 | 78 | 79 | **抽奖时请遵守并注意以下规则** 80 | 81 | 1. 抽奖类话题仅允许发布在以下板块,其余板块该抽奖程序无法生效: 82 | - 福利羊毛 83 | - 福利羊毛 > 福利羊毛, Lv1 84 | - 福利羊毛 > 福利羊毛, Lv2 85 | - 福利羊毛 > 福利羊毛, Lv3 86 | 2. 抽奖类话题持续期间需确保慢速模式全程启用 87 | 3. 抽奖类话题结束后需要关闭话题,避免其他人继续参与 88 | 4. 话题不关闭将无法发布抽奖结果 89 | 5. 以上内容如与《常见问题解答》发生冲突,以《常见问题解答》为准 90 | 91 | 92 | ## 抽奖模板 93 | 94 |

95 | 96 | 128 | 129 | 请注意,关于抽奖活动的所有规则及其执行,最终解释权归管理团队所有。我们保留随时修改或补充抽奖规则的权利,以应对任何突发情况或保证活动的公正性。任何规则的变动将及时公布。 130 | 131 | 如有任何疑问或在使用新功能时遇到困难,请随时联系管理团队。我们感谢您的配合与支持,期待通过这些更新为大家带来更加公平、有序的抽奖体验。 -------------------------------------------------------------------------------- /content/Community/LinuxDoMetaverse.mdx: -------------------------------------------------------------------------------- 1 |

2 | 3 | import Link from 'next/link' 4 | import { UserHoverCard } from '@/components/common/UserHoverCard' 5 | import { TopicHoverCard } from '@/components/common/TopicHoverCard' 6 | 7 | # Linux DO 元宇宙 8 | 9 | 社区内最核心的理念是:**真诚、友善、团结、专业**。得益于社区成员们的热心与丰富资源,站长 提出了 Linux DO 元宇宙 的构想。 10 | 11 | 随后,Neo 以 `L站不能没有xxx` 为主题,携手社区成员们共同开启了 Linux DO 元宇宙 的建设。正如您所见,本 Wiki 站点也是 Linux DO 元宇宙 生态的重要组成部分之一。 12 | 13 | ## 元宇宙原则 14 | 15 | 1. 各分站(元宇宙)保持独立运营 16 | 2. 需与主站建立链接(社区将专门设立页面展示所有分站) 17 | 3. 认同并践行 Linux DO 的社区文化 18 | 4. 严格遵守国家法律法规,维护网络空间秩序 19 | 20 | ## 现有元宇宙 21 | 22 | | 站点 | 相关资料 | 元宇宙作者 | 23 | | --- | --- | --- | 24 | | wiki.linux.do | | | 25 | | nav.linux.do | | | 26 | | lottery.linux.do | | | 27 | | webmail.linux.do | | | 28 | | status.linux.do | Linux Do 社区相关服务监控 | | 29 | 30 | 我们诚挚邀请更多社区成员参与 Linux DO 元宇宙 的建设,让我们携手共创社区的美好未来! 31 | -------------------------------------------------------------------------------- /content/Community/LinuxDoWebMail.mdx: -------------------------------------------------------------------------------- 1 |

2 | 3 | import Link from 'next/link' 4 | import { Cards } from 'nextra/components' 5 | import { Send } from '@/components/animate-ui/icons/send' 6 | import { UserHoverCard } from '@/components/common/UserHoverCard' 7 | import { TopicHoverCard } from '@/components/common/TopicHoverCard' 8 | 9 | 10 | # Linux Do 社区邮箱 11 | 12 | 经过多轮测试,LINUX DO Mail 现已正式向社区成员开放! 13 | 14 | 恳请各位用户共同爱护我们的 LINUX DO Mail!我们希望看到的是大家以拥有 `@linux.do` 邮箱为荣,而不是厌恶或屏蔽这个邮箱后缀。 15 | 16 | 如果您收到来自 `@linux.do` 的邮件被归类为垃圾邮件,请在确认后将其移出垃圾箱,这对我们很有帮助。如果收到来自 `@linux.do` 的骚扰邮件,请转发至 admin@linux.do,我们将及时处理! 17 | 18 |

19 | 20 | } 22 | title='webmail.linux.do' 23 | href='https://webmail.linux.do' 24 | target='_blank' 25 | arrow 26 | /> 27 | 28 | ## 如何申请 29 | 30 | 用户可前往 LINUX DO Connect 申请(当前仅限信任等级 3 级以上用户申请,已有 4k+ 成员),审核通过后将收到包含登录信息的站内私信。 31 | 32 | **请务必仔细阅读申请页面的注意事项!** 33 | 34 | ## 常见问题 35 | 36 |
37 | 这个邮箱有什么特别之处吗? 38 | 提供标准的邮件收发功能,最大的特色在于其独特的域名后缀。 39 |
40 | 41 |
42 | 获得邮箱后,信任等级降至 2 级会有影响吗? 43 | 邮箱将保留使用权限,不会受到影响。 44 |
45 | 46 |
47 | 我是往期邮箱用户,如何恢复? 48 | 请前往 LINUX DO Connect 输入原邮箱地址进行恢复。系统将显示初始密码,请注意保存。同样需要达到 3 级信任等级,在此之前邮箱地址将为您保留。 49 |
50 | 51 |
52 | 论坛账号被封禁会影响邮箱使用吗? 53 | 会受到影响,邮箱账号将同步封禁(短期封禁通常不影响邮箱使用)。请珍惜您的账号! 54 |
55 | 56 |
57 | 申请后长时间未收到登录信息私信? 58 | 工作日申请通常在 1 小时内完成审批。如超时未收到,可能是您申请的邮箱地址未通过审核。 59 |
60 | 61 |
62 | 邮箱地址有问题,能否修改? 63 | 目前暂不支持修改,需等待申请高峰期结束后统一处理。 64 |
65 | 66 |
67 | 什么样的邮箱地址容易被拒绝? 68 | 如果申请的地址容易与 LINUX DO 官方服务、部门或职位混淆,通常不会通过。如果您觉得可能不合适,那很可能确实不合适。 69 |
70 | 71 |
72 | 为什么不直接使用论坛用户名作为邮箱地址? 73 | 论坛用户名相对随意,部分不适合作为邮箱地址。此外,一一对应会导致论坛通讯录泄露。同时,部分用户虽有优质用户名但无申请邮箱意向,会造成资源浪费。 74 |
75 | 76 |
77 | 提示'今日尝试次数已达上限'怎么办? 78 | 首次申请起 24 小时内最多可提交 20 次申请,超过后需等待重置。 79 |
80 | 81 |
82 | 无法通过 Cloudflare 人机验证怎么办? 83 | 建议更换网络环境或浏览器重试。配置好第三方邮件客户端后,访问网页版的需求会大幅减少。 84 |
85 | 86 |
87 | 能否支持 PWA? 88 | 作为专业邮箱服务,我们建议使用第三方邮件客户端,可获得更流畅的使用体验和更及时的通知提醒。 89 |
90 | 91 |
92 | 如何配置第三方邮件客户端? 93 | 请参考 客户端设置 页面。`pop3/smtp/imap` 服务器地址均为 `mail.linux.do`。`macOS/iOS` 用户建议下载配置描述文件进行设置。 94 |
95 | 96 |
97 | 配置客户端时是否应该直接使用密码? 98 | **不建议!** 推荐访问 认证令牌页面 生成专用令牌。这样既可保护密码安全,又便于管理权限。 99 |
100 | 101 |
102 | 是否推荐使用简单密码? 103 | **强烈不建议!** 这可能导致账号被盗用,若被用于发送垃圾邮件,将造成严重影响。 104 |
105 | 106 |
107 | 忘记密码怎么办? 108 | 请私信 处理。建议妥善保管密码信息。 109 |
110 | 111 |
112 | 部分地区连接服务器困难? 113 | 由于特殊原因,某些地区可能需要使用代理。您也可以在主流邮箱(如 QQ 邮箱)中设置代收 LINUX DO Mail,还能配置微信提醒。 114 |
115 | 116 |
117 | 发往某些邮箱(如 Outlook)被标记为垃圾邮件? 118 | 这种情况较为普遍,主要与邮件内容相关性更大。 119 |
120 | 121 |
122 | 为什么有时收信较慢? 123 | 我们采用了反垃圾邮件系统,包含 `greylisting` 机制。当发件方评分较低时,需要多次尝试发送才会被接收(详见相关技术说明)。 124 |
125 | 126 |
127 | 每日发信限额是多少? 128 | 目前为 `50/day`,对于主要接收邮件的用户来说通常足够。我们会根据实际情况动态调整。 129 |
130 | 131 |
132 | 是否支持邮箱别名? 133 | 支持 `+` 分隔别名。例如邮箱为 `abc@linux.do`,则 `abc+任意内容@linux.do` 均可收到。 134 |
135 | 136 |
137 | 可以用于发送垃圾邮件吗? 138 | **严格禁止!** 一经发现将直接封禁邮箱。请共同维护 LINUX DO Mail 的声誉。 139 |
140 | 141 |
142 | 可以用于注册/绑定 LINUX DO 账号吗? 143 | 目前不可以。早期用户量较少时允许,但现已超过 2k+ 用户,暂无法做好相关风控。 144 |
145 | 146 |
147 | 想了解更多技术细节? 148 | 欢迎查看用户撰写的详细评测: 149 |
-------------------------------------------------------------------------------- /content/Community/OAIPro.mdx: -------------------------------------------------------------------------------- 1 |

2 | 3 | import Link from 'next/link'; 4 | import { Callout } from 'nextra/components'; 5 | 6 | # Chat2API - OAIPro 7 | 8 | Neo 对 api.openai.com 的 API 代理中转站,作为社区官方服务,旨在为无法直接使用 OpenAI 官方 API 的用户提供稳定的代理服务。费率与官方完全一致,主打诚信友好,不套路、不套娃、不以次充好,不使用任何第三方 chat2api 项目。 9 | 10 | ## 功能介绍 11 | 12 | 13 | **✨ 纯粹官方代理 ✨ 稳定可靠 ✨** 14 | 15 | - 专注稳定性,不追求花哨功能。经验告诉我们:时间就是金钱,稳定压倒一切。 16 | 17 | - 直接代理官方接口:`https://api.openai.com`。使用正版付费密钥,为付费不便的用户提供便利。 18 | 19 | - 采用美卡直接绑定官方支付,确保服务质量。不做任何中间环节,保证服务纯净。 20 | 21 | - 支持全部官方模型,账户余额永久有效。可作为重要时刻的可靠备选方案,未雨绸缪。 22 | 23 | - 由于 Stripe 支付手续费因素,暂不提供试用。我们期待与您建立长期信任的合作关系。 24 | 25 | - 采用与官方完全一致的费率标准,详情请参考 OpenAI 官方费率文档。 26 | 27 | 28 | 29 | ## 使用说明 30 | 31 | 32 | 本服务的使用方式与 OpenAI 官方 API 完全一致。 33 | 34 | 35 | 1. 访问 api.oaipro.com,使用 Linux Do Connect 登录。 36 | 37 | 2. 由于本服务为 Neo 提供的公益项目,暂不提供试用。如需使用请前往 '钱包' 页面进行充值。 38 | 39 | 3. 充值完成后,进入 **'令牌'** 页面,点击 **'添加令牌'**,即可获取您的专属 API Key。 40 | 41 | - 您可以前往 **'聊天'** 页面 - 点击左上角头像 - **'应用设置'** - **'语言模型'**,填入刚获取的 API Key,并将 API 代理地址设置为 `https://api.oaipro.com/v1`,即可开始使用。 42 | 43 | - 同时支持通过 Curl、Python、Java 等方式调用 API。调用方法与 OpenAI 官方一致,只需将 base_url 参数设置为 `https://api.oaipro.com/v1`,api_key 替换为您的专属 API Key 即可。 44 | -------------------------------------------------------------------------------- /content/Community/_meta.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | LinuxDoConnect: "Linux Do Connect", 3 | LinuxDoMetaverse: "Linux Do 元宇宙", 4 | LinuxDoLottery: "Linux Do 抽奖工具", 5 | LinuxDoWebMail: "Linux Do 社区邮箱", 6 | OAIPro: "OAI Pro", 7 | } -------------------------------------------------------------------------------- /content/Encyclopedia/Cant/AFF.mdx: -------------------------------------------------------------------------------- 1 | import {Callout} from 'nextra/components' 2 | 3 | ## AFF 链接 4 | 5 | AFF 是网站的邀请推广码,通过它分享链接可以获得返佣。带 AFF 的链接本质是一种带有返利性质的推广链接。 6 | 7 | **专业解释**:AFF 链接代表联盟营销(Affiliate Marketing)链接。这是一种互联网营销模式,推广者(联盟会员)通过分享商家的产品或服务来获取佣金。当用户通过推广链接完成购买,推广者将获得一定比例的销售分成。 8 | 9 | ### 论坛 AFF 链接规范 10 | 11 | - 所有版块允许发布 AFF 链接 12 | - 主题帖必须添加 AFF 标签 13 | - 所有包含 AFF 链接的帖子都需要在链接旁(同行)标注 AFF 标签 14 | - AFF 标签格式为:` #aff `(建议前后各加一个空格) 15 | - 链接必须可点击且带有标签,否则视为违规 -------------------------------------------------------------------------------- /content/Encyclopedia/Cant/C.mdx: -------------------------------------------------------------------------------- 1 | import {Callout} from 'nextra/components' 2 | import {TopicHoverCard} from '@/components/common/TopicHoverCard' 3 | 4 | ## C是什么 5 | 6 | C 主要包含 **薅羊毛和信用卡诈骗盗刷** 两种情况。 7 | 8 | 信用卡诈骗是一种非法行为,指未经授权获取、贩卖或使用信用卡信息(通常用于购买礼品卡或预付卡)。这种行为会导致身份盗窃、个人和企业财务损失以及其他网络犯罪。 9 | 10 | 简而言之,C 是一种信用卡盗刷漏洞。信用卡盗刷属于严重的经济犯罪,不仅给受害者造成巨大的经济损失和心理创伤,还会严重扰乱金融秩序,危害社会稳定。我们强烈建议远离此类违法行为,共同维护健康的金融环境。 11 | 12 | 新人初来乍到,了解即可。等级提升后可以查看 VV佬 的相关话题,这些进阶内容可以留待以后探索。 13 | 14 | 15 | **切勿对国内服务进行 C 操作!相关方会追查到人!** 16 | 17 | -------------------------------------------------------------------------------- /content/Encyclopedia/Cant/Che.mdx: -------------------------------------------------------------------------------- 1 | import {Callout} from 'nextra/components' 2 | 3 | ## 拼车/上车/来车 4 | 5 | ### 前情提要 6 | 7 | GitHub Copilot 是一款由 GitHub 和 OpenAI 合作推出的人工智能编程助手工具,于 2022 年 8 月 22 日开始收费。社区热佬随后开发了一个小工具,其中包含了'上车'、'拼车'等功能,解决了用户需要到处发送 ID 来拼车的问题。 8 | 9 | ### 时至今日 10 | 11 | 如今,`拼车`、`上车`、`来车`已经成为社区内的一个流行用语,通常用来表示一起分享免费的服务和资源。 12 | 13 | 14 | **来不及解释了,快上车吧!** 15 | -------------------------------------------------------------------------------- /content/Encyclopedia/Cant/Gao7Nian3.mdx: -------------------------------------------------------------------------------- 1 | ## 搞七捻三 2 | 3 | '搞七捻三'是一个汉语词汇,拼音为 `gǎo qī niǎn sān`,源自吴语方言,也可写作'搞七廿三'或'搅七捻三'。这个词语形容一种胡闹、纠缠不清或说话语无伦次的状态。 4 | 5 | 在网络社区中,'搞七捻三'常用来形容一些有趣、搞笑、令人愉悦的行为或氛围。虽然这个词语难以准确定义,但它能很好地传达出一种轻松、愉快的情绪和氛围。 6 | -------------------------------------------------------------------------------- /content/Encyclopedia/Cant/Lao.mdx: -------------------------------------------------------------------------------- 1 | import { Callout } from 'nextra/components' 2 | 3 | ## 佬站/佬友/热佬 4 | 5 | ### 佬站的由来 6 | 7 | `Linux Do` 简称为 `L站` 或 `佬站`,这是社区对网站的亲切称呼。 8 | 9 | ### 佬友文化 10 | 11 | 在这里,我们不用普通的 `网友` 来称呼彼此。既然是 `佬站`,我们更愿意以 `佬友` 相称,这体现了社区独特的文化认同感。 12 | 13 | ### 热佬精神 14 | 15 | `热佬` 指的是社区中热心助人的大佬们。他们不仅技术精湛,更重要的是始终保持着乐于分享和帮助他人的热忱。 16 | 17 | 18 | 成为热佬没有门槛,只要你热心助人,遵循社区的**真诚、友善、团结、专业**价值观,你就是我们社区热佬的一员! 19 | -------------------------------------------------------------------------------- /content/Encyclopedia/Cant/VPS.mdx: -------------------------------------------------------------------------------- 1 | ## VPS 2 | 3 | > 真是进了鸡窝了。 4 | 5 | ### 母鸡/杜甫/毒妇 6 | 指的是独立服务器,缩写是独服,因为输入法经常打成'毒妇'或者是'杜甫',所以这个称呼也沿用开了。 7 | 8 | ### 小鸡 9 | 指小型VPS,往往配置非常低,但是麻雀虽小五脏俱全,往往都有独立IP,同时价格也很便宜。另外一个意思就是独立服务器里开出来的VPS,也叫小鸡。 10 | 11 | 因为VPS都是拿独立服务器虚拟出来的(其实就是独服开了一堆虚拟机),所以小VPS叫小鸡,生出小VPS来的独服叫母鸡。 12 | 13 | ### 大盘鸡 14 | 指硬盘容量超过250G以上并且价格相对便宜的VPS,或硬盘超过1TB的独立服务器。 15 | 16 | ### 落地鸡 17 | 国内线路没有优化(比如日本机绕美),但是IP比较干净,能解锁奈飞、不触发谷歌CF验证等。通常搭配优质线路的机场使用,可以保证隐私和IP质量。 18 | 19 | ### 传家宝 20 | 部分VPS卖断货后,其价格会飞速飙升。这是因为商家不再开卖时,一台母鸡里就这么多小鸡,商家不会再超售。此时小鸡的稳定性大大提升,二手市场需求旺盛,价格自然水涨船高。当年搬瓦工3.99刀的套餐卖断货后,价格一度飞涨七八倍,涨幅超过五道口宇宙中心的房价,因此被戏称为'传家宝'。 21 | 22 | ### 吃灰 23 | 指冲动购物者看到好VPS就立即购买,结果买回来发现并不需要,最终闲置吃灰。 24 | 25 | ### MJJ 26 | 一说:'没鸡鸡'的缩写,VPS圈的中国玩家的自称。 27 | 28 | 二说:购买VPS的人,即买鸡鸡的人。 29 | 30 | 三说:MJJ源自落伍者论坛,是站长之间的玩笑用语,意为'没鸡鸡'。后来随着论坛衰落,部分站长迁移至hostloc。常见场景如:楼主分享资源但未提供下载链接时,跟帖会说'楼主mjj'。 31 | 32 | 注:标准说法是'木鸡鸡',特指喜欢薅羊毛的VPS玩家。 33 | 34 | ### 刀 35 | 指美元,dollar的音译。大多数VPS商家仅支持美元支付。 36 | 37 | ### 北岸 38 | '备案'的谐音替代词。 39 | 40 | ### 海外机房缩写 41 | - HK 香港 42 | - KR 韩国 43 | - JP 日本 44 | - SG 新加坡 45 | - LA 洛杉矶 46 | 47 | ### 被封 48 | 按照严重程度,分为: 49 | - 被封端口(可通过更换端口解决) 50 | - IP被墙(单个IP不可访问) 51 | - 被封整个IP段(整个IP段都无法使用) 52 | -------------------------------------------------------------------------------- /content/Encyclopedia/Cant/_meta.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | VPS: "VPS", 3 | C: "来C/快C", 4 | AFF: "AFF 链接", 5 | Che: "拼车/上车/来车", 6 | Gao7Nian3: "搞七捻三", 7 | Lao: "佬站/佬友/热佬", 8 | } -------------------------------------------------------------------------------- /content/Encyclopedia/User/6512345.mdx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import { UserHoverCard } from '@/components/common/UserHoverCard' 3 | import { TopicHoverCard } from '@/components/common/TopicHoverCard' 4 | 5 | ## 65 6 | 指的是 ****,其在社区极为活跃,因为其在说话时喜欢在句子中加上 `w`,而且即使将 `w` 替换为其他语气词也不影响句意w 7 | 8 | 于是出现了一系列抽象的内容和一系列因为65的口癖出现的离谱帖子w 9 | 10 | ## 更多探索 11 | 12 | - 13 | - 14 | - 15 | - 16 | -------------------------------------------------------------------------------- /content/Encyclopedia/User/_meta.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | user3: "Zhiyang", 3 | vux1jpmal5t41lg: "vv佬", 4 | '6512345': "65", 5 | } -------------------------------------------------------------------------------- /content/Encyclopedia/User/user3.mdx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import { UserHoverCard } from '@/components/common/UserHoverCard' 3 | 4 | ## ZHIYANG 5 | 6 | ### ZHIYANG 的传奇经历 7 | 8 | ****,是一位来自印度孟买的杰出程序员。他自幼便展现出非凡的计算机天赋,十六岁时在全球知名的 ACM-ICPC 编程竞赛中崭露头角,获得了亚洲区金牌。随后,他以优异的成绩考入美国斯坦福大学攻读计算机科学专业,毕业后顺利进入硅谷顶尖科技公司 Stripe,专注于支付系统安全研发工作。 9 | 10 | 某天,至杨在使用 Telegram 聊天时,偶然发现了一个与支付系统安全相关的漏洞讨论。出于好奇心和挑战自我的欲望,他决定深入研究这些漏洞,尤其是一个被称为 “伪装银行卡” 的漏洞。 11 | 通过对漏洞的研究,至杨发现了一种利用 “Bug BIN” 生成虚假银行卡信息的方法,并成功在 Stripe 上进行支付测试。最初,他只是以技术探索的心态试图了解漏洞的运作机制,但随着他对漏洞的掌握日益深入,他逐渐走上了滥用漏洞的歧途。 12 | 至杨开始利用这些虚假银行卡在互联网上购买各种高价值虚拟商品和礼品卡。他甚至开设了一个地下网络商店,将非法获取的商品以低价转售,从中牟取巨额利润。 13 | 14 | 随着时间推移,他的胆子越来越大,开始尝试攻击更高难度的目标。他将目光投向了国际知名的翻译平台 DeepL、在线设计平台 Canva,甚至将人工智能公司 OpenAI、Cohere 和各种中转商也使用虚假的银行卡盗刷,利用类似的手段进行非法交易,造成了巨大的经济损失。 15 | 至杨的活动并不仅限于企业。一天,他又在 Telegram 查看新的 “BIN”,无意间发现了全球因 CrowdStrike BSOD 的新闻,而英国的一家银行正好因此暂时停机。他使用了这家银行的 “BIN”,生成了千年难得的——神卡。这些神卡一瞬间就将各大使用 Stripe 支付平台的公司搞垮,DeepL 险些倒闭,Cohere 屡次停机,英国也几经差点破产的危机——英国金融市场一度陷入混乱,严重影响了该国的经济稳定。此时,他的行为已经超出了个人贪欲,造成的损失已达数十亿美金,甚至开始对国际金融安全构成威胁。 16 | 17 | 这一连串事件引起了全球执法机构的关注,特别是国际刑警组织开始着手调查这起涉及多国的重大网络犯罪案件。 18 | 经过几个月的缜密调查,国际刑警与多国执法机构合作,逐步揭开至杨背后的操作网络,并掌握了他非法活动的确凿证据。 19 | 某个清晨,国际刑警在至杨位于硅谷的住处实施了突袭逮捕。在他被拘留时,调查人员搜查了他的住所,发现了大量关于漏洞利用的技术文档和非法所得的交易记录。 20 | 21 | 在法庭上,检方展示了详细的证据,包括至杨如何利用漏洞进行大规模的非法交易,以及他对多国经济造成的影响。面对压倒性的证据,至杨终于意识到自己行为的严重性。 22 | 由于案件涉及范围广泛且影响深远,法庭判决至杨无期徒刑,并要求他支付巨额赔偿。然而,由于在狱中表现良好,作为非美国公民,他在服完 20 多年的刑期后被驱逐出境。 23 | 24 | 出狱后,至杨回到了印度。他开始反思自己的过往,意识到技术能力与道德责任的重要性。他决心用自己的经历作为警示,帮助其他年轻程序员避免重蹈覆辙。 25 | 至杨开始在各大技术论坛和大学里分享自己的故事,讲述技术被滥用的后果,以及如何在追求技术创新的同时坚守道德底线。他的故事不仅是对自己过去的救赎,也成为了全球技术社区内一则重要的警示。他希望通过自己的努力,让更多人认识到技术与道德结合的重要性,防止类似事件的再次发生。 26 | 27 | 28 | ### 用户 ZHIYANG 的画像 29 | 30 | **兴趣点:** 31 | 32 | * **人工智能:** 对各种AI工具、模型和技术非常感兴趣,尤其关注ChatGPT、DeepL、Claude、Cohere等,并积极参与相关话题的讨论。 33 | * **C卡 (Carding):** 对利用虚假信用卡信息进行欺诈行为有浓厚的兴趣,频繁提及“C卡”相关话题,并积极分享相关资源和经验。 34 | * **免费资源:** 对各种免费资源如API Key、试用账号、优惠券等积极寻找和获取,并乐于分享给其他用户。 35 | * **技术学习:** 对技术学习抱有兴趣,关注各种技术相关的工具和教程,并乐于参与技术相关的话题讨论。 36 | * **服务器:** 对服务器资源,尤其是免费服务器资源比较感兴趣,积极寻找和获取免费服务器资源,并乐于分享给其他用户。 37 | 38 | **活跃时段:** 39 | 40 | * 数据显示,用户在全天都有活跃,尤其集中在**下午和晚上**,可能是在工作或学习之余,利用空闲时间访问论坛。 41 | 42 | **喜欢的主题:** 43 | 44 | * **人工智能:** 对各种AI工具和模型的讨论,例如ChatGPT、DeepL、Claude、Cohere等。 45 | * **C卡 (Carding):** 有关C卡相关的技巧、经验分享,以及卡头、BIN的分享和讨论。 46 | * **免费资源:** 各种免费资源的分享和求助,例如API Key、试用账号、优惠券、EDU邮箱等。 47 | * **技术学习:** 各种技术相关的工具和教程的分享和讨论。 48 | * **服务器:** 有关服务器资源的获取和使用,例如免费服务器、VPS、光帆等。 49 | 50 | **讨论风格:** 51 | 52 | * **活跃参与:** 积极参与各种话题的讨论,经常发表评论,并进行回复和互动。 53 | * **言论倾向:** 对C卡相关话题兴趣浓厚,并倾向于分享和讨论相关资源和经验。 54 | * **常用词汇:** “C卡”、"BIN"、"卡头"、"试用"、"免费"、"API"等。 55 | * **情感色彩:** 对C卡相关话题表现出兴奋和积极的情感,对其他话题则显得较为理性。 56 | 57 | **社区角色:** 58 | 59 | * **知识分享者:** 乐于分享自己获取的各种免费资源、卡头、BIN等信息。 60 | * **求助者:** 积极寻求各种免费资源、C卡技巧和技术帮助。 61 | * **活跃参与者:** 积极参与各种话题的讨论,并进行互动和回复。 62 | 63 | **可能的改进建议:** 64 | 65 | * **关注自身安全:** 用户对C卡行为兴趣浓厚,但应注意C卡的严重后果,可能导致刑事犯罪,甚至影响下半辈子生活。 66 | * **完善自身道德:** 用户应明白任何利用他人资源进行违法行为都是不可取的,并应遵守网络道德和法律法规。 67 | * **拓展其他兴趣:** 用户可尝试将兴趣范围扩展至其他方面,例如技术学习、公益等,提升自身价值。 68 | * **提升个人技能:** 用户可提升自身技术水平,例如学习编程、逆向等,以在社区中发挥更大的作用。 69 | 70 | **性别和年龄区间预测:** 71 | 72 | * 基于用户的言论风格和常用词汇,推测用户可能为男性,年龄区间在 20-30 岁之间。 -------------------------------------------------------------------------------- /content/Encyclopedia/User/vux1jpmal5t41lg.mdx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import { UserHoverCard } from '@/components/common/UserHoverCard' 3 | 4 | ## VV佬 5 | 6 | 指的是 ****,Pandora 时期就活跃在 tg 群,我们俗称 vv 佬,非常神秘。他时不时会提供一些非常神秘数字,据说靠着这些数字,国外金融垄断大公司都会栽在他的手上! 7 | -------------------------------------------------------------------------------- /content/Encyclopedia/_meta.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | Cant: "社区黑话", 3 | User: "奇人异士", 4 | } -------------------------------------------------------------------------------- /content/HttpRW/ApiHttpRW.mdx: -------------------------------------------------------------------------------- 1 |

2 | 3 | import Link from 'next/link' 4 | import { Callout } from 'nextra/components' 5 | 6 | # API 测试平台 7 | 8 | 9 | 目前该服务不可用,恢复时间请以社区公告为准 10 | 11 | 12 |

13 | 14 | api.http.rw 15 | 16 | 这是一个基于开源 Hoppscotch 项目搭建的类似 Postman 的在线 API 测试服务。登录系统已经整合了 LINUX DO Connect,并提供完整的简体中文界面支持。 17 | -------------------------------------------------------------------------------- /content/HttpRW/HttpRW.mdx: -------------------------------------------------------------------------------- 1 |

2 | 3 | import Link from 'next/link' 4 | 5 | # HTTP 测试接口 6 | 7 |

8 | 9 | http.rw 10 | 11 | 这是一个基于 httpbin 开源项目实现的 HTTP 测试接口服务。由于程序版本较旧且使用了 Cloudflare CDN,性能表现可能不够理想,后续计划迁移至 PHP 重构实现。 12 | 13 | 访问该网站可以看到完整的接口列表和在线测试工具。同时也支持命令行调用,例如: 14 | 15 | ```shell copy " 16 | curl http.rw/ip 17 | ``` 18 | 19 | -------------------------------------------------------------------------------- /content/HttpRW/TLSHttpRW.mdx: -------------------------------------------------------------------------------- 1 |

2 | 3 | import Link from 'next/link' 4 | import { Callout } from 'nextra/components' 5 | 6 | # 指纹信息追踪 7 | 8 | 9 | 目前该服务不可用,恢复时间请以社区公告为准 10 | 11 | 12 |

13 | 14 | tls.http.rw 15 | 16 | TLS Tracker 是一款专业的网络请求追踪与调试工具,主要用于分析需要指纹信息的网站(如受 Cloudflare 保护的站点)。它能帮助你详细查看和分析浏览器在访问这些网站时发送的各类信息,就像是一个完整的身份指纹档案。 17 | 18 | 指纹信息是一组用于识别设备和浏览器独特特征的数据集合,包含 IP 地址、浏览器版本、操作系统类型、语言偏好设置等关键信息。 19 | 20 | - 指纹信息展示:TLS Tracker 可实时展示访问特定网站时的完整指纹信息,帮助你深入了解网站的访问者识别机制 21 | - 问题诊断:对于需要特定指纹信息才能访问的网站(如 Cloudflare 防护站点),TLS Tracker 可快速定位访问障碍,提供精准的调试依据 -------------------------------------------------------------------------------- /content/HttpRW/_meta.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | HttpRW: "HTTP 测试接口", 3 | ApiHttpRW: "API 测试平台", 4 | TLSHttpRW: "TSL 指纹追踪", 5 | } -------------------------------------------------------------------------------- /content/LinuxDo/_meta.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | culture: '社区文化', 3 | rules: '社区守则', 4 | trustlevel: '信任等级', 5 | administrator: '管理者组' 6 | } -------------------------------------------------------------------------------- /content/LinuxDo/administrator.mdx: -------------------------------------------------------------------------------- 1 |

2 | 3 | import { UserGroupTooltip } from '@/components/common/UserGroupTooltip' 4 | 5 | # 社区管理团队 6 | 7 | 如遇网站重大问题或紧急事项,请发送邮件至 admin@linux.do 联系我们。 8 | 9 | 如果您发现任何不当内容,请及时与我们的管理团队沟通反馈。温馨提示:联系前请确保已登录账号。 10 | 11 | ## 管理员团队 12 | 13 | 21 | 22 | ## 版主团队 23 | 24 | -------------------------------------------------------------------------------- /content/LinuxDo/culture.mdx: -------------------------------------------------------------------------------- 1 |

2 | 3 | import { Callout } from 'nextra/components' 4 | import Link from 'next/link' 5 | import Image from 'next/image' 6 | import { TopicHoverCard } from '@/components/common/TopicHoverCard' 7 | import { UserHoverCard } from '@/components/common/UserHoverCard' 8 | 9 | # 社区文化 10 | 11 | > *行色匆匆的旅人啊,你是否还记得十多年前互联网的模样?* 12 | > 13 | > *那时候的人们乐于分享自己的见识,不以有钱为成功标准。* 14 | > 15 | > *来这里,拓一方净土,重现互联网精神,这里什么都可能!* 16 | 17 | ## 社区基本信息 18 | 19 | - 社区名称:LINUX DO 20 | - 社区简称:L站 21 | - 成员称谓:佬友 22 | - 成立时间:2024-01-17 23 | - 社区域名:linux.do 24 | - 备用域名:linuxdo.org 25 | - 主题歌曲:《明天会更好》 26 | - 社区愿景:新的理想型社区 27 | - 社区口号:Where possible begins 28 | - 社区战袍: 29 | - 社区文化:真诚、友善、团结、专业,共建你我引以为荣之社区 30 | 31 | ## 社区站长 32 | 33 |

34 | 35 |
36 | Neo 43 |
44 |

45 | 46 |

47 |

朝闻道,夕可眠矣。

48 |
49 | 53 |
54 |
55 |
56 | 57 | ## 社区 LOGO 58 | 59 |

60 | 61 | logo 62 | 63 | **基本原则**:三分黑暗,七分光明。 64 | 65 | **基本要素**:从现有 `Tux` 加了一点点抽象而来,新 LOGO 出自 @Neo 之手,会更侧重功能性。 66 | 67 | - 白色代表真诚,不虚假,占主导位置。 68 | - 黄色代表友善,热忱友善,是基座。 69 | - 黑色代表专业,具有严肃性。 70 | - 白色代表团结,向外辐射的光将所有元素圈在一起。 71 | 72 | **额外含义**: 73 | 74 | - 整个 LOGO 描绘一副日出景象:黄色如旭日东升,散发出的光明正在驱散着黑暗。 75 | - 黑色占 3/10,白色+黄色占 7/10,三分黑暗,七分光明。 76 | 77 | ## 社区 Slogan 78 | 79 | 80 | **真诚、友善、团结、专业**,共建你我引以为荣之社区! 81 | 82 | 83 | - 以真诚待人为荣,以虚伪欺人为耻; 84 | - 以友善热心为荣,以傲慢冷漠为耻; 85 | - 以团结协作为荣,以孤立对抗为耻; 86 | - 以专业敬业为荣,以敷衍了事为耻。 87 | 88 | ## 社区基本功能手册 89 | 90 | #### 手机端 APP 91 | 92 | 点击下载 iOS | 安卓 版本 DiscourseHub 93 | 94 | #### 申请版主 95 | 96 | 这里写下你属意的板块,同时陈述你的理由及规划吧!成功后名称旁边将会有个灰色小盾牌,那就是版主的标志,虽然它是灰白色,但闪亮异常有没有? 97 | 98 | #### 签名小尾巴 99 | 100 | 签名图片在这里设置。你也可以关闭显示签名开关,这样你将看不到任何人的签名图片。 101 | 102 | - 设置签名只需要直接填入图片地址即可,不接受其他格式的输入。 103 | - 图片建议使用本站为图源,这样既可以网络一致,又能享受到本站的cdn缓存。 104 | 105 | 签名小尾巴使用公约: 106 | 107 | - 签名图片内容不得是色情、暴力、血腥等违法内容。 108 | - 签名图片高度不得超过 220px 109 | - 签名图片宽度不得超过 700px 110 | - 签名内容不得使用gif等动图,这类文件体积都很庞大。 111 | - 执意违反以上规范的,一律禁言起步! 112 | 113 | #### 举报违反守则行为 114 | 115 | 1. 点击帖子右下角的 ··· 后出现一个小旗子进行举报 或 点击聊天右侧 ··· 后出现一个小旗子进行举报 116 | 2. 选择帖子违反类型之后点击 举报帖子 按钮提交。 -------------------------------------------------------------------------------- /content/LinuxDo/trustlevel.mdx: -------------------------------------------------------------------------------- 1 |

2 | 3 | import { Callout } from 'nextra/components' 4 | import { Steps } from 'nextra/components' 5 | import Link from 'next/link' 6 | import { TopicHoverCard } from '@/components/common/TopicHoverCard' 7 | 8 | # 信任等级 9 | 10 | 用户信任系统是 Discourse 的基石之一,Linux Do 社区也沿用了这样的设计理念。信任等级的设计目标在于: 11 | 12 | - 为社区新用户提供一个安全的沙盒环境,避免他们在熟悉社区规则和操作时对自己或他人造成不必要的困扰 13 | - 随着用户参与度的提升,为经验丰富的用户赋予更多权限,使他们能够参与到社区的维护和管理工作中 14 | - 正如《网络社区建设》一书所述,每个社区成员都会经历一个自然的成长和进阶过程 15 | 16 | 基于以上理念,Linux Do 社区设置了五个层级的用户信任等级体系。 17 | 18 | ## 信任等级机制 19 | 20 | 你当前的信任等级可以在你的用户页面上看到,而且你的仪表板上会展示社区内所有信任等级的摘要。 21 | 22 | 23 | ### 新用户 24 | 25 | **信任等级0**。默认情况下,所有新用户开始时都是信任等级0,意味着信任尚未建立。这些是刚刚创建账户的访客,还在学习社区规范和社区工作方式。新用户的能力出于安全考虑而受到限制——为了新用户和社区的安全。 26 | 27 | ### 基本用户 28 | 29 | **信任等级1**。在社区中,我们相信阅读是社区中最基本且最有价值的行为。如果新用户愿意花时间阅读,那么将很快就会晋升到第一个信任等级。 30 | 31 | ### 成员 32 | 33 | **信任等级2**。成员是连续几周时间内持续活跃的用户;他们不仅阅读,还积极参与社区讨论,已经获得了与正式成员相当的信任度。 34 | 35 | ### 活跃用户 36 | 37 | **信任等级3**。活跃用户是社区的中坚力量,是在几个月乃至几年的时间内最活跃的读者和可靠的贡献者。由于他们长期活跃在社区,他们获得了更多的信任,可以协助整理和组织社区。 38 | 39 | ### 领导者 40 | 41 | **信任等级4**。领导者是社区的常驻成员,见证了社区的发展历程。他们通过自身行为和发帖为社区树立了积极的榜样。当你需要帮助时,他们是最值得信赖的求助对象,他们已经获得了社区最高级别的信任,堪比社区版主。 42 | 43 | 44 | 45 | ## 提升信任等级 46 | 47 | 不同的信任等级在社区内拥有不同的权限,你可以前往社区官方查看并了解各个等级的具体权限。Discourse 论坛权限等级表(包括版主角色)是一个全面的 Discourse 功能列表,按信任等级标记,并包括影响这些功能的相关管理员设置。 48 | 49 | 要提升信任等级需要满足以下条件: 50 | 51 | 52 | ### 信任等级1 53 | 54 | * 进入至少**5**个话题 55 | * 阅读至少**30**篇帖子 56 | * 总共花费**10**分钟阅读帖子 57 | 58 | ### 信任等级2 59 | 60 | * 至少访问**15**天,不必连续 61 | * 至少点赞**1**次 62 | * 至少收到**1**次点赞 63 | * 回复至少**3**个不同的话题 64 | * 进入至少**20**个话题 65 | * 阅读至少**100**篇帖子 66 | * 总共花费**60**分钟阅读帖子 67 | 68 | ### 信任等级3 69 | 70 | * 必须至少访问了 **50%** 的天数 71 | * 必须至少在**10**个不同的非私信话题上进行了回复 72 | * 在过去100天内创建的话题中,必须浏览了**25%**(上限为500) 73 | * 在过去100天内创建的帖子中,必须阅读了**25%**(上限为20k) 74 | * 必须收到**20**个点赞,并送出**30**个点赞。\* 75 | * 不得收到超过5个垃圾邮件或不当内容标记(每个都需不同的帖子和不同的用户,并由版主确认) 76 | * 过去6个月内不能被暂停或禁言 77 | 78 | **这些点赞必须来自不同的用户数量最少为用户数的1/5,不同的天数最少为天数的1/4。这些点赞不可以来自私信。** 79 | 80 | 上述所有标准都必须满足才能达到信任等级3。此外,与其他信任等级不同,*你可能随时失去信任等级3的状态*。如果在过去100天内你的表现低于这些要求,你将会降级回成员。为了避免频繁的升降级情况,获得信任等级3后,有2周的宽限期,在此期间你不会被降级。 81 | 82 | ### 信任等级4 83 | 84 | * Neo 是论坛的创始人,他是唯一的领导者(即:信任等级4)。 85 | * 您无法被提升到信任等级4。 86 | 87 | 88 | 89 | - 90 | 91 | ## 一图了解 Linux Do 等级要求 92 | 93 | |提升条件|信任等级1|信任等级2|信任等级3|信任等级4| 94 | |---|:---:|:---:|:---:|:---:| 95 | |进入至少5个话题|✔️|||| 96 | |阅读至少30篇帖子|✔️|||| 97 | |总共花费10分钟阅读帖子|✔️|||| 98 | |至少访问15天(不必连续)||✔️||| 99 | |至少点赞1次||✔️||| 100 | |至少收到1次点赞||✔️||| 101 | |回复至少3个不同的话题||✔️||| 102 | |进入至少20个话题||✔️||| 103 | |阅读至少100篇帖子||✔️||| 104 | |总共花费60分钟阅读帖子||✔️||| 105 | |必须至少访问了50%的天数(过去100天内)|||✔️|| 106 | |必须至少在10个不同的非私信话题上进行了回复|||✔️|| 107 | |在过去100天内创建的话题中,必须浏览了25%(上限为500)|||✔️|| 108 | |在过去100天内创建的帖子中,必须阅读了25%(上限为20k)|||✔️|| 109 | |必须收到20个点赞,并送出30个点赞|||✔️|| 110 | |不得收到超过5个不当内容标记|||✔️|| 111 | |过去6个月内不能被暂停或禁言|||✔️|| 112 | |只能由工作人员手动晋升||||✔️| 113 | 114 | ## 查询升级状态 115 | 116 | - 通过 查看自己的勋章 了解当前信任等级。 117 | 118 | - 通过 Linux Do Connect 查询详细信息。 119 | 120 | 121 | ### 积分获取明细 122 | 123 | |行为|获得积分| 124 | |------|:------:| 125 | |收到他人点赞|1| 126 | |给出点评|1| 127 | |帖子被标记为解决方案|10| 128 | |邀请被接受|10| 129 | |每小时阅读时长|1| 130 | |每回复100个帖子|5| 131 | |创建新话题|10| 132 | |创建子话题|2| 133 | |举报被管理员采纳|10| 134 | |每日访问|1| 135 | -------------------------------------------------------------------------------- /content/_meta.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | index: '首页', 3 | tools: '百宝箱', 4 | honor: '共建荣誉墙', 5 | 'Linux Do': { 6 | type: 'separator', 7 | title: '社区概览', 8 | }, 9 | LinuxDo: 'Linux Do 社区介绍', 10 | 'Services': { 11 | type: 'separator', 12 | title: '服务导航', 13 | }, 14 | Community: "社区资源", 15 | AI: "镜像服务", 16 | HttpRW: "HTTP.RW", 17 | 'Other': { 18 | type: 'separator', 19 | title: '其他信息', 20 | }, 21 | Encyclopedia: '社区百科', 22 | } -------------------------------------------------------------------------------- /content/honor.mdx: -------------------------------------------------------------------------------- 1 |

2 | 3 | import Gold from '@/components/common/card/Gold'; 4 | import Niello from '@/components/common/card/Niello'; 5 | import User from '../components/common/card/User'; 6 | import CardGrid from '../components/common/CardGrid'; 7 | import Link from 'next/link'; 8 | 9 | # 荣誉榜 10 | 11 | --- 12 | 13 | **感谢 @Neo 的支持 和 以下各位热佬的付出,让我们一起共建你我引以为荣之社区!** 14 | 15 |

16 | 17 | 23 | 29 | 35 | 41 | 47 | 48 | 49 | 50 | 56 | 62 | 68 | 74 | 80 | 86 | 92 | 93 | 94 |

95 | 96 | ## Wiki 共建相关说明 97 | 98 | 为营造并鼓励共建环境,秉着**真诚、友善、团结、专业,共建你我引以为荣之社区的精神**。现对所有提交的 Issue/PR 的社区用户,在成功采纳后按照相应规则向各位积极参与的佬友表示感谢! 99 | 100 | - 反馈参与地址:Linux Do Wiki 官方 GitHub,若被遗漏,请及时私信。 101 | 102 | ### 反馈格式和说明 103 | 104 | 为了方便更好的管理和维护 Wiki 站,希望大家在提交 Issue/PR 时遵循一定的格式。 105 | 106 | - PR 时请遵循项目框架 Nextjs、Nextra 的编写规范 107 | - 除紧急情况外,所有 Merge 会在每周定时更新 108 | - Issue/PR 提交时的格式和说明: 109 | 110 | |
标题格式
|
Issue/PR 内容
| 111 | |------|:------:| 112 | | 【建议】xxx页面的xxx内容 | 想要建议的内容 + 理由 + 社区个人主页链接 | 113 | | 【错误】xxx页面的xxxBUG | 报告xxx页面内的BUG + 具体情况 + 社区个人主页链接 | 114 | | 【修复】xxx页面的xxxBUG | xxx页面内的 xxx BUG + 社区个人主页链接 | 115 | | 【更正】xxx页面的xxx内容 | xxx页面内需要更新的内容 + 社区个人主页链接 | 116 | | 【新增】xxx页面的xxx内容 | xxx页面内需要新增的内容 + 理由 + 社区个人主页链接 | 117 | | 【改进】xxx页面的xxx内容 | xxx页面内需要改进的内容 + 理由 + 社区个人主页链接 | 118 | 119 | ### 卡片预览 120 | 121 | - 社区内 C 语言学的不亦乐乎,因此我就做了以信用卡为模板的的小卡片,以此卡片为主题发放奖励吧! 122 | - 声明:C 是什么我真的一点都不懂啊! 123 | - 此卡仅作纪念,不具备任何实际价值,无任何直接联系,无任何特别暗示,仅供娱乐! 124 | 125 | 126 | 132 | 139 | -------------------------------------------------------------------------------- /content/index.mdx: -------------------------------------------------------------------------------- 1 |

2 | 3 | import Link from 'next/link' 4 | import Image from 'next/image' 5 | import { ThemeWrapper } from '@/components/common/ThemeWrapper' 6 | import { UserHoverCard } from '@/components/common/UserHoverCard' 7 | 8 | # 欢迎来到 LINUX DO WIKI 站 9 | 10 | --- 11 | 12 | **本 Wiki 旨在为 LINUX DO 社区用户整合社区相关服务说明和工具使用指南,以帮助您更好地了解 LINUX DO 及我们的社区文化!Wiki 由 搭建并结合自身使用与社区帖子整合编写,在此非常感谢您的访问!** 13 | 14 | 15 | Wiki 站并不是万能站,不可能完全收录所有信息,它仅作为社区辅助工具!由于时间精力有限,难免会出现错误信息,如有任何问题、建议、改进、想法、指正,欢迎前往 官方 Wiki GitHub 中反馈,感谢! 16 | 17 | 18 | - 致敬 **** ! 19 | - 致敬 所有 热佬 ! 20 | - 致敬 所有 LINUX DO 社区用户 ! 21 | 22 | --- 23 | 24 | } 26 | childrenInDarkTheme={LinuxDo_Light_Logo} 27 | /> 28 | 29 | > **真诚、友善、团结、专业**,共建你我引以为荣之社区! 30 | -------------------------------------------------------------------------------- /content/tools.mdx: -------------------------------------------------------------------------------- 1 |

2 | import { UserHoverCard } from '@/components/common/UserHoverCard' 3 | import User from '@/components/common/card/User' 4 | import MemoryCard from '@/components/pandora/MemoryCard' 5 | import { Cards } from 'nextra/components' 6 | import { Gavel } from '@/components/animate-ui/icons/gavel' 7 | import Image from 'next/image' 8 | 9 | 10 | # Pandora 百宝箱 11 | > 探索佬友们的一些小玩意儿 ~ 12 | 13 | ## Linux Do 社区卡片 14 | > Created By 15 | 16 |
17 | 18 |
19 | 20 | ## 2048 21 | > Created By 22 | 23 |

24 | 25 | } 27 | title='2048.linux.do' 28 | href='https://2048.linux.do' 29 | target='_blank' 30 | arrow 31 | /> 32 | 33 |

34 | 35 | 2048 Game 36 | 37 | ## 记忆翻牌 38 | > Created By 39 | 40 |
41 | 42 |
43 | 44 | 45 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { dirname } from "path"; 2 | import { fileURLToPath } from "url"; 3 | import { FlatCompat } from "@eslint/eslintrc"; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = dirname(__filename); 7 | 8 | const compat = new FlatCompat({ 9 | baseDirectory: __dirname, 10 | }); 11 | 12 | const eslintConfig = [ 13 | ...compat.extends("next/core-web-vitals", "next/typescript"), 14 | ]; 15 | 16 | export default eslintConfig; 17 | -------------------------------------------------------------------------------- /hooks/useDataCache.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 数据缓存 React Hook 3 | * 提供优化的用户和话题数据获取功能 4 | */ 5 | 6 | import { useState, useEffect, useCallback, useRef } from 'react'; 7 | import { dataCache, UserData, TopicData } from '@/lib/data-cache'; 8 | 9 | /** 10 | * 使用用户数据的 Hook 11 | */ 12 | export function useUserData(username: string) { 13 | const [data, setData] = useState(null); 14 | const [loading, setLoading] = useState(true); 15 | const [error, setError] = useState(false); 16 | const callbackRef = useRef<(() => void) | undefined>(undefined); 17 | 18 | // 更新状态的回调函数 19 | const updateState = useCallback(() => { 20 | const result = dataCache.getUserData(username); 21 | setData(result.data); 22 | setLoading(result.loading); 23 | setError(result.error); 24 | }, [username]); 25 | 26 | useEffect(() => { 27 | if (!username) { 28 | setData(null); 29 | setLoading(false); 30 | setError(true); 31 | return; 32 | } 33 | 34 | // 保存回调引用以便清理 35 | callbackRef.current = updateState; 36 | 37 | // 获取数据并设置更新回调 38 | const result = dataCache.getUserData(username, updateState); 39 | setData(result.data); 40 | setLoading(result.loading); 41 | setError(result.error); 42 | 43 | // 清理函数 44 | return () => { 45 | if (callbackRef.current) { 46 | dataCache.unsubscribeUser(username, callbackRef.current); 47 | } 48 | }; 49 | }, [username, updateState]); 50 | 51 | return { data, loading, error }; 52 | } 53 | 54 | /** 55 | * 使用话题数据的 Hook 56 | */ 57 | export function useTopicData(topicId: string) { 58 | const [data, setData] = useState(null); 59 | const [loading, setLoading] = useState(true); 60 | const [error, setError] = useState(false); 61 | const callbackRef = useRef<(() => void) | undefined>(undefined); 62 | 63 | // 更新状态的回调函数 64 | const updateState = useCallback(() => { 65 | const result = dataCache.getTopicData(topicId); 66 | setData(result.data); 67 | setLoading(result.loading); 68 | setError(result.error); 69 | }, [topicId]); 70 | 71 | useEffect(() => { 72 | if (!topicId) { 73 | setData(null); 74 | setLoading(false); 75 | setError(true); 76 | return; 77 | } 78 | 79 | // 保存回调引用以便清理 80 | callbackRef.current = updateState; 81 | 82 | // 获取数据并设置更新回调 83 | const result = dataCache.getTopicData(topicId, updateState); 84 | setData(result.data); 85 | setLoading(result.loading); 86 | setError(result.error); 87 | 88 | // 清理函数 89 | return () => { 90 | if (callbackRef.current) { 91 | dataCache.unsubscribeTopic(topicId, callbackRef.current); 92 | } 93 | }; 94 | }, [topicId, updateState]); 95 | 96 | return { data, loading, error }; 97 | } 98 | 99 | /** 100 | * 使用多个用户数据的 Hook 101 | */ 102 | export function useMultipleUsers(usernames: string[]) { 103 | const [users, setUsers] = useState>({}); 104 | const callbacksRef = useRef void>>({}); 105 | 106 | // 为依赖数组创建一个稳定的字符串键 107 | const usernamesKey = usernames.join(','); 108 | 109 | // 更新单个用户状态 110 | const updateUserState = useCallback((username: string) => { 111 | const result = dataCache.getUserData(username); 112 | setUsers(prev => ({ 113 | ...prev, 114 | [username]: { 115 | data: result.data, 116 | loading: result.loading, 117 | error: result.error, 118 | }, 119 | })); 120 | }, []); 121 | 122 | useEffect(() => { 123 | // 清理之前的订阅 124 | Object.entries(callbacksRef.current).forEach(([username, callback]) => { 125 | dataCache.unsubscribeUser(username, callback); 126 | }); 127 | callbacksRef.current = {}; 128 | 129 | // 为每个用户名设置数据获取和订阅 130 | const newUsers: Record = {}; 131 | 132 | usernames.forEach(username => { 133 | if (username) { 134 | const callback = () => updateUserState(username); 135 | callbacksRef.current[username] = callback; 136 | 137 | const result = dataCache.getUserData(username, callback); 138 | newUsers[username] = { 139 | data: result.data, 140 | loading: result.loading, 141 | error: result.error, 142 | }; 143 | } 144 | }); 145 | 146 | setUsers(newUsers); 147 | 148 | // 预加载所有用户数据 149 | dataCache.preloadUsers(usernames.filter(Boolean)); 150 | 151 | // 清理函数 152 | return () => { 153 | Object.entries(callbacksRef.current).forEach(([username, callback]) => { 154 | dataCache.unsubscribeUser(username, callback); 155 | }); 156 | }; 157 | }, [usernamesKey, updateUserState, usernames]); 158 | 159 | return users; 160 | } 161 | 162 | /** 163 | * 使用多个话题数据的 Hook 164 | */ 165 | export function useMultipleTopics(topicIds: string[]) { 166 | const [topics, setTopics] = useState>({}); 167 | const callbacksRef = useRef void>>({}); 168 | 169 | // 为依赖数组创建一个稳定的字符串键 170 | const topicIdsKey = topicIds.join(','); 171 | 172 | // 更新单个话题状态 173 | const updateTopicState = useCallback((topicId: string) => { 174 | const result = dataCache.getTopicData(topicId); 175 | setTopics(prev => ({ 176 | ...prev, 177 | [topicId]: { 178 | data: result.data, 179 | loading: result.loading, 180 | error: result.error, 181 | }, 182 | })); 183 | }, []); 184 | 185 | useEffect(() => { 186 | // 清理之前的订阅 187 | Object.entries(callbacksRef.current).forEach(([topicId, callback]) => { 188 | dataCache.unsubscribeTopic(topicId, callback); 189 | }); 190 | callbacksRef.current = {}; 191 | 192 | // 为每个话题ID设置数据获取和订阅 193 | const newTopics: Record = {}; 194 | 195 | topicIds.forEach(topicId => { 196 | if (topicId) { 197 | const callback = () => updateTopicState(topicId); 198 | callbacksRef.current[topicId] = callback; 199 | 200 | const result = dataCache.getTopicData(topicId, callback); 201 | newTopics[topicId] = { 202 | data: result.data, 203 | loading: result.loading, 204 | error: result.error, 205 | }; 206 | } 207 | }); 208 | 209 | setTopics(newTopics); 210 | 211 | // 预加载所有话题数据 212 | dataCache.preloadTopics(topicIds.filter(Boolean)); 213 | 214 | // 清理函数 215 | return () => { 216 | Object.entries(callbacksRef.current).forEach(([topicId, callback]) => { 217 | dataCache.unsubscribeTopic(topicId, callback); 218 | }); 219 | }; 220 | }, [topicIdsKey, updateTopicState, topicIds]); 221 | 222 | return topics; 223 | } 224 | 225 | /** 226 | * 预加载数据的 Hook 227 | */ 228 | export function usePreloadData() { 229 | const preloadUsers = useCallback((usernames: string[]) => { 230 | dataCache.preloadUsers(usernames.filter(Boolean)); 231 | }, []); 232 | 233 | const preloadTopics = useCallback((topicIds: string[]) => { 234 | dataCache.preloadTopics(topicIds.filter(Boolean)); 235 | }, []); 236 | 237 | return { preloadUsers, preloadTopics }; 238 | } -------------------------------------------------------------------------------- /lib/utils.ts: -------------------------------------------------------------------------------- 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 | } -------------------------------------------------------------------------------- /mdx-components.js: -------------------------------------------------------------------------------- 1 | import { useMDXComponents as getThemeComponents } from 'nextra-theme-docs' // nextra-theme-blog or your custom theme 2 | 3 | // Get the default MDX components 4 | const themeComponents = getThemeComponents() 5 | 6 | // Merge components 7 | export function useMDXComponents(components) { 8 | return { 9 | ...themeComponents, 10 | ...components 11 | } 12 | } -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | import nextra from 'nextra' 2 | 3 | // Set up Nextra with its configuration 4 | const withNextra = nextra({ 5 | contentDirBasePath: '/' 6 | }) 7 | 8 | // Export the final Next.js config with Nextra included 9 | export default withNextra({ 10 | allowedDevOrigins: [ 11 | 'test.chenyme.com' 12 | ], 13 | 14 | // 配置图片域名 15 | images: { 16 | remotePatterns: [ 17 | { 18 | protocol: 'https', 19 | hostname: 'linux.do', 20 | pathname: '/**', 21 | }, 22 | ], 23 | } 24 | }) -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Linux Do Wiki", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev --turbopack -p 3001", 7 | "build": "next build", 8 | "start": "next start -p 3001", 9 | "lint": "next lint", 10 | "postbuild": "pagefind --site .next/server/app --output-path public/_pagefind" 11 | }, 12 | "dependencies": { 13 | "@radix-ui/react-avatar": "^1.1.10", 14 | "@radix-ui/react-slot": "^1.2.3", 15 | "class-variance-authority": "^0.7.1", 16 | "clsx": "^2.1.1", 17 | "lucide-react": "^0.512.0", 18 | "motion": "^12.16.0", 19 | "next": "15.3.3", 20 | "next-theme": "^0.1.5", 21 | "next-themes": "^0.4.6", 22 | "nextra": "^4.2.17", 23 | "nextra-theme-docs": "^4.2.17", 24 | "radix-ui": "^1.4.2", 25 | "react": "^19.0.0", 26 | "react-dom": "^19.0.0", 27 | "react-use-measure": "^2.1.7", 28 | "shiki": "^3.4.2", 29 | "tailwind-merge": "^3.3.0" 30 | }, 31 | "devDependencies": { 32 | "@eslint/eslintrc": "^3.3.1", 33 | "@tailwindcss/postcss": "^4", 34 | "@types/node": "22.15.29", 35 | "@types/react": "19.1.6", 36 | "@types/react-dom": "^19.1.6", 37 | "eslint": "^9.28.0", 38 | "eslint-config-next": "^15.3.3", 39 | "pagefind": "^1.3.0", 40 | "tailwindcss": "^4", 41 | "tw-animate-css": "^1.3.3" 42 | }, 43 | "packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39" 44 | } 45 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: ["@tailwindcss/postcss"], 3 | }; 4 | 5 | export default config; 6 | -------------------------------------------------------------------------------- /public/2048.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenyme/LinuxDoWiki/b0f28127c613852d088ddb805ac0e2f074d6eee0/public/2048.png -------------------------------------------------------------------------------- /public/Farrington-7B.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenyme/LinuxDoWiki/b0f28127c613852d088ddb805ac0e2f074d6eee0/public/Farrington-7B.ttf -------------------------------------------------------------------------------- /public/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenyme/LinuxDoWiki/b0f28127c613852d088ddb805ac0e2f074d6eee0/public/demo.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenyme/LinuxDoWiki/b0f28127c613852d088ddb805ac0e2f074d6eee0/public/favicon.ico -------------------------------------------------------------------------------- /public/fuclaude.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenyme/LinuxDoWiki/b0f28127c613852d088ddb805ac0e2f074d6eee0/public/fuclaude.png -------------------------------------------------------------------------------- /public/linuxdo_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenyme/LinuxDoWiki/b0f28127c613852d088ddb805ac0e2f074d6eee0/public/linuxdo_dark.png -------------------------------------------------------------------------------- /public/linuxdo_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenyme/LinuxDoWiki/b0f28127c613852d088ddb805ac0e2f074d6eee0/public/linuxdo_light.png -------------------------------------------------------------------------------- /public/linuxdoconnect_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenyme/LinuxDoWiki/b0f28127c613852d088ddb805ac0e2f074d6eee0/public/linuxdoconnect_1.png -------------------------------------------------------------------------------- /public/linuxdoconnect_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenyme/LinuxDoWiki/b0f28127c613852d088ddb805ac0e2f074d6eee0/public/linuxdoconnect_2.png -------------------------------------------------------------------------------- /public/linuxdoconnect_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenyme/LinuxDoWiki/b0f28127c613852d088ddb805ac0e2f074d6eee0/public/linuxdoconnect_3.png -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenyme/LinuxDoWiki/b0f28127c613852d088ddb805ac0e2f074d6eee0/public/logo.png -------------------------------------------------------------------------------- /public/neo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenyme/LinuxDoWiki/b0f28127c613852d088ddb805ac0e2f074d6eee0/public/neo.jpg -------------------------------------------------------------------------------- /public/oaifree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenyme/LinuxDoWiki/b0f28127c613852d088ddb805ac0e2f074d6eee0/public/oaifree.png -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | './app/**/*.{js,ts,jsx,tsx}', 5 | './pages/**/*.{js,ts,jsx,tsx}', 6 | './components/**/*.{js,ts,jsx,tsx}', 7 | ], 8 | theme: { 9 | extend: { 10 | transitionDuration: { 11 | '800': '800ms', 12 | } 13 | }, 14 | }, 15 | plugins: [], 16 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "next.config.mjs"], 26 | "exclude": ["node_modules"] 27 | } 28 | --------------------------------------------------------------------------------