From 3541af7a9674b8484a4eb3701d7472fb496f4527 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=B2=81=E6=A0=91=E4=BA=BA?= Date: Mon, 19 May 2025 09:23:14 +0900 Subject: [PATCH] refactor: deep link for faq --- src/App.css | 11 +++ src/components/AppRoot.tsx | 9 +- src/components/HelpText/HeaderAnchor.tsx | 9 ++ src/components/HelpText/Headers.tsx | 20 ++++- src/components/ImageFigure.tsx | 43 ++++++++++ src/components/InstructionsTabs.tsx | 12 ++- src/faq/AndroidEmulatorFAQ.tsx | 75 +++++++++++++++++ src/faq/FAQPages.tsx | 20 +++++ src/faq/FaqHome.tsx | 14 ++++ src/faq/KugouFAQ.tsx | 18 ++-- src/faq/KuwoFAQ.tsx | 14 ++-- src/faq/OtherFAQ.tsx | 82 ++++--------------- src/faq/QQMusicFAQ.tsx | 15 +++- src/faq/SegmentTryOfficialPlayer.tsx | 2 +- src/features/nav/ResponsiveNav.tsx | 2 +- src/features/nav/TabNavLink.tsx | 21 +++++ src/features/settings/Settings.tsx | 18 +--- .../QMCv2/QMCv2QQMusicAllInstructions.tsx | 4 +- src/tabs/FaqTab.tsx | 60 +++++--------- 19 files changed, 300 insertions(+), 149 deletions(-) create mode 100644 src/components/HelpText/HeaderAnchor.tsx create mode 100644 src/components/ImageFigure.tsx create mode 100644 src/faq/AndroidEmulatorFAQ.tsx create mode 100644 src/faq/FAQPages.tsx create mode 100644 src/faq/FaqHome.tsx create mode 100644 src/features/nav/TabNavLink.tsx diff --git a/src/App.css b/src/App.css index 387b613..36a5e7b 100644 --- a/src/App.css +++ b/src/App.css @@ -19,3 +19,14 @@ display: flex; flex-direction: column; } + +h1, +h2, +h3, +h4, +h5, +h6 { + &:hover > a[data-anchor] { + opacity: 0.75; + } +} diff --git a/src/components/AppRoot.tsx b/src/components/AppRoot.tsx index f293b43..dbee96a 100644 --- a/src/components/AppRoot.tsx +++ b/src/components/AppRoot.tsx @@ -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() { ))} - + + + {FAQ_PAGES.map(({ id, Component }) => ( + + ))} + diff --git a/src/components/HelpText/HeaderAnchor.tsx b/src/components/HelpText/HeaderAnchor.tsx new file mode 100644 index 0000000..c9f3c4a --- /dev/null +++ b/src/components/HelpText/HeaderAnchor.tsx @@ -0,0 +1,9 @@ +import { RiLink } from 'react-icons/ri'; + +export function HeaderAnchor({ id }: { id: string }) { + return ( + + + + ); +} diff --git a/src/components/HelpText/Headers.tsx b/src/components/HelpText/Headers.tsx index e41eab1..726772f 100644 --- a/src/components/HelpText/Headers.tsx +++ b/src/components/HelpText/Headers.tsx @@ -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 ( +

+ {id && } + {children} +

+ ); +} export function Header3({ children, className, id }: HeaderProps) { return ( -

+

+ {id && } {children}

); @@ -16,7 +28,8 @@ export function Header3({ children, className, id }: HeaderProps) { export function Header4({ children, className, id }: HeaderProps) { return ( -

+

+ {id && } {children}

); @@ -24,7 +37,8 @@ export function Header4({ children, className, id }: HeaderProps) { export function Header5({ children, className, id }: HeaderProps) { return ( -
+
+ {id && } {children}
); diff --git a/src/components/ImageFigure.tsx b/src/components/ImageFigure.tsx new file mode 100644 index 0000000..1192480 --- /dev/null +++ b/src/components/ImageFigure.tsx @@ -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(null); + + return ( +
+ {alt} refDialog?.current?.showModal()} + /> + {children &&
{children}
} + + +
+
+ +
+

查看图片

+ +
+ {alt} + {children &&
{children}
} +
+
+
+ +
+
+
+ ); +} diff --git a/src/components/InstructionsTabs.tsx b/src/components/InstructionsTabs.tsx index 5edbe93..0b5289c 100644 --- a/src/components/InstructionsTabs.tsx +++ b/src/components/InstructionsTabs.tsx @@ -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 ( -
+
{tabs.map(({ id: _tabId, label, content }, index) => ( -
+
{content}
diff --git a/src/faq/AndroidEmulatorFAQ.tsx b/src/faq/AndroidEmulatorFAQ.tsx new file mode 100644 index 0000000..557a1ea --- /dev/null +++ b/src/faq/AndroidEmulatorFAQ.tsx @@ -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 ( + <> + 安卓模拟器 +

目前市面上主流的可以很方便 root 的安卓模拟器有两个:

+ +
    +
  • + 网易 MuMu 模拟器(安卓 12) - Hyper-V 兼容较好 +
  • +
  • + 雷电模拟器(安卓 9) +
  • +
+ +

上述两款模拟器均包含广告,使用时请注意。

+ +
+ +

+ 根据应用的风控策略,使用模拟器登录的账号有可能会导致账号被封锁。 +

+
+

读者在使用前请自行评估风险。

+ + 启用 root +

上述的两款模拟器都有提供比较直接的启用 root 的方法。

+ + 网易 MuMu 模拟器 +
    +
  • + 打开设置中心 +
  • +
  • + 选择其他 +
  • +
  • + 勾选开启手机Root权限 +
  • +
+
+ + 网易木木模拟器设置界面 + +
+ + 雷电模拟器 +
    +
  • + 打开模拟器设置 +
  • +
  • + 选择其他 +
  • +
  • + 设置ROOT 权限开启状态 +
  • +
+
+ + 雷电模拟器设置界面 + +
+ + ); +} diff --git a/src/faq/FAQPages.tsx b/src/faq/FAQPages.tsx new file mode 100644 index 0000000..51c0215 --- /dev/null +++ b/src/faq/FAQPages.tsx @@ -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 }, +]; diff --git a/src/faq/FaqHome.tsx b/src/faq/FaqHome.tsx new file mode 100644 index 0000000..2941645 --- /dev/null +++ b/src/faq/FaqHome.tsx @@ -0,0 +1,14 @@ +import { ExtLink } from '~/components/ExtLink'; + +export function FaqHome() { + return ( +
+

答疑

+

从目录选择一项来查看相关说明。

+

+ 也欢迎造访 + “音乐解锁-交流” 交流群 进行交流。 +

+
+ ); +} diff --git a/src/faq/KugouFAQ.tsx b/src/faq/KugouFAQ.tsx index 586b3f4..6c55edc 100644 --- a/src/faq/KugouFAQ.tsx +++ b/src/faq/KugouFAQ.tsx @@ -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 ( <> - 解锁失败 -

+ 酷狗音乐 + 解锁失败 +

酷狗现在对部分用户推送了 kgg 加密格式(安卓、Windows 客户端)。

-

根据平台不同,你需要提取密钥数据库。

+

根据平台不同,你需要提取密钥数据库。

-
-
- -

安卓用户提取密钥需要 root 权限,或注入文件提供器。

-
+
+ +

安卓用户提取密钥需要 root 权限,或注入文件提供器。

+ 导入密钥 } /> ); diff --git a/src/faq/KuwoFAQ.tsx b/src/faq/KuwoFAQ.tsx index f814590..2d26a12 100644 --- a/src/faq/KuwoFAQ.tsx +++ b/src/faq/KuwoFAQ.tsx @@ -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 ( <> - 解锁失败 + 酷我音乐 + 解锁失败 -

+

日前,仅手机客户端下载的 至臻全景声 @@ -22,10 +23,11 @@ export function KuwoFAQ() { 音质的音乐文件采用新版加密。

-

其他音质目前不需要提取密钥。

-

PC平台暂未推出使用新版加密的音质。

+

其他音质目前不需要提取密钥。

+

PC平台暂未推出使用新版加密的音质。

-
+ 导入密钥 +

安卓用户提取密钥需要 root 权限,或注入文件提供器。

diff --git a/src/faq/OtherFAQ.tsx b/src/faq/OtherFAQ.tsx index d6a0331..0304b45 100644 --- a/src/faq/OtherFAQ.tsx +++ b/src/faq/OtherFAQ.tsx @@ -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 ( <> - 解密后没有封面等信息 + 其它问题 + 解密后没有封面等信息

该项目进行解密处理。如果加密前的资源没有内嵌元信息或封面,解密的文件也没有。

请使用第三方工具进行编辑或管理元信息。

- 批量下载 + 批量下载

{'暂时没有实现,不过你可以在 '} @@ -23,11 +21,11 @@ export function OtherFAQ() { {' 追踪该问题。'}

- 安卓: 浏览器支持说明 + 安卓: 浏览器支持说明

⚠️ 手机端浏览器支持有限,请使用最新版本的 Chrome 或 Firefox 官方浏览器。

- 已知有问题的浏览器 + 已知有问题的浏览器
  • Via 浏览器
  • 夸克浏览器
  • @@ -36,7 +34,7 @@ export function OtherFAQ() {
- 可能会遇到的问题包括 + 可能会遇到的问题包括
  • 网页白屏
  • 无法下载解密后内容
  • @@ -45,70 +43,20 @@ export function OtherFAQ() {
- 安卓: root 相关说明 + 安卓 root

对安卓设备获取 root 特权通常会破坏系统的完整性并导致部分功能无法使用。 例如部分厂商的安卓设备会在解锁后丧失保修资格,或导致无法使用 NFC 移动支付等限制。

-

如果希望不破坏系统完整性,你可以考虑在电脑上使用安卓模拟器。

- 很多安卓模拟器都提供了 root 特权支持,可以很方便的启用,例如 - - 网易 MuMu 模拟器(安卓 12,推荐) - - 或 - - 雷电模拟器(安卓 9) - + 如果希望不破坏系统完整性,你可以考虑在电脑上使用 + + 安卓模拟器 +

-
- -
-

- 根据应用的风控策略,使用模拟器登录的账号有可能会导致账号被封锁。 -

-

使用前请自行评估风险。

-
-
- -

以下是为上述模拟器启用 root 的方式:

-
-
- 网易 MuMu模拟器 -
    -
  • - 打开设置中心 -
  • -
  • - 选择其他 -
  • -
  • - 勾选开启手机Root权限 -
  • -
- -
- -
- 雷电模拟器 -
    -
  • - 打开模拟器设置 -
  • -
  • - 选择其他 -
  • -
  • - 设置ROOT 权限开启状态 -
  • -
- -
-
- - 相关项目 + 相关项目
  • @@ -150,7 +98,7 @@ export function OtherFAQ() {

- 有更多问题? + 有更多问题?

欢迎加入 “音乐解锁-交流” 交流群 diff --git a/src/faq/QQMusicFAQ.tsx b/src/faq/QQMusicFAQ.tsx index b1e0391..08e93ae 100644 --- a/src/faq/QQMusicFAQ.tsx +++ b/src/faq/QQMusicFAQ.tsx @@ -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 ( <> - 解锁失败 + QQ 音乐 + 解锁失败 -

重复下载同一首的歌曲不重复扣下载配额,但是同一首歌的两个版本会重复扣下载配额,请仔细分辨。

-

+

新版本的 QQ 音乐客户端下载的文件通常都需要导入密钥数据库。

+

每一个资源(即一首歌的某个音质)都有独立的密钥,下载音乐时会被写出到密钥数据库中。

+

因此若是解密失败,很有可能是因为你需要导入密钥,或降级客户端。

+ + 关于下载 +

重复下载同一首的歌曲不重复扣下载配额,但是同一首歌的两个版本会重复扣下载配额,请仔细分辨。

+

部分平台获取的加密文件未包含密钥。选择你下载文件时使用的客户端来查看说明。

+ 导入密钥或降级客户端 ); diff --git a/src/faq/SegmentTryOfficialPlayer.tsx b/src/faq/SegmentTryOfficialPlayer.tsx index f67efee..ef8bc93 100644 --- a/src/faq/SegmentTryOfficialPlayer.tsx +++ b/src/faq/SegmentTryOfficialPlayer.tsx @@ -2,7 +2,7 @@ import { RiErrorWarningLine } from 'react-icons/ri'; export function SegmentTryOfficialPlayer({ className = '' }: { className?: string }) { return ( -
+

尝试用下载音乐的设备播放一次看看,如果官方客户端都无法播放,那解锁肯定会失败哦。

diff --git a/src/features/nav/ResponsiveNav.tsx b/src/features/nav/ResponsiveNav.tsx index 0a15ab2..b92bb29 100644 --- a/src/features/nav/ResponsiveNav.tsx +++ b/src/features/nav/ResponsiveNav.tsx @@ -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 */} - + {/* Main content */}
{children}
diff --git a/src/features/nav/TabNavLink.tsx b/src/features/nav/TabNavLink.tsx new file mode 100644 index 0000000..517c7c6 --- /dev/null +++ b/src/features/nav/TabNavLink.tsx @@ -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) { + return ( + + {children} + + ); +} diff --git a/src/features/settings/Settings.tsx b/src/features/settings/Settings.tsx index a1cff80..ea3af14 100644 --- a/src/features/settings/Settings.tsx +++ b/src/features/settings/Settings.tsx @@ -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 (
{Object.entries(SETTINGS_TABS).map(([id, { name }]) => ( - + {name} - + ))}
} diff --git a/src/features/settings/panels/QMCv2/QMCv2QQMusicAllInstructions.tsx b/src/features/settings/panels/QMCv2/QMCv2QQMusicAllInstructions.tsx index ba46b77..df483d6 100644 --- a/src/features/settings/panels/QMCv2/QMCv2QQMusicAllInstructions.tsx +++ b/src/features/settings/panels/QMCv2/QMCv2QQMusicAllInstructions.tsx @@ -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: }, ]; - return ; + return ; } diff --git a/src/tabs/FaqTab.tsx b/src/tabs/FaqTab.tsx index d3352e5..aeaaf1f 100644 --- a/src/tabs/FaqTab.tsx +++ b/src/tabs/FaqTab.tsx @@ -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 ( -
-

常见问题解答

- 答疑目录 -
    - {faqEntries.map(({ id, title }) => ( -
  • - - {title} - -
  • - ))} -
- {faqEntries.map(({ id, title, Help }) => ( - - {title} - - - ))} -
+
+ + {FAQ_PAGES.map(({ id, name }) => ( + + {name} + + ))} +
+ } + > + + +
); }