refactor: deep link for faq

This commit is contained in:
鲁树人
2025-05-19 09:23:14 +09:00
parent 3ab73d8369
commit 3541af7a96
19 changed files with 300 additions and 149 deletions

View File

@@ -19,3 +19,14 @@
display: flex;
flex-direction: column;
}
h1,
h2,
h3,
h4,
h5,
h6 {
&:hover > a[data-anchor] {
opacity: 0.75;
}
}

View File

@@ -13,6 +13,8 @@ import { FaqTab } from '~/tabs/FaqTab';
import { SETTINGS_TABS } from '~/features/settings/settingsTabs';
import { Bounce, ToastContainer } from 'react-toastify';
import { SettingsHome } from '~/features/settings/SettingsHome';
import { FAQ_PAGES } from '~/faq/FAQPages';
import { FaqHome } from '~/faq/FaqHome';
// Private to this file only.
const store = setupStore();
@@ -49,7 +51,12 @@ export function AppRoot() {
<Route key={key} path={key} Component={Tab} />
))}
</Route>
<Route path="/questions" Component={FaqTab} />
<Route path="/questions" Component={FaqTab}>
<Route index Component={FaqHome} />
{FAQ_PAGES.map(({ id, Component }) => (
<Route key={id} path={id} Component={Component} />
))}
</Route>
</Routes>
</main>

View File

@@ -0,0 +1,9 @@
import { RiLink } from 'react-icons/ri';
export function HeaderAnchor({ id }: { id: string }) {
return (
<a href={`#${id}`} data-anchor={id} className="absolute -left-6 opacity-10 transition-opacity duration-200">
<RiLink className="max-h-[.75em]" />
</a>
);
}

View File

