From 025b0822df11e95b19960a015494fed9a75c8de7 Mon Sep 17 00:00:00 2001
From: insistence <92962165+insistence@users.noreply.github.com>
Date: Sat, 10 Oct 2026 21:15:22 +0800
Subject: [PATCH] =?UTF-8?q?refactor:=20=E7=BB=9F=E4=B8=80=E5=A4=9A?=
=?UTF-8?q?=E5=89=8D=E7=AB=AF=E5=B7=A5=E7=A8=8B=E6=9E=B6=E6=9E=84=E3=80=81?=
=?UTF-8?q?=E6=8F=92=E4=BB=B6=E9=85=8D=E7=BD=AE=E4=B8=8E=20CI=20(#131)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* refactor: 重构项目架构
* refactor: 重构ci和测试
* fix: 修复测试
* feat: 新增更多E2E测试
* fix: 修复E2E测试
---
.github/requirements.txt | 3 +
.github/ruff.toml | 2 +
.github/scripts/check_compose.py | 72 +
.github/scripts/check_test_report.py | 39 +
.github/scripts/ci_plan.py | 245 ++
.../scripts/install_ai_test_dependencies.py | 23 +
.github/scripts/mobile_smoke.py | 184 ++
.github/tests/test_ci_contracts.py | 91 +
.github/tests/test_ci_plan.py | 169 +
.github/workflows/backend.yml | 64 -
.github/workflows/ci-backend.yml | 110 +
.github/workflows/ci-e2e.yml | 111 +
.github/workflows/ci-frontend.yml | 116 +
.github/workflows/ci-integration.yml | 218 ++
.github/workflows/ci-native.yml | 80 +
.github/workflows/ci-quality.yml | 54 +
.github/workflows/ci.yml | 106 +
.github/workflows/native-plugin.yml | 97 -
.github/workflows/playwright.yml | 174 -
.github/workflows/plugin-frontend.yml | 69 -
.github/workflows/plugin-network-smoke.yml | 63 -
.../workflows/plugin-release-integration.yml | 94 -
.github/workflows/ruff.yml | 33 -
.github/workflows/timezone.yml | 106 -
CHANGELOG.md | 244 +-
README.md | 98 +-
docker-compose.my.yml | 5 +-
docker-compose.pg.yml | 5 +-
docker.env | 1 +
ruoyi-fastapi-app/tests/time.test.js | 3 -
ruoyi-fastapi-backend/.dockerignore | 12 +
ruoyi-fastapi-backend/Dockerfile.my | 6 +-
ruoyi-fastapi-backend/Dockerfile.pg | 6 +-
.../cli/groups/plugin/commands/developer.py | 16 +-
.../cli/groups/plugin/commands/sdk.py | 43 +-
.../cli/groups/plugin/controller.py | 2 +-
.../cli/groups/plugin/options.py | 6 +-
.../cli/groups/plugin/presenter.py | 2 +-
.../cli/runtime/plugin/scaffold/__init__.py | 6 +-
.../cli/runtime/plugin/scaffold/backend.py | 3 +-
.../cli/runtime/plugin/scaffold/builder.py | 69 +-
.../cli/runtime/plugin/scaffold/frontend.py | 114 +-
.../cli/runtime/plugin/scaffold/payload.py | 4 +-
.../cli/runtime/plugin/scaffold/v2.py | 29 +-
.../cli/runtime/plugin/sdk.py | 50 +-
.../cli/runtime/plugin/service.py | 13 +-
.../cli/runtime/plugin/support.py | 31 +-
ruoyi-fastapi-backend/config/env.py | 2 +-
ruoyi-fastapi-backend/docs/cli_usage.md | 31 +-
.../docs/plugin_development.md | 139 +-
.../unified_authentication_developer_guide.md | 2 +-
.../module_admin/service/server_service.py | 75 +-
.../module_identity/service/audit_service.py | 5 +-
ruoyi-fastapi-backend/plugins/ai/README.md | 14 +-
ruoyi-fastapi-backend/plugins/ai/plugin.yaml | 37 +-
.../plugins/core/capability.py | 3 +-
.../plugins/core/environment.py | 85 +-
.../plugins/core/frontend.py | 214 ++
.../core/management/service/service.py | 27 +-
.../plugins/core/manifest/schema.py | 57 +-
.../plugins/core/manifest/v2.py | 2 +-
.../core/runtime/service/dependencies.py | 8 +-
.../plugins/core/runtime/service/facade.py | 2 +
.../runtime/support/payload/documentation.py | 18 +-
.../plugins/core/validation/dependencies.py | 29 +-
.../core/validation/dependency_policy.py | 46 +-
.../plugins/core/validation/manifest.py | 9 +-
.../examples/python/bundle_demo/README.md | 6 +-
.../python/bundle_demo/web/vite.config.js | 2 +-
.../python/task_demo/web/vite.config.js | 2 +-
ruoyi-fastapi-backend/pyproject.toml | 8 +-
ruoyi-fastapi-backend/requirements-test.txt | 4 +
.../tests/cli/root/conftest.py | 38 +-
.../tests/cli/root/test_bootstrap.py | 7 +
.../tests/cli/root/test_contract_app_ops.py | 45 +-
.../tests/cli/root/test_contract_cli.py | 4 +
.../tests/cli/root/test_contract_config.py | 4 +
.../cli/root/test_contract_db_cache_dev.py | 3 +
.../tests/cli/root/test_contract_job_gen.py | 3 +
.../cli/root/test_plugin_artifact_commands.py | 3 +-
.../cli/root/test_plugin_create_templates.py | 46 +
.../root/test_plugin_frontend_environment.py | 130 +
.../tests/cli/root/test_plugin_lazy_import.py | 5 +
.../cli/root/test_plugin_sdk_commands.py | 102 +-
.../tests/cli/runtime/plugin/conftest.py | 5 +
.../plugin/test_runtime_dependency_lock.py | 5 +-
.../runtime/plugin/test_runtime_scaffold.py | 123 +-
.../plugin/test_runtime_scaffold_v2.py | 48 +-
.../cli/runtime/plugin/test_runtime_sdk.py | 51 +
.../runtime/plugin/test_runtime_testing.py | 6 +-
.../tests/{ => config}/conftest.py | 0
.../test_scheduler_execution_coordination.py | 47 +-
.../tests/module_admin/service/conftest.py | 1 +
.../service/test_server_service.py | 169 +
.../services/test_audit_service.py | 44 +-
.../plugins/core/deployment/test_worker.py | 1 +
.../plugins/core/runtime/fakes/environment.py | 6 +
.../service/test_batch_dependencies.py | 22 +-
.../core/runtime/service/test_catalog.py | 6 +-
.../core/runtime/service/test_check_docs.py | 6 +-
.../service/test_install_config_health.py | 18 +-
.../runtime/service/test_structure_checks.py | 2 +-
.../service/test_upgrade_enable_purge.py | 28 +-
.../core/runtime/test_plugin_bootstrap.py | 4 +-
.../plugins/core/test_frontend_layout.py | 151 +
.../core/validation/test_dependencies.py | 19 +-
.../validation/test_frontend_dependencies.py | 351 ++
.../plugins/core/validation/test_manifest.py | 9 +-
.../core/validation/test_manifest_v2.py | 2 +-
.../plugins/integration/test_network_smoke.py | 41 +-
.../tests/plugins/native/test_rust_demo.py | 5 +-
.../plugins/sample_plugins/test_ai_plugin.py | 27 +-
.../tests/time/time.test.js | 3 -
ruoyi-fastapi-frontend/vue2/mobile/.env | 1 +
ruoyi-fastapi-frontend/vue2/mobile/.gitignore | 31 +
ruoyi-fastapi-frontend/vue2/mobile/.npmrc | 1 +
ruoyi-fastapi-frontend/vue2/mobile/README.md | 35 +
.../vue2/mobile/babel.config.js | 86 +
.../vue2/mobile/package.json | 150 +
.../vue2/mobile/platform.js | 12 +
.../vue2/mobile/postcss.config.js | 32 +
.../vue2/mobile/public/index.html | 20 +
.../vue2/mobile/scripts/patch-uni-webpack.cjs | 19 +
.../vue2/mobile/src/App.vue | 70 +
.../vue2/mobile}/src/api/login.js | 124 +-
.../vue2/mobile}/src/api/system/dict/data.js | 0
.../vue2/mobile}/src/api/system/dict/type.js | 0
.../vue2/mobile}/src/api/system/user.js | 0
.../vue2/mobile/src/config.js | 26 +
.../vue2/mobile/src/main.ts | 17 +
.../vue2/mobile/src/manifest.json | 68 +
.../vue2/mobile}/src/pages.json | 276 +-
.../src/pages/common/agreement/index.vue | 69 +
.../mobile/src/pages/common/privacy/index.vue | 93 +
.../src/pages/common/textview/index.vue | 39 +
.../mobile/src/pages/common/webview/index.vue | 34 +
.../vue2/mobile/src/pages/index.vue | 214 ++
.../vue2/mobile/src/pages/login.vue | 218 ++
.../mobile/src/pages/mine/about/index.vue | 73 +
.../mobile/src/pages/mine/avatar/index.vue | 757 +++++
.../vue2/mobile/src/pages/mine/help/index.vue | 94 +
.../vue2/mobile/src/pages/mine/index.vue | 212 ++
.../vue2/mobile/src/pages/mine/info/edit.vue | 147 +
.../vue2/mobile/src/pages/mine/info/index.vue | 133 +
.../vue2/mobile/src/pages/mine/pwd/index.vue | 100 +
.../mobile/src/pages/mine/setting/index.vue | 98 +
.../src/pages/mine/setting/timezone.vue | 198 ++
.../vue2/mobile/src/pages/register.vue | 207 ++
.../vue2/mobile/src/pages/work/index.vue | 235 ++
.../vue2/mobile}/src/permission.js | 87 +-
.../vue2/mobile/src/plugins/auth.js | 60 +
.../vue2/mobile/src/plugins/index.js | 14 +
.../vue2/mobile}/src/plugins/modal.js | 156 +-
.../vue2/mobile}/src/plugins/tab.js | 60 +-
.../vue2/mobile/src/sfc.d.ts | 4 +
.../vue2/mobile}/src/static/favicon.ico | Bin
.../src/static/images/banner/banner01.jpg | Bin
.../src/static/images/banner/banner02.jpg | Bin
.../src/static/images/banner/banner03.jpg | Bin
.../mobile}/src/static/images/profile.jpg | Bin
.../mobile}/src/static/images/tabbar/home.png | Bin
.../src/static/images/tabbar/home_.png | Bin
.../mobile}/src/static/images/tabbar/mine.png | Bin
.../src/static/images/tabbar/mine_.png | Bin
.../mobile}/src/static/images/tabbar/work.png | Bin
.../src/static/images/tabbar/work_.png | Bin
.../vue2/mobile}/src/static/logo.png | Bin
.../vue2/mobile}/src/static/logo200.png | Bin
.../vue2/mobile/src/store/getters.js | 9 +
.../vue2/mobile/src/store/index.js | 15 +
.../vue2/mobile/src/store/modules/user.js | 147 +
.../vue2/mobile}/src/uni.scss | 0
.../vue2/mobile}/src/utils/auth.js | 26 +-
.../vue2/mobile}/src/utils/common.js | 0
.../vue2/mobile}/src/utils/constant.js | 0
.../vue2/mobile}/src/utils/errorCode.js | 12 +-
.../vue2/mobile}/src/utils/permission.js | 110 +-
.../vue2/mobile/src/utils/request.js | 140 +
.../vue2/mobile}/src/utils/storage.js | 0
.../vue2/mobile/src/utils/time.js | 535 ++++
.../vue2/mobile}/src/utils/transportCrypto.js | 0
.../src/utils/transportCryptoPolicy.js | 0
.../vue2/mobile}/src/utils/transportForge.js | 0
.../vue2/mobile/src/utils/upload.js | 72 +
.../vue2/mobile}/src/utils/validate.js | 0
.../mobile/src/vendor/node-forge/lib/prng.js | 421 +++
.../src/vendor/node-forge/lib/random.js | 191 ++
.../mobile/src/vendor/node-forge/lib/rsa.js | 1954 ++++++++++++
.../mobile/src/vendor/node-forge/lib/util.js | 2652 ++++++++++++++++
.../vue2/mobile/tailwind.config.js | 43 +
.../vue2/mobile/tests/behavior.test.mjs | 3 +
.../vue2/mobile/tests/time-bundle.test.cjs | 68 +
.../vue2/mobile/tests/time-polyfill.test.mjs | 5 +
.../vue2/mobile/tests/time.test.mjs | 3 +
.../vue2/mobile/tsconfig.json | 27 +
.../vue2/mobile/vue.config.js | 71 +
ruoyi-fastapi-frontend/vue2/web/.dockerignore | 6 +
.../{ => vue2/web}/.editorconfig | 0
.../vue2/web/.env.development | 14 +
ruoyi-fastapi-frontend/vue2/web/.env.docker | 15 +
.../vue2/web/.env.production | 11 +
ruoyi-fastapi-frontend/vue2/web/.env.staging | 15 +
.../{ => vue2/web}/.gitattributes | 0
.../{ => vue2/web}/.gitignore | 0
.../vue2/web/.prettierignore | 30 +
.../{ => vue2/web}/.prettierrc.json | 0
ruoyi-fastapi-frontend/vue2/web/Dockerfile | 31 +
ruoyi-fastapi-frontend/vue2/web/FORMATTING.md | 122 +
.../vue2/web/babel.config.js | 16 +
ruoyi-fastapi-frontend/vue2/web/bin/build.bat | 12 +
.../vue2/web/bin/nginx.dockermy.conf | 72 +
.../vue2/web/bin/nginx.dockerpg.conf | 72 +
.../vue2/web/bin/package.bat | 12 +
.../vue2/web/bin/run-web.bat | 12 +
.../vue2/web/build/index.js | 34 +
.../vue2/web/frontend.code-workspace | 21 +
ruoyi-fastapi-frontend/vue2/web/package.json | 101 +
.../vue2/web/plugins/ai/README.md | 84 +
.../{ => vue2/web}/plugins/ai/api/chat.js | 0
.../{ => vue2/web}/plugins/ai/api/model.js | 0
.../ai/views/chat/components/AiMessage.vue | 194 ++
.../vue2/web/plugins/ai/views/chat/index.vue | 1317 ++++++++
.../vue2/web/plugins/ai/views/model/index.vue | 585 ++++
.../{ => vue2/web}/public/favicon.ico | Bin
.../{ => vue2/web/public}/html/ie.html | 0
.../vue2/web/public/index.html | 211 ++
.../vue2/web/public/robots.txt | 2 +
.../web/public/styles/theme-chalk/index.css | 1 +
ruoyi-fastapi-frontend/vue2/web/src/App.vue | 34 +
.../{ => vue2/web}/src/api/authCenter.js | 0
.../{ => vue2/web}/src/api/login.js | 0
.../{ => vue2/web}/src/api/menu.js | 0
.../{ => vue2/web}/src/api/monitor/cache.js | 0
.../{ => vue2/web}/src/api/monitor/job.js | 0
.../{ => vue2/web}/src/api/monitor/jobLog.js | 0
.../web}/src/api/monitor/logininfor.js | 0
.../web}/src/api/monitor/oauthAudit.js | 0
.../{ => vue2/web}/src/api/monitor/online.js | 0
.../{ => vue2/web}/src/api/monitor/operlog.js | 0
.../{ => vue2/web}/src/api/monitor/server.js | 0
.../web}/src/api/monitor/transportCrypto.js | 0
.../{ => vue2/web}/src/api/system/config.js | 0
.../{ => vue2/web}/src/api/system/dept.js | 0
.../web}/src/api/system/dict/data.js | 0
.../web}/src/api/system/dict/type.js | 0
.../{ => vue2/web}/src/api/system/file.js | 0
.../{ => vue2/web}/src/api/system/menu.js | 0
.../{ => vue2/web}/src/api/system/notice.js | 0
.../web}/src/api/system/oauthClient.js | 0
.../web}/src/api/system/oauthResource.js | 0
.../web}/src/api/system/oauthSession.js | 0
.../{ => vue2/web}/src/api/system/plugin.js | 0
.../{ => vue2/web}/src/api/system/post.js | 0
.../{ => vue2/web}/src/api/system/role.js | 0
.../{ => vue2/web}/src/api/system/user.js | 0
.../{ => vue2/web}/src/api/tool/gen.js | 0
.../web}/src/assets/401_images/401.gif | Bin
.../web}/src/assets/404_images/404.png | Bin
.../web}/src/assets/404_images/404_cloud.png | Bin
.../vue2/web/src/assets/icons/index.js | 9 +
.../web}/src/assets/icons/svg/404.svg | 0
.../web}/src/assets/icons/svg/ai-chat.svg | 0
.../web}/src/assets/icons/svg/ai-manage.svg | 0
.../web}/src/assets/icons/svg/ai-model.svg | 0
.../web}/src/assets/icons/svg/bell.svg | 0
.../web}/src/assets/icons/svg/bug.svg | 0
.../web}/src/assets/icons/svg/build.svg | 0
.../web}/src/assets/icons/svg/button.svg | 0
.../web}/src/assets/icons/svg/cascader.svg | 0
.../web}/src/assets/icons/svg/chart.svg | 0
.../web}/src/assets/icons/svg/checkbox.svg | 0
.../web}/src/assets/icons/svg/client.svg | 0
.../web}/src/assets/icons/svg/clipboard.svg | 0
.../web}/src/assets/icons/svg/code.svg | 0
.../web}/src/assets/icons/svg/color.svg | 0
.../web}/src/assets/icons/svg/component.svg | 0
.../web}/src/assets/icons/svg/dashboard.svg | 0
.../web}/src/assets/icons/svg/date-range.svg | 0
.../web}/src/assets/icons/svg/date.svg | 0
.../web}/src/assets/icons/svg/deepthink.svg | 0
.../web}/src/assets/icons/svg/dict.svg | 0
.../src/assets/icons/svg/documentation.svg | 0
.../web}/src/assets/icons/svg/download.svg | 0
.../web}/src/assets/icons/svg/drag.svg | 0
.../web}/src/assets/icons/svg/druid.svg | 0
.../web}/src/assets/icons/svg/edit.svg | 0
.../web}/src/assets/icons/svg/education.svg | 0
.../web}/src/assets/icons/svg/email.svg | 0
.../web}/src/assets/icons/svg/enter.svg | 0
.../web}/src/assets/icons/svg/example.svg | 0
.../web}/src/assets/icons/svg/excel.svg | 0
.../src/assets/icons/svg/exit-fullscreen.svg | 0
.../web}/src/assets/icons/svg/eye-open.svg | 0
.../web}/src/assets/icons/svg/eye.svg | 0
.../web}/src/assets/icons/svg/form.svg | 0
.../web}/src/assets/icons/svg/fullscreen.svg | 0
.../web}/src/assets/icons/svg/github.svg | 0
.../web}/src/assets/icons/svg/grant.svg | 0
.../web}/src/assets/icons/svg/guide.svg | 0
.../web}/src/assets/icons/svg/icon.svg | 0
.../web}/src/assets/icons/svg/input.svg | 0
.../src/assets/icons/svg/international.svg | 0
.../web}/src/assets/icons/svg/job.svg | 0
.../web}/src/assets/icons/svg/key.svg | 0
.../web}/src/assets/icons/svg/language.svg | 0
.../web}/src/assets/icons/svg/link.svg | 0
.../web}/src/assets/icons/svg/list.svg | 0
.../web}/src/assets/icons/svg/lock.svg | 0
.../web}/src/assets/icons/svg/log.svg | 0
.../web}/src/assets/icons/svg/logininfor.svg | 0
.../web}/src/assets/icons/svg/message.svg | 0
.../web}/src/assets/icons/svg/money.svg | 0
.../web}/src/assets/icons/svg/monitor.svg | 0
.../web}/src/assets/icons/svg/more-up.svg | 0
.../web}/src/assets/icons/svg/nested.svg | 0
.../web}/src/assets/icons/svg/number.svg | 0
.../web}/src/assets/icons/svg/oauth.svg | 0
.../web}/src/assets/icons/svg/online.svg | 0
.../web}/src/assets/icons/svg/password.svg | 0
.../web}/src/assets/icons/svg/pdf.svg | 0
.../web}/src/assets/icons/svg/people.svg | 0
.../web}/src/assets/icons/svg/peoples.svg | 0
.../web}/src/assets/icons/svg/phone.svg | 0
.../web}/src/assets/icons/svg/post.svg | 0
.../web}/src/assets/icons/svg/qq.svg | 0
.../web}/src/assets/icons/svg/question.svg | 0
.../web}/src/assets/icons/svg/radio.svg | 0
.../web}/src/assets/icons/svg/rate.svg | 0
.../web/src/assets/icons/svg/redis-list.svg | 1 +
.../web}/src/assets/icons/svg/redis.svg | 0
.../web}/src/assets/icons/svg/resource.svg | 0
.../web}/src/assets/icons/svg/row.svg | 0
.../web}/src/assets/icons/svg/scope.svg | 0
.../web}/src/assets/icons/svg/search.svg | 0
.../web}/src/assets/icons/svg/select.svg | 0
.../web}/src/assets/icons/svg/server.svg | 0
.../web}/src/assets/icons/svg/session.svg | 0
.../web}/src/assets/icons/svg/shopping.svg | 0
.../vue2/web/src/assets/icons/svg/size.svg | 1 +
.../web}/src/assets/icons/svg/skill.svg | 0
.../web}/src/assets/icons/svg/slider.svg | 0
.../web}/src/assets/icons/svg/star.svg | 0
.../web}/src/assets/icons/svg/swagger.svg | 0
.../web}/src/assets/icons/svg/switch.svg | 0
.../web}/src/assets/icons/svg/system.svg | 0
.../web}/src/assets/icons/svg/tab.svg | 0
.../web}/src/assets/icons/svg/table.svg | 0
.../web}/src/assets/icons/svg/textarea.svg | 0
.../web}/src/assets/icons/svg/theme.svg | 0
.../web}/src/assets/icons/svg/time-range.svg | 0
.../web}/src/assets/icons/svg/time.svg | 0
.../web}/src/assets/icons/svg/tool.svg | 0
.../web}/src/assets/icons/svg/tree-table.svg | 0
.../web}/src/assets/icons/svg/tree.svg | 0
.../web}/src/assets/icons/svg/upload.svg | 0
.../web}/src/assets/icons/svg/user.svg | 0
.../web}/src/assets/icons/svg/validCode.svg | 0
.../web}/src/assets/icons/svg/wechat.svg | 0
.../web}/src/assets/icons/svg/zip.svg | 0
.../vue2/web/src/assets/icons/svgo.yml | 21 +
.../{ => vue2/web}/src/assets/images/dark.svg | 0
.../web}/src/assets/images/light.svg | 0
.../src/assets/images/login-background.jpg | Bin
.../web}/src/assets/images/profile.jpg | Bin
.../{ => vue2/web}/src/assets/logo/logo.png | Bin
.../vue2/web/src/assets/styles/btn.scss | 99 +
.../web/src/assets/styles/element-ui.scss | 88 +
.../src/assets/styles/element-variables.scss | 31 +
.../vue2/web/src/assets/styles/index.scss | 193 ++
.../web}/src/assets/styles/mixin.scss | 0
.../vue2/web/src/assets/styles/oauth.scss | 182 ++
.../vue2/web/src/assets/styles/ruoyi.scss | 541 ++++
.../vue2/web/src/assets/styles/sidebar.scss | 322 ++
.../web/src/assets/styles/transition.scss | 49 +
.../vue2/web/src/assets/styles/variables.scss | 39 +
.../web/src/components/Breadcrumb/index.vue | 120 +
.../BusinessDateTimePicker/index.vue | 129 +
.../components/BusinessFileUpload/index.vue | 578 ++++
.../vue2/web/src/components/Crontab/day.vue | 218 ++
.../vue2/web/src/components/Crontab/hour.vue | 163 +
.../vue2/web/src/components/Crontab/index.vue | 478 +++
.../vue2/web/src/components/Crontab/min.vue | 157 +
.../vue2/web/src/components/Crontab/month.vue | 157 +
.../web/src/components/Crontab/result.vue | 112 +
.../web/src/components/Crontab/second.vue | 160 +
.../vue2/web/src/components/Crontab/week.vue | 262 ++
.../vue2/web/src/components/Crontab/year.vue | 174 +
.../vue2/web/src/components/DictData/index.js | 53 +
.../vue2/web/src/components/DictTag/index.vue | 104 +
.../vue2/web/src/components/Editor/index.vue | 309 ++
.../components/ExcelImportDialog/index.vue | 178 ++
.../web/src/components/FileUpload/index.vue | 308 ++
.../web/src/components/Hamburger/index.vue | 49 +
.../web/src/components/HeaderSearch/index.vue | 421 +++
.../web/src/components/IconSelect/index.vue | 123 +
.../src/components/IconSelect/requireIcons.js | 10 +
.../web/src/components/ImagePreview/index.vue | 93 +
.../web/src/components/ImageUpload/index.vue | 280 ++
.../web/src/components/JsonEditor/index.vue | 353 +++
.../components/OAuthWorkspace/AccessMap.vue | 127 +
.../components/OAuthWorkspace/FieldLabel.vue | 50 +
.../components/OAuthWorkspace/PageFrame.vue | 259 ++
.../OAuthWorkspace/StringListInput.vue | 99 +
.../web/src/components/Pagination/index.vue | 115 +
.../web/src/components/PanThumb/index.vue | 152 +
.../web}/src/components/ParentView/index.vue | 0
.../web/src/components/PluginFrame/index.vue | 305 ++
.../web/src/components/RightToolbar/index.vue | 293 ++
.../web/src/components/RuoYi/Doc/index.vue | 24 +
.../web/src/components/RuoYi/Git/index.vue | 24 +
.../web/src/components/Screenfull/index.vue | 60 +
.../web/src/components/SizeSelect/index.vue | 66 +
.../vue2/web/src/components/SvgIcon/index.vue | 71 +
.../web/src/components/ThemePicker/index.vue | 179 ++
.../web/src/components/TreePanel/index.vue | 780 +++++
.../vue2/web/src/components/iFrame/index.vue | 39 +
.../vue2/web/src/directive/dialog/drag.js | 63 +
.../web/src/directive/dialog/dragHeight.js | 38 +
.../web/src/directive/dialog/dragWidth.js | 34 +
.../vue2/web/src/directive/index.js | 23 +
.../web/src/directive/module/clipboard.js | 56 +
.../web/src/directive/permission/hasPermi.js | 28 +
.../web/src/directive/permission/hasRole.js | 28 +
.../web/src/layout/components/AppMain.vue | 146 +
.../src/layout/components/Copyright/index.vue | 38 +
.../components/HeaderNotice/DetailView.vue | 400 +++
.../layout/components/HeaderNotice/index.vue | 246 ++
.../layout/components/IframeToggle/index.vue | 38 +
.../src/layout/components/InnerLink/index.vue | 51 +
.../vue2/web/src/layout/components/Navbar.vue | 320 ++
.../src/layout/components/Settings/index.vue | 553 ++++
.../layout/components/Sidebar/FixiOSBug.js | 25 +
.../src/layout/components/Sidebar/Item.vue | 40 +
.../src/layout/components/Sidebar/Link.vue | 46 +
.../src/layout/components/Sidebar/Logo.vue | 148 +
.../layout/components/Sidebar/SidebarItem.vue | 123 +
.../src/layout/components/Sidebar/index.vue | 77 +
.../layout/components/TagsView/ScrollPane.vue | 156 +
.../src/layout/components/TagsView/index.vue | 893 ++++++
.../src/layout/components/TopBar/index.vue | 123 +
.../src/layout/components/TopNav/index.vue | 206 ++
.../vue2/web/src/layout/components/index.js | 5 +
.../vue2/web/src/layout/index.vue | 133 +
.../web/src/layout/mixin/ResizeHandler.js | 45 +
ruoyi-fastapi-frontend/vue2/web/src/main.js | 109 +
.../vue2/web/src/permission.js | 87 +
.../vue2/web/src/plugins/auth.js | 60 +
.../{ => vue2/web}/src/plugins/cache.js | 0
.../vue2/web/src/plugins/download.js | 199 ++
.../vue2/web/src/plugins/index.js | 20 +
.../vue2/web/src/plugins/modal.js | 83 +
.../vue2/web/src/plugins/tab.js | 75 +
.../vue2/web/src/router/index.js | 213 ++
.../vue2/web/src/settings.js | 66 +
.../vue2/web/src/store/getters.js | 23 +
.../vue2/web/src/store/index.js | 27 +
.../vue2/web/src/store/modules/app.js | 66 +
.../vue2/web/src/store/modules/dict.js | 48 +
.../vue2/web/src/store/modules/lock.js | 34 +
.../vue2/web/src/store/modules/permission.js | 139 +
.../vue2/web/src/store/modules/settings.js | 68 +
.../vue2/web/src/store/modules/tagsView.js | 284 ++
.../vue2/web/src/store/modules/user.js | 166 +
.../{ => vue2/web}/src/utils/auth.js | 0
.../web}/src/utils/authCenterAccess.js | 0
.../vue2/web/src/utils/dict/Dict.js | 81 +
.../vue2/web/src/utils/dict/DictConverter.js | 17 +
.../vue2/web/src/utils/dict/DictData.js | 13 +
.../vue2/web/src/utils/dict/DictMeta.js | 37 +
.../vue2/web/src/utils/dict/DictOptions.js | 51 +
.../vue2/web/src/utils/dict/index.js | 37 +
.../vue2/web/src/utils/dynamicTitle.js | 13 +
.../{ => vue2/web}/src/utils/errorCode.js | 0
.../vue2/web/src/utils/generator/config.js | 451 +++
.../{ => vue2/web}/src/utils/generator/css.js | 0
.../web/src/utils/generator/drawingDefault.js | 39 +
.../vue2/web/src/utils/generator/html.js | 371 +++
.../web}/src/utils/generator/icon.json | 0
.../vue2/web/src/utils/generator/js.js | 254 ++
.../vue2/web/src/utils/generator/render.js | 164 +
.../vue2/web/src/utils/index.js | 361 +++
.../{ => vue2/web}/src/utils/job.js | 0
.../{ => vue2/web}/src/utils/jsencrypt.js | 0
.../vue2/web/src/utils/monaco.js | 29 +
.../{ => vue2/web}/src/utils/oauthUri.js | 0
.../vue2/web/src/utils/passwordRule.js | 89 +
.../vue2/web/src/utils/permission.js | 45 +
.../web}/src/utils/pluginBridge.d.ts | 0
.../{ => vue2/web}/src/utils/pluginBridge.js | 0
.../web}/src/utils/pluginBridge.sdk.json | 0
.../web}/src/utils/pluginPlanFormatter.js | 0
.../web}/src/utils/pluginReleaseFormatter.js | 0
.../{ => vue2/web}/src/utils/pluginSse.js | 0
.../vue2/web/src/utils/pluginViewResolver.js | 33 +
.../vue2/web/src/utils/request.js | 264 ++
.../vue2/web/src/utils/ruoyi.js | 204 ++
.../vue2/web/src/utils/scroll-to.js | 69 +
.../vue2/web/src/utils/time.js | 597 ++++
.../vue2/web/src/utils/transportCrypto.js | 753 +++++
.../web/src/utils/transportCryptoPolicy.js | 356 +++
.../{ => vue2/web}/src/utils/transportPath.js | 0
.../{ => vue2/web}/src/utils/validate.js | 0
.../src/views/auth-center/AuthCenterShell.vue | 454 +++
.../src/views/auth-center/changePassword.vue | 402 +++
.../web/src/views/auth-center/consent.vue | 477 +++
.../vue2/web/src/views/auth-center/error.vue | 169 +
.../views/auth-center/interactionSecurity.js | 0
.../vue2/web/src/views/auth-center/login.vue | 344 ++
.../src/views/auth-center/useInteraction.js | 112 +
.../vue2/web/src/views/dashboard/BarChart.vue | 114 +
.../web/src/views/dashboard/LineChart.vue | 141 +
.../web/src/views/dashboard/PanelGroup.vue | 240 ++
.../vue2/web/src/views/dashboard/PieChart.vue | 82 +
.../vue2/web/src/views/dashboard/Radar.vue | 104 +
.../web/src/views/dashboard/RaddarChart.vue | 122 +
.../web/src/views/dashboard/Workplace.less | 112 +
.../vue2/web/src/views/dashboard/index.vue | 622 ++++
.../web/src/views/dashboard/mixins/resize.js | 58 +
.../vue2/web/src/views/error/401.vue | 93 +
.../vue2/web/src/views/error/404.vue | 249 ++
.../vue2/web/src/views/index.vue | 1097 +++++++
.../vue2/web/src/views/index_v1.vue | 107 +
.../vue2/web/src/views/lock.vue | 429 +++
.../vue2/web/src/views/login.vue | 258 ++
.../web/src/views/monitor/cache/index.vue | 258 ++
.../vue2/web/src/views/monitor/cache/list.vue | 275 ++
.../web/src/views/monitor/druid/index.vue | 18 +
.../vue2/web/src/views/monitor/job/detail.vue | 493 +++
.../vue2/web/src/views/monitor/job/index.vue | 1048 ++++++
.../vue2/web/src/views/monitor/job/log.vue | 414 +++
.../web/src/views/monitor/job/runtime.vue | 472 +++
.../src/views/monitor/logininfor/index.vue | 364 +++
.../src/views/monitor/oauthAudit/index.vue | 721 +++++
.../web/src/views/monitor/online/index.vue | 198 ++
.../web/src/views/monitor/operlog/detail.vue | 215 ++
.../web/src/views/monitor/operlog/index.vue | 413 +++
.../web/src/views/monitor/server/index.vue | 433 +++
.../views/monitor/transportCrypto/index.vue | 1258 ++++++++
.../vue2/web/src/views/redirect.vue | 12 +
.../vue2/web/src/views/register.vue | 263 ++
.../web/src/views/system/config/index.vue | 497 +++
.../vue2/web/src/views/system/dept/index.vue | 537 ++++
.../vue2/web/src/views/system/dict/data.vue | 563 ++++
.../vue2/web/src/views/system/dict/detail.vue | 240 ++
.../vue2/web/src/views/system/dict/index.vue | 524 +++
.../system/file/components/FileAclDrawer.vue | 491 +++
.../file/components/FileAuditDrawer.vue | 408 +++
.../file/components/FileDetailDialog.vue | 170 +
.../file/components/FileReconcileDrawer.vue | 902 ++++++
.../file/components/FileReferenceDrawer.vue | 134 +
.../components/FileRetentionPolicyDrawer.vue | 307 ++
.../FileRetentionReminderDrawer.vue | 485 +++
.../system/file/components/FileSearchForm.vue | 202 ++
.../system/file/components/FileStatistics.vue | 282 ++
.../system/file/components/FileTable.vue | 353 +++
.../file/components/FileTransferDialog.vue | 201 ++
.../system/file/components/fileFormatters.js | 163 +
.../vue2/web/src/views/system/file/index.vue | 382 +++
.../vue2/web/src/views/system/menu/index.vue | 765 +++++
.../web/src/views/system/notice/ReadUsers.vue | 178 ++
.../web/src/views/system/notice/index.vue | 460 +++
.../src/views/system/oauth/client/index.vue | 1280 ++++++++
.../src/views/system/oauth/client/secret.vue | 116 +
.../system/oauth/grant/AccessPolicyDrawer.vue | 386 +++
.../src/views/system/oauth/grant/index.vue | 759 +++++
.../web/src/views/system/oauth/key/index.vue | 914 ++++++
.../src/views/system/oauth/resource/index.vue | 713 +++++
.../src/views/system/oauth/scope/index.vue | 753 +++++
.../src/views/system/oauth/session/index.vue | 631 ++++
.../plugin/components/PluginConfigDialog.vue | 480 +++
.../plugin/components/PluginConfigStatus.vue | 183 ++
.../components/PluginDependencyDialog.vue | 501 +++
.../plugin/components/PluginDetailDialog.vue | 861 +++++
.../components/PluginDiagnosticDialog.vue | 297 ++
.../plugin/components/PluginPlanDialog.vue | 383 +++
.../plugin/components/PluginReleaseStatus.vue | 34 +
.../components/PluginRuntimeMetrics.vue | 222 ++
.../web/src/views/system/plugin/index.vue | 2821 +++++++++++++++++
.../vue2/web/src/views/system/post/index.vue | 441 +++
.../web/src/views/system/role/authUser.vue | 283 ++
.../vue2/web/src/views/system/role/index.vue | 815 +++++
.../web/src/views/system/role/selectUser.vue | 208 ++
.../web/src/views/system/user/authRole.vue | 187 ++
.../vue2/web/src/views/system/user/index.vue | 830 +++++
.../src/views/system/user/profile/index.vue | 129 +
.../views/system/user/profile/resetPwd.vue | 109 +
.../system/user/profile/timezoneSettings.vue | 192 ++
.../views/system/user/profile/userAvatar.vue | 260 ++
.../views/system/user/profile/userInfo.vue | 124 +
.../vue2/web/src/views/system/user/view.vue | 231 ++
.../src/views/tool/build/CodeTypeDialog.vue | 123 +
.../src/views/tool/build/DraggableItem.vue | 136 +
.../web/src/views/tool/build/IconsDialog.vue | 123 +
.../web/src/views/tool/build/RightPanel.vue | 1348 ++++++++
.../src/views/tool/build/TreeNodeDialog.vue | 146 +
.../vue2/web/src/views/tool/build/index.vue | 822 +++++
.../web/src/views/tool/gen/basicInfoForm.vue | 88 +
.../web/src/views/tool/gen/createTable.vue | 98 +
.../vue2/web/src/views/tool/gen/editTable.vue | 437 +++
.../web/src/views/tool/gen/genInfoForm.vue | 425 +++
.../web/src/views/tool/gen/importTable.vue | 241 ++
.../vue2/web/src/views/tool/gen/index.vue | 516 +++
.../vue2/web/src/views/tool/swagger/index.vue | 15 +
.../tests/auth-center/availability.test.cjs | 52 +
.../auth-center/interactionSecurity.test.cjs | 27 +
.../web/tests/auth-center/oauthUri.test.cjs | 45 +
.../auth-center/vue2-components.test.cjs | 246 ++
.../web/tests/job/job-form.test.mjs} | 0
.../web/tests/job/vue2-components.test.cjs | 426 +++
.../fixtures/bridge-b0d16a3e/.gitattributes | 0
.../fixtures/bridge-b0d16a3e/pluginBridge.js | 0
.../fixtures/bridge-b0d16a3e/provenance.json | 0
.../tests/plugins/fixtures/json-only-host.js | 0
.../tests/plugins/network-smoke/server.mjs | 134 +
.../web/tests/plugins/pluginBridge.test.mjs} | 0
.../pluginBridgeCompatibility.test.mjs} | 0
.../tests/plugins/pluginConfigStatus.test.mjs | 77 +
.../web/tests/plugins/pluginDevMock.test.mjs | 77 +
.../web/tests/plugins/pluginFrame.test.mjs | 71 +
.../plugins/pluginHostIntegration.test.cjs | 297 ++
.../plugins/pluginReleaseFormatter.test.mjs | 116 +
.../plugins/pluginRuntimeMetrics.test.mjs | 79 +
.../web/tests/plugins/pluginSse.test.mjs} | 0
.../tests/plugins/pluginViewResolver.test.js | 14 +
.../plugins/pluginVue2Components.test.cjs | 51 +
.../web/tests/plugins/run-plugin-tests.js | 40 +
.../vue2/web/tests/support/load-module.cjs | 55 +
.../vue2/web/tests/time/time.test.mjs | 3 +
ruoyi-fastapi-frontend/vue2/web/vue.config.js | 445 +++
.../vue3/mobile}/.editorconfig | 0
.../vue3/mobile}/.gitignore | 0
.../vue3/mobile}/.npmrc | 0
.../vue3/mobile}/README.md | 14 +-
.../vue3/mobile}/index.html | 0
.../vue3/mobile}/netlify.toml | 0
.../vue3/mobile}/package.json | 5 +-
.../vue3/mobile}/platform.ts | 0
.../vue3/mobile}/postcss.config.ts | 0
.../vue3/mobile}/src/App.vue | 0
.../vue3/mobile/src/api/login.js | 62 +
.../vue3/mobile/src/api/system/dict/data.js | 52 +
.../vue3/mobile/src/api/system/dict/type.js | 60 +
.../vue3/mobile/src/api/system/user.js | 55 +
.../vue3/mobile}/src/auto-imports.d.ts | 0
.../vue3/mobile}/src/config.js | 0
.../vue3/mobile}/src/env.d.ts | 0
.../vue3/mobile}/src/main.ts | 0
.../vue3/mobile}/src/manifest.json | 0
.../vue3/mobile/src/pages.json | 138 +
.../src/pages/common/agreement/index.vue | 0
.../src/pages/common/privacy/index.vue | 0
.../src/pages/common/textview/index.vue | 0
.../src/pages/common/webview/index.vue | 0
.../vue3/mobile}/src/pages/index.vue | 0
.../vue3/mobile}/src/pages/login.vue | 0
.../mobile}/src/pages/mine/about/index.vue | 0
.../mobile}/src/pages/mine/avatar/index.vue | 1356 ++++----
.../mobile}/src/pages/mine/help/index.vue | 182 +-
.../vue3/mobile}/src/pages/mine/index.vue | 418 +--
.../vue3/mobile}/src/pages/mine/info/edit.vue | 0
.../mobile}/src/pages/mine/info/index.vue | 0
.../vue3/mobile}/src/pages/mine/pwd/index.vue | 0
.../mobile}/src/pages/mine/setting/index.vue | 0
.../src/pages/mine/setting/timezone.vue | 0
.../vue3/mobile}/src/pages/register.vue | 0
.../vue3/mobile}/src/pages/work/index.vue | 0
.../vue3/mobile/src/permission.js | 44 +
.../vue3/mobile}/src/plugins/auth.js | 120 +-
.../vue3/mobile}/src/plugins/index.js | 24 +-
.../vue3/mobile/src/plugins/modal.js | 78 +
.../vue3/mobile/src/plugins/tab.js | 30 +
.../vue3/mobile/src/static/favicon.ico | Bin 0 -> 10729 bytes
.../src/static/images/banner/banner01.jpg | Bin 0 -> 39640 bytes
.../src/static/images/banner/banner02.jpg | Bin 0 -> 37061 bytes
.../src/static/images/banner/banner03.jpg | Bin 0 -> 38123 bytes
.../vue3/mobile/src/static/images/profile.jpg | Bin 0 -> 8997 bytes
.../mobile/src/static/images/tabbar/home.png | Bin 0 -> 3265 bytes
.../mobile/src/static/images/tabbar/home_.png | Bin 0 -> 3229 bytes
.../mobile/src/static/images/tabbar/mine.png | Bin 0 -> 4235 bytes
.../mobile/src/static/images/tabbar/mine_.png | Bin 0 -> 4219 bytes
.../mobile/src/static/images/tabbar/work.png | Bin 0 -> 4108 bytes
.../mobile/src/static/images/tabbar/work_.png | Bin 0 -> 5008 bytes
.../vue3/mobile/src/static/logo.png | Bin 0 -> 3501 bytes
.../vue3/mobile/src/static/logo200.png | Bin 0 -> 11619 bytes
.../vue3/mobile}/src/store/index.js | 0
.../vue3/mobile}/src/store/modules/config.js | 0
.../vue3/mobile}/src/store/modules/dict.js | 0
.../vue3/mobile}/src/store/modules/user.js | 0
.../vue3/mobile}/src/theme.json | 0
.../vue3/mobile/src/uni.scss | 0
.../vue3/mobile/src/utils/auth.js | 13 +
.../vue3/mobile/src/utils/common.js | 58 +
.../vue3/mobile/src/utils/constant.js | 10 +
.../vue3/mobile}/src/utils/dict.js | 0
.../vue3/mobile/src/utils/errorCode.js | 6 +
.../vue3/mobile/src/utils/permission.js | 55 +
.../vue3/mobile}/src/utils/request.js | 0
.../vue3/mobile/src/utils/storage.js | 39 +
.../vue3/mobile}/src/utils/time.js | 0
.../vue3/mobile/src/utils/transportCrypto.js | 1236 ++++++++
.../mobile/src/utils/transportCryptoPolicy.js | 438 +++
.../vue3/mobile/src/utils/transportForge.js | 214 ++
.../vue3/mobile}/src/utils/upload.js | 144 +-
.../vue3/mobile/src/utils/validate.js | 124 +
.../vue3/mobile}/stylelint.config.mjs | 0
.../vue3/mobile}/tailwind.config.ts | 0
.../vue3/mobile/tests/behavior.test.mjs | 3 +
.../vue3/mobile}/tests/time-polyfill.test.js | 2 +-
.../vue3/mobile/tests/time.test.js | 3 +
.../vue3/mobile}/tsconfig.json | 0
.../vue3/mobile}/vite.config.ts | 0
ruoyi-fastapi-frontend/vue3/web/.dockerignore | 6 +
ruoyi-fastapi-frontend/vue3/web/.editorconfig | 15 +
.../{ => vue3/web}/.env.development | 0
.../{ => vue3/web}/.env.docker | 0
.../{ => vue3/web}/.env.production | 0
.../{ => vue3/web}/.env.staging | 0
.../vue3/web/.gitattributes | 6 +
ruoyi-fastapi-frontend/vue3/web/.gitignore | 23 +
.../{ => vue3/web}/.prettierignore | 0
.../vue3/web/.prettierrc.json | 17 +
.../{ => vue3/web}/Dockerfile | 0
.../{ => vue3/web}/FORMATTING.md | 8 +-
ruoyi-fastapi-frontend/{ => vue3/web}/LICENSE | 0
.../{ => vue3/web}/bin/build.bat | 0
.../{ => vue3/web}/bin/nginx.dockermy.conf | 0
.../{ => vue3/web}/bin/nginx.dockerpg.conf | 0
.../{ => vue3/web}/bin/package.bat | 0
.../{ => vue3/web}/bin/run-web.bat | 0
.../{ => vue3/web}/frontend.code-workspace | 2 +-
ruoyi-fastapi-frontend/vue3/web/html/ie.html | 46 +
.../{ => vue3/web}/index.html | 0
.../{ => vue3/web}/package.json | 7 +-
.../{ => vue3/web}/plugins/ai/README.md | 9 +-
.../vue3/web/plugins/ai/api/chat.js | 53 +
.../vue3/web/plugins/ai/api/model.js | 52 +
.../ai/views/chat/components/AiMessage.vue | 0
.../web}/plugins/ai/views/chat/index.vue | 0
.../web}/plugins/ai/views/model/index.vue | 0
.../vue3/web/public/favicon.ico | Bin 0 -> 10729 bytes
.../web}/public/monaco-environment.js | 0
.../{ => vue3/web}/src/App.vue | 0
.../vue3/web/src/api/authCenter.js | 91 +
.../vue3/web/src/api/login.js | 70 +
.../vue3/web/src/api/menu.js | 9 +
.../vue3/web/src/api/monitor/cache.js | 57 +
.../vue3/web/src/api/monitor/job.js | 119 +
.../vue3/web/src/api/monitor/jobLog.js | 26 +
.../vue3/web/src/api/monitor/logininfor.js | 34 +
.../vue3/web/src/api/monitor/oauthAudit.js | 10 +
.../vue3/web/src/api/monitor/online.js | 18 +
.../vue3/web/src/api/monitor/operlog.js | 26 +
.../vue3/web/src/api/monitor/server.js | 9 +
.../web/src/api/monitor/transportCrypto.js | 9 +
.../vue3/web/src/api/system/config.js | 60 +
.../vue3/web/src/api/system/dept.js | 61 +
.../vue3/web/src/api/system/dict/data.js | 52 +
.../vue3/web/src/api/system/dict/type.js | 60 +
.../vue3/web/src/api/system/file.js | 241 ++
.../vue3/web/src/api/system/menu.js | 69 +
.../vue3/web/src/api/system/notice.js | 79 +
.../vue3/web/src/api/system/oauthClient.js | 92 +
.../vue3/web/src/api/system/oauthResource.js | 110 +
.../vue3/web/src/api/system/oauthSession.js | 127 +
.../vue3/web/src/api/system/plugin.js | 228 ++
.../vue3/web/src/api/system/post.js | 44 +
.../vue3/web/src/api/system/role.js | 119 +
.../vue3/web/src/api/system/user.js | 146 +
.../vue3/web/src/api/tool/gen.js | 95 +
.../vue3/web/src/assets/401_images/401.gif | Bin 0 -> 164227 bytes
.../vue3/web/src/assets/404_images/404.png | Bin 0 -> 98071 bytes
.../web/src/assets/404_images/404_cloud.png | Bin 0 -> 4766 bytes
.../vue3/web/src/assets/icons/svg/404.svg | 1 +
.../vue3/web/src/assets/icons/svg/ai-chat.svg | 1 +
.../web/src/assets/icons/svg/ai-manage.svg | 1 +
.../web/src/assets/icons/svg/ai-model.svg | 1 +
.../vue3/web/src/assets/icons/svg/bell.svg | 1 +
.../vue3/web/src/assets/icons/svg/bug.svg | 1 +
.../vue3/web/src/assets/icons/svg/build.svg | 1 +
.../vue3/web/src/assets/icons/svg/button.svg | 1 +
.../web/src/assets/icons/svg/cascader.svg | 1 +
.../vue3/web/src/assets/icons/svg/chart.svg | 1 +
.../web/src/assets/icons/svg/checkbox.svg | 1 +
.../vue3/web/src/assets/icons/svg/client.svg | 1 +
.../web/src/assets/icons/svg/clipboard.svg | 1 +
.../vue3/web/src/assets/icons/svg/code.svg | 1 +
.../vue3/web/src/assets/icons/svg/color.svg | 1 +
.../web/src/assets/icons/svg/component.svg | 1 +
.../web/src/assets/icons/svg/dashboard.svg | 1 +
.../web/src/assets/icons/svg/date-range.svg | 1 +
.../vue3/web/src/assets/icons/svg/date.svg | 1 +
.../web/src/assets/icons/svg/deepthink.svg | 1 +
.../vue3/web/src/assets/icons/svg/dict.svg | 1 +
.../src/assets/icons/svg/documentation.svg | 1 +
.../web/src/assets/icons/svg/download.svg | 1 +
.../vue3/web/src/assets/icons/svg/drag.svg | 1 +
.../vue3/web/src/assets/icons/svg/druid.svg | 1 +
.../vue3/web/src/assets/icons/svg/edit.svg | 1 +
.../web/src/assets/icons/svg/education.svg | 1 +
.../vue3/web/src/assets/icons/svg/email.svg | 1 +
.../vue3/web/src/assets/icons/svg/enter.svg | 1 +
.../vue3/web/src/assets/icons/svg/example.svg | 1 +
.../vue3/web/src/assets/icons/svg/excel.svg | 1 +
.../src/assets/icons/svg/exit-fullscreen.svg | 1 +
.../web/src/assets/icons/svg/eye-open.svg | 1 +
.../vue3/web/src/assets/icons/svg/eye.svg | 1 +
.../vue3/web/src/assets/icons/svg/form.svg | 1 +
.../web/src/assets/icons/svg/fullscreen.svg | 1 +
.../vue3/web/src/assets/icons/svg/github.svg | 1 +
.../vue3/web/src/assets/icons/svg/grant.svg | 1 +
.../vue3/web/src/assets/icons/svg/guide.svg | 1 +
.../vue3/web/src/assets/icons/svg/icon.svg | 1 +
.../vue3/web/src/assets/icons/svg/input.svg | 1 +
.../src/assets/icons/svg/international.svg | 1 +
.../vue3/web/src/assets/icons/svg/job.svg | 1 +
.../vue3/web/src/assets/icons/svg/key.svg | 1 +
.../web/src/assets/icons/svg/language.svg | 1 +
.../vue3/web/src/assets/icons/svg/link.svg | 1 +
.../vue3/web/src/assets/icons/svg/list.svg | 1 +
.../vue3/web/src/assets/icons/svg/lock.svg | 1 +
.../vue3/web/src/assets/icons/svg/log.svg | 1 +
.../web/src/assets/icons/svg/logininfor.svg | 1 +
.../vue3/web/src/assets/icons/svg/message.svg | 1 +
.../vue3/web/src/assets/icons/svg/money.svg | 1 +
.../vue3/web/src/assets/icons/svg/monitor.svg | 2 +
.../web}/src/assets/icons/svg/moon.svg | 0
.../vue3/web/src/assets/icons/svg/more-up.svg | 1 +
.../vue3/web/src/assets/icons/svg/nested.svg | 1 +
.../vue3/web/src/assets/icons/svg/number.svg | 1 +
.../vue3/web/src/assets/icons/svg/oauth.svg | 1 +
.../vue3/web/src/assets/icons/svg/online.svg | 1 +
.../web/src/assets/icons/svg/password.svg | 1 +
.../vue3/web/src/assets/icons/svg/pdf.svg | 1 +
.../vue3/web/src/assets/icons/svg/people.svg | 1 +
.../vue3/web/src/assets/icons/svg/peoples.svg | 1 +
.../vue3/web/src/assets/icons/svg/phone.svg | 1 +
.../vue3/web/src/assets/icons/svg/post.svg | 1 +
.../vue3/web/src/assets/icons/svg/qq.svg | 1 +
.../web/src/assets/icons/svg/question.svg | 1 +
.../vue3/web/src/assets/icons/svg/radio.svg | 1 +
.../vue3/web/src/assets/icons/svg/rate.svg | 1 +
.../web}/src/assets/icons/svg/redis-list.svg | 0
.../vue3/web/src/assets/icons/svg/redis.svg | 1 +
.../web/src/assets/icons/svg/resource.svg | 1 +
.../vue3/web/src/assets/icons/svg/row.svg | 1 +
.../vue3/web/src/assets/icons/svg/scope.svg | 1 +
.../vue3/web/src/assets/icons/svg/search.svg | 1 +
.../vue3/web/src/assets/icons/svg/select.svg | 1 +
.../vue3/web/src/assets/icons/svg/server.svg | 1 +
.../vue3/web/src/assets/icons/svg/session.svg | 1 +
.../web/src/assets/icons/svg/shopping.svg | 1 +
.../web}/src/assets/icons/svg/size.svg | 0
.../vue3/web/src/assets/icons/svg/skill.svg | 1 +
.../vue3/web/src/assets/icons/svg/slider.svg | 1 +
.../vue3/web/src/assets/icons/svg/star.svg | 1 +
.../web}/src/assets/icons/svg/sunny.svg | 0
.../vue3/web/src/assets/icons/svg/swagger.svg | 1 +
.../vue3/web/src/assets/icons/svg/switch.svg | 1 +
.../vue3/web/src/assets/icons/svg/system.svg | 2 +
.../vue3/web/src/assets/icons/svg/tab.svg | 1 +
.../vue3/web/src/assets/icons/svg/table.svg | 1 +
.../web/src/assets/icons/svg/textarea.svg | 1 +
.../vue3/web/src/assets/icons/svg/theme.svg | 1 +
.../web/src/assets/icons/svg/time-range.svg | 1 +
.../vue3/web/src/assets/icons/svg/time.svg | 1 +
.../vue3/web/src/assets/icons/svg/tool.svg | 1 +
.../web/src/assets/icons/svg/tree-table.svg | 1 +
.../vue3/web/src/assets/icons/svg/tree.svg | 1 +
.../vue3/web/src/assets/icons/svg/upload.svg | 1 +
.../vue3/web/src/assets/icons/svg/user.svg | 1 +
.../web/src/assets/icons/svg/validCode.svg | 1 +
.../vue3/web/src/assets/icons/svg/wechat.svg | 1 +
.../vue3/web/src/assets/icons/svg/zip.svg | 1 +
.../vue3/web/src/assets/images/dark.svg | 39 +
.../vue3/web/src/assets/images/light.svg | 39 +
.../src/assets/images/login-background.jpg | Bin 0 -> 521275 bytes
.../vue3/web/src/assets/images/profile.jpg | Bin 0 -> 8997 bytes
.../vue3/web/src/assets/logo/logo.png | Bin 0 -> 3501 bytes
.../{ => vue3/web}/src/assets/styles/btn.scss | 0
.../web}/src/assets/styles/element-ui.scss | 0
.../web}/src/assets/styles/index.scss | 0
.../vue3/web/src/assets/styles/mixin.scss | 60 +
.../web}/src/assets/styles/ruoyi.scss | 0
.../web}/src/assets/styles/sidebar.scss | 0
.../web}/src/assets/styles/transition.scss | 0
.../src/assets/styles/variables.module.scss | 0
.../web}/src/components/Breadcrumb/index.vue | 0
.../BusinessDateTimePicker/index.vue | 0
.../components/BusinessFileUpload/index.vue | 0
.../web}/src/components/Crontab/day.vue | 0
.../web}/src/components/Crontab/hour.vue | 0
.../web}/src/components/Crontab/index.vue | 0
.../web}/src/components/Crontab/min.vue | 0
.../web}/src/components/Crontab/month.vue | 0
.../web}/src/components/Crontab/result.vue | 0
.../web}/src/components/Crontab/second.vue | 0
.../web}/src/components/Crontab/week.vue | 0
.../web}/src/components/Crontab/year.vue | 0
.../web}/src/components/DictTag/index.vue | 0
.../web}/src/components/Editor/index.vue | 0
.../components/ExcelImportDialog/index.vue | 0
.../web}/src/components/FileUpload/index.vue | 0
.../web}/src/components/Hamburger/index.vue | 0
.../src/components/HeaderSearch/index.vue | 0
.../web}/src/components/IconSelect/index.vue | 0
.../src/components/IconSelect/requireIcons.js | 0
.../src/components/ImagePreview/index.vue | 0
.../web}/src/components/ImageUpload/index.vue | 0
.../web}/src/components/JsonEditor/index.vue | 0
.../components/OAuthWorkspace/AccessMap.vue | 0
.../components/OAuthWorkspace/FieldLabel.vue | 0
.../components/OAuthWorkspace/PageFrame.vue | 0
.../OAuthWorkspace/StringListInput.vue | 0
.../web}/src/components/Pagination/index.vue | 0
.../web/src/components/ParentView/index.vue | 3 +
.../web}/src/components/PluginFrame/index.vue | 0
.../src/components/RightToolbar/index.vue | 0
.../web}/src/components/RuoYi/Doc/index.vue | 0
.../web}/src/components/RuoYi/Git/index.vue | 2 +-
.../web}/src/components/Screenfull/index.vue | 0
.../web}/src/components/SizeSelect/index.vue | 0
.../web}/src/components/SvgIcon/index.vue | 0
.../web}/src/components/SvgIcon/svgicon.js | 0
.../web}/src/components/TreePanel/index.vue | 0
.../web}/src/components/iFrame/index.vue | 0
.../web}/src/directive/common/copyText.js | 0
.../{ => vue3/web}/src/directive/index.js | 0
.../web}/src/directive/permission/hasPermi.js | 0
.../web}/src/directive/permission/hasRole.js | 0
.../web}/src/layout/components/AppMain.vue | 0
.../src/layout/components/Copyright/index.vue | 0
.../components/HeaderNotice/DetailView.vue | 0
.../layout/components/HeaderNotice/index.vue | 0
.../layout/components/IframeToggle/index.vue | 0
.../src/layout/components/InnerLink/index.vue | 0
.../web}/src/layout/components/Navbar.vue | 0
.../src/layout/components/Settings/index.vue | 0
.../src/layout/components/Sidebar/Link.vue | 0
.../src/layout/components/Sidebar/Logo.vue | 0
.../layout/components/Sidebar/SidebarItem.vue | 0
.../src/layout/components/Sidebar/index.vue | 0
.../layout/components/TagsView/ScrollPane.vue | 0
.../src/layout/components/TagsView/index.vue | 0
.../src/layout/components/TopBar/index.vue | 0
.../src/layout/components/TopNav/index.vue | 0
.../web}/src/layout/components/index.js | 0
.../{ => vue3/web}/src/layout/index.vue | 0
.../{ => vue3/web}/src/main.js | 0
.../{ => vue3/web}/src/permission.js | 0
.../{ => vue3/web}/src/plugins/auth.js | 0
.../vue3/web/src/plugins/cache.js | 79 +
.../{ => vue3/web}/src/plugins/download.js | 0
.../{ => vue3/web}/src/plugins/index.js | 0
.../{ => vue3/web}/src/plugins/modal.js | 0
.../{ => vue3/web}/src/plugins/tab.js | 0
.../{ => vue3/web}/src/router/index.js | 0
.../{ => vue3/web}/src/settings.js | 0
.../{ => vue3/web}/src/store/index.js | 0
.../{ => vue3/web}/src/store/modules/app.js | 0
.../{ => vue3/web}/src/store/modules/dict.js | 0
.../{ => vue3/web}/src/store/modules/lock.js | 0
.../web}/src/store/modules/permission.js | 0
.../web}/src/store/modules/settings.js | 0
.../web}/src/store/modules/tagsView.js | 0
.../{ => vue3/web}/src/store/modules/user.js | 0
.../vue3/web/src/utils/auth.js | 15 +
.../vue3/web/src/utils/authCenterAccess.js | 25 +
.../{ => vue3/web}/src/utils/dict.js | 0
.../{ => vue3/web}/src/utils/dynamicTitle.js | 0
.../vue3/web/src/utils/errorCode.js | 6 +
.../web}/src/utils/generator/config.js | 0
.../vue3/web/src/utils/generator/css.js | 18 +
.../src/utils/generator/drawingDefault.js | 0
.../web}/src/utils/generator/html.js | 0
.../vue3/web/src/utils/generator/icon.json | 282 ++
.../{ => vue3/web}/src/utils/generator/js.js | 0
.../web}/src/utils/generator/render.js | 0
.../{ => vue3/web}/src/utils/index.js | 0
.../vue3/web/src/utils/job.js | 67 +
.../vue3/web/src/utils/jsencrypt.js | 31 +
.../vue3/web/src/utils/oauthUri.js | 82 +
.../{ => vue3/web}/src/utils/passwordRule.js | 0
.../{ => vue3/web}/src/utils/permission.js | 0
.../vue3/web/src/utils/pluginBridge.d.ts | 156 +
.../vue3/web/src/utils/pluginBridge.js | 884 ++++++
.../vue3/web/src/utils/pluginBridge.sdk.json | 9 +
.../vue3/web/src/utils/pluginPlanFormatter.js | 196 ++
.../web/src/utils/pluginReleaseFormatter.js | 51 +
.../vue3/web/src/utils/pluginSse.js | 134 +
.../web}/src/utils/pluginViewResolver.js | 0
.../{ => vue3/web}/src/utils/request.js | 0
.../{ => vue3/web}/src/utils/ruoyi.js | 0
.../{ => vue3/web}/src/utils/scroll-to.js | 0
.../{ => vue3/web}/src/utils/theme.js | 0
.../{ => vue3/web}/src/utils/time.js | 0
.../web}/src/utils/transportCrypto.js | 0
.../web}/src/utils/transportCryptoPolicy.js | 0
.../vue3/web/src/utils/transportPath.js | 29 +
.../vue3/web/src/utils/validate.js | 121 +
.../src/views/auth-center/AuthCenterShell.vue | 0
.../src/views/auth-center/changePassword.vue | 0
.../web}/src/views/auth-center/consent.vue | 0
.../web}/src/views/auth-center/error.vue | 0
.../views/auth-center/interactionSecurity.js | 45 +
.../web}/src/views/auth-center/login.vue | 0
.../src/views/auth-center/useInteraction.js | 0
.../views/dashboard/editable-link-group.vue | 0
.../web}/src/views/dashboard/index.vue | 0
.../{ => vue3/web}/src/views/error/401.vue | 0
.../{ => vue3/web}/src/views/error/404.vue | 0
.../{ => vue3/web}/src/views/lock.vue | 0
.../{ => vue3/web}/src/views/login.vue | 0
.../web}/src/views/monitor/cache/index.vue | 0
.../web}/src/views/monitor/cache/list.vue | 0
.../web}/src/views/monitor/druid/index.vue | 0
.../web}/src/views/monitor/job/detail.vue | 0
.../web}/src/views/monitor/job/index.vue | 0
.../web}/src/views/monitor/job/log.vue | 0
.../web}/src/views/monitor/job/runtime.vue | 0
.../src/views/monitor/logininfor/index.vue | 0
.../src/views/monitor/oauthAudit/index.vue | 0
.../web}/src/views/monitor/online/index.vue | 0
.../web}/src/views/monitor/operlog/detail.vue | 0
.../web}/src/views/monitor/operlog/index.vue | 0
.../web}/src/views/monitor/server/index.vue | 0
.../views/monitor/transportCrypto/index.vue | 0
.../web}/src/views/redirect/index.vue | 0
.../{ => vue3/web}/src/views/register.vue | 0
.../web}/src/views/system/config/index.vue | 0
.../web}/src/views/system/dept/index.vue | 0
.../web}/src/views/system/dict/data.vue | 0
.../web}/src/views/system/dict/detail.vue | 0
.../web}/src/views/system/dict/index.vue | 0
.../system/file/components/FileAclDrawer.vue | 0
.../file/components/FileAuditDrawer.vue | 0
.../file/components/FileDetailDialog.vue | 0
.../file/components/FileReconcileDrawer.vue | 0
.../file/components/FileReferenceDrawer.vue | 0
.../components/FileRetentionPolicyDrawer.vue | 0
.../FileRetentionReminderDrawer.vue | 0
.../system/file/components/FileSearchForm.vue | 0
.../system/file/components/FileStatistics.vue | 0
.../system/file/components/FileTable.vue | 0
.../file/components/FileTransferDialog.vue | 0
.../system/file/components/fileFormatters.js | 0
.../web}/src/views/system/file/index.vue | 0
.../web}/src/views/system/menu/index.vue | 0
.../src/views/system/notice/ReadUsers.vue | 0
.../web}/src/views/system/notice/index.vue | 0
.../src/views/system/oauth/client/index.vue | 0
.../src/views/system/oauth/client/secret.vue | 0
.../system/oauth/grant/AccessPolicyDrawer.vue | 0
.../src/views/system/oauth/grant/index.vue | 0
.../web}/src/views/system/oauth/key/index.vue | 0
.../src/views/system/oauth/resource/index.vue | 0
.../src/views/system/oauth/scope/index.vue | 0
.../src/views/system/oauth/session/index.vue | 0
.../plugin/components/PluginConfigDialog.vue | 0
.../plugin/components/PluginConfigStatus.vue | 0
.../components/PluginDependencyDialog.vue | 0
.../plugin/components/PluginDetailDialog.vue | 0
.../components/PluginDiagnosticDialog.vue | 0
.../plugin/components/PluginPlanDialog.vue | 0
.../plugin/components/PluginReleaseStatus.vue | 0
.../components/PluginRuntimeMetrics.vue | 0
.../web}/src/views/system/plugin/index.vue | 0
.../web}/src/views/system/post/index.vue | 0
.../web}/src/views/system/role/authUser.vue | 0
.../web}/src/views/system/role/index.vue | 13 +
.../web}/src/views/system/role/selectUser.vue | 0
.../web}/src/views/system/user/authRole.vue | 0
.../web}/src/views/system/user/index.vue | 0
.../src/views/system/user/profile/index.vue | 0
.../views/system/user/profile/resetPwd.vue | 0
.../system/user/profile/timezoneSettings.vue | 0
.../views/system/user/profile/userAvatar.vue | 0
.../views/system/user/profile/userInfo.vue | 0
.../web}/src/views/system/user/view.vue | 0
.../src/views/tool/build/CodeTypeDialog.vue | 0
.../src/views/tool/build/DraggableItem.vue | 0
.../web}/src/views/tool/build/IconsDialog.vue | 0
.../web}/src/views/tool/build/RightPanel.vue | 0
.../src/views/tool/build/TreeNodeDialog.vue | 0
.../web}/src/views/tool/build/index.vue | 0
.../web}/src/views/tool/gen/basicInfoForm.vue | 0
.../web}/src/views/tool/gen/createTable.vue | 0
.../web}/src/views/tool/gen/editTable.vue | 0
.../web}/src/views/tool/gen/genInfoForm.vue | 0
.../web}/src/views/tool/gen/importTable.vue | 0
.../web}/src/views/tool/gen/index.vue | 0
.../web}/src/views/tool/swagger/index.vue | 0
.../tests/auth-center/availability.test.js | 0
.../web}/tests/auth-center/clientForm.test.js | 0
.../auth-center/interactionSecurity.test.js | 0
.../web}/tests/auth-center/oauthUri.test.js | 0
.../tests/auth-center/useInteraction.test.js | 0
.../vue3/web/tests/job/job-form.test.js | 39 +
.../fixtures/bridge-b0d16a3e/.gitattributes | 1 +
.../fixtures/bridge-b0d16a3e/pluginBridge.js | 882 ++++++
.../fixtures/bridge-b0d16a3e/provenance.json | 8 +
.../tests/plugins/fixtures/json-only-host.js | 61 +
.../tests/plugins/monacoEnvironment.test.js | 0
.../tests/plugins/network-smoke/server.mjs | 0
.../web/tests/plugins/pluginBridge.test.js | 950 ++++++
.../plugins/pluginBridgeCompatibility.test.js | 226 ++
.../tests/plugins/pluginConfigStatus.test.js | 0
.../web}/tests/plugins/pluginDevMock.test.js | 2 +-
.../web}/tests/plugins/pluginFrame.test.js | 0
.../web}/tests/plugins/pluginProxy.test.js | 2 +-
.../plugins/pluginReleaseFormatter.test.js | 0
.../plugins/pluginRuntimeMetrics.test.js | 0
.../vue3/web/tests/plugins/pluginSse.test.js | 169 +
.../tests/plugins/pluginViewResolver.test.js | 0
.../web}/tests/plugins/run-plugin-tests.js | 0
.../{ => vue3/web}/tests/run-tests.js | 0
.../vue3/web/tests/time/time.test.js | 3 +
.../{ => vue3/web}/vite.config.js | 0
.../web}/vite/plugins/auto-import.js | 0
.../web}/vite/plugins/compression.js | 0
.../{ => vue3/web}/vite/plugins/index.js | 0
.../web}/vite/plugins/monaco-environment.js | 0
.../web}/vite/plugins/setup-extend.js | 0
.../{ => vue3/web}/vite/plugins/svg-icon.js | 0
ruoyi-fastapi-test/README.md | 180 +-
ruoyi-fastapi-test/common/base_page_test.py | 52 +-
ruoyi-fastapi-test/common/browser_harness.py | 188 ++
ruoyi-fastapi-test/common/config.py | 1 +
ruoyi-fastapi-test/common/page_catalog.py | 45 +
ruoyi-fastapi-test/conftest.py | 59 +
ruoyi-fastapi-test/docker-compose.test.my.yml | 5 +-
ruoyi-fastapi-test/docker-compose.test.pg.yml | 5 +-
ruoyi-fastapi-test/docker.env | 1 +
.../frontend/mobile-behavior.mjs | 141 +
.../frontend/mobile-harness.mjs | 170 +
ruoyi-fastapi-test/monitor/test_cache_list.py | 133 +-
.../monitor/test_cache_monitor.py | 82 +-
.../monitor/test_job_management.py | 110 +-
.../monitor/test_oauth_audit.py | 88 +
.../monitor/test_online_user.py | 125 +-
.../monitor/test_server_monitor.py | 91 +-
.../monitor/test_transport_crypto.py | 50 +
ruoyi-fastapi-test/prepare_e2e_compose.py | 115 +
ruoyi-fastapi-test/pytest.ini | 11 +
ruoyi-fastapi-test/requirements.txt | 12 +-
ruoyi-fastapi-test/system/page.py | 251 ++
.../system/test_config_management.py | 198 +-
.../system/test_dept_management.py | 165 +-
.../system/test_dict_management.py | 253 +-
.../system/test_file_management.py | 139 +
.../system/test_log_management.py | 195 +-
.../system/test_menu_management.py | 201 +-
.../system/test_notice_management.py | 200 +-
.../system/test_oauth_management.py | 297 ++
.../system/test_plugin_management.py | 406 +++
.../system/test_post_management.py | 163 +-
.../system/test_role_management.py | 247 +-
.../system/test_user_management.py | 249 +-
ruoyi-fastapi-test/test_account_pages.py | 159 +
ruoyi-fastapi-test/test_browser_harness.py | 266 ++
ruoyi-fastapi-test/test_login.py | 233 +-
ruoyi-fastapi-test/test_pages.py | 196 +-
.../test_plugin_response_contract.py | 216 ++
ruoyi-fastapi-test/test_timezone.py | 134 +-
.../time-contract/run-host-timezones.mjs | 19 +-
ruoyi-fastapi-test/tool/test_build.py | 46 +
ruoyi-fastapi-test/tool/test_code_gen.py | 350 +-
ruoyi-fastapi-test/tool/test_swagger.py | 60 +-
1167 files changed, 94684 insertions(+), 5231 deletions(-)
create mode 100644 .github/requirements.txt
create mode 100644 .github/ruff.toml
create mode 100644 .github/scripts/check_compose.py
create mode 100644 .github/scripts/check_test_report.py
create mode 100644 .github/scripts/ci_plan.py
create mode 100644 .github/scripts/install_ai_test_dependencies.py
create mode 100644 .github/scripts/mobile_smoke.py
create mode 100644 .github/tests/test_ci_contracts.py
create mode 100644 .github/tests/test_ci_plan.py
delete mode 100644 .github/workflows/backend.yml
create mode 100644 .github/workflows/ci-backend.yml
create mode 100644 .github/workflows/ci-e2e.yml
create mode 100644 .github/workflows/ci-frontend.yml
create mode 100644 .github/workflows/ci-integration.yml
create mode 100644 .github/workflows/ci-native.yml
create mode 100644 .github/workflows/ci-quality.yml
create mode 100644 .github/workflows/ci.yml
delete mode 100644 .github/workflows/native-plugin.yml
delete mode 100644 .github/workflows/playwright.yml
delete mode 100644 .github/workflows/plugin-frontend.yml
delete mode 100644 .github/workflows/plugin-network-smoke.yml
delete mode 100644 .github/workflows/plugin-release-integration.yml
delete mode 100644 .github/workflows/ruff.yml
delete mode 100644 .github/workflows/timezone.yml
create mode 100644 docker.env
delete mode 100644 ruoyi-fastapi-app/tests/time.test.js
create mode 100644 ruoyi-fastapi-backend/.dockerignore
create mode 100644 ruoyi-fastapi-backend/plugins/core/frontend.py
create mode 100644 ruoyi-fastapi-backend/requirements-test.txt
create mode 100644 ruoyi-fastapi-backend/tests/cli/root/test_plugin_frontend_environment.py
rename ruoyi-fastapi-backend/tests/{ => config}/conftest.py (100%)
create mode 100644 ruoyi-fastapi-backend/tests/module_admin/service/conftest.py
create mode 100644 ruoyi-fastapi-backend/tests/module_admin/service/test_server_service.py
create mode 100644 ruoyi-fastapi-backend/tests/plugins/core/test_frontend_layout.py
create mode 100644 ruoyi-fastapi-backend/tests/plugins/core/validation/test_frontend_dependencies.py
delete mode 100644 ruoyi-fastapi-frontend/tests/time/time.test.js
create mode 100644 ruoyi-fastapi-frontend/vue2/mobile/.env
create mode 100644 ruoyi-fastapi-frontend/vue2/mobile/.gitignore
create mode 100644 ruoyi-fastapi-frontend/vue2/mobile/.npmrc
create mode 100644 ruoyi-fastapi-frontend/vue2/mobile/README.md
create mode 100644 ruoyi-fastapi-frontend/vue2/mobile/babel.config.js
create mode 100644 ruoyi-fastapi-frontend/vue2/mobile/package.json
create mode 100644 ruoyi-fastapi-frontend/vue2/mobile/platform.js
create mode 100644 ruoyi-fastapi-frontend/vue2/mobile/postcss.config.js
create mode 100644 ruoyi-fastapi-frontend/vue2/mobile/public/index.html
create mode 100644 ruoyi-fastapi-frontend/vue2/mobile/scripts/patch-uni-webpack.cjs
create mode 100644 ruoyi-fastapi-frontend/vue2/mobile/src/App.vue
rename {ruoyi-fastapi-app => ruoyi-fastapi-frontend/vue2/mobile}/src/api/login.js (94%)
rename {ruoyi-fastapi-app => ruoyi-fastapi-frontend/vue2/mobile}/src/api/system/dict/data.js (100%)
rename {ruoyi-fastapi-app => ruoyi-fastapi-frontend/vue2/mobile}/src/api/system/dict/type.js (100%)
rename {ruoyi-fastapi-app => ruoyi-fastapi-frontend/vue2/mobile}/src/api/system/user.js (100%)
create mode 100644 ruoyi-fastapi-frontend/vue2/mobile/src/config.js
create mode 100644 ruoyi-fastapi-frontend/vue2/mobile/src/main.ts
create mode 100644 ruoyi-fastapi-frontend/vue2/mobile/src/manifest.json
rename {ruoyi-fastapi-app => ruoyi-fastapi-frontend/vue2/mobile}/src/pages.json (95%)
create mode 100644 ruoyi-fastapi-frontend/vue2/mobile/src/pages/common/agreement/index.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/mobile/src/pages/common/privacy/index.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/mobile/src/pages/common/textview/index.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/mobile/src/pages/common/webview/index.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/mobile/src/pages/index.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/mobile/src/pages/login.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/mobile/src/pages/mine/about/index.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/mobile/src/pages/mine/avatar/index.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/mobile/src/pages/mine/help/index.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/mobile/src/pages/mine/index.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/mobile/src/pages/mine/info/edit.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/mobile/src/pages/mine/info/index.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/mobile/src/pages/mine/pwd/index.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/mobile/src/pages/mine/setting/index.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/mobile/src/pages/mine/setting/timezone.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/mobile/src/pages/register.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/mobile/src/pages/work/index.vue
rename {ruoyi-fastapi-app => ruoyi-fastapi-frontend/vue2/mobile}/src/permission.js (91%)
create mode 100644 ruoyi-fastapi-frontend/vue2/mobile/src/plugins/auth.js
create mode 100644 ruoyi-fastapi-frontend/vue2/mobile/src/plugins/index.js
rename {ruoyi-fastapi-app => ruoyi-fastapi-frontend/vue2/mobile}/src/plugins/modal.js (94%)
rename {ruoyi-fastapi-app => ruoyi-fastapi-frontend/vue2/mobile}/src/plugins/tab.js (95%)
create mode 100644 ruoyi-fastapi-frontend/vue2/mobile/src/sfc.d.ts
rename {ruoyi-fastapi-app => ruoyi-fastapi-frontend/vue2/mobile}/src/static/favicon.ico (100%)
rename {ruoyi-fastapi-app => ruoyi-fastapi-frontend/vue2/mobile}/src/static/images/banner/banner01.jpg (100%)
rename {ruoyi-fastapi-app => ruoyi-fastapi-frontend/vue2/mobile}/src/static/images/banner/banner02.jpg (100%)
rename {ruoyi-fastapi-app => ruoyi-fastapi-frontend/vue2/mobile}/src/static/images/banner/banner03.jpg (100%)
rename {ruoyi-fastapi-app => ruoyi-fastapi-frontend/vue2/mobile}/src/static/images/profile.jpg (100%)
rename {ruoyi-fastapi-app => ruoyi-fastapi-frontend/vue2/mobile}/src/static/images/tabbar/home.png (100%)
rename {ruoyi-fastapi-app => ruoyi-fastapi-frontend/vue2/mobile}/src/static/images/tabbar/home_.png (100%)
rename {ruoyi-fastapi-app => ruoyi-fastapi-frontend/vue2/mobile}/src/static/images/tabbar/mine.png (100%)
rename {ruoyi-fastapi-app => ruoyi-fastapi-frontend/vue2/mobile}/src/static/images/tabbar/mine_.png (100%)
rename {ruoyi-fastapi-app => ruoyi-fastapi-frontend/vue2/mobile}/src/static/images/tabbar/work.png (100%)
rename {ruoyi-fastapi-app => ruoyi-fastapi-frontend/vue2/mobile}/src/static/images/tabbar/work_.png (100%)
rename {ruoyi-fastapi-app => ruoyi-fastapi-frontend/vue2/mobile}/src/static/logo.png (100%)
rename {ruoyi-fastapi-app => ruoyi-fastapi-frontend/vue2/mobile}/src/static/logo200.png (100%)
create mode 100644 ruoyi-fastapi-frontend/vue2/mobile/src/store/getters.js
create mode 100644 ruoyi-fastapi-frontend/vue2/mobile/src/store/index.js
create mode 100644 ruoyi-fastapi-frontend/vue2/mobile/src/store/modules/user.js
rename {ruoyi-fastapi-app => ruoyi-fastapi-frontend/vue2/mobile}/src/uni.scss (100%)
rename {ruoyi-fastapi-app => ruoyi-fastapi-frontend/vue2/mobile}/src/utils/auth.js (95%)
rename {ruoyi-fastapi-app => ruoyi-fastapi-frontend/vue2/mobile}/src/utils/common.js (100%)
rename {ruoyi-fastapi-app => ruoyi-fastapi-frontend/vue2/mobile}/src/utils/constant.js (100%)
rename {ruoyi-fastapi-app => ruoyi-fastapi-frontend/vue2/mobile}/src/utils/errorCode.js (97%)
rename {ruoyi-fastapi-app => ruoyi-fastapi-frontend/vue2/mobile}/src/utils/permission.js (96%)
create mode 100644 ruoyi-fastapi-frontend/vue2/mobile/src/utils/request.js
rename {ruoyi-fastapi-app => ruoyi-fastapi-frontend/vue2/mobile}/src/utils/storage.js (100%)
create mode 100644 ruoyi-fastapi-frontend/vue2/mobile/src/utils/time.js
rename {ruoyi-fastapi-app => ruoyi-fastapi-frontend/vue2/mobile}/src/utils/transportCrypto.js (100%)
rename {ruoyi-fastapi-app => ruoyi-fastapi-frontend/vue2/mobile}/src/utils/transportCryptoPolicy.js (100%)
rename {ruoyi-fastapi-app => ruoyi-fastapi-frontend/vue2/mobile}/src/utils/transportForge.js (100%)
create mode 100644 ruoyi-fastapi-frontend/vue2/mobile/src/utils/upload.js
rename {ruoyi-fastapi-app => ruoyi-fastapi-frontend/vue2/mobile}/src/utils/validate.js (100%)
create mode 100644 ruoyi-fastapi-frontend/vue2/mobile/src/vendor/node-forge/lib/prng.js
create mode 100644 ruoyi-fastapi-frontend/vue2/mobile/src/vendor/node-forge/lib/random.js
create mode 100644 ruoyi-fastapi-frontend/vue2/mobile/src/vendor/node-forge/lib/rsa.js
create mode 100644 ruoyi-fastapi-frontend/vue2/mobile/src/vendor/node-forge/lib/util.js
create mode 100644 ruoyi-fastapi-frontend/vue2/mobile/tailwind.config.js
create mode 100644 ruoyi-fastapi-frontend/vue2/mobile/tests/behavior.test.mjs
create mode 100644 ruoyi-fastapi-frontend/vue2/mobile/tests/time-bundle.test.cjs
create mode 100644 ruoyi-fastapi-frontend/vue2/mobile/tests/time-polyfill.test.mjs
create mode 100644 ruoyi-fastapi-frontend/vue2/mobile/tests/time.test.mjs
create mode 100644 ruoyi-fastapi-frontend/vue2/mobile/tsconfig.json
create mode 100644 ruoyi-fastapi-frontend/vue2/mobile/vue.config.js
create mode 100644 ruoyi-fastapi-frontend/vue2/web/.dockerignore
rename ruoyi-fastapi-frontend/{ => vue2/web}/.editorconfig (100%)
create mode 100644 ruoyi-fastapi-frontend/vue2/web/.env.development
create mode 100644 ruoyi-fastapi-frontend/vue2/web/.env.docker
create mode 100644 ruoyi-fastapi-frontend/vue2/web/.env.production
create mode 100644 ruoyi-fastapi-frontend/vue2/web/.env.staging
rename ruoyi-fastapi-frontend/{ => vue2/web}/.gitattributes (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/.gitignore (100%)
create mode 100644 ruoyi-fastapi-frontend/vue2/web/.prettierignore
rename ruoyi-fastapi-frontend/{ => vue2/web}/.prettierrc.json (100%)
create mode 100644 ruoyi-fastapi-frontend/vue2/web/Dockerfile
create mode 100644 ruoyi-fastapi-frontend/vue2/web/FORMATTING.md
create mode 100644 ruoyi-fastapi-frontend/vue2/web/babel.config.js
create mode 100644 ruoyi-fastapi-frontend/vue2/web/bin/build.bat
create mode 100644 ruoyi-fastapi-frontend/vue2/web/bin/nginx.dockermy.conf
create mode 100644 ruoyi-fastapi-frontend/vue2/web/bin/nginx.dockerpg.conf
create mode 100644 ruoyi-fastapi-frontend/vue2/web/bin/package.bat
create mode 100644 ruoyi-fastapi-frontend/vue2/web/bin/run-web.bat
create mode 100644 ruoyi-fastapi-frontend/vue2/web/build/index.js
create mode 100644 ruoyi-fastapi-frontend/vue2/web/frontend.code-workspace
create mode 100644 ruoyi-fastapi-frontend/vue2/web/package.json
create mode 100644 ruoyi-fastapi-frontend/vue2/web/plugins/ai/README.md
rename ruoyi-fastapi-frontend/{ => vue2/web}/plugins/ai/api/chat.js (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/plugins/ai/api/model.js (100%)
create mode 100644 ruoyi-fastapi-frontend/vue2/web/plugins/ai/views/chat/components/AiMessage.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/plugins/ai/views/chat/index.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/plugins/ai/views/model/index.vue
rename ruoyi-fastapi-frontend/{ => vue2/web}/public/favicon.ico (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web/public}/html/ie.html (100%)
create mode 100644 ruoyi-fastapi-frontend/vue2/web/public/index.html
create mode 100644 ruoyi-fastapi-frontend/vue2/web/public/robots.txt
create mode 100644 ruoyi-fastapi-frontend/vue2/web/public/styles/theme-chalk/index.css
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/App.vue
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/api/authCenter.js (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/api/login.js (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/api/menu.js (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/api/monitor/cache.js (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/api/monitor/job.js (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/api/monitor/jobLog.js (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/api/monitor/logininfor.js (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/api/monitor/oauthAudit.js (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/api/monitor/online.js (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/api/monitor/operlog.js (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/api/monitor/server.js (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/api/monitor/transportCrypto.js (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/api/system/config.js (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/api/system/dept.js (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/api/system/dict/data.js (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/api/system/dict/type.js (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/api/system/file.js (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/api/system/menu.js (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/api/system/notice.js (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/api/system/oauthClient.js (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/api/system/oauthResource.js (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/api/system/oauthSession.js (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/api/system/plugin.js (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/api/system/post.js (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/api/system/role.js (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/api/system/user.js (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/api/tool/gen.js (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/401_images/401.gif (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/404_images/404.png (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/404_images/404_cloud.png (100%)
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/assets/icons/index.js
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/404.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/ai-chat.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/ai-manage.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/ai-model.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/bell.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/bug.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/build.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/button.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/cascader.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/chart.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/checkbox.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/client.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/clipboard.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/code.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/color.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/component.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/dashboard.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/date-range.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/date.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/deepthink.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/dict.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/documentation.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/download.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/drag.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/druid.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/edit.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/education.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/email.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/enter.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/example.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/excel.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/exit-fullscreen.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/eye-open.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/eye.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/form.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/fullscreen.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/github.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/grant.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/guide.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/icon.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/input.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/international.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/job.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/key.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/language.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/link.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/list.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/lock.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/log.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/logininfor.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/message.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/money.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/monitor.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/more-up.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/nested.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/number.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/oauth.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/online.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/password.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/pdf.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/people.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/peoples.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/phone.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/post.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/qq.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/question.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/radio.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/rate.svg (100%)
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/assets/icons/svg/redis-list.svg
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/redis.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/resource.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/row.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/scope.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/search.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/select.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/server.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/session.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/shopping.svg (100%)
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/assets/icons/svg/size.svg
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/skill.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/slider.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/star.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/swagger.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/switch.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/system.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/tab.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/table.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/textarea.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/theme.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/time-range.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/time.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/tool.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/tree-table.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/tree.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/upload.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/user.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/validCode.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/wechat.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/icons/svg/zip.svg (100%)
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/assets/icons/svgo.yml
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/images/dark.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/images/light.svg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/images/login-background.jpg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/images/profile.jpg (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/logo/logo.png (100%)
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/assets/styles/btn.scss
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/assets/styles/element-ui.scss
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/assets/styles/element-variables.scss
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/assets/styles/index.scss
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/assets/styles/mixin.scss (100%)
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/assets/styles/oauth.scss
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/assets/styles/ruoyi.scss
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/assets/styles/sidebar.scss
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/assets/styles/transition.scss
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/assets/styles/variables.scss
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/components/Breadcrumb/index.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/components/BusinessDateTimePicker/index.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/components/BusinessFileUpload/index.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/components/Crontab/day.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/components/Crontab/hour.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/components/Crontab/index.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/components/Crontab/min.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/components/Crontab/month.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/components/Crontab/result.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/components/Crontab/second.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/components/Crontab/week.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/components/Crontab/year.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/components/DictData/index.js
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/components/DictTag/index.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/components/Editor/index.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/components/ExcelImportDialog/index.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/components/FileUpload/index.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/components/Hamburger/index.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/components/HeaderSearch/index.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/components/IconSelect/index.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/components/IconSelect/requireIcons.js
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/components/ImagePreview/index.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/components/ImageUpload/index.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/components/JsonEditor/index.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/components/OAuthWorkspace/AccessMap.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/components/OAuthWorkspace/FieldLabel.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/components/OAuthWorkspace/PageFrame.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/components/OAuthWorkspace/StringListInput.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/components/Pagination/index.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/components/PanThumb/index.vue
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/components/ParentView/index.vue (100%)
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/components/PluginFrame/index.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/components/RightToolbar/index.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/components/RuoYi/Doc/index.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/components/RuoYi/Git/index.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/components/Screenfull/index.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/components/SizeSelect/index.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/components/SvgIcon/index.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/components/ThemePicker/index.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/components/TreePanel/index.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/components/iFrame/index.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/directive/dialog/drag.js
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/directive/dialog/dragHeight.js
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/directive/dialog/dragWidth.js
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/directive/index.js
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/directive/module/clipboard.js
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/directive/permission/hasPermi.js
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/directive/permission/hasRole.js
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/layout/components/AppMain.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/layout/components/Copyright/index.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/layout/components/HeaderNotice/DetailView.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/layout/components/HeaderNotice/index.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/layout/components/IframeToggle/index.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/layout/components/InnerLink/index.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/layout/components/Navbar.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/layout/components/Settings/index.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/layout/components/Sidebar/FixiOSBug.js
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/layout/components/Sidebar/Item.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/layout/components/Sidebar/Link.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/layout/components/Sidebar/Logo.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/layout/components/Sidebar/SidebarItem.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/layout/components/Sidebar/index.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/layout/components/TagsView/ScrollPane.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/layout/components/TagsView/index.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/layout/components/TopBar/index.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/layout/components/TopNav/index.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/layout/components/index.js
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/layout/index.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/layout/mixin/ResizeHandler.js
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/main.js
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/permission.js
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/plugins/auth.js
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/plugins/cache.js (100%)
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/plugins/download.js
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/plugins/index.js
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/plugins/modal.js
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/plugins/tab.js
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/router/index.js
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/settings.js
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/store/getters.js
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/store/index.js
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/store/modules/app.js
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/store/modules/dict.js
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/store/modules/lock.js
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/store/modules/permission.js
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/store/modules/settings.js
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/store/modules/tagsView.js
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/store/modules/user.js
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/utils/auth.js (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/utils/authCenterAccess.js (100%)
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/utils/dict/Dict.js
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/utils/dict/DictConverter.js
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/utils/dict/DictData.js
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/utils/dict/DictMeta.js
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/utils/dict/DictOptions.js
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/utils/dict/index.js
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/utils/dynamicTitle.js
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/utils/errorCode.js (100%)
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/utils/generator/config.js
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/utils/generator/css.js (100%)
mode change 100755 => 100644
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/utils/generator/drawingDefault.js
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/utils/generator/html.js
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/utils/generator/icon.json (100%)
mode change 100755 => 100644
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/utils/generator/js.js
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/utils/generator/render.js
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/utils/index.js
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/utils/job.js (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/utils/jsencrypt.js (100%)
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/utils/monaco.js
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/utils/oauthUri.js (100%)
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/utils/passwordRule.js
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/utils/permission.js
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/utils/pluginBridge.d.ts (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/utils/pluginBridge.js (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/utils/pluginBridge.sdk.json (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/utils/pluginPlanFormatter.js (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/utils/pluginReleaseFormatter.js (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/utils/pluginSse.js (100%)
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/utils/pluginViewResolver.js
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/utils/request.js
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/utils/ruoyi.js
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/utils/scroll-to.js
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/utils/time.js
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/utils/transportCrypto.js
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/utils/transportCryptoPolicy.js
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/utils/transportPath.js (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/utils/validate.js (100%)
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/auth-center/AuthCenterShell.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/auth-center/changePassword.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/auth-center/consent.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/auth-center/error.vue
rename ruoyi-fastapi-frontend/{ => vue2/web}/src/views/auth-center/interactionSecurity.js (100%)
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/auth-center/login.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/auth-center/useInteraction.js
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/dashboard/BarChart.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/dashboard/LineChart.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/dashboard/PanelGroup.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/dashboard/PieChart.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/dashboard/Radar.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/dashboard/RaddarChart.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/dashboard/Workplace.less
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/dashboard/index.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/dashboard/mixins/resize.js
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/error/401.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/error/404.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/index.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/index_v1.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/lock.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/login.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/monitor/cache/index.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/monitor/cache/list.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/monitor/druid/index.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/monitor/job/detail.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/monitor/job/index.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/monitor/job/log.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/monitor/job/runtime.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/monitor/logininfor/index.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/monitor/oauthAudit/index.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/monitor/online/index.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/monitor/operlog/detail.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/monitor/operlog/index.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/monitor/server/index.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/monitor/transportCrypto/index.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/redirect.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/register.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/system/config/index.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/system/dept/index.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/system/dict/data.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/system/dict/detail.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/system/dict/index.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/system/file/components/FileAclDrawer.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/system/file/components/FileAuditDrawer.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/system/file/components/FileDetailDialog.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/system/file/components/FileReconcileDrawer.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/system/file/components/FileReferenceDrawer.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/system/file/components/FileRetentionPolicyDrawer.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/system/file/components/FileRetentionReminderDrawer.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/system/file/components/FileSearchForm.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/system/file/components/FileStatistics.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/system/file/components/FileTable.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/system/file/components/FileTransferDialog.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/system/file/components/fileFormatters.js
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/system/file/index.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/system/menu/index.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/system/notice/ReadUsers.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/system/notice/index.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/system/oauth/client/index.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/system/oauth/client/secret.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/system/oauth/grant/AccessPolicyDrawer.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/system/oauth/grant/index.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/system/oauth/key/index.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/system/oauth/resource/index.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/system/oauth/scope/index.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/system/oauth/session/index.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/system/plugin/components/PluginConfigDialog.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/system/plugin/components/PluginConfigStatus.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/system/plugin/components/PluginDependencyDialog.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/system/plugin/components/PluginDetailDialog.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/system/plugin/components/PluginDiagnosticDialog.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/system/plugin/components/PluginPlanDialog.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/system/plugin/components/PluginReleaseStatus.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/system/plugin/components/PluginRuntimeMetrics.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/system/plugin/index.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/system/post/index.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/system/role/authUser.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/system/role/index.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/system/role/selectUser.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/system/user/authRole.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/system/user/index.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/system/user/profile/index.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/system/user/profile/resetPwd.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/system/user/profile/timezoneSettings.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/system/user/profile/userAvatar.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/system/user/profile/userInfo.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/system/user/view.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/tool/build/CodeTypeDialog.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/tool/build/DraggableItem.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/tool/build/IconsDialog.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/tool/build/RightPanel.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/tool/build/TreeNodeDialog.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/tool/build/index.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/tool/gen/basicInfoForm.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/tool/gen/createTable.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/tool/gen/editTable.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/tool/gen/genInfoForm.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/tool/gen/importTable.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/tool/gen/index.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/src/views/tool/swagger/index.vue
create mode 100644 ruoyi-fastapi-frontend/vue2/web/tests/auth-center/availability.test.cjs
create mode 100644 ruoyi-fastapi-frontend/vue2/web/tests/auth-center/interactionSecurity.test.cjs
create mode 100644 ruoyi-fastapi-frontend/vue2/web/tests/auth-center/oauthUri.test.cjs
create mode 100644 ruoyi-fastapi-frontend/vue2/web/tests/auth-center/vue2-components.test.cjs
rename ruoyi-fastapi-frontend/{tests/job/job-form.test.js => vue2/web/tests/job/job-form.test.mjs} (100%)
create mode 100644 ruoyi-fastapi-frontend/vue2/web/tests/job/vue2-components.test.cjs
rename ruoyi-fastapi-frontend/{ => vue2/web}/tests/plugins/fixtures/bridge-b0d16a3e/.gitattributes (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/tests/plugins/fixtures/bridge-b0d16a3e/pluginBridge.js (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/tests/plugins/fixtures/bridge-b0d16a3e/provenance.json (100%)
rename ruoyi-fastapi-frontend/{ => vue2/web}/tests/plugins/fixtures/json-only-host.js (100%)
create mode 100644 ruoyi-fastapi-frontend/vue2/web/tests/plugins/network-smoke/server.mjs
rename ruoyi-fastapi-frontend/{tests/plugins/pluginBridge.test.js => vue2/web/tests/plugins/pluginBridge.test.mjs} (100%)
rename ruoyi-fastapi-frontend/{tests/plugins/pluginBridgeCompatibility.test.js => vue2/web/tests/plugins/pluginBridgeCompatibility.test.mjs} (100%)
create mode 100644 ruoyi-fastapi-frontend/vue2/web/tests/plugins/pluginConfigStatus.test.mjs
create mode 100644 ruoyi-fastapi-frontend/vue2/web/tests/plugins/pluginDevMock.test.mjs
create mode 100644 ruoyi-fastapi-frontend/vue2/web/tests/plugins/pluginFrame.test.mjs
create mode 100644 ruoyi-fastapi-frontend/vue2/web/tests/plugins/pluginHostIntegration.test.cjs
create mode 100644 ruoyi-fastapi-frontend/vue2/web/tests/plugins/pluginReleaseFormatter.test.mjs
create mode 100644 ruoyi-fastapi-frontend/vue2/web/tests/plugins/pluginRuntimeMetrics.test.mjs
rename ruoyi-fastapi-frontend/{tests/plugins/pluginSse.test.js => vue2/web/tests/plugins/pluginSse.test.mjs} (100%)
create mode 100644 ruoyi-fastapi-frontend/vue2/web/tests/plugins/pluginViewResolver.test.js
create mode 100644 ruoyi-fastapi-frontend/vue2/web/tests/plugins/pluginVue2Components.test.cjs
create mode 100644 ruoyi-fastapi-frontend/vue2/web/tests/plugins/run-plugin-tests.js
create mode 100644 ruoyi-fastapi-frontend/vue2/web/tests/support/load-module.cjs
create mode 100644 ruoyi-fastapi-frontend/vue2/web/tests/time/time.test.mjs
create mode 100644 ruoyi-fastapi-frontend/vue2/web/vue.config.js
rename {ruoyi-fastapi-app => ruoyi-fastapi-frontend/vue3/mobile}/.editorconfig (100%)
rename {ruoyi-fastapi-app => ruoyi-fastapi-frontend/vue3/mobile}/.gitignore (100%)
rename {ruoyi-fastapi-app => ruoyi-fastapi-frontend/vue3/mobile}/.npmrc (100%)
rename {ruoyi-fastapi-app => ruoyi-fastapi-frontend/vue3/mobile}/README.md (89%)
rename {ruoyi-fastapi-app => ruoyi-fastapi-frontend/vue3/mobile}/index.html (100%)
rename {ruoyi-fastapi-app => ruoyi-fastapi-frontend/vue3/mobile}/netlify.toml (100%)
rename {ruoyi-fastapi-app => ruoyi-fastapi-frontend/vue3/mobile}/package.json (94%)
rename {ruoyi-fastapi-app => ruoyi-fastapi-frontend/vue3/mobile}/platform.ts (100%)
rename {ruoyi-fastapi-app => ruoyi-fastapi-frontend/vue3/mobile}/postcss.config.ts (100%)
rename {ruoyi-fastapi-app => ruoyi-fastapi-frontend/vue3/mobile}/src/App.vue (100%)
create mode 100644 ruoyi-fastapi-frontend/vue3/mobile/src/api/login.js
create mode 100644 ruoyi-fastapi-frontend/vue3/mobile/src/api/system/dict/data.js
create mode 100644 ruoyi-fastapi-frontend/vue3/mobile/src/api/system/dict/type.js
create mode 100644 ruoyi-fastapi-frontend/vue3/mobile/src/api/system/user.js
rename {ruoyi-fastapi-app => ruoyi-fastapi-frontend/vue3/mobile}/src/auto-imports.d.ts (100%)
rename {ruoyi-fastapi-app => ruoyi-fastapi-frontend/vue3/mobile}/src/config.js (100%)
rename {ruoyi-fastapi-app => ruoyi-fastapi-frontend/vue3/mobile}/src/env.d.ts (100%)
rename {ruoyi-fastapi-app => ruoyi-fastapi-frontend/vue3/mobile}/src/main.ts (100%)
rename {ruoyi-fastapi-app => ruoyi-fastapi-frontend/vue3/mobile}/src/manifest.json (100%)
create mode 100644 ruoyi-fastapi-frontend/vue3/mobile/src/pages.json
rename {ruoyi-fastapi-app => ruoyi-fastapi-frontend/vue3/mobile}/src/pages/common/agreement/index.vue (100%)
rename {ruoyi-fastapi-app => ruoyi-fastapi-frontend/vue3/mobile}/src/pages/common/privacy/index.vue (100%)
rename {ruoyi-fastapi-app => ruoyi-fastapi-frontend/vue3/mobile}/src/pages/common/textview/index.vue (100%)
rename {ruoyi-fastapi-app => ruoyi-fastapi-frontend/vue3/mobile}/src/pages/common/webview/index.vue (100%)
rename {ruoyi-fastapi-app => ruoyi-fastapi-frontend/vue3/mobile}/src/pages/index.vue (100%)
rename {ruoyi-fastapi-app => ruoyi-fastapi-frontend/vue3/mobile}/src/pages/login.vue (100%)
rename {ruoyi-fastapi-app => ruoyi-fastapi-frontend/vue3/mobile}/src/pages/mine/about/index.vue (100%)
rename {ruoyi-fastapi-app => ruoyi-fastapi-frontend/vue3/mobile}/src/pages/mine/avatar/index.vue (96%)
rename {ruoyi-fastapi-app => ruoyi-fastapi-frontend/vue3/mobile}/src/pages/mine/help/index.vue (96%)
rename {ruoyi-fastapi-app => ruoyi-fastapi-frontend/vue3/mobile}/src/pages/mine/index.vue (97%)
rename {ruoyi-fastapi-app => ruoyi-fastapi-frontend/vue3/mobile}/src/pages/mine/info/edit.vue (100%)
rename {ruoyi-fastapi-app => ruoyi-fastapi-frontend/vue3/mobile}/src/pages/mine/info/index.vue (100%)
rename {ruoyi-fastapi-app => ruoyi-fastapi-frontend/vue3/mobile}/src/pages/mine/pwd/index.vue (100%)
rename {ruoyi-fastapi-app => ruoyi-fastapi-frontend/vue3/mobile}/src/pages/mine/setting/index.vue (100%)
rename {ruoyi-fastapi-app => ruoyi-fastapi-frontend/vue3/mobile}/src/pages/mine/setting/timezone.vue (100%)
rename {ruoyi-fastapi-app => ruoyi-fastapi-frontend/vue3/mobile}/src/pages/register.vue (100%)
rename {ruoyi-fastapi-app => ruoyi-fastapi-frontend/vue3/mobile}/src/pages/work/index.vue (100%)
create mode 100644 ruoyi-fastapi-frontend/vue3/mobile/src/permission.js
rename {ruoyi-fastapi-app => ruoyi-fastapi-frontend/vue3/mobile}/src/plugins/auth.js (96%)
rename {ruoyi-fastapi-app => ruoyi-fastapi-frontend/vue3/mobile}/src/plugins/index.js (96%)
create mode 100644 ruoyi-fastapi-frontend/vue3/mobile/src/plugins/modal.js
create mode 100644 ruoyi-fastapi-frontend/vue3/mobile/src/plugins/tab.js
create mode 100644 ruoyi-fastapi-frontend/vue3/mobile/src/static/favicon.ico
create mode 100644 ruoyi-fastapi-frontend/vue3/mobile/src/static/images/banner/banner01.jpg
create mode 100644 ruoyi-fastapi-frontend/vue3/mobile/src/static/images/banner/banner02.jpg
create mode 100644 ruoyi-fastapi-frontend/vue3/mobile/src/static/images/banner/banner03.jpg
create mode 100644 ruoyi-fastapi-frontend/vue3/mobile/src/static/images/profile.jpg
create mode 100644 ruoyi-fastapi-frontend/vue3/mobile/src/static/images/tabbar/home.png
create mode 100644 ruoyi-fastapi-frontend/vue3/mobile/src/static/images/tabbar/home_.png
create mode 100644 ruoyi-fastapi-frontend/vue3/mobile/src/static/images/tabbar/mine.png
create mode 100644 ruoyi-fastapi-frontend/vue3/mobile/src/static/images/tabbar/mine_.png
create mode 100644 ruoyi-fastapi-frontend/vue3/mobile/src/static/images/tabbar/work.png
create mode 100644 ruoyi-fastapi-frontend/vue3/mobile/src/static/images/tabbar/work_.png
create mode 100644 ruoyi-fastapi-frontend/vue3/mobile/src/static/logo.png
create mode 100644 ruoyi-fastapi-frontend/vue3/mobile/src/static/logo200.png
rename {ruoyi-fastapi-app => ruoyi-fastapi-frontend/vue3/mobile}/src/store/index.js (100%)
rename {ruoyi-fastapi-app => ruoyi-fastapi-frontend/vue3/mobile}/src/store/modules/config.js (100%)
rename {ruoyi-fastapi-app => ruoyi-fastapi-frontend/vue3/mobile}/src/store/modules/dict.js (100%)
rename {ruoyi-fastapi-app => ruoyi-fastapi-frontend/vue3/mobile}/src/store/modules/user.js (100%)
rename {ruoyi-fastapi-app => ruoyi-fastapi-frontend/vue3/mobile}/src/theme.json (100%)
create mode 100644 ruoyi-fastapi-frontend/vue3/mobile/src/uni.scss
create mode 100644 ruoyi-fastapi-frontend/vue3/mobile/src/utils/auth.js
create mode 100644 ruoyi-fastapi-frontend/vue3/mobile/src/utils/common.js
create mode 100644 ruoyi-fastapi-frontend/vue3/mobile/src/utils/constant.js
rename {ruoyi-fastapi-app => ruoyi-fastapi-frontend/vue3/mobile}/src/utils/dict.js (100%)
create mode 100644 ruoyi-fastapi-frontend/vue3/mobile/src/utils/errorCode.js
create mode 100644 ruoyi-fastapi-frontend/vue3/mobile/src/utils/permission.js
rename {ruoyi-fastapi-app => ruoyi-fastapi-frontend/vue3/mobile}/src/utils/request.js (100%)
create mode 100644 ruoyi-fastapi-frontend/vue3/mobile/src/utils/storage.js
rename {ruoyi-fastapi-app => ruoyi-fastapi-frontend/vue3/mobile}/src/utils/time.js (100%)
create mode 100644 ruoyi-fastapi-frontend/vue3/mobile/src/utils/transportCrypto.js
create mode 100644 ruoyi-fastapi-frontend/vue3/mobile/src/utils/transportCryptoPolicy.js
create mode 100644 ruoyi-fastapi-frontend/vue3/mobile/src/utils/transportForge.js
rename {ruoyi-fastapi-app => ruoyi-fastapi-frontend/vue3/mobile}/src/utils/upload.js (97%)
create mode 100644 ruoyi-fastapi-frontend/vue3/mobile/src/utils/validate.js
rename {ruoyi-fastapi-app => ruoyi-fastapi-frontend/vue3/mobile}/stylelint.config.mjs (100%)
rename {ruoyi-fastapi-app => ruoyi-fastapi-frontend/vue3/mobile}/tailwind.config.ts (100%)
create mode 100644 ruoyi-fastapi-frontend/vue3/mobile/tests/behavior.test.mjs
rename {ruoyi-fastapi-app => ruoyi-fastapi-frontend/vue3/mobile}/tests/time-polyfill.test.js (67%)
create mode 100644 ruoyi-fastapi-frontend/vue3/mobile/tests/time.test.js
rename {ruoyi-fastapi-app => ruoyi-fastapi-frontend/vue3/mobile}/tsconfig.json (100%)
rename {ruoyi-fastapi-app => ruoyi-fastapi-frontend/vue3/mobile}/vite.config.ts (100%)
create mode 100644 ruoyi-fastapi-frontend/vue3/web/.dockerignore
create mode 100644 ruoyi-fastapi-frontend/vue3/web/.editorconfig
rename ruoyi-fastapi-frontend/{ => vue3/web}/.env.development (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/.env.docker (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/.env.production (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/.env.staging (100%)
create mode 100644 ruoyi-fastapi-frontend/vue3/web/.gitattributes
create mode 100644 ruoyi-fastapi-frontend/vue3/web/.gitignore
rename ruoyi-fastapi-frontend/{ => vue3/web}/.prettierignore (100%)
create mode 100644 ruoyi-fastapi-frontend/vue3/web/.prettierrc.json
rename ruoyi-fastapi-frontend/{ => vue3/web}/Dockerfile (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/FORMATTING.md (93%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/LICENSE (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/bin/build.bat (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/bin/nginx.dockermy.conf (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/bin/nginx.dockerpg.conf (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/bin/package.bat (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/bin/run-web.bat (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/frontend.code-workspace (93%)
create mode 100644 ruoyi-fastapi-frontend/vue3/web/html/ie.html
rename ruoyi-fastapi-frontend/{ => vue3/web}/index.html (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/package.json (87%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/plugins/ai/README.md (67%)
create mode 100644 ruoyi-fastapi-frontend/vue3/web/plugins/ai/api/chat.js
create mode 100644 ruoyi-fastapi-frontend/vue3/web/plugins/ai/api/model.js
rename ruoyi-fastapi-frontend/{ => vue3/web}/plugins/ai/views/chat/components/AiMessage.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/plugins/ai/views/chat/index.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/plugins/ai/views/model/index.vue (100%)
create mode 100644 ruoyi-fastapi-frontend/vue3/web/public/favicon.ico
rename ruoyi-fastapi-frontend/{ => vue3/web}/public/monaco-environment.js (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/App.vue (100%)
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/api/authCenter.js
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/api/login.js
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/api/menu.js
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/api/monitor/cache.js
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/api/monitor/job.js
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/api/monitor/jobLog.js
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/api/monitor/logininfor.js
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/api/monitor/oauthAudit.js
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/api/monitor/online.js
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/api/monitor/operlog.js
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/api/monitor/server.js
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/api/monitor/transportCrypto.js
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/api/system/config.js
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/api/system/dept.js
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/api/system/dict/data.js
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/api/system/dict/type.js
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/api/system/file.js
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/api/system/menu.js
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/api/system/notice.js
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/api/system/oauthClient.js
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/api/system/oauthResource.js
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/api/system/oauthSession.js
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/api/system/plugin.js
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/api/system/post.js
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/api/system/role.js
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/api/system/user.js
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/api/tool/gen.js
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/401_images/401.gif
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/404_images/404.png
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/404_images/404_cloud.png
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/404.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/ai-chat.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/ai-manage.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/ai-model.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/bell.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/bug.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/build.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/button.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/cascader.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/chart.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/checkbox.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/client.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/clipboard.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/code.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/color.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/component.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/dashboard.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/date-range.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/date.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/deepthink.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/dict.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/documentation.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/download.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/drag.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/druid.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/edit.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/education.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/email.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/enter.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/example.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/excel.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/exit-fullscreen.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/eye-open.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/eye.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/form.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/fullscreen.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/github.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/grant.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/guide.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/icon.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/input.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/international.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/job.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/key.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/language.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/link.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/list.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/lock.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/log.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/logininfor.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/message.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/money.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/monitor.svg
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/assets/icons/svg/moon.svg (100%)
mode change 100755 => 100644
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/more-up.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/nested.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/number.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/oauth.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/online.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/password.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/pdf.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/people.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/peoples.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/phone.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/post.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/qq.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/question.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/radio.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/rate.svg
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/assets/icons/svg/redis-list.svg (100%)
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/redis.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/resource.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/row.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/scope.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/search.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/select.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/server.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/session.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/shopping.svg
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/assets/icons/svg/size.svg (100%)
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/skill.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/slider.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/star.svg
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/assets/icons/svg/sunny.svg (100%)
mode change 100755 => 100644
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/swagger.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/switch.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/system.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/tab.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/table.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/textarea.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/theme.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/time-range.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/time.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/tool.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/tree-table.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/tree.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/upload.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/user.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/validCode.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/wechat.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/icons/svg/zip.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/images/dark.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/images/light.svg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/images/login-background.jpg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/images/profile.jpg
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/logo/logo.png
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/assets/styles/btn.scss (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/assets/styles/element-ui.scss (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/assets/styles/index.scss (100%)
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/assets/styles/mixin.scss
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/assets/styles/ruoyi.scss (100%)
mode change 100755 => 100644
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/assets/styles/sidebar.scss (100%)
mode change 100755 => 100644
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/assets/styles/transition.scss (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/assets/styles/variables.module.scss (100%)
mode change 100755 => 100644
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/components/Breadcrumb/index.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/components/BusinessDateTimePicker/index.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/components/BusinessFileUpload/index.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/components/Crontab/day.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/components/Crontab/hour.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/components/Crontab/index.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/components/Crontab/min.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/components/Crontab/month.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/components/Crontab/result.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/components/Crontab/second.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/components/Crontab/week.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/components/Crontab/year.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/components/DictTag/index.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/components/Editor/index.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/components/ExcelImportDialog/index.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/components/FileUpload/index.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/components/Hamburger/index.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/components/HeaderSearch/index.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/components/IconSelect/index.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/components/IconSelect/requireIcons.js (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/components/ImagePreview/index.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/components/ImageUpload/index.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/components/JsonEditor/index.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/components/OAuthWorkspace/AccessMap.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/components/OAuthWorkspace/FieldLabel.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/components/OAuthWorkspace/PageFrame.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/components/OAuthWorkspace/StringListInput.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/components/Pagination/index.vue (100%)
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/components/ParentView/index.vue
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/components/PluginFrame/index.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/components/RightToolbar/index.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/components/RuoYi/Doc/index.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/components/RuoYi/Git/index.vue (71%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/components/Screenfull/index.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/components/SizeSelect/index.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/components/SvgIcon/index.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/components/SvgIcon/svgicon.js (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/components/TreePanel/index.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/components/iFrame/index.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/directive/common/copyText.js (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/directive/index.js (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/directive/permission/hasPermi.js (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/directive/permission/hasRole.js (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/layout/components/AppMain.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/layout/components/Copyright/index.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/layout/components/HeaderNotice/DetailView.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/layout/components/HeaderNotice/index.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/layout/components/IframeToggle/index.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/layout/components/InnerLink/index.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/layout/components/Navbar.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/layout/components/Settings/index.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/layout/components/Sidebar/Link.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/layout/components/Sidebar/Logo.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/layout/components/Sidebar/SidebarItem.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/layout/components/Sidebar/index.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/layout/components/TagsView/ScrollPane.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/layout/components/TagsView/index.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/layout/components/TopBar/index.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/layout/components/TopNav/index.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/layout/components/index.js (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/layout/index.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/main.js (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/permission.js (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/plugins/auth.js (100%)
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/plugins/cache.js
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/plugins/download.js (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/plugins/index.js (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/plugins/modal.js (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/plugins/tab.js (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/router/index.js (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/settings.js (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/store/index.js (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/store/modules/app.js (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/store/modules/dict.js (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/store/modules/lock.js (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/store/modules/permission.js (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/store/modules/settings.js (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/store/modules/tagsView.js (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/store/modules/user.js (100%)
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/utils/auth.js
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/utils/authCenterAccess.js
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/utils/dict.js (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/utils/dynamicTitle.js (100%)
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/utils/errorCode.js
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/utils/generator/config.js (100%)
mode change 100755 => 100644
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/utils/generator/css.js
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/utils/generator/drawingDefault.js (100%)
mode change 100755 => 100644
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/utils/generator/html.js (100%)
mode change 100755 => 100644
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/utils/generator/icon.json
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/utils/generator/js.js (100%)
mode change 100755 => 100644
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/utils/generator/render.js (100%)
mode change 100755 => 100644
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/utils/index.js (100%)
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/utils/job.js
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/utils/jsencrypt.js
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/utils/oauthUri.js
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/utils/passwordRule.js (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/utils/permission.js (100%)
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/utils/pluginBridge.d.ts
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/utils/pluginBridge.js
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/utils/pluginBridge.sdk.json
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/utils/pluginPlanFormatter.js
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/utils/pluginReleaseFormatter.js
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/utils/pluginSse.js
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/utils/pluginViewResolver.js (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/utils/request.js (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/utils/ruoyi.js (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/utils/scroll-to.js (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/utils/theme.js (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/utils/time.js (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/utils/transportCrypto.js (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/utils/transportCryptoPolicy.js (100%)
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/utils/transportPath.js
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/utils/validate.js
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/auth-center/AuthCenterShell.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/auth-center/changePassword.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/auth-center/consent.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/auth-center/error.vue (100%)
create mode 100644 ruoyi-fastapi-frontend/vue3/web/src/views/auth-center/interactionSecurity.js
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/auth-center/login.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/auth-center/useInteraction.js (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/dashboard/editable-link-group.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/dashboard/index.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/error/401.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/error/404.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/lock.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/login.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/monitor/cache/index.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/monitor/cache/list.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/monitor/druid/index.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/monitor/job/detail.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/monitor/job/index.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/monitor/job/log.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/monitor/job/runtime.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/monitor/logininfor/index.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/monitor/oauthAudit/index.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/monitor/online/index.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/monitor/operlog/detail.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/monitor/operlog/index.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/monitor/server/index.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/monitor/transportCrypto/index.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/redirect/index.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/register.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/system/config/index.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/system/dept/index.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/system/dict/data.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/system/dict/detail.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/system/dict/index.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/system/file/components/FileAclDrawer.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/system/file/components/FileAuditDrawer.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/system/file/components/FileDetailDialog.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/system/file/components/FileReconcileDrawer.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/system/file/components/FileReferenceDrawer.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/system/file/components/FileRetentionPolicyDrawer.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/system/file/components/FileRetentionReminderDrawer.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/system/file/components/FileSearchForm.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/system/file/components/FileStatistics.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/system/file/components/FileTable.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/system/file/components/FileTransferDialog.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/system/file/components/fileFormatters.js (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/system/file/index.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/system/menu/index.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/system/notice/ReadUsers.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/system/notice/index.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/system/oauth/client/index.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/system/oauth/client/secret.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/system/oauth/grant/AccessPolicyDrawer.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/system/oauth/grant/index.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/system/oauth/key/index.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/system/oauth/resource/index.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/system/oauth/scope/index.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/system/oauth/session/index.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/system/plugin/components/PluginConfigDialog.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/system/plugin/components/PluginConfigStatus.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/system/plugin/components/PluginDependencyDialog.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/system/plugin/components/PluginDetailDialog.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/system/plugin/components/PluginDiagnosticDialog.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/system/plugin/components/PluginPlanDialog.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/system/plugin/components/PluginReleaseStatus.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/system/plugin/components/PluginRuntimeMetrics.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/system/plugin/index.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/system/post/index.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/system/role/authUser.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/system/role/index.vue (97%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/system/role/selectUser.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/system/user/authRole.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/system/user/index.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/system/user/profile/index.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/system/user/profile/resetPwd.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/system/user/profile/timezoneSettings.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/system/user/profile/userAvatar.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/system/user/profile/userInfo.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/system/user/view.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/tool/build/CodeTypeDialog.vue (100%)
mode change 100755 => 100644
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/tool/build/DraggableItem.vue (100%)
mode change 100755 => 100644
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/tool/build/IconsDialog.vue (100%)
mode change 100755 => 100644
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/tool/build/RightPanel.vue (100%)
mode change 100755 => 100644
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/tool/build/TreeNodeDialog.vue (100%)
mode change 100755 => 100644
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/tool/build/index.vue (100%)
mode change 100755 => 100644
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/tool/gen/basicInfoForm.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/tool/gen/createTable.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/tool/gen/editTable.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/tool/gen/genInfoForm.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/tool/gen/importTable.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/tool/gen/index.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/src/views/tool/swagger/index.vue (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/tests/auth-center/availability.test.js (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/tests/auth-center/clientForm.test.js (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/tests/auth-center/interactionSecurity.test.js (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/tests/auth-center/oauthUri.test.js (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/tests/auth-center/useInteraction.test.js (100%)
create mode 100644 ruoyi-fastapi-frontend/vue3/web/tests/job/job-form.test.js
create mode 100644 ruoyi-fastapi-frontend/vue3/web/tests/plugins/fixtures/bridge-b0d16a3e/.gitattributes
create mode 100644 ruoyi-fastapi-frontend/vue3/web/tests/plugins/fixtures/bridge-b0d16a3e/pluginBridge.js
create mode 100644 ruoyi-fastapi-frontend/vue3/web/tests/plugins/fixtures/bridge-b0d16a3e/provenance.json
create mode 100644 ruoyi-fastapi-frontend/vue3/web/tests/plugins/fixtures/json-only-host.js
rename ruoyi-fastapi-frontend/{ => vue3/web}/tests/plugins/monacoEnvironment.test.js (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/tests/plugins/network-smoke/server.mjs (100%)
create mode 100644 ruoyi-fastapi-frontend/vue3/web/tests/plugins/pluginBridge.test.js
create mode 100644 ruoyi-fastapi-frontend/vue3/web/tests/plugins/pluginBridgeCompatibility.test.js
rename ruoyi-fastapi-frontend/{ => vue3/web}/tests/plugins/pluginConfigStatus.test.js (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/tests/plugins/pluginDevMock.test.js (96%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/tests/plugins/pluginFrame.test.js (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/tests/plugins/pluginProxy.test.js (99%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/tests/plugins/pluginReleaseFormatter.test.js (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/tests/plugins/pluginRuntimeMetrics.test.js (100%)
create mode 100644 ruoyi-fastapi-frontend/vue3/web/tests/plugins/pluginSse.test.js
rename ruoyi-fastapi-frontend/{ => vue3/web}/tests/plugins/pluginViewResolver.test.js (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/tests/plugins/run-plugin-tests.js (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/tests/run-tests.js (100%)
create mode 100644 ruoyi-fastapi-frontend/vue3/web/tests/time/time.test.js
rename ruoyi-fastapi-frontend/{ => vue3/web}/vite.config.js (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/vite/plugins/auto-import.js (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/vite/plugins/compression.js (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/vite/plugins/index.js (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/vite/plugins/monaco-environment.js (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/vite/plugins/setup-extend.js (100%)
rename ruoyi-fastapi-frontend/{ => vue3/web}/vite/plugins/svg-icon.js (100%)
create mode 100644 ruoyi-fastapi-test/common/browser_harness.py
create mode 100644 ruoyi-fastapi-test/common/page_catalog.py
create mode 100644 ruoyi-fastapi-test/conftest.py
create mode 100644 ruoyi-fastapi-test/docker.env
create mode 100644 ruoyi-fastapi-test/frontend/mobile-behavior.mjs
create mode 100644 ruoyi-fastapi-test/frontend/mobile-harness.mjs
create mode 100644 ruoyi-fastapi-test/monitor/test_oauth_audit.py
create mode 100644 ruoyi-fastapi-test/monitor/test_transport_crypto.py
create mode 100644 ruoyi-fastapi-test/prepare_e2e_compose.py
create mode 100644 ruoyi-fastapi-test/pytest.ini
create mode 100644 ruoyi-fastapi-test/system/page.py
create mode 100644 ruoyi-fastapi-test/system/test_file_management.py
create mode 100644 ruoyi-fastapi-test/system/test_oauth_management.py
create mode 100644 ruoyi-fastapi-test/system/test_plugin_management.py
create mode 100644 ruoyi-fastapi-test/test_account_pages.py
create mode 100644 ruoyi-fastapi-test/test_browser_harness.py
create mode 100644 ruoyi-fastapi-test/test_plugin_response_contract.py
create mode 100644 ruoyi-fastapi-test/tool/test_build.py
diff --git a/.github/requirements.txt b/.github/requirements.txt
new file mode 100644
index 0000000..26cde44
--- /dev/null
+++ b/.github/requirements.txt
@@ -0,0 +1,3 @@
+pytest==9.1.1
+PyYAML==6.0.3
+ruff==0.16.1
diff --git a/.github/ruff.toml b/.github/ruff.toml
new file mode 100644
index 0000000..3c59e39
--- /dev/null
+++ b/.github/ruff.toml
@@ -0,0 +1,2 @@
+# CI 脚本和测试与后端使用相同的 Python 风格。
+extend = "../ruoyi-fastapi-backend/ruff.toml"
diff --git a/.github/scripts/check_compose.py b/.github/scripts/check_compose.py
new file mode 100644
index 0000000..2cc2c83
--- /dev/null
+++ b/.github/scripts/check_compose.py
@@ -0,0 +1,72 @@
+import argparse
+import json
+import os
+import subprocess
+from pathlib import Path
+from typing import Any
+
+ROOT = Path(__file__).resolve().parents[2]
+CONFIGURATIONS = (
+ ('docker.env', 'docker-compose.my.yml', 'my'),
+ ('docker.env', 'docker-compose.pg.yml', 'pg'),
+ ('ruoyi-fastapi-test/docker.env', 'ruoyi-fastapi-test/docker-compose.test.my.yml', 'my'),
+ ('ruoyi-fastapi-test/docker.env', 'ruoyi-fastapi-test/docker-compose.test.pg.yml', 'pg'),
+)
+
+
+def validate_configuration(config: dict[str, Any], framework: str, engine: str, root: Path) -> None:
+ """
+ 核对 Compose 实际解析后的前后端框架及 nginx 来源。
+
+ :param config: docker compose config 的 JSON 结果
+ :param framework: 本次选择的框架
+ :param engine: my 或 pg
+ :param root: 仓库根目录
+ :return: None
+ """
+ expected = (root / 'ruoyi-fastapi-frontend' / framework / 'web').resolve()
+ services = config['services']
+ frontend = services['ruoyi-frontend']
+ build = frontend['build']
+ if Path(build['context']).resolve() != expected:
+ raise ValueError(f'{framework}: 前端构建目录不一致')
+ if not (expected / build.get('dockerfile', 'Dockerfile')).is_file():
+ raise ValueError(f'{framework}: 前端 Dockerfile 不存在')
+ volumes = [
+ volume for volume in frontend.get('volumes', []) if volume.get('target') == '/etc/nginx/conf.d/default.conf'
+ ]
+ nginx = expected / 'bin' / f'nginx.docker{engine}.conf'
+ if len(volumes) != 1 or Path(volumes[0]['source']).resolve() != nginx or not nginx.is_file():
+ raise ValueError(f'{framework}: nginx 配置未选择同一框架')
+ backend = services[f'ruoyi-backend-{engine}']
+ if backend['environment'].get('RUOYI_PLUGIN_FRONTEND_FRAMEWORK') != framework:
+ raise ValueError(f'{framework}: 后端插件框架与前端不一致')
+
+
+def main() -> None:
+ """
+ 使用两种框架解析全部生产和测试 Compose,无需启动容器。
+
+ :return: None
+ """
+ parser = argparse.ArgumentParser()
+ parser.add_argument('--docker', default='docker')
+ args = parser.parse_args()
+ for framework in ('vue2', 'vue3'):
+ for env_file, compose_file, engine in CONFIGURATIONS:
+ result = subprocess.run(
+ [args.docker, 'compose', '--env-file', env_file, '-f', compose_file, 'config', '--format', 'json'],
+ cwd=ROOT,
+ env={**os.environ, 'FRONTEND_FRAMEWORK': framework},
+ check=True,
+ capture_output=True,
+ text=True,
+ encoding='utf-8',
+ timeout=60,
+ )
+ validate_configuration(json.loads(result.stdout), framework, engine, ROOT)
+ print(f'{framework}: {compose_file} PASS')
+
+
+if __name__ == '__main__':
+ main()
diff --git a/.github/scripts/check_test_report.py b/.github/scripts/check_test_report.py
new file mode 100644
index 0000000..d9a057e
--- /dev/null
+++ b/.github/scripts/check_test_report.py
@@ -0,0 +1,39 @@
+import argparse
+import xml.etree.ElementTree as ET
+from pathlib import Path
+
+
+def check_report(path: Path, *, allow_skips: bool = False) -> tuple[int, int]:
+ """
+ 验证测试确实执行,专用验收任务不允许缺少依赖而全部跳过。
+
+ :param path: pytest JUnit 报告路径
+ :param allow_skips: 是否允许普通跨平台回归中的预期跳过
+ :return: 实际执行及跳过的测试数量
+ """
+ root = ET.parse(path).getroot()
+ cases = root.findall('.//testcase')
+ skipped = sum(case.find('skipped') is not None for case in cases)
+ executed = len(cases) - skipped
+ failures = sum(case.find('failure') is not None or case.find('error') is not None for case in cases)
+ if executed == 0 or failures or (skipped and not allow_skips):
+ raise ValueError(f'{path}: 执行 {executed},跳过 {skipped},失败 {failures}')
+ return executed, skipped
+
+
+def main() -> None:
+ """
+ 检查报告并将执行数量写入 CI 摘要。
+
+ :return: None
+ """
+ parser = argparse.ArgumentParser()
+ parser.add_argument('report', type=Path)
+ parser.add_argument('--allow-skips', action='store_true')
+ args = parser.parse_args()
+ executed, skipped = check_report(args.report, allow_skips=args.allow_skips)
+ print(f'{args.report}: executed={executed}, skipped={skipped}')
+
+
+if __name__ == '__main__':
+ main()
diff --git a/.github/scripts/ci_plan.py b/.github/scripts/ci_plan.py
new file mode 100644
index 0000000..28acc93
--- /dev/null
+++ b/.github/scripts/ci_plan.py
@@ -0,0 +1,245 @@
+import argparse
+import json
+import os
+import subprocess
+from pathlib import Path
+from typing import Any
+
+ROOT = Path(__file__).resolve().parents[2]
+FRAMEWORKS = ('vue2', 'vue3')
+PYTHON_VERSIONS = ('3.10', '3.11', '3.12', '3.13')
+CATEGORIES = ('quality', 'backend', 'frontend', 'integration', 'e2e', 'native')
+FRONTEND_FILE_PATH_PARTS = 4
+
+
+# 将影响范围规则集中列出,便于逐条审核;拆散分支会掩盖任务之间的联动。
+def build_plan(paths: list[str], *, full: bool = False) -> dict[str, Any]: # noqa: PLR0912, PLR0915
+ """
+ 根据源码、共享依赖及测试的影响范围生成 CI 执行计划。
+
+ :param paths: 变更文件,重命名需要同时传入旧路径和新路径
+ :param full: 是否执行完整兼容矩阵
+ :return: 分类开关、矩阵和必跑任务
+ """
+ projects: set[tuple[str, str]] = set()
+ browsers: set[str] = set()
+ networks: set[str] = set()
+ backend = native = database = release = False
+ normalized = [path.replace('\\', '/').removeprefix('./') for path in paths]
+ shared = full or any(
+ path.startswith(('.github/', 'ruoyi-fastapi-backend/requirements'))
+ or path
+ in (
+ 'ruoyi-fastapi-backend/pyproject.toml',
+ 'ruoyi-fastapi-backend/tests/conftest.py',
+ 'ruoyi-fastapi-test/requirements.txt',
+ '.gitignore',
+ )
+ for path in normalized
+ )
+ if shared:
+ projects.update((framework, target) for framework in FRAMEWORKS for target in ('web', 'mobile'))
+ browsers.update(FRAMEWORKS)
+ networks.update(FRAMEWORKS)
+ backend = native = database = release = True
+ for path in normalized:
+ if path.lower().endswith('.md'):
+ continue
+ parts = path.split('/')
+ if path.startswith('ruoyi-fastapi-backend/'):
+ backend = True
+ relative = path.removeprefix('ruoyi-fastapi-backend/')
+ if not relative.startswith(('tests/', 'ruff.toml')):
+ browsers.update(FRAMEWORKS)
+ if relative.startswith(('common/', 'config/', 'utils/', 'sql/', 'scripts/verify_timezone_database')):
+ database = True
+ if relative.startswith(
+ (
+ 'plugins/',
+ 'module_plugin/',
+ 'cli/',
+ 'tests/plugins/',
+ 'tests/cli/',
+ 'tests/module_plugin/',
+ 'tests/sql/',
+ )
+ ):
+ native = release = True
+ if relative == 'scripts/build_native_plugin.py':
+ native = True
+ if relative.startswith(
+ ('plugins/', 'module_plugin/', 'common/', 'config/', 'middlewares/', 'utils/')
+ ) or relative in ('server.py', 'app.py', 'module_admin/service/login_service.py'):
+ networks.update(FRAMEWORKS)
+ if relative.startswith(('config/', 'sql/', 'scripts/plugin_', 'tests/server/')) or relative in (
+ 'server.py',
+ 'app.py',
+ ):
+ release = True
+ if relative.startswith('tests/plugins/integration/') or relative in (
+ 'tests/scheduler_helpers.py',
+ 'tests/plugins/core/runtime/test_browser_session.py',
+ ):
+ networks.update(FRAMEWORKS)
+ if relative in (
+ 'plugins/core/frontend.py',
+ 'plugins/core/environment.py',
+ 'plugins/core/manifest/schema.py',
+ ):
+ projects.update((framework, target) for framework in FRAMEWORKS for target in ('web', 'mobile'))
+ if relative.startswith('plugins/examples/python/bundle_demo/web/'):
+ projects.update((framework, 'web') for framework in FRAMEWORKS)
+ elif (
+ len(parts) >= FRONTEND_FILE_PATH_PARTS
+ and parts[0] == 'ruoyi-fastapi-frontend'
+ and parts[1] in FRAMEWORKS
+ and parts[2] in ('web', 'mobile')
+ ):
+ framework, target = parts[1:3]
+ projects.add((framework, target))
+ if target == 'web':
+ browsers.add(framework)
+ networks.add(framework)
+ if target == 'web' and (parts[-1].startswith('pluginBridge') or parts[-1] == 'package.json'):
+ backend = native = release = True
+ elif path.startswith('ruoyi-fastapi-frontend/'):
+ projects.update((framework, target) for framework in FRAMEWORKS for target in ('web', 'mobile'))
+ browsers.update(FRAMEWORKS)
+ networks.update(FRAMEWORKS)
+ elif path.startswith('ruoyi-fastapi-test/frontend/'):
+ projects.update((framework, 'mobile') for framework in FRAMEWORKS)
+ elif path.startswith('ruoyi-fastapi-test/time-contract/'):
+ projects.update((framework, target) for framework in FRAMEWORKS for target in ('web', 'mobile'))
+ database = True
+ browsers.update(FRAMEWORKS)
+ elif path.startswith('ruoyi-fastapi-test/') or path in (
+ 'docker.env',
+ 'docker-compose.my.yml',
+ 'docker-compose.pg.yml',
+ ):
+ browsers.update(FRAMEWORKS)
+ if 'docker' in path or path.endswith('.sql'):
+ database = True
+
+ required = {
+ 'quality': True,
+ 'backend': backend,
+ 'frontend': bool(projects),
+ 'integration': database or release or bool(networks),
+ 'e2e': bool(browsers),
+ 'native': native,
+ }
+ return {
+ 'full': full,
+ 'required': required,
+ 'frontend_matrix': {
+ 'include': [{'framework': framework, 'target': target} for framework, target in sorted(projects)]
+ },
+ 'e2e_matrix': {
+ 'include': [
+ {'framework': framework, 'database': engine, 'python': version}
+ for framework in sorted(browsers)
+ for engine in ('mysql', 'postgresql')
+ for version in (PYTHON_VERSIONS if full else ('3.12',))
+ ]
+ },
+ 'network_frameworks': sorted(networks),
+ 'database': database,
+ 'release': release,
+ 'paths': normalized,
+ }
+
+
+def check_results(plan: dict[str, Any], needs: dict[str, Any]) -> list[str]:
+ """
+ 核验必跑任务,拒绝失败、取消、缺失和意外跳过。
+
+ :param plan: 选路任务生成的计划
+ :param needs: GitHub needs 上下文
+ :return: 不满足门禁的原因
+ """
+ errors = []
+ if needs.get('plan', {}).get('result') != 'success':
+ errors.append('CI 计划未成功生成')
+ required = plan.get('required', {})
+ if set(required) != set(CATEGORIES) or not all(isinstance(value, bool) for value in required.values()):
+ return [*errors, 'CI 计划缺少完整的任务分类']
+ for category in CATEGORIES:
+ result = needs.get(category, {}).get('result')
+ allowed = ('success',) if required[category] else ('success', 'skipped')
+ if result not in allowed:
+ errors.append(f'{category}: {result or "missing"},预期 {"/".join(allowed)}')
+ return errors
+
+
+def changed_paths(base: str) -> list[str]:
+ """
+ 从 Git 获取变更文件,保留删除及重命名前后的路径。
+
+ :param base: 已检出的比较基准提交
+ :return: 相对仓库根目录的路径
+ """
+ result = subprocess.run(
+ ['git', 'diff', '--no-renames', '--name-only', '-z', base, 'HEAD'],
+ cwd=ROOT,
+ check=True,
+ capture_output=True,
+ timeout=60,
+ )
+ return [path for path in result.stdout.decode('utf-8').split('\0') if path]
+
+
+def write_outputs(plan: dict[str, Any]) -> None:
+ """
+ 输出 GitHub 任务参数及可审阅的执行摘要。
+
+ :param plan: 本次运行计划
+ :return: None
+ """
+ payload = {
+ **plan['required'],
+ **{key: value for key, value in plan.items() if key not in ('required', 'paths')},
+ 'plan_json': plan,
+ }
+ if output := os.environ.get('GITHUB_OUTPUT'):
+ with Path(output).open('a', encoding='utf-8') as stream:
+ for key, value in payload.items():
+ stream.write(f'{key}={json.dumps(value, ensure_ascii=False, separators=(",", ":"))}\n')
+ if summary := os.environ.get('GITHUB_STEP_SUMMARY'):
+ with Path(summary).open('a', encoding='utf-8') as stream:
+ stream.write('## CI 执行计划\n\n| 分类 | 执行 |\n|---|---|\n')
+ for category, required in plan['required'].items():
+ stream.write(f'| {category} | {"运行" if required else "无相关改动"} |\n')
+ stream.write(f'\n完整矩阵:{plan["full"]}\n')
+ print(json.dumps(plan, ensure_ascii=False, indent=2))
+
+
+def main() -> None:
+ """
+ 执行变更选路或最终结果校验。
+
+ :return: None
+ """
+ parser = argparse.ArgumentParser()
+ parser.add_argument('command', choices=('plan', 'gate'))
+ args = parser.parse_args()
+ if args.command == 'gate':
+ plan = json.loads(os.environ.get('PLAN_JSON') or '{}')
+ needs = json.loads(os.environ.get('NEEDS_JSON') or '{}')
+ errors = check_results(plan, needs)
+ if errors:
+ raise SystemExit('\n'.join(errors))
+ print('所有应执行的 CI 分类均已通过。')
+ return
+ event = json.loads(Path(os.environ['GITHUB_EVENT_PATH']).read_text(encoding='utf-8'))
+ event_name = os.environ['GITHUB_EVENT_NAME']
+ full = event_name in ('push', 'schedule') or (
+ str(event.get('inputs', {}).get('full', 'true')).lower() == 'true' and event_name == 'workflow_dispatch'
+ )
+ base = event.get('pull_request', {}).get('base', {}).get('sha', '')
+ paths = changed_paths(base) if base else ['.github/workflows/ci.yml']
+ write_outputs(build_plan(paths, full=full))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/.github/scripts/install_ai_test_dependencies.py b/.github/scripts/install_ai_test_dependencies.py
new file mode 100644
index 0000000..6ab49c7
--- /dev/null
+++ b/.github/scripts/install_ai_test_dependencies.py
@@ -0,0 +1,23 @@
+import subprocess
+import sys
+from pathlib import Path
+
+import yaml
+
+
+def main() -> None:
+ """
+ 从 AI 插件清单安装测试依赖,避免 CI 维护第二份版本列表。
+
+ :return: None
+ """
+ backend = Path(__file__).resolve().parents[2] / 'ruoyi-fastapi-backend'
+ manifest = yaml.safe_load((backend / 'plugins/ai/plugin.yaml').read_text(encoding='utf-8'))
+ requirements = manifest['dependencies']['python']
+ if not requirements or not all(isinstance(item, str) and not item.startswith('-') for item in requirements):
+ raise ValueError('AI 插件 Python 依赖清单无效')
+ subprocess.run([sys.executable, '-m', 'pip', 'install', *requirements], check=True, timeout=900)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/.github/scripts/mobile_smoke.py b/.github/scripts/mobile_smoke.py
new file mode 100644
index 0000000..1530bb8
--- /dev/null
+++ b/.github/scripts/mobile_smoke.py
@@ -0,0 +1,184 @@
+import argparse
+import asyncio
+import json
+import os
+import re
+import threading
+import time
+from collections.abc import Awaitable, Callable
+from functools import partial
+from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
+from pathlib import Path
+from typing import Any
+from urllib.parse import urlparse
+
+from playwright.async_api import Route, async_playwright, expect
+
+ROOT = Path(__file__).resolve().parents[2]
+TOKEN = 'isolated-mobile-smoke-token'
+USER = {
+ 'userId': 7,
+ 'userName': 'ci-user',
+ 'nickName': '移动端 CI',
+ 'avatar': '',
+ 'phonenumber': '13800000000',
+ 'email': 'mobile-ci@example.test',
+ 'createTime': '2026-01-01T00:00:00Z',
+ 'timeZone': 'America/New_York',
+}
+
+
+def create_api_handler(errors: list[str], observed: list[dict[str, Any]]) -> Callable[[Route], Awaitable[None]]:
+ """
+ 创建隔离 API 处理函数,向共享记录追加请求及异常。
+
+ :param errors: 本次验收收集的异常说明
+ :param observed: 实际发出的 API 请求记录
+ :return: 供浏览器路由使用的异步处理函数
+ """
+
+ async def api(route: Route) -> None:
+ """
+ 隔离 API 边界,记录真实构建发出的认证和时区请求。
+
+ :param route: 当前浏览器 API 请求
+ :return: None
+ """
+ request = route.request
+ path = urlparse(request.url).path
+ headers = {'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': '*'}
+ if request.method == 'OPTIONS':
+ await route.fulfill(status=204, headers=headers)
+ return
+ observed.append(
+ {
+ 'path': path,
+ 'method': request.method,
+ 'authorization': request.headers.get('authorization'),
+ 'timezone': request.headers.get('x-timezone'),
+ }
+ )
+ payload: dict[str, Any]
+ if path == '/transport/crypto/frontend-config':
+ payload = {
+ 'code': 200,
+ 'data': {
+ 'transportCryptoEnabled': False,
+ 'transportCryptoMode': 'off',
+ 'transportCryptoActive': False,
+ 'configExpireAt': int(time.time()) + 600,
+ },
+ }
+ elif path == '/captchaImage':
+ payload = {'code': 200, 'captchaEnabled': False}
+ elif path == '/login':
+ if 'ci-user' not in (request.post_data or ''):
+ errors.append('登录请求未包含输入的用户名')
+ payload = {'code': 200, 'token': TOKEN}
+ elif path == '/getInfo':
+ payload = {
+ 'code': 200,
+ 'user': USER,
+ 'roles': ['admin'],
+ 'permissions': ['*:*:*'],
+ 'appTimezone': 'Asia/Shanghai',
+ }
+ elif path == '/system/user/profile':
+ payload = {'code': 200, 'data': USER, 'postGroup': 'CI', 'roleGroup': '管理员'}
+ elif path == '/logout':
+ payload = {'code': 200}
+ else:
+ errors.append(f'未预期的 API 请求:{path}')
+ await route.fulfill(status=500, json={'code': 500, 'msg': 'unexpected smoke request'}, headers=headers)
+ return
+ await route.fulfill(json=payload, headers=headers)
+
+ return api
+
+
+async def run_smoke(framework: str, base_url: str, output: Path) -> None:
+ """
+ 使用真实 H5 构建验证登录、持久化身份和个人资料页。
+
+ :param framework: vue2 或 vue3
+ :param base_url: 仅用于本次测试的静态文件服务器
+ :param output: 浏览器报告目录
+ :return: None
+ """
+ logs: list[str] = []
+ errors: list[str] = []
+ observed: list[dict[str, Any]] = []
+ api = create_api_handler(errors, observed)
+
+ async with async_playwright() as playwright:
+ executable = os.environ.get('RUOYI_SMOKE_BROWSER_EXECUTABLE')
+ browser = await playwright.chromium.launch(
+ headless=True, **({'executable_path': executable} if executable else {})
+ )
+ context = await browser.new_context(viewport={'width': 390, 'height': 844}, timezone_id='UTC')
+ await context.tracing.start(screenshots=True, snapshots=True, sources=True)
+ await context.route('http://localhost:9099/**', api)
+ page = await context.new_page()
+ page.on('pageerror', lambda error: errors.append(str(error)))
+ page.on('console', lambda message: logs.append(f'{message.type}: {message.text}'))
+ failed = False
+ try:
+ await page.goto(f'{base_url}/#/pages/login')
+ await expect(page.locator('input[type="text"]')).to_be_visible(timeout=30000)
+ await expect(page.locator('input')).to_have_count(2, timeout=15000)
+ await page.locator('input[type="text"]').fill('ci-user')
+ await page.locator('input[type="password"]').fill('ci-test-password')
+ await page.locator('uni-button').filter(has_text=re.compile(r'登\s*录')).click()
+ await expect(page).to_have_url(re.compile(r'/pages/index'), timeout=30000)
+ await page.goto(f'{base_url}/#/pages/mine/info/index')
+ await expect(page.get_by_text(USER['nickName'], exact=True)).to_be_visible(timeout=30000)
+ await expect(page.get_by_text('2025-12-31 19:00:00', exact=True)).to_be_visible(timeout=15000)
+ profile_requests = [request for request in observed if request['path'] == '/system/user/profile']
+ assert profile_requests, '个人资料页未发送真实 API 请求'
+ assert all(request['authorization'] == f'Bearer {TOKEN}' for request in profile_requests)
+ assert all(request['timezone'] == 'America/New_York' for request in profile_requests)
+ assert not errors, errors
+ (output / 'result.json').write_text(
+ json.dumps({'framework': framework, 'ok': True, 'requests': observed}, ensure_ascii=False, indent=2),
+ encoding='utf-8',
+ )
+ print(f'{framework}: H5 login, persisted session and timezone profile PASS')
+ except BaseException:
+ failed = True
+ await page.screenshot(path=str(output / 'failure.png'), full_page=True)
+ (output / 'page.html').write_text(await page.content(), encoding='utf-8')
+ raise
+ finally:
+ (output / 'browser.log').write_text('\n'.join([*logs, *errors]), encoding='utf-8')
+ await context.tracing.stop(**({'path': str(output / 'trace.zip')} if failed else {}))
+ await context.close()
+ await browser.close()
+
+
+def main() -> None:
+ """
+ 在独立临时端口启动 H5 静态服务,结束后关闭自身创建的资源。
+
+ :return: None
+ """
+ parser = argparse.ArgumentParser()
+ parser.add_argument('--framework', choices=('vue2', 'vue3'), required=True)
+ args = parser.parse_args()
+ distribution = ROOT / 'ruoyi-fastapi-frontend' / args.framework / 'mobile/dist/build/h5'
+ if not (distribution / 'index.html').is_file():
+ raise SystemExit(f'请先构建 H5:{distribution}')
+ output = ROOT / 'target/ci/mobile-smoke' / args.framework
+ output.mkdir(parents=True, exist_ok=True)
+ server = ThreadingHTTPServer(('127.0.0.1', 0), partial(SimpleHTTPRequestHandler, directory=str(distribution)))
+ thread = threading.Thread(target=server.serve_forever, daemon=True)
+ thread.start()
+ try:
+ asyncio.run(run_smoke(args.framework, f'http://127.0.0.1:{server.server_port}', output))
+ finally:
+ server.shutdown()
+ server.server_close()
+ thread.join(timeout=5)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/.github/tests/test_ci_contracts.py b/.github/tests/test_ci_contracts.py
new file mode 100644
index 0000000..8e52c66
--- /dev/null
+++ b/.github/tests/test_ci_contracts.py
@@ -0,0 +1,91 @@
+import importlib.util
+from copy import deepcopy
+from pathlib import Path
+from types import ModuleType
+from typing import Any
+
+import pytest
+
+ROOT = Path(__file__).resolve().parents[2]
+
+
+def load_script(name: str) -> ModuleType:
+ """
+ 按文件路径加载 CI 脚本,避免依赖仓库外的模块搜索路径。
+
+ :param name: 不含扩展名的脚本名称
+ :return: 已执行模块顶层定义的脚本模块
+ """
+ spec = importlib.util.spec_from_file_location(name, ROOT / '.github/scripts' / f'{name}.py')
+ module = importlib.util.module_from_spec(spec)
+ spec.loader.exec_module(module)
+ return module
+
+
+compose = load_script('check_compose')
+reports = load_script('check_test_report')
+
+
+@pytest.fixture
+def configuration() -> dict[str, Any]:
+ """提供前端、nginx 和后端框架一致的 Compose 配置。"""
+ frontend = ROOT / 'ruoyi-fastapi-frontend/vue2/web'
+ return {
+ 'services': {
+ 'ruoyi-frontend': {
+ 'build': {'context': str(frontend), 'dockerfile': 'Dockerfile'},
+ 'volumes': [
+ {'source': str(frontend / 'bin/nginx.dockermy.conf'), 'target': '/etc/nginx/conf.d/default.conf'}
+ ],
+ },
+ 'ruoyi-backend-my': {'environment': {'RUOYI_PLUGIN_FRONTEND_FRAMEWORK': 'vue2'}},
+ }
+ }
+
+
+def test_compose_accepts_consistent_framework(configuration: dict[str, Any]) -> None:
+ """框架一致的 Compose 配置可以通过校验。"""
+ compose.validate_configuration(configuration, 'vue2', 'my', ROOT)
+
+
+@pytest.mark.parametrize('component', ['build', 'nginx', 'backend'])
+def test_compose_rejects_mixed_framework_deployment(configuration: dict[str, Any], component: str) -> None:
+ """前端、nginx 或后端混用框架时拒绝部署配置。"""
+ invalid = deepcopy(configuration)
+ if component == 'build':
+ invalid['services']['ruoyi-frontend']['build']['context'] = str(ROOT / 'ruoyi-fastapi-frontend/vue3/web')
+ elif component == 'nginx':
+ invalid['services']['ruoyi-frontend']['volumes'][0]['source'] = str(
+ ROOT / 'ruoyi-fastapi-frontend/vue3/web/bin/nginx.dockermy.conf'
+ )
+ else:
+ invalid['services']['ruoyi-backend-my']['environment']['RUOYI_PLUGIN_FRONTEND_FRAMEWORK'] = 'vue3'
+ with pytest.raises(ValueError):
+ compose.validate_configuration(invalid, 'vue2', 'my', ROOT)
+
+
+@pytest.mark.parametrize(
+ 'cases',
+ ['', '', '', ''],
+)
+def test_report_rejects_empty_skipped_or_failed_verification(tmp_path: Path, cases: str) -> None:
+ """空报告、全部跳过和失败用例都不能视为验收成功。"""
+ report = tmp_path / 'results.xml'
+ report.write_text(f'{cases}', encoding='utf-8')
+ with pytest.raises(ValueError):
+ reports.check_report(report)
+
+
+def test_expected_platform_skip_still_requires_executed_tests(tmp_path: Path) -> None:
+ """允许预期平台跳过时仍要求实际执行测试。"""
+ report = tmp_path / 'results.xml'
+ report.write_text(
+ '', encoding='utf-8'
+ )
+ assert reports.check_report(report, allow_skips=True) == (1, 1)
+
+
+def test_missing_report_is_not_success(tmp_path: Path) -> None:
+ """报告缺失时不能视为验收成功。"""
+ with pytest.raises(FileNotFoundError):
+ reports.check_report(tmp_path / 'missing.xml')
diff --git a/.github/tests/test_ci_plan.py b/.github/tests/test_ci_plan.py
new file mode 100644
index 0000000..fe548e3
--- /dev/null
+++ b/.github/tests/test_ci_plan.py
@@ -0,0 +1,169 @@
+import importlib.util
+from pathlib import Path
+
+import pytest
+
+SCRIPT = Path(__file__).resolve().parents[1] / 'scripts' / 'ci_plan.py'
+SPEC = importlib.util.spec_from_file_location('ci_plan', SCRIPT)
+ci = importlib.util.module_from_spec(SPEC)
+SPEC.loader.exec_module(ci)
+FRONTEND_PROJECT_COUNT = 4
+STANDARD_E2E_COMBINATION_COUNT = 4
+FULL_E2E_COMBINATION_COUNT = 16
+
+
+def test_readme_change_only_runs_quality() -> None:
+ """仅文档变更时只执行质量检查。"""
+ assert ci.build_plan(['README.md'])['required'] == {
+ 'quality': True,
+ 'backend': False,
+ 'frontend': False,
+ 'integration': False,
+ 'e2e': False,
+ 'native': False,
+ }
+
+
+@pytest.mark.parametrize(
+ 'path',
+ [
+ 'ruoyi-fastapi-backend/cli/groups/plugin/options.py',
+ 'ruoyi-fastapi-backend/cli/groups/plugin/controller.py',
+ 'ruoyi-fastapi-backend/cli/runtime/plugin/service.py',
+ 'ruoyi-fastapi-backend/tests/module_plugin/controller/test_plugin_release_controller.py',
+ 'ruoyi-fastapi-backend/tests/sql/test_plugin_release_schema.py',
+ ],
+)
+def test_shared_plugin_changes_retain_windows_and_release_coverage(path: str) -> None:
+ """共享插件变更保留原生模块及发布验收覆盖。"""
+ plan = ci.build_plan([path])
+ assert plan['required']['backend']
+ assert plan['required']['native']
+ assert plan['release']
+
+
+def test_native_builder_change_runs_native_verification() -> None:
+ """原生构建器变更必须触发原生验收。"""
+ plan = ci.build_plan(['ruoyi-fastapi-backend/scripts/build_native_plugin.py'])
+ assert plan['required']['native']
+
+
+def test_bundle_example_change_runs_both_web_consumers() -> None:
+ """示例开发代理由两版 Web 测试直接引用,变更时均应执行。"""
+ plan = ci.build_plan(['ruoyi-fastapi-backend/plugins/examples/python/bundle_demo/web/dev/mockRequest.js'])
+ assert plan['frontend_matrix'] == {
+ 'include': [
+ {'framework': 'vue2', 'target': 'web'},
+ {'framework': 'vue3', 'target': 'web'},
+ ]
+ }
+
+
+@pytest.mark.parametrize(
+ 'path',
+ [
+ 'ruoyi-fastapi-backend/module_admin/service/login_service.py',
+ 'ruoyi-fastapi-backend/tests/scheduler_helpers.py',
+ 'ruoyi-fastapi-backend/tests/plugins/core/runtime/test_browser_session.py',
+ ],
+)
+def test_shared_auth_and_test_helpers_keep_both_network_suites(path: str) -> None:
+ """共享鉴权和测试夹具变更触发两版网络验收。"""
+ plan = ci.build_plan([path])
+ assert plan['required']['backend']
+ assert plan['required']['integration']
+ assert plan['network_frameworks'] == ['vue2', 'vue3']
+
+
+@pytest.mark.parametrize(
+ 'path', ['.github/workflows/ci.yml', '.github/scripts/ci_plan.py', 'ruoyi-fastapi-backend/requirements-test.txt']
+)
+def test_ci_and_dependency_changes_select_every_category(path: str) -> None:
+ """CI 及共享依赖变更覆盖全部任务分类。"""
+ plan = ci.build_plan([path])
+ assert all(plan['required'].values())
+ assert len(plan['frontend_matrix']['include']) == FRONTEND_PROJECT_COUNT
+ assert len(plan['e2e_matrix']['include']) == STANDARD_E2E_COMBINATION_COUNT
+
+
+def test_mobile_change_does_not_build_other_projects_or_web_e2e() -> None:
+ """单个移动端变更只选中对应工程。"""
+ plan = ci.build_plan(['ruoyi-fastapi-frontend/vue2/mobile/src/utils/request.js'])
+ assert plan['frontend_matrix'] == {'include': [{'framework': 'vue2', 'target': 'mobile'}]}
+ assert not plan['required']['e2e']
+
+
+@pytest.mark.parametrize(
+ 'path',
+ [
+ 'ruoyi-fastapi-test/frontend/mobile-harness.mjs',
+ 'ruoyi-fastapi-test/frontend/mobile-behavior.mjs',
+ ],
+)
+def test_shared_mobile_harness_runs_both_mobile_projects(path: str) -> None:
+ """共享移动端测试工具变更同时验证两版工程。"""
+ plan = ci.build_plan([path])
+ assert plan['required']['frontend']
+ assert plan['frontend_matrix'] == {
+ 'include': [
+ {'framework': 'vue2', 'target': 'mobile'},
+ {'framework': 'vue3', 'target': 'mobile'},
+ ]
+ }
+ assert not plan['required']['e2e']
+
+
+def test_web_change_selects_matching_framework_and_both_databases() -> None:
+ """Web 变更使用对应框架并覆盖两类数据库。"""
+ plan = ci.build_plan(['ruoyi-fastapi-frontend/vue2/web/src/views/login.vue'])
+ assert plan['network_frameworks'] == ['vue2']
+ assert {(row['framework'], row['database'], row['python']) for row in plan['e2e_matrix']['include']} == {
+ ('vue2', 'mysql', '3.12'),
+ ('vue2', 'postgresql', '3.12'),
+ }
+
+
+def test_shared_time_contract_runs_all_frontends_and_database_checks() -> None:
+ """共享时间契约触发全部前端及数据库检查。"""
+ plan = ci.build_plan(['ruoyi-fastapi-test/time-contract/fixtures.json'])
+ assert len(plan['frontend_matrix']['include']) == FRONTEND_PROJECT_COUNT
+ assert plan['database']
+
+
+def test_framework_move_keeps_both_sides_in_plan() -> None:
+ """跨框架移动文件保留来源和目标两侧验证。"""
+ plan = ci.build_plan(
+ ['ruoyi-fastapi-frontend/vue2/web/src/utils/demo.js', 'ruoyi-fastapi-frontend/vue3/web/src/utils/demo.js']
+ )
+ assert plan['network_frameworks'] == ['vue2', 'vue3']
+
+
+def test_full_run_preserves_python_database_and_framework_combinations() -> None:
+ """完整运行保留 Python、数据库和框架的全部组合。"""
+ plan = ci.build_plan([], full=True)
+ assert all(plan['required'].values())
+ assert len(plan['e2e_matrix']['include']) == FULL_E2E_COMBINATION_COUNT
+ assert {row['python'] for row in plan['e2e_matrix']['include']} == {'3.10', '3.11', '3.12', '3.13'}
+
+
+@pytest.mark.parametrize('result', ['failure', 'cancelled', 'skipped', None])
+def test_gate_rejects_unsuccessful_required_job(result: str | None) -> None:
+ """必跑任务失败、取消、跳过或缺失均不能通过门禁。"""
+ plan = ci.build_plan(['README.md'])
+ needs = {'plan': {'result': 'success'}, **{name: {'result': 'skipped'} for name in ci.CATEGORIES}}
+ needs['quality']['result'] = result
+ assert ci.check_results(plan, needs)
+
+
+def test_gate_allows_only_unselected_jobs_to_skip() -> None:
+ """只有未选中的任务允许跳过。"""
+ plan = ci.build_plan(['README.md'])
+ needs = {'plan': {'result': 'success'}, **{name: {'result': 'skipped'} for name in ci.CATEGORIES}}
+ needs['quality']['result'] = 'success'
+ assert ci.check_results(plan, needs) == []
+
+
+def test_gate_fails_closed_when_plan_or_job_is_missing() -> None:
+ """缺少执行计划或任务结果时门禁必须拒绝放行。"""
+ assert ci.check_results({}, {})
+ assert ci.check_results(ci.build_plan(['README.md']), {'plan': {'result': 'success'}})
diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml
deleted file mode 100644
index 7c332c9..0000000
--- a/.github/workflows/backend.yml
+++ /dev/null
@@ -1,64 +0,0 @@
-name: Backend Tests
-
-on:
- push:
- branches: [master]
- pull_request:
- branches: [master]
-
-jobs:
- pytest:
- runs-on: ubuntu-latest
- env:
- # Typer otherwise forces ANSI help output when GITHUB_ACTIONS is set.
- _TYPER_FORCE_DISABLE_TERMINAL: "1"
- strategy:
- fail-fast: false
- matrix:
- python-version: ["3.10", "3.11", "3.12", "3.13"]
-
- defaults:
- run:
- working-directory: ruoyi-fastapi-backend
-
- steps:
- - uses: actions/checkout@v5
-
- - name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v6
- with:
- python-version: ${{ matrix.python-version }}
- cache: pip
- cache-dependency-path: |
- ruoyi-fastapi-backend/requirements.txt
- ruoyi-fastapi-backend/plugins/ai/plugin.yaml
-
- - name: Install dependencies
- run: |
- python -m pip install --upgrade pip
- pip install -r requirements.txt pytest pytest-asyncio aiosqlite
-
- - name: Install AI plugin dependencies
- run: |
- pip install \
- agno==2.8.6 \
- anthropic==0.120.2 \
- cerebras-cloud-sdk==1.91.0 \
- cohere==7.0.8 \
- google-genai==2.16.0 \
- groq==1.6.0 \
- litellm==1.94.0 \
- llama-api-client==0.6.0 \
- mistralai==2.8.0 \
- ollama==0.6.2 \
- openai==2.52.0 \
- portkey-ai==2.3.4
-
- - name: Run backend tests
- run: pytest -q
-
- - name: Check timezone behavior under independent host timezones
- run: |
- for zone in UTC Asia/Shanghai America/New_York; do
- TZ="$zone" python -m pytest -q tests/common/test_types.py tests/common/test_query_time.py tests/utils/test_time_util.py tests/config/test_scheduler_database.py tests/utils/test_cron_util.py tests/module_admin/controller/test_job_controller.py tests/config/test_scheduler_job_persistence.py
- done
diff --git a/.github/workflows/ci-backend.yml b/.github/workflows/ci-backend.yml
new file mode 100644
index 0000000..d447384
--- /dev/null
+++ b/.github/workflows/ci-backend.yml
@@ -0,0 +1,110 @@
+name: CI / Backend
+
+on:
+ workflow_call:
+
+permissions:
+ contents: read
+
+env:
+ _TYPER_FORCE_DISABLE_TERMINAL: '1'
+ PYTHONUTF8: '1'
+
+jobs:
+ unit-contract:
+ name: Python ${{ matrix.python }}
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+ strategy:
+ fail-fast: false
+ matrix:
+ python: ['3.10', '3.11', '3.12', '3.13']
+ defaults:
+ run:
+ working-directory: ruoyi-fastapi-backend
+ steps:
+ - uses: actions/checkout@v5
+ - uses: actions/setup-python@v6
+ with:
+ python-version: ${{ matrix.python }}
+ cache: pip
+ cache-dependency-path: |
+ ruoyi-fastapi-backend/requirements*.txt
+ ruoyi-fastapi-backend/plugins/ai/plugin.yaml
+ - name: Install test dependencies
+ run: python -m pip install -r requirements.txt -r requirements-test.txt
+ - name: Install AI plugin dependencies from its manifest
+ run: python ../.github/scripts/install_ai_test_dependencies.py
+ - name: Run backend unit and contract tests
+ run: python -m pytest tests -q -m 'not browser and not native and not integration' --strict-markers --durations=20 --junitxml=target/ci/backend.xml
+ - name: Verify tests were executed
+ run: python ../.github/scripts/check_test_report.py target/ci/backend.xml --allow-skips
+ - uses: actions/upload-artifact@v6
+ if: always()
+ with:
+ name: backend-python-${{ matrix.python }}
+ path: ruoyi-fastapi-backend/target/ci/
+ retention-days: 7
+
+ windows-contract:
+ name: Windows CLI and paths
+ runs-on: windows-latest
+ timeout-minutes: 20
+ defaults:
+ run:
+ working-directory: ruoyi-fastapi-backend
+ steps:
+ - uses: actions/checkout@v5
+ - uses: actions/setup-python@v6
+ with:
+ python-version: '3.12'
+ cache: pip
+ cache-dependency-path: ruoyi-fastapi-backend/requirements*.txt
+ - run: python -m pip install -r requirements.txt -r requirements-test.txt
+ - name: Verify CLI, paths, artifacts and portable runtime contracts
+ run: >-
+ python -m pytest tests/cli
+ tests/plugins/core/test_frontend_layout.py tests/plugins/core/validation/test_frontend_dependencies.py
+ tests/plugins/core/native tests/plugins/core/runtime/test_host_services.py tests/plugins/core/runtime/test_job_dispatcher.py
+ tests/plugins/core/artifacts tests/plugins/core/deployment
+ tests/plugins/core/management/test_release_dao.py tests/plugins/core/management/test_artifact_views.py
+ tests/module_plugin/controller/test_plugin_release_controller.py tests/sql/test_plugin_release_schema.py
+ tests/plugins/examples/test_task_demo.py
+ -m 'not native and not browser and not integration' -q --strict-markers --durations=15 --junitxml=target/ci/windows-contract.xml
+ - run: python ../.github/scripts/check_test_report.py target/ci/windows-contract.xml --allow-skips
+ - uses: actions/upload-artifact@v6
+ if: always()
+ with:
+ name: backend-windows-contract
+ path: ruoyi-fastapi-backend/target/ci/
+ retention-days: 7
+
+ timezone:
+ name: Host timezone contracts
+ runs-on: ubuntu-latest
+ timeout-minutes: 15
+ defaults:
+ run:
+ working-directory: ruoyi-fastapi-backend
+ steps:
+ - uses: actions/checkout@v5
+ - uses: actions/setup-python@v6
+ with:
+ python-version: '3.12'
+ cache: pip
+ cache-dependency-path: ruoyi-fastapi-backend/requirements*.txt
+ - run: python -m pip install -r requirements.txt -r requirements-test.txt
+ - name: Verify UTC, Shanghai and New York in separate processes
+ shell: bash
+ run: |
+ for zone in UTC Asia/Shanghai America/New_York; do
+ report="target/ci/timezone-${zone//\//_}.xml"
+ TZ="$zone" python -m pytest -q tests/common/test_types.py tests/common/test_query_time.py tests/utils/test_time_util.py tests/config/test_scheduler_database.py tests/utils/test_cron_util.py tests/module_admin/controller/test_job_controller.py tests/config/test_scheduler_job_persistence.py --junitxml="$report"
+ python ../.github/scripts/check_test_report.py "$report"
+ done
+ - uses: actions/upload-artifact@v6
+ if: always()
+ with:
+ name: backend-timezones
+ path: ruoyi-fastapi-backend/target/ci/
+ retention-days: 7
diff --git a/.github/workflows/ci-e2e.yml b/.github/workflows/ci-e2e.yml
new file mode 100644
index 0000000..f3ef383
--- /dev/null
+++ b/.github/workflows/ci-e2e.yml
@@ -0,0 +1,111 @@
+name: CI / E2E
+
+on:
+ workflow_call:
+ inputs:
+ matrix_json:
+ type: string
+ required: true
+
+permissions:
+ contents: read
+
+jobs:
+ browser:
+ name: ${{ matrix.framework }} / ${{ matrix.database }} / Python ${{ matrix.python }}
+ runs-on: ubuntu-latest
+ timeout-minutes: 45
+ strategy:
+ fail-fast: false
+ matrix: ${{ fromJSON(inputs.matrix_json) }}
+ env:
+ COMPOSE_PROJECT_NAME: ruoyi-ci-${{ github.run_id }}-${{ github.run_attempt }}-${{ strategy.job-index }}
+ COMPOSE_FILE: ${{ github.workspace }}/ruoyi-fastapi-test/target/e2e/compose.json
+ FRONTEND_FRAMEWORK: ${{ matrix.framework }}
+ E2E_OUTPUT_DIR: ${{ github.workspace }}/ruoyi-fastapi-test/target/e2e/browser
+ steps:
+ - uses: actions/checkout@v5
+ - uses: actions/setup-python@v6
+ with:
+ python-version: ${{ matrix.python }}
+ cache: pip
+ cache-dependency-path: ruoyi-fastapi-test/requirements.txt
+ - name: Install test runner and Chromium
+ run: |
+ python -m pip install -r ruoyi-fastapi-test/requirements.txt
+ python -m playwright install --with-deps chromium
+ - name: Verify reusable browser and isolated contexts
+ working-directory: ruoyi-fastapi-test
+ run: |
+ python -m pytest -m harness -q --junitxml=target/e2e/harness.xml
+ python ../.github/scripts/check_test_report.py target/e2e/harness.xml
+ - name: Generate isolated Compose project
+ run: |
+ python ruoyi-fastapi-test/prepare_e2e_compose.py \
+ --database '${{ matrix.database }}' --framework '${{ matrix.framework }}' \
+ --python-version '${{ matrix.python }}' --project "$COMPOSE_PROJECT_NAME" \
+ --output "$COMPOSE_FILE"
+ docker compose config --quiet
+ - uses: docker/setup-buildx-action@v3
+ - name: Build frontend with reusable layers
+ uses: docker/build-push-action@v6
+ with:
+ context: ruoyi-fastapi-frontend/${{ matrix.framework }}/web
+ file: ruoyi-fastapi-frontend/${{ matrix.framework }}/web/Dockerfile
+ load: true
+ tags: ${{ env.COMPOSE_PROJECT_NAME }}-ruoyi-frontend:ci
+ cache-from: type=gha,scope=e2e-frontend-${{ matrix.framework }}
+ cache-to: type=gha,scope=e2e-frontend-${{ matrix.framework }},mode=max
+ - name: Select database Dockerfile
+ id: database
+ env:
+ DATABASE: ${{ matrix.database }}
+ run: |
+ if [ "$DATABASE" = mysql ]; then
+ echo 'suffix=my' >> "$GITHUB_OUTPUT"
+ else
+ echo 'suffix=pg' >> "$GITHUB_OUTPUT"
+ fi
+ - name: Build backend with reusable layers
+ uses: docker/build-push-action@v6
+ with:
+ context: ruoyi-fastapi-backend
+ file: ruoyi-fastapi-backend/Dockerfile.${{ steps.database.outputs.suffix }}
+ load: true
+ tags: ${{ env.COMPOSE_PROJECT_NAME }}-ruoyi-backend-${{ steps.database.outputs.suffix }}:ci
+ build-args: |
+ PYTHON_VERSION=${{ matrix.python }}
+ PIP_INDEX_URL=https://pypi.org/simple
+ cache-from: type=gha,scope=e2e-backend-${{ matrix.database }}-py${{ matrix.python }}
+ cache-to: type=gha,scope=e2e-backend-${{ matrix.database }}-py${{ matrix.python }},mode=max
+ - name: Wait for healthy database, API and frontend proxy
+ run: |
+ docker compose up --detach --no-build --wait --wait-timeout 240
+ echo "TEST_FRONTEND_URL=http://$(docker compose port ruoyi-frontend 80)" >> "$GITHUB_ENV"
+ echo "TEST_BACKEND_URL=http://$(docker compose port ruoyi-backend-${{ steps.database.outputs.suffix }} 9099)" >> "$GITHUB_ENV"
+ - name: Run all application pages and functional E2E checks
+ working-directory: ruoyi-fastapi-test
+ run: |
+ python -m pytest -m e2e -v --durations=20 --junitxml=target/e2e/junit.xml
+ python ../.github/scripts/check_test_report.py target/e2e/junit.xml
+ - name: Collect service diagnostics
+ if: always()
+ run: |
+ mkdir -p ruoyi-fastapi-test/target/e2e
+ if [ -f "$COMPOSE_FILE" ]; then
+ docker compose ps --all > ruoyi-fastapi-test/target/e2e/services.txt 2>&1 || true
+ docker compose logs --no-color --timestamps > ruoyi-fastapi-test/target/e2e/services.log 2>&1 || true
+ fi
+ - name: Remove only this test project's containers and data
+ if: always()
+ run: |
+ if [ -f "$COMPOSE_FILE" ]; then
+ docker compose down --volumes --remove-orphans --timeout 20
+ fi
+ - uses: actions/upload-artifact@v6
+ if: always()
+ with:
+ name: e2e-${{ matrix.framework }}-${{ matrix.database }}-py${{ matrix.python }}
+ path: ruoyi-fastapi-test/target/e2e
+ retention-days: 7
+ if-no-files-found: warn
diff --git a/.github/workflows/ci-frontend.yml b/.github/workflows/ci-frontend.yml
new file mode 100644
index 0000000..68cf449
--- /dev/null
+++ b/.github/workflows/ci-frontend.yml
@@ -0,0 +1,116 @@
+name: CI / Frontend
+
+on:
+ workflow_call:
+ inputs:
+ matrix_json:
+ description: 'JSON matrix with framework and target entries under include'
+ required: true
+ type: string
+ full:
+ description: 'Reserved for callers requesting the full project matrix'
+ required: false
+ default: false
+ type: boolean
+
+permissions:
+ contents: read
+
+jobs:
+ frontend:
+ name: ${{ matrix.framework }} / ${{ matrix.target }}
+ runs-on: ubuntu-latest
+ timeout-minutes: 35
+ strategy:
+ fail-fast: false
+ matrix: ${{ fromJSON(inputs.matrix_json) }}
+ env:
+ NODE_OPTIONS: ${{ matrix.framework == 'vue2' && '--openssl-legacy-provider' || '' }}
+ defaults:
+ run:
+ working-directory: ruoyi-fastapi-frontend/${{ matrix.framework }}/${{ matrix.target }}
+ steps:
+ - uses: actions/checkout@v5
+ - uses: actions/setup-node@v6
+ with:
+ node-version: '22'
+ - name: Install the Vue 2 Mobile package manager
+ if: matrix.framework == 'vue2' && matrix.target == 'mobile'
+ run: npm install --global yarn@1.22.22
+ - uses: pnpm/action-setup@v5
+ if: matrix.framework == 'vue3' && matrix.target == 'mobile'
+ with:
+ package_json_file: ruoyi-fastapi-frontend/vue3/mobile/package.json
+ - name: Locate the package cache
+ id: package-cache
+ env:
+ FRAMEWORK: ${{ matrix.framework }}
+ TARGET: ${{ matrix.target }}
+ run: |
+ if [ "$TARGET" = web ]; then
+ echo "path=$(npm config get cache)" >> "$GITHUB_OUTPUT"
+ elif [ "$FRAMEWORK" = vue2 ]; then
+ echo "path=$(yarn cache dir --silent)" >> "$GITHUB_OUTPUT"
+ else
+ echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT"
+ fi
+ - uses: actions/cache@v5
+ with:
+ path: ${{ steps.package-cache.outputs.path }}
+ # Lockfiles are intentionally not tracked in these projects.
+ key: ${{ runner.os }}-node22-${{ matrix.framework }}-${{ matrix.target }}-${{ hashFiles(format('ruoyi-fastapi-frontend/{0}/{1}/package.json', matrix.framework, matrix.target)) }}
+ restore-keys: |
+ ${{ runner.os }}-node22-${{ matrix.framework }}-${{ matrix.target }}-
+ - name: Install Web dependencies
+ if: matrix.target == 'web'
+ run: npm install --no-audit --no-fund --package-lock=false
+ - name: Install Vue 2 Mobile dependencies
+ if: matrix.framework == 'vue2' && matrix.target == 'mobile'
+ run: yarn install --non-interactive
+ - name: Install Vue 3 Mobile dependencies
+ if: matrix.framework == 'vue3' && matrix.target == 'mobile'
+ run: pnpm install --no-frozen-lockfile
+ - name: Run unit and component behavior tests
+ run: npm run test:unit
+ - name: Verify time contracts in three host timezones
+ run: |
+ for zone in UTC Asia/Shanghai America/New_York; do
+ TZ="$zone" npm run test:contract
+ done
+ - name: Verify the Vue 2 Mobile browser bundle without native Intl
+ if: matrix.framework == 'vue2' && matrix.target == 'mobile'
+ run: npm run test:bundle
+ - name: Build Web production and Docker targets
+ if: matrix.target == 'web'
+ run: |
+ npm run build:prod
+ npm run build:docker
+ - name: Build Mobile H5
+ if: matrix.target == 'mobile'
+ run: npm run build:h5
+ - uses: actions/setup-python@v6
+ if: matrix.target == 'mobile'
+ with:
+ python-version: '3.12'
+ - name: Install the Mobile smoke browser
+ if: matrix.target == 'mobile'
+ run: |
+ python -m pip install playwright==1.62.0
+ python -m playwright install --with-deps chromium
+ - name: Verify Mobile H5 login and profile
+ if: matrix.target == 'mobile'
+ working-directory: .
+ run: python .github/scripts/mobile_smoke.py --framework ${{ matrix.framework }}
+ - name: Build Mobile WeChat target
+ if: matrix.target == 'mobile'
+ run: npm run build:mp-weixin
+ - name: Upload frontend diagnostics
+ if: always()
+ uses: actions/upload-artifact@v6
+ with:
+ name: frontend-${{ matrix.framework }}-${{ matrix.target }}
+ path: |
+ target/ci/mobile-smoke/${{ matrix.framework }}/
+ ruoyi-fastapi-frontend/${{ matrix.framework }}/${{ matrix.target }}/test-results/
+ if-no-files-found: ignore
+ retention-days: 7
diff --git a/.github/workflows/ci-integration.yml b/.github/workflows/ci-integration.yml
new file mode 100644
index 0000000..475b36c
--- /dev/null
+++ b/.github/workflows/ci-integration.yml
@@ -0,0 +1,218 @@
+name: CI / Integration
+
+on:
+ workflow_call:
+ inputs:
+ database:
+ type: boolean
+ default: false
+ release:
+ type: boolean
+ default: false
+ network_frameworks:
+ type: string
+ default: '[]'
+ full:
+ type: boolean
+ default: false
+
+permissions:
+ contents: read
+
+jobs:
+ database-timezone:
+ name: MySQL and PostgreSQL time contracts
+ if: inputs.database
+ runs-on: ubuntu-latest
+ timeout-minutes: 20
+ env:
+ TZ: America/New_York
+ PYTHONUTF8: '1'
+ defaults:
+ run:
+ working-directory: ruoyi-fastapi-backend
+ services:
+ mysql:
+ image: mysql:8.0
+ env:
+ MYSQL_ROOT_PASSWORD: timezone_test
+ TZ: Asia/Shanghai
+ ports: ['3306:3306']
+ options: >-
+ --health-cmd="mysqladmin ping -h localhost -ptimezone_test"
+ --health-interval=10s --health-timeout=5s --health-retries=12
+ postgres:
+ image: postgres:14
+ env:
+ POSTGRES_PASSWORD: timezone_test
+ TZ: America/New_York
+ PGTZ: America/New_York
+ ports: ['5432:5432']
+ options: >-
+ --health-cmd="pg_isready -U postgres"
+ --health-interval=10s --health-timeout=5s --health-retries=12
+ steps:
+ - uses: actions/checkout@v5
+ - uses: actions/setup-python@v6
+ with:
+ python-version: '3.12'
+ cache: pip
+ cache-dependency-path: ruoyi-fastapi-backend/requirements*.txt
+ - run: python -m pip install -r requirements.txt -r requirements-pg.txt -r requirements-test.txt
+ - name: Configure isolated database verification
+ run: |
+ mkdir -p target/ci/timezone
+ python - <<'PY'
+ import json
+ from pathlib import Path
+ for dialect, port, username, database in [('mysql', 3306, 'root', 'mysql'), ('postgresql', 5432, 'postgres', 'postgres')]:
+ source = dict(db_type=dialect, db_host='127.0.0.1', db_port=port, db_username=username,
+ db_password='timezone_test', db_database=database)
+ Path(f'target/ci/timezone/{dialect}.json').write_text(json.dumps(source))
+ PY
+ - name: Verify drivers, CRUD and scheduler persistence
+ run: |
+ python -m scripts.verify_timezone_database --config target/ci/timezone/mysql.json --report target/ci/timezone/mysql-report.json
+ python -m scripts.verify_timezone_database --config target/ci/timezone/postgresql.json --report target/ci/timezone/postgresql-report.json
+ - uses: actions/upload-artifact@v6
+ if: always()
+ with:
+ name: integration-database-timezones
+ path: ruoyi-fastapi-backend/target/ci/timezone/*-report.json
+ retention-days: 7
+
+ task-bundle:
+ name: Task plugin bundle
+ if: inputs.release
+ runs-on: ubuntu-latest
+ timeout-minutes: 10
+ defaults:
+ run:
+ working-directory: ruoyi-fastapi-backend/plugins/examples/python/task_demo/web
+ steps:
+ - uses: actions/checkout@v5
+ - uses: actions/setup-node@v6
+ with:
+ node-version: '22'
+ cache: npm
+ cache-dependency-path: ruoyi-fastapi-backend/plugins/examples/python/task_demo/web/package.json
+ - run: npm install --no-audit --no-fund --package-lock=false
+ - run: npm test
+ - run: npm run build
+ - uses: actions/upload-artifact@v6
+ with:
+ name: integration-task-bundle
+ path: ruoyi-fastapi-backend/plugins/examples/python/task_demo/web/dist/
+ if-no-files-found: error
+ retention-days: 1
+
+ release:
+ name: Plugin maintenance / Python ${{ matrix.python }}
+ needs: task-bundle
+ if: inputs.release
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+ strategy:
+ fail-fast: false
+ matrix:
+ python: ${{ fromJSON(inputs.full && '["3.10","3.11","3.12","3.13"]' || '["3.12"]') }}
+ defaults:
+ run:
+ working-directory: ruoyi-fastapi-backend
+ services:
+ mysql:
+ image: mysql:8.0
+ env:
+ MYSQL_ROOT_PASSWORD: ci-only-mysql
+ ports: ['3306:3306']
+ options: >-
+ --health-cmd="mysqladmin ping -h localhost -pci-only-mysql"
+ --health-interval=10s --health-timeout=5s --health-retries=12
+ postgres:
+ image: postgres:14
+ env:
+ POSTGRES_PASSWORD: ci-only-postgres
+ ports: ['5432:5432']
+ options: >-
+ --health-cmd="pg_isready -U postgres"
+ --health-interval=10s --health-timeout=5s --health-retries=12
+ redis:
+ image: redis:6.2
+ ports: ['6379:6379']
+ options: >-
+ --health-cmd="redis-cli ping"
+ --health-interval=10s --health-timeout=5s --health-retries=12
+ env:
+ RUOYI_PLUGIN_IT_ALLOW: '1'
+ RUOYI_IT_MYSQL_PASSWORD: ci-only-mysql
+ RUOYI_IT_PG_PASSWORD: ci-only-postgres
+ RUOYI_IT_REDIS_PASSWORD: ''
+ PYTHONUTF8: '1'
+ _TYPER_FORCE_DISABLE_TERMINAL: '1'
+ steps:
+ - uses: actions/checkout@v5
+ - uses: actions/download-artifact@v6
+ with:
+ name: integration-task-bundle
+ path: ruoyi-fastapi-backend/plugins/examples/python/task_demo/web/dist
+ - uses: actions/setup-python@v6
+ with:
+ python-version: ${{ matrix.python }}
+ cache: pip
+ cache-dependency-path: ruoyi-fastapi-backend/requirements*.txt
+ - run: python -m pip install -r requirements.txt -r requirements-pg.txt
+ - name: Verify MySQL and PostgreSQL maintenance releases
+ shell: bash
+ run: |
+ mkdir -p target/ci/release
+ python scripts/plugin_release_integration.py --engine mysql 2>&1 | tee target/ci/release/mysql.log
+ python scripts/plugin_release_integration.py --engine postgresql 2>&1 | tee target/ci/release/postgresql.log
+ - uses: actions/upload-artifact@v6
+ if: always()
+ with:
+ name: integration-release-${{ matrix.python }}
+ path: ruoyi-fastapi-backend/target/ci/release/
+ retention-days: 7
+
+ network:
+ name: ${{ matrix.framework }} proxy and plugin sessions
+ if: inputs.network_frameworks != '[]'
+ runs-on: ubuntu-latest
+ timeout-minutes: 20
+ strategy:
+ fail-fast: false
+ matrix:
+ framework: ${{ fromJSON(inputs.network_frameworks) }}
+ defaults:
+ run:
+ working-directory: ruoyi-fastapi-backend
+ env:
+ RUOYI_PLUGIN_NETWORK_SMOKE: '1'
+ RUOYI_PLUGIN_FRONTEND_FRAMEWORK: ${{ matrix.framework }}
+ NODE_OPTIONS: ${{ matrix.framework == 'vue2' && '--openssl-legacy-provider' || '' }}
+ DB_SOURCES: '{"primary":{"db_type":"mysql","db_host":"127.0.0.1","db_port":3306,"db_username":"network-smoke","db_password":"network-smoke","db_database":"network-smoke"}}'
+ JWT_SECRET_KEY: isolated-plugin-network-smoke-ci-only
+ steps:
+ - uses: actions/checkout@v5
+ - uses: actions/setup-python@v6
+ with:
+ python-version: '3.12'
+ cache: pip
+ cache-dependency-path: ruoyi-fastapi-backend/requirements*.txt
+ - uses: actions/setup-node@v6
+ with:
+ node-version: '22'
+ cache: npm
+ cache-dependency-path: ruoyi-fastapi-frontend/${{ matrix.framework }}/web/package.json
+ - run: python -m pip install -r requirements.txt -r requirements-test.txt playwright==1.62.0
+ - run: python -m playwright install --with-deps chromium
+ - run: npm --prefix ../ruoyi-fastapi-frontend/${{ matrix.framework }}/web install --no-audit --no-fund --package-lock=false
+ - name: Verify HTTPS proxy, iframe sessions and long connections
+ run: python -m pytest tests/plugins/integration/test_network_smoke.py -m browser --strict-markers -v --junitxml=target/plugin-network-smoke/${{ matrix.framework }}/results.xml
+ - run: python ../.github/scripts/check_test_report.py target/plugin-network-smoke/${{ matrix.framework }}/results.xml
+ - uses: actions/upload-artifact@v6
+ if: always()
+ with:
+ name: integration-network-${{ matrix.framework }}
+ path: ruoyi-fastapi-backend/target/plugin-network-smoke/${{ matrix.framework }}/
+ retention-days: 7
diff --git a/.github/workflows/ci-native.yml b/.github/workflows/ci-native.yml
new file mode 100644
index 0000000..373a13d
--- /dev/null
+++ b/.github/workflows/ci-native.yml
@@ -0,0 +1,80 @@
+name: CI / Native
+
+on:
+ workflow_call:
+
+permissions:
+ contents: read
+
+env:
+ PYTHONUTF8: '1'
+ _TYPER_FORCE_DISABLE_TERMINAL: '1'
+
+jobs:
+ rust-quality:
+ name: Rust format and clippy
+ runs-on: ubuntu-latest
+ timeout-minutes: 20
+ defaults:
+ run:
+ working-directory: ruoyi-fastapi-backend
+ steps:
+ - uses: actions/checkout@v5
+ - uses: actions/setup-python@v6
+ with:
+ python-version: '3.12'
+ - run: rustup toolchain install 1.99.0 --profile minimal --component rustfmt --component clippy
+ - run: rustup default 1.99.0
+ - uses: actions/cache@v5
+ with:
+ path: |
+ ~/.cargo/registry
+ ~/.cargo/git
+ ruoyi-fastapi-backend/plugins/examples/rust/rust_demo/target
+ key: rust-quality-${{ runner.os }}-1.99.0-${{ hashFiles('**/Cargo.lock') }}
+ - run: cargo fmt --manifest-path plugins/examples/rust/rust_demo/Cargo.toml --check
+ - run: cargo clippy --manifest-path plugins/examples/rust/rust_demo/Cargo.toml --locked -- -D warnings
+
+ runtime:
+ name: ${{ matrix.os }} / Python ${{ matrix.python }}
+ runs-on: ${{ matrix.os }}
+ timeout-minutes: 30
+ strategy:
+ fail-fast: false
+ matrix:
+ os: [ubuntu-latest, windows-latest]
+ python: ['3.10', '3.11', '3.12', '3.13']
+ defaults:
+ run:
+ working-directory: ruoyi-fastapi-backend
+ steps:
+ - uses: actions/checkout@v5
+ - uses: actions/setup-python@v6
+ with:
+ python-version: ${{ matrix.python }}
+ cache: pip
+ cache-dependency-path: ruoyi-fastapi-backend/requirements*.txt
+ - run: python -m pip install -r requirements.txt -r requirements-test.txt 'maturin>=1.15,<2'
+ - run: rustup toolchain install 1.99.0 --profile minimal
+ - run: rustup default 1.99.0
+ - uses: actions/cache@v5
+ with:
+ path: |
+ ~/.cargo/registry
+ ~/.cargo/git
+ ruoyi-fastapi-backend/plugins/examples/rust/rust_demo/target
+ key: rust-native-${{ runner.os }}-${{ matrix.python }}-1.99.0-${{ hashFiles('**/Cargo.lock') }}
+ - name: Build and assemble source-free plugin
+ run: python scripts/build_native_plugin.py --output target/native-package
+ - name: Exercise real native runtime and signed artifact loading
+ env:
+ RUOYI_NATIVE_PLUGIN_DIR: ${{ github.workspace }}/ruoyi-fastapi-backend/target/native-package/rust_demo
+ run: python -m pytest tests/plugins/native tests/plugins/core/deployment -m native --strict-markers -q --durations=15 --junitxml=target/ci/native.xml
+ - name: Require native tests to run without missing-artifact skips
+ run: python ../.github/scripts/check_test_report.py target/ci/native.xml
+ - uses: actions/upload-artifact@v6
+ if: always()
+ with:
+ name: native-${{ runner.os }}-${{ matrix.python }}
+ path: ruoyi-fastapi-backend/target/ci/
+ retention-days: 7
diff --git a/.github/workflows/ci-quality.yml b/.github/workflows/ci-quality.yml
new file mode 100644
index 0000000..a1c0bbf
--- /dev/null
+++ b/.github/workflows/ci-quality.yml
@@ -0,0 +1,54 @@
+name: CI / Quality
+
+on:
+ workflow_call:
+
+permissions:
+ contents: read
+
+jobs:
+ checks:
+ name: Code and CI contracts
+ runs-on: ubuntu-latest
+ timeout-minutes: 15
+ steps:
+ - uses: actions/checkout@v5
+ - uses: actions/setup-python@v6
+ with:
+ python-version: '3.12'
+ cache: pip
+ cache-dependency-path: .github/requirements.txt
+ - run: python -m pip install -r .github/requirements.txt
+ - name: Python lint and format
+ run: |
+ ruff check ruoyi-fastapi-backend ruoyi-fastapi-test .github/scripts .github/tests
+ ruff format --check ruoyi-fastapi-backend ruoyi-fastapi-test .github/scripts .github/tests
+ - name: Test CI routing, failure gates and deployment contracts
+ run: python -m pytest .github/tests -q --junitxml=target/ci/quality.xml
+ - name: Validate every Compose framework selection
+ run: python .github/scripts/check_compose.py
+ - name: Install actionlint
+ env:
+ ACTIONLINT_VERSION: '1.7.12'
+ run: |
+ archive="actionlint_${ACTIONLINT_VERSION}_linux_amd64.tar.gz"
+ curl --fail --location --retry 3 "https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/${archive}" --output "${RUNNER_TEMP}/${archive}"
+ echo "8aca8db96f1b94770f1b0d72b6dddcb1ebb8123cb3712530b08cc387b349a3d8 ${RUNNER_TEMP}/${archive}" | sha256sum --check
+ tar -xzf "${RUNNER_TEMP}/${archive}" -C "$RUNNER_TEMP" actionlint
+ "$RUNNER_TEMP/actionlint" -color
+ - uses: actions/setup-node@v6
+ with:
+ node-version: '22'
+ cache: npm
+ cache-dependency-path: ruoyi-fastapi-frontend/*/web/package.json
+ - name: Check Web formatting with the existing project configurations
+ run: |
+ for framework in vue2 vue3; do
+ (cd "ruoyi-fastapi-frontend/$framework/web" && npx --yes prettier@3.6.2 --check .)
+ done
+ - uses: actions/upload-artifact@v6
+ if: always()
+ with:
+ name: quality-reports
+ path: target/ci/
+ retention-days: 7
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..fe59a04
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,106 @@
+name: CI
+
+on:
+ pull_request:
+ branches: [master]
+ push:
+ branches: [master]
+ schedule:
+ - cron: '21 18 * * *'
+ workflow_dispatch:
+ inputs:
+ full:
+ description: Run the full compatibility and end-to-end matrix
+ type: boolean
+ default: true
+
+permissions:
+ contents: read
+
+concurrency:
+ group: ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
+
+jobs:
+ plan:
+ name: Plan
+ runs-on: ubuntu-latest
+ timeout-minutes: 5
+ outputs:
+ backend: ${{ steps.plan.outputs.backend }}
+ frontend: ${{ steps.plan.outputs.frontend }}
+ integration: ${{ steps.plan.outputs.integration }}
+ e2e: ${{ steps.plan.outputs.e2e }}
+ native: ${{ steps.plan.outputs.native }}
+ full: ${{ steps.plan.outputs.full }}
+ frontend_matrix: ${{ steps.plan.outputs.frontend_matrix }}
+ e2e_matrix: ${{ steps.plan.outputs.e2e_matrix }}
+ network_frameworks: ${{ steps.plan.outputs.network_frameworks }}
+ database: ${{ steps.plan.outputs.database }}
+ release: ${{ steps.plan.outputs.release }}
+ plan_json: ${{ steps.plan.outputs.plan_json }}
+ steps:
+ - uses: actions/checkout@v5
+ with:
+ fetch-depth: 0
+ - id: plan
+ run: python .github/scripts/ci_plan.py plan
+
+ quality:
+ name: Quality
+ needs: plan
+ uses: ./.github/workflows/ci-quality.yml
+
+ backend:
+ name: Backend
+ needs: plan
+ if: needs.plan.outputs.backend == 'true'
+ uses: ./.github/workflows/ci-backend.yml
+
+ frontend:
+ name: Frontend
+ needs: plan
+ if: needs.plan.outputs.frontend == 'true'
+ uses: ./.github/workflows/ci-frontend.yml
+ with:
+ matrix_json: ${{ needs.plan.outputs.frontend_matrix }}
+ full: ${{ needs.plan.outputs.full == 'true' }}
+
+ integration:
+ name: Integration
+ needs: plan
+ if: needs.plan.outputs.integration == 'true'
+ uses: ./.github/workflows/ci-integration.yml
+ with:
+ database: ${{ needs.plan.outputs.database == 'true' }}
+ release: ${{ needs.plan.outputs.release == 'true' }}
+ network_frameworks: ${{ needs.plan.outputs.network_frameworks }}
+ full: ${{ needs.plan.outputs.full == 'true' }}
+
+ e2e:
+ name: E2E
+ needs: plan
+ if: needs.plan.outputs.e2e == 'true'
+ uses: ./.github/workflows/ci-e2e.yml
+ with:
+ matrix_json: ${{ needs.plan.outputs.e2e_matrix }}
+
+ native:
+ name: Native
+ needs: plan
+ if: needs.plan.outputs.native == 'true'
+ uses: ./.github/workflows/ci-native.yml
+
+ result:
+ name: Result
+ if: always()
+ needs: [plan, quality, backend, frontend, integration, e2e, native]
+ runs-on: ubuntu-latest
+ timeout-minutes: 5
+ steps:
+ - uses: actions/checkout@v5
+ - name: Require every selected category to succeed
+ env:
+ PLAN_JSON: ${{ needs.plan.outputs.plan_json }}
+ NEEDS_JSON: ${{ toJSON(needs) }}
+ run: python .github/scripts/ci_plan.py gate
diff --git a/.github/workflows/native-plugin.yml b/.github/workflows/native-plugin.yml
deleted file mode 100644
index 49555c5..0000000
--- a/.github/workflows/native-plugin.yml
+++ /dev/null
@@ -1,97 +0,0 @@
-name: Native Plugin
-
-on:
- push:
- branches: [master]
- paths:
- - 'ruoyi-fastapi-backend/plugins/core/**'
- - 'ruoyi-fastapi-backend/plugins/examples/rust/**'
- - 'ruoyi-fastapi-backend/plugins/examples/python/task_demo/**'
- - 'ruoyi-fastapi-backend/scripts/plugin_task_integration.py'
- - 'ruoyi-fastapi-backend/scripts/build_native_plugin.py'
- - 'ruoyi-fastapi-backend/cli/runtime/plugin/scaffold/**'
- - 'ruoyi-fastapi-backend/cli/runtime/plugin/sdk.py'
- - 'ruoyi-fastapi-backend/cli/groups/plugin/commands/developer.py'
- - 'ruoyi-fastapi-backend/cli/groups/plugin/commands/sdk.py'
- - 'ruoyi-fastapi-backend/cli/groups/plugin/commands/artifact*.py'
- - 'ruoyi-fastapi-backend/cli/groups/plugin/artifact_controller.py'
- - 'ruoyi-fastapi-backend/cli/groups/plugin/command.py'
- - 'ruoyi-fastapi-frontend/src/utils/pluginBridge*'
- - 'ruoyi-fastapi-backend/pyproject.toml'
- - 'ruoyi-fastapi-backend/tests/cli/runtime/plugin/test_runtime_scaffold*.py'
- - 'ruoyi-fastapi-backend/tests/cli/runtime/plugin/test_runtime_sdk.py'
- - 'ruoyi-fastapi-backend/tests/cli/root/test_plugin_sdk_commands.py'
- - 'ruoyi-fastapi-backend/tests/cli/root/test_plugin_artifact_commands.py'
- - 'ruoyi-fastapi-backend/tests/cli/root/test_plugin_release_wait.py'
- - 'ruoyi-fastapi-backend/tests/plugins/**'
- - '.github/workflows/native-plugin.yml'
- pull_request:
- branches: [master]
- paths:
- - 'ruoyi-fastapi-backend/plugins/core/**'
- - 'ruoyi-fastapi-backend/plugins/examples/rust/**'
- - 'ruoyi-fastapi-backend/plugins/examples/python/task_demo/**'
- - 'ruoyi-fastapi-backend/scripts/plugin_task_integration.py'
- - 'ruoyi-fastapi-backend/scripts/build_native_plugin.py'
- - 'ruoyi-fastapi-backend/cli/runtime/plugin/scaffold/**'
- - 'ruoyi-fastapi-backend/cli/runtime/plugin/sdk.py'
- - 'ruoyi-fastapi-backend/cli/groups/plugin/commands/developer.py'
- - 'ruoyi-fastapi-backend/cli/groups/plugin/commands/sdk.py'
- - 'ruoyi-fastapi-backend/cli/groups/plugin/commands/artifact*.py'
- - 'ruoyi-fastapi-backend/cli/groups/plugin/artifact_controller.py'
- - 'ruoyi-fastapi-backend/cli/groups/plugin/command.py'
- - 'ruoyi-fastapi-frontend/src/utils/pluginBridge*'
- - 'ruoyi-fastapi-backend/pyproject.toml'
- - 'ruoyi-fastapi-backend/tests/cli/runtime/plugin/test_runtime_scaffold*.py'
- - 'ruoyi-fastapi-backend/tests/cli/runtime/plugin/test_runtime_sdk.py'
- - 'ruoyi-fastapi-backend/tests/cli/root/test_plugin_sdk_commands.py'
- - 'ruoyi-fastapi-backend/tests/cli/root/test_plugin_artifact_commands.py'
- - 'ruoyi-fastapi-backend/tests/cli/root/test_plugin_release_wait.py'
- - 'ruoyi-fastapi-backend/tests/plugins/**'
- - '.github/workflows/native-plugin.yml'
- workflow_dispatch:
-
-jobs:
- native:
- strategy:
- fail-fast: false
- matrix:
- os: [ubuntu-latest, windows-latest]
- python: ['3.10', '3.11', '3.12', '3.13']
- runs-on: ${{ matrix.os }}
- defaults:
- run:
- working-directory: ruoyi-fastapi-backend
- steps:
- - uses: actions/checkout@v5
- - uses: actions/setup-python@v6
- with:
- python-version: ${{ matrix.python }}
- cache: pip
- cache-dependency-path: ruoyi-fastapi-backend/requirements.txt
- - name: Install host and build dependencies
- run: python -m pip install -r requirements.txt 'maturin>=1.15,<2' pytest pytest-asyncio aiosqlite
- - name: Install Rust
- run: rustup toolchain install 1.99.0 --profile minimal --component rustfmt --component clippy
- - name: Select Rust
- run: rustup default 1.99.0
- - name: Check Rust formatting
- run: cargo fmt --manifest-path plugins/examples/rust/rust_demo/Cargo.toml --check
- - name: Verify v1 and v2 plugin scaffolds
- run: python -m pytest tests/cli/runtime/plugin/test_runtime_scaffold.py tests/cli/runtime/plugin/test_runtime_scaffold_v2.py -q
- - name: Verify SDK provenance and offline update commands
- run: python -m pytest tests/cli/runtime/plugin/test_runtime_sdk.py tests/cli/root/test_plugin_sdk_commands.py -q
- - name: Verify persistent task plugin
- run: python -m pytest tests/plugins/examples/test_task_demo.py -q
- - name: Build and assemble source-free plugin
- run: python scripts/build_native_plugin.py --output target/native-package
- - name: Check Rust code
- run: cargo clippy --manifest-path plugins/examples/rust/rust_demo/Cargo.toml --locked -- -D warnings
- - name: Exercise native library through the host runtime
- env:
- RUOYI_NATIVE_PLUGIN_DIR: ${{ github.workspace }}/ruoyi-fastapi-backend/target/native-package/rust_demo
- run: python -m pytest tests/plugins/native tests/plugins/core/native tests/plugins/core/runtime/test_host_services.py tests/plugins/core/runtime/test_job_dispatcher.py -q
- - name: Verify signed artifacts and maintenance releases
- env:
- RUOYI_NATIVE_PLUGIN_DIR: ${{ github.workspace }}/ruoyi-fastapi-backend/target/native-package/rust_demo
- run: python -m pytest tests/plugins/core/artifacts tests/plugins/core/deployment tests/plugins/core/management/test_release_dao.py tests/plugins/core/management/test_artifact_views.py tests/module_plugin/controller/test_plugin_release_controller.py tests/cli/root/test_plugin_artifact_commands.py tests/cli/root/test_plugin_release_wait.py tests/sql/test_plugin_release_schema.py -q
diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml
deleted file mode 100644
index 8c802ba..0000000
--- a/.github/workflows/playwright.yml
+++ /dev/null
@@ -1,174 +0,0 @@
-name: Playwright Tests
-
-on:
- push:
- branches: [master]
- pull_request:
- branches: [master]
-
-jobs:
- mysql-test:
- runs-on: ubuntu-latest
- strategy:
- fail-fast: false
- matrix:
- python-version: ["3.10", "3.11", "3.12", "3.13"]
-
- steps:
- - uses: actions/checkout@v5
-
- - name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v5
- with:
- python-version: ${{ matrix.python-version }}
-
- - name: Cache Docker layers
- uses: actions/cache@v5
- with:
- path: /tmp/.docker_cache
- key: ${{ runner.os }}-docker-${{ matrix.python-version }}-${{ hashFiles('**/Dockerfile*', '**/requirements.txt') }}
-
- - name: Set up Docker Buildx
- uses: docker/setup-buildx-action@v3
-
- - name: Create temporary Dockerfile with Python ${{ matrix.python-version }}
- run: |
- # Save original Dockerfile.my
- cp ruoyi-fastapi-backend/Dockerfile.my ruoyi-fastapi-backend/Dockerfile.my.bak
- # Create temporary Dockerfile with target Python version and default PyPI index
- sed -e "s/FROM python:3.10/FROM python:${{ matrix.python-version }}/g" \
- -e 's| -i https://pypi.tuna.tsinghua.edu.cn/simple||g' \
- ruoyi-fastapi-backend/Dockerfile.my.bak > ruoyi-fastapi-backend/Dockerfile.my
-
- - name: Start services with docker-compose
- run: |
- cd ruoyi-fastapi-test
- docker compose -f docker-compose.test.my.yml up -d --build
-
- - name: Wait for services to be ready
- run: |
- cd ruoyi-fastapi-test
- # Wait for backend to be running
- timeout 180 bash -c 'until docker compose -f docker-compose.test.my.yml ps ruoyi-backend-my | grep -q "Up"; do sleep 5; done'
- # Wait for frontend to be running
- timeout 120 bash -c 'until docker compose -f docker-compose.test.my.yml ps ruoyi-frontend | grep -q "Up"; do sleep 5; done'
- # Additional wait for services to be fully ready and listening on ports
- sleep 30
- # Check that backend is actually responding on the API endpoint
- echo "Checking if backend service is ready..."
- for i in {1..30}; do
- if curl -f http://localhost:9099/captchaImage > /dev/null 2>&1; then
- echo "Backend service is ready!"
- break
- fi
- echo "Waiting for backend service to be ready... ($i/30)"
- sleep 5
- done
- # Final check
- if ! curl -f http://localhost:9099/captchaImage > /dev/null 2>&1; then
- echo "Backend service failed to start properly. Checking logs..."
- docker logs ruoyi-backend-my-test
- exit 1
- fi
-
- - name: Run tests
- run: |
- cd ruoyi-fastapi-test
- pip install -r requirements.txt
- playwright install
- pytest -v
-
- - name: Stop services
- if: always()
- run: |
- cd ruoyi-fastapi-test
- docker compose -f docker-compose.test.my.yml down
-
- - name: Restore original Dockerfile.my
- if: always()
- run: |
- # Restore original Dockerfile.my after test
- mv ruoyi-fastapi-backend/Dockerfile.my.bak ruoyi-fastapi-backend/Dockerfile.my
-
- pg-test:
- runs-on: ubuntu-latest
- strategy:
- fail-fast: false
- matrix:
- python-version: ["3.10", "3.11", "3.12", "3.13"]
-
- steps:
- - uses: actions/checkout@v5
-
- - name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v5
- with:
- python-version: ${{ matrix.python-version }}
-
- - name: Cache Docker layers
- uses: actions/cache@v5
- with:
- path: /tmp/.docker_cache
- key: ${{ runner.os }}-docker-${{ matrix.python-version }}-${{ hashFiles('**/Dockerfile*', '**/requirements.txt') }}
-
- - name: Set up Docker Buildx
- uses: docker/setup-buildx-action@v3
-
- - name: Create temporary Dockerfile with Python ${{ matrix.python-version }}
- run: |
- # Save original Dockerfile.pg
- cp ruoyi-fastapi-backend/Dockerfile.pg ruoyi-fastapi-backend/Dockerfile.pg.bak
- # Create temporary Dockerfile with target Python version and default PyPI index
- sed -e "s/FROM python:3.10/FROM python:${{ matrix.python-version }}/g" \
- -e 's| -i https://pypi.tuna.tsinghua.edu.cn/simple||g' \
- ruoyi-fastapi-backend/Dockerfile.pg.bak > ruoyi-fastapi-backend/Dockerfile.pg
-
- - name: Start services with docker-compose
- run: |
- cd ruoyi-fastapi-test
- docker compose -f docker-compose.test.pg.yml up -d --build
-
- - name: Wait for services to be ready
- run: |
- cd ruoyi-fastapi-test
- # Wait for backend to be running
- timeout 180 bash -c 'until docker compose -f docker-compose.test.pg.yml ps ruoyi-backend-pg | grep -q "Up"; do sleep 5; done'
- # Wait for frontend to be running
- timeout 120 bash -c 'until docker compose -f docker-compose.test.pg.yml ps ruoyi-frontend | grep -q "Up"; do sleep 5; done'
- # Additional wait for services to be fully ready and listening on ports
- sleep 30
- # Check that backend is actually responding on the API endpoint
- echo "Checking if backend service is ready..."
- for i in {1..30}; do
- if curl -f http://localhost:9099/captchaImage > /dev/null 2>&1; then
- echo "Backend service is ready!"
- break
- fi
- echo "Waiting for backend service to be ready... ($i/30)"
- sleep 5
- done
- # Final check
- if ! curl -f http://localhost:9099/captchaImage > /dev/null 2>&1; then
- echo "Backend service failed to start properly. Checking logs..."
- docker logs ruoyi-backend-pg-test
- exit 1
- fi
-
- - name: Run tests
- run: |
- cd ruoyi-fastapi-test
- pip install -r requirements.txt
- playwright install
- pytest -v
-
- - name: Stop services
- if: always()
- run: |
- cd ruoyi-fastapi-test
- docker compose -f docker-compose.test.pg.yml down
-
- - name: Restore original Dockerfile.pg
- if: always()
- run: |
- # Restore original Dockerfile.pg after test
- mv ruoyi-fastapi-backend/Dockerfile.pg.bak ruoyi-fastapi-backend/Dockerfile.pg
diff --git a/.github/workflows/plugin-frontend.yml b/.github/workflows/plugin-frontend.yml
deleted file mode 100644
index ad3769c..0000000
--- a/.github/workflows/plugin-frontend.yml
+++ /dev/null
@@ -1,69 +0,0 @@
-name: Plugin Frontend Tests
-
-on:
- push:
- branches: [master]
- paths:
- - 'ruoyi-fastapi-frontend/src/api/system/plugin.js'
- - 'ruoyi-fastapi-frontend/src/components/PluginFrame/**'
- - 'ruoyi-fastapi-frontend/src/views/system/plugin/**'
- - 'ruoyi-fastapi-frontend/src/utils/**'
- - 'ruoyi-fastapi-frontend/src/plugins/**'
- - 'ruoyi-fastapi-frontend/src/router/**'
- - 'ruoyi-fastapi-frontend/src/store/modules/permission.js'
- - 'ruoyi-fastapi-frontend/src/store/modules/user.js'
- - 'ruoyi-fastapi-frontend/src/permission.js'
- - 'ruoyi-fastapi-frontend/plugins/**'
- - 'ruoyi-fastapi-frontend/tests/plugins/**'
- - 'ruoyi-fastapi-frontend/tests/run-tests.js'
- - 'ruoyi-fastapi-frontend/package.json'
- - 'ruoyi-fastapi-frontend/package-lock.json'
- - 'ruoyi-fastapi-frontend/vite.config.js'
- - 'ruoyi-fastapi-frontend/vite/**'
- - 'ruoyi-fastapi-backend/plugins/examples/python/bundle_demo/web/**'
- - '.github/workflows/plugin-frontend.yml'
- pull_request:
- branches: [master]
- paths:
- - 'ruoyi-fastapi-frontend/src/api/system/plugin.js'
- - 'ruoyi-fastapi-frontend/src/components/PluginFrame/**'
- - 'ruoyi-fastapi-frontend/src/views/system/plugin/**'
- - 'ruoyi-fastapi-frontend/src/utils/**'
- - 'ruoyi-fastapi-frontend/src/plugins/**'
- - 'ruoyi-fastapi-frontend/src/router/**'
- - 'ruoyi-fastapi-frontend/src/store/modules/permission.js'
- - 'ruoyi-fastapi-frontend/src/store/modules/user.js'
- - 'ruoyi-fastapi-frontend/src/permission.js'
- - 'ruoyi-fastapi-frontend/plugins/**'
- - 'ruoyi-fastapi-frontend/tests/plugins/**'
- - 'ruoyi-fastapi-frontend/tests/run-tests.js'
- - 'ruoyi-fastapi-frontend/package.json'
- - 'ruoyi-fastapi-frontend/package-lock.json'
- - 'ruoyi-fastapi-frontend/vite.config.js'
- - 'ruoyi-fastapi-frontend/vite/**'
- - 'ruoyi-fastapi-backend/plugins/examples/python/bundle_demo/web/**'
- - '.github/workflows/plugin-frontend.yml'
- workflow_dispatch:
-
-permissions:
- contents: read
-
-jobs:
- plugins:
- runs-on: ubuntu-latest
- timeout-minutes: 15
- defaults:
- run:
- working-directory: ruoyi-fastapi-frontend
- steps:
- - uses: actions/checkout@v5
- - uses: actions/setup-node@v6
- with:
- node-version: '22'
- cache: npm
- cache-dependency-path: ruoyi-fastapi-frontend/package.json
- - name: Install frontend dependencies
- # This repository does not commit npm lockfiles, so npm ci is not available.
- run: npm install --no-audit --no-fund --package-lock=false
- - name: Verify plugin bridge, sessions and management views
- run: npm run test:plugin
diff --git a/.github/workflows/plugin-network-smoke.yml b/.github/workflows/plugin-network-smoke.yml
deleted file mode 100644
index 6d26c4b..0000000
--- a/.github/workflows/plugin-network-smoke.yml
+++ /dev/null
@@ -1,63 +0,0 @@
-name: Plugin Network Smoke
-
-on:
- pull_request:
- branches: [master]
- paths:
- - 'ruoyi-fastapi-backend/plugins/**'
- - 'ruoyi-fastapi-backend/module_plugin/**'
- - 'ruoyi-fastapi-backend/module_admin/service/login_service.py'
- - 'ruoyi-fastapi-backend/common/**'
- - 'ruoyi-fastapi-backend/config/**'
- - 'ruoyi-fastapi-backend/middlewares/**'
- - 'ruoyi-fastapi-backend/utils/**'
- - 'ruoyi-fastapi-backend/tests/**'
- - 'ruoyi-fastapi-backend/requirements.txt'
- - 'ruoyi-fastapi-backend/pyproject.toml'
- - 'ruoyi-fastapi-frontend/**'
- - '.github/workflows/plugin-network-smoke.yml'
- workflow_dispatch:
-
-permissions:
- contents: read
-
-jobs:
- browser:
- runs-on: ubuntu-latest
- timeout-minutes: 15
- defaults:
- run:
- working-directory: ruoyi-fastapi-backend
- env:
- RUOYI_PLUGIN_NETWORK_SMOKE: '1'
- # The fixture replaces account, DB and Redis access; no external service is used.
- DB_SOURCES: '{"primary":{"db_type":"mysql","db_host":"127.0.0.1","db_port":3306,"db_username":"network-smoke","db_password":"network-smoke","db_database":"network-smoke"}}'
- JWT_SECRET_KEY: 'isolated-plugin-network-smoke-ci-only'
- steps:
- - uses: actions/checkout@v5
- - uses: actions/setup-python@v6
- with:
- python-version: '3.12'
- cache: pip
- cache-dependency-path: ruoyi-fastapi-backend/requirements.txt
- - uses: actions/setup-node@v6
- with:
- node-version: '22'
- cache: npm
- cache-dependency-path: ruoyi-fastapi-frontend/package.json
- - name: Install host and browser test dependencies
- run: python -m pip install -r requirements.txt pytest pytest-asyncio aiosqlite playwright
- - name: Install Chromium
- run: python -m playwright install --with-deps chromium
- - name: Install frontend dependencies
- # This repository does not commit npm lockfiles, so npm ci is not available.
- run: npm --prefix ../ruoyi-fastapi-frontend install --no-audit --no-fund --package-lock=false
- - name: Verify HTTPS proxy, iframe sessions and long connections
- run: python -m pytest tests/plugins/integration/test_network_smoke.py -v --junitxml=target/plugin-network-smoke/results.xml
- - name: Upload browser trace, screenshots and proxy log
- if: always()
- uses: actions/upload-artifact@v4
- with:
- name: plugin-network-smoke
- path: ruoyi-fastapi-backend/target/plugin-network-smoke/
- retention-days: 7
diff --git a/.github/workflows/plugin-release-integration.yml b/.github/workflows/plugin-release-integration.yml
deleted file mode 100644
index 793ad00..0000000
--- a/.github/workflows/plugin-release-integration.yml
+++ /dev/null
@@ -1,94 +0,0 @@
-name: Plugin Release Integration
-
-on:
- pull_request:
- branches: [master]
- paths:
- - 'ruoyi-fastapi-backend/plugins/core/**'
- - 'ruoyi-fastapi-backend/plugins/examples/python/task_demo/**'
- - 'ruoyi-fastapi-frontend/src/utils/pluginBridge.js'
- - 'ruoyi-fastapi-backend/config/**'
- - 'ruoyi-fastapi-backend/cli/groups/plugin/**'
- - 'ruoyi-fastapi-backend/cli/runtime/plugin/**'
- - 'ruoyi-fastapi-backend/module_plugin/**'
- - 'ruoyi-fastapi-backend/server.py'
- - 'ruoyi-fastapi-backend/app.py'
- - 'ruoyi-fastapi-backend/sql/ruoyi-fastapi*.sql'
- - 'ruoyi-fastapi-backend/scripts/plugin_release_integration.py'
- - 'ruoyi-fastapi-backend/scripts/plugin_task_integration.py'
- - 'ruoyi-fastapi-backend/tests/plugins/**'
- - 'ruoyi-fastapi-backend/tests/cli/root/test_plugin_artifact_commands.py'
- - 'ruoyi-fastapi-backend/tests/cli/root/test_plugin_release_wait.py'
- - 'ruoyi-fastapi-backend/tests/cli/root/test_plugin_diagnostics.py'
- - 'ruoyi-fastapi-backend/tests/cli/runtime/plugin/**'
- - 'ruoyi-fastapi-backend/tests/module_plugin/**'
- - 'ruoyi-fastapi-backend/tests/server/test_plugin_runtime.py'
- - 'ruoyi-fastapi-backend/tests/sql/test_plugin_release_schema.py'
- - 'ruoyi-fastapi-backend/pyproject.toml'
- - 'ruoyi-fastapi-backend/requirements.txt'
- - 'ruoyi-fastapi-backend/requirements-pg.txt'
- - '.github/workflows/plugin-release-integration.yml'
- workflow_dispatch:
-
-permissions:
- contents: read
-
-jobs:
- release:
- runs-on: ubuntu-latest
- strategy:
- fail-fast: false
- matrix:
- python: ['3.10', '3.11', '3.12', '3.13']
- defaults:
- run:
- working-directory: ruoyi-fastapi-backend
- services:
- mysql:
- image: mysql:8.0
- env:
- MYSQL_ROOT_PASSWORD: ci-only-mysql
- ports: ['3306:3306']
- options: >-
- --health-cmd="mysqladmin ping -h localhost"
- --health-interval=10s --health-timeout=5s --health-retries=12
- postgres:
- image: postgres:14
- env:
- POSTGRES_PASSWORD: ci-only-postgres
- ports: ['5432:5432']
- options: >-
- --health-cmd="pg_isready -U postgres"
- --health-interval=10s --health-timeout=5s --health-retries=12
- redis:
- image: redis:6.2
- ports: ['6379:6379']
- options: >-
- --health-cmd="redis-cli ping"
- --health-interval=10s --health-timeout=5s --health-retries=12
- env:
- RUOYI_PLUGIN_IT_ALLOW: '1'
- RUOYI_IT_MYSQL_PASSWORD: ci-only-mysql
- RUOYI_IT_PG_PASSWORD: ci-only-postgres
- RUOYI_IT_REDIS_PASSWORD: ''
- steps:
- - uses: actions/checkout@v5
- - uses: actions/setup-python@v6
- with:
- python-version: ${{ matrix.python }}
- cache: pip
- cache-dependency-path: ruoyi-fastapi-backend/requirements*.txt
- - name: Install host dependencies
- run: python -m pip install -r requirements.txt -r requirements-pg.txt
- - uses: actions/setup-node@v5
- with:
- node-version: '22'
- - name: Test and build task plugin bundle
- run: |
- npm --prefix plugins/examples/python/task_demo/web install --no-audit --no-fund --package-lock=false
- npm --prefix plugins/examples/python/task_demo/web test
- npm --prefix plugins/examples/python/task_demo/web run build
- - name: Verify MySQL and Redis maintenance releases
- run: python scripts/plugin_release_integration.py --engine mysql
- - name: Verify PostgreSQL and Redis maintenance releases
- run: python scripts/plugin_release_integration.py --engine postgresql
diff --git a/.github/workflows/ruff.yml b/.github/workflows/ruff.yml
deleted file mode 100644
index 0d76548..0000000
--- a/.github/workflows/ruff.yml
+++ /dev/null
@@ -1,33 +0,0 @@
-name: Ruff Check
-
-on:
- push:
- branches: [master]
- pull_request:
- branches: [master]
-
-jobs:
- lint-format:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v5
-
- - name: Set up Python
- uses: actions/setup-python@v5
- with:
- python-version: "3.10"
-
- - name: Install dependencies
- run: |
- python -m pip install --upgrade pip
- pip install ruff
-
- - name: Run linting
- run: |
- ruff check ruoyi-fastapi-backend
- ruff check ruoyi-fastapi-test
-
- - name: Run format check
- run: |
- ruff format ruoyi-fastapi-backend --check
- ruff format ruoyi-fastapi-test --check
diff --git a/.github/workflows/timezone.yml b/.github/workflows/timezone.yml
deleted file mode 100644
index 99d14da..0000000
--- a/.github/workflows/timezone.yml
+++ /dev/null
@@ -1,106 +0,0 @@
-name: Timezone Integration
-
-on:
- push:
- branches: [master]
- pull_request:
- branches: [master]
- workflow_dispatch:
-
-jobs:
- databases:
- runs-on: ubuntu-latest
- env:
- TZ: America/New_York
- PYTHONUTF8: '1'
- services:
- mysql:
- image: mysql:8.0
- env:
- MYSQL_ROOT_PASSWORD: timezone_test
- TZ: Asia/Shanghai
- ports: ['3306:3306']
- options: >-
- --health-cmd="mysqladmin ping -h localhost -ptimezone_test"
- --health-interval=10s --health-timeout=5s --health-retries=12
- postgres:
- image: postgres:14
- env:
- POSTGRES_PASSWORD: timezone_test
- TZ: America/New_York
- PGTZ: America/New_York
- ports: ['5432:5432']
- options: >-
- --health-cmd="pg_isready -U postgres"
- --health-interval=10s --health-timeout=5s --health-retries=12
- steps:
- - uses: actions/checkout@v5
- - uses: actions/setup-python@v6
- with:
- python-version: '3.10'
- cache: pip
- cache-dependency-path: ruoyi-fastapi-backend/requirements*.txt
- - name: Install verification dependencies
- working-directory: ruoyi-fastapi-backend
- run: python -m pip install -r requirements.txt asyncpg psycopg2-binary pytest pytest-asyncio aiosqlite
- - name: Create isolated source configurations
- working-directory: ruoyi-fastapi-backend
- run: |
- mkdir -p .cache/timezone-ci
- python - <<'PY'
- import json
- from pathlib import Path
- for dialect, port, username, database in [('mysql', 3306, 'root', 'mysql'), ('postgresql', 5432, 'postgres', 'postgres')]:
- source = dict(db_type=dialect, db_host='127.0.0.1', db_port=port, db_username=username,
- db_password='timezone_test', db_database=database)
- Path(f'.cache/timezone-ci/{dialect}.json').write_text(json.dumps(source))
- PY
- - name: Verify empty databases, drivers, generated CRUD and persistent schedulers
- working-directory: ruoyi-fastapi-backend
- run: |
- python -m scripts.verify_timezone_database --config .cache/timezone-ci/mysql.json --report .cache/timezone-ci/mysql-report.json
- python -m scripts.verify_timezone_database --config .cache/timezone-ci/postgresql.json --report .cache/timezone-ci/postgresql-report.json
- - uses: actions/upload-artifact@v6
- if: always()
- with:
- name: timezone-database-reports
- path: ruoyi-fastapi-backend/.cache/timezone-ci/*-report.json
- include-hidden-files: true
-
- web-app:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v5
- - uses: actions/setup-node@v6
- with:
- node-version: '22'
- cache: npm
- # Lockfiles are ignored by this repository; cache by the committed manifest.
- cache-dependency-path: ruoyi-fastapi-frontend/package.json
- - uses: pnpm/action-setup@v5
- with:
- package_json_file: ruoyi-fastapi-app/package.json
- - name: Install Web dependencies
- working-directory: ruoyi-fastapi-frontend
- run: npm install --no-package-lock
- - name: Install App dependencies
- working-directory: ruoyi-fastapi-app
- run: pnpm install --no-frozen-lockfile
- - name: Run time contracts in three host timezones
- run: node ruoyi-fastapi-test/time-contract/run-host-timezones.mjs
- - name: Build Web targets
- working-directory: ruoyi-fastapi-frontend
- run: |
- npm run build:prod
- npm run build:docker
- - name: Build App targets
- working-directory: ruoyi-fastapi-app
- run: |
- pnpm build:h5
- pnpm build:mp-weixin
- - name: Validate all Compose configurations
- run: |
- docker compose -f docker-compose.my.yml config --quiet
- docker compose -f docker-compose.pg.yml config --quiet
- docker compose -f ruoyi-fastapi-test/docker-compose.test.my.yml config --quiet
- docker compose -f ruoyi-fastapi-test/docker-compose.test.pg.yml config --quiet
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fe7a308..bbc904f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,6 @@
# 更新日志
-## RuoYi-Vue3-FastAPI v1.10.0
+## RuoYi-FastAPI v1.10.0
### 项目依赖
@@ -8,68 +8,68 @@
### 新增功能
-1. 新增ApiCache接口缓存装饰器和ApiCacheEvict接口缓存失效装饰器 ([#97](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/97))。
-2. 新增接口限流装饰器 ([#99](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/99))。
-3. 新增传输加密功能 ([#100](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/100))。
-4. 新增日志脱敏功能 ([#101](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/101))。
-5. 新增cli系统 ([#103](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/103))。
-6. 新增文件管理功能 ([#111](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/111))。
-7. 新增插件系统 ([#112](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/112))。
-8. 新增自动生成jwt密钥 ([#116](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/116))。
-9. 新增锁定屏幕功能 ([#117](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/117))。
-10. 字典类型列表新增抽屉效果详细信息 ([#117](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/117))。
-11. 添加持久化标签页开关功能 ([#117](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/117))。
-12. 支持表格列显隐状态记忆 ([#117](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/117))。
-13. 代码生成支持表单布局选项 ([#117](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/117))。
-14. 新增树分割组件TreePanel ([#117](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/117))。
-15. 新增Excel导入组件ExcelImportDialog ([#117](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/117))。
-16. 用户列表新增抽屉效果详细信息 ([#117](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/117))。
-17. 新增标签页样式chrome风格 ([#117](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/117))。
-18. 新增菜单路由地址和名称的校验规则 ([#117](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/117))。
-19. 首页新增通知公告消息提醒 ([#117](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/117))。
-20. 定时任务详情补充执行时间字段 ([#117](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/117))。
-21. 部门管理支持批量保存排序 ([#117](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/117))。
-22. 菜单管理支持批量保存排序 ([#117](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/117))。
-23. 菜单管理列表新增类型显示 ([#117](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/117))。
-24. 新增代码生成详情页功能 ([#117](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/117))。
-25. 通知公告新增详细显示 ([#117](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/117))。
-26. 通知公告新增阅读用户列表 ([#117](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/117))。
-27. 用户密码支持自定义配置规则 ([#117](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/117))。
+1. 新增ApiCache接口缓存装饰器和ApiCacheEvict接口缓存失效装饰器 ([#97](https://github.com/insistence/RuoYi-FastAPI/pull/97))。
+2. 新增接口限流装饰器 ([#99](https://github.com/insistence/RuoYi-FastAPI/pull/99))。
+3. 新增传输加密功能 ([#100](https://github.com/insistence/RuoYi-FastAPI/pull/100))。
+4. 新增日志脱敏功能 ([#101](https://github.com/insistence/RuoYi-FastAPI/pull/101))。
+5. 新增cli系统 ([#103](https://github.com/insistence/RuoYi-FastAPI/pull/103))。
+6. 新增文件管理功能 ([#111](https://github.com/insistence/RuoYi-FastAPI/pull/111))。
+7. 新增插件系统 ([#112](https://github.com/insistence/RuoYi-FastAPI/pull/112))。
+8. 新增自动生成jwt密钥 ([#116](https://github.com/insistence/RuoYi-FastAPI/pull/116))。
+9. 新增锁定屏幕功能 ([#117](https://github.com/insistence/RuoYi-FastAPI/pull/117))。
+10. 字典类型列表新增抽屉效果详细信息 ([#117](https://github.com/insistence/RuoYi-FastAPI/pull/117))。
+11. 添加持久化标签页开关功能 ([#117](https://github.com/insistence/RuoYi-FastAPI/pull/117))。
+12. 支持表格列显隐状态记忆 ([#117](https://github.com/insistence/RuoYi-FastAPI/pull/117))。
+13. 代码生成支持表单布局选项 ([#117](https://github.com/insistence/RuoYi-FastAPI/pull/117))。
+14. 新增树分割组件TreePanel ([#117](https://github.com/insistence/RuoYi-FastAPI/pull/117))。
+15. 新增Excel导入组件ExcelImportDialog ([#117](https://github.com/insistence/RuoYi-FastAPI/pull/117))。
+16. 用户列表新增抽屉效果详细信息 ([#117](https://github.com/insistence/RuoYi-FastAPI/pull/117))。
+17. 新增标签页样式chrome风格 ([#117](https://github.com/insistence/RuoYi-FastAPI/pull/117))。
+18. 新增菜单路由地址和名称的校验规则 ([#117](https://github.com/insistence/RuoYi-FastAPI/pull/117))。
+19. 首页新增通知公告消息提醒 ([#117](https://github.com/insistence/RuoYi-FastAPI/pull/117))。
+20. 定时任务详情补充执行时间字段 ([#117](https://github.com/insistence/RuoYi-FastAPI/pull/117))。
+21. 部门管理支持批量保存排序 ([#117](https://github.com/insistence/RuoYi-FastAPI/pull/117))。
+22. 菜单管理支持批量保存排序 ([#117](https://github.com/insistence/RuoYi-FastAPI/pull/117))。
+23. 菜单管理列表新增类型显示 ([#117](https://github.com/insistence/RuoYi-FastAPI/pull/117))。
+24. 新增代码生成详情页功能 ([#117](https://github.com/insistence/RuoYi-FastAPI/pull/117))。
+25. 通知公告新增详细显示 ([#117](https://github.com/insistence/RuoYi-FastAPI/pull/117))。
+26. 通知公告新增阅读用户列表 ([#117](https://github.com/insistence/RuoYi-FastAPI/pull/117))。
+27. 用户密码支持自定义配置规则 ([#117](https://github.com/insistence/RuoYi-FastAPI/pull/117))。
### BUG修复
-1. 修复日志输出格式问题 ([#104](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/104))。
-2. 修复文件服务安全性问题 ([#111](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/111))。
-3. 修复windows下应用启动日志异常的问题 ([#113](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/113))。
-4. 修复tui卡顿的问题 ([#114](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/114))。
-5. 修复E2E测试失败的问题 ([#117](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/117))。
+1. 修复日志输出格式问题 ([#104](https://github.com/insistence/RuoYi-FastAPI/pull/104))。
+2. 修复文件服务安全性问题 ([#111](https://github.com/insistence/RuoYi-FastAPI/pull/111))。
+3. 修复windows下应用启动日志异常的问题 ([#113](https://github.com/insistence/RuoYi-FastAPI/pull/113))。
+4. 修复tui卡顿的问题 ([#114](https://github.com/insistence/RuoYi-FastAPI/pull/114))。
+5. 修复E2E测试失败的问题 ([#117](https://github.com/insistence/RuoYi-FastAPI/pull/117))。
### 代码优化
-1. 补充演示模式限制路由 ([#115](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/115))。
-2. 优化字典类型属性提醒说明 ([#117](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/117))。
-3. 优化顶部导航更多菜单路由截取 ([#117](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/117))。
-4. 优化RightToolbar搜索栏切换动画 ([#117](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/117))。
-5. 优化操作日志详细页面 ([#117](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/117))。
-7. 优化页签功能&支持全屏按钮操作 ([#117](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/117))。
-8. 优化tag全屏为页签内区域 ([#117](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/117))。
-9. 菜单搜索支持文本高亮&数量提示 ([#117](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/117))。
-10. 优化页签显示 ([#117](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/117))。
-11. 优化菜单主题风格 ([#117](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/117))。
-12. 黑暗模式下全屏不需要设置背景 ([#117](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/117))。
-13. 暗黑浅色主题优化 ([#117](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/117))。
-14. 优化快速点击页签刷新出现404问题 ([#117](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/117))。
-15. 代码生成修改拖拽时显示手指样式 ([#117](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/117))。
-16. 优化登录页暗黑模式下样式 ([#117](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/117))。
-17. 优化白名单支持对通配符路径匹配 ([#117](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/117))。
-18. 优化定时任务详情页展示 ([#117](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/117))。
-19. 优化点击任务名称查看详细 ([#117](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/117))。
-20. 更换router4过期方法next ([#117](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/117))。
-21. 优化暗黑模式下主题样式 ([#117](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/117))。
-22. 优化暗黑模式详情样式 ([#117](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/117))。
-23. 优化jwt解析异常处理机制 ([#118](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/118))。
+1. 补充演示模式限制路由 ([#115](https://github.com/insistence/RuoYi-FastAPI/pull/115))。
+2. 优化字典类型属性提醒说明 ([#117](https://github.com/insistence/RuoYi-FastAPI/pull/117))。
+3. 优化顶部导航更多菜单路由截取 ([#117](https://github.com/insistence/RuoYi-FastAPI/pull/117))。
+4. 优化RightToolbar搜索栏切换动画 ([#117](https://github.com/insistence/RuoYi-FastAPI/pull/117))。
+5. 优化操作日志详细页面 ([#117](https://github.com/insistence/RuoYi-FastAPI/pull/117))。
+7. 优化页签功能&支持全屏按钮操作 ([#117](https://github.com/insistence/RuoYi-FastAPI/pull/117))。
+8. 优化tag全屏为页签内区域 ([#117](https://github.com/insistence/RuoYi-FastAPI/pull/117))。
+9. 菜单搜索支持文本高亮&数量提示 ([#117](https://github.com/insistence/RuoYi-FastAPI/pull/117))。
+10. 优化页签显示 ([#117](https://github.com/insistence/RuoYi-FastAPI/pull/117))。
+11. 优化菜单主题风格 ([#117](https://github.com/insistence/RuoYi-FastAPI/pull/117))。
+12. 黑暗模式下全屏不需要设置背景 ([#117](https://github.com/insistence/RuoYi-FastAPI/pull/117))。
+13. 暗黑浅色主题优化 ([#117](https://github.com/insistence/RuoYi-FastAPI/pull/117))。
+14. 优化快速点击页签刷新出现404问题 ([#117](https://github.com/insistence/RuoYi-FastAPI/pull/117))。
+15. 代码生成修改拖拽时显示手指样式 ([#117](https://github.com/insistence/RuoYi-FastAPI/pull/117))。
+16. 优化登录页暗黑模式下样式 ([#117](https://github.com/insistence/RuoYi-FastAPI/pull/117))。
+17. 优化白名单支持对通配符路径匹配 ([#117](https://github.com/insistence/RuoYi-FastAPI/pull/117))。
+18. 优化定时任务详情页展示 ([#117](https://github.com/insistence/RuoYi-FastAPI/pull/117))。
+19. 优化点击任务名称查看详细 ([#117](https://github.com/insistence/RuoYi-FastAPI/pull/117))。
+20. 更换router4过期方法next ([#117](https://github.com/insistence/RuoYi-FastAPI/pull/117))。
+21. 优化暗黑模式下主题样式 ([#117](https://github.com/insistence/RuoYi-FastAPI/pull/117))。
+22. 优化暗黑模式详情样式 ([#117](https://github.com/insistence/RuoYi-FastAPI/pull/117))。
+23. 优化jwt解析异常处理机制 ([#118](https://github.com/insistence/RuoYi-FastAPI/pull/118))。
-## RuoYi-Vue3-FastAPI v1.9.0
+## RuoYi-FastAPI v1.9.0
### 项目依赖
@@ -77,52 +77,52 @@
### 新增功能
-1. 新增AI管理模块 ([#69](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/69))。
-2. 新增移动端模块 ([#73](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/73))。
-3. 新增多worker运行支持 ([#76](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/76))。
-4. 应用新增演示模式 ([#78](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/78))。
+1. 新增AI管理模块 ([#69](https://github.com/insistence/RuoYi-FastAPI/pull/69))。
+2. 新增移动端模块 ([#73](https://github.com/insistence/RuoYi-FastAPI/pull/73))。
+3. 新增多worker运行支持 ([#76](https://github.com/insistence/RuoYi-FastAPI/pull/76))。
+4. 应用新增演示模式 ([#78](https://github.com/insistence/RuoYi-FastAPI/pull/78))。
### BUG修复
-1. 修复代码生成controller模板删除接口query_db参数异常的问题 ([#63](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/63))。
-2. 修复登录接口response_model声明错误 ([#71](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/71))。
-3. 修复无法直接通过后端地址访问API文档的问题 ([#74](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/74))。
-4. 修复create_app重复执行的问题 ([#84](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/84))。
+1. 修复代码生成controller模板删除接口query_db参数异常的问题 ([#63](https://github.com/insistence/RuoYi-FastAPI/pull/63))。
+2. 修复登录接口response_model声明错误 ([#71](https://github.com/insistence/RuoYi-FastAPI/pull/71))。
+3. 修复无法直接通过后端地址访问API文档的问题 ([#74](https://github.com/insistence/RuoYi-FastAPI/pull/74))。
+4. 修复create_app重复执行的问题 ([#84](https://github.com/insistence/RuoYi-FastAPI/pull/84))。
### 代码重构
-1. 移除对python3.9的支持 ([#67](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/67))。
+1. 移除对python3.9的支持 ([#67](https://github.com/insistence/RuoYi-FastAPI/pull/67))。
### 代码优化
-1. 优化alembic处理表模型逻辑,避免无关表影响 ([#68](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/68))。
-2. 优化代码生成后端模板 ([#72](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/72))。
-3. 自动注册路由出错时抛出异常以便于调试 ([#79](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/79))。
+1. 优化alembic处理表模型逻辑,避免无关表影响 ([#68](https://github.com/insistence/RuoYi-FastAPI/pull/68))。
+2. 优化代码生成后端模板 ([#72](https://github.com/insistence/RuoYi-FastAPI/pull/72))。
+3. 自动注册路由出错时抛出异常以便于调试 ([#79](https://github.com/insistence/RuoYi-FastAPI/pull/79))。
4. 优化部分页面字段tooltip说明 (#80)。
-5. 优化项目启动速度 ([#82](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/82))。
-6. 优化暗黑模式切换效果 ([#83](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/83))。
-7. 优化热重载模式或单worker下scheduler的任务状态同步机制 ([#85](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/85))。
-8. 优化防重提交间隔时间可自定义 ([#87](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/87))。
-9. 优化验证码计算结果为非负数 ([#88](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/88))。
-10. 优化ci测试稳定性 ([#90](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/90))。
+5. 优化项目启动速度 ([#82](https://github.com/insistence/RuoYi-FastAPI/pull/82))。
+6. 优化暗黑模式切换效果 ([#83](https://github.com/insistence/RuoYi-FastAPI/pull/83))。
+7. 优化热重载模式或单worker下scheduler的任务状态同步机制 ([#85](https://github.com/insistence/RuoYi-FastAPI/pull/85))。
+8. 优化防重提交间隔时间可自定义 ([#87](https://github.com/insistence/RuoYi-FastAPI/pull/87))。
+9. 优化验证码计算结果为非负数 ([#88](https://github.com/insistence/RuoYi-FastAPI/pull/88))。
+10. 优化ci测试稳定性 ([#90](https://github.com/insistence/RuoYi-FastAPI/pull/90))。
-## RuoYi-Vue3-FastAPI v1.8.1
+## RuoYi-FastAPI v1.8.1
### 新增功能
-1. 新增E2E测试 ([#57](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/57))。
+1. 新增E2E测试 ([#57](https://github.com/insistence/RuoYi-FastAPI/pull/57))。
### BUG修复
-1. 修复DictTag组件渲染异常的问题 ([#59](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/59))。
+1. 修复DictTag组件渲染异常的问题 ([#59](https://github.com/insistence/RuoYi-FastAPI/pull/59))。
### 代码优化
-1. 优化数据权限依赖 ([#55](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/55))。
-2. 动态导入定时任务函数,移除eval ([#56](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/56))。
-3. 优化pg版本的docker compose配置文件 ([#61](https://github.com/insistence/RuoYi-Vue3-FastAPI/pull/61))。
+1. 优化数据权限依赖 ([#55](https://github.com/insistence/RuoYi-FastAPI/pull/55))。
+2. 动态导入定时任务函数,移除eval ([#56](https://github.com/insistence/RuoYi-FastAPI/pull/56))。
+3. 优化pg版本的docker compose配置文件 ([#61](https://github.com/insistence/RuoYi-FastAPI/pull/61))。
-## RuoYi-Vue3-FastAPI v1.8.0
+## RuoYi-FastAPI v1.8.0
### 项目依赖
@@ -143,8 +143,8 @@
### BUG修复
-1. 修复单账号登录模式下强退功能失效的问题 [#52](https://github.com/insistence/RuoYi-Vue3-FastAPI/issues/52)。
-2. 确保ApschedulerJobs字段类型与apscheduler默认创建的表字段类型一致 [#53](https://github.com/insistence/RuoYi-Vue3-FastAPI/issues/53)。
+1. 修复单账号登录模式下强退功能失效的问题 [#52](https://github.com/insistence/RuoYi-FastAPI/issues/52)。
+2. 确保ApschedulerJobs字段类型与apscheduler默认创建的表字段类型一致 [#53](https://github.com/insistence/RuoYi-FastAPI/issues/53)。
3. 修复磁盘存在异常时服务监控无法正常运行的问题。
4. 移除代码生成表业务表外键,修复无法删除的问题。
5. 修复固定头部时出现的导航栏偏移问题。
@@ -175,7 +175,7 @@
14. 优化字典组件值宽松匹配。
15. 默认固定头部。
-## RuoYi-Vue3-FastAPI v1.7.1
+## RuoYi-FastAPI v1.7.1
### 项目依赖
@@ -184,13 +184,13 @@
### BUG修复
1. 修复代码生成controller模板编辑接口异常生成字段的问题。
-2. 移除passlib直接使用bcrypt修复密码校验异常的问题 [#48](https://github.com/insistence/RuoYi-Vue3-FastAPI/issues/48) [#49](https://github.com/insistence/RuoYi-Vue3-FastAPI/issues/49)。
+2. 移除passlib直接使用bcrypt修复密码校验异常的问题 [#48](https://github.com/insistence/RuoYi-FastAPI/issues/48) [#49](https://github.com/insistence/RuoYi-FastAPI/issues/49)。
### 代码优化
1. 代码生成do模板补充表描述。
-## RuoYi-Vue3-FastAPI v1.7.0
+## RuoYi-FastAPI v1.7.0
### 项目依赖
@@ -221,7 +221,7 @@
### BUG修复
-1. 修复logout接口未按照app_same_time_login配置项动态判断的问题 [#IBZZ1S](https://gitee.com/insistence2022/RuoYi-Vue3-FastAPI/issues/IBZZ1S)。
+1. 修复logout接口未按照app_same_time_login配置项动态判断的问题 [#IBZZ1S](https://gitee.com/insistence2022/RuoYi-FastAPI/issues/IBZZ1S)。
2. 修复上传组件被多次引用拖动仅对第一个有效的问题。
### 代码优化
@@ -241,7 +241,7 @@
2. 调整do与sql使其相互适配以支持alembic。
3. 富文本复制粘贴图片上传至url。
-## RuoYi-Vue3-FastAPI v1.6.2
+## RuoYi-FastAPI v1.6.2
### 新增功能
@@ -250,20 +250,20 @@
### BUG修复
-1. 修复日志管理时间查询报错 [#27](https://github.com/insistence/RuoYi-Vue3-FastAPI/issues/27)。
-2. 修复定时任务状态暂停时执行单次任务会触发cron表达式的问题 [#31](https://github.com/insistence/RuoYi-Vue3-FastAPI/issues/31)。
+1. 修复日志管理时间查询报错 [#27](https://github.com/insistence/RuoYi-FastAPI/issues/27)。
+2. 修复定时任务状态暂停时执行单次任务会触发cron表达式的问题 [#31](https://github.com/insistence/RuoYi-FastAPI/issues/31)。
3. 修复修改字典类型时获取dict_code异常的问题。
4. 修复修改字典类型时字典数据更新时间异常的问题。
-5. 修复代码生成模板时间查询问题 [#28](https://github.com/insistence/RuoYi-Vue3-FastAPI/issues/28)。
+5. 修复代码生成模板时间查询问题 [#28](https://github.com/insistence/RuoYi-FastAPI/issues/28)。
6. 修复用户导出缺失部门名称的问题。
### 代码优化
1. 优化代码生成新增和编辑字段显示和渲染。
2. pagination更换成flex布局。
-3. 优化代码生成vue模板 [#23](https://github.com/insistence/RuoYi-Vue3-FastAPI/issues/23)。
+3. 优化代码生成vue模板 [#23](https://github.com/insistence/RuoYi-FastAPI/issues/23)。
-## RuoYi-Vue3-FastAPI v1.6.1
+## RuoYi-FastAPI v1.6.1
### 项目依赖
@@ -282,7 +282,7 @@ pip install sqlglot[rs]==26.6.0 -i https://pypi.tuna.tsinghua.edu.cn/simple
3. 引入泛型修复as_query和as_form装饰模型文档丢失的问题。
4. 修复代码生成主子表vo模板可能缺失NotBlank的问题。
-## RuoYi-Vue3-FastAPI v1.6.0
+## RuoYi-FastAPI v1.6.0
### 项目依赖
@@ -304,7 +304,7 @@ pip install sqlglot[rs]==26.6.0 -i https://pypi.tuna.tsinghua.edu.cn/simple
1. 修复默认关闭Tags-Views时,内链页面打不开。
2. 修复删除当前登录用户拦截失效的问题。
3. 修复定时任务目标字符串规则校验不全的问题。
-4. 修复执行单次任务时会覆盖已启用任务的问题 [#IBEKD2](https://gitee.com/insistence2022/RuoYi-Vue3-FastAPI/issues/IBEKD2)。
+4. 修复执行单次任务时会覆盖已启用任务的问题 [#IBEKD2](https://gitee.com/insistence2022/RuoYi-FastAPI/issues/IBEKD2)。
5. 修复个人中心特殊字符密码修改失败问题。
### 代码优化
@@ -317,7 +317,7 @@ pip install sqlglot[rs]==26.6.0 -i https://pypi.tuna.tsinghua.edu.cn/simple
6. 优化TopNav内链菜单点击没有高亮。
7. ResponseUtil补充完整参数。
-## RuoYi-Vue3-FastAPI v1.5.1
+## RuoYi-FastAPI v1.5.1
### 新增功能
@@ -329,7 +329,7 @@ pip install sqlglot[rs]==26.6.0 -i https://pypi.tuna.tsinghua.edu.cn/simple
2. 校检文件名是否包含特殊字符。
3. 移除已弃用的log_decorator装饰器。
-## RuoYi-Vue3-FastAPI v1.5.0
+## RuoYi-FastAPI v1.5.0
### 新增功能
@@ -337,7 +337,7 @@ pip install sqlglot[rs]==26.6.0 -i https://pypi.tuna.tsinghua.edu.cn/simple
### BUG修复
-1. 修复DictTag组件控制台抛异常的问题 [#IAYSVZ](https://gitee.com/insistence2022/RuoYi-Vue3-FastAPI/issues/IAYSVZ)。
+1. 修复DictTag组件控制台抛异常的问题 [#IAYSVZ](https://gitee.com/insistence2022/RuoYi-FastAPI/issues/IAYSVZ)。
2. 修复登录日志导出文件名称错误的问题。
### 代码回滚
@@ -351,7 +351,7 @@ pip install sqlglot[rs]==26.6.0 -i https://pypi.tuna.tsinghua.edu.cn/simple
3. 优化参数设置页面。
4. 优化上传图片带域名时不增加前缀。
-## RuoYi-Vue3-FastAPI v1.4.0
+## RuoYi-FastAPI v1.4.0
### 项目依赖
@@ -375,7 +375,7 @@ pip install fastapi[all]==0.115.0 -i https://pypi.tuna.tsinghua.edu.cn/simple
1. 优化前端登录请求方法。
-## RuoYi-Vue3-FastAPI v1.3.3
+## RuoYi-FastAPI v1.3.3
### 项目依赖
@@ -392,7 +392,7 @@ pip install fastapi[all]==0.115.0 -i https://pypi.tuna.tsinghua.edu.cn/simple
1. 优化在线用户模块前后端字段描述一致。
2. 日志装饰器异常处理增加logger打印日志。
-## RuoYi-Vue3-FastAPI v1.3.2
+## RuoYi-FastAPI v1.3.2
### 新增功能
@@ -400,14 +400,14 @@ pip install fastapi[all]==0.115.0 -i https://pypi.tuna.tsinghua.edu.cn/simple
### BUG修复
-1. 修复分页函数计算has_next错误的问题 [#10](https://github.com/insistence/RuoYi-Vue3-FastAPI/issues/10)。
+1. 修复分页函数计算has_next错误的问题 [#10](https://github.com/insistence/RuoYi-FastAPI/issues/10)。
2. 修复定时任务监听函数中事件没有job_id报错的问题。
### 代码优化
1. 优化添加中间件函数注释。
-## RuoYi-Vue3-FastAPI v1.3.1
+## RuoYi-FastAPI v1.3.1
### BUG修复
@@ -417,7 +417,7 @@ pip install fastapi[all]==0.115.0 -i https://pypi.tuna.tsinghua.edu.cn/simple
1. 补充定时任务违规字符串。
-## RuoYi-Vue3-FastAPI v1.3.0
+## RuoYi-FastAPI v1.3.0
### 项目依赖
@@ -459,14 +459,14 @@ ALTER TABLE sys_menu ADD COLUMN route_name varchar(50) DEFAULT '';
3. 各模块`vo`层使用`Field`声明字段。
4. 优化API文档字段描述显示。
-## RuoYi-Vue3-FastAPI v1.2.2
+## RuoYi-FastAPI v1.2.2
### BUG修复
1. 修复删除定时任务时未移除调度中任务的问题。
2. 修复菜单生成路由时组件条件判断错误的问题。
-## RuoYi-Vue3-FastAPI v1.2.1
+## RuoYi-FastAPI v1.2.1
### BUG修复
@@ -477,7 +477,7 @@ ALTER TABLE sys_menu ADD COLUMN route_name varchar(50) DEFAULT '';
1. 修改代理localhost为127.0.0.1以适配部分设备解析localhost异常的问题。
-## RuoYi-Vue3-FastAPI v1.2.0
+## RuoYi-FastAPI v1.2.0
### 重要说明
@@ -505,7 +505,7 @@ pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple。
1. 修复用户管理模块重置用户密码时会异常重置用户岗位和角色的问题。
2. 修复清空定时任务日志异常的问题。
-## RuoYi-Vue3-FastAPI v1.1.3
+## RuoYi-FastAPI v1.1.3
### 新增功能
@@ -516,7 +516,7 @@ pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple。
1. 修复通知公告列表查询前后端字段不一致的问题。
2. 修复个人中心修改基本资料后端异常的问题。
-## RuoYi-Vue3-FastAPI v1.1.2
+## RuoYi-FastAPI v1.1.2
### 新增功能
@@ -530,17 +530,17 @@ pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple。
1. 使用@lru_cache缓存ip归属区域查询结果,避免重复调用ip归属区域查询接口以优化性能。
-## RuoYi-Vue3-FastAPI v1.1.1
+## RuoYi-FastAPI v1.1.1
### BUG修复
-1. 修复编辑定时任务时更新的信息未同步至scheduler的问题 [#I9EK56](https://gitee.com/insistence2022/RuoYi-Vue3-FastAPI/issues/I9EK56)。
-2. 修复编辑角色数据权限时后端异常的问题 [#I9ENQN](https://gitee.com/insistence2022/RuoYi-Vue3-FastAPI/issues/I9ENQN)。
+1. 修复编辑定时任务时更新的信息未同步至scheduler的问题 [#I9EK56](https://gitee.com/insistence2022/RuoYi-FastAPI/issues/I9EK56)。
+2. 修复编辑角色数据权限时后端异常的问题 [#I9ENQN](https://gitee.com/insistence2022/RuoYi-FastAPI/issues/I9ENQN)。
3. 修复菜单配置路由参数不生效的问题。
4. 修复获取路由信息时菜单排序不生效的问题。
5. 修复添加菜单时是否外链和是否缓存回显异常的问题。
-## RuoYi-Vue3-FastAPI v1.1.0
+## RuoYi-FastAPI v1.1.0
### 新增功能
@@ -550,10 +550,10 @@ pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple。
### BUG修复
-1. 修复token本身过期时退出登录接口异常的问题 [#I9CBWT](https://gitee.com/insistence2022/RuoYi-Vue3-FastAPI/issues/I9CBWT)。
-2. 修复系统版本号或浏览器版本号无法获取时登录异常的问题 [#I9CYNM](https://gitee.com/insistence2022/RuoYi-Vue3-FastAPI/issues/I9CYNM)。
+1. 修复token本身过期时退出登录接口异常的问题 [#I9CBWT](https://gitee.com/insistence2022/RuoYi-FastAPI/issues/I9CBWT)。
+2. 修复系统版本号或浏览器版本号无法获取时登录异常的问题 [#I9CYNM](https://gitee.com/insistence2022/RuoYi-FastAPI/issues/I9CYNM)。
-## RuoYi-Vue3-FastAPI v1.0.3
+## RuoYi-FastAPI v1.0.3
### 新增功能
@@ -561,10 +561,10 @@ pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple。
### BUG修复
-1. 修复外链菜单无法打开的问题 [#I95KBK](https://gitee.com/insistence2022/RuoYi-Vue3-FastAPI/issues/I95KBK)。
-2. 修复添加和编辑菜单页面中是否缓存和是否外链字段回显异常的问题 [#I95KBK](https://gitee.com/insistence2022/RuoYi-Vue3-FastAPI/issues/I95KBK)。
+1. 修复外链菜单无法打开的问题 [#I95KBK](https://gitee.com/insistence2022/RuoYi-FastAPI/issues/I95KBK)。
+2. 修复添加和编辑菜单页面中是否缓存和是否外链字段回显异常的问题 [#I95KBK](https://gitee.com/insistence2022/RuoYi-FastAPI/issues/I95KBK)。
-## RuoYi-Vue3-FastAPI v1.0.2
+## RuoYi-FastAPI v1.0.2
### 新增功能
@@ -579,7 +579,7 @@ pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple。
1. 调整参数设置、部门管理、字典管理、定时任务、日志管理、角色管理、菜单管理模块部分接口权限标识。
-## RuoYi-Vue3-FastAPI v1.0.1
+## RuoYi-FastAPI v1.0.1
### 项目依赖
@@ -595,9 +595,9 @@ pip install fastapi[all]==0.109.1 -i https://mirrors.aliyun.com/pypi/simple/
1. 日志管理模块新增字段排序查询。
-## RuoYi-Vue3-FastAPI v1.0.0
+## RuoYi-FastAPI v1.0.0
-RuoYi-Vue3-FastAPI第一个版本发布啦!
+RuoYi-FastAPI第一个版本发布啦!
此版本功能如下:
1. 用户管理:用户是系统操作者,该功能主要完成系统用户配置。
2. 角色管理:角色菜单权限分配。
diff --git a/README.md b/README.md
index ab9bc27..38ff587 100644
--- a/README.md
+++ b/README.md
@@ -1,28 +1,25 @@
-
+
-RuoYi-Vue3-FastAPI
-基于RuoYi-Vue3+FastAPI前后端分离的快速开发框架
+RuoYi-FastAPI
+基于 FastAPI 的前后端分离快速开发框架
-
-
+
+
-
-
+
+
-
-
+
+
-
-
-
-
+
-
+
@@ -33,16 +30,13 @@
## 平台简介
-RuoYi-Vue3-FastAPI是一套全部开源的快速开发平台,毫无保留给个人及企业免费使用。
+RuoYi-FastAPI是一套全部开源的快速开发平台,毫无保留给个人及企业免费使用。
-* 前端采用Vue3、Element Plus,基于[RuoYi-Vue3](https://github.com/yangzongzhuan/RuoYi-Vue3)前端项目修改。
-* 移动端采用uni-app、Vue3、Vite,内置tailwindcss,基于[RuoYi-App](https://github.com/yangzongzhuan/RuoYi-App)项目修改。
+* Web 前端提供 Vue2 + Element UI 和 Vue3 + Element Plus 两套工程,分别基于[RuoYi-Vue](https://github.com/yangzongzhuan/RuoYi-Vue)和[RuoYi-Vue3](https://github.com/yangzongzhuan/RuoYi-Vue3)修改。
+* 移动端提供 uni-app 的 Vue2、Vue3 两套工程,内置 tailwindcss,基于[RuoYi-App](https://github.com/yangzongzhuan/RuoYi-App)项目修改。
* 后端采用FastAPI、sqlalchemy、MySQL(PostgreSQL)、Redis、OAuth2 & Jwt。
* 权限认证使用OAuth2 & Jwt,支持多终端认证系统。
* 支持加载动态权限菜单,多方式轻松权限控制。
-* Vue2版本:
- * Gitte仓库地址:
- * GitHub仓库地址:
* 纯Python版本:
* Gitte仓库地址:
* GitHub仓库地址:
@@ -200,6 +194,23 @@ RuoYi-Vue3-FastAPI是一套全部开源的快速开发平台,毫无保留给
## 项目开发及发布相关
+### 工程目录
+
+前端目录约定为 `ruoyi-fastapi-frontend//{web,mobile}`,其中 `` 是前端框架标识,当前为 `vue2`、`vue3`。
+
+```text
+RuoYi-FastAPI/
+├── ruoyi-fastapi-backend/ # 共用 FastAPI 后端
+├── ruoyi-fastapi-frontend/
+│ ├── vue2/
+│ │ ├── web/ # Vue2 + Element UI
+│ │ └── mobile/ # uni-app + Vue2
+│ └── vue3/
+│ ├── web/ # Vue3 + Element Plus + Vite
+│ └── mobile/ # uni-app + Vue3 + Vite
+└── ruoyi-fastapi-test/ # 端到端与跨工程契约测试
+```
+
### 传输层加解密配置说明
后端密钥配置与轮换说明:[ruoyi-fastapi-backend/docs/transport_crypto_config.md](./ruoyi-fastapi-backend/docs/transport_crypto_config.md)
@@ -208,17 +219,18 @@ RuoYi-Vue3-FastAPI是一套全部开源的快速开发平台,毫无保留给
```bash
# 克隆项目
-git clone https://gitee.com/insistence2022/RuoYi-Vue3-FastAPI.git
+git clone https://gitee.com/insistence2022/RuoYi-FastAPI.git
# 进入项目根目录
-cd RuoYi-Vue3-FastAPI
+cd RuoYi-FastAPI
```
-#### 前端
+#### Web 前端
```bash
-# 进入前端目录
-cd ruoyi-fastapi-frontend
+# 从仓库根目录进入 Vue3 Web 工程
+cd ruoyi-fastapi-frontend/vue3/web
+# 使用 Vue2 时改为:cd ruoyi-fastapi-frontend/vue2/web
# 安装依赖
npm install 或 yarn --registry=https://registry.npmmirror.com
@@ -230,11 +242,15 @@ npm install --registry=https://registry.npmmirror.com
npm run dev 或 yarn dev
```
-#### 移动端
+两个 Web 工程默认都使用 `80` 端口,开发代理均指向同一个 `9099` 后端。同时运行时,请分别配置不同的前端端口。
+
+Vue2 Web 使用 Vue CLI 4 / webpack 4,在 Node.js 18 及以上启动或构建前,需要设置 `NODE_OPTIONS=--openssl-legacy-provider`:PowerShell 执行 `$env:NODE_OPTIONS = '--openssl-legacy-provider'`,Bash 执行 `export NODE_OPTIONS=--openssl-legacy-provider`。Docker 和构建 CI 已配置此选项。
+
+#### Vue3 移动端
```bash
-# 进入移动端目录
-cd ruoyi-fastapi-app
+# 从仓库根目录进入移动端工程
+cd ruoyi-fastapi-frontend/vue3/mobile
# 安装依赖
npm install -g pnpm
@@ -247,7 +263,21 @@ pnpm dev:h5
pnpm dev:mp-weixin
```
-移动端详细文档请参考:[ruoyi-fastapi-app/README.md](./ruoyi-fastapi-app/README.md)
+#### Vue2 移动端
+
+```bash
+# 从仓库根目录进入移动端工程
+cd ruoyi-fastapi-frontend/vue2/mobile
+yarn install
+
+# 启动 H5
+yarn dev:h5
+
+# 启动微信小程序
+yarn dev:mp-weixin
+```
+
+移动端详细文档请参考:[Vue3 移动端](./ruoyi-fastapi-frontend/vue3/mobile/README.md)、[Vue2 移动端](./ruoyi-fastapi-frontend/vue2/mobile/README.md)。
#### 后端
@@ -289,7 +319,9 @@ ruoyi app run --env=dev
### 发布
-#### 前端
+#### Web 前端
+
+先进入要发布的 `ruoyi-fastapi-frontend/vue2/web` 或 `ruoyi-fastapi-frontend/vue3/web` 工程,再运行:
```bash
# 构建测试环境
@@ -313,16 +345,18 @@ ruoyi app run --env=prod
> ⚠️ **警告:** 默认未做数据持久化配置,请注意数据备份或自行配置持久化
+Compose 通过前端框架标识选择 Web 工程。仓库根目录的 `docker.env` 默认设置 `FRONTEND_FRAMEWORK=vue3`,当前可选值为 `vue2`、`vue3`;改为 `vue2` 可切换到 Vue2 Web。执行下方命令时,通过 `--env-file docker.env` 显式加载配置。该变量同时选择 `ruoyi-fastapi-frontend//web` 构建目录与对应的 nginx 配置,并通过 `RUOYI_PLUGIN_FRONTEND_FRAMEWORK` 传入后端,使插件依赖检查和安装使用相同框架分类。各框架共用后端服务与数据库配置;本次部署只构建所选 Web 工程,不会构建移动端。
+
#### MySQL版本
```bash
-docker compose -f docker-compose.my.yml up -d --build
+docker compose --env-file docker.env -f docker-compose.my.yml up -d --build
```
#### PostgreSQL版本
```bash
-docker compose -f docker-compose.pg.yml up -d --build
+docker compose --env-file docker.env -f docker-compose.pg.yml up -d --build
```
## 交流与赞助
diff --git a/docker-compose.my.yml b/docker-compose.my.yml
index e6a8dd8..eb1458a 100644
--- a/docker-compose.my.yml
+++ b/docker-compose.my.yml
@@ -2,7 +2,7 @@ services:
# 前端服务
ruoyi-frontend:
build:
- context: ./ruoyi-fastapi-frontend
+ context: ./ruoyi-fastapi-frontend/${FRONTEND_FRAMEWORK:-vue3}/web
dockerfile: Dockerfile
image: ruoyi-frontend:latest
container_name: ruoyi-frontend
@@ -11,7 +11,7 @@ services:
ports:
- "12580:80"
volumes:
- - ./ruoyi-fastapi-frontend/bin/nginx.dockermy.conf:/etc/nginx/conf.d/default.conf
+ - ./ruoyi-fastapi-frontend/${FRONTEND_FRAMEWORK:-vue3}/web/bin/nginx.dockermy.conf:/etc/nginx/conf.d/default.conf
depends_on:
- ruoyi-backend-my
networks:
@@ -26,6 +26,7 @@ services:
container_name: ruoyi-backend-my
environment:
TZ: UTC
+ RUOYI_PLUGIN_FRONTEND_FRAMEWORK: ${FRONTEND_FRAMEWORK:-vue3}
ports:
- "19099:9099"
depends_on:
diff --git a/docker-compose.pg.yml b/docker-compose.pg.yml
index 496008d..70fc764 100644
--- a/docker-compose.pg.yml
+++ b/docker-compose.pg.yml
@@ -2,7 +2,7 @@ services:
# 前端服务
ruoyi-frontend:
build:
- context: ./ruoyi-fastapi-frontend
+ context: ./ruoyi-fastapi-frontend/${FRONTEND_FRAMEWORK:-vue3}/web
dockerfile: Dockerfile
image: ruoyi-frontend:latest
container_name: ruoyi-frontend
@@ -11,7 +11,7 @@ services:
ports:
- "12580:80"
volumes:
- - ./ruoyi-fastapi-frontend/bin/nginx.dockerpg.conf:/etc/nginx/conf.d/default.conf
+ - ./ruoyi-fastapi-frontend/${FRONTEND_FRAMEWORK:-vue3}/web/bin/nginx.dockerpg.conf:/etc/nginx/conf.d/default.conf
depends_on:
- ruoyi-backend-pg
networks:
@@ -26,6 +26,7 @@ services:
container_name: ruoyi-backend-pg
environment:
TZ: UTC
+ RUOYI_PLUGIN_FRONTEND_FRAMEWORK: ${FRONTEND_FRAMEWORK:-vue3}
ports:
- "19099:9099"
depends_on:
diff --git a/docker.env b/docker.env
new file mode 100644
index 0000000..8393b26
--- /dev/null
+++ b/docker.env
@@ -0,0 +1 @@
+FRONTEND_FRAMEWORK=vue3
diff --git a/ruoyi-fastapi-app/tests/time.test.js b/ruoyi-fastapi-app/tests/time.test.js
deleted file mode 100644
index 3fdff14..0000000
--- a/ruoyi-fastapi-app/tests/time.test.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import * as time from '../src/utils/time.js'
-import { testTimeContract } from '../../ruoyi-fastapi-test/time-contract/contract.mjs'
-testTimeContract(time)
diff --git a/ruoyi-fastapi-backend/.dockerignore b/ruoyi-fastapi-backend/.dockerignore
new file mode 100644
index 0000000..9ed1862
--- /dev/null
+++ b/ruoyi-fastapi-backend/.dockerignore
@@ -0,0 +1,12 @@
+.git
+.venv
+venv
+__pycache__
+*.pyc
+.pytest_cache
+.ruff_cache
+.cache
+target
+logs
+*.log
+*.egg-info
diff --git a/ruoyi-fastapi-backend/Dockerfile.my b/ruoyi-fastapi-backend/Dockerfile.my
index 240396f..f872739 100644
--- a/ruoyi-fastapi-backend/Dockerfile.my
+++ b/ruoyi-fastapi-backend/Dockerfile.my
@@ -1,12 +1,14 @@
-FROM python:3.10
+ARG PYTHON_VERSION=3.10
+FROM python:${PYTHON_VERSION}
ENV TZ=UTC
WORKDIR /app
+ARG PIP_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple
# 复制源代码
COPY . .
# 安装依赖
-RUN pip install --no-cache-dir -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
+RUN pip install --no-cache-dir -r requirements.txt -i ${PIP_INDEX_URL}
# 暴露端口
EXPOSE 9099
diff --git a/ruoyi-fastapi-backend/Dockerfile.pg b/ruoyi-fastapi-backend/Dockerfile.pg
index cd22c60..28ddb35 100644
--- a/ruoyi-fastapi-backend/Dockerfile.pg
+++ b/ruoyi-fastapi-backend/Dockerfile.pg
@@ -1,12 +1,14 @@
-FROM python:3.10
+ARG PYTHON_VERSION=3.10
+FROM python:${PYTHON_VERSION}
ENV TZ=UTC
WORKDIR /app
+ARG PIP_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple
# 复制源代码
COPY . .
# 安装依赖
-RUN pip install --no-cache-dir -r requirements-pg.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
+RUN pip install --no-cache-dir -r requirements-pg.txt -i ${PIP_INDEX_URL}
# 暴露端口
EXPOSE 9099
diff --git a/ruoyi-fastapi-backend/cli/groups/plugin/commands/developer.py b/ruoyi-fastapi-backend/cli/groups/plugin/commands/developer.py
index 79cd577..ec3dac4 100644
--- a/ruoyi-fastapi-backend/cli/groups/plugin/commands/developer.py
+++ b/ruoyi-fastapi-backend/cli/groups/plugin/commands/developer.py
@@ -1,11 +1,11 @@
from collections.abc import Callable
-from typing import Annotated, Any, Literal
+from typing import Annotated, Any
import typer
from cli.context import DryRunOption, EnvOption, OutputOption
-from ..options import PluginCreateCommandOptions
+from ..options import PluginCreateCommandOptions, PluginFrontendFrameworkChoice
def register_developer_commands(app: typer.Typer, get_controller: Callable[[], Any]) -> None:
@@ -63,9 +63,11 @@ def register_developer_commands(app: typer.Typer, get_controller: Callable[[], A
'v2:python-asgi、python-bundle、rust-asgi、rust-bundle(生成独立源码工程)',
),
] = 'full-stack',
- frontend_version: Annotated[
- Literal['auto', 'vue2', 'vue3'],
- typer.Option('--frontend-version', help='v1 前端 Vue 版本:auto、vue2、vue3;v2 bundle 不使用宿主 Vue'),
+ frontend_framework: Annotated[
+ PluginFrontendFrameworkChoice,
+ typer.Option(
+ '--frontend-framework', help='宿主框架,auto 自动识别;v1 源码模板仅支持 vue2、vue3,v2 模板仅接受 auto'
+ ),
] = 'auto',
backend_only: Annotated[bool, typer.Option('--backend-only', help='只创建后端插件模板')] = False,
frontend_only: Annotated[bool, typer.Option('--frontend-only', help='只创建前端插件模板')] = False,
@@ -83,7 +85,7 @@ def register_developer_commands(app: typer.Typer, get_controller: Callable[[], A
:param env: 当前命令运行环境
:param output: 输出格式
:param template: 插件模板名称
- :param frontend_version: 前端 Vue 版本
+ :param frontend_framework: 前端框架标识,auto 表示自动识别
:param backend_only: 是否只创建后端插件模板
:param frontend_only: 是否只创建前端插件模板
:param no_migration: 是否不创建 migration 示例
@@ -102,7 +104,7 @@ def register_developer_commands(app: typer.Typer, get_controller: Callable[[], A
backend_only=backend_only,
frontend_only=frontend_only,
template=template,
- frontend_version=frontend_version,
+ frontend_framework=frontend_framework,
no_migration=no_migration,
no_seed=no_seed,
no_job=no_job,
diff --git a/ruoyi-fastapi-backend/cli/groups/plugin/commands/sdk.py b/ruoyi-fastapi-backend/cli/groups/plugin/commands/sdk.py
index e6aefc2..62f4db8 100644
--- a/ruoyi-fastapi-backend/cli/groups/plugin/commands/sdk.py
+++ b/ruoyi-fastapi-backend/cli/groups/plugin/commands/sdk.py
@@ -1,6 +1,5 @@
import importlib
import json
-import os
from pathlib import Path
from typing import Annotated, Any
@@ -10,9 +9,15 @@ from cli.context import CliContext, DryRunOption, OutputOption
from cli.core import DEFAULT_CORE_SERVICES
from cli.exit_codes import ARGUMENT_ERROR, DEPENDENCY_ERROR, RUNTIME_ERROR, SUCCESS
+from ..options import PluginFrontendFrameworkChoice
+
FrontendRootOption = Annotated[
Path | None,
- typer.Option('--frontend-root', help='作为 SDK 来源的宿主前端目录,默认相邻前端或 RUOYI_PLUGIN_FRONTEND_ROOT'),
+ typer.Option('--frontend-root', help='SDK 宿主工程或前端容器目录,默认使用所选框架的 web 工程'),
+]
+FrontendFrameworkOption = Annotated[
+ PluginFrontendFrameworkChoice,
+ typer.Option('--frontend-framework', help='宿主工程框架;auto 使用环境变量或默认 vue3'),
]
@@ -50,24 +55,28 @@ def _execute_sdk(
*,
dry_run: bool = False,
force: bool = False,
+ frontend_framework: PluginFrontendFrameworkChoice = 'auto',
) -> None:
"""
仅加载离线 SDK 文件服务,使用统一 CLI 输出和非零失败退出码。
:param operation: check 或 update
:param project: 独立 bundle 源码工程目录
- :param frontend_root: 可选宿主前端源码目录
+ :param frontend_root: 可选宿主前端工程根目录或包含框架分类的聚合目录
:param output: text 或 json
:param dry_run: 是否只预演更新
:param force: 是否备份后替换改动过或未跟踪的 SDK
+ :param frontend_framework: SDK 来源宿主的前端框架标识
:return: None
"""
try:
- root = frontend_root or Path(
- os.environ.get('RUOYI_PLUGIN_FRONTEND_ROOT')
- or Path(__file__).resolve().parents[5] / 'ruoyi-fastapi-frontend'
+ frontend_root_resolver = importlib.import_module('plugins.core.frontend').resolve_frontend_root
+ resolved_frontend_root = frontend_root_resolver(
+ Path(__file__).resolve().parents[4], frontend_root, frontend_framework
+ )
+ service = importlib.import_module('cli.runtime.plugin.sdk').PluginBridgeSdk(
+ resolved_frontend_root, frontend_framework=frontend_framework
)
- service = importlib.import_module('cli.runtime.plugin.sdk').PluginBridgeSdk(root)
payload = (
service.check(project) if operation == 'check' else service.update(project, dry_run=dry_run, force=force)
)
@@ -101,16 +110,18 @@ def register_sdk_commands(app: typer.Typer) -> None:
project: Annotated[Path, typer.Argument(help='含 plugin.yaml 的独立 bundle 源码工程目录')],
frontend_root: FrontendRootOption = None,
output: OutputOption = 'text',
+ frontend_framework: FrontendFrameworkOption = 'auto',
) -> None:
"""
检查源码工程的 SDK 来源与本地修改,不写文件或连接外部服务。
:param project: 独立 bundle 源码工程目录
- :param frontend_root: 可选宿主前端源码目录
+ :param frontend_root: 可选宿主前端工程根目录或包含框架分类的聚合目录
:param output: 输出格式
+ :param frontend_framework: SDK 来源宿主的前端框架标识
:return: None
"""
- _execute_sdk('check', project, frontend_root, output)
+ _execute_sdk('check', project, frontend_root, output, frontend_framework=frontend_framework)
@sdk_app.command('update', help='备份后更新 SDK 的三个文件;不会构建、安装或重启插件')
def update_sdk(
@@ -119,15 +130,25 @@ def register_sdk_commands(app: typer.Typer) -> None:
output: OutputOption = 'text',
dry_run: DryRunOption = False,
force: Annotated[bool, typer.Option('--force', help='备份后纳管旧副本或替换本地 SDK 修改')] = False,
+ frontend_framework: FrontendFrameworkOption = 'auto',
) -> None:
"""
显式更新源码工程的 SDK;默认拒绝覆盖用户改动或来源不明的副本。
:param project: 独立 bundle 源码工程目录
- :param frontend_root: 可选宿主前端源码目录
+ :param frontend_root: 可选宿主前端工程根目录或包含框架分类的聚合目录
:param output: 输出格式
:param dry_run: 是否仅预演而不写入文件
:param force: 是否备份后替换改动或未跟踪副本
+ :param frontend_framework: SDK 来源宿主的前端框架标识
:return: None
"""
- _execute_sdk('update', project, frontend_root, output, dry_run=dry_run, force=force)
+ _execute_sdk(
+ 'update',
+ project,
+ frontend_root,
+ output,
+ dry_run=dry_run,
+ force=force,
+ frontend_framework=frontend_framework,
+ )
diff --git a/ruoyi-fastapi-backend/cli/groups/plugin/controller.py b/ruoyi-fastapi-backend/cli/groups/plugin/controller.py
index 89b0d78..149933a 100644
--- a/ruoyi-fastapi-backend/cli/groups/plugin/controller.py
+++ b/ruoyi-fastapi-backend/cli/groups/plugin/controller.py
@@ -661,7 +661,7 @@ class PluginCommandController:
job=not options.no_job,
config=not options.no_config,
test=not options.no_test,
- frontend_version=options.frontend_version,
+ frontend_framework=options.frontend_framework,
dry_run=options.dry_run,
)
self.execution_service.complete_payload_with_text(
diff --git a/ruoyi-fastapi-backend/cli/groups/plugin/options.py b/ruoyi-fastapi-backend/cli/groups/plugin/options.py
index 556fa52..0950704 100644
--- a/ruoyi-fastapi-backend/cli/groups/plugin/options.py
+++ b/ruoyi-fastapi-backend/cli/groups/plugin/options.py
@@ -1,6 +1,8 @@
from dataclasses import dataclass
from typing import Literal
+PluginFrontendFrameworkChoice = Literal['auto', 'vue2', 'vue3']
+
@dataclass(frozen=True)
class PluginCreateCommandOptions:
@@ -10,7 +12,7 @@ class PluginCreateCommandOptions:
:param backend_only: 是否只创建后端插件模板
:param frontend_only: 是否只创建前端插件模板
:param template: 插件模板名称
- :param frontend_version: 前端 Vue 版本,支持 auto、vue2、vue3
+ :param frontend_framework: 前端框架标识,auto 表示自动识别;源码模板仅支持 vue2、vue3
:param no_migration: 是否不创建 migration 示例
:param no_seed: 是否不创建 seed 示例
:param no_job: 是否不创建定时任务示例
@@ -22,7 +24,7 @@ class PluginCreateCommandOptions:
backend_only: bool = False
frontend_only: bool = False
template: str = 'full-stack'
- frontend_version: Literal['auto', 'vue2', 'vue3'] = 'auto'
+ frontend_framework: PluginFrontendFrameworkChoice = 'auto'
no_migration: bool = False
no_seed: bool = False
no_job: bool = False
diff --git a/ruoyi-fastapi-backend/cli/groups/plugin/presenter.py b/ruoyi-fastapi-backend/cli/groups/plugin/presenter.py
index c18eeb2..414bf68 100644
--- a/ruoyi-fastapi-backend/cli/groups/plugin/presenter.py
+++ b/ruoyi-fastapi-backend/cli/groups/plugin/presenter.py
@@ -556,7 +556,7 @@ class PluginCommandPresenter:
f'template: {payload.get("template", "-")}',
f'backend: {str(payload.get("backend", False)).lower()}',
f'frontend: {str(payload.get("frontend", False)).lower()}',
- f'frontend_version: {payload.get("frontendVersion") or "-"}',
+ f'frontend_framework: {payload.get("frontendFramework") or "-"}',
f'migration: {str(payload.get("migration", False)).lower()}',
f'seed: {str(payload.get("seed", False)).lower()}',
f'job: {str(payload.get("job", False)).lower()}',
diff --git a/ruoyi-fastapi-backend/cli/runtime/plugin/scaffold/__init__.py b/ruoyi-fastapi-backend/cli/runtime/plugin/scaffold/__init__.py
index 9c359a4..d3b3b89 100644
--- a/ruoyi-fastapi-backend/cli/runtime/plugin/scaffold/__init__.py
+++ b/ruoyi-fastapi-backend/cli/runtime/plugin/scaffold/__init__.py
@@ -1,6 +1,6 @@
from .backend import PluginBackendScaffoldTemplateBuilder
from .builder import PluginScaffoldBuilder
-from .frontend import FrontendVersion, PluginFrontendScaffoldTemplateBuilder, PluginFrontendVersionResolver
+from .frontend import FrontendFramework, PluginFrontendFrameworkResolver, PluginFrontendScaffoldTemplateBuilder
from .naming import PluginScaffoldNaming
from .options import PluginScaffoldOptions, PluginScaffoldTemplateResolver
from .payload import (
@@ -11,10 +11,10 @@ from .payload import (
)
__all__ = [
- 'FrontendVersion',
+ 'FrontendFramework',
'PluginBackendScaffoldTemplateBuilder',
+ 'PluginFrontendFrameworkResolver',
'PluginFrontendScaffoldTemplateBuilder',
- 'PluginFrontendVersionResolver',
'PluginScaffoldBuilder',
'PluginScaffoldConflictPayload',
'PluginScaffoldNaming',
diff --git a/ruoyi-fastapi-backend/cli/runtime/plugin/scaffold/backend.py b/ruoyi-fastapi-backend/cli/runtime/plugin/scaffold/backend.py
index 9984e44..3061328 100644
--- a/ruoyi-fastapi-backend/cli/runtime/plugin/scaffold/backend.py
+++ b/ruoyi-fastapi-backend/cli/runtime/plugin/scaffold/backend.py
@@ -95,8 +95,7 @@ permissions:
dependencies:
python: []
- npm: []
- npmDev: []
+ frontend: {{}}
plugins: []
{config}
"""
diff --git a/ruoyi-fastapi-backend/cli/runtime/plugin/scaffold/builder.py b/ruoyi-fastapi-backend/cli/runtime/plugin/scaffold/builder.py
index a48542d..a6f6baa 100644
--- a/ruoyi-fastapi-backend/cli/runtime/plugin/scaffold/builder.py
+++ b/ruoyi-fastapi-backend/cli/runtime/plugin/scaffold/builder.py
@@ -1,10 +1,12 @@
+from collections.abc import Callable
from pathlib import Path
from typing import Any
+from plugins.core.frontend import select_frontend_root
from plugins.core.utils import validate_plugin_id_value
from .backend import PluginBackendScaffoldTemplateBuilder
-from .frontend import FrontendVersion, PluginFrontendScaffoldTemplateBuilder, PluginFrontendVersionResolver
+from .frontend import FrontendFramework, PluginFrontendFrameworkResolver, PluginFrontendScaffoldTemplateBuilder
from .options import PluginScaffoldOptions, PluginScaffoldTemplateResolver
from .payload import PluginScaffoldPayloadBuilder, PluginScaffoldPlanPayload
from .v2 import PluginV2ScaffoldBuilder
@@ -17,15 +19,23 @@ class PluginScaffoldBuilder:
使用 Builder 模式生成后端与前端插件模板文件计划,并在确认无冲突后落地。
"""
- def __init__(self, backend_root: Path, frontend_root: Path) -> None:
+ def __init__(
+ self,
+ backend_root: Path,
+ frontend_root: Path,
+ frontend_framework_resolver: Callable[[Path], str] | None = None,
+ ) -> None:
"""
初始化插件模板构建器。
:param backend_root: 后端项目根目录
- :param frontend_root: 前端项目根目录
+ :param frontend_root: 前端工程根目录或包含框架分类的聚合目录
+ :param frontend_framework_resolver: 宿主框架解析函数,保留运行环境中选择的框架
+ :return: None
"""
self.backend_root = backend_root
- self.frontend_root = frontend_root
+ self.frontend_root = select_frontend_root(frontend_root)
+ self.frontend_framework_resolver = frontend_framework_resolver
def build_plan(
self,
@@ -39,7 +49,7 @@ class PluginScaffoldBuilder:
job: bool = True,
config: bool = True,
test: bool = True,
- frontend_version: str = PluginFrontendVersionResolver.AUTO,
+ frontend_framework: str = PluginFrontendFrameworkResolver.AUTO,
) -> dict[str, Any]:
"""
构建插件模板写入计划。
@@ -53,19 +63,22 @@ class PluginScaffoldBuilder:
:param job: 是否创建定时任务示例
:param config: 是否创建配置项示例
:param test: 是否创建测试样例
- :param frontend_version: 前端 Vue 版本,支持 auto、vue2、vue3
+ :param frontend_framework: 目标前端框架标识,auto 表示自动识别;源码模板仅支持 vue2、vue3
:return: 插件模板写入计划
"""
self._validate_plugin_id(plugin_id)
+ frontend_root = select_frontend_root(self.frontend_root, frontend_framework)
normalized_template = (template or PluginScaffoldTemplateResolver.DEFAULT_TEMPLATE).strip()
if normalized_template in PluginScaffoldTemplateResolver.V2_TEMPLATES:
- return PluginV2ScaffoldBuilder(self.backend_root, self.frontend_root).build_plan(
+ return PluginV2ScaffoldBuilder(
+ self.backend_root, frontend_root, frontend_framework_resolver=self.frontend_framework_resolver
+ ).build_plan(
plugin_id,
template=normalized_template,
backend=backend,
frontend=frontend,
test=test,
- frontend_version=frontend_version,
+ frontend_framework=frontend_framework,
)
options = self._merge_options(
PluginScaffoldTemplateResolver.resolve(template),
@@ -84,8 +97,8 @@ class PluginScaffoldBuilder:
target_dirs = []
effective_backend_test = options.test and options.backend
effective_frontend_test = options.test and options.frontend
- resolved_frontend_version = (
- PluginFrontendVersionResolver.resolve(self.frontend_root, frontend_version) if options.frontend else None
+ resolved_frontend_framework = (
+ self._resolve_frontend_framework(frontend_root, frontend_framework) if options.frontend else None
)
if options.backend:
backend_plugin_root = self.backend_root / 'plugins' / plugin_id
@@ -94,17 +107,17 @@ class PluginScaffoldBuilder:
target_dirs.append(str(self.backend_root / 'tests' / 'plugins' / plugin_id))
files.extend(self._build_backend_files(plugin_id, backend_plugin_root, options))
if options.frontend:
- assert resolved_frontend_version is not None
- frontend_plugin_root = self.frontend_root / 'plugins' / plugin_id
+ assert resolved_frontend_framework is not None
+ frontend_plugin_root = frontend_root / 'plugins' / plugin_id
target_dirs.append(str(frontend_plugin_root))
if effective_frontend_test:
- target_dirs.append(str(self.frontend_root / 'tests' / 'plugins' / plugin_id))
+ target_dirs.append(str(frontend_root / 'tests' / 'plugins' / plugin_id))
files.extend(
self._build_frontend_files(
plugin_id,
frontend_plugin_root,
options,
- frontend_version=resolved_frontend_version,
+ frontend_framework=resolved_frontend_framework,
)
)
@@ -122,7 +135,7 @@ class PluginScaffoldBuilder:
test=effective_backend_test or effective_frontend_test,
backend_test=effective_backend_test,
frontend_test=effective_frontend_test,
- frontend_version=resolved_frontend_version,
+ frontend_framework=resolved_frontend_framework,
target_dirs=target_dirs,
files=files,
conflicts=conflicts,
@@ -131,6 +144,18 @@ class PluginScaffoldBuilder:
build_conflict_payload = staticmethod(PluginScaffoldPayloadBuilder.build_conflict_payload)
build_success_payload = staticmethod(PluginScaffoldPayloadBuilder.build_success_payload)
+ def _resolve_frontend_framework(self, frontend_root: Path, requested_framework: str) -> str:
+ """
+ 优先使用命令显式框架,其次沿用宿主运行环境的框架选择。
+
+ :param frontend_root: 已选中的前端工程目录
+ :param requested_framework: 命令指定的框架标识
+ :return: 已解析的前端框架标识
+ """
+ if (requested_framework or 'auto').strip().lower() == 'auto' and self.frontend_framework_resolver is not None:
+ return self.frontend_framework_resolver(frontend_root)
+ return PluginFrontendFrameworkResolver.resolve(frontend_root, requested_framework)
+
@classmethod
def _validate_plugin_id(cls, plugin_id: str) -> None:
"""
@@ -251,7 +276,7 @@ class PluginScaffoldBuilder:
plugin_root: Path,
options: PluginScaffoldOptions,
*,
- frontend_version: FrontendVersion,
+ frontend_framework: FrontendFramework,
) -> list[tuple[Path, str]]:
"""
构建前端插件模板文件。
@@ -259,7 +284,7 @@ class PluginScaffoldBuilder:
:param plugin_id: 插件ID
:param plugin_root: 前端插件根目录
:param options: 插件模板生成选项
- :param frontend_version: 已解析的前端 Vue 版本
+ :param frontend_framework: 已解析的前端框架标识
:return: 文件路径和内容列表
"""
files = [
@@ -271,20 +296,20 @@ class PluginScaffoldBuilder:
),
(
plugin_root / 'views' / 'index.vue',
- PluginFrontendScaffoldTemplateBuilder.build_crud_view(plugin_id, frontend_version)
+ PluginFrontendScaffoldTemplateBuilder.build_crud_view(plugin_id, frontend_framework)
if options.crud
- else PluginFrontendScaffoldTemplateBuilder.build_view(plugin_id, frontend_version),
+ else PluginFrontendScaffoldTemplateBuilder.build_view(plugin_id, frontend_framework),
),
(
plugin_root / 'README.md',
- PluginFrontendScaffoldTemplateBuilder.build_readme(plugin_id, frontend_version),
+ PluginFrontendScaffoldTemplateBuilder.build_readme(plugin_id, frontend_framework),
),
]
if options.test:
files.append(
(
- self.frontend_root / 'tests' / 'plugins' / plugin_id / 'pluginView.test.js',
- PluginFrontendScaffoldTemplateBuilder.build_test(plugin_id, frontend_version),
+ plugin_root.parents[1] / 'tests' / 'plugins' / plugin_id / 'pluginView.test.js',
+ PluginFrontendScaffoldTemplateBuilder.build_test(plugin_id, frontend_framework),
)
)
diff --git a/ruoyi-fastapi-backend/cli/runtime/plugin/scaffold/frontend.py b/ruoyi-fastapi-backend/cli/runtime/plugin/scaffold/frontend.py
index 7b418fc..cbf643e 100644
--- a/ruoyi-fastapi-backend/cli/runtime/plugin/scaffold/frontend.py
+++ b/ruoyi-fastapi-backend/cli/runtime/plugin/scaffold/frontend.py
@@ -1,94 +1,8 @@
-import json
-import re
-from pathlib import Path
-from typing import Literal, cast
+from plugins.core.frontend import FrontendFramework, PluginFrontendFrameworkResolver # noqa: F401
from .frontend_vue2 import PluginVue2FrontendScaffoldTemplateBuilder
from .naming import PluginScaffoldNaming
-FrontendVersion = Literal['vue2', 'vue3']
-
-
-class PluginFrontendVersionResolver:
- """
- 前端 Vue 版本解析器。
- """
-
- AUTO = 'auto'
- DEFAULT_VERSION: FrontendVersion = 'vue3'
- SUPPORTED_VALUES = (AUTO, 'vue2', 'vue3')
-
- @classmethod
- def resolve(cls, frontend_root: Path, requested_version: str = AUTO) -> FrontendVersion:
- """
- 解析脚手架应使用的 Vue 版本。
-
- 显式版本优先;auto 模式读取目标前端 package.json。临时目录等没有
- package.json 的场景保持历史行为,默认生成 Vue 3 模板。
-
- :param frontend_root: 前端项目根目录
- :param requested_version: auto、vue2 或 vue3
- :return: 解析后的 Vue 版本
- """
- normalized_version = (requested_version or cls.AUTO).strip().lower()
- if normalized_version not in cls.SUPPORTED_VALUES:
- supported = '、'.join(cls.SUPPORTED_VALUES)
- raise ValueError(f'frontend_version 仅支持 {supported},当前值:{requested_version}')
- if normalized_version != cls.AUTO:
- return cast('FrontendVersion', normalized_version)
-
- package_json_path = frontend_root / 'package.json'
- if not package_json_path.is_file():
- return cls.DEFAULT_VERSION
-
- try:
- package_payload = json.loads(package_json_path.read_text(encoding='utf-8'))
- except (OSError, json.JSONDecodeError) as exc:
- raise ValueError(f'读取前端 package.json 失败:{package_json_path}({exc})') from exc
- if not isinstance(package_payload, dict):
- raise ValueError(f'前端 package.json 顶层必须是对象:{package_json_path}')
-
- dependencies = cls._collect_dependencies(package_payload)
- vue_version = cls._resolve_vue_dependency(dependencies.get('vue'))
- if vue_version is not None:
- return vue_version
- if 'element-ui' in dependencies:
- return 'vue2'
- if 'element-plus' in dependencies:
- return 'vue3'
-
- raise ValueError(f'无法从 {package_json_path} 识别 Vue 版本,请使用 --frontend-version vue2 或 vue3 显式指定')
-
- @staticmethod
- def _collect_dependencies(package_payload: dict[str, object]) -> dict[str, object]:
- """
- 合并 dependencies 和 devDependencies。
-
- :param package_payload: package.json 负载
- :return: 依赖映射
- """
- dependencies: dict[str, object] = {}
- for key in ('devDependencies', 'dependencies'):
- section = package_payload.get(key)
- if isinstance(section, dict):
- dependencies.update(section)
- return dependencies
-
- @staticmethod
- def _resolve_vue_dependency(version_spec: object) -> FrontendVersion | None:
- """
- 从 npm Vue 版本约束中提取主版本。
-
- :param version_spec: Vue npm 版本约束
- :return: Vue 版本,无法识别时返回 None
- """
- if not isinstance(version_spec, str):
- return None
- match = re.search(r'(? str:
- return cls._resolve_builder(frontend_version).build_view(plugin_id)
+ def build_view(cls, plugin_id: str, frontend_framework: FrontendFramework = 'vue3') -> str:
+ return cls._resolve_builder(frontend_framework).build_view(plugin_id)
@classmethod
- def build_crud_view(cls, plugin_id: str, frontend_version: FrontendVersion = 'vue3') -> str:
- return cls._resolve_builder(frontend_version).build_crud_view(plugin_id)
+ def build_crud_view(cls, plugin_id: str, frontend_framework: FrontendFramework = 'vue3') -> str:
+ return cls._resolve_builder(frontend_framework).build_crud_view(plugin_id)
@classmethod
- def build_readme(cls, plugin_id: str, frontend_version: FrontendVersion = 'vue3') -> str:
- return cls._resolve_builder(frontend_version).build_readme(plugin_id)
+ def build_readme(cls, plugin_id: str, frontend_framework: FrontendFramework = 'vue3') -> str:
+ return cls._resolve_builder(frontend_framework).build_readme(plugin_id)
@classmethod
- def build_test(cls, plugin_id: str, frontend_version: FrontendVersion = 'vue3') -> str:
- return cls._resolve_builder(frontend_version).build_test(plugin_id)
+ def build_test(cls, plugin_id: str, frontend_framework: FrontendFramework = 'vue3') -> str:
+ return cls._resolve_builder(frontend_framework).build_test(plugin_id)
@staticmethod
def _resolve_builder(
- frontend_version: FrontendVersion,
+ frontend_framework: FrontendFramework,
) -> type[PluginVue2FrontendScaffoldTemplateBuilder] | type[PluginVue3FrontendScaffoldTemplateBuilder]:
- if frontend_version == 'vue2':
+ if frontend_framework == 'vue2':
return PluginVue2FrontendScaffoldTemplateBuilder
- if frontend_version == 'vue3':
+ if frontend_framework == 'vue3':
return PluginVue3FrontendScaffoldTemplateBuilder
- raise ValueError(f'不支持的 Vue 版本:{frontend_version}')
+ raise ValueError(f'暂不支持前端框架 {frontend_framework} 的源码模板,目前仅支持 vue2、vue3')
diff --git a/ruoyi-fastapi-backend/cli/runtime/plugin/scaffold/payload.py b/ruoyi-fastapi-backend/cli/runtime/plugin/scaffold/payload.py
index 2f8ac25..bfefb30 100644
--- a/ruoyi-fastapi-backend/cli/runtime/plugin/scaffold/payload.py
+++ b/ruoyi-fastapi-backend/cli/runtime/plugin/scaffold/payload.py
@@ -23,7 +23,7 @@ class PluginScaffoldPlanPayload:
test: bool
backend_test: bool
frontend_test: bool
- frontend_version: str | None
+ frontend_framework: str | None
target_dirs: list[str]
files: list[tuple[Path, str]]
conflicts: list[str]
@@ -46,7 +46,7 @@ class PluginScaffoldPlanPayload:
'test': self.test,
'backendTest': self.backend_test,
'frontendTest': self.frontend_test,
- 'frontendVersion': self.frontend_version,
+ 'frontendFramework': self.frontend_framework,
'targetDirs': [format_cli_path(path) for path in self.target_dirs],
'files': [{'path': path.as_posix(), 'content': content} for path, content in self.files],
'conflicts': self.conflicts,
diff --git a/ruoyi-fastapi-backend/cli/runtime/plugin/scaffold/v2.py b/ruoyi-fastapi-backend/cli/runtime/plugin/scaffold/v2.py
index 8b99fb7..c4587cd 100644
--- a/ruoyi-fastapi-backend/cli/runtime/plugin/scaffold/v2.py
+++ b/ruoyi-fastapi-backend/cli/runtime/plugin/scaffold/v2.py
@@ -1,4 +1,5 @@
import json
+from collections.abc import Callable
from importlib.resources import files
from pathlib import Path
from typing import Any
@@ -15,19 +16,26 @@ class PluginV2ScaffoldBuilder:
独立 v2 插件源码工程模板构建器。
"""
- def __init__(self, backend_root: Path, frontend_root: Path) -> None:
+ def __init__(
+ self,
+ backend_root: Path,
+ frontend_root: Path,
+ frontend_framework_resolver: Callable[[Path], str] | None = None,
+ ) -> None:
"""
初始化独立 v2 工程模板构建器。
:param backend_root: 宿主后端项目根目录
:param frontend_root: 宿主前端项目根目录,用于读取桥接 SDK
+ :param frontend_framework_resolver: 宿主框架解析函数,仅 bundle 复制 SDK 时调用
:return: None
"""
self.backend_root = backend_root
self.frontend_root = frontend_root
+ self.frontend_framework_resolver = frontend_framework_resolver
def build_plan(
- self, plugin_id: str, *, template: str, backend: bool, frontend: bool, test: bool, frontend_version: str
+ self, plugin_id: str, *, template: str, backend: bool, frontend: bool, test: bool, frontend_framework: str
) -> dict[str, Any]:
"""
生成独立源码工程的文件计划,不安装、启用或导入插件。
@@ -37,7 +45,7 @@ class PluginV2ScaffoldBuilder:
:param backend: 是否创建后端工程
:param frontend: 是否创建前端工程
:param test: 是否生成本地合约测试
- :param frontend_version: 旧版前端选项,v2 模板仅接受 auto
+ :param frontend_framework: 宿主前端框架选项,v2 模板仅接受 auto
:return: 包含生成文件、目标目录与冲突信息的计划
"""
native = template.startswith('rust-')
@@ -46,8 +54,8 @@ class PluginV2ScaffoldBuilder:
raise ValueError('v2 ASGI 模板需要后端,不能使用 --frontend-only')
if bundle and not frontend:
raise ValueError('bundle 模板不能使用 --backend-only;仅 API 请使用 python-asgi 或 rust-asgi')
- if frontend_version != 'auto':
- raise ValueError('v2 模板不使用 --frontend-version;独立 bundle 与宿主 Vue 版本无关')
+ if frontend_framework != 'auto':
+ raise ValueError('v2 模板的 --frontend-framework 仅接受 auto;独立 bundle 与宿主前端框架无关')
root = self.backend_root / 'plugin-projects' / plugin_id
replacements = {
@@ -86,9 +94,16 @@ class PluginV2ScaffoldBuilder:
if test:
entries.append((root / 'tests' / 'test_plugin.py', self._render('test_plugin.py', replacements)))
if bundle:
+ host_framework = (
+ self.frontend_framework_resolver(self.frontend_root)
+ if self.frontend_framework_resolver is not None
+ else 'auto'
+ )
entries.extend(
(root / 'web' / 'vendor' / sdk_name, content)
- for sdk_name, content in PluginBridgeSdk(self.frontend_root).build_vendor_files().items()
+ for sdk_name, content in PluginBridgeSdk(self.frontend_root, frontend_framework=host_framework)
+ .build_vendor_files()
+ .items()
)
entries.extend(
(root / 'web' / name, self._render(f'web/{name.removeprefix("src/")}', replacements))
@@ -115,7 +130,7 @@ class PluginV2ScaffoldBuilder:
test=test,
backend_test=test,
frontend_test=False,
- frontend_version=None,
+ frontend_framework=None,
target_dirs=[str(root)],
files=entries,
conflicts=[str(root)] if root.exists() else [],
diff --git a/ruoyi-fastapi-backend/cli/runtime/plugin/sdk.py b/ruoyi-fastapi-backend/cli/runtime/plugin/sdk.py
index 0ee2f43..7a73a0c 100644
--- a/ruoyi-fastapi-backend/cli/runtime/plugin/sdk.py
+++ b/ruoyi-fastapi-backend/cli/runtime/plugin/sdk.py
@@ -11,11 +11,19 @@ from uuid import uuid4
import yaml
+from plugins.core.frontend import (
+ PluginFrontendFrameworkResolver,
+ resolve_frontend_framework_request,
+ select_frontend_root,
+ validate_frontend_framework,
+)
+
SDK_FILES = ('pluginBridge.js', 'pluginBridge.d.ts')
SDK_METADATA = 'pluginBridge.sdk.json'
VENDOR_FILES = (*SDK_FILES, SDK_METADATA)
HASH_ALGORITHM = 'sha256-utf8-lf'
-SDK_SOURCE = {'project': 'RuoYi-Vue3-FastAPI', 'directory': 'ruoyi-fastapi-frontend/src/utils'}
+SDK_PROJECT = 'RuoYi-FastAPI'
+SDK_SOURCE = {'project': SDK_PROJECT, 'directory': 'ruoyi-fastapi-frontend/vue3/web/src/utils'}
METADATA_FIELDS = {'schemaVersion', 'sdkVersion', 'bridgeVersion', 'capabilities'}
SCHEMA_VERSION = 1
MANIFEST_VERSION = 2
@@ -24,14 +32,16 @@ MANIFEST_VERSION = 2
class PluginBridgeSdk:
"""离线检查和更新独立 bundle 工程中的桥接 SDK 副本。"""
- def __init__(self, frontend_root: Path) -> None:
+ def __init__(self, frontend_root: Path, frontend_framework: str = 'auto') -> None:
"""
保存用于读取当前宿主 SDK 的前端目录,不初始化宿主运行环境。
- :param frontend_root: 宿主前端工程根目录
+ :param frontend_root: 宿主前端工程根目录或包含框架分类的聚合目录
+ :param frontend_framework: SDK 来源宿主的框架标识,auto 表示自动识别
:return: None
"""
- self.frontend_root = Path(frontend_root)
+ self.frontend_root = select_frontend_root(frontend_root, frontend_framework)
+ self.frontend_framework = resolve_frontend_framework_request(self.frontend_root, frontend_framework)
def build_vendor_files(self) -> dict[str, str]:
"""
@@ -48,9 +58,14 @@ class PluginBridgeSdk:
contents[name] = self._text(path.read_bytes())
metadata = self._metadata(contents[SDK_METADATA], vendor=False)
self._validate_exports(contents, metadata)
+ self._safe_path(self.frontend_root / 'package.json')
+ frontend_framework = PluginFrontendFrameworkResolver.resolve(self.frontend_root, self.frontend_framework)
metadata.update(
hashAlgorithm=HASH_ALGORITHM,
- source=dict(SDK_SOURCE),
+ source={
+ 'project': SDK_PROJECT,
+ 'directory': f'ruoyi-fastapi-frontend/{frontend_framework}/web/src/utils',
+ },
files={name: self._hash(contents[name]) for name in SDK_FILES},
)
contents[SDK_METADATA] = json.dumps(metadata, ensure_ascii=False, indent=2) + '\n'
@@ -234,8 +249,9 @@ class PluginBridgeSdk:
raise ValueError('桥接 SDK 的 capabilities 无效')
if vendor:
hashes = metadata['files']
- if metadata['hashAlgorithm'] != HASH_ALGORITHM or metadata['source'] != SDK_SOURCE:
- raise ValueError('桥接 SDK 的摘要算法或来源记录无效')
+ if metadata['hashAlgorithm'] != HASH_ALGORITHM:
+ raise ValueError('桥接 SDK 的摘要算法无效')
+ PluginBridgeSdk._validate_source(metadata['source'])
if (
not isinstance(hashes, dict)
or set(hashes) != set(SDK_FILES)
@@ -246,6 +262,26 @@ class PluginBridgeSdk:
raise ValueError('桥接 SDK 文件摘要记录无效')
return metadata
+ @staticmethod
+ def _validate_source(source: Any) -> None:
+ """
+ 校验来源记录使用当前项目名和规范的框架工程路径。
+
+ :param source: SDK 副本中的来源记录
+ :return: None
+ """
+ if not isinstance(source, dict) or set(source) != {'project', 'directory'} or source['project'] != SDK_PROJECT:
+ raise ValueError('桥接 SDK 的来源记录无效')
+ directory = source['directory']
+ match = (
+ re.fullmatch(r'ruoyi-fastapi-frontend/([^/]+)/web/src/utils', directory)
+ if isinstance(directory, str)
+ else None
+ )
+ if match is None:
+ raise ValueError('桥接 SDK 的来源目录无效')
+ validate_frontend_framework(match.group(1))
+
@staticmethod
def _wire_versions(content: str) -> list[str]:
"""
diff --git a/ruoyi-fastapi-backend/cli/runtime/plugin/service.py b/ruoyi-fastapi-backend/cli/runtime/plugin/service.py
index 3e2923f..09a4ffb 100644
--- a/ruoyi-fastapi-backend/cli/runtime/plugin/service.py
+++ b/ruoyi-fastapi-backend/cli/runtime/plugin/service.py
@@ -253,6 +253,12 @@ class CliPluginRuntimeService:
lockfile_template = PluginDependencyLockfileTemplateBuilder.build(
discovered_plugin.manifest,
offline_dir=offline_dir or None,
+ frontend_root=runtime_environment.get_frontend_dir(),
+ frontend_framework=(
+ runtime_environment.get_frontend_framework()
+ if discovered_plugin.manifest.dependencies.frontend
+ else 'auto'
+ ),
)
if resolved_output_path.exists() and not overwrite and not dry_run:
return PluginDependencyLockPayloadBuilder.build_exists_payload(plugin_id, resolved_output_path)
@@ -463,7 +469,7 @@ class CliPluginRuntimeService:
job: bool = True,
config: bool = True,
test: bool = True,
- frontend_version: str = 'auto',
+ frontend_framework: str = 'auto',
dry_run: bool = False,
) -> dict[str, object]:
"""
@@ -478,7 +484,7 @@ class CliPluginRuntimeService:
:param job: 是否创建定时任务示例
:param config: 是否创建配置项示例
:param test: 是否创建测试样例
- :param frontend_version: 前端 Vue 版本,支持 auto、vue2、vue3
+ :param frontend_framework: 前端框架标识,auto 表示自动识别;源码模板仅支持 vue2、vue3
:param dry_run: 是否仅预演
:return: 插件创建结果负载
"""
@@ -487,6 +493,7 @@ class CliPluginRuntimeService:
scaffold = PluginScaffoldBuilder(
Path(runtime_environment.get_backend_dir()),
frontend_root=Path(runtime_environment.get_frontend_dir()),
+ frontend_framework_resolver=runtime_environment.get_frontend_framework,
)
scaffold_plan = scaffold.build_plan(
plugin_id,
@@ -498,7 +505,7 @@ class CliPluginRuntimeService:
job=job,
config=config,
test=test,
- frontend_version=frontend_version,
+ frontend_framework=frontend_framework,
)
if scaffold_plan['conflicts']:
return PluginScaffoldBuilder.build_conflict_payload(
diff --git a/ruoyi-fastapi-backend/cli/runtime/plugin/support.py b/ruoyi-fastapi-backend/cli/runtime/plugin/support.py
index 67c1b8e..21d435c 100644
--- a/ruoyi-fastapi-backend/cli/runtime/plugin/support.py
+++ b/ruoyi-fastapi-backend/cli/runtime/plugin/support.py
@@ -8,6 +8,7 @@ from typing import Any
import yaml
from cli.exit_codes import RUNTIME_ERROR, SUCCESS
+from plugins.core.frontend import PluginFrontendFrameworkResolver
from plugins.core.utils import validate_plugin_id_value
from utils.time_util import TimezoneUtil
@@ -223,6 +224,7 @@ class PluginDependencyLockfileTemplate:
:param npm_dev: npmDev 依赖锁定项
:param artifact_count: 已从离线制品反填的依赖项数量
:param warnings: 生成过程告警
+ :param frontend_framework: 锁定的宿主前端框架,无前端依赖分类时为 None
"""
plugin_id: str
@@ -233,6 +235,7 @@ class PluginDependencyLockfileTemplate:
npm_dev: list[dict[str, object]]
artifact_count: int = 0
warnings: list[str] = field(default_factory=list)
+ frontend_framework: str | None = None
@property
def entry_count(self) -> int:
@@ -256,6 +259,7 @@ class PluginDependencyLockfileTemplate:
'python': self.python,
'npm': self.npm,
'npmDev': self.npm_dev,
+ **({'frontendFramework': self.frontend_framework} if self.frontend_framework else {}),
}
def to_yaml(self) -> str:
@@ -275,16 +279,36 @@ class PluginDependencyLockfileTemplateBuilder:
"""
@classmethod
- def build(cls, manifest: Any, *, offline_dir: Path | str | None = None) -> PluginDependencyLockfileTemplate:
+ def build(
+ cls,
+ manifest: Any,
+ *,
+ offline_dir: Path | str | None = None,
+ frontend_root: Path | str | None = None,
+ frontend_framework: str = 'auto',
+ ) -> PluginDependencyLockfileTemplate:
"""
根据插件 manifest 构建锁文件模板。
:param manifest: 插件 manifest
:param offline_dir: 离线制品根目录
+ :param frontend_root: 宿主前端工程根目录,未指定时使用插件运行时目录
+ :param frontend_framework: 宿主显式框架选择,auto 时从运行时环境与工程目录解析
:return: 锁文件模板
"""
generated_at = TimezoneUtil.format_rfc3339(TimezoneUtil.utc_now())
dependencies = manifest.dependencies
+ resolved_frontend_framework = None
+ if dependencies.frontend:
+ from plugins.core.environment import PLUGIN_RUNTIME_ENVIRONMENT # noqa: PLC0415
+
+ frontend_root = frontend_root or PLUGIN_RUNTIME_ENVIRONMENT.get_frontend_dir()
+ resolved_frontend_framework = (
+ PLUGIN_RUNTIME_ENVIRONMENT.get_frontend_framework(frontend_root)
+ if frontend_framework == 'auto'
+ else PluginFrontendFrameworkResolver.resolve(Path(frontend_root), frontend_framework)
+ )
+ frontend_dependencies = dependencies.resolve_frontend(resolved_frontend_framework or 'vue3')
artifact_resolver = PluginDependencyOfflineArtifactResolver(offline_dir)
warnings: list[str] = []
artifact_count = 0
@@ -297,7 +321,7 @@ class PluginDependencyLockfileTemplateBuilder:
artifact_count += 1 if entry.get('resolvedVersion') else 0
if artifact_warning:
warnings.append(artifact_warning)
- for requirement in dependencies.npm:
+ for requirement in frontend_dependencies.npm:
entry, artifact_warning = cls._build_npm_entry(
requirement,
kind='npm',
@@ -307,7 +331,7 @@ class PluginDependencyLockfileTemplateBuilder:
artifact_count += 1 if entry.get('resolvedVersion') else 0
if artifact_warning:
warnings.append(artifact_warning)
- for requirement in dependencies.npm_dev:
+ for requirement in frontend_dependencies.npm_dev:
entry, artifact_warning = cls._build_npm_entry(
requirement,
kind='npmDev',
@@ -326,6 +350,7 @@ class PluginDependencyLockfileTemplateBuilder:
npm_dev=npm_dev_entries,
artifact_count=artifact_count,
warnings=warnings,
+ frontend_framework=resolved_frontend_framework,
)
@staticmethod
diff --git a/ruoyi-fastapi-backend/config/env.py b/ruoyi-fastapi-backend/config/env.py
index e7e5391..3aa490e 100644
--- a/ruoyi-fastapi-backend/config/env.py
+++ b/ruoyi-fastapi-backend/config/env.py
@@ -22,7 +22,7 @@ class AppSettings(BaseSettings):
"""
app_env: str = 'dev'
- app_name: str = 'RuoYi-FasAPI'
+ app_name: str = 'RuoYi-FastAPI'
app_root_path: str = '/dev-api'
app_host: str = '0.0.0.0'
app_port: int = 9099
diff --git a/ruoyi-fastapi-backend/docs/cli_usage.md b/ruoyi-fastapi-backend/docs/cli_usage.md
index c8bcc96..8cc4136 100644
--- a/ruoyi-fastapi-backend/docs/cli_usage.md
+++ b/ruoyi-fastapi-backend/docs/cli_usage.md
@@ -256,7 +256,7 @@ ruoyi dev test tests --keyword sanitize --maxfail=1 -q
```bash
ruoyi plugin create demo --env=dev --template=full-stack --dry-run
-ruoyi plugin create demo --env=dev --template=crud-page --frontend-version=vue2 --dry-run
+ruoyi plugin create demo --env=dev --template=crud-page --frontend-framework=vue2 --dry-run
ruoyi plugin check demo --env=dev
ruoyi plugin check-deps demo --env=dev
ruoyi plugin allowlist-example --env=dev --dry-run
@@ -271,9 +271,32 @@ ruoyi plugin test demo --env=dev
`allowlist-example` 会按需生成插件依赖允许列表示例,默认输出路径为 `config/plugin_dependency_allowlist.yaml`。仓库不再默认携带 `.example` 文件;需要模板时可先 `--dry-run` 查看,也可指定 `--output-path` 写入并按团队实际批准范围调整。
-`plugin create` 默认使用 `--frontend-version=auto`,从目标前端 `package.json` 的 `vue` 依赖自动识别 Vue 2/3;只有无法识别或需要覆盖时才显式传入 `vue2` 或 `vue3`。插件清单通常保持跨版本一致;确实存在 Vue 绑定库等版本专属依赖时,允许各项目的 `plugin.yaml` 分别声明,并使用按前端版本自动选择的测试覆盖两套清单。
+`plugin create` 的 `--frontend-framework` 当前固定可选 `auto`、`vue2`、`vue3`,默认值为 `auto`。显式选择 `vue2` 或 `vue3` 优先确定框架;`auto` 先采用标准目录 `ruoyi-fastapi-frontend//{web,mobile}` 中的框架标识,独立工程再通过 `package.json` 依赖识别框架。标准目录标识不会被底层依赖覆盖;无法识别的独立工程会报错。未指定目标时,目录选择默认定位 `vue3/web`。显式传入 `--frontend-framework=vue2` 会选择 `vue2/web` 并生成 Vue2 模板,传入 `vue3` 则选择 Vue3 工程,脚手架结果使用 `frontendFramework` 记录框架标识。
-`lock-deps` 会根据 `plugin.yaml` 中声明的 Python/npm/npmDev 外部依赖生成 `plugin.lock.yaml` 模板。默认模式不联网解析版本,也不生成 hash/integrity;如传入 `--offline-dir`,命令会从已有本地 wheel/tgz 反填 `resolvedVersion`、Python `hashes` 和 npm/npmDev `integrity`。该命令仍不会下载、安装或访问 registry,无法反填的项需要由人工审核或 CI 发布流水线补齐后,才能用于 `locked` 或 `offline` 策略。
+当前 v1 源码前端模板仅提供 Vue2、Vue3 实现,`auto` 解析到其他框架时,生成源码页面会明确报错,提示模板尚未支持。v2 独立模板的 `--frontend-framework` 只允许默认值 `auto`。CLI 选项的固定取值不改变底层通用框架结构。
+
+插件依赖检查、安装和测试使用当前选定的 Web 宿主。可通过 `RUOYI_PLUGIN_FRONTEND_FRAMEWORK=vue2` 切换默认框架;未设置该变量时也可使用 `RUOYI_FRONTEND_FRAMEWORK`。`RUOYI_PLUGIN_FRONTEND_ROOT`/`RUOYI_FRONTEND_ROOT` 可直接指定 Web 工程,或指定前端聚合目录后按框架定位,旧版平铺目录仍受支持。此前按版本命名的选择变量和命令选项不再提供别名,已有脚本需同步更新。
+
+各框架工程共用同一个后端插件清单。前端依赖分别完整声明到 `dependencies.frontend.` 的 `npm/npmDev` 中,分类键接受 `vue2`、`vue3`、`react` 等安全的小写框架标识;检查和安装只选择当前宿主的分类,不跨分类合并。未声明目标分类时,该宿主没有 npm 依赖;旧顶层 `dependencies.npm/npmDev` 会被清单校验拒绝,需迁移到对应分类。各工程分别安装前端依赖,后端插件只需安装、启用一次。依赖分类可扩展不代表对应框架的工程或源码模板已实现。
+
+```powershell
+# Windows PowerShell:检查 Vue2 Web 的 AI 插件依赖
+$env:RUOYI_PLUGIN_FRONTEND_FRAMEWORK = 'vue2'
+ruoyi plugin check-deps ai --env=dev
+ruoyi plugin install-deps ai --env=dev --dry-run
+```
+
+```bash
+# macOS / Linux
+RUOYI_PLUGIN_FRONTEND_FRAMEWORK=vue2 ruoyi plugin check-deps ai --env=dev
+RUOYI_PLUGIN_FRONTEND_FRAMEWORK=vue2 ruoyi plugin install-deps ai --env=dev --dry-run
+```
+
+`lock-deps` 会根据 `plugin.yaml` 的 Python 依赖和当前框架分类的前端依赖生成 `plugin.lock.yaml` 模板。锁文件产物保持顶层 `python/npm/npmDev` 列表,与插件清单的分类结构不同。默认模式不联网解析版本,也不生成 hash/integrity;如传入 `--offline-dir`,命令会从已有本地 wheel/tgz 反填 `resolvedVersion`、Python `hashes` 和 npm/npmDev `integrity`。该命令仍不会下载、安装或访问 registry,无法反填的项需要由人工审核或 CI 发布流水线补齐后,才能用于 `locked` 或 `offline` 策略。
+
+清单声明 `dependencies.frontend` 时,锁模板记录当前宿主的 `frontendFramework`,只锁定该框架分类的前端依赖。请为各框架分别保存锁文件,例如切换到 Vue2、Vue3 后,使用 `--output-path plugins/ai/plugin.vue2.lock.yaml`、`--output-path plugins/ai/plugin.vue3.lock.yaml` 保存,再在 `install-deps` 中通过 `--lockfile` 传入对应文件;锁文件框架与宿主不一致时会被阻断。
+
+包含旧框架字段 `frontendVersion` 的锁文件会被明确拒绝,不做静默迁移,应按目标框架重新生成并审核。既没有 `frontendFramework` 也没有旧框架字段的旧锁文件,保持原有依赖项校验方式;这不代表插件清单仍接受旧顶层 npm 字段。清单中的 `compatibility.frontendVersion` 仍是前端工程的语义版本约束,保留原名,与框架标识 `frontendFramework` 不同。
`install-deps` 是真实 Python/npm 依赖安装的唯一显式入口。文本 TTY 下可省略 `--yes`,CLI 会先输出依赖安装计划和策略判定,再询问是否执行;非 TTY、JSON 输出或 CI 场景应传 `--yes`,否则会由策略返回确认阻断。
@@ -307,7 +330,7 @@ ruoyi plugin sdk update plugin-projects/report_center --dry-run --output=json
ruoyi plugin sdk update plugin-projects/report_center
```
-这些命令不接受 `--env`,也不读取宿主数据库配置或执行插件代码。来源默认为相邻前端目录,可用 `--frontend-root PATH` 或 `RUOYI_PLUGIN_FRONTEND_ROOT` 指定。`check` 仅在版本、来源记录及文件摘要与选定宿主一致(`current`)时返回 0;过期副本、本地修改或缺少记录返回非零,适合纳入构建检查。`protocolCompatible` 只比较桥协议,不表示业务功能已经验收。
+这些命令不接受 `--env`,也不读取宿主数据库配置或执行插件代码。SDK 命令的 `--frontend-framework` 当前固定可选 `auto`、`vue2`、`vue3`,默认值为 `auto`。来源默认是 `ruoyi-fastapi-frontend/vue3/web`;可用 `--frontend-framework=vue2` 选择 Vue2 Web,也可用 `--frontend-root PATH` 或 `RUOYI_PLUGIN_FRONTEND_ROOT` 指定工程。`auto` 跟随框架环境变量和显式根目录选择,所选 `ruoyi-fastapi-frontend//web` 工程必须已提供对应 bridge SDK 文件。`check` 仅在版本、来源记录及文件摘要与选定宿主一致(`current`)时返回 0;过期副本、本地修改或缺少记录返回非零,适合纳入构建检查。`protocolCompatible` 只比较桥协议,不表示业务功能已经验收。
`update` 是显式写入命令,先备份再替换三个 SDK 文件。默认拒绝覆盖本地 SDK 修改或旧工程的未跟踪副本;需要纳管时先审阅 `update --force --dry-run`,再执行 `update --force`。备份位于工程 `.plugin-sdk-backups/`,业务代码保持原样。更新后应重建 bundle 并执行兼容测试;不会下载依赖、签名发布或改变运行中的插件。完整状态与摘要规则见开发手册第 16.1 节。
diff --git a/ruoyi-fastapi-backend/docs/plugin_development.md b/ruoyi-fastapi-backend/docs/plugin_development.md
index d64722d..d53e2ee 100644
--- a/ruoyi-fastapi-backend/docs/plugin_development.md
+++ b/ruoyi-fastapi-backend/docs/plugin_development.md
@@ -23,16 +23,33 @@
```text
ruoyi-fastapi-backend/plugins//
-ruoyi-fastapi-frontend/plugins//
+ruoyi-fastapi-frontend//web/plugins//
```
+`` 是前端框架标识。当前仓库提供 `vue2`、`vue3` 工程,各框架的源码页面分别维护,共用一份后端代码与 `plugin.yaml`。移动端位于 `ruoyi-fastapi-frontend//mobile`,不作为 Web 插件的宿主构建目录。目录和依赖分类可以扩展到 `react` 等框架标识,但当前没有 React 工程。
+
开发示例统一位于后端 `plugins/examples/`,按实现语言分类:`python/` 保存 Python 示例,`rust/` 保存 Rust 原生插件工程。
该目录不参与插件发现和启动代际计算。使用示例时,按各自 README 构建后部署到 `plugins//`,或通过签名制品发布流程安装。
-运行时不会把前后端仓库名写死在各操作入口中。插件系统优先使用显式传入的目录,其次读取
-`RUOYI_PLUGIN_BACKEND_ROOT`/`RUOYI_BACKEND_ROOT` 和
-`RUOYI_PLUGIN_FRONTEND_ROOT`/`RUOYI_FRONTEND_ROOT`,再尝试从后端同级目录中识别前端工程;
-最后才按后端目录名把 `backend` 推断为 `frontend`。非默认目录名的项目,应优先配置上述环境变量或在运行时注入目录。
+插件系统优先使用显式传入的目录,其次读取 `RUOYI_PLUGIN_BACKEND_ROOT`/`RUOYI_BACKEND_ROOT` 和 `RUOYI_PLUGIN_FRONTEND_ROOT`/`RUOYI_FRONTEND_ROOT`。默认框架为 `vue3`,对应 `ruoyi-fastapi-frontend/vue3/web`;设置 `RUOYI_PLUGIN_FRONTEND_FRAMEWORK=vue2` 可选择 `vue2/web`,未设置该变量时也可使用 `RUOYI_FRONTEND_FRAMEWORK`。前端根目录变量可以指向具体 Web 工程,也可以指向 `ruoyi-fastapi-frontend` 聚合目录,由框架标识继续定位。旧版包含 `package.json` 的平铺前端目录仍受支持。非默认目录名的项目应优先配置根目录变量。
+
+脚手架和 SDK 命令的 `--frontend-framework` 当前固定可选 `auto`、`vue2`、`vue3`,默认值为 `auto`。框架身份按以下顺序确定:显式选择 `vue2` 或 `vue3` 优先;`auto` 模式先读取标准目录 `ruoyi-fastapi-frontend//{web,mobile}` 中的框架标识;独立工程再根据 `package.json` 依赖识别框架。标准目录标识不会被底层依赖覆盖。独立工程无法识别时会报错,不会退回 Vue3;未指定目标的目录选择仍默认定位 `vue3/web`。
+
+CLI 选项的固定取值不改变底层通用框架目录与依赖分类结构。此前按版本命名的选择变量和命令选项不再提供别名,已有脚本需同步更新。
+
+以下命令切换当前终端的宿主选择,后续依赖检查、安装及插件测试都针对该 Web 工程;不改变 HTTP 业务接口或插件生命周期状态。
+
+```powershell
+# Windows PowerShell
+$env:RUOYI_PLUGIN_FRONTEND_FRAMEWORK = 'vue2'
+```
+
+```bash
+# macOS / Linux
+export RUOYI_PLUGIN_FRONTEND_FRAMEWORK=vue2
+```
+
+切回 Vue3 时将变量值设为 `vue3`。每个 Web 工程需要单独安装前端依赖和构建,但后端插件只安装、启用一次。
后端插件必须包含 `plugin.yaml`。插件发现、安装、菜单、依赖、配置、迁移、种子数据和定时任务都以这个文件为入口。
@@ -54,14 +71,14 @@ cd ruoyi-fastapi-backend
ruoyi plugin create demo --env=dev --template=full-stack
```
-脚手架默认使用 `--frontend-version=auto`,会读取目标前端 `package.json` 的 `vue` 依赖,并自动生成 Vue 2(Element UI、Options API、CommonJS 测试)或 Vue 3(Element Plus、Composition API、ESM 测试)模板。通常无需传参;识别失败或需要覆盖时可显式指定:
+脚手架的 `--frontend-framework` 当前固定可选 `auto`、`vue2`、`vue3`,默认以 `auto` 识别目标框架。当前 v1 源码前端模板只提供 Vue 2(Element UI、Options API、CommonJS 测试)和 Vue 3(Element Plus、Composition API、ESM 测试)实现;`auto` 解析到其他框架时,生成源码页面会明确报错,提示该框架模板尚未支持。在聚合目录下,显式选择 `vue2` 或 `vue3` 还会选择对应的 `vue2/web` 或 `vue3/web` 输出目录:
```bash
-ruoyi plugin create demo --env=dev --template=crud-page --frontend-version=vue2
-ruoyi plugin create demo --env=dev --template=crud-page --frontend-version=vue3
+ruoyi plugin create demo --env=dev --template=crud-page --frontend-framework=vue2
+ruoyi plugin create demo --env=dev --template=crud-page --frontend-framework=vue3
```
-后端实现应在 Vue 2/3 项目间保持一致。`plugin.yaml` 通常只声明两个前端都使用的业务依赖;如果插件确实依赖不同的 Vue 绑定库或构建插件,允许各项目保留不同清单,但应分别提供 Vue 2/3 测试,并根据目标前端 `package.json` 自动选择执行。
+脚手架结果中的 `frontendFramework` 记录所选前端框架。各框架工程共用后端实现和 `plugin.yaml`,前端依赖必须分别完整写入 `dependencies.frontend.` 的 `npm/npmDev`;多个框架都使用的包也需在各自分类中声明。检查和安装时根据目标 Web 工程选择对应分类,不跨分类合并。当前 Vue2、Vue3 页面分别使用 `vue2`、`vue3` 分类,并各自测试。字段规则见[依赖声明](#56-dependencies)。
常用模板:
@@ -222,9 +239,15 @@ permissions:
dependencies:
python:
- requests>=2.32.0
- npm:
- - dayjs>=1.11.0
- npmDev: []
+ frontend:
+ vue2:
+ npm:
+ - dayjs>=1.11.0
+ npmDev: []
+ vue3:
+ npm:
+ - dayjs>=1.11.0
+ npmDev: []
plugins:
- id: ai
version: ">=0.1.0"
@@ -418,10 +441,36 @@ permissions:
| 字段 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `python` | `string[]` | `[]` | Python 依赖声明,例如 `requests>=2.32.0`。 |
-| `npm` | `string[]` | `[]` | 前端运行依赖声明,例如 `dayjs>=1.11.0`。 |
-| `npmDev` | `string[]` | `[]` | 前端开发依赖声明。 |
+| `frontend` | `object` | `{}` | 以安全的小写框架标识为键,完整声明各框架的前端依赖,如 `vue2`、`vue3`、`react`。 |
+| `frontend..npm` | `string[]` | `[]` | 对应框架的全部前端运行依赖,例如 `dayjs>=1.11.0`。 |
+| `frontend..npmDev` | `string[]` | `[]` | 对应框架的全部前端开发依赖。 |
| `plugins` | `object[]` | `[]` | 插件间依赖声明。 |
+同一个后端插件的前端依赖按框架分类声明,以下是当前 Vue2、Vue3 实现的示例:
+
+```yaml
+dependencies:
+ frontend:
+ vue2:
+ npm:
+ - dayjs>=1.11.0
+ - markstream-vue2==0.0.50
+ npmDev: []
+ vue3:
+ npm:
+ - dayjs>=1.11.0
+ - markstream-vue==1.0.9-beta.2
+ - stream-diffs==0.0.2
+ npmDev:
+ - vite-plugin-monaco-editor-esm==2.0.2
+```
+
+`frontend` 的键不限于 `vue2`、`vue3`,也接受 `react` 等安全的小写框架标识。新增分类只声明该框架的依赖,不会自动生成对应前端工程或源码模板。
+
+插件系统解析选定 Web 工程的框架,仅使用对应分类内的 `npm/npmDev`,不会读取其他框架的依赖。未声明目标框架分类时,该宿主没有 npm 依赖;未声明 `frontend` 时,插件没有宿主前端依赖。多个框架都需要的包必须在各自分类中分别列出,不设公共依赖层。
+
+旧顶层 `dependencies.npm`、`dependencies.npmDev` 已移除,清单校验会拒绝,不会自动迁移或合并。升级已有插件时,请将原前端依赖完整迁入对应框架分类;同时支持多个框架的插件需分别维护完整列表。这里的框架标识与 `manifestVersion: 1/2` 无关。
+
`dependencies.plugins[]` 支持对象写法:
| 字段 | 类型 | 默认值 | 说明 |
@@ -444,13 +493,15 @@ dependencies:
| --- | --- | --- | --- |
| `backendVersion` | `string \| null` | `null` | 后端版本约束。 |
| `hostApiVersion` | `string` | `^1.0.0` | 仅 v2:宿主插件 SDK 版本约束,独立于应用版本;当前 Host API 为 `1.4.0`。bundle 至少使用 `^1.2.0`;请求 DTO、显式事务、字典及缓存服务使用 `^1.3.0`;按需配置读取使用 `^1.4.0`。 |
-| `frontendVersion` | `string \| null` | `null` | 前端版本约束。 |
+| `frontendVersion` | `string \| null` | `null` | 前端工程的语义版本约束,保留此字段名;不是框架标识。 |
| `pythonVersion` | `string \| null` | `null` | Python 版本约束。 |
| `nodeVersion` | `string \| null` | `null` | Node.js 版本约束。 |
| `databases` | `("mysql" \| "postgresql")[]` | `[]` | 插件支持的数据库类型声明,不能重复。 |
版本约束可以是版本号,也可以带比较操作符,例如 `>=3.10`、`^20.0.0`。
+`compatibility.frontendVersion` 约束前端工程的发行版本,与脚手架结果、依赖记录和锁文件中的 `frontendFramework` 不同;后者取 `vue2`、`vue3` 等框架标识,不能填入版本约束。
+
### 5.8 resources
| 字段 | 类型 | 默认值 | 说明 |
@@ -599,7 +650,7 @@ v2 新增或改变的字段如下,其余迁移、菜单、配置及插件依
`entrypoint`、Hook、健康检查都使用插件自身模块的完整路径;任务使用 `.`。ASGI 插件不能同时声明 `hooks.onStartup/onShutdown`,启动和关闭统一写入 lifespan。资源路径使用 `/` 分隔的相对路径,不允许绝对路径、反斜杠、`.` 或 `..` 目录。
-bundle 不因存在菜单而变为源码前端,`buildRequired` 必须为 `false`,构建依赖由插件自己的 `package.json` 管理,不写入清单的 `dependencies.npm/npmDev`。清单校验和静态预检不会导入插件代码;通过预检仍需进行实际加载、权限与生命周期测试。
+bundle 不因存在菜单而变为源码前端,`buildRequired` 必须为 `false`,构建依赖由插件自己的 `package.json` 管理,不写入清单的 `dependencies.frontend`。清单校验和静态预检不会导入插件代码;通过预检仍需进行实际加载、权限与生命周期测试。
## 6. 后端开发约定
@@ -913,14 +964,14 @@ Rust 通过 `PluginHostContext`、`plugin_endpoint`、`plugin_lifespan` 和版
插件前端代码放在:
```text
-ruoyi-fastapi-frontend/plugins//
+ruoyi-fastapi-frontend//web/plugins//
```
-菜单组件路径和真实 Vue 文件的映射关系:
+当前 Vue2、Vue3 实现中,菜单组件路径和真实 Vue 文件的映射关系如下,`` 分别替换为 `vue2` 或 `vue3`:
```text
-plugin/demo/index -> ruoyi-fastapi-frontend/plugins/demo/views/index.vue
-plugin/demo/report/list -> ruoyi-fastapi-frontend/plugins/demo/views/report/list.vue
+plugin/demo/index -> ruoyi-fastapi-frontend//web/plugins/demo/views/index.vue
+plugin/demo/report/list -> ruoyi-fastapi-frontend//web/plugins/demo/views/report/list.vue
```
只允许两类组件值:
@@ -946,9 +997,9 @@ UI GET/HEAD 使用普通 HTTPS,仍经过插件身份门禁;`/apps//api/.
HTML 注入有效 `uiBase`、`apiBase` 与 ``。SPA 回退仅接受 UI GET/HEAD 中带 `Accept: text/html` 的无扩展名页面导航;缺失脚本、API、保留路径和非 HTML 请求不能返回首页。静态服务拒绝路径越界和链接逃逸,并限制为同源 iframe 嵌入。HTML 当前 `no-store`,其他静态资源 `no-cache`,不自动启用 immutable 缓存。
-前端各环境已显式配置 `VITE_APP_PLUGIN_BASE`:development 为 `/dev-api`、production 为 `/prod-api`、docker 为 `/docker-api`、staging 为 `/stage-api`,与各环境的 `VITE_APP_BASE_API` 一致。开发、生产、Docker 分别对齐后端 `.env.dev`、`.env.prod`、`.env.dockermy`/`.env.dockerpg` 的 `APP_ROOT_PATH`;仓库未提供独立的后端 staging 环境文件,部署 staging 时需将后端 `APP_ROOT_PATH` 配为 `/stage-api`。以开发环境为例,插件使用同源 `/dev-api/apps/` 和 `/dev-api/plugin/runtime/`,Cookie Path 和会话响应也包含此前缀。
+Vue3 Web 各环境已显式配置 `VITE_APP_PLUGIN_BASE`,Vue2 Web 使用 `VUE_APP_PLUGIN_BASE`:development 为 `/dev-api`、production 为 `/prod-api`、docker 为 `/docker-api`、staging 为 `/stage-api`,分别与各环境的 `VITE_APP_BASE_API`、`VUE_APP_BASE_API` 一致。开发、生产、Docker 分别对齐后端 `.env.dev`、`.env.prod`、`.env.dockermy`/`.env.dockerpg` 的 `APP_ROOT_PATH`;仓库未提供独立的后端 staging 环境文件,部署 staging 时需将后端 `APP_ROOT_PATH` 配为 `/stage-api`。以开发环境为例,插件使用同源 `/dev-api/apps/` 和 `/dev-api/plugin/runtime/`,Cookie Path 和会话响应也包含此前缀。
-自定义部署时,宿主前端的 `VITE_APP_PLUGIN_BASE` 必须与后端 `APP_ROOT_PATH` 一致;只有后端前缀为空时,前端才留空并使用 `/apps/` 和 `/plugin/runtime/`。浏览器侧保留部署前缀,代理转发至通过 `app.py` 启动的 Uvicorn 时剥离此前缀一次,由 Uvicorn 补回 ASGI `root_path`;保留外部 Host,HTTPS 的原始 scheme 由可信代理配置传递。仓库 Vite 已按此规则改写插件路径;两份 Docker Nginx 配置也已提供 `/docker-api/apps/` 和 `/docker-api/plugin/runtime/` 专用代理,并在转发时剥离 `/docker-api`。生产、staging 或自定义反向代理须配套设置相同前缀、专用代理入口及转发路径,保留 WebSocket 升级和 SSE 非缓冲配置,并验证 Cookie Path、Origin、Secure 和加密 AAD。独立页面属于同源可信代码;iframe 提供布局和依赖隔离,不提供对恶意插件的安全隔离。
+自定义部署时,宿主前端的 `VITE_APP_PLUGIN_BASE`(Vue3)或 `VUE_APP_PLUGIN_BASE`(Vue2)必须与后端 `APP_ROOT_PATH` 一致;只有后端前缀为空时,前端才留空并使用 `/apps/` 和 `/plugin/runtime/`。浏览器侧保留部署前缀,代理转发至通过 `app.py` 启动的 Uvicorn 时剥离此前缀一次,由 Uvicorn 补回 ASGI `root_path`;保留外部 Host,HTTPS 的原始 scheme 由可信代理配置传递。两个 Web 工程的开发代理已按此规则改写插件路径;各自的 Docker Nginx 配置也已提供 `/docker-api/apps/` 和 `/docker-api/plugin/runtime/` 专用代理,并在转发时剥离 `/docker-api`。生产、staging 或自定义反向代理须配套设置相同前缀、专用代理入口及转发路径,保留 WebSocket 升级和 SSE 非缓冲配置,并验证 Cookie Path、Origin、Secure 和加密 AAD。独立页面属于同源可信代码;iframe 提供布局和依赖隔离,不提供对恶意插件的安全隔离。
### 7.3 bundle 浏览器 SDK
@@ -1100,10 +1151,14 @@ async def summary(context: PluginRequestContext) -> dict[str, str]:
插件依赖声明在 `dependencies` 中:
- `python`:Python 包。
-- `npm`:前端运行依赖。
-- `npmDev`:前端开发依赖。
+- `frontend..npm`:对应框架 Web 工程的全部前端运行依赖。
+- `frontend..npmDev`:对应框架 Web 工程的全部前端开发依赖。
- `plugins`:插件间依赖。
+未声明目标框架分类时,该宿主没有 npm 依赖。清单不再接受顶层 `dependencies.npm/npmDev`;迁移时需将依赖完整移入目标分类,同时支持多个框架的包需分别列出。
+
+依赖检查、安装和锁文件生成均针对当前选定的 Web 宿主。默认是 `vue3/web`;验证 Vue2 时,按[基本模型](#1-基本模型)设置 `RUOYI_PLUGIN_FRONTEND_FRAMEWORK=vue2`,或通过根目录变量选择具体工程,再运行相同命令。各框架工程不共享 `node_modules`。
+
检查和安装:
```bash
@@ -1120,6 +1175,20 @@ ruoyi plugin install-deps demo --env=dev --yes
`lock-deps` 默认生成锁文件模板,输出到 `plugins//plugin.lock.yaml`;如文件已存在,需要传 `--overwrite` 才会覆盖。默认模式不会联网解析真实版本,也不会写入 hash/integrity;如果传入 `--offline-dir`,命令会从已有本地 wheel/tgz 反填 `resolvedVersion`、Python `hashes` 和 npm/npmDev `integrity`。它仍不会下载、安装或访问 registry,未能反填的项发布前应由人工审核或 CI 流水线补齐。
+清单声明 `dependencies.frontend` 时,锁模板只包含当前宿主分类的前端依赖,并记录 `frontendFramework`;安装策略会拒绝框架标识与宿主不一致的锁文件。锁文件产物仍使用顶层 `npm/npmDev` 列表,检查报告的依赖类型也仍为 `npm/npmDev`,它们与 `plugin.yaml` 清单格式不同。
+
+包含旧框架字段 `frontendVersion` 的锁文件会被明确拒绝,不做静默迁移,应按目标框架重新生成并审核锁文件。既没有 `frontendFramework` 也没有旧框架字段的旧锁文件,仍沿用原有依赖项校验。这里的旧锁字段与仍受支持的 `compatibility.frontendVersion` 语义版本约束不同。
+
+建议用 `--output-path` 分别维护各框架的锁文件,安装时用 `--lockfile` 选择。例如先切换到 Vue2,再执行:
+
+```bash
+ruoyi plugin lock-deps ai --env=dev --output-path plugins/ai/plugin.vue2.lock.yaml
+# 发布流程补齐并审核锁文件中的 resolvedVersion、hash/integrity 后再安装
+ruoyi plugin install-deps ai --env=dev --lockfile plugins/ai/plugin.vue2.lock.yaml --dry-run
+```
+
+Vue3 使用相同流程,将宿主框架和文件名改为 `vue3`。各框架分别维护宿主依赖锁,不要通过覆盖同一文件共享锁内容。
+
如果当前终端是交互式 TTY,且输出格式为 text,也可以不传 `--yes`:
```bash
@@ -1290,7 +1359,8 @@ ruoyi plugin test demo --env=dev
全栈插件还应执行前端构建检查:
```bash
-cd ../ruoyi-fastapi-frontend
+cd ../ruoyi-fastapi-frontend/vue3/web
+# Vue2 工程改为:cd ../ruoyi-fastapi-frontend/vue2/web
npm run build:prod
```
@@ -1554,7 +1624,7 @@ ruoyi plugin create report_center --template rust-bundle --env dev --dry-run
ruoyi plugin create report_center --template rust-bundle --env dev
```
-每个工程包含 v2 清单、受 `:view` 权限保护的 `/api/info`、显式入口、健康检查、lifespan、构建与发布 README,以及 SDK 合约测试。Rust 模板固定 `ruoyi_plugin_` Python 命名空间,包含锁定的 Cargo 依赖。bundle 复制当前宿主的 bridge SDK 及配套 `.d.ts`,使用相对资源基址和专用插件会话,不内嵌主登录 token。它与宿主 Vue 版本独立,v2 模板不接受 `--frontend-version`;bundle 也不能配合 `--backend-only`。
+每个工程包含 v2 清单、受 `:view` 权限保护的 `/api/info`、显式入口、健康检查、lifespan、构建与发布 README,以及 SDK 合约测试。Rust 模板固定 `ruoyi_plugin_` Python 命名空间,包含锁定的 Cargo 依赖。bundle 复制当前宿主的 bridge SDK 及配套 `.d.ts`,使用相对资源基址和专用插件会话,不内嵌主登录 token。独立 bundle 与宿主前端框架解耦,v2 模板的 `--frontend-framework` 只允许默认值 `auto`;bundle 也不能配合 `--backend-only`。
bundle 安装 npm 依赖后,可运行 `npm --prefix plugin-projects/report_center/web run dev`,自动打开生成的 `/dev.html`。`dev.js` 提供 `/api/info` 的内存模拟、主题、延迟、故障、重载和退出,可按业务扩展;默认模板未实现文件或 SSE 业务接口。模拟配置仅在开发服务注入,生产构建不包含开发宿主。文件与 SSE 的完整示例见 `bundle_demo`。
@@ -1605,7 +1675,7 @@ ruoyi plugin sdk update plugin-projects/report_center
npm --prefix plugin-projects/report_center/web run build
```
-默认来源为宿主相邻前端目录;独立检出时可传 `--frontend-root PATH`,或设置 `RUOYI_PLUGIN_FRONTEND_ROOT`。命令只静态读取工程清单和固定 SDK 文件,不导入插件、不读取宿主运行环境、不连接数据库或 registry。清单检查仅确认 YAML 可读、`manifestVersion: 2` 和 bundle 工程类型,不执行完整插件清单校验;完整检查与业务测试仍按第 13 节执行。
+SDK 命令的 `--frontend-framework` 当前固定可选 `auto`、`vue2`、`vue3`,默认值为 `auto`。默认来源为 `ruoyi-fastapi-frontend/vue3/web`;可用 `--frontend-framework=vue2` 选择 Vue2 Web,`auto` 则跟随框架环境变量和显式根目录选择。通用来源目录为 `ruoyi-fastapi-frontend//web`,所选工程必须提供对应 bridge SDK 文件;指定框架不会生成这些文件。独立检出时可传 `--frontend-root PATH`,或设置 `RUOYI_PLUGIN_FRONTEND_ROOT`。命令只静态读取工程清单和固定 SDK 文件,不导入插件、不读取宿主运行环境、不连接数据库或 registry。清单检查仅确认 YAML 可读、`manifestVersion: 2` 和 bundle 工程类型,不执行完整插件清单校验;完整检查与业务测试仍按第 13 节执行。
| `check` 状态 | 含义与处理 |
| --- | --- |
@@ -1665,7 +1735,8 @@ python -m pytest tests/plugins/core/artifacts tests/plugins/core/deployment test
bundle 插件完成独立前端构建后,还应执行宿主桥接测试(在后端目录执行,三种平台通用):
```bash
-npm --prefix ../ruoyi-fastapi-frontend run test:plugin
+npm --prefix ../ruoyi-fastapi-frontend/vue2/web run test:plugin
+npm --prefix ../ruoyi-fastapi-frontend/vue3/web run test:plugin
```
### 17.2 真实服务与多 worker 验收
@@ -1695,27 +1766,27 @@ npm --prefix ../ruoyi-fastapi-frontend run test:plugin
### 17.4 CI 覆盖
-仓库的原生插件工作流配置 Windows/Linux、Python 3.10、3.11、3.12、3.13 构建测试矩阵,覆盖 v1/v2 脚手架、SDK 溯源与离线更新命令、原生扩展以及签名制品与维护发布回归。
+统一入口为 [`ci.yml`](../../.github/workflows/ci.yml),完整分类和本地命令见[测试说明](../../ruoyi-fastapi-test/README.md)。[`ci-backend.yml`](../../.github/workflows/ci-backend.yml) 承担 v1/v2 脚手架、SDK 溯源、离线更新及 Windows CLI/路径回归;[`ci-native.yml`](../../.github/workflows/ci-native.yml) 保留 Windows/Linux × Python 3.10–3.13 的真实原生制品测试,Rust fmt/clippy 单独运行一次。
-发布集成工作流在 Python 3.10、3.11、3.12、3.13 上使用临时 MySQL、PostgreSQL 和 Redis 服务验证多 worker 发布行为。工作流配置不代表已经执行成功;发布前应核对对应提交的实际 CI 结果,并完成目标平台的部署验收。
+[`ci-integration.yml`](../../.github/workflows/ci-integration.yml) 使用临时 MySQL、PostgreSQL 和 Redis 服务验证多 worker 发布行为,PR 使用 Python 3.12,完整运行覆盖 Python 3.10–3.13。工作流配置不代表已经执行成功;发布前应核对对应提交的实际 CI 结果,并完成目标平台的部署验收。
`scripts/plugin_release_integration.py` 还检查双 worker 首次发布、升级和代码回滚后的固定目标收敛,无 worker 时单次断言不收敛,旧目标代际被拒绝、单 worker 故障立即失败,以及报告过期、缺员和遗留陈旧记录下的等待结果。足够的新 worker 全部就绪后,遗留陈旧记录不应造成永久等待;失败结果保留退出码和固定原因供流水线判断。脚本还针对两种数据库启动独立的完整 CLI 进程,验证标准输出可整体解析为一个 JSON 对象,并核对未就绪时退出 `50`、就绪后退出 `0`。
该工作流还实际构建 `task_demo` 的 bundle,并通过 `scripts/plugin_task_integration.py` 验证 Python 受控目录打包、签名、导入、维护准备、目标选择、旧结构数据保留以及当前业务 CRUD/读写分权。旧版本是仅承载 001 迁移的测试夹具;当前版本从真实签名制品加载。业务 HTTP 验收使用已认证上下文和 ASGI 适配器,不代替浏览器登录验收。运行真实发布集成脚本前须先按任务示例 README 构建 `web/dist`;本地 SQLite 交付回归使用最小 HTML 夹具,单独验证维护链路。
-[`plugin-frontend.yml`](../../.github/workflows/plugin-frontend.yml) 在 Node.js 22 上执行 `npm run test:plugin`,相关源码、测试、SDK、构建配置及依赖变更会触发检查。仓库当前不提交 npm 锁文件,因此使用 `npm install --package-lock=false`,依赖解析仍遵循 `package.json` 的版本范围。
+[`ci-frontend.yml`](../../.github/workflows/ci-frontend.yml) 在 Node.js 22 上按框架和 Web/Mobile 工程执行 `test:unit`、三时区 `test:contract` 及各目标构建;Web 单元入口包含插件、认证和任务测试,覆盖 `vue2`、`vue3`。当前两个 Web 工程不提交 npm 锁文件,因此使用 `npm install --package-lock=false`,依赖解析仍遵循各自 `package.json` 的版本范围。
-[`plugin-network-smoke.yml`](../../.github/workflows/plugin-network-smoke.yml) 在 Linux、Python 3.12、Node.js 22 与 Chromium 上执行真实网络 smoke。链路为宿主 Vue `PluginFrame` → iframe SDK → 临时 HTTPS Vite 代理 → Uvicorn → 生产插件会话与门禁,使用 `/gateway` 部署前缀。覆盖未登录拒绝、限定路径的 Secure/HttpOnly/SameSite Cookie、JSON POST、CSRF/Origin 拒绝、SSE 增量接收与取消后游标恢复、SSE/WebSocket 在权限及主会话撤销后的关闭,以及 runtime drain 后连接和 lifespan 回收。工作流保留截图、Playwright trace、代理日志与 JUnit 结果。
+`ci-integration.yml` 的 network 任务在 Linux、Python 3.12、Node.js 22 与 Chromium 上分别执行 Vue2/Vue3 真实网络 smoke。链路为宿主 `PluginFrame` → iframe SDK → 临时 HTTPS 代理 → Uvicorn → 生产插件会话与门禁,使用 `/gateway` 部署前缀。覆盖未登录拒绝、限定路径的 Secure/HttpOnly/SameSite Cookie、JSON POST、CSRF/Origin 拒绝、SSE 增量接收与取消后游标恢复、SSE/WebSocket 在权限及主会话撤销后的关闭,以及 runtime drain 后连接和 lifespan 回收。工作流保留截图、Playwright trace、代理日志与 JUnit 结果。
本地复现(在后端目录执行,三个平台通用):
```bash
python -m pip install pytest pytest-asyncio aiosqlite playwright
python -m playwright install chromium
-npm --prefix ../ruoyi-fastapi-frontend install --no-audit --no-fund --package-lock=false
+npm --prefix ../ruoyi-fastapi-frontend/vue3/web install --no-audit --no-fund --package-lock=false
python -c "import os,pytest; os.environ['RUOYI_PLUGIN_NETWORK_SMOKE']='1'; raise SystemExit(pytest.main(['tests/plugins/integration/test_network_smoke.py','-q']))"
```
-常规 pytest 不设置 `RUOYI_PLUGIN_NETWORK_SMOKE=1` 时跳过此项,不要求安装浏览器。显式启用后,缺少浏览器或 Node 依赖会直接失败。可通过 `RUOYI_SMOKE_BROWSER_EXECUTABLE` 指定已有 Chromium 浏览器,通过 `RUOYI_SMOKE_OUTPUT` 指定诊断目录;默认输出在后端 `target/plugin-network-smoke/`。测试仅绑定 loopback 随机端口,临时证书不加入系统信任库,结束时回收浏览器和服务。
+常规 Backend 分类使用 marker 排除此项;单独运行但不设置 `RUOYI_PLUGIN_NETWORK_SMOKE=1` 时跳过,不要求安装浏览器。显式启用后,缺少浏览器或 Node 依赖会直接失败。设置 `RUOYI_PLUGIN_FRONTEND_FRAMEWORK=vue2` 可验证 Vue2(先安装对应 Web 依赖并设置 `NODE_OPTIONS=--openssl-legacy-provider`),默认是 Vue3。可通过 `RUOYI_SMOKE_BROWSER_EXECUTABLE` 指定已有 Chromium 浏览器,通过 `RUOYI_SMOKE_OUTPUT` 指定诊断目录;默认输出在后端 `target/plugin-network-smoke//`。测试仅绑定 loopback 随机端口,临时证书不加入系统信任库,结束时回收浏览器和服务。
该 smoke 的账号查询、数据库会话、插件启用状态和 Redis 使用隔离夹具,传输加密为 off;它验证真实网络上的生产会话协议,不覆盖真实账号登录、Redis 故障、required 加密、生产证书信任或进程信号触发的停机。runtime drain 之后才让 Uvicorn 正常退出,SIGTERM、完整宿主 lifespan 及目标部署代理仍按 17.2 节另行验收。
diff --git a/ruoyi-fastapi-backend/docs/unified_authentication_developer_guide.md b/ruoyi-fastapi-backend/docs/unified_authentication_developer_guide.md
index ff0b9ae..d6d1cc2 100644
--- a/ruoyi-fastapi-backend/docs/unified_authentication_developer_guide.md
+++ b/ruoyi-fastapi-backend/docs/unified_authentication_developer_guide.md
@@ -1,6 +1,6 @@
# 统一认证中心开发者使用手册
-更新日期:2026-10-01。适用于当前 Vue3、Vue2 两个仓库的统一认证中心;两者使用相同的后端协议和接入配置。
+更新日期:2026-10-10。适用于本仓库 `ruoyi-fastapi-frontend/vue3/web`、`ruoyi-fastapi-frontend/vue2/web` 两个 Web 工程的统一认证中心;两者共用 `ruoyi-fastapi-backend`,使用相同的后端协议和接入配置。
本文面向平台管理员、业务应用开发者和资源服务开发者,按“启用服务 → 注册应用 → 登录 → 调用 API → 续期和退出”的顺序说明接入方法。本文的域名、客户端编号和凭据均为占位示例,使用时替换为实际值。
diff --git a/ruoyi-fastapi-backend/module_admin/service/server_service.py b/ruoyi-fastapi-backend/module_admin/service/server_service.py
index 8c70add..bb9f585 100644
--- a/ruoyi-fastapi-backend/module_admin/service/server_service.py
+++ b/ruoyi-fastapi-backend/module_admin/service/server_service.py
@@ -80,25 +80,64 @@ class ServerService:
)
# 磁盘信息
- io = psutil.disk_partitions()
- sys_files = []
- for i in io:
- try:
- o = psutil.disk_usage(i.device)
- disk_data = SysFiles(
- dirName=i.device,
- sysTypeName=i.fstype,
- typeName='本地固定磁盘(' + i.mountpoint.replace('\\', '') + ')',
- total=bytes2human(o.total),
- used=bytes2human(o.used),
- free=bytes2human(o.free),
- usage=f'{psutil.disk_usage(i.device).percent}%',
- )
- sys_files.append(disk_data)
- except Exception: # noqa: PERF203
- # 忽略所有异常,跳过有问题的磁盘
- continue
+ sys_files = ServerService._get_disk_info()
result = ServerMonitorModel(cpu=cpu, mem=mem, sys=sys, py=py, sysFiles=sys_files)
return result
+
+ @classmethod
+ def _get_disk_info(cls) -> list[SysFiles]:
+ """
+ 获取可访问的磁盘信息
+
+ :return: 磁盘信息列表
+ """
+ sys_files = []
+ for partition in psutil.disk_partitions():
+ disk_info = cls._read_disk_info(partition.device, partition.mountpoint, partition.fstype)
+ if disk_info is not None:
+ sys_files.append(disk_info)
+ if sys_files:
+ return sys_files
+
+ # 默认分区列表会过滤overlay等容器文件系统,无可读分区时补充根文件系统
+ root_path = os.path.abspath(os.sep)
+ # 仅匹配根目录,避免展示proc、sysfs等虚拟挂载
+ root_partition = next(
+ (partition for partition in psutil.disk_partitions(all=True) if partition.mountpoint == root_path),
+ None,
+ )
+ disk_info = cls._read_disk_info(
+ root_partition.device if root_partition else root_path,
+ root_path,
+ root_partition.fstype if root_partition else '',
+ )
+
+ return [disk_info] if disk_info is not None else []
+
+ @staticmethod
+ def _read_disk_info(device: str, mountpoint: str, filesystem: str) -> SysFiles | None:
+ """
+ 读取指定磁盘的容量信息
+
+ :param device: 磁盘设备名称
+ :param mountpoint: 磁盘挂载路径
+ :param filesystem: 文件系统类型
+ :return: 磁盘容量信息,挂载路径不可访问时返回None
+ """
+ try:
+ disk_usage = psutil.disk_usage(mountpoint)
+ except OSError:
+ # 跳过不存在或无权访问的挂载路径
+ return None
+
+ return SysFiles(
+ dirName=device,
+ sysTypeName=filesystem,
+ typeName='本地固定磁盘(' + mountpoint.replace('\\', '') + ')',
+ total=bytes2human(disk_usage.total),
+ used=bytes2human(disk_usage.used),
+ free=bytes2human(disk_usage.free),
+ usage=f'{disk_usage.percent}%',
+ )
diff --git a/ruoyi-fastapi-backend/module_identity/service/audit_service.py b/ruoyi-fastapi-backend/module_identity/service/audit_service.py
index ddb8e7c..8a742c8 100644
--- a/ruoyi-fastapi-backend/module_identity/service/audit_service.py
+++ b/ruoyi-fastapi-backend/module_identity/service/audit_service.py
@@ -9,7 +9,7 @@ from exceptions.exception import OidcInteractionException
from module_identity.dao.oauth_audit_dao import OAuthAuditDao
from module_identity.entity.do.oauth_audit_do import SysOAuthAuditLog
from module_identity.entity.vo.oauth_session_vo import AuditModel, AuditPageQueryModel
-from utils.common_util import export_list2excel
+from utils.excel_util import ExcelUtil
from utils.oidc_util import OidcUtil
@@ -270,8 +270,9 @@ class AuditService:
"""
rows = await OAuthAuditDao.list_admin_page(db, offset=0, limit=5000, **cls._admin_filters(query))
+ mapping = {field.alias or name: field.alias or name for name, field in AuditModel.model_fields.items()}
- return export_list2excel([cls._admin_model(row) for row in rows])
+ return ExcelUtil.export_list2excel([cls._admin_model(row) for row in rows], mapping)
@classmethod
async def record_interaction_failure(cls, db: AsyncSession, event_type: str, **fields: Any) -> None:
diff --git a/ruoyi-fastapi-backend/plugins/ai/README.md b/ruoyi-fastapi-backend/plugins/ai/README.md
index 8535dfc..aca5767 100644
--- a/ruoyi-fastapi-backend/plugins/ai/README.md
+++ b/ruoyi-fastapi-backend/plugins/ai/README.md
@@ -1,6 +1,6 @@
# AI 管理后端插件
-AI 管理插件提供模型配置、供应商字典、会话对话和流式输出能力。插件保留原有接口前缀和权限标识,安装后会在系统菜单中生成「AI 管理」目录,以及「模型管理」「AI 对话」两个页面。
+AI 管理插件提供模型配置、供应商字典、会话对话和流式输出能力。插件保留原有接口前缀和权限标识,安装后会在系统菜单中生成「AI 管理」目录,以及「模型管理」「AI 对话」两个页面。各前端框架共用此后端插件及同一份 `plugin.yaml`,页面目录约定为 `ruoyi-fastapi-frontend//web/plugins/ai`;当前提供 Vue2、Vue3 页面,分别位于 `vue2/web/plugins/ai` 和 `vue3/web/plugins/ai`。
## 功能
@@ -93,7 +93,17 @@ ruoyi plugin disable ai --env=dev --yes
- `ollama`
- 以及其他供应商 SDK
-插件启动期会检查已启用插件的 Python 依赖。缺失依赖时,CLI 启动流程会提示是否安装依赖;生产环境建议在发布阶段提前安装,避免运行期临时拉取依赖。
+应用启动时只做默认启用插件的依赖门禁,不会自动安装或提示安装依赖。缺失依赖时,应在启动前显式执行 `ruoyi plugin install-deps ai --env=dev`;生产环境应在发布阶段提前准备依赖。
+
+前端依赖按宿主框架完整声明,当前 AI 插件提供以下分类:
+
+- `dependencies.frontend.vue2.npm`:Vue2 所需的全部运行依赖,包括 `markstream-vue2` 以及 Markdown、代码高亮、图表等依赖。
+- `dependencies.frontend.vue3.npm`:Vue3 所需的全部运行依赖,包括 `markstream-vue`、`stream-diffs` 以及 Markdown、代码高亮、图表等依赖。
+- `dependencies.frontend.vue3.npmDev`:Vue3 构建使用的 `vite-plugin-monaco-editor-esm`。
+
+多个框架都使用的包也在各自分类中列出,不设公共依赖层。`dependencies.frontend` 接受 `react` 等安全的小写框架标识,但新增分类不会生成对应页面;当前 AI 插件仅提供 Vue2、Vue3 实现。旧顶层 `dependencies.npm/npmDev` 已移除,清单校验会拒绝;未声明目标框架分类时,该宿主没有 npm 依赖。
+
+插件系统解析选定 Web 工程的框架,只检查、安装匹配框架分类中的依赖。默认选择 `vue3/web`;检查或安装 Vue2 依赖时,先在当前终端设置 `RUOYI_PLUGIN_FRONTEND_FRAMEWORK=vue2`(PowerShell:`$env:RUOYI_PLUGIN_FRONTEND_FRAMEWORK = 'vue2'`;Bash:`export RUOYI_PLUGIN_FRONTEND_FRAMEWORK=vue2`),再执行同样的插件命令。未设置该变量时也可使用 `RUOYI_FRONTEND_FRAMEWORK`,或用 `RUOYI_PLUGIN_FRONTEND_ROOT` 指向具体 Web 工程。各框架工程分别安装依赖和构建,后端插件仅需安装、启用一次。完整规则见[插件开发手册](../../docs/plugin_development.md#9-依赖管理)。
## 数据库
diff --git a/ruoyi-fastapi-backend/plugins/ai/plugin.yaml b/ruoyi-fastapi-backend/plugins/ai/plugin.yaml
index 7001000..9355bdc 100644
--- a/ruoyi-fastapi-backend/plugins/ai/plugin.yaml
+++ b/ruoyi-fastapi-backend/plugins/ai/plugin.yaml
@@ -86,18 +86,31 @@ dependencies:
- ollama==0.6.2
- openai==2.52.0
- portkey-ai==2.3.4
- npm:
- - '@antv/infographic^0.2.13'
- - '@terrastruct/d2==0.1.33'
- - katex>=0.16.27
- - markstream-vue==1.0.9-beta.2
- - mermaid>=11.15.0
- - shiki^3.21.0
- - stream-diffs==0.0.2
- - stream-markdown==0.0.16
- - stream-monaco==0.0.48
- npmDev:
- - vite-plugin-monaco-editor-esm==2.0.2
+ frontend:
+ vue2:
+ npm:
+ - '@antv/infographic^0.2.13'
+ - '@terrastruct/d2==0.1.33'
+ - katex>=0.16.27
+ - markstream-vue2==0.0.50
+ - mermaid>=11.15.0
+ - shiki^3.21.0
+ - stream-markdown==0.0.16
+ - stream-monaco==0.0.48
+ npmDev: []
+ vue3:
+ npm:
+ - '@antv/infographic^0.2.13'
+ - '@terrastruct/d2==0.1.33'
+ - katex>=0.16.27
+ - markstream-vue==1.0.9-beta.2
+ - mermaid>=11.15.0
+ - shiki^3.21.0
+ - stream-diffs==0.0.2
+ - stream-markdown==0.0.16
+ - stream-monaco==0.0.48
+ npmDev:
+ - vite-plugin-monaco-editor-esm==2.0.2
plugins: []
compatibility:
diff --git a/ruoyi-fastapi-backend/plugins/core/capability.py b/ruoyi-fastapi-backend/plugins/core/capability.py
index f460867..81e4b99 100644
--- a/ruoyi-fastapi-backend/plugins/core/capability.py
+++ b/ruoyi-fastapi-backend/plugins/core/capability.py
@@ -116,8 +116,7 @@ class PluginRuntimeCapabilityResolver:
manifest = discovered_plugin.manifest
has_frontend_resources = bool(
manifest.frontend.menus
- or manifest.dependencies.npm
- or manifest.dependencies.npm_dev
+ or manifest.dependencies.has_frontend_dependencies
or manifest.frontend.delivery.type == 'bundle'
)
frontend_build_required = manifest.frontend.delivery.type != 'bundle' and (
diff --git a/ruoyi-fastapi-backend/plugins/core/environment.py b/ruoyi-fastapi-backend/plugins/core/environment.py
index 016d710..6db39f3 100644
--- a/ruoyi-fastapi-backend/plugins/core/environment.py
+++ b/ruoyi-fastapi-backend/plugins/core/environment.py
@@ -3,19 +3,23 @@ from pathlib import Path
from typing import Literal
from config.env import AppConfig
+from plugins.core.frontend import (
+ PluginFrontendFrameworkResolver,
+ resolve_frontend_framework_request,
+ resolve_frontend_root,
+)
PluginFrontendMode = Literal['dev', 'built']
PluginBackendRuntimeMode = Literal['dev', 'service', 'maintenance']
BACKEND_ROOT_ENV_NAMES = ('RUOYI_PLUGIN_BACKEND_ROOT', 'RUOYI_BACKEND_ROOT')
-FRONTEND_ROOT_ENV_NAMES = ('RUOYI_PLUGIN_FRONTEND_ROOT', 'RUOYI_FRONTEND_ROOT')
class PluginRuntimeEnvironmentService:
"""
插件运行时环境服务。
- 为插件运行时提供后端目录和 Python 可执行文件等路径信息。
+ 为插件运行时提供前后端工程目录、Python 可执行文件和运行模式。
"""
def __init__(
@@ -23,17 +27,20 @@ class PluginRuntimeEnvironmentService:
backend_root: Path | str | None = None,
frontend_root: Path | str | None = None,
python_executable: str | None = None,
+ frontend_framework: str = 'auto',
) -> None:
"""
初始化插件运行时环境服务。
:param backend_root: 后端项目根目录
- :param frontend_root: 前端项目根目录
+ :param frontend_root: 前端项目根目录或统一前端目录
:param python_executable: Python 可执行文件路径
+ :param frontend_framework: 前端框架标识,auto 使用环境变量,未配置时默认 vue3
:return: None
"""
self.backend_root = self._resolve_backend_root(backend_root)
- self.frontend_root = self._resolve_frontend_root(self.backend_root, frontend_root)
+ self.frontend_root = resolve_frontend_root(self.backend_root, frontend_root, frontend_framework)
+ self.frontend_framework = resolve_frontend_framework_request(self.frontend_root, frontend_framework)
self.python_executable = python_executable or 'python'
self.frontend_mode = self._get_frontend_mode()
self.backend_runtime_mode = self._get_backend_runtime_mode()
@@ -53,27 +60,6 @@ class PluginRuntimeEnvironmentService:
return configured_backend_root.resolve()
return Path(__file__).resolve().parents[2]
- @staticmethod
- def _resolve_frontend_root(backend_root: Path, frontend_root: Path | str | None) -> Path:
- """
- 解析前端项目根目录。
-
- 解析顺序为:显式参数、环境变量、后端同级目录中的前端工程、按后端目录名推断。
-
- :param backend_root: 后端项目根目录
- :param frontend_root: 显式传入的前端项目根目录
- :return: 前端项目根目录
- """
- if frontend_root:
- return Path(frontend_root).resolve()
- configured_frontend_root = PluginRuntimeEnvironmentService._first_env_path(FRONTEND_ROOT_ENV_NAMES)
- if configured_frontend_root:
- return configured_frontend_root.resolve()
- sibling_frontend_root = PluginRuntimeEnvironmentService._find_sibling_frontend_root(backend_root)
- if sibling_frontend_root:
- return sibling_frontend_root.resolve()
- return backend_root.parent / PluginRuntimeEnvironmentService._infer_frontend_dir_name(backend_root.name)
-
@staticmethod
def _first_env_path(env_names: tuple[str, ...]) -> Path | None:
"""
@@ -88,41 +74,6 @@ class PluginRuntimeEnvironmentService:
return Path(value)
return None
- @staticmethod
- def _find_sibling_frontend_root(backend_root: Path) -> Path | None:
- """
- 从后端同级目录中寻找前端工程。
-
- :param backend_root: 后端项目根目录
- :return: 前端项目根目录,未找到时返回 None
- """
- parent = backend_root.parent
- if not parent.is_dir():
- return None
- candidates = [
- path
- for path in parent.iterdir()
- if path.is_dir()
- and path != backend_root
- and (path / 'package.json').is_file()
- and (path / 'plugins').is_dir()
- ]
- if not candidates:
- return None
- return sorted(candidates)[0]
-
- @staticmethod
- def _infer_frontend_dir_name(backend_dir_name: str) -> str:
- """
- 根据后端目录名推断前端目录名。
-
- :param backend_dir_name: 后端目录名
- :return: 推断出的前端目录名
- """
- if 'backend' in backend_dir_name:
- return backend_dir_name.replace('backend', 'frontend', 1)
- return 'frontend'
-
@staticmethod
def _get_frontend_mode() -> PluginFrontendMode:
"""
@@ -169,6 +120,20 @@ class PluginRuntimeEnvironmentService:
"""
return str(self.frontend_root)
+ def get_frontend_framework(self, frontend_root: Path | str | None = None) -> str:
+ """
+ 获取指定前端工程的框架标识,保留当前工程的显式选择。
+
+ 其他工程按自身目录和依赖解析,不沿用当前工程的框架标识。
+ 仅在需要前端依赖时解析,避免后端独立运行时要求存在前端文件。
+
+ :param frontend_root: 前端工程根目录,未指定时使用当前工程
+ :return: 前端框架标识
+ """
+ root = Path(os.path.abspath(frontend_root)) if frontend_root is not None else self.frontend_root
+ framework = self.frontend_framework if root == self.frontend_root else 'auto'
+ return PluginFrontendFrameworkResolver.resolve(root, framework)
+
def get_frontend_plugins_dir(self) -> str:
"""
获取前端插件根目录。
diff --git a/ruoyi-fastapi-backend/plugins/core/frontend.py b/ruoyi-fastapi-backend/plugins/core/frontend.py
new file mode 100644
index 0000000..ce6efab
--- /dev/null
+++ b/ruoyi-fastapi-backend/plugins/core/frontend.py
@@ -0,0 +1,214 @@
+import json
+import os
+import re
+from pathlib import Path
+
+FrontendFramework = str
+
+FRONTEND_ROOT_ENV_NAMES = ('RUOYI_PLUGIN_FRONTEND_ROOT', 'RUOYI_FRONTEND_ROOT')
+FRONTEND_FRAMEWORK_ENV_NAMES = ('RUOYI_PLUGIN_FRONTEND_FRAMEWORK', 'RUOYI_FRONTEND_FRAMEWORK')
+FRONTEND_FRAMEWORK_PATTERN = re.compile(r'^[a-z][a-z0-9_-]*$')
+
+
+def validate_frontend_framework(value: str) -> str:
+ """
+ 校验可用于依赖分类和工程目录的前端框架标识。
+
+ auto 仅用于请求自动解析,不能作为实际框架名称。
+
+ :param value: 待校验的前端框架标识
+ :return: 校验通过的前端框架标识
+ """
+ if not isinstance(value, str) or value == 'auto' or FRONTEND_FRAMEWORK_PATTERN.fullmatch(value) is None:
+ raise ValueError('frontend_framework 必须以小写字母开头,且仅包含小写字母、数字、下划线或连字符,不能为 auto')
+ return value
+
+
+def resolve_frontend_framework(frontend_framework: str = 'auto') -> FrontendFramework:
+ """
+ 解析前端工程使用的框架标识。
+
+ 显式框架优先;auto 模式读取前端框架环境变量,未配置时默认使用 vue3。
+
+ :param frontend_framework: 前端框架标识,auto 表示自动选择
+ :return: 解析后的前端框架标识
+ """
+ framework = (frontend_framework or 'auto').strip().lower()
+ if framework == 'auto':
+ framework = next(
+ (os.environ[name].strip().lower() for name in FRONTEND_FRAMEWORK_ENV_NAMES if os.getenv(name, '').strip()),
+ 'vue3',
+ )
+ return validate_frontend_framework(framework)
+
+
+def resolve_frontend_framework_request(frontend_root: Path | str, requested_framework: str = 'auto') -> str:
+ """
+ 保存前端工程的显式框架选择,供后续依赖、模板与 SDK 按需解析。
+
+ 标准工程的 auto 保留目录框架身份;独立工程的 auto 使用已配置的框架
+ 环境变量,无配置时继续保留 auto,交由包依赖识别。
+
+ :param frontend_root: 已选中的前端工程目录
+ :param requested_framework: 请求的框架标识,auto 表示自动识别
+ :return: 已规范化并校验的框架请求,可以是 auto
+ """
+ root = Path(frontend_root)
+ framework = (requested_framework or 'auto').strip().lower()
+ standard_frontend_root = root.name in {'web', 'mobile'} and root.parent.parent.name == 'ruoyi-fastapi-frontend'
+ if framework == 'auto' and not standard_frontend_root:
+ framework = next(
+ (os.environ[name].strip().lower() for name in FRONTEND_FRAMEWORK_ENV_NAMES if os.getenv(name, '').strip()),
+ 'auto',
+ )
+ return framework if framework == 'auto' else validate_frontend_framework(framework)
+
+
+def select_frontend_root(frontend_root: Path | str, frontend_framework: str = 'auto') -> Path:
+ """
+ 从统一前端目录中选择 Web 工程,保留显式指定的独立工程目录。
+
+ :param frontend_root: 前端工程目录、框架目录或统一前端目录
+ :param frontend_framework: 前端框架标识,auto 表示自动选择
+ :return: 所选前端工程的绝对路径
+ """
+ # 仅转换为绝对路径,保留符号链接信息供 SDK 后续执行路径安全检查。
+ root = Path(os.path.abspath(frontend_root))
+ framework = resolve_frontend_framework(frontend_framework)
+ automatic = (frontend_framework or 'auto').strip().lower() == 'auto'
+ if root.name == 'web' and root.parent.parent.name == 'ruoyi-fastapi-frontend':
+ validate_frontend_framework(root.parent.name)
+ # 显式框架选择同级工程,auto 模式保留当前指定的工程。
+ return root if automatic else root.parent.parent / framework / 'web'
+ if (root / 'package.json').is_file():
+ return root
+ if root.parent.name == 'ruoyi-fastapi-frontend':
+ validate_frontend_framework(root.name)
+ return root / 'web' if automatic else root.parent / framework / 'web'
+ if root.name == 'ruoyi-fastapi-frontend' or (
+ root.is_dir()
+ and any(
+ path.name != 'auto' and FRONTEND_FRAMEWORK_PATTERN.fullmatch(path.name) and (path / 'web').is_dir()
+ for path in root.iterdir()
+ )
+ ):
+ return root / framework / 'web'
+ return root
+
+
+def resolve_frontend_root(
+ backend_root: Path | str,
+ frontend_root: Path | str | None = None,
+ frontend_framework: str = 'auto',
+) -> Path:
+ """
+ 解析前端工程根目录。
+
+ 依次使用显式目录、环境变量和统一前端目录,并兼容同级的旧版独立前端工程。
+ 未发现现存工程时,返回统一前端目录下所选框架的 Web 工程路径。
+
+ :param backend_root: 后端项目根目录
+ :param frontend_root: 显式指定的前端工程目录或统一前端目录
+ :param frontend_framework: 前端框架标识,auto 表示自动选择
+ :return: 解析后的前端工程根目录
+ """
+ backend = Path(backend_root).resolve()
+ configured = frontend_root or next(
+ (os.environ[name].strip() for name in FRONTEND_ROOT_ENV_NAMES if os.getenv(name, '').strip()), None
+ )
+ if configured:
+ return select_frontend_root(configured, frontend_framework)
+ framework = resolve_frontend_framework(frontend_framework)
+ container = backend.parent / 'ruoyi-fastapi-frontend'
+ if container.is_dir():
+ return select_frontend_root(container, framework)
+ # 兼容重命名或单独检出的旧版前端工程。
+ if backend.parent.is_dir():
+ candidates = sorted(
+ path
+ for path in backend.parent.iterdir()
+ if path.is_dir() and path != backend and (path / 'package.json').is_file() and (path / 'plugins').is_dir()
+ )
+ if candidates:
+ return candidates[0]
+ return container / framework / 'web'
+
+
+class PluginFrontendFrameworkResolver:
+ """
+ 前端框架解析器。
+ """
+
+ AUTO = 'auto'
+
+ @classmethod
+ def resolve(cls, frontend_root: Path, requested_framework: str = AUTO) -> FrontendFramework:
+ """
+ 解析宿主依赖及脚手架应使用的前端框架。
+
+ 显式框架优先;标准工程使用目录中的框架标识,独立工程从 package.json
+ 识别底层框架。无法识别的独立工程要求显式指定。
+
+ :param frontend_root: 前端项目根目录
+ :param requested_framework: 前端框架标识,auto 表示自动识别
+ :return: 解析后的前端框架标识
+ """
+ normalized_framework = (requested_framework or cls.AUTO).strip().lower()
+ if normalized_framework != cls.AUTO:
+ return validate_frontend_framework(normalized_framework)
+
+ # 目录标识决定标准工程的依赖分类,避免被底层 Vue 或 React 包覆盖。
+ if frontend_root.name in {'web', 'mobile'} and frontend_root.parent.parent.name == 'ruoyi-fastapi-frontend':
+ return validate_frontend_framework(frontend_root.parent.name)
+
+ package_json_path = frontend_root / 'package.json'
+ if package_json_path.is_file():
+ try:
+ package_payload = json.loads(package_json_path.read_text(encoding='utf-8'))
+ except (OSError, json.JSONDecodeError) as exc:
+ raise ValueError(f'读取前端 package.json 失败:{package_json_path}({exc})') from exc
+ if not isinstance(package_payload, dict):
+ raise ValueError(f'前端 package.json 顶层必须是对象:{package_json_path}')
+
+ dependencies = cls._collect_dependencies(package_payload)
+ vue_version = cls._resolve_vue_dependency(dependencies.get('vue'))
+ if vue_version is not None:
+ return vue_version
+ if 'element-ui' in dependencies:
+ return 'vue2'
+ if 'element-plus' in dependencies:
+ return 'vue3'
+ if 'react' in dependencies:
+ return 'react'
+
+ raise ValueError(f'无法从 {package_json_path} 识别前端框架,请使用 --frontend-framework 显式指定')
+
+ @staticmethod
+ def _collect_dependencies(package_payload: dict[str, object]) -> dict[str, object]:
+ """
+ 合并 dependencies 和 devDependencies。
+
+ :param package_payload: package.json 负载
+ :return: 依赖映射
+ """
+ dependencies: dict[str, object] = {}
+ for key in ('devDependencies', 'dependencies'):
+ section = package_payload.get(key)
+ if isinstance(section, dict):
+ dependencies.update(section)
+ return dependencies
+
+ @staticmethod
+ def _resolve_vue_dependency(version_spec: object) -> FrontendFramework | None:
+ """
+ 从 npm Vue 版本约束中提取主版本。
+
+ :param version_spec: Vue npm 版本约束
+ :return: Vue 版本,无法识别时返回 None
+ """
+ if not isinstance(version_spec, str):
+ return None
+ match = re.search(r'(? dict[str, FrontendDependencyManifest]:
+ """
+ 校验前端依赖分类使用安全的小写框架标识。
+
+ :param value: 按宿主前端框架分类的依赖声明
+ :return: 校验后的前端依赖分类
+ """
+ for framework in value:
+ validate_frontend_framework(framework)
+ return value
+
+ @property
+ def has_frontend_dependencies(self) -> bool:
+ """
+ 判断是否为任一前端框架声明宿主前端依赖。
+
+ :return: 是否存在前端依赖声明
+ """
+ return any(item.npm or item.npm_dev for item in self.frontend.values())
+
+ def resolve_frontend(self, frontend_framework: str) -> FrontendDependencyManifest:
+ """
+ 获取指定前端框架的前端依赖声明副本。
+
+ 返回独立副本,避免调用方修改原始清单;未声明的分类返回空依赖声明。
+
+ :param frontend_framework: 宿主前端框架标识,例如 vue2、vue3、react
+ :return: 指定前端框架的前端依赖声明副本
+ """
+ profile = self.frontend.get(validate_frontend_framework(frontend_framework))
+ return profile.model_copy(deep=True) if profile is not None else FrontendDependencyManifest()
+
@field_validator('python')
@classmethod
def validate_python_requirements(cls, value: list[str]) -> list[str]:
@@ -1111,7 +1162,7 @@ class PluginManifest(BaseModel):
:return: None
"""
- has_frontend_resources = bool(self.frontend.menus or self.dependencies.npm or self.dependencies.npm_dev)
+ has_frontend_resources = bool(self.frontend.menus or self.dependencies.has_frontend_dependencies)
if has_frontend_resources and self.frontend.delivery.type == 'none':
self.frontend.delivery.type = 'source'
if self.frontend.delivery.type == 'source':
diff --git a/ruoyi-fastapi-backend/plugins/core/manifest/v2.py b/ruoyi-fastapi-backend/plugins/core/manifest/v2.py
index f290b7f..1bcde75 100644
--- a/ruoyi-fastapi-backend/plugins/core/manifest/v2.py
+++ b/ruoyi-fastapi-backend/plugins/core/manifest/v2.py
@@ -231,7 +231,7 @@ class PluginManifestV2(PluginManifest):
raise ValueError('bundle 交付必须声明 frontend.bundle')
if self.frontend.delivery.build_required:
raise ValueError('bundle 不参与宿主构建,buildRequired 必须为 false')
- if self.dependencies.npm or self.dependencies.npm_dev:
+ if self.dependencies.has_frontend_dependencies:
raise ValueError('bundle 的 npm 构建依赖由插件独立管理,不能安装到宿主')
def _validate_menu_components(self) -> None:
diff --git a/ruoyi-fastapi-backend/plugins/core/runtime/service/dependencies.py b/ruoyi-fastapi-backend/plugins/core/runtime/service/dependencies.py
index d3210ec..643cbba 100644
--- a/ruoyi-fastapi-backend/plugins/core/runtime/service/dependencies.py
+++ b/ruoyi-fastapi-backend/plugins/core/runtime/service/dependencies.py
@@ -241,7 +241,9 @@ class PluginDependencyUseCase:
frontend_root=Path(self.dependencies.runtime_environment.get_frontend_dir())
).build_plan(dependency_result)
resolved_policy_config = policy_config or DependencyInstallPolicyConfig.from_environment()
- policy_decision = DependencyInstallPolicyEvaluator(resolved_policy_config).evaluate(
+ policy_decision = DependencyInstallPolicyEvaluator(
+ resolved_policy_config, runtime_environment=self.dependencies.runtime_environment
+ ).evaluate(
install_plan,
confirmed=confirmed,
)
@@ -345,7 +347,9 @@ class PluginDependencyUseCase:
frontend_root=Path(self.dependencies.runtime_environment.get_frontend_dir())
).build_plan(dependency_result)
resolved_policy_config = policy_config or DependencyInstallPolicyConfig.from_environment()
- policy_decision = DependencyInstallPolicyEvaluator(resolved_policy_config).evaluate(
+ policy_decision = DependencyInstallPolicyEvaluator(
+ resolved_policy_config, runtime_environment=self.dependencies.runtime_environment
+ ).evaluate(
install_plan,
confirmed=confirmed,
)
diff --git a/ruoyi-fastapi-backend/plugins/core/runtime/service/facade.py b/ruoyi-fastapi-backend/plugins/core/runtime/service/facade.py
index 50ce7e5..625883a 100644
--- a/ruoyi-fastapi-backend/plugins/core/runtime/service/facade.py
+++ b/ruoyi-fastapi-backend/plugins/core/runtime/service/facade.py
@@ -88,6 +88,7 @@ class PluginRuntimeService:
resolved_dependency_checker = dependency_checker or PluginDependencyChecker(
npm_inspector=NpmDependencyInspector(frontend_root=resolved_environment.get_frontend_dir()),
frontend_mode=resolved_environment.get_frontend_mode(),
+ runtime_environment=resolved_environment,
)
gateway_overrides = gateways or PluginRuntimeGatewayOverrides()
self._replace_dependencies(
@@ -159,6 +160,7 @@ class PluginRuntimeService:
frontend_root=self.dependencies.runtime_environment.get_frontend_dir(),
),
frontend_mode=self.dependencies.runtime_environment.get_frontend_mode(),
+ runtime_environment=self.dependencies.runtime_environment,
)
)
diff --git a/ruoyi-fastapi-backend/plugins/core/runtime/support/payload/documentation.py b/ruoyi-fastapi-backend/plugins/core/runtime/support/payload/documentation.py
index 95d1d43..e1356ef 100644
--- a/ruoyi-fastapi-backend/plugins/core/runtime/support/payload/documentation.py
+++ b/ruoyi-fastapi-backend/plugins/core/runtime/support/payload/documentation.py
@@ -174,28 +174,26 @@ class PluginDocumentationBuilder:
@staticmethod
def _build_dependency_section(discovered_plugin: DiscoveredPlugin) -> list[str]:
"""
- 构建依赖文档段落。
+ 构建依赖文档段落,按前端框架分别展示依赖分类。
:param discovered_plugin: 已发现插件
:return: Markdown 行列表
"""
dependencies = discovered_plugin.manifest.dependencies
lines = ['## 依赖', '']
- if not dependencies.python and not dependencies.npm and not dependencies.npm_dev and not dependencies.plugins:
+ if not dependencies.python and not dependencies.has_frontend_dependencies and not dependencies.plugins:
return [*lines, '无依赖声明。', '']
lines.append('### Python')
lines.extend(f'- `{item}`' for item in dependencies.python)
if not dependencies.python:
lines.append('- 无')
- lines.extend(['', '### NPM'])
- lines.extend(f'- `{item}`' for item in dependencies.npm)
- if not dependencies.npm:
- lines.append('- 无')
- lines.extend(['', '### NPM 开发依赖'])
- lines.extend(f'- `{item}`' for item in dependencies.npm_dev)
- if not dependencies.npm_dev:
- lines.append('- 无')
+ for framework, profile in dependencies.frontend.items():
+ lines.extend(['', f'### {framework} 宿主依赖'])
+ lines.extend(f'- npm: `{item}`' for item in profile.npm)
+ lines.extend(f'- npmDev: `{item}`' for item in profile.npm_dev)
+ if not profile.npm and not profile.npm_dev:
+ lines.append('- 无')
lines.extend(['', '### 插件'])
lines.extend(
f'- `{item.id}` {item.version or ""} {item.description or ""}'.rstrip() for item in dependencies.plugins
diff --git a/ruoyi-fastapi-backend/plugins/core/validation/dependencies.py b/ruoyi-fastapi-backend/plugins/core/validation/dependencies.py
index c770fed..11fd9e3 100644
--- a/ruoyi-fastapi-backend/plugins/core/validation/dependencies.py
+++ b/ruoyi-fastapi-backend/plugins/core/validation/dependencies.py
@@ -8,7 +8,7 @@ from typing import Literal
from packaging.requirements import InvalidRequirement
-from plugins.core.environment import PLUGIN_RUNTIME_ENVIRONMENT
+from plugins.core.environment import PLUGIN_RUNTIME_ENVIRONMENT, PluginRuntimeEnvironmentService
from plugins.core.manifest.schema import PluginManifest
from plugins.core.validation.python_requirements import PythonRequirementParser
from plugins.core.validation.versioning import PluginVersionComparator, PluginVersionConstraintMatcher
@@ -534,32 +534,47 @@ class PluginDependencyChecker:
python_inspector: PythonDependencyInspector | None = None,
npm_inspector: NpmDependencyInspector | None = None,
frontend_mode: Literal['dev', 'built'] = 'dev',
+ runtime_environment: PluginRuntimeEnvironmentService | None = None,
) -> None:
"""
初始化插件依赖检查器。
:param python_inspector: Python 依赖检查器
- :param npm_inspector: npm 依赖检查器
+ :param npm_inspector: 当前宿主前端的 npm 依赖检查器
+ :param frontend_mode: 前端运行模式,已构建模式跳过 npm 依赖实际检查
+ :param runtime_environment: 保留当前宿主显式框架选择的运行时环境
+ :return: None
"""
+ self.runtime_environment = runtime_environment or PLUGIN_RUNTIME_ENVIRONMENT
self.python_inspector = python_inspector or PythonDependencyInspector()
- self.npm_inspector = npm_inspector or NpmDependencyInspector()
+ self.npm_inspector = npm_inspector or NpmDependencyInspector(
+ frontend_root=self.runtime_environment.get_frontend_dir()
+ )
self.frontend_mode = frontend_mode
def check_manifest(self, manifest: PluginManifest) -> DependencyCheckResult:
"""
检查插件清单依赖。
+ 根据当前宿主前端框架选择 frontend 中对应分类,只检查该分类声明的前端依赖。
+
:param manifest: 插件清单
:return: 插件依赖检查结果
"""
items = []
items.extend(self.python_inspector.check(manifest.dependencies.python))
+ frontend_framework = (
+ self.runtime_environment.get_frontend_framework(self.npm_inspector.frontend_root)
+ if manifest.dependencies.frontend
+ else 'vue3'
+ )
+ frontend_dependencies = manifest.dependencies.resolve_frontend(frontend_framework)
if self.frontend_mode == 'built':
- items.extend(self.npm_inspector.skip(manifest.dependencies.npm))
- items.extend(self.npm_inspector.skip(manifest.dependencies.npm_dev, dev=True))
+ items.extend(self.npm_inspector.skip(frontend_dependencies.npm))
+ items.extend(self.npm_inspector.skip(frontend_dependencies.npm_dev, dev=True))
else:
- items.extend(self.npm_inspector.check(manifest.dependencies.npm))
- items.extend(self.npm_inspector.check(manifest.dependencies.npm_dev, dev=True))
+ items.extend(self.npm_inspector.check(frontend_dependencies.npm))
+ items.extend(self.npm_inspector.check(frontend_dependencies.npm_dev, dev=True))
return DependencyCheckResult(plugin_id=manifest.id, items=items)
diff --git a/ruoyi-fastapi-backend/plugins/core/validation/dependency_policy.py b/ruoyi-fastapi-backend/plugins/core/validation/dependency_policy.py
index b7feb85..abf242f 100644
--- a/ruoyi-fastapi-backend/plugins/core/validation/dependency_policy.py
+++ b/ruoyi-fastapi-backend/plugins/core/validation/dependency_policy.py
@@ -10,6 +10,8 @@ import yaml
from packaging.requirements import InvalidRequirement
from pydantic import ValidationError
+from plugins.core.environment import PLUGIN_RUNTIME_ENVIRONMENT, PluginRuntimeEnvironmentService
+from plugins.core.frontend import validate_frontend_framework
from plugins.core.validation.dependencies import (
DependencyInstallPlan,
DependencyInstallPlanItem,
@@ -465,10 +467,15 @@ class DependencyLockEntry:
class DependencyLockfile:
"""
插件依赖锁文件。
+
+ :param path: 锁文件路径
+ :param entries: 按依赖类型和包名索引的锁定依赖
+ :param frontend_framework: 锁文件对应的宿主前端框架,未声明时不检查框架
"""
path: Path
entries: dict[tuple[DependencyKind, str], DependencyLockEntry]
+ frontend_framework: str | None = None
@classmethod
def load(cls, path: Path | str | None) -> 'DependencyLockfile | None':
@@ -477,6 +484,7 @@ class DependencyLockfile:
:param path: 锁文件路径
:return: 锁文件对象,缺失时返回 None
+ :raises ValueError: 锁文件使用旧框架字段或包含无效的前端框架标识
"""
if path is None:
return None
@@ -484,6 +492,15 @@ class DependencyLockfile:
if not lockfile_path.is_file():
return None
data = yaml.safe_load(lockfile_path.read_text(encoding='utf-8')) or {}
+ if not isinstance(data, dict):
+ raise ValueError('依赖锁文件顶层必须是对象')
+ if 'frontendVersion' in data:
+ raise ValueError('依赖锁文件 frontendVersion 已移除,请迁移为 frontendFramework 或重新生成锁文件')
+ frontend_framework = None
+ if 'frontendFramework' in data:
+ if not isinstance(data['frontendFramework'], str):
+ raise ValueError('依赖锁文件 frontendFramework 必须是小写框架标识')
+ frontend_framework = validate_frontend_framework(data['frontendFramework'])
entries: dict[tuple[DependencyKind, str], DependencyLockEntry] = {}
for kind in ('python', 'npm', 'npmDev'):
for raw_entry in data.get(kind, []) or []:
@@ -502,7 +519,7 @@ class DependencyLockfile:
)
entries[(kind, normalize_dependency_name(kind, name))] = entry
- return cls(path=lockfile_path, entries=entries)
+ return cls(path=lockfile_path, entries=entries, frontend_framework=frontend_framework)
def get_entry(self, item: DependencyInstallPlanItem) -> DependencyLockEntry | None:
"""
@@ -728,14 +745,21 @@ class DependencyInstallPolicyEvaluator:
插件依赖安装策略判定器。
"""
- def __init__(self, config: DependencyInstallPolicyConfig | None = None) -> None:
+ def __init__(
+ self,
+ config: DependencyInstallPolicyConfig | None = None,
+ *,
+ runtime_environment: PluginRuntimeEnvironmentService | None = None,
+ ) -> None:
"""
初始化策略判定器。
:param config: 策略配置
+ :param runtime_environment: 保留当前宿主显式框架选择的运行时环境
:return: None
"""
self.config = config or DependencyInstallPolicyConfig.from_environment()
+ self.runtime_environment = runtime_environment or PLUGIN_RUNTIME_ENVIRONMENT
def evaluate(
self, install_plan: DependencyInstallPlan, *, confirmed: bool = False
@@ -977,16 +1001,16 @@ class DependencyInstallPolicyEvaluator:
return True
return version_satisfies_range(lock_entry.resolved_version, required_version)
- @staticmethod
def _build_lockfile_extra_reasons(
+ self,
lockfile: DependencyLockfile | None,
install_plan: DependencyInstallPlan,
) -> list[str]:
"""
- 构建锁文件额外安装项阻断原因。
+ 构建锁文件宿主框架及额外依赖的阻断原因。
:param lockfile: 锁文件
- :param install_plan: 安装计划
+ :param install_plan: 安装计划,包含前端依赖的目标工作目录
:return: 阻断原因
"""
if lockfile is None:
@@ -996,6 +1020,18 @@ class DependencyInstallPolicyEvaluator:
for item in install_plan.items
}
reasons = []
+ if lockfile.frontend_framework:
+ frontend_roots = {item.workdir for item in install_plan.items if item.kind in {'npm', 'npmDev'}}
+ for frontend_root in sorted(frontend_roots):
+ try:
+ frontend_framework = self.runtime_environment.get_frontend_framework(Path(frontend_root))
+ except ValueError as exc:
+ reasons.append(str(exc))
+ continue
+ if frontend_framework != lockfile.frontend_framework:
+ reasons.append(
+ f'锁文件前端框架不匹配:lock={lockfile.frontend_framework} host={frontend_framework}'
+ )
for entry in lockfile.entries.values():
key = (entry.kind, normalize_dependency_name(entry.kind, entry.name), entry.requirement)
if key not in plan_keys:
diff --git a/ruoyi-fastapi-backend/plugins/core/validation/manifest.py b/ruoyi-fastapi-backend/plugins/core/validation/manifest.py
index 5aede7d..496310b 100644
--- a/ruoyi-fastapi-backend/plugins/core/validation/manifest.py
+++ b/ruoyi-fastapi-backend/plugins/core/validation/manifest.py
@@ -185,14 +185,19 @@ class PluginManifestChecker:
"""
检查未声明版本约束的依赖。
+ 检查 Python 依赖及 frontend 中所有前端框架分类,不依赖当前宿主选择。
+
:param manifest: 插件 manifest
:return: 未声明版本约束问题项列表
"""
issues = []
for dependency_kind, requirements in (
('python', manifest.dependencies.python),
- ('npm', manifest.dependencies.npm),
- ('npmDev', manifest.dependencies.npm_dev),
+ *[
+ (f'frontend.{framework}.{kind}', requirements)
+ for framework, profile in manifest.dependencies.frontend.items()
+ for kind, requirements in (('npm', profile.npm), ('npmDev', profile.npm_dev))
+ ],
):
for index, requirement in enumerate(requirements):
if dependency_kind == 'python':
diff --git a/ruoyi-fastapi-backend/plugins/examples/python/bundle_demo/README.md b/ruoyi-fastapi-backend/plugins/examples/python/bundle_demo/README.md
index 42a1284..89680c6 100644
--- a/ruoyi-fastapi-backend/plugins/examples/python/bundle_demo/README.md
+++ b/ruoyi-fastapi-backend/plugins/examples/python/bundle_demo/README.md
@@ -15,12 +15,12 @@ npm --prefix plugins/examples/python/bundle_demo/web install
npm --prefix plugins/examples/python/bundle_demo/web run build
```
-首次安装会生成此示例自己的锁文件;独立维护插件时应保留锁文件,在 CI 中使用 `npm ci`。示例的 SDK 别名指向本仓库 `ruoyi-fastapi-frontend/src/utils/pluginBridge.js`。迁移到独立插件仓库时,将该模块及相邻的 `pluginBridge.d.ts` 一起复制为自己的版本化源码依赖,并调整 `vite.config.js` 中 `@ruoyi/plugin-bridge` 的别名。TypeScript 项目还应在自己的 `tsconfig.json` 中将同一别名映射到复制后的模块路径。
+首次安装会生成此示例自己的锁文件;独立维护插件时应保留锁文件,在 CI 中使用 `npm ci`。示例的 SDK 别名指向本仓库 `ruoyi-fastapi-frontend/vue3/web/src/utils/pluginBridge.js`。迁移到独立插件仓库时,将该模块及相邻的 `pluginBridge.d.ts` 一起复制为自己的版本化源码依赖,并调整 `vite.config.js` 中 `@ruoyi/plugin-bridge` 的别名。TypeScript 项目还应在自己的 `tsconfig.json` 中将同一别名映射到复制后的模块路径。
如果本仓库宿主前端依赖已安装,也可复用其中的 Vite 可执行文件构建**同一个插件前端项目**:
```bash
-node ../ruoyi-fastapi-frontend/node_modules/vite/bin/vite.js build plugins/examples/python/bundle_demo/web --config plugins/examples/python/bundle_demo/web/vite.config.js
+node ../ruoyi-fastapi-frontend/vue3/web/node_modules/vite/bin/vite.js build plugins/examples/python/bundle_demo/web --config plugins/examples/python/bundle_demo/web/vite.config.js
```
两种方式均输出 `plugins/examples/python/bundle_demo/web/dist`。配置使用 `base: './'`、关闭 sourcemap;宿主交付 HTML 时注入有效 `` 和运行基址,因此资源和页面深层链接不需写死域名或代理前缀。直接用 `file://` 打开 dist 不会建立宿主身份桥;真实账号交互应从管理平台的插件菜单进入。
@@ -233,7 +233,7 @@ ASGI 网关对已建立的 SSE/WebSocket 按 15 秒间隔复核登录、插件
```bash
python -m pytest tests/plugins/core/runtime/test_bundle.py tests/plugins/core/runtime/test_browser_session.py tests/plugins/core/runtime/test_bundle_transport.py tests/plugins/core/runtime/test_bundle_files.py tests/plugins/core/runtime/test_bundle_events.py tests/plugins/core/runtime/test_connections.py tests/plugins/core/runtime/test_connection_sessions.py tests/plugins/core/runtime/test_configuration.py tests/module_admin/service/test_login_plugin_routes.py -q
-npm --prefix ../ruoyi-fastapi-frontend run test:plugin
+npm --prefix ../ruoyi-fastapi-frontend/vue3/web run test:plugin
```
测试覆盖静态边界、Cookie/CSRF/退出、真实 RSA/AES 协议、文件大小、`root_path`、流连接复核及运行时关闭顺序;浏览器会话测试替换真实账号查询,配置读取测试使用隔离 SQLite 验证保存、解密及重启版本。实际角色配置、TLS/Nginx、多 worker 和 ASGI 服务器的优雅停机阶段仍需部署验收。网关的周期关闭与协作式取消不能强制终止忽略取消的 Python 代码,也不提供跨 worker 的即时撤销广播。
diff --git a/ruoyi-fastapi-backend/plugins/examples/python/bundle_demo/web/vite.config.js b/ruoyi-fastapi-backend/plugins/examples/python/bundle_demo/web/vite.config.js
index 7b87f88..55f3a43 100644
--- a/ruoyi-fastapi-backend/plugins/examples/python/bundle_demo/web/vite.config.js
+++ b/ruoyi-fastapi-backend/plugins/examples/python/bundle_demo/web/vite.config.js
@@ -27,7 +27,7 @@ export default {
alias: {
'@ruoyi/plugin-bridge': fileURLToPath(
new URL(
- '../../../../../../ruoyi-fastapi-frontend/src/utils/pluginBridge.js',
+ '../../../../../../ruoyi-fastapi-frontend/vue3/web/src/utils/pluginBridge.js',
import.meta.url
)
),
diff --git a/ruoyi-fastapi-backend/plugins/examples/python/task_demo/web/vite.config.js b/ruoyi-fastapi-backend/plugins/examples/python/task_demo/web/vite.config.js
index 32ac176..7e471c2 100644
--- a/ruoyi-fastapi-backend/plugins/examples/python/task_demo/web/vite.config.js
+++ b/ruoyi-fastapi-backend/plugins/examples/python/task_demo/web/vite.config.js
@@ -27,7 +27,7 @@ export default {
alias: {
'@ruoyi/plugin-bridge': fileURLToPath(
new URL(
- '../../../../../../ruoyi-fastapi-frontend/src/utils/pluginBridge.js',
+ '../../../../../../ruoyi-fastapi-frontend/vue3/web/src/utils/pluginBridge.js',
import.meta.url
)
),
diff --git a/ruoyi-fastapi-backend/pyproject.toml b/ruoyi-fastapi-backend/pyproject.toml
index 19973ac..6fd99e0 100644
--- a/ruoyi-fastapi-backend/pyproject.toml
+++ b/ruoyi-fastapi-backend/pyproject.toml
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "ruoyi-fastapi-backend-cli"
version = "1.10.0"
-description = "RuoYi FastAPI backend internal CLI entrypoint"
+description = "RuoYi-FastAPI backend internal CLI entrypoint"
requires-python = ">=3.10"
[project.scripts]
@@ -13,6 +13,12 @@ ruoyi = "cli.main:main"
[tool.pytest.ini_options]
pythonpath = ["."]
+markers = [
+ "browser: requires a real browser and the frontend network test host",
+ "native: requires a prebuilt native plugin artifact",
+ "integration: requires explicitly enabled external database or cache services",
+ "contract: validates public CLI arguments, output, or cold-process behavior",
+]
[tool.setuptools.packages.find]
include = ["cli*"]
diff --git a/ruoyi-fastapi-backend/requirements-test.txt b/ruoyi-fastapi-backend/requirements-test.txt
new file mode 100644
index 0000000..c78db4b
--- /dev/null
+++ b/ruoyi-fastapi-backend/requirements-test.txt
@@ -0,0 +1,4 @@
+pytest==9.1.1
+pytest-asyncio==1.4.0
+aiosqlite==0.22.1
+PyYAML==6.0.3
diff --git a/ruoyi-fastapi-backend/tests/cli/root/conftest.py b/ruoyi-fastapi-backend/tests/cli/root/conftest.py
index 2f322f1..2a0716b 100644
--- a/ruoyi-fastapi-backend/tests/cli/root/conftest.py
+++ b/ruoyi-fastapi-backend/tests/cli/root/conftest.py
@@ -2,12 +2,14 @@ import json
import os
import subprocess
import sys
-from collections.abc import Callable
+from collections.abc import Callable, Mapping
from pathlib import Path
from typing import Any
import pytest
+CLI_PROCESS_TIMEOUT_SECONDS = 60
+
@pytest.fixture
def backend_dir() -> Path:
@@ -16,13 +18,16 @@ def backend_dir() -> Path:
@pytest.fixture
def run_cli_command(backend_dir: Path) -> Callable[..., subprocess.CompletedProcess[str]]:
- def _run_cli_command(*args: str) -> subprocess.CompletedProcess[str]:
+ def _run_cli_command(*args: str, env: Mapping[str, str] | None = None) -> subprocess.CompletedProcess[str]:
return subprocess.run(
- [sys.executable, '-m', 'cli.main', *args],
+ [sys.executable, '-X', 'utf8', '-m', 'cli.main', *args],
cwd=backend_dir,
+ env=env,
capture_output=True,
text=True,
+ encoding='utf-8',
check=False,
+ timeout=CLI_PROCESS_TIMEOUT_SECONDS,
)
return _run_cli_command
@@ -49,11 +54,13 @@ def run_cli_completion_command(
instruction: str = 'bash_complete',
) -> subprocess.CompletedProcess[str]:
return subprocess.run(
- [sys.executable, '-m', 'cli.main'],
+ [sys.executable, '-X', 'utf8', '-m', 'cli.main'],
cwd=backend_dir,
capture_output=True,
text=True,
+ encoding='utf-8',
check=False,
+ timeout=CLI_PROCESS_TIMEOUT_SECONDS,
env={
**os.environ,
'COMP_WORDS': comp_words,
@@ -65,6 +72,29 @@ def run_cli_completion_command(
return _run_cli_completion_command
+@pytest.fixture
+def untracked_bundle_project(tmp_path: Path) -> Path:
+ """
+ 创建带未跟踪 SDK 的隔离 bundle 工程,供 CLI 检查与更新测试复用。
+
+ :param tmp_path: pytest 临时目录
+ :return: 不包含可执行业务插件的测试工程目录
+ """
+ root = tmp_path / 'example'
+ vendor = root / 'web' / 'vendor'
+ vendor.mkdir(parents=True)
+ (root / 'plugin.yaml').write_text(
+ 'manifestVersion: 2\nid: example\nname: Example\nversion: 1.0.0\n'
+ 'backend:\n module: plugins.example\n entrypoint: plugins.example:create_plugin\n integration: asgi\n'
+ 'frontend:\n delivery:\n type: bundle\n',
+ encoding='utf-8',
+ )
+ (root / '__init__.py').write_text('raise RuntimeError("must never import this project")\n', encoding='utf-8')
+ for name in ('pluginBridge.js', 'pluginBridge.d.ts'):
+ (vendor / name).write_text('old untracked SDK\n', encoding='utf-8')
+ return root
+
+
@pytest.fixture
def parse_json_stdout() -> Callable[[subprocess.CompletedProcess[str]], dict[str, Any]]:
def _parse_json_stdout(completed: subprocess.CompletedProcess[str]) -> dict[str, Any]:
diff --git a/ruoyi-fastapi-backend/tests/cli/root/test_bootstrap.py b/ruoyi-fastapi-backend/tests/cli/root/test_bootstrap.py
index c13565b..6cb1c3c 100644
--- a/ruoyi-fastapi-backend/tests/cli/root/test_bootstrap.py
+++ b/ruoyi-fastapi-backend/tests/cli/root/test_bootstrap.py
@@ -4,6 +4,9 @@ import subprocess
import sys
from pathlib import Path
+import pytest
+
+pytestmark = pytest.mark.contract
BACKEND_DIR = Path(__file__).resolve().parents[3]
@@ -31,6 +34,7 @@ print(json.dumps({
capture_output=True,
text=True,
check=False,
+ timeout=30,
)
assert completed.returncode == 0, completed.stderr
@@ -78,6 +82,7 @@ print(json.dumps({
capture_output=True,
text=True,
check=False,
+ timeout=30,
)
assert completed.returncode == 0, completed.stderr
@@ -143,6 +148,7 @@ print(json.dumps({
capture_output=True,
text=True,
check=False,
+ timeout=30,
)
assert completed.returncode == 0, completed.stderr
@@ -204,6 +210,7 @@ print(json.dumps({
capture_output=True,
text=True,
check=False,
+ timeout=30,
)
assert completed.returncode == 0, completed.stderr
diff --git a/ruoyi-fastapi-backend/tests/cli/root/test_contract_app_ops.py b/ruoyi-fastapi-backend/tests/cli/root/test_contract_app_ops.py
index 810333d..ce6875b 100644
--- a/ruoyi-fastapi-backend/tests/cli/root/test_contract_app_ops.py
+++ b/ruoyi-fastapi-backend/tests/cli/root/test_contract_app_ops.py
@@ -5,10 +5,13 @@ from collections.abc import Callable
from pathlib import Path
from textwrap import dedent
-from cli.exit_codes import DEPENDENCY_ERROR, SUCCESS
-from cli.runtime.plugin.scaffold import PluginFrontendVersionResolver
+import pytest
-FRONTEND_ROOT = Path(__file__).resolve().parents[4] / 'ruoyi-fastapi-frontend'
+from cli.exit_codes import ARGUMENT_ERROR, DEPENDENCY_ERROR, SUCCESS
+from cli.runtime.plugin.scaffold import PluginFrontendFrameworkResolver
+
+pytestmark = pytest.mark.contract
+FRONTEND_ROOT = Path(__file__).resolve().parents[4] / 'ruoyi-fastapi-frontend' / 'vue3' / 'web'
def test_app_config_json_output_is_pure_json(
@@ -469,7 +472,7 @@ def test_plugin_create_dry_run_json_output_has_stable_contract(
assert payload['dryRun'] is True
assert payload['backend'] is True
assert payload['frontend'] is True
- assert payload['frontendVersion'] == PluginFrontendVersionResolver.resolve(FRONTEND_ROOT)
+ assert payload['frontendFramework'] == PluginFrontendFrameworkResolver.resolve(FRONTEND_ROOT)
assert payload['migration'] is True
assert payload['seed'] is True
assert payload['job'] is True
@@ -508,7 +511,7 @@ def test_plugin_create_optional_parts_json_output_has_stable_contract(
assert payload['config'] is False
-def test_plugin_create_frontend_version_override_json_output_has_stable_contract(
+def test_plugin_create_frontend_framework_override_json_output_has_stable_contract(
run_cli_command: Callable[..., subprocess.CompletedProcess[str]],
parse_json_stdout: Callable[[subprocess.CompletedProcess[str]], dict],
) -> None:
@@ -518,7 +521,7 @@ def test_plugin_create_frontend_version_override_json_output_has_stable_contract
'contract_vue2',
'--dry-run',
'--frontend-only',
- '--frontend-version=vue2',
+ '--frontend-framework=vue2',
'--env=dev',
'--output=json',
)
@@ -527,10 +530,38 @@ def test_plugin_create_frontend_version_override_json_output_has_stable_contract
view_payload = next(file for file in payload['files'] if str(file['path']).endswith('/views/index.vue'))
assert completed.returncode == SUCCESS
assert payload['ok'] is True
- assert payload['frontendVersion'] == 'vue2'
+ assert payload['frontendFramework'] == 'vue2'
+ assert FRONTEND_ROOT.parents[1] / 'vue2/web/plugins/contract_vue2' in [Path(path) for path in payload['targetDirs']]
assert 'slot="header"' in view_payload['content']
+def test_plugin_create_rejects_framework_outside_fixed_choices(
+ run_cli_command: Callable[..., subprocess.CompletedProcess[str]],
+) -> None:
+ """校验根命令在参数解析阶段拒绝不属于固定选项的 React 框架。"""
+ completed = run_cli_command(
+ 'plugin',
+ 'create',
+ 'contract_react',
+ '--dry-run',
+ '--frontend-only',
+ '--frontend-framework=react',
+ '--env=dev',
+ '--output=json',
+ )
+ assert completed.returncode == ARGUMENT_ERROR
+ assert 'Invalid value' in completed.stderr
+ assert 'react' in completed.stderr
+
+
+def test_plugin_create_rejects_removed_frontend_version_option(
+ run_cli_command: Callable[..., subprocess.CompletedProcess[str]],
+) -> None:
+ """校验创建命令不再接受已移除的前端版本选项。"""
+ completed = run_cli_command('plugin', 'create', 'contract_old_option', '--dry-run', '--frontend-version=vue3')
+ assert completed.returncode != SUCCESS
+
+
def test_plugin_install_dry_run_json_output_has_stable_contract(
run_cli_command: Callable[..., subprocess.CompletedProcess[str]],
parse_json_stdout: Callable[[subprocess.CompletedProcess[str]], dict],
diff --git a/ruoyi-fastapi-backend/tests/cli/root/test_contract_cli.py b/ruoyi-fastapi-backend/tests/cli/root/test_contract_cli.py
index 6ac66d7..e5f6570 100644
--- a/ruoyi-fastapi-backend/tests/cli/root/test_contract_cli.py
+++ b/ruoyi-fastapi-backend/tests/cli/root/test_contract_cli.py
@@ -2,8 +2,12 @@ import subprocess
from collections.abc import Callable
from pathlib import Path
+import pytest
+
from cli.exit_codes import SUCCESS
+pytestmark = pytest.mark.contract
+
def test_root_help_shows_commands_without_completion_options(
run_cli_command: Callable[..., subprocess.CompletedProcess[str]],
diff --git a/ruoyi-fastapi-backend/tests/cli/root/test_contract_config.py b/ruoyi-fastapi-backend/tests/cli/root/test_contract_config.py
index d7f00c3..81a0adb 100644
--- a/ruoyi-fastapi-backend/tests/cli/root/test_contract_config.py
+++ b/ruoyi-fastapi-backend/tests/cli/root/test_contract_config.py
@@ -1,8 +1,12 @@
import subprocess
from collections.abc import Callable
+import pytest
+
from cli.exit_codes import DATABASE_ERROR, REDIS_ERROR, RUNTIME_ERROR, SUCCESS
+pytestmark = pytest.mark.contract
+
def test_config_doctor_json_output_has_stable_contract(
run_cli_command: Callable[..., subprocess.CompletedProcess[str]],
diff --git a/ruoyi-fastapi-backend/tests/cli/root/test_contract_db_cache_dev.py b/ruoyi-fastapi-backend/tests/cli/root/test_contract_db_cache_dev.py
index 1e5ed95..45c7b96 100644
--- a/ruoyi-fastapi-backend/tests/cli/root/test_contract_db_cache_dev.py
+++ b/ruoyi-fastapi-backend/tests/cli/root/test_contract_db_cache_dev.py
@@ -2,8 +2,11 @@ import subprocess
from collections.abc import Callable
from pathlib import Path
+import pytest
+
from cli.exit_codes import DATABASE_ERROR, GUARD_REJECTED, REDIS_ERROR, RUNTIME_ERROR, SUCCESS
+pytestmark = pytest.mark.contract
DB_HISTORY_TEST_LIMIT = 5
diff --git a/ruoyi-fastapi-backend/tests/cli/root/test_contract_job_gen.py b/ruoyi-fastapi-backend/tests/cli/root/test_contract_job_gen.py
index f713da5..0426aaf 100644
--- a/ruoyi-fastapi-backend/tests/cli/root/test_contract_job_gen.py
+++ b/ruoyi-fastapi-backend/tests/cli/root/test_contract_job_gen.py
@@ -1,8 +1,11 @@
import subprocess
from collections.abc import Callable
+import pytest
+
from cli.exit_codes import ARGUMENT_ERROR, DATABASE_ERROR, GUARD_REJECTED, RUNTIME_ERROR, SUCCESS
+pytestmark = pytest.mark.contract
GEN_TEST_TABLE_NAME = 'demo_table'
GEN_CREATE_SQL = 'CREATE TABLE demo_cli_test (id bigint);'
MISSING_JOB_ID = 999999999
diff --git a/ruoyi-fastapi-backend/tests/cli/root/test_plugin_artifact_commands.py b/ruoyi-fastapi-backend/tests/cli/root/test_plugin_artifact_commands.py
index ba48c56..707dd7e 100644
--- a/ruoyi-fastapi-backend/tests/cli/root/test_plugin_artifact_commands.py
+++ b/ruoyi-fastapi-backend/tests/cli/root/test_plugin_artifact_commands.py
@@ -20,6 +20,7 @@ from cli.exit_codes import ARGUMENT_ERROR, GUARD_REJECTED
from cli.groups.plugin.artifact_controller import PluginArtifactCommandController
from cli.groups.plugin.commands.artifact import register_artifact_commands
+pytestmark = pytest.mark.contract
BACKEND_ROOT = Path(__file__).resolve().parents[3]
DIGEST = 'a' * 64
GENERATION = 'b' * 32
@@ -257,7 +258,7 @@ def test_help_keeps_config_crypto_database_and_plugin_code_unloaded(help_argumen
f'print(json.dumps({{"exit":r.exit_code,"loaded":{{name:name in sys.modules for name in {observed!r}}}}}))'
)
result = subprocess.run(
- [sys.executable, '-c', script], cwd=BACKEND_ROOT, capture_output=True, text=True, check=True
+ [sys.executable, '-c', script], cwd=BACKEND_ROOT, capture_output=True, text=True, check=True, timeout=30
)
payload = json.loads(result.stdout)
assert payload['exit'] == 0
diff --git a/ruoyi-fastapi-backend/tests/cli/root/test_plugin_create_templates.py b/ruoyi-fastapi-backend/tests/cli/root/test_plugin_create_templates.py
index 4fd5c4e..d2ea521 100644
--- a/ruoyi-fastapi-backend/tests/cli/root/test_plugin_create_templates.py
+++ b/ruoyi-fastapi-backend/tests/cli/root/test_plugin_create_templates.py
@@ -4,9 +4,12 @@ import pytest
import typer
from typer.testing import CliRunner
+from cli.exit_codes import ARGUMENT_ERROR, SUCCESS
from cli.groups.plugin.commands.developer import register_developer_commands
from cli.groups.plugin.options import PluginCreateCommandOptions
+pytestmark = pytest.mark.contract
+
@pytest.mark.parametrize('template', ['python-asgi', 'python-bundle', 'rust-asgi', 'rust-bundle'])
def test_create_cli_forwards_v2_template_without_changing_default_options(template: str) -> None:
@@ -47,3 +50,46 @@ def test_create_cli_defaults_to_existing_v1_full_stack_template() -> None:
result = CliRunner().invoke(app, ['create', 'legacy_demo'])
assert result.exit_code == 0
assert recorded == [PluginCreateCommandOptions()]
+
+
+@pytest.mark.parametrize('framework', ['auto', 'vue2', 'vue3'])
+def test_create_cli_accepts_fixed_frontend_framework_choices(framework: str) -> None:
+ """校验创建命令接受固定框架选项并将原始字符串传递给控制器。"""
+ recorded: list[PluginCreateCommandOptions] = []
+
+ class Controller:
+ @staticmethod
+ def create_plugin(plugin_id: str, env: str, output: str, options: PluginCreateCommandOptions) -> None:
+ recorded.append(options)
+
+ app = typer.Typer()
+ register_developer_commands(app, Controller)
+ result = CliRunner().invoke(app, ['create', 'demo', '--frontend-framework', framework])
+ assert result.exit_code == SUCCESS, result.output
+ assert recorded[0].frontend_framework == framework
+
+
+@pytest.mark.parametrize('framework', ['react', 'custom_ui-1', '../react', 'Vue3'])
+def test_create_cli_rejects_framework_before_resolving_controller(framework: str) -> None:
+ """校验非法框架选项在加载控制器或运行时之前被拒绝。"""
+
+ def unavailable_controller() -> None:
+ """禁止参数错误路径加载控制器。"""
+ raise AssertionError('不应加载控制器')
+
+ app = typer.Typer()
+ register_developer_commands(app, unavailable_controller)
+ result = CliRunner().invoke(app, ['create', 'demo', '--frontend-framework', framework])
+ assert result.exit_code == ARGUMENT_ERROR
+ assert 'Invalid value' in result.output
+ assert framework in result.output
+ assert 'auto' in result.output and 'vue2' in result.output and 'vue3' in result.output
+
+
+def test_create_cli_help_displays_fixed_frontend_framework_choices() -> None:
+ """校验创建命令帮助展示固定的三种框架选项。"""
+ app = typer.Typer()
+ register_developer_commands(app, lambda: None)
+ result = CliRunner().invoke(app, ['create', '--help'], terminal_width=160)
+ assert result.exit_code == SUCCESS
+ assert 'auto|vue2|vue3' in result.output
diff --git a/ruoyi-fastapi-backend/tests/cli/root/test_plugin_frontend_environment.py b/ruoyi-fastapi-backend/tests/cli/root/test_plugin_frontend_environment.py
new file mode 100644
index 0000000..6c52f87
--- /dev/null
+++ b/ruoyi-fastapi-backend/tests/cli/root/test_plugin_frontend_environment.py
@@ -0,0 +1,130 @@
+import json
+import os
+import subprocess
+from collections.abc import Callable
+from pathlib import Path
+
+import pytest
+import yaml
+
+from cli.exit_codes import SUCCESS
+
+pytestmark = pytest.mark.contract
+SDK_FILES = ('pluginBridge.js', 'pluginBridge.d.ts', 'pluginBridge.sdk.json')
+
+
+@pytest.mark.parametrize('framework', ['vue2', 'vue3'])
+def test_cold_cli_preserves_frontend_framework_across_scaffold_sdk_and_dependency_lock(
+ tmp_path: Path,
+ backend_dir: Path,
+ untracked_bundle_project: Path,
+ run_cli_command: Callable[..., subprocess.CompletedProcess[str]],
+ framework: str,
+) -> None:
+ """
+ 校验独立进程从框架环境变量启动后,模板、SDK 与依赖锁选择相同工程。
+
+ 所有宿主文件位于临时目录;创建和锁定只预演,SDK 更新仅修改测试副本。
+ """
+ backend = tmp_path / 'backend'
+ frontend_container = tmp_path / 'ruoyi-fastapi-frontend'
+ for host_framework in ('vue2', 'vue3'):
+ frontend = frontend_container / host_framework / 'web'
+ source = frontend / 'src' / 'utils'
+ source.mkdir(parents=True)
+ (frontend / 'package.json').write_text(
+ json.dumps({'dependencies': {'vue': f'^{host_framework[-1]}.0.0'}}), encoding='utf-8'
+ )
+ original_source = backend_dir.parent / 'ruoyi-fastapi-frontend' / host_framework / 'web' / 'src' / 'utils'
+ for name in SDK_FILES:
+ (source / name).write_bytes((original_source / name).read_bytes())
+ plugin = backend / 'plugins' / 'environment_demo'
+ plugin.mkdir(parents=True)
+ (plugin / 'plugin.yaml').write_text(
+ yaml.safe_dump(
+ {
+ 'id': 'environment_demo',
+ 'name': 'Environment Demo',
+ 'version': '1.0.0',
+ 'backend': {'module': 'plugins.environment_demo'},
+ 'dependencies': {
+ 'frontend': {
+ 'vue2': {'npm': ['vue2-renderer==2.0.0']},
+ 'vue3': {'npm': ['vue3-renderer==3.0.0']},
+ }
+ },
+ }
+ ),
+ encoding='utf-8',
+ )
+ (plugin / '__init__.py').write_text('raise RuntimeError("must never import this plugin")\n', encoding='utf-8')
+ environment = {
+ key: value for key, value in os.environ.items() if key != 'APP_ENV' and not key.startswith(('RUOYI_', 'OIDC_'))
+ }
+ environment.update(
+ RUOYI_PLUGIN_BACKEND_ROOT=str(backend),
+ RUOYI_PLUGIN_FRONTEND_ROOT=str(frontend_container),
+ RUOYI_PLUGIN_FRONTEND_FRAMEWORK=framework,
+ RUOYI_FRONTEND_FRAMEWORK='vue3' if framework == 'vue2' else 'vue2',
+ LOG_FILE_ENABLED='false',
+ )
+ original_files = {path: path.read_bytes() for path in tmp_path.rglob('*') if path.is_file()}
+
+ created = run_cli_command(
+ 'plugin',
+ 'create',
+ 'environment_scaffold',
+ '--frontend-framework',
+ 'auto',
+ '--dry-run',
+ '--output',
+ 'json',
+ env=environment,
+ )
+ assert created.returncode == SUCCESS, created.stdout + created.stderr
+ scaffold = json.loads(created.stdout)
+ selected_frontend = frontend_container / framework / 'web'
+ assert scaffold['frontendFramework'] == framework
+ assert str(backend / 'plugins' / 'environment_scaffold') in scaffold['targetDirs']
+ assert str(selected_frontend / 'plugins' / 'environment_scaffold') in scaffold['targetDirs']
+ assert all(Path(item['path']).is_relative_to(tmp_path) for item in scaffold['files'])
+
+ locked = run_cli_command(
+ 'plugin', 'lock-deps', 'environment_demo', '--dry-run', '--output', 'json', env=environment
+ )
+ assert locked.returncode == SUCCESS, locked.stdout + locked.stderr
+ lock_payload = json.loads(locked.stdout)
+ lockfile = yaml.safe_load(lock_payload['lockfile'])
+ assert Path(lock_payload['outputFile']) == plugin / 'plugin.lock.yaml'
+ assert lockfile['frontendFramework'] == framework
+ assert [item['requirement'] for item in lockfile['npm']] == [f'{framework}-renderer=={framework[-1]}.0.0']
+ assert lock_payload['written'] is False
+ assert original_files == {path: path.read_bytes() for path in tmp_path.rglob('*') if path.is_file()}
+
+ updated = run_cli_command(
+ 'plugin',
+ 'sdk',
+ 'update',
+ str(untracked_bundle_project),
+ '--frontend-framework',
+ 'auto',
+ '--force',
+ '--output',
+ 'json',
+ env=environment,
+ )
+ assert updated.returncode == SUCCESS, updated.stdout + updated.stderr
+ assert json.loads(updated.stdout)['updated'] is True
+ metadata = json.loads((untracked_bundle_project / 'web/vendor/pluginBridge.sdk.json').read_text(encoding='utf-8'))
+ assert metadata['source'] == {
+ 'project': 'RuoYi-FastAPI',
+ 'directory': f'ruoyi-fastapi-frontend/{framework}/web/src/utils',
+ }
+ host_files = {
+ path: content for path, content in original_files.items() if not path.is_relative_to(untracked_bundle_project)
+ }
+ assert host_files == {
+ path: path.read_bytes()
+ for path in tmp_path.rglob('*')
+ if path.is_file() and not path.is_relative_to(untracked_bundle_project)
+ }
diff --git a/ruoyi-fastapi-backend/tests/cli/root/test_plugin_lazy_import.py b/ruoyi-fastapi-backend/tests/cli/root/test_plugin_lazy_import.py
index 8cc1379..43e0fb3 100644
--- a/ruoyi-fastapi-backend/tests/cli/root/test_plugin_lazy_import.py
+++ b/ruoyi-fastapi-backend/tests/cli/root/test_plugin_lazy_import.py
@@ -4,6 +4,9 @@ import subprocess
import sys
from pathlib import Path
+import pytest
+
+pytestmark = pytest.mark.contract
BACKEND_ROOT = Path(__file__).resolve().parents[3]
@@ -20,6 +23,7 @@ def inspect_cold_import(module_name: str, observed_modules: tuple[str, ...]) ->
check=True,
capture_output=True,
text=True,
+ timeout=30,
)
return json.loads(completed.stdout)
@@ -132,6 +136,7 @@ print(json.dumps({'initial': initial, 'afterController': after_controller, 'afte
check=True,
capture_output=True,
text=True,
+ timeout=30,
)
observed = json.loads(completed.stdout)
diff --git a/ruoyi-fastapi-backend/tests/cli/root/test_plugin_sdk_commands.py b/ruoyi-fastapi-backend/tests/cli/root/test_plugin_sdk_commands.py
index 49cd357..ecf760f 100644
--- a/ruoyi-fastapi-backend/tests/cli/root/test_plugin_sdk_commands.py
+++ b/ruoyi-fastapi-backend/tests/cli/root/test_plugin_sdk_commands.py
@@ -11,8 +11,9 @@ from typer.testing import CliRunner
from cli.exit_codes import ARGUMENT_ERROR, DEPENDENCY_ERROR, SUCCESS
from cli.groups.plugin.commands.sdk import register_sdk_commands
+pytestmark = pytest.mark.contract
BACKEND = Path(__file__).resolve().parents[3]
-FRONTEND = BACKEND.parent / 'ruoyi-fastapi-frontend'
+FRONTEND = BACKEND.parent / 'ruoyi-fastapi-frontend' / 'vue3' / 'web'
def sdk_app() -> typer.Typer:
@@ -27,26 +28,14 @@ def sdk_app() -> typer.Typer:
@pytest.fixture
-def project(tmp_path: Path) -> Path:
+def project(untracked_bundle_project: Path) -> Path:
"""
创建未跟踪 SDK 的旧 bundle 工程,避免读取业务插件。
- :param tmp_path: 隔离测试目录
+ :param untracked_bundle_project: 共用的隔离 bundle 工程
:return: 工程根目录
"""
- root = tmp_path / 'example'
- vendor = root / 'web' / 'vendor'
- vendor.mkdir(parents=True)
- (root / 'plugin.yaml').write_text(
- 'manifestVersion: 2\nid: example\nname: Example\nversion: 1.0.0\n'
- 'backend:\n module: plugins.example\n entrypoint: plugins.example:create_plugin\n integration: asgi\n'
- 'frontend:\n delivery:\n type: bundle\n',
- encoding='utf-8',
- )
- (root / '__init__.py').write_text('raise RuntimeError("must never import this project")\n', encoding='utf-8')
- for name in ('pluginBridge.js', 'pluginBridge.d.ts'):
- (vendor / name).write_text('old untracked SDK\n', encoding='utf-8')
- return root
+ return untracked_bundle_project
def invoke_sdk(operation: str, project: Path, *options: str) -> tuple[int, dict]:
@@ -104,6 +93,87 @@ def test_sdk_cli_bad_source_is_json_and_nonzero(project: Path, tmp_path: Path) -
assert json.loads(result.stdout)['ok'] is False
+@pytest.mark.parametrize(('framework', 'selected_framework'), [('auto', 'vue3'), ('vue2', 'vue2'), ('vue3', 'vue3')])
+def test_sdk_cli_selects_supported_framework_from_frontend_container(
+ project: Path, framework: str, selected_framework: str
+) -> None:
+ """校验 SDK 更新和检查命令均接受三个固定框架选项。"""
+ result = CliRunner().invoke(
+ sdk_app(),
+ [
+ 'sdk',
+ 'update',
+ str(project),
+ '--frontend-root',
+ str(FRONTEND.parents[1]),
+ '--frontend-framework',
+ framework,
+ '--force',
+ '--output',
+ 'json',
+ ],
+ )
+ assert result.exit_code == SUCCESS, result.stdout
+ metadata = json.loads((project / 'web/vendor/pluginBridge.sdk.json').read_text(encoding='utf-8'))
+ assert metadata['source']['directory'] == f'ruoyi-fastapi-frontend/{selected_framework}/web/src/utils'
+ check_result = CliRunner().invoke(
+ sdk_app(),
+ [
+ 'sdk',
+ 'check',
+ str(project),
+ '--frontend-root',
+ str(FRONTEND.parents[1]),
+ '--frontend-framework',
+ framework,
+ '--output',
+ 'json',
+ ],
+ )
+ assert check_result.exit_code == SUCCESS, check_result.stdout
+ assert json.loads(check_result.stdout)['status'] == 'current'
+
+
+@pytest.mark.parametrize('operation', ['check', 'update'])
+@pytest.mark.parametrize('framework', ['react', 'custom_ui-1', '../react', 'Vue3'])
+def test_sdk_cli_rejects_unsupported_framework_before_file_operations(
+ project: Path, operation: str, framework: str
+) -> None:
+ """校验 SDK 命令在参数解析阶段拒绝固定选项之外的框架且不修改工程。"""
+ before = {path.relative_to(project): path.read_bytes() for path in project.rglob('*') if path.is_file()}
+ result = CliRunner().invoke(
+ sdk_app(),
+ [
+ 'sdk',
+ operation,
+ str(project),
+ '--frontend-framework',
+ framework,
+ '--output',
+ 'json',
+ ],
+ )
+ assert result.exit_code == ARGUMENT_ERROR
+ assert 'Invalid value' in result.output
+ assert framework in result.output
+ assert 'auto' in result.output and 'vue2' in result.output and 'vue3' in result.output
+ assert before == {path.relative_to(project): path.read_bytes() for path in project.rglob('*') if path.is_file()}
+
+
+@pytest.mark.parametrize('operation', ['check', 'update'])
+def test_sdk_cli_help_displays_fixed_frontend_framework_choices(operation: str) -> None:
+ """校验两个 SDK 命令的帮助均展示固定框架选项。"""
+ result = CliRunner().invoke(sdk_app(), ['sdk', operation, '--help'], terminal_width=160)
+ assert result.exit_code == SUCCESS
+ assert 'auto|vue2|vue3' in result.output
+
+
+def test_sdk_cli_rejects_removed_frontend_version_option(project: Path) -> None:
+ """校验旧版本选项已移除,不保留兼容别名。"""
+ result = CliRunner().invoke(sdk_app(), ['sdk', 'check', str(project), '--frontend-version', 'vue3'])
+ assert result.exit_code == ARGUMENT_ERROR
+
+
def test_sdk_cli_invalid_output_is_argument_error(project: Path) -> None:
"""
验证 SDK 子命令复用 CLI 输出格式约束。
diff --git a/ruoyi-fastapi-backend/tests/cli/runtime/plugin/conftest.py b/ruoyi-fastapi-backend/tests/cli/runtime/plugin/conftest.py
index 4167f11..d305dc0 100644
--- a/ruoyi-fastapi-backend/tests/cli/runtime/plugin/conftest.py
+++ b/ruoyi-fastapi-backend/tests/cli/runtime/plugin/conftest.py
@@ -4,6 +4,7 @@ from subprocess import CompletedProcess
from cli.runtime.plugin.service import CliPluginRuntimeService
from plugins.core.environment import PluginRuntimeEnvironmentService
+from plugins.core.frontend import PluginFrontendFrameworkResolver
from plugins.core.validation.dependencies import (
NpmDependencyInspector,
PluginDependencyChecker,
@@ -37,6 +38,10 @@ class FakeRuntimeEnvironment:
"""获取前端项目根目录。"""
return str(self.frontend_dir)
+ def get_frontend_framework(self, frontend_root: Path | None = None) -> str:
+ """获取指定测试前端工程的框架标识。"""
+ return PluginFrontendFrameworkResolver.resolve(frontend_root or self.frontend_dir)
+
def get_frontend_plugins_dir(self) -> str:
"""获取前端插件根目录。"""
return str(self.frontend_dir / 'plugins')
diff --git a/ruoyi-fastapi-backend/tests/cli/runtime/plugin/test_runtime_dependency_lock.py b/ruoyi-fastapi-backend/tests/cli/runtime/plugin/test_runtime_dependency_lock.py
index 8c94796..e2b7abf 100644
--- a/ruoyi-fastapi-backend/tests/cli/runtime/plugin/test_runtime_dependency_lock.py
+++ b/ruoyi-fastapi-backend/tests/cli/runtime/plugin/test_runtime_dependency_lock.py
@@ -15,8 +15,7 @@ def write_demo_dependency_manifest(backend_root: Path) -> None:
manifest = yaml.safe_load(manifest_path.read_text(encoding='utf-8'))
manifest['dependencies'] = {
'python': ['openai>=2.0.0,<3.0.0'],
- 'npm': ['dayjs>=1.11.0,<2.0.0'],
- 'npmDev': [],
+ 'frontend': {'vue3': {'npm': ['dayjs>=1.11.0,<2.0.0'], 'npmDev': []}},
'plugins': [],
}
manifest_path.write_text(yaml.safe_dump(manifest, allow_unicode=True, sort_keys=False), encoding='utf-8')
@@ -43,6 +42,8 @@ def test_plugin_runtime_lock_dependencies_dry_run_returns_lockfile_template(tmp_
lockfile = yaml.safe_load(payload['lockfile'])
assert lockfile['plugin'] == 'demo'
assert lockfile['version'] == '0.1.0'
+ assert lockfile['frontendFramework'] == 'vue3'
+ assert 'frontendVersion' not in lockfile
assert lockfile['python'][0]['name'] == 'openai'
assert lockfile['python'][0]['requirement'] == 'openai>=2.0.0,<3.0.0'
assert lockfile['python'][0]['resolvedVersion'] == ''
diff --git a/ruoyi-fastapi-backend/tests/cli/runtime/plugin/test_runtime_scaffold.py b/ruoyi-fastapi-backend/tests/cli/runtime/plugin/test_runtime_scaffold.py
index bc63757..73f00ec 100644
--- a/ruoyi-fastapi-backend/tests/cli/runtime/plugin/test_runtime_scaffold.py
+++ b/ruoyi-fastapi-backend/tests/cli/runtime/plugin/test_runtime_scaffold.py
@@ -1,7 +1,10 @@
import json
from pathlib import Path
+import pytest
+
from cli.runtime.plugin.service import CliPluginRuntimeService
+from plugins.core.environment import PluginRuntimeEnvironmentService
from .conftest import FakeRuntimeEnvironment, build_runtime
@@ -62,16 +65,21 @@ def test_plugin_runtime_create_plugin_dry_run_does_not_write_files(tmp_path: Pat
assert payload['template'] == 'full-stack'
assert payload['backend'] is True
assert payload['frontend'] is True
- assert payload['frontendVersion'] == 'vue3'
+ assert payload['frontendFramework'] == 'vue3'
assert payload['test'] is True
assert payload['backendTest'] is True
assert payload['frontendTest'] is True
assert str(backend_root / 'tests' / 'plugins' / 'demo') in payload['targetDirs']
- assert str(project_root / 'ruoyi-fastapi-frontend' / 'tests' / 'plugins' / 'demo') in payload['targetDirs']
+ assert (
+ str(project_root / 'ruoyi-fastapi-frontend' / 'vue3' / 'web' / 'tests' / 'plugins' / 'demo')
+ in payload['targetDirs']
+ )
assert payload['files']
assert not (backend_root / 'plugins' / 'demo' / 'plugin.yaml').exists()
assert not (backend_root / 'tests' / 'plugins' / 'demo' / 'test_ping.py').exists()
- assert not (project_root / 'ruoyi-fastapi-frontend' / 'tests' / 'plugins' / 'demo' / 'pluginView.test.js').exists()
+ assert not (
+ project_root / 'ruoyi-fastapi-frontend' / 'vue3' / 'web' / 'tests' / 'plugins' / 'demo' / 'pluginView.test.js'
+ ).exists()
def test_plugin_runtime_create_plugin_rejects_unsafe_plugin_id(tmp_path: Path) -> None:
@@ -93,6 +101,7 @@ def test_plugin_runtime_create_plugin_uses_runtime_frontend_dir(tmp_path: Path)
backend_root = project_root / 'api-server'
frontend_root = project_root / 'web-client'
backend_root.mkdir(parents=True)
+ write_frontend_package(frontend_root, {'vue': '^3.5.26'})
payload = build_runtime(backend_root, frontend_root=frontend_root).create_plugin('demo')
@@ -115,7 +124,7 @@ def test_plugin_runtime_create_plugin_auto_detects_vue2_frontend(tmp_path: Path)
view_content = (frontend_root / 'plugins' / 'demo' / 'views' / 'index.vue').read_text(encoding='utf-8')
test_content = (frontend_root / 'tests' / 'plugins' / 'demo' / 'pluginView.test.js').read_text(encoding='utf-8')
assert payload['ok'] is True
- assert payload['frontendVersion'] == 'vue2'
+ assert payload['frontendFramework'] == 'vue2'
assert '
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ruoyi-fastapi-frontend/vue2/mobile/scripts/patch-uni-webpack.cjs b/ruoyi-fastapi-frontend/vue2/mobile/scripts/patch-uni-webpack.cjs
new file mode 100644
index 0000000..b779613
--- /dev/null
+++ b/ruoyi-fastapi-frontend/vue2/mobile/scripts/patch-uni-webpack.cjs
@@ -0,0 +1,19 @@
+const fs = require('fs')
+
+// 此 uni-app 版本即使关闭独立分包,也会预加载依赖 webpack 4 GraphHelpers
+// 的插件。延迟到功能启用后再加载,保证 Vue CLI 5 的普通小程序构建可用。
+const filename = require.resolve('@dcloudio/uni-mp-weixin/lib/createIndependentPlugin.js')
+const source = fs.readFileSync(filename, 'utf8')
+const marker = '// Lazy independent plugins for webpack 5'
+
+if (!source.includes(marker)) {
+ const imports = source.match(/^const .+ = require\('.\/independent-plugins\/.+'\)\r?\n/gm)
+ const guard = 'if (!independentSwitch) return []'
+ if (!imports || !source.includes(guard)) {
+ throw new Error('uni-app 独立分包插件结构已变化,请检查 webpack 兼容补丁')
+ }
+ let patched = source
+ imports.forEach(line => { patched = patched.replace(line, '') })
+ patched = patched.replace(guard, `${guard}\n\n ${marker}\n${imports.map(line => ` ${line.trim()}`).join('\n')}`)
+ fs.writeFileSync(filename, patched)
+}
diff --git a/ruoyi-fastapi-frontend/vue2/mobile/src/App.vue b/ruoyi-fastapi-frontend/vue2/mobile/src/App.vue
new file mode 100644
index 0000000..b5b848c
--- /dev/null
+++ b/ruoyi-fastapi-frontend/vue2/mobile/src/App.vue
@@ -0,0 +1,70 @@
+
+
+
diff --git a/ruoyi-fastapi-app/src/api/login.js b/ruoyi-fastapi-frontend/vue2/mobile/src/api/login.js
similarity index 94%
rename from ruoyi-fastapi-app/src/api/login.js
rename to ruoyi-fastapi-frontend/vue2/mobile/src/api/login.js
index 4fda1aa..3733b15 100644
--- a/ruoyi-fastapi-app/src/api/login.js
+++ b/ruoyi-fastapi-frontend/vue2/mobile/src/api/login.js
@@ -1,62 +1,62 @@
-import request from "@/utils/request";
-
-// 登录方法
-export function login(username, password, code, uuid) {
- const data = {
- username,
- password,
- code,
- uuid,
- };
- return request({
- url: "/login",
- header: {
- "Content-Type": "application/x-www-form-urlencoded",
- },
- headers: {
- isToken: false,
- },
- method: "post",
- data: data,
- });
-}
-
-// 注册方法
-export function register(data) {
- return request({
- url: "/register",
- headers: {
- isToken: false,
- },
- method: "post",
- data: data,
- });
-}
-
-// 获取用户详细信息
-export function getInfo() {
- return request({
- url: "/getInfo",
- method: "get",
- });
-}
-
-// 退出方法
-export function logout() {
- return request({
- url: "/logout",
- method: "post",
- });
-}
-
-// 获取验证码
-export function getCodeImg() {
- return request({
- url: "/captchaImage",
- headers: {
- isToken: false,
- },
- method: "get",
- timeout: 20000,
- });
-}
+import request from "@/utils/request";
+
+// 登录方法
+export function login(username, password, code, uuid) {
+ const data = {
+ username,
+ password,
+ code,
+ uuid,
+ };
+ return request({
+ url: "/login",
+ header: {
+ "Content-Type": "application/x-www-form-urlencoded",
+ },
+ headers: {
+ isToken: false,
+ },
+ method: "post",
+ data: data,
+ });
+}
+
+// 注册方法
+export function register(data) {
+ return request({
+ url: "/register",
+ headers: {
+ isToken: false,
+ },
+ method: "post",
+ data: data,
+ });
+}
+
+// 获取用户详细信息
+export function getInfo() {
+ return request({
+ url: "/getInfo",
+ method: "get",
+ });
+}
+
+// 退出方法
+export function logout() {
+ return request({
+ url: "/logout",
+ method: "post",
+ });
+}
+
+// 获取验证码
+export function getCodeImg() {
+ return request({
+ url: "/captchaImage",
+ headers: {
+ isToken: false,
+ },
+ method: "get",
+ timeout: 20000,
+ });
+}
diff --git a/ruoyi-fastapi-app/src/api/system/dict/data.js b/ruoyi-fastapi-frontend/vue2/mobile/src/api/system/dict/data.js
similarity index 100%
rename from ruoyi-fastapi-app/src/api/system/dict/data.js
rename to ruoyi-fastapi-frontend/vue2/mobile/src/api/system/dict/data.js
diff --git a/ruoyi-fastapi-app/src/api/system/dict/type.js b/ruoyi-fastapi-frontend/vue2/mobile/src/api/system/dict/type.js
similarity index 100%
rename from ruoyi-fastapi-app/src/api/system/dict/type.js
rename to ruoyi-fastapi-frontend/vue2/mobile/src/api/system/dict/type.js
diff --git a/ruoyi-fastapi-app/src/api/system/user.js b/ruoyi-fastapi-frontend/vue2/mobile/src/api/system/user.js
similarity index 100%
rename from ruoyi-fastapi-app/src/api/system/user.js
rename to ruoyi-fastapi-frontend/vue2/mobile/src/api/system/user.js
diff --git a/ruoyi-fastapi-frontend/vue2/mobile/src/config.js b/ruoyi-fastapi-frontend/vue2/mobile/src/config.js
new file mode 100644
index 0000000..c651d42
--- /dev/null
+++ b/ruoyi-fastapi-frontend/vue2/mobile/src/config.js
@@ -0,0 +1,26 @@
+// 应用全局配置
+module.exports = {
+ baseUrl: "http://localhost:9099",
+ // 应用信息
+ appInfo: {
+ // 应用名称
+ name: "RuoYi-FastAPI-APP",
+ // 应用版本
+ version: "1.10.0",
+ // 应用logo
+ logo: "/static/logo.png",
+ // 官方网站
+ site_url: "https://vfadmin.insistence.tech",
+ // 政策协议
+ agreements: [
+ {
+ title: "隐私政策",
+ url: "https://ruoyi.vip/protocol.html",
+ },
+ {
+ title: "用户服务协议",
+ url: "https://ruoyi.vip/protocol.html",
+ },
+ ],
+ },
+};
diff --git a/ruoyi-fastapi-frontend/vue2/mobile/src/main.ts b/ruoyi-fastapi-frontend/vue2/mobile/src/main.ts
new file mode 100644
index 0000000..72b0d10
--- /dev/null
+++ b/ruoyi-fastapi-frontend/vue2/mobile/src/main.ts
@@ -0,0 +1,17 @@
+// @ts-nocheck
+import Vue from "vue";
+import App from "./App.vue";
+import store from "./store";
+import plugins from "./plugins";
+import "./permission";
+import { getDicts } from "@/api/system/dict/data";
+
+Vue.use(plugins);
+Vue.config.productionTip = false;
+Vue.prototype.$store = store;
+Vue.prototype.getDicts = getDicts;
+App.mpType = "app";
+const app = new Vue({
+ ...App,
+});
+app.$mount();
diff --git a/ruoyi-fastapi-frontend/vue2/mobile/src/manifest.json b/ruoyi-fastapi-frontend/vue2/mobile/src/manifest.json
new file mode 100644
index 0000000..e77550b
--- /dev/null
+++ b/ruoyi-fastapi-frontend/vue2/mobile/src/manifest.json
@@ -0,0 +1,68 @@
+{
+ "name": "RuoYi-FastAPI移动端",
+ "appid": "__UNI__25A9D80",
+ "description": "",
+ "versionName": "1.10.0",
+ "versionCode": "100",
+ "transformPx": false,
+ "app-plus": {
+ "usingComponents": true,
+ "nvueCompiler": "uni-app",
+ "splashscreen": {
+ "alwaysShowBeforeRender": true,
+ "waiting": true,
+ "autoclose": true,
+ "delay": 0
+ },
+ "modules": {},
+ "distribute": {
+ "android": {
+ "permissions": [
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ ""
+ ]
+ },
+ "ios": {},
+ "sdkConfigs": {}
+ }
+ },
+ "quickapp": {},
+ "mp-weixin": {
+ "appid": "wxccd7e2a0911b3397",
+ "setting": {
+ "urlCheck": false,
+ "es6": false,
+ "minified": true,
+ "postcss": true
+ },
+ "optimization": {
+ "subPackages": true
+ },
+ "usingComponents": true
+ },
+ "vueVersion": "2",
+ "h5": {
+ "devServer": {
+ "port": 9090,
+ "https": false
+ },
+ "title": "RuoYi-FastAPI-APP",
+ "router": {
+ "mode": "hash",
+ "base": "./"
+ }
+ }
+}
diff --git a/ruoyi-fastapi-app/src/pages.json b/ruoyi-fastapi-frontend/vue2/mobile/src/pages.json
similarity index 95%
rename from ruoyi-fastapi-app/src/pages.json
rename to ruoyi-fastapi-frontend/vue2/mobile/src/pages.json
index ecf76ca..6e4105e 100644
--- a/ruoyi-fastapi-app/src/pages.json
+++ b/ruoyi-fastapi-frontend/vue2/mobile/src/pages.json
@@ -1,138 +1,138 @@
-{
- "pages": [
- {
- "path": "pages/login",
- "style": {
- "navigationBarTitleText": "登录"
- }
- },
- {
- "path": "pages/register",
- "style": {
- "navigationBarTitleText": "注册"
- }
- },
- {
- "path": "pages/index",
- "style": {
- "navigationBarTitleText": "RuoYi-FastAPI移动端",
- "navigationStyle": "custom"
- }
- },
- {
- "path": "pages/work/index",
- "style": {
- "navigationBarTitleText": "工作台"
- }
- },
- {
- "path": "pages/mine/index",
- "style": {
- "navigationBarTitleText": "我的"
- }
- },
- {
- "path": "pages/mine/avatar/index",
- "style": {
- "navigationBarTitleText": "修改头像"
- }
- },
- {
- "path": "pages/mine/info/index",
- "style": {
- "navigationBarTitleText": "个人信息"
- }
- },
- {
- "path": "pages/mine/info/edit",
- "style": {
- "navigationBarTitleText": "编辑资料"
- }
- },
- {
- "path": "pages/mine/pwd/index",
- "style": {
- "navigationBarTitleText": "修改密码"
- }
- },
- {
- "path": "pages/mine/setting/index",
- "style": {
- "navigationBarTitleText": "应用设置"
- }
- },
- {
- "path": "pages/mine/setting/timezone",
- "style": {
- "navigationBarTitleText": "时区设置"
- }
- },
- {
- "path": "pages/mine/help/index",
- "style": {
- "navigationBarTitleText": "常见问题"
- }
- },
- {
- "path": "pages/mine/about/index",
- "style": {
- "navigationBarTitleText": "关于我们"
- }
- },
- {
- "path": "pages/common/agreement/index",
- "style": {
- "navigationBarTitleText": "用户协议"
- }
- },
- {
- "path": "pages/common/privacy/index",
- "style": {
- "navigationBarTitleText": "隐私协议"
- }
- },
- {
- "path": "pages/common/webview/index",
- "style": {
- "navigationBarTitleText": "浏览网页"
- }
- },
- {
- "path": "pages/common/textview/index",
- "style": {
- "navigationBarTitleText": "浏览文本"
- }
- }
- ],
- "tabBar": {
- "color": "#000000",
- "selectedColor": "#000000",
- "borderStyle": "white",
- "backgroundColor": "#ffffff",
- "list": [
- {
- "pagePath": "pages/index",
- "iconPath": "static/images/tabbar/home.png",
- "selectedIconPath": "static/images/tabbar/home_.png",
- "text": "首页"
- },
- {
- "pagePath": "pages/work/index",
- "iconPath": "static/images/tabbar/work.png",
- "selectedIconPath": "static/images/tabbar/work_.png",
- "text": "工作台"
- },
- {
- "pagePath": "pages/mine/index",
- "iconPath": "static/images/tabbar/mine.png",
- "selectedIconPath": "static/images/tabbar/mine_.png",
- "text": "我的"
- }
- ]
- },
- "globalStyle": {
- "navigationBarTextStyle": "black",
- "navigationBarTitleText": "RuoYi",
- "navigationBarBackgroundColor": "#FFFFFF"
- }
-}
+{
+ "pages": [
+ {
+ "path": "pages/login",
+ "style": {
+ "navigationBarTitleText": "登录"
+ }
+ },
+ {
+ "path": "pages/register",
+ "style": {
+ "navigationBarTitleText": "注册"
+ }
+ },
+ {
+ "path": "pages/index",
+ "style": {
+ "navigationBarTitleText": "RuoYi-FastAPI移动端",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/work/index",
+ "style": {
+ "navigationBarTitleText": "工作台"
+ }
+ },
+ {
+ "path": "pages/mine/index",
+ "style": {
+ "navigationBarTitleText": "我的"
+ }
+ },
+ {
+ "path": "pages/mine/avatar/index",
+ "style": {
+ "navigationBarTitleText": "修改头像"
+ }
+ },
+ {
+ "path": "pages/mine/info/index",
+ "style": {
+ "navigationBarTitleText": "个人信息"
+ }
+ },
+ {
+ "path": "pages/mine/info/edit",
+ "style": {
+ "navigationBarTitleText": "编辑资料"
+ }
+ },
+ {
+ "path": "pages/mine/pwd/index",
+ "style": {
+ "navigationBarTitleText": "修改密码"
+ }
+ },
+ {
+ "path": "pages/mine/setting/index",
+ "style": {
+ "navigationBarTitleText": "应用设置"
+ }
+ },
+ {
+ "path": "pages/mine/setting/timezone",
+ "style": {
+ "navigationBarTitleText": "时区设置"
+ }
+ },
+ {
+ "path": "pages/mine/help/index",
+ "style": {
+ "navigationBarTitleText": "常见问题"
+ }
+ },
+ {
+ "path": "pages/mine/about/index",
+ "style": {
+ "navigationBarTitleText": "关于我们"
+ }
+ },
+ {
+ "path": "pages/common/agreement/index",
+ "style": {
+ "navigationBarTitleText": "用户协议"
+ }
+ },
+ {
+ "path": "pages/common/privacy/index",
+ "style": {
+ "navigationBarTitleText": "隐私协议"
+ }
+ },
+ {
+ "path": "pages/common/webview/index",
+ "style": {
+ "navigationBarTitleText": "浏览网页"
+ }
+ },
+ {
+ "path": "pages/common/textview/index",
+ "style": {
+ "navigationBarTitleText": "浏览文本"
+ }
+ }
+ ],
+ "tabBar": {
+ "color": "#000000",
+ "selectedColor": "#000000",
+ "borderStyle": "white",
+ "backgroundColor": "#ffffff",
+ "list": [
+ {
+ "pagePath": "pages/index",
+ "iconPath": "static/images/tabbar/home.png",
+ "selectedIconPath": "static/images/tabbar/home_.png",
+ "text": "首页"
+ },
+ {
+ "pagePath": "pages/work/index",
+ "iconPath": "static/images/tabbar/work.png",
+ "selectedIconPath": "static/images/tabbar/work_.png",
+ "text": "工作台"
+ },
+ {
+ "pagePath": "pages/mine/index",
+ "iconPath": "static/images/tabbar/mine.png",
+ "selectedIconPath": "static/images/tabbar/mine_.png",
+ "text": "我的"
+ }
+ ]
+ },
+ "globalStyle": {
+ "navigationBarTextStyle": "black",
+ "navigationBarTitleText": "RuoYi",
+ "navigationBarBackgroundColor": "#FFFFFF"
+ }
+}
diff --git a/ruoyi-fastapi-frontend/vue2/mobile/src/pages/common/agreement/index.vue b/ruoyi-fastapi-frontend/vue2/mobile/src/pages/common/agreement/index.vue
new file mode 100644
index 0000000..54488fc
--- /dev/null
+++ b/ruoyi-fastapi-frontend/vue2/mobile/src/pages/common/agreement/index.vue
@@ -0,0 +1,69 @@
+
+
+
+ 用户协议
+
+
+
+
+ 1. 服务条款的确认和接纳
+ 欢迎使用RuoYi-FastAPI移动端框架(以下简称“本产品”)。本产品的服务所有权和运作权归RuoYi-FastAPI所有。用户在使用本产品前,请仔细阅读本协议。当您点击“同意”或实际使用本产品服务时,即视为您已阅读并完全同意本协议的所有条款。
+
+
+
+ 2. 用户账号与安全
+ 您在注册账号时,应提供真实、准确、最新和完整的个人资料。您有责任维护账号和密码的安全,并对以您账号进行的所有活动承担法律责任。如发现账号被非法使用,请立即通知我们。
+
+
+
+ 3. 用户行为规范
+ 用户在使用本产品过程中,必须遵守国家法律法规,不得利用本产品从事违法违规活动,包括但不限于:发布虚假信息、侵犯他人知识产权、传播淫秽色情信息、进行网络攻击等。
+
+
+
+ 4. 服务内容的变更、中断或终止
+ 鉴于网络服务的特殊性,我们有权随时变更、中断或终止部分或全部网络服务。对于因服务器维护、网络故障等原因导致的服务中断,我们将尽量提前公告,但不对因此产生的损失承担责任。
+
+
+
+ 5. 知识产权声明
+ 本产品包含的所有内容(包括但不限于文字、图片、音频、视频、软件代码等)的知识产权归RuoYi-FastAPI或相关权利人所有。未经授权,任何人不得擅自使用、复制、修改或传播。
+
+
+
+ 6. 免责声明
+ 本产品按“现状”提供,不包含任何明示或暗示的保证。我们不对因不可抗力、黑客攻击、系统不稳定等原因导致的服务中断或数据丢失承担责任。
+
+
+
+
+ 更新日期:2024年01月01日
+
+
+
+
+
diff --git a/ruoyi-fastapi-frontend/vue2/mobile/src/pages/common/privacy/index.vue b/ruoyi-fastapi-frontend/vue2/mobile/src/pages/common/privacy/index.vue
new file mode 100644
index 0000000..702522d
--- /dev/null
+++ b/ruoyi-fastapi-frontend/vue2/mobile/src/pages/common/privacy/index.vue
@@ -0,0 +1,93 @@
+
+
+
+ 隐私协议
+
+
+
+
+ RuoYi-FastAPI非常重视您的隐私保护。本隐私政策旨在向您说明我们如何收集、使用、存储和保护您的个人信息。请您在使用本产品前仔细阅读本政策。
+
+
+
+ 1. 我们如何收集和使用您的个人信息
+ 为了向您提供更好的服务,我们可能会收集您的以下信息:
+ •
+ 账号信息:当您注册或登录时,我们需要收集您的用户名、密码、手机号码等信息。
+ •
+ 设备信息:为了保障账户安全和优化服务,我们会收集您的设备型号、操作系统版本、唯一设备标识符等信息。
+ •
+ 日志信息:当您使用本产品时,我们会自动收集您的操作日志、IP地址、访问时间等信息。
+
+
+
+ 2. 我们如何共享、转让、公开披露您的个人信息
+ 我们不会向任何第三方共享、转让或公开披露您的个人信息,但以下情况除外:
+ • 获得您的明确授权或同意;
+ • 根据法律法规或司法机关的要求;
+ • 为了维护国家安全、公共安全或重大公共利益。
+
+
+
+ 3. 我们如何保护您的个人信息
+ 我们采取了多种安全措施来保护您的个人信息,包括数据加密、访问控制、安全审计等。我们会尽力防止您的信息被未经授权的访问、篡改或泄露。
+
+
+
+ 4. 您的权利
+ 您有权访问、更正或删除您的个人信息。您也可以随时注销账号。如您需要行使这些权利,请通过“联系我们”章节中的方式与我们联系。
+
+
+
+ 5. 本政策如何更新
+ 随着法律法规的变化或业务的发展,我们可能会不时更新本隐私政策。更新后的政策将在本产品中发布,并在生效前通过适当方式通知您。
+
+
+
+ 6. 联系我们
+ 如果您对本隐私政策有任何疑问或建议,请通过以下方式联系我们:
+ 邮箱:xxx@xxx.com
+
+
+
+
+ 更新日期:2024年01月01日
+
+
+
+
+
diff --git a/ruoyi-fastapi-frontend/vue2/mobile/src/pages/common/textview/index.vue b/ruoyi-fastapi-frontend/vue2/mobile/src/pages/common/textview/index.vue
new file mode 100644
index 0000000..1969fb7
--- /dev/null
+++ b/ruoyi-fastapi-frontend/vue2/mobile/src/pages/common/textview/index.vue
@@ -0,0 +1,39 @@
+
+
+
+
+ {{ title }}
+
+
+ {{ content }}
+
+
+
+
+
+
+
+
diff --git a/ruoyi-fastapi-frontend/vue2/mobile/src/pages/common/webview/index.vue b/ruoyi-fastapi-frontend/vue2/mobile/src/pages/common/webview/index.vue
new file mode 100644
index 0000000..65e4199
--- /dev/null
+++ b/ruoyi-fastapi-frontend/vue2/mobile/src/pages/common/webview/index.vue
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
diff --git a/ruoyi-fastapi-frontend/vue2/mobile/src/pages/index.vue b/ruoyi-fastapi-frontend/vue2/mobile/src/pages/index.vue
new file mode 100644
index 0000000..02b6b06
--- /dev/null
+++ b/ruoyi-fastapi-frontend/vue2/mobile/src/pages/index.vue
@@ -0,0 +1,214 @@
+
+
+
+
+
+
+ RuoYi-FastAPI移动端
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 系统运行正常
+ 刚刚更新
+
+
+
+ 85%
+ 工作效率
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 新通知
+ 12
+
+
+
+
+
+
+
+ 待办任务
+ 5
+
+
+
+
+
+
+
+
+
+
+ 快捷入口
+
+
+
+
+
+
+ 申请
+
+
+
+
+
+ 新闻
+
+
+
+
+
+ 统计
+
+
+
+
+
+ 计划
+
+
+
+
+
+
+
+ 最近更新
+ 查看全部
+
+
+
+
+
+
+
+ 请假申请
+ 等待经理审批
+
+ 待处理
+
+
+
+
+
+
+ 周报
+ 提交成功
+
+ 已完成
+
+
+
+
+
+
+
+
+
diff --git a/ruoyi-fastapi-frontend/vue2/mobile/src/pages/login.vue b/ruoyi-fastapi-frontend/vue2/mobile/src/pages/login.vue
new file mode 100644
index 0000000..5f482b0
--- /dev/null
+++ b/ruoyi-fastapi-frontend/vue2/mobile/src/pages/login.vue
@@ -0,0 +1,218 @@
+
+
+
+
+
+
+
+ RuoYi-FastAPI移动端登录
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 没有账号?
+ 立即注册
+
+
+
+ 登录即代表同意
+ 《用户协议》
+ 和
+ 《隐私协议》
+
+
+
+
+
+
+
+
+
diff --git a/ruoyi-fastapi-frontend/vue2/mobile/src/pages/mine/about/index.vue b/ruoyi-fastapi-frontend/vue2/mobile/src/pages/mine/about/index.vue
new file mode 100644
index 0000000..66a5453
--- /dev/null
+++ b/ruoyi-fastapi-frontend/vue2/mobile/src/pages/mine/about/index.vue
@@ -0,0 +1,73 @@
+
+
+
+
+
+ RuoYi-FastAPI移动端
+
+
+
+
+
+
+ 版本信息
+ v{{ version }}
+
+
+
+
+ 官方邮箱
+ xxx@xxx.com
+
+
+
+
+ 服务热线
+ 400-999-9999
+
+
+
+
+ 公司网站
+ {{
+ url
+ }}
+
+
+
+
+
+ Copyright © 2026 insistence.tech All Rights Reserved.
+
+
+
+
+
+
+
diff --git a/ruoyi-fastapi-frontend/vue2/mobile/src/pages/mine/avatar/index.vue b/ruoyi-fastapi-frontend/vue2/mobile/src/pages/mine/avatar/index.vue
new file mode 100644
index 0000000..1e91372
--- /dev/null
+++ b/ruoyi-fastapi-frontend/vue2/mobile/src/pages/mine/avatar/index.vue
@@ -0,0 +1,757 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 选择头像
+
+
+ 提交
+
+
+
+
+
+
+
+
+
+
diff --git a/ruoyi-fastapi-frontend/vue2/mobile/src/pages/mine/help/index.vue b/ruoyi-fastapi-frontend/vue2/mobile/src/pages/mine/help/index.vue
new file mode 100644
index 0000000..f4fc929
--- /dev/null
+++ b/ruoyi-fastapi-frontend/vue2/mobile/src/pages/mine/help/index.vue
@@ -0,0 +1,94 @@
+
+
+
+
+ {{ item.title }}
+
+
+
+ {{ child.title }}
+
+
+
+
+
+
+
+
+
+
diff --git a/ruoyi-fastapi-frontend/vue2/mobile/src/pages/mine/index.vue b/ruoyi-fastapi-frontend/vue2/mobile/src/pages/mine/index.vue
new file mode 100644
index 0000000..f5bb4fb
--- /dev/null
+++ b/ruoyi-fastapi-frontend/vue2/mobile/src/pages/mine/index.vue
@@ -0,0 +1,212 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ name }}
+
+
+ 查看个人信息
+
+
+
+
+ 点击登录
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 交流群
+
+
+
+
+
+ 在线客服
+
+
+
+
+
+ 反馈社区
+
+
+
+
+
+ 点赞我们
+
+
+
+
+
+
+
+
+ 编辑资料
+
+
+
+
+
+
+
+ 常见问题
+
+
+
+
+
+
+
+ 关于我们
+
+
+
+
+
+
+
+ 应用设置
+
+
+
+
+
+
+
+
+
diff --git a/ruoyi-fastapi-frontend/vue2/mobile/src/pages/mine/info/edit.vue b/ruoyi-fastapi-frontend/vue2/mobile/src/pages/mine/info/edit.vue
new file mode 100644
index 0000000..5fe346c
--- /dev/null
+++ b/ruoyi-fastapi-frontend/vue2/mobile/src/pages/mine/info/edit.vue
@@ -0,0 +1,147 @@
+
+
+
+
+
+ 用户昵称
+
+
+
+
+
+ 手机号码
+
+
+
+
+
+ 邮箱
+
+
+
+
+
+ 性别
+
+
+ {{ item.text }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ruoyi-fastapi-frontend/vue2/mobile/src/pages/mine/info/index.vue b/ruoyi-fastapi-frontend/vue2/mobile/src/pages/mine/info/index.vue
new file mode 100644
index 0000000..14c7bcf
--- /dev/null
+++ b/ruoyi-fastapi-frontend/vue2/mobile/src/pages/mine/info/index.vue
@@ -0,0 +1,133 @@
+
+
+
+
+
+
+
+
+
+ 昵称
+
+ {{ user.nickName }}
+
+
+
+
+
+
+
+
+ 手机号码
+
+ {{ user.phonenumber }}
+
+
+
+
+
+
+
+
+ 邮箱
+
+ {{ user.email }}
+
+
+
+
+
+
+
+
+ 岗位
+
+ {{ postGroup }}
+
+
+
+
+
+
+
+
+ 角色
+
+ {{ roleGroup }}
+
+
+
+
+
+
+
+
+ 创建日期
+
+ {{ formatBusinessTime(user.createTime) || '-' }}
+
+
+
+
+
+
+
+
diff --git a/ruoyi-fastapi-frontend/vue2/mobile/src/pages/mine/pwd/index.vue b/ruoyi-fastapi-frontend/vue2/mobile/src/pages/mine/pwd/index.vue
new file mode 100644
index 0000000..5e32295
--- /dev/null
+++ b/ruoyi-fastapi-frontend/vue2/mobile/src/pages/mine/pwd/index.vue
@@ -0,0 +1,100 @@
+
+
+
+
+
+ 旧密码
+
+
+
+
+ 新密码
+
+
+
+
+ 确认密码
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ruoyi-fastapi-frontend/vue2/mobile/src/pages/mine/setting/index.vue b/ruoyi-fastapi-frontend/vue2/mobile/src/pages/mine/setting/index.vue
new file mode 100644
index 0000000..3a029c9
--- /dev/null
+++ b/ruoyi-fastapi-frontend/vue2/mobile/src/pages/mine/setting/index.vue
@@ -0,0 +1,98 @@
+
+
+
+
+
+
+
+ 修改密码
+
+
+
+
+
+
+
+
+ 时区设置
+
+
+
+
+
+
+
+
+ 检查更新
+
+
+
+
+
+
+
+
+ 清理缓存
+
+
+
+
+
+
+
+ 退出登录
+
+
+
+
+
+
+
diff --git a/ruoyi-fastapi-frontend/vue2/mobile/src/pages/mine/setting/timezone.vue b/ruoyi-fastapi-frontend/vue2/mobile/src/pages/mine/setting/timezone.vue
new file mode 100644
index 0000000..d2dd990
--- /dev/null
+++ b/ruoyi-fastapi-frontend/vue2/mobile/src/pages/mine/setting/timezone.vue
@@ -0,0 +1,198 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ 没有匹配的时区,请修改搜索词。
+
+
+ 时区列表加载失败,仍可使用当前时区。
+
+
+
+
+ 时间预览
+
+ 预览时区:{{ previewZone }}
+ 时间预览:{{ previewTime }}
+ 当前设备无法识别时区,已使用系统时区;你也可以手动选择。
+
+ 用于时间显示、日期筛选和导出。定时任务和业务统计使用各自配置的时区。
+
+ {{
+ saveError
+ }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ruoyi-fastapi-frontend/vue2/mobile/src/pages/register.vue b/ruoyi-fastapi-frontend/vue2/mobile/src/pages/register.vue
new file mode 100644
index 0000000..1b57ae9
--- /dev/null
+++ b/ruoyi-fastapi-frontend/vue2/mobile/src/pages/register.vue
@@ -0,0 +1,207 @@
+
+
+
+
+
+
+
+ RuoYi-FastAPI移动端注册
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 使用已有账号登录
+
+
+
+
+
+
+
+
+
diff --git a/ruoyi-fastapi-frontend/vue2/mobile/src/pages/work/index.vue b/ruoyi-fastapi-frontend/vue2/mobile/src/pages/work/index.vue
new file mode 100644
index 0000000..f4c71c5
--- /dev/null
+++ b/ruoyi-fastapi-frontend/vue2/mobile/src/pages/work/index.vue
@@ -0,0 +1,235 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ AI 生产力
+
+
+
+
+
+
+
+
+ 智能对话
+ 智能助手
+
+
+
+
+
+
+
+
+ 图像生成
+ 创意工坊
+
+
+
+
+
+
+
+
+
+ 系统管理
+
+
+
+
+
+
+
+ 用户管理
+
+
+
+
+
+ 角色管理
+
+
+
+
+
+ 部门管理
+
+
+
+
+
+ 配置管理
+
+
+
+
+
+
+ 字典管理
+
+
+
+
+
+ 通知公告
+
+
+
+
+
+ 日志管理
+
+
+
+
+
+ 更多
+
+
+
+
+
+
+
+
+
+
diff --git a/ruoyi-fastapi-app/src/permission.js b/ruoyi-fastapi-frontend/vue2/mobile/src/permission.js
similarity index 91%
rename from ruoyi-fastapi-app/src/permission.js
rename to ruoyi-fastapi-frontend/vue2/mobile/src/permission.js
index 31a8abf..31152ce 100644
--- a/ruoyi-fastapi-app/src/permission.js
+++ b/ruoyi-fastapi-frontend/vue2/mobile/src/permission.js
@@ -1,43 +1,44 @@
-import { getToken } from "@/utils/auth";
-
-// 登录页面
-const loginPage = "/pages/login";
-
-// 页面白名单
-const whiteList = [
- "/pages/login",
- "/pages/register",
- "/pages/common/webview/index",
- "/pages/common/agreement/index",
- "/pages/common/privacy/index",
-];
-
-// 检查地址白名单
-function checkWhite(url) {
- const path = url.split("?")[0];
- return whiteList.indexOf(path) !== -1;
-}
-
-// 页面跳转验证拦截器
-let list = ["navigateTo", "redirectTo", "reLaunch", "switchTab"];
-list.forEach((item) => {
- uni.addInterceptor(item, {
- invoke(to) {
- if (getToken()) {
- if (to.url === loginPage) {
- uni.reLaunch({ url: "/" });
- }
- return true;
- } else {
- if (checkWhite(to.url)) {
- return true;
- }
- uni.reLaunch({ url: loginPage });
- return false;
- }
- },
- fail(err) {
- console.log(err);
- },
- });
-});
+import { getToken } from "@/utils/auth";
+
+// 登录页面
+const loginPage = "/pages/login";
+
+// 页面白名单
+const whiteList = [
+ "/pages/login",
+ "/pages/register",
+ "/pages/common/webview/index",
+ "/pages/common/agreement/index",
+ "/pages/common/privacy/index",
+];
+
+// 检查地址白名单
+function checkWhite(url) {
+ const path = url.split("?")[0];
+ return whiteList.indexOf(path) !== -1;
+}
+
+// 页面跳转验证拦截器
+let list = ["navigateTo", "redirectTo", "reLaunch", "switchTab"];
+list.forEach((item) => {
+ uni.addInterceptor(item, {
+ invoke(to) {
+ if (getToken()) {
+ if (to.url.split("?")[0] === loginPage) {
+ uni.reLaunch({ url: "/" });
+ return false;
+ }
+ return true;
+ } else {
+ if (checkWhite(to.url)) {
+ return true;
+ }
+ uni.reLaunch({ url: loginPage });
+ return false;
+ }
+ },
+ fail(err) {
+ console.log(err);
+ },
+ });
+});
diff --git a/ruoyi-fastapi-frontend/vue2/mobile/src/plugins/auth.js b/ruoyi-fastapi-frontend/vue2/mobile/src/plugins/auth.js
new file mode 100644
index 0000000..4582585
--- /dev/null
+++ b/ruoyi-fastapi-frontend/vue2/mobile/src/plugins/auth.js
@@ -0,0 +1,60 @@
+import store from "@/store";
+
+function authPermission(permission) {
+ const all_permission = "*:*:*";
+ const permissions = store.getters && store.getters.permissions;
+ if (permission && permission.length > 0) {
+ return permissions.some((v) => {
+ return all_permission === v || v === permission;
+ });
+ } else {
+ return false;
+ }
+}
+
+function authRole(role) {
+ const super_admin = "admin";
+ const roles = store.getters && store.getters.roles;
+ if (role && role.length > 0) {
+ return roles.some((v) => {
+ return super_admin === v || v === role;
+ });
+ } else {
+ return false;
+ }
+}
+
+export default {
+ // 验证用户是否具备某权限
+ hasPermi(permission) {
+ return authPermission(permission);
+ },
+ // 验证用户是否含有指定权限,只需包含其中一个
+ hasPermiOr(permissions) {
+ return permissions.some((item) => {
+ return authPermission(item);
+ });
+ },
+ // 验证用户是否含有指定权限,必须全部拥有
+ hasPermiAnd(permissions) {
+ return permissions.every((item) => {
+ return authPermission(item);
+ });
+ },
+ // 验证用户是否具备某角色
+ hasRole(role) {
+ return authRole(role);
+ },
+ // 验证用户是否含有指定角色,只需包含其中一个
+ hasRoleOr(roles) {
+ return roles.some((item) => {
+ return authRole(item);
+ });
+ },
+ // 验证用户是否含有指定角色,必须全部拥有
+ hasRoleAnd(roles) {
+ return roles.every((item) => {
+ return authRole(item);
+ });
+ },
+};
diff --git a/ruoyi-fastapi-frontend/vue2/mobile/src/plugins/index.js b/ruoyi-fastapi-frontend/vue2/mobile/src/plugins/index.js
new file mode 100644
index 0000000..83a40b5
--- /dev/null
+++ b/ruoyi-fastapi-frontend/vue2/mobile/src/plugins/index.js
@@ -0,0 +1,14 @@
+import tab from "./tab";
+import auth from "./auth";
+import modal from "./modal";
+
+export default {
+ install(Vue) {
+ // 页签操作
+ Vue.prototype.$tab = tab;
+ // 认证对象
+ Vue.prototype.$auth = auth;
+ // 模态框对象
+ Vue.prototype.$modal = modal;
+ },
+};
diff --git a/ruoyi-fastapi-app/src/plugins/modal.js b/ruoyi-fastapi-frontend/vue2/mobile/src/plugins/modal.js
similarity index 94%
rename from ruoyi-fastapi-app/src/plugins/modal.js
rename to ruoyi-fastapi-frontend/vue2/mobile/src/plugins/modal.js
index 54374e1..175d11c 100644
--- a/ruoyi-fastapi-app/src/plugins/modal.js
+++ b/ruoyi-fastapi-frontend/vue2/mobile/src/plugins/modal.js
@@ -1,78 +1,78 @@
-export default {
- // 消息提示
- msg(content) {
- uni.showToast({
- title: content,
- icon: "none",
- });
- },
- // 错误消息
- msgError(content) {
- uni.showToast({
- title: content,
- icon: "error",
- });
- },
- // 成功消息
- msgSuccess(content) {
- uni.showToast({
- title: content,
- icon: "success",
- });
- },
- // 隐藏消息
- hideMsg(content) {
- uni.hideToast();
- },
- // 弹出提示
- alert(content, title) {
- uni.showModal({
- title: title || "系统提示",
- content: content,
- showCancel: false,
- });
- },
- // 确认窗体
- confirm(content, title) {
- return new Promise((resolve, reject) => {
- uni.showModal({
- title: title || "系统提示",
- content: content,
- cancelText: "取消",
- confirmText: "确定",
- success: function (res) {
- if (res.confirm) {
- resolve(res.confirm);
- }
- },
- });
- });
- },
- // 提示信息
- showToast(option) {
- if (typeof option === "object") {
- uni.showToast(option);
- } else {
- uni.showToast({
- title: option,
- icon: "none",
- duration: 2500,
- });
- }
- },
- // 打开遮罩层
- loading(content) {
- uni.showLoading({
- title: content,
- icon: "none",
- });
- },
- // 关闭遮罩层
- closeLoading() {
- try {
- uni.hideLoading();
- } catch (e) {
- console.log(e);
- }
- },
-};
+export default {
+ // 消息提示
+ msg(content) {
+ uni.showToast({
+ title: content,
+ icon: "none",
+ });
+ },
+ // 错误消息
+ msgError(content) {
+ uni.showToast({
+ title: content,
+ icon: "error",
+ });
+ },
+ // 成功消息
+ msgSuccess(content) {
+ uni.showToast({
+ title: content,
+ icon: "success",
+ });
+ },
+ // 隐藏消息
+ hideMsg(content) {
+ uni.hideToast();
+ },
+ // 弹出提示
+ alert(content, title) {
+ uni.showModal({
+ title: title || "系统提示",
+ content: content,
+ showCancel: false,
+ });
+ },
+ // 确认窗体
+ confirm(content, title) {
+ return new Promise((resolve, reject) => {
+ uni.showModal({
+ title: title || "系统提示",
+ content: content,
+ cancelText: "取消",
+ confirmText: "确定",
+ success: function (res) {
+ if (res.confirm) {
+ resolve(res.confirm);
+ }
+ },
+ });
+ });
+ },
+ // 提示信息
+ showToast(option) {
+ if (typeof option === "object") {
+ uni.showToast(option);
+ } else {
+ uni.showToast({
+ title: option,
+ icon: "none",
+ duration: 2500,
+ });
+ }
+ },
+ // 打开遮罩层
+ loading(content) {
+ uni.showLoading({
+ title: content,
+ icon: "none",
+ });
+ },
+ // 关闭遮罩层
+ closeLoading() {
+ try {
+ uni.hideLoading();
+ } catch (e) {
+ console.log(e);
+ }
+ },
+};
diff --git a/ruoyi-fastapi-app/src/plugins/tab.js b/ruoyi-fastapi-frontend/vue2/mobile/src/plugins/tab.js
similarity index 95%
rename from ruoyi-fastapi-app/src/plugins/tab.js
rename to ruoyi-fastapi-frontend/vue2/mobile/src/plugins/tab.js
index bd3135d..c68475b 100644
--- a/ruoyi-fastapi-app/src/plugins/tab.js
+++ b/ruoyi-fastapi-frontend/vue2/mobile/src/plugins/tab.js
@@ -1,30 +1,30 @@
-export default {
- // 关闭所有页面,打开到应用内的某个页面
- reLaunch(url) {
- return uni.reLaunch({
- url: url,
- });
- },
- // 跳转到tabBar页面,并关闭其他所有非tabBar页面
- switchTab(url) {
- return uni.switchTab({
- url: url,
- });
- },
- // 关闭当前页面,跳转到应用内的某个页面
- redirectTo(url) {
- return uni.redirectTo({
- url: url,
- });
- },
- // 保留当前页面,跳转到应用内的某个页面
- navigateTo(url) {
- return uni.navigateTo({
- url: url,
- });
- },
- // 关闭当前页面,返回上一页面或多级页面
- navigateBack() {
- return uni.navigateBack();
- },
-};
+export default {
+ // 关闭所有页面,打开到应用内的某个页面
+ reLaunch(url) {
+ return uni.reLaunch({
+ url: url,
+ });
+ },
+ // 跳转到tabBar页面,并关闭其他所有非tabBar页面
+ switchTab(url) {
+ return uni.switchTab({
+ url: url,
+ });
+ },
+ // 关闭当前页面,跳转到应用内的某个页面
+ redirectTo(url) {
+ return uni.redirectTo({
+ url: url,
+ });
+ },
+ // 保留当前页面,跳转到应用内的某个页面
+ navigateTo(url) {
+ return uni.navigateTo({
+ url: url,
+ });
+ },
+ // 关闭当前页面,返回上一页面或多级页面
+ navigateBack() {
+ return uni.navigateBack();
+ },
+};
diff --git a/ruoyi-fastapi-frontend/vue2/mobile/src/sfc.d.ts b/ruoyi-fastapi-frontend/vue2/mobile/src/sfc.d.ts
new file mode 100644
index 0000000..0660bd6
--- /dev/null
+++ b/ruoyi-fastapi-frontend/vue2/mobile/src/sfc.d.ts
@@ -0,0 +1,4 @@
+declare module "*.vue" {
+ import Vue from "vue";
+ export default Vue;
+}
diff --git a/ruoyi-fastapi-app/src/static/favicon.ico b/ruoyi-fastapi-frontend/vue2/mobile/src/static/favicon.ico
similarity index 100%
rename from ruoyi-fastapi-app/src/static/favicon.ico
rename to ruoyi-fastapi-frontend/vue2/mobile/src/static/favicon.ico
diff --git a/ruoyi-fastapi-app/src/static/images/banner/banner01.jpg b/ruoyi-fastapi-frontend/vue2/mobile/src/static/images/banner/banner01.jpg
similarity index 100%
rename from ruoyi-fastapi-app/src/static/images/banner/banner01.jpg
rename to ruoyi-fastapi-frontend/vue2/mobile/src/static/images/banner/banner01.jpg
diff --git a/ruoyi-fastapi-app/src/static/images/banner/banner02.jpg b/ruoyi-fastapi-frontend/vue2/mobile/src/static/images/banner/banner02.jpg
similarity index 100%
rename from ruoyi-fastapi-app/src/static/images/banner/banner02.jpg
rename to ruoyi-fastapi-frontend/vue2/mobile/src/static/images/banner/banner02.jpg
diff --git a/ruoyi-fastapi-app/src/static/images/banner/banner03.jpg b/ruoyi-fastapi-frontend/vue2/mobile/src/static/images/banner/banner03.jpg
similarity index 100%
rename from ruoyi-fastapi-app/src/static/images/banner/banner03.jpg
rename to ruoyi-fastapi-frontend/vue2/mobile/src/static/images/banner/banner03.jpg
diff --git a/ruoyi-fastapi-app/src/static/images/profile.jpg b/ruoyi-fastapi-frontend/vue2/mobile/src/static/images/profile.jpg
similarity index 100%
rename from ruoyi-fastapi-app/src/static/images/profile.jpg
rename to ruoyi-fastapi-frontend/vue2/mobile/src/static/images/profile.jpg
diff --git a/ruoyi-fastapi-app/src/static/images/tabbar/home.png b/ruoyi-fastapi-frontend/vue2/mobile/src/static/images/tabbar/home.png
similarity index 100%
rename from ruoyi-fastapi-app/src/static/images/tabbar/home.png
rename to ruoyi-fastapi-frontend/vue2/mobile/src/static/images/tabbar/home.png
diff --git a/ruoyi-fastapi-app/src/static/images/tabbar/home_.png b/ruoyi-fastapi-frontend/vue2/mobile/src/static/images/tabbar/home_.png
similarity index 100%
rename from ruoyi-fastapi-app/src/static/images/tabbar/home_.png
rename to ruoyi-fastapi-frontend/vue2/mobile/src/static/images/tabbar/home_.png
diff --git a/ruoyi-fastapi-app/src/static/images/tabbar/mine.png b/ruoyi-fastapi-frontend/vue2/mobile/src/static/images/tabbar/mine.png
similarity index 100%
rename from ruoyi-fastapi-app/src/static/images/tabbar/mine.png
rename to ruoyi-fastapi-frontend/vue2/mobile/src/static/images/tabbar/mine.png
diff --git a/ruoyi-fastapi-app/src/static/images/tabbar/mine_.png b/ruoyi-fastapi-frontend/vue2/mobile/src/static/images/tabbar/mine_.png
similarity index 100%
rename from ruoyi-fastapi-app/src/static/images/tabbar/mine_.png
rename to ruoyi-fastapi-frontend/vue2/mobile/src/static/images/tabbar/mine_.png
diff --git a/ruoyi-fastapi-app/src/static/images/tabbar/work.png b/ruoyi-fastapi-frontend/vue2/mobile/src/static/images/tabbar/work.png
similarity index 100%
rename from ruoyi-fastapi-app/src/static/images/tabbar/work.png
rename to ruoyi-fastapi-frontend/vue2/mobile/src/static/images/tabbar/work.png
diff --git a/ruoyi-fastapi-app/src/static/images/tabbar/work_.png b/ruoyi-fastapi-frontend/vue2/mobile/src/static/images/tabbar/work_.png
similarity index 100%
rename from ruoyi-fastapi-app/src/static/images/tabbar/work_.png
rename to ruoyi-fastapi-frontend/vue2/mobile/src/static/images/tabbar/work_.png
diff --git a/ruoyi-fastapi-app/src/static/logo.png b/ruoyi-fastapi-frontend/vue2/mobile/src/static/logo.png
similarity index 100%
rename from ruoyi-fastapi-app/src/static/logo.png
rename to ruoyi-fastapi-frontend/vue2/mobile/src/static/logo.png
diff --git a/ruoyi-fastapi-app/src/static/logo200.png b/ruoyi-fastapi-frontend/vue2/mobile/src/static/logo200.png
similarity index 100%
rename from ruoyi-fastapi-app/src/static/logo200.png
rename to ruoyi-fastapi-frontend/vue2/mobile/src/static/logo200.png
diff --git a/ruoyi-fastapi-frontend/vue2/mobile/src/store/getters.js b/ruoyi-fastapi-frontend/vue2/mobile/src/store/getters.js
new file mode 100644
index 0000000..b580ea0
--- /dev/null
+++ b/ruoyi-fastapi-frontend/vue2/mobile/src/store/getters.js
@@ -0,0 +1,9 @@
+const getters = {
+ token: state => state.user.token,
+ avatar: state => state.user.avatar,
+ id: state => state.user.id,
+ name: state => state.user.name,
+ roles: state => state.user.roles,
+ permissions: state => state.user.permissions
+}
+export default getters
diff --git a/ruoyi-fastapi-frontend/vue2/mobile/src/store/index.js b/ruoyi-fastapi-frontend/vue2/mobile/src/store/index.js
new file mode 100644
index 0000000..8d0d94c
--- /dev/null
+++ b/ruoyi-fastapi-frontend/vue2/mobile/src/store/index.js
@@ -0,0 +1,15 @@
+import Vue from "vue";
+import Vuex from "vuex";
+import user from "@/store/modules/user";
+import getters from "./getters";
+
+Vue.use(Vuex);
+
+const store = new Vuex.Store({
+ modules: {
+ user,
+ },
+ getters,
+});
+
+export default store;
diff --git a/ruoyi-fastapi-frontend/vue2/mobile/src/store/modules/user.js b/ruoyi-fastapi-frontend/vue2/mobile/src/store/modules/user.js
new file mode 100644
index 0000000..4682d86
--- /dev/null
+++ b/ruoyi-fastapi-frontend/vue2/mobile/src/store/modules/user.js
@@ -0,0 +1,147 @@
+import { setBusinessTimezone, setUserTimezone } from "@/utils/time";
+import config from "@/config";
+import storage from "@/utils/storage";
+import constant from "@/utils/constant";
+import { isHttp, isEmpty } from "@/utils/validate";
+import { login, logout, getInfo } from "@/api/login";
+import { getToken, setToken, removeToken } from "@/utils/auth";
+import defAva from "@/static/images/profile.jpg";
+
+const baseUrl = config.baseUrl;
+
+const user = {
+ state: {
+ token: getToken(),
+ appTimezone: "Asia/Shanghai",
+ timeZone: "auto",
+ id: storage.get(constant.id),
+ name: storage.get(constant.name),
+ avatar: storage.get(constant.avatar),
+ roles: storage.get(constant.roles),
+ permissions: storage.get(constant.permissions),
+ },
+
+ mutations: {
+ SET_APP_TIMEZONE: (state, timezone) => {
+ setBusinessTimezone(timezone);
+ state.appTimezone = timezone;
+ },
+ SET_TIMEZONE: (state, preference = "auto") => {
+ setUserTimezone(preference);
+ state.timeZone = preference;
+ storage.set(constant.timezone, { appTimezone: state.appTimezone, timeZone: preference });
+ },
+ SET_TOKEN: (state, token) => {
+ state.token = token;
+ },
+ SET_ID: (state, id) => {
+ state.id = id;
+ storage.set(constant.id, id);
+ },
+ SET_NAME: (state, name) => {
+ state.name = name;
+ storage.set(constant.name, name);
+ },
+ SET_AVATAR: (state, avatar) => {
+ state.avatar = avatar;
+ storage.set(constant.avatar, avatar);
+ },
+ SET_ROLES: (state, roles) => {
+ state.roles = roles;
+ storage.set(constant.roles, roles);
+ },
+ SET_PERMISSIONS: (state, permissions) => {
+ state.permissions = permissions;
+ storage.set(constant.permissions, permissions);
+ },
+ },
+
+ actions: {
+ ApplyTimezone({ commit }, preference = "auto") {
+ commit("SET_TIMEZONE", preference);
+ },
+ // 登录
+ Login({ commit }, userInfo) {
+ const username = userInfo.username.trim();
+ const password = userInfo.password;
+ const code = userInfo.code;
+ const uuid = userInfo.uuid;
+ return new Promise((resolve, reject) => {
+ login(username, password, code, uuid)
+ .then((res) => {
+ setToken(res.token);
+ commit("SET_TOKEN", res.token);
+ resolve();
+ })
+ .catch((error) => {
+ reject(error);
+ });
+ });
+ },
+
+ // 获取用户信息
+ GetInfo({ commit, state }) {
+ return new Promise((resolve, reject) => {
+ getInfo()
+ .then((res) => {
+ const user = res.user;
+ commit("SET_APP_TIMEZONE", res.appTimezone);
+ commit("SET_TIMEZONE", user.timeZone);
+ let avatar = user.avatar || "";
+ if (!isHttp(avatar)) {
+ avatar = isEmpty(avatar) ? defAva : baseUrl + avatar;
+ }
+ const userid =
+ isEmpty(user) || isEmpty(user.userId) ? "" : user.userId;
+ const username =
+ isEmpty(user) || isEmpty(user.userName) ? "" : user.userName;
+ if (res.roles && res.roles.length > 0) {
+ commit("SET_ROLES", res.roles);
+ commit("SET_PERMISSIONS", res.permissions);
+ } else {
+ commit("SET_ROLES", ["ROLE_DEFAULT"]);
+ }
+ commit("SET_ID", userid);
+ commit("SET_NAME", username);
+ commit("SET_AVATAR", avatar);
+ resolve(res);
+ })
+ .catch((error) => {
+ reject(error);
+ });
+ });
+ },
+
+ // 退出系统
+ LogOut({ commit, state }) {
+ return new Promise((resolve, reject) => {
+ logout(state.token)
+ .then(() => {
+ commit("SET_TOKEN", "");
+ commit("SET_TIMEZONE", "auto");
+ commit("SET_ROLES", []);
+ commit("SET_PERMISSIONS", []);
+ removeToken();
+ storage.clean();
+ resolve();
+ })
+ .catch((error) => {
+ reject(error);
+ });
+ });
+ },
+ },
+};
+
+// 恢复已登录账号的偏好,避免冷启动首屏使用错误时区。
+const savedTimezone = storage.get(constant.timezone);
+if (user.state.token && savedTimezone) {
+ try {
+ user.mutations.SET_APP_TIMEZONE(user.state, savedTimezone.appTimezone);
+ user.mutations.SET_TIMEZONE(user.state, savedTimezone.timeZone);
+ } catch {
+ storage.remove(constant.timezone);
+ }
+}
+
+export default user;
diff --git a/ruoyi-fastapi-app/src/uni.scss b/ruoyi-fastapi-frontend/vue2/mobile/src/uni.scss
similarity index 100%
rename from ruoyi-fastapi-app/src/uni.scss
rename to ruoyi-fastapi-frontend/vue2/mobile/src/uni.scss
diff --git a/ruoyi-fastapi-app/src/utils/auth.js b/ruoyi-fastapi-frontend/vue2/mobile/src/utils/auth.js
similarity index 95%
rename from ruoyi-fastapi-app/src/utils/auth.js
rename to ruoyi-fastapi-frontend/vue2/mobile/src/utils/auth.js
index f22d9b5..0e5b269 100644
--- a/ruoyi-fastapi-app/src/utils/auth.js
+++ b/ruoyi-fastapi-frontend/vue2/mobile/src/utils/auth.js
@@ -1,13 +1,13 @@
-const TokenKey = "App-Token";
-
-export function getToken() {
- return uni.getStorageSync(TokenKey);
-}
-
-export function setToken(token) {
- return uni.setStorageSync(TokenKey, token);
-}
-
-export function removeToken() {
- return uni.removeStorageSync(TokenKey);
-}
+const TokenKey = "App-Token";
+
+export function getToken() {
+ return uni.getStorageSync(TokenKey);
+}
+
+export function setToken(token) {
+ return uni.setStorageSync(TokenKey, token);
+}
+
+export function removeToken() {
+ return uni.removeStorageSync(TokenKey);
+}
diff --git a/ruoyi-fastapi-app/src/utils/common.js b/ruoyi-fastapi-frontend/vue2/mobile/src/utils/common.js
similarity index 100%
rename from ruoyi-fastapi-app/src/utils/common.js
rename to ruoyi-fastapi-frontend/vue2/mobile/src/utils/common.js
diff --git a/ruoyi-fastapi-app/src/utils/constant.js b/ruoyi-fastapi-frontend/vue2/mobile/src/utils/constant.js
similarity index 100%
rename from ruoyi-fastapi-app/src/utils/constant.js
rename to ruoyi-fastapi-frontend/vue2/mobile/src/utils/constant.js
diff --git a/ruoyi-fastapi-app/src/utils/errorCode.js b/ruoyi-fastapi-frontend/vue2/mobile/src/utils/errorCode.js
similarity index 97%
rename from ruoyi-fastapi-app/src/utils/errorCode.js
rename to ruoyi-fastapi-frontend/vue2/mobile/src/utils/errorCode.js
index 59c0a01..984fded 100644
--- a/ruoyi-fastapi-app/src/utils/errorCode.js
+++ b/ruoyi-fastapi-frontend/vue2/mobile/src/utils/errorCode.js
@@ -1,6 +1,6 @@
-export default {
- 401: "认证失败,无法访问系统资源",
- 403: "当前操作没有权限",
- 404: "访问资源不存在",
- default: "系统未知错误,请反馈给管理员",
-};
+export default {
+ 401: "认证失败,无法访问系统资源",
+ 403: "当前操作没有权限",
+ 404: "访问资源不存在",
+ default: "系统未知错误,请反馈给管理员",
+};
diff --git a/ruoyi-fastapi-app/src/utils/permission.js b/ruoyi-fastapi-frontend/vue2/mobile/src/utils/permission.js
similarity index 96%
rename from ruoyi-fastapi-app/src/utils/permission.js
rename to ruoyi-fastapi-frontend/vue2/mobile/src/utils/permission.js
index cd67d99..e30a24c 100644
--- a/ruoyi-fastapi-app/src/utils/permission.js
+++ b/ruoyi-fastapi-frontend/vue2/mobile/src/utils/permission.js
@@ -1,55 +1,55 @@
-import store from "@/store";
-
-/**
- * 字符权限校验
- * @param {Array} value 校验值
- * @returns {Boolean}
- */
-export function checkPermi(value) {
- if (value && value instanceof Array && value.length > 0) {
- const permissions = store.getters && store.getters.permissions;
- const permissionDatas = value;
- const all_permission = "*:*:*";
-
- const hasPermission = permissions.some((permission) => {
- return (
- all_permission === permission || permissionDatas.includes(permission)
- );
- });
-
- if (!hasPermission) {
- return false;
- }
- return true;
- } else {
- console.error(
- `need roles! Like checkPermi="['system:user:add','system:user:edit']"`,
- );
- return false;
- }
-}
-
-/**
- * 角色权限校验
- * @param {Array} value 校验值
- * @returns {Boolean}
- */
-export function checkRole(value) {
- if (value && value instanceof Array && value.length > 0) {
- const roles = store.getters && store.getters.roles;
- const permissionRoles = value;
- const super_admin = "admin";
-
- const hasRole = roles.some((role) => {
- return super_admin === role || permissionRoles.includes(role);
- });
-
- if (!hasRole) {
- return false;
- }
- return true;
- } else {
- console.error(`need roles! Like checkRole="['admin','editor']"`);
- return false;
- }
-}
+import store from "@/store";
+
+/**
+ * 字符权限校验
+ * @param {Array} value 校验值
+ * @returns {Boolean}
+ */
+export function checkPermi(value) {
+ if (value && value instanceof Array && value.length > 0) {
+ const permissions = store.getters && store.getters.permissions;
+ const permissionDatas = value;
+ const all_permission = "*:*:*";
+
+ const hasPermission = permissions.some((permission) => {
+ return (
+ all_permission === permission || permissionDatas.includes(permission)
+ );
+ });
+
+ if (!hasPermission) {
+ return false;
+ }
+ return true;
+ } else {
+ console.error(
+ `need roles! Like checkPermi="['system:user:add','system:user:edit']"`,
+ );
+ return false;
+ }
+}
+
+/**
+ * 角色权限校验
+ * @param {Array} value 校验值
+ * @returns {Boolean}
+ */
+export function checkRole(value) {
+ if (value && value instanceof Array && value.length > 0) {
+ const roles = store.getters && store.getters.roles;
+ const permissionRoles = value;
+ const super_admin = "admin";
+
+ const hasRole = roles.some((role) => {
+ return super_admin === role || permissionRoles.includes(role);
+ });
+
+ if (!hasRole) {
+ return false;
+ }
+ return true;
+ } else {
+ console.error(`need roles! Like checkRole="['admin','editor']"`);
+ return false;
+ }
+}
diff --git a/ruoyi-fastapi-frontend/vue2/mobile/src/utils/request.js b/ruoyi-fastapi-frontend/vue2/mobile/src/utils/request.js
new file mode 100644
index 0000000..ca2040b
--- /dev/null
+++ b/ruoyi-fastapi-frontend/vue2/mobile/src/utils/request.js
@@ -0,0 +1,140 @@
+import { getDisplayTimezone } from "@/utils/time";
+import store from "@/store";
+import config from "@/config";
+import { getToken } from "@/utils/auth";
+import errorCode from "@/utils/errorCode";
+import { toast, showConfirm, tansParams } from "@/utils/common";
+import {
+ decryptTransportErrorResponse,
+ decryptTransportResponse,
+ encryptTransportRequest,
+ invalidateTransportKeyMeta,
+ resetTransportRequestConfig,
+ shouldRetryTransportWithFreshKey,
+} from "@/utils/transportCrypto";
+
+let timeout = 10000;
+const baseUrl = config.baseUrl;
+
+/**
+ * 对外暴露的统一请求方法。
+ *
+ * @param {Object} config 请求配置
+ * @returns {Promise
+
+
+ 版本身份与最近异常
+
+ - 制品摘要
+ - {{ row.digest || '源码插件' }}
+ - 发布代际
+ - {{ row.generation || '-' }}
+ - 最近异常
+ - {{ row.lastErrorType || '-' }}
+ - 追踪标识
+ - {{ row.lastErrorRequestId || '-' }}
+
+
+
+
+
+
+
+
+
+
diff --git a/ruoyi-fastapi-frontend/vue2/web/src/views/system/plugin/index.vue b/ruoyi-fastapi-frontend/vue2/web/src/views/system/plugin/index.vue
new file mode 100644
index 0000000..95421d0
--- /dev/null
+++ b/ruoyi-fastapi-frontend/vue2/web/src/views/system/plugin/index.vue
@@ -0,0 +1,2821 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 搜索
+ 重置
+
+
+
+
+
+ 安装计划
+
+
+ 启用计划
+
+
+ 升级计划
+
+
+ 审计记录
+
+
+
+ {{ planTargetSummary }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ scope.row.installedVersion || '-' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{
+ getStatusLabel(scope.row.status)
+ }}
+
+
+
+
+
+ -
+
+
+
+
+
+ {{ formatPluginTime(scope.row.updateTime) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 搜索
+ 重置
+
+
+
+
+
+
+
+ 审计保留策略
+ 默认 {{ operationLogRetentionDefaultDays }} 天
+
+
+
+
+
+
+ 预览清理
+ 确认清理
+
+
+
+
+
+
+
+
+
+
+ 导出
+
+
+
+
+
+
+
+ {{
+ formatPluginOperation(scope.row.operation)
+ }}
+
+
+
+ {{
+ getOperationLogStatusLabel(scope.row.status)
+ }}
+
+
+
+ {{
+ formatPlanDependencies(scope.row.pluginIds)
+ }}
+
+
+ {{ getSummaryCount(scope.row, 'succeeded') }}
+
+
+ {{ getSummaryCount(scope.row, 'failed') }}
+
+
+ {{ parseTime(scope.row.createTime) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{
+ operationLogDetail.operationId || '-'
+ }}
+ {{
+ formatPluginOperation(operationLogDetail.operation)
+ }}
+ {{
+ getOperationLogStatusLabel(operationLogDetail.status)
+ }}
+ {{
+ operationLogDetail.dryRun ? '是' : '否'
+ }}
+ {{
+ operationLogDetail.continueOnError ? '是' : '否'
+ }}
+ {{
+ parseTime(operationLogDetail.createTime) || '-'
+ }}
+ {{ formatPlanDependencies(operationLogDetail.pluginIds) }}
+ {{ operationLogDetail.remark || '-' }}
+
+
+
+
+ {{
+ getSummaryCount(operationLogDetail, 'succeeded')
+ }}
+ {{
+ getSummaryCount(operationLogDetail, 'failed')
+ }}
+ {{
+ getSummaryCount(operationLogDetail, 'skipped')
+ }}
+
+ {{ formatPlanDependencies(getSummaryChangedKeys(operationLogDetail)) }}
+
+
+ {{ operationLogDetail.failedSuggestion || '暂无' }}
+
+
+ {{ getOperationFailedStep(operationLogDetail) }}
+
+
+
+
+
+
+
+
+ {{
+ getValidationLevelLabel(scope.row.level)
+ }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{
+ scope.row.enabled ? '是' : '否'
+ }}
+
+
+
+ {{ formatNullable(scope.row.count) }}
+
+
+ {{ formatActionPlanState(scope.row) }}
+
+
+
+
+
+
+
+
+
+ {{
+ scope.row.ready ? '是' : '否'
+ }}
+
+
+
+ {{
+ formatPlanDependencies(scope.row.dependencies)
+ }}
+
+
+
+
+
+
+
+ {{
+ formatPluginOperation(scope.row.operation)
+ }}
+
+
+
+ {{
+ scope.row.status || '-'
+ }}
+
+
+
+ {{ formatNullable(scope.row.durationMs) }}ms
+
+
+ {{ getExecutedFailedStep(scope.row) }}
+
+
+
+
+
+
+
+
+
+
+
+ {{
+ scope.row.secret ? '是' : '否'
+ }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{
+ actionResult.pluginId || '-'
+ }}
+ {{
+ formatBoolean(actionResult.dependencyOk)
+ }}
+ {{
+ formatBoolean(actionResult.structureOk)
+ }}
+ {{
+ formatBoolean(actionResult.menuConflictOk)
+ }}
+ {{
+ actionResult.dryRun ? '是' : '否'
+ }}
+ {{
+ formatBoolean(actionResult.needsUpgrade)
+ }}
+
+
+
+
+
+
+ {{
+ actionResult.pluginId || pendingAction.pluginId || '-'
+ }}
+ {{
+ actionResult.operation
+ ? formatPluginOperation(actionResult.operation)
+ : pendingAction.label || '-'
+ }}
+ {{
+ actionResult.failedStep || actionResult.error.failedStep || '-'
+ }}
+
+ {{ actionResult.migrationRecovery.migrationPath || '-' }}
+
+
+ {{ actionResult.migrationRecovery.status || '-' }}
+
+ {{
+ actionResult.error.suggestion || '请查看后端日志或重新执行检查。'
+ }}
+
+
+
+
+
+
+
+
+ {{
+ scope.row.enabled ? '是' : '否'
+ }}
+
+
+
+
+
+ {{ scope.row.ok ? '通过' : '异常' }}
+
+ -
+
+
+
+ {{ formatNullable(scope.row.count) }}
+
+
+
+
+
+
+
+
+ {{ getValidationLevelLabel(scope.row.level) }}
+
+
+
+
+
+
+
+ {{
+ formatDependencyInstalled(scope.row)
+ }}
+
+
+
+
+ {{
+ formatDependencyVersionSatisfied(scope.row)
+ }}
+
+
+
+
+
+
+
+
+
+
+ {{ getValidationLevelLabel(scope.row.level) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ getValidationLevelLabel(scope.row.level) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ getValidationLevelLabel(scope.row.level) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ruoyi-fastapi-frontend/vue2/web/src/views/system/post/index.vue b/ruoyi-fastapi-frontend/vue2/web/src/views/system/post/index.vue
new file mode 100644
index 0000000..b6782d1
--- /dev/null
+++ b/ruoyi-fastapi-frontend/vue2/web/src/views/system/post/index.vue
@@ -0,0 +1,441 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 搜索
+ 重置
+
+
+
+
+
+ 新增
+
+
+ 修改
+
+
+ 删除
+
+
+ 导出
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ parseTime(scope.row.createTime) }}
+
+
+
+
+ 修改
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ dict.label }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ruoyi-fastapi-frontend/vue2/web/src/views/system/role/authUser.vue b/ruoyi-fastapi-frontend/vue2/web/src/views/system/role/authUser.vue
new file mode 100644
index 0000000..7f5fde8
--- /dev/null
+++ b/ruoyi-fastapi-frontend/vue2/web/src/views/system/role/authUser.vue
@@ -0,0 +1,283 @@
+
+
+
+
+
+
+
+
+
+
+ 搜索
+ 重置
+
+
+
+
+
+ 添加用户
+
+
+ 批量取消授权
+
+
+ 关闭
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ parseTime(scope.row.createTime) }}
+
+
+
+
+ 取消授权
+
+
+
+
+
+
+
+
+
+
diff --git a/ruoyi-fastapi-frontend/vue2/web/src/views/system/role/index.vue b/ruoyi-fastapi-frontend/vue2/web/src/views/system/role/index.vue
new file mode 100644
index 0000000..47a46e6
--- /dev/null
+++ b/ruoyi-fastapi-frontend/vue2/web/src/views/system/role/index.vue
@@ -0,0 +1,815 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 搜索
+ 重置
+
+
+
+
+
+ 新增
+
+
+ 修改
+
+
+ 删除
+
+
+ 导出
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ parseTime(scope.row.createTime) }}
+
+
+
+
+ 修改
+ 删除
+ handleCommand(command, scope.row)"
+ v-hasPermi="['system:role:edit']"
+ >
+ 更多
+
+ 数据权限
+ 分配用户
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 权限字符
+
+
+
+
+
+
+
+
+ {{ dict.label }}
+
+
+
+ 展开/折叠
+ 全选/全不选
+ 父子联动
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 展开/折叠
+ 全选/全不选
+ 父子联动
+
+
+
+
+
+
+
+
+
diff --git a/ruoyi-fastapi-frontend/vue2/web/src/views/system/role/selectUser.vue b/ruoyi-fastapi-frontend/vue2/web/src/views/system/role/selectUser.vue
new file mode 100644
index 0000000..691f362
--- /dev/null
+++ b/ruoyi-fastapi-frontend/vue2/web/src/views/system/role/selectUser.vue
@@ -0,0 +1,208 @@
+
+
+
+
+
+
+
+
+
+
+
+ 搜索
+ 重置
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ parseTime(scope.row.createTime) }}
+
+
+
+
+
+
+
+
+
+
diff --git a/ruoyi-fastapi-frontend/vue2/web/src/views/system/user/authRole.vue b/ruoyi-fastapi-frontend/vue2/web/src/views/system/user/authRole.vue
new file mode 100644
index 0000000..7dda523
--- /dev/null
+++ b/ruoyi-fastapi-frontend/vue2/web/src/views/system/user/authRole.vue
@@ -0,0 +1,187 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ (pageNum - 1) * pageSize + scope.$index + 1 }}
+
+
+
+
+
+
+
+
+ {{ parseTime(scope.row.createTime) }}
+
+
+
+
+
+
+
+
+ 提交
+ 返回
+
+
+
+
+
+
diff --git a/ruoyi-fastapi-frontend/vue2/web/src/views/system/user/index.vue b/ruoyi-fastapi-frontend/vue2/web/src/views/system/user/index.vue
new file mode 100644
index 0000000..2e477ee
--- /dev/null
+++ b/ruoyi-fastapi-frontend/vue2/web/src/views/system/user/index.vue
@@ -0,0 +1,830 @@
+
+
+
+
+
diff --git a/ruoyi-fastapi-frontend/vue2/web/src/views/system/user/profile/index.vue b/ruoyi-fastapi-frontend/vue2/web/src/views/system/user/profile/index.vue
new file mode 100644
index 0000000..3dd093c
--- /dev/null
+++ b/ruoyi-fastapi-frontend/vue2/web/src/views/system/user/profile/index.vue
@@ -0,0 +1,129 @@
+
+
+
+
+
+
+ 个人信息
+
+
+
+
+
+
+
+ 基本资料
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ruoyi-fastapi-frontend/vue2/web/src/views/system/user/profile/resetPwd.vue b/ruoyi-fastapi-frontend/vue2/web/src/views/system/user/profile/resetPwd.vue
new file mode 100644
index 0000000..2783dad
--- /dev/null
+++ b/ruoyi-fastapi-frontend/vue2/web/src/views/system/user/profile/resetPwd.vue
@@ -0,0 +1,109 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ 保存
+ 关闭
+
+
+
+
+
diff --git a/ruoyi-fastapi-frontend/vue2/web/src/views/system/user/profile/timezoneSettings.vue b/ruoyi-fastapi-frontend/vue2/web/src/views/system/user/profile/timezoneSettings.vue
new file mode 100644
index 0000000..80cb864
--- /dev/null
+++ b/ruoyi-fastapi-frontend/vue2/web/src/views/system/user/profile/timezoneSettings.vue
@@ -0,0 +1,192 @@
+
+
+
+
+ 自动跟随设备
+ 手动选择
+
+
+
+
+
+
+
+
+
+
+ 时区列表加载失败,仍可使用当前时区。
+ 重试
+
+
+
+
+ {{ previewZone }}
+
+
+ {{ previewTime }}
+
+ 当前设备无法识别时区,已使用系统时区;你也可以手动选择。
+
+
+ 用于时间显示、日期筛选和导出。定时任务和业务统计使用各自配置的时区。
+
+
+
+ 保存
+ 关闭
+
+
+
+
+
+
+
diff --git a/ruoyi-fastapi-frontend/vue2/web/src/views/system/user/profile/userAvatar.vue b/ruoyi-fastapi-frontend/vue2/web/src/views/system/user/profile/userAvatar.vue
new file mode 100644
index 0000000..47dd729
--- /dev/null
+++ b/ruoyi-fastapi-frontend/vue2/web/src/views/system/user/profile/userAvatar.vue
@@ -0,0 +1,260 @@
+
+
+
+
![点击上传头像]()
+
+
+
+
+
+
+
+
+
![]()
+
+
+
+
+
+
+
+
+ 选择
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 提 交
+
+
+
+
+
+
+
+
diff --git a/ruoyi-fastapi-frontend/vue2/web/src/views/system/user/profile/userInfo.vue b/ruoyi-fastapi-frontend/vue2/web/src/views/system/user/profile/userInfo.vue
new file mode 100644
index 0000000..a7c805b
--- /dev/null
+++ b/ruoyi-fastapi-frontend/vue2/web/src/views/system/user/profile/userInfo.vue
@@ -0,0 +1,124 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 男
+ 女
+
+
+
+ 保存
+ 关闭
+
+
+
+
+
diff --git a/ruoyi-fastapi-frontend/vue2/web/src/views/system/user/view.vue b/ruoyi-fastapi-frontend/vue2/web/src/views/system/user/view.vue
new file mode 100644
index 0000000..4d3eccd
--- /dev/null
+++ b/ruoyi-fastapi-frontend/vue2/web/src/views/system/user/view.vue
@@ -0,0 +1,231 @@
+
+
+
+
+
+
+
+
+
+ {{ info.nickName }}
+
+
+
+
+
+ {{ info.dept && info.dept.deptName }}
+
+
+
+
+
+
+
+ {{ info.phonenumber }}
+
+
+
+
+
+ {{ info.email }}
+
+
+
+
+
+
+
+ {{ info.userName }}
+
+
+
+
+
+
+ {{ info.status === '0' ? '正常' : '停用' }}
+
+
+
+
+
+
+
+
+ {{ postNames || '无岗位' }}
+
+
+
+
+
+ {{ sexLabel }}
+
+
+
+
+
+
+
+ {{ roleNames || '无角色' }}
+
+
+
+
+
+
+
+
+
+ {{ info.createBy }}
+
+
+
+
+
+ {{ parseTime(info.createTime) }}
+
+
+
+
+
+
+
+ {{ info.updateBy }}
+
+
+
+
+
+ {{ parseTime(info.updateTime) }}
+
+
+
+
+
+
+
+ {{ info.loginIp }}
+
+
+
+
+
+ {{ parseTime(info.loginDate) }}
+
+
+
+
+
+
+
+ {{ info.remark }}
+
+
+
+
+
+
+
+
diff --git a/ruoyi-fastapi-frontend/vue2/web/src/views/tool/build/CodeTypeDialog.vue b/ruoyi-fastapi-frontend/vue2/web/src/views/tool/build/CodeTypeDialog.vue
new file mode 100644
index 0000000..c73de74
--- /dev/null
+++ b/ruoyi-fastapi-frontend/vue2/web/src/views/tool/build/CodeTypeDialog.vue
@@ -0,0 +1,123 @@
+
+
+
+
+
+
+
+
+
+ {{ item.label }}
+
+
+
+
+
+
+
+
+
+
+
+ 取消
+
+ 确定
+
+
+
+
+
+
diff --git a/ruoyi-fastapi-frontend/vue2/web/src/views/tool/build/DraggableItem.vue b/ruoyi-fastapi-frontend/vue2/web/src/views/tool/build/DraggableItem.vue
new file mode 100644
index 0000000..709500b
--- /dev/null
+++ b/ruoyi-fastapi-frontend/vue2/web/src/views/tool/build/DraggableItem.vue
@@ -0,0 +1,136 @@
+
diff --git a/ruoyi-fastapi-frontend/vue2/web/src/views/tool/build/IconsDialog.vue b/ruoyi-fastapi-frontend/vue2/web/src/views/tool/build/IconsDialog.vue
new file mode 100644
index 0000000..1516905
--- /dev/null
+++ b/ruoyi-fastapi-frontend/vue2/web/src/views/tool/build/IconsDialog.vue
@@ -0,0 +1,123 @@
+
+
+
+
+
diff --git a/ruoyi-fastapi-frontend/vue2/web/src/views/tool/build/RightPanel.vue b/ruoyi-fastapi-frontend/vue2/web/src/views/tool/build/RightPanel.vue
new file mode 100644
index 0000000..277f02e
--- /dev/null
+++ b/ruoyi-fastapi-frontend/vue2/web/src/views/tool/build/RightPanel.vue
@@ -0,0 +1,1348 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ item.label }}
+
+
+
+
+
+
+
+
+ {{ activeData.componentName }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 选择
+
+
+
+
+
+
+ 选择
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 默认
+ 右侧
+
+
+
+
+ 个字符
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text
+ picture
+ picture-card
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 选项
+
+
+
+
+
+ 添加选项
+
+
+
+
+
+
+ 选项
+
+
+ 动态数据
+ 静态数据
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 添加父级
+
+
+
+
+
+
+
+ 默认
+ 按钮
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 中等
+ 较小
+ 迷你
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 布局结构树
+
+
+
+
+ {{ node.label }}
+
+
+
+
+
+
+ 正则校验
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 添加规则
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 中等
+ 较小
+ 迷你
+
+
+
+
+ 左对齐
+ 右对齐
+ 顶部对齐
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ruoyi-fastapi-frontend/vue2/web/src/views/tool/build/TreeNodeDialog.vue b/ruoyi-fastapi-frontend/vue2/web/src/views/tool/build/TreeNodeDialog.vue
new file mode 100644
index 0000000..363a385
--- /dev/null
+++ b/ruoyi-fastapi-frontend/vue2/web/src/views/tool/build/TreeNodeDialog.vue
@@ -0,0 +1,146 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 确定
+
+ 取消
+
+
+
+
+
diff --git a/ruoyi-fastapi-frontend/vue2/web/src/views/tool/build/index.vue b/ruoyi-fastapi-frontend/vue2/web/src/views/tool/build/index.vue
new file mode 100644
index 0000000..83ed8dd
--- /dev/null
+++ b/ruoyi-fastapi-frontend/vue2/web/src/views/tool/build/index.vue
@@ -0,0 +1,822 @@
+
+
+
+
+
+
![logo]()
+ Form Generator
+
+
+
+
+
输入型组件
+
+
+
+
+ {{ element.label }}
+
+
+
+
选择型组件
+
+
+
+
+ {{ element.label }}
+
+
+
+
布局型组件
+
+
+
+
+ {{ element.label }}
+
+
+
+
+
+
+
+
+
+
+ 导出vue文件
+
+
+ 复制代码
+
+
+ 清空
+
+
+
+
+
+
+
+
+
+ 从左侧拖入或点选组件进行表单设计
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ruoyi-fastapi-frontend/vue2/web/src/views/tool/gen/basicInfoForm.vue b/ruoyi-fastapi-frontend/vue2/web/src/views/tool/gen/basicInfoForm.vue
new file mode 100644
index 0000000..988c890
--- /dev/null
+++ b/ruoyi-fastapi-frontend/vue2/web/src/views/tool/gen/basicInfoForm.vue
@@ -0,0 +1,88 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ruoyi-fastapi-frontend/vue2/web/src/views/tool/gen/createTable.vue b/ruoyi-fastapi-frontend/vue2/web/src/views/tool/gen/createTable.vue
new file mode 100644
index 0000000..4897e01
--- /dev/null
+++ b/ruoyi-fastapi-frontend/vue2/web/src/views/tool/gen/createTable.vue
@@ -0,0 +1,98 @@
+
+
+
+
+
+
+
+
+
+
+ 创建表语句(支持多个建表语句):
+
+
+
+
+
+
diff --git a/ruoyi-fastapi-frontend/vue2/web/src/views/tool/gen/editTable.vue b/ruoyi-fastapi-frontend/vue2/web/src/views/tool/gen/editTable.vue
new file mode 100644
index 0000000..1c746eb
--- /dev/null
+++ b/ruoyi-fastapi-frontend/vue2/web/src/views/tool/gen/editTable.vue
@@ -0,0 +1,437 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ dict.dictName }}
+ {{
+ dict.dictType
+ }}
+
+
+
+
+
+
+
+
+
+
+
+
+ 提交
+ 返回
+
+
+
+
+
+
diff --git a/ruoyi-fastapi-frontend/vue2/web/src/views/tool/gen/genInfoForm.vue b/ruoyi-fastapi-frontend/vue2/web/src/views/tool/gen/genInfoForm.vue
new file mode 100644
index 0000000..6d4afa9
--- /dev/null
+++ b/ruoyi-fastapi-frontend/vue2/web/src/views/tool/gen/genInfoForm.vue
@@ -0,0 +1,425 @@
+
+
+
+
+
+ 生成模板
+
+
+
+
+
+
+
+
+
+ 前端类型
+
+
+
+
+
+
+
+
+
+ 生成包路径
+
+
+
+
+
+
+
+
+
+
+
+ 生成模块名
+
+
+
+
+
+
+
+
+
+
+
+ 生成业务名
+
+
+
+
+
+
+
+
+
+
+
+ 生成功能名
+
+
+
+
+
+
+
+
+
+
+
+ 表单布局
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 扩展功能
+ 生成详情页
+
+
+
+
+
+
+ 生成代码方式
+
+
+
+
+ zip压缩包
+ 自定义路径
+
+
+
+
+
+
+ 上级菜单
+
+
+
+
+
+
+
+
+
+
+
+ 自定义路径
+
+
+
+
+
+
+
+ 最近路径快速选择
+
+
+
+ 恢复默认的生成基础路径
+
+
+
+
+
+
+
+
+
+
+
+
+ 树编码字段
+
+
+
+
+
+
+
+
+
+
+
+
+ 树父编码字段
+
+
+
+
+
+
+
+
+
+
+
+
+ 树名称字段
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 关联子表的表名
+
+
+
+
+
+
+
+
+
+
+
+
+ 子表关联的外键名
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ruoyi-fastapi-frontend/vue2/web/src/views/tool/gen/importTable.vue b/ruoyi-fastapi-frontend/vue2/web/src/views/tool/gen/importTable.vue
new file mode 100644
index 0000000..9480185
--- /dev/null
+++ b/ruoyi-fastapi-frontend/vue2/web/src/views/tool/gen/importTable.vue
@@ -0,0 +1,241 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 搜索
+ 重置
+
+
+
+
+
+
+
+
+ {{ parseTime(scope.row.createTime) || '-' }}
+
+
+
+
+
+
+
+
+
+
diff --git a/ruoyi-fastapi-frontend/vue2/web/src/views/tool/gen/index.vue b/ruoyi-fastapi-frontend/vue2/web/src/views/tool/gen/index.vue
new file mode 100644
index 0000000..486e8fe
--- /dev/null
+++ b/ruoyi-fastapi-frontend/vue2/web/src/views/tool/gen/index.vue
@@ -0,0 +1,516 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 搜索
+ 重置
+
+
+
+
+
+ 生成
+
+
+ 创建
+
+
+ 导入
+
+
+ 修改
+
+
+ 删除
+
+
+
+
+
+
+
+
+ {{ (queryParams.pageNum - 1) * queryParams.pageSize + scope.$index + 1 }}
+
+
+
+
+
+
+
+
+
+
+ 预览
+ 编辑
+ 删除
+ 同步
+ 生成代码
+
+
+
+
+
+
+
+
+ 复制
+
+
+
+
+
+
+
+
+
+
diff --git a/ruoyi-fastapi-frontend/vue2/web/src/views/tool/swagger/index.vue b/ruoyi-fastapi-frontend/vue2/web/src/views/tool/swagger/index.vue
new file mode 100644
index 0000000..1474e30
--- /dev/null
+++ b/ruoyi-fastapi-frontend/vue2/web/src/views/tool/swagger/index.vue
@@ -0,0 +1,15 @@
+
+
+
+
diff --git a/ruoyi-fastapi-frontend/vue2/web/tests/auth-center/availability.test.cjs b/ruoyi-fastapi-frontend/vue2/web/tests/auth-center/availability.test.cjs
new file mode 100644
index 0000000..4626a47
--- /dev/null
+++ b/ruoyi-fastapi-frontend/vue2/web/tests/auth-center/availability.test.cjs
@@ -0,0 +1,52 @@
+const assert = require('node:assert/strict')
+const test = require('node:test')
+const path = require('node:path')
+const { loadModule } = require('../support/load-module.cjs')
+const { checkAuthCenterAccess } = loadModule(
+ path.resolve(__dirname, '../../src/utils/authCenterAccess.js')
+)
+
+test('公共认证页面按最新开关准入,故障拒绝进入且错误页保持可访问', async () => {
+ const route = {
+ path: '/auth-center/login',
+ meta: { requiresAuthCenter: true },
+ query: { interaction: 'private-interaction' },
+ hash: '#csrf=private-csrf',
+ }
+ let enabled = true
+ let calls = 0
+ const loadStatus = async () => {
+ calls++
+ return { data: { enabled } }
+ }
+ assert.equal(await checkAuthCenterAccess(route, loadStatus), undefined)
+ enabled = false
+ assert.deepEqual(await checkAuthCenterAccess(route, loadStatus), {
+ path: '/auth-center/error',
+ query: { reason: 'disabled' },
+ replace: true,
+ })
+ enabled = true
+ assert.equal(await checkAuthCenterAccess(route, loadStatus), undefined)
+ assert.equal(calls, 3, '每次进入都检查开关,不沿用上次结果')
+
+ for (const path of ['/auth-center/error', '/login', '/system/user']) {
+ assert.equal(await checkAuthCenterAccess({ path, meta: {} }, loadStatus), undefined)
+ }
+ assert.equal(calls, 3, '错误说明页与普通页面不请求认证状态')
+
+ const unavailable = {
+ path: '/auth-center/error',
+ query: { reason: 'unavailable' },
+ replace: true,
+ }
+ for (const response of [{}, { data: {} }, { data: { enabled: 'false' } }, null]) {
+ assert.deepEqual(await checkAuthCenterAccess(route, async () => response), unavailable)
+ }
+ assert.deepEqual(
+ await checkAuthCenterAccess(route, async () => {
+ throw new Error('network unavailable')
+ }),
+ unavailable
+ )
+})
diff --git a/ruoyi-fastapi-frontend/vue2/web/tests/auth-center/interactionSecurity.test.cjs b/ruoyi-fastapi-frontend/vue2/web/tests/auth-center/interactionSecurity.test.cjs
new file mode 100644
index 0000000..91999c5
--- /dev/null
+++ b/ruoyi-fastapi-frontend/vue2/web/tests/auth-center/interactionSecurity.test.cjs
@@ -0,0 +1,27 @@
+const assert = require('node:assert/strict')
+const { loadModule } = require('../support/load-module.cjs')
+
+const { interactionRouteLocation, readInteractionCsrf, trustedCompletionUrl } = loadModule(
+ require('node:path').resolve(__dirname, '../../src/views/auth-center/interactionSecurity.js')
+)
+
+assert.equal(readInteractionCsrf('#csrf=token%2Bvalue&ignored=1'), 'token+value')
+assert.equal(readInteractionCsrf('#ignored=1'), '')
+assert.equal(readInteractionCsrf(null), '')
+assert.equal(
+ interactionRouteLocation('/auth-center/login', 'id/with space'),
+ '/auth-center/login?interaction=id%2Fwith%20space'
+)
+
+const origin = 'https://auth.example.com'
+const interactionId = 'interaction-1'
+assert.equal(
+ trustedCompletionUrl('/auth/interaction/interaction-1/complete', origin, interactionId)?.origin,
+ origin
+)
+assert.equal(trustedCompletionUrl('https://evil.example/complete', origin, interactionId), null)
+assert.equal(trustedCompletionUrl('/auth/interaction/other/complete', origin, interactionId), null)
+assert.equal(
+ trustedCompletionUrl('/auth/interaction/interaction-1/complete.evil', origin, interactionId),
+ null
+)
diff --git a/ruoyi-fastapi-frontend/vue2/web/tests/auth-center/oauthUri.test.cjs b/ruoyi-fastapi-frontend/vue2/web/tests/auth-center/oauthUri.test.cjs
new file mode 100644
index 0000000..26eb508
--- /dev/null
+++ b/ruoyi-fastapi-frontend/vue2/web/tests/auth-center/oauthUri.test.cjs
@@ -0,0 +1,45 @@
+const assert = require('node:assert/strict')
+const { loadModule } = require('../support/load-module.cjs')
+
+const { splitRegisteredUris, validateRegisteredUri, validateRegisteredUriList } = loadModule(
+ require('node:path').resolve(__dirname, '../../src/utils/oauthUri.js')
+)
+
+assert.deepEqual(splitRegisteredUris('https://one.example/cb,\nhttps://two.example/cb'), [
+ 'https://one.example/cb',
+ 'https://two.example/cb',
+])
+assert.equal(
+ validateRegisteredUri('https://portal.example/callback?tenant=one', 'redirect'),
+ 'https://portal.example/callback?tenant=one'
+)
+assert.equal(
+ validateRegisteredUri('http://localhost:8080/callback', 'redirect'),
+ 'http://localhost:8080/callback'
+)
+assert.equal(
+ validateRegisteredUri('https://portal.example', 'cors_origin'),
+ 'https://portal.example'
+)
+assert.equal(
+ validateRegisteredUri('https://portal.example:443', 'cors_origin'),
+ 'https://portal.example:443'
+)
+assert.throws(() => validateRegisteredUri('http://portal.example/callback', 'redirect'), /本地开发/)
+assert.throws(
+ () => validateRegisteredUri('https://user@portal.example/callback', 'redirect'),
+ /用户信息/
+)
+assert.throws(
+ () => validateRegisteredUri('https://portal.example/callback#fragment', 'redirect'),
+ /Fragment/
+)
+assert.throws(
+ () => validateRegisteredUri('https://portal.example/logout?token=1', 'backchannel_logout'),
+ /查询参数/
+)
+assert.throws(
+ () => validateRegisteredUri('https://portal.example/', 'cors_origin'),
+ /协议、主机和端口/
+)
+assert.throws(() => validateRegisteredUriList('', 'redirect', true), /至少填写/)
diff --git a/ruoyi-fastapi-frontend/vue2/web/tests/auth-center/vue2-components.test.cjs b/ruoyi-fastapi-frontend/vue2/web/tests/auth-center/vue2-components.test.cjs
new file mode 100644
index 0000000..fbb38ca
--- /dev/null
+++ b/ruoyi-fastapi-frontend/vue2/web/tests/auth-center/vue2-components.test.cjs
@@ -0,0 +1,246 @@
+const assert = require('node:assert/strict')
+const test = require('node:test')
+const path = require('node:path')
+const Vue = require('vue')
+const Router = require('vue-router')
+const { createLoader } = require('../support/load-module.cjs')
+
+const root = path.resolve(__dirname, '../../src')
+const settle = async () => {
+ await Vue.nextTick()
+ await new Promise((resolve) => setImmediate(resolve))
+}
+const component = (load, file) => load(path.join(root, file)).default
+const notices = { msgSuccess() {}, msgError() {}, confirm: async () => {} }
+Vue.use(Router)
+
+// 直接实例化 Vue 2.7 组件,验证迁移后真实的 value/input、响应式与路由行为。
+test('URI 列表使用 Vue 2 input 事件更新父表单,删除最后一行保留输入位置', async () => {
+ const vm = new Vue({
+ ...component(createLoader(), 'components/OAuthWorkspace/StringListInput.vue'),
+ propsData: { value: ['https://one.example/cb'] },
+ })
+ const state = vm._setupProxy
+ const values = []
+ vm.$on('input', (value) => values.push(value))
+ state.localItems = [' https://two.example/cb ', ' ']
+ state.emitValue()
+ assert.deepEqual(values[0], ['https://two.example/cb'])
+ state.remove(1)
+ state.remove(0)
+ assert.deepEqual(values.at(-1), [])
+ assert.deepEqual(Array.from(state.localItems), [''])
+ vm.value = ['https://new.example/cb']
+ await settle()
+ assert.equal(state.localItems[0], 'https://new.example/cb')
+ vm.$destroy()
+})
+
+test('密钥弹窗关闭通过 input 同步,重新打开必须重新确认已保存', async () => {
+ const load = createLoader({ '@/components/OAuthWorkspace/FieldLabel.vue': {} })
+ const vm = new Vue({
+ ...component(load, 'views/system/oauth/client/secret.vue'),
+ propsData: { value: true, secret: {} },
+ })
+ const state = vm._setupProxy
+ const emitted = []
+ vm.$on('input', (value) => emitted.push(value))
+ state.confirmed = true
+ state.close()
+ assert.deepEqual(emitted, [false])
+ vm.value = false
+ await settle()
+ vm.value = true
+ await settle()
+ assert.equal(state.confirmed, false)
+ vm.$destroy()
+})
+
+test('独立访问策略可在首次授权前创建,关闭弹层会通知父组件', async () => {
+ const calls = []
+ const load = createLoader({
+ '@/api/system/oauthSession': {
+ listOAuthAccessPolicies: async () => ({ rows: [], total: 0 }),
+ setOAuthClientAccess: async (...args) => {
+ calls.push(args)
+ },
+ },
+ })
+ const vm = new Vue({
+ ...component(load, 'views/system/oauth/grant/AccessPolicyDrawer.vue'),
+ propsData: { value: true },
+ beforeCreate() {
+ this.$modal = notices
+ },
+ })
+ const state = vm._setupProxy
+ vm.$refs.accessRef = { clearValidate() {}, validate: async () => true }
+ let changes = 0
+ vm.$on('changed', () => {
+ changes++
+ })
+ state.openAccess()
+ state.accessForm.userId = ' 42 '
+ state.accessForm.clientId = ' demo-client '
+ state.accessForm.reason = ' 验证首次访问控制 '
+ await state.saveAccess()
+ assert.deepEqual(calls, [['42', 'demo-client', { blocked: true, reason: '验证首次访问控制' }]])
+ assert.equal(changes, 1)
+ assert.equal(state.accessOpen, false)
+ const visibility = []
+ vm.$on('input', (value) => visibility.push(value))
+ state.visible = false
+ assert.deepEqual(visibility, [false])
+ vm.$destroy()
+})
+
+test('撤销已过期会话仍提交精确 sid 和原因', async () => {
+ const calls = []
+ const load = createLoader({
+ '@/components/OAuthWorkspace/PageFrame.vue': {},
+ '@/api/system/oauthSession': {
+ listOAuthSessions: async () => ({ rows: [], total: 0 }),
+ revokeOAuthSessions: async (...args) => {
+ calls.push(args)
+ },
+ },
+ })
+ const vm = new Vue({
+ ...component(load, 'views/system/oauth/session/index.vue'),
+ beforeCreate() {
+ this.$modal = notices
+ },
+ })
+ const state = vm._setupProxy
+ assert.equal(vm.$options.name, 'OAuthSession')
+ state.openRevoke({ sid: 'expired-sid', status: 'expired' })
+ state.reason = ' 结束离线访问 '
+ await state.revoke()
+ assert.deepEqual(calls, [['expired-sid', { reason: '结束离线访问' }]])
+ assert.equal(state.revokeOpen, false)
+ vm.$destroy()
+})
+
+test('认证交互恢复已完成流程,失败保留凭据并拒绝非当前交互的完成地址', async () => {
+ const completed = [],
+ assigned = [],
+ replaced = []
+ const storage = new Map()
+ let completionError = new Error('完成认证暂时失败')
+ const load = createLoader(
+ {
+ '@/api/authCenter': {
+ completeInteraction: async (...args) => {
+ completed.push(args)
+ if (completionError) throw completionError
+ return { data: { redirectUrl: 'https://app.example/callback?code=opaque' } }
+ },
+ },
+ },
+ {
+ sessionStorage: {
+ getItem: (key) => storage.get(key),
+ setItem: (key, value) => storage.set(key, value),
+ removeItem: (key) => storage.delete(key),
+ },
+ window: {
+ location: {
+ hash: '#csrf=csrf-1',
+ origin: 'https://issuer.example',
+ assign: (url) => assigned.push(url),
+ },
+ history: { state: {}, replaceState: (...args) => replaced.push(args) },
+ },
+ }
+ )
+ const router = new Router({ mode: 'abstract', routes: [{ path: '/auth-center/login' }] })
+ await router.push('/auth-center/login?interaction=one')
+ const useContext = load(
+ path.join(root, 'views/auth-center/useInteraction.js')
+ ).useInteractionContext
+ const vm = new Vue({ router, setup: () => useContext() })
+ assert.equal(vm.interactionId, 'one')
+ assert.equal(vm.csrfToken(), 'csrf-1')
+ assert.equal(replaced[0][2], '/auth-center/login?interaction=one')
+ await assert.rejects(vm.followServerRedirect('/auth/interaction/other/complete'))
+ assert.equal(completed.length, 0)
+ await assert.rejects(vm.goToAction('redirect'), /完成认证暂时失败/)
+ assert.equal(vm.csrfToken(), 'csrf-1')
+ assert.equal(assigned.length, 0)
+ completionError = null
+ await vm.goToAction('redirect')
+ assert.deepEqual(completed, [
+ ['one', 'csrf-1'],
+ ['one', 'csrf-1'],
+ ])
+ assert.equal(assigned.length, 1)
+ assert.equal(vm.csrfToken(), '')
+ await router.replace('/auth-center/login?interaction=two')
+ await settle()
+ assert.equal(vm.interactionId, 'two')
+ vm.$destroy()
+})
+
+test('服务应用切换用户登录方式时同步保存所需响应类型', async () => {
+ const load = createLoader({
+ '@/api/system/oauthClient': {
+ listOAuthClients: async () => ({ rows: [], total: 0 }),
+ },
+ '@/api/system/oauthResource': {
+ listOAuthScopes: async () => ({ rows: [] }),
+ listOAuthResources: async () => ({ rows: [] }),
+ },
+ '@/components/OAuthWorkspace/AccessMap.vue': {},
+ '@/components/OAuthWorkspace/FieldLabel.vue': {},
+ '@/components/OAuthWorkspace/PageFrame.vue': {},
+ '@/components/OAuthWorkspace/StringListInput.vue': {},
+ './secret.vue': {},
+ })
+ const vm = new Vue({ ...component(load, 'views/system/oauth/client/index.vue') })
+ try {
+ const state = vm._setupProxy
+ Object.assign(
+ state.form,
+ state.toEditor({
+ clientId: 'machine-client',
+ clientName: '服务应用',
+ clientType: 'confidential',
+ tokenEndpointAuthMethod: 'client_secret_basic',
+ grantTypes: ['client_credentials'],
+ responseTypes: [],
+ scopeCodes: ['openid'],
+ })
+ )
+ state.form.grantTypes.push('authorization_code')
+ state.form.redirectUris = ['https://app.example/callback']
+ await settle()
+ const withLogin = state.payload()
+ assert.deepEqual(withLogin.responseTypes, ['code'])
+ assert.deepEqual(withLogin.grantTypes, ['client_credentials', 'authorization_code'])
+ assert.deepEqual(withLogin.redirectUris, ['https://app.example/callback'])
+ Object.assign(state.form, state.toEditor(withLogin))
+ state.form.grantTypes = ['client_credentials']
+ await settle()
+ assert.deepEqual(state.payload().responseTypes, [])
+ state.form.grantTypes = ['authorization_code', 'refresh_token']
+ await settle()
+ assert.deepEqual(state.payload().responseTypes, ['code'])
+ } finally {
+ vm.$destroy()
+ }
+})
+
+test('Vue 2 密码规则保留原有 mixin,同时向认证页提供相同校验', () => {
+ const load = createLoader({ '@/plugins/cache': { session: { get: () => '3' } } })
+ const rule = load(path.join(root, 'utils/passwordRule.js'))
+ const state = rule.usePasswordRule()
+ assert.equal(state.pwdChrType.value, '3')
+ const mixin = new Vue(rule.default)
+ assert.equal(
+ state.infoPwdValidator.value[2].pattern.source,
+ mixin.infoPwdValidator[2].pattern.source
+ )
+ assert.equal(state.infoPwdValidator.value[2].pattern.test('onlyletters'), false)
+ assert.equal(state.infoPwdValidator.value[2].pattern.test('valid123'), true)
+ mixin.$destroy()
+})
diff --git a/ruoyi-fastapi-frontend/tests/job/job-form.test.js b/ruoyi-fastapi-frontend/vue2/web/tests/job/job-form.test.mjs
similarity index 100%
rename from ruoyi-fastapi-frontend/tests/job/job-form.test.js
rename to ruoyi-fastapi-frontend/vue2/web/tests/job/job-form.test.mjs
diff --git a/ruoyi-fastapi-frontend/vue2/web/tests/job/vue2-components.test.cjs b/ruoyi-fastapi-frontend/vue2/web/tests/job/vue2-components.test.cjs
new file mode 100644
index 0000000..6014f3f
--- /dev/null
+++ b/ruoyi-fastapi-frontend/vue2/web/tests/job/vue2-components.test.cjs
@@ -0,0 +1,426 @@
+const assert = require('node:assert/strict')
+const test = require('node:test')
+const fs = require('node:fs')
+const path = require('node:path')
+const vm = require('node:vm')
+const { createRequire } = require('node:module')
+const Vue = require('vue')
+const { transformSync } = require('@babel/core')
+const compiler = require('vue/compiler-sfc')
+const transformModules = require('@babel/plugin-transform-modules-commonjs')
+
+const sourceRoot = path.resolve(__dirname, '../../src')
+const settle = async () => {
+ await Vue.nextTick()
+ await new Promise((resolve) => setImmediate(resolve))
+}
+const normalize = (value) => JSON.parse(JSON.stringify(value))
+
+// Exercise real Vue 2 instances; API and browser resources are isolated per test.
+function createLoader(mocks = {}, globals = {}) {
+ const cache = new Map()
+ function load(filename) {
+ filename = path.resolve(filename)
+ if (!path.extname(filename)) {
+ filename = ['.js', '.vue', '/index.js', '/index.vue']
+ .map((suffix) => filename + suffix)
+ .find(fs.existsSync)
+ }
+ if (cache.has(filename)) return cache.get(filename).exports
+ let source = fs.readFileSync(filename, 'utf8')
+ if (filename.endsWith('.vue')) {
+ const descriptor = compiler.parse({ source, filename })
+ assert.ok(descriptor.script || descriptor.scriptSetup, filename)
+ // Vue 2.7 supports the setup components already present in this project.
+ source = descriptor.scriptSetup
+ ? compiler.compileScript(descriptor, { id: filename }).content
+ : descriptor.script.content
+ }
+ const code = transformSync(source, {
+ filename,
+ configFile: false,
+ babelrc: false,
+ plugins: [transformModules],
+ }).code
+ const module = { exports: {} }
+ cache.set(filename, module)
+ const nativeRequire = createRequire(filename)
+ const requireModule = (specifier) => {
+ if (Object.prototype.hasOwnProperty.call(mocks, specifier)) return mocks[specifier]
+ if (specifier.startsWith('@/')) return load(path.resolve(sourceRoot, specifier.slice(2)))
+ if (specifier.startsWith('.')) return load(path.resolve(path.dirname(filename), specifier))
+ return nativeRequire(specifier)
+ }
+ vm.runInNewContext(
+ code,
+ {
+ module,
+ exports: module.exports,
+ require: requireModule,
+ console,
+ setTimeout,
+ clearTimeout,
+ AbortController,
+ Intl,
+ ...globals,
+ },
+ { filename }
+ )
+ return module.exports
+ }
+ return (relative) => {
+ const result = load(path.join(sourceRoot, relative))
+ return result.default || result
+ }
+}
+
+function fakeTimers() {
+ let id = 0
+ const pending = new Map()
+ return {
+ pending,
+ setTimeout(callback) {
+ pending.set(++id, callback)
+ return id
+ },
+ clearTimeout(timer) {
+ pending.delete(timer)
+ },
+ runNext() {
+ const [timer, callback] = pending.entries().next().value
+ pending.delete(timer)
+ return callback()
+ },
+ }
+}
+
+function deferred() {
+ let resolve
+ const promise = new Promise((done) => {
+ resolve = done
+ })
+ return { promise, resolve }
+}
+
+test('Vue 2 computed date text reacts immediately to account timezone changes', async () => {
+ const time = createLoader()('utils/time.js')
+ time.setUserTimezone('America/New_York')
+ const instance = new Vue({
+ computed: {
+ display() {
+ return time.formatBusinessTime('2026-01-01T00:00:00Z')
+ },
+ },
+ })
+ assert.equal(instance.display, '2025-12-31 19:00:00')
+ time.setUserTimezone('UTC')
+ await settle()
+ assert.equal(instance.display, '2026-01-01 00:00:00')
+ instance.$destroy()
+})
+
+test('Cron preview cancels obsolete requests and ignores responses after destruction', async () => {
+ const timers = fakeTimers()
+ const requests = []
+ const component = createLoader(
+ {
+ '@/api/monitor/job': {
+ previewJob(payload, signal) {
+ const response = deferred()
+ requests.push({ payload, signal, ...response })
+ return response.promise
+ },
+ },
+ },
+ timers
+ )('components/Crontab/result.vue')
+ const instance = new Vue({
+ ...component,
+ propsData: { ex: '0 0 9 * * ?', timeZone: 'Asia/Shanghai' },
+ })
+ assert.equal(timers.pending.size, 1)
+ const first = timers.runNext()
+ instance.ex = '0 0 10 * * ?'
+ await settle()
+ assert.equal(requests[0].signal.aborted, true)
+ requests[0].resolve({ data: { nextRunTimes: ['stale'], timeZone: 'UTC' } })
+ await first
+ assert.deepEqual(normalize(instance.resultList), [])
+ const second = timers.runNext()
+ assert.equal(requests[1].payload.cronExpression, '0 0 10 * * ?')
+ instance.$destroy()
+ assert.equal(requests[1].signal.aborted, true)
+ requests[1].resolve({ data: { nextRunTimes: ['late'], timeZone: 'UTC' } })
+ await second
+ assert.deepEqual(normalize(instance.resultList), [])
+ assert.equal(timers.pending.size, 0)
+})
+
+test('Runtime dialog applies initial filters, cancels changed requests and stops on destroy', async () => {
+ const requests = []
+ const fetch = (query, options) => {
+ const response = deferred()
+ requests.push({ query, ...options, ...response })
+ return response.promise
+ }
+ const component = createLoader(
+ {
+ '@/api/monitor/job': { listJobExecutions: fetch, listJobSync: fetch },
+ },
+ { document: { hidden: false, removeEventListener() {} } }
+ )('views/monitor/job/runtime.vue')
+ const instance = new Vue({
+ ...component,
+ propsData: { visible: true, initialTab: 'executions', jobId: '7', executionId: 'exec-1' },
+ })
+ assert.equal(requests[0].query.jobId, 7)
+ assert.equal(requests[0].query.executionId, 'exec-1')
+ instance.initialTab = 'sync'
+ await settle()
+ assert.equal(requests[0].signal.aborted, true)
+ assert.equal(requests[1].query.executionId, undefined)
+ requests[0].resolve({ rows: [{ status: 'running' }], total: 1 })
+ requests[1].resolve({ rows: [{ syncStatus: 'applied' }], total: 1 })
+ await settle()
+ assert.equal(instance.rows[0].syncStatus, 'applied')
+ const visibility = []
+ instance.$on('update:visible', (value) => visibility.push(value))
+ instance.dialogVisible = false
+ assert.deepEqual(visibility, [false])
+ instance.$destroy()
+ assert.equal(requests[1].signal.aborted, true)
+})
+
+test('Job log initializes from stable IDs and follows route and display timezone changes', async () => {
+ const queries = []
+ const load = createLoader({
+ './detail': {},
+ '@/api/monitor/jobLog': {
+ listJobLog: async (query) => {
+ queries.push(query)
+ return { rows: [], total: 0 }
+ },
+ },
+ })
+ const time = load('utils/time.js')
+ time.setUserTimezone('UTC')
+ const route = Vue.observable({ params: { jobId: '7' }, query: { executionId: 'exec-1' } })
+ const component = load('views/monitor/job/log.vue')
+ const instance = new Vue({
+ ...component,
+ beforeCreate() {
+ this.$route = route
+ },
+ methods: { ...component.methods, addDateRange: (query) => query },
+ })
+ await settle()
+ assert.equal(queries[0].jobId, '7')
+ assert.equal(queries[0].executionId, 'exec-1')
+ route.params.jobId = '8'
+ route.query.executionId = ''
+ await settle()
+ assert.equal(queries.at(-1).jobId, '8')
+ assert.equal(queries.at(-1).executionId, undefined)
+ instance.dateRange = ['2026-01-01', '2026-01-02']
+ const before = queries.length
+ time.setUserTimezone('Asia/Shanghai')
+ await settle()
+ assert.equal(queries.length, before + 1)
+ instance.$destroy()
+})
+
+test('Plugin config dynamic fields stay reactive and JSON values submit with their original types', async () => {
+ const component = createLoader({
+ 'element-ui': { Message: { error: assert.fail } },
+ '@/api/system/plugin': {
+ getPluginConfigStatus: () => assert.fail('Closed config status must not request data'),
+ },
+ })('views/system/plugin/components/PluginConfigDialog.vue')
+ const instance = new Vue({
+ ...component,
+ propsData: {
+ items: [
+ { key: 'enabled', value: false, type: 'switch' },
+ { key: 'options', value: { limit: 3 }, type: 'json' },
+ ],
+ formatConfigDefaultValue: () => '-',
+ formatConfigConstraint: () => '-',
+ },
+ })
+ const state = instance._setupState || instance
+ let updates = 0
+ const stopWatchingEnabled = instance.$watch(
+ () => state.configForm.values.enabled,
+ () => updates++
+ )
+ state.configForm.values.enabled = true
+ await settle()
+ assert.equal(updates, 1)
+ const values = state.buildConfigSubmitValues()
+ assert.equal(values.enabled, true)
+ assert.deepEqual(normalize(values.options), { limit: 3 })
+ stopWatchingEnabled()
+ instance.items = [{ key: 'anotherKey', value: 'initial', type: 'string' }]
+ await settle()
+ instance.$watch(
+ () => state.configForm.values.anotherKey,
+ () => updates++
+ )
+ state.configForm.values.anotherKey = 'updated'
+ await settle()
+ assert.equal(updates, 2)
+ assert.equal(state.configForm.values.enabled, undefined)
+ instance.$destroy()
+})
+
+test('JSON editor uses Element UI validation and releases Monaco resources in beforeDestroy', async () => {
+ let text = '[]'
+ let disposals = 0
+ const disposable = () => ({ dispose: () => disposals++ })
+ const model = {
+ updateOptions() {},
+ getValue: () => text,
+ setValue: (value) => {
+ text = value
+ },
+ ...disposable(),
+ }
+ const editor = {
+ updateOptions() {},
+ onDidChangeModelContent: disposable,
+ onDidBlurEditorText: disposable,
+ ...disposable(),
+ }
+ const monaco = {
+ editor: { createModel: () => model, create: () => editor, onDidChangeMarkers: disposable },
+ }
+ const component = createLoader(
+ { '@/utils/monaco': { loadMonaco: async () => monaco } },
+ {
+ document: { documentElement: { classList: { contains: () => false } } },
+ MutationObserver: class {
+ observe() {}
+ disconnect() {
+ disposals++
+ }
+ },
+ }
+ )('components/JsonEditor/index.vue')
+ let validations = 0
+ const parent = new Vue({
+ provide: {
+ elFormItem: {
+ validateState: 'error',
+ validate() {
+ validations++
+ },
+ },
+ },
+ })
+ const instance = new Vue({
+ ...component,
+ parent,
+ propsData: { value: '[]', validate: JSON.parse },
+ })
+ instance.$refs.editorRef = {}
+ await component.mounted.call(instance)
+ assert.equal(instance.editorReady, true)
+ assert.equal(editor.__ob__, undefined, 'Monaco editor must not be deeply observed by Vue')
+ const inputs = []
+ instance.$on('input', (value) => inputs.push(value))
+ instance.updateValue('[1]')
+ await settle()
+ assert.equal(validations, 1)
+ assert.deepEqual(inputs, ['[1]'])
+ instance.$destroy()
+ assert.equal(disposals, 6)
+ parent.$destroy()
+})
+
+test('Business date input emits the Vue 2 v-model event with the selected wall time', () => {
+ const component = createLoader()('components/BusinessDateTimePicker/index.vue')
+ const instance = new Vue({
+ ...component,
+ propsData: { value: '2026-01-01 09:00:00', timezone: 'Asia/Shanghai' },
+ })
+ const values = []
+ instance.$on('input', (value) => values.push(value))
+ instance.timeInput = '10:30'
+ assert.deepEqual(values, ['2026-01-01 10:30:00'])
+ instance.$destroy()
+})
+
+test('Native job form uses callback validation, submits typed fields and retains More menu dispatch', async () => {
+ const payloads = []
+ const notices = []
+ const component = createLoader(
+ {
+ './detail': {},
+ './runtime': {},
+ '@/components/Crontab': {},
+ '@/components/JsonEditor': {},
+ '@/utils/permission': { checkPermi: () => true },
+ '@/api/monitor/job': {
+ listJob: async () => ({ rows: [], total: 0 }),
+ addJob: async (payload) => {
+ payloads.push(payload)
+ return { msg: '配置已保存,等待同步', data: { syncStatus: 'pending' } }
+ },
+ },
+ },
+ { document: { hidden: false, removeEventListener() {} } }
+ )('views/monitor/job/index.vue')
+ const instance = new Vue({
+ ...component,
+ beforeCreate: [
+ function () {
+ this.$store = { state: { user: { appTimezone: 'Asia/Shanghai' } } }
+ this.$modal = { msg: (message) => notices.push(message), msgSuccess: assert.fail }
+ },
+ component.beforeCreate,
+ ],
+ methods: { ...component.methods, resetForm() {} },
+ })
+ instance.reset()
+ assert.equal(instance.form.status, '1')
+ instance.form.jobName = 'native form'
+ instance.form.invokeTarget = 'module_task.scheduler_test.job'
+ instance.form.cronExpression = '0 0 9 * * ?'
+ instance.form.jobArgs = '["tenant,a", 3, true]'
+ instance.form.jobKwargs = '{"enabled":false}'
+ instance.unlimitedDelay = true
+ let valid = false
+ instance.$refs.form = {
+ validate(callback) {
+ callback(valid)
+ },
+ }
+ instance.submitForm()
+ assert.equal(payloads.length, 0)
+ valid = true
+ instance.submitForm()
+ instance.submitForm()
+ await settle()
+ assert.equal(payloads.length, 1)
+ assert.deepEqual(normalize(payloads[0].jobArgs), ['tenant,a', 3, true])
+ assert.deepEqual(normalize(payloads[0].jobKwargs), { enabled: false })
+ assert.equal(payloads[0].misfireGraceTime, null)
+ assert.equal(payloads[0].timeZone, 'Asia/Shanghai')
+ assert.equal('misfirePolicy' in payloads[0], false)
+ assert.equal(instance.submitting, false)
+ assert.deepEqual(notices, ['配置已保存,等待同步'])
+ const commands = []
+ instance.handleRun = (row) => commands.push(['run', row.jobId])
+ instance.handleJobLog = (row) => commands.push(['log', row.jobId])
+ instance.handleRuntime = (tab, row) => commands.push([tab, row.jobId])
+ for (const command of ['handleRun', 'handleJobLog', 'handleExecutions', 'handleSync']) {
+ instance.handleCommand(command, { jobId: 7 })
+ }
+ assert.deepEqual(commands, [
+ ['run', 7],
+ ['log', 7],
+ ['executions', 7],
+ ['sync', 7],
+ ])
+ instance.$destroy()
+})
diff --git a/ruoyi-fastapi-frontend/tests/plugins/fixtures/bridge-b0d16a3e/.gitattributes b/ruoyi-fastapi-frontend/vue2/web/tests/plugins/fixtures/bridge-b0d16a3e/.gitattributes
similarity index 100%
rename from ruoyi-fastapi-frontend/tests/plugins/fixtures/bridge-b0d16a3e/.gitattributes
rename to ruoyi-fastapi-frontend/vue2/web/tests/plugins/fixtures/bridge-b0d16a3e/.gitattributes
diff --git a/ruoyi-fastapi-frontend/tests/plugins/fixtures/bridge-b0d16a3e/pluginBridge.js b/ruoyi-fastapi-frontend/vue2/web/tests/plugins/fixtures/bridge-b0d16a3e/pluginBridge.js
similarity index 100%
rename from ruoyi-fastapi-frontend/tests/plugins/fixtures/bridge-b0d16a3e/pluginBridge.js
rename to ruoyi-fastapi-frontend/vue2/web/tests/plugins/fixtures/bridge-b0d16a3e/pluginBridge.js
diff --git a/ruoyi-fastapi-frontend/tests/plugins/fixtures/bridge-b0d16a3e/provenance.json b/ruoyi-fastapi-frontend/vue2/web/tests/plugins/fixtures/bridge-b0d16a3e/provenance.json
similarity index 100%
rename from ruoyi-fastapi-frontend/tests/plugins/fixtures/bridge-b0d16a3e/provenance.json
rename to ruoyi-fastapi-frontend/vue2/web/tests/plugins/fixtures/bridge-b0d16a3e/provenance.json
diff --git a/ruoyi-fastapi-frontend/tests/plugins/fixtures/json-only-host.js b/ruoyi-fastapi-frontend/vue2/web/tests/plugins/fixtures/json-only-host.js
similarity index 100%
rename from ruoyi-fastapi-frontend/tests/plugins/fixtures/json-only-host.js
rename to ruoyi-fastapi-frontend/vue2/web/tests/plugins/fixtures/json-only-host.js
diff --git a/ruoyi-fastapi-frontend/vue2/web/tests/plugins/network-smoke/server.mjs b/ruoyi-fastapi-frontend/vue2/web/tests/plugins/network-smoke/server.mjs
new file mode 100644
index 0000000..7d411a7
--- /dev/null
+++ b/ruoyi-fastapi-frontend/vue2/web/tests/plugins/network-smoke/server.mjs
@@ -0,0 +1,134 @@
+import { readFile, rename, writeFile } from 'node:fs/promises'
+import { createServer } from 'node:https'
+import { createRequire } from 'node:module'
+import path from 'node:path'
+import { fileURLToPath } from 'node:url'
+
+const require = createRequire(import.meta.url)
+const Service = require('@vue/cli-service/lib/Service')
+const webpack = require('webpack')
+const MemoryFileSystem = require('memory-fs')
+const requireDevServer = createRequire(require.resolve('webpack-dev-server'))
+const express = requireDevServer('express')
+// Resolve the same proxy version as Vue CLI's server, independent of npm hoisting.
+const proxyFactory = requireDevServer('http-proxy-middleware')
+const createProxyMiddleware = proxyFactory.createProxyMiddleware || proxyFactory
+const root = fileURLToPath(new URL('../../../', import.meta.url))
+const [upstream, certificate, key, readyFile] = process.argv.slice(2)
+if (!upstream || !certificate || !key || !readyFile) {
+ throw new Error('upstream, certificate, key and ready-file are required')
+}
+
+process.chdir(root)
+process.env.NODE_ENV = 'development'
+process.env.BABEL_ENV = 'development'
+process.env.VUE_APP_BASE_API = '/dev-api'
+process.env.VUE_APP_PLUGIN_BASE = '/gateway'
+
+// Compile the real Vue2 application, including its router, Vuex and PluginFrame.
+// Fixed test configuration never reads .env files or writes a production dist.
+const service = new Service(root)
+service.loadEnv = () => {}
+service.init('development')
+const configuredProxy = service.projectOptions.devServer.proxy
+const appProxy = configuredProxy['/gateway/apps/']
+const runtimeProxy = configuredProxy['/gateway/plugin/runtime/']
+const apiProxy = configuredProxy['/dev-api']
+if (!appProxy || !runtimeProxy || !apiProxy || typeof appProxy.onProxyRes !== 'function') {
+ throw new Error('The real Vue2 plugin proxy configuration is required')
+}
+const config = service.resolveWebpackConfig()
+config.devtool = 'cheap-module-source-map'
+const compiler = webpack(config)
+const output = new MemoryFileSystem()
+compiler.outputFileSystem = output
+const outputRoot = config.output.path
+let server
+let closing = false
+const sockets = new Set()
+async function close(exitCode = 0) {
+ if (closing) return
+ closing = true
+ const deadline = setTimeout(() => process.exit(1), 5000)
+ deadline.unref()
+ for (const socket of sockets) socket.destroy()
+ if (server?.listening) await new Promise((resolve) => server.close(resolve))
+ clearTimeout(deadline)
+ process.exit(exitCode)
+}
+process.stdin.resume()
+process.stdin.on('data', () => void close())
+process.stdin.on('end', () => void close())
+process.on('SIGTERM', () => void close())
+process.on('SIGINT', () => void close())
+
+try {
+ // A single compilation finishes before listening. Watch rebuilds must not
+ // stall TLS handshakes or long-connection assertions during the smoke test.
+ await new Promise((resolve, reject) => {
+ compiler.run((error, stats) => {
+ if (error) return reject(error)
+ if (stats.hasErrors()) {
+ return reject(new Error(stats.toString({ all: false, errors: true })))
+ }
+ console.log(`Vue2 smoke host compiled successfully in ${stats.endTime - stats.startTime}ms`)
+ resolve()
+ })
+ })
+ const app = express()
+ const proxy = {
+ target: upstream,
+ changeOrigin: false,
+ headers: { 'x-forwarded-proto': 'https' },
+ logLevel: 'warn',
+ }
+ // Reuse real path rewriting and SSE cleanup; only override the isolated upstream.
+ const apps = createProxyMiddleware('/gateway/apps/', { ...appProxy, ...proxy })
+ app.use(apps)
+ app.use(createProxyMiddleware('/gateway/plugin/runtime/', { ...runtimeProxy, ...proxy }))
+ app.use(
+ createProxyMiddleware('/dev-api', {
+ ...apiProxy,
+ ...proxy,
+ })
+ )
+ app.use(express.static(path.join(root, 'public'), { index: false }))
+ app.use((request, response) => {
+ if (!['GET', 'HEAD'].includes(request.method)) return response.sendStatus(405)
+ let asset
+ try {
+ asset = path.resolve(outputRoot, '.' + decodeURIComponent(request.path))
+ } catch {
+ return response.sendStatus(400)
+ }
+ const relative = path.relative(outputRoot, asset)
+ if (relative.startsWith('..') || path.isAbsolute(relative)) return response.sendStatus(403)
+ if (!output.existsSync(asset) || !output.statSync(asset).isFile()) {
+ if (path.extname(request.path) || !request.accepts('html')) return response.sendStatus(404)
+ asset = path.join(outputRoot, 'index.html')
+ }
+ response.type(path.extname(asset)).send(output.readFileSync(asset))
+ })
+ server = createServer({ cert: await readFile(certificate), key: await readFile(key) }, app)
+ server.on('upgrade', (request, socket, head) => {
+ // HPM 0.19 auto-subscribes apps.upgrade after the first HTTP request.
+ // Avoid invoking that same handler twice, while supporting WS-first clients.
+ if (!server.listeners('upgrade').includes(apps.upgrade)) {
+ apps.upgrade(request, socket, head)
+ }
+ })
+ server.on('connection', (socket) => {
+ sockets.add(socket)
+ socket.on('close', () => sockets.delete(socket))
+ })
+ await new Promise((resolve, reject) => {
+ server.once('error', reject)
+ server.listen(0, '127.0.0.1', resolve)
+ })
+ // Publish readiness atomically: the Python observer must never see an empty file.
+ await writeFile(readyFile + '.tmp', JSON.stringify({ port: server.address().port }))
+ await rename(readyFile + '.tmp', readyFile)
+} catch (error) {
+ console.error(error)
+ await close(1)
+}
diff --git a/ruoyi-fastapi-frontend/tests/plugins/pluginBridge.test.js b/ruoyi-fastapi-frontend/vue2/web/tests/plugins/pluginBridge.test.mjs
similarity index 100%
rename from ruoyi-fastapi-frontend/tests/plugins/pluginBridge.test.js
rename to ruoyi-fastapi-frontend/vue2/web/tests/plugins/pluginBridge.test.mjs
diff --git a/ruoyi-fastapi-frontend/tests/plugins/pluginBridgeCompatibility.test.js b/ruoyi-fastapi-frontend/vue2/web/tests/plugins/pluginBridgeCompatibility.test.mjs
similarity index 100%
rename from ruoyi-fastapi-frontend/tests/plugins/pluginBridgeCompatibility.test.js
rename to ruoyi-fastapi-frontend/vue2/web/tests/plugins/pluginBridgeCompatibility.test.mjs
diff --git a/ruoyi-fastapi-frontend/vue2/web/tests/plugins/pluginConfigStatus.test.mjs b/ruoyi-fastapi-frontend/vue2/web/tests/plugins/pluginConfigStatus.test.mjs
new file mode 100644
index 0000000..41fb246
--- /dev/null
+++ b/ruoyi-fastapi-frontend/vue2/web/tests/plugins/pluginConfigStatus.test.mjs
@@ -0,0 +1,77 @@
+import assert from 'node:assert/strict'
+import { readFileSync } from 'node:fs'
+import { compileScript, parse } from 'vue/compiler-sfc'
+import { computed, effectScope, nextTick, reactive, ref, watch } from 'vue'
+
+const source = readFileSync(
+ new URL('../../src/views/system/plugin/components/PluginConfigStatus.vue', import.meta.url),
+ 'utf8'
+)
+const descriptor = parse({ source, filename: 'PluginComponent.vue' })
+const compiled = compileScript(descriptor, { id: 'config-status-test' })
+const script = compiled.scriptSetupAst
+ .filter((statement) => statement.type !== 'ImportDeclaration')
+ .map((statement) => descriptor.scriptSetup.content.slice(statement.start, statement.end))
+ .join('\n')
+const setup = new Function(
+ 'computed',
+ 'ref',
+ 'watch',
+ 'onBeforeUnmount',
+ 'defineProps',
+ 'getPluginConfigStatus',
+ `${script}\nreturn { data, loading, error, refresh, statusText }`
+)
+const props = reactive({ pluginId: 'first_plugin', refreshKey: 0 })
+const calls = []
+let unmount
+const scope = effectScope()
+const state = scope.run(() =>
+ setup(
+ computed,
+ ref,
+ watch,
+ (callback) => {
+ unmount = callback
+ },
+ () => props,
+ (pluginId, signal) =>
+ new Promise((resolve, reject) => {
+ calls.push({ pluginId, signal, resolve, reject })
+ })
+ )
+)
+assert.equal(calls[0].pluginId, 'first_plugin')
+props.pluginId = 'second_plugin'
+await nextTick()
+assert.equal(calls[0].signal.aborted, true)
+calls[1].resolve({ data: { state: 'restart_required', pendingWorkers: 2 } })
+await nextTick()
+calls[0].resolve({ data: { state: 'observed_match', matchedWorkers: 1 } })
+await nextTick()
+assert.match(state.statusText.value, /2 个已观测进程.*待重启/)
+
+// 保存成功后主动重查,不用旧状态冒充最新保存版本。
+props.refreshKey++
+await nextTick()
+assert.equal(state.data.value, null)
+calls[2].resolve({ data: { state: 'observed_match', matchedWorkers: 2 } })
+await nextTick()
+assert.match(state.statusText.value, /已观测进程.*匹配/)
+assert.equal(state.loading.value, false)
+const failed = state.refresh()
+calls[3].reject(new Error('unavailable'))
+await failed
+assert.equal(state.data.value, null)
+assert.match(state.error.value, /查询失败/)
+const retry = state.refresh()
+calls[4].resolve({ data: { state: 'unobserved' } })
+await retry
+assert.match(state.statusText.value, /尚不能确认|尚未观测/)
+const closing = state.refresh()
+unmount()
+scope.stop()
+assert.equal(calls[5].signal.aborted, true)
+calls[5].reject(new Error('cancelled'))
+await closing
+assert.equal(state.error.value, '')
diff --git a/ruoyi-fastapi-frontend/vue2/web/tests/plugins/pluginDevMock.test.mjs b/ruoyi-fastapi-frontend/vue2/web/tests/plugins/pluginDevMock.test.mjs
new file mode 100644
index 0000000..1b96aa3
--- /dev/null
+++ b/ruoyi-fastapi-frontend/vue2/web/tests/plugins/pluginDevMock.test.mjs
@@ -0,0 +1,77 @@
+import assert from 'node:assert/strict'
+import {
+ createMockPluginRequest,
+ createMockPluginStream,
+} from '../../../../../ruoyi-fastapi-backend/plugins/examples/python/bundle_demo/web/dev/mockRequest.js'
+
+const request = createMockPluginRequest({ getDelay: () => 0 })
+const config = (path, options = {}) => ({
+ url: '/apps/bundle_demo/api/' + path,
+ method: 'get',
+ ...options,
+})
+assert.equal((await request(config('summary'))).userName, '本地模拟用户')
+assert.equal(
+ (await request(config('echo', { method: 'post', data: { message: 'test' } }))).message,
+ 'test'
+)
+const form = new FormData()
+form.append('file', new Blob(['test']), 'test.txt')
+const events = []
+const file = await request(
+ config('files/inspect', {
+ method: 'post',
+ data: form,
+ onUploadProgress: (event) => events.push(event),
+ })
+)
+assert.deepEqual(file, {
+ filename: 'test.txt',
+ size: 4,
+ sha256: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08',
+})
+assert.deepEqual(
+ events.map((event) => event.loaded),
+ [2, 4]
+)
+const report = await request(config('files/report'))
+assert.match(await report.text(), /local-mock/)
+await assert.rejects(request(config('unknown')), /未实现/)
+await assert.rejects(request({ url: 'https://outside.test', method: 'get' }), /当前插件/)
+const fail = createMockPluginRequest({ getDelay: () => 0, shouldFail: () => true })
+await assert.rejects(fail(config('summary')), /模拟接口失败/)
+const controller = new AbortController()
+const slow = createMockPluginRequest({ getDelay: () => 5000 })
+const pending = slow(config('summary', { signal: controller.signal }))
+const rejected = assert.rejects(pending, /取消/)
+controller.abort()
+await rejected
+await assert.rejects(slow(config('summary', { signal: controller.signal })), /取消/)
+
+const streamController = new AbortController()
+const stream = createMockPluginStream({ getDelay: () => 100 })
+const streamConfig = config('events', {
+ params: { count: 3 },
+ lastEventId: '1',
+ signal: streamController.signal,
+})
+const received = []
+for await (const event of stream(streamConfig)) received.push(event)
+assert.deepEqual(
+ received.map((event) => event.id),
+ ['2', '3']
+)
+assert.equal(JSON.parse(received[0].data).index, 2)
+for (const invalid of [{ params: { count: 0 } }, { lastEventId: '1.0' }, { lastEventId: '001' }]) {
+ await assert.rejects(stream({ ...streamConfig, ...invalid }).next(), /参数无效/)
+}
+const interrupted = stream(streamConfig)
+const next = interrupted.next()
+const interruptedResult = assert.rejects(next, /取消/)
+streamController.abort()
+await interruptedResult
+const failedStream = createMockPluginStream({ getDelay: () => 100, shouldFail: () => true })
+await assert.rejects(
+ failedStream({ ...streamConfig, signal: new AbortController().signal }).next(),
+ /失败/
+)
diff --git a/ruoyi-fastapi-frontend/vue2/web/tests/plugins/pluginFrame.test.mjs b/ruoyi-fastapi-frontend/vue2/web/tests/plugins/pluginFrame.test.mjs
new file mode 100644
index 0000000..75e9888
--- /dev/null
+++ b/ruoyi-fastapi-frontend/vue2/web/tests/plugins/pluginFrame.test.mjs
@@ -0,0 +1,71 @@
+import assert from 'node:assert/strict'
+import { readFileSync } from 'node:fs'
+import { computed, effectScope, nextTick, reactive, ref, watch } from 'vue'
+import { compileScript, parse } from 'vue/compiler-sfc'
+
+// 使用 Vue 调度器执行组件的真实会话监听逻辑。
+// 仅查询参数变化导致 router.meta 重建时,不应重新打开已有页面。
+const source = readFileSync(
+ new URL('../../src/components/PluginFrame/index.vue', import.meta.url),
+ 'utf8'
+)
+const descriptor = parse({ source, filename: 'PluginComponent.vue' })
+const compiled = compileScript(descriptor, { id: 'plugin-frame-watch-test' })
+const sessionWatch = compiled.scriptSetupAst.find(
+ (statement) =>
+ statement.type === 'ExpressionStatement' &&
+ statement.expression.type === 'CallExpression' &&
+ statement.expression.callee.name === 'watch'
+).expression
+const installWatcher = new Function(
+ 'watch',
+ 'route',
+ 'userStore',
+ 'cleanup',
+ 'error',
+ 'active',
+ 'openPlugin',
+ `return ${descriptor.scriptSetup.content.slice(sessionWatch.start, sessionWatch.end)}`
+)
+const routeState = reactive({ meta: { pluginId: 'demo' }, query: { pluginRoute: '/' } })
+const route = computed(() => routeState)
+const userStore = reactive({ token: 'host-session' })
+const error = ref('')
+const cleanupCalls = []
+let reopened = 0
+const scope = effectScope()
+scope.run(() =>
+ installWatcher(
+ watch,
+ route,
+ userStore,
+ (logout) => cleanupCalls.push(logout),
+ error,
+ true,
+ () => {
+ reopened += 1
+ }
+ )
+)
+
+routeState.meta = { pluginId: 'demo' }
+routeState.query = { pluginRoute: '/details' }
+await nextTick()
+assert.equal(
+ reopened,
+ 0,
+ 'same plugin + same token must preserve the frame across query navigation'
+)
+assert.deepEqual(cleanupCalls, [])
+
+routeState.meta = { pluginId: 'another_demo' }
+await nextTick()
+assert.equal(reopened, 1, 'switching plugin must open a new session')
+userStore.token = 'new-host-session'
+await nextTick()
+assert.equal(reopened, 2, 'a changed host login must not reuse the previous plugin session')
+userStore.token = ''
+await nextTick()
+assert.deepEqual(cleanupCalls, [true], 'logout must destroy the bridge and notify the frame')
+assert.match(error.value, /登录状态已失效/)
+scope.stop()
diff --git a/ruoyi-fastapi-frontend/vue2/web/tests/plugins/pluginHostIntegration.test.cjs b/ruoyi-fastapi-frontend/vue2/web/tests/plugins/pluginHostIntegration.test.cjs
new file mode 100644
index 0000000..c2b025b
--- /dev/null
+++ b/ruoyi-fastapi-frontend/vue2/web/tests/plugins/pluginHostIntegration.test.cjs
@@ -0,0 +1,297 @@
+const assert = require('node:assert/strict')
+const { EventEmitter } = require('node:events')
+const { readFileSync } = require('node:fs')
+const path = require('node:path')
+const Service = require('@vue/cli-service/lib/Service')
+const { createLoader } = require('../support/load-module.cjs')
+
+const root = path.resolve(__dirname, '../..')
+const originalEnvironment = { ...process.env }
+const originalDirectory = process.cwd()
+const loadUtility = createLoader()
+const { normalizePluginBase, validatePluginSession } = loadUtility(
+ path.join(root, 'src/utils/pluginBridge.js')
+)
+const { normalizeTransportPath } = loadUtility(path.join(root, 'src/utils/transportPath.js'))
+
+/**
+ * 使用 Vue CLI 自带环境加载器读取实际配置,完整恢复环境与配置缓存。
+ *
+ * @param {string} mode Vue CLI 环境名称
+ * @param {string|undefined} pluginBase 显式前缀;未指定时使用环境文件默认值
+ * @returns {Object} 实际代理配置和客户端环境值
+ */
+function loadProxyConfiguration(mode, pluginBase) {
+ const environment = { ...process.env }
+ const directory = process.cwd()
+ const configPath = require.resolve(path.join(root, 'vue.config.js'))
+ const cachedConfiguration = require.cache[configPath]
+ try {
+ process.chdir(root)
+ for (const key of Object.keys(process.env)) {
+ if (key.startsWith('VUE_APP_') || key === 'NODE_ENV' || key === 'BABEL_ENV') {
+ delete process.env[key]
+ }
+ }
+ if (pluginBase !== undefined) process.env.VUE_APP_PLUGIN_BASE = pluginBase
+ // 复用真实 mode 与通用 .env 加载顺序,不初始化服务或 Webpack 编译。
+ Service.prototype.loadEnv.call({ context: root }, mode)
+ Service.prototype.loadEnv.call({ context: root })
+ delete require.cache[configPath]
+ return {
+ proxy: require(configPath).devServer.proxy,
+ pluginBase: process.env.VUE_APP_PLUGIN_BASE || '',
+ baseApi: process.env.VUE_APP_BASE_API || '',
+ }
+ } finally {
+ process.chdir(directory)
+ for (const key of Object.keys(process.env)) {
+ if (!Object.hasOwn(environment, key)) delete process.env[key]
+ }
+ Object.assign(process.env, environment)
+ if (cachedConfiguration) require.cache[configPath] = cachedConfiguration
+ else delete require.cache[configPath]
+ }
+}
+
+/**
+ * 验证真实代理保留 SSE 上下游异常与退出清理。
+ *
+ * @param {Object} appProxy 插件应用的实际 Vue CLI 代理配置
+ * @returns {void}
+ */
+function checkStreamCleanup(appProxy) {
+ const pair = (contentType = 'text/event-stream; charset=utf-8') => {
+ const upstream = new EventEmitter()
+ const response = new EventEmitter()
+ upstream.headers = { 'content-type': contentType }
+ upstream.complete = false
+ upstream.destroy = () => {
+ upstream.destroyed = true
+ }
+ response.destroy = () => {
+ response.destroyed = true
+ }
+ appProxy.onProxyRes(upstream, {}, response)
+ return { upstream, response }
+ }
+ for (const event of ['aborted', 'error']) {
+ const { upstream, response } = pair()
+ upstream.emit(event)
+ assert.equal(response.destroyed, true, `SSE 上游 ${event} 必须结束下游`)
+ }
+ const disconnected = pair()
+ disconnected.response.emit('close')
+ assert.equal(disconnected.upstream.destroyed, true)
+ const complete = pair()
+ complete.upstream.complete = true
+ complete.response.emit('close')
+ assert.equal(complete.upstream.destroyed, undefined)
+ const json = pair('application/json')
+ assert.equal(json.upstream.listenerCount('aborted'), 0)
+ assert.equal(json.upstream.listenerCount('error'), 0)
+ assert.equal(json.response.listenerCount('close'), 0)
+}
+
+/**
+ * 验证浏览器会话、专用代理与加密 AAD 使用同一个部署前缀。
+ *
+ * @param {Object} configuration 实际加载的环境配置
+ * @returns {void}
+ */
+function checkPluginPaths({ proxy, pluginBase, baseApi }) {
+ const prefix = normalizePluginBase(pluginBase)
+ const appProxy = proxy[`${prefix}/apps/`]
+ const sessionProxy = proxy[`${prefix}/plugin/runtime/`]
+ assert.ok(appProxy, '插件应用必须走独立代理')
+ assert.ok(sessionProxy, '插件会话必须走独立代理')
+ assert.equal(appProxy.changeOrigin, false)
+ assert.equal(sessionProxy.changeOrigin, false)
+ assert.equal(appProxy.ws, true)
+ assert.equal(proxy[baseApi].pathRewrite[`^${baseApi}`], '')
+ for (const [entry, requestPath] of [
+ [appProxy, '/apps/bundle_demo/ui/index.html?asset=main.js'],
+ [appProxy, '/apps/task_demo/api/events?tag=first&tag=second&next=%2Fdev-api%2F'],
+ [sessionProxy, '/plugin/runtime/bundle_demo/session?mode=preview'],
+ ]) {
+ assert.equal(typeof entry.pathRewrite, 'function')
+ assert.equal(
+ entry.pathRewrite(`${prefix}${requestPath}`),
+ requestPath,
+ '只剥离一次外部前缀,并完整保留查询串'
+ )
+ assert.equal(
+ normalizeTransportPath(`${prefix}${requestPath}`, baseApi, pluginBase),
+ requestPath.split('?')[0],
+ 'AAD 使用后端接收到的无前缀路径'
+ )
+ }
+ for (const pluginId of ['bundle_demo', 'task_demo']) {
+ const session = {
+ pluginId,
+ bridgeVersion: 1,
+ uiBase: `${prefix}/apps/${pluginId}/ui/`,
+ apiBase: `${prefix}/apps/${pluginId}/api/`,
+ csrfToken: 'test-plugin-csrf-token',
+ expiresIn: 300,
+ }
+ assert.deepEqual(validatePluginSession(session, pluginId, pluginBase, 100), {
+ ...session,
+ expiresAt: 300100,
+ })
+ }
+ checkStreamCleanup(appProxy)
+}
+
+/**
+ * 仅提取后端非敏感路径配置,断言和日志不包含其他环境值。
+ *
+ * @param {string} filename 已有的后端环境文件名
+ * @returns {string} 后端应用部署前缀
+ */
+function readBackendRootPath(filename) {
+ const match = readFileSync(
+ path.resolve(root, '../../../ruoyi-fastapi-backend', filename),
+ 'utf8'
+ ).match(/^APP_ROOT_PATH\s*=\s*['"]?([^'"\s#]*)['"]?\s*(?:#.*)?$/m)
+ assert.ok(match, `${filename} 必须显式配置 APP_ROOT_PATH`)
+ return match[1]
+}
+
+/**
+ * 检查实际 Docker nginx 插件代理的路径替换与同源流式配置。
+ *
+ * @param {string} variant Docker 数据库配置名称
+ * @param {string} upstream 对应的后端服务名
+ * @param {string} prefix 客户端与后端共有的部署前缀
+ * @returns {void}
+ */
+function checkDockerNginx(variant, upstream, prefix) {
+ const source = readFileSync(path.join(root, `bin/nginx.${variant}.conf`), 'utf8')
+ const locations = [...source.matchAll(/^\s*location\s+\^~\s+(\S+)\s*\{([^{}]*)\}/gm)]
+ const blocks = {}
+ for (const route of ['/apps/', '/plugin/runtime/']) {
+ const matches = locations.filter((match) => match[1] === `${prefix}${route}`)
+ assert.equal(matches.length, 1, `${variant} 必须有唯一的 ${prefix}${route} 专用代理`)
+ const block = matches[0][2]
+ const proxyPass = block.match(/^\s*proxy_pass\s+(\S+)\s*;/m)
+ assert.equal(proxyPass?.[1], `http://${upstream}:9099${route}`)
+ assert.match(block, /^\s*proxy_set_header\s+Host\s+\$http_host\s*;/m)
+ assert.match(block, /^\s*proxy_set_header\s+X-Forwarded-Proto\s+\$scheme\s*;/m)
+ assert.ok(!locations.some((match) => match[1] === route), '不得遗留无部署前缀的专用代理')
+ blocks[route] = block
+ }
+ assert.match(blocks['/apps/'], /^\s*proxy_http_version\s+1\.1\s*;/m)
+ assert.match(blocks['/apps/'], /^\s*proxy_set_header\s+Upgrade\s+\$http_upgrade\s*;/m)
+ assert.match(
+ blocks['/apps/'],
+ /^\s*proxy_set_header\s+Connection\s+\$plugin_connection_upgrade\s*;/m
+ )
+ assert.match(blocks['/apps/'], /^\s*proxy_buffering\s+off\s*;/m)
+ assert.match(blocks['/apps/'], /^\s*proxy_read_timeout\s+300s\s*;/m)
+ assert.match(
+ blocks['/plugin/runtime/'],
+ /^\s*add_header\s+Cache-Control\s+"no-store"\s+always\s*;/m
+ )
+ const upgradeMap = source.match(
+ /^\s*map\s+\$http_upgrade\s+\$plugin_connection_upgrade\s*\{([^{}]*)\}/m
+ )
+ assert.ok(upgradeMap, 'WebSocket 的 Connection 请求头必须按 Upgrade 状态切换')
+ assert.match(upgradeMap[1], /^\s*default\s+upgrade\s*;/m)
+ assert.match(upgradeMap[1], /^\s*''\s+close\s*;/m)
+}
+
+module.exports = (async () => {
+ for (const [mode, expectedBase, backendFiles] of [
+ ['development', '/dev-api', ['.env.dev']],
+ ['production', '/prod-api', ['.env.prod']],
+ ['docker', '/docker-api', ['.env.dockermy', '.env.dockerpg']],
+ ['staging', '/stage-api', []],
+ ]) {
+ const configuration = loadProxyConfiguration(mode)
+ assert.equal(configuration.pluginBase, expectedBase)
+ assert.equal(configuration.pluginBase, configuration.baseApi, `${mode} 的两种 API 前缀必须一致`)
+ for (const filename of backendFiles) {
+ assert.equal(configuration.pluginBase, readBackendRootPath(filename))
+ }
+ checkPluginPaths(configuration)
+ if (mode === 'docker') {
+ checkDockerNginx('dockermy', 'ruoyi-backend-my', configuration.pluginBase)
+ checkDockerNginx('dockerpg', 'ruoyi-backend-pg', configuration.pluginBase)
+ }
+ }
+ for (const base of ['', '/', '/gateway', '/gateway/nested-v1/']) {
+ const configuration = loadProxyConfiguration('development', base)
+ assert.equal(configuration.pluginBase, base, '显式前缀必须覆盖开发环境默认值')
+ checkPluginPaths(configuration)
+ }
+ for (const base of [
+ 'https://foreign.example',
+ '//gateway',
+ '/gateway/../admin',
+ '/gateway?x=1',
+ ]) {
+ assert.throws(() => loadProxyConfiguration('development', base), /插件部署路径无效/)
+ }
+ assert.equal(
+ normalizeTransportPath('/dev-api/system/user', '/dev-api', '/gateway'),
+ '/system/user'
+ )
+ assert.equal(
+ normalizeTransportPath('/dev-api-other/system/user', '/dev-api', '/gateway'),
+ '/dev-api-other/system/user'
+ )
+
+ let responseHandler
+ let hostLoginPrompts = 0
+ const axios = {
+ defaults: { headers: {} },
+ create: () => ({
+ interceptors: {
+ request: { use() {} },
+ response: {
+ use(handler) {
+ responseHandler = handler
+ },
+ },
+ },
+ }),
+ isCancel: () => false,
+ }
+ const load = createLoader({
+ axios,
+ 'element-ui': {
+ Notification: { error() {} },
+ MessageBox: {
+ confirm() {
+ hostLoginPrompts += 1
+ return new Promise(() => {})
+ },
+ },
+ Message() {},
+ Loading: {},
+ },
+ '@/store': { dispatch: async () => {} },
+ '@/utils/auth': { getToken: () => 'test-main-session' },
+ '@/utils/time': { getDisplayTimezone: () => 'UTC' },
+ '@/utils/errorCode': {},
+ '@/utils/ruoyi': { tansParams: () => '', blobValidate: () => true },
+ '@/plugins/cache': {},
+ 'file-saver': { saveAs() {} },
+ '@/utils/transportCrypto': { decryptTransportResponse: async (response) => response },
+ })
+ load(path.join(root, 'src/utils/request.js'))
+ const result = (code, pluginBridge) => ({
+ data: { code, msg: 'session status' },
+ request: { responseType: 'json' },
+ config: { pluginBridge },
+ })
+ await assert.rejects(responseHandler(result(401, true)), /插件请求失败/)
+ await assert.rejects(responseHandler(result(403, true)), /插件请求失败/)
+ assert.equal(hostLoginPrompts, 0)
+ assert.deepEqual(await responseHandler(result(200, true)), { code: 200, msg: 'session status' })
+ await assert.rejects(responseHandler(result(401, false)))
+ assert.equal(hostLoginPrompts, 1)
+ assert.deepEqual({ ...process.env }, originalEnvironment)
+ assert.equal(process.cwd(), originalDirectory)
+})()
diff --git a/ruoyi-fastapi-frontend/vue2/web/tests/plugins/pluginReleaseFormatter.test.mjs b/ruoyi-fastapi-frontend/vue2/web/tests/plugins/pluginReleaseFormatter.test.mjs
new file mode 100644
index 0000000..67a65a2
--- /dev/null
+++ b/ruoyi-fastapi-frontend/vue2/web/tests/plugins/pluginReleaseFormatter.test.mjs
@@ -0,0 +1,116 @@
+import assert from 'node:assert/strict'
+import { readFileSync } from 'node:fs'
+import { compileScript, parse } from 'vue/compiler-sfc'
+import { computed, reactive } from 'vue'
+import { getPluginReleaseView } from '../../src/utils/pluginReleaseFormatter.js'
+
+const plugin = reactive({
+ source: 'artifact',
+ enabled: '0',
+ release: {
+ status: 'pending_restart',
+ prepareStatus: 'prepared',
+ healthyWorkers: 0,
+ disabledWorkers: 0,
+ liveWorkers: 0,
+ expectedWorkers: 2,
+ missingWorkers: 2,
+ mismatchWorkers: 0,
+ staleWorkers: 0,
+ failedWorkers: 0,
+ restartRequired: true,
+ verificationError: '',
+ lastError: '',
+ },
+})
+const view = computed(() => getPluginReleaseView(plugin))
+assert.equal(view.value.label, '等待重启')
+assert.equal(view.value.workerSummary, '已就绪 0 · 在线 0 / 预期 2')
+assert.equal(view.value.preparationLabel, '准备完成')
+assert.equal(view.value.restartRequired, true)
+
+plugin.release.status = 'partial'
+plugin.release.liveWorkers = 2
+plugin.release.healthyWorkers = 1
+plugin.release.mismatchWorkers = 1
+assert.equal(view.value.label, '部分生效')
+assert.equal(view.value.tagType, 'warning')
+assert.equal(view.value.counts.mismatch, '1')
+assert.match(view.value.workerSummary, /^已就绪 1/)
+
+plugin.release.status = 'active'
+plugin.release.healthyWorkers = 2
+plugin.release.restartRequired = false
+assert.equal(view.value.label, '已生效')
+assert.equal(view.value.tagType, 'success')
+assert.equal(view.value.restartRequired, false)
+
+plugin.enabled = '1'
+plugin.release.status = 'disabled'
+plugin.release.healthyWorkers = 0
+plugin.release.disabledWorkers = 2
+assert.equal(view.value.label, '已停用')
+assert.equal(view.value.workerSummary, '已停用 2 · 在线 2 / 预期 2')
+
+plugin.release.verificationError = '公钥已撤销'
+plugin.release.lastError = 'older maintenance failure'
+assert.equal(view.value.label, '验证失败', 'signature failure must not retain a healthy label')
+assert.equal(view.value.tagType, 'danger')
+assert.equal(view.value.error, '公钥已撤销')
+
+const missing = getPluginReleaseView({ source: 'artifact' })
+assert.equal(missing.label, '状态未知')
+assert.equal(missing.unavailable, true)
+assert.equal(missing.workerSummary, '已就绪 未知 · 在线 未知 / 预期 未知')
+assert.equal(missing.restartRequired, false)
+for (const value of [undefined, null, -1, NaN, '2', true]) {
+ assert.equal(getPluginReleaseView({ release: { healthyWorkers: value } }).counts.healthy, '未知')
+}
+assert.equal(getPluginReleaseView({ release: { status: 'no_target' } }).label, '未选择目标')
+assert.equal(
+ getPluginReleaseView({ release: { status: 'failed', prepareStatus: 'failed' } }).preparationLabel,
+ '准备失败'
+)
+assert.equal(getPluginReleaseView({ release: { status: 'new_server_state' } }).label, '状态未知')
+assert.equal(
+ getPluginReleaseView({ release: { lastError: 'failed preparation' } }).error,
+ 'failed preparation'
+)
+
+// 执行页面的真实操作限制:即使旧版接口响应缺少能力元数据,制品记录也应保持只读。
+const pageSource = readFileSync(
+ new URL('../../src/views/system/plugin/index.vue', import.meta.url),
+ 'utf8'
+)
+const descriptor = parse({ source: pageSource, filename: 'PluginIndex.vue' })
+const compiled = compileScript(descriptor, { id: 'plugin-release-read-only-test' })
+const gateNames = [
+ 'canSelectForBatch',
+ 'canInstall',
+ 'canUpgrade',
+ 'canUninstall',
+ 'isOrphanPlugin',
+ 'isOperationBlocked',
+ 'isCapabilityOperationBlocked',
+]
+const gateSource = compiled.scriptSetupAst
+ .filter(
+ (statement) => statement.type === 'FunctionDeclaration' && gateNames.includes(statement.id.name)
+ )
+ .map((statement) => descriptor.scriptSetup.content.slice(statement.start, statement.end))
+ .join('\n')
+const gates = new Function(`${gateSource}\nreturn {${gateNames.join(',')}}`)()
+for (const status of ['discovered', 'installed', 'pending_upgrade', 'error']) {
+ const artifact = { source: 'artifact', enabled: '0', installedVersion: '1.0.0', status }
+ assert.equal(gates.canSelectForBatch(artifact), false)
+ assert.equal(gates.canInstall(artifact), false)
+ assert.equal(gates.canUpgrade(artifact), false)
+ assert.equal(gates.canUninstall(artifact), false)
+ for (const operation of ['install', 'upgrade', 'enable', 'disable', 'uninstall']) {
+ assert.equal(gates.isOperationBlocked(artifact, operation), true)
+ }
+}
+const local = { source: 'local', status: 'discovered' }
+assert.equal(gates.canSelectForBatch(local), true)
+assert.equal(gates.canInstall(local), true)
+assert.equal(gates.isOperationBlocked(local, 'install'), false)
diff --git a/ruoyi-fastapi-frontend/vue2/web/tests/plugins/pluginRuntimeMetrics.test.mjs b/ruoyi-fastapi-frontend/vue2/web/tests/plugins/pluginRuntimeMetrics.test.mjs
new file mode 100644
index 0000000..11843f6
--- /dev/null
+++ b/ruoyi-fastapi-frontend/vue2/web/tests/plugins/pluginRuntimeMetrics.test.mjs
@@ -0,0 +1,79 @@
+import assert from 'node:assert/strict'
+import { readFileSync } from 'node:fs'
+import { compileScript, parse } from 'vue/compiler-sfc'
+import { computed, effectScope, nextTick, reactive, ref, watch } from 'vue'
+
+// 执行组件的真实监听与请求逻辑,只替换网络边界和卸载钩子。
+const source = readFileSync(
+ new URL('../../src/views/system/plugin/components/PluginRuntimeMetrics.vue', import.meta.url),
+ 'utf8'
+)
+const descriptor = parse({ source, filename: 'PluginComponent.vue' })
+const compiled = compileScript(descriptor, { id: 'plugin-runtime-metrics-test' })
+const script = compiled.scriptSetupAst
+ .filter((statement) => statement.type !== 'ImportDeclaration')
+ .map((statement) => descriptor.scriptSetup.content.slice(statement.start, statement.end))
+ .join('\n')
+const setup = new Function(
+ 'computed',
+ 'ref',
+ 'watch',
+ 'onBeforeUnmount',
+ 'defineProps',
+ 'getPluginRuntimeMetrics',
+ `${script}\nreturn { data, loading, error, refresh }`
+)
+const props = reactive({ pluginId: 'first_plugin' })
+const requests = []
+let unmount
+const scope = effectScope()
+const state = scope.run(() =>
+ setup(
+ computed,
+ ref,
+ watch,
+ (callback) => {
+ unmount = callback
+ },
+ () => props,
+ (pluginId, signal) =>
+ new Promise((resolve, reject) => {
+ requests.push({ pluginId, signal, resolve, reject })
+ })
+ )
+)
+assert.equal(state.loading.value, true)
+assert.equal(requests[0].pluginId, 'first_plugin')
+
+props.pluginId = 'second_plugin'
+await nextTick()
+assert.equal(requests[0].signal.aborted, true)
+requests[1].resolve({ data: { series: [{ version: 'second' }] } })
+await nextTick()
+requests[0].resolve({ data: { series: [{ version: 'stale' }] } })
+await nextTick()
+assert.equal(
+ state.data.value.series[0].version,
+ 'second',
+ 'late response must not replace the selected plugin'
+)
+assert.equal(state.loading.value, false)
+
+const failed = state.refresh()
+requests[2].reject(new Error('network unavailable'))
+await failed
+assert.match(state.error.value, /查询失败/)
+assert.equal(state.loading.value, false)
+const retry = state.refresh()
+assert.equal(state.error.value, '')
+requests[3].resolve({ data: { supported: false, series: [] } })
+await retry
+assert.equal(state.data.value.supported, false)
+
+const closing = state.refresh()
+unmount()
+scope.stop()
+assert.equal(requests[4].signal.aborted, true)
+requests[4].reject(new Error('cancelled'))
+await closing
+assert.equal(state.error.value, '', 'closing the dialog must not report a failed query')
diff --git a/ruoyi-fastapi-frontend/tests/plugins/pluginSse.test.js b/ruoyi-fastapi-frontend/vue2/web/tests/plugins/pluginSse.test.mjs
similarity index 100%
rename from ruoyi-fastapi-frontend/tests/plugins/pluginSse.test.js
rename to ruoyi-fastapi-frontend/vue2/web/tests/plugins/pluginSse.test.mjs
diff --git a/ruoyi-fastapi-frontend/vue2/web/tests/plugins/pluginViewResolver.test.js b/ruoyi-fastapi-frontend/vue2/web/tests/plugins/pluginViewResolver.test.js
new file mode 100644
index 0000000..72f575b
--- /dev/null
+++ b/ruoyi-fastapi-frontend/vue2/web/tests/plugins/pluginViewResolver.test.js
@@ -0,0 +1,14 @@
+const assert = require('assert').strict
+
+const { resolvePluginViewPath } = require('../../src/utils/pluginViewResolver')
+
+assert.equal(resolvePluginViewPath('plugin/demo/index'), './demo/views/index.vue')
+assert.equal(resolvePluginViewPath('/plugin/demo/detail/list'), './demo/views/detail/list.vue')
+assert.equal(resolvePluginViewPath('system/user/index'), '')
+assert.equal(resolvePluginViewPath('plugin/demo'), '')
+assert.equal(resolvePluginViewPath('plugin/demo/../admin'), '')
+assert.equal(resolvePluginViewPath('plugin/demo//index'), '')
+assert.equal(resolvePluginViewPath('plugin/Demo/index'), '')
+assert.equal(resolvePluginViewPath('plugin/demo/index.vue'), '')
+assert.equal(resolvePluginViewPath('plugin/demo/detail\\list'), '')
+assert.equal(resolvePluginViewPath(null), '')
diff --git a/ruoyi-fastapi-frontend/vue2/web/tests/plugins/pluginVue2Components.test.cjs b/ruoyi-fastapi-frontend/vue2/web/tests/plugins/pluginVue2Components.test.cjs
new file mode 100644
index 0000000..eb8b195
--- /dev/null
+++ b/ruoyi-fastapi-frontend/vue2/web/tests/plugins/pluginVue2Components.test.cjs
@@ -0,0 +1,51 @@
+const assert = require('node:assert/strict')
+const { readdirSync, readFileSync } = require('node:fs')
+const { join, resolve } = require('node:path')
+const compiler = require('vue/compiler-sfc')
+const { transformSync } = require('@babel/core')
+const transformModules = require('@babel/plugin-transform-modules-commonjs')
+
+const frontend = resolve(__dirname, '../..')
+const components = join(frontend, 'src/views/system/plugin/components')
+const files = [
+ join(frontend, 'src/components/PluginFrame/index.vue'),
+ join(frontend, 'src/views/system/plugin/index.vue'),
+ ...readdirSync(components)
+ .filter((name) => name.endsWith('.vue'))
+ .map((name) => join(components, name)),
+]
+
+// 同时编译真实 Vue 2 模板、setup 脚本和 scoped 样式,避免仅通过脚本逻辑测试。
+for (const filename of files) {
+ const source = readFileSync(filename, 'utf8')
+ const descriptor = compiler.parse({ source, filename })
+ const script = compiler.compileScript(descriptor, { id: filename })
+ const template = compiler.compileTemplate({
+ source: descriptor.template.content,
+ filename,
+ compilerOptions: { bindingMetadata: script.bindings },
+ })
+ assert.deepEqual(template.errors, [], `${filename}: template must compile with Vue 2`)
+ assert.ok(template.code.includes('render'), `${filename}: render function must exist`)
+ assert.doesNotThrow(() =>
+ transformSync(script.content, {
+ filename,
+ configFile: false,
+ babelrc: false,
+ plugins: [transformModules],
+ })
+ )
+ for (const style of descriptor.styles) {
+ const result = compiler.compileStyle({
+ source: style.content,
+ filename,
+ id: 'data-v-plugin',
+ scoped: style.scoped,
+ })
+ assert.deepEqual(result.errors, [], `${filename}: styles must compile with Vue 2`)
+ }
+ assert.doesNotMatch(
+ source,
+ /from ['"](?:element-plus|@vue\/compiler-sfc)['"]|VITE_APP_PLUGIN_BASE/
+ )
+}
diff --git a/ruoyi-fastapi-frontend/vue2/web/tests/plugins/run-plugin-tests.js b/ruoyi-fastapi-frontend/vue2/web/tests/plugins/run-plugin-tests.js
new file mode 100644
index 0000000..afd1ded
--- /dev/null
+++ b/ruoyi-fastapi-frontend/vue2/web/tests/plugins/run-plugin-tests.js
@@ -0,0 +1,40 @@
+const { readdirSync } = require('node:fs')
+const { join, relative } = require('node:path')
+const { pathToFileURL } = require('node:url')
+
+const collectTestFiles = (root) => {
+ const entries = readdirSync(root, { withFileTypes: true })
+ const testFiles = []
+ for (const entry of entries) {
+ const entryPath = join(root, entry.name)
+ if (entry.isDirectory()) testFiles.push(...collectTestFiles(entryPath))
+ else if (entry.isFile() && /\.test\.(?:js|cjs|mjs)$/.test(entry.name)) testFiles.push(entryPath)
+ }
+ return testFiles.sort()
+}
+
+async function run() {
+ const testFiles = collectTestFiles(__dirname)
+ if (testFiles.length === 0) throw new Error('No plugin tests found')
+ let failedCount = 0
+ for (const testFile of testFiles) {
+ const testName = relative(__dirname, testFile)
+ try {
+ // 等待 ESM 顶层 await,同时兼容旧 CommonJS 测试导出的 Promise。
+ const loaded = await import(pathToFileURL(testFile).href)
+ await loaded.default
+ console.log(`ok ${testName}`)
+ } catch (error) {
+ failedCount += 1
+ console.error(`not ok ${testName}`)
+ console.error(error && error.stack ? error.stack : error)
+ }
+ }
+ if (failedCount > 0) throw new Error(`Plugin tests failed: ${failedCount}/${testFiles.length}`)
+ console.log(`Plugin tests passed: ${testFiles.length}`)
+}
+
+run().catch((error) => {
+ console.error(error && error.stack ? error.stack : error)
+ process.exitCode = 1
+})
diff --git a/ruoyi-fastapi-frontend/vue2/web/tests/support/load-module.cjs b/ruoyi-fastapi-frontend/vue2/web/tests/support/load-module.cjs
new file mode 100644
index 0000000..09bc899
--- /dev/null
+++ b/ruoyi-fastapi-frontend/vue2/web/tests/support/load-module.cjs
@@ -0,0 +1,55 @@
+const fs = require('node:fs')
+const path = require('node:path')
+const vm = require('node:vm')
+const { createRequire } = require('node:module')
+const { transformSync } = require('@babel/core')
+const transformModules = require('@babel/plugin-transform-modules-commonjs')
+const compiler = require('vue/compiler-sfc')
+
+// 编译真实 Vue 2.7 组件,只隔离 API 和浏览器资源。
+function createLoader(mocks = {}, globals = {}) {
+ const cache = new Map()
+ function loadModule(filename) {
+ filename = path.resolve(filename)
+ if (!path.extname(filename)) {
+ filename = ['.js', '.vue', '/index.js', '/index.vue']
+ .map((suffix) => filename + suffix)
+ .find(fs.existsSync)
+ }
+ if (cache.has(filename)) return cache.get(filename).exports
+ let source = fs.readFileSync(filename, 'utf8')
+ if (filename.endsWith('.vue')) {
+ const descriptor = compiler.parse({ source, filename })
+ source = descriptor.scriptSetup
+ ? compiler.compileScript(descriptor, { id: filename }).content
+ : descriptor.script.content
+ }
+ const code = transformSync(source, {
+ filename,
+ configFile: false,
+ babelrc: false,
+ plugins: [transformModules],
+ }).code
+ const module = { exports: {} }
+ cache.set(filename, module)
+ const nativeRequire = createRequire(filename)
+ const requireModule = (specifier) => {
+ if (Object.prototype.hasOwnProperty.call(mocks, specifier)) return mocks[specifier]
+ if (specifier.startsWith('@/'))
+ return loadModule(path.resolve(__dirname, '../../src', specifier.slice(2)))
+ if (specifier.startsWith('.'))
+ return loadModule(path.resolve(path.dirname(filename), specifier))
+ return nativeRequire(specifier)
+ }
+ const execute = vm.compileFunction(
+ code,
+ ['module', 'exports', 'require', ...Object.keys(globals)],
+ { filename }
+ )
+ execute(module, module.exports, requireModule, ...Object.values(globals))
+ return module.exports
+ }
+ return loadModule
+}
+
+module.exports = { createLoader, loadModule: createLoader() }
diff --git a/ruoyi-fastapi-frontend/vue2/web/tests/time/time.test.mjs b/ruoyi-fastapi-frontend/vue2/web/tests/time/time.test.mjs
new file mode 100644
index 0000000..14c641b
--- /dev/null
+++ b/ruoyi-fastapi-frontend/vue2/web/tests/time/time.test.mjs
@@ -0,0 +1,3 @@
+import * as time from '../../src/utils/time.js'
+import { testTimeContract } from '../../../../../ruoyi-fastapi-test/time-contract/contract.mjs'
+testTimeContract(time)
diff --git a/ruoyi-fastapi-frontend/vue2/web/vue.config.js b/ruoyi-fastapi-frontend/vue2/web/vue.config.js
new file mode 100644
index 0000000..fdc6caa
--- /dev/null
+++ b/ruoyi-fastapi-frontend/vue2/web/vue.config.js
@@ -0,0 +1,445 @@
+'use strict'
+const fs = require('fs')
+const path = require('path')
+const CopyWebpackPlugin = require('copy-webpack-plugin')
+const webpack = require('webpack')
+
+function resolve(dir) {
+ return path.join(__dirname, dir)
+}
+
+function tryResolve(specifier) {
+ try {
+ return require.resolve(specifier)
+ } catch {
+ return null
+ }
+}
+
+function resolveNodeModulesPackageRoot(pkgName) {
+ const candidates = [
+ path.join(__dirname, 'node_modules', pkgName),
+ path.resolve(__dirname, '../node_modules', pkgName),
+ ]
+ for (const candidate of candidates) {
+ try {
+ if (fs.existsSync(candidate)) {
+ return candidate
+ }
+ } catch {}
+ }
+ return null
+}
+
+function resolvePackageRoot(pkgName) {
+ const entry = tryResolve(pkgName)
+ if (!entry) {
+ return null
+ }
+ return path.dirname(path.dirname(entry))
+}
+
+function resolveSiblingPackagePath(fromPkgRoot, siblingPkgName) {
+ if (!fromPkgRoot) {
+ return null
+ }
+ const candidate = path.join(path.dirname(fromPkgRoot), siblingPkgName)
+ try {
+ if (fs.existsSync(candidate)) {
+ return candidate
+ }
+ } catch {}
+ return null
+}
+
+function setAliasIfResolved(alias, key, specifier) {
+ const resolved = tryResolve(specifier)
+ if (resolved) {
+ alias[key] = resolved
+ }
+}
+
+function setAliasIfExists(alias, key, filePath) {
+ if (!filePath) {
+ return
+ }
+ alias[key] = filePath
+}
+
+const markstreamVue2Root = resolvePackageRoot('markstream-vue2')
+const terrastructD2Root = resolveNodeModulesPackageRoot('@terrastruct/d2')
+const streamMonacoRoot = resolvePackageRoot('stream-monaco')
+const streamMarkdownRoot =
+ resolvePackageRoot('stream-markdown') || resolveNodeModulesPackageRoot('stream-markdown')
+const monacoEditorRoot =
+ resolveNodeModulesPackageRoot('monaco-editor') ||
+ resolveSiblingPackagePath(streamMonacoRoot, 'monaco-editor')
+const shikiRoot = resolvePackageRoot('shiki')
+const shikijsLangsRoot = resolvePackageRoot('@shikijs/langs')
+const shikijsThemesRoot = resolvePackageRoot('@shikijs/themes')
+const shikijsOnigurumaRoot = resolvePackageRoot('@shikijs/engine-oniguruma')
+const shikijsMonacoRoot = resolvePackageRoot('@shikijs/monaco')
+
+const markstreamAlias = {}
+setAliasIfResolved(markstreamAlias, 'vue$', 'vue/dist/vue.runtime.esm.js')
+setAliasIfExists(
+ markstreamAlias,
+ 'markstream-vue2$',
+ markstreamVue2Root ? path.join(markstreamVue2Root, 'dist/index.cjs') : null
+)
+setAliasIfExists(
+ markstreamAlias,
+ 'markstream-vue2/index.css',
+ markstreamVue2Root ? path.join(markstreamVue2Root, 'dist/index.css') : null
+)
+setAliasIfExists(
+ markstreamAlias,
+ 'markstream-vue2/index.tailwind.css',
+ markstreamVue2Root ? path.join(markstreamVue2Root, 'dist/index.tailwind.css') : null
+)
+setAliasIfExists(
+ markstreamAlias,
+ 'markstream-vue2/workers/katexRenderer.worker',
+ markstreamVue2Root ? path.join(markstreamVue2Root, 'dist/workers/katexRenderer.worker.js') : null
+)
+setAliasIfExists(
+ markstreamAlias,
+ 'markstream-vue2/workers/mermaidParser.worker',
+ markstreamVue2Root ? path.join(markstreamVue2Root, 'dist/workers/mermaidParser.worker.js') : null
+)
+setAliasIfExists(
+ markstreamAlias,
+ '@terrastruct/d2$',
+ terrastructD2Root ? path.join(terrastructD2Root, 'dist/browser/index.js') : null
+)
+setAliasIfExists(
+ markstreamAlias,
+ 'stream-monaco/legacy',
+ streamMonacoRoot ? path.join(streamMonacoRoot, 'dist/index.legacy.js') : null
+)
+setAliasIfExists(
+ markstreamAlias,
+ 'stream-monaco$',
+ streamMonacoRoot ? path.join(streamMonacoRoot, 'dist/index.js') : null
+)
+setAliasIfExists(
+ markstreamAlias,
+ 'stream-monaco',
+ streamMonacoRoot ? path.join(streamMonacoRoot, 'dist') : null
+)
+setAliasIfExists(markstreamAlias, 'monaco-editor', monacoEditorRoot)
+setAliasIfExists(
+ markstreamAlias,
+ '@shikijs/langs',
+ shikijsLangsRoot ? path.join(shikijsLangsRoot, 'dist') : null
+)
+setAliasIfExists(
+ markstreamAlias,
+ '@shikijs/themes',
+ shikijsThemesRoot ? path.join(shikijsThemesRoot, 'dist') : null
+)
+setAliasIfExists(
+ markstreamAlias,
+ '@shikijs/engine-oniguruma',
+ shikijsOnigurumaRoot ? path.join(shikijsOnigurumaRoot, 'dist') : null
+)
+setAliasIfExists(
+ markstreamAlias,
+ '@shikijs/engine-oniguruma$',
+ shikijsOnigurumaRoot ? path.join(shikijsOnigurumaRoot, 'dist/index.mjs') : null
+)
+setAliasIfExists(
+ markstreamAlias,
+ '@shikijs/engine-oniguruma/wasm-inlined',
+ shikijsOnigurumaRoot ? path.join(shikijsOnigurumaRoot, 'dist/wasm-inlined.mjs') : null
+)
+setAliasIfExists(
+ markstreamAlias,
+ '@shikijs/engine-oniguruma/wasm-inlined$',
+ shikijsOnigurumaRoot ? path.join(shikijsOnigurumaRoot, 'dist/wasm-inlined.mjs') : null
+)
+setAliasIfExists(
+ markstreamAlias,
+ '@shikijs/monaco',
+ shikijsMonacoRoot ? path.join(shikijsMonacoRoot, 'dist') : null
+)
+setAliasIfExists(
+ markstreamAlias,
+ 'shiki$',
+ shikiRoot ? path.join(shikiRoot, 'dist/index.mjs') : null
+)
+setAliasIfExists(markstreamAlias, 'shiki', shikiRoot ? path.join(shikiRoot, 'dist') : null)
+setAliasIfExists(
+ markstreamAlias,
+ 'shiki/wasm',
+ shikiRoot ? path.join(shikiRoot, 'dist/wasm.mjs') : null
+)
+setAliasIfExists(
+ markstreamAlias,
+ 'stream-markdown$',
+ streamMarkdownRoot ? path.join(streamMarkdownRoot, 'dist/index.js') : null
+)
+setAliasIfExists(
+ markstreamAlias,
+ 'stream-markdown',
+ streamMarkdownRoot ? path.join(streamMarkdownRoot, 'dist') : null
+)
+setAliasIfResolved(markstreamAlias, 'alien-signals$', 'alien-signals/esm')
+setAliasIfResolved(markstreamAlias, 'alien-signals', 'alien-signals/esm')
+setAliasIfResolved(
+ markstreamAlias,
+ '@antv/infographic/jsx-runtime',
+ '@antv/infographic/jsx-runtime'
+)
+setAliasIfResolved(
+ markstreamAlias,
+ '@antv/infographic/jsx-dev-runtime',
+ '@antv/infographic/jsx-dev-runtime'
+)
+setAliasIfResolved(
+ markstreamAlias,
+ 'measury/fonts/AlibabaPuHuiTi-Regular',
+ 'measury/fonts/AlibabaPuHuiTi-Regular'
+)
+
+function createOptionalIgnoreRegex() {
+ const alwaysIgnore = [
+ 'mermaid',
+ '@mermaid-js/parser',
+ 'langium',
+ '@antv/infographic',
+ '@antv/hierarchy',
+ 'monaco-editor/esm/vs/language/css/monaco.contribution',
+ 'monaco-editor/esm/vs/language/html/monaco.contribution',
+ 'monaco-editor/esm/vs/language/json/monaco.contribution',
+ 'monaco-editor/esm/vs/language/typescript/monaco.contribution',
+ ]
+ const maybeIgnore = []
+ if (!streamMarkdownRoot) {
+ maybeIgnore.push('stream-markdown')
+ }
+ if (!tryResolve('stream-monaco')) {
+ maybeIgnore.push('stream-monaco')
+ }
+ if (!monacoEditorRoot) {
+ maybeIgnore.push('monaco-editor')
+ }
+ if (!tryResolve('shiki')) {
+ maybeIgnore.push('shiki')
+ }
+ if (!tryResolve('@shikijs/langs')) {
+ maybeIgnore.push('@shikijs/langs')
+ }
+ if (!tryResolve('@shikijs/themes')) {
+ maybeIgnore.push('@shikijs/themes')
+ }
+ const combined = [...alwaysIgnore, ...maybeIgnore]
+ return new RegExp(`^(${combined.join('|')})$`)
+}
+
+function createMonacoAssetCopyPlugins() {
+ if (!monacoEditorRoot) {
+ return []
+ }
+ const from = path.join(monacoEditorRoot, 'esm/vs')
+ if (!fs.existsSync(from)) {
+ return []
+ }
+ return [
+ new CopyWebpackPlugin([
+ { from, to: 'monaco/vs' },
+ { from: path.join(monacoEditorRoot, 'min/vs'), to: 'monaco-json/vs' },
+ ]),
+ ]
+}
+
+const CompressionPlugin = require('compression-webpack-plugin')
+
+const name = process.env.VUE_APP_TITLE || 'vfadmin管理系统' // 网页标题
+
+const port = process.env.port || process.env.npm_config_port || 80 // 端口
+
+// 与浏览器通信桥使用同样的同源路径规则,拒绝外部 URL 和路径跳转。
+const pluginBaseValue = process.env.VUE_APP_PLUGIN_BASE || ''
+const pluginBase = pluginBaseValue === '/' ? '' : pluginBaseValue.replace(/\/$/, '')
+if (pluginBase && !/^\/[a-zA-Z0-9_-]+(?:\/[a-zA-Z0-9_-]+)*$/.test(pluginBase)) {
+ throw new Error('插件部署路径无效')
+}
+
+// vue.config.js 配置说明
+//官方vue.config.js 参考文档 https://cli.vuejs.org/zh/config/#css-loaderoptions
+// 这里只列一部分,具体配置参考文档
+module.exports = {
+ // 部署生产环境和开发环境下的URL。
+ // 默认情况下,Vue CLI 会假设你的应用是被部署在一个域名的根路径上
+ // 例如 https://www.ruoyi.vip/。如果应用被部署在一个子路径上,你就需要用这个选项指定这个子路径。例如,如果你的应用被部署在 https://www.ruoyi.vip/admin/,则设置 baseUrl 为 /admin/。
+ publicPath: process.env.NODE_ENV === 'production' ? '/' : '/',
+ // 在npm run build 或 yarn build 时 ,生成文件的目录名称(要和baseUrl的生产环境路径一致)(默认dist)
+ outputDir: 'dist',
+ // 用于放置生成的静态资源 (js、css、img、fonts) 的;(项目打包之后,静态资源会放在这个文件夹下)
+ assetsDir: 'static',
+ // 如果你不需要生产环境的 source map,可以将其设置为 false 以加速生产环境构建。
+ productionSourceMap: false,
+ transpileDependencies: [
+ 'quill',
+ 'markstream-vue2',
+ 'markstream-core',
+ '@terrastruct/d2',
+ 'stream-markdown-parser',
+ 'stream-markdown',
+ 'shiki-stream',
+ 'stream-monaco',
+ 'monaco-editor',
+ 'shiki',
+ '@shikijs/core',
+ '@shikijs/engine-javascript',
+ '@shikijs/engine-oniguruma',
+ '@shikijs/types',
+ '@shikijs/vscode-textmate',
+ '@shikijs/monaco',
+ '@shikijs/langs',
+ '@shikijs/themes',
+ 'oniguruma-to-es',
+ '@antv/infographic',
+ ],
+ // webpack-dev-server 相关配置
+ devServer: {
+ host: '0.0.0.0',
+ port: port,
+ open: true,
+ proxy: {
+ [`${pluginBase}/apps/`]: {
+ target: 'http://127.0.0.1:9099',
+ changeOrigin: false,
+ ws: true,
+ // Uvicorn 会补回 root_path,转发时只剥离一次外部部署前缀。
+ pathRewrite: (requestPath) => requestPath.slice(pluginBase.length),
+ onProxyRes(upstream, _request, response) {
+ const contentType = upstream.headers['content-type'] || ''
+ if (contentType.split(';')[0].trim().toLowerCase() !== 'text/event-stream') return
+ // 上游异常断开时结束下游,下游退出时也回收仍在读取的事件流。
+ upstream.once('aborted', () => response.destroy())
+ upstream.once('error', () => response.destroy())
+ response.once('close', () => {
+ if (!upstream.complete) upstream.destroy()
+ })
+ },
+ },
+ [`${pluginBase}/plugin/runtime/`]: {
+ target: 'http://127.0.0.1:9099',
+ changeOrigin: false,
+ pathRewrite: (requestPath) => requestPath.slice(pluginBase.length),
+ },
+ // 认证协议与交互 API 使用 issuer 同源根路径。
+ '/.well-known': { target: 'http://127.0.0.1:9099', changeOrigin: true },
+ '/oauth2': { target: 'http://127.0.0.1:9099', changeOrigin: true },
+ '/auth/interaction': { target: 'http://127.0.0.1:9099', changeOrigin: true },
+ // detail: https://cli.vuejs.org/config/#devserver-proxy
+ [process.env.VUE_APP_BASE_API]: {
+ target: `http://127.0.0.1:9099`,
+ changeOrigin: true,
+ pathRewrite: {
+ ['^' + process.env.VUE_APP_BASE_API]: '',
+ },
+ },
+ },
+ disableHostCheck: true,
+ },
+ css: {
+ loaderOptions: {
+ sass: {
+ sassOptions: { outputStyle: 'expanded' },
+ },
+ less: {
+ javascriptEnabled: true,
+ },
+ },
+ },
+ configureWebpack: {
+ name: name,
+ resolve: {
+ symlinks: false,
+ modules: ['node_modules', path.resolve(__dirname, '../node_modules')],
+ alias: {
+ '@': resolve('src'),
+ ...markstreamAlias,
+ },
+ },
+ plugins: [
+ new webpack.IgnorePlugin({
+ resourceRegExp: createOptionalIgnoreRegex(),
+ }),
+ new CopyWebpackPlugin([
+ {
+ from: path.resolve(__dirname, 'node_modules/mermaid/dist/mermaid.min.js'),
+ to: 'mermaid/mermaid.min.js',
+ },
+ {
+ from: path.resolve(__dirname, 'node_modules/katex/dist/katex.min.js'),
+ to: 'katex/katex.min.js',
+ },
+ {
+ from: path.resolve(__dirname, 'node_modules/katex/dist/contrib/mhchem.min.js'),
+ to: 'katex/contrib/mhchem.min.js',
+ },
+ ]),
+ ...createMonacoAssetCopyPlugins(),
+ // http://doc.ruoyi.vip/ruoyi-vue/other/faq.html#使用gzip解压缩静态文件
+ new CompressionPlugin({
+ cache: false, // 不启用文件缓存
+ test: /\.(js|css|html|jpe?g|png|gif|svg)?$/i, // 压缩文件格式
+ filename: '[path][base].gz[query]', // 压缩后的文件名
+ algorithm: 'gzip', // 使用gzip压缩
+ // threshold: 10240, // 只有大于 10kb 的文件会被压缩
+ minRatio: 0.8, // 压缩比例,小于 80% 的文件不会被压缩
+ deleteOriginalAssets: false, // 压缩后删除原文件
+ }),
+ ],
+ },
+ chainWebpack(config) {
+ config.plugins.delete('preload') // TODO: need test
+ config.plugins.delete('prefetch') // TODO: need test
+
+ // set svg-sprite-loader
+ config.module.rule('svg').exclude.add(resolve('src/assets/icons')).end()
+ config.module
+ .rule('icons')
+ .test(/\.svg$/)
+ .include.add(resolve('src/assets/icons'))
+ .end()
+ .use('svg-sprite-loader')
+ .loader('svg-sprite-loader')
+ .options({
+ symbolId: 'icon-[name]',
+ })
+ .end()
+
+ config.when(process.env.NODE_ENV !== 'development', (config) => {
+ // 保持 runtime 为外部脚本,使认证页面可使用严格的 script-src 'self'。
+ config.optimization.splitChunks({
+ chunks: 'all',
+ cacheGroups: {
+ libs: {
+ name: 'chunk-libs',
+ test: /[\\/]node_modules[\\/]/,
+ priority: 10,
+ chunks: 'initial', // only package third parties that are initially dependent
+ },
+ elementUI: {
+ name: 'chunk-elementUI', // split elementUI into a single package
+ test: /[\\/]node_modules[\\/]_?element-ui(.*)/, // in order to adapt to cnpm
+ priority: 20, // the weight needs to be larger than libs and app or it will be packaged into libs or app
+ },
+ commons: {
+ name: 'chunk-commons',
+ test: resolve('src/components'), // can customize your rules
+ minChunks: 3, // minimum common number
+ priority: 5,
+ reuseExistingChunk: true,
+ },
+ },
+ })
+ config.optimization.runtimeChunk('single')
+ })
+ },
+}
diff --git a/ruoyi-fastapi-app/.editorconfig b/ruoyi-fastapi-frontend/vue3/mobile/.editorconfig
similarity index 100%
rename from ruoyi-fastapi-app/.editorconfig
rename to ruoyi-fastapi-frontend/vue3/mobile/.editorconfig
diff --git a/ruoyi-fastapi-app/.gitignore b/ruoyi-fastapi-frontend/vue3/mobile/.gitignore
similarity index 100%
rename from ruoyi-fastapi-app/.gitignore
rename to ruoyi-fastapi-frontend/vue3/mobile/.gitignore
diff --git a/ruoyi-fastapi-app/.npmrc b/ruoyi-fastapi-frontend/vue3/mobile/.npmrc
similarity index 100%
rename from ruoyi-fastapi-app/.npmrc
rename to ruoyi-fastapi-frontend/vue3/mobile/.npmrc
diff --git a/ruoyi-fastapi-app/README.md b/ruoyi-fastapi-frontend/vue3/mobile/README.md
similarity index 89%
rename from ruoyi-fastapi-app/README.md
rename to ruoyi-fastapi-frontend/vue3/mobile/README.md
index 5f136f2..19de607 100644
--- a/ruoyi-fastapi-app/README.md
+++ b/ruoyi-fastapi-frontend/vue3/mobile/README.md
@@ -1,4 +1,4 @@
-# RuoYi-FastAPI-App
+# RuoYi-FastAPI Vue3 移动端
基于 `uni-app` 的 `vite` + `vue3` + `tailwindcss` 开发。
@@ -18,6 +18,18 @@
## 快速开始
+本工程位于 `ruoyi-fastapi-frontend/vue3/mobile`,由原 `ruoyi-fastapi-app` 迁入,与同仓库 Vue2/Vue3 Web 和 Vue2 移动端共用 `ruoyi-fastapi-backend`。从仓库根目录执行:
+
+```bash
+cd ruoyi-fastapi-frontend/vue3/mobile
+pnpm install
+pnpm dev:h5
+# 微信小程序
+pnpm dev:mp-weixin
+```
+
+后续命令均在本工程目录执行。
+
> [!IMPORTANT]
> 推荐使用 `"node": "^20.19.0 || >=22.12.0"` 的 Node.js 版本进行开发!
>
diff --git a/ruoyi-fastapi-app/index.html b/ruoyi-fastapi-frontend/vue3/mobile/index.html
similarity index 100%
rename from ruoyi-fastapi-app/index.html
rename to ruoyi-fastapi-frontend/vue3/mobile/index.html
diff --git a/ruoyi-fastapi-app/netlify.toml b/ruoyi-fastapi-frontend/vue3/mobile/netlify.toml
similarity index 100%
rename from ruoyi-fastapi-app/netlify.toml
rename to ruoyi-fastapi-frontend/vue3/mobile/netlify.toml
diff --git a/ruoyi-fastapi-app/package.json b/ruoyi-fastapi-frontend/vue3/mobile/package.json
similarity index 94%
rename from ruoyi-fastapi-app/package.json
rename to ruoyi-fastapi-frontend/vue3/mobile/package.json
index 76a38ec..468ddb4 100644
--- a/ruoyi-fastapi-app/package.json
+++ b/ruoyi-fastapi-frontend/vue3/mobile/package.json
@@ -4,7 +4,10 @@
"type": "module",
"packageManager": "pnpm@10.28.1",
"scripts": {
- "test:time": "node --test tests/time*.test.js",
+ "test:time": "npm run test:contract",
+ "test:unit": "node --experimental-vm-modules --test tests/behavior.test.mjs",
+ "test:contract": "node --test tests/time*.test.js",
+ "test": "npm run test:unit && npm run test:contract",
"dev": "uni -p mp-weixin",
"build": "npm run build:mp-weixin",
"dev:app": "uni -p app",
diff --git a/ruoyi-fastapi-app/platform.ts b/ruoyi-fastapi-frontend/vue3/mobile/platform.ts
similarity index 100%
rename from ruoyi-fastapi-app/platform.ts
rename to ruoyi-fastapi-frontend/vue3/mobile/platform.ts
diff --git a/ruoyi-fastapi-app/postcss.config.ts b/ruoyi-fastapi-frontend/vue3/mobile/postcss.config.ts
similarity index 100%
rename from ruoyi-fastapi-app/postcss.config.ts
rename to ruoyi-fastapi-frontend/vue3/mobile/postcss.config.ts
diff --git a/ruoyi-fastapi-app/src/App.vue b/ruoyi-fastapi-frontend/vue3/mobile/src/App.vue
similarity index 100%
rename from ruoyi-fastapi-app/src/App.vue
rename to ruoyi-fastapi-frontend/vue3/mobile/src/App.vue
diff --git a/ruoyi-fastapi-frontend/vue3/mobile/src/api/login.js b/ruoyi-fastapi-frontend/vue3/mobile/src/api/login.js
new file mode 100644
index 0000000..3733b15
--- /dev/null
+++ b/ruoyi-fastapi-frontend/vue3/mobile/src/api/login.js
@@ -0,0 +1,62 @@
+import request from "@/utils/request";
+
+// 登录方法
+export function login(username, password, code, uuid) {
+ const data = {
+ username,
+ password,
+ code,
+ uuid,
+ };
+ return request({
+ url: "/login",
+ header: {
+ "Content-Type": "application/x-www-form-urlencoded",
+ },
+ headers: {
+ isToken: false,
+ },
+ method: "post",
+ data: data,
+ });
+}
+
+// 注册方法
+export function register(data) {
+ return request({
+ url: "/register",
+ headers: {
+ isToken: false,
+ },
+ method: "post",
+ data: data,
+ });
+}
+
+// 获取用户详细信息
+export function getInfo() {
+ return request({
+ url: "/getInfo",
+ method: "get",
+ });
+}
+
+// 退出方法
+export function logout() {
+ return request({
+ url: "/logout",
+ method: "post",
+ });
+}
+
+// 获取验证码
+export function getCodeImg() {
+ return request({
+ url: "/captchaImage",
+ headers: {
+ isToken: false,
+ },
+ method: "get",
+ timeout: 20000,
+ });
+}
diff --git a/ruoyi-fastapi-frontend/vue3/mobile/src/api/system/dict/data.js b/ruoyi-fastapi-frontend/vue3/mobile/src/api/system/dict/data.js
new file mode 100644
index 0000000..3fd149f
--- /dev/null
+++ b/ruoyi-fastapi-frontend/vue3/mobile/src/api/system/dict/data.js
@@ -0,0 +1,52 @@
+import request from "@/utils/request";
+
+// 查询字典数据列表
+export function listData(query) {
+ return request({
+ url: "/system/dict/data/list",
+ method: "get",
+ params: query,
+ });
+}
+
+// 查询字典数据详细
+export function getData(dictCode) {
+ return request({
+ url: "/system/dict/data/" + dictCode,
+ method: "get",
+ });
+}
+
+// 根据字典类型查询字典数据信息
+export function getDicts(dictType) {
+ return request({
+ url: "/system/dict/data/type/" + dictType,
+ method: "get",
+ });
+}
+
+// 新增字典数据
+export function addData(data) {
+ return request({
+ url: "/system/dict/data",
+ method: "post",
+ data: data,
+ });
+}
+
+// 修改字典数据
+export function updateData(data) {
+ return request({
+ url: "/system/dict/data",
+ method: "put",
+ data: data,
+ });
+}
+
+// 删除字典数据
+export function delData(dictCode) {
+ return request({
+ url: "/system/dict/data/" + dictCode,
+ method: "delete",
+ });
+}
diff --git a/ruoyi-fastapi-frontend/vue3/mobile/src/api/system/dict/type.js b/ruoyi-fastapi-frontend/vue3/mobile/src/api/system/dict/type.js
new file mode 100644
index 0000000..a0c99f6
--- /dev/null
+++ b/ruoyi-fastapi-frontend/vue3/mobile/src/api/system/dict/type.js
@@ -0,0 +1,60 @@
+import request from "@/utils/request";
+
+// 查询字典类型列表
+export function listType(query) {
+ return request({
+ url: "/system/dict/type/list",
+ method: "get",
+ params: query,
+ });
+}
+
+// 查询字典类型详细
+export function getType(dictId) {
+ return request({
+ url: "/system/dict/type/" + dictId,
+ method: "get",
+ });
+}
+
+// 新增字典类型
+export function addType(data) {
+ return request({
+ url: "/system/dict/type",
+ method: "post",
+ data: data,
+ });
+}
+
+// 修改字典类型
+export function updateType(data) {
+ return request({
+ url: "/system/dict/type",
+ method: "put",
+ data: data,
+ });
+}
+
+// 删除字典类型
+export function delType(dictId) {
+ return request({
+ url: "/system/dict/type/" + dictId,
+ method: "delete",
+ });
+}
+
+// 刷新字典缓存
+export function refreshCache() {
+ return request({
+ url: "/system/dict/type/refreshCache",
+ method: "delete",
+ });
+}
+
+// 获取字典选择框列表
+export function optionselect() {
+ return request({
+ url: "/system/dict/type/optionselect",
+ method: "get",
+ });
+}
diff --git a/ruoyi-fastapi-frontend/vue3/mobile/src/api/system/user.js b/ruoyi-fastapi-frontend/vue3/mobile/src/api/system/user.js
new file mode 100644
index 0000000..ced9040
--- /dev/null
+++ b/ruoyi-fastapi-frontend/vue3/mobile/src/api/system/user.js
@@ -0,0 +1,55 @@
+import upload from "@/utils/upload";
+import request from "@/utils/request";
+
+// 用户密码重置
+export function updateUserPwd(oldPassword, newPassword) {
+ const data = {
+ oldPassword,
+ newPassword,
+ };
+ return request({
+ url: "/system/user/profile/updatePwd",
+ method: "put",
+ data: data,
+ });
+}
+
+// 查询用户个人信息
+export function getUserProfile() {
+ return request({
+ url: "/system/user/profile",
+ method: "get",
+ });
+}
+
+// 查询服务端支持的显示时区
+export function getTimezoneOptions() {
+ return request({ url: "/system/user/profile/timezones", method: "get" });
+}
+
+// 修改当前账号的显示时区
+export function updateUserTimezone(timeZone) {
+ return request({
+ url: "/system/user/profile/timezone",
+ method: "put",
+ data: { timeZone },
+ });
+}
+
+// 修改用户个人信息
+export function updateUserProfile(data) {
+ return request({
+ url: "/system/user/profile",
+ method: "put",
+ data: data,
+ });
+}
+
+// 用户头像上传
+export function uploadAvatar(data) {
+ return upload({
+ url: "/system/user/profile/avatar",
+ name: data.name,
+ filePath: data.filePath,
+ });
+}
diff --git a/ruoyi-fastapi-app/src/auto-imports.d.ts b/ruoyi-fastapi-frontend/vue3/mobile/src/auto-imports.d.ts
similarity index 100%
rename from ruoyi-fastapi-app/src/auto-imports.d.ts
rename to ruoyi-fastapi-frontend/vue3/mobile/src/auto-imports.d.ts
diff --git a/ruoyi-fastapi-app/src/config.js b/ruoyi-fastapi-frontend/vue3/mobile/src/config.js
similarity index 100%
rename from ruoyi-fastapi-app/src/config.js
rename to ruoyi-fastapi-frontend/vue3/mobile/src/config.js
diff --git a/ruoyi-fastapi-app/src/env.d.ts b/ruoyi-fastapi-frontend/vue3/mobile/src/env.d.ts
similarity index 100%
rename from ruoyi-fastapi-app/src/env.d.ts
rename to ruoyi-fastapi-frontend/vue3/mobile/src/env.d.ts
diff --git a/ruoyi-fastapi-app/src/main.ts b/ruoyi-fastapi-frontend/vue3/mobile/src/main.ts
similarity index 100%
rename from ruoyi-fastapi-app/src/main.ts
rename to ruoyi-fastapi-frontend/vue3/mobile/src/main.ts
diff --git a/ruoyi-fastapi-app/src/manifest.json b/ruoyi-fastapi-frontend/vue3/mobile/src/manifest.json
similarity index 100%
rename from ruoyi-fastapi-app/src/manifest.json
rename to ruoyi-fastapi-frontend/vue3/mobile/src/manifest.json
diff --git a/ruoyi-fastapi-frontend/vue3/mobile/src/pages.json b/ruoyi-fastapi-frontend/vue3/mobile/src/pages.json
new file mode 100644
index 0000000..6e4105e
--- /dev/null
+++ b/ruoyi-fastapi-frontend/vue3/mobile/src/pages.json
@@ -0,0 +1,138 @@
+{
+ "pages": [
+ {
+ "path": "pages/login",
+ "style": {
+ "navigationBarTitleText": "登录"
+ }
+ },
+ {
+ "path": "pages/register",
+ "style": {
+ "navigationBarTitleText": "注册"
+ }
+ },
+ {
+ "path": "pages/index",
+ "style": {
+ "navigationBarTitleText": "RuoYi-FastAPI移动端",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/work/index",
+ "style": {
+ "navigationBarTitleText": "工作台"
+ }
+ },
+ {
+ "path": "pages/mine/index",
+ "style": {
+ "navigationBarTitleText": "我的"
+ }
+ },
+ {
+ "path": "pages/mine/avatar/index",
+ "style": {
+ "navigationBarTitleText": "修改头像"
+ }
+ },
+ {
+ "path": "pages/mine/info/index",
+ "style": {
+ "navigationBarTitleText": "个人信息"
+ }
+ },
+ {
+ "path": "pages/mine/info/edit",
+ "style": {
+ "navigationBarTitleText": "编辑资料"
+ }
+ },
+ {
+ "path": "pages/mine/pwd/index",
+ "style": {
+ "navigationBarTitleText": "修改密码"
+ }
+ },
+ {
+ "path": "pages/mine/setting/index",
+ "style": {
+ "navigationBarTitleText": "应用设置"
+ }
+ },
+ {
+ "path": "pages/mine/setting/timezone",
+ "style": {
+ "navigationBarTitleText": "时区设置"
+ }
+ },
+ {
+ "path": "pages/mine/help/index",
+ "style": {
+ "navigationBarTitleText": "常见问题"
+ }
+ },
+ {
+ "path": "pages/mine/about/index",
+ "style": {
+ "navigationBarTitleText": "关于我们"
+ }
+ },
+ {
+ "path": "pages/common/agreement/index",
+ "style": {
+ "navigationBarTitleText": "用户协议"
+ }
+ },
+ {
+ "path": "pages/common/privacy/index",
+ "style": {
+ "navigationBarTitleText": "隐私协议"
+ }
+ },
+ {
+ "path": "pages/common/webview/index",
+ "style": {
+ "navigationBarTitleText": "浏览网页"
+ }
+ },
+ {
+ "path": "pages/common/textview/index",
+ "style": {
+ "navigationBarTitleText": "浏览文本"
+ }
+ }
+ ],
+ "tabBar": {
+ "color": "#000000",
+ "selectedColor": "#000000",
+ "borderStyle": "white",
+ "backgroundColor": "#ffffff",
+ "list": [
+ {
+ "pagePath": "pages/index",
+ "iconPath": "static/images/tabbar/home.png",
+ "selectedIconPath": "static/images/tabbar/home_.png",
+ "text": "首页"
+ },
+ {
+ "pagePath": "pages/work/index",
+ "iconPath": "static/images/tabbar/work.png",
+ "selectedIconPath": "static/images/tabbar/work_.png",
+ "text": "工作台"
+ },
+ {
+ "pagePath": "pages/mine/index",
+ "iconPath": "static/images/tabbar/mine.png",
+ "selectedIconPath": "static/images/tabbar/mine_.png",
+ "text": "我的"
+ }
+ ]
+ },
+ "globalStyle": {
+ "navigationBarTextStyle": "black",
+ "navigationBarTitleText": "RuoYi",
+ "navigationBarBackgroundColor": "#FFFFFF"
+ }
+}
diff --git a/ruoyi-fastapi-app/src/pages/common/agreement/index.vue b/ruoyi-fastapi-frontend/vue3/mobile/src/pages/common/agreement/index.vue
similarity index 100%
rename from ruoyi-fastapi-app/src/pages/common/agreement/index.vue
rename to ruoyi-fastapi-frontend/vue3/mobile/src/pages/common/agreement/index.vue
diff --git a/ruoyi-fastapi-app/src/pages/common/privacy/index.vue b/ruoyi-fastapi-frontend/vue3/mobile/src/pages/common/privacy/index.vue
similarity index 100%
rename from ruoyi-fastapi-app/src/pages/common/privacy/index.vue
rename to ruoyi-fastapi-frontend/vue3/mobile/src/pages/common/privacy/index.vue
diff --git a/ruoyi-fastapi-app/src/pages/common/textview/index.vue b/ruoyi-fastapi-frontend/vue3/mobile/src/pages/common/textview/index.vue
similarity index 100%
rename from ruoyi-fastapi-app/src/pages/common/textview/index.vue
rename to ruoyi-fastapi-frontend/vue3/mobile/src/pages/common/textview/index.vue
diff --git a/ruoyi-fastapi-app/src/pages/common/webview/index.vue b/ruoyi-fastapi-frontend/vue3/mobile/src/pages/common/webview/index.vue
similarity index 100%
rename from ruoyi-fastapi-app/src/pages/common/webview/index.vue
rename to ruoyi-fastapi-frontend/vue3/mobile/src/pages/common/webview/index.vue
diff --git a/ruoyi-fastapi-app/src/pages/index.vue b/ruoyi-fastapi-frontend/vue3/mobile/src/pages/index.vue
similarity index 100%
rename from ruoyi-fastapi-app/src/pages/index.vue
rename to ruoyi-fastapi-frontend/vue3/mobile/src/pages/index.vue
diff --git a/ruoyi-fastapi-app/src/pages/login.vue b/ruoyi-fastapi-frontend/vue3/mobile/src/pages/login.vue
similarity index 100%
rename from ruoyi-fastapi-app/src/pages/login.vue
rename to ruoyi-fastapi-frontend/vue3/mobile/src/pages/login.vue
diff --git a/ruoyi-fastapi-app/src/pages/mine/about/index.vue b/ruoyi-fastapi-frontend/vue3/mobile/src/pages/mine/about/index.vue
similarity index 100%
rename from ruoyi-fastapi-app/src/pages/mine/about/index.vue
rename to ruoyi-fastapi-frontend/vue3/mobile/src/pages/mine/about/index.vue
diff --git a/ruoyi-fastapi-app/src/pages/mine/avatar/index.vue b/ruoyi-fastapi-frontend/vue3/mobile/src/pages/mine/avatar/index.vue
similarity index 96%
rename from ruoyi-fastapi-app/src/pages/mine/avatar/index.vue
rename to ruoyi-fastapi-frontend/vue3/mobile/src/pages/mine/avatar/index.vue
index a3381f6..4451768 100644
--- a/ruoyi-fastapi-app/src/pages/mine/avatar/index.vue
+++ b/ruoyi-fastapi-frontend/vue3/mobile/src/pages/mine/avatar/index.vue
@@ -1,678 +1,678 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ruoyi-fastapi-app/src/pages/mine/help/index.vue b/ruoyi-fastapi-frontend/vue3/mobile/src/pages/mine/help/index.vue
similarity index 96%
rename from ruoyi-fastapi-app/src/pages/mine/help/index.vue
rename to ruoyi-fastapi-frontend/vue3/mobile/src/pages/mine/help/index.vue
index 30a740a..be8ff98 100644
--- a/ruoyi-fastapi-app/src/pages/mine/help/index.vue
+++ b/ruoyi-fastapi-frontend/vue3/mobile/src/pages/mine/help/index.vue
@@ -1,91 +1,91 @@
-
-
-
-
- {{ item.title }}
-
-
-
- {{ child.title }}
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+ {{ item.title }}
+
+
+
+ {{ child.title }}
+
+
+
+
+
+
+
+
+
+
diff --git a/ruoyi-fastapi-app/src/pages/mine/index.vue b/ruoyi-fastapi-frontend/vue3/mobile/src/pages/mine/index.vue
similarity index 97%
rename from ruoyi-fastapi-app/src/pages/mine/index.vue
rename to ruoyi-fastapi-frontend/vue3/mobile/src/pages/mine/index.vue
index 37ee0d4..cbbca90 100644
--- a/ruoyi-fastapi-app/src/pages/mine/index.vue
+++ b/ruoyi-fastapi-frontend/vue3/mobile/src/pages/mine/index.vue
@@ -1,209 +1,209 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ name }}
-
-
- 查看个人信息
-
-
-
-
- 点击登录
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 交流群
-
-
-
-
-
- 在线客服
-
-
-
-
-
- 反馈社区
-
-
-
-
-
- 点赞我们
-
-
-
-
-
-
-
-
- 编辑资料
-
-
-
-
-
-
-
- 常见问题
-
-
-
-
-
-
-
- 关于我们
-
-
-
-
-
-
-
- 应用设置
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ name }}
+
+
+ 查看个人信息
+
+
+
+
+ 点击登录
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 交流群
+
+
+
+
+
+ 在线客服
+
+
+
+
+
+ 反馈社区
+
+
+
+
+
+ 点赞我们
+
+
+
+
+
+
+
+
+ 编辑资料
+
+
+
+
+
+
+
+ 常见问题
+
+
+
+
+
+
+
+ 关于我们
+
+
+
+
+
+
+
+ 应用设置
+
+
+
+
+
+
+
+
+
diff --git a/ruoyi-fastapi-app/src/pages/mine/info/edit.vue b/ruoyi-fastapi-frontend/vue3/mobile/src/pages/mine/info/edit.vue
similarity index 100%
rename from ruoyi-fastapi-app/src/pages/mine/info/edit.vue
rename to ruoyi-fastapi-frontend/vue3/mobile/src/pages/mine/info/edit.vue
diff --git a/ruoyi-fastapi-app/src/pages/mine/info/index.vue b/ruoyi-fastapi-frontend/vue3/mobile/src/pages/mine/info/index.vue
similarity index 100%
rename from ruoyi-fastapi-app/src/pages/mine/info/index.vue
rename to ruoyi-fastapi-frontend/vue3/mobile/src/pages/mine/info/index.vue
diff --git a/ruoyi-fastapi-app/src/pages/mine/pwd/index.vue b/ruoyi-fastapi-frontend/vue3/mobile/src/pages/mine/pwd/index.vue
similarity index 100%
rename from ruoyi-fastapi-app/src/pages/mine/pwd/index.vue
rename to ruoyi-fastapi-frontend/vue3/mobile/src/pages/mine/pwd/index.vue
diff --git a/ruoyi-fastapi-app/src/pages/mine/setting/index.vue b/ruoyi-fastapi-frontend/vue3/mobile/src/pages/mine/setting/index.vue
similarity index 100%
rename from ruoyi-fastapi-app/src/pages/mine/setting/index.vue
rename to ruoyi-fastapi-frontend/vue3/mobile/src/pages/mine/setting/index.vue
diff --git a/ruoyi-fastapi-app/src/pages/mine/setting/timezone.vue b/ruoyi-fastapi-frontend/vue3/mobile/src/pages/mine/setting/timezone.vue
similarity index 100%
rename from ruoyi-fastapi-app/src/pages/mine/setting/timezone.vue
rename to ruoyi-fastapi-frontend/vue3/mobile/src/pages/mine/setting/timezone.vue
diff --git a/ruoyi-fastapi-app/src/pages/register.vue b/ruoyi-fastapi-frontend/vue3/mobile/src/pages/register.vue
similarity index 100%
rename from ruoyi-fastapi-app/src/pages/register.vue
rename to ruoyi-fastapi-frontend/vue3/mobile/src/pages/register.vue
diff --git a/ruoyi-fastapi-app/src/pages/work/index.vue b/ruoyi-fastapi-frontend/vue3/mobile/src/pages/work/index.vue
similarity index 100%
rename from ruoyi-fastapi-app/src/pages/work/index.vue
rename to ruoyi-fastapi-frontend/vue3/mobile/src/pages/work/index.vue
diff --git a/ruoyi-fastapi-frontend/vue3/mobile/src/permission.js b/ruoyi-fastapi-frontend/vue3/mobile/src/permission.js
new file mode 100644
index 0000000..31152ce
--- /dev/null
+++ b/ruoyi-fastapi-frontend/vue3/mobile/src/permission.js
@@ -0,0 +1,44 @@
+import { getToken } from "@/utils/auth";
+
+// 登录页面
+const loginPage = "/pages/login";
+
+// 页面白名单
+const whiteList = [
+ "/pages/login",
+ "/pages/register",
+ "/pages/common/webview/index",
+ "/pages/common/agreement/index",
+ "/pages/common/privacy/index",
+];
+
+// 检查地址白名单
+function checkWhite(url) {
+ const path = url.split("?")[0];
+ return whiteList.indexOf(path) !== -1;
+}
+
+// 页面跳转验证拦截器
+let list = ["navigateTo", "redirectTo", "reLaunch", "switchTab"];
+list.forEach((item) => {
+ uni.addInterceptor(item, {
+ invoke(to) {
+ if (getToken()) {
+ if (to.url.split("?")[0] === loginPage) {
+ uni.reLaunch({ url: "/" });
+ return false;
+ }
+ return true;
+ } else {
+ if (checkWhite(to.url)) {
+ return true;
+ }
+ uni.reLaunch({ url: loginPage });
+ return false;
+ }
+ },
+ fail(err) {
+ console.log(err);
+ },
+ });
+});
diff --git a/ruoyi-fastapi-app/src/plugins/auth.js b/ruoyi-fastapi-frontend/vue3/mobile/src/plugins/auth.js
similarity index 96%
rename from ruoyi-fastapi-app/src/plugins/auth.js
rename to ruoyi-fastapi-frontend/vue3/mobile/src/plugins/auth.js
index c028502..ba7b6d5 100644
--- a/ruoyi-fastapi-app/src/plugins/auth.js
+++ b/ruoyi-fastapi-frontend/vue3/mobile/src/plugins/auth.js
@@ -1,60 +1,60 @@
-import { useUserStore } from "@/store";
-
-function authPermission(permission) {
- const all_permission = "*:*:*";
- const permissions = useUserStore().permissions;
- if (permission && permission.length > 0) {
- return permissions.some((v) => {
- return all_permission === v || v === permission;
- });
- } else {
- return false;
- }
-}
-
-function authRole(role) {
- const super_admin = "admin";
- const roles = useUserStore().roles;
- if (role && role.length > 0) {
- return roles.some((v) => {
- return super_admin === v || v === role;
- });
- } else {
- return false;
- }
-}
-
-export default {
- // 验证用户是否具备某权限
- hasPermi(permission) {
- return authPermission(permission);
- },
- // 验证用户是否含有指定权限,只需包含其中一个
- hasPermiOr(permissions) {
- return permissions.some((item) => {
- return authPermission(item);
- });
- },
- // 验证用户是否含有指定权限,必须全部拥有
- hasPermiAnd(permissions) {
- return permissions.every((item) => {
- return authPermission(item);
- });
- },
- // 验证用户是否具备某角色
- hasRole(role) {
- return authRole(role);
- },
- // 验证用户是否含有指定角色,只需包含其中一个
- hasRoleOr(roles) {
- return roles.some((item) => {
- return authRole(item);
- });
- },
- // 验证用户是否含有指定角色,必须全部拥有
- hasRoleAnd(roles) {
- return roles.every((item) => {
- return authRole(item);
- });
- },
-};
+import { useUserStore } from "@/store";
+
+function authPermission(permission) {
+ const all_permission = "*:*:*";
+ const permissions = useUserStore().permissions;
+ if (permission && permission.length > 0) {
+ return permissions.some((v) => {
+ return all_permission === v || v === permission;
+ });
+ } else {
+ return false;
+ }
+}
+
+function authRole(role) {
+ const super_admin = "admin";
+ const roles = useUserStore().roles;
+ if (role && role.length > 0) {
+ return roles.some((v) => {
+ return super_admin === v || v === role;
+ });
+ } else {
+ return false;
+ }
+}
+
+export default {
+ // 验证用户是否具备某权限
+ hasPermi(permission) {
+ return authPermission(permission);
+ },
+ // 验证用户是否含有指定权限,只需包含其中一个
+ hasPermiOr(permissions) {
+ return permissions.some((item) => {
+ return authPermission(item);
+ });
+ },
+ // 验证用户是否含有指定权限,必须全部拥有
+ hasPermiAnd(permissions) {
+ return permissions.every((item) => {
+ return authPermission(item);
+ });
+ },
+ // 验证用户是否具备某角色
+ hasRole(role) {
+ return authRole(role);
+ },
+ // 验证用户是否含有指定角色,只需包含其中一个
+ hasRoleOr(roles) {
+ return roles.some((item) => {
+ return authRole(item);
+ });
+ },
+ // 验证用户是否含有指定角色,必须全部拥有
+ hasRoleAnd(roles) {
+ return roles.every((item) => {
+ return authRole(item);
+ });
+ },
+};
diff --git a/ruoyi-fastapi-app/src/plugins/index.js b/ruoyi-fastapi-frontend/vue3/mobile/src/plugins/index.js
similarity index 96%
rename from ruoyi-fastapi-app/src/plugins/index.js
rename to ruoyi-fastapi-frontend/vue3/mobile/src/plugins/index.js
index c9883ab..886e74a 100644
--- a/ruoyi-fastapi-app/src/plugins/index.js
+++ b/ruoyi-fastapi-frontend/vue3/mobile/src/plugins/index.js
@@ -1,12 +1,12 @@
-import tab from "./tab";
-import auth from "./auth";
-import modal from "./modal";
-
-export function install(app) {
- // 页签操作
- app.config.globalProperties.$tab = tab;
- // 认证对象
- app.config.globalProperties.$auth = auth;
- // 模态框对象
- app.config.globalProperties.$modal = modal;
-}
+import tab from "./tab";
+import auth from "./auth";
+import modal from "./modal";
+
+export function install(app) {
+ // 页签操作
+ app.config.globalProperties.$tab = tab;
+ // 认证对象
+ app.config.globalProperties.$auth = auth;
+ // 模态框对象
+ app.config.globalProperties.$modal = modal;
+}
diff --git a/ruoyi-fastapi-frontend/vue3/mobile/src/plugins/modal.js b/ruoyi-fastapi-frontend/vue3/mobile/src/plugins/modal.js
new file mode 100644
index 0000000..175d11c
--- /dev/null
+++ b/ruoyi-fastapi-frontend/vue3/mobile/src/plugins/modal.js
@@ -0,0 +1,78 @@
+export default {
+ // 消息提示
+ msg(content) {
+ uni.showToast({
+ title: content,
+ icon: "none",
+ });
+ },
+ // 错误消息
+ msgError(content) {
+ uni.showToast({
+ title: content,
+ icon: "error",
+ });
+ },
+ // 成功消息
+ msgSuccess(content) {
+ uni.showToast({
+ title: content,
+ icon: "success",
+ });
+ },
+ // 隐藏消息
+ hideMsg(content) {
+ uni.hideToast();
+ },
+ // 弹出提示
+ alert(content, title) {
+ uni.showModal({
+ title: title || "系统提示",
+ content: content,
+ showCancel: false,
+ });
+ },
+ // 确认窗体
+ confirm(content, title) {
+ return new Promise((resolve, reject) => {
+ uni.showModal({
+ title: title || "系统提示",
+ content: content,
+ cancelText: "取消",
+ confirmText: "确定",
+ success: function (res) {
+ if (res.confirm) {
+ resolve(res.confirm);
+ }
+ },
+ });
+ });
+ },
+ // 提示信息
+ showToast(option) {
+ if (typeof option === "object") {
+ uni.showToast(option);
+ } else {
+ uni.showToast({
+ title: option,
+ icon: "none",
+ duration: 2500,
+ });
+ }
+ },
+ // 打开遮罩层
+ loading(content) {
+ uni.showLoading({
+ title: content,
+ icon: "none",
+ });
+ },
+ // 关闭遮罩层
+ closeLoading() {
+ try {
+ uni.hideLoading();
+ } catch (e) {
+ console.log(e);
+ }
+ },
+};
diff --git a/ruoyi-fastapi-frontend/vue3/mobile/src/plugins/tab.js b/ruoyi-fastapi-frontend/vue3/mobile/src/plugins/tab.js
new file mode 100644
index 0000000..c68475b
--- /dev/null
+++ b/ruoyi-fastapi-frontend/vue3/mobile/src/plugins/tab.js
@@ -0,0 +1,30 @@
+export default {
+ // 关闭所有页面,打开到应用内的某个页面
+ reLaunch(url) {
+ return uni.reLaunch({
+ url: url,
+ });
+ },
+ // 跳转到tabBar页面,并关闭其他所有非tabBar页面
+ switchTab(url) {
+ return uni.switchTab({
+ url: url,
+ });
+ },
+ // 关闭当前页面,跳转到应用内的某个页面
+ redirectTo(url) {
+ return uni.redirectTo({
+ url: url,
+ });
+ },
+ // 保留当前页面,跳转到应用内的某个页面
+ navigateTo(url) {
+ return uni.navigateTo({
+ url: url,
+ });
+ },
+ // 关闭当前页面,返回上一页面或多级页面
+ navigateBack() {
+ return uni.navigateBack();
+ },
+};
diff --git a/ruoyi-fastapi-frontend/vue3/mobile/src/static/favicon.ico b/ruoyi-fastapi-frontend/vue3/mobile/src/static/favicon.ico
new file mode 100644
index 0000000000000000000000000000000000000000..b75826f070d8a557899a4eedfa55ab92730b2905
GIT binary patch
literal 10729
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
z
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
KVCMguIr~4ac{q~*
literal 0
HcmV?d00001
diff --git a/ruoyi-fastapi-frontend/vue3/mobile/src/static/images/banner/banner01.jpg b/ruoyi-fastapi-frontend/vue3/mobile/src/static/images/banner/banner01.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..c44d84c908b2bcb048e4dbe438d1f3ff48058f01
GIT binary patch
literal 39640
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