mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-02 10:31:04 +08:00
Feature/faq (#546)
* feat(web): add FAQ page with search, sidebar, and i18n integration; update navigation and routes; include user feedback analysis docs (faq.md) * docs: add filled frontend PR template for FAQ feature (PR_FRONTEND_FAQ.md) * docs(web): add Contributing & Tasks FAQ category near top with guidance on using GitHub Projects and PR contribution standards * feat(web,api): dynamically embed GitHub Projects roadmap in FAQ via /api/roadmap and RoadmapWidget; add env vars for GitHub token/org/project * chore(docker): pass GitHub roadmap env vars into backend container * docs(web): update FAQ with fork-based PR workflow, yellow links to roadmap/task dashboard, and contribution incentives; remove dynamic roadmap embed\n\nchore(api,docker): remove /api/roadmap endpoint and related env wiring * chore: revert unintended changes (.env.example, api/server.go, docker-compose.yml); remove local-only files (PR_FRONTEND_FAQ.md, web/faq.md) from PR * feat: 添加对重置密码页面的路由支持
This commit is contained in:
@@ -8,6 +8,7 @@ import { RegisterPage } from './components/RegisterPage'
|
||||
import { ResetPasswordPage } from './components/ResetPasswordPage'
|
||||
import { CompetitionPage } from './components/CompetitionPage'
|
||||
import { LandingPage } from './pages/LandingPage'
|
||||
import { FAQPage } from './pages/FAQPage'
|
||||
import HeaderBar from './components/landing/HeaderBar'
|
||||
import AILearning from './components/AILearning'
|
||||
import { LanguageProvider, useLanguage } from './contexts/LanguageContext'
|
||||
@@ -230,11 +231,14 @@ function App() {
|
||||
}
|
||||
if (route === '/register') {
|
||||
if (systemConfig?.admin_mode) {
|
||||
window.history.pushState({}, '', '/login');
|
||||
return <LoginPage />;
|
||||
}
|
||||
window.history.pushState({}, '', '/login')
|
||||
return <LoginPage />
|
||||
}
|
||||
return <RegisterPage />
|
||||
}
|
||||
if (route === '/faq') {
|
||||
return <FAQPage />
|
||||
}
|
||||
if (route === '/reset-password') {
|
||||
return <ResetPasswordPage />
|
||||
}
|
||||
@@ -271,6 +275,10 @@ function App() {
|
||||
window.history.pushState({}, '', '/dashboard')
|
||||
setRoute('/dashboard')
|
||||
setCurrentPage('trader')
|
||||
} else if (page === 'faq') {
|
||||
console.log('Navigating to faq')
|
||||
window.history.pushState({}, '', '/faq')
|
||||
setRoute('/faq')
|
||||
}
|
||||
|
||||
console.log(
|
||||
@@ -290,12 +298,12 @@ function App() {
|
||||
|
||||
// Show landing page for root route
|
||||
if (route === '/' || route === '') {
|
||||
return <LandingPage isAdminMode={systemConfig?.admin_mode} />;
|
||||
return <LandingPage isAdminMode={systemConfig?.admin_mode} />
|
||||
}
|
||||
|
||||
// In admin mode, require authentication for any protected routes
|
||||
if (systemConfig?.admin_mode && (!user || !token)) {
|
||||
return <LoginPage />;
|
||||
return <LoginPage />
|
||||
}
|
||||
|
||||
// Show main app for authenticated users on other routes (non-admin mode)
|
||||
@@ -332,6 +340,9 @@ function App() {
|
||||
window.history.pushState({}, '', '/dashboard')
|
||||
setRoute('/dashboard')
|
||||
setCurrentPage('trader')
|
||||
} else if (page === 'faq') {
|
||||
window.history.pushState({}, '', '/faq')
|
||||
setRoute('/faq')
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
459
web/src/components/faq/FAQContent.tsx
Normal file
459
web/src/components/faq/FAQContent.tsx
Normal file
@@ -0,0 +1,459 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { t, type Language } from '../../i18n/translations'
|
||||
import type { FAQCategory } from '../../data/faqData'
|
||||
// RoadmapWidget 移除动态嵌入,按需仅展示外部链接
|
||||
|
||||
interface FAQContentProps {
|
||||
categories: FAQCategory[]
|
||||
language: Language
|
||||
onActiveItemChange: (itemId: string) => void
|
||||
}
|
||||
|
||||
export function FAQContent({
|
||||
categories,
|
||||
language,
|
||||
onActiveItemChange,
|
||||
}: FAQContentProps) {
|
||||
const sectionRefs = useRef<Map<string, HTMLElement>>(new Map())
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
const itemId = entry.target.getAttribute('data-item-id')
|
||||
if (itemId) {
|
||||
onActiveItemChange(itemId)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
{
|
||||
rootMargin: '-100px 0px -80% 0px',
|
||||
threshold: 0,
|
||||
}
|
||||
)
|
||||
|
||||
sectionRefs.current.forEach((ref) => {
|
||||
if (ref) observer.observe(ref)
|
||||
})
|
||||
|
||||
return () => {
|
||||
sectionRefs.current.forEach((ref) => {
|
||||
if (ref) observer.unobserve(ref)
|
||||
})
|
||||
}
|
||||
}, [onActiveItemChange])
|
||||
|
||||
const setRef = (itemId: string, element: HTMLElement | null) => {
|
||||
if (element) {
|
||||
sectionRefs.current.set(itemId, element)
|
||||
} else {
|
||||
sectionRefs.current.delete(itemId)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-12">
|
||||
{categories.map((category) => (
|
||||
<div key={category.id}>
|
||||
{/* Category Header */}
|
||||
<div
|
||||
className="flex items-center gap-3 mb-6 pb-3"
|
||||
style={{ borderBottom: '2px solid #2B3139' }}
|
||||
>
|
||||
<category.icon className="w-7 h-7" style={{ color: '#F0B90B' }} />
|
||||
<h2 className="text-2xl font-bold" style={{ color: '#EAECEF' }}>
|
||||
{t(category.titleKey, language)}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{/* FAQ Items */}
|
||||
<div className="space-y-8">
|
||||
{category.items.map((item) => (
|
||||
<section
|
||||
key={item.id}
|
||||
id={item.id}
|
||||
data-item-id={item.id}
|
||||
ref={(el) => setRef(item.id, el)}
|
||||
className="scroll-mt-24"
|
||||
>
|
||||
{/* Question */}
|
||||
<h3
|
||||
className="text-xl font-semibold mb-3"
|
||||
style={{ color: '#EAECEF' }}
|
||||
>
|
||||
{t(item.questionKey, language)}
|
||||
</h3>
|
||||
|
||||
{/* Answer */}
|
||||
<div
|
||||
className="prose prose-invert max-w-none"
|
||||
style={{
|
||||
color: '#B7BDC6',
|
||||
lineHeight: '1.7',
|
||||
}}
|
||||
>
|
||||
{item.id === 'github-projects-tasks' ? (
|
||||
<div className="space-y-3">
|
||||
<div className="text-base">
|
||||
{language === 'zh' ? '链接:' : 'Links:'}{' '}
|
||||
<a
|
||||
href="https://github.com/orgs/NoFxAiOS/projects/3"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
style={{ color: '#F0B90B' }}
|
||||
>
|
||||
{language === 'zh' ? '路线图' : 'Roadmap'}
|
||||
</a>
|
||||
{' | '}
|
||||
<a
|
||||
href="https://github.com/orgs/NoFxAiOS/projects/5"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
style={{ color: '#F0B90B' }}
|
||||
>
|
||||
{language === 'zh' ? '任务看板' : 'Task Dashboard'}
|
||||
</a>
|
||||
</div>
|
||||
<ol className="list-decimal pl-5 space-y-1 text-base">
|
||||
{language === 'zh' ? (
|
||||
<>
|
||||
<li>
|
||||
打开以上链接,按标签筛选(good first issue / help
|
||||
wanted / frontend / backend)。
|
||||
</li>
|
||||
<li>
|
||||
打开任务,阅读描述与验收标准(Acceptance
|
||||
Criteria)。
|
||||
</li>
|
||||
<li>评论“assign me”或自助分配(若权限允许)。</li>
|
||||
<li>Fork 仓库到你的 GitHub 账户。</li>
|
||||
<li>
|
||||
同步你的 fork 的 <code>dev</code>{' '}
|
||||
分支与上游保持一致:
|
||||
<code className="ml-2">
|
||||
git remote add upstream
|
||||
https://github.com/NoFxAiOS/nofx.git
|
||||
</code>
|
||||
<br />
|
||||
<code>git fetch upstream</code>
|
||||
<br />
|
||||
<code>git checkout dev</code>
|
||||
<br />
|
||||
<code>git rebase upstream/dev</code>
|
||||
<br />
|
||||
<code>git push origin dev</code>
|
||||
</li>
|
||||
<li>
|
||||
从你的 fork 的 <code>dev</code> 建立特性分支:
|
||||
<code className="ml-2">
|
||||
git checkout -b feat/your-topic
|
||||
</code>
|
||||
</li>
|
||||
<li>
|
||||
推送到你的 fork:
|
||||
<code className="ml-2">
|
||||
git push origin feat/your-topic
|
||||
</code>
|
||||
</li>
|
||||
<li>
|
||||
打开 PR:base 选择 <code>NoFxAiOS/nofx:dev</code>{' '}
|
||||
← compare 选择{' '}
|
||||
<code>你的用户名/nofx:feat/your-topic</code>。
|
||||
</li>
|
||||
<li>
|
||||
在 PR 中关联 Issue(示例:
|
||||
<code className="ml-1">Closes #123</code>
|
||||
),选择正确 PR 模板;必要时与{' '}
|
||||
<code>upstream/dev</code>{' '}
|
||||
同步(rebase)后继续推送。
|
||||
</li>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<li>
|
||||
Open the links above and filter by labels (good
|
||||
first issue / help wanted / frontend / backend).
|
||||
</li>
|
||||
<li>
|
||||
Open the task and read the Description &
|
||||
Acceptance Criteria.
|
||||
</li>
|
||||
<li>
|
||||
Comment "assign me" or self-assign (if permitted).
|
||||
</li>
|
||||
<li>Fork the repository to your GitHub account.</li>
|
||||
<li>
|
||||
Sync your fork's <code>dev</code> with upstream:
|
||||
<code className="ml-2">
|
||||
git remote add upstream
|
||||
https://github.com/NoFxAiOS/nofx.git
|
||||
</code>
|
||||
<br />
|
||||
<code>git fetch upstream</code>
|
||||
<br />
|
||||
<code>git checkout dev</code>
|
||||
<br />
|
||||
<code>git rebase upstream/dev</code>
|
||||
<br />
|
||||
<code>git push origin dev</code>
|
||||
</li>
|
||||
<li>
|
||||
Create a feature branch from your fork's{' '}
|
||||
<code>dev</code>:
|
||||
<code className="ml-2">
|
||||
git checkout -b feat/your-topic
|
||||
</code>
|
||||
</li>
|
||||
<li>
|
||||
Push to your fork:
|
||||
<code className="ml-2">
|
||||
git push origin feat/your-topic
|
||||
</code>
|
||||
</li>
|
||||
<li>
|
||||
Open a PR: base <code>NoFxAiOS/nofx:dev</code> ←
|
||||
compare{' '}
|
||||
<code>your-username/nofx:feat/your-topic</code>.
|
||||
</li>
|
||||
<li>
|
||||
In PR, reference the Issue (e.g.,{' '}
|
||||
<code className="ml-1">Closes #123</code>) and
|
||||
choose the proper PR template; rebase onto{' '}
|
||||
<code>upstream/dev</code> as needed.
|
||||
</li>
|
||||
</>
|
||||
)}
|
||||
</ol>
|
||||
|
||||
<div
|
||||
className="rounded p-3 mt-3"
|
||||
style={{
|
||||
background: 'rgba(240, 185, 11, 0.08)',
|
||||
border: '1px solid rgba(240, 185, 11, 0.25)',
|
||||
}}
|
||||
>
|
||||
{language === 'zh' ? (
|
||||
<div className="text-sm">
|
||||
<strong style={{ color: '#F0B90B' }}>提示:</strong>{' '}
|
||||
参与贡献将享有激励制度(如
|
||||
Bounty/奖金、荣誉徽章与鸣谢、优先
|
||||
Review/合并与内测资格 等)。 可在任务中优先选择带
|
||||
<a
|
||||
href="https://github.com/NoFxAiOS/nofx/labels/bounty"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
style={{ color: '#F0B90B' }}
|
||||
>
|
||||
bounty 标签
|
||||
</a>
|
||||
的事项,或完成后提交
|
||||
<a
|
||||
href="https://github.com/NoFxAiOS/nofx/blob/dev/.github/ISSUE_TEMPLATE/bounty_claim.md"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
style={{ color: '#F0B90B' }}
|
||||
>
|
||||
Bounty Claim
|
||||
</a>
|
||||
申请。
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-sm">
|
||||
<strong style={{ color: '#F0B90B' }}>Note:</strong>{' '}
|
||||
Contribution incentives are available (e.g., cash
|
||||
bounties, badges & shout-outs, priority
|
||||
review/merge, beta access). Prefer tasks with
|
||||
<a
|
||||
href="https://github.com/NoFxAiOS/nofx/labels/bounty"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
style={{ color: '#F0B90B' }}
|
||||
>
|
||||
bounty label
|
||||
</a>
|
||||
, or file a
|
||||
<a
|
||||
href="https://github.com/NoFxAiOS/nofx/blob/dev/.github/ISSUE_TEMPLATE/bounty_claim.md"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
style={{ color: '#F0B90B' }}
|
||||
>
|
||||
Bounty Claim
|
||||
</a>
|
||||
after completion.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : item.id === 'contribute-pr-guidelines' ? (
|
||||
<div className="space-y-3">
|
||||
<div className="text-base">
|
||||
{language === 'zh' ? '参考文档:' : 'References:'}{' '}
|
||||
<a
|
||||
href="https://github.com/NoFxAiOS/nofx/blob/dev/CONTRIBUTING.md"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
style={{ color: '#F0B90B' }}
|
||||
>
|
||||
CONTRIBUTING.md
|
||||
</a>
|
||||
{' | '}
|
||||
<a
|
||||
href="https://github.com/NoFxAiOS/nofx/blob/dev/.github/PR_TITLE_GUIDE.md"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
style={{ color: '#F0B90B' }}
|
||||
>
|
||||
PR_TITLE_GUIDE.md
|
||||
</a>
|
||||
</div>
|
||||
<ol className="list-decimal pl-5 space-y-1 text-base">
|
||||
{language === 'zh' ? (
|
||||
<>
|
||||
<li>
|
||||
Fork 仓库后,从你的 fork 的 <code>dev</code>{' '}
|
||||
分支创建特性分支;避免直接向上游 <code>main</code>{' '}
|
||||
提交。
|
||||
</li>
|
||||
<li>
|
||||
分支命名:feat/…、fix/…、docs/…;提交信息遵循
|
||||
Conventional Commits。
|
||||
</li>
|
||||
<li>
|
||||
提交前运行检查:
|
||||
<code className="ml-2">
|
||||
npm --prefix web run lint && npm --prefix web
|
||||
run build
|
||||
</code>
|
||||
</li>
|
||||
<li>涉及 UI 变更请附截图或短视频。</li>
|
||||
<li>
|
||||
选择正确的 PR
|
||||
模板(frontend/backend/docs/general)。
|
||||
</li>
|
||||
<li>
|
||||
在 PR 中关联 Issue(示例:
|
||||
<code className="ml-1">Closes #123</code>),PR
|
||||
目标选择 <code>NoFxAiOS/nofx:dev</code>。
|
||||
</li>
|
||||
<li>
|
||||
保持与 <code>upstream/dev</code>{' '}
|
||||
同步(rebase),确保 CI 通过;尽量保持 PR
|
||||
小而聚焦。
|
||||
</li>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<li>
|
||||
After forking, branch from your fork's{' '}
|
||||
<code>dev</code>; avoid direct commits to upstream{' '}
|
||||
<code>main</code>.
|
||||
</li>
|
||||
<li>
|
||||
Branch naming: feat/…, fix/…, docs/…; commit
|
||||
messages follow Conventional Commits.
|
||||
</li>
|
||||
<li>
|
||||
Run checks before PR:
|
||||
<code className="ml-2">
|
||||
npm --prefix web run lint && npm --prefix web
|
||||
run build
|
||||
</code>
|
||||
</li>
|
||||
<li>
|
||||
For UI changes, attach screenshots or a short
|
||||
video.
|
||||
</li>
|
||||
<li>
|
||||
Choose the proper PR template
|
||||
(frontend/backend/docs/general).
|
||||
</li>
|
||||
<li>
|
||||
Link the Issue in PR (e.g.,{' '}
|
||||
<code className="ml-1">Closes #123</code>) and
|
||||
target <code>NoFxAiOS/nofx:dev</code>.
|
||||
</li>
|
||||
<li>
|
||||
Keep rebasing onto <code>upstream/dev</code>,
|
||||
ensure CI passes; prefer small and focused PRs.
|
||||
</li>
|
||||
</>
|
||||
)}
|
||||
</ol>
|
||||
|
||||
<div
|
||||
className="rounded p-3 mt-3"
|
||||
style={{
|
||||
background: 'rgba(240, 185, 11, 0.08)',
|
||||
border: '1px solid rgba(240, 185, 11, 0.25)',
|
||||
}}
|
||||
>
|
||||
{language === 'zh' ? (
|
||||
<div className="text-sm">
|
||||
<strong style={{ color: '#F0B90B' }}>提示:</strong>{' '}
|
||||
我们为高质量贡献提供激励(Bounty/奖金、荣誉徽章与鸣谢、优先
|
||||
Review/合并与内测资格 等)。 详情可关注带
|
||||
<a
|
||||
href="https://github.com/NoFxAiOS/nofx/labels/bounty"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
style={{ color: '#F0B90B' }}
|
||||
>
|
||||
bounty 标签
|
||||
</a>
|
||||
的任务,或使用
|
||||
<a
|
||||
href="https://github.com/NoFxAiOS/nofx/blob/dev/.github/ISSUE_TEMPLATE/bounty_claim.md"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
style={{ color: '#F0B90B' }}
|
||||
>
|
||||
Bounty Claim 模板
|
||||
</a>
|
||||
提交申请。
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-sm">
|
||||
<strong style={{ color: '#F0B90B' }}>Note:</strong>{' '}
|
||||
We offer contribution incentives (bounties, badges,
|
||||
shout-outs, priority review/merge, beta access).
|
||||
Look for tasks with
|
||||
<a
|
||||
href="https://github.com/NoFxAiOS/nofx/labels/bounty"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
style={{ color: '#F0B90B' }}
|
||||
>
|
||||
bounty label
|
||||
</a>
|
||||
, or submit a
|
||||
<a
|
||||
href="https://github.com/NoFxAiOS/nofx/blob/dev/.github/ISSUE_TEMPLATE/bounty_claim.md"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
style={{ color: '#F0B90B' }}
|
||||
>
|
||||
Bounty Claim
|
||||
</a>
|
||||
when ready.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-base">{t(item.answerKey, language)}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Divider */}
|
||||
<div className="mt-6 h-px" style={{ background: '#2B3139' }} />
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
181
web/src/components/faq/FAQLayout.tsx
Normal file
181
web/src/components/faq/FAQLayout.tsx
Normal file
@@ -0,0 +1,181 @@
|
||||
import { useState, useMemo } from 'react'
|
||||
import { HelpCircle } from 'lucide-react'
|
||||
import { t, type Language } from '../../i18n/translations'
|
||||
import { FAQSearchBar } from './FAQSearchBar'
|
||||
import { FAQSidebar } from './FAQSidebar'
|
||||
import { FAQContent } from './FAQContent'
|
||||
import { faqCategories } from '../../data/faqData'
|
||||
import type { FAQCategory } from '../../data/faqData'
|
||||
|
||||
interface FAQLayoutProps {
|
||||
language: Language
|
||||
}
|
||||
|
||||
export function FAQLayout({ language }: FAQLayoutProps) {
|
||||
const [searchTerm, setSearchTerm] = useState('')
|
||||
const [activeItemId, setActiveItemId] = useState<string | null>(null)
|
||||
|
||||
// Filter categories based on search term
|
||||
const filteredCategories = useMemo(() => {
|
||||
if (!searchTerm.trim()) {
|
||||
return faqCategories
|
||||
}
|
||||
|
||||
const term = searchTerm.toLowerCase()
|
||||
const filtered: FAQCategory[] = []
|
||||
|
||||
faqCategories.forEach((category) => {
|
||||
const matchingItems = category.items.filter((item) => {
|
||||
const question = t(item.questionKey, language).toLowerCase()
|
||||
const answer = t(item.answerKey, language).toLowerCase()
|
||||
return question.includes(term) || answer.includes(term)
|
||||
})
|
||||
|
||||
if (matchingItems.length > 0) {
|
||||
filtered.push({
|
||||
...category,
|
||||
items: matchingItems,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return filtered
|
||||
}, [searchTerm, language])
|
||||
|
||||
const handleItemClick = (_categoryId: string, itemId: string) => {
|
||||
const element = document.getElementById(itemId)
|
||||
if (element) {
|
||||
const offset = 100
|
||||
const elementPosition = element.getBoundingClientRect().top
|
||||
const offsetPosition = elementPosition + window.pageYOffset - offset
|
||||
|
||||
window.scrollTo({
|
||||
top: offsetPosition,
|
||||
behavior: 'smooth',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6 pt-24">
|
||||
{/* Page Header */}
|
||||
<div className="text-center mb-12">
|
||||
<div className="flex items-center justify-center gap-3 mb-4">
|
||||
<div
|
||||
className="w-16 h-16 rounded-full flex items-center justify-center"
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #F0B90B 0%, #FCD535 100%)',
|
||||
boxShadow: '0 8px 24px rgba(240, 185, 11, 0.4)',
|
||||
}}
|
||||
>
|
||||
<HelpCircle className="w-8 h-8" style={{ color: '#0B0E11' }} />
|
||||
</div>
|
||||
</div>
|
||||
<h1 className="text-4xl font-bold mb-4" style={{ color: '#EAECEF' }}>
|
||||
{t('faqTitle', language)}
|
||||
</h1>
|
||||
<p className="text-lg mb-8" style={{ color: '#848E9C' }}>
|
||||
{t('faqSubtitle', language)}
|
||||
</p>
|
||||
|
||||
{/* Search Bar */}
|
||||
<div className="max-w-2xl mx-auto">
|
||||
<FAQSearchBar
|
||||
searchTerm={searchTerm}
|
||||
onSearchChange={setSearchTerm}
|
||||
placeholder={
|
||||
language === 'zh' ? '搜索常见问题...' : 'Search FAQ...'
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="flex gap-8">
|
||||
{/* Sidebar - Hidden on mobile, visible on desktop */}
|
||||
<aside className="hidden lg:block w-64 flex-shrink-0">
|
||||
<FAQSidebar
|
||||
categories={filteredCategories}
|
||||
activeItemId={activeItemId}
|
||||
language={language}
|
||||
onItemClick={handleItemClick}
|
||||
/>
|
||||
</aside>
|
||||
|
||||
{/* Content Area */}
|
||||
<main className="flex-1 min-w-0">
|
||||
{filteredCategories.length > 0 ? (
|
||||
<FAQContent
|
||||
categories={filteredCategories}
|
||||
language={language}
|
||||
onActiveItemChange={setActiveItemId}
|
||||
/>
|
||||
) : (
|
||||
<div className="text-center py-12">
|
||||
<p className="text-lg" style={{ color: '#848E9C' }}>
|
||||
{language === 'zh'
|
||||
? '没有找到匹配的问题'
|
||||
: 'No matching questions found'}
|
||||
</p>
|
||||
<button
|
||||
onClick={() => setSearchTerm('')}
|
||||
className="mt-4 px-6 py-2 rounded-lg font-semibold transition-all hover:opacity-90"
|
||||
style={{
|
||||
background:
|
||||
'linear-gradient(135deg, #F0B90B 0%, #FCD535 100%)',
|
||||
color: '#0B0E11',
|
||||
}}
|
||||
>
|
||||
{language === 'zh' ? '清除搜索' : 'Clear Search'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
{/* Contact Section */}
|
||||
<div
|
||||
className="mt-16 p-8 rounded-lg text-center"
|
||||
style={{
|
||||
background:
|
||||
'linear-gradient(135deg, rgba(240, 185, 11, 0.1) 0%, rgba(252, 213, 53, 0.05) 100%)',
|
||||
border: '1px solid rgba(240, 185, 11, 0.2)',
|
||||
}}
|
||||
>
|
||||
<h3 className="text-xl font-bold mb-3" style={{ color: '#EAECEF' }}>
|
||||
{t('faqStillHaveQuestions', language)}
|
||||
</h3>
|
||||
<p className="mb-6" style={{ color: '#848E9C' }}>
|
||||
{t('faqContactUs', language)}
|
||||
</p>
|
||||
<div className="flex items-center justify-center gap-4">
|
||||
<a
|
||||
href="https://github.com/tinkle-community/nofx"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="px-6 py-3 rounded-lg font-semibold transition-all hover:scale-105"
|
||||
style={{
|
||||
background: '#1E2329',
|
||||
color: '#EAECEF',
|
||||
border: '1px solid #2B3139',
|
||||
}}
|
||||
>
|
||||
GitHub
|
||||
</a>
|
||||
<a
|
||||
href="https://t.me/nofx_dev_community"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="px-6 py-3 rounded-lg font-semibold transition-all hover:scale-105"
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #F0B90B 0%, #FCD535 100%)',
|
||||
color: '#0B0E11',
|
||||
}}
|
||||
>
|
||||
{t('community', language)}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
51
web/src/components/faq/FAQSearchBar.tsx
Normal file
51
web/src/components/faq/FAQSearchBar.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import { Search, X } from 'lucide-react'
|
||||
|
||||
interface FAQSearchBarProps {
|
||||
searchTerm: string
|
||||
onSearchChange: (value: string) => void
|
||||
placeholder?: string
|
||||
}
|
||||
|
||||
export function FAQSearchBar({
|
||||
searchTerm,
|
||||
onSearchChange,
|
||||
placeholder = 'Search FAQ...',
|
||||
}: FAQSearchBarProps) {
|
||||
return (
|
||||
<div className="relative">
|
||||
<Search
|
||||
className="absolute left-4 top-1/2 transform -translate-y-1/2 w-5 h-5"
|
||||
style={{ color: '#848E9C' }}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={searchTerm}
|
||||
onChange={(e) => onSearchChange(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
className="w-full pl-12 pr-12 py-3 rounded-lg text-base transition-all focus:outline-none focus:ring-2"
|
||||
style={{
|
||||
background: '#1E2329',
|
||||
border: '1px solid #2B3139',
|
||||
color: '#EAECEF',
|
||||
}}
|
||||
onFocus={(e) => {
|
||||
e.target.style.borderColor = '#F0B90B'
|
||||
e.target.style.boxShadow = '0 0 0 3px rgba(240, 185, 11, 0.1)'
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
e.target.style.borderColor = '#2B3139'
|
||||
e.target.style.boxShadow = 'none'
|
||||
}}
|
||||
/>
|
||||
{searchTerm && (
|
||||
<button
|
||||
onClick={() => onSearchChange('')}
|
||||
className="absolute right-4 top-1/2 transform -translate-y-1/2 hover:opacity-70 transition-opacity"
|
||||
style={{ color: '#848E9C' }}
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
83
web/src/components/faq/FAQSidebar.tsx
Normal file
83
web/src/components/faq/FAQSidebar.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import { t, type Language } from '../../i18n/translations'
|
||||
import type { FAQCategory } from '../../data/faqData'
|
||||
|
||||
interface FAQSidebarProps {
|
||||
categories: FAQCategory[]
|
||||
activeItemId: string | null
|
||||
language: Language
|
||||
onItemClick: (categoryId: string, itemId: string) => void
|
||||
}
|
||||
|
||||
export function FAQSidebar({
|
||||
categories,
|
||||
activeItemId,
|
||||
language,
|
||||
onItemClick,
|
||||
}: FAQSidebarProps) {
|
||||
return (
|
||||
<nav
|
||||
className="sticky top-24 h-[calc(100vh-120px)] overflow-y-auto pr-4"
|
||||
style={{
|
||||
scrollbarWidth: 'thin',
|
||||
scrollbarColor: '#2B3139 #1E2329',
|
||||
}}
|
||||
>
|
||||
<div className="space-y-6">
|
||||
{categories.map((category) => (
|
||||
<div key={category.id}>
|
||||
{/* Category Title */}
|
||||
<div className="flex items-center gap-2 mb-3 px-3">
|
||||
<category.icon className="w-5 h-5" style={{ color: '#F0B90B' }} />
|
||||
<h3
|
||||
className="text-sm font-bold uppercase tracking-wide"
|
||||
style={{ color: '#F0B90B' }}
|
||||
>
|
||||
{t(category.titleKey, language)}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
{/* Category Items */}
|
||||
<ul className="space-y-1">
|
||||
{category.items.map((item) => {
|
||||
const isActive = activeItemId === item.id
|
||||
return (
|
||||
<li key={item.id}>
|
||||
<button
|
||||
onClick={() => onItemClick(category.id, item.id)}
|
||||
className="w-full text-left px-3 py-2 rounded-lg text-sm transition-all"
|
||||
style={{
|
||||
background: isActive
|
||||
? 'rgba(240, 185, 11, 0.1)'
|
||||
: 'transparent',
|
||||
color: isActive ? '#F0B90B' : '#848E9C',
|
||||
borderLeft: isActive
|
||||
? '3px solid #F0B90B'
|
||||
: '3px solid transparent',
|
||||
paddingLeft: isActive ? '9px' : '12px',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (!isActive) {
|
||||
e.currentTarget.style.background =
|
||||
'rgba(240, 185, 11, 0.05)'
|
||||
e.currentTarget.style.color = '#EAECEF'
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (!isActive) {
|
||||
e.currentTarget.style.background = 'transparent'
|
||||
e.currentTarget.style.color = '#848E9C'
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t(item.questionKey, language)}
|
||||
</button>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
@@ -215,45 +215,127 @@ export default function HeaderBar({
|
||||
|
||||
{t('dashboardNav', language)}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
console.log(
|
||||
'FAQ button clicked, onPageChange:',
|
||||
onPageChange
|
||||
)
|
||||
onPageChange?.('faq')
|
||||
}}
|
||||
className="text-sm font-bold transition-all duration-300 relative focus:outline-2 focus:outline-yellow-500"
|
||||
style={{
|
||||
color:
|
||||
currentPage === 'faq'
|
||||
? 'var(--brand-yellow)'
|
||||
: 'var(--brand-light-gray)',
|
||||
padding: '8px 16px',
|
||||
borderRadius: '8px',
|
||||
position: 'relative',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (currentPage !== 'faq') {
|
||||
e.currentTarget.style.color = 'var(--brand-yellow)'
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (currentPage !== 'faq') {
|
||||
e.currentTarget.style.color = 'var(--brand-light-gray)'
|
||||
}
|
||||
}}
|
||||
>
|
||||
{/* Background for selected state */}
|
||||
{currentPage === 'faq' && (
|
||||
<span
|
||||
className="absolute inset-0 rounded-lg"
|
||||
style={{
|
||||
background: 'rgba(240, 185, 11, 0.15)',
|
||||
zIndex: -1,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{t('faqNav', language)}
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
// Landing page navigation when not logged in
|
||||
<a
|
||||
href="/competition"
|
||||
className="text-sm font-bold transition-all duration-300 relative focus:outline-2 focus:outline-yellow-500"
|
||||
style={{
|
||||
color:
|
||||
currentPage === 'competition'
|
||||
? 'var(--brand-yellow)'
|
||||
: 'var(--brand-light-gray)',
|
||||
padding: '8px 16px',
|
||||
borderRadius: '8px',
|
||||
position: 'relative',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (currentPage !== 'competition') {
|
||||
e.currentTarget.style.color = 'var(--brand-yellow)'
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (currentPage !== 'competition') {
|
||||
e.currentTarget.style.color = 'var(--brand-light-gray)'
|
||||
}
|
||||
}}
|
||||
>
|
||||
{/* Background for selected state */}
|
||||
{currentPage === 'competition' && (
|
||||
<span
|
||||
className="absolute inset-0 rounded-lg"
|
||||
style={{
|
||||
background: 'rgba(240, 185, 11, 0.15)',
|
||||
zIndex: -1,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<>
|
||||
<a
|
||||
href="/competition"
|
||||
className="text-sm font-bold transition-all duration-300 relative focus:outline-2 focus:outline-yellow-500"
|
||||
style={{
|
||||
color:
|
||||
currentPage === 'competition'
|
||||
? 'var(--brand-yellow)'
|
||||
: 'var(--brand-light-gray)',
|
||||
padding: '8px 16px',
|
||||
borderRadius: '8px',
|
||||
position: 'relative',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (currentPage !== 'competition') {
|
||||
e.currentTarget.style.color = 'var(--brand-yellow)'
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (currentPage !== 'competition') {
|
||||
e.currentTarget.style.color = 'var(--brand-light-gray)'
|
||||
}
|
||||
}}
|
||||
>
|
||||
{/* Background for selected state */}
|
||||
{currentPage === 'competition' && (
|
||||
<span
|
||||
className="absolute inset-0 rounded-lg"
|
||||
style={{
|
||||
background: 'rgba(240, 185, 11, 0.15)',
|
||||
zIndex: -1,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{t('realtimeNav', language)}
|
||||
</a>
|
||||
{t('realtimeNav', language)}
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="/faq"
|
||||
className="text-sm font-bold transition-all duration-300 relative focus:outline-2 focus:outline-yellow-500"
|
||||
style={{
|
||||
color:
|
||||
currentPage === 'faq'
|
||||
? 'var(--brand-yellow)'
|
||||
: 'var(--brand-light-gray)',
|
||||
padding: '8px 16px',
|
||||
borderRadius: '8px',
|
||||
position: 'relative',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (currentPage !== 'faq') {
|
||||
e.currentTarget.style.color = 'var(--brand-yellow)'
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (currentPage !== 'faq') {
|
||||
e.currentTarget.style.color = 'var(--brand-light-gray)'
|
||||
}
|
||||
}}
|
||||
>
|
||||
{/* Background for selected state */}
|
||||
{currentPage === 'faq' && (
|
||||
<span
|
||||
className="absolute inset-0 rounded-lg"
|
||||
style={{
|
||||
background: 'rgba(240, 185, 11, 0.15)',
|
||||
zIndex: -1,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{t('faqNav', language)}
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -650,6 +732,41 @@ export default function HeaderBar({
|
||||
|
||||
{t('dashboardNav', language)}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
console.log(
|
||||
'移动端 FAQ button clicked, onPageChange:',
|
||||
onPageChange
|
||||
)
|
||||
onPageChange?.('faq')
|
||||
setMobileMenuOpen(false)
|
||||
}}
|
||||
className="block text-sm font-bold transition-all duration-300 relative focus:outline-2 focus:outline-yellow-500 hover:text-yellow-500"
|
||||
style={{
|
||||
color:
|
||||
currentPage === 'faq'
|
||||
? 'var(--brand-yellow)'
|
||||
: 'var(--brand-light-gray)',
|
||||
padding: '12px 16px',
|
||||
borderRadius: '8px',
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
textAlign: 'left',
|
||||
}}
|
||||
>
|
||||
{/* Background for selected state */}
|
||||
{currentPage === 'faq' && (
|
||||
<span
|
||||
className="absolute inset-0 rounded-lg"
|
||||
style={{
|
||||
background: 'rgba(240, 185, 11, 0.15)',
|
||||
zIndex: -1,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{t('faqNav', language)}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
268
web/src/data/faqData.ts
Normal file
268
web/src/data/faqData.ts
Normal file
@@ -0,0 +1,268 @@
|
||||
import {
|
||||
BookOpen,
|
||||
Settings,
|
||||
TrendingUp,
|
||||
Wrench,
|
||||
Bot,
|
||||
Database,
|
||||
GitBranch,
|
||||
} from 'lucide-react'
|
||||
import type { LucideIcon } from 'lucide-react'
|
||||
|
||||
export interface FAQItem {
|
||||
id: string
|
||||
questionKey: string
|
||||
answerKey: string
|
||||
}
|
||||
|
||||
export interface FAQCategory {
|
||||
id: string
|
||||
titleKey: string
|
||||
icon: LucideIcon
|
||||
items: FAQItem[]
|
||||
}
|
||||
|
||||
/**
|
||||
* FAQ 数据配置
|
||||
* - titleKey: 分类标题的翻译键
|
||||
* - questionKey: 问题的翻译键
|
||||
* - answerKey: 答案的翻译键
|
||||
*
|
||||
* 所有文本内容都通过翻译键从 i18n/translations.ts 获取
|
||||
*/
|
||||
export const faqCategories: FAQCategory[] = [
|
||||
{
|
||||
id: 'basics',
|
||||
titleKey: 'faqCategoryBasics',
|
||||
icon: BookOpen,
|
||||
items: [
|
||||
{
|
||||
id: 'what-is-nofx',
|
||||
questionKey: 'faqWhatIsNOFX',
|
||||
answerKey: 'faqWhatIsNOFXAnswer',
|
||||
},
|
||||
{
|
||||
id: 'supported-exchanges',
|
||||
questionKey: 'faqSupportedExchanges',
|
||||
answerKey: 'faqSupportedExchangesAnswer',
|
||||
},
|
||||
{
|
||||
id: 'is-profitable',
|
||||
questionKey: 'faqIsProfitable',
|
||||
answerKey: 'faqIsProfitableAnswer',
|
||||
},
|
||||
{
|
||||
id: 'multiple-traders',
|
||||
questionKey: 'faqMultipleTraders',
|
||||
answerKey: 'faqMultipleTradersAnswer',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'contributing',
|
||||
titleKey: 'faqCategoryContributing',
|
||||
icon: GitBranch,
|
||||
items: [
|
||||
{
|
||||
id: 'github-projects-tasks',
|
||||
questionKey: 'faqGithubProjectsTasks',
|
||||
answerKey: 'faqGithubProjectsTasksAnswer',
|
||||
},
|
||||
{
|
||||
id: 'contribute-pr-guidelines',
|
||||
questionKey: 'faqContributePR',
|
||||
answerKey: 'faqContributePRAnswer',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'setup',
|
||||
titleKey: 'faqCategorySetup',
|
||||
icon: Settings,
|
||||
items: [
|
||||
{
|
||||
id: 'system-requirements',
|
||||
questionKey: 'faqSystemRequirements',
|
||||
answerKey: 'faqSystemRequirementsAnswer',
|
||||
},
|
||||
{
|
||||
id: 'need-coding',
|
||||
questionKey: 'faqNeedCoding',
|
||||
answerKey: 'faqNeedCodingAnswer',
|
||||
},
|
||||
{
|
||||
id: 'get-api-keys',
|
||||
questionKey: 'faqGetApiKeys',
|
||||
answerKey: 'faqGetApiKeysAnswer',
|
||||
},
|
||||
{
|
||||
id: 'use-subaccount',
|
||||
questionKey: 'faqUseSubaccount',
|
||||
answerKey: 'faqUseSubaccountAnswer',
|
||||
},
|
||||
{
|
||||
id: 'docker-deployment',
|
||||
questionKey: 'faqDockerDeployment',
|
||||
answerKey: 'faqDockerDeploymentAnswer',
|
||||
},
|
||||
{
|
||||
id: 'balance-shows-zero',
|
||||
questionKey: 'faqBalanceZero',
|
||||
answerKey: 'faqBalanceZeroAnswer',
|
||||
},
|
||||
{
|
||||
id: 'testnet-issues',
|
||||
questionKey: 'faqTestnet',
|
||||
answerKey: 'faqTestnetAnswer',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'trading',
|
||||
titleKey: 'faqCategoryTrading',
|
||||
icon: TrendingUp,
|
||||
items: [
|
||||
{
|
||||
id: 'no-trades',
|
||||
questionKey: 'faqNoTrades',
|
||||
answerKey: 'faqNoTradesAnswer',
|
||||
},
|
||||
{
|
||||
id: 'decision-frequency',
|
||||
questionKey: 'faqDecisionFrequency',
|
||||
answerKey: 'faqDecisionFrequencyAnswer',
|
||||
},
|
||||
{
|
||||
id: 'custom-strategy',
|
||||
questionKey: 'faqCustomStrategy',
|
||||
answerKey: 'faqCustomStrategyAnswer',
|
||||
},
|
||||
{
|
||||
id: 'max-positions',
|
||||
questionKey: 'faqMaxPositions',
|
||||
answerKey: 'faqMaxPositionsAnswer',
|
||||
},
|
||||
{
|
||||
id: 'margin-insufficient',
|
||||
questionKey: 'faqMarginInsufficient',
|
||||
answerKey: 'faqMarginInsufficientAnswer',
|
||||
},
|
||||
{
|
||||
id: 'high-fees',
|
||||
questionKey: 'faqHighFees',
|
||||
answerKey: 'faqHighFeesAnswer',
|
||||
},
|
||||
{
|
||||
id: 'no-take-profit',
|
||||
questionKey: 'faqNoTakeProfit',
|
||||
answerKey: 'faqNoTakeProfitAnswer',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'technical',
|
||||
titleKey: 'faqCategoryTechnical',
|
||||
icon: Wrench,
|
||||
items: [
|
||||
{
|
||||
id: 'binance-api-failed',
|
||||
questionKey: 'faqBinanceApiFailed',
|
||||
answerKey: 'faqBinanceApiFailedAnswer',
|
||||
},
|
||||
{
|
||||
id: 'binance-position-mode',
|
||||
questionKey: 'faqBinancePositionMode',
|
||||
answerKey: 'faqBinancePositionModeAnswer',
|
||||
},
|
||||
{
|
||||
id: 'port-in-use',
|
||||
questionKey: 'faqPortInUse',
|
||||
answerKey: 'faqPortInUseAnswer',
|
||||
},
|
||||
{
|
||||
id: 'frontend-loading',
|
||||
questionKey: 'faqFrontendLoading',
|
||||
answerKey: 'faqFrontendLoadingAnswer',
|
||||
},
|
||||
{
|
||||
id: 'database-locked',
|
||||
questionKey: 'faqDatabaseLocked',
|
||||
answerKey: 'faqDatabaseLockedAnswer',
|
||||
},
|
||||
{
|
||||
id: 'ai-learning-failed',
|
||||
questionKey: 'faqAiLearningFailed',
|
||||
answerKey: 'faqAiLearningFailedAnswer',
|
||||
},
|
||||
{
|
||||
id: 'config-not-effective',
|
||||
questionKey: 'faqConfigNotEffective',
|
||||
answerKey: 'faqConfigNotEffectiveAnswer',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'ai',
|
||||
titleKey: 'faqCategoryAI',
|
||||
icon: Bot,
|
||||
items: [
|
||||
{
|
||||
id: 'which-models',
|
||||
questionKey: 'faqWhichModels',
|
||||
answerKey: 'faqWhichModelsAnswer',
|
||||
},
|
||||
{
|
||||
id: 'api-costs',
|
||||
questionKey: 'faqApiCosts',
|
||||
answerKey: 'faqApiCostsAnswer',
|
||||
},
|
||||
{
|
||||
id: 'multiple-models',
|
||||
questionKey: 'faqMultipleModels',
|
||||
answerKey: 'faqMultipleModelsAnswer',
|
||||
},
|
||||
{
|
||||
id: 'ai-learning',
|
||||
questionKey: 'faqAiLearning',
|
||||
answerKey: 'faqAiLearningAnswer',
|
||||
},
|
||||
{
|
||||
id: 'only-short-positions',
|
||||
questionKey: 'faqOnlyShort',
|
||||
answerKey: 'faqOnlyShortAnswer',
|
||||
},
|
||||
{
|
||||
id: 'model-selection',
|
||||
questionKey: 'faqModelSelection',
|
||||
answerKey: 'faqModelSelectionAnswer',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'data',
|
||||
titleKey: 'faqCategoryData',
|
||||
icon: Database,
|
||||
items: [
|
||||
{
|
||||
id: 'data-storage',
|
||||
questionKey: 'faqDataStorage',
|
||||
answerKey: 'faqDataStorageAnswer',
|
||||
},
|
||||
{
|
||||
id: 'api-key-security',
|
||||
questionKey: 'faqApiKeySecurity',
|
||||
answerKey: 'faqApiKeySecurityAnswer',
|
||||
},
|
||||
{
|
||||
id: 'export-history',
|
||||
questionKey: 'faqExportHistory',
|
||||
answerKey: 'faqExportHistoryAnswer',
|
||||
},
|
||||
{
|
||||
id: 'get-help',
|
||||
questionKey: 'faqGetHelp',
|
||||
answerKey: 'faqGetHelpAnswer',
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
@@ -20,6 +20,7 @@ export const translations = {
|
||||
realtimeNav: 'Live',
|
||||
configNav: 'Config',
|
||||
dashboardNav: 'Dashboard',
|
||||
faqNav: 'FAQ',
|
||||
|
||||
// Footer
|
||||
footerTitle: 'NOFX - AI Trading System',
|
||||
@@ -505,6 +506,176 @@ export const translations = {
|
||||
signalSourceWarningMessage:
|
||||
'You have traders that enabled "Use Coin Pool" or "Use OI Top", but signal source API address is not configured yet. This will cause candidate coins count to be 0, and traders cannot work properly.',
|
||||
configureSignalSourceNow: 'Configure Signal Source Now',
|
||||
|
||||
// FAQ Page
|
||||
faqTitle: 'Frequently Asked Questions',
|
||||
faqSubtitle: 'Find answers to common questions about NOFX',
|
||||
faqStillHaveQuestions: 'Still Have Questions?',
|
||||
faqContactUs: 'Join our community or check our GitHub for more help',
|
||||
|
||||
// FAQ Categories
|
||||
faqCategoryBasics: 'General Questions',
|
||||
faqCategoryContributing: 'Contributing & Tasks',
|
||||
faqCategorySetup: 'Setup & Configuration',
|
||||
faqCategoryTrading: 'Trading Questions',
|
||||
faqCategoryTechnical: 'Technical Issues',
|
||||
faqCategoryAI: 'AI & Model Questions',
|
||||
faqCategoryData: 'Data & Privacy',
|
||||
|
||||
// FAQ Questions & Answers - General
|
||||
faqWhatIsNOFX: 'What is NOFX?',
|
||||
faqWhatIsNOFXAnswer:
|
||||
'NOFX is an AI-powered cryptocurrency trading bot that uses large language models (LLMs) to make trading decisions on futures markets.',
|
||||
|
||||
faqSupportedExchanges: 'Which exchanges are supported?',
|
||||
faqSupportedExchangesAnswer:
|
||||
'Binance Futures, Hyperliquid, Aster DEX, and OKX are supported. More exchanges coming soon.',
|
||||
|
||||
faqIsProfitable: 'Is NOFX profitable?',
|
||||
faqIsProfitableAnswer:
|
||||
'AI trading is experimental and not guaranteed to be profitable. Always start with small amounts and never invest more than you can afford to lose.',
|
||||
|
||||
faqMultipleTraders: 'Can I run multiple traders simultaneously?',
|
||||
faqMultipleTradersAnswer:
|
||||
'Yes! NOFX supports running multiple traders with different configurations, AI models, and trading strategies.',
|
||||
|
||||
// Contributing & Community
|
||||
faqGithubProjectsTasks: 'How to use GitHub Projects and pick up tasks?',
|
||||
faqGithubProjectsTasksAnswer:
|
||||
'Roadmap: https://github.com/orgs/NoFxAiOS/projects/3 • Task Dashboard: https://github.com/orgs/NoFxAiOS/projects/5 • Steps: Open links → filter by labels (good first issue / help wanted / frontend / backend) → read Description & Acceptance Criteria → comment "assign me" or self-assign → Fork the repo → sync your fork\'s dev with upstream/dev → create a feature branch from your fork\'s dev → push to your fork → open PR (base: NoFxAiOS/nofx:dev ← compare: your-username/nofx:feat/your-topic) → reference Issue (Closes #123) and use the proper template.',
|
||||
|
||||
faqContributePR: 'How to properly submit PRs and contribute?',
|
||||
faqContributePRAnswer:
|
||||
"Guidelines: • Fork first; branch from your fork's dev (avoid direct commits to upstream main) • Branch naming: feat/..., fix/..., docs/...; Conventional Commits • Run checks before PR: npm --prefix web run lint && npm --prefix web run build • For UI changes, attach screenshots or a short video • Choose the proper PR template (frontend/backend/docs/general) • Open PR from your fork to NoFxAiOS/nofx:dev and link Issue (Closes #123) • Keep rebasing onto upstream/dev; ensure CI passes; prefer small, focused PRs • Read CONTRIBUTING.md and .github/PR_TITLE_GUIDE.md",
|
||||
|
||||
// Setup & Configuration
|
||||
faqSystemRequirements: 'What are the system requirements?',
|
||||
faqSystemRequirementsAnswer:
|
||||
'OS: Linux, macOS, or Windows (Docker recommended); RAM: 2GB minimum, 4GB recommended; Disk: 1GB for application + logs; Network: Stable internet connection.',
|
||||
|
||||
faqNeedCoding: 'Do I need coding experience?',
|
||||
faqNeedCodingAnswer:
|
||||
'No! NOFX has a web UI for all configuration. However, basic command line knowledge helps with setup and troubleshooting.',
|
||||
|
||||
faqGetApiKeys: 'How do I get API keys?',
|
||||
faqGetApiKeysAnswer:
|
||||
'For Binance: Account → API Management → Create API → Enable Futures. For Hyperliquid: Visit Hyperliquid App → API Settings.',
|
||||
|
||||
faqUseSubaccount: 'Should I use a subaccount?',
|
||||
faqUseSubaccountAnswer:
|
||||
'Recommended: Yes, use a subaccount dedicated to NOFX for better risk isolation. However, note that some subaccounts have restrictions (e.g., 5x max leverage on Binance).',
|
||||
|
||||
faqDockerDeployment: 'Docker deployment keeps failing',
|
||||
faqDockerDeploymentAnswer:
|
||||
'Common issues: Network connection problems, dependency installation failures, insufficient memory (needs at least 2C2G). If stuck at "go build", try: docker compose down && docker compose build --no-cache && docker compose up -d',
|
||||
|
||||
faqBalanceZero: 'Account balance shows 0',
|
||||
faqBalanceZeroAnswer:
|
||||
'Funds are likely in spot account instead of futures account, or locked in savings products. You need to manually transfer funds to futures account in Binance.',
|
||||
|
||||
faqTestnet: 'Can I use testnet for testing?',
|
||||
faqTestnetAnswer:
|
||||
'Binance testnet exists but is not well maintained. Prices often stay flat and data quality is poor. We recommend using real trading with small amounts (10-50 USDT) for testing instead.',
|
||||
|
||||
// Trading Questions
|
||||
faqNoTrades: "Why isn't my trader making any trades?",
|
||||
faqNoTradesAnswer:
|
||||
'Common reasons: AI decided to "wait" due to market conditions; Insufficient balance or margin; Position limits reached (default: max 3 positions); Check troubleshooting guide for detailed diagnostics.',
|
||||
|
||||
faqDecisionFrequency: 'How often does the AI make decisions?',
|
||||
faqDecisionFrequencyAnswer:
|
||||
'Configurable! Default is every 3-5 minutes. Too frequent = overtrading, too slow = missed opportunities.',
|
||||
|
||||
faqCustomStrategy: 'Can I customize the trading strategy?',
|
||||
faqCustomStrategyAnswer:
|
||||
'Yes! You can adjust leverage settings, modify coin selection pool, change decision intervals, and customize system prompts (advanced).',
|
||||
|
||||
faqMaxPositions: "What's the maximum number of concurrent positions?",
|
||||
faqMaxPositionsAnswer:
|
||||
'Default: 3 positions. This is a soft limit defined in the AI prompt, not hard-coded.',
|
||||
|
||||
faqMarginInsufficient: 'Margin is insufficient error (code=-2019)',
|
||||
faqMarginInsufficientAnswer:
|
||||
'Common causes: Funds not transferred to futures account; Leverage set too high (default 20-50x); Existing positions using margin; Need to transfer USDT from spot to futures account first.',
|
||||
|
||||
faqHighFees: 'Trading fees are too high',
|
||||
faqHighFeesAnswer:
|
||||
'NOFX default 3-minute scan interval can cause frequent trading. Solutions: Increase decision interval to 5-10 minutes; Optimize system prompt to reduce overtrading; Adjust leverage to reduce position sizes.',
|
||||
|
||||
faqNoTakeProfit: "AI doesn't close profitable positions",
|
||||
faqNoTakeProfitAnswer:
|
||||
'AI may believe the trend will continue. The system lacks trailing stop-loss feature currently. You can manually close positions or adjust the system prompt to be more conservative with profit-taking.',
|
||||
|
||||
// Technical Issues
|
||||
faqBinanceApiFailed: 'Binance API call failed (code=-2015)',
|
||||
faqBinanceApiFailedAnswer:
|
||||
'Error: "Invalid API-key, IP, or permissions for action". Solutions: Add server IP to Binance API whitelist; Check API permissions (needs Read + Futures Trading); Ensure using futures API not unified account API; VPN IP might be unstable.',
|
||||
|
||||
faqBinancePositionMode: 'Binance Position Mode Error (code=-4061)',
|
||||
faqBinancePositionModeAnswer:
|
||||
'Error: "Order\'s position side does not match user\'s setting". Solution: Switch to Hedge Mode (双向持仓) in Binance Futures settings. You must close all positions first before switching.',
|
||||
|
||||
faqPortInUse: "Backend won't start / Port already in use",
|
||||
faqPortInUseAnswer:
|
||||
'Check what\'s using port 8080 with "lsof -i :8080" and change the port in your .env file with NOFX_BACKEND_PORT=8081.',
|
||||
|
||||
faqFrontendLoading: 'Frontend shows "Loading..." forever',
|
||||
faqFrontendLoadingAnswer:
|
||||
'Check if backend is running with "curl http://localhost:8080/api/health". Should return {"status":"ok"}. If not, check the troubleshooting guide.',
|
||||
|
||||
faqDatabaseLocked: 'Database locked error',
|
||||
faqDatabaseLockedAnswer:
|
||||
'Stop all NOFX processes with "docker compose down" or "pkill nofx", then restart with "docker compose up -d".',
|
||||
|
||||
faqAiLearningFailed: 'AI learning data failed to load',
|
||||
faqAiLearningFailedAnswer:
|
||||
'Causes: TA-Lib library not properly installed; Insufficient historical data (need completed trades); Environment configuration issues. Install TA-Lib: pip install TA-Lib or check system dependencies.',
|
||||
|
||||
faqConfigNotEffective: 'Configuration changes not taking effect',
|
||||
faqConfigNotEffectiveAnswer:
|
||||
'For Docker: Need to rebuild with "docker compose down && docker compose up -d --build". For PM2: Restart with "pm2 restart all". Check configuration file format and path are correct.',
|
||||
|
||||
// AI & Model Questions
|
||||
faqWhichModels: 'Which AI models are supported?',
|
||||
faqWhichModelsAnswer:
|
||||
'DeepSeek (recommended for cost/performance), Qwen (Alibaba Cloud), and Custom OpenAI-compatible APIs (can be used for OpenAI, Claude via proxy, or other providers).',
|
||||
|
||||
faqApiCosts: 'How much do API calls cost?',
|
||||
faqApiCostsAnswer:
|
||||
'Depends on your model and decision frequency: DeepSeek: ~$0.10-0.50 per day (1 trader, 5min intervals); Qwen: ~$0.20-0.80 per day; Custom API (e.g., OpenAI GPT-4): ~$2-5 per day. Estimates based on typical usage.',
|
||||
|
||||
faqMultipleModels: 'Can I use multiple AI models?',
|
||||
faqMultipleModelsAnswer:
|
||||
'Yes! Each trader can use a different AI model. You can even A/B test different models.',
|
||||
|
||||
faqAiLearning: 'Does the AI learn from its mistakes?',
|
||||
faqAiLearningAnswer:
|
||||
'Yes, to some extent. NOFX provides historical performance feedback in each decision prompt, allowing the AI to adjust its strategy.',
|
||||
|
||||
faqOnlyShort: 'AI only opens short positions, no long positions',
|
||||
faqOnlyShortAnswer:
|
||||
'The default system prompt contains "Don\'t have a long bias! Shorting is one of your core tools" which may cause this. Also affected by 4-hour timeframe data and model training bias. You can modify the system prompt to be more balanced.',
|
||||
|
||||
faqModelSelection: 'Which DeepSeek version should I use?',
|
||||
faqModelSelectionAnswer:
|
||||
"DeepSeek V3 is recommended for best performance. Alternatives: DeepSeek R1 (reasoning model, slower but better logic), SiliconFlow's DeepSeek (alternative API provider). Most users report good results with V3.",
|
||||
|
||||
// Data & Privacy
|
||||
faqDataStorage: 'Where is my data stored?',
|
||||
faqDataStorageAnswer:
|
||||
'All data is stored locally on your machine in SQLite databases: config.db (trader configurations), trading.db (trade history), and decision_logs/ (AI decision records).',
|
||||
|
||||
faqApiKeySecurity: 'Is my API key secure?',
|
||||
faqApiKeySecurityAnswer:
|
||||
'API keys are stored in local databases. Never share your databases or .env files. We recommend using API keys with IP whitelist restrictions.',
|
||||
|
||||
faqExportHistory: 'Can I export my trading history?',
|
||||
faqExportHistoryAnswer:
|
||||
'Yes! Trading data is in SQLite format. You can query it directly with: sqlite3 trading.db "SELECT * FROM trades;"',
|
||||
|
||||
faqGetHelp: 'Where can I get help?',
|
||||
faqGetHelpAnswer:
|
||||
'Check GitHub Discussions, join our Telegram Community, or open an issue on GitHub.',
|
||||
},
|
||||
zh: {
|
||||
// Header
|
||||
@@ -525,6 +696,7 @@ export const translations = {
|
||||
realtimeNav: '实时',
|
||||
configNav: '配置',
|
||||
dashboardNav: '看板',
|
||||
faqNav: '常见问题',
|
||||
|
||||
// Footer
|
||||
footerTitle: 'NOFX - AI交易系统',
|
||||
@@ -971,6 +1143,176 @@ export const translations = {
|
||||
signalSourceWarningMessage:
|
||||
'您有交易员启用了"使用币种池"或"使用OI Top",但尚未配置信号源API地址。这将导致候选币种数量为0,交易员无法正常工作。',
|
||||
configureSignalSourceNow: '立即配置信号源',
|
||||
|
||||
// FAQ Page
|
||||
faqTitle: '常见问题',
|
||||
faqSubtitle: '查找关于 NOFX 的常见问题解答',
|
||||
faqStillHaveQuestions: '还有其他问题?',
|
||||
faqContactUs: '加入我们的社区或查看 GitHub 获取更多帮助',
|
||||
|
||||
// FAQ Categories
|
||||
faqCategoryBasics: '基础问题',
|
||||
faqCategoryContributing: '贡献与任务',
|
||||
faqCategorySetup: '安装与配置',
|
||||
faqCategoryTrading: '交易问题',
|
||||
faqCategoryTechnical: '技术问题',
|
||||
faqCategoryAI: 'AI与模型问题',
|
||||
faqCategoryData: '数据与隐私',
|
||||
|
||||
// FAQ Questions & Answers - General
|
||||
faqWhatIsNOFX: 'NOFX 是什么?',
|
||||
faqWhatIsNOFXAnswer:
|
||||
'NOFX 是一个 AI 驱动的加密货币交易机器人,使用大语言模型(LLM)在期货市场进行交易决策。',
|
||||
|
||||
faqSupportedExchanges: '支持哪些交易所?',
|
||||
faqSupportedExchangesAnswer:
|
||||
'支持币安合约(Binance Futures)、Hyperliquid、Aster DEX 和 OKX。更多交易所开发中。',
|
||||
|
||||
faqIsProfitable: 'NOFX 能盈利吗?',
|
||||
faqIsProfitableAnswer:
|
||||
'AI 交易是实验性的,不保证盈利。请始终用小额资金测试,不要投入超过您承受能力的资金。',
|
||||
|
||||
faqMultipleTraders: '可以同时运行多个交易员吗?',
|
||||
faqMultipleTradersAnswer:
|
||||
'可以!NOFX 支持运行多个交易员,每个可配置不同的 AI 模型和交易策略。',
|
||||
|
||||
// Contributing & Community
|
||||
faqGithubProjectsTasks: '如何在 GitHub Projects 中领取任务?',
|
||||
faqGithubProjectsTasksAnswer:
|
||||
'路线图:https://github.com/orgs/NoFxAiOS/projects/3 | 任务看板:https://github.com/orgs/NoFxAiOS/projects/5 | 步骤:打开链接 → 按标签筛选(good first issue / help wanted / frontend / backend)→ 阅读描述与验收标准 → 评论“assign me”或自助分配 → Fork 仓库 → 同步你 fork 的 dev 与 upstream/dev → 从你 fork 的 dev 创建特性分支 → 推送到你的 fork → 打开 PR(base:NoFxAiOS/nofx:dev ← compare:你的用户名/nofx:feat/your-topic)→ 关联 Issue(Closes #123)并选择正确模板。',
|
||||
|
||||
faqContributePR: '如何规范地提交 PR 并参与贡献?',
|
||||
faqContributePRAnswer:
|
||||
'规范:• 先 Fork;在你的 fork 的 dev 分支上创建特性分支(避免直接向上游 main 提交)• 分支命名:feat/...、fix/...、docs/...;提交信息遵循 Conventional Commits • PR 前运行检查:npm --prefix web run lint && npm --prefix web run build • 涉及 UI 变更请附截图/短视频 • 选择正确 PR 模板(frontend/backend/docs/general)• 从你的 fork 发起到 NoFxAiOS/nofx:dev,并在 PR 中关联 Issue(Closes #123)• 持续 rebase 到 upstream/dev,确保 CI 通过;尽量保持 PR 小而聚焦 • 参考 CONTRIBUTING.md 与 .github/PR_TITLE_GUIDE.md',
|
||||
|
||||
// Setup & Configuration
|
||||
faqSystemRequirements: '系统要求是什么?',
|
||||
faqSystemRequirementsAnswer:
|
||||
'操作系统:Linux、macOS 或 Windows(推荐 Docker);内存:最低 2GB,推荐 4GB;硬盘:应用 + 日志需要 1GB;网络:稳定的互联网连接。',
|
||||
|
||||
faqNeedCoding: '需要编程经验吗?',
|
||||
faqNeedCodingAnswer:
|
||||
'不需要!NOFX 有 Web 界面进行所有配置。但基础的命令行知识有助于安装和故障排查。',
|
||||
|
||||
faqGetApiKeys: '如何获取 API 密钥?',
|
||||
faqGetApiKeysAnswer:
|
||||
'币安:账户 → API 管理 → 创建 API → 启用合约。Hyperliquid:访问 Hyperliquid App → API 设置。',
|
||||
|
||||
faqUseSubaccount: '应该使用子账户吗?',
|
||||
faqUseSubaccountAnswer:
|
||||
'推荐:是的,使用专门的子账户运行 NOFX 可以更好地隔离风险。但请注意,某些子账户有限制(例如币安子账户最高 5 倍杠杆)。',
|
||||
|
||||
faqDockerDeployment: 'Docker 部署一直失败',
|
||||
faqDockerDeploymentAnswer:
|
||||
'常见问题:网络连接问题、依赖安装失败、内存不足(需要至少 2C2G)。如果卡在 "go build" 不动,尝试:docker compose down && docker compose build --no-cache && docker compose up -d',
|
||||
|
||||
faqBalanceZero: '账户余额显示为 0',
|
||||
faqBalanceZeroAnswer:
|
||||
'资金可能在现货账户而非合约账户,或被理财功能锁定。您需要在币安手动将资金划转到合约账户。',
|
||||
|
||||
faqTestnet: '可以使用测试网测试吗?',
|
||||
faqTestnetAnswer:
|
||||
'币安测试网存在但维护不佳,价格经常横盘,数据质量差。我们建议使用真实交易但小额资金(10-50 USDT)进行测试。',
|
||||
|
||||
// Trading Questions
|
||||
faqNoTrades: '为什么我的交易员不开仓?',
|
||||
faqNoTradesAnswer:
|
||||
'常见原因:AI 根据市场情况决定"等待";余额或保证金不足;达到持仓上限(默认最多 3 个仓位);查看故障排查指南了解详细诊断。',
|
||||
|
||||
faqDecisionFrequency: 'AI 多久做一次决策?',
|
||||
faqDecisionFrequencyAnswer:
|
||||
'可配置!默认是每 3-5 分钟。太频繁 = 过度交易,太慢 = 错过机会。',
|
||||
|
||||
faqCustomStrategy: '可以自定义交易策略吗?',
|
||||
faqCustomStrategyAnswer:
|
||||
'可以!您可以调整杠杆设置、修改币种选择池、更改决策间隔、自定义系统提示词(高级)。',
|
||||
|
||||
faqMaxPositions: '最多可以同时持有多少个仓位?',
|
||||
faqMaxPositionsAnswer:
|
||||
'默认:3 个仓位。这是 AI 提示词中的软限制,不是硬编码。',
|
||||
|
||||
faqMarginInsufficient: '保证金不足错误 (code=-2019)',
|
||||
faqMarginInsufficientAnswer:
|
||||
'常见原因:资金未划转到合约账户;杠杆倍数设置过高(默认 20-50 倍);已有持仓占用保证金;需要先从现货账户划转 USDT 到合约账户。',
|
||||
|
||||
faqHighFees: '交易手续费太高',
|
||||
faqHighFeesAnswer:
|
||||
'NOFX 默认 3 分钟扫描间隔会导致频繁交易。解决方案:将决策间隔增加到 5-10 分钟;优化系统提示词减少过度交易;调整杠杆降低仓位大小。',
|
||||
|
||||
faqNoTakeProfit: 'AI 不平掉盈利的仓位',
|
||||
faqNoTakeProfitAnswer:
|
||||
'AI 可能认为趋势会继续。系统目前缺少移动止盈功能。您可以手动平仓或调整系统提示词使其在获利时更保守。',
|
||||
|
||||
// Technical Issues
|
||||
faqBinanceApiFailed: '币安 API 调用失败 (code=-2015)',
|
||||
faqBinanceApiFailedAnswer:
|
||||
'错误:"Invalid API-key, IP, or permissions for action"。解决方案:将服务器 IP 添加到币安 API 白名单;检查 API 权限(需要读取 + 合约交易);确保使用合约 API 而非统一账户 API;VPN IP 可能不稳定。',
|
||||
|
||||
faqBinancePositionMode: '币安持仓模式错误 (code=-4061)',
|
||||
faqBinancePositionModeAnswer:
|
||||
'错误信息:"Order\'s position side does not match user\'s setting"。解决方法:切换为双向持仓模式。登录币安合约 → 点击右上角偏好设置 → 选择持仓模式 → 双向持仓。注意:先平掉所有持仓。',
|
||||
|
||||
faqPortInUse: '后端无法启动 / 端口被占用',
|
||||
faqPortInUseAnswer:
|
||||
'使用 "lsof -i :8080" 查看占用端口的进程,在 .env 中修改端口:NOFX_BACKEND_PORT=8081。',
|
||||
|
||||
faqFrontendLoading: '前端一直显示"加载中..."',
|
||||
faqFrontendLoadingAnswer:
|
||||
'使用 "curl http://localhost:8080/api/health" 检查后端是否运行。应该返回 {"status":"ok"}。如果不是,查看故障排查指南。',
|
||||
|
||||
faqDatabaseLocked: '数据库锁定错误',
|
||||
faqDatabaseLockedAnswer:
|
||||
'使用 "docker compose down" 或 "pkill nofx" 停止所有 NOFX 进程,然后使用 "docker compose up -d" 重启。',
|
||||
|
||||
faqAiLearningFailed: 'AI 学习数据加载失败',
|
||||
faqAiLearningFailedAnswer:
|
||||
'原因:TA-Lib 库未正确安装;历史数据不足(需要完成交易);环境配置问题。安装 TA-Lib:pip install TA-Lib 或检查系统依赖。',
|
||||
|
||||
faqConfigNotEffective: '配置文件修改不生效',
|
||||
faqConfigNotEffectiveAnswer:
|
||||
'Docker 需要重新构建:"docker compose down && docker compose up -d --build"。PM2 需要重启:"pm2 restart all"。检查配置文件格式和路径是否正确。',
|
||||
|
||||
// AI & Model Questions
|
||||
faqWhichModels: '支持哪些 AI 模型?',
|
||||
faqWhichModelsAnswer:
|
||||
'DeepSeek(推荐性价比)、Qwen(阿里云通义千问)、自定义 OpenAI 兼容 API(可用于 OpenAI、通过代理的 Claude 或其他提供商)。',
|
||||
|
||||
faqApiCosts: 'API 调用成本是多少?',
|
||||
faqApiCostsAnswer:
|
||||
'取决于您的模型和决策频率:DeepSeek:每天约 $0.10-0.50(1 个交易员,5 分钟间隔);Qwen:每天约 $0.20-0.80;自定义 API(例如 OpenAI GPT-4):每天约 $2-5。基于典型使用的估算。',
|
||||
|
||||
faqMultipleModels: '可以使用多个 AI 模型吗?',
|
||||
faqMultipleModelsAnswer:
|
||||
'可以!每个交易员可以使用不同的 AI 模型。您甚至可以 A/B 测试不同模型。',
|
||||
|
||||
faqAiLearning: 'AI 会从错误中学习吗?',
|
||||
faqAiLearningAnswer:
|
||||
'会的,在一定程度上。NOFX 在每次决策提示中提供历史表现反馈,允许 AI 调整策略。',
|
||||
|
||||
faqOnlyShort: 'AI 只开空单,不开多单',
|
||||
faqOnlyShortAnswer:
|
||||
'默认系统提示词包含"不要有做多偏见!做空是你的核心工具之一",可能导致此问题。还受 4 小时周期数据和模型训练偏向性影响。您可以修改系统提示词使其更平衡。',
|
||||
|
||||
faqModelSelection: '应该使用哪个 DeepSeek 版本?',
|
||||
faqModelSelectionAnswer:
|
||||
'推荐使用 DeepSeek V3 以获得最佳性能。备选:DeepSeek R1(推理模型,较慢但逻辑更好)、SiliconFlow 的 DeepSeek(备用 API 提供商)。大多数用户反馈 V3 效果良好。',
|
||||
|
||||
// Data & Privacy
|
||||
faqDataStorage: '我的数据存储在哪里?',
|
||||
faqDataStorageAnswer:
|
||||
'所有数据都本地存储在您的机器上,使用 SQLite 数据库:config.db(交易员配置)、trading.db(交易历史)、decision_logs/(AI 决策记录)。',
|
||||
|
||||
faqApiKeySecurity: 'API 密钥安全吗?',
|
||||
faqApiKeySecurityAnswer:
|
||||
'API 密钥存储在本地数据库中。永远不要分享您的数据库或 .env 文件。我们建议使用带 IP 白名单限制的 API 密钥。',
|
||||
|
||||
faqExportHistory: '可以导出交易历史吗?',
|
||||
faqExportHistoryAnswer:
|
||||
'可以!交易数据是 SQLite 格式。您可以直接查询:sqlite3 trading.db "SELECT * FROM trades;"',
|
||||
|
||||
faqGetHelp: '在哪里可以获得帮助?',
|
||||
faqGetHelpAnswer:
|
||||
'查看 GitHub Discussions、加入 Telegram 社区或在 GitHub 上提出 issue。',
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
76
web/src/pages/FAQPage.tsx
Normal file
76
web/src/pages/FAQPage.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import HeaderBar from '../components/landing/HeaderBar'
|
||||
import { FAQLayout } from '../components/faq/FAQLayout'
|
||||
import { useLanguage } from '../contexts/LanguageContext'
|
||||
import { useAuth } from '../contexts/AuthContext'
|
||||
import { useSystemConfig } from '../hooks/useSystemConfig'
|
||||
import { t } from '../i18n/translations'
|
||||
|
||||
/**
|
||||
* FAQ 页面
|
||||
*
|
||||
* 这个页面只是组件的集合,负责:
|
||||
* - 组装 HeaderBar 和 FAQLayout
|
||||
* - 提供全局状态(语言、用户、系统配置)
|
||||
* - 处理页面级别的导航
|
||||
*
|
||||
* 所有 FAQ 相关的逻辑都在子组件中:
|
||||
* - FAQLayout: 整体布局和搜索逻辑
|
||||
* - FAQSearchBar: 搜索框
|
||||
* - FAQSidebar: 左侧目录
|
||||
* - FAQContent: 右侧内容区
|
||||
*
|
||||
* FAQ 数据配置在 data/faqData.ts
|
||||
*/
|
||||
export function FAQPage() {
|
||||
const { language, setLanguage } = useLanguage()
|
||||
const { user, logout } = useAuth()
|
||||
const { config: systemConfig } = useSystemConfig()
|
||||
|
||||
return (
|
||||
<div
|
||||
className="min-h-screen"
|
||||
style={{ background: '#000000', color: '#EAECEF' }}
|
||||
>
|
||||
<HeaderBar
|
||||
isLoggedIn={!!user}
|
||||
currentPage="faq"
|
||||
language={language}
|
||||
onLanguageChange={setLanguage}
|
||||
user={user}
|
||||
onLogout={logout}
|
||||
isAdminMode={systemConfig?.admin_mode}
|
||||
onPageChange={(page) => {
|
||||
if (page === 'competition') {
|
||||
window.history.pushState({}, '', '/competition')
|
||||
window.location.href = '/competition'
|
||||
} else if (page === 'traders') {
|
||||
window.history.pushState({}, '', '/traders')
|
||||
window.location.href = '/traders'
|
||||
} else if (page === 'trader') {
|
||||
window.history.pushState({}, '', '/dashboard')
|
||||
window.location.href = '/dashboard'
|
||||
} else if (page === 'faq') {
|
||||
window.history.pushState({}, '', '/faq')
|
||||
window.location.href = '/faq'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<FAQLayout language={language} />
|
||||
|
||||
{/* Footer */}
|
||||
<footer
|
||||
className="mt-16"
|
||||
style={{ borderTop: '1px solid #2B3139', background: '#181A20' }}
|
||||
>
|
||||
<div
|
||||
className="max-w-7xl mx-auto px-6 py-6 text-center text-sm"
|
||||
style={{ color: '#5E6673' }}
|
||||
>
|
||||
<p>{t('footerTitle', language)}</p>
|
||||
<p className="mt-1">{t('footerWarning', language)}</p>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user