@@ -1,4 +1,5 @@
import React from 'react';
import { HeaderAnchor } from './HeaderAnchor';
export interface HeaderProps {
children: React.ReactNode;
@@ -6,9 +7,20 @@ export interface HeaderProps {
className?: string;
}
const commonHeaderClasses = 'relative flex items-center pt-3 pb-1 font-bold';
export function Header2({ children, className, id }: HeaderProps) {
return (
<h2 id={id} className={`${commonHeaderClasses} text-3xl border-b border-base-300 ${className}`}>
{id && <HeaderAnchor id={id} />}
{children}
</h2>
);
}
export function Header3({ children, className, id }: HeaderProps) {
return (
<h3 id={id} className={`text-2xl pt-3 pb-1 font-bold border-b border-base-300 ${className}`}>
<h3 id={id} className={`${commonHeaderClasses} text-2xl border-b border-base-300 ${className}`}>
{id && <HeaderAnchor id={id} />}
{children}
</h3>
);
@@ -16,7 +28,8 @@ export function Header3({ children, className, id }: HeaderProps) {
export function Header4({ children, className, id }: HeaderProps) {
return (
<h4 id={id} className={`text-xl pt-3 pb-1 font-semibold ${className}`}>
<h4 id={id} className={`${commonHeaderClasses} text-xl ${className}`}>
{id && <HeaderAnchor id={id} />}
{children}
</h4>
);
@@ -24,7 +37,8 @@ export function Header4({ children, className, id }: HeaderProps) {
export function Header5({ children, className, id }: HeaderProps) {
return (
<h5 id={id} className={`text-lg pt-3 pb-1 font-semibold ${className}`}>
<h5 id={id} className={`${commonHeaderClasses} text-lg ${className}`}>
{id && <HeaderAnchor id={id} />}
{children}
</h5>
);

View File

@@ -0,0 +1,43 @@
import classNames from 'classnames';
import { useRef } from 'react';
export interface ImageFigureProps {
srcSet: string;
alt: string;
className?: string;
loading?: 'lazy' | 'eager';
children?: React.ReactNode;
}
export function ImageFigure({ alt, srcSet, children, className, loading }: ImageFigureProps) {
const refDialog = useRef<HTMLDialogElement>(null);
return (
<figure className={classNames(className, 'inline-flex flex-col items-center')}>
<img
className={`rounded-md cursor-pointer border border-base-300 max-h-48`}
loading={loading}
srcSet={srcSet}
alt={alt}
onClick={() => refDialog?.current?.showModal()}
/>
{children && <figcaption className="text-sm text-base-content/70">{children}</figcaption>}
<dialog ref={refDialog} className="modal text-left">
<div className="modal-box max-w-[50vw]">
<form method="dialog">
<button className="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"></button>
</form>
<h3 className="font-bold text-lg"></h3>
<figure className="flex flex-col justify-center text-center">
<img srcSet={srcSet} alt={alt} />
{children && <figcaption className="text-sm text-base-content/70">{children}</figcaption>}
</figure>
</div>
<form method="dialog" className="modal-backdrop">
<button></button>
</form>
</dialog>
</figure>
);
}

View File

@@ -1,3 +1,4 @@
import classNames from 'classnames';
import React, { Fragment, useId } from 'react';
export type InstructionTab = {
@@ -8,19 +9,24 @@ export type InstructionTab = {
export interface InstructionsTabsProps {
tabs: InstructionTab[];
limitHeight?: boolean;
}
export function InstructionsTabs({ tabs }: InstructionsTabsProps) {
export function InstructionsTabs({ limitHeight = false, tabs }: InstructionsTabsProps) {
const id = useId();
return (
<div className="tabs tabs-lift max-h-[32rem] pb-4">
<div className={classNames('tabs tabs-lift pb-4', { 'max-h-[32rem]': limitHeight })}>
{tabs.map(({ id: _tabId, label, content }, index) => (
<Fragment key={_tabId}>
<label className="tab">
<input type="radio" name={id} defaultChecked={index === 0} />
{label}
</label>
<div className="tab-content border-base-300 bg-base-100 px-4 py-2 overflow-y-auto max-h-[30rem]">
<div
className={classNames('tab-content border-base-300 bg-base-100 px-4 py-2 overflow-y-auto', {
'max-h-[30rem]': limitHeight,
})}
>
{content}
</div>
</Fragment>

View File

@@ -0,0 +1,75 @@
import { ExtLink } from '~/components/ExtLink';
import { Header2, Header3, Header4 } from '~/components/HelpText/Headers';
import { VQuote } from '~/components/HelpText/VQuote';
import { RiErrorWarningLine } from 'react-icons/ri';
import LdPlayerSettingsMisc2x from './assets/ld_settings_misc@2x.webp';
import MumuSettingsMisc2x from './assets/mumu_settings_misc@2x.webp';
import { ImageFigure } from '~/components/ImageFigure';
export function AndroidEmulatorFAQ() {
return (
<>
<Header2></Header2>
<p className="mb-2">便 root </p>
<ul className="list-disc pl-6 mb-2">
<li>
<ExtLink href="https://mumu.163.com/"> MuMu 12</ExtLink> - Hyper-V
</li>
<li>
<ExtLink href="https://www.ldmnq.com/"> 9</ExtLink>
</li>
</ul>
<p className="mb-2">广使</p>
<div className="my-2 alert alert-warning">
<RiErrorWarningLine className="text-lg" />
<p>
使<strong></strong>
</p>
</div>
<p className="mb-2">使</p>
<Header3 id="enable-root"> root</Header3>
<p className="mb-2"> root </p>
<Header4 id="root-mumu"> MuMu </Header4>
<ul className="list-disc pl-6">
<li>
<VQuote></VQuote>
</li>
<li>
<VQuote></VQuote>
</li>
<li>
<VQuote>Root权限</VQuote>
</li>
</ul>
<div>
<ImageFigure className="ml-2" alt="网易木木模拟器设置界面" loading="lazy" srcSet={`${MumuSettingsMisc2x} 2x`}>
</ImageFigure>
</div>
<Header4 id="root-ld"></Header4>
<ul className="list-disc pl-6">
<li>
<VQuote></VQuote>
</li>
<li>
<VQuote></VQuote>
</li>
<li>
<VQuote>ROOT </VQuote><VQuote></VQuote>
</li>
</ul>
<div>
<ImageFigure className="ml-2" alt="雷电模拟器设置界面" loading="lazy" srcSet={`${LdPlayerSettingsMisc2x} 2x`}>
</ImageFigure>
</div>
</>
);
}

20
src/faq/FAQPages.tsx Normal file
View File

@@ -0,0 +1,20 @@
import type { ComponentType } from 'react';
import { QQMusicFAQ } from './QQMusicFAQ';
import { KuwoFAQ } from './KuwoFAQ';
import { KugouFAQ } from './KugouFAQ';
import { OtherFAQ } from './OtherFAQ';
import { AndroidEmulatorFAQ } from './AndroidEmulatorFAQ';
export type FAQEntry = {
id: string;
name: string;
Component: ComponentType;
};
export const FAQ_PAGES: FAQEntry[] = [
{ id: 'qqmusic', name: 'QQ 音乐', Component: QQMusicFAQ },
{ id: 'kuwo', name: '酷我音乐', Component: KuwoFAQ },
{ id: 'kugou', name: '酷狗音乐', Component: KugouFAQ },
{ id: 'android-emu', name: '安卓模拟器', Component: AndroidEmulatorFAQ },
{ id: 'other', name: '其它问题', Component: OtherFAQ },
];

14
src/faq/FaqHome.tsx Normal file
View File

@@ -0,0 +1,14 @@
import { ExtLink } from '~/components/ExtLink';
export function FaqHome() {
return (
<div className="flex flex-col gap-4">
<h1 className="text-2xl font-bold"></h1>
<p></p>
<p>
访
<ExtLink href={'https://t.me/unlock_music_chat'}>- </ExtLink>
</p>
</div>
);
}

View File

@@ -1,4 +1,4 @@
import { Header4 } from '~/components/HelpText/Headers';
import { Header2, Header3 } from '~/components/HelpText/Headers';
import { SegmentKeyImportInstructions } from './SegmentKeyImportInstructions';
import { KugouAllInstructions } from '~/features/settings/panels/Kugou/KugouAllInstructions.tsx';
import { RiErrorWarningLine } from 'react-icons/ri';
@@ -6,19 +6,19 @@ import { RiErrorWarningLine } from 'react-icons/ri';
export function KugouFAQ() {
return (
<>
<Header4></Header4>
<p>
<Header2></Header2>
<Header3 id="failed"></Header3>
<p className="mb-2">
<code>kgg</code> Windows
</p>
<p className="my-4"></p>
<p className="mb-2"></p>
<div className="p-2 @container">
<div className="alert alert-warning">
<div className="alert alert-warning mb-2">
<RiErrorWarningLine className="size-6" />
<p> root </p>
</div>
</div>
<Header3 id="keys"></Header3>
<SegmentKeyImportInstructions tab="酷狗密钥" clientInstructions={<KugouAllInstructions />} />
</>
);

View File

@@ -1,4 +1,4 @@
import { Header4 } from '~/components/HelpText/Headers';
import { Header2, Header3 } from '~/components/HelpText/Headers';
import { VQuote } from '~/components/HelpText/VQuote';
import { SegmentTryOfficialPlayer } from './SegmentTryOfficialPlayer';
import { HiWord } from '~/components/HelpText/HiWord';
@@ -9,9 +9,10 @@ import { RiErrorWarningLine } from 'react-icons/ri';
export function KuwoFAQ() {
return (
<>
<Header4></Header4>
<Header2></Header2>
<Header3 id="failed"></Header3>
<SegmentTryOfficialPlayer />
<p className="my-4">
<p className="mb-2">
<HiWord></HiWord>
<VQuote>
<strong></strong>
@@ -22,10 +23,11 @@ export function KuwoFAQ() {
</VQuote>
</p>
<p className="my-4"></p>
<p className="my-4">PC平台暂未推出使用新版加密的音质</p>
<p className="mb-2"></p>
<p className="mb-2">PC平台暂未推出使用新版加密的音质</p>
<div className="alert alert-warning mb-4">
<Header3 id="keys"></Header3>
<div className="alert alert-warning my-2">
<RiErrorWarningLine className="text-2xl" />
<div>
<p> root </p>

View File

@@ -1,20 +1,18 @@
import { ExtLink } from '~/components/ExtLink';
import { Header4, Header5 } from '~/components/HelpText/Headers';
import { VQuote } from '~/components/HelpText/VQuote';
import { Header2, Header3, Header4 } from '~/components/HelpText/Headers';
import { ProjectIssue } from '~/components/ProjectIssue';
import { RiErrorWarningLine } from 'react-icons/ri';
import LdPlayerSettingsMisc2x from './assets/ld_settings_misc@2x.webp';
import MumuSettingsMisc2x from './assets/mumu_settings_misc@2x.webp';
import { NavLink } from 'react-router';
export function OtherFAQ() {
return (
<>
<Header4></Header4>
<Header2></Header2>
<Header3 id="metadata"></Header3>
<p></p>
<p>使</p>
<Header4></Header4>
<Header3 id="batch-dl"></Header3>
<p>
{'暂时没有实现,不过你可以在 '}
<ProjectIssue id={34} title="[UI] 全部下载功能" />
@@ -23,11 +21,11 @@ export function OtherFAQ() {
{' 追踪该问题。'}
</p>
<Header4>安卓: 浏览器支持说明</Header4>
<Header3 id="android-browsers">安卓: 浏览器支持说明</Header3>
<p> 使 Chrome Firefox </p>
<div className="flex flex-col md:flex-row gap-2 md:gap-8">
<div>
<Header5></Header5>
<Header4></Header4>
<ul className="list-disc list-inside pl-2">
<li>Via </li>
<li></li>
@@ -36,7 +34,7 @@ export function OtherFAQ() {
</div>
<div>
<Header5></Header5>
<Header4></Header4>
<ul className="list-disc list-inside pl-2">
<li></li>
<li></li>
@@ -45,70 +43,20 @@ export function OtherFAQ() {
</div>
</div>
<Header4>安卓: root </Header4>
<Header3 id="android-root"> root</Header3>
<p>
root 使
使 NFC
</p>
<p className="my-2">使</p>
<p className="my-2">
root 便
<VQuote>
<ExtLink href="https://mumu.163.com/"> MuMu 12</ExtLink>
</VQuote>
<VQuote>
<ExtLink href="https://www.ldmnq.com/"> 9</ExtLink>
</VQuote>
使
<NavLink className="link link-info" to="/questions/android-emu">
</NavLink>
</p>
<div className="my-4 alert alert-warning">
<RiErrorWarningLine className="text-lg" />
<div>
<p>
使<strong></strong>
</p>
<p>使</p>
</div>
</div>
<p> root </p>
<div className="grid grid-cols-1 gap-2 md:gap-4 lg:grid-cols-2">
<div>
<Header5> MuMu模拟器</Header5>
<ul className="list-disc pl-6">
<li>
<VQuote></VQuote>
</li>
<li>
<VQuote></VQuote>
</li>
<li>
<VQuote>Root权限</VQuote>
</li>
</ul>
<img className="rounded-md border border-base-300" loading="lazy" srcSet={`${MumuSettingsMisc2x} 2x`} />
</div>
<div>
<Header5></Header5>
<ul className="list-disc pl-6">
<li>
<VQuote></VQuote>
</li>
<li>
<VQuote></VQuote>
</li>
<li>
<VQuote>ROOT </VQuote><VQuote></VQuote>
</li>
</ul>
<img className="rounded-md border border-base-300" loading="lazy" srcSet={`${LdPlayerSettingsMisc2x} 2x`} />
</div>
</div>
<Header4></Header4>
<Header3 id="projects"></Header3>
<ul className="list-disc pl-6">
<li>
<p>
@@ -150,7 +98,7 @@ export function OtherFAQ() {
</li>
</ul>
<Header4></Header4>
<Header3 id="more-questions"></Header3>
<p className="flex flex-row gap-1">
<ExtLink href={'https://t.me/unlock_music_chat'}>- </ExtLink>

View File

@@ -1,17 +1,24 @@
import { Header4 } from '~/components/HelpText/Headers';
import { Header2, Header3 } from '~/components/HelpText/Headers';
import { SegmentTryOfficialPlayer } from './SegmentTryOfficialPlayer';
import { QMCv2QQMusicAllInstructions } from '~/features/settings/panels/QMCv2/QMCv2QQMusicAllInstructions';
export function QQMusicFAQ() {
return (
<>
<Header4></Header4>
<Header2>QQ </Header2>
<Header3 id="failed"></Header3>
<SegmentTryOfficialPlayer />
<p className="my-4"></p>
<p className="mb-4">
<p className="mb-2"> QQ </p>
<p className="mb-2"></p>
<p className="mb-2"></p>
<Header3 id="about-download"></Header3>
<p></p>
<p className="my-2">
<strong></strong>使
</p>
<Header3 id="keys-or-downgrade"></Header3>
<QMCv2QQMusicAllInstructions />
</>
);

View File

@@ -2,7 +2,7 @@ import { RiErrorWarningLine } from 'react-icons/ri';
export function SegmentTryOfficialPlayer({ className = '' }: { className?: string }) {
return (
<div className={`alert alert-warning ${className}`}>
<div className={`alert alert-warning my-2 ${className}`}>
<RiErrorWarningLine className="text-2xl" />
<p></p>
</div>

View File

@@ -20,7 +20,7 @@ export function ResponsiveNav({
className={`@container/nav grow grid grid-cols-1 grid-rows-[auto_1fr] md:grid-rows-1 md:grid-cols-[10rem_1fr] ${className}`}
>
{/* Sidebar */}
<aside className={`bg-base-300 md:p-4 md:block ${navigationClassName}`}>{navigation}</aside>
<aside className={`bg-base-100 md:p-4 md:block ${navigationClassName}`}>{navigation}</aside>
{/* Main content */}
<div className={`p-4 grow ${contentClassName}`}>{children}</div>

View File

@@ -0,0 +1,21 @@
import classNames from 'classnames';
import type { RefAttributes } from 'react';
import { NavLink, type NavLinkProps } from 'react-router';
const tabClassNames = ({ isActive }: { isActive: boolean }) =>
classNames(
'link inline-flex text-nowrap mb-[-2px] no-underline w-full',
'border-b-2 md:border-b-0 md:border-r-2',
'tab md:grow',
{
'tab-active bg-accent/10 border-accent': isActive,
},
);
export function TabNavLink({ children, ...props }: NavLinkProps & RefAttributes<HTMLAnchorElement>) {
return (
<NavLink className={tabClassNames} role="tab" {...props}>
{children}
</NavLink>
);
}

View File

@@ -1,12 +1,12 @@
import { useAppDispatch, useAppSelector } from '~/hooks';
import { commitStagingChange, discardStagingChanges } from './settingsSlice';
import { selectIsSettingsNotSaved } from './settingsSelector';
import { NavLink, Outlet } from 'react-router';
import { Outlet } from 'react-router';
import { SETTINGS_TABS } from '~/features/settings/settingsTabs.tsx';
import { MdOutlineSettingsBackupRestore } from 'react-icons/md';
import { toast } from 'react-toastify';
import { ResponsiveNav } from '../nav/ResponsiveNav';
import classNames from 'classnames';
import { TabNavLink } from '../nav/TabNavLink';
export function Settings() {
const dispatch = useAppDispatch();
@@ -27,16 +27,6 @@ export function Settings() {
};
const isSettingsNotSaved = useAppSelector(selectIsSettingsNotSaved);
const tabClassNames = ({ isActive }: { isActive: boolean }) =>
classNames(
'link inline-flex text-nowrap mb-[-2px] no-underline w-full',
'border-b-2 md:border-b-0 md:border-r-2',
'tab md:grow',
{
'tab-active bg-accent/10 border-accent': isActive,
},
);
return (
<div className="flex flex-col flex-1 container w-full">
<ResponsiveNav
@@ -46,9 +36,9 @@ export function Settings() {
navigation={
<div role="tablist" className="tabs gap-1 flex-nowrap md:flex-col grow items-center">
{Object.entries(SETTINGS_TABS).map(([id, { name }]) => (
<NavLink className={tabClassNames} key={id} to={`/settings/${id}`} role="tab">
<TabNavLink key={id} to={`/settings/${id}`}>
{name}
</NavLink>
</TabNavLink>
))}
</div>
}

View File

@@ -4,7 +4,7 @@ import { InstructionsMac } from './InstructionsMac';
import { InstructionsPC } from './InstructionsPC';
import { InstructionsTabs, InstructionTab } from '~/components/InstructionsTabs.tsx';
export function QMCv2QQMusicAllInstructions() {
export function QMCv2QQMusicAllInstructions({ limitHeight }: { limitHeight?: boolean }) {
const tabs: InstructionTab[] = [
{
id: 'android',
@@ -16,5 +16,5 @@ export function QMCv2QQMusicAllInstructions() {
{ id: 'windows', label: 'Windows', content: <InstructionsPC /> },
];
return <InstructionsTabs tabs={tabs} />;
return <InstructionsTabs tabs={tabs} limitHeight={limitHeight} />;
}

View File

@@ -1,43 +1,27 @@
import { ComponentType, Fragment } from 'react';
import { Header3 } from '~/components/HelpText/Headers';
import { KuwoFAQ } from '~/faq/KuwoFAQ';
import { OtherFAQ } from '~/faq/OtherFAQ';
import { QQMusicFAQ } from '~/faq/QQMusicFAQ';
import { KugouFAQ } from '~/faq/KugouFAQ.tsx';
type FAQEntry = {
id: string;
title: string;
Help: ComponentType;
};
const faqEntries: FAQEntry[] = [
{ id: 'qqmusic', title: 'QQ 音乐', Help: QQMusicFAQ },
{ id: 'kuwo', title: '酷我音乐', Help: KuwoFAQ },
{ id: 'kugou', title: '酷狗音乐', Help: KugouFAQ },
{ id: 'other', title: '其它问题', Help: OtherFAQ },
];
import { Outlet } from 'react-router';
import { FAQ_PAGES } from '~/faq/FAQPages';
import { ResponsiveNav } from '~/features/nav/ResponsiveNav';
import { TabNavLink } from '~/features/nav/TabNavLink';
export function FaqTab() {
return (
<section className="container pb-10 px-4">
<h2 className="text-3xl font-bold text-center"></h2>
<Header3></Header3>
<ul className="list-disc pl-6">
{faqEntries.map(({ id, title }) => (
<li key={id}>
<a className="link link-info no-underline" href={`#faq-${id}`}>
{title}
</a>
</li>
<div className="flex flex-col flex-1 container w-full">
<ResponsiveNav
className="grow h-full overflow-auto"
contentClassName="flex flex-col overflow-auto pl-6"
navigationClassName="overflow-x-auto pb-[2px] md:pb-0 h-full md:items-center [&]:md:flex"
navigation={
<div role="tablist" className="tabs gap-1 flex-nowrap md:flex-col grow items-center">
{FAQ_PAGES.map(({ id, name }) => (
<TabNavLink key={id} to={`/questions/${id}`}>
{name}
</TabNavLink>
))}
</ul>
{faqEntries.map(({ id, title, Help }) => (
<Fragment key={id}>
<Header3 id={`faq-${id}`}>{title}</Header3>
<Help />
</Fragment>
))}
</section>
</div>
}
>
<Outlet />
</ResponsiveNav>
</div>
);
}