提交 6ffe2961 authored 作者: 王鹏飞's avatar 王鹏飞

feat: update admin and learning apps to use React 19 and Ant Design 6, add…

feat: update admin and learning apps to use React 19 and Ant Design 6, add loading fallback components - Updated admin app to React 19 and Ant Design 6, ensuring both apps maintain separate routers, API clients, and build outputs. - Introduced `LoadingFallback` component for better user experience during loading states in both apps. - Refactored routes in the learning app to utilize lazy loading for improved performance. - Added linting and type checking commands for better code quality assurance. - Updated documentation to reflect changes in project structure and usage instructions.
上级 a68dc1c4
......@@ -2,7 +2,7 @@
## Project Structure & Module Organization
This is a pnpm monorepo. `apps/admin` is the React 18 administration app (mostly JavaScript/JSX); `apps/learning` is the React 19 learning app written in TypeScript. Keep application code, assets, routes, API clients, and tests inside the owning app—never import another app’s `src` directory. Shared packages belong in `packages/` only when they are genuinely used by both apps.
This is a pnpm monorepo. `apps/admin` is the React 19 administration app (mostly JavaScript/JSX); `apps/learning` is the React 19 learning app written in TypeScript. Both apps currently use Ant Design 6, but they keep separate routers, API clients, state, and build outputs. Keep application code, assets, routes, API clients, and tests inside the owning app—never import another app’s `src` directory. Shared packages belong in `packages/` only when they are genuinely used by both apps.
Within an app, feature code lives under `src/modules/<domain>/`; reusable UI belongs in `src/components/`; cross-cutting utilities and hooks belong in `src/utils/` and `src/hooks/`. Keep editor features isolated under `apps/admin/src/components/editor/wangeditor-customer/features/`. Put plans and technical notes in `docs/`.
......@@ -14,6 +14,8 @@ Within an app, feature code lives under `src/modules/<domain>/`; reusable UI bel
- `pnpm build:admin` / `pnpm build:learning` — build one app.
- `pnpm test`, `pnpm test:admin`, `pnpm test:learning` — run Vitest and Node tests.
- `pnpm lint:admin` / `pnpm lint:learning` — run Oxlint with auto-fix; review the resulting diff.
- `pnpm lint:check` — run read-only Oxlint checks for both apps.
- `pnpm typecheck:learning` — run the strict Learning TypeScript check.
- `pnpm --filter @ebook/admin format` — format Admin source with Oxfmt; use the equivalent Learning filter when needed.
## Coding Style & Naming Conventions
......
......@@ -4,6 +4,7 @@ import { useRoutes } from 'react-router'
import { Button } from 'antd'
import './App.less'
import baseRouter from '@/router/routes'
import LoadingFallback from '@/components/LoadingFallback'
function fallbackRender({
error,
......@@ -30,7 +31,7 @@ function AppRoutes() {
export default function App() {
return (
<ErrorBoundary fallbackRender={fallbackRender}>
<Suspense>
<Suspense fallback={<LoadingFallback />}>
<AppRoutes />
</Suspense>
</ErrorBoundary>
......
import { Spin } from 'antd'
export default function LoadingFallback() {
return (
<div
style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
minHeight: 200,
}}
>
<Spin size='large' />
</div>
)
}
......@@ -246,11 +246,13 @@ const Examine = () => {
// 保存方法
const saveContent = async () => {
if ((contentId, editorRef.current, editorRef.current.editor, !loading)) {
const contentHtml = editorRef.current.editor.getHtml()
const editorInstance = editorRef.current?.editor
if (!contentId || loading || !editorInstance) return
const contentHtml = editorInstance.getHtml()
const newMd5 = md5(contentHtml)
clearInterval(saveInterRef.current)
const word_count = editorRef.current.editor.getText().length
const word_count = editorInstance.getText().length
// 保存
const data = await sectionEdit({
id: contentId,
......@@ -267,7 +269,6 @@ const Examine = () => {
}
}
}
}
useEffect(() => {
if (!loading && contentId) {
......
import type { ReactNode } from 'react'
import { Suspense } from 'react'
import { Spin } from 'antd'
import RouteErrorBoundary from './RouteErrorBoundary'
import LoadingFallback from '@/components/LoadingFallback'
export default function ModuleBoundary({ children }: { children: ReactNode }) {
return (
<RouteErrorBoundary>
<Suspense
fallback={
<div
style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
minHeight: 200,
}}
>
<Spin size='large' />
</div>
}
>
{children}
</Suspense>
<Suspense fallback={<LoadingFallback />}>{children}</Suspense>
</RouteErrorBoundary>
)
}
import { lazy } from 'react'
import { Navigate } from 'react-router'
import Layout from '@/components/layout/index'
import ModuleBoundary from '@/router/ModuleBoundary'
const Layout = lazy(() => import('@/components/layout/index'))
const BooksRoutes = lazy(() => import('@/modules/books/routes'))
const MembersRoutes = lazy(() => import('@/modules/members/routes'))
const AdvertisementsRoutes = lazy(() => import('@/modules/advertisements/routes'))
......
import { StrictMode } from 'react'
import { StrictMode, Suspense } from 'react'
import { createRoot } from 'react-dom/client'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ConfigProvider } from 'antd'
......@@ -7,6 +7,7 @@ import zhCN from 'antd/locale/zh_CN'
import { RouterProvider } from 'react-router'
import { NuqsAdapter } from 'nuqs/adapters/react-router/v7'
import { router } from '@/router/routes'
import { LoadingFallback } from '@/router/LoadingFallback'
import { shouldRetryQuery } from '@/api/errors'
import { configureDevelopmentRequestAuth } from '@/api/http-auth'
import { configureSessionExpiry, getMemberToken } from '@/modules/auth/session'
......@@ -31,7 +32,9 @@ createRoot(document.getElementById('root')!).render(
<ConfigProvider locale={zhCN} theme={{ token: { colorPrimary: '#aa1941' } }}>
<NuqsAdapter>
<QueryClientProvider client={queryClient}>
<Suspense fallback={<LoadingFallback />}>
<RouterProvider router={router} />
</Suspense>
</QueryClientProvider>
</NuqsAdapter>
</ConfigProvider>
......
import type { RouteObject } from 'react-router'
import { LoginView } from './views/LoginView'
import { lazyRoute } from '@/router/lazy-route'
export const routes: RouteObject[] = [{ path: '/login', element: <LoginView /> }]
export const routes: RouteObject[] = [
{
path: '/login',
lazy: lazyRoute(() => import('./views/LoginView'), 'LoginView'),
},
]
import { render, screen } from '@testing-library/react'
import { Suspense } from 'react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { createMemoryRouter, RouterProvider } from 'react-router'
import { describe, expect, it } from 'vitest'
import { routes } from './routes'
import { appRoutes } from '@/router/routes'
import { LoadingFallback } from '@/router/LoadingFallback'
describe('book routes', () => {
it('renders a standalone book detail page from the route book id', async () => {
const router = createMemoryRouter(routes, { initialEntries: ['/book/100'] })
const router = createMemoryRouter(appRoutes, { initialEntries: ['/book/100'] })
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } })
render(
<QueryClientProvider client={queryClient}>
<Suspense fallback={<LoadingFallback />}>
<RouterProvider router={router} />
</Suspense>
</QueryClientProvider>,
)
......
import type { RouteObject } from 'react-router'
import { BookDetailView } from './views/BookDetailView'
import { lazyRoute } from '@/router/lazy-route'
export const routes: RouteObject[] = [{ path: 'book/:bookId', element: <BookDetailView /> }]
export const routes: RouteObject[] = [
{
path: 'book/:bookId',
lazy: lazyRoute(() => import('./views/BookDetailView'), 'BookDetailView'),
},
]
import type { RouteObject } from 'react-router'
import { LibraryView } from './views/LibraryView'
import { lazyRoute } from '@/router/lazy-route'
export const routes: RouteObject[] = [{ path: 'library', element: <LibraryView /> }]
export const routes: RouteObject[] = [
{
path: 'library',
lazy: lazyRoute(() => import('./views/LibraryView'), 'LibraryView'),
},
]
import type { RouteObject } from 'react-router'
import { MyCouponsView } from './views/MyCouponsView'
import { lazyRoute } from '@/router/lazy-route'
export const routes: RouteObject[] = [{ path: 'coupons', element: <MyCouponsView /> }]
export const routes: RouteObject[] = [
{
path: 'coupons',
lazy: lazyRoute(() => import('./views/MyCouponsView'), 'MyCouponsView'),
},
]
import type { RouteObject } from 'react-router'
import { FeedbackView } from './views/FeedbackView'
import { lazyRoute } from '@/router/lazy-route'
export const routes: RouteObject[] = [{ path: 'feedback', element: <FeedbackView /> }]
export const routes: RouteObject[] = [
{
path: 'feedback',
lazy: lazyRoute(() => import('./views/FeedbackView'), 'FeedbackView'),
},
]
import type { RouteObject } from 'react-router'
import { HelpDetailView } from './views/HelpDetailView'
import { HelpListView } from './views/HelpListView'
import { lazyRoute } from '@/router/lazy-route'
export const routes: RouteObject[] = [
{ path: 'help', element: <HelpListView /> },
{ path: 'help/:id', element: <HelpDetailView /> },
{
path: 'help',
lazy: lazyRoute(() => import('./views/HelpListView'), 'HelpListView'),
},
{
path: 'help/:id',
lazy: lazyRoute(() => import('./views/HelpDetailView'), 'HelpDetailView'),
},
]
import type { RouteObject } from 'react-router'
import { MyMessagesView } from './views/MyMessagesView'
import { lazyRoute } from '@/router/lazy-route'
export const routes: RouteObject[] = [{ path: 'messages', element: <MyMessagesView /> }]
export const routes: RouteObject[] = [
{
path: 'messages',
lazy: lazyRoute(() => import('./views/MyMessagesView'), 'MyMessagesView'),
},
]
import type { RouteObject } from 'react-router'
import { OrderDetailView } from './views/OrderDetailView'
import { MyOrdersView } from './views/MyOrdersView'
import { lazyRoute } from '@/router/lazy-route'
export const routes: RouteObject[] = [
{ path: 'orders', element: <MyOrdersView /> },
{ path: 'orders/:orderNumber', element: <OrderDetailView /> },
{
path: 'orders',
lazy: lazyRoute(() => import('./views/MyOrdersView'), 'MyOrdersView'),
},
{
path: 'orders/:orderNumber',
lazy: lazyRoute(() => import('./views/OrderDetailView'), 'OrderDetailView'),
},
]
import type { RouteObject } from 'react-router'
import { ProfileView } from './views/ProfileView'
import { lazyRoute } from '@/router/lazy-route'
export const routes: RouteObject[] = [{ path: 'profile', element: <ProfileView /> }]
export const routes: RouteObject[] = [
{
path: 'profile',
lazy: lazyRoute(() => import('./views/ProfileView'), 'ProfileView'),
},
]
import type { RouteObject } from 'react-router'
import { RequireSession } from '@/modules/auth/RequireSession'
import { MyBooksView } from './books/views/MyBooksView'
import { MyBookshelfView } from './bookshelf/views/MyBookshelfView'
import { lazyRoute } from '@/router/lazy-route'
import { routes as couponRoutes } from './coupons/routes'
import { MyDiscussionsView } from './discussions/views/MyDiscussionsView'
import { routes as feedbackRoutes } from './feedback/routes'
import { MyFavoritesView } from './favorites/views/MyFavoritesView'
import { MyHistoryView } from './history/views/MyHistoryView'
import { MyLayout } from './layout/MyLayout'
import { routes as messageRoutes } from './messages/routes'
import { MyNoteDetailPlaceholderView } from './notes/views/MyNoteDetailPlaceholderView'
import { MyNotesView } from './notes/views/MyNotesView'
import { routes as orderRoutes } from './orders/routes'
import { MyOverviewView } from './overview/views/MyOverviewView'
import { routes as profileRoutes } from './profile/routes'
import { routes as securityRoutes } from './security/routes'
import { routes as walletRoutes } from './wallet/routes'
import { MyWrongQuestionsView } from './wrong-questions/views/MyWrongQuestionsView'
export const routes: RouteObject[] = [
{
......@@ -27,15 +19,48 @@ export const routes: RouteObject[] = [
</RequireSession>
),
children: [
{ index: true, element: <MyOverviewView /> },
{ path: 'books', element: <MyBooksView /> },
{ path: 'bookshelf', element: <MyBookshelfView /> },
{ path: 'history', element: <MyHistoryView /> },
{ path: 'favorites', element: <MyFavoritesView /> },
{ path: 'notes', element: <MyNotesView /> },
{ path: 'notes/:bookId', element: <MyNoteDetailPlaceholderView /> },
{ path: 'discussions', element: <MyDiscussionsView /> },
{ path: 'wrong-questions', element: <MyWrongQuestionsView /> },
{
index: true,
lazy: lazyRoute(() => import('./overview/views/MyOverviewView'), 'MyOverviewView'),
},
{
path: 'books',
lazy: lazyRoute(() => import('./books/views/MyBooksView'), 'MyBooksView'),
},
{
path: 'bookshelf',
lazy: lazyRoute(() => import('./bookshelf/views/MyBookshelfView'), 'MyBookshelfView'),
},
{
path: 'history',
lazy: lazyRoute(() => import('./history/views/MyHistoryView'), 'MyHistoryView'),
},
{
path: 'favorites',
lazy: lazyRoute(() => import('./favorites/views/MyFavoritesView'), 'MyFavoritesView'),
},
{
path: 'notes',
lazy: lazyRoute(() => import('./notes/views/MyNotesView'), 'MyNotesView'),
},
{
path: 'notes/:bookId',
lazy: lazyRoute(
() => import('./notes/views/MyNoteDetailPlaceholderView'),
'MyNoteDetailPlaceholderView',
),
},
{
path: 'discussions',
lazy: lazyRoute(() => import('./discussions/views/MyDiscussionsView'), 'MyDiscussionsView'),
},
{
path: 'wrong-questions',
lazy: lazyRoute(
() => import('./wrong-questions/views/MyWrongQuestionsView'),
'MyWrongQuestionsView',
),
},
...orderRoutes,
...couponRoutes,
...walletRoutes,
......
import type { RouteObject } from 'react-router'
import { ChangePasswordView } from './views/ChangePasswordView'
import { ChangePhoneView } from './views/ChangePhoneView'
import { SecurityView } from './views/SecurityView'
import { lazyRoute } from '@/router/lazy-route'
export const routes: RouteObject[] = [
{ path: 'security', element: <SecurityView /> },
{ path: 'security/password', element: <ChangePasswordView /> },
{ path: 'security/phone', element: <ChangePhoneView /> },
{
path: 'security',
lazy: lazyRoute(() => import('./views/SecurityView'), 'SecurityView'),
},
{
path: 'security/password',
lazy: lazyRoute(() => import('./views/ChangePasswordView'), 'ChangePasswordView'),
},
{
path: 'security/phone',
lazy: lazyRoute(() => import('./views/ChangePhoneView'), 'ChangePhoneView'),
},
]
import type { RouteObject } from 'react-router'
import { MyWalletView } from './views/MyWalletView'
import { lazyRoute } from '@/router/lazy-route'
export const routes: RouteObject[] = [{ path: 'wallet', element: <MyWalletView /> }]
export const routes: RouteObject[] = [
{
path: 'wallet',
lazy: lazyRoute(() => import('./views/MyWalletView'), 'MyWalletView'),
},
]
import type { RouteObject } from 'react-router'
import { Outlet } from 'react-router'
import { RequireSession } from '@/modules/auth/RequireSession'
import { ReaderView } from './views/ReaderView'
import { lazyRoute } from '@/router/lazy-route'
export const routes: RouteObject[] = [
{
path: '/reader/:bookId/:chapterId',
element: (
<RequireSession>
<ReaderView />
<Outlet />
</RequireSession>
),
children: [
{
index: true,
lazy: lazyRoute(() => import('./views/ReaderView'), 'ReaderView'),
},
],
},
]
import type { RouteObject } from 'react-router'
import { SearchView } from './views/SearchView'
import { lazyRoute } from '@/router/lazy-route'
export const routes: RouteObject[] = [{ path: 'search', element: <SearchView /> }]
export const routes: RouteObject[] = [
{
path: 'search',
lazy: lazyRoute(() => import('./views/SearchView'), 'SearchView'),
},
]
export function LoadingFallback() {
return (
<div role='status' aria-live='polite' className='route-loading-fallback'>
页面加载中…
</div>
)
}
import { describe, expect, it } from 'vitest'
import { lazyRoute } from './lazy-route'
describe('lazyRoute', () => {
it('does not import the route module until React Router invokes it', async () => {
let imported = false
const loadRoute = lazyRoute(async () => {
imported = true
return { DemoView: () => null }
}, 'DemoView')
expect(imported).toBe(false)
const routeModule = await loadRoute()
expect(imported).toBe(true)
expect(routeModule.Component).toBeDefined()
})
})
import type { ComponentType } from 'react'
import type { LazyRouteFunction, NonIndexRouteObject } from 'react-router'
type ComponentExport<TModule> = {
[Key in keyof TModule]-?: TModule[Key] extends ComponentType ? Key : never
}[keyof TModule]
export function lazyRoute<TModule>(
importer: () => Promise<TModule>,
exportName: ComponentExport<TModule>,
): LazyRouteFunction<NonIndexRouteObject> {
return async () => {
const module = await importer()
const Component = module[exportName] as ComponentType
if (typeof Component !== 'function') {
throw new Error(`路由模块未导出组件:${String(exportName)}`)
}
return { Component }
}
}
import { render, screen } from '@testing-library/react'
import { Suspense } from 'react'
import { LoadingFallback } from './LoadingFallback'
import { createMemoryRouter, RouterProvider } from 'react-router'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { NuqsTestingAdapter } from 'nuqs/adapters/testing'
......@@ -19,16 +21,33 @@ function renderPath(path: string) {
render(
<NuqsTestingAdapter>
<QueryClientProvider client={queryClient}>
<Suspense fallback={<LoadingFallback />}>
<RouterProvider router={router} />
</Suspense>
</QueryClientProvider>
</NuqsTestingAdapter>,
)
}
function getLeafRoutes(routes: typeof appRoutes): typeof appRoutes {
return routes.flatMap((route) =>
route.children ? getLeafRoutes(route.children as typeof appRoutes) : [route],
)
}
describe('learning application skeleton routes', () => {
beforeEach(() => useSessionStore.getState().setSession(testSession))
afterEach(() => useSessionStore.getState().clearSession())
it('loads business page components through RouteObject.lazy', () => {
const pageRoutes = getLeafRoutes(appRoutes).filter(
(route) => route.path !== '*' && !route.index && !route.element,
)
expect(pageRoutes.length).toBeGreaterThan(20)
expect(pageRoutes.every((route) => typeof route.lazy === 'function')).toBe(true)
})
it.each([
['/library', '清控紫荆数智学堂'],
['/my/books', '我的教材'],
......
import { createBrowserRouter, Navigate, type RouteObject } from 'react-router'
import { RootLayout } from './RootLayout'
import { LoadingFallback } from './LoadingFallback'
import { lazyRoute } from './lazy-route'
import { routes as authRoutes } from '@/modules/auth/routes'
import { routes as bookRoutes } from '@/modules/book/routes'
import { routes as libraryRoutes } from '@/modules/library/routes'
import { routes as myRoutes } from '@/modules/my/routes'
import { routes as readerRoutes } from '@/modules/reader/routes'
import { AboutView } from '@/modules/my/help/views/AboutView'
import { ContactView } from '@/modules/my/help/views/ContactView'
import { routes as helpRoutes } from '@/modules/my/help/routes'
export const appChildRoutes: RouteObject[] = [
{ index: true, element: <Navigate to='/library' replace /> },
{ path: 'about', element: <AboutView /> },
{ path: 'contact', element: <ContactView /> },
{
path: 'about',
lazy: lazyRoute(() => import('@/modules/my/help/views/AboutView'), 'AboutView'),
},
{
path: 'contact',
lazy: lazyRoute(() => import('@/modules/my/help/views/ContactView'), 'ContactView'),
},
...helpRoutes,
...libraryRoutes,
...bookRoutes,
......@@ -20,6 +26,9 @@ export const appChildRoutes: RouteObject[] = [
]
export const appRoutes: RouteObject[] = [
{
HydrateFallback: LoadingFallback,
children: [
...authRoutes,
{
path: '/',
......@@ -28,6 +37,8 @@ export const appRoutes: RouteObject[] = [
},
...readerRoutes,
{ path: '*', element: <Navigate to='/library' replace /> },
],
},
]
export const router = createBrowserRouter(appRoutes)
......@@ -16,6 +16,13 @@ body {
font-family: inherit;
}
.route-loading-fallback {
display: grid;
min-height: 240px;
place-items: center;
color: #667085;
}
:where(a, button, input, textarea, [role='button']):focus-visible {
outline: 3px solid rgb(170 25 65 / 24%);
outline-offset: 2px;
......
......@@ -36,6 +36,7 @@ export default defineConfig(({ mode }) => {
},
test: {
environment: 'jsdom',
fileParallelism: false,
setupFiles: './src/test/setup.ts',
},
}
......
......@@ -30,7 +30,7 @@ PC 学习系统使用现有 APP 会员账号。会员表中的 `members.phone`
2. 原管理系统移动到 `apps/admin`。
3. 新 PC 学习系统放在 `apps/learning`。
4. 管理端和学习端是两个独立应用,不合并路由、状态、权限和构建产物。
5. 两个应用统一使用 Vite 和 pnpm workspace;管理端暂留 React 18,学习端使用 React 19。
5. 两个应用统一使用 Vite 和 pnpm workspace;管理端和学习端当前都使用 React 19。
6. PC 学习端功能范围以完整覆盖现有 APP 线上业务为目标。
7. PC 支付使用微信或支付宝扫码支付,不实现 iOS 内购。
8. PC 学习端复用 `com-ebook-app-api` 的业务能力,但使用浏览器专用路由和鉴权方式。
......@@ -165,9 +165,9 @@ center-book/
- TanStack React Query 管理所有服务端数据、请求生命周期和缓存
- Zustand 只管理无法放入 URL、也不属于服务端数据的少量客户端共享状态
- `nuqs` 管理页面 URL 查询状态
- Ant Design `^6.5.1` 作为学习端标准表单、控制、导航、对话与反馈组件;管理端的 Ant Design 5 保持隔离
- Ant Design 6 作为两个应用各自的标准表单、控制、导航、对话与反馈组件;两个应用保持依赖和源码边界隔离
学习端使用自己的 Ant Design 6 依赖;不得导入或复制管理端的 Ant Design 5 源码。页面设计仍面向阅读和学习场景。
学习端使用自己的 Ant Design 6 依赖;不得导入或复制管理端的 Ant Design 6 源码。页面设计仍面向阅读和学习场景。
学习端根节点使用与 React Router 7 对应的 `NuqsAdapter`。适配器导入路径必须锁定主版本,例如 `nuqs/adapters/react-router/v7`,不使用会随 `nuqs` 主版本改变含义的通用适配器路径。
......
......@@ -9,9 +9,13 @@
"build": "pnpm run build:learning && pnpm run build:admin",
"build:admin": "pnpm --filter @ebook/admin build",
"build:learning": "pnpm --filter @ebook/learning build",
"test": "pnpm --recursive test",
"test": "pnpm run test:learning && pnpm run test:admin",
"lint:admin": "pnpm --filter @ebook/admin lint",
"lint:learning": "pnpm --filter @ebook/learning lint",
"lint:check:admin": "pnpm --filter @ebook/admin exec oxlint .",
"lint:check:learning": "pnpm --filter @ebook/learning exec oxlint .",
"lint:check": "pnpm run lint:check:admin && pnpm run lint:check:learning",
"typecheck:learning": "pnpm --filter @ebook/learning exec tsc -b",
"test:admin": "pnpm --filter @ebook/admin test",
"test:learning": "pnpm --filter @ebook/learning test"
}
......
# React + Vite
# center-book
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
清控紫荆数字教材前端 Monorepo,包含管理端和 PC 学习端两个独立应用。
Currently, two official plugins are available:
## 应用
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
- `apps/admin`:管理端,React 19、JavaScript/JSX、Ant Design 6,构建到 `dist/admin`。
- `apps/learning`:学习端,React 19、TypeScript、React Query、nuqs、Ant Design 6,构建到 `dist/learning`。
## PC:
两个应用拥有独立的路由、状态、API 客户端和构建产物,不直接引用对方的 `src`。
1. 预览时气泡的投影样式加深;
2. 修改默认字号为四号,18px;
3. 去掉选择图片时,查看链接的按钮;
4. 添加题目时,选择题选项正确答案的判断,单选和判断必须选一个,多选需要2个以上;
5. 修改画廊删除第一张图片无法正常删除,图片的标题和描述无法更新的问题,同时字段设置为不必须;
6. 为章头、节头、画廊(不包含行内)、扩展(不包含行内)、交互练习右上角添加关闭按钮,可以直接删除添加的内容。
7. 添加题目时,题干、答案、选项会过滤复制粘贴过来的样式,图片也会去除。
8. 添加交互练习时,可以进行分组,分组签名加上编号;同时题干和编号第一行对齐。
## 常用命令
## APP:
```bash
pnpm install
pnpm dev
pnpm dev:admin
pnpm dev:learning
pnpm test
pnpm lint:check
pnpm typecheck:learning
pnpm build
```
1. 划线和高亮问题修复;
2. 画廊的图片可以点击打开放大,添加图片左右切换;
3. app端答题页也添加分组,同时题干和编号第一行对齐。
4. loading关闭的时机调整;
5. 阅读页样式修改
## 目录结构
````
ZIJING-HTML
├─.husky husky 代码提交规范
├─.vscode 本地编辑器配置
├─dist npm run build 之后打包存放目录
├─formula-editor 公式插件源代码
│
├─node_modules npm 包管下载目录
│
└─src 主要代码存放目录
├─assets 静态资源
│ ├─fonts 字体文件
│ ├─images 图片目录
│ │ ├─editor
│ │ ├─icon
│ │ └─svg
│ └─login 验证码图片
├─common 公共组件
│ ├─dropDownCom 下拉菜单
│ ├─editor-simple 编辑器
│ ├─errorBoundary 错误边界信息
│ ├─icon 图标组件
│ ├─iconfont 图标字体
│ ├─notFound 404页面
│ ├─notPermission 无权限页面
│ ├─Pagination 页码控制
│ ├─TableCom 表格组件
│ ├─wangeditor-customer 编辑器
│ └─websocket websocket hook
├─config 路由组件配置
├─css
├─layout layout
│ ├─components
│ └─css
├─pages 页面
│ ├─advertisement 广告模块
│ │ ├─adsense 广告管理
│ │ └─advert 广告位管理
│ ├─books 书籍基本信息
│ │ ├─audit 书籍审核
│ │ ├─classify 书籍分类
│ │ ├─discussion 讨论互动
│ │ ├─label 书籍标签
│ │ ├─management 写书管理
│ │ ├─order-management 订单管理
│ │ ├─question-bank 题库管理
│ │ ├─sale 书籍审核
│ │ └─section 写书管理 -- 章节
│ ├─jurisdiction 权限管理
│ │ ├─admin 管理员管理
│ │ └─role 权限管理
│ ├─member 用户管理模块
│ │ ├─detail 用户详情
│ │ ├─list 用户列表
│ │ └─userlevel 用户等级
│ ├─setting 系统设置模块
│ │ ├─app-version app版本管理
│ │ ├─applied 应用管理
│ │ ├─coupon 优惠券管理
│ │ ├─feedback 意见反馈
│ │ ├─help 帮助中心
│ │ ├─personalized-setting 个性化设置
│ │ ├─point-management 积分管理
│ │ └─sensitive 敏感词搜索
│ ├─teacher 老师管理模块
│ │ └─query 老师列表
│ └─user-module 用户模块
│ ├─login 登录
│ └─userInfo 个人中心
├─routes 路由
├─store 状态管理
│ └─modules 状态管理各模块
└─utils 工具函数和方法
```
````
管理端在 `/admin/` 部署,学习端在 `/` 部署。后续模块代码请遵循 [AGENTS.md](./AGENTS.md) 中的目录、请求、状态和测试约定。
Markdown 格式
0% 或
您添加了 0 人 到此讨论。请谨慎行事。
请先完成此评论的编辑!
请 注册 或者 后发表评论