@qlover/create-app 0.7.15 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/dist/configs/_common/.gitignore.template +6 -0
- package/dist/configs/_common/.prettierignore +17 -5
- package/dist/configs/_common/.vscode/settings.json +6 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/templates/next-app/.env.template +1 -1
- package/dist/templates/next-app/README.en.md +0 -1
- package/dist/templates/next-app/README.md +0 -1
- package/dist/templates/next-app/config/Identifier/api.ts +5 -5
- package/dist/templates/next-app/config/Identifier/common/admint.table.ts +69 -0
- package/dist/templates/next-app/config/Identifier/common/common.ts +76 -0
- package/dist/templates/next-app/config/Identifier/common/index.ts +3 -0
- package/dist/templates/next-app/config/Identifier/{validator.ts → common/validators.ts} +5 -5
- package/dist/templates/next-app/config/Identifier/index.ts +2 -12
- package/dist/templates/next-app/config/Identifier/pages/index.ts +6 -0
- package/dist/templates/next-app/config/Identifier/pages/page.admin.home.ts +27 -0
- package/dist/templates/next-app/config/Identifier/pages/page.admin.locales.ts +266 -0
- package/dist/templates/next-app/config/Identifier/pages/page.admin.user.ts +293 -0
- package/dist/templates/{react-app/config/Identifier → next-app/config/Identifier/pages}/page.home.ts +15 -22
- package/dist/templates/next-app/config/Identifier/{page.login.ts → pages/page.login.ts} +28 -34
- package/dist/templates/next-app/config/Identifier/{page.register.ts → pages/page.register.ts} +30 -29
- package/dist/templates/next-app/config/adminNavs.ts +19 -0
- package/dist/templates/next-app/config/common.ts +22 -13
- package/dist/templates/next-app/config/i18n/HomeI18n.ts +5 -5
- package/dist/templates/next-app/config/i18n/admin18n.ts +61 -19
- package/dist/templates/next-app/config/i18n/i18nConfig.ts +2 -0
- package/dist/templates/next-app/config/i18n/i18nKeyScheam.ts +36 -0
- package/dist/templates/next-app/config/i18n/loginI18n.ts +22 -22
- package/dist/templates/next-app/config/i18n/register18n.ts +23 -24
- package/dist/templates/next-app/docs/en/index.md +0 -1
- package/dist/templates/next-app/docs/en/project-structure.md +0 -1
- package/dist/templates/next-app/docs/zh/index.md +0 -1
- package/dist/templates/next-app/docs/zh/project-structure.md +0 -1
- package/dist/templates/next-app/make/generateLocales.ts +19 -12
- package/dist/templates/next-app/migrations/schema/LocalesSchema.ts +15 -0
- package/dist/templates/next-app/migrations/sql/1694244000000.sql +11 -0
- package/dist/templates/next-app/package.json +7 -3
- package/dist/templates/next-app/public/locales/en.json +172 -207
- package/dist/templates/next-app/public/locales/zh.json +172 -207
- package/dist/templates/next-app/src/app/[locale]/admin/locales/page.tsx +153 -0
- package/dist/templates/next-app/src/app/[locale]/admin/users/page.tsx +48 -50
- package/dist/templates/next-app/src/app/[locale]/login/LoginForm.tsx +2 -2
- package/dist/templates/next-app/src/app/api/admin/locales/create/route.ts +34 -0
- package/dist/templates/next-app/src/app/api/admin/locales/import/route.ts +40 -0
- package/dist/templates/next-app/src/app/api/admin/locales/route.ts +42 -0
- package/dist/templates/next-app/src/app/api/admin/locales/update/route.ts +32 -0
- package/dist/templates/next-app/src/app/api/locales/json/route.ts +44 -0
- package/dist/templates/next-app/src/base/cases/AdminPageManager.ts +1 -13
- package/dist/templates/next-app/src/base/cases/Datetime.ts +18 -0
- package/dist/templates/next-app/src/base/cases/DialogErrorPlugin.ts +12 -6
- package/dist/templates/next-app/src/base/cases/ResourceState.ts +17 -0
- package/dist/templates/next-app/src/base/cases/TranslateI18nInterface.ts +25 -0
- package/dist/templates/next-app/src/base/cases/ZodColumnBuilder.ts +200 -0
- package/dist/templates/next-app/src/base/port/ZodBuilderInterface.ts +8 -0
- package/dist/templates/next-app/src/base/services/AdminLocalesService.ts +20 -0
- package/dist/templates/next-app/src/base/services/AdminPageEvent.ts +26 -0
- package/dist/templates/next-app/src/base/services/AdminPageScheduler.ts +42 -0
- package/dist/templates/next-app/src/base/services/ResourceService.ts +122 -0
- package/dist/templates/next-app/src/base/services/adminApi/AdminLocalesApi.ts +104 -0
- package/dist/templates/next-app/src/base/services/adminApi/AdminUserApi.ts +38 -5
- package/dist/templates/next-app/src/base/services/appApi/AppApiPlugin.ts +1 -1
- package/dist/templates/next-app/src/i18n/request.ts +30 -1
- package/dist/templates/next-app/src/server/PageParams.ts +2 -10
- package/dist/templates/next-app/src/server/port/DBBridgeInterface.ts +5 -0
- package/dist/templates/next-app/src/server/port/DBTableInterface.ts +2 -0
- package/dist/templates/next-app/src/server/port/LocalesRepositoryInterface.ts +43 -0
- package/dist/templates/next-app/src/server/repositorys/LocalesRepository.ts +197 -0
- package/dist/templates/next-app/src/server/services/ApiLocaleService.ts +122 -0
- package/dist/templates/next-app/src/server/sqlBridges/SupabaseBridge.ts +60 -11
- package/dist/templates/next-app/src/server/validators/ExtendedExecutorError.ts +6 -0
- package/dist/templates/next-app/src/server/validators/LocalesValidator.ts +131 -0
- package/dist/templates/next-app/src/server/validators/LoginValidator.ts +2 -5
- package/dist/templates/next-app/src/server/validators/PaginationValidator.ts +32 -16
- package/dist/templates/next-app/src/styles/css/antd-themes/pagination/_default.css +2 -1
- package/dist/templates/next-app/src/styles/css/antd-themes/pagination/dark.css +28 -29
- package/dist/templates/next-app/src/styles/css/antd-themes/pagination/pink.css +2 -1
- package/dist/templates/next-app/src/uikit/components/AdminLayout.tsx +17 -3
- package/dist/templates/next-app/src/uikit/components/BaseHeader.tsx +5 -4
- package/dist/templates/next-app/src/uikit/components/BaseLayout.tsx +5 -4
- package/dist/templates/next-app/src/uikit/components/BootstrapsProvider.tsx +3 -2
- package/dist/templates/next-app/src/uikit/components/ComboProvider.tsx +1 -1
- package/dist/templates/next-app/src/uikit/components/EditableCell.tsx +118 -0
- package/dist/templates/next-app/src/uikit/components/LogoutButton.tsx +5 -6
- package/dist/templates/next-app/src/uikit/components/ThemeSwitcher.tsx +1 -1
- package/dist/templates/next-app/src/uikit/components/With.tsx +2 -2
- package/dist/templates/next-app/src/uikit/components/localesImportButton/LocalesImportButton.tsx +62 -0
- package/dist/templates/next-app/src/uikit/components/localesImportButton/LocalesImportEvent.ts +28 -0
- package/dist/templates/next-app/src/uikit/components/localesImportButton/import.module.css +6 -0
- package/dist/templates/next-app/src/uikit/hook/useI18nInterface.ts +8 -14
- package/dist/templates/next-app/src/uikit/hook/useWarnTranslations.ts +25 -0
- package/dist/templates/react-app/.prettierignore +17 -0
- package/dist/templates/react-app/README.en.md +71 -54
- package/dist/templates/react-app/README.md +35 -18
- package/dist/templates/react-app/__tests__/__mocks__/BootstrapTest.ts +14 -0
- package/dist/templates/react-app/__tests__/__mocks__/MockAppConfit.ts +1 -1
- package/dist/templates/react-app/__tests__/__mocks__/MockDialogHandler.ts +2 -2
- package/dist/templates/react-app/__tests__/__mocks__/MockLogger.ts +1 -1
- package/dist/templates/react-app/__tests__/__mocks__/components/TestApp.tsx +45 -0
- package/dist/templates/react-app/__tests__/__mocks__/components/TestBootstrapsProvider.tsx +34 -0
- package/dist/templates/react-app/__tests__/__mocks__/components/TestRouter.tsx +46 -0
- package/dist/templates/react-app/__tests__/__mocks__/components/index.ts +12 -0
- package/dist/templates/react-app/__tests__/__mocks__/createMockGlobals.ts +1 -2
- package/dist/templates/react-app/__tests__/__mocks__/testIOC/TestIOC.ts +51 -0
- package/dist/templates/react-app/__tests__/__mocks__/testIOC/TestIOCRegister.ts +69 -0
- package/dist/templates/react-app/__tests__/setup/index.ts +1 -51
- package/dist/templates/react-app/__tests__/setup/setupGlobal.ts +51 -0
- package/dist/templates/react-app/__tests__/src/App.structure.test.tsx +115 -0
- package/dist/templates/react-app/__tests__/src/base/cases/AppConfig.test.ts +2 -2
- package/dist/templates/react-app/__tests__/src/base/cases/AppError.test.ts +1 -1
- package/dist/templates/react-app/__tests__/src/base/cases/DialogHandler.test.ts +3 -5
- package/dist/templates/react-app/__tests__/src/base/cases/I18nKeyErrorPlugin.test.ts +13 -2
- package/dist/templates/react-app/__tests__/src/base/cases/InversifyContainer.test.ts +1 -1
- package/dist/templates/react-app/__tests__/src/base/cases/PublicAssetsPath.test.ts +1 -1
- package/dist/templates/react-app/__tests__/src/base/cases/RequestLogger.test.ts +5 -5
- package/dist/templates/react-app/__tests__/src/base/cases/RequestStatusCatcher.test.ts +1 -2
- package/dist/templates/react-app/__tests__/src/base/cases/RouterLoader.test.ts +25 -15
- package/dist/templates/react-app/__tests__/src/base/services/I18nService.test.ts +29 -15
- package/dist/templates/react-app/__tests__/src/core/IOC.test.ts +19 -9
- package/dist/templates/react-app/__tests__/src/core/bootstraps/BootstrapClient.test.ts +153 -0
- package/dist/templates/react-app/__tests__/src/core/bootstraps/BootstrapsApp.test.ts +9 -7
- package/dist/templates/react-app/__tests__/src/main.integration.test.tsx +4 -5
- package/dist/templates/react-app/__tests__/src/main.test.tsx +4 -4
- package/dist/templates/react-app/__tests__/src/uikit/components/BaseHeader.test.tsx +68 -59
- package/dist/templates/react-app/config/IOCIdentifier.ts +8 -8
- package/dist/templates/react-app/config/Identifier/{common.error.ts → common/common.error.ts} +5 -5
- package/dist/templates/react-app/config/Identifier/{common.ts → common/common.ts} +9 -9
- package/dist/templates/react-app/config/Identifier/common/index.ts +2 -0
- package/dist/templates/react-app/config/Identifier/index.ts +1 -9
- package/dist/templates/react-app/config/Identifier/pages/index.ts +8 -0
- package/dist/templates/react-app/config/Identifier/{page.about.ts → pages/page.about.ts} +34 -26
- package/dist/templates/react-app/config/Identifier/{page.executor.ts → pages/page.executor.ts} +47 -39
- package/dist/templates/{next-app/config/Identifier → react-app/config/Identifier/pages}/page.home.ts +24 -23
- package/dist/templates/react-app/config/Identifier/pages/page.identifiter.ts +102 -0
- package/dist/templates/react-app/config/Identifier/{page.jsonStorage.ts → pages/page.jsonStorage.ts} +18 -11
- package/dist/templates/react-app/config/Identifier/{page.login.ts → pages/page.login.ts} +37 -27
- package/dist/templates/react-app/config/Identifier/{page.register.ts → pages/page.register.ts} +37 -25
- package/dist/templates/react-app/config/Identifier/{page.request.ts → pages/page.request.ts} +34 -44
- package/dist/templates/react-app/config/app.router.ts +66 -69
- package/dist/templates/react-app/config/i18n/PageI18nInterface.ts +51 -0
- package/dist/templates/react-app/config/i18n/aboutI18n.ts +42 -0
- package/dist/templates/react-app/config/i18n/executorI18n.ts +51 -0
- package/dist/templates/react-app/config/i18n/homeI18n.ts +24 -0
- package/dist/templates/react-app/config/i18n/i18nConfig.ts +30 -0
- package/dist/templates/react-app/config/i18n/identifiter18n.ts +30 -0
- package/dist/templates/react-app/config/i18n/jsonStorage18n.ts +27 -0
- package/dist/templates/react-app/config/i18n/login18n.ts +42 -0
- package/dist/templates/react-app/config/i18n/notFoundI18n.ts +34 -0
- package/dist/templates/react-app/config/i18n/register18n.ts +40 -0
- package/dist/templates/react-app/config/i18n/request18n.ts +41 -0
- package/dist/templates/react-app/config/theme.ts +14 -4
- package/dist/templates/react-app/docs/en/bootstrap.md +1670 -341
- package/dist/templates/react-app/docs/en/development-guide.md +1021 -345
- package/dist/templates/react-app/docs/en/env.md +1132 -278
- package/dist/templates/react-app/docs/en/i18n.md +858 -147
- package/dist/templates/react-app/docs/en/index.md +733 -104
- package/dist/templates/react-app/docs/en/ioc.md +1228 -287
- package/dist/templates/react-app/docs/en/playwright/e2e-tests.md +321 -0
- package/dist/templates/react-app/docs/en/playwright/index.md +19 -0
- package/dist/templates/react-app/docs/en/playwright/installation-summary.md +332 -0
- package/dist/templates/react-app/docs/en/playwright/overview.md +222 -0
- package/dist/templates/react-app/docs/en/playwright/quickstart.md +325 -0
- package/dist/templates/react-app/docs/en/playwright/reorganization-notes.md +340 -0
- package/dist/templates/react-app/docs/en/playwright/setup-complete.md +290 -0
- package/dist/templates/react-app/docs/en/playwright/testing-guide.md +565 -0
- package/dist/templates/react-app/docs/en/store.md +1194 -184
- package/dist/templates/react-app/docs/en/why-no-globals.md +797 -0
- package/dist/templates/react-app/docs/zh/bootstrap.md +1670 -341
- package/dist/templates/react-app/docs/zh/development-guide.md +1021 -345
- package/dist/templates/react-app/docs/zh/env.md +1132 -275
- package/dist/templates/react-app/docs/zh/i18n.md +858 -147
- package/dist/templates/react-app/docs/zh/index.md +717 -104
- package/dist/templates/react-app/docs/zh/ioc.md +1229 -287
- package/dist/templates/react-app/docs/zh/playwright/e2e-tests.md +321 -0
- package/dist/templates/react-app/docs/zh/playwright/index.md +19 -0
- package/dist/templates/react-app/docs/zh/playwright/installation-summary.md +332 -0
- package/dist/templates/react-app/docs/zh/playwright/overview.md +222 -0
- package/dist/templates/react-app/docs/zh/playwright/quickstart.md +325 -0
- package/dist/templates/react-app/docs/zh/playwright/reorganization-notes.md +340 -0
- package/dist/templates/react-app/docs/zh/playwright/setup-complete.md +290 -0
- package/dist/templates/react-app/docs/zh/playwright/testing-guide.md +565 -0
- package/dist/templates/react-app/docs/zh/store.md +1192 -184
- package/dist/templates/react-app/docs/zh/why-no-globals.md +797 -0
- package/dist/templates/react-app/e2e/App.spec.ts +319 -0
- package/dist/templates/react-app/e2e/fixtures/base.fixture.ts +40 -0
- package/dist/templates/react-app/e2e/main.spec.ts +20 -0
- package/dist/templates/react-app/e2e/utils/test-helpers.ts +19 -0
- package/dist/templates/react-app/eslint.config.mjs +247 -0
- package/dist/templates/react-app/makes/eslint-utils.mjs +195 -0
- package/dist/templates/react-app/makes/generateTs2LocalesOptions.ts +26 -0
- package/dist/templates/react-app/package.json +31 -3
- package/dist/templates/react-app/playwright.config.ts +79 -0
- package/dist/templates/react-app/public/locales/en/common.json +190 -179
- package/dist/templates/react-app/public/locales/zh/common.json +190 -179
- package/dist/templates/react-app/src/App.tsx +15 -42
- package/dist/templates/react-app/src/base/apis/AiApi.ts +5 -5
- package/dist/templates/react-app/src/base/apis/feApi/FeApi.ts +1 -1
- package/dist/templates/react-app/src/base/apis/feApi/FeApiAdapter.ts +1 -1
- package/dist/templates/react-app/src/base/apis/feApi/FeApiBootstarp.ts +8 -8
- package/dist/templates/react-app/src/base/apis/feApi/FeApiType.ts +1 -1
- package/dist/templates/react-app/src/base/apis/userApi/UserApi.ts +6 -6
- package/dist/templates/react-app/src/base/apis/userApi/UserApiAdapter.ts +1 -1
- package/dist/templates/react-app/src/base/apis/userApi/UserApiBootstarp.ts +12 -14
- package/dist/templates/react-app/src/base/apis/userApi/UserApiType.ts +1 -1
- package/dist/templates/react-app/src/base/cases/DialogHandler.ts +5 -2
- package/dist/templates/react-app/src/base/cases/I18nKeyErrorPlugin.ts +3 -3
- package/dist/templates/react-app/src/base/cases/InversifyContainer.ts +3 -3
- package/dist/templates/react-app/src/base/cases/RequestLanguages.ts +2 -2
- package/dist/templates/react-app/src/base/cases/RequestLogger.ts +4 -4
- package/dist/templates/react-app/src/base/cases/RequestStatusCatcher.ts +1 -1
- package/dist/templates/react-app/src/base/cases/ResourceState.ts +23 -0
- package/dist/templates/react-app/src/base/cases/RouterLoader.ts +4 -4
- package/dist/templates/react-app/src/base/cases/TranslateI18nInterface.ts +26 -0
- package/dist/templates/react-app/src/base/port/ExecutorPageBridgeInterface.ts +2 -3
- package/dist/templates/react-app/src/base/port/I18nServiceInterface.ts +1 -1
- package/dist/templates/react-app/src/base/port/IOCInterface.ts +36 -0
- package/dist/templates/react-app/src/base/port/JSONStoragePageBridgeInterface.ts +2 -1
- package/dist/templates/react-app/src/base/port/ProcesserExecutorInterface.ts +1 -1
- package/dist/templates/react-app/src/base/port/RequestPageBridgeInterface.ts +2 -2
- package/dist/templates/react-app/src/base/port/RouteServiceInterface.ts +9 -5
- package/dist/templates/react-app/src/base/port/UserServiceInterface.ts +1 -1
- package/dist/templates/react-app/src/base/services/I18nService.ts +29 -29
- package/dist/templates/react-app/src/base/services/IdentifierService.ts +143 -0
- package/dist/templates/react-app/src/base/services/ProcesserExecutor.ts +3 -3
- package/dist/templates/react-app/src/base/services/RouteService.ts +27 -8
- package/dist/templates/react-app/src/base/services/UserService.ts +8 -8
- package/dist/templates/react-app/src/base/types/Page.ts +14 -2
- package/dist/templates/react-app/src/base/types/global.d.ts +1 -1
- package/dist/templates/react-app/src/core/IOC.ts +5 -46
- package/dist/templates/react-app/src/core/bootstraps/{BootstrapApp.ts → BootstrapClient.ts} +44 -17
- package/dist/templates/react-app/src/core/bootstraps/BootstrapsRegistry.ts +14 -7
- package/dist/templates/react-app/src/core/bootstraps/IocIdentifierTest.ts +1 -1
- package/dist/templates/react-app/src/core/bootstraps/PrintBootstrap.ts +1 -1
- package/dist/templates/react-app/src/core/clientIoc/ClientIOC.ts +40 -0
- package/dist/templates/react-app/src/core/{IocRegisterImpl.ts → clientIoc/ClientIOCRegister.ts} +35 -24
- package/dist/templates/react-app/src/core/globals.ts +9 -9
- package/dist/templates/react-app/src/main.tsx +4 -4
- package/dist/templates/react-app/src/pages/404.tsx +6 -3
- package/dist/templates/react-app/src/pages/500.tsx +5 -2
- package/dist/templates/react-app/src/pages/NoRouteFound.tsx +5 -0
- package/dist/templates/react-app/src/pages/auth/Layout.tsx +9 -6
- package/dist/templates/react-app/src/pages/auth/LoginPage.tsx +46 -56
- package/dist/templates/react-app/src/pages/auth/RegisterPage.tsx +46 -58
- package/dist/templates/react-app/src/pages/base/AboutPage.tsx +35 -40
- package/dist/templates/react-app/src/pages/base/ExecutorPage.tsx +51 -51
- package/dist/templates/react-app/src/pages/base/HomePage.tsx +14 -15
- package/dist/templates/react-app/src/pages/base/IdentifierPage.tsx +70 -11
- package/dist/templates/react-app/src/pages/base/JSONStoragePage.tsx +24 -25
- package/dist/templates/react-app/src/pages/base/Layout.tsx +2 -2
- package/dist/templates/react-app/src/pages/base/RedirectPathname.tsx +3 -2
- package/dist/templates/react-app/src/pages/base/RequestPage.tsx +41 -59
- package/dist/templates/react-app/src/styles/css/antd-themes/{_default.css → _common/_default.css} +85 -0
- package/dist/templates/react-app/src/styles/css/antd-themes/{dark.css → _common/dark.css} +99 -0
- package/dist/templates/react-app/src/styles/css/antd-themes/_common/index.css +3 -0
- package/dist/templates/react-app/src/styles/css/antd-themes/{pink.css → _common/pink.css} +86 -0
- package/dist/templates/react-app/src/styles/css/antd-themes/index.css +4 -3
- package/dist/templates/react-app/src/styles/css/antd-themes/menu/_default.css +108 -0
- package/dist/templates/react-app/src/styles/css/antd-themes/menu/dark.css +67 -0
- package/dist/templates/react-app/src/styles/css/antd-themes/menu/index.css +3 -0
- package/dist/templates/react-app/src/styles/css/antd-themes/menu/pink.css +67 -0
- package/dist/templates/react-app/src/styles/css/antd-themes/pagination/_default.css +34 -0
- package/dist/templates/react-app/src/styles/css/antd-themes/pagination/dark.css +31 -0
- package/dist/templates/react-app/src/styles/css/antd-themes/pagination/index.css +3 -0
- package/dist/templates/react-app/src/styles/css/antd-themes/pagination/pink.css +36 -0
- package/dist/templates/react-app/src/styles/css/antd-themes/table/_default.css +44 -0
- package/dist/templates/react-app/src/styles/css/antd-themes/table/dark.css +43 -0
- package/dist/templates/react-app/src/styles/css/antd-themes/table/index.css +3 -0
- package/dist/templates/react-app/src/styles/css/antd-themes/table/pink.css +43 -0
- package/dist/templates/react-app/src/styles/css/page.css +4 -3
- package/dist/templates/react-app/src/styles/css/themes/_default.css +1 -0
- package/dist/templates/react-app/src/styles/css/themes/dark.css +1 -0
- package/dist/templates/react-app/src/styles/css/themes/pink.css +1 -0
- package/dist/templates/react-app/src/styles/css/zIndex.css +1 -1
- package/dist/templates/react-app/src/uikit/bridges/ExecutorPageBridge.ts +3 -3
- package/dist/templates/react-app/src/uikit/bridges/JSONStoragePageBridge.ts +2 -2
- package/dist/templates/react-app/src/uikit/bridges/NavigateBridge.ts +1 -1
- package/dist/templates/react-app/src/uikit/bridges/RequestPageBridge.ts +3 -3
- package/dist/templates/react-app/src/uikit/components/AppRouterProvider.tsx +35 -0
- package/dist/templates/react-app/src/uikit/components/BaseHeader.tsx +15 -11
- package/dist/templates/react-app/src/uikit/components/BaseRouteProvider.tsx +14 -11
- package/dist/templates/react-app/src/uikit/components/BaseRouteSeo.tsx +18 -0
- package/dist/templates/react-app/src/uikit/components/BootstrapsProvider.tsx +13 -0
- package/dist/templates/react-app/src/uikit/components/ClientSeo.tsx +62 -0
- package/dist/templates/react-app/src/uikit/components/ComboProvider.tsx +38 -0
- package/dist/templates/react-app/src/uikit/components/LanguageSwitcher.tsx +48 -27
- package/dist/templates/react-app/src/uikit/components/Loading.tsx +4 -2
- package/dist/templates/react-app/src/uikit/components/LocaleLink.tsx +4 -5
- package/dist/templates/react-app/src/uikit/components/LogoutButton.tsx +34 -11
- package/dist/templates/react-app/src/uikit/components/ProcessExecutorProvider.tsx +9 -5
- package/dist/templates/react-app/src/uikit/components/RouterRenderComponent.tsx +6 -3
- package/dist/templates/react-app/src/uikit/components/ThemeSwitcher.tsx +97 -40
- package/dist/templates/react-app/src/uikit/components/UserAuthProvider.tsx +5 -5
- package/dist/templates/react-app/src/uikit/components/With.tsx +17 -0
- package/dist/templates/react-app/src/uikit/contexts/BaseRouteContext.ts +17 -11
- package/dist/templates/react-app/src/uikit/contexts/IOCContext.ts +13 -0
- package/dist/templates/react-app/src/uikit/hooks/useAppTranslation.ts +26 -0
- package/dist/templates/react-app/src/uikit/hooks/useI18nGuard.ts +8 -11
- package/dist/templates/react-app/src/uikit/hooks/useI18nInterface.ts +25 -0
- package/dist/templates/react-app/src/uikit/hooks/useIOC.ts +35 -0
- package/dist/templates/react-app/src/uikit/hooks/useNavigateBridge.ts +3 -3
- package/dist/templates/react-app/src/uikit/hooks/useStrictEffect.ts +0 -1
- package/dist/templates/react-app/tsconfig.e2e.json +21 -0
- package/dist/templates/react-app/tsconfig.json +8 -1
- package/dist/templates/react-app/tsconfig.node.json +1 -1
- package/dist/templates/react-app/tsconfig.test.json +3 -1
- package/dist/templates/react-app/vite.config.ts +50 -34
- package/package.json +2 -1
- package/dist/configs/react-app/eslint.config.js +0 -94
- package/dist/templates/next-app/config/Identifier/common.error.ts +0 -41
- package/dist/templates/next-app/config/Identifier/common.ts +0 -69
- package/dist/templates/next-app/config/Identifier/page.about.ts +0 -181
- package/dist/templates/next-app/config/Identifier/page.admin.ts +0 -48
- package/dist/templates/next-app/config/Identifier/page.executor.ts +0 -272
- package/dist/templates/next-app/config/Identifier/page.identifiter.ts +0 -39
- package/dist/templates/next-app/config/Identifier/page.jsonStorage.ts +0 -72
- package/dist/templates/next-app/config/Identifier/page.request.ts +0 -182
- package/dist/templates/next-app/src/base/cases/ChatAction.ts +0 -21
- package/dist/templates/next-app/src/base/cases/FocusBarAction.ts +0 -36
- package/dist/templates/next-app/src/base/cases/RequestState.ts +0 -20
- package/dist/templates/next-app/src/base/port/AdminPageInterface.ts +0 -85
- package/dist/templates/next-app/src/base/port/AsyncStateInterface.ts +0 -7
- package/dist/templates/next-app/src/base/services/AdminUserService.ts +0 -45
- package/dist/templates/next-app/src/uikit/components/ChatRoot.tsx +0 -17
- package/dist/templates/next-app/src/uikit/components/chat/ChatActionInterface.ts +0 -30
- package/dist/templates/next-app/src/uikit/components/chat/ChatFocusBar.tsx +0 -65
- package/dist/templates/next-app/src/uikit/components/chat/ChatMessages.tsx +0 -59
- package/dist/templates/next-app/src/uikit/components/chat/ChatWrap.tsx +0 -28
- package/dist/templates/next-app/src/uikit/components/chat/FocusBarActionInterface.ts +0 -19
- package/dist/templates/next-app/src/uikit/hook/useMountedClient.ts +0 -17
- package/dist/templates/next-app/src/uikit/hook/useStore.ts +0 -15
- package/dist/templates/react-app/__tests__/__mocks__/I18nService.ts +0 -13
- package/dist/templates/react-app/__tests__/src/App.test.tsx +0 -139
- package/dist/templates/react-app/config/Identifier/page.identifiter.ts +0 -39
- package/dist/templates/react-app/config/i18n.ts +0 -15
- package/dist/templates/react-app/docs/en/project-structure.md +0 -434
- package/dist/templates/react-app/docs/zh/project-structure.md +0 -434
- package/dist/templates/react-app/src/base/cases/RequestState.ts +0 -20
- package/dist/templates/react-app/src/base/port/AsyncStateInterface.ts +0 -7
- package/dist/templates/react-app/src/uikit/hooks/useDocumentTitle.ts +0 -15
- package/dist/templates/react-app/src/uikit/hooks/useStore.ts +0 -15
|
@@ -1,274 +1,431 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 开发指南
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> **📖 本文档提供完整的页面开发流程和实战指南,帮助你快速上手项目开发。**
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
2. [代码风格规范](#代码风格规范)
|
|
7
|
-
3. [组件开发规范](#组件开发规范)
|
|
8
|
-
4. [状态管理规范](#状态管理规范)
|
|
9
|
-
5. [路由开发规范](#路由开发规范)
|
|
10
|
-
6. [国际化开发规范](#国际化开发规范)
|
|
11
|
-
7. [主题样式规范](#主题样式规范)
|
|
12
|
-
8. [测试规范](#测试规范)
|
|
13
|
-
9. [文档规范](#文档规范)
|
|
5
|
+
## 📋 目录
|
|
14
6
|
|
|
15
|
-
|
|
7
|
+
- [开发一个页面需要什么](#-开发一个页面需要什么)
|
|
8
|
+
- [完整开发流程](#-完整开发流程)
|
|
9
|
+
- [实战示例:用户列表页](#-实战示例用户列表页)
|
|
10
|
+
- [常见场景](#-常见场景)
|
|
11
|
+
- [代码规范](#-代码规范)
|
|
12
|
+
- [开发工具](#-开发工具)
|
|
16
13
|
|
|
17
|
-
|
|
14
|
+
---
|
|
18
15
|
|
|
19
|
-
|
|
16
|
+
## 🎯 开发一个页面需要什么
|
|
17
|
+
|
|
18
|
+
### 核心清单
|
|
19
|
+
|
|
20
|
+
一个完整的页面通常需要以下组成部分:
|
|
20
21
|
|
|
21
22
|
```
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
│ └── IOC.ts # IOC 容器
|
|
31
|
-
├── pages/ # 页面组件
|
|
32
|
-
│ ├── auth/ # 认证相关页面
|
|
33
|
-
│ └── base/ # 基础页面
|
|
34
|
-
├── styles/ # 样式文件
|
|
35
|
-
│ └── css/
|
|
36
|
-
│ ├── themes/ # 主题相关
|
|
37
|
-
│ └── antd-themes/ # Ant Design 主题
|
|
38
|
-
├── uikit/ # UI 组件库
|
|
39
|
-
│ ├── components/ # 通用组件
|
|
40
|
-
│ ├── contexts/ # React Context
|
|
41
|
-
│ ├── hooks/ # 自定义 Hooks
|
|
42
|
-
│ └── providers/ # 提供者组件
|
|
43
|
-
└── App.tsx # 应用入口
|
|
23
|
+
✅ 1. 接口定义 (Port) - base/port/XxxServiceInterface.ts
|
|
24
|
+
✅ 2. 服务实现 (Service) - base/services/XxxService.ts
|
|
25
|
+
✅ 3. API 适配器 (可选) - base/apis/xxxApi/XxxApi.ts
|
|
26
|
+
✅ 4. 路由配置 - config/app.router.ts
|
|
27
|
+
✅ 5. i18n 文本定义 - config/Identifier/pages/page.xxx.ts
|
|
28
|
+
✅ 6. 页面组件 - pages/xxx/XxxPage.tsx
|
|
29
|
+
✅ 7. IOC 注册 (如果是新服务) - core/clientIoc/ClientIOCRegister.ts
|
|
30
|
+
✅ 8. 测试文件 - __tests__/src/pages/xxx/XxxPage.test.tsx
|
|
44
31
|
```
|
|
45
32
|
|
|
46
|
-
###
|
|
33
|
+
### 依赖关系图
|
|
47
34
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
35
|
+
```
|
|
36
|
+
┌─────────────────────────────────────────┐
|
|
37
|
+
│ 路由配置 (app.router.ts) │
|
|
38
|
+
│ 定义页面路径和元数据 │
|
|
39
|
+
└──────────────┬──────────────────────────┘
|
|
40
|
+
↓
|
|
41
|
+
┌─────────────────────────────────────────┐
|
|
42
|
+
│ 页面组件 (XxxPage.tsx) │
|
|
43
|
+
│ - 使用 useIOC 获取服务 │
|
|
44
|
+
│ - 使用 useStore 订阅状态 │
|
|
45
|
+
│ - 使用 useAppTranslation 获取翻译 │
|
|
46
|
+
│ - 处理 UI 渲染和用户交互 │
|
|
47
|
+
└──────────────┬──────────────────────────┘
|
|
48
|
+
↓
|
|
49
|
+
┌─────────────────────────────────────────┐
|
|
50
|
+
│ 服务层 (XxxService.ts) │
|
|
51
|
+
│ - 实现业务逻辑 │
|
|
52
|
+
│ - 继承 StoreInterface │
|
|
53
|
+
│ - 依赖注入其他服务 │
|
|
54
|
+
└──────────────┬──────────────────────────┘
|
|
55
|
+
↓
|
|
56
|
+
┌─────────────────────────────────────────┐
|
|
57
|
+
│ 接口定义 (XxxServiceInterface.ts) │
|
|
58
|
+
│ - 定义服务契约 │
|
|
59
|
+
│ - 便于测试和 mock │
|
|
60
|
+
└─────────────────────────────────────────┘
|
|
61
|
+
↓
|
|
62
|
+
┌─────────────────────────────────────────┐
|
|
63
|
+
│ API 适配器 (XxxApi.ts) │
|
|
64
|
+
│ - 封装 HTTP 请求 │
|
|
65
|
+
│ - 转换数据格式 │
|
|
66
|
+
└─────────────────────────────────────────┘
|
|
67
|
+
↓
|
|
68
|
+
┌─────────────────────────────────────────┐
|
|
69
|
+
│ i18n 文本 (page.xxx.ts) │
|
|
70
|
+
│ - 定义页面所有文本的 Key │
|
|
71
|
+
│ - 自动生成翻译文件 │
|
|
72
|
+
└─────────────────────────────────────────┘
|
|
73
|
+
```
|
|
53
74
|
|
|
54
|
-
|
|
55
|
-
- 全小写,使用连字符分隔(如:`user-profile/`)
|
|
56
|
-
- 功能模块使用单数形式(如:`auth/`,而不是 `auths/`)
|
|
75
|
+
---
|
|
57
76
|
|
|
58
|
-
##
|
|
77
|
+
## 🚀 完整开发流程
|
|
59
78
|
|
|
60
|
-
|
|
79
|
+
### 流程图
|
|
61
80
|
|
|
62
|
-
|
|
81
|
+
```
|
|
82
|
+
📝 1. 需求分析
|
|
83
|
+
├── 确定页面功能
|
|
84
|
+
├── 确定数据来源(API)
|
|
85
|
+
└── 确定交互逻辑
|
|
86
|
+
↓
|
|
87
|
+
🎨 2. 定义 i18n Key
|
|
88
|
+
├── 页面标题、按钮文本等
|
|
89
|
+
└── 错误提示、成功提示等
|
|
90
|
+
↓
|
|
91
|
+
🔌 3. 定义接口 (Port)
|
|
92
|
+
├── 服务接口
|
|
93
|
+
└── 数据类型
|
|
94
|
+
↓
|
|
95
|
+
⚙️ 4. 实现服务 (Service)
|
|
96
|
+
├── 继承 StoreInterface
|
|
97
|
+
├── 实现业务逻辑
|
|
98
|
+
└── 依赖注入
|
|
99
|
+
↓
|
|
100
|
+
🌐 5. 实现 API 适配器(如果需要)
|
|
101
|
+
├── 封装 HTTP 请求
|
|
102
|
+
└── 数据转换
|
|
103
|
+
↓
|
|
104
|
+
🗺️ 6. 配置路由
|
|
105
|
+
├── 添加路由配置
|
|
106
|
+
└── 设置元数据
|
|
107
|
+
↓
|
|
108
|
+
🎭 7. 实现页面组件
|
|
109
|
+
├── 使用 useIOC 获取服务
|
|
110
|
+
├── 使用 useStore 订阅状态
|
|
111
|
+
└── 实现 UI 渲染
|
|
112
|
+
↓
|
|
113
|
+
🔗 8. 注册到 IOC(如果是新服务)
|
|
114
|
+
└── 在 ClientIOCRegister 中注册
|
|
115
|
+
↓
|
|
116
|
+
🧪 9. 编写测试
|
|
117
|
+
├── 服务测试(逻辑)
|
|
118
|
+
├── UI 测试(渲染)
|
|
119
|
+
└── 集成测试(流程)
|
|
120
|
+
↓
|
|
121
|
+
✅ 10. 自测和提交
|
|
122
|
+
├── 功能自测
|
|
123
|
+
├── 代码检查
|
|
124
|
+
└── 提交 PR
|
|
125
|
+
```
|
|
63
126
|
|
|
64
|
-
|
|
65
|
-
// 使用 interface 定义对象类型
|
|
66
|
-
interface UserProfile {
|
|
67
|
-
id: string;
|
|
68
|
-
name: string;
|
|
69
|
-
age?: number; // 可选属性使用 ?
|
|
70
|
-
}
|
|
127
|
+
---
|
|
71
128
|
|
|
72
|
-
|
|
73
|
-
type Theme = 'light' | 'dark' | 'pink';
|
|
74
|
-
type Nullable<T> = T | null;
|
|
129
|
+
## 📚 实战示例:用户列表页
|
|
75
130
|
|
|
76
|
-
|
|
77
|
-
enum UserRole {
|
|
78
|
-
ADMIN = 'ADMIN',
|
|
79
|
-
USER = 'USER',
|
|
80
|
-
GUEST = 'GUEST'
|
|
81
|
-
}
|
|
131
|
+
假设我们要开发一个用户列表页面,功能包括:
|
|
82
132
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
133
|
+
- 显示用户列表
|
|
134
|
+
- 搜索用户
|
|
135
|
+
- 分页加载
|
|
136
|
+
- 查看用户详情
|
|
87
137
|
|
|
88
|
-
|
|
89
|
-
interface Repository<TEntity> {
|
|
90
|
-
find(id: string): Promise<TEntity>;
|
|
91
|
-
}
|
|
92
|
-
```
|
|
138
|
+
### 1. 需求分析
|
|
93
139
|
|
|
94
|
-
|
|
140
|
+
**功能清单:**
|
|
95
141
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}
|
|
142
|
+
- 📄 显示用户列表(头像、姓名、邮箱、角色)
|
|
143
|
+
- 🔍 搜索用户(按姓名搜索)
|
|
144
|
+
- 📃 分页(每页 10 条)
|
|
145
|
+
- 👁️ 查看详情(点击跳转详情页)
|
|
146
|
+
- 🔄 刷新列表
|
|
102
147
|
|
|
103
|
-
|
|
104
|
-
return (
|
|
105
|
-
<div>
|
|
106
|
-
<h3>{name}</h3>
|
|
107
|
-
<p>{age}</p>
|
|
108
|
-
</div>
|
|
109
|
-
);
|
|
110
|
-
};
|
|
148
|
+
**数据来源:**
|
|
111
149
|
|
|
112
|
-
|
|
113
|
-
const useUser = (userId: string) => {
|
|
114
|
-
const [user, setUser] = useState<UserProfile | null>(null);
|
|
115
|
-
const [loading, setLoading] = useState(false);
|
|
150
|
+
- API: `GET /api/users?page=1&pageSize=10&keyword=xxx`
|
|
116
151
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
152
|
+
### 2. 定义 i18n Key
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
// config/Identifier/pages/page.users.ts
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* @description User list page title
|
|
159
|
+
* @localZh 用户列表
|
|
160
|
+
* @localEn User List
|
|
161
|
+
*/
|
|
162
|
+
export const PAGE_USERS_TITLE = 'page.users.title';
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* @description Search user placeholder
|
|
166
|
+
* @localZh 搜索用户姓名
|
|
167
|
+
* @localEn Search user name
|
|
168
|
+
*/
|
|
169
|
+
export const PAGE_USERS_SEARCH_PLACEHOLDER = 'page.users.search.placeholder';
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* @description View user detail button
|
|
173
|
+
* @localZh 查看详情
|
|
174
|
+
* @localEn View Detail
|
|
175
|
+
*/
|
|
176
|
+
export const PAGE_USERS_VIEW_DETAIL = 'page.users.viewDetail';
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* @description Refresh button
|
|
180
|
+
* @localZh 刷新
|
|
181
|
+
* @localEn Refresh
|
|
182
|
+
*/
|
|
183
|
+
export const PAGE_USERS_REFRESH = 'page.users.refresh';
|
|
120
184
|
|
|
121
|
-
|
|
122
|
-
|
|
185
|
+
/**
|
|
186
|
+
* @description Loading message
|
|
187
|
+
* @localZh 加载中...
|
|
188
|
+
* @localEn Loading...
|
|
189
|
+
*/
|
|
190
|
+
export const PAGE_USERS_LOADING = 'page.users.loading';
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* @description Empty message
|
|
194
|
+
* @localZh 暂无用户数据
|
|
195
|
+
* @localEn No users found
|
|
196
|
+
*/
|
|
197
|
+
export const PAGE_USERS_EMPTY = 'page.users.empty';
|
|
123
198
|
```
|
|
124
199
|
|
|
125
|
-
|
|
200
|
+
### 3. 定义接口和类型
|
|
126
201
|
|
|
127
|
-
|
|
202
|
+
```typescript
|
|
203
|
+
// base/port/UserServiceInterface.ts
|
|
128
204
|
|
|
129
|
-
|
|
205
|
+
import { StoreInterface } from '@qlover/corekit-bridge';
|
|
130
206
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
207
|
+
/**
|
|
208
|
+
* 用户信息
|
|
209
|
+
*/
|
|
210
|
+
export interface UserInfo {
|
|
211
|
+
id: string;
|
|
212
|
+
name: string;
|
|
213
|
+
email: string;
|
|
214
|
+
avatar: string;
|
|
215
|
+
role: 'admin' | 'user';
|
|
216
|
+
}
|
|
135
217
|
|
|
136
|
-
|
|
218
|
+
/**
|
|
219
|
+
* 用户列表查询参数
|
|
220
|
+
*/
|
|
221
|
+
export interface UserListParams {
|
|
222
|
+
page: number;
|
|
223
|
+
pageSize: number;
|
|
224
|
+
keyword?: string;
|
|
225
|
+
}
|
|
137
226
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
227
|
+
/**
|
|
228
|
+
* 用户列表响应
|
|
229
|
+
*/
|
|
230
|
+
export interface UserListResponse {
|
|
231
|
+
list: UserInfo[];
|
|
232
|
+
total: number;
|
|
233
|
+
page: number;
|
|
234
|
+
pageSize: number;
|
|
235
|
+
}
|
|
144
236
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
237
|
+
/**
|
|
238
|
+
* 用户服务状态
|
|
239
|
+
*/
|
|
240
|
+
export interface UserServiceState extends StoreStateInterface {
|
|
241
|
+
users: UserInfo[];
|
|
242
|
+
total: number;
|
|
243
|
+
page: number;
|
|
244
|
+
pageSize: number;
|
|
245
|
+
loading: boolean;
|
|
246
|
+
error: Error | null;
|
|
149
247
|
}
|
|
150
248
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
249
|
+
/**
|
|
250
|
+
* 用户服务接口
|
|
251
|
+
*/
|
|
252
|
+
export abstract class UserServiceInterface extends StoreInterface<UserServiceState> {
|
|
253
|
+
/**
|
|
254
|
+
* 获取用户列表
|
|
255
|
+
*/
|
|
256
|
+
abstract fetchUsers(params: UserListParams): Promise<void>;
|
|
156
257
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
258
|
+
/**
|
|
259
|
+
* 搜索用户
|
|
260
|
+
*/
|
|
261
|
+
abstract searchUsers(keyword: string): Promise<void>;
|
|
161
262
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
263
|
+
/**
|
|
264
|
+
* 刷新列表
|
|
265
|
+
*/
|
|
266
|
+
abstract refreshUsers(): Promise<void>;
|
|
166
267
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
268
|
+
/**
|
|
269
|
+
* 选择器
|
|
270
|
+
*/
|
|
271
|
+
abstract selector: {
|
|
272
|
+
users: (state: UserServiceState) => UserInfo[];
|
|
273
|
+
loading: (state: UserServiceState) => boolean;
|
|
274
|
+
total: (state: UserServiceState) => number;
|
|
170
275
|
};
|
|
171
|
-
|
|
172
|
-
// 3.5 返回 JSX
|
|
173
|
-
return (
|
|
174
|
-
<div>
|
|
175
|
-
{renderHeader()}
|
|
176
|
-
<Button onClick={handleUpdate}>{t('common.update')}</Button>
|
|
177
|
-
</div>
|
|
178
|
-
);
|
|
179
|
-
};
|
|
276
|
+
}
|
|
180
277
|
```
|
|
181
278
|
|
|
182
|
-
|
|
279
|
+
### 4. 实现 API 适配器
|
|
183
280
|
|
|
184
|
-
|
|
281
|
+
```typescript
|
|
282
|
+
// base/apis/userApi/UserApi.ts
|
|
185
283
|
|
|
186
|
-
|
|
284
|
+
import { injectable, inject } from 'inversify';
|
|
285
|
+
import { HttpClient } from '@/base/cases/HttpClient';
|
|
286
|
+
import type {
|
|
287
|
+
UserListParams,
|
|
288
|
+
UserListResponse
|
|
289
|
+
} from '@/base/port/UserServiceInterface';
|
|
187
290
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
291
|
+
@injectable()
|
|
292
|
+
export class UserApi {
|
|
293
|
+
constructor(@inject(HttpClient) private http: HttpClient) {}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* 获取用户列表
|
|
297
|
+
*/
|
|
298
|
+
async getUserList(params: UserListParams): Promise<UserListResponse> {
|
|
299
|
+
const response = await this.http.get('/api/users', { params });
|
|
300
|
+
|
|
301
|
+
// 转换后端数据格式
|
|
302
|
+
return {
|
|
303
|
+
list: response.data.items.map((item: any) => ({
|
|
304
|
+
id: item.user_id,
|
|
305
|
+
name: item.user_name,
|
|
306
|
+
email: item.user_email,
|
|
307
|
+
avatar: item.avatar_url,
|
|
308
|
+
role: item.user_role
|
|
309
|
+
})),
|
|
310
|
+
total: response.data.total_count,
|
|
311
|
+
page: response.data.current_page,
|
|
312
|
+
pageSize: response.data.page_size
|
|
313
|
+
};
|
|
314
|
+
}
|
|
194
315
|
}
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
### 5. 实现服务
|
|
319
|
+
|
|
320
|
+
```typescript
|
|
321
|
+
// base/services/UserService.ts
|
|
322
|
+
|
|
323
|
+
import { injectable, inject } from 'inversify';
|
|
324
|
+
import {
|
|
325
|
+
UserServiceInterface,
|
|
326
|
+
UserServiceState
|
|
327
|
+
} from '@/base/port/UserServiceInterface';
|
|
328
|
+
import { UserApi } from '@/base/apis/userApi/UserApi';
|
|
329
|
+
import type { UserListParams } from '@/base/port/UserServiceInterface';
|
|
195
330
|
|
|
196
|
-
// 2. Store 实现
|
|
197
331
|
@injectable()
|
|
198
|
-
export class
|
|
199
|
-
constructor() {
|
|
332
|
+
export class UserService extends UserServiceInterface {
|
|
333
|
+
constructor(@inject(UserApi) private api: UserApi) {
|
|
334
|
+
// 初始化状态
|
|
200
335
|
super(() => ({
|
|
201
|
-
|
|
336
|
+
users: [],
|
|
337
|
+
total: 0,
|
|
338
|
+
page: 1,
|
|
339
|
+
pageSize: 10,
|
|
202
340
|
loading: false,
|
|
203
341
|
error: null
|
|
204
342
|
}));
|
|
205
343
|
}
|
|
206
344
|
|
|
207
|
-
|
|
345
|
+
/**
|
|
346
|
+
* 选择器
|
|
347
|
+
*/
|
|
208
348
|
selector = {
|
|
209
|
-
|
|
210
|
-
loading: (state:
|
|
349
|
+
users: (state: UserServiceState) => state.users,
|
|
350
|
+
loading: (state: UserServiceState) => state.loading,
|
|
351
|
+
total: (state: UserServiceState) => state.total,
|
|
352
|
+
page: (state: UserServiceState) => state.page,
|
|
353
|
+
pageSize: (state: UserServiceState) => state.pageSize
|
|
211
354
|
};
|
|
212
355
|
|
|
213
|
-
|
|
214
|
-
|
|
356
|
+
/**
|
|
357
|
+
* 获取用户列表
|
|
358
|
+
*/
|
|
359
|
+
async fetchUsers(params: UserListParams): Promise<void> {
|
|
215
360
|
try {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
361
|
+
// 1. 设置加载状态
|
|
362
|
+
this.emit({ ...this.state, loading: true, error: null });
|
|
363
|
+
|
|
364
|
+
// 2. 调用 API
|
|
365
|
+
const response = await this.api.getUserList(params);
|
|
366
|
+
|
|
367
|
+
// 3. 更新状态
|
|
368
|
+
this.emit({
|
|
369
|
+
users: response.list,
|
|
370
|
+
total: response.total,
|
|
371
|
+
page: response.page,
|
|
372
|
+
pageSize: response.pageSize,
|
|
373
|
+
loading: false,
|
|
374
|
+
error: null
|
|
375
|
+
});
|
|
219
376
|
} catch (error) {
|
|
377
|
+
// 4. 错误处理
|
|
220
378
|
this.emit({
|
|
221
379
|
...this.state,
|
|
222
|
-
|
|
223
|
-
|
|
380
|
+
loading: false,
|
|
381
|
+
error: error as Error
|
|
224
382
|
});
|
|
225
383
|
}
|
|
226
384
|
}
|
|
227
|
-
}
|
|
228
|
-
```
|
|
229
385
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
386
|
+
/**
|
|
387
|
+
* 搜索用户
|
|
388
|
+
*/
|
|
389
|
+
async searchUsers(keyword: string): Promise<void> {
|
|
390
|
+
await this.fetchUsers({
|
|
391
|
+
page: 1,
|
|
392
|
+
pageSize: this.state.pageSize,
|
|
393
|
+
keyword
|
|
394
|
+
});
|
|
395
|
+
}
|
|
237
396
|
|
|
238
|
-
|
|
397
|
+
/**
|
|
398
|
+
* 刷新列表
|
|
399
|
+
*/
|
|
400
|
+
async refreshUsers(): Promise<void> {
|
|
401
|
+
await this.fetchUsers({
|
|
402
|
+
page: this.state.page,
|
|
403
|
+
pageSize: this.state.pageSize
|
|
404
|
+
});
|
|
405
|
+
}
|
|
239
406
|
}
|
|
240
407
|
```
|
|
241
408
|
|
|
242
|
-
|
|
409
|
+
### 6. 配置路由
|
|
243
410
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
### 1. 基本规范
|
|
247
|
-
|
|
248
|
-
- 路由配置集中管理在 `config/app.router.ts` 中
|
|
249
|
-
- 使用声明式路由配置
|
|
250
|
-
- 路由组件放置在 `pages` 目录下
|
|
251
|
-
- 支持路由级别的代码分割
|
|
252
|
-
- 路由配置包含元数据支持
|
|
411
|
+
```typescript
|
|
412
|
+
// config/app.router.ts
|
|
253
413
|
|
|
254
|
-
|
|
414
|
+
import * as i18nKeys from './Identifier/pages/page.users';
|
|
255
415
|
|
|
256
|
-
```typescript
|
|
257
|
-
// 路由配置示例
|
|
258
416
|
export const baseRoutes: RouteConfigValue[] = [
|
|
259
417
|
{
|
|
260
418
|
path: '/:lng',
|
|
261
419
|
element: 'base/Layout',
|
|
262
|
-
meta: {
|
|
263
|
-
category: 'main'
|
|
264
|
-
},
|
|
265
420
|
children: [
|
|
421
|
+
// ... 其他路由
|
|
266
422
|
{
|
|
267
423
|
path: 'users',
|
|
268
|
-
element: 'users/
|
|
424
|
+
element: 'users/UserListPage',
|
|
269
425
|
meta: {
|
|
270
426
|
title: i18nKeys.PAGE_USERS_TITLE,
|
|
271
|
-
|
|
427
|
+
requiresAuth: true, // 需要登录
|
|
428
|
+
category: 'main'
|
|
272
429
|
}
|
|
273
430
|
}
|
|
274
431
|
]
|
|
@@ -276,248 +433,767 @@ export const baseRoutes: RouteConfigValue[] = [
|
|
|
276
433
|
];
|
|
277
434
|
```
|
|
278
435
|
|
|
279
|
-
|
|
436
|
+
### 7. 实现页面组件
|
|
437
|
+
|
|
438
|
+
```typescript
|
|
439
|
+
// pages/users/UserListPage.tsx
|
|
440
|
+
|
|
441
|
+
import { useEffect, useState } from 'react';
|
|
442
|
+
import { Table, Input, Button, Avatar, Space } from 'antd';
|
|
443
|
+
import { ReloadOutlined, EyeOutlined } from '@ant-design/icons';
|
|
444
|
+
import { useIOC } from '@/uikit/hooks/useIOC';
|
|
445
|
+
import { useStore } from '@brain-toolkit/react-kit/hooks/useStore';
|
|
446
|
+
import { useAppTranslation } from '@/uikit/hooks/useAppTranslation';
|
|
447
|
+
import { IOCIdentifier } from '@config/IOCIdentifier';
|
|
448
|
+
import * as i18nKeys from '@config/Identifier/pages/page.users';
|
|
449
|
+
import type { UserInfo } from '@/base/port/UserServiceInterface';
|
|
450
|
+
|
|
451
|
+
export default function UserListPage() {
|
|
452
|
+
// 1. 获取服务
|
|
453
|
+
const userService = useIOC(IOCIdentifier.UserServiceInterface);
|
|
454
|
+
const routeService = useIOC(IOCIdentifier.RouteServiceInterface);
|
|
455
|
+
const { t } = useAppTranslation();
|
|
456
|
+
|
|
457
|
+
// 2. 订阅状态
|
|
458
|
+
const users = useStore(userService, userService.selector.users);
|
|
459
|
+
const loading = useStore(userService, userService.selector.loading);
|
|
460
|
+
const total = useStore(userService, userService.selector.total);
|
|
461
|
+
const page = useStore(userService, userService.selector.page);
|
|
462
|
+
const pageSize = useStore(userService, userService.selector.pageSize);
|
|
463
|
+
|
|
464
|
+
// 3. 本地状态
|
|
465
|
+
const [keyword, setKeyword] = useState('');
|
|
466
|
+
|
|
467
|
+
// 4. 初始化加载
|
|
468
|
+
useEffect(() => {
|
|
469
|
+
userService.fetchUsers({ page: 1, pageSize: 10 });
|
|
470
|
+
}, []);
|
|
471
|
+
|
|
472
|
+
// 5. 事件处理
|
|
473
|
+
const handleSearch = () => {
|
|
474
|
+
userService.searchUsers(keyword);
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
const handleRefresh = () => {
|
|
478
|
+
userService.refreshUsers();
|
|
479
|
+
};
|
|
280
480
|
|
|
281
|
-
|
|
481
|
+
const handlePageChange = (newPage: number, newPageSize: number) => {
|
|
482
|
+
userService.fetchUsers({ page: newPage, pageSize: newPageSize, keyword });
|
|
483
|
+
};
|
|
282
484
|
|
|
283
|
-
|
|
485
|
+
const handleViewDetail = (userId: string) => {
|
|
486
|
+
routeService.push(`/users/${userId}`);
|
|
487
|
+
};
|
|
284
488
|
|
|
285
|
-
|
|
489
|
+
// 6. 表格列配置
|
|
490
|
+
const columns = [
|
|
491
|
+
{
|
|
492
|
+
title: t(i18nKeys.PAGE_USERS_COLUMN_AVATAR),
|
|
493
|
+
dataIndex: 'avatar',
|
|
494
|
+
key: 'avatar',
|
|
495
|
+
render: (avatar: string) => <Avatar src={avatar} />
|
|
496
|
+
},
|
|
497
|
+
{
|
|
498
|
+
title: t(i18nKeys.PAGE_USERS_COLUMN_NAME),
|
|
499
|
+
dataIndex: 'name',
|
|
500
|
+
key: 'name'
|
|
501
|
+
},
|
|
502
|
+
{
|
|
503
|
+
title: t(i18nKeys.PAGE_USERS_COLUMN_EMAIL),
|
|
504
|
+
dataIndex: 'email',
|
|
505
|
+
key: 'email'
|
|
506
|
+
},
|
|
507
|
+
{
|
|
508
|
+
title: t(i18nKeys.PAGE_USERS_COLUMN_ROLE),
|
|
509
|
+
dataIndex: 'role',
|
|
510
|
+
key: 'role',
|
|
511
|
+
render: (role: string) => t(`common.role.${role}`)
|
|
512
|
+
},
|
|
513
|
+
{
|
|
514
|
+
title: t(i18nKeys.PAGE_USERS_COLUMN_ACTIONS),
|
|
515
|
+
key: 'actions',
|
|
516
|
+
render: (_: any, record: UserInfo) => (
|
|
517
|
+
<Button
|
|
518
|
+
type="link"
|
|
519
|
+
icon={<EyeOutlined />}
|
|
520
|
+
onClick={() => handleViewDetail(record.id)}
|
|
521
|
+
>
|
|
522
|
+
{t(i18nKeys.PAGE_USERS_VIEW_DETAIL)}
|
|
523
|
+
</Button>
|
|
524
|
+
)
|
|
525
|
+
}
|
|
526
|
+
];
|
|
286
527
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
-
|
|
290
|
-
|
|
528
|
+
// 7. 渲染
|
|
529
|
+
return (
|
|
530
|
+
<div className="p-6">
|
|
531
|
+
{/* 页面标题 */}
|
|
532
|
+
<h1 className="text-2xl font-bold mb-4">
|
|
533
|
+
{t(i18nKeys.PAGE_USERS_TITLE)}
|
|
534
|
+
</h1>
|
|
535
|
+
|
|
536
|
+
{/* 搜索栏 */}
|
|
537
|
+
<div className="mb-4 flex gap-2">
|
|
538
|
+
<Input.Search
|
|
539
|
+
placeholder={t(i18nKeys.PAGE_USERS_SEARCH_PLACEHOLDER)}
|
|
540
|
+
value={keyword}
|
|
541
|
+
onChange={(e) => setKeyword(e.target.value)}
|
|
542
|
+
onSearch={handleSearch}
|
|
543
|
+
style={{ width: 300 }}
|
|
544
|
+
/>
|
|
545
|
+
<Button
|
|
546
|
+
icon={<ReloadOutlined />}
|
|
547
|
+
onClick={handleRefresh}
|
|
548
|
+
loading={loading}
|
|
549
|
+
>
|
|
550
|
+
{t(i18nKeys.PAGE_USERS_REFRESH)}
|
|
551
|
+
</Button>
|
|
552
|
+
</div>
|
|
553
|
+
|
|
554
|
+
{/* 用户表格 */}
|
|
555
|
+
<Table
|
|
556
|
+
columns={columns}
|
|
557
|
+
dataSource={users}
|
|
558
|
+
rowKey="id"
|
|
559
|
+
loading={loading}
|
|
560
|
+
pagination={{
|
|
561
|
+
current: page,
|
|
562
|
+
pageSize: pageSize,
|
|
563
|
+
total: total,
|
|
564
|
+
onChange: handlePageChange,
|
|
565
|
+
showSizeChanger: true,
|
|
566
|
+
showTotal: (total) => `${t('common.total')} ${total} ${t('common.items')}`
|
|
567
|
+
}}
|
|
568
|
+
locale={{
|
|
569
|
+
emptyText: t(i18nKeys.PAGE_USERS_EMPTY)
|
|
570
|
+
}}
|
|
571
|
+
/>
|
|
572
|
+
</div>
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
```
|
|
291
576
|
|
|
292
|
-
###
|
|
577
|
+
### 8. 注册到 IOC(如果是新服务)
|
|
293
578
|
|
|
294
579
|
```typescript
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
580
|
+
// core/clientIoc/ClientIOCRegister.ts
|
|
581
|
+
|
|
582
|
+
export class ClientIOCRegister {
|
|
583
|
+
protected registerImplement(ioc: IOCContainerInterface): void {
|
|
584
|
+
// ... 其他服务注册
|
|
585
|
+
|
|
586
|
+
// 注册 UserService
|
|
587
|
+
ioc.bind(IOCIdentifier.UserServiceInterface, ioc.get(UserService));
|
|
588
|
+
}
|
|
589
|
+
}
|
|
301
590
|
```
|
|
302
591
|
|
|
303
|
-
|
|
592
|
+
### 9. 编写测试
|
|
304
593
|
|
|
305
|
-
|
|
594
|
+
```typescript
|
|
595
|
+
// __tests__/src/base/services/UserService.test.ts
|
|
306
596
|
|
|
307
|
-
|
|
597
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
598
|
+
import { UserService } from '@/base/services/UserService';
|
|
308
599
|
|
|
309
|
-
|
|
600
|
+
describe('UserService', () => {
|
|
601
|
+
let userService: UserService;
|
|
602
|
+
let mockApi: any;
|
|
310
603
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
604
|
+
beforeEach(() => {
|
|
605
|
+
mockApi = {
|
|
606
|
+
getUserList: vi.fn()
|
|
607
|
+
};
|
|
315
608
|
|
|
316
|
-
|
|
609
|
+
userService = new UserService(mockApi);
|
|
610
|
+
});
|
|
317
611
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
612
|
+
it('should fetch users and update state', async () => {
|
|
613
|
+
const mockResponse = {
|
|
614
|
+
list: [
|
|
615
|
+
{
|
|
616
|
+
id: '1',
|
|
617
|
+
name: 'John',
|
|
618
|
+
email: 'john@example.com',
|
|
619
|
+
avatar: '',
|
|
620
|
+
role: 'user'
|
|
621
|
+
}
|
|
622
|
+
],
|
|
623
|
+
total: 1,
|
|
624
|
+
page: 1,
|
|
625
|
+
pageSize: 10
|
|
626
|
+
};
|
|
324
627
|
|
|
325
|
-
|
|
628
|
+
mockApi.getUserList.mockResolvedValue(mockResponse);
|
|
326
629
|
|
|
327
|
-
|
|
630
|
+
const states: any[] = [];
|
|
631
|
+
userService.subscribe((state) => states.push({ ...state }));
|
|
328
632
|
|
|
329
|
-
|
|
633
|
+
await userService.fetchUsers({ page: 1, pageSize: 10 });
|
|
330
634
|
|
|
331
|
-
|
|
635
|
+
// 验证状态变化
|
|
636
|
+
expect(states).toHaveLength(2);
|
|
637
|
+
expect(states[0].loading).toBe(true);
|
|
638
|
+
expect(states[1].loading).toBe(false);
|
|
639
|
+
expect(states[1].users).toEqual(mockResponse.list);
|
|
640
|
+
expect(states[1].total).toBe(1);
|
|
641
|
+
});
|
|
332
642
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
- 使用 Jest 和 Testing Library
|
|
336
|
-
- 保持测试简单和可维护
|
|
643
|
+
it('should handle error when fetch fails', async () => {
|
|
644
|
+
mockApi.getUserList.mockRejectedValue(new Error('Network error'));
|
|
337
645
|
|
|
338
|
-
|
|
646
|
+
await userService.fetchUsers({ page: 1, pageSize: 10 });
|
|
647
|
+
|
|
648
|
+
expect(userService.getState().error).toBeTruthy();
|
|
649
|
+
expect(userService.getState().loading).toBe(false);
|
|
650
|
+
});
|
|
651
|
+
});
|
|
652
|
+
```
|
|
339
653
|
|
|
340
654
|
```typescript
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
655
|
+
// __tests__/src/pages/users/UserListPage.test.tsx
|
|
656
|
+
|
|
657
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
658
|
+
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
|
659
|
+
import UserListPage from '@/pages/users/UserListPage';
|
|
660
|
+
import { IOCProvider } from '@/uikit/contexts/IOCContext';
|
|
661
|
+
|
|
662
|
+
describe('UserListPage', () => {
|
|
663
|
+
it('should display user list', async () => {
|
|
664
|
+
const mockUsers = [
|
|
665
|
+
{
|
|
666
|
+
id: '1',
|
|
667
|
+
name: 'John',
|
|
668
|
+
email: 'john@example.com',
|
|
669
|
+
avatar: '',
|
|
670
|
+
role: 'user'
|
|
671
|
+
}
|
|
672
|
+
];
|
|
673
|
+
|
|
674
|
+
const mockUserService = {
|
|
675
|
+
fetchUsers: vi.fn(),
|
|
676
|
+
searchUsers: vi.fn(),
|
|
677
|
+
refreshUsers: vi.fn(),
|
|
678
|
+
subscribe: vi.fn(),
|
|
679
|
+
getState: () => ({ users: mockUsers, loading: false, total: 1 }),
|
|
680
|
+
selector: {
|
|
681
|
+
users: (state: any) => state.users,
|
|
682
|
+
loading: (state: any) => state.loading,
|
|
683
|
+
total: (state: any) => state.total
|
|
684
|
+
}
|
|
685
|
+
};
|
|
686
|
+
|
|
687
|
+
const mockIOC = (identifier: string) => {
|
|
688
|
+
if (identifier === 'UserServiceInterface') return mockUserService;
|
|
689
|
+
if (identifier === 'RouteServiceInterface') return { push: vi.fn() };
|
|
690
|
+
};
|
|
691
|
+
|
|
692
|
+
render(
|
|
693
|
+
<IOCProvider value={mockIOC}>
|
|
694
|
+
<UserListPage />
|
|
695
|
+
</IOCProvider>
|
|
696
|
+
);
|
|
697
|
+
|
|
698
|
+
await waitFor(() => {
|
|
699
|
+
expect(screen.getByText('John')).toBeInTheDocument();
|
|
700
|
+
expect(screen.getByText('john@example.com')).toBeInTheDocument();
|
|
701
|
+
});
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
it('should search users when search button clicked', async () => {
|
|
705
|
+
const mockUserService = {
|
|
706
|
+
fetchUsers: vi.fn(),
|
|
707
|
+
searchUsers: vi.fn(),
|
|
708
|
+
subscribe: vi.fn(),
|
|
709
|
+
getState: () => ({ users: [], loading: false }),
|
|
710
|
+
selector: {
|
|
711
|
+
users: () => [],
|
|
712
|
+
loading: () => false,
|
|
713
|
+
total: () => 0
|
|
714
|
+
}
|
|
715
|
+
};
|
|
716
|
+
|
|
717
|
+
const mockIOC = (identifier: string) => {
|
|
718
|
+
if (identifier === 'UserServiceInterface') return mockUserService;
|
|
719
|
+
if (identifier === 'RouteServiceInterface') return { push: vi.fn() };
|
|
720
|
+
};
|
|
721
|
+
|
|
722
|
+
render(
|
|
723
|
+
<IOCProvider value={mockIOC}>
|
|
724
|
+
<UserListPage />
|
|
725
|
+
</IOCProvider>
|
|
726
|
+
);
|
|
727
|
+
|
|
728
|
+
const searchInput = screen.getByPlaceholderText(/search/i);
|
|
729
|
+
fireEvent.change(searchInput, { target: { value: 'John' } });
|
|
730
|
+
|
|
731
|
+
const searchButton = screen.getByRole('button', { name: /search/i });
|
|
732
|
+
fireEvent.click(searchButton);
|
|
733
|
+
|
|
734
|
+
expect(mockUserService.searchUsers).toHaveBeenCalledWith('John');
|
|
346
735
|
});
|
|
347
736
|
});
|
|
348
737
|
```
|
|
349
738
|
|
|
350
|
-
|
|
739
|
+
---
|
|
351
740
|
|
|
352
|
-
##
|
|
741
|
+
## 🎬 常见场景
|
|
353
742
|
|
|
354
|
-
|
|
743
|
+
### 场景 1:添加一个新按钮功能
|
|
355
744
|
|
|
356
|
-
|
|
745
|
+
假设要在用户列表页添加"批量删除"功能:
|
|
357
746
|
|
|
358
747
|
```typescript
|
|
748
|
+
// 1. 添加 i18n Key
|
|
359
749
|
/**
|
|
360
|
-
*
|
|
361
|
-
*
|
|
362
|
-
* @
|
|
363
|
-
* @example
|
|
364
|
-
* const userService = IOC(UserService);
|
|
365
|
-
* await userService.login(credentials);
|
|
750
|
+
* @description Delete selected users
|
|
751
|
+
* @localZh 删除选中用户
|
|
752
|
+
* @localEn Delete Selected
|
|
366
753
|
*/
|
|
754
|
+
export const PAGE_USERS_DELETE_SELECTED = 'page.users.deleteSelected';
|
|
755
|
+
|
|
756
|
+
// 2. 在服务中添加方法
|
|
367
757
|
@injectable()
|
|
368
|
-
export class UserService {
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
// 实现
|
|
758
|
+
export class UserService extends UserServiceInterface {
|
|
759
|
+
async deleteUsers(userIds: string[]): Promise<void> {
|
|
760
|
+
try {
|
|
761
|
+
this.emit({ ...this.state, loading: true });
|
|
762
|
+
await this.api.deleteUsers(userIds);
|
|
763
|
+
await this.refreshUsers(); // 刷新列表
|
|
764
|
+
} catch (error) {
|
|
765
|
+
this.emit({ ...this.state, loading: false, error: error as Error });
|
|
766
|
+
}
|
|
378
767
|
}
|
|
379
768
|
}
|
|
769
|
+
|
|
770
|
+
// 3. 在页面中使用
|
|
771
|
+
function UserListPage() {
|
|
772
|
+
const [selectedRowKeys, setSelectedRowKeys] = useState<string[]>([]);
|
|
773
|
+
|
|
774
|
+
const handleDelete = async () => {
|
|
775
|
+
await userService.deleteUsers(selectedRowKeys);
|
|
776
|
+
setSelectedRowKeys([]);
|
|
777
|
+
};
|
|
778
|
+
|
|
779
|
+
return (
|
|
780
|
+
<div>
|
|
781
|
+
<Button
|
|
782
|
+
danger
|
|
783
|
+
onClick={handleDelete}
|
|
784
|
+
disabled={selectedRowKeys.length === 0}
|
|
785
|
+
>
|
|
786
|
+
{t(i18nKeys.PAGE_USERS_DELETE_SELECTED)}
|
|
787
|
+
</Button>
|
|
788
|
+
|
|
789
|
+
<Table
|
|
790
|
+
rowSelection={{
|
|
791
|
+
selectedRowKeys,
|
|
792
|
+
onChange: setSelectedRowKeys
|
|
793
|
+
}}
|
|
794
|
+
// ...
|
|
795
|
+
/>
|
|
796
|
+
</div>
|
|
797
|
+
);
|
|
798
|
+
}
|
|
380
799
|
```
|
|
381
800
|
|
|
382
|
-
### 2
|
|
801
|
+
### 场景 2:添加一个弹窗表单
|
|
383
802
|
|
|
384
|
-
|
|
385
|
-
- **docs/**:
|
|
386
|
-
- `zh/`:中文文档
|
|
387
|
-
- `en/`:英文文档
|
|
388
|
-
- 按功能模块组织文档文件
|
|
803
|
+
假设要添加"编辑用户"弹窗:
|
|
389
804
|
|
|
390
|
-
|
|
805
|
+
```typescript
|
|
806
|
+
// 1. 创建弹窗组件
|
|
807
|
+
// components/UserEditModal.tsx
|
|
808
|
+
interface UserEditModalProps {
|
|
809
|
+
user: UserInfo | null;
|
|
810
|
+
visible: boolean;
|
|
811
|
+
onClose: () => void;
|
|
812
|
+
onSubmit: (user: UserInfo) => void;
|
|
813
|
+
}
|
|
391
814
|
|
|
392
|
-
|
|
393
|
-
|
|
815
|
+
export function UserEditModal({ user, visible, onClose, onSubmit }: UserEditModalProps) {
|
|
816
|
+
const [form] = Form.useForm();
|
|
817
|
+
const { t } = useAppTranslation();
|
|
394
818
|
|
|
395
|
-
|
|
819
|
+
useEffect(() => {
|
|
820
|
+
if (user) {
|
|
821
|
+
form.setFieldsValue(user);
|
|
822
|
+
}
|
|
823
|
+
}, [user]);
|
|
396
824
|
|
|
397
|
-
|
|
825
|
+
const handleSubmit = async () => {
|
|
826
|
+
const values = await form.validateFields();
|
|
827
|
+
onSubmit(values);
|
|
828
|
+
};
|
|
398
829
|
|
|
399
|
-
|
|
830
|
+
return (
|
|
831
|
+
<Modal
|
|
832
|
+
title={t(i18nKeys.PAGE_USERS_EDIT_TITLE)}
|
|
833
|
+
open={visible}
|
|
834
|
+
onCancel={onClose}
|
|
835
|
+
onOk={handleSubmit}
|
|
836
|
+
>
|
|
837
|
+
<Form form={form} layout="vertical">
|
|
838
|
+
<Form.Item
|
|
839
|
+
name="name"
|
|
840
|
+
label={t(i18nKeys.PAGE_USERS_FORM_NAME)}
|
|
841
|
+
rules={[{ required: true }]}
|
|
842
|
+
>
|
|
843
|
+
<Input />
|
|
844
|
+
</Form.Item>
|
|
845
|
+
<Form.Item
|
|
846
|
+
name="email"
|
|
847
|
+
label={t(i18nKeys.PAGE_USERS_FORM_EMAIL)}
|
|
848
|
+
rules={[{ required: true, type: 'email' }]}
|
|
849
|
+
>
|
|
850
|
+
<Input />
|
|
851
|
+
</Form.Item>
|
|
852
|
+
</Form>
|
|
853
|
+
</Modal>
|
|
854
|
+
);
|
|
855
|
+
}
|
|
400
856
|
|
|
401
|
-
|
|
857
|
+
// 2. 在服务中添加更新方法
|
|
858
|
+
@injectable()
|
|
859
|
+
export class UserService extends UserServiceInterface {
|
|
860
|
+
async updateUser(userId: string, data: Partial<UserInfo>): Promise<void> {
|
|
861
|
+
this.emit({ ...this.state, loading: true });
|
|
862
|
+
await this.api.updateUser(userId, data);
|
|
863
|
+
await this.refreshUsers();
|
|
864
|
+
}
|
|
865
|
+
}
|
|
402
866
|
|
|
403
|
-
|
|
867
|
+
// 3. 在页面中使用
|
|
868
|
+
function UserListPage() {
|
|
869
|
+
const [editUser, setEditUser] = useState<UserInfo | null>(null);
|
|
870
|
+
const [modalVisible, setModalVisible] = useState(false);
|
|
404
871
|
|
|
405
|
-
|
|
872
|
+
const handleEdit = (user: UserInfo) => {
|
|
873
|
+
setEditUser(user);
|
|
874
|
+
setModalVisible(true);
|
|
875
|
+
};
|
|
406
876
|
|
|
407
|
-
|
|
877
|
+
const handleSubmit = async (values: UserInfo) => {
|
|
878
|
+
await userService.updateUser(editUser!.id, values);
|
|
879
|
+
setModalVisible(false);
|
|
880
|
+
setEditUser(null);
|
|
881
|
+
};
|
|
408
882
|
|
|
409
|
-
|
|
883
|
+
return (
|
|
884
|
+
<div>
|
|
885
|
+
<Table
|
|
886
|
+
columns={[
|
|
887
|
+
// ...
|
|
888
|
+
{
|
|
889
|
+
title: 'Actions',
|
|
890
|
+
render: (_, record) => (
|
|
891
|
+
<Button onClick={() => handleEdit(record)}>Edit</Button>
|
|
892
|
+
)
|
|
893
|
+
}
|
|
894
|
+
]}
|
|
895
|
+
// ...
|
|
896
|
+
/>
|
|
897
|
+
|
|
898
|
+
<UserEditModal
|
|
899
|
+
user={editUser}
|
|
900
|
+
visible={modalVisible}
|
|
901
|
+
onClose={() => setModalVisible(false)}
|
|
902
|
+
onSubmit={handleSubmit}
|
|
903
|
+
/>
|
|
904
|
+
</div>
|
|
905
|
+
);
|
|
906
|
+
}
|
|
410
907
|
```
|
|
411
908
|
|
|
412
|
-
|
|
909
|
+
### 场景 3:添加实时搜索
|
|
413
910
|
|
|
414
|
-
|
|
911
|
+
假设要实现"输入时自动搜索":
|
|
415
912
|
|
|
416
|
-
|
|
913
|
+
```typescript
|
|
914
|
+
function UserListPage() {
|
|
915
|
+
const [keyword, setKeyword] = useState('');
|
|
417
916
|
|
|
418
|
-
|
|
419
|
-
|
|
917
|
+
// 使用 debounce 优化搜索
|
|
918
|
+
const debouncedKeyword = useDebounce(keyword, 500);
|
|
420
919
|
|
|
421
|
-
|
|
920
|
+
useEffect(() => {
|
|
921
|
+
if (debouncedKeyword !== undefined) {
|
|
922
|
+
userService.searchUsers(debouncedKeyword);
|
|
923
|
+
}
|
|
924
|
+
}, [debouncedKeyword]);
|
|
422
925
|
|
|
423
|
-
|
|
424
|
-
|
|
926
|
+
return (
|
|
927
|
+
<Input
|
|
928
|
+
placeholder={t(i18nKeys.PAGE_USERS_SEARCH_PLACEHOLDER)}
|
|
929
|
+
value={keyword}
|
|
930
|
+
onChange={(e) => setKeyword(e.target.value)}
|
|
931
|
+
/>
|
|
932
|
+
);
|
|
933
|
+
}
|
|
425
934
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
- `docs`:文档更新
|
|
430
|
-
- `style`:代码格式(不影响代码运行的变动)
|
|
431
|
-
- `refactor`:重构
|
|
432
|
-
- `test`:增加测试
|
|
433
|
-
- `chore`:构建过程或辅助工具的变动
|
|
935
|
+
// 自定义 Hook
|
|
936
|
+
function useDebounce<T>(value: T, delay: number): T {
|
|
937
|
+
const [debouncedValue, setDebouncedValue] = useState(value);
|
|
434
938
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
939
|
+
useEffect(() => {
|
|
940
|
+
const timer = setTimeout(() => {
|
|
941
|
+
setDebouncedValue(value);
|
|
942
|
+
}, delay);
|
|
439
943
|
|
|
440
|
-
|
|
944
|
+
return () => {
|
|
945
|
+
clearTimeout(timer);
|
|
946
|
+
};
|
|
947
|
+
}, [value, delay]);
|
|
441
948
|
|
|
949
|
+
return debouncedValue;
|
|
950
|
+
}
|
|
442
951
|
```
|
|
443
|
-
feat(auth): 添加用户角色管理功能
|
|
444
952
|
|
|
445
|
-
|
|
446
|
-
- 实现角色权限配置
|
|
447
|
-
- 添加角色分配功能
|
|
953
|
+
---
|
|
448
954
|
|
|
449
|
-
|
|
450
|
-
```
|
|
955
|
+
## 📐 代码规范
|
|
451
956
|
|
|
452
|
-
|
|
957
|
+
### 1. 命名规范
|
|
453
958
|
|
|
454
|
-
|
|
959
|
+
```typescript
|
|
960
|
+
// ✅ 好的命名
|
|
961
|
+
const userService = useIOC('UserServiceInterface'); // 服务:小驼峰
|
|
962
|
+
const UserListPage = () => {
|
|
963
|
+
/* ... */
|
|
964
|
+
}; // 组件:大驼峰
|
|
965
|
+
const PAGE_USERS_TITLE = 'page.users.title'; // 常量:大写下划线
|
|
966
|
+
interface UserInfo {
|
|
967
|
+
/* ... */
|
|
968
|
+
} // 接口:大驼峰
|
|
969
|
+
type UserRole = 'admin' | 'user'; // 类型:大驼峰
|
|
970
|
+
|
|
971
|
+
// ❌ 不好的命名
|
|
972
|
+
const UserService = useIOC('UserServiceInterface'); // 应该是小驼峰
|
|
973
|
+
const userListPage = () => {
|
|
974
|
+
/* ... */
|
|
975
|
+
}; // 组件应该大驼峰
|
|
976
|
+
const pageUsersTitle = 'page.users.title'; // 常量应该大写
|
|
977
|
+
interface userInfo {
|
|
978
|
+
/* ... */
|
|
979
|
+
} // 接口应该大驼峰
|
|
980
|
+
```
|
|
455
981
|
|
|
456
|
-
###
|
|
982
|
+
### 2. 文件组织
|
|
457
983
|
|
|
458
984
|
```typescript
|
|
459
|
-
//
|
|
460
|
-
|
|
985
|
+
// ✅ 好的文件组织
|
|
986
|
+
import { FC, useEffect, useState } from 'react'; // React
|
|
987
|
+
import { Button, Table, Input } from 'antd'; // 第三方 UI
|
|
988
|
+
import { useIOC } from '@/uikit/hooks/useIOC'; // 项目内部
|
|
989
|
+
import { useAppTranslation } from '@/uikit/hooks/useAppTranslation';
|
|
990
|
+
import * as i18nKeys from '@config/Identifier/pages/page.users';
|
|
991
|
+
import './UserListPage.css'; // 样式
|
|
992
|
+
|
|
993
|
+
// 类型定义
|
|
994
|
+
interface Props {
|
|
995
|
+
/* ... */
|
|
996
|
+
}
|
|
461
997
|
|
|
462
|
-
//
|
|
463
|
-
|
|
998
|
+
// 组件
|
|
999
|
+
export default function UserListPage() {
|
|
1000
|
+
/* ... */
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
// ❌ 不好的文件组织
|
|
1004
|
+
import './UserListPage.css'; // 样式不应该在最前
|
|
1005
|
+
import * as i18nKeys from '@config/Identifier/pages/page.users';
|
|
1006
|
+
import { Button } from 'antd';
|
|
1007
|
+
import { useIOC } from '@/uikit/hooks/useIOC';
|
|
1008
|
+
import { FC } from 'react';
|
|
464
1009
|
```
|
|
465
1010
|
|
|
466
|
-
###
|
|
1011
|
+
### 3. 组件结构
|
|
467
1012
|
|
|
468
1013
|
```typescript
|
|
469
|
-
//
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
// 使用 useCallback 缓存函数
|
|
475
|
-
const handleUpdate = useCallback(() => {
|
|
476
|
-
// 实现
|
|
477
|
-
}, [dependencies]);
|
|
478
|
-
|
|
479
|
-
// 使用 React.memo 避免不必要的重渲染
|
|
480
|
-
const UserCard = React.memo(({ user }) => {
|
|
481
|
-
return <div>{user.name}</div>;
|
|
482
|
-
});
|
|
483
|
-
```
|
|
484
|
-
|
|
485
|
-
## 安全规范
|
|
1014
|
+
// ✅ 好的组件结构
|
|
1015
|
+
export default function UserListPage() {
|
|
1016
|
+
// 1. Hooks
|
|
1017
|
+
const userService = useIOC('UserServiceInterface');
|
|
1018
|
+
const { t } = useAppTranslation();
|
|
486
1019
|
|
|
487
|
-
|
|
1020
|
+
// 2. 状态
|
|
1021
|
+
const users = useStore(userService, userService.selector.users);
|
|
1022
|
+
const [keyword, setKeyword] = useState('');
|
|
488
1023
|
|
|
489
|
-
|
|
1024
|
+
// 3. 副作用
|
|
1025
|
+
useEffect(() => {
|
|
1026
|
+
userService.fetchUsers({ page: 1, pageSize: 10 });
|
|
1027
|
+
}, []);
|
|
490
1028
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
1029
|
+
// 4. 事件处理
|
|
1030
|
+
const handleSearch = () => {
|
|
1031
|
+
userService.searchUsers(keyword);
|
|
1032
|
+
};
|
|
494
1033
|
|
|
495
|
-
//
|
|
496
|
-
const
|
|
1034
|
+
// 5. 渲染函数
|
|
1035
|
+
const renderActions = (record: UserInfo) => {
|
|
1036
|
+
return <Button onClick={() => handleEdit(record)}>Edit</Button>;
|
|
1037
|
+
};
|
|
497
1038
|
|
|
498
|
-
//
|
|
499
|
-
|
|
1039
|
+
// 6. 返回 JSX
|
|
1040
|
+
return (
|
|
1041
|
+
<div>
|
|
1042
|
+
{/* ... */}
|
|
1043
|
+
</div>
|
|
1044
|
+
);
|
|
1045
|
+
}
|
|
500
1046
|
```
|
|
501
1047
|
|
|
502
|
-
###
|
|
1048
|
+
### 4. 注释规范
|
|
503
1049
|
|
|
504
1050
|
```typescript
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
1051
|
+
/**
|
|
1052
|
+
* 用户列表页面
|
|
1053
|
+
*
|
|
1054
|
+
* @description 显示用户列表,支持搜索、分页、查看详情等功能
|
|
1055
|
+
*/
|
|
1056
|
+
export default function UserListPage() {
|
|
1057
|
+
/**
|
|
1058
|
+
* 处理搜索
|
|
1059
|
+
* 根据关键词搜索用户
|
|
1060
|
+
*/
|
|
1061
|
+
const handleSearch = () => {
|
|
1062
|
+
userService.searchUsers(keyword);
|
|
1063
|
+
};
|
|
510
1064
|
|
|
511
|
-
//
|
|
512
|
-
|
|
513
|
-
|
|
1065
|
+
// 初始化加载用户列表
|
|
1066
|
+
useEffect(() => {
|
|
1067
|
+
userService.fetchUsers({ page: 1, pageSize: 10 });
|
|
1068
|
+
}, []);
|
|
514
1069
|
|
|
515
1070
|
return (
|
|
516
1071
|
<div>
|
|
517
|
-
{
|
|
518
|
-
|
|
519
|
-
|
|
1072
|
+
{/* 搜索栏 */}
|
|
1073
|
+
<Input.Search onSearch={handleSearch} />
|
|
1074
|
+
|
|
1075
|
+
{/* 用户表格 */}
|
|
1076
|
+
<Table dataSource={users} />
|
|
520
1077
|
</div>
|
|
521
1078
|
);
|
|
522
1079
|
}
|
|
523
1080
|
```
|
|
1081
|
+
|
|
1082
|
+
---
|
|
1083
|
+
|
|
1084
|
+
## 🛠️ 开发工具
|
|
1085
|
+
|
|
1086
|
+
### 推荐的 VSCode 插件
|
|
1087
|
+
|
|
1088
|
+
```
|
|
1089
|
+
✅ ESLint - 代码检查
|
|
1090
|
+
✅ Prettier - 代码格式化
|
|
1091
|
+
✅ TypeScript Vue Plugin (Volar) - Vue/React 支持
|
|
1092
|
+
✅ Tailwind CSS IntelliSense - Tailwind 智能提示
|
|
1093
|
+
✅ i18n Ally - i18n 管理
|
|
1094
|
+
✅ GitLens - Git 增强
|
|
1095
|
+
✅ Error Lens - 错误提示
|
|
1096
|
+
✅ Auto Rename Tag - 标签自动重命名
|
|
1097
|
+
```
|
|
1098
|
+
|
|
1099
|
+
### 快捷命令
|
|
1100
|
+
|
|
1101
|
+
```bash
|
|
1102
|
+
# 开发
|
|
1103
|
+
npm run dev # 启动开发服务器
|
|
1104
|
+
npm run dev:staging # 启动测试环境
|
|
1105
|
+
|
|
1106
|
+
# 构建
|
|
1107
|
+
npm run build # 生产构建
|
|
1108
|
+
npm run preview # 预览构建结果
|
|
1109
|
+
|
|
1110
|
+
# 代码检查
|
|
1111
|
+
npm run lint # ESLint 检查
|
|
1112
|
+
npm run lint:fix # ESLint 自动修复
|
|
1113
|
+
npm run type-check # TypeScript 类型检查
|
|
1114
|
+
|
|
1115
|
+
# 测试
|
|
1116
|
+
npm run test # 运行测试
|
|
1117
|
+
npm run test:watch # 监听模式测试
|
|
1118
|
+
npm run test:coverage # 测试覆盖率
|
|
1119
|
+
|
|
1120
|
+
# i18n
|
|
1121
|
+
npm run i18n:generate # 生成翻译文件
|
|
1122
|
+
```
|
|
1123
|
+
|
|
1124
|
+
### 调试技巧
|
|
1125
|
+
|
|
1126
|
+
```typescript
|
|
1127
|
+
// 1. 使用 logger
|
|
1128
|
+
import { logger } from '@/core/globals';
|
|
1129
|
+
|
|
1130
|
+
logger.debug('User data:', user);
|
|
1131
|
+
logger.error('Failed to fetch users:', error);
|
|
1132
|
+
|
|
1133
|
+
// 2. 使用 React DevTools
|
|
1134
|
+
// 安装 React Developer Tools 浏览器插件
|
|
1135
|
+
|
|
1136
|
+
// 3. 使用 Redux DevTools(如果需要)
|
|
1137
|
+
// 查看 Store 状态变化
|
|
1138
|
+
|
|
1139
|
+
// 4. 使用 VSCode 断点调试
|
|
1140
|
+
// 在代码行左侧点击设置断点,然后 F5 启动调试
|
|
1141
|
+
```
|
|
1142
|
+
|
|
1143
|
+
---
|
|
1144
|
+
|
|
1145
|
+
## 🎯 开发 Checklist
|
|
1146
|
+
|
|
1147
|
+
### 功能开发
|
|
1148
|
+
|
|
1149
|
+
- [ ] 定义 i18n Key
|
|
1150
|
+
- [ ] 定义接口和类型
|
|
1151
|
+
- [ ] 实现 API 适配器(如果需要)
|
|
1152
|
+
- [ ] 实现服务
|
|
1153
|
+
- [ ] 配置路由
|
|
1154
|
+
- [ ] 实现页面组件
|
|
1155
|
+
- [ ] 注册到 IOC(如果是新服务)
|
|
1156
|
+
- [ ] 功能自测
|
|
1157
|
+
|
|
1158
|
+
### 代码质量
|
|
1159
|
+
|
|
1160
|
+
- [ ] 通过 ESLint 检查
|
|
1161
|
+
- [ ] 通过 TypeScript 类型检查
|
|
1162
|
+
- [ ] 代码格式化(Prettier)
|
|
1163
|
+
- [ ] 移除 console.log 和调试代码
|
|
1164
|
+
- [ ] 移除未使用的导入
|
|
1165
|
+
|
|
1166
|
+
### 测试
|
|
1167
|
+
|
|
1168
|
+
- [ ] 编写服务测试
|
|
1169
|
+
- [ ] 编写 UI 测试
|
|
1170
|
+
- [ ] 测试覆盖率 > 80%
|
|
1171
|
+
- [ ] 所有测试通过
|
|
1172
|
+
|
|
1173
|
+
### 文档
|
|
1174
|
+
|
|
1175
|
+
- [ ] 更新相关文档
|
|
1176
|
+
- [ ] 添加必要的代码注释
|
|
1177
|
+
- [ ] 更新 API 文档(如果有)
|
|
1178
|
+
|
|
1179
|
+
### 提交
|
|
1180
|
+
|
|
1181
|
+
- [ ] Git commit 符合规范
|
|
1182
|
+
- [ ] 代码已 review
|
|
1183
|
+
- [ ] 合并到主分支
|
|
1184
|
+
|
|
1185
|
+
---
|
|
1186
|
+
|
|
1187
|
+
## 📚 相关文档
|
|
1188
|
+
|
|
1189
|
+
- **[项目架构设计](./index.md)** - 了解整体架构
|
|
1190
|
+
- **[IOC 容器](./ioc.md)** - 依赖注入和 UI 分离
|
|
1191
|
+
- **[Store 状态管理](./store.md)** - 应用层如何通知 UI 层
|
|
1192
|
+
- **[Bootstrap 启动器](./bootstrap.md)** - 应用启动和初始化
|
|
1193
|
+
- **[环境变量管理](./env.md)** - 多环境配置
|
|
1194
|
+
- **[国际化](./i18n.md)** - i18n Key 和翻译管理
|
|
1195
|
+
|
|
1196
|
+
---
|
|
1197
|
+
|
|
1198
|
+
**问题反馈:**
|
|
1199
|
+
如果你在开发过程中遇到任何问题,请在团队频道中讨论或提交 Issue。
|