From e634118a222f6c5526aaa32ece939155dc225a4d Mon Sep 17 00:00:00 2001 From: Anyon Date: Fri, 8 May 2026 15:30:46 +0800 Subject: [PATCH] =?UTF-8?q?refactor(plugin):=20=E8=BF=81=E7=A7=BB=20v8=20?= =?UTF-8?q?=E6=8F=92=E4=BB=B6=E5=8C=96=E7=BB=84=E4=BB=B6=E4=BD=93=E7=B3=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将 v6 中直接放在本地 app 的后台与微信能力迁移为 v8 插件组件,并把运行时基础能力沉淀到独立插件包。 主要内容: - 新增 think-library、system、worker、static、install 等基础插件包。 - 新增 account、payment、wechat-client、wechat-service、wemall、wuma 等业务插件包。 - 移除 v6 的 app/admin 与 app/wechat 本地应用实现,改由插件分发接管。 - 将 Helper 能力彻底并入 System,统一为 plugin\system\helper\* 命名空间。 - 同步插件迁移发布清单与根 route 占位,保证安装发布流程可复现。 --- app/admin/Service.php | 69 - app/admin/controller/Auth.php | 141 - app/admin/controller/Base.php | 116 - app/admin/controller/Config.php | 157 - app/admin/controller/File.php | 118 - app/admin/controller/Index.php | 161 - app/admin/controller/Login.php | 137 - app/admin/controller/Menu.php | 189 - app/admin/controller/Queue.php | 126 - app/admin/controller/User.php | 186 - app/admin/controller/api/Queue.php | 125 - app/admin/lang/en-us.php | 396 - app/admin/route/demo.php | 55 - app/admin/view/api/icon.html | 134 - app/admin/view/api/upload/image.html | 161 - app/admin/view/auth/form.html | 143 - app/admin/view/auth/index.html | 76 - app/admin/view/base/form.html | 68 - app/admin/view/base/index.html | 86 - app/admin/view/base/index_search.html | 42 - app/admin/view/config/index.html | 234 - app/admin/view/config/storage-0.html | 49 - app/admin/view/config/storage-alioss.html | 98 - app/admin/view/config/storage-alist.html | 81 - app/admin/view/config/storage-local.html | 51 - app/admin/view/config/storage-qiniu.html | 97 - app/admin/view/config/storage-txcos.html | 96 - app/admin/view/config/storage-upyun.html | 81 - app/admin/view/config/system.html | 129 - app/admin/view/file/form.html | 40 - app/admin/view/file/index.html | 64 - app/admin/view/file/index_search.html | 58 - app/admin/view/full.html | 35 - app/admin/view/index/index-top.html | 44 - app/admin/view/index/theme.html | 36 - app/admin/view/login/index.html | 57 - app/admin/view/menu/form.html | 97 - app/admin/view/menu/index.html | 119 - app/admin/view/oplog/index.html | 47 - app/admin/view/oplog/index_search.html | 87 - app/admin/view/queue/index.html | 131 - app/admin/view/queue/index_search.html | 45 - app/admin/view/user/form.html | 114 - app/admin/view/user/index_search.html | 44 - app/admin/view/user/pass.html | 40 - app/wechat/Service.php | 109 - app/wechat/model/WechatAuto.php | 59 - app/wechat/model/WechatKeys.php | 59 - app/wechat/model/WechatNews.php | 37 - app/wechat/model/WechatPaymentRecord.php | 110 - app/wechat/model/WechatPaymentRefund.php | 81 - app/wechat/service/WechatService.php | 367 - app/wechat/view/config/options.html | 53 - app/wechat/view/config/options_test.html | 21 - app/wechat/view/config/payment_test.html | 40 - database/migrations/.published.json | 34 + .../.github/workflows/release.yml | 75 + plugin/think-library/.gitignore | 8 + plugin/think-library/.php-cs-fixer.php | 120 + plugin/think-library/composer.json | 60 + plugin/think-library/license | 21 + plugin/think-library/phpunit.xml.dist | 19 + plugin/think-library/readme.md | 386 + plugin/think-library/src/Builder.php | 71 + plugin/think-library/src/Command.php | 112 + plugin/think-library/src/Controller.php | 348 + plugin/think-library/src/Exception.php | 63 + plugin/think-library/src/Helper.php | 82 + plugin/think-library/src/Library.php | 158 + plugin/think-library/src/Model.php | 138 + plugin/think-library/src/Plugin.php | 423 + plugin/think-library/src/Service.php | 60 + plugin/think-library/src/Storage.php | 235 + .../think-library/src/builder/BuilderLang.php | 129 + .../src/builder/base/BuilderAttributeBag.php | 174 + .../src/builder/base/BuilderModule.php | 118 + .../src/builder/base/BuilderNode.php | 429 + .../src/builder/base/BuilderOptionSource.php | 106 + .../base/render/BuilderActionRenderer.php | 29 + .../builder/base/render/BuilderAttributes.php | 165 + .../render/BuilderAttributesRenderContext.php | 38 + .../base/render/BuilderAttributesRenderer.php | 20 + .../render/BuilderCallbackNodeRenderer.php | 17 + .../render/BuilderElementNodeRenderer.php | 32 + .../base/render/BuilderHtmlNodeRenderer.php | 22 + .../base/render/BuilderNodeRenderContext.php | 32 + .../render/BuilderNodeRendererFactory.php | 40 + .../base/render/BuilderRenderPipeline.php | 41 + .../base/render/BuilderRenderState.php | 40 + .../base/render/InlineScriptRenderer.php | 28 + .../base/render/JsonScriptRenderer.php | 21 + .../src/builder/form/FormActionBar.php | 18 + .../src/builder/form/FormActions.php | 40 + .../src/builder/form/FormBlocks.php | 114 + .../src/builder/form/FormBuilder.php | 1874 ++ .../src/builder/form/FormButton.php | 33 + .../src/builder/form/FormChoiceField.php | 17 + .../src/builder/form/FormComponents.php | 52 + .../src/builder/form/FormField.php | 472 + .../src/builder/form/FormFieldOptions.php | 22 + .../src/builder/form/FormFieldPart.php | 185 + .../src/builder/form/FormFields.php | 131 + .../src/builder/form/FormLayout.php | 174 + .../src/builder/form/FormNode.php | 201 + .../src/builder/form/FormSelectField.php | 22 + .../src/builder/form/FormTextField.php | 60 + .../src/builder/form/FormUploadConfig.php | 238 + .../src/builder/form/FormUploadField.php | 84 + .../form/component/AbstractFormComponent.php | 47 + .../form/component/FormComponentInterface.php | 16 + .../builder/form/component/IntroComponent.php | 65 + .../builder/form/component/NoteComponent.php | 34 + .../form/component/PickerFieldComponent.php | 77 + .../form/component/ReadonlyFieldComponent.php | 65 + .../form/component/SectionComponent.php | 79 + .../form/component/ThemePaletteComponent.php | 138 + .../src/builder/form/module/FormModules.php | 87 + .../form/render/AbstractFormFieldRenderer.php | 36 + .../form/render/ChoiceFieldRenderer.php | 34 + .../form/render/FormActionBarRenderer.php | 30 + .../form/render/FormButtonNodeRenderer.php | 19 + .../form/render/FormElementNodeRenderer.php | 19 + .../form/render/FormFieldNodeRenderer.php | 26 + .../form/render/FormFieldRenderContext.php | 230 + .../form/render/FormFieldRendererFactory.php | 25 + .../render/FormFieldRendererInterface.php | 14 + .../form/render/FormHtmlNodeRenderer.php | 19 + .../form/render/FormNodeRenderContext.php | 57 + .../form/render/FormNodeRendererFactory.php | 39 + .../form/render/FormNodeRendererInterface.php | 17 + .../form/render/FormOptionRenderer.php | 181 + .../form/render/FormRenderPipeline.php | 61 + .../builder/form/render/FormRenderState.php | 40 + .../builder/form/render/FormShellRenderer.php | 119 + .../form/render/FormUploadRuntimeRenderer.php | 104 + .../form/render/SelectFieldRenderer.php | 27 + .../builder/form/render/TextFieldRenderer.php | 39 + .../form/render/UploadFieldRenderer.php | 77 + .../src/builder/page/PageAction.php | 202 + .../src/builder/page/PageActionNormalizer.php | 170 + .../src/builder/page/PageBlocks.php | 99 + .../src/builder/page/PageBuilder.php | 2041 ++ .../src/builder/page/PageButtons.php | 166 + .../src/builder/page/PageColumn.php | 163 + .../src/builder/page/PageColumnNormalizer.php | 181 + .../src/builder/page/PageComponents.php | 64 + .../src/builder/page/PageLayout.php | 124 + .../src/builder/page/PageNode.php | 232 + .../src/builder/page/PagePresetColumn.php | 161 + .../src/builder/page/PageRowActions.php | 159 + .../src/builder/page/PageSearch.php | 218 + .../src/builder/page/PageSearchField.php | 200 + .../page/PageSearchFieldNormalizer.php | 114 + .../src/builder/page/PageSearchOptions.php | 22 + .../src/builder/page/PageSortInputColumn.php | 73 + .../builder/page/PageStatusSwitchColumn.php | 110 + .../src/builder/page/PageTable.php | 518 + .../src/builder/page/PageTableNormalizer.php | 28 + .../src/builder/page/PageTableOptions.php | 179 + .../src/builder/page/PageToolbarColumn.php | 27 + .../page/component/AbstractPageComponent.php | 47 + .../page/component/ButtonGroupComponent.php | 83 + .../builder/page/component/CardComponent.php | 102 + .../page/component/KeyValueTableComponent.php | 69 + .../page/component/KvGridComponent.php | 65 + .../page/component/PageComponentInterface.php | 16 + .../page/component/ParagraphsComponent.php | 63 + .../component/ReadonlyFieldsComponent.php | 75 + .../src/builder/page/module/PageModules.php | 277 + .../AbstractPageSearchFieldRenderer.php | 27 + .../page/render/PageBootScriptRenderer.php | 35 + .../page/render/PageContentRenderer.php | 39 + .../page/render/PageCustomScriptRenderer.php | 22 + .../page/render/PageElementNodeRenderer.php | 19 + .../page/render/PageHeaderRenderer.php | 34 + .../page/render/PageHtmlNodeRenderer.php | 19 + .../page/render/PageInitScriptRenderer.php | 35 + .../page/render/PageNodeRenderContext.php | 39 + .../page/render/PageNodeRendererFactory.php | 38 + .../page/render/PageNodeRendererInterface.php | 17 + .../page/render/PageNoticeRenderer.php | 27 + .../page/render/PageReadyScriptRenderer.php | 22 + .../page/render/PageRenderPipeline.php | 100 + .../builder/page/render/PageRenderState.php | 58 + .../page/render/PageScriptRenderContext.php | 25 + .../page/render/PageScriptRenderer.php | 24 + .../render/PageSearchFieldRendererFactory.php | 25 + .../PageSearchFieldRendererInterface.php | 17 + .../render/PageSearchHiddenFieldRenderer.php | 22 + .../render/PageSearchInputFieldRenderer.php | 34 + .../page/render/PageSearchNodeRenderer.php | 19 + .../page/render/PageSearchRenderContext.php | 43 + .../page/render/PageSearchRenderer.php | 88 + .../render/PageSearchSelectFieldRenderer.php | 48 + .../render/PageSearchSubmitFieldRenderer.php | 26 + .../builder/page/render/PageShellRenderer.php | 46 + .../render/PageTableInitScriptRenderer.php | 20 + .../page/render/PageTableNodeRenderer.php | 19 + .../page/render/PageTableRenderContext.php | 24 + .../builder/page/render/PageTableRenderer.php | 20 + .../page/render/PageTemplateRenderer.php | 25 + .../render/PageTemplateScriptRenderer.php | 23 + .../render/PageToolbarTemplateRenderer.php | 25 + plugin/think-library/src/common.php | 569 + .../src/contract/QueueHandlerInterface.php | 32 + .../src/contract/QueueManagerInterface.php | 37 + .../src/contract/QueueRuntimeInterface.php | 45 + .../src/contract/StorageInterface.php | 91 + .../src/contract/StorageManagerInterface.php | 40 + .../src/contract/SystemContextInterface.php | 56 + plugin/think-library/src/extend/ArrayTree.php | 84 + .../think-library/src/extend/CodeExtend.php | 28 + .../think-library/src/extend/CodeToolkit.php | 181 + .../think-library/src/extend/DataExtend.php | 28 + plugin/think-library/src/extend/FileTools.php | 149 + .../think-library/src/extend/HttpClient.php | 176 + .../src/extend/JsonRpcClient.php | 41 + .../think-library/src/helper/DeleteHelper.php | 105 + .../think-library/src/helper/FormHelper.php | 89 + .../think-library/src/helper/QueryHelper.php | 571 + .../think-library/src/helper/SaveHelper.php | 85 + .../src/helper/ValidateHelper.php | 88 + .../src/middleware/MultAccess.php | 250 + .../think-library/src/model/ModelFactory.php | 50 + .../think-library/src/model/QueryFactory.php | 81 + .../think-library/src/model/RuntimeModel.php | 57 + .../think-library/src/model/SystemConfig.php | 19 + plugin/think-library/src/route/Route.php | 343 + plugin/think-library/src/route/Url.php | 532 + .../src/runtime/NullSystemContext.php | 113 + .../src/runtime/RequestContext.php | 415 + .../src/runtime/RequestTokenService.php | 278 + .../src/runtime/SystemContext.php | 55 + .../think-library/src/service/AppService.php | 1036 + .../src/service/AuthResponse.php | 126 + .../src/service/CacheSession.php | 465 + .../src/service/FaviconBuilder.php | 208 + .../src/service/ImageSliderVerify.php | 338 + .../src/service/JsonRpcHttpClient.php | 91 + .../src/service/JsonRpcHttpServer.php | 158 + plugin/think-library/src/service/JwtToken.php | 226 + .../src/service/ModuleService.php | 21 + .../think-library/src/service/NodeService.php | 161 + .../src/service/QueueService.php | 85 + .../src/service/ResponseModeService.php | 116 + .../src/service/RuntimeService.php | 238 + plugin/think-library/tests/AppServiceTest.php | 107 + .../tests/ArchitectureBoundaryTest.php | 442 + plugin/think-library/tests/CodeTest.php | 44 + .../tests/CommonFunctionsTest.php | 169 + .../tests/ComposerDependencyBoundaryTest.php | 281 + .../tests/ComposerInstallBoundaryTest.php | 398 + .../think-library/tests/FormBuilderTest.php | 861 + plugin/think-library/tests/JwtTest.php | 40 + .../tests/MigrationOwnershipTest.php | 160 + plugin/think-library/tests/ModelTest.php | 41 + .../tests/MultAccessDispatchTest.php | 293 + .../think-library/tests/PageBuilderTest.php | 973 + .../think-library/tests/PasswordMaskTest.php | 51 + .../tests/RequestTokenServiceTest.php | 53 + .../tests/RouteTemplateBoundaryTest.php | 222 + .../tests/SoftDeleteBoundaryTest.php | 90 + plugin/think-library/tests/bootstrap.php | 39 + plugin/think-plugs-account/.gitattributes | 3 + .../.github/workflows/release.yml | 75 + plugin/think-plugs-account/.gitignore | 12 + plugin/think-plugs-account/.php-cs-fixer.php | 120 + plugin/think-plugs-account/composer.json | 85 + plugin/think-plugs-account/phpunit.xml.dist | 24 + plugin/think-plugs-account/readme.api.md | 235 + plugin/think-plugs-account/readme.md | 15 + plugin/think-plugs-account/src/Service.php | 29 + .../src/controller/Device.php | 182 + .../src/controller/Master.php | 75 + .../src/controller/Message.php | 201 + .../src/controller/api/Auth.php | 115 + .../src/controller/api/Login.php | 286 + .../src/controller/api/Wechat.php | 130 + .../src/controller/api/Wxapp.php | 224 + .../src/controller/api/auth/Center.php | 157 + plugin/think-plugs-account/src/lang/en-us.php | 232 + plugin/think-plugs-account/src/model/Abs.php | 32 + .../src/model/PlainAbs.php | 36 + .../src/model/PluginAccountAuth.php | 50 + .../src/model/PluginAccountBind.php | 79 + .../src/model/PluginAccountMsms.php | 55 + .../src/model/PluginAccountUser.php | 59 + .../src/service/Account.php | 423 + .../src/service/Message.php | 178 + .../src/service/WxappService.php | 147 + .../src/service/contract/AccountAccess.php | 523 + .../src/service/contract/AccountInterface.php | 142 + .../src/service/contract/MessageInterface.php | 63 + .../service/contract/MessageUsageTrait.php | 75 + .../src/service/message/Alisms.php | 152 + .../src/view/device/index.html | 80 + .../src/view/device/index_search.html | 94 + .../src/view/device/types.html | 22 + .../src/view/master/index.html | 70 + .../src/view/master/index_search.html | 71 + .../src/view/message/index.html | 43 + .../src/view/message/index_search.html | 68 + .../think-plugs-account/src}/view/table.html | 2 +- ...20241010000005_install_account20241010.php | 169 + .../tests/AccountAdminListControllerTest.php | 402 + .../tests/AccountCenterControllerTest.php | 198 + .../tests/AccountIntegrationTest.php | 162 + .../tests/AccountLoginControllerTest.php | 157 + .../tests/AccountMessageControllerTest.php | 235 + .../tests/AccountRuntimeTest.php | 267 + .../think-plugs-account/tests/bootstrap.php | 39 + plugin/think-plugs-builder/composer.json | 42 + plugin/think-plugs-builder/readme.api.md | 30 + plugin/think-plugs-builder/readme.md | 14 + plugin/think-plugs-builder/src/Service.php | 36 + .../think-plugs-builder/src/command/Build.php | 69 + .../src/service/PharBuilder.php | 534 + .../src/service/PharRuntime.php | 196 + plugin/think-plugs-install/composer.json | 55 + plugin/think-plugs-install/readme.api.md | 34 + plugin/think-plugs-install/readme.md | 19 + plugin/think-plugs-install/src/Service.php | 34 + .../src/command/project/InstallCommand.php | 71 + .../src/composer/Plugin.php | 87 + .../src/service/ComposerLifecycleService.php | 516 + .../tests/ComposerLifecycleServiceTest.php | 334 + .../tests/InstallCommandTest.php | 133 + .../think-plugs-install/tests/bootstrap.php | 31 + plugin/think-plugs-payment/.gitattributes | 3 + .../.github/workflows/release.yml | 75 + plugin/think-plugs-payment/.gitignore | 12 + plugin/think-plugs-payment/.php-cs-fixer.php | 120 + plugin/think-plugs-payment/composer.json | 112 + plugin/think-plugs-payment/phpunit.xml.dist | 24 + plugin/think-plugs-payment/readme.api.md | 83 + plugin/think-plugs-payment/readme.md | 15 + plugin/think-plugs-payment/src/Service.php | 49 + .../src/controller/Balance.php | 140 + .../src/controller/Config.php | 207 + .../src/controller/Integral.php | 140 + .../src/controller/Record.php | 223 + .../src/controller/Refund.php | 77 + .../src/controller/api/auth/Address.php | 181 + .../src/controller/api/auth/Balance.php | 57 + .../src/controller/api/auth/Integral.php | 57 + plugin/think-plugs-payment/src/lang/en-us.php | 364 + .../src/model/PluginPaymentAddress.php | 59 + .../src/model/PluginPaymentBalance.php | 58 + .../src/model/PluginPaymentConfig.php | 79 + .../src/model/PluginPaymentIntegral.php | 33 + .../src/model/PluginPaymentRecord.php | 64 + .../src/model/PluginPaymentRefund.php | 38 + .../src/service/Balance.php | 197 + .../src/service/Integral.php | 208 + .../src/service/Payment.php | 551 + .../src/service/Recount.php | 79 + .../src/service/contract/PaymentInterface.php | 84 + .../src/service/contract/PaymentResponse.php | 98 + .../service/contract/PaymentUsageTrait.php | 393 + .../src/service/payment/AliPayment.php | 189 + .../src/service/payment/BalancePayment.php | 140 + .../src/service/payment/CouponPayment.php | 135 + .../src/service/payment/EmptyPayment.php | 122 + .../src/service/payment/IntegralPayment.php | 145 + .../src/service/payment/JoinPayment.php | 173 + .../src/service/payment/VoucherPayment.php | 121 + .../src/service/payment/WechatPayment.php | 116 + .../payment/wechat/WechatPaymentV2.php | 221 + .../payment/wechat/WechatPaymentV3.php | 225 + .../src/view/balance/form.html | 66 + .../src/view/balance/index.html | 103 + .../src/view/balance/index_search.html | 41 + .../src/view/config/form.html | 87 + .../src/view/config/form_alipay.html | 17 + .../src/view/config/form_joinpay.html | 23 + .../src/view/config/form_voucher.html | 7 + .../src/view/config/form_wechat.html | 86 + .../src/view/config/index.html | 99 + .../src/view/config/index_search.html | 42 + .../src/view/config/types.html | 53 + .../src/view/integral/form.html | 66 + .../src/view/integral/index.html | 103 + .../src/view/integral/index_search.html | 41 + .../think-plugs-payment/src}/view/main.html | 2 +- .../src/view/record/index.html | 132 + .../src/view/record/index_search.html | 57 + .../src/view/refund/index.html | 135 + .../src/view/refund/index_search.html | 58 + .../think-plugs-payment/src/view/table.html | 23 + ...20241010000006_install_payment20241010.php | 262 + .../think-plugs-payment/tests/.bootstrap.php | 22 + .../tests/BalanceIntegrationTest.php | 96 + .../tests/IntegralIntegrationTest.php | 113 + .../tests/PaymentAddressControllerTest.php | 158 + .../tests/PaymentLedgerControllerTest.php | 580 + .../tests/PaymentLedgerIntegrationTest.php | 204 + .../tests/PaymentRecordControllerTest.php | 511 + .../tests/PaymentRecordIntegrationTest.php | 245 + .../tests/PaymentRecountServiceTest.php | 116 + .../think-plugs-payment/tests/PaymentTest.php | 44 + .../.github/workflows/release.yml | 75 + plugin/think-plugs-static/.gitignore | 17 + plugin/think-plugs-static/.php-cs-fixer.php | 120 + plugin/think-plugs-static/composer.json | 59 + plugin/think-plugs-static/license | 21 + plugin/think-plugs-static/readme.api.md | 33 + plugin/think-plugs-static/readme.md | 14 + plugin/think-plugs-static/stc/.env.example | 16 + plugin/think-plugs-static/stc/config/app.php | 94 + .../think-plugs-static/stc/config/cache.php | 62 + .../think-plugs-static/stc/config/cookie.php | 35 + .../stc/config/database.php | 86 + plugin/think-plugs-static/stc/config/lang.php | 42 + plugin/think-plugs-static/stc/config/log.php | 60 + .../think-plugs-static/stc/config/phinx.php | 27 + .../think-plugs-static/stc/config/route.php | 61 + plugin/think-plugs-static/stc/config/view.php | 45 + .../think-plugs-static/stc/default/Index.php | 43 + .../think-plugs-static/stc/public/.htaccess | 8 + .../think-plugs-static/stc/public/index.php | 26 + .../think-plugs-static/stc/public/robots.txt | 4 + .../think-plugs-static/stc/public/router.php | 24 + .../stc/public/static/extra/script.js | 46 + .../stc/public/static/extra/style.css | 16 + .../stc/public/static/login.js | 353 + .../static/plugs/angular/angular.min.js | 351 + .../public/static/plugs/ckeditor4/ckeditor.js | 1443 + .../public/static/plugs/ckeditor4/config.js | 80 + .../static/plugs/ckeditor4/contents.css | 208 + .../public/static/plugs/ckeditor4/lang/en.js | 5 + .../static/plugs/ckeditor4/lang/zh-cn.js | 5 + .../public/static/plugs/ckeditor4/lang/zh.js | 5 + .../plugins/a11yhelp/dialogs/a11yhelp.js | 10 + .../plugins/a11yhelp/dialogs/lang/en.js | 11 + .../plugins/a11yhelp/dialogs/lang/zh-cn.js | 9 + .../plugins/a11yhelp/dialogs/lang/zh.js | 9 + .../ckeditor4/plugins/about/dialogs/about.js | 8 + .../about/dialogs/hidpi/logo_ckeditor.png | Bin 0 -> 12236 bytes .../plugins/about/dialogs/logo_ckeditor.png | Bin 0 -> 5650 bytes .../plugins/clipboard/dialogs/paste.js | 11 + .../colordialog/dialogs/colordialog.css | 20 + .../colordialog/dialogs/colordialog.js | 14 + .../cursors/cursor-disabled.svg | 25 + .../plugins/copyformatting/cursors/cursor.svg | 14 + .../copyformatting/styles/copyformatting.css | 45 + .../plugins/dialog/dialogDefinition.js | 4 + .../plugins/dialog/styles/dialog.css | 18 + .../ckeditor4/plugins/div/dialogs/div.js | 10 + .../plugins/exportpdf/plugindefinition.js | 1 + .../ckeditor4/plugins/find/dialogs/find.js | 25 + .../ckeditor4/plugins/forms/dialogs/button.js | 8 + .../plugins/forms/dialogs/checkbox.js | 9 + .../ckeditor4/plugins/forms/dialogs/form.js | 8 + .../plugins/forms/dialogs/hiddenfield.js | 7 + .../ckeditor4/plugins/forms/dialogs/radio.js | 9 + .../ckeditor4/plugins/forms/dialogs/select.js | 21 + .../plugins/forms/dialogs/textarea.js | 9 + .../plugins/forms/dialogs/textfield.js | 11 + .../plugins/forms/images/hiddenfield.gif | Bin 0 -> 178 bytes .../static/plugs/ckeditor4/plugins/icons.png | Bin 0 -> 12237 bytes .../plugs/ckeditor4/plugins/icons_hidpi.png | Bin 0 -> 38309 bytes .../plugins/iframe/dialogs/iframe.js | 11 + .../plugins/iframe/images/placeholder.png | Bin 0 -> 265 bytes .../ckeditor4/plugins/image/dialogs/image.js | 44 + .../plugins/image/images/noimage.png | Bin 0 -> 1610 bytes .../ckeditor4/plugins/lineheight/lang/en.js | 3 + .../plugins/lineheight/lang/zh-cn.js | 3 + .../ckeditor4/plugins/lineheight/lang/zh.js | 3 + .../ckeditor4/plugins/lineheight/plugin.js | 82 + .../ckeditor4/plugins/link/dialogs/anchor.js | 8 + .../ckeditor4/plugins/link/dialogs/link.js | 30 + .../ckeditor4/plugins/link/images/anchor.png | Bin 0 -> 752 bytes .../plugins/link/images/hidpi/anchor.png | Bin 0 -> 1109 bytes .../plugins/liststyle/dialogs/liststyle.js | 10 + .../magicline/images/hidpi/icon-rtl.png | Bin 0 -> 176 bytes .../plugins/magicline/images/hidpi/icon.png | Bin 0 -> 199 bytes .../plugins/magicline/images/icon-rtl.png | Bin 0 -> 138 bytes .../plugins/magicline/images/icon.png | Bin 0 -> 133 bytes .../plugins/pagebreak/images/pagebreak.gif | Bin 0 -> 99 bytes .../plugins/pastefromgdocs/filter/default.js | 8 + .../pastefromlibreoffice/filter/default.js | 11 + .../plugins/pastefromword/filter/default.js | 42 + .../plugins/pastetools/filter/common.js | 24 + .../plugins/pastetools/filter/image.js | 12 + .../plugins/preview/images/pagebreak.gif | Bin 0 -> 99 bytes .../ckeditor4/plugins/preview/preview.html | 13 + .../plugins/preview/styles/screen.css | 10 + .../plugins/scayt/dialogs/dialog.css | 23 + .../plugins/scayt/dialogs/options.js | 32 + .../plugins/scayt/dialogs/toolbar.css | 71 + .../plugins/scayt/skins/moono-lisa/scayt.css | 25 + .../showblocks/images/block_address.png | Bin 0 -> 152 bytes .../showblocks/images/block_blockquote.png | Bin 0 -> 154 bytes .../plugins/showblocks/images/block_div.png | Bin 0 -> 127 bytes .../plugins/showblocks/images/block_h1.png | Bin 0 -> 120 bytes .../plugins/showblocks/images/block_h2.png | Bin 0 -> 127 bytes .../plugins/showblocks/images/block_h3.png | Bin 0 -> 123 bytes .../plugins/showblocks/images/block_h4.png | Bin 0 -> 123 bytes .../plugins/showblocks/images/block_h5.png | Bin 0 -> 126 bytes .../plugins/showblocks/images/block_h6.png | Bin 0 -> 123 bytes .../plugins/showblocks/images/block_p.png | Bin 0 -> 115 bytes .../plugins/showblocks/images/block_pre.png | Bin 0 -> 128 bytes .../plugins/smiley/dialogs/smiley.js | 11 + .../plugins/smiley/images/angel_smile.gif | Bin 0 -> 1245 bytes .../plugins/smiley/images/angel_smile.png | Bin 0 -> 1172 bytes .../plugins/smiley/images/angry_smile.gif | Bin 0 -> 1219 bytes .../plugins/smiley/images/angry_smile.png | Bin 0 -> 1220 bytes .../plugins/smiley/images/broken_heart.gif | Bin 0 -> 732 bytes .../plugins/smiley/images/broken_heart.png | Bin 0 -> 1139 bytes .../plugins/smiley/images/confused_smile.gif | Bin 0 -> 1202 bytes .../plugins/smiley/images/confused_smile.png | Bin 0 -> 1101 bytes .../plugins/smiley/images/cry_smile.gif | Bin 0 -> 795 bytes .../plugins/smiley/images/cry_smile.png | Bin 0 -> 1214 bytes .../plugins/smiley/images/devil_smile.gif | Bin 0 -> 1239 bytes .../plugins/smiley/images/devil_smile.png | Bin 0 -> 1220 bytes .../smiley/images/embaressed_smile.gif | Bin 0 -> 786 bytes .../smiley/images/embarrassed_smile.gif | Bin 0 -> 786 bytes .../smiley/images/embarrassed_smile.png | Bin 0 -> 1145 bytes .../plugins/smiley/images/envelope.gif | Bin 0 -> 506 bytes .../plugins/smiley/images/envelope.png | Bin 0 -> 760 bytes .../ckeditor4/plugins/smiley/images/heart.gif | Bin 0 -> 692 bytes .../ckeditor4/plugins/smiley/images/heart.png | Bin 0 -> 999 bytes .../ckeditor4/plugins/smiley/images/kiss.gif | Bin 0 -> 683 bytes .../ckeditor4/plugins/smiley/images/kiss.png | Bin 0 -> 1003 bytes .../plugins/smiley/images/lightbulb.gif | Bin 0 -> 660 bytes .../plugins/smiley/images/lightbulb.png | Bin 0 -> 919 bytes .../plugins/smiley/images/omg_smile.gif | Bin 0 -> 820 bytes .../plugins/smiley/images/omg_smile.png | Bin 0 -> 1122 bytes .../plugins/smiley/images/regular_smile.gif | Bin 0 -> 1209 bytes .../plugins/smiley/images/regular_smile.png | Bin 0 -> 1084 bytes .../plugins/smiley/images/sad_smile.gif | Bin 0 -> 782 bytes .../plugins/smiley/images/sad_smile.png | Bin 0 -> 1115 bytes .../plugins/smiley/images/shades_smile.gif | Bin 0 -> 1231 bytes .../plugins/smiley/images/shades_smile.png | Bin 0 -> 1204 bytes .../plugins/smiley/images/teeth_smile.gif | Bin 0 -> 1201 bytes .../plugins/smiley/images/teeth_smile.png | Bin 0 -> 1183 bytes .../plugins/smiley/images/thumbs_down.gif | Bin 0 -> 715 bytes .../plugins/smiley/images/thumbs_down.png | Bin 0 -> 985 bytes .../plugins/smiley/images/thumbs_up.gif | Bin 0 -> 714 bytes .../plugins/smiley/images/thumbs_up.png | Bin 0 -> 959 bytes .../plugins/smiley/images/tongue_smile.gif | Bin 0 -> 1210 bytes .../plugins/smiley/images/tongue_smile.png | Bin 0 -> 1132 bytes .../plugins/smiley/images/tounge_smile.gif | Bin 0 -> 1210 bytes .../images/whatchutalkingabout_smile.gif | Bin 0 -> 775 bytes .../images/whatchutalkingabout_smile.png | Bin 0 -> 1039 bytes .../plugins/smiley/images/wink_smile.gif | Bin 0 -> 1202 bytes .../plugins/smiley/images/wink_smile.png | Bin 0 -> 1114 bytes .../plugins/specialchar/dialogs/lang/en.js | 13 + .../plugins/specialchar/dialogs/lang/zh-cn.js | 9 + .../plugins/specialchar/dialogs/lang/zh.js | 9 + .../specialchar/dialogs/specialchar.js | 14 + .../ckeditor4/plugins/table/dialogs/table.js | 22 + .../tableselection/styles/tableselection.css | 36 + .../plugins/tabletools/dialogs/tableCell.js | 18 + .../plugins/templates/dialogs/templates.css | 84 + .../plugins/templates/dialogs/templates.js | 11 + .../plugins/templates/templatedefinition.js | 4 + .../plugins/templates/templates/default.js | 7 + .../templates/templates/images/template1.gif | Bin 0 -> 539 bytes .../templates/templates/images/template2.gif | Bin 0 -> 497 bytes .../templates/templates/images/template3.gif | Bin 0 -> 557 bytes .../plugins/widget/images/handle.png | Bin 0 -> 220 bytes .../plugins/wsc/dialogs/ciframe.html | 66 + .../plugins/wsc/dialogs/tmpFrameset.html | 52 + .../ckeditor4/plugins/wsc/dialogs/wsc.css | 82 + .../ckeditor4/plugins/wsc/dialogs/wsc.js | 90 + .../ckeditor4/plugins/wsc/dialogs/wsc_ie.js | 11 + .../plugins/wsc/icons/hidpi/spellchecker.png | Bin 0 -> 2816 bytes .../plugins/wsc/icons/spellchecker.png | Bin 0 -> 836 bytes .../plugs/ckeditor4/plugins/wsc/lang/en.js | 2 + .../plugs/ckeditor4/plugins/wsc/lang/zh-cn.js | 1 + .../plugs/ckeditor4/plugins/wsc/lang/zh.js | 1 + .../plugs/ckeditor4/plugins/wsc/plugin.js | 5 + .../plugins/wsc/skins/moono-lisa/wsc.css | 43 + .../ckeditor4/skins/moono-lisa/dialog.css | 5 + .../ckeditor4/skins/moono-lisa/dialog_ie.css | 5 + .../ckeditor4/skins/moono-lisa/dialog_ie8.css | 5 + .../skins/moono-lisa/dialog_iequirks.css | 5 + .../ckeditor4/skins/moono-lisa/editor.css | 5 + .../skins/moono-lisa/editor_gecko.css | 5 + .../ckeditor4/skins/moono-lisa/editor_ie.css | 5 + .../ckeditor4/skins/moono-lisa/editor_ie8.css | 5 + .../skins/moono-lisa/editor_iequirks.css | 5 + .../ckeditor4/skins/moono-lisa/icons.png | Bin 0 -> 12237 bytes .../skins/moono-lisa/icons_hidpi.png | Bin 0 -> 38309 bytes .../skins/moono-lisa/images/arrow.png | Bin 0 -> 191 bytes .../skins/moono-lisa/images/close.png | Bin 0 -> 615 bytes .../skins/moono-lisa/images/hidpi/close.png | Bin 0 -> 1238 bytes .../moono-lisa/images/hidpi/lock-open.png | Bin 0 -> 1071 bytes .../skins/moono-lisa/images/hidpi/lock.png | Bin 0 -> 1062 bytes .../skins/moono-lisa/images/hidpi/refresh.png | Bin 0 -> 1623 bytes .../skins/moono-lisa/images/lock-open.png | Bin 0 -> 511 bytes .../skins/moono-lisa/images/lock.png | Bin 0 -> 506 bytes .../skins/moono-lisa/images/refresh.png | Bin 0 -> 757 bytes .../skins/moono-lisa/images/spinner.gif | Bin 0 -> 2984 bytes .../ckeditor4/skins/moono-lisa/readme.md | 46 + .../public/static/plugs/ckeditor4/styles.js | 104 + .../static/plugs/ckeditor5/ckeditor.css | 92 + .../public/static/plugs/ckeditor5/ckeditor.js | 7 + .../public/static/plugs/ckeditor5/content.css | 628 + .../static/plugs/ckeditor5/translations/af.js | 1 + .../static/plugs/ckeditor5/translations/ar.js | 1 + .../plugs/ckeditor5/translations/ast.js | 1 + .../static/plugs/ckeditor5/translations/az.js | 1 + .../static/plugs/ckeditor5/translations/bg.js | 1 + .../static/plugs/ckeditor5/translations/bn.js | 1 + .../static/plugs/ckeditor5/translations/bs.js | 1 + .../static/plugs/ckeditor5/translations/ca.js | 1 + .../static/plugs/ckeditor5/translations/cs.js | 1 + .../static/plugs/ckeditor5/translations/da.js | 1 + .../plugs/ckeditor5/translations/de-ch.js | 1 + .../static/plugs/ckeditor5/translations/de.js | 1 + .../static/plugs/ckeditor5/translations/el.js | 1 + .../plugs/ckeditor5/translations/en-au.js | 1 + .../plugs/ckeditor5/translations/en-gb.js | 1 + .../static/plugs/ckeditor5/translations/en.js | 1 + .../static/plugs/ckeditor5/translations/eo.js | 1 + .../plugs/ckeditor5/translations/es-co.js | 1 + .../static/plugs/ckeditor5/translations/es.js | 1 + .../static/plugs/ckeditor5/translations/et.js | 1 + .../static/plugs/ckeditor5/translations/eu.js | 1 + .../static/plugs/ckeditor5/translations/fa.js | 1 + .../static/plugs/ckeditor5/translations/fi.js | 1 + .../static/plugs/ckeditor5/translations/fr.js | 1 + .../static/plugs/ckeditor5/translations/gl.js | 1 + .../static/plugs/ckeditor5/translations/gu.js | 1 + .../static/plugs/ckeditor5/translations/he.js | 1 + .../static/plugs/ckeditor5/translations/hi.js | 1 + .../static/plugs/ckeditor5/translations/hr.js | 1 + .../static/plugs/ckeditor5/translations/hu.js | 1 + .../static/plugs/ckeditor5/translations/hy.js | 1 + .../static/plugs/ckeditor5/translations/id.js | 1 + .../static/plugs/ckeditor5/translations/it.js | 1 + .../static/plugs/ckeditor5/translations/ja.js | 1 + .../static/plugs/ckeditor5/translations/jv.js | 1 + .../static/plugs/ckeditor5/translations/kk.js | 1 + .../static/plugs/ckeditor5/translations/km.js | 1 + .../static/plugs/ckeditor5/translations/kn.js | 1 + .../static/plugs/ckeditor5/translations/ko.js | 1 + .../static/plugs/ckeditor5/translations/ku.js | 1 + .../static/plugs/ckeditor5/translations/lt.js | 1 + .../static/plugs/ckeditor5/translations/lv.js | 1 + .../static/plugs/ckeditor5/translations/ms.js | 1 + .../static/plugs/ckeditor5/translations/nb.js | 1 + .../static/plugs/ckeditor5/translations/ne.js | 1 + .../static/plugs/ckeditor5/translations/nl.js | 1 + .../static/plugs/ckeditor5/translations/no.js | 1 + .../static/plugs/ckeditor5/translations/oc.js | 1 + .../static/plugs/ckeditor5/translations/pl.js | 1 + .../plugs/ckeditor5/translations/pt-br.js | 1 + .../static/plugs/ckeditor5/translations/pt.js | 1 + .../static/plugs/ckeditor5/translations/ro.js | 1 + .../static/plugs/ckeditor5/translations/ru.js | 1 + .../static/plugs/ckeditor5/translations/si.js | 1 + .../static/plugs/ckeditor5/translations/sk.js | 1 + .../static/plugs/ckeditor5/translations/sl.js | 1 + .../static/plugs/ckeditor5/translations/sq.js | 1 + .../plugs/ckeditor5/translations/sr-latn.js | 1 + .../static/plugs/ckeditor5/translations/sr.js | 1 + .../static/plugs/ckeditor5/translations/sv.js | 1 + .../static/plugs/ckeditor5/translations/th.js | 1 + .../static/plugs/ckeditor5/translations/tk.js | 1 + .../static/plugs/ckeditor5/translations/tr.js | 1 + .../static/plugs/ckeditor5/translations/tt.js | 1 + .../static/plugs/ckeditor5/translations/ug.js | 1 + .../static/plugs/ckeditor5/translations/uk.js | 1 + .../static/plugs/ckeditor5/translations/ur.js | 1 + .../static/plugs/ckeditor5/translations/uz.js | 1 + .../static/plugs/ckeditor5/translations/vi.js | 1 + .../static/plugs/ckeditor5/translations/zh.js | 1 + .../static/plugs/cropper/cropper.min.css | 9 + .../static/plugs/cropper/cropper.min.js | 10 + .../static/plugs/echarts/echarts.min.js | 45 + .../stc/public/static/plugs/editor/create.js | 66 + .../public/static/plugs/editor/css/style.css | 27 + .../stc/public/static/plugs/editor/index.js | 24129 ++++++++++++++++ .../public/static/plugs/jquery/area/area.php | 209 + .../public/static/plugs/jquery/area/data.json | 1 + .../static/plugs/jquery/artplayer.min.js | 7 + .../static/plugs/jquery/autocompleter.css | 79 + .../static/plugs/jquery/autocompleter.min.js | 8 + .../public/static/plugs/jquery/base64.min.js | 1 + .../static/plugs/jquery/compressor.min.js | 10 + .../static/plugs/jquery/filesaver.min.js | 1 + .../public/static/plugs/jquery/jquery.min.js | 5 + .../public/static/plugs/jquery/json.min.js | 1 + .../public/static/plugs/jquery/jszip.min.js | 13 + .../public/static/plugs/jquery/less.min.js | 10 + .../public/static/plugs/jquery/marked.min.js | 6 + .../public/static/plugs/jquery/masonry.min.js | 9 + .../stc/public/static/plugs/jquery/md5.min.js | 1 + .../public/static/plugs/jquery/pace.min.js | 2 + .../public/static/plugs/jquery/pcasunzips.js | 85 + .../public/static/plugs/jquery/xlsx.min.js | 17 + .../public/static/plugs/layui/css/layui.css | 1 + .../static/plugs/layui/font/iconfont.eot | Bin 0 -> 54764 bytes .../static/plugs/layui/font/iconfont.svg | 409 + .../static/plugs/layui/font/iconfont.ttf | Bin 0 -> 54588 bytes .../static/plugs/layui/font/iconfont.woff | Bin 0 -> 34928 bytes .../static/plugs/layui/font/iconfont.woff2 | Bin 0 -> 30004 bytes .../stc/public/static/plugs/layui/layui.js | 1 + .../public/static/plugs/layui_exts/excel.js | 10 + .../static/plugs/layui_exts/layCascader.css | 1654 ++ .../static/plugs/layui_exts/layCascader.js | 2085 ++ .../static/plugs/layui_exts/tableSelect.js | 255 + .../static/plugs/layui_exts/xmSelect.js | 8 + .../plugs/notify/img/danger-outline.svg | 1 + .../public/static/plugs/notify/img/danger.png | Bin 0 -> 1218 bytes .../plugs/notify/img/default-outline.svg | 1 + .../static/plugs/notify/img/default.png | Bin 0 -> 859 bytes .../static/plugs/notify/img/info-outline.svg | 1 + .../public/static/plugs/notify/img/info.png | Bin 0 -> 541 bytes .../plugs/notify/img/success-outline.svg | 1 + .../static/plugs/notify/img/success.png | Bin 0 -> 700 bytes .../plugs/notify/img/warning-outline.svg | 1 + .../static/plugs/notify/img/warning.png | Bin 0 -> 991 bytes .../public/static/plugs/notify/notify.min.js | 1 + .../stc/public/static/plugs/notify/theme.css | 348 + .../public/static/plugs/socket/swfobject.js | 4 + .../public/static/plugs/socket/websocket.js | 6 + .../public/static/plugs/socket/websocket.swf | Bin 0 -> 177238 bytes .../static/plugs/sortable/sortable.min.js | 2 + .../plugs/sortable/vue.draggable.min.js | 1 + .../stc/public/static/plugs/system/excel.js | 219 + .../stc/public/static/plugs/system/queue.js | 114 + .../public/static/plugs/system/validate.js | 126 + .../stc/public/static/plugs/vue/vue.min.js | 6 + .../ztree/zTreeStyle/img/diy/1_close.png | Bin 0 -> 601 bytes .../plugs/ztree/zTreeStyle/img/diy/1_open.png | Bin 0 -> 580 bytes .../plugs/ztree/zTreeStyle/img/diy/2.png | Bin 0 -> 570 bytes .../plugs/ztree/zTreeStyle/img/diy/3.png | Bin 0 -> 762 bytes .../plugs/ztree/zTreeStyle/img/diy/4.png | Bin 0 -> 399 bytes .../plugs/ztree/zTreeStyle/img/diy/5.png | Bin 0 -> 710 bytes .../plugs/ztree/zTreeStyle/img/diy/6.png | Bin 0 -> 432 bytes .../plugs/ztree/zTreeStyle/img/diy/7.png | Bin 0 -> 534 bytes .../plugs/ztree/zTreeStyle/img/diy/8.png | Bin 0 -> 529 bytes .../plugs/ztree/zTreeStyle/img/diy/9.png | Bin 0 -> 467 bytes .../plugs/ztree/zTreeStyle/img/line_conn.gif | Bin 0 -> 45 bytes .../plugs/ztree/zTreeStyle/img/loading.gif | Bin 0 -> 381 bytes .../ztree/zTreeStyle/img/zTreeStandard.gif | Bin 0 -> 5564 bytes .../ztree/zTreeStyle/img/zTreeStandard.png | Bin 0 -> 11173 bytes .../plugs/ztree/zTreeStyle/zTreeStyle.css | 97 + .../static/plugs/ztree/ztree.all.min.js | 3 + .../stc/public/static/system.js | 1533 + .../stc/public/static/theme/css/_config.less | 190 + .../stc/public/static/theme/css/_custom.less | 3428 +++ .../stc/public/static/theme/css/_display.less | 446 + .../stc/public/static/theme/css/_layout.less | 485 + .../public/static/theme/css/_layout_1.less | 102 + .../static/theme/css/_layout_1_amber.less | 30 + .../static/theme/css/_layout_1_black.less | 30 + .../static/theme/css/_layout_1_blue.less | 30 + .../static/theme/css/_layout_1_glacier.less | 30 + .../static/theme/css/_layout_1_green.less | 30 + .../static/theme/css/_layout_1_indigo.less | 30 + .../static/theme/css/_layout_1_lime.less | 30 + .../static/theme/css/_layout_1_navy.less | 30 + .../static/theme/css/_layout_1_red.less | 30 + .../static/theme/css/_layout_1_rose.less | 30 + .../static/theme/css/_layout_1_violet.less | 30 + .../public/static/theme/css/_layout_2.less | 223 + .../static/theme/css/_layout_2_black.less | 17 + .../static/theme/css/_layout_2_blue.less | 16 + .../static/theme/css/_layout_2_glacier.less | 16 + .../static/theme/css/_layout_2_green.less | 16 + .../static/theme/css/_layout_2_indigo.less | 16 + .../static/theme/css/_layout_2_lime.less | 16 + .../static/theme/css/_layout_2_ocean.less | 16 + .../static/theme/css/_layout_2_red.less | 16 + .../static/theme/css/_layout_2_rose.less | 16 + .../static/theme/css/_layout_2_slate.less | 16 + .../static/theme/css/_layout_2_sunset.less | 16 + .../static/theme/css/_layout_white.less | 123 + .../stc/public/static/theme/css/console.css | 1 + .../public/static/theme/css/console.css.map | 1 + .../stc/public/static/theme/css/console.less | 109 + .../public/static/theme/css/font/iconfont.ttf | Bin 0 -> 36948 bytes .../static/theme/css/font/iconfont.woff | Bin 0 -> 21552 bytes .../static/theme/css/font/iconfont.woff2 | Bin 0 -> 18716 bytes .../stc/public/static/theme/css/iconfont.css | 1 + .../public/static/theme/css/iconfont.css.map | 1 + .../stc/public/static/theme/css/iconfont.less | 448 + .../stc/public/static/theme/css/login.css | 460 + .../stc/public/static/theme/css/login.css.map | 1 + .../stc/public/static/theme/css/login.less | 533 + .../stc/public/static/theme/css/mobile.css | 1 + .../public/static/theme/css/mobile.css.map | 1 + .../stc/public/static/theme/css/mobile.less | 317 + .../stc/public/static/theme/css/package.json | 9 + .../stc/public/static/theme/err/404.html | 21 + .../stc/public/static/theme/err/404/404.png | Bin 0 -> 44176 bytes .../stc/public/static/theme/err/404/reset.css | 225 + .../stc/public/static/theme/err/404/style.css | 45 + .../stc/public/static/theme/err/500.html | 19 + .../stc/public/static/theme/err/500/style.css | 144 + .../stc/public/static/theme/img/404_icon.png | Bin 0 -> 31096 bytes .../stc/public/static/theme/img/505_icon.png | Bin 0 -> 28583 bytes .../stc/public/static/theme/img/headimg.png | Bin 0 -> 9132 bytes .../stc/public/static/theme/img/image.png | Bin 0 -> 3168 bytes .../stc/public/static/theme/img/login/bg1.jpg | Bin 0 -> 55719 bytes .../stc/public/static/theme/img/login/bg2.jpg | Bin 0 -> 67855 bytes .../stc/public/static/theme/img/upimg.png | Bin 0 -> 4510 bytes .../stc/public/static/theme/img/upvideo.png | Bin 0 -> 4701 bytes .../public/static/theme/img/wechat/index.png | Bin 0 -> 231 bytes .../static/theme/img/wechat/m-icon-error.png | Bin 0 -> 36806 bytes .../theme/img/wechat/m-icon-success.png | Bin 0 -> 34420 bytes .../static/theme/img/wechat/mobile_foot.png | Bin 0 -> 1348 bytes .../static/theme/img/wechat/mobile_head.png | Bin 0 -> 14326 bytes .../static/theme/img/wechat/qrc_pay_error.jpg | Bin 0 -> 34402 bytes .../static/theme/img/wechat/qrc_payed.jpg | Bin 0 -> 28055 bytes plugin/think-plugs-static/stc/think | 27 + plugin/think-plugs-system/composer.json | 148 + plugin/think-plugs-system/phpunit.xml.dist | 13 + plugin/think-plugs-system/readme.api.md | 164 + plugin/think-plugs-system/readme.md | 15 + plugin/think-plugs-system/src/Service.php | 65 + .../src/builder/AuthBuilder.php | 705 + .../src/builder/BaseBuilder.php | 216 + .../src/builder/ConfigBuilder.php | 865 + .../src/builder/FileBuilder.php | 139 + .../src/builder/IconPickerBuilder.php | 128 + .../src/builder/MenuBuilder.php | 449 + .../src/builder/OplogBuilder.php | 132 + .../src/builder/PluginBuilder.php | 146 + .../src/builder/QueueBuilder.php | 281 + .../src/builder/SystemListPage.php | 25 + .../src/builder/SystemListTabs.php | 80 + .../src/builder/SystemTablePreset.php | 185 + .../src/builder/ThemeBuilder.php | 368 + .../src/builder/UploadImageDialogBuilder.php | 224 + .../src/builder/UserBuilder.php | 498 + plugin/think-plugs-system/src/common.php | 249 + .../src/controller/Auth.php | 123 + .../src/controller/Base.php | 125 + .../src/controller/Config.php | 160 + .../src/controller/File.php | 123 + .../src/controller/Index.php | 138 + .../src/controller/Login.php | 292 + .../src/controller/Menu.php | 124 + .../src}/controller/Oplog.php | 45 +- .../src/controller/Plugin.php | 100 + .../src/controller/Queue.php | 105 + .../src/controller/User.php | 147 + .../src}/controller/api/Plugs.php | 59 +- .../src/controller/api/Queue.php | 182 + .../src}/controller/api/System.php | 78 +- .../src}/controller/api/Upload.php | 252 +- .../think-plugs-system/src/helper/Service.php | 54 + .../helper/command/database/BackupCommand.php | 170 + .../command/database/DatabaseCommand.php | 105 + .../helper/command/database/IndexCommand.php | 96 + .../command/database/MigrateCommand.php | 61 + .../helper/command/database/ModelCommand.php | 109 + .../command/database/ReplaceCommand.php | 82 + .../command/database/RestoreCommand.php | 251 + .../helper/command/project/PackageCommand.php | 161 + .../helper/command/project/PublishCommand.php | 759 + .../command/system/MenuResetCommand.php | 100 + .../src/helper/database/IndexNameService.php | 59 + .../src/helper/integration/ExpressService.php | 174 + .../helper/migration/MigrationExporter.php | 282 + .../src/helper/migration/PhinxExtend.php | 588 + .../helper/model/NormalizedModelGenerator.php | 139 + .../src/helper/plugin/PluginMenuService.php | 135 + .../src/helper/plugin/PluginRegistry.php | 236 + .../src/helper/service/bin/package.stub | 108 + .../think-plugs-system/src/lang/_loader.php | 35 + plugin/think-plugs-system/src/lang/en-us.php | 800 + plugin/think-plugs-system/src/lang/zh-cn.php | 50 + plugin/think-plugs-system/src/lang/zh-tw.php | 488 + .../src/middleware/JwtTokenAuth.php | 112 + .../src/middleware/LoadModuleLangPack.php | 27 + .../src/middleware/RbacAccess.php | 93 + .../src/model/SystemAuth.php | 220 + .../src/model/SystemBase.php | 412 + .../src/model/SystemData.php | 74 + .../src/model/SystemFile.php | 134 + .../src/model/SystemMenu.php | 36 + .../src/model/SystemNode.php | 40 + .../src/model/SystemOplog.php | 64 + .../src/model/SystemUser.php | 86 + plugin/think-plugs-system/src/route/demo.php | 55 + .../src/service/AuthService.php | 995 + .../src/service/BaseService.php | 272 + .../src/service/CaptchaService.php | 202 + .../src/service/ConfigService.php | 644 + .../src/service/FileService.php | 213 + .../src/service/IndexService.php | 143 + .../src/service/LangService.php | 40 + .../src/service/LoginService.php | 70 + .../src/service/MenuService.php | 905 + .../src/service/OplogService.php | 82 + .../src/service/PluginService.php | 342 + .../src/service/QueueService.php | 85 + .../src/service/SystemContext.php | 111 + .../src/service/SystemService.php | 351 + .../src/service/UserService.php | 413 + .../src/service/bin/captcha.ttf | Bin 0 -> 34852 bytes .../src/storage/AliossStorage.php | 273 + .../src/storage/AlistStorage.php | 299 + .../src/storage/LocalStorage.php | 162 + .../src/storage/QiniuStorage.php | 242 + .../src/storage/StorageAuthorize.php | 98 + .../src/storage/StorageConfig.php | 313 + .../src/storage/StorageManager.php | 141 + .../src/storage/StorageUsageTrait.php | 118 + .../src/storage/TxcosStorage.php | 285 + .../src/storage/UpyunStorage.php | 222 + .../src/storage/extra/mimes.php | 1016 + .../src/storage/extra}/upload.js | 66 +- .../think-plugs-system/src}/view/error.php | 2 +- plugin/think-plugs-system/src/view/full.html | 9 + .../src}/view/index/index-left.html | 6 +- .../src/view/index/index-top.html | 44 + .../src}/view/index/index.html | 32 +- .../src/view/login/index.html | 91 + .../src/view/plugin/layout.html | 22 + .../20241010000001_install_system20241010.php | 589 + .../tests/ApiQueueControllerTest.php | 270 + .../tests/ApiSystemControllerTest.php | 285 + .../tests/AuthControllerTest.php | 355 + .../tests/BaseControllerTest.php | 371 + .../tests/CommonFunctionsTest.php | 63 + .../tests/ConfigControllerTest.php | 287 + .../tests/ConfigPageRenderTest.php | 199 + .../tests/ConsoleCssUtilityTest.php | 72 + .../tests/FileControllerTest.php | 486 + .../tests/IndexControllerTest.php | 463 + .../think-plugs-system/tests/LangPackTest.php | 162 + .../tests/LoginControllerTest.php | 455 + .../tests/MenuControllerTest.php | 385 + .../tests/OplogControllerTest.php | 292 + .../tests/PluginControllerTest.php | 238 + .../tests/PlugsControllerTest.php | 237 + .../tests/QueueControllerTest.php | 323 + .../tests/RbacAccessTest.php | 115 + .../tests/UploadControllerTest.php | 211 + .../tests/UserControllerTest.php | 641 + plugin/think-plugs-system/tests/bootstrap.php | 34 + .../tests/helper/IndexNameServiceTest.php | 45 + .../tests/helper/PluginMenuServiceTest.php | 71 + .../tests/helper/PublishTest.php | 645 + .../think-plugs-wechat-client/.gitattributes | 3 + .../.github/workflows/release.yml | 75 + plugin/think-plugs-wechat-client/.gitignore | 12 + .../.php-cs-fixer.php | 120 + .../think-plugs-wechat-client/composer.json | 130 + plugin/think-plugs-wechat-client/license | 21 + .../think-plugs-wechat-client/readme.api.md | 220 + plugin/think-plugs-wechat-client/readme.md | 15 + .../think-plugs-wechat-client/src/Service.php | 63 + .../src}/command/Auto.php | 56 +- .../src}/command/Clear.php | 27 +- .../src}/command/Fans.php | 32 +- .../src}/controller/Auto.php | 28 +- .../src}/controller/Config.php | 148 +- .../src}/controller/Fans.php | 32 +- .../src}/controller/Keys.php | 28 +- .../src}/controller/Menu.php | 34 +- .../src}/controller/News.php | 33 +- .../src}/controller/api/Js.php | 20 +- .../src}/controller/api/Login.php | 20 +- .../src}/controller/api/Push.php | 28 +- .../src}/controller/api/Test.php | 52 +- .../src}/controller/api/View.php | 22 +- .../src}/controller/payment/Record.php | 42 +- .../src}/controller/payment/Refund.php | 38 +- .../src}/lang/en-us.php | 16 +- .../src/model/WechatAuto.php | 25 + .../src}/model/WechatFans.php | 20 +- .../src}/model/WechatFansTags.php | 20 +- .../src/model/WechatKeys.php | 25 + .../src}/model/WechatMedia.php | 20 +- .../src/model/WechatNews.php | 35 + .../src}/model/WechatNewsArticle.php | 20 +- .../src/model/WechatPaymentRecord.php | 48 + .../src/model/WechatPaymentRefund.php | 34 + .../src}/service/AutoService.php | 22 +- .../src}/service/FansService.php | 20 +- .../src}/service/LoginService.php | 20 +- .../src}/service/MediaService.php | 33 +- .../src}/service/PaymentService.php | 74 +- .../src/service/WechatService.php | 524 + .../src}/view/api/login/failed.html | 0 .../src}/view/api/login/success.html | 2 +- .../src}/view/api/test/jsapi.html | 0 .../src}/view/api/test/jssdk.html | 0 .../src}/view/api/test/oauth.html | 0 .../src}/view/api/view/image.html | 0 .../src}/view/api/view/item.html | 4 +- .../src}/view/api/view/main.html | 4 +- .../src}/view/api/view/music.html | 0 .../src}/view/api/view/news.html | 6 +- .../src}/view/api/view/text.html | 0 .../src}/view/api/view/video.html | 0 .../src}/view/api/view/voice.html | 0 .../src}/view/auto/form.html | 22 +- .../src}/view/auto/index.html | 18 +- .../src}/view/auto/index_search.html | 2 +- .../src/view/config/options-style.html | 402 + .../src/view/config/options.html | 97 + .../src}/view/config/options_form_api.html | 39 +- .../src}/view/config/options_form_thr.html | 60 +- .../src/view/config/options_test.html | 21 + .../src}/view/config/payment.html | 22 +- .../src/view/config/payment_test.html | 40 + .../src}/view/fans/index.html | 2 +- .../src}/view/fans/index_search.html | 2 +- .../src}/view/full.html | 11 +- .../src}/view/keys/form.html | 24 +- .../src}/view/keys/index.html | 20 +- .../src}/view/keys/index_search.html | 2 +- .../src/view/main.html | 23 + .../src}/view/menu/index.html | 28 +- .../src}/view/news/form.html | 12 +- .../src}/view/news/formstyle.html | 0 .../src}/view/news/index.html | 6 +- .../src}/view/news/select.html | 4 +- .../src}/view/payment/record/index.html | 0 .../view/payment/record/index_search.html | 2 +- .../src}/view/payment/refund/index.html | 2 +- .../view/payment/refund/index_search.html | 0 .../src/view/table.html | 23 + .../20241010000003_install_wechat20241010.php | 334 + .../think-plugs-wechat-service/.gitattributes | 3 + .../.github/workflows/release.yml | 75 + plugin/think-plugs-wechat-service/.gitignore | 12 + .../.php-cs-fixer.php | 120 + .../think-plugs-wechat-service/composer.json | 75 + .../think-plugs-wechat-service/readme.api.md | 73 + plugin/think-plugs-wechat-service/readme.md | 15 + .../src/Service.php | 36 + .../src/command/Wechat.php | 97 + .../src/controller/Config.php | 93 + .../src/controller/Wechat.php | 141 + .../src/controller/api/Client.php | 112 + .../src/controller/api/Push.php | 217 + .../src/lang/en-us.php | 158 + .../src/model/WechatAuth.php | 33 + .../src/service/AuthService.php | 236 + .../src/service/ConfigService.php | 218 + .../src/service/PublishHandle.php | 69 + .../src/service/ReceiveHandle.php | 73 + .../src/view/config/index.html | 111 + .../src/view/main.html | 18 + .../src/view/not-auth.html | 1 + .../src/view/table.html | 16 + .../src/view/wechat/index.html | 106 + .../src/view/wechat/index_search.html | 56 + ...0000009_install_wechat_service20241010.php | 84 + plugin/think-plugs-wemall/.gitattributes | 3 + .../.github/workflows/release.yml | 75 + plugin/think-plugs-wemall/.gitignore | 13 + plugin/think-plugs-wemall/.php-cs-fixer.php | 120 + plugin/think-plugs-wemall/composer.json | 246 + plugin/think-plugs-wemall/readme.api.md | 678 + plugin/think-plugs-wemall/readme.md | 15 + plugin/think-plugs-wemall/src/Service.php | 155 + .../think-plugs-wemall/src/command/Clear.php | 191 + .../think-plugs-wemall/src/command/Trans.php | 302 + .../think-plugs-wemall/src/command/Users.php | 67 + plugin/think-plugs-wemall/src/common.php | 43 + .../src/controller/api/Auth.php | 77 + .../src/controller/api/Data.php | 115 + .../src/controller/api/Goods.php | 199 + .../src/controller/api/auth/Cart.php | 89 + .../src/controller/api/auth/Center.php | 77 + .../src/controller/api/auth/Checkin.php | 142 + .../src/controller/api/auth/Coupon.php | 141 + .../src/controller/api/auth/Order.php | 604 + .../src/controller/api/auth/Rebate.php | 78 + .../src/controller/api/auth/Refund.php | 189 + .../src/controller/api/auth/Spread.php | 101 + .../src/controller/api/auth/Transfer.php | 198 + .../controller/api/auth/action/Collect.php | 114 + .../controller/api/auth/action/History.php | 98 + .../src/controller/api/auth/action/Search.php | 63 + .../src/controller/api/help/Feedback.php | 74 + .../src/controller/api/help/Problem.php | 45 + .../src/controller/api/help/Question.php | 126 + .../src/controller/base/Agent.php | 164 + .../src/controller/base/Config.php | 96 + .../src/controller/base/Design.php | 95 + .../src/controller/base/Discount.php | 115 + .../src/controller/base/Level.php | 162 + .../src/controller/base/Notify.php | 125 + .../src/controller/base/Poster.php | 151 + .../src/controller/base/Report.php | 105 + .../src/controller/base/express/Company.php | 121 + .../src/controller/base/express/Template.php | 144 + .../src/controller/help/Feedback.php | 153 + .../src/controller/help/Problem.php | 118 + .../src/controller/help/Question.php | 125 + .../src/controller/shop/Goods.php | 278 + .../src/controller/shop/Order.php | 210 + .../src/controller/shop/Refund.php | 181 + .../src/controller/shop/Reply.php | 91 + .../src/controller/shop/Sender.php | 181 + .../src/controller/shop/goods/Cate.php | 134 + .../src/controller/shop/goods/Mark.php | 196 + .../src/controller/user/Admin.php | 184 + .../src/controller/user/Checkin.php | 94 + .../src/controller/user/Coupon.php | 61 + .../src/controller/user/Create.php | 163 + .../src/controller/user/Rebate.php | 75 + .../src/controller/user/Recharge.php | 139 + .../src/controller/user/Transfer.php | 168 + .../src/controller/user/coupon/Config.php | 123 + .../src/controller/user/rebate/Config.php | 113 + plugin/think-plugs-wemall/src/lang/en-us.php | 1013 + .../think-plugs-wemall/src/model/AbsUser.php | 54 + .../src/model/PluginWemallConfigAgent.php | 68 + .../src/model/PluginWemallConfigCoupon.php | 94 + .../src/model/PluginWemallConfigDiscount.php | 77 + .../src/model/PluginWemallConfigLevel.php | 77 + .../src/model/PluginWemallConfigNotify.php | 44 + .../src/model/PluginWemallConfigPoster.php | 153 + .../src/model/PluginWemallConfigRebate.php | 80 + .../src/model/PluginWemallExpressCompany.php | 49 + .../src/model/PluginWemallExpressTemplate.php | 117 + .../src/model/PluginWemallGoods.php | 190 + .../src/model/PluginWemallGoodsCate.php | 118 + .../src/model/PluginWemallGoodsItem.php | 108 + .../src/model/PluginWemallGoodsMark.php | 46 + .../src/model/PluginWemallGoodsStock.php | 40 + .../src/model/PluginWemallHelpFeedback.php | 41 + .../src/model/PluginWemallHelpProblem.php | 40 + .../src/model/PluginWemallHelpQuestion.php | 66 + .../src/model/PluginWemallHelpQuestionX.php | 64 + .../src/model/PluginWemallOrder.php | 70 + .../src/model/PluginWemallOrderCart.php | 56 + .../src/model/PluginWemallOrderItem.php | 86 + .../src/model/PluginWemallOrderRefund.php | 76 + .../src/model/PluginWemallOrderSender.php | 33 + .../model/PluginWemallUserActionCollect.php | 47 + .../model/PluginWemallUserActionComment.php | 84 + .../model/PluginWemallUserActionHistory.php | 47 + .../model/PluginWemallUserActionSearch.php | 34 + .../src/model/PluginWemallUserCheckin.php | 46 + .../src/model/PluginWemallUserCoupon.php | 86 + .../src/model/PluginWemallUserCreate.php | 59 + .../src/model/PluginWemallUserRebate.php | 73 + .../src/model/PluginWemallUserRecharge.php | 39 + .../src/model/PluginWemallUserRelation.php | 146 + .../src/model/PluginWemallUserTransfer.php | 36 + .../src/service/ConfigService.php | 118 + .../src/service/ExpressService.php | 147 + .../src/service/GoodsService.php | 150 + .../src/service/OpenApiService.php | 289 + .../src/service/PosterService.php | 287 + .../src/service/UserAction.php | 149 + .../src/service/UserAgent.php | 123 + .../src/service/UserCoupon.php | 148 + .../src/service/UserCreate.php | 164 + .../src/service/UserOrder.php | 397 + .../src/service/UserRebate.php | 449 + .../src/service/UserRefund.php | 81 + .../src/service/UserReward.php | 105 + .../src/service/UserTransfer.php | 85 + .../src/service/UserUpgrade.php | 226 + .../src/service/extra/font01.ttf | Bin 0 -> 9897168 bytes .../src/view/base/agent/form.html | 170 + .../src/view/base/agent/index.html | 102 + .../src/view/base/agent/index_search.html | 38 + .../src/view/base/config/index.html | 92 + .../src/view/base/config/index_content.html | 38 + .../src/view/base/config/order.html | 151 + .../src/view/base/config/params.html | 97 + .../src/view/base/design/index-init.html | 391 + .../src/view/base/design/index-script.html | 384 + .../src/view/base/design/index-style.html | 389 + .../view/base/design/index-view-goods.html | 49 + .../src/view/base/design/index-view-icon.html | 11 + .../view/base/design/index-view-image.html | 75 + .../base/design/index-view-page-cart.html | 27 + .../base/design/index-view-page-cate.html | 9 + .../base/design/index-view-page-center.html | 81 + .../src/view/base/design/index-view.html | 89 + .../view/base/design/index-x-head-form.html | 46 + .../view/base/design/index-x-navbar-form.html | 70 + .../view/base/design/index-x-normal-form.html | 312 + .../src/view/base/design/index.html | 91 + .../src/view/base/design/link.html | 38 + .../src/view/base/design/link_other.html | 14 + .../src/view/base/discount/form.html | 50 + .../src/view/base/discount/index.html | 84 + .../src/view/base/express/company/form.html | 30 + .../src/view/base/express/company/index.html | 94 + .../base/express/company/index_search.html | 27 + .../src/view/base/express/template/form.html | 354 + .../src/view/base/express/template/index.html | 92 + .../base/express/template/index_search.html | 26 + .../view/base/express/template/region.html | 160 + .../src/view/base/level/form.html | 180 + .../src/view/base/level/index.html | 91 + .../src/view/base/level/index_search.html | 38 + .../src/view/base/notify/form.html | 39 + .../src/view/base/notify/index.html | 75 + .../src/view/base/notify/index_search.html | 38 + .../src/view/base/poster/form.html | 212 + .../src/view/base/poster/index.html | 115 + .../src/view/base/poster/index_search.html | 28 + .../src/view/base/report/index.html | 303 + .../src/view/help/feedback/form.html | 48 + .../src/view/help/feedback/index.html | 105 + .../src/view/help/feedback/index_search.html | 34 + .../src/view/help/problem/form.html | 37 + .../src/view/help/problem/index.html | 88 + .../src/view/help/problem/index_search.html | 27 + .../src/view/help/problem/select.html | 69 + .../src/view/help/question/form.html | 93 + .../src/view/help/question/index.html | 73 + .../src/view/help/question}/index_search.html | 29 +- .../think-plugs-wemall/src}/view/main.html | 4 +- .../src/view/shop/goods/cate/form.html | 46 + .../src/view/shop/goods/cate/index.html | 78 + .../src/view/shop/goods/cate/select.html | 32 + .../src/view/shop/goods/form.html | 500 + .../src/view/shop/goods/form_style.html | 185 + .../src/view/shop/goods/index.html | 152 + .../src/view/shop/goods/index_search.html | 95 + .../src/view/shop/goods/mark/select.html | 31 + .../src/view/shop/goods/select.html | 35 + .../src/view/shop/goods/select_search.html | 24 + .../src/view/shop/goods/stock.html | 98 + .../src/view/shop/order/index.html | 172 + .../src/view/shop/order/index_search.html | 130 + .../src/view/shop/refund/form.html | 272 + .../src/view/shop/refund/index.html | 90 + .../src/view/shop/refund/index_search.html | 89 + .../src/view/shop/reply/form.html | 81 + .../src/view/shop/reply/index.html | 97 + .../src/view/shop/reply/index_search.html | 34 + .../src/view/shop/sender/config.html | 50 + .../src/view/shop/sender/delivery_form.html | 89 + .../src/view/shop/sender/delivery_query.html | 17 + .../src/view/shop/sender/index.html | 165 + .../src/view/shop/sender/index_search.html | 111 + .../think-plugs-wemall/src}/view/table.html | 4 +- .../src/view/user/admin/form.html | 61 + .../src/view/user/admin/index.html | 173 + .../src/view/user/admin/index_search.html | 84 + .../src/view/user/admin/parent.html | 63 + .../src/view/user/admin/parent_search.html | 36 + .../src/view/user/checkin/config.html | 109 + .../src/view/user/checkin/index.html | 42 + .../src/view/user/checkin/index_search.html | 38 + .../src/view/user/coupon/config/form.html | 171 + .../src/view/user/coupon/config/index.html | 88 + .../view/user/coupon/config/index_search.html | 36 + .../src/view/user/coupon/index.html | 59 + .../src/view/user/coupon/index_search.html | 45 + .../src/view/user/create/form.html | 95 + .../src/view/user/create/index.html | 113 + .../src/view/user/create/index_search.html | 35 + .../src/view/user/rebate/config/form.html | 128 + .../src/view/user/rebate/config/index.html | 63 + .../view/user/rebate/config/index_search.html | 41 + .../src/view/user/rebate/index.html | 90 + .../src/view/user/rebate/index_search.html | 59 + .../src/view/user/recharge/form.html | 52 + .../src/view/user/recharge/index.html | 58 + .../src/view/user/recharge/index_search.html | 30 + .../src/view/user/transfer/audit.html | 147 + .../src/view/user/transfer/config.html | 135 + .../src/view/user/transfer/index.html | 92 + .../src/view/user/transfer/index_search.html | 53 + .../src/view/user/transfer/payment.html | 36 + .../20241010000007_install_wemall20241010.php | 1284 + .../tests/ConfigServiceTest.php | 50 + .../tests/PaymentEventIntegrationTest.php | 519 + .../tests/UserAdminPasswordTest.php | 91 + .../tests/UserOrderTest.php | 53 + plugin/think-plugs-worker/.gitattributes | 3 + .../.github/workflows/release.yml | 75 + plugin/think-plugs-worker/.gitignore | 11 + plugin/think-plugs-worker/.php-cs-fixer.php | 120 + plugin/think-plugs-worker/composer.json | 69 + plugin/think-plugs-worker/license | 26 + plugin/think-plugs-worker/phpunit.xml.dist | 24 + plugin/think-plugs-worker/readme.api.md | 44 + plugin/think-plugs-worker/readme.md | 15 + plugin/think-plugs-worker/src/Service.php | 50 + .../think-plugs-worker/src/command/Queue.php | 140 + .../think-plugs-worker/src/command/Worker.php | 597 + plugin/think-plugs-worker/src/common.php | 172 + .../src/model/SystemQueue.php | 28 + .../src/service/HttpServer.php | 199 + .../src/service/ProcessService.php | 505 + .../think-plugs-worker/src/service/Queue.php | 92 + .../src/service/QueueExecutor.php | 146 + .../src/service/QueueServer.php | 241 + .../src/service/QueueService.php | 434 + .../think-plugs-worker/src/service/Server.php | 107 + .../src/service/ThinkApp.php | 182 + .../src/service/ThinkCookie.php | 82 + .../src/service/ThinkHttp.php | 67 + .../src/service/ThinkRequest.php | 219 + .../src/service/ThinkResponseFile.php | 152 + .../src/service/WorkerConfig.php | 345 + .../src/service/WorkerExceptionHandle.php | 90 + .../src/service/WorkerMonitor.php | 259 + .../src/service/WorkerState.php | 306 + .../src/service/bin/console.exe | Bin 0 -> 1536 bytes .../20241010000008_install_worker20241010.php | 123 + plugin/think-plugs-worker/stc/worker.php | 71 + .../tests/CommonFunctionsTest.php | 42 + .../tests/ProcessServiceTest.php | 69 + .../tests/QueueServiceTest.php | 100 + .../think-plugs-worker/tests/ThinkAppTest.php | 53 + .../tests/WorkerConfigTest.php | 103 + .../tests/WorkerExceptionHandleTest.php | 77 + .../tests/WorkerStateTest.php | 179 + plugin/think-plugs-worker/tests/bootstrap.php | 33 + plugin/think-plugs-wuma/.gitattributes | 3 + .../.github/workflows/release.yml | 75 + plugin/think-plugs-wuma/.gitignore | 12 + plugin/think-plugs-wuma/.php-cs-fixer.php | 120 + plugin/think-plugs-wuma/composer.json | 213 + plugin/think-plugs-wuma/phpunit.xml.dist | 14 + plugin/think-plugs-wuma/readme.api.md | 78 + plugin/think-plugs-wuma/readme.md | 15 + plugin/think-plugs-wuma/src/Service.php | 41 + .../think-plugs-wuma/src/command/Create.php | 115 + .../think-plugs-wuma/src/controller/Code.php | 187 + .../think-plugs-wuma/src/controller/Query.php | 67 + .../src/controller/Warehouse.php | 103 + .../src/controller/api/Auth.php | 60 + .../src/controller/api/Base.php | 122 + .../src/controller/api/Coder.php | 102 + .../src/controller/api/Login.php | 119 + .../src/controller/sales/Config.php | 76 + .../src/controller/sales/History.php | 70 + .../src/controller/sales/Level.php | 249 + .../src/controller/sales/Order.php | 86 + .../src/controller/sales/Stock.php | 95 + .../src/controller/sales/User.php | 160 + .../src/controller/scaner/Notify.php | 117 + .../src/controller/scaner/Protal.php | 188 + .../src/controller/scaner/Query.php | 52 + .../src/controller/source/Assign.php | 207 + .../src/controller/source/Blockchain.php | 192 + .../src/controller/source/Certificate.php | 154 + .../src/controller/source/Produce.php | 151 + .../src/controller/source/Template.php | 141 + .../src/controller/warehouse/Batch.php | 185 + .../src/controller/warehouse/History.php | 82 + .../src/controller/warehouse/Inter.php | 210 + .../src/controller/warehouse/Outer.php | 194 + .../src/controller/warehouse/Relation.php | 88 + .../src/controller/warehouse/Replace.php | 52 + .../src/controller/warehouse/Stock.php | 89 + .../src/controller/warehouse/User.php | 216 + plugin/think-plugs-wuma/src/lang/en-us.php | 86 + .../src/model/AbstractPrivate.php | 32 + .../src/model/PlainPrivate.php | 25 + .../src/model/PluginWumaCodeRule.php | 155 + .../src/model/PluginWumaCodeRuleRange.php | 49 + .../src/model/PluginWumaSalesOrder.php | 91 + .../src/model/PluginWumaSalesOrderData.php | 34 + .../model/PluginWumaSalesOrderDataMins.php | 38 + .../model/PluginWumaSalesOrderDataNums.php | 32 + .../src/model/PluginWumaSalesUser.php | 99 + .../src/model/PluginWumaSalesUserLevel.php | 73 + .../src/model/PluginWumaSalesUserStock.php | 112 + .../src/model/PluginWumaSourceAssign.php | 75 + .../src/model/PluginWumaSourceAssignItem.php | 73 + .../src/model/PluginWumaSourceBlockchain.php | 55 + .../src/model/PluginWumaSourceCertificate.php | 66 + .../src/model/PluginWumaSourceProduce.php | 109 + .../src/model/PluginWumaSourceQuery.php | 39 + .../src/model/PluginWumaSourceQueryNotify.php | 44 + .../src/model/PluginWumaSourceQueryVerify.php | 36 + .../src/model/PluginWumaSourceTemplate.php | 70 + .../src/model/PluginWumaWarehouse.php | 59 + .../src/model/PluginWumaWarehouseOrder.php | 135 + .../model/PluginWumaWarehouseOrderData.php | 76 + .../PluginWumaWarehouseOrderDataMins.php | 78 + .../PluginWumaWarehouseOrderDataNums.php | 35 + .../src/model/PluginWumaWarehouseRelation.php | 61 + .../model/PluginWumaWarehouseRelationData.php | 40 + .../src/model/PluginWumaWarehouseReplace.php | 39 + .../src/model/PluginWumaWarehouseStock.php | 125 + .../src/model/PluginWumaWarehouseUser.php | 43 + .../src/service/CertService.php | 70 + .../src/service/CodeService.php | 539 + .../src/service/RelationService.php | 364 + .../src/service/RemoveService.php | 128 + .../src/service/StockService.php | 112 + .../src/service/WhCoderService.php | 347 + .../src/service/WhExportService.php | 268 + .../src/service/WhImportService.php | 197 + .../src/service/extra/font01.ttf | Bin 0 -> 9897168 bytes .../think-plugs-wuma/src/view/code/form.html | 256 + .../src/view/code/index-search.html | 56 + .../think-plugs-wuma/src/view/code/index.html | 151 + .../src/view/code/template.html | 76 + plugin/think-plugs-wuma/src/view/main.html | 23 + .../src/view/sales/config/agx_cfg.html | 42 + .../src/view/sales/config/index.html | 44 + .../src/view/sales/history/index.html | 88 + .../src/view/sales/history/index_search.html | 30 + .../src/view/sales/order/index-search.html | 71 + .../src/view/sales/order/index.html | 84 + .../src/view/sales/order/show.html | 69 + .../src/view/sales/stock/index-search.html | 37 + .../src/view/sales/stock/index.html | 55 + .../src/view/sales/stock/show.html | 69 + .../src/view/sales/user/form.html | 145 + .../src/view/sales/user/index.html | 63 + .../src/view/sales/user/index_search.html | 52 + .../src/view/sales/user/select.html | 36 + .../src/view/sales/user/select_search.html | 39 + .../src/view/scaner/notify/agent-search.html | 54 + .../src/view/scaner/notify/agent.html | 47 + .../src/view/scaner/notify/area-search.html | 36 + .../src/view/scaner/notify/area.html | 43 + .../src/view/scaner/notify/index-search.html | 72 + .../src/view/scaner/notify/index.html | 57 + .../src/view/scaner/protal/index.html | 27 + .../src/view/scaner/query/index-search.html | 37 + .../src/view/scaner/query/index.html | 27 + .../src/view/source/assign/form.html | 247 + .../src/view/source/assign/index-search.html | 44 + .../src/view/source/assign/index.html | 105 + .../src/view/source/blockchain/form.html | 209 + .../src/view/source/blockchain/hash.html | 50 + .../view/source/blockchain/index-search.html | 26 + .../src/view/source/blockchain/index.html | 127 + .../src/view/source/blockchain/view.html | 53 + .../src/view/source/certificate/form.html | 139 + .../view/source/certificate/index-search.html | 27 + .../src/view/source/certificate/index.html | 100 + .../src/view/source/produce/form.html | 92 + .../src/view/source/produce/index-search.html | 33 + .../src/view/source/produce/index.html | 100 + .../src/view/source/template/form-edit.html | 431 + .../src/view/source/template/form-script.html | 262 + .../src/view/source/template/form-style.html | 212 + .../src/view/source/template/form-view.html | 229 + .../src/view/source/template/form.html | 84 + .../view/source/template/index-search.html | 27 + .../src/view/source/template/index.html | 99 + .../src/view/source/template/select.html | 120 + plugin/think-plugs-wuma/src/view/table.html | 23 + .../src/view/warehouse/batch/form.html | 325 + .../view/warehouse/batch/index-search.html | 44 + .../src/view/warehouse/batch/index.html | 80 + .../src/view/warehouse/form.html | 72 + .../view/warehouse/history/index-search.html | 37 + .../src/view/warehouse/history/index.html | 55 + .../src/view/warehouse/index-search.html | 33 + .../src/view/warehouse/index.html | 100 + .../src/view/warehouse/inter/form.html | 77 + .../view/warehouse/inter/index-search.html | 52 + .../src/view/warehouse/inter/index.html | 74 + .../src/view/warehouse/inter/show-search.html | 52 + .../src/view/warehouse/inter/show.html | 62 + .../src/view/warehouse/outer/form.html | 68 + .../view/warehouse/outer/index-search.html | 59 + .../src/view/warehouse/outer/index.html | 91 + .../src/view/warehouse/outer/show-search.html | 52 + .../src/view/warehouse/outer/show.html | 62 + .../view/warehouse/relation/index-search.html | 36 + .../src/view/warehouse/relation/index.html | 82 + .../view/warehouse/replace/index-search.html | 45 + .../src/view/warehouse/replace/index.html | 53 + .../view/warehouse/stock/index-search.html | 23 + .../src/view/warehouse/stock/index.html | 55 + .../src/view/warehouse/stock/show.html | 62 + .../src/view/warehouse/user/index-search.html | 36 + .../src/view/warehouse}/user/index.html | 54 +- plugin/think-plugs-wuma/stc/bin/coder | Bin 0 -> 4935680 bytes plugin/think-plugs-wuma/stc/bin/coder.exe | Bin 0 -> 5037056 bytes .../20241010000010_install_wuma20241010.php | 868 + .../tests/CodeControllerTest.php | 138 + plugin/think-plugs-wuma/tests/CodeTest.php | 43 + .../tests/SalesUserControllerTest.php | 89 + route/.gitkeep | 0 1479 files changed, 175051 insertions(+), 6449 deletions(-) delete mode 100644 app/admin/Service.php delete mode 100644 app/admin/controller/Auth.php delete mode 100644 app/admin/controller/Base.php delete mode 100644 app/admin/controller/Config.php delete mode 100644 app/admin/controller/File.php delete mode 100644 app/admin/controller/Index.php delete mode 100644 app/admin/controller/Login.php delete mode 100644 app/admin/controller/Menu.php delete mode 100644 app/admin/controller/Queue.php delete mode 100644 app/admin/controller/User.php delete mode 100644 app/admin/controller/api/Queue.php delete mode 100644 app/admin/lang/en-us.php delete mode 100644 app/admin/route/demo.php delete mode 100644 app/admin/view/api/icon.html delete mode 100644 app/admin/view/api/upload/image.html delete mode 100644 app/admin/view/auth/form.html delete mode 100644 app/admin/view/auth/index.html delete mode 100644 app/admin/view/base/form.html delete mode 100644 app/admin/view/base/index.html delete mode 100644 app/admin/view/base/index_search.html delete mode 100644 app/admin/view/config/index.html delete mode 100644 app/admin/view/config/storage-0.html delete mode 100644 app/admin/view/config/storage-alioss.html delete mode 100644 app/admin/view/config/storage-alist.html delete mode 100644 app/admin/view/config/storage-local.html delete mode 100644 app/admin/view/config/storage-qiniu.html delete mode 100644 app/admin/view/config/storage-txcos.html delete mode 100644 app/admin/view/config/storage-upyun.html delete mode 100644 app/admin/view/config/system.html delete mode 100644 app/admin/view/file/form.html delete mode 100644 app/admin/view/file/index.html delete mode 100644 app/admin/view/file/index_search.html delete mode 100644 app/admin/view/full.html delete mode 100644 app/admin/view/index/index-top.html delete mode 100644 app/admin/view/index/theme.html delete mode 100644 app/admin/view/login/index.html delete mode 100644 app/admin/view/menu/form.html delete mode 100644 app/admin/view/menu/index.html delete mode 100644 app/admin/view/oplog/index.html delete mode 100644 app/admin/view/oplog/index_search.html delete mode 100644 app/admin/view/queue/index.html delete mode 100644 app/admin/view/queue/index_search.html delete mode 100644 app/admin/view/user/form.html delete mode 100644 app/admin/view/user/index_search.html delete mode 100644 app/admin/view/user/pass.html delete mode 100644 app/wechat/Service.php delete mode 100644 app/wechat/model/WechatAuto.php delete mode 100644 app/wechat/model/WechatKeys.php delete mode 100644 app/wechat/model/WechatNews.php delete mode 100644 app/wechat/model/WechatPaymentRecord.php delete mode 100644 app/wechat/model/WechatPaymentRefund.php delete mode 100644 app/wechat/service/WechatService.php delete mode 100644 app/wechat/view/config/options.html delete mode 100644 app/wechat/view/config/options_test.html delete mode 100644 app/wechat/view/config/payment_test.html create mode 100644 database/migrations/.published.json create mode 100644 plugin/think-library/.github/workflows/release.yml create mode 100644 plugin/think-library/.gitignore create mode 100644 plugin/think-library/.php-cs-fixer.php create mode 100644 plugin/think-library/composer.json create mode 100644 plugin/think-library/license create mode 100644 plugin/think-library/phpunit.xml.dist create mode 100644 plugin/think-library/readme.md create mode 100644 plugin/think-library/src/Builder.php create mode 100644 plugin/think-library/src/Command.php create mode 100644 plugin/think-library/src/Controller.php create mode 100644 plugin/think-library/src/Exception.php create mode 100644 plugin/think-library/src/Helper.php create mode 100644 plugin/think-library/src/Library.php create mode 100644 plugin/think-library/src/Model.php create mode 100644 plugin/think-library/src/Plugin.php create mode 100644 plugin/think-library/src/Service.php create mode 100644 plugin/think-library/src/Storage.php create mode 100644 plugin/think-library/src/builder/BuilderLang.php create mode 100644 plugin/think-library/src/builder/base/BuilderAttributeBag.php create mode 100644 plugin/think-library/src/builder/base/BuilderModule.php create mode 100644 plugin/think-library/src/builder/base/BuilderNode.php create mode 100644 plugin/think-library/src/builder/base/BuilderOptionSource.php create mode 100644 plugin/think-library/src/builder/base/render/BuilderActionRenderer.php create mode 100644 plugin/think-library/src/builder/base/render/BuilderAttributes.php create mode 100644 plugin/think-library/src/builder/base/render/BuilderAttributesRenderContext.php create mode 100644 plugin/think-library/src/builder/base/render/BuilderAttributesRenderer.php create mode 100644 plugin/think-library/src/builder/base/render/BuilderCallbackNodeRenderer.php create mode 100644 plugin/think-library/src/builder/base/render/BuilderElementNodeRenderer.php create mode 100644 plugin/think-library/src/builder/base/render/BuilderHtmlNodeRenderer.php create mode 100644 plugin/think-library/src/builder/base/render/BuilderNodeRenderContext.php create mode 100644 plugin/think-library/src/builder/base/render/BuilderNodeRendererFactory.php create mode 100644 plugin/think-library/src/builder/base/render/BuilderRenderPipeline.php create mode 100644 plugin/think-library/src/builder/base/render/BuilderRenderState.php create mode 100644 plugin/think-library/src/builder/base/render/InlineScriptRenderer.php create mode 100644 plugin/think-library/src/builder/base/render/JsonScriptRenderer.php create mode 100644 plugin/think-library/src/builder/form/FormActionBar.php create mode 100644 plugin/think-library/src/builder/form/FormActions.php create mode 100644 plugin/think-library/src/builder/form/FormBlocks.php create mode 100644 plugin/think-library/src/builder/form/FormBuilder.php create mode 100644 plugin/think-library/src/builder/form/FormButton.php create mode 100644 plugin/think-library/src/builder/form/FormChoiceField.php create mode 100644 plugin/think-library/src/builder/form/FormComponents.php create mode 100644 plugin/think-library/src/builder/form/FormField.php create mode 100644 plugin/think-library/src/builder/form/FormFieldOptions.php create mode 100644 plugin/think-library/src/builder/form/FormFieldPart.php create mode 100644 plugin/think-library/src/builder/form/FormFields.php create mode 100644 plugin/think-library/src/builder/form/FormLayout.php create mode 100644 plugin/think-library/src/builder/form/FormNode.php create mode 100644 plugin/think-library/src/builder/form/FormSelectField.php create mode 100644 plugin/think-library/src/builder/form/FormTextField.php create mode 100644 plugin/think-library/src/builder/form/FormUploadConfig.php create mode 100644 plugin/think-library/src/builder/form/FormUploadField.php create mode 100644 plugin/think-library/src/builder/form/component/AbstractFormComponent.php create mode 100644 plugin/think-library/src/builder/form/component/FormComponentInterface.php create mode 100644 plugin/think-library/src/builder/form/component/IntroComponent.php create mode 100644 plugin/think-library/src/builder/form/component/NoteComponent.php create mode 100644 plugin/think-library/src/builder/form/component/PickerFieldComponent.php create mode 100644 plugin/think-library/src/builder/form/component/ReadonlyFieldComponent.php create mode 100644 plugin/think-library/src/builder/form/component/SectionComponent.php create mode 100644 plugin/think-library/src/builder/form/component/ThemePaletteComponent.php create mode 100644 plugin/think-library/src/builder/form/module/FormModules.php create mode 100644 plugin/think-library/src/builder/form/render/AbstractFormFieldRenderer.php create mode 100644 plugin/think-library/src/builder/form/render/ChoiceFieldRenderer.php create mode 100644 plugin/think-library/src/builder/form/render/FormActionBarRenderer.php create mode 100644 plugin/think-library/src/builder/form/render/FormButtonNodeRenderer.php create mode 100644 plugin/think-library/src/builder/form/render/FormElementNodeRenderer.php create mode 100644 plugin/think-library/src/builder/form/render/FormFieldNodeRenderer.php create mode 100644 plugin/think-library/src/builder/form/render/FormFieldRenderContext.php create mode 100644 plugin/think-library/src/builder/form/render/FormFieldRendererFactory.php create mode 100644 plugin/think-library/src/builder/form/render/FormFieldRendererInterface.php create mode 100644 plugin/think-library/src/builder/form/render/FormHtmlNodeRenderer.php create mode 100644 plugin/think-library/src/builder/form/render/FormNodeRenderContext.php create mode 100644 plugin/think-library/src/builder/form/render/FormNodeRendererFactory.php create mode 100644 plugin/think-library/src/builder/form/render/FormNodeRendererInterface.php create mode 100644 plugin/think-library/src/builder/form/render/FormOptionRenderer.php create mode 100644 plugin/think-library/src/builder/form/render/FormRenderPipeline.php create mode 100644 plugin/think-library/src/builder/form/render/FormRenderState.php create mode 100644 plugin/think-library/src/builder/form/render/FormShellRenderer.php create mode 100644 plugin/think-library/src/builder/form/render/FormUploadRuntimeRenderer.php create mode 100644 plugin/think-library/src/builder/form/render/SelectFieldRenderer.php create mode 100644 plugin/think-library/src/builder/form/render/TextFieldRenderer.php create mode 100644 plugin/think-library/src/builder/form/render/UploadFieldRenderer.php create mode 100644 plugin/think-library/src/builder/page/PageAction.php create mode 100644 plugin/think-library/src/builder/page/PageActionNormalizer.php create mode 100644 plugin/think-library/src/builder/page/PageBlocks.php create mode 100644 plugin/think-library/src/builder/page/PageBuilder.php create mode 100644 plugin/think-library/src/builder/page/PageButtons.php create mode 100644 plugin/think-library/src/builder/page/PageColumn.php create mode 100644 plugin/think-library/src/builder/page/PageColumnNormalizer.php create mode 100644 plugin/think-library/src/builder/page/PageComponents.php create mode 100644 plugin/think-library/src/builder/page/PageLayout.php create mode 100644 plugin/think-library/src/builder/page/PageNode.php create mode 100644 plugin/think-library/src/builder/page/PagePresetColumn.php create mode 100644 plugin/think-library/src/builder/page/PageRowActions.php create mode 100644 plugin/think-library/src/builder/page/PageSearch.php create mode 100644 plugin/think-library/src/builder/page/PageSearchField.php create mode 100644 plugin/think-library/src/builder/page/PageSearchFieldNormalizer.php create mode 100644 plugin/think-library/src/builder/page/PageSearchOptions.php create mode 100644 plugin/think-library/src/builder/page/PageSortInputColumn.php create mode 100644 plugin/think-library/src/builder/page/PageStatusSwitchColumn.php create mode 100644 plugin/think-library/src/builder/page/PageTable.php create mode 100644 plugin/think-library/src/builder/page/PageTableNormalizer.php create mode 100644 plugin/think-library/src/builder/page/PageTableOptions.php create mode 100644 plugin/think-library/src/builder/page/PageToolbarColumn.php create mode 100644 plugin/think-library/src/builder/page/component/AbstractPageComponent.php create mode 100644 plugin/think-library/src/builder/page/component/ButtonGroupComponent.php create mode 100644 plugin/think-library/src/builder/page/component/CardComponent.php create mode 100644 plugin/think-library/src/builder/page/component/KeyValueTableComponent.php create mode 100644 plugin/think-library/src/builder/page/component/KvGridComponent.php create mode 100644 plugin/think-library/src/builder/page/component/PageComponentInterface.php create mode 100644 plugin/think-library/src/builder/page/component/ParagraphsComponent.php create mode 100644 plugin/think-library/src/builder/page/component/ReadonlyFieldsComponent.php create mode 100644 plugin/think-library/src/builder/page/module/PageModules.php create mode 100644 plugin/think-library/src/builder/page/render/AbstractPageSearchFieldRenderer.php create mode 100644 plugin/think-library/src/builder/page/render/PageBootScriptRenderer.php create mode 100644 plugin/think-library/src/builder/page/render/PageContentRenderer.php create mode 100644 plugin/think-library/src/builder/page/render/PageCustomScriptRenderer.php create mode 100644 plugin/think-library/src/builder/page/render/PageElementNodeRenderer.php create mode 100644 plugin/think-library/src/builder/page/render/PageHeaderRenderer.php create mode 100644 plugin/think-library/src/builder/page/render/PageHtmlNodeRenderer.php create mode 100644 plugin/think-library/src/builder/page/render/PageInitScriptRenderer.php create mode 100644 plugin/think-library/src/builder/page/render/PageNodeRenderContext.php create mode 100644 plugin/think-library/src/builder/page/render/PageNodeRendererFactory.php create mode 100644 plugin/think-library/src/builder/page/render/PageNodeRendererInterface.php create mode 100644 plugin/think-library/src/builder/page/render/PageNoticeRenderer.php create mode 100644 plugin/think-library/src/builder/page/render/PageReadyScriptRenderer.php create mode 100644 plugin/think-library/src/builder/page/render/PageRenderPipeline.php create mode 100644 plugin/think-library/src/builder/page/render/PageRenderState.php create mode 100644 plugin/think-library/src/builder/page/render/PageScriptRenderContext.php create mode 100644 plugin/think-library/src/builder/page/render/PageScriptRenderer.php create mode 100644 plugin/think-library/src/builder/page/render/PageSearchFieldRendererFactory.php create mode 100644 plugin/think-library/src/builder/page/render/PageSearchFieldRendererInterface.php create mode 100644 plugin/think-library/src/builder/page/render/PageSearchHiddenFieldRenderer.php create mode 100644 plugin/think-library/src/builder/page/render/PageSearchInputFieldRenderer.php create mode 100644 plugin/think-library/src/builder/page/render/PageSearchNodeRenderer.php create mode 100644 plugin/think-library/src/builder/page/render/PageSearchRenderContext.php create mode 100644 plugin/think-library/src/builder/page/render/PageSearchRenderer.php create mode 100644 plugin/think-library/src/builder/page/render/PageSearchSelectFieldRenderer.php create mode 100644 plugin/think-library/src/builder/page/render/PageSearchSubmitFieldRenderer.php create mode 100644 plugin/think-library/src/builder/page/render/PageShellRenderer.php create mode 100644 plugin/think-library/src/builder/page/render/PageTableInitScriptRenderer.php create mode 100644 plugin/think-library/src/builder/page/render/PageTableNodeRenderer.php create mode 100644 plugin/think-library/src/builder/page/render/PageTableRenderContext.php create mode 100644 plugin/think-library/src/builder/page/render/PageTableRenderer.php create mode 100644 plugin/think-library/src/builder/page/render/PageTemplateRenderer.php create mode 100644 plugin/think-library/src/builder/page/render/PageTemplateScriptRenderer.php create mode 100644 plugin/think-library/src/builder/page/render/PageToolbarTemplateRenderer.php create mode 100644 plugin/think-library/src/common.php create mode 100644 plugin/think-library/src/contract/QueueHandlerInterface.php create mode 100644 plugin/think-library/src/contract/QueueManagerInterface.php create mode 100644 plugin/think-library/src/contract/QueueRuntimeInterface.php create mode 100644 plugin/think-library/src/contract/StorageInterface.php create mode 100644 plugin/think-library/src/contract/StorageManagerInterface.php create mode 100644 plugin/think-library/src/contract/SystemContextInterface.php create mode 100644 plugin/think-library/src/extend/ArrayTree.php create mode 100644 plugin/think-library/src/extend/CodeExtend.php create mode 100644 plugin/think-library/src/extend/CodeToolkit.php create mode 100644 plugin/think-library/src/extend/DataExtend.php create mode 100644 plugin/think-library/src/extend/FileTools.php create mode 100644 plugin/think-library/src/extend/HttpClient.php create mode 100644 plugin/think-library/src/extend/JsonRpcClient.php create mode 100644 plugin/think-library/src/helper/DeleteHelper.php create mode 100644 plugin/think-library/src/helper/FormHelper.php create mode 100644 plugin/think-library/src/helper/QueryHelper.php create mode 100644 plugin/think-library/src/helper/SaveHelper.php create mode 100644 plugin/think-library/src/helper/ValidateHelper.php create mode 100644 plugin/think-library/src/middleware/MultAccess.php create mode 100644 plugin/think-library/src/model/ModelFactory.php create mode 100644 plugin/think-library/src/model/QueryFactory.php create mode 100644 plugin/think-library/src/model/RuntimeModel.php create mode 100644 plugin/think-library/src/model/SystemConfig.php create mode 100644 plugin/think-library/src/route/Route.php create mode 100644 plugin/think-library/src/route/Url.php create mode 100644 plugin/think-library/src/runtime/NullSystemContext.php create mode 100644 plugin/think-library/src/runtime/RequestContext.php create mode 100644 plugin/think-library/src/runtime/RequestTokenService.php create mode 100644 plugin/think-library/src/runtime/SystemContext.php create mode 100644 plugin/think-library/src/service/AppService.php create mode 100644 plugin/think-library/src/service/AuthResponse.php create mode 100644 plugin/think-library/src/service/CacheSession.php create mode 100644 plugin/think-library/src/service/FaviconBuilder.php create mode 100644 plugin/think-library/src/service/ImageSliderVerify.php create mode 100644 plugin/think-library/src/service/JsonRpcHttpClient.php create mode 100644 plugin/think-library/src/service/JsonRpcHttpServer.php create mode 100644 plugin/think-library/src/service/JwtToken.php create mode 100644 plugin/think-library/src/service/ModuleService.php create mode 100644 plugin/think-library/src/service/NodeService.php create mode 100644 plugin/think-library/src/service/QueueService.php create mode 100644 plugin/think-library/src/service/ResponseModeService.php create mode 100644 plugin/think-library/src/service/RuntimeService.php create mode 100644 plugin/think-library/tests/AppServiceTest.php create mode 100644 plugin/think-library/tests/ArchitectureBoundaryTest.php create mode 100644 plugin/think-library/tests/CodeTest.php create mode 100644 plugin/think-library/tests/CommonFunctionsTest.php create mode 100644 plugin/think-library/tests/ComposerDependencyBoundaryTest.php create mode 100644 plugin/think-library/tests/ComposerInstallBoundaryTest.php create mode 100644 plugin/think-library/tests/FormBuilderTest.php create mode 100644 plugin/think-library/tests/JwtTest.php create mode 100644 plugin/think-library/tests/MigrationOwnershipTest.php create mode 100644 plugin/think-library/tests/ModelTest.php create mode 100644 plugin/think-library/tests/MultAccessDispatchTest.php create mode 100644 plugin/think-library/tests/PageBuilderTest.php create mode 100644 plugin/think-library/tests/PasswordMaskTest.php create mode 100644 plugin/think-library/tests/RequestTokenServiceTest.php create mode 100644 plugin/think-library/tests/RouteTemplateBoundaryTest.php create mode 100644 plugin/think-library/tests/SoftDeleteBoundaryTest.php create mode 100644 plugin/think-library/tests/bootstrap.php create mode 100644 plugin/think-plugs-account/.gitattributes create mode 100644 plugin/think-plugs-account/.github/workflows/release.yml create mode 100644 plugin/think-plugs-account/.gitignore create mode 100644 plugin/think-plugs-account/.php-cs-fixer.php create mode 100644 plugin/think-plugs-account/composer.json create mode 100644 plugin/think-plugs-account/phpunit.xml.dist create mode 100644 plugin/think-plugs-account/readme.api.md create mode 100644 plugin/think-plugs-account/readme.md create mode 100644 plugin/think-plugs-account/src/Service.php create mode 100644 plugin/think-plugs-account/src/controller/Device.php create mode 100644 plugin/think-plugs-account/src/controller/Master.php create mode 100644 plugin/think-plugs-account/src/controller/Message.php create mode 100644 plugin/think-plugs-account/src/controller/api/Auth.php create mode 100644 plugin/think-plugs-account/src/controller/api/Login.php create mode 100644 plugin/think-plugs-account/src/controller/api/Wechat.php create mode 100644 plugin/think-plugs-account/src/controller/api/Wxapp.php create mode 100644 plugin/think-plugs-account/src/controller/api/auth/Center.php create mode 100644 plugin/think-plugs-account/src/lang/en-us.php create mode 100644 plugin/think-plugs-account/src/model/Abs.php create mode 100644 plugin/think-plugs-account/src/model/PlainAbs.php create mode 100644 plugin/think-plugs-account/src/model/PluginAccountAuth.php create mode 100644 plugin/think-plugs-account/src/model/PluginAccountBind.php create mode 100644 plugin/think-plugs-account/src/model/PluginAccountMsms.php create mode 100644 plugin/think-plugs-account/src/model/PluginAccountUser.php create mode 100644 plugin/think-plugs-account/src/service/Account.php create mode 100644 plugin/think-plugs-account/src/service/Message.php create mode 100644 plugin/think-plugs-account/src/service/WxappService.php create mode 100644 plugin/think-plugs-account/src/service/contract/AccountAccess.php create mode 100644 plugin/think-plugs-account/src/service/contract/AccountInterface.php create mode 100644 plugin/think-plugs-account/src/service/contract/MessageInterface.php create mode 100644 plugin/think-plugs-account/src/service/contract/MessageUsageTrait.php create mode 100644 plugin/think-plugs-account/src/service/message/Alisms.php create mode 100644 plugin/think-plugs-account/src/view/device/index.html create mode 100644 plugin/think-plugs-account/src/view/device/index_search.html create mode 100644 plugin/think-plugs-account/src/view/device/types.html create mode 100644 plugin/think-plugs-account/src/view/master/index.html create mode 100644 plugin/think-plugs-account/src/view/master/index_search.html create mode 100644 plugin/think-plugs-account/src/view/message/index.html create mode 100644 plugin/think-plugs-account/src/view/message/index_search.html rename {app/wechat => plugin/think-plugs-account/src}/view/table.html (87%) create mode 100644 plugin/think-plugs-account/stc/database/20241010000005_install_account20241010.php create mode 100644 plugin/think-plugs-account/tests/AccountAdminListControllerTest.php create mode 100644 plugin/think-plugs-account/tests/AccountCenterControllerTest.php create mode 100644 plugin/think-plugs-account/tests/AccountIntegrationTest.php create mode 100644 plugin/think-plugs-account/tests/AccountLoginControllerTest.php create mode 100644 plugin/think-plugs-account/tests/AccountMessageControllerTest.php create mode 100644 plugin/think-plugs-account/tests/AccountRuntimeTest.php create mode 100644 plugin/think-plugs-account/tests/bootstrap.php create mode 100644 plugin/think-plugs-builder/composer.json create mode 100644 plugin/think-plugs-builder/readme.api.md create mode 100644 plugin/think-plugs-builder/readme.md create mode 100644 plugin/think-plugs-builder/src/Service.php create mode 100644 plugin/think-plugs-builder/src/command/Build.php create mode 100644 plugin/think-plugs-builder/src/service/PharBuilder.php create mode 100644 plugin/think-plugs-builder/src/service/PharRuntime.php create mode 100644 plugin/think-plugs-install/composer.json create mode 100644 plugin/think-plugs-install/readme.api.md create mode 100644 plugin/think-plugs-install/readme.md create mode 100644 plugin/think-plugs-install/src/Service.php create mode 100644 plugin/think-plugs-install/src/command/project/InstallCommand.php create mode 100644 plugin/think-plugs-install/src/composer/Plugin.php create mode 100644 plugin/think-plugs-install/src/service/ComposerLifecycleService.php create mode 100644 plugin/think-plugs-install/tests/ComposerLifecycleServiceTest.php create mode 100644 plugin/think-plugs-install/tests/InstallCommandTest.php create mode 100644 plugin/think-plugs-install/tests/bootstrap.php create mode 100644 plugin/think-plugs-payment/.gitattributes create mode 100644 plugin/think-plugs-payment/.github/workflows/release.yml create mode 100644 plugin/think-plugs-payment/.gitignore create mode 100644 plugin/think-plugs-payment/.php-cs-fixer.php create mode 100644 plugin/think-plugs-payment/composer.json create mode 100644 plugin/think-plugs-payment/phpunit.xml.dist create mode 100644 plugin/think-plugs-payment/readme.api.md create mode 100644 plugin/think-plugs-payment/readme.md create mode 100644 plugin/think-plugs-payment/src/Service.php create mode 100644 plugin/think-plugs-payment/src/controller/Balance.php create mode 100644 plugin/think-plugs-payment/src/controller/Config.php create mode 100644 plugin/think-plugs-payment/src/controller/Integral.php create mode 100644 plugin/think-plugs-payment/src/controller/Record.php create mode 100644 plugin/think-plugs-payment/src/controller/Refund.php create mode 100644 plugin/think-plugs-payment/src/controller/api/auth/Address.php create mode 100644 plugin/think-plugs-payment/src/controller/api/auth/Balance.php create mode 100644 plugin/think-plugs-payment/src/controller/api/auth/Integral.php create mode 100644 plugin/think-plugs-payment/src/lang/en-us.php create mode 100644 plugin/think-plugs-payment/src/model/PluginPaymentAddress.php create mode 100644 plugin/think-plugs-payment/src/model/PluginPaymentBalance.php create mode 100644 plugin/think-plugs-payment/src/model/PluginPaymentConfig.php create mode 100644 plugin/think-plugs-payment/src/model/PluginPaymentIntegral.php create mode 100644 plugin/think-plugs-payment/src/model/PluginPaymentRecord.php create mode 100644 plugin/think-plugs-payment/src/model/PluginPaymentRefund.php create mode 100644 plugin/think-plugs-payment/src/service/Balance.php create mode 100644 plugin/think-plugs-payment/src/service/Integral.php create mode 100644 plugin/think-plugs-payment/src/service/Payment.php create mode 100644 plugin/think-plugs-payment/src/service/Recount.php create mode 100644 plugin/think-plugs-payment/src/service/contract/PaymentInterface.php create mode 100644 plugin/think-plugs-payment/src/service/contract/PaymentResponse.php create mode 100644 plugin/think-plugs-payment/src/service/contract/PaymentUsageTrait.php create mode 100644 plugin/think-plugs-payment/src/service/payment/AliPayment.php create mode 100644 plugin/think-plugs-payment/src/service/payment/BalancePayment.php create mode 100644 plugin/think-plugs-payment/src/service/payment/CouponPayment.php create mode 100644 plugin/think-plugs-payment/src/service/payment/EmptyPayment.php create mode 100644 plugin/think-plugs-payment/src/service/payment/IntegralPayment.php create mode 100644 plugin/think-plugs-payment/src/service/payment/JoinPayment.php create mode 100644 plugin/think-plugs-payment/src/service/payment/VoucherPayment.php create mode 100644 plugin/think-plugs-payment/src/service/payment/WechatPayment.php create mode 100644 plugin/think-plugs-payment/src/service/payment/wechat/WechatPaymentV2.php create mode 100644 plugin/think-plugs-payment/src/service/payment/wechat/WechatPaymentV3.php create mode 100644 plugin/think-plugs-payment/src/view/balance/form.html create mode 100644 plugin/think-plugs-payment/src/view/balance/index.html create mode 100644 plugin/think-plugs-payment/src/view/balance/index_search.html create mode 100644 plugin/think-plugs-payment/src/view/config/form.html create mode 100644 plugin/think-plugs-payment/src/view/config/form_alipay.html create mode 100644 plugin/think-plugs-payment/src/view/config/form_joinpay.html create mode 100644 plugin/think-plugs-payment/src/view/config/form_voucher.html create mode 100644 plugin/think-plugs-payment/src/view/config/form_wechat.html create mode 100644 plugin/think-plugs-payment/src/view/config/index.html create mode 100644 plugin/think-plugs-payment/src/view/config/index_search.html create mode 100644 plugin/think-plugs-payment/src/view/config/types.html create mode 100644 plugin/think-plugs-payment/src/view/integral/form.html create mode 100644 plugin/think-plugs-payment/src/view/integral/index.html create mode 100644 plugin/think-plugs-payment/src/view/integral/index_search.html rename {app/wechat => plugin/think-plugs-payment/src}/view/main.html (87%) create mode 100644 plugin/think-plugs-payment/src/view/record/index.html create mode 100644 plugin/think-plugs-payment/src/view/record/index_search.html create mode 100644 plugin/think-plugs-payment/src/view/refund/index.html create mode 100644 plugin/think-plugs-payment/src/view/refund/index_search.html create mode 100644 plugin/think-plugs-payment/src/view/table.html create mode 100644 plugin/think-plugs-payment/stc/database/20241010000006_install_payment20241010.php create mode 100644 plugin/think-plugs-payment/tests/.bootstrap.php create mode 100644 plugin/think-plugs-payment/tests/BalanceIntegrationTest.php create mode 100644 plugin/think-plugs-payment/tests/IntegralIntegrationTest.php create mode 100644 plugin/think-plugs-payment/tests/PaymentAddressControllerTest.php create mode 100644 plugin/think-plugs-payment/tests/PaymentLedgerControllerTest.php create mode 100644 plugin/think-plugs-payment/tests/PaymentLedgerIntegrationTest.php create mode 100644 plugin/think-plugs-payment/tests/PaymentRecordControllerTest.php create mode 100644 plugin/think-plugs-payment/tests/PaymentRecordIntegrationTest.php create mode 100644 plugin/think-plugs-payment/tests/PaymentRecountServiceTest.php create mode 100644 plugin/think-plugs-payment/tests/PaymentTest.php create mode 100644 plugin/think-plugs-static/.github/workflows/release.yml create mode 100644 plugin/think-plugs-static/.gitignore create mode 100644 plugin/think-plugs-static/.php-cs-fixer.php create mode 100644 plugin/think-plugs-static/composer.json create mode 100644 plugin/think-plugs-static/license create mode 100644 plugin/think-plugs-static/readme.api.md create mode 100644 plugin/think-plugs-static/readme.md create mode 100644 plugin/think-plugs-static/stc/.env.example create mode 100644 plugin/think-plugs-static/stc/config/app.php create mode 100644 plugin/think-plugs-static/stc/config/cache.php create mode 100644 plugin/think-plugs-static/stc/config/cookie.php create mode 100644 plugin/think-plugs-static/stc/config/database.php create mode 100644 plugin/think-plugs-static/stc/config/lang.php create mode 100644 plugin/think-plugs-static/stc/config/log.php create mode 100644 plugin/think-plugs-static/stc/config/phinx.php create mode 100644 plugin/think-plugs-static/stc/config/route.php create mode 100644 plugin/think-plugs-static/stc/config/view.php create mode 100644 plugin/think-plugs-static/stc/default/Index.php create mode 100644 plugin/think-plugs-static/stc/public/.htaccess create mode 100644 plugin/think-plugs-static/stc/public/index.php create mode 100644 plugin/think-plugs-static/stc/public/robots.txt create mode 100644 plugin/think-plugs-static/stc/public/router.php create mode 100644 plugin/think-plugs-static/stc/public/static/extra/script.js create mode 100644 plugin/think-plugs-static/stc/public/static/extra/style.css create mode 100644 plugin/think-plugs-static/stc/public/static/login.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/angular/angular.min.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/ckeditor.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/config.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/contents.css create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/lang/en.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/lang/zh-cn.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/lang/zh.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/a11yhelp/dialogs/a11yhelp.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/a11yhelp/dialogs/lang/en.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/a11yhelp/dialogs/lang/zh-cn.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/a11yhelp/dialogs/lang/zh.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/about/dialogs/about.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/about/dialogs/hidpi/logo_ckeditor.png create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/about/dialogs/logo_ckeditor.png create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/clipboard/dialogs/paste.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/colordialog/dialogs/colordialog.css create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/colordialog/dialogs/colordialog.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/copyformatting/cursors/cursor-disabled.svg create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/copyformatting/cursors/cursor.svg create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/copyformatting/styles/copyformatting.css create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/dialog/dialogDefinition.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/dialog/styles/dialog.css create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/div/dialogs/div.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/exportpdf/plugindefinition.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/find/dialogs/find.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/forms/dialogs/button.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/forms/dialogs/checkbox.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/forms/dialogs/form.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/forms/dialogs/hiddenfield.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/forms/dialogs/radio.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/forms/dialogs/select.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/forms/dialogs/textarea.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/forms/dialogs/textfield.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/forms/images/hiddenfield.gif create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/icons.png create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/icons_hidpi.png create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/iframe/dialogs/iframe.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/iframe/images/placeholder.png create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/image/dialogs/image.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/image/images/noimage.png create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/lineheight/lang/en.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/lineheight/lang/zh-cn.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/lineheight/lang/zh.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/lineheight/plugin.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/link/dialogs/anchor.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/link/dialogs/link.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/link/images/anchor.png create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/link/images/hidpi/anchor.png create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/liststyle/dialogs/liststyle.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/magicline/images/hidpi/icon-rtl.png create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/magicline/images/hidpi/icon.png create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/magicline/images/icon-rtl.png create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/magicline/images/icon.png create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/pagebreak/images/pagebreak.gif create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/pastefromgdocs/filter/default.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/pastefromlibreoffice/filter/default.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/pastefromword/filter/default.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/pastetools/filter/common.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/pastetools/filter/image.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/preview/images/pagebreak.gif create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/preview/preview.html create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/preview/styles/screen.css create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/scayt/dialogs/dialog.css create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/scayt/dialogs/options.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/scayt/dialogs/toolbar.css create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/scayt/skins/moono-lisa/scayt.css create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/showblocks/images/block_address.png create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/showblocks/images/block_blockquote.png create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/showblocks/images/block_div.png create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/showblocks/images/block_h1.png create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/showblocks/images/block_h2.png create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/showblocks/images/block_h3.png create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/showblocks/images/block_h4.png create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/showblocks/images/block_h5.png create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/showblocks/images/block_h6.png create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/showblocks/images/block_p.png create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/showblocks/images/block_pre.png create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/smiley/dialogs/smiley.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/smiley/images/angel_smile.gif create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/smiley/images/angel_smile.png create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/smiley/images/angry_smile.gif create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/smiley/images/angry_smile.png create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/smiley/images/broken_heart.gif create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/smiley/images/broken_heart.png create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/smiley/images/confused_smile.gif create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/smiley/images/confused_smile.png create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/smiley/images/cry_smile.gif create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/smiley/images/cry_smile.png create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/smiley/images/devil_smile.gif create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/smiley/images/devil_smile.png create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/smiley/images/embaressed_smile.gif create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/smiley/images/embarrassed_smile.gif create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/smiley/images/embarrassed_smile.png create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/smiley/images/envelope.gif create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/smiley/images/envelope.png create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/smiley/images/heart.gif create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/smiley/images/heart.png create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/smiley/images/kiss.gif create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/smiley/images/kiss.png create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/smiley/images/lightbulb.gif create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/smiley/images/lightbulb.png create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/smiley/images/omg_smile.gif create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/smiley/images/omg_smile.png create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/smiley/images/regular_smile.gif create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/smiley/images/regular_smile.png create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/smiley/images/sad_smile.gif create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/smiley/images/sad_smile.png create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/smiley/images/shades_smile.gif create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/smiley/images/shades_smile.png create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/smiley/images/teeth_smile.gif create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/smiley/images/teeth_smile.png create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/smiley/images/thumbs_down.gif create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/smiley/images/thumbs_down.png create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/smiley/images/thumbs_up.gif create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/smiley/images/thumbs_up.png create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/smiley/images/tongue_smile.gif create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/smiley/images/tongue_smile.png create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/smiley/images/tounge_smile.gif create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/smiley/images/whatchutalkingabout_smile.gif create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/smiley/images/whatchutalkingabout_smile.png create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/smiley/images/wink_smile.gif create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/smiley/images/wink_smile.png create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/specialchar/dialogs/lang/en.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/specialchar/dialogs/lang/zh-cn.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/specialchar/dialogs/lang/zh.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/specialchar/dialogs/specialchar.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/table/dialogs/table.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/tableselection/styles/tableselection.css create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/tabletools/dialogs/tableCell.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/templates/dialogs/templates.css create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/templates/dialogs/templates.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/templates/templatedefinition.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/templates/templates/default.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/templates/templates/images/template1.gif create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/templates/templates/images/template2.gif create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/templates/templates/images/template3.gif create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/widget/images/handle.png create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/wsc/dialogs/ciframe.html create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/wsc/dialogs/tmpFrameset.html create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/wsc/dialogs/wsc.css create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/wsc/dialogs/wsc.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/wsc/dialogs/wsc_ie.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/wsc/icons/hidpi/spellchecker.png create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/wsc/icons/spellchecker.png create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/wsc/lang/en.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/wsc/lang/zh-cn.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/wsc/lang/zh.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/wsc/plugin.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/plugins/wsc/skins/moono-lisa/wsc.css create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/skins/moono-lisa/dialog.css create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/skins/moono-lisa/dialog_ie.css create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/skins/moono-lisa/dialog_ie8.css create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/skins/moono-lisa/dialog_iequirks.css create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/skins/moono-lisa/editor.css create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/skins/moono-lisa/editor_gecko.css create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/skins/moono-lisa/editor_ie.css create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/skins/moono-lisa/editor_ie8.css create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/skins/moono-lisa/editor_iequirks.css create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/skins/moono-lisa/icons.png create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/skins/moono-lisa/icons_hidpi.png create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/skins/moono-lisa/images/arrow.png create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/skins/moono-lisa/images/close.png create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/skins/moono-lisa/images/hidpi/close.png create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/skins/moono-lisa/images/hidpi/lock-open.png create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/skins/moono-lisa/images/hidpi/lock.png create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/skins/moono-lisa/images/hidpi/refresh.png create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/skins/moono-lisa/images/lock-open.png create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/skins/moono-lisa/images/lock.png create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/skins/moono-lisa/images/refresh.png create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/skins/moono-lisa/images/spinner.gif create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/skins/moono-lisa/readme.md create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor4/styles.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor5/ckeditor.css create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor5/ckeditor.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor5/content.css create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor5/translations/af.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor5/translations/ar.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor5/translations/ast.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor5/translations/az.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor5/translations/bg.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor5/translations/bn.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor5/translations/bs.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor5/translations/ca.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor5/translations/cs.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor5/translations/da.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor5/translations/de-ch.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor5/translations/de.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor5/translations/el.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor5/translations/en-au.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor5/translations/en-gb.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor5/translations/en.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor5/translations/eo.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor5/translations/es-co.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor5/translations/es.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor5/translations/et.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor5/translations/eu.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor5/translations/fa.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor5/translations/fi.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor5/translations/fr.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor5/translations/gl.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor5/translations/gu.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor5/translations/he.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor5/translations/hi.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor5/translations/hr.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor5/translations/hu.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor5/translations/hy.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor5/translations/id.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor5/translations/it.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor5/translations/ja.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor5/translations/jv.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor5/translations/kk.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor5/translations/km.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor5/translations/kn.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor5/translations/ko.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor5/translations/ku.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor5/translations/lt.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor5/translations/lv.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor5/translations/ms.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor5/translations/nb.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor5/translations/ne.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor5/translations/nl.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor5/translations/no.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor5/translations/oc.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor5/translations/pl.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor5/translations/pt-br.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor5/translations/pt.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor5/translations/ro.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor5/translations/ru.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor5/translations/si.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor5/translations/sk.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor5/translations/sl.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor5/translations/sq.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor5/translations/sr-latn.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor5/translations/sr.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor5/translations/sv.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor5/translations/th.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor5/translations/tk.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor5/translations/tr.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor5/translations/tt.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor5/translations/ug.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor5/translations/uk.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor5/translations/ur.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor5/translations/uz.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor5/translations/vi.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ckeditor5/translations/zh.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/cropper/cropper.min.css create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/cropper/cropper.min.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/echarts/echarts.min.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/editor/create.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/editor/css/style.css create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/editor/index.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/jquery/area/area.php create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/jquery/area/data.json create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/jquery/artplayer.min.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/jquery/autocompleter.css create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/jquery/autocompleter.min.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/jquery/base64.min.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/jquery/compressor.min.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/jquery/filesaver.min.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/jquery/jquery.min.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/jquery/json.min.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/jquery/jszip.min.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/jquery/less.min.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/jquery/marked.min.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/jquery/masonry.min.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/jquery/md5.min.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/jquery/pace.min.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/jquery/pcasunzips.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/jquery/xlsx.min.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/layui/css/layui.css create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/layui/font/iconfont.eot create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/layui/font/iconfont.svg create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/layui/font/iconfont.ttf create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/layui/font/iconfont.woff create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/layui/font/iconfont.woff2 create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/layui/layui.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/layui_exts/excel.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/layui_exts/layCascader.css create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/layui_exts/layCascader.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/layui_exts/tableSelect.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/layui_exts/xmSelect.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/notify/img/danger-outline.svg create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/notify/img/danger.png create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/notify/img/default-outline.svg create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/notify/img/default.png create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/notify/img/info-outline.svg create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/notify/img/info.png create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/notify/img/success-outline.svg create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/notify/img/success.png create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/notify/img/warning-outline.svg create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/notify/img/warning.png create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/notify/notify.min.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/notify/theme.css create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/socket/swfobject.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/socket/websocket.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/socket/websocket.swf create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/sortable/sortable.min.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/sortable/vue.draggable.min.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/system/excel.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/system/queue.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/system/validate.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/vue/vue.min.js create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ztree/zTreeStyle/img/diy/1_close.png create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ztree/zTreeStyle/img/diy/1_open.png create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ztree/zTreeStyle/img/diy/2.png create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ztree/zTreeStyle/img/diy/3.png create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ztree/zTreeStyle/img/diy/4.png create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ztree/zTreeStyle/img/diy/5.png create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ztree/zTreeStyle/img/diy/6.png create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ztree/zTreeStyle/img/diy/7.png create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ztree/zTreeStyle/img/diy/8.png create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ztree/zTreeStyle/img/diy/9.png create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ztree/zTreeStyle/img/line_conn.gif create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ztree/zTreeStyle/img/loading.gif create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ztree/zTreeStyle/img/zTreeStandard.gif create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ztree/zTreeStyle/img/zTreeStandard.png create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ztree/zTreeStyle/zTreeStyle.css create mode 100644 plugin/think-plugs-static/stc/public/static/plugs/ztree/ztree.all.min.js create mode 100644 plugin/think-plugs-static/stc/public/static/system.js create mode 100644 plugin/think-plugs-static/stc/public/static/theme/css/_config.less create mode 100644 plugin/think-plugs-static/stc/public/static/theme/css/_custom.less create mode 100644 plugin/think-plugs-static/stc/public/static/theme/css/_display.less create mode 100644 plugin/think-plugs-static/stc/public/static/theme/css/_layout.less create mode 100644 plugin/think-plugs-static/stc/public/static/theme/css/_layout_1.less create mode 100644 plugin/think-plugs-static/stc/public/static/theme/css/_layout_1_amber.less create mode 100644 plugin/think-plugs-static/stc/public/static/theme/css/_layout_1_black.less create mode 100644 plugin/think-plugs-static/stc/public/static/theme/css/_layout_1_blue.less create mode 100644 plugin/think-plugs-static/stc/public/static/theme/css/_layout_1_glacier.less create mode 100644 plugin/think-plugs-static/stc/public/static/theme/css/_layout_1_green.less create mode 100644 plugin/think-plugs-static/stc/public/static/theme/css/_layout_1_indigo.less create mode 100644 plugin/think-plugs-static/stc/public/static/theme/css/_layout_1_lime.less create mode 100644 plugin/think-plugs-static/stc/public/static/theme/css/_layout_1_navy.less create mode 100644 plugin/think-plugs-static/stc/public/static/theme/css/_layout_1_red.less create mode 100644 plugin/think-plugs-static/stc/public/static/theme/css/_layout_1_rose.less create mode 100644 plugin/think-plugs-static/stc/public/static/theme/css/_layout_1_violet.less create mode 100644 plugin/think-plugs-static/stc/public/static/theme/css/_layout_2.less create mode 100644 plugin/think-plugs-static/stc/public/static/theme/css/_layout_2_black.less create mode 100644 plugin/think-plugs-static/stc/public/static/theme/css/_layout_2_blue.less create mode 100644 plugin/think-plugs-static/stc/public/static/theme/css/_layout_2_glacier.less create mode 100644 plugin/think-plugs-static/stc/public/static/theme/css/_layout_2_green.less create mode 100644 plugin/think-plugs-static/stc/public/static/theme/css/_layout_2_indigo.less create mode 100644 plugin/think-plugs-static/stc/public/static/theme/css/_layout_2_lime.less create mode 100644 plugin/think-plugs-static/stc/public/static/theme/css/_layout_2_ocean.less create mode 100644 plugin/think-plugs-static/stc/public/static/theme/css/_layout_2_red.less create mode 100644 plugin/think-plugs-static/stc/public/static/theme/css/_layout_2_rose.less create mode 100644 plugin/think-plugs-static/stc/public/static/theme/css/_layout_2_slate.less create mode 100644 plugin/think-plugs-static/stc/public/static/theme/css/_layout_2_sunset.less create mode 100644 plugin/think-plugs-static/stc/public/static/theme/css/_layout_white.less create mode 100644 plugin/think-plugs-static/stc/public/static/theme/css/console.css create mode 100644 plugin/think-plugs-static/stc/public/static/theme/css/console.css.map create mode 100644 plugin/think-plugs-static/stc/public/static/theme/css/console.less create mode 100644 plugin/think-plugs-static/stc/public/static/theme/css/font/iconfont.ttf create mode 100644 plugin/think-plugs-static/stc/public/static/theme/css/font/iconfont.woff create mode 100644 plugin/think-plugs-static/stc/public/static/theme/css/font/iconfont.woff2 create mode 100644 plugin/think-plugs-static/stc/public/static/theme/css/iconfont.css create mode 100644 plugin/think-plugs-static/stc/public/static/theme/css/iconfont.css.map create mode 100644 plugin/think-plugs-static/stc/public/static/theme/css/iconfont.less create mode 100644 plugin/think-plugs-static/stc/public/static/theme/css/login.css create mode 100644 plugin/think-plugs-static/stc/public/static/theme/css/login.css.map create mode 100644 plugin/think-plugs-static/stc/public/static/theme/css/login.less create mode 100644 plugin/think-plugs-static/stc/public/static/theme/css/mobile.css create mode 100644 plugin/think-plugs-static/stc/public/static/theme/css/mobile.css.map create mode 100644 plugin/think-plugs-static/stc/public/static/theme/css/mobile.less create mode 100644 plugin/think-plugs-static/stc/public/static/theme/css/package.json create mode 100644 plugin/think-plugs-static/stc/public/static/theme/err/404.html create mode 100644 plugin/think-plugs-static/stc/public/static/theme/err/404/404.png create mode 100644 plugin/think-plugs-static/stc/public/static/theme/err/404/reset.css create mode 100644 plugin/think-plugs-static/stc/public/static/theme/err/404/style.css create mode 100644 plugin/think-plugs-static/stc/public/static/theme/err/500.html create mode 100644 plugin/think-plugs-static/stc/public/static/theme/err/500/style.css create mode 100644 plugin/think-plugs-static/stc/public/static/theme/img/404_icon.png create mode 100644 plugin/think-plugs-static/stc/public/static/theme/img/505_icon.png create mode 100644 plugin/think-plugs-static/stc/public/static/theme/img/headimg.png create mode 100644 plugin/think-plugs-static/stc/public/static/theme/img/image.png create mode 100644 plugin/think-plugs-static/stc/public/static/theme/img/login/bg1.jpg create mode 100644 plugin/think-plugs-static/stc/public/static/theme/img/login/bg2.jpg create mode 100644 plugin/think-plugs-static/stc/public/static/theme/img/upimg.png create mode 100644 plugin/think-plugs-static/stc/public/static/theme/img/upvideo.png create mode 100644 plugin/think-plugs-static/stc/public/static/theme/img/wechat/index.png create mode 100644 plugin/think-plugs-static/stc/public/static/theme/img/wechat/m-icon-error.png create mode 100644 plugin/think-plugs-static/stc/public/static/theme/img/wechat/m-icon-success.png create mode 100644 plugin/think-plugs-static/stc/public/static/theme/img/wechat/mobile_foot.png create mode 100644 plugin/think-plugs-static/stc/public/static/theme/img/wechat/mobile_head.png create mode 100644 plugin/think-plugs-static/stc/public/static/theme/img/wechat/qrc_pay_error.jpg create mode 100644 plugin/think-plugs-static/stc/public/static/theme/img/wechat/qrc_payed.jpg create mode 100644 plugin/think-plugs-static/stc/think create mode 100644 plugin/think-plugs-system/composer.json create mode 100644 plugin/think-plugs-system/phpunit.xml.dist create mode 100644 plugin/think-plugs-system/readme.api.md create mode 100644 plugin/think-plugs-system/readme.md create mode 100644 plugin/think-plugs-system/src/Service.php create mode 100644 plugin/think-plugs-system/src/builder/AuthBuilder.php create mode 100644 plugin/think-plugs-system/src/builder/BaseBuilder.php create mode 100644 plugin/think-plugs-system/src/builder/ConfigBuilder.php create mode 100644 plugin/think-plugs-system/src/builder/FileBuilder.php create mode 100644 plugin/think-plugs-system/src/builder/IconPickerBuilder.php create mode 100644 plugin/think-plugs-system/src/builder/MenuBuilder.php create mode 100644 plugin/think-plugs-system/src/builder/OplogBuilder.php create mode 100644 plugin/think-plugs-system/src/builder/PluginBuilder.php create mode 100644 plugin/think-plugs-system/src/builder/QueueBuilder.php create mode 100644 plugin/think-plugs-system/src/builder/SystemListPage.php create mode 100644 plugin/think-plugs-system/src/builder/SystemListTabs.php create mode 100644 plugin/think-plugs-system/src/builder/SystemTablePreset.php create mode 100644 plugin/think-plugs-system/src/builder/ThemeBuilder.php create mode 100644 plugin/think-plugs-system/src/builder/UploadImageDialogBuilder.php create mode 100644 plugin/think-plugs-system/src/builder/UserBuilder.php create mode 100644 plugin/think-plugs-system/src/common.php create mode 100644 plugin/think-plugs-system/src/controller/Auth.php create mode 100644 plugin/think-plugs-system/src/controller/Base.php create mode 100644 plugin/think-plugs-system/src/controller/Config.php create mode 100644 plugin/think-plugs-system/src/controller/File.php create mode 100644 plugin/think-plugs-system/src/controller/Index.php create mode 100644 plugin/think-plugs-system/src/controller/Login.php create mode 100644 plugin/think-plugs-system/src/controller/Menu.php rename {app/admin => plugin/think-plugs-system/src}/controller/Oplog.php (57%) create mode 100644 plugin/think-plugs-system/src/controller/Plugin.php create mode 100644 plugin/think-plugs-system/src/controller/Queue.php create mode 100644 plugin/think-plugs-system/src/controller/User.php rename {app/admin => plugin/think-plugs-system/src}/controller/api/Plugs.php (56%) create mode 100644 plugin/think-plugs-system/src/controller/api/Queue.php rename {app/admin => plugin/think-plugs-system/src}/controller/api/System.php (54%) rename {app/admin => plugin/think-plugs-system/src}/controller/api/Upload.php (50%) create mode 100644 plugin/think-plugs-system/src/helper/Service.php create mode 100644 plugin/think-plugs-system/src/helper/command/database/BackupCommand.php create mode 100644 plugin/think-plugs-system/src/helper/command/database/DatabaseCommand.php create mode 100644 plugin/think-plugs-system/src/helper/command/database/IndexCommand.php create mode 100644 plugin/think-plugs-system/src/helper/command/database/MigrateCommand.php create mode 100644 plugin/think-plugs-system/src/helper/command/database/ModelCommand.php create mode 100644 plugin/think-plugs-system/src/helper/command/database/ReplaceCommand.php create mode 100644 plugin/think-plugs-system/src/helper/command/database/RestoreCommand.php create mode 100644 plugin/think-plugs-system/src/helper/command/project/PackageCommand.php create mode 100644 plugin/think-plugs-system/src/helper/command/project/PublishCommand.php create mode 100644 plugin/think-plugs-system/src/helper/command/system/MenuResetCommand.php create mode 100644 plugin/think-plugs-system/src/helper/database/IndexNameService.php create mode 100644 plugin/think-plugs-system/src/helper/integration/ExpressService.php create mode 100644 plugin/think-plugs-system/src/helper/migration/MigrationExporter.php create mode 100644 plugin/think-plugs-system/src/helper/migration/PhinxExtend.php create mode 100644 plugin/think-plugs-system/src/helper/model/NormalizedModelGenerator.php create mode 100644 plugin/think-plugs-system/src/helper/plugin/PluginMenuService.php create mode 100644 plugin/think-plugs-system/src/helper/plugin/PluginRegistry.php create mode 100644 plugin/think-plugs-system/src/helper/service/bin/package.stub create mode 100644 plugin/think-plugs-system/src/lang/_loader.php create mode 100644 plugin/think-plugs-system/src/lang/en-us.php create mode 100644 plugin/think-plugs-system/src/lang/zh-cn.php create mode 100644 plugin/think-plugs-system/src/lang/zh-tw.php create mode 100644 plugin/think-plugs-system/src/middleware/JwtTokenAuth.php create mode 100644 plugin/think-plugs-system/src/middleware/LoadModuleLangPack.php create mode 100644 plugin/think-plugs-system/src/middleware/RbacAccess.php create mode 100644 plugin/think-plugs-system/src/model/SystemAuth.php create mode 100644 plugin/think-plugs-system/src/model/SystemBase.php create mode 100644 plugin/think-plugs-system/src/model/SystemData.php create mode 100644 plugin/think-plugs-system/src/model/SystemFile.php create mode 100644 plugin/think-plugs-system/src/model/SystemMenu.php create mode 100644 plugin/think-plugs-system/src/model/SystemNode.php create mode 100644 plugin/think-plugs-system/src/model/SystemOplog.php create mode 100644 plugin/think-plugs-system/src/model/SystemUser.php create mode 100644 plugin/think-plugs-system/src/route/demo.php create mode 100644 plugin/think-plugs-system/src/service/AuthService.php create mode 100644 plugin/think-plugs-system/src/service/BaseService.php create mode 100644 plugin/think-plugs-system/src/service/CaptchaService.php create mode 100644 plugin/think-plugs-system/src/service/ConfigService.php create mode 100644 plugin/think-plugs-system/src/service/FileService.php create mode 100644 plugin/think-plugs-system/src/service/IndexService.php create mode 100644 plugin/think-plugs-system/src/service/LangService.php create mode 100644 plugin/think-plugs-system/src/service/LoginService.php create mode 100644 plugin/think-plugs-system/src/service/MenuService.php create mode 100644 plugin/think-plugs-system/src/service/OplogService.php create mode 100644 plugin/think-plugs-system/src/service/PluginService.php create mode 100644 plugin/think-plugs-system/src/service/QueueService.php create mode 100644 plugin/think-plugs-system/src/service/SystemContext.php create mode 100644 plugin/think-plugs-system/src/service/SystemService.php create mode 100644 plugin/think-plugs-system/src/service/UserService.php create mode 100644 plugin/think-plugs-system/src/service/bin/captcha.ttf create mode 100644 plugin/think-plugs-system/src/storage/AliossStorage.php create mode 100644 plugin/think-plugs-system/src/storage/AlistStorage.php create mode 100644 plugin/think-plugs-system/src/storage/LocalStorage.php create mode 100644 plugin/think-plugs-system/src/storage/QiniuStorage.php create mode 100644 plugin/think-plugs-system/src/storage/StorageAuthorize.php create mode 100644 plugin/think-plugs-system/src/storage/StorageConfig.php create mode 100644 plugin/think-plugs-system/src/storage/StorageManager.php create mode 100644 plugin/think-plugs-system/src/storage/StorageUsageTrait.php create mode 100644 plugin/think-plugs-system/src/storage/TxcosStorage.php create mode 100644 plugin/think-plugs-system/src/storage/UpyunStorage.php create mode 100644 plugin/think-plugs-system/src/storage/extra/mimes.php rename {app/admin/view/api => plugin/think-plugs-system/src/storage/extra}/upload.js (86%) rename {app/admin => plugin/think-plugs-system/src}/view/error.php (99%) create mode 100644 plugin/think-plugs-system/src/view/full.html rename {app/admin => plugin/think-plugs-system/src}/view/index/index-left.html (89%) create mode 100644 plugin/think-plugs-system/src/view/index/index-top.html rename {app/admin => plugin/think-plugs-system/src}/view/index/index.html (51%) create mode 100644 plugin/think-plugs-system/src/view/login/index.html create mode 100644 plugin/think-plugs-system/src/view/plugin/layout.html create mode 100644 plugin/think-plugs-system/stc/database/20241010000001_install_system20241010.php create mode 100644 plugin/think-plugs-system/tests/ApiQueueControllerTest.php create mode 100644 plugin/think-plugs-system/tests/ApiSystemControllerTest.php create mode 100644 plugin/think-plugs-system/tests/AuthControllerTest.php create mode 100644 plugin/think-plugs-system/tests/BaseControllerTest.php create mode 100644 plugin/think-plugs-system/tests/CommonFunctionsTest.php create mode 100644 plugin/think-plugs-system/tests/ConfigControllerTest.php create mode 100644 plugin/think-plugs-system/tests/ConfigPageRenderTest.php create mode 100644 plugin/think-plugs-system/tests/ConsoleCssUtilityTest.php create mode 100644 plugin/think-plugs-system/tests/FileControllerTest.php create mode 100644 plugin/think-plugs-system/tests/IndexControllerTest.php create mode 100644 plugin/think-plugs-system/tests/LangPackTest.php create mode 100644 plugin/think-plugs-system/tests/LoginControllerTest.php create mode 100644 plugin/think-plugs-system/tests/MenuControllerTest.php create mode 100644 plugin/think-plugs-system/tests/OplogControllerTest.php create mode 100644 plugin/think-plugs-system/tests/PluginControllerTest.php create mode 100644 plugin/think-plugs-system/tests/PlugsControllerTest.php create mode 100644 plugin/think-plugs-system/tests/QueueControllerTest.php create mode 100644 plugin/think-plugs-system/tests/RbacAccessTest.php create mode 100644 plugin/think-plugs-system/tests/UploadControllerTest.php create mode 100644 plugin/think-plugs-system/tests/UserControllerTest.php create mode 100644 plugin/think-plugs-system/tests/bootstrap.php create mode 100644 plugin/think-plugs-system/tests/helper/IndexNameServiceTest.php create mode 100644 plugin/think-plugs-system/tests/helper/PluginMenuServiceTest.php create mode 100644 plugin/think-plugs-system/tests/helper/PublishTest.php create mode 100644 plugin/think-plugs-wechat-client/.gitattributes create mode 100644 plugin/think-plugs-wechat-client/.github/workflows/release.yml create mode 100644 plugin/think-plugs-wechat-client/.gitignore create mode 100644 plugin/think-plugs-wechat-client/.php-cs-fixer.php create mode 100644 plugin/think-plugs-wechat-client/composer.json create mode 100644 plugin/think-plugs-wechat-client/license create mode 100644 plugin/think-plugs-wechat-client/readme.api.md create mode 100644 plugin/think-plugs-wechat-client/readme.md create mode 100644 plugin/think-plugs-wechat-client/src/Service.php rename {app/wechat => plugin/think-plugs-wechat-client/src}/command/Auto.php (75%) rename {app/wechat => plugin/think-plugs-wechat-client/src}/command/Clear.php (71%) rename {app/wechat => plugin/think-plugs-wechat-client/src}/command/Fans.php (85%) rename {app/wechat => plugin/think-plugs-wechat-client/src}/controller/Auto.php (81%) rename {app/wechat => plugin/think-plugs-wechat-client/src}/controller/Config.php (57%) rename {app/wechat => plugin/think-plugs-wechat-client/src}/controller/Fans.php (82%) rename {app/wechat => plugin/think-plugs-wechat-client/src}/controller/Keys.php (86%) rename {app/wechat => plugin/think-plugs-wechat-client/src}/controller/Menu.php (87%) rename {app/wechat => plugin/think-plugs-wechat-client/src}/controller/News.php (83%) rename {app/wechat => plugin/think-plugs-wechat-client/src}/controller/api/Js.php (84%) rename {app/wechat => plugin/think-plugs-wechat-client/src}/controller/api/Login.php (78%) rename {app/wechat => plugin/think-plugs-wechat-client/src}/controller/api/Push.php (95%) rename {app/wechat => plugin/think-plugs-wechat-client/src}/controller/api/Test.php (76%) rename {app/wechat => plugin/think-plugs-wechat-client/src}/controller/api/View.php (82%) rename {app/wechat => plugin/think-plugs-wechat-client/src}/controller/payment/Record.php (64%) rename {app/wechat => plugin/think-plugs-wechat-client/src}/controller/payment/Refund.php (56%) rename {app/wechat => plugin/think-plugs-wechat-client/src}/lang/en-us.php (94%) create mode 100644 plugin/think-plugs-wechat-client/src/model/WechatAuto.php rename {app/wechat => plugin/think-plugs-wechat-client/src}/model/WechatFans.php (75%) rename {app/wechat => plugin/think-plugs-wechat-client/src}/model/WechatFansTags.php (59%) create mode 100644 plugin/think-plugs-wechat-client/src/model/WechatKeys.php rename {app/wechat => plugin/think-plugs-wechat-client/src}/model/WechatMedia.php (64%) create mode 100644 plugin/think-plugs-wechat-client/src/model/WechatNews.php rename {app/wechat => plugin/think-plugs-wechat-client/src}/model/WechatNewsArticle.php (66%) create mode 100644 plugin/think-plugs-wechat-client/src/model/WechatPaymentRecord.php create mode 100644 plugin/think-plugs-wechat-client/src/model/WechatPaymentRefund.php rename {app/wechat => plugin/think-plugs-wechat-client/src}/service/AutoService.php (71%) rename {app/wechat => plugin/think-plugs-wechat-client/src}/service/FansService.php (77%) rename {app/wechat => plugin/think-plugs-wechat-client/src}/service/LoginService.php (82%) rename {app/wechat => plugin/think-plugs-wechat-client/src}/service/MediaService.php (78%) rename {app/wechat => plugin/think-plugs-wechat-client/src}/service/PaymentService.php (86%) create mode 100644 plugin/think-plugs-wechat-client/src/service/WechatService.php rename {app/wechat => plugin/think-plugs-wechat-client/src}/view/api/login/failed.html (100%) rename {app/wechat => plugin/think-plugs-wechat-client/src}/view/api/login/success.html (94%) rename {app/wechat => plugin/think-plugs-wechat-client/src}/view/api/test/jsapi.html (100%) rename {app/wechat => plugin/think-plugs-wechat-client/src}/view/api/test/jssdk.html (100%) rename {app/wechat => plugin/think-plugs-wechat-client/src}/view/api/test/oauth.html (100%) rename {app/wechat => plugin/think-plugs-wechat-client/src}/view/api/view/image.html (100%) rename {app/wechat => plugin/think-plugs-wechat-client/src}/view/api/view/item.html (93%) rename {app/wechat => plugin/think-plugs-wechat-client/src}/view/api/view/main.html (91%) rename {app/wechat => plugin/think-plugs-wechat-client/src}/view/api/view/music.html (100%) rename {app/wechat => plugin/think-plugs-wechat-client/src}/view/api/view/news.html (93%) rename {app/wechat => plugin/think-plugs-wechat-client/src}/view/api/view/text.html (100%) rename {app/wechat => plugin/think-plugs-wechat-client/src}/view/api/view/video.html (100%) rename {app/wechat => plugin/think-plugs-wechat-client/src}/view/api/view/voice.html (100%) rename {app/wechat => plugin/think-plugs-wechat-client/src}/view/auto/form.html (89%) rename {app/wechat => plugin/think-plugs-wechat-client/src}/view/auto/index.html (82%) rename {app/wechat => plugin/think-plugs-wechat-client/src}/view/auto/index_search.html (92%) create mode 100644 plugin/think-plugs-wechat-client/src/view/config/options-style.html create mode 100644 plugin/think-plugs-wechat-client/src/view/config/options.html rename {app/wechat => plugin/think-plugs-wechat-client/src}/view/config/options_form_api.html (66%) rename {app/wechat => plugin/think-plugs-wechat-client/src}/view/config/options_form_thr.html (54%) create mode 100644 plugin/think-plugs-wechat-client/src/view/config/options_test.html rename {app/wechat => plugin/think-plugs-wechat-client/src}/view/config/payment.html (91%) create mode 100644 plugin/think-plugs-wechat-client/src/view/config/payment_test.html rename {app/wechat => plugin/think-plugs-wechat-client/src}/view/fans/index.html (97%) rename {app/wechat => plugin/think-plugs-wechat-client/src}/view/fans/index_search.html (98%) rename {app/wechat => plugin/think-plugs-wechat-client/src}/view/full.html (78%) rename {app/wechat => plugin/think-plugs-wechat-client/src}/view/keys/form.html (88%) rename {app/wechat => plugin/think-plugs-wechat-client/src}/view/keys/index.html (84%) rename {app/wechat => plugin/think-plugs-wechat-client/src}/view/keys/index_search.html (92%) create mode 100644 plugin/think-plugs-wechat-client/src/view/main.html rename {app/wechat => plugin/think-plugs-wechat-client/src}/view/menu/index.html (89%) rename {app/wechat => plugin/think-plugs-wechat-client/src}/view/news/form.html (96%) rename {app/wechat => plugin/think-plugs-wechat-client/src}/view/news/formstyle.html (100%) rename {app/wechat => plugin/think-plugs-wechat-client/src}/view/news/index.html (95%) rename {app/wechat => plugin/think-plugs-wechat-client/src}/view/news/select.html (97%) rename {app/wechat => plugin/think-plugs-wechat-client/src}/view/payment/record/index.html (100%) rename {app/wechat => plugin/think-plugs-wechat-client/src}/view/payment/record/index_search.html (98%) rename {app/wechat => plugin/think-plugs-wechat-client/src}/view/payment/refund/index.html (97%) rename {app/wechat => plugin/think-plugs-wechat-client/src}/view/payment/refund/index_search.html (100%) create mode 100644 plugin/think-plugs-wechat-client/src/view/table.html create mode 100644 plugin/think-plugs-wechat-client/stc/database/20241010000003_install_wechat20241010.php create mode 100644 plugin/think-plugs-wechat-service/.gitattributes create mode 100644 plugin/think-plugs-wechat-service/.github/workflows/release.yml create mode 100644 plugin/think-plugs-wechat-service/.gitignore create mode 100644 plugin/think-plugs-wechat-service/.php-cs-fixer.php create mode 100644 plugin/think-plugs-wechat-service/composer.json create mode 100644 plugin/think-plugs-wechat-service/readme.api.md create mode 100644 plugin/think-plugs-wechat-service/readme.md create mode 100644 plugin/think-plugs-wechat-service/src/Service.php create mode 100644 plugin/think-plugs-wechat-service/src/command/Wechat.php create mode 100644 plugin/think-plugs-wechat-service/src/controller/Config.php create mode 100644 plugin/think-plugs-wechat-service/src/controller/Wechat.php create mode 100644 plugin/think-plugs-wechat-service/src/controller/api/Client.php create mode 100644 plugin/think-plugs-wechat-service/src/controller/api/Push.php create mode 100644 plugin/think-plugs-wechat-service/src/lang/en-us.php create mode 100644 plugin/think-plugs-wechat-service/src/model/WechatAuth.php create mode 100644 plugin/think-plugs-wechat-service/src/service/AuthService.php create mode 100644 plugin/think-plugs-wechat-service/src/service/ConfigService.php create mode 100644 plugin/think-plugs-wechat-service/src/service/PublishHandle.php create mode 100644 plugin/think-plugs-wechat-service/src/service/ReceiveHandle.php create mode 100644 plugin/think-plugs-wechat-service/src/view/config/index.html create mode 100644 plugin/think-plugs-wechat-service/src/view/main.html create mode 100644 plugin/think-plugs-wechat-service/src/view/not-auth.html create mode 100644 plugin/think-plugs-wechat-service/src/view/table.html create mode 100644 plugin/think-plugs-wechat-service/src/view/wechat/index.html create mode 100644 plugin/think-plugs-wechat-service/src/view/wechat/index_search.html create mode 100644 plugin/think-plugs-wechat-service/stc/database/20241010000009_install_wechat_service20241010.php create mode 100644 plugin/think-plugs-wemall/.gitattributes create mode 100644 plugin/think-plugs-wemall/.github/workflows/release.yml create mode 100644 plugin/think-plugs-wemall/.gitignore create mode 100644 plugin/think-plugs-wemall/.php-cs-fixer.php create mode 100644 plugin/think-plugs-wemall/composer.json create mode 100644 plugin/think-plugs-wemall/readme.api.md create mode 100644 plugin/think-plugs-wemall/readme.md create mode 100644 plugin/think-plugs-wemall/src/Service.php create mode 100644 plugin/think-plugs-wemall/src/command/Clear.php create mode 100644 plugin/think-plugs-wemall/src/command/Trans.php create mode 100644 plugin/think-plugs-wemall/src/command/Users.php create mode 100644 plugin/think-plugs-wemall/src/common.php create mode 100644 plugin/think-plugs-wemall/src/controller/api/Auth.php create mode 100644 plugin/think-plugs-wemall/src/controller/api/Data.php create mode 100644 plugin/think-plugs-wemall/src/controller/api/Goods.php create mode 100644 plugin/think-plugs-wemall/src/controller/api/auth/Cart.php create mode 100644 plugin/think-plugs-wemall/src/controller/api/auth/Center.php create mode 100644 plugin/think-plugs-wemall/src/controller/api/auth/Checkin.php create mode 100644 plugin/think-plugs-wemall/src/controller/api/auth/Coupon.php create mode 100644 plugin/think-plugs-wemall/src/controller/api/auth/Order.php create mode 100644 plugin/think-plugs-wemall/src/controller/api/auth/Rebate.php create mode 100644 plugin/think-plugs-wemall/src/controller/api/auth/Refund.php create mode 100644 plugin/think-plugs-wemall/src/controller/api/auth/Spread.php create mode 100644 plugin/think-plugs-wemall/src/controller/api/auth/Transfer.php create mode 100644 plugin/think-plugs-wemall/src/controller/api/auth/action/Collect.php create mode 100644 plugin/think-plugs-wemall/src/controller/api/auth/action/History.php create mode 100644 plugin/think-plugs-wemall/src/controller/api/auth/action/Search.php create mode 100644 plugin/think-plugs-wemall/src/controller/api/help/Feedback.php create mode 100644 plugin/think-plugs-wemall/src/controller/api/help/Problem.php create mode 100644 plugin/think-plugs-wemall/src/controller/api/help/Question.php create mode 100644 plugin/think-plugs-wemall/src/controller/base/Agent.php create mode 100644 plugin/think-plugs-wemall/src/controller/base/Config.php create mode 100644 plugin/think-plugs-wemall/src/controller/base/Design.php create mode 100644 plugin/think-plugs-wemall/src/controller/base/Discount.php create mode 100644 plugin/think-plugs-wemall/src/controller/base/Level.php create mode 100644 plugin/think-plugs-wemall/src/controller/base/Notify.php create mode 100644 plugin/think-plugs-wemall/src/controller/base/Poster.php create mode 100644 plugin/think-plugs-wemall/src/controller/base/Report.php create mode 100644 plugin/think-plugs-wemall/src/controller/base/express/Company.php create mode 100644 plugin/think-plugs-wemall/src/controller/base/express/Template.php create mode 100644 plugin/think-plugs-wemall/src/controller/help/Feedback.php create mode 100644 plugin/think-plugs-wemall/src/controller/help/Problem.php create mode 100644 plugin/think-plugs-wemall/src/controller/help/Question.php create mode 100644 plugin/think-plugs-wemall/src/controller/shop/Goods.php create mode 100644 plugin/think-plugs-wemall/src/controller/shop/Order.php create mode 100644 plugin/think-plugs-wemall/src/controller/shop/Refund.php create mode 100644 plugin/think-plugs-wemall/src/controller/shop/Reply.php create mode 100644 plugin/think-plugs-wemall/src/controller/shop/Sender.php create mode 100644 plugin/think-plugs-wemall/src/controller/shop/goods/Cate.php create mode 100644 plugin/think-plugs-wemall/src/controller/shop/goods/Mark.php create mode 100644 plugin/think-plugs-wemall/src/controller/user/Admin.php create mode 100644 plugin/think-plugs-wemall/src/controller/user/Checkin.php create mode 100644 plugin/think-plugs-wemall/src/controller/user/Coupon.php create mode 100644 plugin/think-plugs-wemall/src/controller/user/Create.php create mode 100644 plugin/think-plugs-wemall/src/controller/user/Rebate.php create mode 100644 plugin/think-plugs-wemall/src/controller/user/Recharge.php create mode 100644 plugin/think-plugs-wemall/src/controller/user/Transfer.php create mode 100644 plugin/think-plugs-wemall/src/controller/user/coupon/Config.php create mode 100644 plugin/think-plugs-wemall/src/controller/user/rebate/Config.php create mode 100644 plugin/think-plugs-wemall/src/lang/en-us.php create mode 100644 plugin/think-plugs-wemall/src/model/AbsUser.php create mode 100644 plugin/think-plugs-wemall/src/model/PluginWemallConfigAgent.php create mode 100644 plugin/think-plugs-wemall/src/model/PluginWemallConfigCoupon.php create mode 100644 plugin/think-plugs-wemall/src/model/PluginWemallConfigDiscount.php create mode 100644 plugin/think-plugs-wemall/src/model/PluginWemallConfigLevel.php create mode 100644 plugin/think-plugs-wemall/src/model/PluginWemallConfigNotify.php create mode 100644 plugin/think-plugs-wemall/src/model/PluginWemallConfigPoster.php create mode 100644 plugin/think-plugs-wemall/src/model/PluginWemallConfigRebate.php create mode 100644 plugin/think-plugs-wemall/src/model/PluginWemallExpressCompany.php create mode 100644 plugin/think-plugs-wemall/src/model/PluginWemallExpressTemplate.php create mode 100644 plugin/think-plugs-wemall/src/model/PluginWemallGoods.php create mode 100644 plugin/think-plugs-wemall/src/model/PluginWemallGoodsCate.php create mode 100644 plugin/think-plugs-wemall/src/model/PluginWemallGoodsItem.php create mode 100644 plugin/think-plugs-wemall/src/model/PluginWemallGoodsMark.php create mode 100644 plugin/think-plugs-wemall/src/model/PluginWemallGoodsStock.php create mode 100644 plugin/think-plugs-wemall/src/model/PluginWemallHelpFeedback.php create mode 100644 plugin/think-plugs-wemall/src/model/PluginWemallHelpProblem.php create mode 100644 plugin/think-plugs-wemall/src/model/PluginWemallHelpQuestion.php create mode 100644 plugin/think-plugs-wemall/src/model/PluginWemallHelpQuestionX.php create mode 100644 plugin/think-plugs-wemall/src/model/PluginWemallOrder.php create mode 100644 plugin/think-plugs-wemall/src/model/PluginWemallOrderCart.php create mode 100644 plugin/think-plugs-wemall/src/model/PluginWemallOrderItem.php create mode 100644 plugin/think-plugs-wemall/src/model/PluginWemallOrderRefund.php create mode 100644 plugin/think-plugs-wemall/src/model/PluginWemallOrderSender.php create mode 100644 plugin/think-plugs-wemall/src/model/PluginWemallUserActionCollect.php create mode 100644 plugin/think-plugs-wemall/src/model/PluginWemallUserActionComment.php create mode 100644 plugin/think-plugs-wemall/src/model/PluginWemallUserActionHistory.php create mode 100644 plugin/think-plugs-wemall/src/model/PluginWemallUserActionSearch.php create mode 100644 plugin/think-plugs-wemall/src/model/PluginWemallUserCheckin.php create mode 100644 plugin/think-plugs-wemall/src/model/PluginWemallUserCoupon.php create mode 100644 plugin/think-plugs-wemall/src/model/PluginWemallUserCreate.php create mode 100644 plugin/think-plugs-wemall/src/model/PluginWemallUserRebate.php create mode 100644 plugin/think-plugs-wemall/src/model/PluginWemallUserRecharge.php create mode 100644 plugin/think-plugs-wemall/src/model/PluginWemallUserRelation.php create mode 100644 plugin/think-plugs-wemall/src/model/PluginWemallUserTransfer.php create mode 100644 plugin/think-plugs-wemall/src/service/ConfigService.php create mode 100644 plugin/think-plugs-wemall/src/service/ExpressService.php create mode 100644 plugin/think-plugs-wemall/src/service/GoodsService.php create mode 100644 plugin/think-plugs-wemall/src/service/OpenApiService.php create mode 100644 plugin/think-plugs-wemall/src/service/PosterService.php create mode 100644 plugin/think-plugs-wemall/src/service/UserAction.php create mode 100644 plugin/think-plugs-wemall/src/service/UserAgent.php create mode 100644 plugin/think-plugs-wemall/src/service/UserCoupon.php create mode 100644 plugin/think-plugs-wemall/src/service/UserCreate.php create mode 100644 plugin/think-plugs-wemall/src/service/UserOrder.php create mode 100644 plugin/think-plugs-wemall/src/service/UserRebate.php create mode 100644 plugin/think-plugs-wemall/src/service/UserRefund.php create mode 100644 plugin/think-plugs-wemall/src/service/UserReward.php create mode 100644 plugin/think-plugs-wemall/src/service/UserTransfer.php create mode 100644 plugin/think-plugs-wemall/src/service/UserUpgrade.php create mode 100644 plugin/think-plugs-wemall/src/service/extra/font01.ttf create mode 100644 plugin/think-plugs-wemall/src/view/base/agent/form.html create mode 100644 plugin/think-plugs-wemall/src/view/base/agent/index.html create mode 100644 plugin/think-plugs-wemall/src/view/base/agent/index_search.html create mode 100644 plugin/think-plugs-wemall/src/view/base/config/index.html create mode 100644 plugin/think-plugs-wemall/src/view/base/config/index_content.html create mode 100644 plugin/think-plugs-wemall/src/view/base/config/order.html create mode 100644 plugin/think-plugs-wemall/src/view/base/config/params.html create mode 100644 plugin/think-plugs-wemall/src/view/base/design/index-init.html create mode 100644 plugin/think-plugs-wemall/src/view/base/design/index-script.html create mode 100644 plugin/think-plugs-wemall/src/view/base/design/index-style.html create mode 100644 plugin/think-plugs-wemall/src/view/base/design/index-view-goods.html create mode 100644 plugin/think-plugs-wemall/src/view/base/design/index-view-icon.html create mode 100644 plugin/think-plugs-wemall/src/view/base/design/index-view-image.html create mode 100644 plugin/think-plugs-wemall/src/view/base/design/index-view-page-cart.html create mode 100644 plugin/think-plugs-wemall/src/view/base/design/index-view-page-cate.html create mode 100644 plugin/think-plugs-wemall/src/view/base/design/index-view-page-center.html create mode 100644 plugin/think-plugs-wemall/src/view/base/design/index-view.html create mode 100644 plugin/think-plugs-wemall/src/view/base/design/index-x-head-form.html create mode 100644 plugin/think-plugs-wemall/src/view/base/design/index-x-navbar-form.html create mode 100644 plugin/think-plugs-wemall/src/view/base/design/index-x-normal-form.html create mode 100644 plugin/think-plugs-wemall/src/view/base/design/index.html create mode 100644 plugin/think-plugs-wemall/src/view/base/design/link.html create mode 100644 plugin/think-plugs-wemall/src/view/base/design/link_other.html create mode 100644 plugin/think-plugs-wemall/src/view/base/discount/form.html create mode 100644 plugin/think-plugs-wemall/src/view/base/discount/index.html create mode 100644 plugin/think-plugs-wemall/src/view/base/express/company/form.html create mode 100644 plugin/think-plugs-wemall/src/view/base/express/company/index.html create mode 100644 plugin/think-plugs-wemall/src/view/base/express/company/index_search.html create mode 100644 plugin/think-plugs-wemall/src/view/base/express/template/form.html create mode 100644 plugin/think-plugs-wemall/src/view/base/express/template/index.html create mode 100644 plugin/think-plugs-wemall/src/view/base/express/template/index_search.html create mode 100644 plugin/think-plugs-wemall/src/view/base/express/template/region.html create mode 100644 plugin/think-plugs-wemall/src/view/base/level/form.html create mode 100644 plugin/think-plugs-wemall/src/view/base/level/index.html create mode 100644 plugin/think-plugs-wemall/src/view/base/level/index_search.html create mode 100644 plugin/think-plugs-wemall/src/view/base/notify/form.html create mode 100644 plugin/think-plugs-wemall/src/view/base/notify/index.html create mode 100644 plugin/think-plugs-wemall/src/view/base/notify/index_search.html create mode 100644 plugin/think-plugs-wemall/src/view/base/poster/form.html create mode 100644 plugin/think-plugs-wemall/src/view/base/poster/index.html create mode 100644 plugin/think-plugs-wemall/src/view/base/poster/index_search.html create mode 100644 plugin/think-plugs-wemall/src/view/base/report/index.html create mode 100644 plugin/think-plugs-wemall/src/view/help/feedback/form.html create mode 100644 plugin/think-plugs-wemall/src/view/help/feedback/index.html create mode 100644 plugin/think-plugs-wemall/src/view/help/feedback/index_search.html create mode 100644 plugin/think-plugs-wemall/src/view/help/problem/form.html create mode 100644 plugin/think-plugs-wemall/src/view/help/problem/index.html create mode 100644 plugin/think-plugs-wemall/src/view/help/problem/index_search.html create mode 100644 plugin/think-plugs-wemall/src/view/help/problem/select.html create mode 100644 plugin/think-plugs-wemall/src/view/help/question/form.html create mode 100644 plugin/think-plugs-wemall/src/view/help/question/index.html rename {app/admin/view/auth => plugin/think-plugs-wemall/src/view/help/question}/index_search.html (51%) rename {app/admin => plugin/think-plugs-wemall/src}/view/main.html (80%) create mode 100644 plugin/think-plugs-wemall/src/view/shop/goods/cate/form.html create mode 100644 plugin/think-plugs-wemall/src/view/shop/goods/cate/index.html create mode 100644 plugin/think-plugs-wemall/src/view/shop/goods/cate/select.html create mode 100644 plugin/think-plugs-wemall/src/view/shop/goods/form.html create mode 100644 plugin/think-plugs-wemall/src/view/shop/goods/form_style.html create mode 100644 plugin/think-plugs-wemall/src/view/shop/goods/index.html create mode 100644 plugin/think-plugs-wemall/src/view/shop/goods/index_search.html create mode 100644 plugin/think-plugs-wemall/src/view/shop/goods/mark/select.html create mode 100644 plugin/think-plugs-wemall/src/view/shop/goods/select.html create mode 100644 plugin/think-plugs-wemall/src/view/shop/goods/select_search.html create mode 100644 plugin/think-plugs-wemall/src/view/shop/goods/stock.html create mode 100644 plugin/think-plugs-wemall/src/view/shop/order/index.html create mode 100644 plugin/think-plugs-wemall/src/view/shop/order/index_search.html create mode 100644 plugin/think-plugs-wemall/src/view/shop/refund/form.html create mode 100644 plugin/think-plugs-wemall/src/view/shop/refund/index.html create mode 100644 plugin/think-plugs-wemall/src/view/shop/refund/index_search.html create mode 100644 plugin/think-plugs-wemall/src/view/shop/reply/form.html create mode 100644 plugin/think-plugs-wemall/src/view/shop/reply/index.html create mode 100644 plugin/think-plugs-wemall/src/view/shop/reply/index_search.html create mode 100644 plugin/think-plugs-wemall/src/view/shop/sender/config.html create mode 100644 plugin/think-plugs-wemall/src/view/shop/sender/delivery_form.html create mode 100644 plugin/think-plugs-wemall/src/view/shop/sender/delivery_query.html create mode 100644 plugin/think-plugs-wemall/src/view/shop/sender/index.html create mode 100644 plugin/think-plugs-wemall/src/view/shop/sender/index_search.html rename {app/admin => plugin/think-plugs-wemall/src}/view/table.html (80%) create mode 100644 plugin/think-plugs-wemall/src/view/user/admin/form.html create mode 100644 plugin/think-plugs-wemall/src/view/user/admin/index.html create mode 100644 plugin/think-plugs-wemall/src/view/user/admin/index_search.html create mode 100644 plugin/think-plugs-wemall/src/view/user/admin/parent.html create mode 100644 plugin/think-plugs-wemall/src/view/user/admin/parent_search.html create mode 100644 plugin/think-plugs-wemall/src/view/user/checkin/config.html create mode 100644 plugin/think-plugs-wemall/src/view/user/checkin/index.html create mode 100644 plugin/think-plugs-wemall/src/view/user/checkin/index_search.html create mode 100644 plugin/think-plugs-wemall/src/view/user/coupon/config/form.html create mode 100644 plugin/think-plugs-wemall/src/view/user/coupon/config/index.html create mode 100644 plugin/think-plugs-wemall/src/view/user/coupon/config/index_search.html create mode 100644 plugin/think-plugs-wemall/src/view/user/coupon/index.html create mode 100644 plugin/think-plugs-wemall/src/view/user/coupon/index_search.html create mode 100644 plugin/think-plugs-wemall/src/view/user/create/form.html create mode 100644 plugin/think-plugs-wemall/src/view/user/create/index.html create mode 100644 plugin/think-plugs-wemall/src/view/user/create/index_search.html create mode 100644 plugin/think-plugs-wemall/src/view/user/rebate/config/form.html create mode 100644 plugin/think-plugs-wemall/src/view/user/rebate/config/index.html create mode 100644 plugin/think-plugs-wemall/src/view/user/rebate/config/index_search.html create mode 100644 plugin/think-plugs-wemall/src/view/user/rebate/index.html create mode 100644 plugin/think-plugs-wemall/src/view/user/rebate/index_search.html create mode 100644 plugin/think-plugs-wemall/src/view/user/recharge/form.html create mode 100644 plugin/think-plugs-wemall/src/view/user/recharge/index.html create mode 100644 plugin/think-plugs-wemall/src/view/user/recharge/index_search.html create mode 100644 plugin/think-plugs-wemall/src/view/user/transfer/audit.html create mode 100644 plugin/think-plugs-wemall/src/view/user/transfer/config.html create mode 100644 plugin/think-plugs-wemall/src/view/user/transfer/index.html create mode 100644 plugin/think-plugs-wemall/src/view/user/transfer/index_search.html create mode 100644 plugin/think-plugs-wemall/src/view/user/transfer/payment.html create mode 100644 plugin/think-plugs-wemall/stc/database/20241010000007_install_wemall20241010.php create mode 100644 plugin/think-plugs-wemall/tests/ConfigServiceTest.php create mode 100644 plugin/think-plugs-wemall/tests/PaymentEventIntegrationTest.php create mode 100644 plugin/think-plugs-wemall/tests/UserAdminPasswordTest.php create mode 100644 plugin/think-plugs-wemall/tests/UserOrderTest.php create mode 100644 plugin/think-plugs-worker/.gitattributes create mode 100644 plugin/think-plugs-worker/.github/workflows/release.yml create mode 100644 plugin/think-plugs-worker/.gitignore create mode 100644 plugin/think-plugs-worker/.php-cs-fixer.php create mode 100644 plugin/think-plugs-worker/composer.json create mode 100644 plugin/think-plugs-worker/license create mode 100644 plugin/think-plugs-worker/phpunit.xml.dist create mode 100644 plugin/think-plugs-worker/readme.api.md create mode 100644 plugin/think-plugs-worker/readme.md create mode 100644 plugin/think-plugs-worker/src/Service.php create mode 100644 plugin/think-plugs-worker/src/command/Queue.php create mode 100644 plugin/think-plugs-worker/src/command/Worker.php create mode 100644 plugin/think-plugs-worker/src/common.php create mode 100644 plugin/think-plugs-worker/src/model/SystemQueue.php create mode 100644 plugin/think-plugs-worker/src/service/HttpServer.php create mode 100644 plugin/think-plugs-worker/src/service/ProcessService.php create mode 100644 plugin/think-plugs-worker/src/service/Queue.php create mode 100644 plugin/think-plugs-worker/src/service/QueueExecutor.php create mode 100644 plugin/think-plugs-worker/src/service/QueueServer.php create mode 100644 plugin/think-plugs-worker/src/service/QueueService.php create mode 100644 plugin/think-plugs-worker/src/service/Server.php create mode 100644 plugin/think-plugs-worker/src/service/ThinkApp.php create mode 100644 plugin/think-plugs-worker/src/service/ThinkCookie.php create mode 100644 plugin/think-plugs-worker/src/service/ThinkHttp.php create mode 100644 plugin/think-plugs-worker/src/service/ThinkRequest.php create mode 100644 plugin/think-plugs-worker/src/service/ThinkResponseFile.php create mode 100644 plugin/think-plugs-worker/src/service/WorkerConfig.php create mode 100644 plugin/think-plugs-worker/src/service/WorkerExceptionHandle.php create mode 100644 plugin/think-plugs-worker/src/service/WorkerMonitor.php create mode 100644 plugin/think-plugs-worker/src/service/WorkerState.php create mode 100644 plugin/think-plugs-worker/src/service/bin/console.exe create mode 100644 plugin/think-plugs-worker/stc/database/20241010000008_install_worker20241010.php create mode 100644 plugin/think-plugs-worker/stc/worker.php create mode 100644 plugin/think-plugs-worker/tests/CommonFunctionsTest.php create mode 100644 plugin/think-plugs-worker/tests/ProcessServiceTest.php create mode 100644 plugin/think-plugs-worker/tests/QueueServiceTest.php create mode 100644 plugin/think-plugs-worker/tests/ThinkAppTest.php create mode 100644 plugin/think-plugs-worker/tests/WorkerConfigTest.php create mode 100644 plugin/think-plugs-worker/tests/WorkerExceptionHandleTest.php create mode 100644 plugin/think-plugs-worker/tests/WorkerStateTest.php create mode 100644 plugin/think-plugs-worker/tests/bootstrap.php create mode 100644 plugin/think-plugs-wuma/.gitattributes create mode 100644 plugin/think-plugs-wuma/.github/workflows/release.yml create mode 100644 plugin/think-plugs-wuma/.gitignore create mode 100644 plugin/think-plugs-wuma/.php-cs-fixer.php create mode 100644 plugin/think-plugs-wuma/composer.json create mode 100644 plugin/think-plugs-wuma/phpunit.xml.dist create mode 100644 plugin/think-plugs-wuma/readme.api.md create mode 100644 plugin/think-plugs-wuma/readme.md create mode 100644 plugin/think-plugs-wuma/src/Service.php create mode 100644 plugin/think-plugs-wuma/src/command/Create.php create mode 100644 plugin/think-plugs-wuma/src/controller/Code.php create mode 100644 plugin/think-plugs-wuma/src/controller/Query.php create mode 100644 plugin/think-plugs-wuma/src/controller/Warehouse.php create mode 100644 plugin/think-plugs-wuma/src/controller/api/Auth.php create mode 100644 plugin/think-plugs-wuma/src/controller/api/Base.php create mode 100644 plugin/think-plugs-wuma/src/controller/api/Coder.php create mode 100644 plugin/think-plugs-wuma/src/controller/api/Login.php create mode 100644 plugin/think-plugs-wuma/src/controller/sales/Config.php create mode 100644 plugin/think-plugs-wuma/src/controller/sales/History.php create mode 100644 plugin/think-plugs-wuma/src/controller/sales/Level.php create mode 100644 plugin/think-plugs-wuma/src/controller/sales/Order.php create mode 100644 plugin/think-plugs-wuma/src/controller/sales/Stock.php create mode 100644 plugin/think-plugs-wuma/src/controller/sales/User.php create mode 100644 plugin/think-plugs-wuma/src/controller/scaner/Notify.php create mode 100644 plugin/think-plugs-wuma/src/controller/scaner/Protal.php create mode 100644 plugin/think-plugs-wuma/src/controller/scaner/Query.php create mode 100644 plugin/think-plugs-wuma/src/controller/source/Assign.php create mode 100644 plugin/think-plugs-wuma/src/controller/source/Blockchain.php create mode 100644 plugin/think-plugs-wuma/src/controller/source/Certificate.php create mode 100644 plugin/think-plugs-wuma/src/controller/source/Produce.php create mode 100644 plugin/think-plugs-wuma/src/controller/source/Template.php create mode 100644 plugin/think-plugs-wuma/src/controller/warehouse/Batch.php create mode 100644 plugin/think-plugs-wuma/src/controller/warehouse/History.php create mode 100644 plugin/think-plugs-wuma/src/controller/warehouse/Inter.php create mode 100644 plugin/think-plugs-wuma/src/controller/warehouse/Outer.php create mode 100644 plugin/think-plugs-wuma/src/controller/warehouse/Relation.php create mode 100644 plugin/think-plugs-wuma/src/controller/warehouse/Replace.php create mode 100644 plugin/think-plugs-wuma/src/controller/warehouse/Stock.php create mode 100644 plugin/think-plugs-wuma/src/controller/warehouse/User.php create mode 100644 plugin/think-plugs-wuma/src/lang/en-us.php create mode 100644 plugin/think-plugs-wuma/src/model/AbstractPrivate.php create mode 100644 plugin/think-plugs-wuma/src/model/PlainPrivate.php create mode 100644 plugin/think-plugs-wuma/src/model/PluginWumaCodeRule.php create mode 100644 plugin/think-plugs-wuma/src/model/PluginWumaCodeRuleRange.php create mode 100644 plugin/think-plugs-wuma/src/model/PluginWumaSalesOrder.php create mode 100644 plugin/think-plugs-wuma/src/model/PluginWumaSalesOrderData.php create mode 100644 plugin/think-plugs-wuma/src/model/PluginWumaSalesOrderDataMins.php create mode 100644 plugin/think-plugs-wuma/src/model/PluginWumaSalesOrderDataNums.php create mode 100644 plugin/think-plugs-wuma/src/model/PluginWumaSalesUser.php create mode 100644 plugin/think-plugs-wuma/src/model/PluginWumaSalesUserLevel.php create mode 100644 plugin/think-plugs-wuma/src/model/PluginWumaSalesUserStock.php create mode 100644 plugin/think-plugs-wuma/src/model/PluginWumaSourceAssign.php create mode 100644 plugin/think-plugs-wuma/src/model/PluginWumaSourceAssignItem.php create mode 100644 plugin/think-plugs-wuma/src/model/PluginWumaSourceBlockchain.php create mode 100644 plugin/think-plugs-wuma/src/model/PluginWumaSourceCertificate.php create mode 100644 plugin/think-plugs-wuma/src/model/PluginWumaSourceProduce.php create mode 100644 plugin/think-plugs-wuma/src/model/PluginWumaSourceQuery.php create mode 100644 plugin/think-plugs-wuma/src/model/PluginWumaSourceQueryNotify.php create mode 100644 plugin/think-plugs-wuma/src/model/PluginWumaSourceQueryVerify.php create mode 100644 plugin/think-plugs-wuma/src/model/PluginWumaSourceTemplate.php create mode 100644 plugin/think-plugs-wuma/src/model/PluginWumaWarehouse.php create mode 100644 plugin/think-plugs-wuma/src/model/PluginWumaWarehouseOrder.php create mode 100644 plugin/think-plugs-wuma/src/model/PluginWumaWarehouseOrderData.php create mode 100644 plugin/think-plugs-wuma/src/model/PluginWumaWarehouseOrderDataMins.php create mode 100644 plugin/think-plugs-wuma/src/model/PluginWumaWarehouseOrderDataNums.php create mode 100644 plugin/think-plugs-wuma/src/model/PluginWumaWarehouseRelation.php create mode 100644 plugin/think-plugs-wuma/src/model/PluginWumaWarehouseRelationData.php create mode 100644 plugin/think-plugs-wuma/src/model/PluginWumaWarehouseReplace.php create mode 100644 plugin/think-plugs-wuma/src/model/PluginWumaWarehouseStock.php create mode 100644 plugin/think-plugs-wuma/src/model/PluginWumaWarehouseUser.php create mode 100644 plugin/think-plugs-wuma/src/service/CertService.php create mode 100644 plugin/think-plugs-wuma/src/service/CodeService.php create mode 100644 plugin/think-plugs-wuma/src/service/RelationService.php create mode 100644 plugin/think-plugs-wuma/src/service/RemoveService.php create mode 100644 plugin/think-plugs-wuma/src/service/StockService.php create mode 100644 plugin/think-plugs-wuma/src/service/WhCoderService.php create mode 100644 plugin/think-plugs-wuma/src/service/WhExportService.php create mode 100644 plugin/think-plugs-wuma/src/service/WhImportService.php create mode 100644 plugin/think-plugs-wuma/src/service/extra/font01.ttf create mode 100644 plugin/think-plugs-wuma/src/view/code/form.html create mode 100644 plugin/think-plugs-wuma/src/view/code/index-search.html create mode 100644 plugin/think-plugs-wuma/src/view/code/index.html create mode 100644 plugin/think-plugs-wuma/src/view/code/template.html create mode 100644 plugin/think-plugs-wuma/src/view/main.html create mode 100644 plugin/think-plugs-wuma/src/view/sales/config/agx_cfg.html create mode 100644 plugin/think-plugs-wuma/src/view/sales/config/index.html create mode 100644 plugin/think-plugs-wuma/src/view/sales/history/index.html create mode 100644 plugin/think-plugs-wuma/src/view/sales/history/index_search.html create mode 100644 plugin/think-plugs-wuma/src/view/sales/order/index-search.html create mode 100644 plugin/think-plugs-wuma/src/view/sales/order/index.html create mode 100644 plugin/think-plugs-wuma/src/view/sales/order/show.html create mode 100644 plugin/think-plugs-wuma/src/view/sales/stock/index-search.html create mode 100644 plugin/think-plugs-wuma/src/view/sales/stock/index.html create mode 100644 plugin/think-plugs-wuma/src/view/sales/stock/show.html create mode 100644 plugin/think-plugs-wuma/src/view/sales/user/form.html create mode 100644 plugin/think-plugs-wuma/src/view/sales/user/index.html create mode 100644 plugin/think-plugs-wuma/src/view/sales/user/index_search.html create mode 100644 plugin/think-plugs-wuma/src/view/sales/user/select.html create mode 100644 plugin/think-plugs-wuma/src/view/sales/user/select_search.html create mode 100644 plugin/think-plugs-wuma/src/view/scaner/notify/agent-search.html create mode 100644 plugin/think-plugs-wuma/src/view/scaner/notify/agent.html create mode 100644 plugin/think-plugs-wuma/src/view/scaner/notify/area-search.html create mode 100644 plugin/think-plugs-wuma/src/view/scaner/notify/area.html create mode 100644 plugin/think-plugs-wuma/src/view/scaner/notify/index-search.html create mode 100644 plugin/think-plugs-wuma/src/view/scaner/notify/index.html create mode 100644 plugin/think-plugs-wuma/src/view/scaner/protal/index.html create mode 100644 plugin/think-plugs-wuma/src/view/scaner/query/index-search.html create mode 100644 plugin/think-plugs-wuma/src/view/scaner/query/index.html create mode 100644 plugin/think-plugs-wuma/src/view/source/assign/form.html create mode 100644 plugin/think-plugs-wuma/src/view/source/assign/index-search.html create mode 100644 plugin/think-plugs-wuma/src/view/source/assign/index.html create mode 100644 plugin/think-plugs-wuma/src/view/source/blockchain/form.html create mode 100644 plugin/think-plugs-wuma/src/view/source/blockchain/hash.html create mode 100644 plugin/think-plugs-wuma/src/view/source/blockchain/index-search.html create mode 100644 plugin/think-plugs-wuma/src/view/source/blockchain/index.html create mode 100644 plugin/think-plugs-wuma/src/view/source/blockchain/view.html create mode 100644 plugin/think-plugs-wuma/src/view/source/certificate/form.html create mode 100644 plugin/think-plugs-wuma/src/view/source/certificate/index-search.html create mode 100644 plugin/think-plugs-wuma/src/view/source/certificate/index.html create mode 100644 plugin/think-plugs-wuma/src/view/source/produce/form.html create mode 100644 plugin/think-plugs-wuma/src/view/source/produce/index-search.html create mode 100644 plugin/think-plugs-wuma/src/view/source/produce/index.html create mode 100644 plugin/think-plugs-wuma/src/view/source/template/form-edit.html create mode 100644 plugin/think-plugs-wuma/src/view/source/template/form-script.html create mode 100644 plugin/think-plugs-wuma/src/view/source/template/form-style.html create mode 100644 plugin/think-plugs-wuma/src/view/source/template/form-view.html create mode 100644 plugin/think-plugs-wuma/src/view/source/template/form.html create mode 100644 plugin/think-plugs-wuma/src/view/source/template/index-search.html create mode 100644 plugin/think-plugs-wuma/src/view/source/template/index.html create mode 100644 plugin/think-plugs-wuma/src/view/source/template/select.html create mode 100644 plugin/think-plugs-wuma/src/view/table.html create mode 100644 plugin/think-plugs-wuma/src/view/warehouse/batch/form.html create mode 100644 plugin/think-plugs-wuma/src/view/warehouse/batch/index-search.html create mode 100644 plugin/think-plugs-wuma/src/view/warehouse/batch/index.html create mode 100644 plugin/think-plugs-wuma/src/view/warehouse/form.html create mode 100644 plugin/think-plugs-wuma/src/view/warehouse/history/index-search.html create mode 100644 plugin/think-plugs-wuma/src/view/warehouse/history/index.html create mode 100644 plugin/think-plugs-wuma/src/view/warehouse/index-search.html create mode 100644 plugin/think-plugs-wuma/src/view/warehouse/index.html create mode 100644 plugin/think-plugs-wuma/src/view/warehouse/inter/form.html create mode 100644 plugin/think-plugs-wuma/src/view/warehouse/inter/index-search.html create mode 100644 plugin/think-plugs-wuma/src/view/warehouse/inter/index.html create mode 100644 plugin/think-plugs-wuma/src/view/warehouse/inter/show-search.html create mode 100644 plugin/think-plugs-wuma/src/view/warehouse/inter/show.html create mode 100644 plugin/think-plugs-wuma/src/view/warehouse/outer/form.html create mode 100644 plugin/think-plugs-wuma/src/view/warehouse/outer/index-search.html create mode 100644 plugin/think-plugs-wuma/src/view/warehouse/outer/index.html create mode 100644 plugin/think-plugs-wuma/src/view/warehouse/outer/show-search.html create mode 100644 plugin/think-plugs-wuma/src/view/warehouse/outer/show.html create mode 100644 plugin/think-plugs-wuma/src/view/warehouse/relation/index-search.html create mode 100644 plugin/think-plugs-wuma/src/view/warehouse/relation/index.html create mode 100644 plugin/think-plugs-wuma/src/view/warehouse/replace/index-search.html create mode 100644 plugin/think-plugs-wuma/src/view/warehouse/replace/index.html create mode 100644 plugin/think-plugs-wuma/src/view/warehouse/stock/index-search.html create mode 100644 plugin/think-plugs-wuma/src/view/warehouse/stock/index.html create mode 100644 plugin/think-plugs-wuma/src/view/warehouse/stock/show.html create mode 100644 plugin/think-plugs-wuma/src/view/warehouse/user/index-search.html rename {app/admin/view => plugin/think-plugs-wuma/src/view/warehouse}/user/index.html (58%) create mode 100644 plugin/think-plugs-wuma/stc/bin/coder create mode 100644 plugin/think-plugs-wuma/stc/bin/coder.exe create mode 100644 plugin/think-plugs-wuma/stc/database/20241010000010_install_wuma20241010.php create mode 100644 plugin/think-plugs-wuma/tests/CodeControllerTest.php create mode 100644 plugin/think-plugs-wuma/tests/CodeTest.php create mode 100644 plugin/think-plugs-wuma/tests/SalesUserControllerTest.php create mode 100644 route/.gitkeep diff --git a/app/admin/Service.php b/app/admin/Service.php deleted file mode 100644 index 0604ac38e..000000000 --- a/app/admin/Service.php +++ /dev/null @@ -1,69 +0,0 @@ - '系统配置', - 'subs' => [ - ['name' => '系统参数配置', 'icon' => 'layui-icon layui-icon-set', 'node' => 'admin/config/index'], - ['name' => '系统任务管理', 'icon' => 'layui-icon layui-icon-log', 'node' => 'admin/queue/index'], - ['name' => '系统日志管理', 'icon' => 'layui-icon layui-icon-form', 'node' => 'admin/oplog/index'], - ['name' => '数据字典管理', 'icon' => 'layui-icon layui-icon-code-circle', 'node' => 'admin/base/index'], - ['name' => '系统文件管理', 'icon' => 'layui-icon layui-icon-carousel', 'node' => 'admin/file/index'], - ['name' => '系统菜单管理', 'icon' => 'layui-icon layui-icon-layouts', 'node' => 'admin/menu/index'], - ], - ], - [ - 'name' => '权限管理', - 'subs' => [ - ['name' => '系统权限管理', 'icon' => 'layui-icon layui-icon-vercode', 'node' => 'admin/auth/index'], - ['name' => '系统用户管理', 'icon' => 'layui-icon layui-icon-username', 'node' => 'admin/user/index'], - ], - ], - ]; - } -} diff --git a/app/admin/controller/Auth.php b/app/admin/controller/Auth.php deleted file mode 100644 index ec7949864..000000000 --- a/app/admin/controller/Auth.php +++ /dev/null @@ -1,141 +0,0 @@ -layTable(function () { - $this->title = '系统权限管理'; - }, static function (QueryHelper $query) { - $query->like('title,desc')->equal('status,utype')->dateBetween('create_at'); - }); - } - - /** - * 修改权限状态 - * @auth true - */ - public function state() - { - SystemAuth::mSave($this->_vali([ - 'status.in:0,1' => '状态值范围异常!', - 'status.require' => '状态值不能为空!', - ])); - } - - /** - * 删除系统权限. - * @auth true - */ - public function remove() - { - SystemAuth::mDelete(); - } - - /** - * 添加系统权限. - * @auth true - */ - public function add() - { - SystemAuth::mForm('form'); - } - - /** - * 编辑系统权限. - * @auth true - */ - public function edit() - { - SystemAuth::mForm('form'); - } - - /** - * 表单后置数据处理. - */ - protected function _form_filter(array $data) - { - if ($this->request->isGet()) { - $this->title = empty($data['title']) ? '添加访问授权' : "编辑【{$data['title']}】授权"; - } elseif ($this->request->post('action') === 'json') { - if ($this->app->isDebug()) { - AdminService::clear(); - } - $ztree = AdminService::getTree(empty($data['id']) ? [] : SystemNode::mk()->where(['auth' => $data['id']])->column('node')); - usort($ztree, static function ($a, $b) { - if (explode('-', $a['node'])[0] !== explode('-', $b['node'])[0]) { - if (stripos($a['node'], 'plugin-') === 0) { - return 1; - } - } - return $a['node'] === $b['node'] ? 0 : ($a['node'] > $b['node'] ? 1 : -1); - }); - [$ps, $cs] = [Plugin::get(), (array)$this->app->config->get('app.app_names', [])]; - foreach ($ztree as &$n) { - $n['title'] = lang($cs[$n['node']] ?? (($ps[$n['node']] ?? [])['name'] ?? $n['title'])); - } - $this->success('获取权限节点成功!', $ztree); - } elseif (empty($data['nodes'])) { - $this->error('未配置功能节点!'); - } - } - - /** - * 节点更新处理. - */ - protected function _form_result(bool $state, array $post) - { - if ($state && $this->request->post('action') === 'save') { - [$map, $data] = [['auth' => $post['id']], []]; - foreach ($post['nodes'] ?? [] as $node) { - $data[] = $map + ['node' => $node]; - } - SystemNode::mk()->where($map)->delete(); - count($data) > 0 && SystemNode::mk()->insertAll($data); - sysoplog('系统权限管理', "配置系统权限[{$map['auth']}]授权成功"); - $this->success('权限修改成功!', 'javascript:history.back()'); - } - } -} diff --git a/app/admin/controller/Base.php b/app/admin/controller/Base.php deleted file mode 100644 index c9f7fdb89..000000000 --- a/app/admin/controller/Base.php +++ /dev/null @@ -1,116 +0,0 @@ -layTable(function () { - $this->title = '数据字典管理'; - $this->types = SystemBase::types(); - $this->type = $this->get['type'] ?? ($this->types[0] ?? '-'); - }, static function (QueryHelper $query) { - $query->where(['deleted' => 0])->equal('type'); - $query->like('code,name,status')->dateBetween('create_at'); - }); - } - - /** - * 添加数据字典. - * @auth true - */ - public function add() - { - SystemBase::mForm('form'); - } - - /** - * 编辑数据字典. - * @auth true - */ - public function edit() - { - SystemBase::mForm('form'); - } - - /** - * 修改数据状态 - * @auth true - */ - public function state() - { - SystemBase::mSave($this->_vali([ - 'status.in:0,1' => '状态值范围异常!', - 'status.require' => '状态值不能为空!', - ])); - } - - /** - * 删除数据记录. - * @auth true - */ - public function remove() - { - SystemBase::mDelete(); - } - - /** - * 表单数据处理. - * @throws DbException - */ - protected function _form_filter(array &$data) - { - if ($this->request->isGet()) { - $this->types = SystemBase::types(); - $this->types[] = '--- ' . lang('新增类型') . ' ---'; - $this->type = $this->get['type'] ?? ($this->types[0] ?? '-'); - } else { - $map = []; - $map[] = ['deleted', '=', 0]; - $map[] = ['code', '=', $data['code']]; - $map[] = ['type', '=', $data['type']]; - $map[] = ['id', '<>', $data['id'] ?? 0]; - if (SystemBase::mk()->where($map)->count() > 0) { - $this->error('数据编码已经存在!'); - } - } - } -} diff --git a/app/admin/controller/Config.php b/app/admin/controller/Config.php deleted file mode 100644 index 05821b768..000000000 --- a/app/admin/controller/Config.php +++ /dev/null @@ -1,157 +0,0 @@ - '默认色0', - 'white' => '简约白0', - 'red-1' => '玫瑰红1', - 'blue-1' => '深空蓝1', - 'green-1' => '小草绿1', - 'black-1' => '经典黑1', - 'red-2' => '玫瑰红2', - 'blue-2' => '深空蓝2', - 'green-2' => '小草绿2', - 'black-2' => '经典黑2', - ]; - - /** - * 系统参数配置. - * @auth true - * @menu true - */ - public function index() - { - $this->title = '系统参数配置'; - $this->files = Storage::types(); - $this->plugins = Plugin::get(null, true); - $this->issuper = AdminService::isSuper(); - $this->systemid = ModuleService::getRunVar('uni'); - $this->framework = ModuleService::getLibrarys('topthink/framework'); - $this->thinkadmin = ModuleService::getLibrarys('zoujingli/think-library'); - if (AdminService::isSuper() && $this->app->session->get('user.password') === md5('admin')) { - $url = url('admin/index/pass', ['id' => AdminService::getUserId()]); - $this->showErrorMessage = lang("超级管理员账号的密码未修改,建议立即修改密码!", [$url]); - } - uasort($this->plugins, static function ($a, $b) { - if ($a['space'] === $b['space']) { - return 0; - } - return $a['space'] > $b['space'] ? 1 : -1; - }); - $this->fetch(); - } - - /** - * 修改系统参数. - * @auth true - * @throws \think\admin\Exception - */ - public function system() - { - if ($this->request->isGet()) { - $this->title = '修改系统参数'; - $this->themes = static::themes; - $this->fetch(); - } else { - $post = $this->request->post(); - // 修改网站后台入口路径 - if (!empty($post['xpath'])) { - if (!preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $post['xpath'])) { - $this->error('后台入口格式错误!'); - } - if ($post['xpath'] !== 'admin') { - if (is_dir(syspath("app/{$post['xpath']}")) || !empty(Plugin::get($post['xpath']))) { - $this->error(lang('已存在 %s 应用!', [$post['xpath']])); - } - } - RuntimeService::set(null, [$post['xpath'] => 'admin']); - } - // 修改网站 ICON 图标,替换 public/favicon.ico - if (preg_match('#^https?://#', $post['site_icon'] ?? '')) { - try { - SystemService::setFavicon($post['site_icon'] ?? ''); - } catch (\Exception $exception) { - trace_file($exception); - } - } - // 数据数据到系统配置表 - foreach ($post as $k => $v) { - sysconf($k, $v); - } - sysoplog('系统配置管理', '修改系统参数成功'); - $this->success('数据保存成功!', admuri('admin/config/index')); - } - } - - /** - * 修改文件存储. - * @auth true - * @throws \think\admin\Exception - */ - public function storage() - { - $this->_applyFormToken(); - if ($this->request->isGet()) { - $this->type = input('type', 'local'); - if ($this->type === 'alioss') { - $this->points = AliossStorage::region(); - } elseif ($this->type === 'qiniu') { - $this->points = QiniuStorage::region(); - } elseif ($this->type === 'txcos') { - $this->points = TxcosStorage::region(); - } - $this->fetch("storage-{$this->type}"); - } else { - $post = $this->request->post(); - if (!empty($post['storage']['allow_exts'])) { - $deny = ['sh', 'asp', 'bat', 'cmd', 'exe', 'php']; - $exts = array_unique(str2arr(strtolower($post['storage']['allow_exts']))); - if (count(array_intersect($deny, $exts)) > 0) { - $this->error('禁止上传可执行的文件!'); - } - $post['storage']['allow_exts'] = join(',', $exts); - } - foreach ($post as $name => $value) { - sysconf($name, $value); - } - sysoplog('系统配置管理', '修改系统存储参数'); - $this->success('修改文件存储成功!'); - } - } -} diff --git a/app/admin/controller/File.php b/app/admin/controller/File.php deleted file mode 100644 index 1340396a5..000000000 --- a/app/admin/controller/File.php +++ /dev/null @@ -1,118 +0,0 @@ -layTable(function () { - $this->title = '系统文件管理'; - $this->xexts = SystemFile::mk()->distinct()->column('xext'); - }, static function (QueryHelper $query) { - $query->like('name,hash,xext')->equal('type')->dateBetween('create_at'); - $query->where(['issafe' => 0, 'status' => 2, 'uuid' => AdminService::getUserId()]); - }); - } - - /** - * 编辑系统文件. - * @auth true - */ - public function edit() - { - SystemFile::mForm('form'); - } - - /** - * 删除系统文件. - * @auth true - */ - public function remove() - { - if (!AdminService::isSuper()) { - $where = ['uuid' => AdminService::getUserId()]; - } - SystemFile::mDelete('', $where ?? []); - } - - /** - * 清理重复文件. - * @auth true - * @throws DbException - */ - public function distinct() - { - $map = ['issafe' => 0, 'uuid' => AdminService::getUserId()]; - // 使用派生表包装子查询,避免直接引用同一表 - $keepSubQuery = SystemFile::mk()->fieldRaw('MAX(id) AS id')->where($map)->group('type, xkey')->buildSql(); - // 使用 whereNotExists 配合派生表子查询删除,避免 1093 错误和 whereIn - SystemFile::mk()->where($map)->whereNotExists(function ($query) use ($keepSubQuery) { - $query->table("({$keepSubQuery})")->alias('f2')->whereRaw('f2.id = system_file.id'); - })->delete(); - $this->success('清理重复文件成功!'); - } - - /** - * 控制器初始化. - */ - protected function initialize() - { - $this->types = Storage::types(); - } - - /** - * 数据列表处理. - */ - protected function _page_filter(array &$data) - { - foreach ($data as &$vo) { - $vo['ctype'] = $this->types[$vo['type']] ?? $vo['type']; - } - } -} diff --git a/app/admin/controller/Index.php b/app/admin/controller/Index.php deleted file mode 100644 index 7e3716647..000000000 --- a/app/admin/controller/Index.php +++ /dev/null @@ -1,161 +0,0 @@ -app->isDebug()); - /* ! 读取当前用户权限菜单树 */ - $this->menus = MenuService::getTree(); - /* ! 判断当前用户的登录状态 */ - $this->login = AdminService::isLogin(); - /* ! 菜单为空且未登录跳转到登录页 */ - if (empty($this->menus) && empty($this->login)) { - $this->redirect(sysuri('admin/login/index')); - } else { - $this->title = '系统管理后台'; - $this->super = AdminService::isSuper(); - $this->theme = AdminService::getUserTheme(); - $this->fetch(); - } - } - - /** - * 后台主题切换. - * @login true - * @throws Exception - */ - public function theme() - { - if ($this->request->isGet()) { - $this->theme = AdminService::getUserTheme(); - $this->themes = Config::themes; - $this->fetch(); - } else { - $data = $this->_vali(['site_theme.require' => '主题名称不能为空!']); - if (AdminService::setUserTheme($data['site_theme'])) { - $this->success('主题配置保存成功!'); - } else { - $this->error('主题配置保存失败!'); - } - } - } - - /** - * 修改用户资料. - * @login true - */ - public function info() - { - $id = $this->request->param('id'); - if (AdminService::getUserId() == intval($id)) { - SystemUser::mForm('user/form', 'id', [], ['id' => $id]); - } else { - $this->error('只能修改自己的资料!'); - } - } - - /** - * 修改当前用户密码 - * @login true - * @throws DataNotFoundException - * @throws DbException - * @throws ModelNotFoundException - */ - public function pass() - { - $id = $this->request->param('id'); - if (AdminService::getUserId() !== intval($id)) { - $this->error('禁止修改他人密码!'); - } - if ($this->app->request->isGet()) { - $this->verify = true; - SystemUser::mForm('user/pass', 'id', [], ['id' => $id]); - } else { - $data = $this->_vali([ - 'password.require' => '登录密码不能为空!', - 'repassword.require' => '重复密码不能为空!', - 'oldpassword.require' => '旧的密码不能为空!', - 'password.confirm:repassword' => '两次输入的密码不一致!', - ]); - $user = SystemUser::mk()->find($id); - if (empty($user)) { - $this->error('用户不存在!'); - } - if (md5($data['oldpassword']) !== $user['password']) { - $this->error('旧密码验证失败,请重新输入!'); - } - if ($user->save(['password' => md5($data['password'])])) { - sysoplog('系统用户管理', "修改用户[{$user['id']}]密码成功"); - // 修改密码同步事件处理 - $this->app->event->trigger('PluginAdminChangePassword', [ - 'uuid' => intval($user['id']), 'pass' => $data['password'], - ]); - $this->success('密码修改成功,下次请使用新密码登录!', ''); - } else { - $this->error('密码修改失败,请稍候再试!'); - } - } - } - - /** - * 资料修改表单处理. - */ - protected function _info_form_filter(array &$data) - { - if ($this->request->isPost()) { - unset($data['username'], $data['authorize']); - } - } - - /** - * 资料修改结果处理. - */ - protected function _info_form_result(bool $status) - { - if ($status) { - $this->success('用户资料修改成功!', 'javascript:location.reload()'); - } - } -} diff --git a/app/admin/controller/Login.php b/app/admin/controller/Login.php deleted file mode 100644 index 84012dd52..000000000 --- a/app/admin/controller/Login.php +++ /dev/null @@ -1,137 +0,0 @@ -app->request->isGet()) { - if (AdminService::isLogin()) { - $this->redirect(sysuri('admin/index/index')); - } else { - // 加载登录模板 - $this->title = '系统登录'; - // 登录验证令牌 - $this->captchaType = 'LoginCaptcha'; - $this->captchaToken = CodeExtend::uuid(); - // 当前运行模式 - $this->runtimeMode = RuntimeService::check(); - // 后台背景处理 - $images = str2arr(sysconf('login_image|raw') ?: '', '|'); - if (empty($images)) { - $images = [ - SystemService::uri('/static/theme/img/login/bg1.jpg'), - SystemService::uri('/static/theme/img/login/bg2.jpg'), - ]; - } - $this->loginStyle = sprintf('style="background-image:url(%s)" data-bg-transition="%s"', $images[0], join(',', $images)); - // 更新后台主域名,用于部分无法获取域名的场景调用 - if ($this->request->domain() !== sysconf('base.site_host|raw')) { - sysconf('base.site_host', $this->request->domain()); - } - $this->fetch(); - } - } else { - $data = $this->_vali([ - 'username.require' => '登录账号不能为空!', - 'username.min:4' => '账号不能少于4位字符!', - 'password.require' => '登录密码不能为空!', - 'password.min:4' => '密码不能少于4位字符!', - 'verify.require' => '图形验证码不能为空!', - 'uniqid.require' => '图形验证标识不能为空!', - ]); - if (!CaptchaService::instance()->check($data['verify'], $data['uniqid'])) { - $this->error('图形验证码验证失败,请重新输入!'); - } - /* ! 用户信息验证 */ - $map = ['username' => $data['username'], 'is_deleted' => 0]; - $user = SystemUser::mk()->where($map)->findOrEmpty(); - if ($user->isEmpty()) { - $this->app->session->set('LoginInputSessionError', true); - $this->error('登录账号或密码错误,请重新输入!'); - } - if (empty($user['status'])) { - $this->app->session->set('LoginInputSessionError', true); - $this->error('账号已经被禁用,请联系管理员!'); - } - if (md5("{$user['password']}{$data['uniqid']}") !== $data['password']) { - $this->app->session->set('LoginInputSessionError', true); - $this->error('登录账号或密码错误,请重新输入!'); - } - $user->hidden(['sort', 'status', 'password', 'is_deleted']); - $this->app->session->set('user', $user->toArray()); - $this->app->session->delete('LoginInputSessionError'); - // 更新登录次数 - $user->where(['id' => $user->getAttr('id')])->inc('login_num')->update([ - 'login_at' => date('Y-m-d H:i:s'), 'login_ip' => $this->app->request->ip(), - ]); - // 刷新用户权限 - AdminService::apply(true); - sysoplog('系统用户登录', '登录系统后台成功'); - $this->success('登录成功', sysuri('admin/index/index')); - } - } - - /** - * 生成验证码 - */ - public function captcha() - { - $input = $this->_vali([ - 'type.require' => '类型不能为空!', - 'token.require' => '标识不能为空!', - ]); - $image = CaptchaService::instance()->initialize(); - $captcha = ['image' => $image->getData(), 'uniqid' => $image->getUniqid()]; - // 未发生异常时,直接返回验证码内容 - if (!$this->app->session->get('LoginInputSessionError')) { - $captcha['code'] = $image->getCode(); - } - $this->success('生成验证码成功', $captcha); - } - - /** - * 退出登录. - */ - public function out() - { - $this->app->session->destroy(); - $this->success('退出登录成功!', sysuri('admin/login/index')); - } -} diff --git a/app/admin/controller/Menu.php b/app/admin/controller/Menu.php deleted file mode 100644 index 63e1784c5..000000000 --- a/app/admin/controller/Menu.php +++ /dev/null @@ -1,189 +0,0 @@ -title = '系统菜单管理'; - $this->type = $this->get['type'] ?? 'index'; - // 获取顶级菜单ID - $this->pid = $this->get['pid'] ?? ''; - - // 查询顶级菜单集合 - $this->menupList = SystemMenu::mk()->where(['pid' => 0, 'status' => 1])->order('sort desc,id asc')->column('id,pid,title', 'id'); - - SystemMenu::mQuery()->layTable(); - } - - /** - * 添加系统菜单. - * @auth true - */ - public function add() - { - $this->_applyFormToken(); - SystemMenu::mForm('form'); - } - - /** - * 编辑系统菜单. - * @auth true - */ - public function edit() - { - $this->_applyFormToken(); - SystemMenu::mForm('form'); - } - - /** - * 修改菜单状态 - * @auth true - */ - public function state() - { - SystemMenu::mSave($this->_vali([ - 'status.in:0,1' => '状态值范围异常!', - 'status.require' => '状态值不能为空!', - ])); - } - - /** - * 删除系统菜单. - * @auth true - */ - public function remove() - { - SystemMenu::mDelete(); - } - - /** - * 列表数据处理. - */ - protected function _index_page_filter(array &$data) - { - $data = DataExtend::arr2tree($data); - // 回收站过滤有效菜单 - if ($this->type === 'recycle') { - foreach ($data as $k1 => &$p1) { - if (!empty($p1['sub'])) { - foreach ($p1['sub'] as $k2 => &$p2) { - if (!empty($p2['sub'])) { - foreach ($p2['sub'] as $k3 => $p3) { - if ($p3['status'] > 0) { - unset($p2['sub'][$k3]); - } - } - } - if (empty($p2['sub']) && ($p2['url'] === '#' or $p2['status'] > 0)) { - unset($p1['sub'][$k2]); - } - } - } - if (empty($p1['sub']) && ($p1['url'] === '#' or $p1['status'] > 0)) { - unset($data[$k1]); - } - } - } - // 菜单数据树数据变平化 - $data = DataExtend::arr2table($data); - - // 过滤非当前顶级菜单的下级菜单,并重新索引数组 - if ($this->type === 'index' && $this->pid) { - $data = array_values(array_filter($data, function ($item) { - return strpos($item['spp'], ",{$this->pid},") !== false; - })); - } - - foreach ($data as &$vo) { - if ($vo['url'] !== '#' && !preg_match('/^(https?:)?(\/\/|\\\)/i', $vo['url'])) { - $vo['url'] = trim(url($vo['url']) . ($vo['params'] ? "?{$vo['params']}" : ''), '\/'); - } - } - } - - /** - * 表单数据处理. - */ - protected function _form_filter(array &$vo) - { - if ($this->request->isGet()) { - $debug = $this->app->isDebug(); - /* 清理权限节点 */ - $debug && AdminService::clear(); - /* 读取系统功能节点 */ - $this->auths = []; - $this->nodes = MenuService::getList($debug); - foreach (NodeService::getMethods($debug) as $node => $item) { - if ($item['isauth'] && substr_count($node, '/') >= 2) { - $this->auths[] = ['node' => $node, 'title' => $item['title']]; - } - } - /* 选择自己上级菜单 */ - $vo['pid'] = $vo['pid'] ?? input('pid', '0'); - /* 列出可选上级菜单 */ - $menus = SystemMenu::mk()->order('sort desc,id asc')->column('id,pid,icon,url,node,title,params', 'id'); - $this->menus = DataExtend::arr2table(array_merge($menus, [['id' => '0', 'pid' => '-1', 'url' => '#', 'title' => '顶部菜单']])); - if (isset($vo['id'])) { - foreach ($this->menus as $menu) { - if ($menu['id'] === $vo['id']) { - $vo = $menu; - } - } - } - foreach ($this->menus as $key => $menu) { - if ($menu['spt'] >= 3 || $menu['url'] !== '#') { - unset($this->menus[$key]); - } - } - if (isset($vo['spt'], $vo['spc']) && in_array($vo['spt'], [1, 2]) && $vo['spc'] > 0) { - foreach ($this->menus as $key => $menu) { - if ($vo['spt'] <= $menu['spt']) { - unset($this->menus[$key]); - } - } - } - } - } -} diff --git a/app/admin/controller/Queue.php b/app/admin/controller/Queue.php deleted file mode 100644 index 4d6176c38..000000000 --- a/app/admin/controller/Queue.php +++ /dev/null @@ -1,126 +0,0 @@ -layTable(function () { - $this->title = '系统任务管理'; - $this->iswin = ProcessService::iswin(); - if ($this->super = AdminService::isSuper()) { - $this->command = ProcessService::think('xadmin:queue start'); - if (!$this->iswin && !empty($_SERVER['USER'])) { - $this->command = "sudo -u {$_SERVER['USER']} {$this->command}"; - } - } - }, static function (QueryHelper $query) { - $query->equal('status')->like('code|title#title,command'); - $query->timeBetween('enter_time,exec_time')->dateBetween('create_at'); - }); - } - - /** - * 重启系统任务 - * @auth true - */ - public function redo() - { - try { - $data = $this->_vali(['code.require' => '任务编号不能为空!']); - $queue = QueueService::instance()->initialize($data['code'])->reset(); - $queue->progress(1, '>>> 任务重置成功 <<<', '0.00'); - $this->success('任务重置成功!', $queue->code); - } catch (HttpResponseException $exception) { - throw $exception; - } catch (\Exception $exception) { - trace_file($exception); - $this->error($exception->getMessage()); - } - } - - /** - * 清理运行数据. - * @auth true - */ - public function clean() - { - $this->_queue('定时清理系统运行数据', 'xadmin:queue clean', 0, [], 0, 3600); - } - - /** - * 删除系统任务 - * @auth true - */ - public function remove() - { - SystemQueue::mDelete(); - } - - /** - * 分页数据回调处理. - * @throws DataNotFoundException - * @throws DbException - * @throws ModelNotFoundException - */ - protected function _index_page_filter(array $data, array &$result) - { - $result['extra'] = ['dos' => 0, 'pre' => 0, 'oks' => 0, 'ers' => 0]; - SystemQueue::mk()->field('status,count(1) count')->group('status')->select()->map(static function ($item) use (&$result) { - if (intval($item['status']) === 1) { - $result['extra']['pre'] = $item['count']; - } - if (intval($item['status']) === 2) { - $result['extra']['dos'] = $item['count']; - } - if (intval($item['status']) === 3) { - $result['extra']['oks'] = $item['count']; - } - if (intval($item['status']) === 4) { - $result['extra']['ers'] = $item['count']; - } - }); - } -} diff --git a/app/admin/controller/User.php b/app/admin/controller/User.php deleted file mode 100644 index d8d62e251..000000000 --- a/app/admin/controller/User.php +++ /dev/null @@ -1,186 +0,0 @@ -type = $this->get['type'] ?? 'index'; - SystemUser::mQuery()->layTable(function () { - $this->title = '系统用户管理'; - $this->bases = SystemBase::items('身份权限'); - }, function (QueryHelper $query) { - // 加载对应数据列表 - $query->where(['is_deleted' => 0, 'status' => intval($this->type === 'index')]); - - // 关联用户身份资料 - /* @var \think\model\Relation|\think\db\Query $query */ - $query->with(['userinfo' => static function ($query) { - $query->field('code,name,content'); - }]); - - // 数据列表搜索过滤 - $query->equal('status,usertype')->dateBetween('login_at,create_at'); - $query->like('username|nickname#username,contact_phone#phone,contact_mail#mail'); - }); - } - - /** - * 添加系统用户. - * @auth true - */ - public function add() - { - SystemUser::mForm('form'); - } - - /** - * 编辑系统用户. - * @auth true - */ - public function edit() - { - SystemUser::mForm('form'); - } - - /** - * 修改用户密码 - * @auth true - */ - public function pass() - { - $this->_applyFormToken(); - if ($this->request->isGet()) { - $this->verify = false; - SystemUser::mForm('pass'); - } else { - $data = $this->_vali([ - 'id.require' => '用户ID不能为空!', - 'password.require' => '登录密码不能为空!', - 'repassword.require' => '重复密码不能为空!', - 'repassword.confirm:password' => '两次输入的密码不一致!', - ]); - $user = SystemUser::mk()->findOrEmpty($data['id']); - if ($user->isExists() && $user->save(['password' => md5($data['password'])])) { - // 修改密码同步事件处理 - $this->app->event->trigger('PluginAdminChangePassword', [ - 'uuid' => $data['id'], 'pass' => $data['password'], - ]); - sysoplog('系统用户管理', "修改用户[{$data['id']}]密码成功"); - $this->success('密码修改成功,请使用新密码登录!', ''); - } else { - $this->error('密码修改失败,请稍候再试!'); - } - } - } - - /** - * 修改用户状态 - * @auth true - */ - public function state() - { - $this->_checkInput(); - SystemUser::mSave($this->_vali([ - 'status.in:0,1' => '状态值范围异常!', - 'status.require' => '状态值不能为空!', - ])); - } - - /** - * 删除系统用户. - * @auth true - */ - public function remove() - { - $this->_checkInput(); - SystemUser::mDelete(); - } - - /** - * 表单数据处理. - * @throws DataNotFoundException - * @throws DbException - * @throws ModelNotFoundException - */ - protected function _form_filter(array &$data) - { - if ($this->request->isPost()) { - // 检查资料是否完整 - empty($data['username']) && $this->error('登录账号不能为空!'); - if ($data['username'] !== AdminService::getSuperName()) { - empty($data['authorize']) && $this->error('未配置权限!'); - } - // 处理上传的权限格式 - $data['authorize'] = arr2str($data['authorize'] ?? []); - if (empty($data['id'])) { - // 检查账号是否重复 - $map = ['username' => $data['username'], 'is_deleted' => 0]; - if (SystemUser::mk()->where($map)->count() > 0) { - $this->error('账号已经存在,请使用其它账号!'); - } - // 新添加的用户密码与账号相同 - $data['password'] = md5($data['username']); - } else { - unset($data['username']); - } - } else { - // 权限绑定处理 - $data['authorize'] = str2arr($data['authorize'] ?? ''); - $this->auths = SystemAuth::items(); - $this->bases = SystemBase::items('身份权限'); - $this->super = AdminService::getSuperName(); - } - } - - /** - * 检查输入变量. - */ - private function _checkInput() - { - if (in_array('10000', str2arr(input('id', '')))) { - $this->error('系统超级账号禁止删除!'); - } - } -} diff --git a/app/admin/controller/api/Queue.php b/app/admin/controller/api/Queue.php deleted file mode 100644 index ebc34be76..000000000 --- a/app/admin/controller/api/Queue.php +++ /dev/null @@ -1,125 +0,0 @@ -app->console->call('xadmin:queue', ['stop'])->fetch(); - if (stripos($message, 'sent end signal to process')) { - sysoplog('系统运维管理', '尝试停止任务监听服务'); - $this->success('停止任务监听服务成功!'); - } elseif (stripos($message, 'processes to stop')) { - $this->success('没有找到需要停止的服务!'); - } else { - $this->error(nl2br($message)); - } - } catch (HttpResponseException $exception) { - throw $exception; - } catch (\Exception $exception) { - trace_file($exception); - $this->error($exception->getMessage()); - } - } else { - $this->error('请使用超管账号操作!'); - } - } - - /** - * 启动监听服务 - * @login true - */ - public function start() - { - if (AdminService::isSuper()) { - try { - $message = $this->app->console->call('xadmin:queue', ['start'])->fetch(); - if (stripos($message, 'daemons started successfully for pid')) { - sysoplog('系统运维管理', '尝试启动任务监听服务'); - $this->success('任务监听服务启动成功!'); - } elseif (stripos($message, 'daemons already exist for pid')) { - $this->success('任务监听服务已经启动!'); - } else { - $this->error(nl2br($message)); - } - } catch (HttpResponseException $exception) { - throw $exception; - } catch (\Exception $exception) { - trace_file($exception); - $this->error($exception->getMessage()); - } - } else { - $this->error('请使用超管账号操作!'); - } - } - - /** - * 检查监听服务 - * @login true - */ - public function status() - { - if (AdminService::isSuper()) { - try { - $message = $this->app->console->call('xadmin:queue', ['status'])->fetch(); - if (preg_match('/process.*?\d+.*?running/', $message)) { - echo "{$this->app->lang->get('已启动')}"; - } else { - echo "{$this->app->lang->get('未启动')}"; - } - } catch (\Error|\Exception $exception) { - echo "{$this->app->lang->get('异 常')}"; - } - } else { - $message = lang('只有超级管理员才能操作!'); - echo "{$this->app->lang->get('无权限')}"; - } - } - - /** - * 查询任务进度. - * @login true - */ - public function progress() - { - $input = $this->_vali(['code.require' => '任务编号不能为空!']); - $this->app->db->setLog(new NullLogger()); /* 关闭数据库请求日志 */ - $message = SystemQueue::mk()->where($input)->value('message', ''); - $this->success('获取任务进度成功d!', json_decode($message, true)); - } -} diff --git a/app/admin/lang/en-us.php b/app/admin/lang/en-us.php deleted file mode 100644 index 2a1ee14ad..000000000 --- a/app/admin/lang/en-us.php +++ /dev/null @@ -1,396 +0,0 @@ -修改密码!"] = "The super administrator password has not been changed. Suggest changing password."; - -$extra['等待处理'] = 'Pending'; -$extra['正在处理'] = 'Processing'; -$extra['处理完成'] = 'Completed'; -$extra['处理失败'] = 'Failed'; - -$extra['条件搜索'] = 'Search'; -$extra['批量删除'] = 'Batch Delete'; - -$extra['上传进度 %s'] = 'Upload progress %s'; -$extra['文件上传出错!'] = 'File upload error.'; -$extra['文件上传失败!'] = 'File upload failed.'; -$extra['大小超出限制!'] = 'Size exceeds limit.'; -$extra['文件秒传成功!'] = 'Successfully transmitted the file in seconds.'; -$extra['上传接口异常!'] = 'Abnormal upload interface.'; -$extra['文件上传成功!'] = 'File uploaded successfully.'; -$extra['图片压缩失败!'] = 'Image compression failed.'; -$extra['无效的文件上传对象!'] = 'Invalid file upload object.'; - -return array_merge($extra, [ - // 系统操作 - '基本资料' => 'Basic information', - '安全设置' => 'Security setting', - '缓存加速' => 'Cache acceleration', - '清理缓存' => 'Clean cache', - '配色方案' => 'Color scheme', - '立即登录' => 'Login', - '退出登录' => 'Logout', - '系统提示:' => 'System Notify: ', - '清空日志缓存成功!' => 'Successfully cleared the log cache.', - '获取任务进度成功!' => 'Successfully obtained task progress.', - '网站缓存加速成功!' => 'Website cache acceleration successful.', - '请使用超管账号操作!' => 'Please use a super managed account to operate.', - '停止任务监听服务成功!' => 'Successfully stopped task listening service.', - '任务监听服务启动成功!' => 'Task monitoring service started successfully.', - '任务监听服务已经启动!' => 'The task monitoring service has started.', - '没有找到需要停止的服务!' => 'No services found that need to be stopped.', - '已切换后台编辑器!' => 'Switched to background editor.', - // 其他搜索器提示 - '请选择登录时间' => 'Please select the Login time', - '请选择创建时间' => 'Please select the creation time', - '请输入账号或名称' => 'Please enter an account or name', - '请输入权限名称' => 'Please enter the permission name', - '请输入数据编码' => 'Please enter the data code', - '请输入数据名称' => 'Please enter the data name', - '请输入文件名称' => 'Please enter the file name', - '请输入文件哈希' => 'Please enter the file hash', - '请输入操作节点' => 'Please enter the operate node', - '请输入操作内容' => 'Please enter the operate content', - '请输入访问地址' => 'Please enter the access Geoip', - // 系统配置 - '运行模式' => 'Running Mode', - '生产模式' => 'Production mode', - '开发模式' => 'Development mode', - '以开发模式运行' => 'Running in Development mode', - '以生产模式运行' => 'Running in Production mode', - '清理无效配置' => 'Clean up Invalid Configurations', - '修改系统参数' => 'Modify System Parameters', - '清理系统配置成功!' => 'Successfully cleaned.', - '自适应模式' => 'Adaptive Mode', - '富编辑器' => 'RichText Editor', - '存储引擎' => 'Storage Engine', - '系统参数' => 'System Parameter', - '网站名称' => 'Site Name', - '管理程序名称' => 'Program Name', - '管理程序版本' => 'Program Version', - '公安备案号' => 'Public security registration number', - '网站备案号' => 'Website registration number', - '网站版权信息' => 'Website copyright information', - '系统信息' => 'System Information', - '应用插件' => 'Plugin Information', - '核心框架' => 'Core Framework', - '平台框架' => 'Platform Framework', - '操作系统' => 'Operating System', - '运行环境' => 'Runtime Environment', - '仅开发模式可见' => 'Visible only in Development mode', - '仅生产模式可见' => 'Visible only in Production mode', - '插件名称' => 'Plugin Name', - '应用名称' => 'App Name', - '插件包名' => 'Package Name', - '插件版本' => 'Plugin Version', - '授权协议' => 'License', - '文件默认存储方式' => 'Default storage method for file upload', - '当前系统配置参数' => 'Current system configuration parameters', - '仅超级管理员可配置' => 'Only super administrators can configure', - - // 系统任务管理 - '优化数据库' => 'Optimize Database', - '开启服务' => 'Start Service', - '关闭服务' => 'Shutdown Service', - '定时清理' => 'Regular cleaning', - '服务状态' => 'Service', - '任务统计' => 'Total', - '编号名称' => 'Name', - '任务指令' => 'Command', - '任务状态' => 'Status', - '计划时间' => 'scheduled time', - '任务名称' => 'Name', - '检查中' => 'Checking', - '任务计划' => 'Scheduled', - '重 置' => 'Reset', - '日 志' => 'Logs', - '异 常' => 'Abnormal', - '无权限' => 'Denied', - '已启动' => 'Started', - '未启动' => 'Stopped', - // 数据字典管理 - '数据编码' => 'Code', - '数据名称' => 'Name', - '操作账号' => 'User', - '操作节点' => 'Node', - '操作行为' => 'Action', - '操作内容' => 'Content', - '访问地址' => 'Geo IP', - '网络服务商' => 'ISP.', - '日志清理成功!' => 'Logger Clear Complate.', - '成功清理所有日志' => 'Successfully cleared all logs.', - // 系统文件管理 - '文件名称' => 'Name', - '文件哈希' => 'HASH', - '文件大小' => 'Size', - '文件后缀' => 'Exts', - '存储方式' => 'Storage Type', - '清理重复' => 'Clear Replace', - '上传方式' => 'Upload Type', - '查看文件' => 'View', - '文件链接' => 'Link', - '秒传' => 'Speedy', - '普通' => 'Normal', - // 系统菜单管理 - '图 标' => 'Icon', - '添加菜单' => 'Add', - '禁用菜单' => 'Forbid', - '激活菜单' => 'Resume', - '系统菜单' => 'Menus', - '菜单名称' => 'Name', - '跳转链接' => 'Link', - '上级菜单' => 'Parent', - '菜单链接' => 'Link', - '链接参数' => 'Params', - '权限节点' => 'Node', - '菜单图标' => 'Icon', - '选择图标' => 'Select Icon', - // 系统权限管理 - '授 权' => 'Auth', - '添加权限' => 'Add', - '权限名称' => 'Name', - '权限描述' => 'Description', - '请输入权限描述' => 'Please enter a permission description', - // 系统用户管理 - '账号名称' => 'Username', - '添加用户' => 'Add User', - '最后登录' => 'Last Login Time', - '头像' => 'Head', - '登录账号' => 'Username', - '用户名称' => 'Nickname', - '登录次数' => 'Login Times', - '系统用户' => 'System User', - '密 码' => 'Password', - '系统用户管理' => 'Users', - - // 通用操作 - '回 收 站' => 'Recycle Bin', - '排序权重' => 'Sort Weight', - '使用状态' => 'Status', - '操作面板' => 'Actions', - '已激活' => 'Activated', - '已禁用' => 'Disabled', - '已启用' => 'Enabled', - '添 加' => 'Add', - '编 辑' => 'Edit', - '删 除' => 'Delete', - '全部' => 'All', - '搜 索' => 'Search', - '导 出' => 'Export', - '保存数据' => 'Save Data', - '取消编辑' => 'Cancel Edit', - '操作日志' => 'Operation Log', - '创建时间' => 'Create Time', - - // 用户管理 - '批量禁用' => 'Batch Disable', - '批量恢复' => 'Batch Restore', - '编辑用户' => 'Edit User', - '设置密码' => 'Set Password', - '角色身份' => 'Role Identity', - '全部菜单' => 'All Menus', - - // 权限管理 - '确定要批量删除权限吗?' => 'Are you sure you want to batch delete permissions?', - '确定要删除权限吗?' => 'Are you sure you want to delete the permission?', - '功能节点' => 'Function Node', - '访问权限名称需要保持不重复,在给用户授权时需要根据名称选择!' => 'Access permission names must be unique. When authorizing users, select based on the name!', - '已禁用记录' => 'Disabled Records', - '已激活记录' => 'Activated Records', - - // 菜单管理 - '确定要删除菜单吗?' => 'Are you sure you want to delete the menu?', - '添加系统菜单' => 'Add System Menu', - '编辑系统菜单' => 'Edit System Menu', - - // 文件管理 - '确定删除这些记录吗?' => 'Are you sure you want to delete these records?', - '播放视频' => 'Play Video', - '播放音频' => 'Play Audio', - '查看下载' => 'View/Download', - '编辑文件信息' => 'Edit File Info', - - // 任务管理 - '确定批量删除记录吗?' => 'Are you sure you want to batch delete records?', - '确定要重置该任务吗?' => 'Are you sure you want to reset this task?', - '确定要删除该记录吗?' => 'Are you sure you want to delete this record?', - '请选择计划时间' => 'Please select scheduled time', - '请输入名称或编号' => 'Please enter name or code', - '请输入任务指令' => 'Please enter task command', - - // 确认提示 - '确定要永久删除吗?' => 'Are you sure you want to permanently delete?', - '确定要取消编辑吗?' => 'Are you sure you want to cancel editing?', - '确定要取消修改吗?' => 'Are you sure you want to cancel the modification?', - '确定要禁用这些用户吗?' => 'Are you sure you want to disable these users?', - '确定要恢复这些账号吗?' => 'Are you sure you want to restore these accounts?', - '确定永久删除这些账号吗?' => 'Are you sure you want to permanently delete these accounts?', - - // 系统提示 - '新增类型' => 'New Type', - '只有超级管理员才能操作!' => 'Only super administrators can operate!', - '日志清理失败,%s' => 'Log cleanup failed, %s', - '已存在 %s 应用!' => 'Application %s already exists!', - - // 演示环境提示 - '演示环境禁止修改用户密码!' => 'Demo environment prohibits modifying user passwords!', - '演示环境禁止修改系统配置!' => 'Demo environment prohibits modifying system configuration!', - '演示环境禁止给菜单排序!' => 'Demo environment prohibits menu sorting!', - '演示环境禁止添加菜单!' => 'Demo environment prohibits adding menus!', - '演示环境禁止编辑菜单!' => 'Demo environment prohibits editing menus!', - '演示环境禁止禁用菜单!' => 'Demo environment prohibits disabling menus!', - '演示环境禁止删除菜单!' => 'Demo environment prohibits deleting menus!', - '演示环境禁止修改密码!' => 'Demo environment prohibits modifying passwords!', - - // 表单字段 - '用户账号' => 'User Account', - '用户权限' => 'User Permissions', - '用户资料' => 'User Profile', - '登录账号' => 'Login Account', - '用户名称' => 'User Name', - '角色身份' => 'Role Identity', - '访问权限' => 'Access Permissions', - '联系邮箱' => 'Contact Email', - '联系手机' => 'Contact Mobile', - '联系QQ' => 'Contact QQ', - '用户描述' => 'User Description', - '登录用户账号' => 'Login Username', - '旧的登录密码' => 'Old Password', - '新的登录密码' => 'New Password', - '重复登录密码' => 'Repeat Password', - '验证密码' => 'Verify Password', - '登录密码' => 'Login Password', - '重复密码' => 'Repeat Password', - - // 表单提示 - '登录账号不能少于4位字符,创建后不能再次修改.' => 'Login account must be at least 4 characters and cannot be modified after creation.', - '用于区分用户数据的用户名称,请尽量不要重复.' => 'User name used to distinguish user data, please try not to duplicate.', - '超级用户拥所有访问权限,不需要配置权限。' => 'Super users have all access permissions and do not need to configure permissions.', - '可选,请填写用户常用的电子邮箱' => 'Optional, please fill in the user\'s commonly used email address', - '可选,请填写用户常用的联系手机号' => 'Optional, please fill in the user\'s commonly used mobile phone number', - '可选,请填写用户常用的联系QQ号' => 'Optional, please fill in the user\'s commonly used QQ number', - '请输入用户描述' => 'Please enter user description', - '登录用户账号创建后,不允许再次修改。' => 'Login username cannot be modified after creation.', - '请输入旧密码来验证修改权限,旧密码不限制格式。' => 'Please enter the old password to verify modification permission. The old password format is not restricted.', - '密码必须包含大小写字母、数字、符号的任意两者组合。' => 'Password must contain any combination of uppercase letters, lowercase letters, numbers, and symbols.', - '请重复输入登录密码' => 'Please repeat the login password', - - // 系统配置表单 - '登录表单标题' => 'Login Form Title', - '后台登录入口' => 'Backend Login Entry', - '后台默认配色' => 'Backend Default Theme', - '登录背景图片' => 'Login Background Image', - 'JWT 接口密钥' => 'JWT API Key', - '浏览器小图标' => 'Browser Icon', - '后台程序名称' => 'Backend Program Name', - '后台程序版本' => 'Backend Program Version', - '公安安备号' => 'Public Security Registration Number', - '保存配置' => 'Save Configuration', - '取消修改' => 'Cancel Modification', - '登录标题' => 'Login Title', - '登录入口' => 'Login Entry', - '接口密钥' => 'API Key', - '图标文件' => 'Icon File', - '程序名称' => 'Program Name', - '版权信息' => 'Copyright Information', - - // 菜单表单提示 - '必选' => 'Required', - '可选' => 'Optional', - '请选择上级菜单或顶级菜单 ( 目前最多支持三级菜单 )' => 'Please select parent menu or top-level menu (currently supports up to 3 levels)', - '请填写菜单名称 ( 如:系统管理 ),建议字符不要太长,一般 4-6 个汉字' => 'Please fill in the menu name (e.g., System Management), it is recommended not to be too long, generally 4-6 Chinese characters', - '请填写链接地址或选择系统节点 ( 如:https://domain.com/admin/user/index.html 或 admin/user/index )' => 'Please fill in the link address or select a system node (e.g., https://domain.com/admin/user/index.html or admin/user/index)', - '当填写链接地址时,以下面的 "权限节点" 来判断菜单自动隐藏或显示,注意未填写 "权限节点" 时将不会隐藏该菜单哦' => 'When filling in the link address, use the "Permission Node" below to determine whether the menu is automatically hidden or displayed. Note that if the "Permission Node" is not filled in, the menu will not be hidden', - '设置菜单链接的 GET 访问参数 ( 如:name=1&age=3 )' => 'Set GET access parameters for menu links (e.g., name=1&age=3)', - '请填写系统权限节点 ( 如:admin/user/index ),未填写时默认解释"菜单链接"判断是否拥有访问权限;' => 'Please fill in the system permission node (e.g., admin/user/index). If not filled in, the "Menu Link" will be used by default to determine access permissions', - '设置菜单选项前置图标,目前支持 layui 字体图标及 iconfont 定制字体图标。' => 'Set the prefix icon for menu options. Currently supports layui font icons and iconfont custom font icons.', - '请输入或选择图标' => 'Please enter or select an icon', - '请输入菜单名称' => 'Please enter menu name', - '请输入菜单链接' => 'Please enter menu link', - '请输入链接参数' => 'Please enter link parameters', - '请输入权限节点' => 'Please enter permission node', - '请输入登录账号' => 'Please enter login account', - '请输入用户名称' => 'Please enter user name', - '请输入联系电子邮箱' => 'Please enter contact email', - '请输入用户联系手机' => 'Please enter user contact mobile', - '请输入常用的联系QQ' => 'Please enter commonly used contact QQ', - '请输入旧的登录密码' => 'Please enter old login password', - '请输入新的登录密码' => 'Please enter new login password', - - // 系统配置表单提示 - '请输入登录页面的表单标题' => 'Please enter login form title', - '后台登录入口是由英文字母开头,且不能有相同名称的模块,设置之后原地址不能继续访问,请谨慎配置 ~' => 'Backend login entry must start with English letters and cannot have modules with the same name. After setting, the original address cannot be accessed. Please configure carefully.', - '请输入32位JWT接口密钥' => 'Please enter 32-bit JWT API key', - '请输入 32 位 JWT 接口密钥,在使用 JWT 接口时需要使用此密钥进行加密及签名!' => 'Please enter a 32-bit JWT API key. This key is required for encryption and signing when using JWT interfaces!', - '请上传浏览器图标' => 'Please upload browser icon', - '建议上传 128x128 或 256x256 的 JPG,PNG,JPEG 图片,保存后会自动生成 48x48 的 ICO 文件 ~' => 'It is recommended to upload JPG, PNG, or JPEG images of 128x128 or 256x256. After saving, a 48x48 ICO file will be automatically generated.', - '请输入网站名称' => 'Please enter site name', - '网站名称将显示在浏览器的标签上 ~' => 'Site name will be displayed on the browser tab.', - '请输入程序名称' => 'Please enter program name', - '管理程序名称显示在后台左上标题处 ~' => 'Management program name is displayed in the top left title of the backend.', - '请输入程序版本' => 'Please enter program version', - '管理程序版本显示在后台左上标题处 ~' => 'Management program version is displayed in the top left title of the backend.', - '请输入公安安备号' => 'Please enter public security registration number', - '请输入网站备案号' => 'Please enter website registration number', - '请输入版权信息' => 'Please enter copyright information', - '网站备案号和公安备案号可以在备案管理中心查询并获取,网站上线时必需配置备案号,备案号会链接到信息备案管理系统 ~' => 'Website registration number and public security registration number can be queried and obtained at the Registration Management Center. Registration numbers must be configured when the website goes online, and will be linked to the information registration management system.', - - // 数据字典表单提示 - '数据类型' => 'Data Type', - '请选择数据类型,数据创建后不能再次修改哦 ~' => 'Please select data type. Data type cannot be modified after creation.', - '请输入数据类型' => 'Please enter data type', - '请输入新的数据类型,数据创建后不能再次修改哦 ~' => 'Please enter new data type. Data type cannot be modified after creation.', - '数据编码' => 'Data Code', - '请输入新的数据编码,数据创建后不能再次修改,同种数据类型的数据编码不能出现重复 ~' => 'Please enter new data code. Data code cannot be modified after creation, and duplicate codes are not allowed for the same data type.', - '数据名称' => 'Data Name', - '请输入当前数据名称,请尽量保持名称的唯一性,数据名称尽量不要出现重复 ~' => 'Please enter data name. Try to keep the name unique and avoid duplicates.', - '数据内容' => 'Data Content', - '请输入数据内容' => 'Please enter data content', - - // 数据字典列表 - '添加数据' => 'Add Data', - '确定要批量删除数据吗?' => 'Are you sure you want to batch delete data?', - '数据状态' => 'Data Status', - '数据操作' => 'Actions', - '编辑数据' => 'Edit Data', - '确定要删除数据吗?' => 'Are you sure you want to delete data?', -]); diff --git a/app/admin/route/demo.php b/app/admin/route/demo.php deleted file mode 100644 index af132e079..000000000 --- a/app/admin/route/demo.php +++ /dev/null @@ -1,55 +0,0 @@ -route->post('index/pass', static function () { - return json(['code' => 0, 'info' => lang('演示环境禁止修改用户密码!')]); - }); - Library::$sapp->route->post('config/system', static function () { - return json(['code' => 0, 'info' => lang('演示环境禁止修改系统配置!')]); - }); - Library::$sapp->route->post('config/storage', static function () { - return json(['code' => 0, 'info' => lang('演示环境禁止修改系统配置!')]); - }); - Library::$sapp->route->post('menu', static function () { - return json(['code' => 0, 'info' => lang('演示环境禁止给菜单排序!')]); - }); - Library::$sapp->route->post('menu/index', static function () { - return json(['code' => 0, 'info' => lang('演示环境禁止给菜单排序!')]); - }); - Library::$sapp->route->post('menu/add', static function () { - return json(['code' => 0, 'info' => lang('演示环境禁止添加菜单!')]); - }); - Library::$sapp->route->post('menu/edit', static function () { - return json(['code' => 0, 'info' => lang('演示环境禁止编辑菜单!')]); - }); - Library::$sapp->route->post('menu/state', static function () { - return json(['code' => 0, 'info' => lang('演示环境禁止禁用菜单!')]); - }); - Library::$sapp->route->post('menu/remove', static function () { - return json(['code' => 0, 'info' => lang('演示环境禁止删除菜单!')]); - }); - Library::$sapp->route->post('user/pass', static function () { - return json(['code' => 0, 'info' => lang('演示环境禁止修改密码!')]); - }); -} diff --git a/app/admin/view/api/icon.html b/app/admin/view/api/icon.html deleted file mode 100644 index 4ed6d426a..000000000 --- a/app/admin/view/api/icon.html +++ /dev/null @@ -1,134 +0,0 @@ - - - - - {block name="title"}{$title|default=''}{if !empty($title)} · {/if}{:sysconf('site_name')}{/block} - - - - - - - - - {if file_exists(syspath("public/static/extra/icon/iconfont.css"))} - - {/if} - - - - - - - - - - \ No newline at end of file diff --git a/app/admin/view/api/upload/image.html b/app/admin/view/api/upload/image.html deleted file mode 100644 index 9bc6456ff..000000000 --- a/app/admin/view/api/upload/image.html +++ /dev/null @@ -1,161 +0,0 @@ -
- -
- -
-
-
-
- {php} $tag = '{{data.length}}'; {/php} - {:lang('已选 %s 张,确认', [$tag])} -
-
-
- - - - \ No newline at end of file diff --git a/app/admin/view/auth/form.html b/app/admin/view/auth/form.html deleted file mode 100644 index b1961ba06..000000000 --- a/app/admin/view/auth/form.html +++ /dev/null @@ -1,143 +0,0 @@ -{extend name='main'} - -{block name="button"} - - -{/block} - -{block name="content"} -
-
-
- - -
- {:lang('功能节点')}Auth Nodes -
    -
    -
    - {notempty name='vo.id'}{/notempty} -
    - - -
    -
    -
    -
    -{/block} - -{block name="script"} - -{/block} - -{block name="style"} - -{/block} \ No newline at end of file diff --git a/app/admin/view/auth/index.html b/app/admin/view/auth/index.html deleted file mode 100644 index ee0995c89..000000000 --- a/app/admin/view/auth/index.html +++ /dev/null @@ -1,76 +0,0 @@ -{extend name='table'} - -{block name="button"} - - - - - - - -{/block} - -{block name="content"} -
    - {include file='auth/index_search'} -
    -
    -{/block} - -{block name='script'} - - - - - - - - - - -{/block} \ No newline at end of file diff --git a/app/admin/view/base/form.html b/app/admin/view/base/form.html deleted file mode 100644 index 59b60da72..000000000 --- a/app/admin/view/base/form.html +++ /dev/null @@ -1,68 +0,0 @@ -
    - -
    - -
    -
    {:lang('数据类型')}Data Type
    - {if isset($vo.type)} - - {else} - - - {/if} -

    {:lang('请选择数据类型,数据创建后不能再次修改哦 ~')}

    -
    - -

    {:lang('请输入新的数据类型,数据创建后不能再次修改哦 ~')}

    -
    -
    - - - - - - - -
    - -
    - {notempty name='vo.id'}{/notempty} - -
    - - -
    - -
    \ No newline at end of file diff --git a/app/admin/view/base/index.html b/app/admin/view/base/index.html deleted file mode 100644 index 3e90f79d1..000000000 --- a/app/admin/view/base/index.html +++ /dev/null @@ -1,86 +0,0 @@ -{extend name='table'} - -{block name="button"} - - - - - - - -{/block} - -{block name="content"} -
    - -
    - {include file='base/index_search'} -
    -
    -
    -{/block} - -{block name='script'} - - - - - - - - - - -{/block} \ No newline at end of file diff --git a/app/admin/view/base/index_search.html b/app/admin/view/base/index_search.html deleted file mode 100644 index 9ea566ebf..000000000 --- a/app/admin/view/base/index_search.html +++ /dev/null @@ -1,42 +0,0 @@ - \ No newline at end of file diff --git a/app/admin/view/config/index.html b/app/admin/view/config/index.html deleted file mode 100644 index 3b543f34e..000000000 --- a/app/admin/view/config/index.html +++ /dev/null @@ -1,234 +0,0 @@ -{extend name="main"} - -{block name="button"} - -{:lang('清理无效配置')} - - - -{:lang('修改系统参数')} - -{/block} - -{block name="content"} - -
    -
    - - {:lang('运行模式')}( {:lang('仅超级管理员可配置')} ) - -
    -
    - -
    -

    {:lang('开发模式')}:{:lang('开发人员或在功能调试时使用,系统异常时会显示详细的错误信息,同时还会记录操作日志及数据库 SQL 语句信息。')}

    -

    {:lang('生产模式')}:{:lang('项目正式部署上线后使用,系统异常时统一显示 “%s”,只记录重要的异常日志信息,强烈推荐上线后使用此模式。',[config('app.error_message')])}

    -
    -
    -
    - -
    -
    - - {:lang('富编辑器')}( {:lang('仅超级管理员可配置')} ) - -
    -
    -
    - {if !in_array(sysconf('base.editor'),['ckeditor4','ckeditor5','wangEditor','auto'])}{php}sysconf('base.editor','ckeditor4');{/php}{/if} - {foreach ['ckeditor4'=>'CKEditor4','ckeditor5'=>'CKEditor5','wangEditor'=>'wangEditor','auto'=>lang('自适应模式')] as $k => $v}{if sysconf('base.editor') eq $k} - {if auth('storage')}{$v}{else}{$v}{/if} - {else} - {if auth('storage')}{$v}{else}{$v}{/if} - {/if}{/foreach} -
    -
    -

    CKEditor4:{:lang('旧版本编辑器,对浏览器兼容较好,但内容编辑体验稍有不足。')}

    -

    CKEditor5:{:lang('新版本编辑器,只支持新特性浏览器,对内容编辑体验较好,推荐使用。')}

    -

    wangEditor:{:lang('国产优质富文本编辑器,对于小程序及App内容支持会更友好,推荐使用。')}

    -

    {:lang('自适应模式')}:{:lang('优先使用新版本编辑器,若浏览器不支持新版本时自动降级为旧版本编辑器。')}

    -
    -
    -
    - - -
    -
    - - {:lang('存储引擎')}( {:lang('文件默认存储方式')} ) - -
    - - {if !sysconf('storage.type')}{php}sysconf('storage.type','local');{/php}{/if} - {if !sysconf('storage.link_type')}{php}sysconf('storage.link_type','none');{/php}{/if} - {if !sysconf('storage.name_type')}{php}sysconf('storage.name_type','xmd5');{/php}{/if} - {if !sysconf('storage.allow_exts')}{php}sysconf('storage.allow_exts','doc,gif,ico,jpg,mp3,mp4,p12,pem,png,rar,xls,xlsx');{/php}{/if} - {if !sysconf('storage.local_http_protocol')}{php}sysconf('storage.local_http_protocol','follow');{/php}{/if} -
    -
    - {foreach $files as $k => $v}{if sysconf('storage.type') eq $k} - {if auth('storage')}{$v}{else}{$v}{/if} - {else} - {if auth('storage')}{$v}{else}{$v}{/if} - {/if}{/foreach} -
    -
    -

    {:lang('本地服务器存储')}:{:lang('文件上传到本地服务器的 `static/upload` 目录,不支持大文件上传,占用服务器磁盘空间,访问时消耗服务器带宽流量。')}

    -

    {:lang('自建Alist存储')}:{:lang('文件上传到 Alist 存储的服务器或云存储空间,根据服务配置可支持大文件上传,不占用本身服务器空间及服务器带宽流量。')}

    -

    {:lang('七牛云对象存储')}:{:lang('文件上传到七牛云存储空间,支持大文件上传,不占用服务器空间及服务器带宽流量,支持 CDN 加速访问,访问量大时推荐使用。')}

    -

    {:lang('又拍云USS存储')}:{:lang('文件上传到又拍云 USS 存储空间,支持大文件上传,不占用服务器空间及服务器带宽流量,支持 CDN 加速访问,访问量大时推荐使用。')}

    -

    {:lang('阿里云OSS存储')}:{:lang('文件上传到阿里云 OSS 存储空间,支持大文件上传,不占用服务器空间及服务器带宽流量,支持 CDN 加速访问,访问量大时推荐使用。')}

    -

    {:lang('腾讯云COS存储')}:{:lang('文件上传到腾讯云 COS 存储空间,支持大文件上传,不占用服务器空间及服务器带宽流量,支持 CDN 加速访问,访问量大时推荐使用。')}

    -
    -
    -
    - -
    -
    - - {:lang('系统参数')}( {:lang('当前系统配置参数')} ) - -
    -
    -
    -
    {:lang('网站名称')}Website
    - -
    {:lang('网站名称及网站图标,将显示在浏览器的标签上。')}
    -
    -
    -
    {:lang('管理程序名称')}Name
    - -
    {:lang('管理程序名称,将显示在后台左上角标题。')}
    -
    -
    -
    {:lang('管理程序版本')}Version
    - -
    {:lang('管理程序版本,将显示在后台左上角标题。')}
    -
    -
    -
    {:lang('公安备案号')}Beian
    - -

    - {:lang('公安备案号,可以在 %s 查询获取,将在登录页面下面显示。',['www.beian.gov.cn'])} -

    -
    -
    -
    {:lang('网站备案号')}Miitbeian
    - -
    - {:lang('网站备案号,可以在 %s 查询获取,将显示在登录页面下面。',['beian.miit.gov.cn'])} -
    -
    -
    -
    {:lang('网站版权信息')}Copyright
    - -
    {:lang('网站版权信息,在后台登录页面显示版本信息并链接到备案到信息备案管理系统。')}
    -
    -
    -
    - - -
    -
    - - {:lang('系统信息')}( {:lang('仅开发模式可见')} ) - -
    -
    - - - - - - - - - - - - - - - - - - - - - - - - - -
    {:lang('核心框架')}ThinkPHP Version {$framework.version|default='None'}
    {:lang('平台框架')}ThinkAdmin Version {$thinkadmin.version|default='6.0.0'}
    {:lang('操作系统')}{:php_uname()}
    {:lang('运行环境')}{:ucfirst($request->server('SERVER_SOFTWARE',php_sapi_name()))} & PHP {$Think.const.PHP_VERSION} & {:ucfirst(app()->db->connect()->getConfig('type'))}
    {:lang('系统序号')}{$systemid|default=''}
    -
    -
    - -{notempty name='plugins'} -
    -
    - - {:lang('应用插件')}( {:lang('仅开发模式可见')} ) - -
    -
    - - - - - - - - - - - - {foreach $plugins as $key=>$plugin} - - - - - - - - {/foreach} - -
    {:lang('应用名称')}{:lang('插件名称')}{:lang('插件包名')}{:lang('插件版本')}{:lang('授权协议')}
    {$key}{$plugin.name|lang} - {if empty($plugin.install.document)}{$plugin.package} - {else}{$plugin.package}{/if} - {$plugin.install.version|default='unknow'} - {if empty($plugin.install.license)} - - {elseif is_array($plugin.install.license)}{$plugin.install.license|join='、',###} - {else}{$plugin.install.license|default='-'}{/if} -
    -
    -
    -{/notempty} - -{/block} \ No newline at end of file diff --git a/app/admin/view/config/storage-0.html b/app/admin/view/config/storage-0.html deleted file mode 100644 index b74b121ab..000000000 --- a/app/admin/view/config/storage-0.html +++ /dev/null @@ -1,49 +0,0 @@ -
    - -
    -
    - {foreach ['xmd5'=>'文件哈希值 ( 支持秒传 )','date'=>'日期+随机 ( 普通上传 )'] as $k=>$v} - - {/foreach} -
    -

    类型为“文件哈希”时可以实现文件秒传功能,同一个文件只需上传一次节省存储空间,推荐使用。

    -
    -
    - -
    - -
    -
    - {foreach ['none'=>'简洁链接','full'=>'完整链接','none+compress'=>'简洁并压缩图片','full+compress'=>'完整并压缩图片'] as $k=>$v} - - {/foreach} -
    -

    类型为“简洁链接”时链接将只返回 hash 地址,而“完整链接”将携带参数保留文件名,图片压缩功能云平台会单独收费。

    -
    -
    - -
    - -
    - -

    设置系统允许上传的文件后缀,多个以英文逗号隔开如:png,jpg,rar,doc,未包含在设置内的文件后缀将不被允许上传。

    -
    -
    \ No newline at end of file diff --git a/app/admin/view/config/storage-alioss.html b/app/admin/view/config/storage-alioss.html deleted file mode 100644 index 63b7efa80..000000000 --- a/app/admin/view/config/storage-alioss.html +++ /dev/null @@ -1,98 +0,0 @@ -
    -
    - -
    -

    文件将上传到 阿里云 OSS 存储,需要配置 OSS 公开访问及跨域策略

    -

    配置跨域访问 CORS 规则,设置:来源 Origin 为 *,允许 Methods 为 POST,允许 Headers 为 *

    -
    - - {include file='config/storage-0'} - -
    - -
    - {if !sysconf('storage.alioss_http_protocol')}{php}sysconf('storage.alioss_http_protocol','http');{/php}{/if} -
    - {foreach ['http'=>'HTTP','https'=>'HTTPS','auto'=>"AUTO"] as $protocol=>$remark} - - {/foreach} -
    -

    阿里云OSS存储访问协议,其中 HTTPS 需要配置证书才能使用(AUTO 为相对协议)

    -
    -
    - -
    - -
    - -

    阿里云OSS存储空间所在区域,需要严格对应储存所在区域才能上传文件

    -
    -
    - -
    - -
    - -

    填写阿里云OSS存储空间名称,如:think-admin-oss(需要是全区唯一的值,不存在时会自动创建)

    -
    -
    - -
    - -
    - -

    填写阿里云OSS存储外部访问域名,不需要填写访问协议,如:static.alioss.thinkadmin.top

    -
    -
    - -
    - -
    - -

    可以在 [ 阿里云 > 个人中心 ] 设置并获取到访问密钥

    -
    -
    - -
    - -
    - -

    可以在 [ 阿里云 > 个人中心 ] 设置并获取到安全密钥

    -
    -
    - -
    - - -
    - - -
    - -
    -
    \ No newline at end of file diff --git a/app/admin/view/config/storage-alist.html b/app/admin/view/config/storage-alist.html deleted file mode 100644 index 33982d283..000000000 --- a/app/admin/view/config/storage-alist.html +++ /dev/null @@ -1,81 +0,0 @@ -
    -
    - -
    -

    文件将上传到 Alist 自建存储,需要自行搭建 Alist 存储服务器。

    -

    Alist 是一个支持多种存储的文件列表程序,可将各种云盘及本地磁盘资源进行整合。

    -

    建议不要开放匿名用户访问,尽量使用独立账号管理,需要关闭 “签名所有” 让文件可以直接访问。

    -
    - - {include file='config/storage-0'} - -
    - -
    - {if !sysconf('storage.alist_http_protocol')}{php}sysconf('storage.alist_http_protocol','http');{/php}{/if} -
    - {foreach ['http'=>'HTTP','https'=>'HTTPS','auto'=>"AUTO"] as $protocol=>$remark} - - {/foreach} -
    -

    请选择 Alist 存储访问协议,其中 HTTPS 需要配置证书才能使用( AUTO 为相对协议 )

    -
    -
    - -
    - -
    - -

    请填写 Alist 存储访问域名,不需要填写访问协议,如:storage.thinkadmin.top

    -
    -
    - -
    - -
    - -

    请填写 Alist 用户基本目录的相对存储位置,填写 / 表示用户基本目录( 需要拥有读写权限 )

    -
    -
    - -
    - -
    - -

    请填写 Alist 用户账号,注意此账号需要拥有上面存储目录的访问权限。

    -
    -
    - -
    - -
    - -

    请填写 Alist 用户登录密码,用于生成文件上传的接口认证令牌,如果填写错误将无法上传文件。

    -
    -
    - -
    - - -
    - - -
    -
    -
    \ No newline at end of file diff --git a/app/admin/view/config/storage-local.html b/app/admin/view/config/storage-local.html deleted file mode 100644 index 1b1386f64..000000000 --- a/app/admin/view/config/storage-local.html +++ /dev/null @@ -1,51 +0,0 @@ -
    -
    - -
    -

    文件将存储在本地服务器,默认保存在 public/upload 目录,文件以 HASH 命名。

    -

    文件存储的目录需要有读写权限,有足够的存储空间。特别注意,本地存储暂不支持图片压缩!

    -
    - - {include file='config/storage-0'} - -
    - -
    - {if !sysconf('storage.local_http_protocol')}{php}sysconf('storage.local_http_protocol','follow');{/php}{/if} -
    - {foreach ['follow'=>'FOLLOW','http'=>'HTTP','https'=>'HTTPS','path'=>'PATH','auto'=>'AUTO'] as $protocol=>$remark} - - {/foreach} -
    -

    本地存储访问协议,其中 HTTPS 需要配置证书才能使用( FOLLOW 跟随系统,PATH 文件路径,AUTO 相对协议 )

    -
    -
    - -
    - -
    - -

    填写上传后的访问域名(不指定时根据当前访问地址自动计算),不需要填写访问协议,如:static.thinkadmin.top

    -
    -
    - -
    - - -
    - - -
    - -
    -
    \ No newline at end of file diff --git a/app/admin/view/config/storage-qiniu.html b/app/admin/view/config/storage-qiniu.html deleted file mode 100644 index 9bc4aaf03..000000000 --- a/app/admin/view/config/storage-qiniu.html +++ /dev/null @@ -1,97 +0,0 @@ -
    -
    - -
    -

    文件将上传到 七牛云 存储,对象存储需要配置为公开访问的 Bucket 空间

    - 完成实名认证后可获得 10G 免费存储空间哦!我要免费申请 -
    - - {include file='config/storage-0'} - -
    - -
    - {if !sysconf('storage.qiniu_http_protocol')}{php}sysconf('storage.qiniu_http_protocol','http');{/php}{/if} -
    - {foreach ['http'=>'HTTP','https'=>'HTTPS','auto'=>"AUTO"] as $protocol=>$remark} - - {/foreach} -
    -

    七牛云存储访问协议,其中 HTTPS 需要配置证书才能使用( AUTO 为相对协议 )

    -
    -
    - -
    - -
    - -

    七牛云存储空间所在区域,需要严格对应储存所在区域才能上传文件

    -
    -
    - -
    - -
    - -

    填写七牛云存储空间名称,如:static

    -
    -
    - -
    - -
    - -

    填写七牛云存储访问域名,不需要填写访问协议,如:static.qiniu.thinkadmin.top

    -
    -
    - -
    - -
    - -

    可以在 [ 七牛云 > 个人中心 ] 设置并获取到访问密钥

    -
    -
    - -
    - -
    - -

    可以在 [ 七牛云 > 个人中心 ] 设置并获取到安全密钥

    -
    -
    - -
    - - -
    - - -
    -
    -
    \ No newline at end of file diff --git a/app/admin/view/config/storage-txcos.html b/app/admin/view/config/storage-txcos.html deleted file mode 100644 index 06d8d72ba..000000000 --- a/app/admin/view/config/storage-txcos.html +++ /dev/null @@ -1,96 +0,0 @@ -
    -
    - -
    -

    文件将上传到 腾讯云 COS 存储,需要配置 COS 公有读私有写访问权限及跨域策略

    -

    配置跨域访问 CORS 规则,设置来源 Origin 为 *,允许 Methods 为 POST,允许 Headers 为 *

    -
    - - {include file='config/storage-0'} - -
    - -
    - {if !sysconf('storage.txcos_http_protocol')}{php}sysconf('storage.txcos_http_protocol','http');{/php}{/if} - {foreach ['http'=>'HTTP','https'=>'HTTPS','auto'=>"AUTO"] as $protocol=>$remark} - - {/foreach} -

    腾讯云COS存储访问协议,其中 HTTPS 需要配置证书才能使用( AUTO 为相对协议 )

    -
    -
    - -
    - -
    - -

    腾讯云COS存储空间所在区域,需要严格对应储存所在区域才能上传文件

    -
    -
    - -
    - -
    - -

    填写腾讯云COS存储空间名称,如:thinkadmin-1251143395

    -
    -
    - -
    - -
    - -

    填写腾讯云COS存储外部访问域名,不需要填写访问协议,如:static.txcos.thinkadmin.top

    -
    -
    - -
    - -
    - -

    可以在 [ 腾讯云 > 个人中心 ] 设置并获取到访问密钥

    -
    -
    - -
    - -
    - -

    可以在 [ 腾讯云 > 个人中心 ] 设置并获取到安全密钥

    -
    -
    - -
    - - -
    - - -
    - -
    -
    \ No newline at end of file diff --git a/app/admin/view/config/storage-upyun.html b/app/admin/view/config/storage-upyun.html deleted file mode 100644 index 71043fe12..000000000 --- a/app/admin/view/config/storage-upyun.html +++ /dev/null @@ -1,81 +0,0 @@ -
    -
    - -
    -

    文件将上传到 又拍云 USS 存储,需要配置 USS 公开访问及跨域策略

    -

    配置跨域访问 CORS 规则,设置来源 Origin 为 *,允许 Methods 为 POST,允许 Headers 为 *

    -
    - - {include file='config/storage-0'} - -
    - -
    - {if !sysconf('storage.upyun_http_protocol')}{php}sysconf('storage.upyun_http_protocol','http');{/php}{/if} -
    - {foreach ['http'=>'HTTP','https'=>'HTTPS','auto'=>"AUTO"] as $protocol=>$remark} - - {/foreach} -
    -

    又拍云存储访问协议,其中 HTTPS 需要配置证书才能使用(AUTO 为相对协议)

    -
    -
    - -
    - -
    - -

    填写又拍云存储空间名称,如:think-admin-uss(需要是全区唯一的值,不存在时会自动创建)

    -
    -
    - -
    - -
    - -

    填写又拍云存储外部访问域名,不需要填写访问协议,如:static.uss.thinkadmin.top

    -
    -
    - -
    - -
    - -

    可以在 [ 账户管理 > 操作员 ] 设置操作员账号并将空间给予授权。

    -
    -
    - -
    - -
    - -

    可以在 [ 账户管理 > 操作员 ] 设置操作员密码并将空间给予授权

    -
    -
    - -
    - - -
    - - -
    - -
    -
    \ No newline at end of file diff --git a/app/admin/view/config/system.html b/app/admin/view/config/system.html deleted file mode 100644 index bdc7385a6..000000000 --- a/app/admin/view/config/system.html +++ /dev/null @@ -1,129 +0,0 @@ -
    -
    - -
    -
    - -
    -
    -
    {:lang('后台登录入口')}Login Entry
    - -
    -
    -
    {:lang('后台默认配色')}Theme Style
    - -
    -
    - {:lang('后台登录入口是由英文字母开头,且不能有相同名称的模块,设置之后原地址不能继续访问,请谨慎配置 ~')} -
    -
    - -
    -
    {:lang('登录背景图片')}Background Image
    -
    - -
    -
    - -
    -
    {:lang('JWT 接口密钥')}Jwt Key
    - -
    - {:lang('请输入 32 位 JWT 接口密钥,在使用 JWT 接口时需要使用此密钥进行加密及签名!')} -
    -
    - -
    -
    {:lang('浏览器小图标')}Browser Icon
    -
    - - -
    -
    - {:lang('建议上传 128x128 或 256x256 的 JPG,PNG,JPEG 图片,保存后会自动生成 48x48 的 ICO 文件 ~')} -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    - {:lang('网站备案号和公安备案号可以在备案管理中心查询并获取,网站上线时必需配置备案号,备案号会链接到信息备案管理系统 ~')} -
    -
    -
    - -
    -
    - - -
    -
    - - \ No newline at end of file diff --git a/app/admin/view/file/form.html b/app/admin/view/file/form.html deleted file mode 100644 index afdcecb46..000000000 --- a/app/admin/view/file/form.html +++ /dev/null @@ -1,40 +0,0 @@ -
    - -
    - - - - - - - - - - - - -
    - -
    - {notempty name='vo.id'}{/notempty} - -
    - - -
    -
    \ No newline at end of file diff --git a/app/admin/view/file/index.html b/app/admin/view/file/index.html deleted file mode 100644 index 349c35a58..000000000 --- a/app/admin/view/file/index.html +++ /dev/null @@ -1,64 +0,0 @@ -{extend name='table'} - -{block name="button"} - -{:lang('清理重复')} - - -{:lang('批量删除')} - -{/block} - -{block name="content"} -
    - {include file='file/index_search'} -
    -
    - - - -{/block} diff --git a/app/admin/view/file/index_search.html b/app/admin/view/file/index_search.html deleted file mode 100644 index 9cf0a9eea..000000000 --- a/app/admin/view/file/index_search.html +++ /dev/null @@ -1,58 +0,0 @@ -
    - {:lang('条件搜索')} - -
    \ No newline at end of file diff --git a/app/admin/view/full.html b/app/admin/view/full.html deleted file mode 100644 index 75a4f243c..000000000 --- a/app/admin/view/full.html +++ /dev/null @@ -1,35 +0,0 @@ - - - - {block name="title"}{$title|default=''}{if !empty($title)} · {/if}{:sysconf('site_name')}{/block} - - - - - - - - - - - - {if file_exists(syspath("public/static/extra/icon/iconfont.css"))} - - {/if} - {block name="style"}{/block} - - - - -{block name='body'} -
    -
    {block name='content'}{/block}
    -
    -{/block} - - - - -{block name='script'}{/block} - - \ No newline at end of file diff --git a/app/admin/view/index/index-top.html b/app/admin/view/index/index-top.html deleted file mode 100644 index 2f4f35cf9..000000000 --- a/app/admin/view/index/index-top.html +++ /dev/null @@ -1,44 +0,0 @@ -
    - - -
    \ No newline at end of file diff --git a/app/admin/view/index/theme.html b/app/admin/view/index/theme.html deleted file mode 100644 index 870d7caff..000000000 --- a/app/admin/view/index/theme.html +++ /dev/null @@ -1,36 +0,0 @@ -
    -
    - -
    -
    后台配色方案Theme Style
    -
    - {foreach $themes as $k=>$v} - - {/foreach} -
    -

    切换配色方案,需要保存成功后配色方案才会永久生效,下次登录也会有效哦 ~

    -
    -
    - -
    -
    - - -
    -
    - - \ No newline at end of file diff --git a/app/admin/view/login/index.html b/app/admin/view/login/index.html deleted file mode 100644 index c78dc64f8..000000000 --- a/app/admin/view/login/index.html +++ /dev/null @@ -1,57 +0,0 @@ -{extend name="index/index"} - -{block name='style'} - - - -{/block} - -{block name="body"} -
    -
    - {:sysconf('app_name')}{:sysconf('app_version')} - {notempty name='runtimeMode'} - - Fork me on Gitee - - {/notempty} -
    -
    -

    {:sysconf('login_name')?:'系统管理'}

    -
      -
    • - -
    • -
    • - -
    • -
    • - - -
    • -
    • - -
    • -
    -
    - -
    -{/block} - -{block name='script'} - -{/block} \ No newline at end of file diff --git a/app/admin/view/menu/form.html b/app/admin/view/menu/form.html deleted file mode 100644 index abb4c1a8b..000000000 --- a/app/admin/view/menu/form.html +++ /dev/null @@ -1,97 +0,0 @@ -
    - -
    - -
    - -
    - -

    {:lang('必选')},{:lang('请选择上级菜单或顶级菜单 ( 目前最多支持三级菜单 )')}

    -
    -
    - -
    - -
    - -

    {:lang('必选')},{:lang('请填写菜单名称 ( 如:系统管理 ),建议字符不要太长,一般 4-6 个汉字')}

    -
    -
    - -
    - -
    - -

    - {:lang('必选')},{:lang('请填写链接地址或选择系统节点 ( 如:https://domain.com/admin/user/index.html 或 admin/user/index )')} -
    {:lang('当填写链接地址时,以下面的 "权限节点" 来判断菜单自动隐藏或显示,注意未填写 "权限节点" 时将不会隐藏该菜单哦')} -

    -
    -
    - -
    - -
    - -

    {:lang('可选')},{:lang('设置菜单链接的 GET 访问参数 ( 如:name=1&age=3 )')}

    -
    -
    - -
    - -
    - -

    {:lang('可选')},{:lang('请填写系统权限节点 ( 如:admin/user/index ),未填写时默认解释"菜单链接"判断是否拥有访问权限;')}

    -
    -
    - -
    - -
    -
    - -
    - - - - -

    {:lang('可选')},{:lang('设置菜单选项前置图标,目前支持 layui 字体图标及 iconfont 定制字体图标。')}

    -
    -
    - -
    - -
    - {notempty name='vo.id'}{/notempty} - -
    - - -
    -
    - - \ No newline at end of file diff --git a/app/admin/view/menu/index.html b/app/admin/view/menu/index.html deleted file mode 100644 index 4dd759085..000000000 --- a/app/admin/view/menu/index.html +++ /dev/null @@ -1,119 +0,0 @@ -{extend name='table'} - -{block name="button"} - - - - - - - - - - - -{/block} - -{block name="content"} -
    - -
    - -
    -
    - - - - - - - - - - - -{/block} \ No newline at end of file diff --git a/app/admin/view/oplog/index.html b/app/admin/view/oplog/index.html deleted file mode 100644 index f14488bbd..000000000 --- a/app/admin/view/oplog/index.html +++ /dev/null @@ -1,47 +0,0 @@ -{extend name='table'} - -{block name="button"} - - - - - - - -{/block} - -{block name="content"} -
    - {include file='oplog/index_search'} -
    -
    -{/block} - -{block name='script'} - - - -{/block} \ No newline at end of file diff --git a/app/admin/view/oplog/index_search.html b/app/admin/view/oplog/index_search.html deleted file mode 100644 index c955ed81a..000000000 --- a/app/admin/view/oplog/index_search.html +++ /dev/null @@ -1,87 +0,0 @@ -
    - {:lang('条件搜索')} - -
    - - \ No newline at end of file diff --git a/app/admin/view/queue/index.html b/app/admin/view/queue/index.html deleted file mode 100644 index bd1c0f5fb..000000000 --- a/app/admin/view/queue/index.html +++ /dev/null @@ -1,131 +0,0 @@ -{extend name='table'} - -{block name="button"} - -{if isset($super) and $super} - -{:lang('优化数据库')} - -{if isset($iswin) and ($iswin or php_sapi_name() eq 'cli')} - - -{/if} - -{if auth("clean")} - -{/if} - -{/if} - -{if auth("remove")} - -{/if} - -{/block} - -{block name="content"} -
    - - - {:lang('服务状态')}:{:lang('检查中')} - - - - - {:lang('任务统计')}:{:lang('待处理 %s 个任务,处理中 %s 个任务,已完成 %s 个任务,已失败 %s 个任务。', [ - '..', - '..', - '..', - '..' - ])} - -
    - -
    - {include file='queue/index_search'} -
    -
    -{/block} - -{block name='script'} - - - -{/block} diff --git a/app/admin/view/queue/index_search.html b/app/admin/view/queue/index_search.html deleted file mode 100644 index bd79d6bbb..000000000 --- a/app/admin/view/queue/index_search.html +++ /dev/null @@ -1,45 +0,0 @@ -
    - {:lang('条件搜索')} - -
    \ No newline at end of file diff --git a/app/admin/view/user/form.html b/app/admin/view/user/form.html deleted file mode 100644 index 54936e871..000000000 --- a/app/admin/view/user/form.html +++ /dev/null @@ -1,114 +0,0 @@ -
    -
    - -
    - {:lang('用户账号')} - -
    -
    - - -
    -
    - -
    -
    - -
    -
    - -
    - - {if !empty($bases) || !empty($auths)} -
    - {:lang('用户权限')} - {if !empty($bases)} -
    -
    {:lang('角色身份')}Role Identity
    -
    - {foreach $bases as $base} - - {/foreach} -
    -
    - {/if} - {if !empty($auths)} -
    -
    {:lang('访问权限')}Role Permission
    -
    - {if isset($vo.username) and $vo.username eq $super} - {:lang('超级用户拥所有访问权限,不需要配置权限。')} - {else}{foreach $auths as $authorize} - - {/foreach}{/if} -
    -
    - {/if} -
    - {/if} - -
    - {:lang('用户资料')} -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    - -
    - -
    - {notempty name='vo.id'}{/notempty} - -
    - - -
    -
    \ No newline at end of file diff --git a/app/admin/view/user/index_search.html b/app/admin/view/user/index_search.html deleted file mode 100644 index 793f1e00a..000000000 --- a/app/admin/view/user/index_search.html +++ /dev/null @@ -1,44 +0,0 @@ - \ No newline at end of file diff --git a/app/admin/view/user/pass.html b/app/admin/view/user/pass.html deleted file mode 100644 index d281ad904..000000000 --- a/app/admin/view/user/pass.html +++ /dev/null @@ -1,40 +0,0 @@ -
    -
    - - - - - - - - - - - - -
    - -
    - {notempty name='vo.id'}{/notempty} - -
    - - -
    -
    \ No newline at end of file diff --git a/app/wechat/Service.php b/app/wechat/Service.php deleted file mode 100644 index 272671f81..000000000 --- a/app/wechat/Service.php +++ /dev/null @@ -1,109 +0,0 @@ -commands([Fans::class, Auto::class, Clear::class]); - - // 注册粉丝关注事件 - $this->app->event->listen('WechatFansSubscribe', static function ($openid) { - AutoService::register($openid); - }); - - // 注册支付通知路由 - $this->app->route->any('/plugin-wxpay-notify/:vars', static function (Request $request) { - try { - $data = json_decode(CodeExtend::deSafe64($request->param('vars')), true); - return PaymentService::notify($data); - } catch (\Error|\Exception $exception) { - return "Error: {$exception->getMessage()}"; - } - }); - } - - /** - * 增加微信配置. - * @return array[] - */ - public static function menu(): array - { - $code = app(static::class)->appCode; - // 设置插件菜单 - return [ - [ - 'name' => '微信管理', - 'subs' => [ - ['name' => '微信接口配置', 'icon' => 'layui-icon layui-icon-set', 'node' => "{$code}/config/options"], - ['name' => '微信支付配置', 'icon' => 'layui-icon layui-icon-rmb', 'node' => "{$code}/config/payment"], - ], - ], - [ - 'name' => '微信定制', - 'subs' => [ - ['name' => '微信粉丝管理', 'icon' => 'layui-icon layui-icon-username', 'node' => "{$code}/fans/index"], - ['name' => '微信图文管理', 'icon' => 'layui-icon layui-icon-template-1', 'node' => "{$code}/news/index"], - ['name' => '微信菜单配置', 'icon' => 'layui-icon layui-icon-cellphone', 'node' => "{$code}/menu/index"], - ['name' => '回复规则管理', 'icon' => 'layui-icon layui-icon-engine', 'node' => "{$code}/keys/index"], - ['name' => '关注自动回复', 'icon' => 'layui-icon layui-icon-release', 'node' => "{$code}/auto/index"], - ], - ], - [ - 'name' => '微信支付', - 'subs' => [ - ['name' => '微信支付行为', 'icon' => 'layui-icon layui-icon-rmb', 'node' => "{$code}/payment.record/index"], - ['name' => '微信退款管理', 'icon' => 'layui-icon layui-icon-engine', 'node' => "{$code}/payment.refund/index"], - ], - ], - ]; - } -} diff --git a/app/wechat/model/WechatAuto.php b/app/wechat/model/WechatAuto.php deleted file mode 100644 index 67286adfc..000000000 --- a/app/wechat/model/WechatAuto.php +++ /dev/null @@ -1,59 +0,0 @@ -hasOne(WechatFans::class, 'openid', 'openid'); - } - - /** - * 绑定用户粉丝数据. - */ - public function bindFans(): HasOne - { - return $this->fans()->bind([ - 'fans_headimg' => 'headimgurl', - 'fans_nickname' => 'nickname', - ]); - } - - /** - * 格式化输出时间格式. - * @param mixed $value - */ - public function getCreateTimeAttr($value): string - { - return $value ? format_datetime($value) : ''; - } - - /** - * 格式化输出时间格式. - * @param mixed $value - */ - public function getUpdateTimeAttr($value): string - { - return $value ? format_datetime($value) : ''; - } - - /** - * 格式化输出时间格式. - * @param mixed $value - */ - public function getPaymentTimeAttr($value): string - { - return $value ? format_datetime($value) : ''; - } - - /** - * 转换数据类型. - */ - public function toArray(): array - { - $data = parent::toArray(); - $data['type_name'] = PaymentService::tradeTypeNames[$data['type']] ?? $data['type']; - return $data; - } -} diff --git a/app/wechat/model/WechatPaymentRefund.php b/app/wechat/model/WechatPaymentRefund.php deleted file mode 100644 index c15c343d6..000000000 --- a/app/wechat/model/WechatPaymentRefund.php +++ /dev/null @@ -1,81 +0,0 @@ -hasOne(WechatPaymentRecord::class, 'code', 'record_code')->with('bindfans'); - } - - /** - * 格式化输出时间格式. - * @param mixed $value - */ - public function getCreateTimeAttr($value): string - { - return $value ? format_datetime($value) : ''; - } - - /** - * 格式化输出时间格式. - * @param mixed $value - */ - public function getUpdateTimeAttr($value): string - { - return $value ? format_datetime($value) : ''; - } - - /** - * 格式化输出时间格式. - * @param mixed $value - */ - public function getRefundTimeAttr($value): string - { - return $value ? format_datetime($value) : ''; - } -} diff --git a/app/wechat/service/WechatService.php b/app/wechat/service/WechatService.php deleted file mode 100644 index 915095109..000000000 --- a/app/wechat/service/WechatService.php +++ /dev/null @@ -1,367 +0,0 @@ - -// +---------------------------------------------------------------------- -// | 官方网站: https://thinkadmin.top -// +---------------------------------------------------------------------- -// | 开源协议 ( https://mit-license.org ) -// | 免责声明 ( https://thinkadmin.top/disclaimer ) -// +---------------------------------------------------------------------- -// || github 代码仓库:https://github.com/zoujingli/think-plugs-wechat -// +---------------------------------------------------------------------- - -namespace app\wechat\service; - -use think\admin\Exception; -use think\admin\extend\JsonRpcClient; -use think\admin\Library; -use think\admin\Service; -use think\admin\storage\LocalStorage; -use think\exception\HttpResponseException; -use think\Response; -use WeChat\Exceptions\InvalidResponseException; -use WeChat\Exceptions\LocalCacheException; - -/** - * 微信接口调度服务 - * @class WechatService - * - * @method \WeChat\Card WeChatCard() static 微信卡券管理 - * @method \WeChat\Custom WeChatCustom() static 微信客服消息 - * @method \WeChat\Limit WeChatLimit() static 接口调用频次限制 - * @method \WeChat\Media WeChatMedia() static 微信素材管理 - * @method \WeChat\Draft WeChatDraft() static 微信草稿箱管理 - * @method \WeChat\Menu WeChatMenu() static 微信菜单管理 - * @method \WeChat\Oauth WeChatOauth() static 微信网页授权 - * @method \WeChat\Pay WeChatPay() static 微信支付商户 - * @method \WeChat\Product WeChatProduct() static 微信商店管理 - * @method \WeChat\Qrcode WeChatQrcode() static 微信二维码管理 - * @method \WeChat\Receive WeChatReceive() static 微信推送管理 - * @method \WeChat\Scan WeChatScan() static 微信扫一扫接入管理 - * @method \WeChat\Script WeChatScript() static 微信前端支持 - * @method \WeChat\Shake WeChatShake() static 微信揺一揺周边 - * @method \WeChat\Tags WeChatTags() static 微信用户标签管理 - * @method \WeChat\Template WeChatTemplate() static 微信模板消息 - * @method \WeChat\User WeChatUser() static 微信粉丝管理 - * @method \WeChat\Wifi WeChatWifi() static 微信门店WIFI管理 - * @method \WeChat\Freepublish WeChatFreepublish() static 发布能力 - * - * ----- WeMini ----- - * @method \WeMini\Account WeMiniAccount() static 小程序账号管理 - * @method \WeMini\Basic WeMiniBasic() static 小程序基础信息设置 - * @method \WeMini\Code WeMiniCode() static 小程序代码管理 - * @method \WeMini\Domain WeMiniDomain() static 小程序域名管理 - * @method \WeMini\Tester WeMinitester() static 小程序成员管理 - * @method \WeMini\User WeMiniUser() static 小程序帐号管理 - * -------------------- - * @method \WeMini\Crypt WeMiniCrypt() static 小程序数据加密处理 - * @method \WeMini\Delivery WeMiniDelivery() static 小程序即时配送 - * @method \WeMini\Guide WeMiniGuide() static 小程序导购助手 - * @method \WeMini\Image WeMiniImage() static 小程序图像处理 - * @method \WeMini\Live WeMiniLive() static 小程序直播接口 - * @method \WeMini\Logistics WeMiniLogistics() static 小程序物流助手 - * @method \WeMini\Newtmpl WeMiniNewtmpl() static 公众号小程序订阅消息支持 - * @method \WeMini\Message WeMiniMessage() static 小程序动态消息 - * @method \WeMini\Operation WeMiniOperation() static 小程序运维中心 - * @method \WeMini\Ocr WeMiniOcr() static 小程序ORC服务 - * @method \WeMini\Plugs WeMiniPlugs() static 小程序插件管理 - * @method \WeMini\Poi WeMiniPoi() static 小程序地址管理 - * @method \WeMini\Qrcode WeMiniQrcode() static 小程序二维码管理 - * @method \WeMini\Security WeMiniSecurity() static 小程序内容安全 - * @method \WeMini\Soter WeMiniSoter() static 小程序生物认证 - * @method \WeMini\Template WeMiniTemplate() static 小程序模板消息支持 - * @method \WeMini\Total WeMiniTotal() static 小程序数据接口 - * @method \WeMini\Scheme WeMiniScheme() static 小程序URL-Scheme - * @method \WeMini\Search WeMiniSearch() static 小程序搜索 - * @method \WeMini\Shipping WeMiniShipping() static 小程序发货信息管理服务 - * - * ----- WePay ----- - * @method \WePay\Bill WePayBill() static 微信商户账单及评论 - * @method \WePay\Order WePayOrder() static 微信商户订单 - * @method \WePay\Refund WePayRefund() static 微信商户退款 - * @method \WePay\Coupon WePayCoupon() static 微信商户代金券 - * @method \WePay\Custom WePayCustom() static 微信扩展上报海关 - * @method \WePay\ProfitSharing WePayProfitSharing() static 微信分账 - * @method \WePay\Redpack WePayRedpack() static 微信红包支持 - * @method \WePay\Transfers WePayTransfers() static 微信商户打款到零钱 - * @method \WePay\TransfersBank WePayTransfersBank() static 微信商户打款到银行卡 - * - * ----- WePayV3 ----- - * @method \WePayV3\Order WePayV3Order() static 直连商户|订单支付接口 - * @method \WePayV3\Transfers WePayV3Transfers() static 微信商家转账到零钱 - * @method \WePayV3\ProfitSharing WePayV3ProfitSharing() static 微信商户分账 - * - * ----- WeOpen ----- - * @method \WeOpen\Login WeOpenLogin() static 第三方微信登录 - * @method \WeOpen\Service WeOpenService() static 第三方服务 - * - * ----- ThinkService ----- - * @method mixed ThinkServiceConfig() static 平台服务配置 - */ -class WechatService extends Service -{ - /** - * 静态初始化对象 - * @return mixed - * @throws Exception - */ - public static function __callStatic(string $name, array $arguments) - { - [$type, $base, $class] = static::parseName($name); - if ("{$type}{$base}" !== $name) { - throw new Exception("抱歉,实例 {$name} 不符合规则!"); - } - if (sysconf('wechat.type') === 'api' || in_array($type, ['WePay', 'WePayV3'])) { - if (class_exists($class)) { - return new $class($type === 'WeMini' ? static::getWxconf() : static::getConfig()); - } - throw new Exception("抱歉,接口模式无法实例 {$class} 对象!"); - } else { - [$appid, $appkey] = [sysconf('wechat.thr_appid'), sysconf('wechat.thr_appkey')]; - $data = ['class' => $name, 'appid' => $appid, 'time' => time(), 'nostr' => uniqid()]; - $data['sign'] = md5("{$data['class']}#{$appid}#{$appkey}#{$data['time']}#{$data['nostr']}"); - // 创建远程连接,默认使用 JSON-RPC 方式调用接口 - $token = enbase64url(json_encode($data, JSON_UNESCAPED_UNICODE)); - $jsonrpc = sysconf('wechat.service_jsonrpc|raw') ?: 'https://open.cuci.cc/plugin-wechat-service/api.client/jsonrpc?token=TOKEN'; - return new JsonRpcClient(str_replace('token=TOKEN', "token={$token}", $jsonrpc)); - } - } - - /** - * 获取当前微信APPID. - * @throws Exception - */ - public static function getAppid(): string - { - if (static::getType() === 'api') { - return sysconf('wechat.appid'); - } - return sysconf('wechat.thr_appid'); - } - - /** - * 获取接口授权模式. - * @throws Exception - */ - public static function getType(): string - { - $type = strtolower(sysconf('wechat.type')); - if (in_array($type, ['api', 'thr'])) { - return $type; - } - throw new Exception('请在后台配置微信对接授权模式'); - } - - /** - * 获取公众号配置参数. - * @param bool $ispay 获取支付参数 - * @throws Exception - */ - public static function getConfig(bool $ispay = false): array - { - $config = [ - 'appid' => static::getAppid(), - 'token' => sysconf('wechat.token'), - 'appsecret' => sysconf('wechat.appsecret'), - 'encodingaeskey' => sysconf('wechat.encodingaeskey'), - 'cache_path' => syspath('runtime/wechat'), - ]; - return $ispay ? static::withWxpayCert($config) : $config; - } - - /** - * 获取小程序配置参数. - * @param bool $ispay 获取支付参数 - * @throws Exception - */ - public static function getWxconf(bool $ispay = false): array - { - $wxapp = sysdata('plugin.wechat.wxapp'); - $config = [ - 'appid' => $wxapp['appid'] ?? '', - 'appsecret' => $wxapp['appkey'] ?? '', - 'cache_path' => syspath('runtime/wechat'), - ]; - return $ispay ? static::withWxpayCert($config) : $config; - } - - /** - * 处理支付证书配置. - * @throws Exception - */ - public static function withWxpayCert(array $options): array - { - // 文本模式主要是为了解决分布式部署 - $data = sysdata('plugin.wechat.payment'); - if (empty($data['mch_id'])) { - throw new Exception('无效的支付配置!'); - } - $name1 = sprintf('wxpay/%s_%s_cer.pem', $data['mch_id'], md5($data['ssl_cer_text'])); - $name2 = sprintf('wxpay/%s_%s_key.pem', $data['mch_id'], md5($data['ssl_key_text'])); - $local = LocalStorage::instance(); - if ($local->has($name1, true) && $local->has($name2, true)) { - $sslCer = $local->path($name1, true); - $sslKey = $local->path($name2, true); - } else { - $sslCer = $local->set($name1, $data['ssl_cer_text'], true)['file']; - $sslKey = $local->set($name2, $data['ssl_key_text'], true)['file']; - } - $options['mch_id'] = $data['mch_id']; - $options['mch_key'] = $data['mch_key']; - $options['mch_v3_key'] = $data['mch_v3_key']; - $options['ssl_cer'] = $sslCer; - $options['ssl_key'] = $sslKey; - $options['cert_public'] = $sslCer; - $options['cert_private'] = $sslKey; - $options['mp_cert_serial'] = $data['mch_pay_sid'] ?? ''; - $options['mp_cert_content'] = $data['ssl_pay_text'] ?? ''; - return $options; - } - - /** - * 获取会话名称. - */ - public static function getSsid(): string - { - $conf = Library::$sapp->session->getConfig(); - $ssid = Library::$sapp->request->get($conf['name'] ?? 'ssid'); - return empty($ssid) ? Library::$sapp->session->getId() : $ssid; - } - - /** - * 通过网页授权获取粉丝信息. - * @param string $source 回跳URL地址 - * @param int $isfull 获取资料模式 - * @param bool $redirect 是否直接跳转 - * @throws InvalidResponseException - * @throws LocalCacheException - * @throws Exception - */ - public static function getWebOauthInfo(string $source, int $isfull = 0, bool $redirect = true): array - { - [$ssid, $appid] = [static::getSsid(), static::getAppid()]; - $openid = Library::$sapp->cache->get("{$ssid}_openid"); - $userinfo = Library::$sapp->cache->get("{$ssid}_fansinfo"); - if ((empty($isfull) && !empty($openid)) || (!empty($isfull) && !empty($openid) && !empty($userinfo))) { - empty($userinfo) || FansService::set($userinfo, $appid); - return ['openid' => $openid, 'fansinfo' => $userinfo]; - } - if (static::getType() === 'api') { - // 解析 GET 参数 - parse_str(parse_url($source, PHP_URL_QUERY), $params); - $getVars = [ - 'code' => $params['code'] ?? input('code', ''), - 'rcode' => $params['rcode'] ?? input('rcode', ''), - 'state' => $params['state'] ?? input('state', ''), - ]; - $wechat = static::WeChatOauth(); - if ($getVars['state'] !== $appid || empty($getVars['code'])) { - $params['rcode'] = enbase64url($source); - $location = strstr("{$source}?", '?', true) . '?' . http_build_query($params); - $oauthurl = $wechat->getOauthRedirect($location, $appid, $isfull ? 'snsapi_userinfo' : 'snsapi_base'); - throw new HttpResponseException(static::createRedirect($oauthurl, $redirect)); - } - if (($token = $wechat->getOauthAccessToken($getVars['code'])) && isset($token['openid'])) { - $openid = $token['openid']; - // 如果是虚拟账号,不保存会话信息,下次重新授权 - if (empty($token['is_snapshotuser'])) { - Library::$sapp->cache->set("{$ssid}_openid", $openid, 3600); - } - if ($isfull && isset($token['access_token'])) { - $userinfo = $wechat->getUserInfo($token['access_token'], $openid); - // 如果是虚拟账号,不保存会话信息,下次重新授权 - if (empty($token['is_snapshotuser'])) { - $userinfo['is_snapshotuser'] = 0; - // 缓存用户信息 - Library::$sapp->cache->set("{$ssid}_fansinfo", $userinfo, 3600); - empty($userinfo) || FansService::set($userinfo, $appid); - } else { - $userinfo['is_snapshotuser'] = 1; - } - } - } - if ($getVars['rcode']) { - throw new HttpResponseException(static::createRedirect(debase64url($getVars['rcode']), $redirect)); - } - if ((empty($isfull) && !empty($openid)) || (!empty($isfull) && !empty($openid) && !empty($userinfo))) { - return ['openid' => $openid, 'fansinfo' => $userinfo]; - } - throw new Exception('Query params [rcode] not find.'); - } else { - $result = static::ThinkServiceConfig()->oauth(self::getSsid(), $source, $isfull); - [$openid, $userinfo] = [$result['openid'] ?? '', $result['fans'] ?? []]; - // 如果是虚拟账号,不保存会话信息,下次重新授权 - if (empty($result['token']['is_snapshotuser'])) { - Library::$sapp->cache->set("{$ssid}_openid", $openid, 3600); - Library::$sapp->cache->set("{$ssid}_fansinfo", $userinfo, 3600); - } - if ((empty($isfull) && !empty($openid)) || (!empty($isfull) && !empty($openid) && !empty($userinfo))) { - empty($result['token']['is_snapshotuser']) && empty($userinfo) || FansService::set($userinfo, $appid); - return ['openid' => $openid, 'fansinfo' => $userinfo]; - } - throw new HttpResponseException(static::createRedirect($result['url'], $redirect)); - } - } - - /** - * 获取微信网页JSSDK签名参数. - * @param null|string $location 签名地址 - * @throws InvalidResponseException - * @throws LocalCacheException - * @throws Exception - */ - public static function getWebJssdkSign(?string $location = null): array - { - $location = $location ?: Library::$sapp->request->url(true); - if (static::getType() === 'api') { - return static::WeChatScript()->getJsSign($location); - } - return static::ThinkServiceConfig()->jsSign($location); - } - - /** - * 解析调用对象名称. - */ - private static function parseName(string $name): array - { - foreach (['WeChat', 'WeMini', 'WeOpen', 'WePayV3', 'WePay', 'ThinkService'] as $type) { - if (strpos($name, $type) === 0) { - [, $base] = explode($type, $name); - return [$type, $base, "\\{$type}\\{$base}"]; - } - } - return ['-', '-', $name]; - } - - /** - * 网页授权链接跳转. - * @param string $location 跳转链接 - * @param bool $redirect 强制跳转 - */ - private static function createRedirect(string $location, bool $redirect = true): Response - { - return $redirect ? redirect($location) : response(join(";\n", [ - sprintf("sessionStorage.setItem('wechat.session','%s')", self::getSsid()), - sprintf("location.replace('%s')", $location), '', - ])); - } -} diff --git a/app/wechat/view/config/options.html b/app/wechat/view/config/options.html deleted file mode 100644 index 46beaa849..000000000 --- a/app/wechat/view/config/options.html +++ /dev/null @@ -1,53 +0,0 @@ -{extend name="main"} - -{block name="button"} - - - - - - - - - -{/block} - -{block name="content"} -
    -
    - {foreach ['api'=>lang('微信公众平台直接模式'),'thr'=>lang('微信开放平台授权模式')] as $k=>$v} - - {/foreach} -
    {:lang('请选择微信对接方式,其中微信开放平台授权模式需要微信开放平台支持,还需要搭建第三方服务平台托管系统!')}
    -
    -
    -
    -
    -
    {include file='config/options_form_api'}
    -
    {include file='config/options_form_thr'}
    -
    -
    -{/block} - -{block name='script'} - -{/block} diff --git a/app/wechat/view/config/options_test.html b/app/wechat/view/config/options_test.html deleted file mode 100644 index 6298de1ff..000000000 --- a/app/wechat/view/config/options_test.html +++ /dev/null @@ -1,21 +0,0 @@ -
    -
    -
    -
    - img -

    OAUTH 网页授权

    -
    -
    - img -

    JSSDK 接口签名

    -
    -
    -
    -
    - - diff --git a/app/wechat/view/config/payment_test.html b/app/wechat/view/config/payment_test.html deleted file mode 100644 index 18fdd2c8d..000000000 --- a/app/wechat/view/config/payment_test.html +++ /dev/null @@ -1,40 +0,0 @@ -
    -
    -
    -
    -

    微信开放平台授权

    -

    JSSDK 签名测试需要在开放平台配置当前的授权域名:{:request()->host()}

    -
    -
    -

    公众号平台域名授权

    -

    网页授权及 JSSDK 签名都需要在公众号平台授权域名:{:request()->host()}

    -
    -
    -

    微信商户支付测试配置

    -

    JSAPI 支付测试需要在微信商户平台配置支付目录:{:dirname(url('api.test/index',[],'',true))}/

    -

    扫码支付①需要在微信商户平台配置支付通知地址:{:url('api.test/scanOneNotify',[],'',true)}

    -
    -
    -
    -
    - img -

    微信 JSAPI 支付

    -
    -
    - img -

    微信扫码支付①

    -
    -
    - img -

    微信扫码支付②

    -
    -
    -
    -
    - - diff --git a/database/migrations/.published.json b/database/migrations/.published.json new file mode 100644 index 000000000..bcce25e3f --- /dev/null +++ b/database/migrations/.published.json @@ -0,0 +1,34 @@ +{ + "20241010000005_install_account20241010.php": { + "source": "plugin/think-plugs-account/stc/database/20241010000005_install_account20241010.php", + "mtime": 1778063923 + }, + "20241010000006_install_payment20241010.php": { + "source": "plugin/think-plugs-payment/stc/database/20241010000006_install_payment20241010.php", + "mtime": 1778063923 + }, + "20241010000001_install_system20241010.php": { + "source": "plugin/think-plugs-system/stc/database/20241010000001_install_system20241010.php", + "mtime": 1778063812 + }, + "20241010000003_install_wechat20241010.php": { + "source": "plugin/think-plugs-wechat-client/stc/database/20241010000003_install_wechat20241010.php", + "mtime": 1778063923 + }, + "20241010000009_install_wechat_service20241010.php": { + "source": "plugin/think-plugs-wechat-service/stc/database/20241010000009_install_wechat_service20241010.php", + "mtime": 1778063923 + }, + "20241010000007_install_wemall20241010.php": { + "source": "plugin/think-plugs-wemall/stc/database/20241010000007_install_wemall20241010.php", + "mtime": 1778063923 + }, + "20241010000008_install_worker20241010.php": { + "source": "plugin/think-plugs-worker/stc/database/20241010000008_install_worker20241010.php", + "mtime": 1774236276 + }, + "20241010000010_install_wuma20241010.php": { + "source": "plugin/think-plugs-wuma/stc/database/20241010000010_install_wuma20241010.php", + "mtime": 1778063923 + } +} diff --git a/plugin/think-library/.github/workflows/release.yml b/plugin/think-library/.github/workflows/release.yml new file mode 100644 index 000000000..39d622da8 --- /dev/null +++ b/plugin/think-library/.github/workflows/release.yml @@ -0,0 +1,75 @@ +####### 可解析的提交前缀 ######## +# ci: 持续集成 +# fix: 修改 +# feat: 新增 +# refactor: 重构 +# docs: 文档 +# style: 样式 +# chore: 其他 +# build: 构建 +# pref: 优化 +# test: 测试 +############################### + +on: + push: + tags: + - 'v*' # 仅匹配 v* 版本标签,如 v1.0、v20.15.10 + +name: Create Release +permissions: write-all + +jobs: + release: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 18 + + - name: Install dependencies + run: npm install -g gen-git-log + + - name: Find Last Tag + id: last_tag + run: | + # 获取所有标签,按版本号降序排序 + all_tags=$(git tag --list --sort=-version:refname) + + # 获取最新的标签 + LATEST_TAG=$(echo "$all_tags" | head -n 1) + + # 获取倒数第二个标签(如果有) + SECOND_LATEST_TAG=$(echo "$all_tags" | sed -n '2p') + + # 如果没有任何标签,默认 v1.0.0 + LATEST_TAG=${LATEST_TAG:-v1.0.0} + SECOND_LATEST_TAG=${SECOND_LATEST_TAG:-v1.0.0} + + # 设置环境变量 + echo "LATEST_TAG=$LATEST_TAG" >> $GITHUB_ENV + echo "SECOND_LATEST_TAG=$SECOND_LATEST_TAG" >> $GITHUB_ENV + + - name: Generate Release Notes + run: | + rm -rf log + mkdir -p log + git-log -m tag -f -S $SECOND_LATEST_TAG -v ${LATEST_TAG#v} + + - name: Create Release + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ env.LATEST_TAG }} + release_name: Release ${{ env.LATEST_TAG }} + body_path: log/${{ env.LATEST_TAG }}.md + draft: false + prerelease: false \ No newline at end of file diff --git a/plugin/think-library/.gitignore b/plugin/think-library/.gitignore new file mode 100644 index 000000000..5f92c81a4 --- /dev/null +++ b/plugin/think-library/.gitignore @@ -0,0 +1,8 @@ +.git +.svn +.idea +*.cache +/vendor +/composer.lock +!.gitignore +!composer.json diff --git a/plugin/think-library/.php-cs-fixer.php b/plugin/think-library/.php-cs-fixer.php new file mode 100644 index 000000000..2e44859af --- /dev/null +++ b/plugin/think-library/.php-cs-fixer.php @@ -0,0 +1,120 @@ +setRiskyAllowed(true)->setParallelConfig(new ParallelConfig(8, 24)); +$finder = Finder::create()->in(__DIR__)->exclude(['vendor', 'public', 'runtime']); +return $config->setFinder($finder)->setUsingCache(false)->setRules([ + '@PSR2' => true, + '@Symfony' => true, + '@DoctrineAnnotation' => true, + '@PhpCsFixer' => true, + 'header_comment' => [ + 'comment_type' => 'PHPDoc', + 'header' => $header, + 'separate' => 'none', + 'location' => 'after_declare_strict', + ], + 'array_syntax' => [ + 'syntax' => 'short', + ], + 'list_syntax' => [ + 'syntax' => 'short', + ], + 'blank_line_before_statement' => [ + 'statements' => [ + 'declare', + ], + ], + 'general_phpdoc_annotation_remove' => [ + 'annotations' => [ + 'author', + ], + ], + 'ordered_imports' => [ + 'imports_order' => [ + 'class', 'function', 'const', + ], + 'sort_algorithm' => 'alpha', + ], + 'single_line_comment_style' => [ + 'comment_types' => [ + ], + ], + 'yoda_style' => [ + 'always_move_variable' => false, + 'equal' => false, + 'identical' => false, + ], + 'phpdoc_align' => [ + 'align' => 'left', + ], + 'multiline_whitespace_before_semicolons' => [ + 'strategy' => 'no_multi_line', + ], + 'constant_case' => [ + 'case' => 'lower', + ], + 'encoding' => true, // PHP代码必须只使用没有BOM的UTF-8 + 'line_ending' => true, // 所有的PHP文件编码必须一致 + 'single_quote' => true, // 简单字符串应该使用单引号代替双引号 + 'no_empty_statement' => true, // 不应该存在空的结构体 + 'standardize_not_equals' => true, // 使用 <> 代替 != + 'blank_line_after_namespace' => true, // 命名空间之后空一行 + 'no_empty_phpdoc' => true, // 不应该存在空的 phpdoc + 'no_empty_comment' => true, // 不应该存在空注释 + 'no_singleline_whitespace_before_semicolons' => true, // 禁止在关闭分号前使用单行空格 + 'concat_space' => ['spacing' => 'one'], // 连接字符是否需要空格,可选配置项 none:不需要 one:一个空格 + 'no_leading_import_slash' => true, // use 语句中取消前置斜杠 + 'cast_spaces' => ['space' => 'none'], + 'class_attributes_separation' => true, + 'combine_consecutive_unsets' => true, + 'declare_strict_types' => true, + 'lowercase_static_reference' => true, + 'linebreak_after_opening_tag' => true, + 'multiline_comment_opening_closing' => true, + 'no_useless_else' => true, + 'no_unused_imports' => true, + 'not_operator_with_successor_space' => false, + 'not_operator_with_space' => false, + 'ordered_class_elements' => true, + 'php_unit_strict' => false, + 'phpdoc_separation' => false, +]); diff --git a/plugin/think-library/composer.json b/plugin/think-library/composer.json new file mode 100644 index 000000000..7b4a8ef74 --- /dev/null +++ b/plugin/think-library/composer.json @@ -0,0 +1,60 @@ +{ + "name": "zoujingli/think-library", + "version": "8.0.x-dev", + "license": "MIT", + "homepage": "https://thinkadmin.top", + "description": "Library for ThinkAdmin", + "authors": [ + { + "name": "Anyon", + "email": "zoujingli@qq.com" + } + ], + "support": { + "email": "zoujingli@qq.com", + "wiki": "https://thinkadmin.top", + "forum": "https://thinkadmin.top", + "source": "https://gitee.com/zoujingli/ThinkLibrary", + "issues": "https://gitee.com/zoujingli/ThinkLibrary/issues" + }, + "require": { + "php": "^8.1", + "ext-gd": "*", + "ext-curl": "*", + "ext-json": "*", + "ext-zlib": "*", + "ext-iconv": "*", + "ext-openssl": "*", + "ext-mbstring": "*", + "topthink/think-orm": "^4.0", + "topthink/framework": "^8.1" + }, + "autoload": { + "files": [ + "src/common.php" + ], + "psr-4": { + "think\\admin\\": "src" + } + }, + "require-dev": { + "phpunit/phpunit": "^9.5|^10.0" + }, + "autoload-dev": { + "psr-4": { + "think\\admin\\tests\\": "tests" + } + }, + "extra": { + "think": { + "services": [ + "think\\admin\\Library" + ] + } + }, + "prefer-stable": true, + "minimum-stability": "dev", + "config": { + "sort-packages": true + } +} diff --git a/plugin/think-library/license b/plugin/think-library/license new file mode 100644 index 000000000..2aba58015 --- /dev/null +++ b/plugin/think-library/license @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2014~2025 邹景立 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/plugin/think-library/phpunit.xml.dist b/plugin/think-library/phpunit.xml.dist new file mode 100644 index 000000000..6ea0d19c7 --- /dev/null +++ b/plugin/think-library/phpunit.xml.dist @@ -0,0 +1,19 @@ + + + + + ./tests + + + diff --git a/plugin/think-library/readme.md b/plugin/think-library/readme.md new file mode 100644 index 000000000..e077d038a --- /dev/null +++ b/plugin/think-library/readme.md @@ -0,0 +1,386 @@ +# ThinkLibrary + +核心基础库,为 ThinkAdmin 提供运行时基础设施。 + +## 功能定位 + +- 提供运行时服务、认证会话、路由适配、队列契约等核心能力 +- 定义标准控制器、模型、命令等基础类型 +- 提供 Helper 工具集(查询、表单、页面构建器) +- 实现 JWT 令牌、CacheSession 等认证机制 +- 提供 Storage 门面和标准契约 + +## 安装 + +```bash +composer require zoujingli/think-library +``` + +## 配置 + +在 `composer.json` 中注册服务: + +```json +{ + "extra": { + "think": { + "services": ["think\\admin\\Library"] + } + } +} +``` + +## 核心功能 + +### 1. 运行时服务 + +- **RuntimeService**: 运行时环境配置同步,处理 PHAR 兼容、目录初始化 +- **AppService**: 应用管理、插件发现、服务注册、配置管理 +- **NodeService**: 节点管理、权限判断、菜单节点处理 +- **QueueService**: 队列门面服务(真实实现在 Worker 插件) + +### 2. 认证会话 + +- **JwtToken**: JWT 令牌生成、验证、刷新 +- **CacheSession**: 基于 Token SID 的缓存会话管理 +- **RequestTokenService**: 请求级令牌识别服务 +- **SystemContext**: 系统上下文接口(运行时实现) +- **NullSystemContext**: 空实现上下文(未认证状态) + +### 3. 路由适配 + +- **Route**: 自定义路由对象,支持插件路由注册 +- **Url**: URL 构建工具,支持插件 URL 生成 +- **MultAccess**: 多应用访问中间件,处理插件前缀切换 + +### 4. 基础类型 + +- **Controller**: 标准控制器基类,提供通用控制器方法 +- **Model**: 标准模型基类,扩展软删除、时间戳等能力 +- **Command**: 标准命令基类,提供命令通用方法 +- **Plugin**: 插件管理类,处理插件元数据加载 +- **Service**: 服务基类,提供基础服务方法 +- **Exception**: 框架异常类,统一异常处理 + +### 5. Helper 工具 + +- **QueryHelper**: 数据查询构建器,支持分页、筛选、排序 +- **FormBuilder**: 表单构建器,支持表单元素快速生成 +- **PageBuilder**: 列表页面构建器,支持表格、筛选、操作列 +- **ValidateHelper**: 数据验证器,支持规则验证、错误提示 + +### 6. 扩展工具 + +- **CodeToolkit**: 编码工具(加密/解密/Base64/Hash) +- **FileTools**: 文件操作工具(目录创建、文件复制、权限管理) +- **HttpClient**: HTTP 客户端(cURL 封装、请求构建) +- **ArrayTree**: 数组树工具(树形结构构建、扁平化) +- **FaviconBuilder**: 网站图标生成器 +- **ImageSliderVerify**: 图片滑块验证码 +- **JsonRpcHttpClient**: JSON-RPC HTTP 客户端 +- **JsonRpcHttpServer**: JSON-RPC HTTP 服务端 + +## 使用示例 + +### JWT 令牌 + +```php +use think\admin\service\JwtToken; + +// 生成令牌 +$token = JwtToken::token([ + 'user_id' => 1, + 'username' => 'admin', + 'exp' => time() + 7200 +]); + +// 验证令牌 +try { + $data = JwtToken::verify($token); + // $data['user_id'], $data['username'] +} catch (\think\admin\Exception $e) { + // 验证失败 +} +``` + +### CacheSession + +```php +use think\admin\service\CacheSession; + +// 写入会话 +CacheSession::set('key', 'value', 3600); + +// 读取会话 +$value = CacheSession::get('key', 'default'); + +// 批量写入 +CacheSession::put([ + 'key1' => 'value1', + 'key2' => 'value2' +], 3600); + +// 删除会话 +CacheSession::delete('key'); + +// 清空会话 +CacheSession::clear(); +``` + +### 控制器基类 + +```php +success('操作成功', ['id' => 1]); + + // 返回失败 + $this->error('操作失败'); + + // 返回视图 + $this->fetch('index/index', ['title' => '首页']); + + // 数据验证 + $data = $this->_vali([ + 'username.require' => '用户名不能为空', + 'email.email' => '邮箱格式不正确' + ], 'post'); + + // 创建异步任务 + $this->_queue('导出数据', 'php think export:users'); + } +} +``` + +### 查询构建器 + +```php +use think\admin\helper\QueryHelper; + +// 基础查询 +$query = QueryHelper::init(User::class) + ->where('status', 1) + ->order('id', 'DESC') + ->paginate(20); + +// 分页查询 +$page = $query->getPage(); +$list = $query->getList(); + +// 条件筛选 +$query->addWhere($request->get('keyword'), 'username', 'like'); +$query->addFilter($request->get('status'), 'status'); +``` + +### 表单构建器 + +```php +use think\admin\helper\FormBuilder; + +$form = FormBuilder::create(); + +// 文本输入 +$form->text('username', '用户名') + ->required() + ->placeholder('请输入用户名'); + +// 下拉选择 +$form->select('status', '状态') + ->options([1 => '正常', 0 => '禁用']) + ->default(1); + +// 日期选择 +$form->date('birthday', '生日'); + +// 文件上传 +$form->file('avatar', '头像') + ->image() + ->size(2); // 限制 2MB + +// 保存数据 +if ($form->isPost()) { + $data = $form->getData(); + // 保存逻辑 +} +``` + +## 目录结构 + +``` +think-library/ +├── src/ +│ ├── contract/ # 标准契约接口 +│ │ ├── SystemContextInterface.php +│ │ └── ... +│ ├── extend/ # 扩展工具类 +│ │ ├── ArrayTree.php +│ │ ├── CodeToolkit.php +│ │ ├── FileTools.php +│ │ └── HttpClient.php +│ ├── helper/ # 构建器工具 +│ │ ├── FormBuilder.php +│ │ ├── PageBuilder.php +│ │ ├── QueryHelper.php +│ │ └── ValidateHelper.php +│ ├── middleware/ # 中间件 +│ │ └── MultAccess.php +│ ├── model/ # 模型扩展 +│ │ └── QueryFactory.php +│ ├── route/ # 路由相关 +│ │ ├── Route.php +│ │ └── Url.php +│ ├── runtime/ # 运行时上下文 +│ │ ├── RequestContext.php +│ │ ├── RequestTokenService.php +│ │ ├── SystemContext.php +│ │ └── NullSystemContext.php +│ ├── service/ # 核心服务 +│ │ ├── AppService.php +│ │ ├── CacheSession.php +│ │ ├── JwtToken.php +│ │ ├── NodeService.php +│ │ ├── QueueService.php +│ │ └── RuntimeService.php +│ ├── common.php # 全局函数 +│ ├── Controller.php # 标准控制器 +│ ├── Model.php # 标准模型 +│ ├── Command.php # 标准命令 +│ ├── Plugin.php # 插件管理 +│ ├── Service.php # 服务基类 +│ ├── Library.php # 服务注册类 +│ └── Exception.php # 异常类 +└── tests/ # 单元测试 +``` + +## 全局函数 + +ThinkLibrary 提供以下全局函数: + +### 全局函数 + +ThinkLibrary 提供以下全局函数: + +#### 应用相关 + +- `syspath($path)`: 获取系统根目录路径(PHAR 环境下返回包内路径) +- `runpath($path)`: 获取运行时目录路径(PHAR 环境下返回外部可写目录) +- `sysconf($name, $default = null)`: 读取系统配置 +- `sysvar($key, $value = null)`: 读写系统变量 +- `isOnline()`: 判断是否生产环境(非调试模式) + +#### URL 相关 + +- `sysuri($node, $vars = [], $suffix = true)`: 生成系统 URL(后台页面) +- `apiuri($node, $vars = [], $suffix = true)`: 生成 API URL(接口调用) +- `plguri($node, $vars = [], $suffix = true)`: 生成插件工作台 URL(由 ThinkPlugsSystem 提供) + +#### 认证相关 + +- `auth($node)`: 检查权限(判断当前用户是否有指定节点权限) +- `admin_user()`: 获取当前管理员信息(已认证的系统用户) +- `tsession($name = null, $default = null)`: 读写 Token 会话(基于 CacheSession) + +#### 工具函数 + +- `xss_safe($str)`: XSS 安全过滤(过滤危险 HTML/JS 标签) +- `str2arr($str)`: 字符串转数组(支持逗号、分号、换行分隔) +- `arr2str($arr)`: 数组转字符串(逗号连接) +- `data_save($dbQuery, $data, $key = 'id', $where = [])`: 数据保存(新增或更新) +- `normalize($text)`: 文本标准化(全角转半角、统一空格等) + +## 依赖要求 + +- PHP >= 8.1 +- ThinkPHP >= 8.1 +- 扩展:gd, curl, json, zlib, iconv, openssl, mbstring, fileinfo +- 推荐:redis (用于 CacheSession 和缓存驱动) + +## 开发规范 + +### 命名空间 + +所有类使用 `think\admin\` 命名空间 + +### 代码风格 + +遵循 PSR-12 规范,使用 PHP-CS-Fixer 统一代码风格 + +```bash +# 运行代码风格修复 +vendor/bin/php-cs-fixer fix +``` + +### 测试规范 + +每个核心功能都应该有对应的单元测试 + +```bash +# 运行 ThinkLibrary 测试 +vendor/bin/phpunit plugin/think-library/tests/ +``` + +### 静态分析 + +使用 PHPStan 进行静态代码分析 + +```bash +# 运行代码分析 +composer analyse +``` + +## 认证说明 + +### JWT 令牌 + +- 后台认证统一使用 `Authorization: Bearer ` +- JWT 有效期由 `config/app.php` 中的 `system_token_expire` 配置 +- JWT 内包含 `sid` 用于绑定 CacheSession +- 不再使用标准 PHP Session 承载后台登录态 + +### Token Session + +- 基于 `CacheSession` 实现 +- 统一入口为 `tsession()` 函数 +- 支持 file 和 redis 等多种缓存驱动 +- 临时用户态绑定到 Token 的 `sid` + +## PHAR 兼容性 + +当使用 PHAR 打包运行时: + +- `syspath()` 返回 PHAR 包内路径(只读资源) +- `runpath()` 返回 PHAR 外部路径(可写目录) +- 字体、SQLite、缓存等落盘操作必须使用 `runpath()` +- GD 的 `imagettftext()` 不支持 `phar://` 路径 + +## 测试覆盖 + +当前测试覆盖包括: + +- 代码加密解密工具测试 +- JWT 令牌生成验证测试 +- 通用函数测试 +- 架构边界测试 +- 插件依赖边界测试 +- 迁移归属测试 +- 表单/页面构建器测试 +- 请求令牌服务测试 +- 多应用访问调度测试 + +## 许可证 + +MIT License + +## 相关链接 + +- 官网文档:https://thinkadmin.top +- Gitee: https://gitee.com/zoujingli/ThinkLibrary +- Github: https://github.com/zoujingli/ThinkLibrary diff --git a/plugin/think-library/src/Builder.php b/plugin/think-library/src/Builder.php new file mode 100644 index 000000000..e31e5eaea --- /dev/null +++ b/plugin/think-library/src/Builder.php @@ -0,0 +1,71 @@ +builder = $builder; + } + + public static function mk(string $type = 'form', string $mode = 'modal'): self + { + return new self(FormBuilder::make($type, $mode)); + } + + public function addTextInput(string $name, string $title, string $substr = '', bool $required = false, string $remark = '', ?string $pattern = null, array $attrs = []): self + { + $this->builder->addTextInput($name, $title, $substr, $required, $remark, $pattern, $attrs); + return $this; + } + + public function addSubmitButton(string $name = '保存数据', string $confirm = '', array $attrs = [], string $class = ''): self + { + $this->builder->addSubmitButton($name, $confirm, $attrs, $class); + return $this; + } + + public function addCancelButton(string $name = '取消编辑', string $confirm = '确定要取消编辑吗?', array $attrs = [], string $class = 'layui-btn-danger'): self + { + $this->builder->addCancelButton($name, $confirm, $attrs, $class); + return $this; + } + + public function fetch(array $vars = []): mixed + { + return $this->builder->fetch($vars); + } + + public function __call(string $name, array $arguments): mixed + { + $result = $this->builder->{$name}(...$arguments); + return $result === $this->builder ? $this : $result; + } +} diff --git a/plugin/think-library/src/Command.php b/plugin/think-library/src/Command.php new file mode 100644 index 000000000..3e627af29 --- /dev/null +++ b/plugin/think-library/src/Command.php @@ -0,0 +1,112 @@ +queue->message($total, $count, $message, $backline); + return $this; + } + + /** + * Command initialization. + */ + protected function initialize(Input $input, Output $output): static + { + $this->queue = QueueService::instance(); + if (($code = QueueService::currentCode()) !== '' && $this->queue->getCode() !== $code) { + $this->queue->initialize($code); + } + + return $this; + } + + /** + * 设置队列错误消息. + */ + protected function setQueueError(string $message): void + { + if (QueueService::inContext()) { + $this->queue->error($message); + } else { + $this->writeConsoleMessage($message); + exit(0); + } + } + + /** + * 设置队列成功消息. + */ + protected function setQueueSuccess(string $message): void + { + if (QueueService::inContext()) { + $this->queue->success($message); + } else { + $this->writeConsoleMessage($message); + exit(0); + } + } + + /** + * 设置队列进度消息. + */ + protected function setQueueProgress(?string $message = null, ?string $progress = null, int $backline = 0): static + { + if (QueueService::inContext()) { + $this->queue->progress(QueueService::STATE_LOCK, $message, $progress, $backline); + } elseif (is_string($message)) { + $this->writeConsoleMessage($message, $backline); + } + + return $this; + } + + /** + * 输出控制台消息. + */ + protected function writeConsoleMessage(string $message, int $backline = 0): void + { + while ($backline-- > 0) { + $message = "\033[1A\r\033[K{$message}"; + } + + $this->output->write($message . PHP_EOL); + } +} diff --git a/plugin/think-library/src/Controller.php b/plugin/think-library/src/Controller.php new file mode 100644 index 000000000..6c4b768e4 --- /dev/null +++ b/plugin/think-library/src/Controller.php @@ -0,0 +1,348 @@ +request->action(), get_class_methods(__CLASS__))) { + $this->error('禁止访问内置方法!'); + } + $this->get = $app->request->get(); + $this->app = $app->bind('think\admin\Controller', $this); + $this->node = NodeService::getCurrent(); + $this->request = $this->app->request; + $this->initialize(); + } + + /** + * 返回失败的内容. + * @param mixed $info 消息内容 + * @param mixed $data 返回数据 + * @param mixed $code 返回代码 + */ + public function error(mixed $info, mixed $data = '{-null-}', mixed $code = 500): never + { + $this->respond($info, $data, $code, 500); + } + + /** + * 返回成功的内容. + * @param mixed $info 消息内容 + * @param mixed $data 返回数据 + * @param mixed $code 返回代码 + */ + public function success(mixed $info, mixed $data = '{-null-}', mixed $code = 200): never + { + $this->respond($info, $data, $code, 200); + } + + /** + * 输出标准 JSON 响应. + * @param mixed $info 消息内容 + * @param mixed $data 返回数据 + * @param mixed $code 返回代码 + * @param int $default 默认状态 + */ + protected function respond(mixed $info, mixed $data, mixed $code, int $default): never + { + if ($data === '{-null-}') { + $data = new \stdClass(); + } + $status = $this->normalizeResponseCode($code, $default); + $result = [ + 'code' => $status, + 'info' => is_string($info) ? lang($info) : $info, + 'data' => $data, + ]; + if (JwtToken::isRejwt()) { + $result['token'] = JwtToken::token(); + } elseif ($token = SystemContext::instance()->buildToken()) { + $result['token'] = $token; + SystemContext::instance()->syncTokenCookie($token); + } + throw new HttpResponseException(json($result)->code(200)); + } + + /** + * 兼容旧版 1/0 返回码,并收敛到业务状态码. + */ + protected function normalizeResponseCode(mixed $code, int $default): int + { + $status = intval($code); + if ($status === 1) { + return 200; + } + if ($status === 0) { + return 500; + } + if ($status < 100 || $status > 599) { + return $default; + } + return $status; + } + + /** + * URL重定向. + * @param string $url 跳转链接 + * @param int $code 跳转代码 + */ + public function redirect(string $url, int $code = 302): void + { + throw new HttpResponseException(redirect($url, $code)); + } + + /** + * 返回视图内容. + * @param string $tpl 模板名称 + * @param array $vars 模板变量 + * @param null|string $node 授权节点 + */ + public function fetch(string $tpl = '', array $vars = [], ?string $node = null): void + { + if (function_exists('system_view_context')) { + $vars = array_merge(system_view_context(), $vars); + } + foreach (get_object_vars($this) as $name => $value) { + $vars[$name] = $value; + } + $vars['staticRoot'] = strval($vars['staticRoot'] ?? AppService::uri('static')); + if (!isset($vars['pageTitle']) || !is_scalar($vars['pageTitle']) || strval($vars['pageTitle']) === '') { + $vars['pageTitle'] = isset($vars['title']) && is_scalar($vars['title']) ? strval($vars['title']) : ''; + } + throw new HttpResponseException(view($tpl, $vars)); + } + + /** + * 获取当前控制器表现层模式. + */ + public function presentationMode(): string + { + return ResponseModeService::resolve($this->request, static::class); + } + + /** + * 当前控制器是否走 API 模式. + */ + public function usesApiPresentation(): bool + { + return ResponseModeService::prefersApi($this->request, static::class); + } + + /** + * 兼容旧版控制器在表单页显式启用 token 的调用。 + * 新 Builder 已内建请求防重逻辑,这里保留空实现避免老入口报错。 + */ + protected function _applyFormToken(): void + { + } + + /** + * 响应页面 Builder. + * @param array $context + * @param array $payload + * @param null|callable(PageBuilder,array):void $view + */ + protected function respondWithPageBuilder(PageBuilder $builder, array $context = [], ?callable $view = null, array $payload = []): void + { + $mode = $this->presentationMode(); + if ($mode === ResponseModeService::MODE_API) { + $this->success('获取页面成功!', array_merge([ + 'driver' => 'builder', + 'scene' => 'page', + 'mode' => $mode, + 'token' => ['header' => ResponseModeService::apiHeader()], + 'builder' => [ + 'type' => 'page', + 'schema' => $builder->toArray(), + ], + 'context' => $context, + ], $payload)); + } + + if (is_callable($view)) { + $view($builder, $context); + return; + } + + $builder->fetch($context); + } + + /** + * 响应表单 Builder. + * @param array $context + * @param array $data + * @param array $payload + * @param null|callable(FormBuilder,array,array):void $view + */ + protected function respondWithFormBuilder(FormBuilder $builder, array $context = [], array $data = [], ?callable $view = null, array $payload = []): void + { + $mode = $this->presentationMode(); + if ($mode === ResponseModeService::MODE_API) { + $this->success('获取表单成功!', array_merge([ + 'driver' => 'builder', + 'scene' => 'form', + 'mode' => $mode, + 'token' => ['header' => ResponseModeService::apiHeader()], + 'builder' => [ + 'type' => 'form', + 'schema' => $builder->toArray(), + 'rules' => $builder->getValidateRules(), + ], + 'context' => $context, + 'data' => $data, + ], $payload)); + } + + if (is_callable($view)) { + $view($builder, $context, $data); + return; + } + + $builder->fetch(array_merge($context, ['vo' => $data])); + } + + /** + * 模板变量赋值 + * @param mixed $name 要显示的模板变量 + * @param mixed $value 变量的值 + * @return $this + */ + public function assign(mixed $name, mixed $value = ''): static + { + if (is_string($name)) { + $this->{$name} = $value; + } elseif (is_array($name)) { + foreach ($name as $k => $v) { + if (is_string($k)) { + $this->{$k} = $v; + } + } + } + return $this; + } + + /** + * 数据回调处理机制. + * @param string $name 回调方法名称 + * @param mixed $one 回调引用参数1 + * @param mixed $two 回调引用参数2 + * @param mixed $thr 回调引用参数3 + */ + public function callback(string $name, mixed &$one = [], mixed &$two = [], mixed &$thr = []): bool + { + if (is_callable($name)) { + return call_user_func($name, $this, $one, $two, $thr); + } + foreach (["_{$this->app->request->action()}{$name}", $name] as $method) { + if (method_exists($this, $method) && $this->{$method}($one, $two, $thr) === false) { + return false; + } + } + return true; + } + + /** + * 控制器初始化. + */ + protected function initialize() {} + + /** + * 快捷输入并验证( 支持 规则 # 别名 ). + * @param array $rules 验证规则( 验证信息数组 ) + * @param array|string $type 输入方式 ( post. 或 get. ) + * @param null|callable $callable 异常处理操作 + */ + protected function _vali(array $rules, array|string $type = '', ?callable $callable = null): array + { + return ValidateHelper::instance()->init($rules, $type, $callable); + } + + /** + * 创建异步任务并返回任务编号. + * @param string $title 任务名称 + * @param string $command 执行内容 + * @param int $later 延时执行时间 + * @param array $data 任务附加数据 + * @param int $loops 循环等待时间 + * @param ?int $legacyLoops 兼容旧调用的循环等待时间参数 + */ + protected function _queue(string $title, string $command, int $later = 0, array $data = [], int $loops = 0, ?int $legacyLoops = null): void + { + try { + $queue = QueueService::register($title, $command, $later, $data, $loops, $legacyLoops); + $this->success('创建任务成功!', $queue->getCode()); + } catch (Exception $exception) { + $code = $exception->getData(); + if (is_string($code) && stripos($code, 'Q') === 0) { + $this->success('任务已经存在,无需再次创建!', $code); + } else { + $this->error($exception->getMessage()); + } + } catch (HttpResponseException $exception) { + throw $exception; + } catch (\Exception $exception) { + trace_file($exception); + $this->error(lang('创建任务失败,%s', [$exception->getMessage()])); + } + } +} diff --git a/plugin/think-library/src/Exception.php b/plugin/think-library/src/Exception.php new file mode 100644 index 000000000..3bdb7b9ee --- /dev/null +++ b/plugin/think-library/src/Exception.php @@ -0,0 +1,63 @@ +code = $code; + $this->data = $data; + $this->message = $message; + } + + /** + * 获取异常停止数据. + * @return mixed + */ + public function getData() + { + return $this->data; + } + + /** + * 设置异常停止数据. + * @param mixed $data + */ + public function setData($data) + { + $this->data = $data; + } +} diff --git a/plugin/think-library/src/Helper.php b/plugin/think-library/src/Helper.php new file mode 100644 index 000000000..58b4153b1 --- /dev/null +++ b/plugin/think-library/src/Helper.php @@ -0,0 +1,82 @@ +app = $app; + $this->class = $class; + // 计算指定输出格式 + $output = strval($app->request->request('output', 'default')); + $method = $app->request->method() ?: ($app->runningInConsole() ? 'cli' : 'nil'); + $this->method = strtolower($method); + $this->output = "{$this->method}." . strtolower($output); + } + + /** + * 实例化 Helper 对象(支持依赖注入). + * + * @param mixed ...$args 构造函数参数 + */ + public static function instance(...$args): static + { + return Container::getInstance()->invokeClass(static::class, $args); + } +} diff --git a/plugin/think-library/src/Library.php b/plugin/think-library/src/Library.php new file mode 100644 index 000000000..24178cb76 --- /dev/null +++ b/plugin/think-library/src/Library.php @@ -0,0 +1,158 @@ +app; + + // 动态应用运行参数 + RuntimeService::apply(); + + // 请求初始化处理 + $this->app->event->listen('HttpRun', function (Request $request) { + // Worker 常驻模式下,请求开始前先清理上次上下文。 + RequestContext::clear(); + + // 运行环境配置同步 + RuntimeService::sync(); + + // 配置默认输入过滤 + $request->filter([static function ($value) { + return is_string($value) ? xss_safe($value) : $value; + }]); + + // 判断访问模式兼容处理 + if ($this->app->runningInConsole()) { + // 兼容 CLI 访问控制器 + if (empty($_SERVER['REQUEST_URI']) && isset($_SERVER['argv'][1])) { + $request->setPathinfo($_SERVER['argv'][1]); + } + } else { + // 兼容 HTTP 调用 Console 后 URL 问题 + $request->setHost($request->host()); + } + + // 注册多应用中间键 + $this->app->middleware->add(MultAccess::class); + }); + + // 请求结束后处理 + $this->app->event->listen('HttpEnd', static function () { + RequestContext::clear(); + function_exists('sysvar') && sysvar('', ''); + }); + } + + /** + * 初始化服务 + */ + public function register(): void + { + // 动态加载全局配置 + [$dir, $ext] = [$this->app->getBasePath(), $this->app->getConfigExt()]; + FileTools::find($dir, 2, function (\SplFileInfo $info) use ($ext) { + $info->isFile() && $info->getBasename() === "sys{$ext}" && Library::load($info->getPathname()); + }); + if (is_file($file = "{$dir}common{$ext}")) { + Library::load($file); + } + if (is_file($file = "{$dir}provider{$ext}")) { + $this->app->bind(include $file); + } + if (is_file($file = "{$dir}event{$ext}")) { + $this->app->loadEvent(include $file); + } + if (is_file($file = "{$dir}middleware{$ext}")) { + $this->app->middleware->import(include $file, 'app'); + } + + // 终端 HTTP 访问时特殊处理 + if (!$this->app->runningInConsole()) { + // 动态注释 CORS 跨域处理 + $this->app->middleware->add(function (Request $request, \Closure $next): Response { + $header = ['X-Frame-Options' => $this->app->config->get('app.cors_frame') ?: 'sameorigin']; + // HTTP.CORS 跨域规则配置 + if ($this->app->config->get('app.cors_on', true) && ($origin = $request->header('origin', '-')) !== '-') { + if (is_string($hosts = $this->app->config->get('app.cors_host', []))) { + $hosts = str2arr($hosts); + } + if (empty($hosts) || in_array(parse_url(strtolower($origin), PHP_URL_HOST), $hosts)) { + $headers = str2arr(strval($this->app->config->get('app.cors_headers', 'X-Device-Code,X-Device-Type'))); + $headers = array_values(array_filter(array_unique(array_map('trim', $headers)))); + $header['Access-Control-Allow-Origin'] = $origin; + $header['Access-Control-Allow-Methods'] = $this->app->config->get('app.cors_methods', 'GET,PUT,POST,PATCH,DELETE'); + $allow = array_merge(['Authorization', 'Content-Type', 'If-Match', 'If-Modified-Since', 'If-None-Match', 'If-Unmodified-Since', 'X-Requested-With'], $headers); + $header['Access-Control-Allow-Headers'] = join(',', array_unique($allow)); + if ($this->app->config->get('app.cors_credentials', false)) { + $header['Access-Control-Allow-Credentials'] = 'true'; + } + if (!empty($headers)) { + $header['Access-Control-Expose-Headers'] = join(',', $headers); + } + } + } + // 跨域预请求状态处理 + if ($request->isOptions()) { + throw new HttpResponseException(response()->code(204)->header($header)); + } + return $next($request)->header($header); + }); + } + } + + /** + * 动态加载文件. + * @return mixed + */ + public static function load(string $file) + { + try { + return include $file; + } catch (\Error|\Throwable $error) { + trace_file($error); + throw new HttpException(500, $error->getMessage()); + } + } +} diff --git a/plugin/think-library/src/Model.php b/plugin/think-library/src/Model.php new file mode 100644 index 000000000..cce5f0677 --- /dev/null +++ b/plugin/think-library/src/Model.php @@ -0,0 +1,138 @@ + '修改%s[%s]状态', + 'onAdminUpdate' => '更新%s[%s]记录', + 'onAdminInsert' => '增加%s[%s]成功', + 'onAdminDelete' => '删除%s[%s]成功', + ]; + if (isset($oplogs[$method])) { + if ($this->oplogType && $this->oplogName) { + $changeIds = $args[0] ?? ''; + if (is_callable(static::$oplogCall)) { + $changeIds = call_user_func(static::$oplogCall, $method, $changeIds, $this); + } + sysoplog($this->oplogType, lang($oplogs[$method], [lang($this->oplogName), $changeIds])); + } + return $this; + } + return parent::__call($method, $args); + } + + /** + * 创建查询实例。 + */ + public static function mq(array $data = []): BaseQuery + { + return QueryFactory::build(static::mk($data)->newQuery()); + } + + /** + * 创建模型实例。 + */ + public static function mk(array $data = []): static + { + return new static($data); + } + + /** + * 追加模型数据并标记为待持久化变更。 + */ + public function appendData(array $data, bool $overwrite = false): static + { + foreach ($data as $name => $value) { + if ($overwrite || !$this->hasData($name)) { + $this->setAttr($name, $value); + } + } + + return $this; + } +} diff --git a/plugin/think-library/src/Plugin.php b/plugin/think-library/src/Plugin.php new file mode 100644 index 000000000..0ceeb5337 --- /dev/null +++ b/plugin/think-library/src/Plugin.php @@ -0,0 +1,423 @@ +composer = $this->resolveComposerManifest($ref); + $this->hydrateComposerManifest($this->composer); + + // 应用服务注册类 + if (empty($this->appService)) { + $this->appService = static::class; + } + + // 应用命名空间名 + if (empty($this->appSpace)) { + $this->appSpace = $ref->getNamespaceName(); + } + + // 应用插件路径计算 + if (empty($this->appPath) || !is_dir($this->appPath)) { + $this->appPath = dirname($ref->getFileName()); + } + + // 应用插件包名计算 + if ($this->appAlias !== '' && $this->appCode === $this->appAlias) { + $this->appAlias = ''; + } + + if (is_dir($this->appPath)) { + $prefixes = $this->normalizePrefixes(); + // 解析插件路径:phar 内 realpath 常为 false,需回退为原路径并保证末尾分隔符 + $resolved = realpath($this->appPath); + $path = ($resolved !== false ? $resolved : rtrim(str_replace('\\', '/', $this->appPath), '/')) . DIRECTORY_SEPARATOR; + // 写入插件参数信息 + self::$addons[$this->appCode] = [ + 'name' => $this->appName, + 'type' => 'plugin', + 'path' => $path, + 'alias' => $this->appAlias, + 'prefix' => $prefixes[0] ?? '', + 'prefixes' => $prefixes, + 'space' => $this->appSpace ?: NodeService::space($this->appCode), + 'package' => $this->package, + 'service' => $this->appService, + 'document' => $this->appDocument, + 'description' => $this->appDescription, + 'platforms' => $this->normalizeArray($this->appPlatforms), + 'license' => $this->normalizeArray($this->appLicense), + 'version' => $this->appVersion, + 'homepage' => $this->appHomepage, + 'show' => $this->appMenuShow, + ]; + AppService::clear(); + } + } + + /** + * 获取插件编号。 + */ + public static function getAppCode(): string + { + return static::plugin()->appCode; + } + + /** + * 获取插件名称。 + */ + public static function getAppName(): string + { + return static::plugin()->appName; + } + + /** + * 获取插件路径。 + */ + public static function getAppPath(): string + { + return static::plugin()->appPath; + } + + /** + * 获取插件命名空间。 + */ + public static function getAppSpace(): string + { + return static::plugin()->appSpace; + } + + /** + * 获取插件安装包名。 + */ + public static function getAppPackage(): string + { + return static::plugin()->package; + } + + /** + * 获取插件主访问前缀。 + */ + public static function getAppPrefix(): string + { + return static::plugin()->appPrefix; + } + + /** + * 获取插件全部访问前缀。 + * @return string[] + */ + public static function getAppPrefixes(): array + { + return static::plugin()->appPrefixes; + } + + /** + * 获取插件菜单根节点配置。 + */ + public static function getMenuRoot(): array + { + return static::plugin()->appMenuRoot; + } + + /** + * 获取插件菜单存在检测条件。 + */ + public static function getMenuExists(): array + { + return static::plugin()->appMenuExists; + } + + /** + * 获取插件菜单显示配置。 + */ + public static function getMenuShow(): bool + { + return static::plugin()->appMenuShow; + } + + /** + * 获取插件菜单项配置。 + */ + public static function getMenus(): array + { + return static::plugin()->appMenus; + } + + /** + * 获取插件及安装信息. + * @param ?string $code 指定插件编号 + * @param bool $append 关联安装数据 + */ + public static function get(?string $code = null, bool $append = false): ?array + { + // 读取插件原始信息 + $data = empty($code) ? self::$addons : (self::$addons[$code] ?? null); + if (empty($data) || empty($append)) { + return $data; + } + // 关联插件安装信息 + $versions = AppService::getPluginLibrarys(); + return empty($code) ? array_map(static function ($item) use ($versions) { + $item['install'] = $versions[$item['package']] ?? []; + if (empty($item['name'])) { + $item['name'] = $item['install']['name'] ?? ''; + } + return $item; + }, $data) : $data + ['install' => $versions[$data['package']] ?? []]; + } + + /** + * 注册应用启动. + */ + public function boot(): void {} + + /** + * 获取当前插件服务实例。 + */ + protected static function plugin(): static + { + return app(static::class); + } + + /** + * 解析 Composer 配置. + */ + private function resolveComposerManifest(\ReflectionClass $ref): array + { + if (!($path = $ref->getFileName())) { + return []; + } + + for ($level = 1; $level <= 3; ++$level) { + $file = dirname($path, $level) . DIRECTORY_SEPARATOR . 'composer.json'; + if (is_file($file)) { + return json_decode(file_get_contents($file), true) ?: []; + } + } + + return []; + } + + /** + * 同步 Composer 元数据. + */ + private function hydrateComposerManifest(array $manifest): void + { + $app = (array)($manifest['extra']['xadmin']['app'] ?? []); + $menu = (array)($manifest['extra']['xadmin']['menu'] ?? []); + + $this->package = strval($manifest['name'] ?? ''); + $this->appCode = strval($app['code'] ?? ''); + $this->appName = strval($app['name'] ?? ''); + $this->appAlias = strval($app['alias'] ?? ''); + $this->appPrefix = strval($app['prefix'] ?? ''); + $this->appPrefixes = array_key_exists('prefixes', $app) ? (array)$app['prefixes'] : []; + $this->appSpace = array_key_exists('space', $app) ? strval($app['space']) : ''; + $this->appType = 'plugin'; + $this->appDocument = strval($app['document'] ?? ''); + $this->appDescription = array_key_exists('description', $app) ? strval($app['description']) : strval($manifest['description'] ?? ''); + $this->appPlatforms = array_key_exists('platforms', $app) ? (array)$app['platforms'] : []; + $this->appLicense = array_key_exists('license', $app) ? (array)$app['license'] : (array)($manifest['license'] ?? []); + $this->appVersion = strval($manifest['version'] ?? ''); + $this->appHomepage = strval($manifest['homepage'] ?? ''); + $this->appMenuRoot = array_key_exists('root', $menu) ? (array)$menu['root'] : []; + $this->appMenuExists = array_key_exists('exists', $menu) ? (array)$menu['exists'] : []; + $this->appMenuShow = array_key_exists('show', $menu) ? boolval($menu['show']) : true; + $this->appMenus = array_key_exists('items', $menu) ? (array)$menu['items'] : []; + } + + /** + * 获取标准化前缀列表。 + * @return string[] + */ + private function normalizePrefixes(): array + { + $items = []; + foreach ([$this->appPrefix, $this->appPrefixes, $this->appAlias, $this->appCode] as $value) { + foreach ((array)$value as $prefix) { + $prefix = trim((string)$prefix, " \t\n\r\0\x0B\\/"); + if ($prefix === '') { + continue; + } + if (strpos($prefix, '/')) { + $prefix = strstr($prefix, '/', true) ?: $prefix; + } + if (strpos($prefix, '.')) { + $prefix = strstr($prefix, '.', true) ?: $prefix; + } + if ($prefix !== '' && !in_array($prefix, $items, true)) { + $items[] = $prefix; + } + } + } + + $this->appPrefix = $items[0] ?? ''; + $this->appPrefixes = $items; + return $items; + } + + /** + * 标准化字符串数组. + * @return string[] + */ + private function normalizeArray(array $items): array + { + $result = []; + foreach ($items as $item) { + $value = trim(strval($item)); + if ($value !== '' && !in_array($value, $result, true)) { + $result[] = $value; + } + } + + return $result; + } +} diff --git a/plugin/think-library/src/Service.php b/plugin/think-library/src/Service.php new file mode 100644 index 000000000..0b343a54c --- /dev/null +++ b/plugin/think-library/src/Service.php @@ -0,0 +1,60 @@ +app = $app; + $this->initialize(); + } + + /** + * 静态实例对象 + * @param array $var 实例参数 + * @param bool $new 创建新实例 + */ + public static function instance(array $var = [], bool $new = false): static + { + return Container::getInstance()->make(static::class, $var, $new); + } + + /** + * 初始化服务 + */ + protected function initialize() {} +} diff --git a/plugin/think-library/src/Storage.php b/plugin/think-library/src/Storage.php new file mode 100644 index 000000000..ae887292c --- /dev/null +++ b/plugin/think-library/src/Storage.php @@ -0,0 +1,235 @@ +{$method}()"); + } + + /** + * 实例化存储操作对象 + * @param ?string $name 驱动名称 + * @param ?string $class 驱动类名 + * @throws Exception + */ + public static function instance(?string $name = null, ?string $class = null): StorageInterface + { + try { + if (is_null($class)) { + $class = static::manager()->driverClass($name); + } + if (class_exists($class)) { + /* @var StorageInterface */ + return Container::getInstance()->make($class); + } + throw new Exception("Storage driver [{$class}] does not exist."); + } catch (Exception $exception) { + throw $exception; + } catch (\Exception $exception) { + throw new Exception($exception->getMessage()); + } + } + + /** + * 下载文件到本地. + * @param string $url 文件URL地址 + * @param bool $force 是否强制下载 + * @param int $expire 文件保留时间 + */ + public static function down(string $url, bool $force = false, int $expire = 0): array + { + try { + /** @var StorageInterface $local */ + $local = static::instance('local'); + $filename = static::name($url, '', 'down/'); + if (empty($force) && $local->has($filename)) { + if ($expire < 1 || filemtime($local->path($filename)) + $expire > time()) { + return $local->info($filename); + } + } + return $local->set($filename, static::curlGet($url)); + } catch (\Exception $exception) { + return ['url' => $url, 'hash' => md5($url), 'key' => $url, 'file' => $url]; + } + } + + /** + * 获取文件相对名称. + * @param string $url 文件访问链接 + * @param string $ext 文件后缀名称 + * @param string $pre 文件存储前缀 + * @param string $fun 名称规则方法 + */ + public static function name(string $url, string $ext = '', string $pre = '', string $fun = 'md5'): string + { + [$hah, $ext] = [$fun($url), trim($ext ?: pathinfo($url, 4), '.\/')]; + $attr = [trim($pre, '.\/'), substr($hah, 0, 2), substr($hah, 2, 30)]; + return trim(join('/', $attr), '/') . '.' . strtolower($ext ?: 'tmp'); + } + + /** + * 使用CURL读取网络资源. + * @param string $url 资源地址 + */ + public static function curlGet(string $url): string + { + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_HEADER, 0); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); + $body = curl_exec($ch) ?: ''; + curl_close($ch); + return $body; + } + + /** + * 获取后缀类型. + * @param array|string $exts 文件后缀 + * @param array $mime 文件信息 + */ + public static function mime(array|string $exts, array $mime = []): string + { + $mimes = static::mimes(); + foreach (str2arr($exts) as $ext) { + $mime[] = $mimes[strtolower($ext)] ?? 'application/octet-stream'; + } + return join(',', array_unique($mime)); + } + + /** + * 获取所有类型. + */ + public static function mimes(): array + { + return static::manager()->mimes(); + } + + /** + * 获取存储类型. + */ + public static function types(): array + { + return static::manager()->types(); + } + + /** + * 获取驱动区域列表. + */ + public static function regions(string $name): array + { + return static::manager()->regions($name); + } + + /** + * 获取驱动配置模板名. + */ + public static function template(string $name): string + { + return static::manager()->template($name); + } + + /** + * 获取前端上传授权参数. + */ + public static function authorize(string $name, string $key, bool $safe = false, ?string $attname = null, string $hash = ''): array + { + return static::manager()->authorize($name, $key, $safe, $attname, $hash); + } + + /** + * 图片数据存储. + * @param string $base64 图片内容 + * @param string $prefix 保存前缀 + * @param bool $safemode 安全模式 + * @return array [ url => URL ] + * @throws Exception + */ + public static function saveImage(string $base64, string $prefix = 'image', bool $safemode = false): array + { + if (preg_match('|^data:image/(.*?);base64,|i', $base64)) { + [$ext, $img] = explode('|||', preg_replace('|^data:image/(.*?);base64,|i', '$1|||', $base64)); + $name = static::name($img, $ext, $prefix); + if (empty($ext) || !in_array(strtolower($ext), ['gif', 'png', 'jpg', 'jpeg'])) { + throw new Exception('内容格式异常!'); + } + if ($safemode) { + return static::instance('local')->set($name, base64_decode($img), true); + } + return static::instance()->set($name, base64_decode($img)); + } + return ['url' => $base64]; + } + + /** + * 获取存储组件管理器. + * @throws Exception + */ + protected static function manager(): StorageManagerInterface + { + $container = Container::getInstance(); + if ($container->bound(StorageManagerInterface::class)) { + /** @var StorageManagerInterface */ + return $container->make(StorageManagerInterface::class); + } + $class = ''; + if ($container->bound('app')) { + $app = $container->make('app'); + $class = strval($app->config->get('app.storage_manager_class', '')); + } + if ($class !== '' && class_exists($class)) { + /** @var StorageManagerInterface */ + return $container->make($class); + } + throw new Exception('System storage manager is not available.'); + } +} diff --git a/plugin/think-library/src/builder/BuilderLang.php b/plugin/think-library/src/builder/BuilderLang.php new file mode 100644 index 000000000..2216fbb49 --- /dev/null +++ b/plugin/think-library/src/builder/BuilderLang.php @@ -0,0 +1,129 @@ + + */ + private const TRANSLATABLE_ATTRS = [ + 'placeholder', + 'title', + 'data-title', + 'data-confirm', + 'data-tips-text', + 'required-error', + 'pattern-error', + 'lay-text', + ]; + + public static function text(string $text): string + { + if ($text === '' || self::looksLikeHtml($text) || !function_exists('lang')) { + return $text; + } + + try { + return strval(lang($text)); + } catch (\Throwable) { + return $text; + } + } + + /** + * @param array $vars + */ + public static function format(string $text, array $vars = []): string + { + if ($text === '') { + return ''; + } + if (!function_exists('lang')) { + return self::fallbackFormat($text, $vars); + } + + try { + return strval(lang($text, $vars)); + } catch (\Throwable) { + return self::fallbackFormat($text, $vars); + } + } + + public static function pipeText(string $text, string $separator = '|'): string + { + if ($text === '' || !str_contains($text, $separator)) { + return self::text($text); + } + + return join($separator, array_map( + static fn(string $item): string => self::text(trim($item)), + explode($separator, $text) + )); + } + + /** + * @param array $attrs + * @return array + */ + public static function attrs(array $attrs): array + { + foreach ($attrs as $name => $value) { + if (!is_string($name) || !is_string($value) || !in_array($name, self::TRANSLATABLE_ATTRS, true)) { + continue; + } + $attrs[$name] = $name === 'lay-text' ? self::pipeText($value) : self::text($value); + } + + return $attrs; + } + + /** + * @param array $options + * @return array + */ + public static function options(array $options): array + { + $translated = []; + foreach ($options as $value => $label) { + if (is_string($label)) { + $translated[$value] = self::text($label); + continue; + } + if (is_array($label)) { + $translated[$value] = self::options($label); + continue; + } + $translated[$value] = $label; + } + + return $translated; + } + + private static function looksLikeHtml(string $text): bool + { + return str_contains($text, '<') && str_contains($text, '>'); + } + + /** + * @param array $vars + */ + private static function fallbackFormat(string $text, array $vars): string + { + if (count($vars) < 1) { + return $text; + } + + try { + return vsprintf($text, $vars); + } catch (\Throwable) { + return $text; + } + } +} diff --git a/plugin/think-library/src/builder/base/BuilderAttributeBag.php b/plugin/think-library/src/builder/base/BuilderAttributeBag.php new file mode 100644 index 000000000..e1cd03e6a --- /dev/null +++ b/plugin/think-library/src/builder/base/BuilderAttributeBag.php @@ -0,0 +1,174 @@ +): array + */ + private $syncHandler = null; + + /** + * @param array $attrs + */ + public function __construct( + private mixed $owner = null, + private array $attrs = [], + private bool $detachedClass = false, + private string $className = '' + ) { + } + + /** + * @param callable(array): array $syncHandler + */ + public function attach(callable $syncHandler): self + { + $this->syncHandler = $syncHandler; + return $this; + } + + public function attr(string $name, mixed $value = null): self + { + $name = trim($name); + if ($name === '') { + return $this; + } + if ($name === 'class') { + return $this->assignClass($value); + } + $this->attrs[$name] = $value; + return $this->sync(); + } + + /** + * @param array $attrs + */ + public function attrs(array $attrs): self + { + foreach ($attrs as $name => $value) { + if (is_string($name)) { + $this->attr($name, $value); + } + } + return $this; + } + + public function class(string|array $class): self + { + if ($this->detachedClass) { + $this->className = BuilderAttributes::mergeClassNames($this->className, $class); + } else { + $this->attrs = BuilderAttributes::make($this->attrs)->class($class)->all(); + } + return $this->sync(); + } + + public function removeClass(string|array $class): self + { + if ($this->detachedClass) { + $attrs = BuilderAttributes::make(['class' => $this->className])->removeClass($class)->all(); + $this->className = trim(strval($attrs['class'] ?? '')); + } else { + $this->attrs = BuilderAttributes::make($this->attrs)->removeClass($class)->all(); + } + return $this->sync(); + } + + public function toggleClass(string|array $class, ?bool $force = null): self + { + if ($this->detachedClass) { + $attrs = BuilderAttributes::make(['class' => $this->className])->toggleClass($class, $force)->all(); + $this->className = trim(strval($attrs['class'] ?? '')); + } else { + $this->attrs = BuilderAttributes::make($this->attrs)->toggleClass($class, $force)->all(); + } + return $this->sync(); + } + + public function data(string $name, mixed $value = null): self + { + $name = trim($name); + if ($name !== '') { + $this->attr('data-' . ltrim($name, '-'), $value); + } + return $this; + } + + public function id(string $id): self + { + return $this->attr('id', $id); + } + + public function remove(string $name): self + { + $name = trim($name); + if ($name === '') { + return $this; + } + if ($name === 'class') { + if ($this->detachedClass) { + $this->className = ''; + } else { + unset($this->attrs['class']); + } + return $this->sync(); + } + if (array_key_exists($name, $this->attrs)) { + unset($this->attrs[$name]); + $this->sync(); + } + return $this; + } + + /** + * @return array + */ + public function export(): array + { + $state = ['attrs' => $this->attrs]; + if ($this->detachedClass) { + $state['class'] = $this->className; + } + return $state; + } + + public function end(): mixed + { + return $this->owner; + } + + private function assignClass(mixed $value): self + { + $class = is_array($value) ? BuilderAttributes::mergeClassNames('', $value) : trim(strval($value)); + if ($this->detachedClass) { + $this->className = $class; + } elseif ($class === '') { + unset($this->attrs['class']); + } else { + $this->attrs['class'] = $class; + } + return $this->sync(); + } + + private function sync(): self + { + if (is_callable($this->syncHandler)) { + $state = ($this->syncHandler)($this->export()); + $this->attrs = is_array($state['attrs'] ?? null) ? $state['attrs'] : $this->attrs; + if ($this->detachedClass) { + $this->className = trim(strval($state['class'] ?? $this->className)); + } + } + return $this; + } +} diff --git a/plugin/think-library/src/builder/base/BuilderModule.php b/plugin/think-library/src/builder/base/BuilderModule.php new file mode 100644 index 000000000..5fc0d174f --- /dev/null +++ b/plugin/think-library/src/builder/base/BuilderModule.php @@ -0,0 +1,118 @@ +): array + */ + private $syncHandler = null; + + /** + * @param array $config + */ + public function __construct( + private string $name, + private array $config = [], + private mixed $owner = null + ) { + $this->name = trim($this->name); + } + + /** + * @param array $module + * @param callable(int, array): array $syncHandler + */ + public function attach(int $index, array $module, callable $syncHandler): self + { + $this->index = $index; + $this->syncHandler = $syncHandler; + $this->name = trim(strval($module['name'] ?? $this->name)); + $this->config = is_array($module['config'] ?? null) ? $module['config'] : $this->config; + return $this; + } + + public function name(string $name): self + { + $name = trim($name); + if ($name !== '') { + $this->name = $name; + $this->sync(); + } + return $this; + } + + /** + * @param array $config + */ + public function config(array $config): self + { + $this->config = $config; + return $this->sync(); + } + + public function option(string $name, mixed $value): self + { + $name = trim($name); + if ($name !== '') { + $this->config[$name] = $value; + $this->sync(); + } + return $this; + } + + /** + * @param array $config + */ + public function options(array $config): self + { + foreach ($config as $name => $value) { + if (is_string($name) && trim($name) !== '') { + $this->config[trim($name)] = $value; + } + } + return $this->sync(); + } + + public function remove(string $name): self + { + $name = trim($name); + if ($name !== '' && array_key_exists($name, $this->config)) { + unset($this->config[$name]); + $this->sync(); + } + return $this; + } + + /** + * @return array + */ + public function export(): array + { + return ['name' => $this->name, 'config' => $this->config]; + } + + public function end(): mixed + { + return $this->owner; + } + + private function sync(): self + { + if ($this->index !== null && is_callable($this->syncHandler)) { + $module = ($this->syncHandler)($this->index, $this->export()); + $this->name = trim(strval($module['name'] ?? $this->name)); + $this->config = is_array($module['config'] ?? null) ? $module['config'] : $this->config; + } + return $this; + } +} diff --git a/plugin/think-library/src/builder/base/BuilderNode.php b/plugin/think-library/src/builder/base/BuilderNode.php new file mode 100644 index 000000000..f9dc2b7e6 --- /dev/null +++ b/plugin/think-library/src/builder/base/BuilderNode.php @@ -0,0 +1,429 @@ + + */ + protected array $attrs = []; + + /** + * 模块配置. + * @var array> + */ + protected array $modules = []; + + /** + * 子节点. + * @var array + */ + protected array $children = []; + + /** + * 原始 HTML. + */ + protected string $html = ''; + + /** + * 父级节点. + */ + protected ?self $parentNode = null; + + public function __construct( + protected object $builder, + protected string $type = 'element', + protected string $tag = 'div' + ) { + } + + public function attr(string $name, mixed $value = null): static + { + $name = trim($name); + if ($name !== '') { + $this->attrs[$name] = $value; + $this->afterMutate(); + } + return $this; + } + + public function removeAttr(string $name): static + { + $name = trim($name); + if ($name !== '' && array_key_exists($name, $this->attrs)) { + unset($this->attrs[$name]); + $this->afterMutate(); + } + return $this; + } + + public function attrs(array $attrs): static + { + foreach ($attrs as $name => $value) { + if ($name === 'class') { + $this->class($value); + } else { + $this->attr(strval($name), $value); + } + } + return $this; + } + + public function class(string|array $class): static + { + $this->attrs = BuilderAttributes::make($this->attrs)->class($class)->all(); + $this->afterMutate(); + return $this; + } + + public function removeClass(string|array $class): static + { + $this->attrs = BuilderAttributes::make($this->attrs)->removeClass($class)->all(); + $this->afterMutate(); + return $this; + } + + public function toggleClass(string|array $class, ?bool $force = null): static + { + $this->attrs = BuilderAttributes::make($this->attrs)->toggleClass($class, $force)->all(); + $this->afterMutate(); + return $this; + } + + public function data(string $name, mixed $value = null): static + { + $name = trim($name); + if ($name !== '') { + $this->attr('data-' . ltrim($name, '-'), $value); + } + return $this; + } + + public function removeData(string $name): static + { + $name = trim($name); + if ($name !== '') { + $this->removeAttr('data-' . ltrim($name, '-')); + } + return $this; + } + + public function id(string $id): static + { + return $this->attr('id', $id); + } + + public function attrsItem(): BuilderAttributeBag + { + return $this->attachAttributes($this->createAttributes()); + } + + public function module(string $name, array $config = []): static + { + $this->attachModule($this->createModule($name, $config)); + return $this; + } + + public function moduleItem(string $name, array $config = []): BuilderModule + { + return $this->attachModule($this->createModule($name, $config)); + } + + public function clear(): static + { + foreach ($this->children as $child) { + if ($child instanceof self) { + $child->parentNode = null; + $child->onDetached(); + } + $this->onChildDetached($child); + } + $this->children = []; + $this->afterMutate(); + return $this; + } + + public function parentNode(): ?self + { + return $this->parentNode; + } + + /** + * @return array + */ + public function children(): array + { + return $this->children; + } + + public function firstChild(): ?object + { + return $this->children[0] ?? null; + } + + public function lastChild(): ?object + { + return $this->children[count($this->children) - 1] ?? null; + } + + public function appendNode(object $node): object + { + return $this->appendChild($node); + } + + public function prependNode(object $node): object + { + return $this->prependChild($node); + } + + public function beforeNode(object $node): object + { + return $this->insertSibling($node, false); + } + + public function afterNode(object $node): object + { + return $this->insertSibling($node, true); + } + + public function remove(): static + { + return $this->detachFromParent(); + } + + /** + * 追加子节点. + */ + protected function appendChild(object $node): object + { + if (!$this->canAttachNode($node)) { + return $node; + } + if ($node instanceof self) { + $node->detachFromParent(true); + $node->parentNode = $this; + } + $this->children[] = $node; + $this->afterMutate(); + return $node; + } + + /** + * 前置插入子节点. + */ + protected function prependChild(object $node): object + { + if (!$this->canAttachNode($node)) { + return $node; + } + if ($node instanceof self) { + $node->detachFromParent(true); + $node->parentNode = $this; + } + array_unshift($this->children, $node); + $this->afterMutate(); + return $node; + } + + protected function createAttributes(): BuilderAttributeBag + { + return new BuilderAttributeBag($this, $this->attrs); + } + + protected function attachAttributes(BuilderAttributeBag $attributes): BuilderAttributeBag + { + return $attributes->attach(fn(array $state): array => $this->replaceAttributes($state)); + } + + /** + * @param array $state + * @return array + */ + protected function replaceAttributes(array $state): array + { + $this->attrs = is_array($state['attrs'] ?? null) ? BuilderAttributes::make($state['attrs'])->all() : []; + $this->afterMutate(); + return ['attrs' => $this->attrs]; + } + + protected function createModule(string $name, array $config = []): BuilderModule + { + return new BuilderModule($name, $config, $this); + } + + protected function attachModule(BuilderModule $module): BuilderModule + { + $normalized = $this->normalizeModule($module->export()); + if ($normalized['name'] === '') { + return $module; + } + $index = count($this->modules); + $this->modules[$index] = $normalized; + $this->afterMutate(); + return $module->attach($index, $normalized, fn(int $index, array $module): array => $this->replaceModule($index, $module)); + } + + /** + * @param array $module + * @return array + */ + protected function replaceModule(int $index, array $module): array + { + $normalized = $this->normalizeModule($module); + if ($normalized['name'] !== '') { + $this->modules[$index] = $normalized; + $this->afterMutate(); + } + return $this->modules[$index] ?? $normalized; + } + + /** + * 导出 HTML 节点数组. + * @return array + */ + protected function exportHtmlNode(): array + { + return ['type' => 'html', 'html' => $this->html]; + } + + /** + * 导出普通节点数组. + * @return array + */ + protected function exportElementNode(): array + { + return [ + 'type' => $this->type, + 'tag' => $this->tag, + 'attrs' => $this->buildAttrs(), + 'modules' => $this->modules, + 'children' => $this->exportChildren(), + ]; + } + + /** + * 导出子节点数组. + * @return array> + */ + public function exportChildren(): array + { + return array_map(static fn(object $node) => $node->export(), $this->children); + } + + /** + * 获取节点属性. + * @return array + */ + protected function buildAttrs(): array + { + return BuilderAttributes::make($this->attrs)->modules($this->modules)->all(); + } + + /** + * @param array $module + * @return array + */ + protected function normalizeModule(array $module): array + { + return [ + 'name' => trim(strval($module['name'] ?? '')), + 'config' => is_array($module['config'] ?? null) ? $module['config'] : [], + ]; + } + + protected function removeChild(object $node, bool $moving = false): bool + { + foreach ($this->children as $index => $child) { + if ($child === $node) { + if ($child instanceof self) { + $child->parentNode = null; + if (!$moving) { + $child->onDetached(); + } + } + array_splice($this->children, $index, 1); + $this->onChildDetached($child); + $this->afterMutate(); + return true; + } + } + return false; + } + + protected function insertSibling(object $node, bool $after = true): object + { + if (!$this->parentNode instanceof self) { + return $node; + } + return $this->parentNode->insertChildAround($this, $node, $after); + } + + protected function insertChildAround(object $pivot, object $node, bool $after = true): object + { + if (!$this->canAttachNode($node)) { + return $node; + } + foreach ($this->children as $index => $child) { + if ($child === $pivot) { + if ($node instanceof self) { + $node->detachFromParent(true); + $node->parentNode = $this; + } + array_splice($this->children, $after ? $index + 1 : $index, 0, [$node]); + $this->afterMutate(); + return $node; + } + } + return $after ? $this->appendChild($node) : $this->prependChild($node); + } + + /** + * 节点变更后同步. + */ + protected function afterMutate(): void + { + } + + protected function onDetached(): void + { + } + + protected function onChildDetached(object $child): void + { + } + + protected function detachFromParent(bool $moving = false): static + { + if ($this->parentNode instanceof self) { + $this->parentNode->removeChild($this, $moving); + } + return $this; + } + + private function canAttachNode(object $node): bool + { + return !$node instanceof self || ($node !== $this && !$this->isDescendantOf($node)); + } + + private function isDescendantOf(self $node): bool + { + $parent = $this->parentNode; + while ($parent instanceof self) { + if ($parent === $node) { + return true; + } + $parent = $parent->parentNode; + } + return false; + } +} diff --git a/plugin/think-library/src/builder/base/BuilderOptionSource.php b/plugin/think-library/src/builder/base/BuilderOptionSource.php new file mode 100644 index 000000000..dd8847bd1 --- /dev/null +++ b/plugin/think-library/src/builder/base/BuilderOptionSource.php @@ -0,0 +1,106 @@ +): array + */ + private $syncHandler = null; + + /** + * @param array $options + */ + public function __construct( + private string $sourceKey = 'source', + private array $options = [], + private string $source = '', + private mixed $owner = null + ) { + $this->sourceKey = trim($this->sourceKey) ?: 'source'; + } + + /** + * @param callable(array): array $syncHandler + */ + public function attach(callable $syncHandler): static + { + $this->syncHandler = $syncHandler; + return $this; + } + + public function source(string $source): static + { + $this->source = trim($source); + return $this->sync(); + } + + public function variable(string $source): static + { + return $this->source($source); + } + + /** + * @param array $options + */ + public function options(array $options): static + { + $this->options = $options; + return $this->sync(); + } + + public function option(string|int $value, mixed $label): static + { + $this->options[(string)$value] = $label; + return $this->sync(); + } + + public function removeOption(string|int $value): static + { + $key = (string)$value; + if (array_key_exists($key, $this->options)) { + unset($this->options[$key]); + $this->sync(); + } + return $this; + } + + public function clearOptions(): static + { + $this->options = []; + return $this->sync(); + } + + /** + * @return array + */ + public function export(): array + { + return [ + 'options' => $this->options, + $this->sourceKey => $this->source, + ]; + } + + public function end(): mixed + { + return $this->owner; + } + + private function sync(): static + { + if (is_callable($this->syncHandler)) { + $state = ($this->syncHandler)($this->export()); + $this->options = is_array($state['options'] ?? null) ? $state['options'] : $this->options; + $this->source = trim(strval($state[$this->sourceKey] ?? $this->source)); + } + return $this; + } +} diff --git a/plugin/think-library/src/builder/base/render/BuilderActionRenderer.php b/plugin/think-library/src/builder/base/render/BuilderActionRenderer.php new file mode 100644 index 000000000..4e0e88a33 --- /dev/null +++ b/plugin/think-library/src/builder/base/render/BuilderActionRenderer.php @@ -0,0 +1,29 @@ +attributesRenderer = $attributesRenderer ?? new BuilderAttributesRenderer(); + } + + /** + * @param array $attrs + */ + public function render(string $label, array $attrs = [], string $tag = 'a'): string + { + $tag = trim($tag) ?: 'a'; + $attrsHtml = $this->attributesRenderer->render($attrs); + return sprintf('<%s%s>%s', $tag, $attrsHtml === '' ? '' : ' ' . $attrsHtml, $label, $tag); + } +} diff --git a/plugin/think-library/src/builder/base/render/BuilderAttributes.php b/plugin/think-library/src/builder/base/render/BuilderAttributes.php new file mode 100644 index 000000000..b2bbabb77 --- /dev/null +++ b/plugin/think-library/src/builder/base/render/BuilderAttributes.php @@ -0,0 +1,165 @@ + $attrs + */ + public function __construct(private array $attrs = []) + { + } + + /** + * @param array $attrs + */ + public static function make(array $attrs = []): self + { + return new self($attrs); + } + + /** + * @param array $attrs + */ + public function merge(array $attrs): self + { + foreach ($attrs as $name => $value) { + if ($name === 'class') { + $this->class(is_array($value) ? $value : strval($value)); + } else { + $this->attrs[$name] = $value; + } + } + return $this; + } + + public function class(string|array $class): self + { + $merged = self::mergeClassNames(strval($this->attrs['class'] ?? ''), $class); + if ($merged === '') { + unset($this->attrs['class']); + } else { + $this->attrs['class'] = $merged; + } + return $this; + } + + public function removeClass(string|array $class): self + { + $current = self::normalizeClasses(strval($this->attrs['class'] ?? '')); + $remove = self::normalizeClasses($class); + if ($current === [] || $remove === []) { + return $this; + } + $merged = array_values(array_diff($current, $remove)); + if ($merged === []) { + unset($this->attrs['class']); + } else { + $this->attrs['class'] = join(' ', $merged); + } + return $this; + } + + public function toggleClass(string|array $class, ?bool $force = null): self + { + $classes = self::normalizeClasses($class); + if ($classes === []) { + return $this; + } + + $current = self::normalizeClasses(strval($this->attrs['class'] ?? '')); + $active = count(array_intersect($current, $classes)) === count($classes); + $enabled = $force ?? !$active; + + return $enabled ? $this->class($classes) : $this->removeClass($classes); + } + + public function default(string $name, mixed $value): self + { + if (!array_key_exists($name, $this->attrs)) { + $this->attrs[$name] = $value; + } + return $this; + } + + /** + * @param array> $modules + */ + public function modules(array $modules): self + { + if (count($modules) > 0) { + $this->attrs['data-builder-modules'] = json_encode($modules, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + } + return $this; + } + + /** + * @return array + */ + public function all(): array + { + $attrs = $this->attrs; + if (isset($attrs['class']) && trim(strval($attrs['class'])) === '') { + unset($attrs['class']); + } + return $attrs; + } + + public function html(): string + { + $html = ''; + foreach ($this->all() as $key => $value) { + $name = self::escape((string)$key); + $html .= is_null($value) + ? sprintf(' %s', $name) + : sprintf(' %s="%s"', $name, self::escape((string)$value)); + } + return ltrim($html); + } + + public static function escape(string $value): string + { + return htmlspecialchars($value, ENT_QUOTES, 'UTF-8'); + } + + public static function mergeClassNames(string|array $origin, string|array $append): string + { + $classes = []; + foreach ([self::normalizeClasses($origin), self::normalizeClasses($append)] as $items) { + foreach ($items as $class) { + if ($class !== '' && !in_array($class, $classes, true)) { + $classes[] = $class; + } + } + } + return join(' ', $classes); + } + + /** + * @return array + */ + public static function normalizeClasses(string|array $classes): array + { + if (is_array($classes)) { + $items = array_map('strval', $classes); + } else { + $items = preg_split('/\s+/', trim($classes)) ?: []; + } + + $result = []; + foreach ($items as $class) { + $class = trim($class); + if ($class !== '') { + $result[] = $class; + } + } + return $result; + } +} diff --git a/plugin/think-library/src/builder/base/render/BuilderAttributesRenderContext.php b/plugin/think-library/src/builder/base/render/BuilderAttributesRenderContext.php new file mode 100644 index 000000000..40a41b12a --- /dev/null +++ b/plugin/think-library/src/builder/base/render/BuilderAttributesRenderContext.php @@ -0,0 +1,38 @@ +): string $attrsRenderer + */ + public function __construct( + private $attrsRenderer, + ) { + } + + /** + * @param array $attrs + */ + public function attrs(array $attrs): string + { + return ($this->attrsRenderer)($attrs); + } + + public function escape(string $value): string + { + return BuilderAttributes::escape($value); + } + + public function mergeClass(string|array $origin, string|array $append): string + { + return BuilderAttributes::mergeClassNames($origin, $append); + } +} diff --git a/plugin/think-library/src/builder/base/render/BuilderAttributesRenderer.php b/plugin/think-library/src/builder/base/render/BuilderAttributesRenderer.php new file mode 100644 index 000000000..870d5875e --- /dev/null +++ b/plugin/think-library/src/builder/base/render/BuilderAttributesRenderer.php @@ -0,0 +1,20 @@ + $attrs + */ + public function render(array $attrs): string + { + return BuilderAttributes::make($attrs)->html(); + } +} diff --git a/plugin/think-library/src/builder/base/render/BuilderCallbackNodeRenderer.php b/plugin/think-library/src/builder/base/render/BuilderCallbackNodeRenderer.php new file mode 100644 index 000000000..3c5227cf8 --- /dev/null +++ b/plugin/think-library/src/builder/base/render/BuilderCallbackNodeRenderer.php @@ -0,0 +1,17 @@ + $node + */ + protected function renderElement(array $node, BuilderNodeRenderContext $context): string + { + $tag = trim(strval($node['tag'] ?? 'div')) ?: 'div'; + $attrs = is_array($node['attrs'] ?? null) ? $node['attrs'] : []; + $children = is_array($node['children'] ?? null) ? $node['children'] : []; + return $this->wrapElement($tag, $attrs, $context->renderChildren($children), $context); + } + + /** + * @param array $attrs + */ + protected function wrapElement(string $tag, array $attrs, string $content, BuilderNodeRenderContext $context): string + { + $attrsHtml = count($attrs) > 0 ? ' ' . ltrim($context->attrs($attrs)) : ''; + return sprintf('<%s%s>%s', $tag, $attrsHtml, $content, $tag); + } +} diff --git a/plugin/think-library/src/builder/base/render/BuilderHtmlNodeRenderer.php b/plugin/think-library/src/builder/base/render/BuilderHtmlNodeRenderer.php new file mode 100644 index 000000000..41d48e8c2 --- /dev/null +++ b/plugin/think-library/src/builder/base/render/BuilderHtmlNodeRenderer.php @@ -0,0 +1,22 @@ + $node + */ + protected function renderHtml(array $node): string + { + return BuilderLang::text(strval($node['html'] ?? '')); + } +} diff --git a/plugin/think-library/src/builder/base/render/BuilderNodeRenderContext.php b/plugin/think-library/src/builder/base/render/BuilderNodeRenderContext.php new file mode 100644 index 000000000..c328363b3 --- /dev/null +++ b/plugin/think-library/src/builder/base/render/BuilderNodeRenderContext.php @@ -0,0 +1,32 @@ +>): string $contentRenderer + * @param callable(array): string $attrsRenderer + */ + public function __construct( + private $contentRenderer, + callable $attrsRenderer, + ) { + parent::__construct($attrsRenderer); + } + + /** + * @param array> $nodes + */ + public function renderChildren(array $nodes): string + { + return ($this->contentRenderer)($nodes); + } +} diff --git a/plugin/think-library/src/builder/base/render/BuilderNodeRendererFactory.php b/plugin/think-library/src/builder/base/render/BuilderNodeRendererFactory.php new file mode 100644 index 000000000..ea13d8e9b --- /dev/null +++ b/plugin/think-library/src/builder/base/render/BuilderNodeRendererFactory.php @@ -0,0 +1,40 @@ + $node + */ + public function create(array $node): object + { + $class = $this->resolveRendererClass($node); + return new $class(); + } + + /** + * @param array $node + */ + protected function resolveRendererClass(array $node): string + { + $type = strval($node['type'] ?? ''); + return $this->rendererMap()[$type] ?? $this->fallbackRendererClass(); + } + + /** + * @return array + */ + abstract protected function rendererMap(): array; + + /** + * @return class-string + */ + abstract protected function fallbackRendererClass(): string; +} diff --git a/plugin/think-library/src/builder/base/render/BuilderRenderPipeline.php b/plugin/think-library/src/builder/base/render/BuilderRenderPipeline.php new file mode 100644 index 000000000..e86fd2a24 --- /dev/null +++ b/plugin/think-library/src/builder/base/render/BuilderRenderPipeline.php @@ -0,0 +1,41 @@ +> $nodes + */ + protected function renderNodeContent(array $nodes, BuilderRenderState $state, string $separator = "\n"): string + { + $html = []; + $context = $state->nodeRenderContext(); + $factory = $state->nodeRendererFactory(); + foreach ($nodes as $node) { + if (!is_array($node)) { + continue; + } + $renderer = $factory->create($node); + if (!method_exists($renderer, 'render')) { + continue; + } + $item = $renderer->render($node, $context); + if ($item !== '') { + $html[] = $item; + } + } + return join($separator, $html); + } + + protected function renderSchemaScript(BuilderRenderState $state, string $className): string + { + return (new JsonScriptRenderer())->render($state->schema(), $className); + } +} diff --git a/plugin/think-library/src/builder/base/render/BuilderRenderState.php b/plugin/think-library/src/builder/base/render/BuilderRenderState.php new file mode 100644 index 000000000..083bc08d5 --- /dev/null +++ b/plugin/think-library/src/builder/base/render/BuilderRenderState.php @@ -0,0 +1,40 @@ + $schema + */ + public function __construct( + private array $schema, + private BuilderNodeRendererFactory $nodeRendererFactory, + private BuilderNodeRenderContext $nodeRenderContext, + ) { + } + + /** + * @return array + */ + public function schema(): array + { + return $this->schema; + } + + public function nodeRendererFactory(): BuilderNodeRendererFactory + { + return $this->nodeRendererFactory; + } + + public function nodeRenderContext(): BuilderNodeRenderContext + { + return $this->nodeRenderContext; + } +} diff --git a/plugin/think-library/src/builder/base/render/InlineScriptRenderer.php b/plugin/think-library/src/builder/base/render/InlineScriptRenderer.php new file mode 100644 index 000000000..03c2d8148 --- /dev/null +++ b/plugin/think-library/src/builder/base/render/InlineScriptRenderer.php @@ -0,0 +1,28 @@ + $scripts + */ + public function render(array $scripts): string + { + if (count($scripts) < 1) { + return ''; + } + + $html = ''; + foreach ($scripts as $script) { + $html .= "\n"; + } + return $html; + } +} diff --git a/plugin/think-library/src/builder/base/render/JsonScriptRenderer.php b/plugin/think-library/src/builder/base/render/JsonScriptRenderer.php new file mode 100644 index 000000000..22a9d201e --- /dev/null +++ b/plugin/think-library/src/builder/base/render/JsonScriptRenderer.php @@ -0,0 +1,21 @@ + $payload + */ + public function render(array $payload, string $className): string + { + $json = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT); + return $json ? sprintf('', BuilderAttributes::escape($className), $json) : ''; + } +} diff --git a/plugin/think-library/src/builder/form/FormActionBar.php b/plugin/think-library/src/builder/form/FormActionBar.php new file mode 100644 index 000000000..81d9c5d1f --- /dev/null +++ b/plugin/think-library/src/builder/form/FormActionBar.php @@ -0,0 +1,18 @@ +class('layui-form-item text-center'); + } +} diff --git a/plugin/think-library/src/builder/form/FormActions.php b/plugin/think-library/src/builder/form/FormActions.php new file mode 100644 index 000000000..6f3cbc73a --- /dev/null +++ b/plugin/think-library/src/builder/form/FormActions.php @@ -0,0 +1,40 @@ +builder->addSubmitButtonToNode($this->parent, $name, $confirm, $attrs, $class); + return $this; + } + + public function cancel(string $name = '取消编辑', string $confirm = '确定要取消编辑吗?', array $attrs = [], string $class = 'layui-btn-danger'): self + { + $this->builder->addCancelButtonToNode($this->parent, $name, $confirm, $attrs, $class); + return $this; + } + + public function button(string $name, string $type = 'button', string $confirm = '', array $attrs = [], string $class = ''): self + { + $this->builder->addActionButtonToNode($this->parent, $name, $type, $confirm, $attrs, $class); + return $this; + } + + public function html(string $html, array $schema = []): self + { + $this->builder->addButtonHtmlToNode($this->parent, $html, $schema); + return $this; + } +} diff --git a/plugin/think-library/src/builder/form/FormBlocks.php b/plugin/think-library/src/builder/form/FormBlocks.php new file mode 100644 index 000000000..551b2c2c9 --- /dev/null +++ b/plugin/think-library/src/builder/form/FormBlocks.php @@ -0,0 +1,114 @@ +fieldset(); + $node->class('layui-bg-gray')->attrs($attrs); + $node->node('legend')->html(sprintf('%s', self::escape($title))); + $callback($node); + return $node; + } + + public static function row(FormNode $parent, callable $callback, string $class = 'layui-row layui-col-space15'): FormNode + { + $node = $parent->div()->class($class); + $callback($node); + return $node; + } + + public static function col(FormNode $parent, string $class, callable $callback): FormNode + { + $node = $parent->div()->class($class); + $callback($node); + return $node; + } + + public static function selectFilter(FormNode $parent, string $name, string $filter, array $groups, string $title, string $subtitle, string $remark): FormNode + { + $node = $parent->div()->class('layui-form-item'); + $node->div()->class('help-label')->html(sprintf('%s%s', self::escape($title), self::escape($subtitle))); + + $bar = $node->div()->class('mb10'); + $options = [sprintf('', self::escape('全部插件'))]; + foreach ($groups as $group) { + $code = self::escape(strval($group['code'] ?? '')); + $nameText = self::escape(strval($group['name'] ?? $code)); + $options[] = sprintf('', $code, $nameText); + } + $bar->div()->class('layui-input-inline')->html(sprintf( + '', + self::escape($name), + self::escape($filter), + implode('', $options) + )); + $bar->div()->class('layui-form-mid color-desc')->html(self::escape($remark)); + return $node; + } + + public static function groupedTemplateChoices( + FormNode $parent, + array $groups, + string $type, + string $name, + string $groupClass, + string $dataAttribute, + string $selectedField + ): FormNode { + $node = $parent->div()->class('layui-textarea help-checks'); + + foreach ($groups as $group) { + $groupNode = $node->div()->class($groupClass)->data($dataAttribute, strval($group['code'] ?? '')); + $groupNode->div()->class('pl5 mb5')->html(sprintf( + '%s', + self::escape(strval($group['name'] ?? '')) + )); + + foreach ((array)($group['items'] ?? []) as $item) { + $value = strval($item['code'] ?? $item['id'] ?? ''); + $label = strval($item['name'] ?? $item['title'] ?? $value); + $pluginText = trim(strval($item['plugin_text'] ?? '')); + $extra = ''; + if ($pluginText !== '' && intval($item['plugin_count'] ?? 0) > 1) { + $extra = sprintf('(%s)', self::escape($pluginText)); + } + + if ($type === 'radio') { + $checked = sprintf('{if isset($vo.%s) and $vo.%s eq \'%s\'} checked{/if}', $selectedField, $selectedField, addslashes($value)); + $inputName = $name; + } else { + $checked = sprintf('{if in_array(\'%s\', $vo.%s)} checked{/if}', addslashes($value), $selectedField); + $inputName = $name . '[]'; + } + + $groupNode->html(sprintf( + '', + self::escape($type), + self::escape($inputName), + self::escape($value), + $checked, + self::escape($label), + $extra === '' ? '' : (' ' . $extra) + )); + } + } + + return $node; + } + + private static function escape(string $content): string + { + return htmlentities(BuilderLang::text($content), ENT_QUOTES, 'UTF-8'); + } +} diff --git a/plugin/think-library/src/builder/form/FormBuilder.php b/plugin/think-library/src/builder/form/FormBuilder.php new file mode 100644 index 000000000..3400eddd0 --- /dev/null +++ b/plugin/think-library/src/builder/form/FormBuilder.php @@ -0,0 +1,1874 @@ + 'regex:^[1-9][0-9]{4,11}$', + 'ip' => 'ip', + 'url' => 'url', + 'phone' => 'mobile', + 'mobile' => 'mobile', + 'email' => 'email', + 'wechat' => 'regex:^[a-zA-Z]([-_a-zA-Z0-9]{5,19})+$', + 'cardid' => 'idCard', + 'userame' => 'regex:^[a-zA-Z0-9_-]{4,16}$', + 'username' => 'regex:^[a-zA-Z0-9_-]{4,16}$', + ]; + + /** + * 生成类型. + */ + private string $type; + + /** + * 显示方式. + */ + private string $mode; + + /** + * 表单预设. + */ + private string $preset; + + /** + * 当前控制器. + */ + private Controller $class; + + /** + * 提交地址 + */ + private string $action; + + /** + * 表单变量. + */ + private string $variable = '$vo'; + + /** + * 表单标题. + */ + private string $title = ''; + + /** + * 表单项目 HTML. + */ + private array $fields = []; + + /** + * 表单内容节点. + */ + private array $contentNodes = []; + + /** + * 表单项目规则. + */ + private array $items = []; + + /** + * 按钮 HTML. + */ + private array $buttons = []; + + /** + * 按钮配置. + */ + private array $buttonItems = []; + + /** + * 标题栏按钮 HTML. + */ + private array $headerButtons = []; + + /** + * 标题栏按钮配置. + */ + private array $headerButtonItems = []; + + /** + * 附加脚本. + */ + private array $scripts = []; + + /** + * 手动附加的 _vali 兼容规则. + */ + private array $rules = []; + + /** + * 表单附加属性. + */ + private array $formAttrs = []; + + /** + * 表单主体附加属性. + */ + private array $bodyAttrs = []; + + /** + * 表单模块配置. + */ + private array $formModules = []; + + /** + * 当前布局根节点. + */ + private ?FormLayout $layout = null; + + /** + * 当前节点渲染上下文. + */ + private ?FormRenderState $renderState = null; + + /** + * 构造函数. + * + * @param string $type 页面类型 (form/add/edit 等) + * @param string $mode 页面模式 (modal/default 等) + * @param Controller $class 控制器实例 + */ + public function __construct(string $type, string $mode, Controller $class) + { + $this->type = $type; + $this->mode = $mode; + $this->preset = $mode === 'page' ? 'page-form' : 'dialog-form'; + $this->class = $class; + } + + /** + * 创建表单生成器实例. + * + * @param string $type 页面类型 (form=add/edit 等) + * @param string $mode 页面模式 (modal=弹窗,default=默认) + */ + public static function make(string $type = 'form', string $mode = 'modal'): self + { + return Library::$sapp->invokeClass(static::class, ['type' => $type, 'mode' => $mode]); + } + + /** + * 创建弹层表单. + */ + public static function dialogForm(string $type = 'form'): self + { + return self::make($type, 'modal')->preset('dialog-form'); + } + + /** + * 创建整页表单. + */ + public static function pageForm(string $type = 'form'): self + { + return self::make($type, 'page')->preset('page-form'); + } + + /** + * 定义表单结构. + * @param callable(FormLayout): void $callback + * @return $this + */ + public function define(callable $callback): self + { + $layout = new FormLayout($this); + $this->layout = $layout; + $callback($layout); + $this->contentNodes = array_merge($this->contentNodes, $layout->exportChildren()); + return $this; + } + + /** + * 完成表单构建. + * @return $this + */ + public function build(): self + { + return $this; + } + + public function preset(string $preset): self + { + $preset = trim($preset); + if ($preset !== '') { + $this->preset = $preset; + } + return $this; + } + + public function getPreset(): string + { + return $this->preset; + } + + /** + * 设置表单提交地址 + * + * @param string $url 提交地址 + * @return $this + */ + public function setAction(string $url): self + { + $this->action = $url; + return $this; + } + + /** + * 设置表单变量名称. + * + * @param string $name 变量名称 (如 'vo', 'data' 等) + * @return $this + */ + public function setVariable(string $name): self + { + $name = trim($name); + $this->variable = $name === '' ? '$vo' : ('$' . ltrim($name, '$')); + return $this; + } + + /** + * 设置表单标题. + */ + public function setTitle(string $title): self + { + $this->title = trim($title); + return $this; + } + + /** + * 设置表单属性. + * + * @param array $attrs 表单属性数组 + * @return $this + */ + public function setFormAttrs(array $attrs): self + { + $merged = []; + foreach ($attrs as $name => $value) { + $name = is_string($name) ? trim($name) : ''; + if ($name !== '') { + $merged[$name] = $value; + } + } + $this->formAttrs = BuilderAttributes::make($this->formAttrs)->merge($merged)->all(); + return $this; + } + + /** + * @return array + */ + public function getFormAttrs(): array + { + return $this->formAttrs; + } + + public function createFormAttributes(): BuilderAttributeBag + { + return new BuilderAttributeBag($this->layout, $this->formAttrs); + } + + public function createBodyAttributes(): BuilderAttributeBag + { + return new BuilderAttributeBag($this->layout, $this->bodyAttrs); + } + + public function attachFormAttributes(BuilderAttributeBag $attributes): BuilderAttributeBag + { + return $attributes->attach(fn (array $state): array => $this->replaceFormAttributes($state)); + } + + public function attachBodyAttributes(BuilderAttributeBag $attributes): BuilderAttributeBag + { + return $attributes->attach(fn (array $state): array => $this->replaceBodyAttributes($state)); + } + + /** + * @param array $state + * @return array + */ + public function replaceFormAttributes(array $state): array + { + $this->formAttrs = is_array($state['attrs'] ?? null) ? BuilderAttributes::make($state['attrs'])->all() : []; + return ['attrs' => $this->formAttrs]; + } + + /** + * @param array $state + * @return array + */ + public function replaceBodyAttributes(array $state): array + { + $this->bodyAttrs = is_array($state['attrs'] ?? null) ? BuilderAttributes::make($state['attrs'])->all() : []; + return ['attrs' => $this->bodyAttrs]; + } + + /** + * 设置表单属性. + * + * @param string $name 属性名称 + * @param mixed $value 属性值 + * @return $this + */ + public function setFormAttr(string $name, mixed $value = null): self + { + $name = trim($name); + if ($name === '') { + return $this; + } + if ($name === 'class') { + $this->formAttrs = BuilderAttributes::make($this->formAttrs)->class(is_array($value) ? $value : strval($value))->all(); + return $this; + } + $this->formAttrs = BuilderAttributes::make($this->formAttrs)->merge([$name => $value])->all(); + return $this; + } + + public function removeFormAttr(string $name): self + { + $name = trim($name); + if ($name === '') { + return $this; + } + if ($name === 'class') { + unset($this->formAttrs['class']); + return $this; + } + if (array_key_exists($name, $this->formAttrs)) { + unset($this->formAttrs[$name]); + } + return $this; + } + + /** + * 添加表单样式类. + * + * @param array|string $class 样式类 + * @return $this + */ + public function addFormClass(array|string $class): self + { + $this->formAttrs = BuilderAttributes::make($this->formAttrs)->class($class)->all(); + return $this; + } + + public function removeFormClass(array|string $class): self + { + $this->formAttrs = BuilderAttributes::make($this->formAttrs)->removeClass($class)->all(); + return $this; + } + + /** + * 设置表单主体属性. + * + * @param array $attrs 表单主体属性数组 + * @return $this + */ + public function setBodyAttrs(array $attrs): self + { + $merged = []; + foreach ($attrs as $name => $value) { + $name = is_string($name) ? trim($name) : ''; + if ($name !== '') { + $merged[$name] = $value; + } + } + $this->bodyAttrs = BuilderAttributes::make($this->bodyAttrs)->merge($merged)->all(); + return $this; + } + + /** + * @return array + */ + public function getBodyAttrs(): array + { + return $this->bodyAttrs; + } + + /** + * 设置表单主体属性. + * + * @param string $name 属性名称 + * @param mixed $value 属性值 + * @return $this + */ + public function setBodyAttr(string $name, mixed $value = null): self + { + $name = trim($name); + if ($name === '') { + return $this; + } + if ($name === 'class') { + $this->bodyAttrs = BuilderAttributes::make($this->bodyAttrs)->class(is_array($value) ? $value : strval($value))->all(); + return $this; + } + $this->bodyAttrs = BuilderAttributes::make($this->bodyAttrs)->merge([$name => $value])->all(); + return $this; + } + + public function removeBodyAttr(string $name): self + { + $name = trim($name); + if ($name === '') { + return $this; + } + if ($name === 'class') { + unset($this->bodyAttrs['class']); + return $this; + } + if (array_key_exists($name, $this->bodyAttrs)) { + unset($this->bodyAttrs[$name]); + } + return $this; + } + + /** + * 添加表单主体样式类. + * + * @param array|string $class 样式类 + * @return $this + */ + public function addBodyClass(array|string $class): self + { + $this->bodyAttrs = BuilderAttributes::make($this->bodyAttrs)->class($class)->all(); + return $this; + } + + public function removeBodyClass(array|string $class): self + { + $this->bodyAttrs = BuilderAttributes::make($this->bodyAttrs)->removeClass($class)->all(); + return $this; + } + + /** + * 设置表单主体 data 属性. + * + * @param string $name 属性名称 + * @param mixed $value 属性值 + * @return $this + */ + public function setBodyData(string $name, mixed $value = null): self + { + $name = trim($name); + if ($name !== '') { + $this->setBodyAttr('data-' . ltrim($name, '-'), $value); + } + return $this; + } + + /** + * 设置表单 data 属性. + * + * @param string $name 属性名称 + * @param mixed $value 属性值 + * @return $this + */ + public function setFormData(string $name, mixed $value = null): self + { + $name = trim($name); + if ($name !== '') { + $this->setFormAttr('data-' . ltrim($name, '-'), $value); + } + return $this; + } + + public function removeBodyData(string $name): self + { + $name = trim($name); + if ($name !== '') { + $this->removeBodyAttr('data-' . ltrim($name, '-')); + } + return $this; + } + + public function removeFormData(string $name): self + { + $name = trim($name); + if ($name !== '') { + $this->removeFormAttr('data-' . ltrim($name, '-')); + } + return $this; + } + + /** + * 添加表单模块. + * + * @param string $name 模块名称 + * @param array $config 模块配置 + * @return $this + */ + public function addFormModule(string $name, array $config = []): self + { + $this->attachFormModule($this->createFormModule($name, $config)); + return $this; + } + + public function createFormModule(string $name, array $config = []): BuilderModule + { + return new BuilderModule($name, $config, $this->layout); + } + + public function attachFormModule(BuilderModule $module): BuilderModule + { + $normalized = $this->normalizeFormModule($module->export()); + if ($normalized['name'] === '') { + return $module; + } + $index = count($this->formModules); + $this->formModules[$index] = $normalized; + return $module->attach($index, $normalized, fn (int $index, array $module): array => $this->replaceFormModule($index, $module)); + } + + /** + * @param array $module + * @return array + */ + public function replaceFormModule(int $index, array $module): array + { + $normalized = $this->normalizeFormModule($module); + if ($normalized['name'] !== '') { + $this->formModules[$index] = $normalized; + } + return $this->formModules[$index] ?? $normalized; + } + + /** + * 添加页面脚本. + * + * @param string $script JavaScript 脚本代码 + * @return $this + */ + public function addScript(string $script): self + { + $script = trim($script); + if ($script !== '') { + $this->scripts[] = $script; + } + return $this; + } + + /** + * 使用收集到的规则验证请求数据. + * + * @param array|string $input 输入数据 (默认为空,自动从请求获取) + * @param null|callable $callable 自定义验证回调 + * @return array 验证后的数据 + * @throws Exception + */ + public function validate(array|string $input = '', ?callable $callable = null): array + { + return ValidateHelper::instance()->init($this->getRequestRules(), $input, $callable); + } + + /** + * 获取可直接用于 _vali 的请求规则. + * + * @return array 验证规则数组 + */ + public function getRequestRules(): array + { + $rules = []; + foreach ($this->getFields() as $field) { + $rules[sprintf('%s.default', $field['name'])] = $this->resolveFieldDefault($field); + } + return array_merge($rules, $this->getValidateRules()); + } + + /** + * 获取 _vali 兼容规则. + */ + public function getValidateRules(): array + { + $rules = []; + foreach ($this->getFields() as $field) { + foreach ($this->buildFieldRules($field) as $rule => $message) { + $rules[$rule] = $message; + } + } + return array_merge($rules, $this->rules); + } + + /** + * 批量添加 _vali 验证规则. + * @return $this + */ + public function addValidateRules(array $rules): self + { + foreach ($rules as $key => $value) { + if (is_string($key)) { + $this->rules[$key] = $value; + } + } + return $this; + } + + /** + * 动态添加多个字段. + * @return $this + */ + public function addFields(array $fields): self + { + foreach ($fields as $field) { + is_array($field) && $this->addField($field); + } + return $this; + } + + /** + * 向指定节点追加字段. + * @param array $field + */ + public function addFieldToNode(FormNode $parent, array $field): FormField + { + $field = $this->normalizeField($field); + $this->collectField($field); + if (!$this->layout instanceof FormLayout) { + $this->fields[] = $this->renderField($field); + } + $node = $this->createFieldNode($parent, $field); + $parent->append($node); + return $node; + } + + /** + * 动态添加单个字段. + * @return $this + */ + public function addField(array $field): self + { + if ($this->layout instanceof FormLayout) { + $this->addFieldToNode($this->layout, $field); + return $this; + } + $field = $this->normalizeField($field); + $this->collectField($field); + $this->fields[] = $this->renderField($field); + return $this; + } + + /** + * 添加单条 _vali 验证规则. + * @return $this + */ + public function addValidateRule(string $name, string $rule, string $message): self + { + $name = trim($name); + $rule = trim($rule); + if ($name !== '' && $rule !== '') { + $this->rules["{$name}.{$rule}"] = $message; + } + return $this; + } + + /** + * 创建文本输入框架. + * @param string $name 字段名称 + * @param string $title 字段标题 + * @param string $substr 字段子标题 + * @param mixed $remark + * @param array $attrs 附加属性 + * @return $this + */ + public function addTextArea(string $name, string $title, string $substr = '', bool $required = false, $remark = '', array $attrs = []): self + { + return $this->addField([ + 'type' => 'textarea', + 'name' => $name, + 'title' => $title, + 'subtitle' => $substr, + 'required' => $required, + 'remark' => (string)$remark, + 'attrs' => $attrs, + ]); + } + + /** + * 创建密钥输入框. + * @param string $name 字段名称 + * @param string $title 字段标题 + * @param string $substr 字段子标题 + * @param string $remark 字段备注 + * @param bool $required 是否必填 + * @param ?string $pattern 验证规则 + * @param array $attrs 附加属性 + * @return $this + */ + public function addPassInput(string $name, string $title, string $substr = '', bool $required = false, string $remark = '', ?string $pattern = null, array $attrs = []): self + { + $attrs['type'] = 'password'; + return $this->addTextInput($name, $title, $substr, $required, $remark, $pattern, $attrs); + } + + /** + * 创建 Text 输入. + * @param string $name 字段名称 + * @param string $title 字段标题 + * @param string $substr 字段子标题 + * @param string $remark 字段备注 + * @param bool $required 是否必填 + * @param ?string $pattern 验证规则 + * @param array $attrs 附加属性 + * @return $this + */ + public function addTextInput(string $name, string $title, string $substr = '', bool $required = false, string $remark = '', ?string $pattern = null, array $attrs = []): self + { + return $this->addField([ + 'type' => $attrs['type'] ?? 'text', + 'name' => $name, + 'title' => $title, + 'subtitle' => $substr, + 'required' => $required, + 'remark' => $remark, + 'pattern' => $pattern, + 'attrs' => $attrs, + ]); + } + + /** + * 添加取消按钮. + * @param string $name 按钮名称 + * @param string $confirm 确认提示 + * @return $this + */ + public function addCancelButton(string $name = '取消编辑', string $confirm = '确定要取消编辑吗?', array $attrs = [], string $class = 'layui-btn-danger'): self + { + return $this->addButton($name, $confirm, 'button', $class, array_merge($this->buildCancelAttrs(), $attrs)); + } + + /** + * 向指定动作条追加取消按钮. + */ + public function addCancelButtonToNode(FormActionBar $parent, string $name = '取消编辑', string $confirm = '确定要取消编辑吗?', array $attrs = [], string $class = 'layui-btn-danger'): self + { + return $this->addButtonToNode($parent, $name, $confirm, 'button', $class, array_merge($this->buildCancelAttrs(), $attrs)); + } + + /** + * 添加提交按钮. + * @param string $name 按钮名称 + * @param string $confirm 确认提示 + * @return $this + */ + public function addSubmitButton(string $name = '保存数据', string $confirm = '', array $attrs = [], string $class = ''): self + { + return $this->addButton($name, $confirm, 'submit', $class, $attrs); + } + + /** + * 向指定动作条追加提交按钮. + */ + public function addSubmitButtonToNode(FormActionBar $parent, string $name = '保存数据', string $confirm = '', array $attrs = [], string $class = ''): self + { + return $this->addButtonToNode($parent, $name, $confirm, 'submit', $class, $attrs); + } + + /** + * 添加通用动作按钮. + * @return $this + */ + public function addActionButton(string $name, string $type = 'button', string $confirm = '', array $attrs = [], string $class = ''): self + { + return $this->addButton($name, $confirm, $type, $class, $attrs); + } + + /** + * 向指定动作条追加通用按钮. + */ + public function addActionButtonToNode(FormActionBar $parent, string $name, string $type = 'button', string $confirm = '', array $attrs = [], string $class = ''): self + { + return $this->addButtonToNode($parent, $name, $confirm, $type, $class, $attrs); + } + + /** + * 添加上传单图字段. + * @param string $name 字段名称 + * @param string $title 字段标题 + * @param string $substr 字段子标题 + * @param bool $required 必填字段 + * @param array $attrs 附加属性 + * @return $this + */ + public function addUploadOneImage(string $name, string $title, string $substr = '', bool $required = false, array $attrs = []): self + { + return $this->addField([ + 'type' => 'image', + 'name' => $name, + 'title' => $title, + 'subtitle' => $substr, + 'required' => $required, + 'attrs' => $attrs, + ]); + } + + /** + * 添加上传视频字段. + * @param string $name 字段名称 + * @param string $title 字段标题 + * @param string $substr 字段子标题 + * @param bool $required 必填字段 + * @param array $attrs 附加属性 + * @return $this + */ + public function addUploadOneVideo(string $name, string $title, string $substr = '', bool $required = false, array $attrs = []): self + { + return $this->addField([ + 'type' => 'video', + 'name' => $name, + 'title' => $title, + 'subtitle' => $substr, + 'required' => $required, + 'attrs' => $attrs, + ]); + } + + /** + * 创建上传多图字段. + * @param string $name 字段名称 + * @param string $title 字段标题 + * @param string $substr 字段子标题 + * @param bool $required 必填字段 + * @param array $attrs 附加属性 + * @return $this + */ + public function addUploadMulImage(string $name, string $title, string $substr = '', bool $required = false, array $attrs = []): self + { + return $this->addField([ + 'type' => 'images', + 'name' => $name, + 'title' => $title, + 'subtitle' => $substr, + 'required' => $required, + 'attrs' => $attrs, + ]); + } + + /** + * 添加单选框架字段. + * @param string $name 字段名称 + * @param string $title 字段标题 + * @param string $substr 字段子标题 + * @param string $vname 变量名称 + * @param bool $required 是否必选 + * @param array $attrs 附加属性 + * @return $this + */ + public function addRadioInput(string $name, string $title, string $substr, string $vname, bool $required = false, array $attrs = []): self + { + return $this->addCheckInput($name, $title, $substr, $vname, $required, $attrs, 'radio'); + } + + /** + * 创建复选框字段. + * @param string $name 字段名称 + * @param string $title 字段标题 + * @param string $substr 字段子标题 + * @param string $vname 变量名称 + * @param bool $required 是否必选 + * @param array $attrs 附加属性 + * @return $this + */ + public function addCheckInput(string $name, string $title, string $substr, string $vname, bool $required = false, array $attrs = [], string $type = 'checkbox'): self + { + return $this->addField([ + 'type' => $type, + 'name' => $name, + 'title' => $title, + 'subtitle' => $substr, + 'required' => $required, + 'attrs' => $attrs, + 'vname' => $vname, + ]); + } + + /** + * 添加下拉选择字段. + * @param string $name 字段名称 + * @param string $title 字段标题 + * @param string $substr 字段子标题 + * @param bool $required 是否必填 + * @param string $remark 字段备注 + * @param array $options 静态选项 + * @param string $vname 变量名称 + * @param array $attrs 附加属性 + * @return $this + */ + public function addSelectInput(string $name, string $title, string $substr = '', bool $required = false, string $remark = '', array $options = [], string $vname = '', array $attrs = []): self + { + return $this->addField([ + 'type' => 'select', + 'name' => $name, + 'title' => $title, + 'subtitle' => $substr, + 'required' => $required, + 'remark' => $remark, + 'options' => $options, + 'vname' => $vname, + 'attrs' => $attrs, + ]); + } + + /** + * 显示模板内容. + * @return mixed + */ + public function fetch(array $vars = []) + { + $html = ''; + $type = "{$this->type}.{$this->mode}"; + if ($type === 'form.page') { + $html = $this->_buildFormPage(); + } elseif ($type === 'form.modal') { + $html = $this->_buildFormModal(); + } + $vars['formBuilder'] = $vars['formBuilder'] ?? $this; + $vars['formSchema'] = $vars['formSchema'] ?? $this->toArray(); + $vars['formRules'] = $vars['formRules'] ?? $this->getValidateRules(); + $vars['staticRoot'] = strval($vars['staticRoot'] ?? AppService::uri('static')); + foreach (get_object_vars($this->class) as $k => $v) { + $vars[$k] = $v; + } + $html = $this->renderRuntimeTemplate($html, $vars); + throw new HttpResponseException(display($html, $vars)); + } + + /** + * 添加按钮 HTML. + * @return $this + */ + public function addButtonHtml(string $html, array $schema = []): self + { + $this->buttons[] = $html; + $this->buttonItems[] = array_merge(['type' => 'html', 'html' => $html], $schema); + if ($this->layout instanceof FormLayout) { + $bar = $this->layout->actionBar(); + $bar->append(new FormButton($this, array_merge(['type' => 'html', 'html' => $html], $schema), $html)); + } + return $this; + } + + /** + * 添加标题栏按钮 HTML. + */ + public function addHeaderButtonHtml(string $html, array $schema = []): self + { + $this->headerButtons[] = $html; + $this->headerButtonItems[] = array_merge(['type' => 'html', 'html' => $html], $schema); + return $this; + } + + /** + * 向指定动作条追加按钮 HTML. + */ + public function addButtonHtmlToNode(FormActionBar $parent, string $html, array $schema = []): self + { + $button = array_merge(['type' => 'html', 'html' => $html], $schema); + $this->buttons[] = $html; + $this->buttonItems[] = $button; + $parent->append(new FormButton($this, $button, $html)); + return $this; + } + + /** + * 获取构建数据. + */ + public function toArray(): array + { + $content = $this->currentContentNodes(); + return [ + 'type' => $this->type, + 'mode' => $this->mode, + 'preset' => $this->preset, + 'title' => BuilderLang::text($this->title), + 'action' => $this->action ?? '', + 'variable' => $this->variable, + 'attrs' => $this->buildFormAttrs(false), + 'body_attrs' => $this->buildBodyAttrs(), + 'modules' => $this->formModules, + 'content' => $this->normalizeSchemaValue($content), + 'fields' => $this->getFields(), + 'buttons' => $this->getButtons(), + 'header_buttons' => $this->headerButtonItems, + 'rules' => $this->getValidateRules(), + ]; + } + + /** + * 获取字段规则配置. + */ + public function getFields(): array + { + if ($this->layout instanceof FormLayout) { + return $this->extractFieldsFromNodes($this->currentContentNodes()); + } + return array_values($this->items); + } + + /** + * @return array> + */ + public function getButtons(): array + { + if ($this->layout instanceof FormLayout) { + return $this->extractButtonsFromNodes($this->currentContentNodes()); + } + return $this->buttonItems; + } + + /** + * 添加标题栏按钮. + */ + public function addHeaderButton(string $name, string $type = 'button', string $confirm = '', array $attrs = [], string $class = ''): self + { + $renderer = new BuilderAttributesRenderer(); + $name = BuilderLang::text($name); + $confirm = BuilderLang::text($confirm); + $attrs['type'] = $type; + if ($confirm !== '') { + $attrs['data-confirm'] = $confirm; + } + $attrs = BuilderLang::attrs(BuilderAttributes::make($attrs)->class(trim("layui-btn {$class}"))->all()); + $html = sprintf('', $renderer->render($attrs), $name); + $button = [ + 'name' => $name, + 'confirm' => $confirm, + 'type' => $type, + 'class' => trim("layui-btn {$class}"), + 'attrs' => $attrs, + ]; + $this->headerButtons[] = $html; + $this->headerButtonItems[] = $button; + return $this; + } + + /** + * 添加表单按钮. + * @param string $name 按钮名称 + * @param string $confirm 确认提示 + * @param string $type 按钮类型 + * @param string $class 按钮样式 + * @param array $attrs 附加属性 + * @return $this + */ + protected function addButton(string $name, string $confirm, string $type, string $class = '', array $attrs = []): self + { + $renderer = new BuilderAttributesRenderer(); + $name = BuilderLang::text($name); + $confirm = BuilderLang::text($confirm); + $attrs['type'] = $type; + if ($confirm !== '') { + $attrs['data-confirm'] = $confirm; + } + $attrs = BuilderLang::attrs(BuilderAttributes::make($attrs)->class(trim("layui-btn {$class}"))->all()); + $html = sprintf('', $renderer->render($attrs), $name); + $button = [ + 'name' => $name, + 'confirm' => $confirm, + 'type' => $type, + 'class' => trim("layui-btn {$class}"), + 'attrs' => $attrs, + ]; + $this->buttons[] = $html; + $this->buttonItems[] = $button; + if ($this->layout instanceof FormLayout) { + $bar = $this->layout->actionBar(); + $bar->append(new FormButton($this, $button, $html)); + } + return $this; + } + + /** + * 向指定动作条追加按钮. + */ + protected function addButtonToNode(FormActionBar $parent, string $name, string $confirm, string $type, string $class = '', array $attrs = []): self + { + $renderer = new BuilderAttributesRenderer(); + $name = BuilderLang::text($name); + $confirm = BuilderLang::text($confirm); + $attrs['type'] = $type; + if ($confirm !== '') { + $attrs['data-confirm'] = $confirm; + } + $attrs = BuilderLang::attrs(BuilderAttributes::make($attrs)->class(trim("layui-btn {$class}"))->all()); + $html = sprintf('', $renderer->render($attrs), $name); + $button = [ + 'name' => $name, + 'confirm' => $confirm, + 'type' => $type, + 'class' => trim("layui-btn {$class}"), + 'attrs' => $attrs, + ]; + $this->buttons[] = $html; + $this->buttonItems[] = $button; + $parent->append(new FormButton($this, $button, $html)); + return $this; + } + + /** + * 兼容旧版受保护输入方法. + * @param string $name 字段名称 + * @param string $title 字段标题 + * @param string $subtitle 字段子标题 + * @param string $remark 字段备注 + * @param array $attrs 附加属性 + * @return $this + */ + protected function addInput(string $name, string $title, string $subtitle = '', string $remark = '', array $attrs = []): self + { + return $this->addField([ + 'type' => $attrs['type'] ?? 'text', + 'name' => $name, + 'title' => $title, + 'subtitle' => $subtitle, + 'remark' => $remark, + 'required' => !empty($attrs['required']), + 'pattern' => $attrs['pattern'] ?? null, + 'attrs' => $attrs, + ]); + } + + /** + * 创建字段节点对象. + * @param array $field + */ + private function createFieldNode(FormNode $parent, array $field): FormField + { + return match ($field['type']) { + 'text', 'password', 'textarea' => new FormTextField($this, $parent, $field), + 'select' => new FormSelectField($this, $parent, $field), + 'checkbox', 'radio' => new FormChoiceField($this, $parent, $field), + 'image', 'video', 'images' => new FormUploadField($this, $parent, $field), + default => new FormField($this, $parent, $field), + }; + } + + /** + * @param array $module + * @return array + */ + private function normalizeFormModule(array $module): array + { + return [ + 'name' => trim(strval($module['name'] ?? '')), + 'config' => is_array($module['config'] ?? null) ? $module['config'] : [], + ]; + } + + /** + * 解析字段默认值,确保可回填全部输入字段. + */ + private function resolveFieldDefault(array $field): array|string + { + if (array_key_exists('default', $field) && $field['default'] !== null) { + $default = $field['default']; + return $field['type'] === 'checkbox' + ? array_values(array_map('strval', is_array($default) ? $default : str2arr(strval($default)))) + : strval($default); + } + return $field['type'] === 'checkbox' ? [] : ''; + } + + /** + * 规范字段配置. + */ + private function normalizeField(array $field): array + { + $field = array_merge([ + 'type' => 'text', + 'name' => '', + 'title' => '', + 'subtitle' => '', + 'substr' => '', + 'remark' => '', + 'required' => false, + 'pattern' => null, + 'attrs' => [], + 'rules' => [], + 'vname' => '', + 'options' => [], + 'default' => null, + 'upload' => [], + 'parts' => [], + ], $field); + if ($field['subtitle'] === '' && $field['substr'] !== '') { + $field['subtitle'] = (string)$field['substr']; + } + $field['type'] = $this->normalizeType((string)$field['type']); + $field['name'] = trim((string)$field['name']); + $field['title'] = trim(BuilderLang::text((string)$field['title'])); + $field['subtitle'] = BuilderLang::text((string)$field['subtitle']); + $field['remark'] = BuilderLang::text((string)$field['remark']); + $field['required'] = !empty($field['required']); + $field['pattern'] = $field['pattern'] === null || $field['pattern'] === '' ? null : (string)$field['pattern']; + $field['attrs'] = BuilderLang::attrs(is_array($field['attrs']) ? $field['attrs'] : []); + $field['rules'] = is_array($field['rules']) ? $field['rules'] : []; + $field['vname'] = trim((string)$field['vname']); + $field['options'] = BuilderLang::options(is_array($field['options']) ? $field['options'] : []); + if ($field['default'] === null && array_key_exists('value', $field['attrs'])) { + $field['default'] = $field['attrs']['value']; + unset($field['attrs']['value']); + } + $field['upload'] = is_array($field['upload']) ? $field['upload'] : []; + $field['parts'] = is_array($field['parts']) ? $field['parts'] : []; + if ($field['name'] === '' || $field['title'] === '') { + throw new \InvalidArgumentException('FormBuilder 字段 name 与 title 不能为空'); + } + + $field['attrs'] = $this->normalizeFieldAttrs($field); + $field['validate'] = [ + 'messages' => array_filter([ + 'required' => $field['attrs']['required-error'] ?? '', + 'pattern' => $field['attrs']['pattern-error'] ?? '', + ]), + 'portable' => array_filter($this->buildFieldPortableRules($field)), + ]; + + return $field; + } + + /** + * 规范组件类型. + */ + private function normalizeType(string $type): string + { + $type = strtolower(trim($type)); + return match ($type) { + '', 'input' => 'text', + 'pass' => 'password', + 'upload-image', 'upload-one-image' => 'image', + 'upload-video', 'upload-one-video' => 'video', + 'upload-images', 'upload-mul-image' => 'images', + default => $type, + }; + } + + /** + * 解析 pattern 对应的后端规则. + */ + private function resolvePatternRule(string $pattern): ?string + { + $pattern = trim($pattern); + if ($pattern === '') { + return null; + } + if (isset(self::PATTERN_RULES[$pattern])) { + return self::PATTERN_RULES[$pattern]; + } + if (str_contains($pattern, '|')) { + return null; + } + + $regex = preg_replace('~(? $field + * @return array + */ + private function buildFieldRules(array $field): array + { + $field = $this->normalizeField($field); + return $this->buildFieldPortableRules($field); + } + + /** + * 根据已规范化字段构建 _vali 规则. + * @param array $field + * @return array + */ + private function buildFieldPortableRules(array $field): array + { + $rules = []; + if (!empty($field['required'])) { + $rules["{$field['name']}.require"] = strval($field['attrs']['required-error'] ?? BuilderLang::format('%s不能为空!', [$field['title']])); + } + if (is_string($field['pattern']) && ($rule = $this->resolvePatternRule($field['pattern']))) { + $rules["{$field['name']}.{$rule}"] = strval($field['attrs']['pattern-error'] ?? BuilderLang::format('%s格式错误!', [$field['title']])); + } + foreach ($field['rules'] as $rule => $message) { + if (is_string($rule) && $rule !== '') { + $rules["{$field['name']}.{$rule}"] = strval($message); + } + } + return $rules; + } + + /** + * 收集字段配置. + */ + private function collectField(array $field): void + { + $this->items[] = [ + 'name' => $field['name'], + 'type' => $field['type'], + 'title' => $field['title'], + 'subtitle' => $field['subtitle'], + 'remark' => $field['remark'], + 'required' => $field['required'], + 'pattern' => $field['pattern'], + 'attrs' => $field['attrs'], + 'vname' => $field['vname'], + 'options' => $field['options'], + 'default' => $field['default'], + 'upload' => $field['upload'], + 'parts' => $field['parts'], + 'validate' => $field['validate'], + ]; + } + + /** + * 渲染字段 HTML. + */ + private function renderField(array $field): string + { + $field = $this->normalizeField($field); + return $this->renderPipeline()->renderField($field, $this->variable); + } + + /** + * 从内容节点提取字段数组. + * @param array> $nodes + * @return array> + */ + private function extractFieldsFromNodes(array $nodes): array + { + $fields = []; + foreach ($nodes as $node) { + if (!is_array($node)) { + continue; + } + if (($node['type'] ?? '') === 'field' && is_array($node['field'] ?? null)) { + $field = $node['field']; + if (is_array($node['attrs'] ?? null) && count($node['attrs']) > 0) { + $field['container_attrs'] = $node['attrs']; + } + if (is_array($node['modules'] ?? null) && count($node['modules']) > 0) { + $field['container_modules'] = $node['modules']; + } + if (is_array($node['parts'] ?? null) && count($node['parts']) > 0) { + $field['parts'] = $node['parts']; + } + $fields[] = $this->normalizeField($field); + continue; + } + if (is_array($node['children'] ?? null)) { + $fields = array_merge($fields, $this->extractFieldsFromNodes($node['children'])); + } + } + return $fields; + } + + /** + * @param array> $nodes + * @return array> + */ + private function extractButtonsFromNodes(array $nodes): array + { + $buttons = []; + foreach ($nodes as $node) { + if (!is_array($node)) { + continue; + } + if (($node['type'] ?? '') === 'button' && is_array($node['button'] ?? null)) { + $buttons[] = $node['button']; + continue; + } + if (is_array($node['children'] ?? null)) { + $buttons = array_merge($buttons, $this->extractButtonsFromNodes($node['children'])); + } + } + return $buttons; + } + + /** + * 生成页面表单模板 + */ + private function _buildFormPage(): string + { + return $this->buildFormShell(); + } + + /** + * 生成弹层表单模板 + */ + private function _buildFormModal(): string + { + return $this->buildFormShell(); + } + + /** + * 渲染表单主体结构. + */ + private function buildFormShell(): string + { + $content = $this->currentContentNodes(); + $fields = $this->layout instanceof FormLayout ? [] : $this->fields; + $buttons = $this->layout instanceof FormLayout ? [] : $this->buttons; + $this->renderState = $this->createRenderState($this->toArray()); + try { + return $this->renderPipeline()->renderShell( + $this->buildFormAttrs(), + $this->buildBodyAttrs(), + $content, + $fields, + $this->headerButtons, + $buttons, + $this->renderState, + $this->scripts + ); + } finally { + $this->renderState = null; + } + } + + /** + * 渲染内容节点. + * @param array> $nodes + */ + private function renderContentNodes(array $nodes): string + { + return $this->renderPipeline()->renderContentNodes($nodes, $this->currentRenderState()); + } + + /** + * @param mixed $value + * @return mixed + */ + private function normalizeSchemaValue($value) + { + if (is_array($value)) { + if (array_is_list($value)) { + $result = []; + foreach ($value as $key => $item) { + $result[$key] = $this->normalizeSchemaValue($item); + } + return $result; + } + + $result = []; + foreach ($value as $key => $item) { + if ($key === 'attrs' && is_array($item)) { + $result[$key] = BuilderLang::attrs($item); + continue; + } + if (in_array(strval($key), ['title', 'label', 'legend', 'placeholder', 'remark', 'subtitle', 'confirm', 'html'], true) && is_string($item)) { + $result[$key] = BuilderLang::text($item); + continue; + } + $result[$key] = $this->normalizeSchemaValue($item); + } + return $result; + } + + return $value; + } + + /** + * @return array> + */ + private function currentContentNodes(): array + { + return $this->layout instanceof FormLayout ? $this->layout->exportChildren() : $this->contentNodes; + } + + /** + * 获取提交地址. + */ + private function resolveAction(): string + { + if (isset($this->action) && $this->action !== '') { + return $this->action; + } + try { + return url()->build(); + } catch (\Throwable) { + return ''; + } + } + + /** + * 构建表单根节点属性. + */ + private function buildFormAttrs(bool $resolveAction = true): array + { + $class = $this->mode === 'page' ? 'layui-form' : 'layui-form layui-card'; + return BuilderAttributes::make([ + 'action' => $resolveAction ? $this->resolveAction() : ($this->action ?? ''), + 'method' => 'post', + 'data-auto' => 'true', + 'data-builder-scope' => 'form', + 'data-builder-mode' => $this->mode, + 'data-builder-preset' => $this->preset, + ])->merge($this->formAttrs) + ->modules($this->formModules) + ->class($class) + ->all(); + } + + /** + * 构建表单主体属性. + */ + private function buildBodyAttrs(): array + { + $class = $this->mode === 'page' ? 'pa40' : 'layui-card-body pl40 pr40 pt20 pb20'; + return BuilderAttributes::make($this->bodyAttrs) + ->class($class) + ->all(); + } + + /** + * 构建取消动作属性. + * @return array + */ + private function buildCancelAttrs(): array + { + if ($this->mode === 'page') { + return ['data-target-backup' => null]; + } + + return ['data-close' => null]; + } + + /** + * @param array $schema + */ + private function createRenderState(array $schema): FormRenderState + { + $attrsRenderer = new BuilderAttributesRenderer(); + return new FormRenderState( + $schema, + new FormNodeRendererFactory(), + new FormNodeRenderContext( + $this->variable, + fn (array $nodes): string => $this->renderContentNodes($nodes), + [$attrsRenderer, 'render'], + fn (array $field): string => $this->renderField($field) + ) + ); + } + + private function currentRenderState(): FormRenderState + { + return $this->renderState ?? $this->createRenderState($this->toArray()); + } + + private function renderPipeline(): FormRenderPipeline + { + return new FormRenderPipeline(); + } + + /** + * 解析 Builder 运行时模板变量,避免依赖外层视图二次编译。 + * @param array $vars + */ + private function renderRuntimeTemplate(string $html, array $vars): string + { + $html = $this->renderForeachBlocks($html, $vars); + $html = $this->renderConditionBlocks($html, $vars); + $html = $this->renderJoinedValueExpressions($html, $vars); + $html = $this->renderVariableExpressions($html, $vars); + return $this->renderPlainExpressions($html, $vars); + } + + /** + * @param array $vars + */ + private function renderForeachBlocks(string $html, array $vars): string + { + $patterns = [ + '/\{foreach\s+\$(\w+)\s+as\s+\$(\w+)=\>\$(\w+)\}(.*?)\{\/foreach\}/s', + '/(.*?)/s', + ]; + + foreach ($patterns as $pattern) { + while (preg_match($pattern, $html)) { + $html = preg_replace_callback($pattern, function (array $match) use ($vars): string { + $items = $this->resolveTemplateValue($vars, $match[1]); + if (!is_iterable($items)) { + return ''; + } + + $result = ''; + foreach ($items as $key => $value) { + $local = $vars; + $local[$match[2]] = $key; + $local[$match[3]] = $value; + $result .= $this->renderRuntimeTemplate($match[4], $local); + } + return $result; + }, $html) ?? $html; + } + } + + return $html; + } + + /** + * @param array $vars + */ + private function renderConditionBlocks(string $html, array $vars): string + { + $patterns = [ + '/\{notempty\s+name="([^"]+)"\}(.*?)\{\/notempty\}/s' => fn (array $m) => $this->isNotEmptyValue($this->resolveTemplateValue($vars, $m[1])) ? $this->renderRuntimeTemplate($m[2], $vars) : '', + '/\{if\s+(.+?)\}(.*?)(?:\{else\}(.*?))?\{\/if\}/s' => function (array $m) use ($vars): string { + return $this->evaluateTemplateCondition($m[1], $vars) + ? $this->renderRuntimeTemplate($m[2], $vars) + : $this->renderRuntimeTemplate($m[3] ?? '', $vars); + }, + '/(.*?)(?:(.*?))?/s' => function (array $m) use ($vars): string { + return $this->evaluateTemplateCondition($m[1], $vars) + ? $this->renderRuntimeTemplate($m[2], $vars) + : $this->renderRuntimeTemplate($m[3] ?? '', $vars); + }, + ]; + + foreach ($patterns as $pattern => $renderer) { + while (preg_match($pattern, $html)) { + $html = preg_replace_callback($pattern, $renderer, $html) ?? $html; + } + } + + return $html; + } + + /** + * @param array $vars + */ + private function renderJoinedValueExpressions(string $html, array $vars): string + { + $pattern = '/\{:\$(\w+)\.([\w.]+)\s*\?\?\s*null\s*\?\s*\(is_array\(\$\1\.\2\)\s*\?\s*join\(\'([^\']*)\',\s*\$\1\.\2\)\s*:\s*\$\1\.\2\)\s*:\s*\'\'\}/'; + return preg_replace_callback($pattern, function (array $match) use ($vars): string { + $value = $this->resolveTemplateValue($vars, $match[1] . '.' . $match[2]); + if ($value === null) { + return ''; + } + if (is_array($value)) { + return implode(stripslashes($match[3]), array_map('strval', $value)); + } + return strval($value); + }, $html) ?? $html; + } + + /** + * @param array $vars + */ + private function renderVariableExpressions(string $html, array $vars): string + { + $patterns = [ + '/\{(\$?[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)\|default=(["\'])(.*?)\2\}/' => fn (array $match): string => $this->replaceVariableExpression($match, $vars, stripcslashes($match[3])), + '/\{(\$?[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)\|default=([^}\'"]+)\}/' => fn (array $match): string => $this->replaceVariableExpression($match, $vars, trim($match[2])), + ]; + + foreach ($patterns as $pattern => $renderer) { + $html = preg_replace_callback($pattern, $renderer, $html) ?? $html; + } + + return $html; + } + + /** + * @param array $vars + */ + private function renderPlainExpressions(string $html, array $vars): string + { + $pattern = '/\{(\$?[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)\}/'; + return preg_replace_callback($pattern, function (array $match) use ($vars): string { + return $this->stringifyTemplateValue($this->resolveTemplateValue($vars, $match[1])); + }, $html) ?? $html; + } + + /** + * @param array $vars + */ + private function resolveTemplateValue(array $vars, string $path): mixed + { + $segments = array_values(array_filter(explode('.', ltrim($path, '$')), static fn (string $item): bool => $item !== '')); + if (count($segments) < 1) { + return null; + } + + $value = $vars[$segments[0]] ?? null; + foreach (array_slice($segments, 1) as $segment) { + if (is_array($value) && array_key_exists($segment, $value)) { + $value = $value[$segment]; + } elseif (is_object($value) && isset($value->{$segment})) { + $value = $value->{$segment}; + } else { + return null; + } + } + + return $value; + } + + /** + * @param array $vars + */ + private function evaluateTemplateCondition(string $condition, array $vars): bool + { + foreach (preg_split('/\s+and\s+/', trim($condition)) ?: [] as $segment) { + $segment = trim($segment); + if ($segment === '') { + continue; + } + + if (preg_match('/^isset\((.+)\)$/', $segment, $match)) { + if ($this->resolveTemplateOperand($vars, $match[1]) === null) { + return false; + } + continue; + } + + if (preg_match('/^is_array\((.+)\)$/', $segment, $match)) { + if (!is_array($this->resolveTemplateOperand($vars, $match[1]))) { + return false; + } + continue; + } + + if (preg_match('/^in_array\((.+?),\s*(.+)\)$/', $segment, $match)) { + $needle = $this->resolveTemplateOperand($vars, $match[1]); + $haystack = $this->resolveTemplateOperand($vars, $match[2]); + if (!is_array($haystack) || !in_array($needle, $haystack, false)) { + return false; + } + continue; + } + + if (preg_match('/^strval\((.+)\)\s*(?:eq|==)\s*strval\((.+)\)$/', $segment, $match)) { + if (strval($this->resolveTemplateOperand($vars, $match[1])) !== strval($this->resolveTemplateOperand($vars, $match[2]))) { + return false; + } + continue; + } + + if (preg_match('/^strval\((.+)\)\s*(?:eq|==)\s*(.+)$/', $segment, $match)) { + if (strval($this->resolveTemplateOperand($vars, $match[1])) !== strval($this->resolveTemplateOperand($vars, $match[2]))) { + return false; + } + continue; + } + + return false; + } + + return true; + } + + /** + * @param array $vars + */ + private function resolveTemplateOperand(array $vars, string $operand): mixed + { + $operand = trim($operand); + if ($operand === 'null') { + return null; + } + if (preg_match('/^[\'"](.*)[\'"]$/s', $operand, $match)) { + return stripcslashes($match[1]); + } + return $this->resolveTemplateValue($vars, $operand); + } + + private function isNotEmptyValue(mixed $value): bool + { + if (is_array($value)) { + return count($value) > 0; + } + return !($value === null || $value === '' || $value === false); + } + + private function stringifyTemplateValue(mixed $value): string + { + if (is_array($value)) { + return implode(',', array_map('strval', $value)); + } + if ($value === null) { + return ''; + } + return strval($value); + } + + /** + * @param array $match + * @param array $vars + */ + private function replaceVariableExpression(array $match, array $vars, string $default): string + { + $value = $this->resolveTemplateValue($vars, $match[1]); + return $this->stringifyTemplateValue($value === null || $value === '' ? $default : $value); + } +} diff --git a/plugin/think-library/src/builder/form/FormButton.php b/plugin/think-library/src/builder/form/FormButton.php new file mode 100644 index 000000000..579a3969d --- /dev/null +++ b/plugin/think-library/src/builder/form/FormButton.php @@ -0,0 +1,33 @@ + $button + */ + public function __construct(FormBuilder $builder, private array $button, private string $buttonHtml) + { + parent::__construct($builder, 'button', ''); + } + + /** + * 导出节点数组. + * @return array + */ + public function export(): array + { + return [ + 'type' => 'button', + 'button' => $this->button, + 'html' => $this->buttonHtml, + ]; + } +} diff --git a/plugin/think-library/src/builder/form/FormChoiceField.php b/plugin/think-library/src/builder/form/FormChoiceField.php new file mode 100644 index 000000000..b060c91af --- /dev/null +++ b/plugin/think-library/src/builder/form/FormChoiceField.php @@ -0,0 +1,17 @@ +optionsItem()->source($name)->end(); + } +} diff --git a/plugin/think-library/src/builder/form/FormComponents.php b/plugin/think-library/src/builder/form/FormComponents.php new file mode 100644 index 000000000..5f4b480ac --- /dev/null +++ b/plugin/think-library/src/builder/form/FormComponents.php @@ -0,0 +1,52 @@ +text($text); + } + + public static function readonlyField(): ReadonlyFieldComponent + { + return ReadonlyFieldComponent::make(); + } + + public static function pickerField(): PickerFieldComponent + { + return PickerFieldComponent::make(); + } + + /** + * @param array> $themes + */ + public static function themePalette(array $themes = [], string $current = ''): ThemePaletteComponent + { + return ThemePaletteComponent::make()->themes($themes)->current($current); + } +} diff --git a/plugin/think-library/src/builder/form/FormField.php b/plugin/think-library/src/builder/form/FormField.php new file mode 100644 index 000000000..4183d8614 --- /dev/null +++ b/plugin/think-library/src/builder/form/FormField.php @@ -0,0 +1,472 @@ + + */ + private array $parts = []; + + /** + * @param array $field + */ + public function __construct(FormBuilder $builder, private FormNode $parent, protected array $field) + { + parent::__construct($builder, 'field', ''); + } + + public function name(string $name): static + { + $name = trim($name); + if ($name !== '') { + $this->field['name'] = $name; + } + return $this; + } + + public function title(string $title): static + { + $this->field['title'] = trim($title); + return $this; + } + + public function subtitle(string $subtitle): static + { + $this->field['subtitle'] = $subtitle; + return $this; + } + + public function remark(string $remark): static + { + $this->field['remark'] = $remark; + return $this; + } + + public function variable(string $name): static + { + $this->field['vname'] = trim($name); + return $this; + } + + public function source(string $name): static + { + return $this->optionsItem()->source($name)->end(); + } + + public function search(bool $enabled = true): static + { + return $enabled ? $this->setFieldAttr('lay-search', null) : $this->unsetFieldAttr('lay-search'); + } + + public function options(array $options): static + { + return $this->optionsItem()->options($options)->end(); + } + + public function option(string|int $value, string $label): static + { + return $this->optionsItem()->option($value, $label)->end(); + } + + public function optionsItem(): FormFieldOptions + { + $options = is_array($this->field['options'] ?? null) ? $this->field['options'] : []; + $source = trim(strval($this->field['vname'] ?? '')); + return (new FormFieldOptions($this, $options, $source)) + ->attach(fn(array $state): array => $this->replaceOptionsState($state)); + } + + public function required(bool $required = true, string $message = ''): static + { + $this->field['required'] = $required; + if ($message !== '') { + $this->setFieldAttr('required-error', $message); + } elseif (!$required) { + $this->unsetFieldAttr('required-error'); + } + return $this; + } + + public function pattern(?string $pattern, string $message = ''): static + { + $this->field['pattern'] = $pattern === null || trim($pattern) === '' ? null : trim($pattern); + if ($message !== '') { + $this->setFieldAttr('pattern-error', $message); + } elseif ($this->field['pattern'] === null) { + $this->unsetFieldAttr('pattern-error'); + } + return $this; + } + + public function rule(string $rule, string $message): static + { + $rule = trim($rule); + if ($rule !== '') { + $this->field['rules'] = is_array($this->field['rules'] ?? null) ? $this->field['rules'] : []; + $this->field['rules'][$rule] = $message; + } + return $this; + } + + public function rules(array $rules): static + { + foreach ($rules as $rule => $message) { + if (is_string($rule)) { + $this->rule($rule, strval($message)); + } + } + return $this; + } + + public function placeholder(string $placeholder): static + { + return $this->setFieldAttr('placeholder', $placeholder); + } + + public function maxlength(int $length): static + { + return $this->setFieldAttr('maxlength', $length); + } + + public function inputRightIcon(string $iconClass, array $attrs = [], string|array $inputClass = 'pr40'): static + { + $iconClass = trim($iconClass); + if ($iconClass === '') { + return $this; + } + + $renderer = new BuilderAttributesRenderer(); + $attrs['onmousedown'] = $this->mergeEventHandler(strval($attrs['onmousedown'] ?? ''), 'event.preventDefault();event.stopPropagation();'); + $attrs['ontouchstart'] = $this->mergeEventHandler(strval($attrs['ontouchstart'] ?? ''), 'event.preventDefault();event.stopPropagation();'); + $attrs = BuilderAttributes::make($attrs) + ->class(trim("input-right-icon layui-icon {$iconClass}")) + ->all(); + + $this->body()->class('relative'); + $this->input()->class($inputClass)->html(sprintf('', $renderer->render($attrs))); + return $this; + } + + public function defaultValue(mixed $value): static + { + $this->field['default'] = $value; + return $this; + } + + public function readonly(bool $readonly = true): static + { + return $readonly ? $this->setFieldAttr('readonly', null) : $this->unsetFieldAttr('readonly'); + } + + public function disabled(bool $disabled = true): static + { + return $disabled ? $this->setFieldAttr('disabled', null) : $this->unsetFieldAttr('disabled'); + } + + public function types(string $types): static + { + $this->uploadConfig()->types($types); + return $this; + } + + public function previewOnly(): static + { + $this->uploadConfig()->previewOnly(); + return $this; + } + + public function inputTrigger(): static + { + $this->uploadConfig()->inputTrigger(); + return $this; + } + + public function uploadConfig(): FormUploadConfig + { + $config = is_array($this->field['upload'] ?? null) ? $this->field['upload'] : []; + return (new FormUploadConfig($this, $config)) + ->attach(fn(array $state): array => $this->replaceUploadConfig($state)); + } + + public function label(?callable $callback = null): FormFieldPart + { + return $this->part('label', $callback); + } + + public function input(?callable $callback = null): FormFieldPart + { + return $this->part('input', $callback); + } + + public function control(?callable $callback = null): FormFieldPart + { + return $this->part('input', $callback); + } + + public function body(?callable $callback = null): FormFieldPart + { + return $this->part('body', $callback); + } + + public function remarkNode(?callable $callback = null): FormFieldPart + { + return $this->part('remark', $callback); + } + + public function field(array $field): self + { + return $this->builder->addFieldToNode($this->parent, $field); + } + + public function text(string $name, string $title, string $subtitle = '', bool $required = false, string $remark = '', ?string $pattern = null, array $attrs = []): self + { + return $this->builder->addFieldToNode($this->parent, [ + 'type' => $attrs['type'] ?? 'text', + 'name' => $name, + 'title' => $title, + 'subtitle' => $subtitle, + 'required' => $required, + 'remark' => $remark, + 'pattern' => $pattern, + 'attrs' => $attrs, + ]); + } + + public function textarea(string $name, string $title, string $subtitle = '', bool $required = false, string $remark = '', array $attrs = []): self + { + return $this->builder->addFieldToNode($this->parent, [ + 'type' => 'textarea', + 'name' => $name, + 'title' => $title, + 'subtitle' => $subtitle, + 'required' => $required, + 'remark' => $remark, + 'attrs' => $attrs, + ]); + } + + public function password(string $name, string $title, string $subtitle = '', bool $required = false, string $remark = '', ?string $pattern = null, array $attrs = []): self + { + $attrs['type'] = 'password'; + return $this->text($name, $title, $subtitle, $required, $remark, $pattern, $attrs); + } + + public function select(string $name, string $title, string $subtitle = '', bool $required = false, string $remark = '', array $options = [], string $variable = '', array $attrs = []): self + { + return $this->builder->addFieldToNode($this->parent, [ + 'type' => 'select', + 'name' => $name, + 'title' => $title, + 'subtitle' => $subtitle, + 'required' => $required, + 'remark' => $remark, + 'options' => $options, + 'vname' => $variable, + 'attrs' => $attrs, + ]); + } + + public function checkbox(string $name, string $title, string $subtitle, string $variable, bool $required = false, array $attrs = []): self + { + return $this->builder->addFieldToNode($this->parent, [ + 'type' => 'checkbox', + 'name' => $name, + 'title' => $title, + 'subtitle' => $subtitle, + 'required' => $required, + 'attrs' => $attrs, + 'vname' => $variable, + ]); + } + + public function radio(string $name, string $title, string $subtitle, string $variable, bool $required = false, array $attrs = []): self + { + return $this->builder->addFieldToNode($this->parent, [ + 'type' => 'radio', + 'name' => $name, + 'title' => $title, + 'subtitle' => $subtitle, + 'required' => $required, + 'attrs' => $attrs, + 'vname' => $variable, + ]); + } + + public function image(string $name, string $title, string $subtitle = '', bool $required = false, array $attrs = []): self + { + return $this->builder->addFieldToNode($this->parent, [ + 'type' => 'image', + 'name' => $name, + 'title' => $title, + 'subtitle' => $subtitle, + 'required' => $required, + 'attrs' => $attrs, + ]); + } + + public function video(string $name, string $title, string $subtitle = '', bool $required = false, array $attrs = []): self + { + return $this->builder->addFieldToNode($this->parent, [ + 'type' => 'video', + 'name' => $name, + 'title' => $title, + 'subtitle' => $subtitle, + 'required' => $required, + 'attrs' => $attrs, + ]); + } + + public function images(string $name, string $title, string $subtitle = '', bool $required = false, array $attrs = []): self + { + return $this->builder->addFieldToNode($this->parent, [ + 'type' => 'images', + 'name' => $name, + 'title' => $title, + 'subtitle' => $subtitle, + 'required' => $required, + 'attrs' => $attrs, + ]); + } + + /** + * 导出节点数组. + * @return array + */ + public function export(): array + { + $parts = []; + foreach ($this->parts as $name => $part) { + if ($part->configured()) { + $parts[$name] = $part->export(); + } + } + + return [ + 'type' => 'field', + 'attrs' => $this->buildAttrs(), + 'modules' => $this->modules, + 'parts' => $parts, + 'field' => $this->field, + ]; + } + + private function part(string $name, ?callable $callback = null): FormFieldPart + { + $name = trim($name); + if ($name === '') { + throw new \InvalidArgumentException('FormField 子节点名称不能为空'); + } + $part = $this->parts[$name] ?? new FormFieldPart($this, $name); + $this->parts[$name] = $part; + if (is_callable($callback)) { + $callback($part); + } + return $part; + } + + protected function setFieldAttr(string $name, mixed $value = null): static + { + $name = trim($name); + if ($name !== '') { + $attrs = is_array($this->field['attrs'] ?? null) ? $this->field['attrs'] : []; + $attrs[$name] = $value; + $this->field['attrs'] = $attrs; + } + return $this; + } + + protected function unsetFieldAttr(string $name): static + { + $name = trim($name); + if ($name !== '' && is_array($this->field['attrs'] ?? null) && array_key_exists($name, $this->field['attrs'])) { + unset($this->field['attrs'][$name]); + } + return $this; + } + + /** + * @param array $state + * @return array + */ + private function replaceOptionsState(array $state): array + { + $this->field['options'] = is_array($state['options'] ?? null) ? $state['options'] : []; + $this->field['vname'] = trim(strval($state['vname'] ?? '')); + return [ + 'options' => is_array($this->field['options'] ?? null) ? $this->field['options'] : [], + 'vname' => trim(strval($this->field['vname'] ?? '')), + ]; + } + + /** + * @param array $config + * @return array + */ + private function replaceUploadConfig(array $config): array + { + $this->field['upload'] = $this->normalizeUploadConfig($config); + return $this->field['upload']; + } + + /** + * @param array $config + * @return array + */ + private function normalizeUploadConfig(array $config): array + { + $config = array_merge([ + 'types' => '', + 'display' => '', + 'trigger' => [], + 'runtime' => [], + ], $config); + + $config['types'] = trim(strval($config['types'])); + $config['display'] = strtolower(trim(strval($config['display']))); + if (!in_array($config['display'], ['', 'input', 'preview'], true)) { + $config['display'] = ''; + } + $config['trigger'] = is_array($config['trigger']) ? $config['trigger'] : []; + $config['runtime'] = is_array($config['runtime']) ? $config['runtime'] : []; + $config['trigger']['attrs'] = is_array($config['trigger']['attrs'] ?? null) ? $config['trigger']['attrs'] : []; + if (isset($config['trigger']['class'])) { + $config['trigger']['class'] = trim(strval($config['trigger']['class'])); + } + if (isset($config['trigger']['icon'])) { + $config['trigger']['icon'] = trim(strval($config['trigger']['icon'])); + } + if (isset($config['runtime']['method'])) { + $config['runtime']['method'] = trim(strval($config['runtime']['method'])); + } + if (isset($config['runtime']['selector'])) { + $config['runtime']['selector'] = trim(strval($config['runtime']['selector'])); + } + + return $config; + } + + private function mergeEventHandler(string $origin, string $append): string + { + $origin = trim($origin); + $append = trim($append); + if ($origin === '') { + return $append; + } + if ($append === '') { + return $origin; + } + return rtrim($append, ';') . ';' . ltrim($origin, ';'); + } +} diff --git a/plugin/think-library/src/builder/form/FormFieldOptions.php b/plugin/think-library/src/builder/form/FormFieldOptions.php new file mode 100644 index 000000000..75fe1f3d4 --- /dev/null +++ b/plugin/think-library/src/builder/form/FormFieldOptions.php @@ -0,0 +1,22 @@ + $options + */ + public function __construct(FormField $owner, array $options = [], string $source = '') + { + parent::__construct('vname', $options, $source, $owner); + } +} diff --git a/plugin/think-library/src/builder/form/FormFieldPart.php b/plugin/think-library/src/builder/form/FormFieldPart.php new file mode 100644 index 000000000..89a143fc1 --- /dev/null +++ b/plugin/think-library/src/builder/form/FormFieldPart.php @@ -0,0 +1,185 @@ + + */ + private array $attrs = []; + + /** + * 模块配置. + * @var array> + */ + private array $modules = []; + + /** + * 覆盖内容. + */ + private string $content = ''; + + public function __construct(private FormField $owner, private string $name) + { + } + + public function attr(string $name, mixed $value = null): static + { + $name = trim($name); + if ($name !== '') { + $this->attrs[$name] = $value; + } + return $this; + } + + public function attrs(array $attrs): static + { + $this->attrs = BuilderAttributes::make($this->attrs)->merge($attrs)->all(); + return $this; + } + + public function class(string|array $class): static + { + $this->attrs = BuilderAttributes::make($this->attrs)->class($class)->all(); + return $this; + } + + public function data(string $name, mixed $value = null): static + { + $name = trim($name); + if ($name !== '') { + $this->attr('data-' . ltrim($name, '-'), $value); + } + return $this; + } + + public function id(string $id): static + { + return $this->attr('id', $id); + } + + public function attrsItem(): BuilderAttributeBag + { + return $this->attachAttributes($this->createAttributes()); + } + + public function module(string $name, array $config = []): static + { + $this->attachModule($this->createModule($name, $config)); + return $this; + } + + public function moduleItem(string $name, array $config = []): BuilderModule + { + return $this->attachModule($this->createModule($name, $config)); + } + + public function text(string $content): static + { + $this->content = $content; + return $this; + } + + public function html(string $content): static + { + $this->content = $content; + return $this; + } + + public function end(): FormField + { + return $this->owner; + } + + public function configured(): bool + { + return count($this->attrs) > 0 || count($this->modules) > 0 || $this->content !== ''; + } + + /** + * 导出节点数组. + * @return array + */ + public function export(): array + { + return [ + 'name' => $this->name, + 'attrs' => BuilderAttributes::make($this->attrs)->all(), + 'modules' => $this->modules, + 'content' => $this->content, + ]; + } + + protected function createModule(string $name, array $config = []): BuilderModule + { + return new BuilderModule($name, $config, $this); + } + + protected function createAttributes(): BuilderAttributeBag + { + return new BuilderAttributeBag($this, $this->attrs); + } + + protected function attachAttributes(BuilderAttributeBag $attributes): BuilderAttributeBag + { + return $attributes->attach(fn(array $state): array => $this->replaceAttributes($state)); + } + + /** + * @param array $state + * @return array + */ + protected function replaceAttributes(array $state): array + { + $this->attrs = is_array($state['attrs'] ?? null) ? BuilderAttributes::make($state['attrs'])->all() : []; + return ['attrs' => $this->attrs]; + } + + protected function attachModule(BuilderModule $module): BuilderModule + { + $normalized = $this->normalizeModule($module->export()); + if ($normalized['name'] === '') { + return $module; + } + $index = count($this->modules); + $this->modules[$index] = $normalized; + return $module->attach($index, $normalized, fn(int $index, array $module): array => $this->replaceModule($index, $module)); + } + + /** + * @param array $module + * @return array + */ + protected function replaceModule(int $index, array $module): array + { + $normalized = $this->normalizeModule($module); + if ($normalized['name'] !== '') { + $this->modules[$index] = $normalized; + } + return $this->modules[$index] ?? $normalized; + } + + /** + * @param array $module + * @return array + */ + protected function normalizeModule(array $module): array + { + return [ + 'name' => trim(strval($module['name'] ?? '')), + 'config' => is_array($module['config'] ?? null) ? $module['config'] : [], + ]; + } +} diff --git a/plugin/think-library/src/builder/form/FormFields.php b/plugin/think-library/src/builder/form/FormFields.php new file mode 100644 index 000000000..722d8962d --- /dev/null +++ b/plugin/think-library/src/builder/form/FormFields.php @@ -0,0 +1,131 @@ +builder->addFieldToNode($this->parent, $field); + } + + public function text(string $name, string $title, string $subtitle = '', bool $required = false, string $remark = '', ?string $pattern = null, array $attrs = []): FormField + { + return $this->builder->addFieldToNode($this->parent, [ + 'type' => $attrs['type'] ?? 'text', + 'name' => $name, + 'title' => $title, + 'subtitle' => $subtitle, + 'required' => $required, + 'remark' => $remark, + 'pattern' => $pattern, + 'attrs' => $attrs, + ]); + } + + public function textarea(string $name, string $title, string $subtitle = '', bool $required = false, string $remark = '', array $attrs = []): FormField + { + return $this->builder->addFieldToNode($this->parent, [ + 'type' => 'textarea', + 'name' => $name, + 'title' => $title, + 'subtitle' => $subtitle, + 'required' => $required, + 'remark' => $remark, + 'attrs' => $attrs, + ]); + } + + public function password(string $name, string $title, string $subtitle = '', bool $required = false, string $remark = '', ?string $pattern = null, array $attrs = []): FormField + { + $attrs['type'] = 'password'; + return $this->text($name, $title, $subtitle, $required, $remark, $pattern, $attrs); + } + + public function select(string $name, string $title, string $subtitle = '', bool $required = false, string $remark = '', array $options = [], string $variable = '', array $attrs = []): FormField + { + return $this->builder->addFieldToNode($this->parent, [ + 'type' => 'select', + 'name' => $name, + 'title' => $title, + 'subtitle' => $subtitle, + 'required' => $required, + 'remark' => $remark, + 'options' => $options, + 'vname' => $variable, + 'attrs' => $attrs, + ]); + } + + public function checkbox(string $name, string $title, string $subtitle, string $variable, bool $required = false, array $attrs = []): FormField + { + return $this->builder->addFieldToNode($this->parent, [ + 'type' => 'checkbox', + 'name' => $name, + 'title' => $title, + 'subtitle' => $subtitle, + 'required' => $required, + 'attrs' => $attrs, + 'vname' => $variable, + ]); + } + + public function radio(string $name, string $title, string $subtitle, string $variable, bool $required = false, array $attrs = []): FormField + { + return $this->builder->addFieldToNode($this->parent, [ + 'type' => 'radio', + 'name' => $name, + 'title' => $title, + 'subtitle' => $subtitle, + 'required' => $required, + 'attrs' => $attrs, + 'vname' => $variable, + ]); + } + + public function image(string $name, string $title, string $subtitle = '', bool $required = false, array $attrs = []): FormField + { + return $this->builder->addFieldToNode($this->parent, [ + 'type' => 'image', + 'name' => $name, + 'title' => $title, + 'subtitle' => $subtitle, + 'required' => $required, + 'attrs' => $attrs, + ]); + } + + public function video(string $name, string $title, string $subtitle = '', bool $required = false, array $attrs = []): FormField + { + return $this->builder->addFieldToNode($this->parent, [ + 'type' => 'video', + 'name' => $name, + 'title' => $title, + 'subtitle' => $subtitle, + 'required' => $required, + 'attrs' => $attrs, + ]); + } + + public function images(string $name, string $title, string $subtitle = '', bool $required = false, array $attrs = []): FormField + { + return $this->builder->addFieldToNode($this->parent, [ + 'type' => 'images', + 'name' => $name, + 'title' => $title, + 'subtitle' => $subtitle, + 'required' => $required, + 'attrs' => $attrs, + ]); + } +} diff --git a/plugin/think-library/src/builder/form/FormLayout.php b/plugin/think-library/src/builder/form/FormLayout.php new file mode 100644 index 000000000..bd324954f --- /dev/null +++ b/plugin/think-library/src/builder/form/FormLayout.php @@ -0,0 +1,174 @@ +builder->setAction($url); + return $this; + } + + public function title(string $title): self + { + $this->builder->setTitle($title); + return $this; + } + + public function headerButton(string $name, string $type = 'button', string $confirm = '', array $attrs = [], string $class = ''): self + { + $this->builder->addHeaderButton($name, $type, $confirm, $attrs, $class); + return $this; + } + + public function headerHtml(string $html, array $schema = []): self + { + $this->builder->addHeaderButtonHtml($html, $schema); + return $this; + } + + public function variable(string $name): self + { + $this->builder->setVariable($name); + return $this; + } + + public function attrs(array $attrs): static + { + $this->builder->setFormAttrs($attrs); + return $this; + } + + public function attr(string $name, mixed $value = null): static + { + $this->builder->setFormAttr($name, $value); + return $this; + } + + public function removeAttr(string $name): static + { + $this->builder->removeFormAttr($name); + return $this; + } + + public function attrsItem(): BuilderAttributeBag + { + return $this->builder->attachFormAttributes($this->builder->createFormAttributes()); + } + + public function bodyAttrs(array $attrs): static + { + $this->builder->setBodyAttrs($attrs); + return $this; + } + + public function bodyAttr(string $name, mixed $value = null): static + { + $this->builder->setBodyAttr($name, $value); + return $this; + } + + public function bodyAttrsItem(): BuilderAttributeBag + { + return $this->builder->attachBodyAttributes($this->builder->createBodyAttributes()); + } + + public function class(string|array $class): static + { + $this->builder->addFormClass($class); + return $this; + } + + public function removeClass(string|array $class): static + { + $this->builder->removeFormClass($class); + return $this; + } + + public function bodyClass(string|array $class): static + { + $this->builder->addBodyClass($class); + return $this; + } + + public function data(string $name, mixed $value = null): static + { + $this->builder->setFormData($name, $value); + return $this; + } + + public function removeData(string $name): static + { + $this->builder->removeFormData($name); + return $this; + } + + public function bodyData(string $name, mixed $value = null): static + { + $this->builder->setBodyData($name, $value); + return $this; + } + + public function module(string $name, array $config = []): static + { + $this->builder->attachFormModule($this->builder->createFormModule($name, $config)); + return $this; + } + + public function moduleItem(string $name, array $config = []): BuilderModule + { + return $this->builder->attachFormModule($this->builder->createFormModule($name, $config)); + } + + public function script(string $script): self + { + $this->builder->addScript($script); + return $this; + } + + public function rules(array $rules): self + { + $this->builder->addValidateRules($rules); + return $this; + } + + public function rule(string $name, string $rule, string $message): self + { + $this->builder->addValidateRule($name, $rule, $message); + return $this; + } + + /** + * @param callable(FormFields): void $callback + */ + public function fields(callable $callback): static + { + parent::fields($callback); + return $this; + } + + /** + * @param callable(FormActions): void $callback + */ + public function actions(callable $callback): static + { + parent::actions($callback); + return $this; + } +} diff --git a/plugin/think-library/src/builder/form/FormNode.php b/plugin/think-library/src/builder/form/FormNode.php new file mode 100644 index 000000000..03b06491a --- /dev/null +++ b/plugin/think-library/src/builder/form/FormNode.php @@ -0,0 +1,201 @@ +builder, $type, $tag); + } + + public function html(string $html): static + { + $child = $this->createNodeInstance('html'); + $child->html = $html; + $this->appendChild($child); + return $this; + } + + public function textNode(string $text): static + { + return $this->html(BuilderAttributes::escape($text)); + } + + /** + * @param (callable(FormNode): void)|null $callback + */ + public function node(string $tag = 'div', ?callable $callback = null): self + { + $child = $this->createNodeInstance('element', trim($tag) ?: 'div'); + $this->appendChild($child); + if (is_callable($callback)) { + $callback($child); + } + return $child; + } + + /** + * @param (callable(FormNode): void)|null $callback + */ + public function prepend(string $tag = 'div', ?callable $callback = null): self + { + $child = $this->createNodeInstance('element', trim($tag) ?: 'div'); + $this->prependNode($child); + if (is_callable($callback)) { + $callback($child); + } + return $child; + } + + /** + * @param (callable(FormNode): void)|null $callback + */ + public function before(string $tag = 'div', ?callable $callback = null): self + { + $child = $this->createNodeInstance('element', trim($tag) ?: 'div'); + $this->beforeNode($child); + if (is_callable($callback)) { + $callback($child); + } + return $child; + } + + /** + * @param (callable(FormNode): void)|null $callback + */ + public function after(string $tag = 'div', ?callable $callback = null): self + { + $child = $this->createNodeInstance('element', trim($tag) ?: 'div'); + $this->afterNode($child); + if (is_callable($callback)) { + $callback($child); + } + return $child; + } + + /** + * @param null|callable(FormNode): void $callback + */ + public function component(FormComponentInterface $component, ?callable $callback = null): self + { + $node = $component->mount($this); + if (is_callable($callback)) { + $callback($node); + } + return $node; + } + + public function div(?callable $callback = null): self + { + return $this->node('div', $callback); + } + + public function section(?callable $callback = null): self + { + return $this->node('section', $callback); + } + + public function article(?callable $callback = null): self + { + return $this->node('article', $callback); + } + + public function header(?callable $callback = null): self + { + return $this->node('header', $callback); + } + + public function footer(?callable $callback = null): self + { + return $this->node('footer', $callback); + } + + public function fieldset(?callable $callback = null): self + { + return $this->node('fieldset', $callback); + } + + public function fieldsNode(): FormFields + { + return new FormFields($this->builder, $this); + } + + /** + * @param callable(FormFields): void $callback + */ + public function fields(callable $callback): static + { + $callback($this->fieldsNode()); + return $this; + } + + public function actionsNode(): FormActions + { + return new FormActions($this->builder, $this->actionBar()); + } + + /** + * @param (callable(FormActions): void)|null $callback + */ + public function actionBar(?callable $callback = null): FormActionBar + { + $node = $this->actionBar ?? new FormActionBar($this->builder); + if ($this->actionBar === null) { + $this->appendChild($node); + $this->actionBar = $node; + } + if (is_callable($callback)) { + $callback(new FormActions($this->builder, $node)); + } + return $node; + } + + /** + * @param callable(FormActions): void $callback + */ + public function actions(callable $callback): static + { + $callback($this->actionsNode()); + return $this; + } + + public function append(FormNode $node): FormNode + { + return $this->appendChild($node); + } + + protected function onChildDetached(object $child): void + { + if ($child === $this->actionBar) { + $this->actionBar = null; + } + } + + /** + * 导出节点数组. + * @return array + */ + public function export(): array + { + if ($this->type === 'html') { + return $this->exportHtmlNode(); + } + return $this->exportElementNode(); + } +} diff --git a/plugin/think-library/src/builder/form/FormSelectField.php b/plugin/think-library/src/builder/form/FormSelectField.php new file mode 100644 index 000000000..79b42fba2 --- /dev/null +++ b/plugin/think-library/src/builder/form/FormSelectField.php @@ -0,0 +1,22 @@ +optionsItem()->source($name)->end(); + } + + public function search(bool $enabled = true): static + { + return $enabled ? $this->setFieldAttr('lay-search', null) : $this->unsetFieldAttr('lay-search'); + } +} diff --git a/plugin/think-library/src/builder/form/FormTextField.php b/plugin/think-library/src/builder/form/FormTextField.php new file mode 100644 index 000000000..2e91287cf --- /dev/null +++ b/plugin/think-library/src/builder/form/FormTextField.php @@ -0,0 +1,60 @@ +setFieldAttr('maxlength', $length); + } + + public function minlength(int $length): static + { + return $this->setFieldAttr('minlength', $length); + } + + /** + * @param array $attrs + */ + public function inputRightIcon(string $iconClass, array $attrs = [], string|array $inputClass = 'pr40'): static + { + $iconClass = trim($iconClass); + if ($iconClass === '') { + return $this; + } + + $renderer = new BuilderAttributesRenderer(); + $attrs['onmousedown'] = $this->mergeEventHandler(strval($attrs['onmousedown'] ?? ''), 'event.preventDefault();event.stopPropagation();'); + $attrs['ontouchstart'] = $this->mergeEventHandler(strval($attrs['ontouchstart'] ?? ''), 'event.preventDefault();event.stopPropagation();'); + $attrs = BuilderAttributes::make($attrs) + ->class(trim("input-right-icon layui-icon {$iconClass}")) + ->all(); + + $this->body()->class('relative'); + $this->input()->class($inputClass)->html(sprintf('', $renderer->render($attrs))); + return $this; + } + + private function mergeEventHandler(string $origin, string $append): string + { + $origin = trim($origin); + $append = trim($append); + if ($origin === '') { + return $append; + } + if ($append === '') { + return $origin; + } + return rtrim($append, ';') . ';' . ltrim($origin, ';'); + } +} diff --git a/plugin/think-library/src/builder/form/FormUploadConfig.php b/plugin/think-library/src/builder/form/FormUploadConfig.php new file mode 100644 index 000000000..ba25b8ea2 --- /dev/null +++ b/plugin/think-library/src/builder/form/FormUploadConfig.php @@ -0,0 +1,238 @@ +): array + */ + private $syncHandler = null; + + /** + * @param array $config + */ + public function __construct(private FormField $owner, private array $config = []) + { + } + + /** + * @param callable(array): array $syncHandler + */ + public function attach(callable $syncHandler): self + { + $this->syncHandler = $syncHandler; + return $this; + } + + public function types(string $types): self + { + $types = trim($types); + if ($types !== '') { + $this->config['types'] = $types; + $this->sync(); + } + return $this; + } + + public function display(string $mode): self + { + $mode = strtolower(trim($mode)); + if (in_array($mode, ['input', 'preview'], true)) { + $this->config['display'] = $mode; + $this->sync(); + } + return $this; + } + + public function inputTrigger(): self + { + return $this->display('input'); + } + + public function previewOnly(): self + { + return $this->display('preview'); + } + + public function option(string $name, mixed $value): self + { + $name = trim($name); + if ($name !== '') { + $this->config[$name] = $value; + $this->sync(); + } + return $this; + } + + /** + * @param array $options + */ + public function options(array $options): self + { + foreach ($options as $name => $value) { + if (is_string($name) && trim($name) !== '') { + $this->config[trim($name)] = $value; + } + } + return $this->sync(); + } + + /** + * @param array $trigger + */ + public function trigger(array $trigger): self + { + $this->config['trigger'] = $this->mergeAssoc($this->resolveTrigger(), $trigger); + return $this->sync(); + } + + public function triggerClass(string|array $class): self + { + $trigger = $this->resolveTrigger(); + $trigger['class'] = BuilderAttributes::mergeClassNames(strval($trigger['class'] ?? ''), $class); + $this->config['trigger'] = $trigger; + return $this->sync(); + } + + public function triggerIcon(string $icon): self + { + $icon = trim($icon); + if ($icon !== '') { + $trigger = $this->resolveTrigger(); + $trigger['icon'] = $icon; + $this->config['trigger'] = $trigger; + $this->sync(); + } + return $this; + } + + public function triggerAttr(string $name, mixed $value = null): self + { + $name = trim($name); + if ($name !== '') { + $trigger = $this->resolveTrigger(); + $attrs = is_array($trigger['attrs'] ?? null) ? $trigger['attrs'] : []; + $attrs[$name] = $value; + $trigger['attrs'] = $attrs; + $this->config['trigger'] = $trigger; + $this->sync(); + } + return $this; + } + + /** + * @param array $attrs + */ + public function triggerAttrs(array $attrs): self + { + $trigger = $this->resolveTrigger(); + $trigger['attrs'] = $this->mergeAssoc(is_array($trigger['attrs'] ?? null) ? $trigger['attrs'] : [], $attrs); + $this->config['trigger'] = $trigger; + return $this->sync(); + } + + public function triggerFile(string|bool $file): self + { + $trigger = $this->resolveTrigger(); + $trigger['file'] = $file; + $this->config['trigger'] = $trigger; + return $this->sync(); + } + + /** + * @param array $runtime + */ + public function runtime(array $runtime): self + { + $this->config['runtime'] = $this->mergeAssoc($this->resolveRuntime(), $runtime); + return $this->sync(); + } + + public function runtimeMethod(string $method): self + { + $method = trim($method); + if ($method !== '') { + $runtime = $this->resolveRuntime(); + $runtime['method'] = $method; + $this->config['runtime'] = $runtime; + $this->sync(); + } + return $this; + } + + public function runtimeSelector(string $selector): self + { + $selector = trim($selector); + if ($selector !== '') { + $runtime = $this->resolveRuntime(); + $runtime['selector'] = $selector; + $this->config['runtime'] = $runtime; + $this->sync(); + } + return $this; + } + + /** + * @return array + */ + public function export(): array + { + return $this->config; + } + + public function end(): FormField + { + return $this->owner; + } + + private function sync(): self + { + if (is_callable($this->syncHandler)) { + $state = ($this->syncHandler)($this->config); + $this->config = is_array($state) ? $state : $this->config; + } + return $this; + } + + /** + * @return array + */ + private function resolveTrigger(): array + { + return is_array($this->config['trigger'] ?? null) ? $this->config['trigger'] : []; + } + + /** + * @return array + */ + private function resolveRuntime(): array + { + return is_array($this->config['runtime'] ?? null) ? $this->config['runtime'] : []; + } + + /** + * @param array $origin + * @param array $append + * @return array + */ + private function mergeAssoc(array $origin, array $append): array + { + foreach ($append as $key => $value) { + if (is_string($key) && isset($origin[$key]) && is_array($origin[$key]) && is_array($value) && !array_is_list($origin[$key]) && !array_is_list($value)) { + $origin[$key] = $this->mergeAssoc($origin[$key], $value); + } elseif (is_string($key)) { + $origin[$key] = $value; + } + } + return $origin; + } +} diff --git a/plugin/think-library/src/builder/form/FormUploadField.php b/plugin/think-library/src/builder/form/FormUploadField.php new file mode 100644 index 000000000..10527499e --- /dev/null +++ b/plugin/think-library/src/builder/form/FormUploadField.php @@ -0,0 +1,84 @@ +uploadConfig()->types($types); + return $this; + } + + public function inputTrigger(): static + { + $this->uploadConfig()->inputTrigger(); + return $this; + } + + public function previewOnly(): static + { + $this->uploadConfig()->previewOnly(); + return $this; + } + + public function uploadConfig(): FormUploadConfig + { + $config = is_array($this->field['upload'] ?? null) ? $this->field['upload'] : []; + return (new FormUploadConfig($this, $config)) + ->attach(fn(array $config): array => $this->replaceUploadConfig($config)); + } + + /** + * @param array $config + * @return array + */ + private function replaceUploadConfig(array $config): array + { + $this->field['upload'] = $this->normalizeUploadConfig($config); + return $this->field['upload']; + } + + /** + * @param array $config + * @return array + */ + private function normalizeUploadConfig(array $config): array + { + $config = array_merge([ + 'types' => '', + 'display' => '', + 'trigger' => [], + 'runtime' => [], + ], $config); + + $config['types'] = trim(strval($config['types'])); + $config['display'] = strtolower(trim(strval($config['display']))); + if (!in_array($config['display'], ['', 'input', 'preview'], true)) { + $config['display'] = ''; + } + $config['trigger'] = is_array($config['trigger']) ? $config['trigger'] : []; + $config['runtime'] = is_array($config['runtime']) ? $config['runtime'] : []; + $config['trigger']['attrs'] = is_array($config['trigger']['attrs'] ?? null) ? $config['trigger']['attrs'] : []; + if (isset($config['trigger']['class'])) { + $config['trigger']['class'] = trim(strval($config['trigger']['class'])); + } + if (isset($config['trigger']['icon'])) { + $config['trigger']['icon'] = trim(strval($config['trigger']['icon'])); + } + if (isset($config['runtime']['method'])) { + $config['runtime']['method'] = trim(strval($config['runtime']['method'])); + } + if (isset($config['runtime']['selector'])) { + $config['runtime']['selector'] = trim(strval($config['runtime']['selector'])); + } + + return $config; + } +} diff --git a/plugin/think-library/src/builder/form/component/AbstractFormComponent.php b/plugin/think-library/src/builder/form/component/AbstractFormComponent.php new file mode 100644 index 000000000..7797416e4 --- /dev/null +++ b/plugin/think-library/src/builder/form/component/AbstractFormComponent.php @@ -0,0 +1,47 @@ + $config + * @param array $extra + * @return array + */ + protected function mergeConfig(array $config, array $extra): array + { + return array_merge($config, $extra); + } + + /** + * @param array $config + */ + protected function appendClass(array &$config, string $key, string|array $class): void + { + $config[$key] = BuilderAttributes::mergeClassNames(strval($config[$key] ?? ''), $class); + } + + protected function escape(string $content): string + { + return htmlentities(BuilderLang::text($content), ENT_QUOTES, 'UTF-8'); + } +} diff --git a/plugin/think-library/src/builder/form/component/FormComponentInterface.php b/plugin/think-library/src/builder/form/component/FormComponentInterface.php new file mode 100644 index 000000000..7ff0feb50 --- /dev/null +++ b/plugin/think-library/src/builder/form/component/FormComponentInterface.php @@ -0,0 +1,16 @@ + + */ + private array $config = []; + + /** + * @param array $config + */ + public function config(array $config): static + { + $this->config = $this->mergeConfig($this->config, $config); + return $this; + } + + public function title(string $title): static + { + $this->config['title'] = $title; + return $this; + } + + public function description(string $description): static + { + $this->config['description'] = $description; + return $this; + } + + public function class(string|array $class): static + { + $this->appendClass($this->config, 'class', $class); + return $this; + } + + public function mount(FormNode $parent): FormNode + { + $node = $parent->section()->class(trim(strval($this->config['class'] ?? 'layui-card mb15'))); + $body = $node->div()->class(trim(strval($this->config['body_class'] ?? 'layui-card-body'))); + $eyebrow = trim(strval($this->config['eyebrow'] ?? '')); + if ($eyebrow !== '') { + $body->div()->class(trim(strval($this->config['eyebrow_class'] ?? 'color-desc fs12')))->html($this->escape($eyebrow)); + } + $title = trim(strval($this->config['title'] ?? '')); + if ($title !== '') { + $body->node('h2')->class(trim(strval($this->config['title_class'] ?? 'mt10 mb10')))->html($this->escape($title)); + } + $description = trim(strval($this->config['description'] ?? '')); + if ($description !== '') { + $body->div()->class(trim(strval($this->config['description_class'] ?? 'color-desc lh24')))->html($this->escape($description)); + } + return $node; + } +} diff --git a/plugin/think-library/src/builder/form/component/NoteComponent.php b/plugin/think-library/src/builder/form/component/NoteComponent.php new file mode 100644 index 000000000..df3b6747e --- /dev/null +++ b/plugin/think-library/src/builder/form/component/NoteComponent.php @@ -0,0 +1,34 @@ +text = $text; + return $this; + } + + public function class(string $class): static + { + $this->class = trim($class) ?: $this->class; + return $this; + } + + public function mount(FormNode $parent): FormNode + { + return $parent->div()->class($this->class)->html($this->escape($this->text)); + } +} diff --git a/plugin/think-library/src/builder/form/component/PickerFieldComponent.php b/plugin/think-library/src/builder/form/component/PickerFieldComponent.php new file mode 100644 index 000000000..41d1881b7 --- /dev/null +++ b/plugin/think-library/src/builder/form/component/PickerFieldComponent.php @@ -0,0 +1,77 @@ + + */ + private array $config = []; + + /** + * @param array $config + */ + public function config(array $config): static + { + $this->config = $this->mergeConfig($this->config, $config); + return $this; + } + + public function mount(FormNode $parent): FormNode + { + $node = $parent->node('label')->class(trim(strval($this->config['class'] ?? 'relative block'))); + foreach ((array)($this->config['attrs'] ?? []) as $name => $value) { + $node->attr(strval($name), $value); + } + $label = $node->node('span')->class(trim(strval($this->config['label_class'] ?? 'help-label label-required-prev'))); + $title = trim(strval($this->config['title'] ?? '')); + if ($title !== '') { + $label->node('b')->html($this->escape($title)); + } + $subtitle = trim(strval($this->config['subtitle'] ?? '')); + if ($subtitle !== '') { + $label->html($this->escape($title === '' ? $subtitle : " {$subtitle}")); + } + + $hiddenName = trim(strval($this->config['hidden_name'] ?? '')); + if ($hiddenName !== '') { + $node->node('input')->attrs([ + 'type' => 'hidden', + 'name' => $hiddenName, + 'value' => strval($this->config['hidden_value'] ?? ''), + ]); + } + + $input = $node->node('input')->attrs([ + 'readonly' => null, + 'class' => trim(strval($this->config['input_class'] ?? 'layui-input')), + 'value' => strval($this->config['value'] ?? ''), + 'title' => strval($this->config['value'] ?? ''), + ]); + $inputName = trim(strval($this->config['name'] ?? '')); + if ($inputName !== '') { + $input->attr('name', $inputName); + } + foreach ((array)($this->config['input_attrs'] ?? []) as $name => $value) { + $input->attr(strval($name), $value); + } + $icon = $node->node('span')->class(trim(strval($this->config['icon_class'] ?? 'input-right-icon layui-icon layui-icon-theme'))); + foreach ((array)($this->config['icon_attrs'] ?? []) as $name => $value) { + $icon->attr(strval($name), $value); + } + $help = trim(strval($this->config['help'] ?? '')); + if ($help !== '') { + $node->div()->class(trim(strval($this->config['help_class'] ?? 'help-block color-desc')))->html($this->escape($help)); + } + return $node; + } +} diff --git a/plugin/think-library/src/builder/form/component/ReadonlyFieldComponent.php b/plugin/think-library/src/builder/form/component/ReadonlyFieldComponent.php new file mode 100644 index 000000000..f2a6c21b6 --- /dev/null +++ b/plugin/think-library/src/builder/form/component/ReadonlyFieldComponent.php @@ -0,0 +1,65 @@ + + */ + private array $config = []; + + /** + * @param array $config + */ + public function config(array $config): static + { + $this->config = $this->mergeConfig($this->config, $config); + return $this; + } + + public function mount(FormNode $parent): FormNode + { + $node = $parent->node('label')->class(trim(strval($this->config['class'] ?? 'relative block'))); + $label = $node->node('span')->class(trim(strval($this->config['label_class'] ?? 'help-label label-required-prev'))); + $title = trim(strval($this->config['title'] ?? '')); + if ($title !== '') { + $label->node('b')->html($this->escape($title)); + } + $subtitle = trim(strval($this->config['subtitle'] ?? '')); + if ($subtitle !== '') { + $label->html($this->escape($title === '' ? $subtitle : " {$subtitle}")); + } + $wrap = $node->node('label')->class(trim(strval($this->config['wrap_class'] ?? 'relative block'))); + $inputAttrs = [ + 'readonly' => null, + 'class' => trim(strval($this->config['input_class'] ?? 'layui-input layui-bg-gray')), + 'value' => strval($this->config['value'] ?? ''), + ]; + $inputName = trim(strval($this->config['name'] ?? '')); + if ($inputName !== '') { + $inputAttrs['name'] = $inputName; + } + $input = $wrap->node('input')->attrs($inputAttrs); + foreach ((array)($this->config['input_attrs'] ?? []) as $name => $value) { + $input->attr(strval($name), $value); + } + $copy = trim(strval($this->config['copy'] ?? '')); + if ($copy !== '') { + $wrap->node('a')->class(trim(strval($this->config['copy_class'] ?? 'layui-icon layui-icon-release input-right-icon')))->attr('data-copy', $copy); + } + $help = trim(strval($this->config['help'] ?? '')); + if ($help !== '') { + $node->div()->class(trim(strval($this->config['help_class'] ?? 'help-block color-desc')))->html($this->escape($help)); + } + return $node; + } +} diff --git a/plugin/think-library/src/builder/form/component/SectionComponent.php b/plugin/think-library/src/builder/form/component/SectionComponent.php new file mode 100644 index 000000000..b97c7b275 --- /dev/null +++ b/plugin/think-library/src/builder/form/component/SectionComponent.php @@ -0,0 +1,79 @@ + + */ + private array $config = []; + + /** + * @var null|callable(FormNode): void + */ + private $bodyCallback = null; + + /** + * @param array $config + */ + public function config(array $config): static + { + $this->config = $this->mergeConfig($this->config, $config); + return $this; + } + + public function title(string $title): static + { + $this->config['title'] = $title; + return $this; + } + + public function description(string $description): static + { + $this->config['description'] = $description; + return $this; + } + + public function class(string|array $class): static + { + $this->appendClass($this->config, 'class', $class); + return $this; + } + + /** + * @param callable(FormNode): void $callback + */ + public function body(callable $callback): static + { + $this->bodyCallback = $callback; + return $this; + } + + public function mount(FormNode $parent): FormNode + { + $node = $parent->section()->class(trim(strval($this->config['class'] ?? 'mb20'))); + $head = $node->div()->class(trim(strval($this->config['head_class'] ?? 'mb15'))); + $title = trim(strval($this->config['title'] ?? '')); + if ($title !== '') { + $head->node('h3')->class(trim(strval($this->config['title_class'] ?? 'mb5')))->html($this->escape($title)); + } + $description = trim(strval($this->config['description'] ?? '')); + if ($description !== '') { + $head->node('p')->class(trim(strval($this->config['description_class'] ?? 'color-desc lh24')))->html($this->escape($description)); + } + $body = $node->div()->class(trim(strval($this->config['body_class'] ?? ''))); + if (is_callable($this->bodyCallback)) { + ($this->bodyCallback)($body); + } + return $node; + } +} diff --git a/plugin/think-library/src/builder/form/component/ThemePaletteComponent.php b/plugin/think-library/src/builder/form/component/ThemePaletteComponent.php new file mode 100644 index 000000000..3be75b95b --- /dev/null +++ b/plugin/think-library/src/builder/form/component/ThemePaletteComponent.php @@ -0,0 +1,138 @@ +> + */ + private array $themes = []; + + private string $current = ''; + + /** + * @var array + */ + private array $config = []; + + /** + * @param array> $themes + */ + public function themes(array $themes): static + { + $this->themes = $themes; + return $this; + } + + public function current(string $current): static + { + $this->current = $current; + return $this; + } + + /** + * @param array $config + */ + public function config(array $config): static + { + $this->config = $this->mergeConfig($this->config, $config); + return $this; + } + + public function mount(FormNode $parent): FormNode + { + $node = $parent->div()->class(trim(strval($this->config['class'] ?? 'layui-form-item mb5 label-required-prev'))); + $label = $node->div()->class(trim(strval($this->config['label_class'] ?? 'help-label'))); + $title = trim(strval($this->config['title'] ?? '')); + if ($title !== '') { + $label->node('b')->html($this->escape($title)); + } + $subtitle = trim(strval($this->config['subtitle'] ?? '')); + if ($subtitle !== '') { + $label->node('span')->class(trim(strval($this->config['subtitle_class'] ?? 'color-desc')))->html($this->escape($subtitle)); + } + $description = trim(strval($this->config['description'] ?? '')); + if ($description !== '') { + $node->div()->class(trim(strval($this->config['description_class'] ?? 'help-block')))->html($this->escape($description)); + } + + $palette = $node->div()->class(trim(strval($this->config['palette_class'] ?? 'theme-palette'))); + $inputName = trim(strval($this->config['input_name'] ?? 'site_theme')); + foreach ($this->themes as $key => $theme) { + $labelText = trim(strval($theme['label'] ?? $key)); + $card = $palette->node('label')->class(trim('theme-palette-card' . ($this->current === $key ? ' active' : ''))); + $card->data('theme-card', $key) + ->data('theme-label', $labelText) + ->attr('title', trim($labelText . ' / ' . $key, ' /')); + + $input = $card->node('input')->attrs([ + 'name' => $inputName, + 'type' => 'radio', + 'value' => strval($key), + 'lay-ignore' => 'true', + 'class' => 'theme-palette-input', + ]); + if ($this->current === $key) { + $input->attr('checked', null); + } + + $preview = $card->node('span')->class(trim('theme-palette-preview ' . strval($theme['layout'] ?? 'default'))); + $preview->attr('style', $this->themePreviewStyle($theme)); + foreach ([ + 'theme-palette-preview-header', + 'theme-palette-preview-side', + 'theme-palette-preview-side-alt', + 'theme-palette-preview-card theme-palette-preview-hero', + 'theme-palette-preview-card theme-palette-preview-panel', + 'theme-palette-preview-card theme-palette-preview-panel-right', + 'theme-palette-preview-line theme-palette-preview-tone', + 'theme-palette-preview-line theme-palette-preview-copy-1', + 'theme-palette-preview-line theme-palette-preview-copy-2', + 'theme-palette-preview-line theme-palette-preview-copy-3', + 'theme-palette-preview-line theme-palette-preview-copy-4', + ] as $class) { + $preview->node('span')->class($class); + } + + $meta = $card->node('span')->class('theme-palette-meta'); + $meta->node('span')->class('theme-palette-title')->html($this->escape($labelText)); + $meta->node('span')->class('theme-palette-layout')->html($this->escape(strval($theme['layout_label'] ?? ''))); + $card->node('span')->class('theme-palette-check layui-icon layui-icon-ok'); + } + + $help = trim(strval($this->config['help'] ?? '')); + if ($help !== '') { + $node->node('p')->class(trim(strval($this->config['help_class'] ?? 'help-block')))->html($this->escape($help)); + } + return $node; + } + + /** + * @param array $theme + */ + private function themePreviewStyle(array $theme): string + { + $styles = []; + foreach ([ + 'theme-accent' => strval($theme['primary'] ?? ''), + 'theme-header' => strval($theme['header'] ?? ''), + 'theme-side' => strval($theme['side'] ?? ''), + 'theme-surface' => strval($theme['surface'] ?? ''), + 'theme-body' => strval($theme['body'] ?? ''), + ] as $name => $value) { + if ($value !== '') { + $styles[] = '--' . $name . ':' . $value; + } + } + return join(';', $styles); + } +} diff --git a/plugin/think-library/src/builder/form/module/FormModules.php b/plugin/think-library/src/builder/form/module/FormModules.php new file mode 100644 index 000000000..e3f147f28 --- /dev/null +++ b/plugin/think-library/src/builder/form/module/FormModules.php @@ -0,0 +1,87 @@ + $config + */ + public static function intro(FormNode $parent, array $config = []): FormNode + { + return FormComponents::intro()->config($config)->mount($parent); + } + + /** + * @param array $config + * @param callable(FormNode): void $callback + */ + public static function section(FormNode $parent, array $config, callable $callback): FormNode + { + return FormComponents::section()->config($config)->body($callback)->mount($parent); + } + + public static function note(FormNode $parent, string $text, string $class = 'help-block color-desc'): FormNode + { + return FormComponents::note($text)->class($class)->mount($parent); + } + + /** + * @param array $config + */ + public static function readonlyField(FormNode $parent, array $config = []): FormNode + { + return FormComponents::readonlyField()->config($config)->mount($parent); + } + + /** + * @param array $config + */ + public static function pickerField(FormNode $parent, array $config = []): FormNode + { + return FormComponents::pickerField()->config($config)->mount($parent); + } + + /** + * @param array> $themes + * @param array $config + */ + public static function themePalette(FormNode $parent, array $themes, string $current = '', array $config = []): FormNode + { + return FormComponents::themePalette($themes, $current)->config($config)->mount($parent); + } + + /** + * @param array $theme + */ + private static function themePreviewStyle(array $theme): string + { + $styles = []; + foreach ([ + 'theme-accent' => strval($theme['primary'] ?? ''), + 'theme-header' => strval($theme['header'] ?? ''), + 'theme-side' => strval($theme['side'] ?? ''), + 'theme-surface' => strval($theme['surface'] ?? ''), + 'theme-body' => strval($theme['body'] ?? ''), + ] as $name => $value) { + if ($value !== '') { + $styles[] = '--' . $name . ':' . $value; + } + } + return join(';', $styles); + } + + private static function escape(string $content): string + { + return htmlentities($content, ENT_QUOTES, 'UTF-8'); + } +} diff --git a/plugin/think-library/src/builder/form/render/AbstractFormFieldRenderer.php b/plugin/think-library/src/builder/form/render/AbstractFormFieldRenderer.php new file mode 100644 index 000000000..29d7a4e99 --- /dev/null +++ b/plugin/think-library/src/builder/form/render/AbstractFormFieldRenderer.php @@ -0,0 +1,36 @@ +openContainer($containerTag, $containerClass); + $html .= "\n\t\t\t" . $context->renderLabel(); + $html .= "\n\t\t\t" . $context->renderBody($control, $bodyClass, $alwaysWrapBody); + if ($remark = $context->renderRemark()) { + $html .= "\n\t\t\t" . $remark; + } + return "{$html}\n\t\t"; + } + + protected function appendInlineScript(string $html, string $script): string + { + return $html . (new InlineScriptRenderer())->render([$script]); + } +} diff --git a/plugin/think-library/src/builder/form/render/ChoiceFieldRenderer.php b/plugin/think-library/src/builder/form/render/ChoiceFieldRenderer.php new file mode 100644 index 000000000..aae091784 --- /dev/null +++ b/plugin/think-library/src/builder/form/render/ChoiceFieldRenderer.php @@ -0,0 +1,34 @@ +field(); + if (strval($field['vname'] ?? '') === '' && count((array)($field['options'] ?? [])) < 1) { + throw new \InvalidArgumentException('FormBuilder 复选或单选字段需要提供 vname 或 options'); + } + + $type = $context->type(); + $attrs = $context->resolveInputAttrs(); + $attrs['type'] = $type; + $attrs['lay-ignore'] = null; + $attrs['name'] = $field['name'] . ($type === 'checkbox' ? '[]' : ''); + return $this->renderFieldShell( + $context, + $context->renderChoiceOptions($attrs, $type) . "\n\t\t\t", + 'div', + 'layui-form-item', + 'layui-textarea help-checks layui-bg-gray', + true + ); + } +} diff --git a/plugin/think-library/src/builder/form/render/FormActionBarRenderer.php b/plugin/think-library/src/builder/form/render/FormActionBarRenderer.php new file mode 100644 index 000000000..714b28164 --- /dev/null +++ b/plugin/think-library/src/builder/form/render/FormActionBarRenderer.php @@ -0,0 +1,30 @@ + $node + */ + public function render(array $node, FormNodeRenderContext $context): string + { + $attrs = is_array($node['attrs'] ?? null) ? $node['attrs'] : []; + $children = is_array($node['children'] ?? null) ? $node['children'] : []; + + $html = '
    '; + if ($identity = $context->renderIdentityField()) { + $html .= "\n\t\t" . $identity; + } + $html .= "\n\t\t" . $this->wrapElement('div', $attrs, "\n\t\t\t" . $context->renderChildren($children) . "\n\t\t", $context); + return $html; + } +} diff --git a/plugin/think-library/src/builder/form/render/FormButtonNodeRenderer.php b/plugin/think-library/src/builder/form/render/FormButtonNodeRenderer.php new file mode 100644 index 000000000..1fcce7db0 --- /dev/null +++ b/plugin/think-library/src/builder/form/render/FormButtonNodeRenderer.php @@ -0,0 +1,19 @@ +renderHtml($node); + } +} diff --git a/plugin/think-library/src/builder/form/render/FormElementNodeRenderer.php b/plugin/think-library/src/builder/form/render/FormElementNodeRenderer.php new file mode 100644 index 000000000..c6c1db5b9 --- /dev/null +++ b/plugin/think-library/src/builder/form/render/FormElementNodeRenderer.php @@ -0,0 +1,19 @@ +renderElement($node, $context); + } +} diff --git a/plugin/think-library/src/builder/form/render/FormFieldNodeRenderer.php b/plugin/think-library/src/builder/form/render/FormFieldNodeRenderer.php new file mode 100644 index 000000000..763e60149 --- /dev/null +++ b/plugin/think-library/src/builder/form/render/FormFieldNodeRenderer.php @@ -0,0 +1,26 @@ +invoke([$context, 'renderField'], $field); + } +} diff --git a/plugin/think-library/src/builder/form/render/FormFieldRenderContext.php b/plugin/think-library/src/builder/form/render/FormFieldRenderContext.php new file mode 100644 index 000000000..f52b069d0 --- /dev/null +++ b/plugin/think-library/src/builder/form/render/FormFieldRenderContext.php @@ -0,0 +1,230 @@ + $field + */ + public function __construct(private array $field, private string $variable) + { + parent::__construct([new BuilderAttributesRenderer(), 'render']); + } + + /** + * 获取字段配置. + * @return array + */ + public function field(): array + { + return $this->field; + } + + public function type(): string + { + return strval($this->field['type'] ?? 'text'); + } + + public function valueExpression(?string $name = null): string + { + return sprintf('{%s.%s|default=%s}', $this->variable, $this->valuePath($name), $this->templateLiteral($this->defaultValue())); + } + + public function joinedValueExpression(string $separator = '|', ?string $name = null): string + { + $path = $this->valuePath($name); + $separator = addslashes($separator); + return sprintf('{:%s.%s ?? null ? (is_array(%s.%s) ? join(\'%s\', %s.%s) : %s.%s) : \'\'}', $this->variable, $path, $this->variable, $path, $separator, $this->variable, $path, $this->variable, $path); + } + + public function valuePath(?string $name = null): string + { + $name = $name === null ? strval($this->field['name'] ?? '') : $name; + $name = preg_replace('/\[\]$/', '', $name) ?? $name; + $name = str_replace(['][', '[', ']'], ['.', '.', ''], $name); + $name = preg_replace('/\.{2,}/', '.', $name) ?? $name; + return trim($name, '.'); + } + + public function variable(): string + { + return $this->variable; + } + + public function defaultValue(): mixed + { + if (array_key_exists('default', $this->field)) { + return $this->field['default']; + } + return $this->type() === 'checkbox' ? [] : ''; + } + + public function scalarDefaultLiteral(): string + { + return $this->templateLiteral($this->defaultValue()); + } + + /** + * @return array + */ + public function arrayDefaultValues(): array + { + $value = $this->defaultValue(); + if (is_array($value)) { + return array_values(array_map('strval', $value)); + } + $value = trim(strval($value)); + return $value === '' ? [] : array_values(array_map('strval', str2arr($value))); + } + + public function arrayDefaultLiteral(): string + { + $items = array_map(static fn(string $item): string => var_export($item, true), $this->arrayDefaultValues()); + return '[' . join(', ', $items) . ']'; + } + + public function openContainer(string $tag, string $class): string + { + $attrs = $this->buildFieldContainerAttrs($class); + return sprintf('<%s %s>', $tag, ltrim($this->attrs($attrs))); + } + + public function renderLabel(): string + { + $part = $this->resolveFieldPart('label'); + $attrs = BuilderAttributes::make($this->buildPartAttrs($part)) + ->class('help-label' . (!empty($this->field['required']) ? ' label-required-prev' : '')) + ->all(); + $content = $part['content'] !== '' ? $part['content'] : sprintf('%s%s', $this->field['title'], $this->field['subtitle']); + return sprintf('%s', $this->attrs($attrs), $content); + } + + public function renderBody(string $content, string $class = '', bool $alwaysWrap = false): string + { + $part = $this->resolveFieldPart('body'); + if (!$alwaysWrap && $part['content'] === '' && count($part['attrs']) < 1 && count($part['modules']) < 1 && $class === '') { + return $content; + } + if ($part['content'] !== '') { + return $part['content']; + } + $attrs = BuilderAttributes::make($this->buildPartAttrs($part))->class($class)->all(); + return sprintf('
    %s
    ', $this->attrs($attrs), $content); + } + + public function renderRemark(): string + { + $part = $this->resolveFieldPart('remark'); + $content = $part['content'] !== '' ? $part['content'] : strval($this->field['remark'] ?? ''); + if ($content === '') { + return ''; + } + $attrs = BuilderAttributes::make($this->buildPartAttrs($part))->class('help-block')->all(); + return sprintf('%s', $this->attrs($attrs), $content); + } + + public function renderInputContent(): string + { + return strval($this->resolveFieldPart('input')['content'] ?? ''); + } + + /** + * @return array + */ + public function resolveInputAttrs(string $class = ''): array + { + $attrs = is_array($this->field['attrs'] ?? null) ? $this->field['attrs'] : []; + return BuilderAttributes::make($attrs) + ->merge($this->buildPartAttrs($this->resolveFieldPart('input'))) + ->class($class) + ->all(); + } + + public function renderSelectOptions(): string + { + return $this->optionRenderer()->renderSelectOptions($this->field, $this); + } + + /** + * @param array $attrs + */ + public function renderChoiceOptions(array $attrs, ?string $type = null): string + { + $type = $type ?: $this->type(); + return $this->optionRenderer()->renderChoiceOptions($this->field, $this, $type, $attrs); + } + + /** + * @return array + */ + private function buildFieldContainerAttrs(string $class): array + { + $modules = is_array($this->field['container_modules'] ?? null) ? $this->field['container_modules'] : []; + return BuilderAttributes::make(is_array($this->field['container_attrs'] ?? null) ? $this->field['container_attrs'] : []) + ->class($class) + ->default('data-field-name', $this->field['name']) + ->modules($modules) + ->all(); + } + + /** + * @return array + */ + private function resolveFieldPart(string $name): array + { + $parts = is_array($this->field['parts'] ?? null) ? $this->field['parts'] : []; + $part = is_array($parts[$name] ?? null) ? $parts[$name] : []; + $part['attrs'] = is_array($part['attrs'] ?? null) ? $part['attrs'] : []; + $part['modules'] = is_array($part['modules'] ?? null) ? $part['modules'] : []; + $part['content'] = isset($part['content']) ? strval($part['content']) : ''; + return $part; + } + + /** + * @param array $part + * @param array $attrs + * @return array + */ + private function buildPartAttrs(array $part, array $attrs = []): array + { + $modules = is_array($part['modules'] ?? null) ? $part['modules'] : []; + return BuilderAttributes::make($attrs) + ->merge(is_array($part['attrs'] ?? null) ? $part['attrs'] : []) + ->modules($modules) + ->all(); + } + + private function optionRenderer(): FormOptionRenderer + { + return $this->optionRenderer ??= new FormOptionRenderer(); + } + + private function templateLiteral(mixed $value): string + { + if (is_bool($value)) { + return $value ? '\'1\'' : '\'0\''; + } + if (is_int($value) || is_float($value)) { + return var_export($value, true); + } + if (is_array($value)) { + return '\'\''; + } + + return '\'' . addslashes(strval($value)) . '\''; + } +} diff --git a/plugin/think-library/src/builder/form/render/FormFieldRendererFactory.php b/plugin/think-library/src/builder/form/render/FormFieldRendererFactory.php new file mode 100644 index 000000000..a27e14f05 --- /dev/null +++ b/plugin/think-library/src/builder/form/render/FormFieldRendererFactory.php @@ -0,0 +1,25 @@ + $field + */ + public function create(array $field): FormFieldRendererInterface + { + return match ($field['type'] ?? 'text') { + 'select' => new SelectFieldRenderer(), + 'checkbox', 'radio' => new ChoiceFieldRenderer(), + 'image', 'video', 'images' => new UploadFieldRenderer(), + default => new TextFieldRenderer(), + }; + } +} diff --git a/plugin/think-library/src/builder/form/render/FormFieldRendererInterface.php b/plugin/think-library/src/builder/form/render/FormFieldRendererInterface.php new file mode 100644 index 000000000..742af5782 --- /dev/null +++ b/plugin/think-library/src/builder/form/render/FormFieldRendererInterface.php @@ -0,0 +1,14 @@ +renderHtml($node); + } +} diff --git a/plugin/think-library/src/builder/form/render/FormNodeRenderContext.php b/plugin/think-library/src/builder/form/render/FormNodeRenderContext.php new file mode 100644 index 000000000..5ee0049ad --- /dev/null +++ b/plugin/think-library/src/builder/form/render/FormNodeRenderContext.php @@ -0,0 +1,57 @@ +>): string $contentRenderer + * @param callable(array): string $attrsRenderer + * @param callable(array): string $fieldRenderer + */ + public function __construct( + private string $variable, + callable $contentRenderer, + callable $attrsRenderer, + private $fieldRenderer, + ) { + parent::__construct($contentRenderer, $attrsRenderer); + } + + public function variable(): string + { + return $this->variable; + } + + public function variableName(): string + { + return ltrim($this->variable, '$'); + } + + /** + * @param array $field + */ + public function renderField(array $field): string + { + return ($this->fieldRenderer)($field); + } + + public function renderIdentityField(): string + { + if ($this->identityRendered) { + return ''; + } + $this->identityRendered = true; + return sprintf('{notempty name="%s.id"}{/notempty}', $this->variableName(), $this->variable); + } +} diff --git a/plugin/think-library/src/builder/form/render/FormNodeRendererFactory.php b/plugin/think-library/src/builder/form/render/FormNodeRendererFactory.php new file mode 100644 index 000000000..c30055600 --- /dev/null +++ b/plugin/think-library/src/builder/form/render/FormNodeRendererFactory.php @@ -0,0 +1,39 @@ + $node + */ + public function create(array $node): FormNodeRendererInterface + { + $class = $this->resolveRendererClass($node); + return new $class(); + } + + protected function rendererMap(): array + { + return [ + 'html' => FormHtmlNodeRenderer::class, + 'field' => FormFieldNodeRenderer::class, + 'button' => FormButtonNodeRenderer::class, + 'actions' => FormActionBarRenderer::class, + 'element' => FormElementNodeRenderer::class, + ]; + } + + protected function fallbackRendererClass(): string + { + return FormHtmlNodeRenderer::class; + } +} diff --git a/plugin/think-library/src/builder/form/render/FormNodeRendererInterface.php b/plugin/think-library/src/builder/form/render/FormNodeRendererInterface.php new file mode 100644 index 000000000..01a73a5f7 --- /dev/null +++ b/plugin/think-library/src/builder/form/render/FormNodeRendererInterface.php @@ -0,0 +1,17 @@ + $node + */ + public function render(array $node, FormNodeRenderContext $context): string; +} diff --git a/plugin/think-library/src/builder/form/render/FormOptionRenderer.php b/plugin/think-library/src/builder/form/render/FormOptionRenderer.php new file mode 100644 index 000000000..b8857176c --- /dev/null +++ b/plugin/think-library/src/builder/form/render/FormOptionRenderer.php @@ -0,0 +1,181 @@ + $field + */ + public function renderSelectOptions(array $field, FormFieldRenderContext $context): string + { + $variable = $context->variable(); + $valuePath = $context->valuePath(); + $default = $context->scalarDefaultLiteral(); + if (strval($field['vname'] ?? '') !== '') { + $html = sprintf('{foreach $%s as $k=>$v}', $field['vname']); + $html .= sprintf( + '{if (isset(%s.%s) and strval(%s.%s) eq strval($k)) or (!isset(%s.%s) and strval($k) eq strval(%s))}{else}{/if}', + $variable, + $valuePath, + $variable, + $valuePath, + $variable, + $valuePath, + $default + ); + $html .= '{/foreach}'; + return $html; + } + + $html = ''; + foreach ((array)($field['options'] ?? []) as $value => $label) { + $value = $context->escape((string)$value); + $label = $context->escape((string)$label); + $html .= sprintf( + '{if (isset(%s.%s) and strval(%s.%s) eq \'%s\') or (!isset(%s.%s) and strval(%s) eq \'%s\')}{else}{/if}', + $variable, + $valuePath, + $variable, + $valuePath, + addslashes($value), + $variable, + $valuePath, + $default, + $value, + $value, + $label, + $value, + $label + ); + } + return $html; + } + + /** + * @param array $field + * @param array $attrs + */ + public function renderChoiceOptions(array $field, FormFieldRenderContext $context, string $type, array $attrs): string + { + if (strval($field['vname'] ?? '') !== '') { + return $this->renderDynamicChoiceOptions($field, $context, $type, $attrs); + } + + $html = ''; + foreach ((array)$field['options'] as $value => $label) { + $html .= "\n\t\t\t\t" . sprintf(''; + } + return $html; + } + + /** + * @param array $field + * @param array $attrs + */ + private function renderDynamicChoiceOptions(array $field, FormFieldRenderContext $context, string $type, array $attrs): string + { + $html = "\n\t\t\t\t" . sprintf('', $field['vname']); + $html .= "\n\t\t\t\t" . sprintf(''; + $html .= "\n\t\t\t\t" . ''; + return $html; + } + + /** + */ + private function renderChoiceCondition(FormFieldRenderContext $context, string $type): string + { + $valuePath = $context->valuePath(); + $variable = $context->variable(); + if ($type === 'checkbox') { + $defaults = $context->arrayDefaultLiteral(); + return sprintf( + '', + $variable, + $valuePath, + $variable, + $valuePath, + $variable, + $valuePath, + $variable, + $valuePath, + $defaults + ); + } + return sprintf( + '', + $variable, + $valuePath, + $variable, + $valuePath, + $variable, + $valuePath, + $context->scalarDefaultLiteral() + ); + } + + private function renderStaticChoiceCondition(FormFieldRenderContext $context, string $value, string $type): string + { + $valuePath = $context->valuePath(); + $variable = $context->variable(); + $value = addslashes($value); + if ($type === 'checkbox') { + $defaults = $context->arrayDefaultLiteral(); + return sprintf( + '', + $variable, + $valuePath, + $variable, + $valuePath, + $value, + $variable, + $valuePath, + $variable, + $valuePath, + $value, + $defaults + ); + } + return sprintf( + '', + $variable, + $valuePath, + $value, + $variable, + $valuePath, + $variable, + $valuePath, + $value, + $context->scalarDefaultLiteral() + ); + } + +} diff --git a/plugin/think-library/src/builder/form/render/FormRenderPipeline.php b/plugin/think-library/src/builder/form/render/FormRenderPipeline.php new file mode 100644 index 000000000..fc8b934a8 --- /dev/null +++ b/plugin/think-library/src/builder/form/render/FormRenderPipeline.php @@ -0,0 +1,61 @@ + $attrs + * @param array> $content + * @param array $fields + * @param array $headerButtons + * @param array $buttons + * @param array $scripts + */ + public function renderShell( + array $attrs, + array $bodyAttrs, + array $content, + array $fields, + array $headerButtons, + array $buttons, + FormRenderState $state, + array $scripts + ): string { + return (new FormShellRenderer())->render( + $attrs, + $bodyAttrs, + $content, + $fields, + $headerButtons, + $buttons, + $state->schema(), + $scripts, + $state->nodeRenderContext() + ); + } + + /** + * @param array $field + */ + public function renderField(array $field, string $variable): string + { + return (new FormFieldRendererFactory())->create($field)->render(new FormFieldRenderContext($field, $variable)); + } + + /** + * @param array> $nodes + */ + public function renderContentNodes(array $nodes, FormRenderState $state): string + { + return $this->renderNodeContent($nodes, $state, "\n\t\t"); + } +} diff --git a/plugin/think-library/src/builder/form/render/FormRenderState.php b/plugin/think-library/src/builder/form/render/FormRenderState.php new file mode 100644 index 000000000..7bf8b12bb --- /dev/null +++ b/plugin/think-library/src/builder/form/render/FormRenderState.php @@ -0,0 +1,40 @@ + $schema + */ + public function __construct( + array $schema, + FormNodeRendererFactory $nodeRendererFactory, + FormNodeRenderContext $nodeRenderContext, + ) { + parent::__construct($schema, $nodeRendererFactory, $nodeRenderContext); + $this->formNodeRendererFactory = $nodeRendererFactory; + $this->formNodeRenderContext = $nodeRenderContext; + } + + public function nodeRendererFactory(): FormNodeRendererFactory + { + return $this->formNodeRendererFactory; + } + + public function nodeRenderContext(): FormNodeRenderContext + { + return $this->formNodeRenderContext; + } +} diff --git a/plugin/think-library/src/builder/form/render/FormShellRenderer.php b/plugin/think-library/src/builder/form/render/FormShellRenderer.php new file mode 100644 index 000000000..cbe7162ad --- /dev/null +++ b/plugin/think-library/src/builder/form/render/FormShellRenderer.php @@ -0,0 +1,119 @@ + $attrs + * @param array> $content + * @param array $fields + * @param array $headerButtons + * @param array $buttons + */ + public function render( + array $attrs, + array $bodyAttrs, + array $content, + array $fields, + array $headerButtons, + array $buttons, + array $schema, + array $scripts, + FormNodeRenderContext $context + ): string { + $isPage = strval($schema['mode'] ?? '') === 'page'; + if ($isPage) { + return $this->renderPageShell($attrs, $bodyAttrs, $content, $fields, $headerButtons, $buttons, $schema, $scripts, $context); + } + + $html = sprintf('
    ', $context->attrs($attrs)); + $html .= "\n\t" . sprintf('
    ', $context->attrs($bodyAttrs)); + + if (count($content) > 0) { + $html .= $context->renderChildren($content); + } else { + $html .= join("\n", $fields); + if (count($buttons) > 0) { + $html .= "\n\t\t"; + $html .= (new FormActionBarRenderer())->render([ + 'type' => 'actions', + 'attrs' => ['class' => 'layui-form-item text-center'], + 'children' => array_map(static fn(string $button): array => ['type' => 'button', 'html' => $button], $buttons), + ], $context); + } + } + + if ($schemaScript = (new JsonScriptRenderer())->render($schema, 'form-builder-schema')) { + $html .= "\n\t\t" . $schemaScript; + } + $html .= "\n\t" . '
    '; + + return $html . "\n
    " . (new InlineScriptRenderer())->render($scripts); + } + + private function renderPageShell( + array $attrs, + array $bodyAttrs, + array $content, + array $fields, + array $headerButtons, + array $buttons, + array $schema, + array $scripts, + FormNodeRenderContext $context + ): string { + $header = (new PageHeaderRenderer())->render(strval($schema['title'] ?? ''), $headerButtons); + $form = sprintf('
    ', $context->attrs($attrs)); + $form .= "\n\t\t\t\t" . sprintf('
    ', $context->attrs($bodyAttrs)); + + if (count($content) > 0) { + $form .= "\n\t\t\t\t\t" . $context->renderChildren($content); + } else { + $form .= "\n\t\t\t\t\t" . join("\n", $fields); + if (count($buttons) > 0) { + $form .= "\n\t\t\t\t\t"; + $form .= (new FormActionBarRenderer())->render([ + 'type' => 'actions', + 'attrs' => ['class' => 'layui-form-item text-center'], + 'children' => array_map(static fn(string $button): array => ['type' => 'button', 'html' => $button], $buttons), + ], $context); + } + } + + if ($schemaScript = (new JsonScriptRenderer())->render($schema, 'form-builder-schema')) { + $form .= "\n\t\t\t\t\t" . $schemaScript; + } + + $form .= "\n\t\t\t\t" . '
    '; + $form .= "\n\t\t\t" . '
    '; + $form .= (new InlineScriptRenderer())->render($scripts); + + $html = sprintf( + '
    ', + htmlentities(strval($schema['preset'] ?? 'page-form'), ENT_QUOTES, 'UTF-8') + ); + if ($header !== '') { + $html .= "\n\t" . $header; + $html .= "\n\t" . '
    '; + } + $html .= "\n\t" . '
    '; + $html .= "\n\t\t" . '
    '; + $html .= "\n\t\t\t" . '
    '; + $html .= "\n\t\t\t\t" . $form; + $html .= "\n\t\t\t" . '
    '; + $html .= "\n\t\t" . '
    '; + $html .= "\n\t" . '
    '; + return $html . "\n
    "; + } +} diff --git a/plugin/think-library/src/builder/form/render/FormUploadRuntimeRenderer.php b/plugin/think-library/src/builder/form/render/FormUploadRuntimeRenderer.php new file mode 100644 index 000000000..32c4621a0 --- /dev/null +++ b/plugin/think-library/src/builder/form/render/FormUploadRuntimeRenderer.php @@ -0,0 +1,104 @@ + $field + */ + public function resolveDisplayMode(array $field, string $type): string + { + if ($type !== 'image') { + return 'input'; + } + $mode = strtolower(trim(strval($field['upload']['display'] ?? ''))); + return in_array($mode, ['input', 'preview'], true) ? $mode : 'input'; + } + + /** + * @param array $field + */ + public function resolveUploadTypes(array $field, string $type): string + { + return strval($field['upload']['types'] ?? ($type === 'image' ? 'gif,png,jpg,jpeg' : 'mp4')); + } + + public function renderTrigger(FormFieldRenderContext $context, string $fieldName, string $type, string $uploadTypes): string + { + $field = $context->field(); + $trigger = is_array($field['upload']['trigger'] ?? null) ? $field['upload']['trigger'] : []; + $attrs = is_array($trigger['attrs'] ?? null) ? $trigger['attrs'] : []; + $icon = trim(strval($trigger['icon'] ?? 'layui-icon-upload')); + $class = BuilderAttributes::mergeClassNames("layui-icon {$icon} input-right-icon", $trigger['class'] ?? ''); + $attrs = BuilderAttributes::make($attrs) + ->merge([ + 'data-field' => $fieldName, + 'data-type' => $uploadTypes, + ]) + ->class($class) + ->all(); + + if (array_key_exists('file', $trigger)) { + $attrs = $this->applyFileAttr($attrs, $trigger['file']); + } elseif ($type === 'image') { + $attrs['data-file'] = 'image'; + } else { + $attrs['data-file'] = null; + } + + return sprintf('', $context->attrs($attrs)); + } + + /** + * @param array $field + */ + public function renderInitScript(array $field, string $type): string + { + $runtime = is_array($field['upload']['runtime'] ?? null) ? $field['upload']['runtime'] : []; + $display = $this->resolveDisplayMode($field, $type); + $name = strval($field['name'] ?? ''); + $selector = trim(strval($runtime['selector'] ?? sprintf('input[name="%s"]', $name))); + $method = trim(strval($runtime['method'] ?? $this->runtimeMethod($type, $display))); + if ($selector === '' || $method === '') { + return ''; + } + return sprintf('$(%s).%s()', json_encode($selector, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?: '""', $method); + } + + private function runtimeMethod(string $type, string $display): string + { + return match ($type) { + 'image' => $display === 'preview' ? 'uploadOneImage' : '', + 'video' => 'uploadOneVideo', + 'images' => 'uploadMultipleImage', + default => throw new \InvalidArgumentException("FormBuilder 上传字段类型无效: {$type}"), + }; + } + + /** + * @param array $attrs + * @return array + */ + private function applyFileAttr(array $attrs, mixed $file): array + { + if ($file === false) { + unset($attrs['data-file']); + return $attrs; + } + if ($file === true || $file === null || $file === '') { + $attrs['data-file'] = null; + return $attrs; + } + $attrs['data-file'] = strval($file); + return $attrs; + } +} diff --git a/plugin/think-library/src/builder/form/render/SelectFieldRenderer.php b/plugin/think-library/src/builder/form/render/SelectFieldRenderer.php new file mode 100644 index 000000000..021529b92 --- /dev/null +++ b/plugin/think-library/src/builder/form/render/SelectFieldRenderer.php @@ -0,0 +1,27 @@ +field(); + $attrs = $context->resolveInputAttrs('layui-select'); + $attrs['name'] = $field['name']; + + $control = sprintf(''; + return $this->renderFieldShell($context, $control); + } +} diff --git a/plugin/think-library/src/builder/form/render/TextFieldRenderer.php b/plugin/think-library/src/builder/form/render/TextFieldRenderer.php new file mode 100644 index 000000000..9a201f9ab --- /dev/null +++ b/plugin/think-library/src/builder/form/render/TextFieldRenderer.php @@ -0,0 +1,39 @@ +field(); + if ($context->type() === 'textarea') { + $attrs = $context->resolveInputAttrs('layui-textarea'); + $attrs['placeholder'] = $attrs['placeholder'] ?? BuilderLang::format('请输入%s', [strval($field['title'])]); + return $this->renderFieldShell( + $context, + sprintf('', $field['name'], $context->attrs($attrs), $context->valueExpression()) + ); + } + + $attrs = $context->resolveInputAttrs('layui-input'); + if ($context->type() !== 'text' && !isset($attrs['type'])) { + $attrs['type'] = $context->type(); + } + $attrs['placeholder'] = $attrs['placeholder'] ?? BuilderLang::format('请输入%s', [strval($field['title'])]); + $control = sprintf('', $field['name'], $context->attrs($attrs), $context->valueExpression()); + $addon = $context->renderInputContent(); + if ($addon !== '') { + $control .= $addon; + } + return $this->renderFieldShell($context, $control, 'label', 'layui-form-item block relative', $addon !== '' ? 'relative' : '', $addon !== ''); + } +} diff --git a/plugin/think-library/src/builder/form/render/UploadFieldRenderer.php b/plugin/think-library/src/builder/form/render/UploadFieldRenderer.php new file mode 100644 index 000000000..3322bf190 --- /dev/null +++ b/plugin/think-library/src/builder/form/render/UploadFieldRenderer.php @@ -0,0 +1,77 @@ +type() === 'images' ? $this->renderMultiple($context) : $this->renderSingle($context); + } + + private function renderSingle(FormFieldRenderContext $context): string + { + $field = $context->field(); + $type = $context->type(); + $display = $this->runtimeRenderer()->resolveDisplayMode($field, $type); + $attrs = $context->resolveInputAttrs($display === 'preview' ? '' : 'layui-input layui-bg-gray'); + $attrs['type'] = $display === 'preview' ? 'hidden' : 'text'; + if ($display === 'preview') { + $attrs['data-upload-display'] = 'preview'; + } else { + $attrs['placeholder'] = $attrs['placeholder'] ?? BuilderLang::format('请上传%s', [strval($field['title'])]); + } + + $uploadTypes = $this->runtimeRenderer()->resolveUploadTypes($field, $type); + $body = "\n\t\t\t\t" . sprintf('', $field['name'], $context->attrs($attrs), $context->valueExpression()); + if ($display === 'input') { + $body .= "\n\t\t\t\t" . $this->runtimeRenderer()->renderTrigger($context, $field['name'], $type, $uploadTypes); + } + + $html = $this->renderFieldShell( + $context, + $body . "\n\t\t\t", + 'div', + 'layui-form-item', + 'relative block label-required-null', + true + ); + return $this->appendInlineScript($html, $this->runtimeRenderer()->renderInitScript($field, $type)); + } + + private function renderMultiple(FormFieldRenderContext $context): string + { + $field = $context->field(); + $attrs = $context->resolveInputAttrs(); + $attrs['type'] = 'hidden'; + $attrs['placeholder'] = $attrs['placeholder'] ?? BuilderLang::format('请上传%s ( 多图 )', [strval($field['title'])]); + + $body = "\n\t\t\t\t" . sprintf('', $field['name'], $context->attrs($attrs), $context->joinedValueExpression()) . "\n\t\t\t"; + return $this->appendInlineScript( + $this->renderFieldShell( + $context, + $body, + 'div', + 'layui-form-item', + 'layui-textarea help-images layui-bg-gray', + true + ), + $this->runtimeRenderer()->renderInitScript($field, 'images') + ); + } + + private function runtimeRenderer(): FormUploadRuntimeRenderer + { + return $this->runtimeRenderer ??= new FormUploadRuntimeRenderer(); + } +} diff --git a/plugin/think-library/src/builder/page/PageAction.php b/plugin/think-library/src/builder/page/PageAction.php new file mode 100644 index 000000000..02edb0f20 --- /dev/null +++ b/plugin/think-library/src/builder/page/PageAction.php @@ -0,0 +1,202 @@ + $action + */ + public function __construct(private PageBuilder $builder, private string $scope, private array $action = []) + { + } + + public function attach(int $index, ?int $version = null): self + { + $this->index = $index; + $this->version = $version; + $this->syncHandler = null; + return $this; + } + + public function attachSync(callable $syncHandler): self + { + $this->index = null; + $this->version = null; + $this->syncHandler = $syncHandler; + return $this; + } + + public function type(string $type): self + { + $this->action['type'] = trim($type); + return $this->sync(); + } + + public function label(string $label): self + { + $this->action['label'] = $label; + return $this->sync(); + } + + public function url(string $url): self + { + $this->action['url'] = $url; + return $this->sync(); + } + + public function title(string $title): self + { + $this->action['title'] = $title; + return $this->sync(); + } + + public function tag(string $tag): self + { + $this->action['tag'] = trim($tag); + return $this->sync(); + } + + public function value(string $value): self + { + $this->action['value'] = $value; + return $this->sync(); + } + + public function confirm(string $confirm): self + { + $this->action['confirm'] = $confirm; + return $this->sync(); + } + + public function rule(string $rule): self + { + $this->action['rule'] = $rule; + return $this->sync(); + } + + public function auth(?string $auth): self + { + $this->action['auth'] = $auth; + return $this->sync(); + } + + public function html(string $html): self + { + $this->action['type'] = 'html'; + $this->action['html'] = $html; + return $this->sync(); + } + + public function attrs(array $attrs): self + { + $merged = is_array($this->action['attrs'] ?? null) ? $this->action['attrs'] : []; + foreach ($attrs as $name => $value) { + if (is_string($name) && trim($name) !== '') { + $merged[trim($name)] = $value; + } + } + $this->action['attrs'] = $merged; + return $this->sync(); + } + + public function attr(string $name, mixed $value = null): self + { + $name = trim($name); + if ($name === '') { + return $this; + } + $attrs = is_array($this->action['attrs'] ?? null) ? $this->action['attrs'] : []; + $attrs[$name] = $value; + $this->action['attrs'] = $attrs; + return $this->sync(); + } + + public function attrsItem(): BuilderAttributeBag + { + $attrs = is_array($this->action['attrs'] ?? null) ? $this->action['attrs'] : []; + $class = trim(strval($this->action['class'] ?? '')); + return (new BuilderAttributeBag($this, $attrs, true, $class)) + ->attach(fn(array $state): array => $this->replaceAttributeState($state)); + } + + public function class(string|array $class): self + { + $this->action['class'] = BuilderAttributes::mergeClassNames(strval($this->action['class'] ?? ''), $class); + return $this->sync(); + } + + /** + * @return array + */ + public function export(): array + { + return $this->action; + } + + private function sync(): self + { + if (is_callable($this->syncHandler)) { + $this->action = ($this->syncHandler)($this->action); + return $this; + } + + if (!$this->canSync()) { + return $this; + } + + if ($this->scope === 'row') { + $this->builder->replaceRowAction($this->index, $this->action); + } else { + $this->builder->replaceButtonAction($this->index, $this->action); + } + + return $this; + } + + private function canSync(): bool + { + if ($this->index === null) { + return false; + } + return $this->scope !== 'row' || $this->builder->canSyncTableAttachment($this->version); + } + + /** + * @param array $state + * @return array + */ + private function replaceAttributeState(array $state): array + { + $this->action['attrs'] = is_array($state['attrs'] ?? null) ? BuilderAttributes::make($state['attrs'])->all() : []; + $this->action['class'] = trim(strval($state['class'] ?? $this->action['class'] ?? '')); + + if (is_callable($this->syncHandler)) { + $this->action = ($this->syncHandler)($this->action); + } elseif ($this->canSync()) { + if ($this->scope === 'row') { + $this->action = $this->builder->replaceRowAction($this->index, $this->action); + } else { + $this->action = $this->builder->replaceButtonAction($this->index, $this->action); + } + } + + return [ + 'attrs' => is_array($this->action['attrs'] ?? null) ? $this->action['attrs'] : [], + 'class' => trim(strval($this->action['class'] ?? '')), + ]; + } +} diff --git a/plugin/think-library/src/builder/page/PageActionNormalizer.php b/plugin/think-library/src/builder/page/PageActionNormalizer.php new file mode 100644 index 000000000..60099ff70 --- /dev/null +++ b/plugin/think-library/src/builder/page/PageActionNormalizer.php @@ -0,0 +1,170 @@ +renderer = $renderer ?? new BuilderActionRenderer(); + } + + /** + * @param array $action + * @return array + */ + public function button(array $action): array + { + $action = $this->normalizeBase($action); + if ($action['type'] === 'html') { + return $action; + } + + $attrs = match ($action['type']) { + 'modal' => $this->buildAttrs($action['attrs'], 'layui-btn layui-btn-sm layui-btn-primary', [ + 'data-modal' => $action['url'], + ], [ + 'data-title' => $action['title'], + ]), + 'button' => $this->buildAttrs($action['attrs'], 'layui-btn layui-btn-sm layui-btn-primary'), + 'open' => $this->buildAttrs($action['attrs'], 'layui-btn layui-btn-sm layui-btn-primary', [ + 'data-open' => $action['url'], + ]), + 'load' => $this->buildAttrs($action['attrs'], 'layui-btn layui-btn-sm layui-btn-primary', [ + 'data-load' => $action['url'], + 'data-table-id' => strval($action['attrs']['data-table-id'] ?? $this->tableId), + ]), + 'action' => $this->buildAttrs($action['attrs'], 'layui-btn layui-btn-sm layui-btn-primary', [ + 'data-action' => $action['url'], + ], [ + 'data-value' => $action['value'], + 'data-confirm' => $action['confirm'], + ]), + 'batch-action' => $this->buildAttrs($action['attrs'], 'layui-btn layui-btn-sm layui-btn-primary', [ + 'data-table-id' => $this->tableId, + 'data-action' => $action['url'], + 'data-rule' => $action['rule'], + ], [ + 'data-confirm' => $action['confirm'], + ]), + default => throw new \InvalidArgumentException("PageBuilder 按钮类型无效: {$action['type']}"), + }; + + return array_merge($action, [ + 'attrs' => $attrs, + 'html' => $this->renderer->render(strval($action['label']), $attrs, strval($action['tag'] ?? 'a')), + ]); + } + + /** + * @param array $action + * @return array + */ + public function row(array $action): array + { + $action = $this->normalizeBase($action); + if ($action['type'] === 'html') { + return $action; + } + + $attrs = match ($action['type']) { + 'modal' => $this->buildAttrs($action['attrs'], 'layui-btn layui-btn-sm', [ + 'data-modal' => $action['url'], + ], [ + 'data-title' => $action['title'], + ]), + 'button' => $this->buildAttrs($action['attrs'], 'layui-btn layui-btn-sm'), + 'open' => $this->buildAttrs($action['attrs'], 'layui-btn layui-btn-sm', [ + 'data-open' => $action['url'], + ], [ + 'data-title' => $action['title'], + ]), + 'action' => $this->buildAttrs($action['attrs'], 'layui-btn layui-btn-sm layui-btn-danger', [ + 'data-action' => $action['url'], + 'data-value' => $action['value'] === '' ? 'id#{{d.id}}' : $action['value'], + ], [ + 'data-confirm' => $action['confirm'], + ]), + default => throw new \InvalidArgumentException("PageBuilder 行操作类型无效: {$action['type']}"), + }; + + return array_merge($action, [ + 'attrs' => $attrs, + 'html' => $this->renderer->render(strval($action['label']), $attrs, strval($action['tag'] ?? 'a')), + ]); + } + + /** + * @param array $action + * @return array + */ + private function normalizeBase(array $action): array + { + $action = array_merge([ + 'type' => '', + 'label' => '', + 'url' => '', + 'title' => '', + 'value' => '', + 'confirm' => '', + 'rule' => '', + 'auth' => null, + 'html' => '', + 'attrs' => [], + 'class' => '', + 'tag' => 'a', + ], $action); + + $auth = $action['auth']; + $action['type'] = trim(strval($action['type'])); + $action['label'] = BuilderLang::text(strval($action['label'])); + $action['url'] = strval($action['url']); + $action['title'] = BuilderLang::text(strval($action['title'])); + $action['value'] = strval($action['value']); + $action['confirm'] = BuilderLang::text(strval($action['confirm'])); + $action['rule'] = strval($action['rule']); + $action['html'] = strval($action['html']); + $action['attrs'] = BuilderLang::attrs(is_array($action['attrs']) ? $action['attrs'] : []); + $action['auth'] = $auth === null ? null : (trim(strval($auth)) ?: null); + $action['tag'] = trim(strval($action['tag'])) ?: 'a'; + + $class = trim(strval($action['class'])); + if ($class !== '') { + $action['attrs']['class'] = BuilderAttributes::mergeClassNames(strval($action['attrs']['class'] ?? ''), $class); + } + + return $action; + } + + /** + * @param array $attrs + * @param array $fixed + * @param array $optional + * @return array + */ + private function buildAttrs(array $attrs, string $class, array $fixed = [], array $optional = []): array + { + $attrs = BuilderAttributes::make($attrs)->class($class)->all(); + foreach ($fixed as $name => $value) { + $attrs[$name] = $value; + } + foreach ($optional as $name => $value) { + if ($value !== '') { + $attrs[$name] = $value; + } + } + return BuilderLang::attrs($attrs); + } +} diff --git a/plugin/think-library/src/builder/page/PageBlocks.php b/plugin/think-library/src/builder/page/PageBlocks.php new file mode 100644 index 000000000..1203d52aa --- /dev/null +++ b/plugin/think-library/src/builder/page/PageBlocks.php @@ -0,0 +1,99 @@ +section(); + if ($class !== null && $class !== '') { + $node->class($class); + } + if (is_callable($callback)) { + $callback($node); + } + return $node; + } + + public static function card(PageNode $parent, ?string $title = null, ?callable $callback = null, string $class = 'layui-card'): PageNode + { + $card = $parent->div()->class($class); + if ($title !== null && $title !== '') { + $card->div()->class('layui-card-header notselect')->html(sprintf( + '%s', + self::escape($title) + )); + } + $body = $card->div()->class('layui-card-body'); + if (is_callable($callback)) { + $callback($body); + } + return $card; + } + + public static function grid(PageNode $parent, string $class, callable $callback): PageNode + { + $node = $parent->div()->class($class); + $callback($node); + return $node; + } + + public static function column(PageNode $parent, string $class, callable $callback): PageNode + { + $node = $parent->div()->class($class); + $callback($node); + return $node; + } + + public static function title(PageNode $parent, string $title, string $tag = 'h3', string $class = ''): PageNode + { + $node = $parent->node($tag); + if ($class !== '') { + $node->class($class); + } + $node->html(self::escape($title)); + return $node; + } + + public static function text(PageNode $parent, string $text, string $class = ''): PageNode + { + $node = $parent->div(); + if ($class !== '') { + $node->class($class); + } + $node->html(self::escape($text)); + return $node; + } + + public static function stat(PageNode $parent, string $label, string $value, string $class = ''): PageNode + { + $node = $parent->div()->class(trim($class) === '' ? 'ta-stat-item' : $class); + $node->html(sprintf('%s%s', self::escape($label), self::escape($value))); + return $node; + } + + public static function kv(PageNode $parent, string $label, string $value, string $class = ''): PageNode + { + $node = $parent->div()->class(trim($class) === '' ? 'ta-kv-item' : $class); + $node->html(sprintf( + '%s%s', + self::escape($label), + self::escape($value) + )); + return $node; + } + + private static function escape(string $content): string + { + return htmlentities(BuilderLang::text($content), ENT_QUOTES, 'UTF-8'); + } +} diff --git a/plugin/think-library/src/builder/page/PageBuilder.php b/plugin/think-library/src/builder/page/PageBuilder.php new file mode 100644 index 000000000..d11df7394 --- /dev/null +++ b/plugin/think-library/src/builder/page/PageBuilder.php @@ -0,0 +1,2041 @@ + + */ + private array $tableTemplateKeys = []; + + /** + * 表格生成的启动脚本索引. + * @var array + */ + private array $tableBootScriptIndexes = []; + + /** + * 表格生成的脚本索引. + * @var array + */ + private array $tableScriptIndexes = []; + + /** + * PageBuilder 构造函数. + * + * @param Controller $class 当前控制器实例 + */ + public function __construct(Controller $class) + { + $this->class = $class; + $this->tableOptions = $this->normalizeTableOptions([]); + } + + /** + * 创建列表页生成器. + */ + public static function make(): self + { + return Library::$sapp->invokeClass(static::class); + } + + /** + * 创建普通 DOM 页面. + */ + public static function domPage(): self + { + return self::make()->preset('dom-page'); + } + + /** + * 创建整页表格页面. + */ + public static function tablePage(): self + { + return self::make()->preset('table-page'); + } + + /** + * 创建弹层列表页面. + */ + public static function dialogList(): self + { + return self::make()->preset('dialog-list'); + } + + /** + * 创建原始 JS 片段包装对象 + * + * @param string $script JavaScript 代码 + * @return array 返回包装数组 + */ + public static function js(string $script): array + { + return ['__raw__' => $script]; + } + + /** + * 定义页面结构. + * @param callable(PageLayout): void $callback + * @return $this + */ + public function define(callable $callback): self + { + $layout = new PageLayout($this); + $this->layout = $layout; + $callback($layout); + return $this; + } + + /** + * 完成页面构建. + * @return $this + */ + public function build(): self + { + return $this; + } + + public function preset(string $preset): self + { + $preset = trim($preset); + if ($preset !== '') { + $this->preset = $preset; + } + return $this; + } + + public function getPreset(): string + { + return $this->preset; + } + + /** + * 设置页面标题. + * @return $this + */ + public function setTitle(string $title): self + { + $this->title = $title; + return $this; + } + + /** + * 设置内容样式类. + * @return $this + */ + public function setContentClass(string $class): self + { + $this->contentClass = trim($class); + return $this; + } + + /** + * 设置搜索表单属性. + * @return $this + */ + public function setSearchAttrs(array $attrs): self + { + $this->searchAttrs = BuilderAttributes::make($this->searchAttrs)->merge($attrs)->all(); + return $this; + } + + public function syncSearchNode(PageSearch $node, array $attrs): self + { + if ($this->searchNode === $node) { + $this->searchNodeAttrs = BuilderAttributes::make($attrs)->all(); + } + return $this; + } + + public function activateSearchNode(PageSearch $node): self + { + ++$this->searchVersion; + $this->searchNode = $node; + $this->searchFields = []; + $this->searchNodeAttrs = []; + return $this; + } + + public function deactivateSearchNode(PageSearch $node): self + { + if ($this->searchNode === $node) { + ++$this->searchVersion; + $this->searchNode = null; + $this->searchFields = []; + $this->searchNodeAttrs = []; + } + return $this; + } + + public function isActiveSearchNode(PageSearch $node): bool + { + return $this->searchNode === $node; + } + + public function currentSearchVersion(): int + { + return $this->searchVersion; + } + + public function nextSearchFormId(): string + { + ++$this->searchNodeSeed; + return 'PageSearchForm' . $this->searchNodeSeed; + } + + public function canSyncSearchAttachment(?int $version): bool + { + return $version === null || ($this->searchNode instanceof PageSearch && $this->searchVersion === $version); + } + + /** + * 设置搜索图例. + * @return $this + */ + public function setSearchLegend(string $legend): self + { + $this->searchLegend = $legend; + return $this; + } + + /** + * 切换搜索图例显示. + * @return $this + */ + public function withSearchLegend(bool $show = true): self + { + $this->searchLegendEnabled = $show; + return $this; + } + + /** + * 设置表格. + * @return $this + */ + public function setTable(string $id = 'PageDataTable', ?string $url = null, array $attrs = []): self + { + $this->tableId = $id; + $this->tableUrl = $url; + $this->tableAttrs = BuilderAttributes::make($attrs)->all(); + return $this; + } + + public function activateTableNode(PageTable $node, string $id = 'PageDataTable', ?string $url = null, array $attrs = []): self + { + ++$this->tableVersion; + if ($this->tableNode !== $node) { + $this->resetTableState(); + } + $this->tableNode = $node; + return $this->setTable($id, $url, $attrs); + } + + public function deactivateTableNode(PageTable $node): self + { + if ($this->tableNode === $node) { + ++$this->tableVersion; + $this->tableNode = null; + $this->resetTableState(); + } + return $this; + } + + public function isActiveTableNode(PageTable $node): bool + { + return $this->tableNode === $node; + } + + public function currentTableVersion(): int + { + return $this->tableVersion; + } + + public function canSyncTableAttachment(?int $version): bool + { + return $version === null || ($this->tableNode instanceof PageTable && $this->tableVersion === $version); + } + + public function encodeJsValue(mixed $value): string + { + return $this->encodeJs($value); + } + + /** + * 设置表格参数. + * @return $this + */ + public function setTableOptions(array $options): self + { + $this->tableOptions = $this->mergeAssoc($this->tableOptions, $options); + return $this; + } + + public function addTableBootScript(PageTable $node, string $script): self + { + if (!$this->isActiveTableNode($node)) { + return $this; + } + $script = trim($script); + if ($script === '') { + return $this; + } + $this->bootScripts[] = $script; + end($this->bootScripts); + $index = key($this->bootScripts); + if (is_int($index)) { + $this->tableBootScriptIndexes[] = $index; + } + return $this; + } + + public function addTableScript(PageTable $node, string $script): self + { + if (!$this->isActiveTableNode($node)) { + return $this; + } + $script = trim($script); + if ($script === '') { + return $this; + } + $this->scripts[] = $script; + end($this->scripts); + $index = key($this->scripts); + if (is_int($index)) { + $this->tableScriptIndexes[] = $index; + } + return $this; + } + + public function addTableTemplate(PageTable $node, string $id, string $html): self + { + if (!$this->isActiveTableNode($node)) { + return $this; + } + $id = trim($id); + if ($id !== '') { + $this->templates[$id] = $html; + $this->trackTableTemplateKeys([$id], []); + } + return $this; + } + + /** + * @return array + */ + public function getTableOptions(): array + { + return $this->tableOptions; + } + + /** + * @param array $options + */ + public function createTableOptions(array $options = []): PageTableOptions + { + return new PageTableOptions($this, $this->mergeAssoc($this->tableOptions, $options)); + } + + public function attachTableOptions(PageTableOptions $options): PageTableOptions + { + return $options->attach($this->replaceTableOptions($options->export()), $this->currentTableVersion()); + } + + /** + * @param array $options + * @return array + */ + public function replaceTableOptions(array $options): array + { + $this->tableOptions = $this->normalizeTableOptions($options); + return $this->tableOptions; + } + + /** + * 设置工具条模板 ID. + * @return $this + */ + public function setToolbarId(string $toolbarId): self + { + $this->toolbarId = trim($toolbarId) ?: 'toolbar'; + return $this; + } + + /** + * 添加弹窗按钮. + */ + public function addModalButton(string $label, string $url, string $title = '', array $attrs = [], ?string $auth = null): self + { + return $this->appendButtonAction($this->actionNormalizer()->button([ + 'type' => 'modal', + 'label' => $label, + 'title' => $title, + 'url' => $url, + 'auth' => $auth, + 'attrs' => $attrs, + ])); + } + + /** + * 添加跳转按钮. + */ + public function addOpenButton(string $label, string $url, array $attrs = [], ?string $auth = null): self + { + return $this->appendButtonAction($this->actionNormalizer()->button([ + 'type' => 'open', + 'label' => $label, + 'url' => $url, + 'auth' => $auth, + 'attrs' => $attrs, + ])); + } + + /** + * 添加加载按钮. + */ + public function addLoadButton(string $label, string $url, array $attrs = [], ?string $auth = null): self + { + return $this->appendButtonAction($this->actionNormalizer()->button([ + 'type' => 'load', + 'label' => $label, + 'url' => $url, + 'auth' => $auth, + 'attrs' => $attrs, + ])); + } + + /** + * 添加通用动作按钮. + */ + public function addActionButton(string $label, string $url, string $value = '', string $confirm = '', array $attrs = [], ?string $auth = null): self + { + return $this->appendButtonAction($this->actionNormalizer()->button([ + 'type' => 'action', + 'label' => $label, + 'url' => $url, + 'value' => $value, + 'confirm' => $confirm, + 'auth' => $auth, + 'attrs' => $attrs, + ])); + } + + /** + * 添加头部按钮 HTML. + * @return $this + */ + public function addButtonHtml(string $html, array $schema = []): self + { + $this->buttons[] = $html; + $this->buttonItems[] = array_merge(['html' => $html], $schema); + return $this; + } + + /** + * 添加结构化按钮. + */ + public function addButton(array $button): self + { + return $this->appendButtonAction($this->actionNormalizer()->button($button)); + } + + /** + * @param array $action + */ + public function createButtonAction(array $action = []): PageAction + { + return new PageAction($this, 'button', $action); + } + + public function attachButtonAction(PageAction $action): PageAction + { + $index = count($this->buttonItems); + $this->storeButtonAction($action->export(), $index); + return $action->attach($index); + } + + /** + * @param array $action + * @return array + */ + public function replaceButtonAction(int $index, array $action): array + { + return $this->storeButtonAction($action, $index); + } + + /** + * 添加批量操作按钮. + */ + public function addBatchActionButton(string $label, string $url, string $rule, string $confirm = '', array $attrs = [], ?string $auth = null): self + { + return $this->appendButtonAction($this->actionNormalizer()->button([ + 'type' => 'batch-action', + 'label' => $label, + 'url' => $url, + 'rule' => $rule, + 'confirm' => $confirm, + 'auth' => $auth, + 'attrs' => $attrs, + ])); + } + + /** + * 添加搜索输入框. + * @return $this + */ + public function addSearchInput(string $name, string $label, string $placeholder = '', array $attrs = []): self + { + return $this->addSearchField($this->searchFieldNormalizer()->input($name, $label, $placeholder, $attrs)); + } + + /** + * 添加搜索字段. + * @return $this + */ + public function addSearchField(array $field): self + { + $this->searchFields[] = $this->searchFieldNormalizer()->field($field); + return $this; + } + + /** + * 创建搜索字段对象. + * @param array $field + */ + public function createSearchField(array $field): PageSearchField + { + return new PageSearchField($this, $this->searchFieldNormalizer()->field($field)); + } + + public function attachSearchField(PageSearchField $field): PageSearchField + { + $index = count($this->searchFields); + $normalized = $this->searchFieldNormalizer()->field($field->export()); + $this->searchFields[$index] = $normalized; + return $field->attach($index, $normalized, $this->currentSearchVersion()); + } + + /** + * @param array $field + * @return array + */ + public function replaceSearchField(int $index, array $field): array + { + $normalized = $this->searchFieldNormalizer()->field($field); + $this->searchFields[$index] = $normalized; + return $normalized; + } + + /** + * 添加搜索下拉框. + * @return $this + */ + public function addSearchSelect(string $name, string $label, array $options = [], array $attrs = [], string $source = ''): self + { + return $this->addSearchField($this->searchFieldNormalizer()->select($name, $label, $options, $attrs, $source)); + } + + /** + * 添加搜索日期范围. + * @return $this + */ + public function addSearchDateRange(string $name, string $label, string $placeholder = '', array $attrs = []): self + { + return $this->addSearchField($this->searchFieldNormalizer()->dateRange($name, $label, $placeholder, $attrs)); + } + + /** + * 添加搜索隐藏字段. + * @return $this + */ + public function addSearchHidden(string $name, string $value = ''): self + { + return $this->addSearchField($this->searchFieldNormalizer()->hidden($name, $value)); + } + + /** + * 添加搜索按钮. + * @return $this + */ + public function addSearchSubmitButton(string $label = '搜 索', array $attrs = []): self + { + return $this->addSearchField($this->searchFieldNormalizer()->submit($label, $attrs)); + } + + /** + * 添加勾选列. + * @return $this + */ + public function addCheckboxColumn(array $options = []): self + { + return $this->addColumn(array_merge(['checkbox' => true, 'fixed' => true], $options)); + } + + /** + * 添加排序输入列. + * @return $this + */ + public function addSortInputColumn(string $actionUrl = '{:sysuri()}', array $options = []): self + { + $this->attachSortInputColumn($this->createSortInputColumn($actionUrl, $options)); + return $this; + } + + /** + * 添加表格列. + * @return $this + */ + public function addColumn(array $column): self + { + $this->columns[] = $column; + return $this; + } + + /** + * @param array $column + */ + public function createColumn(array $column = []): PageColumn + { + return new PageColumn($this, $column); + } + + public function createSortInputColumn(string $actionUrl = '{:sysuri()}', array $options = []): PageSortInputColumn + { + return new PageSortInputColumn($this, $actionUrl, $options); + } + + public function attachSortInputColumn(PageSortInputColumn $column): PageSortInputColumn + { + return $column->attachResult($this->storeSortInputColumn($column->getActionUrl(), $column->export())); + } + + /** + * @param array $options + * @param array{templateKeys?: array, scriptIndexes?: array} $meta + * @return array + */ + public function replaceSortInputColumn(int $index, string $actionUrl, array $options, array $meta = []): array + { + return $this->storeSortInputColumn($actionUrl, $options, $index, $meta); + } + + public function attachColumn(PageColumn $column): PageColumn + { + $index = count($this->columns); + $this->columns[$index] = $column->export(); + return $column->attach($index, $this->columns[$index], $this->currentTableVersion()); + } + + /** + * @param array $column + * @return array + */ + public function replaceColumn(int $index, array $column): array + { + $this->columns[$index] = $column; + return $column; + } + + /** + * 添加工具条列. + * @return $this + */ + public function addToolbarColumn(string $title = '操作面板', array $options = []): self + { + $this->attachToolbarColumn($this->createToolbarColumn($title, $options)); + return $this; + } + + /** + * 添加状态开关列. + * @return $this + */ + public function addStatusSwitchColumn(string $actionUrl, array $options = []): self + { + $this->attachStatusSwitchColumn($this->createStatusSwitchColumn($actionUrl, $options)); + return $this; + } + + public function createToolbarColumn(string $title = '操作面板', array $options = []): PageToolbarColumn + { + return new PageToolbarColumn($this, $title, $options); + } + + public function attachToolbarColumn(PageToolbarColumn $column): PageToolbarColumn + { + return $column->attachResult($this->storeToolbarColumn($column->export())); + } + + /** + * @param array $options + * @param array{templateKeys?: array, scriptIndexes?: array} $meta + * @return array + */ + public function replaceToolbarColumn(int $index, array $options, array $meta = []): array + { + return $this->storeToolbarColumn($options, $index, $meta); + } + + public function createStatusSwitchColumn(string $actionUrl, array $options = []): PageStatusSwitchColumn + { + return new PageStatusSwitchColumn($this, $actionUrl, $options); + } + + public function attachStatusSwitchColumn(PageStatusSwitchColumn $column): PageStatusSwitchColumn + { + return $column->attachResult($this->storeStatusSwitchColumn($column->getActionUrl(), $column->export())); + } + + /** + * @param array $options + * @param array{templateKeys?: array, scriptIndexes?: array} $meta + * @return array + */ + public function replaceStatusSwitchColumn(int $index, string $actionUrl, array $options, array $meta = []): array + { + return $this->storeStatusSwitchColumn($actionUrl, $options, $index, $meta); + } + + /** + * 添加行弹窗按钮. + */ + public function addRowModalAction(string $label, string $url, string $title = '', array $attrs = [], ?string $auth = null): self + { + return $this->appendRowAction($this->actionNormalizer()->row([ + 'type' => 'modal', + 'label' => $label, + 'title' => $title, + 'url' => $url, + 'auth' => $auth, + 'attrs' => $attrs, + ])); + } + + /** + * 添加行跳转按钮. + */ + public function addRowOpenAction(string $label, string $url, string $title = '', array $attrs = [], ?string $auth = null): self + { + return $this->appendRowAction($this->actionNormalizer()->row([ + 'type' => 'open', + 'label' => $label, + 'title' => $title, + 'url' => $url, + 'auth' => $auth, + 'attrs' => $attrs, + ])); + } + + /** + * 添加行工具按钮 HTML. + * @return $this + */ + public function addRowActionHtml(string $html): self + { + $this->rowActions[] = $html; + return $this; + } + + /** + * 添加结构化行操作. + */ + public function addRowAction(array $action): self + { + return $this->appendRowAction($this->actionNormalizer()->row($action)); + } + + /** + * @param array $action + */ + public function createRowAction(array $action = []): PageAction + { + return new PageAction($this, 'row', $action); + } + + public function attachRowAction(PageAction $action): PageAction + { + $index = count($this->rowActions); + $this->storeRowAction($action->export(), $index); + return $action->attach($index, $this->currentTableVersion()); + } + + /** + * @param array $action + * @return array + */ + public function replaceRowAction(int $index, array $action): array + { + return $this->storeRowAction($action, $index); + } + + /** + * 添加行操作按钮. + */ + public function addRowActionButton(string $label, string $url, string $value = 'id#{{d.id}}', string $confirm = '', array $attrs = [], ?string $auth = null): self + { + return $this->appendRowAction($this->actionNormalizer()->row([ + 'type' => 'action', + 'label' => $label, + 'url' => $url, + 'value' => $value, + 'confirm' => $confirm, + 'auth' => $auth, + 'attrs' => $attrs, + ])); + } + + /** + * 添加模板片段. + * @return $this + */ + public function addTemplate(string $id, string $html): self + { + $this->templates[$id] = $html; + return $this; + } + + /** + * 添加初始化前脚本. + * @return $this + */ + public function addBootScript(string $script): self + { + $this->bootScripts[] = trim($script); + return $this; + } + + /** + * 添加初始化后脚本. + * @return $this + */ + public function addInitScript(string $script): self + { + $this->initScripts[] = trim($script); + return $this; + } + + /** + * 添加附加脚本. + * @return $this + */ + public function addScript(string $script): self + { + $this->scripts[] = trim($script); + return $this; + } + + /** + * 输出页面内容. + * @return mixed + */ + public function fetch(array $vars = []) + { + throw new HttpResponseException($this->renderResponse($vars)); + } + + /** + * 渲染页面 HTML. + * @param array $vars + */ + public function renderHtml(array $vars = []): string + { + return $this->renderResponse($vars)->getContent(); + } + + /** + * 获取页面配置. + */ + public function toArray(): array + { + $content = $this->schemaContentNodes(); + return [ + 'preset' => $this->preset, + 'title' => BuilderLang::text($this->title), + 'buttons' => $this->normalizeSchemaValue($this->resolveButtonItems($content)), + 'content' => $this->normalizeSchemaValue($content), + 'templates' => array_keys($this->collectTemplateMap($content, false)), + ]; + } + + /** + * 合并数组配置. + */ + private function mergeAssoc(array $origin, array $append): array + { + foreach ($append as $key => $value) { + if (isset($origin[$key]) && is_array($origin[$key]) && is_array($value) && !array_is_list($origin[$key]) && !array_is_list($value)) { + $origin[$key] = $this->mergeAssoc($origin[$key], $value); + } else { + $origin[$key] = $value; + } + } + return $origin; + } + + /** + * @param array $vars + */ + private function renderResponse(array $vars = []) + { + $vars['title'] = $vars['title'] ?? BuilderLang::text($this->title); + $vars['pageBuilder'] = $vars['pageBuilder'] ?? $this; + $vars['pageSchema'] = $vars['pageSchema'] ?? $this->toArray(); + $vars['staticRoot'] = strval($vars['staticRoot'] ?? AppService::uri('static')); + foreach (get_object_vars($this->class) as $k => $v) { + $vars[$k] = $v; + } + $this->renderVars = $vars; + return display($this->render(), $vars); + } + + /** + * Schema 值标准化. + * @param mixed $value + */ + private function normalizeSchemaValue($value) + { + if (is_array($value) && isset($value['__raw__'])) { + return ['type' => 'js', 'code' => $value['__raw__']]; + } + if (is_array($value)) { + if (array_is_list($value)) { + $result = []; + foreach ($value as $key => $item) { + $result[$key] = $this->normalizeSchemaValue($item); + } + return $result; + } + + $result = []; + foreach ($value as $key => $item) { + if ($key === 'attrs' && is_array($item)) { + $result[$key] = BuilderLang::attrs($item); + continue; + } + if (in_array(strval($key), ['title', 'label', 'legend', 'placeholder', 'remark', 'subtitle', 'confirm', 'html'], true) && is_string($item)) { + $result[$key] = BuilderLang::text($item); + continue; + } + $result[$key] = $this->normalizeSchemaValue($item); + } + return $result; + } + return $value; + } + + /** + * 构建表格参数. + */ + private function buildTableOptions(): array + { + $options = $this->tableOptions; + if (count($this->columns) > 0) { + $options['cols'] = [$this->columns]; + } + return $options; + } + + /** + * @param array $options + * @return array + */ + private function normalizeTableOptions(array $options): array + { + return $this->mergeAssoc(['even' => true, 'height' => 'full'], $options); + } + + /** + * 追加头部动作. + * @param array $action + */ + private function appendButtonAction(array $action): self + { + $this->storeButtonAction($action); + return $this; + } + + /** + * 追加行动作. + * @param array $action + */ + private function appendRowAction(array $action): self + { + $this->storeRowAction($action); + return $this; + } + + /** + * @param array $action + * @return array + */ + private function storeButtonAction(array $action, ?int $index = null): array + { + $action = $this->actionNormalizer()->button($action); + $html = strval($action['html'] ?? ''); + $schema = array_merge($action, ['html' => $html]); + + if ($index === null) { + $this->buttons[] = $html; + $this->buttonItems[] = $schema; + } else { + $this->buttons[$index] = $html; + $this->buttonItems[$index] = $schema; + } + + return $schema; + } + + /** + * @param array $action + * @return array + */ + private function storeRowAction(array $action, ?int $index = null): array + { + $action = $this->actionNormalizer()->row($action); + $html = strval($action['html'] ?? ''); + $schema = array_merge($action, ['html' => $html]); + + if ($index === null) { + $this->rowActions[] = $html; + } else { + $this->rowActions[$index] = $html; + } + + return $schema; + } + + private function actionNormalizer(): PageActionNormalizer + { + return new PageActionNormalizer($this->tableId); + } + + private function columnNormalizer(): PageColumnNormalizer + { + return new PageColumnNormalizer($this->tableId, $this->toolbarId, fn($value): string => $this->encodeJs($value)); + } + + private function searchFieldNormalizer(): PageSearchFieldNormalizer + { + return new PageSearchFieldNormalizer(); + } + + /** + * @param array $preset + */ + private function appendColumnPreset(array $preset): self + { + $this->storeColumnPreset($preset); + return $this; + } + + /** + * @param array $options + * @param array{templateKeys?: array, scriptIndexes?: array} $meta + * @return array + */ + private function storeSortInputColumn(string $actionUrl, array $options, ?int $index = null, array $meta = []): array + { + return $this->storeColumnPreset($this->columnNormalizer()->sortInput($actionUrl, $options), $index, $meta); + } + + /** + * @param array $options + * @param array{templateKeys?: array, scriptIndexes?: array} $meta + * @return array + */ + private function storeToolbarColumn(array $options, ?int $index = null, array $meta = []): array + { + $title = strval($options['title'] ?? '操作面板'); + return $this->storeColumnPreset($this->columnNormalizer()->toolbar($title, $options), $index, $meta); + } + + /** + * @param array $options + * @param array{templateKeys?: array, scriptIndexes?: array} $meta + * @return array + */ + private function storeStatusSwitchColumn(string $actionUrl, array $options, ?int $index = null, array $meta = []): array + { + return $this->storeColumnPreset($this->columnNormalizer()->statusSwitch($actionUrl, $options), $index, $meta); + } + + /** + * @param array $preset + * @param array{templateKeys?: array, scriptIndexes?: array} $meta + * @return array + */ + private function storeColumnPreset(array $preset, ?int $index = null, array $meta = []): array + { + $columnIndex = $index ?? count($this->columns); + if (is_array($preset['column'] ?? null)) { + $this->columns[$columnIndex] = $preset['column']; + } + $templateKeys = $this->replacePresetTemplates((array)($preset['templates'] ?? []), (array)($meta['templateKeys'] ?? [])); + $scriptIndexes = $this->replacePresetScripts((array)($preset['scripts'] ?? []), (array)($meta['scriptIndexes'] ?? [])); + $this->trackTableTemplateKeys($templateKeys, (array)($meta['templateKeys'] ?? [])); + $this->trackTableScriptIndexes($scriptIndexes, (array)($meta['scriptIndexes'] ?? [])); + return [ + 'index' => $columnIndex, + 'version' => $this->currentTableVersion(), + 'column' => $this->columns[$columnIndex] ?? [], + 'templateKeys' => $templateKeys, + 'scriptIndexes' => $scriptIndexes, + ]; + } + + /** + * @param array $templates + * @param array $keys + * @return array + */ + private function replacePresetTemplates(array $templates, array $keys = []): array + { + foreach ($keys as $key) { + if (is_string($key) && $key !== '') { + unset($this->templates[$key]); + } + } + $result = []; + foreach ($templates as $id => $html) { + $id = is_string($id) ? trim($id) : ''; + if ($id !== '') { + $this->templates[$id] = strval($html); + $result[] = $id; + } + } + return $result; + } + + /** + * @param array $scripts + * @param array $indexes + * @return array + */ + private function replacePresetScripts(array $scripts, array $indexes = []): array + { + $normalized = []; + foreach ($scripts as $script) { + $script = trim(strval($script)); + if ($script !== '') { + $normalized[] = $script; + } + } + + $result = []; + $indexes = array_values(array_map('intval', $indexes)); + $count = max(count($normalized), count($indexes)); + for ($i = 0; $i < $count; $i++) { + if (isset($normalized[$i])) { + if (isset($indexes[$i])) { + $this->scripts[$indexes[$i]] = $normalized[$i]; + $result[] = $indexes[$i]; + } else { + $this->scripts[] = $normalized[$i]; + end($this->scripts); + $key = key($this->scripts); + $result[] = is_int($key) ? $key : count($this->scripts) - 1; + } + } elseif (isset($indexes[$i])) { + unset($this->scripts[$indexes[$i]]); + } + } + + return $result; + } + + /** + * @param array $keys + * @param array $replaced + */ + private function trackTableTemplateKeys(array $keys, array $replaced): void + { + $current = array_flip($this->tableTemplateKeys); + foreach ($replaced as $key) { + unset($current[strval($key)]); + } + foreach ($keys as $key) { + $key = trim(strval($key)); + if ($key !== '') { + $current[$key] = true; + } + } + $this->tableTemplateKeys = array_keys($current); + } + + /** + * @param array $indexes + * @param array $replaced + */ + private function trackTableScriptIndexes(array $indexes, array $replaced): void + { + $current = array_flip(array_map('intval', $this->tableScriptIndexes)); + foreach ($replaced as $index) { + unset($current[(int)$index]); + } + foreach ($indexes as $index) { + $current[(int)$index] = true; + } + $this->tableScriptIndexes = array_map('intval', array_keys($current)); + } + + private function resetTableState(): void + { + foreach ($this->tableTemplateKeys as $key) { + unset($this->templates[$key]); + } + foreach ($this->tableBootScriptIndexes as $index) { + unset($this->bootScripts[$index]); + } + foreach ($this->tableScriptIndexes as $index) { + unset($this->scripts[$index]); + } + + $this->tableId = ''; + $this->tableUrl = null; + $this->tableAttrs = []; + $this->tableOptions = $this->normalizeTableOptions([]); + $this->columns = []; + $this->toolbarId = 'toolbar'; + $this->rowActions = []; + $this->tableTemplateKeys = []; + $this->tableBootScriptIndexes = []; + $this->tableScriptIndexes = []; + } + + /** + * 渲染页面. + */ + private function render(): string + { + $schema = $this->toArray(); + $buttons = $this->layout instanceof PageLayout + ? array_values(array_filter(array_map(static fn(array $button): string => strval($button['html'] ?? ''), $schema['buttons'] ?? []))) + : $this->buttons; + $this->renderState = $this->createRenderState($schema); + try { + return $this->renderPipeline()->renderShell( + $this->renderState, + $this->preset, + $this->title, + $buttons, + strval($this->renderVars['showErrorMessage'] ?? ''), + $this->contentClass, + $this->renderPageContent(), + $this->renderTemplates(), + $this->renderScripts() + ); + } finally { + $this->renderState = null; + } + } + + /** + * 渲染内容节点. + * @param array> $nodes + */ + private function renderContentNodes(array $nodes): string + { + return $this->renderPipeline()->renderContentNodes($nodes, $this->currentRenderState()); + } + + /** + * 渲染搜索表单. + */ + /** + * @param array|null $node + */ + private function renderSearchForm(?array $node = null): string + { + if ($node !== null) { + $nodeAttrs = is_array($node['attrs'] ?? null) ? $node['attrs'] : []; + $attrs = BuilderAttributes::make($this->searchAttrs)->merge($nodeAttrs)->all(); + $formId = trim(strval($attrs['id'] ?? $node['formId'] ?? '')); + if ($formId !== '') { + $attrs['id'] = $formId; + } + $legend = array_key_exists('legend', $node) && $node['legend'] !== null ? strval($node['legend']) : $this->searchLegend; + $legendEnabled = array_key_exists('legendEnabled', $node) && $node['legendEnabled'] !== null ? boolval($node['legendEnabled']) : $this->searchLegendEnabled; + return $this->renderPipeline()->renderSearch( + is_array($node['fields'] ?? null) ? $node['fields'] : [], + $attrs, + trim(strval($node['tableId'] ?? '')), + strval($attrs['action'] ?? $this->resolveCurrentUrl()), + $legend, + $legendEnabled, + $this->currentRenderState() + ); + } + + $attrs = BuilderAttributes::make($this->searchAttrs)->merge($this->searchNodeAttrs)->all(); + return $this->renderPipeline()->renderSearch( + $this->searchFields, + $attrs, + $this->tableId, + strval($attrs['action'] ?? $this->resolveCurrentUrl()), + $this->searchLegend, + $this->searchLegendEnabled, + $this->currentRenderState() + ); + } + + /** + * 解析当前 URL. + */ + private function resolveCurrentUrl(): string + { + try { + return url()->build(); + } catch (\Throwable) { + return ''; + } + } + + /** + * 获取搜索值. + */ + private function searchValue(string $name): string + { + if (isset($this->class->get) && is_array($this->class->get) && array_key_exists($name, $this->class->get)) { + return strval($this->class->get[$name]); + } + if (isset($this->renderVars['get']) && is_array($this->renderVars['get']) && array_key_exists($name, $this->renderVars['get'])) { + return strval($this->renderVars['get'][$name]); + } + if (isset($this->class->request)) { + return strval($this->class->request->get($name, '')); + } + return ''; + } + + /** + * 获取搜索选项. + */ + private function resolveSearchOptions(array $field): array + { + if (!empty($field['source']) && isset($this->renderVars[$field['source']]) && is_array($this->renderVars[$field['source']])) { + return BuilderLang::options($this->renderVars[$field['source']]); + } + return BuilderLang::options(is_array($field['options'] ?? null) ? $field['options'] : []); + } + + /** + * 渲染表格节点. + */ + /** + * @param array|null $node + */ + private function renderTable(?array $node = null): string + { + if ($node !== null) { + $nodeAttrs = is_array($node['attrs'] ?? null) ? $node['attrs'] : []; + $attrs = $this->tableNormalizer()->table( + trim(strval($node['id'] ?? '')), + strval($node['url'] ?? $this->resolveCurrentUrl()), + $nodeAttrs, + trim(strval($node['searchTarget'] ?? '')) + ); + return $this->renderPipeline()->renderTable($attrs, $this->currentRenderState()); + } + + $attrs = $this->tableNormalizer()->table( + $this->tableId, + strval($this->tableUrl ?? $this->resolveCurrentUrl()), + $this->tableAttrs, + count($this->searchFields) > 0 ? 'form.form-search' : '' + ); + return $this->renderPipeline()->renderTable($attrs, $this->currentRenderState()); + } + + /** + * 渲染页面主体内容. + */ + private function renderPageContent(): string + { + $contentNodes = $this->currentContentNodes(); + if (count($contentNodes) > 0) { + return $this->renderContentNodes($contentNodes); + } + if ($this->layout instanceof PageLayout) { + return ''; + } + + $content = []; + if ($search = $this->renderSearchForm()) { + $content[] = $search; + } + $content[] = $this->renderTable(); + return join("\n\t\t\t\t", $content); + } + + /** + * 渲染模板片段. + */ + private function renderTemplates(): string + { + if ($this->layout instanceof PageLayout) { + return $this->renderPipeline()->renderTemplates($this->collectTemplateMap($this->currentContentNodes(), true)); + } + return $this->renderPipeline()->renderTemplates($this->templates); + } + + /** + * @return array> + */ + private function currentContentNodes(): array + { + if (!($this->layout instanceof PageLayout)) { + return $this->contentNodes; + } + + [$nodes] = $this->resolveContentNodes($this->layout->exportChildren()); + return $this->assignSearchTableIds($nodes, $this->collectSearchTableIds($nodes)); + } + + /** + * @return array> + */ + private function schemaContentNodes(): array + { + $content = $this->currentContentNodes(); + if (count($content) > 0 || $this->layout instanceof PageLayout) { + return $content; + } + + return $this->buildLegacySchemaContentNodes(); + } + + /** + * 把旧式 page state 投影成新的 content 节点树 schema。 + * + * @return array> + */ + private function buildLegacySchemaContentNodes(): array + { + $content = []; + $searchSelector = ''; + + if (count($this->searchFields) > 0) { + $formId = $this->resolveLegacySearchFormId(); + $searchSelector = "#{$formId}"; + $content[] = [ + 'type' => 'search', + 'formId' => $formId, + 'searchSelector' => $searchSelector, + 'attrs' => BuilderAttributes::make($this->searchAttrs)->all(), + 'modules' => [], + 'fields' => $this->searchFields, + 'legend' => $this->searchLegend, + 'legendEnabled' => $this->searchLegendEnabled, + 'tableId' => $this->tableId, + ]; + } + + $content[] = [ + 'type' => 'table', + 'id' => $this->tableId, + 'url' => strval($this->tableUrl ?? ''), + 'attrs' => $this->tableAttrs, + 'modules' => [], + 'options' => $this->tableOptions, + 'columns' => $this->columns, + 'toolbarId' => $this->toolbarId, + 'rowActions' => $this->rowActions, + 'templates' => $this->templates, + 'bootScripts' => [], + 'scripts' => [], + 'searchTarget' => $searchSelector, + ]; + + return $content; + } + + private function resolveLegacySearchFormId(): string + { + if ($this->legacySearchFormId === '') { + $this->legacySearchFormId = $this->nextSearchFormId(); + } + + return $this->legacySearchFormId; + } + + /** + * 渲染脚本. + */ + private function renderScripts(): string + { + if ($this->layout instanceof PageLayout) { + $readyScripts = []; + foreach ($this->bootScripts as $script) { + $script = trim($script); + if ($script !== '') { + $readyScripts[] = $script; + } + } + + $scriptContext = $this->currentRenderState()->scriptRenderContext(); + foreach ($this->collectNodesOfType($this->currentContentNodes(), 'table') as $table) { + foreach ((array)($table['bootScripts'] ?? []) as $script) { + $script = trim(strval($script)); + if ($script !== '') { + $readyScripts[] = $script; + } + } + + $tableId = trim(strval($table['id'] ?? '')); + if ($tableId !== '') { + $readyScripts[] = (new \think\admin\builder\page\render\PageTableInitScriptRenderer())->render( + $tableId, + $this->buildTableOptionsFromNode($table), + $scriptContext + ); + } + } + + foreach ($this->initScripts as $script) { + $script = trim($script); + if ($script !== '') { + $readyScripts[] = $script; + } + } + + $scripts = []; + foreach ($this->scripts as $script) { + $script = trim($script); + if ($script !== '') { + $scripts[] = $script; + } + } + foreach ($this->collectNodesOfType($this->currentContentNodes(), 'table') as $table) { + foreach ((array)($table['scripts'] ?? []) as $script) { + $script = trim(strval($script)); + if ($script !== '') { + $scripts[] = $script; + } + } + } + + return $this->renderPipeline()->renderScripts($readyScripts, $scripts); + } + + $readyScripts = array_merge( + array_values(array_filter(array_map('trim', $this->bootScripts))), + [($this->tableNode instanceof PageTable || !($this->layout instanceof PageLayout)) + ? (new \think\admin\builder\page\render\PageTableInitScriptRenderer())->render( + $this->tableId, + $this->buildTableOptions(), + $this->currentRenderState()->scriptRenderContext() + ) + : '' + ], + array_values(array_filter(array_map('trim', $this->initScripts))) + ); + + return $this->renderPipeline()->renderScripts( + array_values(array_filter($readyScripts, static fn(string $script): bool => $script !== '')), + array_values(array_filter(array_map('trim', $this->scripts))) + ); + } + + /** + * @param array> $nodes + * @return array{0: array>, 1: string|null} + */ + private function resolveContentNodes(array $nodes, ?string $lastSearchSelector = null): array + { + $resolved = []; + foreach ($nodes as $node) { + if (!is_array($node)) { + continue; + } + + if (strval($node['type'] ?? '') === 'search') { + $selector = $this->resolveSearchSelector($node); + if ($selector !== null) { + $node['searchSelector'] = $selector; + $lastSearchSelector = $selector; + } + } + + if (is_array($node['children'] ?? null)) { + [$children, $lastSearchSelector] = $this->resolveContentNodes($node['children'], $lastSearchSelector); + $node['children'] = $children; + } + + if (strval($node['type'] ?? '') === 'table') { + $attrs = is_array($node['attrs'] ?? null) ? $node['attrs'] : []; + $target = trim(strval($attrs['data-target-search'] ?? '')); + if ($target === '' && $lastSearchSelector !== null) { + $node['searchTarget'] = $lastSearchSelector; + } + } + + $resolved[] = $node; + } + + return [$resolved, $lastSearchSelector]; + } + + /** + * @param array $node + */ + private function resolveSearchSelector(array $node): ?string + { + $attrs = is_array($node['attrs'] ?? null) ? $node['attrs'] : []; + $formId = trim(strval($attrs['id'] ?? $node['formId'] ?? '')); + return $formId === '' ? null : "#{$formId}"; + } + + /** + * @param array> $nodes + * @return array + */ + private function collectSearchTableIds(array $nodes, array $map = []): array + { + foreach ($nodes as $node) { + if (!is_array($node)) { + continue; + } + + if (strval($node['type'] ?? '') === 'table') { + $attrs = is_array($node['attrs'] ?? null) ? $node['attrs'] : []; + $target = trim(strval($attrs['data-target-search'] ?? $node['searchTarget'] ?? '')); + $tableId = trim(strval($node['id'] ?? '')); + if ($target !== '' && $tableId !== '' && !isset($map[$target])) { + $map[$target] = $tableId; + } + } + + if (is_array($node['children'] ?? null)) { + $map = $this->collectSearchTableIds($node['children'], $map); + } + } + + return $map; + } + + /** + * @param array> $nodes + * @param array $map + * @return array> + */ + private function assignSearchTableIds(array $nodes, array $map): array + { + $resolved = []; + foreach ($nodes as $node) { + if (!is_array($node)) { + continue; + } + + if (strval($node['type'] ?? '') === 'search') { + $selector = trim(strval($node['searchSelector'] ?? '')); + if ($selector !== '' && isset($map[$selector])) { + $node['tableId'] = $map[$selector]; + } + } + + if (is_array($node['children'] ?? null)) { + $node['children'] = $this->assignSearchTableIds($node['children'], $map); + } + + $resolved[] = $node; + } + + return $resolved; + } + + /** + * @param array> $nodes + */ + private function firstNodeOfType(array $nodes, string $type): ?array + { + foreach ($nodes as $node) { + if (!is_array($node)) { + continue; + } + if (strval($node['type'] ?? '') === $type) { + return $node; + } + if (is_array($node['children'] ?? null)) { + $child = $this->firstNodeOfType($node['children'], $type); + if ($child !== null) { + return $child; + } + } + } + return null; + } + + /** + * @param array> $nodes + * @return array> + */ + private function collectNodesOfType(array $nodes, string $type): array + { + $result = []; + foreach ($nodes as $node) { + if (!is_array($node)) { + continue; + } + if (strval($node['type'] ?? '') === $type) { + $result[] = $node; + } + if (is_array($node['children'] ?? null)) { + $result = array_merge($result, $this->collectNodesOfType($node['children'], $type)); + } + } + return $result; + } + + /** + * @param array> $nodes + * @return array + */ + private function collectTemplateMap(array $nodes, bool $includeGeneratedToolbars): array + { + $templates = $this->templates; + foreach ($this->collectNodesOfType($nodes, 'table') as $table) { + foreach ((array)($table['templates'] ?? []) as $id => $html) { + $id = trim(strval($id)); + if ($id !== '') { + $templates[$id] = strval($html); + } + } + + $toolbarId = trim(strval($table['toolbarId'] ?? '')); + $rowActions = is_array($table['rowActions'] ?? null) ? $table['rowActions'] : []; + if ($includeGeneratedToolbars && $toolbarId !== '' && count($rowActions) > 0 && !isset($templates[$toolbarId])) { + $html = []; + foreach ($rowActions as $action) { + if (is_array($action)) { + $item = strval($action['html'] ?? ''); + if ($item !== '') { + $html[] = $item; + } + } + } + $templates[$toolbarId] = join("\n", $html); + } + } + return $templates; + } + + /** + * @param array $node + * @return array + */ + private function buildTableOptionsFromNode(array $node): array + { + $options = is_array($node['options'] ?? null) ? $node['options'] : $this->normalizeTableOptions([]); + $columns = is_array($node['columns'] ?? null) ? $node['columns'] : []; + if (count($columns) > 0) { + $options['cols'] = [array_map(function ($column): mixed { + if (!is_array($column)) { + return $column; + } + if (isset($column['title'])) { + $column['title'] = BuilderLang::text(strval($column['title'])); + } + return $column; + }, $columns)]; + } + return $options; + } + + /** + * @param array> $content + * @return array> + */ + private function resolveButtonItems(array $content): array + { + $table = $this->firstNodeOfType($content, 'table'); + $tableId = trim(strval($table['id'] ?? $this->tableId ?: 'PageDataTable')); + $normalizer = new PageActionNormalizer($tableId); + $items = []; + foreach ($this->buttonItems as $button) { + if (is_array($button)) { + $items[] = $normalizer->button($button); + } + } + return $items; + } + + /** + * JS 值编码. + * @param mixed $value + */ + private function encodeJs($value): string + { + if (is_array($value) && isset($value['__raw__'])) { + return $value['__raw__']; + } + if (is_array($value)) { + $items = []; + if (array_is_list($value)) { + foreach ($value as $item) { + $items[] = $this->encodeJs($item); + } + return '[' . join(',', $items) . ']'; + } + foreach ($value as $key => $item) { + $items[] = json_encode((string)$key, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . ':' . $this->encodeJs($item); + } + return '{' . join(',', $items) . '}'; + } + if (is_bool($value)) { + return $value ? 'true' : 'false'; + } + if (is_null($value)) { + return 'null'; + } + if (is_int($value) || is_float($value)) { + return (string)$value; + } + return json_encode((string)$value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + } + + private function tableNormalizer(): PageTableNormalizer + { + return new PageTableNormalizer(); + } + + /** + * @param array $schema + */ + private function createRenderState(array $schema): PageRenderState + { + $attrsRenderer = new BuilderAttributesRenderer(); + return new PageRenderState( + $schema, + new PageNodeRendererFactory(), + new PageNodeRenderContext( + fn(array $nodes): string => $this->renderContentNodes($nodes), + [$attrsRenderer, 'render'], + fn(array $node): string => $this->renderSearchForm($node), + fn(array $node): string => $this->renderTable($node), + ), + new PageSearchRenderContext( + [$attrsRenderer, 'render'], + fn(string $name): string => $this->searchValue($name), + fn(array $field): array => $this->resolveSearchOptions($field), + ), + new PageTableRenderContext( + [$attrsRenderer, 'render'], + ), + new PageScriptRenderContext( + fn($value): string => $this->encodeJs($value), + ) + ); + } + + private function currentRenderState(): PageRenderState + { + return $this->renderState ?? $this->createRenderState($this->toArray()); + } + + private function renderPipeline(): PageRenderPipeline + { + return new PageRenderPipeline(); + } +} diff --git a/plugin/think-library/src/builder/page/PageButtons.php b/plugin/think-library/src/builder/page/PageButtons.php new file mode 100644 index 000000000..d8b6713d0 --- /dev/null +++ b/plugin/think-library/src/builder/page/PageButtons.php @@ -0,0 +1,166 @@ + $button + */ + public function create(array $button = []): PageAction + { + return $this->builder->createButtonAction($button); + } + + /** + * @param array|PageAction $button + */ + public function append(array|PageAction $button): PageAction + { + $action = $button instanceof PageAction ? $button : $this->create($button); + return $this->builder->attachButtonAction($action); + } + + public function modal(string $label, string $url, string $title = '', array $attrs = [], ?string $auth = null): self + { + $this->builder->addModalButton($label, $url, $title, $attrs, $auth); + return $this; + } + + public function open(string $label, string $url, array $attrs = [], ?string $auth = null): self + { + $this->builder->addOpenButton($label, $url, $attrs, $auth); + return $this; + } + + public function load(string $label, string $url, array $attrs = [], ?string $auth = null): self + { + $this->builder->addLoadButton($label, $url, $attrs, $auth); + return $this; + } + + public function batchAction(string $label, string $url, string $rule, string $confirm = '', array $attrs = [], ?string $auth = null): self + { + $this->builder->addBatchActionButton($label, $url, $rule, $confirm, $attrs, $auth); + return $this; + } + + public function action(string $label, string $url, string $value = '', string $confirm = '', array $attrs = [], ?string $auth = null): self + { + $this->builder->addActionButton($label, $url, $value, $confirm, $attrs, $auth); + return $this; + } + + public function html(string $html): self + { + $this->builder->addButtonHtml($html); + return $this; + } + + public function button(string $label, array $attrs = [], ?string $auth = null, string $tag = 'a'): self + { + $this->append([ + 'type' => 'button', + 'label' => $label, + 'tag' => $tag, + 'auth' => $auth, + 'attrs' => $attrs, + ]); + return $this; + } + + public function modalItem(string $label, string $url, string $title = '', array $attrs = [], ?string $auth = null): PageAction + { + return $this->append([ + 'type' => 'modal', + 'label' => $label, + 'title' => $title, + 'url' => $url, + 'auth' => $auth, + 'attrs' => $attrs, + ]); + } + + public function openItem(string $label, string $url, array $attrs = [], ?string $auth = null): PageAction + { + return $this->append([ + 'type' => 'open', + 'label' => $label, + 'url' => $url, + 'auth' => $auth, + 'attrs' => $attrs, + ]); + } + + public function loadItem(string $label, string $url, array $attrs = [], ?string $auth = null): PageAction + { + return $this->append([ + 'type' => 'load', + 'label' => $label, + 'url' => $url, + 'auth' => $auth, + 'attrs' => $attrs, + ]); + } + + public function batchActionItem(string $label, string $url, string $rule, string $confirm = '', array $attrs = [], ?string $auth = null): PageAction + { + return $this->append([ + 'type' => 'batch-action', + 'label' => $label, + 'url' => $url, + 'rule' => $rule, + 'confirm' => $confirm, + 'auth' => $auth, + 'attrs' => $attrs, + ]); + } + + public function actionItem(string $label, string $url, string $value = '', string $confirm = '', array $attrs = [], ?string $auth = null): PageAction + { + return $this->append([ + 'type' => 'action', + 'label' => $label, + 'url' => $url, + 'value' => $value, + 'confirm' => $confirm, + 'auth' => $auth, + 'attrs' => $attrs, + ]); + } + + /** + * @param array $schema + */ + public function htmlItem(string $html, array $schema = []): PageAction + { + return $this->append(array_merge($schema, ['type' => 'html', 'html' => $html])); + } + + public function buttonItem(string $label, array $attrs = [], ?string $auth = null, string $tag = 'a'): PageAction + { + return $this->append([ + 'type' => 'button', + 'label' => $label, + 'tag' => $tag, + 'auth' => $auth, + 'attrs' => $attrs, + ]); + } + + public function item(array $button): self + { + $this->builder->addButton($button); + return $this; + } +} diff --git a/plugin/think-library/src/builder/page/PageColumn.php b/plugin/think-library/src/builder/page/PageColumn.php new file mode 100644 index 000000000..d15f21f02 --- /dev/null +++ b/plugin/think-library/src/builder/page/PageColumn.php @@ -0,0 +1,163 @@ + $column + */ + public function __construct(private PageBuilder $builder, private array $column = []) + { + } + + /** + * @param array $column + */ + public function attach(int $index, array $column, ?int $version = null): self + { + $this->index = $index; + $this->version = $version; + $this->syncHandler = null; + $this->column = $column; + return $this; + } + + public function attachSync(callable $syncHandler): self + { + $this->index = null; + $this->version = null; + $this->syncHandler = $syncHandler; + return $this; + } + + public function field(string $field): self + { + $this->column['field'] = trim($field); + return $this->sync(); + } + + public function title(string $title): self + { + $this->column['title'] = $title; + return $this->sync(); + } + + public function width(int|string $width): self + { + $this->column['width'] = $width; + return $this->sync(); + } + + public function minWidth(int|string $width): self + { + $this->column['minWidth'] = $width; + return $this->sync(); + } + + public function align(string $align): self + { + $this->column['align'] = trim($align); + return $this->sync(); + } + + public function fixed(bool|string $fixed = true): self + { + $this->column['fixed'] = $fixed; + return $this->sync(); + } + + public function sort(bool $sort = true): self + { + $this->column['sort'] = $sort; + return $this->sync(); + } + + public function hide(bool $hide = true): self + { + $this->column['hide'] = $hide; + return $this->sync(); + } + + public function event(string $event): self + { + $this->column['event'] = trim($event); + return $this->sync(); + } + + public function style(string $style): self + { + $this->column['style'] = $style; + return $this->sync(); + } + + public function edit(bool|string $mode = true): self + { + $this->column['edit'] = $mode; + return $this->sync(); + } + + public function toolbar(string $toolbar): self + { + $this->column['toolbar'] = $toolbar; + return $this->sync(); + } + + public function templet(mixed $templet): self + { + $this->column['templet'] = $templet; + return $this->sync(); + } + + public function options(array $options): self + { + foreach ($options as $name => $value) { + if (is_string($name) && trim($name) !== '') { + $this->column[trim($name)] = $value; + } + } + return $this->sync(); + } + + public function option(string $name, mixed $value): self + { + $name = trim($name); + if ($name !== '') { + $this->column[$name] = $value; + } + return $this->sync(); + } + + /** + * @return array + */ + public function export(): array + { + return $this->column; + } + + private function sync(): self + { + if (is_callable($this->syncHandler)) { + $this->column = ($this->syncHandler)($this->column); + } elseif ($this->canSync()) { + $this->column = $this->builder->replaceColumn($this->index, $this->column); + } + return $this; + } + + private function canSync(): bool + { + return $this->index !== null && $this->builder->canSyncTableAttachment($this->version); + } +} diff --git a/plugin/think-library/src/builder/page/PageColumnNormalizer.php b/plugin/think-library/src/builder/page/PageColumnNormalizer.php new file mode 100644 index 000000000..6ad7aeafa --- /dev/null +++ b/plugin/think-library/src/builder/page/PageColumnNormalizer.php @@ -0,0 +1,181 @@ +attributesRenderer = $attributesRenderer ?? new BuilderAttributesRenderer(); + $this->jsEncoder = is_callable($jsEncoder) ? $jsEncoder : static fn(mixed $value): string => json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?: 'null'; + } + + /** + * @param array $options + * @return array + */ + public function sortInput(string $actionUrl = '{:sysuri()}', array $options = []): array + { + $templateId = trim(strval($options['templateId'] ?? $this->buildTemplateId('SortInput'))); + $dataValue = strval($options['dataValue'] ?? 'id#{{d.id}};action#sort;sort#{value}'); + $inputValue = strval($options['inputValue'] ?? '{{d.sort}}'); + $inputAttrs = is_array($options['inputAttrs'] ?? null) ? $options['inputAttrs'] : []; + unset($options['templateId'], $options['dataValue'], $options['inputValue'], $options['inputAttrs']); + + $column = array_merge([ + 'field' => 'sort', + 'title' => '排序权重', + 'width' => 100, + 'align' => 'center', + 'sort' => true, + 'templet' => "#{$templateId}", + ], $options); + $column['title'] = BuilderLang::text(strval($column['title'] ?? '')); + + $attrs = array_merge([ + 'type' => 'number', + 'min' => '0', + 'data-blur-number' => '0', + 'data-action-blur' => $actionUrl, + 'data-value' => $dataValue, + 'data-loading' => 'false', + 'value' => $inputValue, + 'class' => 'layui-input text-center', + ], $inputAttrs); + + return [ + 'column' => $column, + 'templates' => [$templateId => 'attributesRenderer->render($attrs) . '>'], + 'scripts' => [], + ]; + } + + /** + * @param array $options + * @return array + */ + public function toolbar(string $title = '操作面板', array $options = []): array + { + return [ + 'column' => array_merge([ + 'toolbar' => "#{$this->toolbarId}", + 'title' => BuilderLang::text($title), + 'align' => 'center', + 'minWidth' => 150, + 'fixed' => 'right', + ], $options), + 'templates' => [], + 'scripts' => [], + ]; + } + + /** + * @param array $options + * @return array + */ + public function statusSwitch(string $actionUrl, array $options = []): array + { + $templateId = trim(strval($options['templateId'] ?? $this->buildTemplateId('StatusSwitch'))); + $filter = trim(strval($options['filter'] ?? preg_replace('/Tpl$/', '', $templateId))); + $auth = trim(strval($options['auth'] ?? 'state')); + $valueExpr = strval($options['value'] ?? '{{d.id}}'); + $checkedExpr = strval($options['checked'] ?? "{{-d.status>0?'checked':''}}"); + $toggleText = strval($options['text'] ?? BuilderLang::pipeText('已激活|已禁用')); + $activeHtml = strval($options['activeHtml'] ?? sprintf('%s', BuilderLang::text('已激活'))); + $inactiveHtml = strval($options['inactiveHtml'] ?? sprintf('%s', BuilderLang::text('已禁用'))); + $dataScript = trim(strval($options['dataScript'] ?? 'var data = {id: obj.value, status: obj.elem.checked > 0 ? 1 : 0};')); + $reloadSelector = strval($options['reloadSelector'] ?? "#{$this->tableId}"); + $reloadOnError = !array_key_exists('reloadOnError', $options) || intval($options['reloadOnError']) > 0; + $reloadOnSuccess = !array_key_exists('reloadOnSuccess', $options) || intval($options['reloadOnSuccess']) > 0; + unset( + $options['templateId'], + $options['filter'], + $options['auth'], + $options['value'], + $options['checked'], + $options['text'], + $options['activeHtml'], + $options['inactiveHtml'], + $options['dataScript'], + $options['reloadSelector'], + $options['reloadOnError'], + $options['reloadOnSuccess'] + ); + + $column = array_merge([ + 'field' => 'status', + 'title' => '使用状态', + 'align' => 'center', + 'minWidth' => 110, + 'templet' => "#{$templateId}", + ], $options); + $column['title'] = BuilderLang::text(strval($column['title'] ?? '')); + + $attrs = [ + 'type' => 'checkbox', + 'value' => $valueExpr, + 'lay-skin' => 'switch', + 'lay-text' => BuilderLang::pipeText($toggleText), + 'lay-filter' => $filter, + ]; + $toggle = 'attributesRenderer->render($attrs) . ' ' . $checkedExpr . '>'; + $activeLiteral = $this->encodeJs($activeHtml); + $inactiveLiteral = $this->encodeJs($inactiveHtml); + $template = sprintf( + '%s{{-d.status ? %s : %s}}', + addslashes($auth), + $toggle, + $activeLiteral, + $inactiveLiteral + ); + + $reloadScript = sprintf('$(%s).trigger("reload");', $this->encodeJs($reloadSelector)); + $errorScript = $reloadOnError + ? sprintf('$.msg.error(ret.info, 3, function () { %s });', $reloadScript) + : '$.msg.error(ret.info);'; + $successScript = $reloadOnSuccess ? " else {\n {$reloadScript}\n }\n" : ''; + $script = <<"; + } +} diff --git a/plugin/think-library/src/builder/page/render/PageRenderPipeline.php b/plugin/think-library/src/builder/page/render/PageRenderPipeline.php new file mode 100644 index 000000000..fdd974beb --- /dev/null +++ b/plugin/think-library/src/builder/page/render/PageRenderPipeline.php @@ -0,0 +1,100 @@ + $buttons + */ + public function renderShell( + PageRenderState $state, + string $preset, + string $title, + array $buttons, + string $message, + string $contentClass, + string $content, + string $templates, + string $scripts + ): string { + return (new PageShellRenderer())->render( + $preset, + $title, + $buttons, + $message, + $contentClass, + $content, + $templates, + $scripts, + $this->renderSchemaScript($state, 'page-builder-schema') + ); + } + + /** + * @param array> $nodes + */ + public function renderContentNodes(array $nodes, PageRenderState $state): string + { + return $this->renderNodeContent($nodes, $state, "\n\t\t\t\t"); + } + + /** + * @param array> $fields + * @param array $attrs + */ + public function renderSearch( + array $fields, + array $attrs, + string $tableId, + string $action, + string $legend, + bool $legendEnabled, + PageRenderState $state + ): string { + return (new PageSearchRenderer())->render( + $fields, + $attrs, + $tableId, + $action, + $legend, + $legendEnabled, + $state->searchRenderContext() + ); + } + + /** + * @param array $attrs + */ + public function renderTable(array $attrs, PageRenderState $state): string + { + return (new PageTableRenderer())->render($attrs, $state->tableRenderContext()); + } + + /** + * @param array $templates + */ + public function renderTemplates(array $templates): string + { + return (new PageTemplateRenderer())->render($templates); + } + + /** + * @param array $readyScripts + * @param array $scripts + */ + public function renderScripts( + array $readyScripts, + array $scripts + ): string { + return (new PageScriptRenderer())->render($readyScripts, $scripts); + } +} diff --git a/plugin/think-library/src/builder/page/render/PageRenderState.php b/plugin/think-library/src/builder/page/render/PageRenderState.php new file mode 100644 index 000000000..ca4b2afe3 --- /dev/null +++ b/plugin/think-library/src/builder/page/render/PageRenderState.php @@ -0,0 +1,58 @@ + $schema + */ + public function __construct( + array $schema, + PageNodeRendererFactory $nodeRendererFactory, + PageNodeRenderContext $nodeRenderContext, + private PageSearchRenderContext $searchRenderContext, + private PageTableRenderContext $tableRenderContext, + private PageScriptRenderContext $scriptRenderContext, + ) { + parent::__construct($schema, $nodeRendererFactory, $nodeRenderContext); + $this->pageNodeRendererFactory = $nodeRendererFactory; + $this->pageNodeRenderContext = $nodeRenderContext; + } + + public function nodeRendererFactory(): PageNodeRendererFactory + { + return $this->pageNodeRendererFactory; + } + + public function nodeRenderContext(): PageNodeRenderContext + { + return $this->pageNodeRenderContext; + } + + public function searchRenderContext(): PageSearchRenderContext + { + return $this->searchRenderContext; + } + + public function tableRenderContext(): PageTableRenderContext + { + return $this->tableRenderContext; + } + + public function scriptRenderContext(): PageScriptRenderContext + { + return $this->scriptRenderContext; + } +} diff --git a/plugin/think-library/src/builder/page/render/PageScriptRenderContext.php b/plugin/think-library/src/builder/page/render/PageScriptRenderContext.php new file mode 100644 index 000000000..251aabacf --- /dev/null +++ b/plugin/think-library/src/builder/page/render/PageScriptRenderContext.php @@ -0,0 +1,25 @@ +jsEncoder)($value); + } +} diff --git a/plugin/think-library/src/builder/page/render/PageScriptRenderer.php b/plugin/think-library/src/builder/page/render/PageScriptRenderer.php new file mode 100644 index 000000000..d99c868e3 --- /dev/null +++ b/plugin/think-library/src/builder/page/render/PageScriptRenderer.php @@ -0,0 +1,24 @@ + $readyScripts + * @param array $scripts + */ + public function render(array $readyScripts, array $scripts): string + { + $ready = (new PageReadyScriptRenderer())->render( + (new PageBootScriptRenderer())->render($readyScripts), + ); + return $ready . (new PageCustomScriptRenderer())->render($scripts); + } +} diff --git a/plugin/think-library/src/builder/page/render/PageSearchFieldRendererFactory.php b/plugin/think-library/src/builder/page/render/PageSearchFieldRendererFactory.php new file mode 100644 index 000000000..fc51e62ad --- /dev/null +++ b/plugin/think-library/src/builder/page/render/PageSearchFieldRendererFactory.php @@ -0,0 +1,25 @@ + $field + */ + public function create(array $field): PageSearchFieldRendererInterface + { + return match (strtolower(strval($field['type'] ?? 'input'))) { + 'hidden' => new PageSearchHiddenFieldRenderer(), + 'select' => new PageSearchSelectFieldRenderer(), + 'submit' => new PageSearchSubmitFieldRenderer(), + default => new PageSearchInputFieldRenderer(), + }; + } +} diff --git a/plugin/think-library/src/builder/page/render/PageSearchFieldRendererInterface.php b/plugin/think-library/src/builder/page/render/PageSearchFieldRendererInterface.php new file mode 100644 index 000000000..84b370a07 --- /dev/null +++ b/plugin/think-library/src/builder/page/render/PageSearchFieldRendererInterface.php @@ -0,0 +1,17 @@ + $field + */ + public function render(array $field, PageSearchRenderContext $context): string; +} diff --git a/plugin/think-library/src/builder/page/render/PageSearchHiddenFieldRenderer.php b/plugin/think-library/src/builder/page/render/PageSearchHiddenFieldRenderer.php new file mode 100644 index 000000000..35be898d4 --- /dev/null +++ b/plugin/think-library/src/builder/page/render/PageSearchHiddenFieldRenderer.php @@ -0,0 +1,22 @@ +searchValue($name); + return sprintf('', $context->attrs($attrs)); + } +} diff --git a/plugin/think-library/src/builder/page/render/PageSearchInputFieldRenderer.php b/plugin/think-library/src/builder/page/render/PageSearchInputFieldRenderer.php new file mode 100644 index 000000000..ec918298c --- /dev/null +++ b/plugin/think-library/src/builder/page/render/PageSearchInputFieldRenderer.php @@ -0,0 +1,34 @@ +searchValue($name); + if (!array_key_exists('placeholder', $attrs)) { + $attrs['placeholder'] = $field['placeholder'] ?: ($label === '' ? '' : BuilderLang::format('请输入%s', [$label])); + } + $attrs = BuilderLang::attrs($attrs); + $attrs['class'] = $context->mergeClass(strval($attrs['class'] ?? ''), trim('layui-input ' . strval($field['class'] ?? ''))); + + return $this->renderItem( + $field, + '', + $context + ); + } +} diff --git a/plugin/think-library/src/builder/page/render/PageSearchNodeRenderer.php b/plugin/think-library/src/builder/page/render/PageSearchNodeRenderer.php new file mode 100644 index 000000000..9a65bcf55 --- /dev/null +++ b/plugin/think-library/src/builder/page/render/PageSearchNodeRenderer.php @@ -0,0 +1,19 @@ +invoke([$context, 'renderSearch'], $node); + } +} diff --git a/plugin/think-library/src/builder/page/render/PageSearchRenderContext.php b/plugin/think-library/src/builder/page/render/PageSearchRenderContext.php new file mode 100644 index 000000000..3d2e78ff4 --- /dev/null +++ b/plugin/think-library/src/builder/page/render/PageSearchRenderContext.php @@ -0,0 +1,43 @@ +): string $attrsRenderer + * @param callable(string): string $searchValueResolver + * @param callable(array): array $optionsResolver + */ + public function __construct( + callable $attrsRenderer, + private $searchValueResolver, + private $optionsResolver, + ) { + parent::__construct($attrsRenderer); + } + + public function searchValue(string $name): string + { + return ($this->searchValueResolver)($name); + } + + /** + * @param array $field + * @return array + */ + public function resolveOptions(array $field): array + { + return ($this->optionsResolver)($field); + } + +} diff --git a/plugin/think-library/src/builder/page/render/PageSearchRenderer.php b/plugin/think-library/src/builder/page/render/PageSearchRenderer.php new file mode 100644 index 000000000..6cd6e03ca --- /dev/null +++ b/plugin/think-library/src/builder/page/render/PageSearchRenderer.php @@ -0,0 +1,88 @@ +> $fields + * @param array $attrs + */ + public function render( + array $fields, + array $attrs, + string $tableId, + string $action, + string $legend, + bool $legendEnabled, + PageSearchRenderContext $context + ): string { + if (count($fields) < 1) { + return ''; + } + + $attrs = array_merge([ + 'action' => $action, + 'data-table-id' => $tableId, + 'autocomplete' => 'off', + 'method' => 'get', + 'onsubmit' => 'return false', + ], $attrs); + $attrs['class'] = $context->mergeClass(strval($attrs['class'] ?? ''), 'layui-form layui-form-pane form-search'); + + $items = []; + $hasSubmit = false; + $factory = new PageSearchFieldRendererFactory(); + foreach ($fields as $field) { + if (!is_array($field)) { + continue; + } + $field = $this->normalizeField($field); + $items[] = $factory->create($field)->render($field, $context); + $hasSubmit = $hasSubmit || strval($field['type'] ?? '') === 'submit'; + } + if (!$hasSubmit) { + $field = $this->normalizeField(['type' => 'submit', 'label' => BuilderLang::text('搜 索'), 'attrs' => []]); + $items[] = $factory->create($field)->render($field, $context); + } + + $html = ''; + if ($legendEnabled) { + $html .= '
    ' . $context->escape(BuilderLang::text($legend)) . ''; + } + $html .= sprintf('
    ', $context->attrs($attrs)); + $html .= "\n\t" . join("\n\t", array_filter($items)); + $html .= "\n
    "; + if ($legendEnabled) { + $html .= '
    '; + } + return $html; + } + + /** + * @param array $field + * @return array + */ + private function normalizeField(array $field): array + { + return array_merge([ + 'type' => 'input', + 'name' => '', + 'label' => '', + 'placeholder' => '', + 'attrs' => [], + 'options' => [], + 'source' => '', + 'class' => '', + 'wrapClass' => '', + ], $field); + } +} diff --git a/plugin/think-library/src/builder/page/render/PageSearchSelectFieldRenderer.php b/plugin/think-library/src/builder/page/render/PageSearchSelectFieldRenderer.php new file mode 100644 index 000000000..5c5eff5cb --- /dev/null +++ b/plugin/think-library/src/builder/page/render/PageSearchSelectFieldRenderer.php @@ -0,0 +1,48 @@ +mergeClass(strval($attrs['class'] ?? ''), trim('layui-select ' . strval($field['class'] ?? ''))); + + $control = '
    '; + + return $this->renderItem($field, $control, $context); + } + + /** + * @param array $field + */ + private function renderOptions(array $field, PageSearchRenderContext $context): string + { + $html = ''; + $current = $context->searchValue(strval($field['name'] ?? '')); + foreach ($context->resolveOptions($field) as $value => $label) { + $value = strval($value); + $selected = $current !== '' && $current === $value ? ' selected' : ''; + $html .= sprintf( + '%s', + $selected, + $context->escape($value), + $context->escape(BuilderLang::text(strval($label))) + ); + } + return $html; + } +} diff --git a/plugin/think-library/src/builder/page/render/PageSearchSubmitFieldRenderer.php b/plugin/think-library/src/builder/page/render/PageSearchSubmitFieldRenderer.php new file mode 100644 index 000000000..b9ce5705c --- /dev/null +++ b/plugin/think-library/src/builder/page/render/PageSearchSubmitFieldRenderer.php @@ -0,0 +1,26 @@ +mergeClass( + strval($attrs['class'] ?? ''), + trim('layui-btn layui-btn-primary ' . strval($field['class'] ?? '')) + ); + $label = strval($field['label'] ?? '') ?: BuilderLang::text('搜 索'); + + return '
    '; + } +} diff --git a/plugin/think-library/src/builder/page/render/PageShellRenderer.php b/plugin/think-library/src/builder/page/render/PageShellRenderer.php new file mode 100644 index 000000000..c7c1cb4ec --- /dev/null +++ b/plugin/think-library/src/builder/page/render/PageShellRenderer.php @@ -0,0 +1,46 @@ + $buttons + */ + public function render( + string $preset, + string $title, + array $buttons, + string $message, + string $contentClass, + string $content, + string $templates, + string $scripts, + string $schemaScript + ): string { + $header = (new PageHeaderRenderer())->render($title, $buttons); + $notice = (new PageNoticeRenderer())->render($message); + $contentHtml = (new PageContentRenderer())->render($notice, $contentClass, $content); + + $html = sprintf( + '
    ', + htmlentities($preset, ENT_QUOTES, 'UTF-8') + ); + if ($header !== '') { + $html .= "\n\t" . $header; + } + + $html .= "\n\t" . '
    '; + $html .= "\n\t" . $contentHtml; + $html .= $templates; + $html .= $scripts; + $html .= "\n\t" . $schemaScript; + return $html . "\n
    "; + } +} diff --git a/plugin/think-library/src/builder/page/render/PageTableInitScriptRenderer.php b/plugin/think-library/src/builder/page/render/PageTableInitScriptRenderer.php new file mode 100644 index 000000000..10617ba0f --- /dev/null +++ b/plugin/think-library/src/builder/page/render/PageTableInitScriptRenderer.php @@ -0,0 +1,20 @@ + $options + */ + public function render(string $tableId, array $options, PageScriptRenderContext $context): string + { + return " $('#" . addslashes($tableId) . "').layTable(" . $context->encodeJs($options) . ');'; + } +} diff --git a/plugin/think-library/src/builder/page/render/PageTableNodeRenderer.php b/plugin/think-library/src/builder/page/render/PageTableNodeRenderer.php new file mode 100644 index 000000000..2b2f95cf0 --- /dev/null +++ b/plugin/think-library/src/builder/page/render/PageTableNodeRenderer.php @@ -0,0 +1,19 @@ +invoke([$context, 'renderTable'], $node); + } +} diff --git a/plugin/think-library/src/builder/page/render/PageTableRenderContext.php b/plugin/think-library/src/builder/page/render/PageTableRenderContext.php new file mode 100644 index 000000000..4db6568f6 --- /dev/null +++ b/plugin/think-library/src/builder/page/render/PageTableRenderContext.php @@ -0,0 +1,24 @@ +): string $attrsRenderer + */ + public function __construct( + callable $attrsRenderer, + ) { + parent::__construct($attrsRenderer); + } +} diff --git a/plugin/think-library/src/builder/page/render/PageTableRenderer.php b/plugin/think-library/src/builder/page/render/PageTableRenderer.php new file mode 100644 index 000000000..e2697fcb9 --- /dev/null +++ b/plugin/think-library/src/builder/page/render/PageTableRenderer.php @@ -0,0 +1,20 @@ + $attrs + */ + public function render(array $attrs, PageTableRenderContext $context): string + { + return sprintf('
    ', $context->attrs($attrs)); + } +} diff --git a/plugin/think-library/src/builder/page/render/PageTemplateRenderer.php b/plugin/think-library/src/builder/page/render/PageTemplateRenderer.php new file mode 100644 index 000000000..06c4ab2a6 --- /dev/null +++ b/plugin/think-library/src/builder/page/render/PageTemplateRenderer.php @@ -0,0 +1,25 @@ + $templates + */ + public function render(array $templates): string + { + $html = ''; + $renderer = new PageTemplateScriptRenderer(); + foreach ($templates as $id => $tpl) { + $html .= $renderer->render((string)$id, $tpl); + } + return $html; + } +} diff --git a/plugin/think-library/src/builder/page/render/PageTemplateScriptRenderer.php b/plugin/think-library/src/builder/page/render/PageTemplateScriptRenderer.php new file mode 100644 index 000000000..60aac30b5 --- /dev/null +++ b/plugin/think-library/src/builder/page/render/PageTemplateScriptRenderer.php @@ -0,0 +1,23 @@ +\n%s\n", + BuilderAttributes::escape($id), + $template + ); + } +} diff --git a/plugin/think-library/src/builder/page/render/PageToolbarTemplateRenderer.php b/plugin/think-library/src/builder/page/render/PageToolbarTemplateRenderer.php new file mode 100644 index 000000000..9ec4403c3 --- /dev/null +++ b/plugin/think-library/src/builder/page/render/PageToolbarTemplateRenderer.php @@ -0,0 +1,25 @@ + $templates + * @param array $rowActions + * @return array + */ + public function render(array $templates, string $toolbarId, array $rowActions): array + { + if (count($rowActions) > 0 && !isset($templates[$toolbarId])) { + $templates[$toolbarId] = join("\n", $rowActions); + } + return $templates; + } +} diff --git a/plugin/think-library/src/common.php b/plugin/think-library/src/common.php new file mode 100644 index 000000000..469bf9ae7 --- /dev/null +++ b/plugin/think-library/src/common.php @@ -0,0 +1,569 @@ +init($rules, $type, $callable); + } +} + +if (!function_exists('_query')) { + /** + * 创建快捷查询构造器。 + * + * @param BaseQuery|Model|string $dbQuery 查询对象或模型名称 + * @param null|array|string $input 附加输入条件 + */ + function _query(BaseQuery|Model|string $dbQuery, array|string|null $input = null): QueryHelper + { + return QueryHelper::instance()->init($dbQuery, $input); + } +} + +if (!function_exists('sysvar')) { + /** + * 读写单次请求内的轻量级内存变量。 + * + * 仅用于当前请求周期内的临时缓存。 + * 传入空字符串 `('', '')` 时会清空全部缓存。 + * + * @param ?string $name 变量名 + * @param null|mixed $value 变量值 + * @return mixed + */ + function sysvar(?string $name = null, mixed $value = null) + { + static $swap = []; + + if ($name === '' && $value === '') { + return $swap = []; + } + if ($value === null) { + return $name === null ? $swap : ($swap[$name] ?? null); + } + + return $swap[$name] = $value; + } +} + +if (!function_exists('sysuri')) { + /** + * 生成系统页面 URL。 + * + * 参数与 ThinkPHP `url()` 保持一致, + * 但会在构建前先把后台页面地址标准化为短链目标。 + * + * @param string $url 路由地址 + * @param array $vars 路由参数 + * @param bool|string $suffix 后缀配置 + * @param bool|string $domain 域名配置 + */ + function sysuri(string $url = '', array $vars = [], bool|string $suffix = true, bool|string $domain = false): string + { + $target = Url::normalizeWebTarget($url); + return Library::$sapp->route->buildUrl($target, $vars)->suffix($suffix)->domain($domain)->build(); + } +} + +if (!function_exists('apiuri')) { + /** + * 生成标准插件 API URL。 + * + * 统一输出 `/api/{plugin}/{controller}/{action}` 风格地址, + * 并兼容当前插件上下文与 `controller/api/*` 的历史写法。 + * + * @param string $url 路由地址 + * @param array $vars 路由参数 + * @param bool|string $suffix 后缀配置 + * @param bool|string $domain 域名配置 + */ + function apiuri(string $url = '', array $vars = [], bool|string $suffix = true, bool|string $domain = false): string + { + $target = Url::normalizeApiTarget($url); + return Library::$sapp->route->buildUrl($target, $vars)->suffix($suffix)->domain($domain)->build(); + } +} + +if (!function_exists('tsession')) { + /** + * 获取令牌会话服务实例。 + */ + function tsession(): CacheSession + { + return CacheSession::instance(); + } +} + +if (!function_exists('encode')) { + /** + * 将 UTF-8 文本编码为兼容旧逻辑的短字符串。 + */ + function encode(string $content): string + { + $string = CodeToolkit::text2utf8($content); + $length = strlen($string); + if ($length === 0) { + return ''; + } + + $chars = ''; + for ($i = 0; $i < $length; ++$i) { + $chars .= str_pad(base_convert((string)ord($string[$i]), 10, 36), 2, '0', STR_PAD_LEFT); + } + + return $chars; + } +} + +if (!function_exists('decode')) { + /** + * 将 `encode()` 结果还原为 UTF-8 文本。 + */ + function decode(string $content): string + { + if ($content === '') { + return ''; + } + + $chars = ''; + foreach (str_split($content, 2) as $char) { + if (strlen($char) < 2) { + continue; + } + $chars .= chr((int)base_convert($char, 36, 10)); + } + + return CodeToolkit::text2utf8($chars); + } +} + +if (!function_exists('str2arr')) { + /** + * 将字符串或数组标准化为数组。 + * + * 字符串会按分隔符拆分;数组会递归展开。 + * 返回结果会自动去空白,并按需执行 allow 白名单过滤。 + * + * @param array|string $text 原始内容 + * @param string $separ 分隔符 + * @param ?array $allow 白名单限制 + */ + function str2arr(array|string $text, string $separ = ',', ?array $allow = null): array + { + $items = []; + + foreach ((array)$text as $item) { + if (is_array($item)) { + foreach (str2arr($item, $separ, $allow) as $value) { + $items[] = $value; + } + continue; + } + if (!is_scalar($item) || $item === false) { + continue; + } + if (is_string($item)) { + foreach (explode($separ, trim($item, $separ)) as $value) { + $value = trim($value); + if ($value !== '' && (!is_array($allow) || in_array($value, $allow, true))) { + $items[] = $value; + } + } + continue; + } + if (!is_array($allow) || in_array($item, $allow, true)) { + $items[] = $item; + } + } + + return $items; + } +} + +if (!function_exists('arr2str')) { + /** + * 将字符串或数组标准化为分隔字符串。 + * + * 内部会复用 `str2arr()` 做统一归一化, + * 最终输出形如 `,a,b,c,` 的历史兼容格式。 + * + * @param array|string $data 原始内容 + * @param string $separ 分隔符 + * @param ?array $allow 白名单限制 + */ + function arr2str(array|string $data, string $separ = ',', ?array $allow = null): string + { + $items = str2arr($data, $separ, $allow); + return empty($items) ? '' : $separ . join($separ, $items) . $separ; + } +} + +if (!function_exists('format_datetime')) { + /** + * 兼容旧版时间格式化函数。 + * + * @param mixed $value 原始时间值 + * @param string $format 输出格式 + */ + function format_datetime(mixed $value, string $format = 'Y-m-d H:i:s'): string + { + if ($value === null || $value === '' || $value === false) { + return '-'; + } + if (is_numeric($value)) { + $timestamp = intval($value); + } else { + $timestamp = strtotime(strval($value)) ?: 0; + } + return $timestamp > 0 ? date($format, $timestamp) : '-'; + } +} + +if (!function_exists('isDebug')) { + /** + * 判断当前是否处于调试模式。 + */ + function isDebug(): bool + { + return RuntimeService::isDebug(); + } +} + +if (!function_exists('isOnline')) { + /** + * 判断当前是否处于生产模式。 + */ + function isOnline(): bool + { + return RuntimeService::isOnline(); + } +} + +if (!function_exists('syspath')) { + /** + * 获取系统路径(兼容 Phar 的内部路径). + * + * 直接读取打包在 Phar 包内的文件。 + * - PHAR 环境:返回 phar:// 协议路径(只读) + * - 普通环境:返回实际文件系统路径 + * + * 示例:syspath('config') 读取 phar 包内的 config 目录 + * + * @param string $path 相对路径 + * @param ?string $root 自定义根路径(一般不传) + * @return string 完整的系统路径(Phar 内部路径或文件系统路径) + */ + function syspath(string $path = '', ?string $root = null): string + { + // 如果未提供 root,自动检测运行环境 + if ($root === null) { + // Phar::running(false) 返回物理路径(不含 phar://),避免出现 phar://phar:// 这种重复前缀 + $phar = Phar::running(false); + $root = $phar !== '' ? "phar://{$phar}" : Library::$sapp->getRootPath(); + } + + $root = rtrim(strval($root), '/\\'); + return $path === '' ? $root : "{$root}/" . ltrim(str_replace('\\', '/', $path), '/'); + } +} + +if (!function_exists('runpath')) { + /** + * 获取运行时路径(操作系统文件系统路径). + * + * 直接读取/写入操作系统的文件。 + * - PHAR 环境:返回 Phar 包外部的安装目录(可写) + * - 普通环境:返回项目根目录(可写) + * + * 示例:runpath('runtime') 读取/写入操作系统文件系统的 runtime 目录 + * + * @param string $path 相对路径 + * @return string 完整的运行时路径(操作系统文件系统路径) + */ + function runpath(string $path = ''): string + { + $phar = Phar::running(false); + $base = rtrim($phar !== '' ? dirname($phar) : Library::$sapp->getRootPath(), '/\\'); + return $path === '' ? $base : (($path === '/') ? $base : "{$base}/" . ltrim(str_replace('\\', '/', $path), '/')); + } +} + +if (!function_exists('is_phar')) { + /** + * 判断当前是否运行在 PHAR 环境中。 + */ + function is_phar(): bool + { + static $cache = null; + if ($cache === null) { + $cache = Phar::running() !== ''; + } + return $cache; + } +} + +if (!function_exists('enbase64url')) { + /** + * Base64 URL 安全编码。 + */ + function enbase64url(string $string): string + { + return CodeToolkit::enSafe64($string); + } +} + +if (!function_exists('debase64url')) { + /** + * Base64 URL 安全解码。 + */ + function debase64url(string $string): string + { + return CodeToolkit::deSafe64($string); + } +} + +if (!function_exists('xss_safe')) { + /** + * 对文本执行基础 XSS 安全处理。 + * + * 当前逻辑会移除 script 标签,并中和内联事件属性。 + */ + function xss_safe(string $text): string + { + $rules = [ + '#]*>.*?#is' => '', + '#(\s+)on([a-z_][\w:-]*\s*=)#i' => '$1data-on-$2', + ]; + + return preg_replace(array_keys($rules), array_values($rules), trim($text)) ?? trim($text); + } +} + +if (!function_exists('password_mask')) { + /** + * 返回编辑态密码占位符。 + * + * 默认固定为 6 个星号,前端展示统一使用该值, + * 用户保留全星号提交时可视为“不修改密码”。 + */ + function password_mask(int $length = 6): string + { + return str_repeat('*', max(1, $length)); + } +} + +if (!function_exists('password_is_mask')) { + /** + * 判断输入是否为纯星号密码占位。 + * + * 只要输入内容全部由 `*` 组成,且长度大于 0, + * 就认为它是保留旧密码的占位内容。 + */ + function password_is_mask(mixed $value): bool + { + $value = trim(strval($value)); + return $value !== '' && preg_match('/^\*+$/', $value) === 1; + } +} + +if (!function_exists('password_is_unchanged')) { + /** + * 判断编辑态密码输入是否表示“保持不变”。 + * + * 兼容两种历史/新标准: + * - 空字符串:历史“留空不修改” + * - 纯星号:新标准“保留星号不修改” + */ + function password_is_unchanged(mixed $value): bool + { + $value = trim(strval($value)); + return $value === '' || password_is_mask($value); + } +} + +if (!function_exists('http_get')) { + /** + * 发送 GET 请求。 + * + * @param string $url 请求地址 + * @param array|string $query 查询参数 + * @param array $options 客户端配置 + */ + function http_get(string $url, array|string $query = [], array $options = []): bool|string + { + return HttpClient::get($url, $query, $options); + } +} + +if (!function_exists('http_post')) { + /** + * 发送 POST 请求。 + * + * @param string $url 请求地址 + * @param array|string $data 提交数据 + * @param array $options 客户端配置 + */ + function http_post(string $url, array|string $data, array $options = []): bool|string + { + return HttpClient::post($url, $data, $options); + } +} + +if (!function_exists('data_save')) { + /** + * 按主键或条件执行增量保存。 + * + * @param Model|Query|string $dbQuery 查询对象或模型 + * @param array $data 保存数据 + * @param string $key 主键字段 + * @param null|array $where 附加条件 + * @throws Exception + */ + function data_save(Model|Query|string $dbQuery, array $data, string $key = 'id', ?array $where = []): bool|int + { + return AppService::save($dbQuery, $data, $key, $where); + } +} + +if (!function_exists('down_file')) { + /** + * 下载远程文件并返回本地访问地址。 + * + * @param string $source 源文件地址 + * @param bool $force 是否强制重下 + * @param int $expire 本地缓存秒数 + */ + function down_file(string $source, bool $force = false, int $expire = 0): string + { + return Storage::down($source, $force, $expire)['url'] ?? $source; + } +} + +if (!function_exists('trace_file')) { + /** + * 将异常信息落盘到 runtime/trace 目录。 + * + * @param Throwable $exception 异常对象 + */ + function trace_file(Throwable $exception): bool + { + $path = rtrim(Library::$sapp->getRuntimePath(), '\/') . DIRECTORY_SEPARATOR . 'trace'; + if (!is_dir($path) && !mkdir($path, 0777, true) && !is_dir($path)) { + return false; + } + + $root = strtr(rtrim(syspath(), '\/'), '\\', '/'); + $source = strtr($exception->getFile(), '\\', '/'); + $name = basename($source); + if ($root !== '' && str_starts_with(strtolower($source), strtolower($root . '/'))) { + $name = ltrim(substr($source, strlen($root)), '/'); + } + + $file = $path . DIRECTORY_SEPARATOR . date('Ymd_His_') . strtr($name, ['/' => '.', '\\' => '.']); + $json = json_encode( + $exception instanceof Exception ? $exception->getData() : [], + JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE + ) ?: '[]'; + $class = get_class($exception); + + return file_put_contents( + $file, + "[CODE] {$exception->getCode()}" . PHP_EOL + . "[INFO] {$exception->getMessage()}" . PHP_EOL + . ($exception instanceof Exception ? "[DATA] {$json}" . PHP_EOL : '') + . "[FILE] {$class} in {$name} line {$exception->getLine()}" . PHP_EOL + . '[TIME] ' . date('Y-m-d H:i:s') . PHP_EOL . PHP_EOL + . '[TRACE]' . PHP_EOL . $exception->getTraceAsString() + ) !== false; + } +} + +if (!function_exists('format_bytes')) { + /** + * 将字节数格式化为可读单位。 + * + * @param float|int|string $size 原始字节值 + */ + function format_bytes(float|int|string $size): string + { + if (is_numeric($size)) { + $size = (float)$size; + $units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; + for ($i = 0; $size >= 1024 && $i < count($units) - 1; ++$i) { + $size /= 1024; + } + + return round($size, 2) . ' ' . $units[$i]; + } + + return is_string($size) ? $size : strval($size); + } +} diff --git a/plugin/think-library/src/contract/QueueHandlerInterface.php b/plugin/think-library/src/contract/QueueHandlerInterface.php new file mode 100644 index 000000000..cbcdec0cc --- /dev/null +++ b/plugin/think-library/src/contract/QueueHandlerInterface.php @@ -0,0 +1,32 @@ + 0 && (int)$vo[$pkey] === (int)$value) { + $ids = array_merge($ids, static::getArrSubIds($list, (int)$vo[$ckey], $ckey, $pkey)); + } + } + return $ids; + } +} diff --git a/plugin/think-library/src/extend/CodeExtend.php b/plugin/think-library/src/extend/CodeExtend.php new file mode 100644 index 000000000..951398a44 --- /dev/null +++ b/plugin/think-library/src/extend/CodeExtend.php @@ -0,0 +1,28 @@ + $iv, 'value' => $value])); + } + + /** + * 生成随机编码。 + * `type` 仅允许数字、字母、数字字母三种规则,避免继续引入隐式分支。 + */ + public static function random(int $size = 10, int $type = 1, string $prefix = ''): string + { + $chars = self::alphabet($type); + $code = $prefix . $chars[mt_rand(1, strlen($chars) - 1)]; + while (strlen($code) < $size) { + $code .= $chars[mt_rand(0, strlen($chars) - 1)]; + } + return $code; + } + + /** + * Base64Url 安全编码。 + */ + public static function enSafe64(string $text): string + { + return rtrim(strtr(base64_encode($text), '+/', '-_'), '='); + } + + /** + * 数据解密处理。 + */ + public static function decrypt(string $data, string $skey): mixed + { + $attr = json_decode(static::deSafe64($data), true) ?: []; + return unserialize(openssl_decrypt((string)($attr['value'] ?? ''), 'AES-256-CBC', $skey, 0, (string)($attr['iv'] ?? ''))); + } + + /** + * Base64Url 安全解码。 + */ + public static function deSafe64(string $text): string + { + return base64_decode(str_pad(strtr($text, '-_', '+/'), (int)(ceil(strlen($text) / 4) * 4), '=')); + } + + /** + * 压缩数据对象。 + */ + public static function enzip(mixed $data): string + { + return static::enSafe64(gzcompress(serialize($data))); + } + + /** + * 解压数据对象。 + */ + public static function dezip(string $string): mixed + { + return unserialize(gzuncompress(static::deSafe64($string))); + } + + /** + * 尝试通过 BOM 判断文本编码。 + */ + private static function detectEncoding(string $text): string + { + [$first2, $first4] = [substr($text, 0, 2), substr($text, 0, 4)]; + if ($first4 === chr(0x00) . chr(0x00) . chr(0xFE) . chr(0xFF)) { + return 'UTF-32BE'; + } + if ($first4 === chr(0xFF) . chr(0xFE) . chr(0x00) . chr(0x00)) { + return 'UTF-32LE'; + } + if ($first2 === chr(0xFE) . chr(0xFF)) { + return 'UTF-16BE'; + } + if ($first2 === chr(0xFF) . chr(0xFE)) { + return 'UTF-16LE'; + } + return mb_detect_encoding($text) ?: 'UTF-8'; + } + + /** + * 根据规则选择随机字符集。 + */ + private static function alphabet(int $type): string + { + $numbers = '0123456789'; + $letters = 'abcdefghijklmnopqrstuvwxyz'; + if ($type === 1) { + return $numbers; + } + if ($type === 3) { + return $numbers . $letters; + } + return $letters; + } +} diff --git a/plugin/think-library/src/extend/DataExtend.php b/plugin/think-library/src/extend/DataExtend.php new file mode 100644 index 000000000..b73d51305 --- /dev/null +++ b/plugin/think-library/src/extend/DataExtend.php @@ -0,0 +1,28 @@ +isDir() || $ext === '' || strtolower($info->getExtension()) === strtolower($ext); + }, $short); + } + + /** + * 扫描目录并返回文件路径数组。 + */ + public static function find(string $path, ?int $depth = null, ?\Closure $filter = null, bool $short = true): array + { + [$info, $files] = [new \SplFileInfo($path), []]; + if (!$info->isDir() && !$info->isFile()) { + return $files; + } + if ($info->isFile() && ($filter === null || $filter($info) !== false)) { + $files[] = $short ? $info->getBasename() : $info->getPathname(); + } + if ($info->isDir()) { + foreach (static::findFilesYield($info->getPathname(), $depth, $filter) as $file) { + $files[] = $short ? self::relativePath($info->getPathname(), $file->getPathname()) : $file->getPathname(); + } + } + return $files; + } + + /** + * 递归扫描指定目录,返回文件或目录的 SplFileInfo 对象。 + */ + public static function findFilesYield(string $path, ?int $depth = null, ?\Closure $filter = null, bool $appendPath = false, int $currDepth = 1): \Generator + { + if (!file_exists($path) || !is_dir($path) || (!is_null($depth) && $currDepth > $depth)) { + return; + } + foreach (new \FilesystemIterator($path, \FilesystemIterator::SKIP_DOTS) as $item) { + if ($filter !== null && $filter($item) === false) { + continue; + } + if ($item->isDir() && !$item->isLink()) { + if ($appendPath) { + yield $item; + } + yield from static::findFilesYield($item->getPathname(), $depth, $filter, $appendPath, $currDepth + 1); + } else { + yield $item; + } + } + } + + /** + * 深度拷贝到指定目录。 + * `remove=true` 时会在复制后删除源文件,适合初始化发布场景。 + */ + public static function copy(string $frdir, string $todir, array $files = [], bool $force = true, bool $remove = true): bool + { + $frdir = self::normalizeDirectory($frdir); + $todir = self::normalizeDirectory($todir); + if (empty($files) && is_dir($frdir)) { + $files = static::find($frdir, null, static function (\SplFileInfo $info) { + return $info->getBasename()[0] !== '.'; + }); + } + foreach ($files as $target) { + [$fromPath, $destPath] = [$frdir . $target, $todir . $target]; + if ($force || !is_file($destPath)) { + is_dir($dir = dirname($destPath)) || mkdir($dir, 0777, true); + copy($fromPath, $destPath); + } + if ($remove && is_file($fromPath)) { + unlink($fromPath); + } + } + if ($remove) { + static::remove($frdir); + } + return true; + } + + /** + * 移除文件或清空目录。 + */ + public static function remove(string $path): bool + { + if (!file_exists($path)) { + return true; + } + if (is_file($path)) { + return unlink($path); + } + $dirs = [$path]; + iterator_to_array(self::findFilesYield($path, null, function (\SplFileInfo $file) use (&$dirs) { + $file->isDir() ? $dirs[] = $file->getPathname() : unlink($file->getPathname()); + })); + usort($dirs, static function ($a, $b) { + return strlen($b) <=> strlen($a); + }); + foreach ($dirs as $dir) { + file_exists($dir) && is_dir($dir) && rmdir($dir); + } + return !file_exists($path); + } + + /** + * 计算相对于根目录的短路径。 + */ + private static function relativePath(string $root, string $pathname): string + { + return substr($pathname, strlen($root) + 1); + } + + /** + * 统一目录分隔符并补尾部分隔符。 + */ + private static function normalizeDirectory(string $path): string + { + return rtrim($path, '\/') . DIRECTORY_SEPARATOR; + } +} diff --git a/plugin/think-library/src/extend/HttpClient.php b/plugin/think-library/src/extend/HttpClient.php new file mode 100644 index 000000000..08841e637 --- /dev/null +++ b/plugin/think-library/src/extend/HttpClient.php @@ -0,0 +1,176 @@ + $value) { + $lines[] = "--{$boundary}"; + $lines[] = "Content-Disposition: form-data; name=\"{$key}\""; + $lines[] = ''; + $lines[] = $value; + } + if (is_array($file) && isset($file['field'], $file['name'])) { + $lines[] = "--{$boundary}"; + $lines[] = "Content-Disposition: form-data; name=\"{$file['field']}\"; filename=\"{$file['name']}\""; + if (isset($file['type'])) { + $lines[] = "Content-Type: \"{$file['type']}\""; + } + $lines[] = ''; + $lines[] = $file['content']; + } + $lines[] = "--{$boundary}--"; + $header[] = "Content-type:multipart/form-data;boundary={$boundary}"; + return static::request($method, $url, [ + 'data' => join("\r\n", $lines), + 'returnHeader' => $returnHeader, + 'headers' => $header, + ]); + } + + /** + * 公共 cURL 参数。 + * 这些参数在整个项目里应该保持一致,避免不同调用点各自拼一套。 + */ + private static function applyCommonOptions(mixed $curl, array $options): void + { + curl_setopt($curl, CURLOPT_USERAGENT, $options['agent'] ?? self::getUserAgent()); + curl_setopt($curl, CURLOPT_AUTOREFERER, true); + curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true); + curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); + curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false); + curl_setopt($curl, CURLOPT_TIMEOUT, isset($options['timeout']) && is_numeric($options['timeout']) ? (int)$options['timeout'] : 60); + curl_setopt($curl, CURLOPT_HEADER, !empty($options['returnHeader'])); + } + + /** + * 获取浏览器代理信息。 + */ + private static function getUserAgent(): string + { + $agents = [ + 'Mozilla/5.0 (Windows NT 6.1; rv:2.0.1) Gecko/20100101 Firefox/4.0.1', + 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/536.11 (KHTML, like Gecko) Chrome/20.0.1132.57 Safari/536.11', + 'Mozilla/5.0 (Windows NT 10.0; WOW64; rv:38.0) Gecko/20100101 Firefox/38.0', + 'Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; .NET4.0C; .NET4.0E; .NET CLR 2.0.50727; .NET CLR 3.0.30729; .NET CLR 3.5.30729; InfoPath.3; rv:11.0) like Gecko', + 'Mozilla/5.0 (Windows; U; Windows NT 6.1; en-us) AppleWebKit/534.50 (KHTML, like Gecko) Version/5.1 Safari/534.50', + 'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0)', + 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:2.0.1) Gecko/20100101 Firefox/4.0.1', + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_0) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.56 Safari/535.11', + ]; + return $agents[array_rand($agents)]; + } + + /** + * 请求级参数设置。 + */ + private static function applyRequestOptions(mixed $curl, string $method, array $options): void + { + if (!empty($options['cookie'])) { + curl_setopt($curl, CURLOPT_COOKIE, $options['cookie']); + } + if (!empty($options['headers'])) { + curl_setopt($curl, CURLOPT_HTTPHEADER, $options['headers']); + } + if (!empty($options['cookie_file'])) { + curl_setopt($curl, CURLOPT_COOKIEJAR, $options['cookie_file']); + curl_setopt($curl, CURLOPT_COOKIEFILE, $options['cookie_file']); + } + $method = strtolower($method); + curl_setopt($curl, CURLOPT_CUSTOMREQUEST, strtoupper($method)); + if ($method === 'head') { + curl_setopt($curl, CURLOPT_NOBODY, true); + } elseif (array_key_exists('data', $options)) { + if ($method === 'post') { + curl_setopt($curl, CURLOPT_POST, true); + } + curl_setopt($curl, CURLOPT_POSTFIELDS, $options['data']); + } + if (isset($options['setopt']) && is_array($options['setopt'])) { + foreach ($options['setopt'] as $value) { + if (is_array($value)) { + curl_setopt($curl, ...$value); + } + } + } + } + + /** + * 给 URL 安全追加 query 参数。 + */ + private static function appendQuery(string $location, mixed $query): string + { + if (empty($query)) { + return $location; + } + $location .= str_contains($location, '?') ? '&' : '?'; + if (is_array($query)) { + return $location . http_build_query($query); + } + if (is_string($query)) { + return $location . $query; + } + return $location; + } +} diff --git a/plugin/think-library/src/extend/JsonRpcClient.php b/plugin/think-library/src/extend/JsonRpcClient.php new file mode 100644 index 000000000..935790e55 --- /dev/null +++ b/plugin/think-library/src/extend/JsonRpcClient.php @@ -0,0 +1,41 @@ +client = new JsonRpcHttpClient($proxy, $header); + } + + public function __call(string $method, array $params = []): mixed + { + return $this->client->{$method}(...$params); + } +} diff --git a/plugin/think-library/src/helper/DeleteHelper.php b/plugin/think-library/src/helper/DeleteHelper.php new file mode 100644 index 000000000..2a09e95e6 --- /dev/null +++ b/plugin/think-library/src/helper/DeleteHelper.php @@ -0,0 +1,105 @@ +getPk() ?: 'id'); + $value = $this->app->request->post($field); + + if (!empty($where)) { + $query->where($where); + } + if (!isset($where[$field]) && $value !== null && $value !== '') { + $query->whereIn($field, is_array($value) ? $value : str2arr(strval($value))); + } + + if ($this->class->callback('_delete_filter', $query, $where) === false) { + return false; + } + + if (empty($query->getOptions()['where'] ?? [])) { + $this->class->error('数据删除失败!'); + } + + $model = $query->getModel(); + $result = $this->deleteRecords($query, $model); + if ($result) { + if ($model instanceof \think\admin\Model) { + $model->onAdminDelete(strval($value)); + } + } + + if ($this->class->callback('_delete_result', $result) === false) { + return $result; + } + + if ($result !== false) { + $this->class->success('数据删除成功!', ''); + } else { + $this->class->error('数据删除失败!'); + } + } + + private function deleteRecords(Query $query, ?Model $model = null): bool + { + if ($model instanceof Model && $this->usesSoftDelete($model)) { + $result = false; + foreach ((clone $query)->select() as $item) { + $result = $item->delete() || $result; + } + return $result; + } + + return $query->delete() !== false; + } + + private function usesSoftDelete(Model $model): bool + { + $traits = []; + foreach ([get_class($model), ...class_parents($model)] as $class) { + $traits = array_merge($traits, class_uses($class) ?: []); + } + + return in_array(SoftDelete::class, array_values(array_unique($traits)), true) + && $model->getOption('deleteTime', 'delete_time') !== false; + } +} diff --git a/plugin/think-library/src/helper/FormHelper.php b/plugin/think-library/src/helper/FormHelper.php new file mode 100644 index 000000000..2db1f0657 --- /dev/null +++ b/plugin/think-library/src/helper/FormHelper.php @@ -0,0 +1,89 @@ +getPk() ?: 'id'); + $value = $edata[$field] ?? input($field); + if ($this->app->request->isGet()) { + if ($value !== null) { + $exist = $query->where([$field => $value])->where($where)->find(); + if ($exist instanceof Model) { + $exist = $exist->toArray(); + } + $edata = array_merge($edata, $exist ?: []); + } + if ($this->class->callback('_form_filter', $edata) !== false) { + $this->class->fetch($template, ['vo' => $edata]); + } else { + return $edata; + } + } + if ($this->app->request->isPost()) { + $edata = array_merge($this->app->request->post(), $edata); + if ($this->class->callback('_form_filter', $edata, $where) !== false) { + $result = AppService::save($query, $edata, $field, $where) !== false; + if ($this->class->callback('_form_result', $result, $edata) !== false) { + if ($result !== false) { + $this->class->success('数据保存成功!'); + } else { + $this->class->error('数据保存失败!'); + } + } + return $result; + } + } + } +} diff --git a/plugin/think-library/src/helper/QueryHelper.php b/plugin/think-library/src/helper/QueryHelper.php new file mode 100644 index 000000000..5bcb8c7f0 --- /dev/null +++ b/plugin/think-library/src/helper/QueryHelper.php @@ -0,0 +1,571 @@ +query = clone $this->query; + } + + /** + * QueryHelper 魔术方法调用. + * + * 支持链式调用 Query 类的所有方法 + * 如果方法名以 _ 开头或返回 Query 对象,则返回当前实例 + * + * @param string $name 调用方法名称 + * @param array $args 调用参数内容 + * @return $this|mixed 返回当前实例或查询结果 + */ + public function __call(string $name, array $args) + { + return static::make($this->query, $name, $args, function ($name, $args) { + if (is_callable($callable = [$this->query, $name])) { + $value = call_user_func_array($callable, $args); + if ($name[0] === '_' || $value instanceof $this->query) { + return $this; + } + return $value; + } + return $this; + }); + } + + /** + * 快捷助手调用钩子. + * + * 根据方法名调用对应的 Helper 类 + * 支持的快捷方法:mForm, mSave, mQuery, mDelete, mUpdate + * + * @param Model|Query|string $model 模型对象或查询 + * @param string $method 方法名称 + * @param array $args 方法参数 + * @param null|callable $nohook 未匹配时的回调函数 + * @return false|int|mixed|QueryHelper 返回处理结果 + */ + public static function make(Model|Query|string $model, string $method = 'init', array $args = [], ?callable $nohook = null): mixed + { + $hooks = [ + 'mForm' => [FormHelper::class, 'init'], + 'mSave' => [SaveHelper::class, 'init'], + 'mQuery' => [QueryHelper::class, 'init'], + 'mDelete' => [DeleteHelper::class, 'init'], + 'mUpdate' => [AppService::class, 'update'], + ]; + if (isset($hooks[$method])) { + [$class, $method] = $hooks[$method]; + return Container::getInstance()->invokeClass($class)->{$method}($model, ...$args); + } + return is_callable($nohook) ? $nohook($method, $args) : false; + } + + /** + * 获取当前数据库查询对象 + * + * @return Query 返回当前的 Query 对象 + */ + public function db(): Query + { + return $this->query; + } + + /** + * 初始化查询构建器. + * + * @param BaseQuery|Model|string $dbQuery 数据库查询对象或模型 + * @param null|array|string $input 输入数据(默认为空,自动从请求获取) + * @param null|callable $callable 初始化回调函数 + * @return $this + */ + public function init(BaseQuery|Model|string $dbQuery, array|string|null $input = null, ?callable $callable = null): QueryHelper + { + $this->input = $this->getInputData($input); + $this->query = $this->autoSortQuery($dbQuery); + is_callable($callable) && call_user_func($callable, $this, $this->query); + return $this; + } + + /** + * 绑定排序并返回查询对象 + * + * 自动根据请求参数绑定排序字段 + * 支持 POST 请求的拖拽排序功能 + * + * @param BaseQuery|Model|string $dbQuery 数据库查询对象或模型 + * @param string $field 默认排序字段 + * @return Query 返回处理后的 Query 对象 + * @throws \InvalidArgumentException 不支持的查询类型时抛出 + */ + public function autoSortQuery(BaseQuery|Model|string $dbQuery, string $field = 'sort'): Query + { + $query = QueryFactory::build($dbQuery); + if (!$query instanceof Query) { + throw new \InvalidArgumentException('QueryHelper only supports relational Query instances.'); + } + if ($this->app->request->isPost() && $this->app->request->post('action') === 'sort') { + SystemContext::instance()->isLogin() or $this->class->error('请重新登录!'); + if (method_exists($query, 'getTableFields') && in_array($field, $query->getTableFields(), true)) { + if ($this->app->request->has($pk = $query->getPk() ?: 'id', 'post')) { + $map = [$pk => $this->app->request->post($pk, 0)]; + $data = [$field => intval($this->app->request->post($field, 0))]; + try { + $query->newQuery()->where($map)->update($data); + } catch (\Throwable) { + $this->class->error('列表排序失败!'); + } + $this->class->success('列表排序成功!', ''); + } + } + $this->class->error('列表排序失败!'); + } + return $query; + } + + /** + * 设置 Like 查询条件. + * @param array|string $fields 查询字段 + * @param string $split 前后分割符 + * @param null|array|string $input 输入数据 + * @param string $alias 别名分割符 + * @return $this + */ + public function like(array|string $fields, string $split = '', array|string|null $input = null, string $alias = '#'): QueryHelper + { + $data = $this->getInputData($input ?: $this->input); + foreach (is_array($fields) ? $fields : explode(',', $fields) as $field) { + [$dk, $qk] = [$field, $field]; + if (stripos($field, $alias) !== false) { + [$dk, $qk] = explode($alias, $field); + } + if (isset($data[$qk]) && $data[$qk] !== '') { + $this->query->whereLike($dk, "%{$split}{$data[$qk]}{$split}%"); + } + } + return $this; + } + + /** + * 设置 Equal 查询条件. + * @param array|string $fields 查询字段 + * @param null|array|string $input 输入类型 + * @param string $alias 别名分割符 + * @return $this + */ + public function equal(array|string $fields, array|string|null $input = null, string $alias = '#'): QueryHelper + { + $data = $this->getInputData($input ?: $this->input); + foreach (is_array($fields) ? $fields : explode(',', $fields) as $field) { + [$dk, $qk] = [$field, $field]; + if (stripos($field, $alias) !== false) { + [$dk, $qk] = explode($alias, $field); + } + if (isset($data[$qk]) && $data[$qk] !== '') { + $this->query->where($dk, strval($data[$qk])); + } + } + return $this; + } + + /** + * 设置 IN 区间查询. + * @param array|string $fields 查询字段 + * @param string $split 输入分隔符 + * @param null|array|string $input 输入数据 + * @param string $alias 别名分割符 + * @return $this + */ + public function in(array|string $fields, string $split = ',', array|string|null $input = null, string $alias = '#'): QueryHelper + { + $data = $this->getInputData($input ?: $this->input); + foreach (is_array($fields) ? $fields : explode(',', $fields) as $field) { + [$dk, $qk] = [$field, $field]; + if (stripos($field, $alias) !== false) { + [$dk, $qk] = explode($alias, $field); + } + if (isset($data[$qk]) && $data[$qk] !== '') { + $this->query->whereIn($dk, explode($split, strval($data[$qk]))); + } + } + return $this; + } + + /** + * 两字段范围查询. + * @example field1:field2#field,field11:field22#field00 + * @param array|string $fields 查询字段 + * @param null|array|string $input 输入数据 + * @param string $alias 别名分割符 + * @return $this + */ + public function valueRange(array|string $fields, array|string|null $input = null, string $alias = '#'): QueryHelper + { + $data = $this->getInputData($input ?: $this->input); + foreach (is_array($fields) ? $fields : explode(',', $fields) as $field) { + if (str_contains($field, ':')) { + if (stripos($field, $alias) !== false) { + [$dk0, $qk0] = explode($alias, $field); + [$dk1, $dk2] = explode(':', $dk0); + } else { + [$qk0] = [$dk1, $dk2] = explode(':', $field, 2); + } + if (isset($data[$qk0]) && $data[$qk0] !== '') { + $this->query->where([[$dk1, '<=', $data[$qk0]], [$dk2, '>=', $data[$qk0]]]); + } + } + } + return $this; + } + + /** + * 设置内容区间查询. + * @param array|string $fields 查询字段 + * @param string $split 输入分隔符 + * @param null|array|string $input 输入数据 + * @param string $alias 别名分割符 + * @return $this + */ + public function valueBetween(array|string $fields, string $split = ' ', array|string|null $input = null, string $alias = '#'): QueryHelper + { + return $this->setBetweenWhere($fields, $split, $input, $alias); + } + + /** + * 设置日期时间区间查询. + * @param array|string $fields 查询字段 + * @param string $split 输入分隔符 + * @param null|array|string $input 输入数据 + * @param string $alias 别名分割符 + * @return $this + */ + public function dateBetween(array|string $fields, string $split = ' - ', array|string|null $input = null, string $alias = '#'): QueryHelper + { + return $this->setBetweenWhere($fields, $split, $input, $alias, static function ($value, $type) { + if (preg_match('#^\d{4}(-\d\d){2}\s+\d\d(:\d\d){2}$#', $value)) { + return $value; + } + return $type === 'after' ? "{$value} 23:59:59" : "{$value} 00:00:00"; + }); + } + + /** + * 仅查询已软删除的数据. + * @return $this + */ + public function onlyTrashed(): QueryHelper + { + $field = strval($this->query->getOption('deleteTime', 'delete_time')); + $condition = ['not null', '']; + $softDelete = $this->query->getOption('soft_delete'); + if (is_array($softDelete) && isset($softDelete[0])) { + $field = strval($softDelete[0]); + $softCondition = $softDelete[1] ?? null; + if (is_array($softCondition) && isset($softCondition[0])) { + $operator = strtolower(strval($softCondition[0])); + if ($operator === '=' && array_key_exists(1, $softCondition)) { + $condition = ['<>', $softCondition[1]]; + } + } + } + $this->query->useSoftDelete($field, $condition); + return $this; + } + + /** + * 设置时间戳区间查询. + * @param array|string $fields 查询字段 + * @param string $split 输入分隔符 + * @param null|array|string $input 输入数据 + * @param string $alias 别名分割符 + * @return $this + */ + public function timeBetween(array|string $fields, string $split = ' - ', array|string|null $input = null, string $alias = '#'): QueryHelper + { + return $this->setBetweenWhere($fields, $split, $input, $alias, static function ($value, $type) { + if (preg_match('#^\d{4}(-\d\d){2}\s+\d\d(:\d\d){2}$#', $value)) { + return strtotime($value); + } + return $type === 'after' ? strtotime("{$value} 23:59:59") : strtotime("{$value} 00:00:00"); + }); + } + + /** + * 清空数据并保留表结构. + * @return $this + */ + public function empty(): QueryHelper + { + $table = $this->query->getTable(); + $ctype = strtolower($this->query->getConfig('type')); + $connection = $this->query->getConnection(); + if ($ctype === 'mysql' && $connection instanceof PDOConnection) { + $connection->execute("truncate table `{$table}`"); + } elseif (in_array($ctype, ['sqlsrv', 'oracle', 'pgsql'], true) && $connection instanceof PDOConnection) { + $connection->execute("truncate table {$table}"); + } else { + try { + $this->query->newQuery()->whereRaw('1=1')->delete(); + } catch (\Throwable $exception) { + trace_file($exception); + } + } + return $this; + } + + /** + * 中间回调处理. + * @return $this + */ + public function filter(callable $after): QueryHelper + { + call_user_func($after, $this, $this->query); + return $this; + } + + /** + * 输出 Layui.Table 组件数据或普通列表 JSON。 + * @param ?callable $befor 表单前置操作 + * @param ?callable $after 表单后置操作 + * @param string $template 视图模板文件 + * @throws DataNotFoundException + * @throws DbException + * @throws ModelNotFoundException + */ + public function layTable(?callable $befor = null, ?callable $after = null, string $template = '') + { + if (in_array($this->output, ['get.json', 'get.layui.table'])) { + if (is_callable($after)) { + call_user_func($after, $this, $this->query); + } + if ($this->output === 'get.json') { + $this->applyOrderParams($this->query); + return $this->page(true, true, false, 0, $template); + } + $this->applyOrderParams($this->query); + $get = $this->app->request->get(); + if (empty($get['page']) || empty($get['limit'])) { + $data = $this->query->select()->toArray(); + $result = ['msg' => '', 'code' => 200, 'count' => count($data), 'data' => $data]; + } else { + $cfg = ['list_rows' => $get['limit'], 'query' => $get]; + $data = $this->query->paginate($cfg, self::getCount($this->query))->toArray(); + $result = ['msg' => '', 'code' => 200, 'count' => $data['total'], 'data' => $data['data']]; + } + if ($this->class->callback('_page_filter', $result['data'], $result) !== false) { + self::xssFilter($result['data']); + throw new HttpResponseException(json($result)->code(200)); + } + return $result; + } + if (is_callable($befor)) { + call_user_func($befor, $this, $this->query); + } + $this->class->fetch($template); + return null; + } + + /** + * 执行分页查询并按控制器约定渲染结果。 + * @param bool|int $page 是否启用分页 + * @param bool $display 是否渲染模板 + * @param bool|int $total 集合分页记录数 + * @param int $limit 集合每页记录数 + * @param string $template 模板文件名称 + * @throws DataNotFoundException + * @throws DbException + * @throws ModelNotFoundException + */ + public function page($page = true, bool $display = true, $total = false, int $limit = 0, string $template = ''): array + { + if ($page !== false) { + $get = $this->app->request->get(); + $limits = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160, 170, 180, 190, 200]; + if ($limit <= 1) { + $limit = intval($get['limit'] ?? 20); + if (!in_array($limit, $limits, true)) { + $limit = 20; + } + } + $inner = strpos($get['spm'] ?? '', 'm-') === 0; + $prefix = $inner ? (sysuri('system/index/index') . '#') : ''; + $config = ['list_rows' => $limit, 'query' => $get]; + if (is_numeric($page)) { + $config['page'] = $page; + } elseif (isset($get['page']) && is_numeric($get['page'])) { + $config['page'] = max(1, intval($get['page'])); + } + $data = ($paginate = $this->query->paginate($config, self::getCount($this->query, $total)))->toArray(); + $result = ['page' => ['limit' => $data['per_page'], 'total' => $data['total'], 'pages' => $data['last_page'], 'current' => $data['current_page']], 'list' => $data['data']]; + $select = "", $data['last_page'], $data['current_page']]); + $link = $inner ? str_replace('render() ?: '') : ($paginate->render() ?: ''); + $this->class->assign('pagehtml', "
    {$html}{$link}
    "); + } else { + $result = ['list' => $this->query->select()->toArray()]; + } + if ($this->class->callback('_page_filter', $result['list'], $result) !== false && $display) { + if ($this->output === 'get.json') { + $this->class->success('JSON-DATA', $result); + } else { + $this->class->fetch($template, $result); + } + } + return $result; + } + + /** + * 获取输入数据. + * @param null|array|string $input + */ + private function getInputData($input): array + { + if (is_array($input)) { + return $input; + } + $input = $input ?: 'request'; + return $this->app->request->{$input}(); + } + + /** + * 设置区域查询条件. + * @param array|string $fields 查询字段 + * @param string $split 输入分隔符 + * @param null|array|string $input 输入数据 + * @param string $alias 别名分割符 + * @param null|callable $callback 回调函数 + * @return $this + */ + private function setBetweenWhere($fields, string $split = ' ', $input = null, string $alias = '#', ?callable $callback = null): QueryHelper + { + $data = $this->getInputData($input ?: $this->input); + foreach (is_array($fields) ? $fields : explode(',', $fields) as $field) { + [$dk, $qk] = [$field, $field]; + if (stripos($field, $alias) !== false) { + [$dk, $qk] = explode($alias, $field); + } + if (isset($data[$qk]) && $data[$qk] !== '') { + [$begin, $after] = explode($split, strval($data[$qk])); + if (is_callable($callback)) { + $after = call_user_func($callback, $after, 'after'); + $begin = call_user_func($callback, $begin, 'begin'); + } + $this->query->whereBetween($dk, [$begin, $after]); + } + } + return $this; + } + + /** + * 根据查询参数补充排序规则。 + */ + private function applyOrderParams(Query $query): void + { + $get = $this->app->request->get(); + if (isset($get['_field_'], $get['_order_'])) { + $query->order("{$get['_field_']} {$get['_order_']}"); + } + } + + /** + * 查询对象数量统计。 + * @param bool|int $total + * @return bool|int|string + * @throws DbException + */ + private static function getCount(Query $query, $total = false) + { + if ($total === true || is_numeric($total)) { + return $total; + } + [$query, $options] = [clone $query, $query->getOptions()]; + if (isset($options['order'])) { + $query->removeOption('order'); + } + Library::$sapp->db->trigger('think_before_page_count', $query); + if (empty($options['union'])) { + return $query->count(); + } + $table = [$query->buildSql() => '_union_count_']; + return $query->newQuery()->table($table)->count(); + } + + /** + * 输出 XSS 过滤处理。 + */ + private static function xssFilter(array &$items): void + { + foreach ($items as &$item) { + if (is_array($item)) { + self::xssFilter($item); + } elseif (is_string($item)) { + $item = htmlspecialchars($item, ENT_QUOTES); + } + } + } +} diff --git a/plugin/think-library/src/helper/SaveHelper.php b/plugin/think-library/src/helper/SaveHelper.php new file mode 100644 index 000000000..a2bedbe8e --- /dev/null +++ b/plugin/think-library/src/helper/SaveHelper.php @@ -0,0 +1,85 @@ +getPk() ?: 'id'); + $edata = $edata ?: $this->app->request->post(); + $value = $this->app->request->post($field); + + // 主键限制处理 + if (!isset($where[$field]) && !is_null($value)) { + $query->whereIn($field, str2arr(strval($value))); + if (isset($edata)) { + unset($edata[$field]); + } + } + + // 前置回调处理 + if ($this->class->callback('_save_filter', $query, $edata) === false) { + return false; + } + + // 检查原始数据 + $query->master()->where($where)->update($edata); + + // 模型自定义事件回调 + $model = $query->getModel(); + if ($model instanceof \think\admin\Model) { + $model->onAdminSave(strval($value)); + } + + // 结果回调处理 + $result = true; + if ($this->class->callback('_save_result', $result, $model) === false) { + return $result; + } + + // 回复前端结果 + $this->class->success('数据保存成功!', ''); + } +} diff --git a/plugin/think-library/src/helper/ValidateHelper.php b/plugin/think-library/src/helper/ValidateHelper.php new file mode 100644 index 000000000..41dda65d4 --- /dev/null +++ b/plugin/think-library/src/helper/ValidateHelper.php @@ -0,0 +1,88 @@ + message // 最大值限定 + * - age.between:1,120 => message // 范围限定 + * - name.require => message // 必填内容 + * - name.default => 100 // 获取并设置默认值 + * - region.value => value // 固定字段数值内容 + * + * @param array $rules 验证规则数组(键为字段。规则,值为错误提示) + * @param array|string $input 输入内容(默认为空,自动从请求获取,可指定 post/get 等) + * @param null|callable $callable 验证失败时的回调函数(接收错误信息和数据) + * @return array 验证通过的数据数组 + */ + public function init(array $rules, array|string $input = '', ?callable $callable = null): array + { + if (is_string($input)) { + $type = trim($input, '.') ?: 'param'; + $input = $this->app->request->{$type}(); + } + [$data, $rule, $info] = [[], [], []]; + foreach ($rules as $key => $value) { + if (is_numeric($key)) { + [$key, $alias] = explode('#', "{$value}#"); + $data[$key] = $input[$alias ?: $key] ?? null; + } elseif (!str_contains($key, '.')) { + $data[$key] = $value; + } elseif (preg_match('|^(.*?)\.(.*?)#(.*?)#?$|', "{$key}#", $matches)) { + [, $_key, $_rule, $alias] = $matches; + if (in_array($_rule, ['value', 'default'])) { + if ($_rule === 'value') { + $data[$_key] = $value; + } + if ($_rule === 'default') { + $data[$_key] = $input[$alias ?: $_key] ?? $value; + } + } else { + $info[explode(':', "{$_key}.{$_rule}")[0]] = $value; + $data[$_key] = $data[$_key] ?? ($input[$alias ?: $_key] ?? null); + $rule[$_key] = isset($rule[$_key]) ? "{$rule[$_key]}|{$_rule}" : $_rule; + } + } + } + $validate = new Validate(); + if ($validate->rule($rule)->message($info)->check($data)) { + return $data; + } + if (is_callable($callable)) { + return call_user_func($callable, lang($validate->getError()), $data); + } + $this->class->error(lang($validate->getError())); + } +} diff --git a/plugin/think-library/src/middleware/MultAccess.php b/plugin/think-library/src/middleware/MultAccess.php new file mode 100644 index 000000000..bc26c258c --- /dev/null +++ b/plugin/think-library/src/middleware/MultAccess.php @@ -0,0 +1,250 @@ +app = $app; + } + + /** + * 多应用解析. + */ + public function handle(Request $request, \Closure $next): Response + { + [$this->appPath, $this->appSpace] = ['', '']; + if (!$this->parseMultiApp()) { + return $next($request); + } + + return $this->app->middleware->pipeline('app')->send($request)->then(fn ($request) => $next($request)); + } + + /** + * 调度优先级: + * 1. 显式插件前缀 + * 2. 显式本地 app 前缀 + * 3. 根路由声明的目标应用 + * 4. 动态插件切换 + * 5. 默认本地 app. + */ + private function parseMultiApp(): bool + { + $request = $this->app->request; + $pathinfo = $request->pathinfo(); + + if ($plugin = AppService::matchPluginPath($pathinfo)) { + return $this->applyPlugin($plugin, true); + } + + if ($local = AppService::matchPath($pathinfo)) { + return $this->applyLocal($local, true); + } + + if ($target = $this->resolveGlobalRouteTarget($request)) { + return ($target['type'] ?? '') === 'plugin' + ? $this->applyPlugin($target, false) + : $this->applyLocal($target, false); + } + + $switch = AppService::detectPluginSwitch($request); + if ($switch && ($plugin = AppService::resolvePlugin($switch))) { + $plugin['entry'] = RequestContext::ENTRY_WEB; + $plugin['matched_prefix'] = ''; + $plugin['pathinfo'] = $pathinfo; + return $this->applyPlugin($plugin, false); + } + + RequestContext::instance()->setEntryType(RequestContext::ENTRY_WEB); + return $this->setMultiApp(AppService::defaultAppCode(), true); + } + + /** + * 应用插件调度结果. + * + * @param array $plugin + */ + private function applyPlugin(array $plugin, bool $stripPrefix): bool + { + [$this->appPath, $this->appSpace] = [strval($plugin['path'] ?? ''), strval($plugin['space'] ?? '')]; + RequestContext::instance()->setEntryType(strval($plugin['entry'] ?? RequestContext::ENTRY_WEB)); + + $prefix = trim(strval($plugin['matched_prefix'] ?? ''), '\/'); + if ($stripPrefix && $prefix !== '') { + $root = strval($plugin['entry'] ?? '') === RequestContext::ENTRY_API + ? '/' . trim(AppService::pluginApiPrefix() . '/' . $prefix, '/') + : '/' . $prefix; + $this->app->request->setRoot($root); + $this->app->request->setPathinfo(strval($plugin['pathinfo'] ?? '')); + } + + return $this->setMultiApp(strval($plugin['code'] ?? ''), true, $prefix); + } + + /** + * 设置应用参数. + */ + private function setMultiApp(string $appName, bool $appBind, string $prefix = ''): bool + { + if ($appName === '') { + return false; + } + + $app = AppService::get($appName); + ($app['type'] ?? '') === 'plugin' ? AppService::activatePlugin($appName, $prefix) : AppService::activatePlugin(); + + if (empty($this->appPath) && $app) { + [$this->appPath, $this->appSpace] = [strval($app['path'] ?? ''), strval($app['space'] ?? '')]; + } + + if (!is_dir($this->appPath)) { + return false; + } + + $this->app->setNamespace($this->appSpace ?: NodeService::space($appName))->setAppPath($this->appPath); + $this->app->http->setBind($appBind)->name($appName)->path($this->appPath)->setRoutePath($this->appPath . 'route' . DIRECTORY_SEPARATOR); + + $replace = $this->app->config->get('view.tpl_replace_string', []); + $replace = is_array($replace) ? $replace : []; + $replace = array_filter($replace, static function ($value, $key) { + return is_string($key) && (is_scalar($value) || $value === null); + }, ARRAY_FILTER_USE_BOTH); + $replace = array_map(static fn ($value) => strval($value ?? ''), $replace); + $uris = array_merge($replace, AppService::uris()); + $viewConfig = [ + 'view_path' => $this->appPath . 'view' . DIRECTORY_SEPARATOR, + 'tpl_replace_string' => $uris, + ]; + $this->app->config->set($viewConfig, 'view'); + $this->app->make(View::class)->engine()->config($viewConfig); + + return $this->loadMultiApp($this->appPath); + } + + /** + * 加载应用文件. + * @codeCoverageIgnore + */ + private function loadMultiApp(string $appPath): bool + { + [$ext, $fmaps] = [$this->app->getConfigExt(), []]; + + if (is_file($file = "{$appPath}common{$ext}")) { + Library::load($file); + } + + FileTools::find($appPath . 'config', 1, function (\SplFileInfo $info) use ($ext, &$fmaps) { + if ($info->isFile() && strtolower(".{$info->getExtension()}") === $ext) { + $name = $info->getBasename($ext); + $fmaps[] = $name; + $this->app->config->load($info->getPathname(), $name); + } + }); + + if ((in_array('route', $fmaps, true) || is_dir($appPath . 'route')) && method_exists($this->app->route, 'reload')) { + $this->app->route->reload(); + } + + if (is_file($file = "{$appPath}provider{$ext}")) { + $this->app->bind(include $file); + } + + if (is_file($file = "{$appPath}event{$ext}")) { + $this->app->loadEvent(include $file); + } + + if (is_file($file = "{$appPath}middleware{$ext}")) { + $this->app->middleware->import(include $file, 'app'); + } + + if (method_exists($this->app->lang, 'switchLangSet')) { + $this->app->lang->switchLangSet($this->app->lang->getLangSet()); + } + + return true; + } + + /** + * 应用本地 app 调度结果. + * + * @param array $local + */ + private function applyLocal(array $local, bool $stripPrefix): bool + { + [$this->appPath, $this->appSpace] = [strval($local['path'] ?? ''), strval($local['space'] ?? '')]; + RequestContext::instance()->setEntryType(strval($local['entry'] ?? RequestContext::ENTRY_WEB)); + + $prefix = trim(strval($local['matched_prefix'] ?? ''), '\/'); + if ($stripPrefix && $prefix !== '') { + $this->app->request->setRoot('/' . $prefix); + $this->app->request->setPathinfo(strval($local['pathinfo'] ?? '')); + } + + return $this->setMultiApp(strval($local['code'] ?? ''), true); + } + + /** + * 从根路由预解析目标应用。 + * + * @return null|array + */ + private function resolveGlobalRouteTarget(Request $request): ?array + { + $route = $this->app->route; + if (!$route instanceof AdminRoute) { + return null; + } + + return $route->resolveTarget($request, $this->app->getRootPath() . 'route' . DIRECTORY_SEPARATOR); + } +} diff --git a/plugin/think-library/src/model/ModelFactory.php b/plugin/think-library/src/model/ModelFactory.php new file mode 100644 index 000000000..7c55d219c --- /dev/null +++ b/plugin/think-library/src/model/ModelFactory.php @@ -0,0 +1,50 @@ +db->table($query); + } else { + return self::triggerBeforeEvent(ModelFactory::build($query)->db()); + } + } + if ($query instanceof Model) { + return self::triggerBeforeEvent($query->db()); + } + if ($query instanceof BaseQuery && !$query->getModel()) { + // 子查询不挂载模型,实体表查询则补齐运行时模型。 + if (!self::isSubquery($query->getTable())) { + $name = $query->getConfig('name') ?: ''; + if (is_string($name) && strlen($name) > 0) { + $name = config("database.connections.{$name}") ? $name : ''; + } + $query->model(ModelFactory::build($query->getName(), [], $name)); + } + } + return self::triggerBeforeEvent($query); + } + + /** + * 判断是否为子查询 SQL。 + */ + private static function isSubquery(string $sql): bool + { + return preg_match('/^\(?\s*select\s+/i', $sql) > 0; + } + + /** + * 触发查询执行前事件。 + * @param BaseQuery|mixed|Model $query + * @return BaseQuery|mixed|Model + */ + private static function triggerBeforeEvent(mixed $query): mixed + { + Library::$sapp->db->trigger('think_before_event', $query); + return $query; + } +} diff --git a/plugin/think-library/src/model/RuntimeModel.php b/plugin/think-library/src/model/RuntimeModel.php new file mode 100644 index 000000000..76e557e9b --- /dev/null +++ b/plugin/think-library/src/model/RuntimeModel.php @@ -0,0 +1,57 @@ +runtimeName = $data; + $this->runtimeConnection = is_string($name) && $connection === '' ? $name : $connection; + $data = is_array($name) || is_object($name) ? $name : []; + } else { + $this->runtimeName = is_string($name) ? $name : ''; + $this->runtimeConnection = $connection; + } + + parent::__construct($data); + } + + protected function getBaseOptions(): array + { + $options = ['name' => $this->runtimeName]; + if ($this->runtimeConnection !== '') { + $options['connection'] = $this->runtimeConnection; + } + + return $options; + } +} diff --git a/plugin/think-library/src/model/SystemConfig.php b/plugin/think-library/src/model/SystemConfig.php new file mode 100644 index 000000000..8740bcd1b --- /dev/null +++ b/plugin/think-library/src/model/SystemConfig.php @@ -0,0 +1,19 @@ + + */ + private array $loadedPaths = []; + + /** + * 重载路由配置. + * @return $this + */ + public function reload(): Route + { + $this->config = array_merge($this->config, $this->app->config->get('route')); + return $this; + } + + /** + * 注册绑定到本地 app 的根路由。 + */ + public function bindApp(string $rule, mixed $route, string $app, string $method = '*'): RuleItem + { + return $this->bindTarget($rule, $route, [self::OPTION_APP => $app], $method); + } + + /** + * 注册带目标声明的根路由。 + * 这里的 $route 必须是目标应用内部的相对控制器地址,而不是带模块前缀的旧写法。 + * + * @param string $rule 路由规则 + * @param mixed $route 路由地址 + * @param array $target 目标声明 + * @param string $method 请求方法 + */ + public function bindTarget(string $rule, mixed $route, array $target, string $method = '*'): RuleItem + { + $item = $this->rule($rule, $route, $method); + $option = $this->normalizeTargetOptions($target); + return empty($option) ? $item : $item->option($option); + } + + /** + * 注册绑定到插件的根路由。 + */ + public function bindPlugin( + string $rule, + mixed $route, + string $plugin, + string $entry = RequestContext::ENTRY_WEB, + string $method = '*' + ): RuleItem { + return $this->bindTarget($rule, $route, [ + self::OPTION_PLUGIN => $plugin, + self::OPTION_ENTRY => $entry, + ], $method); + } + + /** + * 注册绑定到本地 app 的根路由分组。 + */ + public function appGroup(string $app, \Closure|string $name, mixed $route = null): RuleGroup + { + return $this->groupTarget([self::OPTION_APP => $app], $name, $route); + } + + /** + * 注册带目标声明的根路由分组。 + * + * @param array $target + */ + public function groupTarget(array $target, \Closure|string $name, mixed $route = null): RuleGroup + { + $group = $this->group($name, $route); + $option = $this->normalizeTargetOptions($target); + return empty($option) ? $group : $group->option($option); + } + + /** + * 注册绑定到插件的根路由分组。 + * 当第二个参数是 Closure 时,第三个参数可直接传 api/web 入口类型。 + */ + public function pluginGroup( + string $plugin, + \Closure|string $name, + mixed $route = null, + string $entry = RequestContext::ENTRY_WEB + ): RuleGroup { + if ($name instanceof \Closure && in_array($route, [RequestContext::ENTRY_WEB, RequestContext::ENTRY_API], true)) { + $entry = $route; + $route = null; + } + + return $this->groupTarget([ + self::OPTION_PLUGIN => $plugin, + self::OPTION_ENTRY => $entry, + ], $name, $route); + } + + /** + * 预解析根路由目标,在实际路由调度前先确定要绑定的应用。 + * + * @return null|array + */ + public function resolveTarget(Request $request, string $routePath): ?array + { + if (!$this->app->config->get('app.with_route', true) || !$this->loadPath($routePath)) { + return null; + } + + $this->request = $request; + $this->host = $request->host(true); + + try { + $dispatch = $this->check( + $this->normalizePathinfo($request), + boolval($this->config['route_complete_match'] ?? true) + ); + } catch (RouteNotFoundException $exception) { + return null; + } + + if (!$dispatch) { + return null; + } + + return $this->extractDispatchTarget($dispatch, $request->pathinfo()); + } + + /** + * 标准化根路由目标声明。 + * + * @param array $target + * @return array + */ + private function normalizeTargetOptions(array $target): array + { + $option = []; + + $app = trim(strval($target[self::OPTION_APP] ?? '')); + if ($app !== '') { + $option[self::OPTION_APP] = $app; + } + + $plugin = trim(strval($target[self::OPTION_PLUGIN] ?? '')); + if ($plugin !== '') { + $option[self::OPTION_PLUGIN] = $plugin; + } + + $entry = trim(strval($target[self::OPTION_ENTRY] ?? '')); + if (in_array($entry, [RequestContext::ENTRY_WEB, RequestContext::ENTRY_API], true)) { + $option[self::OPTION_ENTRY] = $entry; + } + + return $option; + } + + /** + * 按目录加载路由文件,并做一次进程级缓存,避免重复 include。 + */ + private function loadPath(string $routePath): bool + { + $routePath = rtrim($routePath, '\/') . DIRECTORY_SEPARATOR; + if (array_key_exists($routePath, $this->loadedPaths)) { + return $this->loadedPaths[$routePath]; + } + + $files = is_dir($routePath) ? (glob($routePath . '*.php') ?: []) : []; + if (empty($files)) { + return $this->loadedPaths[$routePath] = false; + } + + foreach ($files as $file) { + include $file; + } + + return $this->loadedPaths[$routePath] = true; + } + + /** + * 规范化当前请求 pathinfo,使其与框架路由检测逻辑一致。 + */ + private function normalizePathinfo(Request $request): string + { + $pathinfo = trim($request->pathinfo(), '\/'); + $suffix = $this->config['url_html_suffix'] ?? 'html'; + + if ($suffix === false) { + $path = $pathinfo; + } elseif (!empty($suffix)) { + $path = preg_replace('/\.(' . preg_quote(ltrim(strval($suffix), '.'), '/') . ')$/i', '', $pathinfo) ?: $pathinfo; + } else { + $ext = $request->ext(); + $path = $ext === '' ? $pathinfo : (preg_replace('/\.' . preg_quote($ext, '/') . '$/i', '', $pathinfo) ?: $pathinfo); + } + + return str_replace(strval($this->config['pathinfo_depr'] ?? '/'), '|', $path); + } + + /** + * 从命中的根路由中提取目标应用声明。 + * + * @return null|array + */ + private function extractDispatchTarget(object $dispatch, string $pathinfo): ?array + { + $option = $this->dispatchOptions($dispatch); + $target = trim($this->dispatchTarget($dispatch), '\/'); + + $pluginCode = trim(strval($option[self::OPTION_PLUGIN] ?? $option['plugin'] ?? '')); + if ($pluginCode !== '' && ($plugin = AppService::resolvePlugin($pluginCode))) { + $plugin['type'] = 'plugin'; + $plugin['entry'] = $this->detectPluginEntry($dispatch, $option, $target); + $plugin['matched_prefix'] = ''; + $plugin['pathinfo'] = $pathinfo; + return $plugin; + } + + $appCode = trim(strval($option[self::OPTION_APP] ?? $option['app'] ?? $option['module'] ?? '')); + if ($appCode !== '' && ($local = AppService::localApp($appCode))) { + $local['type'] = 'local'; + $local['entry'] = RequestContext::ENTRY_WEB; + $local['matched_prefix'] = ''; + $local['pathinfo'] = $pathinfo; + return $local; + } + + // 兼容旧三段式全局路由:system/login/index、index/demo/index。 + // 新代码仍应优先使用 bindApp/bindPlugin 显式声明目标,避免把目标选择耦合到路由字符串首段。 + if ($target !== '' && count($parts = array_values(array_filter(explode('/', $target), 'strlen'))) >= 3) { + $code = trim(strval($parts[0])); + $inner = join('/', array_slice($parts, 1)); + + if ($code !== '' && ($plugin = AppService::resolvePlugin($code))) { + $plugin['type'] = 'plugin'; + $plugin['entry'] = $this->detectPluginEntry($dispatch, $option, $inner); + $plugin['matched_prefix'] = ''; + $plugin['pathinfo'] = $pathinfo; + return $plugin; + } + + if ($code !== '' && ($local = AppService::localApp($code))) { + $local['type'] = 'local'; + $local['entry'] = RequestContext::ENTRY_WEB; + $local['matched_prefix'] = ''; + $local['pathinfo'] = $pathinfo; + return $local; + } + } + + return null; + } + + /** + * 读取命中路由的 option 参数。 + * + * @return array + */ + private function dispatchOptions(object $dispatch): array + { + return (array)\Closure::bind(function (): array { + if (isset($this->rule) && method_exists($this->rule, 'getOption')) { + return (array)$this->rule->getOption(); + } + return $this->option ?? []; + }, $dispatch, get_class($dispatch))(); + } + + /** + * 读取命中路由的最终调度目标。 + */ + private function dispatchTarget(object $dispatch): string + { + $target = method_exists($dispatch, 'getDispatch') ? $dispatch->getDispatch() : ''; + return is_array($target) ? join('/', array_map('strval', $target)) : strval($target); + } + + /** + * 推断插件根路由绑定的入口类型。 + * + * @param array $option + */ + private function detectPluginEntry(object $dispatch, array $option, string $target = ''): string + { + $entry = trim(strval($option[self::OPTION_ENTRY] ?? $option['entry'] ?? '')); + if (in_array($entry, [RequestContext::ENTRY_API, RequestContext::ENTRY_WEB], true)) { + return $entry; + } + + return preg_match('#^api([/.]|$)#i', trim($target ?: $this->dispatchTarget($dispatch), '\/')) + ? RequestContext::ENTRY_API + : RequestContext::ENTRY_WEB; + } +} diff --git a/plugin/think-library/src/route/Url.php b/plugin/think-library/src/route/Url.php new file mode 100644 index 000000000..85c43762e --- /dev/null +++ b/plugin/think-library/src/route/Url.php @@ -0,0 +1,532 @@ + +// +---------------------------------------------------------------------- +// 以下代码来自 topthink/think-multi-app,有部分修改以兼容 ThinkAdmin 的需求 +// +---------------------------------------------------------------------- + +declare(strict_types=1); +/** + * +---------------------------------------------------------------------- + * | ThinkAdmin Plugin for ThinkAdminDeveloper + * +---------------------------------------------------------------------- + * | Copyright (c) 2014~2026 ThinkAdmin [ thinkadmin.top ] + * +---------------------------------------------------------------------- + * | Official Website: https://thinkadmin.top + * +---------------------------------------------------------------------- + * | Licensed: https://mit-license.org + * | Disclaimer: https://thinkadmin.top/disclaimer + * | Vip Rights: https://thinkadmin.top/vip-introduce + * +---------------------------------------------------------------------- + * | Gitee Repository: https://gitee.com/zoujingli/ThinkAdmin + * | Github Repository: https://github.com/zoujingli/ThinkAdmin + * +---------------------------------------------------------------------- + */ + +namespace think\admin\route; + +use think\admin\Library; +use think\admin\runtime\RequestContext; +use think\admin\service\AppService; +use think\admin\service\NodeService; +use think\helper\Str; +use think\route\Url as ThinkUrl; + +/** + * 多应用 URL 生成与解析. + * @class Url + */ +class Url extends ThinkUrl +{ + /** + * 将后台页面地址标准化为短链目标。 + */ + public static function normalizeWebTarget(string $url): string + { + if ( + preg_match('#^(?:https?://|@|\[)#', $url) + || (strpos($url, '@') !== false && strpos($url, '\\') === false) + ) { + return $url; + } + + $info = parse_url($url); + if (!is_array($info)) { + return $url; + } + + $path = strval($info['path'] ?? ''); + if ($path === '' && $url !== '') { + return $url; + } + + $absolute = str_starts_with($path, '/'); + $segments = array_values(array_filter(explode('/', trim(str_replace('\\', '/', $path), '/')), 'strlen')); + if (!$absolute && count($segments) < 3) { + $map = [ + Library::$sapp->http->getName(), + Library::$sapp->request->controller(), + Library::$sapp->request->action(true), + ]; + while (count($segments) < 3) { + array_unshift($segments, $map[2 - count($segments)] ?? 'index'); + } + } + + if (isset($segments[0])) { + $segments[0] = Str::lower($segments[0]); + } + if (isset($segments[1])) { + $segments[1] = Str::snake($segments[1]); + } + + $segments = self::shrinkWebSegments($segments, false); + $target = join('/', $segments); + $target = $target === '' ? '/' : '/' . ltrim($target, '/'); + + if (isset($info['query']) && $info['query'] !== '') { + $target .= '?' . $info['query']; + } + if (isset($info['fragment']) && $info['fragment'] !== '') { + $target .= '#' . $info['fragment']; + } + + return $target; + } + + /** + * 将 API 地址标准化为统一目标。 + */ + public static function normalizeApiTarget(string $url): string + { + if ( + preg_match('#^(?:https?://|@|\[)#', $url) + || (strpos($url, '@') !== false && strpos($url, '\\') === false) + ) { + return $url; + } + + $info = parse_url($url); + if (!is_array($info)) { + return $url; + } + + $path = strval($info['path'] ?? ''); + if ($path === '' && $url !== '') { + return $url; + } + + $absolute = str_starts_with($path, '/'); + $segments = array_values(array_filter(explode('/', trim(str_replace('\\', '/', $path), '/')), 'strlen')); + $apiPrefix = trim(AppService::pluginApiPrefix(), '/'); + if ($apiPrefix !== '' && strcasecmp(strval($segments[0] ?? ''), $apiPrefix) === 0) { + array_shift($segments); + } + + [$module, $controller, $action] = self::resolveApiSegments($segments, $absolute); + $target = '/' . trim("{$apiPrefix}/{$module}/{$controller}/{$action}", '/'); + + if (isset($info['query']) && $info['query'] !== '') { + $target .= '?' . $info['query']; + } + if (isset($info['fragment']) && $info['fragment'] !== '') { + $target .= '#' . $info['fragment']; + } + + return $target; + } + + /** + * Build URL. + */ + public function build(): string + { + $url = $this->url; + $vars = $this->vars; + $domain = $this->domain; + $suffix = $this->suffix; + $request = $this->app->request; + if (strpos($url, '[') === 0 && $pos = strpos($url, ']')) { + // [name] 表示使用路由命名标识生成URL + $name = substr($url, 1, $pos - 1); + $url = 'name' . substr($url, $pos + 1); + } + if (strpos($url, '://') === false && strpos($url, '/') !== 0) { + $info = parse_url($url); + $url = !empty($info['path']) ? $info['path'] : ''; + if (isset($info['fragment'])) { + // 解析锚点 + $anchor = $info['fragment']; + if (strpos($anchor, '?') !== false) { + // 解析参数 + [$anchor, $info['query']] = explode('?', $anchor, 2); + } + if (strpos($anchor, '@') !== false) { + // 解析域名 + [$anchor, $domain] = explode('@', $anchor, 2); + } + } elseif (strpos($url, '@') && strpos($url, '\\') === false) { + // 解析域名 + [$url, $domain] = explode('@', $url, 2); + } + } + if ($url) { + $checkDomain = $domain && is_string($domain) ? $domain : null; + $checkName = $name ?? $url . (isset($info['query']) ? '?' . $info['query'] : ''); + $rule = $this->route->getName($checkName, $checkDomain); + if (empty($rule) && isset($info['query'])) { + $rule = $this->route->getName($url, $checkDomain); + parse_str($info['query'], $params); + $vars = array_merge($params, $vars); + unset($info['query']); + } + } + if (!empty($rule) && $match = $this->getRuleUrl($rule, $vars, $domain)) { + $url = $match[0]; + if ($domain && !empty($match[1])) { + $domain = $match[1]; + } + if (!is_null($match[2])) { + $suffix = $match[2]; + } + if (!$this->app->http->isBind()) { + $url = $this->app->http->getName() . '/' . $url; + } + } elseif (!empty($rule) && isset($name)) { + throw new \InvalidArgumentException('route name not exists:' . $name); + } else { + // 检测URL绑定 + $bind = $this->route->getDomainBind($domain && is_string($domain) ? $domain : null); + if ($bind && strpos($url, $bind) === 0) { + $url = substr($url, strlen($bind) + 1); + } + // 路由标识不存在 直接解析 + $url = $this->parseUrl($url, $domain); + if (isset($info['query'])) { + // 解析地址里面参数 合并到vars + parse_str($info['query'], $params); + $vars = array_merge($params, $vars); + } + } + // 还原 URL 分隔符 + $file = $request->baseFile(); + $depr = $this->route->config('pathinfo_depr'); + [$uri, $url] = [$request->url(), str_replace('/', $depr, $url)]; + if ($file && strpos($uri, $file) !== 0) { + $file = str_replace('\\', '/', dirname($file)); + // 内置服务器常见入口是 project-root + public/router.php -> public/index.php。 + // 这时真实业务路由位于站点根路径,不能再额外拼接 /public 前缀。 + if (basename($file) === 'public') { + $file = str_replace('\\', '/', dirname($file)); + } + $file = in_array($file, ['.', '/', '\\'], true) ? '' : $file; + } + /* + * 插件优先模式下,公开访问路径始终以 URL 前缀区分应用。 + * 这里不再沿用旧多应用模式的“去掉当前应用前缀”或“强制回落 index.php”逻辑, + * 否则会导致插件绝对链接丢失前缀,或者在 Worker 环境生成不可访问的 /index.php/... 地址。 + */ + $path = self::normalizeWebPath(ltrim($url, '/')); + $url = rtrim($file, '/') . '/' . ltrim($path, '/'); + // URL后缀 + if (substr($url, -1) == '/' || $url == '') { + $suffix = ''; + } else { + $suffix = $this->parseSuffix($suffix); + } + // 锚点 + $anchor = !empty($anchor) ? '#' . $anchor : ''; + // 参数组装 + if (!empty($vars)) { + // 添加参数 + if ($this->route->config('url_common_param')) { + $vars = http_build_query($vars); + $url .= $suffix . '?' . $vars . $anchor; + } else { + foreach ($vars as $var => $val) { + if ('' !== ($val = (string)$val)) { + $url .= $depr . $var . $depr . urlencode($val); + } + } + $url .= $suffix . $anchor; + } + } else { + $url .= $suffix . $anchor; + } + // 检测域名 + $domain = $this->parseDomain($url, $domain); + // URL 组装 + return $domain . rtrim($this->root, '/') . '/' . ltrim($url, '/'); + } + + /** + * 直接解析 URL 地址 + * @param string $url URL + * @param bool|string $domain Domain + */ + protected function parseUrl(string $url, bool|string &$domain): string + { + $request = $this->app->request; + if (str_starts_with($url, '/')) { + $url = substr($url, 1); + } elseif (str_contains($url, '\\')) { + $url = ltrim(str_replace('\\', '/', $url), '/'); + } elseif (str_starts_with($url, '@')) { + $url = substr($url, 1); + } else { + $attrs = str2arr($url, '/'); + $action = empty($attrs) ? $request->action() : array_pop($attrs); + $contrl = empty($attrs) ? $request->controller() : array_pop($attrs); + $module = empty($attrs) ? $this->app->http->getName() : array_pop($attrs); + // 拼装新的链接地址 + $url = NodeService::nameTolower($contrl) . '/' . $action; + if ($plugin = AppService::resolvePlugin($module)) { + $prefix = AppService::pluginPrefix($plugin['code']); + $url = ($prefix ?: $plugin['code']) . '/' . $url; + } elseif ($module !== AppService::defaultAppCode()) { + $url = $module . '/' . $url; + } + } + return $url; + } + + /** + * 标准页面路径短链压缩。 + */ + private static function normalizeWebPath(string $path): string + { + $path = trim(str_replace('\\', '/', $path), '/'); + if ($path === '') { + return ''; + } + + $segments = array_values(array_filter(explode('/', $path), 'strlen')); + if ($segments === []) { + return ''; + } + + if (isset($segments[0])) { + $segments[0] = self::normalizeWebPrefix($segments[0]); + } + + return join('/', self::shrinkWebSegments($segments, true)); + } + + /** + * 将插件编码或别名统一映射为当前生效前缀。 + */ + private static function normalizeWebPrefix(string $segment): string + { + $segment = Str::lower(trim($segment)); + if ($segment === '') { + return ''; + } + + if ($prefix = self::configuredPluginPrefix($segment)) { + return $prefix; + } + + $plugin = AppService::resolvePlugin($segment); + if ($plugin === null) { + return $segment; + } + + $prefix = trim(strval(AppService::pluginPrefix(strval($plugin['code'] ?? ''))), '/'); + return $prefix !== '' ? Str::lower($prefix) : $segment; + } + + /** + * 在插件元数据未初始化时,直接从运行时配置读取绑定前缀。 + */ + private static function configuredPluginPrefix(string $code): string + { + $bindings = (array)Library::$sapp->config->get('app.plugin.bindings', []); + if (array_key_exists($code, $bindings)) { + return self::normalizeConfiguredPrefix($bindings[$code]); + } + + foreach ($bindings as $item) { + if (!is_array($item)) { + continue; + } + + $name = Str::lower(strval($item['code'] ?? $item['plugin'] ?? '')); + if ($name === $code) { + return self::normalizeConfiguredPrefix($item['prefixes'] ?? ($item['prefix'] ?? [])); + } + } + + return ''; + } + + /** + * 标准化运行时配置中的插件前缀。 + */ + private static function normalizeConfiguredPrefix(mixed $value): string + { + foreach ((array)$value as $prefix) { + $prefix = Str::lower(trim(strval($prefix), " \t\n\r\0\x0B\\/")); + if ($prefix === '') { + continue; + } + if (str_contains($prefix, '/')) { + $prefix = strstr($prefix, '/', true) ?: $prefix; + } + if (str_contains($prefix, '.')) { + $prefix = strstr($prefix, '.', true) ?: $prefix; + } + if ($prefix !== '') { + return $prefix; + } + } + + return ''; + } + + /** + * 解析 API 目标片段。 + * + * @param array $segments + * @return array{0:string,1:string,2:string} + */ + private static function resolveApiSegments(array $segments, bool $absolute): array + { + $module = RequestContext::instance()->pluginCode() ?: (Library::$sapp->http->getName() ?: AppService::defaultAppCode()); + $controller = Library::$sapp->request->controller(); + $action = Library::$sapp->request->action(true); + + if ($absolute) { + if (count($segments) >= 3) { + $module = array_shift($segments) ?: $module; + $controller = array_shift($segments) ?: $controller; + $action = join('/', $segments) ?: $action; + } elseif (count($segments) === 2) { + $module = $segments[0]; + $controller = $segments[1]; + $action = 'index'; + } elseif (count($segments) === 1) { + $module = $segments[0]; + $controller = 'index'; + $action = 'index'; + } + } else { + if (count($segments) >= 3) { + $module = array_shift($segments) ?: $module; + $controller = array_shift($segments) ?: $controller; + $action = join('/', $segments) ?: $action; + } elseif (count($segments) === 2) { + [$controller, $action] = $segments; + } elseif (count($segments) === 1) { + $action = $segments[0]; + } + } + + return [ + Str::lower(trim(str_replace('\\', '/', strval($module)), '/')) ?: AppService::defaultAppCode(), + self::normalizeApiController(strval($controller)), + trim(str_replace('\\', '/', strval($action)), '/') ?: 'index', + ]; + } + + /** + * 标准化 API 控制器路径。 + */ + private static function normalizeApiController(string $controller): string + { + $controller = trim(str_replace(['.', '\\'], '/', $controller), '/'); + $segments = array_values(array_filter(explode('/', $controller), 'strlen')); + if (($segments[0] ?? '') !== '' && strcasecmp($segments[0], 'api') === 0) { + array_shift($segments); + } + + return join('/', array_map(static function (string $segment): string { + return Str::snake($segment); + }, $segments)) ?: 'index'; + } + + /** + * 判断是否属于标准应用/插件页面路径。 + * + * @param array $segments + */ + private static function supportsShortWebPath(array $segments): bool + { + $first = Str::lower(strval($segments[0] ?? '')); + if ($first === '') { + return false; + } + + if ($first === Str::lower(AppService::defaultAppCode() ?: 'index')) { + return true; + } + + if (AppService::localApp($first) !== null) { + return true; + } + + if (AppService::resolvePluginPrefix($first) !== null) { + return true; + } + + return AppService::resolvePlugin($first) !== null; + } + + /** + * 按默认应用/控制器/方法收缩页面路径。 + * + * @param array $segments + * @return array + */ + private static function shrinkWebSegments(array $segments, bool $strict): array + { + if ($segments === []) { + return []; + } + + $apiPrefix = trim(AppService::pluginApiPrefix(), '/'); + if ($apiPrefix !== '' && strcasecmp($segments[0], $apiPrefix) === 0) { + return $segments; + } + + if ($strict && !self::supportsShortWebPath($segments)) { + return $segments; + } + + $defaultAction = Str::lower(strval(Library::$sapp->route->config('default_action') ?: 'index')); + $defaultController = Str::snake(strval(Library::$sapp->route->config('default_controller') ?: 'index')); + $defaultApp = Str::lower(AppService::defaultAppCode() ?: 'index'); + + if (count($segments) >= 2 && Str::lower(end($segments) ?: '') === $defaultAction) { + array_pop($segments); + } + + if (count($segments) === 2 && Str::snake($segments[1]) === $defaultController) { + array_pop($segments); + } + + if (count($segments) === 1 && Str::lower($segments[0]) === $defaultApp) { + array_pop($segments); + } + + return $segments; + } +} diff --git a/plugin/think-library/src/runtime/NullSystemContext.php b/plugin/think-library/src/runtime/NullSystemContext.php new file mode 100644 index 000000000..63f9d5c73 --- /dev/null +++ b/plugin/think-library/src/runtime/NullSystemContext.php @@ -0,0 +1,113 @@ + + */ + private array $currentUser = []; + + /** + * 当前后台令牌。 + */ + private string $currentToken = ''; + + /** + * 当前认证会话编号。 + */ + private string $currentSessionId = ''; + + /** + * 当前认证失败状态码。 + */ + private int $authFailureStatus = 0; + + /** + * 当前认证失败标识。 + */ + private string $authFailureError = ''; + + /** + * 当前认证失败消息。 + */ + private string $authFailureInfo = ''; + + /** + * 当前请求是否已经完成认证判定。 + */ + private bool $authReady = false; + + /** + * 当前请求令牌是否已初始化。 + */ + private bool $requestTokensReady = false; + + /** + * 当前请求 Authorization Bearer 令牌。 + */ + private string $authorizationToken = ''; + + /** + * 当前请求系统鉴权令牌。 + */ + private string $systemRequestToken = ''; + + /** + * 当前请求账号鉴权令牌。 + */ + private string $accountRequestToken = ''; + + /** + * 当前请求系统认证 Cookie 令牌。 + */ + private string $systemCookieToken = ''; + + /** + * 当前请求账号认证 Cookie 令牌。 + */ + private string $accountCookieToken = ''; + + /** + * 获取请求上下文实例。 + */ + public static function instance(): self + { + return self::$instance ??= new self(); + } + + /** + * 清理当前请求上下文。 + */ + public static function clear(): void + { + self::$instance = new self(); + } + + /** + * 清理当前插件信息。 + */ + public function clearPlugin(): self + { + return $this->setPlugin(); + } + + /** + * 设置当前插件信息。 + */ + public function setPlugin(string $code = '', string $prefix = ''): self + { + $this->pluginCode = trim($code); + $this->pluginPrefix = trim($prefix, '\/'); + return $this; + } + + /** + * 设置当前入口类型。 + */ + public function setEntryType(string $entryType = self::ENTRY_WEB): self + { + $this->entryType = in_array($entryType, [self::ENTRY_WEB, self::ENTRY_API], true) ? $entryType : self::ENTRY_WEB; + return $this; + } + + /** + * 获取当前插件编码。 + */ + public function pluginCode(): string + { + return $this->pluginCode; + } + + /** + * 获取当前插件前缀。 + */ + public function pluginPrefix(): string + { + return $this->pluginPrefix; + } + + /** + * 获取当前请求入口类型。 + */ + public function entryType(): string + { + return $this->entryType; + } + + /** + * 判断是否为 API 入口。 + */ + public function isApiEntry(): bool + { + return $this->entryType === self::ENTRY_API; + } + + /** + * 仅更新当前令牌。 + */ + public function setToken(string $token): self + { + $this->currentToken = $token; + return $this; + } + + /** + * 清理当前认证信息。 + */ + public function clearAuth(bool $ready = false): self + { + $this->clearAuthFailure(); + $this->setSessionId(''); + return $this->setAuth([], '', $ready); + } + + /** + * 设置当前认证会话编号。 + */ + public function setSessionId(string $sessionId = ''): self + { + $this->currentSessionId = trim($sessionId); + return $this; + } + + /** + * 更新当前认证信息。 + * @param array $user + */ + public function setAuth(array $user = [], string $token = '', bool $ready = true): self + { + $this->clearAuthFailure(); + $this->authReady = $ready; + $this->currentUser = $user; + $this->currentToken = $token; + return $this; + } + + /** + * 记录当前认证失败信息。 + */ + public function setAuthFailure(int $status = 0, string $info = '', string $error = ''): self + { + $this->authFailureStatus = max(0, $status); + $this->authFailureInfo = trim($info); + $this->authFailureError = trim($error); + return $this; + } + + /** + * 清理当前认证失败信息。 + */ + public function clearAuthFailure(): self + { + $this->authFailureStatus = 0; + $this->authFailureInfo = ''; + $this->authFailureError = ''; + return $this; + } + + /** + * 更新当前请求令牌解析结果。 + */ + public function setRequestTokens( + string $authorization = '', + string $system = '', + string $account = '', + string $systemCookie = '', + string $accountCookie = '' + ): self { + $this->requestTokensReady = true; + $this->authorizationToken = trim($authorization); + $this->systemRequestToken = trim($system); + $this->accountRequestToken = trim($account); + $this->systemCookieToken = trim($systemCookie); + $this->accountCookieToken = trim($accountCookie); + return $this; + } + + /** + * 清理当前请求令牌解析结果。 + */ + public function clearRequestTokens(): self + { + $this->requestTokensReady = false; + $this->authorizationToken = ''; + $this->systemRequestToken = ''; + $this->accountRequestToken = ''; + $this->systemCookieToken = ''; + $this->accountCookieToken = ''; + return $this; + } + + /** + * 获取当前后台用户。 + * @return array + */ + public function user(): array + { + return $this->currentUser; + } + + /** + * 获取当前后台令牌。 + */ + public function token(): string + { + return $this->currentToken; + } + + /** + * 获取当前认证会话编号。 + */ + public function sessionId(): string + { + return $this->currentSessionId; + } + + /** + * 当前请求是否已完成认证判定。 + */ + public function authReady(): bool + { + return $this->authReady; + } + + /** + * 获取当前认证失败状态码。 + */ + public function authFailureStatus(): int + { + return $this->authFailureStatus; + } + + /** + * 获取当前认证失败标识。 + */ + public function authFailureError(): string + { + return $this->authFailureError; + } + + /** + * 获取当前认证失败消息。 + */ + public function authFailureInfo(): string + { + return $this->authFailureInfo; + } + + /** + * 获取当前认证失败信息。 + * @return array{status:int,error:string,info:string} + */ + public function authFailure(): array + { + return [ + 'status' => $this->authFailureStatus(), + 'error' => $this->authFailureError(), + 'info' => $this->authFailureInfo(), + ]; + } + + /** + * 当前请求令牌是否已初始化。 + */ + public function requestTokensReady(): bool + { + return $this->requestTokensReady; + } + + /** + * 获取当前请求 Authorization Bearer 令牌。 + */ + public function authorizationToken(): string + { + return $this->authorizationToken; + } + + /** + * 获取当前请求系统鉴权令牌。 + */ + public function systemRequestToken(): string + { + return $this->systemRequestToken; + } + + /** + * 获取当前请求账号鉴权令牌。 + */ + public function accountRequestToken(): string + { + return $this->accountRequestToken; + } + + /** + * 获取当前请求系统认证 Cookie 令牌。 + */ + public function systemCookieToken(): string + { + return $this->systemCookieToken; + } + + /** + * 获取当前请求账号认证 Cookie 令牌。 + */ + public function accountCookieToken(): string + { + return $this->accountCookieToken; + } +} diff --git a/plugin/think-library/src/runtime/RequestTokenService.php b/plugin/think-library/src/runtime/RequestTokenService.php new file mode 100644 index 000000000..62438a4c2 --- /dev/null +++ b/plugin/think-library/src/runtime/RequestTokenService.php @@ -0,0 +1,278 @@ +systemRequestToken(); + } + + /** + * 初始化并返回当前请求令牌上下文。 + */ + public static function capture(?Request $request = null): RequestContext + { + $context = RequestContext::instance(); + if ($context->requestTokensReady()) { + return $context; + } + + $request = $request ?: Library::$sapp->request; + $authorization = static::parseHeaderToken(strval($request->header(SystemContext::getTokenHeader(), ''))); + $systemCookie = static::decodeCookieToken(strval($request->cookie(SystemContext::getTokenCookie(), ''))); + $accountCookie = static::decodeCookieToken(strval($request->cookie(static::getAccountTokenCookie(), ''))); + $system = ''; + $account = ''; + + if ($authorization !== '') { + if (static::isSystemToken($authorization)) { + $system = $authorization; + } elseif (static::isAccountToken($authorization)) { + $account = $authorization; + } + } else { + $system = $systemCookie; + $account = $accountCookie; + } + + return $context->setRequestTokens($authorization, $system, $account, $systemCookie, $accountCookie); + } + + /** + * 解析标准认证头。 + */ + public static function parseHeaderToken(string $authorization): string + { + $authorization = trim($authorization); + if (!preg_match('/^Bearer\s+(.+)$/i', $authorization, $matches)) { + return ''; + } + + return static::normalizeToken($matches[1]); + } + + public static function normalizeToken(string $token): string + { + $token = trim($token); + if ($token === '') { + return ''; + } + + if (preg_match('/^Bearer\s+(.+)$/i', $token, $matches)) { + $token = trim($matches[1]); + } + + return preg_replace('/\s+/', '', $token) ?: ''; + } + + /** + * 解码 Cookie 中的认证令牌。 + * 新版本优先解析加密前缀,旧明文 Cookie 继续兼容。 + */ + public static function decodeCookieToken(string $token): string + { + $token = trim($token); + if ($token === '') { + return ''; + } + + if (str_contains($token, '%')) { + $token = rawurldecode($token); + } + + if (stripos($token, self::COOKIE_TOKEN_PREFIX) === 0) { + try { + return static::normalizeToken(strval(CodeToolkit::decrypt(substr($token, strlen(self::COOKIE_TOKEN_PREFIX)), static::cookieTokenSecret()))); + } catch (\Throwable) { + return ''; + } + } + + return static::normalizeToken($token); + } + + /** + * 获取账号认证 Cookie 名称。 + */ + public static function getAccountTokenCookie(): string + { + $cookie = trim(strval(config('app.account_token_cookie') ?: self::ACCOUNT_TOKEN_COOKIE)); + return $cookie !== '' ? $cookie : self::ACCOUNT_TOKEN_COOKIE; + } + + /** + * 获取当前请求账号鉴权令牌。 + */ + public static function accountToken(?Request $request = null): string + { + return static::capture($request)->accountRequestToken(); + } + + /** + * 清理当前请求中的系统鉴权令牌。 + */ + public static function forgetSystem(?Request $request = null): void + { + $request = $request ?: Library::$sapp->request; + $context = static::capture($request); + $authorization = $context->authorizationToken(); + $account = $context->accountRequestToken(); + $systemCookie = $context->systemCookieToken(); + $accountCookie = $context->accountCookieToken(); + $system = $context->systemRequestToken(); + + if ($system !== '' && $system === $authorization) { + $headers = $request->header(); + unset($headers['authorization'], $headers['Authorization']); + $request->withHeader($headers); + + $server = $_SERVER; + unset($server['HTTP_AUTHORIZATION'], $server['REDIRECT_HTTP_AUTHORIZATION']); + $request->withServer($server); + $authorization = ''; + } + + if ($system !== '' && $system === $systemCookie) { + $cookies = $request->cookie(); + unset($cookies[SystemContext::getTokenCookie()]); + $request->withCookie($cookies); + $systemCookie = ''; + } + + $context->setRequestTokens($authorization, '', $account, $systemCookie, $accountCookie); + } + + /** + * 获取当前请求 Authorization Bearer 令牌。 + */ + public static function authorizationToken(?Request $request = null): string + { + return static::capture($request)->authorizationToken(); + } + + /** + * 编码要写入 Cookie 的认证令牌。 + * Header 仍保持标准 Bearer 原文,只有 Cookie 分支做对称加密。 + */ + public static function encodeCookieToken(string $token): string + { + $token = static::normalizeToken($token); + if ($token === '') { + return ''; + } + if (!static::cookieTokenEncryptionEnabled()) { + return $token; + } + + return self::COOKIE_TOKEN_PREFIX . CodeToolkit::encrypt($token, static::cookieTokenSecret()); + } + + public static function shouldUpgradeCookieToken(string $rawToken, ?string $decodedToken = null): bool + { + $rawToken = trim($rawToken); + if ($rawToken === '' || !static::cookieTokenEncryptionEnabled() || static::isEncryptedCookieToken($rawToken)) { + return false; + } + + $decodedToken = static::normalizeToken(strval($decodedToken)); + return $decodedToken !== '' && static::normalizeToken($rawToken) === $decodedToken; + } + + /** + * 标准化令牌内容。 + */ + public static function isEncryptedCookieToken(string $token): bool + { + return stripos(trim($token), self::COOKIE_TOKEN_PREFIX) === 0; + } + + /** + * 获取 Cookie 令牌加密密钥。 + */ + private static function cookieTokenSecret(): string + { + $secret = trim(strval(config('app.token_cookie_secret', ''))); + return $secret !== '' ? $secret : JwtToken::jwtkey(); + } + + /** + * 判断是否为系统 JWT。 + */ + private static function isSystemToken(string $token): bool + { + try { + return strval(JwtToken::verify($token)['typ'] ?? '') === SystemContext::getTokenType(); + } catch (\Throwable) { + return false; + } + } + + /** + * 判断是否为账号 JWT。 + */ + private static function isAccountToken(string $token): bool + { + try { + return strval(JwtToken::verify($token)['typ'] ?? '') === self::ACCOUNT_TOKEN_TYPE; + } catch (\Throwable) { + return false; + } + } + + /** + * 是否启用 Cookie 令牌加密。 + */ + private static function cookieTokenEncryptionEnabled(): bool + { + return boolval(config('app.token_cookie_encrypt', true)); + } +} diff --git a/plugin/think-library/src/runtime/SystemContext.php b/plugin/think-library/src/runtime/SystemContext.php new file mode 100644 index 000000000..a891a680a --- /dev/null +++ b/plugin/think-library/src/runtime/SystemContext.php @@ -0,0 +1,55 @@ +{$name}(...$arguments); + } + + /** + * 获取系统上下文实例. + */ + public static function instance(): SystemContextInterface + { + $container = Container::getInstance(); + if (!$container->bound(SystemContextInterface::class)) { + $container->bind(SystemContextInterface::class, NullSystemContext::class); + } + return $container->make(SystemContextInterface::class); + } +} diff --git a/plugin/think-library/src/service/AppService.php b/plugin/think-library/src/service/AppService.php new file mode 100644 index 000000000..43a169f8b --- /dev/null +++ b/plugin/think-library/src/service/AppService.php @@ -0,0 +1,1036 @@ +> + */ + public static function all(bool $force = false): array + { + if (!$force && is_array($apps = sysvar(self::CACHE_APPS))) { + return $apps; + } + + $apps = array_merge(self::local($force), self::allPlugins($force)); + ksort($apps); + return sysvar(self::CACHE_APPS, $apps); + } + + /** + * 获取本地 app/* 应用定义. + * + * @return array> + */ + public static function local(bool $force = false): array + { + if (!$force && is_array($apps = sysvar(self::CACHE_LOCALS))) { + return $apps; + } + + $apps = self::discoverLocalApps(); + ksort($apps); + return sysvar(self::CACHE_LOCALS, $apps); + } + + /** + * 获取插件应用定义. + * + * @return array> + */ + public static function plugins(bool $force = false): array + { + return self::allPlugins(false, $force); + } + + /** + * 获取全部应用编号. + * + * @return string[] + */ + public static function codes(bool $force = false): array + { + return array_keys(self::all($force)); + } + + /** + * 获取默认本地应用编号. + */ + public static function defaultAppCode(): string + { + $apps = self::local(); + $code = strval(Library::$sapp->config->get('route.default_app') ?: Library::$sapp->config->get('app.single_app') ?: ''); + if ($code !== '' && !in_array($code, self::IGNORE_LOCAL_APPS, true) && isset($apps[$code])) { + return $code; + } + if (isset($apps['index'])) { + return 'index'; + } + return strval(array_key_first($apps) ?: 'index'); + } + + /** + * 获取默认本地应用编号. + */ + public static function singleCode(): string + { + return self::defaultAppCode(); + } + + /** + * 获取指定应用定义. + * + * @param ?string $code 应用编号 + */ + public static function get(?string $code = null, bool $force = false): ?array + { + $apps = self::all($force); + return is_null($code) ? $apps : ($apps[$code] ?? null); + } + + /** + * 按首段路径命中本地应用. + * + * @return null|array + */ + public static function matchPath(string $pathinfo, bool $force = false): ?array + { + $pathinfo = trim($pathinfo, '\/'); + if ($pathinfo === '') { + return null; + } + + [$prefix, $suffix] = array_pad(explode('/', $pathinfo, 2), 2, ''); + if (strpos($prefix, '.')) { + $prefix = strstr($prefix, '.', true) ?: $prefix; + } + + if ($prefix === '' || !($app = self::localApp($prefix, $force))) { + return null; + } + + $app['matched_prefix'] = $prefix; + $app['pathinfo'] = $suffix; + return $app; + } + + /** + * 获取指定本地应用定义. + */ + public static function localApp(?string $code = null, bool $force = false): ?array + { + $apps = self::local($force); + return is_null($code) ? null : ($apps[$code] ?? null); + } + + /** + * 判断插件是否存在。 + * @param string $code 插件编码或别名 + * @param bool $force 强制刷新 + */ + public static function pluginExists(string $code, bool $force = false): bool + { + return self::resolvePlugin($code, false, $force) !== null; + } + + /** + * 解析插件编码或别名。 + * @param ?string $name 插件编码或别名 + * @param bool $append 关联安装信息 + * @param bool $force 强制刷新 + */ + public static function resolvePlugin(?string $name, bool $append = false, bool $force = false): ?array + { + if ($name === null || $name === '') { + return null; + } + + $plugins = self::allPlugins($append, $force); + if (isset($plugins[$name])) { + return $plugins[$name]; + } + + $bind = self::pluginBindings($force)[$name] ?? null; + if ($bind && isset($plugins[$bind])) { + return $plugins[$bind]; + } + + $code = self::pluginAliases($force)[$name] ?? null; + return $code ? ($plugins[$code] ?? null) : null; + } + + /** + * 获取全部插件定义。 + * @param bool $append 关联安装信息 + * @param bool $force 强制刷新 + */ + public static function allPlugins(bool $append = false, bool $force = false): array + { + if (!$append && !$force && is_array($plugins = sysvar(self::CACHE_PLUGINS))) { + return $plugins; + } + + $plugins = []; + foreach ((array)Plugin::get() as $code => $plugin) { + $plugins[$code] = self::normalizePlugin($code, $plugin); + } + + ksort($plugins); + if (!$append) { + sysvar(self::CACHE_PLUGINS, $plugins); + return $plugins; + } + + return array_map(static function (array $plugin): array { + return self::appendPluginInstall($plugin); + }, $plugins); + } + + /** + * 获取指定插件定义。 + * @param ?string $code 插件编码 + * @param bool $append 关联安装信息 + * @param bool $force 强制刷新 + */ + public static function getPlugin(?string $code = null, bool $append = false, bool $force = false): ?array + { + $plugins = self::allPlugins($append, $force); + return is_null($code) ? $plugins : ($plugins[$code] ?? null); + } + + /** + * 获取插件前缀绑定关系。 + * @param bool $force 强制刷新 + * @return array + */ + public static function pluginBindings(bool $force = false): array + { + if (!$force && is_array($bindings = sysvar(self::CACHE_BINDINGS))) { + return $bindings; + } + + $bindings = []; + foreach (self::allPlugins(false, $force) as $code => $plugin) { + foreach ($plugin['prefixes'] ?? [] as $prefix) { + if ($prefix === '') { + continue; + } + if (isset($bindings[$prefix]) && $bindings[$prefix] !== $code) { + throw new \RuntimeException("Plugin prefix conflict [{$prefix}] between [{$bindings[$prefix]}] and [{$code}]"); + } + $bindings[$prefix] = $code; + } + } + + ksort($bindings); + return sysvar(self::CACHE_BINDINGS, $bindings); + } + + /** + * 获取插件别名映射。 + * @param bool $force 强制刷新 + * @return array + */ + public static function pluginAliases(bool $force = false): array + { + if (!$force && is_array($aliases = sysvar(self::CACHE_ALIASES))) { + return $aliases; + } + + $aliases = []; + foreach (self::allPlugins(false, $force) as $code => $plugin) { + if (!empty($plugin['alias'])) { + $aliases[$plugin['alias']] = $code; + } + } + + ksort($aliases); + return sysvar(self::CACHE_ALIASES, $aliases); + } + + /** + * 获取插件主访问前缀。 + * @param string $code 插件编码 + * @param bool $force 强制刷新 + */ + public static function pluginPrefix(string $code, bool $force = false): string + { + return self::activePluginPrefix($code, $force) ?: (self::pluginPrefixes($code, $force)[0] ?? ''); + } + + /** + * 获取当前激活前缀。 + * @param ?string $code 插件编码 + * @param bool $force 强制刷新 + */ + public static function activePluginPrefix(?string $code = null, bool $force = false): string + { + $context = RequestContext::instance(); + $current = $context->pluginCode(); + $prefix = $context->pluginPrefix(); + if ($current === '' || $prefix === '') { + return ''; + } + if ($code !== null && $current !== $code) { + return ''; + } + return in_array($prefix, self::pluginPrefixes($current, $force), true) ? $prefix : ''; + } + + /** + * 获取插件前缀集合。 + * @param ?string $code 插件编码 + * @param bool $force 强制刷新 + * @return array>|array + */ + public static function pluginPrefixes(?string $code = null, bool $force = false): array + { + $plugins = self::allPlugins(false, $force); + if ($code === null) { + $items = []; + foreach ($plugins as $name => $plugin) { + $items[$name] = $plugin['prefixes'] ?? []; + } + return $items; + } + + return $plugins[$code]['prefixes'] ?? []; + } + + /** + * 按首段路径命中插件。 + * @param string $pathinfo 请求路径 + * @param ?string $switch 动态插件切换 + */ + public static function matchPluginPath(string $pathinfo, ?string $switch = null): ?array + { + $pathinfo = trim($pathinfo, '\/'); + $apiPrefix = self::pluginApiPrefix(); + if ($pathinfo !== '' && $apiPrefix !== '') { + $paths = explode('/', $pathinfo, 3); + if (strval($paths[0] ?? '') === $apiPrefix) { + $prefix = strval($paths[1] ?? ''); + if ($prefix !== '' && ($plugin = self::resolvePluginPrefix($prefix))) { + $plugin['entry'] = RequestContext::ENTRY_API; + $plugin['matched_prefix'] = $prefix; + $plugin['pathinfo'] = self::normalizeApiPathinfo(strval($paths[2] ?? 'index/index')); + return $plugin; + } + } + } + + if ($pathinfo !== '') { + $paths = explode('/', $pathinfo, 2); + $prefix = strval($paths[0] ?? ''); + if (strpos($prefix, '.')) { + $prefix = strstr($prefix, '.', true) ?: $prefix; + } + if ($prefix !== '' && ($plugin = self::resolvePluginPrefix($prefix))) { + $plugin['entry'] = RequestContext::ENTRY_WEB; + $plugin['matched_prefix'] = $prefix; + $plugin['pathinfo'] = $paths[1] ?? ''; + return $plugin; + } + } + + if ($switch && ($plugin = self::resolvePlugin($switch))) { + $plugin['entry'] = RequestContext::ENTRY_WEB; + $plugin['matched_prefix'] = ''; + $plugin['pathinfo'] = $pathinfo; + return $plugin; + } + + return null; + } + + /** + * 获取 API 入口前缀。 + */ + public static function pluginApiPrefix(): string + { + if (is_string($entry = sysvar(self::CACHE_ENTRY)) && $entry !== '') { + return $entry; + } + $entry = trim(strval(Library::$sapp->config->get('app.plugin.api_prefix', 'api')), '\/'); + $entry = $entry !== '' ? $entry : 'api'; + sysvar(self::CACHE_ENTRY, $entry); + return $entry; + } + + /** + * 解析指定前缀绑定。 + * @param ?string $prefix 路由前缀 + * @param bool $append 关联安装信息 + * @param bool $force 强制刷新 + */ + public static function resolvePluginPrefix(?string $prefix, bool $append = false, bool $force = false): ?array + { + if ($prefix === null || $prefix === '') { + return null; + } + + $code = self::pluginBindings($force)[$prefix] ?? null; + return $code ? self::getPlugin($code, $append, $force) : null; + } + + /** + * 检测动态插件切换参数。 + */ + public static function detectPluginSwitch(?Request $request = null): ?string + { + $config = self::pluginSwitchConfig(); + if (empty($config['enabled'])) { + return null; + } + + $request = $request ?: Library::$sapp->request; + $header = trim(strval($config['header'] ?? '')); + $query = trim(strval($config['query'] ?? '')); + $value = $header === '' ? '' : trim(strval($request->header($header) ?: '')); + if ($value === '' && $query !== '') { + $value = trim(strval($request->get($query, ''))); + } + + return $value === '' ? null : $value; + } + + /** + * 激活当前请求插件。 + * @param null|array|string $plugin 插件编码或定义 + * @param string $prefix 当前请求前缀 + */ + public static function activatePlugin($plugin = null, string $prefix = ''): ?array + { + $context = RequestContext::instance(); + if (is_array($plugin)) { + $code = strval($plugin['code'] ?? ''); + $current = $code === '' ? null : self::resolvePlugin($code); + } else { + $current = empty($plugin) ? null : self::resolvePlugin(strval($plugin)); + } + + if (empty($current)) { + $context->clearPlugin()->setEntryType(RequestContext::ENTRY_WEB); + return null; + } + + $prefix = trim($prefix, '\/'); + if ($prefix === '' || !in_array($prefix, $current['prefixes'] ?? [], true)) { + $prefix = $current['prefixes'][0] ?? ''; + } + + $context->setPlugin($current['code'], $prefix); + return $current; + } + + /** + * 获取当前请求插件。 + * @param bool $append 关联安装信息 + * @param bool $force 强制刷新 + */ + public static function currentPlugin(bool $append = false, bool $force = false): ?array + { + $code = RequestContext::instance()->pluginCode(); + return $code === '' ? null : self::getPlugin($code, $append, $force); + } + + /** + * 获取当前请求插件前缀。 + */ + public static function currentPluginPrefix(): string + { + return RequestContext::instance()->pluginPrefix(); + } + + /** + * 设置当前请求插件入口类型。 + */ + public static function activatePluginEntry(string $entryType = RequestContext::ENTRY_WEB): void + { + RequestContext::instance()->setEntryType($entryType); + } + + /** + * 获取当前请求插件入口类型。 + */ + public static function currentPluginEntry(): string + { + return RequestContext::instance()->entryType(); + } + + /** + * 获取当前请求插件编码。 + */ + public static function currentPluginCode(): string + { + return RequestContext::instance()->pluginCode(); + } + + /** + * 获取插件菜单定义。 + * @param null|array|string $plugin 插件编码或定义 + * @param bool $check 检查权限 + * @param bool $normalize 标准化输出 + */ + public static function menus($plugin, bool $check = false, bool $normalize = false): array + { + if (is_array($plugin)) { + $code = strval($plugin['code'] ?? ''); + $current = $code === '' ? $plugin : array_replace(self::resolvePlugin($code, true) ?: [], $plugin); + } else { + $current = self::resolvePlugin($plugin, true); + } + if (empty($current['service']) || !class_exists($current['service'])) { + return []; + } + + $menus = (array)$current['service']::getMenus(); + return ($check || $normalize) ? self::normalizeMenus($menus, $check) : $menus; + } + + /** + * 获取版本信息。 + */ + public static function getVersion(): string + { + $library = self::getPluginLibrarys('zoujingli/think-library'); + return trim($library['version'] ?? 'v8.0.0', 'v'); + } + + /** + * 获取插件包版本信息。 + * @param ?string $package 包名 + * @param bool $force 强制刷新 + * @return array|mixed + */ + public static function getPluginLibrarys(?string $package = null, bool $force = false) + { + $plugs = sysvar($keys = 'think.admin.version'); + if (empty($plugs) || $force) { + foreach (array_unique([runpath('vendor/versions.php'), syspath('vendor/versions.php')]) as $file) { + if (is_file($file)) { + $plugs = sysvar($keys, include $file); + break; + } + } + } + return empty($package) ? $plugs : ($plugs[$package] ?? null); + } + + /** + * 生成全部静态路径。 + * @param string $path 后缀路径 + * @return string[] + */ + public static function uris(string $path = ''): array + { + return self::uri($path, null); + } + + /** + * 生成静态路径链接。 + * @param string $path 后缀路径 + * @param ?string $type 路径类型 + * @param mixed $default 默认数据 + * @return array|string + */ + public static function uri(string $path = '', ?string $type = '__ROOT__', $default = '') + { + $plugin = Library::$sapp->http->getName(); + if (strlen($path)) { + $path = '/' . ltrim($path, '/'); + } + $prefix = rtrim(dirname(Library::$sapp->request->basefile()), '\/'); + $data = [ + '__APP__' => rtrim(url('@')->build(), '\/') . $path, + '__ROOT__' => $prefix . $path, + '__PLUG__' => "{$prefix}/static/extra/{$plugin}{$path}", + '__FULL__' => Library::$sapp->request->domain() . $prefix . $path, + ]; + return is_null($type) ? $data : ($data[$type] ?? $default); + } + + /** + * 打印调试数据到文件。 + * @param mixed $data 输出的数据 + * @param bool $new 强制替换文件 + * @param null|string $file 文件名称 + * @return false|int + */ + public static function putDebug($data, bool $new = false, ?string $file = null) + { + ob_start(); + var_dump($data); + $output = preg_replace('/]=>\n(\s+)/m', '] => ', ob_get_clean()); + if (is_null($file)) { + $file = runpath('runtime/' . date('Ymd') . '.log'); + } elseif (!preg_match('#[/\\\]+#', $file)) { + $file = runpath("runtime/{$file}.log"); + } + is_dir($dir = dirname($file)) or mkdir($dir, 0777, true); + return $new ? file_put_contents($file, $output) : file_put_contents($file, $output, FILE_APPEND); + } + + /** + * 批量更新保存数据。 + * @param Model|Query|string $query 数据查询对象 + * @param array $data 需要保存的数据 + * @param string $key 更新条件查询主键 + * @param mixed $map 额外更新查询条件 + * @return bool|int + * @throws \think\admin\Exception + */ + public static function update($query, array $data, string $key = 'id', $map = []) + { + try { + $query = QueryFactory::build($query)->master()->where($map); + if (empty($map[$key])) { + $query->where([$key => $data[$key] ?? null]); + } + return (clone $query)->count() > 1 ? $query->strict(false)->update($data) : $query->findOrEmpty()->save($data); + } catch (\Exception|\Throwable $exception) { + throw new \think\admin\Exception($exception->getMessage(), $exception->getCode()); + } + } + + /** + * 数据增量保存。 + * @param Model|Query|string $query 数据查询对象 + * @param array $data 需要保存的数据 + * @param string $key 更新条件查询主键 + * @param mixed $map 额外更新查询条件 + * @return bool|int + * @throws \think\admin\Exception + */ + public static function save($query, array &$data, string $key = 'id', $map = []) + { + try { + $query = QueryFactory::build($query)->master()->strict(false); + if (empty($map[$key])) { + $query->where([$key => $data[$key] ?? null]); + } + $model = $query->where($map)->findOrEmpty(); + $action = $model->isExists() ? 'onAdminUpdate' : 'onAdminInsert'; + if ($model->save($data) === false) { + return false; + } + if ($model instanceof \think\admin\Model) { + $model->{$action}(strval($model->getAttr($key))); + } + $data = $model->toArray(); + return $model[$key] ?? true; + } catch (\Exception $exception) { + throw new \think\admin\Exception($exception->getMessage(), $exception->getCode()); + } + } + + /** + * 扫描 app/* 本地应用目录. + * + * @return array> + */ + private static function discoverLocalApps(): array + { + $apps = []; + $basePath = rtrim(Library::$sapp->getBasePath(), '\/') . DIRECTORY_SEPARATOR; + if (!is_dir($basePath)) { + return $apps; + } + foreach (scandir($basePath) ?: [] as $code) { + if ($code === '.' || $code === '..' || in_array($code, self::IGNORE_LOCAL_APPS, true)) { + continue; + } + if (!preg_match('/^[A-Za-z][A-Za-z0-9_]*$/', $code)) { + continue; + } + + $path = $basePath . $code . DIRECTORY_SEPARATOR; + if (!is_dir($path) || !self::isLocalAppPath($path)) { + continue; + } + + $apps[$code] = self::normalize($code, [ + 'type' => 'local', + 'name' => ucfirst($code), + 'path' => $path, + 'space' => NodeService::space($code), + ]); + } + + return $apps; + } + + /** + * 判断是否为本地应用目录. + */ + private static function isLocalAppPath(string $path): bool + { + foreach (['controller', 'route', 'view', 'config'] as $name) { + if (is_dir($path . $name)) { + return true; + } + } + + return is_file($path . 'Service.php'); + } + + /** + * 递归收集菜单节点。 + * @param array> $menus + * @param array $nodes + */ + private static function collectMenuNodes(array $menus, array &$nodes): void + { + foreach ($menus as $menu) { + if (!empty($menu['node'])) { + $nodes[] = strval($menu['node']); + } + if (!empty($menu['subs'])) { + self::collectMenuNodes((array)$menu['subs'], $nodes); + } + } + } + + /** + * 标准化插件菜单并可选按权限过滤。 + * @param array> $menus + * @return array> + */ + private static function normalizeMenus(array $menus, bool $check = false): array + { + foreach ($menus as $k1 => &$one) { + $one['title'] = lang($one['title'] ?? ($one['name'] ?? '')); + $one['url'] = $one['url'] ?? self::buildMenuUrl(strval($one['node'] ?? '')); + if (!empty($one['subs'])) { + foreach ($one['subs'] as $k2 => &$two) { + if ($check && isset($two['node']) && !auth($two['node'])) { + unset($one['subs'][$k2]); + continue; + } + $two['title'] = lang($two['title'] ?? ($two['name'] ?? '')); + $two['url'] = $two['url'] ?? self::buildMenuUrl(strval($two['node'] ?? '')); + } + $one['subs'] = array_values($one['subs']); + } + + if ($check && isset($one['node']) && !auth($one['node'])) { + unset($menus[$k1]); + continue; + } + if ($one['url'] === '#' && empty($one['subs'])) { + unset($menus[$k1]); + } + } + + return array_values($menus); + } + + /** + * 生成菜单 URL,缺少插件上下文时回退为系统 URL。 + */ + private static function buildMenuUrl(string $node): string + { + if ($node === '') { + return '#'; + } + + if (function_exists('plguri') && self::currentPluginCode() !== '') { + return plguri($node); + } + + return sysuri($node); + } + + /** + * 标准化应用定义. + * + * @param string $code 应用编号 + * @param array $app 应用配置 + * @return array + */ + private static function normalize(string $code, array $app): array + { + $path = $app['path'] ?? ''; + $path = $path === '' ? '' : rtrim((string)$path, '\/') . DIRECTORY_SEPARATOR; + + return [ + 'code' => $code, + 'type' => $app['type'] ?? 'local', + 'name' => $app['name'] ?? ucfirst($code), + 'path' => $path, + 'alias' => $app['alias'] ?? '', + 'space' => $app['space'] ?? NodeService::space($code), + 'package' => $app['package'] ?? '', + 'service' => $app['service'] ?? '', + ]; + } + + /** + * 标准化插件定义。 + * @param string $code 插件编码 + * @param array $plugin 原始定义 + */ + private static function normalizePlugin(string $code, array $plugin): array + { + $path = $plugin['path'] ?? ''; + $path = $path === '' ? '' : rtrim((string)$path, '\/') . DIRECTORY_SEPARATOR; + $prefixes = self::effectivePluginPrefixes($code, $plugin); + + return [ + 'code' => $code, + 'type' => $plugin['type'] ?? 'plugin', + 'name' => $plugin['name'] ?? ucfirst($code), + 'path' => $path, + 'alias' => $plugin['alias'] ?? '', + 'prefix' => $prefixes[0] ?? '', + 'prefixes' => $prefixes, + 'space' => $plugin['space'] ?? '', + 'package' => $plugin['package'] ?? '', + 'service' => $plugin['service'] ?? '', + 'document' => $plugin['document'] ?? '', + 'description' => $plugin['description'] ?? '', + 'platforms' => (array)($plugin['platforms'] ?? []), + 'license' => (array)($plugin['license'] ?? []), + 'version' => strval($plugin['version'] ?? ''), + 'homepage' => strval($plugin['homepage'] ?? ''), + 'show' => !array_key_exists('show', $plugin) || !empty($plugin['show']), + ]; + } + + /** + * 获取插件有效前缀。 + * @param string $code 插件编码 + * @param array $plugin 插件定义 + * @return string[] + */ + private static function effectivePluginPrefixes(string $code, array $plugin): array + { + $prefixes = self::configuredPluginPrefixes($code); + if ($prefixes === null) { + $prefixes = self::normalizePluginPrefixes($plugin['prefixes'] ?? [], $plugin['prefix'] ?? '', $plugin['alias'] ?? '', $code); + } + if (empty($prefixes)) { + $prefixes = [$code]; + } + return $prefixes; + } + + /** + * 获取配置文件中的插件前缀定义。 + * @param string $code 插件编码 + * @return null|string[] + */ + private static function configuredPluginPrefixes(string $code): ?array + { + $config = (array)Library::$sapp->config->get('app.plugin.bindings', []); + if (array_key_exists($code, $config)) { + return self::normalizePluginPrefixes($config[$code]); + } + + foreach ($config as $item) { + if (!is_array($item)) { + continue; + } + $name = strval($item['code'] ?? $item['plugin'] ?? ''); + if ($name === $code) { + return self::normalizePluginPrefixes($item['prefixes'] ?? ($item['prefix'] ?? [])); + } + } + + return null; + } + + /** + * 标准化前缀集合。 + * @param mixed ...$values 原始前缀 + * @return string[] + */ + private static function normalizePluginPrefixes(...$values): array + { + $items = []; + foreach ($values as $value) { + foreach ((array)$value as $prefix) { + $prefix = trim((string)$prefix, " \t\n\r\0\x0B\\/"); + if ($prefix === '') { + continue; + } + if (strpos($prefix, '/')) { + $prefix = strstr($prefix, '/', true) ?: $prefix; + } + if (strpos($prefix, '.')) { + $prefix = strstr($prefix, '.', true) ?: $prefix; + } + if ($prefix !== '' && !in_array($prefix, $items, true)) { + $items[] = $prefix; + } + } + } + + return $items; + } + + /** + * 附加插件安装信息。 + * @param array $plugin 插件定义 + */ + private static function appendPluginInstall(array $plugin): array + { + $versions = self::getPluginLibrarys(); + $plugin['install'] = $versions[$plugin['package']] ?? []; + foreach (['type', 'name', 'document', 'description', 'homepage', 'version'] as $field) { + if (empty($plugin[$field])) { + $plugin[$field] = $plugin['install'][$field] ?? ($field === 'type' ? 'plugin' : ''); + } + } + $plugin['platforms'] = array_values(array_unique(array_filter(array_merge( + (array)($plugin['platforms'] ?? []), + (array)($plugin['install']['platforms'] ?? []) + )))); + $plugin['license'] = array_values(array_unique(array_filter(array_merge( + (array)($plugin['license'] ?? []), + (array)($plugin['install']['license'] ?? []) + )))); + return $plugin; + } + + /** + * 获取插件切换配置。 + * @return array{enabled:bool,query:string,header:string} + */ + private static function pluginSwitchConfig(): array + { + $config = (array)Library::$sapp->config->get('app.plugin.switch', []); + return [ + 'enabled' => isset($config['enabled']) ? boolval($config['enabled']) : false, + 'query' => strval($config['query'] ?? '_plugin'), + 'header' => strval($config['header'] ?? 'X-Plugin-App'), + ]; + } + + /** + * 标准化 API 入口路径。 + * /api/{plugin}/upload/file -> api.upload/file. + */ + private static function normalizeApiPathinfo(string $pathinfo): string + { + $pathinfo = trim($pathinfo, '\/'); + if ($pathinfo === '') { + return 'api.index/index'; + } + if (strpos($pathinfo, 'api.') === 0) { + return $pathinfo; + } + [$controller, $action] = array_pad(explode('/', $pathinfo, 2), 2, 'index'); + $controller = trim(strtr($controller, '/', '.'), '.'); + return 'api.' . $controller . '/' . trim($action, '\/'); + } +} diff --git a/plugin/think-library/src/service/AuthResponse.php b/plugin/think-library/src/service/AuthResponse.php new file mode 100644 index 000000000..c2adb1c48 --- /dev/null +++ b/plugin/think-library/src/service/AuthResponse.php @@ -0,0 +1,126 @@ +code(200); + } + + /** + * 构建权限响应内容. + * @return array + */ + public static function payload(int $status, mixed $info, mixed $data = '{-null-}', array $extra = []): array + { + if ($data === '{-null-}') { + $data = new stdClass(); + } + + $payload = [ + 'code' => $status, + 'error' => self::errorName($status), + 'info' => is_string($info) ? lang($info) : $info, + 'data' => $data, + ]; + + foreach ($extra as $key => $value) { + if (!array_key_exists($key, $payload)) { + $payload[$key] = $value; + } + } + + return $payload; + } + + /** + * 规范化权限状态码. + */ + public static function normalizeStatus(int $status): int + { + return $status === self::STATUS_FORBIDDEN ? self::STATUS_FORBIDDEN : self::STATUS_UNAUTHORIZED; + } + + /** + * 获取权限错误标识. + */ + public static function errorName(int $status): string + { + return $status === self::STATUS_FORBIDDEN ? self::ERROR_FORBIDDEN : self::ERROR_UNAUTHORIZED; + } +} diff --git a/plugin/think-library/src/service/CacheSession.php b/plugin/think-library/src/service/CacheSession.php new file mode 100644 index 000000000..bdabae55b --- /dev/null +++ b/plugin/think-library/src/service/CacheSession.php @@ -0,0 +1,465 @@ + + */ + public static function all(?string $scope = null, ?bool $touch = null): array + { + self::sweep(); + $scope = self::scope($scope); + $cache = self::store(); + $key = self::sessionKey($scope); + $payload = $cache->get($key, []); + if (!is_array($payload) || !array_key_exists('data', $payload) || !is_array($payload['data'])) { + self::dropIndex($key); + return []; + } + + $touch = is_null($touch) ? self::autoTouch() : $touch; + if ($touch && intval($payload['expire'] ?? 0) > 0) { + $payload['updated_at'] = time(); + $cache->set($key, $payload, intval($payload['expire'])); + self::saveIndex($key, intval($payload['expire'])); + } + + return $payload['data']; + } + + /** + * 惰性清理已过期会话。 + */ + public static function sweep(bool $force = false): int + { + $cache = self::store(); + $interval = self::gcInterval(); + $now = time(); + if (!$force && $interval > 0 && intval($cache->get(self::GC_KEY, 0)) > $now) { + return 0; + } + + $index = $cache->get(self::INDEX_KEY, []); + if (!is_array($index)) { + $index = []; + } + + $count = 0; + foreach ($index as $key => $expireAt) { + $expireAt = intval($expireAt); + if ($expireAt > 0 && $expireAt <= $now) { + $cache->delete($key); + unset($index[$key]); + ++$count; + } elseif ($expireAt <= 0 && !$cache->has($key)) { + unset($index[$key]); + } + } + + $cache->set(self::INDEX_KEY, $index); + $cache->set(self::GC_KEY, $now + $interval, max(60, $interval)); + return $count; + } + + /** + * 删除指定会话字段。 + * @throws Exception + */ + public static function delete(string $name, ?string $scope = null): bool + { + self::sweep(); + $scope = self::scope($scope); + $cache = self::store(); + $key = self::sessionKey($scope); + $payload = self::payload($scope, $cache->get($key, [])); + if (!array_key_exists($name, $payload['data'])) { + return true; + } + + unset($payload['data'][$name]); + $payload['updated_at'] = time(); + if (!$cache->set($key, $payload, intval($payload['expire']))) { + return false; + } + + self::saveIndex($key, intval($payload['expire'])); + return true; + } + + /** + * 获取当前会话作用域。 + * @throws Exception + */ + public static function scope(?string $scope = null): string + { + $scope = trim(strval($scope)); + if ($scope !== '') { + return $scope; + } + + if (($sessionId = self::currentSessionId()) !== '') { + return "sid:{$sessionId}"; + } + + throw new Exception('令牌会话未初始化,请先完成 Token 鉴权或显式传入作用域标识!', 401); + } + + /** + * 获取缓存会话键名。 + * @throws Exception + */ + public static function sessionKey(?string $scope = null): string + { + return self::CACHE_PREFIX . hash('sha256', self::scope($scope)); + } + + /** + * 写入指定会话数据。 + * @throws Exception + */ + public static function set(string $name, mixed $value, ?int $expire = null, ?string $scope = null): bool + { + return self::put([$name => $value], $expire, $scope); + } + + /** + * 批量写入会话数据。 + * @param array $data + * @throws InvalidArgumentException + * @throws Exception + */ + public static function put(array $data, ?int $expire = null, ?string $scope = null, bool $replace = false): bool + { + self::sweep(); + $scope = self::scope($scope); + $cache = self::store(); + $key = self::sessionKey($scope); + $payload = self::payload($scope, $cache->get($key, [])); + $payload['expire'] = self::ttl(is_null($expire) ? (intval($payload['expire'] ?? 0) ?: self::getExpire()) : $expire); + $payload['updated_at'] = time(); + $payload['data'] = $replace ? $data : array_merge($payload['data'], $data); + if (!$cache->set($key, $payload, $payload['expire'])) { + return false; + } + + self::saveIndex($key, $payload['expire']); + return true; + } + + /** + * 判断指定键是否存在。 + */ + public static function has(string $name, ?string $scope = null): bool + { + return array_key_exists($name, self::all($scope, false)); + } + + /** + * 写入指定会话数据别名。 + * @throws Exception + */ + public static function write(string $name, mixed $value, ?int $expire = null, ?string $scope = null): bool + { + return self::set($name, $value, $expire, $scope); + } + + /** + * 读取并删除指定会话字段。 + * @throws Exception + */ + public static function pull(string $name, mixed $default = null, ?string $scope = null): mixed + { + $value = self::get($name, $default, $scope, false); + self::delete($name, $scope); + return $value; + } + + /** + * 清空当前会话数据,但保留会话本身。 + * @throws InvalidArgumentException + * @throws Exception + */ + public static function clear(?string $scope = null): bool + { + $scope = self::scope($scope); + $cache = self::store(); + $key = self::sessionKey($scope); + $payload = self::payload($scope, $cache->get($key, [])); + if (!self::exists($scope)) { + self::dropIndex($key); + return false; + } + + $payload['updated_at'] = time(); + $payload['data'] = []; + if (!$cache->set($key, $payload, intval($payload['expire']))) { + return false; + } + + self::saveIndex($key, intval($payload['expire'])); + return true; + } + + /** + * 判断当前会话是否存在。 + * @throws InvalidArgumentException + * @throws Exception + */ + public static function exists(?string $scope = null): bool + { + self::sweep(); + $payload = self::store()->get(self::sessionKey($scope), null); + return is_array($payload) && array_key_exists('data', $payload) && is_array($payload['data']); + } + + /** + * 销毁当前会话别名。 + * @throws Exception + */ + public static function forget(?string $scope = null): bool + { + return self::destroy($scope); + } + + /** + * 销毁当前会话。 + * @throws InvalidArgumentException + * @throws Exception + */ + public static function destroy(?string $scope = null): bool + { + $scope = self::scope($scope); + $key = self::sessionKey($scope); + self::dropIndex($key); + self::store()->delete($key); + return true; + } + + /** + * 刷新当前会话过期时间。 + * @throws InvalidArgumentException + * @throws Exception + */ + public static function touch(?int $expire = null, ?string $scope = null): bool + { + self::sweep(); + $scope = self::scope($scope); + $cache = self::store(); + $key = self::sessionKey($scope); + $payload = self::payload($scope, $cache->get($key, [])); + if (empty($payload['data'])) { + if (!self::exists($scope)) { + self::dropIndex($key); + return false; + } + } + + $payload['expire'] = self::ttl(is_null($expire) ? intval($payload['expire'] ?? 0) : $expire); + $payload['updated_at'] = time(); + if (!$cache->set($key, $payload, intval($payload['expire']))) { + return false; + } + + self::saveIndex($key, intval($payload['expire'])); + return true; + } + + /** + * 垃圾清理别名。 + */ + public static function gc(bool $force = false): int + { + return self::sweep($force); + } + + /** + * 获取缓存驱动。 + */ + private static function store(): Cache|Driver + { + $store = trim(strval(self::config('token_session_store', ''))); + return $store === '' ? Library::$sapp->cache : Library::$sapp->cache->store($store); + } + + /** + * 读取令牌会话配置。 + */ + private static function config(string $name, mixed $default = null): mixed + { + $config = Library::$sapp->config->get('app', []); + if (is_array($config) && array_key_exists($name, $config)) { + return $config[$name]; + } + return $default; + } + + /** + * 获取垃圾清理间隔。 + */ + private static function gcInterval(): int + { + return max(60, intval(self::config('token_session_gc_interval', self::DEFAULT_GC_INTERVAL))); + } + + /** + * 获取当前请求绑定的认证会话编号。 + */ + private static function currentSessionId(): string + { + $sessionId = RequestContext::instance()->sessionId(); + if ($sessionId !== '') { + return $sessionId; + } + + return trim(strval(sysvar('plugin_account_user_session_id') ?: '')); + } + + /** + * 规范化缓存载荷。 + * @return array{scope:string,expire:int,updated_at:int,data:array} + */ + private static function payload(string $scope, mixed $payload): array + { + $data = is_array($payload['data'] ?? null) ? $payload['data'] : []; + return [ + 'scope' => $scope, + 'expire' => self::ttl(is_array($payload) ? intval($payload['expire'] ?? 0) : 0), + 'updated_at' => is_array($payload) ? intval($payload['updated_at'] ?? time()) : time(), + 'data' => $data, + ]; + } + + /** + * 获取缓存会话默认过期时间。 + */ + private static function ttl(?int $expire = null): int + { + $expire = is_null($expire) ? self::getExpire() : $expire; + return max(0, intval($expire)); + } + + /** + * 获取默认过期时间。 + */ + private static function getExpire(): int + { + return max(0, intval(self::config('token_session_expire', self::DEFAULT_EXPIRE))); + } + + /** + * 保存会话索引。 + */ + private static function saveIndex(string $key, int $expire): void + { + $cache = self::store(); + $index = $cache->get(self::INDEX_KEY, []); + if (!is_array($index)) { + $index = []; + } + + $index[$key] = $expire > 0 ? time() + $expire : 0; + $cache->set(self::INDEX_KEY, $index); + } + + /** + * 删除会话索引。 + */ + private static function dropIndex(string $key): void + { + $cache = self::store(); + $index = $cache->get(self::INDEX_KEY, []); + if (!is_array($index) || !array_key_exists($key, $index)) { + return; + } + + unset($index[$key]); + $cache->set(self::INDEX_KEY, $index); + } + + /** + * 获取自动续期配置。 + */ + private static function autoTouch(): bool + { + return boolval(self::config('token_session_touch', true)); + } +} diff --git a/plugin/think-library/src/service/FaviconBuilder.php b/plugin/think-library/src/service/FaviconBuilder.php new file mode 100644 index 000000000..1141f1833 --- /dev/null +++ b/plugin/think-library/src/service/FaviconBuilder.php @@ -0,0 +1,208 @@ +assertGdFunctions(); + if (is_string($file)) { + $this->addImage($file, $size); + } + } + + /** + * 添加图像到生成器中。 + * + * @throws Exception + */ + public function addImage(string $file, array $size = []): static + { + $im = $this->loadImageFile($file); + if ($im === false) { + throw new Exception(lang('Read picture file Failed.')); + } + if (empty($size)) { + $size = [imagesx($im), imagesy($im)]; + } + [$width, $height] = $size; + $image = imagecreatetruecolor($width, $height); + imagecolortransparent($image, imagecolorallocatealpha($image, 0, 0, 0, 127)); + imagealphablending($image, false); + imagesavealpha($image, true); + if (imagecopyresampled($image, $im, 0, 0, 0, 0, $width, $height, imagesx($im), imagesy($im)) === false) { + throw new Exception(lang('Parse and process picture Failed.')); + } + $this->addImageData($image); + return $this; + } + + /** + * 将 ICO 内容写入到文件。 + */ + public function saveIco(string $file): bool + { + if (false === ($data = $this->getIcoData())) { + return false; + } + if (false === ($fh = fopen($file, 'w'))) { + return false; + } + if (fwrite($fh, $data) === false) { + fclose($fh); + return false; + } + fclose($fh); + return true; + } + + /** + * 检查 GD 依赖函数是否可用。 + * + * @throws Exception + */ + private function assertGdFunctions(): void + { + $functions = [ + 'imagesx', + 'imagesy', + 'getimagesize', + 'imagesavealpha', + 'imagecreatefromstring', + 'imagecreatetruecolor', + 'imagecolortransparent', + 'imagecolorallocatealpha', + 'imagecopyresampled', + 'imagealphablending', + ]; + foreach ($functions as $function) { + if (!function_exists($function)) { + throw new Exception(lang('Required %s function not found.', [$function])); + } + } + } + + /** + * 读取图片资源。 + * + * @return false|\GdImage|resource + */ + private function loadImageFile(string $file) + { + if (getimagesize($file) === false) { + return false; + } + if (false === ($data = file_get_contents($file))) { + return false; + } + if (false === ($image = @imagecreatefromstring($data))) { + return false; + } + return $image; + } + + /** + * 将 GD 图像转为 BMP 格式。 + * + * @param mixed $im + */ + private function addImageData($im): void + { + [$width, $height] = [imagesx($im), imagesy($im)]; + [$pixelData, $opacityData, $opacityValue] = [[], [], 0]; + for ($y = $height - 1; $y >= 0; --$y) { + for ($x = 0; $x < $width; ++$x) { + $color = imagecolorat($im, $x, $y); + $alpha = ($color & 0x7F000000) >> 24; + $alpha = (1 - ($alpha / 127)) * 255; + $color &= 0xFFFFFF; + $color |= 0xFF000000 & ((int)$alpha << 24); + $pixelData[] = $color; + $opacity = ($alpha <= 127) ? 1 : 0; + $opacityValue = ($opacityValue << 1) | $opacity; + if ((($x + 1) % 32) === 0) { + $opacityData[] = $opacityValue; + $opacityValue = 0; + } + } + if (($x % 32) > 0) { + while (($x++ % 32) > 0) { + $opacityValue <<= 1; + } + $opacityData[] = $opacityValue; + $opacityValue = 0; + } + } + $imageHeaderSize = 40; + $colorMaskSize = $width * $height * 4; + $opacityMaskSize = (ceil($width / 32) * 4) * $height; + $data = pack('VVVvvVVVVVV', 40, $width, $height * 2, 1, 32, 0, 0, 0, 0, 0, 0); + foreach ($pixelData as $color) { + $data .= pack('V', $color); + } + foreach ($opacityData as $opacity) { + $data .= pack('N', $opacity); + } + $this->images[] = [ + 'data' => $data, + 'size' => $imageHeaderSize + $colorMaskSize + $opacityMaskSize, + 'width' => $width, + 'height' => $height, + 'pixel' => 32, + 'colors' => 0, + ]; + } + + /** + * 生成并获取 ICO 图像数据。 + */ + private function getIcoData() + { + if (empty($this->images)) { + return false; + } + [$pixelData, $entrySize] = ['', 16]; + $data = pack('vvv', 0, 1, count($this->images)); + $offset = 6 + ($entrySize * count($this->images)); + foreach ($this->images as $image) { + $data .= pack('CCCCvvVV', $image['width'], $image['height'], $image['colors'], 0, 1, $image['pixel'], $image['size'], $offset); + $pixelData .= $image['data']; + $offset += $image['size']; + } + return $data . $pixelData; + } +} diff --git a/plugin/think-library/src/service/ImageSliderVerify.php b/plugin/think-library/src/service/ImageSliderVerify.php new file mode 100644 index 000000000..61862b98d --- /dev/null +++ b/plugin/think-library/src/service/ImageSliderVerify.php @@ -0,0 +1,338 @@ + $v) { + if (property_exists($this, $k)) { + $this->{$k} = $v; + } + } + $this->srcImage = $image; + } + + /** + * 生成图片拼图。 + * + * @return array [code, bgimg, water] + */ + public static function render(string $image, int $time = 1800, int $diff = 10, int $retry = 3): array + { + $data = (new self($image))->create(); + $range = [$data['point'] - $diff, $data['point'] + $diff]; + $result = ['retry' => $retry, 'error' => 0, 'expire' => time() + $time, 'range' => $range]; + Library::$sapp->cache->set($code = CodeToolkit::uniqidNumber(16, 'V'), $result, $time); + return [ + 'code' => $code, + 'bgimg' => $data['bgimg'], + 'water' => $data['water'], + 'width' => $data['width'], + 'height' => $data['height'], + 'piece_width' => $data['piece_width'], + ]; + } + + /** + * 创建背景图和浮层图、浮层图 X 坐标。 + * + * @return array [point, bgimg, water] + */ + public function create(): array + { + $dstim = $this->cover($this->srcImage, $this->dstWidth, $this->dstHeight); + $watim = imagecreatetruecolor($this->picWidth, $this->dstHeight); + imagesavealpha($watim, true) && imagealphablending($watim, false); + imagefill($watim, 0, 0, imagecolorallocatealpha($watim, 255, 255, 255, 127)); + + $srcX1 = mt_rand(150, $this->dstWidth - $this->picWidth); + $srcY1 = mt_rand(0, $this->dstHeight - $this->picHeight); + + $borders = [ + imagecolorallocatealpha($dstim, 250, 100, 0, 50), + imagecolorallocatealpha($dstim, 250, 0, 100, 50), + imagecolorallocatealpha($dstim, 100, 0, 250, 50), + imagecolorallocatealpha($dstim, 100, 250, 0, 50), + imagecolorallocatealpha($dstim, 0, 250, 100, 50), + ]; + shuffle($borders); + $c1 = array_pop($borders); + $gray = imagecolorallocatealpha($dstim, 0, 0, 0, 80); + $blue = imagecolorallocatealpha($watim, 0, 100, 250, 50); + $waters = $this->withWaterPoint(); + + for ($i = 0; $i < $this->picHeight; ++$i) { + for ($j = 0; $j < $this->picWidth; ++$j) { + if ($waters[$i][$j] === 1) { + if ( + empty($waters[$i - 1][$j - 1]) || empty($waters[$i - 2][$j - 2]) + || empty($waters[$i + 1][$j + 1]) || empty($waters[$i + 2][$j + 2]) + ) { + imagesetpixel($watim, $j, $srcY1 + $i, $blue); + } else { + imagesetpixel($watim, $j, $srcY1 + $i, imagecolorat($dstim, $srcX1 + $j, $srcY1 + $i)); + } + } + } + } + + for ($i = 0; $i < $this->picHeight; ++$i) { + for ($j = 0; $j < $this->picWidth; ++$j) { + if ($waters[$i][$j] === 1) { + if ( + empty($waters[$i - 1][$j - 1]) || empty($waters[$i - 2][$j - 2]) + || empty($waters[$i + 1][$j + 1]) || empty($waters[$i + 2][$j + 2]) + ) { + imagesetpixel($dstim, $srcX1 + $j, $srcY1 + $i, $c1); + } else { + imagesetpixel($dstim, $srcX1 + $j, $srcY1 + $i, $gray); + } + } + } + } + + [, , $bgimg] = [ob_start(), imagepng($dstim), ob_get_contents(), ob_end_clean(), imagedestroy($dstim)]; + [, , $water] = [ob_start(), imagepng($watim), ob_get_contents(), ob_end_clean(), imagedestroy($watim)]; + + return [ + 'point' => $srcX1, + 'bgimg' => 'data:image/png;base64,' . base64_encode($bgimg), + 'water' => 'data:image/png;base64,' . base64_encode($water), + 'width' => $this->dstWidth, + 'height' => $this->dstHeight, + 'piece_width' => $this->picWidth, + ]; + } + + /** + * 居中裁剪图片。 + * + * @return \GdImage|resource + */ + public static function cover(string $image, int $width, int $height) + { + $file = self::coverFile($image, $width, $height); + if (is_file($file) && ($data = file_get_contents($file)) !== false) { + if (($cached = imagecreatefromstring($data)) !== false) { + return $cached; + } + } + [$w, $h, $srcim] = self::sourceImage($image) ?? self::fallbackSource($image, $width, $height); + if ($w > $h) { + [$_sw, $_sh, $_sx, $_sy] = [$h, $h, (int)(($w - $h) / 2), 0]; + } elseif ($w < $h) { + [$_sw, $_sh, $_sx, $_sy] = [$w, $w, 0, (int)(($h - $w) / 2)]; + } else { + [$_sw, $_sh, $_sx, $_sy] = [$w, $h, 0, 0]; + } + $newim = imagecreatetruecolor($width, $height); + imagecopyresampled($newim, $srcim, 0, 0, $_sx, $_sy, $width, $height, $_sw, $_sh); + imagedestroy($srcim); + self::storeCover($newim, $file); + return $newim; + } + + /** + * 在线验证是否通过。 + * 返回值约定: + * `-1` 需要刷新,`0` 验证失败,`1` 验证成功。 + */ + public static function verify(string $code, string $value, bool $clear = false): int + { + $cache = Library::$sapp->cache->get($code); + if (empty($cache['range']) || empty($cache['retry'])) { + return -1; + } + if ($cache['range'][0] <= $value && $value <= $cache['range'][1]) { + if ($clear) { + Library::$sapp->cache->delete($code); + } + return 1; + } + if (++$cache['error'] < $cache['retry']) { + if (($ttl = $cache['expire'] - time()) > 0) { + Library::$sapp->cache->set($code, $cache, $ttl); + return 0; + } + } + Library::$sapp->cache->delete($code); + return -1; + } + + /** + * 生成裁剪缓存文件。 + */ + private static function coverFile(string $image, int $width, int $height): string + { + clearstatcache(true, $image); + $mtime = is_file($image) ? filemtime($image) : 0; + $size = is_file($image) ? filesize($image) : 0; + $hash = hash('sha256', "{$image}#{$mtime}#{$size}#{$width}#{$height}"); + return runpath('runtime/slider/' . substr($hash, 0, 2) . '/' . $hash . '.png'); + } + + /** + * 读取源图片资源。 + * + * @return array{0:int,1:int,2:\GdImage|resource}|null + */ + private static function sourceImage(string $image): ?array + { + clearstatcache(true, $image); + if (!is_file($image) || !is_readable($image)) { + return null; + } + $size = @getimagesize($image); + if (!is_array($size) || empty($size[0]) || empty($size[1])) { + return null; + } + $data = file_get_contents($image); + if ($data === false) { + return null; + } + $srcim = @imagecreatefromstring($data); + if ($srcim === false) { + return null; + } + return [intval($size[0]), intval($size[1]), $srcim]; + } + + /** + * 在静态资源缺失时生成兜底背景图。 + * + * @return array{0:int,1:int,2:\GdImage|resource} + */ + private static function fallbackSource(string $image, int $width, int $height): array + { + $hash = hash('sha256', $image); + $palettes = [ + [[22, 66, 122], [120, 188, 222]], + [[31, 84, 68], [194, 235, 191]], + [[88, 44, 24], [236, 183, 90]], + [[58, 34, 96], [233, 205, 146]], + [[24, 58, 94], [218, 237, 255]], + ]; + [$start, $end] = $palettes[hexdec(substr($hash, 0, 2)) % count($palettes)]; + $imageim = imagecreatetruecolor($width, $height); + imagealphablending($imageim, true); + imagesavealpha($imageim, true); + + $scale = max($height - 1, 1); + for ($y = 0; $y < $height; ++$y) { + $color = imagecolorallocate( + $imageim, + (int)round($start[0] + ($end[0] - $start[0]) * $y / $scale), + (int)round($start[1] + ($end[1] - $start[1]) * $y / $scale), + (int)round($start[2] + ($end[2] - $start[2]) * $y / $scale), + ); + imageline($imageim, 0, $y, $width, $y, $color); + } + + $line = imagecolorallocatealpha($imageim, 255, 255, 255, 96); + for ($x = -$height; $x < $width + $height; $x += 48) { + imageline($imageim, $x, 0, $x + $height, $height, $line); + } + + for ($i = 0; $i < 4; ++$i) { + $offset = 2 + $i * 2; + $radius = 60 + hexdec(substr($hash, $offset, 2)) % 60; + $alpha = 88 + hexdec(substr($hash, $offset + 8, 2)) % 24; + $shape = imagecolorallocatealpha($imageim, 255, 255, 255, $alpha); + imagefilledellipse( + $imageim, + (int)(($i + 1) * $width / 5), + 30 + hexdec(substr($hash, $offset + 16, 2)) % max($height - 60, 1), + $radius * 2, + $radius * 2, + $shape + ); + } + + return [$width, $height, $imageim]; + } + + /** + * 保存裁剪缓存文件。 + */ + private static function storeCover($image, string $file): void + { + is_dir($dir = dirname($file)) || mkdir($dir, 0755, true); + imagepng($image, $file); + } + + /** + * 计算水印矩阵坐标。 + */ + private function withWaterPoint(): array + { + $waters = []; + $dr = $this->r * $this->r; + $lw = $this->r * 2 - 5; + $c1x = $lw + ($this->picWidth - $lw * 2) / 2; + $c1y = $this->r; + $c2x = $this->picHeight - $this->r; + $c2y = $lw + ($this->picHeight - $lw * 2) / 2; + + for ($i = 0; $i < $this->picHeight; ++$i) { + for ($j = 0; $j < $this->picWidth; ++$j) { + $d1 = pow($j - $c1x, 2) + pow($i - $c1y, 2); + $d2 = pow($j - $c2x, 2) + pow($i - $c2y, 2); + $waters[$i][$j] = (($i >= $lw && $j >= $lw && $i <= $this->picHeight - $lw && $j <= $this->picWidth - $lw) || $d1 <= $dr || $d2 <= $dr) ? 1 : 0; + } + } + return $waters; + } +} diff --git a/plugin/think-library/src/service/JsonRpcHttpClient.php b/plugin/think-library/src/service/JsonRpcHttpClient.php new file mode 100644 index 000000000..696e67b4a --- /dev/null +++ b/plugin/think-library/src/service/JsonRpcHttpClient.php @@ -0,0 +1,91 @@ +id = time(); + $this->proxy = $proxy; + $this->header = $header; + } + + /** + * 执行 JSON-RPC 请求。 + * + * @return mixed + * @throws Exception + */ + public function __call(string $method, array $params = []) + { + $request = json_encode([ + 'jsonrpc' => '2.0', + 'method' => $method, + 'params' => $params, + 'id' => $this->id, + ], JSON_UNESCAPED_UNICODE); + + $content = HttpClient::post($this->proxy, $request, [ + 'timeout' => 60, + 'returnHeader' => false, + 'headers' => array_merge([ + 'Content-Type: application/json', + 'User-Agent: think-admin-jsonrpc', + ], $this->header), + ]); + $response = json_decode((string)$content, true) ?: []; + if (empty($response)) { + throw new Exception(lang('Unable connect: %s', [$this->proxy])); + } + if (isset($response['code'], $response['info'])) { + throw new Exception($response['info'], (int)$response['code'], $response['data'] ?? []); + } + if (empty($response['id']) || $response['id'] !== $this->id) { + throw new Exception(lang('Error flag ( Request tag: %s, Response tag: %s )', [$this->id, $response['id'] ?? '-']), 0, $response); + } + if (is_null($response['error'] ?? null)) { + return $response['result'] ?? null; + } + throw new Exception($response['error']['message'], (int)$response['error']['code'], $response['result'] ?? []); + } +} diff --git a/plugin/think-library/src/service/JsonRpcHttpServer.php b/plugin/think-library/src/service/JsonRpcHttpServer.php new file mode 100644 index 000000000..095b19a39 --- /dev/null +++ b/plugin/think-library/src/service/JsonRpcHttpServer.php @@ -0,0 +1,158 @@ +app = $app; + } + + /** + * 静态实例对象。 + */ + public static function instance(...$args): JsonRpcHttpServer + { + return Container::getInstance()->make(self::class, $args); + } + + /** + * 设置监听对象。 + */ + public function handle(mixed $object): void + { + if ($this->app->request->method() !== 'POST' || $this->app->request->contentType() !== 'application/json') { + $this->printMethod($object); + return; + } + $request = json_decode(file_get_contents('php://input'), true) ?: []; + if (empty($request)) { + $response = $this->errorResponse('0', '-32700', lang('Syntax parsing error.'), lang('Invalid JSON parameter.')); + } elseif (!isset($request['id'], $request['method'], $request['params'])) { + $response = $this->errorResponse($request['id'] ?? '0', '-32600', lang('Invalid request.'), lang('Invalid JSON parameter.')); + } else { + $response = $this->dispatchRequest($object, $request); + } + throw new HttpResponseException(json($response)); + } + + /** + * 打印输出对象方法。 + */ + private function printMethod(mixed $object): void + { + try { + $object = new \ReflectionClass($object); + echo "

    {$object->getName()}


    "; + foreach ($object->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) { + if (stripos($method->getName(), '_') === 0) { + continue; + } + $params = []; + foreach ($method->getParameters() as $parameter) { + $type = $this->normalizeReflectionType($parameter->getType()); + $params[] = ($type ? "{$type} $" : '$') . $parameter->getName(); + } + $params = count($params) > 0 ? join(', ', $params) : ''; + echo '
    ' . nl2br($method->getDocComment() ?: '') . '
    '; + echo "
    {$object->getShortName()}::{$method->getName()}({$params})

    "; + } + } catch (\Exception $exception) { + echo "

    [{$exception->getCode()}] {$exception->getMessage()}

    "; + } + } + + /** + * 执行 RPC 方法调用。 + */ + private function dispatchRequest(mixed $object, array $request): array + { + try { + if ($object instanceof \Exception) { + throw $object; + } + if (strtolower($request['method']) === '_get_class_name_') { + return $this->successResponse($request['id'], get_class($object)); + } + if (!method_exists($object, $request['method'])) { + $info = lang('method not exists: %s::%s', [class_basename($object), $request['method']]); + return $this->errorResponse($request['id'], '-32601', $info, lang('The method does not exist or is invalid.')); + } + $result = call_user_func_array([$object, $request['method']], $request['params']); + return $this->successResponse($request['id'], $result); + } catch (\think\admin\Exception $exception) { + return $this->errorResponse($request['id'], (string)$exception->getCode(), lang($exception->getMessage()), lang('Business Exception.'), $exception->getData()); + } catch (\Exception $exception) { + return $this->errorResponse($request['id'], (string)$exception->getCode(), lang($exception->getMessage()), lang('System Exception.')); + } + } + + /** + * 构建错误响应。 + */ + private function errorResponse(mixed $id, string $code, string $message, string $meaning, mixed $result = null): array + { + return [ + 'jsonrpc' => '2.0', + 'id' => $id, + 'result' => $result, + 'error' => ['code' => $code, 'message' => $message, 'meaning' => $meaning], + ]; + } + + /** + * 构建成功响应。 + */ + private function successResponse(mixed $id, mixed $result): array + { + return ['jsonrpc' => '2.0', 'id' => $id, 'result' => $result, 'error' => null]; + } + + /** + * 标准化反射类型名称,兼容联合类型与交叉类型。 + */ + private function normalizeReflectionType(?\ReflectionType $type): string + { + if ($type instanceof \ReflectionNamedType) { + return $type->getName(); + } + if ($type instanceof \ReflectionUnionType) { + return implode('|', array_map(fn (\ReflectionNamedType $item) => $item->getName(), $type->getTypes())); + } + if ($type instanceof \ReflectionIntersectionType) { + return implode('&', array_map(fn (\ReflectionNamedType $item) => $item->getName(), $type->getTypes())); + } + return ''; + } +} diff --git a/plugin/think-library/src/service/JwtToken.php b/plugin/think-library/src/service/JwtToken.php new file mode 100644 index 000000000..e8213d753 --- /dev/null +++ b/plugin/think-library/src/service/JwtToken.php @@ -0,0 +1,226 @@ + 'JWT', 'alg' => 'HS256']; + + /** + * 支持的签名算法。 + */ + private const SIGN_TYPES = [ + 'HS256' => 'sha256', + 'HS384' => 'sha384', + 'HS512' => 'sha512', + ]; + + /** + * 标准 claim 字段集合。 + */ + private const CLAIM_FIELDS = ['iss', 'sub', 'aud', 'exp', 'iat', 'nbf']; + + /** + * 是否返回新令牌。 + */ + private static bool $rejwt = false; + + /** + * 当前请求解析后的令牌数据。 + */ + private static array $input = []; + + /** + * 是否需要回写新令牌。 + */ + public static function isRejwt(): bool + { + return self::$rejwt; + } + + /** + * 获取当前请求解析后的令牌数据。 + */ + public static function getInData(): array + { + return self::$input; + } + + /** + * 生成 jwt token。 + */ + public static function token(array $data = [], ?string $jwtkey = null, ?bool $rejwt = null): string + { + $jwtkey = self::jwtkey($jwtkey); + if (is_bool($rejwt)) { + self::$rejwt = $rejwt; + } + [$claims, $extra] = self::splitClaims($data); + $claims['enc'] = CodeToolkit::encrypt(json_encode($extra, JSON_UNESCAPED_UNICODE), $jwtkey); + $header = CodeToolkit::enSafe64(json_encode(self::HEADER, JSON_UNESCAPED_UNICODE)); + $payload = CodeToolkit::enSafe64(json_encode($claims, JSON_UNESCAPED_UNICODE)); + return "{$header}.{$payload}." . self::withSign("{$header}.{$payload}", self::HEADER['alg'], $jwtkey); + } + + /** + * 获取 JWT 密钥。 + */ + public static function jwtkey(?string $jwtkey = null): string + { + try { + if (!empty($jwtkey)) { + return $jwtkey; + } + $jwtkey = config('app.jwtkey'); + if (!empty($jwtkey)) { + return $jwtkey; + } + $jwtkey = function_exists('sysdata') ? sysdata('system.security.jwt_secret') : ''; + if (!empty($jwtkey)) { + return strval($jwtkey); + } + $jwtkey = bin2hex(random_bytes(16)); + function_exists('sysdata') && sysdata('system.security.jwt_secret', $jwtkey); + return $jwtkey; + } catch (\Exception $exception) { + trace_file($exception); + return 'thinkadmin'; + } + } + + /** + * 验证 token 是否有效,默认验证 exp、nbf、iat 时间。 + * + * @throws Exception + */ + public static function verify(string $token, ?string $jwtkey = null): array + { + [$base64header, $base64payload, $signature] = self::splitToken($token); + $header = self::decodeSegment($base64header); + if (empty($header['alg'])) { + throw new Exception('数据解密失败!', 0, []); + } + $jwtkey = self::jwtkey($jwtkey); + if (self::withSign("{$base64header}.{$base64payload}", $header['alg'], $jwtkey) !== $signature) { + throw new Exception('验证签名失败!', 0, []); + } + $payload = self::decodeSegment($base64payload); + self::validatePayload($payload); + $extra = []; + if (isset($payload['enc'])) { + $extra = json_decode(CodeToolkit::decrypt($payload['enc'], $jwtkey), true) ?: []; + unset($payload['enc']); + } + return self::$input = array_merge($payload, $extra); + } + + /** + * 输出模板变量。 + * 这是旧接口能力,仍然保留在这里,避免影响现有 API 控制器。 + */ + public static function fetch(Controller $class, array $vars = []): void + { + $ignore = array_keys(get_class_vars(Controller::class)); + foreach (get_object_vars($class) as $name => $value) { + if (!in_array($name, $ignore, true)) { + if (is_array($value) || is_numeric($value) || is_string($value) || is_bool($value) || is_null($value)) { + $vars[$name] = $value; + } + } + } + $class->success('获取变量成功!', $vars); + } + + /** + * 拆分 JWT 标准 claim 与业务扩展数据。 + */ + private static function splitClaims(array $data): array + { + $claims = ['iat' => time()]; + foreach ($data as $k => $v) { + if (in_array($k, self::CLAIM_FIELDS, true)) { + $claims[$k] = $v; + unset($data[$k]); + } + } + return [$claims, $data]; + } + + /** + * 生成数据签名。 + */ + private static function withSign(string $input, string $alg = 'HS256', ?string $key = null): string + { + return CodeToolkit::enSafe64(hash_hmac(self::SIGN_TYPES[$alg], $input, self::jwtkey($key), true)); + } + + /** + * 拆分 token 三段结构。 + * + * @throws Exception + */ + private static function splitToken(string $token): array + { + $tokens = explode('.', $token); + if (count($tokens) !== 3) { + throw new Exception('数据解密失败!', 0, []); + } + return $tokens; + } + + /** + * 解码 JWT 段内容。 + */ + private static function decodeSegment(string $segment): array + { + return json_decode(CodeToolkit::deSafe64($segment), true) ?: []; + } + + /** + * 验证 token 时间有效性。 + * + * @throws Exception + */ + private static function validatePayload(array $payload): void + { + $time = time(); + if (isset($payload['iat']) && $payload['iat'] > $time) { + throw new Exception('服务器时间验证失败!', 0, $payload); + } + if (isset($payload['exp']) && $payload['exp'] < $time) { + throw new Exception('服务器时间验证失败!', 0, $payload); + } + if (isset($payload['nbf']) && $payload['nbf'] > $time) { + throw new Exception('不接收处理该TOKEN', 0, $payload); + } + } +} diff --git a/plugin/think-library/src/service/ModuleService.php b/plugin/think-library/src/service/ModuleService.php new file mode 100644 index 000000000..c6242b6f0 --- /dev/null +++ b/plugin/think-library/src/service/ModuleService.php @@ -0,0 +1,21 @@ +config->get('app.app_namespace') ?: 'app'; + return empty($suffix) ? $default : trim($default . '\\' . trim($suffix, '\/'), '\\'); + } + + /** + * 获取完整节点名称. + */ + public static function fullNode(?string $node = ''): string + { + if (empty($node)) { + return self::getCurrent(); + } + switch (count($attrs = explode('/', $node))) { + case 1: + return self::getCurrent('controller') . '/' . strtolower($node); + case 2: + $suffix = self::nameTolower($attrs[0]) . '/' . $attrs[1]; + return self::getCurrent('module') . '/' . strtolower($suffix); + default: + $attrs[1] = self::nameTolower($attrs[1]); + return strtolower(join('/', $attrs)); + } + } + + /** + * 获取当前节点名称. + */ + public static function getCurrent(string $type = ''): string + { + $appname = strtolower(Library::$sapp->http->getName()); + if (in_array($type, ['app', 'module'])) { + return $appname; + } + $controller = self::nameTolower(Library::$sapp->request->controller()); + if ($type === 'controller') { + return "{$appname}/{$controller}"; + } + $method = strtolower(Library::$sapp->request->action()); + return "{$appname}/{$controller}/{$method}"; + } + + /** + * 获取节点名称. + */ + public static function nameTolower(string $name): string + { + $dots = []; + foreach (explode('.', strtr($name, '/', '.')) as $dot) { + $dots[] = trim(preg_replace('/[A-Z]/', '_\0', $dot), '_'); + } + return strtolower(join('.', $dots)); + } + + /** + * 获取应用节点列表. + */ + public static function getMethods(bool $force = false): array + { + $skey = 'think.admin.methods'; + if (empty($force)) { + $data = sysvar($skey) ?: Library::$sapp->cache->get('SystemAuthNode', []); + if (count($data) > 0) { + return sysvar($skey, $data); + } + } else { + $data = []; + } + $ignoreMethods = get_class_methods('\think\admin\Controller'); + $ignoreAppNames = Library::$sapp->config->get('app.rbac_ignore', []); + foreach (AppService::all() as $appName => $app) { + if (in_array($appName, $ignoreAppNames)) { + continue; + } + if (empty($app['path']) || !is_dir($app['path'])) { + continue; + } + foreach (FileTools::scan($app['path'], null, 'php') as $name) { + if (preg_match('|^.*?controller/(.+)\.php$|i', strtr($name, '\\', '/'), $matches)) { + self::parseClass($appName, $app['space'], $matches[1], $ignoreMethods, $data); + } + } + } + if (function_exists('admin_node_filter')) { + $data = call_user_func('admin_node_filter', $data); + } + Library::$sapp->cache->set('SystemAuthNode', $data); + return sysvar($skey, $data); + } + + /** + * 解析类文件中的方法注释. + */ + private static function parseClass(string $appName, string $appSpace, string $className, array $ignoreNode, array &$data): void + { + $classfull = strtr("{$appSpace}/controller/{$className}", '/', '\\'); + if (class_exists($classfull) && ($class = new \ReflectionClass($classfull))) { + $prefix = strtolower(strtr("{$appName}/" . self::nameTolower($className), '\\', '/')); + $data[$prefix] = self::parseComment($class->getDocComment() ?: '', $className); + foreach ($class->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) { + if (in_array($metname = $method->getName(), $ignoreNode)) { + continue; + } + $data[strtolower("{$prefix}/{$metname}")] = self::parseComment($method->getDocComment() ?: '', $metname); + } + } + } + + /** + * 解析方法注释信息. + */ + private static function parseComment(string $comment, string $default = ''): array + { + $text = strtr($comment, "\n", ' '); + $title = preg_replace('/^\/\*\s*\*\s*\*\s*(.*?)\s*\*.*?$/', '$1', $text); + if (in_array(substr($title, 0, 5), ['@auth', '@menu', '@logi'])) { + $title = $default; + } + return [ + 'title' => $title ?: $default, + 'isauth' => intval(preg_match('/@auth\s*true/i', $text)), + 'ismenu' => intval(preg_match('/@menu\s*true/i', $text)), + 'islogin' => intval(preg_match('/@login\s*true/i', $text)), + ]; + } +} diff --git a/plugin/think-library/src/service/QueueService.php b/plugin/think-library/src/service/QueueService.php new file mode 100644 index 000000000..31279096d --- /dev/null +++ b/plugin/think-library/src/service/QueueService.php @@ -0,0 +1,85 @@ +registerTask($title, $command, $later, $data, $loops, $legacyLoops); + } + + /** + * Get the current queue task code. + */ + public static function currentCode(): string + { + return self::provider()->getCurrentCode(); + } + + /** + * Check if the current request is in the queue context. + */ + public static function inContext(?string $code = null): bool + { + return self::provider()->isInContext($code); + } + + /** + * Resolve the concrete queue provider. + */ + private static function provider(array $vars = [], bool $newInstance = false): QueueManagerInterface + { + $container = Container::getInstance(); + if (!$container->bound(self::BIND_NAME)) { + throw new \RuntimeException('ThinkPlugsWorker is required for queue runtime operations.'); + } + + $provider = $container->make($container->getAlias(self::BIND_NAME), $vars, $newInstance); + if (!$provider instanceof QueueManagerInterface) { + throw new \RuntimeException('Queue runtime provider must implement think\admin\contract\QueueManagerInterface.'); + } + + return $provider; + } +} diff --git a/plugin/think-library/src/service/ResponseModeService.php b/plugin/think-library/src/service/ResponseModeService.php new file mode 100644 index 000000000..14944f925 --- /dev/null +++ b/plugin/think-library/src/service/ResponseModeService.php @@ -0,0 +1,116 @@ +header('accept', ''))); + if (str_contains($accept, 'application/json')) { + return self::MODE_API; + } + + $output = strtolower(trim(strval($request->param('output', '')))); + if (in_array($output, ['json', 'layui.table'], true)) { + return self::MODE_API; + } + + return self::MODE_VIEW; + } + + /** + * 当前请求是否走 API 模式. + */ + public static function prefersApi(?Request $request = null, string $controllerClass = ''): bool + { + return self::resolve($request, $controllerClass) === self::MODE_API; + } + + /** + * 获取 API Token 请求头名称. + */ + public static function apiHeader(): string + { + $header = trim(strval(config('app.presentation.api_header', 'Authorization'))); + return $header !== '' ? $header : 'Authorization'; + } + + /** + * 获取配置声明的模式. + */ + public static function configuredMode(): string + { + return self::normalizeMode(strval(config('app.presentation.mode', self::MODE_MIXED))); + } + + /** + * 获取当前请求头 Token. + */ + public static function headerToken(?Request $request = null): string + { + $request ??= request(); + return RequestTokenService::parseHeaderToken(strval($request->header(self::apiHeader(), ''))); + } + + /** + * 规范化模式值. + */ + private static function normalizeMode(string $mode): string + { + $mode = strtolower(trim($mode)); + return in_array($mode, [self::MODE_VIEW, self::MODE_API], true) ? $mode : self::MODE_MIXED; + } + + /** + * 是否为显式 API 控制器. + */ + private static function isApiController(string $controllerClass): bool + { + return $controllerClass !== '' && str_contains(str_replace('/', '\\', $controllerClass), '\\controller\\api\\'); + } +} diff --git a/plugin/think-library/src/service/RuntimeService.php b/plugin/think-library/src/service/RuntimeService.php new file mode 100644 index 000000000..89509a4c6 --- /dev/null +++ b/plugin/think-library/src/service/RuntimeService.php @@ -0,0 +1,238 @@ +debug($data['mode'] !== 'product')->isDebug(); + } + + /** + * 获取动态配置. + * @param null|string $name 配置名称 + * @param array $default 配置内容 + */ + public static function get(?string $name = null, array $default = []): array|string + { + $keys = 'think.admin.runtime'; + if (empty($envs = sysvar($keys) ?: [])) { + // 读取默认配置 + clearstatcache(true, self::$envFile); + is_file(self::$envFile) && Library::$sapp->env->load(self::$envFile); + // 动态判断赋值 + $envs['mode'] = Library::$sapp->env->get('RUNTIME_MODE') ?: 'debug'; + $envs['appmap'] = []; + $envs['domain'] = []; + sysvar($keys, $envs); + } + return is_null($name) ? $envs : ($envs[$name] ?? $default); + } + + /** + * 开发模式运行. + */ + public static function isDebug(): bool + { + return self::get('mode') !== 'product'; + } + + /** + * 压缩发布项目. + */ + public static function push(): string + { + self::set('product'); + $connection = Library::$sapp->db->getConfig('default'); + Library::$sapp->console->call('optimize:schema', ["--connection={$connection}"]); + return $connection; + } + + /** + * 设置动态配置. + * @param null|string $mode 支持模式 + * @param null|array $appmap 历史保留参数(已停用) + * @param null|array $domain 历史保留参数(已停用) + * @return bool 是否调试模式 + */ + public static function set(?string $mode = null, ?array $appmap = [], ?array $domain = []): bool + { + $envs = self::get(); + $envs['mode'] = is_null($mode) ? $envs['mode'] : $mode; + $envs['appmap'] = []; + $envs['domain'] = []; + + // 组装配置文件格式 + $rows[] = "mode = {$envs['mode']}"; + + is_dir($dir = dirname(self::$envFile)) || @mkdir($dir, 0777, true); + + // 写入并刷新文件哈希值 + @file_put_contents(self::$envFile, "[RUNTIME]\n" . join("\n", $rows)); + + // 同步更新当前环境 + sysvar('think.admin.runtime', $envs); + + // 应用当前的配置文件 + return self::apply($envs); + } + + /** + * 判断运行环境. + * @param string $type 运行模式(dev|demo|local) + */ + public static function check(string $type = 'dev'): bool + { + $domain = Library::$sapp->request->host(true); + $isDemo = boolval(preg_match('|v\d+\.thinkadmin\.top|', $domain)); + $isLocal = $domain === '127.0.0.1' || is_numeric(stripos($domain, 'local')); + if ($type === self::MODE_DEV) { + return $isLocal || $isDemo; + } + if ($type === self::MODE_DEMO) { + return $isDemo; + } + if ($type === self::MODE_LOCAL) { + return $isLocal; + } + return true; + } + + /** + * 清理运行缓存. + * @param bool $force 清理目录 + */ + public static function clear(bool $force = true): bool + { + $data = self::get(); + SystemContext::instance()->clearAuth() && Library::$sapp->cache->clear(); + $force && Library::$sapp->console->call('clear', ['--dir']); + self::set($data['mode']); + return true; + } + + /** + * 生产模式运行. + */ + public static function isOnline(): bool + { + return self::get('mode') === 'product'; + } + + /** + * 初始化主程序. + */ + public static function doWebsiteInit(?App $app = null, ?Request $request = null): Response + { + $http = self::init($app)->http; + $request = $request ?: Library::$sapp->make(Request::class); + Library::$sapp->instance('request', $request); + ($response = $http->run($request))->send(); + $http->end($response); + return $response; + } + + /** + * 系统服务初始化. + */ + public static function init(?App $app = null): App + { + // 初始化运行环境 + Library::$sapp = $app ?: Container::getInstance()->make(App::class); + Library::$sapp->bind('think\Route', Route::class); + Library::$sapp->bind('think\route\Url', Url::class); + // 初始化运行配置位置 + // 运行配置固定落在可写目录(Phar 环境为安装目录,普通环境为项目根目录) + self::$envFile = runpath('runtime/.env'); + return Library::$sapp->debug(self::isDebug()); + } + + /** + * 初始化命令行. + */ + public static function doConsoleInit(?App $app = null): int + { + try { + return self::init($app)->console->run(); + } catch (\Exception $exception) { + print_r($exception->getMessage() . PHP_EOL); + return 0; + } + } +} diff --git a/plugin/think-library/tests/AppServiceTest.php b/plugin/think-library/tests/AppServiceTest.php new file mode 100644 index 000000000..63f78e6e2 --- /dev/null +++ b/plugin/think-library/tests/AppServiceTest.php @@ -0,0 +1,107 @@ +initialize(); + AppService::clear(); + } + + public function testMenusAcceptPluginDefinitionArray(): void + { + $plugin = AppService::resolvePlugin('system', true); + $menus = AppService::menus('system', false, true); + + $this->assertIsArray($plugin); + $this->assertNotSame([], $plugin); + $this->assertSame('plugin\system\Service', $plugin['service'] ?? null); + $this->assertTrue(boolval($plugin['show'] ?? false)); + $this->assertFalse(method_exists($plugin['service'], 'menu')); + $this->assertNotSame([], $menus); + $this->assertSame('system/config/index', $menus[0]['subs'][0]['node'] ?? null); + $this->assertSame( + $menus, + AppService::menus($plugin, false, true) + ); + } + + public function testRuntimeMenusAreLoadedFromComposerMetadata(): void + { + foreach ($this->pluginComposerManifests() as $service => $manifest) { + $app = (array)($manifest['extra']['xadmin']['app'] ?? []); + $meta = (array)($manifest['extra']['xadmin']['menu'] ?? []); + $menus = (array)($manifest['extra']['xadmin']['menu']['items'] ?? []); + $show = !array_key_exists('show', $meta) || !empty($meta['show']); + $this->assertTrue(class_exists($service), "{$service} must be autoloadable"); + $this->assertFalse(method_exists($service, 'menu'), "{$service} should not declare menu()"); + if (array_key_exists('code', $app)) { + $this->assertSame(strval($app['code']), $service::getAppCode(), "{$service} code must come from composer metadata"); + } + if (array_key_exists('name', $app)) { + $this->assertSame(strval($app['name']), $service::getAppName(), "{$service} name must come from composer metadata"); + } + if (array_key_exists('prefix', $app)) { + $this->assertSame(strval($app['prefix']), $service::getAppPrefix(), "{$service} prefix must come from composer metadata"); + } + $this->assertSame($show, $service::getMenuShow(), "{$service} menu show flag must come from composer metadata"); + $this->assertSame($menus, $service::getMenus(), "{$service} menus must come from composer metadata"); + } + } + + /** + * @return array> + */ + private function pluginComposerManifests(): array + { + $items = []; + foreach (glob(TEST_PROJECT_ROOT . '/plugin/*/composer.json') ?: [] as $file) { + $manifest = json_decode((string)file_get_contents($file), true); + if (!is_array($manifest)) { + continue; + } + $services = (array)($manifest['extra']['think']['services'] ?? []); + $service = strval($services[0] ?? ''); + if ($service === '' || !class_exists($service) || !is_subclass_of($service, Plugin::class)) { + continue; + } + $items[$service] = $manifest; + } + + ksort($items); + return $items; + } +} diff --git a/plugin/think-library/tests/ArchitectureBoundaryTest.php b/plugin/think-library/tests/ArchitectureBoundaryTest.php new file mode 100644 index 000000000..3d04edc4e --- /dev/null +++ b/plugin/think-library/tests/ArchitectureBoundaryTest.php @@ -0,0 +1,442 @@ +projectRoot = TEST_PROJECT_ROOT; + } + + public function testLibraryServiceFilesStayInServiceDirectory(): void + { + $this->assertFileExists($this->path('plugin/think-library/src/service/CacheSession.php')); + $this->assertFileExists($this->path('plugin/think-library/src/service/QueueService.php')); + $this->assertFileExists($this->path('plugin/think-library/src/service/RuntimeService.php')); + $this->assertFileExists($this->path('plugin/think-library/src/service/JwtToken.php')); + $this->assertFileExists($this->path('plugin/think-library/src/service/NodeService.php')); + $this->assertFileExists($this->path('plugin/think-library/src/service/AppService.php')); + $this->assertFileExists($this->path('plugin/think-library/src/Helper.php')); + $this->assertFileExists($this->path('plugin/think-library/src/runtime/RequestTokenService.php')); + $this->assertFileExists($this->path('plugin/think-library/src/builder/form/FormBuilder.php')); + $this->assertFileExists($this->path('plugin/think-library/src/builder/page/PageBuilder.php')); + + $this->assertFileDoesNotExist($this->path('plugin/think-library/src/helper/Helper.php')); + $this->assertFileDoesNotExist($this->path('plugin/think-library/src/service/FormBuilder.php')); + $this->assertFileDoesNotExist($this->path('plugin/think-library/src/service/PageBuilder.php')); + $this->assertFileDoesNotExist($this->path('plugin/think-plugs-system/src/service/FormBuilder.php')); + $this->assertFileDoesNotExist($this->path('plugin/think-plugs-system/src/service/PageBuilder.php')); + $this->assertFileDoesNotExist($this->path('plugin/think-library/src/auth/CacheSession.php')); + $this->assertFileDoesNotExist($this->path('plugin/think-library/src/auth/RequestTokenService.php')); + $this->assertFileDoesNotExist($this->path('plugin/think-library/src/system/NodeService.php')); + $this->assertFileDoesNotExist($this->path('plugin/think-library/src/runtime/AppService.php')); + $this->assertFileDoesNotExist($this->path('plugin/think-library/src/runtime/QueueService.php')); + $this->assertFileDoesNotExist($this->path('plugin/think-library/src/service/ProcessService.php')); + $this->assertFileDoesNotExist($this->path('plugin/think-library/src/runtime/ProcessService.php')); + $this->assertFileDoesNotExist($this->path('plugin/think-library/src/runtime/RuntimeService.php')); + $this->assertFileExists($this->path('plugin/think-library/src/service/FaviconBuilder.php')); + $this->assertFileExists($this->path('plugin/think-library/src/service/ImageSliderVerify.php')); + $this->assertFileExists($this->path('plugin/think-library/src/service/JsonRpcHttpClient.php')); + $this->assertFileExists($this->path('plugin/think-library/src/service/JsonRpcHttpServer.php')); + $this->assertFileDoesNotExist($this->path('plugin/think-library/src/extend/JwtToken.php')); + $this->assertFileDoesNotExist($this->path('plugin/think-library/src/extend/FaviconBuilder.php')); + $this->assertFileDoesNotExist($this->path('plugin/think-library/src/extend/ImageSliderVerify.php')); + $this->assertFileDoesNotExist($this->path('plugin/think-library/src/extend/JsonRpcHttpClient.php')); + $this->assertFileDoesNotExist($this->path('plugin/think-library/src/extend/JsonRpcHttpServer.php')); + $this->assertDirectoryDoesNotExist($this->path('plugin/think-library/src/system')); + $this->assertDirectoryDoesNotExist($this->path('plugin/think-library/src/auth')); + } + + public function testLibrarySourceDirectoriesStayStandardized(): void + { + $dirs = array_values(array_filter(scandir($this->path('plugin/think-library/src')) ?: [], function ($name) { + return $name !== '.' && $name !== '..' && is_dir($this->path("plugin/think-library/src/{$name}")); + })); + sort($dirs); + + $this->assertSame(['builder', 'contract', 'extend', 'helper', 'middleware', 'model', 'route', 'runtime', 'service'], $dirs); + } + + public function testLibraryRouteFilesStayInRouteDirectory(): void + { + $this->assertFileExists($this->path('plugin/think-library/src/route/Route.php')); + $this->assertFileExists($this->path('plugin/think-library/src/route/Url.php')); + $this->assertFileDoesNotExist($this->path('plugin/think-library/src/runtime/Route.php')); + $this->assertFileDoesNotExist($this->path('plugin/think-library/src/runtime/Url.php')); + } + + public function testLibraryMiddlewareFilesStayInMiddlewareDirectory(): void + { + $this->assertFileExists($this->path('plugin/think-library/src/middleware/MultAccess.php')); + $this->assertFileDoesNotExist($this->path('plugin/think-library/src/runtime/MultAccess.php')); + } + + public function testLibraryRuntimeFilesStayContextFocused(): void + { + $files = array_values(array_filter(scandir($this->path('plugin/think-library/src/runtime')) ?: [], function ($name) { + return $this->isVisibleFile($name, "plugin/think-library/src/runtime/{$name}"); + })); + sort($files); + + $this->assertSame(['NullSystemContext.php', 'RequestContext.php', 'RequestTokenService.php', 'SystemContext.php'], $files); + } + + public function testLibraryExtendFilesStayUtilityOnly(): void + { + $files = array_values(array_filter(scandir($this->path('plugin/think-library/src/extend')) ?: [], function ($name) { + return $this->isVisibleFile($name, "plugin/think-library/src/extend/{$name}"); + })); + sort($files); + + $this->assertSame(['ArrayTree.php', 'CodeExtend.php', 'CodeToolkit.php', 'DataExtend.php', 'FileTools.php', 'HttpClient.php', 'JsonRpcClient.php'], $files); + } + + public function testCaptchaServiceLivesInSystemServiceDirectory(): void + { + $this->assertFileExists($this->path('plugin/think-plugs-system/src/service/CaptchaService.php')); + $this->assertFileExists($this->path('plugin/think-plugs-system/src/service/bin/captcha.ttf')); + + $this->assertFileDoesNotExist($this->path('plugin/think-library/src/auth/CaptchaService.php')); + $this->assertDirectoryDoesNotExist($this->path('plugin/think-library/src/auth/bin')); + $this->assertDirectoryDoesNotExist($this->path('plugin/think-plugs-system/src/auth')); + } + + public function testSystemContextAndAuthSymbolsUseSystemNaming(): void + { + $this->assertFileExists($this->path('plugin/think-library/src/contract/SystemContextInterface.php')); + $this->assertFileExists($this->path('plugin/think-library/src/runtime/SystemContext.php')); + $this->assertFileExists($this->path('plugin/think-library/src/runtime/NullSystemContext.php')); + $this->assertFileExists($this->path('plugin/think-plugs-system/src/service/SystemContext.php')); + $this->assertFileExists($this->path('plugin/think-plugs-system/src/service/SystemService.php')); + $this->assertFileExists($this->path('plugin/think-plugs-system/src/service/AuthService.php')); + $this->assertFileExists($this->path('plugin/think-plugs-system/src/middleware/JwtTokenAuth.php')); + $this->assertFileExists($this->path('plugin/think-plugs-system/src/middleware/RbacAccess.php')); + + $this->assertFileDoesNotExist($this->path('plugin/think-library/src/contract/AdminContextInterface.php')); + $this->assertFileDoesNotExist($this->path('plugin/think-library/src/runtime/AdminContext.php')); + $this->assertFileDoesNotExist($this->path('plugin/think-library/src/runtime/NullAdminContext.php')); + $this->assertDirectoryDoesNotExist($this->path('plugin/think-plugs-system/src/runtime')); + $this->assertDirectoryDoesNotExist($this->path('plugin/think-plugs-system/src/system')); + $this->assertFileDoesNotExist($this->path('plugin/think-plugs-system/src/runtime/AdminContext.php')); + $this->assertFileDoesNotExist($this->path('plugin/think-plugs-system/src/auth/AdminService.php')); + $this->assertFileDoesNotExist($this->path('plugin/think-plugs-system/src/auth/SystemAuthService.php')); + $this->assertFileDoesNotExist($this->path('plugin/think-plugs-system/src/service/JwtTokenAuth.php')); + $this->assertFileDoesNotExist($this->path('plugin/think-plugs-system/src/service/RbacAccess.php')); + $this->assertFileDoesNotExist($this->path('plugin/think-plugs-system/src/runtime/SystemContext.php')); + $this->assertFileDoesNotExist($this->path('plugin/think-plugs-system/src/system/SystemService.php')); + $this->assertFileDoesNotExist($this->path('plugin/think-plugs-system/src/service/FaviconBuilder.php')); + } + + public function testSystemPluginSourceDirectoriesStayStandardized(): void + { + $dirs = array_values(array_filter(scandir($this->path('plugin/think-plugs-system/src')) ?: [], function ($name) { + return $name !== '.' && $name !== '..' && is_dir($this->path("plugin/think-plugs-system/src/{$name}")); + })); + sort($dirs); + + $this->assertSame(['builder', 'controller', 'helper', 'lang', 'middleware', 'model', 'route', 'service', 'storage', 'view'], $dirs); + } + + public function testLegacyViewPluginDirectoriesStayRemoved(): void + { + $this->assertDirectoryDoesNotExist($this->path('plugin/think-plugs-view')); + $this->assertDirectoryDoesNotExist($this->path('plugin/think-plugs-system-view')); + $this->assertDirectoryDoesNotExist($this->path('plugin/think-plugs-wechat-client-view')); + $this->assertDirectoryDoesNotExist($this->path('plugin/think-plugs-wechat-service-view')); + } + + /** + * 存储能力已合并至 system 插件的 src/storage,独立 think-plugs-storage 包已移除。 + */ + public function testSystemPluginStorageSourceLayout(): void + { + $this->assertDirectoryDoesNotExist($this->path('plugin/think-plugs-storage/src')); + $this->assertFileDoesNotExist($this->path('plugin/think-plugs-storage/Service.php')); + $this->assertFileExists($this->path('plugin/think-plugs-system/src/storage/StorageConfig.php')); + $this->assertFileExists($this->path('plugin/think-plugs-system/src/storage/StorageManager.php')); + $this->assertFileExists($this->path('plugin/think-plugs-system/src/storage/StorageAuthorize.php')); + $this->assertFileExists($this->path('plugin/think-plugs-system/src/storage/LocalStorage.php')); + } + + public function testAccountPluginSourceDirectoriesStayStandardized(): void + { + $dirs = array_values(array_filter(scandir($this->path('plugin/think-plugs-account/src')) ?: [], function ($name) { + return $name !== '.' && $name !== '..' && is_dir($this->path("plugin/think-plugs-account/src/{$name}")); + })); + sort($dirs); + + $this->assertSame(['controller', 'lang', 'model', 'service', 'view'], $dirs); + $this->assertFileDoesNotExist($this->path('plugin/think-plugs-account/src/service/ImageSliderVerify.php')); + } + + public function testWorkerPluginSourceDirectoriesStayStandardized(): void + { + $dirs = array_values(array_filter(scandir($this->path('plugin/think-plugs-worker/src')) ?: [], function ($name) { + return $name !== '.' && $name !== '..' && is_dir($this->path("plugin/think-plugs-worker/src/{$name}")); + })); + sort($dirs); + + $this->assertSame(['command', 'model', 'service'], $dirs); + $this->assertDirectoryDoesNotExist($this->path('plugin/think-plugs-worker/src/queue')); + $this->assertDirectoryDoesNotExist($this->path('plugin/think-plugs-worker/src/support')); + $this->assertFileDoesNotExist($this->path('plugin/think-plugs-worker/src/Server.php')); + $this->assertFileExists($this->path('plugin/think-plugs-worker/src/service/Server.php')); + $this->assertFileExists($this->path('plugin/think-plugs-worker/src/service/QueueService.php')); + $this->assertFileExists($this->path('plugin/think-plugs-worker/src/service/ProcessService.php')); + } + + public function testSystemHelperSourceDirectoriesStayStandardized(): void + { + $this->assertDirectoryDoesNotExist($this->path('plugin/think-plugs-helper')); + + $dirs = array_values(array_filter(scandir($this->path('plugin/think-plugs-system/src/helper')) ?: [], function ($name) { + return $name !== '.' && $name !== '..' && is_dir($this->path("plugin/think-plugs-system/src/helper/{$name}")); + })); + sort($dirs); + + $this->assertSame(['command', 'database', 'integration', 'migration', 'model', 'plugin', 'service'], $dirs); + $this->assertDirectoryDoesNotExist($this->path('plugin/think-plugs-system/src/helper/support')); + $this->assertFileDoesNotExist($this->path('plugin/think-plugs-system/src/helper/command/DbMigrateStruct.php')); + $this->assertFileDoesNotExist($this->path('plugin/think-plugs-system/src/helper/command/DbModelStruct.php')); + $this->assertFileDoesNotExist($this->path('plugin/think-plugs-system/src/helper/command/DbBackupStruct.php')); + $this->assertFileExists($this->path('plugin/think-plugs-system/src/helper/command/database/MigrateCommand.php')); + $this->assertFileExists($this->path('plugin/think-plugs-system/src/helper/command/database/ModelCommand.php')); + $this->assertFileExists($this->path('plugin/think-plugs-system/src/helper/command/database/BackupCommand.php')); + $this->assertFileExists($this->path('plugin/think-plugs-system/src/helper/plugin/PluginMenuService.php')); + $this->assertFileExists($this->path('plugin/think-plugs-system/src/helper/plugin/PluginRegistry.php')); + $this->assertFileExists($this->path('plugin/think-plugs-system/src/helper/migration/PhinxExtend.php')); + $this->assertFileExists($this->path('plugin/think-plugs-system/src/helper/integration/ExpressService.php')); + } + + public function testWechatServicePluginSourceDirectoriesStayStandardized(): void + { + $dirs = array_values(array_filter(scandir($this->path('plugin/think-plugs-wechat-service/src')) ?: [], function ($name) { + return $name !== '.' && $name !== '..' && is_dir($this->path("plugin/think-plugs-wechat-service/src/{$name}")); + })); + sort($dirs); + + $this->assertSame(['command', 'controller', 'lang', 'model', 'service', 'view'], $dirs); + $this->assertDirectoryDoesNotExist($this->path('plugin/think-plugs-wechat-service/src/handle')); + $this->assertFileDoesNotExist($this->path('plugin/think-plugs-wechat-service/src/AuthService.php')); + $this->assertFileDoesNotExist($this->path('plugin/think-plugs-wechat-service/src/ConfigService.php')); + $this->assertFileExists($this->path('plugin/think-plugs-wechat-service/src/service/AuthService.php')); + $this->assertFileExists($this->path('plugin/think-plugs-wechat-service/src/service/ConfigService.php')); + $this->assertFileDoesNotExist($this->path('plugin/think-plugs-wechat-service/src/service/JsonRpcHttpServer.php')); + $this->assertFileExists($this->path('plugin/think-plugs-wechat-service/src/service/PublishHandle.php')); + $this->assertFileExists($this->path('plugin/think-plugs-wechat-service/src/service/ReceiveHandle.php')); + } + + public function testWechatClientPluginSourceDirectoriesStayStandardized(): void + { + $dirs = array_values(array_filter(scandir($this->path('plugin/think-plugs-wechat-client/src')) ?: [], function ($name) { + return $name !== '.' && $name !== '..' && is_dir($this->path("plugin/think-plugs-wechat-client/src/{$name}")); + })); + sort($dirs); + + $this->assertSame(['command', 'controller', 'lang', 'model', 'service', 'view'], $dirs); + $this->assertFileDoesNotExist($this->path('plugin/think-plugs-wechat-client/src/service/JsonRpcHttpClient.php')); + } + + public function testPaymentPluginSourceDirectoriesStayStandardized(): void + { + $dirs = array_values(array_filter(scandir($this->path('plugin/think-plugs-payment/src')) ?: [], function ($name) { + return $name !== '.' && $name !== '..' && is_dir($this->path("plugin/think-plugs-payment/src/{$name}")); + })); + sort($dirs); + + $this->assertSame(['controller', 'lang', 'model', 'service', 'view'], $dirs); + $this->assertDirectoryDoesNotExist($this->path('plugin/think-plugs-payment/src/queue')); + $this->assertFileExists($this->path('plugin/think-plugs-payment/src/service/Recount.php')); + $this->assertFileDoesNotExist($this->path('plugin/think-plugs-payment/src/queue/Recount.php')); + } + + public function testWemallPluginSourceDirectoriesStayStandardized(): void + { + $dirs = array_values(array_filter(scandir($this->path('plugin/think-plugs-wemall/src')) ?: [], function ($name) { + return $name !== '.' && $name !== '..' && is_dir($this->path("plugin/think-plugs-wemall/src/{$name}")); + })); + sort($dirs); + + $this->assertSame(['command', 'controller', 'lang', 'model', 'service', 'view'], $dirs); + $this->assertDirectoryDoesNotExist($this->path('plugin/think-plugs-wemall/src/integration')); + $this->assertFileExists($this->path('plugin/think-plugs-wemall/src/service/OpenApiService.php')); + $this->assertFileDoesNotExist($this->path('plugin/think-plugs-wemall/src/integration/OpenApiService.php')); + } + + public function testWumaPluginSourceDirectoriesStayStandardized(): void + { + $dirs = array_values(array_filter(scandir($this->path('plugin/think-plugs-wuma/src')) ?: [], function ($name) { + return $name !== '.' && $name !== '..' && is_dir($this->path("plugin/think-plugs-wuma/src/{$name}")); + })); + sort($dirs); + + $this->assertSame(['command', 'controller', 'lang', 'model', 'service', 'view'], $dirs); + } + + public function testPluginSourceRootFilesStayMinimal(): void + { + $allowed = [ + 'think-plugs-account' => ['Service.php'], + 'think-plugs-payment' => ['Service.php'], + 'think-plugs-system' => ['Service.php', 'common.php'], + 'think-plugs-wechat-client' => ['Service.php'], + 'think-plugs-wechat-service' => ['Service.php'], + 'think-plugs-wemall' => ['Service.php', 'common.php'], + 'think-plugs-worker' => ['Service.php', 'common.php'], + 'think-plugs-wuma' => ['Service.php'], + ]; + + foreach ($allowed as $plugin => $expected) { + $files = array_values(array_filter(scandir($this->path("plugin/{$plugin}/src")) ?: [], function ($name) use ($plugin) { + return $this->isVisibleFile($name, "plugin/{$plugin}/src/{$name}"); + })); + sort($files); + sort($expected); + $this->assertSame($expected, $files, "{$plugin} has unexpected source root files"); + } + + $this->assertDirectoryDoesNotExist($this->path('plugin/think-plugs-center/src')); + $this->assertFileDoesNotExist($this->path('plugin/think-plugs-center/Service.php')); + $this->assertFileDoesNotExist($this->path('plugin/think-plugs-wemall/src/helper.php')); + $this->assertFileDoesNotExist($this->path('plugin/think-plugs-wuma/src/Query.php')); + $this->assertFileDoesNotExist($this->path('plugin/think-plugs-wuma/src/Script.php')); + $this->assertFileExists($this->path('plugin/think-plugs-wemall/src/common.php')); + $this->assertFileExists($this->path('plugin/think-plugs-wuma/src/controller/Query.php')); + } + + public function testLibrarySourceRootFilesStayFocused(): void + { + $files = array_values(array_filter(scandir($this->path('plugin/think-library/src')) ?: [], function ($name) { + return $this->isVisibleFile($name, "plugin/think-library/src/{$name}"); + })); + sort($files); + + $this->assertSame(['Builder.php', 'Command.php', 'Controller.php', 'Exception.php', 'Helper.php', 'Library.php', 'Model.php', 'Plugin.php', 'Service.php', 'Storage.php', 'common.php'], $files); + } + + public function testPhpSourcesDoNotReferenceLegacyBoundaryNamespaces(): void + { + $legacyHelperNamespace = 'plugin' . '\\helper\\'; + $forbidden = [ + 'think\admin\system\NodeService', + 'think\admin\runtime\AppService', + 'think\admin\runtime\ModuleService', + 'think\admin\runtime\PluginService', + 'think\admin\runtime\MultAccess', + 'think\admin\runtime\QueueService', + 'think\admin\runtime\ProcessService', + 'think\admin\runtime\RuntimeService', + 'think\admin\runtime\RuntimeTools', + 'think\admin\runtime\Route', + 'think\admin\runtime\Url', + 'think\admin\service\PluginService', + 'think\admin\service\ProcessService', + 'think\admin\auth\CaptchaService', + 'think\admin\auth\CacheSession', + 'think\admin\auth\RequestTokenService', + 'think\admin\extend\JwtToken', + 'think\admin\extend\FaviconBuilder', + 'think\admin\extend\ImageSliderVerify', + 'think\admin\extend\JsonRpcHttpClient', + 'think\admin\extend\JsonRpcHttpServer', + 'think\admin\contract\AdminContextInterface', + 'think\admin\runtime\AdminContext', + 'think\admin\runtime\NullAdminContext', + 'plugin\system\runtime\AdminContext', + 'plugin\system\runtime\SystemContext', + 'plugin\system\auth\AdminService', + 'plugin\system\auth\\', + 'plugin\system\system\SystemService', + 'plugin\storage\StorageConfig', + 'plugin\storage\StorageManager', + 'plugin\storage\support\StorageAuthorize', + 'think\admin\storage\\', + 'plugin\worker\queue\\', + 'plugin\worker\support\\', + 'plugin\worker\Server', + $legacyHelperNamespace, + 'plugin\wechat\service\handle\\', + 'plugin\wechat\service\AuthService', + 'plugin\wechat\service\ConfigService', + 'plugin\view\\', + 'think\admin\view\\', + 'ViewRouteService', + 'LegacyPage', + 'ViewPage', + 'controller\\Page', + 'plugin\payment\queue\\', + 'plugin\wemall\integration\\', + 'plugin\wuma\Query', + 'plugin\wuma\Script', + 'usession(', + 'user_session_store', + 'user_session_touch', + 'user_session_expire', + 'user_session_gc_interval', + 'admin_user(', + 'admuri(', + ]; + + $violations = []; + $iterator = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator($this->path('plugin'), \FilesystemIterator::SKIP_DOTS) + ); + + foreach ($iterator as $file) { + if (!$file->isFile() || $file->getExtension() !== 'php') { + continue; + } + + $path = str_replace('\\', '/', $file->getPathname()); + if ($path === str_replace('\\', '/', __FILE__) || str_contains($path, '/tests/')) { + continue; + } + $content = file_get_contents($path) ?: ''; + foreach ($forbidden as $legacy) { + if (strpos($content, $legacy) !== false) { + $violations[] = [$legacy, $path]; + } + } + } + + $this->assertSame([], $violations, 'Legacy namespace references found: ' . json_encode($violations, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)); + } + + private function path(string $relative): string + { + return $this->projectRoot . '/' . ltrim($relative, '/'); + } + + private function isVisibleFile(string $name, string $relative): bool + { + return $name !== '' && $name[0] !== '.' && is_file($this->path($relative)); + } +} diff --git a/plugin/think-library/tests/CodeTest.php b/plugin/think-library/tests/CodeTest.php new file mode 100644 index 000000000..22095078e --- /dev/null +++ b/plugin/think-library/tests/CodeTest.php @@ -0,0 +1,44 @@ +assertNotEmpty(preg_match('|^[a-z0-9]{8}-([a-z0-9]{4}-){3}[a-z0-9]{12}$|i', $uuid)); + } + + public function testEncode() + { + $value = '235215321351235123dasfdasfasdfas'; + $encode = CodeToolkit::encrypt($value, 'thinkadmin'); + $this->assertEquals($value, CodeToolkit::decrypt($encode, 'thinkadmin'), '验证加密解密'); + } +} diff --git a/plugin/think-library/tests/CommonFunctionsTest.php b/plugin/think-library/tests/CommonFunctionsTest.php new file mode 100644 index 000000000..a89ba1d65 --- /dev/null +++ b/plugin/think-library/tests/CommonFunctionsTest.php @@ -0,0 +1,169 @@ +resetRuntimeContext(); + $this->app = new App(TEST_PROJECT_ROOT); + RuntimeService::init($this->app); + $this->app->initialize(); + $this->app->config->set(['with_route' => true], 'app'); + $this->app->config->set(['default_app' => 'index'], 'route'); + + $request = $this->app->make('request', [], true); + $request->setRoot(''); + $request->setPathinfo('system/login/index'); + $this->app->instance('request', $request); + } + + protected function tearDown(): void + { + $this->resetRuntimeContext(); + parent::tearDown(); + } + + public function testLibraryHelpersAreLoadedFromLibraryPackage(): void + { + foreach (['sysuri', 'apiuri', 'xss_safe', 'format_bytes'] as $name) { + $this->assertTrue(function_exists($name), "{$name} should be autoloaded"); + + $reflection = new \ReflectionFunction($name); + + $this->assertSame( + realpath(TEST_PROJECT_ROOT . '/plugin/think-library/src/common.php'), + realpath((string)$reflection->getFileName()) + ); + } + } + + public function testPlguriIsLoadedFromSystemPackage(): void + { + $this->assertTrue(function_exists('plguri')); + + $reflection = new \ReflectionFunction('plguri'); + + $this->assertSame( + realpath(TEST_PROJECT_ROOT . '/plugin/think-plugs-system/src/common.php'), + realpath((string)$reflection->getFileName()) + ); + } + + public function testSysuriAndUrlBuildSupportShortWebPaths(): void + { + $this->assertSame('/system', Url::normalizeWebTarget('system/index/index')); + $this->assertSame('/system/plugin?from=force', Url::normalizeWebTarget('/system/plugin/index?from=force')); + $this->assertSame('/system/plugin/layout?encode=test', Url::normalizeWebTarget('/system/plugin/layout?encode=test')); + $this->assertSame('/system.html', sysuri('system/index/index')); + $this->assertSame('/system.html', sysuri('/system/index/index')); + $this->assertSame('/system/login.html', sysuri('/system/login/index')); + $this->assertSame('/system/plugin/layout?encode=test', sysuri('/system/plugin/layout', ['encode' => 'test'], false)); + $this->assertSame('/system/plugin.html?from=force', sysuri('/system/plugin/index', ['from' => 'force'])); + $this->assertSame('/system/plugin.html', sysuri('/system/plugin/index')); + $this->assertSame('/system/plugin.html', url('system/plugin/index')->build()); + $this->assertSame('/system/plugin.html', url('/system/plugin/index')->build()); + + AppService::activatePlugin('system', 'system'); + + $this->assertSame('/api/system/upload/file', Url::normalizeApiTarget('upload/file')); + $this->assertSame('/api/system/upload/file?from=test', Url::normalizeApiTarget('/api/system/upload/file?from=test')); + $this->assertSame('/api/system/upload/index', Url::normalizeApiTarget('/system/upload')); + $this->assertSame('/api/system/upload/file', Url::normalizeApiTarget('api/upload/file')); + $this->assertSame('/api/system/upload/file.html', apiuri('upload/file')); + $this->assertSame('/api/system/upload/file.html', apiuri('/api/system/upload/file')); + $this->assertSame('/api/system/upload/index.html', apiuri('/system/upload')); + $this->assertSame('/api/system/upload/file.html', apiuri('api/upload/file')); + } + + public function testXssSafeStripsScriptsAndNeutralizesInlineEvents(): void + { + $safe = xss_safe('
    safe
    '); + + $this->assertStringNotContainsString('assertStringContainsString('data-on-click=', strtolower($safe)); + $this->assertStringContainsString('safe', strtolower($safe)); + } + + public function testFormatBytesSupportsLargeUnits(): void + { + $this->assertSame('1 PB', format_bytes(1024 ** 5)); + $this->assertSame('2 KB', format_bytes(2048)); + $this->assertSame('plain', format_bytes('plain')); + } + + public function testStr2arrAndArr2strSupportArrayAndStringInputs(): void + { + $this->assertSame(['a', 'b', 'c'], str2arr('a,b,c')); + $this->assertSame(['a', 'b', 'c'], str2arr(['a', ' b ', 'c'])); + $this->assertSame(['a', 'b', 'c'], str2arr(['a,b', ['c']])); + $this->assertSame([1, 2], str2arr([1, 2], ',', [1, 2])); + $this->assertSame(['a', 'c'], str2arr('a,b,c', ',', ['a', 'c'])); + + $this->assertSame(',a,b,c,', arr2str(['a', ' b ', 'c'])); + $this->assertSame(',a,b,c,', arr2str('a,b,c')); + $this->assertSame(',a,c,', arr2str(['a', 'b', 'c'], ',', ['a', 'c'])); + $this->assertSame('', arr2str('')); + } + + public function testImageSliderVerifyFallsBackWhenSourceImageIsMissing(): void + { + $image = ImageSliderVerify::render(TEST_PROJECT_ROOT . '/runtime/missing-slider-' . uniqid('', true) . '.jpg', 60); + $background = base64_decode(substr($image['bgimg'], strlen('data:image/png;base64,')), true); + $piece = base64_decode(substr($image['water'], strlen('data:image/png;base64,')), true); + + $this->assertStringStartsWith('V', $image['code']); + $this->assertSame(600, $image['width']); + $this->assertSame(300, $image['height']); + $this->assertSame(100, $image['piece_width']); + $this->assertNotFalse($background); + $this->assertNotFalse($piece); + $this->assertSame([600, 300], array_slice(getimagesizefromstring($background) ?: [0, 0], 0, 2)); + $this->assertSame([100, 300], array_slice(getimagesizefromstring($piece) ?: [0, 0], 0, 2)); + } + + private function resetRuntimeContext(): void + { + AppService::clear(); + RequestContext::clear(); + if (function_exists('sysvar')) { + sysvar('', ''); + } + function_exists('test_reset_model_makers') && test_reset_model_makers(); + } +} diff --git a/plugin/think-library/tests/ComposerDependencyBoundaryTest.php b/plugin/think-library/tests/ComposerDependencyBoundaryTest.php new file mode 100644 index 000000000..30b89a080 --- /dev/null +++ b/plugin/think-library/tests/ComposerDependencyBoundaryTest.php @@ -0,0 +1,281 @@ +}> + */ + private array $packages = []; + + protected function setUp(): void + { + parent::setUp(); + $this->projectRoot = TEST_PROJECT_ROOT; + $this->packages = $this->loadPackages(); + } + + public function testLocalPluginDependencyGraphIsAcyclic(): void + { + $graph = $this->localGraph(); + $state = []; + $stack = []; + $cycle = []; + + $visit = function (string $package) use (&$visit, &$graph, &$state, &$stack, &$cycle): void { + if ($cycle !== []) { + return; + } + $state[$package] = 1; + $stack[] = $package; + + foreach ($graph[$package] ?? [] as $dependency) { + $depState = $state[$dependency] ?? 0; + if ($depState === 0) { + $visit($dependency); + if ($cycle !== []) { + return; + } + continue; + } + if ($depState === 1) { + $offset = array_search($dependency, $stack, true); + $cycle = array_slice($stack, $offset === false ? 0 : $offset); + $cycle[] = $dependency; + return; + } + } + + array_pop($stack); + $state[$package] = 2; + }; + + foreach (array_keys($graph) as $package) { + if (($state[$package] ?? 0) === 0) { + $visit($package); + } + } + + $this->assertSame([], $cycle, 'Local composer dependency cycle detected: ' . implode(' -> ', $cycle)); + } + + public function testBasePackagesStayAtTheBottomOfDependencyGraph(): void + { + $this->assertSame([], $this->localRequires('zoujingli/think-library')); + $this->assertSame([ + 'zoujingli/think-library', + 'zoujingli/think-plugs-static', + 'zoujingli/think-plugs-worker', + ], $this->localRequires('zoujingli/think-plugs-system')); + $this->assertSame(['zoujingli/think-library'], $this->localRequires('zoujingli/think-plugs-worker')); + } + + public function testSystemDoesNotReintroduceLegacyStoragePackage(): void + { + $system = $this->localRequires('zoujingli/think-plugs-system'); + + $this->assertNotContains('zoujingli/think-plugs-storage', $system); + } + + public function testHelperPackageStaysMergedIntoSystem(): void + { + $this->assertDirectoryDoesNotExist($this->path('plugin/think-plugs-helper')); + $this->assertFileExists($this->path('plugin/think-plugs-system/src/helper/Service.php')); + $this->assertFileExists($this->path('plugin/think-plugs-system/src/helper/command/project/PublishCommand.php')); + + $legacyHelperNamespace = 'plugin' . '\\helper\\'; + $systemComposer = $this->jsonFile($this->path('plugin/think-plugs-system/composer.json')); + $systemPsr4 = is_array($systemComposer['autoload']['psr-4'] ?? null) ? $systemComposer['autoload']['psr-4'] : []; + $systemServices = is_array($systemComposer['extra']['think']['services'] ?? null) ? $systemComposer['extra']['think']['services'] : []; + + $this->assertArrayNotHasKey($legacyHelperNamespace, $systemPsr4); + $this->assertContains('plugin\system\helper\Service', $systemServices); + $this->assertNotContains($legacyHelperNamespace . 'Service', $systemServices); + + $violations = []; + foreach ($this->composerFiles() as $file) { + $json = $this->jsonFile($file); + + if (str_ends_with($file, '/composer.lock')) { + $packages = array_merge( + is_array($json['packages'] ?? null) ? $json['packages'] : [], + is_array($json['packages-dev'] ?? null) ? $json['packages-dev'] : [] + ); + + foreach ($packages as $package) { + if (!is_array($package)) { + continue; + } + if (($package['name'] ?? null) === 'zoujingli/think-plugs-helper') { + $violations[] = [$file, 'package']; + } + foreach (['require', 'require-dev'] as $section) { + if (isset($package[$section]['zoujingli/think-plugs-helper'])) { + $violations[] = [$file, $section, $package['name'] ?? 'unknown']; + } + } + } + + continue; + } + + foreach (['require', 'require-dev'] as $section) { + if (isset($json[$section]['zoujingli/think-plugs-helper'])) { + $violations[] = [$file, $section]; + } + } + } + + $this->assertSame([], $violations, 'Legacy helper package dependencies found: ' . json_encode($violations, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)); + } + + public function testLegacyViewPackagesStayRemoved(): void + { + $legacyPackages = [ + 'zoujingli/think-plugs-view' => 'plugin/think-plugs-view', + 'zoujingli/think-plugs-system-view' => 'plugin/think-plugs-system-view', + 'zoujingli/think-plugs-wechat-client-view' => 'plugin/think-plugs-wechat-client-view', + 'zoujingli/think-plugs-wechat-service-view' => 'plugin/think-plugs-wechat-service-view', + ]; + + foreach ($legacyPackages as $package => $directory) { + $this->assertDirectoryDoesNotExist($this->path($directory), $package . ' directory should stay removed'); + } + + $violations = []; + foreach ($this->composerFiles() as $file) { + $content = file_get_contents($file) ?: ''; + foreach (array_keys($legacyPackages) as $package) { + if (strpos($content, $package) !== false) { + $violations[] = [$package, $file]; + } + } + } + + $this->assertSame([], $violations, 'Legacy view package references found: ' . json_encode($violations, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)); + } + + public function testEveryLocalDependencyPointsToAnExistingWorkspacePackage(): void + { + $known = array_keys($this->packages); + $missing = []; + + foreach ($this->packages as $name => $package) { + foreach ($this->localRequires($name) as $dependency) { + if (!in_array($dependency, $known, true)) { + $missing[] = [$name, $dependency]; + } + } + } + + $this->assertSame([], $missing, 'Unknown local package dependencies found: ' . json_encode($missing, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)); + } + + /** + * @return array}> + */ + private function loadPackages(): array + { + $items = []; + foreach (glob($this->path('plugin/*/composer.json')) ?: [] as $file) { + $json = json_decode(file_get_contents($file) ?: '', true); + if (!is_array($json) || empty($json['name'])) { + continue; + } + $items[strval($json['name'])] = [ + 'name' => strval($json['name']), + 'path' => $file, + 'require' => is_array($json['require'] ?? null) ? $json['require'] : [], + ]; + } + + ksort($items); + return $items; + } + + /** + * @return array> + */ + private function localGraph(): array + { + $graph = []; + foreach (array_keys($this->packages) as $name) { + $graph[$name] = $this->localRequires($name); + } + return $graph; + } + + /** + * @return list + */ + private function localRequires(string $package): array + { + $requires = array_keys($this->packages[$package]['require'] ?? []); + $locals = array_values(array_filter($requires, fn (string $name): bool => isset($this->packages[$name]))); + sort($locals); + return $locals; + } + + /** + * @return list + */ + private function composerFiles(): array + { + $files = []; + + foreach (['composer.json', 'composer.lock'] as $name) { + $file = $this->path($name); + if (is_file($file)) { + $files[] = str_replace('\\', '/', $file); + } + } + + foreach (glob($this->path('plugin/*/composer.json')) ?: [] as $file) { + $files[] = str_replace('\\', '/', $file); + } + + sort($files); + return $files; + } + + /** + * @return array + */ + private function jsonFile(string $file): array + { + $json = json_decode(file_get_contents($file) ?: '', true); + return is_array($json) ? $json : []; + } + + private function path(string $relative): string + { + return $this->projectRoot . '/' . ltrim($relative, '/'); + } +} diff --git a/plugin/think-library/tests/ComposerInstallBoundaryTest.php b/plugin/think-library/tests/ComposerInstallBoundaryTest.php new file mode 100644 index 000000000..23bc0ee56 --- /dev/null +++ b/plugin/think-library/tests/ComposerInstallBoundaryTest.php @@ -0,0 +1,398 @@ +assertIsArray($json); + + $require = is_array($json['require'] ?? null) ? $json['require'] : []; + $requireDev = is_array($json['require-dev'] ?? null) ? $json['require-dev'] : []; + $allowPlugins = is_array($json['config']['allow-plugins'] ?? null) ? $json['config']['allow-plugins'] : []; + + $this->assertArrayNotHasKey('zoujingli/think-install', $require); + $this->assertArrayNotHasKey('zoujingli/think-install', $requireDev); + $this->assertArrayNotHasKey('zoujingli/think-install', $allowPlugins); + } + + public function testRootComposerAllowsInstallComposerPluginAndDoesNotKeepLegacyHook(): void + { + $json = json_decode((string)file_get_contents(TEST_PROJECT_ROOT . '/composer.json'), true); + $this->assertIsArray($json); + + $require = is_array($json['require'] ?? null) ? $json['require'] : []; + $allowPlugins = is_array($json['config']['allow-plugins'] ?? null) ? $json['config']['allow-plugins'] : []; + $scripts = $json['scripts'] ?? []; + $this->assertIsArray($scripts); + + $this->assertArrayHasKey('zoujingli/think-plugs-install', $require); + $this->assertTrue(boolval($allowPlugins['zoujingli/think-plugs-install'] ?? false)); + $this->assertArrayNotHasKey('post-autoload-dump', $scripts); + } + + public function testInstallPackageIsRegisteredAsComposerPlugin(): void + { + $json = json_decode((string)file_get_contents(TEST_PROJECT_ROOT . '/plugin/think-plugs-install/composer.json'), true); + $this->assertIsArray($json); + + $require = is_array($json['require'] ?? null) ? $json['require'] : []; + $services = (array)($json['extra']['think']['services'] ?? []); + + $this->assertSame('composer-plugin', $json['type'] ?? null); + $this->assertSame('plugin\\install\\composer\\Plugin', $json['extra']['class'] ?? null); + $this->assertContains('plugin\\install\\Service', $services); + $this->assertArrayHasKey('composer-plugin-api', $require); + } + + public function testComposerLockKeepsInstallPackageAsComposerPlugin(): void + { + $lock = json_decode((string)file_get_contents(TEST_PROJECT_ROOT . '/composer.lock'), true); + $this->assertIsArray($lock); + + $package = null; + foreach ((array)($lock['packages'] ?? []) as $item) { + if (($item['name'] ?? null) === 'zoujingli/think-plugs-install') { + $package = $item; + break; + } + } + + $this->assertIsArray($package, 'composer.lock is missing zoujingli/think-plugs-install'); + $require = is_array($package['require'] ?? null) ? $package['require'] : []; + + $this->assertSame('composer-plugin', $package['type'] ?? null); + $this->assertSame('plugin\\install\\composer\\Plugin', $package['extra']['class'] ?? null); + $this->assertArrayHasKey('composer-plugin-api', $require); + } + + public function testComposerPluginClassStaysSafeOutsideComposerRuntime(): void + { + $loaded = class_exists('plugin\\install\\composer\\Plugin'); + + $this->assertIsBool($loaded); + if (!interface_exists('Composer\\Plugin\\PluginInterface', false)) { + $this->assertFalse($loaded); + } + } + + public function testPluginServicesDoNotDeclareMenuMethod(): void + { + $violations = []; + + foreach (glob(TEST_PROJECT_ROOT . '/plugin/*/src/Service.php') ?: [] as $file) { + $source = (string)file_get_contents($file); + if (preg_match('/function\s+menu\s*\(/i', $source) === 1) { + $violations[] = str_replace(TEST_PROJECT_ROOT . '/', '', $file); + } + } + + $this->assertSame([], $violations, 'Plugin menus must be declared in composer.json: ' . implode(', ', $violations)); + } + + public function testRuntimePluginServicesDoNotDeclareMetadataProperties(): void + { + $violations = []; + $pattern = '/protected\s+(?:string|array|bool)\s+\$(appCode|appName|appPrefix|appPrefixes|package|appAlias|appDocument|appDescription|appPlatforms|appLicense|appVersion|appHomepage)\b/'; + + foreach (glob(TEST_PROJECT_ROOT . '/plugin/*/composer.json') ?: [] as $file) { + $manifest = json_decode((string)file_get_contents($file), true); + if (!is_array($manifest)) { + continue; + } + + $services = (array)($manifest['extra']['think']['services'] ?? []); + $service = strval($services[0] ?? ''); + if ($service === '' || !class_exists($service) || !is_subclass_of($service, Plugin::class)) { + continue; + } + + $serviceFile = dirname($file) . '/src/Service.php'; + if (!is_file($serviceFile)) { + continue; + } + + $source = (string)file_get_contents($serviceFile); + if (preg_match_all($pattern, $source, $matches) < 1) { + continue; + } + + $names = array_values(array_unique($matches[1])); + $label = str_replace(TEST_PROJECT_ROOT . '/', '', $serviceFile); + $violations[] = "{$label} declares service metadata properties: " . implode(', ', $names); + } + + $this->assertSame([], $violations, 'Runtime plugin metadata must only come from composer.json: ' . implode(', ', $violations)); + } + + public function testPluginComposerMetadataUsesXadminAppOnly(): void + { + $required = ['code', 'name']; + $stringFields = ['code', 'name', 'prefix', 'alias', 'space', 'document', 'description', 'icon', 'cover']; + $arrayFields = ['prefixes', 'platforms', 'license']; + $allowed = array_merge($stringFields, $arrayFields, ['super']); + $legacy = []; + $missing = []; + $invalid = []; + + foreach (glob(TEST_PROJECT_ROOT . '/plugin/*/composer.json') ?: [] as $file) { + $manifest = json_decode((string)file_get_contents($file), true); + if (!is_array($manifest)) { + continue; + } + + if (isset($manifest['extra']['config'])) { + $legacy[] = str_replace(TEST_PROJECT_ROOT . '/', '', $file) . ' uses extra.config'; + } + if (isset($manifest['extra']['xadmin']['service'])) { + $legacy[] = str_replace(TEST_PROJECT_ROOT . '/', '', $file) . ' uses extra.xadmin.service'; + } + if (isset($manifest['extra']['xadmin']['config'])) { + $legacy[] = str_replace(TEST_PROJECT_ROOT . '/', '', $file) . ' uses extra.xadmin.config'; + } + + $services = (array)($manifest['extra']['think']['services'] ?? []); + $service = strval($services[0] ?? ''); + if ($service === '' || !class_exists($service) || !is_subclass_of($service, Plugin::class)) { + continue; + } + $app = $manifest['extra']['xadmin']['app'] ?? null; + if (!is_array($app) || $app === []) { + $missing[] = str_replace(TEST_PROJECT_ROOT . '/', '', $file); + continue; + } + foreach ($required as $key) { + if (!is_string($app[$key] ?? null) || trim($app[$key]) === '') { + $missing[] = str_replace(TEST_PROJECT_ROOT . '/', '', $file) . " missing {$key}"; + } + } + foreach (array_keys($app) as $key) { + if (!in_array($key, $allowed, true)) { + $invalid[] = str_replace(TEST_PROJECT_ROOT . '/', '', $file) . " contains unsupported xadmin.app.{$key}"; + } + } + foreach ($stringFields as $key) { + if (!array_key_exists($key, $app)) { + continue; + } + if (!is_string($app[$key])) { + $invalid[] = str_replace(TEST_PROJECT_ROOT . '/', '', $file) . " requires xadmin.app.{$key} to be a string"; + continue; + } + if (!in_array($key, $required, true) && trim($app[$key]) === '') { + $invalid[] = str_replace(TEST_PROJECT_ROOT . '/', '', $file) . " requires xadmin.app.{$key} to be non-empty when declared"; + } + } + foreach ($arrayFields as $key) { + if (!array_key_exists($key, $app)) { + continue; + } + if (!is_array($app[$key])) { + $invalid[] = str_replace(TEST_PROJECT_ROOT . '/', '', $file) . " requires xadmin.app.{$key} to be an array"; + continue; + } + foreach ($app[$key] as $index => $value) { + if (!is_string($value) || trim($value) === '') { + $invalid[] = str_replace(TEST_PROJECT_ROOT . '/', '', $file) . " requires xadmin.app.{$key}[{$index}] to be a non-empty string"; + } + } + } + if (array_key_exists('super', $app) && !is_bool($app['super'])) { + $invalid[] = str_replace(TEST_PROJECT_ROOT . '/', '', $file) . ' requires xadmin.app.super to be a boolean'; + } + if (isset($app['prefix'], $app['prefixes']) && is_string($app['prefix']) && is_array($app['prefixes'])) { + if (!in_array($app['prefix'], $app['prefixes'], true)) { + $invalid[] = str_replace(TEST_PROJECT_ROOT . '/', '', $file) . ' requires xadmin.app.prefix to be included in xadmin.app.prefixes'; + } + } + } + + $this->assertSame([], $legacy, 'Legacy plugin metadata blocks are not allowed: ' . implode(', ', $legacy)); + $this->assertSame([], $missing, 'Runtime plugin metadata must be declared in extra.xadmin.app: ' . implode(', ', $missing)); + $this->assertSame([], $invalid, 'Unsupported xadmin.app fields found: ' . implode(', ', $invalid)); + } + + public function testPluginComposerPublishRulesUseCopyOnlyAndNoLegacyKeys(): void + { + $invalid = []; + + foreach (glob(TEST_PROJECT_ROOT . '/plugin/*/composer.json') ?: [] as $file) { + $manifest = json_decode((string)file_get_contents($file), true); + if (!is_array($manifest)) { + continue; + } + + $publish = $manifest['extra']['xadmin']['publish'] ?? null; + if ($publish === null) { + continue; + } + if (!is_array($publish)) { + $invalid[] = str_replace(TEST_PROJECT_ROOT . '/', '', $file) . ' requires extra.xadmin.publish to be an object'; + continue; + } + foreach (array_keys($publish) as $key) { + if ($key !== 'copy') { + $invalid[] = str_replace(TEST_PROJECT_ROOT . '/', '', $file) . " contains unsupported xadmin.publish.{$key}"; + } + } + + $copy = $publish['copy'] ?? null; + if ($copy === null) { + continue; + } + if (!is_array($copy)) { + $invalid[] = str_replace(TEST_PROJECT_ROOT . '/', '', $file) . ' requires extra.xadmin.publish.copy to be an array/object'; + continue; + } + + foreach ($copy as $source => $target) { + $label = str_replace(TEST_PROJECT_ROOT . '/', '', $file); + if (is_array($target)) { + $keys = array_keys($target); + $isKeyValueObject = is_string($source); + $allowed = $isKeyValueObject ? ['to', 'force', 'exclude'] : ['from', 'to', 'force', 'exclude']; + + foreach ($keys as $key) { + if (!in_array($key, $allowed, true)) { + $invalid[] = "{$label} contains unsupported publish rule key {$key}"; + } + } + + if ($isKeyValueObject) { + if (!is_string($source) || trim($source) === '') { + $invalid[] = "{$label} requires key-value publish source to be non-empty"; + } + if (!is_string($target['to'] ?? null) || trim($target['to']) === '') { + $invalid[] = "{$label} requires key-value publish object rules to declare non-empty to"; + } + } else { + if (!is_string($target['from'] ?? null) || trim($target['from']) === '') { + $invalid[] = "{$label} requires object publish rules to declare non-empty from"; + } + if (!is_string($target['to'] ?? null) || trim($target['to']) === '') { + $invalid[] = "{$label} requires object publish rules to declare non-empty to"; + } + } + + if (array_key_exists('force', $target) && !is_bool($target['force'])) { + $invalid[] = "{$label} requires publish rule force to be boolean"; + } + if (array_key_exists('exclude', $target) && !is_string($target['exclude']) && !is_array($target['exclude'])) { + $invalid[] = "{$label} requires publish rule exclude to be string or array"; + } + continue; + } + + if (!is_string($source) || trim($source) === '') { + $invalid[] = "{$label} requires publish source to be non-empty"; + } + if (!is_string($target) || trim($target) === '') { + $invalid[] = "{$label} requires publish target to be non-empty"; + } + } + } + + $this->assertSame([], $invalid, 'Plugin publish rules must only use copy with current keys: ' . implode(', ', $invalid)); + } + + public function testWorkspacePluginsDoNotUseLegacyExtraPluginInstallerBlock(): void + { + $violations = []; + + foreach (glob(TEST_PROJECT_ROOT . '/plugin/*/composer.json') ?: [] as $file) { + $manifest = json_decode((string)file_get_contents($file), true); + if (!is_array($manifest)) { + continue; + } + if (isset($manifest['extra']['plugin'])) { + $violations[] = str_replace(TEST_PROJECT_ROOT . '/', '', $file); + } + } + + $this->assertSame([], $violations, 'Legacy extra.plugin installer blocks are not allowed: ' . implode(', ', $violations)); + } + + public function testRuntimePluginComposerManifestProvidesMinimalSkeleton(): void + { + $invalid = []; + + foreach (glob(TEST_PROJECT_ROOT . '/plugin/*/composer.json') ?: [] as $file) { + $manifest = json_decode((string)file_get_contents($file), true); + if (!is_array($manifest)) { + continue; + } + + $services = (array)($manifest['extra']['think']['services'] ?? []); + $service = strval($services[0] ?? ''); + if ($service === '' || !class_exists($service) || !is_subclass_of($service, Plugin::class)) { + continue; + } + + $label = str_replace(TEST_PROJECT_ROOT . '/', '', $file); + if (strval($manifest['type'] ?? '') !== 'think-admin-plugin') { + $invalid[] = "{$label} requires type=think-admin-plugin"; + } + if (!is_string($manifest['name'] ?? null) || trim($manifest['name']) === '') { + $invalid[] = "{$label} requires composer.name"; + } + if (!is_string($manifest['description'] ?? null) || trim($manifest['description']) === '') { + $invalid[] = "{$label} requires composer.description"; + } + if (!is_array($manifest['autoload']['psr-4'] ?? null) || ($manifest['autoload']['psr-4'] ?? []) === []) { + $invalid[] = "{$label} requires autoload.psr-4"; + } + if (count($services) !== 1) { + $invalid[] = "{$label} requires exactly one extra.think.services entry"; + } + if (!is_subclass_of($service, Plugin::class)) { + $invalid[] = "{$label} service must extend think\\admin\\Plugin"; + } + + $autoload = (array)($manifest['autoload']['psr-4'] ?? []); + $matched = false; + foreach ($autoload as $namespace => $directory) { + $namespace = trim(strval($namespace), '\\'); + $directory = trim(strval($directory), '\/'); + if ($namespace === '' || $directory === '') { + continue; + } + if (str_starts_with(trim($service, '\\'), $namespace . '\\') && $directory === 'src') { + $matched = true; + break; + } + } + if (!$matched) { + $invalid[] = "{$label} requires service namespace to be mapped to src in autoload.psr-4"; + } + } + + $this->assertSame([], $invalid, 'Runtime plugin composer skeleton violations found: ' . implode(', ', $invalid)); + } +} diff --git a/plugin/think-library/tests/FormBuilderTest.php b/plugin/think-library/tests/FormBuilderTest.php new file mode 100644 index 000000000..e4381724e --- /dev/null +++ b/plugin/think-library/tests/FormBuilderTest.php @@ -0,0 +1,861 @@ +newBuilder(); + $builder->define(function ($form) { + $form->fields(function ($fields) { + $fields->text('appid', '小程序', 'AppId', true, '必填应用标识', '^wx[0-9a-z]{16}$', ['maxlength' => 18]) + ->field([ + 'type' => 'textarea', + 'name' => 'remark', + 'title' => '备注', + 'required' => true, + 'rules' => ['max:100' => '备注最多100个字符!'], + 'attrs' => ['maxlength' => 100], + ]); + }); + })->build(); + + $schema = $builder->toArray(); + $rules = $builder->getValidateRules(); + $requestRules = $builder->getRequestRules(); + + $this->assertCount(2, $schema['fields']); + $this->assertSame('$vo', $schema['variable']); + $this->assertSame('appid', $schema['fields'][0]['name']); + $this->assertTrue($schema['fields'][0]['required']); + $this->assertSame('^wx[0-9a-z]{16}$', $schema['fields'][0]['pattern']); + $this->assertSame('', $requestRules['appid.default']); + $this->assertSame('', $requestRules['remark.default']); + $this->assertSame('小程序不能为空!', $rules['appid.require']); + $this->assertSame('小程序格式错误!', $rules['appid.regex:/^wx[0-9a-z]{16}$/']); + $this->assertSame('备注不能为空!', $rules['remark.require']); + $this->assertSame('备注最多100个字符!', $rules['remark.max:100']); + } + + public function testFieldDefaultsCanRenderAndValidateWithoutInitialVo(): void + { + $builder = $this->newBuilder(); + $builder->define(function ($form) { + $form->fields(function ($fields) { + $fields->text('sort', '排序', 'Sort', false, '', null, ['type' => 'number'])->defaultValue(0); + $fields->select('driver', '驱动', 'Driver', false, '', [ + 'local' => '本地', + 'qiniu' => '七牛', + ])->defaultValue('local'); + $fields->radio('status', '状态', 'Status', '', false) + ->options([1 => '启用', 0 => '禁用']) + ->defaultValue('1'); + $fields->checkbox('scene', '场景', 'Scene', '', false) + ->options(['index' => '列表', 'form' => '表单']) + ->defaultValue(['index']); + }); + })->build(); + + $requestRules = $builder->getRequestRules(); + $html = $this->invokePrivate($builder, '_buildFormModal'); + + $this->assertSame('0', $requestRules['sort.default']); + $this->assertSame('local', $requestRules['driver.default']); + $this->assertSame('1', $requestRules['status.default']); + $this->assertSame(['index'], $requestRules['scene.default']); + $this->assertStringContainsString('value="{$vo.sort|default=0}"', $html); + $this->assertStringContainsString("!isset(\$vo.driver) and strval('local') eq 'local'", $html); + $this->assertStringContainsString("!isset(\$vo.status) and strval('1')==strval('1')", $html); + $this->assertStringContainsString("!isset(\$vo.scene) and in_array('index',['index'])", $html); + } + + public function testCheckboxTemplateUsesRealFieldNameAndVariable() + { + $builder = $this->newBuilder(); + $builder->define(function ($form) { + $form->action('/submit') + ->variable('data') + ->fields(function ($fields) { + $fields->checkbox('roles', '角色', '', 'roles', true); + })->actions(function ($actions) { + $actions->submit(); + }); + })->build(); + + $html = $this->invokePrivate($builder, '_buildFormModal'); + + $this->assertStringContainsString('isset($data.roles)', $html); + $this->assertStringContainsString('name="roles[]"', $html); + $this->assertStringContainsString('{notempty name="data.id"}', $html); + $this->assertStringContainsString('form-builder-schema', $html); + } + + public function testUrlPatternRuleEscapesRegexDelimiterForBackendValidation() + { + $builder = $this->newBuilder(); + $builder->define(function ($form) { + $form->fields(function ($fields) { + $fields->text('auth_url', '授权跳转入口', 'Getway', true, 'URL pattern', '^https?://.*?auth.*?source=SOURCE'); + }); + })->build(); + + $rules = $builder->getValidateRules(); + $rule = 'regex:/^https?:\/\/.*?auth.*?source=SOURCE$/'; + + $this->assertArrayHasKey("auth_url.{$rule}", $rules); + + $validate = new Validate(); + $this->assertTrue($validate->rule(['auth_url' => $rule])->check([ + 'auth_url' => 'https://open.cuci.cc/auth?source=SOURCE', + ])); + } + + public function testCheckAndUploadFieldsCanRenderRemarks() + { + $builder = $this->newBuilder(); + $builder->define(function ($form) { + $form->fields(function ($fields) { + $fields->field([ + 'type' => 'image', + 'name' => 'headimg', + 'title' => '头像', + 'remark' => '上传头像', + ])->field([ + 'type' => 'checkbox', + 'name' => 'types', + 'title' => '通道', + 'vname' => 'types', + 'remark' => '选择可用通道', + ]); + }); + })->build(); + + $html = $this->invokePrivate($builder, '_buildFormModal'); + + $this->assertStringContainsString('上传头像', $html); + $this->assertStringContainsString('选择可用通道', $html); + } + + public function testUploadFieldsCanRenderRuntimeInitializers(): void + { + $builder = $this->newBuilder(); + $builder->define(function ($form) { + $form->fields(function ($fields) { + $fields->image('headimg', '头像')->types('jpg,png'); + $fields->video('intro_video', '介绍视频'); + $fields->images('gallery', '图集'); + }); + })->build(); + + $html = $this->invokePrivate($builder, '_buildFormModal'); + + $this->assertStringContainsString('data-file="image"', $html); + $this->assertStringContainsString('data-field="headimg"', $html); + $this->assertStringContainsString('data-type="jpg,png"', $html); + $this->assertStringContainsString('data-field="intro_video"', $html); + $this->assertStringContainsString("join('|', \$vo.gallery)", $html); + $this->assertStringNotContainsString('uploadOneImage()', $html); + $this->assertStringContainsString('uploadOneVideo()', $html); + $this->assertStringContainsString('uploadMultipleImage()', $html); + } + + public function testImageUploadFieldCanRenderPreviewOnlyMode(): void + { + $builder = $this->newBuilder(); + $builder->define(function ($form) { + $form->fields(function ($fields) { + $fields->image('headimg', '头像')->previewOnly(); + }); + })->build(); + + $schema = $builder->toArray(); + $html = $this->invokePrivate($builder, '_buildFormModal'); + + $this->assertSame('preview', $schema['fields'][0]['upload']['display'] ?? null); + $this->assertMatchesRegularExpression('/]*name="headimg"[^>]*type="hidden"[^>]*data-upload-display="preview"/', $html); + $this->assertStringNotContainsString('data-field="headimg"', $html); + $this->assertStringContainsString('uploadOneImage()', $html); + } + + public function testTextFieldInputContentCanRenderRightAddon(): void + { + $builder = $this->newBuilder(); + $builder->define(function ($form) { + $form->fields(function ($fields) { + $field = $fields->text('browser_icon', '浏览器小图标', 'Browser Icon', true, '', 'url', [ + 'placeholder' => '请上传浏览器图标', + ]); + $field->inputRightIcon('layui-icon-upload-drag', [ + 'data-file' => 'btn', + 'data-type' => 'png,jpg,jpeg', + 'data-field' => 'browser_icon', + ]); + }); + })->build(); + + $html = $this->invokePrivate($builder, '_buildFormModal'); + + $this->assertStringContainsString('name="browser_icon"', $html); + $this->assertStringContainsString('class="pr40 layui-input"', $html); + $this->assertStringContainsString('data-field="browser_icon"', $html); + $this->assertStringContainsString('layui-icon-upload-drag', $html); + $this->assertStringContainsString('onmousedown="event.preventDefault();event.stopPropagation();"', $html); + $this->assertStringContainsString('ontouchstart="event.preventDefault();event.stopPropagation();"', $html); + } + + public function testSelectAndStaticChoiceFieldsCanRenderWithoutTemplateVariables() + { + $builder = $this->newBuilder(); + $builder->define(function ($form) { + $form->fields(function ($fields) { + $fields->select('table_name', '数据表', 'Table', true, '', [ + 'system_user' => 'system_user', + 'system_menu' => '系统<菜单>', + ])->field([ + 'type' => 'radio', + 'name' => 'status', + 'title' => '状态', + 'options' => [1 => '启用&公开', 0 => '禁用'], + ]); + }); + })->build(); + + $html = $this->invokePrivate($builder, '_buildFormModal'); + + $this->assertStringContainsString('name="table_name"', $html); + $this->assertStringContainsString('value="system_user"', $html); + $this->assertStringContainsString('系统<菜单>', $html); + $this->assertStringContainsString('name="status"', $html); + $this->assertStringContainsString('启用&公开', $html); + $this->assertStringContainsString('禁用', $html); + } + + public function testActionsCanCustomizeAttrsAndHtml(): void + { + $builder = $this->newBuilder(); + $builder->define(function ($form) { + $form->actions(function ($actions) { + $actions->submit('保存', '', ['data-scene' => 'submit'], 'layui-btn-warm') + ->cancel('关闭', '确定关闭吗?', ['data-close-mode' => 'drawer'], 'layui-btn-primary') + ->button('预览', 'button', '', ['data-preview' => 'true'], 'layui-btn-normal') + ->html(''); + }); + })->build(); + + $schema = $builder->toArray(); + $html = $this->invokePrivate($builder, '_buildFormModal'); + + $this->assertSame('submit', $schema['buttons'][0]['type']); + $this->assertSame('submit', $schema['buttons'][0]['attrs']['type']); + $this->assertSame('submit', $schema['buttons'][0]['attrs']['data-scene']); + $this->assertSame('drawer', $schema['buttons'][1]['attrs']['data-close-mode']); + $this->assertSame('button', $schema['buttons'][2]['type']); + $this->assertSame('html', $schema['buttons'][3]['type']); + $this->assertStringContainsString('data-scene="submit"', $html); + $this->assertStringContainsString('data-close-mode="drawer"', $html); + $this->assertStringContainsString('data-preview="true"', $html); + $this->assertStringContainsString('data-extra="1"', $html); + } + + public function testFormLayoutCanSetRootAttrsAndModules(): void + { + $builder = $this->newBuilder(); + $builder->define(function ($form) { + $form->attr('id', 'ProfileForm') + ->class('profile-form') + ->class(['profile-form', 'profile-form-shell']) + ->data('scene', 'profile') + ->module('editor', ['field' => 'content']) + ->fields(function ($fields) { + $fields->text('content', '内容'); + }); + })->build(); + + $schema = $builder->toArray(); + $html = $this->invokePrivate($builder, '_buildFormModal'); + + $this->assertSame('ProfileForm', $schema['attrs']['id']); + $this->assertSame('profile', $schema['attrs']['data-scene']); + $this->assertSame('profile-form profile-form-shell layui-form layui-card', $schema['attrs']['class']); + $this->assertSame('form', $schema['attrs']['data-builder-scope']); + $this->assertSame('editor', $schema['modules'][0]['name']); + $this->assertSame('content', $schema['modules'][0]['config']['field']); + $this->assertStringContainsString('id="ProfileForm"', $html); + $this->assertStringContainsString('class="profile-form profile-form-shell layui-form layui-card"', $html); + $this->assertStringContainsString('data-scene="profile"', $html); + $this->assertStringContainsString('data-builder-scope="form"', $html); + $this->assertStringContainsString('data-builder-modules=', $html); + } + + public function testFormCanRenderStructuredContentNodes(): void + { + $builder = $this->newBuilder(); + $builder->define(function ($form) { + $group = $form->section()->class('profile-section')->data('scene', 'profile')->module('region', ['code' => 'cn']); + $group->fields(function ($fields) { + $fields->text('nickname', '用户名称', 'Nickname', true, '请输入用户名称'); + }); + $form->actionBar(function ($actions) { + $actions->submit('保存', '', ['data-scene' => 'submit']); + })->class('profile-actions')->data('scene', 'actions'); + })->build(); + + $schema = $builder->toArray(); + $html = $this->invokePrivate($builder, '_buildFormModal'); + + $this->assertSame('element', $schema['content'][0]['type']); + $this->assertSame('section', $schema['content'][0]['tag']); + $this->assertSame('profile', $schema['content'][0]['attrs']['data-scene']); + $this->assertSame('region', $schema['content'][0]['modules'][0]['name']); + $this->assertSame('actions', $schema['content'][1]['type']); + $this->assertSame('actions', $schema['content'][1]['attrs']['data-scene']); + $this->assertStringContainsString('class="profile-section"', $html); + $this->assertStringContainsString('class="layui-form-item text-center profile-actions"', $html); + $this->assertStringContainsString('data-scene="actions"', $html); + $this->assertStringContainsString('data-builder-modules=', $html); + } + + public function testPageModeCanRenderTitleAndDefaultPadding(): void + { + $builder = $this->newBuilder('form', 'page'); + $builder->define(function ($form) { + $form->title('资料编辑') + ->headerButton('返回列表', 'button', '', ['data-target-backup' => null], 'layui-btn-primary layui-btn-sm') + ->fields(function ($fields) { + $fields->text('nickname', '用户名称'); + }); + })->build(); + + $schema = $builder->toArray(); + $html = $this->invokePrivate($builder, '_buildFormPage'); + + $this->assertSame('资料编辑', $schema['title']); + $this->assertSame('pa40', $schema['body_attrs']['class']); + $this->assertSame('返回列表', $schema['header_buttons'][0]['name']); + $this->assertStringContainsString('data-builder-scope="page"', $html); + $this->assertStringContainsString('layui-card-header', $html); + $this->assertStringContainsString('layui-card-line', $html); + $this->assertStringContainsString('pull-right', $html); + $this->assertStringContainsString('layui-icon font-s10 color-desc mr5', $html); + $this->assertStringContainsString('class="layui-card-body"', $html); + $this->assertStringContainsString('class="layui-card-table"', $html); + $this->assertStringContainsString('class="think-box-shadow"', $html); + $this->assertStringContainsString('资料编辑', $html); + $this->assertStringContainsString('返回列表', $html); + $this->assertStringContainsString('class="pa40"', $html); + } + + public function testModalModeUsesCurrentDefaultBodyPadding(): void + { + $builder = $this->newBuilder('form', 'modal'); + $builder->define(function ($form) { + $form->fields(function ($fields) { + $fields->text('nickname', '用户名称'); + }); + })->build(); + + $schema = $builder->toArray(); + $html = $this->invokePrivate($builder, '_buildFormModal'); + + $this->assertSame('layui-card-body pl40 pr40 pt20 pb20', $schema['body_attrs']['class']); + $this->assertStringContainsString('class="layui-card-body pl40 pr40 pt20 pb20"', $html); + } + + public function testModuleObjectsCanMutateRootNodeAndFieldParts(): void + { + $builder = $this->newBuilder(); + $builder->define(function ($form) { + $form->moduleItem('editor', ['field' => 'content']) + ->option('mode', 'markdown'); + + $section = $form->section()->class('module-section'); + $section->moduleItem('region', ['code' => 'cn']) + ->option('level', 2); + + $form->fields(function ($fields) { + $field = $fields->text('nickname', '用户名称'); + $field->label()->moduleItem('tooltip', ['target' => 'nickname'])->option('placement', 'top'); + $field->input()->moduleItem('picker', ['target' => 'nickname'])->option('mode', 'dialog'); + }); + })->build(); + + $schema = $builder->toArray(); + $html = $this->invokePrivate($builder, '_buildFormModal'); + + $this->assertSame('editor', $schema['modules'][0]['name'] ?? null); + $this->assertSame('markdown', $schema['modules'][0]['config']['mode'] ?? null); + $this->assertSame('region', $schema['content'][0]['modules'][0]['name'] ?? null); + $this->assertSame(2, $schema['content'][0]['modules'][0]['config']['level'] ?? null); + $this->assertSame('tooltip', $schema['fields'][0]['parts']['label']['modules'][0]['name'] ?? null); + $this->assertSame('top', $schema['fields'][0]['parts']['label']['modules'][0]['config']['placement'] ?? null); + $this->assertSame('picker', $schema['fields'][0]['parts']['input']['modules'][0]['name'] ?? null); + $this->assertSame('dialog', $schema['fields'][0]['parts']['input']['modules'][0]['config']['mode'] ?? null); + $this->assertStringContainsString('data-builder-modules=', $html); + $this->assertStringContainsString('markdown', $html); + $this->assertStringContainsString('dialog', $html); + } + + public function testAttributeObjectsCanMutateRootAndFieldParts(): void + { + $builder = $this->newBuilder(); + $builder->define(function ($form) { + $form->attrsItem() + ->id('AttrForm') + ->class('attr-form') + ->data('scene', 'root'); + + $form->fields(function ($fields) { + $field = $fields->text('nickname', '用户名称'); + $field->attrsItem()->class('field-shell')->data('scene', 'field'); + $field->label()->attrsItem()->class('label-bag')->data('scene', 'label'); + }); + })->build(); + + $schema = $builder->toArray(); + $html = $this->invokePrivate($builder, '_buildFormModal'); + + $this->assertSame('AttrForm', $schema['attrs']['id'] ?? null); + $this->assertSame('root', $schema['attrs']['data-scene'] ?? null); + $this->assertSame('attr-form layui-form layui-card', $schema['attrs']['class'] ?? null); + $this->assertSame('field', $schema['content'][0]['attrs']['data-scene'] ?? null); + $this->assertSame('label-bag', $schema['fields'][0]['parts']['label']['attrs']['class'] ?? null); + $this->assertSame('label', $schema['fields'][0]['parts']['label']['attrs']['data-scene'] ?? null); + $this->assertStringContainsString('id="AttrForm"', $html); + $this->assertStringContainsString('class="attr-form layui-form layui-card"', $html); + $this->assertStringContainsString('class="field-shell layui-form-item block relative"', $html); + $this->assertStringContainsString('class="label-bag help-label"', $html); + $this->assertStringContainsString('data-scene="label"', $html); + } + + public function testFormCanRenderRawHtmlNodes(): void + { + $builder = $this->newBuilder(); + $builder->define(function ($form) { + $form->section(function ($group) { + $group->class('profile-section'); + $group->html('
    表单说明
    '); + }); + })->build(); + + $schema = $builder->toArray(); + $html = $this->invokePrivate($builder, '_buildFormModal'); + + $this->assertSame('html', $schema['content'][0]['children'][0]['type']); + $this->assertStringContainsString('class="profile-tip"', $html); + $this->assertStringContainsString('data-scene="tip"', $html); + } + + public function testFieldNodeCanMutateStructuredParts(): void + { + $builder = $this->newBuilder(); + $builder->define(function ($form) { + $form->fields(function ($fields) { + $fields->text('nickname', '用户名称', 'Nickname', true, '请输入用户名称') + ->class('field-shell') + ->data('scene', 'profile') + ->module('picker', ['target' => 'nickname']) + ->label()->class('field-label')->class(['field-label', 'field-label-strong'])->module('tooltip', ['target' => 'nickname'])->end() + ->body()->class('field-body')->data('body', 'profile')->end() + ->input()->class('input-lg')->data('role', 'primary')->attr('placeholder', '请填写用户名称')->end() + ->remarkNode()->class('field-remark')->html('请填写用户名称')->end() + ->text('email', '联系邮箱', 'Email'); + }); + })->build(); + + $schema = $builder->toArray(); + $html = $this->invokePrivate($builder, '_buildFormModal'); + + $this->assertSame('field', $schema['content'][0]['type']); + $this->assertSame('profile', $schema['content'][0]['attrs']['data-scene']); + $this->assertSame('picker', $schema['content'][0]['modules'][0]['name']); + $this->assertSame('field-label field-label-strong', $schema['fields'][0]['parts']['label']['attrs']['class']); + $this->assertSame('tooltip', $schema['fields'][0]['parts']['label']['modules'][0]['name']); + $this->assertSame('field-body', $schema['fields'][0]['parts']['body']['attrs']['class']); + $this->assertSame('input-lg', $schema['fields'][0]['parts']['input']['attrs']['class']); + $this->assertSame('primary', $schema['fields'][0]['parts']['input']['attrs']['data-role']); + $this->assertSame('请填写用户名称', $schema['fields'][0]['parts']['input']['attrs']['placeholder']); + $this->assertSame('field-remark', $schema['fields'][0]['parts']['remark']['attrs']['class']); + $this->assertStringContainsString('class="field-shell layui-form-item block relative"', $html); + $this->assertStringContainsString('data-scene="profile"', $html); + $this->assertStringContainsString('data-builder-modules=', $html); + $this->assertStringContainsString('class="field-label field-label-strong help-label label-required-prev"', $html); + $this->assertStringContainsString('class="field-body"', $html); + $this->assertStringContainsString('data-body="profile"', $html); + $this->assertStringContainsString('data-role="primary"', $html); + $this->assertStringContainsString('class="input-lg layui-input"', $html); + $this->assertStringContainsString('class="field-remark help-block"', $html); + $this->assertStringContainsString('placeholder="请填写用户名称"', $html); + } + + public function testTypedFieldsCanMutateRuntimeSchemaAndRules(): void + { + $builder = $this->newBuilder(); + $nodes = []; + $builder->define(function ($form) use (&$nodes) { + $form->fields(function ($fields) use (&$nodes) { + $nodes['text'] = $fields->text('appid', '应用') + ->title('应用标识') + ->required() + ->pattern('^wx[0-9a-z]{16}$') + ->rule('max:18', '应用标识最多18位字符!') + ->placeholder('请输入 AppId'); + $nodes['text']->maxlength(18)->readonly(); + + $nodes['select'] = $fields->select('status', '状态', 'Status', false, '', [1 => '启用', 0 => '禁用']); + $nodes['select']->source('statusOptions')->search()->option(2, '待审核'); + + $nodes['choice'] = $fields->checkbox('roles', '角色', '', 'roles'); + $nodes['choice']->source('roleOptions')->required(); + + $nodes['upload'] = $fields->image('cover', '封面'); + $nodes['upload']->types('png,webp'); + }); + })->build(); + + $schema = $builder->toArray(); + $rules = $builder->getValidateRules(); + $html = $this->invokePrivate($builder, '_buildFormModal'); + + $this->assertInstanceOf(FormTextField::class, $nodes['text']); + $this->assertInstanceOf(FormSelectField::class, $nodes['select']); + $this->assertInstanceOf(FormChoiceField::class, $nodes['choice']); + $this->assertInstanceOf(FormUploadField::class, $nodes['upload']); + + $this->assertSame('请输入 AppId', $schema['fields'][0]['attrs']['placeholder']); + $this->assertSame(18, $schema['fields'][0]['attrs']['maxlength']); + $this->assertArrayHasKey('readonly', $schema['fields'][0]['attrs']); + $this->assertSame('statusOptions', $schema['fields'][1]['vname']); + $this->assertArrayHasKey('lay-search', $schema['fields'][1]['attrs']); + $this->assertSame('待审核', $schema['fields'][1]['options']['2']); + $this->assertSame('roleOptions', $schema['fields'][2]['vname']); + $this->assertSame('png,webp', $schema['fields'][3]['upload']['types']); + + $this->assertSame('应用标识不能为空!', $rules['appid.require']); + $this->assertSame('应用标识格式错误!', $rules['appid.regex:/^wx[0-9a-z]{16}$/']); + $this->assertSame('应用标识最多18位字符!', $rules['appid.max:18']); + $this->assertSame('角色不能为空!', $rules['roles.require']); + $this->assertStringContainsString('placeholder="请输入 AppId"', $html); + $this->assertStringContainsString('data-type="png,webp"', $html); + } + + public function testUploadConfigObjectCanMutateSchemaAndRuntime(): void + { + $builder = $this->newBuilder(); + $builder->define(function ($form) { + $form->fields(function ($fields) { + $fields->image('cover', '封面') + ->uploadConfig() + ->types('png,webp') + ->triggerClass('upload-trigger') + ->triggerIcon('layui-icon-camera') + ->triggerAttr('data-scene', 'cover-upload') + ->runtimeSelector('#cover-upload') + ->runtimeMethod('uploadCoverImage'); + }); + })->build(); + + $schema = $builder->toArray(); + $html = $this->invokePrivate($builder, '_buildFormModal'); + + $this->assertSame('png,webp', $schema['fields'][0]['upload']['types'] ?? null); + $this->assertSame('upload-trigger', $schema['fields'][0]['upload']['trigger']['class'] ?? null); + $this->assertSame('layui-icon-camera', $schema['fields'][0]['upload']['trigger']['icon'] ?? null); + $this->assertSame('cover-upload', $schema['fields'][0]['upload']['trigger']['attrs']['data-scene'] ?? null); + $this->assertSame('#cover-upload', $schema['fields'][0]['upload']['runtime']['selector'] ?? null); + $this->assertSame('uploadCoverImage', $schema['fields'][0]['upload']['runtime']['method'] ?? null); + $this->assertStringContainsString('class="layui-icon layui-icon-camera input-right-icon upload-trigger"', $html); + $this->assertStringContainsString('data-scene="cover-upload"', $html); + $this->assertStringContainsString('data-type="png,webp"', $html); + $this->assertStringContainsString('$("#cover-upload").uploadCoverImage()', $html); + } + + public function testFieldOptionsObjectsCanMutateSourceAndOptions(): void + { + $builder = $this->newBuilder(); + $builder->define(function ($form) { + $form->fields(function ($fields) { + $fields->select('status', '状态', 'Status', false, '', [1 => '启用', 0 => '禁用']) + ->optionsItem() + ->source('statusOptions') + ->option(2, '待审核') + ->removeOption(0); + + $fields->checkbox('roles', '角色', '', 'roles') + ->optionsItem() + ->source('roleOptions') + ->option('admin', '管理员'); + }); + })->build(); + + $schema = $builder->toArray(); + $html = $this->invokePrivate($builder, '_buildFormModal'); + + $this->assertSame('statusOptions', $schema['fields'][0]['vname'] ?? null); + $this->assertSame('启用', $schema['fields'][0]['options']['1'] ?? null); + $this->assertSame('待审核', $schema['fields'][0]['options']['2'] ?? null); + $this->assertArrayNotHasKey('0', $schema['fields'][0]['options']); + $this->assertSame('roleOptions', $schema['fields'][1]['vname'] ?? null); + $this->assertSame('管理员', $schema['fields'][1]['options']['admin'] ?? null); + $this->assertStringContainsString('{foreach $statusOptions as $k=>$v}', $html); + $this->assertStringContainsString('', $html); + } + + public function testFormPresetsAndComponentsCanRenderStructuredNodes(): void + { + $builder = $this->newBuilder('form', 'page')->preset('page-form'); + $builder->define(function ($form) { + $form->title('资料设置'); + + $form->component(FormComponents::intro()->config([ + 'title' => '基础资料', + 'description' => '通过组件对象输出表单结构。', + ])); + + $section = $form->component(FormComponents::section()->config([ + 'title' => '账号信息', + 'description' => '支持 DOM 级插入和字段组合。', + ])->body(function ($body) { + $body->component(FormComponents::note('请确认账号信息后再保存。')); + $body->fields(function ($fields) { + $fields->text('nickname', '用户名称'); + }); + })); + + $section->prepend('div', function ($node) { + $node->class('section-prefix')->html('前置提示'); + }); + })->build(); + + $schema = $builder->toArray(); + $html = $this->invokePrivate($builder, '_buildFormPage'); + + $this->assertSame('page-form', $schema['preset']); + $this->assertStringContainsString('data-builder-mode="page"', $html); + $this->assertStringContainsString('data-builder-preset="page-form"', $html); + $this->assertStringContainsString('通过组件对象输出表单结构。', $html); + $this->assertStringContainsString('前置提示', $html); + $this->assertStringContainsString('请确认账号信息后再保存。', $html); + } + + public function testDialogFormPresetCanExposeExpectedSchema(): void + { + $builder = $this->newBuilder('form', 'modal')->preset('dialog-form')->build(); + $schema = $builder->toArray(); + $html = $this->invokePrivate($builder, '_buildFormModal'); + + $this->assertSame('dialog-form', $schema['preset'] ?? null); + $this->assertStringContainsString('data-builder-mode="modal"', $html); + $this->assertStringContainsString('data-builder-preset="dialog-form"', $html); + } + + public function testMultipleActionBarsOnlyRenderIdentityFieldOnce(): void + { + $builder = $this->newBuilder(); + $builder->define(function ($form) { + $form->variable('data') + ->fields(function ($fields) { + $fields->text('nickname', '用户名称'); + }); + $form->actionBar(function ($actions) { + $actions->submit('保存'); + }); + $form->actionBar(function ($actions) { + $actions->cancel('关闭'); + }); + })->build(); + + $html = $this->invokePrivate($builder, '_buildFormModal'); + + $this->assertSame(1, substr_count($html, 'name="id"')); + $this->assertSame(1, substr_count($html, '
    ')); + $this->assertStringContainsString('>保存', $html); + $this->assertStringContainsString('>关闭', $html); + } + + public function testFormCanUseDirectObjectAccessors(): void + { + $builder = $this->newBuilder(); + $builder->define(function ($form) { + $form->action('/submit')->variable('data'); + $form->fieldsNode()->text('nickname', '用户名称')->placeholder('请输入用户名称'); + $form->actionsNode()->submit('保存', '', ['data-scene' => 'direct-submit']); + })->build(); + + $schema = $builder->toArray(); + $html = $this->invokePrivate($builder, '_buildFormModal'); + + $this->assertSame('/submit', $schema['action']); + $this->assertSame('nickname', $schema['fields'][0]['name']); + $this->assertSame('direct-submit', $schema['buttons'][0]['attrs']['data-scene'] ?? null); + $this->assertStringContainsString('placeholder="请输入用户名称"', $html); + $this->assertStringContainsString('data-scene="direct-submit"', $html); + } + + public function testFormBodyAttributesCanBeConfiguredByBuilder(): void + { + $builder = $this->newBuilder(); + $builder->define(function ($form) { + $form->bodyClass('pa20') + ->bodyClass(['profile-body', 'pa20']) + ->bodyData('scene', 'profile') + ->bodyAttrsItem() + ->id('ProfileFormBody') + ->attr('data-mode', 'modal') + ->end(); + + $form->fields(function ($fields) { + $fields->text('nickname', '用户名称'); + }); + })->build(); + + $schema = $builder->toArray(); + $html = $this->invokePrivate($builder, '_buildFormModal'); + + $bodyClass = strval($schema['body_attrs']['class'] ?? ''); + foreach (['layui-card-body', 'pl40', 'pr40', 'pt20', 'pb20', 'pa20', 'profile-body'] as $class) { + $this->assertStringContainsString($class, $bodyClass); + } + $this->assertSame('profile', $schema['body_attrs']['data-scene'] ?? null); + $this->assertSame('modal', $schema['body_attrs']['data-mode'] ?? null); + $this->assertSame('ProfileFormBody', $schema['body_attrs']['id'] ?? null); + $this->assertStringContainsString('id="ProfileFormBody"', $html); + foreach (['layui-card-body', 'pl40', 'pr40', 'pt20', 'pb20', 'pa20', 'profile-body'] as $class) { + $this->assertStringContainsString($class, $html); + } + $this->assertStringContainsString('data-scene="profile"', $html); + $this->assertStringContainsString('data-mode="modal"', $html); + } + + public function testRemovingFormNodesKeepsSchemaAndHtmlConsistent(): void + { + $builder = $this->newBuilder(); + $builder->define(function ($form) { + $field = $form->fieldsNode()->text('name', '名称'); + $form->actions(function ($actions) { + $actions->submit('保存'); + }); + + $field->remove(); + $form->actionBar()->remove(); + })->build(); + + $schema = $builder->toArray(); + $html = $this->invokePrivate($builder, '_buildFormModal'); + + $this->assertSame([], $schema['content']); + $this->assertSame([], $schema['fields']); + $this->assertSame([], $schema['buttons']); + $this->assertStringNotContainsString('name="name"', $html); + $this->assertStringNotContainsString('>保存', $html); + } + + public function testActionBarCanBeRecreatedAfterRemoval(): void + { + $builder = $this->newBuilder(); + $builder->define(function ($form) { + $bar = $form->actionBar(); + $bar->class('first-bar'); + $bar->remove(); + + $form->actionBar()->class('second-bar'); + $form->actions(function ($actions) { + $actions->submit('保存'); + }); + })->build(); + + $schema = $builder->toArray(); + $html = $this->invokePrivate($builder, '_buildFormModal'); + + $this->assertSame(1, count($schema['buttons'])); + $this->assertStringNotContainsString('first-bar', $html); + $this->assertStringContainsString('second-bar', $html); + $this->assertStringContainsString('>保存', $html); + } + + public function testFormLayoutRemoveApisCanUpdateRootAttributes(): void + { + $builder = $this->newBuilder(); + $builder->define(function ($form) { + $form->class('alpha') + ->attr('data-scene', 'demo') + ->data('mode', 'modal') + ->removeClass('alpha') + ->removeAttr('data-scene') + ->removeData('mode'); + })->build(); + + $schema = $builder->toArray(); + $html = $this->invokePrivate($builder, '_buildFormModal'); + + $this->assertStringNotContainsString('alpha', strval($schema['attrs']['class'] ?? '')); + $this->assertArrayNotHasKey('data-scene', $schema['attrs']); + $this->assertArrayNotHasKey('data-mode', $schema['attrs']); + $this->assertStringNotContainsString('data-scene="demo"', $html); + $this->assertStringNotContainsString('data-mode="modal"', $html); + } + + public function testFormNodesPreventCyclicReparenting(): void + { + $builder = $this->newBuilder(); + $builder->define(function ($form) { + $outer = $form->div()->class('outer'); + $inner = $outer->div()->class('inner'); + $inner->appendNode($form); + })->build(); + + $schema = $builder->toArray(); + $html = $this->invokePrivate($builder, '_buildFormModal'); + + $this->assertCount(1, $schema['content']); + $this->assertSame('outer', $schema['content'][0]['attrs']['class'] ?? null); + $this->assertSame('inner', $schema['content'][0]['children'][0]['attrs']['class'] ?? null); + $this->assertStringContainsString('class="outer"', $html); + $this->assertStringContainsString('class="inner"', $html); + } + + private function newBuilder(string $type = 'form', string $mode = 'modal'): FormBuilder + { + $controller = $this->getMockBuilder(Controller::class)->disableOriginalConstructor()->getMock(); + return new FormBuilder($type, $mode, $controller); + } + + private function invokePrivate(object $object, string $method): mixed + { + $ref = new \ReflectionMethod($object, $method); + $ref->setAccessible(true); + return $ref->invoke($object); + } +} diff --git a/plugin/think-library/tests/JwtTest.php b/plugin/think-library/tests/JwtTest.php new file mode 100644 index 000000000..b11dea059 --- /dev/null +++ b/plugin/think-library/tests/JwtTest.php @@ -0,0 +1,40 @@ + 'admin' . mt_rand(0, 1000), 'iss' => 'thinkadmin.top', 'exp' => time() + 30]; + $token = JwtToken::token($testdata, $jwtkey); + $result = JwtToken::verify($token, $jwtkey); + $this->assertEquals($testdata['user'], $result['user']); + } +} diff --git a/plugin/think-library/tests/MigrationOwnershipTest.php b/plugin/think-library/tests/MigrationOwnershipTest.php new file mode 100644 index 000000000..bb755bacf --- /dev/null +++ b/plugin/think-library/tests/MigrationOwnershipTest.php @@ -0,0 +1,160 @@ +projectRoot = TEST_PROJECT_ROOT; + } + + public function testSystemTablesAreOwnedBySystemPluginMigration(): void + { + $owner = $this->path('plugin/think-plugs-system/stc/database/20241010000001_install_system20241010.php'); + $this->assertFileExists($owner); + + $content = $this->read($owner); + foreach (['system_base', 'system_data', 'system_oplog', 'system_auth', 'system_auth_node', 'system_menu', 'system_user'] as $table) { + $this->assertStringContainsString($table, $content); + } + } + + public function testSharedTablesAreOwnedBySystemAndWorkerPlugins(): void + { + $system = $this->path('plugin/think-plugs-system/stc/database/20241010000001_install_system20241010.php'); + $worker = $this->path('plugin/think-plugs-worker/stc/database/20241010000008_install_worker20241010.php'); + + $this->assertFileExists($system); + $this->assertFileExists($worker); + + $this->assertStringContainsString('system_file', $this->read($system)); + $this->assertStringContainsString('system_queue', $this->read($worker)); + } + + public function testSystemPluginUsesSingleInstallMigration(): void + { + $this->assertFileDoesNotExist($this->path('plugin/think-plugs-system/stc/database/20241010000001_install_system_manage20241010.php')); + $this->assertFileDoesNotExist($this->path('plugin/think-plugs-system/stc/database/20241010000011_install_system20241010.php')); + } + + public function testEveryPluginKeepsOnlyOnePrimaryMigrationFile(): void + { + $plugins = [ + 'account', + 'payment', + 'system', + 'wechat-client', + 'wechat-service', + 'wemall', + 'worker', + 'wuma', + ]; + + foreach ($plugins as $plugin) { + $files = glob($this->path("plugin/think-plugs-{$plugin}/stc/database/*.php")) ?: []; + sort($files); + $this->assertCount(1, $files, "plugin {$plugin} should keep exactly one migration file"); + $this->assertStringContainsString('_install_', basename($files[0])); + } + } + + public function testPluginsWithMigrationFilesDeclareExtraXadminMigrate(): void + { + $missing = []; + + foreach (glob($this->path('plugin/*/composer.json')) ?: [] as $file) { + $manifest = json_decode($this->read($file), true); + if (!is_array($manifest)) { + continue; + } + + $migrations = glob(dirname($file) . '/stc/database/*.php') ?: []; + if ($migrations === []) { + continue; + } + + $migrate = $manifest['extra']['xadmin']['migrate'] ?? null; + if (!is_array($migrate) || trim(strval($migrate['file'] ?? '')) === '') { + $missing[] = str_replace($this->projectRoot . '/', '', $file); + } + } + + $this->assertSame([], $missing, 'Plugins with stc/database migrations must declare extra.xadmin.migrate: ' . implode(', ', $missing)); + } + + public function testSharedMigrationTablesDoNotLeakToOtherPlugins(): void + { + $owners = [ + 'system_base' => $this->path('plugin/think-plugs-system/stc/database/20241010000001_install_system20241010.php'), + 'system_data' => $this->path('plugin/think-plugs-system/stc/database/20241010000001_install_system20241010.php'), + 'system_oplog' => $this->path('plugin/think-plugs-system/stc/database/20241010000001_install_system20241010.php'), + 'system_file' => $this->path('plugin/think-plugs-system/stc/database/20241010000001_install_system20241010.php'), + 'system_queue' => $this->path('plugin/think-plugs-worker/stc/database/20241010000008_install_worker20241010.php'), + ]; + + $migrations = $this->migrationFiles(); + $violations = []; + + foreach ($migrations as $file) { + $content = $this->read($file); + foreach ($owners as $table => $owner) { + if ($file === $owner) { + continue; + } + if (strpos($content, $table) !== false) { + $violations[] = [$table, $file]; + } + } + } + + $this->assertSame([], $violations, 'Unexpected migration ownership violations: ' . json_encode($violations, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)); + } + + /** + * @return list + */ + private function migrationFiles(): array + { + $files = glob($this->path('plugin/*/stc/database/*.php')) ?: []; + sort($files); + return array_values($files); + } + + private function read(string $path): string + { + return file_get_contents($path) ?: ''; + } + + private function path(string $relative): string + { + return $this->projectRoot . '/' . ltrim($relative, '/'); + } +} diff --git a/plugin/think-library/tests/ModelTest.php b/plugin/think-library/tests/ModelTest.php new file mode 100644 index 000000000..4d86f2f90 --- /dev/null +++ b/plugin/think-library/tests/ModelTest.php @@ -0,0 +1,41 @@ +assertEquals(m('SystemUser')->getTable(), VirtualModelTestStub::mk()->getTable(), '动态模型测试'); + } +} diff --git a/plugin/think-library/tests/MultAccessDispatchTest.php b/plugin/think-library/tests/MultAccessDispatchTest.php new file mode 100644 index 000000000..d2d56e8c0 --- /dev/null +++ b/plugin/think-library/tests/MultAccessDispatchTest.php @@ -0,0 +1,293 @@ + */ + private array $createdFiles = []; + + /** @var list */ + private array $createdDirectories = []; + + protected function setUp(): void + { + parent::setUp(); + $this->projectRoot = TEST_PROJECT_ROOT; + $this->resetRuntimeContext(); + } + + protected function tearDown(): void + { + $this->cleanupPaths(); + $this->resetRuntimeContext(); + parent::tearDown(); + } + + public function testLocalAppsAreDiscoveredWithoutTreatingSharedDirectoriesAsApps(): void + { + $this->createLocalApp('demoapp'); + $this->bootApplication(); + + $locals = AppService::local(true); + $this->assertArrayHasKey('index', $locals); + $this->assertArrayHasKey('demoapp', $locals); + $this->assertArrayNotHasKey('controller', $locals); + $this->assertArrayNotHasKey('model', $locals); + + $payload = $this->dispatchPath($this->bootApplication(), 'demoapp/dashboard/index'); + $this->assertSame('demoapp', $payload['app']); + $this->assertSame('', $payload['plugin']); + $this->assertSame(RequestContext::ENTRY_WEB, $payload['entry']); + $this->assertSame('/demoapp', $payload['root']); + $this->assertSame('dashboard/index', $payload['pathinfo']); + } + + public function testExplicitPluginPrefixStillWinsOverGlobalRoutes(): void + { + $this->createLocalApp('demoapp'); + $this->createRouteFile('multaccess_explicit_plugin.php', <<<'PHP' +dispatchPath($this->bootApplication(), 'system/login'); + $this->assertSame('system', $payload['app']); + $this->assertSame('system', $payload['plugin']); + $this->assertSame(RequestContext::ENTRY_WEB, $payload['entry']); + $this->assertSame('/system', $payload['root']); + $this->assertSame('login', $payload['pathinfo']); + } + + public function testGlobalRoutesCanBindLocalAndPluginTargets(): void + { + $this->createLocalApp('demoapp'); + $this->createRouteFile('multaccess_targets.php', <<<'PHP' +dispatchPath($this->bootApplication(), 'shortcut'); + $this->assertSame('demoapp', $local['app']); + $this->assertSame('', $local['plugin']); + $this->assertSame(RequestContext::ENTRY_WEB, $local['entry']); + $this->assertSame('shortcut', $local['pathinfo']); + + $plugin = $this->dispatchPath($this->bootApplication(), 'open-upload'); + $this->assertSame('system', $plugin['app']); + $this->assertSame('system', $plugin['plugin']); + $this->assertSame(RequestContext::ENTRY_API, $plugin['entry']); + $this->assertSame('open-upload', $plugin['pathinfo']); + } + + public function testGlobalRouteGroupsCanDeclareDispatchTarget(): void + { + $this->createLocalApp('demoapp'); + $this->createRouteFile('multaccess_groups.php', <<<'PHP' +dispatchPath($this->bootApplication(), 'group-shortcut'); + $this->assertSame('demoapp', $local['app']); + $this->assertSame('', $local['plugin']); + $this->assertSame(RequestContext::ENTRY_WEB, $local['entry']); + + $plugin = $this->dispatchPath($this->bootApplication(), 'group-open-upload'); + $this->assertSame('system', $plugin['app']); + $this->assertSame('system', $plugin['plugin']); + $this->assertSame(RequestContext::ENTRY_API, $plugin['entry']); + } + + public function testLegacyModuleStyleGlobalRoutesCanStillInferDispatchTarget(): void + { + $this->createLocalApp('demoapp'); + $this->createRouteFile('multaccess_legacy_targets.php', <<<'PHP' +dispatchPath($this->bootApplication(), 'legacy-demo'); + $this->assertSame('demoapp', $local['app']); + $this->assertSame('', $local['plugin']); + $this->assertSame(RequestContext::ENTRY_WEB, $local['entry']); + + $plugin = $this->dispatchPath($this->bootApplication(), 'legacy-system'); + $this->assertSame('system', $plugin['app']); + $this->assertSame('system', $plugin['plugin']); + $this->assertSame(RequestContext::ENTRY_WEB, $plugin['entry']); + } + + private function bootApplication(): App + { + $this->resetRuntimeContext(); + + $app = new App($this->projectRoot); + RuntimeService::init($app); + $app->initialize(); + $app->config->set(['with_route' => true], 'app'); + $app->config->set(['default_app' => 'index'], 'route'); + + return $app; + } + + /** + * @return array + */ + private function dispatchPath(App $app, string $pathinfo): array + { + $request = $app->make('request', [], true); + $request->setRoot(''); + $request->setPathinfo($pathinfo); + $app->instance('request', $request); + + $response = (new MultAccess($app))->handle($request, static function (Request $request) use ($app): Response { + return Response::create([ + 'app' => $app->http->getName(), + 'plugin' => RequestContext::instance()->pluginCode(), + 'entry' => RequestContext::instance()->entryType(), + 'root' => $request->root(), + 'pathinfo' => $request->pathinfo(), + ], 'json'); + }); + + return (array)json_decode((string)$response->getContent(), true, 512, JSON_THROW_ON_ERROR); + } + + private function createLocalApp(string $code): void + { + $base = $this->projectRoot . '/app/' . $code; + $controllerPath = $base . '/controller'; + if (!is_dir($controllerPath)) { + mkdir($controllerPath, 0777, true); + } + + $this->createdDirectories[] = $base; + + $file = $controllerPath . '/Index.php'; + file_put_contents($file, str_replace('__CODE__', $code, <<<'PHP' +createdFiles[] = $file; + } + + private function createRouteFile(string $name, string $content): void + { + $directory = $this->projectRoot . '/route'; + if (!is_dir($directory)) { + mkdir($directory, 0777, true); + $this->createdDirectories[] = $directory; + } + $file = $this->projectRoot . '/route/' . $name; + file_put_contents($file, $content); + $this->createdFiles[] = $file; + } + + private function cleanupPaths(): void + { + foreach (array_reverse($this->createdFiles) as $file) { + if (is_file($file)) { + @unlink($file); + } + } + + foreach (array_reverse(array_unique($this->createdDirectories)) as $directory) { + $this->removeDirectory($directory); + } + } + + private function removeDirectory(string $path): void + { + if ($path === '' || !is_dir($path)) { + return; + } + + foreach (scandir($path) ?: [] as $item) { + if ($item === '.' || $item === '..') { + continue; + } + + $target = $path . DIRECTORY_SEPARATOR . $item; + if (is_dir($target)) { + $this->removeDirectory($target); + } elseif (is_file($target)) { + @unlink($target); + } + } + + @rmdir($path); + } + + private function resetRuntimeContext(): void + { + AppService::clear(); + RequestContext::clear(); + if (function_exists('sysvar')) { + sysvar('', ''); + } + function_exists('test_reset_model_makers') && test_reset_model_makers(); + } +} diff --git a/plugin/think-library/tests/PageBuilderTest.php b/plugin/think-library/tests/PageBuilderTest.php new file mode 100644 index 000000000..b25cf1c81 --- /dev/null +++ b/plugin/think-library/tests/PageBuilderTest.php @@ -0,0 +1,973 @@ +newBuilder(); + $builder->define(function ($page) { + $page->title('短信管理') + ->searchAttrs(['action' => '/message', 'class' => 'form-search search-panel']) + ->buttons(function ($buttons) { + $buttons->modal('短信配置', '/config', '', [], 'config') + ->open('查看报表', '/report', [], 'report') + ->batchAction('批量删除', '/remove', 'id#{key}', '确定删除吗?', ['data-scene' => 'batch']); + }) + ->bootScript('let scenes = {};') + ->search(function ($search) { + $search->input('smsid', '消息编号', '请输入消息编号') + ->field(['type' => 'input', 'name' => 'scene_keyword', 'label' => '业务场景', 'class' => 'search-scene-keyword']) + ->select('status', '执行结果', [0 => '失败', 1 => '成功']) + ->dateRange('create_time', '发送时间', '请选择发送时间') + ->hidden('source', 'system') + ->submit('筛选', ['data-scene' => 'search-submit']); + }) + ->table('MessageData', '/message', function ($table) { + $table->checkbox() + ->sortInput('/sort') + ->column(['field' => 'smsid', 'title' => '消息编号', 'sort' => true]) + ->column(['field' => 'scene', 'title' => '业务场景', 'templet' => PageBuilder::js('function(d){ return d.scene; }')]) + ->statusSwitch('/state', [ + 'title' => '状态', + 'activeHtml' => '成功', + 'inactiveHtml' => '失败', + 'text' => '成功|失败', + ]) + ->rows(function ($rows) { + $rows->open('查看', '/detail?id={{d.id}}', '查看详情', [], 'view') + ->modal('编辑', '/edit?id={{d.id}}', '编辑', [], 'edit'); + }) + ->toolbar(); + }); + }); + $builder->addInitScript('window.pageReady = true;')->build(); + + $schema = $builder->toArray(); + $searches = $this->schemaNodesOfType($schema, 'search'); + $tables = $this->schemaNodesOfType($schema, 'table'); + $html = $this->invokePrivate($builder, 'render'); + + $this->assertSame('短信管理', $schema['title']); + $this->assertSame('MessageData', $tables[0]['id'] ?? null); + $this->assertSame('#SortInputMessageDataTpl', $tables[0]['columns'][1]['templet'] ?? null); + $this->assertSame('js', $tables[0]['columns'][3]['templet']['type'] ?? null); + $this->assertSame('#toolbarMessageData', $tables[0]['columns'][5]['toolbar'] ?? null); + $this->assertSame('config', $schema['buttons'][0]['auth'] ?? null); + $this->assertSame('open', $schema['buttons'][1]['type'] ?? null); + $this->assertSame('/report', $schema['buttons'][1]['url'] ?? null); + $this->assertSame('batch-action', $schema['buttons'][2]['type'] ?? null); + $this->assertSame('MessageData', $schema['buttons'][2]['attrs']['data-table-id'] ?? null); + $this->assertSame('hidden', $searches[0]['fields'][4]['type'] ?? null); + $this->assertSame('system', $searches[0]['fields'][4]['attrs']['value'] ?? null); + $this->assertSame('submit', $searches[0]['fields'][5]['type'] ?? null); + $this->assertStringContainsString('layui-card-header', $html); + $this->assertStringContainsString('layui-card-table', $html); + $this->assertStringContainsString('form-search', $html); + $this->assertStringContainsString('class="form-search search-panel layui-form layui-form-pane"', $html); + $this->assertStringContainsString('data-scene="batch"', $html); + $this->assertStringContainsString('type="hidden"', $html); + $this->assertStringContainsString('name="source"', $html); + $this->assertStringContainsString('value="system"', $html); + $this->assertStringContainsString('', $html); + $this->assertStringContainsString('data-scene="search-submit"', $html); + $this->assertStringContainsString('data-action="/remove"', $html); + $this->assertStringContainsString('data-rule="id#{key}"', $html); + $this->assertStringContainsString('data-confirm="确定删除吗?"', $html); + $this->assertStringContainsString('search-scene-keyword', $html); + $this->assertStringContainsString('data-url="/message"', $html); + $this->assertStringContainsString('id="PageSearchForm1"', $html); + $this->assertStringContainsString('data-target-search="#PageSearchForm1"', $html); + $this->assertStringContainsString('id="SortInputMessageDataTpl"', $html); + $this->assertStringContainsString('id="StatusSwitchMessageDataTpl"', $html); + $this->assertStringContainsString('lay-filter="StatusSwitchMessageData"', $html); + $this->assertStringContainsString('$.form.load("/state"', $html); + $this->assertStringContainsString("else {\n \$(\"#MessageData\").trigger(\"reload\");\n }", $html); + $this->assertTrue( + strpos($html, 'let scenes = {};') < strpos($html, "$('#MessageData').layTable(") + ); + $this->assertTrue( + strpos($html, "$('#MessageData').layTable(") < strpos($html, 'window.pageReady = true;') + ); + $this->assertStringContainsString('function(d){ return d.scene; }', $html); + $this->assertStringContainsString(' + * + * 授权模式支持两种模块,参数 mode=0 时为静默授权,mode=1 时为完整授权 + * 注意:回跳地址默认从 Header 中的 http_referer 获取,也可以传 source 参数 + */ +class Wechat extends Controller +{ + /** + * 通道认证类型. + * @var string + */ + private const type = Account::WECHAT; + + /** + * 接口原地址 + * @var string + */ + private $source; + + /** + * 微信调度器. + * @var WechatService + */ + private $wechat; + + /** + * 生成微信网页签名. + * @throws InvalidResponseException + * @throws LocalCacheException + * @throws Exception + */ + public function jssdk() + { + $this->success('获取网页签名', $this->wechat->getWebJssdkSign($this->source)); + } + + /** + * 微信网页授权脚本. + * @throws InvalidResponseException + * @throws LocalCacheException + * @throws Exception + * @remark 基于 OAuth 标识与 Token 的登录机制 + */ + public function oauth(): Response + { + $oauthFailed = json_encode(lang('微信网页授权失败'), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + $virtualDenied = json_encode(lang("不支持虚拟用户登录!\n请 10 秒后刷新页面选择授权!"), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + $script = []; + $result = $this->wechat->getWebOauthInfo($this->source, intval(input('mode', 0)), false); + if (empty($result['openid'])) { + $script[] = "alert({$oauthFailed})"; + } else { + $fansinfo = $result['fansinfo'] ?? []; + if (empty($fansinfo['is_snapshotuser'])) { + // 筛选保存数据 + $data = ['appid' => WechatService::getAppid(), 'openid' => $result['openid'], 'extra' => $fansinfo]; + if (isset($fansinfo['unionid'])) { + $data['unionid'] = $fansinfo['unionid']; + } + if (isset($fansinfo['nickname'])) { + $data['nickname'] = $fansinfo['nickname']; + } + if (isset($fansinfo['headimgurl'])) { + $data['headimg'] = $fansinfo['headimgurl']; + } + $result['userinfo'] = Account::mk(self::type)->set($data, true); + Account::syncTokenCookie(strval($result['userinfo']['token'] ?? '')); + // 返回数据给前端 + $script[] = "window.WeChatOpenid='{$result['openid']}'"; + $script[] = 'window.WeChatFansInfo=' . json_encode($result['fansinfo'], 64 | 128 | 256); + $script[] = 'window.WeChatUserInfo=' . json_encode($result['userinfo'], 64 | 128 | 256); + $script[] = "sessionStorage.setItem('wechat.token','{$result['userinfo']['token']}')"; + } else { + $script[] = "alert({$virtualDenied})"; + $script[] = 'location.reload()'; + } + } + $script[] = ''; + return Response::create(join(";\n", $script))->contentType('application/javascript'); + } + + /** + * 控制器初始化. + */ + protected function initialize() + { + if (Account::field(self::type)) { + $this->wechat = WechatService::instance(); + $this->source = input('source') ?: $this->request->server('http_referer', $this->request->url(true)); + } else { + $this->error('接口未开通'); + } + } +} diff --git a/plugin/think-plugs-account/src/controller/api/Wxapp.php b/plugin/think-plugs-account/src/controller/api/Wxapp.php new file mode 100644 index 000000000..a064b94cc --- /dev/null +++ b/plugin/think-plugs-account/src/controller/api/Wxapp.php @@ -0,0 +1,224 @@ +_vali(['code.require' => '凭证编码为空']); + $session = $this->wxapp->getSession($input['code']); + $data = [ + 'appid' => $this->wxapp->getAppid(), + 'openid' => $session['openid'], + 'unionid' => $session['unionid'] ?? '', + 'session_key' => $session['session_key'], + ]; + $this->successWithToken('授权换取成功', Account::mk($this->type)->set($data, true)); + } catch (HttpResponseException $exception) { + throw $exception; + } catch (\Exception $exception) { + trace_file($exception); + $this->error(sprintf(lang('处理失败,%s'), $exception->getMessage())); + } + } + + /** + * 数据解密. + */ + public function decode() + { + try { + $input = $this->_vali([ + 'iv.require' => '解密向量为空', + 'code.require' => '授权编码为空', + 'encrypted.require' => '密文内容为空', + ]); + $session = $this->wxapp->getSession($input['code']); + $result = $this->wxapp->decode($input['iv'], strval($session['session_key']), $input['encrypted']); + if (is_array($result) && isset($result['avatarUrl'], $result['nickName'])) { + $data = [ + 'extra' => $result, + 'appid' => $this->wxapp->getAppid(), + 'openid' => $session['openid'], + 'unionid' => $session['unionid'] ?? '', + 'headimg' => $result['avatarUrl'], + 'nickname' => $result['nickName'], + ]; + if ($data['nickname'] === '微信用户') { + unset($data['headimg'], $data['nickname']); + } + $this->successWithToken('解密成功', Account::mk($this->type)->set($data, true)); + } elseif (is_array($result)) { + if (!empty($result['phoneNumber'])) { + $data = ['appid' => $this->wxapp->getAppid(), 'openid' => $session['openid'], 'unionid' => $session['unionid'] ?? '']; + ($account = Account::mk($this->type))->set($data); + $account->bind(['phone' => $result['phoneNumber']], $data); + $this->successWithToken('绑定成功', $account->get(true)); + } else { + $this->success('解密成功', $result); + } + } else { + $this->error('解析失败'); + } + } catch (HttpResponseException $exception) { + throw $exception; + } catch (\Exception $exception) { + trace_file($exception); + $this->error(sprintf(lang('处理失败,%s'), $exception->getMessage())); + } + } + + /** + * 快速获取手机号. + */ + public function phone() + { + try { + $input = $this->_vali([ + 'code.require' => '授权编码为空', + 'openid.require' => '用户编号为空', + ]); + $result = $this->wxapp->getPhoneNumber($input['code']); + if (is_array($result)) { + $this->success('解密成功', $result); + } else { + $this->error('解析失败'); + } + } catch (HttpResponseException $exception) { + throw $exception; + } catch (\Exception $exception) { + trace_file($exception); + $this->error(sprintf(lang('处理失败,%s'), $exception->getMessage())); + } + } + + /** + * 获取小程序码 + */ + public function qrcode(): Response + { + try { + $data = $this->_vali([ + 'size.default' => 430, + 'type.default' => 'base64', + 'path.require' => '跳转链接为空', + ]); + $result = $this->wxapp->createMiniPath($data['path'], intval($data['size'])); + if ($data['type'] === 'base64') { + $this->success('生成小程序码', ['base64' => 'data:image/png;base64,' . base64_encode($result)]); + } + return response($result)->contentType('image/png'); + } catch (HttpResponseException $exception) { + throw $exception; + } catch (\Exception $exception) { + trace_file($exception); + $this->error($exception->getMessage()); + } + } + + /** + * 获取直播列表. + */ + public function getLiveList() + { + try { + $data = $this->_vali(['start.default' => 0, 'limit.default' => 10]); + $list = $this->wxapp->getLiveList(intval($data['start']), intval($data['limit'])); + $this->success('直播列表', $list); + } catch (HttpResponseException $exception) { + throw $exception; + } catch (\Exception $exception) { + trace_file($exception); + $this->error($exception->getMessage()); + } + } + + /** + * 获取回放源视频. + */ + public function getLiveInfo() + { + try { + $data = $this->_vali([ + 'start.default' => 0, + 'limit.default' => 10, + 'action.default' => 'get_replay', + 'room_id.require' => '直播间号为空', + ]); + $result = $this->wxapp->getLiveInfo($data); + $this->success('回放列表', $result); + } catch (HttpResponseException $exception) { + throw $exception; + } catch (\Exception $exception) { + trace_file($exception); + $this->error($exception->getMessage()); + } + } + + /** + * 接口初始化. + * @throws \think\admin\Exception + */ + protected function initialize() + { + if (Account::field($this->type)) { + $this->wxapp = WxappService::instance(); + } else { + $this->error('接口未开通'); + } + } + + /** + * 返回带账号令牌的成功结果并同步 Cookie。 + */ + private function successWithToken(string $info, array $data): void + { + Account::syncTokenCookie(strval($data['token'] ?? '')); + $this->success($info, $data); + } +} diff --git a/plugin/think-plugs-account/src/controller/api/auth/Center.php b/plugin/think-plugs-account/src/controller/api/auth/Center.php new file mode 100644 index 000000000..c135a615c --- /dev/null +++ b/plugin/think-plugs-account/src/controller/api/auth/Center.php @@ -0,0 +1,157 @@ +success('获取资料', $this->account->get()); + } + + /** + * 修改帐号信息. + */ + public function set() + { + try { + $data = $this->checkUserStatus()->_vali([ + 'headimg.default' => '', + 'nickname.default' => '', + 'password.default' => '', + 'region_prov.default' => '', + 'region_city.default' => '', + 'region_area.default' => '', + ]); + // 保存用户头像 + if (!empty($data['headimg'])) { + $data['headimg'] = Storage::saveImage($data['headimg'], 'headimg')['url'] ?? ''; + } + // 修改登录密码 + $password = trim(strval($data['password'] ?? '')); + if (!password_is_unchanged($password) && strlen($password) > 4) { + $this->account->pwdModify($password); + } + unset($data['password']); + foreach ($data as $k => $v) { + if ($v === '') { + unset($data[$k]); + } + } + if (empty($data)) { + $this->success('无需修改', $this->account->get()); + } + $this->success('修改成功', $this->account->bind(['id' => $this->unid], $data)); + } catch (HttpResponseException $exception) { + throw $exception; + } catch (\Exception $exception) { + $this->error($exception->getMessage()); + } + } + + /** + * 注销当前账号. + */ + public function forbid() + { + if (($user = $this->account->user())->isExists()) { + try { + $this->app->db->transaction(function () use ($user) { + $user->save(['delete_time' => date('Y-m-d H:i:s'), 'remark' => '用户主动申请注销账号!']); + PluginAccountAuth::mk()->where(['usid' => $this->usid])->delete(); + PluginAccountBind::mk()->where(['unid' => $this->unid])->delete(); + }); + Account::destroySession(); + Account::forgetTokenCookie(); + $this->success('账号注销成功'); + } catch (HttpResponseException $exception) { + throw $exception; + } catch (\Exception $exception) { + $this->error($exception->getMessage()); + } + } else { + $this->error('未完成注册'); + } + } + + /** + * 绑定主账号. + */ + public function bind() + { + try { + $data = $this->_vali([ + 'phone.mobile' => '手机号错误', + 'phone.require' => '手机号为空', + 'verify.require' => '验证码为空', + 'passwd.default' => '', + ]); + if (Message::checkVerifyCode($data['verify'], $data['phone'])) { + Message::clearVerifyCode($data['phone']); + $map = $bind = ['phone' => $data['phone']]; + if (!$this->account->isBind()) { + $user = $this->account->get(); + $bind['headimg'] = $user['headimg']; + $bind['unionid'] = $user['unionid']; + $bind['nickname'] = $user['nickname']; + } + $this->account->set($map); + $this->account->bind($map, $bind); + if (!empty($data['passwd'])) { + $this->account->pwdModify($data['passwd']); + } + $result = $this->account->get(true); + Account::syncTokenCookie(strval($result['token'] ?? '')); + $this->success('关联成功', $result); + } else { + $this->error('验证失败'); + } + } catch (HttpResponseException $exception) { + throw $exception; + } catch (\Exception $exception) { + $this->error($exception->getMessage()); + } + } + + /** + * 解除账号关联. + */ + public function unbind() + { + $this->account->unBind(); + $this->success('关联成功', $this->account->get()); + } +} diff --git a/plugin/think-plugs-account/src/lang/en-us.php b/plugin/think-plugs-account/src/lang/en-us.php new file mode 100644 index 000000000..6048918ae --- /dev/null +++ b/plugin/think-plugs-account/src/lang/en-us.php @@ -0,0 +1,232 @@ + 'User Management', + '用户账号管理' => 'User Account Management', + '回 收 站' => 'Recycle Bin', + '排序权重' => 'Sort Weight', + '头像' => 'Avatar', + '账号状态' => 'Account Status', + '操作面板' => 'Actions', + '已激活' => 'Activated', + '已禁用' => 'Disabled', + '已启用' => 'Enabled', + '已冻结的用户' => 'Frozen Users', + '已激活的用户' => 'Activated Users', + '删 除' => 'Delete', + '保存数据' => 'Save Data', + '取消编辑' => 'Cancel Edit', + '保存配置' => 'Save Configuration', + '取消修改' => 'Cancel Modification', + '确定要取消编辑吗?' => 'Are you sure you want to cancel editing?', + '确定要取消修改吗?' => 'Are you sure you want to cancel the modification?', + '确定要永久删除此账号吗?' => 'Are you sure you want to permanently delete this account?', + '全部' => 'All', + '条件搜索' => 'Search Filters', + '搜 索' => 'Search', + '导 出' => 'Export', + '类型为空' => 'Type is required', + '手机号错误' => 'Mobile number format is invalid', + '手机号为空' => 'Mobile number is required', + '验证码为空' => 'Verification code is required', + '登录成功' => 'Login successful', + '短信验证失败' => 'SMS verification failed', + '授权编号为空' => 'Authorization code is required', + '无效账号' => 'Invalid account', + '解密失败' => 'Decryption failed', + '接口类型为空' => 'Interface type is required', + '登录手机错误' => 'Login mobile number is invalid', + '登录手机为空' => 'Login mobile number is required', + '拼图编号为空' => 'Slider verification ID is required', + '拼图位置为空' => 'Slider verification position is required', + '登录密码为空' => 'Login password is required', + '不支持登录' => 'Login is not supported', + '不支持密码' => 'Password login is not supported', + '拼图验证失败' => 'Slider verification failed', + '密码错误' => 'Incorrect password', + '短信验证为空' => 'SMS verification code is required', + '密码不能为空' => 'Password cannot be empty', + '账号不存在' => 'Account does not exist', + '重置成功' => 'Password reset successful', + '验证码错误' => 'Verification code is incorrect', + '注册成功' => 'Registration successful', + '无效通道' => 'Invalid channel', + '生成拼图成功' => 'Slider puzzle generated successfully', + '拼图验证为空' => 'Slider verification is required', + '拼图数值为空' => 'Slider verification value is required', + '验证结果' => 'Verification result', + '需要登录授权' => 'Login authorization required', + '终端已冻结' => 'Device account is frozen', + '请绑定账号' => 'Please bind an account', + '账号已冻结' => 'User account is frozen', + '获取资料' => 'Profile loaded successfully', + '无需修改' => 'No changes needed', + '修改成功' => 'Update successful', + '账号注销成功' => 'Account cancelled successfully', + '未完成注册' => 'Registration has not been completed', + '关联成功' => 'Linked successfully', + '验证失败' => 'Verification failed', + '获取网页签名' => 'Web signature retrieved successfully', + '接口未开通' => 'Interface is not enabled', + '凭证编码为空' => 'Credential code is required', + '授权换取成功' => 'Authorization exchanged successfully', + '处理失败,%s' => 'Processing failed: %s', + '解密向量为空' => 'Decryption IV is required', + '授权编码为空' => 'Authorization code is required', + '密文内容为空' => 'Encrypted payload is required', + '解密成功' => 'Decryption successful', + '绑定成功' => 'Binding successful', + '解析失败' => 'Parsing failed', + '用户编号为空' => 'User identifier is required', + '跳转链接为空' => 'Redirect path is required', + '生成小程序码' => 'Mini program QR code generated successfully', + '直播列表' => 'Live stream list', + '直播间号为空' => 'Live room ID is required', + '回放列表' => 'Replay list', + '微信网页授权失败' => 'WeChat OAuth failed', + "不支持虚拟用户登录!\n请 10 秒后刷新页面选择授权!" => "Virtual users are not supported.\nPlease refresh in 10 seconds and choose authorization again!", + + // 设备管理 + '账号接口配置' => 'Account Interface Configuration', + '账号配置' => 'Account Configuration', + '终端账号管理' => 'Device Account Management', + '终端类型' => 'Device Type', + '绑定手机' => 'Bound Mobile', + '用户姓名' => 'User Name', + '用户昵称' => 'User Nickname', + '关联账号' => 'Associated Account', + '使用状态' => 'Status', + '首次登录' => 'First Login', + '手机浏览器' => 'Mobile Browser', + '电脑浏览器' => 'Desktop Browser', + '微信小程序' => 'WeChat Mini Program', + '微信服务号' => 'WeChat Official Account', + '苹果APP应用' => 'iOS App', + '安卓APP应用' => 'Android App', + '请输入绑定手机' => 'Please enter bound mobile', + '请输入用户姓名' => 'Please enter user name', + '请输入用户昵称' => 'Please enter user nickname', + '请选择绑定时间' => 'Please select binding time', + '用户账号数据' => 'User Account Data', + + // 主账号管理 + '用户编号' => 'User Code', + '绑定邮箱' => 'Bound Email', + '绑定时间' => 'Binding Time', + '请输入用户编号' => 'Please enter user code', + '请输入绑定邮箱' => 'Please enter bound email', + + // 消息管理 + '手机短信管理' => 'SMS Message Management', + '短信配置' => 'SMS Configuration', + '消息编号' => 'Message Code', + '短信类型' => 'SMS Type', + '发送手机' => 'Send Mobile', + '业务场景' => 'Business Scene', + '短信内容' => 'SMS Content', + '返回结果' => 'Response Result', + '执行结果' => 'Execution Result', + '发送时间' => 'Send Time', + '失败' => 'Failed', + '成功' => 'Success', + '发送失败' => 'Send Failed', + '发送成功' => 'Send Success', + '用户登录验证' => 'User Login Verification', + '找回用户密码' => 'Recover User Password', + '用户注册绑定' => 'User Registration Binding', + '手机未注册' => 'Mobile number is not registered', + '请输入消息编号' => 'Please enter message code', + '请输入发送手机' => 'Please enter send mobile', + '请输入短信内容' => 'Please enter SMS content', + '请选择发送时间' => 'Please select send time', + + // 短信配置 + '服务区域' => 'Service Region', + '阿里云账号' => 'Aliyun Account', + '阿里云密钥' => 'Aliyun Secret Key', + '短信签名' => 'SMS Signature', + '短信模板编号' => 'SMS Template Code', + '请输入阿里云账号' => 'Please enter Aliyun account', + '请输入阿里云密钥' => 'Please enter Aliyun secret key', + '请输入短信签名' => 'Please enter SMS signature', + '请输入短信模板编号' => 'Please enter SMS template code', + '华北1(青岛)' => 'North China 1 (Qingdao)', + '华北2(北京)' => 'North China 2 (Beijing)', + '华北3(张家口)' => 'North China 3 (Zhangjiakou)', + '华北5(呼和浩特)' => 'North China 5 (Hohhot)', + '华北6(乌兰察布)' => 'North China 6 (Ulanqab)', + '华东1(杭州)' => 'East China 1 (Hangzhou)', + '华东2(上海)' => 'East China 2 (Shanghai)', + '华南1(深圳)' => 'South China 1 (Shenzhen)', + '西南1(成都)' => 'Southwest China 1 (Chengdu)', + '中国(香港)' => 'China (Hong Kong)', + '日本(东京)' => 'Japan (Tokyo)', + '新加坡' => 'Singapore', + '澳大利亚(悉尼)' => 'Australia (Sydney)', + '马来西亚(吉隆坡)' => 'Malaysia (Kuala Lumpur)', + '印度尼西亚(雅加达)' => 'Indonesia (Jakarta)', + '美国(弗吉尼亚)' => 'United States (Virginia)', + '美国(硅谷)' => 'United States (Silicon Valley)', + '英国(伦敦)' => 'United Kingdom (London)', + '德国(法兰克福)' => 'Germany (Frankfurt)', + '印度(孟买)' => 'India (Mumbai)', + '阿联酋(迪拜)' => 'United Arab Emirates (Dubai)', + '华东1 金融云' => 'East China 1 Finance Cloud', + '华东2 金融云' => 'East China 2 Finance Cloud', + '华南1 金融云' => 'South China 1 Finance Cloud', + '华北2 金融云' => 'North China 2 Finance Cloud', + + // 账号配置 + '认证有效时间' => 'Authentication Expire Time', + '登录自动注册' => 'Auto Register on Login', + '启用自动注册' => 'Enable Auto Registration', + '禁止自动注册' => 'Disable Auto Registration', + '默认昵称前缀' => 'Default Nickname Prefix', + '默认用户头像' => 'Default User Avatar', + '开放接口通道' => 'Open Interface Channels', + '用户' => 'User', + '设置为 0 表示永不过期,建议设置有效时间达到系统自动回收令牌。' => 'Set to 0 means never expires. It is recommended to set an expiration time for automatic token recycling.', + '启用自动登录时,通过验证码登录时账号不存在会自动创建!' => 'When auto login is enabled, accounts that do not exist will be automatically created when logging in with verification code!', + '用户绑定账号后会自动使用此前缀与手机号后4位拼接为新默认昵称。' => 'After user binds account, this prefix will be automatically combined with the last 4 digits of mobile number as new default nickname.', + '当用户未设置头像时,自动使用此头像设置的图片链接。' => 'When user has not set avatar, automatically use the image link set in this avatar.', + '请输入默认昵称前缀' => 'Please enter default nickname prefix', + '验证码已发送' => 'Verification code already sent', + '验证码发送成功' => 'Verification code sent successfully', + '未定义的业务' => 'Undefined business scene', + '业务场景未配置' => 'Business scene is not configured', + '授权不匹配' => 'Authorization does not match', + '登录已超时' => 'Login has expired', + '禁止强行关联' => 'Forced binding is not allowed', + '字段 %s 为空' => 'Field %s cannot be empty', + '更新资料失败' => 'Failed to update profile', + '终端账号异常' => 'Device account is invalid', + '绑定用户失败' => 'Failed to bind user', + '请重新登录' => 'Please log in again', + '无授权记录' => 'Authorization record not found', + '资料不能为空' => 'Profile data cannot be empty', + '资料保存失败' => 'Failed to save profile data', + '获取会话失败' => 'Failed to get session', + '配置保存成功' => 'Configuration saved successfully', + '配置保存失败' => 'Configuration save failed', + '修改配置成功' => 'Configuration updated successfully', +]); diff --git a/plugin/think-plugs-account/src/model/Abs.php b/plugin/think-plugs-account/src/model/Abs.php new file mode 100644 index 000000000..f341fc5e4 --- /dev/null +++ b/plugin/think-plugs-account/src/model/Abs.php @@ -0,0 +1,32 @@ +hasOne(PluginAccountBind::class, 'id', 'usid'); + $relation->with(['user']); + return $relation; + } +} diff --git a/plugin/think-plugs-account/src/model/PluginAccountBind.php b/plugin/think-plugs-account/src/model/PluginAccountBind.php new file mode 100644 index 000000000..fbabf82e0 --- /dev/null +++ b/plugin/think-plugs-account/src/model/PluginAccountBind.php @@ -0,0 +1,79 @@ +hasOne(PluginAccountUser::class, 'id', 'unid'); + } + + /** + * 关联授权数据. + */ + public function auths(): HasMany + { + return $this->hasMany(PluginAccountAuth::class, 'usid', 'id'); + } + + /** + * 增加通道名称显示. + */ + public function toArray(): array + { + $data = parent::toArray(); + if (isset($data['type'])) { + $data['type_name'] = Account::get($data['type'])['name'] ?? $data['type']; + } + return $data; + } +} diff --git a/plugin/think-plugs-account/src/model/PluginAccountMsms.php b/plugin/think-plugs-account/src/model/PluginAccountMsms.php new file mode 100644 index 000000000..dfb2c7075 --- /dev/null +++ b/plugin/think-plugs-account/src/model/PluginAccountMsms.php @@ -0,0 +1,55 @@ +hasMany(PluginAccountBind::class, 'unid', 'id'); + } +} diff --git a/plugin/think-plugs-account/src/service/Account.php b/plugin/think-plugs-account/src/service/Account.php new file mode 100644 index 000000000..5397cbd74 --- /dev/null +++ b/plugin/think-plugs-account/src/service/Account.php @@ -0,0 +1,423 @@ + ['name' => '手机浏览器', 'field' => 'phone', 'status' => 1], + self::WEB => ['name' => '电脑浏览器', 'field' => 'phone', 'status' => 1], + self::WXAPP => ['name' => '微信小程序', 'field' => 'openid', 'status' => 1], + self::WECHAT => ['name' => '微信服务号', 'field' => 'openid', 'status' => 1], + self::IOSAPP => ['name' => '苹果APP应用', 'field' => 'phone', 'status' => 1], + self::ANDROID => ['name' => '安卓APP应用', 'field' => 'phone', 'status' => 1], + ]; + + /** + * 创建账号实例. + * @param string $type 通道编号 + * @param array|string $token 令牌或条件 + * @param bool $isjwt 是否JWT模式 + * @throws Exception + */ + public static function mk(string $type, $token = '', bool $isjwt = true): AccountInterface + { + $jwtData = []; + if ($isjwt && is_string($token) && strlen($token) > 32) { + $jwtData = JwtToken::verify($token); + self::verifyJwtPayload($jwtData); + self::verifyTokenSession($jwtData); + [$type, $token] = [$type ?: ($jwtData['type'] ?? ''), $jwtData['token'] ?? $token]; + if (($jwtData['type'] ?? '') !== $type) { + throw new Exception('授权不匹配'); + } + } + if (($field = self::field($type)) || is_array($token)) { + $vars = ['type' => $type, 'field' => $field]; + return app(AccountAccess::class, $vars, true)->init($token, $isjwt); + } + throw new Exception('登录已超时', 401); + } + + /** + * 动态增加通道. + * @return array[] + */ + public static function add(string $type, string $name, string $field = 'phone'): array + { + self::$types[$type] = ['name' => $name, 'field' => $field, 'status' => 1]; + return self::types(); + } + + /** + * 设置通道状态 + * @param string $type 通道编号 + * @param int $status 通道状态 + */ + public static function set(string $type, int $status): bool + { + if (isset(self::$types[$type])) { + self::$types[$type]['status'] = $status; + return true; + } + return false; + } + + /** + * 获取通道参数. + */ + public static function get(string $type): array + { + return self::$types[$type] ?? []; + } + + /** + * 获取全部通道. + * @param ?int $status 指定状态 + */ + public static function types(?int $status = null): array + { + try { + $all = []; + foreach (self::init() as $type => $item) { + $item['code'] = $type; + if (is_null($status) || $item['status'] === $status) { + $all[$type] = $item; + } + } + return $all; + } catch (\Exception $exception) { + return []; + } + } + + /** + * 保存用户通道状态 + * @return mixed + * @throws Exception + */ + public static function save() + { + self::$denys = []; + foreach (self::types() as $k => $v) { + if (empty($v['status'])) { + self::$denys[] = $k; + } + } + return sysdata(self::$cacheKey, self::$denys); + } + + /** + * 获取认证字段. + * @param string $type 通道编码 + */ + public static function field(string $type): string + { + $types = self::init(); + if (!empty($types[$type]['status'])) { + return $types[$type]['field'] ?? ''; + } + return ''; + } + + /** + * 接口授权有效时间及默认头像. + * @param null|int|string $expire 有效时间 + * @param null|string $headimg 默认头像 + * @throws Exception + */ + public static function expire($expire = null, ?string $headimg = null): int + { + $data = sysdata('plugin.account.access'); + if (!is_null($expire) || !is_null($headimg)) { + if (!is_null($expire)) { + $data['expire'] = $expire; + } + if (!is_null($headimg)) { + $data['headimg'] = $headimg; + } + $data = sysdata('plugin.account.access', $data); + } + return intval($data['expire'] ?? 0); + } + + /** + * 获取账号认证 Cookie 名称. + */ + public static function getTokenCookie(): string + { + return RequestTokenService::getAccountTokenCookie(); + } + + /** + * 解析当前请求中的账号令牌. + */ + public static function requestToken(?Request $request = null): string + { + $token = RequestTokenService::accountToken($request); + self::upgradeLegacyCookieToken($request); + return $token; + } + + /** + * 获取账号 JWT 类型。 + */ + public static function getTokenType(): string + { + return self::TOKEN_TYPE; + } + + /** + * 获取当前账号会话编号。 + */ + public static function currentSessionId(): string + { + return trim(strval(sysvar('plugin_account_user_session_id') ?: '')); + } + + /** + * 绑定账号缓存会话并返回会话编号。 + * @throws Exception + */ + public static function bindSession(?string $sessionId = null): string + { + $sessionId = trim(strval($sessionId ?: self::currentSessionId())); + if ($sessionId === '') { + $sessionId = CodeToolkit::uuid(); + } + + $scope = self::sessionScope($sessionId); + if (CacheSession::exists($scope)) { + CacheSession::touch(self::expire(), $scope); + } else { + CacheSession::put([], self::expire(), $scope); + } + + sysvar('plugin_account_user_session_id', $sessionId); + return $sessionId; + } + + /** + * 构建账号 JWT。 + * @throws Exception + */ + public static function buildJwtToken(string $type, string $token, ?string $sessionId = null): string + { + return JwtToken::token([ + 'typ' => self::TOKEN_TYPE, + 'sid' => self::bindSession($sessionId), + 'jti' => CodeToolkit::uuid(), + 'type' => $type, + 'token' => $token, + ]); + } + + /** + * 销毁当前账号缓存会话。 + * @throws Exception + */ + public static function destroySession(?string $sessionId = null): void + { + $sessionId = trim(strval($sessionId ?: self::currentSessionId())); + if ($sessionId !== '') { + CacheSession::destroy(self::sessionScope($sessionId)); + } + sysvar('plugin_account_user_session_id', ''); + } + + /** + * 同步账号认证 Cookie. + */ + public static function syncTokenCookie(string $token): string + { + $token = RequestTokenService::normalizeToken($token); + if ($token === '') { + self::forgetTokenCookie(); + return ''; + } + + cookie(self::getTokenCookie(), RequestTokenService::encodeCookieToken($token), ['expire' => self::expire()]); + return $token; + } + + /** + * 清理账号认证 Cookie. + */ + public static function forgetTokenCookie(): void + { + cookie(static::getTokenCookie(), null); + } + + /** + * 解析请求令牌. + * @throws Exception + */ + public static function token(string $token = '', ?string &$type = null): AccountInterface + { + $data = JwtToken::verify($token); + self::verifyJwtPayload($data); + self::verifyTokenSession($data); + return self::mk($type = $data['type'] ?? '-', $data['token'] ?? '-'); + } + + /** + * 账号配置参数设置与读取. + * @param null|array|string $data + * @return null|mixed|void + * @throws Exception + */ + public static function config($data = null) + { + if (is_null($data)) { + return sysdata('plugin.account.access'); + } + if (is_array($data)) { + return sysdata('plugin.account.access', $data); + } + if (is_string($data)) { + return sysdata('plugin.account.access')[$data] ?? null; + } + return null; + } + + /** + * 是否自动注册. + * @throws Exception + */ + public static function enableAutoReigster(): bool + { + return empty(self::config('disRegister')); + } + + /** + * 获取默认头像. + * @throws Exception + */ + public static function headimg(?string $headimg = null): string + { + $data = sysdata('plugin.account.access'); + if (!is_null($headimg)) { + $data['headimg'] = $headimg; + sysdata('plugin.account.access', $data); + } + return $data['headimg'] ?? 'https://thinkadmin.top/static/img/logo.png'; + } + + /** + * 初始化数据状态 + * @return array[] + */ + private static function init(): array + { + if (is_null(self::$denys)) { + try { + self::$denys = sysdata(self::$cacheKey); + foreach (self::$types as $type => &$item) { + $item['status'] = intval(!in_array($type, self::$denys)); + } + } catch (\Exception $exception) { + } + } + return self::$types; + } + + /** + * 获取账号会话作用域。 + */ + private static function sessionScope(string $sessionId): string + { + return 'sid:' . trim($sessionId); + } + + /** + * 校验账号 JWT 绑定的缓存会话。 + * @throws Exception + */ + private static function verifyTokenSession(array $data): void + { + $sessionId = trim(strval($data['sid'] ?? '')); + if ($sessionId === '') { + return; + } + + $scope = self::sessionScope($sessionId); + if (!CacheSession::exists($scope)) { + throw new Exception('登录已超时', 401); + } + + CacheSession::touch(self::expire(), $scope); + sysvar('plugin_account_user_session_id', $sessionId); + } + + /** + * 校验账号 JWT 载荷类型。 + * @throws Exception + */ + private static function upgradeLegacyCookieToken(?Request $request = null): void + { + $request = $request ?: Library::$sapp->request; + $rawToken = strval($request->cookie(self::getTokenCookie(), '')); + $decodedToken = RequestTokenService::capture($request)->accountCookieToken(); + if (RequestTokenService::shouldUpgradeCookieToken($rawToken, $decodedToken)) { + self::syncTokenCookie($decodedToken); + } + } + + private static function verifyJwtPayload(array $data): void + { + if (strval($data['typ'] ?? '') !== self::TOKEN_TYPE) { + throw new Exception('登录已超时', 401); + } + } +} diff --git a/plugin/think-plugs-account/src/service/Message.php b/plugin/think-plugs-account/src/service/Message.php new file mode 100644 index 000000000..e0554b6c8 --- /dev/null +++ b/plugin/think-plugs-account/src/service/Message.php @@ -0,0 +1,178 @@ + '用户登录验证', + self::tForget => '找回用户密码', + self::tRegister => '用户注册绑定', + ]; + + /** + * 获取业务场景标签. + */ + public static function sceneLabel(string $code): string + { + return lang(static::$scenes[$code] ?? $code); + } + + /** + * 获取业务场景选项. + * + * @return string[] + */ + public static function sceneOptions(): array + { + $items = []; + foreach (static::$scenes as $code => $label) { + $items[$code] = lang($label); + } + return $items; + } + + /** + * 静态方法调用. + * @return mixed + * @throws Exception + */ + public static function __callStatic(string $name, array $arguments) + { + return static::mk()->{$name}(...$arguments); + } + + /** + * 创建短信通道. + * @throws Exception + */ + public static function mk(array $config = [], ?string $driver = null): MessageInterface + { + if (!is_null($driver) && !isset(class_implements($driver)[MessageInterface::class])) { + throw new Exception("Sms driver [{$driver}] Not implements MessageInterface."); + } + return app($driver ?: Alisms::class)->init($config); + } + + /** + * 构建标准结果. + * @param array $data + * @return array + */ + private static function createResult(int $status, string $info, array $data = []): array + { + return ['code' => $status, 'info' => $info, 'data' => $data]; + } + + /** + * 发送短信验证码 + * @param string $phone 手机号码 + * @param int $wait 等待时间 + * @param string $scene 业务场景 + * @return array + */ + public static function sendVerifyCode(string $phone, int $wait = 120, string $scene = self::tLogin): array + { + try { + $ckey = self::genCacheKey($phone, $scene); + $cache = Library::$sapp->cache->get($ckey, []); + // 检查是否已经发送 + if (is_array($cache) && isset($cache['time']) && $cache['time'] > time() - $wait) { + $dtime = ($cache['time'] + $wait < time()) ? 0 : ($wait - time() + $cache['time']); + return self::createResult(200, '验证码已发送', ['time' => $dtime]); + } + // 生成新的验证码 + [$code, $time] = [rand(100000, 999999), time()]; + Library::$sapp->cache->set($ckey, ['code' => $code, 'time' => $time], 600); + // 尝试发送短信内容 + self::mk()->verify($scene, $phone, ['code' => $code]); + return self::createResult(200, '验证码发送成功', ['time' => ($time + $wait < time()) ? 0 : ($wait - time() + $time)]); + } catch (\Exception $ex) { + trace_file($ex); + isset($ckey) && Library::$sapp->cache->delete($ckey); + return self::createResult(500, $ex->getMessage()); + } + } + + /** + * 检查短信验证码 + * @param string $vcode 验证码 + * @param string $phone 手机号码 + * @param string $scene 业务场景 + * @throws Exception + */ + public static function checkVerifyCode(string $vcode, string $phone, string $scene = self::tLogin): bool + { + if (stripos(Library::$sapp->request->domain(), '.thinkadmin.top') !== false) { + if ($vcode === '123456') { + return true; + } + } + $cache = Library::$sapp->cache->get(self::genCacheKey($phone, $scene), []); + return is_array($cache) && isset($cache['code']) && $cache['code'] == $vcode; + } + + /** + * 清理短信验证码 + */ + public static function clearVerifyCode(string $phone, string $scene = self::tLogin): bool + { + try { + return Library::$sapp->cache->delete(self::genCacheKey($phone, $scene)); + } catch (\Exception $exception) { + return false; + } + } + + /** + * 生成验证码缓存名. + * @param string $phone 手机号码 + * @param string $scene 业务场景 + * @throws Exception + */ + private static function genCacheKey(string $phone, string $scene = self::tLogin): string + { + if (isset(array_change_key_case(static::$scenes)[strtolower($scene)])) { + return md5(strtolower("sms-{$scene}-{$phone}")); + } + throw new Exception('未定义的业务'); + } +} diff --git a/plugin/think-plugs-account/src/service/WxappService.php b/plugin/think-plugs-account/src/service/WxappService.php new file mode 100644 index 000000000..6199f4de3 --- /dev/null +++ b/plugin/think-plugs-account/src/service/WxappService.php @@ -0,0 +1,147 @@ +getConfig()['appid'] ?? ''); + } + + /** + * 获取小程序会话信息并缓存. + * @throws Exception + */ + public function getSession(string $code): array + { + $cacheKey = $this->buildSessionCacheKey($code); + $cached = $this->app->cache->get($cacheKey, []); + if (isset($cached['openid'], $cached['session_key'])) { + return $cached; + } + + $result = WechatService::WeMiniCrypt()->session($code); + if (!isset($result['openid'], $result['session_key'])) { + throw new Exception(strval($result['errmsg'] ?? '获取会话失败')); + } + + $this->app->cache->set($cacheKey, $result, $this->resolveSessionExpire($result)); + return $result; + } + + /** + * 解密小程序数据. + * @throws Exception + */ + public function decode(string $iv, string $sessionKey, string $encrypted): array + { + $result = WechatService::WeMiniCrypt()->decode($iv, $sessionKey, $encrypted); + if (!is_array($result)) { + throw new Exception('解析失败'); + } + + return $result; + } + + /** + * 获取用户手机号. + * @throws Exception + */ + public function getPhoneNumber(string $code): array + { + $result = WechatService::WeMiniCrypt()->getPhoneNumber($code); + if (!is_array($result)) { + throw new Exception('解析失败'); + } + + return $result; + } + + /** + * 生成小程序码. + */ + public function createMiniPath(string $path, int $size = 430): string + { + return WechatService::WeMiniQrcode()->createMiniPath($path, $size); + } + + /** + * 获取直播列表. + */ + public function getLiveList(int $start = 0, int $limit = 10): array + { + $result = WechatService::WeMiniLive()->getLiveList($start, $limit); + return is_array($result) ? $result : []; + } + + /** + * 获取直播回放信息. + */ + public function getLiveInfo(array $data): array + { + $result = WechatService::WeMiniLive()->getLiveInfo($data); + return is_array($result) ? $result : []; + } + + /** + * 构建 Session 缓存键. + * @throws Exception + */ + private function buildSessionCacheKey(string $code): string + { + return sprintf('%s:%s:%s', self::SESSION_CACHE_PREFIX, $this->getAppid() ?: '-', md5($code)); + } + + /** + * 解析 Session 缓存时长. + */ + private function resolveSessionExpire(array $result): int + { + $expire = intval($result['expires_in'] ?? self::SESSION_EXPIRE); + return $expire > 0 ? $expire : self::SESSION_EXPIRE; + } +} diff --git a/plugin/think-plugs-account/src/service/contract/AccountAccess.php b/plugin/think-plugs-account/src/service/contract/AccountAccess.php new file mode 100644 index 000000000..6087a33ca --- /dev/null +++ b/plugin/think-plugs-account/src/service/contract/AccountAccess.php @@ -0,0 +1,523 @@ +app = $app; + $this->type = $type; + $this->field = $field; + $this->expire = Account::expire(); + } + + /** + * 初始化通道. + * @param array|string $token 令牌或条件 + * @param bool $isjwt 是否返回令牌 + * @throws Exception + * @throws DbException + */ + public function init($token = '', bool $isjwt = true): AccountInterface + { + $this->isjwt = $isjwt; + $this->auth = PluginAccountAuth::mk(); + $this->bind = PluginAccountBind::mk(); + $this->user = PluginAccountUser::mk(); + if (is_string($token)) { + $map = ['type' => $this->type, 'token' => $token]; + $this->auth = PluginAccountAuth::mk()->where($map)->findOrEmpty(); + $this->bind = $this->auth->client()->findOrEmpty(); + $this->user = $this->bind->user()->findOrEmpty(); + } elseif (is_array($token)) { + // 返向查询终端账号 + $map = []; + if ($this->type) { + $map['type'] = $this->type; + } + $this->bind = PluginAccountBind::mk()->where($map)->where($token)->findOrEmpty(); + $this->user = $this->bind->user()->findOrEmpty(); + if ($this->bind->isExists()) { + if (empty($this->type)) { + $this->type = $this->bind->getAttr('type'); + } + if ($this->auth->isEmpty()) { + $this->token(false); + } + } + } + return $this; + } + + /** + * 设置子账号资料. + * @param array $data 用户资料 + * @param bool $rejwt 返回令牌 + * @throws Exception + * @throws DbException + */ + public function set(array $data = [], bool $rejwt = false): array + { + // 如果传入授权验证字段 + if (isset($data[$this->field])) { + if ($this->bind->isExists()) { + if ($data[$this->field] !== $this->bind->getAttr($this->field)) { + throw new Exception('禁止强行关联'); + } + } else { + $map = [$this->field => $data[$this->field]]; + if ($this->type) { + $map['type'] = $this->type; + } + $this->bind = PluginAccountBind::mk()->where($map)->findOrEmpty(); + } + } elseif ($this->bind->isEmpty()) { + throw new Exception(sprintf(lang('字段 %s 为空'), $this->field)); + } + $this->bind = $this->save(array_merge($data, ['type' => $this->type])); + if ($this->bind->isEmpty()) { + throw new Exception('更新资料失败'); + } + // 刷新更新用户模型 + $this->user = $this->bind->user()->findOrEmpty(); + return $this->token()->get($rejwt); + } + + /** + * 获取用户数据. + * @param bool $rejwt 返回令牌 + * @param bool $refresh 刷新数据 + */ + public function get(bool $rejwt = false, bool $refresh = false): array + { + if ($refresh) { + $this->bind->isExists() && $this->bind->refresh(); + $this->user->isExists() && $this->user->refresh(); + } + $data = $this->bind->hidden(['sort', 'password'], true)->toArray(); + if ($this->bind->isExists()) { + if ($this->user->isEmpty()) { + $this->user = $this->bind->user()->findOrEmpty(); + } + $data['user'] = $this->user->hidden(['sort', 'password'], true)->toArray(); + if ($rejwt) { + $data['token'] = $this->isjwt ? Account::buildJwtToken( + strval($this->auth->getAttr('type')), + strval($this->auth->getAttr('token')) + ) : $this->auth->getAttr('token'); + } + } + return $data; + } + + /** + * 验证终端密码 + * @param string $pass 待验证密码 + * @throws Exception + */ + public function pwdVerify(string $pass): bool + { + if ($this->verifyPasswordHash(strval($this->user->getAttr('password')), $pass)) { + return (bool)$this->expire(); + } + return $this->verifyPasswordHash(strval($this->bind->getAttr('password')), $pass) && $this->expire(); + } + + /** + * 修改终端密码 + * @param string $pass 待修改密码 + * @param bool $event 触发事件 + */ + public function pwdModify(string $pass, bool $event = true): bool + { + if ($this->bind->isEmpty()) { + return false; + } + $data = ['password' => $this->hashPassword($pass)]; + $this->user->isExists() && $this->user->save($data); + if (!$this->bind->save($data)) { + return false; + } + if ($event) { + $this->app->event->trigger('PluginAccountChangePassword', [ + 'unid' => $this->getUnid(), 'pass' => $pass, + ]); + } + return true; + } + + /** + * 绑定主账号. + * @param array $map 主账号条件 + * @param array $data 主账号资料 + * @throws Exception + */ + public function bind(array $map, array $data = []): array + { + if ($this->bind->isEmpty()) { + throw new Exception('终端账号异常'); + } + $this->user = PluginAccountUser::mk()->where($map)->findOrEmpty(); + if (!empty($data['extra'])) { + $this->user->setAttr('extra', array_merge($this->user->getAttr('extra'), $data['extra'])); + } + unset($data['id'], $data['code'], $data['extra']); + // 生成新的用户编号 + if ($this->user->isEmpty()) { + do { + $check = ['code' => $data['code'] = $this->userCode()]; + } while (PluginAccountUser::mk()->master()->where($check)->findOrEmpty()->isExists()); + } + // 自动绑定默认头像 + if (empty($data['headimg']) && $this->user->isEmpty() || empty($this->user->getAttr('headimg'))) { + if (empty($data['headimg'] = $this->bind->getAttr('headimg'))) { + $data['headimg'] = Account::headimg(); + } + } + // 自动生成用户昵称 + if (empty($data['nickname']) && empty($this->user->getAttr('nickname'))) { + if (empty($data['nickname'] = $this->bind->getAttr('nickname'))) { + $prefix = Account::config('userPrefix') ?: (Account::get($this->type)['name'] ?? $this->type); + if ($phone = $data['phone'] ?? $this->user->getAttr('phone')) { + $data['nickname'] = $prefix . substr($phone, -4); + } else { + $data['nickname'] = "{$prefix}{$this->bind->getAttr('id')}"; + } + } + } + // 同步用户登录密码 + if (!empty($this->bind->getAttr('password'))) { + $data['password'] = $this->bind->getAttr('password'); + } + // 保存更新用户数据 + if ($this->user->save($data + $map)) { + $this->bind->save(['unid' => $this->user['id']]); + $this->app->event->trigger('PluginAccountBind', [ + 'type' => $this->type, + 'unid' => intval($this->user->getAttr('id')), + 'usid' => intval($this->bind->getAttr('id')), + ]); + return $this->get(); + } + throw new Exception('绑定用户失败'); + } + + /** + * 解绑主账号. + * @throws Exception + */ + public function unBind(): array + { + if ($this->bind->isEmpty()) { + throw new Exception('终端账号异常'); + } + if (($unid = $this->bind->getAttr('unid')) > 0) { + $this->bind->save(['unid' => 0]); + $this->app->event->trigger('PluginAccountUnbind', [ + 'type' => $this->type, + 'unid' => intval($unid), + 'usid' => intval($this->bind->getAttr('id')), + ]); + } + $this->bind->refresh(); + $this->user = $this->bind->user()->findOrEmpty(); + return $this->get(); + } + + /** + * 判断绑定主账号. + */ + public function isBind(): bool + { + return $this->user->isExists(); + } + + /** + * 判断是否空账号. + */ + public function isNull(): bool + { + return $this->bind->isEmpty(); + } + + /** + * 获取关联终端. + */ + public function allBind(): array + { + try { + if ($this->isNull()) { + return []; + } + if ($this->isBind() && ($unid = $this->bind->getAttr('unid'))) { + $map = ['unid' => $unid]; + return PluginAccountBind::mk()->where($map)->select()->toArray(); + } + return [$this->bind->refresh()->toArray()]; + } catch (\Exception $exception) { + return []; + } + } + + /** + * 解除终端关联. + * @param int $usid 终端编号 + * @throws DbException + */ + public function delBind(int $usid): array + { + if ($this->isBind() && ($unid = $this->bind->getAttr('unid'))) { + $map = ['id' => $usid, 'unid' => $unid]; + PluginAccountBind::mk()->where($map)->update(['unid' => 0]); + } + return $this->allBind(); + } + + /** + * 刷新账号序号. + */ + public function recode(): array + { + if ($this->bind->isEmpty()) { + return $this->get(); + } + if ($this->user->isExists()) { + do { + $check = ['code' => $this->userCode()]; + } while (PluginAccountUser::mk()->master()->where($check)->findOrEmpty()->isExists()); + $this->user->save($check); + } + return $this->get(); + } + + /** + * 检查是否有效. + * @throws Exception + */ + public function check(): array + { + if ($this->bind->isEmpty()) { + throw new Exception('请重新登录', AuthResponse::STATUS_UNAUTHORIZED); + } + if ($this->expire > 0 && $this->auth->getAttr('time') < time()) { + throw new Exception('登录已超时', AuthResponse::STATUS_UNAUTHORIZED); + } + return static::expire()->get(); + } + + /** + * 获取用户模型. + */ + public function user(): PluginAccountUser + { + return $this->user->hidden(['sort', 'password'], true); + } + + /** + * 获取用户编号. + */ + public function getCode(): string + { + return $this->user->getAttr('code') ?: ''; + } + + /** + * 获取终端类型. + */ + public function getType(): string + { + return $this->bind->getAttr('type') ?: ''; + } + + /** + * 获取用户编号. + */ + public function getUnid(): int + { + return intval($this->bind->getAttr('unid')); + } + + /** + * 获取终端编号. + */ + public function getUsid(): int + { + return intval($this->bind->getAttr('id')); + } + + /** + * 生成授权令牌. + * @throws Exception + * @throws DbException + */ + public function token(bool $expire = true): AccountInterface + { + // 百分之一概率清理令牌 + if (mt_rand(1, 1000) < 10) { + PluginAccountAuth::mk()->whereBetween('time', [1, time()])->delete(); + } + $usid = $this->bind->getAttr('id'); + // 查询该通道历史授权记录 + if ($this->auth->isEmpty()) { + $where = ['usid' => $usid, 'type' => $this->type]; + $this->auth = PluginAccountAuth::mk()->where($where)->findOrEmpty(); + } + // 生成新令牌数据 + if ($this->auth->isEmpty()) { + do { + $check = ['type' => $this->type, 'token' => bin2hex(random_bytes(16))]; + } while (PluginAccountAuth::mk()->master()->where($check)->findOrEmpty()->isExists()); + $time = $this->expire > 0 ? $this->expire + time() : 0; + $this->auth->save($check + ['usid' => $usid, 'time' => $time]); + } + return $expire ? $this->expire() : $this; + } + + /** + * 延期令牌时间. + * @throws Exception + */ + public function expire(): AccountInterface + { + if ($this->auth->isEmpty()) { + throw new Exception('无授权记录'); + } + $this->auth->save(['time' => $this->expire > 0 ? $this->expire + time() : 0]); + return $this; + } + + /** + * 更新用户资料. + * @throws Exception + */ + private function save(array $data): PluginAccountBind + { + if (empty($data)) { + throw new Exception('资料不能为空'); + } + $data['extra'] = array_merge($this->bind->getAttr('extra'), $data['extra'] ?? []); + // 写入默认头像内容 + if (empty($data['headimg']) && empty($this->bind->getAttr('headimg'))) { + $data['headimg'] = Account::headimg(); + } + // 自动生成账号昵称 + if (empty($data['nickname']) && empty($this->bind->getAttr('nickname'))) { + $name = Account::get($this->type)['name'] ?? $this->type; + $data['nickname'] = "{$name}{$this->bind->getAttr('id')}"; + } + // 更新写入终端账号 + if ($this->bind->save($data) && $this->bind->isExists()) { + return $this->bind->refresh(); + } + throw new Exception('资料保存失败'); + } + + /** + * 生成用户编号. + */ + private function userCode(): string + { + return CodeToolkit::uniqidNumber(12, 'U'); + } + + private function hashPassword(string $pass): string + { + return password_hash($pass, PASSWORD_DEFAULT); + } + + private function verifyPasswordHash(string $hash, string $pass): bool + { + $hash = trim($hash); + if ($hash === '') { + return false; + } + return password_verify($pass, $hash); + } +} diff --git a/plugin/think-plugs-account/src/service/contract/AccountInterface.php b/plugin/think-plugs-account/src/service/contract/AccountInterface.php new file mode 100644 index 000000000..98daad5c0 --- /dev/null +++ b/plugin/think-plugs-account/src/service/contract/AccountInterface.php @@ -0,0 +1,142 @@ +scenes); + if (empty($scenes) || empty($scenes[strtolower($scene)])) { + throw new Exception('业务场景未配置'); + } + $result = $this->send($scenes[strtolower($scene)], $phone, $params, $options); + PluginAccountMsms::mk()->save([ + 'unid' => intval(sysvar('plugin_account_user_unid')), + 'usid' => intval(sysvar('plugin_account_user_usid')), + 'type' => class_basename(static::class), + 'smsid' => $result['smsid'] ?? '', + 'scene' => $scene, + 'phone' => $phone, + 'status' => 1, + 'result' => json_encode($result['result'], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE), + 'params' => json_encode($result['params'], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE), + ]); + return $result; + } +} diff --git a/plugin/think-plugs-account/src/service/message/Alisms.php b/plugin/think-plugs-account/src/service/message/Alisms.php new file mode 100644 index 000000000..fc69cd7e0 --- /dev/null +++ b/plugin/think-plugs-account/src/service/message/Alisms.php @@ -0,0 +1,152 @@ + ['host' => 'dysmsapi.aliyuncs.com', 'name' => '华北1(青岛)'], + 'cn-beijing' => ['host' => 'dysmsapi.aliyuncs.com', 'name' => '华北2(北京)'], + 'cn-zhangjiakou' => ['host' => 'dysmsapi.aliyuncs.com', 'name' => '华北3(张家口)'], + 'cn-huhehaote' => ['host' => 'dysmsapi.aliyuncs.com', 'name' => '华北5(呼和浩特)'], + 'cn-wulanchabu' => ['host' => 'dysmsapi.aliyuncs.com', 'name' => '华北6(乌兰察布)'], + 'cn-hangzhou' => ['host' => 'dysmsapi.aliyuncs.com', 'name' => '华东1(杭州)'], + 'cn-shanghai' => ['host' => 'dysmsapi.aliyuncs.com', 'name' => '华东2(上海)'], + 'cn-shenzhen' => ['host' => 'dysmsapi.aliyuncs.com', 'name' => '华南1(深圳)'], + 'cn-chengdu' => ['host' => 'dysmsapi.aliyuncs.com', 'name' => '西南1(成都)'], + 'cn-hongkong' => ['host' => 'dysmsapi.aliyuncs.com', 'name' => '中国(香港)'], + 'ap-northeast-1' => ['host' => 'dysmsapi.ap-southeast-1.aliyuncs.com', 'name' => '日本(东京)'], + 'ap-southeast-1' => ['host' => 'dysmsapi.ap-southeast-1.aliyuncs.com', 'name' => '新加坡'], + 'ap-southeast-2' => ['host' => 'dysmsapi.ap-southeast-1.aliyuncs.com', 'name' => '澳大利亚(悉尼)'], + 'ap-southeast-3' => ['host' => 'dysmsapi.ap-southeast-1.aliyuncs.com', 'name' => '马来西亚(吉隆坡)'], + 'ap-southeast-5' => ['host' => 'dysmsapi.ap-southeast-1.aliyuncs.com', 'name' => '印度尼西亚(雅加达)'], + 'us-east-1' => ['host' => 'dysmsapi.ap-southeast-1.aliyuncs.com', 'name' => '美国(弗吉尼亚)'], + 'us-west-1' => ['host' => 'dysmsapi.ap-southeast-1.aliyuncs.com', 'name' => '美国(硅谷)'], + 'eu-west-1' => ['host' => 'dysmsapi.ap-southeast-1.aliyuncs.com', 'name' => '英国(伦敦)'], + 'eu-central-1' => ['host' => 'dysmsapi.ap-southeast-1.aliyuncs.com', 'name' => '德国(法兰克福)'], + 'ap-south-1' => ['host' => 'dysmsapi.ap-southeast-1.aliyuncs.com', 'name' => '印度(孟买)'], + 'me-east-1' => ['host' => 'dysmsapi.ap-southeast-1.aliyuncs.com', 'name' => '阿联酋(迪拜)'], + 'cn-hangzhou-finance' => ['host' => 'dysmsapi.aliyuncs.com', 'name' => '华东1 金融云'], + 'cn-shanghai-finance-1' => ['host' => 'dysmsapi.aliyuncs.com', 'name' => '华东2 金融云'], + 'cn-shenzhen-finance-1' => ['host' => 'dysmsapi.aliyuncs.com', 'name' => '华南1 金融云'], + 'cn-beijing-finance-1' => ['host' => 'dysmsapi.aliyuncs.com', 'name' => '华北2 金融云'], + ]; + + private $keyid; + + private $secret; + + private $region; + + private $getway; + + private $signtx; + + /** + * 初始化短信通道. + * @return $this + * @throws Exception + */ + public function init(array $config = []): MessageInterface + { + $options = array_merge(sysdata('plugin.account.smscfg'), $config); + $this->keyid = $options['alisms_keyid'] ?? ''; + $this->secret = $options['alisms_secret'] ?? ''; + $this->signtx = $options['alisms_signtx'] ?? ''; + $this->region = $options['alisms_region'] ?? 'cn-shanghai'; + $this->scenes = $options['alisms_scenes'] ?? []; + $this->getway = self::$regions[$this->region]['host'] ?? 'dysmsapi.aliyuncs.com'; + return $this; + } + + /** + * 发送短信内容. + * @param string $code 短信模板CODE + * @param string $phone 接收手机号码 + * @param array $params 短信模板变量 + * @param array $options 其他配置参数 + * @throws Exception + */ + public function send(string $code, string $phone, array $params = [], array $options = []): array + { + $result = $this->request($params = array_merge([ + 'SignName' => $this->signtx, + 'PhoneNumbers' => $phone, + 'TemplateCode' => $code, + 'TemplateParam' => json_encode((object)$params), + ], $options)); + return ['smsid' => $result['BizId'], 'params' => $params, 'result' => $result]; + } + + /** + * 生成接口请求 TOKEN. + * @throws Exception + */ + protected function request(array $params = [], string $action = 'SendSms', string $method = 'POST'): array + { + date_default_timezone_set('UTC'); + $querys = array_merge([ + 'AccessKeyId' => $this->keyid, + 'Action' => $action, + 'Format' => 'JSON', + 'RegionId' => $this->region, + 'SignatureMethod' => 'HMAC-SHA1', + 'SignatureNonce' => CodeToolkit::uuid(), + 'SignatureVersion' => '1.0', + 'Timestamp' => date('Y-m-d\TH:i:s\Z'), + 'Version' => '2017-05-25', + ], $params); + $result = HttpClient::request($method, "https://{$this->getway}", [ + 'data' => $params, 'query' => ['Signature' => $this->buildSign($method, $querys)] + $querys, + ]); + if (is_string($result) && is_array($json = json_decode($result, true))) { + if (isset($json['Code']) && $json['Code'] === 'OK') { + return $json; + } + throw new Exception($json['Message'] ?? $result, 500, $json); + } + throw new Exception('接口调用失败!' . var_export($result, true), 500); + } + + /** + * 生成数据签名. + */ + private function buildSign(string $method, array $querys): string + { + [$vars] = [[], ksort($querys)]; + foreach ($querys as $k => $v) { + $vars[] = urlencode($k) . '=' . urlencode($v); + } + return base64_encode(hash_hmac('sha1', "{$method}&%2F&" . urlencode(join('&', $vars)), "{$this->secret}&", true)); + } +} diff --git a/plugin/think-plugs-account/src/view/device/index.html b/plugin/think-plugs-account/src/view/device/index.html new file mode 100644 index 000000000..07ee32251 --- /dev/null +++ b/plugin/think-plugs-account/src/view/device/index.html @@ -0,0 +1,80 @@ +{extend name='table'} + +{block name="button"} + + + +{/block} + +{block name="content"} +
    +
      + {foreach ['index'=>lang('用户管理'),'recycle'=>lang('回 收 站')] as $k=>$v}{if isset($type) and $type eq $k} +
    • {$v}
    • + {else} +
    • {$v}
    • + {/if}{/foreach} +
    +
    + {include file='device/index_search'} +
    +
    +
    + + + + + + + + + + +{/block} diff --git a/plugin/think-plugs-account/src/view/device/index_search.html b/plugin/think-plugs-account/src/view/device/index_search.html new file mode 100644 index 000000000..f8687f307 --- /dev/null +++ b/plugin/think-plugs-account/src/view/device/index_search.html @@ -0,0 +1,94 @@ + + + diff --git a/plugin/think-plugs-account/src/view/device/types.html b/plugin/think-plugs-account/src/view/device/types.html new file mode 100644 index 000000000..ca26b4682 --- /dev/null +++ b/plugin/think-plugs-account/src/view/device/types.html @@ -0,0 +1,22 @@ +
    +
    + + {foreach $types as $k=>$v} + + {/foreach} +
    + +
    + {notempty name='vo.id'}{/notempty} + +
    + + +
    +
    diff --git a/plugin/think-plugs-account/src/view/master/index.html b/plugin/think-plugs-account/src/view/master/index.html new file mode 100644 index 000000000..c190f841d --- /dev/null +++ b/plugin/think-plugs-account/src/view/master/index.html @@ -0,0 +1,70 @@ +{extend name='table'} + +{block name="content"} +
    +
      + {foreach ['index'=>lang('用户管理'),'recycle'=>lang('回 收 站')] as $k=>$v}{if isset($type) and $type eq $k} +
    • {$v}
    • + {else} +
    • {$v}
    • + {/if}{/foreach} +
    +
    + {include file='master/index_search'} +
    +
    +
    + + + + + + + + + + +{/block} diff --git a/plugin/think-plugs-account/src/view/master/index_search.html b/plugin/think-plugs-account/src/view/master/index_search.html new file mode 100644 index 000000000..8e8195848 --- /dev/null +++ b/plugin/think-plugs-account/src/view/master/index_search.html @@ -0,0 +1,71 @@ + + + diff --git a/plugin/think-plugs-account/src/view/message/index.html b/plugin/think-plugs-account/src/view/message/index.html new file mode 100644 index 000000000..4d6ec3e3d --- /dev/null +++ b/plugin/think-plugs-account/src/view/message/index.html @@ -0,0 +1,43 @@ +{extend name='table'} + +{block name="button"} + +
    {:lang('短信配置')} + +{/block} + +{block name="content"} +
    + {include file='message/index_search'} + +
    + +
    +{/block} diff --git a/plugin/think-plugs-account/src/view/message/index_search.html b/plugin/think-plugs-account/src/view/message/index_search.html new file mode 100644 index 000000000..79feb676c --- /dev/null +++ b/plugin/think-plugs-account/src/view/message/index_search.html @@ -0,0 +1,68 @@ +
    + {:lang('条件搜索')} + +
    diff --git a/app/wechat/view/table.html b/plugin/think-plugs-account/src/view/table.html similarity index 87% rename from app/wechat/view/table.html rename to plugin/think-plugs-account/src/view/table.html index 83fdb3566..a9fb98344 100644 --- a/app/wechat/view/table.html +++ b/plugin/think-plugs-account/src/view/table.html @@ -3,7 +3,7 @@ {block name='header'} {notempty name='title'}
    - {$title|lang} + {$title|lang}
    {block name='button'}{/block}
    {/notempty} diff --git a/plugin/think-plugs-account/stc/database/20241010000005_install_account20241010.php b/plugin/think-plugs-account/stc/database/20241010000005_install_account20241010.php new file mode 100644 index 000000000..6132a7b2e --- /dev/null +++ b/plugin/think-plugs-account/stc/database/20241010000005_install_account20241010.php @@ -0,0 +1,169 @@ +_create_plugin_account_auth(); + $this->_create_plugin_account_bind(); + $this->_create_plugin_account_msms(); + $this->_create_plugin_account_user(); + } + + /** + * 创建数据对象 + * @class PluginAccountAuth + * @table plugin_account_auth + */ + private function _create_plugin_account_auth() + { + // 创建数据表对象 + $table = $this->table('plugin_account_auth', [ + 'engine' => 'InnoDB', 'collation' => 'utf8mb4_general_ci', 'comment' => '插件-账号-授权', + ]); + // 创建或更新数据表 + PhinxExtend::upgrade($table, [ + ['usid', 'biginteger', ['limit' => 20, 'default' => 0, 'null' => true, 'comment' => '终端账号']], + ['time', 'biginteger', ['limit' => 20, 'default' => 0, 'null' => true, 'comment' => '有效时间']], + ['type', 'string', ['limit' => 20, 'default' => '', 'null' => true, 'comment' => '授权类型']], + ['token', 'string', ['limit' => 32, 'default' => '', 'null' => true, 'comment' => '授权令牌']], + ['tokenv', 'string', ['limit' => 32, 'default' => '', 'null' => true, 'comment' => '授权验证']], + ['create_time', 'datetime', ['default' => null, 'null' => true, 'comment' => '创建时间']], + ['update_time', 'datetime', ['default' => null, 'null' => true, 'comment' => '更新时间']], + ], [ + 'usid', 'type', 'time', 'token', 'create_time', + ], true); + } + + /** + * 创建数据对象 + * @class PluginAccountBind + * @table plugin_account_bind + */ + private function _create_plugin_account_bind() + { + // 创建数据表对象 + $table = $this->table('plugin_account_bind', [ + 'engine' => 'InnoDB', 'collation' => 'utf8mb4_general_ci', 'comment' => '插件-账号-终端', + ]); + // 创建或更新数据表 + PhinxExtend::upgrade($table, [ + ['unid', 'biginteger', ['limit' => 20, 'default' => 0, 'null' => true, 'comment' => '会员编号']], + ['type', 'string', ['limit' => 20, 'default' => '', 'null' => true, 'comment' => '终端类型']], + ['phone', 'string', ['limit' => 30, 'default' => '', 'null' => true, 'comment' => '绑定手机']], + ['appid', 'string', ['limit' => 30, 'default' => '', 'null' => true, 'comment' => 'APPID']], + ['openid', 'string', ['limit' => 50, 'default' => '', 'null' => true, 'comment' => 'OPENID']], + ['unionid', 'string', ['limit' => 50, 'default' => '', 'null' => true, 'comment' => 'UnionID']], + ['headimg', 'string', ['limit' => 500, 'default' => '', 'null' => true, 'comment' => '用户头像']], + ['nickname', 'string', ['limit' => 99, 'default' => '', 'null' => true, 'comment' => '用户昵称']], + ['password', 'string', ['limit' => 32, 'default' => '', 'null' => true, 'comment' => '登录密码']], + ['extra', 'text', ['default' => null, 'null' => true, 'comment' => '扩展数据']], + ['sort', 'biginteger', ['limit' => 20, 'default' => 0, 'null' => true, 'comment' => '排序权重']], + ['status', 'integer', ['limit' => 1, 'default' => 1, 'null' => true, 'comment' => '账号状态']], + ['delete_time', 'datetime', ['default' => null, 'null' => true, 'comment' => '删除时间']], + ['create_time', 'datetime', ['default' => null, 'null' => true, 'comment' => '注册时间']], + ['update_time', 'datetime', ['default' => null, 'null' => true, 'comment' => '更新时间']], + ], [ + 'type', 'unid', 'sort', 'phone', 'appid', 'status', 'openid', 'unionid', 'delete_time', 'create_time', + ], true); + } + + /** + * 创建数据对象 + * @class PluginAccountMsms + * @table plugin_account_msms + */ + private function _create_plugin_account_msms() + { + // 创建数据表对象 + $table = $this->table('plugin_account_msms', [ + 'engine' => 'InnoDB', 'collation' => 'utf8mb4_general_ci', 'comment' => '插件-账号-短信', + ]); + // 创建或更新数据表 + PhinxExtend::upgrade($table, [ + ['unid', 'biginteger', ['limit' => 20, 'default' => 0, 'null' => false, 'comment' => '账号编号']], + ['usid', 'biginteger', ['limit' => 20, 'default' => 0, 'null' => false, 'comment' => '终端编号']], + ['type', 'string', ['limit' => 100, 'default' => '', 'null' => true, 'comment' => '短信类型']], + ['scene', 'string', ['limit' => 100, 'default' => '', 'null' => true, 'comment' => '业务场景']], + ['smsid', 'string', ['limit' => 100, 'default' => '', 'null' => true, 'comment' => '消息编号']], + ['phone', 'string', ['limit' => 100, 'default' => '', 'null' => true, 'comment' => '目标手机']], + ['result', 'string', ['limit' => 512, 'default' => '', 'null' => true, 'comment' => '返回结果']], + ['params', 'string', ['limit' => 512, 'default' => '', 'null' => true, 'comment' => '短信内容']], + ['status', 'integer', ['limit' => 1, 'default' => 0, 'null' => true, 'comment' => '短信状态(0失败,1成功)']], + ['create_time', 'datetime', ['default' => null, 'null' => true, 'comment' => '创建时间']], + ['update_time', 'datetime', ['default' => null, 'null' => true, 'comment' => '更新时间']], + ], [ + 'type', 'usid', 'unid', 'phone', 'smsid', 'scene', 'status', 'create_time', + ], true); + } + + /** + * 创建数据对象 + * @class PluginAccountUser + * @table plugin_account_user + */ + private function _create_plugin_account_user() + { + // 创建数据表对象 + $table = $this->table('plugin_account_user', [ + 'engine' => 'InnoDB', 'collation' => 'utf8mb4_general_ci', 'comment' => '插件-账号-资料', + ]); + // 创建或更新数据表 + PhinxExtend::upgrade($table, [ + ['code', 'string', ['limit' => 16, 'default' => '', 'null' => true, 'comment' => '用户编号']], + ['phone', 'string', ['limit' => 20, 'default' => '', 'null' => true, 'comment' => '用户手机']], + ['email', 'string', ['limit' => 99, 'default' => '', 'null' => true, 'comment' => '用户邮箱']], + ['unionid', 'string', ['limit' => 50, 'default' => '', 'null' => true, 'comment' => 'UnionID']], + ['username', 'string', ['limit' => 50, 'default' => '', 'null' => true, 'comment' => '用户姓名']], + ['nickname', 'string', ['limit' => 99, 'default' => '', 'null' => true, 'comment' => '用户昵称']], + ['password', 'string', ['limit' => 32, 'default' => '', 'null' => true, 'comment' => '认证密码']], + ['headimg', 'string', ['limit' => 500, 'default' => '', 'null' => true, 'comment' => '用户头像']], + ['region_prov', 'string', ['limit' => 99, 'default' => '', 'null' => true, 'comment' => '所在省份']], + ['region_city', 'string', ['limit' => 99, 'default' => '', 'null' => true, 'comment' => '所在城市']], + ['region_area', 'string', ['limit' => 99, 'default' => '', 'null' => true, 'comment' => '所在区域']], + ['remark', 'string', ['limit' => 500, 'default' => '', 'null' => true, 'comment' => '备注(内部使用)']], + ['extra', 'text', ['default' => null, 'null' => true, 'comment' => '扩展数据']], + ['sort', 'biginteger', ['limit' => 20, 'default' => 0, 'null' => true, 'comment' => '排序权重']], + ['status', 'integer', ['limit' => 1, 'default' => 1, 'null' => true, 'comment' => '用户状态(0拉黑,1正常)']], + ['delete_time', 'datetime', ['default' => null, 'null' => true, 'comment' => '删除时间']], + ['create_time', 'datetime', ['default' => null, 'null' => true, 'comment' => '注册时间']], + ['update_time', 'datetime', ['default' => null, 'null' => true, 'comment' => '更新时间']], + ], [ + 'code', 'sort', 'phone', 'email', 'status', 'unionid', 'delete_time', 'username', 'nickname', 'region_prov', 'region_city', 'region_area', 'create_time', + ], true); + } +} diff --git a/plugin/think-plugs-account/tests/AccountAdminListControllerTest.php b/plugin/think-plugs-account/tests/AccountAdminListControllerTest.php new file mode 100644 index 000000000..a12739cf9 --- /dev/null +++ b/plugin/think-plugs-account/tests/AccountAdminListControllerTest.php @@ -0,0 +1,402 @@ +configureAccountAccess(); + } + + protected function afterSchemaCreated(): void + { + $this->app->setAppPath(TEST_PROJECT_ROOT . '/plugin/think-plugs-account/src/'); + $this->configureView([ + 'view_path' => TEST_PROJECT_ROOT . '/plugin/think-plugs-account/src/view' . DIRECTORY_SEPARATOR, + ]); + } + + public function testMasterIndexFiltersActiveUsersByKeywordAndDateRange(): void + { + $older = $this->createAccountUser([ + 'username' => 'master-filter-older', + 'nickname' => '主账号检索', + 'phone' => '13888000001', + ]); + $newer = $this->createAccountUser([ + 'username' => 'master-filter-newer', + 'nickname' => '主账号检索', + 'phone' => '13888000002', + ]); + $outRange = $this->createAccountUser([ + 'username' => 'master-filter-out-range', + 'nickname' => '主账号检索', + 'phone' => '13888000003', + ]); + $disabled = $this->createAccountUser([ + 'username' => 'master-filter-disabled', + 'nickname' => '主账号检索', + 'phone' => '13888000004', + 'status' => 0, + ]); + $deleted = $this->createAccountUser([ + 'username' => 'master-filter-deleted', + 'nickname' => '主账号检索', + 'phone' => '13888000005', + ]); + + PluginAccountUser::mk()->where(['id' => $older->getAttr('id')])->update([ + 'create_time' => '2026-03-10 08:00:00', + 'update_time' => '2026-03-10 08:00:00', + ]); + PluginAccountUser::mk()->where(['id' => $newer->getAttr('id')])->update([ + 'create_time' => '2026-03-10 18:00:00', + 'update_time' => '2026-03-10 18:00:00', + ]); + PluginAccountUser::mk()->where(['id' => $outRange->getAttr('id')])->update([ + 'create_time' => '2026-03-09 18:00:00', + 'update_time' => '2026-03-09 18:00:00', + ]); + PluginAccountUser::mk()->where(['id' => $disabled->getAttr('id')])->update([ + 'create_time' => '2026-03-10 12:00:00', + 'update_time' => '2026-03-10 12:00:00', + ]); + PluginAccountUser::mk()->where(['id' => $deleted->getAttr('id')])->update([ + 'create_time' => '2026-03-10 20:00:00', + 'update_time' => '2026-03-10 20:00:00', + 'delete_time' => '2026-03-11 09:00:00', + ]); + + $result = $this->callIndexController(MasterController::class, [ + 'output' => 'json', + 'username' => 'master-filter', + 'create_time' => '2026-03-10 - 2026-03-10', + '_field_' => 'create_time', + '_order_' => 'desc', + 'page' => 1, + 'limit' => 20, + ]); + + $this->assertSame(200, intval($result['code'] ?? 0)); + $this->assertSame('JSON-DATA', $result['info'] ?? ''); + $this->assertSame(2, intval($result['data']['page']['total'] ?? 0)); + $this->assertSame([ + 'master-filter-newer', + 'master-filter-older', + ], array_column($result['data']['list'] ?? [], 'username')); + } + + public function testMasterIndexHistoryViewPaginatesDisabledUsers(): void + { + for ($i = 1; $i <= 11; ++$i) { + $this->createAccountUser([ + 'username' => sprintf('master-history-%02d', $i), + 'nickname' => '主账号历史', + 'phone' => sprintf('1397700%04d', $i), + 'status' => 0, + ]); + } + + $this->createAccountUser([ + 'username' => 'master-history-active', + 'nickname' => '主账号历史', + 'phone' => '13977009999', + 'status' => 1, + ]); + + $result = $this->callIndexController(MasterController::class, [ + 'output' => 'json', + 'type' => 'history', + 'username' => 'master-history', + '_field_' => 'id', + '_order_' => 'asc', + 'page' => 2, + 'limit' => 10, + ]); + + $this->assertSame(200, intval($result['code'] ?? 0)); + $this->assertSame('JSON-DATA', $result['info'] ?? ''); + $this->assertSame(11, intval($result['data']['page']['total'] ?? 0)); + $this->assertSame(2, intval($result['data']['page']['pages'] ?? 0)); + $this->assertSame(2, intval($result['data']['page']['current'] ?? 0)); + $this->assertSame(10, intval($result['data']['page']['limit'] ?? 0)); + $this->assertCount(1, $result['data']['list'] ?? []); + $this->assertSame('master-history-11', $result['data']['list'][0]['username'] ?? ''); + } + + public function testDeviceIndexFiltersByTypeAndPhoneAndIncludesUserRelation(): void + { + $owner = $this->createAccountUser([ + 'username' => 'device-owner-hit', + 'nickname' => '设备归属用户', + ]); + $other = $this->createAccountUser([ + 'username' => 'device-owner-other', + 'nickname' => '其他设备用户', + ]); + + $this->createDeviceBindFixture(intval($owner->getAttr('id')), [ + 'type' => Account::WAP, + 'phone' => '13788000001', + 'nickname' => '命中设备', + 'create_time' => '2026-03-10 08:00:00', + 'update_time' => '2026-03-10 08:00:00', + ]); + $this->createDeviceBindFixture(intval($owner->getAttr('id')), [ + 'type' => Account::WEB, + 'phone' => '13788000002', + 'nickname' => '不同类型设备', + ]); + $this->createDeviceBindFixture(intval($other->getAttr('id')), [ + 'type' => Account::WAP, + 'phone' => '13788100001', + 'nickname' => '其他用户设备', + ]); + $this->createDeviceBindFixture(intval($owner->getAttr('id')), [ + 'type' => Account::WAP, + 'phone' => '13788000003', + 'nickname' => '已删除设备', + 'delete_time' => '2026-03-11 09:00:00', + ]); + + $result = $this->callIndexController(DeviceController::class, [ + 'output' => 'json', + 'utype' => Account::WAP, + 'phone' => '1378800', + '_field_' => 'id', + '_order_' => 'asc', + 'page' => 1, + 'limit' => 20, + ]); + + $this->assertSame(200, intval($result['code'] ?? 0)); + $this->assertSame('JSON-DATA', $result['info'] ?? ''); + $this->assertSame(1, intval($result['data']['page']['total'] ?? 0)); + $this->assertCount(1, $result['data']['list'] ?? []); + $this->assertSame('13788000001', $result['data']['list'][0]['phone'] ?? ''); + $this->assertSame(Account::WAP, $result['data']['list'][0]['type'] ?? ''); + $this->assertSame('device-owner-hit', $result['data']['list'][0]['user']['username'] ?? ''); + } + + public function testDeviceIndexHistoryViewPaginatesDisabledDevices(): void + { + $owner = $this->createAccountUser([ + 'username' => 'device-history-owner', + 'nickname' => '设备历史用户', + ]); + + for ($i = 1; $i <= 11; ++$i) { + $this->createDeviceBindFixture(intval($owner->getAttr('id')), [ + 'type' => Account::WEB, + 'phone' => sprintf('1366600%04d', $i), + 'nickname' => sprintf('历史设备%02d', $i), + 'status' => 0, + 'create_time' => sprintf('2026-03-10 %02d:00:00', min($i, 23)), + 'update_time' => sprintf('2026-03-10 %02d:00:00', min($i, 23)), + ]); + } + + $this->createDeviceBindFixture(intval($owner->getAttr('id')), [ + 'type' => Account::WEB, + 'phone' => '13666009999', + 'status' => 1, + ]); + + $result = $this->callIndexController(DeviceController::class, [ + 'output' => 'json', + 'type' => 'history', + 'utype' => Account::WEB, + 'phone' => '1366600', + '_field_' => 'id', + '_order_' => 'asc', + 'page' => 2, + 'limit' => 10, + ]); + + $this->assertSame(200, intval($result['code'] ?? 0)); + $this->assertSame('JSON-DATA', $result['info'] ?? ''); + $this->assertSame(11, intval($result['data']['page']['total'] ?? 0)); + $this->assertSame(2, intval($result['data']['page']['pages'] ?? 0)); + $this->assertSame(2, intval($result['data']['page']['current'] ?? 0)); + $this->assertSame(10, intval($result['data']['page']['limit'] ?? 0)); + $this->assertCount(1, $result['data']['list'] ?? []); + $this->assertSame('13666000011', $result['data']['list'][0]['phone'] ?? ''); + $this->assertSame(Account::WEB, $result['data']['list'][0]['type'] ?? ''); + } + + public function testMasterIndexRendersEnglishTextsWhenLangSetIsEnUs(): void + { + $this->switchAccountLang('en-us'); + + $html = $this->callActionHtml(MasterController::class, 'index'); + + $this->assertStringContainsString('User Management', $html); + $this->assertStringContainsString('Recycle Bin', $html); + $this->assertStringContainsString('User Code', $html); + $this->assertStringContainsString('Account Status', $html); + $this->assertStringContainsString('Search', $html); + $this->assertStringContainsString('Export', $html); + $this->assertStringContainsString('User Account Data', $html); + $this->assertStringNotContainsString('搜 索', $html); + $this->assertStringNotContainsString('用户管理', $html); + } + + public function testDeviceIndexRendersEnglishTextsWhenLangSetIsEnUs(): void + { + $this->switchAccountLang('en-us'); + + $html = $this->callActionHtml(DeviceController::class, 'index'); + + $this->assertStringContainsString('Device Type', $html); + $this->assertStringContainsString('-- All --', $html); + $this->assertStringContainsString('Associated Account', $html); + $this->assertStringContainsString('First Login', $html); + $this->assertStringContainsString('Search', $html); + $this->assertStringContainsString('Export', $html); + $this->assertStringContainsString('Mobile Browser', $html); + $this->assertStringNotContainsString('终端类型', $html); + } + + public function testDeviceConfigRendersEnglishTextsWhenLangSetIsEnUs(): void + { + $this->switchAccountLang('en-us'); + + $html = $this->callActionHtml(DeviceController::class, 'config'); + + $this->assertStringContainsString('Authentication Expire Time', $html); + $this->assertStringContainsString('Auto Register on Login', $html); + $this->assertStringContainsString('Enable Auto Registration', $html); + $this->assertStringContainsString('Default Nickname Prefix', $html); + $this->assertStringContainsString('Open Interface Channels', $html); + $this->assertStringContainsString('Mobile Browser', $html); + $this->assertStringContainsString('Save Data', $html); + $this->assertStringContainsString('Cancel Edit', $html); + $this->assertStringNotContainsString('启用自动注册', $html); + } + + protected function defineSchema(): void + { + $this->createAccountTables(); + } + + /** + * @param class-string $controllerClass + */ + private function callIndexController(string $controllerClass, array $query): array + { + $parts = explode('\\', $controllerClass); + $request = (new Request()) + ->withGet($query) + ->setMethod('GET') + ->setController(strtolower(strval(end($parts)))) + ->setAction('index'); + + $this->setRequestPayload($request, $query); + RequestContext::clear(); + $this->app->instance('request', $request); + + try { + $controller = new $controllerClass($this->app); + $controller->index(); + self::fail("Expected {$controllerClass}::index to throw HttpResponseException."); + } catch (HttpResponseException $exception) { + return json_decode($exception->getResponse()->getContent(), true) ?: []; + } + } + + private function callActionHtml(string $controllerClass, string $action, array $query = []): string + { + $parts = explode('\\', $controllerClass); + $request = (new Request()) + ->withGet($query) + ->setMethod('GET') + ->setController(strtolower(strval(end($parts)))) + ->setAction($action); + + $this->setRequestPayload($request, $query); + RequestContext::clear(); + $this->activateApplicationContext($request); + + try { + $controller = new $controllerClass($this->app); + $controller->{$action}(); + self::fail("Expected {$controllerClass}::{$action} to throw HttpResponseException."); + } catch (HttpResponseException $exception) { + return $exception->getResponse()->getContent(); + } + } + + private function createDeviceBindFixture(int $unid, array $overrides = []): PluginAccountBind + { + $bind = PluginAccountBind::mk(); + $bind->save(array_merge([ + 'unid' => $unid, + 'type' => Account::WAP, + 'phone' => $this->randomPhone('1375500'), + 'appid' => '', + 'openid' => '', + 'unionid' => '', + 'headimg' => 'https://example.com/device.png', + 'nickname' => '测试设备', + 'password' => '', + 'extra' => [], + 'sort' => 0, + 'status' => 1, + 'create_time' => date('Y-m-d H:i:s'), + 'update_time' => date('Y-m-d H:i:s'), + 'delete_time' => null, + ], $overrides)); + + return PluginAccountBind::mk()->withTrashed()->findOrEmpty($bind->getKey()); + } + + private function setRequestPayload(Request $request, array $data): void + { + $property = new \ReflectionProperty(Request::class, 'request'); + $property->setAccessible(true); + $property->setValue($request, $data); + } + + private function switchAccountLang(string $langSet): void + { + $this->app->lang->switchLangSet($langSet); + $file = TEST_PROJECT_ROOT . "/plugin/think-plugs-account/src/lang/{$langSet}.php"; + if (is_file($file)) { + $this->app->lang->load($file, $langSet); + } + } +} diff --git a/plugin/think-plugs-account/tests/AccountCenterControllerTest.php b/plugin/think-plugs-account/tests/AccountCenterControllerTest.php new file mode 100644 index 000000000..de3e8fb8e --- /dev/null +++ b/plugin/think-plugs-account/tests/AccountCenterControllerTest.php @@ -0,0 +1,198 @@ +configureAccountAccess(); + } + + public function testBindControllerAssociatesUserAndClearsVerifyCode(): void + { + $phone = $this->randomPhone('1360099'); + $account = $this->createAccountFixture(Account::WAP, ['phone' => $phone]); + $login = $account->get(true); + $verify = '246810'; + + $this->rememberVerifyCode($phone, $verify); + $response = $this->callCenterController('bind', [ + 'phone' => $phone, + 'verify' => $verify, + 'passwd' => 'Secret@123', + ], strval($login['token'] ?? '')); + + $bind = PluginAccountBind::mk()->where(['phone' => $phone, 'type' => Account::WAP])->findOrEmpty(); + $user = PluginAccountUser::mk()->findOrEmpty(intval($bind->getAttr('unid'))); + + $this->assertSame(200, intval($response['code'] ?? 0)); + $this->assertSame('关联成功', $response['info'] ?? ''); + $this->assertNotEmpty($response['data']['token'] ?? ''); + $this->assertSame($phone, $response['data']['user']['phone'] ?? ''); + $this->assertTrue($bind->isExists()); + $this->assertGreaterThan(0, intval($bind->getAttr('unid'))); + $this->assertTrue($user->isExists()); + $this->assertTrue(password_verify('Secret@123', strval($user->getAttr('password')))); + $this->assertTrue(password_verify('Secret@123', strval($bind->getAttr('password')))); + $this->assertFalse($this->app->cache->has($this->verifyCacheKey($phone))); + } + + public function testUnbindControllerReturnsDetachedAccountView(): void + { + $phone = $this->randomPhone('1370099'); + $account = $this->createBoundAccountFixture(Account::WAP, ['phone' => $phone]); + $login = $account->token()->get(true); + + $response = $this->callCenterController('unbind', [], strval($login['token'] ?? '')); + $bind = PluginAccountBind::mk()->findOrEmpty($account->getUsid()); + + $this->assertSame(200, intval($response['code'] ?? 0)); + $this->assertSame('关联成功', $response['info'] ?? ''); + $this->assertSame(0, intval($response['data']['unid'] ?? -1)); + $this->assertArrayNotHasKey('id', $response['data']['user'] ?? []); + $this->assertSame(0, intval($bind->getAttr('unid'))); + } + + public function testSetControllerKeepsExistingPasswordWhenMaskIsSubmitted(): void + { + $phone = $this->randomPhone('1380099'); + $account = $this->createBoundAccountFixture(Account::WAP, ['phone' => $phone]); + $account->pwdModify('Secret@123', false); + $login = $account->token()->get(true); + + $response = $this->callCenterController('set', [ + 'nickname' => '星号保留昵称', + 'password' => password_mask(), + ], strval($login['token'] ?? '')); + + $bind = PluginAccountBind::mk()->findOrEmpty($account->getUsid()); + $user = PluginAccountUser::mk()->findOrEmpty($account->getUnid()); + + $this->assertSame(200, intval($response['code'] ?? 0)); + $this->assertSame('修改成功', $response['info'] ?? ''); + $this->assertSame('星号保留昵称', $response['data']['user']['nickname'] ?? ''); + $this->assertTrue(password_verify('Secret@123', strval($bind->getAttr('password')))); + $this->assertTrue(password_verify('Secret@123', strval($user->getAttr('password')))); + } + + public function testGetControllerReturnsEnglishInfoWhenLangSetIsEnUs(): void + { + $phone = $this->randomPhone('1350099'); + $account = $this->createBoundAccountFixture(Account::WAP, ['phone' => $phone]); + $login = $account->token()->get(true); + $this->switchAccountLang('en-us'); + + $response = $this->callCenterController('get', [], strval($login['token'] ?? '')); + + $this->assertSame(200, intval($response['code'] ?? 0)); + $this->assertSame('Profile loaded successfully', $response['info'] ?? ''); + $this->assertSame($phone, $response['phone'] ?? $response['data']['phone'] ?? ''); + } + + public function testGetControllerReturnsEnglishUnauthorizedInfoWhenTokenMissing(): void + { + $this->switchAccountLang('en-us'); + + $response = $this->callCenterController('get', [], ''); + + $this->assertSame(401, intval($response['code'] ?? 0)); + $this->assertSame('auth.unauthorized', $response['error'] ?? ''); + $this->assertSame('Login authorization required', $response['info'] ?? ''); + } + + public function testSetControllerReturnsForbiddenWhenPrimaryAccountIsNotBound(): void + { + $phone = $this->randomPhone('1340099'); + $account = $this->createAccountFixture(Account::WAP, ['phone' => $phone]); + $login = $account->get(true); + + $response = $this->callCenterController('set', [ + 'nickname' => '未绑定账号', + ], strval($login['token'] ?? '')); + + $this->assertSame(403, intval($response['code'] ?? 0)); + $this->assertSame('auth.forbidden', $response['error'] ?? ''); + $this->assertSame('请绑定账号', $response['info'] ?? ''); + } + + protected function defineSchema(): void + { + $this->createAccountTables(); + } + + private function callCenterController(string $action, array $post, string $token): array + { + $request = $this->app->request + ->withGet($post) + ->withPost($post) + ->withHeader(['authorization' => "Bearer {$token}"]) + ->setController('api.auth.center') + ->setAction($action); + + RequestContext::clear(); + $this->app->instance('request', $request); + + try { + $controller = new AuthCenterController($this->app); + $controller->{$action}(); + self::fail("Expected {$action} to throw HttpResponseException."); + } catch (HttpResponseException $exception) { + return json_decode($exception->getResponse()->getContent(), true) ?: []; + } + } + + private function rememberVerifyCode(string $phone, string $code, string $scene = Message::tLogin): void + { + $this->app->cache->set($this->verifyCacheKey($phone, $scene), [ + 'code' => $code, + 'time' => time(), + ], 600); + } + + private function verifyCacheKey(string $phone, string $scene = Message::tLogin): string + { + return md5(strtolower("sms-{$scene}-{$phone}")); + } + + private function switchAccountLang(string $langSet): void + { + $this->app->lang->switchLangSet($langSet); + $file = TEST_PROJECT_ROOT . "/plugin/think-plugs-account/src/lang/{$langSet}.php"; + if (is_file($file)) { + $this->app->lang->load($file, $langSet); + } + } +} diff --git a/plugin/think-plugs-account/tests/AccountIntegrationTest.php b/plugin/think-plugs-account/tests/AccountIntegrationTest.php new file mode 100644 index 000000000..7233184e1 --- /dev/null +++ b/plugin/think-plugs-account/tests/AccountIntegrationTest.php @@ -0,0 +1,162 @@ +configureAccountAccess(); + } + + public function testSetCreatesBindAndAuthRecords(): void + { + $phone = '1380013' . random_int(1000, 9999); + $info = Account::mk(Account::WAP)->set([ + 'phone' => $phone, + 'extra' => ['source' => 'integration'], + ], true); + + $this->assertSame($phone, $info['phone']); + $this->assertSame(Account::WAP, $info['type']); + $this->assertSame('https://example.com/default-account.png', $info['headimg']); + $this->assertNotEmpty($info['nickname']); + $this->assertNotEmpty($info['token']); + $this->assertArrayNotHasKey('id', $info['user']); + + $bind = PluginAccountBind::mk()->where(['phone' => $phone])->findOrEmpty(); + $this->assertTrue($bind->isExists()); + $this->assertSame(['source' => 'integration'], $bind->getAttr('extra')); + + $auth = PluginAccountAuth::mk()->where([ + 'usid' => $bind->getAttr('id'), + 'type' => Account::WAP, + ])->findOrEmpty(); + $this->assertTrue($auth->isExists()); + $this->assertGreaterThan(time(), intval($auth->getAttr('time'))); + } + + public function testBindRecodeAndUnbindUpdateUserRelations(): void + { + $phone = $this->randomPhone('1390013'); + $account = $this->createAccountFixture(Account::WAP, ['phone' => $phone]); + + $bound = $account->bind(['phone' => $phone], [ + 'username' => 'tester-' . random_int(100, 999), + 'extra' => ['scene' => 'integration'], + ]); + + $this->assertSame($phone, $bound['phone']); + $this->assertNotEmpty($bound['user']); + $this->assertSame($phone, $bound['user']['phone']); + $this->assertSame('integration', $bound['user']['extra']['scene']); + $this->assertSame('https://example.com/default-account.png', $bound['user']['headimg']); + + $user = PluginAccountUser::mk()->findOrEmpty(intval($bound['user']['id'])); + $oldCode = strval($user->getAttr('code')); + $recode = $account->recode(); + $this->assertNotSame($oldCode, $recode['user']['code']); + + $account->unBind(); + $fresh = Account::mk(Account::WAP, ['phone' => $phone], false)->get(); + + $this->assertSame(0, intval($fresh['unid'])); + $this->assertArrayNotHasKey('id', $fresh['user']); + } + + public function testPwdModifySynchronizesBindAndUserPasswords(): void + { + $phone = $this->randomPhone('1370013'); + $account = $this->createAccountFixture(Account::WAP, ['phone' => $phone]); + $bound = $account->bind(['phone' => $phone], ['username' => 'pwd-user-' . random_int(100, 999)]); + + $this->assertTrue($account->pwdModify('Secret@123', false)); + $this->assertTrue($account->pwdVerify('Secret@123')); + $this->assertFalse($account->pwdVerify('invalid-pass')); + + $user = PluginAccountUser::mk()->findOrEmpty(intval($bound['user']['id'])); + $bind = PluginAccountBind::mk()->where(['phone' => $phone])->findOrEmpty(); + $this->assertTrue(password_verify('Secret@123', strval($user->getAttr('password')))); + $this->assertTrue(password_verify('Secret@123', strval($bind->getAttr('password')))); + } + + public function testAllBindAndDelBindReflectMultipleClients(): void + { + $primaryPhone = $this->randomPhone('1350013'); + $secondaryPhone = $this->randomPhone('1340013'); + + $primary = $this->createAccountFixture(Account::WAP, ['phone' => $primaryPhone]); + $bound = $primary->bind(['phone' => $primaryPhone], ['username' => 'multi-' . random_int(100, 999)]); + $unid = intval($bound['user']['id']); + + $secondary = $this->createAccountFixture(Account::WEB, ['phone' => $secondaryPhone]); + $secondary->bind(['id' => $unid]); + + $all = $primary->allBind(); + $phones = array_column($all, 'phone'); + $expected = [$primaryPhone, $secondaryPhone]; + sort($phones); + sort($expected); + + $this->assertCount(2, $all); + $this->assertSame($expected, $phones); + + $after = $primary->delBind($secondary->getUsid()); + $this->assertCount(1, $after); + $this->assertSame($primaryPhone, $after[0]['phone']); + + $detached = PluginAccountBind::mk()->findOrEmpty($secondary->getUsid()); + $this->assertSame(0, intval($detached->getAttr('unid'))); + } + + public function testCheckReturnsUnauthorizedWhenTokenExpired(): void + { + $account = $this->createAccountFixture(Account::WAP, ['phone' => $this->randomPhone('1330013')]); + PluginAccountAuth::mk()->where(['usid' => $account->getUsid(), 'type' => Account::WAP])->update([ + 'time' => time() - 10, + ]); + + try { + Account::mk(Account::WAP, ['phone' => $account->get()['phone'] ?? ''], false)->check(); + self::fail('Expected AccountAccess::check to throw Exception.'); + } catch (Exception $exception) { + $this->assertSame(401, $exception->getCode()); + $this->assertSame('登录已超时', $exception->getMessage()); + } + } + + protected function defineSchema(): void + { + $this->createAccountTables(); + } +} diff --git a/plugin/think-plugs-account/tests/AccountLoginControllerTest.php b/plugin/think-plugs-account/tests/AccountLoginControllerTest.php new file mode 100644 index 000000000..7061ceb7d --- /dev/null +++ b/plugin/think-plugs-account/tests/AccountLoginControllerTest.php @@ -0,0 +1,157 @@ +configureAccountAccess(); + } + + public function testSmsLoginReturnsEnglishSuccessMessageWhenLangSetIsEnUs(): void + { + $phone = $this->randomPhone('1390088'); + $this->switchAccountLang('en-us'); + $this->rememberVerifyCode($phone, '246810'); + + $response = $this->callLoginController('in', [ + 'type' => Account::WAP, + 'phone' => $phone, + 'verify' => '246810', + ]); + + $this->assertSame(200, intval($response['code'] ?? 0)); + $this->assertSame('Login successful', $response['info'] ?? ''); + $this->assertSame($phone, $response['data']['phone'] ?? ''); + $this->assertNotEmpty($response['data']['token'] ?? ''); + } + + public function testAutoReturnsEnglishValidationMessageWhenCodeIsMissing(): void + { + $this->switchAccountLang('en-us'); + + $response = $this->callLoginController('auto', []); + + $this->assertSame(500, intval($response['code'] ?? 0)); + $this->assertSame('Authorization code is required', $response['info'] ?? ''); + } + + public function testVerifyReturnsEnglishValidationMessageWhenPayloadIsMissing(): void + { + $this->switchAccountLang('en-us'); + + $response = $this->callLoginController('verify', []); + + $this->assertSame(500, intval($response['code'] ?? 0)); + $this->assertSame('Slider verification is required', $response['info'] ?? ''); + } + + public function testSendReturnsStandardizedSuccessPayloadWhenCodeWasRecentlySent(): void + { + $phone = $this->randomPhone('1390077'); + $this->rememberVerifyCode($phone, '246810'); + + $image = $this->callLoginController('image', []); + $uniqid = strval($image['data']['uniqid'] ?? ''); + $verify = $this->sliderVerifyValue($uniqid); + $response = $this->callLoginController('send', [ + 'type' => 'login', + 'phone' => $phone, + 'uniqid' => $uniqid, + 'verify' => $verify, + ]); + + $this->assertSame(200, intval($response['code'] ?? 0)); + $this->assertSame('验证码已发送', $response['info'] ?? ''); + $this->assertGreaterThanOrEqual(0, intval($response['data']['time'] ?? -1)); + } + + protected function defineSchema(): void + { + $this->createAccountTables(); + } + + private function callLoginController(string $action, array $data): array + { + $request = (new Request()) + ->withGet($data) + ->withPost($data) + ->setMethod('POST') + ->setController('api.login') + ->setAction($action); + + $this->setRequestPayload($request, $data); + RequestContext::clear(); + $this->activateApplicationContext($request); + + try { + $controller = new LoginController($this->app); + $controller->{$action}(); + self::fail("Expected LoginController::{$action} to throw HttpResponseException."); + } catch (HttpResponseException $exception) { + return json_decode($exception->getResponse()->getContent(), true) ?: []; + } + } + + private function rememberVerifyCode(string $phone, string $code, string $scene = Message::tLogin): void + { + $this->app->cache->set(md5(strtolower("sms-{$scene}-{$phone}")), [ + 'code' => $code, + 'time' => time(), + ], 600); + } + + private function sliderVerifyValue(string $uniqid): string + { + $range = $this->app->cache->get($uniqid, [])['range'] ?? [0, 0]; + return (string)intval((intval($range[0] ?? 0) + intval($range[1] ?? 0)) / 2); + } + + private function setRequestPayload(Request $request, array $data): void + { + $property = new \ReflectionProperty(Request::class, 'request'); + $property->setAccessible(true); + $property->setValue($request, $data); + } + + private function switchAccountLang(string $langSet): void + { + $this->app->lang->switchLangSet($langSet); + $file = TEST_PROJECT_ROOT . "/plugin/think-plugs-account/src/lang/{$langSet}.php"; + if (is_file($file)) { + $this->app->lang->load($file, $langSet); + } + } +} diff --git a/plugin/think-plugs-account/tests/AccountMessageControllerTest.php b/plugin/think-plugs-account/tests/AccountMessageControllerTest.php new file mode 100644 index 000000000..730842090 --- /dev/null +++ b/plugin/think-plugs-account/tests/AccountMessageControllerTest.php @@ -0,0 +1,235 @@ +createSystemDataTable(); + $this->executeStatements([ + <<<'SQL' +CREATE TABLE plugin_account_msms ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + unid INTEGER DEFAULT 0, + usid INTEGER DEFAULT 0, + type TEXT DEFAULT '', + scene TEXT DEFAULT '', + smsid TEXT DEFAULT '', + phone TEXT DEFAULT '', + result TEXT DEFAULT '', + params TEXT DEFAULT '', + status INTEGER DEFAULT 0, + create_time TEXT DEFAULT NULL, + update_time TEXT DEFAULT NULL +) +SQL, + ]); + } + + protected function afterSchemaCreated(): void + { + $this->app->setAppPath(TEST_PROJECT_ROOT . '/plugin/think-plugs-account/src/'); + $this->configureView([ + 'view_path' => TEST_PROJECT_ROOT . '/plugin/think-plugs-account/src/view' . DIRECTORY_SEPARATOR, + ]); + } + + public function testIndexJsonTranslatesSceneNameWhenLangSetIsEnUs(): void + { + $this->switchAccountLang('en-us'); + $this->createMessageFixture([ + 'scene' => AccountMessage::tLogin, + 'status' => 1, + 'phone' => '13800000001', + 'smsid' => 'SMS-EN-001', + ]); + + $result = $this->callJson('index', [ + 'output' => 'json', + 'scene' => AccountMessage::tLogin, + '_field_' => 'id', + '_order_' => 'desc', + 'page' => 1, + 'limit' => 10, + ]); + + $this->assertSame(200, intval($result['code'] ?? 0)); + $this->assertSame('JSON-DATA', $result['info'] ?? ''); + $this->assertSame('User Login Verification', $result['data']['list'][0]['scene_name'] ?? ''); + } + + public function testIndexRendersEnglishTextsWhenLangSetIsEnUs(): void + { + $this->switchAccountLang('en-us'); + + $html = $this->callHtml('index'); + + $this->assertStringContainsString('SMS Message Management', $html); + $this->assertStringContainsString('Search Filters', $html); + $this->assertStringContainsString('Message Code', $html); + $this->assertStringContainsString('Business Scene', $html); + $this->assertStringContainsString('Execution Result', $html); + $this->assertStringContainsString('Send Failed', $html); + $this->assertStringContainsString('Send Success', $html); + $this->assertStringContainsString('Failed', $html); + $this->assertStringContainsString('Success', $html); + $this->assertStringNotContainsString('手机短信管理', $html); + } + + public function testConfigRendersEnglishTextsWhenLangSetIsEnUs(): void + { + $this->switchAccountLang('en-us'); + + $html = $this->callHtml('config'); + + $this->assertStringContainsString('Service Region', $html); + $this->assertStringContainsString('Aliyun Account', $html); + $this->assertStringContainsString('Aliyun Secret Key', $html); + $this->assertStringContainsString('SMS Signature', $html); + $this->assertStringContainsString('User Login Verification', $html); + $this->assertStringContainsString('North China 1 (Qingdao)', $html); + $this->assertStringContainsString('Save Configuration', $html); + $this->assertStringContainsString('Cancel Modification', $html); + $this->assertStringNotContainsString('短信配置', $html); + } + + public function testConfigPostReturnsTranslatedSuccessMessageAndPersistsPayload(): void + { + $this->switchAccountLang('en-us'); + + $result = $this->callJson('config', [ + 'alisms_region' => 'cn-qingdao', + 'alisms_keyid' => 'demo-key', + 'alisms_secret' => 'demo-secret', + 'alisms_signtx' => 'DemoSign', + 'scene_login' => 'SMS_LOGIN', + 'scene_forget' => 'SMS_FORGET', + 'scene_register' => 'SMS_REGISTER', + ], 'POST'); + + $this->assertSame(200, intval($result['code'] ?? 0)); + $this->assertSame('Configuration updated successfully', $result['info'] ?? ''); + + $payload = (array) sysdata('plugin.account.smscfg'); + + $this->assertSame('cn-qingdao', $payload['alisms_region'] ?? ''); + $this->assertSame('demo-key', $payload['alisms_keyid'] ?? ''); + $this->assertSame('SMS_LOGIN', $payload['alisms_scenes']['LOGIN'] ?? ''); + $this->assertSame('SMS_FORGET', $payload['alisms_scenes']['FORGET'] ?? ''); + $this->assertSame('SMS_REGISTER', $payload['alisms_scenes']['REGISTER'] ?? ''); + } + + private function callHtml(string $action, array $query = []): string + { + $request = (new Request()) + ->withGet($query) + ->setMethod('GET') + ->setController('message') + ->setAction($action); + + $this->setRequestPayload($request, $query); + RequestContext::clear(); + $this->activateApplicationContext($request); + + try { + $controller = new MessageController($this->app); + $controller->{$action}(); + self::fail("Expected MessageController::{$action} to throw HttpResponseException."); + } catch (HttpResponseException $exception) { + return $exception->getResponse()->getContent(); + } + } + + private function callJson(string $action, array $data = [], string $method = 'GET'): array + { + $request = (new Request()) + ->setMethod($method) + ->setController('message') + ->setAction($action); + + if ($method === 'GET') { + $request = $request->withGet($data); + } else { + $request = $request->withGet($data)->withPost($data); + } + + $this->setRequestPayload($request, $data); + RequestContext::clear(); + $this->activateApplicationContext($request); + + try { + $controller = new MessageController($this->app); + $controller->{$action}(); + self::fail("Expected MessageController::{$action} to throw HttpResponseException."); + } catch (HttpResponseException $exception) { + return json_decode($exception->getResponse()->getContent(), true) ?: []; + } + } + + private function createMessageFixture(array $overrides = []): PluginAccountMsms + { + $message = PluginAccountMsms::mk(); + $message->save(array_merge([ + 'unid' => 0, + 'usid' => 0, + 'type' => 'Alisms', + 'scene' => AccountMessage::tLogin, + 'smsid' => 'SMS-' . random_int(1000, 9999), + 'phone' => '13800000000', + 'result' => '{}', + 'params' => '{"code":"123456"}', + 'status' => 1, + 'create_time' => date('Y-m-d H:i:s'), + 'update_time' => date('Y-m-d H:i:s'), + ], $overrides)); + + return $message->refresh(); + } + + private function setRequestPayload(Request $request, array $data): void + { + $property = new \ReflectionProperty(Request::class, 'request'); + $property->setAccessible(true); + $property->setValue($request, $data); + } + + private function switchAccountLang(string $langSet): void + { + $this->app->lang->switchLangSet($langSet); + $file = TEST_PROJECT_ROOT . "/plugin/think-plugs-account/src/lang/{$langSet}.php"; + if (is_file($file)) { + $this->app->lang->load($file, $langSet); + } + } +} diff --git a/plugin/think-plugs-account/tests/AccountRuntimeTest.php b/plugin/think-plugs-account/tests/AccountRuntimeTest.php new file mode 100644 index 000000000..86166202c --- /dev/null +++ b/plugin/think-plugs-account/tests/AccountRuntimeTest.php @@ -0,0 +1,267 @@ +app = new App(dirname(__DIR__, 3)); + RuntimeService::init($this->app); + $this->app->config->set([ + 'default' => 'file', + 'stores' => [ + 'file' => ['type' => 'File', 'path' => sys_get_temp_dir() . '/thinkadmin-account-test-cache'], + ], + ], 'cache'); + $this->app->config->set(['jwtkey' => 'account-test-jwt'], 'app'); + + $this->context = new AccountRuntimeSystemContextStub(); + Container::getInstance()->instance(SystemContextInterface::class, $this->context); + + $this->defaultTypes = (new \ReflectionClass(Account::class))->getDefaultProperties()['types']; + $this->resetAccountState(); + RequestContext::clear(); + sysvar('', ''); + } + + protected function tearDown(): void + { + RequestContext::clear(); + sysvar('', ''); + $this->resetAccountState(); + Container::getInstance()->instance(SystemContextInterface::class, new NullSystemContext()); + } + + public function testTypesExposeKnownChannels(): void + { + $types = Account::types(1); + + $this->assertSame('phone', $types[Account::WAP]['field']); + $this->assertSame('openid', $types[Account::WECHAT]['field']); + $this->assertSame(Account::WXAPP, $types[Account::WXAPP]['code']); + } + + public function testDisabledChannelReturnsEmptyField(): void + { + $this->context->setData('plugin.account.denys', [Account::WEB]); + $this->resetAccountState(); + + $this->assertSame('', Account::field(Account::WEB)); + $this->assertArrayNotHasKey(Account::WEB, Account::types(1)); + } + + public function testBuildJwtTokenBindsSessionPayload(): void + { + $token = Account::buildJwtToken(Account::WAP, 'token-123', 'sid-account-test'); + $data = JwtToken::verify($token, 'account-test-jwt'); + + $this->assertSame(Account::getTokenType(), $data['typ']); + $this->assertSame(Account::WAP, $data['type']); + $this->assertSame('token-123', $data['token']); + $this->assertSame('sid-account-test', $data['sid']); + $this->assertTrue(CacheSession::exists('sid:sid-account-test')); + } + + public function testRequestTokenPrefersAuthorizationHeader(): void + { + RequestContext::clear(); + $headerToken = JwtToken::token([ + 'typ' => Account::getTokenType(), + 'type' => Account::WAP, + 'token' => 'header-token', + ], 'account-test-jwt'); + $cookieToken = JwtToken::token([ + 'typ' => Account::getTokenType(), + 'type' => Account::WAP, + 'token' => 'cookie-token', + ], 'account-test-jwt'); + $encodedCookie = RequestTokenService::encodeCookieToken($cookieToken); + + $request = (new Request()) + ->withHeader(['authorization' => "Bearer {$headerToken}"]) + ->withCookie([Account::getTokenCookie() => $encodedCookie]); + + $this->assertSame($headerToken, Account::requestToken($request)); + } + + public function testRequestTokenCanDecryptEncryptedCookie(): void + { + RequestContext::clear(); + $cookieToken = JwtToken::token([ + 'typ' => Account::getTokenType(), + 'type' => Account::WAP, + 'token' => 'cookie-only-token', + ], 'account-test-jwt'); + $encodedCookie = RequestTokenService::encodeCookieToken($cookieToken); + $request = (new Request())->withCookie([Account::getTokenCookie() => $encodedCookie]); + + $this->assertNotSame($cookieToken, $encodedCookie); + $this->assertSame($cookieToken, Account::requestToken($request)); + } + + public function testRequestTokenUpgradesLegacyPlainCookie(): void + { + RequestContext::clear(); + $cookieToken = JwtToken::token([ + 'typ' => Account::getTokenType(), + 'type' => Account::WAP, + 'token' => 'legacy-cookie-token', + ], 'account-test-jwt'); + $request = (new Request())->withCookie([Account::getTokenCookie() => $cookieToken]); + + $this->app->instance('request', $request); + + $this->assertSame($cookieToken, Account::requestToken($request)); + + $queuedCookie = strval($this->app->cookie->getCookie()[Account::getTokenCookie()][0] ?? ''); + $this->assertStringStartsWith('enc:', $queuedCookie); + $this->assertNotSame($cookieToken, $queuedCookie); + $this->assertSame($cookieToken, RequestTokenService::decodeCookieToken($queuedCookie)); + } + + public function testAccountTokenCookieCanBeConfigured(): void + { + app()->config->set(['account_token_cookie' => 'custom_account_cookie'], 'app'); + + $this->assertSame('custom_account_cookie', Account::getTokenCookie()); + } + + private function resetAccountState(): void + { + $reflection = new \ReflectionClass(Account::class); + + $types = $reflection->getProperty('types'); + $types->setValue(null, $this->defaultTypes); + + $denys = $reflection->getProperty('denys'); + $denys->setValue(null, null); + } +} + +class AccountRuntimeSystemContextStub implements SystemContextInterface +{ + private array $data = []; + + public function buildToken(): string + { + return ''; + } + + public function getTokenHeader(): string + { + return 'Authorization'; + } + + public function getTokenCookie(): string + { + return 'system_access_token'; + } + + public function getTokenType(): string + { + return 'system-auth'; + } + + public function syncTokenCookie(?string $token = null): string + { + return strval($token); + } + + public function check(?string $node = ''): bool + { + return false; + } + + public function getUser(?string $field = null, $default = null) + { + return is_null($field) ? [] : $default; + } + + public function getUserId(): int + { + return 0; + } + + public function isSuper(): bool + { + return false; + } + + public function isLogin(): bool + { + return false; + } + + public function withUploadUnid(?string $uptoken = null): array + { + return [0, []]; + } + + public function clearAuth(): bool + { + return true; + } + + public function getData(string $name, $default = []) + { + return $this->data[$name] ?? $default; + } + + public function setData(string $name, $value): bool + { + $this->data[$name] = $value; + return true; + } + + public function setOplog(string $action, string $content): bool + { + return true; + } + + public function baseItems(string $type, array &$data = [], string $field = 'base_code', string $bind = 'base_info'): array + { + return []; + } +} diff --git a/plugin/think-plugs-account/tests/bootstrap.php b/plugin/think-plugs-account/tests/bootstrap.php new file mode 100644 index 000000000..18486184d --- /dev/null +++ b/plugin/think-plugs-account/tests/bootstrap.php @@ -0,0 +1,39 @@ + 'mysql', + 'connections' => [ + 'mysql' => [ + 'type' => 'mysql', + 'hostname' => '127.0.0.1', + 'database' => 'admin_v6', + 'username' => 'admin_v6', + 'password' => 'FbYBHcWKr2', + 'hostport' => '3306', + 'charset' => 'utf8mb4', + 'debug' => true, + ], + ], +]); diff --git a/plugin/think-plugs-builder/composer.json b/plugin/think-plugs-builder/composer.json new file mode 100644 index 000000000..287fee3f4 --- /dev/null +++ b/plugin/think-plugs-builder/composer.json @@ -0,0 +1,42 @@ +{ + "type": "think-admin-plugin", + "name": "zoujingli/think-plugs-builder", + "version": "8.0.x-dev", + "license": "Apache-2.0", + "homepage": "https://thinkadmin.top", + "description": "PHAR builder for ThinkAdmin projects", + "authors": [ + { + "name": "Anyon", + "email": "zoujingli@qq.com" + } + ], + "require": { + "php": "^8.1", + "zoujingli/think-library": "^8.0" + }, + "autoload": { + "psr-4": { + "plugin\\builder\\": "src" + } + }, + "extra": { + "think": { + "services": [ + "plugin\\builder\\Service" + ] + }, + "xadmin": { + "app": { + "name": "PHAR 构建工具", + "description": "为 ThinkAdmin 项目提供 PHAR 打包构建能力。", + "code": "builder", + "prefix": "builder" + } + } + }, + "minimum-stability": "dev", + "config": { + "sort-packages": true + } +} diff --git a/plugin/think-plugs-builder/readme.api.md b/plugin/think-plugs-builder/readme.api.md new file mode 100644 index 000000000..e99642418 --- /dev/null +++ b/plugin/think-plugs-builder/readme.api.md @@ -0,0 +1,30 @@ +# PHAR 构建工具接口 + +## 接口标准 +- 接口类型:命令行接口 +- 调用入口:`php think ` 或封装脚本 `composer build:phar` +- 返回形式:控制台文本输出 + 生成的 `.phar` 文件 + +## 接口列表 + +### `xadmin:builder` +- 说明:把当前项目打包为可执行 PHAR + +```jsonc +{ + "name": "admin.phar", // 输出的 PHAR 文件名 + "main": "think", // 包内主入口文件 + "extract": ["public", "database"], // 首次启动时需要解压到外部目录的路径列表 + "mount": [".env", "runtime", "safefile", "public", "database"], // 运行时挂载到 PHAR 外部的文件或目录 + "exclude": [] // 额外排除的文件或目录 +} +``` + +### `composer build:phar` +- 说明:项目层封装命令,先执行发布,再调用 `xadmin:builder` + +```jsonc +{ + "database:publish": true // 是否先执行发布流程,默认会调用 composer database:publish +} +``` diff --git a/plugin/think-plugs-builder/readme.md b/plugin/think-plugs-builder/readme.md new file mode 100644 index 000000000..88f821e19 --- /dev/null +++ b/plugin/think-plugs-builder/readme.md @@ -0,0 +1,14 @@ +# PHAR 构建工具 + +## 定位 +- 组件编码:`builder` +- 组件包名:`zoujingli/think-plugs-builder` +- 主要职责:把当前 ThinkAdmin 项目打包为可运行的 PHAR 文件 + +## 边界 +- 仅暴露命令接口,不提供 HTTP API +- 主要入口:`php think xadmin:builder` 或 `composer build:phar` +- 负责打包策略,不负责插件发布、迁移执行与业务运行时 + +## 文档 +- 命令接口说明:`readme.api.md` diff --git a/plugin/think-plugs-builder/src/Service.php b/plugin/think-plugs-builder/src/Service.php new file mode 100644 index 000000000..b95d1fbdf --- /dev/null +++ b/plugin/think-plugs-builder/src/Service.php @@ -0,0 +1,36 @@ +commands([ + Build::class, + ]); + } +} diff --git a/plugin/think-plugs-builder/src/command/Build.php b/plugin/think-plugs-builder/src/command/Build.php new file mode 100644 index 000000000..aba120630 --- /dev/null +++ b/plugin/think-plugs-builder/src/command/Build.php @@ -0,0 +1,69 @@ +setName('xadmin:builder') + ->addOption('name', '', Option::VALUE_OPTIONAL, '输出的 PHAR 文件名。', 'admin.phar') + ->addOption('main', '', Option::VALUE_OPTIONAL, '项目在包内的主入口文件。', 'think') + ->addOption('extract', '', Option::VALUE_OPTIONAL | Option::VALUE_IS_ARRAY, '首次启动时解压到外部目录的路径。', ['public', 'database']) + ->addOption('mount', '', Option::VALUE_OPTIONAL | Option::VALUE_IS_ARRAY, '运行时挂载到 PHAR 外部的文件或目录。', ['.env', 'runtime', 'safefile', 'public', 'database']) + ->addOption('exclude', '', Option::VALUE_OPTIONAL | Option::VALUE_IS_ARRAY, '额外排除的文件或目录。') + ->setDescription('将当前 ThinkAdmin 项目打包为可直接运行的 PHAR'); + } + + /** + * 仅在调试模式下允许执行打包。 + */ + public function isEnabled(): bool + { + return RuntimeService::isDebug(); + } + + /** + * 执行 PHAR 打包。 + */ + public function handle(): void + { + $builder = new PharBuilder($this->app, $this->output); + $target = $builder->build( + strval($this->input->getOption('name')), + strval($this->input->getOption('main')), + array_values(array_filter((array)$this->input->getOption('extract'), 'is_string')), + array_values(array_filter((array)$this->input->getOption('mount'), 'is_string')), + array_values(array_filter((array)$this->input->getOption('exclude'), 'is_string')), + ); + + $this->output->writeln("PHAR 打包完成: {$target}"); + $this->output->writeln('若运行时出现内存不足,请使用: php -d memory_limit=256M ' . basename($target) . ' [命令]'); + } +} diff --git a/plugin/think-plugs-builder/src/service/PharBuilder.php b/plugin/think-plugs-builder/src/service/PharBuilder.php new file mode 100644 index 000000000..5c9e3fb36 --- /dev/null +++ b/plugin/think-plugs-builder/src/service/PharBuilder.php @@ -0,0 +1,534 @@ + $extractDirs + * @param array $mountPaths + * @param array $extraExcludes + */ + public function build(string $name, string $main, array $extractDirs = [], array $mountPaths = [], array $extraExcludes = []): string + { + if (ini_get('phar.readonly') === '1') { + throw new \RuntimeException('phar.readonly is enabled, please run with -d phar.readonly=0'); + } + + $root = rtrim($this->app->getRootPath(), DIRECTORY_SEPARATOR); + if (!is_dir($root . DIRECTORY_SEPARATOR . 'vendor')) { + throw new \RuntimeException('The vendor directory is missing, please run composer install first.'); + } + + $main = trim($main, '\/'); + if (!is_file($root . DIRECTORY_SEPARATOR . $main)) { + throw new \RuntimeException("Main entry file not found: {$main}"); + } + + $target = $this->normalizeTarget($name, $root); + [$excludeNames, $excludePaths] = $this->collectExcludes($extraExcludes); + $workspace = $this->prepareStage($root, $excludeNames, $excludePaths); + $temp = $target . '.tmp'; + $alias = basename($target); + $stageRoot = $workspace . DIRECTORY_SEPARATOR . 'project'; + + try { + @unlink($temp); + @unlink($target); + is_dir(dirname($target)) || mkdir(dirname($target), 0777, true); + + $this->output->writeln("开始打包 PHAR {$target}"); + $phar = new \Phar($temp, 0, $alias); + $phar->startBuffering(); + + $this->addDirectory($phar, $stageRoot); + $phar->addFromString(self::BOOTSTRAP_FILE, PharRuntime::buildEntry($main, $extractDirs, $mountPaths)); + $phar->setStub("#!/usr/bin/env php\n" . $phar->createDefaultStub(self::BOOTSTRAP_FILE)); + $phar->setSignatureAlgorithm(\Phar::SHA256); + $phar->stopBuffering(); + + rename($temp, $target); + @chmod($target, 0755); + + return $target; + } finally { + $this->removePath($workspace); + } + } + + /** + * 准备临时分发目录。 + */ + private function prepareStage(string $root, array $excludeNames, array $excludePaths): string + { + $workspace = $this->createWorkspace(); + $stageRoot = $workspace . DIRECTORY_SEPARATOR . 'project'; + is_dir($stageRoot) || mkdir($stageRoot, 0777, true); + + $this->output->writeln("准备临时目录 {$stageRoot}"); + $this->stageRootFiles($root, $stageRoot, $excludeNames, $excludePaths); + $this->stageProjectDirectories($root, $stageRoot, $excludeNames, $excludePaths); + $this->stageVendor($root, $stageRoot, $excludeNames, $excludePaths); + $this->patchStageFiles($stageRoot); + + return $workspace; + } + + /** + * 拷贝根目录下需要打包的文件。 + */ + private function stageRootFiles(string $root, string $stageRoot, array $excludeNames, array $excludePaths): void + { + foreach ($this->includeFiles as $file) { + if ($this->shouldExclude($file, $excludeNames, $excludePaths)) { + continue; + } + + $source = $root . DIRECTORY_SEPARATOR . $file; + if (!is_file($source)) { + continue; + } + + $this->output->writeln(" - 复制文件 {$file}"); + $this->copyFile($source, $stageRoot . DIRECTORY_SEPARATOR . $file); + } + } + + /** + * 拷贝项目根目录下的业务目录。 + */ + private function stageProjectDirectories(string $root, string $stageRoot, array $excludeNames, array $excludePaths): void + { + foreach (scandir($root) ?: [] as $name) { + if ($name === '.' || $name === '..' || $name === 'vendor') { + continue; + } + + $source = $root . DIRECTORY_SEPARATOR . $name; + if (!is_dir($source) || $this->shouldExclude($name, $excludeNames, $excludePaths)) { + continue; + } + + $this->output->writeln(" - 复制目录 {$name}"); + $this->copyDirectory($source, $stageRoot . DIRECTORY_SEPARATOR . $name, $excludeNames, $excludePaths, $name); + } + } + + /** + * 拷贝 Composer 自动加载文件与依赖目录。 + */ + private function stageVendor(string $root, string $stageRoot, array $excludeNames, array $excludePaths): void + { + $vendorRoot = $root . DIRECTORY_SEPARATOR . 'vendor'; + $stageVendorRoot = $stageRoot . DIRECTORY_SEPARATOR . 'vendor'; + $composerRoot = $vendorRoot . DIRECTORY_SEPARATOR . 'composer'; + + $this->output->writeln(' - 复制 Composer 自动加载文件'); + $this->copyFile($vendorRoot . DIRECTORY_SEPARATOR . 'autoload.php', $stageVendorRoot . DIRECTORY_SEPARATOR . 'autoload.php'); + $this->copyFile($vendorRoot . DIRECTORY_SEPARATOR . 'services.php', $stageVendorRoot . DIRECTORY_SEPARATOR . 'services.php'); + + foreach (scandir($composerRoot) ?: [] as $name) { + if ($name === '.' || $name === '..') { + continue; + } + + $source = $composerRoot . DIRECTORY_SEPARATOR . $name; + if (!is_file($source)) { + continue; + } + + $logical = "vendor/composer/{$name}"; + if ($this->shouldExclude($logical, $excludeNames, $excludePaths)) { + continue; + } + + $this->copyFile($source, $stageVendorRoot . DIRECTORY_SEPARATOR . 'composer' . DIRECTORY_SEPARATOR . $name); + } + + if (is_dir($vendorRoot . DIRECTORY_SEPARATOR . 'bin')) { + $this->output->writeln(' - 复制 vendor/bin'); + $this->copyDirectory( + $vendorRoot . DIRECTORY_SEPARATOR . 'bin', + $stageVendorRoot . DIRECTORY_SEPARATOR . 'bin', + $excludeNames, + $excludePaths, + 'vendor/bin', + ); + } + + $copied = []; + foreach ($this->readInstalledPackages($composerRoot . DIRECTORY_SEPARATOR . 'installed.json') as $package) { + $installPath = strval($package['install-path'] ?? ''); + if ($installPath === '') { + continue; + } + + $sourcePath = $this->resolvePath($composerRoot, $installPath); + $targetPath = $this->resolvePath($stageVendorRoot . DIRECTORY_SEPARATOR . 'composer', $installPath); + $logical = $this->relativeTo($stageRoot, $targetPath); + + if ($logical === '' || isset($copied[$logical]) || $this->shouldExclude($logical, $excludeNames, $excludePaths)) { + continue; + } + + if (is_dir($sourcePath)) { + $realSource = realpath($sourcePath) ?: $sourcePath; + $this->output->writeln(" - 复制依赖 {$package['name']}"); + $this->copyDirectory($realSource, $targetPath, $excludeNames, $excludePaths, $logical); + $copied[$logical] = true; + } elseif (is_file($sourcePath)) { + $this->output->writeln(" - 复制依赖文件 {$package['name']}"); + $this->copyFile($sourcePath, $targetPath); + $copied[$logical] = true; + } + } + } + + /** + * 将临时目录中的全部文件写入 PHAR。 + */ + private function addDirectory(\Phar $phar, string $root): void + { + $root = rtrim($root, DIRECTORY_SEPARATOR); + $iterator = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator($root, \FilesystemIterator::SKIP_DOTS), + \RecursiveIteratorIterator::LEAVES_ONLY + ); + $phar->buildFromIterator($iterator, $root); + } + + /** + * 读取 Composer 已安装依赖清单。 + */ + private function readInstalledPackages(string $path): array + { + if (!is_file($path)) { + throw new \RuntimeException('The vendor/composer/installed.json file is missing.'); + } + + $installed = json_decode(file_get_contents($path) ?: '[]', true, 512, JSON_THROW_ON_ERROR); + $packages = isset($installed['packages']) && is_array($installed['packages']) ? $installed['packages'] : $installed; + + return array_values(array_filter($packages, static fn ($package): bool => is_array($package))); + } + + /** + * 拷贝目录,支持基于逻辑路径的排除规则。 + */ + private function copyDirectory( + string $source, + string $target, + array $excludeNames, + array $excludePaths, + string $logicalPrefix = '', + ): void { + $logicalPrefix = trim($this->normalizeRelativePath($logicalPrefix), '/'); + if ($logicalPrefix !== '' && $this->shouldExclude($logicalPrefix, $excludeNames, $excludePaths)) { + return; + } + + is_dir($target) || mkdir($target, 0777, true); + $source = rtrim($source, DIRECTORY_SEPARATOR); + + $filter = new \RecursiveCallbackFilterIterator( + new \RecursiveDirectoryIterator($source, \FilesystemIterator::SKIP_DOTS), + function (\SplFileInfo $item) use ($source, $logicalPrefix, $excludeNames, $excludePaths): bool { + $relative = $this->relativeTo($source, $item->getPathname()); + $logical = trim($logicalPrefix === '' ? $relative : "{$logicalPrefix}/{$relative}", '/'); + + return $logical === '' || !$this->shouldExclude($logical, $excludeNames, $excludePaths); + } + ); + + $iterator = new \RecursiveIteratorIterator($filter, \RecursiveIteratorIterator::SELF_FIRST); + foreach ($iterator as $item) { + $relative = $this->relativeTo($source, $item->getPathname()); + if ($relative === '') { + continue; + } + + $destination = $target . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $relative); + if ($item->isDir()) { + is_dir($destination) || mkdir($destination, 0777, true); + continue; + } + + $this->copyFile($item->getPathname(), $destination); + } + } + + /** + * 拷贝单个文件。 + */ + private function copyFile(string $source, string $target): void + { + if (!is_file($source)) { + return; + } + + is_dir(dirname($target)) || mkdir(dirname($target), 0777, true); + copy($source, $target); + } + + /** + * 修补已知的 PHAR 兼容性问题。 + */ + private function patchStageFiles(string $stageRoot): void + { + $patches = [ + 'vendor/topthink/framework/src/think/App.php' => [ + '$this->thinkPath = realpath(dirname(__DIR__)) . DIRECTORY_SEPARATOR;' => '$this->thinkPath = (realpath(dirname(__DIR__)) ?: dirname(__DIR__)) . DIRECTORY_SEPARATOR;', + '$this->rootPath = $this->getRootPath();' => '$this->rootPath = runpath();', + '$this->runtimePath = $this->rootPath . \'runtime\' . DIRECTORY_SEPARATOR;' => '$this->runtimePath = rtrim(runpath(\'runtime\'), \'\\\/\') . DIRECTORY_SEPARATOR;', + " if (is_dir(\$configPath)) {\n \$files = glob(\$configPath . '*' . \$this->configExt);\n }\n" => " if (is_dir(\$configPath)) {\n foreach (new \\DirectoryIterator(\$configPath) as \$item) {\n if (\$item->isFile() && \$item->getExtension() === ltrim(\$this->configExt, '.')) {\n \$files[] = \$item->getPathname();\n }\n }\n }\n", + ], + 'vendor/topthink/framework/src/think/Http.php' => [ + " if (is_dir(\$routePath)) {\n \$files = glob(\$routePath . '*.php');\n foreach (\$files as \$file) {\n include \$file;\n }\n }\n" => " if (is_dir(\$routePath)) {\n foreach (new \\DirectoryIterator(\$routePath) as \$item) {\n if (\$item->isFile() && \$item->getExtension() === 'php') {\n include \$item->getPathname();\n }\n }\n }\n", + ], + 'vendor/topthink/framework/src/think/Lang.php' => [ + " \$files = glob(\$this->app->getAppPath() . 'lang' . DIRECTORY_SEPARATOR . \$langset . '.*');\n \$this->load(\$files);\n" => " \$files = [];\n \$langPath = \$this->app->getAppPath() . 'lang' . DIRECTORY_SEPARATOR;\n if (is_dir(\$langPath)) {\n foreach (new \\DirectoryIterator(\$langPath) as \$item) {\n if (\$item->isFile() && str_starts_with(\$item->getFilename(), \$langset . '.')) {\n \$files[] = \$item->getPathname();\n }\n }\n }\n \$this->load(\$files);\n", + ], + 'vendor/topthink/framework/src/think/route/Domain.php' => [ + " if (is_dir(\$routePath)) {\n \$dirs = glob(\$routePath . '*', GLOB_ONLYDIR);\n foreach (\$dirs as \$dir) {\n" => " if (is_dir(\$routePath)) {\n \$dirs = [];\n foreach (new \\DirectoryIterator(\$routePath) as \$item) {\n if (\$item->isDir() && !\$item->isDot()) {\n \$dirs[] = \$item->getPathname();\n }\n }\n foreach (\$dirs as \$dir) {\n", + ], + 'vendor/topthink/framework/src/think/route/RuleGroup.php' => [ + " if (is_dir(\$routePath)) {\n // 动态加载分组路由\n \$files = glob(\$routePath . '*.php');\n foreach (\$files as \$file) {\n include_once \$file;\n }\n\n // 自动扫描下级分组\n \$dirs = \$this->config('route_auto_group') ? glob(\$routePath . '*', GLOB_ONLYDIR) : [];\n foreach (\$dirs as \$dir) {\n" => " if (is_dir(\$routePath)) {\n // 动态加载分组路由\n \$files = [];\n \$dirs = [];\n foreach (new \\DirectoryIterator(\$routePath) as \$item) {\n if (\$item->isFile() && \$item->getExtension() === 'php') {\n \$files[] = \$item->getPathname();\n }\n if (\$item->isDir() && !\$item->isDot()) {\n \$dirs[] = \$item->getPathname();\n }\n }\n foreach (\$files as \$file) {\n include_once \$file;\n }\n\n // 自动扫描下级分组\n if (!\$this->config('route_auto_group')) {\n \$dirs = [];\n }\n foreach (\$dirs as \$dir) {\n", + ], + ]; + + foreach ($patches as $relative => $replaces) { + $file = $stageRoot . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $relative); + if (!is_file($file)) { + continue; + } + + $content = file_get_contents($file); + if (!is_string($content) || $content === '') { + continue; + } + + $patched = str_replace(array_keys($replaces), array_values($replaces), $content, $count); + if ($count > 0) { + $this->output->writeln(" - 修补兼容文件 {$relative}"); + file_put_contents($file, $patched); + } + } + } + + /** + * 收集排除规则,兼容按名称排除和按路径排除两种写法。 + * + * @param array $extraExcludes + * @return array{0:array,1:array} + */ + private function collectExcludes(array $extraExcludes): array + { + $excludeNames = $this->excludeNames; + $excludePaths = $this->excludePaths; + + foreach ($extraExcludes as $rule) { + $rule = trim($this->normalizeRelativePath($rule), '/'); + if ($rule === '') { + continue; + } + + if (str_contains($rule, '/')) { + $excludePaths[] = $rule; + } else { + $excludeNames[] = $rule; + } + } + + return [ + array_values(array_unique($excludeNames)), + array_values(array_unique($excludePaths)), + ]; + } + + /** + * 判断逻辑路径是否需要排除。 + */ + private function shouldExclude(string $relative, array $excludeNames, array $excludePaths): bool + { + $relative = trim($this->normalizeRelativePath($relative), '/'); + if ($relative === '') { + return false; + } + + $parts = array_values(array_filter(explode('/', $relative), 'strlen')); + foreach ($parts as $part) { + if (in_array($part, $excludeNames, true)) { + return true; + } + } + + foreach ($excludePaths as $path) { + $path = trim($this->normalizeRelativePath($path), '/'); + if ($path !== '' && ($relative === $path || str_starts_with($relative, "{$path}/"))) { + return true; + } + } + + return false; + } + + /** + * 解析相对路径并返回绝对路径。 + */ + private function resolvePath(string $base, string $path): string + { + $path = str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $path); + if (preg_match('#^(?:[A-Za-z]:\\\|/)#', $path) === 1) { + return $path; + } + + $full = $base . DIRECTORY_SEPARATOR . $path; + $prefix = ''; + if (preg_match('#^[A-Za-z]:\\\#', $full) === 1) { + $prefix = substr($full, 0, 3); + $full = substr($full, 3); + } elseif (str_starts_with($full, DIRECTORY_SEPARATOR)) { + $prefix = DIRECTORY_SEPARATOR; + $full = ltrim($full, DIRECTORY_SEPARATOR); + } + + $parts = []; + foreach (explode(DIRECTORY_SEPARATOR, $full) as $part) { + if ($part === '' || $part === '.') { + continue; + } + if ($part === '..') { + array_pop($parts); + continue; + } + $parts[] = $part; + } + + return $prefix . implode(DIRECTORY_SEPARATOR, $parts); + } + + /** + * 计算目标路径相对于基准目录的逻辑路径。 + */ + private function relativeTo(string $root, string $path): string + { + $root = rtrim($this->normalizePath($root), '/'); + $path = $this->normalizePath($path); + + return ltrim(substr($path, strlen($root)), '/'); + } + + /** + * 标准化相对路径分隔符。 + */ + private function normalizeRelativePath(string $path): string + { + return str_replace('\\', '/', trim($path)); + } + + /** + * 标准化绝对路径分隔符。 + */ + private function normalizePath(string $path): string + { + return str_replace('\\', '/', $path); + } + + /** + * 创建临时工作目录。 + */ + private function createWorkspace(): string + { + $workspace = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'thinkadmin-phar-' . uniqid('', true); + is_dir($workspace) || mkdir($workspace, 0777, true); + + return $workspace; + } + + /** + * 递归删除目录或文件。 + */ + private function removePath(string $path): void + { + if (is_file($path) || is_link($path)) { + @unlink($path); + return; + } + + if (!is_dir($path)) { + return; + } + + $iterator = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator($path, \FilesystemIterator::SKIP_DOTS), + \RecursiveIteratorIterator::CHILD_FIRST + ); + + foreach ($iterator as $item) { + if ($item->isDir()) { + @rmdir($item->getPathname()); + } else { + @unlink($item->getPathname()); + } + } + + @rmdir($path); + } + + /** + * 标准化输出文件名。 + */ + private function normalizeTarget(string $name, string $root): string + { + $name = trim($name); + if ($name === '') { + $name = 'admin.phar'; + } + + if (preg_match('#^(?:[A-Za-z]:[\\\/]|/)#', $name) === 1) { + return $name; + } + + return $root . DIRECTORY_SEPARATOR . $name; + } +} diff --git a/plugin/think-plugs-builder/src/service/PharRuntime.php b/plugin/think-plugs-builder/src/service/PharRuntime.php new file mode 100644 index 000000000..4eb67a8df --- /dev/null +++ b/plugin/think-plugs-builder/src/service/PharRuntime.php @@ -0,0 +1,196 @@ + $extractDirs + * @param array $mountPaths + */ + public static function buildEntry(string $main, array $extractDirs, array $mountPaths): string + { + $mountFiles = []; + $mountDirs = []; + + foreach ($mountPaths as $path) { + $path = str_replace('\\', '/', trim($path)); + if ($path === '') { + continue; + } + + if ($path === '.env' || pathinfo($path, PATHINFO_EXTENSION) !== '') { + $mountFiles[] = trim($path, '/'); + } else { + $mountDirs[] = trim($path, '/'); + } + } + + $extractDirs = array_values(array_unique(array_map(static function (string $path): string { + return trim(str_replace('\\', '/', $path), '/'); + }, array_filter($extractDirs, 'strlen')))); + $mountFiles = array_values(array_unique($mountFiles)); + $mountDirs = array_values(array_unique($mountDirs)); + + $exportMain = var_export($main, true); + $exportExtract = var_export($extractDirs, true); + $exportMountFiles = var_export($mountFiles, true); + $exportMountDirs = var_export($mountDirs, true); + + return << 0 && \$n < 256 * 1024 * 1024) { + fwrite(STDERR, 'Warning: memory_limit may be too low for this PHAR. Run: php -d memory_limit=256M ' . basename(\$archive) . PHP_EOL); + } + } + + \$installRoot = dirname(\$archive); + \$installRoot = rtrim(\$installRoot, '\\\\/') . DIRECTORY_SEPARATOR; + \$archiveRoot = 'phar://' . \$archive; + \$extractDirs = {$exportExtract}; + \$mountFiles = {$exportMountFiles}; + \$mountDirs = {$exportMountDirs}; + + chdir(\$installRoot); + + \$normalize = static function (string \$path) use (\$installRoot): string { + \$path = str_replace(['/', '\\\\'], DIRECTORY_SEPARATOR, \$path); + return \$installRoot . ltrim(\$path, DIRECTORY_SEPARATOR); + }; + + \$ensureDir = static function (string \$path): void { + if (!is_dir(\$path)) { + mkdir(\$path, 0777, true); + } + }; + + \$syncRuntimeEnv = static function () use (\$normalize, \$ensureDir): void { + \$source = \$normalize('.env'); + \$target = \$normalize('runtime/.env'); + if (!is_file(\$source)) { + return; + } + + \$ensureDir(dirname(\$target)); + if (!is_file(\$target) || md5_file(\$target) !== md5_file(\$source)) { + copy(\$source, \$target); + } + }; + + \$copyMissing = static function (string \$source, string \$target) use (&\$copyMissing, \$ensureDir): void { + if (is_dir(\$source)) { + \$ensureDir(\$target); + \$iterator = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator(\$source, FilesystemIterator::SKIP_DOTS), + RecursiveIteratorIterator::SELF_FIRST + ); + + foreach (\$iterator as \$item) { + \$relative = substr(str_replace(['/', '\\\\'], '/', \$item->getPathname()), strlen(str_replace(['/', '\\\\'], '/', \$source))); + \$relative = ltrim(\$relative, '/'); + \$destination = rtrim(\$target, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, \$relative); + if (\$item->isDir()) { + \$ensureDir(\$destination); + } elseif (!is_file(\$destination)) { + \$ensureDir(dirname(\$destination)); + copy(\$item->getPathname(), \$destination); + } + } + return; + } + + if (is_file(\$source) && !is_file(\$target)) { + \$ensureDir(dirname(\$target)); + copy(\$source, \$target); + } + }; + + if (!is_file(\$normalize('.env'))) { + if (is_file(\$archiveRoot . '/.env.example')) { + \$copyMissing(\$archiveRoot . '/.env.example', \$normalize('.env')); + } else { + \$ensureDir(dirname(\$normalize('.env'))); + file_put_contents(\$normalize('.env'), ''); + } + } + + foreach (['public', 'runtime'] as \$dir) { + \$ensureDir(\$normalize(\$dir)); + } + \$syncRuntimeEnv(); + + foreach (\$extractDirs as \$dir) { + if (\$dir === '') { + continue; + } + \$copyMissing(\$archiveRoot . '/' . \$dir, \$normalize(\$dir)); + } + + foreach (\$mountDirs as \$dir) { + if (\$dir === '') { + continue; + } + \$target = \$normalize(\$dir); + \$ensureDir(\$target); + \\Phar::mount(\$dir, \$target); + } + + foreach (\$mountFiles as \$file) { + if (\$file === '') { + continue; + } + \$target = \$normalize(\$file); + \$ensureDir(dirname(\$target)); + if (!is_file(\$target)) { + file_put_contents(\$target, ''); + } + \\Phar::mount(\$file, \$target); + } + + \$syncRuntimeEnv(); +})(); + +require __DIR__ . DIRECTORY_SEPARATOR . {$exportMain}; +PHP; + } +} diff --git a/plugin/think-plugs-install/composer.json b/plugin/think-plugs-install/composer.json new file mode 100644 index 000000000..9d6c9ed60 --- /dev/null +++ b/plugin/think-plugs-install/composer.json @@ -0,0 +1,55 @@ +{ + "type": "composer-plugin", + "name": "zoujingli/think-plugs-install", + "version": "8.0.x-dev", + "license": "MIT", + "homepage": "https://thinkadmin.top", + "description": "Composer-driven install workflow for ThinkAdmin", + "authors": [ + { + "name": "Anyon", + "email": "zoujingli@qq.com" + } + ], + "require": { + "composer-plugin-api": "^2.6", + "php": "^8.1", + "topthink/framework": "^8.1", + "zoujingli/think-library": "^8.0", + "zoujingli/think-plugs-system": "^8.0" + }, + "autoload": { + "psr-4": { + "plugin\\install\\": "src" + } + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "autoload-dev": { + "psr-4": { + "plugin\\install\\tests\\": "tests/" + } + }, + "extra": { + "class": "plugin\\install\\composer\\Plugin", + "think": { + "services": [ + "plugin\\install\\Service" + ] + }, + "xadmin": { + "app": { + "name": "安装工具", + "description": "提供 Composer 安装链路与显式安装入口,统一执行服务发现、资源发布与按需数据库迁移。", + "code": "install", + "prefix": "install" + } + } + }, + "prefer-stable": true, + "minimum-stability": "dev", + "config": { + "sort-packages": true + } +} diff --git a/plugin/think-plugs-install/readme.api.md b/plugin/think-plugs-install/readme.api.md new file mode 100644 index 000000000..22fdb942b --- /dev/null +++ b/plugin/think-plugs-install/readme.api.md @@ -0,0 +1,34 @@ +# 安装工具接口 + +## 接口标准 +- 接口类型:命令行接口 + Composer 生命周期接口 +- 调用入口:`php think xadmin:install` 或 Composer 自动安装钩子 +- 返回形式:控制台文本输出 +- 执行链路:先执行 `service:discover`,再执行 `xadmin:publish` 或 `xadmin:publish --migrate` +- 迁移规则:只有声明了 `composer.json > extra.xadmin.migrate` 的插件才会自动执行数据库迁移 +- 首装规则:首次 `composer install` 只发布资源和迁移脚本,不直接执行数据库迁移 +- 状态文件:`vendor/.plugin-state.json` + +## 接口列表 + +### `xadmin:install` +- 说明:显式安装当前项目,完成服务发现、插件资源发布,并对已声明 `extra.xadmin.migrate` 的插件自动执行数据库迁移 + +```jsonc +{ + "force": false, // 发布时是否覆盖已存在文件 + "migrate": false // 是否强制执行所有已发布迁移,即使当前未命中自动迁移条件 +} +``` + +### `composer install / update / remove / dump-autoload` +- 说明:由 `zoujingli/think-plugs-install` Composer plugin 自动触发安装链路;首次安装只执行发布,后续新增或变更的插件如果声明了 `extra.xadmin.migrate`,会自动执行数据库迁移;未声明时仅输出提示 + +```jsonc +{ + "allow_plugin": true, // 根项目 composer.json 需要允许 zoujingli/think-plugs-install 作为 Composer plugin 运行 + "auto_publish": true, // 自动执行 service:discover + xadmin:publish + "auto_migrate": true, // 仅对非首次安装且本次变更中声明了 extra.xadmin.migrate 的插件自动执行 + "initial_install": false // 首次安装时固定为只发布不迁移 +} +``` diff --git a/plugin/think-plugs-install/readme.md b/plugin/think-plugs-install/readme.md new file mode 100644 index 000000000..9f233568c --- /dev/null +++ b/plugin/think-plugs-install/readme.md @@ -0,0 +1,19 @@ +# 安装工具 + +## 定位 +- 组件编码:`install` +- 组件包名:`zoujingli/think-plugs-install` +- 主要职责:作为 Composer plugin 与显式命令入口,统一调度服务发现、资源发布和按需数据库迁移 + +## 边界 +- 仅暴露命令接口,不提供 HTTP API +- 主要入口:`php think xadmin:install` +- 本组件只负责安装链路协调,不直接维护业务资源或迁移脚本内容 +- 包类型为 `composer-plugin`,由 Composer 自动加载 `plugin\\install\\composer\\Plugin` +- 根项目 `composer install/update/remove/dump-autoload` 后会自动执行同一条安装链路 +- 首次 `composer install` 只执行服务发现与资源发布,不直接建表 +- 自动迁移只对声明了 `extra.xadmin.migrate` 的插件生效,未声明时只输出提示 +- 运行时会维护安装快照文件:`vendor/.plugin-state.json` + +## 文档 +- 命令接口说明:`readme.api.md` diff --git a/plugin/think-plugs-install/src/Service.php b/plugin/think-plugs-install/src/Service.php new file mode 100644 index 000000000..7278d20a9 --- /dev/null +++ b/plugin/think-plugs-install/src/Service.php @@ -0,0 +1,34 @@ +commands([InstallCommand::class]); + } +} diff --git a/plugin/think-plugs-install/src/command/project/InstallCommand.php b/plugin/think-plugs-install/src/command/project/InstallCommand.php new file mode 100644 index 000000000..f88f7a936 --- /dev/null +++ b/plugin/think-plugs-install/src/command/project/InstallCommand.php @@ -0,0 +1,71 @@ +setName('xadmin:install'); + $this->addOption('force', 'f', Option::VALUE_NONE, 'Overwrite any existing files while publishing'); + $this->addOption('migrate', 'm', Option::VALUE_NONE, 'Force all published database migrations after publishing'); + $this->setDescription('Discover services, publish plugin assets, and auto-run configured plugin migrations'); + } + + /** + * 执行显式安装流程。 + */ + public function execute(Input $input, Output $output): int + { + $service = new ComposerLifecycleService( + strval($this->app->getRootPath()), + function (string $message): void { + $this->output->writeln("{$message}"); + }, + function (array $command, string $cwd = '') use ($output): int { + $args = array_values(array_slice($command, 2)); + $name = strval(array_shift($args) ?? ''); + if ($name === '') { + return 1; + } + + return $this->app->console->find($name)->run(new Input($args), $output); + } + ); + + return $service->installFromCommand( + boolval($input->getOption('force')), + boolval($input->getOption('migrate')) + ); + } +} diff --git a/plugin/think-plugs-install/src/composer/Plugin.php b/plugin/think-plugs-install/src/composer/Plugin.php new file mode 100644 index 000000000..4506715eb --- /dev/null +++ b/plugin/think-plugs-install/src/composer/Plugin.php @@ -0,0 +1,87 @@ +composer = $composer; + $this->io = $io; + } + + public function deactivate(Composer $composer, IOInterface $io): void + { + } + + public function uninstall(Composer $composer, IOInterface $io): void + { + } + + /** + * @return array + */ + public static function getSubscribedEvents(): array + { + return [ + 'post-autoload-dump' => 'onPostAutoloadDump', + ]; + } + + /** + * Composer 自动发现服务、发布资源并按需执行迁移。 + */ + public function onPostAutoloadDump($event = null): void + { + $service = new ComposerLifecycleService( + dirname(str_replace('\\', '/', strval($this->composer->getConfig()->get('vendor-dir')))), + function (string $message): void { + if ($message !== '') { + $this->io->write("{$message}"); + } + } + ); + + $status = $service->syncAfterComposer(); + if ($status !== 0) { + throw new \RuntimeException("ThinkAdmin composer install hook failed with status {$status}."); + } + } + } +} diff --git a/plugin/think-plugs-install/src/service/ComposerLifecycleService.php b/plugin/think-plugs-install/src/service/ComposerLifecycleService.php new file mode 100644 index 000000000..200c60118 --- /dev/null +++ b/plugin/think-plugs-install/src/service/ComposerLifecycleService.php @@ -0,0 +1,516 @@ +, string):int + */ + private $runner; + + /** + * @param null|callable(string):void $writer + * @param null|callable(array, string):int $runner + */ + public function __construct(private readonly string $rootPath, ?callable $writer = null, ?callable $runner = null) + { + $this->writer = $writer ?? static function (string $message): void { + }; + $this->runner = $runner ?? [$this, 'runProcess']; + } + + /** + * 处理 Composer 自动安装链路。 + */ + public function syncAfterComposer(): int + { + $current = $this->discoverInstalledPlugins(); + $previous = $this->readState(); + $plan = $this->buildComposerPlan($previous, $current, is_file($this->stateFile())); + $status = $this->runThinkInstall(boolval($plan['migrate'] ?? false)); + + if ($status !== 0) { + return $status; + } + + $this->writeComposerMessages($plan); + $this->writeState($current); + return 0; + } + + /** + * 处理显式安装命令。 + */ + public function installFromCommand(bool $force = false, bool $forceMigrate = false): int + { + $current = $this->discoverInstalledPlugins(); + $plan = $this->buildInstallPlan($current, $forceMigrate); + $status = $this->runThinkInstall(boolval($plan['migrate'] ?? false), $force); + + if ($status !== 0) { + return $status; + } + + $this->writeInstallMessages($plan); + $this->writeState($current); + return 0; + } + + /** + * 构建 Composer 自动迁移计划。 + * @param array> $previous + * @param array> $current + * @param bool $initialized 是否存在历史安装快照 + * @return array + */ + public function buildComposerPlan(array $previous, array $current, bool $initialized = true): array + { + $installed = []; + $updated = []; + $removed = []; + + foreach ($current as $name => $package) { + if (!isset($previous[$name])) { + $installed[$name] = $package; + continue; + } + if (strval($previous[$name]['signature'] ?? '') !== strval($package['signature'] ?? '')) { + $updated[$name] = $package; + } + } + + foreach ($previous as $name => $package) { + if (!isset($current[$name])) { + $removed[$name] = $package; + } + } + + $changed = $installed + $updated; + $configured = $this->filterConfiguredMigrations($changed); + + return [ + 'initial' => !$initialized, + 'installed' => $installed, + 'updated' => $updated, + 'removed' => $removed, + 'configured' => $configured, + 'missing' => $this->filterMissingMigrations($changed), + 'removed_configured' => $this->filterConfiguredMigrations($removed), + 'migrate' => $initialized && $configured !== [], + ]; + } + + /** + * 构建显式安装计划。 + * @param array> $current + * @return array + */ + public function buildInstallPlan(array $current, bool $forceMigrate = false): array + { + $configured = $this->filterConfiguredMigrations($current); + + return [ + 'configured' => $configured, + 'forced' => $forceMigrate, + 'migrate' => $forceMigrate || $configured !== [], + ]; + } + + /** + * 发现当前已安装的业务插件。 + * @return array> + */ + public function discoverInstalledPlugins(): array + { + $packages = []; + $workspacePackages = []; + $file = $this->rootPath() . '/vendor/composer/installed.json'; + + foreach ($this->discoverWorkspacePlugins() as $package) { + $name = strval($package['name'] ?? ''); + if ($name !== '') { + $workspacePackages[$name] = $package; + } + } + + if (is_file($file)) { + $data = json_decode((string)file_get_contents($file), true); + $items = $data['packages'] ?? $data; + if (is_array($items)) { + $base = dirname($file); + foreach ($items as $item) { + if (!is_array($item) || strval($item['type'] ?? '') !== 'think-admin-plugin') { + continue; + } + + $name = strval($item['name'] ?? ''); + if ($name === '') { + continue; + } + + if (isset($workspacePackages[$name])) { + $workspace = $workspacePackages[$name]; + $packages[$name] = $this->buildPackageRecord( + $workspace, + $item, + strval($workspace['__path'] ?? '') + ); + unset($workspacePackages[$name]); + continue; + } + + $path = $this->resolvePackagePath($base, strval($item['install-path'] ?? '')); + $manifest = $this->readManifest($path . '/composer.json'); + if ($manifest === []) { + $manifest = $item; + } + + $packages[$name] = $this->buildPackageRecord($manifest, $item, $path); + } + } + } + + foreach ($workspacePackages as $name => $workspace) { + $packages[$name] = $this->buildPackageRecord( + $workspace, + $workspace, + strval($workspace['__path'] ?? '') + ); + } + + ksort($packages); + return $packages; + } + + /** + * 读取已保存的安装快照。 + * @return array> + */ + public function readState(): array + { + $file = $this->stateFile(); + if (!is_file($file)) { + return []; + } + + $data = json_decode((string)file_get_contents($file), true); + return is_array($data) ? $this->normalizeState($data) : []; + } + + /** + * 写入安装快照。 + * @param array> $state + */ + public function writeState(array $state): void + { + is_dir($dir = dirname($this->stateFile())) || mkdir($dir, 0777, true); + $state = $this->normalizeState($state); + ksort($state); + file_put_contents($this->stateFile(), json_encode($state, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . PHP_EOL); + } + + /** + * 执行 ThinkAdmin 安装链路。 + */ + private function runThinkInstall(bool $migrate, bool $force = false): int + { + if (!is_file($think = $this->rootPath() . '/think')) { + $this->write('Skip ThinkAdmin install hook: think entry not found.'); + return 0; + } + + $this->write('Run ThinkAdmin service discovery...'); + $status = $this->runCommand(['service:discover']); + if ($status !== 0) { + return $status; + } + + $command = ['xadmin:publish']; + if ($force) { + $command[] = '--force'; + } + if ($migrate) { + $command[] = '--migrate'; + } + + $this->write($migrate ? 'Run ThinkAdmin publish and migrate...' : 'Run ThinkAdmin publish...'); + return $this->runCommand($command); + } + + /** + * 输出 Composer 自动链路消息。 + * @param array $plan + */ + private function writeComposerMessages(array $plan): void + { + $configured = array_values($plan['configured'] ?? []); + $missing = array_values($plan['missing'] ?? []); + $removed = array_values($plan['removed_configured'] ?? []); + $initial = !empty($plan['initial']); + + if ($configured !== []) { + if ($initial) { + $this->write('Skip automatic database migration on initial Composer install. Run `php think xadmin:install` after configuring the environment: ' . implode(', ', array_map(fn(array $item): string => strval($item['title'] ?? $item['name'] ?? ''), $configured))); + } else { + $this->write('Auto migrate plugins: ' . implode(', ', array_map(fn(array $item): string => strval($item['title'] ?? $item['name'] ?? ''), $configured))); + } + } + if ($missing !== []) { + $this->write('Skip database migration for plugins without extra.xadmin.migrate: ' . implode(', ', array_map(fn(array $item): string => strval($item['title'] ?? $item['name'] ?? ''), $missing))); + } + if ($removed !== []) { + $this->write('Removed plugins keep historical tables; rollback is not automatic: ' . implode(', ', array_map(fn(array $item): string => strval($item['title'] ?? $item['name'] ?? ''), $removed))); + } + } + + /** + * 输出显式安装消息。 + * @param array $plan + */ + private function writeInstallMessages(array $plan): void + { + $configured = array_values($plan['configured'] ?? []); + $forced = !empty($plan['forced']); + + if ($forced) { + $this->write('Force database migration for all published scripts.'); + return; + } + + if ($configured === []) { + $this->write('Skip database migration: no installed plugin declares extra.xadmin.migrate.'); + return; + } + + $this->write('Install database migrations for: ' . implode(', ', array_map(fn(array $item): string => strval($item['title'] ?? $item['name'] ?? ''), $configured))); + } + + /** + * 过滤已配置迁移的插件。 + * @param array> $packages + * @return array> + */ + private function filterConfiguredMigrations(array $packages): array + { + return array_filter($packages, static function (array $package): bool { + return !empty($package['migrate']['configured']); + }); + } + + /** + * 过滤未配置迁移的插件。 + * @param array> $packages + * @return array> + */ + private function filterMissingMigrations(array $packages): array + { + return array_filter($packages, static function (array $package): bool { + return empty($package['migrate']['configured']); + }); + } + + /** + * 执行单个 Think 命令。 + * @param array $arguments + */ + private function runCommand(array $arguments): int + { + return intval(call_user_func($this->runner, array_merge([PHP_BINARY, $this->rootPath() . '/think'], $arguments), $this->rootPath())); + } + + /** + * 默认进程执行器。 + * @param array $command + */ + private function runProcess(array $command, string $cwd): int + { + $stdin = defined('STDIN') && is_resource(STDIN) ? STDIN : fopen('php://stdin', 'r'); + $stdout = defined('STDOUT') && is_resource(STDOUT) ? STDOUT : fopen('php://stdout', 'w'); + $stderr = defined('STDERR') && is_resource(STDERR) ? STDERR : fopen('php://stderr', 'w'); + + $process = proc_open($command, [0 => $stdin, 1 => $stdout, 2 => $stderr], $pipes, $cwd); + return is_resource($process) ? proc_close($process) : 1; + } + + /** + * 解析安装目录。 + */ + private function resolvePackagePath(string $base, string $path): string + { + $pathname = $path === '' ? '' : $base . '/' . ltrim(str_replace('\\', '/', $path), '/'); + $real = $pathname === '' ? false : realpath($pathname); + return str_replace('\\', '/', $real ?: $pathname); + } + + /** + * 扫描当前工作区本地插件。 + * @return array> + */ + private function discoverWorkspacePlugins(): array + { + $items = []; + $pluginRoot = $this->rootPath() . '/plugin'; + if (!is_dir($pluginRoot)) { + return $items; + } + + foreach (glob($pluginRoot . '/*/composer.json') ?: [] as $file) { + $manifest = $this->readManifest($file); + if (($manifest['type'] ?? '') !== 'think-admin-plugin' || empty($manifest['name'])) { + continue; + } + + $manifest['__path'] = str_replace('\\', '/', dirname($file)); + $items[] = $manifest; + } + + return $items; + } + + /** + * 标准化单个插件记录。 + * @param array $manifest + * @param array $package + * @return array + */ + private function buildPackageRecord(array $manifest, array $package, string $path): array + { + $name = strval($manifest['name'] ?? ($package['name'] ?? '')); + $title = strval($manifest['extra']['xadmin']['app']['name'] ?? $name); + $migrate = is_array($manifest['extra']['xadmin']['migrate'] ?? null) ? $manifest['extra']['xadmin']['migrate'] : []; + $migrateFile = strval($migrate['file'] ?? ''); + $migratePath = $migrateFile === '' ? '' : rtrim($path, '/\\') . '/stc/database/' . ltrim($migrateFile, '/'); + + return [ + 'name' => $name, + 'title' => $title, + 'migrate' => [ + 'configured' => $migrate !== [], + 'file' => $migrateFile, + 'class' => strval($migrate['class'] ?? ''), + 'name' => strval($migrate['name'] ?? ''), + ], + 'signature' => sha1(json_encode([ + 'version' => strval($manifest['version'] ?? ($package['version'] ?? '')), + 'reference' => strval($package['dist']['reference'] ?? ($package['source']['reference'] ?? '')), + 'manifest' => $this->fileSha1(rtrim($path, '/\\') . '/composer.json'), + 'migrate' => [ + 'file' => $migrateFile, + 'class' => strval($migrate['class'] ?? ''), + 'name' => strval($migrate['name'] ?? ''), + 'content' => $this->fileSha1($migratePath), + ], + ], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?: $name), + ]; + } + + /** + * 归一化安装状态快照。 + * @param array $state + * @return array> + */ + private function normalizeState(array $state): array + { + $result = []; + + foreach ($state as $name => $item) { + if (!is_array($item)) { + continue; + } + + $package = strval($item['name'] ?? $name); + if ($package === '') { + continue; + } + + $result[$package] = [ + 'name' => $package, + 'title' => strval($item['title'] ?? $package), + 'signature' => strval($item['signature'] ?? ''), + 'migrate' => [ + 'configured' => !empty($item['migrate']['configured']), + ], + ]; + } + + return $result; + } + + /** + * 读取包清单。 + * @return array + */ + private function readManifest(string $file): array + { + if (!is_file($file)) { + return []; + } + + $data = json_decode((string)file_get_contents($file), true); + return is_array($data) ? $data : []; + } + + /** + * 计算文件散列。 + */ + private function fileSha1(string $file): string + { + return is_file($file) ? strval(sha1_file($file) ?: '') : ''; + } + + /** + * 输出单行消息。 + */ + private function write(string $message): void + { + call_user_func($this->writer, trim($message)); + } + + /** + * 获取项目根目录。 + */ + private function rootPath(): string + { + return rtrim(str_replace('\\', '/', $this->rootPath), '/'); + } + + /** + * 获取状态文件路径。 + */ + private function stateFile(): string + { + return $this->rootPath() . '/vendor/' . self::STATE_FILE; + } +} diff --git a/plugin/think-plugs-install/tests/ComposerLifecycleServiceTest.php b/plugin/think-plugs-install/tests/ComposerLifecycleServiceTest.php new file mode 100644 index 000000000..9be8ff876 --- /dev/null +++ b/plugin/think-plugs-install/tests/ComposerLifecycleServiceTest.php @@ -0,0 +1,334 @@ +root = sys_get_temp_dir() . '/thinkadmin-composer-install-' . bin2hex(random_bytes(6)); + mkdir($this->root . '/vendor/composer', 0777, true); + file_put_contents($this->root . '/think', "#!/usr/bin/env php\n"); + } + + protected function tearDown(): void + { + $this->removeTree($this->root); + } + + public function testDiscoverInstalledPluginsReadsConfiguredMigrations(): void + { + $this->createInstalledPlugin('system', 'vendor/system-plugin', '系统管理', '20241010000001_install_system20241010.php'); + $this->createInstalledPlugin('static', 'vendor/static-plugin', '静态资源'); + + $service = new ComposerLifecycleService($this->root); + $plugins = $service->discoverInstalledPlugins(); + + $this->assertCount(2, $plugins); + $this->assertTrue($plugins['vendor/system-plugin']['migrate']['configured']); + $this->assertSame('20241010000001_install_system20241010.php', $plugins['vendor/system-plugin']['migrate']['file']); + $this->assertFalse($plugins['vendor/static-plugin']['migrate']['configured']); + $this->assertArrayNotHasKey('signature', $plugins['vendor/system-plugin']['migrate']); + $this->assertNotSame('', $plugins['vendor/system-plugin']['signature']); + } + + public function testDiscoverInstalledPluginsIncludesWorkspacePluginsOutsideInstalledJson(): void + { + $this->createWorkspacePlugin('worker', 'vendor/worker-plugin', '运行时服务', '20241010000008_install_worker20241010.php'); + + $service = new ComposerLifecycleService($this->root); + $plugins = $service->discoverInstalledPlugins(); + + $this->assertArrayHasKey('vendor/worker-plugin', $plugins); + $this->assertTrue($plugins['vendor/worker-plugin']['migrate']['configured']); + $this->assertSame('运行时服务', $plugins['vendor/worker-plugin']['title']); + } + + public function testInstallFromCommandRunsAutoMigrateWhenConfigured(): void + { + $this->createInstalledPlugin('system', 'vendor/system-plugin', '系统管理', '20241010000001_install_system20241010.php'); + + $messages = []; + $commands = []; + $service = new ComposerLifecycleService( + $this->root, + function (string $message) use (&$messages): void { + $messages[] = $message; + }, + function (array $command, string $cwd) use (&$commands): int { + $commands[] = [$command, $cwd]; + return 0; + } + ); + + $status = $service->installFromCommand(); + + $this->assertSame(0, $status); + $this->assertSame([ + [[PHP_BINARY, $this->root . '/think', 'service:discover'], $this->root], + [[PHP_BINARY, $this->root . '/think', 'xadmin:publish', '--migrate'], $this->root], + ], $commands); + $this->assertContains('Install database migrations for: 系统管理', $messages); + $this->assertFileExists($this->root . '/vendor/' . ComposerLifecycleService::STATE_FILE); + } + + public function testInstallFromCommandWritesForceMessageWhenForced(): void + { + $messages = []; + $commands = []; + $service = new ComposerLifecycleService( + $this->root, + function (string $message) use (&$messages): void { + $messages[] = $message; + }, + function (array $command, string $cwd) use (&$commands): int { + $commands[] = [$command, $cwd]; + return 0; + } + ); + + $status = $service->installFromCommand(false, true); + + $this->assertSame(0, $status); + $this->assertSame([ + [[PHP_BINARY, $this->root . '/think', 'service:discover'], $this->root], + [[PHP_BINARY, $this->root . '/think', 'xadmin:publish', '--migrate'], $this->root], + ], $commands); + $this->assertContains('Force database migration for all published scripts.', $messages); + } + + public function testWriteStateNormalizesLegacyPayload(): void + { + $service = new ComposerLifecycleService($this->root); + $service->writeState([ + 'vendor/system-plugin' => [ + 'name' => 'vendor/system-plugin', + 'title' => '系统管理', + 'version' => '1.0.0', + 'reference' => 'ref-demo', + 'signature' => 'sig-demo', + 'migrate' => [ + 'configured' => true, + 'file' => 'legacy.php', + ], + ], + ]); + + $state = json_decode((string)file_get_contents($this->root . '/vendor/' . ComposerLifecycleService::STATE_FILE), true, 512, JSON_THROW_ON_ERROR); + + $this->assertSame([ + 'vendor/system-plugin' => [ + 'name' => 'vendor/system-plugin', + 'title' => '系统管理', + 'signature' => 'sig-demo', + 'migrate' => [ + 'configured' => true, + ], + ], + ], $state); + } + + public function testSyncAfterComposerWarnsForMissingAndRemovedMigrations(): void + { + $this->createInstalledPlugin('system', 'vendor/system-plugin', '系统管理', '20241010000001_install_system20241010.php', '1.0.0', 'ref-new'); + $this->createInstalledPlugin('static', 'vendor/static-plugin', '静态资源', null, '1.0.0', 'ref-static'); + + $messages = []; + $commands = []; + $service = new ComposerLifecycleService( + $this->root, + function (string $message) use (&$messages): void { + $messages[] = $message; + }, + function (array $command, string $cwd) use (&$commands): int { + $commands[] = [$command, $cwd]; + return 0; + } + ); + + $service->writeState([ + 'vendor/system-plugin' => [ + 'name' => 'vendor/system-plugin', + 'title' => '系统管理', + 'signature' => 'ref-old', + 'migrate' => ['configured' => true], + ], + 'vendor/payment-plugin' => [ + 'name' => 'vendor/payment-plugin', + 'title' => '支付管理', + 'signature' => 'ref-pay', + 'migrate' => ['configured' => true], + ], + ]); + + $status = $service->syncAfterComposer(); + + $this->assertSame(0, $status); + $this->assertSame([ + [[PHP_BINARY, $this->root . '/think', 'service:discover'], $this->root], + [[PHP_BINARY, $this->root . '/think', 'xadmin:publish', '--migrate'], $this->root], + ], $commands); + $this->assertContains('Auto migrate plugins: 系统管理', $messages); + $this->assertContains('Skip database migration for plugins without extra.xadmin.migrate: 静态资源', $messages); + $this->assertContains('Removed plugins keep historical tables; rollback is not automatic: 支付管理', $messages); + } + + public function testSyncAfterComposerDefersInitialMigrationUntilExplicitInstall(): void + { + $this->createInstalledPlugin('system', 'vendor/system-plugin', '系统管理', '20241010000001_install_system20241010.php', '1.0.0', 'ref-new'); + $this->createInstalledPlugin('static', 'vendor/static-plugin', '静态资源', null, '1.0.0', 'ref-static'); + + $messages = []; + $commands = []; + $service = new ComposerLifecycleService( + $this->root, + function (string $message) use (&$messages): void { + $messages[] = $message; + }, + function (array $command, string $cwd) use (&$commands): int { + $commands[] = [$command, $cwd]; + return 0; + } + ); + + $status = $service->syncAfterComposer(); + + $this->assertSame(0, $status); + $this->assertSame([ + [[PHP_BINARY, $this->root . '/think', 'service:discover'], $this->root], + [[PHP_BINARY, $this->root . '/think', 'xadmin:publish'], $this->root], + ], $commands); + $this->assertContains('Skip automatic database migration on initial Composer install. Run `php think xadmin:install` after configuring the environment: 系统管理', $messages); + $this->assertContains('Skip database migration for plugins without extra.xadmin.migrate: 静态资源', $messages); + } + + private function createInstalledPlugin( + string $code, + string $name, + string $title, + ?string $migration = null, + string $version = '1.0.0', + string $reference = 'ref-demo' + ): void { + $vendorPath = $this->root . '/vendor/' . dirname($name) . '/' . basename($name); + mkdir($vendorPath . '/src', 0777, true); + + $manifest = [ + 'name' => $name, + 'version' => $version, + 'type' => 'think-admin-plugin', + 'autoload' => ['psr-4' => ["plugin\\{$code}\\" => 'src']], + 'extra' => [ + 'think' => ['services' => ["plugin\\{$code}\\Service"]], + 'xadmin' => [ + 'app' => ['code' => $code, 'name' => $title], + ], + ], + ]; + + if ($migration !== null) { + mkdir($vendorPath . '/stc/database', 0777, true); + file_put_contents($vendorPath . '/stc/database/' . $migration, " $migration, + 'class' => 'Install' . ucfirst(str_replace('-', '', $code)), + 'name' => ucfirst(str_replace('-', '', $code)) . 'Plugin', + ]; + } + + file_put_contents($vendorPath . '/composer.json', json_encode($manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)); + + $installedFile = $this->root . '/vendor/composer/installed.json'; + $data = is_file($installedFile) ? json_decode((string)file_get_contents($installedFile), true) : []; + $packages = is_array($data['packages'] ?? null) ? $data['packages'] : []; + $packages[] = [ + 'name' => $name, + 'version' => $version, + 'type' => 'think-admin-plugin', + 'dist' => ['reference' => $reference], + 'extra' => $manifest['extra'], + 'install-path' => '../' . dirname($name) . '/' . basename($name), + ]; + file_put_contents($installedFile, json_encode(['packages' => $packages], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)); + } + + private function createWorkspacePlugin( + string $code, + string $name, + string $title, + ?string $migration = null + ): void { + $pluginPath = $this->root . '/plugin/' . $code; + mkdir($pluginPath . '/src', 0777, true); + + $manifest = [ + 'name' => $name, + 'version' => '1.0.0', + 'type' => 'think-admin-plugin', + 'autoload' => ['psr-4' => ["plugin\\{$code}\\" => 'src']], + 'extra' => [ + 'think' => ['services' => ["plugin\\{$code}\\Service"]], + 'xadmin' => [ + 'app' => ['code' => $code, 'name' => $title], + ], + ], + ]; + + if ($migration !== null) { + mkdir($pluginPath . '/stc/database', 0777, true); + file_put_contents($pluginPath . '/stc/database/' . $migration, " $migration, + 'class' => 'Install' . ucfirst(str_replace('-', '', $code)), + 'name' => ucfirst(str_replace('-', '', $code)) . 'Plugin', + ]; + } + + file_put_contents($pluginPath . '/composer.json', json_encode($manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)); + } + + private function removeTree(string $path): void + { + if (!file_exists($path)) { + return; + } + if (is_file($path) || is_link($path)) { + @unlink($path); + return; + } + foreach (scandir($path) ?: [] as $item) { + if ($item === '.' || $item === '..') { + continue; + } + $this->removeTree($path . '/' . $item); + } + @rmdir($path); + } +} diff --git a/plugin/think-plugs-install/tests/InstallCommandTest.php b/plugin/think-plugs-install/tests/InstallCommandTest.php new file mode 100644 index 000000000..f14ccb306 --- /dev/null +++ b/plugin/think-plugs-install/tests/InstallCommandTest.php @@ -0,0 +1,133 @@ +createConsole($app, 23); + $app->instance('console', $console); + + $command = new InstallCommand(); + $command->setApp($app); + + $status = $command->run(new Input(['--force', '--migrate']), new Output('buffer')); + + $this->assertSame(23, $status); + $this->assertSame([ + ['service:discover', []], + ['xadmin:publish', ['--force', '--migrate']], + ], $console->calls); + } + + public function testRunSkipsMigrateWhenNoPluginDeclaresMigration(): void + { + $root = sys_get_temp_dir() . '/thinkadmin-install-command-' . bin2hex(random_bytes(6)); + mkdir($root . '/vendor/composer', 0777, true); + file_put_contents($root . '/think', "#!/usr/bin/env php\n"); + file_put_contents($root . '/vendor/composer/installed.json', json_encode(['packages' => []], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); + + $app = new App($root); + $console = $this->createConsole($app); + $app->instance('console', $console); + + $command = new InstallCommand(); + $command->setApp($app); + + $status = $command->run(new Input([]), new Output('buffer')); + + $this->assertSame(0, $status); + $this->assertSame([ + ['service:discover', []], + ['xadmin:publish', []], + ], $console->calls); + } + + private function createConsole(App $app, int $publishStatus = 0): object + { + return new class($app, $publishStatus) { + public array $calls = []; + + public function __construct(private readonly App $app, private readonly int $publishStatus) + { + } + + public function find(string $name): Command + { + $command = new class($this, $name, $name === 'xadmin:publish' ? $this->publishStatus : 0) extends Command { + public function __construct( + private readonly object $owner, + private readonly string $commandName, + private readonly int $status + ) { + parent::__construct(); + } + + protected function configure() + { + $this->setName($this->commandName); + if ($this->commandName === 'xadmin:publish') { + $this->addOption('force', 'f', Option::VALUE_NONE); + $this->addOption('migrate', 'm', Option::VALUE_NONE); + } + } + + protected function execute(Input $input, Output $output): int + { + $arguments = []; + if ($this->commandName === 'xadmin:publish') { + if ($input->getOption('force')) { + $arguments[] = '--force'; + } + if ($input->getOption('migrate')) { + $arguments[] = '--migrate'; + } + } + + $this->owner->calls[] = [$this->commandName, $arguments]; + return $this->status; + } + }; + + $command->setApp($this->app); + return $command; + } + }; + } +} diff --git a/plugin/think-plugs-install/tests/bootstrap.php b/plugin/think-plugs-install/tests/bootstrap.php new file mode 100644 index 000000000..fddf20933 --- /dev/null +++ b/plugin/think-plugs-install/tests/bootstrap.php @@ -0,0 +1,31 @@ +> $GITHUB_ENV + echo "SECOND_LATEST_TAG=$SECOND_LATEST_TAG" >> $GITHUB_ENV + + - name: Generate Release Notes + run: | + rm -rf log + mkdir -p log + git-log -m tag -f -S $SECOND_LATEST_TAG -v ${LATEST_TAG#v} + + - name: Create Release + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ env.LATEST_TAG }} + release_name: Release ${{ env.LATEST_TAG }} + body_path: log/${{ env.LATEST_TAG }}.md + draft: false + prerelease: false \ No newline at end of file diff --git a/plugin/think-plugs-payment/.gitignore b/plugin/think-plugs-payment/.gitignore new file mode 100644 index 000000000..a8cd966aa --- /dev/null +++ b/plugin/think-plugs-payment/.gitignore @@ -0,0 +1,12 @@ +.env +.git +.svn +.idea +.fleet +.vscode +.DS_Store +*.log +*.zip +*.cache +/vendor +/composer.lock diff --git a/plugin/think-plugs-payment/.php-cs-fixer.php b/plugin/think-plugs-payment/.php-cs-fixer.php new file mode 100644 index 000000000..a3a6c1335 --- /dev/null +++ b/plugin/think-plugs-payment/.php-cs-fixer.php @@ -0,0 +1,120 @@ +setRiskyAllowed(true)->setParallelConfig(new ParallelConfig(8, 24)); +$finder = Finder::create()->in(__DIR__)->exclude(['vendor', 'public', 'runtime']); +return $config->setFinder($finder)->setUsingCache(false)->setRules([ + '@PSR2' => true, + '@Symfony' => true, + '@DoctrineAnnotation' => true, + '@PhpCsFixer' => true, + 'header_comment' => [ + 'comment_type' => 'PHPDoc', + 'header' => $header, + 'separate' => 'none', + 'location' => 'after_declare_strict', + ], + 'array_syntax' => [ + 'syntax' => 'short', + ], + 'list_syntax' => [ + 'syntax' => 'short', + ], + 'blank_line_before_statement' => [ + 'statements' => [ + 'declare', + ], + ], + 'general_phpdoc_annotation_remove' => [ + 'annotations' => [ + 'author', + ], + ], + 'ordered_imports' => [ + 'imports_order' => [ + 'class', 'function', 'const', + ], + 'sort_algorithm' => 'alpha', + ], + 'single_line_comment_style' => [ + 'comment_types' => [ + ], + ], + 'yoda_style' => [ + 'always_move_variable' => false, + 'equal' => false, + 'identical' => false, + ], + 'phpdoc_align' => [ + 'align' => 'left', + ], + 'multiline_whitespace_before_semicolons' => [ + 'strategy' => 'no_multi_line', + ], + 'constant_case' => [ + 'case' => 'lower', + ], + 'encoding' => true, // PHP代码必须只使用没有BOM的UTF-8 + 'line_ending' => true, // 所有的PHP文件编码必须一致 + 'single_quote' => true, // 简单字符串应该使用单引号代替双引号 + 'no_empty_statement' => true, // 不应该存在空的结构体 + 'standardize_not_equals' => true, // 使用 <> 代替 != + 'blank_line_after_namespace' => true, // 命名空间之后空一行 + 'no_empty_phpdoc' => true, // 不应该存在空的 phpdoc + 'no_empty_comment' => true, // 不应该存在空注释 + 'no_singleline_whitespace_before_semicolons' => true, // 禁止在关闭分号前使用单行空格 + 'concat_space' => ['spacing' => 'one'], // 连接字符是否需要空格,可选配置项 none:不需要 one:一个空格 + 'no_leading_import_slash' => true, // use 语句中取消前置斜杠 + 'cast_spaces' => ['space' => 'none'], + 'class_attributes_separation' => true, + 'combine_consecutive_unsets' => true, + 'declare_strict_types' => true, + 'lowercase_static_reference' => true, + 'linebreak_after_opening_tag' => true, + 'multiline_comment_opening_closing' => true, + 'no_useless_else' => true, + 'no_unused_imports' => true, + 'not_operator_with_successor_space' => false, + 'not_operator_with_space' => false, + 'ordered_class_elements' => true, + 'php_unit_strict' => false, + 'phpdoc_separation' => false, +]); diff --git a/plugin/think-plugs-payment/composer.json b/plugin/think-plugs-payment/composer.json new file mode 100644 index 000000000..3d96a4e95 --- /dev/null +++ b/plugin/think-plugs-payment/composer.json @@ -0,0 +1,112 @@ +{ + "type": "think-admin-plugin", + "name": "zoujingli/think-plugs-payment", + "version": "8.0.x-dev", + "license": "proprietary", + "homepage": "https://thinkadmin.top", + "description": "Payment Plugin for ThinkAdmin", + "authors": [ + { + "name": "Anyon", + "email": "zoujingli@qq.com" + } + ], + "require": { + "php": "^8.1", + "ext-json": "*", + "zoujingli/think-plugs-account": "^8.0", + "zoujingli/think-plugs-system": "^8.0", + "zoujingli/think-plugs-worker": "^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.5|^10.0" + }, + "autoload": { + "psr-4": { + "plugin\\payment\\": "src" + } + }, + "autoload-dev": { + "psr-4": { + "think\\admin\\tests\\": "tests/" + } + }, + "extra": { + "think": { + "services": [ + "plugin\\payment\\Service" + ] + }, + "xadmin": { + "migrate": { + "file": "20241010000006_install_payment20241010.php", + "class": "InstallPayment20241010", + "name": "PaymentPlugin" + }, + "menu": { + "items": [ + { + "name": "用户管理", + "subs": [ + { + "name": "用户账号管理", + "icon": "layui-icon layui-icon-user", + "node": "account/master/index" + }, + { + "name": "终端账号管理", + "icon": "layui-icon layui-icon-cellphone", + "node": "account/device/index" + }, + { + "name": "手机短信管理", + "icon": "layui-icon layui-icon-email", + "node": "account/message/index" + } + ] + }, + { + "name": "支付管理", + "subs": [ + { + "name": "支付配置管理", + "icon": "layui-icon layui-icon-set", + "node": "payment/config/index" + }, + { + "name": "支付行为管理", + "icon": "layui-icon layui-icon-edge", + "node": "payment/record/index" + }, + { + "name": "支付退款管理", + "icon": "layui-icon layui-icon-firefox", + "node": "payment/refund/index" + }, + { + "name": "余额明细管理", + "icon": "layui-icon layui-icon-rmb", + "node": "payment/balance/index" + }, + { + "name": "积分明细管理", + "icon": "layui-icon layui-icon-diamond", + "node": "payment/integral/index" + } + ] + } + ] + }, + "app": { + "name": "支付管理", + "document": "https://thinkadmin.top/plugin/think-plugs-payment.html", + "description": "提供支付配置、支付行为、支付退款和余额积分管理功能。", + "license": [ + "VIP" + ], + "code": "payment", + "prefix": "payment" + } + } + } +} diff --git a/plugin/think-plugs-payment/phpunit.xml.dist b/plugin/think-plugs-payment/phpunit.xml.dist new file mode 100644 index 000000000..45875d290 --- /dev/null +++ b/plugin/think-plugs-payment/phpunit.xml.dist @@ -0,0 +1,24 @@ + + + + + ./src + + + + + ./tests + + + diff --git a/plugin/think-plugs-payment/readme.api.md b/plugin/think-plugs-payment/readme.api.md new file mode 100644 index 000000000..e97fa327c --- /dev/null +++ b/plugin/think-plugs-payment/readme.api.md @@ -0,0 +1,83 @@ +# 支付管理接口 + +## 接口标准 +- HTTP 入口:`/api/payment/auth/{controller}/{action}` +- 所有接口都需要 `Authorization: Bearer ` +- 返回格式为 JSON,继承账号授权接口返回结构 +- HTTP 状态码固定返回 `200` +- `code` 统一使用常见业务状态语义:`200` 成功、`401` 未认证/登录过期、`403` 已认证但无权限、`404` 资源不存在、`500` 服务端异常 + +```jsonc +{ + "code": 200, // 业务状态码 + "info": "ok", // 提示信息 + "data": {}, // 业务数据 + "error": "", // 可选,401/403 等鉴权异常时返回稳定错误标识 + "token": "jwt" // 续签时可能返回,可选 +} +``` + +## 接口列表 + +### `/api/payment/auth/address/set` +- 说明:新增或修改收货地址 + +```jsonc +{ + "id": 0, // 地址 ID,0 表示新增,非 0 表示更新 + "type": 1, // 默认地址标记,0 否,1 是 + "user_name": "张三", // 收货人姓名 + "user_phone": "13800000000", // 收货人手机号 + "region_prov": "浙江省", // 省份 + "region_city": "杭州市", // 城市 + "region_area": "西湖区", // 区县 + "region_addr": "文三路 1 号", // 详细地址 + "idcode": "", // 可选,身份证号码 + "idimg1": "", // 可选,身份证正面图片 + "idimg2": "" // 可选,身份证反面图片 +} +``` + +### `/api/payment/auth/address/get` +- 说明:获取当前用户地址列表 + +```jsonc +{} +``` + +### `/api/payment/auth/address/state` +- 说明:切换默认地址状态 + +```jsonc +{ + "id": 1, // 地址 ID + "type": 1 // 目标状态,0 非默认,1 默认 +} +``` + +### `/api/payment/auth/address/remove` +- 说明:删除收货地址 + +```jsonc +{ + "id": 1 // 地址 ID +} +``` + +### `/api/payment/auth/balance/get` +- 说明:分页获取余额记录 + +```jsonc +{ + "page": 1 // 页码 +} +``` + +### `/api/payment/auth/integral/get` +- 说明:分页获取积分记录 + +```jsonc +{ + "page": 1 // 页码 +} +``` diff --git a/plugin/think-plugs-payment/readme.md b/plugin/think-plugs-payment/readme.md new file mode 100644 index 000000000..9c59a6f38 --- /dev/null +++ b/plugin/think-plugs-payment/readme.md @@ -0,0 +1,15 @@ +# 支付管理 + +## 定位 +- 组件编码:`payment` +- 组件包名:`zoujingli/think-plugs-payment` +- 主要职责:支付配置、余额积分、收货地址、支付记录与退款相关能力 + +## 边界 +- 对外 HTTP 接口前缀:`/api/payment/auth/*` +- 依赖账号登录态,接口基于 `Authorization: Bearer ` 认证 +- 为商城等业务组件提供支付、地址和余额积分基础能力 + +## 文档 +- 接口与参数说明:`readme.api.md` +- 官方页面:[https://thinkadmin.top/plugin/think-plugs-payment.html](https://thinkadmin.top/plugin/think-plugs-payment.html) diff --git a/plugin/think-plugs-payment/src/Service.php b/plugin/think-plugs-payment/src/Service.php new file mode 100644 index 000000000..8fd7430ec --- /dev/null +++ b/plugin/think-plugs-payment/src/Service.php @@ -0,0 +1,49 @@ +app->route->any('/plugin-payment-notify/:vars', function (Request $request) { + try { + $data = json_decode(CodeToolkit::deSafe64($request->param('vars')), true); + return Payment::mk($data['channel'])->notify($data); + } catch (\Error|\Exception $exception) { + return 'Error: ' . $exception->getMessage(); + } + }); + } +} diff --git a/plugin/think-plugs-payment/src/controller/Balance.php b/plugin/think-plugs-payment/src/controller/Balance.php new file mode 100644 index 000000000..20fa0c0dd --- /dev/null +++ b/plugin/think-plugs-payment/src/controller/Balance.php @@ -0,0 +1,140 @@ +type = $this->get['type'] ?? 'index'; + PluginPaymentBalance::mQuery()->layTable(function () { + $this->title = lang('余额明细管理'); + $map = ['cancel' => 0]; + $this->balanceTotal = PluginPaymentBalance::mk()->where($map)->whereRaw('amount>0')->sum('amount'); + $this->balanceCount = PluginPaymentBalance::mk()->where($map)->whereRaw('amount<0')->sum('amount'); + }, function (QueryHelper $query) { + $query->with(['user']); + $query->like('code,remark')->dateBetween('create_time'); + $query->where(['cancel' => intval($this->type !== 'index')]); + $userQuery = PluginAccountUser::mQuery(); + $userQuery->like('email|nickname|username|phone#user'); + $db = $userQuery->db(); + if (!empty($db->getOptions()['where'] ?? [])) { + $query->whereRaw("unid in {$db->field('id')->buildSql()}"); + } + }); + } + + /** + * 交易锁定处理. + * @auth true + */ + public function unlock() + { + try { + $data = $this->_vali([ + 'code.require' => lang('单号不能为空!'), + 'unlock.require' => lang('状态不能为空!'), + ]); + BalanceService::unlock($data['code'], intval($data['unlock'])); + $this->success(lang('交易操作成功!')); + } catch (HttpResponseException $exception) { + throw $exception; + } catch (\Exception $exception) { + $this->error($exception->getMessage()); + } + } + + /** + * 交易状态处理. + * @auth true + */ + public function cancel() + { + try { + $data = $this->_vali([ + 'code.require' => lang('单号不能为空!'), + 'cancel.require' => lang('状态不能为空!'), + ]); + BalanceService::cancel($data['code'], intval($data['cancel'])); + $this->success(lang('交易操作成功!')); + } catch (HttpResponseException $exception) { + throw $exception; + } catch (\Exception $exception) { + $this->error($exception->getMessage()); + } + } + + /** + * 删除余额记录. + * @auth true + */ + public function remove() + { + try { + $data = $this->_vali([ + 'code.require' => lang('单号不能为空!'), + ]); + BalanceService::remove($data['code']); + $this->success(lang('交易操作成功!')); + } catch (HttpResponseException $exception) { + throw $exception; + } catch (\Exception $exception) { + $this->error($exception->getMessage()); + } + } +} diff --git a/plugin/think-plugs-payment/src/controller/Config.php b/plugin/think-plugs-payment/src/controller/Config.php new file mode 100644 index 000000000..de4e6244e --- /dev/null +++ b/plugin/think-plugs-payment/src/controller/Config.php @@ -0,0 +1,207 @@ +type = $this->get['type'] ?? 'index'; + PluginPaymentConfig::mQuery()->layTable(function () { + $this->title = lang('支付配置管理'); + $this->types = Payment::types(1); + }, function (QueryHelper $query) { + $query->withoutField('content'); + $query->where(['status' => intval($this->type === 'index')]); + $query->like('name,code')->equal('status,type#ptype')->dateBetween('create_time'); + }); + } + + /** + * 添加支付配置. + * @auth true + */ + public function add() + { + $this->title = lang('添加支付配置'); + PluginPaymentConfig::mForm('form'); + } + + /** + * 编辑支付配置. + * @auth true + */ + public function edit() + { + $this->title = lang('编辑支付配置'); + PluginPaymentConfig::mForm('form'); + } + + /** + * 修改通道状态 + * @auth true + */ + public function state() + { + PluginPaymentConfig::mSave($this->_vali([ + 'status.in:0,1' => lang('状态值范围异常!'), + 'status.require' => lang('状态值不能为空!'), + ])); + } + + /** + * 删除支付配置. + * @auth true + */ + public function remove() + { + PluginPaymentConfig::mDelete(); + } + + /** + * 配置支付方式. + * @auth true + * @throws Exception + */ + public function types() + { + $this->types = Payment::types(); + $this->config = sysdata('plugin.payment.config'); + if ($this->request->isGet()) { + $this->fetch(); + } else { + $post = $this->request->post(['types', 'integral']); + if (($post['integral'] ?? 0) < 1) { + $this->error(lang('兑换积分不能少于1积分!')); + } + sysdata('plugin.payment.config', $post); + foreach ($this->types as $k => $v) { + Payment::set($k, intval(in_array($k, $post['types']))); + } + if (Payment::save()) { + $this->success(lang('配置保存成功!')); + } else { + $this->error(lang('配置保存失败!')); + } + } + } + + /** + * 获取支付配置. + */ + protected function _page_filter(array &$data) + { + [$ptypes, $atypes] = [Payment::types(), Account::types(1)]; + $separator = str_starts_with($this->app->lang->getLangSet(), 'zh') ? '、' : ' / '; + foreach ($data as &$vo) { + [$vo['ntype'], $vo['atype']] = [lang($ptypes[$vo['type']]['name'] ?? $vo['type']), []]; + if (isset($ptypes[$vo['type']])) { + foreach ($ptypes[$vo['type']]['account'] as $account) { + if (isset($atypes[$account])) { + $vo['atype'][$account] = lang($atypes[$account]['name']); + } + } + if (!empty($vo['atype'])) { + $vo['atype_text'] = implode($separator, $vo['atype']); + } + } + } + } + + /** + * 数据表单处理. + */ + protected function _form_filter(array &$data) + { + if (empty($data['code'])) { + $data['code'] = CodeToolkit::uniqidNumber(12, 'M'); + } + if ($this->request->isGet()) { + $data['content'] = $data['content'] ?? []; + [$this->payments, $types] = [[], Account::types(1)]; + $separator = str_starts_with($this->app->lang->getLangSet(), 'zh') ? '、' : ' / '; + foreach (Payment::types(1) as $k => $v) { + // 屏蔽内置支付方式 + if (in_array($k, [Payment::BALANCE, Payment::INTEGRAL, Payment::COUPON])) { + continue; + } + $allow = []; + foreach ($v['account'] as $api) { + if (isset($types[$api])) { + $allow[$api] = lang($types[$api]['name']); + } + } + if (empty($allow)) { + continue; + } + $this->payments[$k] = array_merge($v, ['allow' => implode($separator, $allow)]); + } + } else { + if (empty($data['type'])) { + $this->error(lang('请选择支付方式!')); + } + if (empty($data['cover'])) { + $this->error(lang('请上传支付图标!')); + } + // 保存配置参数 + $data['content'] = $this->request->post(); + $fields = PluginPaymentConfig::mk()->getTableFields(); + foreach ($data['content'] as $k => $v) { + if (in_array($k, $fields) || $v === '') { + unset($data['content'][$k]); + } + } + } + } + + /** + * 处理结果处理. + */ + protected function _form_result(bool $state) + { + if ($state) { + $this->success(lang('参数保存成功!'), 'javascript:history.back()'); + } + } +} diff --git a/plugin/think-plugs-payment/src/controller/Integral.php b/plugin/think-plugs-payment/src/controller/Integral.php new file mode 100644 index 000000000..6c93b968c --- /dev/null +++ b/plugin/think-plugs-payment/src/controller/Integral.php @@ -0,0 +1,140 @@ +type = $this->get['type'] ?? 'index'; + PluginPaymentIntegral::mQuery()->layTable(function () { + $this->title = lang('积分明细管理'); + $map = ['cancel' => 0]; + $this->integralTotal = PluginPaymentIntegral::mk()->where($map)->whereRaw('amount>0')->sum('amount'); + $this->integralCount = PluginPaymentIntegral::mk()->where($map)->whereRaw('amount<0')->sum('amount'); + }, function (QueryHelper $query) { + $userQuery = PluginAccountUser::mQuery(); + $userQuery->like('email|nickname|username|phone#user'); + $db = $userQuery->db(); + if (!empty($db->getOptions()['where'] ?? [])) { + $query->whereRaw("unid in {$db->field('id')->buildSql()}"); + } + $query->with(['user']); + $query->like('code,remark')->dateBetween('create_time'); + $query->where(['cancel' => intval($this->type !== 'index')]); + }); + } + + /** + * 交易锁定处理. + * @auth true + */ + public function unlock() + { + try { + $data = $this->_vali([ + 'code.require' => lang('单号不能为空!'), + 'unlock.require' => lang('状态不能为空!'), + ]); + IntegralService::unlock($data['code'], intval($data['unlock'])); + $this->success(lang('交易操作成功!')); + } catch (HttpResponseException $exception) { + throw $exception; + } catch (\Exception $exception) { + $this->error($exception->getMessage()); + } + } + + /** + * 交易状态处理. + * @auth true + */ + public function cancel() + { + try { + $data = $this->_vali([ + 'code.require' => lang('单号不能为空!'), + 'cancel.require' => lang('状态不能为空!'), + ]); + IntegralService::cancel($data['code'], intval($data['cancel'])); + $this->success(lang('交易操作成功!')); + } catch (HttpResponseException $exception) { + throw $exception; + } catch (\Exception $exception) { + $this->error($exception->getMessage()); + } + } + + /** + * 删除余额记录. + * @auth true + */ + public function remove() + { + try { + $data = $this->_vali([ + 'code.require' => lang('单号不能为空!'), + ]); + IntegralService::remove($data['code']); + $this->success(lang('交易操作成功!')); + } catch (HttpResponseException $exception) { + throw $exception; + } catch (\Exception $exception) { + $this->error($exception->getMessage()); + } + } +} diff --git a/plugin/think-plugs-payment/src/controller/Record.php b/plugin/think-plugs-payment/src/controller/Record.php new file mode 100644 index 000000000..6cc472d8d --- /dev/null +++ b/plugin/think-plugs-payment/src/controller/Record.php @@ -0,0 +1,223 @@ +mode = $this->get['open_type'] ?? 'index'; + PluginPaymentRecord::mQuery()->layTable(function () { + if ($this->mode === 'index') { + $this->title = lang('支付行为管理'); + } + }, static function (QueryHelper $query) { + $userQuery = PluginAccountUser::mQuery(); + $userQuery->like('email|nickname|username|phone#userinfo'); + $db = $userQuery->db(); + if (!empty($db->getOptions()['where'] ?? [])) { + $query->whereRaw("unid in {$db->field('id')->buildSql()}"); + } + $query->with(['user']); + $query->like('order_no|order_name#orderinfo')->dateBetween('create_time'); + }); + } + + /** + * 单据凭证审核. + * @auth true + */ + public function audit() + { + if ($this->request->isGet()) { + $this->buildAuditForm()->fetch([ + 'vo' => $this->loadAuditRecord(intval($this->request->param('id', 0))), + ]); + } + + $data = $this->buildAuditForm()->validate(); + $data['id'] = intval($this->request->param('id', 0)); + if (intval($data['status']) === 1) { + $this->error(lang('请选择通过或驳回!')); + } + $action = PluginPaymentRecord::mk()->findOrEmpty($data['id']); + if ($action->isEmpty()) { + $this->error(lang('支付记录不存在!')); + } + if ($action->getAttr('channel_type') !== Payment::VOUCHER) { + $this->error(lang('无需审核操作!')); + } + if ($action->getAttr('payment_status') === 1) { + $this->success(lang('该凭证已审核!')); + } + $data['audit_user'] = AuthService::getUserId(); + $data['audit_time'] = date('Y-m-d H:i:s'); + $data['audit_remark'] = $data['remark']; + $data['payment_time'] = date('Y-m-d H:i:s'); + $data['payment_trade'] = CodeToolkit::uniqidNumber(18, 'AU'); + if (empty($data['status'])) { + $data['audit_status'] = 0; + $data['payment_status'] = 0; + $data['payment_remark'] = $data['remark'] ?: lang('后台支付凭证被驳回'); + } else { + $data['audit_status'] = 2; + $data['payment_status'] = 1; + $data['payment_remark'] = $data['remark'] ?: lang('后台支付凭证已通过'); + } + if ($action->save($data)) { + if (empty($data['status'])) { + $this->app->event->trigger('PluginPaymentRefuse', $action->refresh()); + $this->success(lang('凭证审核驳回!')); + } else { + $this->app->event->trigger('PluginPaymentSuccess', $action->refresh()); + $this->success(lang('凭证审核通过!')); + } + } else { + $this->error(lang('凭证审核失败!')); + } + } + + /** + * 取消支付订单. + * @auth true + */ + public function cancel() + { + try { + $data = $this->_vali(['code.require' => lang('支付单号不能为空!')]); + $items = PluginPaymentRecord::mk()->where(function (Query $query) { + $query->whereOr([['payment_status', '=', 1], ['audit_status', '>', 0]]); + })->where($data)->column('code,channel_code,payment_amount,payment_coupon'); + foreach ($items as $item) { + $amount = bcsub(strval($item['payment_amount']), strval($item['payment_coupon']), 2); + Payment::mk($item['channel_code'])->refund($item['code'], $amount); + } + $this->success(lang('退款申请成功!')); + } catch (HttpResponseException $exception) { + throw $exception; + } catch (\Exception $exception) { + $this->error($exception->getMessage()); + } + } + + /** + * 重新触发支付行为. + * @auth true + */ + public function notify() + { + $data = $this->_vali(['code.require' => lang('支付单号不能为空!')]); + $record = PluginPaymentRecord::mk()->where(['code' => $data['code']])->findOrEmpty(); + if ($record->isEmpty()) { + $this->error(lang('支付单号异常!')); + } + if (empty($record->getAttr('payment_status'))) { + $this->error(lang('未完成支付!')); + } + $this->app->event->trigger('PluginPaymentSuccess', $record); + $this->success(lang('重新触发支付行为!')); + } + + private function buildAuditForm(): FormBuilder + { + $id = intval($this->request->param('id', 0)); + $voucherRemark = <<<'HTML' +
    img
    +HTML; + + return FormBuilder::make() + ->define(function ($form) use ($id, $voucherRemark) { + $form->action(url('audit', array_filter(['id' => $id ?: null]))->build()) + ->fields(function ($fields) use ($voucherRemark) { + $fields->text('order_no_display', lang('业务单号'), 'Order No.', false, '', null, [ + 'readonly' => null, + 'class' => 'layui-bg-gray', + ])->text('code_display', lang('交易单号'), 'Payment No.', false, '', null, [ + 'readonly' => null, + 'class' => 'layui-bg-gray', + ])->text('payment_amount_display', lang('交易金额'), 'Payment Amount', false, '', null, [ + 'readonly' => null, + 'class' => 'layui-bg-gray', + ])->text('payment_images_display', lang('支付单据凭证'), 'Payment Voucher', false, $voucherRemark, null, [ + 'readonly' => null, + 'class' => 'layui-bg-gray', + ])->field([ + 'type' => 'radio', + 'name' => 'status', + 'title' => lang('审核操作类型'), + 'subtitle' => 'Audit Status', + 'required' => true, + 'options' => [0 => lang('驳回凭证'), 1 => lang('等待审核'), 2 => lang('审核通过')], + ])->textarea('remark', lang('订单审核描述'), 'Audit Remark', false, '', [ + 'placeholder' => lang('请输入订单审核描述'), + ]); + })->actions(function ($actions) { + $actions->submit()->cancel(lang('取消操作'), lang('确定要取消吗?')); + }); + }) + ->build(); + } + + private function loadAuditRecord(int $id): array + { + if ($id < 1) { + $this->error(lang('支付号不能为空!')); + } + $record = PluginPaymentRecord::mk()->findOrEmpty($id); + if ($record->isEmpty()) { + $this->error(lang('支付记录不存在!')); + } + $data = $record->toArray(); + $data['order_no_display'] = strval($data['order_no'] ?? ''); + $data['code_display'] = strval($data['code'] ?? ''); + $data['payment_amount_display'] = strval(($data['payment_amount'] ?? '0') + 0) . ' ' . lang('元'); + $data['payment_images_display'] = strval($data['payment_images'] ?? ''); + $data['remark'] = strval($data['remark'] ?? ($data['audit_remark'] ?? lang('支付凭证已查验'))); + $data['status'] = intval($data['audit_status'] ?? 1); + return $data; + } +} diff --git a/plugin/think-plugs-payment/src/controller/Refund.php b/plugin/think-plugs-payment/src/controller/Refund.php new file mode 100644 index 000000000..59dc5a93a --- /dev/null +++ b/plugin/think-plugs-payment/src/controller/Refund.php @@ -0,0 +1,77 @@ +mode = $this->get['open_type'] ?? 'index'; + PluginPaymentRefund::mQuery()->layTable(function () { + if ($this->mode === 'index') { + $this->title = lang('支付退款管理'); + } + }, static function (QueryHelper $query) { + $query->with(['user', 'record']); + $query->like('order_no|order_name#orderinfo')->dateBetween('create_time'); + $userQuery = PluginAccountUser::mQuery(); + $userQuery->like('email|nickname|username|phone#userinfo'); + $db = $userQuery->db(); + if (!empty($db->getOptions()['where'] ?? [])) { + $query->whereRaw("unid in {$db->field('id')->buildSql()}"); + } + }); + } +} diff --git a/plugin/think-plugs-payment/src/controller/api/auth/Address.php b/plugin/think-plugs-payment/src/controller/api/auth/Address.php new file mode 100644 index 000000000..dd9483004 --- /dev/null +++ b/plugin/think-plugs-payment/src/controller/api/auth/Address.php @@ -0,0 +1,181 @@ +_vali([ + 'id.default' => 0, + 'unid.value' => $this->unid, + 'type.default' => 0, + 'idcode.default' => '', // 身份证号码 + 'idimg1.default' => '', // 身份证正面 + 'idimg2.default' => '', // 身份证反面 + 'type.in:0,1' => lang('状态不在范围!'), + 'user_name.require' => lang('姓名不能为空!'), + 'user_phone.mobile' => lang('手机格式错误!'), + 'user_phone.require' => lang('手机不能为空!'), + 'region_prov.require' => lang('省份不能为空!'), + 'region_city.require' => lang('城市不能为空!'), + 'region_area.require' => lang('区域不能为空!'), + 'region_addr.require' => lang('地址不能为空!'), + ]); + + if (empty($data['id'])) { + unset($data['id']); + $map = ['unid' => $this->unid]; + if (PluginPaymentAddress::mk()->where($map)->count() >= 10) { + $this->error(lang('最多10个地址!')); + } + } + + // 设置默认值 + $model = $this->withDefault(intval($data['id'] ?? 0), intval($data['type']), true); + + // 保存收货地址 + if ($model->save($data) && $model->isExists()) { + $this->success(lang('保存成功!'), $model->refresh()->hidden(['delete_time'])->toArray()); + } else { + $this->error(lang('保存失败!')); + } + } + + /** + * 获取地址 + * @throws DataNotFoundException + * @throws DbException + * @throws ModelNotFoundException + */ + public function get() + { + PluginPaymentAddress::mQuery(null, function (QueryHelper $query) { + $query->where(['unid' => $this->unid]); + $query->equal('id'); + $query->db()->withoutField('delete_time')->order('type desc,id desc'); + $this->success(lang('获取地址数据!'), $query->page(false, false)); + }); + } + + /** + * 修改地址状态 + */ + public function state() + { + $data = $this->_vali([ + 'id.require' => lang('编号不能为空!'), + 'type.in:0,1' => lang('状态不在范围!'), + 'type.require' => lang('状态不能为空!'), + ]); + + // 检查是否存在 + $model = $this->withDefault(intval($data['id']), intval($data['type'])); + $model->isEmpty() && $this->error(lang('地址不存在!')); + + // 返回成功消息 + $this->success(lang('设置默认成功!'), $model->refresh()->toArray()); + } + + /** + * 删除收货地址 + */ + public function remove() + { + $map = $this->_vali(['id.require' => lang('地址ID为空!')]); + $model = $this->withModel($map)->findOrEmpty(); + if ($model->isEmpty()) { + $this->error(lang('地址不存在!')); + } elseif ($model->delete() !== false) { + $this->success(lang('删除成功!')); + } else { + $this->error(lang('删除失败!')); + } + } + + /** + * 初始化检查. + */ + protected function initialize() + { + parent::initialize(); + $this->checkUserStatus(); + } + + /** + * 创建数据模型. + * @param mixed $map 地址查询条件 + * @return mixed + */ + private function withModel($map = []) + { + $model = PluginPaymentAddress::mk()->withoutField('delete_time'); + return $model->where($map)->where(['unid' => $this->unid]); + } + + /** + * 取消默认选项. + * @param int $id 地址编号 + * @param int $type 是否默认 + * @param bool $force 强制更新 + */ + private function withDefault(int $id = 0, int $type = 1, bool $force = false): PluginPaymentAddress + { + $model = $this->withModel(['id' => $id])->findOrEmpty(); + if ($model->isExists() && intval($model->getAttr('type')) !== $type) { + $model->save(['type' => $type]); + } + if (($force || $model->isExists()) && $type > 0) { + $map = [['id', '<>', $id], ['unid', '=', $this->unid]]; + $model->newQuery()->where($map)->update(['type' => 0]); + } + return $model; + } +} diff --git a/plugin/think-plugs-payment/src/controller/api/auth/Balance.php b/plugin/think-plugs-payment/src/controller/api/auth/Balance.php new file mode 100644 index 000000000..9195109af --- /dev/null +++ b/plugin/think-plugs-payment/src/controller/api/auth/Balance.php @@ -0,0 +1,57 @@ +where(['unid' => $this->unid, 'cancel' => 0])->order('id desc'); + $this->success(lang('获取余额记录!'), $query->page(intval(input('page', 1)), false, false, 20)); + }); + } +} diff --git a/plugin/think-plugs-payment/src/controller/api/auth/Integral.php b/plugin/think-plugs-payment/src/controller/api/auth/Integral.php new file mode 100644 index 000000000..f25ecebcf --- /dev/null +++ b/plugin/think-plugs-payment/src/controller/api/auth/Integral.php @@ -0,0 +1,57 @@ +where(['unid' => $this->unid, 'cancel' => 0])->order('id desc'); + $this->success(lang('获取积分记录!'), $query->page(intval(input('page', 1)), false, false, 20)); + }); + } +} diff --git a/plugin/think-plugs-payment/src/lang/en-us.php b/plugin/think-plugs-payment/src/lang/en-us.php new file mode 100644 index 000000000..eb0bc742d --- /dev/null +++ b/plugin/think-plugs-payment/src/lang/en-us.php @@ -0,0 +1,364 @@ + 'Recycle Bin', + '排序权重' => 'Sort Weight', + '头像' => 'Avatar', + '操作面板' => 'Actions', + '已激活' => 'Activated', + '已禁用' => 'Disabled', + '已启用' => 'Enabled', + '已取消' => 'Cancelled', + '已生效' => 'Effective', + '锁定中' => 'Locked', + '锁定' => 'Lock', + '解 锁' => 'Unlock', + '锁 定' => 'Lock', + '删 除' => 'Delete', + '编 辑' => 'Edit', + '保存数据' => 'Save Data', + '取消编辑' => 'Cancel Edit', + '确定要永久删除吗?' => 'Are you sure you want to permanently delete?', + '确定要删除选中的记录吗?' => 'Are you sure you want to delete selected records?', + '确定要删除文章吗?' => 'Are you sure you want to delete the article?', + '确定要取消编辑吗?' => 'Are you sure you want to cancel editing?', + '确定要取消吗?' => 'Are you sure you want to cancel?', + '全部' => 'All', + '搜 索' => 'Search', + '导 出' => 'Export', + '条件搜索' => 'Condition Search', + '创建时间' => 'Create Time', + '操作时间' => 'Operation Time', + '更新时间' => 'Update Time', + '时间' => 'Time', + '当前状态' => 'Current Status', + '取消' => 'Cancel', + '生效' => 'Effective', + '使用' => 'Use', + '支付' => 'Payment', + '生成' => 'Generated', + '单号不能为空!' => 'Transaction code is required', + '状态不能为空!' => 'Status is required', + '状态值范围异常!' => 'Status value is out of range', + '状态值不能为空!' => 'Status value is required', + '交易操作成功!' => 'Transaction updated successfully', + '二维码图片' => 'QR Code Image', + '获取余额记录!' => 'Balance records loaded successfully', + '获取积分记录!' => 'Integral records loaded successfully', + '账号不存在!' => 'Account does not exist', + '无效的操作编号!' => 'Invalid operation code', + '创建支付成功' => 'Payment created successfully', + '创建支付成功!' => 'Payment created successfully', + '获取预支付码失败!' => 'Failed to get prepay code', + '无需退款!' => 'No refund is required', + '发起退款成功!' => 'Refund requested successfully', + '已提交退款!' => 'Refund submitted successfully', + '支付未完成!' => 'Payment is not completed', + '退款单已存在!' => 'Refund record already exists', + '退款金额溢出!' => 'Refund amount exceeds the paid amount', + '支付金额溢出!' => 'Payment amount exceeds the order total', + '支付大于金额!' => 'Payment amount exceeds the order total', + '已经完成支付!' => 'Payment has already been completed', + '无效的支付单!' => 'Invalid payment record', + '获取 %s 字段值失败!' => 'Failed to get field %s', + '%s已被禁用!' => '%s has been disabled', + '支付配置[#%s]参数异常!' => 'Payment configuration [#%s] is invalid', + '支付配置[#%s]参数无效!' => 'Payment configuration [#%s] parameters are invalid', + '支付配置[@%s]未启用!' => 'Payment configuration [@%s] is not enabled', + '在线支付' => 'Online Payment', + '无需支付' => 'No payment required', + '状态不在范围!' => 'Status is out of range', + '姓名不能为空!' => 'Recipient name is required', + '手机格式错误!' => 'Phone format is invalid', + '手机不能为空!' => 'Phone number is required', + '省份不能为空!' => 'Province is required', + '城市不能为空!' => 'City is required', + '区域不能为空!' => 'Area is required', + '地址不能为空!' => 'Address is required', + '最多10个地址!' => 'A maximum of 10 addresses is allowed', + '保存成功!' => 'Saved successfully', + '保存失败!' => 'Save failed', + '获取地址数据!' => 'Address data loaded successfully', + '编号不能为空!' => 'ID is required', + '地址不存在!' => 'Address does not exist', + '设置默认成功!' => 'Default address set successfully', + '地址ID为空!' => 'Address ID is required', + '删除成功!' => 'Deleted successfully', + '删除失败!' => 'Delete failed', + '刷新用户余额及积分完成!' => 'Balance and integral refresh completed', + '开始刷新用户 [%s %s] 余额及积分' => 'Start refreshing user [%s %s] balance and integral', + '刷新用户 [%s %s] 余额及积分' => 'Refreshed user [%s %s] balance and integral', + '刷新用户 [%s %s] 余额及积分失败, %s' => 'Failed to refresh user [%s %s] balance and integral, %s', + + // 余额管理 + '余额明细管理' => 'Balance Ledger Management', + '余额管理' => 'Balance Management', + '余额统计' => 'Balance Statistics', + '累计充值' => 'Total Recharge', + '已消费' => 'Consumed', + '剩余可用余额' => 'Available Balance', + '用户账号' => 'User Account', + '用户昵称' => 'User Nickname', + '绑定账号' => 'Bound Account', + '交易金额' => 'Transaction Amount', + '交易单号' => 'Transaction Code', + '交易状态' => 'Transaction Status', + '操作描述' => 'Operation Description', + '操作名称' => 'Operation Name', + '状态操作' => 'Status Operation', + '取消时间' => 'Cancel Time', + '生效时间' => 'Effective Time', + '锁定时间' => 'Lock Time', + '元' => 'Yuan', + '扣减余额不足!' => 'Insufficient balance for deduction', + '余额变更失败!' => 'Failed to update balance', + '账号余额退款' => 'Balance Refund', + '账户余额不足' => 'Insufficient account balance', + '支付订单 %s 金额 %s 元' => 'Pay order %s amount %s yuan', + '余额支付完成!' => 'Balance payment completed', + '来自订单 %s 退回余额' => 'Refund balance from order %s', + + // 积分管理 + '积分明细管理' => 'Integral Ledger Management', + '积分管理' => 'Integral Management', + '积分统计' => 'Integral Statistics', + '累计发放' => 'Total Issued', + '剩余可用' => 'Available', + '积分' => 'Integral', + '操作备注' => 'Operation Remark', + '请输入用户账号' => 'Please enter user account', + '请输入交易单号' => 'Please enter transaction code', + '请输入操作名称' => 'Please enter operation name', + '请输入操作备注' => 'Please enter operation remark', + '扣减积分不足!' => 'Insufficient integral for deduction', + '积分变更失败!' => 'Failed to update integral', + '账号积分退还' => 'Integral Refund', + '可抵扣的积分不足' => 'Insufficient integral available for deduction', + '抵扣订单 %s 金额 %s 元' => 'Deduct order %s amount %s yuan', + '账户积分支付' => 'Integral Payment', + '积分抵扣完成!' => 'Integral deduction completed', + '来自订单 %s 退回积分' => 'Refund integral from order %s', + + // 支付管理 + '支付配置管理' => 'Payment Configuration Management', + '支付行为管理' => 'Payment Activity Management', + '支付退款管理' => 'Payment Refund Management', + '支付管理' => 'Payment Management', + '支付配置' => 'Payment Configuration', + '添加支付配置' => 'Add Payment Configuration', + '编辑支付配置' => 'Edit Payment Configuration', + '添加支付' => 'Add Payment', + '批量删除' => 'Batch Delete', + '图标' => 'Icon', + '支付方式' => 'Payment Type', + '支付编号' => 'Payment Code', + '支付类型' => 'Payment Type', + '支付名称' => 'Payment Name', + '支付图标' => 'Payment Icon', + '终端授权' => 'Terminal Authorization', + '请输入支付编号' => 'Please enter payment code', + '请输入支付名称' => 'Please enter payment name', + '请上传支付图标' => 'Please upload payment icon', + '请输入支付描述' => 'Please enter payment description', + '必选,' => 'Required selection, ', + '请选择预置的支付方式,支付方式创建之后不能修改。' => 'Please select a preset payment type. The payment type cannot be changed after creation.', + '请填写支付名称,支付名称尽量不要重复,建议字符长度为 4-8 个汉字。' => 'Please enter a payment name. Avoid duplicates. A concise name is recommended.', + '兑换积分不能少于1积分!' => 'The integral exchange ratio must be at least 1', + '配置保存成功!' => 'Configuration saved successfully', + '配置保存失败!' => 'Failed to save configuration', + '请选择支付方式!' => 'Please select a payment type', + '请上传支付图标!' => 'Please upload a payment icon', + '参数保存成功!' => 'Parameters saved successfully', + '积分抵扣配置' => 'Integral Deduction Settings', + '支付方式开关' => 'Payment Channel Switch', + '积分可抵扣' => 'points can deduct', + '线下支付二维码' => 'Offline Payment QR Code', + '请上传线下支付二维码图片' => 'Please upload offline payment QR code image', + '凭证待审核!' => 'Voucher pending review', + '无效优惠券!' => 'Invalid coupon', + '优惠券已使用!' => 'Coupon has already been used', + '使用优惠券抵扣' => 'Coupon deduction', + '优惠券抵扣完成!' => 'Coupon deduction completed', + '凭证不能为空!' => 'Voucher is required', + '上传成功!' => 'Uploaded successfully', + '支付回跳地址不能为空!' => 'Payment return URL is required', + '支付类型[%s]暂时不支持!' => 'Payment type [%s] is not supported yet', + + // 支付记录 + '业务单号' => 'Business Order No.', + '订单内容' => 'Order Content', + '订单标题' => 'Order Title', + '订单编号' => 'Order Number', + '订单名称' => 'Order Name', + '订单金额' => 'Order Amount', + '需支付' => 'Need to Pay', + '已付' => 'Paid', + '待审' => 'Pending Review', + '支付描述' => 'Payment Description', + '支付状态' => 'Payment Status', + '支付单据凭证' => 'Payment Voucher', + '已支付' => 'Paid', + '未支付' => 'Unpaid', + '待支付' => 'Pending Payment', + '待审核' => 'Pending Review', + '已拒绝' => 'Rejected', + '已完成' => 'Completed', + '已退款' => 'Refunded', + '支付时间' => 'Payment Time', + '用户编号' => 'User Code', + '支付行为数据' => 'Payment Behavior Data', + '审核操作类型' => 'Audit Action Type', + '驳回凭证' => 'Reject Voucher', + '审核通过' => 'Approve Voucher', + '订单审核描述' => 'Order Audit Remark', + '请输入订单审核描述' => 'Please enter order audit remark', + '取消操作' => 'Cancel Action', + '请选择通过或驳回!' => 'Please approve or reject the voucher', + '支付记录不存在!' => 'Payment record does not exist', + '无需审核操作!' => 'No audit action is required', + '该凭证已审核!' => 'This voucher has already been audited', + '后台支付凭证被驳回' => 'The backend payment voucher was rejected', + '后台支付凭证已通过' => 'The backend payment voucher was approved', + '凭证审核驳回!' => 'Voucher audit rejected', + '凭证审核通过!' => 'Voucher audit approved', + '凭证审核失败!' => 'Voucher audit failed', + '支付单号不能为空!' => 'Payment code is required', + '支付单号异常!' => 'Payment code is invalid', + '未完成支付!' => 'Payment is not completed', + '重新触发支付行为!' => 'Payment event retriggered successfully', + '支付号不能为空!' => 'Payment ID is required', + '支付凭证已查验' => 'Payment voucher has been verified', + '重发通知' => 'Resend Notice', + '取消审核' => 'Cancel Audit', + '已经退款' => 'Already Refunded', + '确认要取消审核吗?' => 'Are you sure you want to cancel this audit?', + '支付凭证审核' => 'Payment Voucher Audit', + '凭证审核' => 'Voucher Audit', + '已经拒绝' => 'Already Rejected', + '已经通过' => 'Already Approved', + '无需操作' => 'No Action Required', + '订单无需支付!' => 'No payment is required for this order', + + // 退款管理 + '退款内容' => 'Refund Content', + '退款数据' => 'Refund Data', + '用户姓名' => 'User Name', + '请输入用户姓名' => 'Please enter user name', + '请输入订单内容' => 'Please enter order content', + '请选择创建时间' => 'Please select create time', + + // 支付渠道与终端 + '订单无需支付' => 'No Payment Required', + '优惠券抵扣' => 'Coupon Deduction', + '账户余额支付' => 'Balance Payment', + '账户积分抵扣' => 'Integral Deduction', + '单据凭证支付' => 'Voucher Payment', + '微信WAP支付' => 'WeChat WAP Payment', + '微信APP支付' => 'WeChat APP Payment', + '微信小程序支付' => 'WeChat Mini Program Payment', + '微信公众号支付' => 'WeChat Official Account Payment', + '微信二维码支付' => 'WeChat QR Code Payment', + '支付宝WAP支付' => 'Alipay WAP Payment', + '支付宝WEB支付' => 'Alipay WEB Payment', + '支付宝APP支付' => 'Alipay APP Payment', + '手机浏览器' => 'Mobile Browser', + '电脑浏览器' => 'Desktop Browser', + '微信小程序' => 'WeChat Mini Program', + '微信服务号' => 'WeChat Official Account', + '苹果APP应用' => 'iOS App', + '安卓APP应用' => 'Android App', + + // 支付宝配置 + '支付宝商户编号' => 'Alipay Merchant ID', + '请输入支付宝商户编号(必填)' => 'Please enter Alipay merchant ID (required)', + '支付宝商户编号,开通企业支付宝的唯一商户编号' => 'The unique merchant ID for the Alipay business account.', + '支付宝私钥文件内容' => 'Alipay Private Key Content', + '请输入支付宝私钥文件内容(必填)' => 'Please enter Alipay private key content (required)', + '从商户平台上下载支付证书,解压并取得其中的支付宝私钥文件用记事本打开并复制文件内容填至此处' => 'Download the payment certificate from the merchant platform, extract the Alipay private key file, open it with a text editor, and paste the full content here.', + '应用公钥证书文件内容' => 'Application Public Certificate Content', + '请输入应用公钥证书文件内容(必填)' => 'Please enter application public certificate content (required)', + '从商户平台上下载支付证书,解压并取得其中的应用公钥证书文件用记事本打开并复制文件内容填至此处' => 'Download the payment certificate from the merchant platform, extract the application public certificate file, open it with a text editor, and paste the full content here.', + + // 汇聚支付配置 + '商户绑定的公众号' => 'Bound Official Account', + '请输入商户绑定的公众号(必填)' => 'Please enter the bound official account APPID (required)', + '商户绑定的公众号,授权给汇聚支付平台的公众号APPID' => 'The official account APPID authorized to the JoinPay platform.', + '汇聚支付报备商户号' => 'JoinPay Reporting Merchant ID', + '请输入汇聚支付报备商户号(必填)' => 'Please enter the JoinPay reporting merchant ID (required)', + '汇聚支付报备商户号,需要联系汇聚支付平台的客服获取,通常以 777 开头的15位数字!' => 'The JoinPay reporting merchant ID is provided by JoinPay support and is usually a 15-digit number starting with 777.', + '汇聚支付的商户编号' => 'JoinPay Merchant ID', + '请输入汇聚支付的商户编号(必填)' => 'Please enter the JoinPay merchant ID (required)', + '汇聚支付的商户编号,需要在汇聚支付平台商户中心获取,通常是以 888 开头的15位数字!' => 'The JoinPay merchant ID can be found in the JoinPay merchant center and is usually a 15-digit number starting with 888.', + '汇聚支付的商户密钥' => 'JoinPay Merchant Key', + '请输入汇聚支付的商户密钥(必填)' => 'Please enter the JoinPay merchant key (required)', + '汇聚支付的商户密钥,需要在汇聚支付平台商户中心的密钥管理处获取,通常为32位字符串!' => 'The JoinPay merchant key is available in the JoinPay merchant center key management page and is usually a 32-character string.', + + // 微信支付配置 + '绑定公众号' => 'Bound Official Account', + '公众号APPID' => 'WeChat Official Account APPID', + '请输入18位绑定公众号(必填)' => 'Please enter 18-digit bound official account (required)', + '公众号APPID,微信商户绑定的 服务号APPID 或 小程序APPID' => 'The service account APPID or mini program APPID bound to the WeChat merchant account.', + '微信商户号' => 'WeChat Merchant Number', + '请输入微信商户号(必填)' => 'Please enter WeChat merchant number (required)', + '微信商户编号,需要在微信商户平台获取,微信商户号 与 公众号APPID 匹配' => 'The WeChat merchant number must be obtained from the WeChat merchant platform and must match the APPID.', + '商户接口版本' => 'Merchant API Version', + '微信支付 V2 接口' => 'WeChat Payment V2 API', + '微信支付 V3 接口' => 'WeChat Payment V3 API', + '微信商户 V2 密钥' => 'WeChat Merchant V2 Key', + '微信商户 V3 密钥' => 'WeChat Merchant V3 Key', + '商户密钥' => 'Merchant Key', + '请输入32位微信商户密钥(必填)' => 'Please enter 32-digit WeChat merchant key (required)', + '微信商户密钥,需要在微信商户平台操作设置密码并获取密钥,建议定期更换密钥。' => 'The WeChat merchant key must be configured in the WeChat merchant platform. Regular rotation is recommended.', + '微信商户证书' => 'WeChat Merchant Certificate', + '商户证书公钥序号' => 'Merchant Certificate Serial Number', + '支付公钥ID' => 'Payment Public Key ID', + '请输入商户证书公钥序号' => 'Please enter merchant certificate public key serial number', + '商户证书公钥序号,需要在微信商户平台生成商户证书时,可以获取到公钥序号。' => 'The merchant certificate serial number is available when generating the merchant certificate in the WeChat merchant platform.', + '商户证书公钥内容' => 'Merchant Certificate Public Key Content', + '证书内容' => 'Certificate Content', + '请输入商户证书公钥内容' => 'Please enter merchant certificate public key content', + '必填,' => 'Required, ', + '从商户平台上下载支付证书,解压并取得其中的 apiclient_cert.pem 用记事本打开并复制文件内容填至此处。' => 'Download the payment certificate from the merchant platform, extract it and get apiclient_cert.pem, open it with Notepad and copy the file content to fill here.', + '密钥内容' => 'Key Content', + '请输入微信商户密钥文件内容' => 'Please enter WeChat merchant key file content', + '从商户平台上下载支付证书,解压并取得其中的 apiclient_key.pem 用记事本打开并复制文件内容填至此处。' => 'Download the payment certificate from the merchant platform, extract it and get apiclient_key.pem, open it with Notepad and copy the file content to fill here.', + '微信支付公钥' => 'WeChat Payment Public Key', + '微信支付公钥 ID' => 'WeChat Payment Public Key ID', + '请输入微信支付公钥ID' => 'Please enter WeChat payment public key ID', + '支付公钥' => 'Payment Public Key', + '微信支付公钥内容' => 'WeChat Payment Public Key Content', + '( 需要填写文件的全部内容 )' => '(Need to fill in the full content of the file)', + '请输入微信支付公钥内容' => 'Please enter WeChat payment public key content', + '可选,' => 'Optional, ', + '从商户平台上下载支付证书,解压并取得其中的 pub_key.pem 用记事本打开并复制文件内容填至此处。' => 'Download the payment certificate from the merchant platform, extract it and get pub_key.pem, open it with Notepad and copy the file content to fill here.', +]); diff --git a/plugin/think-plugs-payment/src/model/PluginPaymentAddress.php b/plugin/think-plugs-payment/src/model/PluginPaymentAddress.php new file mode 100644 index 000000000..2a543233f --- /dev/null +++ b/plugin/think-plugs-payment/src/model/PluginPaymentAddress.php @@ -0,0 +1,59 @@ +setExtraAttr($value); + } + + /** + * 格式化数据格式. + * @param mixed $value + */ + public function getContentAttr($value): array + { + return $this->getExtraAttr($value); + } +} diff --git a/plugin/think-plugs-payment/src/model/PluginPaymentIntegral.php b/plugin/think-plugs-payment/src/model/PluginPaymentIntegral.php new file mode 100644 index 000000000..6d21c8f0b --- /dev/null +++ b/plugin/think-plugs-payment/src/model/PluginPaymentIntegral.php @@ -0,0 +1,33 @@ +hasOne(PluginAccountUser::class, 'id', 'unid'); + } +} diff --git a/plugin/think-plugs-payment/src/model/PluginPaymentRecord.php b/plugin/think-plugs-payment/src/model/PluginPaymentRecord.php new file mode 100644 index 000000000..98b7a3641 --- /dev/null +++ b/plugin/think-plugs-payment/src/model/PluginPaymentRecord.php @@ -0,0 +1,64 @@ +hasOne(PluginAccountUser::class, 'id', 'unid'); + } + + public function device(): HasOne + { + return $this->hasOne(PluginAccountBind::class, 'id', 'usid'); + } + + public function getUserAttr($value): array + { + return is_array($value) ? $value : []; + } + + public function setPaymentNotifyAttr($value): string + { + return $this->setExtraAttr($value); + } + + public function getPaymentNotifyAttr($value): array + { + return $this->getExtraAttr($value); + } + + public function toArray(): array + { + $data = parent::toArray(); + if (isset($data['channel_type'])) { + $data['channel_type_name'] = Payment::typeName($data['channel_type']); + } + return $data; + } +} diff --git a/plugin/think-plugs-payment/src/model/PluginPaymentRefund.php b/plugin/think-plugs-payment/src/model/PluginPaymentRefund.php new file mode 100644 index 000000000..bac282bfc --- /dev/null +++ b/plugin/think-plugs-payment/src/model/PluginPaymentRefund.php @@ -0,0 +1,38 @@ +hasOne(PluginAccountUser::class, 'id', 'unid'); + } + + public function record(): HasOne + { + return $this->hasOne(PluginPaymentRecord::class, 'code', 'record_code'); + } +} diff --git a/plugin/think-plugs-payment/src/service/Balance.php b/plugin/think-plugs-payment/src/service/Balance.php new file mode 100644 index 000000000..99af8d0e7 --- /dev/null +++ b/plugin/think-plugs-payment/src/service/Balance.php @@ -0,0 +1,197 @@ +findOrEmpty($unid); + if ($user->isEmpty()) { + throw new Exception(lang('账号不存在!')); + } + + // 扣减余额检查 + $map = ['unid' => $unid, 'cancel' => 0]; + $usable = PluginPaymentBalance::mk()->where($map)->sum('amount'); + $amountValue = strval($amount); + $isDecrease = bccomp($amountValue, '0', 2) < 0; + $decrease = ltrim($amountValue, '-'); + if ($isDecrease && bccomp($decrease, strval($usable), 2) === 1) { + throw new Exception(lang('扣减余额不足!')); + } + + // 余额标准字段 + $data = ['unid' => $unid, 'code' => $code, 'name' => $name, 'amount' => $amountValue, 'remark' => $remark]; + + // 锁定状态处理 + $data['unlock'] = intval($unlock); + if ($data['unlock']) { + $data['unlock_time'] = date('Y-m-d H:i:s'); + } + + // 统计操作前的金额 + $data['amount_prev'] = $usable; + $data['amount_next'] = bcadd(strval($usable), $amountValue, 2); + + // 检查编号是否重复 + $map = ['unid' => $unid, 'code' => $code]; + $model = PluginPaymentBalance::mk()->where($map)->findOrEmpty(); + + // 更新或写入余额变更 + if ($model->save($data)) { + self::recount($unid); + return $model->refresh(); + } + throw new Exception(lang('余额变更失败!')); + } + + /** + * 解锁余额变更操作. + * @param string $code 交易订单 + * @param int $unlock 锁定状态 + * @throws Exception + */ + public static function unlock(string $code, int $unlock = 1): PluginPaymentBalance + { + return self::set($code, ['unlock' => $unlock, 'unlock_time' => date('Y-m-d H:i:s')]); + } + + /** + * 作废余额变更操作. + * @param string $code 交易订单 + * @param int $cancel 取消状态 + * @throws Exception + */ + public static function cancel(string $code, int $cancel = 1): PluginPaymentBalance + { + return self::set($code, ['cancel' => $cancel, 'cancel_time' => date('Y-m-d H:i:s')]); + } + + /** + * 删除余额记录. + * @throws Exception + */ + public static function remove(string $code): PluginPaymentBalance + { + $model = self::get($code); + $unid = intval($model->getAttr('unid')); + $key = $model->getKey(); + $model->delete(); + self::recount($unid); + return PluginPaymentBalance::mk()->withTrashed()->findOrEmpty($key); + } + + /** + * 刷新用户余额. + * @param int $unid 指定用户编号 + * @param null|array &$data 非数组时更新数据 + * @return array [lock,used,total,usable] + * @throws Exception + */ + public static function recount(int $unid, ?array &$data = null): array + { + $isUpdate = !is_array($data); + if ($isUpdate) { + $data = []; + } + + if ($isUpdate) { + $user = PluginAccountUser::mk()->findOrEmpty($unid); + if ($user->isEmpty()) { + throw new Exception(lang('账号不存在!')); + } + } + + // 统计用户余额数据 + $map = ['unid' => $unid, 'cancel' => 0]; + $lock = PluginPaymentBalance::mk()->where($map)->where('unlock', '=', '0')->sum('amount'); + $used = PluginPaymentBalance::mk()->where($map)->where('amount', '<', '0')->sum('amount'); + $total = PluginPaymentBalance::mk()->where($map)->where('amount', '>', '0')->sum('amount'); + + // 更新余额统计 + $data['balance_lock'] = strval($lock); + $data['balance_used'] = bcmul(strval($used), '-1', 2); + $data['balance_total'] = strval($total); + $data['balance_usable'] = bcsub($data['balance_total'], $data['balance_used'], 2); + if ($isUpdate) { + $user->save(['extra' => array_merge($user->getAttr('extra'), $data)]); + } + return ['lock' => $lock, 'used' => abs($used), 'total' => $total, 'usable' => $data['balance_usable']]; + } + + /** + * 获取余额模型. + * @throws Exception + */ + public static function get(string $code): PluginPaymentBalance + { + $map = ['code' => $code]; + $model = PluginPaymentBalance::mk()->where($map)->findOrEmpty(); + if ($model->isEmpty()) { + throw new Exception(lang('无效的操作编号!')); + } + return $model; + } + + /** + * 更新余额记录. + * @throws Exception + */ + public static function set(string $code, array $data): PluginPaymentBalance + { + ($model = self::get($code))->save($data); + self::recount(intval($model->getAttr('unid'))); + return PluginPaymentBalance::mk()->withTrashed()->findOrEmpty($model->getKey()); + } +} diff --git a/plugin/think-plugs-payment/src/service/Integral.php b/plugin/think-plugs-payment/src/service/Integral.php new file mode 100644 index 000000000..a5b105617 --- /dev/null +++ b/plugin/think-plugs-payment/src/service/Integral.php @@ -0,0 +1,208 @@ +findOrEmpty($unid); + if ($user->isEmpty()) { + throw new Exception(lang('账号不存在!')); + } + + // 扣减积分检查 + $map = ['unid' => $unid, 'cancel' => 0]; + $usable = PluginPaymentIntegral::mk()->where($map)->sum('amount'); + $amountValue = strval($amount); + $isDecrease = bccomp($amountValue, '0', 2) < 0; + $decrease = ltrim($amountValue, '-'); + if ($isDecrease && bccomp($decrease, strval($usable), 2) === 1) { + throw new Exception(lang('扣减积分不足!')); + } + + // 积分标准字段 + $data = ['unid' => $unid, 'code' => $code, 'name' => $name, 'amount' => $amountValue, 'remark' => $remark]; + + // 统计操作前的金额 + $data['amount_prev'] = $usable; + $data['amount_next'] = bcadd(strval($usable), $amountValue, 2); + + // 锁定状态处理 + $data['unlock'] = intval($unlock); + if ($data['unlock']) { + $data['unlock_time'] = date('Y-m-d H:i:s'); + } + + // 检查编号是否重复 + $map = ['unid' => $unid, 'code' => $code]; + $model = PluginPaymentIntegral::mk()->where($map)->findOrEmpty(); + + // 更新或写入积分变更 + if ($model->save($data)) { + self::recount($unid); + return $model->refresh(); + } + throw new Exception(lang('积分变更失败!')); + } + + /** + * 解锁积分变更操作. + * @param string $code 交易订单 + * @param int $unlock 锁定状态 + * @throws Exception + */ + public static function unlock(string $code, int $unlock = 1): PluginPaymentIntegral + { + return self::set($code, ['unlock' => $unlock, 'unlock_time' => date('Y-m-d H:i:s')]); + } + + /** + * 作废积分变更操作. + * @param string $code 交易订单 + * @param int $cancel 取消状态 + * @throws Exception + */ + public static function cancel(string $code, int $cancel = 1): PluginPaymentIntegral + { + return self::set($code, ['cancel' => $cancel, 'cancel_time' => date('Y-m-d H:i:s')]); + } + + /** + * 删除积分记录. + * @throws Exception + */ + public static function remove(string $code): PluginPaymentIntegral + { + $model = self::get($code); + $unid = intval($model->getAttr('unid')); + $key = $model->getKey(); + $model->delete(); + self::recount($unid); + return PluginPaymentIntegral::mk()->withTrashed()->findOrEmpty($key); + } + + /** + * 刷新用户积分. + * @param int $unid 指定用户编号 + * @param null|array &$data 非数组时更新数据 + * @return array [lock,used,total,usable] + * @throws Exception + */ + public static function recount(int $unid, ?array &$data = null): array + { + $isUpdate = !is_array($data); + if ($isUpdate) { + $data = []; + } + if ($isUpdate) { + $user = PluginAccountUser::mk()->findOrEmpty($unid); + if ($user->isEmpty()) { + throw new Exception(lang('账号不存在!')); + } + } + // 统计用户积分数据 + $map = ['unid' => $unid, 'cancel' => 0]; + $lock = PluginPaymentIntegral::mk()->where($map)->where('unlock', '=', '0')->sum('amount'); + $used = PluginPaymentIntegral::mk()->where($map)->where('amount', '<', '0')->sum('amount'); + $total = PluginPaymentIntegral::mk()->where($map)->where('amount', '>', '0')->sum('amount'); + + // 更新积分统计 + $data['integral_lock'] = strval($lock); + $data['integral_used'] = bcmul(strval($used), '-1', 2); + $data['integral_total'] = strval($total); + $data['integral_usable'] = bcsub($data['integral_total'], $data['integral_used'], 2); + if ($isUpdate) { + $user->save(['extra' => array_merge($user->getAttr('extra'), $data)]); + } + return ['lock' => $lock, 'used' => abs($used), 'total' => $total, 'usable' => $data['integral_usable']]; + } + + /** + * 获取积分模型. + * @throws Exception + */ + public static function get(string $code): PluginPaymentIntegral + { + $map = ['code' => $code]; + $model = PluginPaymentIntegral::mk()->where($map)->findOrEmpty(); + if ($model->isEmpty()) { + throw new Exception(lang('无效的操作编号!')); + } + return $model; + } + + /** + * 更新积分记录. + * @throws Exception + */ + public static function set(string $code, array $data): PluginPaymentIntegral + { + ($model = self::get($code))->save($data); + self::recount(intval($model->getAttr('unid'))); + return PluginPaymentIntegral::mk()->withTrashed()->findOrEmpty($model->getKey()); + } +} diff --git a/plugin/think-plugs-payment/src/service/Payment.php b/plugin/think-plugs-payment/src/service/Payment.php new file mode 100644 index 000000000..4a479897b --- /dev/null +++ b/plugin/think-plugs-payment/src/service/Payment.php @@ -0,0 +1,551 @@ + [ + 'name' => '订单无需支付', + 'class' => EmptyPayment::class, + 'status' => 1, + 'account' => [], + ], + // 优惠券抵扣,只维护编号+金额 + self::COUPON => [ + 'name' => '优惠券抵扣', + 'class' => CouponPayment::class, + 'status' => 1, + 'account' => [ + Account::WAP, + Account::WEB, + Account::WXAPP, + Account::WECHAT, + Account::IOSAPP, + Account::ANDROID, + ], + ], + // 余额支付,使用账户余额支付 + self::BALANCE => [ + 'name' => '账户余额支付', + 'class' => BalancePayment::class, + 'status' => 1, + 'account' => [ + Account::WAP, + Account::WEB, + Account::WXAPP, + Account::WECHAT, + Account::IOSAPP, + Account::ANDROID, + ], + ], + // 积分抵扣,使用账户积分抵扣 + self::INTEGRAL => [ + 'name' => '账户积分抵扣', + 'class' => IntegralPayment::class, + 'status' => 1, + 'account' => [ + Account::WAP, + Account::WEB, + Account::WXAPP, + Account::WECHAT, + Account::IOSAPP, + Account::ANDROID, + ], + ], + // 凭证支付,上传凭证后台审核支付 + self::VOUCHER => [ + 'name' => '单据凭证支付', + 'class' => VoucherPayment::class, + 'status' => 1, + 'account' => [ + Account::WAP, + Account::WEB, + Account::WXAPP, + Account::WECHAT, + Account::IOSAPP, + Account::ANDROID, + ], + ], + // 微信支付配置(不需要的直接注释) + self::WECHAT_WAP => [ + 'name' => '微信WAP支付', + 'class' => WechatPayment::class, + 'status' => 1, + 'account' => [Account::WAP], + ], + self::WECHAT_APP => [ + 'name' => '微信APP支付', + 'class' => WechatPayment::class, + 'status' => 1, + 'account' => [Account::IOSAPP, Account::ANDROID], + ], + self::WECHAT_XCX => [ + 'name' => '微信小程序支付', + 'class' => WechatPayment::class, + 'status' => 1, + 'account' => [Account::WXAPP], + ], + self::WECHAT_GZH => [ + 'name' => '微信公众号支付', + 'class' => WechatPayment::class, + 'status' => 1, + 'account' => [Account::WECHAT], + ], + self::WECHAT_QRC => [ + 'name' => '微信二维码支付', + 'class' => WechatPayment::class, + 'status' => 1, + 'account' => [Account::WEB], + ], + // 支付宝支持配置(不需要的直接注释) + self::ALIPAY_WAP => [ + 'name' => '支付宝WAP支付', + 'class' => AliPayment::class, + 'status' => 1, + 'account' => [Account::WAP], + ], + self::ALIPAY_WEB => [ + 'name' => '支付宝WEB支付', + 'class' => AliPayment::class, + 'status' => 1, + 'account' => [Account::WEB], + ], + self::ALIAPY_APP => [ + 'name' => '支付宝APP支付', + 'class' => AliPayment::class, + 'status' => 1, + 'account' => [Account::ANDROID, Account::IOSAPP], + ], + // 汇聚支持配置(不需要的直接注释) + /* self::JOINPAY_XCX => [ + 'name' => '汇聚小程序支付', + 'class' => JoinPayment::class, + 'status' => 1, + 'account' => [Account::WXAPP], + ], + self::JOINPAY_GZH => [ + 'name' => '汇聚公众号支付', + 'class' => JoinPayment::class, + 'status' => 1, + 'account' => [Account::WECHAT], + ], */ + ]; + + /** + * 实例化支付配置. + * @param string $code 编号或类型 + * @throws Exception + */ + public static function mk(string $code): PaymentInterface + { + if (in_array($code, [self::EMPTY, self::COUPON, self::BALANCE, self::INTEGRAL, self::VOUCHER], true)) { + if (empty(self::$types[$code]['status'])) { + throw new Exception(lang('%s已被禁用!', [self::typeName($code)])); + } + return self::$types[$code]['class']::mk($code, $code, []); + } + [$type, $attr, $params] = self::params($code); + if (self::typeStatus($type)) { + return $attr['class']::mk($code, $type, $params); + } + throw new Exception(lang('%s已被禁用!', [self::typeName($type)])); + } + + /** + * 获取支付参数. + * @param string $code 支付配置编号 + * @param array $config 支付配置参数 + * @return array [type, attr, params] + * @throws Exception + */ + public static function params(string $code, array $config = []): array + { + try { + if (empty($config)) { + $map = ['code' => $code, 'status' => 1]; + $config = PluginPaymentConfig::mk()->where($map)->findOrEmpty()->toArray(); + } + if (empty($config)) { + throw new Exception(lang('支付配置[#%s]参数异常!', [$code])); + } + $params = is_string($config['content']) ? @json_decode($config['content'], true) : $config['content']; + if (empty($params)) { + throw new Exception(lang('支付配置[#%s]参数无效!', [$code])); + } + + if (empty(self::$types[$config['type']]['status'])) { + throw new Exception(lang('支付配置[@%s]未启用!', [$config['type']])); + } + return [$config['type'], self::$types[$config['type']], $params]; + } catch (\Exception $exception) { + throw new Exception($exception->getMessage(), $exception->getCode()); + } + } + + /** + * 添加支付方式. + * @param string $type 支付编码 + * @param string $name 支付名称 + * @param string $class 处理机制 + * @param array $account 绑定终端 + * @return array[] + */ + public static function add(string $type, string $name, string $class, array $account = []): array + { + if (class_exists($class) && in_array(PaymentInterface::class, class_implements($class))) { + self::$types[$type] = ['name' => $name, 'class' => $class, 'status' => 1, 'account' => $account]; + } + return self::types(); + } + + /** + * 设置方式状态 + * @param string $type 支付编码 + * @param int $status 支付状态 + */ + public static function set(string $type, int $status): bool + { + if (isset(self::$types[$type])) { + self::$types[$type]['status'] = $status; + return true; + } + return false; + } + + /** + * 保存支付方式. + * @return int|true + * @throws Exception + */ + public static function save() + { + self::$denys = []; + foreach (self::types() as $k => $v) { + if (empty($v['status'])) { + self::$denys[] = $k; + } + } + return sysdata(self::$cakey, self::$denys); + } + + /** + * 获取支付方式. + */ + public static function types(?int $status = null): array + { + try { + [$all, $binds] = [[], array_keys(Account::types(1))]; + foreach (self::init() as $type => $item) { + if (is_null($status) || $status == $item['status']) { + if (array_intersect($item['account'], $binds)) { + $all[$type] = $item; + } + } + } + return $all; + } catch (\Exception $exception) { + return []; + } + } + + /** + * 通过接口类型筛选支付方式. + * @param string $account 指定终端 + * @param bool $getfull 读取参数 + */ + public static function typesByAccess(string $account, bool $getfull = false): array + { + $types = []; + foreach (self::types(1) as $type => $attr) { + if (in_array($account, $attr['account'])) { + $types[$type] = $attr['name']; + } + } + if ($getfull) { + $items = []; + $query = PluginPaymentConfig::mk()->field('type,code,name,cover,content'); + $query->where(['status' => 1])->whereIn('type', array_keys($types)); + foreach ($query->order('sort desc,id desc')->cursor() as $item) { + $item['qrcode'] = $item['content']['voucher_qrcode'] ?? ''; + unset($item['content']); + $items[] = $item->toArray(); + } + return $items; + } + return $types; + } + + /** + * 读取支付配置. + */ + public static function items(): array + { + $map = ['status' => 1]; + return PluginPaymentConfig::mk()->where($map)->order('sort desc,id desc')->column('type,code,name', 'code'); + } + + /** + * 获取支付类型名称. + */ + public static function typeName(string $type): string + { + return lang(self::$types[$type]['name'] ?? $type); + } + + /** + * 判断支付类型状态 + */ + public static function typeStatus(string $type): bool + { + return !empty(self::$types[$type]['status']); + } + + /** + * 判断是否完成支付. + * @param string $orderNo 原订单号 + * @param string $amount 需支付金额 + */ + public static function isPayed(string $orderNo, string $amount): bool + { + $paidAmount = strval(self::paidAmount($orderNo)); + return bccomp($paidAmount, $amount, 2) >= 0; + } + + /** + * 发起订单整体退款. + * @throws Exception + */ + public static function refund(string $orderNo) + { + $items = PluginPaymentRecord::mq()->where(function (Query $query) { + $query->whereOr([['payment_status', '=', 1], ['audit_status', '>', '0']]); + })->where(['order_no' => $orderNo])->column('code,channel_code,payment_amount'); + foreach ($items as $item) { + static::mk($item['channel_code'])->refund($item['code'], $item['payment_amount']); + } + } + + /** + * 获取已支付金额. + * @param string $orderNo 订单单号 + * @param bool $realtime 有效金额 + */ + public static function paidAmount(string $orderNo, bool $realtime = false): string + { + $map = ['order_no' => $orderNo, 'payment_status' => 1]; + $raw = new Raw($realtime ? 'payment_amount - refund_amount' : 'payment_amount'); + return bcadd('0.00', strval(PluginPaymentRecord::mk()->where($map)->sum($raw)), 2); + } + + /** + * 订单剩余支付金额. + * @param mixed $orderAmount + */ + public static function leaveAmount(string $orderNo, $orderAmount): string + { + $orderAmountFloat = strval($orderAmount); + $paidAmount = strval(self::paidAmount($orderNo, true)); + return bcsub($orderAmountFloat, $paidAmount, 2); + } + + /** + * 统计三种模式支付金额. + * @return array ['amount'=>0,'payment'=>0,'balance'=>0,'integral'=>0] + */ + public static function totalPaymentAmount(string $orderNo): array + { + $total = ['amount' => '0.00', 'payment' => '0.00', 'balance' => '0.00', 'integral' => '0.00']; + try { + PluginPaymentRecord::mk()->where(['order_no' => $orderNo, 'payment_status' => 1])->field([ + 'channel_type', + 'sum(payment_amount-refund_amount)' => 'amount', + 'sum(used_payment-refund_payment)' => 'payment', + 'sum(used_balance-refund_balance)' => 'balance', + 'sum(used_integral-refund_integral)' => 'integral', + ])->group('channel_type')->select()->map(static function (PluginPaymentRecord $item) use (&$total) { + $total['amount'] = bcadd($total['amount'], strval($item->getAttr('amount')), 2); + $type = $item->getAttr('channel_type'); + if (!in_array($type, [self::INTEGRAL, self::BALANCE])) { + $type = 'payment'; + } + $total[$type] = bcadd($total[$type], strval($item[$type] ?? '0.00'), 2); + }); + } catch (\Exception $exception) { + trace_file($exception); + } + return $total; + } + + /** + * 根据支付号统计退款金额. + * @return array ['amount'=>0,'payment'=>0,'balance'=>0,'integral'=>0] + */ + public static function totalRefundAmount(string $pCode): array + { + $total = ['amount' => '0.00', 'payment' => '0.00', 'balance' => '0.00', 'integral' => '0.00']; + try { + PluginPaymentRefund::mk()->where(['record_code' => $pCode, 'refund_status' => [0, 1]])->field([ + 'refund_account', 'sum(refund_amount) amount', 'sum(used_payment)' => 'payment', 'sum(used_balance)' => 'balance', 'sum(used_integral)' => 'integral', + ])->group('refund_account')->select()->map(static function (PluginPaymentRefund $item) use (&$total) { + $total['amount'] = bcadd($total['amount'], strval($item->getAttr('amount')), 2); + $type = $item->getAttr('refund_account'); + if (!in_array($type, [self::INTEGRAL, self::BALANCE])) { + $type = 'payment'; + } + $total[$type] = bcadd($total[$type], strval($item[$type] ?? '0.00'), 2); + }); + } catch (\Exception $exception) { + trace_file($exception); + } + return $total; + } + + /** + * 生成支付单号. + */ + public static function withPaymentCode(): string + { + do { + $data = ['code' => CodeToolkit::uniqidNumber(16, 'P')]; + } while (PluginPaymentRecord::mk()->master()->where($data)->findOrEmpty()->isExists()); + return $data['code']; + } + + /** + * 生成退款单号. + */ + public static function withRefundCode(): string + { + do { + $data = ['code' => CodeToolkit::uniqidNumber(16, 'R')]; + } while (PluginPaymentRefund::mk()->master()->where($data)->findOrEmpty()->isExists()); + return $data['code']; + } + + /** + * 创建订单空支付. + * @param string $orderNo 订单单号 + * @param string $title 订单标题 + * @param string $remark 订单描述 + * @throws Exception + */ + public static function emptyPayment(AccountInterface $account, string $orderNo, string $title = '商城订单支付', string $remark = '订单金额为0,无需要支付'): PaymentResponse + { + return self::mk(self::EMPTY)->create($account, $orderNo, $title, '0.00', '0.00', $remark); + } + + /** + * 初始化数据状态 + * @return array[] + */ + private static function init(): array + { + if (is_null(self::$denys)) { + try { + self::$denys = sysdata(self::$cakey); + foreach (self::$types as $type => &$item) { + $item['status'] = intval(!in_array($type, self::$denys)); + } + } catch (\Exception $exception) { + } + } + return self::$types; + } +} diff --git a/plugin/think-plugs-payment/src/service/Recount.php b/plugin/think-plugs-payment/src/service/Recount.php new file mode 100644 index 000000000..2dfeca8f6 --- /dev/null +++ b/plugin/think-plugs-payment/src/service/Recount.php @@ -0,0 +1,79 @@ +balance()->setQueueSuccess(lang('刷新用户余额及积分完成!')); + } + + /** + * 刷新用户余额. + * @return static + * @throws \think\admin\Exception + * @throws DbException + */ + private function balance(): Recount + { + [$total, $count] = [PluginAccountUser::mk()->count(), 0]; + foreach (PluginAccountUser::mk()->field('id,username,nickname,email')->cursor() as $user) { + try { + $nick = strval($user['username'] ?: ($user['nickname'] ?: $user['email'])); + $this->setQueueMessage($total, ++$count, lang('开始刷新用户 [%s %s] 余额及积分', [strval($user['id']), $nick])); + BalanceAlias::recount(intval($user['id'])) && IntegralAlias::recount(intval($user['id'])); + $this->setQueueMessage($total, $count, lang('刷新用户 [%s %s] 余额及积分', [strval($user['id']), $nick]), 1); + } catch (\Exception $exception) { + $this->setQueueMessage($total, $count, lang('刷新用户 [%s %s] 余额及积分失败, %s', [strval($user['id']), $nick, $exception->getMessage()]), 1); + } + } + return $this; + } +} diff --git a/plugin/think-plugs-payment/src/service/contract/PaymentInterface.php b/plugin/think-plugs-payment/src/service/contract/PaymentInterface.php new file mode 100644 index 000000000..9a9eb3ab6 --- /dev/null +++ b/plugin/think-plugs-payment/src/service/contract/PaymentInterface.php @@ -0,0 +1,84 @@ + + */ + public function refund(string $pcode, string $amount, string $reason = '', ?string &$rcode = null): array; +} diff --git a/plugin/think-plugs-payment/src/service/contract/PaymentResponse.php b/plugin/think-plugs-payment/src/service/contract/PaymentResponse.php new file mode 100644 index 000000000..3526a7621 --- /dev/null +++ b/plugin/think-plugs-payment/src/service/contract/PaymentResponse.php @@ -0,0 +1,98 @@ +record = $record; + $this->status = $status; + $this->params = $params; + $this->message = $message === '' ? lang('创建支付成功') : $message; + } + + /** + * 更新返回内容. + * @return $this + */ + public function set(bool $status = true, string $message = '', array $record = [], array $params = []): PaymentResponse + { + $this->record = $record; + $this->status = $status; + $this->params = $params; + $this->message = $message === '' ? lang('创建支付成功') : $message; + return $this; + } + + /** + * 输出数组数据. + */ + public function toArray(): array + { + return [ + 'record' => $this->record, + 'params' => $this->params, + 'channel' => [ + 'type' => $this->channelType, + 'code' => $this->channleCode, + ], + ]; + } + + /** + * 创建支付响应对象 + */ + public static function mk(bool $status = true, string $message = '', array $record = [], array $params = []): PaymentResponse + { + return new self($status, $message, $record, $params); + } +} diff --git a/plugin/think-plugs-payment/src/service/contract/PaymentUsageTrait.php b/plugin/think-plugs-payment/src/service/contract/PaymentUsageTrait.php new file mode 100644 index 000000000..69ea9d366 --- /dev/null +++ b/plugin/think-plugs-payment/src/service/contract/PaymentUsageTrait.php @@ -0,0 +1,393 @@ +app = $app; + $this->cfgCode = $code; + $this->cfgType = $type; + $this->cfgParams = $params; + // 初始化支付响应对象 + $this->res = new PaymentResponse(); + $this->res->channleCode = $code; + $this->res->channelType = $type; + $this->init(); + } + + /** + * 获取支付参数. + */ + public function config(): array + { + return array_merge($this->config, [ + 'channel_type' => $this->cfgType, + 'channel_code' => $this->cfgCode, + ]); + } + + /** + * 支付实例创建器. + */ + public static function mk(string $code, string $type, array $params): PaymentInterface + { + /* @var \plugin\payment\service\contract\PaymentInterface */ + return app(static::class, ['code' => $code, 'type' => $type, 'params' => $params]); + } + + /** + * 构建标准退款结果. + * @param array $data + * @return array + */ + protected function refundResult(int $status, string $info, array $data = []): array + { + return ['code' => $status, 'info' => $info, 'data' => $data]; + } + + /** + * 初始化支付方式. + */ + abstract public function init(): PaymentInterface; + + /** + * 同步退款统计状态 + * @param string $pCode 支付单号 + * @param ?string $rCode 退款单号&引用 + * @param ?string $amount 退款金额 ( null 表示需要处理退款,仅同步数据 ) + * @param string $reason 退款原因 + * @throws Exception + */ + public static function syncRefund(string $pCode, ?string &$rCode = '', ?string $amount = null, string $reason = ''): PluginPaymentRecord + { + // 检查退款单号 + if ($rCode && PluginPaymentRefund::mk()->where(['code' => $rCode])->findOrEmpty()->isExists()) { + throw new Exception(lang('退款单已存在!'), 2); + } + // 查询支付记录 + $record = self::withPaymentByRefundTotal($pCode); + if ($record->getAttr('payment_status') < 1) { + throw new Exception(lang('支付未完成!')); + } + // 是否需要写入退款 + if (!is_numeric($amount)) { + return $record->refresh(); + } + // 生成退款记录 + $pType = $record->getAttr('channel_type'); + $extra = ['used_payment' => $amount, 'refund_status' => 0]; + if (in_array($pType, [Payment::EMPTY, Payment::COUPON, Payment::BALANCE, Payment::INTEGRAL, Payment::VOUCHER])) { + if ($pType === Payment::BALANCE) { + $extra['used_balance'] = $amount; + } elseif ($pType === Payment::INTEGRAL) { + $extra['used_integral'] = strval(bcdiv(bcmul(strval($amount), strval($record->getAttr('used_integral')), 6), strval($record->getAttr('payment_amount')), 2)); + } + $extra['refund_trade'] = CodeToolkit::uniqidNumber(16, 'RT'); + $extra['refund_account'] = $pType; + $extra['refund_scode'] = 'SUCCESS'; + $extra['refund_status'] = 1; + $extra['refund_time'] = date('Y-m-d H:i:s'); + } + // 支付金额大于0,并需要创建退款记录 + $refundAmountFloat = strval($amount); + $currentRefundAmount = strval($record->getAttr('refund_amount')); + if (bccomp(bcadd($currentRefundAmount, $refundAmountFloat, 2), strval($record->getAttr('payment_amount')), 2) > 0) { + throw new Exception(lang('退款金额溢出!')); + } + PluginPaymentRefund::mk()->save(array_merge([ + 'unid' => $record->getAttr('unid'), 'record_code' => $pCode, + 'usid' => $record->getAttr('usid'), 'refund_amount' => $amount, + 'code' => $rCode = $rCode ?: Payment::withRefundCode(), 'refund_remark' => $reason, + ], $extra)); + // 同步刷新金额 + self::withPaymentByRefundTotal($record); + // 更新模型数据 + $record->save(); + // 触发取消支付事件 + Library::$sapp->event->trigger('PluginPaymentCancel', $record->refresh()); + return $record; + } + + /** + * 检查订单支付金额. + * @param string $orderNo + * @param mixed $payAmount + * @param mixed $orderAmount + * @throws Exception + */ + protected function checkLeaveAmount($orderNo, $payAmount, $orderAmount): string + { + // 检查未审核的记录 + $map = ['order_no' => $orderNo, 'audit_status' => 1]; + $model = PluginPaymentRecord::mk()->where($map)->findOrEmpty(); + if ($model->isExists()) { + throw new Exception(lang('凭证待审核!'), 0); + } + // 检查支付金额是否超出 + $payAmountFloat = strval($payAmount); + $orderAmountFloat = strval($orderAmount); + $paidAmount = strval(Payment::paidAmount($orderNo, true)); + if (bccomp(bcadd($payAmountFloat, $paidAmount, 2), $orderAmountFloat, 2) > 0) { + throw new Exception(lang('支付金额溢出!')); + } + return $payAmountFloat; + } + + /** + * 创建支付行为. + * @param string $orderNo 订单单号 + * @param string $orderTitle 订单标题 + * @param string $orderAmount 订单总金额 + * @param string $payCode 此次支付单号 + * @param string $payAmount 此次支付金额 + * @param string $payImages 支付凭证图片 + * @param string $usedBalance 使用余额 + * @param string $usedIntegral 使用积分 + * @throws Exception + */ + protected function createAction(string $orderNo, string $orderTitle, string $orderAmount, string $payCode, string $payAmount, string $payImages = '', string $usedBalance = '0.00', string $usedIntegral = '0.00'): array + { + // 检查是否已经支付 + $map = ['order_no' => $orderNo, 'payment_status' => 1]; + $total = strval(Payment::paidAmount($orderNo, true)); + $orderAmountFloat = strval($orderAmount); + if (bccomp($total, $orderAmountFloat, 2) >= 0 && bccomp($orderAmountFloat, '0.00', 2) > 0) { + throw new Exception(lang('已经完成支付!'), 1); + } + $payAmountFloat = strval($payAmount); + if (bccomp(bcadd($total, $payAmountFloat, 2), $orderAmountFloat, 2) > 0) { + throw new Exception(lang('支付大于金额!'), 0); + } + $map['code'] = $payCode; + if (($model = PluginPaymentRecord::mk()->where($map)->findOrEmpty())->isExists()) { + throw new Exception(lang('已经完成支付!'), 1); + } + // 写入订单支付行为 + $model->save([ + 'unid' => intval(sysvar('PluginPaymentUnid')), + 'usid' => intval(sysvar('PluginPaymentUsid')), + 'code' => $payCode, + 'order_no' => $orderNo, + 'order_name' => $orderTitle, + 'order_amount' => $orderAmount, + 'channel_code' => $this->cfgCode, + 'channel_type' => $this->cfgType, + 'payment_amount' => $this->cfgType === Payment::VOUCHER ? $payAmount : 0.00, + 'payment_images' => $payImages, + 'audit_time' => date('Y-m-d H:i:s'), + 'audit_status' => $this->cfgType === Payment::VOUCHER ? 1 : 2, + 'used_payment' => $payAmount, + 'used_balance' => $usedBalance, + 'used_integral' => $usedIntegral, + ]); + + // 触发支付审核事件 + $record = $model->refresh(); + if ($this->cfgType === Payment::VOUCHER) { + $this->app->event->trigger('PluginPaymentAudit', $record); + } + return $record->toArray(); + } + + /** + * 更新支付行为记录. + * @param string $pCode 商户订单单号 + * @param string $pTrade 平台交易单号 + * @param string $pAmount 实际支付金额 + * @param null|string $pRemark 平台支付备注 + * @param null|string $pCoupon 优惠券金额 + * @param null|array $pNotify 支付通知数据 + * @return array|false + */ + protected function updateAction(string $pCode, string $pTrade, string $pAmount, ?string $pRemark = null, ?string $pCoupon = null, ?array $pNotify = null) + { + // 更新支付记录 + $map = ['code' => $pCode, 'channel_code' => $this->cfgCode, 'channel_type' => $this->cfgType]; + if (($model = PluginPaymentRecord::mk()->where($map)->findOrEmpty())->isEmpty()) { + return false; + } + $data = [ + 'code' => $pCode, + 'channel_code' => $this->cfgCode, + 'channel_type' => $this->cfgType, + 'payment_time' => date('Y-m-d H:i:s'), + 'payment_trade' => $pTrade, + 'payment_status' => 1, + 'payment_amount' => $pAmount, + ]; + if (is_array($pNotify)) { + $data['payment_notify'] = $pNotify; + } + if (is_null($pRemark)) { + $pRemark = lang('在线支付'); + } + if (is_string($pRemark)) { + $data['payment_remark'] = $pRemark; + } + if (is_numeric($pCoupon)) { + $data['payment_coupon'] = $pCoupon; + } + // 更新支付行为 + $model->save($data); + // 触发支付成功事件 + $this->app->event->trigger('PluginPaymentSuccess', $model->refresh()); + // 更新记录状态 + return $model->toArray(); + } + + /** + * 获取并同步退款金额的支付单. + * @param PluginPaymentRecord|string $record + * @throws Exception + */ + protected static function withPaymentByRefundTotal($record): PluginPaymentRecord + { + if (is_string($record)) { + $record = PluginPaymentRecord::mk()->where(['code' => $record])->findOrEmpty(); + } + if (!$record instanceof PluginPaymentRecord || $record->isEmpty()) { + throw new Exception(lang('无效的支付单!')); + } + $total = Payment::totalRefundAmount($record->getAttr('code')); + return $record->appendData([ + 'refund_amount' => $total['amount'], + 'refund_payment' => $total['payment'], + 'refund_balance' => $total['balance'], + 'refund_integral' => $total['integral'], + 'refund_status' => intval($total['amount'] > 0), + ], true); + } + + /** + * 获取账号编号. + * @param ?int $unid 用户账号 + * @param ?int $usid 终端账号 + * @throws Exception + */ + protected function withUserUnid(AccountInterface $account, ?int &$unid = 0, ?int &$usid = 0): int + { + sysvar('PluginPaymentUsid', $usid = intval($this->withUserField($account, 'id'))); + sysvar('PluginPaymentUnid', $unid = intval($this->withUserField($account, 'unid'))); + return $unid; + } + + /** + * 获取账号指定字段. + * @return mixed|string + * @throws Exception + */ + protected function withUserField(AccountInterface $account, string $field) + { + $auth = $account->get(); + if (isset($auth[$field])) { + return $auth[$field]; + } + throw new Exception(lang('获取 %s 字段值失败!', [$field])); + } + + /** + * 获取通知地址 + * @param string $order 订单单号 + * @param string $scene 支付场景 + * @param array $extra 扩展数据 + */ + protected function withNotifyUrl(string $order, string $scene = 'order', array $extra = []): string + { + $data = ['scen' => $scene, 'order' => $order, 'channel' => $this->cfgCode]; + $vars = CodeToolkit::enSafe64(json_encode($extra + $data, 64 | 256)); + return sysuri('@plugin-payment-notify', [], false, true) . "/{$vars}"; + } +} diff --git a/plugin/think-plugs-payment/src/service/payment/AliPayment.php b/plugin/think-plugs-payment/src/service/payment/AliPayment.php new file mode 100644 index 000000000..3b8e86b05 --- /dev/null +++ b/plugin/think-plugs-payment/src/service/payment/AliPayment.php @@ -0,0 +1,189 @@ +config = [ + // 沙箱模式 + 'debug' => false, + // 应用ID + 'appid' => $this->cfgParams['alipay_appid'], + // 签名类型(RSA|RSA2) + 'sign_type' => 'RSA2', + // 支付宝公钥 (1行填写,特别注意,这里是支付宝公钥,不是应用公钥,最好从开发者中心的网页上去复制) + 'public_key' => $this->_trimCert($this->cfgParams['alipay_public_key']), + // 支付宝私钥 (1行填写) + 'private_key' => $this->_trimCert($this->cfgParams['alipay_private_key']), + // 支付成功通知地址 + 'notify_url' => '', + // 网页支付回跳地址 + 'return_url' => '', + ]; + return $this; + } + + /** + * 创建支付订单. + * @param AccountInterface $account 支付账号 + * @param string $orderNo 交易订单单号 + * @param string $orderTitle 交易订单标题 + * @param string $orderAmount 订单支付金额(元) + * @param string $payAmount 本次交易金额 + * @param string $payRemark 交易订单描述 + * @param string $payReturn 支付回跳地址 + * @param string $payImages 支付凭证图片 + * @param string $payCoupon 优惠券编号 + * @throws Exception + */ + public function create(AccountInterface $account, string $orderNo, string $orderTitle, string $orderAmount, string $payAmount, string $payRemark = '', string $payReturn = '', string $payImages = '', string $payCoupon = ''): PaymentResponse + { + try { + $this->checkLeaveAmount($orderNo, $payAmount, $orderAmount); + [$payCode] = [Payment::withPaymentCode(), $this->withUserUnid($account)]; + $this->config['notify_url'] = $this->withNotifyUrl($payCode); + if (in_array($this->cfgType, [Payment::ALIPAY_WAP, Payment::ALIPAY_WEB])) { + if (empty($payReturn)) { + throw new Exception(lang('支付回跳地址不能为空!')); + } + $this->config['return_url'] = $payReturn; + } + if ($this->cfgType === Payment::WECHAT_APP) { + $payment = App::instance($this->config); + } elseif ($this->cfgType === Payment::ALIPAY_WAP) { + $payment = Wap::instance($this->config); + } elseif ($this->cfgType === Payment::ALIPAY_WEB) { + $payment = Web::instance($this->config); + } else { + throw new Exception(lang('支付类型[%s]暂时不支持!', [$this->cfgType])); + } + $param = ['out_trade_no' => $payCode, 'total_amount' => $payAmount, 'subject' => $orderTitle]; + if ($payRemark !== '') { + $param['body'] = $payRemark; + } + // 创建支付记录 + $data = $this->createAction($orderNo, $orderTitle, $orderAmount, $payCode, $payAmount); + // 返回支付参数 + return $this->res->set(true, lang('创建支付成功!'), $data, [$payment->apply($param)]); + } catch (Exception $exception) { + throw $exception; + } catch (\Exception $exception) { + throw new Exception($exception->getMessage(), $exception->getCode()); + } + } + + /** + * 支付通知处理. + * @throws InvalidResponseException + */ + public function notify(array $data = [], ?array $body = null): Response + { + $notify = $body ?: App::instance($this->config)->notify(); + if (in_array($notify['trade_status'], ['TRADE_SUCCESS', 'TRADE_FINISHED'])) { + if ($this->updateAction($notify['out_trade_no'], $notify['trade_no'], $notify['total_amount'])) { + return response('success'); + } + return response('error'); + } + return response('success'); + } + + /** + * 发起支付退款. + * @return array + * @throws Exception + */ + public function refund(string $pcode, string $amount, string $reason = '', ?string &$rcode = null): array + { + try { + // 记录退款数据 + if (bccomp(strval($amount), '0.00', 2) <= 0) { + return $this->refundResult(200, lang('无需退款!')); + } + static::syncRefund($pcode, $rcode, $amount, $reason); + // 发起退款申请 + App::instance($this->config)->refund([ + 'out_trade_no' => $pcode, + 'out_request_no' => $rcode, + 'refund_amount' => $amount, + ]); + return $this->refundResult(200, lang('发起退款成功!'), ['refund_code' => $rcode]); + } catch (\Exception $exception) { + throw new Exception($exception->getMessage(), $exception->getCode()); + } + } + + /** + * 查询订单数据. + * @throws InvalidResponseException + * @throws LocalCacheException + */ + public function query(string $pcode): array + { + return App::instance($this->config)->query($pcode); + } + + /** + * 去除证书内容前后缀 + */ + private function _trimCert(string $content): string + { + return preg_replace(['/\s+/', '/-{5}.*?-{5}/'], '', $content); + } +} diff --git a/plugin/think-plugs-payment/src/service/payment/BalancePayment.php b/plugin/think-plugs-payment/src/service/payment/BalancePayment.php new file mode 100644 index 000000000..14befd6c2 --- /dev/null +++ b/plugin/think-plugs-payment/src/service/payment/BalancePayment.php @@ -0,0 +1,140 @@ + + * @throws Exception + */ + public function refund(string $pcode, string $amount, string $reason = '', ?string &$rcode = null): array + { + try { + // 记录并退回 + if (bccomp(strval($amount), '0.00', 2) <= 0) { + return $this->refundResult(200, lang('无需退款!')); + } + $record = static::syncRefund($pcode, $rcode, $amount, $reason); + $remark = lang('来自订单 %s 退回余额', [strval($record->getAttr('order_no'))]); + BalanceService::create(intval($record->getAttr('unid')), $rcode, lang('账号余额退款'), strval($amount), $remark, true); + return $this->refundResult(200, lang('发起退款成功!'), ['refund_code' => $rcode]); + } catch (\Exception $exception) { + throw new Exception($exception->getMessage(), $exception->getCode()); + } + } + + /** + * 创建支付订单. + * @param AccountInterface $account 支付账号 + * @param string $orderNo 交易订单单号 + * @param string $orderTitle 交易订单标题 + * @param string $orderAmount 订单支付金额(元) + * @param string $payAmount 本次交易金额 + * @param string $payRemark 交易订单描述 + * @param string $payReturn 支付回跳地址 + * @param string $payImages 支付凭证图片 + * @param string $payCoupon 优惠券编号 + * @throws Exception + */ + public function create(AccountInterface $account, string $orderNo, string $orderTitle, string $orderAmount, string $payAmount, string $payRemark = '', string $payReturn = '', string $payImages = '', string $payCoupon = ''): PaymentResponse + { + try { + $this->checkLeaveAmount($orderNo, $payAmount, $orderAmount); + [$unid, $payCode] = [$this->withUserUnid($account), Payment::withPaymentCode()]; + // 检查能否支付 + $data = BalanceService::recount($unid); + if ($payAmount > $data['usable']) { + throw new Exception(lang('账户余额不足')); + } + // 创建支付行为 + $this->createAction($orderNo, $orderTitle, $orderAmount, $payCode, $payAmount, '', $payAmount); + // 扣除余额金额 + $payRemark = $payRemark ?: lang('支付订单 %s 金额 %s 元', [$orderNo, $payAmount]); + BalanceService::create($unid, "ZF{$payCode}", $orderTitle, strval(bcmul(strval($payAmount), '-1', 2)), $payRemark, true); + // 更新支付行为 + $data = $this->updateAction($payCode, "ZF{$payCode}", $payAmount, lang('账户余额支付')); + // 刷新用户余额 + BalanceService::recount($unid); + // 返回支付结果 + return $this->res->set(true, lang('余额支付完成!'), $data); + } catch (Exception $exception) { + throw $exception; + } catch (\Exception $exception) { + throw new Exception($exception->getMessage(), $exception->getCode()); + } + } +} diff --git a/plugin/think-plugs-payment/src/service/payment/CouponPayment.php b/plugin/think-plugs-payment/src/service/payment/CouponPayment.php new file mode 100644 index 000000000..12044a093 --- /dev/null +++ b/plugin/think-plugs-payment/src/service/payment/CouponPayment.php @@ -0,0 +1,135 @@ + + * @throws Exception + */ + public function refund(string $pcode, string $amount, string $reason = '', ?string &$rcode = null): array + { + try { + // 记录并退回 + static::syncRefund($pcode, $rcode, $amount, $reason); + return $this->refundResult(200, lang('发起退款成功!'), ['refund_code' => $rcode]); + } catch (\Exception $exception) { + throw new Exception($exception->getMessage(), $exception->getCode()); + } + } + + /** + * 创建支付订单. + * @param AccountInterface $account 支付账号 + * @param string $orderNo 交易订单单号 + * @param string $orderTitle 交易订单标题 + * @param string $orderAmount 订单支付金额(元) + * @param string $payAmount 本次交易金额 + * @param string $payRemark 交易订单描述 + * @param string $payReturn 支付回跳地址 + * @param string $payImages 支付凭证图片 + * @param string $payCoupon 优惠券编号 + * @throws Exception + */ + public function create(AccountInterface $account, string $orderNo, string $orderTitle, string $orderAmount, string $payAmount, string $payRemark = '', string $payReturn = '', string $payImages = '', string $payCoupon = ''): PaymentResponse + { + try { + // 检查优惠券是否已使用 + if (empty($payCoupon)) { + throw new Exception(lang('无效优惠券!')); + } + $where = ['payment_trade' => $payCoupon, 'refund_status' => 0]; + $record = PluginPaymentRecord::mk()->where($where)->findOrEmpty(); + if ($record->isExists() && $record->getAttr('order_no') !== $payCoupon) { + throw new Exception(lang('优惠券已使用!')); + } + // 检查剩余金额 + $this->checkLeaveAmount($orderNo, $payAmount, $orderAmount); + // 创建支付行为 + [$payCode] = [Payment::withPaymentCode(), $this->withUserUnid($account)]; + $this->createAction($orderNo, $orderTitle, $orderAmount, $payCode, $payAmount, '', $payAmount); + // 更新支付行为 + $data = $this->updateAction($payCode, $payCoupon, $payAmount, lang('使用优惠券抵扣'), $payAmount); + // 返回支付结果 + return $this->res->set(true, lang('优惠券抵扣完成!'), $data); + } catch (Exception $exception) { + throw $exception; + } catch (\Exception $exception) { + throw new Exception($exception->getMessage(), $exception->getCode()); + } + } +} diff --git a/plugin/think-plugs-payment/src/service/payment/EmptyPayment.php b/plugin/think-plugs-payment/src/service/payment/EmptyPayment.php new file mode 100644 index 000000000..91bd7e3d1 --- /dev/null +++ b/plugin/think-plugs-payment/src/service/payment/EmptyPayment.php @@ -0,0 +1,122 @@ +withUserUnid($account)]; + $this->createAction($orderNo, $orderTitle, $orderAmount, $payCode, $payAmount); + $data = $this->updateAction($payCode, CodeToolkit::uniqidNumber(18, 'EMT'), $payAmount, lang('无需支付')); + return $this->res->set(true, lang('订单无需支付!'), $data); + } catch (Exception $exception) { + throw $exception; + } catch (\Exception $exception) { + throw new Exception($exception->getMessage(), $exception->getCode()); + } + } + + /** + * 订单主动查询. + */ + public function query(string $pcode): array + { + return []; + } + + /** + * 支付通知处理. + */ + public function notify(array $data = [], ?array $body = null): Response + { + return response('SUCCESS'); + } + + /** + * 发起支付退款. + * @return array + * @throws Exception + */ + public function refund(string $pcode, string $amount, string $reason = '', ?string &$rcode = null): array + { + try { + if (bccomp(strval($amount), '0.00', 2) <= 0) { + return $this->refundResult(200, lang('无需退款!')); + } + static::syncRefund($pcode, $rcode, $amount, $reason); + return $this->refundResult(200, lang('发起退款成功!'), ['refund_code' => $rcode]); + } catch (\Exception $exception) { + throw new Exception($exception->getMessage(), $exception->getCode()); + } + } +} diff --git a/plugin/think-plugs-payment/src/service/payment/IntegralPayment.php b/plugin/think-plugs-payment/src/service/payment/IntegralPayment.php new file mode 100644 index 000000000..0e4001b22 --- /dev/null +++ b/plugin/think-plugs-payment/src/service/payment/IntegralPayment.php @@ -0,0 +1,145 @@ + + * @throws Exception + */ + public function refund(string $pcode, string $amount, string $reason = '', ?string &$rcode = null): array + { + try { + // 记录并退回 + if (bccomp(strval($amount), '0.00', 2) <= 0) { + return $this->refundResult(200, lang('无需退款!')); + } + $record = static::syncRefund($pcode, $rcode, $amount, $reason); + $remark = lang('来自订单 %s 退回积分', [strval($record->getAttr('order_no'))]); + $integral = bcdiv( + bcmul(strval($amount), strval($record->getAttr('used_integral')), 6), + strval($record->getAttr('payment_amount')), + 2 + ); + IntegralService::create(intval($record->getAttr('unid')), $rcode, lang('账号积分退还'), strval($integral), $remark, true); + return $this->refundResult(200, lang('发起退款成功!'), ['refund_code' => $rcode]); + } catch (\Exception $exception) { + throw new Exception($exception->getMessage(), $exception->getCode()); + } + } + + /** + * 创建支付订单. + * @param AccountInterface $account 支付账号 + * @param string $orderNo 交易订单单号 + * @param string $orderTitle 交易订单标题 + * @param string $orderAmount 订单支付金额(元) + * @param string $payAmount 本次交易积分 + * @param string $payRemark 交易订单描述 + * @param string $payReturn 支付回跳地址 + * @param string $payImages 支付凭证图片 + * @param string $payCoupon 优惠券编号 + * @throws Exception + */ + public function create(AccountInterface $account, string $orderNo, string $orderTitle, string $orderAmount, string $payAmount, string $payRemark = '', string $payReturn = '', string $payImages = '', string $payCoupon = ''): PaymentResponse + { + try { + $unid = $this->withUserUnid($account); + $integral = IntegralService::recount($unid); + if ($payAmount > $integral['usable']) { + throw new Exception(lang('可抵扣的积分不足')); + } + $realAmount = $this->checkLeaveAmount($orderNo, bcmul($payAmount, '1', 2), $orderAmount); + $payCode = Payment::withPaymentCode(); + // 创建支付行为 + $this->createAction($orderNo, $orderTitle, $orderAmount, $payCode, strval($realAmount), '', '0.00', $payAmount); + // 扣除积分金额 + $payRemark = $payRemark ?: lang('抵扣订单 %s 金额 %s 元', [$orderNo, $realAmount]); + IntegralService::create($unid, "DK{$payCode}", $orderTitle, strval(bcmul(strval($payAmount), '-1', 2)), $payRemark, true); + // 更新支付行为 + $data = $this->updateAction($payCode, "DK{$payCode}", strval($realAmount), lang('账户积分支付')); + // 刷新用户积分 + IntegralService::recount($unid); + // 返回支付结果 + return $this->res->set(true, lang('积分抵扣完成!'), $data); + } catch (Exception $exception) { + throw $exception; + } catch (\Exception $exception) { + throw new Exception($exception->getMessage(), $exception->getCode()); + } + } +} diff --git a/plugin/think-plugs-payment/src/service/payment/JoinPayment.php b/plugin/think-plugs-payment/src/service/payment/JoinPayment.php new file mode 100644 index 000000000..99ac61db1 --- /dev/null +++ b/plugin/think-plugs-payment/src/service/payment/JoinPayment.php @@ -0,0 +1,173 @@ + 'WEIXIN_GZH', + Payment::JOINPAY_XCX => 'WEIXIN_XCX', + ]; + + /** + * 初始化支付方式. + */ + public function init(): PaymentInterface + { + $this->config['appid'] = $this->cfgParams['joinpay_appid']; + $this->config['trade'] = $this->cfgParams['joinpay_trade']; + $this->config['mchid'] = $this->cfgParams['joinpay_mch_id']; + $this->config['mchkey'] = $this->cfgParams['joinpay_mch_key']; + return $this; + } + + /** + * 创建支付订单. + * @param AccountInterface $account 支付账号 + * @param string $orderNo 交易订单单号 + * @param string $orderTitle 交易订单标题 + * @param string $orderAmount 订单支付金额(元) + * @param string $payAmount 本次交易金额 + * @param string $payRemark 交易订单描述 + * @param string $payReturn 支付回跳地址 + * @param string $payImages 支付凭证图片 + * @param string $payCoupon 优惠券编号 + * @throws Exception + */ + public function create(AccountInterface $account, string $orderNo, string $orderTitle, string $orderAmount, string $payAmount, string $payRemark = '', string $payReturn = '', string $payImages = '', string $payCoupon = ''): PaymentResponse + { + [$payCode] = [Payment::withPaymentCode(), $this->withUserUnid($account)]; + $data = [ + 'p0_Version' => '1.0', + 'p1_MerchantNo' => $this->config['mchid'], + 'p2_OrderNo' => $payCode, + 'p3_Amount' => $payAmount, + 'p4_Cur' => '1', + 'p5_ProductName' => $orderTitle, + 'p6_ProductDesc' => $payRemark, + 'p9_NotifyUrl' => $this->withNotifyUrl($payCode), + 'q1_FrpCode' => self::tradeTypes[$this->cfgType] ?? '', + 'q5_OpenId' => $this->withUserField($account, 'openid'), + 'q7_AppId' => $this->config['appid'], + 'qa_TradeMerchantNo' => $this->config['trade'], + ]; + if (empty($data['q5_OpenId'])) { + unset($data['q5_OpenId']); + } + $result = $this->_doReuest('uniPayApi.action', $data); + if (isset($result['ra_Code']) && intval($result['ra_Code']) === 100) { + // 创建支付记录 + $data = $this->createAction($orderNo, $orderTitle, $orderAmount, $payCode, $payAmount); + // 返回支付参数 + return $this->res->set(true, lang('创建支付成功!'), $data, json_decode($result['rc_Result'], true)); + } + throw new Exception($result['rb_CodeMsg'] ?? lang('获取预支付码失败!')); + } + + /** + * 查询订单数据. + */ + public function query(string $pcode): array + { + return $this->_doReuest('queryOrder.action', ['p1_MerchantNo' => $this->config['mchid'], 'p2_OrderNo' => $pcode]); + } + + /** + * 支付结果处理. + */ + public function notify(?array $data = null, ?array $body = null): Response + { + $body = $data ?: $this->app->request->get(); + foreach ($body as &$item) { + $item = urldecode($item); + } + if (empty($body['hmac']) || $body['hmac'] !== $this->_doSign($body)) { + return response('error'); + } + if (isset($body['r6_Status']) && intval($body['r6_Status']) === 100) { + if ($this->updateAction($body['r2_OrderNo'], $body['r9_BankTrxNo'], $body['r3_Amount'])) { + return response('success'); + } + return response('error'); + } + return response('success'); + } + + /** + * 发起支付退款. + * @return array + * @todo 发起支付退款 + */ + public function refund(string $pcode, string $amount, string $reason = '', ?string &$rcode = null): array + { + return $this->refundResult(500, lang('暂不支持退款!')); + } + + /** + * 执行数据请求 + */ + private function _doReuest(string $uri, array $data = []): array + { + $main = 'https://www.joinpay.com/trade'; + $data['hmac'] = $this->_doSign($data); + return json_decode(HttpClient::post("{$main}/{$uri}", $data), true); + } + + /** + * 请求数据签名. + */ + private function _doSign(array $data): string + { + ksort($data); + unset($data['hmac']); + return md5(join('', $data) . $this->config['mchkey']); + } +} diff --git a/plugin/think-plugs-payment/src/service/payment/VoucherPayment.php b/plugin/think-plugs-payment/src/service/payment/VoucherPayment.php new file mode 100644 index 000000000..ca323f8f9 --- /dev/null +++ b/plugin/think-plugs-payment/src/service/payment/VoucherPayment.php @@ -0,0 +1,121 @@ + + * @throws Exception + */ + public function refund(string $pcode, string $amount, string $reason = '', ?string &$rcode = null): array + { + try { + // 记录退款 + if (bccomp(strval($amount), '0.00', 2) <= 0) { + return $this->refundResult(200, lang('无需退款!')); + } + static::syncRefund($pcode, $rcode, $amount, $reason); + return $this->refundResult(200, lang('发起退款成功!'), ['refund_code' => $rcode]); + } catch (\Exception $exception) { + throw new Exception($exception->getMessage(), $exception->getCode()); + } + } + + /** + * 创建支付订单. + * @param AccountInterface $account 支付账号 + * @param string $orderNo 交易订单单号 + * @param string $orderTitle 交易订单标题 + * @param string $orderAmount 订单支付金额(元) + * @param string $payAmount 本次交易金额 + * @param string $payRemark 交易订单描述 + * @param string $payReturn 支付回跳地址 + * @param string $payImages 支付凭证图片 + * @param string $payCoupon 优惠券编号 + * @throws Exception + */ + public function create(AccountInterface $account, string $orderNo, string $orderTitle, string $orderAmount, string $payAmount, string $payRemark = '', string $payReturn = '', string $payImages = '', string $payCoupon = ''): PaymentResponse + { + // 订单及凭证检查 + if (empty($payImages)) { + throw new Exception(lang('凭证不能为空!')); + } + $this->checkLeaveAmount($orderNo, $payAmount, $orderAmount); + // 生成新的待审核记录 + [$payCode] = [Payment::withPaymentCode(), $this->withUserUnid($account)]; + $data = $this->createAction($orderNo, $orderTitle, $orderAmount, $payCode, $payAmount, $payImages); + return $this->res->set(true, lang('上传成功!'), $data); + } +} diff --git a/plugin/think-plugs-payment/src/service/payment/WechatPayment.php b/plugin/think-plugs-payment/src/service/payment/WechatPayment.php new file mode 100644 index 000000000..63b57a995 --- /dev/null +++ b/plugin/think-plugs-payment/src/service/payment/WechatPayment.php @@ -0,0 +1,116 @@ + 'APP', + Payment::WECHAT_WAP => 'MWEB', + Payment::WECHAT_GZH => 'JSAPI', + Payment::WECHAT_XCX => 'JSAPI', + Payment::WECHAT_QRC => 'NATIVE', + ]; + + /** + * 初始化支付方式. + */ + public static function mk(string $code, string $type, array $params): PaymentInterface + { + if (isset($params['wechat_mch_ver']) && $params['wechat_mch_ver'] === 'v3') { + /* @var PaymentInterface */ + return app(WechatPaymentV3::class, ['code' => $code, 'type' => $type, 'params' => $params]); + } + /* @var PaymentInterface */ + return app(WechatPaymentV2::class, ['code' => $code, 'type' => $type, 'params' => $params]); + } + + /** + * 初始化支付方式. + */ + public function init(): PaymentInterface + { + $this->config['appid'] = $this->cfgParams['wechat_appid']; + $this->config['mch_id'] = $this->cfgParams['wechat_mch_id']; + $this->config['mch_key'] = $this->cfgParams['wechat_mch_key'] ?? ''; + $this->config['mch_v3_key'] = $this->cfgParams['wechat_mch_v3_key'] ?? ''; + $this->withCertConfig(); + $this->config['cache_path'] = runpath('runtime/wechat'); + return $this; + } + + /** + * 设置商户证书. + */ + private function withCertConfig() + { + if (empty($this->cfgParams['wechat_mch_cer_text'])) { + return; + } + if (empty($this->cfgParams['wechat_mch_key_text'])) { + return; + } + $local = LocalStorage::instance(); + $prefix = "wxpay/{$this->config['mch_id']}_"; + $sslKey = $prefix . md5($this->cfgParams['wechat_mch_key_text']) . '_key.pem'; + $sslCer = $prefix . md5($this->cfgParams['wechat_mch_cer_text']) . '_cert.pem'; + if (!$local->has($sslKey, true)) { + $local->set($sslKey, $this->cfgParams['wechat_mch_key_text'], true); + } + if (!$local->has($sslCer, true)) { + $local->set($sslCer, $this->cfgParams['wechat_mch_cer_text'], true); + } + $this->config['ssl_cer'] = $local->path($sslCer, true); + $this->config['ssl_key'] = $local->path($sslKey, true); + $this->config['cert_public'] = $this->config['ssl_cer']; + $this->config['cert_private'] = $this->config['ssl_key']; + $this->config['cert_serial'] = $this->cfgParams['wechat_mch_cer_id'] ?? ''; + $this->config['mp_cert_serial'] = $this->cfgParams['wechat_mch_v3_paycer_id'] ?? ''; + $this->config['mp_cert_content'] = $this->cfgParams['wechat_mch_v3_paycer_text'] ?? ''; + } +} diff --git a/plugin/think-plugs-payment/src/service/payment/wechat/WechatPaymentV2.php b/plugin/think-plugs-payment/src/service/payment/wechat/WechatPaymentV2.php new file mode 100644 index 000000000..960803f34 --- /dev/null +++ b/plugin/think-plugs-payment/src/service/payment/wechat/WechatPaymentV2.php @@ -0,0 +1,221 @@ +payment = Order::instance($this->config); + return $this; + } + + /** + * 创建支付订单. + * @param AccountInterface $account 支付账号 + * @param string $orderNo 交易订单单号 + * @param string $orderTitle 交易订单标题 + * @param string $orderAmount 订单支付金额(元) + * @param string $payAmount 本次交易金额 + * @param string $payRemark 交易订单描述 + * @param string $payReturn 支付回跳地址 + * @param string $payImages 支付凭证图片 + * @param string $payCoupon 优惠券编号 + * @throws Exception + */ + public function create(AccountInterface $account, string $orderNo, string $orderTitle, string $orderAmount, string $payAmount, string $payRemark = '', string $payReturn = '', string $payImages = '', string $payCoupon = ''): PaymentResponse + { + try { + $this->checkLeaveAmount($orderNo, $payAmount, $orderAmount); + [$payCode] = [Payment::withPaymentCode(), $this->withUserUnid($account)]; + $body = $payRemark === '' ? $orderTitle : ($orderTitle . '-' . $payRemark); + $data = [ + 'body' => $body, + 'openid' => $this->withUserField($account, 'openid'), + 'attach' => $this->cfgCode, + 'out_trade_no' => $payCode, + 'trade_type' => static::tradeTypes[$this->cfgType] ?? '', + 'total_fee' => intval(strval(bcmul(strval($payAmount), '100', 0))), + 'notify_url' => $this->withNotifyUrl($payCode), + 'spbill_create_ip' => $this->app->request->ip(), + ]; + if (empty($data['openid'])) { + unset($data['openid']); + } + $info = $this->payment->create($data); + if ($info['return_code'] === 'SUCCESS' && $info['result_code'] === 'SUCCESS') { + // 支付参数过滤 + if ($this->cfgType === Payment::WECHAT_APP) { + $param = $this->payment->appParams($info['prepay_id']); + } elseif (isset($info['prepay_id'])) { + $param = $this->payment->jsapiParams($info['prepay_id']); + } else { + $param = $info; + } + // 创建支付记录 + $data = $this->createAction($orderNo, $orderTitle, $orderAmount, $payCode, $payAmount); + // 返回支付参数 + return $this->res->set(true, lang('创建支付成功!'), $data, $param); + } + throw new Exception($info['err_code_des'] ?? lang('获取预支付码失败!')); + } catch (Exception $exception) { + throw $exception; + } catch (\Exception $exception) { + throw new Exception($exception->getMessage(), $exception->getCode()); + } + } + + /** + * 查询微信支付订单. + * @param string $pcode 支付号 + * @throws InvalidResponseException + * @throws LocalCacheException + */ + public function query(string $pcode): array + { + $result = $this->payment->query(['out_trade_no' => $pcode]); + if (isset($result['return_code'], $result['result_code'], $result['attach'])) { + if ($result['return_code'] === 'SUCCESS' && $result['result_code'] === 'SUCCESS') { + $this->updateAction($result['out_trade_no'], strval($result['cash_fee'] / 100), $result['transaction_id']); + } + } + return $result; + } + + /** + * 支付通知处理. + * @throws InvalidDecryptException + * @throws InvalidResponseException + * @throws Exception + */ + public function notify(array $data = [], ?array $body = null): Response + { + if ($data['scen'] === 'order') { + // 支付通知处理 + $notify = $this->payment->getNotify($body); + p($notify, false, 'notify_payment_v2'); + if ($notify['result_code'] == 'SUCCESS' && $notify['return_code'] == 'SUCCESS') { + [$pCode, $pTrade] = [$notify['out_trade_no'], $notify['transaction_id']]; + [$pAmount, $pCoupon] = [strval($notify['cash_fee'] / 100), strval(($notify['coupon_fee'] ?? 0) / 100)]; + if (!$this->updateAction($pCode, $pTrade, $pAmount, null, $pCoupon, $notify)) { + return xml(['return_code' => 'ERROR', 'return_msg' => '数据更新失败']); + } + } + } elseif ($data['scen'] === 'refund') { + // 退款通知信息 + $notify = Refund::instance($this->config)->getNotify($body); + p($notify, false, 'notify_refund_v2'); + if (!empty($notify['result']) && is_array($notify['result'])) { + $notify = array_merge($notify, $notify['result']); + unset($notify['result'], $notify['req_info']); + } + if (isset($notify['refund_status']) && $notify['refund_status'] == 'SUCCESS') { + $refund = PluginPaymentRefund::mk()->where(['code' => $notify['out_refund_no']])->findOrEmpty(); + if ($refund->isEmpty()) { + return xml(['return_code' => 'ERROR', 'return_msg' => '数据更新失败']); + } + $refund->save([ + 'refund_time' => date('Y-m-d H:i:s', strtotime($notify['success_time'])), + 'refund_trade' => $notify['transaction_id'], + 'refund_scode' => $notify['refund_status'], + 'refund_status' => 1, + 'refund_notify' => json_encode($notify, 64 | 256), + 'refund_account' => $notify['refund_recv_accout'] ?? '', + ]); + static::syncRefund($refund->getAttr('record_code')); + } + } + return xml(['return_code' => 'SUCCESS', 'return_msg' => 'OK']); + } + + /** + * 发起支付退款. + * @return array + * @throws Exception + */ + public function refund(string $pcode, string $amount, string $reason = '', ?string &$rcode = null): array + { + try { + // 记录退款 + if (bccomp(strval($amount), '0.00', 2) <= 0) { + return $this->refundResult(200, lang('无需退款!')); + } + $record = static::syncRefund($pcode, $rcode, $amount, $reason); + // 发起退款申请 + $options = [ + 'out_trade_no' => $pcode, + 'out_refund_no' => $rcode, + 'total_fee' => intval($record->getAttr('payment_amount') * 100), + 'refund_fee' => intval(strval(bcmul(strval($amount), '100', 0))), + 'notify_url' => static::withNotifyUrl($rcode, 'refund'), + ]; + if (strlen($reason) > 0) { + $options['refund_desc'] = $reason; + } + $result = Refund::instance($this->config)->create($options); + if (in_array($result['return_code'] ?? $result['result_code'], ['SUCCESS', 'PROCESSING'])) { + return $this->refundResult(200, lang('已提交退款!'), ['refund_code' => $rcode]); + } + throw new Exception($result['err_code_des'] ?? $result['result_code'], 0); + } catch (\Exception $exception) { + throw new Exception($exception->getMessage(), $exception->getCode()); + } + } +} diff --git a/plugin/think-plugs-payment/src/service/payment/wechat/WechatPaymentV3.php b/plugin/think-plugs-payment/src/service/payment/wechat/WechatPaymentV3.php new file mode 100644 index 000000000..c10423985 --- /dev/null +++ b/plugin/think-plugs-payment/src/service/payment/wechat/WechatPaymentV3.php @@ -0,0 +1,225 @@ +payment = Order::instance($this->config); + return $this; + } + + /** + * 创建支付订单. + * @param AccountInterface $account 支付账号 + * @param string $orderNo 交易订单单号 + * @param string $orderTitle 交易订单标题 + * @param string $orderAmount 订单支付金额(元) + * @param string $payAmount 本次交易金额 + * @param string $payRemark 交易订单描述 + * @param string $payReturn 支付回跳地址 + * @param string $payImages 支付凭证图片 + * @param string $payCoupon 优惠券编号 + * @throws Exception + */ + public function create(AccountInterface $account, string $orderNo, string $orderTitle, string $orderAmount, string $payAmount, string $payRemark = '', string $payReturn = '', string $payImages = '', string $payCoupon = ''): PaymentResponse + { + try { + $this->checkLeaveAmount($orderNo, $payAmount, $orderAmount); + [$payCode] = [Payment::withPaymentCode(), $this->withUserUnid($account)]; + $body = $payRemark === '' ? $orderTitle : ($orderTitle . '-' . $payRemark); + $data = [ + 'appid' => $this->config['appid'], + 'mchid' => $this->config['mch_id'], + 'payer' => ['openid' => $this->withUserField($account, 'openid')], + 'amount' => ['total' => (int)round((float)$payAmount * 100), 'currency' => 'CNY'], + 'notify_url' => $this->withNotifyUrl($payCode), + 'description' => $body, + 'out_trade_no' => $payCode, + ]; + $tradeType = static::tradeTypes[$this->cfgType] ?? ''; + if (in_array($this->cfgType, [Payment::WECHAT_WAP, Payment::WECHAT_QRC])) { + unset($data['payer']); + } + if ($this->cfgType === Payment::WECHAT_WAP) { + $tradeType = 'h5'; + $data['scene_info'] = ['h5_info' => ['type' => 'Wap'], 'payer_client_ip' => request()->ip()]; + } + if ($this->cfgType === Payment::WECHAT_APP) { + unset($data['payer']); + } + // 创建预支付 + $param = $this->payment->create(strtolower($tradeType), $data); + if ($this->cfgType === Payment::WECHAT_APP) { + $param = array_change_key_case($param); + } + // 创建支付记录 + $this->createAction($orderNo, $orderTitle, $orderAmount, $payCode, $payAmount); + // 返回支付参数 + return $this->res->set(true, lang('创建支付成功!'), $data, $param); + } catch (Exception $exception) { + throw $exception; + } catch (\Exception $exception) { + throw new Exception($exception->getMessage(), $exception->getCode()); + } + } + + /** + * 查询微信支付订单. + * @param string $pcode 订单单号 + */ + public function query(string $pcode): array + { + try { + $result = $this->payment->query($pcode); + if (isset($result['trade_state']) && $result['trade_state'] === 'SUCCESS') { + $this->updateAction($result['out_trade_no'], $result['transaction_id'] ?? '', strval($result['amount']['total'] / 100)); + } + return $result; + } catch (\Exception $exception) { + return ['trade_state' => 'ERROR', 'trade_state_desc' => $exception->getMessage()]; + } + } + + /** + * 支付通知处理. + */ + public function notify(array $data = [], ?array $body = null): Response + { + try { + // 接收通知内容 + $notify = $this->payment->notify($body); + p($notify, false, 'notify_v3'); + $result = empty($notify['result']) ? [] : json_decode($notify['result'], true); + if (empty($result) || !is_array($result)) { + return response('error', 500); + } + // 支付通知处理 + if ($data['scen'] === 'order' && $result['trade_state'] ?? '' == 'SUCCESS') { + // 不考虑支付平台的优惠券金额 + $pAmount = strval($result['amount']['total'] / 100); + [$pCode, $pTrade] = [$result['out_trade_no'], $result['transaction_id']]; + $pCoupon = strval(($result['amount']['total'] - $result['amount']['payer_total']) / 100); + if (!$this->updateAction($pCode, $pTrade, $pAmount, null, $pCoupon, $result)) { + return response('error', 500); + } + } elseif ($data['scen'] === 'refund' && $result['refund_status'] ?? '' == 'SUCCESS') { + // 退款通知信息 + $refund = PluginPaymentRefund::mk()->where(['code' => $result['out_refund_no']])->findOrEmpty(); + if ($refund->isEmpty()) { + return response('error', 500); + } + $refund->save([ + 'refund_time' => date('Y-m-d H:i:s', strtotime($result['success_time'])), + 'refund_trade' => $result['refund_id'], + 'refund_scode' => $result['refund_status'], + 'refund_status' => 1, + 'refund_notify' => json_encode($result, 64 | 256), + 'refund_account' => $result['user_received_account'] ?? '', + ]); + static::syncRefund($refund->getAttr('record_code')); + } + return response('success'); + } catch (\Exception $exception) { + return json(['code' => 'FAIL', 'message' => $exception->getMessage()])->code(500); + } + } + + /** + * 发起支付退款. + * @return array + * @throws Exception + */ + public function refund(string $pcode, string $amount, string $reason = '', ?string &$rcode = null): array + { + try { + // 记录退款 + if (bccomp(strval($amount), '0.00', 2) <= 0) { + return $this->refundResult(200, lang('无需退款!')); + } + $record = static::syncRefund($pcode, $rcode, $amount, $reason); + // 发起退款申请 + $options = [ + 'out_trade_no' => $pcode, + 'out_refund_no' => $rcode, + 'notify_url' => static::withNotifyUrl($rcode, 'refund'), + 'amount' => [ + 'total' => intval($record->getAttr('payment_amount') * 100), + 'refund' => intval(strval(bcmul(strval($amount), '100', 0))), + 'currency' => 'CNY', + ], + ]; + if (strlen($reason) > 0) { + $options['reason'] = $reason; + } + $result = $this->payment->createRefund($options); + if (in_array($result['code'] ?? $result['status'], ['SUCCESS', 'PROCESSING'])) { + return $this->refundResult(200, lang('已提交退款!'), ['refund_code' => $rcode]); + } + throw new Exception($result['message'] ?? $result['status'], 0); + } catch (\Exception $exception) { + throw new Exception($exception->getMessage(), $exception->getCode()); + } + } +} diff --git a/plugin/think-plugs-payment/src/view/balance/form.html b/plugin/think-plugs-payment/src/view/balance/form.html new file mode 100644 index 000000000..36b612532 --- /dev/null +++ b/plugin/think-plugs-payment/src/view/balance/form.html @@ -0,0 +1,66 @@ +
    +
    + +
    + 用户资料 +
    +
    +
    +
    +
    +
    +
    + + +
    +
    +
    + + + + + + + + +
    + 余额充值备注Remark + +
    + +
    + +
    + + {notempty name='vo.id'}{/notempty} + +
    + + +
    +
    + + \ No newline at end of file diff --git a/plugin/think-plugs-payment/src/view/balance/index.html b/plugin/think-plugs-payment/src/view/balance/index.html new file mode 100644 index 000000000..fcf55a89c --- /dev/null +++ b/plugin/think-plugs-payment/src/view/balance/index.html @@ -0,0 +1,103 @@ +{extend name='table'} + +{block name="content"} +
    + {:lang('余额统计')}{:lang('累计充值')} {$balanceTotal|number_format} {:lang('元')},{:lang('已消费')} {$balanceCount|abs|number_format} {:lang('元')},{:lang('剩余可用余额')} {:number_format($balanceTotal+$balanceCount)} {:lang('元')}。 +
    + +
    +
      + {foreach ['index'=>lang('余额管理'),'recycle'=>lang('回 收 站')] as $k=>$v}{if isset($type) and $type eq $k} +
    • {$v}
    • + {else} +
    • {$v}
    • + {/if}{/foreach} +
    +
    + {include file='balance/index_search'} +
    +
    +
    + + + + + + + +{/block} diff --git a/plugin/think-plugs-payment/src/view/balance/index_search.html b/plugin/think-plugs-payment/src/view/balance/index_search.html new file mode 100644 index 000000000..25e0f8498 --- /dev/null +++ b/plugin/think-plugs-payment/src/view/balance/index_search.html @@ -0,0 +1,41 @@ + \ No newline at end of file diff --git a/plugin/think-plugs-payment/src/view/config/form.html b/plugin/think-plugs-payment/src/view/config/form.html new file mode 100644 index 000000000..d98f6befa --- /dev/null +++ b/plugin/think-plugs-payment/src/view/config/form.html @@ -0,0 +1,87 @@ +{extend name='main'} + +{block name='content'} +
    +
    + +
    + {:lang('支付方式')} + {empty name='vo.type'} + + {else} + + + {/empty} + {:lang('必选,')}{:lang('请选择预置的支付方式,支付方式创建之后不能修改。')} +
    + + + +
    + {:lang('支付图标')} +
    + + +
    +
    + +
    {include file='config/form_wechat'}
    +
    {include file='config/form_alipay'}
    +
    {include file='config/form_joinpay'}
    +
    {include file='config/form_voucher'}
    + + + +
    + {notempty name='vo.id'}{/notempty} + {notempty name='vo.code'}{/notempty} + +
    + + +
    + +
    + +
    +{/block} + +{block name='script'} + +{/block} diff --git a/plugin/think-plugs-payment/src/view/config/form_alipay.html b/plugin/think-plugs-payment/src/view/config/form_alipay.html new file mode 100644 index 000000000..7de1fab47 --- /dev/null +++ b/plugin/think-plugs-payment/src/view/config/form_alipay.html @@ -0,0 +1,17 @@ + + + + + diff --git a/plugin/think-plugs-payment/src/view/config/form_joinpay.html b/plugin/think-plugs-payment/src/view/config/form_joinpay.html new file mode 100644 index 000000000..f2980e151 --- /dev/null +++ b/plugin/think-plugs-payment/src/view/config/form_joinpay.html @@ -0,0 +1,23 @@ + + + + + + + diff --git a/plugin/think-plugs-payment/src/view/config/form_voucher.html b/plugin/think-plugs-payment/src/view/config/form_voucher.html new file mode 100644 index 000000000..c5a1ca90e --- /dev/null +++ b/plugin/think-plugs-payment/src/view/config/form_voucher.html @@ -0,0 +1,7 @@ +
    + {:lang('线下支付二维码')} +
    + + +
    +
    diff --git a/plugin/think-plugs-payment/src/view/config/form_wechat.html b/plugin/think-plugs-payment/src/view/config/form_wechat.html new file mode 100644 index 000000000..c1667149f --- /dev/null +++ b/plugin/think-plugs-payment/src/view/config/form_wechat.html @@ -0,0 +1,86 @@ + + + + +
    + {:lang('商户接口版本')} +
    + {empty name='vo.content.wechat_mch_ver'}{assign name='vo.content.wechat_mch_ver' value='v2'}{/empty} + {foreach ['v2'=>'微信支付 V2 接口','v3'=>'微信支付 V3 接口'] as $k=>$v} + + {/foreach} +
    +
    + + + + + +
    + {:lang('微信商户证书')} +
    + + + +
    +
    + +
    + {:lang('微信支付公钥')} +
    + + +
    +
    + + diff --git a/plugin/think-plugs-payment/src/view/config/index.html b/plugin/think-plugs-payment/src/view/config/index.html new file mode 100644 index 000000000..6862cc73e --- /dev/null +++ b/plugin/think-plugs-payment/src/view/config/index.html @@ -0,0 +1,99 @@ +{extend name='table'} + +{block name="button"} + + + + + + + + + + + +{/block} + +{block name="content"} +
    +
      + {foreach ['index'=>lang('支付管理'),'recycle'=>lang('回 收 站')] as $k=>$v}{if isset($type) and $type eq $k} +
    • {$v}
    • + {else} +
    • {$v}
    • + {/if}{/foreach} +
    +
    + {include file='config/index_search'} +
    +
    +
    +{/block} + +{block name='script'} + + + + + + + + + + +{/block} \ No newline at end of file diff --git a/plugin/think-plugs-payment/src/view/config/index_search.html b/plugin/think-plugs-payment/src/view/config/index_search.html new file mode 100644 index 000000000..9cdda7e51 --- /dev/null +++ b/plugin/think-plugs-payment/src/view/config/index_search.html @@ -0,0 +1,42 @@ + diff --git a/plugin/think-plugs-payment/src/view/config/types.html b/plugin/think-plugs-payment/src/view/config/types.html new file mode 100644 index 000000000..c89ba4872 --- /dev/null +++ b/plugin/think-plugs-payment/src/view/config/types.html @@ -0,0 +1,53 @@ + + +
    + +
    +
    + {:lang('积分抵扣配置')} +
    + {:lang('使用')} + + {:lang('积分可抵扣')} 1 {:lang('元')}。 +
    +
    +
    + {:lang('支付方式开关')} +
    + {foreach $types as $k => $v} + + {/foreach} +
    +
    +
    + +
    + +
    + + +
    +
    diff --git a/plugin/think-plugs-payment/src/view/integral/form.html b/plugin/think-plugs-payment/src/view/integral/form.html new file mode 100644 index 000000000..1b6808c29 --- /dev/null +++ b/plugin/think-plugs-payment/src/view/integral/form.html @@ -0,0 +1,66 @@ +
    +
    + +
    + 用户资料 +
    +
    +
    +
    +
    +
    +
    + + +
    +
    +
    + + + + + + + + +
    + 余额充值备注Remark + +
    + +
    + +
    + + {notempty name='vo.id'}{/notempty} + +
    + + +
    +
    + + \ No newline at end of file diff --git a/plugin/think-plugs-payment/src/view/integral/index.html b/plugin/think-plugs-payment/src/view/integral/index.html new file mode 100644 index 000000000..e98535e31 --- /dev/null +++ b/plugin/think-plugs-payment/src/view/integral/index.html @@ -0,0 +1,103 @@ +{extend name='table'} + +{block name="content"} +
    + {:lang('积分统计')}{:lang('累计发放')} {$integralTotal|number_format} {:lang('积分')},{:lang('已消费')} {$integralCount|abs|number_format} {:lang('积分')},{:lang('剩余可用')} {:number_format($integralTotal+$integralCount)} {:lang('积分')}。 +
    + +
    +
      + {foreach ['index'=>lang('积分管理'),'recycle'=>lang('回 收 站')] as $k=>$v}{if isset($type) and $type eq $k} +
    • {$v}
    • + {else} +
    • {$v}
    • + {/if}{/foreach} +
    +
    + {include file='integral/index_search'} +
    +
    +
    + + + + + + + +{/block} diff --git a/plugin/think-plugs-payment/src/view/integral/index_search.html b/plugin/think-plugs-payment/src/view/integral/index_search.html new file mode 100644 index 000000000..b64866375 --- /dev/null +++ b/plugin/think-plugs-payment/src/view/integral/index_search.html @@ -0,0 +1,41 @@ + diff --git a/app/wechat/view/main.html b/plugin/think-plugs-payment/src/view/main.html similarity index 87% rename from app/wechat/view/main.html rename to plugin/think-plugs-payment/src/view/main.html index bc3b8a62d..130355323 100644 --- a/app/wechat/view/main.html +++ b/plugin/think-plugs-payment/src/view/main.html @@ -3,7 +3,7 @@ {block name='header'} {notempty name='title'}
    - {$title|lang} + {$title|lang}
    {block name='button'}{/block}
    {/notempty} diff --git a/plugin/think-plugs-payment/src/view/record/index.html b/plugin/think-plugs-payment/src/view/record/index.html new file mode 100644 index 000000000..001d5fe6c --- /dev/null +++ b/plugin/think-plugs-payment/src/view/record/index.html @@ -0,0 +1,132 @@ +{extend name="table"} + +{block name="content"} +
    + {include file='record/index_search'} +
    +
    +{/block} + +{block name='script'} + + + +{/block} diff --git a/plugin/think-plugs-payment/src/view/record/index_search.html b/plugin/think-plugs-payment/src/view/record/index_search.html new file mode 100644 index 000000000..b810cc6e9 --- /dev/null +++ b/plugin/think-plugs-payment/src/view/record/index_search.html @@ -0,0 +1,57 @@ +
    + {:lang('条件搜索')} + +
    + + diff --git a/plugin/think-plugs-payment/src/view/refund/index.html b/plugin/think-plugs-payment/src/view/refund/index.html new file mode 100644 index 000000000..c93c51902 --- /dev/null +++ b/plugin/think-plugs-payment/src/view/refund/index.html @@ -0,0 +1,135 @@ +{extend name="table"} + +{block name="button"} + + + +{/block} + +{block name="content"} +
    + {include file='refund/index_search'} +
    +
    +{/block} + +{block name='script'} + + + +{/block} diff --git a/plugin/think-plugs-payment/src/view/refund/index_search.html b/plugin/think-plugs-payment/src/view/refund/index_search.html new file mode 100644 index 000000000..781d062b9 --- /dev/null +++ b/plugin/think-plugs-payment/src/view/refund/index_search.html @@ -0,0 +1,58 @@ +
    + {:lang('条件搜索')} + +
    + + diff --git a/plugin/think-plugs-payment/src/view/table.html b/plugin/think-plugs-payment/src/view/table.html new file mode 100644 index 000000000..a9fb98344 --- /dev/null +++ b/plugin/think-plugs-payment/src/view/table.html @@ -0,0 +1,23 @@ +
    + {block name='style'}{/block} + {block name='header'} + {notempty name='title'} +
    + {$title|lang} +
    {block name='button'}{/block}
    +
    + {/notempty} + {/block} +
    +
    +
    + {notempty name='showErrorMessage'} +
    + 系统提示:{$showErrorMessage|raw} +
    + {/notempty} + {block name='content'}{/block} +
    +
    + {block name='script'}{/block} +
    \ No newline at end of file diff --git a/plugin/think-plugs-payment/stc/database/20241010000006_install_payment20241010.php b/plugin/think-plugs-payment/stc/database/20241010000006_install_payment20241010.php new file mode 100644 index 000000000..0302c9764 --- /dev/null +++ b/plugin/think-plugs-payment/stc/database/20241010000006_install_payment20241010.php @@ -0,0 +1,262 @@ +_create_plugin_payment_address(); + $this->_create_plugin_payment_balance(); + $this->_create_plugin_payment_config(); + $this->_create_plugin_payment_integral(); + $this->_create_plugin_payment_record(); + $this->_create_plugin_payment_refund(); + } + + /** + * 创建数据对象 + * @class PluginPaymentAddress + * @table plugin_payment_address + */ + private function _create_plugin_payment_address() + { + // 创建数据表对象 + $table = $this->table('plugin_payment_address', [ + 'engine' => 'InnoDB', 'collation' => 'utf8mb4_general_ci', 'comment' => '插件-支付-地址', + ]); + // 创建或更新数据表 + PhinxExtend::upgrade($table, [ + ['unid', 'biginteger', ['limit' => 20, 'default' => 0, 'null' => true, 'comment' => '主账号ID']], + ['type', 'integer', ['limit' => 1, 'default' => 0, 'null' => true, 'comment' => '默认状态(0普通,1默认)']], + ['idcode', 'string', ['limit' => 180, 'default' => '', 'null' => true, 'comment' => '身体证证号']], + ['idimg1', 'string', ['limit' => 500, 'default' => '', 'null' => true, 'comment' => '身份证正面']], + ['idimg2', 'string', ['limit' => 500, 'default' => '', 'null' => true, 'comment' => '身份证反面']], + ['user_name', 'string', ['limit' => 100, 'default' => '', 'null' => true, 'comment' => '收货人姓名']], + ['user_phone', 'string', ['limit' => 20, 'default' => '', 'null' => true, 'comment' => '收货人手机']], + ['region_prov', 'string', ['limit' => 100, 'default' => '', 'null' => true, 'comment' => '地址-省份']], + ['region_city', 'string', ['limit' => 100, 'default' => '', 'null' => true, 'comment' => '地址-城市']], + ['region_area', 'string', ['limit' => 100, 'default' => '', 'null' => true, 'comment' => '地址-区域']], + ['region_addr', 'string', ['limit' => 500, 'default' => '', 'null' => true, 'comment' => '地址-详情']], + ['delete_time', 'datetime', ['default' => null, 'null' => true, 'comment' => '删除时间']], + ['create_time', 'datetime', ['default' => null, 'null' => true, 'comment' => '创建时间']], + ['update_time', 'datetime', ['default' => null, 'null' => true, 'comment' => '更新时间']], + ], [ + 'type', 'unid', 'delete_time', 'user_phone', 'create_time', + ], true); + } + + /** + * 创建数据对象 + * @class PluginPaymentBalance + * @table plugin_payment_balance + */ + private function _create_plugin_payment_balance() + { + // 创建数据表对象 + $table = $this->table('plugin_payment_balance', [ + 'engine' => 'InnoDB', 'collation' => 'utf8mb4_general_ci', 'comment' => '插件-支付-余额', + ]); + // 创建或更新数据表 + PhinxExtend::upgrade($table, [ + ['unid', 'biginteger', ['limit' => 20, 'default' => 0, 'null' => true, 'comment' => '账号编号']], + ['code', 'string', ['limit' => 20, 'default' => '', 'null' => true, 'comment' => '操作编号']], + ['source_type', 'string', ['limit' => 50, 'default' => '', 'null' => true, 'comment' => '资金来源类型']], + ['source_id', 'string', ['limit' => 50, 'default' => '', 'null' => true, 'comment' => '资金来源ID']], + ['name', 'string', ['limit' => 200, 'default' => '', 'null' => true, 'comment' => '操作名称']], + ['remark', 'string', ['limit' => 999, 'default' => '', 'null' => true, 'comment' => '操作备注']], + ['amount', 'decimal', ['precision' => 20, 'scale' => 2, 'default' => '0.00', 'null' => true, 'comment' => '操作金额']], + ['amount_prev', 'decimal', ['precision' => 20, 'scale' => 2, 'default' => '0.00', 'null' => true, 'comment' => '操作前金额']], + ['amount_next', 'decimal', ['precision' => 20, 'scale' => 2, 'default' => '0.00', 'null' => true, 'comment' => '操作后金额']], + ['cancel', 'integer', ['limit' => 1, 'default' => 0, 'null' => true, 'comment' => '作废状态(0未作废,1已作废)']], + ['unlock', 'integer', ['limit' => 1, 'default' => 0, 'null' => true, 'comment' => '解锁状态(0锁定中,1已生效)']], + ['delete_time', 'datetime', ['default' => null, 'null' => true, 'comment' => '删除时间']], + ['create_by', 'biginteger', ['limit' => 20, 'default' => 0, 'null' => true, 'comment' => '系统用户']], + ['cancel_time', 'datetime', ['default' => null, 'null' => true, 'comment' => '作废时间']], + ['unlock_time', 'datetime', ['default' => null, 'null' => true, 'comment' => '解锁时间']], + ['create_time', 'datetime', ['default' => null, 'null' => true, 'comment' => '创建时间']], + ['update_time', 'datetime', ['default' => null, 'null' => true, 'comment' => '更新时间']], + ], [ + 'unid', 'code', 'cancel', 'unlock', 'delete_time', 'create_time', + ], true); + } + + /** + * 创建数据对象 + * @class PluginPaymentConfig + * @table plugin_payment_config + */ + private function _create_plugin_payment_config() + { + // 创建数据表对象 + $table = $this->table('plugin_payment_config', [ + 'engine' => 'InnoDB', 'collation' => 'utf8mb4_general_ci', 'comment' => '插件-支付-配置', + ]); + // 创建或更新数据表 + PhinxExtend::upgrade($table, [ + ['type', 'string', ['limit' => 50, 'default' => '', 'null' => true, 'comment' => '支付类型']], + ['code', 'string', ['limit' => 20, 'default' => '', 'null' => true, 'comment' => '通道编号']], + ['name', 'string', ['limit' => 100, 'default' => '', 'null' => true, 'comment' => '支付名称']], + ['cover', 'string', ['limit' => 500, 'default' => '', 'null' => true, 'comment' => '支付图标']], + ['remark', 'string', ['limit' => 500, 'default' => '', 'null' => true, 'comment' => '支付说明']], + ['content', 'text', ['default' => null, 'null' => true, 'comment' => '支付参数']], + ['sort', 'biginteger', ['limit' => 20, 'default' => 0, 'null' => true, 'comment' => '排序权重']], + ['status', 'integer', ['limit' => 1, 'default' => 1, 'null' => true, 'comment' => '支付状态(1使用,0禁用)']], + ['delete_time', 'datetime', ['default' => null, 'null' => true, 'comment' => '删除时间']], + ['create_time', 'datetime', ['default' => null, 'null' => true, 'comment' => '创建时间']], + ['update_time', 'datetime', ['default' => null, 'null' => true, 'comment' => '更新时间']], + ], [ + 'type', 'code', 'sort', 'status', 'delete_time', 'create_time', + ], true); + } + + /** + * 创建数据对象 + * @class PluginPaymentIntegral + * @table plugin_payment_integral + */ + private function _create_plugin_payment_integral() + { + // 创建数据表对象 + $table = $this->table('plugin_payment_integral', [ + 'engine' => 'InnoDB', 'collation' => 'utf8mb4_general_ci', 'comment' => '插件-支付-积分', + ]); + // 创建或更新数据表 + PhinxExtend::upgrade($table, [ + ['unid', 'biginteger', ['limit' => 20, 'default' => 0, 'null' => true, 'comment' => '账号编号']], + ['code', 'string', ['limit' => 20, 'default' => '', 'null' => true, 'comment' => '操作编号']], + ['source_type', 'string', ['limit' => 50, 'default' => '', 'null' => true, 'comment' => '积分来源类型']], + ['source_id', 'string', ['limit' => 50, 'default' => '', 'null' => true, 'comment' => '积分来源ID']], + ['name', 'string', ['limit' => 200, 'default' => '', 'null' => true, 'comment' => '操作名称']], + ['remark', 'string', ['limit' => 999, 'default' => '', 'null' => true, 'comment' => '操作备注']], + ['amount', 'decimal', ['precision' => 20, 'scale' => 2, 'default' => '0.00', 'null' => true, 'comment' => '操作金额']], + ['amount_prev', 'decimal', ['precision' => 20, 'scale' => 2, 'default' => '0.00', 'null' => true, 'comment' => '操作前金额']], + ['amount_next', 'decimal', ['precision' => 20, 'scale' => 2, 'default' => '0.00', 'null' => true, 'comment' => '操作后金额']], + ['cancel', 'integer', ['limit' => 1, 'default' => 0, 'null' => true, 'comment' => '作废状态(0未作废,1已作废)']], + ['unlock', 'integer', ['limit' => 1, 'default' => 0, 'null' => true, 'comment' => '解锁状态(0锁定中,1已生效)']], + ['delete_time', 'datetime', ['default' => null, 'null' => true, 'comment' => '删除时间']], + ['create_by', 'biginteger', ['limit' => 20, 'default' => 0, 'null' => true, 'comment' => '系统用户']], + ['cancel_time', 'datetime', ['default' => null, 'null' => true, 'comment' => '作废时间']], + ['unlock_time', 'datetime', ['default' => null, 'null' => true, 'comment' => '解锁时间']], + ['create_time', 'datetime', ['default' => null, 'null' => true, 'comment' => '创建时间']], + ['update_time', 'datetime', ['default' => null, 'null' => true, 'comment' => '更新时间']], + ], [ + 'unid', 'code', 'cancel', 'unlock', 'delete_time', 'create_time', + ], true); + } + + /** + * 创建数据对象 + * @class PluginPaymentRecord + * @table plugin_payment_record + */ + private function _create_plugin_payment_record() + { + // 创建数据表对象 + $table = $this->table('plugin_payment_record', [ + 'engine' => 'InnoDB', 'collation' => 'utf8mb4_general_ci', 'comment' => '插件-支付-行为', + ]); + // 创建或更新数据表 + PhinxExtend::upgrade($table, [ + ['unid', 'biginteger', ['limit' => 20, 'default' => 0, 'null' => true, 'comment' => '主账号编号']], + ['usid', 'biginteger', ['limit' => 20, 'default' => 0, 'null' => true, 'comment' => '子账号编号']], + ['code', 'string', ['limit' => 20, 'default' => '', 'null' => true, 'comment' => '发起支付号']], + ['order_no', 'string', ['limit' => 20, 'default' => '', 'null' => true, 'comment' => '原订单编号']], + ['order_name', 'string', ['limit' => 255, 'default' => '', 'null' => true, 'comment' => '原订单标题']], + ['order_amount', 'decimal', ['precision' => 20, 'scale' => 2, 'default' => '0.00', 'null' => true, 'comment' => '原订单金额']], + ['channel_type', 'string', ['limit' => 50, 'default' => '', 'null' => true, 'comment' => '支付通道类型']], + ['channel_code', 'string', ['limit' => 20, 'default' => '', 'null' => true, 'comment' => '支付通道编号']], + ['payment_time', 'datetime', ['default' => null, 'null' => true, 'comment' => '支付生效时间']], + ['payment_trade', 'string', ['limit' => 100, 'default' => '', 'null' => true, 'comment' => '平台交易编号']], + ['payment_status', 'integer', ['limit' => 1, 'default' => 0, 'null' => true, 'comment' => '支付状态(0未付,1已付,2取消)']], + ['payment_amount', 'decimal', ['precision' => 20, 'scale' => 2, 'default' => '0.00', 'null' => true, 'comment' => '实际支付金额']], + ['payment_coupon', 'decimal', ['precision' => 20, 'scale' => 2, 'default' => '0.00', 'null' => true, 'comment' => '平台优惠券金额']], + ['payment_images', 'string', ['limit' => 999, 'default' => '', 'null' => true, 'comment' => '凭证支付图片']], + ['payment_remark', 'string', ['limit' => 999, 'default' => '', 'null' => true, 'comment' => '支付状态备注']], + ['payment_notify', 'text', ['default' => null, 'null' => true, 'comment' => '支付通知内容']], + ['audit_user', 'biginteger', ['limit' => 20, 'default' => 0, 'null' => true, 'comment' => '审核用户(系统用户ID)']], + ['audit_time', 'datetime', ['default' => null, 'null' => true, 'comment' => '审核时间']], + ['audit_status', 'integer', ['limit' => 1, 'default' => 1, 'null' => true, 'comment' => '审核状态(0已拒,1待审,2已审)']], + ['audit_remark', 'string', ['limit' => 999, 'default' => '', 'null' => true, 'comment' => '审核描述']], + ['refund_status', 'integer', ['limit' => 1, 'default' => 0, 'null' => true, 'comment' => '退款状态(0未退,1已退)']], + ['refund_amount', 'decimal', ['precision' => 20, 'scale' => 2, 'default' => '0.00', 'null' => true, 'comment' => '累计退款']], + ['refund_payment', 'decimal', ['precision' => 20, 'scale' => 2, 'default' => '0.00', 'null' => true, 'comment' => '退回金额']], + ['refund_balance', 'decimal', ['precision' => 20, 'scale' => 2, 'default' => '0.00', 'null' => true, 'comment' => '退回余额']], + ['refund_integral', 'decimal', ['precision' => 20, 'scale' => 2, 'default' => '0.00', 'null' => true, 'comment' => '退回积分']], + ['used_payment', 'decimal', ['precision' => 20, 'scale' => 2, 'default' => '0.00', 'null' => true, 'comment' => '支付金额']], + ['used_balance', 'decimal', ['precision' => 20, 'scale' => 2, 'default' => '0.00', 'null' => true, 'comment' => '扣除余额']], + ['used_integral', 'decimal', ['precision' => 20, 'scale' => 2, 'default' => '0.00', 'null' => true, 'comment' => '扣除积分']], + ['create_time', 'datetime', ['default' => null, 'null' => true, 'comment' => '创建时间']], + ['update_time', 'datetime', ['default' => null, 'null' => true, 'comment' => '更新时间']], + ], [ + 'unid', 'usid', 'code', 'order_no', 'create_time', 'audit_status', 'channel_type', 'channel_code', 'payment_trade', 'refund_status', 'payment_status', + ], true); + } + + /** + * 创建数据对象 + * @class PluginPaymentRefund + * @table plugin_payment_refund + */ + private function _create_plugin_payment_refund() + { + // 创建数据表对象 + $table = $this->table('plugin_payment_refund', [ + 'engine' => 'InnoDB', 'collation' => 'utf8mb4_general_ci', 'comment' => '插件-支付-退款', + ]); + // 创建或更新数据表 + PhinxExtend::upgrade($table, [ + ['unid', 'biginteger', ['limit' => 20, 'default' => 0, 'null' => true, 'comment' => '主账号编号']], + ['usid', 'biginteger', ['limit' => 20, 'default' => 0, 'null' => true, 'comment' => '子账号编号']], + ['code', 'string', ['limit' => 20, 'default' => '', 'null' => true, 'comment' => '发起支付号']], + ['record_code', 'string', ['limit' => 20, 'default' => '', 'null' => true, 'comment' => '子支付编号']], + ['refund_time', 'datetime', ['default' => null, 'null' => true, 'comment' => '完成时间']], + ['refund_trade', 'string', ['limit' => 100, 'default' => '', 'null' => true, 'comment' => '交易编号']], + ['refund_status', 'integer', ['limit' => 1, 'default' => 0, 'null' => true, 'comment' => '支付状态(0未付,1已付,2取消)']], + ['refund_amount', 'decimal', ['precision' => 20, 'scale' => 2, 'default' => '0.00', 'null' => true, 'comment' => '退款金额']], + ['refund_account', 'string', ['limit' => 180, 'default' => '', 'null' => true, 'comment' => '退回账号']], + ['refund_scode', 'string', ['limit' => 50, 'default' => '', 'null' => true, 'comment' => '状态编码']], + ['refund_remark', 'string', ['limit' => 999, 'default' => '', 'null' => true, 'comment' => '退款备注']], + ['refund_notify', 'text', ['default' => null, 'null' => true, 'comment' => '通知内容']], + ['used_payment', 'decimal', ['precision' => 20, 'scale' => 2, 'default' => '0.00', 'null' => true, 'comment' => '退回金额']], + ['used_balance', 'decimal', ['precision' => 20, 'scale' => 2, 'default' => '0.00', 'null' => true, 'comment' => '退回余额']], + ['used_integral', 'decimal', ['precision' => 20, 'scale' => 2, 'default' => '0.00', 'null' => true, 'comment' => '退回积分']], + ['create_time', 'datetime', ['default' => null, 'null' => true, 'comment' => '创建时间']], + ['update_time', 'datetime', ['default' => null, 'null' => true, 'comment' => '更新时间']], + ], [ + 'unid', 'usid', 'code', 'record_code', 'create_time', 'refund_trade', 'refund_status', 'refund_account', + ], true); + } +} diff --git a/plugin/think-plugs-payment/tests/.bootstrap.php b/plugin/think-plugs-payment/tests/.bootstrap.php new file mode 100644 index 000000000..7d0b8a7a8 --- /dev/null +++ b/plugin/think-plugs-payment/tests/.bootstrap.php @@ -0,0 +1,22 @@ + 'mysql', + 'connections' => [ + 'mysql' => [ + 'type' => 'mysql', + 'hostname' => '127.0.0.1', + 'database' => 'admin_v6', + 'username' => 'admin_v6', + 'password' => 'FbYBHcWKr2', + 'hostport' => '3306', + 'charset' => 'utf8mb4', + 'debug' => true, + ], + ], +]); \ No newline at end of file diff --git a/plugin/think-plugs-payment/tests/BalanceIntegrationTest.php b/plugin/think-plugs-payment/tests/BalanceIntegrationTest.php new file mode 100644 index 000000000..6700b82b2 --- /dev/null +++ b/plugin/think-plugs-payment/tests/BalanceIntegrationTest.php @@ -0,0 +1,96 @@ +createAccountUser([ + 'phone' => $this->randomPhone('1331013'), + 'username' => 'balance-' . random_int(100, 999), + 'nickname' => '余额用户', + ]); + Balance::create(intval($user->getAttr('id')), 'charge-enough', '余额发放', '10.00', '英文提示测试'); + $this->switchPaymentLang('en-us'); + + $this->expectException(Exception::class); + $this->expectExceptionMessage('Insufficient balance for deduction'); + Balance::create(intval($user->getAttr('id')), 'charge-minus', '余额扣减', '-20.00', '超额扣减'); + } + + public function testCreateRecordsBalanceAndRecountsUserExtra(): void + { + $user = $this->createAccountUser(); + $model = Balance::create(intval($user->getAttr('id')), 'charge-create', '充值测试', '50.00', '首次充值'); + $user = $user->refresh(); + + $this->assertTrue($model->isExists()); + $this->assertSame('0.00', $this->decimal($model->getAttr('amount_prev'))); + $this->assertSame('50.00', $this->decimal($model->getAttr('amount_next'))); + $this->assertSame(0, intval($model->getAttr('unlock'))); + $this->assertSame('50.00', $this->decimal($user->getAttr('extra')['balance_lock'] ?? 0)); + $this->assertSame('50.00', $this->decimal($user->getAttr('extra')['balance_total'] ?? 0)); + $this->assertSame('50.00', $this->decimal($user->getAttr('extra')['balance_usable'] ?? 0)); + } + + public function testUnlockAndCancelRefreshStoredState(): void + { + $user = $this->createAccountUser(); + Balance::create(intval($user->getAttr('id')), 'charge-cancel', '充值测试', '20.00', '用于状态变更'); + + $unlocked = Balance::unlock('charge-cancel'); + $this->assertSame(1, intval($unlocked->getAttr('unlock'))); + $this->assertNotEmpty($unlocked->getAttr('unlock_time')); + + $cancelled = Balance::cancel('charge-cancel'); + $user = $user->refresh(); + + $this->assertSame(1, intval($cancelled->getAttr('cancel'))); + $this->assertNotEmpty($cancelled->getAttr('cancel_time')); + $this->assertSame('0.00', $this->decimal($user->getAttr('extra')['balance_lock'] ?? 0)); + $this->assertSame('0.00', $this->decimal($user->getAttr('extra')['balance_total'] ?? 0)); + $this->assertSame('0.00', $this->decimal($user->getAttr('extra')['balance_usable'] ?? 0)); + } + + protected function defineSchema(): void + { + $this->createAccountTables(); + $this->createPaymentBalanceTable(); + } + + private function switchPaymentLang(string $langSet): void + { + $this->app->lang->switchLangSet($langSet); + $file = TEST_PROJECT_ROOT . "/plugin/think-plugs-payment/src/lang/{$langSet}.php"; + if (is_file($file)) { + $this->app->lang->load($file, $langSet); + } + } +} diff --git a/plugin/think-plugs-payment/tests/IntegralIntegrationTest.php b/plugin/think-plugs-payment/tests/IntegralIntegrationTest.php new file mode 100644 index 000000000..51da64c53 --- /dev/null +++ b/plugin/think-plugs-payment/tests/IntegralIntegrationTest.php @@ -0,0 +1,113 @@ +context->setData('plugin.payment.config', ['integral' => 100]); + } + + public function testRatioAndCreateRefreshIntegralSummary(): void + { + $user = $this->createAccountUser([ + 'phone' => $this->randomPhone('1330013'), + 'username' => 'integral-' . random_int(100, 999), + 'nickname' => '积分用户', + ]); + $this->assertSame('2.500000', Integral::ratio('250')); + + $model = Integral::create(intval($user->getAttr('id')), 'integral-create', '积分发放', '30.00', '签到积分'); + $user = $user->refresh(); + + $this->assertTrue($model->isExists()); + $this->assertSame('0.00', $this->decimal($model->getAttr('amount_prev'))); + $this->assertSame('30.00', $this->decimal($model->getAttr('amount_next'))); + $this->assertSame('30.00', $this->decimal($user->getAttr('extra')['integral_lock'] ?? 0)); + $this->assertSame('30.00', $this->decimal($user->getAttr('extra')['integral_total'] ?? 0)); + $this->assertSame('30.00', $this->decimal($user->getAttr('extra')['integral_usable'] ?? 0)); + } + + public function testUnlockCancelAndInsufficientDeduction(): void + { + $user = $this->createAccountUser([ + 'phone' => $this->randomPhone('1330013'), + 'username' => 'integral-' . random_int(100, 999), + 'nickname' => '积分用户', + ]); + Integral::create(intval($user->getAttr('id')), 'integral-state', '积分发放', '12.00', '用于状态变更'); + + $unlocked = Integral::unlock('integral-state'); + $this->assertSame(1, intval($unlocked->getAttr('unlock'))); + $this->assertNotEmpty($unlocked->getAttr('unlock_time')); + + $cancelled = Integral::cancel('integral-state'); + $user = $user->refresh(); + $this->assertSame(1, intval($cancelled->getAttr('cancel'))); + $this->assertSame('0.00', $this->decimal($user->getAttr('extra')['integral_lock'] ?? 0)); + $this->assertSame('0.00', $this->decimal($user->getAttr('extra')['integral_total'] ?? 0)); + $this->assertSame('0.00', $this->decimal($user->getAttr('extra')['integral_usable'] ?? 0)); + + $this->expectException(Exception::class); + $this->expectExceptionMessage('扣减积分不足'); + Integral::create(intval($user->getAttr('id')), 'integral-minus', '积分扣减', '-20.00', '超额扣减'); + } + + public function testInsufficientDeductionReturnsEnglishMessageWhenLangSetIsEnUs(): void + { + $user = $this->createAccountUser([ + 'phone' => $this->randomPhone('1330014'), + 'username' => 'integral-en-' . random_int(100, 999), + 'nickname' => '积分英文用户', + ]); + Integral::create(intval($user->getAttr('id')), 'integral-enough', '积分发放', '12.00', '英文提示测试'); + $this->switchPaymentLang('en-us'); + + $this->expectException(Exception::class); + $this->expectExceptionMessage('Insufficient integral for deduction'); + Integral::create(intval($user->getAttr('id')), 'integral-minus-en', '积分扣减', '-20.00', '超额扣减'); + } + + protected function defineSchema(): void + { + $this->createAccountTables(); + $this->createPaymentIntegralTable(); + } + + private function switchPaymentLang(string $langSet): void + { + $this->app->lang->switchLangSet($langSet); + $file = TEST_PROJECT_ROOT . "/plugin/think-plugs-payment/src/lang/{$langSet}.php"; + if (is_file($file)) { + $this->app->lang->load($file, $langSet); + } + } +} diff --git a/plugin/think-plugs-payment/tests/PaymentAddressControllerTest.php b/plugin/think-plugs-payment/tests/PaymentAddressControllerTest.php new file mode 100644 index 000000000..56a784390 --- /dev/null +++ b/plugin/think-plugs-payment/tests/PaymentAddressControllerTest.php @@ -0,0 +1,158 @@ +configureAccountAccess([ + 'headimg' => 'https://example.com/payment-address.png', + 'userPrefix' => '地址账号', + ]); + } + + public function testGetControllerReturnsEnglishInfoWhenLangSetIsEnUs(): void + { + $account = $this->createBoundAccountFixture(); + $login = $account->token()->get(true); + $this->createPaymentAddressFixture($account->getUnid()); + $this->switchPaymentLang('en-us'); + + $response = $this->callAuthApiController('GET', 'get', [], strval($login['token'] ?? '')); + + $this->assertSame(200, intval($response['code'] ?? 0)); + $this->assertSame('Address data loaded successfully', $response['info'] ?? ''); + $this->assertNotEmpty($response['data'] ?? []); + } + + public function testSetControllerReturnsEnglishValidationInfoWhenLangSetIsEnUs(): void + { + $account = $this->createBoundAccountFixture(); + $login = $account->token()->get(true); + $this->switchPaymentLang('en-us'); + + $response = $this->callAuthApiController('POST', 'set', [ + 'user_phone' => $this->randomPhone('1361100'), + 'region_prov' => 'Guangdong', + 'region_city' => 'Shenzhen', + 'region_area' => 'Nanshan', + 'region_addr' => 'Science Park', + ], strval($login['token'] ?? '')); + + $this->assertSame(500, intval($response['code'] ?? 0)); + $this->assertSame('Recipient name is required', $response['info'] ?? ''); + } + + public function testSetStateAndRemoveReturnEnglishInfosWhenLangSetIsEnUs(): void + { + $account = $this->createBoundAccountFixture(); + $login = $account->token()->get(true); + $this->switchPaymentLang('en-us'); + + $create = $this->callAuthApiController('POST', 'set', [ + 'type' => 0, + 'user_name' => 'Alice Receiver', + 'user_phone' => $this->randomPhone('1361101'), + 'region_prov' => 'Guangdong', + 'region_city' => 'Shenzhen', + 'region_area' => 'Nanshan', + 'region_addr' => 'No. 1 Science Park', + ], strval($login['token'] ?? '')); + + $addressId = intval($create['data']['id'] ?? 0); + $state = $this->callAuthApiController('POST', 'state', [ + 'id' => $addressId, + 'type' => 1, + ], strval($login['token'] ?? '')); + $remove = $this->callAuthApiController('POST', 'remove', [ + 'id' => $addressId, + ], strval($login['token'] ?? '')); + + $this->assertSame(200, intval($create['code'] ?? 0)); + $this->assertSame('Saved successfully', $create['info'] ?? ''); + $this->assertGreaterThan(0, $addressId); + $this->assertSame(200, intval($state['code'] ?? 0)); + $this->assertSame('Default address set successfully', $state['info'] ?? ''); + $this->assertSame(200, intval($remove['code'] ?? 0)); + $this->assertSame('Deleted successfully', $remove['info'] ?? ''); + } + + protected function defineSchema(): void + { + $this->createAccountTables(); + $this->createPaymentAddressTable(); + } + + private function callAuthApiController(string $method, string $action, array $data, string $token): array + { + $request = (new Request()) + ->withGet($data) + ->withPost($data) + ->withHeader(['authorization' => "Bearer {$token}"]) + ->setMethod($method) + ->setController('api.auth.address') + ->setAction($action); + + $this->setRequestPayload($request, $data); + RequestContext::clear(); + $this->app->instance('request', $request); + + try { + $controller = new AuthAddressController($this->app); + $controller->{$action}(); + self::fail("Expected {$action} to throw HttpResponseException."); + } catch (HttpResponseException $exception) { + return json_decode($exception->getResponse()->getContent(), true) ?: []; + } + } + + private function setRequestPayload(Request $request, array $data): void + { + $property = new \ReflectionProperty(Request::class, 'request'); + $property->setAccessible(true); + $property->setValue($request, $data); + } + + private function switchPaymentLang(string $langSet): void + { + $this->app->lang->switchLangSet($langSet); + foreach ([ + TEST_PROJECT_ROOT . "/plugin/think-plugs-payment/src/lang/{$langSet}.php", + TEST_PROJECT_ROOT . "/plugin/think-plugs-account/src/lang/{$langSet}.php", + ] as $file) { + if (is_file($file)) { + $this->app->lang->load($file, $langSet); + } + } + } +} diff --git a/plugin/think-plugs-payment/tests/PaymentLedgerControllerTest.php b/plugin/think-plugs-payment/tests/PaymentLedgerControllerTest.php new file mode 100644 index 000000000..29df23096 --- /dev/null +++ b/plugin/think-plugs-payment/tests/PaymentLedgerControllerTest.php @@ -0,0 +1,580 @@ +configureAccountAccess([ + 'headimg' => 'https://example.com/payment-ledger-controller.png', + 'userPrefix' => '台账控制器账号', + ]); + } + + protected function afterSchemaCreated(): void + { + $this->app->setAppPath(TEST_PROJECT_ROOT . '/plugin/think-plugs-payment/src/'); + $this->configureView([ + 'view_path' => TEST_PROJECT_ROOT . '/plugin/think-plugs-payment/src/view' . DIRECTORY_SEPARATOR, + ]); + } + + public function testBalanceControllerUnlockCancelAndRemoveChain(): void + { + $user = $this->createAccountUser(); + Balance::create(intval($user->getAttr('id')), 'ledger-balance-001', '余额发放', '30.00', '后台台账测试'); + + $unlock = $this->callController(BalanceController::class, 'unlock', [ + 'code' => 'ledger-balance-001', + 'unlock' => 1, + ]); + $cancel = $this->callController(BalanceController::class, 'cancel', [ + 'code' => 'ledger-balance-001', + 'cancel' => 1, + ]); + $remove = $this->callController(BalanceController::class, 'remove', [ + 'code' => 'ledger-balance-001', + ]); + + $model = PluginPaymentBalance::mk()->withTrashed()->where(['code' => 'ledger-balance-001'])->findOrEmpty(); + $user = $user->refresh(); + $extra = $user->getAttr('extra'); + + $this->assertSame(200, intval($unlock['code'] ?? 0)); + $this->assertSame(200, intval($cancel['code'] ?? 0)); + $this->assertSame(200, intval($remove['code'] ?? 0)); + $this->assertSame('交易操作成功!', $remove['info'] ?? ''); + $this->assertSame(1, intval($model->getAttr('unlock'))); + $this->assertSame(1, intval($model->getAttr('cancel'))); + $this->assertNotEmpty($model->getAttr('unlock_time')); + $this->assertNotEmpty($model->getAttr('cancel_time')); + $this->assertNotEmpty($model->getAttr('delete_time')); + $this->assertSame('0.00', $this->decimal($extra['balance_lock'] ?? 0)); + $this->assertSame('0.00', $this->decimal($extra['balance_total'] ?? 0)); + $this->assertSame('0.00', $this->decimal($extra['balance_usable'] ?? 0)); + } + + public function testIntegralControllerUnlockCancelAndRemoveChain(): void + { + $user = $this->createAccountUser(); + Integral::create(intval($user->getAttr('id')), 'ledger-integral-001', '积分发放', '18.00', '后台台账测试'); + + $unlock = $this->callController(IntegralController::class, 'unlock', [ + 'code' => 'ledger-integral-001', + 'unlock' => 1, + ]); + $cancel = $this->callController(IntegralController::class, 'cancel', [ + 'code' => 'ledger-integral-001', + 'cancel' => 1, + ]); + $remove = $this->callController(IntegralController::class, 'remove', [ + 'code' => 'ledger-integral-001', + ]); + + $model = PluginPaymentIntegral::mk()->withTrashed()->where(['code' => 'ledger-integral-001'])->findOrEmpty(); + $user = $user->refresh(); + $extra = $user->getAttr('extra'); + + $this->assertSame(200, intval($unlock['code'] ?? 0)); + $this->assertSame(200, intval($cancel['code'] ?? 0)); + $this->assertSame(200, intval($remove['code'] ?? 0)); + $this->assertSame('交易操作成功!', $remove['info'] ?? ''); + $this->assertSame(1, intval($model->getAttr('unlock'))); + $this->assertSame(1, intval($model->getAttr('cancel'))); + $this->assertNotEmpty($model->getAttr('unlock_time')); + $this->assertNotEmpty($model->getAttr('cancel_time')); + $this->assertNotEmpty($model->getAttr('delete_time')); + $this->assertSame('0.00', $this->decimal($extra['integral_lock'] ?? 0)); + $this->assertSame('0.00', $this->decimal($extra['integral_total'] ?? 0)); + $this->assertSame('0.00', $this->decimal($extra['integral_usable'] ?? 0)); + } + + public function testBalanceIndexControllerFiltersActiveLedgersByUserKeyword(): void + { + $user = $this->createAccountUser([ + 'username' => 'balance-search-user', + 'nickname' => '余额检索用户', + ]); + $other = $this->createAccountUser([ + 'username' => 'balance-other-user', + 'nickname' => '余额其他用户', + ]); + + Balance::create(intval($user->getAttr('id')), 'ledger-balance-active', '有效余额', '30.00', '有效余额记录'); + Balance::create(intval($user->getAttr('id')), 'ledger-balance-cancelled', '作废余额', '12.00', '作废余额记录'); + Balance::cancel('ledger-balance-cancelled', 1); + Balance::create(intval($user->getAttr('id')), 'ledger-balance-deleted', '删除余额', '8.00', '删除余额记录'); + Balance::remove('ledger-balance-deleted'); + Balance::create(intval($other->getAttr('id')), 'ledger-balance-other', '其他余额', '6.00', '其他用户余额'); + + $result = $this->callIndexController(BalanceController::class, [ + 'output' => 'json', + 'user' => 'balance-search-user', + 'page' => 1, + 'limit' => 20, + ]); + + $this->assertSame(200, intval($result['code'] ?? 0)); + $this->assertSame('JSON-DATA', $result['info'] ?? ''); + $this->assertSame(1, intval($result['data']['page']['total'] ?? 0)); + $this->assertCount(1, $result['data']['list'] ?? []); + $this->assertSame('ledger-balance-active', $result['data']['list'][0]['code'] ?? ''); + $this->assertSame('balance-search-user', $result['data']['list'][0]['user']['username'] ?? ''); + } + + public function testIntegralIndexControllerShowsCancelledLedgersForHistoryType(): void + { + $user = $this->createAccountUser([ + 'username' => 'integral-history-user', + 'nickname' => '积分检索用户', + ]); + $other = $this->createAccountUser([ + 'username' => 'integral-other-user', + 'nickname' => '积分其他用户', + ]); + + Integral::create(intval($user->getAttr('id')), 'ledger-integral-active', '有效积分', '18.00', '有效积分记录'); + Integral::create(intval($user->getAttr('id')), 'ledger-integral-cancelled', '作废积分', '9.00', '作废积分记录'); + Integral::cancel('ledger-integral-cancelled', 1); + Integral::create(intval($user->getAttr('id')), 'ledger-integral-deleted', '删除积分', '5.00', '删除积分记录'); + Integral::cancel('ledger-integral-deleted', 1); + Integral::remove('ledger-integral-deleted'); + Integral::create(intval($other->getAttr('id')), 'ledger-integral-other', '其他积分', '7.00', '其他用户积分'); + Integral::cancel('ledger-integral-other', 1); + + $result = $this->callIndexController(IntegralController::class, [ + 'output' => 'json', + 'type' => 'history', + 'user' => 'integral-history-user', + 'page' => 1, + 'limit' => 20, + ]); + + $this->assertSame(200, intval($result['code'] ?? 0)); + $this->assertSame('JSON-DATA', $result['info'] ?? ''); + $this->assertSame(1, intval($result['data']['page']['total'] ?? 0)); + $this->assertCount(1, $result['data']['list'] ?? []); + $this->assertSame('ledger-integral-cancelled', $result['data']['list'][0]['code'] ?? ''); + $this->assertSame('integral-history-user', $result['data']['list'][0]['user']['username'] ?? ''); + } + + public function testBalanceIndexControllerAppliesDateRangeAndDescendingOrder(): void + { + $user = $this->createAccountUser([ + 'username' => 'balance-range-user', + 'nickname' => '余额时间用户', + ]); + + Balance::create(intval($user->getAttr('id')), 'ledger-balance-older', '旧余额', '10.00', '旧余额记录'); + Balance::create(intval($user->getAttr('id')), 'ledger-balance-middle', '中余额', '20.00', '中余额记录'); + Balance::create(intval($user->getAttr('id')), 'ledger-balance-newer', '新余额', '30.00', '新余额记录'); + + PluginPaymentBalance::mk()->where(['code' => 'ledger-balance-older'])->update([ + 'create_time' => '2026-03-09 08:00:00', + 'update_time' => '2026-03-09 08:00:00', + ]); + PluginPaymentBalance::mk()->where(['code' => 'ledger-balance-middle'])->update([ + 'create_time' => '2026-03-10 09:00:00', + 'update_time' => '2026-03-10 09:00:00', + ]); + PluginPaymentBalance::mk()->where(['code' => 'ledger-balance-newer'])->update([ + 'create_time' => '2026-03-10 18:30:00', + 'update_time' => '2026-03-10 18:30:00', + ]); + + $result = $this->callIndexController(BalanceController::class, [ + 'output' => 'json', + 'user' => 'balance-range-user', + 'create_time' => '2026-03-10 - 2026-03-10', + '_field_' => 'create_time', + '_order_' => 'desc', + 'page' => 1, + 'limit' => 20, + ]); + + $this->assertSame(200, intval($result['code'] ?? 0)); + $this->assertSame('JSON-DATA', $result['info'] ?? ''); + $this->assertSame(2, intval($result['data']['page']['total'] ?? 0)); + $this->assertSame([ + 'ledger-balance-newer', + 'ledger-balance-middle', + ], array_column($result['data']['list'] ?? [], 'code')); + } + + public function testIntegralIndexControllerAppliesAmountSortForHistoryView(): void + { + $user = $this->createAccountUser([ + 'username' => 'integral-sort-user', + 'nickname' => '积分排序用户', + ]); + + Integral::create(intval($user->getAttr('id')), 'ledger-integral-low', '低积分', '6.00', '低积分记录'); + Integral::cancel('ledger-integral-low', 1); + Integral::create(intval($user->getAttr('id')), 'ledger-integral-mid', '中积分', '12.00', '中积分记录'); + Integral::cancel('ledger-integral-mid', 1); + Integral::create(intval($user->getAttr('id')), 'ledger-integral-high', '高积分', '18.00', '高积分记录'); + Integral::cancel('ledger-integral-high', 1); + Integral::create(intval($user->getAttr('id')), 'ledger-integral-deleted-history', '删积分', '24.00', '删除积分记录'); + Integral::cancel('ledger-integral-deleted-history', 1); + Integral::remove('ledger-integral-deleted-history'); + + $result = $this->callIndexController(IntegralController::class, [ + 'output' => 'json', + 'type' => 'history', + 'user' => 'integral-sort-user', + '_field_' => 'amount', + '_order_' => 'desc', + 'page' => 1, + 'limit' => 20, + ]); + + $this->assertSame(200, intval($result['code'] ?? 0)); + $this->assertSame('JSON-DATA', $result['info'] ?? ''); + $this->assertSame(3, intval($result['data']['page']['total'] ?? 0)); + $this->assertSame([ + 'ledger-integral-high', + 'ledger-integral-mid', + 'ledger-integral-low', + ], array_column($result['data']['list'] ?? [], 'code')); + } + + public function testBalanceIndexControllerReturnsSecondPageWithConfiguredLimit(): void + { + $user = $this->createAccountUser([ + 'username' => 'balance-page-user', + 'nickname' => '余额分页用户', + ]); + + for ($i = 1; $i <= 11; ++$i) { + $code = sprintf('ledger-balance-page-%02d', $i); + Balance::create(intval($user->getAttr('id')), $code, "分页余额{$i}", number_format((float)$i, 2, '.', ''), '余额分页测试'); + } + + $result = $this->callIndexController(BalanceController::class, [ + 'output' => 'json', + 'user' => 'balance-page-user', + '_field_' => 'amount', + '_order_' => 'asc', + 'page' => 2, + 'limit' => 10, + ]); + + $this->assertSame(200, intval($result['code'] ?? 0)); + $this->assertSame('JSON-DATA', $result['info'] ?? ''); + $this->assertSame(11, intval($result['data']['page']['total'] ?? 0)); + $this->assertSame(2, intval($result['data']['page']['pages'] ?? 0)); + $this->assertSame(2, intval($result['data']['page']['current'] ?? 0)); + $this->assertSame(10, intval($result['data']['page']['limit'] ?? 0)); + $this->assertCount(1, $result['data']['list'] ?? []); + $this->assertSame('ledger-balance-page-11', $result['data']['list'][0]['code'] ?? ''); + $this->assertSame('11.00', $this->decimal($result['data']['list'][0]['amount'] ?? 0)); + } + + public function testIntegralIndexControllerFallsBackToDefaultLimitWhenRequestedLimitIsInvalid(): void + { + $user = $this->createAccountUser([ + 'username' => 'integral-page-user', + 'nickname' => '积分分页用户', + ]); + + for ($i = 1; $i <= 21; ++$i) { + $code = sprintf('ledger-integral-page-%02d', $i); + Integral::create(intval($user->getAttr('id')), $code, "分页积分{$i}", number_format((float)$i, 2, '.', ''), '积分分页测试'); + Integral::cancel($code, 1); + } + + $result = $this->callIndexController(IntegralController::class, [ + 'output' => 'json', + 'type' => 'history', + 'user' => 'integral-page-user', + '_field_' => 'amount', + '_order_' => 'asc', + 'page' => 2, + 'limit' => 999, + ]); + + $this->assertSame(200, intval($result['code'] ?? 0)); + $this->assertSame('JSON-DATA', $result['info'] ?? ''); + $this->assertSame(21, intval($result['data']['page']['total'] ?? 0)); + $this->assertSame(2, intval($result['data']['page']['pages'] ?? 0)); + $this->assertSame(2, intval($result['data']['page']['current'] ?? 0)); + $this->assertSame(20, intval($result['data']['page']['limit'] ?? 0)); + $this->assertCount(1, $result['data']['list'] ?? []); + $this->assertSame('ledger-integral-page-21', $result['data']['list'][0]['code'] ?? ''); + $this->assertSame('21.00', $this->decimal($result['data']['list'][0]['amount'] ?? 0)); + } + + public function testConfigIndexRendersEnglishTextsWhenLangSetIsEnUs(): void + { + $this->switchPaymentLang('en-us'); + + $html = $this->callActionHtml(ConfigController::class, 'index'); + + $this->assertStringContainsString('Payment Management', $html); + $this->assertStringContainsString('Recycle Bin', $html); + $this->assertStringContainsString('Payment Code', $html); + $this->assertStringContainsString('Payment Name', $html); + $this->assertStringContainsString('Payment Type', $html); + $this->assertStringContainsString('Payment Configuration Management', $html); + $this->assertStringContainsString('Search', $html); + $this->assertStringNotContainsString('支付编号', $html); + } + + public function testConfigAddRendersEnglishTextsWhenLangSetIsEnUs(): void + { + $this->switchPaymentLang('en-us'); + + $html = $this->callActionHtml(ConfigController::class, 'add'); + + $this->assertStringContainsString('Payment Type', $html); + $this->assertStringContainsString('Payment Name', $html); + $this->assertStringContainsString('Payment Icon', $html); + $this->assertStringContainsString('Payment Description', $html); + $this->assertStringContainsString('Save Data', $html); + $this->assertStringContainsString('Offline Payment QR Code', $html); + $this->assertStringNotContainsString('支付方式', $html); + } + + public function testIntegralIndexRendersEnglishTextsWhenLangSetIsEnUs(): void + { + $user = $this->createAccountUser([ + 'username' => 'integral-english-user', + 'nickname' => '积分英文用户', + ]); + + Integral::create(intval($user->getAttr('id')), 'ledger-integral-en', '英文积分', '16.00', '英文积分记录'); + $this->switchPaymentLang('en-us'); + + $html = $this->callActionHtml(IntegralController::class, 'index'); + + $this->assertStringContainsString('Integral Statistics', $html); + $this->assertStringContainsString('Total Issued', $html); + $this->assertStringContainsString('User Account', $html); + $this->assertStringContainsString('Transaction Status', $html); + $this->assertStringContainsString('Operation Remark', $html); + $this->assertStringContainsString('Actions', $html); + $this->assertStringNotContainsString('积分统计', $html); + } + + public function testApiBalanceGetReturnsEnglishInfoWhenLangSetIsEnUs(): void + { + $account = $this->createBoundAccountFixture(); + $login = $account->token()->get(true); + Balance::create($account->getUnid(), 'api-balance-001', 'API余额', '18.00', 'API余额记录', true); + $this->switchPaymentLang('en-us'); + + $response = $this->callAuthApiController(AuthBalanceController::class, 'get', ['page' => 1], strval($login['token'] ?? '')); + + $this->assertSame(200, intval($response['code'] ?? 0)); + $this->assertSame('Balance records loaded successfully', $response['info'] ?? ''); + $this->assertNotEmpty($response['data'] ?? []); + } + + public function testApiIntegralGetReturnsEnglishInfoWhenLangSetIsEnUs(): void + { + $account = $this->createBoundAccountFixture(); + $login = $account->token()->get(true); + Integral::create($account->getUnid(), 'api-integral-001', 'API积分', '16.00', 'API积分记录', true); + $this->switchPaymentLang('en-us'); + + $response = $this->callAuthApiController(AuthIntegralController::class, 'get', ['page' => 1], strval($login['token'] ?? '')); + + $this->assertSame(200, intval($response['code'] ?? 0)); + $this->assertSame('Integral records loaded successfully', $response['info'] ?? ''); + $this->assertNotEmpty($response['data'] ?? []); + } + + protected function defineSchema(): void + { + $this->createAccountTables(); + $this->createPaymentConfigTable(); + $this->createPaymentBalanceTable(); + $this->createPaymentIntegralTable(); + } + + /** + * @param class-string $controllerClass + */ + private function callController(string $controllerClass, string $action, array $data): array + { + $parts = explode('\\', $controllerClass); + $request = (new Request()) + ->withGet($data) + ->withPost($data) + ->setMethod('POST') + ->setController(strtolower(strval(end($parts)))) + ->setAction($action); + + RequestContext::clear(); + $this->app->instance('request', $request); + + try { + $controller = new $controllerClass($this->app); + $controller->{$action}(); + self::fail("Expected {$controllerClass}::{$action} to throw HttpResponseException."); + } catch (HttpResponseException $exception) { + return json_decode($exception->getResponse()->getContent(), true) ?: []; + } + } + + /** + * @param class-string $controllerClass + */ + private function callIndexController(string $controllerClass, array $query): array + { + $parts = explode('\\', $controllerClass); + $request = (new Request()) + ->withGet($query) + ->setMethod('GET') + ->setController(strtolower(strval(end($parts)))) + ->setAction('index'); + + $this->setRequestPayload($request, $query); + RequestContext::clear(); + $this->app->instance('request', $request); + + try { + $controller = new $controllerClass($this->app); + $controller->index(); + self::fail("Expected {$controllerClass}::index to throw HttpResponseException."); + } catch (HttpResponseException $exception) { + return json_decode($exception->getResponse()->getContent(), true) ?: []; + } + } + + /** + * @param class-string $controllerClass + */ + private function callAuthApiController(string $controllerClass, string $action, array $query, string $token): array + { + $parts = explode('\\', $controllerClass); + $request = (new Request()) + ->withGet($query) + ->withHeader(['authorization' => "Bearer {$token}"]) + ->setMethod('GET') + ->setController('api.auth.' . strtolower(strval(end($parts)))) + ->setAction($action); + + $this->setRequestPayload($request, $query); + RequestContext::clear(); + $this->app->instance('request', $request); + + try { + $controller = new $controllerClass($this->app); + $controller->{$action}(); + self::fail("Expected {$controllerClass}::{$action} to throw HttpResponseException."); + } catch (HttpResponseException $exception) { + return json_decode($exception->getResponse()->getContent(), true) ?: []; + } + } + + /** + * @param class-string $controllerClass + */ + private function callActionHtml(string $controllerClass, string $action, array $query = []): string + { + $parts = explode('\\', $controllerClass); + $request = (new Request()) + ->withGet($query) + ->setMethod('GET') + ->setController(strtolower(strval(end($parts)))) + ->setAction($action); + + $this->setRequestPayload($request, $query); + RequestContext::clear(); + RequestContext::instance()->setAuth([ + 'id' => 9001, + 'username' => 'admin', + 'password' => 'test-admin-password', + ], '', true); + $this->activateApplicationContext($request); + + try { + $controller = new $controllerClass($this->app); + $controller->{$action}(); + self::fail("Expected {$controllerClass}::{$action} to throw HttpResponseException."); + } catch (HttpResponseException $exception) { + return $exception->getResponse()->getContent(); + } + } + + private function setRequestPayload(Request $request, array $data): void + { + $property = new \ReflectionProperty(Request::class, 'request'); + $property->setAccessible(true); + $property->setValue($request, $data); + } + + private function switchPaymentLang(string $langSet): void + { + $this->app->lang->switchLangSet($langSet); + foreach ([ + TEST_PROJECT_ROOT . "/plugin/think-plugs-payment/src/lang/{$langSet}.php", + TEST_PROJECT_ROOT . "/plugin/think-plugs-account/src/lang/{$langSet}.php", + ] as $file) { + if (is_file($file)) { + $this->app->lang->load($file, $langSet); + } + } + } + + private function createPaymentConfigTable(): void + { + $this->executeStatements([ + <<<'SQL' +CREATE TABLE plugin_payment_config ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + type TEXT DEFAULT '', + code TEXT DEFAULT '', + name TEXT DEFAULT '', + cover TEXT DEFAULT '', + remark TEXT DEFAULT '', + content TEXT DEFAULT '', + sort INTEGER DEFAULT 0, + status INTEGER DEFAULT 1, + delete_time TEXT DEFAULT NULL, + create_time TEXT DEFAULT NULL, + update_time TEXT DEFAULT NULL +) +SQL, + ]); + } +} diff --git a/plugin/think-plugs-payment/tests/PaymentLedgerIntegrationTest.php b/plugin/think-plugs-payment/tests/PaymentLedgerIntegrationTest.php new file mode 100644 index 000000000..de1a31fb7 --- /dev/null +++ b/plugin/think-plugs-payment/tests/PaymentLedgerIntegrationTest.php @@ -0,0 +1,204 @@ +configureAccountAccess([ + 'headimg' => 'https://example.com/payment-ledger-account.png', + 'userPrefix' => '台账账号', + ]); + } + + public function testBalancePaymentRefundRoundTripsBalanceLedger(): void + { + $account = $this->createBoundAccountFixture(); + $unid = $account->getUnid(); + + Balance::create($unid, 'balance-seed', '初始余额', '50.00', '初始余额注入', true); + $response = Payment::mk(Payment::BALANCE)->create( + $account, + 'ORDER-BALANCE-001', + '余额支付订单', + '20.00', + '20.00', + '余额支付下单' + ); + + $record = PluginPaymentRecord::mk()->where(['code' => $response->record['code']])->findOrEmpty(); + $this->assertSame(Payment::BALANCE, $record->getAttr('channel_type')); + $this->assertSame('20.00', $this->decimal($record->getAttr('used_balance'))); + $this->assertSame('20.00', $this->decimal($record->getAttr('payment_amount'))); + $this->assertSame(1, intval($record->getAttr('payment_status'))); + $this->assertSame('30.00', $this->decimal(Balance::recount($unid)['usable'])); + + $refundCode = ''; + $result = Payment::mk(Payment::BALANCE)->refund($record->getAttr('code'), '5.00', '余额退款', $refundCode); + $refund = PluginPaymentRefund::mk()->where(['code' => $refundCode])->findOrEmpty(); + $refundBalance = PluginPaymentBalance::mk()->where(['code' => $refundCode])->findOrEmpty(); + + $this->assertSame(200, intval($result['code'] ?? 0)); + $this->assertSame('发起退款成功!', $result['info'] ?? ''); + $this->assertSame($refundCode, $result['data']['refund_code'] ?? ''); + $this->assertTrue($refund->isExists()); + $this->assertTrue($refundBalance->isExists()); + $this->assertSame(Payment::BALANCE, $refund->getAttr('refund_account')); + $this->assertSame('5.00', $this->decimal($refund->getAttr('used_balance'))); + $this->assertSame('5.00', $this->decimal($refundBalance->getAttr('amount'))); + $this->assertSame('来自订单 ORDER-BALANCE-001 退回余额', $refundBalance->getAttr('remark')); + $this->assertSame('35.00', $this->decimal(Balance::recount($unid)['usable'])); + $this->assertSame('5.00', $this->decimal(Payment::totalRefundAmount($record->getAttr('code'))['balance'])); + } + + public function testIntegralPaymentRefundRoundTripsIntegralLedger(): void + { + $account = $this->createBoundAccountFixture(); + $unid = $account->getUnid(); + + Integral::create($unid, 'integral-seed', '初始积分', '30.00', '初始积分注入', true); + $response = Payment::mk(Payment::INTEGRAL)->create( + $account, + 'ORDER-INTEGRAL-001', + '积分支付订单', + '12.00', + '12.00', + '积分支付下单' + ); + + $record = PluginPaymentRecord::mk()->where(['code' => $response->record['code']])->findOrEmpty(); + $this->assertSame(Payment::INTEGRAL, $record->getAttr('channel_type')); + $this->assertSame('12.00', $this->decimal($record->getAttr('used_integral'))); + $this->assertSame('12.00', $this->decimal($record->getAttr('payment_amount'))); + $this->assertSame(1, intval($record->getAttr('payment_status'))); + $this->assertSame('18.00', $this->decimal(Integral::recount($unid)['usable'])); + + $refundCode = ''; + $result = Payment::mk(Payment::INTEGRAL)->refund($record->getAttr('code'), '4.00', '积分退款', $refundCode); + $refund = PluginPaymentRefund::mk()->where(['code' => $refundCode])->findOrEmpty(); + $refundIntegral = PluginPaymentIntegral::mk()->where(['code' => $refundCode])->findOrEmpty(); + + $this->assertSame(200, intval($result['code'] ?? 0)); + $this->assertSame('发起退款成功!', $result['info'] ?? ''); + $this->assertSame($refundCode, $result['data']['refund_code'] ?? ''); + $this->assertTrue($refund->isExists()); + $this->assertTrue($refundIntegral->isExists()); + $this->assertSame(Payment::INTEGRAL, $refund->getAttr('refund_account')); + $this->assertSame('4.00', $this->decimal($refund->getAttr('used_integral'))); + $this->assertSame('4.00', $this->decimal($refundIntegral->getAttr('amount'))); + $this->assertSame('来自订单 ORDER-INTEGRAL-001 退回积分', $refundIntegral->getAttr('remark')); + $this->assertSame('22.00', $this->decimal(Integral::recount($unid)['usable'])); + $this->assertSame('4.00', $this->decimal(Payment::totalRefundAmount($record->getAttr('code'))['integral'])); + } + + public function testBalancePaymentRefundUsesEnglishRemarkWhenLangSetIsEnUs(): void + { + $account = $this->createBoundAccountFixture(); + $unid = $account->getUnid(); + + Balance::create($unid, 'balance-seed-en', 'Initial balance', '50.00', 'Initial balance seed', true); + $this->switchPaymentLang('en-us'); + + $response = Payment::mk(Payment::BALANCE)->create( + $account, + 'ORDER-BALANCE-EN-001', + 'Balance payment order', + '20.00', + '20.00', + 'Balance payment create' + ); + + $refundCode = ''; + $result = Payment::mk(Payment::BALANCE)->refund($response->record['code'], '5.00', 'Balance refund', $refundCode); + $refundBalance = PluginPaymentBalance::mk()->where(['code' => $refundCode])->findOrEmpty(); + + $this->assertSame('Balance payment completed', $response->message); + $this->assertSame(200, intval($result['code'] ?? 0)); + $this->assertSame('Refund requested successfully', $result['info'] ?? ''); + $this->assertSame('Balance Refund', $refundBalance->getAttr('name')); + $this->assertSame('Refund balance from order ORDER-BALANCE-EN-001', $refundBalance->getAttr('remark')); + } + + public function testIntegralPaymentRefundUsesEnglishRemarkWhenLangSetIsEnUs(): void + { + $account = $this->createBoundAccountFixture(); + $unid = $account->getUnid(); + + Integral::create($unid, 'integral-seed-en', 'Initial integral', '30.00', 'Initial integral seed', true); + $this->switchPaymentLang('en-us'); + + $response = Payment::mk(Payment::INTEGRAL)->create( + $account, + 'ORDER-INTEGRAL-EN-001', + 'Integral payment order', + '12.00', + '12.00', + 'Integral payment create' + ); + + $refundCode = ''; + $result = Payment::mk(Payment::INTEGRAL)->refund($response->record['code'], '4.00', 'Integral refund', $refundCode); + $refundIntegral = PluginPaymentIntegral::mk()->where(['code' => $refundCode])->findOrEmpty(); + + $this->assertSame('Integral deduction completed', $response->message); + $this->assertSame(200, intval($result['code'] ?? 0)); + $this->assertSame('Refund requested successfully', $result['info'] ?? ''); + $this->assertSame('Integral Refund', $refundIntegral->getAttr('name')); + $this->assertSame('Refund integral from order ORDER-INTEGRAL-EN-001', $refundIntegral->getAttr('remark')); + } + + protected function defineSchema(): void + { + $this->createAccountTables(); + $this->createPaymentRecordTable(); + $this->createPaymentRefundTable(); + $this->createPaymentBalanceTable(); + $this->createPaymentIntegralTable(); + } + + private function switchPaymentLang(string $langSet): void + { + $this->app->lang->switchLangSet($langSet); + foreach ([ + TEST_PROJECT_ROOT . "/plugin/think-plugs-payment/src/lang/{$langSet}.php", + TEST_PROJECT_ROOT . "/plugin/think-plugs-account/src/lang/{$langSet}.php", + ] as $file) { + if (is_file($file)) { + $this->app->lang->load($file, $langSet); + } + } + } +} diff --git a/plugin/think-plugs-payment/tests/PaymentRecordControllerTest.php b/plugin/think-plugs-payment/tests/PaymentRecordControllerTest.php new file mode 100644 index 000000000..51e422640 --- /dev/null +++ b/plugin/think-plugs-payment/tests/PaymentRecordControllerTest.php @@ -0,0 +1,511 @@ +configureAccountAccess([ + 'headimg' => 'https://example.com/payment-controller.png', + 'userPrefix' => '支付测试', + ]); + } + + protected function afterSchemaCreated(): void + { + $this->app->setAppPath(TEST_PROJECT_ROOT . '/plugin/think-plugs-payment/src/'); + $this->configureView([ + 'view_path' => TEST_PROJECT_ROOT . '/plugin/think-plugs-payment/src/view' . DIRECTORY_SEPARATOR, + ]); + } + + public function testAuditControllerApprovesVoucherAndPromotesWemallOrder(): void + { + $account = $this->createBoundAccountFixture(); + $this->registerWemallService(); + $order = $this->createWemallOrderFixture($account, [ + 'order_no' => 'PAY-AUDIT-PASS-001', + 'amount_real' => '15.00', + 'amount_total' => '15.00', + 'delivery_type' => 1, + ]); + + $response = Payment::mk(Payment::VOUCHER)->create( + $account, + $order->getAttr('order_no'), + '后台审核通过订单', + '15.00', + '15.00', + '后台审核通过', + '', + 'https://example.com/voucher-pass.png' + ); + + $record = PluginPaymentRecord::mk()->where(['code' => $response->record['code']])->findOrEmpty(); + $this->assertSame(3, intval($order->refresh()->getAttr('status'))); + $this->assertSame(1, intval($order->refresh()->getAttr('payment_status'))); + + $result = $this->callAuditController([ + 'id' => $record->getAttr('id'), + 'status' => 2, + 'remark' => '', + ]); + + $record = $record->refresh(); + $order = $order->refresh(); + + $this->assertSame(200, intval($result['code'] ?? 0)); + $this->assertSame('凭证审核通过!', $result['info'] ?? ''); + $this->assertSame(2, intval($record->getAttr('audit_status'))); + $this->assertSame(1, intval($record->getAttr('payment_status'))); + $this->assertSame(9001, intval($record->getAttr('audit_user'))); + $this->assertSame('后台支付凭证已通过', $record->getAttr('payment_remark')); + $this->assertNotEmpty($record->getAttr('payment_trade')); + $this->assertNotEmpty($record->getAttr('payment_time')); + $this->assertSame(4, intval($order->getAttr('status'))); + $this->assertSame(1, intval($order->getAttr('payment_status'))); + $this->assertSame('15.00', $this->decimal($order->getAttr('payment_amount'))); + } + + public function testAuditGetRendersBuilderForm(): void + { + $record = PluginPaymentRecord::mk(); + $record->save([ + 'unid' => 1, + 'usid' => 1, + 'code' => 'PAYAUDITHTML001', + 'order_no' => 'PAY-AUDIT-HTML-001', + 'order_name' => '后台审核表单', + 'order_amount' => '5.00', + 'channel_type' => Payment::VOUCHER, + 'channel_code' => Payment::VOUCHER, + 'payment_images' => 'https://example.com/payment-audit.png', + 'payment_status' => 0, + 'payment_amount' => '5.00', + 'used_payment' => '5.00', + 'audit_status' => 1, + ]); + + $html = $this->callRecordHtml('audit', ['id' => intval($record->getAttr('id'))], 'GET'); + + $this->assertStringContainsString('form-builder-schema', $html); + $this->assertStringContainsString('name="status"', $html); + $this->assertStringContainsString('data-tips-image', $html); + } + + public function testRecordIndexRendersEnglishTextsWhenLangSetIsEnUs(): void + { + $this->switchPaymentLang('en-us'); + + $html = $this->callActionHtml(PaymentRecordController::class, 'index'); + + $this->assertStringContainsString('Payment Activity Management', $html); + $this->assertStringContainsString('User Account', $html); + $this->assertStringContainsString('Order Content', $html); + $this->assertStringContainsString('Payment Description', $html); + $this->assertStringContainsString('Search', $html); + $this->assertStringContainsString('Export', $html); + $this->assertStringContainsString('Payment Behavior Data', $html); + $this->assertStringNotContainsString('支付行为管理', $html); + } + + public function testAuditGetRendersEnglishBuilderFormWhenLangSetIsEnUs(): void + { + $record = PluginPaymentRecord::mk(); + $record->save([ + 'unid' => 1, + 'usid' => 1, + 'code' => 'PAYAUDITHTMLEN001', + 'order_no' => 'PAY-AUDIT-HTML-EN-001', + 'order_name' => '英文审核表单', + 'order_amount' => '8.00', + 'channel_type' => Payment::VOUCHER, + 'channel_code' => Payment::VOUCHER, + 'payment_images' => 'https://example.com/payment-audit-en.png', + 'payment_status' => 0, + 'payment_amount' => '8.00', + 'used_payment' => '8.00', + 'audit_status' => 1, + ]); + + $this->switchPaymentLang('en-us'); + $html = $this->callRecordHtml('audit', ['id' => intval($record->getAttr('id'))], 'GET'); + + $this->assertStringContainsString('Business Order No.', $html); + $this->assertStringContainsString('Audit Action Type', $html); + $this->assertStringContainsString('Order Audit Remark', $html); + $this->assertStringContainsString('Payment Voucher', $html); + $this->assertStringNotContainsString('审核操作类型', $html); + } + + public function testAuditControllerRefusesVoucherAndReturnsWemallOrderToPayable(): void + { + $account = $this->createBoundAccountFixture(); + $this->registerWemallService(); + $order = $this->createWemallOrderFixture($account, [ + 'order_no' => 'PAY-AUDIT-REFUSE-001', + 'amount_real' => '9.00', + 'amount_total' => '9.00', + 'delivery_type' => 1, + ]); + + $response = Payment::mk(Payment::VOUCHER)->create( + $account, + $order->getAttr('order_no'), + '后台审核驳回订单', + '9.00', + '9.00', + '后台审核驳回', + '', + 'https://example.com/voucher-refuse.png' + ); + + $record = PluginPaymentRecord::mk()->where(['code' => $response->record['code']])->findOrEmpty(); + $this->assertSame(3, intval($order->refresh()->getAttr('status'))); + $this->assertSame(1, intval($order->refresh()->getAttr('payment_status'))); + + $result = $this->callAuditController([ + 'id' => $record->getAttr('id'), + 'status' => 0, + 'remark' => '凭证信息不足', + ]); + + $record = $record->refresh(); + $order = $order->refresh(); + + $this->assertSame(200, intval($result['code'] ?? 0)); + $this->assertSame('凭证审核驳回!', $result['info'] ?? ''); + $this->assertSame(0, intval($record->getAttr('audit_status'))); + $this->assertSame(0, intval($record->getAttr('payment_status'))); + $this->assertSame(9001, intval($record->getAttr('audit_user'))); + $this->assertSame('凭证信息不足', $record->getAttr('payment_remark')); + $this->assertSame('凭证信息不足', $record->getAttr('audit_remark')); + $this->assertSame(2, intval($order->getAttr('status'))); + $this->assertSame(1, intval($order->getAttr('payment_status'))); + $this->assertSame('0.00', $this->decimal($order->getAttr('payment_amount'))); + } + + public function testNotifyControllerReplaysSuccessEventForPaidRecord(): void + { + $account = $this->createBoundAccountFixture(); + $this->registerWemallService(); + $order = $this->createWemallOrderFixture($account, [ + 'order_no' => 'PAY-NOTIFY-OK-001', + 'status' => 2, + 'payment_status' => 0, + 'amount_real' => '11.00', + 'amount_total' => '11.00', + 'delivery_type' => 1, + ]); + + $record = PluginPaymentRecord::mk(); + $record->save([ + 'unid' => $account->getUnid(), + 'usid' => $account->getUsid(), + 'code' => 'PAYNOTIFYOK001', + 'order_no' => $order->getAttr('order_no'), + 'order_name' => '后台重放成功订单', + 'order_amount' => '11.00', + 'channel_type' => Payment::EMPTY, + 'channel_code' => Payment::EMPTY, + 'payment_trade' => 'EMT-NOTIFY-001', + 'payment_status' => 1, + 'payment_amount' => '11.00', + 'used_payment' => '11.00', + 'audit_status' => 2, + 'payment_time' => date('Y-m-d H:i:s'), + 'payment_remark' => '已完成支付待重放', + ]); + + $response = $this->callRecordController('notify', [ + 'code' => $record->getAttr('code'), + ], 'GET'); + + $order = $order->refresh(); + + $this->assertSame(200, intval($response['code'] ?? 0)); + $this->assertSame('重新触发支付行为!', $response['info'] ?? ''); + $this->assertSame(4, intval($order->getAttr('status'))); + $this->assertSame(1, intval($order->getAttr('payment_status'))); + $this->assertSame('11.00', $this->decimal($order->getAttr('payment_amount'))); + $this->assertSame('11.00', $this->decimal($order->getAttr('amount_payment'))); + } + + public function testNotifyControllerRejectsUnpaidRecord(): void + { + $record = PluginPaymentRecord::mk(); + $record->save([ + 'unid' => 1, + 'usid' => 1, + 'code' => 'PAYNOTIFYFAIL001', + 'order_no' => 'PAY-NOTIFY-FAIL-001', + 'order_name' => '后台重放未支付订单', + 'order_amount' => '6.00', + 'channel_type' => Payment::VOUCHER, + 'channel_code' => Payment::VOUCHER, + 'payment_status' => 0, + 'payment_amount' => '0.00', + 'used_payment' => '6.00', + 'audit_status' => 1, + ]); + + $response = $this->callRecordController('notify', [ + 'code' => $record->getAttr('code'), + ], 'GET'); + + $this->assertSame(500, intval($response['code'] ?? 1)); + $this->assertSame('未完成支付!', $response['info'] ?? ''); + } + + public function testCancelControllerRefundsCouponAdjustedAmountAndRefreshesOrder(): void + { + $account = $this->createBoundAccountFixture(); + $this->registerWemallService(); + $order = $this->createWemallOrderFixture($account, [ + 'order_no' => 'PAY-CANCEL-001', + 'amount_real' => '11.00', + 'amount_total' => '11.00', + 'delivery_type' => 1, + ]); + + $response = Payment::mk(Payment::EMPTY)->create( + $account, + $order->getAttr('order_no'), + '后台退款订单', + '11.00', + '11.00', + '后台退款前支付' + ); + + $record = PluginPaymentRecord::mk()->where(['code' => $response->record['code']])->findOrEmpty(); + $record->save(['payment_coupon' => '2.00']); + + $result = $this->callRecordController('cancel', [ + 'code' => $record->getAttr('code'), + ], 'GET'); + + $record = $record->refresh(); + $order = $order->refresh(); + $refund = PluginPaymentRefund::mk()->where(['record_code' => $record->getAttr('code')])->findOrEmpty(); + + $this->assertSame(200, intval($result['code'] ?? 0)); + $this->assertSame('退款申请成功!', $result['info'] ?? ''); + $this->assertTrue($refund->isExists()); + $this->assertSame('9.00', $this->decimal($refund->getAttr('refund_amount'))); + $this->assertSame('9.00', $this->decimal($refund->getAttr('used_payment'))); + $this->assertSame(Payment::EMPTY, $refund->getAttr('refund_account')); + $this->assertSame(1, intval($record->getAttr('refund_status'))); + $this->assertSame('9.00', $this->decimal($record->getAttr('refund_amount'))); + $this->assertSame('2.00', $this->decimal($order->getAttr('payment_amount'))); + $this->assertSame('2.00', $this->decimal($order->getAttr('amount_payment'))); + $this->assertSame(4, intval($order->getAttr('status'))); + $this->assertSame(1, intval($order->getAttr('payment_status'))); + } + + public function testRefundIndexRendersEnglishTextsWhenLangSetIsEnUs(): void + { + $user = $this->createAccountUser([ + 'username' => 'refund-english-user', + 'nickname' => '退款英文用户', + ]); + + $record = PluginPaymentRecord::mk(); + $record->save([ + 'unid' => intval($user->getAttr('id')), + 'usid' => 1, + 'code' => 'PAYREFUNDHTML001', + 'order_no' => 'PAY-REFUND-HTML-001', + 'order_name' => '退款页面订单', + 'order_amount' => '12.00', + 'channel_type' => Payment::VOUCHER, + 'channel_code' => Payment::VOUCHER, + 'payment_trade' => 'TRADE-REFUND-001', + 'payment_status' => 1, + 'payment_amount' => '12.00', + 'used_payment' => '12.00', + 'audit_status' => 2, + 'payment_time' => date('Y-m-d H:i:s'), + 'payment_remark' => '退款前已支付', + ]); + + $refund = PluginPaymentRefund::mk(); + $refund->save([ + 'unid' => intval($user->getAttr('id')), + 'record_code' => 'PAYREFUNDHTML001', + 'code' => 'RFD-HTML-001', + 'refund_account' => Payment::EMPTY, + 'refund_amount' => '12.00', + 'used_payment' => '12.00', + 'refund_remark' => '全额退款', + 'create_time' => date('Y-m-d H:i:s'), + 'update_time' => date('Y-m-d H:i:s'), + ]); + + $this->switchPaymentLang('en-us'); + $html = $this->callActionHtml(PaymentRefundController::class, 'index'); + + $this->assertStringContainsString('Payment Refund Management', $html); + $this->assertStringContainsString('Refund Content', $html); + $this->assertStringContainsString('Payment Description', $html); + $this->assertStringContainsString('Search', $html); + $this->assertStringContainsString('Refund Data', $html); + $this->assertStringNotContainsString('支付退款管理', $html); + } + + protected function defineSchema(): void + { + $this->createAccountTables(); + $this->createPaymentRecordTable(); + $this->createPaymentRefundTable(); + $this->createWemallOrderTable(); + } + + private function registerWemallService(): void + { + (new WemallService($this->app))->register(); + } + + private function callAuditController(array $post): array + { + return $this->callRecordController('audit', $post, 'POST'); + } + + private function callRecordHtml(string $action, array $data, string $method = 'GET'): string + { + RequestContext::clear(); + RequestContext::instance()->setAuth([ + 'id' => 9001, + 'username' => 'admin', + 'password' => 'test-admin-password', + ], '', true); + + $request = $this->app->request + ->withGet($data) + ->withPost($data) + ->setMethod($method) + ->setController('record') + ->setAction($action); + + $this->activateApplicationContext($request); + + try { + $controller = new PaymentRecordController($this->app); + $controller->{$action}(); + self::fail("Expected {$action} to throw HttpResponseException."); + } catch (HttpResponseException $exception) { + return $exception->getResponse()->getContent(); + } + } + + private function callRecordController(string $action, array $data, string $method = 'POST'): array + { + RequestContext::clear(); + RequestContext::instance()->setAuth([ + 'id' => 9001, + 'username' => 'admin', + 'password' => 'test-admin-password', + ], '', true); + + $request = $this->app->request + ->withGet($data) + ->withPost($data) + ->setMethod($method) + ->setController('record') + ->setAction($action); + + $this->activateApplicationContext($request); + + try { + $controller = new PaymentRecordController($this->app); + $controller->{$action}(); + self::fail("Expected {$action} to throw HttpResponseException."); + } catch (HttpResponseException $exception) { + return json_decode($exception->getResponse()->getContent(), true) ?: []; + } + } + + /** + * @param class-string $controllerClass + */ + private function callActionHtml(string $controllerClass, string $action, array $query = []): string + { + $parts = explode('\\', $controllerClass); + $request = (new Request()) + ->withGet($query) + ->setMethod('GET') + ->setController(strtolower(strval(end($parts)))) + ->setAction($action); + + $this->setRequestPayload($request, $query); + RequestContext::clear(); + RequestContext::instance()->setAuth([ + 'id' => 9001, + 'username' => 'admin', + 'password' => 'test-admin-password', + ], '', true); + $this->activateApplicationContext($request); + + try { + $controller = new $controllerClass($this->app); + $controller->{$action}(); + self::fail("Expected {$controllerClass}::{$action} to throw HttpResponseException."); + } catch (HttpResponseException $exception) { + return $exception->getResponse()->getContent(); + } + } + + private function setRequestPayload(Request $request, array $data): void + { + $property = new \ReflectionProperty(Request::class, 'request'); + $property->setAccessible(true); + $property->setValue($request, $data); + } + + private function switchPaymentLang(string $langSet): void + { + $this->app->lang->switchLangSet($langSet); + foreach ([ + TEST_PROJECT_ROOT . "/plugin/think-plugs-payment/src/lang/{$langSet}.php", + TEST_PROJECT_ROOT . "/plugin/think-plugs-account/src/lang/{$langSet}.php", + ] as $file) { + if (is_file($file)) { + $this->app->lang->load($file, $langSet); + } + } + } +} diff --git a/plugin/think-plugs-payment/tests/PaymentRecordIntegrationTest.php b/plugin/think-plugs-payment/tests/PaymentRecordIntegrationTest.php new file mode 100644 index 000000000..b7ca793a1 --- /dev/null +++ b/plugin/think-plugs-payment/tests/PaymentRecordIntegrationTest.php @@ -0,0 +1,245 @@ +configureAccountAccess([ + 'headimg' => 'https://example.com/payment-account.png', + 'userPrefix' => '支付账号', + ]); + } + + public function testEmptyPaymentCreatesPaidRecordAndSupportsRefundSummary(): void + { + $account = $this->createBoundAccountFixture(); + $response = Payment::mk(Payment::EMPTY)->create( + $account, + 'ORDER-EMPTY-001', + '空支付订单', + '10.00', + '10.00', + '无需第三方支付' + ); + + $record = PluginPaymentRecord::mk()->where(['code' => $response->record['code']])->findOrEmpty(); + $this->assertTrue($response->status); + $this->assertTrue($record->isExists()); + $this->assertSame(Payment::EMPTY, $record->getAttr('channel_type')); + $this->assertSame(1, intval($record->getAttr('payment_status'))); + $this->assertSame(2, intval($record->getAttr('audit_status'))); + $this->assertNotEmpty($record->getAttr('payment_trade')); + $this->assertSame('10.00', $this->decimal(Payment::paidAmount('ORDER-EMPTY-001'))); + $this->assertSame('10.00', Payment::leaveAmount('ORDER-EMPTY-001', '20.00')); + + $total = Payment::totalPaymentAmount('ORDER-EMPTY-001'); + $this->assertSame('10.00', $this->decimal($total['amount'])); + $this->assertSame('10.00', $this->decimal($total['payment'])); + $this->assertSame('0.00', $this->decimal($total['balance'])); + $this->assertSame('0.00', $this->decimal($total['integral'])); + + $result = Payment::mk(Payment::EMPTY)->refund($record->getAttr('code'), '4.00', '部分退款'); + $refund = PluginPaymentRefund::mk()->where(['record_code' => $record->getAttr('code')])->findOrEmpty(); + $refundTotal = Payment::totalRefundAmount($record->getAttr('code')); + + $this->assertSame(200, intval($result['code'] ?? 0)); + $this->assertSame('发起退款成功!', $result['info'] ?? ''); + $this->assertTrue($refund->isExists()); + $this->assertSame(Payment::EMPTY, $refund->getAttr('refund_account')); + $this->assertSame(1, intval($refund->getAttr('refund_status'))); + $this->assertSame('4.00', $this->decimal($refund->getAttr('refund_amount'))); + $this->assertSame('4.00', $this->decimal($refundTotal['amount'])); + $this->assertSame('4.00', $this->decimal($refundTotal['payment'])); + $this->assertSame('6.00', $this->decimal(Payment::paidAmount('ORDER-EMPTY-001', true))); + } + + public function testVoucherPaymentCreatesPendingAuditRecordAndBlocksDuplicatePendingOrder(): void + { + $account = $this->createBoundAccountFixture('web'); + $response = Payment::mk(Payment::VOUCHER)->create( + $account, + 'ORDER-VOUCHER-001', + '凭证支付订单', + '15.00', + '8.00', + '上传转账凭证', + '', + 'https://example.com/voucher.png' + ); + + $record = PluginPaymentRecord::mk()->where(['code' => $response->record['code']])->findOrEmpty(); + $this->assertTrue($response->status); + $this->assertTrue($record->isExists()); + $this->assertSame(Payment::VOUCHER, $record->getAttr('channel_type')); + $this->assertSame(1, intval($record->getAttr('audit_status'))); + $this->assertSame(0, intval($record->getAttr('payment_status'))); + $this->assertSame('8.00', $this->decimal($record->getAttr('payment_amount'))); + $this->assertSame('https://example.com/voucher.png', $record->getAttr('payment_images')); + $this->assertSame('0.00', $this->decimal(Payment::paidAmount('ORDER-VOUCHER-001'))); + + $this->expectException(Exception::class); + $this->expectExceptionMessage('凭证待审核'); + Payment::mk(Payment::VOUCHER)->create( + $account, + 'ORDER-VOUCHER-001', + '凭证支付订单', + '15.00', + '2.00', + '重复提交凭证', + '', + 'https://example.com/voucher-2.png' + ); + } + + public function testRefundAcceptsCustomCodeAndRejectsDuplicateCustomCode(): void + { + $first = $this->createPaidEmptyOrderFixture('ORDER-REFUND-CODE-1'); + $second = $this->createPaidEmptyOrderFixture('ORDER-REFUND-CODE-2'); + $customCode = 'R-CUSTOM-0001'; + + $result = Payment::mk(Payment::EMPTY)->refund($first->getAttr('code'), '2.00', '首次退款', $customCode); + $refund = PluginPaymentRefund::mk()->where(['code' => $customCode])->findOrEmpty(); + + $this->assertSame(200, intval($result['code'] ?? 0)); + $this->assertSame('发起退款成功!', $result['info'] ?? ''); + $this->assertSame($customCode, $result['data']['refund_code'] ?? ''); + $this->assertTrue($refund->isExists()); + $this->assertSame($first->getAttr('code'), $refund->getAttr('record_code')); + + $duplicateCode = $customCode; + $this->expectException(Exception::class); + $this->expectExceptionMessage('退款单已存在'); + Payment::mk(Payment::EMPTY)->refund($second->getAttr('code'), '1.00', '重复退款单号', $duplicateCode); + } + + public function testRefundRejectsOverflowAndDoesNotCreateExtraRefundRecord(): void + { + $record = $this->createPaidEmptyOrderFixture('ORDER-REFUND-OVERFLOW'); + Payment::mk(Payment::EMPTY)->refund($record->getAttr('code'), '8.00', '首次退款'); + + $this->assertSame(1, PluginPaymentRefund::mk()->where(['record_code' => $record->getAttr('code')])->count()); + + $this->expectException(Exception::class); + $this->expectExceptionMessage('退款金额溢出'); + try { + Payment::mk(Payment::EMPTY)->refund($record->getAttr('code'), '3.00', '超额退款'); + } finally { + $this->assertSame(1, PluginPaymentRefund::mk()->where(['record_code' => $record->getAttr('code')])->count()); + $this->assertSame('8.00', $this->decimal(Payment::totalRefundAmount($record->getAttr('code'))['amount'])); + } + } + + public function testEmptyPaymentReturnsEnglishMessagesWhenLangSetIsEnUs(): void + { + $account = $this->createBoundAccountFixture(); + $this->switchPaymentLang('en-us'); + + $response = Payment::mk(Payment::EMPTY)->create( + $account, + 'ORDER-EMPTY-EN-001', + 'English empty order', + '10.00', + '10.00', + 'No gateway required' + ); + + $result = Payment::mk(Payment::EMPTY)->refund($response->record['code'], '4.00', 'English refund'); + + $this->assertTrue($response->status); + $this->assertSame('No payment is required for this order', $response->message); + $this->assertSame(200, intval($result['code'] ?? 0)); + $this->assertSame('Refund requested successfully', $result['info'] ?? ''); + } + + public function testVoucherCreateReturnsEnglishPendingAuditMessageWhenLangSetIsEnUs(): void + { + $account = $this->createBoundAccountFixture('web'); + $this->switchPaymentLang('en-us'); + + Payment::mk(Payment::VOUCHER)->create( + $account, + 'ORDER-VOUCHER-EN-001', + 'Voucher payment order', + '15.00', + '8.00', + 'Upload voucher', + '', + 'https://example.com/voucher-en.png' + ); + + $this->expectException(Exception::class); + $this->expectExceptionMessage('Voucher pending review'); + Payment::mk(Payment::VOUCHER)->create( + $account, + 'ORDER-VOUCHER-EN-001', + 'Voucher payment order', + '15.00', + '2.00', + 'Duplicate voucher', + '', + 'https://example.com/voucher-en-2.png' + ); + } + + public function testRefundRejectsOverflowWithEnglishMessageWhenLangSetIsEnUs(): void + { + $record = $this->createPaidEmptyOrderFixture('ORDER-REFUND-OVERFLOW-EN'); + $this->switchPaymentLang('en-us'); + Payment::mk(Payment::EMPTY)->refund($record->getAttr('code'), '8.00', 'First refund'); + + $this->expectException(Exception::class); + $this->expectExceptionMessage('Refund amount exceeds the paid amount'); + Payment::mk(Payment::EMPTY)->refund($record->getAttr('code'), '3.00', 'Overflow refund'); + } + + protected function defineSchema(): void + { + $this->createAccountTables(); + $this->createPaymentRecordTable(); + $this->createPaymentRefundTable(); + } + + private function switchPaymentLang(string $langSet): void + { + $this->app->lang->switchLangSet($langSet); + foreach ([ + TEST_PROJECT_ROOT . "/plugin/think-plugs-payment/src/lang/{$langSet}.php", + TEST_PROJECT_ROOT . "/plugin/think-plugs-account/src/lang/{$langSet}.php", + ] as $file) { + if (is_file($file)) { + $this->app->lang->load($file, $langSet); + } + } + } +} diff --git a/plugin/think-plugs-payment/tests/PaymentRecountServiceTest.php b/plugin/think-plugs-payment/tests/PaymentRecountServiceTest.php new file mode 100644 index 000000000..9b9231871 --- /dev/null +++ b/plugin/think-plugs-payment/tests/PaymentRecountServiceTest.php @@ -0,0 +1,116 @@ +createAccountUser([ + 'username' => 'queue-user', + 'nickname' => '队列用户', + ]); + $this->switchPaymentLang('en-us'); + + $queue = new class implements QueueRuntimeInterface { + public array $messages = []; + + public string $successMessage = ''; + + public string $errorMessage = ''; + + public function getCode(): string + { + return 'payment-recount-test'; + } + + public function getTitle(): string + { + return 'payment recount'; + } + + public function getData(): array + { + return []; + } + + public function getRecord(): Model + { + return new class extends Model {}; + } + + public function progress(?int $status = null, ?string $message = null, ?string $progress = null, int $backline = 0): array + { + return ['status' => $status, 'message' => $message, 'progress' => $progress, 'backline' => $backline]; + } + + public function message(int $total, int $count, string $message = '', int $backline = 0): void + { + $this->messages[] = compact('total', 'count', 'message', 'backline'); + } + + public function success(string $message): void + { + $this->successMessage = $message; + } + + public function error(string $message): void + { + $this->errorMessage = $message; + } + }; + + $service = new Recount($this->app, $this->app->make(ProcessService::class)); + $service->initialize($queue)->execute(); + + $this->assertSame('', $queue->errorMessage); + $this->assertSame('Balance and integral refresh completed', $queue->successMessage); + $this->assertCount(2, $queue->messages); + $this->assertSame("Start refreshing user [{$user->getAttr('id')} queue-user] balance and integral", $queue->messages[0]['message']); + $this->assertSame("Refreshed user [{$user->getAttr('id')} queue-user] balance and integral", $queue->messages[1]['message']); + } + + protected function defineSchema(): void + { + $this->createAccountTables(); + $this->createPaymentBalanceTable(); + $this->createPaymentIntegralTable(); + } + + private function switchPaymentLang(string $langSet): void + { + $this->app->lang->switchLangSet($langSet); + $file = TEST_PROJECT_ROOT . "/plugin/think-plugs-payment/src/lang/{$langSet}.php"; + if (is_file($file)) { + $this->app->lang->load($file, $langSet); + } + } +} diff --git a/plugin/think-plugs-payment/tests/PaymentTest.php b/plugin/think-plugs-payment/tests/PaymentTest.php new file mode 100644 index 000000000..c6f8fa7e6 --- /dev/null +++ b/plugin/think-plugs-payment/tests/PaymentTest.php @@ -0,0 +1,44 @@ +assertNotEmpty($all); + } + + public function testGetTypesByChannel() + { + $all = Payment::typesByAccess(Account::WXAPP); + $this->assertNotEmpty($all); + } +} diff --git a/plugin/think-plugs-static/.github/workflows/release.yml b/plugin/think-plugs-static/.github/workflows/release.yml new file mode 100644 index 000000000..39d622da8 --- /dev/null +++ b/plugin/think-plugs-static/.github/workflows/release.yml @@ -0,0 +1,75 @@ +####### 可解析的提交前缀 ######## +# ci: 持续集成 +# fix: 修改 +# feat: 新增 +# refactor: 重构 +# docs: 文档 +# style: 样式 +# chore: 其他 +# build: 构建 +# pref: 优化 +# test: 测试 +############################### + +on: + push: + tags: + - 'v*' # 仅匹配 v* 版本标签,如 v1.0、v20.15.10 + +name: Create Release +permissions: write-all + +jobs: + release: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 18 + + - name: Install dependencies + run: npm install -g gen-git-log + + - name: Find Last Tag + id: last_tag + run: | + # 获取所有标签,按版本号降序排序 + all_tags=$(git tag --list --sort=-version:refname) + + # 获取最新的标签 + LATEST_TAG=$(echo "$all_tags" | head -n 1) + + # 获取倒数第二个标签(如果有) + SECOND_LATEST_TAG=$(echo "$all_tags" | sed -n '2p') + + # 如果没有任何标签,默认 v1.0.0 + LATEST_TAG=${LATEST_TAG:-v1.0.0} + SECOND_LATEST_TAG=${SECOND_LATEST_TAG:-v1.0.0} + + # 设置环境变量 + echo "LATEST_TAG=$LATEST_TAG" >> $GITHUB_ENV + echo "SECOND_LATEST_TAG=$SECOND_LATEST_TAG" >> $GITHUB_ENV + + - name: Generate Release Notes + run: | + rm -rf log + mkdir -p log + git-log -m tag -f -S $SECOND_LATEST_TAG -v ${LATEST_TAG#v} + + - name: Create Release + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ env.LATEST_TAG }} + release_name: Release ${{ env.LATEST_TAG }} + body_path: log/${{ env.LATEST_TAG }}.md + draft: false + prerelease: false \ No newline at end of file diff --git a/plugin/think-plugs-static/.gitignore b/plugin/think-plugs-static/.gitignore new file mode 100644 index 000000000..04987fdf1 --- /dev/null +++ b/plugin/think-plugs-static/.gitignore @@ -0,0 +1,17 @@ +.env +.git +.svn +.idea +.fleet +.vscode +.DS_Store +*.log +*.zip + +/vendor +/composer.lock +!composer.json + +/stc/public/static/theme/css/_*.css* +/stc/public/static/theme/css/node_modules +/stc/public/static/theme/css/package-lock.json diff --git a/plugin/think-plugs-static/.php-cs-fixer.php b/plugin/think-plugs-static/.php-cs-fixer.php new file mode 100644 index 000000000..2e44859af --- /dev/null +++ b/plugin/think-plugs-static/.php-cs-fixer.php @@ -0,0 +1,120 @@ +setRiskyAllowed(true)->setParallelConfig(new ParallelConfig(8, 24)); +$finder = Finder::create()->in(__DIR__)->exclude(['vendor', 'public', 'runtime']); +return $config->setFinder($finder)->setUsingCache(false)->setRules([ + '@PSR2' => true, + '@Symfony' => true, + '@DoctrineAnnotation' => true, + '@PhpCsFixer' => true, + 'header_comment' => [ + 'comment_type' => 'PHPDoc', + 'header' => $header, + 'separate' => 'none', + 'location' => 'after_declare_strict', + ], + 'array_syntax' => [ + 'syntax' => 'short', + ], + 'list_syntax' => [ + 'syntax' => 'short', + ], + 'blank_line_before_statement' => [ + 'statements' => [ + 'declare', + ], + ], + 'general_phpdoc_annotation_remove' => [ + 'annotations' => [ + 'author', + ], + ], + 'ordered_imports' => [ + 'imports_order' => [ + 'class', 'function', 'const', + ], + 'sort_algorithm' => 'alpha', + ], + 'single_line_comment_style' => [ + 'comment_types' => [ + ], + ], + 'yoda_style' => [ + 'always_move_variable' => false, + 'equal' => false, + 'identical' => false, + ], + 'phpdoc_align' => [ + 'align' => 'left', + ], + 'multiline_whitespace_before_semicolons' => [ + 'strategy' => 'no_multi_line', + ], + 'constant_case' => [ + 'case' => 'lower', + ], + 'encoding' => true, // PHP代码必须只使用没有BOM的UTF-8 + 'line_ending' => true, // 所有的PHP文件编码必须一致 + 'single_quote' => true, // 简单字符串应该使用单引号代替双引号 + 'no_empty_statement' => true, // 不应该存在空的结构体 + 'standardize_not_equals' => true, // 使用 <> 代替 != + 'blank_line_after_namespace' => true, // 命名空间之后空一行 + 'no_empty_phpdoc' => true, // 不应该存在空的 phpdoc + 'no_empty_comment' => true, // 不应该存在空注释 + 'no_singleline_whitespace_before_semicolons' => true, // 禁止在关闭分号前使用单行空格 + 'concat_space' => ['spacing' => 'one'], // 连接字符是否需要空格,可选配置项 none:不需要 one:一个空格 + 'no_leading_import_slash' => true, // use 语句中取消前置斜杠 + 'cast_spaces' => ['space' => 'none'], + 'class_attributes_separation' => true, + 'combine_consecutive_unsets' => true, + 'declare_strict_types' => true, + 'lowercase_static_reference' => true, + 'linebreak_after_opening_tag' => true, + 'multiline_comment_opening_closing' => true, + 'no_useless_else' => true, + 'no_unused_imports' => true, + 'not_operator_with_successor_space' => false, + 'not_operator_with_space' => false, + 'ordered_class_elements' => true, + 'php_unit_strict' => false, + 'phpdoc_separation' => false, +]); diff --git a/plugin/think-plugs-static/composer.json b/plugin/think-plugs-static/composer.json new file mode 100644 index 000000000..9ed639d71 --- /dev/null +++ b/plugin/think-plugs-static/composer.json @@ -0,0 +1,59 @@ +{ + "type": "think-admin-plugin", + "name": "zoujingli/think-plugs-static", + "version": "8.0.x-dev", + "license": "MIT", + "description": "Static Files for ThinkAdmin", + "authors": [ + { + "name": "Anyon", + "email": "zoujingli@qq.com" + } + ], + "require": { + "php": "^8.1" + }, + "extra": { + "think": { + "services": [ + "plugin\\static\\Service" + ] + }, + "xadmin": { + "app": { + "name": "静态资源", + "description": "提供系统前端静态资源文件,包括样式、脚本和第三方组件。", + "code": "static", + "prefix": "static" + }, + "publish": { + "copy": { + "stc/config": "config", + "stc/.env.example": ".env.example", + "stc/default/Index.php": "app/index/controller/Index.php", + "stc/public/router.php": "public/router.php", + "stc/public/robots.txt": "public/robots.txt", + "stc/public/.htaccess": "public/.htaccess", + "stc/public/static/extra/style.css": "public/static/extra/style.css", + "stc/public/static/extra/script.js": "public/static/extra/script.js", + "stc/think": "think", + "stc/public/index.php": "public/index.php", + "stc/public/static/plugs": "public/static/plugs", + "stc/public/static/theme": { + "to": "public/static/theme", + "exclude": [ + "*.less", + "*.css.map", + "package.json" + ] + }, + "stc/public/static/system.js": "public/static/system.js", + "stc/public/static/login.js": "public/static/login.js" + } + } + } + }, + "config": { + "sort-packages": true + } +} diff --git a/plugin/think-plugs-static/license b/plugin/think-plugs-static/license new file mode 100644 index 000000000..1bbacfb85 --- /dev/null +++ b/plugin/think-plugs-static/license @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2014~2025 Anyon + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/plugin/think-plugs-static/readme.api.md b/plugin/think-plugs-static/readme.api.md new file mode 100644 index 000000000..1fa1a6086 --- /dev/null +++ b/plugin/think-plugs-static/readme.api.md @@ -0,0 +1,33 @@ +# 静态资源接口 + +## 接口标准 +- 接口类型:发布命令 + 发布规则配置 +- 调用入口:`php think xadmin:publish` +- 规则来源:`composer.json > extra.xadmin.publish.copy` +- 返回形式:控制台发布结果 + 项目根目录静态资源产物 + +## 接口列表 + +### `xadmin:publish` +- 说明:把静态资源、项目骨架和入口文件发布到项目根目录 + +```jsonc +{ + "force": false, // 是否覆盖已存在的目标文件 + "migrate": false // 是否在发布后执行数据库迁移 +} +``` + +### `extra.xadmin.publish.copy` +- 说明:组件级发布规则,目录发布时支持排除模式 + +```jsonc +{ + "stc/public/static/theme": { + "to": "public/static/theme", // 发布目标目录 + "exclude": ["*.less", "*.css.map", "package.json"] // 不进入运行目录的源文件 + }, + "stc/public/static/system.js": "public/static/system.js", // 直接按 source: target 复制文件 + "stc/public/static/login.js": "public/static/login.js" // 登录页脚本发布目标 +} +``` diff --git a/plugin/think-plugs-static/readme.md b/plugin/think-plugs-static/readme.md new file mode 100644 index 000000000..548a73b93 --- /dev/null +++ b/plugin/think-plugs-static/readme.md @@ -0,0 +1,14 @@ +# 静态资源 + +## 定位 +- 组件编码:`static` +- 组件包名:`zoujingli/think-plugs-static` +- 主要职责:发布项目骨架、入口文件、公共静态资源与默认扩展资源 + +## 边界 +- 不提供业务 HTTP API,仅通过发布接口生效 +- 资源发布统一来自 `composer.json > extra.xadmin.publish.copy` +- `public/static/theme` 默认排除 `*.less`、`*.css.map`、`package.json` + +## 文档 +- 发布接口说明:`readme.api.md` diff --git a/plugin/think-plugs-static/stc/.env.example b/plugin/think-plugs-static/stc/.env.example new file mode 100644 index 000000000..ac3dbf5ca --- /dev/null +++ b/plugin/think-plugs-static/stc/.env.example @@ -0,0 +1,16 @@ +# 数据配置 +DB_TYPE=sqlite +DB_MYSQL_HOST=thinkadmin.top +DB_MYSQL_PORT=3306 +DB_MYSQL_PREFIX= +DB_MYSQL_USERNAME=root +DB_MYSQL_DATABASE=admin +DB_MYSQL_CHARSET=utf8mb4 +DB_MYSQL_PASSWORD= + +# 缓存配置 +CACHE_TYPE=file +CACHE_REDIS_HOST=127.0.0.1 +CACHE_REDIS_PORT=6379 +CACHE_REDIS_SELECT= +CACHE_REDIS_PASSWORD= diff --git a/plugin/think-plugs-static/stc/config/app.php b/plugin/think-plugs-static/stc/config/app.php new file mode 100644 index 000000000..a6d006e96 --- /dev/null +++ b/plugin/think-plugs-static/stc/config/app.php @@ -0,0 +1,94 @@ + '', + // 默认本地应用兼容回退(建议优先使用 route.default_app) + 'single_app' => 'index', + // 插件机制配置 + 'plugin' => [ + // 插件编码 => 前缀 或 前缀数组,配置后覆盖插件默认前缀 + 'bindings' => [], + // 插件 API 统一入口前缀,例如 /api/{plugin}/... + 'api_prefix' => 'api', + // 动态插件切换默认关闭,仅在显式开启时作为调试或兼容入口 + 'switch' => [ + 'enabled' => false, + 'query' => '_plugin', + 'header' => 'X-Plugin-App', + ], + ], + // 是否启用路由 + 'with_route' => true, + // 超级用户账号 + 'super_user' => 'admin', + // 默认时区 + 'default_timezone' => 'Asia/Shanghai', + // 表现层模式:view 仅渲染页面,api 仅返回 JSON,mixed 根据控制器命名空间、Token 请求头与 Accept 自动切换 + 'presentation' => [ + 'mode' => 'mixed', + 'api_header' => 'Authorization', + ], + // 后台 JWT 有效期(秒,0 表示不过期) + 'system_token_expire' => 604800, + // 后台 JWT 认证 Cookie 名称(Authorization 优先,其次读取此 Cookie) + 'system_token_cookie' => 'system_access_token', + // 上传令牌有效期(秒) + 'system_upload_token_expire' => 1800, + // Token 会话默认有效期(秒,0 表示不过期) + 'token_session_expire' => 7200, + // Token 会话读取时是否自动续期 + 'token_session_touch' => true, + // Token 会话惰性清理间隔(秒) + 'token_session_gc_interval' => 300, + // Token 会话指定缓存仓库,留空使用默认仓库 + 'token_session_store' => '', + // 认证 Cookie 中的 Token 是否加密存储(Header 中仍使用原始 Bearer Token) + 'token_cookie_encrypt' => true, + // 认证 Cookie Token 加密密钥,留空默认复用 jwtkey + 'token_cookie_secret' => '', + // 终端账号 JWT 认证 Cookie 名称(Authorization 优先,其次读取此 Cookie) + 'account_token_cookie' => 'account_access_token', + // 存储管理器实现类(由 System 插件提供默认实现) + 'storage_manager_class' => \plugin\system\storage\StorageManager::class, + // CORS 启用状态(默认开启跨域) + 'cors_on' => true, + // CORS 配置跨域域名(仅需填域名,留空则自动域名) + 'cors_host' => [], + // CORS 授权请求方法 + 'cors_methods' => 'GET,PUT,POST,PATCH,DELETE', + // CORS 是否允许携带 Cookie 等凭证 + 'cors_credentials' => false, + // CORS 跨域头部字段 + 'cors_headers' => 'X-Device-Code,X-Device-Type', + // X-Frame-Options 配置 + 'cors_frame' => 'sameorigin', + // RBAC 登录页面(填写登录地址) + 'rbac_login' => '', + // RBAC 忽略应用(填写应用名称) + 'rbac_ignore' => ['index'], + // 显示错误消息内容,仅生产模式有效 + 'error_message' => '页面错误!请稍后再试~', + // 异常状态模板配置,仅生产模式有效 + 'http_exception_template' => [ + 404 => syspath('public/static/theme/err/404.html'), + 500 => syspath('public/static/theme/err/500.html'), + ], +]; diff --git a/plugin/think-plugs-static/stc/config/cache.php b/plugin/think-plugs-static/stc/config/cache.php new file mode 100644 index 000000000..352eb9bd0 --- /dev/null +++ b/plugin/think-plugs-static/stc/config/cache.php @@ -0,0 +1,62 @@ + env('CACHE_TYPE', 'file'), + // 缓存连接配置 + 'stores' => [ + 'file' => [ + // 驱动方式 + 'type' => 'File', + // 缓存保存目录 + 'path' => '', + // 缓存名称前缀 + 'prefix' => '', + // 缓存有效期 0 表示永久缓存 + 'expire' => 0, + // 缓存标签前缀 + 'tag_prefix' => 'tag:', + // 序列化机制 + 'serialize' => [], + ], + 'safe' => [ + // 驱动方式 + 'type' => 'File', + // 缓存保存目录 + 'path' => runpath('safefile/cache/'), + // 缓存名称前缀 + 'prefix' => '', + // 缓存有效期 0 表示永久缓存 + 'expire' => 0, + // 缓存标签前缀 + 'tag_prefix' => 'tag:', + // 序列化机制 + 'serialize' => [], + ], + 'redis' => [ + // 驱动方式 + 'type' => 'redis', + 'host' => env('CACHE_REDIS_HOST', '127.0.0.1'), + 'port' => env('CACHE_REDIS_PORT', 6379), + 'select' => env('CACHE_REDIS_SELECT', 0), + 'password' => env('CACHE_REDIS_PASSWORD', ''), + ], + ], +]; diff --git a/plugin/think-plugs-static/stc/config/cookie.php b/plugin/think-plugs-static/stc/config/cookie.php new file mode 100644 index 000000000..139513dd9 --- /dev/null +++ b/plugin/think-plugs-static/stc/config/cookie.php @@ -0,0 +1,35 @@ + 0, + // cookie 保存路径 + 'path' => '/', + // cookie 有效域名 + 'domain' => '', + // httponly 访问设置 + 'httponly' => true, + // 是否允许服务端写回 Cookie(默认关闭,开启后可回写认证与语言 Cookie) + 'setcookie' => false, + // cookie 安全传输,只支持 https 协议 + 'secure' => request()->isSsl(), + // samesite 安全设置,支持 'strict' 'lax' 'none' + 'samesite' => request()->isSsl() ? 'none' : 'lax', +]; diff --git a/plugin/think-plugs-static/stc/config/database.php b/plugin/think-plugs-static/stc/config/database.php new file mode 100644 index 000000000..29934877e --- /dev/null +++ b/plugin/think-plugs-static/stc/config/database.php @@ -0,0 +1,86 @@ + env('DB_TYPE', 'sqlite'), + // 自定义时间查询规则 + 'time_query_rule' => [], + // 自动写入时间戳字段 + 'auto_timestamp' => true, + // 时间字段取出后的默认时间格式 + 'datetime_format' => 'Y-m-d H:i:s', + // 数据库连接配置信息 + 'connections' => [ + 'mysql' => [ + // 数据库类型 + 'type' => 'mysql', + // 服务器地址 + 'hostname' => env('DB_MYSQL_HOST', '127.0.0.1'), + // 服务器端口 + 'hostport' => env('DB_MYSQL_PORT', '3306'), + // 数据库名 + 'database' => env('DB_MYSQL_DATABASE', 'thinkadmin'), + // 用户名 + 'username' => env('DB_MYSQL_USERNAME', 'root'), + // 密码 + 'password' => env('DB_MYSQL_PASSWORD', ''), + // 数据库连接参数 + 'params' => [], + // 数据库表前缀 + 'prefix' => env('DB_MYSQL_PREFIX', ''), + // 数据库编码默认采用 utf8mb4 + 'charset' => env('DB_MYSQL_CHARSET', 'utf8mb4'), + // 数据库部署方式:0 集中式(单一服务器),1 分布式(主从服务器) + 'deploy' => 0, + // 数据库读写是否分离 主从式有效 + 'rw_separate' => false, + // 读写分离后 主服务器数量 + 'master_num' => 1, + // 指定从服务器序号 + 'slave_no' => '', + // 检查字段是否存在 + 'fields_strict' => true, + // 是否需要断线重连 + 'break_reconnect' => false, + // 监听SQL执行日志 + 'trigger_sql' => true, + // 开启字段类型缓存 + 'fields_cache' => isOnline(), + ], + 'sqlite' => [ + // 数据库类型 + 'type' => 'sqlite', + // 数据库文件 + 'database' => runpath('database/sqlite.db'), + // 数据库编码默认采用 utf8 + 'charset' => 'utf8', + // 监听执行日志 + 'trigger_sql' => true, + // 其他参数字段 + 'deploy' => 0, + 'suffix' => '', + 'prefix' => '', + 'hostname' => '', + 'hostport' => '', + 'username' => '', + 'password' => '', + ], + ], +]; diff --git a/plugin/think-plugs-static/stc/config/lang.php b/plugin/think-plugs-static/stc/config/lang.php new file mode 100644 index 000000000..d4f633239 --- /dev/null +++ b/plugin/think-plugs-static/stc/config/lang.php @@ -0,0 +1,42 @@ + 'zh-cn', + // 允许的语言列表 + 'allow_lang_list' => ['zh-cn'], + // 转义为对应语言包名称 + 'accept_language' => [ + 'en' => 'en-us', + 'zh-hans-cn' => 'zh-cn', + ], + // 多语言自动侦测变量名 + 'detect_var' => 'lang', + // 多语言 Cookie 变量(禁用 Cookie 持久化) + 'cookie_var' => '__lang_disabled__', + // 多语言 Header 变量 + 'header_var' => 'lang', + // 使用 Cookie 记录 + 'use_cookie' => false, + // 是否支持语言分组 + 'allow_group' => false, + // 扩展语言包 + 'extend_list' => [], +]; diff --git a/plugin/think-plugs-static/stc/config/log.php b/plugin/think-plugs-static/stc/config/log.php new file mode 100644 index 000000000..1b58bf4e0 --- /dev/null +++ b/plugin/think-plugs-static/stc/config/log.php @@ -0,0 +1,60 @@ + 'file', + // 日志记录级别 + 'level' => [], + // 日志类型记录的通道 + 'type_channel' => [], + // 关闭全局日志写入 + 'close' => false, + // 全局日志处理 支持闭包 + 'processor' => null, + // 日志通道列表 + 'channels' => [ + 'file' => [ + // 日志记录方式 + 'type' => 'File', + // 日志保存目录 + 'path' => '', + // 单文件日志写入 + 'single' => true, + // 独立日志级别 + 'apart_level' => true, + // 每个文件大小 ( 10兆 ) + 'file_size' => 10485760, + // 日志日期格式 + 'time_format' => 'Y-m-d H:i:s', + // 最大日志文件数量 + 'max_files' => 100, + // 使用JSON格式记录 + 'json' => false, + // 日志处理 + 'processor' => null, + // 关闭通道日志写入 + 'close' => false, + // 日志输出格式化 + 'format' => '[%s][%s] %s', + // 是否实时写入 + 'realtime_write' => false, + ], + ], +]; diff --git a/plugin/think-plugs-static/stc/config/phinx.php b/plugin/think-plugs-static/stc/config/phinx.php new file mode 100644 index 000000000..18168f8d9 --- /dev/null +++ b/plugin/think-plugs-static/stc/config/phinx.php @@ -0,0 +1,27 @@ + [], + // 创建数据表,填写表名 + 'tables' => [], + // 备份数据表,填写表名 + 'backup' => [], +]; diff --git a/plugin/think-plugs-static/stc/config/route.php b/plugin/think-plugs-static/stc/config/route.php new file mode 100644 index 000000000..670845e63 --- /dev/null +++ b/plugin/think-plugs-static/stc/config/route.php @@ -0,0 +1,61 @@ + '/', + // URL伪静态后缀 + 'url_html_suffix' => 'html', + // URL普通方式参数 用于自动生成 + 'url_common_param' => true, + // 是否开启路由延迟解析 + 'url_lazy_route' => false, + // 是否强制使用路由 + 'url_route_must' => false, + // 合并路由规则 + 'route_rule_merge' => true, + // 路由是否完全匹配 + 'route_complete_match' => true, + // 访问控制器层名称 + 'controller_layer' => 'controller', + // 空控制器名 + 'empty_controller' => 'Error', + // 是否使用控制器后缀 + 'controller_suffix' => false, + // 默认的路由变量规则 + 'default_route_pattern' => '[\w\.]+', + // 是否开启请求缓存 true 自动缓存 支持设置请求缓存规则 + 'request_cache' => false, + // 请求缓存有效期 + 'request_cache_expire' => null, + // 全局请求缓存排除规则 + 'request_cache_except' => [], + // 默认本地应用 + 'default_app' => 'index', + // 默认控制器名 + 'default_controller' => 'Index', + // 默认操作名 + 'default_action' => 'index', + // 操作方法后缀 + 'action_suffix' => '', + // 默认JSONP格式返回的处理方法 + 'default_jsonp_handler' => 'jsonpReturn', + // 默认JSONP处理方法 + 'var_jsonp_handler' => 'callback', +]; diff --git a/plugin/think-plugs-static/stc/config/view.php b/plugin/think-plugs-static/stc/config/view.php new file mode 100644 index 000000000..78c9ea7eb --- /dev/null +++ b/plugin/think-plugs-static/stc/config/view.php @@ -0,0 +1,45 @@ + 'Think', + // 默认模板渲染规则 1.解析为小写+下划线 2.全部转换小写 3.保持操作方法 + 'auto_rule' => 1, + // 模板目录名 + 'view_dir_name' => 'view', + // 模板文件后缀 + 'view_suffix' => 'html', + // 模板文件名分隔符 + 'view_depr' => DIRECTORY_SEPARATOR, + // 模板缓存配置 + 'tpl_cache' => isOnline(), + // 模板引擎标签开始标记 + 'tpl_begin' => '{', + // 模板引擎标签结束标记 + 'tpl_end' => '}', + // 标签库标签开始标记 + 'taglib_begin' => '{', + // 标签库标签结束标记 + 'taglib_end' => '}', + // 去除HTML空格换行 + 'strip_space' => true, + // 标签默认过滤输出方法 + 'default_filter' => 'htmlentities=###,ENT_QUOTES', +]; diff --git a/plugin/think-plugs-static/stc/default/Index.php b/plugin/think-plugs-static/stc/default/Index.php new file mode 100644 index 000000000..265d7bee0 --- /dev/null +++ b/plugin/think-plugs-static/stc/default/Index.php @@ -0,0 +1,43 @@ +redirect(sysuri('system/login/index')); + } +} diff --git a/plugin/think-plugs-static/stc/public/.htaccess b/plugin/think-plugs-static/stc/public/.htaccess new file mode 100644 index 000000000..75d9a79c1 --- /dev/null +++ b/plugin/think-plugs-static/stc/public/.htaccess @@ -0,0 +1,8 @@ + + Options +FollowSymlinks -Multiviews + RewriteEngine On + + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_FILENAME} !-f + RewriteRule ^(.*)$ index.php/$1 [QSA,PT,L] + \ No newline at end of file diff --git a/plugin/think-plugs-static/stc/public/index.php b/plugin/think-plugs-static/stc/public/index.php new file mode 100644 index 000000000..5ad1fb198 --- /dev/null +++ b/plugin/think-plugs-static/stc/public/index.php @@ -0,0 +1,26 @@ + 0 ? loginI18n[key] : fallback; + } + + /*! 登录界面背景切换 */ + $('[data-bg-transition]').each(function (i, el) { + el.idx = 0, el.imgs = [], el.SetBackImage = function (css) { + window.setTimeout(function () { + $(el).removeClass(el.imgs.join(' ')).addClass(css) + }, 1000) && $body.removeClass(el.imgs.join(' ')).addClass(css) + }, el.lazy = window.setInterval(function () { + el.imgs.length > 0 && el.SetBackImage(el.imgs[++el.idx] || el.imgs[el.idx = 0]); + }, 5000) && el.dataset.bgTransition.split(',').forEach(function (image) { + layui.img(image, function (img, cssid, style) { + style = document.createElement('style'), cssid = 'LoginBackImage' + (el.imgs.length + 1); + style.innerHTML = '.' + cssid + '{background-image:url("' + encodeURI(image) + '")!important}'; + document.head.appendChild(style) && el.imgs.push(cssid); + }); + }); + }); + + let ambientFrame = 0, ambientPoint = { + x: Math.round(window.innerWidth * 0.78), + y: Math.round(window.innerHeight * 0.22) + }; + + function paintAmbient() { + ambientFrame = 0; + $('.login-container').css({ + '--cursor-x': ambientPoint.x + 'px', + '--cursor-y': ambientPoint.y + 'px' + }); + } + + function queueAmbient(x, y) { + ambientPoint.x = x; + ambientPoint.y = y; + if (ambientFrame) return; + ambientFrame = (window.requestAnimationFrame || window.setTimeout)(paintAmbient, 16); + } + + queueAmbient(ambientPoint.x, ambientPoint.y); + $(document).on('mousemove touchmove', function (event) { + let point = event.touches && event.touches[0] ? event.touches[0] : event; + queueAmbient(point.clientX, point.clientY); + }); + $(window).on('resize', function () { + queueAmbient(Math.round(window.innerWidth * 0.78), Math.round(window.innerHeight * 0.22)); + }); + + function decodeBase64ToArrayBuffer(value) { + let binary = atob(value), bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i); + return bytes.buffer; + } + + function encodeArrayBufferToBase64(buffer) { + let bytes = new Uint8Array(buffer), binary = ''; + for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]); + return btoa(binary); + } + + async function encryptPassword(form, password) { + let publicKey = form.dataset.loginPasswordKey || ''; + if (!publicKey || !window.crypto || !window.crypto.subtle || typeof TextEncoder === 'undefined' || window.isSecureContext === false) { + return {mode: 'plain', value: password}; + } + try { + // PHP openssl_private_decrypt with OAEP padding only interoperates with the SHA-1 variant here. + let key = await window.crypto.subtle.importKey( + 'spki', + decodeBase64ToArrayBuffer(publicKey), + {name: 'RSA-OAEP', hash: 'SHA-1'}, + false, + ['encrypt'] + ); + let result = await window.crypto.subtle.encrypt({name: 'RSA-OAEP'}, key, new TextEncoder().encode(password)); + return {mode: 'rsa', value: encodeArrayBufferToBase64(result)}; + } catch (e) { + return {mode: 'plain', value: password}; + } + } + + function reloadLoginPage() { + try { + let url = new URL(location.href); + url.hash = ''; + url.searchParams.set('_login_reload', String(Date.now())); + location.replace(url.toString()); + } catch (e) { + location.href = location.pathname + '?_login_reload=' + Date.now(); + } + } + + function createLoginSlider(form) { + let $form = $(form), $container = $form.closest('.login-container'); + let $popup = $container.find('[data-login-slider-popup]'); + let $panel = $popup.find('[data-login-slider-panel]'); + if ($panel.length < 1) { + return { + syncError: $.noop, + ensureVerified: function () { + return true; + } + }; + } + + let $bg = $panel.find('[data-slider-bg]'); + let $piece = $panel.find('[data-slider-piece]'); + let $stage = $panel.find('.slider-stage'); + let $track = $panel.find('[data-slider-track]'); + let $message = $panel.find('[data-slider-message]'); + let $status = $panel.find('[data-slider-status]'); + let $handle = $panel.find('[data-slider-handle]'); + let $refresh = $panel.find('[data-slider-refresh]'); + let $uniqid = $form.find('[name="uniqid"]'); + let $verify = $form.find('[name="verify"]'); + let $mode = $form.find('[name="password_mode"]'); + let request = form.dataset.loginSlider || ''; + let check = form.dataset.loginCheck || ''; + let state = { + bgWidth: 0, + currentLeft: 0, + dragging: false, + loaded: false, + maxLeft: 0, + originX: 0, + pieceWidth: 100, + required: false, + sourceWidth: 600, + startLeft: 0, + verified: false, + working: false, + }; + + function setStatus(text, type) { + $panel.removeClass('is-error is-success'); + type && $panel.addClass(type); + $message.text(text); + $status.text(text); + } + + function setPosition(left) { + state.currentLeft = Math.max(0, Math.min(left, state.maxLeft)); + $handle.css('left', state.currentLeft + 'px'); + $track.css('width', (state.currentLeft + $handle.outerWidth()) + 'px'); + $piece.css('left', state.currentLeft + 'px'); + } + + function recalculate() { + state.bgWidth = $stage.innerWidth(); + state.maxLeft = Math.max(state.bgWidth - $handle.outerWidth(), 0); + $piece.css('width', (state.pieceWidth / state.sourceWidth * 100) + '%'); + setPosition(state.currentLeft); + } + + function resetChallenge() { + state.verified = false; + state.working = false; + $uniqid.val(''); + $verify.val(''); + $panel.removeClass('is-error is-success'); + setPosition(0); + setStatus(t('dragToVerify', '请按住滑块,拖动完成验证')); + } + + function hideChallenge() { + $popup.removeClass('is-visible'); + $body.removeClass('login-verify-active'); + window.setTimeout(function () { + if (!$popup.hasClass('is-visible')) { + $popup.addClass('layui-hide'); + } + }, 220); + } + + function loadChallenge() { + if (request.length < 5) return $.msg.tips(t('sliderApiMissing', '请设置滑块验证接口')); + resetChallenge(); + let handleChallenge = function (ret) { + if (parseInt(ret.code, 10) !== 200) { + ret.data && ret.data.reload && reloadLoginPage(); + return false; + } + state.sourceWidth = parseInt(ret.data.width || 600); + state.pieceWidth = parseInt(ret.data.piece_width || 100); + state.loaded = true; + $uniqid.val(ret.data.uniqid || ''); + $bg.attr('src', ret.data.bgimg || ''); + $piece.attr('src', ret.data.water || ''); + (window.requestAnimationFrame || window.setTimeout)(recalculate, 0); + setStatus(t('dragToVerify', '请按住滑块,拖动完成验证')); + return false; + }; + handleChallenge.allowHttpError = true; + $.form.load(request, {token: form.dataset.loginToken || ''}, 'post', handleChallenge, false); + } + + function showChallenge(refresh) { + state.required = true; + $popup.removeClass('layui-hide'); + $body.addClass('login-verify-active'); + (window.requestAnimationFrame || window.setTimeout)(function () { + $popup.addClass('is-visible'); + recalculate(); + }, 0); + if (refresh || !$uniqid.val()) loadChallenge(); + } + + function verifyCurrentPosition() { + if (state.working || !$uniqid.val() || check.length < 5) return; + state.working = true; + setStatus(t('verifying', '正在校验...')); + $.form.load(check, { + uniqid: $uniqid.val(), + verify: Math.round(state.currentLeft * state.sourceWidth / Math.max(state.bgWidth, 1)) + }, 'post', function (ret) { + state.working = false; + let value = Math.round(state.currentLeft * state.sourceWidth / Math.max(state.bgWidth, 1)); + let result = parseInt(ret.data && ret.data.state || -1); + if (result === 1) { + state.verified = true; + state.required = false; + $verify.val(String(value)); + $panel.removeClass('is-error').addClass('is-success'); + $message.text(t('verifyPassedContinue', '验证通过,请继续登录')); + $status.text(t('sliderVerified', '滑块验证通过')); + window.setTimeout(hideChallenge, 260); + } else if (result === 0) { + state.verified = false; + state.required = true; + $verify.val(''); + $panel.removeClass('is-success').addClass('is-error'); + $message.text(t('wrongPositionRetry', '位置不正确,请重试')); + $status.text(t('wrongPositionRetry', '位置不正确,请重试')); + window.setTimeout(function () { + if (!state.verified) { + $panel.removeClass('is-error'); + setPosition(0); + setStatus(t('dragToVerify', '请按住滑块,拖动完成验证')); + } + }, 500); + } else { + loadChallenge(); + } + return false; + }, false); + } + + function getPoint(event) { + return event.touches && event.touches[0] ? event.touches[0] : event; + } + + function startDrag(event) { + if (state.working || state.verified || !$uniqid.val()) return; + let point = getPoint(event); + state.dragging = true; + state.originX = point.clientX; + state.startLeft = state.currentLeft; + $handle.addClass('is-active'); + event.preventDefault(); + } + + function moveDrag(event) { + if (!state.dragging) return; + let point = getPoint(event); + setPosition(state.startLeft + point.clientX - state.originX); + event.preventDefault(); + } + + function endDrag() { + if (!state.dragging) return; + state.dragging = false; + $handle.removeClass('is-active'); + verifyCurrentPosition(); + } + + $bg.on('load', recalculate); + $(window).on('resize', recalculate); + $handle.on('mousedown touchstart', startDrag); + $(document).on('mousemove touchmove', moveDrag); + $(document).on('mouseup touchend touchcancel', endDrag); + $refresh.on('click', function () { + showChallenge(true); + }); + + return { + syncError: function (data) { + if (data && data.need_verify) { + state.verified = false; + state.required = true; + showChallenge(!!data.refresh_verify); + } + }, + ensureVerified: function () { + if (state.required && !state.verified) { + showChallenge(false); + $.msg.tips(t('needVerifyFirst', '请先完成滑块验证')); + return false; + } + $mode.val('plain'); + return true; + }, + setPasswordMode: function (mode) { + $mode.val(mode); + } + }; + } + + /*! 后台登录提交处理 */ + $body.find('form[data-login-form]').each(function (idx, form) { + let slider = createLoginSlider(form); + $(form).vali(function (data) { + if (!slider.ensureVerified()) return false; + encryptPassword(form, data.password || '').then(function (cipher) { + let payload = $.extend({}, data, {password: cipher.value, password_mode: cipher.mode}); + slider.setPasswordMode(cipher.mode); + let handleSubmit = function (ret) { + if (parseInt(ret.code, 10) !== 200) { + if (ret.data && ret.data.reload) { + reloadLoginPage(); + return false; + } + slider.syncError(ret.data || {}); + return false; + } + }; + handleSubmit.allowHttpError = true; + $.form.load(location.href, payload, "post", handleSubmit, null, null, 'false'); + }); + }); + }); + +}); diff --git a/plugin/think-plugs-static/stc/public/static/plugs/angular/angular.min.js b/plugin/think-plugs-static/stc/public/static/plugs/angular/angular.min.js new file mode 100644 index 000000000..4bcfc0816 --- /dev/null +++ b/plugin/think-plugs-static/stc/public/static/plugs/angular/angular.min.js @@ -0,0 +1,351 @@ +/* + AngularJS v1.8.3 + (c) 2010-2020 Google LLC. http://angularjs.org + License: MIT +*/ +(function(z){'use strict';function ve(a){if(D(a))w(a.objectMaxDepth)&&(Xb.objectMaxDepth=Yb(a.objectMaxDepth)?a.objectMaxDepth:NaN),w(a.urlErrorParamsEnabled)&&Ga(a.urlErrorParamsEnabled)&&(Xb.urlErrorParamsEnabled=a.urlErrorParamsEnabled);else return Xb}function Yb(a){return X(a)&&0c)return"...";var d=b.$$hashKey,f;if(H(a)){f=0;for(var g=a.length;f").append(a).html();try{return a[0].nodeType===Pa?K(b):b.match(/^(<[^>]+>)/)[1].replace(/^<([\w-]+)/,function(a,b){return"<"+K(b)})}catch(d){return K(b)}}function Vc(a){try{return decodeURIComponent(a)}catch(b){}}function hc(a){var b={};r((a||"").split("&"), +function(a){var c,e,f;a&&(e=a=a.replace(/\+/g,"%20"),c=a.indexOf("="),-1!==c&&(e=a.substring(0,c),f=a.substring(c+1)),e=Vc(e),w(e)&&(f=w(f)?Vc(f):!0,ta.call(b,e)?H(b[e])?b[e].push(f):b[e]=[b[e],f]:b[e]=f))});return b}function Ce(a){var b=[];r(a,function(a,c){H(a)?r(a,function(a){b.push(ba(c,!0)+(!0===a?"":"="+ba(a,!0)))}):b.push(ba(c,!0)+(!0===a?"":"="+ba(a,!0)))});return b.length?b.join("&"):""}function ic(a){return ba(a,!0).replace(/%26/gi,"&").replace(/%3D/gi,"=").replace(/%2B/gi,"+")}function ba(a, +b){return encodeURIComponent(a).replace(/%40/gi,"@").replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",").replace(/%3B/gi,";").replace(/%20/g,b?"%20":"+")}function De(a,b){var d,c,e=Qa.length;for(c=0;c protocol indicates an extension, document.location.href does not match."))}function Wc(a,b,d){D(d)||(d={});d=S({strictDi:!1},d);var c=function(){a=x(a);if(a.injector()){var c=a[0]===z.document?"document":Aa(a);throw oa("btstrpd",c.replace(//,">"));}b=b||[];b.unshift(["$provide",function(b){b.value("$rootElement",a)}]);d.debugInfoEnabled&&b.push(["$compileProvider", +function(a){a.debugInfoEnabled(!0)}]);b.unshift("ng");c=fb(b,d.strictDi);c.invoke(["$rootScope","$rootElement","$compile","$injector",function(a,b,c,d){a.$apply(function(){b.data("$injector",d);c(b)(a)})}]);return c},e=/^NG_ENABLE_DEBUG_INFO!/,f=/^NG_DEFER_BOOTSTRAP!/;z&&e.test(z.name)&&(d.debugInfoEnabled=!0,z.name=z.name.replace(e,""));if(z&&!f.test(z.name))return c();z.name=z.name.replace(f,"");ca.resumeBootstrap=function(a){r(a,function(a){b.push(a)});return c()};B(ca.resumeDeferredBootstrap)&& +ca.resumeDeferredBootstrap()}function Ge(){z.name="NG_ENABLE_DEBUG_INFO!"+z.name;z.location.reload()}function He(a){a=ca.element(a).injector();if(!a)throw oa("test");return a.get("$$testability")}function Xc(a,b){b=b||"_";return a.replace(Ie,function(a,c){return(c?b:"")+a.toLowerCase()})}function Je(){var a;if(!Yc){var b=rb();(sb=A(b)?z.jQuery:b?z[b]:void 0)&&sb.fn.on?(x=sb,S(sb.fn,{scope:Wa.scope,isolateScope:Wa.isolateScope,controller:Wa.controller,injector:Wa.injector,inheritedData:Wa.inheritedData})): +x=U;a=x.cleanData;x.cleanData=function(b){for(var c,e=0,f;null!=(f=b[e]);e++)(c=(x._data(f)||{}).events)&&c.$destroy&&x(f).triggerHandler("$destroy");a(b)};ca.element=x;Yc=!0}}function Ke(){U.legacyXHTMLReplacement=!0}function gb(a,b,d){if(!a)throw oa("areq",b||"?",d||"required");return a}function tb(a,b,d){d&&H(a)&&(a=a[a.length-1]);gb(B(a),b,"not a function, got "+(a&&"object"===typeof a?a.constructor.name||"Object":typeof a));return a}function Ja(a,b){if("hasOwnProperty"===a)throw oa("badname", +b);}function Le(a,b,d){if(!b)return a;b=b.split(".");for(var c,e=a,f=b.length,g=0;g"):a;if(10>wa)for(c=hb[c]||hb._default,d.innerHTML=c[1]+e+c[2],k=c[0];k--;)d=d.firstChild;else{c=qa[c]||[];for(k=c.length;-1<--k;)d.appendChild(z.document.createElement(c[k])),d=d.firstChild;d.innerHTML=e}g=db(g,d.childNodes);d=f.firstChild;d.textContent=""}else g.push(b.createTextNode(a)); +f.textContent="";f.innerHTML="";r(g,function(a){f.appendChild(a)});return f}function U(a){if(a instanceof U)return a;var b;C(a)&&(a=V(a),b=!0);if(!(this instanceof U)){if(b&&"<"!==a.charAt(0))throw oc("nosel");return new U(a)}if(b){b=z.document;var d;a=(d=tg.exec(a))?[b.createElement(d[1])]:(d=gd(a,b))?d.childNodes:[];pc(this,a)}else B(a)?hd(a):pc(this,a)}function qc(a){return a.cloneNode(!0)}function zb(a,b){!b&&mc(a)&&x.cleanData([a]);a.querySelectorAll&&x.cleanData(a.querySelectorAll("*"))}function id(a){for(var b in a)return!1; +return!0}function jd(a){var b=a.ng339,d=b&&Ka[b],c=d&&d.events,d=d&&d.data;d&&!id(d)||c&&!id(c)||(delete Ka[b],a.ng339=void 0)}function kd(a,b,d,c){if(w(c))throw oc("offargs");var e=(c=Ab(a))&&c.events,f=c&&c.handle;if(f){if(b){var g=function(b){var c=e[b];w(d)&&cb(c||[],d);w(d)&&c&&0l&&this.remove(n.key);return b}},get:function(a){if(l";b=Fa.firstChild.attributes;var d=b[0];b.removeNamedItem(d.name);d.value=c;a.attributes.setNamedItem(d)}function sa(a,b){try{a.addClass(b)}catch(c){}}function da(a,b,c,d,e){a instanceof x||(a=x(a));var f=Xa(a,b,a,c,d,e);da.$$addScopeClass(a);var g=null;return function(b,c,d){if(!a)throw $("multilink");gb(b,"scope");e&&e.needsNewScope&&(b=b.$parent.$new());d=d||{};var h=d.parentBoundTranscludeFn,k=d.transcludeControllers;d=d.futureParentElement; +h&&h.$$boundTransclude&&(h=h.$$boundTransclude);g||(g=(d=d&&d[0])?"foreignobject"!==ua(d)&&la.call(d).match(/SVG/)?"svg":"html":"html");d="html"!==g?x(ja(g,x("
    ").append(a).html())):c?Wa.clone.call(a):a;if(k)for(var l in k)d.data("$"+l+"Controller",k[l].instance);da.$$addScopeInfo(d,b);c&&c(d,b);f&&f(b,d,d,h);c||(a=f=null);return d}}function Xa(a,b,c,d,e,f){function g(a,c,d,e){var f,k,l,m,p,I,t;if(n)for(t=Array(c.length),m=0;mu.priority)break;if(O=u.scope)u.templateUrl||(D(O)?(ba("new/isolated scope",s||t,u,y),s=u):ba("new/isolated scope",s,u,y)),t=t||u;Q=u.name;if(!ma&&(u.replace&&(u.templateUrl||u.template)||u.transclude&& +!u.$$tlb)){for(O=sa+1;ma=a[O++];)if(ma.transclude&&!ma.$$tlb||ma.replace&&(ma.templateUrl||ma.template)){Jb=!0;break}ma=!0}!u.templateUrl&&u.controller&&(J=J||T(),ba("'"+Q+"' controller",J[Q],u,y),J[Q]=u);if(O=u.transclude)if(G=!0,u.$$tlb||(ba("transclusion",L,u,y),L=u),"element"===O)N=!0,n=u.priority,M=y,y=d.$$element=x(da.$$createComment(Q,d[Q])),b=y[0],oa(f,Ha.call(M,0),b),R=Z(Jb,M,e,n,g&&g.name,{nonTlbTranscludeDirective:L});else{var ka=T();if(D(O)){M=z.document.createDocumentFragment();var Xa= +T(),F=T();r(O,function(a,b){var c="?"===a.charAt(0);a=c?a.substring(1):a;Xa[a]=b;ka[b]=null;F[b]=c});r(y.contents(),function(a){var b=Xa[xa(ua(a))];b?(F[b]=!0,ka[b]=ka[b]||z.document.createDocumentFragment(),ka[b].appendChild(a)):M.appendChild(a)});r(F,function(a,b){if(!a)throw $("reqslot",b);});for(var K in ka)ka[K]&&(R=x(ka[K].childNodes),ka[K]=Z(Jb,R,e));M=x(M.childNodes)}else M=x(qc(b)).contents();y.empty();R=Z(Jb,M,e,void 0,void 0,{needsNewScope:u.$$isolateScope||u.$$newScope});R.$$slots=ka}if(u.template)if(P= +!0,ba("template",v,u,y),v=u,O=B(u.template)?u.template(y,d):u.template,O=Na(O),u.replace){g=u;M=nc.test(O)?td(ja(u.templateNamespace,V(O))):[];b=M[0];if(1!==M.length||1!==b.nodeType)throw $("tplrt",Q,"");oa(f,y,b);C={$attr:{}};O=tc(b,[],C);var Ig=a.splice(sa+1,a.length-(sa+1));(s||t)&&fa(O,s,t);a=a.concat(O).concat(Ig);ga(d,C);C=a.length}else y.html(O);if(u.templateUrl)P=!0,ba("template",v,u,y),v=u,u.replace&&(g=u),p=ha(a.splice(sa,a.length-sa),y,d,f,G&&R,h,k,{controllerDirectives:J,newScopeDirective:t!== +u&&t,newIsolateScopeDirective:s,templateDirective:v,nonTlbTranscludeDirective:L}),C=a.length;else if(u.compile)try{q=u.compile(y,d,R);var Y=u.$$originalDirective||u;B(q)?m(null,Va(Y,q),E,jb):q&&m(Va(Y,q.pre),Va(Y,q.post),E,jb)}catch(ca){c(ca,Aa(y))}u.terminal&&(p.terminal=!0,n=Math.max(n,u.priority))}p.scope=t&&!0===t.scope;p.transcludeOnThisElement=G;p.templateOnThisElement=P;p.transclude=R;l.hasElementTranscludeDirective=N;return p}function X(a,b,c,d){var e;if(C(b)){var f=b.match(l);b=b.substring(f[0].length); +var g=f[1]||f[3],f="?"===f[2];"^^"===g?c=c.parent():e=(e=d&&d[b])&&e.instance;if(!e){var h="$"+b+"Controller";e="^^"===g&&c[0]&&9===c[0].nodeType?null:g?c.inheritedData(h):c.data(h)}if(!e&&!f)throw $("ctreq",b,a);}else if(H(b))for(e=[],g=0,f=b.length;gc.priority)&&-1!==c.restrict.indexOf(e)){k&&(c=bc(c,{$$start:k,$$end:l}));if(!c.$$bindings){var I=m=c,t=c.name,u={isolateScope:null,bindToController:null}; +D(I.scope)&&(!0===I.bindToController?(u.bindToController=d(I.scope,t,!0),u.isolateScope={}):u.isolateScope=d(I.scope,t,!1));D(I.bindToController)&&(u.bindToController=d(I.bindToController,t,!0));if(u.bindToController&&!I.controller)throw $("noctrl",t);m=m.$$bindings=u;D(m.isolateScope)&&(c.$$isolateBindings=m.isolateScope)}b.push(c);m=c}}return m}function ca(b){if(f.hasOwnProperty(b))for(var c=a.get(b+"Directive"),d=0,e=c.length;d"+b+"";return c.childNodes[0].childNodes;default:return b}}function qa(a,b){if("srcdoc"=== +b)return u.HTML;if("src"===b||"ngSrc"===b)return-1===["img","video","audio","source","track"].indexOf(a)?u.RESOURCE_URL:u.MEDIA_URL;if("xlinkHref"===b)return"image"===a?u.MEDIA_URL:"a"===a?u.URL:u.RESOURCE_URL;if("form"===a&&"action"===b||"base"===a&&"href"===b||"link"===a&&"href"===b)return u.RESOURCE_URL;if("a"===a&&("href"===b||"ngHref"===b))return u.URL}function ya(a,b){var c=b.toLowerCase();return v[a+"|"+c]||v["*|"+c]}function za(a){return ma(u.valueOf(a),"ng-prop-srcset")}function Ea(a,b,c, +d){if(m.test(d))throw $("nodomevents");a=ua(a);var e=ya(a,d),f=Ta;"srcset"!==d||"img"!==a&&"source"!==a?e&&(f=u.getTrusted.bind(u,e)):f=za;b.push({priority:100,compile:function(a,b){var e=p(b[c]),g=p(b[c],function(a){return u.valueOf(a)});return{pre:function(a,b){function c(){var g=e(a);b[0][d]=f(g)}c();a.$watch(g,c)}}}})}function Ia(a,c,d,e,f){var g=ua(a),k=qa(g,e),l=h[e]||f,p=b(d,!f,k,l);if(p){if("multiple"===e&&"select"===g)throw $("selmulti",Aa(a));if(m.test(e))throw $("nodomevents");c.push({priority:100, +compile:function(){return{pre:function(a,c,f){c=f.$$observers||(f.$$observers=T());var g=f[e];g!==d&&(p=g&&b(g,!0,k,l),d=g);p&&(f[e]=p(a),(c[e]||(c[e]=[])).$$inter=!0,(f.$$observers&&f.$$observers[e].$$scope||a).$watch(p,function(a,b){"class"===e&&a!==b?f.$updateClass(a,b):f.$set(e,a)}))}}}})}}function oa(a,b,c){var d=b[0],e=b.length,f=d.parentNode,g,h;if(a)for(g=0,h=a.length;g=b)return a;for(;b--;){var d=a[b];(8===d.nodeType||d.nodeType===Pa&&""===d.nodeValue.trim())&&Kg.call(a,b,1)}return a} +function Gg(a,b){if(b&&C(b))return b;if(C(a)){var d=wd.exec(a);if(d)return d[3]}}function Kf(){var a={};this.has=function(b){return a.hasOwnProperty(b)};this.register=function(b,d){Ja(b,"controller");D(b)?S(a,b):a[b]=d};this.$get=["$injector",function(b){function d(a,b,d,g){if(!a||!D(a.$scope))throw F("$controller")("noscp",g,b);a.$scope[b]=d}return function(c,e,f,g){var k,h,l;f=!0===f;g&&C(g)&&(l=g);if(C(c)){g=c.match(wd);if(!g)throw xd("ctrlfmt",c);h=g[1];l=l||g[3];c=a.hasOwnProperty(h)?a[h]:Le(e.$scope, +h,!0);if(!c)throw xd("ctrlreg",h);tb(c,h,!0)}if(f)return f=(H(c)?c[c.length-1]:c).prototype,k=Object.create(f||null),l&&d(e,l,k,h||c.name),S(function(){var a=b.invoke(c,k,e,h);a!==k&&(D(a)||B(a))&&(k=a,l&&d(e,l,k,h||c.name));return k},{instance:k,identifier:l});k=b.instantiate(c,e,h);l&&d(e,l,k,h||c.name);return k}}]}function Lf(){this.$get=["$window",function(a){return x(a.document)}]}function Mf(){this.$get=["$document","$rootScope",function(a,b){function d(){e=c.hidden}var c=a[0],e=c&&c.hidden; +a.on("visibilitychange",d);b.$on("$destroy",function(){a.off("visibilitychange",d)});return function(){return e}}]}function Nf(){this.$get=["$log",function(a){return function(b,d){a.error.apply(a,arguments)}}]}function vc(a){return D(a)?ha(a)?a.toISOString():eb(a):a}function Tf(){this.$get=function(){return function(a){if(!a)return"";var b=[];Qc(a,function(a,c){null===a||A(a)||B(a)||(H(a)?r(a,function(a){b.push(ba(c)+"="+ba(vc(a)))}):b.push(ba(c)+"="+ba(vc(a))))});return b.join("&")}}}function Uf(){this.$get= +function(){return function(a){function b(a,e,f){H(a)?r(a,function(a,c){b(a,e+"["+(D(a)?c:"")+"]")}):D(a)&&!ha(a)?Qc(a,function(a,c){b(a,e+(f?"":"[")+c+(f?"":"]"))}):(B(a)&&(a=a()),d.push(ba(e)+"="+(null==a?"":ba(vc(a)))))}if(!a)return"";var d=[];b(a,"",!0);return d.join("&")}}}function wc(a,b){if(C(a)){var d=a.replace(Lg,"").trim();if(d){var c=b("Content-Type"),c=c&&0===c.indexOf(yd),e;(e=c)||(e=(e=d.match(Mg))&&Ng[e[0]].test(d));if(e)try{a=Tc(d)}catch(f){if(!c)return a;throw Lb("baddata",a,f);}}}return a} +function zd(a){var b=T(),d;C(a)?r(a.split("\n"),function(a){d=a.indexOf(":");var e=K(V(a.substr(0,d)));a=V(a.substr(d+1));e&&(b[e]=b[e]?b[e]+", "+a:a)}):D(a)&&r(a,function(a,d){var f=K(d),g=V(a);f&&(b[f]=b[f]?b[f]+", "+g:g)});return b}function Ad(a){var b;return function(d){b||(b=zd(a));return d?(d=b[K(d)],void 0===d&&(d=null),d):b}}function Bd(a,b,d,c){if(B(c))return c(a,b,d);r(c,function(c){a=c(a,b,d)});return a}function Sf(){var a=this.defaults={transformResponse:[wc],transformRequest:[function(a){return D(a)&& +"[object File]"!==la.call(a)&&"[object Blob]"!==la.call(a)&&"[object FormData]"!==la.call(a)?eb(a):a}],headers:{common:{Accept:"application/json, text/plain, */*"},post:ja(xc),put:ja(xc),patch:ja(xc)},xsrfCookieName:"XSRF-TOKEN",xsrfHeaderName:"X-XSRF-TOKEN",paramSerializer:"$httpParamSerializer",jsonpCallbackParam:"callback"},b=!1;this.useApplyAsync=function(a){return w(a)?(b=!!a,this):b};var d=this.interceptors=[],c=this.xsrfTrustedOrigins=[];Object.defineProperty(this,"xsrfWhitelistedOrigins", +{get:function(){return this.xsrfTrustedOrigins},set:function(a){this.xsrfTrustedOrigins=a}});this.$get=["$browser","$httpBackend","$$cookieReader","$cacheFactory","$rootScope","$q","$injector","$sce",function(e,f,g,k,h,l,m,p){function n(b){function c(a,b){for(var d=0,e=b.length;da?b:l.reject(b)}if(!D(b))throw F("$http")("badreq",b);if(!C(p.valueOf(b.url)))throw F("$http")("badreq",b.url);var g=S({method:"get",transformRequest:a.transformRequest,transformResponse:a.transformResponse,paramSerializer:a.paramSerializer,jsonpCallbackParam:a.jsonpCallbackParam},b);g.headers=function(b){var c=a.headers,e=S({},b.headers),f,g,h,c=S({},c.common,c[K(b.method)]);a:for(f in c){g=K(f);for(h in e)if(K(h)===g)continue a;e[f]=c[f]}return d(e,ja(b))}(b);g.method= +vb(g.method);g.paramSerializer=C(g.paramSerializer)?m.get(g.paramSerializer):g.paramSerializer;e.$$incOutstandingRequestCount("$http");var h=[],k=[];b=l.resolve(g);r(v,function(a){(a.request||a.requestError)&&h.unshift(a.request,a.requestError);(a.response||a.responseError)&&k.push(a.response,a.responseError)});b=c(b,h);b=b.then(function(b){var c=b.headers,d=Bd(b.data,Ad(c),void 0,b.transformRequest);A(d)&&r(c,function(a,b){"content-type"===K(b)&&delete c[b]});A(b.withCredentials)&&!A(a.withCredentials)&& +(b.withCredentials=a.withCredentials);return s(b,d).then(f,f)});b=c(b,k);return b=b.finally(function(){e.$$completeOutstandingRequest(E,"$http")})}function s(c,d){function e(a){if(a){var c={};r(a,function(a,d){c[d]=function(c){function d(){a(c)}b?h.$applyAsync(d):h.$$phase?d():h.$apply(d)}});return c}}function k(a,c,d,e,f){function g(){m(c,a,d,e,f)}R&&(200<=a&&300>a?R.put(O,[a,c,zd(d),e,f]):R.remove(O));b?h.$applyAsync(g):(g(),h.$$phase||h.$apply())}function m(a,b,d,e,f){b=-1<=b?b:0;(200<=b&&300> +b?L.resolve:L.reject)({data:a,status:b,headers:Ad(d),config:c,statusText:e,xhrStatus:f})}function s(a){m(a.data,a.status,ja(a.headers()),a.statusText,a.xhrStatus)}function v(){var a=n.pendingRequests.indexOf(c);-1!==a&&n.pendingRequests.splice(a,1)}var L=l.defer(),u=L.promise,R,q,ma=c.headers,x="jsonp"===K(c.method),O=c.url;x?O=p.getTrustedResourceUrl(O):C(O)||(O=p.valueOf(O));O=G(O,c.paramSerializer(c.params));x&&(O=t(O,c.jsonpCallbackParam));n.pendingRequests.push(c);u.then(v,v);!c.cache&&!a.cache|| +!1===c.cache||"GET"!==c.method&&"JSONP"!==c.method||(R=D(c.cache)?c.cache:D(a.cache)?a.cache:N);R&&(q=R.get(O),w(q)?q&&B(q.then)?q.then(s,s):H(q)?m(q[1],q[0],ja(q[2]),q[3],q[4]):m(q,200,{},"OK","complete"):R.put(O,u));A(q)&&((q=kc(c.url)?g()[c.xsrfCookieName||a.xsrfCookieName]:void 0)&&(ma[c.xsrfHeaderName||a.xsrfHeaderName]=q),f(c.method,O,d,k,ma,c.timeout,c.withCredentials,c.responseType,e(c.eventHandlers),e(c.uploadEventHandlers)));return u}function G(a,b){0=h&&(t.resolve(s),f(r.$$intervalId));G||c.$apply()},k,t,G);return r}}}]}function Cd(a,b){var d=ga(a);b.$$protocol=d.protocol;b.$$host= +d.hostname;b.$$port=fa(d.port)||Rg[d.protocol]||null}function Dd(a,b,d){if(Sg.test(a))throw kb("badpath",a);var c="/"!==a.charAt(0);c&&(a="/"+a);a=ga(a);for(var c=(c&&"/"===a.pathname.charAt(0)?a.pathname.substring(1):a.pathname).split("/"),e=c.length;e--;)c[e]=decodeURIComponent(c[e]),d&&(c[e]=c[e].replace(/\//g,"%2F"));d=c.join("/");b.$$path=d;b.$$search=hc(a.search);b.$$hash=decodeURIComponent(a.hash);b.$$path&&"/"!==b.$$path.charAt(0)&&(b.$$path="/"+b.$$path)}function yc(a,b){return a.slice(0, +b.length)===b}function ya(a,b){if(yc(b,a))return b.substr(a.length)}function Da(a){var b=a.indexOf("#");return-1===b?a:a.substr(0,b)}function zc(a,b,d){this.$$html5=!0;d=d||"";Cd(a,this);this.$$parse=function(a){var d=ya(b,a);if(!C(d))throw kb("ipthprfx",a,b);Dd(d,this,!0);this.$$path||(this.$$path="/");this.$$compose()};this.$$normalizeUrl=function(a){return b+a.substr(1)};this.$$parseLinkUrl=function(c,e){if(e&&"#"===e[0])return this.hash(e.slice(1)),!0;var f,g;w(f=ya(a,c))?(g=f,g=d&&w(f=ya(d,f))? +b+(ya("/",f)||f):a+g):w(f=ya(b,c))?g=b+f:b===c+"/"&&(g=b);g&&this.$$parse(g);return!!g}}function Ac(a,b,d){Cd(a,this);this.$$parse=function(c){var e=ya(a,c)||ya(b,c),f;A(e)||"#"!==e.charAt(0)?this.$$html5?f=e:(f="",A(e)&&(a=c,this.replace())):(f=ya(d,e),A(f)&&(f=e));Dd(f,this,!1);c=this.$$path;var e=a,g=/^\/[A-Z]:(\/.*)/;yc(f,e)&&(f=f.replace(e,""));g.exec(f)||(c=(f=g.exec(c))?f[1]:c);this.$$path=c;this.$$compose()};this.$$normalizeUrl=function(b){return a+(b?d+b:"")};this.$$parseLinkUrl=function(b, +d){return Da(a)===Da(b)?(this.$$parse(b),!0):!1}}function Ed(a,b,d){this.$$html5=!0;Ac.apply(this,arguments);this.$$parseLinkUrl=function(c,e){if(e&&"#"===e[0])return this.hash(e.slice(1)),!0;var f,g;a===Da(c)?f=c:(g=ya(b,c))?f=a+d+g:b===c+"/"&&(f=b);f&&this.$$parse(f);return!!f};this.$$normalizeUrl=function(b){return a+d+b}}function Mb(a){return function(){return this[a]}}function Fd(a,b){return function(d){if(A(d))return this[a];this[a]=b(d);this.$$compose();return this}}function Yf(){var a="!", +b={enabled:!1,requireBase:!0,rewriteLinks:!0};this.hashPrefix=function(b){return w(b)?(a=b,this):a};this.html5Mode=function(a){if(Ga(a))return b.enabled=a,this;if(D(a)){Ga(a.enabled)&&(b.enabled=a.enabled);Ga(a.requireBase)&&(b.requireBase=a.requireBase);if(Ga(a.rewriteLinks)||C(a.rewriteLinks))b.rewriteLinks=a.rewriteLinks;return this}return b};this.$get=["$rootScope","$browser","$sniffer","$rootElement","$window",function(d,c,e,f,g){function k(a,b){return a===b||ga(a).href===ga(b).href}function h(a, +b,d){var e=m.url(),f=m.$$state;try{c.url(a,b,d),m.$$state=c.state()}catch(g){throw m.url(e),m.$$state=f,g;}}function l(a,b){d.$broadcast("$locationChangeSuccess",m.absUrl(),a,m.$$state,b)}var m,p;p=c.baseHref();var n=c.url(),s;if(b.enabled){if(!p&&b.requireBase)throw kb("nobase");s=n.substring(0,n.indexOf("/",n.indexOf("//")+2))+(p||"/");p=e.history?zc:Ed}else s=Da(n),p=Ac;var r=s.substr(0,Da(s).lastIndexOf("/")+1);m=new p(s,r,"#"+a);m.$$parseLinkUrl(n,n);m.$$state=c.state();var t=/^\s*(javascript|mailto):/i; +f.on("click",function(a){var e=b.rewriteLinks;if(e&&!a.ctrlKey&&!a.metaKey&&!a.shiftKey&&2!==a.which&&2!==a.button){for(var g=x(a.target);"a"!==ua(g[0]);)if(g[0]===f[0]||!(g=g.parent())[0])return;if(!C(e)||!A(g.attr(e))){var e=g.prop("href"),h=g.attr("href")||g.attr("xlink:href");D(e)&&"[object SVGAnimatedString]"===e.toString()&&(e=ga(e.animVal).href);t.test(e)||!e||g.attr("target")||a.isDefaultPrevented()||!m.$$parseLinkUrl(e,h)||(a.preventDefault(),m.absUrl()!==c.url()&&d.$apply())}}});m.absUrl()!== +n&&c.url(m.absUrl(),!0);var N=!0;c.onUrlChange(function(a,b){yc(a,r)?(d.$evalAsync(function(){var c=m.absUrl(),e=m.$$state,f;m.$$parse(a);m.$$state=b;f=d.$broadcast("$locationChangeStart",a,c,b,e).defaultPrevented;m.absUrl()===a&&(f?(m.$$parse(c),m.$$state=e,h(c,!1,e)):(N=!1,l(c,e)))}),d.$$phase||d.$digest()):g.location.href=a});d.$watch(function(){if(N||m.$$urlUpdatedByLocation){m.$$urlUpdatedByLocation=!1;var a=c.url(),b=m.absUrl(),f=c.state(),g=m.$$replace,n=!k(a,b)||m.$$html5&&e.history&&f!== +m.$$state;if(N||n)N=!1,d.$evalAsync(function(){var b=m.absUrl(),c=d.$broadcast("$locationChangeStart",b,a,m.$$state,f).defaultPrevented;m.absUrl()===b&&(c?(m.$$parse(a),m.$$state=f):(n&&h(b,g,f===m.$$state?null:m.$$state),l(a,f)))})}m.$$replace=!1});return m}]}function Zf(){var a=!0,b=this;this.debugEnabled=function(b){return w(b)?(a=b,this):a};this.$get=["$window",function(d){function c(a){dc(a)&&(a.stack&&f?a=a.message&&-1===a.stack.indexOf(a.message)?"Error: "+a.message+"\n"+a.stack:a.stack:a.sourceURL&& +(a=a.message+"\n"+a.sourceURL+":"+a.line));return a}function e(a){var b=d.console||{},e=b[a]||b.log||E;return function(){var a=[];r(arguments,function(b){a.push(c(b))});return Function.prototype.apply.call(e,b,a)}}var f=wa||/\bEdge\//.test(d.navigator&&d.navigator.userAgent);return{log:e("log"),info:e("info"),warn:e("warn"),error:e("error"),debug:function(){var c=e("debug");return function(){a&&c.apply(b,arguments)}}()}}]}function Tg(a){return a+""}function Ug(a,b){return"undefined"!==typeof a?a: +b}function Gd(a,b){return"undefined"===typeof a?b:"undefined"===typeof b?a:a+b}function Vg(a,b){switch(a.type){case q.MemberExpression:if(a.computed)return!1;break;case q.UnaryExpression:return 1;case q.BinaryExpression:return"+"!==a.operator?1:!1;case q.CallExpression:return!1}return void 0===b?Hd:b}function Z(a,b,d){var c,e,f=a.isPure=Vg(a,d);switch(a.type){case q.Program:c=!0;r(a.body,function(a){Z(a.expression,b,f);c=c&&a.expression.constant});a.constant=c;break;case q.Literal:a.constant=!0;a.toWatch= +[];break;case q.UnaryExpression:Z(a.argument,b,f);a.constant=a.argument.constant;a.toWatch=a.argument.toWatch;break;case q.BinaryExpression:Z(a.left,b,f);Z(a.right,b,f);a.constant=a.left.constant&&a.right.constant;a.toWatch=a.left.toWatch.concat(a.right.toWatch);break;case q.LogicalExpression:Z(a.left,b,f);Z(a.right,b,f);a.constant=a.left.constant&&a.right.constant;a.toWatch=a.constant?[]:[a];break;case q.ConditionalExpression:Z(a.test,b,f);Z(a.alternate,b,f);Z(a.consequent,b,f);a.constant=a.test.constant&& +a.alternate.constant&&a.consequent.constant;a.toWatch=a.constant?[]:[a];break;case q.Identifier:a.constant=!1;a.toWatch=[a];break;case q.MemberExpression:Z(a.object,b,f);a.computed&&Z(a.property,b,f);a.constant=a.object.constant&&(!a.computed||a.property.constant);a.toWatch=a.constant?[]:[a];break;case q.CallExpression:c=d=a.filter?!b(a.callee.name).$stateful:!1;e=[];r(a.arguments,function(a){Z(a,b,f);c=c&&a.constant;e.push.apply(e,a.toWatch)});a.constant=c;a.toWatch=d?e:[a];break;case q.AssignmentExpression:Z(a.left, +b,f);Z(a.right,b,f);a.constant=a.left.constant&&a.right.constant;a.toWatch=[a];break;case q.ArrayExpression:c=!0;e=[];r(a.elements,function(a){Z(a,b,f);c=c&&a.constant;e.push.apply(e,a.toWatch)});a.constant=c;a.toWatch=e;break;case q.ObjectExpression:c=!0;e=[];r(a.properties,function(a){Z(a.value,b,f);c=c&&a.value.constant;e.push.apply(e,a.value.toWatch);a.computed&&(Z(a.key,b,!1),c=c&&a.key.constant,e.push.apply(e,a.key.toWatch))});a.constant=c;a.toWatch=e;break;case q.ThisExpression:a.constant= +!1;a.toWatch=[];break;case q.LocalsExpression:a.constant=!1,a.toWatch=[]}}function Id(a){if(1===a.length){a=a[0].expression;var b=a.toWatch;return 1!==b.length?b:b[0]!==a?b:void 0}}function Jd(a){return a.type===q.Identifier||a.type===q.MemberExpression}function Kd(a){if(1===a.body.length&&Jd(a.body[0].expression))return{type:q.AssignmentExpression,left:a.body[0].expression,right:{type:q.NGValueParameter},operator:"="}}function Ld(a){this.$filter=a}function Md(a){this.$filter=a}function Nb(a,b,d){this.ast= +new q(a,d);this.astCompiler=d.csp?new Md(b):new Ld(b)}function Bc(a){return B(a.valueOf)?a.valueOf():Wg.call(a)}function $f(){var a=T(),b={"true":!0,"false":!1,"null":null,undefined:void 0},d,c;this.addLiteral=function(a,c){b[a]=c};this.setIdentifierFns=function(a,b){d=a;c=b;return this};this.$get=["$filter",function(e){function f(b,c){var d,f;switch(typeof b){case "string":return f=b=b.trim(),d=a[f],d||(d=new Ob(G),d=(new Nb(d,e,G)).parse(b),a[f]=p(d)),s(d,c);case "function":return s(b,c);default:return s(E, +c)}}function g(a,b,c){return null==a||null==b?a===b:"object"!==typeof a||(a=Bc(a),"object"!==typeof a||c)?a===b||a!==a&&b!==b:!1}function k(a,b,c,d,e){var f=d.inputs,h;if(1===f.length){var k=g,f=f[0];return a.$watch(function(a){var b=f(a);g(b,k,f.isPure)||(h=d(a,void 0,void 0,[b]),k=b&&Bc(b));return h},b,c,e)}for(var l=[],m=[],n=0,p=f.length;n=c.$$state.status&&e&&e.length&&a(function(){for(var a,c,f=0,g=e.length;fa)for(b in l++, +f)ta.call(e,b)||(t--,delete f[b])}else f!==e&&(f=e,l++);return l}}c.$$pure=g(a).literal;c.$stateful=!c.$$pure;var d=this,e,f,h,k=1r&&(A=4-r,N[A]||(N[A]=[]),N[A].push({msg:B(a.exp)?"fn: "+(a.exp.name||a.exp.toString()):a.exp,newVal:g,oldVal:h}));else if(a===c){s= +!1;break a}}catch(E){f(E)}if(!(n=!q.$$suspended&&q.$$watchersCount&&q.$$childHead||q!==y&&q.$$nextSibling))for(;q!==y&&!(n=q.$$nextSibling);)q=q.$parent}while(q=n);if((s||w.length)&&!r--)throw v.$$phase=null,d("infdig",b,N);}while(s||w.length);for(v.$$phase=null;Jwa)throw Ea("iequirks");var c=ja(W);c.isEnabled=function(){return a};c.trustAs=d.trustAs;c.getTrusted=d.getTrusted;c.valueOf=d.valueOf;a||(c.trustAs=c.getTrusted=function(a,b){return b},c.valueOf=Ta);c.parseAs=function(a,d){var e=b(d);return e.literal&&e.constant?e:b(d,function(b){return c.getTrusted(a,b)})};var e=c.parseAs,f=c.getTrusted,g=c.trustAs;r(W, +function(a,b){var d=K(b);c[("parse_as_"+d).replace(Dc,xb)]=function(b){return e(a,b)};c[("get_trusted_"+d).replace(Dc,xb)]=function(b){return f(a,b)};c[("trust_as_"+d).replace(Dc,xb)]=function(b){return g(a,b)}});return c}]}function fg(){this.$get=["$window","$document",function(a,b){var d={},c=!((!a.nw||!a.nw.process)&&a.chrome&&(a.chrome.app&&a.chrome.app.runtime||!a.chrome.app&&a.chrome.runtime&&a.chrome.runtime.id))&&a.history&&a.history.pushState,e=fa((/android (\d+)/.exec(K((a.navigator||{}).userAgent))|| +[])[1]),f=/Boxee/i.test((a.navigator||{}).userAgent),g=b[0]||{},k=g.body&&g.body.style,h=!1,l=!1;k&&(h=!!("transition"in k||"webkitTransition"in k),l=!!("animation"in k||"webkitAnimation"in k));return{history:!(!c||4>e||f),hasEvent:function(a){if("input"===a&&wa)return!1;if(A(d[a])){var b=g.createElement("div");d[a]="on"+a in b}return d[a]},csp:Ba(),transitions:h,animations:l,android:e}}]}function gg(){this.$get=ia(function(a){return new Yg(a)})}function Yg(a){function b(){var a=e.pop();return a&& +a.cb}function d(a){for(var b=e.length-1;0<=b;--b){var c=e[b];if(c.type===a)return e.splice(b,1),c.cb}}var c={},e=[],f=this.ALL_TASKS_TYPE="$$all$$",g=this.DEFAULT_TASK_TYPE="$$default$$";this.completeTask=function(e,h){h=h||g;try{e()}finally{var l;l=h||g;c[l]&&(c[l]--,c[f]--);l=c[h];var m=c[f];if(!m||!l)for(l=m?d:b;m=l(h);)try{m()}catch(p){a.error(p)}}};this.incTaskCount=function(a){a=a||g;c[a]=(c[a]||0)+1;c[f]=(c[f]||0)+1};this.notifyWhenNoPendingTasks=function(a,b){b=b||f;c[b]?e.push({type:b,cb:a}): +a()}}function ig(){var a;this.httpOptions=function(b){return b?(a=b,this):a};this.$get=["$exceptionHandler","$templateCache","$http","$q","$sce",function(b,d,c,e,f){function g(k,h){g.totalPendingRequests++;if(!C(k)||A(d.get(k)))k=f.getTrustedResourceUrl(k);var l=c.defaults&&c.defaults.transformResponse;H(l)?l=l.filter(function(a){return a!==wc}):l===wc&&(l=null);return c.get(k,S({cache:d,transformResponse:l},a)).finally(function(){g.totalPendingRequests--}).then(function(a){return d.put(k,a.data)}, +function(a){h||(a=Zg("tpload",k,a.status,a.statusText),b(a));return e.reject(a)})}g.totalPendingRequests=0;return g}]}function jg(){this.$get=["$rootScope","$browser","$location",function(a,b,d){return{findBindings:function(a,b,d){a=a.getElementsByClassName("ng-binding");var g=[];r(a,function(a){var c=ca.element(a).data("$binding");c&&r(c,function(c){d?(new RegExp("(^|\\s)"+Od(b)+"(\\s|\\||$)")).test(c)&&g.push(a):-1!==c.indexOf(b)&&g.push(a)})});return g},findModels:function(a,b,d){for(var g=["ng-", +"data-ng-","ng\\:"],k=0;kc&&(c=e),c+=+a.slice(e+1),a=a.substring(0,e)):0>c&&(c=a.length);for(e=0;a.charAt(e)===Fc;e++);if(e===(g=a.length))d=[0],c=1;else{for(g--;a.charAt(g)===Fc;)g--;c-=e;d=[];for(f=0;e<=g;e++,f++)d[f]=+a.charAt(e)}c>Yd&&(d=d.splice(0,Yd-1),b=c-1,c=1);return{d:d,e:b,i:c}}function ih(a, +b,d,c){var e=a.d,f=e.length-a.i;b=A(b)?Math.min(Math.max(d,f),c):+b;d=b+a.i;c=e[d];if(0d-1){for(c=0;c>d;c--)e.unshift(0),a.i++;e.unshift(1);a.i++}else e[d-1]++;for(;fk;)h.unshift(0),k++;0=b.lgSize&&k.unshift(h.splice(-b.lgSize,h.length).join(""));h.length>b.gSize;)k.unshift(h.splice(-b.gSize,h.length).join(""));h.length&&k.unshift(h.join(""));h=k.join(d);f.length&&(h+=c+f.join(""));e&&(h+="e+"+e)}return 0>a&&!g?b.negPre+h+b.negSuf:b.posPre+ +h+b.posSuf}function Pb(a,b,d,c){var e="";if(0>a||c&&0>=a)c?a=-a+1:(a=-a,e="-");for(a=""+a;a.length-d)f+=d;0===f&&-12===d&&(f=12);return Pb(f,b,c,e)}}function lb(a,b,d){return function(c,e){var f=c["get"+a](),g=vb((d?"STANDALONE":"")+(b?"SHORT":"")+a);return e[g][f]}}function Zd(a){var b=(new Date(a,0,1)).getDay();return new Date(a,0,(4>=b?5:12)-b)}function $d(a){return function(b){var d= +Zd(b.getFullYear());b=+new Date(b.getFullYear(),b.getMonth(),b.getDate()+(4-b.getDay()))-+d;b=1+Math.round(b/6048E5);return Pb(b,a)}}function Gc(a,b){return 0>=a.getFullYear()?b.ERAS[0]:b.ERAS[1]}function Td(a){function b(a){var b;if(b=a.match(d)){a=new Date(0);var f=0,g=0,k=b[8]?a.setUTCFullYear:a.setFullYear,h=b[8]?a.setUTCHours:a.setHours;b[9]&&(f=fa(b[9]+b[10]),g=fa(b[9]+b[11]));k.call(a,fa(b[1]),fa(b[2])-1,fa(b[3]));f=fa(b[4]||0)-f;g=fa(b[5]||0)-g;k=fa(b[6]||0);b=Math.round(1E3*parseFloat("0."+ +(b[7]||0)));h.call(a,f,g,k,b)}return a}var d=/^(\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?::?(\d\d)(?::?(\d\d)(?:\.(\d+))?)?)?(Z|([+-])(\d\d):?(\d\d))?)?$/;return function(c,d,f){var g="",k=[],h,l;d=d||"mediumDate";d=a.DATETIME_FORMATS[d]||d;C(c)&&(c=jh.test(c)?fa(c):b(c));X(c)&&(c=new Date(c));if(!ha(c)||!isFinite(c.getTime()))return c;for(;d;)(l=kh.exec(d))?(k=db(k,l,1),d=k.pop()):(k.push(d),d=null);var m=c.getTimezoneOffset();f&&(m=fc(f,m),c=gc(c,f,!0));r(k,function(b){h=lh[b];g+=h?h(c,a.DATETIME_FORMATS, +m):"''"===b?"'":b.replace(/(^'|'$)/g,"").replace(/''/g,"'")});return g}}function ch(){return function(a,b){A(b)&&(b=2);return eb(a,b)}}function dh(){return function(a,b,d){b=Infinity===Math.abs(Number(b))?Number(b):fa(b);if(Y(b))return a;X(a)&&(a=a.toString());if(!za(a))return a;d=!d||isNaN(d)?0:fa(d);d=0>d?Math.max(0,a.length+d):d;return 0<=b?Hc(a,d,d+b):0===d?Hc(a,b,a.length):Hc(a,Math.max(0,d+b),d)}}function Hc(a,b,d){return C(a)?a.slice(b,d):Ha.call(a,b,d)}function Vd(a){function b(b){return b.map(function(b){var c= +1,d=Ta;if(B(b))d=b;else if(C(b)){if("+"===b.charAt(0)||"-"===b.charAt(0))c="-"===b.charAt(0)?-1:1,b=b.substring(1);if(""!==b&&(d=a(b),d.constant))var e=d(),d=function(a){return a[e]}}return{get:d,descending:c}})}function d(a){switch(typeof a){case "number":case "boolean":case "string":return!0;default:return!1}}function c(a,b){var c=0,d=a.type,h=b.type;if(d===h){var h=a.value,l=b.value;"string"===d?(h=h.toLowerCase(),l=l.toLowerCase()):"object"===d&&(D(h)&&(h=a.index),D(l)&&(l=b.index));h!==l&&(c= +hb||37<=b&&40>=b|| +m(a,this,this.value)});if(e.hasEvent("paste"))b.on("paste cut drop",m)}b.on("change",l);if(ee[g]&&c.$$hasNativeValidators&&g===d.type)b.on("keydown wheel mousedown",function(a){if(!h){var b=this.validity,c=b.badInput,d=b.typeMismatch;h=f.defer(function(){h=null;b.badInput===c&&b.typeMismatch===d||l(a)})}});c.$render=function(){var a=c.$isEmpty(c.$viewValue)?"":c.$viewValue;b.val()!==a&&b.val(a)}}function Rb(a,b){return function(d,c){var e,f;if(ha(d))return d;if(C(d)){'"'===d.charAt(0)&&'"'===d.charAt(d.length- +1)&&(d=d.substring(1,d.length-1));if(mh.test(d))return new Date(d);a.lastIndex=0;if(e=a.exec(d))return e.shift(),f=c?{yyyy:c.getFullYear(),MM:c.getMonth()+1,dd:c.getDate(),HH:c.getHours(),mm:c.getMinutes(),ss:c.getSeconds(),sss:c.getMilliseconds()/1E3}:{yyyy:1970,MM:1,dd:1,HH:0,mm:0,ss:0,sss:0},r(e,function(a,c){cf.yyyy&&e.setFullYear(f.yyyy),e}return NaN}}function ob(a,b,d,c){return function(e,f,g,k,h,l,m, +p){function n(a){return a&&!(a.getTime&&a.getTime()!==a.getTime())}function s(a){return w(a)&&!ha(a)?r(a)||void 0:a}function r(a,b){var c=k.$options.getOption("timezone");v&&v!==c&&(b=Uc(b,fc(v)));var e=d(a,b);!isNaN(e)&&c&&(e=gc(e,c));return e}Jc(e,f,g,k,a);Sa(e,f,g,k,h,l);var t="time"===a||"datetimelocal"===a,q,v;k.$parsers.push(function(c){if(k.$isEmpty(c))return null;if(b.test(c))return r(c,q);k.$$parserName=a});k.$formatters.push(function(a){if(a&&!ha(a))throw pb("datefmt",a);if(n(a)){q=a;var b= +k.$options.getOption("timezone");b&&(v=b,q=gc(q,b,!0));var d=c;t&&C(k.$options.getOption("timeSecondsFormat"))&&(d=c.replace("ss.sss",k.$options.getOption("timeSecondsFormat")).replace(/:$/,""));a=m("date")(a,d,b);t&&k.$options.getOption("timeStripZeroSeconds")&&(a=a.replace(/(?::00)?(?:\.000)?$/,""));return a}v=q=null;return""});if(w(g.min)||g.ngMin){var x=g.min||p(g.ngMin)(e),z=s(x);k.$validators.min=function(a){return!n(a)||A(z)||d(a)>=z};g.$observe("min",function(a){a!==x&&(z=s(a),x=a,k.$validate())})}if(w(g.max)|| +g.ngMax){var y=g.max||p(g.ngMax)(e),J=s(y);k.$validators.max=function(a){return!n(a)||A(J)||d(a)<=J};g.$observe("max",function(a){a!==y&&(J=s(a),y=a,k.$validate())})}}}function Jc(a,b,d,c,e){(c.$$hasNativeValidators=D(b[0].validity))&&c.$parsers.push(function(a){var d=b.prop("validity")||{};if(d.badInput||d.typeMismatch)c.$$parserName=e;else return a})}function fe(a){a.$parsers.push(function(b){if(a.$isEmpty(b))return null;if(nh.test(b))return parseFloat(b);a.$$parserName="number"});a.$formatters.push(function(b){if(!a.$isEmpty(b)){if(!X(b))throw pb("numfmt", +b);b=b.toString()}return b})}function na(a){w(a)&&!X(a)&&(a=parseFloat(a));return Y(a)?void 0:a}function Kc(a){var b=a.toString(),d=b.indexOf(".");return-1===d?-1a&&(a=/e-(\d+)$/.exec(b))?Number(a[1]):0:b.length-d-1}function ge(a,b,d){a=Number(a);var c=(a|0)!==a,e=(b|0)!==b,f=(d|0)!==d;if(c||e||f){var g=c?Kc(a):0,k=e?Kc(b):0,h=f?Kc(d):0,g=Math.max(g,k,h),g=Math.pow(10,g);a*=g;b*=g;d*=g;c&&(a=Math.round(a));e&&(b=Math.round(b));f&&(d=Math.round(d))}return 0===(a-b)%d}function he(a,b,d,c,e){if(w(c)){a= +a(c);if(!a.constant)throw pb("constexpr",d,c);return a(b)}return e}function Lc(a,b){function d(a,b){if(!a||!a.length)return[];if(!b||!b.length)return a;var c=[],d=0;a:for(;d(?:<\/\1>|)$/,nc=/<|&#?\w+;/,rg=/<([\w:-]+)/,sg=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:-]+)[^>]*)\/>/gi,qa={thead:["table"],col:["colgroup","table"],tr:["tbody","table"],td:["tr", +"tbody","table"]};qa.tbody=qa.tfoot=qa.colgroup=qa.caption=qa.thead;qa.th=qa.td;var hb={option:[1,'"],_default:[0,"",""]},Nc;for(Nc in qa){var le=qa[Nc],me=le.slice().reverse();hb[Nc]=[me.length,"<"+me.join("><")+">",""]}hb.optgroup=hb.option;var zg=z.Node.prototype.contains||function(a){return!!(this.compareDocumentPosition(a)&16)},Wa=U.prototype={ready:hd,toString:function(){var a=[];r(this,function(b){a.push(""+b)});return"["+a.join(", ")+ +"]"},eq:function(a){return 0<=a?x(this[a]):x(this[this.length+a])},length:0,push:ph,sort:[].sort,splice:[].splice},Hb={};r("multiple selected checked disabled readOnly required open".split(" "),function(a){Hb[K(a)]=a});var od={};r("input select option textarea button form details".split(" "),function(a){od[a]=!0});var vd={ngMinlength:"minlength",ngMaxlength:"maxlength",ngMin:"min",ngMax:"max",ngPattern:"pattern",ngStep:"step"};r({data:sc,removeData:rc,hasData:function(a){for(var b in Ka[a.ng339])return!0; +return!1},cleanData:function(a){for(var b=0,d=a.length;b/,Cg=/^[^(]*\(\s*([^)]*)\)/m,sh=/,/,th=/^\s*(_?)(\S+?)\1\s*$/,Ag=/((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg,Ca=F("$injector");fb.$$annotate=function(a,b,d){var c;if("function"===typeof a){if(!(c=a.$inject)){c=[];if(a.length){if(b)throw C(d)&&d||(d=a.name||Dg(a)),Ca("strictdi",d);b=qd(a);r(b[1].split(sh),function(a){a.replace(th,function(a,b,d){c.push(d)})})}a.$inject=c}}else H(a)?(b=a.length-1,tb(a[b],"fn"),c=a.slice(0,b)):tb(a,"fn",!0);return c};var ne=F("$animate"), +Ef=function(){this.$get=E},Ff=function(){var a=new Ib,b=[];this.$get=["$$AnimateRunner","$rootScope",function(d,c){function e(a,b,c){var d=!1;b&&(b=C(b)?b.split(" "):H(b)?b:[],r(b,function(b){b&&(d=!0,a[b]=c)}));return d}function f(){r(b,function(b){var c=a.get(b);if(c){var d=Eg(b.attr("class")),e="",f="";r(c,function(a,b){a!==!!d[b]&&(a?e+=(e.length?" ":"")+b:f+=(f.length?" ":"")+b)});r(b,function(a){e&&Eb(a,e);f&&Db(a,f)});a.delete(b)}});b.length=0}return{enabled:E,on:E,off:E,pin:E,push:function(g, +k,h,l){l&&l();h=h||{};h.from&&g.css(h.from);h.to&&g.css(h.to);if(h.addClass||h.removeClass)if(k=h.addClass,l=h.removeClass,h=a.get(g)||{},k=e(h,k,!0),l=e(h,l,!1),k||l)a.set(g,h),b.push(g),1===b.length&&c.$$postDigest(f);g=new d;g.complete();return g}}}]},Cf=["$provide",function(a){var b=this,d=null,c=null;this.$$registeredAnimations=Object.create(null);this.register=function(c,d){if(c&&"."!==c.charAt(0))throw ne("notcsel",c);var g=c+"-animation";b.$$registeredAnimations[c.substr(1)]=g;a.factory(g, +d)};this.customFilter=function(a){1===arguments.length&&(c=B(a)?a:null);return c};this.classNameFilter=function(a){if(1===arguments.length&&(d=a instanceof RegExp?a:null)&&/[(\s|\/)]ng-animate[(\s|\/)]/.test(d.toString()))throw d=null,ne("nongcls","ng-animate");return d};this.$get=["$$animateQueue",function(a){function b(a,c,d){if(d){var e;a:{for(e=0;e <= >= && || ! = |".split(" "),function(a){Vb[a]= +!0});var wh={n:"\n",f:"\f",r:"\r",t:"\t",v:"\v","'":"'",'"':'"'},Ob=function(a){this.options=a};Ob.prototype={constructor:Ob,lex:function(a){this.text=a;this.index=0;for(this.tokens=[];this.index=a&&"string"=== +typeof a},isWhitespace:function(a){return" "===a||"\r"===a||"\t"===a||"\n"===a||"\v"===a||"\u00a0"===a},isIdentifierStart:function(a){return this.options.isIdentifierStart?this.options.isIdentifierStart(a,this.codePointAt(a)):this.isValidIdentifierStart(a)},isValidIdentifierStart:function(a){return"a"<=a&&"z">=a||"A"<=a&&"Z">=a||"_"===a||"$"===a},isIdentifierContinue:function(a){return this.options.isIdentifierContinue?this.options.isIdentifierContinue(a,this.codePointAt(a)):this.isValidIdentifierContinue(a)}, +isValidIdentifierContinue:function(a,b){return this.isValidIdentifierStart(a,b)||this.isNumber(a)},codePointAt:function(a){return 1===a.length?a.charCodeAt(0):(a.charCodeAt(0)<<10)+a.charCodeAt(1)-56613888},peekMultichar:function(){var a=this.text.charAt(this.index),b=this.peek();if(!b)return a;var d=a.charCodeAt(0),c=b.charCodeAt(0);return 55296<=d&&56319>=d&&56320<=c&&57343>=c?a+b:a},isExpOperator:function(a){return"-"===a||"+"===a||this.isNumber(a)},throwError:function(a,b,d){d=d||this.index;b= +w(b)?"s "+b+"-"+this.index+" ["+this.text.substring(b,d)+"]":" "+d;throw Ya("lexerr",a,b,this.text);},readNumber:function(){for(var a="",b=this.index;this.index","<=",">=");)a={type:q.BinaryExpression,operator:b.text, +left:a,right:this.additive()};return a},additive:function(){for(var a=this.multiplicative(),b;b=this.expect("+","-");)a={type:q.BinaryExpression,operator:b.text,left:a,right:this.multiplicative()};return a},multiplicative:function(){for(var a=this.unary(),b;b=this.expect("*","/","%");)a={type:q.BinaryExpression,operator:b.text,left:a,right:this.unary()};return a},unary:function(){var a;return(a=this.expect("+","-","!"))?{type:q.UnaryExpression,operator:a.text,prefix:!0,argument:this.unary()}:this.primary()}, +primary:function(){var a;this.expect("(")?(a=this.filterChain(),this.consume(")")):this.expect("[")?a=this.arrayDeclaration():this.expect("{")?a=this.object():this.selfReferential.hasOwnProperty(this.peek().text)?a=Ia(this.selfReferential[this.consume().text]):this.options.literals.hasOwnProperty(this.peek().text)?a={type:q.Literal,value:this.options.literals[this.consume().text]}:this.peek().identifier?a=this.identifier():this.peek().constant?a=this.constant():this.throwError("not a primary expression", +this.peek());for(var b;b=this.expect("(","[",".");)"("===b.text?(a={type:q.CallExpression,callee:a,arguments:this.parseArguments()},this.consume(")")):"["===b.text?(a={type:q.MemberExpression,object:a,property:this.expression(),computed:!0},this.consume("]")):"."===b.text?a={type:q.MemberExpression,object:a,property:this.identifier(),computed:!1}:this.throwError("IMPOSSIBLE");return a},filter:function(a){a=[a];for(var b={type:q.CallExpression,callee:this.identifier(),arguments:a,filter:!0};this.expect(":");)a.push(this.expression()); +return b},parseArguments:function(){var a=[];if(")"!==this.peekToken().text){do a.push(this.filterChain());while(this.expect(","))}return a},identifier:function(){var a=this.consume();a.identifier||this.throwError("is not a valid identifier",a);return{type:q.Identifier,name:a.text}},constant:function(){return{type:q.Literal,value:this.consume().value}},arrayDeclaration:function(){var a=[];if("]"!==this.peekToken().text){do{if(this.peek("]"))break;a.push(this.expression())}while(this.expect(","))}this.consume("]"); +return{type:q.ArrayExpression,elements:a}},object:function(){var a=[],b;if("}"!==this.peekToken().text){do{if(this.peek("}"))break;b={type:q.Property,kind:"init"};this.peek().constant?(b.key=this.constant(),b.computed=!1,this.consume(":"),b.value=this.expression()):this.peek().identifier?(b.key=this.identifier(),b.computed=!1,this.peek(":")?(this.consume(":"),b.value=this.expression()):b.value=b.key):this.peek("[")?(this.consume("["),b.key=this.expression(),this.consume("]"),b.computed=!0,this.consume(":"), +b.value=this.expression()):this.throwError("invalid key",this.peek());a.push(b)}while(this.expect(","))}this.consume("}");return{type:q.ObjectExpression,properties:a}},throwError:function(a,b){throw Ya("syntax",b.text,a,b.index+1,this.text,this.text.substring(b.index));},consume:function(a){if(0===this.tokens.length)throw Ya("ueoe",this.text);var b=this.expect(a);b||this.throwError("is unexpected, expecting ["+a+"]",this.peek());return b},peekToken:function(){if(0===this.tokens.length)throw Ya("ueoe", +this.text);return this.tokens[0]},peek:function(a,b,d,c){return this.peekAhead(0,a,b,d,c)},peekAhead:function(a,b,d,c,e){if(this.tokens.length>a){a=this.tokens[a];var f=a.text;if(f===b||f===d||f===c||f===e||!(b||d||c||e))return a}return!1},expect:function(a,b,d,c){return(a=this.peek(a,b,d,c))?(this.tokens.shift(),a):!1},selfReferential:{"this":{type:q.ThisExpression},$locals:{type:q.LocalsExpression}}};var Hd=2;Ld.prototype={compile:function(a){var b=this;this.state={nextId:0,filters:{},fn:{vars:[], +body:[],own:{}},assign:{vars:[],body:[],own:{}},inputs:[]};Z(a,b.$filter);var d="",c;this.stage="assign";if(c=Kd(a))this.state.computing="assign",d=this.nextId(),this.recurse(c,d),this.return_(d),d="fn.assign="+this.generateFunction("assign","s,v,l");c=Id(a.body);b.stage="inputs";r(c,function(a,c){var d="fn"+c;b.state[d]={vars:[],body:[],own:{}};b.state.computing=d;var k=b.nextId();b.recurse(a,k);b.return_(k);b.state.inputs.push({name:d,isPure:a.isPure});a.watchId=c});this.state.computing="fn";this.stage= +"main";this.recurse(a);a='"'+this.USE+" "+this.STRICT+'";\n'+this.filterPrefix()+"var fn="+this.generateFunction("fn","s,l,a,i")+d+this.watchFns()+"return fn;";a=(new Function("$filter","getStringValue","ifDefined","plus",a))(this.$filter,Tg,Ug,Gd);this.state=this.stage=void 0;return a},USE:"use",STRICT:"strict",watchFns:function(){var a=[],b=this.state.inputs,d=this;r(b,function(b){a.push("var "+b.name+"="+d.generateFunction(b.name,"s"));b.isPure&&a.push(b.name,".isPure="+JSON.stringify(b.isPure)+ +";")});b.length&&a.push("fn.inputs=["+b.map(function(a){return a.name}).join(",")+"];");return a.join("")},generateFunction:function(a,b){return"function("+b+"){"+this.varsPrefix(a)+this.body(a)+"};"},filterPrefix:function(){var a=[],b=this;r(this.state.filters,function(d,c){a.push(d+"=$filter("+b.escape(c)+")")});return a.length?"var "+a.join(",")+";":""},varsPrefix:function(a){return this.state[a].vars.length?"var "+this.state[a].vars.join(",")+";":""},body:function(a){return this.state[a].body.join("")}, +recurse:function(a,b,d,c,e,f){var g,k,h=this,l,m,p;c=c||E;if(!f&&w(a.watchId))b=b||this.nextId(),this.if_("i",this.lazyAssign(b,this.computedMember("i",a.watchId)),this.lazyRecurse(a,b,d,c,e,!0));else switch(a.type){case q.Program:r(a.body,function(b,c){h.recurse(b.expression,void 0,void 0,function(a){k=a});c!==a.body.length-1?h.current().body.push(k,";"):h.return_(k)});break;case q.Literal:m=this.escape(a.value);this.assign(b,m);c(b||m);break;case q.UnaryExpression:this.recurse(a.argument,void 0, +void 0,function(a){k=a});m=a.operator+"("+this.ifDefined(k,0)+")";this.assign(b,m);c(m);break;case q.BinaryExpression:this.recurse(a.left,void 0,void 0,function(a){g=a});this.recurse(a.right,void 0,void 0,function(a){k=a});m="+"===a.operator?this.plus(g,k):"-"===a.operator?this.ifDefined(g,0)+a.operator+this.ifDefined(k,0):"("+g+")"+a.operator+"("+k+")";this.assign(b,m);c(m);break;case q.LogicalExpression:b=b||this.nextId();h.recurse(a.left,b);h.if_("&&"===a.operator?b:h.not(b),h.lazyRecurse(a.right, +b));c(b);break;case q.ConditionalExpression:b=b||this.nextId();h.recurse(a.test,b);h.if_(b,h.lazyRecurse(a.alternate,b),h.lazyRecurse(a.consequent,b));c(b);break;case q.Identifier:b=b||this.nextId();d&&(d.context="inputs"===h.stage?"s":this.assign(this.nextId(),this.getHasOwnProperty("l",a.name)+"?l:s"),d.computed=!1,d.name=a.name);h.if_("inputs"===h.stage||h.not(h.getHasOwnProperty("l",a.name)),function(){h.if_("inputs"===h.stage||"s",function(){e&&1!==e&&h.if_(h.isNull(h.nonComputedMember("s",a.name)), +h.lazyAssign(h.nonComputedMember("s",a.name),"{}"));h.assign(b,h.nonComputedMember("s",a.name))})},b&&h.lazyAssign(b,h.nonComputedMember("l",a.name)));c(b);break;case q.MemberExpression:g=d&&(d.context=this.nextId())||this.nextId();b=b||this.nextId();h.recurse(a.object,g,void 0,function(){h.if_(h.notNull(g),function(){a.computed?(k=h.nextId(),h.recurse(a.property,k),h.getStringValue(k),e&&1!==e&&h.if_(h.not(h.computedMember(g,k)),h.lazyAssign(h.computedMember(g,k),"{}")),m=h.computedMember(g,k),h.assign(b, +m),d&&(d.computed=!0,d.name=k)):(e&&1!==e&&h.if_(h.isNull(h.nonComputedMember(g,a.property.name)),h.lazyAssign(h.nonComputedMember(g,a.property.name),"{}")),m=h.nonComputedMember(g,a.property.name),h.assign(b,m),d&&(d.computed=!1,d.name=a.property.name))},function(){h.assign(b,"undefined")});c(b)},!!e);break;case q.CallExpression:b=b||this.nextId();a.filter?(k=h.filter(a.callee.name),l=[],r(a.arguments,function(a){var b=h.nextId();h.recurse(a,b);l.push(b)}),m=k+"("+l.join(",")+")",h.assign(b,m),c(b)): +(k=h.nextId(),g={},l=[],h.recurse(a.callee,k,g,function(){h.if_(h.notNull(k),function(){r(a.arguments,function(b){h.recurse(b,a.constant?void 0:h.nextId(),void 0,function(a){l.push(a)})});m=g.name?h.member(g.context,g.name,g.computed)+"("+l.join(",")+")":k+"("+l.join(",")+")";h.assign(b,m)},function(){h.assign(b,"undefined")});c(b)}));break;case q.AssignmentExpression:k=this.nextId();g={};this.recurse(a.left,void 0,g,function(){h.if_(h.notNull(g.context),function(){h.recurse(a.right,k);m=h.member(g.context, +g.name,g.computed)+a.operator+k;h.assign(b,m);c(b||m)})},1);break;case q.ArrayExpression:l=[];r(a.elements,function(b){h.recurse(b,a.constant?void 0:h.nextId(),void 0,function(a){l.push(a)})});m="["+l.join(",")+"]";this.assign(b,m);c(b||m);break;case q.ObjectExpression:l=[];p=!1;r(a.properties,function(a){a.computed&&(p=!0)});p?(b=b||this.nextId(),this.assign(b,"{}"),r(a.properties,function(a){a.computed?(g=h.nextId(),h.recurse(a.key,g)):g=a.key.type===q.Identifier?a.key.name:""+a.key.value;k=h.nextId(); +h.recurse(a.value,k);h.assign(h.member(b,g,a.computed),k)})):(r(a.properties,function(b){h.recurse(b.value,a.constant?void 0:h.nextId(),void 0,function(a){l.push(h.escape(b.key.type===q.Identifier?b.key.name:""+b.key.value)+":"+a)})}),m="{"+l.join(",")+"}",this.assign(b,m));c(b||m);break;case q.ThisExpression:this.assign(b,"s");c(b||"s");break;case q.LocalsExpression:this.assign(b,"l");c(b||"l");break;case q.NGValueParameter:this.assign(b,"v"),c(b||"v")}},getHasOwnProperty:function(a,b){var d=a+"."+ +b,c=this.current().own;c.hasOwnProperty(d)||(c[d]=this.nextId(!1,a+"&&("+this.escape(b)+" in "+a+")"));return c[d]},assign:function(a,b){if(a)return this.current().body.push(a,"=",b,";"),a},filter:function(a){this.state.filters.hasOwnProperty(a)||(this.state.filters[a]=this.nextId(!0));return this.state.filters[a]},ifDefined:function(a,b){return"ifDefined("+a+","+this.escape(b)+")"},plus:function(a,b){return"plus("+a+","+b+")"},return_:function(a){this.current().body.push("return ",a,";")},if_:function(a, +b,d){if(!0===a)b();else{var c=this.current().body;c.push("if(",a,"){");b();c.push("}");d&&(c.push("else{"),d(),c.push("}"))}},not:function(a){return"!("+a+")"},isNull:function(a){return a+"==null"},notNull:function(a){return a+"!=null"},nonComputedMember:function(a,b){var d=/[^$_a-zA-Z0-9]/g;return/^[$_a-zA-Z][$_a-zA-Z0-9]*$/.test(b)?a+"."+b:a+'["'+b.replace(d,this.stringEscapeFn)+'"]'},computedMember:function(a,b){return a+"["+b+"]"},member:function(a,b,d){return d?this.computedMember(a,b):this.nonComputedMember(a, +b)},getStringValue:function(a){this.assign(a,"getStringValue("+a+")")},lazyRecurse:function(a,b,d,c,e,f){var g=this;return function(){g.recurse(a,b,d,c,e,f)}},lazyAssign:function(a,b){var d=this;return function(){d.assign(a,b)}},stringEscapeRegex:/[^ a-zA-Z0-9]/g,stringEscapeFn:function(a){return"\\u"+("0000"+a.charCodeAt(0).toString(16)).slice(-4)},escape:function(a){if(C(a))return"'"+a.replace(this.stringEscapeRegex,this.stringEscapeFn)+"'";if(X(a))return a.toString();if(!0===a)return"true";if(!1=== +a)return"false";if(null===a)return"null";if("undefined"===typeof a)return"undefined";throw Ya("esc");},nextId:function(a,b){var d="v"+this.state.nextId++;a||this.current().vars.push(d+(b?"="+b:""));return d},current:function(){return this.state[this.state.computing]}};Md.prototype={compile:function(a){var b=this;Z(a,b.$filter);var d,c;if(d=Kd(a))c=this.recurse(d);d=Id(a.body);var e;d&&(e=[],r(d,function(a,c){var d=b.recurse(a);d.isPure=a.isPure;a.input=d;e.push(d);a.watchId=c}));var f=[];r(a.body, +function(a){f.push(b.recurse(a.expression))});a=0===a.body.length?E:1===a.body.length?f[0]:function(a,b){var c;r(f,function(d){c=d(a,b)});return c};c&&(a.assign=function(a,b,d){return c(a,d,b)});e&&(a.inputs=e);return a},recurse:function(a,b,d){var c,e,f=this,g;if(a.input)return this.inputs(a.input,a.watchId);switch(a.type){case q.Literal:return this.value(a.value,b);case q.UnaryExpression:return e=this.recurse(a.argument),this["unary"+a.operator](e,b);case q.BinaryExpression:return c=this.recurse(a.left), +e=this.recurse(a.right),this["binary"+a.operator](c,e,b);case q.LogicalExpression:return c=this.recurse(a.left),e=this.recurse(a.right),this["binary"+a.operator](c,e,b);case q.ConditionalExpression:return this["ternary?:"](this.recurse(a.test),this.recurse(a.alternate),this.recurse(a.consequent),b);case q.Identifier:return f.identifier(a.name,b,d);case q.MemberExpression:return c=this.recurse(a.object,!1,!!d),a.computed||(e=a.property.name),a.computed&&(e=this.recurse(a.property)),a.computed?this.computedMember(c, +e,b,d):this.nonComputedMember(c,e,b,d);case q.CallExpression:return g=[],r(a.arguments,function(a){g.push(f.recurse(a))}),a.filter&&(e=this.$filter(a.callee.name)),a.filter||(e=this.recurse(a.callee,!0)),a.filter?function(a,c,d,f){for(var p=[],n=0;n":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)>b(c,e,f,g);return d?{value:c}:c}},"binary<=":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)<=b(c,e,f,g);return d?{value:c}:c}},"binary>=":function(a,b,d){return function(c,e,f,g){c= +a(c,e,f,g)>=b(c,e,f,g);return d?{value:c}:c}},"binary&&":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)&&b(c,e,f,g);return d?{value:c}:c}},"binary||":function(a,b,d){return function(c,e,f,g){c=a(c,e,f,g)||b(c,e,f,g);return d?{value:c}:c}},"ternary?:":function(a,b,d,c){return function(e,f,g,k){e=a(e,f,g,k)?b(e,f,g,k):d(e,f,g,k);return c?{value:e}:e}},value:function(a,b){return function(){return b?{context:void 0,name:void 0,value:a}:a}},identifier:function(a,b,d){return function(c,e,f,g){c= +e&&a in e?e:c;d&&1!==d&&c&&null==c[a]&&(c[a]={});e=c?c[a]:void 0;return b?{context:c,name:a,value:e}:e}},computedMember:function(a,b,d,c){return function(e,f,g,k){var h=a(e,f,g,k),l,m;null!=h&&(l=b(e,f,g,k),l+="",c&&1!==c&&h&&!h[l]&&(h[l]={}),m=h[l]);return d?{context:h,name:l,value:m}:m}},nonComputedMember:function(a,b,d,c){return function(e,f,g,k){e=a(e,f,g,k);c&&1!==c&&e&&null==e[b]&&(e[b]={});f=null!=e?e[b]:void 0;return d?{context:e,name:b,value:f}:f}},inputs:function(a,b){return function(d, +c,e,f){return f?f[b]:a(d,c,e)}}};Nb.prototype={constructor:Nb,parse:function(a){a=this.getAst(a);var b=this.astCompiler.compile(a.ast),d=a.ast;b.literal=0===d.body.length||1===d.body.length&&(d.body[0].expression.type===q.Literal||d.body[0].expression.type===q.ArrayExpression||d.body[0].expression.type===q.ObjectExpression);b.constant=a.ast.constant;b.oneTime=a.oneTime;return b},getAst:function(a){var b=!1;a=a.trim();":"===a.charAt(0)&&":"===a.charAt(1)&&(b=!0,a=a.substring(2));return{ast:this.ast.ast(a), +oneTime:b}}};var Ea=F("$sce"),W={HTML:"html",CSS:"css",MEDIA_URL:"mediaUrl",URL:"url",RESOURCE_URL:"resourceUrl",JS:"js"},Dc=/_([a-z])/g,Zg=F("$templateRequest"),$g=F("$timeout"),aa=z.document.createElement("a"),Qd=ga(z.location.href),Na;aa.href="http://[::1]";var ah="[::1]"===aa.hostname;Rd.$inject=["$document"];fd.$inject=["$provide"];var Yd=22,Xd=".",Fc="0";Sd.$inject=["$locale"];Ud.$inject=["$locale"];var lh={yyyy:ea("FullYear",4,0,!1,!0),yy:ea("FullYear",2,0,!0,!0),y:ea("FullYear",1,0,!1,!0), +MMMM:lb("Month"),MMM:lb("Month",!0),MM:ea("Month",2,1),M:ea("Month",1,1),LLLL:lb("Month",!1,!0),dd:ea("Date",2),d:ea("Date",1),HH:ea("Hours",2),H:ea("Hours",1),hh:ea("Hours",2,-12),h:ea("Hours",1,-12),mm:ea("Minutes",2),m:ea("Minutes",1),ss:ea("Seconds",2),s:ea("Seconds",1),sss:ea("Milliseconds",3),EEEE:lb("Day"),EEE:lb("Day",!0),a:function(a,b){return 12>a.getHours()?b.AMPMS[0]:b.AMPMS[1]},Z:function(a,b,d){a=-1*d;return a=(0<=a?"+":"")+(Pb(Math[0=a.getFullYear()?b.ERANAMES[0]:b.ERANAMES[1]}},kh=/((?:[^yMLdHhmsaZEwG']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|L+|d+|H+|h+|m+|s+|a|Z|G+|w+))([\s\S]*)/,jh=/^-?\d+$/;Td.$inject=["$locale"];var eh=ia(K),fh=ia(vb);Vd.$inject=["$parse"];var Re=ia({restrict:"E",compile:function(a,b){if(!b.href&&!b.xlinkHref)return function(a,b){if("a"===b[0].nodeName.toLowerCase()){var e="[object SVGAnimatedString]"===la.call(b.prop("href"))?"xlink:href":"href"; +b.on("click",function(a){b.attr(e)||a.preventDefault()})}}}}),wb={};r(Hb,function(a,b){function d(a,d,e){a.$watch(e[c],function(a){e.$set(b,!!a)})}if("multiple"!==a){var c=xa("ng-"+b),e=d;"checked"===a&&(e=function(a,b,e){e.ngModel!==e[c]&&d(a,b,e)});wb[c]=function(){return{restrict:"A",priority:100,link:e}}}});r(vd,function(a,b){wb[b]=function(){return{priority:100,link:function(a,c,e){if("ngPattern"===b&&"/"===e.ngPattern.charAt(0)&&(c=e.ngPattern.match(ke))){e.$set("ngPattern",new RegExp(c[1], +c[2]));return}a.$watch(e[b],function(a){e.$set(b,a)})}}}});r(["src","srcset","href"],function(a){var b=xa("ng-"+a);wb[b]=["$sce",function(d){return{priority:99,link:function(c,e,f){var g=a,k=a;"href"===a&&"[object SVGAnimatedString]"===la.call(e.prop("href"))&&(k="xlinkHref",f.$attr[k]="xlink:href",g=null);f.$set(b,d.getTrustedMediaUrl(f[b]));f.$observe(b,function(b){b?(f.$set(k,b),wa&&g&&e.prop(g,f[k])):"href"===a&&f.$set(k,null)})}}}]});var mb={$addControl:E,$getControls:ia([]),$$renameControl:function(a, +b){a.$name=b},$removeControl:E,$setValidity:E,$setDirty:E,$setPristine:E,$setSubmitted:E,$$setSubmitted:E};Qb.$inject=["$element","$attrs","$scope","$animate","$interpolate"];Qb.prototype={$rollbackViewValue:function(){r(this.$$controls,function(a){a.$rollbackViewValue()})},$commitViewValue:function(){r(this.$$controls,function(a){a.$commitViewValue()})},$addControl:function(a){Ja(a.$name,"input");this.$$controls.push(a);a.$name&&(this[a.$name]=a);a.$$parentForm=this},$getControls:function(){return ja(this.$$controls)}, +$$renameControl:function(a,b){var d=a.$name;this[d]===a&&delete this[d];this[b]=a;a.$name=b},$removeControl:function(a){a.$name&&this[a.$name]===a&&delete this[a.$name];r(this.$pending,function(b,d){this.$setValidity(d,null,a)},this);r(this.$error,function(b,d){this.$setValidity(d,null,a)},this);r(this.$$success,function(b,d){this.$setValidity(d,null,a)},this);cb(this.$$controls,a);a.$$parentForm=mb},$setDirty:function(){this.$$animate.removeClass(this.$$element,Za);this.$$animate.addClass(this.$$element, +Wb);this.$dirty=!0;this.$pristine=!1;this.$$parentForm.$setDirty()},$setPristine:function(){this.$$animate.setClass(this.$$element,Za,Wb+" ng-submitted");this.$dirty=!1;this.$pristine=!0;this.$submitted=!1;r(this.$$controls,function(a){a.$setPristine()})},$setUntouched:function(){r(this.$$controls,function(a){a.$setUntouched()})},$setSubmitted:function(){for(var a=this;a.$$parentForm&&a.$$parentForm!==mb;)a=a.$$parentForm;a.$$setSubmitted()},$$setSubmitted:function(){this.$$animate.addClass(this.$$element, +"ng-submitted");this.$submitted=!0;r(this.$$controls,function(a){a.$$setSubmitted&&a.$$setSubmitted()})}};ce({clazz:Qb,set:function(a,b,d){var c=a[b];c?-1===c.indexOf(d)&&c.push(d):a[b]=[d]},unset:function(a,b,d){var c=a[b];c&&(cb(c,d),0===c.length&&delete a[b])}});var oe=function(a){return["$timeout","$parse",function(b,d){function c(a){return""===a?d('this[""]').assign:d(a).assign||E}return{name:"form",restrict:a?"EAC":"E",require:["form","^^?form"],controller:Qb,compile:function(d,f){d.addClass(Za).addClass(nb); +var g=f.name?"name":a&&f.ngForm?"ngForm":!1;return{pre:function(a,d,e,f){var p=f[0];if(!("action"in e)){var n=function(b){a.$apply(function(){p.$commitViewValue();p.$setSubmitted()});b.preventDefault()};d[0].addEventListener("submit",n);d.on("$destroy",function(){b(function(){d[0].removeEventListener("submit",n)},0,!1)})}(f[1]||p.$$parentForm).$addControl(p);var s=g?c(p.$name):E;g&&(s(a,p),e.$observe(g,function(b){p.$name!==b&&(s(a,void 0),p.$$parentForm.$$renameControl(p,b),s=c(p.$name),s(a,p))})); +d.on("$destroy",function(){p.$$parentForm.$removeControl(p);s(a,void 0);S(p,mb)})}}}}}]},Se=oe(),df=oe(!0),mh=/^\d{4,}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+(?:[+-][0-2]\d:[0-5]\d|Z)$/,xh=/^[a-z][a-z\d.+-]*:\/*(?:[^:@]+(?::[^@]+)?@)?(?:[^\s:/?#]+|\[[a-f\d:]+])(?::\d+)?(?:\/[^?#]*)?(?:\?[^#]*)?(?:#.*)?$/i,yh=/^(?=.{1,254}$)(?=.{1,64}@)[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+(\.[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+)*@[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$/, +nh=/^\s*(-|\+)?(\d+|(\d*(\.\d*)))([eE][+-]?\d+)?\s*$/,pe=/^(\d{4,})-(\d{2})-(\d{2})$/,qe=/^(\d{4,})-(\d\d)-(\d\d)T(\d\d):(\d\d)(?::(\d\d)(\.\d{1,3})?)?$/,Oc=/^(\d{4,})-W(\d\d)$/,re=/^(\d{4,})-(\d\d)$/,se=/^(\d\d):(\d\d)(?::(\d\d)(\.\d{1,3})?)?$/,ee=T();r(["date","datetime-local","month","time","week"],function(a){ee[a]=!0});var te={text:function(a,b,d,c,e,f){Sa(a,b,d,c,e,f);Ic(c)},date:ob("date",pe,Rb(pe,["yyyy","MM","dd"]),"yyyy-MM-dd"),"datetime-local":ob("datetimelocal",qe,Rb(qe,"yyyy MM dd HH mm ss sss".split(" ")), +"yyyy-MM-ddTHH:mm:ss.sss"),time:ob("time",se,Rb(se,["HH","mm","ss","sss"]),"HH:mm:ss.sss"),week:ob("week",Oc,function(a,b){if(ha(a))return a;if(C(a)){Oc.lastIndex=0;var d=Oc.exec(a);if(d){var c=+d[1],e=+d[2],f=d=0,g=0,k=0,h=Zd(c),e=7*(e-1);b&&(d=b.getHours(),f=b.getMinutes(),g=b.getSeconds(),k=b.getMilliseconds());return new Date(c,0,h.getDate()+e,d,f,g,k)}}return NaN},"yyyy-Www"),month:ob("month",re,Rb(re,["yyyy","MM"]),"yyyy-MM"),number:function(a,b,d,c,e,f,g,k){Jc(a,b,d,c,"number");fe(c);Sa(a, +b,d,c,e,f);var h;if(w(d.min)||d.ngMin){var l=d.min||k(d.ngMin)(a);h=na(l);c.$validators.min=function(a,b){return c.$isEmpty(b)||A(h)||b>=h};d.$observe("min",function(a){a!==l&&(h=na(a),l=a,c.$validate())})}if(w(d.max)||d.ngMax){var m=d.max||k(d.ngMax)(a),p=na(m);c.$validators.max=function(a,b){return c.$isEmpty(b)||A(p)||b<=p};d.$observe("max",function(a){a!==m&&(p=na(a),m=a,c.$validate())})}if(w(d.step)||d.ngStep){var n=d.step||k(d.ngStep)(a),s=na(n);c.$validators.step=function(a,b){return c.$isEmpty(b)|| +A(s)||ge(b,h||0,s)};d.$observe("step",function(a){a!==n&&(s=na(a),n=a,c.$validate())})}},url:function(a,b,d,c,e,f){Sa(a,b,d,c,e,f);Ic(c);c.$validators.url=function(a,b){var d=a||b;return c.$isEmpty(d)||xh.test(d)}},email:function(a,b,d,c,e,f){Sa(a,b,d,c,e,f);Ic(c);c.$validators.email=function(a,b){var d=a||b;return c.$isEmpty(d)||yh.test(d)}},radio:function(a,b,d,c){var e=!d.ngTrim||"false"!==V(d.ngTrim);A(d.name)&&b.attr("name",++qb);b.on("change",function(a){var g;b[0].checked&&(g=d.value,e&&(g= +V(g)),c.$setViewValue(g,a&&a.type))});c.$render=function(){var a=d.value;e&&(a=V(a));b[0].checked=a===c.$viewValue};d.$observe("value",c.$render)},range:function(a,b,d,c,e,f){function g(a,c){b.attr(a,d[a]);var e=d[a];d.$observe(a,function(a){a!==e&&(e=a,c(a))})}function k(a){p=na(a);Y(c.$modelValue)||(m?(a=b.val(),p>a&&(a=p,b.val(a)),c.$setViewValue(a)):c.$validate())}function h(a){n=na(a);Y(c.$modelValue)||(m?(a=b.val(),n=p},g("min",k)); +e&&(n=na(d.max),c.$validators.max=m?function(){return!0}:function(a,b){return c.$isEmpty(b)||A(n)||b<=n},g("max",h));f&&(s=na(d.step),c.$validators.step=m?function(){return!r.stepMismatch}:function(a,b){return c.$isEmpty(b)||A(s)||ge(b,p||0,s)},g("step",l))},checkbox:function(a,b,d,c,e,f,g,k){var h=he(k,a,"ngTrueValue",d.ngTrueValue,!0),l=he(k,a,"ngFalseValue",d.ngFalseValue,!1);b.on("change",function(a){c.$setViewValue(b[0].checked,a&&a.type)});c.$render=function(){b[0].checked=c.$viewValue};c.$isEmpty= +function(a){return!1===a};c.$formatters.push(function(a){return va(a,h)});c.$parsers.push(function(a){return a?h:l})},hidden:E,button:E,submit:E,reset:E,file:E},$c=["$browser","$sniffer","$filter","$parse",function(a,b,d,c){return{restrict:"E",require:["?ngModel"],link:{pre:function(e,f,g,k){k[0]&&(te[K(g.type)]||te.text)(e,f,g,k[0],b,a,d,c)}}}}],Af=function(){var a={configurable:!0,enumerable:!1,get:function(){return this.getAttribute("value")||""},set:function(a){this.setAttribute("value",a)}}; +return{restrict:"E",priority:200,compile:function(b,d){if("hidden"===K(d.type))return{pre:function(b,d,f,g){b=d[0];b.parentNode&&b.parentNode.insertBefore(b,b.nextSibling);Object.defineProperty&&Object.defineProperty(b,"value",a)}}}}},zh=/^(true|false|\d+)$/,xf=function(){function a(a,d,c){var e=w(c)?c:9===wa?"":null;a.prop("value",e);d.$set("value",c)}return{restrict:"A",priority:100,compile:function(b,d){return zh.test(d.ngValue)?function(b,d,f){b=b.$eval(f.ngValue);a(d,f,b)}:function(b,d,f){b.$watch(f.ngValue, +function(b){a(d,f,b)})}}}},We=["$compile",function(a){return{restrict:"AC",compile:function(b){a.$$addBindingClass(b);return function(b,c,e){a.$$addBindingInfo(c,e.ngBind);c=c[0];b.$watch(e.ngBind,function(a){c.textContent=jc(a)})}}}}],Ye=["$interpolate","$compile",function(a,b){return{compile:function(d){b.$$addBindingClass(d);return function(c,d,f){c=a(d.attr(f.$attr.ngBindTemplate));b.$$addBindingInfo(d,c.expressions);d=d[0];f.$observe("ngBindTemplate",function(a){d.textContent=A(a)?"":a})}}}}], +Xe=["$sce","$parse","$compile",function(a,b,d){return{restrict:"A",compile:function(c,e){var f=b(e.ngBindHtml),g=b(e.ngBindHtml,function(b){return a.valueOf(b)});d.$$addBindingClass(c);return function(b,c,e){d.$$addBindingInfo(c,e.ngBindHtml);b.$watch(g,function(){var d=f(b);c.html(a.getTrustedHtml(d)||"")})}}}}],wf=ia({restrict:"A",require:"ngModel",link:function(a,b,d,c){c.$viewChangeListeners.push(function(){a.$eval(d.ngChange)})}}),Ze=Lc("",!0),af=Lc("Odd",0),$e=Lc("Even",1),bf=Ra({compile:function(a, +b){b.$set("ngCloak",void 0);a.removeClass("ng-cloak")}}),cf=[function(){return{restrict:"A",scope:!0,controller:"@",priority:500}}],ed={},Ah={blur:!0,focus:!0};r("click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave keydown keyup keypress submit focus blur copy cut paste".split(" "),function(a){var b=xa("ng-"+a);ed[b]=["$parse","$rootScope","$exceptionHandler",function(d,c,e){return sd(d,c,e,b,a,Ah[a])}]});var ff=["$animate","$compile",function(a,b){return{multiElement:!0, +transclude:"element",priority:600,terminal:!0,restrict:"A",$$tlb:!0,link:function(d,c,e,f,g){var k,h,l;d.$watch(e.ngIf,function(d){d?h||g(function(d,f){h=f;d[d.length++]=b.$$createComment("end ngIf",e.ngIf);k={clone:d};a.enter(d,c.parent(),c)}):(l&&(l.remove(),l=null),h&&(h.$destroy(),h=null),k&&(l=ub(k.clone),a.leave(l).done(function(a){!1!==a&&(l=null)}),k=null))})}}}],gf=["$templateRequest","$anchorScroll","$animate",function(a,b,d){return{restrict:"ECA",priority:400,terminal:!0,transclude:"element", +controller:ca.noop,compile:function(c,e){var f=e.ngInclude||e.src,g=e.onload||"",k=e.autoscroll;return function(c,e,m,p,n){var r=0,q,t,x,v=function(){t&&(t.remove(),t=null);q&&(q.$destroy(),q=null);x&&(d.leave(x).done(function(a){!1!==a&&(t=null)}),t=x,x=null)};c.$watch(f,function(f){var m=function(a){!1===a||!w(k)||k&&!c.$eval(k)||b()},t=++r;f?(a(f,!0).then(function(a){if(!c.$$destroyed&&t===r){var b=c.$new();p.template=a;a=n(b,function(a){v();d.enter(a,null,e).done(m)});q=b;x=a;q.$emit("$includeContentLoaded", +f);c.$eval(g)}},function(){c.$$destroyed||t!==r||(v(),c.$emit("$includeContentError",f))}),c.$emit("$includeContentRequested",f)):(v(),p.template=null)})}}}}],zf=["$compile",function(a){return{restrict:"ECA",priority:-400,require:"ngInclude",link:function(b,d,c,e){la.call(d[0]).match(/SVG/)?(d.empty(),a(gd(e.template,z.document).childNodes)(b,function(a){d.append(a)},{futureParentElement:d})):(d.html(e.template),a(d.contents())(b))}}}],hf=Ra({priority:450,compile:function(){return{pre:function(a, +b,d){a.$eval(d.ngInit)}}}}),vf=function(){return{restrict:"A",priority:100,require:"ngModel",link:function(a,b,d,c){var e=d.ngList||", ",f="false"!==d.ngTrim,g=f?V(e):e;c.$parsers.push(function(a){if(!A(a)){var b=[];a&&r(a.split(g),function(a){a&&b.push(f?V(a):a)});return b}});c.$formatters.push(function(a){if(H(a))return a.join(e)});c.$isEmpty=function(a){return!a||!a.length}}}},nb="ng-valid",be="ng-invalid",Za="ng-pristine",Wb="ng-dirty",pb=F("ngModel");Sb.$inject="$scope $exceptionHandler $attrs $element $parse $animate $timeout $q $interpolate".split(" "); +Sb.prototype={$$initGetterSetters:function(){if(this.$options.getOption("getterSetter")){var a=this.$$parse(this.$$attr.ngModel+"()"),b=this.$$parse(this.$$attr.ngModel+"($$$p)");this.$$ngModelGet=function(b){var c=this.$$parsedNgModel(b);B(c)&&(c=a(b));return c};this.$$ngModelSet=function(a,c){B(this.$$parsedNgModel(a))?b(a,{$$$p:c}):this.$$parsedNgModelAssign(a,c)}}else if(!this.$$parsedNgModel.assign)throw pb("nonassign",this.$$attr.ngModel,Aa(this.$$element));},$render:E,$isEmpty:function(a){return A(a)|| +""===a||null===a||a!==a},$$updateEmptyClasses:function(a){this.$isEmpty(a)?(this.$$animate.removeClass(this.$$element,"ng-not-empty"),this.$$animate.addClass(this.$$element,"ng-empty")):(this.$$animate.removeClass(this.$$element,"ng-empty"),this.$$animate.addClass(this.$$element,"ng-not-empty"))},$setPristine:function(){this.$dirty=!1;this.$pristine=!0;this.$$animate.removeClass(this.$$element,Wb);this.$$animate.addClass(this.$$element,Za)},$setDirty:function(){this.$dirty=!0;this.$pristine=!1;this.$$animate.removeClass(this.$$element, +Za);this.$$animate.addClass(this.$$element,Wb);this.$$parentForm.$setDirty()},$setUntouched:function(){this.$touched=!1;this.$untouched=!0;this.$$animate.setClass(this.$$element,"ng-untouched","ng-touched")},$setTouched:function(){this.$touched=!0;this.$untouched=!1;this.$$animate.setClass(this.$$element,"ng-touched","ng-untouched")},$rollbackViewValue:function(){this.$$timeout.cancel(this.$$pendingDebounce);this.$viewValue=this.$$lastCommittedViewValue;this.$render()},$validate:function(){if(!Y(this.$modelValue)){var a= +this.$$lastCommittedViewValue,b=this.$$rawModelValue,d=this.$valid,c=this.$modelValue,e=this.$options.getOption("allowInvalid"),f=this;this.$$runValidators(b,a,function(a){e||d===a||(f.$modelValue=a?b:void 0,f.$modelValue!==c&&f.$$writeModelToScope())})}},$$runValidators:function(a,b,d){function c(){var c=!0;r(h.$validators,function(d,e){var g=Boolean(d(a,b));c=c&&g;f(e,g)});return c?!0:(r(h.$asyncValidators,function(a,b){f(b,null)}),!1)}function e(){var c=[],d=!0;r(h.$asyncValidators,function(e, +g){var h=e(a,b);if(!h||!B(h.then))throw pb("nopromise",h);f(g,void 0);c.push(h.then(function(){f(g,!0)},function(){d=!1;f(g,!1)}))});c.length?h.$$q.all(c).then(function(){g(d)},E):g(!0)}function f(a,b){k===h.$$currentValidationRunId&&h.$setValidity(a,b)}function g(a){k===h.$$currentValidationRunId&&d(a)}this.$$currentValidationRunId++;var k=this.$$currentValidationRunId,h=this;(function(){var a=h.$$parserName;if(A(h.$$parserValid))f(a,null);else return h.$$parserValid||(r(h.$validators,function(a, +b){f(b,null)}),r(h.$asyncValidators,function(a,b){f(b,null)})),f(a,h.$$parserValid),h.$$parserValid;return!0})()?c()?e():g(!1):g(!1)},$commitViewValue:function(){var a=this.$viewValue;this.$$timeout.cancel(this.$$pendingDebounce);if(this.$$lastCommittedViewValue!==a||""===a&&this.$$hasNativeValidators)this.$$updateEmptyClasses(a),this.$$lastCommittedViewValue=a,this.$pristine&&this.$setDirty(),this.$$parseAndValidate()},$$parseAndValidate:function(){var a=this.$$lastCommittedViewValue,b=this;this.$$parserValid= +A(a)?void 0:!0;this.$setValidity(this.$$parserName,null);this.$$parserName="parse";if(this.$$parserValid)for(var d=0;dg||e.$isEmpty(b)||b.length<=g}}}}}],cd= +["$parse",function(a){return{restrict:"A",require:"?ngModel",link:function(b,d,c,e){if(e){var f=c.minlength||a(c.ngMinlength)(b),g=Ub(f)||-1;c.$observe("minlength",function(a){f!==a&&(g=Ub(a)||-1,f=a,e.$validate())});e.$validators.minlength=function(a,b){return e.$isEmpty(b)||b.length>=g}}}}}];z.angular.bootstrap?z.console&&console.log("WARNING: Tried to load AngularJS more than once."):(Je(),Oe(ca),ca.module("ngLocale",[],["$provide",function(a){function b(a){a+="";var b=a.indexOf(".");return-1== +b?0:a.length-b-1}a.value("$locale",{DATETIME_FORMATS:{AMPMS:["AM","PM"],DAY:"Sunday Monday Tuesday Wednesday Thursday Friday Saturday".split(" "),ERANAMES:["Before Christ","Anno Domini"],ERAS:["BC","AD"],FIRSTDAYOFWEEK:6,MONTH:"January February March April May June July August September October November December".split(" "),SHORTDAY:"Sun Mon Tue Wed Thu Fri Sat".split(" "),SHORTMONTH:"Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec".split(" "),STANDALONEMONTH:"January February March April May June July August September October November December".split(" "), +WEEKENDRANGE:[5,6],fullDate:"EEEE, MMMM d, y",longDate:"MMMM d, y",medium:"MMM d, y h:mm:ss a",mediumDate:"MMM d, y",mediumTime:"h:mm:ss a","short":"M/d/yy h:mm a",shortDate:"M/d/yy",shortTime:"h:mm a"},NUMBER_FORMATS:{CURRENCY_SYM:"$",DECIMAL_SEP:".",GROUP_SEP:",",PATTERNS:[{gSize:3,lgSize:3,maxFrac:3,minFrac:0,minInt:1,negPre:"-",negSuf:"",posPre:"",posSuf:""},{gSize:3,lgSize:3,maxFrac:2,minFrac:2,minInt:1,negPre:"-\u00a4",negSuf:"",posPre:"\u00a4",posSuf:""}]},id:"en-us",localeID:"en_US",pluralCat:function(a, +c){var e=a|0,f=c;void 0===f&&(f=Math.min(b(a),3));Math.pow(10,f);return 1==e&&0==f?"one":"other"}})}]),x(function(){Ee(z.document,Wc)}))})(window);!window.angular.$$csp().noInlineStyle&&window.angular.element(document.head).prepend(window.angular.element("`),t=this.attachShadow({mode:"open"});t.appendChild(e.content.cloneNode(!0)),t.addEventListener("move",this),this[bg]=this[vg].map((e=>new e(t)))}connectedCallback(){if(this.hasOwnProperty("color")){const e=this.color;delete this.color,this.color=e}else this.color||(this.color=this.colorModel.defaultColor)}attributeChangedCallback(e,t,i){const n=this.colorModel.fromAttr(i);this[mg](n)||(this.color=n)}handleEvent(e){const t=this[fg],i={...t,...e.detail};let n;this[pg](i),ng(i,t)||this[mg](n=this.colorModel.fromHsva(i))||(this[gg]=n,rg(this,"color-changed",{value:n}))}[mg](e){return this.color&&this.colorModel.equal(e,this.color)}[pg](e){this[fg]=e,this[bg].forEach((t=>t.update(e)))}}const yg={defaultColor:"#000",toHsva:e=>ig(Jm(e)),fromHsva:({h:e,s:t,v:i})=>tg(Xm({h:e,s:t,v:i,a:1})),equal:(e,t)=>e.toLowerCase()===t.toLowerCase()||ng(Jm(e),Jm(t)),fromAttr:e=>e};class kg extends _g{get colorModel(){return yg}}class Cg extends Du{constructor(e,t={}){super(e),this.set({color:"",_hexColor:""}),this.hexInputRow=this._createInputRow();const i=this.createCollection();t.hideInput||i.add(this.hexInputRow),this.setTemplate({tag:"div",attributes:{class:["ck","ck-color-picker"],tabindex:-1},children:i}),this._config=t,this._debounceColorPickerEvent=La((e=>{this.set("color",e),this.fire("colorSelected",{color:this.color})}),150,{leading:!0}),this.on("set:color",((e,t,i)=>{e.return=dm(i,this._config.format||"hsl")})),this.on("change:color",(()=>{this._hexColor=Ag(this.color)})),this.on("change:_hexColor",(()=>{document.activeElement!==this.picker&&this.picker.setAttribute("color",this._hexColor),Ag(this.color)!=Ag(this._hexColor)&&(this.color=this._hexColor)}))}render(){var e,t;if(super.render(),e="hex-color-picker",t=kg,void 0===customElements.get(e)&&customElements.define(e,t),this.picker=Mn.document.createElement("hex-color-picker"),this.picker.setAttribute("class","hex-color-picker"),this.picker.setAttribute("tabindex","-1"),this._createSlidersView(),this.element){this.hexInputRow.element?this.element.insertBefore(this.picker,this.hexInputRow.element):this.element.appendChild(this.picker);const e=document.createElement("style");e.textContent='[role="slider"]:focus [part$="pointer"] {border: 1px solid #fff;outline: 1px solid var(--ck-color-focus-border);box-shadow: 0 0 0 2px #fff;}',this.picker.shadowRoot.appendChild(e)}this.picker.addEventListener("color-changed",(e=>{const t=e.detail.value;this._debounceColorPickerEvent(t)}))}focus(){if(!this._config.hideInput&&(l.isGecko||l.isiOS||l.isSafari)){this.hexInputRow.children.get(1).focus()}this.slidersView.first.focus()}_createSlidersView(){const e=[...this.picker.shadowRoot.children].filter((e=>"slider"===e.getAttribute("role"))).map((e=>new xg(e)));this.slidersView=this.createCollection(),e.forEach((e=>{this.slidersView.add(e)}))}_createInputRow(){const e=new Eg,t=this._createColorInput();return new Tg(this.locale,[e,t])}_createColorInput(){const e=new um(this.locale,Um),{t}=this.locale;return e.set({label:t("HEX"),class:"color-picker-hex-input"}),e.fieldView.bind("value").to(this,"_hexColor",(t=>e.isFocused?e.fieldView.value:t.startsWith("#")?t.substring(1):t)),e.fieldView.on("input",(()=>{const t=e.fieldView.element.value;if(t){const e=t.trim(),i=e.startsWith("#")?e.substring(1):e;[3,4,6,8].includes(i.length)&&/(([0-9a-fA-F]{2}){3,4}|([0-9a-fA-F]){3,4})/.test(i)&&this._debounceColorPickerEvent("#"+i)}})),e}}function Ag(e){let t=function(e){if(!e)return"";const t=hm(e);return t?"hex"===t.space?t.hexValue:dm(e,"hex"):"#000"}(e);return t||(t="#000"),4===t.length&&(t="#"+[t[1],t[1],t[2],t[2],t[3],t[3]].join("")),t.toLowerCase()}class xg extends Du{constructor(e){super(),this.element=e}focus(){this.element.focus()}}class Eg extends Du{constructor(e){super(e),this.setTemplate({tag:"div",attributes:{class:["ck","ck-color-picker__hash-view"]},children:"#"})}}class Tg extends Du{constructor(e,t){super(e),this.children=this.createCollection(t),this.setTemplate({tag:"div",attributes:{class:["ck","ck-color-picker__row"]},children:this.children})}}class Sg extends(G(Ks)){constructor(e){super(e),this.set("isEmpty",!0),this.on("change",(()=>{this.set("isEmpty",0===this.length)}))}add(e,t){return this.find((t=>t.color===e.color))?this:super.add(e,t)}hasColor(e){return!!this.find((t=>t.color===e))}}const{eraser:Pg,colorPalette:Ig}=fu;class Vg extends Du{constructor(e,{colors:t,columns:i,removeButtonLabel:n,documentColorsLabel:s,documentColorsCount:o,colorPickerLabel:r,focusTracker:a,focusables:l}){super(e);const c=this.bindTemplate;this.set("isVisible",!0),this.focusTracker=a,this.items=this.createCollection(),this.colorDefinitions=t,this.columns=i,this.documentColors=new Sg,this.documentColorsCount=o,this._focusables=l,this._removeButtonLabel=n,this._colorPickerLabel=r,this._documentColorsLabel=s,this.setTemplate({tag:"div",attributes:{class:["ck-color-grids-fragment",c.if("isVisible","ck-hidden",(e=>!e))]},children:this.items}),this.removeColorButtonView=this._createRemoveColorButton(),this.items.add(this.removeColorButtonView)}updateDocumentColors(e,t){const i=e.document,n=this.documentColorsCount;this.documentColors.clear();for(const s of i.getRoots()){const i=e.createRangeIn(s);for(const e of i.getItems())if(e.is("$textProxy")&&e.hasAttribute(t)&&(this._addColorToDocumentColors(e.getAttribute(t)),this.documentColors.length>=n))return}}updateSelectedColors(){const e=this.documentColorsGrid,t=this.staticColorsGrid,i=this.selectedColor;t.selectedColor=i,e&&(e.selectedColor=i)}render(){if(super.render(),this.staticColorsGrid=this._createStaticColorsGrid(),this.items.add(this.staticColorsGrid),this.documentColorsCount){const e=bu.bind(this.documentColors,this.documentColors),t=new Lu(this.locale);t.text=this._documentColorsLabel,t.extendTemplate({attributes:{class:["ck","ck-color-grid__label",e.if("isEmpty","ck-hidden")]}}),this.items.add(t),this.documentColorsGrid=this._createDocumentColorsGrid(),this.items.add(this.documentColorsGrid)}this._createColorPickerButton(),this._addColorSelectorElementsToFocusTracker()}focus(){this.removeColorButtonView.focus()}destroy(){super.destroy()}addColorPickerButton(){this.colorPickerButtonView&&(this.items.add(this.colorPickerButtonView),this.focusTracker.add(this.colorPickerButtonView.element),this._focusables.add(this.colorPickerButtonView))}_addColorSelectorElementsToFocusTracker(){this.focusTracker.add(this.removeColorButtonView.element),this._focusables.add(this.removeColorButtonView),this.staticColorsGrid&&(this.focusTracker.add(this.staticColorsGrid.element),this._focusables.add(this.staticColorsGrid)),this.documentColorsGrid&&(this.focusTracker.add(this.documentColorsGrid.element),this._focusables.add(this.documentColorsGrid))}_createColorPickerButton(){this.colorPickerButtonView=new Ku,this.colorPickerButtonView.set({label:this._colorPickerLabel,withText:!0,icon:Ig,class:"ck-color-selector__color-picker"}),this.colorPickerButtonView.on("execute",(()=>{this.fire("colorPicker:show")}))}_createRemoveColorButton(){const e=new Ku;return e.set({withText:!0,icon:Pg,label:this._removeButtonLabel}),e.class="ck-color-selector__remove-color",e.on("execute",(()=>{this.fire("execute",{value:null,source:"removeColorButton"})})),e.render(),e}_createStaticColorsGrid(){const e=new sm(this.locale,{colorDefinitions:this.colorDefinitions,columns:this.columns});return e.on("execute",((e,t)=>{this.fire("execute",{value:t.value,source:"staticColorsGrid"})})),e}_createDocumentColorsGrid(){const e=bu.bind(this.documentColors,this.documentColors),t=new sm(this.locale,{columns:this.columns});return t.extendTemplate({attributes:{class:e.if("isEmpty","ck-hidden")}}),t.items.bindTo(this.documentColors).using((e=>{const t=new nm;return t.set({color:e.color,hasBorder:e.options&&e.options.hasBorder}),e.label&&t.set({label:e.label,tooltip:!0}),t.on("execute",(()=>{this.fire("execute",{value:e.color,source:"documentColorsGrid"})})),t})),this.documentColors.on("change:isEmpty",((e,i,n)=>{n&&(t.selectedColor=null)})),t}_addColorToDocumentColors(e){const t=this.colorDefinitions.find((t=>t.color===e));t?this.documentColors.add(Object.assign({},t)):this.documentColors.add({color:e,label:e,options:{hasBorder:!1}})}}class Rg extends Du{constructor(e,{focusTracker:t,focusables:i,keystrokes:n,colorPickerViewConfig:s}){super(e),this.items=this.createCollection(),this.focusTracker=t,this.keystrokes=n,this.set("isVisible",!1),this.set("selectedColor",void 0),this._focusables=i,this._colorPickerViewConfig=s;const o=this.bindTemplate,{saveButtonView:r,cancelButtonView:a}=this._createActionButtons();this.saveButtonView=r,this.cancelButtonView=a,this.actionBarView=this._createActionBarView({saveButtonView:r,cancelButtonView:a}),this.setTemplate({tag:"div",attributes:{class:["ck-color-picker-fragment",o.if("isVisible","ck-hidden",(e=>!e))]},children:this.items})}render(){super.render();const e=new Cg(this.locale,{...this._colorPickerViewConfig});this.colorPickerView=e,this.colorPickerView.render(),this.selectedColor&&(e.color=this.selectedColor),this.listenTo(this,"change:selectedColor",((t,i,n)=>{e.color=n})),this.items.add(this.colorPickerView),this.items.add(this.actionBarView),this._addColorPickersElementsToFocusTracker(),this._stopPropagationOnArrowsKeys(),this._executeOnEnterPress(),this._executeUponColorChange()}destroy(){super.destroy()}focus(){this.colorPickerView.focus()}_executeOnEnterPress(){this.keystrokes.set("enter",(e=>{this.isVisible&&this.focusTracker.focusedElement!==this.cancelButtonView.element&&(this.fire("execute",{value:this.selectedColor}),e.stopPropagation(),e.preventDefault())}))}_stopPropagationOnArrowsKeys(){const e=e=>e.stopPropagation();this.keystrokes.set("arrowright",e),this.keystrokes.set("arrowleft",e),this.keystrokes.set("arrowup",e),this.keystrokes.set("arrowdown",e)}_addColorPickersElementsToFocusTracker(){for(const e of this.colorPickerView.slidersView)this.focusTracker.add(e.element),this._focusables.add(e);const e=this.colorPickerView.hexInputRow.children.get(1);e.element&&(this.focusTracker.add(e.element),this._focusables.add(e)),this.focusTracker.add(this.saveButtonView.element),this._focusables.add(this.saveButtonView),this.focusTracker.add(this.cancelButtonView.element),this._focusables.add(this.cancelButtonView)}_createActionBarView({saveButtonView:e,cancelButtonView:t}){const i=new Du,n=this.createCollection();return n.add(e),n.add(t),i.setTemplate({tag:"div",attributes:{class:["ck","ck-color-selector_action-bar"]},children:n}),i}_createActionButtons(){const e=this.locale,t=e.t,i=new Ku(e),n=new Ku(e);return i.set({icon:fu.check,class:"ck-button-save",type:"button",withText:!1,label:t("Accept")}),n.set({icon:fu.cancel,class:"ck-button-cancel",type:"button",withText:!1,label:t("Cancel")}),i.on("execute",(()=>{this.fire("execute",{source:"colorPickerSaveButton",value:this.selectedColor})})),n.on("execute",(()=>{this.fire("colorPicker:cancel")})),{saveButtonView:i,cancelButtonView:n}}_executeUponColorChange(){this.colorPickerView.on("colorSelected",((e,t)=>{this.fire("execute",{value:t.color,source:"colorPicker"}),this.set("selectedColor",t.color)}))}}class Og extends Du{constructor(e,{colors:t,columns:i,removeButtonLabel:n,documentColorsLabel:s,documentColorsCount:o,colorPickerLabel:r,colorPickerViewConfig:a}){super(e),this.items=this.createCollection(),this.focusTracker=new Js,this.keystrokes=new Qs,this._focusables=new pu,this._colorPickerViewConfig=a,this._focusCycler=new ym({focusables:this._focusables,focusTracker:this.focusTracker,keystrokeHandler:this.keystrokes,actions:{focusPrevious:"shift + tab",focusNext:"tab"}}),this.colorGridsFragmentView=new Vg(e,{colors:t,columns:i,removeButtonLabel:n,documentColorsLabel:s,documentColorsCount:o,colorPickerLabel:r,focusTracker:this.focusTracker,focusables:this._focusables}),this.colorPickerFragmentView=new Rg(e,{focusables:this._focusables,focusTracker:this.focusTracker,keystrokes:this.keystrokes,colorPickerViewConfig:a}),this.set("_isColorGridsFragmentVisible",!0),this.set("_isColorPickerFragmentVisible",!1),this.set("selectedColor",void 0),this.colorGridsFragmentView.bind("isVisible").to(this,"_isColorGridsFragmentVisible"),this.colorPickerFragmentView.bind("isVisible").to(this,"_isColorPickerFragmentVisible"),this.on("change:selectedColor",((e,t,i)=>{this.colorGridsFragmentView.set("selectedColor",i),this.colorPickerFragmentView.set("selectedColor",i)})),this.colorGridsFragmentView.on("change:selectedColor",((e,t,i)=>{this.set("selectedColor",i)})),this.colorPickerFragmentView.on("change:selectedColor",((e,t,i)=>{this.set("selectedColor",i)})),this.setTemplate({tag:"div",attributes:{class:["ck","ck-color-selector"]},children:this.items})}render(){super.render(),this.keystrokes.listenTo(this.element)}destroy(){super.destroy(),this.focusTracker.destroy(),this.keystrokes.destroy()}appendUI(){this._appendColorGridsFragment(),this._colorPickerViewConfig&&this._appendColorPickerFragment()}showColorPickerFragment(){this.colorPickerFragmentView.colorPickerView&&!this._isColorPickerFragmentVisible&&(this._isColorPickerFragmentVisible=!0,this.colorPickerFragmentView.focus(),this._isColorGridsFragmentVisible=!1)}showColorGridsFragment(){this._isColorGridsFragmentVisible||(this._isColorGridsFragmentVisible=!0,this.colorGridsFragmentView.focus(),this._isColorPickerFragmentVisible=!1)}focus(){this._focusCycler.focusFirst()}focusLast(){this._focusCycler.focusLast()}updateDocumentColors(e,t){this.colorGridsFragmentView.updateDocumentColors(e,t)}updateSelectedColors(){this.colorGridsFragmentView.updateSelectedColors()}_appendColorGridsFragment(){this.items.length||(this.items.add(this.colorGridsFragmentView),this.colorGridsFragmentView.delegate("execute").to(this),this.colorGridsFragmentView.delegate("colorPicker:show").to(this))}_appendColorPickerFragment(){2!==this.items.length&&(this.items.add(this.colorPickerFragmentView),this.colorGridsFragmentView.colorPickerButtonView&&this.colorGridsFragmentView.colorPickerButtonView.on("execute",(()=>{this.showColorPickerFragment()})),this.colorGridsFragmentView.addColorPickerButton(),this.colorPickerFragmentView.delegate("execute").to(this),this.colorPickerFragmentView.delegate("colorPicker:cancel").to(this))}}class Bg{constructor(e){this._components=new Map,this.editor=e}*names(){for(const e of this._components.values())yield e.originalName}add(e,t){this._components.set(Mg(e),{callback:t,originalName:e})}create(e){if(!this.has(e))throw new y("componentfactory-item-missing",this,{name:e});return this._components.get(Mg(e)).callback(this.editor.locale)}has(e){return this._components.has(Mg(e))}}function Mg(e){return String(e).toLowerCase()}class Ng extends Du{constructor(e,t={}){super(e);const i=this.bindTemplate;this.set("label",t.label||""),this.set("class",t.class||null),this.children=this.createCollection(),this.setTemplate({tag:"div",attributes:{class:["ck","ck-form__header",i.to("class")]},children:this.children}),t.icon&&(this.iconView=new qu,this.iconView.content=t.icon,this.children.add(this.iconView));const n=new Du(e);n.setTemplate({tag:"h2",attributes:{class:["ck","ck-form__header__label"],role:"presentation"},children:[{text:i.to("label")}]}),this.children.add(n)}}class Fg extends Du{constructor(e){super(e),this.children=this.createCollection(),this.keystrokes=new Qs,this._focusTracker=new Js,this._focusables=new pu,this.focusCycler=new ym({focusables:this._focusables,focusTracker:this._focusTracker,keystrokeHandler:this.keystrokes,actions:{focusPrevious:"shift + tab",focusNext:"tab"}}),this.setTemplate({tag:"div",attributes:{class:["ck","ck-dialog__actions"]},children:this.children})}render(){super.render(),this.keystrokes.listenTo(this.element)}setButtons(e){for(const t of e){const e=new Ku(this.locale);let i;for(i in e.on("execute",(()=>t.onExecute())),t.onCreate&&t.onCreate(e),t)"onExecute"!=i&&"onCreate"!=i&&e.set(i,t[i]);this.children.add(e)}this._updateFocusCyclableItems()}focus(e){-1===e?this.focusCycler.focusLast():this.focusCycler.focusFirst()}_updateFocusCyclableItems(){Array.from(this.children).forEach((e=>{this._focusables.add(e),this._focusTracker.add(e.element)}))}}class Dg extends Du{constructor(e){super(e),this.children=this.createCollection(),this.setTemplate({tag:"div",attributes:{class:["ck","ck-dialog__content"]},children:this.children})}reset(){for(;this.children.length;)this.children.remove(0)}}const Lg="screen-center",zg="editor-center",Hg="editor-top-side",$g="editor-top-center",Wg="editor-bottom-center",jg="editor-above-center",Ug="editor-below-center",qg=Zn("px");class Gg extends(function(e){return class extends e{constructor(...e){super(...e),this._onDragBound=this._onDrag.bind(this),this._onDragEndBound=this._onDragEnd.bind(this),this._lastDraggingCoordinates={x:0,y:0},this.on("render",(()=>{this._attachListeners()})),this.set("isDragging",!1)}_attachListeners(){this.listenTo(this.element,"mousedown",this._onDragStart.bind(this)),this.listenTo(this.element,"touchstart",this._onDragStart.bind(this))}_attachDragListeners(){this.listenTo(Mn.document,"mouseup",this._onDragEndBound),this.listenTo(Mn.document,"touchend",this._onDragEndBound),this.listenTo(Mn.document,"mousemove",this._onDragBound),this.listenTo(Mn.document,"touchmove",this._onDragBound)}_detachDragListeners(){this.stopListening(Mn.document,"mouseup",this._onDragEndBound),this.stopListening(Mn.document,"touchend",this._onDragEndBound),this.stopListening(Mn.document,"mousemove",this._onDragBound),this.stopListening(Mn.document,"touchmove",this._onDragBound)}_onDragStart(e,t){if(!this._isHandleElementPressed(t))return;this._attachDragListeners();let i=0,n=0;t instanceof MouseEvent?(i=t.clientX,n=t.clientY):(i=t.touches[0].clientX,n=t.touches[0].clientY),this._lastDraggingCoordinates={x:i,y:n},this.isDragging=!0}_onDrag(e,t){if(!this.isDragging)return void this._detachDragListeners();let i=0,n=0;t instanceof MouseEvent?(i=t.clientX,n=t.clientY):(i=t.touches[0].clientX,n=t.touches[0].clientY),t.preventDefault(),this.fire("drag",{deltaX:Math.round(i-this._lastDraggingCoordinates.x),deltaY:Math.round(n-this._lastDraggingCoordinates.y)}),this._lastDraggingCoordinates={x:i,y:n}}_onDragEnd(){this._detachDragListeners(),this.isDragging=!1}_isHandleElementPressed(e){return!!this.dragHandleElement&&(this.dragHandleElement===e.target||e.target instanceof HTMLElement&&this.dragHandleElement.contains(e.target))}}}(Du)){constructor(e,{getCurrentDomRoot:t,getViewportOffset:i}){super(e),this.wasMoved=!1;const n=this.bindTemplate,s=e.t;this.set("className",""),this.set("ariaLabel",s("Editor dialog")),this.set("isModal",!1),this.set("position",Lg),this.set("_isVisible",!1),this.set("_isTransparent",!1),this.set("_top",0),this.set("_left",0),this._getCurrentDomRoot=t,this._getViewportOffset=i,this.decorate("moveTo"),this.parts=this.createCollection(),this.keystrokes=new Qs,this.focusTracker=new Js,this._focusables=new pu,this._focusCycler=new ym({focusables:this._focusables,focusTracker:this.focusTracker,keystrokeHandler:this.keystrokes,actions:{focusPrevious:"shift + tab",focusNext:"tab"}}),this.setTemplate({tag:"div",attributes:{class:["ck","ck-dialog-overlay",n.if("isModal","ck-dialog-overlay__transparent",(e=>!e)),n.if("_isVisible","ck-hidden",(e=>!e))],tabindex:"-1"},children:[{tag:"div",attributes:{tabindex:"-1",class:["ck","ck-dialog",n.to("className")],role:"dialog","aria-label":n.to("ariaLabel"),style:{top:n.to("_top",(e=>qg(e))),left:n.to("_left",(e=>qg(e))),visibility:n.if("_isTransparent","hidden")}},children:this.parts}]})}render(){super.render(),this.keystrokes.set("Esc",((e,t)=>{this.fire("close",{source:"escKeyPress"}),t()})),this.on("drag",((e,{deltaX:t,deltaY:i})=>{this.wasMoved=!0,this.moveBy(t,i)})),this.listenTo(Mn.window,"resize",(()=>{this._isVisible&&!this.wasMoved&&this.updatePosition()})),this.listenTo(Mn.document,"scroll",(()=>{this._isVisible&&!this.wasMoved&&this.updatePosition()})),this.on("change:_isVisible",((e,t,i)=>{i&&(this._isTransparent=!0,setTimeout((()=>{this.updatePosition(),this._isTransparent=!1,this.focus()}),10))})),this.keystrokes.listenTo(this.element)}get dragHandleElement(){return this.headerView?this.headerView.element:null}setupParts({icon:e,title:t,hasCloseButton:i=!0,content:n,actionButtons:s}){t&&(this.headerView=new Ng(this.locale,{icon:e}),i&&(this.closeButtonView=this._createCloseButton(),this.headerView.children.add(this.closeButtonView)),this.headerView.label=t,this.ariaLabel=t,this.parts.add(this.headerView,0)),n&&(n instanceof Du&&(n=[n]),this.contentView=new Dg(this.locale),this.contentView.children.addMany(n),this.parts.add(this.contentView)),s&&(this.actionsView=new Fg(this.locale),this.actionsView.setButtons(s),this.parts.add(this.actionsView)),this._updateFocusCyclableItems()}focus(){this._focusCycler.focusFirst()}moveTo(e,t){const i=this._getViewportRect(),n=this._getDialogRect();e+n.width>i.right&&(e=i.right-n.width),e{var t;this._focusables.add(e),this.focusTracker.add(e.element),Cm(t=e)&&"focusCycler"in t&&t.focusCycler instanceof ym&&(this.listenTo(e.focusCycler,"forwardCycle",(e=>{this._focusCycler.focusNext(),this._focusCycler.next!==this._focusCycler.focusables.get(this._focusCycler.current)&&e.stop()})),this.listenTo(e.focusCycler,"backwardCycle",(e=>{this._focusCycler.focusPrevious(),this._focusCycler.previous!==this._focusCycler.focusables.get(this._focusCycler.current)&&e.stop()})))}))}_createCloseButton(){const e=new Ku(this.locale),t=this.locale.t;return e.set({label:t("Close"),tooltip:!0,icon:fu.cancel}),e.on("execute",(()=>this.fire("close",{source:"closeButton"}))),e}}Gg.defaultOffset=15;const Kg=Gg;class Zg extends so{static get pluginName(){return"Dialog"}constructor(e){super(e);const t=e.t;this._initShowHideListeners(),this._initFocusToggler(),this._initMultiRootIntegration(),this.set("id",null),e.accessibility.addKeystrokeInfos({categoryId:"navigation",keystrokes:[{label:t("Move focus in and out of an active dialog window"),keystroke:"Ctrl+F6",mayRequireFn:!0}]})}_initShowHideListeners(){this.on("show",((e,t)=>{this._show(t)})),this.on("show",((e,t)=>{t.onShow&&t.onShow(this)}),{priority:"low"}),this.on("hide",(()=>{Zg._visibleDialogPlugin&&Zg._visibleDialogPlugin._hide()})),this.on("hide",(()=>{this._onHide&&(this._onHide(this),this._onHide=void 0)}),{priority:"low"})}_initFocusToggler(){const e=this.editor;e.keystrokes.set("Ctrl+F6",((t,i)=>{this.isOpen&&!this.view.isModal&&(this.view.focusTracker.isFocused?e.editing.view.focus():this.view.focus(),i())}))}_initMultiRootIntegration(){const e=this.editor.model;e.document.on("change:data",(()=>{if(!this.view)return;const t=e.document.differ.getChangedRoots();for(const e of t)e.state&&this.view.updatePosition()}))}show(e){this.hide(),this.fire(`show:${e.id}`,e)}_show({id:e,icon:t,title:i,hasCloseButton:n=!0,content:s,actionButtons:o,className:r,isModal:a,position:l,onHide:c}){const d=this.editor;this.view=new Kg(d.locale,{getCurrentDomRoot:()=>d.editing.view.getDomRoot(d.model.document.selection.anchor.root.rootName),getViewportOffset:()=>d.ui.viewportOffset});const h=this.view;h.on("close",(()=>{this.hide()})),d.ui.view.body.add(h),d.ui.focusTracker.add(h.element),d.keystrokes.listenTo(h.element),l||(l=a?Lg:zg),h.set({position:l,_isVisible:!0,className:r,isModal:a}),h.setupParts({icon:t,title:i,hasCloseButton:n,content:s,actionButtons:o}),this.id=e,c&&(this._onHide=c),this.isOpen=!0,Zg._visibleDialogPlugin=this}hide(){Zg._visibleDialogPlugin&&Zg._visibleDialogPlugin.fire(`hide:${Zg._visibleDialogPlugin.id}`)}_hide(){if(!this.view)return;const e=this.editor,t=this.view;t.contentView&&t.contentView.reset(),e.ui.view.body.remove(t),e.ui.focusTracker.remove(t.element),e.keystrokes.stopListening(t.element),t.destroy(),e.editing.view.focus(),this.id=null,this.isOpen=!1,Zg._visibleDialogPlugin=null}}const Jg=Zn("px"),Qg=Mn.document.body,Yg={top:-99999,left:-99999,name:"arrowless",config:{withArrow:!1}};class Xg extends Du{constructor(e){super(e);const t=this.bindTemplate;this.set("top",0),this.set("left",0),this.set("position","arrow_nw"),this.set("isVisible",!1),this.set("withArrow",!0),this.set("class",void 0),this._pinWhenIsVisibleCallback=null,this.content=this.createCollection(),this.setTemplate({tag:"div",attributes:{class:["ck","ck-balloon-panel",t.to("position",(e=>`ck-balloon-panel_${e}`)),t.if("isVisible","ck-balloon-panel_visible"),t.if("withArrow","ck-balloon-panel_with-arrow"),t.to("class")],style:{top:t.to("top",Jg),left:t.to("left",Jg)}},children:this.content})}show(){this.isVisible=!0}hide(){this.isVisible=!1}attachTo(e){this.show();const t=Xg.defaultPositions,i=Object.assign({},{element:this.element,positions:[t.southArrowNorth,t.southArrowNorthMiddleWest,t.southArrowNorthMiddleEast,t.southArrowNorthWest,t.southArrowNorthEast,t.northArrowSouth,t.northArrowSouthMiddleWest,t.northArrowSouthMiddleEast,t.northArrowSouthWest,t.northArrowSouthEast,t.viewportStickyNorth],limiter:Qg,fitInViewport:!0},e),n=Xg._getOptimalPosition(i)||Yg,s=parseInt(n.left),o=parseInt(n.top),r=n.name,a=n.config||{},{withArrow:l=!0}=a;this.top=o,this.left=s,this.position=r,this.withArrow=l}pin(e){this.unpin(),this._pinWhenIsVisibleCallback=()=>{this.isVisible?this._startPinning(e):this._stopPinning()},this._startPinning(e),this.listenTo(this,"change:isVisible",this._pinWhenIsVisibleCallback)}unpin(){this._pinWhenIsVisibleCallback&&(this._stopPinning(),this.stopListening(this,"change:isVisible",this._pinWhenIsVisibleCallback),this._pinWhenIsVisibleCallback=null,this.hide())}_startPinning(e){this.attachTo(e);const t=tf(e.target),i=e.limiter?tf(e.limiter):Qg;this.listenTo(Mn.document,"scroll",((n,s)=>{const o=s.target,r=t&&o.contains(t),a=i&&o.contains(i);!r&&!a&&t&&i||this.attachTo(e)}),{useCapture:!0}),this.listenTo(Mn.window,"resize",(()=>{this.attachTo(e)}))}_stopPinning(){this.stopListening(Mn.document,"scroll"),this.stopListening(Mn.window,"resize")}}Xg.arrowSideOffset=25,Xg.arrowHeightOffset=10,Xg.stickyVerticalOffset=20,Xg._getOptimalPosition=ts,Xg.defaultPositions=nf();const ef=Xg;function tf(e){return An(e)?e:Dn(e)?e.commonAncestorContainer:"function"==typeof e?tf(e()):null}function nf(e={}){const{sideOffset:t=Xg.arrowSideOffset,heightOffset:i=Xg.arrowHeightOffset,stickyVerticalOffset:n=Xg.stickyVerticalOffset,config:s}=e;return{northWestArrowSouthWest:(e,i)=>({top:o(e,i),left:e.left-t,name:"arrow_sw",...s&&{config:s}}),northWestArrowSouthMiddleWest:(e,i)=>({top:o(e,i),left:e.left-.25*i.width-t,name:"arrow_smw",...s&&{config:s}}),northWestArrowSouth:(e,t)=>({top:o(e,t),left:e.left-t.width/2,name:"arrow_s",...s&&{config:s}}),northWestArrowSouthMiddleEast:(e,i)=>({top:o(e,i),left:e.left-.75*i.width+t,name:"arrow_sme",...s&&{config:s}}),northWestArrowSouthEast:(e,i)=>({top:o(e,i),left:e.left-i.width+t,name:"arrow_se",...s&&{config:s}}),northArrowSouthWest:(e,i)=>({top:o(e,i),left:e.left+e.width/2-t,name:"arrow_sw",...s&&{config:s}}),northArrowSouthMiddleWest:(e,i)=>({top:o(e,i),left:e.left+e.width/2-.25*i.width-t,name:"arrow_smw",...s&&{config:s}}),northArrowSouth:(e,t)=>({top:o(e,t),left:e.left+e.width/2-t.width/2,name:"arrow_s",...s&&{config:s}}),northArrowSouthMiddleEast:(e,i)=>({top:o(e,i),left:e.left+e.width/2-.75*i.width+t,name:"arrow_sme",...s&&{config:s}}),northArrowSouthEast:(e,i)=>({top:o(e,i),left:e.left+e.width/2-i.width+t,name:"arrow_se",...s&&{config:s}}),northEastArrowSouthWest:(e,i)=>({top:o(e,i),left:e.right-t,name:"arrow_sw",...s&&{config:s}}),northEastArrowSouthMiddleWest:(e,i)=>({top:o(e,i),left:e.right-.25*i.width-t,name:"arrow_smw",...s&&{config:s}}),northEastArrowSouth:(e,t)=>({top:o(e,t),left:e.right-t.width/2,name:"arrow_s",...s&&{config:s}}),northEastArrowSouthMiddleEast:(e,i)=>({top:o(e,i),left:e.right-.75*i.width+t,name:"arrow_sme",...s&&{config:s}}),northEastArrowSouthEast:(e,i)=>({top:o(e,i),left:e.right-i.width+t,name:"arrow_se",...s&&{config:s}}),southWestArrowNorthWest:e=>({top:r(e),left:e.left-t,name:"arrow_nw",...s&&{config:s}}),southWestArrowNorthMiddleWest:(e,i)=>({top:r(e),left:e.left-.25*i.width-t,name:"arrow_nmw",...s&&{config:s}}),southWestArrowNorth:(e,t)=>({top:r(e),left:e.left-t.width/2,name:"arrow_n",...s&&{config:s}}),southWestArrowNorthMiddleEast:(e,i)=>({top:r(e),left:e.left-.75*i.width+t,name:"arrow_nme",...s&&{config:s}}),southWestArrowNorthEast:(e,i)=>({top:r(e),left:e.left-i.width+t,name:"arrow_ne",...s&&{config:s}}),southArrowNorthWest:e=>({top:r(e),left:e.left+e.width/2-t,name:"arrow_nw",...s&&{config:s}}),southArrowNorthMiddleWest:(e,i)=>({top:r(e),left:e.left+e.width/2-.25*i.width-t,name:"arrow_nmw",...s&&{config:s}}),southArrowNorth:(e,t)=>({top:r(e),left:e.left+e.width/2-t.width/2,name:"arrow_n",...s&&{config:s}}),southArrowNorthMiddleEast:(e,i)=>({top:r(e),left:e.left+e.width/2-.75*i.width+t,name:"arrow_nme",...s&&{config:s}}),southArrowNorthEast:(e,i)=>({top:r(e),left:e.left+e.width/2-i.width+t,name:"arrow_ne",...s&&{config:s}}),southEastArrowNorthWest:e=>({top:r(e),left:e.right-t,name:"arrow_nw",...s&&{config:s}}),southEastArrowNorthMiddleWest:(e,i)=>({top:r(e),left:e.right-.25*i.width-t,name:"arrow_nmw",...s&&{config:s}}),southEastArrowNorth:(e,t)=>({top:r(e),left:e.right-t.width/2,name:"arrow_n",...s&&{config:s}}),southEastArrowNorthMiddleEast:(e,i)=>({top:r(e),left:e.right-.75*i.width+t,name:"arrow_nme",...s&&{config:s}}),southEastArrowNorthEast:(e,i)=>({top:r(e),left:e.right-i.width+t,name:"arrow_ne",...s&&{config:s}}),westArrowEast:(e,t)=>({top:e.top+e.height/2-t.height/2,left:e.left-t.width-i,name:"arrow_e",...s&&{config:s}}),eastArrowWest:(e,t)=>({top:e.top+e.height/2-t.height/2,left:e.right+i,name:"arrow_w",...s&&{config:s}}),viewportStickyNorth:(e,t,i,o)=>{const r=o||i;return e.getIntersection(r)?r.height-e.height>n?null:{top:r.top+n,left:e.left+e.width/2-t.width/2,name:"arrowless",config:{withArrow:!1,...s}}:null}};function o(e,t){return e.top-t.height-i}function r(e){return e.bottom+i}}const sf="ck-tooltip";class of extends(Vn()){constructor(e){if(super(),this._currentElementWithTooltip=null,this._currentTooltipPosition=null,this._resizeObserver=null,this._mutationObserver=null,of._editors.add(e),of._instance)return of._instance;of._instance=this,this.tooltipTextView=new Du(e.locale),this.tooltipTextView.set("text",""),this.tooltipTextView.setTemplate({tag:"span",attributes:{class:["ck","ck-tooltip__text"]},children:[{text:this.tooltipTextView.bindTemplate.to("text")}]}),this.balloonPanelView=new ef(e.locale),this.balloonPanelView.class=sf,this.balloonPanelView.content.add(this.tooltipTextView),this._mutationObserver=function(e){const t=new MutationObserver((()=>{e()}));return{attach(e){t.observe(e,{attributes:!0,attributeFilter:["data-cke-tooltip-text","data-cke-tooltip-position"]})},detach(){t.disconnect()}}}((()=>{this._updateTooltipPosition()})),this._pinTooltipDebounced=La(this._pinTooltip,600),this._unpinTooltipDebounced=La(this._unpinTooltip,400),this.listenTo(Mn.document,"keydown",this._onKeyDown.bind(this),{useCapture:!0}),this.listenTo(Mn.document,"mouseenter",this._onEnterOrFocus.bind(this),{useCapture:!0}),this.listenTo(Mn.document,"mouseleave",this._onLeaveOrBlur.bind(this),{useCapture:!0}),this.listenTo(Mn.document,"focus",this._onEnterOrFocus.bind(this),{useCapture:!0}),this.listenTo(Mn.document,"blur",this._onLeaveOrBlur.bind(this),{useCapture:!0}),this.listenTo(Mn.document,"scroll",this._onScroll.bind(this),{useCapture:!0}),this._watchdogExcluded=!0}destroy(e){const t=e.ui.view&&e.ui.view.body;of._editors.delete(e),this.stopListening(e.ui),t&&t.has(this.balloonPanelView)&&t.remove(this.balloonPanelView),of._editors.size||(this._unpinTooltip(),this.balloonPanelView.destroy(),this.stopListening(),of._instance=null)}static getPositioningFunctions(e){const t=of.defaultBalloonPositions;return{s:[t.southArrowNorth,t.southArrowNorthEast,t.southArrowNorthWest],n:[t.northArrowSouth],e:[t.eastArrowWest],w:[t.westArrowEast],sw:[t.southArrowNorthEast],se:[t.southArrowNorthWest]}[e]}_onKeyDown(e,t){"Escape"===t.key&&this._currentElementWithTooltip&&(this._unpinTooltip(),t.stopPropagation())}_onEnterOrFocus(e,{target:t}){const i=af(t);i?i!==this._currentElementWithTooltip&&(this._unpinTooltip(),this._pinTooltipDebounced(i,lf(i))):"focus"===e.name&&this._unpinTooltip()}_onLeaveOrBlur(e,{target:t,relatedTarget:i}){if("mouseleave"===e.name){if(!An(t))return;const e=this.balloonPanelView.element,n=e&&(e===i||e.contains(i)),s=!n&&t===e;if(n)return void this._unpinTooltipDebounced.cancel();if(!s&&this._currentElementWithTooltip&&t!==this._currentElementWithTooltip)return;const o=af(t),r=af(i);(s||o&&o!==r)&&this._unpinTooltipDebounced()}else{if(this._currentElementWithTooltip&&t!==this._currentElementWithTooltip)return;this._unpinTooltipDebounced()}}_onScroll(e,{target:t}){this._currentElementWithTooltip&&(t.contains(this.balloonPanelView.element)&&t.contains(this._currentElementWithTooltip)||this._unpinTooltip())}_pinTooltip(e,{text:t,position:i,cssClass:n}){this._unpinTooltip();const s=Zs(of._editors.values()).ui.view.body;s.has(this.balloonPanelView)||s.add(this.balloonPanelView),this.tooltipTextView.text=t,this.balloonPanelView.pin({target:e,positions:of.getPositioningFunctions(i)}),this._resizeObserver=new Gn(e,(()=>{es(e)||this._unpinTooltip()})),this._mutationObserver.attach(e),this.balloonPanelView.class=[sf,n].filter((e=>e)).join(" ");for(const e of of._editors)this.listenTo(e.ui,"update",this._updateTooltipPosition.bind(this),{priority:"low"});this._currentElementWithTooltip=e,this._currentTooltipPosition=i}_unpinTooltip(){this._unpinTooltipDebounced.cancel(),this._pinTooltipDebounced.cancel(),this.balloonPanelView.unpin();for(const e of of._editors)this.stopListening(e.ui,"update");this._currentElementWithTooltip=null,this._currentTooltipPosition=null,this.tooltipTextView.text="",this._resizeObserver&&this._resizeObserver.destroy(),this._mutationObserver.detach()}_updateTooltipPosition(){const e=lf(this._currentElementWithTooltip);es(this._currentElementWithTooltip)&&e.text?this.balloonPanelView.pin({target:this._currentElementWithTooltip,positions:of.getPositioningFunctions(e.position)}):this._unpinTooltip()}}of.defaultBalloonPositions=nf({heightOffset:5,sideOffset:13}),of._editors=new Set,of._instance=null;const rf=of;function af(e){return An(e)?e.closest("[data-cke-tooltip-text]:not([data-cke-tooltip-disabled])"):null}function lf(e){return{text:e.dataset.ckeTooltipText,position:e.dataset.ckeTooltipPosition||"s",cssClass:e.dataset.ckeTooltipClass||""}}const cf=function(e,t,i){var n=!0,s=!0;if("function"!=typeof e)throw new TypeError("Expected a function");return L(i)&&(n="leading"in i?!!i.leading:n,s="trailing"in i?!!i.trailing:s),La(e,t,{leading:n,maxWait:t,trailing:s})},df=50,hf=350,uf="Powered by";class mf extends(Vn()){constructor(e){super(),this.editor=e,this._balloonView=null,this._lastFocusedEditableElement=null,this._showBalloonThrottled=cf(this._showBalloon.bind(this),50,{leading:!0}),e.on("ready",this._handleEditorReady.bind(this))}destroy(){const e=this._balloonView;e&&(e.unpin(),this._balloonView=null),this._showBalloonThrottled.cancel(),this.stopListening()}_handleEditorReady(){const e=this.editor;(!!e.config.get("ui.poweredBy.forceVisible")||"VALID"!==function(e){function t(e){return e.length>=40&&e.length<=255?"VALID":"INVALID"}if(!e)return"INVALID";let i="";try{i=atob(e)}catch(e){return"INVALID"}const n=i.split("-"),s=n[0],o=n[1];if(!o)return t(e);try{atob(o)}catch(i){try{if(atob(s),!atob(s).length)return t(e)}catch(i){return t(e)}}if(s.length<40||s.length>255)return"INVALID";let r="";try{atob(s),r=atob(o)}catch(e){return"INVALID"}if(8!==r.length)return"INVALID";const a=Number(r.substring(0,4)),l=Number(r.substring(4,6))-1,c=Number(r.substring(6,8)),d=new Date(a,l,c);return d{this._updateLastFocusedEditableElement(),i?this._showBalloon():this._hideBalloon()})),e.ui.focusTracker.on("change:focusedElement",((e,t,i)=>{this._updateLastFocusedEditableElement(),i&&this._showBalloon()})),e.ui.on("update",(()=>{this._showBalloonThrottled()})))}_createBalloonView(){const e=this.editor,t=this._balloonView=new ef,i=pf(e),n=new gf(e.locale,i.label);t.content.add(n),t.set({class:"ck-powered-by-balloon"}),e.ui.view.body.add(t),e.ui.focusTracker.add(t.element),this._balloonView=t}_showBalloon(){if(!this._lastFocusedEditableElement)return;const e=function(e,t){const i=pf(e),n="right"===i.side?function(e,t){return ff(e,t,((e,i)=>e.left+e.width-i.width-t.horizontalOffset))}(t,i):function(e,t){return ff(e,t,(e=>e.left+t.horizontalOffset))}(t,i);return{target:t,positions:[n]}}(this.editor,this._lastFocusedEditableElement);e&&(this._balloonView||this._createBalloonView(),this._balloonView.pin(e))}_hideBalloon(){this._balloonView&&this._balloonView.unpin()}_updateLastFocusedEditableElement(){const e=this.editor,t=e.ui.focusTracker.isFocused,i=e.ui.focusTracker.focusedElement;if(!t||!i)return void(this._lastFocusedEditableElement=null);const n=Array.from(e.ui.getEditableElementsNames()).map((t=>e.ui.getEditableElement(t)));n.includes(i)?this._lastFocusedEditableElement=i:this._lastFocusedEditableElement=n[0]}}class gf extends Du{constructor(e,t){super(e);const i=new qu,n=this.bindTemplate;i.set({content:'\n',isColorInherited:!1}),i.extendTemplate({attributes:{style:{width:"53px",height:"10px"}}}),this.setTemplate({tag:"div",attributes:{class:["ck","ck-powered-by"],"aria-hidden":!0},children:[{tag:"a",attributes:{href:"https://ckeditor.com/?utm_source=ckeditor&utm_medium=referral&utm_campaign=701Dn000000hVgmIAE_powered_by_ckeditor_logo",target:"_blank",tabindex:"-1"},children:[...t?[{tag:"span",attributes:{class:["ck","ck-powered-by__label"]},children:[t]}]:[],i],on:{dragstart:n.to((e=>e.preventDefault()))}}]})}}function ff(e,t,i){return(n,s)=>{const o=new Hn(e);if(o.widtht.regionName===e));s||(s=new _f(this.view.locale),this.view.regionViews.add(s)),s.set({regionName:e,text:t,politeness:i})}}class vf extends Du{constructor(e){super(e),this.regionViews=this.createCollection(),this.setTemplate({tag:"div",attributes:{class:["ck","ck-aria-live-announcer"]},children:this.regionViews})}}class _f extends Du{constructor(e){super(e);const t=this.bindTemplate;this.set("regionName",""),this.set("text",""),this.set("politeness",bf),this.setTemplate({tag:"div",attributes:{role:"region","data-region":t.to("regionName"),"aria-live":t.to("politeness")},children:[{text:t.to("text")}]})}}class yf extends(G()){constructor(e){super(),this.isReady=!1,this._editableElementsMap=new Map,this._focusableToolbarDefinitions=[];const t=e.editing.view;this.editor=e,this.componentFactory=new Bg(e),this.focusTracker=new Js,this.tooltipManager=new rf(e),this.poweredBy=new mf(e),this.ariaLiveAnnouncer=new wf(e),this.set("viewportOffset",this._readViewportOffsetFromConfig()),this.once("ready",(()=>{this.isReady=!0})),this.listenTo(t.document,"layoutChanged",this.update.bind(this)),this.listenTo(t,"scrollToTheSelection",this._handleScrollToTheSelection.bind(this)),this._initFocusTracking()}get element(){return null}update(){this.fire("update")}destroy(){this.stopListening(),this.focusTracker.destroy(),this.tooltipManager.destroy(this.editor),this.poweredBy.destroy();for(const e of this._editableElementsMap.values())e.ckeditorInstance=null,this.editor.keystrokes.stopListening(e);this._editableElementsMap=new Map,this._focusableToolbarDefinitions=[]}setEditableElement(e,t){this._editableElementsMap.set(e,t),t.ckeditorInstance||(t.ckeditorInstance=this.editor),this.focusTracker.add(t);const i=()=>{this.editor.editing.view.getDomRoot(e)||this.editor.keystrokes.listenTo(t)};this.isReady?i():this.once("ready",i)}removeEditableElement(e){const t=this._editableElementsMap.get(e);t&&(this._editableElementsMap.delete(e),this.editor.keystrokes.stopListening(t),this.focusTracker.remove(t),t.ckeditorInstance=null)}getEditableElement(e="main"){return this._editableElementsMap.get(e)}getEditableElementsNames(){return this._editableElementsMap.keys()}addToolbar(e,t={}){e.isRendered?(this.focusTracker.add(e.element),this.editor.keystrokes.listenTo(e.element)):e.once("render",(()=>{this.focusTracker.add(e.element),this.editor.keystrokes.listenTo(e.element)})),this._focusableToolbarDefinitions.push({toolbarView:e,options:t})}get _editableElements(){return console.warn("editor-ui-deprecated-editable-elements: The EditorUI#_editableElements property has been deprecated and will be removed in the near future.",{editorUI:this}),this._editableElementsMap}_readViewportOffsetFromConfig(){const e=this.editor,t=e.config.get("ui.viewportOffset");if(t)return t;const i=e.config.get("toolbar.viewportTopOffset");return i?(console.warn("editor-ui-deprecated-viewport-offset-config: The `toolbar.vieportTopOffset` configuration option is deprecated. It will be removed from future CKEditor versions. Use `ui.viewportOffset.top` instead."),{top:i}):{top:0}}_initFocusTracking(){const e=this.editor,t=e.editing.view;let i,n;e.keystrokes.set("Alt+F10",((e,s)=>{const o=this.focusTracker.focusedElement;Array.from(this._editableElementsMap.values()).includes(o)&&!Array.from(t.domRoots.values()).includes(o)&&(i=o);const r=this._getCurrentFocusedToolbarDefinition();r&&n||(n=this._getFocusableCandidateToolbarDefinitions());for(let e=0;e{const s=this._getCurrentFocusedToolbarDefinition();s&&(i?(i.focus(),i=null):e.editing.view.focus(),s.options.afterBlur&&s.options.afterBlur(),n())}))}_getFocusableCandidateToolbarDefinitions(){const e=[];for(const t of this._focusableToolbarDefinitions){const{toolbarView:i,options:n}=t;(es(i.element)||n.beforeFocus)&&e.push(t)}return e.sort(((e,t)=>kf(e)-kf(t))),e}_getCurrentFocusedToolbarDefinition(){for(const e of this._focusableToolbarDefinitions)if(e.toolbarView.element&&e.toolbarView.element.contains(this.focusTracker.focusedElement))return e;return null}_focusFocusableCandidateToolbar(e){const{toolbarView:t,options:{beforeFocus:i}}=e;return i&&i(),!!es(t.element)&&(t.focus(),!0)}_handleScrollToTheSelection(e,t){const i={top:0,bottom:0,left:0,right:0,...this.viewportOffset};t.viewportOffset.top+=i.top,t.viewportOffset.bottom+=i.bottom,t.viewportOffset.left+=i.left,t.viewportOffset.right+=i.right}}function kf(e){const{toolbarView:t,options:i}=e;let n=10;return es(t.element)&&n--,i.isContextual&&n--,n}class Cf extends Du{constructor(e){super(e),this.body=new ju(e)}render(){super.render(),this.body.attachToDom()}destroy(){return this.body.detachFromDom(),super.destroy()}}class Af extends Cf{constructor(e){super(e),this.top=this.createCollection(),this.main=this.createCollection(),this._voiceLabelView=this._createVoiceLabel(),this.setTemplate({tag:"div",attributes:{class:["ck","ck-reset","ck-editor","ck-rounded-corners"],role:"application",dir:e.uiLanguageDirection,lang:e.uiLanguage,"aria-labelledby":this._voiceLabelView.id},children:[this._voiceLabelView,{tag:"div",attributes:{class:["ck","ck-editor__top","ck-reset_all"],role:"presentation"},children:this.top},{tag:"div",attributes:{class:["ck","ck-editor__main"],role:"presentation"},children:this.main}]})}_createVoiceLabel(){const e=this.t,t=new Lu;return t.text=e("Rich Text Editor"),t.extendTemplate({attributes:{class:"ck-voice-label"}}),t}}class xf extends Du{constructor(e,t,i){super(e),this.name=null,this.setTemplate({tag:"div",attributes:{class:["ck","ck-content","ck-editor__editable","ck-rounded-corners"],lang:e.contentLanguage,dir:e.contentLanguageDirection}}),this.set("isFocused",!1),this._editableElement=i,this._hasExternalElement=!!this._editableElement,this._editingView=t}render(){super.render(),this._hasExternalElement?this.template.apply(this.element=this._editableElement):this._editableElement=this.element,this.on("change:isFocused",(()=>this._updateIsFocusedClasses())),this._updateIsFocusedClasses()}destroy(){this._hasExternalElement&&this.template.revert(this._editableElement),super.destroy()}get hasExternalElement(){return this._hasExternalElement}_updateIsFocusedClasses(){const e=this._editingView;function t(t){e.change((i=>{const n=e.document.getRoot(t.name);i.addClass(t.isFocused?"ck-focused":"ck-blurred",n),i.removeClass(t.isFocused?"ck-blurred":"ck-focused",n)}))}e.isRenderingInProgress?function i(n){e.once("change:isRenderingInProgress",((e,s,o)=>{o?i(n):t(n)}))}(this):t(this)}}class Ef extends xf{constructor(e,t,i,n={}){super(e,t,i);const s=e.t;this.extendTemplate({attributes:{role:"textbox",class:"ck-editor__editable_inline"}}),this._generateLabel=n.label||(()=>s("Editor editing area: %0",this.name))}render(){super.render();const e=this._editingView;e.change((t=>{const i=e.document.getRoot(this.name);t.setAttribute("aria-label",this._generateLabel(this),i)}))}}class Tf extends uo{static get pluginName(){return"Notification"}init(){this.on("show:warning",((e,t)=>{window.alert(t.message)}),{priority:"lowest"})}showSuccess(e,t={}){this._showNotification({message:e,type:"success",namespace:t.namespace,title:t.title})}showInfo(e,t={}){this._showNotification({message:e,type:"info",namespace:t.namespace,title:t.title})}showWarning(e,t={}){this._showNotification({message:e,type:"warning",namespace:t.namespace,title:t.title})}_showNotification(e){const t=e.namespace?`show:${e.type}:${e.namespace}`:`show:${e.type}`;this.fire(t,{message:e.message,type:e.type,title:e.title||""})}}class Sf extends(G()){constructor(e,t){super(),t&&Ca(this,t),e&&this.set(e)}}const Pf=Zn("px");class If extends so{static get pluginName(){return"ContextualBalloon"}constructor(e){super(e),this._viewToStack=new Map,this._idToStack=new Map,this._view=null,this._rotatorView=null,this._fakePanelsView=null,this.positionLimiter=()=>{const e=this.editor.editing.view,t=e.document.selection.editableElement;return t?e.domConverter.mapViewToDom(t.root):null},this.set("visibleView",null),this.set("_numberOfStacks",0),this.set("_singleViewMode",!1)}destroy(){super.destroy(),this._view&&this._view.destroy(),this._rotatorView&&this._rotatorView.destroy(),this._fakePanelsView&&this._fakePanelsView.destroy()}get view(){return this._view||this._createPanelView(),this._view}hasView(e){return Array.from(this._viewToStack.keys()).includes(e)}add(e){if(this._view||this._createPanelView(),this.hasView(e.view))throw new y("contextualballoon-add-view-exist",[this,e]);const t=e.stackId||"main";if(!this._idToStack.has(t))return this._idToStack.set(t,new Map([[e.view,e]])),this._viewToStack.set(e.view,this._idToStack.get(t)),this._numberOfStacks=this._idToStack.size,void(this._visibleStack&&!e.singleViewMode||this.showStack(t));const i=this._idToStack.get(t);e.singleViewMode&&this.showStack(t),i.set(e.view,e),this._viewToStack.set(e.view,i),i===this._visibleStack&&this._showView(e)}remove(e){if(!this.hasView(e))throw new y("contextualballoon-remove-view-not-exist",[this,e]);const t=this._viewToStack.get(e);this._singleViewMode&&this.visibleView===e&&(this._singleViewMode=!1),this.visibleView===e&&(1===t.size?this._idToStack.size>1?this._showNextStack():(this.view.hide(),this.visibleView=null,this._rotatorView.hideView()):this._showView(Array.from(t.values())[t.size-2])),1===t.size?(this._idToStack.delete(this._getStackId(t)),this._numberOfStacks=this._idToStack.size):t.delete(e),this._viewToStack.delete(e)}updatePosition(e){e&&(this._visibleStack.get(this.visibleView).position=e),this.view.pin(this._getBalloonPosition()),this._fakePanelsView.updatePosition()}showStack(e){this.visibleStack=e;const t=this._idToStack.get(e);if(!t)throw new y("contextualballoon-showstack-stack-not-exist",this);this._visibleStack!==t&&this._showView(Array.from(t.values()).pop())}_createPanelView(){this._view=new ef(this.editor.locale),this.editor.ui.view.body.add(this._view),this.editor.ui.focusTracker.add(this._view.element),this._rotatorView=this._createRotatorView(),this._fakePanelsView=this._createFakePanelsView()}get _visibleStack(){return this._viewToStack.get(this.visibleView)}_getStackId(e){return Array.from(this._idToStack.entries()).find((t=>t[1]===e))[0]}_showNextStack(){const e=Array.from(this._idToStack.values());let t=e.indexOf(this._visibleStack)+1;e[t]||(t=0),this.showStack(this._getStackId(e[t]))}_showPrevStack(){const e=Array.from(this._idToStack.values());let t=e.indexOf(this._visibleStack)-1;e[t]||(t=e.length-1),this.showStack(this._getStackId(e[t]))}_createRotatorView(){const e=new Vf(this.editor.locale),t=this.editor.locale.t;return this.view.content.add(e),e.bind("isNavigationVisible").to(this,"_numberOfStacks",this,"_singleViewMode",((e,t)=>!t&&e>1)),e.on("change:isNavigationVisible",(()=>this.updatePosition()),{priority:"low"}),e.bind("counter").to(this,"visibleView",this,"_numberOfStacks",((e,i)=>{if(i<2)return"";const n=Array.from(this._idToStack.values()).indexOf(this._visibleStack)+1;return t("%0 of %1",[n,i])})),e.buttonNextView.on("execute",(()=>{e.focusTracker.isFocused&&this.editor.editing.view.focus(),this._showNextStack()})),e.buttonPrevView.on("execute",(()=>{e.focusTracker.isFocused&&this.editor.editing.view.focus(),this._showPrevStack()})),e}_createFakePanelsView(){const e=new Rf(this.editor.locale,this.view);return e.bind("numberOfPanels").to(this,"_numberOfStacks",this,"_singleViewMode",((e,t)=>!t&&e>=2?Math.min(e-1,2):0)),e.listenTo(this.view,"change:top",(()=>e.updatePosition())),e.listenTo(this.view,"change:left",(()=>e.updatePosition())),this.editor.ui.view.body.add(e),e}_showView({view:e,balloonClassName:t="",withArrow:i=!0,singleViewMode:n=!1}){this.view.class=t,this.view.withArrow=i,this._rotatorView.showView(e),this.visibleView=e,this.view.pin(this._getBalloonPosition()),this._fakePanelsView.updatePosition(),n&&(this._singleViewMode=!0)}_getBalloonPosition(){let e=Array.from(this._visibleStack.values()).pop().position;return e&&(e.limiter||(e=Object.assign({},e,{limiter:this.positionLimiter})),e=Object.assign({},e,{viewportOffsetConfig:this.editor.ui.viewportOffset})),e}}class Vf extends Du{constructor(e){super(e);const t=e.t,i=this.bindTemplate;this.set("isNavigationVisible",!0),this.focusTracker=new Js,this.buttonPrevView=this._createButtonView(t("Previous"),fu.previousArrow),this.buttonNextView=this._createButtonView(t("Next"),fu.nextArrow),this.content=this.createCollection(),this.setTemplate({tag:"div",attributes:{class:["ck","ck-balloon-rotator"],"z-index":"-1"},children:[{tag:"div",attributes:{class:["ck-balloon-rotator__navigation",i.to("isNavigationVisible",(e=>e?"":"ck-hidden"))]},children:[this.buttonPrevView,{tag:"span",attributes:{class:["ck-balloon-rotator__counter"]},children:[{text:i.to("counter")}]},this.buttonNextView]},{tag:"div",attributes:{class:"ck-balloon-rotator__content"},children:this.content}]})}render(){super.render(),this.focusTracker.add(this.element)}destroy(){super.destroy(),this.focusTracker.destroy()}showView(e){this.hideView(),this.content.add(e)}hideView(){this.content.clear()}_createButtonView(e,t){const i=new Ku(this.locale);return i.set({label:e,icon:t,tooltip:!0}),i}}class Rf extends Du{constructor(e,t){super(e);const i=this.bindTemplate;this.set("top",0),this.set("left",0),this.set("height",0),this.set("width",0),this.set("numberOfPanels",0),this.content=this.createCollection(),this._balloonPanelView=t,this.setTemplate({tag:"div",attributes:{class:["ck-fake-panel",i.to("numberOfPanels",(e=>e?"":"ck-hidden"))],style:{top:i.to("top",Pf),left:i.to("left",Pf),width:i.to("width",Pf),height:i.to("height",Pf)}},children:this.content}),this.on("change:numberOfPanels",((e,t,i,n)=>{i>n?this._addPanels(i-n):this._removePanels(n-i),this.updatePosition()}))}_addPanels(e){for(;e--;){const e=new Du;e.setTemplate({tag:"div"}),this.content.add(e),this.registerChild(e)}}_removePanels(e){for(;e--;){const e=this.content.last;this.content.remove(e),this.deregisterChild(e),e.destroy()}}updatePosition(){if(this.numberOfPanels){const{top:e,left:t}=this._balloonPanelView,{width:i,height:n}=new Hn(this._balloonPanelView.element);Object.assign(this,{top:e,left:t,width:i,height:n})}}}const Of=Zn("px");class Bf extends Du{constructor(e){super(e);const t=this.bindTemplate;this.set("isActive",!1),this.set("isSticky",!1),this.set("limiterElement",null),this.set("limiterBottomOffset",50),this.set("viewportTopOffset",0),this.set("_marginLeft",null),this.set("_isStickyToTheBottomOfLimiter",!1),this.set("_stickyTopOffset",null),this.set("_stickyBottomOffset",null),this.content=this.createCollection(),this._contentPanelPlaceholder=new bu({tag:"div",attributes:{class:["ck","ck-sticky-panel__placeholder"],style:{display:t.to("isSticky",(e=>e?"block":"none")),height:t.to("isSticky",(e=>e?Of(this._contentPanelRect.height):null))}}}).render(),this.contentPanelElement=new bu({tag:"div",attributes:{class:["ck","ck-sticky-panel__content",t.if("isSticky","ck-sticky-panel__content_sticky"),t.if("_isStickyToTheBottomOfLimiter","ck-sticky-panel__content_sticky_bottom-limit")],style:{width:t.to("isSticky",(e=>e?Of(this._contentPanelPlaceholder.getBoundingClientRect().width):null)),top:t.to("_stickyTopOffset",(e=>e?Of(e):e)),bottom:t.to("_stickyBottomOffset",(e=>e?Of(e):e)),marginLeft:t.to("_marginLeft")}},children:this.content}).render(),this.setTemplate({tag:"div",attributes:{class:["ck","ck-sticky-panel"]},children:[this._contentPanelPlaceholder,this.contentPanelElement]})}render(){super.render(),this.checkIfShouldBeSticky(),this.listenTo(Mn.document,"scroll",(()=>{this.checkIfShouldBeSticky()}),{useCapture:!0}),this.listenTo(this,"change:isActive",(()=>{this.checkIfShouldBeSticky()}))}checkIfShouldBeSticky(){if(!this.limiterElement||!this.isActive)return void this._unstick();const e=new Hn(this.limiterElement);let t=e.getVisible();if(t){const e=new Hn(Mn.window);e.top+=this.viewportTopOffset,e.height-=this.viewportTopOffset,t=t.getIntersection(e)}if(t&&e.topt.bottom){const i=Math.max(e.bottom-t.bottom,0)+this.limiterBottomOffset;e.bottom-i>e.top+this._contentPanelRect.height?this._stickToBottomOfLimiter(i):this._unstick()}else this._contentPanelRect.height+this.limiterBottomOffset{this.reset(),this.focus(),this.fire("reset")})),this.resetButtonView.bind("isVisible").to(this.fieldView,"isEmpty",(e=>!e)),this.fieldWrapperChildren.add(this.resetButtonView),this.extendTemplate({attributes:{class:"ck-search__query_with-reset"}}))}reset(){this.fieldView.reset(),this._viewConfig.showResetButton&&(this.resetButtonView.isVisible=!1)}}class Nf extends Du{constructor(){super();const e=this.bindTemplate;this.set({isVisible:!1,primaryText:"",secondaryText:""}),this.setTemplate({tag:"div",attributes:{class:["ck","ck-search__info",e.if("isVisible","ck-hidden",(e=>!e))],tabindex:-1},children:[{tag:"span",children:[{text:[e.to("primaryText")]}]},{tag:"span",children:[{text:[e.to("secondaryText")]}]}]})}focus(){this.element.focus()}}class Ff extends Du{constructor(e){super(e),this.children=this.createCollection(),this.focusTracker=new Js,this.setTemplate({tag:"div",attributes:{class:["ck","ck-search__results"],tabindex:-1},children:this.children}),this._focusCycler=new ym({focusables:this.children,focusTracker:this.focusTracker})}render(){super.render();for(const e of this.children)this.focusTracker.add(e.element)}focus(){this._focusCycler.focusFirst()}focusFirst(){this._focusCycler.focusFirst()}focusLast(){this._focusCycler.focusLast()}}var Df=/[\\^$.*+?()[\]{}|]/g,Lf=RegExp(Df.source);const zf=function(e){return(e=Uo(e))&&Lf.test(e)?e.replace(Df,"\\$&"):e};class Hf extends Du{constructor(e,t){super(e),this._config=t,this.filteredView=t.filteredView,this.queryView=this._createSearchTextQueryView(),this.focusTracker=new Js,this.keystrokes=new Qs,this.resultsView=new Ff(e),this.children=this.createCollection(),this.focusableChildren=this.createCollection([this.queryView,this.resultsView]),this.set("isEnabled",!0),this.set("resultsCount",0),this.set("totalItemsCount",0),t.infoView&&t.infoView.instance?this.infoView=t.infoView.instance:(this.infoView=new Nf,this._enableDefaultInfoViewBehavior(),this.on("render",(()=>{this.search("")}))),this.resultsView.children.addMany([this.infoView,this.filteredView]),this.focusCycler=new ym({focusables:this.focusableChildren,focusTracker:this.focusTracker,keystrokeHandler:this.keystrokes,actions:{focusPrevious:"shift + tab",focusNext:"tab"}}),this.on("search",((e,{resultsCount:t,totalItemsCount:i})=>{this.resultsCount=t,this.totalItemsCount=i})),this.setTemplate({tag:"div",attributes:{class:["ck","ck-search",t.class||null],tabindex:"-1"},children:this.children})}render(){super.render(),this.children.addMany([this.queryView,this.resultsView]);const e=e=>e.stopPropagation();for(const e of this.focusableChildren)this.focusTracker.add(e.element);this.keystrokes.listenTo(this.element),this.keystrokes.set("arrowright",e),this.keystrokes.set("arrowleft",e),this.keystrokes.set("arrowup",e),this.keystrokes.set("arrowdown",e)}focus(){this.queryView.focus()}reset(){this.queryView.reset(),this.search("")}search(e){const t=e?new RegExp(zf(e),"ig"):null,i=this.filteredView.filter(t);this.fire("search",{query:e,...i})}_createSearchTextQueryView(){const e=new Mf(this.locale,this._config.queryView);return this.listenTo(e.fieldView,"input",(()=>{this.search(e.fieldView.element.value)})),e.on("reset",(()=>this.reset())),e.bind("isEnabled").to(this),e}_enableDefaultInfoViewBehavior(){const e=this.locale.t,t=this.infoView;function i(e,{query:t,resultsCount:i,totalItemsCount:n}){return"function"==typeof e?e(t,i,n):e}this.on("search",((n,s)=>{if(s.resultsCount)t.set({isVisible:!1});else{const n=this._config.infoView&&this._config.infoView.text;let o,r;s.totalItemsCount?n&&n.notFound?(o=n.notFound.primary,r=n.notFound.secondary):(o=e("No results found"),r=""):n&&n.noSearchableItems?(o=n.noSearchableItems.primary,r=n.noSearchableItems.secondary):(o=e("No searchable items"),r=""),t.set({primaryText:i(o,s),secondaryText:i(r,s),isVisible:!0})}}))}}class $f extends Hf{constructor(e,t){super(e,t),this._config=t;const i=Zn("px");this.extendTemplate({attributes:{class:["ck-autocomplete"]}});const n=this.resultsView.bindTemplate;this.resultsView.set("isVisible",!1),this.resultsView.set("_position","s"),this.resultsView.set("_width",0),this.resultsView.extendTemplate({attributes:{class:[n.if("isVisible","ck-hidden",(e=>!e)),n.to("_position",(e=>`ck-search__results_${e}`))],style:{width:n.to("_width",i)}}}),this.focusTracker.on("change:isFocused",((e,i,n)=>{this._updateResultsVisibility(),n?this.resultsView.element.scrollTop=0:t.resetOnBlur&&this.queryView.reset()})),this.on("search",(()=>{this._updateResultsVisibility(),this._updateResultsViewWidthAndPosition()})),this.keystrokes.set("esc",((e,t)=>{this.resultsView.isVisible&&(this.queryView.focus(),this.resultsView.isVisible=!1,t())})),this.listenTo(Mn.document,"scroll",(()=>{this._updateResultsViewWidthAndPosition()})),this.on("change:isEnabled",(()=>{this._updateResultsVisibility()})),this.filteredView.on("execute",((e,{value:t})=>{this.focus(),this.reset(),this.queryView.fieldView.value=this.queryView.fieldView.element.value=t,this.resultsView.isVisible=!1})),this.resultsView.on("change:isVisible",(()=>{this._updateResultsViewWidthAndPosition()}))}_updateResultsViewWidthAndPosition(){if(!this.resultsView.isVisible)return;this.resultsView._width=new Hn(this.queryView.fieldView.element).width;const e=$f._getOptimalPosition({element:this.resultsView.element,target:this.queryView.element,fitInViewport:!0,positions:$f.defaultResultsPositions});this.resultsView._position=e?e.name:"s"}_updateResultsVisibility(){const e=void 0===this._config.queryMinChars?0:this._config.queryMinChars,t=this.queryView.fieldView.element.value.length;this.resultsView.isVisible=this.focusTracker.isFocused&&this.isEnabled&&t>=e}}$f.defaultResultsPositions=[e=>({top:e.bottom,left:e.left,name:"s"}),(e,t)=>({top:e.top-t.height,left:e.left,name:"n"})],$f._getOptimalPosition=ts;const Wf=function(e){return function(t){return null==e?void 0:e[t]}};Wf({"&":"&","<":"<",">":">",'"':""","'":"'"});var jf=/[&<>"']/g;RegExp(jf.source);Zn("px");Zn("px");Zn("px");class Uf extends Ku{constructor(e){super(e);const t=this.bindTemplate;this.set({withText:!0,role:"menuitem"}),this.arrowView=this._createArrowView(),this.extendTemplate({attributes:{class:["ck-menu-bar__menu__button"],"aria-haspopup":!0,"aria-expanded":this.bindTemplate.to("isOn",(e=>String(e))),"data-cke-tooltip-disabled":t.to("isOn")},on:{mouseenter:t.to("mouseenter")}})}render(){super.render(),this.children.add(this.arrowView)}_createArrowView(){const e=new qu;return e.content=Yu,e.extendTemplate({attributes:{class:"ck-menu-bar__menu__button__arrow"}}),e}}class qf extends Om{constructor(e,t){super(e);const i=this.bindTemplate;this.extendTemplate({attributes:{class:["ck-menu-bar__menu__item"]},on:{mouseenter:i.to("mouseenter")}}),this.delegate("mouseenter").to(t)}}const Gf={toggleMenusAndFocusItemsOnHover(e){e.on("menu:mouseenter",(t=>{if(e.isOpen){for(const i of e.menus){const e=t.path[0],n=e instanceof qf&&e.children.first===i;i.isOpen=(t.path.includes(i)||n)&&i.isEnabled}t.source.focus()}}))},focusCycleMenusOnArrows(e){const t="rtl"===e.locale.uiLanguageDirection;function i(t,i){const n=e.children.getIndex(t),s=t.isOpen,o=e.children.length,r=e.children.get((n+o+i)%o);t.isOpen=!1,s&&(r.isOpen=!0),r.buttonView.focus()}e.on("menu:arrowright",(e=>{i(e.source,t?-1:1)})),e.on("menu:arrowleft",(e=>{i(e.source,t?1:-1)}))},closeMenusWhenTheBarCloses(e){e.on("change:isOpen",(()=>{e.isOpen||e.menus.forEach((e=>{e.isOpen=!1}))}))},closeMenuWhenAnotherOnTheSameLevelOpens(e){e.on("menu:change:isOpen",((t,i,n)=>{n&&e.menus.filter((e=>t.source.parentMenuView===e.parentMenuView&&t.source!==e&&e.isOpen)).forEach((e=>{e.isOpen=!1}))}))},closeOnClickOutside(t){e({emitter:t,activator:()=>t.isOpen,callback:()=>t.close(),contextElements:()=>t.children.map((e=>e.element))})}},Kf={openAndFocusPanelOnArrowDownKey(e){e.keystrokes.set("arrowdown",((t,i)=>{e.focusTracker.focusedElement===e.buttonView.element&&(e.isOpen||(e.isOpen=!0),e.panelView.focus(),i())}))},openOnArrowRightKey(e){const t="rtl"===e.locale.uiLanguageDirection?"arrowleft":"arrowright";e.keystrokes.set(t,((t,i)=>{e.focusTracker.focusedElement===e.buttonView.element&&e.isEnabled&&(e.isOpen||(e.isOpen=!0),e.panelView.focus(),i())}))},openOnButtonClick(e){e.buttonView.on("execute",(()=>{e.isOpen=!0,e.panelView.focus()}))},toggleOnButtonClick(e){e.buttonView.on("execute",(()=>{e.isOpen=!e.isOpen,e.isOpen&&e.panelView.focus()}))},closeOnArrowLeftKey(e){const t="rtl"===e.locale.uiLanguageDirection?"arrowright":"arrowleft";e.keystrokes.set(t,((t,i)=>{e.isOpen&&(e.isOpen=!1,e.focus(),i())}))},closeOnEscKey(e){e.keystrokes.set("esc",((t,i)=>{e.isOpen&&(e.isOpen=!1,e.focus(),i())}))},closeOnParentClose(e){e.parentMenuView.on("change:isOpen",((t,i,n)=>{n||t.source!==e.parentMenuView||(e.isOpen=!1)}))}},Zf={southEast:e=>({top:e.bottom,left:e.left,name:"se"}),southWest:(e,t)=>({top:e.bottom,left:e.left-t.width+e.width,name:"sw"}),northEast:(e,t)=>({top:e.top-t.height,left:e.left,name:"ne"}),northWest:(e,t)=>({top:e.top-t.height,left:e.left-t.width+e.width,name:"nw"}),eastSouth:e=>({top:e.top,left:e.right-5,name:"es"}),eastNorth:(e,t)=>({top:e.top-t.height,left:e.right-5,name:"en"}),westSouth:(e,t)=>({top:e.top,left:e.left-t.width+5,name:"ws"}),westNorth:(e,t)=>({top:e.top-t.height,left:e.left-t.width+5,name:"wn"})},Jf=[{menuId:"file",label:"File",groups:[{groupId:"export",items:["menuBar:exportPdf","menuBar:exportWord"]},{groupId:"import",items:["menuBar:importWord"]},{groupId:"revisionHistory",items:["menuBar:revisionHistory"]}]},{menuId:"edit",label:"Edit",groups:[{groupId:"undo",items:["menuBar:undo","menuBar:redo"]},{groupId:"selectAll",items:["menuBar:selectAll"]},{groupId:"findAndReplace",items:["menuBar:findAndReplace"]}]},{menuId:"view",label:"View",groups:[{groupId:"sourceEditing",items:["menuBar:sourceEditing"]},{groupId:"showBlocks",items:["menuBar:showBlocks"]},{groupId:"restrictedEditingException",items:["menuBar:restrictedEditingException"]}]},{menuId:"insert",label:"Insert",groups:[{groupId:"insertMainWidgets",items:["menuBar:uploadImage","menuBar:ckbox","menuBar:ckfinder","menuBar:insertTable"]},{groupId:"insertInline",items:["menuBar:link","menuBar:comment"]},{groupId:"insertMinorWidgets",items:["menuBar:insertTemplate","menuBar:blockQuote","menuBar:codeBlock","menuBar:htmlEmbed"]},{groupId:"insertStructureWidgets",items:["menuBar:horizontalLine","menuBar:pageBreak","menuBar:tableOfContents"]},{groupId:"restrictedEditing",items:["menuBar:restrictedEditing"]}]},{menuId:"format",label:"Format",groups:[{groupId:"textAndFont",items:[{menuId:"text",label:"Text",groups:[{groupId:"basicStyles",items:["menuBar:bold","menuBar:italic","menuBar:underline","menuBar:strikethrough","menuBar:superscript","menuBar:subscript","menuBar:code"]},{groupId:"textPartLanguage",items:["menuBar:textPartLanguage"]}]},{menuId:"font",label:"Font",groups:[{groupId:"fontProperties",items:["menuBar:fontSize","menuBar:fontFamily"]},{groupId:"fontColors",items:["menuBar:fontColor","menuBar:fontBackgroundColor"]},{groupId:"highlight",items:["menuBar:highlight"]}]},"menuBar:heading"]},{groupId:"list",items:["menuBar:bulletedList","menuBar:numberedList","menuBar:todoList"]},{groupId:"indent",items:["menuBar:alignment","menuBar:indent","menuBar:outdent"]},{groupId:"caseChange",items:["menuBar:caseChange"]},{groupId:"removeFormat",items:["menuBar:removeFormat"]}]},{menuId:"tools",label:"Tools",groups:[{groupId:"aiTools",items:["menuBar:aiAssistant","menuBar:aiCommands"]},{groupId:"tools",items:["menuBar:trackChanges","menuBar:commentsArchive"]}]},{menuId:"help",label:"Help",groups:[{groupId:"help",items:["menuBar:accessibilityHelp"]}]}];function Qf({normalizedConfig:e,locale:t,componentFactory:i}){const n=wl(e);return function(e,t){const i=t.removeItems,n=[];t.items=t.items.filter((({menuId:e})=>!i.includes(e)||(n.push(e),!1))),tp(t.items,(e=>{e.groups=e.groups.filter((({groupId:e})=>!i.includes(e)||(n.push(e),!1)));for(const t of e.groups)t.items=t.items.filter((e=>{const t=rp(e);return!i.includes(t)||(n.push(t),!1)}))}));for(const t of i)n.includes(t)||k("menu-bar-item-could-not-be-removed",{menuBarConfig:e,itemName:t})}(e,n),function(e,t){const i=t.addItems,n=[];for(const e of i){const i=sp(e.position),s=op(e.position);if(ip(e))if(s){const o=t.items.findIndex((e=>e.menuId===s));if(-1!=o)"before"===i?(t.items.splice(o,0,e.menu),n.push(e)):"after"===i&&(t.items.splice(o+1,0,e.menu),n.push(e));else{Yf(t,e.menu,s,i)&&n.push(e)}}else"start"===i?(t.items.unshift(e.menu),n.push(e)):"end"===i&&(t.items.push(e.menu),n.push(e));else if(np(e))tp(t.items,(t=>{if(t.menuId===s)"start"===i?(t.groups.unshift(e.group),n.push(e)):"end"===i&&(t.groups.push(e.group),n.push(e));else{const o=t.groups.findIndex((e=>e.groupId===s));-1!==o&&("before"===i?(t.groups.splice(o,0,e.group),n.push(e)):"after"===i&&(t.groups.splice(o+1,0,e.group),n.push(e)))}}));else{Yf(t,e.item,s,i)&&n.push(e)}}for(const t of i)n.includes(t)||k("menu-bar-item-could-not-be-added",{menuBarConfig:e,addedItemConfig:t})}(e,n),function(e,t,i){tp(t.items,(n=>{for(const s of n.groups)s.items=s.items.filter((s=>{const o="string"==typeof s&&!i.has(s);return o&&!t.isUsingDefaultConfig&&k("menu-bar-item-unavailable",{menuBarConfig:e,parentMenuConfig:wl(n),componentName:s}),!o}))}))}(e,n,i),Xf(e,n),function(e,t){const i=t.t,n={File:i({string:"File",id:"MENU_BAR_MENU_FILE"}),Edit:i({string:"Edit",id:"MENU_BAR_MENU_EDIT"}),View:i({string:"View",id:"MENU_BAR_MENU_VIEW"}),Insert:i({string:"Insert",id:"MENU_BAR_MENU_INSERT"}),Format:i({string:"Format",id:"MENU_BAR_MENU_FORMAT"}),Tools:i({string:"Tools",id:"MENU_BAR_MENU_TOOLS"}),Help:i({string:"Help",id:"MENU_BAR_MENU_HELP"}),Text:i({string:"Text",id:"MENU_BAR_MENU_TEXT"}),Font:i({string:"Font",id:"MENU_BAR_MENU_FONT"})};tp(e.items,(e=>{e.label in n&&(e.label=n[e.label])}))}(n,t),n}function Yf(e,t,i,n){let s=!1;return tp(e.items,(e=>{for(const{groupId:o,items:r}of e.groups){if(s)return;if(o===i)"start"===n?(r.unshift(t),s=!0):"end"===n&&(r.push(t),s=!0);else{const e=r.findIndex((e=>rp(e)===i));-1!==e&&("before"===n?(r.splice(e,0,t),s=!0):"after"===n&&(r.splice(e+1,0,t),s=!0))}}})),s}function Xf(e,t){const i=t.isUsingDefaultConfig;let n=!1;t.items=t.items.filter((t=>!!t.groups.length||(ep(e,t,i),!1))),t.items.length?(tp(t.items,(t=>{t.groups=t.groups.filter((e=>!!e.items.length||(n=!0,!1)));for(const s of t.groups)s.items=s.items.filter((t=>!(ap(t)&&!t.groups.length)||(ep(e,t,i),n=!0,!1)))})),n&&Xf(e,t)):ep(e,e,i)}function ep(e,t,i){i||k("menu-bar-menu-empty",{menuBarConfig:e,emptyMenuConfig:t})}function tp(e,t){if(Array.isArray(e))for(const t of e)i(t);function i(e){t(e);for(const t of e.groups)for(const e of t.items)ap(e)&&i(e)}}function ip(e){return"object"==typeof e&&"menu"in e}function np(e){return"object"==typeof e&&"group"in e}function sp(e){return e.startsWith("start")?"start":e.startsWith("end")?"end":e.startsWith("after")?"after":"before"}function op(e){const t=e.match(/^[^:]+:(.+)/);return t?t[1]:null}function rp(e){return"string"==typeof e?e:e.menuId}function ap(e){return"object"==typeof e&&"menuId"in e}class lp extends Du{constructor(e){super(e);const t=this.bindTemplate;this.set("isVisible",!1),this.set("position","se"),this.children=this.createCollection(),this.setTemplate({tag:"div",attributes:{class:["ck","ck-reset","ck-menu-bar__menu__panel",t.to("position",(e=>`ck-menu-bar__menu__panel_position_${e}`)),t.if("isVisible","ck-hidden",(e=>!e))],tabindex:"-1"},children:this.children,on:{selectstart:t.to((e=>{"input"!==e.target.tagName.toLocaleLowerCase()&&e.preventDefault()}))}})}focus(e=1){this.children.length&&(1===e?this.children.first.focus():this.children.last.focus())}}class cp extends Du{constructor(e){super(e);const t=this.bindTemplate;this.buttonView=new Uf(e),this.buttonView.delegate("mouseenter").to(this),this.buttonView.bind("isOn","isEnabled").to(this,"isOpen","isEnabled"),this.panelView=new lp(e),this.panelView.bind("isVisible").to(this,"isOpen"),this.keystrokes=new Qs,this.focusTracker=new Js,this.set("isOpen",!1),this.set("isEnabled",!0),this.set("panelPosition","w"),this.set("class",void 0),this.set("parentMenuView",null),this.setTemplate({tag:"div",attributes:{class:["ck","ck-menu-bar__menu",t.to("class"),t.if("isEnabled","ck-disabled",(e=>!e)),t.if("parentMenuView","ck-menu-bar__menu_top-level",(e=>!e))]},children:[this.buttonView,this.panelView]})}render(){super.render(),this.focusTracker.add(this.buttonView.element),this.focusTracker.add(this.panelView.element),this.keystrokes.listenTo(this.element),Kf.closeOnEscKey(this),this._repositionPanelOnOpen()}_attachBehaviors(){this.parentMenuView?(Kf.openOnButtonClick(this),Kf.openOnArrowRightKey(this),Kf.closeOnArrowLeftKey(this),Kf.closeOnParentClose(this)):(this._propagateArrowKeystrokeEvents(),Kf.openAndFocusPanelOnArrowDownKey(this),Kf.toggleOnButtonClick(this))}_propagateArrowKeystrokeEvents(){this.keystrokes.set("arrowright",((e,t)=>{this.fire("arrowright"),t()})),this.keystrokes.set("arrowleft",((e,t)=>{this.fire("arrowleft"),t()}))}_repositionPanelOnOpen(){this.on("change:isOpen",((e,t,i)=>{if(!i)return;const n=cp._getOptimalPosition({element:this.panelView.element,target:this.buttonView.element,fitInViewport:!0,positions:this._panelPositions});this.panelView.position=n?n.name:this._panelPositions[0].name}))}focus(){this.buttonView.focus()}get _panelPositions(){const{southEast:e,southWest:t,northEast:i,northWest:n,westSouth:s,eastSouth:o,westNorth:r,eastNorth:a}=Zf;return"ltr"===this.locale.uiLanguageDirection?this.parentMenuView?[o,a,s,r]:[e,t,i,n]:this.parentMenuView?[s,r,o,a]:[t,e,n,i]}}cp._getOptimalPosition=ts;const dp=cp;class hp extends Nm{constructor(e){super(e),this.role="menu"}}class up extends Ku{constructor(e){super(e),this.set({withText:!0,withKeystroke:!0,tooltip:!1,role:"menuitem"}),this.extendTemplate({attributes:{class:["ck-menu-bar__menu__item__button"]}})}}class mp extends Ju{constructor(e){super(e),this.set({withText:!0,withKeystroke:!0,tooltip:!1,role:"menuitem"}),this.extendTemplate({attributes:{class:["ck-menu-bar__menu__item__button"]}})}}const gp=["mouseenter","arrowleft","arrowright","change:isOpen"];class fp extends Du{constructor(e){super(e),this.menus=[];const t=e.t;this.set("isOpen",!1),this._setupIsOpenUpdater(),this.children=this.createCollection(),this.setTemplate({tag:"div",attributes:{class:["ck","ck-menu-bar"],"aria-label":t("Editor menu bar"),role:"menubar"},children:this.children})}fillFromConfig(e,t){const i=Qf({normalizedConfig:e,locale:this.locale,componentFactory:t}).items.map((e=>this._createMenu({componentFactory:t,menuDefinition:e})));this.children.addMany(i)}render(){super.render(),Gf.toggleMenusAndFocusItemsOnHover(this),Gf.closeMenusWhenTheBarCloses(this),Gf.closeMenuWhenAnotherOnTheSameLevelOpens(this),Gf.focusCycleMenusOnArrows(this),Gf.closeOnClickOutside(this)}focus(){this.children.first&&this.children.first.focus()}close(){for(const e of this.children)e.isOpen=!1}registerMenu(e,t=null){t?(e.delegate(...gp).to(t),e.parentMenuView=t):e.delegate(...gp).to(this,(e=>"menu:"+e)),e._attachBehaviors(),this.menus.push(e)}_createMenu({componentFactory:e,menuDefinition:t,parentMenuView:i}){const n=this.locale,s=new dp(n);return this.registerMenu(s,i),s.buttonView.set({label:t.label}),s.once("change:isOpen",(()=>{const i=new hp(n);i.ariaLabel=t.label,s.panelView.children.add(i),i.items.addMany(this._createMenuItems({menuDefinition:t,parentMenuView:s,componentFactory:e}))})),s}_createMenuItems({menuDefinition:e,parentMenuView:t,componentFactory:i}){const n=this.locale,s=[];for(const o of e.groups){for(const e of o.items){const o=new qf(n,t);if(L(e))o.children.add(this._createMenu({componentFactory:i,menuDefinition:e,parentMenuView:t}));else{const n=this._createMenuItemContentFromFactory({componentName:e,componentFactory:i,parentMenuView:t});if(!n)continue;o.children.add(n)}s.push(o)}o!==e.groups[e.groups.length-1]&&s.push(new Bm(n))}return s}_createMenuItemContentFromFactory({componentName:e,parentMenuView:t,componentFactory:i}){const n=i.create(e);return n instanceof dp||n instanceof up||n instanceof mp?(this._registerMenuTree(n,t),n.on("execute",(()=>{this.close()})),n):(k("menu-bar-component-unsupported",{componentName:e,componentView:n}),null)}_registerMenuTree(e,t){if(!(e instanceof dp))return void e.delegate("mouseenter").to(t);this.registerMenu(e,t);const i=e.panelView.children.filter((e=>e instanceof hp))[0];if(!i)return void e.delegate("mouseenter").to(t);const n=i.items.filter((e=>e instanceof Om));for(const t of n)this._registerMenuTree(t.children.get(0),e)}_setupIsOpenUpdater(){let e;this.on("menu:change:isOpen",((t,i,n)=>{clearTimeout(e),n?this.isOpen=!0:e=setTimeout((()=>{this.isOpen=Array.from(this.children).some((e=>e.isOpen))}),0)}))}}class pp extends yf{constructor(e,t){super(e),this.view=t,this._toolbarConfig=Em(e.config.get("toolbar")),this._menuBarConfig=function(e){let t;return t="items"in e&&e.items?{items:e.items,removeItems:[],addItems:[],isVisible:!0,isUsingDefaultConfig:!1,...e}:{items:wl(Jf),addItems:[],removeItems:[],isVisible:!0,isUsingDefaultConfig:!0,...e},t}(e.config.get("menuBar")||{}),this._elementReplacer=new X,this.listenTo(e.editing.view,"scrollToTheSelection",this._handleScrollToTheSelectionWithStickyPanel.bind(this))}get element(){return this.view.element}init(e){const t=this.editor,i=this.view,n=t.editing.view,s=i.editable,o=n.document.getRoot();s.name=o.rootName,i.render();const r=s.element;this.setEditableElement(s.name,r),i.editable.bind("isFocused").to(this.focusTracker),n.attachDomRoot(r),e&&this._elementReplacer.replace(e,this.element),this._initPlaceholder(),this._initToolbar(),this._initMenuBar(),this._initDialogPluginIntegration(),this.fire("ready")}destroy(){super.destroy();const e=this.view,t=this.editor.editing.view;this._elementReplacer.restore(),t.detachDomRoot(e.editable.name),e.destroy()}_initToolbar(){const e=this.view;e.stickyPanel.bind("isActive").to(this.focusTracker,"isFocused"),e.stickyPanel.limiterElement=e.element,e.stickyPanel.bind("viewportTopOffset").to(this,"viewportOffset",(({top:e})=>e||0)),e.toolbar.fillFromConfig(this._toolbarConfig,this.componentFactory),this.addToolbar(e.toolbar)}_initMenuBar(){const e=this.view;e.menuBarView&&(this._setupMenuBarBehaviors(e.menuBarView.element),e.menuBarView.fillFromConfig(this._menuBarConfig,this.componentFactory))}_initPlaceholder(){const e=this.editor,t=e.editing.view,i=t.document.getRoot(),n=e.sourceElement;let s;const o=e.config.get("placeholder");o&&(s="string"==typeof o?o:o[this.view.editable.name]),!s&&n&&"textarea"===n.tagName.toLowerCase()&&(s=n.getAttribute("placeholder")),s&&(i.placeholder=s),fo({view:t,element:i,isDirectHost:!1,keepOnFocus:!0})}_handleScrollToTheSelectionWithStickyPanel(e,t,i){const n=this.view.stickyPanel;if(n.isSticky){const e=new Hn(n.element).height;t.viewportOffset.top+=e}else{const e=()=>{this.editor.editing.view.scrollToTheSelection(i)};this.listenTo(n,"change:isSticky",e),setTimeout((()=>{this.stopListening(n,"change:isSticky",e)}),20)}}_initDialogPluginIntegration(){if(!this.editor.plugins.has("Dialog"))return;const e=this.view.stickyPanel,t=this.editor.plugins.get("Dialog");t.on("show",(()=>{const i=t.view;i.on("moveTo",((t,n)=>{if(!e.isSticky||i.wasMoved)return;const s=new Hn(e.contentPanelElement);n[1]{e.contains(this.focusTracker.focusedElement)&&(t.editing.view.focus(),n())})),t.keystrokes.set("Alt+F9",((t,i)=>{e.contains(this.focusTracker.focusedElement)||(this.view.menuBarView.focus(),i())}))}}class bp extends Af{constructor(e,t,i={}){super(e),this.stickyPanel=new Bf(e),this.toolbar=new Pm(e,{shouldGroupWhenFull:i.shouldToolbarGroupWhenFull}),i.useMenuBar&&(this.menuBarView=new fp(e)),this.editable=new Ef(e,t)}render(){super.render(),this.menuBarView?this.stickyPanel.content.addMany([this.menuBarView,this.toolbar]):this.stickyPanel.content.add(this.toolbar),this.top.add(this.stickyPanel),this.main.add(this.editable)}}class wp{constructor(e){if(this.crashes=[],this.state="initializing",this._now=Date.now,this.crashes=[],this._crashNumberLimit="number"==typeof e.crashNumberLimit?e.crashNumberLimit:3,this._minimumNonErrorTimePeriod="number"==typeof e.minimumNonErrorTimePeriod?e.minimumNonErrorTimePeriod:5e3,this._boundErrorHandler=e=>{const t="error"in e?e.error:e.reason;t instanceof Error&&this._handleError(t,e)},this._listeners={},!this._restart)throw new Error("The Watchdog class was split into the abstract `Watchdog` class and the `EditorWatchdog` class. Please, use `EditorWatchdog` if you have used the `Watchdog` class previously.")}destroy(){this._stopErrorHandling(),this._listeners={}}on(e,t){this._listeners[e]||(this._listeners[e]=[]),this._listeners[e].push(t)}off(e,t){this._listeners[e]=this._listeners[e].filter((e=>e!==t))}_fire(e,...t){const i=this._listeners[e]||[];for(const e of i)e.apply(this,[null,...t])}_startErrorHandling(){window.addEventListener("error",this._boundErrorHandler),window.addEventListener("unhandledrejection",this._boundErrorHandler)}_stopErrorHandling(){window.removeEventListener("error",this._boundErrorHandler),window.removeEventListener("unhandledrejection",this._boundErrorHandler)}_handleError(e,t){if(this._shouldReactToError(e)){this.crashes.push({message:e.message,stack:e.stack,filename:t instanceof ErrorEvent?t.filename:void 0,lineno:t instanceof ErrorEvent?t.lineno:void 0,colno:t instanceof ErrorEvent?t.colno:void 0,date:this._now()});const i=this._shouldRestart();this.state="crashed",this._fire("stateChange"),this._fire("error",{error:e,causesRestart:i}),i?this._restart():(this.state="crashedPermanently",this._fire("stateChange"))}}_shouldReactToError(e){return e.is&&e.is("CKEditorError")&&void 0!==e.context&&null!==e.context&&"ready"===this.state&&this._isErrorComingFromThisItem(e)}_shouldRestart(){if(this.crashes.length<=this._crashNumberLimit)return!0;return(this.crashes[this.crashes.length-1].date-this.crashes[this.crashes.length-1-this._crashNumberLimit].date)/this._crashNumberLimit>this._minimumNonErrorTimePeriod}}function vp(e,t=new Set){const i=[e],n=new Set;let s=0;for(;i.length>s;){const e=i[s++];if(!n.has(e)&&_p(e)&&!t.has(e))if(n.add(e),Symbol.iterator in e)try{for(const t of e)i.push(t)}catch(e){}else for(const t in e)"defaultValue"!==t&&i.push(e[t])}return n}function _p(e){const t=Object.prototype.toString.call(e),i=typeof e;return!("number"===i||"boolean"===i||"string"===i||"symbol"===i||"function"===i||"[object Date]"===t||"[object RegExp]"===t||"[object Module]"===t||null==e||e._watchdogExcluded||e instanceof EventTarget||e instanceof Event)}function yp(e,t,i=new Set){if(e===t&&("object"==typeof(n=e)&&null!==n))return!0;var n;const s=vp(e,i),o=vp(t,i);for(const e of s)if(o.has(e))return!0;return!1}class kp extends wp{constructor(e,t={}){super(t),this._editor=null,this._lifecyclePromise=null,this._initUsingData=!0,this._editables={},this._throttledSave=cf(this._save.bind(this),"number"==typeof t.saveInterval?t.saveInterval:5e3),e&&(this._creator=(t,i)=>e.create(t,i)),this._destructor=e=>e.destroy()}get editor(){return this._editor}get _item(){return this._editor}setCreator(e){this._creator=e}setDestructor(e){this._destructor=e}_restart(){return Promise.resolve().then((()=>(this.state="initializing",this._fire("stateChange"),this._destroy()))).catch((e=>{console.error("An error happened during the editor destroying.",e)})).then((()=>{const e={},t=[],i=this._config.rootsAttributes||{},n={};for(const[s,o]of Object.entries(this._data.roots))o.isLoaded?(e[s]="",n[s]=i[s]||{}):t.push(s);const s={...this._config,extraPlugins:this._config.extraPlugins||[],lazyRoots:t,rootsAttributes:n,_watchdogInitialData:this._data};return delete s.initialData,s.extraPlugins.push(Cp),this._initUsingData?this.create(e,s,s.context):An(this._elementOrData)?this.create(this._elementOrData,s,s.context):this.create(this._editables,s,s.context)})).then((()=>{this._fire("restart")}))}create(e=this._elementOrData,t=this._config,i){return this._lifecyclePromise=Promise.resolve(this._lifecyclePromise).then((()=>(super._startErrorHandling(),this._elementOrData=e,this._initUsingData="string"==typeof e||Object.keys(e).length>0&&"string"==typeof Object.values(e)[0],this._config=this._cloneEditorConfiguration(t)||{},this._config.context=i,this._creator(e,this._config)))).then((e=>{this._editor=e,e.model.document.on("change:data",this._throttledSave),this._lastDocumentVersion=e.model.document.version,this._data=this._getData(),this._initUsingData||(this._editables=this._getEditables()),this.state="ready",this._fire("stateChange")})).finally((()=>{this._lifecyclePromise=null})),this._lifecyclePromise}destroy(){return this._lifecyclePromise=Promise.resolve(this._lifecyclePromise).then((()=>(this.state="destroyed",this._fire("stateChange"),super.destroy(),this._destroy()))).finally((()=>{this._lifecyclePromise=null})),this._lifecyclePromise}_destroy(){return Promise.resolve().then((()=>{this._stopErrorHandling(),this._throttledSave.cancel();const e=this._editor;return this._editor=null,e.model.document.off("change:data",this._throttledSave),this._destructor(e)}))}_save(){const e=this._editor.model.document.version;try{this._data=this._getData(),this._initUsingData||(this._editables=this._getEditables()),this._lastDocumentVersion=e}catch(e){console.error(e,"An error happened during restoring editor data. Editor will be restored from the previously saved data.")}}_setExcludedProperties(e){this._excludedProps=e}_getData(){const e=this._editor,t=e.model.document.roots.filter((e=>e.isAttached()&&"$graveyard"!=e.rootName)),{plugins:i}=e,n=i.has("CommentsRepository")&&i.get("CommentsRepository"),s=i.has("TrackChanges")&&i.get("TrackChanges"),o={roots:{},markers:{},commentThreads:JSON.stringify([]),suggestions:JSON.stringify([])};t.forEach((e=>{o.roots[e.rootName]={content:JSON.stringify(Array.from(e.getChildren())),attributes:JSON.stringify(Array.from(e.getAttributes())),isLoaded:e._isLoaded}}));for(const t of e.model.markers)t._affectsData&&(o.markers[t.name]={rangeJSON:t.getRange().toJSON(),usingOperation:t._managedUsingOperations,affectsData:t._affectsData});return n&&(o.commentThreads=JSON.stringify(n.getCommentThreads({toJSON:!0,skipNotAttached:!0}))),s&&(o.suggestions=JSON.stringify(s.getSuggestions({toJSON:!0,skipNotAttached:!0}))),o}_getEditables(){const e={};for(const t of this.editor.model.document.getRootNames()){const i=this.editor.ui.getEditableElement(t);i&&(e[t]=i)}return e}_isErrorComingFromThisItem(e){return yp(this._editor,e.context,this._excludedProps)}_cloneEditorConfiguration(e){return Cn(e,((e,t)=>An(e)||"context"===t?e:void 0))}}class Cp{constructor(e){this.editor=e,this._data=e.config.get("_watchdogInitialData")}init(){this.editor.data.on("init",(e=>{e.stop(),this.editor.model.enqueueChange({isUndoable:!1},(e=>{this._restoreCollaborationData(),this._restoreEditorData(e)})),this.editor.data.fire("ready")}),{priority:999})}_createNode(e,t){if("name"in t){const i=e.createElement(t.name,t.attributes);if(t.children)for(const n of t.children)i._appendChild(this._createNode(e,n));return i}return e.createText(t.data,t.attributes)}_restoreEditorData(e){const t=this.editor;Object.entries(this._data.roots).forEach((([i,{content:n,attributes:s}])=>{const o=JSON.parse(n),r=JSON.parse(s),a=t.model.document.getRoot(i);for(const[t,i]of r)e.setAttribute(t,i,a);for(const t of o){const i=this._createNode(e,t);e.insert(i,a,"end")}})),Object.entries(this._data.markers).forEach((([i,n])=>{const{document:s}=t.model,{rangeJSON:{start:o,end:r},...a}=n,l=s.getRoot(o.root),c=e.createPositionFromPath(l,o.path,o.stickiness),d=e.createPositionFromPath(l,r.path,r.stickiness),h=e.createRange(c,d);e.addMarker(i,{range:h,...a})}))}_restoreCollaborationData(){const e=JSON.parse(this._data.commentThreads),t=JSON.parse(this._data.suggestions);e.forEach((e=>{const t=this.editor.config.get("collaboration.channelId"),i=this.editor.plugins.get("CommentsRepository");if(i.hasCommentThread(e.threadId)){i.getCommentThread(e.threadId).remove()}i.addCommentThread({channelId:t,...e})})),t.forEach((e=>{const t=this.editor.plugins.get("TrackChangesEditing");if(t.hasSuggestion(e.id)){t.getSuggestion(e.id).attributes=e.attributes}else t.addSuggestionData(e)}))}}const Ap=Symbol("MainQueueId");class xp{constructor(){this._onEmptyCallbacks=[],this._queues=new Map,this._activeActions=0}onEmpty(e){this._onEmptyCallbacks.push(e)}enqueue(e,t){const i=e===Ap;this._activeActions++,this._queues.get(e)||this._queues.set(e,Promise.resolve());const n=(i?Promise.all(this._queues.values()):Promise.all([this._queues.get(Ap),this._queues.get(e)])).then(t),s=n.catch((()=>{}));return this._queues.set(e,s),n.finally((()=>{this._activeActions--,this._queues.get(e)===s&&0===this._activeActions&&this._onEmptyCallbacks.forEach((e=>e()))}))}}function Ep(e){return Array.isArray(e)?e:[e]}class Tp extends(mu(uu)){constructor(e,t={}){if(!Pp(e)&&void 0!==t.initialData)throw new y("editor-create-initial-data",null);super(t),this.config.define("menuBar.isVisible",!1),void 0===this.config.get("initialData")&&this.config.set("initialData",function(e){return Pp(e)?(t=e,t instanceof HTMLTextAreaElement?t.value:t.innerHTML):e;var t}(e)),Pp(e)&&(this.sourceElement=e),this.model.document.createRoot();const i=!this.config.get("toolbar.shouldNotGroupWhenFull"),n=this.config.get("menuBar"),s=new bp(this.locale,this.editing.view,{shouldToolbarGroupWhenFull:i,useMenuBar:n.isVisible});this.ui=new pp(this,s),function(e){if(!$e(e.updateSourceElement))throw new y("attachtoform-missing-elementapi-interface",e);const t=e.sourceElement;if(function(e){return!!e&&"textarea"===e.tagName.toLowerCase()}(t)&&t.form){let i;const n=t.form,s=()=>e.updateSourceElement();$e(n.submit)&&(i=n.submit,n.submit=()=>{s(),i.apply(n)}),n.addEventListener("submit",s),e.on("destroy",(()=>{n.removeEventListener("submit",s),i&&(n.submit=i)}))}}(this)}destroy(){return this.sourceElement&&this.updateSourceElement(),this.ui.destroy(),super.destroy()}static create(e,t={}){return new Promise((i=>{const n=new this(e,t);i(n.initPlugins().then((()=>n.ui.init(Pp(e)?e:null))).then((()=>n.data.init(n.config.get("initialData")))).then((()=>n.fire("ready"))).then((()=>n)))}))}}Tp.Context=ho,Tp.EditorWatchdog=kp,Tp.ContextWatchdog=class extends wp{constructor(e,t={}){super(t),this._watchdogs=new Map,this._context=null,this._contextProps=new Set,this._actionQueues=new xp,this._watchdogConfig=t,this._creator=t=>e.create(t),this._destructor=e=>e.destroy(),this._actionQueues.onEmpty((()=>{"initializing"===this.state&&(this.state="ready",this._fire("stateChange"))}))}setCreator(e){this._creator=e}setDestructor(e){this._destructor=e}get context(){return this._context}create(e={}){return this._actionQueues.enqueue(Ap,(()=>(this._contextConfig=e,this._create())))}getItem(e){return this._getWatchdog(e)._item}getItemState(e){return this._getWatchdog(e).state}add(e){const t=Ep(e);return Promise.all(t.map((e=>this._actionQueues.enqueue(e.id,(()=>{if("destroyed"===this.state)throw new Error("Cannot add items to destroyed watchdog.");if(!this._context)throw new Error("Context was not created yet. You should call the `ContextWatchdog#create()` method first.");let t;if(this._watchdogs.has(e.id))throw new Error(`Item with the given id is already added: '${e.id}'.`);if("editor"===e.type)return t=new kp(null,this._watchdogConfig),t.setCreator(e.creator),t._setExcludedProperties(this._contextProps),e.destructor&&t.setDestructor(e.destructor),this._watchdogs.set(e.id,t),t.on("error",((i,{error:n,causesRestart:s})=>{this._fire("itemError",{itemId:e.id,error:n}),s&&this._actionQueues.enqueue(e.id,(()=>new Promise((i=>{const n=()=>{t.off("restart",n),this._fire("itemRestart",{itemId:e.id}),i()};t.on("restart",n)}))))})),t.create(e.sourceElementOrData,e.config,this._context);throw new Error(`Not supported item type: '${e.type}'.`)})))))}remove(e){const t=Ep(e);return Promise.all(t.map((e=>this._actionQueues.enqueue(e,(()=>{const t=this._getWatchdog(e);return this._watchdogs.delete(e),t.destroy()})))))}destroy(){return this._actionQueues.enqueue(Ap,(()=>(this.state="destroyed",this._fire("stateChange"),super.destroy(),this._destroy())))}_restart(){return this._actionQueues.enqueue(Ap,(()=>(this.state="initializing",this._fire("stateChange"),this._destroy().catch((e=>{console.error("An error happened during destroying the context or items.",e)})).then((()=>this._create())).then((()=>this._fire("restart"))))))}_create(){return Promise.resolve().then((()=>(this._startErrorHandling(),this._creator(this._contextConfig)))).then((e=>(this._context=e,this._contextProps=vp(this._context),Promise.all(Array.from(this._watchdogs.values()).map((e=>(e._setExcludedProperties(this._contextProps),e.create(void 0,void 0,this._context))))))))}_destroy(){return Promise.resolve().then((()=>{this._stopErrorHandling();const e=this._context;return this._context=null,this._contextProps=new Set,Promise.all(Array.from(this._watchdogs.values()).map((e=>e.destroy()))).then((()=>this._destructor(e)))}))}_getWatchdog(e){const t=this._watchdogs.get(e);if(!t)throw new Error(`Item with the given id was not registered: ${e}.`);return t}_isErrorComingFromThisItem(e){for(const t of this._watchdogs.values())if(t._isErrorComingFromThisItem(e))return!1;return yp(this._context,e.context)}};const Sp=Tp;function Pp(e){return An(e)}const Ip=["left","right","center","justify"];function Vp(e){return Ip.includes(e)}function Rp(e,t){return"rtl"==t.contentLanguageDirection?"right"===e:"left"===e}function Op(e){const t=e.map((e=>{let t;return t="string"==typeof e?{name:e}:e,t})).filter((e=>{const t=Ip.includes(e.name);return t||k("alignment-config-name-not-recognized",{option:e}),t})),i=t.filter((e=>Boolean(e.className))).length;if(i&&i{const s=n.slice(i+1);if(s.some((e=>e.name==t.name)))throw new y("alignment-config-name-already-defined",{option:t,configuredOptions:e});if(t.className){if(s.some((e=>e.className==t.className)))throw new y("alignment-config-classname-already-defined",{option:t,configuredOptions:e})}})),t}const Bp="alignment";class Mp extends ro{refresh(){const e=this.editor.locale,t=Zs(this.editor.model.document.selection.getSelectedBlocks());this.isEnabled=Boolean(t)&&this._canBeAligned(t),this.isEnabled&&t.hasAttribute("alignment")?this.value=t.getAttribute("alignment"):this.value="rtl"===e.contentLanguageDirection?"right":"left"}execute(e={}){const t=this.editor,i=t.locale,n=t.model,s=n.document,o=e.value;n.change((e=>{const t=Array.from(s.selection.getSelectedBlocks()).filter((e=>this._canBeAligned(e))),n=t[0].getAttribute("alignment");Rp(o,i)||n===o||!o?function(e,t){for(const i of e)t.removeAttribute(Bp,i)}(t,e):function(e,t,i){for(const n of e)t.setAttribute(Bp,i,n)}(t,e,o)}))}_canBeAligned(e){return this.editor.model.schema.checkAttribute(e,Bp)}}class Np extends so{static get pluginName(){return"AlignmentEditing"}constructor(e){super(e),e.config.define("alignment",{options:Ip.map((e=>({name:e})))})}init(){const e=this.editor,t=e.locale,i=e.model.schema,n=Op(e.config.get("alignment.options")).filter((e=>Vp(e.name)&&!Rp(e.name,t))),s=n.some((e=>!!e.className));i.extend("$block",{allowAttributes:"alignment"}),e.model.schema.setAttributeProperties("alignment",{isFormatting:!0}),s?e.conversion.attributeToAttribute(function(e){const t={};for(const i of e)t[i.name]={key:"class",value:i.className};const i={model:{key:"alignment",values:e.map((e=>e.name))},view:t};return i}(n)):e.conversion.for("downcast").attributeToAttribute(function(e){const t={};for(const{name:i}of e)t[i]={key:"style",value:{"text-align":i}};const i={model:{key:"alignment",values:e.map((e=>e.name))},view:t};return i}(n));const o=function(e){const t=[];for(const{name:i}of e)t.push({view:{key:"style",value:{"text-align":i}},model:{key:"alignment",value:i}});return t}(n);for(const t of o)e.conversion.for("upcast").attributeToAttribute(t);const r=function(e){const t=[];for(const{name:i}of e)t.push({view:{key:"align",value:i},model:{key:"alignment",value:i}});return t}(n);for(const t of r)e.conversion.for("upcast").attributeToAttribute(t);e.commands.add("alignment",new Mp(e))}}const Fp=new Map([["left",fu.alignLeft],["right",fu.alignRight],["center",fu.alignCenter],["justify",fu.alignJustify]]);class Dp extends so{get localizedOptionTitles(){const e=this.editor.t;return{left:e("Align left"),right:e("Align right"),center:e("Align center"),justify:e("Justify")}}static get pluginName(){return"AlignmentUI"}init(){const e=Op(this.editor.config.get("alignment.options"));e.map((e=>e.name)).filter(Vp).forEach((e=>this._addButton(e))),this._addToolbarDropdown(e),this._addMenuBarMenu(e)}_addButton(e){this.editor.ui.componentFactory.add(`alignment:${e}`,(t=>this._createButton(t,e)))}_createButton(e,t,i={}){const n=this.editor,s=n.commands.get("alignment"),o=new Ku(e);return o.set({label:this.localizedOptionTitles[t],icon:Fp.get(t),tooltip:!0,isToggleable:!0,...i}),o.bind("isEnabled").to(s),o.bind("isOn").to(s,"value",(e=>e===t)),this.listenTo(o,"execute",(()=>{n.execute("alignment",{value:t}),n.editing.view.focus()})),o}_addToolbarDropdown(e){const t=this.editor;t.ui.componentFactory.add("alignment",(i=>{const n=Dm(i),s="rtl"===i.uiLanguageDirection?"w":"e",o=i.t;Lm(n,(()=>e.map((e=>this._createButton(i,e.name,{tooltipPosition:s})))),{enableActiveItemFocusOnDropdownOpen:!0,isVertical:!0,ariaLabel:o("Text alignment toolbar")}),n.buttonView.set({label:o("Text alignment"),tooltip:!0}),n.extendTemplate({attributes:{class:"ck-alignment-dropdown"}});const r="rtl"===i.contentLanguageDirection?Fp.get("right"):Fp.get("left"),a=t.commands.get("alignment");return n.buttonView.bind("icon").to(a,"value",(e=>Fp.get(e)||r)),n.bind("isEnabled").to(a,"isEnabled"),this.listenTo(n,"execute",(()=>{t.editing.view.focus()})),n}))}_addMenuBarMenu(e){const t=this.editor;t.ui.componentFactory.add("menuBar:alignment",(i=>{const n=t.commands.get("alignment"),s=i.t,o=new dp(i),r=new hp(i);o.bind("isEnabled").to(n),r.set({ariaLabel:s("Text alignment"),role:"menu"}),o.buttonView.set({label:s("Text alignment")});for(const s of e){const e=new qf(i,o),a=new up(i);a.extendTemplate({attributes:{"aria-checked":a.bindTemplate.to("isOn")}}),a.delegate("execute").to(o),a.set({label:this.localizedOptionTitles[s.name],icon:Fp.get(s.name)}),a.on("execute",(()=>{t.execute("alignment",{value:s.name}),t.editing.view.focus()})),a.bind("isOn").to(n,"value",(e=>e===s.name)),a.bind("isEnabled").to(n,"isEnabled"),e.children.add(a),r.items.add(e)}return o.panelView.children.add(r),o}))}}class Lp{constructor(e,t=20){this._batch=null,this.model=e,this._size=0,this.limit=t,this._isLocked=!1,this._changeCallback=(e,t)=>{t.isLocal&&t.isUndoable&&t!==this._batch&&this._reset(!0)},this._selectionChangeCallback=()=>{this._reset()},this.model.document.on("change",this._changeCallback),this.model.document.selection.on("change:range",this._selectionChangeCallback),this.model.document.selection.on("change:attribute",this._selectionChangeCallback)}get batch(){return this._batch||(this._batch=this.model.createBatch({isTyping:!0})),this._batch}get size(){return this._size}input(e){this._size+=e,this._size>=this.limit&&this._reset(!0)}get isLocked(){return this._isLocked}lock(){this._isLocked=!0}unlock(){this._isLocked=!1}destroy(){this.model.document.off("change",this._changeCallback),this.model.document.selection.off("change:range",this._selectionChangeCallback),this.model.document.selection.off("change:attribute",this._selectionChangeCallback)}_reset(e=!1){this.isLocked&&!e||(this._batch=null,this._size=0)}}class zp extends ro{constructor(e,t){super(e),this._buffer=new Lp(e.model,t),this._isEnabledBasedOnSelection=!1}get buffer(){return this._buffer}destroy(){super.destroy(),this._buffer.destroy()}execute(e={}){const t=this.editor.model,i=t.document,n=e.text||"",s=n.length;let o=i.selection;if(e.selection?o=e.selection:e.range&&(o=t.createSelection(e.range)),!t.canEditAt(o))return;const r=e.resultRange;t.enqueueChange(this._buffer.batch,(e=>{this._buffer.lock();const a=Array.from(i.selection.getAttributes());t.deleteContent(o),n&&t.insertContent(e.createText(n,a),o),r?e.setSelection(r):o.is("documentSelection")||e.setSelection(o),this._buffer.unlock(),this._buffer.input(s)}))}}const Hp=["insertText","insertReplacementText"];class $p extends ka{constructor(e){super(e),this.focusObserver=e.getObserver(dl),l.isAndroid&&Hp.push("insertCompositionText");const t=e.document;t.on("beforeinput",((i,n)=>{if(!this.isEnabled)return;const{data:s,targetRanges:o,inputType:r,domEvent:a}=n;if(!Hp.includes(r))return;this.focusObserver.flush();const l=new f(t,"insertText");t.fire(l,new Aa(e,a,{text:s,selection:e.createSelection(o)})),l.stop.called&&i.stop()})),t.on("compositionend",((i,{data:n,domEvent:s})=>{this.isEnabled&&!l.isAndroid&&n&&t.fire("insertText",new Aa(e,s,{text:n,selection:t.selection}))}),{priority:"lowest"})}observe(){}stopObserving(){}}class Wp extends so{static get pluginName(){return"Input"}init(){const e=this.editor,t=e.model,i=e.editing.view,n=t.document.selection;i.addObserver($p);const s=new zp(e,e.config.get("typing.undoStep")||20);e.commands.add("insertText",s),e.commands.add("input",s),this.listenTo(i.document,"insertText",((n,s)=>{i.document.isComposing||s.preventDefault();const{text:o,selection:r,resultRange:a}=s,c=Array.from(r.getRanges()).map((t=>e.editing.mapper.toModelRange(t)));let d=o;if(l.isAndroid){const e=Array.from(c[0].getItems()).reduce(((e,t)=>e+(t.is("$textProxy")?t.data:"")),"");e&&(e.length<=d.length?d.startsWith(e)&&(d=d.substring(e.length),c[0].start=c[0].start.getShiftedBy(e.length)):e.startsWith(d)&&(c[0].start=c[0].start.getShiftedBy(d.length),d=""))}const h={text:d,selection:t.createSelection(c)};a&&(h.resultRange=e.editing.mapper.toModelRange(a)),e.execute("insertText",h),i.scrollToTheSelection()})),l.isAndroid?this.listenTo(i.document,"keydown",((e,o)=>{!n.isCollapsed&&229==o.keyCode&&i.document.isComposing&&jp(t,s)})):this.listenTo(i.document,"compositionstart",(()=>{n.isCollapsed||jp(t,s)}))}}function jp(e,t){if(!t.isEnabled)return;const i=t.buffer;i.lock(),e.enqueueChange(i.batch,(()=>{e.deleteContent(e.document.selection)})),i.unlock()}class Up extends ro{constructor(e,t){super(e),this.direction=t,this._buffer=new Lp(e.model,e.config.get("typing.undoStep")),this._isEnabledBasedOnSelection=!1}get buffer(){return this._buffer}execute(e={}){const t=this.editor.model,i=t.document;t.enqueueChange(this._buffer.batch,(n=>{this._buffer.lock();const s=n.createSelection(e.selection||i.selection);if(!t.canEditAt(s))return;const o=e.sequence||1,r=s.isCollapsed;if(s.isCollapsed&&t.modifySelection(s,{direction:this.direction,unit:e.unit,treatEmojiAsSingleUnit:!0}),this._shouldEntireContentBeReplacedWithParagraph(o))return void this._replaceEntireContentWithParagraph(n);if(this._shouldReplaceFirstBlockWithParagraph(s,o))return void this.editor.execute("paragraph",{selection:s});if(s.isCollapsed)return;let a=0;s.getFirstRange().getMinimalFlatRanges().forEach((e=>{a+=ee(e.getWalker({singleCharacters:!0,ignoreElementEnd:!0,shallow:!0}))})),t.deleteContent(s,{doNotResetEntireContent:r,direction:this.direction}),this._buffer.input(a),n.setSelection(s),this._buffer.unlock()}))}_shouldEntireContentBeReplacedWithParagraph(e){if(e>1)return!1;const t=this.editor.model,i=t.document.selection,n=t.schema.getLimitElement(i);if(!(i.isCollapsed&&i.containsEntireContent(n)))return!1;if(!t.schema.checkChild(n,"paragraph"))return!1;const s=n.getChild(0);return!s||!s.is("element","paragraph")}_replaceEntireContentWithParagraph(e){const t=this.editor.model,i=t.document.selection,n=t.schema.getLimitElement(i),s=e.createElement("paragraph");e.remove(e.createRangeIn(n)),e.insert(s,n),e.setSelection(s,0)}_shouldReplaceFirstBlockWithParagraph(e,t){const i=this.editor.model;if(t>1||"backward"!=this.direction)return!1;if(!e.isCollapsed)return!1;const n=e.getFirstPosition(),s=i.schema.getLimitElement(n),o=s.getChild(0);return n.parent==o&&(!!e.containsEntireContent(o)&&(!!i.schema.checkChild(s,"paragraph")&&"paragraph"!=o.name))}}const qp="word",Gp="selection",Kp="backward",Zp="forward",Jp={deleteContent:{unit:Gp,direction:Kp},deleteContentBackward:{unit:"codePoint",direction:Kp},deleteWordBackward:{unit:qp,direction:Kp},deleteHardLineBackward:{unit:Gp,direction:Kp},deleteSoftLineBackward:{unit:Gp,direction:Kp},deleteContentForward:{unit:"character",direction:Zp},deleteWordForward:{unit:qp,direction:Zp},deleteHardLineForward:{unit:Gp,direction:Zp},deleteSoftLineForward:{unit:Gp,direction:Zp}};class Qp extends ka{constructor(e){super(e);const t=e.document;let i=0;t.on("keydown",(()=>{i++})),t.on("keyup",(()=>{i=0})),t.on("beforeinput",((n,s)=>{if(!this.isEnabled)return;const{targetRanges:o,domEvent:r,inputType:a}=s,c=Jp[a];if(!c)return;const d={direction:c.direction,unit:c.unit,sequence:i};d.unit==Gp&&(d.selectionToRemove=e.createSelection(o[0])),"deleteContentBackward"===a&&(l.isAndroid&&(d.sequence=1),function(e){if(1!=e.length||e[0].isCollapsed)return!1;const t=e[0].getWalker({direction:"backward",singleCharacters:!0,ignoreElementEnd:!0});let i=0;for(const{nextPosition:e}of t){if(e.parent.is("$text")){const t=e.parent.data,n=e.offset;if(eo(t,n)||to(t,n)||no(t,n))continue;i++}else i++;if(i>1)return!0}return!1}(o)&&(d.unit=Gp,d.selectionToRemove=e.createSelection(o)));const h=new _r(t,"delete",o[0]);t.fire(h,new Aa(e,r,d)),h.stop.called&&n.stop()})),l.isBlink&&function(e){const t=e.view,i=t.document;let n=null,s=!1;function o(e){return e==bs.backspace||e==bs.delete}function r(e){return e==bs.backspace?Kp:Zp}i.on("keydown",((e,{keyCode:t})=>{n=t,s=!1})),i.on("keyup",((a,{keyCode:l,domEvent:c})=>{const d=i.selection,h=e.isEnabled&&l==n&&o(l)&&!d.isCollapsed&&!s;if(n=null,h){const e=d.getFirstRange(),n=new _r(i,"delete",e),s={unit:Gp,direction:r(l),selectionToRemove:d};i.fire(n,new Aa(t,c,s))}})),i.on("beforeinput",((e,{inputType:t})=>{const i=Jp[t];o(n)&&i&&i.direction==r(n)&&(s=!0)}),{priority:"high"}),i.on("beforeinput",((e,{inputType:t,data:i})=>{n==bs.delete&&"insertText"==t&&""==i&&e.stop()}),{priority:"high"})}(this)}observe(){}stopObserving(){}}class Yp extends so{static get pluginName(){return"Delete"}init(){const e=this.editor,t=e.editing.view,i=t.document,n=e.model.document;t.addObserver(Qp),this._undoOnBackspace=!1;const s=new Up(e,"forward");e.commands.add("deleteForward",s),e.commands.add("forwardDelete",s),e.commands.add("delete",new Up(e,"backward")),this.listenTo(i,"delete",((n,s)=>{i.isComposing||s.preventDefault();const{direction:o,sequence:r,selectionToRemove:a,unit:l}=s,c="forward"===o?"deleteForward":"delete",d={sequence:r};if("selection"==l){const t=Array.from(a.getRanges()).map((t=>e.editing.mapper.toModelRange(t)));d.selection=e.model.createSelection(t)}else d.unit=l;e.execute(c,d),t.scrollToTheSelection()}),{priority:"low"}),this.editor.plugins.has("UndoEditing")&&(this.listenTo(i,"delete",((t,i)=>{this._undoOnBackspace&&"backward"==i.direction&&1==i.sequence&&"codePoint"==i.unit&&(this._undoOnBackspace=!1,e.execute("undo"),i.preventDefault(),t.stop())}),{context:"$capture"}),this.listenTo(n,"change",(()=>{this._undoOnBackspace=!1})))}requestUndoOnBackspace(){this.editor.plugins.has("UndoEditing")&&(this._undoOnBackspace=!0)}}class Xp extends so{static get requires(){return[Wp,Yp]}static get pluginName(){return"Typing"}}function eb(e,t){let i=e.start;return{text:Array.from(e.getWalker({ignoreElementEnd:!1})).reduce(((e,{item:n})=>n.is("$text")||n.is("$textProxy")?e+n.data:(i=t.createPositionAfter(n),"")),""),range:t.createRange(i,e.end)}}class tb extends(G()){constructor(e,t){super(),this.model=e,this.testCallback=t,this._hasMatch=!1,this.set("isEnabled",!0),this.on("change:isEnabled",(()=>{this.isEnabled?this._startListening():(this.stopListening(e.document.selection),this.stopListening(e.document))})),this._startListening()}get hasMatch(){return this._hasMatch}_startListening(){const e=this.model.document;this.listenTo(e.selection,"change:range",((t,{directChange:i})=>{i&&(e.selection.isCollapsed?this._evaluateTextBeforeSelection("selection"):this.hasMatch&&(this.fire("unmatched"),this._hasMatch=!1))})),this.listenTo(e,"change:data",((e,t)=>{!t.isUndo&&t.isLocal&&this._evaluateTextBeforeSelection("data",{batch:t})}))}_evaluateTextBeforeSelection(e,t={}){const i=this.model,n=i.document.selection,s=i.createRange(i.createPositionAt(n.focus.parent,0),n.focus),{text:o,range:r}=eb(s,i),a=this.testCallback(o);if(!a&&this.hasMatch&&this.fire("unmatched"),this._hasMatch=!!a,a){const i=Object.assign(t,{text:o,range:r});"object"==typeof a&&Object.assign(i,a),this.fire(`matched:${e}`,i)}}}class ib extends so{static get pluginName(){return"TwoStepCaretMovement"}constructor(e){super(e),this._isNextGravityRestorationSkipped=!1,this.attributes=new Set,this._overrideUid=null}init(){const e=this.editor,t=e.model,i=e.editing.view,n=e.locale,s=t.document.selection;this.listenTo(i.document,"arrowKey",((e,t)=>{if(!s.isCollapsed)return;if(t.shiftKey||t.altKey||t.ctrlKey)return;const i=t.keyCode==bs.arrowright,o=t.keyCode==bs.arrowleft;if(!i&&!o)return;const r=n.contentLanguageDirection;let a=!1;a="ltr"===r&&i||"rtl"===r&&o?this._handleForwardMovement(t):this._handleBackwardMovement(t),!0===a&&e.stop()}),{context:"$text",priority:"highest"}),this.listenTo(s,"change:range",((e,t)=>{this._isNextGravityRestorationSkipped?this._isNextGravityRestorationSkipped=!1:this._isGravityOverridden&&(!t.directChange&&lb(s.getFirstPosition(),this.attributes)||this._restoreGravity())})),this._enableClickingAfterNode(),this._enableInsertContentSelectionAttributesFixer(),this._handleDeleteContentAfterNode()}registerAttribute(e){this.attributes.add(e)}_handleForwardMovement(e){const t=this.attributes,i=this.editor.model,n=i.document.selection,s=n.getFirstPosition();return!this._isGravityOverridden&&((!s.isAtStart||!nb(n,t))&&(!!lb(s,t)&&(rb(e),nb(n,t)&&lb(s,t,!0)?ob(i,t):this._overrideGravity(),!0)))}_handleBackwardMovement(e){const t=this.attributes,i=this.editor.model,n=i.document.selection,s=n.getFirstPosition();return this._isGravityOverridden?(rb(e),this._restoreGravity(),lb(s,t,!0)?ob(i,t):sb(i,t,s),!0):s.isAtStart?!!nb(n,t)&&(rb(e),sb(i,t,s),!0):!nb(n,t)&&lb(s,t,!0)?(rb(e),sb(i,t,s),!0):!!ab(s,t)&&(s.isAtEnd&&!nb(n,t)&&lb(s,t)?(rb(e),sb(i,t,s),!0):(this._isNextGravityRestorationSkipped=!0,this._overrideGravity(),!1))}_enableClickingAfterNode(){const e=this.editor,t=e.model,i=t.document.selection,n=e.editing.view.document;e.editing.view.addObserver(vh);let s=!1;this.listenTo(n,"mousedown",(()=>{s=!0})),this.listenTo(n,"selectionChange",(()=>{const e=this.attributes;if(!s)return;if(s=!1,!i.isCollapsed)return;if(!nb(i,e))return;const n=i.getFirstPosition();lb(n,e)&&(n.isAtStart||lb(n,e,!0)?ob(t,e):this._isGravityOverridden||this._overrideGravity())}))}_enableInsertContentSelectionAttributesFixer(){const e=this.editor.model,t=e.document.selection,i=this.attributes;this.listenTo(e,"insertContent",(()=>{const n=t.getFirstPosition();nb(t,i)&&lb(n,i)&&ob(e,i)}),{priority:"low"})}_handleDeleteContentAfterNode(){const e=this.editor,t=e.model,i=t.document.selection,n=e.editing.view;let s=!1,o=!1;this.listenTo(n.document,"delete",((e,t)=>{s="backward"===t.direction}),{priority:"high"}),this.listenTo(t,"deleteContent",(()=>{if(!s)return;const e=i.getFirstPosition();o=nb(i,this.attributes)&&!ab(e,this.attributes)}),{priority:"high"}),this.listenTo(t,"deleteContent",(()=>{s&&(s=!1,o||e.model.enqueueChange((()=>{const e=i.getFirstPosition();nb(i,this.attributes)&&lb(e,this.attributes)&&(e.isAtStart||lb(e,this.attributes,!0)?ob(t,this.attributes):this._isGravityOverridden||this._overrideGravity())})))}),{priority:"low"})}get _isGravityOverridden(){return!!this._overrideUid}_overrideGravity(){this._overrideUid=this.editor.model.change((e=>e.overrideSelectionGravity()))}_restoreGravity(){this.editor.model.change((e=>{e.restoreSelectionGravity(this._overrideUid),this._overrideUid=null}))}}function nb(e,t){for(const i of t)if(e.hasAttribute(i))return!0;return!1}function sb(e,t,i){const n=i.nodeBefore;e.change((i=>{if(n){const t=[],s=e.schema.isObject(n)&&e.schema.isInline(n);for(const[i,o]of n.getAttributes())!e.schema.checkAttribute("$text",i)||s&&!1===e.schema.getAttributeProperties(i).copyFromObject||t.push([i,o]);i.setSelectionAttribute(t)}else i.removeSelectionAttribute(t)}))}function ob(e,t){e.change((e=>{e.removeSelectionAttribute(t)}))}function rb(e){e.preventDefault()}function ab(e,t){return lb(e.getShiftedBy(-1),t)}function lb(e,t,i=!1){const{nodeBefore:n,nodeAfter:s}=e;for(const e of t){const t=n?n.getAttribute(e):void 0,o=s?s.getAttribute(e):void 0;if((!i||void 0!==t&&void 0!==o)&&o!==t)return!0}return!1}const cb={copyright:{from:"(c)",to:"©"},registeredTrademark:{from:"(r)",to:"®"},trademark:{from:"(tm)",to:"™"},oneHalf:{from:/(^|[^/a-z0-9])(1\/2)([^/a-z0-9])$/i,to:[null,"½",null]},oneThird:{from:/(^|[^/a-z0-9])(1\/3)([^/a-z0-9])$/i,to:[null,"⅓",null]},twoThirds:{from:/(^|[^/a-z0-9])(2\/3)([^/a-z0-9])$/i,to:[null,"⅔",null]},oneForth:{from:/(^|[^/a-z0-9])(1\/4)([^/a-z0-9])$/i,to:[null,"¼",null]},threeQuarters:{from:/(^|[^/a-z0-9])(3\/4)([^/a-z0-9])$/i,to:[null,"¾",null]},lessThanOrEqual:{from:"<=",to:"≤"},greaterThanOrEqual:{from:">=",to:"≥"},notEqual:{from:"!=",to:"≠"},arrowLeft:{from:"<-",to:"←"},arrowRight:{from:"->",to:"→"},horizontalEllipsis:{from:"...",to:"…"},enDash:{from:/(^| )(--)( )$/,to:[null,"–",null]},emDash:{from:/(^| )(---)( )$/,to:[null,"—",null]},quotesPrimary:{from:fb('"'),to:[null,"“",null,"”"]},quotesSecondary:{from:fb("'"),to:[null,"‘",null,"’"]},quotesPrimaryEnGb:{from:fb("'"),to:[null,"‘",null,"’"]},quotesSecondaryEnGb:{from:fb('"'),to:[null,"“",null,"”"]},quotesPrimaryPl:{from:fb('"'),to:[null,"„",null,"”"]},quotesSecondaryPl:{from:fb("'"),to:[null,"‚",null,"’"]}},db={symbols:["copyright","registeredTrademark","trademark"],mathematical:["oneHalf","oneThird","twoThirds","oneForth","threeQuarters","lessThanOrEqual","greaterThanOrEqual","notEqual","arrowLeft","arrowRight"],typography:["horizontalEllipsis","enDash","emDash"],quotes:["quotesPrimary","quotesSecondary"]},hb=["symbols","mathematical","typography","quotes"];function ub(e){return"string"==typeof e?new RegExp(`(${zf(e)})$`):e}function mb(e){return"string"==typeof e?()=>[e]:e instanceof Array?()=>e:e}function gb(e){return(e.textNode?e.textNode:e.nodeAfter).getAttributes()}function fb(e){return new RegExp(`(^|\\s)(${e})([^${e}]*)(${e})$`)}function pb(e,t,i,n){return n.createRange(bb(e,t,i,!0,n),bb(e,t,i,!1,n))}function bb(e,t,i,n,s){let o=e.textNode||(n?e.nodeBefore:e.nodeAfter),r=null;for(;o&&o.getAttribute(t)==i;)r=o,o=n?o.previousSibling:o.nextSibling;return r?s.createPositionAt(r,n?"before":"after"):e}function wb(e,t,i,n){const s=e.editing.view,o=new Set;s.document.registerPostFixer((s=>{const r=e.model.document.selection;let a=!1;if(r.hasAttribute(t)){const l=pb(r.getFirstPosition(),t,r.getAttribute(t),e.model),c=e.editing.mapper.toViewRange(l);for(const e of c.getItems())e.is("element",i)&&!e.hasClass(n)&&(s.addClass(n,e),o.add(e),a=!0)}return a})),e.conversion.for("editingDowncast").add((e=>{function t(){s.change((e=>{for(const t of o.values())e.removeClass(n,t),o.delete(t)}))}e.on("insert",t,{priority:"highest"}),e.on("remove",t,{priority:"highest"}),e.on("attribute",t,{priority:"highest"}),e.on("selection",t,{priority:"highest"})}))}function vb(e,t,i,n){let s,o=null;"function"==typeof n?s=n:(o=e.commands.get(n),s=()=>{e.execute(n)}),e.model.document.on("change:data",((r,a)=>{if(o&&!o.isEnabled||!t.isEnabled)return;const l=Zs(e.model.document.selection.getRanges());if(!l.isCollapsed)return;if(a.isUndo||!a.isLocal)return;const c=Array.from(e.model.document.differ.getChanges()),d=c[0];if(1!=c.length||"insert"!==d.type||"$text"!=d.name||1!=d.length)return;const h=d.position.parent;if(h.is("element","codeBlock"))return;if(h.is("element","listItem")&&"function"!=typeof n&&!["numberedList","bulletedList","todoList"].includes(n))return;if(o&&!0===o.value)return;const u=h.getChild(0),m=e.model.createRangeOn(u);if(!m.containsRange(l)&&!l.end.isEqual(m.end))return;const g=i.exec(u.data.substr(0,l.end.offset));g&&e.model.enqueueChange((t=>{const i=t.createPositionAt(h,0),n=t.createPositionAt(h,g[0].length),o=new Kl(i,n);if(!1!==s({match:g})){t.remove(o);const i=e.model.document.selection.getFirstRange(),n=t.createRangeIn(h);!h.isEmpty||n.isEqual(i)||n.containsRange(i,!0)||t.remove(h)}o.detach(),e.model.enqueueChange((()=>{e.plugins.get("Delete").requestUndoOnBackspace()}))}))}))}function _b(e,t,i,n){let s,o;i instanceof RegExp?s=i:o=i,o=o||(e=>{let t;const i=[],n=[];for(;null!==(t=s.exec(e))&&!(t&&t.length<4);){let{index:e,1:s,2:o,3:r}=t;const a=s+o+r;e+=t[0].length-a.length;const l=[e,e+s.length],c=[e+s.length+o.length,e+s.length+o.length+r.length];i.push(l),i.push(c),n.push([e+s.length,e+s.length+o.length])}return{remove:i,format:n}}),e.model.document.on("change:data",((i,s)=>{if(s.isUndo||!s.isLocal||!t.isEnabled)return;const r=e.model,a=r.document.selection;if(!a.isCollapsed)return;const l=Array.from(r.document.differ.getChanges()),c=l[0];if(1!=l.length||"insert"!==c.type||"$text"!=c.name||1!=c.length)return;const d=a.focus,h=d.parent,{text:u,range:m}=function(e,t){let i=e.start;const n=Array.from(e.getItems()).reduce(((e,n)=>!n.is("$text")&&!n.is("$textProxy")||n.getAttribute("code")?(i=t.createPositionAfter(n),""):e+n.data),"");return{text:n,range:t.createRange(i,e.end)}}(r.createRange(r.createPositionAt(h,0),d),r),g=o(u),f=yb(m.start,g.format,r),p=yb(m.start,g.remove,r);f.length&&p.length&&r.enqueueChange((t=>{if(!1!==n(t,f)){for(const e of p.reverse())t.remove(e);r.enqueueChange((()=>{e.plugins.get("Delete").requestUndoOnBackspace()}))}}))}))}function yb(e,t,i){return t.filter((e=>void 0!==e[0]&&void 0!==e[1])).map((t=>i.createRange(e.getShiftedBy(t[0]),e.getShiftedBy(t[1]))))}function kb(e,t){return(i,n)=>{if(!e.commands.get(t).isEnabled)return!1;const s=e.model.schema.getValidRanges(n,t);for(const e of s)i.setAttribute(t,!0,e);i.removeSelectionAttribute(t)}}class Cb extends ro{constructor(e,t){super(e),this.attributeKey=t}refresh(){const e=this.editor.model,t=e.document;this.value=this._getValueFromFirstAllowedNode(),this.isEnabled=e.schema.checkAttributeInSelection(t.selection,this.attributeKey)}execute(e={}){const t=this.editor.model,i=t.document.selection,n=void 0===e.forceValue?!this.value:e.forceValue;t.change((e=>{if(i.isCollapsed)n?e.setSelectionAttribute(this.attributeKey,!0):e.removeSelectionAttribute(this.attributeKey);else{const s=t.schema.getValidRanges(i.getRanges(),this.attributeKey);for(const t of s)n?e.setAttribute(this.attributeKey,n,t):e.removeAttribute(this.attributeKey,t)}}))}_getValueFromFirstAllowedNode(){const e=this.editor.model,t=e.schema,i=e.document.selection;if(i.isCollapsed)return i.hasAttribute(this.attributeKey);for(const e of i.getRanges())for(const i of e.getItems())if(t.checkAttribute(i,this.attributeKey))return i.hasAttribute(this.attributeKey);return!1}}const Ab="bold";class xb extends so{static get pluginName(){return"BoldEditing"}init(){const e=this.editor,t=this.editor.t;e.model.schema.extend("$text",{allowAttributes:Ab}),e.model.schema.setAttributeProperties(Ab,{isFormatting:!0,copyOnEnter:!0}),e.conversion.attributeToElement({model:Ab,view:"strong",upcastAlso:["b",e=>{const t=e.getStyle("font-weight");return t&&("bold"==t||Number(t)>=600)?{name:!0,styles:["font-weight"]}:null}]}),e.commands.add(Ab,new Cb(e,Ab)),e.keystrokes.set("CTRL+B",Ab),e.accessibility.addKeystrokeInfos({keystrokes:[{label:t("Bold text"),keystroke:"CTRL+B"}]})}}function Eb({editor:e,commandName:t,plugin:i,icon:n,label:s,keystroke:o}){return r=>{const a=e.commands.get(t),l=new r(e.locale);return l.set({label:s,icon:n,keystroke:o,isToggleable:!0}),l.bind("isEnabled").to(a,"isEnabled"),i.listenTo(l,"execute",(()=>{e.execute(t),e.editing.view.focus()})),l}}const Tb="bold";class Sb extends so{static get pluginName(){return"BoldUI"}init(){const e=this.editor,t=e.locale.t,i=e.commands.get(Tb),n=Eb({editor:e,commandName:Tb,plugin:this,icon:fu.bold,label:t("Bold"),keystroke:"CTRL+B"});e.ui.componentFactory.add(Tb,(()=>{const e=n(Ku);return e.set({tooltip:!0}),e.bind("isOn").to(i,"value"),e})),e.ui.componentFactory.add("menuBar:"+Tb,(()=>n(up)))}}const Pb="code";class Ib extends so{static get pluginName(){return"CodeEditing"}static get requires(){return[ib]}init(){const e=this.editor,t=this.editor.t;e.model.schema.extend("$text",{allowAttributes:Pb}),e.model.schema.setAttributeProperties(Pb,{isFormatting:!0,copyOnEnter:!1}),e.conversion.attributeToElement({model:Pb,view:"code",upcastAlso:{styles:{"word-wrap":"break-word"}}}),e.commands.add(Pb,new Cb(e,Pb)),e.plugins.get(ib).registerAttribute(Pb),wb(e,Pb,"code","ck-code_selected"),e.accessibility.addKeystrokeInfos({keystrokes:[{label:t("Move out of an inline code style"),keystroke:[["arrowleft","arrowleft"],["arrowright","arrowright"]]}]})}}const Vb="code";class Rb extends so{static get pluginName(){return"CodeUI"}init(){const e=this.editor,t=e.locale.t,i=Eb({editor:e,commandName:Vb,plugin:this,icon:'',label:t("Code")});e.ui.componentFactory.add(Vb,(()=>{const t=i(Ku),n=e.commands.get(Vb);return t.set({tooltip:!0}),t.bind("isOn").to(n,"value"),t})),e.ui.componentFactory.add("menuBar:"+Vb,(()=>i(up)))}}const Ob="italic";class Bb extends so{static get pluginName(){return"ItalicEditing"}init(){const e=this.editor,t=this.editor.t;e.model.schema.extend("$text",{allowAttributes:Ob}),e.model.schema.setAttributeProperties(Ob,{isFormatting:!0,copyOnEnter:!0}),e.conversion.attributeToElement({model:Ob,view:"i",upcastAlso:["em",{styles:{"font-style":"italic"}}]}),e.commands.add(Ob,new Cb(e,Ob)),e.keystrokes.set("CTRL+I",Ob),e.accessibility.addKeystrokeInfos({keystrokes:[{label:t("Italic text"),keystroke:"CTRL+I"}]})}}const Mb="italic";class Nb extends so{static get pluginName(){return"ItalicUI"}init(){const e=this.editor,t=e.commands.get(Mb),i=e.locale.t,n=Eb({editor:e,commandName:Mb,plugin:this,icon:'',keystroke:"CTRL+I",label:i("Italic")});e.ui.componentFactory.add(Mb,(()=>{const e=n(Ku);return e.set({tooltip:!0}),e.bind("isOn").to(t,"value"),e})),e.ui.componentFactory.add("menuBar:"+Mb,(()=>n(up)))}}const Fb="strikethrough";class Db extends so{static get pluginName(){return"StrikethroughEditing"}init(){const e=this.editor,t=this.editor.t;e.model.schema.extend("$text",{allowAttributes:Fb}),e.model.schema.setAttributeProperties(Fb,{isFormatting:!0,copyOnEnter:!0}),e.conversion.attributeToElement({model:Fb,view:"s",upcastAlso:["del","strike",{styles:{"text-decoration":"line-through"}}]}),e.commands.add(Fb,new Cb(e,Fb)),e.keystrokes.set("CTRL+SHIFT+X","strikethrough"),e.accessibility.addKeystrokeInfos({keystrokes:[{label:t("Strikethrough text"),keystroke:"CTRL+SHIFT+X"}]})}}const Lb="strikethrough";class zb extends so{static get pluginName(){return"StrikethroughUI"}init(){const e=this.editor,t=e.locale.t,i=Eb({editor:e,commandName:Lb,plugin:this,icon:'',keystroke:"CTRL+SHIFT+X",label:t("Strikethrough")});e.ui.componentFactory.add(Lb,(()=>{const t=i(Ku),n=e.commands.get(Lb);return t.set({tooltip:!0}),t.bind("isOn").to(n,"value"),t})),e.ui.componentFactory.add("menuBar:"+Lb,(()=>i(up)))}}const Hb="underline";class $b extends so{static get pluginName(){return"UnderlineEditing"}init(){const e=this.editor,t=this.editor.t;e.model.schema.extend("$text",{allowAttributes:Hb}),e.model.schema.setAttributeProperties(Hb,{isFormatting:!0,copyOnEnter:!0}),e.conversion.attributeToElement({model:Hb,view:"u",upcastAlso:{styles:{"text-decoration":"underline"}}}),e.commands.add(Hb,new Cb(e,Hb)),e.keystrokes.set("CTRL+U","underline"),e.accessibility.addKeystrokeInfos({keystrokes:[{label:t("Underline text"),keystroke:"CTRL+U"}]})}}const Wb="underline";class jb extends so{static get pluginName(){return"UnderlineUI"}init(){const e=this.editor,t=e.commands.get(Wb),i=e.locale.t,n=Eb({editor:e,commandName:Wb,plugin:this,icon:'',label:i("Underline"),keystroke:"CTRL+U"});e.ui.componentFactory.add(Wb,(()=>{const e=n(Ku);return e.set({tooltip:!0}),e.bind("isOn").to(t,"value"),e})),e.ui.componentFactory.add("menuBar:"+Wb,(()=>n(up)))}}function*Ub(e,t){for(const i of t)i&&e.getAttributeProperties(i[0]).copyOnEnter&&(yield i)}class qb extends ro{execute(){this.editor.model.change((e=>{this.enterBlock(e),this.fire("afterExecute",{writer:e})}))}enterBlock(e){const t=this.editor.model,i=t.document.selection,n=t.schema,s=i.isCollapsed,o=i.getFirstRange(),r=o.start.parent,a=o.end.parent;if(n.isLimit(r)||n.isLimit(a))return s||r!=a||t.deleteContent(i),!1;if(s){const t=Ub(e.model.schema,i.getAttributes());return Gb(e,o.start),e.setSelectionAttribute(t),!0}{const n=!(o.start.isAtStart&&o.end.isAtEnd),s=r==a;if(t.deleteContent(i,{leaveUnmerged:n}),n){if(s)return Gb(e,i.focus),!0;e.setSelection(a,0)}}return!1}}function Gb(e,t){e.split(t),e.setSelection(t.parent.nextSibling,0)}const Kb={insertParagraph:{isSoft:!1},insertLineBreak:{isSoft:!0}};class Zb extends ka{constructor(e){super(e);const t=this.document;let i=!1;t.on("keydown",((e,t)=>{i=t.shiftKey})),t.on("beforeinput",((n,s)=>{if(!this.isEnabled)return;let o=s.inputType;l.isSafari&&i&&"insertParagraph"==o&&(o="insertLineBreak");const r=s.domEvent,a=Kb[o];if(!a)return;const c=new _r(t,"enter",s.targetRanges[0]);t.fire(c,new Aa(e,r,{isSoft:a.isSoft})),c.stop.called&&n.stop()}))}observe(){}stopObserving(){}}class Jb extends so{static get pluginName(){return"Enter"}init(){const e=this.editor,t=e.editing.view,i=t.document,n=this.editor.t;t.addObserver(Zb),e.commands.add("enter",new qb(e)),this.listenTo(i,"enter",((n,s)=>{i.isComposing||s.preventDefault(),s.isSoft||(e.execute("enter"),t.scrollToTheSelection())}),{priority:"low"}),e.accessibility.addKeystrokeInfos({keystrokes:[{label:n("Insert a hard break (a new paragraph)"),keystroke:"Enter"}]})}}class Qb extends ro{execute(){const e=this.editor.model,t=e.document;e.change((i=>{!function(e,t,i){const n=i.isCollapsed,s=i.getFirstRange(),o=s.start.parent,r=s.end.parent,a=o==r;if(n){const n=Ub(e.schema,i.getAttributes());Yb(e,t,s.end),t.removeSelectionAttribute(i.getAttributeKeys()),t.setSelectionAttribute(n)}else{const n=!(s.start.isAtStart&&s.end.isAtEnd);e.deleteContent(i,{leaveUnmerged:n}),a?Yb(e,t,i.focus):n&&t.setSelection(r,0)}}(e,i,t.selection),this.fire("afterExecute",{writer:i})}))}refresh(){const e=this.editor.model,t=e.document;this.isEnabled=function(e,t){if(t.rangeCount>1)return!1;const i=t.anchor;if(!i||!e.checkChild(i,"softBreak"))return!1;const n=t.getFirstRange(),s=n.start.parent,o=n.end.parent;if((Xb(s,e)||Xb(o,e))&&s!==o)return!1;return!0}(e.schema,t.selection)}}function Yb(e,t,i){const n=t.createElement("softBreak");e.insertContent(n,i),t.setSelection(n,"after")}function Xb(e,t){return!e.is("rootElement")&&(t.isLimit(e)||Xb(e.parent,t))}class ew extends so{static get pluginName(){return"ShiftEnter"}init(){const e=this.editor,t=e.model.schema,i=e.conversion,n=e.editing.view,s=n.document,o=this.editor.t;t.register("softBreak",{allowWhere:"$text",isInline:!0}),i.for("upcast").elementToElement({model:"softBreak",view:"br"}),i.for("downcast").elementToElement({model:"softBreak",view:(e,{writer:t})=>t.createEmptyElement("br")}),n.addObserver(Zb),e.commands.add("shiftEnter",new Qb(e)),this.listenTo(s,"enter",((t,i)=>{s.isComposing||i.preventDefault(),i.isSoft&&(e.execute("shiftEnter"),n.scrollToTheSelection())}),{priority:"low"}),e.accessibility.addKeystrokeInfos({keystrokes:[{label:o("Insert a soft break (a <br> element)"),keystroke:"Shift+Enter"}]})}}class tw extends ro{refresh(){this.value=this._getValue(),this.isEnabled=this._checkEnabled()}execute(e={}){const t=this.editor.model,i=t.schema,n=t.document.selection,s=Array.from(n.getSelectedBlocks()),o=void 0===e.forceValue?!this.value:e.forceValue;t.change((e=>{if(o){const t=s.filter((e=>iw(e)||sw(i,e)));this._applyQuote(e,t)}else this._removeQuote(e,s.filter(iw))}))}_getValue(){const e=Zs(this.editor.model.document.selection.getSelectedBlocks());return!(!e||!iw(e))}_checkEnabled(){if(this.value)return!0;const e=this.editor.model.document.selection,t=this.editor.model.schema,i=Zs(e.getSelectedBlocks());return!!i&&sw(t,i)}_removeQuote(e,t){nw(e,t).reverse().forEach((t=>{if(t.start.isAtStart&&t.end.isAtEnd)return void e.unwrap(t.start.parent);if(t.start.isAtStart){const i=e.createPositionBefore(t.start.parent);return void e.move(t,i)}t.end.isAtEnd||e.split(t.end);const i=e.createPositionAfter(t.end.parent);e.move(t,i)}))}_applyQuote(e,t){const i=[];nw(e,t).reverse().forEach((t=>{let n=iw(t.start);n||(n=e.createElement("blockQuote"),e.wrap(t,n)),i.push(n)})),i.reverse().reduce(((t,i)=>t.nextSibling==i?(e.merge(e.createPositionAfter(t)),t):i))}}function iw(e){return"blockQuote"==e.parent.name?e.parent:null}function nw(e,t){let i,n=0;const s=[];for(;n{const n=e.model.document.differ.getChanges();for(const e of n)if("insert"==e.type){const n=e.position.nodeAfter;if(!n)continue;if(n.is("element","blockQuote")&&n.isEmpty)return i.remove(n),!0;if(n.is("element","blockQuote")&&!t.checkChild(e.position,n))return i.unwrap(n),!0;if(n.is("element")){const e=i.createRangeIn(n);for(const n of e.getItems())if(n.is("element","blockQuote")&&!t.checkChild(i.createPositionBefore(n),n))return i.unwrap(n),!0}}else if("remove"==e.type){const t=e.position.parent;if(t.is("element","blockQuote")&&t.isEmpty)return i.remove(t),!0}return!1}));const i=this.editor.editing.view.document,n=e.model.document.selection,s=e.commands.get("blockQuote");this.listenTo(i,"enter",((t,i)=>{if(!n.isCollapsed||!s.value)return;n.getLastPosition().parent.isEmpty&&(e.execute("blockQuote"),e.editing.view.scrollToTheSelection(),i.preventDefault(),t.stop())}),{context:"blockquote"}),this.listenTo(i,"delete",((t,i)=>{if("backward"!=i.direction||!n.isCollapsed||!s.value)return;const o=n.getLastPosition().parent;o.isEmpty&&!o.previousSibling&&(e.execute("blockQuote"),e.editing.view.scrollToTheSelection(),i.preventDefault(),t.stop())}),{context:"blockquote"})}}class rw extends so{static get pluginName(){return"BlockQuoteUI"}init(){const e=this.editor,t=e.commands.get("blockQuote");e.ui.componentFactory.add("blockQuote",(()=>{const e=this._createButton(Ku);return e.set({tooltip:!0}),e.bind("isOn").to(t,"value"),e})),e.ui.componentFactory.add("menuBar:blockQuote",(()=>this._createButton(up)))}_createButton(e){const t=this.editor,i=t.locale,n=t.commands.get("blockQuote"),s=new e(t.locale),o=i.t;return s.set({label:o("Block quote"),icon:fu.quote,isToggleable:!0}),s.bind("isEnabled").to(n,"isEnabled"),this.listenTo(s,"execute",(()=>{t.execute("blockQuote"),t.editing.view.focus()})),s}}class aw extends xa{constructor(e){super(e),this.domEventType=["paste","copy","cut","drop","dragover","dragstart","dragend","dragenter","dragleave"];const t=this.document;function i(e){return(i,n)=>{n.preventDefault();const s=n.dropRange?[n.dropRange]:null,o=new f(t,e);t.fire(o,{dataTransfer:n.dataTransfer,method:i.name,targetRanges:s,target:n.target,domEvent:n.domEvent}),o.stop.called&&n.stopPropagation()}}this.listenTo(t,"paste",i("clipboardInput"),{priority:"low"}),this.listenTo(t,"drop",i("clipboardInput"),{priority:"low"}),this.listenTo(t,"dragover",i("dragging"),{priority:"low"})}onDomEvent(e){const t="clipboardData"in e?e.clipboardData:e.dataTransfer,i="drop"==e.type||"paste"==e.type,n={dataTransfer:new ml(t,{cacheFiles:i})};"drop"!=e.type&&"dragover"!=e.type||(n.dropRange=function(e,t){const i=t.target.ownerDocument,n=t.clientX,s=t.clientY;let o;i.caretRangeFromPoint&&i.caretRangeFromPoint(n,s)?o=i.caretRangeFromPoint(n,s):t.rangeParent&&(o=i.createRange(),o.setStart(t.rangeParent,t.rangeOffset),o.collapse(!0));if(o)return e.domConverter.domRangeToView(o);return null}(this.view,e)),this.fire(e.type,e,n)}}const lw=["figcaption","li"],cw=["ol","ul"];function dw(e){if(e.is("$text")||e.is("$textProxy"))return e.data;if(e.is("element","img")&&e.hasAttribute("alt"))return e.getAttribute("alt");if(e.is("element","br"))return"\n";let t="",i=null;for(const n of e.getChildren())t+=hw(n,i)+dw(n),i=n;return t}function hw(e,t){return t?e.is("element","li")&&!e.isEmpty&&e.getChild(0).is("containerElement")||cw.includes(e.name)&&cw.includes(t.name)?"\n\n":e.is("containerElement")||t.is("containerElement")?lw.includes(e.name)||lw.includes(t.name)?"\n":"\n\n":"":""}const uw=function(e,t){return e&&xs(e,t,ci)};const mw=function(e,t,i,n){var s=i.length,o=s,r=!n;if(null==e)return!o;for(e=Object(e);s--;){var a=i[s];if(r&&a[2]?a[1]!==e[a[0]]:!(a[0]in e))return!1}for(;++se.model.getSelectedContent(e.model.document.selection))){return this.editor.model.change((n=>{const s=n.model.document.selection;n.setSelection(t);const o=this._insertFakeMarkersIntoSelection(n,n.model.document.selection,e),r=i(n),a=this._removeFakeMarkersInsideElement(n,r);for(const[e,t]of Object.entries(o)){a[e]||(a[e]=n.createRangeIn(r));for(const e of t)n.remove(e)}r.markers.clear();for(const[e,t]of Object.entries(a))r.markers.set(e,t);return n.setSelection(s),r}))}_pasteMarkersIntoTransformedElement(e,t){const i=this._getPasteMarkersFromRangeMap(e);return this.editor.model.change((e=>{const n=this._insertFakeMarkersElements(e,i),s=t(e),o=this._removeFakeMarkersInsideElement(e,s);for(const t of Object.values(n).flat())e.remove(t);for(const[t,i]of Object.entries(o))e.model.markers.has(t)||e.addMarker(t,{usingOperation:!0,affectsData:!0,range:i});return s}))}_pasteFragmentWithMarkers(e){const t=this._getPasteMarkersFromRangeMap(e.markers);e.markers.clear();for(const i of t)e.markers.set(i.name,i.range);return this.editor.model.insertContent(e)}_forceMarkersCopy(e,t,i={allowedActions:"all",copyPartiallySelected:!0,duplicateOnPaste:!0}){const n=this._markersToCopy.get(e);this._markersToCopy.set(e,i),t(),n?this._markersToCopy.set(e,n):this._markersToCopy.delete(e)}_isMarkerCopyable(e,t){const i=this._getMarkerClipboardConfig(e);if(!i)return!1;if(!t)return!0;const{allowedActions:n}=i;return"all"===n||n.includes(t)}_hasMarkerConfiguration(e){return!!this._getMarkerClipboardConfig(e)}_getMarkerClipboardConfig(e){const[t]=e.split(":");return this._markersToCopy.get(t)||null}_insertFakeMarkersIntoSelection(e,t,i){const n=this._getCopyableMarkersFromSelection(e,t,i);return this._insertFakeMarkersElements(e,n)}_getCopyableMarkersFromSelection(e,t,i){const n=Array.from(t.getRanges()),s=new Set(n.flatMap((t=>Array.from(e.model.markers.getMarkersIntersectingRange(t)))));return Array.from(s).filter((e=>{if(!this._isMarkerCopyable(e.name,i))return!1;const{copyPartiallySelected:t}=this._getMarkerClipboardConfig(e.name);if(!t){const t=e.getRange();return n.some((e=>e.containsRange(t,!0)))}return!0})).map((e=>({name:"dragstart"===i?this._getUniqueMarkerName(e.name):e.name,range:e.getRange()})))}_getPasteMarkersFromRangeMap(e,t=null){const{model:i}=this.editor;return(e instanceof Map?Array.from(e.entries()):Object.entries(e)).flatMap((([e,n])=>{if(!this._hasMarkerConfiguration(e))return[{name:e,range:n}];if(this._isMarkerCopyable(e,t)){const t=this._getMarkerClipboardConfig(e),s=i.markers.has(e)&&"$graveyard"===i.markers.get(e).getRange().root.rootName;return(t.duplicateOnPaste||s)&&(e=this._getUniqueMarkerName(e)),[{name:e,range:n}]}return[]}))}_insertFakeMarkersElements(e,t){const i={},n=t.flatMap((e=>{const{start:t,end:i}=e.range;return[{position:t,marker:e,type:"start"},{position:i,marker:e,type:"end"}]})).sort((({position:e},{position:t})=>e.isBefore(t)?1:-1));for(const{position:t,marker:s,type:o}of n){const n=e.createElement("$marker",{"data-name":s.name,"data-type":o});i[s.name]||(i[s.name]=[]),i[s.name].push(n),e.insert(n,t)}return i}_removeFakeMarkersInsideElement(e,t){const i=this._getAllFakeMarkersFromElement(e,t).reduce(((t,i)=>{const n=i.markerElement&&e.createPositionBefore(i.markerElement);let s=t[i.name],o=!1;if(s&&s.start&&s.end){this._getMarkerClipboardConfig(i.name).duplicateOnPaste?t[this._getUniqueMarkerName(i.name)]=t[i.name]:o=!0,s=null}return o||(t[i.name]={...s,[i.type]:n}),i.markerElement&&e.remove(i.markerElement),t}),{});return Ew(i,(i=>new Bl(i.start||e.createPositionFromPath(t,[0]),i.end||e.createPositionAt(t,"end"))))}_getAllFakeMarkersFromElement(e,t){const i=Array.from(e.createRangeIn(t)).flatMap((({item:e})=>{if(!e.is("element","$marker"))return[];const t=e.getAttribute("data-name"),i=e.getAttribute("data-type");return[{markerElement:e,name:t,type:i}]})),n=[],s=[];for(const e of i){if("end"===e.type){i.some((t=>t.name===e.name&&"start"===t.type))||n.push({markerElement:null,name:e.name,type:"start"})}if("start"===e.type){i.some((t=>t.name===e.name&&"end"===t.type))||s.unshift({markerElement:null,name:e.name,type:"end"})}}return[...n,...i,...s]}_getUniqueMarkerName(e){const t=e.split(":"),i=b().substring(1,6);return 3===t.length?`${t.slice(0,2).join(":")}:${i}`:`${t.join(":")}:${i}`}}class Sw extends so{static get pluginName(){return"ClipboardPipeline"}static get requires(){return[Tw]}init(){this.editor.editing.view.addObserver(aw),this._setupPasteDrop(),this._setupCopyCut()}_fireOutputTransformationEvent(e,t,i){const n=this.editor.plugins.get("ClipboardMarkersUtils");this.editor.model.enqueueChange({isUndoable:"cut"===i},(()=>{const s=n._copySelectedFragmentWithMarkers(i,t);this.fire("outputTransformation",{dataTransfer:e,content:s,method:i})}))}_setupPasteDrop(){const e=this.editor,t=e.model,i=e.editing.view,n=i.document,s=this.editor.plugins.get("ClipboardMarkersUtils");this.listenTo(n,"clipboardInput",((t,i)=>{"paste"!=i.method||e.model.canEditAt(e.model.document.selection)||t.stop()}),{priority:"highest"}),this.listenTo(n,"clipboardInput",((e,t)=>{const n=t.dataTransfer;let s;if(t.content)s=t.content;else{let e="";n.getData("text/html")?e=function(e){return e.replace(/(\s+)<\/span>/g,((e,t)=>1==t.length?" ":t)).replace(//g,"")}(n.getData("text/html")):n.getData("text/plain")&&(((o=(o=n.getData("text/plain")).replace(/&/g,"&").replace(//g,">").replace(/\r?\n\r?\n/g,"

    ").replace(/\r?\n/g,"
    ").replace(/\t/g,"    ").replace(/^\s/," ").replace(/\s$/," ").replace(/\s\s/g,"  ")).includes("

    ")||o.includes("
    "))&&(o=`

    ${o}

    `),e=o),s=this.editor.data.htmlProcessor.toView(e)}var o;const r=new f(this,"inputTransformation");this.fire(r,{content:s,dataTransfer:n,targetRanges:t.targetRanges,method:t.method}),r.stop.called&&e.stop(),i.scrollToTheSelection()}),{priority:"low"}),this.listenTo(this,"inputTransformation",((e,i)=>{if(i.content.isEmpty)return;const n=this.editor.data.toModel(i.content,"$clipboardHolder");0!=n.childCount&&(e.stop(),t.change((()=>{this.fire("contentInsertion",{content:n,method:i.method,dataTransfer:i.dataTransfer,targetRanges:i.targetRanges})})))}),{priority:"low"}),this.listenTo(this,"contentInsertion",((e,t)=>{t.resultRange=s._pasteFragmentWithMarkers(t.content)}),{priority:"low"})}_setupCopyCut(){const e=this.editor,t=e.model.document,i=e.editing.view.document,n=(e,i)=>{const n=i.dataTransfer;i.preventDefault(),this._fireOutputTransformationEvent(n,t.selection,e.name)};this.listenTo(i,"copy",n,{priority:"low"}),this.listenTo(i,"cut",((t,i)=>{e.model.canEditAt(e.model.document.selection)?n(t,i):i.preventDefault()}),{priority:"low"}),this.listenTo(this,"outputTransformation",((t,n)=>{const s=e.data.toView(n.content);i.fire("clipboardOutput",{dataTransfer:n.dataTransfer,content:s,method:n.method})}),{priority:"low"}),this.listenTo(i,"clipboardOutput",((i,n)=>{n.content.isEmpty||(n.dataTransfer.setData("text/html",this.editor.data.htmlProcessor.toData(n.content)),n.dataTransfer.setData("text/plain",dw(n.content))),"cut"==n.method&&e.model.deleteContent(t.selection)}),{priority:"low"})}}class Pw extends(V()){constructor(){super(...arguments),this._stack=[]}add(e,t){const i=this._stack,n=i[0];this._insertDescriptor(e);const s=i[0];n===s||Iw(n,s)||this.fire("change:top",{oldDescriptor:n,newDescriptor:s,writer:t})}remove(e,t){const i=this._stack,n=i[0];this._removeDescriptor(e);const s=i[0];n===s||Iw(n,s)||this.fire("change:top",{oldDescriptor:n,newDescriptor:s,writer:t})}_insertDescriptor(e){const t=this._stack,i=t.findIndex((t=>t.id===e.id));if(Iw(e,t[i]))return;i>-1&&t.splice(i,1);let n=0;for(;t[n]&&Vw(t[n],e);)n++;t.splice(n,0,e)}_removeDescriptor(e){const t=this._stack,i=t.findIndex((t=>t.id===e));i>-1&&t.splice(i,1)}}function Iw(e,t){return e&&t&&e.priority==t.priority&&Rw(e.classes)==Rw(t.classes)}function Vw(e,t){return e.priority>t.priority||!(e.priorityRw(t.classes)}function Rw(e){return Array.isArray(e)?e.sort().join(","):e}const Ow='',Bw="ck-widget",Mw="ck-widget_selected";function Nw(e){return!!e.is("element")&&!!e.getCustomProperty("widget")}function Fw(e,t,i={}){if(!e.is("containerElement"))throw new y("widget-to-widget-wrong-element-type",null,{element:e});return t.setAttribute("contenteditable","false",e),t.addClass(Bw,e),t.setCustomProperty("widget",!0,e),e.getFillerOffset=Ww,t.setCustomProperty("widgetLabel",[],e),i.label&&function(e,t){const i=e.getCustomProperty("widgetLabel");i.push(t)}(e,i.label),i.hasSelectionHandle&&function(e,t){const i=t.createUIElement("div",{class:"ck ck-widget__selection-handle"},(function(e){const t=this.toDomElement(e),i=new qu;return i.set("content",Ow),i.render(),t.appendChild(i.element),t}));t.insert(t.createPositionAt(e,0),i),t.addClass(["ck-widget_with-selection-handle"],e)}(e,t),zw(e,t),e}function Dw(e,t,i){if(t.classes&&i.addClass(Cs(t.classes),e),t.attributes)for(const n in t.attributes)i.setAttribute(n,t.attributes[n],e)}function Lw(e,t,i){if(t.classes&&i.removeClass(Cs(t.classes),e),t.attributes)for(const n in t.attributes)i.removeAttribute(n,e)}function zw(e,t,i=Dw,n=Lw){const s=new Pw;s.on("change:top",((t,s)=>{s.oldDescriptor&&n(e,s.oldDescriptor,s.writer),s.newDescriptor&&i(e,s.newDescriptor,s.writer)}));t.setCustomProperty("addHighlight",((e,t,i)=>s.add(t,i)),e),t.setCustomProperty("removeHighlight",((e,t,i)=>s.remove(t,i)),e)}function Hw(e,t,i={}){return t.addClass(["ck-editor__editable","ck-editor__nested-editable"],e),t.setAttribute("role","textbox",e),t.setAttribute("tabindex","-1",e),i.label&&t.setAttribute("aria-label",i.label,e),t.setAttribute("contenteditable",e.isReadOnly?"false":"true",e),e.on("change:isReadOnly",((i,n,s)=>{t.setAttribute("contenteditable",s?"false":"true",e)})),e.on("change:isFocused",((i,n,s)=>{s?t.addClass("ck-editor__nested-editable_focused",e):t.removeClass("ck-editor__nested-editable_focused",e)})),zw(e,t),e}function $w(e,t){const i=e.getSelectedElement();if(i){const n=qw(e);if(n)return t.createRange(t.createPositionAt(i,n))}return t.schema.findOptimalInsertionRange(e)}function Ww(){return null}const jw="widget-type-around";function Uw(e,t,i){return!!e&&Nw(e)&&!i.isInline(t)}function qw(e){return e.getAttribute(jw)}const Gw=["before","after"],Kw=(new DOMParser).parseFromString('',"image/svg+xml").firstChild,Zw="ck-widget__type-around_disabled";class Jw extends so{constructor(){super(...arguments),this._currentFakeCaretModelElement=null}static get pluginName(){return"WidgetTypeAround"}static get requires(){return[Jb,Yp]}init(){const e=this.editor,t=e.editing.view;this.on("change:isEnabled",((i,n,s)=>{t.change((e=>{for(const i of t.document.roots)s?e.removeClass(Zw,i):e.addClass(Zw,i)})),s||e.model.change((e=>{e.removeSelectionAttribute(jw)}))})),this._enableTypeAroundUIInjection(),this._enableInsertingParagraphsOnButtonClick(),this._enableInsertingParagraphsOnEnterKeypress(),this._enableInsertingParagraphsOnTypingKeystroke(),this._enableTypeAroundFakeCaretActivationUsingKeyboardArrows(),this._enableDeleteIntegration(),this._enableInsertContentIntegration(),this._enableInsertObjectIntegration(),this._enableDeleteContentIntegration()}destroy(){super.destroy(),this._currentFakeCaretModelElement=null}_insertParagraph(e,t){const i=this.editor,n=i.editing.view,s=i.model.schema.getAttributesWithProperty(e,"copyOnReplace",!0);i.execute("insertParagraph",{position:i.model.createPositionAt(e,t),attributes:s}),n.focus(),n.scrollToTheSelection()}_listenToIfEnabled(e,t,i,n){this.listenTo(e,t,((...e)=>{this.isEnabled&&i(...e)}),n)}_insertParagraphAccordingToFakeCaretPosition(){const e=this.editor.model.document.selection,t=qw(e);if(!t)return!1;const i=e.getSelectedElement();return this._insertParagraph(i,t),!0}_enableTypeAroundUIInjection(){const e=this.editor,t=e.model.schema,i=e.locale.t,n={before:i("Insert paragraph before block"),after:i("Insert paragraph after block")};e.editing.downcastDispatcher.on("insert",((e,s,o)=>{const r=o.mapper.toViewElement(s.item);if(r&&Uw(r,s.item,t)){!function(e,t,i){const n=e.createUIElement("div",{class:"ck ck-reset_all ck-widget__type-around"},(function(e){const i=this.toDomElement(e);return function(e,t){for(const i of Gw){const n=new bu({tag:"div",attributes:{class:["ck","ck-widget__type-around__button",`ck-widget__type-around__button_${i}`],title:t[i],"aria-hidden":"true"},children:[e.ownerDocument.importNode(Kw,!0)]});e.appendChild(n.render())}}(i,t),function(e){const t=new bu({tag:"div",attributes:{class:["ck","ck-widget__type-around__fake-caret"]}});e.appendChild(t.render())}(i),i}));e.insert(e.createPositionAt(i,"end"),n)}(o.writer,n,r);r.getCustomProperty("widgetLabel").push((()=>this.isEnabled?i("Press Enter to type after or press Shift + Enter to type before the widget"):""))}}),{priority:"low"})}_enableTypeAroundFakeCaretActivationUsingKeyboardArrows(){const e=this.editor,t=e.model,i=t.document.selection,n=t.schema,s=e.editing.view;function o(e){return`ck-widget_type-around_show-fake-caret_${e}`}this._listenToIfEnabled(s.document,"arrowKey",((e,t)=>{this._handleArrowKeyPress(e,t)}),{context:[Nw,"$text"],priority:"high"}),this._listenToIfEnabled(i,"change:range",((t,i)=>{i.directChange&&e.model.change((e=>{e.removeSelectionAttribute(jw)}))})),this._listenToIfEnabled(t.document,"change:data",(()=>{const t=i.getSelectedElement();if(t){if(Uw(e.editing.mapper.toViewElement(t),t,n))return}e.model.change((e=>{e.removeSelectionAttribute(jw)}))})),this._listenToIfEnabled(e.editing.downcastDispatcher,"selection",((e,t,i)=>{const s=i.writer;if(this._currentFakeCaretModelElement){const e=i.mapper.toViewElement(this._currentFakeCaretModelElement);e&&(s.removeClass(Gw.map(o),e),this._currentFakeCaretModelElement=null)}const r=t.selection.getSelectedElement();if(!r)return;const a=i.mapper.toViewElement(r);if(!Uw(a,r,n))return;const l=qw(t.selection);l&&(s.addClass(o(l),a),this._currentFakeCaretModelElement=r)})),this._listenToIfEnabled(e.ui.focusTracker,"change:isFocused",((t,i,n)=>{n||e.model.change((e=>{e.removeSelectionAttribute(jw)}))}))}_handleArrowKeyPress(e,t){const i=this.editor,n=i.model,s=n.document.selection,o=n.schema,r=i.editing.view,a=function(e,t){const i=ks(e,t);return"down"===i||"right"===i}(t.keyCode,i.locale.contentLanguageDirection),l=r.document.selection.getSelectedElement();let c;Uw(l,i.editing.mapper.toModelElement(l),o)?c=this._handleArrowKeyPressOnSelectedWidget(a):s.isCollapsed?c=this._handleArrowKeyPressWhenSelectionNextToAWidget(a):t.shiftKey||(c=this._handleArrowKeyPressWhenNonCollapsedSelection(a)),c&&(t.preventDefault(),e.stop())}_handleArrowKeyPressOnSelectedWidget(e){const t=this.editor.model,i=qw(t.document.selection);return t.change((t=>{if(!i)return t.setSelectionAttribute(jw,e?"after":"before"),!0;if(!(i===(e?"after":"before")))return t.removeSelectionAttribute(jw),!0;return!1}))}_handleArrowKeyPressWhenSelectionNextToAWidget(e){const t=this.editor,i=t.model,n=i.schema,s=t.plugins.get("Widget"),o=s._getObjectElementNextToSelection(e);return!!Uw(t.editing.mapper.toViewElement(o),o,n)&&(i.change((t=>{s._setSelectionOverElement(o),t.setSelectionAttribute(jw,e?"before":"after")})),!0)}_handleArrowKeyPressWhenNonCollapsedSelection(e){const t=this.editor,i=t.model,n=i.schema,s=t.editing.mapper,o=i.document.selection,r=e?o.getLastPosition().nodeBefore:o.getFirstPosition().nodeAfter;return!!Uw(s.toViewElement(r),r,n)&&(i.change((t=>{t.setSelection(r,"on"),t.setSelectionAttribute(jw,e?"after":"before")})),!0)}_enableInsertingParagraphsOnButtonClick(){const e=this.editor,t=e.editing.view;this._listenToIfEnabled(t.document,"mousedown",((i,n)=>{const s=n.domTarget.closest(".ck-widget__type-around__button");if(!s)return;const o=function(e){return e.classList.contains("ck-widget__type-around__button_before")?"before":"after"}(s),r=function(e,t){const i=e.closest(".ck-widget");return t.mapDomToView(i)}(s,t.domConverter),a=e.editing.mapper.toModelElement(r);this._insertParagraph(a,o),n.preventDefault(),i.stop()}))}_enableInsertingParagraphsOnEnterKeypress(){const e=this.editor,t=e.model.document.selection,i=e.editing.view;this._listenToIfEnabled(i.document,"enter",((i,n)=>{if("atTarget"!=i.eventPhase)return;const s=t.getSelectedElement(),o=e.editing.mapper.toViewElement(s),r=e.model.schema;let a;this._insertParagraphAccordingToFakeCaretPosition()?a=!0:Uw(o,s,r)&&(this._insertParagraph(s,n.isSoft?"before":"after"),a=!0),a&&(n.preventDefault(),i.stop())}),{context:Nw})}_enableInsertingParagraphsOnTypingKeystroke(){const e=this.editor.editing.view.document;this._listenToIfEnabled(e,"insertText",((t,i)=>{this._insertParagraphAccordingToFakeCaretPosition()&&(i.selection=e.selection)}),{priority:"high"}),l.isAndroid?this._listenToIfEnabled(e,"keydown",((e,t)=>{229==t.keyCode&&this._insertParagraphAccordingToFakeCaretPosition()})):this._listenToIfEnabled(e,"compositionstart",(()=>{this._insertParagraphAccordingToFakeCaretPosition()}),{priority:"high"})}_enableDeleteIntegration(){const e=this.editor,t=e.editing.view,i=e.model,n=i.schema;this._listenToIfEnabled(t.document,"delete",((t,s)=>{if("atTarget"!=t.eventPhase)return;const o=qw(i.document.selection);if(!o)return;const r=s.direction,a=i.document.selection.getSelectedElement(),l="forward"==r;if("before"===o===l)e.execute("delete",{selection:i.createSelection(a,"on")});else{const t=n.getNearestSelectionRange(i.createPositionAt(a,o),r);if(t)if(t.isCollapsed){const s=i.createSelection(t.start);if(i.modifySelection(s,{direction:r}),s.focus.isEqual(t.start)){const e=function(e,t){let i=t;for(const n of t.getAncestors({parentFirst:!0})){if(n.childCount>1||e.isLimit(n))break;i=n}return i}(n,t.start.parent);i.deleteContent(i.createSelection(e,"on"),{doNotAutoparagraph:!0})}else i.change((i=>{i.setSelection(t),e.execute(l?"deleteForward":"delete")}))}else i.change((i=>{i.setSelection(t),e.execute(l?"deleteForward":"delete")}))}s.preventDefault(),t.stop()}),{context:Nw})}_enableInsertContentIntegration(){const e=this.editor,t=this.editor.model,i=t.document.selection;this._listenToIfEnabled(e.model,"insertContent",((e,[n,s])=>{if(s&&!s.is("documentSelection"))return;const o=qw(i);return o?(e.stop(),t.change((e=>{const s=i.getSelectedElement(),r=t.createPositionAt(s,o),a=e.createSelection(r),l=t.insertContent(n,a);return e.setSelection(a),l}))):void 0}),{priority:"high"})}_enableInsertObjectIntegration(){const e=this.editor,t=this.editor.model.document.selection;this._listenToIfEnabled(e.model,"insertObject",((e,i)=>{const[,n,s={}]=i;if(n&&!n.is("documentSelection"))return;const o=qw(t);o&&(s.findOptimalPosition=o,i[3]=s)}),{priority:"high"})}_enableDeleteContentIntegration(){const e=this.editor,t=this.editor.model.document.selection;this._listenToIfEnabled(e.model,"deleteContent",((e,[i])=>{if(i&&!i.is("documentSelection"))return;qw(t)&&e.stop()}),{priority:"high"})}}function Qw(e){const t=e.model;return(i,n)=>{const s=n.keyCode==bs.arrowup,o=n.keyCode==bs.arrowdown,r=n.shiftKey,a=t.document.selection;if(!s&&!o)return;const l=o;if(r&&function(e,t){return!e.isCollapsed&&e.isBackward==t}(a,l))return;const c=function(e,t,i){const n=e.model;if(i){const e=t.isCollapsed?t.focus:t.getLastPosition(),i=Yw(n,e,"forward");if(!i)return null;const s=n.createRange(e,i),o=Xw(n.schema,s,"backward");return o?n.createRange(e,o):null}{const e=t.isCollapsed?t.focus:t.getFirstPosition(),i=Yw(n,e,"backward");if(!i)return null;const s=n.createRange(i,e),o=Xw(n.schema,s,"forward");return o?n.createRange(o,e):null}}(e,a,l);if(c){if(c.isCollapsed){if(a.isCollapsed)return;if(r)return}(c.isCollapsed||function(e,t,i){const n=e.model,s=e.view.domConverter;if(i){const e=n.createSelection(t.start);n.modifySelection(e),e.focus.isAtEnd||t.start.isEqual(e.focus)||(t=n.createRange(e.focus,t.end))}const o=e.mapper.toViewRange(t),r=s.viewRangeToDom(o),a=Hn.getDomRangeRects(r);let l;for(const e of a)if(void 0!==l){if(Math.round(e.top)>=l)return!1;l=Math.max(l,Math.round(e.bottom))}else l=Math.round(e.bottom);return!0}(e,c,l))&&(t.change((e=>{const i=l?c.end:c.start;if(r){const n=t.createSelection(a.anchor);n.setFocus(i),e.setSelection(n)}else e.setSelection(i)})),i.stop(),n.preventDefault(),n.stopPropagation())}}}function Yw(e,t,i){const n=e.schema,s=e.createRangeIn(t.root),o="forward"==i?"elementStart":"elementEnd";for(const{previousPosition:e,item:r,type:a}of s.getWalker({startPosition:t,direction:i})){if(n.isLimit(r)&&!n.isInline(r))return e;if(a==o&&n.isBlock(r))return null}return null}function Xw(e,t,i){const n="backward"==i?t.end:t.start;if(e.checkChild(n,"$text"))return n;for(const{nextPosition:n}of t.getWalker({direction:i}))if(e.checkChild(n,"$text"))return n;return null}class ev extends so{constructor(){super(...arguments),this._previouslySelected=new Set}static get pluginName(){return"Widget"}static get requires(){return[Jw,Yp]}init(){const e=this.editor,t=e.editing.view,i=t.document,n=e.t;this.editor.editing.downcastDispatcher.on("selection",((t,i,n)=>{const s=n.writer,o=i.selection;if(o.isCollapsed)return;const r=o.getSelectedElement();if(!r)return;const a=e.editing.mapper.toViewElement(r);var l;Nw(a)&&(n.consumable.consume(o,"selection")&&s.setSelection(s.createRangeOn(a),{fake:!0,label:(l=a,l.getCustomProperty("widgetLabel").reduce(((e,t)=>"function"==typeof t?e?e+". "+t():t():e?e+". "+t:t),""))}))})),this.editor.editing.downcastDispatcher.on("selection",((e,t,i)=>{this._clearPreviouslySelectedWidgets(i.writer);const n=i.writer,s=n.document.selection;let o=null;for(const e of s.getRanges())for(const t of e){const e=t.item;Nw(e)&&!tv(e,o)&&(n.addClass(Mw,e),this._previouslySelected.add(e),o=e)}}),{priority:"low"}),t.addObserver(vh),this.listenTo(i,"mousedown",((...e)=>this._onMousedown(...e))),this.listenTo(i,"arrowKey",((...e)=>{this._handleSelectionChangeOnArrowKeyPress(...e)}),{context:[Nw,"$text"]}),this.listenTo(i,"arrowKey",((...e)=>{this._preventDefaultOnArrowKeyPress(...e)}),{context:"$root"}),this.listenTo(i,"arrowKey",Qw(this.editor.editing),{context:"$text"}),this.listenTo(i,"delete",((e,t)=>{this._handleDelete("forward"==t.direction)&&(t.preventDefault(),e.stop())}),{context:"$root"}),this.listenTo(i,"tab",((e,t)=>{"atTarget"==e.eventPhase&&(t.shiftKey||this._selectFirstNestedEditable()&&(t.preventDefault(),e.stop()))}),{context:Nw,priority:"low"}),this.listenTo(i,"tab",((e,t)=>{t.shiftKey&&this._selectAncestorWidget()&&(t.preventDefault(),e.stop())}),{priority:"low"}),this.listenTo(i,"keydown",((e,t)=>{t.keystroke==bs.esc&&this._selectAncestorWidget()&&(t.preventDefault(),e.stop())}),{priority:"low"}),e.accessibility.addKeystrokeInfoGroup({id:"widget",label:n("Keystrokes that can be used when a widget is selected (for example: image, table, etc.)"),keystrokes:[{label:n("Insert a new paragraph directly after a widget"),keystroke:"Enter"},{label:n("Insert a new paragraph directly before a widget"),keystroke:"Shift+Enter"},{label:n("Move the caret to allow typing directly before a widget"),keystroke:[["arrowup"],["arrowleft"]]},{label:n("Move the caret to allow typing directly after a widget"),keystroke:[["arrowdown"],["arrowright"]]}]})}_onMousedown(e,t){const i=this.editor,n=i.editing.view,s=n.document;let o=t.target;if(t.domEvent.detail>=3)return void(this._selectBlockContent(o)&&t.preventDefault());if(function(e){let t=e;for(;t;){if(t.is("editableElement")&&!t.is("rootElement"))return!0;if(Nw(t))return!1;t=t.parent}return!1}(o))return;if(!Nw(o)&&(o=o.findAncestor(Nw),!o))return;l.isAndroid&&t.preventDefault(),s.isFocused||n.focus();const r=i.editing.mapper.toModelElement(o);this._setSelectionOverElement(r)}_selectBlockContent(e){const t=this.editor,i=t.model,n=t.editing.mapper,s=i.schema,o=n.findMappedViewAncestor(this.editor.editing.view.createPositionAt(e,0)),r=function(e,t){for(const i of e.getAncestors({includeSelf:!0,parentFirst:!0})){if(t.checkChild(i,"$text"))return i;if(t.isLimit(i)&&!t.isObject(i))break}return null}(n.toModelElement(o),i.schema);return!!r&&(i.change((e=>{const t=s.isLimit(r)?null:function(e,t){const i=new El({startPosition:e});for(const{item:e}of i){if(t.isLimit(e)||!e.is("element"))return null;if(t.checkChild(e,"$text"))return e}return null}(e.createPositionAfter(r),s),i=e.createPositionAt(r,0),n=t?e.createPositionAt(t,0):e.createPositionAt(r,"end");e.setSelection(e.createRange(i,n))})),!0)}_handleSelectionChangeOnArrowKeyPress(e,t){const i=t.keyCode,n=this.editor.model,s=n.schema,o=n.document.selection,r=o.getSelectedElement(),a=ks(i,this.editor.locale.contentLanguageDirection),l="down"==a||"right"==a,c="up"==a||"down"==a;if(r&&s.isObject(r)){const i=l?o.getLastPosition():o.getFirstPosition(),r=s.getNearestSelectionRange(i,l?"forward":"backward");return void(r&&(n.change((e=>{e.setSelection(r)})),t.preventDefault(),e.stop()))}if(!o.isCollapsed&&!t.shiftKey){const i=o.getFirstPosition(),r=o.getLastPosition(),a=i.nodeAfter,c=r.nodeBefore;return void((a&&s.isObject(a)||c&&s.isObject(c))&&(n.change((e=>{e.setSelection(l?r:i)})),t.preventDefault(),e.stop()))}if(!o.isCollapsed)return;const d=this._getObjectElementNextToSelection(l);if(d&&s.isObject(d)){if(s.isInline(d)&&c)return;this._setSelectionOverElement(d),t.preventDefault(),e.stop()}}_preventDefaultOnArrowKeyPress(e,t){const i=this.editor.model,n=i.schema,s=i.document.selection.getSelectedElement();s&&n.isObject(s)&&(t.preventDefault(),e.stop())}_handleDelete(e){const t=this.editor.model.document.selection;if(!this.editor.model.canEditAt(t))return;if(!t.isCollapsed)return;const i=this._getObjectElementNextToSelection(e);return i?(this.editor.model.change((e=>{let n=t.anchor.parent;for(;n.isEmpty;){const t=n;n=t.parent,e.remove(t)}this._setSelectionOverElement(i)})),!0):void 0}_setSelectionOverElement(e){this.editor.model.change((t=>{t.setSelection(t.createRangeOn(e))}))}_getObjectElementNextToSelection(e){const t=this.editor.model,i=t.schema,n=t.document.selection,s=t.createSelection(n);if(t.modifySelection(s,{direction:e?"forward":"backward"}),s.isEqual(n))return null;const o=e?s.focus.nodeBefore:s.focus.nodeAfter;return o&&i.isObject(o)?o:null}_clearPreviouslySelectedWidgets(e){for(const t of this._previouslySelected)e.removeClass(Mw,t);this._previouslySelected.clear()}_selectFirstNestedEditable(){const e=this.editor,t=this.editor.editing.view.document;for(const i of t.selection.getFirstRange().getItems())if(i.is("editableElement")){const t=e.editing.mapper.toModelElement(i);if(!t)continue;const n=e.model.createPositionAt(t,0),s=e.model.schema.getNearestSelectionRange(n,"forward");return e.model.change((e=>{e.setSelection(s)})),!0}return!1}_selectAncestorWidget(){const e=this.editor,t=e.editing.mapper,i=e.editing.view.document.selection.getFirstPosition().parent,n=(i.is("$text")?i.parent:i).findAncestor(Nw);if(!n)return!1;const s=t.toModelElement(n);return!!s&&(e.model.change((e=>{e.setSelection(s,"on")})),!0)}}function tv(e,t){return!!t&&Array.from(e.getAncestors()).includes(t)}class iv extends so{constructor(){super(...arguments),this._toolbarDefinitions=new Map}static get requires(){return[If]}static get pluginName(){return"WidgetToolbarRepository"}init(){const e=this.editor;if(e.plugins.has("BalloonToolbar")){const t=e.plugins.get("BalloonToolbar");this.listenTo(t,"show",(t=>{(function(e){const t=e.getSelectedElement();return!(!t||!Nw(t))})(e.editing.view.document.selection)&&t.stop()}),{priority:"high"})}this._balloon=this.editor.plugins.get("ContextualBalloon"),this.on("change:isEnabled",(()=>{this._updateToolbarsVisibility()})),this.listenTo(e.ui,"update",(()=>{this._updateToolbarsVisibility()})),this.listenTo(e.ui.focusTracker,"change:isFocused",(()=>{this._updateToolbarsVisibility()}),{priority:"low"})}destroy(){super.destroy();for(const e of this._toolbarDefinitions.values())e.view.destroy()}register(e,{ariaLabel:t,items:i,getRelatedElement:n,balloonClassName:s="ck-toolbar-container"}){if(!i.length)return void k("widget-toolbar-no-items",{toolbarId:e});const o=this.editor,r=o.t,a=new Pm(o.locale);if(a.ariaLabel=t||r("Widget toolbar"),this._toolbarDefinitions.has(e))throw new y("widget-toolbar-duplicated",this,{toolbarId:e});const l={view:a,getRelatedElement:n,balloonClassName:s,itemsConfig:i,initialized:!1};o.ui.addToolbar(a,{isContextual:!0,beforeFocus:()=>{const e=n(o.editing.view.document.selection);e&&this._showToolbar(l,e)},afterBlur:()=>{this._hideToolbar(l)}}),this._toolbarDefinitions.set(e,l)}_updateToolbarsVisibility(){let e=0,t=null,i=null;for(const n of this._toolbarDefinitions.values()){const s=n.getRelatedElement(this.editor.editing.view.document.selection);if(this.isEnabled&&s)if(this.editor.ui.focusTracker.isFocused){const o=s.getAncestors().length;o>e&&(e=o,t=s,i=n)}else this._isToolbarVisible(n)&&this._hideToolbar(n);else this._isToolbarInBalloon(n)&&this._hideToolbar(n)}i&&this._showToolbar(i,t)}_hideToolbar(e){this._balloon.remove(e.view),this.stopListening(this._balloon,"change:visibleView")}_showToolbar(e,t){this._isToolbarVisible(e)?nv(this.editor,t):this._isToolbarInBalloon(e)||(e.initialized||(e.initialized=!0,e.view.fillFromConfig(e.itemsConfig,this.editor.ui.componentFactory)),this._balloon.add({view:e.view,position:sv(this.editor,t),balloonClassName:e.balloonClassName}),this.listenTo(this._balloon,"change:visibleView",(()=>{for(const e of this._toolbarDefinitions.values())if(this._isToolbarVisible(e)){const t=e.getRelatedElement(this.editor.editing.view.document.selection);nv(this.editor,t)}})))}_isToolbarVisible(e){return this._balloon.visibleView===e.view}_isToolbarInBalloon(e){return this._balloon.hasView(e.view)}}function nv(e,t){const i=e.plugins.get("ContextualBalloon"),n=sv(e,t);i.updatePosition(n)}function sv(e,t){const i=e.editing.view,n=ef.defaultPositions;return{target:i.domConverter.mapViewToDom(t),positions:[n.northArrowSouth,n.northArrowSouthWest,n.northArrowSouthEast,n.southArrowNorth,n.southArrowNorthWest,n.southArrowNorthEast,n.viewportStickyNorth]}}class ov extends(G()){constructor(e){super(),this.set("activeHandlePosition",null),this.set("proposedWidthPercents",null),this.set("proposedWidth",null),this.set("proposedHeight",null),this.set("proposedHandleHostWidth",null),this.set("proposedHandleHostHeight",null),this._options=e,this._referenceCoordinates=null}get originalWidth(){return this._originalWidth}get originalHeight(){return this._originalHeight}get originalWidthPercents(){return this._originalWidthPercents}get aspectRatio(){return this._aspectRatio}begin(e,t,i){const n=new Hn(t);this.activeHandlePosition=function(e){const t=["top-left","top-right","bottom-right","bottom-left"];for(const i of t)if(e.classList.contains(rv(i)))return i}(e),this._referenceCoordinates=function(e,t){const i=new Hn(e),n=t.split("-"),s={x:"right"==n[1]?i.right:i.left,y:"bottom"==n[0]?i.bottom:i.top};return s.x+=e.ownerDocument.defaultView.scrollX,s.y+=e.ownerDocument.defaultView.scrollY,s}(t,function(e){const t=e.split("-"),i={top:"bottom",bottom:"top",left:"right",right:"left"};return`${i[t[0]]}-${i[t[1]]}`}(this.activeHandlePosition)),this._originalWidth=n.width,this._originalHeight=n.height,this._aspectRatio=n.width/n.height;const s=i.style.width;s&&s.match(/^\d+(\.\d*)?%$/)?this._originalWidthPercents=parseFloat(s):this._originalWidthPercents=function(e,t){const i=e.parentElement;let n=parseFloat(i.ownerDocument.defaultView.getComputedStyle(i).width);const s=5;let o=0,r=i;for(;isNaN(n);){if(r=r.parentElement,++o>s)return 0;n=parseFloat(i.ownerDocument.defaultView.getComputedStyle(r).width)}return t.width/n*100}(i,n)}update(e){this.proposedWidth=e.width,this.proposedHeight=e.height,this.proposedWidthPercents=e.widthPercents,this.proposedHandleHostWidth=e.handleHostWidth,this.proposedHandleHostHeight=e.handleHostHeight}}function rv(e){return`ck-widget__resizer__handle-${e}`}class av extends Du{constructor(){super();const e=this.bindTemplate;this.setTemplate({tag:"div",attributes:{class:["ck","ck-size-view",e.to("_viewPosition",(e=>e?`ck-orientation-${e}`:""))],style:{display:e.if("_isVisible","none",(e=>!e))}},children:[{text:e.to("_label")}]})}_bindToState(e,t){this.bind("_isVisible").to(t,"proposedWidth",t,"proposedHeight",((e,t)=>null!==e&&null!==t)),this.bind("_label").to(t,"proposedHandleHostWidth",t,"proposedHandleHostHeight",t,"proposedWidthPercents",((t,i,n)=>"px"===e.unit?`${t}×${i}`:`${n}%`)),this.bind("_viewPosition").to(t,"activeHandlePosition",t,"proposedHandleHostWidth",t,"proposedHandleHostHeight",((e,t,i)=>t<50||i<50?"above-center":e))}_dismiss(){this.unbind(),this._isVisible=!1}}class lv extends(G()){constructor(e){super(),this._viewResizerWrapper=null,this._options=e,this.set("isEnabled",!0),this.set("isSelected",!1),this.bind("isVisible").to(this,"isEnabled",this,"isSelected",((e,t)=>e&&t)),this.decorate("begin"),this.decorate("cancel"),this.decorate("commit"),this.decorate("updateSize"),this.on("commit",(e=>{this.state.proposedWidth||this.state.proposedWidthPercents||(this._cleanup(),e.stop())}),{priority:"high"})}get state(){return this._state}show(){this._options.editor.editing.view.change((e=>{e.removeClass("ck-hidden",this._viewResizerWrapper)}))}hide(){this._options.editor.editing.view.change((e=>{e.addClass("ck-hidden",this._viewResizerWrapper)}))}attach(){const e=this,t=this._options.viewElement;this._options.editor.editing.view.change((i=>{const n=i.createUIElement("div",{class:"ck ck-reset_all ck-widget__resizer"},(function(t){const i=this.toDomElement(t);return e._appendHandles(i),e._appendSizeUI(i),i}));i.insert(i.createPositionAt(t,"end"),n),i.addClass("ck-widget_with-resizer",t),this._viewResizerWrapper=n,this.isVisible||this.hide()})),this.on("change:isVisible",(()=>{this.isVisible?(this.show(),this.redraw()):this.hide()}))}begin(e){this._state=new ov(this._options),this._sizeView._bindToState(this._options,this.state),this._initialViewWidth=this._options.viewElement.getStyle("width"),this.state.begin(e,this._getHandleHost(),this._getResizeHost())}updateSize(e){const t=this._proposeNewSize(e);this._options.editor.editing.view.change((e=>{const i=this._options.unit||"%",n=("%"===i?t.widthPercents:t.width)+i;e.setStyle("width",n,this._options.viewElement)}));const i=this._getHandleHost(),n=new Hn(i),s=Math.round(n.width),o=Math.round(n.height),r=new Hn(i);t.width=Math.round(r.width),t.height=Math.round(r.height),this.redraw(n),this.state.update({...t,handleHostWidth:s,handleHostHeight:o})}commit(){const e=this._options.unit||"%",t=("%"===e?this.state.proposedWidthPercents:this.state.proposedWidth)+e;this._options.editor.editing.view.change((()=>{this._cleanup(),this._options.onCommit(t)}))}cancel(){this._cleanup()}destroy(){this.cancel()}redraw(e){const t=this._domResizerWrapper;if(!((i=t)&&i.ownerDocument&&i.ownerDocument.contains(i)))return;var i;const n=t.parentElement,s=this._getHandleHost(),o=this._viewResizerWrapper,r=[o.getStyle("width"),o.getStyle("height"),o.getStyle("left"),o.getStyle("top")];let a;if(n.isSameNode(s)){const t=e||new Hn(s);a=[t.width+"px",t.height+"px",void 0,void 0]}else a=[s.offsetWidth+"px",s.offsetHeight+"px",s.offsetLeft+"px",s.offsetTop+"px"];"same"!==te(r,a)&&this._options.editor.editing.view.change((e=>{e.setStyle({width:a[0],height:a[1],left:a[2],top:a[3]},o)}))}containsHandle(e){return this._domResizerWrapper.contains(e)}static isResizeHandle(e){return e.classList.contains("ck-widget__resizer__handle")}_cleanup(){this._sizeView._dismiss();this._options.editor.editing.view.change((e=>{e.setStyle("width",this._initialViewWidth,this._options.viewElement)}))}_proposeNewSize(e){const t=this.state,i={x:(n=e).pageX,y:n.pageY};var n;const s=!this._options.isCentered||this._options.isCentered(this),o={x:t._referenceCoordinates.x-(i.x+t.originalWidth),y:i.y-t.originalHeight-t._referenceCoordinates.y};s&&t.activeHandlePosition.endsWith("-right")&&(o.x=i.x-(t._referenceCoordinates.x+t.originalWidth)),s&&(o.x*=2);let r=Math.abs(t.originalWidth+o.x),a=Math.abs(t.originalHeight+o.y);return"width"==(r/t.aspectRatio>a?"width":"height")?a=r/t.aspectRatio:r=a*t.aspectRatio,{width:Math.round(r),height:Math.round(a),widthPercents:Math.min(Math.round(t.originalWidthPercents/t.originalWidth*r*100)/100,100)}}_getResizeHost(){const e=this._domResizerWrapper.parentElement;return this._options.getResizeHost(e)}_getHandleHost(){const e=this._domResizerWrapper.parentElement;return this._options.getHandleHost(e)}get _domResizerWrapper(){return this._options.editor.editing.view.domConverter.mapViewToDom(this._viewResizerWrapper)}_appendHandles(e){const t=["top-left","top-right","bottom-right","bottom-left"];for(const n of t)e.appendChild(new bu({tag:"div",attributes:{class:"ck-widget__resizer__handle "+(i=n,`ck-widget__resizer__handle-${i}`)}}).render());var i}_appendSizeUI(e){this._sizeView=new av,this._sizeView.render(),e.appendChild(this._sizeView.element)}}class cv extends so{constructor(){super(...arguments),this._resizers=new Map}static get pluginName(){return"WidgetResize"}init(){const e=this.editor.editing,t=Mn.window.document;this.set("selectedResizer",null),this.set("_activeResizer",null),e.view.addObserver(vh),this._observer=new(Vn()),this.listenTo(e.view.document,"mousedown",this._mouseDownListener.bind(this),{priority:"high"}),this._observer.listenTo(t,"mousemove",this._mouseMoveListener.bind(this)),this._observer.listenTo(t,"mouseup",this._mouseUpListener.bind(this)),this._redrawSelectedResizerThrottled=cf((()=>this.redrawSelectedResizer()),200),this.editor.ui.on("update",this._redrawSelectedResizerThrottled),this.editor.model.document.on("change",(()=>{for(const[e,t]of this._resizers)e.isAttached()||(this._resizers.delete(e),t.destroy())}),{priority:"lowest"}),this._observer.listenTo(Mn.window,"resize",this._redrawSelectedResizerThrottled);const i=this.editor.editing.view.document.selection;i.on("change",(()=>{const e=i.getSelectedElement(),t=this.getResizerByViewElement(e)||null;t?this.select(t):this.deselect()}))}redrawSelectedResizer(){this.selectedResizer&&this.selectedResizer.isVisible&&this.selectedResizer.redraw()}destroy(){super.destroy(),this._observer.stopListening();for(const e of this._resizers.values())e.destroy();this._redrawSelectedResizerThrottled.cancel()}select(e){this.deselect(),this.selectedResizer=e,this.selectedResizer.isSelected=!0}deselect(){this.selectedResizer&&(this.selectedResizer.isSelected=!1),this.selectedResizer=null}attachTo(e){const t=new lv(e),i=this.editor.plugins;if(t.attach(),i.has("WidgetToolbarRepository")){const e=i.get("WidgetToolbarRepository");t.on("begin",(()=>{e.forceDisabled("resize")}),{priority:"lowest"}),t.on("cancel",(()=>{e.clearForceDisabled("resize")}),{priority:"highest"}),t.on("commit",(()=>{e.clearForceDisabled("resize")}),{priority:"highest"})}this._resizers.set(e.viewElement,t);const n=this.editor.editing.view.document.selection.getSelectedElement();return this.getResizerByViewElement(n)==t&&this.select(t),t}getResizerByViewElement(e){return this._resizers.get(e)}_getResizerByHandle(e){for(const t of this._resizers.values())if(t.containsHandle(e))return t}_mouseDownListener(e,t){const i=t.domTarget;lv.isResizeHandle(i)&&(this._activeResizer=this._getResizerByHandle(i)||null,this._activeResizer&&(this._activeResizer.begin(i),e.stop(),t.preventDefault()))}_mouseMoveListener(e,t){this._activeResizer&&this._activeResizer.updateSize(t)}_mouseUpListener(){this._activeResizer&&(this._activeResizer.commit(),this._activeResizer=null)}}const dv=Zn("px");class hv extends Du{constructor(){super();const e=this.bindTemplate;this.set({isVisible:!1,left:null,top:null,width:null}),this.setTemplate({tag:"div",attributes:{class:["ck","ck-clipboard-drop-target-line",e.if("isVisible","ck-hidden",(e=>!e))],style:{left:e.to("left",(e=>dv(e))),top:e.to("top",(e=>dv(e))),width:e.to("width",(e=>dv(e)))}}})}}class uv extends so{constructor(){super(...arguments),this.removeDropMarkerDelayed=Xs((()=>this.removeDropMarker()),40),this._updateDropMarkerThrottled=cf((e=>this._updateDropMarker(e)),40),this._reconvertMarkerThrottled=cf((()=>{this.editor.model.markers.has("drop-target")&&this.editor.editing.reconvertMarker("drop-target")}),0),this._dropTargetLineView=new hv,this._domEmitter=new(Vn()),this._scrollables=new Map}static get pluginName(){return"DragDropTarget"}init(){this._setupDropMarker()}destroy(){this._domEmitter.stopListening();for(const{resizeObserver:e}of this._scrollables.values())e.destroy();return this._updateDropMarkerThrottled.cancel(),this.removeDropMarkerDelayed.cancel(),this._reconvertMarkerThrottled.cancel(),super.destroy()}updateDropMarker(e,t,i,n,s,o){this.removeDropMarkerDelayed.cancel();const r=mv(this.editor,e,t,i,n,s,o);if(r)return o&&o.containsRange(r)?this.removeDropMarker():void this._updateDropMarkerThrottled(r)}getFinalDropRange(e,t,i,n,s,o){const r=mv(this.editor,e,t,i,n,s,o);return this.removeDropMarker(),r}removeDropMarker(){const e=this.editor.model;this.removeDropMarkerDelayed.cancel(),this._updateDropMarkerThrottled.cancel(),this._dropTargetLineView.isVisible=!1,e.markers.has("drop-target")&&e.change((e=>{e.removeMarker("drop-target")}))}_setupDropMarker(){const e=this.editor;e.ui.view.body.add(this._dropTargetLineView),e.conversion.for("editingDowncast").markerToHighlight({model:"drop-target",view:{classes:["ck-clipboard-drop-target-range"]}}),e.conversion.for("editingDowncast").markerToElement({model:"drop-target",view:(t,{writer:i})=>{if(e.model.schema.checkChild(t.markerRange.start,"$text"))return this._dropTargetLineView.isVisible=!1,this._createDropTargetPosition(i);t.markerRange.isCollapsed?this._updateDropTargetLine(t.markerRange):this._dropTargetLineView.isVisible=!1}})}_updateDropMarker(e){const t=this.editor,i=t.model.markers;t.model.change((t=>{i.has("drop-target")?i.get("drop-target").getRange().isEqual(e)||t.updateMarker("drop-target",{range:e}):t.addMarker("drop-target",{range:e,usingOperation:!1,affectsData:!1})}))}_createDropTargetPosition(e){return e.createUIElement("span",{class:"ck ck-clipboard-drop-target-position"},(function(e){const t=this.toDomElement(e);return t.append("⁠",e.createElement("span"),"⁠"),t}))}_updateDropTargetLine(e){const t=this.editor.editing,i=e.start.nodeBefore,n=e.start.nodeAfter,s=e.start.parent,o=i?t.mapper.toViewElement(i):null,r=o?t.view.domConverter.mapViewToDom(o):null,a=n?t.mapper.toViewElement(n):null,l=a?t.view.domConverter.mapViewToDom(a):null,c=t.mapper.toViewElement(s);if(!c)return;const d=t.view.domConverter.mapViewToDom(c),h=this._getScrollableRect(c),{scrollX:u,scrollY:m}=Mn.window,g=r?new Hn(r):null,f=l?new Hn(l):null,p=new Hn(d).excludeScrollbarsAndBorders(),b=g?g.bottom:p.top,w=f?f.top:p.bottom,v=Mn.window.getComputedStyle(d),_=b<=w?(b+w)/2:w;if(h.top<_&&_a.schema.checkChild(o,e)))){if(a.schema.checkChild(o,"$text"))return a.createRange(o);if(t)return fv(e,bv(e,t.parent),n,s)}}}else if(a.schema.isInline(c))return fv(e,c,n,s);if(a.schema.isBlock(c))return fv(e,c,n,s);if(a.schema.checkChild(c,"$block")){const t=Array.from(c.getChildren()).filter((t=>t.is("element")&&!gv(e,t)));let i=0,o=t.length;if(0==o)return a.createRange(a.createPositionAt(c,"end"));for(;i{i?(this.forceDisabled("readOnlyMode"),this._isBlockDragging=!1):this.clearForceDisabled("readOnlyMode")})),l.isAndroid&&this.forceDisabled("noAndroidSupport"),e.plugins.has("BlockToolbar")){const t=e.plugins.get("BlockToolbar").buttonView.element;this._domEmitter.listenTo(t,"dragstart",((e,t)=>this._handleBlockDragStart(t))),this._domEmitter.listenTo(Mn.document,"dragover",((e,t)=>this._handleBlockDragging(t))),this._domEmitter.listenTo(Mn.document,"drop",((e,t)=>this._handleBlockDragging(t))),this._domEmitter.listenTo(Mn.document,"dragend",(()=>this._handleBlockDragEnd()),{useCapture:!0}),this.isEnabled&&t.setAttribute("draggable","true"),this.on("change:isEnabled",((e,i,n)=>{t.setAttribute("draggable",n?"true":"false")}))}}destroy(){return this._domEmitter.stopListening(),super.destroy()}_handleBlockDragStart(e){if(!this.isEnabled)return;const t=this.editor.model,i=t.document.selection,n=this.editor.editing.view,s=Array.from(i.getSelectedBlocks()),o=t.createRange(t.createPositionBefore(s[0]),t.createPositionAfter(s[s.length-1]));t.change((e=>e.setSelection(o))),this._isBlockDragging=!0,n.focus(),n.getObserver(aw).onDomEvent(e)}_handleBlockDragging(e){if(!this.isEnabled||!this._isBlockDragging)return;const t=e.clientX+("ltr"==this.editor.locale.contentLanguageDirection?100:-100),i=e.clientY,n=document.elementFromPoint(t,i),s=this.editor.editing.view;n&&n.closest(".ck-editor__editable")&&s.getObserver(aw).onDomEvent({...e,type:e.type,dataTransfer:e.dataTransfer,target:n,clientX:t,clientY:i,preventDefault:()=>e.preventDefault(),stopPropagation:()=>e.stopPropagation()})}_handleBlockDragEnd(){this._isBlockDragging=!1}}class vv extends so{constructor(){super(...arguments),this._clearDraggableAttributesDelayed=Xs((()=>this._clearDraggableAttributes()),40),this._blockMode=!1,this._domEmitter=new(Vn())}static get pluginName(){return"DragDrop"}static get requires(){return[Sw,ev,uv,wv]}init(){const e=this.editor,t=e.editing.view;this._draggedRange=null,this._draggingUid="",this._draggableElement=null,t.addObserver(aw),t.addObserver(vh),this._setupDragging(),this._setupContentInsertionIntegration(),this._setupClipboardInputIntegration(),this._setupDraggableAttributeHandling(),this.listenTo(e,"change:isReadOnly",((e,t,i)=>{i?this.forceDisabled("readOnlyMode"):this.clearForceDisabled("readOnlyMode")})),this.on("change:isEnabled",((e,t,i)=>{i||this._finalizeDragging(!1)})),l.isAndroid&&this.forceDisabled("noAndroidSupport")}destroy(){return this._draggedRange&&(this._draggedRange.detach(),this._draggedRange=null),this._previewContainer&&this._previewContainer.remove(),this._domEmitter.stopListening(),this._clearDraggableAttributesDelayed.cancel(),super.destroy()}_setupDragging(){const e=this.editor,t=e.model,i=e.editing.view,n=i.document,s=e.plugins.get(uv);this.listenTo(n,"dragstart",((e,i)=>{if(i.target&&i.target.is("editableElement"))return void i.preventDefault();if(this._prepareDraggedRange(i.target),!this._draggedRange)return void i.preventDefault();this._draggingUid=b(),i.dataTransfer.effectAllowed=this.isEnabled?"copyMove":"copy",i.dataTransfer.setData("application/ckeditor5-dragging-uid",this._draggingUid);const n=t.createSelection(this._draggedRange.toRange());this.editor.plugins.get("ClipboardPipeline")._fireOutputTransformationEvent(i.dataTransfer,n,"dragstart");const{dataTransfer:s,domTarget:o,domEvent:r}=i,{clientX:a}=r;this._updatePreview({dataTransfer:s,domTarget:o,clientX:a}),i.stopPropagation(),this.isEnabled||(this._draggedRange.detach(),this._draggedRange=null,this._draggingUid="")}),{priority:"low"}),this.listenTo(n,"dragend",((e,t)=>{this._finalizeDragging(!t.dataTransfer.isCanceled&&"move"==t.dataTransfer.dropEffect)}),{priority:"low"}),this._domEmitter.listenTo(Mn.document,"dragend",(()=>{this._blockMode=!1}),{useCapture:!0}),this.listenTo(n,"dragenter",(()=>{this.isEnabled&&i.focus()})),this.listenTo(n,"dragleave",(()=>{s.removeDropMarkerDelayed()})),this.listenTo(n,"dragging",((e,t)=>{if(!this.isEnabled)return void(t.dataTransfer.dropEffect="none");const{clientX:i,clientY:n}=t.domEvent;s.updateDropMarker(t.target,t.targetRanges,i,n,this._blockMode,this._draggedRange),this._draggedRange||(t.dataTransfer.dropEffect="copy"),l.isGecko||("copy"==t.dataTransfer.effectAllowed?t.dataTransfer.dropEffect="copy":["all","copyMove"].includes(t.dataTransfer.effectAllowed)&&(t.dataTransfer.dropEffect="move")),e.stop()}),{priority:"low"})}_setupClipboardInputIntegration(){const e=this.editor,t=e.editing.view.document,i=e.plugins.get(uv);this.listenTo(t,"clipboardInput",((t,n)=>{if("drop"!=n.method)return;const{clientX:s,clientY:o}=n.domEvent,r=i.getFinalDropRange(n.target,n.targetRanges,s,o,this._blockMode,this._draggedRange);if(!r)return this._finalizeDragging(!1),void t.stop();this._draggedRange&&this._draggingUid!=n.dataTransfer.getData("application/ckeditor5-dragging-uid")&&(this._draggedRange.detach(),this._draggedRange=null,this._draggingUid="");if("move"==_v(n.dataTransfer)&&this._draggedRange&&this._draggedRange.containsRange(r,!0))return this._finalizeDragging(!1),void t.stop();n.targetRanges=[e.editing.mapper.toViewRange(r)]}),{priority:"high"})}_setupContentInsertionIntegration(){const e=this.editor.plugins.get(Sw);e.on("contentInsertion",((e,t)=>{if(!this.isEnabled||"drop"!==t.method)return;const i=t.targetRanges.map((e=>this.editor.editing.mapper.toModelRange(e)));this.editor.model.change((e=>e.setSelection(i)))}),{priority:"high"}),e.on("contentInsertion",((e,t)=>{if(!this.isEnabled||"drop"!==t.method)return;const i="move"==_v(t.dataTransfer),n=!t.resultRange||!t.resultRange.isCollapsed;this._finalizeDragging(n&&i)}),{priority:"lowest"})}_setupDraggableAttributeHandling(){const e=this.editor,t=e.editing.view,i=t.document;this.listenTo(i,"mousedown",((n,s)=>{if(l.isAndroid||!s)return;this._clearDraggableAttributesDelayed.cancel();let o=yv(s.target);if(l.isBlink&&!e.isReadOnly&&!o&&!i.selection.isCollapsed){const e=i.selection.getSelectedElement();e&&Nw(e)||(o=i.selection.editableElement)}o&&(t.change((e=>{e.setAttribute("draggable","true",o)})),this._draggableElement=e.editing.mapper.toModelElement(o))})),this.listenTo(i,"mouseup",(()=>{l.isAndroid||this._clearDraggableAttributesDelayed()}))}_clearDraggableAttributes(){const e=this.editor.editing;e.view.change((t=>{this._draggableElement&&"$graveyard"!=this._draggableElement.root.rootName&&t.removeAttribute("draggable",e.mapper.toViewElement(this._draggableElement)),this._draggableElement=null}))}_finalizeDragging(e){const t=this.editor,i=t.model;if(t.plugins.get(uv).removeDropMarker(),this._clearDraggableAttributes(),t.plugins.has("WidgetToolbarRepository")){t.plugins.get("WidgetToolbarRepository").clearForceDisabled("dragDrop")}this._draggingUid="",this._previewContainer&&(this._previewContainer.remove(),this._previewContainer=void 0),this._draggedRange&&(e&&this.isEnabled&&i.change((e=>{const t=i.createSelection(this._draggedRange);i.deleteContent(t,{doNotAutoparagraph:!0});const n=t.getFirstPosition().parent;n.isEmpty&&!i.schema.checkChild(n,"$text")&&i.schema.checkChild(n,"paragraph")&&e.insertElement("paragraph",n,0)})),this._draggedRange.detach(),this._draggedRange=null)}_prepareDraggedRange(e){const t=this.editor,i=t.model,n=i.document.selection,s=e?yv(e):null;if(s){const e=t.editing.mapper.toModelElement(s);if(this._draggedRange=Kl.fromRange(i.createRangeOn(e)),this._blockMode=i.schema.isBlock(e),t.plugins.has("WidgetToolbarRepository")){t.plugins.get("WidgetToolbarRepository").forceDisabled("dragDrop")}return}if(n.isCollapsed&&!n.getFirstPosition().parent.isEmpty)return;const o=Array.from(n.getSelectedBlocks()),r=n.getFirstRange();if(0==o.length)return void(this._draggedRange=Kl.fromRange(r));const a=kv(i,o);if(o.length>1)this._draggedRange=Kl.fromRange(a),this._blockMode=!0;else if(1==o.length){const e=r.start.isTouching(a.start)&&r.end.isTouching(a.end);this._draggedRange=Kl.fromRange(e?a:r),this._blockMode=e}i.change((e=>e.setSelection(this._draggedRange.toRange())))}_updatePreview({dataTransfer:e,domTarget:t,clientX:i}){const n=this.editor.editing.view,s=n.document.selection.editableElement,o=n.domConverter.mapViewToDom(s),r=Mn.window.getComputedStyle(o);this._previewContainer?this._previewContainer.firstElementChild&&this._previewContainer.removeChild(this._previewContainer.firstElementChild):(this._previewContainer=ve(Mn.document,"div",{style:"position: fixed; left: -999999px;"}),Mn.document.body.appendChild(this._previewContainer));const a=new Hn(o);if(o.contains(t))return;const c=parseFloat(r.paddingLeft),d=ve(Mn.document,"div");d.className="ck ck-content",d.style.width=r.width,d.style.paddingLeft=`${a.left-i+c}px`,l.isiOS&&(d.style.backgroundColor="white"),d.innerHTML=e.getData("text/html"),e.setDragImage(d,0,0),this._previewContainer.appendChild(d)}}function _v(e){return l.isGecko?e.dropEffect:["all","copyMove"].includes(e.effectAllowed)?"move":"copy"}function yv(e){if(e.is("editableElement"))return null;if(e.hasClass("ck-widget__selection-handle"))return e.findAncestor(Nw);if(Nw(e))return e;const t=e.findAncestor((e=>Nw(e)||e.is("editableElement")));return Nw(t)?t:null}function kv(e,t){const i=t[0],n=t[t.length-1],s=i.getCommonAncestor(n),o=e.createPositionBefore(i),r=e.createPositionAfter(n);if(s&&s.is("element")&&!e.schema.isLimit(s)){const t=e.createRangeOn(s),i=o.isTouching(t.start),n=r.isTouching(t.end);if(i&&n)return kv(e,[s])}return e.createRange(o,r)}class Cv extends so{static get pluginName(){return"PastePlainText"}static get requires(){return[Sw]}init(){const e=this.editor,t=e.model,i=e.editing.view,n=i.document,s=t.document.selection;let o=!1;i.addObserver(aw),this.listenTo(n,"keydown",((e,t)=>{o=t.shiftKey})),e.plugins.get(Sw).on("contentInsertion",((e,i)=>{(o||function(e,t){if(e.childCount>1)return!1;const i=e.getChild(0);if(t.isObject(i))return!1;return 0==Array.from(i.getAttributeKeys()).length}(i.content,t.schema))&&t.change((e=>{const n=Array.from(s.getAttributes()).filter((([e])=>t.schema.getAttributeProperties(e).isFormatting));s.isCollapsed||t.deleteContent(s,{doNotAutoparagraph:!0}),n.push(...s.getAttributes());const o=e.createRangeIn(i.content);for(const t of o.getItems())t.is("$textProxy")&&e.setAttributes(n,t)}))}))}}class Av extends so{static get pluginName(){return"Clipboard"}static get requires(){return[Tw,Sw,vv,Cv]}init(){const e=this.editor,t=this.editor.t;e.accessibility.addKeystrokeInfos({keystrokes:[{label:t("Copy selected content"),keystroke:"CTRL+C"},{label:t("Paste content"),keystroke:"CTRL+V"},{label:t("Paste content as plain text"),keystroke:"CTRL+SHIFT+V"}]})}}class xv extends ro{constructor(e){super(e),this.affectsData=!1}execute(){const e=this.editor.model,t=e.document.selection;let i=e.schema.getLimitElement(t);if(t.containsEntireContent(i)||!Ev(e.schema,i))do{if(i=i.parent,!i)return}while(!Ev(e.schema,i));e.change((e=>{e.setSelection(i,"in")}))}}function Ev(e,t){return e.isLimit(t)&&(e.checkChild(t,"$text")||e.checkChild(t,"paragraph"))}const Tv=_s("Ctrl+A");class Sv extends so{static get pluginName(){return"SelectAllEditing"}init(){const e=this.editor,t=e.t,i=e.editing.view.document;e.commands.add("selectAll",new xv(e)),this.listenTo(i,"keydown",((t,i)=>{vs(i)===Tv&&(e.execute("selectAll"),i.preventDefault())})),e.accessibility.addKeystrokeInfos({keystrokes:[{label:t("Select all"),keystroke:"CTRL+A"}]})}}class Pv extends so{static get pluginName(){return"SelectAllUI"}init(){const e=this.editor;e.ui.componentFactory.add("selectAll",(()=>{const e=this._createButton(Ku);return e.set({tooltip:!0}),e})),e.ui.componentFactory.add("menuBar:selectAll",(()=>this._createButton(up)))}_createButton(e){const t=this.editor,i=t.locale,n=t.commands.get("selectAll"),s=new e(t.locale),o=i.t;return s.set({label:o("Select all"),icon:'',keystroke:"Ctrl+A"}),s.bind("isEnabled").to(n,"isEnabled"),this.listenTo(s,"execute",(()=>{t.execute("selectAll"),t.editing.view.focus()})),s}}class Iv extends so{static get requires(){return[Sv,Pv]}static get pluginName(){return"SelectAll"}}class Vv extends ro{constructor(e){super(e),this._stack=[],this._createdBatches=new WeakSet,this.refresh(),this._isEnabledBasedOnSelection=!1,this.listenTo(e.data,"set",((e,t)=>{t[1]={...t[1]};const i=t[1];i.batchType||(i.batchType={isUndoable:!1})}),{priority:"high"}),this.listenTo(e.data,"set",((e,t)=>{t[1].batchType.isUndoable||this.clearStack()}))}refresh(){this.isEnabled=this._stack.length>0}get createdBatches(){return this._createdBatches}addBatch(e){const t=this.editor.model.document.selection,i={ranges:t.hasOwnRange?Array.from(t.getRanges()):[],isBackward:t.isBackward};this._stack.push({batch:e,selection:i}),this.refresh()}clearStack(){this._stack=[],this.refresh()}_restoreSelection(e,t,i){const n=this.editor.model,s=n.document,o=[],r=e.map((e=>e.getTransformedByOperations(i))),a=r.flat();for(const e of r){const t=e.filter((e=>e.root!=s.graveyard)).filter((e=>!Ov(e,a)));t.length&&(Rv(t),o.push(t[0]))}o.length&&n.change((e=>{e.setSelection(o,{backward:t})}))}_undo(e,t){const i=this.editor.model,n=i.document;this._createdBatches.add(t);const s=e.operations.slice().filter((e=>e.isDocumentOperation));s.reverse();for(const e of s){const s=e.baseVersion+1,o=Array.from(n.history.getOperations(s)),r=Ed([e.getReversed()],o,{useRelations:!0,document:this.editor.model.document,padWithNoOps:!1,forceWeakRemove:!0}).operationsA;for(let s of r){const o=s.affectedSelectable;o&&!i.canEditAt(o)&&(s=new pd(s.baseVersion)),t.addOperation(s),i.applyOperation(s),n.history.setOperationAsUndone(e,s)}}}}function Rv(e){e.sort(((e,t)=>e.start.isBefore(t.start)?-1:1));for(let t=1;tt!==e&&t.containsRange(e,!0)))}class Bv extends Vv{execute(e=null){const t=e?this._stack.findIndex((t=>t.batch==e)):this._stack.length-1,i=this._stack.splice(t,1)[0],n=this.editor.model.createBatch({isUndo:!0});this.editor.model.enqueueChange(n,(()=>{this._undo(i.batch,n);const e=this.editor.model.document.history.getOperations(i.batch.baseVersion);this._restoreSelection(i.selection.ranges,i.selection.isBackward,e)})),this.fire("revert",i.batch,n),this.refresh()}}class Mv extends Vv{execute(){const e=this._stack.pop(),t=this.editor.model.createBatch({isUndo:!0});this.editor.model.enqueueChange(t,(()=>{const i=e.batch.operations[e.batch.operations.length-1].baseVersion+1,n=this.editor.model.document.history.getOperations(i);this._restoreSelection(e.selection.ranges,e.selection.isBackward,n),this._undo(e.batch,t)})),this.refresh()}}class Nv extends so{constructor(){super(...arguments),this._batchRegistry=new WeakSet}static get pluginName(){return"UndoEditing"}init(){const e=this.editor,t=e.t;this._undoCommand=new Bv(e),this._redoCommand=new Mv(e),e.commands.add("undo",this._undoCommand),e.commands.add("redo",this._redoCommand),this.listenTo(e.model,"applyOperation",((e,t)=>{const i=t[0];if(!i.isDocumentOperation)return;const n=i.batch,s=this._redoCommand.createdBatches.has(n),o=this._undoCommand.createdBatches.has(n);this._batchRegistry.has(n)||(this._batchRegistry.add(n),n.isUndoable&&(s?this._undoCommand.addBatch(n):o||(this._undoCommand.addBatch(n),this._redoCommand.clearStack())))}),{priority:"highest"}),this.listenTo(this._undoCommand,"revert",((e,t,i)=>{this._redoCommand.addBatch(i)})),e.keystrokes.set("CTRL+Z","undo"),e.keystrokes.set("CTRL+Y","redo"),e.keystrokes.set("CTRL+SHIFT+Z","redo"),e.accessibility.addKeystrokeInfos({keystrokes:[{label:t("Undo"),keystroke:"CTRL+Z"},{label:t("Redo"),keystroke:[["CTRL+Y"],["CTRL+SHIFT+Z"]]}]})}}class Fv extends so{static get pluginName(){return"UndoUI"}init(){const e=this.editor,t=e.locale,i=e.t,n="ltr"==t.uiLanguageDirection?fu.undo:fu.redo,s="ltr"==t.uiLanguageDirection?fu.redo:fu.undo;this._addButtonsToFactory("undo",i("Undo"),"CTRL+Z",n),this._addButtonsToFactory("redo",i("Redo"),"CTRL+Y",s)}_addButtonsToFactory(e,t,i,n){const s=this.editor;s.ui.componentFactory.add(e,(()=>{const s=this._createButton(Ku,e,t,i,n);return s.set({tooltip:!0}),s})),s.ui.componentFactory.add("menuBar:"+e,(()=>this._createButton(up,e,t,i,n)))}_createButton(e,t,i,n,s){const o=this.editor,r=o.locale,a=o.commands.get(t),l=new e(r);return l.set({label:i,icon:s,keystroke:n}),l.bind("isEnabled").to(a,"isEnabled"),this.listenTo(l,"execute",(()=>{o.execute(t),o.editing.view.focus()})),l}}class Dv extends so{static get requires(){return[Nv,Fv]}static get pluginName(){return"Undo"}}class Lv extends ro{constructor(e,t){super(e),this.attributeKey=t}refresh(){const e=this.editor.model,t=e.document;this.value=t.selection.getAttribute(this.attributeKey),this.isEnabled=e.schema.checkAttributeInSelection(t.selection,this.attributeKey)}execute(e={}){const t=this.editor.model,i=t.document.selection,n=e.value,s=e.batch,o=e=>{if(i.isCollapsed)n?e.setSelectionAttribute(this.attributeKey,n):e.removeSelectionAttribute(this.attributeKey);else{const s=t.schema.getValidRanges(i.getRanges(),this.attributeKey);for(const t of s)n?e.setAttribute(this.attributeKey,n,t):e.removeAttribute(this.attributeKey,t)}};s?t.enqueueChange(s,(e=>{o(e)})):t.change((e=>{o(e)}))}}const zv="fontSize",Hv="fontFamily",$v="fontColor",Wv="fontBackgroundColor";function jv(e,t){const i={model:{key:e,values:[]},view:{},upcastAlso:{}};for(const e of t)i.model.values.push(e.model),i.view[e.model]=e.view,e.upcastAlso&&(i.upcastAlso[e.model]=e.upcastAlso);return i}function Uv(e){return t=>t.getStyle(e).replace(/\s/g,"")}function qv(e){return(t,{writer:i})=>i.createAttributeElement("span",{style:`${e}:${t}`},{priority:7})}class Gv extends Lv{constructor(e){super(e,Hv)}}function Kv(e){return e.map(Zv).filter((e=>void 0!==e))}function Zv(e){return"object"==typeof e?e:"default"===e?{title:"Default",model:void 0}:"string"==typeof e?function(e){const t=e.replace(/"|'/g,"").split(","),i=t[0],n=t.map(Jv).join(", ");return{title:i,model:n,view:{name:"span",styles:{"font-family":n},priority:7}}}(e):void 0}function Jv(e){return(e=e.trim()).indexOf(" ")>0&&(e=`'${e}'`),e}class Qv extends so{static get pluginName(){return"FontFamilyEditing"}constructor(e){super(e),e.config.define(Hv,{options:["default","Arial, Helvetica, sans-serif","Courier New, Courier, monospace","Georgia, serif","Lucida Sans Unicode, Lucida Grande, sans-serif","Tahoma, Geneva, sans-serif","Times New Roman, Times, serif","Trebuchet MS, Helvetica, sans-serif","Verdana, Geneva, sans-serif"],supportAllValues:!1})}init(){const e=this.editor;e.model.schema.extend("$text",{allowAttributes:Hv}),e.model.schema.setAttributeProperties(Hv,{isFormatting:!0,copyOnEnter:!0});const t=Kv(e.config.get("fontFamily.options")).filter((e=>e.model)),i=jv(Hv,t);e.config.get("fontFamily.supportAllValues")?(this._prepareAnyValueConverters(),this._prepareCompatibilityConverter()):e.conversion.attributeToElement(i),e.commands.add(Hv,new Gv(e))}_prepareAnyValueConverters(){const e=this.editor;e.conversion.for("downcast").attributeToElement({model:Hv,view:(e,{writer:t})=>t.createAttributeElement("span",{style:"font-family:"+e},{priority:7})}),e.conversion.for("upcast").elementToAttribute({model:{key:Hv,value:e=>e.getStyle("font-family")},view:{name:"span",styles:{"font-family":/.*/}}})}_prepareCompatibilityConverter(){this.editor.conversion.for("upcast").elementToAttribute({view:{name:"font",attributes:{face:/.*/}},model:{key:Hv,value:e=>e.getAttribute("face")}})}}const Yv='';class Xv extends so{static get pluginName(){return"FontFamilyUI"}init(){const e=this.editor,t=e.t,i=this._getLocalizedOptions(),n=e.commands.get(Hv),s=t("Font Family"),o=function(e,t){const i=new Ks;for(const n of e){const e={type:"button",model:new Sf({commandName:Hv,commandParam:n.model,label:n.title,role:"menuitemradio",withText:!0})};e.model.bind("isOn").to(t,"value",(e=>e===n.model||!(!e||!n.model)&&e.split(",")[0].replace(/'/g,"").toLowerCase()===n.model.toLowerCase())),n.view&&"string"!=typeof n.view&&n.view.styles&&e.model.set("labelStyle",`font-family: ${n.view.styles["font-family"]}`),i.add(e)}return i}(i,n);e.ui.componentFactory.add(Hv,(t=>{const i=Dm(t);return Hm(i,o,{role:"menu",ariaLabel:s}),i.buttonView.set({label:s,icon:Yv,tooltip:!0}),i.extendTemplate({attributes:{class:"ck-font-family-dropdown"}}),i.bind("isEnabled").to(n),this.listenTo(i,"execute",(t=>{e.execute(t.source.commandName,{value:t.source.commandParam}),e.editing.view.focus()})),i})),e.ui.componentFactory.add(`menuBar:${Hv}`,(t=>{const i=new dp(t);i.buttonView.set({label:s,icon:Yv}),i.bind("isEnabled").to(n);const r=new hp(t);for(const n of o){const s=new qf(t,i),o=new up(t);o.bind(...Object.keys(n.model)).to(n.model),o.bind("ariaChecked").to(o,"isOn"),o.delegate("execute").to(i),o.on("execute",(()=>{e.execute(n.model.commandName,{value:n.model.commandParam}),e.editing.view.focus()})),s.children.add(o),r.items.add(s)}return i.panelView.children.add(r),i}))}_getLocalizedOptions(){const e=this.editor,t=e.t;return Kv(e.config.get(Hv).options).map((e=>("Default"===e.title&&(e.title=t("Default")),e)))}}class e_ extends Lv{constructor(e){super(e,zv)}}function t_(e){return e.map((e=>function(e){"number"==typeof e&&(e=String(e));if("object"==typeof e&&(t=e,t.title&&t.model&&t.view))return n_(e);var t;const i=function(e){return"string"==typeof e?i_[e]:i_[e.model]}(e);if(i)return n_(i);if("default"===e)return{model:void 0,title:"Default"};if(function(e){let t;if("object"==typeof e){if(!e.model)throw new y("font-size-invalid-definition",null,e);t=parseFloat(e.model)}else t=parseFloat(e);return isNaN(t)}(e))return;return function(e){"string"==typeof e&&(e={title:e,model:`${parseFloat(e)}px`});return e.view={name:"span",styles:{"font-size":e.model}},n_(e)}(e)}(e))).filter((e=>void 0!==e))}const i_={get tiny(){return{title:"Tiny",model:"tiny",view:{name:"span",classes:"text-tiny",priority:7}}},get small(){return{title:"Small",model:"small",view:{name:"span",classes:"text-small",priority:7}}},get big(){return{title:"Big",model:"big",view:{name:"span",classes:"text-big",priority:7}}},get huge(){return{title:"Huge",model:"huge",view:{name:"span",classes:"text-huge",priority:7}}}};function n_(e){return e.view&&"string"!=typeof e.view&&!e.view.priority&&(e.view.priority=7),e}const s_=["x-small","x-small","small","medium","large","x-large","xx-large","xxx-large"];class o_ extends so{static get pluginName(){return"FontSizeEditing"}constructor(e){super(e),e.config.define(zv,{options:["tiny","small","default","big","huge"],supportAllValues:!1})}init(){const e=this.editor;e.model.schema.extend("$text",{allowAttributes:zv}),e.model.schema.setAttributeProperties(zv,{isFormatting:!0,copyOnEnter:!0});const t=e.config.get("fontSize.supportAllValues"),i=t_(this.editor.config.get("fontSize.options")).filter((e=>e.model)),n=jv(zv,i);t?(this._prepareAnyValueConverters(n),this._prepareCompatibilityConverter()):e.conversion.attributeToElement(n),e.commands.add(zv,new e_(e))}_prepareAnyValueConverters(e){const t=this.editor,i=e.model.values.filter((e=>!Rh(String(e))&&!Bh(String(e))));if(i.length)throw new y("font-size-invalid-use-of-named-presets",null,{presets:i});t.conversion.for("downcast").attributeToElement({model:zv,view:(e,{writer:t})=>{if(e)return t.createAttributeElement("span",{style:"font-size:"+e},{priority:7})}}),t.conversion.for("upcast").elementToAttribute({model:{key:zv,value:e=>e.getStyle("font-size")},view:{name:"span",styles:{"font-size":/.*/}}})}_prepareCompatibilityConverter(){this.editor.conversion.for("upcast").elementToAttribute({view:{name:"font",attributes:{size:/^[+-]?\d{1,3}$/}},model:{key:zv,value:e=>{const t=e.getAttribute("size"),i="-"===t[0]||"+"===t[0];let n=parseInt(t,10);i&&(n=3+n);const s=s_.length-1,o=Math.min(Math.max(n,0),s);return s_[o]}}})}}const r_='';class a_ extends so{static get pluginName(){return"FontSizeUI"}init(){const e=this.editor,t=e.t,i=this._getLocalizedOptions(),n=e.commands.get(zv),s=t("Font Size"),o=function(e,t){const i=new Ks;for(const n of e){const e={type:"button",model:new Sf({commandName:zv,commandParam:n.model,label:n.title,class:"ck-fontsize-option",role:"menuitemradio",withText:!0})};n.view&&"string"!=typeof n.view&&(n.view.styles&&e.model.set("labelStyle",`font-size:${n.view.styles["font-size"]}`),n.view.classes&&e.model.set("class",`${e.model.class} ${n.view.classes}`)),e.model.bind("isOn").to(t,"value",(e=>e===n.model)),i.add(e)}return i}(i,n);e.ui.componentFactory.add(zv,(t=>{const i=Dm(t);return Hm(i,o,{role:"menu",ariaLabel:s}),i.buttonView.set({label:s,icon:r_,tooltip:!0}),i.extendTemplate({attributes:{class:["ck-font-size-dropdown"]}}),i.bind("isEnabled").to(n),this.listenTo(i,"execute",(t=>{e.execute(t.source.commandName,{value:t.source.commandParam}),e.editing.view.focus()})),i})),e.ui.componentFactory.add(`menuBar:${zv}`,(t=>{const i=new dp(t);i.buttonView.set({label:s,icon:r_}),i.bind("isEnabled").to(n);const r=new hp(t);for(const n of o){const s=new qf(t,i),o=new up(t);o.bind(...Object.keys(n.model)).to(n.model),o.bind("ariaChecked").to(o,"isOn"),o.delegate("execute").to(i),o.on("execute",(()=>{e.execute(n.model.commandName,{value:n.model.commandParam}),e.editing.view.focus()})),s.children.add(o),r.items.add(s)}return i.panelView.children.add(r),i}))}_getLocalizedOptions(){const e=this.editor,t=e.t,i={Default:t("Default"),Tiny:t("Tiny"),Small:t("Small"),Big:t("Big"),Huge:t("Huge")};return t_(e.config.get(zv).options).map((e=>{const t=i[e.title];return t&&t!=e.title&&(e=Object.assign({},e,{title:t})),e}))}}class l_ extends Lv{constructor(e){super(e,$v)}}class c_ extends so{static get pluginName(){return"FontColorEditing"}constructor(e){super(e),e.config.define($v,{colors:[{color:"hsl(0, 0%, 0%)",label:"Black"},{color:"hsl(0, 0%, 30%)",label:"Dim grey"},{color:"hsl(0, 0%, 60%)",label:"Grey"},{color:"hsl(0, 0%, 90%)",label:"Light grey"},{color:"hsl(0, 0%, 100%)",label:"White",hasBorder:!0},{color:"hsl(0, 75%, 60%)",label:"Red"},{color:"hsl(30, 75%, 60%)",label:"Orange"},{color:"hsl(60, 75%, 60%)",label:"Yellow"},{color:"hsl(90, 75%, 60%)",label:"Light green"},{color:"hsl(120, 75%, 60%)",label:"Green"},{color:"hsl(150, 75%, 60%)",label:"Aquamarine"},{color:"hsl(180, 75%, 60%)",label:"Turquoise"},{color:"hsl(210, 75%, 60%)",label:"Light blue"},{color:"hsl(240, 75%, 60%)",label:"Blue"},{color:"hsl(270, 75%, 60%)",label:"Purple"}],columns:5}),e.conversion.for("upcast").elementToAttribute({view:{name:"span",styles:{color:/[\s\S]+/}},model:{key:$v,value:Uv("color")}}),e.conversion.for("upcast").elementToAttribute({view:{name:"font",attributes:{color:/^#?\w+$/}},model:{key:$v,value:e=>e.getAttribute("color")}}),e.conversion.for("downcast").attributeToElement({model:$v,view:qv("color")}),e.commands.add($v,new l_(e)),e.model.schema.extend("$text",{allowAttributes:$v}),e.model.schema.setAttributeProperties($v,{isFormatting:!0,copyOnEnter:!0})}}class d_ extends so{constructor(e,{commandName:t,componentName:i,icon:n,dropdownLabel:s}){super(e),this.commandName=t,this.componentName=i,this.icon=n,this.dropdownLabel=s,this.columns=e.config.get(`${this.componentName}.columns`)}init(){const e=this.editor,t=e.locale,i=t.t,n=e.commands.get(this.commandName),s=e.config.get(this.componentName),o=em(t,tm(s.colors)),r=s.documentColors,a=!1!==s.colorPicker;e.ui.componentFactory.add(this.componentName,(t=>{const l=Dm(t);let c=!1;const d=function({dropdownView:e,colors:t,columns:i,removeButtonLabel:n,colorPickerLabel:s,documentColorsLabel:o,documentColorsCount:r,colorPickerViewConfig:a}){const l=e.locale,c=new Og(l,{colors:t,columns:i,removeButtonLabel:n,colorPickerLabel:s,documentColorsLabel:o,documentColorsCount:r,colorPickerViewConfig:a});return e.colorSelectorView=c,e.panelView.children.add(c),c}({dropdownView:l,colors:o.map((e=>({label:e.label,color:e.model,options:{hasBorder:e.hasBorder}}))),columns:this.columns,removeButtonLabel:i("Remove color"),colorPickerLabel:i("Color picker"),documentColorsLabel:0!==r?i("Document colors"):"",documentColorsCount:void 0===r?this.columns:r,colorPickerViewConfig:!!a&&(s.colorPicker||{})});return d.bind("selectedColor").to(n,"value"),l.buttonView.set({label:this.dropdownLabel,icon:this.icon,tooltip:!0}),l.extendTemplate({attributes:{class:"ck-color-ui-dropdown"}}),l.bind("isEnabled").to(n),d.on("execute",((t,i)=>{l.isOpen&&e.execute(this.commandName,{value:i.value,batch:this._undoStepBatch}),"colorPicker"!==i.source&&e.editing.view.focus(),"colorPickerSaveButton"===i.source&&(l.isOpen=!1)})),d.on("colorPicker:show",(()=>{this._undoStepBatch=e.model.createBatch()})),d.on("colorPicker:cancel",(()=>{this._undoStepBatch.operations.length&&(l.isOpen=!1,e.execute("undo",this._undoStepBatch)),e.editing.view.focus()})),l.on("change:isOpen",((t,i,n)=>{c||(c=!0,l.colorSelectorView.appendUI()),n&&(0!==r&&d.updateDocumentColors(e.model,this.componentName),d.updateSelectedColors(),d.showColorGridsFragment())})),Wm(l,(()=>l.colorSelectorView.colorGridsFragmentView.staticColorsGrid.items.find((e=>e.isOn)))),l})),e.ui.componentFactory.add(`menuBar:${this.componentName}`,(t=>{const s=new dp(t);s.buttonView.set({label:this.dropdownLabel,icon:this.icon}),s.bind("isEnabled").to(n);let a=!1;const l=new Og(t,{colors:o.map((e=>({label:e.label,color:e.model,options:{hasBorder:e.hasBorder}}))),columns:this.columns,removeButtonLabel:i("Remove color"),colorPickerLabel:i("Color picker"),documentColorsLabel:0!==r?i("Document colors"):"",documentColorsCount:void 0===r?this.columns:r,colorPickerViewConfig:!1});return l.bind("selectedColor").to(n,"value"),l.delegate("execute").to(s),l.on("execute",((t,i)=>{e.execute(this.commandName,{value:i.value,batch:this._undoStepBatch}),e.editing.view.focus()})),s.on("change:isOpen",((t,i,n)=>{a||(a=!0,l.appendUI()),n&&(0!==r&&l.updateDocumentColors(e.model,this.componentName),l.updateSelectedColors(),l.showColorGridsFragment())})),s.panelView.children.add(l),s}))}}class h_ extends d_{constructor(e){const t=e.locale.t;super(e,{commandName:$v,componentName:$v,icon:'',dropdownLabel:t("Font Color")})}static get pluginName(){return"FontColorUI"}}class u_ extends Lv{constructor(e){super(e,Wv)}}class m_ extends so{static get pluginName(){return"FontBackgroundColorEditing"}constructor(e){super(e),e.config.define(Wv,{colors:[{color:"hsl(0, 0%, 0%)",label:"Black"},{color:"hsl(0, 0%, 30%)",label:"Dim grey"},{color:"hsl(0, 0%, 60%)",label:"Grey"},{color:"hsl(0, 0%, 90%)",label:"Light grey"},{color:"hsl(0, 0%, 100%)",label:"White",hasBorder:!0},{color:"hsl(0, 75%, 60%)",label:"Red"},{color:"hsl(30, 75%, 60%)",label:"Orange"},{color:"hsl(60, 75%, 60%)",label:"Yellow"},{color:"hsl(90, 75%, 60%)",label:"Light green"},{color:"hsl(120, 75%, 60%)",label:"Green"},{color:"hsl(150, 75%, 60%)",label:"Aquamarine"},{color:"hsl(180, 75%, 60%)",label:"Turquoise"},{color:"hsl(210, 75%, 60%)",label:"Light blue"},{color:"hsl(240, 75%, 60%)",label:"Blue"},{color:"hsl(270, 75%, 60%)",label:"Purple"}],columns:5}),e.data.addStyleProcessorRules(Kh),e.conversion.for("upcast").elementToAttribute({view:{name:"span",styles:{"background-color":/[\s\S]+/}},model:{key:Wv,value:Uv("background-color")}}),e.conversion.for("downcast").attributeToElement({model:Wv,view:qv("background-color")}),e.commands.add(Wv,new u_(e)),e.model.schema.extend("$text",{allowAttributes:Wv}),e.model.schema.setAttributeProperties(Wv,{isFormatting:!0,copyOnEnter:!0})}}class g_ extends d_{constructor(e){const t=e.locale.t;super(e,{commandName:Wv,componentName:Wv,icon:'',dropdownLabel:t("Font Background Color")})}static get pluginName(){return"FontBackgroundColorUI"}}class f_ extends ro{constructor(e){super(e),this._isEnabledBasedOnSelection=!1}refresh(){const e=this.editor.model,t=Zs(e.document.selection.getSelectedBlocks());this.value=!!t&&t.is("element","paragraph"),this.isEnabled=!!t&&p_(t,e.schema)}execute(e={}){const t=this.editor.model,i=t.document,n=e.selection||i.selection;t.canEditAt(n)&&t.change((e=>{const i=n.getSelectedBlocks();for(const n of i)!n.is("element","paragraph")&&p_(n,t.schema)&&e.rename(n,"paragraph")}))}}function p_(e,t){return t.checkChild(e.parent,"paragraph")&&!t.isObject(e)}class b_ extends ro{constructor(e){super(e),this._isEnabledBasedOnSelection=!1}execute(e){const t=this.editor.model,i=e.attributes;let n=e.position;t.canEditAt(n)&&t.change((e=>{if(n=this._findPositionToInsertParagraph(n,e),!n)return;const s=e.createElement("paragraph");i&&t.schema.setAllowedAttributes(s,i,e),t.insertContent(s,n),e.setSelection(s,"in")}))}_findPositionToInsertParagraph(e,t){const i=this.editor.model;if(i.schema.checkChild(e,"paragraph"))return e;const n=i.schema.findAllowedParent(e,"paragraph");if(!n)return null;const s=e.parent,o=i.schema.checkChild(s,"$text");return s.isEmpty||o&&e.isAtEnd?i.createPositionAfter(s):!s.isEmpty&&o&&e.isAtStart?i.createPositionBefore(s):t.split(e,n).position}}class w_ extends so{static get pluginName(){return"Paragraph"}init(){const e=this.editor,t=e.model;e.commands.add("paragraph",new f_(e)),e.commands.add("insertParagraph",new b_(e)),t.schema.register("paragraph",{inheritAllFrom:"$block"}),e.conversion.elementToElement({model:"paragraph",view:"p"}),e.conversion.for("upcast").elementToElement({model:(e,{writer:t})=>w_.paragraphLikeElements.has(e.name)?e.isEmpty?null:t.createElement("paragraph"):null,view:/.+/,converterPriority:"low"})}}w_.paragraphLikeElements=new Set(["blockquote","dd","div","dt","h1","h2","h3","h4","h5","h6","li","p","td","th"]);const v_=w_;class __ extends ro{constructor(e,t){super(e),this.modelElements=t}refresh(){const e=Zs(this.editor.model.document.selection.getSelectedBlocks());this.value=!!e&&this.modelElements.includes(e.name)&&e.name,this.isEnabled=!!e&&this.modelElements.some((t=>y_(e,t,this.editor.model.schema)))}execute(e){const t=this.editor.model,i=t.document,n=e.value;t.change((e=>{const s=Array.from(i.selection.getSelectedBlocks()).filter((e=>y_(e,n,t.schema)));for(const t of s)t.is("element",n)||e.rename(t,n)}))}}function y_(e,t,i){return i.checkChild(e.parent,t)&&!i.isObject(e)}const k_="paragraph";class C_ extends so{static get pluginName(){return"HeadingEditing"}constructor(e){super(e),e.config.define("heading",{options:[{model:"paragraph",title:"Paragraph",class:"ck-heading_paragraph"},{model:"heading1",view:"h2",title:"Heading 1",class:"ck-heading_heading1"},{model:"heading2",view:"h3",title:"Heading 2",class:"ck-heading_heading2"},{model:"heading3",view:"h4",title:"Heading 3",class:"ck-heading_heading3"}]})}static get requires(){return[v_]}init(){const e=this.editor,t=e.config.get("heading.options"),i=[];for(const n of t)"paragraph"!==n.model&&(e.model.schema.register(n.model,{inheritAllFrom:"$block"}),e.conversion.elementToElement(n),i.push(n.model));this._addDefaultH1Conversion(e),e.commands.add("heading",new __(e,i))}afterInit(){const e=this.editor,t=e.commands.get("enter"),i=e.config.get("heading.options");t&&this.listenTo(t,"afterExecute",((t,n)=>{const s=e.model.document.selection.getFirstPosition().parent;i.some((e=>s.is("element",e.model)))&&!s.is("element",k_)&&0===s.childCount&&n.writer.rename(s,k_)}))}_addDefaultH1Conversion(e){e.conversion.for("upcast").elementToElement({model:"heading1",view:"h1",converterPriority:w.low+1})}}class A_ extends so{static get pluginName(){return"HeadingUI"}init(){const e=this.editor,t=e.t,i=function(e){const t=e.t,i={Paragraph:t("Paragraph"),"Heading 1":t("Heading 1"),"Heading 2":t("Heading 2"),"Heading 3":t("Heading 3"),"Heading 4":t("Heading 4"),"Heading 5":t("Heading 5"),"Heading 6":t("Heading 6")};return e.config.get("heading.options").map((e=>{const t=i[e.title];return t&&t!=e.title&&(e.title=t),e}))}(e),n=t("Choose heading"),s=t("Heading");e.ui.componentFactory.add("heading",(t=>{const o={},r=new Ks,a=e.commands.get("heading"),l=e.commands.get("paragraph"),c=[a];for(const e of i){const t={type:"button",model:new Sf({label:e.title,class:e.class,role:"menuitemradio",withText:!0})};"paragraph"===e.model?(t.model.bind("isOn").to(l,"value"),t.model.set("commandName","paragraph"),c.push(l)):(t.model.bind("isOn").to(a,"value",(t=>t===e.model)),t.model.set({commandName:"heading",commandValue:e.model})),r.add(t),o[e.model]=e.title}const d=Dm(t);return Hm(d,r,{ariaLabel:s,role:"menu"}),d.buttonView.set({ariaLabel:s,ariaLabelledBy:void 0,isOn:!1,withText:!0,tooltip:s}),d.extendTemplate({attributes:{class:["ck-heading-dropdown"]}}),d.bind("isEnabled").toMany(c,"isEnabled",((...e)=>e.some((e=>e)))),d.buttonView.bind("label").to(a,"value",l,"value",((e,t)=>{const i=t?"paragraph":e;return"boolean"==typeof i?n:o[i]?o[i]:n})),d.buttonView.bind("ariaLabel").to(a,"value",l,"value",((e,t)=>{const i=t?"paragraph":e;return"boolean"==typeof i?s:o[i]?`${o[i]}, ${s}`:s})),this.listenTo(d,"execute",(t=>{const{commandName:i,commandValue:n}=t.source;e.execute(i,n?{value:n}:void 0),e.editing.view.focus()})),d})),e.ui.componentFactory.add("menuBar:heading",(n=>{const s=new dp(n),o=e.commands.get("heading"),r=e.commands.get("paragraph"),a=[o],l=new hp(n);s.set({class:"ck-heading-dropdown"}),l.set({ariaLabel:t("Heading"),role:"menu"}),s.buttonView.set({label:t("Heading")}),s.panelView.children.add(l);for(const t of i){const i=new qf(n,s),c=new up(n);i.children.add(c),l.items.add(i),c.set({label:t.title,role:"menuitemradio",class:t.class}),c.bind("ariaChecked").to(c,"isOn"),c.delegate("execute").to(s),c.on("execute",(()=>{const i="paragraph"===t.model?"paragraph":"heading";e.execute(i,{value:t.model}),e.editing.view.focus()})),"paragraph"===t.model?(c.bind("isOn").to(r,"value"),a.push(r)):c.bind("isOn").to(o,"value",(e=>e===t.model))}return s.bind("isEnabled").toMany(a,"isEnabled",((...e)=>e.some((e=>e)))),s}))}}new Set(["paragraph","heading1","heading2","heading3","heading4","heading5","heading6"]);class x_ extends ro{refresh(){const e=this.editor.model,t=e.schema,i=e.document.selection;this.isEnabled=function(e,t,i){const n=function(e,t){const i=$w(e,t),n=i.start.parent;if(n.isEmpty&&!n.is("element","$root"))return n.parent;return n}(e,i);return t.checkChild(n,"horizontalLine")}(i,t,e)}execute(){const e=this.editor.model;e.change((t=>{const i=t.createElement("horizontalLine");e.insertObject(i,null,null,{setSelection:"after"})}))}}class E_ extends so{static get pluginName(){return"HorizontalLineEditing"}init(){const e=this.editor,t=e.model.schema,i=e.t,n=e.conversion;t.register("horizontalLine",{inheritAllFrom:"$blockObject"}),n.for("dataDowncast").elementToElement({model:"horizontalLine",view:(e,{writer:t})=>t.createEmptyElement("hr")}),n.for("editingDowncast").elementToStructure({model:"horizontalLine",view:(e,{writer:t})=>{const n=i("Horizontal line"),s=t.createContainerElement("div",null,t.createEmptyElement("hr"));return t.addClass("ck-horizontal-line",s),t.setCustomProperty("hr",!0,s),function(e,t,i){return t.setCustomProperty("horizontalLine",!0,e),Fw(e,t,{label:i})}(s,t,n)}}),n.for("upcast").elementToElement({view:"hr",model:"horizontalLine"}),e.commands.add("horizontalLine",new x_(e))}}class T_ extends so{static get pluginName(){return"HorizontalLineUI"}init(){const e=this.editor;e.ui.componentFactory.add("horizontalLine",(()=>{const e=this._createButton(Ku);return e.set({tooltip:!0}),e})),e.ui.componentFactory.add("menuBar:horizontalLine",(()=>this._createButton(up)))}_createButton(e){const t=this.editor,i=t.locale,n=t.commands.get("horizontalLine"),s=new e(t.locale),o=i.t;return s.set({label:o("Horizontal line"),icon:fu.horizontalLine}),s.bind("isEnabled").to(n,"isEnabled"),this.listenTo(s,"execute",(()=>{t.execute("horizontalLine"),t.editing.view.focus()})),s}}const S_=function(e,t,i,n){var s=-1,o=null==e?0:e.length;for(n&&o&&(i=e[++s]);++s=n?e:Jo(e,t,i)};var dy=RegExp("[\\u200d\\ud800-\\udfff\\u0300-\\u036f\\ufe20-\\ufe2f\\u20d0-\\u20ff\\ufe0e\\ufe0f]");const hy=function(e){return dy.test(e)};const uy=function(e){return e.split("")};var my="\\ud800-\\udfff",gy="["+my+"]",fy="[\\u0300-\\u036f\\ufe20-\\ufe2f\\u20d0-\\u20ff]",py="\\ud83c[\\udffb-\\udfff]",by="[^"+my+"]",wy="(?:\\ud83c[\\udde6-\\uddff]){2}",vy="[\\ud800-\\udbff][\\udc00-\\udfff]",_y="(?:"+fy+"|"+py+")"+"?",yy="[\\ufe0e\\ufe0f]?",ky=yy+_y+("(?:\\u200d(?:"+[by,wy,vy].join("|")+")"+yy+_y+")*"),Cy="(?:"+[by+fy+"?",fy,wy,vy,gy].join("|")+")",Ay=RegExp(py+"(?="+py+")|"+Cy+ky,"g");const xy=function(e){return e.match(Ay)||[]};const Ey=function(e){return hy(e)?xy(e):uy(e)};const Ty=function(e){return function(t){t=Uo(t);var i=hy(t)?Ey(t):void 0,n=i?i[0]:t.charAt(0),s=i?cy(i,1).join(""):t.slice(1);return n[e]()+s}}("toUpperCase");const Sy=ly((function(e,t,i){return e+(i?" ":"")+Ty(t)}));function Py(e,t,i,n){t&&function(e,t,i){if(t.attributes)for(const[n]of Object.entries(t.attributes))e.removeAttribute(n,i);if(t.styles)for(const n of Object.keys(t.styles))e.removeStyle(n,i);t.classes&&e.removeClass(t.classes,i)}(e,t,n),i&&Iy(e,i,n)}function Iy(e,t,i){if(t.attributes)for(const[n,s]of Object.entries(t.attributes))e.setAttribute(n,s,i);t.styles&&e.setStyle(t.styles,i),t.classes&&e.addClass(t.classes,i)}function Vy(e,t,i,n,s){const o=t.getAttribute(i),r={};for(const e of["attributes","styles","classes"]){if(e!=n){o&&o[e]&&(r[e]=o[e]);continue}if("classes"==n){const t=new Set(o&&o.classes||[]);s(t),t.size&&(r[e]=Array.from(t));continue}const t=new Map(Object.entries(o&&o[e]||{}));s(t),t.size&&(r[e]=Object.fromEntries(t))}Object.keys(r).length?t.is("documentSelection")?e.setSelectionAttribute(i,r):e.setAttribute(i,r,t):o&&(t.is("documentSelection")?e.removeSelectionAttribute(i):e.removeAttribute(i,t))}function Ry(e){return`html${t=e,Sy(t).replace(/ /g,"")}Attributes`;var t}function Oy({model:e}){return(t,i)=>i.writer.createElement(e,{htmlContent:t.getCustomProperty("$rawContent")})}function By(e,{view:t,isInline:i}){const n=e.t;return(e,{writer:s})=>{const o=n("HTML object"),r=My(t,e,s),a=e.getAttribute(Ry(t));s.addClass("html-object-embed__content",r),a&&Iy(s,a,r);return Fw(s.createContainerElement(i?"span":"div",{class:"html-object-embed","data-html-object-embed-label":o},r),s,{label:o})}}function My(e,t,i){return i.createRawElement(e,null,((e,i)=>{i.setContentOf(e,t.getAttribute("htmlContent"))}))}function Ny({view:e,model:t,allowEmpty:i},n){return t=>{t.on(`element:${e}`,((e,t,o)=>{let r=n.processViewAttributes(t.viewItem,o);if(r||o.consumable.test(t.viewItem,{name:!0})){if(r=r||{},o.consumable.consume(t.viewItem,{name:!0}),t.modelRange||(t=Object.assign(t,o.convertChildren(t.viewItem,t.modelCursor))),i&&t.modelRange.isCollapsed&&Object.keys(r).length){const e=o.writer.createElement("htmlEmptyElement");if(!o.safeInsert(e,t.modelCursor))return;const i=o.getSplitParts(e);return t.modelRange=o.writer.createRange(t.modelRange.start,o.writer.createPositionAfter(i[i.length-1])),o.updateConversionResult(e,t),void s(e,r,o)}for(const e of t.modelRange.getItems())s(e,r,o)}}),{priority:"low"})};function s(e,i,n){if(n.schema.checkAttribute(e,t)){const s=function(e,t){const i=wl(e);let n="attributes";for(n in t)i[n]="classes"==n?Array.from(new Set([...e[n]||[],...t[n]])):{...e[n],...t[n]};return i}(i,e.getAttribute(t)||{});n.writer.setAttribute(t,s,e)}}}function Fy({model:e,view:t},i){return(n,{writer:s,consumable:o})=>{if(!n.hasAttribute(e))return null;const r=s.createContainerElement(t),a=n.getAttribute(e);return o.consume(n,`attribute:${e}`),Iy(s,a,r),r.getFillerOffset=()=>null,i?Fw(r,s):r}}function Dy({priority:e,view:t}){return(i,n)=>{if(!i)return;const{writer:s}=n,o=s.createAttributeElement(t,null,{priority:e});return Iy(s,i,o),o}}function Ly({view:e},t){return i=>{i.on(`element:${e}`,((e,i,n)=>{if(!i.modelRange||i.modelRange.isCollapsed)return;const s=t.processViewAttributes(i.viewItem,n);s&&n.writer.setAttribute(Ry(i.viewItem.name),s,i.modelRange)}),{priority:"low"})}}function zy({view:e,model:t}){return i=>{i.on(`attribute:${Ry(e)}:${t}`,((e,t,i)=>{if(!i.consumable.consume(t.item,e.name))return;const{attributeOldValue:n,attributeNewValue:s}=t;Py(i.writer,n,s,i.mapper.toViewElement(t.item))}))}}const Hy=[{model:"codeBlock",view:"pre"},{model:"paragraph",view:"p"},{model:"blockQuote",view:"blockquote"},{model:"listItem",view:"li"},{model:"pageBreak",view:"div"},{model:"rawHtml",view:"div"},{model:"table",view:"table"},{model:"tableRow",view:"tr"},{model:"tableCell",view:"td"},{model:"tableCell",view:"th"},{model:"tableColumnGroup",view:"colgroup"},{model:"tableColumn",view:"col"},{model:"caption",view:"caption"},{model:"caption",view:"figcaption"},{model:"imageBlock",view:"img"},{model:"imageInline",view:"img"},{model:"htmlP",view:"p",modelSchema:{inheritAllFrom:"$block"}},{model:"htmlBlockquote",view:"blockquote",modelSchema:{inheritAllFrom:"$container"}},{model:"htmlTable",view:"table",modelSchema:{allowWhere:"$block",isBlock:!0}},{model:"htmlTbody",view:"tbody",modelSchema:{allowIn:"htmlTable",isBlock:!1}},{model:"htmlThead",view:"thead",modelSchema:{allowIn:"htmlTable",isBlock:!1}},{model:"htmlTfoot",view:"tfoot",modelSchema:{allowIn:"htmlTable",isBlock:!1}},{model:"htmlCaption",view:"caption",modelSchema:{allowIn:"htmlTable",allowChildren:"$text",isBlock:!1}},{model:"htmlColgroup",view:"colgroup",modelSchema:{allowIn:"htmlTable",allowChildren:"col",isBlock:!1}},{model:"htmlCol",view:"col",modelSchema:{allowIn:"htmlColgroup",isBlock:!1}},{model:"htmlTr",view:"tr",modelSchema:{allowIn:["htmlTable","htmlThead","htmlTbody"],isLimit:!0}},{model:"htmlTd",view:"td",modelSchema:{allowIn:"htmlTr",allowContentOf:"$container",isLimit:!0,isBlock:!1}},{model:"htmlTh",view:"th",modelSchema:{allowIn:"htmlTr",allowContentOf:"$container",isLimit:!0,isBlock:!1}},{model:"htmlFigure",view:"figure",modelSchema:{inheritAllFrom:"$container",isBlock:!1}},{model:"htmlFigcaption",view:"figcaption",modelSchema:{allowIn:"htmlFigure",allowChildren:"$text",isBlock:!1}},{model:"htmlAddress",view:"address",modelSchema:{inheritAllFrom:"$container",isBlock:!1}},{model:"htmlAside",view:"aside",modelSchema:{inheritAllFrom:"$container",isBlock:!1}},{model:"htmlMain",view:"main",modelSchema:{inheritAllFrom:"$container",isBlock:!1}},{model:"htmlDetails",view:"details",modelSchema:{inheritAllFrom:"$container",isBlock:!1}},{model:"htmlSummary",view:"summary",modelSchema:{allowChildren:"$text",allowIn:"htmlDetails",isBlock:!1}},{model:"htmlDiv",view:"div",paragraphLikeModel:"htmlDivParagraph",modelSchema:{inheritAllFrom:"$container"}},{model:"htmlFieldset",view:"fieldset",modelSchema:{inheritAllFrom:"$container",isBlock:!1}},{model:"htmlLegend",view:"legend",modelSchema:{allowIn:"htmlFieldset",allowChildren:"$text"}},{model:"htmlHeader",view:"header",modelSchema:{inheritAllFrom:"$container",isBlock:!1}},{model:"htmlFooter",view:"footer",modelSchema:{inheritAllFrom:"$container",isBlock:!1}},{model:"htmlForm",view:"form",modelSchema:{inheritAllFrom:"$container",isBlock:!0}},{model:"htmlHgroup",view:"hgroup",modelSchema:{allowChildren:["htmlH1","htmlH2","htmlH3","htmlH4","htmlH5","htmlH6"],isBlock:!1}},{model:"htmlH1",view:"h1",modelSchema:{inheritAllFrom:"$block"}},{model:"htmlH2",view:"h2",modelSchema:{inheritAllFrom:"$block"}},{model:"htmlH3",view:"h3",modelSchema:{inheritAllFrom:"$block"}},{model:"htmlH4",view:"h4",modelSchema:{inheritAllFrom:"$block"}},{model:"htmlH5",view:"h5",modelSchema:{inheritAllFrom:"$block"}},{model:"htmlH6",view:"h6",modelSchema:{inheritAllFrom:"$block"}},{model:"$htmlList",modelSchema:{allowWhere:"$container",allowChildren:["$htmlList","htmlLi"],isBlock:!1}},{model:"htmlDir",view:"dir",modelSchema:{inheritAllFrom:"$htmlList"}},{model:"htmlMenu",view:"menu",modelSchema:{inheritAllFrom:"$htmlList"}},{model:"htmlUl",view:"ul",modelSchema:{inheritAllFrom:"$htmlList"}},{model:"htmlOl",view:"ol",modelSchema:{inheritAllFrom:"$htmlList"}},{model:"htmlLi",view:"li",modelSchema:{allowIn:"$htmlList",allowChildren:"$text",isBlock:!1}},{model:"htmlPre",view:"pre",modelSchema:{inheritAllFrom:"$block"}},{model:"htmlArticle",view:"article",modelSchema:{inheritAllFrom:"$container",isBlock:!1}},{model:"htmlSection",view:"section",modelSchema:{inheritAllFrom:"$container",isBlock:!1}},{model:"htmlNav",view:"nav",modelSchema:{inheritAllFrom:"$container",isBlock:!1}},{model:"htmlDivDl",view:"div",modelSchema:{allowChildren:["htmlDt","htmlDd"],allowIn:"htmlDl"}},{model:"htmlDl",view:"dl",modelSchema:{allowWhere:"$container",allowChildren:["htmlDt","htmlDd","htmlDivDl"],isBlock:!1}},{model:"htmlDt",view:"dt",modelSchema:{allowChildren:"$block",isBlock:!1}},{model:"htmlDd",view:"dd",modelSchema:{allowChildren:"$block",isBlock:!1}},{model:"htmlCenter",view:"center",modelSchema:{inheritAllFrom:"$container",isBlock:!1}}],$y=[{model:"htmlLiAttributes",view:"li",appliesToBlock:!0,coupledAttribute:"listItemId"},{model:"htmlOlAttributes",view:"ol",appliesToBlock:!0,coupledAttribute:"listItemId"},{model:"htmlUlAttributes",view:"ul",appliesToBlock:!0,coupledAttribute:"listItemId"},{model:"htmlFigureAttributes",view:"figure",appliesToBlock:"table"},{model:"htmlTheadAttributes",view:"thead",appliesToBlock:"table"},{model:"htmlTbodyAttributes",view:"tbody",appliesToBlock:"table"},{model:"htmlFigureAttributes",view:"figure",appliesToBlock:"imageBlock"},{model:"htmlAcronym",view:"acronym",attributeProperties:{copyOnEnter:!0,isFormatting:!0}},{model:"htmlTt",view:"tt",attributeProperties:{copyOnEnter:!0,isFormatting:!0}},{model:"htmlFont",view:"font",attributeProperties:{copyOnEnter:!0,isFormatting:!0}},{model:"htmlTime",view:"time",attributeProperties:{copyOnEnter:!0,isFormatting:!0}},{model:"htmlVar",view:"var",attributeProperties:{copyOnEnter:!0,isFormatting:!0}},{model:"htmlBig",view:"big",attributeProperties:{copyOnEnter:!0,isFormatting:!0}},{model:"htmlSmall",view:"small",attributeProperties:{copyOnEnter:!0,isFormatting:!0}},{model:"htmlSamp",view:"samp",attributeProperties:{copyOnEnter:!0,isFormatting:!0}},{model:"htmlQ",view:"q",attributeProperties:{copyOnEnter:!0,isFormatting:!0}},{model:"htmlOutput",view:"output",attributeProperties:{copyOnEnter:!0,isFormatting:!0}},{model:"htmlKbd",view:"kbd",attributeProperties:{copyOnEnter:!0,isFormatting:!0}},{model:"htmlBdi",view:"bdi",attributeProperties:{copyOnEnter:!0,isFormatting:!0}},{model:"htmlBdo",view:"bdo",attributeProperties:{copyOnEnter:!0,isFormatting:!0}},{model:"htmlAbbr",view:"abbr",attributeProperties:{copyOnEnter:!0,isFormatting:!0}},{model:"htmlA",view:"a",priority:5,coupledAttribute:"linkHref"},{model:"htmlStrong",view:"strong",coupledAttribute:"bold",attributeProperties:{copyOnEnter:!0,isFormatting:!0}},{model:"htmlB",view:"b",coupledAttribute:"bold",attributeProperties:{copyOnEnter:!0,isFormatting:!0}},{model:"htmlI",view:"i",coupledAttribute:"italic",attributeProperties:{copyOnEnter:!0,isFormatting:!0}},{model:"htmlEm",view:"em",coupledAttribute:"italic",attributeProperties:{copyOnEnter:!0,isFormatting:!0}},{model:"htmlS",view:"s",coupledAttribute:"strikethrough",attributeProperties:{copyOnEnter:!0,isFormatting:!0}},{model:"htmlDel",view:"del",coupledAttribute:"strikethrough",attributeProperties:{copyOnEnter:!0,isFormatting:!0}},{model:"htmlIns",view:"ins",attributeProperties:{copyOnEnter:!0,isFormatting:!0}},{model:"htmlU",view:"u",coupledAttribute:"underline",attributeProperties:{copyOnEnter:!0,isFormatting:!0}},{model:"htmlSub",view:"sub",coupledAttribute:"subscript",attributeProperties:{copyOnEnter:!0,isFormatting:!0}},{model:"htmlSup",view:"sup",coupledAttribute:"superscript",attributeProperties:{copyOnEnter:!0,isFormatting:!0}},{model:"htmlCode",view:"code",coupledAttribute:"code",attributeProperties:{copyOnEnter:!0,isFormatting:!0}},{model:"htmlMark",view:"mark",attributeProperties:{copyOnEnter:!0,isFormatting:!0}},{model:"htmlSpan",view:"span",attributeProperties:{copyOnEnter:!0,isFormatting:!0}},{model:"htmlCite",view:"cite",attributeProperties:{copyOnEnter:!0,isFormatting:!0}},{model:"htmlLabel",view:"label",attributeProperties:{copyOnEnter:!0,isFormatting:!0}},{model:"htmlDfn",view:"dfn",attributeProperties:{copyOnEnter:!0,isFormatting:!0}},{model:"htmlObject",view:"object",isObject:!0,modelSchema:{inheritAllFrom:"$inlineObject"}},{model:"htmlIframe",view:"iframe",isObject:!0,modelSchema:{inheritAllFrom:"$inlineObject"}},{model:"htmlInput",view:"input",isObject:!0,modelSchema:{inheritAllFrom:"$inlineObject"}},{model:"htmlButton",view:"button",isObject:!0,modelSchema:{inheritAllFrom:"$inlineObject"}},{model:"htmlTextarea",view:"textarea",isObject:!0,modelSchema:{inheritAllFrom:"$inlineObject"}},{model:"htmlSelect",view:"select",isObject:!0,modelSchema:{inheritAllFrom:"$inlineObject"}},{model:"htmlVideo",view:"video",isObject:!0,modelSchema:{inheritAllFrom:"$inlineObject"}},{model:"htmlEmbed",view:"embed",isObject:!0,modelSchema:{inheritAllFrom:"$inlineObject"}},{model:"htmlOembed",view:"oembed",isObject:!0,modelSchema:{inheritAllFrom:"$inlineObject"}},{model:"htmlAudio",view:"audio",isObject:!0,modelSchema:{inheritAllFrom:"$inlineObject"}},{model:"htmlImg",view:"img",isObject:!0,modelSchema:{inheritAllFrom:"$inlineObject"}},{model:"htmlCanvas",view:"canvas",isObject:!0,modelSchema:{inheritAllFrom:"$inlineObject"}},{model:"htmlMeter",view:"meter",isObject:!0,modelSchema:{inheritAllFrom:"$inlineObject"}},{model:"htmlProgress",view:"progress",isObject:!0,modelSchema:{inheritAllFrom:"$inlineObject"}},{model:"htmlScript",view:"script",modelSchema:{allowWhere:["$text","$block"],isInline:!0}},{model:"htmlStyle",view:"style",modelSchema:{allowWhere:["$text","$block"],isInline:!0}},{model:"htmlCustomElement",view:"$customElement",modelSchema:{allowWhere:["$text","$block"],allowAttributesOf:"$inlineObject",isInline:!0}}];const Wy=$s((function(e,t,i,n){Is(e,t,i,n)}));class jy extends so{constructor(){super(...arguments),this._definitions=[]}static get pluginName(){return"DataSchema"}init(){for(const e of Hy)this.registerBlockElement(e);for(const e of $y)this.registerInlineElement(e)}registerBlockElement(e){this._definitions.push({...e,isBlock:!0})}registerInlineElement(e){this._definitions.push({...e,isInline:!0})}extendBlockElement(e){this._extendDefinition({...e,isBlock:!0})}extendInlineElement(e){this._extendDefinition({...e,isInline:!0})}getDefinitionsForView(e,t=!1){const i=new Set;for(const n of this._getMatchingViewDefinitions(e)){if(t)for(const e of this._getReferences(n.model))i.add(e);i.add(n)}return i}getDefinitionsForModel(e){return this._definitions.filter((t=>t.model==e))}_getMatchingViewDefinitions(e){return this._definitions.filter((t=>t.view&&function(e,t){if("string"==typeof e)return e===t;if(e instanceof RegExp)return e.test(t);return!1}(e,t.view)))}*_getReferences(e){const t=["inheritAllFrom","inheritTypesFrom","allowWhere","allowContentOf","allowAttributesOf"],i=this._definitions.filter((t=>t.model==e));for(const{modelSchema:n}of i)if(n)for(const i of t)for(const t of Cs(n[i]||[])){const i=this._definitions.filter((e=>e.model==t));for(const n of i)t!==e&&(yield*this._getReferences(n.model),yield n)}}_extendDefinition(e){const t=Array.from(this._definitions.entries()).filter((([,t])=>t.model==e.model));if(0!=t.length)for(const[i,n]of t)this._definitions[i]=Wy({},n,e,((e,t)=>Array.isArray(e)?e.concat(t):void 0));else this._definitions.push(e)}}class Uy extends so{constructor(e){super(e),this._dataSchema=e.plugins.get("DataSchema"),this._allowedAttributes=new To,this._disallowedAttributes=new To,this._allowedElements=new Set,this._disallowedElements=new Set,this._dataInitialized=!1,this._coupledAttributes=null,this._registerElementsAfterInit(),this._registerElementHandlers(),this._registerCoupledAttributesPostFixer(),this._registerAssociatedHtmlAttributesPostFixer()}static get pluginName(){return"DataFilter"}static get requires(){return[jy,ev]}loadAllowedConfig(e){for(const t of e){const e=t.name||/[\s\S]+/,i=Jy(t);this.allowElement(e),i.forEach((e=>this.allowAttributes(e)))}}loadDisallowedConfig(e){for(const t of e){const e=t.name||/[\s\S]+/,i=Jy(t);0==i.length?this.disallowElement(e):i.forEach((e=>this.disallowAttributes(e)))}}loadAllowedEmptyElementsConfig(e){for(const t of e)this.allowEmptyElement(t)}allowElement(e){for(const t of this._dataSchema.getDefinitionsForView(e,!0))this._addAllowedElement(t),this._coupledAttributes=null}disallowElement(e){for(const t of this._dataSchema.getDefinitionsForView(e,!1))this._disallowedElements.add(t.view)}allowEmptyElement(e){for(const t of this._dataSchema.getDefinitionsForView(e,!0))t.isInline&&this._dataSchema.extendInlineElement({...t,allowEmpty:!0})}allowAttributes(e){this._allowedAttributes.add(e)}disallowAttributes(e){this._disallowedAttributes.add(e)}processViewAttributes(e,t){const{consumable:i}=t;return qy(e,this._disallowedAttributes,i),function(e,{attributes:t,classes:i,styles:n}){if(!t.length&&!i.length&&!n.length)return null;return{...t.length&&{attributes:Gy(e,t)},...n.length&&{styles:Ky(e,n)},...i.length&&{classes:i}}}(e,qy(e,this._allowedAttributes,i))}_addAllowedElement(e){if(!this._allowedElements.has(e)){if(this._allowedElements.add(e),"appliesToBlock"in e&&"string"==typeof e.appliesToBlock)for(const t of this._dataSchema.getDefinitionsForModel(e.appliesToBlock))t.isBlock&&this._addAllowedElement(t);this._dataInitialized&&this.editor.data.once("set",(()=>{this._fireRegisterEvent(e)}),{priority:w.highest+1})}}_registerElementsAfterInit(){this.editor.data.on("init",(()=>{this._dataInitialized=!0;for(const e of this._allowedElements)this._fireRegisterEvent(e)}),{priority:w.highest+1})}_registerElementHandlers(){this.on("register",((e,t)=>{const i=this.editor.model.schema;if(t.isObject&&!i.isRegistered(t.model))this._registerObjectElement(t);else if(t.isBlock)this._registerBlockElement(t);else{if(!t.isInline)throw new y("data-filter-invalid-definition",null,t);this._registerInlineElement(t)}e.stop()}),{priority:"lowest"})}_registerCoupledAttributesPostFixer(){const e=this.editor.model,t=e.document.selection;e.document.registerPostFixer((t=>{const i=e.document.differ.getChanges();let n=!1;const s=this._getCoupledAttributesMap();for(const e of i){if("attribute"!=e.type||null!==e.attributeNewValue)continue;const i=s.get(e.attributeKey);if(i)for(const{item:s}of e.range.getWalker())for(const e of i)s.hasAttribute(e)&&(t.removeAttribute(e,s),n=!0)}return n})),this.listenTo(t,"change:attribute",((i,{attributeKeys:n})=>{const s=new Set,o=this._getCoupledAttributesMap();for(const e of n){if(t.hasAttribute(e))continue;const i=o.get(e);if(i)for(const e of i)t.hasAttribute(e)&&s.add(e)}0!=s.size&&e.change((e=>{for(const t of s)e.removeSelectionAttribute(t)}))}))}_registerAssociatedHtmlAttributesPostFixer(){const e=this.editor.model;e.document.registerPostFixer((t=>{const i=e.document.differ.getChanges();let n=!1;for(const s of i)if("insert"===s.type&&"$text"!==s.name)for(const i of s.attributes.keys())i.startsWith("html")&&i.endsWith("Attributes")&&(e.schema.checkAttribute(s.name,i)||(t.removeAttribute(i,s.position.nodeAfter),n=!0));return n}))}_getCoupledAttributesMap(){if(this._coupledAttributes)return this._coupledAttributes;this._coupledAttributes=new Map;for(const e of this._allowedElements)if(e.coupledAttribute&&e.model){const t=this._coupledAttributes.get(e.coupledAttribute);t?t.push(e.model):this._coupledAttributes.set(e.coupledAttribute,[e.model])}return this._coupledAttributes}_fireRegisterEvent(e){e.view&&this._disallowedElements.has(e.view)||this.fire(e.view?`register:${e.view}`:"register",e)}_registerObjectElement(e){const t=this.editor,i=t.model.schema,n=t.conversion,{view:s,model:o}=e;i.register(o,e.modelSchema),s&&(i.extend(e.model,{allowAttributes:[Ry(s),"htmlContent"]}),t.data.registerRawContentMatcher({name:s}),n.for("upcast").elementToElement({view:s,model:Oy(e),converterPriority:w.low+2}),n.for("upcast").add(Ly(e,this)),n.for("editingDowncast").elementToStructure({model:{name:o,attributes:[Ry(s)]},view:By(t,e)}),n.for("dataDowncast").elementToElement({model:o,view:(e,{writer:t})=>My(s,e,t)}),n.for("dataDowncast").add(zy(e)))}_registerBlockElement(e){const t=this.editor,i=t.model.schema,n=t.conversion,{view:s,model:o}=e;if(!i.isRegistered(e.model)){if(i.register(e.model,e.modelSchema),!s)return;n.for("upcast").elementToElement({model:o,view:s,converterPriority:w.low+2}),n.for("downcast").elementToElement({model:o,view:s})}s&&(i.extend(e.model,{allowAttributes:Ry(s)}),n.for("upcast").add(Ly(e,this)),n.for("downcast").add(zy(e)))}_registerInlineElement(e){const t=this.editor,i=t.model.schema,n=t.conversion,s=e.model;e.appliesToBlock||(i.extend("$text",{allowAttributes:s}),e.attributeProperties&&i.setAttributeProperties(s,e.attributeProperties),n.for("upcast").add(Ny(e,this)),n.for("downcast").attributeToElement({model:s,view:Dy(e)}),e.allowEmpty&&(i.setAttributeProperties(s,{copyFromObject:!1}),i.isRegistered("htmlEmptyElement")||i.register("htmlEmptyElement",{inheritAllFrom:"$inlineObject"}),t.data.htmlProcessor.domConverter.registerInlineObjectMatcher((t=>t.name==e.view&&t.isEmpty&&Array.from(t.getAttributeKeys()).length?{name:!0}:null)),n.for("editingDowncast").elementToElement({model:"htmlEmptyElement",view:Fy(e,!0)}),n.for("dataDowncast").elementToElement({model:"htmlEmptyElement",view:Fy(e)})))}}function qy(e,t,i){const n=t.matchAll(e)||[],s=e.document.stylesProcessor;return n.reduce(((t,{match:n})=>{for(const o of n.styles||[]){const n=s.getRelatedStyles(o).filter((e=>e.split("-").length>o.split("-").length)).sort(((e,t)=>t.split("-").length-e.split("-").length));for(const s of n)i.consume(e,{styles:[s]})&&t.styles.push(s);i.consume(e,{styles:[o]})&&t.styles.push(o)}for(const s of n.classes||[])i.consume(e,{classes:[s]})&&t.classes.push(s);for(const s of n.attributes||[])i.consume(e,{attributes:[s]})&&t.attributes.push(s);return t}),{attributes:[],classes:[],styles:[]})}function Gy(e,t){const i={};for(const n of t){const t=e.getAttribute(n);void 0!==t&&Xn(n)&&(i[n]=t)}return i}function Ky(e,t){const i=new nr(e.document.stylesProcessor);for(const n of t){const t=e.getStyle(n);void 0!==t&&i.set(n,t)}return Object.fromEntries(i.getStylesEntries())}function Zy(e,t){const{name:i}=e,n=e[t];return Te(n)?Object.entries(n).map((([e,n])=>({name:i,[t]:{[e]:n}}))):Array.isArray(n)?n.map((e=>({name:i,[t]:[e]}))):[e]}function Jy(e){const{name:t,attributes:i,classes:n,styles:s}=e,o=[];return i&&o.push(...Zy({name:t,attributes:i},"attributes")),n&&o.push(...Zy({name:t,classes:n},"classes")),s&&o.push(...Zy({name:t,styles:s},"styles")),o}class Qy extends so{static get requires(){return[Uy]}static get pluginName(){return"CodeBlockElementSupport"}init(){if(!this.editor.plugins.has("CodeBlockEditing"))return;const e=this.editor.plugins.get(Uy);e.on("register:pre",((t,i)=>{if("codeBlock"!==i.model)return;const n=this.editor,s=n.model.schema,o=n.conversion;s.extend("codeBlock",{allowAttributes:["htmlPreAttributes","htmlContentAttributes"]}),o.for("upcast").add(function(e){return t=>{t.on("element:code",((t,i,n)=>{const s=i.viewItem,o=s.parent;function r(t,s){const o=e.processViewAttributes(t,n);o&&n.writer.setAttribute(s,o,i.modelRange)}o&&o.is("element","pre")&&(r(o,"htmlPreAttributes"),r(s,"htmlContentAttributes"))}),{priority:"low"})}}(e)),o.for("downcast").add((e=>{e.on("attribute:htmlPreAttributes:codeBlock",((e,t,i)=>{if(!i.consumable.consume(t.item,e.name))return;const{attributeOldValue:n,attributeNewValue:s}=t,o=i.mapper.toViewElement(t.item).parent;Py(i.writer,n,s,o)})),e.on("attribute:htmlContentAttributes:codeBlock",((e,t,i)=>{if(!i.consumable.consume(t.item,e.name))return;const{attributeOldValue:n,attributeNewValue:s}=t,o=i.mapper.toViewElement(t.item);Py(i.writer,n,s,o)}))})),t.stop()}))}}class Yy extends so{static get requires(){return[Uy]}static get pluginName(){return"DualContentModelElementSupport"}init(){this.editor.plugins.get(Uy).on("register",((e,t)=>{const i=t,n=this.editor,s=n.model.schema,o=n.conversion;if(!i.paragraphLikeModel)return;if(s.isRegistered(i.model)||s.isRegistered(i.paragraphLikeModel))return;const r={model:i.paragraphLikeModel,view:i.view};s.register(i.model,i.modelSchema),s.register(r.model,{inheritAllFrom:"$block"}),o.for("upcast").elementToElement({view:i.view,model:(e,{writer:t})=>this._hasBlockContent(e)?t.createElement(i.model):t.createElement(r.model),converterPriority:w.low+.5}),o.for("downcast").elementToElement({view:i.view,model:i.model}),this._addAttributeConversion(i),o.for("downcast").elementToElement({view:r.view,model:r.model}),this._addAttributeConversion(r),e.stop()}))}_hasBlockContent(e){const t=this.editor.editing.view,i=t.domConverter.blockElements;for(const n of t.createRangeIn(e).getItems())if(n.is("element")&&i.includes(n.name))return!0;return!1}_addAttributeConversion(e){const t=this.editor,i=t.conversion,n=t.plugins.get(Uy);t.model.schema.extend(e.model,{allowAttributes:Ry(e.view)}),i.for("upcast").add(Ly(e,n)),i.for("downcast").add(zy(e))}}class Xy extends so{static get requires(){return[jy,Jb]}static get pluginName(){return"HeadingElementSupport"}init(){const e=this.editor;if(!e.plugins.has("HeadingEditing"))return;const t=e.config.get("heading.options");this.registerHeadingElements(e,t)}registerHeadingElements(e,t){const i=e.plugins.get(jy),n=[];for(const e of t)"model"in e&&"view"in e&&(i.registerBlockElement({view:e.view,model:e.model}),n.push(e.model));i.extendBlockElement({model:"htmlHgroup",modelSchema:{allowChildren:n}})}}function ek(e,t,i){const n=e.createRangeOn(t);for(const{item:e}of n.getWalker())if(e.is("element",i))return e}class tk extends so{static get requires(){return[Uy]}static get pluginName(){return"ImageElementSupport"}init(){const e=this.editor;if(!e.plugins.has("ImageInlineEditing")&&!e.plugins.has("ImageBlockEditing"))return;const t=e.model.schema,i=e.conversion,n=e.plugins.get(Uy);n.on("register:figure",(()=>{i.for("upcast").add(function(e){return t=>{t.on("element:figure",((t,i,n)=>{const s=i.viewItem;if(!i.modelRange||!s.hasClass("image"))return;const o=e.processViewAttributes(s,n);o&&n.writer.setAttribute("htmlFigureAttributes",o,i.modelRange)}),{priority:"low"})}}(n))})),n.on("register:img",((s,o)=>{"imageBlock"!==o.model&&"imageInline"!==o.model||(t.isRegistered("imageBlock")&&t.extend("imageBlock",{allowAttributes:["htmlImgAttributes","htmlFigureAttributes","htmlLinkAttributes"]}),t.isRegistered("imageInline")&&t.extend("imageInline",{allowAttributes:["htmlA","htmlImgAttributes"]}),i.for("upcast").add(function(e){return t=>{t.on("element:img",((t,i,n)=>{if(!i.modelRange)return;const s=i.viewItem,o=e.processViewAttributes(s,n);o&&n.writer.setAttribute("htmlImgAttributes",o,i.modelRange)}),{priority:"low"})}}(n)),i.for("downcast").add((e=>{function t(t){e.on(`attribute:${t}:imageInline`,((e,t,i)=>{if(!i.consumable.consume(t.item,e.name))return;const{attributeOldValue:n,attributeNewValue:s}=t,o=i.mapper.toViewElement(t.item);Py(i.writer,n,s,o)}),{priority:"low"})}function i(t,i){e.on(`attribute:${i}:imageBlock`,((e,i,n)=>{if(!n.consumable.test(i.item,e.name))return;const{attributeOldValue:s,attributeNewValue:o}=i,r=n.mapper.toViewElement(i.item),a=ek(n.writer,r,t);a&&(Py(n.writer,s,o,a),n.consumable.consume(i.item,e.name))}),{priority:"low"}),"a"===t&&e.on("attribute:linkHref:imageBlock",((e,t,i)=>{if(!i.consumable.consume(t.item,"attribute:htmlLinkAttributes:imageBlock"))return;const n=i.mapper.toViewElement(t.item),s=ek(i.writer,n,"a");Iy(i.writer,t.item.getAttribute("htmlLinkAttributes"),s)}),{priority:"low"})}t("htmlImgAttributes"),i("img","htmlImgAttributes"),i("figure","htmlFigureAttributes"),i("a","htmlLinkAttributes")})),e.plugins.has("LinkImage")&&i.for("upcast").add(function(e,t){const i=t.plugins.get("ImageUtils");return t=>{t.on("element:a",((t,n,s)=>{const o=n.viewItem;if(!i.findViewImgElement(o))return;const r=n.modelCursor.parent;if(!r.is("element","imageBlock"))return;const a=e.processViewAttributes(o,s);a&&s.writer.setAttribute("htmlLinkAttributes",a,r)}),{priority:"low"})}}(n,e)),s.stop())}))}}class ik extends so{static get requires(){return[Uy]}static get pluginName(){return"MediaEmbedElementSupport"}init(){const e=this.editor;if(!e.plugins.has("MediaEmbed")||e.config.get("mediaEmbed.previewsInData"))return;const t=e.model.schema,i=e.conversion,n=this.editor.plugins.get(Uy),s=this.editor.plugins.get(jy),o=e.config.get("mediaEmbed.elementName");s.registerBlockElement({model:"media",view:o}),n.on("register:figure",(()=>{i.for("upcast").add(function(e){return t=>{t.on("element:figure",((t,i,n)=>{const s=i.viewItem;if(!i.modelRange||!s.hasClass("media"))return;const o=e.processViewAttributes(s,n);o&&n.writer.setAttribute("htmlFigureAttributes",o,i.modelRange)}),{priority:"low"})}}(n))})),n.on(`register:${o}`,((e,s)=>{"media"===s.model&&(t.extend("media",{allowAttributes:[Ry(o),"htmlFigureAttributes"]}),i.for("upcast").add(function(e,t){const i=(i,n,s)=>{function o(t,i){const o=e.processViewAttributes(t,s);o&&s.writer.setAttribute(i,o,n.modelRange)}o(n.viewItem,Ry(t))};return e=>{e.on(`element:${t}`,i,{priority:"low"})}}(n,o)),i.for("dataDowncast").add(function(e){return t=>{function i(e,i){t.on(`attribute:${i}:media`,((t,i,n)=>{if(!n.consumable.consume(i.item,t.name))return;const{attributeOldValue:s,attributeNewValue:o}=i,r=n.mapper.toViewElement(i.item),a=ek(n.writer,r,e);Py(n.writer,s,o,a)}))}i(e,Ry(e)),i("figure","htmlFigureAttributes")}}(o)),e.stop())}))}}class nk extends so{static get requires(){return[Uy]}static get pluginName(){return"ScriptElementSupport"}init(){const e=this.editor.plugins.get(Uy);e.on("register:script",((t,i)=>{const n=this.editor,s=n.model.schema,o=n.conversion;s.register("htmlScript",i.modelSchema),s.extend("htmlScript",{allowAttributes:["htmlScriptAttributes","htmlContent"],isContent:!0}),n.data.registerRawContentMatcher({name:"script"}),o.for("upcast").elementToElement({view:"script",model:Oy(i)}),o.for("upcast").add(Ly(i,e)),o.for("downcast").elementToElement({model:"htmlScript",view:(e,{writer:t})=>My("script",e,t)}),o.for("downcast").add(zy(i)),t.stop()}))}}class sk extends so{static get requires(){return[Uy]}static get pluginName(){return"TableElementSupport"}init(){const e=this.editor;if(!e.plugins.has("TableEditing"))return;const t=e.model.schema,i=e.conversion,n=e.plugins.get(Uy),s=e.plugins.get("TableUtils");n.on("register:figure",(()=>{i.for("upcast").add(function(e){return t=>{t.on("element:figure",((t,i,n)=>{const s=i.viewItem;if(!i.modelRange||!s.hasClass("table"))return;const o=e.processViewAttributes(s,n);o&&n.writer.setAttribute("htmlFigureAttributes",o,i.modelRange)}),{priority:"low"})}}(n))})),n.on("register:table",((o,r)=>{"table"===r.model&&(t.extend("table",{allowAttributes:["htmlTableAttributes","htmlFigureAttributes","htmlTheadAttributes","htmlTbodyAttributes"]}),i.for("upcast").add(function(e){return t=>{t.on("element:table",((t,i,n)=>{if(!i.modelRange)return;const s=i.viewItem;o(s,"htmlTableAttributes");for(const e of s.getChildren())e.is("element","thead")&&o(e,"htmlTheadAttributes"),e.is("element","tbody")&&o(e,"htmlTbodyAttributes");function o(t,s){const o=e.processViewAttributes(t,n);o&&n.writer.setAttribute(s,o,i.modelRange)}}),{priority:"low"})}}(n)),i.for("downcast").add((e=>{function t(t,i){e.on(`attribute:${i}:table`,((e,i,n)=>{if(!n.consumable.test(i.item,e.name))return;const s=n.mapper.toViewElement(i.item),o=ek(n.writer,s,t);o&&(n.consumable.consume(i.item,e.name),Py(n.writer,i.attributeOldValue,i.attributeNewValue,o))}))}t("table","htmlTableAttributes"),t("figure","htmlFigureAttributes"),t("thead","htmlTheadAttributes"),t("tbody","htmlTbodyAttributes")})),e.model.document.registerPostFixer(function(e,t){return i=>{const n=e.document.differ.getChanges();let s=!1;for(const e of n){if("attribute"!=e.type||"headingRows"!=e.attributeKey)continue;const n=e.range.start.nodeAfter,o=n.getAttribute("htmlTheadAttributes"),r=n.getAttribute("htmlTbodyAttributes");o&&!e.attributeNewValue?(i.removeAttribute("htmlTheadAttributes",n),s=!0):r&&e.attributeNewValue==t.getRows(n)&&(i.removeAttribute("htmlTbodyAttributes",n),s=!0)}return s}}(e.model,s)),o.stop())}))}}class ok extends so{static get requires(){return[Uy]}static get pluginName(){return"StyleElementSupport"}init(){const e=this.editor.plugins.get(Uy);e.on("register:style",((t,i)=>{const n=this.editor,s=n.model.schema,o=n.conversion;s.register("htmlStyle",i.modelSchema),s.extend("htmlStyle",{allowAttributes:["htmlStyleAttributes","htmlContent"],isContent:!0}),n.data.registerRawContentMatcher({name:"style"}),o.for("upcast").elementToElement({view:"style",model:Oy(i)}),o.for("upcast").add(Ly(i,e)),o.for("downcast").elementToElement({model:"htmlStyle",view:(e,{writer:t})=>My("style",e,t)}),o.for("downcast").add(zy(i)),t.stop()}))}}class rk extends so{static get requires(){return[Uy]}static get pluginName(){return"ListElementSupport"}init(){const e=this.editor;if(!e.plugins.has("ListEditing"))return;const t=e.model.schema,i=e.conversion,n=e.plugins.get(Uy),s=e.plugins.get("ListEditing"),o=e.plugins.get("ListUtils"),r=["ul","ol","li"];s.registerDowncastStrategy({scope:"item",attributeName:"htmlLiAttributes",setAttributeOnDowncast:Iy}),s.registerDowncastStrategy({scope:"list",attributeName:"htmlUlAttributes",setAttributeOnDowncast:Iy}),s.registerDowncastStrategy({scope:"list",attributeName:"htmlOlAttributes",setAttributeOnDowncast:Iy}),n.on("register",((e,s)=>{if(!r.includes(s.view))return;if(e.stop(),t.checkAttribute("$block","htmlLiAttributes"))return;const o=r.map((e=>Ry(e)));t.extend("$listItem",{allowAttributes:o}),i.for("upcast").add((e=>{e.on("element:ul",ak("htmlUlAttributes",n),{priority:"low"}),e.on("element:ol",ak("htmlOlAttributes",n),{priority:"low"}),e.on("element:li",ak("htmlLiAttributes",n),{priority:"low"})}))})),s.on("postFixer",((e,{listNodes:t,writer:i})=>{for(const{node:n,previousNodeInList:s}of t)if(s){if(s.getAttribute("listType")==n.getAttribute("listType")){const t=lk(s.getAttribute("listType")),o=s.getAttribute(t);!gd(n.getAttribute(t),o)&&i.model.schema.checkAttribute(n,t)&&(i.setAttribute(t,o,n),e.return=!0)}if(s.getAttribute("listItemId")==n.getAttribute("listItemId")){const t=s.getAttribute("htmlLiAttributes");!gd(n.getAttribute("htmlLiAttributes"),t)&&i.model.schema.checkAttribute(n,"htmlLiAttributes")&&(i.setAttribute("htmlLiAttributes",t,n),e.return=!0)}}})),s.on("postFixer",((e,{listNodes:t,writer:i})=>{for(const{node:n}of t){const t=n.getAttribute("listType");!o.isNumberedListType(t)&&n.getAttribute("htmlOlAttributes")&&(i.removeAttribute("htmlOlAttributes",n),e.return=!0),o.isNumberedListType(t)&&n.getAttribute("htmlUlAttributes")&&(i.removeAttribute("htmlUlAttributes",n),e.return=!0)}}))}afterInit(){const e=this.editor;if(!e.commands.get("indentList"))return;const t=e.commands.get("indentList");this.listenTo(t,"afterExecute",((t,i)=>{e.model.change((t=>{for(const n of i){const i=lk(n.getAttribute("listType"));e.model.schema.checkAttribute(n,i)&&t.setAttribute(i,{},n)}}))}))}}function ak(e,t){return(i,n,s)=>{const o=n.viewItem;n.modelRange||Object.assign(n,s.convertChildren(n.viewItem,n.modelCursor));const r=t.processViewAttributes(o,s);for(const t of n.modelRange.getItems({shallow:!0}))t.hasAttribute("listItemId")&&(t.hasAttribute(e)||s.writer.model.schema.checkAttribute(t,e)&&s.writer.setAttribute(e,r||{},t))}}function lk(e){return"numbered"===e||"customNumbered"==e?"htmlOlAttributes":"htmlUlAttributes"}class ck extends so{static get requires(){return[Uy,jy]}static get pluginName(){return"CustomElementSupport"}init(){const e=this.editor.plugins.get(Uy),t=this.editor.plugins.get(jy);e.on("register:$customElement",((i,n)=>{i.stop();const s=this.editor,o=s.model.schema,r=s.conversion,a=s.editing.view.domConverter.unsafeElements,l=s.data.htmlProcessor.domConverter.preElements;o.register(n.model,n.modelSchema),o.extend(n.model,{allowAttributes:["htmlElementName","htmlCustomElementAttributes","htmlContent"],isContent:!0}),s.data.htmlProcessor.domConverter.registerRawContentMatcher({name:"template"}),r.for("upcast").elementToElement({view:/.*/,model:(i,o)=>{if("$comment"==i.name)return null;if(!function(e){try{document.createElement(e)}catch(e){return!1}return!0}(i.name))return null;if(t.getDefinitionsForView(i.name).size)return null;a.includes(i.name)||a.push(i.name),l.includes(i.name)||l.push(i.name);const r=o.writer.createElement(n.model,{htmlElementName:i.name}),c=e.processViewAttributes(i,o);let d;if(c&&o.writer.setAttribute("htmlCustomElementAttributes",c,r),i.is("element","template")&&i.getCustomProperty("$rawContent"))d=i.getCustomProperty("$rawContent");else{const e=new _h(i.document).createDocumentFragment(i),t=s.data.htmlProcessor.domConverter.viewToDom(e),n=t.firstChild;for(;n.firstChild;)t.appendChild(n.firstChild);n.remove(),d=s.data.htmlProcessor.htmlWriter.getHtml(t)}o.writer.setAttribute("htmlContent",d,r);for(const{item:e}of s.editing.view.createRangeIn(i))o.consumable.consume(e,{name:!0});return r},converterPriority:"low"}),r.for("editingDowncast").elementToElement({model:{name:n.model,attributes:["htmlElementName","htmlCustomElementAttributes","htmlContent"]},view:(e,{writer:t})=>{const i=e.getAttribute("htmlElementName"),n=t.createRawElement(i);return e.hasAttribute("htmlCustomElementAttributes")&&Iy(t,e.getAttribute("htmlCustomElementAttributes"),n),n}}),r.for("dataDowncast").elementToElement({model:{name:n.model,attributes:["htmlElementName","htmlCustomElementAttributes","htmlContent"]},view:(e,{writer:t})=>{const i=e.getAttribute("htmlElementName"),n=e.getAttribute("htmlContent"),s=t.createRawElement(i,null,((e,t)=>{t.setContentOf(e,n)}));return e.hasAttribute("htmlCustomElementAttributes")&&Iy(t,e.getAttribute("htmlCustomElementAttributes"),s),s}})}))}}function*dk(e,t,i){if(t)if(!(Symbol.iterator in t)&&t.is("documentSelection")&&t.isCollapsed)e.schema.checkAttributeInSelection(t,i)&&(yield t);else for(const n of function(e,t,i){return!(Symbol.iterator in t)&&(t.is("node")||t.is("$text")||t.is("$textProxy"))?e.schema.checkAttribute(t,i)?[e.createRangeOn(t)]:[]:e.schema.getValidRanges(e.createSelection(t).getRanges(),i)}(e,t,i))yield*n.getItems({shallow:!0})}function hk(e){return e.createContainerElement("figure",{class:"image"},[e.createEmptyElement("img"),e.createSlot("children")])}function uk(e,t){const i=e.plugins.get("ImageUtils"),n=e.plugins.has("ImageInlineEditing")&&e.plugins.has("ImageBlockEditing");return e=>{if(!i.isInlineImageView(e))return null;if(!n)return s(e);return("block"==e.getStyle("display")||e.findAncestor(i.isBlockImageView)?"imageBlock":"imageInline")!==t?null:s(e)};function s(e){const t={name:!0};return e.hasAttribute("src")&&(t.attributes=["src"]),t}}function mk(e,t){const i=Zs(t.getSelectedBlocks());return!i||e.isObject(i)||i.isEmpty&&"listItem"!=i.name?"imageBlock":"imageInline"}function gk(e){return e&&e.endsWith("px")?parseInt(e):null}function fk(e){const t=gk(e.getStyle("width")),i=gk(e.getStyle("height"));return!(!t||!i)}const pk=/^(image|image-inline)$/;class bk extends so{constructor(){super(...arguments),this._domEmitter=new(Vn())}static get pluginName(){return"ImageUtils"}isImage(e){return this.isInlineImage(e)||this.isBlockImage(e)}isInlineImageView(e){return!!e&&e.is("element","img")}isBlockImageView(e){return!!e&&e.is("element","figure")&&e.hasClass("image")}insertImage(e={},t=null,i=null,n={}){const s=this.editor,o=s.model,r=o.document.selection,a=wk(s,t||r,i);e={...Object.fromEntries(r.getAttributes()),...e};for(const t in e)o.schema.checkAttribute(a,t)||delete e[t];return o.change((i=>{const{setImageSizes:s=!0}=n,r=i.createElement(a,e);return o.insertObject(r,t,null,{setSelection:"on",findOptimalPosition:t||"imageInline"==a?void 0:"auto"}),r.parent?(s&&this.setImageNaturalSizeAttributes(r),r):null}))}setImageNaturalSizeAttributes(e){const t=e.getAttribute("src");t&&(e.getAttribute("width")||e.getAttribute("height")||this.editor.model.change((i=>{const n=new Mn.window.Image;this._domEmitter.listenTo(n,"load",(()=>{e.getAttribute("width")||e.getAttribute("height")||this.editor.model.enqueueChange(i.batch,(t=>{t.setAttribute("width",n.naturalWidth,e),t.setAttribute("height",n.naturalHeight,e)})),this._domEmitter.stopListening(n,"load")})),n.src=t})))}getClosestSelectedImageWidget(e){const t=e.getFirstPosition();if(!t)return null;const i=e.getSelectedElement();if(i&&this.isImageWidget(i))return i;let n=t.parent;for(;n;){if(n.is("element")&&this.isImageWidget(n))return n;n=n.parent}return null}getClosestSelectedImageElement(e){const t=e.getSelectedElement();return this.isImage(t)?t:e.getFirstPosition().findAncestor("imageBlock")}getImageWidgetFromImageView(e){return e.findAncestor({classes:pk})}isImageAllowed(){const e=this.editor.model.document.selection;return function(e,t){const i=wk(e,t,null);if("imageBlock"==i){const i=function(e,t){const i=$w(e,t),n=i.start.parent;if(n.isEmpty&&!n.is("element","$root"))return n.parent;return n}(t,e.model);if(e.model.schema.checkChild(i,"imageBlock"))return!0}else if(e.model.schema.checkChild(t.focus,"imageInline"))return!0;return!1}(this.editor,e)&&function(e){return[...e.focus.getAncestors()].every((e=>!e.is("element","imageBlock")))}(e)}toImageWidget(e,t,i){t.setCustomProperty("image",!0,e);return Fw(e,t,{label:()=>{const t=this.findViewImgElement(e).getAttribute("alt");return t?`${t} ${i}`:i}})}isImageWidget(e){return!!e.getCustomProperty("image")&&Nw(e)}isBlockImage(e){return!!e&&e.is("element","imageBlock")}isInlineImage(e){return!!e&&e.is("element","imageInline")}findViewImgElement(e){if(this.isInlineImageView(e))return e;const t=this.editor.editing.view;for(const{item:i}of t.createRangeIn(e))if(this.isInlineImageView(i))return i}destroy(){return this._domEmitter.stopListening(),super.destroy()}}function wk(e,t,i){const n=e.model.schema,s=e.config.get("image.insert.type");return e.plugins.has("ImageBlockEditing")?e.plugins.has("ImageInlineEditing")?i||("inline"===s?"imageInline":"auto"!==s?"imageBlock":t.is("selection")?mk(n,t):n.checkChild(t,"imageInline")?"imageInline":"imageBlock"):"imageBlock":"imageInline"}const vk=new RegExp(String(/^(http(s)?:\/\/)?[\w-]+\.[\w.~:/[\]@!$&'()*+,;=%-]+/.source+/\.(jpg|jpeg|png|gif|ico|webp|JPG|JPEG|PNG|GIF|ICO|WEBP)/.source+/(\?[\w.~:/[\]@!$&'()*+,;=%-]*)?/.source+/(#[\w.~:/[\]@!$&'()*+,;=%-]*)?$/.source));class _k extends ro{refresh(){const e=this.editor.plugins.get("ImageUtils").getClosestSelectedImageElement(this.editor.model.document.selection);this.isEnabled=!!e,this.isEnabled&&e.hasAttribute("alt")?this.value=e.getAttribute("alt"):this.value=!1}execute(e){const t=this.editor,i=t.plugins.get("ImageUtils"),n=t.model,s=i.getClosestSelectedImageElement(n.document.selection);n.change((t=>{t.setAttribute("alt",e.newValue,s)}))}}class yk extends so{static get requires(){return[bk]}static get pluginName(){return"ImageTextAlternativeEditing"}init(){this.editor.commands.add("imageTextAlternative",new _k(this.editor))}}class kk extends Du{constructor(e){super(e);const t=this.locale.t;this.focusTracker=new Js,this.keystrokes=new Qs,this.labeledInput=this._createLabeledInputView(),this.saveButtonView=this._createButton(t("Save"),fu.check,"ck-button-save"),this.saveButtonView.type="submit",this.cancelButtonView=this._createButton(t("Cancel"),fu.cancel,"ck-button-cancel","cancel"),this._focusables=new pu,this._focusCycler=new ym({focusables:this._focusables,focusTracker:this.focusTracker,keystrokeHandler:this.keystrokes,actions:{focusPrevious:"shift + tab",focusNext:"tab"}}),this.setTemplate({tag:"form",attributes:{class:["ck","ck-text-alternative-form","ck-responsive-form"],tabindex:"-1"},children:[this.labeledInput,this.saveButtonView,this.cancelButtonView]})}render(){super.render(),this.keystrokes.listenTo(this.element),i({view:this}),[this.labeledInput,this.saveButtonView,this.cancelButtonView].forEach((e=>{this._focusables.add(e),this.focusTracker.add(e.element)}))}destroy(){super.destroy(),this.focusTracker.destroy(),this.keystrokes.destroy()}_createButton(e,t,i,n){const s=new Ku(this.locale);return s.set({label:e,icon:t,tooltip:!0}),s.extendTemplate({attributes:{class:i}}),n&&s.delegate("execute").to(this,n),s}_createLabeledInputView(){const e=this.locale.t,t=new um(this.locale,Um);return t.label=e("Text alternative"),t}}function Ck(e){const t=e.editing.view,i=ef.defaultPositions,n=e.plugins.get("ImageUtils");return{target:t.domConverter.mapViewToDom(n.getClosestSelectedImageWidget(t.document.selection)),positions:[i.northArrowSouth,i.northArrowSouthWest,i.northArrowSouthEast,i.southArrowNorth,i.southArrowNorthWest,i.southArrowNorthEast,i.viewportStickyNorth]}}class Ak extends so{static get requires(){return[If]}static get pluginName(){return"ImageTextAlternativeUI"}init(){this._createButton()}destroy(){super.destroy(),this._form&&this._form.destroy()}_createButton(){const e=this.editor,t=e.t;e.ui.componentFactory.add("imageTextAlternative",(i=>{const n=e.commands.get("imageTextAlternative"),s=new Ku(i);return s.set({label:t("Change image text alternative"),icon:fu.textAlternative,tooltip:!0}),s.bind("isEnabled").to(n,"isEnabled"),s.bind("isOn").to(n,"value",(e=>!!e)),this.listenTo(s,"execute",(()=>{this._showForm()})),s}))}_createForm(){const i=this.editor,n=i.editing.view.document,s=i.plugins.get("ImageUtils");this._balloon=this.editor.plugins.get("ContextualBalloon"),this._form=new(t(kk))(i.locale),this._form.render(),this.listenTo(this._form,"submit",(()=>{i.execute("imageTextAlternative",{newValue:this._form.labeledInput.fieldView.element.value}),this._hideForm(!0)})),this.listenTo(this._form,"cancel",(()=>{this._hideForm(!0)})),this._form.keystrokes.set("Esc",((e,t)=>{this._hideForm(!0),t()})),this.listenTo(i.ui,"update",(()=>{s.getClosestSelectedImageWidget(n.selection)?this._isVisible&&function(e){const t=e.plugins.get("ContextualBalloon");if(e.plugins.get("ImageUtils").getClosestSelectedImageWidget(e.editing.view.document.selection)){const i=Ck(e);t.updatePosition(i)}}(i):this._hideForm(!0)})),e({emitter:this._form,activator:()=>this._isVisible,contextElements:()=>[this._balloon.view.element],callback:()=>this._hideForm()})}_showForm(){if(this._isVisible)return;this._form||this._createForm();const e=this.editor,t=e.commands.get("imageTextAlternative"),i=this._form.labeledInput;this._form.disableCssTransitions(),this._isInBalloon||this._balloon.add({view:this._form,position:Ck(e)}),i.fieldView.value=i.fieldView.element.value=t.value||"",this._form.labeledInput.fieldView.select(),this._form.enableCssTransitions()}_hideForm(e=!1){this._isInBalloon&&(this._form.focusTracker.isFocused&&this._form.saveButtonView.focus(),this._balloon.remove(this._form),e&&this.editor.editing.view.focus())}get _isVisible(){return!!this._balloon&&this._balloon.visibleView===this._form}get _isInBalloon(){return!!this._balloon&&this._balloon.hasView(this._form)}}class xk extends so{static get requires(){return[yk,Ak]}static get pluginName(){return"ImageTextAlternative"}}function Ek(e,t){const i=(t,i,n)=>{if(!n.consumable.consume(i.item,t.name))return;const s=n.writer,o=n.mapper.toViewElement(i.item),r=e.findViewImgElement(o);null===i.attributeNewValue?(s.removeAttribute("srcset",r),s.removeAttribute("sizes",r)):i.attributeNewValue&&(s.setAttribute("srcset",i.attributeNewValue,r),s.setAttribute("sizes","100vw",r))};return e=>{e.on(`attribute:srcset:${t}`,i)}}function Tk(e,t,i){const n=(t,i,n)=>{if(!n.consumable.consume(i.item,t.name))return;const s=n.writer,o=n.mapper.toViewElement(i.item),r=e.findViewImgElement(o);s.setAttribute(i.attributeKey,i.attributeNewValue||"",r)};return e=>{e.on(`attribute:${i}:${t}`,n)}}class Sk extends ka{observe(e){this.listenTo(e,"load",((e,t)=>{const i=t.target;this.checkShouldIgnoreEventFromTarget(i)||"IMG"==i.tagName&&this._fireEvents(t)}),{useCapture:!0})}stopObserving(e){this.stopListening(e)}_fireEvents(e){this.isEnabled&&(this.document.fire("layoutChanged"),this.document.fire("imageLoaded",e))}}class Pk extends ro{constructor(e){super(e);const t=e.config.get("image.insert.type");e.plugins.has("ImageBlockEditing")||"block"===t&&k("image-block-plugin-required"),e.plugins.has("ImageInlineEditing")||"inline"===t&&k("image-inline-plugin-required")}refresh(){const e=this.editor.plugins.get("ImageUtils");this.isEnabled=e.isImageAllowed()}execute(e){const t=Cs(e.source),i=this.editor.model.document.selection,n=this.editor.plugins.get("ImageUtils"),s=Object.fromEntries(i.getAttributes());t.forEach(((e,t)=>{const o=i.getSelectedElement();if("string"==typeof e&&(e={src:e}),t&&o&&n.isImage(o)){const t=this.editor.model.createPositionAfter(o);n.insertImage({...e,...s},t)}else n.insertImage({...e,...s})}))}}class Ik extends ro{constructor(e){super(e),this.decorate("cleanupImage")}refresh(){const e=this.editor.plugins.get("ImageUtils"),t=this.editor.model.document.selection.getSelectedElement();this.isEnabled=e.isImage(t),this.value=this.isEnabled?t.getAttribute("src"):null}execute(e){const t=this.editor.model.document.selection.getSelectedElement(),i=this.editor.plugins.get("ImageUtils");this.editor.model.change((n=>{n.setAttribute("src",e.source,t),this.cleanupImage(n,t),i.setImageNaturalSizeAttributes(t)}))}cleanupImage(e,t){e.removeAttribute("srcset",t),e.removeAttribute("sizes",t),e.removeAttribute("sources",t),e.removeAttribute("width",t),e.removeAttribute("height",t),e.removeAttribute("alt",t)}}class Vk extends so{static get requires(){return[bk]}static get pluginName(){return"ImageEditing"}init(){const e=this.editor,t=e.conversion;e.editing.view.addObserver(Sk),t.for("upcast").attributeToAttribute({view:{name:"img",key:"alt"},model:"alt"}).attributeToAttribute({view:{name:"img",key:"srcset"},model:"srcset"});const i=new Pk(e),n=new Ik(e);e.commands.add("insertImage",i),e.commands.add("replaceImageSource",n),e.commands.add("imageInsert",i)}}class Rk extends so{static get requires(){return[bk]}static get pluginName(){return"ImageSizeAttributes"}afterInit(){this._registerSchema(),this._registerConverters("imageBlock"),this._registerConverters("imageInline")}_registerSchema(){this.editor.plugins.has("ImageBlockEditing")&&this.editor.model.schema.extend("imageBlock",{allowAttributes:["width","height"]}),this.editor.plugins.has("ImageInlineEditing")&&this.editor.model.schema.extend("imageInline",{allowAttributes:["width","height"]})}_registerConverters(e){const t=this.editor,i=t.plugins.get("ImageUtils"),n="imageBlock"===e?"figure":"img";function s(t,n,s,o){t.on(`attribute:${n}:${e}`,((t,n,r)=>{if(!r.consumable.consume(n.item,t.name))return;const a=r.writer,l=r.mapper.toViewElement(n.item),c=i.findViewImgElement(l);if(null!==n.attributeNewValue?a.setAttribute(s,n.attributeNewValue,c):a.removeAttribute(s,c),n.item.hasAttribute("sources"))return;const d=n.item.hasAttribute("resizedWidth");if("imageInline"===e&&!d&&!o)return;const h=n.item.getAttribute("width"),u=n.item.getAttribute("height");h&&u&&a.setStyle("aspect-ratio",`${h}/${u}`,c)}))}t.conversion.for("upcast").attributeToAttribute({view:{name:n,styles:{width:/.+/}},model:{key:"width",value:e=>fk(e)?gk(e.getStyle("width")):null}}).attributeToAttribute({view:{name:n,key:"width"},model:"width"}).attributeToAttribute({view:{name:n,styles:{height:/.+/}},model:{key:"height",value:e=>fk(e)?gk(e.getStyle("height")):null}}).attributeToAttribute({view:{name:n,key:"height"},model:"height"}),t.conversion.for("editingDowncast").add((e=>{s(e,"width","width",!0),s(e,"height","height",!0)})),t.conversion.for("dataDowncast").add((e=>{s(e,"width","width",!1),s(e,"height","height",!1)}))}}class Ok extends ro{constructor(e,t){super(e),this._modelElementName=t}refresh(){const e=this.editor.plugins.get("ImageUtils"),t=e.getClosestSelectedImageElement(this.editor.model.document.selection);"imageBlock"===this._modelElementName?this.isEnabled=e.isInlineImage(t):this.isEnabled=e.isBlockImage(t)}execute(e={}){const t=this.editor,i=this.editor.model,n=t.plugins.get("ImageUtils"),s=n.getClosestSelectedImageElement(i.document.selection),o=Object.fromEntries(s.getAttributes());return o.src||o.uploadId?i.change((t=>{const{setImageSizes:r=!0}=e,a=Array.from(i.markers).filter((e=>e.getRange().containsItem(s))),l=n.insertImage(o,i.createSelection(s,"on"),this._modelElementName,{setImageSizes:r});if(!l)return null;const c=t.createRangeOn(l);for(const e of a){const i=e.getRange(),n="$graveyard"!=i.root.rootName?i.getJoined(c,!0):c;t.updateMarker(e,{range:n})}return{oldElement:s,newElement:l}})):null}}class Bk extends so{static get requires(){return[bk]}static get pluginName(){return"ImagePlaceholder"}afterInit(){this._setupSchema(),this._setupConversion(),this._setupLoadListener()}_setupSchema(){const e=this.editor.model.schema;e.isRegistered("imageBlock")&&e.extend("imageBlock",{allowAttributes:["placeholder"]}),e.isRegistered("imageInline")&&e.extend("imageInline",{allowAttributes:["placeholder"]})}_setupConversion(){const e=this.editor,t=e.conversion,i=e.plugins.get("ImageUtils");t.for("editingDowncast").add((e=>{e.on("attribute:placeholder",((e,t,n)=>{if(!n.consumable.test(t.item,e.name))return;if(!t.item.is("element","imageBlock")&&!t.item.is("element","imageInline"))return;n.consumable.consume(t.item,e.name);const s=n.writer,o=n.mapper.toViewElement(t.item),r=i.findViewImgElement(o);t.attributeNewValue?(s.addClass("image_placeholder",r),s.setStyle("background-image",`url(${t.attributeNewValue})`,r),s.setCustomProperty("editingPipeline:doNotReuseOnce",!0,r)):(s.removeClass("image_placeholder",r),s.removeStyle("background-image",r))}))}))}_setupLoadListener(){const e=this.editor,t=e.model,i=e.editing,n=i.view,s=e.plugins.get("ImageUtils");n.addObserver(Sk),this.listenTo(n.document,"imageLoaded",((e,o)=>{const r=n.domConverter.mapDomToView(o.target);if(!r)return;const a=s.getImageWidgetFromImageView(r);if(!a)return;const l=i.mapper.toModelElement(a);l&&l.hasAttribute("placeholder")&&t.enqueueChange({isUndoable:!1},(e=>{e.removeAttribute("placeholder",l)}))}))}}class Mk extends so{static get requires(){return[Vk,Rk,bk,Bk,Sw]}static get pluginName(){return"ImageBlockEditing"}init(){const e=this.editor;e.model.schema.register("imageBlock",{inheritAllFrom:"$blockObject",allowAttributes:["alt","src","srcset"]}),this._setupConversion(),e.plugins.has("ImageInlineEditing")&&(e.commands.add("imageTypeBlock",new Ok(this.editor,"imageBlock")),this._setupClipboardIntegration())}_setupConversion(){const e=this.editor,t=e.t,i=e.conversion,n=e.plugins.get("ImageUtils");i.for("dataDowncast").elementToStructure({model:"imageBlock",view:(e,{writer:t})=>hk(t)}),i.for("editingDowncast").elementToStructure({model:"imageBlock",view:(e,{writer:i})=>n.toImageWidget(hk(i),i,t("image widget"))}),i.for("downcast").add(Tk(n,"imageBlock","src")).add(Tk(n,"imageBlock","alt")).add(Ek(n,"imageBlock")),i.for("upcast").elementToElement({view:uk(e,"imageBlock"),model:(e,{writer:t})=>t.createElement("imageBlock",e.hasAttribute("src")?{src:e.getAttribute("src")}:void 0)}).add(function(e){const t=(t,i,n)=>{if(!n.consumable.test(i.viewItem,{name:!0,classes:"image"}))return;const s=e.findViewImgElement(i.viewItem);if(!s||!n.consumable.test(s,{name:!0}))return;n.consumable.consume(i.viewItem,{name:!0,classes:"image"});const o=Zs(n.convertItem(s,i.modelCursor).modelRange.getItems());o?(n.convertChildren(i.viewItem,o),n.updateConversionResult(o,i)):n.consumable.revert(i.viewItem,{name:!0,classes:"image"})};return e=>{e.on("element:figure",t)}}(n))}_setupClipboardIntegration(){const e=this.editor,t=e.model,i=e.editing.view,n=e.plugins.get("ImageUtils"),s=e.plugins.get("ClipboardPipeline");this.listenTo(s,"inputTransformation",((s,o)=>{const r=Array.from(o.content.getChildren());let a;if(!r.every(n.isInlineImageView))return;a=o.targetRanges?e.editing.mapper.toModelRange(o.targetRanges[0]):t.document.selection.getFirstRange();const l=t.createSelection(a);if("imageBlock"===mk(t.schema,l)){const e=new _h(i.document),t=r.map((t=>e.createElement("figure",{class:"image"},t)));o.content=e.createDocumentFragment(t)}})),this.listenTo(s,"contentInsertion",((e,i)=>{"paste"===i.method&&t.change((e=>{const t=e.createRangeIn(i.content);for(const e of t.getItems())e.is("element","imageBlock")&&n.setImageNaturalSizeAttributes(e)}))}))}}class Nk extends Du{constructor(e,t=[]){super(e),this.focusTracker=new Js,this.keystrokes=new Qs,this._focusables=new pu,this.children=this.createCollection(),this._focusCycler=new ym({focusables:this._focusables,focusTracker:this.focusTracker,keystrokeHandler:this.keystrokes,actions:{focusPrevious:"shift + tab",focusNext:"tab"}});for(const e of t)this.children.add(e),this._focusables.add(e),e instanceof Xu&&this._focusables.addMany(e.children);if(this._focusables.length>1)for(const e of this._focusables)Fk(e)&&(e.focusCycler.on("forwardCycle",(e=>{this._focusCycler.focusNext(),e.stop()})),e.focusCycler.on("backwardCycle",(e=>{this._focusCycler.focusPrevious(),e.stop()})));this.setTemplate({tag:"form",attributes:{class:["ck","ck-image-insert-form"],tabindex:-1},children:this.children})}render(){super.render(),i({view:this});for(const e of this._focusables)this.focusTracker.add(e.element);this.keystrokes.listenTo(this.element);const e=e=>e.stopPropagation();this.keystrokes.set("arrowright",e),this.keystrokes.set("arrowleft",e),this.keystrokes.set("arrowup",e),this.keystrokes.set("arrowdown",e)}destroy(){super.destroy(),this.focusTracker.destroy(),this.keystrokes.destroy()}focus(){this._focusCycler.focusFirst()}}function Fk(e){return"focusCycler"in e}class Dk extends so{static get pluginName(){return"ImageInsertUI"}static get requires(){return[bk]}constructor(e){super(e),this._integrations=new Map,e.config.define("image.insert.integrations",["upload","assetManager","url"])}init(){const e=this.editor,t=e.model.document.selection,i=e.plugins.get("ImageUtils");this.set("isImageSelected",!1),this.listenTo(e.model.document,"change",(()=>{this.isImageSelected=i.isImage(t.getSelectedElement())}));const n=e=>this._createToolbarComponent(e);e.ui.componentFactory.add("insertImage",n),e.ui.componentFactory.add("imageInsert",n)}registerIntegration({name:e,observable:t,buttonViewCreator:i,formViewCreator:n,requiresForm:s}){this._integrations.has(e)&&k("image-insert-integration-exists",{name:e}),this._integrations.set(e,{observable:t,buttonViewCreator:i,formViewCreator:n,requiresForm:!!s})}_createToolbarComponent(e){const t=this.editor,i=e.t,n=this._prepareIntegrations();if(!n.length)return null;let s;const o=n[0];if(1==n.length){if(!o.requiresForm)return o.buttonViewCreator(!0);s=o.buttonViewCreator(!0)}else{const t=o.buttonViewCreator(!1);s=new Fm(e,t),s.tooltip=!0,s.bind("label").to(this,"isImageSelected",(e=>i(e?"Replace image":"Insert image")))}const r=this.dropdownView=Dm(e,s),a=n.map((({observable:e})=>"function"==typeof e?e():e));return r.bind("isEnabled").toMany(a,"isEnabled",((...e)=>e.some((e=>e)))),r.once("change:isOpen",(()=>{const e=n.map((({formViewCreator:e})=>e(1==n.length))),i=new Nk(t.locale,e);r.panelView.children.add(i)})),r}_prepareIntegrations(){const e=this.editor.config.get("image.insert.integrations"),t=[];if(!e.length)return k("image-insert-integrations-not-specified"),t;for(const i of e)this._integrations.has(i)?t.push(this._integrations.get(i)):["upload","assetManager","url"].includes(i)||k("image-insert-unknown-integration",{item:i});return t.length||k("image-insert-integrations-not-registered"),t}}class Lk extends so{static get requires(){return[Mk,ev,xk,Dk]}static get pluginName(){return"ImageBlock"}}class zk extends so{static get requires(){return[Vk,Rk,bk,Bk,Sw]}static get pluginName(){return"ImageInlineEditing"}init(){const e=this.editor,t=e.model.schema;t.register("imageInline",{inheritAllFrom:"$inlineObject",allowAttributes:["alt","src","srcset"]}),t.addChildCheck(((e,t)=>{if(e.endsWith("caption")&&"imageInline"===t.name)return!1})),this._setupConversion(),e.plugins.has("ImageBlockEditing")&&(e.commands.add("imageTypeInline",new Ok(this.editor,"imageInline")),this._setupClipboardIntegration())}_setupConversion(){const e=this.editor,t=e.t,i=e.conversion,n=e.plugins.get("ImageUtils");i.for("dataDowncast").elementToElement({model:"imageInline",view:(e,{writer:t})=>t.createEmptyElement("img")}),i.for("editingDowncast").elementToStructure({model:"imageInline",view:(e,{writer:i})=>n.toImageWidget(function(e){return e.createContainerElement("span",{class:"image-inline"},e.createEmptyElement("img"))}(i),i,t("image widget"))}),i.for("downcast").add(Tk(n,"imageInline","src")).add(Tk(n,"imageInline","alt")).add(Ek(n,"imageInline")),i.for("upcast").elementToElement({view:uk(e,"imageInline"),model:(e,{writer:t})=>t.createElement("imageInline",e.hasAttribute("src")?{src:e.getAttribute("src")}:void 0)})}_setupClipboardIntegration(){const e=this.editor,t=e.model,i=e.editing.view,n=e.plugins.get("ImageUtils"),s=e.plugins.get("ClipboardPipeline");this.listenTo(s,"inputTransformation",((s,o)=>{const r=Array.from(o.content.getChildren());let a;if(!r.every(n.isBlockImageView))return;a=o.targetRanges?e.editing.mapper.toModelRange(o.targetRanges[0]):t.document.selection.getFirstRange();const l=t.createSelection(a);if("imageInline"===mk(t.schema,l)){const e=new _h(i.document),t=r.map((t=>1===t.childCount?(Array.from(t.getAttributes()).forEach((i=>e.setAttribute(...i,n.findViewImgElement(t)))),t.getChild(0)):t));o.content=e.createDocumentFragment(t)}})),this.listenTo(s,"contentInsertion",((e,i)=>{"paste"===i.method&&t.change((e=>{const t=e.createRangeIn(i.content);for(const e of t.getItems())e.is("element","imageInline")&&n.setImageNaturalSizeAttributes(e)}))}))}}class Hk extends so{static get requires(){return[zk,ev,xk,Dk]}static get pluginName(){return"ImageInline"}}class $k extends so{static get pluginName(){return"ImageCaptionUtils"}static get requires(){return[bk]}getCaptionFromImageModelElement(e){for(const t of e.getChildren())if(t&&t.is("element","caption"))return t;return null}getCaptionFromModelSelection(e){const t=this.editor.plugins.get("ImageUtils"),i=e.getFirstPosition().findAncestor("caption");return i&&t.isBlockImage(i.parent)?i:null}matchImageCaptionViewElement(e){const t=this.editor.plugins.get("ImageUtils");return"figcaption"==e.name&&t.isBlockImageView(e.parent)?{name:!0}:null}}class Wk extends ro{refresh(){const e=this.editor,t=e.plugins.get("ImageCaptionUtils"),i=e.plugins.get("ImageUtils");if(!e.plugins.has(Mk))return this.isEnabled=!1,void(this.value=!1);const n=e.model.document.selection,s=n.getSelectedElement();if(!s){const e=t.getCaptionFromModelSelection(n);return this.isEnabled=!!e,void(this.value=!!e)}this.isEnabled=i.isImage(s),this.isEnabled?this.value=!!t.getCaptionFromImageModelElement(s):this.value=!1}execute(e={}){const{focusCaptionOnShow:t}=e;this.editor.model.change((e=>{this.value?this._hideImageCaption(e):this._showImageCaption(e,t)}))}_showImageCaption(e,t){const i=this.editor.model.document.selection,n=this.editor.plugins.get("ImageCaptionEditing"),s=this.editor.plugins.get("ImageUtils");let o=i.getSelectedElement();const r=n._getSavedCaption(o);s.isInlineImage(o)&&(this.editor.execute("imageTypeBlock"),o=i.getSelectedElement());const a=r||e.createElement("caption");e.append(a,o),t&&e.setSelection(a,"in")}_hideImageCaption(e){const t=this.editor,i=t.model.document.selection,n=t.plugins.get("ImageCaptionEditing"),s=t.plugins.get("ImageCaptionUtils");let o,r=i.getSelectedElement();r?o=s.getCaptionFromImageModelElement(r):(o=s.getCaptionFromModelSelection(i),r=o.parent),n._saveCaption(r,o),e.setSelection(r,"on"),e.remove(o)}}class jk extends so{static get requires(){return[bk,$k]}static get pluginName(){return"ImageCaptionEditing"}constructor(e){super(e),this._savedCaptionsMap=new WeakMap}init(){const e=this.editor,t=e.model.schema;t.isRegistered("caption")?t.extend("caption",{allowIn:"imageBlock"}):t.register("caption",{allowIn:"imageBlock",allowContentOf:"$block",isLimit:!0}),e.commands.add("toggleImageCaption",new Wk(this.editor)),this._setupConversion(),this._setupImageTypeCommandsIntegration(),this._registerCaptionReconversion()}_setupConversion(){const e=this.editor,t=e.editing.view,i=e.plugins.get("ImageUtils"),n=e.plugins.get("ImageCaptionUtils"),s=e.t;e.conversion.for("upcast").elementToElement({view:e=>n.matchImageCaptionViewElement(e),model:"caption"}),e.conversion.for("dataDowncast").elementToElement({model:"caption",view:(e,{writer:t})=>i.isBlockImage(e.parent)?t.createContainerElement("figcaption"):null}),e.conversion.for("editingDowncast").elementToElement({model:"caption",view:(e,{writer:n})=>{if(!i.isBlockImage(e.parent))return null;const o=n.createEditableElement("figcaption");n.setCustomProperty("imageCaption",!0,o),o.placeholder=s("Enter image caption"),fo({view:t,element:o,keepOnFocus:!0});const r=e.parent.getAttribute("alt");return Hw(o,n,{label:r?s("Caption for image: %0",[r]):s("Caption for the image")})}})}_setupImageTypeCommandsIntegration(){const e=this.editor,t=e.plugins.get("ImageUtils"),i=e.plugins.get("ImageCaptionUtils"),n=e.commands.get("imageTypeInline"),s=e.commands.get("imageTypeBlock"),o=e=>{if(!e.return)return;const{oldElement:n,newElement:s}=e.return;if(!n)return;if(t.isBlockImage(n)){const e=i.getCaptionFromImageModelElement(n);if(e)return void this._saveCaption(s,e)}const o=this._getSavedCaption(n);o&&this._saveCaption(s,o)};n&&this.listenTo(n,"execute",o,{priority:"low"}),s&&this.listenTo(s,"execute",o,{priority:"low"})}_getSavedCaption(e){const t=this._savedCaptionsMap.get(e);return t?xl.fromJSON(t):null}_saveCaption(e,t){this._savedCaptionsMap.set(e,t.toJSON())}_registerCaptionReconversion(){const e=this.editor,t=e.model,i=e.plugins.get("ImageUtils"),n=e.plugins.get("ImageCaptionUtils");t.document.on("change:data",(()=>{const s=t.document.differ.getChanges();for(const t of s){if("alt"!==t.attributeKey)continue;const s=t.range.start.nodeAfter;if(i.isBlockImage(s)){const t=n.getCaptionFromImageModelElement(s);if(!t)return;e.editing.reconvertItem(t)}}}))}}class Uk extends so{static get requires(){return[$k]}static get pluginName(){return"ImageCaptionUI"}init(){const e=this.editor,t=e.editing.view,i=e.plugins.get("ImageCaptionUtils"),n=e.t;e.ui.componentFactory.add("toggleImageCaption",(s=>{const o=e.commands.get("toggleImageCaption"),r=new Ku(s);return r.set({icon:fu.caption,tooltip:!0,isToggleable:!0}),r.bind("isOn","isEnabled").to(o,"value","isEnabled"),r.bind("label").to(o,"value",(e=>n(e?"Toggle caption off":"Toggle caption on"))),this.listenTo(r,"execute",(()=>{e.execute("toggleImageCaption",{focusCaptionOnShow:!0});const n=i.getCaptionFromModelSelection(e.model.document.selection);if(n){const i=e.editing.mapper.toViewElement(n);t.scrollToTheSelection(),t.change((e=>{e.addClass("image__caption_highlighted",i)}))}e.editing.view.focus()})),r}))}}function qk(e){const t=e.map((e=>e.replace("+","\\+")));return new RegExp(`^image\\/(${t.join("|")})$`)}function Gk(e){return new Promise(((t,i)=>{const n=e.getAttribute("src");fetch(n).then((e=>e.blob())).then((e=>{const i=Kk(e,n),s=i.replace("image/",""),o=new File([e],`image.${s}`,{type:i});t(o)})).catch((e=>e&&"TypeError"===e.name?function(e){return function(e){return new Promise(((t,i)=>{const n=Mn.document.createElement("img");n.addEventListener("load",(()=>{const e=Mn.document.createElement("canvas");e.width=n.width,e.height=n.height;e.getContext("2d").drawImage(n,0,0),e.toBlob((e=>e?t(e):i()))})),n.addEventListener("error",(()=>i())),n.src=e}))}(e).then((t=>{const i=Kk(t,e),n=i.replace("image/","");return new File([t],`image.${n}`,{type:i})}))}(n).then(t).catch(i):i(e)))}))}function Kk(e,t){return e.type?e.type:t.match(/data:(image\/\w+);base64/)?t.match(/data:(image\/\w+);base64/)[1].toLowerCase():"image/jpeg"}class Zk extends so{static get pluginName(){return"ImageUploadUI"}init(){const e=this.editor,t=e.t,i=()=>{const e=this._createButton(Ju);return e.set({label:t("Upload image from computer"),tooltip:!0}),e};if(e.ui.componentFactory.add("uploadImage",i),e.ui.componentFactory.add("imageUpload",i),e.ui.componentFactory.add("menuBar:uploadImage",(()=>{const e=this._createButton(mp);return e.label=t("Image from computer"),e})),e.plugins.has("ImageInsertUI")){const i=e.plugins.get("ImageInsertUI");i.registerIntegration({name:"upload",observable:()=>e.commands.get("uploadImage"),buttonViewCreator:()=>{const n=e.ui.componentFactory.create("uploadImage");return n.bind("label").to(i,"isImageSelected",(e=>t(e?"Replace image from computer":"Upload image from computer"))),n},formViewCreator:()=>{const n=e.ui.componentFactory.create("uploadImage");return n.withText=!0,n.bind("label").to(i,"isImageSelected",(e=>t(e?"Replace from computer":"Upload from computer"))),n.on("execute",(()=>{i.dropdownView.isOpen=!1})),n}})}}_createButton(e){const t=this.editor,i=t.locale,n=t.commands.get("uploadImage"),s=t.config.get("image.upload.types"),o=qk(s),r=new e(t.locale),a=i.t;return r.set({acceptedType:s.map((e=>`image/${e}`)).join(","),allowMultipleFiles:!0,label:a("Upload image from computer"),icon:fu.imageUpload}),r.bind("isEnabled").to(n),r.on("done",((e,i)=>{const n=Array.from(i).filter((e=>o.test(e.type)));n.length&&(t.execute("uploadImage",{file:n}),t.editing.view.focus())})),r}}class Jk extends(G()){constructor(){super();const e=new window.FileReader;this._reader=e,this._data=void 0,this.set("loaded",0),e.onprogress=e=>{this.loaded=e.loaded}}get error(){return this._reader.error}get data(){return this._data}read(e){const t=this._reader;return this.total=e.size,new Promise(((i,n)=>{t.onload=()=>{const e=t.result;this._data=e,i(e)},t.onerror=()=>{n("error")},t.onabort=()=>{n("aborted")},this._reader.readAsDataURL(e)}))}abort(){this._reader.abort()}}class Qk extends so{constructor(){super(...arguments),this.loaders=new Ks,this._loadersMap=new Map,this._pendingAction=null}static get pluginName(){return"FileRepository"}static get requires(){return[gu]}init(){this.loaders.on("change",(()=>this._updatePendingAction())),this.set("uploaded",0),this.set("uploadTotal",null),this.bind("uploadedPercent").to(this,"uploaded",this,"uploadTotal",((e,t)=>t?e/t*100:0))}getLoader(e){return this._loadersMap.get(e)||null}createLoader(e){if(!this.createUploadAdapter)return k("filerepository-no-upload-adapter"),null;const t=new Yk(Promise.resolve(e),this.createUploadAdapter);return this.loaders.add(t),this._loadersMap.set(e,t),e instanceof Promise&&t.file.then((e=>{this._loadersMap.set(e,t)})).catch((()=>{})),t.on("change:uploaded",(()=>{let e=0;for(const t of this.loaders)e+=t.uploaded;this.uploaded=e})),t.on("change:uploadTotal",(()=>{let e=0;for(const t of this.loaders)t.uploadTotal&&(e+=t.uploadTotal);this.uploadTotal=e})),t}destroyLoader(e){const t=e instanceof Yk?e:this.getLoader(e);t._destroy(),this.loaders.remove(t),this._loadersMap.forEach(((e,i)=>{e===t&&this._loadersMap.delete(i)}))}_updatePendingAction(){const e=this.editor.plugins.get(gu);if(this.loaders.length){if(!this._pendingAction){const t=this.editor.t,i=e=>`${t("Upload in progress")} ${parseInt(e)}%.`;this._pendingAction=e.add(i(this.uploadedPercent)),this._pendingAction.bind("message").to(this,"uploadedPercent",i)}}else e.remove(this._pendingAction),this._pendingAction=null}}class Yk extends(G()){constructor(e,t){super(),this.id=b(),this._filePromiseWrapper=this._createFilePromiseWrapper(e),this._adapter=t(this),this._reader=new Jk,this.set("status","idle"),this.set("uploaded",0),this.set("uploadTotal",null),this.bind("uploadedPercent").to(this,"uploaded",this,"uploadTotal",((e,t)=>t?e/t*100:0)),this.set("uploadResponse",null)}get file(){return this._filePromiseWrapper?this._filePromiseWrapper.promise.then((e=>this._filePromiseWrapper?e:null)):Promise.resolve(null)}get data(){return this._reader.data}read(){if("idle"!=this.status)throw new y("filerepository-read-wrong-status",this);return this.status="reading",this.file.then((e=>this._reader.read(e))).then((e=>{if("reading"!==this.status)throw this.status;return this.status="idle",e})).catch((e=>{if("aborted"===e)throw this.status="aborted","aborted";throw this.status="error",this._reader.error?this._reader.error:e}))}upload(){if("idle"!=this.status)throw new y("filerepository-upload-wrong-status",this);return this.status="uploading",this.file.then((()=>this._adapter.upload())).then((e=>(this.uploadResponse=e,this.status="idle",e))).catch((e=>{if("aborted"===this.status)throw"aborted";throw this.status="error",e}))}abort(){const e=this.status;this.status="aborted",this._filePromiseWrapper.isFulfilled?"reading"==e?this._reader.abort():"uploading"==e&&this._adapter.abort&&this._adapter.abort():(this._filePromiseWrapper.promise.catch((()=>{})),this._filePromiseWrapper.rejecter("aborted")),this._destroy()}_destroy(){this._filePromiseWrapper=void 0,this._reader=void 0,this._adapter=void 0,this.uploadResponse=void 0}_createFilePromiseWrapper(e){const t={};return t.promise=new Promise(((i,n)=>{t.rejecter=n,t.isFulfilled=!1,e.then((e=>{t.isFulfilled=!0,i(e)})).catch((e=>{t.isFulfilled=!0,n(e)}))})),t}}class Xk extends so{static get pluginName(){return"ImageUploadProgress"}constructor(e){super(e),this.uploadStatusChange=(e,t,i)=>{const n=this.editor,s=t.item,o=s.getAttribute("uploadId");if(!i.consumable.consume(t.item,e.name))return;const r=n.plugins.get("ImageUtils"),a=n.plugins.get(Qk),l=o?t.attributeNewValue:null,c=this.placeholder,d=n.editing.mapper.toViewElement(s),h=i.writer;if("reading"==l)return eC(d,h),void tC(r,c,d,h);if("uploading"==l){const e=a.loaders.get(o);return eC(d,h),void(e?(iC(d,h),function(e,t,i,n){const s=function(e){const t=e.createUIElement("div",{class:"ck-progress-bar"});return e.setCustomProperty("progressBar",!0,t),t}(t);t.insert(t.createPositionAt(e,"end"),s),i.on("change:uploadedPercent",((e,t,i)=>{n.change((e=>{e.setStyle("width",i+"%",s)}))}))}(d,h,e,n.editing.view),function(e,t,i,n){if(n.data){const s=e.findViewImgElement(t);i.setAttribute("src",n.data,s)}}(r,d,h,e)):tC(r,c,d,h))}"complete"==l&&a.loaders.get(o)&&function(e,t,i){const n=t.createUIElement("div",{class:"ck-image-upload-complete-icon"});t.insert(t.createPositionAt(e,"end"),n),setTimeout((()=>{i.change((e=>e.remove(e.createRangeOn(n))))}),3e3)}(d,h,n.editing.view),function(e,t){sC(e,t,"progressBar")}(d,h),iC(d,h),function(e,t){t.removeClass("ck-appear",e)}(d,h)},this.placeholder="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="}init(){const e=this.editor;e.plugins.has("ImageBlockEditing")&&e.editing.downcastDispatcher.on("attribute:uploadStatus:imageBlock",this.uploadStatusChange),e.plugins.has("ImageInlineEditing")&&e.editing.downcastDispatcher.on("attribute:uploadStatus:imageInline",this.uploadStatusChange)}}function eC(e,t){e.hasClass("ck-appear")||t.addClass("ck-appear",e)}function tC(e,t,i,n){i.hasClass("ck-image-upload-placeholder")||n.addClass("ck-image-upload-placeholder",i);const s=e.findViewImgElement(i);s.getAttribute("src")!==t&&n.setAttribute("src",t,s),nC(i,"placeholder")||n.insert(n.createPositionAfter(s),function(e){const t=e.createUIElement("div",{class:"ck-upload-placeholder-loader"});return e.setCustomProperty("placeholder",!0,t),t}(n))}function iC(e,t){e.hasClass("ck-image-upload-placeholder")&&t.removeClass("ck-image-upload-placeholder",e),sC(e,t,"placeholder")}function nC(e,t){for(const i of e.getChildren())if(i.getCustomProperty(t))return i}function sC(e,t,i){const n=nC(e,i);n&&t.remove(t.createRangeOn(n))}class oC extends ro{refresh(){const e=this.editor,t=e.plugins.get("ImageUtils"),i=e.model.document.selection.getSelectedElement();this.isEnabled=t.isImageAllowed()||t.isImage(i)}execute(e){const t=Cs(e.file),i=this.editor.model.document.selection,n=this.editor.plugins.get("ImageUtils"),s=Object.fromEntries(i.getAttributes());t.forEach(((e,t)=>{const o=i.getSelectedElement();if(t&&o&&n.isImage(o)){const t=this.editor.model.createPositionAfter(o);this._uploadImage(e,s,t)}else this._uploadImage(e,s)}))}_uploadImage(e,t,i){const n=this.editor,s=n.plugins.get(Qk).createLoader(e),o=n.plugins.get("ImageUtils");s&&o.insertImage({...t,uploadId:s.id},i)}}class rC extends so{static get requires(){return[Qk,Tf,Sw,bk]}static get pluginName(){return"ImageUploadEditing"}constructor(e){super(e),e.config.define("image",{upload:{types:["jpeg","png","gif","bmp","webp","tiff"]}}),this._uploadImageElements=new Map}init(){const e=this.editor,t=e.model.document,i=e.conversion,n=e.plugins.get(Qk),s=e.plugins.get("ImageUtils"),o=e.plugins.get("ClipboardPipeline"),r=qk(e.config.get("image.upload.types")),a=new oC(e);e.commands.add("uploadImage",a),e.commands.add("imageUpload",a),i.for("upcast").attributeToAttribute({view:{name:"img",key:"uploadId"},model:"uploadId"}),this.listenTo(e.editing.view.document,"clipboardInput",((t,i)=>{if(n=i.dataTransfer,Array.from(n.types).includes("text/html")&&""!==n.getData("text/html"))return;var n;const s=Array.from(i.dataTransfer.files).filter((e=>!!e&&r.test(e.type)));s.length&&(t.stop(),e.model.change((t=>{i.targetRanges&&t.setSelection(i.targetRanges.map((t=>e.editing.mapper.toModelRange(t)))),e.execute("uploadImage",{file:s})})))})),this.listenTo(o,"inputTransformation",((t,i)=>{const o=Array.from(e.editing.view.createRangeIn(i.content)).map((e=>e.item)).filter((e=>function(e,t){return!(!e.isInlineImageView(t)||!t.getAttribute("src")||!t.getAttribute("src").match(/^data:image\/\w+;base64,/g)&&!t.getAttribute("src").match(/^blob:/g))}(s,e)&&!e.getAttribute("uploadProcessed"))).map((e=>({promise:Gk(e),imageElement:e})));if(!o.length)return;const r=new _h(e.editing.view.document);for(const e of o){r.setAttribute("uploadProcessed",!0,e.imageElement);const t=n.createLoader(e.promise);t&&(r.setAttribute("src","",e.imageElement),r.setAttribute("uploadId",t.id,e.imageElement))}})),e.editing.view.document.on("dragover",((e,t)=>{t.preventDefault()})),t.on("change",(()=>{const i=t.differ.getChanges({includeChangesInGraveyard:!0}).reverse(),s=new Set;for(const t of i)if("insert"==t.type&&"$text"!=t.name){const i=t.position.nodeAfter,o="$graveyard"==t.position.root.rootName;for(const t of aC(e,i)){const e=t.getAttribute("uploadId");if(!e)continue;const i=n.loaders.get(e);i&&(o?s.has(e)||i.abort():(s.add(e),this._uploadImageElements.set(e,t),"idle"==i.status&&this._readAndUpload(i)))}}})),this.on("uploadComplete",((e,{imageElement:t,data:i})=>{const n=i.urls?i.urls:i;this.editor.model.change((e=>{e.setAttribute("src",n.default,t),this._parseAndSetSrcsetAttributeOnImage(n,t,e),s.setImageNaturalSizeAttributes(t)}))}),{priority:"low"})}afterInit(){const e=this.editor.model.schema;this.editor.plugins.has("ImageBlockEditing")&&e.extend("imageBlock",{allowAttributes:["uploadId","uploadStatus"]}),this.editor.plugins.has("ImageInlineEditing")&&e.extend("imageInline",{allowAttributes:["uploadId","uploadStatus"]})}_readAndUpload(e){const t=this.editor,i=t.model,n=t.locale.t,s=t.plugins.get(Qk),o=t.plugins.get(Tf),r=t.plugins.get("ImageUtils"),a=this._uploadImageElements;return i.enqueueChange({isUndoable:!1},(t=>{t.setAttribute("uploadStatus","reading",a.get(e.id))})),e.read().then((()=>{const n=e.upload(),s=a.get(e.id);if(l.isSafari){const e=t.editing.mapper.toViewElement(s),i=r.findViewImgElement(e);t.editing.view.once("render",(()=>{if(!i.parent)return;const e=t.editing.view.domConverter.mapViewToDom(i.parent);if(!e)return;const n=e.style.display;e.style.display="none",e._ckHack=e.offsetHeight,e.style.display=n}))}return i.enqueueChange({isUndoable:!1},(e=>{e.setAttribute("uploadStatus","uploading",s)})),n})).then((t=>{i.enqueueChange({isUndoable:!1},(i=>{const n=a.get(e.id);i.setAttribute("uploadStatus","complete",n),this.fire("uploadComplete",{data:t,imageElement:n})})),c()})).catch((t=>{if("error"!==e.status&&"aborted"!==e.status)throw t;"error"==e.status&&t&&o.showWarning(t,{title:n("Upload failed"),namespace:"upload"}),i.enqueueChange({isUndoable:!1},(t=>{t.remove(a.get(e.id))})),c()}));function c(){i.enqueueChange({isUndoable:!1},(t=>{const i=a.get(e.id);t.removeAttribute("uploadId",i),t.removeAttribute("uploadStatus",i),a.delete(e.id)})),s.destroyLoader(e)}}_parseAndSetSrcsetAttributeOnImage(e,t,i){let n=0;const s=Object.keys(e).filter((e=>{const t=parseInt(e,10);if(!isNaN(t))return n=Math.max(n,t),!0})).map((t=>`${e[t]} ${t}w`)).join(", ");if(""!=s){const e={srcset:s};t.hasAttribute("width")||t.hasAttribute("height")||(e.width=n),i.setAttributes(e,t)}}}function aC(e,t){const i=e.plugins.get("ImageUtils");return Array.from(e.model.createRangeOn(t)).filter((e=>i.isImage(e.item))).map((e=>e.item))}class lC extends so{static get pluginName(){return"ImageUpload"}static get requires(){return[rC,Zk,Xk]}}class cC extends Du{constructor(e){super(e),this.set("imageURLInputValue",""),this.set("isImageSelected",!1),this.set("isEnabled",!0),this.focusTracker=new Js,this.keystrokes=new Qs,this._focusables=new pu,this.focusCycler=new ym({focusables:this._focusables,focusTracker:this.focusTracker,keystrokeHandler:this.keystrokes,actions:{focusPrevious:"shift + tab",focusNext:"tab"}}),this.urlInputView=this._createUrlInputView(),this.insertButtonView=this._createInsertButton(),this.cancelButtonView=this._createCancelButton(),this._focusables.addMany([this.urlInputView,this.insertButtonView,this.cancelButtonView]),this.setTemplate({tag:"div",attributes:{class:["ck","ck-image-insert-url"]},children:[this.urlInputView,{tag:"div",attributes:{class:["ck","ck-image-insert-url__action-row"]},children:[this.insertButtonView,this.cancelButtonView]}]})}render(){super.render();for(const e of this._focusables)this.focusTracker.add(e.element);this.keystrokes.listenTo(this.element)}destroy(){super.destroy(),this.focusTracker.destroy(),this.keystrokes.destroy()}_createUrlInputView(){const e=this.locale,t=e.t,i=new um(e,Um);return i.bind("label").to(this,"isImageSelected",(e=>t(e?"Update image URL":"Insert image via URL"))),i.bind("isEnabled").to(this),i.fieldView.placeholder="https://example.com/image.png",i.fieldView.bind("value").to(this,"imageURLInputValue",(e=>e||"")),i.fieldView.on("input",(()=>{this.imageURLInputValue=i.fieldView.element.value.trim()})),i}_createInsertButton(){const e=this.locale,t=e.t,i=new Ku(e);return i.set({icon:fu.check,class:"ck-button-save",type:"submit",withText:!0}),i.bind("label").to(this,"isImageSelected",(e=>t(e?"Update":"Insert"))),i.bind("isEnabled").to(this,"imageURLInputValue",this,"isEnabled",((...e)=>e.every((e=>e)))),i.delegate("execute").to(this,"submit"),i}_createCancelButton(){const e=this.locale,t=e.t,i=new Ku(e);return i.set({label:t("Cancel"),icon:fu.cancel,class:"ck-button-cancel",withText:!0}),i.bind("isEnabled").to(this),i.delegate("execute").to(this,"cancel"),i}focus(e){-1===e?this.focusCycler.focusLast():this.focusCycler.focusFirst()}}class dC extends so{static get pluginName(){return"ImageInsertViaUrlUI"}static get requires(){return[Dk]}afterInit(){this._imageInsertUI=this.editor.plugins.get("ImageInsertUI"),this._imageInsertUI.registerIntegration({name:"url",observable:()=>this.editor.commands.get("insertImage"),requiresForm:!0,buttonViewCreator:e=>this._createInsertUrlButton(e),formViewCreator:e=>this._createInsertUrlView(e)})}_createInsertUrlView(e){const t=this.editor,i=t.locale,n=i.t,s=t.commands.get("replaceImageSource"),o=t.commands.get("insertImage"),r=new cC(i),a=e?null:new Xu(i,[r]);return r.bind("isImageSelected").to(this._imageInsertUI),r.bind("isEnabled").toMany([o,s],"isEnabled",((...e)=>e.some((e=>e)))),r.imageURLInputValue=s.value||"",this._imageInsertUI.dropdownView.on("change:isOpen",(()=>{this._imageInsertUI.dropdownView.isOpen&&(r.imageURLInputValue=s.value||"",a&&(a.isCollapsed=!0))}),{priority:"low"}),r.on("submit",(()=>{s.isEnabled?t.execute("replaceImageSource",{source:r.imageURLInputValue}):t.execute("insertImage",{source:r.imageURLInputValue}),this._closePanel()})),r.on("cancel",(()=>this._closePanel())),a?(a.set({isCollapsed:!0}),a.bind("label").to(this._imageInsertUI,"isImageSelected",(e=>n(e?"Update image URL":"Insert image via URL"))),a):r}_createInsertUrlButton(e){const t=e?_m:Ku,i=this.editor,n=new t(i.locale),s=i.locale.t;return n.set({icon:fu.imageUrl,tooltip:!0}),n.bind("label").to(this._imageInsertUI,"isImageSelected",(e=>s(e?"Update image URL":"Insert image via URL"))),n}_closePanel(){this.editor.editing.view.focus(),this._imageInsertUI.dropdownView.isOpen=!1}}class hC extends so{static get pluginName(){return"ImageInsertViaUrl"}static get requires(){return[dC,Dk]}}class uC extends ro{refresh(){const e=this.editor,t=e.plugins.get("ImageUtils").getClosestSelectedImageElement(e.model.document.selection);this.isEnabled=!!t,t&&t.hasAttribute("resizedWidth")?this.value={width:t.getAttribute("resizedWidth"),height:null}:this.value=null}execute(e){const t=this.editor,i=t.model,n=t.plugins.get("ImageUtils"),s=n.getClosestSelectedImageElement(i.document.selection);this.value={width:e.width,height:null},s&&i.change((t=>{t.setAttribute("resizedWidth",e.width,s),t.removeAttribute("resizedHeight",s),n.setImageNaturalSizeAttributes(s)}))}}class mC extends so{static get requires(){return[bk]}static get pluginName(){return"ImageResizeEditing"}constructor(e){super(e),e.config.define("image",{resizeUnit:"%",resizeOptions:[{name:"resizeImage:original",value:null,icon:"original"},{name:"resizeImage:25",value:"25",icon:"small"},{name:"resizeImage:50",value:"50",icon:"medium"},{name:"resizeImage:75",value:"75",icon:"large"}]})}init(){const e=this.editor,t=new uC(e);this._registerConverters("imageBlock"),this._registerConverters("imageInline"),e.commands.add("resizeImage",t),e.commands.add("imageResize",t)}afterInit(){this._registerSchema()}_registerSchema(){this.editor.plugins.has("ImageBlockEditing")&&this.editor.model.schema.extend("imageBlock",{allowAttributes:["resizedWidth","resizedHeight"]}),this.editor.plugins.has("ImageInlineEditing")&&this.editor.model.schema.extend("imageInline",{allowAttributes:["resizedWidth","resizedHeight"]})}_registerConverters(e){const t=this.editor,i=t.plugins.get("ImageUtils");t.conversion.for("downcast").add((t=>t.on(`attribute:resizedWidth:${e}`,((e,t,i)=>{if(!i.consumable.consume(t.item,e.name))return;const n=i.writer,s=i.mapper.toViewElement(t.item);null!==t.attributeNewValue?(n.setStyle("width",t.attributeNewValue,s),n.addClass("image_resized",s)):(n.removeStyle("width",s),n.removeClass("image_resized",s))})))),t.conversion.for("dataDowncast").attributeToAttribute({model:{name:e,key:"resizedHeight"},view:e=>({key:"style",value:{height:e}})}),t.conversion.for("editingDowncast").add((t=>t.on(`attribute:resizedHeight:${e}`,((t,n,s)=>{if(!s.consumable.consume(n.item,t.name))return;const o=s.writer,r=s.mapper.toViewElement(n.item),a="imageInline"===e?i.findViewImgElement(r):r;null!==n.attributeNewValue?o.setStyle("height",n.attributeNewValue,a):o.removeStyle("height",a)})))),t.conversion.for("upcast").attributeToAttribute({view:{name:"imageBlock"===e?"figure":"img",styles:{width:/.+/}},model:{key:"resizedWidth",value:e=>fk(e)?null:e.getStyle("width")}}),t.conversion.for("upcast").attributeToAttribute({view:{name:"imageBlock"===e?"figure":"img",styles:{height:/.+/}},model:{key:"resizedHeight",value:e=>fk(e)?null:e.getStyle("height")}})}}const gC={small:fu.objectSizeSmall,medium:fu.objectSizeMedium,large:fu.objectSizeLarge,original:fu.objectSizeFull};class fC extends so{static get requires(){return[mC]}static get pluginName(){return"ImageResizeButtons"}constructor(e){super(e),this._resizeUnit=e.config.get("image.resizeUnit")}init(){const e=this.editor,t=e.config.get("image.resizeOptions"),i=e.commands.get("resizeImage");this.bind("isEnabled").to(i);for(const e of t)this._registerImageResizeButton(e);this._registerImageResizeDropdown(t)}_registerImageResizeButton(e){const t=this.editor,{name:i,value:n,icon:s}=e,o=n?n+this._resizeUnit:null;t.ui.componentFactory.add(i,(i=>{const n=new Ku(i),r=t.commands.get("resizeImage"),a=this._getOptionLabelValue(e,!0);if(!gC[s])throw new y("imageresizebuttons-missing-icon",t,e);return n.set({label:a,icon:gC[s],tooltip:a,isToggleable:!0}),n.bind("isEnabled").to(this),n.bind("isOn").to(r,"value",pC(o)),this.listenTo(n,"execute",(()=>{t.execute("resizeImage",{width:o})})),n}))}_registerImageResizeDropdown(e){const t=this.editor,i=t.t,n=e.find((e=>!e.value)),s=s=>{const o=t.commands.get("resizeImage"),r=Dm(s,_m),a=r.buttonView,l=i("Resize image");return a.set({tooltip:l,commandValue:n.value,icon:gC.medium,isToggleable:!0,label:this._getOptionLabelValue(n),withText:!0,class:"ck-resize-image-button",ariaLabel:l,ariaLabelledBy:void 0}),a.bind("label").to(o,"value",(e=>e&&e.width?e.width:this._getOptionLabelValue(n))),r.bind("isEnabled").to(this),Hm(r,(()=>this._getResizeDropdownListItemDefinitions(e,o)),{ariaLabel:i("Image resize list"),role:"menu"}),this.listenTo(r,"execute",(e=>{t.execute(e.source.commandName,{width:e.source.commandValue}),t.editing.view.focus()})),r};t.ui.componentFactory.add("resizeImage",s),t.ui.componentFactory.add("imageResize",s)}_getOptionLabelValue(e,t=!1){const i=this.editor.t;return e.label?e.label:t?e.value?i("Resize image to %0",e.value+this._resizeUnit):i("Resize image to the original size"):e.value?e.value+this._resizeUnit:i("Original")}_getResizeDropdownListItemDefinitions(e,t){const i=new Ks;return e.map((e=>{const n=e.value?e.value+this._resizeUnit:null,s={type:"button",model:new Sf({commandName:"resizeImage",commandValue:n,label:this._getOptionLabelValue(e),role:"menuitemradio",withText:!0,icon:null})};s.model.bind("isOn").to(t,"value",pC(n)),i.add(s)})),i}}function pC(e){return t=>null===e&&t===e||null!==t&&t.width===e}const bC="image_resized";class wC extends so{static get requires(){return[cv,bk]}static get pluginName(){return"ImageResizeHandles"}init(){const e=this.editor.commands.get("resizeImage");this.bind("isEnabled").to(e),this._setupResizerCreator()}_setupResizerCreator(){const e=this.editor,t=e.editing.view,i=e.plugins.get("ImageUtils");t.addObserver(Sk),this.listenTo(t.document,"imageLoaded",((n,s)=>{if(!s.target.matches("figure.image.ck-widget > img,figure.image.ck-widget > picture > img,figure.image.ck-widget > a > img,figure.image.ck-widget > a > picture > img,span.image-inline.ck-widget > img,span.image-inline.ck-widget > picture > img"))return;const o=e.editing.view.domConverter,r=o.domToView(s.target),a=i.getImageWidgetFromImageView(r);let l=this.editor.plugins.get(cv).getResizerByViewElement(a);if(l)return void l.redraw();const c=e.editing.mapper,d=c.toModelElement(a);l=e.plugins.get(cv).attachTo({unit:e.config.get("image.resizeUnit"),modelElement:d,viewElement:a,editor:e,getHandleHost:e=>e.querySelector("img"),getResizeHost:()=>o.mapViewToDom(c.toViewElement(d.parent)),isCentered:()=>"alignCenter"==d.getAttribute("imageStyle"),onCommit(i){t.change((e=>{e.removeClass(bC,a)})),e.execute("resizeImage",{width:i})}}),l.on("updateSize",(()=>{a.hasClass(bC)||t.change((e=>{e.addClass(bC,a)}));const e="imageInline"===d.name?r:a;e.getStyle("height")&&t.change((t=>{t.removeStyle("height",e)}))})),l.bind("isEnabled").to(this)}))}}class vC extends ro{constructor(e,t){super(e),this._defaultStyles={imageBlock:!1,imageInline:!1},this._styles=new Map(t.map((e=>{if(e.isDefault)for(const t of e.modelElements)this._defaultStyles[t]=e.name;return[e.name,e]})))}refresh(){const e=this.editor.plugins.get("ImageUtils").getClosestSelectedImageElement(this.editor.model.document.selection);this.isEnabled=!!e,this.isEnabled?e.hasAttribute("imageStyle")?this.value=e.getAttribute("imageStyle"):this.value=this._defaultStyles[e.name]:this.value=!1}execute(e={}){const t=this.editor,i=t.model,n=t.plugins.get("ImageUtils");i.change((t=>{const s=e.value,{setImageSizes:o=!0}=e;let r=n.getClosestSelectedImageElement(i.document.selection);s&&this.shouldConvertImageType(s,r)&&(this.editor.execute(n.isBlockImage(r)?"imageTypeInline":"imageTypeBlock",{setImageSizes:o}),r=n.getClosestSelectedImageElement(i.document.selection)),!s||this._styles.get(s).isDefault?t.removeAttribute("imageStyle",r):t.setAttribute("imageStyle",s,r),o&&n.setImageNaturalSizeAttributes(r)}))}shouldConvertImageType(e,t){return!this._styles.get(e).modelElements.includes(t.name)}}const{objectFullWidth:_C,objectInline:yC,objectLeft:kC,objectRight:CC,objectCenter:AC,objectBlockLeft:xC,objectBlockRight:EC}=fu,TC={get inline(){return{name:"inline",title:"In line",icon:yC,modelElements:["imageInline"],isDefault:!0}},get alignLeft(){return{name:"alignLeft",title:"Left aligned image",icon:kC,modelElements:["imageBlock","imageInline"],className:"image-style-align-left"}},get alignBlockLeft(){return{name:"alignBlockLeft",title:"Left aligned image",icon:xC,modelElements:["imageBlock"],className:"image-style-block-align-left"}},get alignCenter(){return{name:"alignCenter",title:"Centered image",icon:AC,modelElements:["imageBlock"],className:"image-style-align-center"}},get alignRight(){return{name:"alignRight",title:"Right aligned image",icon:CC,modelElements:["imageBlock","imageInline"],className:"image-style-align-right"}},get alignBlockRight(){return{name:"alignBlockRight",title:"Right aligned image",icon:EC,modelElements:["imageBlock"],className:"image-style-block-align-right"}},get block(){return{name:"block",title:"Centered image",icon:AC,modelElements:["imageBlock"],isDefault:!0}},get side(){return{name:"side",title:"Side image",icon:CC,modelElements:["imageBlock"],className:"image-style-side"}}},SC={full:_C,left:xC,right:EC,center:AC,inlineLeft:kC,inlineRight:CC,inline:yC},PC=[{name:"imageStyle:wrapText",title:"Wrap text",defaultItem:"imageStyle:alignLeft",items:["imageStyle:alignLeft","imageStyle:alignRight"]},{name:"imageStyle:breakText",title:"Break text",defaultItem:"imageStyle:block",items:["imageStyle:alignBlockLeft","imageStyle:block","imageStyle:alignBlockRight"]}];function IC(e){k("image-style-configuration-definition-invalid",e)}const VC={normalizeStyles:function(e){return(e.configuredStyles.options||[]).map((e=>function(e){e="string"==typeof e?TC[e]?{...TC[e]}:{name:e}:function(e,t){const i={...t};for(const n in e)Object.prototype.hasOwnProperty.call(t,n)||(i[n]=e[n]);return i}(TC[e.name],e);"string"==typeof e.icon&&(e.icon=SC[e.icon]||e.icon);return e}(e))).filter((t=>function(e,{isBlockPluginLoaded:t,isInlinePluginLoaded:i}){const{modelElements:n,name:s}=e;if(!(n&&n.length&&s))return IC({style:e}),!1;{const s=[t?"imageBlock":null,i?"imageInline":null];if(!n.some((e=>s.includes(e))))return k("image-style-missing-dependency",{style:e,missingPlugins:n.map((e=>"imageBlock"===e?"ImageBlockEditing":"ImageInlineEditing"))}),!1}return!0}(t,e)))},getDefaultStylesConfiguration:function(e,t){return e&&t?{options:["inline","alignLeft","alignRight","alignCenter","alignBlockLeft","alignBlockRight","block","side"]}:e?{options:["block","side"]}:t?{options:["inline","alignLeft","alignRight"]}:{}},getDefaultDropdownDefinitions:function(e){return e.has("ImageBlockEditing")&&e.has("ImageInlineEditing")?[...PC]:[]},warnInvalidStyle:IC,DEFAULT_OPTIONS:TC,DEFAULT_ICONS:SC,DEFAULT_DROPDOWN_DEFINITIONS:PC};function RC(e,t){for(const i of t)if(i.name===e)return i}class OC extends so{static get pluginName(){return"ImageStyleEditing"}static get requires(){return[bk]}init(){const{normalizeStyles:e,getDefaultStylesConfiguration:t}=VC,i=this.editor,n=i.plugins.has("ImageBlockEditing"),s=i.plugins.has("ImageInlineEditing");i.config.define("image.styles",t(n,s)),this.normalizedStyles=e({configuredStyles:i.config.get("image.styles"),isBlockPluginLoaded:n,isInlinePluginLoaded:s}),this._setupConversion(n,s),this._setupPostFixer(),i.commands.add("imageStyle",new vC(i,this.normalizedStyles))}_setupConversion(e,t){const i=this.editor,n=i.model.schema,s=(o=this.normalizedStyles,(e,t,i)=>{if(!i.consumable.consume(t.item,e.name))return;const n=RC(t.attributeNewValue,o),s=RC(t.attributeOldValue,o),r=i.mapper.toViewElement(t.item),a=i.writer;s&&a.removeClass(s.className,r),n&&a.addClass(n.className,r)});var o;const r=function(e){const t={imageInline:e.filter((e=>!e.isDefault&&e.modelElements.includes("imageInline"))),imageBlock:e.filter((e=>!e.isDefault&&e.modelElements.includes("imageBlock")))};return(e,i,n)=>{if(!i.modelRange)return;const s=i.viewItem,o=Zs(i.modelRange.getItems());if(o&&n.schema.checkAttribute(o,"imageStyle"))for(const e of t[o.name])n.consumable.consume(s,{classes:e.className})&&n.writer.setAttribute("imageStyle",e.name,o)}}(this.normalizedStyles);i.editing.downcastDispatcher.on("attribute:imageStyle",s),i.data.downcastDispatcher.on("attribute:imageStyle",s),e&&(n.extend("imageBlock",{allowAttributes:"imageStyle"}),i.data.upcastDispatcher.on("element:figure",r,{priority:"low"})),t&&(n.extend("imageInline",{allowAttributes:"imageStyle"}),i.data.upcastDispatcher.on("element:img",r,{priority:"low"}))}_setupPostFixer(){const e=this.editor,t=e.model.document,i=e.plugins.get(bk),n=new Map(this.normalizedStyles.map((e=>[e.name,e])));t.registerPostFixer((e=>{let s=!1;for(const o of t.differ.getChanges())if("insert"==o.type||"attribute"==o.type&&"imageStyle"==o.attributeKey){let t="insert"==o.type?o.position.nodeAfter:o.range.start.nodeAfter;if(t&&t.is("element","paragraph")&&t.childCount>0&&(t=t.getChild(0)),!i.isImage(t))continue;const r=t.getAttribute("imageStyle");if(!r)continue;const a=n.get(r);a&&a.modelElements.includes(t.name)||(e.removeAttribute("imageStyle",t),s=!0)}return s}))}}class BC extends so{static get requires(){return[OC]}static get pluginName(){return"ImageStyleUI"}get localizedDefaultStylesTitles(){const e=this.editor.t;return{"Wrap text":e("Wrap text"),"Break text":e("Break text"),"In line":e("In line"),"Full size image":e("Full size image"),"Side image":e("Side image"),"Left aligned image":e("Left aligned image"),"Centered image":e("Centered image"),"Right aligned image":e("Right aligned image")}}init(){const e=this.editor.plugins,t=this.editor.config.get("image.toolbar")||[],i=MC(e.get("ImageStyleEditing").normalizedStyles,this.localizedDefaultStylesTitles);for(const e of i)this._createButton(e);const n=MC([...t.filter(L),...VC.getDefaultDropdownDefinitions(e)],this.localizedDefaultStylesTitles);for(const e of n)this._createDropdown(e,i)}_createDropdown(e,t){const i=this.editor.ui.componentFactory;i.add(e.name,(n=>{let s;const{defaultItem:o,items:r,title:a}=e,l=r.filter((e=>t.find((({name:t})=>NC(t)===e)))).map((e=>{const t=i.create(e);return e===o&&(s=t),t}));r.length!==l.length&&VC.warnInvalidStyle({dropdown:e});const c=Dm(n,Fm),d=c.buttonView,h=d.arrowView;return Lm(c,l,{enableActiveItemFocusOnDropdownOpen:!0}),d.set({label:FC(a,s.label),class:null,tooltip:!0}),h.unbind("label"),h.set({label:a}),d.bind("icon").toMany(l,"isOn",((...e)=>{const t=e.findIndex(Vs);return t<0?s.icon:l[t].icon})),d.bind("label").toMany(l,"isOn",((...e)=>{const t=e.findIndex(Vs);return FC(a,t<0?s.label:l[t].label)})),d.bind("isOn").toMany(l,"isOn",((...e)=>e.some(Vs))),d.bind("class").toMany(l,"isOn",((...e)=>e.some(Vs)?"ck-splitbutton_flatten":void 0)),d.on("execute",(()=>{l.some((({isOn:e})=>e))?c.isOpen=!c.isOpen:s.fire("execute")})),c.bind("isEnabled").toMany(l,"isEnabled",((...e)=>e.some(Vs))),this.listenTo(c,"execute",(()=>{this.editor.editing.view.focus()})),c}))}_createButton(e){const t=e.name;this.editor.ui.componentFactory.add(NC(t),(i=>{const n=this.editor.commands.get("imageStyle"),s=new Ku(i);return s.set({label:e.title,icon:e.icon,tooltip:!0,isToggleable:!0}),s.bind("isEnabled").to(n,"isEnabled"),s.bind("isOn").to(n,"value",(e=>e===t)),s.on("execute",this._executeCommand.bind(this,t)),s}))}_executeCommand(e){this.editor.execute("imageStyle",{value:e}),this.editor.editing.view.focus()}}function MC(e,t){for(const i of e)t[i.title]&&(i.title=t[i.title]);return e}function NC(e){return`imageStyle:${e}`}function FC(e,t){return(e?e+": ":"")+t}class DC extends so{static get pluginName(){return"IndentEditing"}init(){const e=this.editor;e.commands.add("indent",new lo(e)),e.commands.add("outdent",new lo(e))}}class LC extends so{static get pluginName(){return"IndentUI"}init(){const e=this.editor,t=e.locale,i=e.t,n="ltr"==t.uiLanguageDirection?fu.indent:fu.outdent,s="ltr"==t.uiLanguageDirection?fu.outdent:fu.indent;this._defineButton("indent",i("Increase indent"),n),this._defineButton("outdent",i("Decrease indent"),s)}_defineButton(e,t,i){const n=this.editor;n.ui.componentFactory.add(e,(()=>{const n=this._createButton(Ku,e,t,i);return n.set({tooltip:!0}),n})),n.ui.componentFactory.add("menuBar:"+e,(()=>this._createButton(up,e,t,i)))}_createButton(e,t,i,n){const s=this.editor,o=s.commands.get(t),r=new e(s.locale);return r.set({label:i,icon:n}),r.bind("isEnabled").to(o,"isEnabled"),this.listenTo(r,"execute",(()=>{s.execute(t),s.editing.view.focus()})),r}}class zC extends ro{constructor(e,t){super(e),this._indentBehavior=t}refresh(){const e=Zs(this.editor.model.document.selection.getSelectedBlocks());e&&this._isIndentationChangeAllowed(e)?this.isEnabled=this._indentBehavior.checkEnabled(e.getAttribute("blockIndent")):this.isEnabled=!1}execute(){const e=this.editor.model,t=this._getBlocksToChange();e.change((e=>{for(const i of t){const t=i.getAttribute("blockIndent"),n=this._indentBehavior.getNextIndent(t);n?e.setAttribute("blockIndent",n,i):e.removeAttribute("blockIndent",i)}}))}_getBlocksToChange(){const e=this.editor.model.document.selection;return Array.from(e.getSelectedBlocks()).filter((e=>this._isIndentationChangeAllowed(e)))}_isIndentationChangeAllowed(e){const t=this.editor;if(!t.model.schema.checkAttribute(e,"blockIndent"))return!1;if(!t.plugins.has("ListUtils"))return!0;if(!this._indentBehavior.isForward)return!0;return!t.plugins.get("ListUtils").isListItemBlock(e)}}class HC{constructor(e){this.isForward="forward"===e.direction,this.offset=e.offset,this.unit=e.unit}checkEnabled(e){const t=parseFloat(e||"0");return this.isForward||t>0}getNextIndent(e){const t=parseFloat(e||"0");if(!(!e||e.endsWith(this.unit)))return this.isForward?this.offset+this.unit:void 0;const i=t+(this.isForward?this.offset:-this.offset);return i>0?i+this.unit:void 0}}class $C{constructor(e){this.isForward="forward"===e.direction,this.classes=e.classes}checkEnabled(e){const t=this.classes.indexOf(e);return this.isForward?t=0}getNextIndent(e){const t=this.classes.indexOf(e),i=this.isForward?1:-1;return this.classes[t+i]}}const WC=["paragraph","heading1","heading2","heading3","heading4","heading5","heading6"];class jC{constructor(){this._definitions=new Set}get length(){return this._definitions.size}add(e){Array.isArray(e)?e.forEach((e=>this._definitions.add(e))):this._definitions.add(e)}getDispatcher(){return e=>{e.on("attribute:linkHref",((e,t,i)=>{if(!i.consumable.test(t.item,"attribute:linkHref"))return;if(!t.item.is("selection")&&!i.schema.isInline(t.item))return;const n=i.writer,s=n.document.selection;for(const e of this._definitions){const o=n.createAttributeElement("a",e.attributes,{priority:5});e.classes&&n.addClass(e.classes,o);for(const t in e.styles)n.setStyle(t,e.styles[t],o);n.setCustomProperty("link",!0,o),e.callback(t.attributeNewValue)?t.item.is("selection")?n.wrap(s.getFirstRange(),o):n.wrap(i.mapper.toViewRange(t.range),o):n.unwrap(i.mapper.toViewRange(t.range),o)}}),{priority:"high"})}}getDispatcherForLinkedImage(){return e=>{e.on("attribute:linkHref:imageBlock",((e,t,{writer:i,mapper:n})=>{const s=n.toViewElement(t.item),o=Array.from(s.getChildren()).find((e=>e.is("element","a")));for(const e of this._definitions){const n=Ys(e.attributes);if(e.callback(t.attributeNewValue)){for(const[e,t]of n)"class"===e?i.addClass(t,o):i.setAttribute(e,t,o);e.classes&&i.addClass(e.classes,o);for(const t in e.styles)i.setStyle(t,e.styles[t],o)}else{for(const[e,t]of n)"class"===e?i.removeClass(t,o):i.removeAttribute(e,o);e.classes&&i.removeClass(e.classes,o);for(const t in e.styles)i.removeStyle(t,o)}}}))}}}const UC=/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205f\u3000]/g,qC=/^[\S]+@((?![-_])(?:[-\w\u00a1-\uffff]{0,63}[^-_]\.))+(?:[a-z\u00a1-\uffff]{2,})$/i,GC=/^((\w+:(\/{2,})?)|(\W))/i,KC=["https?","ftps?","mailto"],ZC="Ctrl+K";function JC(e,{writer:t}){const i=t.createAttributeElement("a",{href:e},{priority:5});return t.setCustomProperty("link",!0,i),i}function QC(e,t=KC){const i=String(e),n=t.join("|");return function(e,t){const i=e.replace(UC,"");return!!i.match(t)}(i,new RegExp(`${"^(?:(?:):|[^a-z]|[a-z+.-]+(?:[^a-z+.:-]|$))".replace("",n)}`,"i"))?i:"#"}function YC(e,t){return!!e&&t.checkAttribute(e.name,"linkHref")}function XC(e,t){const i=(n=e,qC.test(n)?"mailto:":t);var n;const s=!!i&&!eA(e);return e&&s?i+e:e}function eA(e){return GC.test(e)}function tA(e){window.open(e,"_blank","noopener")}class iA extends ro{constructor(){super(...arguments),this.manualDecorators=new Ks,this.automaticDecorators=new jC}restoreManualDecoratorStates(){for(const e of this.manualDecorators)e.value=this._getDecoratorStateFromModel(e.id)}refresh(){const e=this.editor.model,t=e.document.selection,i=t.getSelectedElement()||Zs(t.getSelectedBlocks());YC(i,e.schema)?(this.value=i.getAttribute("linkHref"),this.isEnabled=e.schema.checkAttribute(i,"linkHref")):(this.value=t.getAttribute("linkHref"),this.isEnabled=e.schema.checkAttributeInSelection(t,"linkHref"));for(const e of this.manualDecorators)e.value=this._getDecoratorStateFromModel(e.id)}execute(e,t={}){const i=this.editor.model,n=i.document.selection,s=[],o=[];for(const e in t)t[e]?s.push(e):o.push(e);i.change((t=>{if(n.isCollapsed){const r=n.getFirstPosition();if(n.hasAttribute("linkHref")){const a=nA(n);let l=pb(r,"linkHref",n.getAttribute("linkHref"),i);n.getAttribute("linkHref")===a&&(l=this._updateLinkContent(i,t,l,e)),t.setAttribute("linkHref",e,l),s.forEach((e=>{t.setAttribute(e,!0,l)})),o.forEach((e=>{t.removeAttribute(e,l)})),t.setSelection(t.createPositionAfter(l.end.nodeBefore))}else if(""!==e){const o=Ys(n.getAttributes());o.set("linkHref",e),s.forEach((e=>{o.set(e,!0)}));const{end:a}=i.insertContent(t.createText(e,o),r);t.setSelection(a)}["linkHref",...s,...o].forEach((e=>{t.removeSelectionAttribute(e)}))}else{const r=i.schema.getValidRanges(n.getRanges(),"linkHref"),a=[];for(const e of n.getSelectedBlocks())i.schema.checkAttribute(e,"linkHref")&&a.push(t.createRangeOn(e));const l=a.slice();for(const e of r)this._isRangeToUpdate(e,a)&&l.push(e);for(const r of l){let a=r;if(1===l.length){const s=nA(n);n.getAttribute("linkHref")===s&&(a=this._updateLinkContent(i,t,r,e),t.setSelection(t.createSelection(a)))}t.setAttribute("linkHref",e,a),s.forEach((e=>{t.setAttribute(e,!0,a)})),o.forEach((e=>{t.removeAttribute(e,a)}))}}}))}_getDecoratorStateFromModel(e){const t=this.editor.model,i=t.document.selection,n=i.getSelectedElement();return YC(n,t.schema)?n.getAttribute(e):i.getAttribute(e)}_isRangeToUpdate(e,t){for(const i of t)if(i.containsRange(e))return!1;return!0}_updateLinkContent(e,t,i,n){const s=t.createText(n,{linkHref:n});return e.insertContent(s,i)}}function nA(e){if(e.isCollapsed){const t=e.getFirstPosition();return t.textNode&&t.textNode.data}{const t=Array.from(e.getFirstRange().getItems());if(t.length>1)return null;const i=t[0];return i.is("$text")||i.is("$textProxy")?i.data:null}}class sA extends ro{refresh(){const e=this.editor.model,t=e.document.selection,i=t.getSelectedElement();YC(i,e.schema)?this.isEnabled=e.schema.checkAttribute(i,"linkHref"):this.isEnabled=e.schema.checkAttributeInSelection(t,"linkHref")}execute(){const e=this.editor,t=this.editor.model,i=t.document.selection,n=e.commands.get("link");t.change((e=>{const s=i.isCollapsed?[pb(i.getFirstPosition(),"linkHref",i.getAttribute("linkHref"),t)]:t.schema.getValidRanges(i.getRanges(),"linkHref");for(const t of s)if(e.removeAttribute("linkHref",t),n)for(const i of n.manualDecorators)e.removeAttribute(i.id,t)}))}}class oA extends(G()){constructor({id:e,label:t,attributes:i,classes:n,styles:s,defaultValue:o}){super(),this.id=e,this.set("value",void 0),this.defaultValue=o,this.label=t,this.attributes=i,this.classes=n,this.styles=s}_createPattern(){return{attributes:this.attributes,classes:this.classes,styles:this.styles}}}const rA="automatic",aA=/^(https?:)?\/\//;class lA extends so{static get pluginName(){return"LinkEditing"}static get requires(){return[ib,Wp,Sw]}constructor(e){super(e),e.config.define("link",{allowCreatingEmptyLinks:!1,addTargetToExternalLinks:!1})}init(){const e=this.editor,t=this.editor.config.get("link.allowedProtocols");e.model.schema.extend("$text",{allowAttributes:"linkHref"}),e.conversion.for("dataDowncast").attributeToElement({model:"linkHref",view:JC}),e.conversion.for("editingDowncast").attributeToElement({model:"linkHref",view:(e,i)=>JC(QC(e,t),i)}),e.conversion.for("upcast").elementToAttribute({view:{name:"a",attributes:{href:!0}},model:{key:"linkHref",value:e=>e.getAttribute("href")}}),e.commands.add("link",new iA(e)),e.commands.add("unlink",new sA(e));const i=function(e,t){const i={"Open in a new tab":e("Open in a new tab"),Downloadable:e("Downloadable")};return t.forEach((e=>("label"in e&&i[e.label]&&(e.label=i[e.label]),e))),t}(e.t,function(e){const t=[];if(e)for(const[i,n]of Object.entries(e)){const e=Object.assign({},n,{id:`link${Ty(i)}`});t.push(e)}return t}(e.config.get("link.decorators")));this._enableAutomaticDecorators(i.filter((e=>e.mode===rA))),this._enableManualDecorators(i.filter((e=>"manual"===e.mode)));e.plugins.get(ib).registerAttribute("linkHref"),wb(e,"linkHref","a","ck-link_selected"),this._enableLinkOpen(),this._enableSelectionAttributesFixer(),this._enableClipboardIntegration()}_enableAutomaticDecorators(e){const t=this.editor,i=t.commands.get("link").automaticDecorators;t.config.get("link.addTargetToExternalLinks")&&i.add({id:"linkIsExternal",mode:rA,callback:e=>!!e&&aA.test(e),attributes:{target:"_blank",rel:"noopener noreferrer"}}),i.add(e),i.length&&t.conversion.for("downcast").add(i.getDispatcher())}_enableManualDecorators(e){if(!e.length)return;const t=this.editor,i=t.commands.get("link").manualDecorators;e.forEach((e=>{t.model.schema.extend("$text",{allowAttributes:e.id});const n=new oA(e);i.add(n),t.conversion.for("downcast").attributeToElement({model:n.id,view:(e,{writer:t,schema:i},{item:s})=>{if((s.is("selection")||i.isInline(s))&&e){const e=t.createAttributeElement("a",n.attributes,{priority:5});n.classes&&t.addClass(n.classes,e);for(const i in n.styles)t.setStyle(i,n.styles[i],e);return t.setCustomProperty("link",!0,e),e}}}),t.conversion.for("upcast").elementToAttribute({view:{name:"a",...n._createPattern()},model:{key:n.id}})}))}_enableLinkOpen(){const e=this.editor,t=e.editing.view.document;this.listenTo(t,"click",((e,t)=>{if(!(l.isMac?t.domEvent.metaKey:t.domEvent.ctrlKey))return;let i=t.domTarget;if("a"!=i.tagName.toLowerCase()&&(i=i.closest("a")),!i)return;const n=i.getAttribute("href");n&&(e.stop(),t.preventDefault(),tA(n))}),{context:"$capture"}),this.listenTo(t,"keydown",((t,i)=>{const n=e.commands.get("link").value;!!n&&i.keyCode===bs.enter&&i.altKey&&(t.stop(),tA(n))}))}_enableSelectionAttributesFixer(){const e=this.editor.model,t=e.document.selection;this.listenTo(t,"change:attribute",((i,{attributeKeys:n})=>{n.includes("linkHref")&&!t.hasAttribute("linkHref")&&e.change((t=>{var i;!function(e,t){e.removeSelectionAttribute("linkHref");for(const i of t)e.removeSelectionAttribute(i)}(t,(i=e.schema,i.getDefinition("$text").allowAttributes.filter((e=>e.startsWith("link")))))}))}))}_enableClipboardIntegration(){const e=this.editor,t=e.model,i=this.editor.config.get("link.defaultProtocol");i&&this.listenTo(e.plugins.get("ClipboardPipeline"),"contentInsertion",((e,n)=>{t.change((e=>{const t=e.createRangeIn(n.content);for(const n of t.getItems())if(n.hasAttribute("linkHref")){const t=XC(n.getAttribute("linkHref"),i);e.setAttribute("linkHref",t,n)}}))}))}}class cA extends Du{constructor(e,t){super(e),this.focusTracker=new Js,this.keystrokes=new Qs,this._focusables=new pu;const i=e.t;this.urlInputView=this._createUrlInput(),this.saveButtonView=this._createButton(i("Save"),fu.check,"ck-button-save"),this.saveButtonView.type="submit",this.cancelButtonView=this._createButton(i("Cancel"),fu.cancel,"ck-button-cancel","cancel"),this._manualDecoratorSwitches=this._createManualDecoratorSwitches(t),this.children=this._createFormChildren(t.manualDecorators),this._focusCycler=new ym({focusables:this._focusables,focusTracker:this.focusTracker,keystrokeHandler:this.keystrokes,actions:{focusPrevious:"shift + tab",focusNext:"tab"}});const n=["ck","ck-link-form","ck-responsive-form"];t.manualDecorators.length&&n.push("ck-link-form_layout-vertical","ck-vertical-form"),this.setTemplate({tag:"form",attributes:{class:n,tabindex:"-1"},children:this.children})}getDecoratorSwitchesState(){return Array.from(this._manualDecoratorSwitches).reduce(((e,t)=>(e[t.name]=t.isOn,e)),{})}render(){super.render(),i({view:this});[this.urlInputView,...this._manualDecoratorSwitches,this.saveButtonView,this.cancelButtonView].forEach((e=>{this._focusables.add(e),this.focusTracker.add(e.element)})),this.keystrokes.listenTo(this.element)}destroy(){super.destroy(),this.focusTracker.destroy(),this.keystrokes.destroy()}focus(){this._focusCycler.focusFirst()}_createUrlInput(){const e=this.locale.t,t=new um(this.locale,Um);return t.label=e("Link URL"),t}_createButton(e,t,i,n){const s=new Ku(this.locale);return s.set({label:e,icon:t,tooltip:!0}),s.extendTemplate({attributes:{class:i}}),n&&s.delegate("execute").to(this,n),s}_createManualDecoratorSwitches(e){const t=this.createCollection();for(const i of e.manualDecorators){const n=new Zu(this.locale);n.set({name:i.id,label:i.label,withText:!0}),n.bind("isOn").toMany([i,e],"value",((e,t)=>void 0===t&&void 0===e?!!i.defaultValue:!!e)),n.on("execute",(()=>{i.set("value",!n.isOn)})),t.add(n)}return t}_createFormChildren(e){const t=this.createCollection();if(t.add(this.urlInputView),e.length){const e=new Du;e.setTemplate({tag:"ul",children:this._manualDecoratorSwitches.map((e=>({tag:"li",children:[e],attributes:{class:["ck","ck-list__item"]}}))),attributes:{class:["ck","ck-reset","ck-list"]}}),t.add(e)}return t.add(this.saveButtonView),t.add(this.cancelButtonView),t}}class dA extends Du{constructor(e,t={}){super(e),this.focusTracker=new Js,this.keystrokes=new Qs,this._focusables=new pu;const i=e.t;this.previewButtonView=this._createPreviewButton(),this.unlinkButtonView=this._createButton(i("Unlink"),'',"unlink"),this.editButtonView=this._createButton(i("Edit link"),fu.pencil,"edit"),this.set("href",void 0),this._linkConfig=t,this._focusCycler=new ym({focusables:this._focusables,focusTracker:this.focusTracker,keystrokeHandler:this.keystrokes,actions:{focusPrevious:"shift + tab",focusNext:"tab"}}),this.setTemplate({tag:"div",attributes:{class:["ck","ck-link-actions","ck-responsive-form"],tabindex:"-1"},children:[this.previewButtonView,this.editButtonView,this.unlinkButtonView]})}render(){super.render();[this.previewButtonView,this.editButtonView,this.unlinkButtonView].forEach((e=>{this._focusables.add(e),this.focusTracker.add(e.element)})),this.keystrokes.listenTo(this.element)}destroy(){super.destroy(),this.focusTracker.destroy(),this.keystrokes.destroy()}focus(){this._focusCycler.focusFirst()}_createButton(e,t,i){const n=new Ku(this.locale);return n.set({label:e,icon:t,tooltip:!0}),n.delegate("execute").to(this,i),n}_createPreviewButton(){const e=new Ku(this.locale),t=this.bindTemplate,i=this.t;return e.set({withText:!0,tooltip:i("Open link in new tab")}),e.extendTemplate({attributes:{class:["ck","ck-link-actions__preview"],href:t.to("href",(e=>e&&QC(e,this._linkConfig.allowedProtocols))),target:"_blank",rel:"noopener noreferrer"}}),e.bind("label").to(this,"href",(e=>e||i("This link has no URL"))),e.bind("isEnabled").to(this,"href",(e=>!!e)),e.template.tag="a",e.template.eventListeners={},e}}const hA='',uA="link-ui";class mA extends so{constructor(){super(...arguments),this.actionsView=null,this.formView=null}static get requires(){return[If]}static get pluginName(){return"LinkUI"}init(){const e=this.editor,t=this.editor.t;e.editing.view.addObserver(wh),this._balloon=e.plugins.get(If),this._createToolbarLinkButton(),this._enableBalloonActivators(),e.conversion.for("editingDowncast").markerToHighlight({model:uA,view:{classes:["ck-fake-link-selection"]}}),e.conversion.for("editingDowncast").markerToElement({model:uA,view:{name:"span",classes:["ck-fake-link-selection","ck-fake-link-selection_collapsed"]}}),e.accessibility.addKeystrokeInfos({keystrokes:[{label:t("Create link"),keystroke:ZC},{label:t("Move out of a link"),keystroke:[["arrowleft","arrowleft"],["arrowright","arrowright"]]}]})}destroy(){super.destroy(),this.formView&&this.formView.destroy(),this.actionsView&&this.actionsView.destroy()}_createViews(){this.actionsView=this._createActionsView(),this.formView=this._createFormView(),this._enableUserBalloonInteractions()}_createActionsView(){const e=this.editor,t=new dA(e.locale,e.config.get("link")),i=e.commands.get("link"),n=e.commands.get("unlink");return t.bind("href").to(i,"value"),t.editButtonView.bind("isEnabled").to(i),t.unlinkButtonView.bind("isEnabled").to(n),this.listenTo(t,"edit",(()=>{this._addFormView()})),this.listenTo(t,"unlink",(()=>{e.execute("unlink"),this._hideUI()})),t.keystrokes.set("Esc",((e,t)=>{this._hideUI(),t()})),t.keystrokes.set(ZC,((e,t)=>{this._addFormView(),t()})),t}_createFormView(){const e=this.editor,i=e.commands.get("link"),n=e.config.get("link.defaultProtocol"),s=e.config.get("link.allowCreatingEmptyLinks"),o=new(t(cA))(e.locale,i);return o.urlInputView.fieldView.bind("value").to(i,"value"),o.urlInputView.bind("isEnabled").to(i,"isEnabled"),o.saveButtonView.bind("isEnabled").to(i,"isEnabled",o.urlInputView,"isEmpty",((e,t)=>e&&(s||!t))),this.listenTo(o,"submit",(()=>{const{value:t}=o.urlInputView.fieldView.element,i=XC(t,n);e.execute("link",i,o.getDecoratorSwitchesState()),this._closeFormView()})),this.listenTo(o,"cancel",(()=>{this._closeFormView()})),o.keystrokes.set("Esc",((e,t)=>{this._closeFormView(),t()})),o}_createToolbarLinkButton(){const e=this.editor,t=e.commands.get("link");e.ui.componentFactory.add("link",(()=>{const e=this._createButton(Ku);return e.set({tooltip:!0,isToggleable:!0}),e.bind("isOn").to(t,"value",(e=>!!e)),e})),e.ui.componentFactory.add("menuBar:link",(()=>this._createButton(up)))}_createButton(e){const t=this.editor,i=t.locale,n=t.commands.get("link"),s=new e(t.locale),o=i.t;return s.set({label:o("Link"),icon:hA,keystroke:ZC}),s.bind("isEnabled").to(n,"isEnabled"),this.listenTo(s,"execute",(()=>this._showUI(!0))),s}_enableBalloonActivators(){const e=this.editor,t=e.editing.view.document;this.listenTo(t,"click",(()=>{this._getSelectedLinkElement()&&this._showUI()})),e.keystrokes.set(ZC,((t,i)=>{i(),e.commands.get("link").isEnabled&&this._showUI(!0)}))}_enableUserBalloonInteractions(){this.editor.keystrokes.set("Tab",((e,t)=>{this._areActionsVisible&&!this.actionsView.focusTracker.isFocused&&(this.actionsView.focus(),t())}),{priority:"high"}),this.editor.keystrokes.set("Esc",((e,t)=>{this._isUIVisible&&(this._hideUI(),t())})),e({emitter:this.formView,activator:()=>this._isUIInPanel,contextElements:()=>[this._balloon.view.element],callback:()=>this._hideUI()})}_addActionsView(){this.actionsView||this._createViews(),this._areActionsInPanel||this._balloon.add({view:this.actionsView,position:this._getBalloonPositionData()})}_addFormView(){if(this.formView||this._createViews(),this._isFormInPanel)return;const e=this.editor.commands.get("link");this.formView.disableCssTransitions(),this._balloon.add({view:this.formView,position:this._getBalloonPositionData()}),this.formView.urlInputView.fieldView.value=e.value||"",this._balloon.visibleView===this.formView&&this.formView.urlInputView.fieldView.select(),this.formView.enableCssTransitions()}_closeFormView(){const e=this.editor.commands.get("link");e.restoreManualDecoratorStates(),void 0!==e.value?this._removeFormView():this._hideUI()}_removeFormView(){this._isFormInPanel&&(this.formView.saveButtonView.focus(),this.formView.urlInputView.fieldView.reset(),this._balloon.remove(this.formView),this.editor.editing.view.focus(),this._hideFakeVisualSelection())}_showUI(e=!1){this.formView||this._createViews(),this._getSelectedLinkElement()?(this._areActionsVisible?this._addFormView():this._addActionsView(),e&&this._balloon.showStack("main")):(this._showFakeVisualSelection(),this._addActionsView(),e&&this._balloon.showStack("main"),this._addFormView()),this._startUpdatingUI()}_hideUI(){if(!this._isUIInPanel)return;const e=this.editor;this.stopListening(e.ui,"update"),this.stopListening(this._balloon,"change:visibleView"),e.editing.view.focus(),this._removeFormView(),this._balloon.remove(this.actionsView),this._hideFakeVisualSelection()}_startUpdatingUI(){const e=this.editor,t=e.editing.view.document;let i=this._getSelectedLinkElement(),n=o();const s=()=>{const e=this._getSelectedLinkElement(),t=o();i&&!e||!i&&t!==n?this._hideUI():this._isUIVisible&&this._balloon.updatePosition(this._getBalloonPositionData()),i=e,n=t};function o(){return t.selection.focus.getAncestors().reverse().find((e=>e.is("element")))}this.listenTo(e.ui,"update",s),this.listenTo(this._balloon,"change:visibleView",s)}get _isFormInPanel(){return!!this.formView&&this._balloon.hasView(this.formView)}get _areActionsInPanel(){return!!this.actionsView&&this._balloon.hasView(this.actionsView)}get _areActionsVisible(){return!!this.actionsView&&this._balloon.visibleView===this.actionsView}get _isUIInPanel(){return this._isFormInPanel||this._areActionsInPanel}get _isUIVisible(){const e=this._balloon.visibleView;return!!this.formView&&e==this.formView||this._areActionsVisible}_getBalloonPositionData(){const e=this.editor.editing.view,t=this.editor.model,i=e.document;let n;if(t.markers.has(uA)){const t=Array.from(this.editor.editing.mapper.markerNameToElements(uA)),i=e.createRange(e.createPositionBefore(t[0]),e.createPositionAfter(t[t.length-1]));n=e.domConverter.viewRangeToDom(i)}else n=()=>{const t=this._getSelectedLinkElement();return t?e.domConverter.mapViewToDom(t):e.domConverter.viewRangeToDom(i.selection.getFirstRange())};return{target:n}}_getSelectedLinkElement(){const e=this.editor.editing.view,t=e.document.selection,i=t.getSelectedElement();if(t.isCollapsed||i&&Nw(i))return gA(t.getFirstPosition());{const i=t.getFirstRange().getTrimmed(),n=gA(i.start),s=gA(i.end);return n&&n==s&&e.createRangeIn(n).getTrimmed().isEqual(i)?n:null}}_showFakeVisualSelection(){const e=this.editor.model;e.change((t=>{const i=e.document.selection.getFirstRange();if(e.markers.has(uA))t.updateMarker(uA,{range:i});else if(i.start.isAtEnd){const n=i.start.getLastMatchingPosition((({item:t})=>!e.schema.isContent(t)),{boundaries:i});t.addMarker(uA,{usingOperation:!1,affectsData:!1,range:t.createRange(n,i.end)})}else t.addMarker(uA,{usingOperation:!1,affectsData:!1,range:i})}))}_hideFakeVisualSelection(){const e=this.editor.model;e.markers.has(uA)&&e.change((e=>{e.removeMarker(uA)}))}}function gA(e){return e.getAncestors().find((e=>{return(t=e).is("attributeElement")&&!!t.getCustomProperty("link");var t}))||null}const fA=new RegExp("(^|\\s)(((?:(?:(?:https?|ftp):)?\\/\\/)(?:\\S+(?::\\S*)?@)?(?:(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}(?:\\.(?:[1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))|(((?!www\\.)|(www\\.))(?![-_])(?:[-_a-z0-9\\u00a1-\\uffff]{1,63}\\.)+(?:[a-z\\u00a1-\\uffff]{2,63})))(?::\\d{2,5})?(?:[/?#]\\S*)?)|((www.|(\\S+@))((?![-_])(?:[-_a-z0-9\\u00a1-\\uffff]{1,63}\\.))+(?:[a-z\\u00a1-\\uffff]{2,63})))$","i");class pA extends so{static get requires(){return[Yp,lA]}static get pluginName(){return"AutoLink"}init(){const e=this.editor.model.document.selection;e.on("change:range",(()=>{this.isEnabled=!e.anchor.parent.is("element","codeBlock")})),this._enableTypingHandling()}afterInit(){this._enableEnterHandling(),this._enableShiftEnterHandling(),this._enablePasteLinking()}_expandLinkRange(e,t){return t.textNode&&t.textNode.hasAttribute("linkHref")?pb(t,"linkHref",t.textNode.getAttribute("linkHref"),e):null}_selectEntireLinks(e,t){const i=this.editor.model,n=i.document.selection,s=n.getFirstPosition(),o=n.getLastPosition();let r=t.getJoined(this._expandLinkRange(i,s)||t);r&&(r=r.getJoined(this._expandLinkRange(i,o)||t)),r&&(r.start.isBefore(s)||r.end.isAfter(o))&&e.setSelection(r)}_enablePasteLinking(){const e=this.editor,t=e.model,i=t.document.selection,n=e.plugins.get("ClipboardPipeline"),s=e.commands.get("link");n.on("inputTransformation",((e,n)=>{if(!this.isEnabled||!s.isEnabled||i.isCollapsed||"paste"!==n.method)return;if(i.rangeCount>1)return;const o=i.getFirstRange(),r=n.dataTransfer.getData("text/plain");if(!r)return;const a=r.match(fA);a&&a[2]===r&&(t.change((e=>{this._selectEntireLinks(e,o),s.execute(r)})),e.stop())}),{priority:"high"})}_enableTypingHandling(){const e=this.editor,t=new tb(e.model,(e=>{if(!function(e){return e.length>4&&" "===e[e.length-1]&&" "!==e[e.length-2]}(e))return;const t=bA(e.substr(0,e.length-1));return t?{url:t}:void 0}));t.on("matched:data",((t,i)=>{const{batch:n,range:s,url:o}=i;if(!n.isTyping)return;const r=s.end.getShiftedBy(-1),a=r.getShiftedBy(-o.length),l=e.model.createRange(a,r);this._applyAutoLink(o,l)})),t.bind("isEnabled").to(this)}_enableEnterHandling(){const e=this.editor,t=e.model,i=e.commands.get("enter");i&&i.on("execute",(()=>{const e=t.document.selection.getFirstPosition();if(!e.parent.previousSibling)return;const i=t.createRangeIn(e.parent.previousSibling);this._checkAndApplyAutoLinkOnRange(i)}))}_enableShiftEnterHandling(){const e=this.editor,t=e.model,i=e.commands.get("shiftEnter");i&&i.on("execute",(()=>{const e=t.document.selection.getFirstPosition(),i=t.createRange(t.createPositionAt(e.parent,0),e.getShiftedBy(-1));this._checkAndApplyAutoLinkOnRange(i)}))}_checkAndApplyAutoLinkOnRange(e){const t=this.editor.model,{text:i,range:n}=eb(e,t),s=bA(i);if(s){const e=t.createRange(n.end.getShiftedBy(-s.length),n.end);this._applyAutoLink(s,e)}}_applyAutoLink(e,t){const i=this.editor.model,n=XC(e,this.editor.config.get("link.defaultProtocol"));this.isEnabled&&function(e,t){return t.schema.checkAttributeInSelection(t.createSelection(e),"linkHref")}(t,i)&&eA(n)&&!function(e){const t=e.start.nodeAfter;return!!t&&t.hasAttribute("linkHref")}(t)&&this._persistAutoLink(n,t)}_persistAutoLink(e,t){const i=this.editor.model,n=this.editor.plugins.get("Delete");i.enqueueChange((s=>{s.setAttribute("linkHref",e,t),i.enqueueChange((()=>{n.requestUndoOnBackspace()}))}))}}function bA(e){const t=fA.exec(e);return t?t[2]:null}class wA extends so{static get requires(){return["ImageEditing","ImageUtils",lA]}static get pluginName(){return"LinkImageEditing"}afterInit(){const e=this.editor,t=e.model.schema;e.plugins.has("ImageBlockEditing")&&t.extend("imageBlock",{allowAttributes:["linkHref"]}),e.conversion.for("upcast").add(function(e){const t=e.plugins.has("ImageInlineEditing"),i=e.plugins.get("ImageUtils");return e=>{e.on("element:a",((e,n,s)=>{const o=n.viewItem,r=i.findViewImgElement(o);if(!r)return;const a=r.findAncestor((e=>i.isBlockImageView(e)));if(t&&!a)return;const l={attributes:["href"]};if(!s.consumable.consume(o,l))return;const c=o.getAttribute("href");if(!c)return;let d=n.modelCursor.parent;if(!d.is("element","imageBlock")){const e=s.convertItem(r,n.modelCursor);n.modelRange=e.modelRange,n.modelCursor=e.modelCursor,d=n.modelCursor.nodeBefore}d&&d.is("element","imageBlock")&&s.writer.setAttribute("linkHref",c,d)}),{priority:"high"})}}(e)),e.conversion.for("downcast").add(function(e){const t=e.plugins.get("ImageUtils");return e=>{e.on("attribute:linkHref:imageBlock",((e,i,n)=>{if(!n.consumable.consume(i.item,e.name))return;const s=n.mapper.toViewElement(i.item),o=n.writer,r=Array.from(s.getChildren()).find((e=>e.is("element","a"))),a=t.findViewImgElement(s),l=a.parent.is("element","picture")?a.parent:a;if(r)i.attributeNewValue?o.setAttribute("href",i.attributeNewValue,r):(o.move(o.createRangeOn(l),o.createPositionAt(s,0)),o.remove(r));else{const e=o.createContainerElement("a",{href:i.attributeNewValue});o.insert(o.createPositionAt(s,0),e),o.move(o.createRangeOn(l),o.createPositionAt(e,0))}}),{priority:"high"})}}(e)),this._enableAutomaticDecorators(),this._enableManualDecorators()}_enableAutomaticDecorators(){const e=this.editor,t=e.commands.get("link").automaticDecorators;t.length&&e.conversion.for("downcast").add(t.getDispatcherForLinkedImage())}_enableManualDecorators(){const e=this.editor,t=e.commands.get("link");for(const i of t.manualDecorators)e.plugins.has("ImageBlockEditing")&&e.model.schema.extend("imageBlock",{allowAttributes:i.id}),e.plugins.has("ImageInlineEditing")&&e.model.schema.extend("imageInline",{allowAttributes:i.id}),e.conversion.for("downcast").add(vA(i)),e.conversion.for("upcast").add(_A(e,i))}}function vA(e){return t=>{t.on(`attribute:${e.id}:imageBlock`,((t,i,n)=>{const s=n.mapper.toViewElement(i.item),o=Array.from(s.getChildren()).find((e=>e.is("element","a")));if(o){for(const[t,i]of Ys(e.attributes))n.writer.setAttribute(t,i,o);e.classes&&n.writer.addClass(e.classes,o);for(const t in e.styles)n.writer.setStyle(t,e.styles[t],o)}}))}}function _A(e,t){const i=e.plugins.has("ImageInlineEditing"),n=e.plugins.get("ImageUtils");return e=>{e.on("element:a",((e,s,o)=>{const r=s.viewItem,a=n.findViewImgElement(r);if(!a)return;const l=a.findAncestor((e=>n.isBlockImageView(e)));if(i&&!l)return;const c=new To(t._createPattern()).match(r);if(!c)return;if(!o.consumable.consume(r,c.match))return;const d=s.modelCursor.nodeBefore||s.modelCursor.parent;o.writer.setAttribute(t.id,!0,d)}),{priority:"high"})}}class yA extends so{static get requires(){return[lA,mA,"ImageBlockEditing"]}static get pluginName(){return"LinkImageUI"}init(){const e=this.editor,t=e.editing.view.document;this.listenTo(t,"click",((t,i)=>{this._isSelectedLinkedImage(e.model.document.selection)&&(i.preventDefault(),t.stop())}),{priority:"high"}),this._createToolbarLinkImageButton()}_createToolbarLinkImageButton(){const e=this.editor,t=e.t;e.ui.componentFactory.add("linkImage",(i=>{const n=new Ku(i),s=e.plugins.get("LinkUI"),o=e.commands.get("link");return n.set({isEnabled:!0,label:t("Link image"),icon:hA,keystroke:ZC,tooltip:!0,isToggleable:!0}),n.bind("isEnabled").to(o,"isEnabled"),n.bind("isOn").to(o,"value",(e=>!!e)),this.listenTo(n,"execute",(()=>{this._isSelectedLinkedImage(e.model.document.selection)?s._addActionsView():s._showUI(!0)})),n}))}_isSelectedLinkedImage(e){const t=e.getSelectedElement();return this.editor.plugins.get("ImageUtils").isImage(t)&&t.hasAttribute("linkHref")}}class kA{constructor(e,t){this._startElement=e,this._referenceIndent=e.getAttribute("listIndent"),this._isForward="forward"==t.direction,this._includeSelf=!!t.includeSelf,this._sameAttributes=Cs(t.sameAttributes||[]),this._sameIndent=!!t.sameIndent,this._lowerIndent=!!t.lowerIndent,this._higherIndent=!!t.higherIndent}static first(e,t){return Zs(new this(e,t)[Symbol.iterator]())}*[Symbol.iterator](){const e=[];for(const{node:t}of CA(this._getStartNode(),this._isForward?"forward":"backward")){const i=t.getAttribute("listIndent");if(ithis._referenceIndent){if(!this._higherIndent)continue;if(!this._isForward){e.push(t);continue}}else{if(!this._sameIndent){if(this._higherIndent){e.length&&(yield*e,e.length=0);break}continue}if(this._sameAttributes.some((e=>t.getAttribute(e)!==this._startElement.getAttribute(e))))break}e.length&&(yield*e,e.length=0),yield t}}_getStartNode(){return this._includeSelf?this._startElement:this._isForward?this._startElement.nextSibling:this._startElement.previousSibling}}function*CA(e,t="forward"){const i="forward"==t,n=[];let s=null;for(;EA(e);){let t=null;if(s){const i=e.getAttribute("listIndent"),o=s.getAttribute("listIndent");i>o?n[o]=s:ie.getAttribute("listItemId")!=t))}function LA(e){return Array.from(e).filter((e=>"$graveyard"!==e.root.rootName)).sort(((e,t)=>e.index-t.index))}function zA(e){const t=e.document.selection.getSelectedElement();return t&&e.schema.isObject(t)&&e.schema.isBlock(t)?t:null}function HA(e,t){return t.checkChild(e.parent,"listItem")&&t.checkChild(e,"$text")&&!t.isObject(e)}function $A(e){return"numbered"==e||"customNumbered"==e}function WA(e,t,i){return SA(t,{direction:"forward"}).pop().index>e.index?MA(e,t,i):[]}class jA extends ro{constructor(e,t){super(e),this._direction=t}refresh(){this.isEnabled=this._checkEnabled()}execute(){const e=this.editor.model,t=UA(e.document.selection);e.change((e=>{const i=[];DA(t)&&!IA(t[0])?("forward"==this._direction&&i.push(...NA(t,e)),i.push(...BA(t[0],e))):"forward"==this._direction?i.push(...NA(t,e,{expand:!0})):i.push(...function(e,t){const i=RA(e=Cs(e)),n=new Set,s=Math.min(...i.map((e=>e.getAttribute("listIndent")))),o=new Map;for(const e of i)o.set(e,kA.first(e,{lowerIndent:!0}));for(const e of i){if(n.has(e))continue;n.add(e);const i=e.getAttribute("listIndent")-1;if(i<0)FA(e,t);else{if(e.getAttribute("listIndent")==s){const i=WA(e,o.get(e),t);for(const e of i)n.add(e);if(i.length)continue}t.setAttribute("listIndent",i,e)}}return LA(n)}(t,e));for(const t of i){if(!t.hasAttribute("listType"))continue;const i=kA.first(t,{sameIndent:!0});i&&e.setAttribute("listType",i.getAttribute("listType"),t)}this._fireAfterExecute(i)}))}_fireAfterExecute(e){this.fire("afterExecute",LA(new Set(e)))}_checkEnabled(){let e=UA(this.editor.model.document.selection),t=e[0];if(!t)return!1;if("backward"==this._direction)return!0;if(DA(e)&&!IA(e[0]))return!0;e=RA(e),t=e[0];const i=kA.first(t,{sameIndent:!0});return!!i&&i.getAttribute("listType")==t.getAttribute("listType")}}function UA(e){const t=Array.from(e.getSelectedBlocks()),i=t.findIndex((e=>!EA(e)));return-1!=i&&(t.length=i),t}class qA extends ro{constructor(e,t,i={}){super(e),this.type=t,this._listWalkerOptions=i.multiLevel?{higherIndent:!0,lowerIndent:!0,sameAttributes:[]}:void 0}refresh(){this.value=this._getValue(),this.isEnabled=this._checkEnabled()}execute(e={}){const t=this.editor.model,i=t.document,n=zA(t),s=Array.from(i.selection.getSelectedBlocks()).filter((e=>t.schema.checkAttribute(e,"listType")||HA(e,t.schema))),o=void 0!==e.forceValue?!e.forceValue:this.value;t.change((r=>{if(o){const e=s[s.length-1],t=SA(e,{direction:"forward"}),i=[];t.length>1&&i.push(...BA(t[1],r)),i.push(...FA(s,r)),i.push(...function(e,t){const i=[];let n=Number.POSITIVE_INFINITY;for(const{node:s}of CA(e.nextSibling,"forward")){const e=s.getAttribute("listIndent");if(0==e)break;e{const{firstElement:o,lastElement:r}=this._getMergeSubjectElements(i,e),a=o.getAttribute("listIndent")||0,l=r.getAttribute("listIndent"),c=r.getAttribute("listItemId");if(a!=l){const e=(d=r,Array.from(new kA(d,{direction:"forward",higherIndent:!0})));n.push(...NA([r,...e],s,{indentBy:a-l,expand:a{const t=BA(this._getStartBlock(),e);this._fireAfterExecute(t)}))}_fireAfterExecute(e){this.fire("afterExecute",LA(new Set(e)))}_checkEnabled(){const e=this.editor.model.document.selection,t=this._getStartBlock();return e.isCollapsed&&EA(t)&&!IA(t)}_getStartBlock(){const e=this.editor.model.document.selection.getFirstPosition().parent;return"before"==this._direction?e:e.nextSibling}}class ZA extends so{static get pluginName(){return"ListUtils"}expandListBlocksToCompleteList(e){return OA(e)}isFirstBlockOfListItem(e){return IA(e)}isListItemBlock(e){return EA(e)}expandListBlocksToCompleteItems(e,t={}){return RA(e,t)}isNumberedListType(e){return $A(e)}}function JA(e){return e.is("element","ol")||e.is("element","ul")}function QA(e){return e.is("element","li")}function YA(e,t,i,n=tx(i,t)){return e.createAttributeElement(ex(i),null,{priority:2*t/100-100,id:n})}function XA(e,t,i){return e.createAttributeElement("li",null,{priority:(2*t+1)/100-100,id:i})}function ex(e){return"numbered"==e||"customNumbered"==e?"ol":"ul"}function tx(e,t){return`list-${e}-${t}`}function ix(e,t){const i=e.nodeBefore;if(EA(i)){let e=i;for(const{node:i}of CA(e,"backward"))if(e=i,t.has(e))return;t.set(i,e)}else{const i=e.nodeAfter;EA(i)&&t.set(i,i)}}function nx(){return(e,t,i)=>{const{writer:n,schema:s}=i;if(!t.modelRange)return;const o=Array.from(t.modelRange.getItems({shallow:!0})).filter((e=>s.checkAttribute(e,"listItemId")));if(!o.length)return;const r=xA.next(),a=function(e){let t=0,i=e.parent;for(;i;){if(QA(i))t++;else{const e=i.previousSibling;e&&QA(e)&&t++}i=i.parent}return t}(t.viewItem);let l=t.viewItem.parent&&t.viewItem.parent.is("element","ol")?"numbered":"bulleted";const c=o[0].getAttribute("listType");c&&(l=c);const d={listItemId:r,listIndent:a,listType:l};for(const e of o)e.hasAttribute("listItemId")||n.setAttributes(d,e);o.length>1&&o[1].getAttribute("listItemId")!=d.listItemId&&i.keepEmptyElement(o[0])}}function sx(){return(e,t,i)=>{if(!i.consumable.test(t.viewItem,{name:!0}))return;const n=new _h(t.viewItem.document);for(const e of Array.from(t.viewItem.getChildren()))QA(e)||JA(e)||n.remove(e)}}function ox(e,t,i,{dataPipeline:n}={}){const s=function(e){return(t,i)=>{const n=[];for(const i of e)t.hasAttribute(i)&&n.push(`attribute:${i}`);return!!n.every((e=>!1!==i.test(t,e)))&&(n.forEach((e=>i.consume(t,e))),!0)}}(e);return(o,r,a)=>{const{writer:l,mapper:c,consumable:d}=a,h=r.item;if(!e.includes(r.attributeKey))return;if(!s(h,d))return;const u=function(e,t,i){const n=i.createRangeOn(e),s=t.toViewRange(n).getTrimmed();return s.end.nodeBefore}(h,c,i);ax(u,l,c),function(e,t){let i=e.parent;for(;i.is("attributeElement")&&["ul","ol","li"].includes(i.name);){const n=i.parent;t.unwrap(t.createRangeOn(e),i),i=n}}(u,l);const m=function(e,t,i,n,{dataPipeline:s}){let o=n.createRangeOn(t);if(!IA(e))return o;for(const r of i){if("itemMarker"!=r.scope)continue;const i=r.createElement(n,e,{dataPipeline:s});if(!i)continue;if(n.setCustomProperty("listItemMarker",!0,i),r.canInjectMarkerIntoElement&&r.canInjectMarkerIntoElement(e)?n.insert(n.createPositionAt(t,0),i):(n.insert(o.start,i),o=n.createRange(n.createPositionBefore(i),n.createPositionAfter(t))),!r.createWrapperElement||!r.canWrapElement)continue;const a=r.createWrapperElement(n,e,{dataPipeline:s});n.setCustomProperty("listItemWrapper",!0,a),r.canWrapElement(e)?o=n.wrap(o,a):(o=n.wrap(n.createRangeOn(i),a),o=n.createRange(o.start,n.createPositionAfter(t)))}return o}(h,u,t,l,{dataPipeline:n});!function(e,t,i,n){if(!e.hasAttribute("listIndent"))return;const s=e.getAttribute("listIndent");let o=e;for(let e=s;e>=0;e--){const s=XA(n,e,o.getAttribute("listItemId")),r=YA(n,e,o.getAttribute("listType"));for(const e of i)"list"!=e.scope&&"item"!=e.scope||!o.hasAttribute(e.attributeName)||e.setAttributeOnDowncast(n,o.getAttribute(e.attributeName),"list"==e.scope?r:s);if(t=n.wrap(t,s),t=n.wrap(t,r),0==e)break;if(o=kA.first(o,{lowerIndent:!0}),!o)break}}(h,m,t,l)}}function rx(e,{dataPipeline:t}={}){return(i,{writer:n})=>{if(!lx(i,e))return null;if(!t)return n.createContainerElement("span",{class:"ck-list-bogus-paragraph"});const s=n.createContainerElement("p");return n.setCustomProperty("dataPipeline:transparentRendering",!0,s),s}}function ax(e,t,i){for(;e.parent.is("attributeElement")&&e.parent.getCustomProperty("listItemWrapper");)t.unwrap(t.createRangeOn(e),e.parent);const n=[];s(t.createPositionBefore(e).getWalker({direction:"backward"})),s(t.createRangeIn(e).getWalker());for(const e of n)t.remove(e);function s(e){for(const{item:t}of e){if(t.is("element")&&i.toModelElement(t))break;t.is("element")&&t.getCustomProperty("listItemMarker")&&n.push(t)}}}function lx(e,t,i=TA(e)){if(!EA(e))return!1;for(const i of e.getAttributeKeys())if(!i.startsWith("selection:")&&!t.includes(i))return!1;return i.length<2}const cx=["listType","listIndent","listItemId"];class dx extends so{static get pluginName(){return"ListEditing"}static get requires(){return[Jb,Yp,ZA,Sw]}constructor(e){super(e),this._downcastStrategies=[],e.config.define("list.multiBlock",!0)}init(){const e=this.editor,t=e.model,i=e.config.get("list.multiBlock");if(e.plugins.has("LegacyListEditing"))throw new y("list-feature-conflict",this,{conflictPlugin:"LegacyListEditing"});t.schema.register("$listItem",{allowAttributes:cx}),i?(t.schema.extend("$container",{allowAttributesOf:"$listItem"}),t.schema.extend("$block",{allowAttributesOf:"$listItem"}),t.schema.extend("$blockObject",{allowAttributesOf:"$listItem"})):t.schema.register("listItem",{inheritAllFrom:"$block",allowAttributesOf:"$listItem"});for(const e of cx)t.schema.setAttributeProperties(e,{copyOnReplace:!0});e.commands.add("numberedList",new qA(e,"numbered")),e.commands.add("bulletedList",new qA(e,"bulleted")),e.commands.add("customNumberedList",new qA(e,"customNumbered",{multiLevel:!0})),e.commands.add("customBulletedList",new qA(e,"customBulleted",{multiLevel:!0})),e.commands.add("indentList",new jA(e,"forward")),e.commands.add("outdentList",new jA(e,"backward")),e.commands.add("splitListItemBefore",new KA(e,"before")),e.commands.add("splitListItemAfter",new KA(e,"after")),i&&(e.commands.add("mergeListItemBackward",new GA(e,"backward")),e.commands.add("mergeListItemForward",new GA(e,"forward"))),this._setupDeleteIntegration(),this._setupEnterIntegration(),this._setupTabIntegration(),this._setupClipboardIntegration(),this._setupAccessibilityIntegration()}afterInit(){const e=this.editor.commands,t=e.get("indent"),i=e.get("outdent");t&&t.registerChildCommand(e.get("indentList"),{priority:"high"}),i&&i.registerChildCommand(e.get("outdentList"),{priority:"lowest"}),this._setupModelPostFixing(),this._setupConversion()}registerDowncastStrategy(e){this._downcastStrategies.push(e)}getListAttributeNames(){return[...cx,...this._downcastStrategies.map((e=>e.attributeName))]}_setupDeleteIntegration(){const e=this.editor,t=e.commands.get("mergeListItemBackward"),i=e.commands.get("mergeListItemForward");this.listenTo(e.editing.view.document,"delete",((n,s)=>{const o=e.model.document.selection;zA(e.model)||e.model.change((()=>{const r=o.getFirstPosition();if(o.isCollapsed&&"backward"==s.direction){if(!r.isAtStart)return;const i=r.parent;if(!EA(i))return;if(kA.first(i,{sameAttributes:"listType",sameIndent:!0})||0!==i.getAttribute("listIndent")){if(!t||!t.isEnabled)return;t.execute({shouldMergeOnBlocksContentLevel:hx(e.model,"backward")})}else VA(i)||e.execute("splitListItemAfter"),e.execute("outdentList");s.preventDefault(),n.stop()}else{if(o.isCollapsed&&!o.getLastPosition().isAtEnd)return;if(!i||!i.isEnabled)return;i.execute({shouldMergeOnBlocksContentLevel:hx(e.model,"forward")}),s.preventDefault(),n.stop()}}))}),{context:"li"})}_setupEnterIntegration(){const e=this.editor,t=e.model,i=e.commands,n=i.get("enter");this.listenTo(e.editing.view.document,"enter",((i,n)=>{const s=t.document,o=s.selection.getFirstPosition().parent;if(s.selection.isCollapsed&&EA(o)&&o.isEmpty&&!n.isSoft){const t=IA(o),s=VA(o);t&&s?(e.execute("outdentList"),n.preventDefault(),i.stop()):t&&!s?(e.execute("splitListItemAfter"),n.preventDefault(),i.stop()):s&&(e.execute("splitListItemBefore"),n.preventDefault(),i.stop())}}),{context:"li"}),this.listenTo(n,"afterExecute",(()=>{const t=i.get("splitListItemBefore");if(t.refresh(),!t.isEnabled)return;2===TA(e.model.document.selection.getLastPosition().parent).length&&t.execute()}))}_setupTabIntegration(){const e=this.editor;this.listenTo(e.editing.view.document,"tab",((t,i)=>{const n=i.shiftKey?"outdentList":"indentList";this.editor.commands.get(n).isEnabled&&(e.execute(n),i.stopPropagation(),i.preventDefault(),t.stop())}),{context:"li"})}_setupConversion(){const e=this.editor,t=e.model,i=this.getListAttributeNames(),n=e.config.get("list.multiBlock"),s=n?"paragraph":"listItem";e.conversion.for("upcast").elementToElement({view:"li",model:(e,{writer:t})=>t.createElement(s,{listType:""})}).elementToElement({view:"p",model:(e,{writer:t})=>e.parent&&e.parent.is("element","li")?t.createElement(s,{listType:""}):null,converterPriority:"high"}).add((e=>{e.on("element:li",nx()),e.on("element:ul",sx(),{priority:"high"}),e.on("element:ol",sx(),{priority:"high"})})),n||e.conversion.for("downcast").elementToElement({model:"listItem",view:"p"}),e.conversion.for("editingDowncast").elementToElement({model:s,view:rx(i),converterPriority:"high"}).add((e=>{var n;e.on("attribute",ox(i,this._downcastStrategies,t)),e.on("remove",(n=t.schema,(e,t,i)=>{const{writer:s,mapper:o}=i,r=e.name.split(":")[1];if(!n.checkAttribute(r,"listItemId"))return;const a=o.toViewPosition(t.position),l=t.position.getShiftedBy(t.length),c=o.toViewPosition(l,{isPhantom:!0}),d=s.createRange(a,c).getTrimmed().end.nodeBefore;d&&ax(d,s,o)}))})),e.conversion.for("dataDowncast").elementToElement({model:s,view:rx(i,{dataPipeline:!0}),converterPriority:"high"}).add((e=>{e.on("attribute",ox(i,this._downcastStrategies,t,{dataPipeline:!0}))}));const o=(r=this._downcastStrategies,a=e.editing.view,(e,t)=>{if(t.modelPosition.offset>0)return;const i=t.modelPosition.parent;if(!EA(i))return;if(!r.some((e=>"itemMarker"==e.scope&&e.canInjectMarkerIntoElement&&e.canInjectMarkerIntoElement(i))))return;const n=t.mapper.toViewElement(i),s=a.createRangeIn(n),o=s.getWalker();let l=s.start;for(const{item:e}of o){if(e.is("element")&&t.mapper.toModelElement(e)||e.is("$textProxy"))break;e.is("element")&&e.getCustomProperty("listItemMarker")&&(l=a.createPositionAfter(e),o.skip((({previousPosition:e})=>!e.isEqual(l))))}t.viewPosition=l});var r,a;e.editing.mapper.on("modelToViewPosition",o),e.data.mapper.on("modelToViewPosition",o),this.listenTo(t.document,"change:data",function(e,t,i,n){return()=>{const n=e.document.differ.getChanges(),r=[],a=new Map,l=new Set;for(const e of n)if("insert"==e.type&&"$text"!=e.name)ix(e.position,a),e.attributes.has("listItemId")?l.add(e.position.nodeAfter):ix(e.position.getShiftedBy(e.length),a);else if("remove"==e.type&&e.attributes.has("listItemId"))ix(e.position,a);else if("attribute"==e.type){const t=e.range.start.nodeAfter;i.includes(e.attributeKey)?(ix(e.range.start,a),null===e.attributeNewValue?(ix(e.range.start.getShiftedBy(1),a),o(t)&&r.push(t)):l.add(t)):EA(t)&&o(t)&&r.push(t)}for(const e of a.values())r.push(...s(e,l));for(const e of new Set(r))t.reconvertItem(e)};function s(e,t){const n=[],s=new Set,a=[];for(const{node:l,previous:c}of CA(e,"forward")){if(s.has(l))continue;const e=l.getAttribute("listIndent");c&&ei.includes(e))));const d=SA(l,{direction:"forward"});for(const e of d)s.add(e),(o(e,d)||r(e,a,t))&&n.push(e)}return n}function o(e,s){const o=t.mapper.toViewElement(e);if(!o)return!1;if(n.fire("checkElement",{modelElement:e,viewElement:o}))return!0;if(!e.is("element","paragraph")&&!e.is("element","listItem"))return!1;const r=lx(e,i,s);return!(!r||!o.is("element","p"))||!(r||!o.is("element","span"))}function r(e,i,s){if(s.has(e))return!1;const o=t.mapper.toViewElement(e);let r=i.length-1;for(let e=o.parent;!e.is("editableElement");e=e.parent){const t=QA(e),s=JA(e);if(!s&&!t)continue;const o="checkAttributes:"+(t?"item":"list");if(n.fire(o,{viewElement:e,modelAttributes:i[r]}))break;if(s&&(r--,r<0))return!1}return!0}}(t,e.editing,i,this),{priority:"high"}),this.on("checkAttributes:item",((e,{viewElement:t,modelAttributes:i})=>{t.id!=i.listItemId&&(e.return=!0,e.stop())})),this.on("checkAttributes:list",((e,{viewElement:t,modelAttributes:i})=>{t.name==ex(i.listType)&&t.id==tx(i.listType,i.listIndent)||(e.return=!0,e.stop())}))}_setupModelPostFixing(){const e=this.editor.model,t=this.getListAttributeNames();e.document.registerPostFixer((i=>function(e,t,i,n){const s=e.document.differ.getChanges(),o=new Map,r=n.editor.config.get("list.multiBlock");let a=!1;for(const n of s){if("insert"==n.type&&"$text"!=n.name){const s=n.position.nodeAfter;if(!e.schema.checkAttribute(s,"listItemId"))for(const e of Array.from(s.getAttributeKeys()))i.includes(e)&&(t.removeAttribute(e,s),a=!0);ix(n.position,o),n.attributes.has("listItemId")||ix(n.position.getShiftedBy(n.length),o);for(const{item:t,previousPosition:i}of e.createRangeIn(s))EA(t)&&ix(i,o)}else"remove"==n.type?ix(n.position,o):"attribute"==n.type&&i.includes(n.attributeKey)&&(ix(n.range.start,o),null===n.attributeNewValue&&ix(n.range.start.getShiftedBy(1),o));if(!r&&"attribute"==n.type&&cx.includes(n.attributeKey)){const e=n.range.start.nodeAfter;null===n.attributeNewValue&&e&&e.is("element","listItem")?(t.rename(e,"paragraph"),a=!0):null===n.attributeOldValue&&e&&e.is("element")&&"listItem"!=e.name&&(t.rename(e,"listItem"),a=!0)}}const l=new Set;for(const e of o.values())a=n.fire("postFixer",{listNodes:new AA(e),listHead:e,writer:t,seenIds:l})||a;return a}(e,i,t,this))),this.on("postFixer",((e,{listNodes:t,writer:i})=>{e.return=function(e,t){let i=0,n=-1,s=null,o=!1;for(const{node:r}of e){const e=r.getAttribute("listIndent");if(e>i){let a;null===s?(s=e-i,a=i):(s>e&&(s=e),a=e-s),a>n+1&&(a=n+1),t.setAttribute("listIndent",a,r),o=!0,n=a}else s=null,i=e+1,n=e}return o}(t,i)||e.return}),{priority:"high"}),this.on("postFixer",((e,{listNodes:t,writer:i,seenIds:n})=>{e.return=function(e,t,i){const n=new Set;let s=!1;for(const{node:o}of e){if(n.has(o))continue;let e=o.getAttribute("listType"),r=o.getAttribute("listItemId");if(t.has(r)&&(r=xA.next()),t.add(r),o.is("element","listItem"))o.getAttribute("listItemId")!=r&&(i.setAttribute("listItemId",r,o),s=!0);else for(const t of SA(o,{direction:"forward"}))n.add(t),t.getAttribute("listType")!=e&&(r=xA.next(),e=t.getAttribute("listType")),t.getAttribute("listItemId")!=r&&(i.setAttribute("listItemId",r,t),s=!0)}return s}(t,n,i)||e.return}),{priority:"high"})}_setupClipboardIntegration(){const e=this.editor.model,t=this.editor.plugins.get("ClipboardPipeline");this.listenTo(e,"insertContent",function(e){return(t,[i,n])=>{const s=i.is("documentFragment")?Array.from(i.getChildren()):[i];if(!s.length)return;const o=(n?e.createSelection(n):e.document.selection).getFirstPosition();let r;if(EA(o.parent))r=o.parent;else{if(!EA(o.nodeBefore))return;r=o.nodeBefore}e.change((e=>{const t=r.getAttribute("listType"),i=r.getAttribute("listIndent"),n=s[0].getAttribute("listIndent")||0,o=Math.max(i-n,0);for(const i of s){const n=EA(i);r.is("element","listItem")&&i.is("element","paragraph")&&e.rename(i,"listItem"),e.setAttributes({listIndent:(n?i.getAttribute("listIndent"):0)+o,listItemId:n?i.getAttribute("listItemId"):xA.next(),listType:t},i)}}))}}(e),{priority:"high"}),this.listenTo(t,"outputTransformation",((t,i)=>{e.change((e=>{const t=Array.from(i.content.getChildren()),n=t[t.length-1];if(t.length>1&&n.is("element")&&n.isEmpty){t.slice(0,-1).every(EA)&&e.remove(n)}if("copy"==i.method||"cut"==i.method){const t=Array.from(i.content.getChildren());DA(t)&&FA(t,e)}}))}))}_setupAccessibilityIntegration(){const e=this.editor,t=e.t;e.accessibility.addKeystrokeInfoGroup({id:"list",label:t("Keystrokes that can be used in a list"),keystrokes:[{label:t("Increase list item indent"),keystroke:"Tab"},{label:t("Decrease list item indent"),keystroke:"Shift+Tab"}]})}}function hx(e,t){const i=e.document.selection;if(!i.isCollapsed)return!zA(e);if("forward"===t)return!0;const n=i.getFirstPosition().parent,s=n.previousSibling;return!e.schema.isObject(s)&&(!!s.isEmpty||DA([n,s]))}function ux(e,t,i,n){e.ui.componentFactory.add(t,(()=>{const s=mx(Ku,e,t,i,n);return s.set({tooltip:!0,isToggleable:!0}),s})),e.ui.componentFactory.add(`menuBar:${t}`,(()=>mx(up,e,t,i,n)))}function mx(e,t,i,n,s){const o=t.commands.get(i),r=new e(t.locale);return r.set({label:n,icon:s}),r.bind("isOn","isEnabled").to(o,"value","isEnabled"),r.on("execute",(()=>{t.execute(i),t.editing.view.focus()})),r}class gx extends so{static get pluginName(){return"ListUI"}init(){const e=this.editor.t;ux(this.editor,"numberedList",e("Numbered List"),fu.numberedList),ux(this.editor,"bulletedList",e("Bulleted List"),fu.bulletedList)}}class fx extends so{static get requires(){return[dx,gx]}static get pluginName(){return"List"}}class px extends ro{refresh(){const e=this._getValue();this.value=e,this.isEnabled=null!=e}execute({startIndex:e=1}={}){const t=this.editor.model,i=t.document;let n=Array.from(i.selection.getSelectedBlocks()).filter((e=>EA(e)&&$A(e.getAttribute("listType"))));n=OA(n),t.change((t=>{for(const i of n)t.setAttribute("listStart",e>=0?e:1,i)}))}_getValue(){const e=Zs(this.editor.model.document.selection.getSelectedBlocks());return e&&EA(e)&&$A(e.getAttribute("listType"))?e.getAttribute("listStart"):null}}const bx={},wx={},vx={},_x=[{listStyle:"disc",typeAttribute:"disc",listType:"bulleted"},{listStyle:"circle",typeAttribute:"circle",listType:"bulleted"},{listStyle:"square",typeAttribute:"square",listType:"bulleted"},{listStyle:"decimal",typeAttribute:"1",listType:"numbered"},{listStyle:"decimal-leading-zero",typeAttribute:null,listType:"numbered"},{listStyle:"lower-roman",typeAttribute:"i",listType:"numbered"},{listStyle:"upper-roman",typeAttribute:"I",listType:"numbered"},{listStyle:"lower-alpha",typeAttribute:"a",listType:"numbered"},{listStyle:"upper-alpha",typeAttribute:"A",listType:"numbered"},{listStyle:"lower-latin",typeAttribute:"a",listType:"numbered"},{listStyle:"upper-latin",typeAttribute:"A",listType:"numbered"}];for(const{listStyle:e,typeAttribute:t,listType:i}of _x)bx[e]=i,wx[e]=t,t&&(vx[t]=e);function yx(){return _x.map((e=>e.listStyle))}function kx(e){return bx[e]||null}function Cx(e){return vx[e]||null}function Ax(e){return wx[e]||null}class xx extends ro{constructor(e,t,i){super(e),this.defaultType=t,this._supportedTypes=i}refresh(){this.value=this._getValue(),this.isEnabled=this._checkEnabled()}execute(e={}){const t=this.editor.model,i=t.document;t.change((t=>{this._tryToConvertItemsToList(e);let n=Array.from(i.selection.getSelectedBlocks()).filter((e=>e.hasAttribute("listType")));if(n.length){n=OA(n);for(const i of n)t.setAttribute("listStyle",e.type||this.defaultType,i)}}))}isStyleTypeSupported(e){return!this._supportedTypes||this._supportedTypes.includes(e)}_getValue(){const e=Zs(this.editor.model.document.selection.getSelectedBlocks());return EA(e)?e.getAttribute("listStyle"):null}_checkEnabled(){const e=this.editor,t=e.commands.get("numberedList"),i=e.commands.get("bulletedList");return t.isEnabled||i.isEnabled}_tryToConvertItemsToList(e){if(!e.type)return;const t=kx(e.type);if(!t)return;const i=this.editor,n=`${t}List`;i.commands.get(n).value||i.execute(n)}}class Ex extends ro{refresh(){const e=this._getValue();this.value=e,this.isEnabled=null!=e}execute(e={}){const t=this.editor.model,i=t.document;let n=Array.from(i.selection.getSelectedBlocks()).filter((e=>EA(e)&&"numbered"==e.getAttribute("listType")));n=OA(n),t.change((t=>{for(const i of n)t.setAttribute("listReversed",!!e.reversed,i)}))}_getValue(){const e=Zs(this.editor.model.document.selection.getSelectedBlocks());return EA(e)&&"numbered"==e.getAttribute("listType")?e.getAttribute("listReversed"):null}}function Tx(e){return(t,i,n)=>{const{writer:s,schema:o,consumable:r}=n;if(!1===r.test(i.viewItem,e.viewConsumables))return;i.modelRange||Object.assign(i,n.convertChildren(i.viewItem,i.modelCursor));let a=!1;for(const t of i.modelRange.getItems({shallow:!0}))o.checkAttribute(t,e.attributeName)&&e.appliesToListItem(t)&&(t.hasAttribute(e.attributeName)||(s.setAttribute(e.attributeName,e.getAttributeOnUpcast(i.viewItem),t),a=!0));a&&r.consume(i.viewItem,e.viewConsumables)}}class Sx extends so{static get pluginName(){return"ListPropertiesUtils"}getAllSupportedStyleTypes(){return yx()}getListTypeFromListStyleType(e){return kx(e)}getListStyleTypeFromTypeAttribute(e){return Cx(e)}getTypeAttributeFromListStyleType(e){return Ax(e)}}const Px="default";class Ix extends so{static get requires(){return[dx,Sx]}static get pluginName(){return"ListPropertiesEditing"}constructor(e){super(e),e.config.define("list.properties",{styles:!0,startIndex:!1,reversed:!1})}init(){const e=this.editor,t=e.model,i=e.plugins.get(dx),n=function(e){const t=[];if(e.styles){const i="object"==typeof e.styles&&e.styles.useAttribute;t.push({attributeName:"listStyle",defaultValue:Px,viewConsumables:{styles:"list-style-type"},addCommand(e){let t=yx();i&&(t=t.filter((e=>!!Ax(e)))),e.commands.add("listStyle",new xx(e,Px,t))},appliesToListItem:e=>"numbered"==e.getAttribute("listType")||"bulleted"==e.getAttribute("listType"),hasValidAttribute(e){if(!this.appliesToListItem(e))return!e.hasAttribute("listStyle");if(!e.hasAttribute("listStyle"))return!1;const t=e.getAttribute("listStyle");return t==Px||kx(t)==e.getAttribute("listType")},setAttributeOnDowncast(e,t,n){if(t&&t!==Px){if(!i)return void e.setStyle("list-style-type",t,n);{const i=Ax(t);if(i)return void e.setAttribute("type",i,n)}}e.removeStyle("list-style-type",n),e.removeAttribute("type",n)},getAttributeOnUpcast(e){const t=e.getStyle("list-style-type");if(t)return t;const i=e.getAttribute("type");return i?Cx(i):Px}})}e.reversed&&t.push({attributeName:"listReversed",defaultValue:!1,viewConsumables:{attributes:"reversed"},addCommand(e){e.commands.add("listReversed",new Ex(e))},appliesToListItem:e=>"numbered"==e.getAttribute("listType"),hasValidAttribute(e){return this.appliesToListItem(e)==e.hasAttribute("listReversed")},setAttributeOnDowncast(e,t,i){t?e.setAttribute("reversed","reversed",i):e.removeAttribute("reversed",i)},getAttributeOnUpcast:e=>e.hasAttribute("reversed")});e.startIndex&&t.push({attributeName:"listStart",defaultValue:1,viewConsumables:{attributes:"start"},addCommand(e){e.commands.add("listStart",new px(e))},appliesToListItem:e=>$A(e.getAttribute("listType")),hasValidAttribute(e){return this.appliesToListItem(e)==e.hasAttribute("listStart")},setAttributeOnDowncast(e,t,i){0==t||t>1?e.setAttribute("start",t,i):e.removeAttribute("start",i)},getAttributeOnUpcast(e){const t=e.getAttribute("start");return t>=0?t:1}});return t}(e.config.get("list.properties"));for(const s of n)s.addCommand(e),t.schema.extend("$listItem",{allowAttributes:s.attributeName}),i.registerDowncastStrategy({scope:"list",attributeName:s.attributeName,setAttributeOnDowncast(e,t,i){s.setAttributeOnDowncast(e,t,i)}});e.conversion.for("upcast").add((e=>{for(const t of n)e.on("element:ol",Tx(t)),e.on("element:ul",Tx(t))})),i.on("checkAttributes:list",((e,{viewElement:t,modelAttributes:i})=>{for(const s of n)s.getAttributeOnUpcast(t)!=i[s.attributeName]&&(e.return=!0,e.stop())})),this.listenTo(e.commands.get("indentList"),"afterExecute",((e,i)=>{t.change((e=>{for(const t of i)for(const i of n)i.appliesToListItem(t)&&e.setAttribute(i.attributeName,i.defaultValue,t)}))})),i.on("postFixer",((e,{listNodes:t,writer:i})=>{for(const{node:s}of t)for(const t of n)t.hasValidAttribute(s)||(t.appliesToListItem(s)?i.setAttribute(t.attributeName,t.defaultValue,s):i.removeAttribute(t.attributeName,s),e.return=!0)})),i.on("postFixer",((e,{listNodes:t,writer:i})=>{for(const{node:s,previousNodeInList:o}of t)if(o&&o.getAttribute("listType")==s.getAttribute("listType"))for(const t of n){const{attributeName:n}=t;if(!t.appliesToListItem(s))continue;const r=o.getAttribute(n);s.getAttribute(n)!=r&&(i.setAttribute(n,r,s),e.return=!0)}}))}}class Vx extends Du{constructor(e,{enabledProperties:t,styleButtonViews:i,styleGridAriaLabel:n}){super(e),this.stylesView=null,this.additionalPropertiesCollapsibleView=null,this.startIndexFieldView=null,this.reversedSwitchButtonView=null,this.focusTracker=new Js,this.keystrokes=new Qs,this.focusables=new pu;const s=["ck","ck-list-properties"];this.children=this.createCollection(),this.focusCycler=new ym({focusables:this.focusables,focusTracker:this.focusTracker,keystrokeHandler:this.keystrokes,actions:{focusPrevious:"shift + tab",focusNext:"tab"}}),t.styles?(this.stylesView=this._createStylesView(i,n),this.children.add(this.stylesView)):s.push("ck-list-properties_without-styles"),(t.startIndex||t.reversed)&&(this._addNumberedListPropertyViews(t),s.push("ck-list-properties_with-numbered-properties")),this.setTemplate({tag:"div",attributes:{class:s},children:this.children})}render(){if(super.render(),this.stylesView){this.focusables.add(this.stylesView),this.focusTracker.add(this.stylesView.element),(this.startIndexFieldView||this.reversedSwitchButtonView)&&(this.focusables.add(this.children.last.buttonView),this.focusTracker.add(this.children.last.buttonView.element));for(const e of this.stylesView.children)this.stylesView.focusTracker.add(e.element);n({keystrokeHandler:this.stylesView.keystrokes,focusTracker:this.stylesView.focusTracker,gridItems:this.stylesView.children,numberOfColumns:()=>Mn.window.getComputedStyle(this.stylesView.element).getPropertyValue("grid-template-columns").split(" ").length,uiLanguageDirection:this.locale&&this.locale.uiLanguageDirection})}if(this.startIndexFieldView){this.focusables.add(this.startIndexFieldView),this.focusTracker.add(this.startIndexFieldView.element);const e=e=>e.stopPropagation();this.keystrokes.set("arrowright",e),this.keystrokes.set("arrowleft",e),this.keystrokes.set("arrowup",e),this.keystrokes.set("arrowdown",e)}this.reversedSwitchButtonView&&(this.focusables.add(this.reversedSwitchButtonView),this.focusTracker.add(this.reversedSwitchButtonView.element)),this.keystrokes.listenTo(this.element)}focus(){this.focusCycler.focusFirst()}focusLast(){this.focusCycler.focusLast()}destroy(){super.destroy(),this.focusTracker.destroy(),this.keystrokes.destroy()}_createStylesView(e,t){const i=new Du(this.locale);return i.children=i.createCollection(),i.children.addMany(e),i.setTemplate({tag:"div",attributes:{"aria-label":t,class:["ck","ck-list-styles-list"]},children:i.children}),i.children.delegate("execute").to(this),i.focus=function(){this.children.first.focus()},i.focusTracker=new Js,i.keystrokes=new Qs,i.render(),i.keystrokes.listenTo(i.element),i}_addNumberedListPropertyViews(e){const t=this.locale.t,i=[];e.startIndex&&(this.startIndexFieldView=this._createStartIndexField(),i.push(this.startIndexFieldView)),e.reversed&&(this.reversedSwitchButtonView=this._createReversedSwitchButton(),i.push(this.reversedSwitchButtonView)),e.styles?(this.additionalPropertiesCollapsibleView=new Xu(this.locale,i),this.additionalPropertiesCollapsibleView.set({label:t("List properties"),isCollapsed:!0}),this.additionalPropertiesCollapsibleView.buttonView.bind("isEnabled").toMany(i,"isEnabled",((...e)=>e.some((e=>e)))),this.additionalPropertiesCollapsibleView.buttonView.on("change:isEnabled",((e,t,i)=>{i||(this.additionalPropertiesCollapsibleView.isCollapsed=!0)})),this.children.add(this.additionalPropertiesCollapsibleView)):this.children.addMany(i)}_createStartIndexField(){const e=this.locale.t,t=new um(this.locale,qm);return t.set({label:e("Start at"),class:"ck-numbered-list-properties__start-index"}),t.fieldView.set({min:0,step:1,value:1,inputMode:"numeric"}),t.fieldView.on("input",(()=>{const i=t.fieldView.element,n=i.valueAsNumber;Number.isNaN(n)?t.errorText=e("Invalid start index value."):i.checkValidity()?this.fire("listStart",{startIndex:n}):t.errorText=e("Start index must be greater than 0.")})),t}_createReversedSwitchButton(){const e=this.locale.t,t=new Zu(this.locale);return t.set({withText:!0,label:e("Reversed order"),class:"ck-numbered-list-properties__reversed-order"}),t.delegate("execute").to(this,"listReversed"),t}}class Rx extends so{static get pluginName(){return"ListPropertiesUI"}init(){const e=this.editor,t=e.locale.t,i=e.config.get("list.properties");if(i.styles){const n=[{label:t("Toggle the disc list style"),tooltip:t("Disc"),type:"disc",icon:''},{label:t("Toggle the circle list style"),tooltip:t("Circle"),type:"circle",icon:''},{label:t("Toggle the square list style"),tooltip:t("Square"),type:"square",icon:''}],s=t("Bulleted List"),o=t("Bulleted list styles toolbar"),r="bulletedList";e.ui.componentFactory.add(r,Ox({editor:e,propertiesConfig:i,parentCommandName:r,buttonLabel:s,buttonIcon:fu.bulletedList,styleGridAriaLabel:o,styleDefinitions:n})),e.ui.componentFactory.add(`menuBar:${r}`,Mx({editor:e,propertiesConfig:i,parentCommandName:r,buttonLabel:s,styleGridAriaLabel:o,styleDefinitions:n}))}if(i.styles||i.startIndex||i.reversed){const n=[{label:t("Toggle the decimal list style"),tooltip:t("Decimal"),type:"decimal",icon:''},{label:t("Toggle the decimal with leading zero list style"),tooltip:t("Decimal with leading zero"),type:"decimal-leading-zero",icon:''},{label:t("Toggle the lower–roman list style"),tooltip:t("Lower–roman"),type:"lower-roman",icon:''},{label:t("Toggle the upper–roman list style"),tooltip:t("Upper-roman"),type:"upper-roman",icon:''},{label:t("Toggle the lower–latin list style"),tooltip:t("Lower-latin"),type:"lower-latin",icon:''},{label:t("Toggle the upper–latin list style"),tooltip:t("Upper-latin"),type:"upper-latin",icon:''}],s=t("Numbered List"),o=t("Numbered list styles toolbar"),r="numberedList";e.ui.componentFactory.add(r,Ox({editor:e,propertiesConfig:i,parentCommandName:r,buttonLabel:s,buttonIcon:fu.numberedList,styleGridAriaLabel:o,styleDefinitions:n})),i.styles&&e.ui.componentFactory.add(`menuBar:${r}`,Mx({editor:e,propertiesConfig:i,parentCommandName:r,buttonLabel:s,styleGridAriaLabel:o,styleDefinitions:n}))}}}function Ox({editor:e,propertiesConfig:t,parentCommandName:i,buttonLabel:n,buttonIcon:s,styleGridAriaLabel:o,styleDefinitions:r}){const a=e.commands.get(i);return l=>{const c=Dm(l,Fm),d=c.buttonView;return c.bind("isEnabled").to(a),c.class="ck-list-styles-dropdown",d.on("execute",(()=>{e.execute(i),e.editing.view.focus()})),d.set({label:n,icon:s,tooltip:!0,isToggleable:!0}),d.bind("isOn").to(a,"value",(e=>!!e)),c.once("change:isOpen",(()=>{const n=function({editor:e,propertiesConfig:t,dropdownView:i,parentCommandName:n,styleDefinitions:s,styleGridAriaLabel:o}){const r=e.locale,a={...t};"numberedList"!=n&&(a.startIndex=!1,a.reversed=!1);let l=null;if(a.styles){const t=e.commands.get("listStyle"),i=Bx({editor:e,parentCommandName:n,listStyleCommand:t}),o=Nx(t);l=s.filter(o).map(i)}const c=new Vx(r,{styleGridAriaLabel:o,enabledProperties:a,styleButtonViews:l});a.styles&&Wm(i,(()=>c.stylesView.children.find((e=>e.isOn))));if(a.startIndex){const t=e.commands.get("listStart");c.startIndexFieldView.bind("isEnabled").to(t),c.startIndexFieldView.fieldView.bind("value").to(t),c.on("listStart",((t,i)=>e.execute("listStart",i)))}if(a.reversed){const t=e.commands.get("listReversed");c.reversedSwitchButtonView.bind("isEnabled").to(t),c.reversedSwitchButtonView.bind("isOn").to(t,"value",(e=>!!e)),c.on("listReversed",(()=>{const i=t.value;e.execute("listReversed",{reversed:!i})}))}return c.delegate("execute").to(i),c}({editor:e,propertiesConfig:t,dropdownView:c,parentCommandName:i,styleGridAriaLabel:o,styleDefinitions:r});c.panelView.children.add(n)})),c.on("execute",(()=>{e.editing.view.focus()})),c}}function Bx({editor:e,listStyleCommand:t,parentCommandName:i}){const n=e.locale,s=e.commands.get(i);return({label:o,type:r,icon:a,tooltip:l})=>{const c=new Ku(n);return c.set({label:o,icon:a,tooltip:l}),t.on("change:value",(()=>{c.isOn=t.value===r})),c.on("execute",(()=>{s.value?t.value===r?e.execute(i):t.value!==r&&e.execute("listStyle",{type:r}):e.model.change((()=>{e.execute("listStyle",{type:r})}))})),c}}function Mx({editor:e,propertiesConfig:t,parentCommandName:i,buttonLabel:n,styleGridAriaLabel:s,styleDefinitions:o}){return r=>{const a=new dp(r),l=e.commands.get(i),c=e.commands.get("listStyle"),d=Nx(c),h=Bx({editor:e,parentCommandName:i,listStyleCommand:c}),u=o.filter(d).map(h),m=new Vx(r,{styleGridAriaLabel:s,enabledProperties:{...t,startIndex:!1,reversed:!1},styleButtonViews:u});return m.delegate("execute").to(a),a.buttonView.set({label:n,icon:fu[i]}),a.panelView.children.add(m),a.bind("isEnabled").to(l,"isEnabled"),a.on("execute",(()=>{e.editing.view.focus()})),a}}function Nx(e){return"function"==typeof e.isStyleTypeSupported?t=>e.isStyleTypeSupported(t.type):()=>!0}class Fx extends so{static get requires(){return[Ix,Rx]}static get pluginName(){return"ListProperties"}}_s("Ctrl+Enter");_s("Ctrl+Enter");function Dx(e){return void 0!==e&&e.endsWith("px")}function Lx(e){return e.toFixed(2).replace(/\.?0+$/,"")+"px"}function zx(e,t,i){if(!e.childCount)return;const n=new _h(e.document),s=function(e,t){const i=t.createRangeIn(e),n=[],s=new Set;for(const e of i.getItems()){if(!e.is("element")||!e.name.match(/^(p|h\d+|li|div)$/))continue;let t=Kx(e);if(void 0===t||0!=parseFloat(t)||Array.from(e.getClassNames()).find((e=>e.startsWith("MsoList")))||(t=void 0),e.hasStyle("mso-list")||void 0!==t&&s.has(t)){const i=qx(e);n.push({element:e,id:i.id,order:i.order,indent:i.indent,marginLeft:t}),void 0!==t&&s.add(t)}else s.clear()}return n}(e,n);if(!s.length)return;const o={},r=[];for(const e of s)if(void 0!==e.indent){Hx(e)||(r.length=0);const s=`${e.id}:${e.indent}`,a=Math.min(e.indent-1,r.length);if(ar.length-1||r[a].listElement.name!=l.type){0==a&&"ol"==l.type&&void 0!==e.id&&o[s]&&(l.startIndex=o[s]);const t=Ux(l,n,i);if(Dx(e.marginLeft)&&(0==a||Dx(r[a-1].marginLeft))){let i=e.marginLeft;a>0&&(i=Lx(parseFloat(i)-parseFloat(r[a-1].marginLeft))),n.setStyle("padding-left",i,t)}if(0==r.length){const i=e.element.parent,s=i.getChildIndex(e.element)+1;n.insertChild(s,t,i)}else{const e=r[a-1].listItemElements;n.appendChild(t,e[e.length-1])}r[a]={...e,listElement:t,listItemElements:[]},0==a&&void 0!==e.id&&(o[s]=l.startIndex||1)}}const l="li"==e.element.name?e.element:n.createElement("li");n.appendChild(l,r[a].listElement),r[a].listItemElements.push(l),0==a&&void 0!==e.id&&o[s]++,e.element!=l&&n.appendChild(e.element,l),Gx(e.element,n),n.removeStyle("text-indent",e.element),n.removeStyle("margin-left",e.element)}else{const t=r.find((t=>t.marginLeft==e.marginLeft));if(t){const i=t.listItemElements;n.appendChild(e.element,i[i.length-1]),n.removeStyle("margin-left",e.element)}else r.length=0}}function Hx(e){const t=e.element.previousSibling;return $x(t||e.element.parent)}function $x(e){return e.is("element","ol")||e.is("element","ul")}function Wx(e,t){const i=new RegExp(`@list l${e.id}:level${e.indent}\\s*({[^}]*)`,"gi"),n=/mso-level-number-format:([^;]{0,100});/gi,s=/mso-level-start-at:\s{0,100}([0-9]{0,10})\s{0,100};/gi,o=new RegExp(`@list\\s+l${e.id}:level\\d\\s*{[^{]*mso-level-text:"%\\d\\\\.`,"gi"),r=new RegExp(`@list l${e.id}:level\\d\\s*{[^{]*mso-level-number-format:`,"gi"),a=o.exec(t),l=r.exec(t),c=a&&!l,d=i.exec(t);let h="decimal",u="ol",m=null;if(d&&d[1]){const t=n.exec(d[1]);if(t&&t[1]&&(h=t[1].trim(),u="bullet"!==h&&"image"!==h?"ol":"ul"),"bullet"===h){const t=function(e){if("li"==e.name&&"ul"==e.parent.name&&e.parent.hasAttribute("type"))return e.parent.getAttribute("type");const t=function(e){if(e.getChild(0).is("$text"))return null;for(const t of e.getChildren()){if(!t.is("element","span"))continue;const e=t.getChild(0);if(e)return e.is("$text")?e:e.getChild(0)}return null}(e);if(!t)return null;const i=t._data;if("o"===i)return"circle";if("·"===i)return"disc";if("§"===i)return"square";return null}(e.element);t&&(h=t)}else{const e=s.exec(d[1]);e&&e[1]&&(m=parseInt(e[1]))}c&&(u="ol")}return{type:u,startIndex:m,style:jx(h),isLegalStyleList:c}}function jx(e){if(e.startsWith("arabic-leading-zero"))return"decimal-leading-zero";switch(e){case"alpha-upper":return"upper-alpha";case"alpha-lower":return"lower-alpha";case"roman-upper":return"upper-roman";case"roman-lower":return"lower-roman";case"circle":case"disc":case"square":return e;default:return null}}function Ux(e,t,i){const n=t.createElement(e.type);return e.style&&t.setStyle("list-style-type",e.style,n),e.startIndex&&e.startIndex>1&&t.setAttribute("start",e.startIndex,n),e.isLegalStyleList&&i&&t.addClass("legal-list",n),n}function qx(e){const t=e.getStyle("mso-list");if(void 0===t)return{};const i=t.match(/(^|\s{1,100})l(\d+)/i),n=t.match(/\s{0,100}lfo(\d+)/i),s=t.match(/\s{0,100}level(\d+)/i);return i&&n&&s?{id:i[2],order:n[1],indent:parseInt(s[1])}:{indent:1}}function Gx(e,t){const i=new To({name:"span",styles:{"mso-list":"Ignore"}}),n=t.createRangeIn(e);for(const e of n)"elementStart"===e.type&&i.match(e.item)&&t.remove(e.item)}function Kx(e){const t=e.getStyle("margin-left");return void 0===t||t.endsWith("px")?t:function(e){const t=parseFloat(e);return e.endsWith("pt")?Lx(96*t/72):e.endsWith("pc")?Lx(12*t*96/72):e.endsWith("in")?Lx(96*t):e.endsWith("cm")?Lx(96*t/2.54):e.endsWith("mm")?Lx(t/10*96/2.54):e}(t)}function Zx(e,t){if(!e.childCount)return;const i=new _h(e.document),n=function(e,t){const i=t.createRangeIn(e),n=new To({name:/v:(.+)/}),s=[];for(const e of i){if("elementStart"!=e.type)continue;const t=e.item,i=t.previousSibling,o=i&&i.is("element")?i.name:null,r=["Chart"],a=n.match(t),l=t.getAttribute("o:gfxdata"),c="v:shapetype"===o,d=l&&r.some((e=>t.getAttribute("id").includes(e)));a&&l&&!c&&!d&&s.push(e.item.getAttribute("id"))}return s}(e,i);!function(e,t,i){const n=i.createRangeIn(t),s=new To({name:"img"}),o=[];for(const t of n)if(t.item.is("element")&&s.match(t.item)){const i=t.item,n=i.getAttribute("v:shapes")?i.getAttribute("v:shapes").split(" "):[];n.length&&n.every((t=>e.indexOf(t)>-1))?o.push(i):i.getAttribute("src")||o.push(i)}for(const e of o)i.remove(e)}(n,e,i),function(e,t,i){const n=i.createRangeIn(t),s=[];for(const t of n)if("elementStart"==t.type&&t.item.is("element","v:shape")){const i=t.item.getAttribute("id");if(e.includes(i))continue;o(t.item.parent.getChildren(),i)||s.push(t.item)}for(const e of s){const t={src:r(e)};e.hasAttribute("alt")&&(t.alt=e.getAttribute("alt"));const n=i.createElement("img",t);i.insertChild(e.index+1,n,e.parent)}function o(e,t){for(const i of e)if(i.is("element")){if("img"==i.name&&i.getAttribute("v:shapes")==t)return!0;if(o(i.getChildren(),t))return!0}return!1}function r(e){for(const t of e.getChildren())if(t.is("element")&&t.getAttribute("src"))return t.getAttribute("src")}}(n,e,i),function(e,t){const i=t.createRangeIn(e),n=new To({name:/v:(.+)/}),s=[];for(const e of i)"elementStart"==e.type&&n.match(e.item)&&s.push(e.item);for(const e of s)t.remove(e)}(e,i);const s=function(e,t){const i=t.createRangeIn(e),n=new To({name:"img"}),s=[];for(const e of i)e.item.is("element")&&n.match(e.item)&&e.item.getAttribute("src").startsWith("file://")&&s.push(e.item);return s}(e,i);s.length&&function(e,t,i){if(e.length===t.length)for(let n=0;nString.fromCharCode(parseInt(e,16)))).join(""))}const Qx=//i,Yx=/xmlns:o="urn:schemas-microsoft-com/i;class Xx{constructor(e,t=!1){this.document=e,this.hasMultiLevelListPlugin=t}isActive(e){return Qx.test(e)||Yx.test(e)}execute(e){const{body:t,stylesString:i}=e._parsedData;zx(t,i,this.hasMultiLevelListPlugin),Zx(t,e.dataTransfer.getData("text/rtf")),function(e){const t=[],i=new _h(e.document);for(const{item:n}of i.createRangeIn(e))if(n.is("element")){for(const e of n.getClassNames())/\bmso/gi.exec(e)&&i.removeClass(e,n);for(const e of n.getStyleNames())/\bmso/gi.exec(e)&&i.removeStyle(e,n);(n.is("element","w:sdt")||n.is("element","w:sdtpr")&&n.isEmpty||n.is("element","o:p")&&n.isEmpty)&&t.push(n)}for(const e of t){const t=e.parent,n=t.getChildIndex(e);i.insertChild(n,e.getChildren(),t),i.remove(e)}}(t),e.content=t}}function eE(e,t,i,{blockElements:n,inlineObjectElements:s}){let o=i.createPositionAt(e,"forward"==t?"after":"before");return o=o.getLastMatchingPosition((({item:e})=>e.is("element")&&!n.includes(e.name)&&!s.includes(e.name)),{direction:t}),"forward"==t?o.nodeAfter:o.nodeBefore}function tE(e,t){return!!e&&e.is("element")&&t.includes(e.name)}const iE=/id=("|')docs-internal-guid-[-0-9a-f]+("|')/i;class nE{constructor(e){this.document=e}isActive(e){return iE.test(e)}execute(e){const t=new _h(this.document),{body:i}=e._parsedData;!function(e,t){for(const i of e.getChildren())if(i.is("element","b")&&"normal"===i.getStyle("font-weight")){const n=e.getChildIndex(i);t.remove(i),t.insertChild(n,i.getChildren(),e)}}(i,t),function(e,t){for(const i of t.createRangeIn(e)){const e=i.item;if(e.is("element","li")){const i=e.getChild(0);i&&i.is("element","p")&&t.unwrapElement(i)}}}(i,t),function(e,t){const i=new Tr(t.document.stylesProcessor),n=new ba(i,{renderingMode:"data"}),s=n.blockElements,o=n.inlineObjectElements,r=[];for(const i of t.createRangeIn(e)){const e=i.item;if(e.is("element","br")){const i=eE(e,"forward",t,{blockElements:s,inlineObjectElements:o}),n=eE(e,"backward",t,{blockElements:s,inlineObjectElements:o}),a=tE(i,s);(tE(n,s)||a)&&r.push(e)}}for(const e of r)e.hasClass("Apple-interchange-newline")?t.remove(e):t.replace(e,t.createElement("p"))}(i,t),e.content=i}}const sE=/(\s+)<\/span>/g,((e,t)=>1===t.length?" ":Array(t.length+1).join("  ").substr(0,t.length)))}function aE(e,t){const i=new DOMParser,n=function(e){return rE(rE(e)).replace(/([^\S\r\n]*?)[\r\n]+([^\S\r\n]*<\/span>)/g,"$1$2").replace(/<\/span>/g,"").replace(/()[\r\n]+(<\/span>)/g,"$1 $2").replace(/ <\//g," <\/o:p>/g," ").replace(/( |\u00A0)<\/o:p>/g,"").replace(/>([^\S\r\n]*[\r\n]\s*)<")}(function(e){const t="",i="",n=e.indexOf(t);if(n<0)return e;const s=e.indexOf(i,n+t.length);return e.substring(0,n+t.length)+(s>=0?e.substring(s):"")}(e=(e=e.replace(//,greedy:!0},prolog:{pattern:/<\?[\s\S]+?\?>/,greedy:!0},doctype:{pattern:/"'[\]]|"[^"]*"|'[^']*')+(?:\[(?:[^<"'\]]|"[^"]*"|'[^']*'|<(?!!--)|)*\]\s*)?>/i,greedy:!0,inside:{"internal-subset":{pattern:/(^[^\[]*\[)[\s\S]+(?=\]>$)/,lookbehind:!0,greedy:!0,inside:null},string:{pattern:/"[^"]*"|'[^']*'/,greedy:!0},punctuation:/^$|[[\]]/,"doctype-tag":/^DOCTYPE/i,name:/[^\s<>'"]+/}},cdata:{pattern://i,greedy:!0},tag:{pattern:/<\/?(?!\d)[^\s>\/=$<%]+(?:\s(?:\s*[^\s>\/=]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+(?=[\s>]))|(?=[\s/>])))+)?\s*\/?>/,greedy:!0,inside:{tag:{pattern:/^<\/?[^\s>\/]+/,inside:{punctuation:/^<\/?/,namespace:/^[^\s>\/:]+:/}},"special-attr":[],"attr-value":{pattern:/=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+)/,inside:{punctuation:[{pattern:/^=/,alias:"attr-equals"},/"|'/]}},punctuation:/\/?>/,"attr-name":{pattern:/[^\s>\/]+/,inside:{namespace:/^[^\s>\/:]+:/}}}},entity:[{pattern:/&[\da-z]{1,8};/i,alias:"named-entity"},/&#x?[\da-f]{1,8};/i]},t.languages.markup.tag.inside["attr-value"].inside.entity=t.languages.markup.entity,t.languages.markup.doctype.inside["internal-subset"].inside=t.languages.markup,t.hooks.add("wrap",(function(e){"entity"===e.type&&(e.attributes.title=e.content.replace(/&/,"&"));})),Object.defineProperty(t.languages.markup.tag,"addInlined",{value:function(e,n){var a={};a["language-"+n]={pattern:/(^$)/i,lookbehind:!0,inside:t.languages[n]},a.cdata=/^$/i;var r={"included-cdata":{pattern://i,inside:a}};r["language-"+n]={pattern:/[\s\S]+/,inside:t.languages[n]};var i={};i[e]={pattern:RegExp(/(<__[^>]*>)(?:))*\]\]>|(?!)/.source.replace(/__/g,(function(){return e})),"i"),lookbehind:!0,greedy:!0,inside:r},t.languages.insertBefore("markup","cdata",i);}}),Object.defineProperty(t.languages.markup.tag,"addAttribute",{value:function(e,n){t.languages.markup.tag.inside["special-attr"].push({pattern:RegExp(/(^|["'\s])/.source+"(?:"+e+")"+/\s*=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+(?=[\s>]))/.source,"i"),lookbehind:!0,inside:{"attr-name":/^[^\s=]+/,"attr-value":{pattern:/=[\s\S]+/,inside:{value:{pattern:/(^=\s*(["']|(?!["'])))\S[\s\S]*(?=\2$)/,lookbehind:!0,alias:[n,"language-"+n],inside:t.languages[n]},punctuation:[{pattern:/^=/,alias:"attr-equals"},/"|'/]}}}});}}),t.languages.html=t.languages.markup,t.languages.mathml=t.languages.markup,t.languages.svg=t.languages.markup,t.languages.xml=t.languages.extend("markup",{}),t.languages.ssml=t.languages.xml,t.languages.atom=t.languages.xml,t.languages.rss=t.languages.xml,function(e){var t=/(?:"(?:\\(?:\r\n|[\s\S])|[^"\\\r\n])*"|'(?:\\(?:\r\n|[\s\S])|[^'\\\r\n])*')/;e.languages.css={comment:/\/\*[\s\S]*?\*\//,atrule:{pattern:/@[\w-](?:[^;{\s]|\s+(?![\s{]))*(?:;|(?=\s*\{))/,inside:{rule:/^@[\w-]+/,"selector-function-argument":{pattern:/(\bselector\s*\(\s*(?![\s)]))(?:[^()\s]|\s+(?![\s)])|\((?:[^()]|\([^()]*\))*\))+(?=\s*\))/,lookbehind:!0,alias:"selector"},keyword:{pattern:/(^|[^\w-])(?:and|not|only|or)(?![\w-])/,lookbehind:!0}}},url:{pattern:RegExp("\\burl\\((?:"+t.source+"|"+/(?:[^\\\r\n()"']|\\[\s\S])*/.source+")\\)","i"),greedy:!0,inside:{function:/^url/i,punctuation:/^\(|\)$/,string:{pattern:RegExp("^"+t.source+"$"),alias:"url"}}},selector:{pattern:RegExp("(^|[{}\\s])[^{}\\s](?:[^{};\"'\\s]|\\s+(?![\\s{])|"+t.source+")*(?=\\s*\\{)"),lookbehind:!0},string:{pattern:t,greedy:!0},property:{pattern:/(^|[^-\w\xA0-\uFFFF])(?!\s)[-_a-z\xA0-\uFFFF](?:(?!\s)[-\w\xA0-\uFFFF])*(?=\s*:)/i,lookbehind:!0},important:/!important\b/i,function:{pattern:/(^|[^-a-z0-9])[-a-z0-9]+(?=\()/i,lookbehind:!0},punctuation:/[(){};:,]/},e.languages.css.atrule.inside.rest=e.languages.css;var n=e.languages.markup;n&&(n.tag.addInlined("style","css"),n.tag.addAttribute("style","css"));}(t),t.languages.clike={comment:[{pattern:/(^|[^\\])\/\*[\s\S]*?(?:\*\/|$)/,lookbehind:!0,greedy:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0,greedy:!0}],string:{pattern:/(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,greedy:!0},"class-name":{pattern:/(\b(?:class|interface|extends|implements|trait|instanceof|new)\s+|\bcatch\s+\()[\w.\\]+/i,lookbehind:!0,inside:{punctuation:/[.\\]/}},keyword:/\b(?:if|else|while|do|for|return|in|instanceof|function|new|try|throw|catch|finally|null|break|continue)\b/,boolean:/\b(?:true|false)\b/,function:/\b\w+(?=\()/,number:/\b0x[\da-f]+\b|(?:\b\d+(?:\.\d*)?|\B\.\d+)(?:e[+-]?\d+)?/i,operator:/[<>]=?|[!=]=?=?|--?|\+\+?|&&?|\|\|?|[?*/~^%]/,punctuation:/[{}[\];(),.:]/},t.languages.javascript=t.languages.extend("clike",{"class-name":[t.languages.clike["class-name"],{pattern:/(^|[^$\w\xA0-\uFFFF])(?!\s)[_$A-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\.(?:prototype|constructor))/,lookbehind:!0}],keyword:[{pattern:/((?:^|\})\s*)catch\b/,lookbehind:!0},{pattern:/(^|[^.]|\.\.\.\s*)\b(?:as|assert(?=\s*\{)|async(?=\s*(?:function\b|\(|[$\w\xA0-\uFFFF]|$))|await|break|case|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally(?=\s*(?:\{|$))|for|from(?=\s*(?:['"]|$))|function|(?:get|set)(?=\s*(?:[#\[$\w\xA0-\uFFFF]|$))|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)\b/,lookbehind:!0}],function:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*(?:\.\s*(?:apply|bind|call)\s*)?\()/,number:/\b(?:(?:0[xX](?:[\dA-Fa-f](?:_[\dA-Fa-f])?)+|0[bB](?:[01](?:_[01])?)+|0[oO](?:[0-7](?:_[0-7])?)+)n?|(?:\d(?:_\d)?)+n|NaN|Infinity)\b|(?:\b(?:\d(?:_\d)?)+\.?(?:\d(?:_\d)?)*|\B\.(?:\d(?:_\d)?)+)(?:[Ee][+-]?(?:\d(?:_\d)?)+)?/,operator:/--|\+\+|\*\*=?|=>|&&=?|\|\|=?|[!=]==|<<=?|>>>?=?|[-+*/%&|^!=<>]=?|\.{3}|\?\?=?|\?\.?|[~:]/}),t.languages.javascript["class-name"][0].pattern=/(\b(?:class|interface|extends|implements|instanceof|new)\s+)[\w.\\]+/,t.languages.insertBefore("javascript","keyword",{regex:{pattern:/((?:^|[^$\w\xA0-\uFFFF."'\])\s]|\b(?:return|yield))\s*)\/(?:\[(?:[^\]\\\r\n]|\\.)*\]|\\.|[^/\\\[\r\n])+\/[dgimyus]{0,7}(?=(?:\s|\/\*(?:[^*]|\*(?!\/))*\*\/)*(?:$|[\r\n,.;:})\]]|\/\/))/,lookbehind:!0,greedy:!0,inside:{"regex-source":{pattern:/^(\/)[\s\S]+(?=\/[a-z]*$)/,lookbehind:!0,alias:"language-regex",inside:t.languages.regex},"regex-delimiter":/^\/|\/$/,"regex-flags":/^[a-z]+$/}},"function-variable":{pattern:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*[=:]\s*(?:async\s*)?(?:\bfunction\b|(?:\((?:[^()]|\([^()]*\))*\)|(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)\s*=>))/,alias:"function"},parameter:[{pattern:/(function(?:\s+(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)?\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\))/,lookbehind:!0,inside:t.languages.javascript},{pattern:/(^|[^$\w\xA0-\uFFFF])(?!\s)[_$a-z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*=>)/i,lookbehind:!0,inside:t.languages.javascript},{pattern:/(\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*=>)/,lookbehind:!0,inside:t.languages.javascript},{pattern:/((?:\b|\s|^)(?!(?:as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)(?![$\w\xA0-\uFFFF]))(?:(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*\s*)\(\s*|\]\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*\{)/,lookbehind:!0,inside:t.languages.javascript}],constant:/\b[A-Z](?:[A-Z_]|\dx?)*\b/}),t.languages.insertBefore("javascript","string",{hashbang:{pattern:/^#!.*/,greedy:!0,alias:"comment"},"template-string":{pattern:/`(?:\\[\s\S]|\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}|(?!\$\{)[^\\`])*`/,greedy:!0,inside:{"template-punctuation":{pattern:/^`|`$/,alias:"string"},interpolation:{pattern:/((?:^|[^\\])(?:\\{2})*)\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}/,lookbehind:!0,inside:{"interpolation-punctuation":{pattern:/^\$\{|\}$/,alias:"punctuation"},rest:t.languages.javascript}},string:/[\s\S]+/}}}),t.languages.markup&&(t.languages.markup.tag.addInlined("script","javascript"),t.languages.markup.tag.addAttribute(/on(?:abort|blur|change|click|composition(?:end|start|update)|dblclick|error|focus(?:in|out)?|key(?:down|up)|load|mouse(?:down|enter|leave|move|out|over|up)|reset|resize|scroll|select|slotchange|submit|unload|wheel)/.source,"javascript")),t.languages.js=t.languages.javascript,function(){if(void 0!==t&&"undefined"!=typeof document){Element.prototype.matches||(Element.prototype.matches=Element.prototype.msMatchesSelector||Element.prototype.webkitMatchesSelector);var e={js:"javascript",py:"python",rb:"ruby",ps1:"powershell",psm1:"powershell",sh:"bash",bat:"batch",h:"c",tex:"latex"},n="data-src-status",a="loading",r="loaded",i='pre[data-src]:not([data-src-status="loaded"]):not([data-src-status="loading"])',s=/\blang(?:uage)?-([\w-]+)\b/i;t.hooks.add("before-highlightall",(function(e){e.selector+=", "+i;})),t.hooks.add("before-sanity-check",(function(s){var o=s.element;if(o.matches(i)){s.code="",o.setAttribute(n,a);var u=o.appendChild(document.createElement("CODE"));u.textContent="Loading…";var c=o.getAttribute("data-src"),d=s.language;if("none"===d){var p=(/\.(\w+)$/.exec(c)||[,"none"])[1];d=e[p]||p;}l(u,d),l(o,d);var g=t.plugins.autoloader;g&&g.loadLanguages(d);var f=new XMLHttpRequest;f.open("GET",c,!0),f.onreadystatechange=function(){var e,a;4==f.readyState&&(f.status<400&&f.responseText?(o.setAttribute(n,r),u.textContent=f.responseText,t.highlightElement(u)):(o.setAttribute(n,"failed"),f.status>=400?u.textContent=(e=f.status,a=f.statusText,"✖ Error "+e+" while fetching file: "+a):u.textContent="✖ Error: File does not exist or is empty"));},f.send(null);}})),t.plugins.fileHighlight={highlight:function(e){for(var n,a=(e||document).querySelectorAll(i),r=0;n=a[r++];)t.highlightElement(n);}};var o=!1;t.fileHighlight=function(){o||(console.warn("Prism.fileHighlight is deprecated. Use `Prism.plugins.fileHighlight.highlight` instead."),o=!0),t.plugins.fileHighlight.highlight.apply(this,arguments);};}function l(e,t){var n=e.className;n=n.replace(s," ")+" language-"+t,e.className=n.replace(/\s+/g," ").trim();}}();}));!function(e){var t=e.util.clone(e.languages.javascript),n=/(?:\s|\/\/.*(?!.)|\/\*(?:[^*]|\*(?!\/))\*\/)/.source,a=/(?:\{(?:\{(?:\{[^{}]*\}|[^{}])*\}|[^{}])*\})/.source,r=/(?:\{*\.{3}(?:[^{}]|)*\})/.source;function i(e,t){return e=e.replace(//g,(function(){return n})).replace(//g,(function(){return a})).replace(//g,(function(){return r})),RegExp(e,t)}r=i(r).source,e.languages.jsx=e.languages.extend("markup",t),e.languages.jsx.tag.pattern=i(/<\/?(?:[\w.:-]+(?:+(?:[\w.:$-]+(?:=(?:"(?:\\[\s\S]|[^\\"])*"|'(?:\\[\s\S]|[^\\'])*'|[^\s{'"/>=]+|))?|))**\/?)?>/.source),e.languages.jsx.tag.inside.tag.pattern=/^<\/?[^\s>\/]*/i,e.languages.jsx.tag.inside["attr-value"].pattern=/=(?!\{)(?:"(?:\\[\s\S]|[^\\"])*"|'(?:\\[\s\S]|[^\\'])*'|[^\s'">]+)/i,e.languages.jsx.tag.inside.tag.inside["class-name"]=/^[A-Z]\w*(?:\.[A-Z]\w*)*$/,e.languages.jsx.tag.inside.comment=t.comment,e.languages.insertBefore("inside","attr-name",{spread:{pattern:i(//.source),inside:e.languages.jsx}},e.languages.jsx.tag),e.languages.insertBefore("inside","special-attr",{script:{pattern:i(/=/.source),inside:{"script-punctuation":{pattern:/^=(?=\{)/,alias:"punctuation"},rest:e.languages.jsx},alias:"language-javascript"}},e.languages.jsx.tag);var s=function(e){return e?"string"==typeof e?e:"string"==typeof e.content?e.content:e.content.map(s).join(""):""},o=function(t){for(var n=[],a=0;a0&&n[n.length-1].tagName===s(r.content[0].content[1])&&n.pop():"/>"===r.content[r.content.length-1].content||n.push({tagName:s(r.content[0].content[1]),openedBraces:0}):n.length>0&&"punctuation"===r.type&&"{"===r.content?n[n.length-1].openedBraces++:n.length>0&&n[n.length-1].openedBraces>0&&"punctuation"===r.type&&"}"===r.content?n[n.length-1].openedBraces--:i=!0),(i||"string"==typeof r)&&n.length>0&&0===n[n.length-1].openedBraces){var l=s(r);a0&&("string"==typeof t[a-1]||"plain-text"===t[a-1].type)&&(l=s(t[a-1])+l,t.splice(a-1,1),a--),t[a]=new e.Token("plain-text",l,null,l);}r.content&&"string"!=typeof r.content&&o(r.content);}};e.hooks.add("after-tokenize",(function(e){"jsx"!==e.language&&"tsx"!==e.language||o(e.tokens);}));}(Prism),function(e){e.languages.typescript=e.languages.extend("javascript",{"class-name":{pattern:/(\b(?:class|extends|implements|instanceof|interface|new|type)\s+)(?!keyof\b)(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?:\s*<(?:[^<>]|<(?:[^<>]|<[^<>]*>)*>)*>)?/,lookbehind:!0,greedy:!0,inside:null},builtin:/\b(?:string|Function|any|number|boolean|Array|symbol|console|Promise|unknown|never)\b/}),e.languages.typescript.keyword.push(/\b(?:abstract|as|declare|implements|is|keyof|readonly|require)\b/,/\b(?:asserts|infer|interface|module|namespace|type)\b(?=\s*(?:[{_$a-zA-Z\xA0-\uFFFF]|$))/,/\btype\b(?=\s*(?:[\{*]|$))/),delete e.languages.typescript.parameter;var t=e.languages.extend("typescript",{});delete t["class-name"],e.languages.typescript["class-name"].inside=t,e.languages.insertBefore("typescript","function",{decorator:{pattern:/@[$\w\xA0-\uFFFF]+/,inside:{at:{pattern:/^@/,alias:"operator"},function:/^[\s\S]+/}},"generic-function":{pattern:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*\s*<(?:[^<>]|<(?:[^<>]|<[^<>]*>)*>)*>(?=\s*\()/,greedy:!0,inside:{function:/^#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*/,generic:{pattern:/<[\s\S]+/,alias:"class-name",inside:t}}}}),e.languages.ts=e.languages.typescript;}(Prism),Prism.languages.markup={comment:{pattern://,greedy:!0},prolog:{pattern:/<\?[\s\S]+?\?>/,greedy:!0},doctype:{pattern:/"'[\]]|"[^"]*"|'[^']*')+(?:\[(?:[^<"'\]]|"[^"]*"|'[^']*'|<(?!!--)|)*\]\s*)?>/i,greedy:!0,inside:{"internal-subset":{pattern:/(^[^\[]*\[)[\s\S]+(?=\]>$)/,lookbehind:!0,greedy:!0,inside:null},string:{pattern:/"[^"]*"|'[^']*'/,greedy:!0},punctuation:/^$|[[\]]/,"doctype-tag":/^DOCTYPE/i,name:/[^\s<>'"]+/}},cdata:{pattern://i,greedy:!0},tag:{pattern:/<\/?(?!\d)[^\s>\/=$<%]+(?:\s(?:\s*[^\s>\/=]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+(?=[\s>]))|(?=[\s/>])))+)?\s*\/?>/,greedy:!0,inside:{tag:{pattern:/^<\/?[^\s>\/]+/,inside:{punctuation:/^<\/?/,namespace:/^[^\s>\/:]+:/}},"special-attr":[],"attr-value":{pattern:/=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+)/,inside:{punctuation:[{pattern:/^=/,alias:"attr-equals"},/"|'/]}},punctuation:/\/?>/,"attr-name":{pattern:/[^\s>\/]+/,inside:{namespace:/^[^\s>\/:]+:/}}}},entity:[{pattern:/&[\da-z]{1,8};/i,alias:"named-entity"},/&#x?[\da-f]{1,8};/i]},Prism.languages.markup.tag.inside["attr-value"].inside.entity=Prism.languages.markup.entity,Prism.languages.markup.doctype.inside["internal-subset"].inside=Prism.languages.markup,Prism.hooks.add("wrap",(function(e){"entity"===e.type&&(e.attributes.title=e.content.replace(/&/,"&"));})),Object.defineProperty(Prism.languages.markup.tag,"addInlined",{value:function(e,t){var n={};n["language-"+t]={pattern:/(^$)/i,lookbehind:!0,inside:Prism.languages[t]},n.cdata=/^$/i;var a={"included-cdata":{pattern://i,inside:n}};a["language-"+t]={pattern:/[\s\S]+/,inside:Prism.languages[t]};var r={};r[e]={pattern:RegExp(/(<__[^>]*>)(?:))*\]\]>|(?!)/.source.replace(/__/g,(function(){return e})),"i"),lookbehind:!0,greedy:!0,inside:a},Prism.languages.insertBefore("markup","cdata",r);}}),Object.defineProperty(Prism.languages.markup.tag,"addAttribute",{value:function(e,t){Prism.languages.markup.tag.inside["special-attr"].push({pattern:RegExp(/(^|["'\s])/.source+"(?:"+e+")"+/\s*=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+(?=[\s>]))/.source,"i"),lookbehind:!0,inside:{"attr-name":/^[^\s=]+/,"attr-value":{pattern:/=[\s\S]+/,inside:{value:{pattern:/(^=\s*(["']|(?!["'])))\S[\s\S]*(?=\2$)/,lookbehind:!0,alias:[t,"language-"+t],inside:Prism.languages[t]},punctuation:[{pattern:/^=/,alias:"attr-equals"},/"|'/]}}}});}}),Prism.languages.html=Prism.languages.markup,Prism.languages.mathml=Prism.languages.markup,Prism.languages.svg=Prism.languages.markup,Prism.languages.xml=Prism.languages.extend("markup",{}),Prism.languages.ssml=Prism.languages.xml,Prism.languages.atom=Prism.languages.xml,Prism.languages.rss=Prism.languages.xml,Prism.languages.go=Prism.languages.extend("clike",{string:{pattern:/(["'`])(?:\\[\s\S]|(?!\1)[^\\])*\1/,greedy:!0},keyword:/\b(?:break|case|chan|const|continue|default|defer|else|fallthrough|for|func|go(?:to)?|if|import|interface|map|package|range|return|select|struct|switch|type|var)\b/,boolean:/\b(?:_|iota|nil|true|false)\b/,number:/(?:\b0x[a-f\d]+|(?:\b\d+(?:\.\d*)?|\B\.\d+)(?:e[-+]?\d+)?)i?/i,operator:/[*\/%^!=]=?|\+[=+]?|-[=-]?|\|[=|]?|&(?:=|&|\^=?)?|>(?:>=?|=)?|<(?:<=?|=|-)?|:=|\.\.\./,builtin:/\b(?:bool|byte|complex(?:64|128)|error|float(?:32|64)|rune|string|u?int(?:8|16|32|64)?|uintptr|append|cap|close|complex|copy|delete|imag|len|make|new|panic|print(?:ln)?|real|recover)\b/}),delete Prism.languages.go["class-name"],function(e){var t=/\/\*[\s\S]*?\*\/|\/\/.*|#(?!\[).*/,n=[{pattern:/\b(?:false|true)\b/i,alias:"boolean"},{pattern:/(::\s*)\b[a-z_]\w*\b(?!\s*\()/i,greedy:!0,lookbehind:!0},{pattern:/(\b(?:case|const)\s+)\b[a-z_]\w*(?=\s*[;=])/i,greedy:!0,lookbehind:!0},/\b(?:null)\b/i,/\b[A-Z_][A-Z0-9_]*\b(?!\s*\()/],a=/\b0b[01]+(?:_[01]+)*\b|\b0o[0-7]+(?:_[0-7]+)*\b|\b0x[\da-f]+(?:_[\da-f]+)*\b|(?:\b\d+(?:_\d+)*\.?(?:\d+(?:_\d+)*)?|\B\.\d+)(?:e[+-]?\d+)?/i,r=/|\?\?=?|\.{3}|\??->|[!=]=?=?|::|\*\*=?|--|\+\+|&&|\|\||<<|>>|[?~]|[/^|%*&<>.+-]=?/,i=/[{}\[\](),:;]/;e.languages.php={delimiter:{pattern:/\?>$|^<\?(?:php(?=\s)|=)?/i,alias:"important"},comment:t,variable:/\$+(?:\w+\b|(?=\{))/i,package:{pattern:/(namespace\s+|use\s+(?:function\s+)?)(?:\\?\b[a-z_]\w*)+\b(?!\\)/i,lookbehind:!0,inside:{punctuation:/\\/}},"class-name-definition":{pattern:/(\b(?:class|enum|interface|trait)\s+)\b[a-z_]\w*(?!\\)\b/i,lookbehind:!0,alias:"class-name"},"function-definition":{pattern:/(\bfunction\s+)[a-z_]\w*(?=\s*\()/i,lookbehind:!0,alias:"function"},keyword:[{pattern:/(\(\s*)\b(?:bool|boolean|int|integer|float|string|object|array)\b(?=\s*\))/i,alias:"type-casting",greedy:!0,lookbehind:!0},{pattern:/([(,?]\s*)\b(?:bool|int|float|string|object|array(?!\s*\()|mixed|self|static|callable|iterable|(?:null|false)(?=\s*\|))\b(?=\s*\$)/i,alias:"type-hint",greedy:!0,lookbehind:!0},{pattern:/([(,?]\s*[\w|]\|\s*)(?:null|false)\b(?=\s*\$)/i,alias:"type-hint",greedy:!0,lookbehind:!0},{pattern:/(\)\s*:\s*(?:\?\s*)?)\b(?:bool|int|float|string|object|void|array(?!\s*\()|mixed|self|static|callable|iterable|(?:null|false)(?=\s*\|))\b/i,alias:"return-type",greedy:!0,lookbehind:!0},{pattern:/(\)\s*:\s*(?:\?\s*)?[\w|]\|\s*)(?:null|false)\b/i,alias:"return-type",greedy:!0,lookbehind:!0},{pattern:/\b(?:bool|int|float|string|object|void|array(?!\s*\()|mixed|iterable|(?:null|false)(?=\s*\|))\b/i,alias:"type-declaration",greedy:!0},{pattern:/(\|\s*)(?:null|false)\b/i,alias:"type-declaration",greedy:!0,lookbehind:!0},{pattern:/\b(?:parent|self|static)(?=\s*::)/i,alias:"static-context",greedy:!0},{pattern:/(\byield\s+)from\b/i,lookbehind:!0},/\bclass\b/i,{pattern:/((?:^|[^\s>:]|(?:^|[^-])>|(?:^|[^:]):)\s*)\b(?:__halt_compiler|abstract|and|array|as|break|callable|case|catch|clone|const|continue|declare|default|die|do|echo|else|elseif|empty|enddeclare|endfor|endforeach|endif|endswitch|endwhile|enum|eval|exit|extends|final|finally|fn|for|foreach|function|global|goto|if|implements|include|include_once|instanceof|insteadof|interface|isset|list|namespace|match|new|or|parent|print|private|protected|public|require|require_once|return|self|static|switch|throw|trait|try|unset|use|var|while|xor|yield)\b/i,lookbehind:!0}],"argument-name":{pattern:/([(,]\s+)\b[a-z_]\w*(?=\s*:(?!:))/i,lookbehind:!0},"class-name":[{pattern:/(\b(?:extends|implements|instanceof|new(?!\s+self|\s+static))\s+|\bcatch\s*\()\b[a-z_]\w*(?!\\)\b/i,greedy:!0,lookbehind:!0},{pattern:/(\|\s*)\b[a-z_]\w*(?!\\)\b/i,greedy:!0,lookbehind:!0},{pattern:/\b[a-z_]\w*(?!\\)\b(?=\s*\|)/i,greedy:!0},{pattern:/(\|\s*)(?:\\?\b[a-z_]\w*)+\b/i,alias:"class-name-fully-qualified",greedy:!0,lookbehind:!0,inside:{punctuation:/\\/}},{pattern:/(?:\\?\b[a-z_]\w*)+\b(?=\s*\|)/i,alias:"class-name-fully-qualified",greedy:!0,inside:{punctuation:/\\/}},{pattern:/(\b(?:extends|implements|instanceof|new(?!\s+self\b|\s+static\b))\s+|\bcatch\s*\()(?:\\?\b[a-z_]\w*)+\b(?!\\)/i,alias:"class-name-fully-qualified",greedy:!0,lookbehind:!0,inside:{punctuation:/\\/}},{pattern:/\b[a-z_]\w*(?=\s*\$)/i,alias:"type-declaration",greedy:!0},{pattern:/(?:\\?\b[a-z_]\w*)+(?=\s*\$)/i,alias:["class-name-fully-qualified","type-declaration"],greedy:!0,inside:{punctuation:/\\/}},{pattern:/\b[a-z_]\w*(?=\s*::)/i,alias:"static-context",greedy:!0},{pattern:/(?:\\?\b[a-z_]\w*)+(?=\s*::)/i,alias:["class-name-fully-qualified","static-context"],greedy:!0,inside:{punctuation:/\\/}},{pattern:/([(,?]\s*)[a-z_]\w*(?=\s*\$)/i,alias:"type-hint",greedy:!0,lookbehind:!0},{pattern:/([(,?]\s*)(?:\\?\b[a-z_]\w*)+(?=\s*\$)/i,alias:["class-name-fully-qualified","type-hint"],greedy:!0,lookbehind:!0,inside:{punctuation:/\\/}},{pattern:/(\)\s*:\s*(?:\?\s*)?)\b[a-z_]\w*(?!\\)\b/i,alias:"return-type",greedy:!0,lookbehind:!0},{pattern:/(\)\s*:\s*(?:\?\s*)?)(?:\\?\b[a-z_]\w*)+\b(?!\\)/i,alias:["class-name-fully-qualified","return-type"],greedy:!0,lookbehind:!0,inside:{punctuation:/\\/}}],constant:n,function:{pattern:/(^|[^\\\w])\\?[a-z_](?:[\w\\]*\w)?(?=\s*\()/i,lookbehind:!0,inside:{punctuation:/\\/}},property:{pattern:/(->\s*)\w+/,lookbehind:!0},number:a,operator:r,punctuation:i};var s={pattern:/\{\$(?:\{(?:\{[^{}]+\}|[^{}]+)\}|[^{}])+\}|(^|[^\\{])\$+(?:\w+(?:\[[^\r\n\[\]]+\]|->\w+)?)/,lookbehind:!0,inside:e.languages.php},o=[{pattern:/<<<'([^']+)'[\r\n](?:.*[\r\n])*?\1;/,alias:"nowdoc-string",greedy:!0,inside:{delimiter:{pattern:/^<<<'[^']+'|[a-z_]\w*;$/i,alias:"symbol",inside:{punctuation:/^<<<'?|[';]$/}}}},{pattern:/<<<(?:"([^"]+)"[\r\n](?:.*[\r\n])*?\1;|([a-z_]\w*)[\r\n](?:.*[\r\n])*?\2;)/i,alias:"heredoc-string",greedy:!0,inside:{delimiter:{pattern:/^<<<(?:"[^"]+"|[a-z_]\w*)|[a-z_]\w*;$/i,alias:"symbol",inside:{punctuation:/^<<<"?|[";]$/}},interpolation:s}},{pattern:/`(?:\\[\s\S]|[^\\`])*`/,alias:"backtick-quoted-string",greedy:!0},{pattern:/'(?:\\[\s\S]|[^\\'])*'/,alias:"single-quoted-string",greedy:!0},{pattern:/"(?:\\[\s\S]|[^\\"])*"/,alias:"double-quoted-string",greedy:!0,inside:{interpolation:s}}];e.languages.insertBefore("php","variable",{string:o,attribute:{pattern:/#\[(?:[^"'\/#]|\/(?![*/])|\/\/.*$|#(?!\[).*$|\/\*(?:[^*]|\*(?!\/))*\*\/|"(?:\\[\s\S]|[^\\"])*"|'(?:\\[\s\S]|[^\\'])*')+\](?=\s*[a-z$#])/im,greedy:!0,inside:{"attribute-content":{pattern:/^(#\[)[\s\S]+(?=\]$)/,lookbehind:!0,inside:{comment:t,string:o,"attribute-class-name":[{pattern:/([^:]|^)\b[a-z_]\w*(?!\\)\b/i,alias:"class-name",greedy:!0,lookbehind:!0},{pattern:/([^:]|^)(?:\\?\b[a-z_]\w*)+/i,alias:["class-name","class-name-fully-qualified"],greedy:!0,lookbehind:!0,inside:{punctuation:/\\/}}],constant:n,number:a,operator:r,punctuation:i}},delimiter:{pattern:/^#\[|\]$/,alias:"punctuation"}}}}),e.hooks.add("before-tokenize",(function(t){if(/<\?/.test(t.code)){e.languages["markup-templating"].buildPlaceholders(t,"php",/<\?(?:[^"'/#]|\/(?![*/])|("|')(?:\\[\s\S]|(?!\1)[^\\])*\1|(?:\/\/|#(?!\[))(?:[^?\n\r]|\?(?!>))*(?=$|\?>|[\r\n])|#\[|\/\*(?:[^*]|\*(?!\/))*(?:\*\/|$))*?(?:\?>|$)/gi);}})),e.hooks.add("after-tokenize",(function(t){e.languages["markup-templating"].tokenizePlaceholders(t,"php");}));}(Prism),Prism.languages.c=Prism.languages.extend("clike",{comment:{pattern:/\/\/(?:[^\r\n\\]|\\(?:\r\n?|\n|(?![\r\n])))*|\/\*[\s\S]*?(?:\*\/|$)/,greedy:!0},"class-name":{pattern:/(\b(?:enum|struct)\s+(?:__attribute__\s*\(\([\s\S]*?\)\)\s*)?)\w+|\b[a-z]\w*_t\b/,lookbehind:!0},keyword:/\b(?:__attribute__|_Alignas|_Alignof|_Atomic|_Bool|_Complex|_Generic|_Imaginary|_Noreturn|_Static_assert|_Thread_local|asm|typeof|inline|auto|break|case|char|const|continue|default|do|double|else|enum|extern|float|for|goto|if|int|long|register|return|short|signed|sizeof|static|struct|switch|typedef|union|unsigned|void|volatile|while)\b/,function:/\b[a-z_]\w*(?=\s*\()/i,number:/(?:\b0x(?:[\da-f]+(?:\.[\da-f]*)?|\.[\da-f]+)(?:p[+-]?\d+)?|(?:\b\d+(?:\.\d*)?|\B\.\d+)(?:e[+-]?\d+)?)[ful]{0,4}/i,operator:/>>=?|<<=?|->|([-+&|:])\1|[?:~]|[-+*/%&|^!=<>]=?/}),Prism.languages.insertBefore("c","string",{macro:{pattern:/(^[\t ]*)#\s*[a-z](?:[^\r\n\\/]|\/(?!\*)|\/\*(?:[^*]|\*(?!\/))*\*\/|\\(?:\r\n|[\s\S]))*/im,lookbehind:!0,greedy:!0,alias:"property",inside:{string:[{pattern:/^(#\s*include\s*)<[^>]+>/,lookbehind:!0},Prism.languages.c.string],comment:Prism.languages.c.comment,"macro-name":[{pattern:/(^#\s*define\s+)\w+\b(?!\()/i,lookbehind:!0},{pattern:/(^#\s*define\s+)\w+\b(?=\()/i,lookbehind:!0,alias:"function"}],directive:{pattern:/^(#\s*)[a-z]+/,lookbehind:!0,alias:"keyword"},"directive-hash":/^#/,punctuation:/##|\\(?=[\r\n])/,expression:{pattern:/\S[\s\S]*/,inside:Prism.languages.c}}},constant:/\b(?:__FILE__|__LINE__|__DATE__|__TIME__|__TIMESTAMP__|__func__|EOF|NULL|SEEK_CUR|SEEK_END|SEEK_SET|stdin|stdout|stderr)\b/}),delete Prism.languages.c.boolean,Prism.languages.python={comment:{pattern:/(^|[^\\])#.*/,lookbehind:!0},"string-interpolation":{pattern:/(?:f|rf|fr)(?:("""|''')[\s\S]*?\1|("|')(?:\\.|(?!\2)[^\\\r\n])*\2)/i,greedy:!0,inside:{interpolation:{pattern:/((?:^|[^{])(?:\{\{)*)\{(?!\{)(?:[^{}]|\{(?!\{)(?:[^{}]|\{(?!\{)(?:[^{}])+\})+\})+\}/,lookbehind:!0,inside:{"format-spec":{pattern:/(:)[^:(){}]+(?=\}$)/,lookbehind:!0},"conversion-option":{pattern:/![sra](?=[:}]$)/,alias:"punctuation"},rest:null}},string:/[\s\S]+/}},"triple-quoted-string":{pattern:/(?:[rub]|rb|br)?("""|''')[\s\S]*?\1/i,greedy:!0,alias:"string"},string:{pattern:/(?:[rub]|rb|br)?("|')(?:\\.|(?!\1)[^\\\r\n])*\1/i,greedy:!0},function:{pattern:/((?:^|\s)def[ \t]+)[a-zA-Z_]\w*(?=\s*\()/g,lookbehind:!0},"class-name":{pattern:/(\bclass\s+)\w+/i,lookbehind:!0},decorator:{pattern:/(^[\t ]*)@\w+(?:\.\w+)*/im,lookbehind:!0,alias:["annotation","punctuation"],inside:{punctuation:/\./}},keyword:/\b(?:and|as|assert|async|await|break|class|continue|def|del|elif|else|except|exec|finally|for|from|global|if|import|in|is|lambda|nonlocal|not|or|pass|print|raise|return|try|while|with|yield)\b/,builtin:/\b(?:__import__|abs|all|any|apply|ascii|basestring|bin|bool|buffer|bytearray|bytes|callable|chr|classmethod|cmp|coerce|compile|complex|delattr|dict|dir|divmod|enumerate|eval|execfile|file|filter|float|format|frozenset|getattr|globals|hasattr|hash|help|hex|id|input|int|intern|isinstance|issubclass|iter|len|list|locals|long|map|max|memoryview|min|next|object|oct|open|ord|pow|property|range|raw_input|reduce|reload|repr|reversed|round|set|setattr|slice|sorted|staticmethod|str|sum|super|tuple|type|unichr|unicode|vars|xrange|zip)\b/,boolean:/\b(?:True|False|None)\b/,number:/\b0(?:b(?:_?[01])+|o(?:_?[0-7])+|x(?:_?[a-f0-9])+)\b|(?:\b\d+(?:_\d+)*(?:\.(?:\d+(?:_\d+)*)?)?|\B\.\d+(?:_\d+)*)(?:e[+-]?\d+(?:_\d+)*)?j?\b/i,operator:/[-+%=]=?|!=|\*\*?=?|\/\/?=?|<[<=>]?|>[=>]?|[&|^~]/,punctuation:/[{}[\];(),.:]/},Prism.languages.python["string-interpolation"].inside.interpolation.inside.rest=Prism.languages.python,Prism.languages.py=Prism.languages.python,function(e){var t=/\b(?:abstract|assert|boolean|break|byte|case|catch|char|class|const|continue|default|do|double|else|enum|exports|extends|final|finally|float|for|goto|if|implements|import|instanceof|int|interface|long|module|native|new|non-sealed|null|open|opens|package|permits|private|protected|provides|public|record|requires|return|sealed|short|static|strictfp|super|switch|synchronized|this|throw|throws|to|transient|transitive|try|uses|var|void|volatile|while|with|yield)\b/,n=/(^|[^\w.])(?:[a-z]\w*\s*\.\s*)*(?:[A-Z]\w*\s*\.\s*)*/.source,a={pattern:RegExp(n+/[A-Z](?:[\d_A-Z]*[a-z]\w*)?\b/.source),lookbehind:!0,inside:{namespace:{pattern:/^[a-z]\w*(?:\s*\.\s*[a-z]\w*)*(?:\s*\.)?/,inside:{punctuation:/\./}},punctuation:/\./}};e.languages.java=e.languages.extend("clike",{"class-name":[a,{pattern:RegExp(n+/[A-Z]\w*(?=\s+\w+\s*[;,=()])/.source),lookbehind:!0,inside:a.inside}],keyword:t,function:[e.languages.clike.function,{pattern:/(::\s*)[a-z_]\w*/,lookbehind:!0}],number:/\b0b[01][01_]*L?\b|\b0x(?:\.[\da-f_p+-]+|[\da-f_]+(?:\.[\da-f_p+-]+)?)\b|(?:\b\d[\d_]*(?:\.[\d_]*)?|\B\.\d[\d_]*)(?:e[+-]?\d[\d_]*)?[dfl]?/i,operator:{pattern:/(^|[^.])(?:<<=?|>>>?=?|->|--|\+\+|&&|\|\||::|[?:~]|[-+*/%&|^!=<>]=?)/m,lookbehind:!0}}),e.languages.insertBefore("java","string",{"triple-quoted-string":{pattern:/"""[ \t]*[\r\n](?:(?:"|"")?(?:\\.|[^"\\]))*"""/,greedy:!0,alias:"string"}}),e.languages.insertBefore("java","class-name",{annotation:{pattern:/(^|[^.])@\w+(?:\s*\.\s*\w+)*/,lookbehind:!0,alias:"punctuation"},generics:{pattern:/<(?:[\w\s,.?]|&(?!&)|<(?:[\w\s,.?]|&(?!&)|<(?:[\w\s,.?]|&(?!&)|<(?:[\w\s,.?]|&(?!&))*>)*>)*>)*>/,inside:{"class-name":a,keyword:t,punctuation:/[<>(),.:]/,operator:/[?&|]/}},namespace:{pattern:RegExp(/(\b(?:exports|import(?:\s+static)?|module|open|opens|package|provides|requires|to|transitive|uses|with)\s+)(?!)[a-z]\w*(?:\.[a-z]\w*)*\.?/.source.replace(//g,(function(){return t.source}))),lookbehind:!0,inside:{punctuation:/\./}}});}(Prism),function(e){var t=/\b(?:alignas|alignof|asm|auto|bool|break|case|catch|char|char8_t|char16_t|char32_t|class|compl|concept|const|consteval|constexpr|constinit|const_cast|continue|co_await|co_return|co_yield|decltype|default|delete|do|double|dynamic_cast|else|enum|explicit|export|extern|final|float|for|friend|goto|if|import|inline|int|int8_t|int16_t|int32_t|int64_t|uint8_t|uint16_t|uint32_t|uint64_t|long|module|mutable|namespace|new|noexcept|nullptr|operator|override|private|protected|public|register|reinterpret_cast|requires|return|short|signed|sizeof|static|static_assert|static_cast|struct|switch|template|this|thread_local|throw|try|typedef|typeid|typename|union|unsigned|using|virtual|void|volatile|wchar_t|while)\b/,n=/\b(?!)\w+(?:\s*\.\s*\w+)*\b/.source.replace(//g,(function(){return t.source}));e.languages.cpp=e.languages.extend("c",{"class-name":[{pattern:RegExp(/(\b(?:class|concept|enum|struct|typename)\s+)(?!)\w+/.source.replace(//g,(function(){return t.source}))),lookbehind:!0},/\b[A-Z]\w*(?=\s*::\s*\w+\s*\()/,/\b[A-Z_]\w*(?=\s*::\s*~\w+\s*\()/i,/\b\w+(?=\s*<(?:[^<>]|<(?:[^<>]|<[^<>]*>)*>)*>\s*::\s*\w+\s*\()/],keyword:t,number:{pattern:/(?:\b0b[01']+|\b0x(?:[\da-f']+(?:\.[\da-f']*)?|\.[\da-f']+)(?:p[+-]?[\d']+)?|(?:\b[\d']+(?:\.[\d']*)?|\B\.[\d']+)(?:e[+-]?[\d']+)?)[ful]{0,4}/i,greedy:!0},operator:/>>=?|<<=?|->|--|\+\+|&&|\|\||[?:~]|<=>|[-+*/%&|^!=<>]=?|\b(?:and|and_eq|bitand|bitor|not|not_eq|or|or_eq|xor|xor_eq)\b/,boolean:/\b(?:true|false)\b/}),e.languages.insertBefore("cpp","string",{module:{pattern:RegExp(/(\b(?:module|import)\s+)/.source+"(?:"+/"(?:\\(?:\r\n|[\s\S])|[^"\\\r\n])*"|<[^<>\r\n]*>/.source+"|"+/(?:\s*:\s*)?|:\s*/.source.replace(//g,(function(){return n}))+")"),lookbehind:!0,greedy:!0,inside:{string:/^[<"][\s\S]+/,operator:/:/,punctuation:/\./}},"raw-string":{pattern:/R"([^()\\ ]{0,16})\([\s\S]*?\)\1"/,alias:"string",greedy:!0}}),e.languages.insertBefore("cpp","keyword",{"generic-function":{pattern:/\b(?!operator\b)[a-z_]\w*\s*<(?:[^<>]|<[^<>]*>)*>(?=\s*\()/i,inside:{function:/^\w+/,generic:{pattern:/<[\s\S]+/,alias:"class-name",inside:e.languages.cpp}}}}),e.languages.insertBefore("cpp","operator",{"double-colon":{pattern:/::/,alias:"punctuation"}}),e.languages.insertBefore("cpp","class-name",{"base-clause":{pattern:/(\b(?:class|struct)\s+\w+\s*:\s*)[^;{}"'\s]+(?:\s+[^;{}"'\s]+)*(?=\s*[;{])/,lookbehind:!0,greedy:!0,inside:e.languages.extend("cpp",{})}}),e.languages.insertBefore("inside","double-colon",{"class-name":/\b[a-z_]\w*\b(?!\s*::)/i},e.languages.cpp["base-clause"]);}(Prism),function(e){function t(e,t){return e.replace(/<<(\d+)>>/g,(function(e,n){return "(?:"+t[+n]+")"}))}function n(e,n,a){return RegExp(t(e,n),a||"")}function a(e,t){for(var n=0;n>/g,(function(){return "(?:"+e+")"}));return e.replace(/<>/g,"[^\\s\\S]")}var r="bool byte char decimal double dynamic float int long object sbyte short string uint ulong ushort var void",i="class enum interface record struct",s="add alias and ascending async await by descending from(?=\\s*(?:\\w|$)) get global group into init(?=\\s*;) join let nameof not notnull on or orderby partial remove select set unmanaged value when where with(?=\\s*{)",o="abstract as base break case catch checked const continue default delegate do else event explicit extern finally fixed for foreach goto if implicit in internal is lock namespace new null operator out override params private protected public readonly ref return sealed sizeof stackalloc static switch this throw try typeof unchecked unsafe using virtual volatile while yield";function l(e){return "\\b(?:"+e.trim().replace(/ /g,"|")+")\\b"}var u=l(i),c=RegExp(l(r+" "+i+" "+s+" "+o)),d=l(i+" "+s+" "+o),p=l(r+" "+i+" "+o),g=a(/<(?:[^<>;=+\-*/%&|^]|<>)*>/.source,2),f=a(/\((?:[^()]|<>)*\)/.source,2),b=/@?\b[A-Za-z_]\w*\b/.source,h=t(/<<0>>(?:\s*<<1>>)?/.source,[b,g]),m=t(/(?!<<0>>)<<1>>(?:\s*\.\s*<<1>>)*/.source,[d,h]),y=/\[\s*(?:,\s*)*\]/.source,E=t(/<<0>>(?:\s*(?:\?\s*)?<<1>>)*(?:\s*\?)?/.source,[m,y]),v=t(/[^,()<>[\];=+\-*/%&|^]|<<0>>|<<1>>|<<2>>/.source,[g,f,y]),S=t(/\(<<0>>+(?:,<<0>>+)+\)/.source,[v]),w=t(/(?:<<0>>|<<1>>)(?:\s*(?:\?\s*)?<<2>>)*(?:\s*\?)?/.source,[S,m,y]),k={keyword:c,punctuation:/[<>()?,.:[\]]/},A=/'(?:[^\r\n'\\]|\\.|\\[Uux][\da-fA-F]{1,8})'/.source,x=/"(?:\\.|[^\\"\r\n])*"/.source,T=/@"(?:""|\\[\s\S]|[^\\"])*"(?!")/.source;e.languages.csharp=e.languages.extend("clike",{string:[{pattern:n(/(^|[^$\\])<<0>>/.source,[T]),lookbehind:!0,greedy:!0},{pattern:n(/(^|[^@$\\])<<0>>/.source,[x]),lookbehind:!0,greedy:!0},{pattern:RegExp(A),greedy:!0,alias:"character"}],"class-name":[{pattern:n(/(\busing\s+static\s+)<<0>>(?=\s*;)/.source,[m]),lookbehind:!0,inside:k},{pattern:n(/(\busing\s+<<0>>\s*=\s*)<<1>>(?=\s*;)/.source,[b,w]),lookbehind:!0,inside:k},{pattern:n(/(\busing\s+)<<0>>(?=\s*=)/.source,[b]),lookbehind:!0},{pattern:n(/(\b<<0>>\s+)<<1>>/.source,[u,h]),lookbehind:!0,inside:k},{pattern:n(/(\bcatch\s*\(\s*)<<0>>/.source,[m]),lookbehind:!0,inside:k},{pattern:n(/(\bwhere\s+)<<0>>/.source,[b]),lookbehind:!0},{pattern:n(/(\b(?:is(?:\s+not)?|as)\s+)<<0>>/.source,[E]),lookbehind:!0,inside:k},{pattern:n(/\b<<0>>(?=\s+(?!<<1>>|with\s*\{)<<2>>(?:\s*[=,;:{)\]]|\s+(?:in|when)\b))/.source,[w,p,b]),inside:k}],keyword:c,number:/(?:\b0(?:x[\da-f_]*[\da-f]|b[01_]*[01])|(?:\B\.\d+(?:_+\d+)*|\b\d+(?:_+\d+)*(?:\.\d+(?:_+\d+)*)?)(?:e[-+]?\d+(?:_+\d+)*)?)(?:ul|lu|[dflmu])?\b/i,operator:/>>=?|<<=?|[-=]>|([-+&|])\1|~|\?\?=?|[-+*/%&|^!=<>]=?/,punctuation:/\?\.?|::|[{}[\];(),.:]/}),e.languages.insertBefore("csharp","number",{range:{pattern:/\.\./,alias:"operator"}}),e.languages.insertBefore("csharp","punctuation",{"named-parameter":{pattern:n(/([(,]\s*)<<0>>(?=\s*:)/.source,[b]),lookbehind:!0,alias:"punctuation"}}),e.languages.insertBefore("csharp","class-name",{namespace:{pattern:n(/(\b(?:namespace|using)\s+)<<0>>(?:\s*\.\s*<<0>>)*(?=\s*[;{])/.source,[b]),lookbehind:!0,inside:{punctuation:/\./}},"type-expression":{pattern:n(/(\b(?:default|typeof|sizeof)\s*\(\s*(?!\s))(?:[^()\s]|\s(?!\s)|<<0>>)*(?=\s*\))/.source,[f]),lookbehind:!0,alias:"class-name",inside:k},"return-type":{pattern:n(/<<0>>(?=\s+(?:<<1>>\s*(?:=>|[({]|\.\s*this\s*\[)|this\s*\[))/.source,[w,m]),inside:k,alias:"class-name"},"constructor-invocation":{pattern:n(/(\bnew\s+)<<0>>(?=\s*[[({])/.source,[w]),lookbehind:!0,inside:k,alias:"class-name"},"generic-method":{pattern:n(/<<0>>\s*<<1>>(?=\s*\()/.source,[b,g]),inside:{function:n(/^<<0>>/.source,[b]),generic:{pattern:RegExp(g),alias:"class-name",inside:k}}},"type-list":{pattern:n(/\b((?:<<0>>\s+<<1>>|record\s+<<1>>\s*<<5>>|where\s+<<2>>)\s*:\s*)(?:<<3>>|<<4>>|<<1>>\s*<<5>>|<<6>>)(?:\s*,\s*(?:<<3>>|<<4>>|<<6>>))*(?=\s*(?:where|[{;]|=>|$))/.source,[u,h,b,w,c.source,f,/\bnew\s*\(\s*\)/.source]),lookbehind:!0,inside:{"record-arguments":{pattern:n(/(^(?!new\s*\()<<0>>\s*)<<1>>/.source,[h,f]),lookbehind:!0,greedy:!0,inside:e.languages.csharp},keyword:c,"class-name":{pattern:RegExp(w),greedy:!0,inside:k},punctuation:/[,()]/}},preprocessor:{pattern:/(^[\t ]*)#.*/m,lookbehind:!0,alias:"property",inside:{directive:{pattern:/(#)\b(?:define|elif|else|endif|endregion|error|if|line|nullable|pragma|region|undef|warning)\b/,lookbehind:!0,alias:"keyword"}}}});var _=x+"|"+A,O=t(/\/(?![*/])|\/\/[^\r\n]*[\r\n]|\/\*(?:[^*]|\*(?!\/))*\*\/|<<0>>/.source,[_]),I=a(t(/[^"'/()]|<<0>>|\(<>*\)/.source,[O]),2),R=/\b(?:assembly|event|field|method|module|param|property|return|type)\b/.source,N=t(/<<0>>(?:\s*\(<<1>>*\))?/.source,[m,I]);e.languages.insertBefore("csharp","class-name",{attribute:{pattern:n(/((?:^|[^\s\w>)?])\s*\[\s*)(?:<<0>>\s*:\s*)?<<1>>(?:\s*,\s*<<1>>)*(?=\s*\])/.source,[R,N]),lookbehind:!0,greedy:!0,inside:{target:{pattern:n(/^<<0>>(?=\s*:)/.source,[R]),alias:"keyword"},"attribute-arguments":{pattern:n(/\(<<0>>*\)/.source,[I]),inside:e.languages.csharp},"class-name":{pattern:RegExp(m),inside:{punctuation:/\./}},punctuation:/[:,]/}}});var L=/:[^}\r\n]+/.source,P=a(t(/[^"'/()]|<<0>>|\(<>*\)/.source,[O]),2),C=t(/\{(?!\{)(?:(?![}:])<<0>>)*<<1>>?\}/.source,[P,L]),F=a(t(/[^"'/()]|\/(?!\*)|\/\*(?:[^*]|\*(?!\/))*\*\/|<<0>>|\(<>*\)/.source,[_]),2),D=t(/\{(?!\{)(?:(?![}:])<<0>>)*<<1>>?\}/.source,[F,L]);function $(t,a){return {interpolation:{pattern:n(/((?:^|[^{])(?:\{\{)*)<<0>>/.source,[t]),lookbehind:!0,inside:{"format-string":{pattern:n(/(^\{(?:(?![}:])<<0>>)*)<<1>>(?=\}$)/.source,[a,L]),lookbehind:!0,inside:{punctuation:/^:/}},punctuation:/^\{|\}$/,expression:{pattern:/[\s\S]+/,alias:"language-csharp",inside:e.languages.csharp}}},string:/[\s\S]+/}}e.languages.insertBefore("csharp","string",{"interpolation-string":[{pattern:n(/(^|[^\\])(?:\$@|@\$)"(?:""|\\[\s\S]|\{\{|<<0>>|[^\\{"])*"/.source,[C]),lookbehind:!0,greedy:!0,inside:$(C,P)},{pattern:n(/(^|[^@\\])\$"(?:\\.|\{\{|<<0>>|[^\\"{])*"/.source,[D]),lookbehind:!0,greedy:!0,inside:$(D,F)}]});}(Prism),Prism.languages.dotnet=Prism.languages.cs=Prism.languages.csharp,Prism.languages["visual-basic"]={comment:{pattern:/(?:['‘’]|REM\b)(?:[^\r\n_]|_(?:\r\n?|\n)?)*/i,inside:{keyword:/^REM/i}},directive:{pattern:/#(?:Const|Else|ElseIf|End|ExternalChecksum|ExternalSource|If|Region)(?:[^\S\r\n]_[^\S\r\n]*(?:\r\n?|\n)|.)+/i,alias:"comment",greedy:!0},string:{pattern:/\$?["“”](?:["“”]{2}|[^"“”])*["“”]C?/i,greedy:!0},date:{pattern:/#[^\S\r\n]*(?:\d+([/-])\d+\1\d+(?:[^\S\r\n]+(?:\d+[^\S\r\n]*(?:AM|PM)|\d+:\d+(?::\d+)?(?:[^\S\r\n]*(?:AM|PM))?))?|\d+[^\S\r\n]*(?:AM|PM)|\d+:\d+(?::\d+)?(?:[^\S\r\n]*(?:AM|PM))?)[^\S\r\n]*#/i,alias:"builtin"},number:/(?:(?:\b\d+(?:\.\d+)?|\.\d+)(?:E[+-]?\d+)?|&[HO][\dA-F]+)(?:U?[ILS]|[FRD])?/i,boolean:/\b(?:True|False|Nothing)\b/i,keyword:/\b(?:AddHandler|AddressOf|Alias|And(?:Also)?|As|Boolean|ByRef|Byte|ByVal|Call|Case|Catch|C(?:Bool|Byte|Char|Date|Dbl|Dec|Int|Lng|Obj|SByte|Short|Sng|Str|Type|UInt|ULng|UShort)|Char|Class|Const|Continue|Currency|Date|Decimal|Declare|Default|Delegate|Dim|DirectCast|Do|Double|Each|Else(?:If)?|End(?:If)?|Enum|Erase|Error|Event|Exit|Finally|For|Friend|Function|Get(?:Type|XMLNamespace)?|Global|GoSub|GoTo|Handles|If|Implements|Imports|In|Inherits|Integer|Interface|Is|IsNot|Let|Lib|Like|Long|Loop|Me|Mod|Module|Must(?:Inherit|Override)|My(?:Base|Class)|Namespace|Narrowing|New|Next|Not(?:Inheritable|Overridable)?|Object|Of|On|Operator|Option(?:al)?|Or(?:Else)?|Out|Overloads|Overridable|Overrides|ParamArray|Partial|Private|Property|Protected|Public|RaiseEvent|ReadOnly|ReDim|RemoveHandler|Resume|Return|SByte|Select|Set|Shadows|Shared|short|Single|Static|Step|Stop|String|Structure|Sub|SyncLock|Then|Throw|To|Try|TryCast|Type|TypeOf|U(?:Integer|Long|Short)|Using|Variant|Wend|When|While|Widening|With(?:Events)?|WriteOnly|Until|Xor)\b/i,operator:[/[+\-*/\\^<=>&#@$%!]/,{pattern:/([^\S\r\n])_(?=[^\S\r\n]*[\r\n])/,lookbehind:!0}],punctuation:/[{}().,:?]/},Prism.languages.vb=Prism.languages["visual-basic"],Prism.languages.vba=Prism.languages["visual-basic"],Prism.languages.sql={comment:{pattern:/(^|[^\\])(?:\/\*[\s\S]*?\*\/|(?:--|\/\/|#).*)/,lookbehind:!0},variable:[{pattern:/@(["'`])(?:\\[\s\S]|(?!\1)[^\\])+\1/,greedy:!0},/@[\w.$]+/],string:{pattern:/(^|[^@\\])("|')(?:\\[\s\S]|(?!\2)[^\\]|\2\2)*\2/,greedy:!0,lookbehind:!0},function:/\b(?:AVG|COUNT|FIRST|FORMAT|LAST|LCASE|LEN|MAX|MID|MIN|MOD|NOW|ROUND|SUM|UCASE)(?=\s*\()/i,keyword:/\b(?:ACTION|ADD|AFTER|ALGORITHM|ALL|ALTER|ANALYZE|ANY|APPLY|AS|ASC|AUTHORIZATION|AUTO_INCREMENT|BACKUP|BDB|BEGIN|BERKELEYDB|BIGINT|BINARY|BIT|BLOB|BOOL|BOOLEAN|BREAK|BROWSE|BTREE|BULK|BY|CALL|CASCADED?|CASE|CHAIN|CHAR(?:ACTER|SET)?|CHECK(?:POINT)?|CLOSE|CLUSTERED|COALESCE|COLLATE|COLUMNS?|COMMENT|COMMIT(?:TED)?|COMPUTE|CONNECT|CONSISTENT|CONSTRAINT|CONTAINS(?:TABLE)?|CONTINUE|CONVERT|CREATE|CROSS|CURRENT(?:_DATE|_TIME|_TIMESTAMP|_USER)?|CURSOR|CYCLE|DATA(?:BASES?)?|DATE(?:TIME)?|DAY|DBCC|DEALLOCATE|DEC|DECIMAL|DECLARE|DEFAULT|DEFINER|DELAYED|DELETE|DELIMITERS?|DENY|DESC|DESCRIBE|DETERMINISTIC|DISABLE|DISCARD|DISK|DISTINCT|DISTINCTROW|DISTRIBUTED|DO|DOUBLE|DROP|DUMMY|DUMP(?:FILE)?|DUPLICATE|ELSE(?:IF)?|ENABLE|ENCLOSED|END|ENGINE|ENUM|ERRLVL|ERRORS|ESCAPED?|EXCEPT|EXEC(?:UTE)?|EXISTS|EXIT|EXPLAIN|EXTENDED|FETCH|FIELDS|FILE|FILLFACTOR|FIRST|FIXED|FLOAT|FOLLOWING|FOR(?: EACH ROW)?|FORCE|FOREIGN|FREETEXT(?:TABLE)?|FROM|FULL|FUNCTION|GEOMETRY(?:COLLECTION)?|GLOBAL|GOTO|GRANT|GROUP|HANDLER|HASH|HAVING|HOLDLOCK|HOUR|IDENTITY(?:_INSERT|COL)?|IF|IGNORE|IMPORT|INDEX|INFILE|INNER|INNODB|INOUT|INSERT|INT|INTEGER|INTERSECT|INTERVAL|INTO|INVOKER|ISOLATION|ITERATE|JOIN|KEYS?|KILL|LANGUAGE|LAST|LEAVE|LEFT|LEVEL|LIMIT|LINENO|LINES|LINESTRING|LOAD|LOCAL|LOCK|LONG(?:BLOB|TEXT)|LOOP|MATCH(?:ED)?|MEDIUM(?:BLOB|INT|TEXT)|MERGE|MIDDLEINT|MINUTE|MODE|MODIFIES|MODIFY|MONTH|MULTI(?:LINESTRING|POINT|POLYGON)|NATIONAL|NATURAL|NCHAR|NEXT|NO|NONCLUSTERED|NULLIF|NUMERIC|OFF?|OFFSETS?|ON|OPEN(?:DATASOURCE|QUERY|ROWSET)?|OPTIMIZE|OPTION(?:ALLY)?|ORDER|OUT(?:ER|FILE)?|OVER|PARTIAL|PARTITION|PERCENT|PIVOT|PLAN|POINT|POLYGON|PRECEDING|PRECISION|PREPARE|PREV|PRIMARY|PRINT|PRIVILEGES|PROC(?:EDURE)?|PUBLIC|PURGE|QUICK|RAISERROR|READS?|REAL|RECONFIGURE|REFERENCES|RELEASE|RENAME|REPEAT(?:ABLE)?|REPLACE|REPLICATION|REQUIRE|RESIGNAL|RESTORE|RESTRICT|RETURN(?:S|ING)?|REVOKE|RIGHT|ROLLBACK|ROUTINE|ROW(?:COUNT|GUIDCOL|S)?|RTREE|RULE|SAVE(?:POINT)?|SCHEMA|SECOND|SELECT|SERIAL(?:IZABLE)?|SESSION(?:_USER)?|SET(?:USER)?|SHARE|SHOW|SHUTDOWN|SIMPLE|SMALLINT|SNAPSHOT|SOME|SONAME|SQL|START(?:ING)?|STATISTICS|STATUS|STRIPED|SYSTEM_USER|TABLES?|TABLESPACE|TEMP(?:ORARY|TABLE)?|TERMINATED|TEXT(?:SIZE)?|THEN|TIME(?:STAMP)?|TINY(?:BLOB|INT|TEXT)|TOP?|TRAN(?:SACTIONS?)?|TRIGGER|TRUNCATE|TSEQUAL|TYPES?|UNBOUNDED|UNCOMMITTED|UNDEFINED|UNION|UNIQUE|UNLOCK|UNPIVOT|UNSIGNED|UPDATE(?:TEXT)?|USAGE|USE|USER|USING|VALUES?|VAR(?:BINARY|CHAR|CHARACTER|YING)|VIEW|WAITFOR|WARNINGS|WHEN|WHERE|WHILE|WITH(?: ROLLUP|IN)?|WORK|WRITE(?:TEXT)?|YEAR)\b/i,boolean:/\b(?:TRUE|FALSE|NULL)\b/i,number:/\b0x[\da-f]+\b|\b\d+(?:\.\d*)?|\B\.\d+\b/i,operator:/[-+*\/=%^~]|&&?|\|\|?|!=?|<(?:=>?|<|>)?|>[>=]?|\b(?:AND|BETWEEN|DIV|IN|ILIKE|IS|LIKE|NOT|OR|REGEXP|RLIKE|SOUNDS LIKE|XOR)\b/i,punctuation:/[;[\]()`,.]/},function(e){e.languages.ruby=e.languages.extend("clike",{comment:[/#.*/,{pattern:/^=begin\s[\s\S]*?^=end/m,greedy:!0}],"class-name":{pattern:/(\b(?:class)\s+|\bcatch\s+\()[\w.\\]+/i,lookbehind:!0,inside:{punctuation:/[.\\]/}},keyword:/\b(?:alias|and|BEGIN|begin|break|case|class|def|define_method|defined|do|each|else|elsif|END|end|ensure|extend|for|if|in|include|module|new|next|nil|not|or|prepend|protected|private|public|raise|redo|require|rescue|retry|return|self|super|then|throw|undef|unless|until|when|while|yield)\b/});var t={pattern:/#\{[^}]+\}/,inside:{delimiter:{pattern:/^#\{|\}$/,alias:"tag"},rest:e.languages.ruby}};delete e.languages.ruby.function,e.languages.insertBefore("ruby","keyword",{regex:[{pattern:RegExp(/%r/.source+"(?:"+[/([^a-zA-Z0-9\s{(\[<])(?:(?!\1)[^\\]|\\[\s\S])*\1/.source,/\((?:[^()\\]|\\[\s\S])*\)/.source,/\{(?:[^#{}\\]|#(?:\{[^}]+\})?|\\[\s\S])*\}/.source,/\[(?:[^\[\]\\]|\\[\s\S])*\]/.source,/<(?:[^<>\\]|\\[\s\S])*>/.source].join("|")+")"+/[egimnosux]{0,6}/.source),greedy:!0,inside:{interpolation:t}},{pattern:/(^|[^/])\/(?!\/)(?:\[[^\r\n\]]+\]|\\.|[^[/\\\r\n])+\/[egimnosux]{0,6}(?=\s*(?:$|[\r\n,.;})#]))/,lookbehind:!0,greedy:!0,inside:{interpolation:t}}],variable:/[@$]+[a-zA-Z_]\w*(?:[?!]|\b)/,symbol:{pattern:/(^|[^:]):[a-zA-Z_]\w*(?:[?!]|\b)/,lookbehind:!0},"method-definition":{pattern:/(\bdef\s+)[\w.]+/,lookbehind:!0,inside:{function:/\w+$/,rest:e.languages.ruby}}}),e.languages.insertBefore("ruby","number",{builtin:/\b(?:Array|Bignum|Binding|Class|Continuation|Dir|Exception|FalseClass|File|Stat|Fixnum|Float|Hash|Integer|IO|MatchData|Method|Module|NilClass|Numeric|Object|Proc|Range|Regexp|String|Struct|TMS|Symbol|ThreadGroup|Thread|Time|TrueClass)\b/,constant:/\b[A-Z]\w*(?:[?!]|\b)/}),e.languages.ruby.string=[{pattern:RegExp(/%[qQiIwWxs]?/.source+"(?:"+[/([^a-zA-Z0-9\s{(\[<])(?:(?!\1)[^\\]|\\[\s\S])*\1/.source,/\((?:[^()\\]|\\[\s\S])*\)/.source,/\{(?:[^#{}\\]|#(?:\{[^}]+\})?|\\[\s\S])*\}/.source,/\[(?:[^\[\]\\]|\\[\s\S])*\]/.source,/<(?:[^<>\\]|\\[\s\S])*>/.source].join("|")+")"),greedy:!0,inside:{interpolation:t}},{pattern:/("|')(?:#\{[^}]+\}|#(?!\{)|\\(?:\r\n|[\s\S])|(?!\1)[^\\#\r\n])*\1/,greedy:!0,inside:{interpolation:t}},{pattern:/<<[-~]?([a-z_]\w*)[\r\n](?:.*[\r\n])*?[\t ]*\1/i,alias:"heredoc-string",greedy:!0,inside:{delimiter:{pattern:/^<<[-~]?[a-z_]\w*|[a-z_]\w*$/i,alias:"symbol",inside:{punctuation:/^<<[-~]?/}},interpolation:t}},{pattern:/<<[-~]?'([a-z_]\w*)'[\r\n](?:.*[\r\n])*?[\t ]*\1/i,alias:"heredoc-string",greedy:!0,inside:{delimiter:{pattern:/^<<[-~]?'[a-z_]\w*'|[a-z_]\w*$/i,alias:"symbol",inside:{punctuation:/^<<[-~]?'|'$/}}}}],e.languages.rb=e.languages.ruby;}(Prism),Prism.languages.swift={comment:{pattern:/(^|[^\\:])(?:\/\/.*|\/\*(?:[^/*]|\/(?!\*)|\*(?!\/)|\/\*(?:[^*]|\*(?!\/))*\*\/)*\*\/)/,lookbehind:!0,greedy:!0},"string-literal":[{pattern:RegExp(/(^|[^"#])/.source+"(?:"+/"(?:\\(?:\((?:[^()]|\([^()]*\))*\)|\r\n|[^(])|[^\\\r\n"])*"/.source+"|"+/"""(?:\\(?:\((?:[^()]|\([^()]*\))*\)|[^(])|[^\\"]|"(?!""))*"""/.source+")"+/(?!["#])/.source),lookbehind:!0,greedy:!0,inside:{interpolation:{pattern:/(\\\()(?:[^()]|\([^()]*\))*(?=\))/,lookbehind:!0,inside:null},"interpolation-punctuation":{pattern:/^\)|\\\($/,alias:"punctuation"},punctuation:/\\(?=[\r\n])/,string:/[\s\S]+/}},{pattern:RegExp(/(^|[^"#])(#+)/.source+"(?:"+/"(?:\\(?:#+\((?:[^()]|\([^()]*\))*\)|\r\n|[^#])|[^\\\r\n])*?"/.source+"|"+/"""(?:\\(?:#+\((?:[^()]|\([^()]*\))*\)|[^#])|[^\\])*?"""/.source+")\\2"),lookbehind:!0,greedy:!0,inside:{interpolation:{pattern:/(\\#+\()(?:[^()]|\([^()]*\))*(?=\))/,lookbehind:!0,inside:null},"interpolation-punctuation":{pattern:/^\)|\\#+\($/,alias:"punctuation"},string:/[\s\S]+/}}],directive:{pattern:RegExp(/#/.source+"(?:"+/(?:elseif|if)\b/.source+"(?:[ \t]*"+/(?:![ \t]*)?(?:\b\w+\b(?:[ \t]*\((?:[^()]|\([^()]*\))*\))?|\((?:[^()]|\([^()]*\))*\))(?:[ \t]*(?:&&|\|\|))?/.source+")+|"+/(?:else|endif)\b/.source+")"),alias:"property",inside:{"directive-name":/^#\w+/,boolean:/\b(?:true|false)\b/,number:/\b\d+(?:\.\d+)*\b/,operator:/!|&&|\|\||[<>]=?/,punctuation:/[(),]/}},literal:{pattern:/#(?:colorLiteral|column|dsohandle|file(?:ID|Literal|Path)?|function|imageLiteral|line)\b/,alias:"constant"},"other-directive":{pattern:/#\w+\b/,alias:"property"},attribute:{pattern:/@\w+/,alias:"atrule"},"function-definition":{pattern:/(\bfunc\s+)\w+/,lookbehind:!0,alias:"function"},label:{pattern:/\b(break|continue)\s+\w+|\b[a-zA-Z_]\w*(?=\s*:\s*(?:for|repeat|while)\b)/,lookbehind:!0,alias:"important"},keyword:/\b(?:Any|Protocol|Self|Type|actor|as|assignment|associatedtype|associativity|async|await|break|case|catch|class|continue|convenience|default|defer|deinit|didSet|do|dynamic|else|enum|extension|fallthrough|fileprivate|final|for|func|get|guard|higherThan|if|import|in|indirect|infix|init|inout|internal|is|lazy|left|let|lowerThan|mutating|none|nonisolated|nonmutating|open|operator|optional|override|postfix|precedencegroup|prefix|private|protocol|public|repeat|required|rethrows|return|right|safe|self|set|some|static|struct|subscript|super|switch|throw|throws|try|typealias|unowned|unsafe|var|weak|where|while|willSet)\b/,boolean:/\b(?:true|false)\b/,nil:{pattern:/\bnil\b/,alias:"constant"},"short-argument":/\$\d+\b/,omit:{pattern:/\b_\b/,alias:"keyword"},number:/\b(?:[\d_]+(?:\.[\de_]+)?|0x[a-f0-9_]+(?:\.[a-f0-9p_]+)?|0b[01_]+|0o[0-7_]+)\b/i,"class-name":/\b[A-Z](?:[A-Z_\d]*[a-z]\w*)?\b/,function:/\b[a-z_]\w*(?=\s*\()/i,constant:/\b(?:[A-Z_]{2,}|k[A-Z][A-Za-z_]+)\b/,operator:/[-+*/%=!<>&|^~?]+|\.[.\-+*/%=!<>&|^~?]+/,punctuation:/[{}[\]();,.:\\]/},Prism.languages.swift["string-literal"].forEach((function(e){e.inside.interpolation.inside=Prism.languages.swift;})),function(e){var t="\\b(?:BASH|BASHOPTS|BASH_ALIASES|BASH_ARGC|BASH_ARGV|BASH_CMDS|BASH_COMPLETION_COMPAT_DIR|BASH_LINENO|BASH_REMATCH|BASH_SOURCE|BASH_VERSINFO|BASH_VERSION|COLORTERM|COLUMNS|COMP_WORDBREAKS|DBUS_SESSION_BUS_ADDRESS|DEFAULTS_PATH|DESKTOP_SESSION|DIRSTACK|DISPLAY|EUID|GDMSESSION|GDM_LANG|GNOME_KEYRING_CONTROL|GNOME_KEYRING_PID|GPG_AGENT_INFO|GROUPS|HISTCONTROL|HISTFILE|HISTFILESIZE|HISTSIZE|HOME|HOSTNAME|HOSTTYPE|IFS|INSTANCE|JOB|LANG|LANGUAGE|LC_ADDRESS|LC_ALL|LC_IDENTIFICATION|LC_MEASUREMENT|LC_MONETARY|LC_NAME|LC_NUMERIC|LC_PAPER|LC_TELEPHONE|LC_TIME|LESSCLOSE|LESSOPEN|LINES|LOGNAME|LS_COLORS|MACHTYPE|MAILCHECK|MANDATORY_PATH|NO_AT_BRIDGE|OLDPWD|OPTERR|OPTIND|ORBIT_SOCKETDIR|OSTYPE|PAPERSIZE|PATH|PIPESTATUS|PPID|PS1|PS2|PS3|PS4|PWD|RANDOM|REPLY|SECONDS|SELINUX_INIT|SESSION|SESSIONTYPE|SESSION_MANAGER|SHELL|SHELLOPTS|SHLVL|SSH_AUTH_SOCK|TERM|UID|UPSTART_EVENTS|UPSTART_INSTANCE|UPSTART_JOB|UPSTART_SESSION|USER|WINDOWID|XAUTHORITY|XDG_CONFIG_DIRS|XDG_CURRENT_DESKTOP|XDG_DATA_DIRS|XDG_GREETER_DATA_DIR|XDG_MENU_PREFIX|XDG_RUNTIME_DIR|XDG_SEAT|XDG_SEAT_PATH|XDG_SESSION_DESKTOP|XDG_SESSION_ID|XDG_SESSION_PATH|XDG_SESSION_TYPE|XDG_VTNR|XMODIFIERS)\\b",n={pattern:/(^(["']?)\w+\2)[ \t]+\S.*/,lookbehind:!0,alias:"punctuation",inside:null},a={bash:n,environment:{pattern:RegExp("\\$"+t),alias:"constant"},variable:[{pattern:/\$?\(\([\s\S]+?\)\)/,greedy:!0,inside:{variable:[{pattern:/(^\$\(\([\s\S]+)\)\)/,lookbehind:!0},/^\$\(\(/],number:/\b0x[\dA-Fa-f]+\b|(?:\b\d+(?:\.\d*)?|\B\.\d+)(?:[Ee]-?\d+)?/,operator:/--|\+\+|\*\*=?|<<=?|>>=?|&&|\|\||[=!+\-*/%<>^&|]=?|[?~:]/,punctuation:/\(\(?|\)\)?|,|;/}},{pattern:/\$\((?:\([^)]+\)|[^()])+\)|`[^`]+`/,greedy:!0,inside:{variable:/^\$\(|^`|\)$|`$/}},{pattern:/\$\{[^}]+\}/,greedy:!0,inside:{operator:/:[-=?+]?|[!\/]|##?|%%?|\^\^?|,,?/,punctuation:/[\[\]]/,environment:{pattern:RegExp("(\\{)"+t),lookbehind:!0,alias:"constant"}}},/\$(?:\w+|[#?*!@$])/],entity:/\\(?:[abceEfnrtv\\"]|O?[0-7]{1,3}|x[0-9a-fA-F]{1,2}|u[0-9a-fA-F]{4}|U[0-9a-fA-F]{8})/};e.languages.bash={shebang:{pattern:/^#!\s*\/.*/,alias:"important"},comment:{pattern:/(^|[^"{\\$])#.*/,lookbehind:!0},"function-name":[{pattern:/(\bfunction\s+)[\w-]+(?=(?:\s*\(?:\s*\))?\s*\{)/,lookbehind:!0,alias:"function"},{pattern:/\b[\w-]+(?=\s*\(\s*\)\s*\{)/,alias:"function"}],"for-or-select":{pattern:/(\b(?:for|select)\s+)\w+(?=\s+in\s)/,alias:"variable",lookbehind:!0},"assign-left":{pattern:/(^|[\s;|&]|[<>]\()\w+(?=\+?=)/,inside:{environment:{pattern:RegExp("(^|[\\s;|&]|[<>]\\()"+t),lookbehind:!0,alias:"constant"}},alias:"variable",lookbehind:!0},string:[{pattern:/((?:^|[^<])<<-?\s*)(\w+)\s[\s\S]*?(?:\r?\n|\r)\2/,lookbehind:!0,greedy:!0,inside:a},{pattern:/((?:^|[^<])<<-?\s*)(["'])(\w+)\2\s[\s\S]*?(?:\r?\n|\r)\3/,lookbehind:!0,greedy:!0,inside:{bash:n}},{pattern:/(^|[^\\](?:\\\\)*)"(?:\\[\s\S]|\$\([^)]+\)|\$(?!\()|`[^`]+`|[^"\\`$])*"/,lookbehind:!0,greedy:!0,inside:a},{pattern:/(^|[^$\\])'[^']*'/,lookbehind:!0,greedy:!0},{pattern:/\$'(?:[^'\\]|\\[\s\S])*'/,greedy:!0,inside:{entity:a.entity}}],environment:{pattern:RegExp("\\$?"+t),alias:"constant"},variable:a.variable,function:{pattern:/(^|[\s;|&]|[<>]\()(?:add|apropos|apt|aptitude|apt-cache|apt-get|aspell|automysqlbackup|awk|basename|bash|bc|bconsole|bg|bzip2|cal|cat|cfdisk|chgrp|chkconfig|chmod|chown|chroot|cksum|clear|cmp|column|comm|composer|cp|cron|crontab|csplit|curl|cut|date|dc|dd|ddrescue|debootstrap|df|diff|diff3|dig|dir|dircolors|dirname|dirs|dmesg|du|egrep|eject|env|ethtool|expand|expect|expr|fdformat|fdisk|fg|fgrep|file|find|fmt|fold|format|free|fsck|ftp|fuser|gawk|git|gparted|grep|groupadd|groupdel|groupmod|groups|grub-mkconfig|gzip|halt|head|hg|history|host|hostname|htop|iconv|id|ifconfig|ifdown|ifup|import|install|ip|jobs|join|kill|killall|less|link|ln|locate|logname|logrotate|look|lpc|lpr|lprint|lprintd|lprintq|lprm|ls|lsof|lynx|make|man|mc|mdadm|mkconfig|mkdir|mke2fs|mkfifo|mkfs|mkisofs|mknod|mkswap|mmv|more|most|mount|mtools|mtr|mutt|mv|nano|nc|netstat|nice|nl|nohup|notify-send|npm|nslookup|op|open|parted|passwd|paste|pathchk|ping|pkill|pnpm|popd|pr|printcap|printenv|ps|pushd|pv|quota|quotacheck|quotactl|ram|rar|rcp|reboot|remsync|rename|renice|rev|rm|rmdir|rpm|rsync|scp|screen|sdiff|sed|sendmail|seq|service|sftp|sh|shellcheck|shuf|shutdown|sleep|slocate|sort|split|ssh|stat|strace|su|sudo|sum|suspend|swapon|sync|tac|tail|tar|tee|time|timeout|top|touch|tr|traceroute|tsort|tty|umount|uname|unexpand|uniq|units|unrar|unshar|unzip|update-grub|uptime|useradd|userdel|usermod|users|uudecode|uuencode|v|vdir|vi|vim|virsh|vmstat|wait|watch|wc|wget|whereis|which|who|whoami|write|xargs|xdg-open|yarn|yes|zenity|zip|zsh|zypper)(?=$|[)\s;|&])/,lookbehind:!0},keyword:{pattern:/(^|[\s;|&]|[<>]\()(?:if|then|else|elif|fi|for|while|in|case|esac|function|select|do|done|until)(?=$|[)\s;|&])/,lookbehind:!0},builtin:{pattern:/(^|[\s;|&]|[<>]\()(?:\.|:|break|cd|continue|eval|exec|exit|export|getopts|hash|pwd|readonly|return|shift|test|times|trap|umask|unset|alias|bind|builtin|caller|command|declare|echo|enable|help|let|local|logout|mapfile|printf|read|readarray|source|type|typeset|ulimit|unalias|set|shopt)(?=$|[)\s;|&])/,lookbehind:!0,alias:"class-name"},boolean:{pattern:/(^|[\s;|&]|[<>]\()(?:true|false)(?=$|[)\s;|&])/,lookbehind:!0},"file-descriptor":{pattern:/\B&\d\b/,alias:"important"},operator:{pattern:/\d?<>|>\||\+=|=[=~]?|!=?|<<[<-]?|[&\d]?>>|\d[<>]&?|[<>][&=]?|&[>&]?|\|[&|]?/,inside:{"file-descriptor":{pattern:/^\d/,alias:"important"}}},punctuation:/\$?\(\(?|\)\)?|\.\.|[{}[\];\\]/,number:{pattern:/(^|\s)(?:[1-9]\d*|0)(?:[.,]\d+)?\b/,lookbehind:!0}},n.inside=e.languages.bash;for(var r=["comment","function-name","for-or-select","assign-left","string","environment","function","keyword","builtin","boolean","file-descriptor","operator","punctuation","number"],i=a.variable[1].inside,s=0;s/g,(function(){return t})),RegExp(/((?:^|[^\\])(?:\\{2})*)/.source+"(?:"+e+")")}var a=/(?:\\.|``(?:[^`\r\n]|`(?!`))+``|`[^`\r\n]+`|[^\\|\r\n`])+/.source,r=/\|?__(?:\|__)+\|?(?:(?:\n|\r\n?)|(?![\s\S]))/.source.replace(/__/g,(function(){return a})),i=/\|?[ \t]*:?-{3,}:?[ \t]*(?:\|[ \t]*:?-{3,}:?[ \t]*)+\|?(?:\n|\r\n?)/.source;e.languages.markdown=e.languages.extend("markup",{}),e.languages.insertBefore("markdown","prolog",{"front-matter-block":{pattern:/(^(?:\s*[\r\n])?)---(?!.)[\s\S]*?[\r\n]---(?!.)/,lookbehind:!0,greedy:!0,inside:{punctuation:/^---|---$/,"font-matter":{pattern:/\S+(?:\s+\S+)*/,alias:["yaml","language-yaml"],inside:e.languages.yaml}}},blockquote:{pattern:/^>(?:[\t ]*>)*/m,alias:"punctuation"},table:{pattern:RegExp("^"+r+i+"(?:"+r+")*","m"),inside:{"table-data-rows":{pattern:RegExp("^("+r+i+")(?:"+r+")*$"),lookbehind:!0,inside:{"table-data":{pattern:RegExp(a),inside:e.languages.markdown},punctuation:/\|/}},"table-line":{pattern:RegExp("^("+r+")"+i+"$"),lookbehind:!0,inside:{punctuation:/\||:?-{3,}:?/}},"table-header-row":{pattern:RegExp("^"+r+"$"),inside:{"table-header":{pattern:RegExp(a),alias:"important",inside:e.languages.markdown},punctuation:/\|/}}}},code:[{pattern:/((?:^|\n)[ \t]*\n|(?:^|\r\n?)[ \t]*\r\n?)(?: {4}|\t).+(?:(?:\n|\r\n?)(?: {4}|\t).+)*/,lookbehind:!0,alias:"keyword"},{pattern:/^```[\s\S]*?^```$/m,greedy:!0,inside:{"code-block":{pattern:/^(```.*(?:\n|\r\n?))[\s\S]+?(?=(?:\n|\r\n?)^```$)/m,lookbehind:!0},"code-language":{pattern:/^(```).+/,lookbehind:!0},punctuation:/```/}}],title:[{pattern:/\S.*(?:\n|\r\n?)(?:==+|--+)(?=[ \t]*$)/m,alias:"important",inside:{punctuation:/==+$|--+$/}},{pattern:/(^\s*)#.+/m,lookbehind:!0,alias:"important",inside:{punctuation:/^#+|#+$/}}],hr:{pattern:/(^\s*)([*-])(?:[\t ]*\2){2,}(?=\s*$)/m,lookbehind:!0,alias:"punctuation"},list:{pattern:/(^\s*)(?:[*+-]|\d+\.)(?=[\t ].)/m,lookbehind:!0,alias:"punctuation"},"url-reference":{pattern:/!?\[[^\]]+\]:[\t ]+(?:\S+|<(?:\\.|[^>\\])+>)(?:[\t ]+(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\((?:\\.|[^)\\])*\)))?/,inside:{variable:{pattern:/^(!?\[)[^\]]+/,lookbehind:!0},string:/(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\((?:\\.|[^)\\])*\))$/,punctuation:/^[\[\]!:]|[<>]/},alias:"url"},bold:{pattern:n(/\b__(?:(?!_)|_(?:(?!_))+_)+__\b|\*\*(?:(?!\*)|\*(?:(?!\*))+\*)+\*\*/.source),lookbehind:!0,greedy:!0,inside:{content:{pattern:/(^..)[\s\S]+(?=..$)/,lookbehind:!0,inside:{}},punctuation:/\*\*|__/}},italic:{pattern:n(/\b_(?:(?!_)|__(?:(?!_))+__)+_\b|\*(?:(?!\*)|\*\*(?:(?!\*))+\*\*)+\*/.source),lookbehind:!0,greedy:!0,inside:{content:{pattern:/(^.)[\s\S]+(?=.$)/,lookbehind:!0,inside:{}},punctuation:/[*_]/}},strike:{pattern:n(/(~~?)(?:(?!~))+\2/.source),lookbehind:!0,greedy:!0,inside:{content:{pattern:/(^~~?)[\s\S]+(?=\1$)/,lookbehind:!0,inside:{}},punctuation:/~~?/}},"code-snippet":{pattern:/(^|[^\\`])(?:``[^`\r\n]+(?:`[^`\r\n]+)*``(?!`)|`[^`\r\n]+`(?!`))/,lookbehind:!0,greedy:!0,alias:["code","keyword"]},url:{pattern:n(/!?\[(?:(?!\]))+\](?:\([^\s)]+(?:[\t ]+"(?:\\.|[^"\\])*")?\)|[ \t]?\[(?:(?!\]))+\])/.source),lookbehind:!0,greedy:!0,inside:{operator:/^!/,content:{pattern:/(^\[)[^\]]+(?=\])/,lookbehind:!0,inside:{}},variable:{pattern:/(^\][ \t]?\[)[^\]]+(?=\]$)/,lookbehind:!0},url:{pattern:/(^\]\()[^\s)]+/,lookbehind:!0},string:{pattern:/(^[ \t]+)"(?:\\.|[^"\\])*"(?=\)$)/,lookbehind:!0}}}}),["url","bold","italic","strike"].forEach((function(t){["url","bold","italic","strike","code-snippet"].forEach((function(n){t!==n&&(e.languages.markdown[t].inside.content.inside[n]=e.languages.markdown[n]);}));})),e.hooks.add("after-tokenize",(function(e){"markdown"!==e.language&&"md"!==e.language||function e(t){if(t&&"string"!=typeof t)for(var n=0,a=t.length;n",quot:'"'},l=String.fromCodePoint||String.fromCharCode;e.languages.md=e.languages.markdown;}(Prism),Prism.languages.lua={comment:/^#!.+|--(?:\[(=*)\[[\s\S]*?\]\1\]|.*)/m,string:{pattern:/(["'])(?:(?!\1)[^\\\r\n]|\\z(?:\r\n|\s)|\\(?:\r\n|[^z]))*\1|\[(=*)\[[\s\S]*?\]\2\]/,greedy:!0},number:/\b0x[a-f\d]+(?:\.[a-f\d]*)?(?:p[+-]?\d+)?\b|\b\d+(?:\.\B|(?:\.\d*)?(?:e[+-]?\d+)?\b)|\B\.\d+(?:e[+-]?\d+)?\b/i,keyword:/\b(?:and|break|do|else|elseif|end|false|for|function|goto|if|in|local|nil|not|or|repeat|return|then|true|until|while)\b/,function:/(?!\d)\w+(?=\s*(?:[({]))/,operator:[/[-+*%^&|#]|\/\/?|<[<=]?|>[>=]?|[=~]=?/,{pattern:/(^|[^.])\.\.(?!\.)/,lookbehind:!0}],punctuation:/[\[\](){},;]|\.+|:+/},Prism.languages.groovy=Prism.languages.extend("clike",{string:[{pattern:/("""|''')(?:[^\\]|\\[\s\S])*?\1|\$\/(?:[^/$]|\$(?:[/$]|(?![/$]))|\/(?!\$))*\/\$/,greedy:!0},{pattern:/(["'/])(?:\\.|(?!\1)[^\\\r\n])*\1/,greedy:!0}],keyword:/\b(?:as|def|in|abstract|assert|boolean|break|byte|case|catch|char|class|const|continue|default|do|double|else|enum|extends|final|finally|float|for|goto|if|implements|import|instanceof|int|interface|long|native|new|package|private|protected|public|return|short|static|strictfp|super|switch|synchronized|this|throw|throws|trait|transient|try|void|volatile|while)\b/,number:/\b(?:0b[01_]+|0x[\da-f_]+(?:\.[\da-f_p\-]+)?|[\d_]+(?:\.[\d_]+)?(?:e[+-]?\d+)?)[glidf]?\b/i,operator:{pattern:/(^|[^.])(?:~|==?~?|\?[.:]?|\*(?:[.=]|\*=?)?|\.[@&]|\.\.<|\.\.(?!\.)|-[-=>]?|\+[+=]?|!=?|<(?:<=?|=>?)?|>(?:>>?=?|=)?|&[&=]?|\|[|=]?|\/=?|\^=?|%=?)/,lookbehind:!0},punctuation:/\.+|[{}[\];(),:$]/}),Prism.languages.insertBefore("groovy","string",{shebang:{pattern:/#!.+/,alias:"comment"}}),Prism.languages.insertBefore("groovy","punctuation",{"spock-block":/\b(?:setup|given|when|then|and|cleanup|expect|where):/}),Prism.languages.insertBefore("groovy","function",{annotation:{pattern:/(^|[^.])@\w+/,lookbehind:!0,alias:"punctuation"}}),Prism.hooks.add("wrap",(function(e){if("groovy"===e.language&&"string"===e.type){var t=e.content[0];if("'"!=t){var n=/([^\\])(?:\$(?:\{.*?\}|[\w.]+))/;"$"===t&&(n=/([^\$])(?:\$(?:\{.*?\}|[\w.]+))/),e.content=e.content.replace(/</g,"<").replace(/&/g,"&"),e.content=Prism.highlight(e.content,{expression:{pattern:n,lookbehind:!0,inside:Prism.languages.groovy}}),e.classes.push("/"===t?"regex":"gstring");}}}));var vn=["comment","prolog","doctype","cdata","punctuation","namespace","property","tag","boolean","number","constant","symbol","deleted","selector","attr-name","string","builtin","inserted","operator","entity","url","string","atrule","attr-value","keyword","function","class-name","regex","important","variable","bold","italic","entity","char"];function Sn(e){return "string"==typeof e?e.length:"string"==typeof e.content?e.content.length:e.content.reduce((function(e,t){return e+Sn(t)}),0)}var wn,kn=d.String,An=function(e){if("Symbol"===nt(e))throw TypeError("Cannot convert a Symbol value to a string");return kn(e)},xn=function(){var e=ie(this),t="";return e.global&&(t+="g"),e.ignoreCase&&(t+="i"),e.multiline&&(t+="m"),e.dotAll&&(t+="s"),e.unicode&&(t+="u"),e.sticky&&(t+="y"),t},Tn=d.RegExp,_n=j((function(){var e=Tn("a","y");return e.lastIndex=2,null!=e.exec("abcd")})),On=_n||j((function(){return !Tn("a","y").sticky})),In={BROKEN_CARET:_n||j((function(){var e=Tn("^r","gy");return e.lastIndex=2,null!=e.exec("str")})),MISSED_STICKY:On,UNSUPPORTED_Y:_n},Rn=K?Object.defineProperties:function(e,t){ie(e);for(var n,a=jt(t),r=fn(t),i=r.length,s=0;i>s;)ke.f(e,n=r[s++],a[n]);return e},Nn=C("document","documentElement"),Ln=Ce("IE_PROTO"),Pn=function(){},Cn=function(e){return " + + *** 省市联动 *** + new PCAS("Province","City") + new PCAS("Province","City","吉林省") + new PCAS("Province","City","吉林省","吉林市") + + *** 省市区联动 *** + new PCAS("Province","City","Area") + new PCAS("Province","City","Area","吉林省") + new PCAS("Province","City","Area","吉林省","松原市") + new PCAS("Province","City","Area","吉林省","松原市","宁江区") + + 省、市、地区对象取得的值均为实际值。 + 注:省、市、地区提示信息选项的值为""(空字符串) + *********************************************************/ + +SPT = window.SPT || "-省份-"; +SCT = window.SCT || "-城市-"; +SAT = window.SAT || "-地区-"; +SWT = window.SWT || 0; // 提示文字 0:不显示 1:显示 + +function PCAS() { + this.SelP = document.getElementsByName(arguments[0])[0]; + this.SelC = document.getElementsByName(arguments[1])[0]; + this.SelA = document.getElementsByName(arguments[2])[0]; + this.DefP = this.SelA ? arguments[3] : arguments[2]; + this.DefC = this.SelA ? arguments[4] : arguments[3]; + this.DefA = this.SelA ? arguments[5] : arguments[4]; + if (this.SelP) this.SelP.PCA = this; + if (this.SelC) this.SelC.PCA = this; + if (this.SelA) this.SelA.PCA = this; + if (this.SelP && this.SelC) { + this.SelP.onchange = function () { + PCAS.SetC(this.PCA) + }; + if (this.SelA) this.SelC.onchange = function () { + PCAS.SetA(this.PCA) + }; + } + PCAS.init(this).SetP(this) +} + +PCAS.init = function (PCA) { + PCA.PCAP = [], PCA.PCAC = [], PCA.PCAA = [], PCA.PCAD = "__STRING__"; + if (SWT) PCA.PCAD = SPT + "$" + SCT + "," + SAT + "#" + PCA.PCAD; + PCA.PCAD.split("#").forEach(function (VAL1, ID1) { + PCA.PCAP[ID1] = VAL1.split("$")[0], PCA.PCAC[ID1] = [], PCA.PCAA[ID1] = []; + VAL1.split("$")[1].split("|").forEach(function (VAL2, ID2) { + PCA.PCAC[ID1].push((PCA.PCAR = VAL2.split(",")).shift()), PCA.PCAA[ID1][ID2] = PCA.PCAR; + }); + }); + return this; +}; + +PCAS.SetP = function (PCA) { + PCA.PCAP.forEach(function (VAL, IDX) { + PCA.PCAT = PCA.PCAV = VAL; + if (PCA.PCAT === SPT) PCA.PCAV = ""; + PCA.SelP.options.add(new Option(PCA.PCAT, PCA.PCAV)); + if (PCA.DefP === PCA.PCAV) PCA.SelP[IDX].selected = true + }), PCA.SelC ? PCAS.SetC(PCA) : $(PCA.SelP).trigger('change'); +}; + +PCAS.SetC = function (PCA) { + PCA.SelC.length = 0; + PCA.PCAC[PCA.SelP.selectedIndex].forEach(function (VAL, IDX) { + PCA.PCAT = PCA.PCAV = VAL; + if (PCA.PCAT === SCT) PCA.PCAV = ""; + PCA.SelC.options.add(new Option(PCA.PCAT, PCA.PCAV)); + if (PCA.DefC === PCA.PCAV) PCA.SelC[IDX].selected = true + }), PCA.SelA ? PCAS.SetA(PCA) : $(PCA.SelC).trigger('change'); +}; + +PCAS.SetA = function (PCA) { + PCA.SelA.length = 0; + PCA.PCAA[PCA.SelP.selectedIndex][PCA.SelC.selectedIndex].forEach(function (VAL, IDX) { + PCA.PCAT = PCA.PCAV = VAL; + if (PCA.PCAT === SAT) PCA.PCAV = ""; + PCA.SelA.options.add(new Option(PCA.PCAT, PCA.PCAV)); + if (PCA.DefA === PCA.PCAV) PCA.SelA[IDX].selected = true + }), $(PCA.SelA).trigger('change') +}; +EOL + ); + + // 写入区域文件 + return file_put_contents($scriptFile, $scriptContent) && file_put_contents($jsonFile, $jsonContent); + } +}; diff --git a/plugin/think-plugs-static/stc/public/static/plugs/jquery/area/data.json b/plugin/think-plugs-static/stc/public/static/plugs/jquery/area/data.json new file mode 100644 index 000000000..49903342c --- /dev/null +++ b/plugin/think-plugs-static/stc/public/static/plugs/jquery/area/data.json @@ -0,0 +1 @@ +[{"code":"110000","name":"北京市","list":[{"code":"110100","name":"北京市","list":[{"code":"110101","name":"东城区"},{"code":"110102","name":"西城区"},{"code":"110105","name":"朝阳区"},{"code":"110106","name":"丰台区"},{"code":"110107","name":"石景山区"},{"code":"110108","name":"海淀区"},{"code":"110109","name":"门头沟区"},{"code":"110111","name":"房山区"},{"code":"110112","name":"通州区"},{"code":"110113","name":"顺义区"},{"code":"110114","name":"昌平区"},{"code":"110115","name":"大兴区"},{"code":"110116","name":"怀柔区"},{"code":"110117","name":"平谷区"},{"code":"110118","name":"密云区"},{"code":"110119","name":"延庆区"}]}]},{"code":"120000","name":"天津市","list":[{"code":"120100","name":"天津市","list":[{"code":"120101","name":"和平区"},{"code":"120102","name":"河东区"},{"code":"120103","name":"河西区"},{"code":"120104","name":"南开区"},{"code":"120105","name":"河北区"},{"code":"120106","name":"红桥区"},{"code":"120110","name":"东丽区"},{"code":"120111","name":"西青区"},{"code":"120112","name":"津南区"},{"code":"120113","name":"北辰区"},{"code":"120114","name":"武清区"},{"code":"120115","name":"宝坻区"},{"code":"120116","name":"滨海新区"},{"code":"120117","name":"宁河区"},{"code":"120118","name":"静海区"},{"code":"120119","name":"蓟州区"}]}]},{"code":"130000","name":"河北省","list":[{"code":"130100","name":"石家庄市","list":[{"code":"130102","name":"长安区"},{"code":"130104","name":"桥西区"},{"code":"130105","name":"新华区"},{"code":"130107","name":"井陉矿区"},{"code":"130108","name":"裕华区"},{"code":"130109","name":"藁城区"},{"code":"130110","name":"鹿泉区"},{"code":"130111","name":"栾城区"},{"code":"130121","name":"井陉县"},{"code":"130123","name":"正定县"},{"code":"130125","name":"行唐县"},{"code":"130126","name":"灵寿县"},{"code":"130127","name":"高邑县"},{"code":"130128","name":"深泽县"},{"code":"130129","name":"赞皇县"},{"code":"130130","name":"无极县"},{"code":"130131","name":"平山县"},{"code":"130132","name":"元氏县"},{"code":"130133","name":"赵县"},{"code":"130181","name":"辛集市"},{"code":"130183","name":"晋州市"},{"code":"130184","name":"新乐市"}]},{"code":"130200","name":"唐山市","list":[{"code":"130202","name":"路南区"},{"code":"130203","name":"路北区"},{"code":"130204","name":"古冶区"},{"code":"130205","name":"开平区"},{"code":"130207","name":"丰南区"},{"code":"130208","name":"丰润区"},{"code":"130209","name":"曹妃甸区"},{"code":"130224","name":"滦南县"},{"code":"130225","name":"乐亭县"},{"code":"130227","name":"迁西县"},{"code":"130229","name":"玉田县"},{"code":"130281","name":"遵化市"},{"code":"130283","name":"迁安市"},{"code":"130284","name":"滦州市"}]},{"code":"130300","name":"秦皇岛市","list":[{"code":"130302","name":"海港区"},{"code":"130303","name":"山海关区"},{"code":"130304","name":"北戴河区"},{"code":"130306","name":"抚宁区"},{"code":"130321","name":"青龙满族自治县"},{"code":"130322","name":"昌黎县"},{"code":"130324","name":"卢龙县"}]},{"code":"130400","name":"邯郸市","list":[{"code":"130402","name":"邯山区"},{"code":"130403","name":"丛台区"},{"code":"130404","name":"复兴区"},{"code":"130406","name":"峰峰矿区"},{"code":"130407","name":"肥乡区"},{"code":"130408","name":"永年区"},{"code":"130423","name":"临漳县"},{"code":"130424","name":"成安县"},{"code":"130425","name":"大名县"},{"code":"130426","name":"涉县"},{"code":"130427","name":"磁县"},{"code":"130430","name":"邱县"},{"code":"130431","name":"鸡泽县"},{"code":"130432","name":"广平县"},{"code":"130433","name":"馆陶县"},{"code":"130434","name":"魏县"},{"code":"130435","name":"曲周县"},{"code":"130481","name":"武安市"}]},{"code":"130500","name":"邢台市","list":[{"code":"130502","name":"襄都区"},{"code":"130503","name":"信都区"},{"code":"130505","name":"任泽区"},{"code":"130506","name":"南和区"},{"code":"130522","name":"临城县"},{"code":"130523","name":"内丘县"},{"code":"130524","name":"柏乡县"},{"code":"130525","name":"隆尧县"},{"code":"130528","name":"宁晋县"},{"code":"130529","name":"巨鹿县"},{"code":"130530","name":"新河县"},{"code":"130531","name":"广宗县"},{"code":"130532","name":"平乡县"},{"code":"130533","name":"威县"},{"code":"130534","name":"清河县"},{"code":"130535","name":"临西县"},{"code":"130581","name":"南宫市"},{"code":"130582","name":"沙河市"}]},{"code":"130600","name":"保定市","list":[{"code":"130602","name":"竞秀区"},{"code":"130606","name":"莲池区"},{"code":"130607","name":"满城区"},{"code":"130608","name":"清苑区"},{"code":"130609","name":"徐水区"},{"code":"130623","name":"涞水县"},{"code":"130624","name":"阜平县"},{"code":"130626","name":"定兴县"},{"code":"130627","name":"唐县"},{"code":"130628","name":"高阳县"},{"code":"130629","name":"容城县"},{"code":"130630","name":"涞源县"},{"code":"130631","name":"望都县"},{"code":"130632","name":"安新县"},{"code":"130633","name":"易县"},{"code":"130634","name":"曲阳县"},{"code":"130635","name":"蠡县"},{"code":"130636","name":"顺平县"},{"code":"130637","name":"博野县"},{"code":"130638","name":"雄县"},{"code":"130681","name":"涿州市"},{"code":"130682","name":"定州市"},{"code":"130683","name":"安国市"},{"code":"130684","name":"高碑店市"}]},{"code":"130700","name":"张家口市","list":[{"code":"130702","name":"桥东区"},{"code":"130703","name":"桥西区"},{"code":"130705","name":"宣化区"},{"code":"130706","name":"下花园区"},{"code":"130708","name":"万全区"},{"code":"130709","name":"崇礼区"},{"code":"130722","name":"张北县"},{"code":"130723","name":"康保县"},{"code":"130724","name":"沽源县"},{"code":"130725","name":"尚义县"},{"code":"130726","name":"蔚县"},{"code":"130727","name":"阳原县"},{"code":"130728","name":"怀安县"},{"code":"130730","name":"怀来县"},{"code":"130731","name":"涿鹿县"},{"code":"130732","name":"赤城县"}]},{"code":"130800","name":"承德市","list":[{"code":"130802","name":"双桥区"},{"code":"130803","name":"双滦区"},{"code":"130804","name":"鹰手营子矿区"},{"code":"130821","name":"承德县"},{"code":"130822","name":"兴隆县"},{"code":"130824","name":"滦平县"},{"code":"130825","name":"隆化县"},{"code":"130826","name":"丰宁满族自治县"},{"code":"130827","name":"宽城满族自治县"},{"code":"130828","name":"围场满族蒙古族自治县"},{"code":"130881","name":"平泉市"}]},{"code":"130900","name":"沧州市","list":[{"code":"130902","name":"新华区"},{"code":"130903","name":"运河区"},{"code":"130921","name":"沧县"},{"code":"130922","name":"青县"},{"code":"130923","name":"东光县"},{"code":"130924","name":"海兴县"},{"code":"130925","name":"盐山县"},{"code":"130926","name":"肃宁县"},{"code":"130927","name":"南皮县"},{"code":"130928","name":"吴桥县"},{"code":"130929","name":"献县"},{"code":"130930","name":"孟村回族自治县"},{"code":"130981","name":"泊头市"},{"code":"130982","name":"任丘市"},{"code":"130983","name":"黄骅市"},{"code":"130984","name":"河间市"}]},{"code":"131000","name":"廊坊市","list":[{"code":"131002","name":"安次区"},{"code":"131003","name":"广阳区"},{"code":"131022","name":"固安县"},{"code":"131023","name":"永清县"},{"code":"131024","name":"香河县"},{"code":"131025","name":"大城县"},{"code":"131026","name":"文安县"},{"code":"131028","name":"大厂回族自治县"},{"code":"131081","name":"霸州市"},{"code":"131082","name":"三河市"}]},{"code":"131100","name":"衡水市","list":[{"code":"131102","name":"桃城区"},{"code":"131103","name":"冀州区"},{"code":"131121","name":"枣强县"},{"code":"131122","name":"武邑县"},{"code":"131123","name":"武强县"},{"code":"131124","name":"饶阳县"},{"code":"131125","name":"安平县"},{"code":"131126","name":"故城县"},{"code":"131127","name":"景县"},{"code":"131128","name":"阜城县"},{"code":"131182","name":"深州市"}]}]},{"code":"140000","name":"山西省","list":[{"code":"140100","name":"太原市","list":[{"code":"140105","name":"小店区"},{"code":"140106","name":"迎泽区"},{"code":"140107","name":"杏花岭区"},{"code":"140108","name":"尖草坪区"},{"code":"140109","name":"万柏林区"},{"code":"140110","name":"晋源区"},{"code":"140121","name":"清徐县"},{"code":"140122","name":"阳曲县"},{"code":"140123","name":"娄烦县"},{"code":"140181","name":"古交市"}]},{"code":"140200","name":"大同市","list":[{"code":"140212","name":"新荣区"},{"code":"140213","name":"平城区"},{"code":"140214","name":"云冈区"},{"code":"140215","name":"云州区"},{"code":"140221","name":"阳高县"},{"code":"140222","name":"天镇县"},{"code":"140223","name":"广灵县"},{"code":"140224","name":"灵丘县"},{"code":"140225","name":"浑源县"},{"code":"140226","name":"左云县"}]},{"code":"140300","name":"阳泉市","list":[{"code":"140302","name":"城区"},{"code":"140303","name":"矿区"},{"code":"140311","name":"郊区"},{"code":"140321","name":"平定县"},{"code":"140322","name":"盂县"}]},{"code":"140400","name":"长治市","list":[{"code":"140403","name":"潞州区"},{"code":"140404","name":"上党区"},{"code":"140405","name":"屯留区"},{"code":"140406","name":"潞城区"},{"code":"140423","name":"襄垣县"},{"code":"140425","name":"平顺县"},{"code":"140426","name":"黎城县"},{"code":"140427","name":"壶关县"},{"code":"140428","name":"长子县"},{"code":"140429","name":"武乡县"},{"code":"140430","name":"沁县"},{"code":"140431","name":"沁源县"}]},{"code":"140500","name":"晋城市","list":[{"code":"140502","name":"城区"},{"code":"140521","name":"沁水县"},{"code":"140522","name":"阳城县"},{"code":"140524","name":"陵川县"},{"code":"140525","name":"泽州县"},{"code":"140581","name":"高平市"}]},{"code":"140600","name":"朔州市","list":[{"code":"140602","name":"朔城区"},{"code":"140603","name":"平鲁区"},{"code":"140621","name":"山阴县"},{"code":"140622","name":"应县"},{"code":"140623","name":"右玉县"},{"code":"140681","name":"怀仁市"}]},{"code":"140700","name":"晋中市","list":[{"code":"140702","name":"榆次区"},{"code":"140703","name":"太谷区"},{"code":"140721","name":"榆社县"},{"code":"140722","name":"左权县"},{"code":"140723","name":"和顺县"},{"code":"140724","name":"昔阳县"},{"code":"140725","name":"寿阳县"},{"code":"140727","name":"祁县"},{"code":"140728","name":"平遥县"},{"code":"140729","name":"灵石县"},{"code":"140781","name":"介休市"}]},{"code":"140800","name":"运城市","list":[{"code":"140802","name":"盐湖区"},{"code":"140821","name":"临猗县"},{"code":"140822","name":"万荣县"},{"code":"140823","name":"闻喜县"},{"code":"140824","name":"稷山县"},{"code":"140825","name":"新绛县"},{"code":"140826","name":"绛县"},{"code":"140827","name":"垣曲县"},{"code":"140828","name":"夏县"},{"code":"140829","name":"平陆县"},{"code":"140830","name":"芮城县"},{"code":"140881","name":"永济市"},{"code":"140882","name":"河津市"}]},{"code":"140900","name":"忻州市","list":[{"code":"140902","name":"忻府区"},{"code":"140921","name":"定襄县"},{"code":"140922","name":"五台县"},{"code":"140923","name":"代县"},{"code":"140924","name":"繁峙县"},{"code":"140925","name":"宁武县"},{"code":"140926","name":"静乐县"},{"code":"140927","name":"神池县"},{"code":"140928","name":"五寨县"},{"code":"140929","name":"岢岚县"},{"code":"140930","name":"河曲县"},{"code":"140931","name":"保德县"},{"code":"140932","name":"偏关县"},{"code":"140981","name":"原平市"}]},{"code":"141000","name":"临汾市","list":[{"code":"141002","name":"尧都区"},{"code":"141021","name":"曲沃县"},{"code":"141022","name":"翼城县"},{"code":"141023","name":"襄汾县"},{"code":"141024","name":"洪洞县"},{"code":"141025","name":"古县"},{"code":"141026","name":"安泽县"},{"code":"141027","name":"浮山县"},{"code":"141028","name":"吉县"},{"code":"141029","name":"乡宁县"},{"code":"141030","name":"大宁县"},{"code":"141031","name":"隰县"},{"code":"141032","name":"永和县"},{"code":"141033","name":"蒲县"},{"code":"141034","name":"汾西县"},{"code":"141081","name":"侯马市"},{"code":"141082","name":"霍州市"}]},{"code":"141100","name":"吕梁市","list":[{"code":"141102","name":"离石区"},{"code":"141121","name":"文水县"},{"code":"141122","name":"交城县"},{"code":"141123","name":"兴县"},{"code":"141124","name":"临县"},{"code":"141125","name":"柳林县"},{"code":"141126","name":"石楼县"},{"code":"141127","name":"岚县"},{"code":"141128","name":"方山县"},{"code":"141129","name":"中阳县"},{"code":"141130","name":"交口县"},{"code":"141181","name":"孝义市"},{"code":"141182","name":"汾阳市"}]}]},{"code":"150000","name":"内蒙古自治区","list":[{"code":"150100","name":"呼和浩特市","list":[{"code":"150102","name":"新城区"},{"code":"150103","name":"回民区"},{"code":"150104","name":"玉泉区"},{"code":"150105","name":"赛罕区"},{"code":"150121","name":"土默特左旗"},{"code":"150122","name":"托克托县"},{"code":"150123","name":"和林格尔县"},{"code":"150124","name":"清水河县"},{"code":"150125","name":"武川县"}]},{"code":"150200","name":"包头市","list":[{"code":"150202","name":"东河区"},{"code":"150203","name":"昆都仑区"},{"code":"150204","name":"青山区"},{"code":"150205","name":"石拐区"},{"code":"150206","name":"白云鄂博矿区"},{"code":"150207","name":"九原区"},{"code":"150221","name":"土默特右旗"},{"code":"150222","name":"固阳县"},{"code":"150223","name":"达尔罕茂明安联合旗"}]},{"code":"150300","name":"乌海市","list":[{"code":"150302","name":"海勃湾区"},{"code":"150303","name":"海南区"},{"code":"150304","name":"乌达区"}]},{"code":"150400","name":"赤峰市","list":[{"code":"150402","name":"红山区"},{"code":"150403","name":"元宝山区"},{"code":"150404","name":"松山区"},{"code":"150421","name":"阿鲁科尔沁旗"},{"code":"150422","name":"巴林左旗"},{"code":"150423","name":"巴林右旗"},{"code":"150424","name":"林西县"},{"code":"150425","name":"克什克腾旗"},{"code":"150426","name":"翁牛特旗"},{"code":"150428","name":"喀喇沁旗"},{"code":"150429","name":"宁城县"},{"code":"150430","name":"敖汉旗"}]},{"code":"150500","name":"通辽市","list":[{"code":"150502","name":"科尔沁区"},{"code":"150521","name":"科尔沁左翼中旗"},{"code":"150522","name":"科尔沁左翼后旗"},{"code":"150523","name":"开鲁县"},{"code":"150524","name":"库伦旗"},{"code":"150525","name":"奈曼旗"},{"code":"150526","name":"扎鲁特旗"},{"code":"150581","name":"霍林郭勒市"}]},{"code":"150600","name":"鄂尔多斯市","list":[{"code":"150602","name":"东胜区"},{"code":"150603","name":"康巴什区"},{"code":"150621","name":"达拉特旗"},{"code":"150622","name":"准格尔旗"},{"code":"150623","name":"鄂托克前旗"},{"code":"150624","name":"鄂托克旗"},{"code":"150625","name":"杭锦旗"},{"code":"150626","name":"乌审旗"},{"code":"150627","name":"伊金霍洛旗"}]},{"code":"150700","name":"呼伦贝尔市","list":[{"code":"150702","name":"海拉尔区"},{"code":"150703","name":"扎赉诺尔区"},{"code":"150721","name":"阿荣旗"},{"code":"150722","name":"莫力达瓦达斡尔族自治旗"},{"code":"150723","name":"鄂伦春自治旗"},{"code":"150724","name":"鄂温克族自治旗"},{"code":"150725","name":"陈巴尔虎旗"},{"code":"150726","name":"新巴尔虎左旗"},{"code":"150727","name":"新巴尔虎右旗"},{"code":"150781","name":"满洲里市"},{"code":"150782","name":"牙克石市"},{"code":"150783","name":"扎兰屯市"},{"code":"150784","name":"额尔古纳市"},{"code":"150785","name":"根河市"}]},{"code":"150800","name":"巴彦淖尔市","list":[{"code":"150802","name":"临河区"},{"code":"150821","name":"五原县"},{"code":"150822","name":"磴口县"},{"code":"150823","name":"乌拉特前旗"},{"code":"150824","name":"乌拉特中旗"},{"code":"150825","name":"乌拉特后旗"},{"code":"150826","name":"杭锦后旗"}]},{"code":"150900","name":"乌兰察布市","list":[{"code":"150902","name":"集宁区"},{"code":"150921","name":"卓资县"},{"code":"150922","name":"化德县"},{"code":"150923","name":"商都县"},{"code":"150924","name":"兴和县"},{"code":"150925","name":"凉城县"},{"code":"150926","name":"察哈尔右翼前旗"},{"code":"150927","name":"察哈尔右翼中旗"},{"code":"150928","name":"察哈尔右翼后旗"},{"code":"150929","name":"四子王旗"},{"code":"150981","name":"丰镇市"}]},{"code":"152200","name":"兴安盟","list":[{"code":"152201","name":"乌兰浩特市"},{"code":"152202","name":"阿尔山市"},{"code":"152221","name":"科尔沁右翼前旗"},{"code":"152222","name":"科尔沁右翼中旗"},{"code":"152223","name":"扎赉特旗"},{"code":"152224","name":"突泉县"}]},{"code":"152500","name":"锡林郭勒盟","list":[{"code":"152501","name":"二连浩特市"},{"code":"152502","name":"锡林浩特市"},{"code":"152522","name":"阿巴嘎旗"},{"code":"152523","name":"苏尼特左旗"},{"code":"152524","name":"苏尼特右旗"},{"code":"152525","name":"东乌珠穆沁旗"},{"code":"152526","name":"西乌珠穆沁旗"},{"code":"152527","name":"太仆寺旗"},{"code":"152528","name":"镶黄旗"},{"code":"152529","name":"正镶白旗"},{"code":"152530","name":"正蓝旗"},{"code":"152531","name":"多伦县"}]},{"code":"152900","name":"阿拉善盟","list":[{"code":"152921","name":"阿拉善左旗"},{"code":"152922","name":"阿拉善右旗"},{"code":"152923","name":"额济纳旗"}]}]},{"code":"210000","name":"辽宁省","list":[{"code":"210100","name":"沈阳市","list":[{"code":"210102","name":"和平区"},{"code":"210103","name":"沈河区"},{"code":"210104","name":"大东区"},{"code":"210105","name":"皇姑区"},{"code":"210106","name":"铁西区"},{"code":"210111","name":"苏家屯区"},{"code":"210112","name":"浑南区"},{"code":"210113","name":"沈北新区"},{"code":"210114","name":"于洪区"},{"code":"210115","name":"辽中区"},{"code":"210123","name":"康平县"},{"code":"210124","name":"法库县"},{"code":"210181","name":"新民市"}]},{"code":"210200","name":"大连市","list":[{"code":"210202","name":"中山区"},{"code":"210203","name":"西岗区"},{"code":"210204","name":"沙河口区"},{"code":"210211","name":"甘井子区"},{"code":"210212","name":"旅顺口区"},{"code":"210213","name":"金州区"},{"code":"210214","name":"普兰店区"},{"code":"210224","name":"长海县"},{"code":"210281","name":"瓦房店市"},{"code":"210283","name":"庄河市"}]},{"code":"210300","name":"鞍山市","list":[{"code":"210302","name":"铁东区"},{"code":"210303","name":"铁西区"},{"code":"210304","name":"立山区"},{"code":"210311","name":"千山区"},{"code":"210321","name":"台安县"},{"code":"210323","name":"岫岩满族自治县"},{"code":"210381","name":"海城市"}]},{"code":"210400","name":"抚顺市","list":[{"code":"210402","name":"新抚区"},{"code":"210403","name":"东洲区"},{"code":"210404","name":"望花区"},{"code":"210411","name":"顺城区"},{"code":"210421","name":"抚顺县"},{"code":"210422","name":"新宾满族自治县"},{"code":"210423","name":"清原满族自治县"}]},{"code":"210500","name":"本溪市","list":[{"code":"210502","name":"平山区"},{"code":"210503","name":"溪湖区"},{"code":"210504","name":"明山区"},{"code":"210505","name":"南芬区"},{"code":"210521","name":"本溪满族自治县"},{"code":"210522","name":"桓仁满族自治县"}]},{"code":"210600","name":"丹东市","list":[{"code":"210602","name":"元宝区"},{"code":"210603","name":"振兴区"},{"code":"210604","name":"振安区"},{"code":"210624","name":"宽甸满族自治县"},{"code":"210681","name":"东港市"},{"code":"210682","name":"凤城市"}]},{"code":"210700","name":"锦州市","list":[{"code":"210702","name":"古塔区"},{"code":"210703","name":"凌河区"},{"code":"210711","name":"太和区"},{"code":"210726","name":"黑山县"},{"code":"210727","name":"义县"},{"code":"210781","name":"凌海市"},{"code":"210782","name":"北镇市"}]},{"code":"210800","name":"营口市","list":[{"code":"210802","name":"站前区"},{"code":"210803","name":"西市区"},{"code":"210804","name":"鲅鱼圈区"},{"code":"210811","name":"老边区"},{"code":"210881","name":"盖州市"},{"code":"210882","name":"大石桥市"}]},{"code":"210900","name":"阜新市","list":[{"code":"210902","name":"海州区"},{"code":"210903","name":"新邱区"},{"code":"210904","name":"太平区"},{"code":"210905","name":"清河门区"},{"code":"210911","name":"细河区"},{"code":"210921","name":"阜新蒙古族自治县"},{"code":"210922","name":"彰武县"}]},{"code":"211000","name":"辽阳市","list":[{"code":"211002","name":"白塔区"},{"code":"211003","name":"文圣区"},{"code":"211004","name":"宏伟区"},{"code":"211005","name":"弓长岭区"},{"code":"211011","name":"太子河区"},{"code":"211021","name":"辽阳县"},{"code":"211081","name":"灯塔市"}]},{"code":"211100","name":"盘锦市","list":[{"code":"211102","name":"双台子区"},{"code":"211103","name":"兴隆台区"},{"code":"211104","name":"大洼区"},{"code":"211122","name":"盘山县"}]},{"code":"211200","name":"铁岭市","list":[{"code":"211202","name":"银州区"},{"code":"211204","name":"清河区"},{"code":"211221","name":"铁岭县"},{"code":"211223","name":"西丰县"},{"code":"211224","name":"昌图县"},{"code":"211281","name":"调兵山市"},{"code":"211282","name":"开原市"}]},{"code":"211300","name":"朝阳市","list":[{"code":"211302","name":"双塔区"},{"code":"211303","name":"龙城区"},{"code":"211321","name":"朝阳县"},{"code":"211322","name":"建平县"},{"code":"211324","name":"喀喇沁左翼蒙古族自治县"},{"code":"211381","name":"北票市"},{"code":"211382","name":"凌源市"}]},{"code":"211400","name":"葫芦岛市","list":[{"code":"211402","name":"连山区"},{"code":"211403","name":"龙港区"},{"code":"211404","name":"南票区"},{"code":"211421","name":"绥中县"},{"code":"211422","name":"建昌县"},{"code":"211481","name":"兴城市"}]}]},{"code":"220000","name":"吉林省","list":[{"code":"220100","name":"长春市","list":[{"code":"220102","name":"南关区"},{"code":"220103","name":"宽城区"},{"code":"220104","name":"朝阳区"},{"code":"220105","name":"二道区"},{"code":"220106","name":"绿园区"},{"code":"220112","name":"双阳区"},{"code":"220113","name":"九台区"},{"code":"220122","name":"农安县"},{"code":"220182","name":"榆树市"},{"code":"220183","name":"德惠市"},{"code":"220184","name":"公主岭市"}]},{"code":"220200","name":"吉林市","list":[{"code":"220202","name":"昌邑区"},{"code":"220203","name":"龙潭区"},{"code":"220204","name":"船营区"},{"code":"220211","name":"丰满区"},{"code":"220221","name":"永吉县"},{"code":"220281","name":"蛟河市"},{"code":"220282","name":"桦甸市"},{"code":"220283","name":"舒兰市"},{"code":"220284","name":"磐石市"}]},{"code":"220300","name":"四平市","list":[{"code":"220302","name":"铁西区"},{"code":"220303","name":"铁东区"},{"code":"220322","name":"梨树县"},{"code":"220323","name":"伊通满族自治县"},{"code":"220382","name":"双辽市"}]},{"code":"220400","name":"辽源市","list":[{"code":"220402","name":"龙山区"},{"code":"220403","name":"西安区"},{"code":"220421","name":"东丰县"},{"code":"220422","name":"东辽县"}]},{"code":"220500","name":"通化市","list":[{"code":"220502","name":"东昌区"},{"code":"220503","name":"二道江区"},{"code":"220521","name":"通化县"},{"code":"220523","name":"辉南县"},{"code":"220524","name":"柳河县"},{"code":"220581","name":"梅河口市"},{"code":"220582","name":"集安市"}]},{"code":"220600","name":"白山市","list":[{"code":"220602","name":"浑江区"},{"code":"220605","name":"江源区"},{"code":"220621","name":"抚松县"},{"code":"220622","name":"靖宇县"},{"code":"220623","name":"长白朝鲜族自治县"},{"code":"220681","name":"临江市"}]},{"code":"220700","name":"松原市","list":[{"code":"220702","name":"宁江区"},{"code":"220721","name":"前郭尔罗斯蒙古族自治县"},{"code":"220722","name":"长岭县"},{"code":"220723","name":"乾安县"},{"code":"220781","name":"扶余市"}]},{"code":"220800","name":"白城市","list":[{"code":"220802","name":"洮北区"},{"code":"220821","name":"镇赉县"},{"code":"220822","name":"通榆县"},{"code":"220881","name":"洮南市"},{"code":"220882","name":"大安市"}]},{"code":"222400","name":"延边朝鲜族自治州","list":[{"code":"222401","name":"延吉市"},{"code":"222402","name":"图们市"},{"code":"222403","name":"敦化市"},{"code":"222404","name":"珲春市"},{"code":"222405","name":"龙井市"},{"code":"222406","name":"和龙市"},{"code":"222424","name":"汪清县"},{"code":"222426","name":"安图县"}]}]},{"code":"230000","name":"黑龙江省","list":[{"code":"230100","name":"哈尔滨市","list":[{"code":"230102","name":"道里区"},{"code":"230103","name":"南岗区"},{"code":"230104","name":"道外区"},{"code":"230108","name":"平房区"},{"code":"230109","name":"松北区"},{"code":"230110","name":"香坊区"},{"code":"230111","name":"呼兰区"},{"code":"230112","name":"阿城区"},{"code":"230113","name":"双城区"},{"code":"230123","name":"依兰县"},{"code":"230124","name":"方正县"},{"code":"230125","name":"宾县"},{"code":"230126","name":"巴彦县"},{"code":"230127","name":"木兰县"},{"code":"230128","name":"通河县"},{"code":"230129","name":"延寿县"},{"code":"230183","name":"尚志市"},{"code":"230184","name":"五常市"}]},{"code":"230200","name":"齐齐哈尔市","list":[{"code":"230202","name":"龙沙区"},{"code":"230203","name":"建华区"},{"code":"230204","name":"铁锋区"},{"code":"230205","name":"昂昂溪区"},{"code":"230206","name":"富拉尔基区"},{"code":"230207","name":"碾子山区"},{"code":"230208","name":"梅里斯达斡尔族区"},{"code":"230221","name":"龙江县"},{"code":"230223","name":"依安县"},{"code":"230224","name":"泰来县"},{"code":"230225","name":"甘南县"},{"code":"230227","name":"富裕县"},{"code":"230229","name":"克山县"},{"code":"230230","name":"克东县"},{"code":"230231","name":"拜泉县"},{"code":"230281","name":"讷河市"}]},{"code":"230300","name":"鸡西市","list":[{"code":"230302","name":"鸡冠区"},{"code":"230303","name":"恒山区"},{"code":"230304","name":"滴道区"},{"code":"230305","name":"梨树区"},{"code":"230306","name":"城子河区"},{"code":"230307","name":"麻山区"},{"code":"230321","name":"鸡东县"},{"code":"230381","name":"虎林市"},{"code":"230382","name":"密山市"}]},{"code":"230400","name":"鹤岗市","list":[{"code":"230402","name":"向阳区"},{"code":"230403","name":"工农区"},{"code":"230404","name":"南山区"},{"code":"230405","name":"兴安区"},{"code":"230406","name":"东山区"},{"code":"230407","name":"兴山区"},{"code":"230421","name":"萝北县"},{"code":"230422","name":"绥滨县"}]},{"code":"230500","name":"双鸭山市","list":[{"code":"230502","name":"尖山区"},{"code":"230503","name":"岭东区"},{"code":"230505","name":"四方台区"},{"code":"230506","name":"宝山区"},{"code":"230521","name":"集贤县"},{"code":"230522","name":"友谊县"},{"code":"230523","name":"宝清县"},{"code":"230524","name":"饶河县"}]},{"code":"230600","name":"大庆市","list":[{"code":"230602","name":"萨尔图区"},{"code":"230603","name":"龙凤区"},{"code":"230604","name":"让胡路区"},{"code":"230605","name":"红岗区"},{"code":"230606","name":"大同区"},{"code":"230621","name":"肇州县"},{"code":"230622","name":"肇源县"},{"code":"230623","name":"林甸县"},{"code":"230624","name":"杜尔伯特蒙古族自治县"}]},{"code":"230700","name":"伊春市","list":[{"code":"230717","name":"伊美区"},{"code":"230718","name":"乌翠区"},{"code":"230719","name":"友好区"},{"code":"230722","name":"嘉荫县"},{"code":"230723","name":"汤旺县"},{"code":"230724","name":"丰林县"},{"code":"230725","name":"大箐山县"},{"code":"230726","name":"南岔县"},{"code":"230751","name":"金林区"},{"code":"230781","name":"铁力市"}]},{"code":"230800","name":"佳木斯市","list":[{"code":"230803","name":"向阳区"},{"code":"230804","name":"前进区"},{"code":"230805","name":"东风区"},{"code":"230811","name":"郊区"},{"code":"230822","name":"桦南县"},{"code":"230826","name":"桦川县"},{"code":"230828","name":"汤原县"},{"code":"230881","name":"同江市"},{"code":"230882","name":"富锦市"},{"code":"230883","name":"抚远市"}]},{"code":"230900","name":"七台河市","list":[{"code":"230902","name":"新兴区"},{"code":"230903","name":"桃山区"},{"code":"230904","name":"茄子河区"},{"code":"230921","name":"勃利县"}]},{"code":"231000","name":"牡丹江市","list":[{"code":"231002","name":"东安区"},{"code":"231003","name":"阳明区"},{"code":"231004","name":"爱民区"},{"code":"231005","name":"西安区"},{"code":"231025","name":"林口县"},{"code":"231081","name":"绥芬河市"},{"code":"231083","name":"海林市"},{"code":"231084","name":"宁安市"},{"code":"231085","name":"穆棱市"},{"code":"231086","name":"东宁市"}]},{"code":"231100","name":"黑河市","list":[{"code":"231102","name":"爱辉区"},{"code":"231123","name":"逊克县"},{"code":"231124","name":"孙吴县"},{"code":"231181","name":"北安市"},{"code":"231182","name":"五大连池市"},{"code":"231183","name":"嫩江市"}]},{"code":"231200","name":"绥化市","list":[{"code":"231202","name":"北林区"},{"code":"231221","name":"望奎县"},{"code":"231222","name":"兰西县"},{"code":"231223","name":"青冈县"},{"code":"231224","name":"庆安县"},{"code":"231225","name":"明水县"},{"code":"231226","name":"绥棱县"},{"code":"231281","name":"安达市"},{"code":"231282","name":"肇东市"},{"code":"231283","name":"海伦市"}]},{"code":"232700","name":"大兴安岭地区","list":[{"code":"232701","name":"漠河市"},{"code":"232721","name":"呼玛县"},{"code":"232722","name":"塔河县"},{"code":"232761","name":"加格达奇区"},{"code":"232762","name":"松岭区"},{"code":"232763","name":"新林区"},{"code":"232764","name":"呼中区"}]}]},{"code":"310000","name":"上海市","list":[{"code":"310100","name":"上海市","list":[{"code":"310101","name":"黄浦区"},{"code":"310104","name":"徐汇区"},{"code":"310105","name":"长宁区"},{"code":"310106","name":"静安区"},{"code":"310107","name":"普陀区"},{"code":"310109","name":"虹口区"},{"code":"310110","name":"杨浦区"},{"code":"310112","name":"闵行区"},{"code":"310113","name":"宝山区"},{"code":"310114","name":"嘉定区"},{"code":"310115","name":"浦东新区"},{"code":"310116","name":"金山区"},{"code":"310117","name":"松江区"},{"code":"310118","name":"青浦区"},{"code":"310120","name":"奉贤区"},{"code":"310151","name":"崇明区"}]}]},{"code":"320000","name":"江苏省","list":[{"code":"320100","name":"南京市","list":[{"code":"320102","name":"玄武区"},{"code":"320104","name":"秦淮区"},{"code":"320105","name":"建邺区"},{"code":"320106","name":"鼓楼区"},{"code":"320111","name":"浦口区"},{"code":"320113","name":"栖霞区"},{"code":"320114","name":"雨花台区"},{"code":"320115","name":"江宁区"},{"code":"320116","name":"六合区"},{"code":"320117","name":"溧水区"},{"code":"320118","name":"高淳区"}]},{"code":"320200","name":"无锡市","list":[{"code":"320205","name":"锡山区"},{"code":"320206","name":"惠山区"},{"code":"320211","name":"滨湖区"},{"code":"320213","name":"梁溪区"},{"code":"320214","name":"新吴区"},{"code":"320281","name":"江阴市"},{"code":"320282","name":"宜兴市"}]},{"code":"320300","name":"徐州市","list":[{"code":"320302","name":"鼓楼区"},{"code":"320303","name":"云龙区"},{"code":"320305","name":"贾汪区"},{"code":"320311","name":"泉山区"},{"code":"320312","name":"铜山区"},{"code":"320321","name":"丰县"},{"code":"320322","name":"沛县"},{"code":"320324","name":"睢宁县"},{"code":"320381","name":"新沂市"},{"code":"320382","name":"邳州市"}]},{"code":"320400","name":"常州市","list":[{"code":"320402","name":"天宁区"},{"code":"320404","name":"钟楼区"},{"code":"320411","name":"新北区"},{"code":"320412","name":"武进区"},{"code":"320413","name":"金坛区"},{"code":"320481","name":"溧阳市"}]},{"code":"320500","name":"苏州市","list":[{"code":"320505","name":"虎丘区"},{"code":"320506","name":"吴中区"},{"code":"320507","name":"相城区"},{"code":"320508","name":"姑苏区"},{"code":"320509","name":"吴江区"},{"code":"320576","name":"苏州工业园区"},{"code":"320581","name":"常熟市"},{"code":"320582","name":"张家港市"},{"code":"320583","name":"昆山市"},{"code":"320585","name":"太仓市"}]},{"code":"320600","name":"南通市","list":[{"code":"320612","name":"通州区"},{"code":"320613","name":"崇川区"},{"code":"320614","name":"海门区"},{"code":"320623","name":"如东县"},{"code":"320681","name":"启东市"},{"code":"320682","name":"如皋市"},{"code":"320685","name":"海安市"}]},{"code":"320700","name":"连云港市","list":[{"code":"320703","name":"连云区"},{"code":"320706","name":"海州区"},{"code":"320707","name":"赣榆区"},{"code":"320722","name":"东海县"},{"code":"320723","name":"灌云县"},{"code":"320724","name":"灌南县"}]},{"code":"320800","name":"淮安市","list":[{"code":"320803","name":"淮安区"},{"code":"320804","name":"淮阴区"},{"code":"320812","name":"清江浦区"},{"code":"320813","name":"洪泽区"},{"code":"320826","name":"涟水县"},{"code":"320830","name":"盱眙县"},{"code":"320831","name":"金湖县"}]},{"code":"320900","name":"盐城市","list":[{"code":"320902","name":"亭湖区"},{"code":"320903","name":"盐都区"},{"code":"320904","name":"大丰区"},{"code":"320921","name":"响水县"},{"code":"320922","name":"滨海县"},{"code":"320923","name":"阜宁县"},{"code":"320924","name":"射阳县"},{"code":"320925","name":"建湖县"},{"code":"320981","name":"东台市"}]},{"code":"321000","name":"扬州市","list":[{"code":"321002","name":"广陵区"},{"code":"321003","name":"邗江区"},{"code":"321012","name":"江都区"},{"code":"321023","name":"宝应县"},{"code":"321081","name":"仪征市"},{"code":"321084","name":"高邮市"}]},{"code":"321100","name":"镇江市","list":[{"code":"321102","name":"京口区"},{"code":"321111","name":"润州区"},{"code":"321112","name":"丹徒区"},{"code":"321181","name":"丹阳市"},{"code":"321182","name":"扬中市"},{"code":"321183","name":"句容市"}]},{"code":"321200","name":"泰州市","list":[{"code":"321202","name":"海陵区"},{"code":"321203","name":"高港区"},{"code":"321204","name":"姜堰区"},{"code":"321281","name":"兴化市"},{"code":"321282","name":"靖江市"},{"code":"321283","name":"泰兴市"}]},{"code":"321300","name":"宿迁市","list":[{"code":"321302","name":"宿城区"},{"code":"321311","name":"宿豫区"},{"code":"321322","name":"沭阳县"},{"code":"321323","name":"泗阳县"},{"code":"321324","name":"泗洪县"}]}]},{"code":"330000","name":"浙江省","list":[{"code":"330100","name":"杭州市","list":[{"code":"330102","name":"上城区"},{"code":"330105","name":"拱墅区"},{"code":"330106","name":"西湖区"},{"code":"330108","name":"滨江区"},{"code":"330109","name":"萧山区"},{"code":"330110","name":"余杭区"},{"code":"330111","name":"富阳区"},{"code":"330112","name":"临安区"},{"code":"330113","name":"临平区"},{"code":"330114","name":"钱塘区"},{"code":"330122","name":"桐庐县"},{"code":"330127","name":"淳安县"},{"code":"330182","name":"建德市"}]},{"code":"330200","name":"宁波市","list":[{"code":"330203","name":"海曙区"},{"code":"330205","name":"江北区"},{"code":"330206","name":"北仑区"},{"code":"330211","name":"镇海区"},{"code":"330212","name":"鄞州区"},{"code":"330213","name":"奉化区"},{"code":"330225","name":"象山县"},{"code":"330226","name":"宁海县"},{"code":"330281","name":"余姚市"},{"code":"330282","name":"慈溪市"}]},{"code":"330300","name":"温州市","list":[{"code":"330302","name":"鹿城区"},{"code":"330303","name":"龙湾区"},{"code":"330304","name":"瓯海区"},{"code":"330305","name":"洞头区"},{"code":"330324","name":"永嘉县"},{"code":"330326","name":"平阳县"},{"code":"330327","name":"苍南县"},{"code":"330328","name":"文成县"},{"code":"330329","name":"泰顺县"},{"code":"330381","name":"瑞安市"},{"code":"330382","name":"乐清市"},{"code":"330383","name":"龙港市"}]},{"code":"330400","name":"嘉兴市","list":[{"code":"330402","name":"南湖区"},{"code":"330411","name":"秀洲区"},{"code":"330421","name":"嘉善县"},{"code":"330424","name":"海盐县"},{"code":"330481","name":"海宁市"},{"code":"330482","name":"平湖市"},{"code":"330483","name":"桐乡市"}]},{"code":"330500","name":"湖州市","list":[{"code":"330502","name":"吴兴区"},{"code":"330503","name":"南浔区"},{"code":"330521","name":"德清县"},{"code":"330522","name":"长兴县"},{"code":"330523","name":"安吉县"}]},{"code":"330600","name":"绍兴市","list":[{"code":"330602","name":"越城区"},{"code":"330603","name":"柯桥区"},{"code":"330604","name":"上虞区"},{"code":"330624","name":"新昌县"},{"code":"330681","name":"诸暨市"},{"code":"330683","name":"嵊州市"}]},{"code":"330700","name":"金华市","list":[{"code":"330702","name":"婺城区"},{"code":"330703","name":"金东区"},{"code":"330723","name":"武义县"},{"code":"330726","name":"浦江县"},{"code":"330727","name":"磐安县"},{"code":"330781","name":"兰溪市"},{"code":"330782","name":"义乌市"},{"code":"330783","name":"东阳市"},{"code":"330784","name":"永康市"}]},{"code":"330800","name":"衢州市","list":[{"code":"330802","name":"柯城区"},{"code":"330803","name":"衢江区"},{"code":"330822","name":"常山县"},{"code":"330824","name":"开化县"},{"code":"330825","name":"龙游县"},{"code":"330881","name":"江山市"}]},{"code":"330900","name":"舟山市","list":[{"code":"330902","name":"定海区"},{"code":"330903","name":"普陀区"},{"code":"330921","name":"岱山县"},{"code":"330922","name":"嵊泗县"}]},{"code":"331000","name":"台州市","list":[{"code":"331002","name":"椒江区"},{"code":"331003","name":"黄岩区"},{"code":"331004","name":"路桥区"},{"code":"331022","name":"三门县"},{"code":"331023","name":"天台县"},{"code":"331024","name":"仙居县"},{"code":"331081","name":"温岭市"},{"code":"331082","name":"临海市"},{"code":"331083","name":"玉环市"}]},{"code":"331100","name":"丽水市","list":[{"code":"331102","name":"莲都区"},{"code":"331121","name":"青田县"},{"code":"331122","name":"缙云县"},{"code":"331123","name":"遂昌县"},{"code":"331124","name":"松阳县"},{"code":"331125","name":"云和县"},{"code":"331126","name":"庆元县"},{"code":"331127","name":"景宁畲族自治县"},{"code":"331181","name":"龙泉市"}]}]},{"code":"340000","name":"安徽省","list":[{"code":"340100","name":"合肥市","list":[{"code":"340102","name":"瑶海区"},{"code":"340103","name":"庐阳区"},{"code":"340104","name":"蜀山区"},{"code":"340111","name":"包河区"},{"code":"340121","name":"长丰县"},{"code":"340122","name":"肥东县"},{"code":"340123","name":"肥西县"},{"code":"340124","name":"庐江县"},{"code":"340181","name":"巢湖市"}]},{"code":"340200","name":"芜湖市","list":[{"code":"340202","name":"镜湖区"},{"code":"340207","name":"鸠江区"},{"code":"340209","name":"弋江区"},{"code":"340210","name":"湾沚区"},{"code":"340212","name":"繁昌区"},{"code":"340223","name":"南陵县"},{"code":"340281","name":"无为市"}]},{"code":"340300","name":"蚌埠市","list":[{"code":"340302","name":"龙子湖区"},{"code":"340303","name":"蚌山区"},{"code":"340304","name":"禹会区"},{"code":"340311","name":"淮上区"},{"code":"340321","name":"怀远县"},{"code":"340322","name":"五河县"},{"code":"340323","name":"固镇县"}]},{"code":"340400","name":"淮南市","list":[{"code":"340402","name":"大通区"},{"code":"340403","name":"田家庵区"},{"code":"340404","name":"谢家集区"},{"code":"340405","name":"八公山区"},{"code":"340406","name":"潘集区"},{"code":"340421","name":"凤台县"},{"code":"340422","name":"寿县"}]},{"code":"340500","name":"马鞍山市","list":[{"code":"340503","name":"花山区"},{"code":"340504","name":"雨山区"},{"code":"340506","name":"博望区"},{"code":"340521","name":"当涂县"},{"code":"340522","name":"含山县"},{"code":"340523","name":"和县"}]},{"code":"340600","name":"淮北市","list":[{"code":"340602","name":"杜集区"},{"code":"340603","name":"相山区"},{"code":"340604","name":"烈山区"},{"code":"340621","name":"濉溪县"}]},{"code":"340700","name":"铜陵市","list":[{"code":"340705","name":"铜官区"},{"code":"340706","name":"义安区"},{"code":"340711","name":"郊区"},{"code":"340722","name":"枞阳县"}]},{"code":"340800","name":"安庆市","list":[{"code":"340802","name":"迎江区"},{"code":"340803","name":"大观区"},{"code":"340811","name":"宜秀区"},{"code":"340822","name":"怀宁县"},{"code":"340825","name":"太湖县"},{"code":"340826","name":"宿松县"},{"code":"340827","name":"望江县"},{"code":"340828","name":"岳西县"},{"code":"340881","name":"桐城市"},{"code":"340882","name":"潜山市"}]},{"code":"341000","name":"黄山市","list":[{"code":"341002","name":"屯溪区"},{"code":"341003","name":"黄山区"},{"code":"341004","name":"徽州区"},{"code":"341021","name":"歙县"},{"code":"341022","name":"休宁县"},{"code":"341023","name":"黟县"},{"code":"341024","name":"祁门县"}]},{"code":"341100","name":"滁州市","list":[{"code":"341102","name":"琅琊区"},{"code":"341103","name":"南谯区"},{"code":"341122","name":"来安县"},{"code":"341124","name":"全椒县"},{"code":"341125","name":"定远县"},{"code":"341126","name":"凤阳县"},{"code":"341181","name":"天长市"},{"code":"341182","name":"明光市"}]},{"code":"341200","name":"阜阳市","list":[{"code":"341202","name":"颍州区"},{"code":"341203","name":"颍东区"},{"code":"341204","name":"颍泉区"},{"code":"341221","name":"临泉县"},{"code":"341222","name":"太和县"},{"code":"341225","name":"阜南县"},{"code":"341226","name":"颍上县"},{"code":"341282","name":"界首市"}]},{"code":"341300","name":"宿州市","list":[{"code":"341302","name":"埇桥区"},{"code":"341321","name":"砀山县"},{"code":"341322","name":"萧县"},{"code":"341323","name":"灵璧县"},{"code":"341324","name":"泗县"}]},{"code":"341500","name":"六安市","list":[{"code":"341502","name":"金安区"},{"code":"341503","name":"裕安区"},{"code":"341504","name":"叶集区"},{"code":"341522","name":"霍邱县"},{"code":"341523","name":"舒城县"},{"code":"341524","name":"金寨县"},{"code":"341525","name":"霍山县"}]},{"code":"341600","name":"亳州市","list":[{"code":"341602","name":"谯城区"},{"code":"341621","name":"涡阳县"},{"code":"341622","name":"蒙城县"},{"code":"341623","name":"利辛县"}]},{"code":"341700","name":"池州市","list":[{"code":"341702","name":"贵池区"},{"code":"341721","name":"东至县"},{"code":"341722","name":"石台县"},{"code":"341723","name":"青阳县"}]},{"code":"341800","name":"宣城市","list":[{"code":"341802","name":"宣州区"},{"code":"341821","name":"郎溪县"},{"code":"341823","name":"泾县"},{"code":"341824","name":"绩溪县"},{"code":"341825","name":"旌德县"},{"code":"341881","name":"宁国市"},{"code":"341882","name":"广德市"}]}]},{"code":"350000","name":"福建省","list":[{"code":"350100","name":"福州市","list":[{"code":"350102","name":"鼓楼区"},{"code":"350103","name":"台江区"},{"code":"350104","name":"仓山区"},{"code":"350105","name":"马尾区"},{"code":"350111","name":"晋安区"},{"code":"350112","name":"长乐区"},{"code":"350121","name":"闽侯县"},{"code":"350122","name":"连江县"},{"code":"350123","name":"罗源县"},{"code":"350124","name":"闽清县"},{"code":"350125","name":"永泰县"},{"code":"350128","name":"平潭县"},{"code":"350181","name":"福清市"}]},{"code":"350200","name":"厦门市","list":[{"code":"350203","name":"思明区"},{"code":"350205","name":"海沧区"},{"code":"350206","name":"湖里区"},{"code":"350211","name":"集美区"},{"code":"350212","name":"同安区"},{"code":"350213","name":"翔安区"}]},{"code":"350300","name":"莆田市","list":[{"code":"350302","name":"城厢区"},{"code":"350303","name":"涵江区"},{"code":"350304","name":"荔城区"},{"code":"350305","name":"秀屿区"},{"code":"350322","name":"仙游县"}]},{"code":"350400","name":"三明市","list":[{"code":"350404","name":"三元区"},{"code":"350405","name":"沙县区"},{"code":"350421","name":"明溪县"},{"code":"350423","name":"清流县"},{"code":"350424","name":"宁化县"},{"code":"350425","name":"大田县"},{"code":"350426","name":"尤溪县"},{"code":"350428","name":"将乐县"},{"code":"350429","name":"泰宁县"},{"code":"350430","name":"建宁县"},{"code":"350481","name":"永安市"}]},{"code":"350500","name":"泉州市","list":[{"code":"350502","name":"鲤城区"},{"code":"350503","name":"丰泽区"},{"code":"350504","name":"洛江区"},{"code":"350505","name":"泉港区"},{"code":"350521","name":"惠安县"},{"code":"350524","name":"安溪县"},{"code":"350525","name":"永春县"},{"code":"350526","name":"德化县"},{"code":"350527","name":"金门县"},{"code":"350581","name":"石狮市"},{"code":"350582","name":"晋江市"},{"code":"350583","name":"南安市"}]},{"code":"350600","name":"漳州市","list":[{"code":"350602","name":"芗城区"},{"code":"350603","name":"龙文区"},{"code":"350604","name":"龙海区"},{"code":"350605","name":"长泰区"},{"code":"350622","name":"云霄县"},{"code":"350623","name":"漳浦县"},{"code":"350624","name":"诏安县"},{"code":"350626","name":"东山县"},{"code":"350627","name":"南靖县"},{"code":"350628","name":"平和县"},{"code":"350629","name":"华安县"}]},{"code":"350700","name":"南平市","list":[{"code":"350702","name":"延平区"},{"code":"350703","name":"建阳区"},{"code":"350721","name":"顺昌县"},{"code":"350722","name":"浦城县"},{"code":"350723","name":"光泽县"},{"code":"350724","name":"松溪县"},{"code":"350725","name":"政和县"},{"code":"350781","name":"邵武市"},{"code":"350782","name":"武夷山市"},{"code":"350783","name":"建瓯市"}]},{"code":"350800","name":"龙岩市","list":[{"code":"350802","name":"新罗区"},{"code":"350803","name":"永定区"},{"code":"350821","name":"长汀县"},{"code":"350823","name":"上杭县"},{"code":"350824","name":"武平县"},{"code":"350825","name":"连城县"},{"code":"350881","name":"漳平市"}]},{"code":"350900","name":"宁德市","list":[{"code":"350902","name":"蕉城区"},{"code":"350921","name":"霞浦县"},{"code":"350922","name":"古田县"},{"code":"350923","name":"屏南县"},{"code":"350924","name":"寿宁县"},{"code":"350925","name":"周宁县"},{"code":"350926","name":"柘荣县"},{"code":"350981","name":"福安市"},{"code":"350982","name":"福鼎市"}]}]},{"code":"360000","name":"江西省","list":[{"code":"360100","name":"南昌市","list":[{"code":"360102","name":"东湖区"},{"code":"360103","name":"西湖区"},{"code":"360104","name":"青云谱区"},{"code":"360111","name":"青山湖区"},{"code":"360112","name":"新建区"},{"code":"360113","name":"红谷滩区"},{"code":"360121","name":"南昌县"},{"code":"360123","name":"安义县"},{"code":"360124","name":"进贤县"}]},{"code":"360200","name":"景德镇市","list":[{"code":"360202","name":"昌江区"},{"code":"360203","name":"珠山区"},{"code":"360222","name":"浮梁县"},{"code":"360281","name":"乐平市"}]},{"code":"360300","name":"萍乡市","list":[{"code":"360302","name":"安源区"},{"code":"360313","name":"湘东区"},{"code":"360321","name":"莲花县"},{"code":"360322","name":"上栗县"},{"code":"360323","name":"芦溪县"}]},{"code":"360400","name":"九江市","list":[{"code":"360402","name":"濂溪区"},{"code":"360403","name":"浔阳区"},{"code":"360404","name":"柴桑区"},{"code":"360423","name":"武宁县"},{"code":"360424","name":"修水县"},{"code":"360425","name":"永修县"},{"code":"360426","name":"德安县"},{"code":"360428","name":"都昌县"},{"code":"360429","name":"湖口县"},{"code":"360430","name":"彭泽县"},{"code":"360481","name":"瑞昌市"},{"code":"360482","name":"共青城市"},{"code":"360483","name":"庐山市"}]},{"code":"360500","name":"新余市","list":[{"code":"360502","name":"渝水区"},{"code":"360521","name":"分宜县"}]},{"code":"360600","name":"鹰潭市","list":[{"code":"360602","name":"月湖区"},{"code":"360603","name":"余江区"},{"code":"360681","name":"贵溪市"}]},{"code":"360700","name":"赣州市","list":[{"code":"360702","name":"章贡区"},{"code":"360703","name":"南康区"},{"code":"360704","name":"赣县区"},{"code":"360722","name":"信丰县"},{"code":"360723","name":"大余县"},{"code":"360724","name":"上犹县"},{"code":"360725","name":"崇义县"},{"code":"360726","name":"安远县"},{"code":"360728","name":"定南县"},{"code":"360729","name":"全南县"},{"code":"360730","name":"宁都县"},{"code":"360731","name":"于都县"},{"code":"360732","name":"兴国县"},{"code":"360733","name":"会昌县"},{"code":"360734","name":"寻乌县"},{"code":"360735","name":"石城县"},{"code":"360781","name":"瑞金市"},{"code":"360783","name":"龙南市"}]},{"code":"360800","name":"吉安市","list":[{"code":"360802","name":"吉州区"},{"code":"360803","name":"青原区"},{"code":"360821","name":"吉安县"},{"code":"360822","name":"吉水县"},{"code":"360823","name":"峡江县"},{"code":"360824","name":"新干县"},{"code":"360825","name":"永丰县"},{"code":"360826","name":"泰和县"},{"code":"360827","name":"遂川县"},{"code":"360828","name":"万安县"},{"code":"360829","name":"安福县"},{"code":"360830","name":"永新县"},{"code":"360881","name":"井冈山市"}]},{"code":"360900","name":"宜春市","list":[{"code":"360902","name":"袁州区"},{"code":"360921","name":"奉新县"},{"code":"360922","name":"万载县"},{"code":"360923","name":"上高县"},{"code":"360924","name":"宜丰县"},{"code":"360925","name":"靖安县"},{"code":"360926","name":"铜鼓县"},{"code":"360981","name":"丰城市"},{"code":"360982","name":"樟树市"},{"code":"360983","name":"高安市"}]},{"code":"361000","name":"抚州市","list":[{"code":"361002","name":"临川区"},{"code":"361003","name":"东乡区"},{"code":"361021","name":"南城县"},{"code":"361022","name":"黎川县"},{"code":"361023","name":"南丰县"},{"code":"361024","name":"崇仁县"},{"code":"361025","name":"乐安县"},{"code":"361026","name":"宜黄县"},{"code":"361027","name":"金溪县"},{"code":"361028","name":"资溪县"},{"code":"361030","name":"广昌县"}]},{"code":"361100","name":"上饶市","list":[{"code":"361102","name":"信州区"},{"code":"361103","name":"广丰区"},{"code":"361104","name":"广信区"},{"code":"361123","name":"玉山县"},{"code":"361124","name":"铅山县"},{"code":"361125","name":"横峰县"},{"code":"361126","name":"弋阳县"},{"code":"361127","name":"余干县"},{"code":"361128","name":"鄱阳县"},{"code":"361129","name":"万年县"},{"code":"361130","name":"婺源县"},{"code":"361181","name":"德兴市"}]}]},{"code":"370000","name":"山东省","list":[{"code":"370100","name":"济南市","list":[{"code":"370102","name":"历下区"},{"code":"370103","name":"市中区"},{"code":"370104","name":"槐荫区"},{"code":"370105","name":"天桥区"},{"code":"370112","name":"历城区"},{"code":"370113","name":"长清区"},{"code":"370114","name":"章丘区"},{"code":"370115","name":"济阳区"},{"code":"370116","name":"莱芜区"},{"code":"370117","name":"钢城区"},{"code":"370124","name":"平阴县"},{"code":"370126","name":"商河县"}]},{"code":"370200","name":"青岛市","list":[{"code":"370202","name":"市南区"},{"code":"370203","name":"市北区"},{"code":"370211","name":"黄岛区"},{"code":"370212","name":"崂山区"},{"code":"370213","name":"李沧区"},{"code":"370214","name":"城阳区"},{"code":"370215","name":"即墨区"},{"code":"370281","name":"胶州市"},{"code":"370283","name":"平度市"},{"code":"370285","name":"莱西市"}]},{"code":"370300","name":"淄博市","list":[{"code":"370302","name":"淄川区"},{"code":"370303","name":"张店区"},{"code":"370304","name":"博山区"},{"code":"370305","name":"临淄区"},{"code":"370306","name":"周村区"},{"code":"370321","name":"桓台县"},{"code":"370322","name":"高青县"},{"code":"370323","name":"沂源县"}]},{"code":"370400","name":"枣庄市","list":[{"code":"370402","name":"市中区"},{"code":"370403","name":"薛城区"},{"code":"370404","name":"峄城区"},{"code":"370405","name":"台儿庄区"},{"code":"370406","name":"山亭区"},{"code":"370481","name":"滕州市"}]},{"code":"370500","name":"东营市","list":[{"code":"370502","name":"东营区"},{"code":"370503","name":"河口区"},{"code":"370505","name":"垦利区"},{"code":"370522","name":"利津县"},{"code":"370523","name":"广饶县"}]},{"code":"370600","name":"烟台市","list":[{"code":"370602","name":"芝罘区"},{"code":"370611","name":"福山区"},{"code":"370612","name":"牟平区"},{"code":"370613","name":"莱山区"},{"code":"370614","name":"蓬莱区"},{"code":"370681","name":"龙口市"},{"code":"370682","name":"莱阳市"},{"code":"370683","name":"莱州市"},{"code":"370685","name":"招远市"},{"code":"370686","name":"栖霞市"},{"code":"370687","name":"海阳市"}]},{"code":"370700","name":"潍坊市","list":[{"code":"370702","name":"潍城区"},{"code":"370703","name":"寒亭区"},{"code":"370704","name":"坊子区"},{"code":"370705","name":"奎文区"},{"code":"370724","name":"临朐县"},{"code":"370725","name":"昌乐县"},{"code":"370781","name":"青州市"},{"code":"370782","name":"诸城市"},{"code":"370783","name":"寿光市"},{"code":"370784","name":"安丘市"},{"code":"370785","name":"高密市"},{"code":"370786","name":"昌邑市"}]},{"code":"370800","name":"济宁市","list":[{"code":"370811","name":"任城区"},{"code":"370812","name":"兖州区"},{"code":"370826","name":"微山县"},{"code":"370827","name":"鱼台县"},{"code":"370828","name":"金乡县"},{"code":"370829","name":"嘉祥县"},{"code":"370830","name":"汶上县"},{"code":"370831","name":"泗水县"},{"code":"370832","name":"梁山县"},{"code":"370881","name":"曲阜市"},{"code":"370883","name":"邹城市"}]},{"code":"370900","name":"泰安市","list":[{"code":"370902","name":"泰山区"},{"code":"370911","name":"岱岳区"},{"code":"370921","name":"宁阳县"},{"code":"370923","name":"东平县"},{"code":"370982","name":"新泰市"},{"code":"370983","name":"肥城市"}]},{"code":"371000","name":"威海市","list":[{"code":"371002","name":"环翠区"},{"code":"371003","name":"文登区"},{"code":"371082","name":"荣成市"},{"code":"371083","name":"乳山市"}]},{"code":"371100","name":"日照市","list":[{"code":"371102","name":"东港区"},{"code":"371103","name":"岚山区"},{"code":"371121","name":"五莲县"},{"code":"371122","name":"莒县"}]},{"code":"371300","name":"临沂市","list":[{"code":"371302","name":"兰山区"},{"code":"371311","name":"罗庄区"},{"code":"371312","name":"河东区"},{"code":"371321","name":"沂南县"},{"code":"371322","name":"郯城县"},{"code":"371323","name":"沂水县"},{"code":"371324","name":"兰陵县"},{"code":"371325","name":"费县"},{"code":"371326","name":"平邑县"},{"code":"371327","name":"莒南县"},{"code":"371328","name":"蒙阴县"},{"code":"371329","name":"临沭县"}]},{"code":"371400","name":"德州市","list":[{"code":"371402","name":"德城区"},{"code":"371403","name":"陵城区"},{"code":"371422","name":"宁津县"},{"code":"371423","name":"庆云县"},{"code":"371424","name":"临邑县"},{"code":"371425","name":"齐河县"},{"code":"371426","name":"平原县"},{"code":"371427","name":"夏津县"},{"code":"371428","name":"武城县"},{"code":"371481","name":"乐陵市"},{"code":"371482","name":"禹城市"}]},{"code":"371500","name":"聊城市","list":[{"code":"371502","name":"东昌府区"},{"code":"371503","name":"茌平区"},{"code":"371521","name":"阳谷县"},{"code":"371522","name":"莘县"},{"code":"371524","name":"东阿县"},{"code":"371525","name":"冠县"},{"code":"371526","name":"高唐县"},{"code":"371581","name":"临清市"}]},{"code":"371600","name":"滨州市","list":[{"code":"371602","name":"滨城区"},{"code":"371603","name":"沾化区"},{"code":"371621","name":"惠民县"},{"code":"371622","name":"阳信县"},{"code":"371623","name":"无棣县"},{"code":"371625","name":"博兴县"},{"code":"371681","name":"邹平市"}]},{"code":"371700","name":"菏泽市","list":[{"code":"371702","name":"牡丹区"},{"code":"371703","name":"定陶区"},{"code":"371721","name":"曹县"},{"code":"371722","name":"单县"},{"code":"371723","name":"成武县"},{"code":"371724","name":"巨野县"},{"code":"371725","name":"郓城县"},{"code":"371726","name":"鄄城县"},{"code":"371728","name":"东明县"}]}]},{"code":"410000","name":"河南省","list":[{"code":"410100","name":"郑州市","list":[{"code":"410102","name":"中原区"},{"code":"410103","name":"二七区"},{"code":"410104","name":"管城回族区"},{"code":"410105","name":"金水区"},{"code":"410106","name":"上街区"},{"code":"410108","name":"惠济区"},{"code":"410122","name":"中牟县"},{"code":"410181","name":"巩义市"},{"code":"410182","name":"荥阳市"},{"code":"410183","name":"新密市"},{"code":"410184","name":"新郑市"},{"code":"410185","name":"登封市"}]},{"code":"410200","name":"开封市","list":[{"code":"410202","name":"龙亭区"},{"code":"410203","name":"顺河回族区"},{"code":"410204","name":"鼓楼区"},{"code":"410205","name":"禹王台区"},{"code":"410212","name":"祥符区"},{"code":"410221","name":"杞县"},{"code":"410222","name":"通许县"},{"code":"410223","name":"尉氏县"},{"code":"410225","name":"兰考县"}]},{"code":"410300","name":"洛阳市","list":[{"code":"410302","name":"老城区"},{"code":"410303","name":"西工区"},{"code":"410304","name":"瀍河回族区"},{"code":"410305","name":"涧西区"},{"code":"410307","name":"偃师区"},{"code":"410308","name":"孟津区"},{"code":"410311","name":"洛龙区"},{"code":"410323","name":"新安县"},{"code":"410324","name":"栾川县"},{"code":"410325","name":"嵩县"},{"code":"410326","name":"汝阳县"},{"code":"410327","name":"宜阳县"},{"code":"410328","name":"洛宁县"},{"code":"410329","name":"伊川县"}]},{"code":"410400","name":"平顶山市","list":[{"code":"410402","name":"新华区"},{"code":"410403","name":"卫东区"},{"code":"410404","name":"石龙区"},{"code":"410411","name":"湛河区"},{"code":"410421","name":"宝丰县"},{"code":"410422","name":"叶县"},{"code":"410423","name":"鲁山县"},{"code":"410425","name":"郏县"},{"code":"410481","name":"舞钢市"},{"code":"410482","name":"汝州市"}]},{"code":"410500","name":"安阳市","list":[{"code":"410502","name":"文峰区"},{"code":"410503","name":"北关区"},{"code":"410505","name":"殷都区"},{"code":"410506","name":"龙安区"},{"code":"410522","name":"安阳县"},{"code":"410523","name":"汤阴县"},{"code":"410526","name":"滑县"},{"code":"410527","name":"内黄县"},{"code":"410581","name":"林州市"}]},{"code":"410600","name":"鹤壁市","list":[{"code":"410602","name":"鹤山区"},{"code":"410603","name":"山城区"},{"code":"410611","name":"淇滨区"},{"code":"410621","name":"浚县"},{"code":"410622","name":"淇县"}]},{"code":"410700","name":"新乡市","list":[{"code":"410702","name":"红旗区"},{"code":"410703","name":"卫滨区"},{"code":"410704","name":"凤泉区"},{"code":"410711","name":"牧野区"},{"code":"410721","name":"新乡县"},{"code":"410724","name":"获嘉县"},{"code":"410725","name":"原阳县"},{"code":"410726","name":"延津县"},{"code":"410727","name":"封丘县"},{"code":"410781","name":"卫辉市"},{"code":"410782","name":"辉县市"},{"code":"410783","name":"长垣市"}]},{"code":"410800","name":"焦作市","list":[{"code":"410802","name":"解放区"},{"code":"410803","name":"中站区"},{"code":"410804","name":"马村区"},{"code":"410811","name":"山阳区"},{"code":"410821","name":"修武县"},{"code":"410822","name":"博爱县"},{"code":"410823","name":"武陟县"},{"code":"410825","name":"温县"},{"code":"410882","name":"沁阳市"},{"code":"410883","name":"孟州市"}]},{"code":"410900","name":"濮阳市","list":[{"code":"410902","name":"华龙区"},{"code":"410922","name":"清丰县"},{"code":"410923","name":"南乐县"},{"code":"410926","name":"范县"},{"code":"410927","name":"台前县"},{"code":"410928","name":"濮阳县"}]},{"code":"411000","name":"许昌市","list":[{"code":"411002","name":"魏都区"},{"code":"411003","name":"建安区"},{"code":"411024","name":"鄢陵县"},{"code":"411025","name":"襄城县"},{"code":"411081","name":"禹州市"},{"code":"411082","name":"长葛市"}]},{"code":"411100","name":"漯河市","list":[{"code":"411102","name":"源汇区"},{"code":"411103","name":"郾城区"},{"code":"411104","name":"召陵区"},{"code":"411121","name":"舞阳县"},{"code":"411122","name":"临颍县"}]},{"code":"411200","name":"三门峡市","list":[{"code":"411202","name":"湖滨区"},{"code":"411203","name":"陕州区"},{"code":"411221","name":"渑池县"},{"code":"411224","name":"卢氏县"},{"code":"411281","name":"义马市"},{"code":"411282","name":"灵宝市"}]},{"code":"411300","name":"南阳市","list":[{"code":"411302","name":"宛城区"},{"code":"411303","name":"卧龙区"},{"code":"411321","name":"南召县"},{"code":"411322","name":"方城县"},{"code":"411323","name":"西峡县"},{"code":"411324","name":"镇平县"},{"code":"411325","name":"内乡县"},{"code":"411326","name":"淅川县"},{"code":"411327","name":"社旗县"},{"code":"411328","name":"唐河县"},{"code":"411329","name":"新野县"},{"code":"411330","name":"桐柏县"},{"code":"411381","name":"邓州市"}]},{"code":"411400","name":"商丘市","list":[{"code":"411402","name":"梁园区"},{"code":"411403","name":"睢阳区"},{"code":"411421","name":"民权县"},{"code":"411422","name":"睢县"},{"code":"411423","name":"宁陵县"},{"code":"411424","name":"柘城县"},{"code":"411425","name":"虞城县"},{"code":"411426","name":"夏邑县"},{"code":"411481","name":"永城市"}]},{"code":"411500","name":"信阳市","list":[{"code":"411502","name":"浉河区"},{"code":"411503","name":"平桥区"},{"code":"411521","name":"罗山县"},{"code":"411522","name":"光山县"},{"code":"411523","name":"新县"},{"code":"411524","name":"商城县"},{"code":"411525","name":"固始县"},{"code":"411526","name":"潢川县"},{"code":"411527","name":"淮滨县"},{"code":"411528","name":"息县"}]},{"code":"411600","name":"周口市","list":[{"code":"411602","name":"川汇区"},{"code":"411603","name":"淮阳区"},{"code":"411621","name":"扶沟县"},{"code":"411622","name":"西华县"},{"code":"411623","name":"商水县"},{"code":"411624","name":"沈丘县"},{"code":"411625","name":"郸城县"},{"code":"411627","name":"太康县"},{"code":"411628","name":"鹿邑县"},{"code":"411681","name":"项城市"}]},{"code":"411700","name":"驻马店市","list":[{"code":"411702","name":"驿城区"},{"code":"411721","name":"西平县"},{"code":"411722","name":"上蔡县"},{"code":"411723","name":"平舆县"},{"code":"411724","name":"正阳县"},{"code":"411725","name":"确山县"},{"code":"411726","name":"泌阳县"},{"code":"411727","name":"汝南县"},{"code":"411728","name":"遂平县"},{"code":"411729","name":"新蔡县"}]},{"code":"419001","name":"济源市","list":[{"code":"419001001","name":"沁园街道"},{"code":"419001002","name":"济水街道"},{"code":"419001003","name":"北海街道"},{"code":"419001004","name":"天坛街道"},{"code":"419001005","name":"玉泉街道"},{"code":"419001100","name":"克井镇"},{"code":"419001101","name":"五龙口镇"},{"code":"419001102","name":"轵城镇"},{"code":"419001103","name":"承留镇"},{"code":"419001104","name":"邵原镇"},{"code":"419001105","name":"坡头镇"},{"code":"419001106","name":"梨林镇"},{"code":"419001107","name":"大峪镇"},{"code":"419001108","name":"思礼镇"},{"code":"419001109","name":"王屋镇"},{"code":"419001110","name":"下冶镇"}]}]},{"code":"420000","name":"湖北省","list":[{"code":"420100","name":"武汉市","list":[{"code":"420102","name":"江岸区"},{"code":"420103","name":"江汉区"},{"code":"420104","name":"硚口区"},{"code":"420105","name":"汉阳区"},{"code":"420106","name":"武昌区"},{"code":"420107","name":"青山区"},{"code":"420111","name":"洪山区"},{"code":"420112","name":"东西湖区"},{"code":"420113","name":"汉南区"},{"code":"420114","name":"蔡甸区"},{"code":"420115","name":"江夏区"},{"code":"420116","name":"黄陂区"},{"code":"420117","name":"新洲区"}]},{"code":"420200","name":"黄石市","list":[{"code":"420202","name":"黄石港区"},{"code":"420203","name":"西塞山区"},{"code":"420204","name":"下陆区"},{"code":"420205","name":"铁山区"},{"code":"420222","name":"阳新县"},{"code":"420281","name":"大冶市"}]},{"code":"420300","name":"十堰市","list":[{"code":"420302","name":"茅箭区"},{"code":"420303","name":"张湾区"},{"code":"420304","name":"郧阳区"},{"code":"420322","name":"郧西县"},{"code":"420323","name":"竹山县"},{"code":"420324","name":"竹溪县"},{"code":"420325","name":"房县"},{"code":"420381","name":"丹江口市"}]},{"code":"420500","name":"宜昌市","list":[{"code":"420502","name":"西陵区"},{"code":"420503","name":"伍家岗区"},{"code":"420504","name":"点军区"},{"code":"420505","name":"猇亭区"},{"code":"420506","name":"夷陵区"},{"code":"420525","name":"远安县"},{"code":"420526","name":"兴山县"},{"code":"420527","name":"秭归县"},{"code":"420528","name":"长阳土家族自治县"},{"code":"420529","name":"五峰土家族自治县"},{"code":"420581","name":"宜都市"},{"code":"420582","name":"当阳市"},{"code":"420583","name":"枝江市"}]},{"code":"420600","name":"襄阳市","list":[{"code":"420602","name":"襄城区"},{"code":"420606","name":"樊城区"},{"code":"420607","name":"襄州区"},{"code":"420624","name":"南漳县"},{"code":"420625","name":"谷城县"},{"code":"420626","name":"保康县"},{"code":"420682","name":"老河口市"},{"code":"420683","name":"枣阳市"},{"code":"420684","name":"宜城市"}]},{"code":"420700","name":"鄂州市","list":[{"code":"420702","name":"梁子湖区"},{"code":"420703","name":"华容区"},{"code":"420704","name":"鄂城区"}]},{"code":"420800","name":"荆门市","list":[{"code":"420802","name":"东宝区"},{"code":"420804","name":"掇刀区"},{"code":"420822","name":"沙洋县"},{"code":"420881","name":"钟祥市"},{"code":"420882","name":"京山市"}]},{"code":"420900","name":"孝感市","list":[{"code":"420902","name":"孝南区"},{"code":"420921","name":"孝昌县"},{"code":"420922","name":"大悟县"},{"code":"420923","name":"云梦县"},{"code":"420981","name":"应城市"},{"code":"420982","name":"安陆市"},{"code":"420984","name":"汉川市"}]},{"code":"421000","name":"荆州市","list":[{"code":"421002","name":"沙市区"},{"code":"421003","name":"荆州区"},{"code":"421022","name":"公安县"},{"code":"421024","name":"江陵县"},{"code":"421081","name":"石首市"},{"code":"421083","name":"洪湖市"},{"code":"421087","name":"松滋市"},{"code":"421088","name":"监利市"}]},{"code":"421100","name":"黄冈市","list":[{"code":"421102","name":"黄州区"},{"code":"421121","name":"团风县"},{"code":"421122","name":"红安县"},{"code":"421123","name":"罗田县"},{"code":"421124","name":"英山县"},{"code":"421125","name":"浠水县"},{"code":"421126","name":"蕲春县"},{"code":"421127","name":"黄梅县"},{"code":"421181","name":"麻城市"},{"code":"421182","name":"武穴市"}]},{"code":"421200","name":"咸宁市","list":[{"code":"421202","name":"咸安区"},{"code":"421221","name":"嘉鱼县"},{"code":"421222","name":"通城县"},{"code":"421223","name":"崇阳县"},{"code":"421224","name":"通山县"},{"code":"421281","name":"赤壁市"}]},{"code":"421300","name":"随州市","list":[{"code":"421303","name":"曾都区"},{"code":"421321","name":"随县"},{"code":"421381","name":"广水市"}]},{"code":"422800","name":"恩施土家族苗族自治州","list":[{"code":"422801","name":"恩施市"},{"code":"422802","name":"利川市"},{"code":"422822","name":"建始县"},{"code":"422823","name":"巴东县"},{"code":"422825","name":"宣恩县"},{"code":"422826","name":"咸丰县"},{"code":"422827","name":"来凤县"},{"code":"422828","name":"鹤峰县"}]},{"code":"429004","name":"仙桃市","list":[{"code":"429004001","name":"沙嘴街道"},{"code":"429004002","name":"干河街道"},{"code":"429004003","name":"龙华山街道"},{"code":"429004100","name":"郑场镇"},{"code":"429004101","name":"毛嘴镇"},{"code":"429004102","name":"豆河镇"},{"code":"429004103","name":"三伏潭镇"},{"code":"429004104","name":"胡场镇"},{"code":"429004105","name":"长埫口镇"},{"code":"429004106","name":"西流河镇"},{"code":"429004107","name":"沙湖镇"},{"code":"429004108","name":"杨林尾镇"},{"code":"429004109","name":"彭场镇"},{"code":"429004110","name":"张沟镇"},{"code":"429004111","name":"郭河镇"},{"code":"429004112","name":"沔城回族镇"},{"code":"429004113","name":"通海口镇"},{"code":"429004115","name":"陈场镇"},{"code":"429004400","name":"杜湖街道"}]},{"code":"429005","name":"潜江市","list":[{"code":"429005001","name":"园林街道"},{"code":"429005002","name":"泽口街道"},{"code":"429005003","name":"广华寺街道"},{"code":"429005004","name":"周矶街道"},{"code":"429005005","name":"杨市街道"},{"code":"429005006","name":"泰丰街道"},{"code":"429005007","name":"高场街道"},{"code":"429005100","name":"竹根滩镇"},{"code":"429005101","name":"渔洋镇"},{"code":"429005102","name":"老新镇"},{"code":"429005103","name":"熊口镇"},{"code":"429005104","name":"王场镇"},{"code":"429005105","name":"高石碑镇"},{"code":"429005106","name":"积玉口镇"},{"code":"429005107","name":"浩口镇"},{"code":"429005108","name":"张金镇"},{"code":"429005109","name":"龙湾镇"},{"code":"429005451","name":"后湖管理区"},{"code":"429005452","name":"熊口管理区"},{"code":"429005453","name":"总口管理区"},{"code":"429005455","name":"运粮湖管理区"}]},{"code":"429006","name":"天门市","list":[{"code":"429006001","name":"竟陵街道"},{"code":"429006002","name":"候口街道"},{"code":"429006003","name":"杨林街道"},{"code":"429006100","name":"多宝镇"},{"code":"429006101","name":"拖市镇"},{"code":"429006102","name":"张港镇"},{"code":"429006103","name":"蒋场镇"},{"code":"429006104","name":"汪场镇"},{"code":"429006105","name":"渔薪镇"},{"code":"429006106","name":"黄潭镇"},{"code":"429006107","name":"岳口镇"},{"code":"429006108","name":"横林镇"},{"code":"429006109","name":"彭市镇"},{"code":"429006110","name":"麻洋镇"},{"code":"429006111","name":"多祥镇"},{"code":"429006112","name":"干驿镇"},{"code":"429006113","name":"马湾镇"},{"code":"429006114","name":"卢市镇"},{"code":"429006115","name":"小板镇"},{"code":"429006116","name":"九真镇"},{"code":"429006118","name":"皂市镇"},{"code":"429006119","name":"胡市镇"},{"code":"429006120","name":"石家河镇"},{"code":"429006121","name":"佛子山镇"},{"code":"429006201","name":"净潭乡"},{"code":"429006452","name":"沉湖管委会"}]},{"code":"429021","name":"神农架林区","list":[{"code":"429021100","name":"松柏镇"},{"code":"429021101","name":"阳日镇"},{"code":"429021102","name":"木鱼镇"},{"code":"429021103","name":"红坪镇"},{"code":"429021104","name":"新华镇"},{"code":"429021105","name":"九湖镇"},{"code":"429021201","name":"宋洛乡"},{"code":"429021203","name":"下谷坪土家族乡"}]}]},{"code":"430000","name":"湖南省","list":[{"code":"430100","name":"长沙市","list":[{"code":"430102","name":"芙蓉区"},{"code":"430103","name":"天心区"},{"code":"430104","name":"岳麓区"},{"code":"430105","name":"开福区"},{"code":"430111","name":"雨花区"},{"code":"430112","name":"望城区"},{"code":"430121","name":"长沙县"},{"code":"430181","name":"浏阳市"},{"code":"430182","name":"宁乡市"}]},{"code":"430200","name":"株洲市","list":[{"code":"430202","name":"荷塘区"},{"code":"430203","name":"芦淞区"},{"code":"430204","name":"石峰区"},{"code":"430211","name":"天元区"},{"code":"430212","name":"渌口区"},{"code":"430223","name":"攸县"},{"code":"430224","name":"茶陵县"},{"code":"430225","name":"炎陵县"},{"code":"430281","name":"醴陵市"}]},{"code":"430300","name":"湘潭市","list":[{"code":"430302","name":"雨湖区"},{"code":"430304","name":"岳塘区"},{"code":"430321","name":"湘潭县"},{"code":"430381","name":"湘乡市"},{"code":"430382","name":"韶山市"}]},{"code":"430400","name":"衡阳市","list":[{"code":"430405","name":"珠晖区"},{"code":"430406","name":"雁峰区"},{"code":"430407","name":"石鼓区"},{"code":"430408","name":"蒸湘区"},{"code":"430412","name":"南岳区"},{"code":"430421","name":"衡阳县"},{"code":"430422","name":"衡南县"},{"code":"430423","name":"衡山县"},{"code":"430424","name":"衡东县"},{"code":"430426","name":"祁东县"},{"code":"430481","name":"耒阳市"},{"code":"430482","name":"常宁市"}]},{"code":"430500","name":"邵阳市","list":[{"code":"430502","name":"双清区"},{"code":"430503","name":"大祥区"},{"code":"430511","name":"北塔区"},{"code":"430522","name":"新邵县"},{"code":"430523","name":"邵阳县"},{"code":"430524","name":"隆回县"},{"code":"430525","name":"洞口县"},{"code":"430527","name":"绥宁县"},{"code":"430528","name":"新宁县"},{"code":"430529","name":"城步苗族自治县"},{"code":"430581","name":"武冈市"},{"code":"430582","name":"邵东市"}]},{"code":"430600","name":"岳阳市","list":[{"code":"430602","name":"岳阳楼区"},{"code":"430603","name":"云溪区"},{"code":"430611","name":"君山区"},{"code":"430621","name":"岳阳县"},{"code":"430623","name":"华容县"},{"code":"430624","name":"湘阴县"},{"code":"430626","name":"平江县"},{"code":"430681","name":"汨罗市"},{"code":"430682","name":"临湘市"}]},{"code":"430700","name":"常德市","list":[{"code":"430702","name":"武陵区"},{"code":"430703","name":"鼎城区"},{"code":"430721","name":"安乡县"},{"code":"430722","name":"汉寿县"},{"code":"430723","name":"澧县"},{"code":"430724","name":"临澧县"},{"code":"430725","name":"桃源县"},{"code":"430726","name":"石门县"},{"code":"430781","name":"津市市"}]},{"code":"430800","name":"张家界市","list":[{"code":"430802","name":"永定区"},{"code":"430811","name":"武陵源区"},{"code":"430821","name":"慈利县"},{"code":"430822","name":"桑植县"}]},{"code":"430900","name":"益阳市","list":[{"code":"430902","name":"资阳区"},{"code":"430903","name":"赫山区"},{"code":"430921","name":"南县"},{"code":"430922","name":"桃江县"},{"code":"430923","name":"安化县"},{"code":"430981","name":"沅江市"}]},{"code":"431000","name":"郴州市","list":[{"code":"431002","name":"北湖区"},{"code":"431003","name":"苏仙区"},{"code":"431021","name":"桂阳县"},{"code":"431022","name":"宜章县"},{"code":"431023","name":"永兴县"},{"code":"431024","name":"嘉禾县"},{"code":"431025","name":"临武县"},{"code":"431026","name":"汝城县"},{"code":"431027","name":"桂东县"},{"code":"431028","name":"安仁县"},{"code":"431081","name":"资兴市"}]},{"code":"431100","name":"永州市","list":[{"code":"431102","name":"零陵区"},{"code":"431103","name":"冷水滩区"},{"code":"431122","name":"东安县"},{"code":"431123","name":"双牌县"},{"code":"431124","name":"道县"},{"code":"431125","name":"江永县"},{"code":"431126","name":"宁远县"},{"code":"431127","name":"蓝山县"},{"code":"431128","name":"新田县"},{"code":"431129","name":"江华瑶族自治县"},{"code":"431181","name":"祁阳市"}]},{"code":"431200","name":"怀化市","list":[{"code":"431202","name":"鹤城区"},{"code":"431221","name":"中方县"},{"code":"431222","name":"沅陵县"},{"code":"431223","name":"辰溪县"},{"code":"431224","name":"溆浦县"},{"code":"431225","name":"会同县"},{"code":"431226","name":"麻阳苗族自治县"},{"code":"431227","name":"新晃侗族自治县"},{"code":"431228","name":"芷江侗族自治县"},{"code":"431229","name":"靖州苗族侗族自治县"},{"code":"431230","name":"通道侗族自治县"},{"code":"431281","name":"洪江市"}]},{"code":"431300","name":"娄底市","list":[{"code":"431302","name":"娄星区"},{"code":"431321","name":"双峰县"},{"code":"431322","name":"新化县"},{"code":"431381","name":"冷水江市"},{"code":"431382","name":"涟源市"}]},{"code":"433100","name":"湘西土家族苗族自治州","list":[{"code":"433101","name":"吉首市"},{"code":"433122","name":"泸溪县"},{"code":"433123","name":"凤凰县"},{"code":"433124","name":"花垣县"},{"code":"433125","name":"保靖县"},{"code":"433126","name":"古丈县"},{"code":"433127","name":"永顺县"},{"code":"433130","name":"龙山县"}]}]},{"code":"440000","name":"广东省","list":[{"code":"440100","name":"广州市","list":[{"code":"440103","name":"荔湾区"},{"code":"440104","name":"越秀区"},{"code":"440105","name":"海珠区"},{"code":"440106","name":"天河区"},{"code":"440111","name":"白云区"},{"code":"440112","name":"黄埔区"},{"code":"440113","name":"番禺区"},{"code":"440114","name":"花都区"},{"code":"440115","name":"南沙区"},{"code":"440117","name":"从化区"},{"code":"440118","name":"增城区"}]},{"code":"440200","name":"韶关市","list":[{"code":"440203","name":"武江区"},{"code":"440204","name":"浈江区"},{"code":"440205","name":"曲江区"},{"code":"440222","name":"始兴县"},{"code":"440224","name":"仁化县"},{"code":"440229","name":"翁源县"},{"code":"440232","name":"乳源瑶族自治县"},{"code":"440233","name":"新丰县"},{"code":"440281","name":"乐昌市"},{"code":"440282","name":"南雄市"}]},{"code":"440300","name":"深圳市","list":[{"code":"440303","name":"罗湖区"},{"code":"440304","name":"福田区"},{"code":"440305","name":"南山区"},{"code":"440306","name":"宝安区"},{"code":"440307","name":"龙岗区"},{"code":"440308","name":"盐田区"},{"code":"440309","name":"龙华区"},{"code":"440310","name":"坪山区"},{"code":"440311","name":"光明区"}]},{"code":"440400","name":"珠海市","list":[{"code":"440402","name":"香洲区"},{"code":"440403","name":"斗门区"},{"code":"440404","name":"金湾区"}]},{"code":"440500","name":"汕头市","list":[{"code":"440507","name":"龙湖区"},{"code":"440511","name":"金平区"},{"code":"440512","name":"濠江区"},{"code":"440513","name":"潮阳区"},{"code":"440514","name":"潮南区"},{"code":"440515","name":"澄海区"},{"code":"440523","name":"南澳县"}]},{"code":"440600","name":"佛山市","list":[{"code":"440604","name":"禅城区"},{"code":"440605","name":"南海区"},{"code":"440606","name":"顺德区"},{"code":"440607","name":"三水区"},{"code":"440608","name":"高明区"}]},{"code":"440700","name":"江门市","list":[{"code":"440703","name":"蓬江区"},{"code":"440704","name":"江海区"},{"code":"440705","name":"新会区"},{"code":"440781","name":"台山市"},{"code":"440783","name":"开平市"},{"code":"440784","name":"鹤山市"},{"code":"440785","name":"恩平市"}]},{"code":"440800","name":"湛江市","list":[{"code":"440802","name":"赤坎区"},{"code":"440803","name":"霞山区"},{"code":"440804","name":"坡头区"},{"code":"440811","name":"麻章区"},{"code":"440823","name":"遂溪县"},{"code":"440825","name":"徐闻县"},{"code":"440881","name":"廉江市"},{"code":"440882","name":"雷州市"},{"code":"440883","name":"吴川市"}]},{"code":"440900","name":"茂名市","list":[{"code":"440902","name":"茂南区"},{"code":"440904","name":"电白区"},{"code":"440981","name":"高州市"},{"code":"440982","name":"化州市"},{"code":"440983","name":"信宜市"}]},{"code":"441200","name":"肇庆市","list":[{"code":"441202","name":"端州区"},{"code":"441203","name":"鼎湖区"},{"code":"441204","name":"高要区"},{"code":"441223","name":"广宁县"},{"code":"441224","name":"怀集县"},{"code":"441225","name":"封开县"},{"code":"441226","name":"德庆县"},{"code":"441284","name":"四会市"}]},{"code":"441300","name":"惠州市","list":[{"code":"441302","name":"惠城区"},{"code":"441303","name":"惠阳区"},{"code":"441322","name":"博罗县"},{"code":"441323","name":"惠东县"},{"code":"441324","name":"龙门县"}]},{"code":"441400","name":"梅州市","list":[{"code":"441402","name":"梅江区"},{"code":"441403","name":"梅县区"},{"code":"441422","name":"大埔县"},{"code":"441423","name":"丰顺县"},{"code":"441424","name":"五华县"},{"code":"441426","name":"平远县"},{"code":"441427","name":"蕉岭县"},{"code":"441481","name":"兴宁市"}]},{"code":"441500","name":"汕尾市","list":[{"code":"441502","name":"城区"},{"code":"441521","name":"海丰县"},{"code":"441523","name":"陆河县"},{"code":"441581","name":"陆丰市"}]},{"code":"441600","name":"河源市","list":[{"code":"441602","name":"源城区"},{"code":"441621","name":"紫金县"},{"code":"441622","name":"龙川县"},{"code":"441623","name":"连平县"},{"code":"441624","name":"和平县"},{"code":"441625","name":"东源县"}]},{"code":"441700","name":"阳江市","list":[{"code":"441702","name":"江城区"},{"code":"441704","name":"阳东区"},{"code":"441721","name":"阳西县"},{"code":"441781","name":"阳春市"}]},{"code":"441800","name":"清远市","list":[{"code":"441802","name":"清城区"},{"code":"441803","name":"清新区"},{"code":"441821","name":"佛冈县"},{"code":"441823","name":"阳山县"},{"code":"441825","name":"连山壮族瑶族自治县"},{"code":"441826","name":"连南瑶族自治县"},{"code":"441881","name":"英德市"},{"code":"441882","name":"连州市"}]},{"code":"441900","name":"东莞市","list":[{"code":"441900003","name":"东城街道"},{"code":"441900004","name":"南城街道"},{"code":"441900005","name":"万江街道"},{"code":"441900006","name":"莞城街道"},{"code":"441900101","name":"石碣镇"},{"code":"441900102","name":"石龙镇"},{"code":"441900103","name":"茶山镇"},{"code":"441900104","name":"石排镇"},{"code":"441900105","name":"企石镇"},{"code":"441900106","name":"横沥镇"},{"code":"441900107","name":"桥头镇"},{"code":"441900108","name":"谢岗镇"},{"code":"441900109","name":"东坑镇"},{"code":"441900110","name":"常平镇"},{"code":"441900111","name":"寮步镇"},{"code":"441900112","name":"樟木头镇"},{"code":"441900113","name":"大朗镇"},{"code":"441900114","name":"黄江镇"},{"code":"441900115","name":"清溪镇"},{"code":"441900116","name":"塘厦镇"},{"code":"441900117","name":"凤岗镇"},{"code":"441900118","name":"大岭山镇"},{"code":"441900119","name":"长安镇"},{"code":"441900121","name":"虎门镇"},{"code":"441900122","name":"厚街镇"},{"code":"441900123","name":"沙田镇"},{"code":"441900124","name":"道滘镇"},{"code":"441900125","name":"洪梅镇"},{"code":"441900126","name":"麻涌镇"},{"code":"441900127","name":"望牛墩镇"},{"code":"441900128","name":"中堂镇"},{"code":"441900129","name":"高埗镇"}]},{"code":"442000","name":"中山市","list":[{"code":"442000001","name":"石岐街道"},{"code":"442000002","name":"东区街道"},{"code":"442000003","name":"中山港街道"},{"code":"442000004","name":"西区街道"},{"code":"442000005","name":"南区街道"},{"code":"442000006","name":"五桂山街道"},{"code":"442000007","name":"民众街道"},{"code":"442000008","name":"南朗街道"},{"code":"442000101","name":"黄圃镇"},{"code":"442000103","name":"东凤镇"},{"code":"442000105","name":"古镇镇"},{"code":"442000106","name":"沙溪镇"},{"code":"442000107","name":"坦洲镇"},{"code":"442000108","name":"港口镇"},{"code":"442000109","name":"三角镇"},{"code":"442000110","name":"横栏镇"},{"code":"442000111","name":"南头镇"},{"code":"442000112","name":"阜沙镇"},{"code":"442000114","name":"三乡镇"},{"code":"442000115","name":"板芙镇"},{"code":"442000116","name":"大涌镇"},{"code":"442000117","name":"神湾镇"},{"code":"442000118","name":"小榄镇"}]},{"code":"445100","name":"潮州市","list":[{"code":"445102","name":"湘桥区"},{"code":"445103","name":"潮安区"},{"code":"445122","name":"饶平县"}]},{"code":"445200","name":"揭阳市","list":[{"code":"445202","name":"榕城区"},{"code":"445203","name":"揭东区"},{"code":"445222","name":"揭西县"},{"code":"445224","name":"惠来县"},{"code":"445281","name":"普宁市"}]},{"code":"445300","name":"云浮市","list":[{"code":"445302","name":"云城区"},{"code":"445303","name":"云安区"},{"code":"445321","name":"新兴县"},{"code":"445322","name":"郁南县"},{"code":"445381","name":"罗定市"}]}]},{"code":"450000","name":"广西壮族自治区","list":[{"code":"450100","name":"南宁市","list":[{"code":"450102","name":"兴宁区"},{"code":"450103","name":"青秀区"},{"code":"450105","name":"江南区"},{"code":"450107","name":"西乡塘区"},{"code":"450108","name":"良庆区"},{"code":"450109","name":"邕宁区"},{"code":"450110","name":"武鸣区"},{"code":"450123","name":"隆安县"},{"code":"450124","name":"马山县"},{"code":"450125","name":"上林县"},{"code":"450126","name":"宾阳县"},{"code":"450181","name":"横州市"}]},{"code":"450200","name":"柳州市","list":[{"code":"450202","name":"城中区"},{"code":"450203","name":"鱼峰区"},{"code":"450204","name":"柳南区"},{"code":"450205","name":"柳北区"},{"code":"450206","name":"柳江区"},{"code":"450222","name":"柳城县"},{"code":"450223","name":"鹿寨县"},{"code":"450224","name":"融安县"},{"code":"450225","name":"融水苗族自治县"},{"code":"450226","name":"三江侗族自治县"}]},{"code":"450300","name":"桂林市","list":[{"code":"450302","name":"秀峰区"},{"code":"450303","name":"叠彩区"},{"code":"450304","name":"象山区"},{"code":"450305","name":"七星区"},{"code":"450311","name":"雁山区"},{"code":"450312","name":"临桂区"},{"code":"450321","name":"阳朔县"},{"code":"450323","name":"灵川县"},{"code":"450324","name":"全州县"},{"code":"450325","name":"兴安县"},{"code":"450326","name":"永福县"},{"code":"450327","name":"灌阳县"},{"code":"450328","name":"龙胜各族自治县"},{"code":"450329","name":"资源县"},{"code":"450330","name":"平乐县"},{"code":"450332","name":"恭城瑶族自治县"},{"code":"450381","name":"荔浦市"}]},{"code":"450400","name":"梧州市","list":[{"code":"450403","name":"万秀区"},{"code":"450405","name":"长洲区"},{"code":"450406","name":"龙圩区"},{"code":"450421","name":"苍梧县"},{"code":"450422","name":"藤县"},{"code":"450423","name":"蒙山县"},{"code":"450481","name":"岑溪市"}]},{"code":"450500","name":"北海市","list":[{"code":"450502","name":"海城区"},{"code":"450503","name":"银海区"},{"code":"450512","name":"铁山港区"},{"code":"450521","name":"合浦县"}]},{"code":"450600","name":"防城港市","list":[{"code":"450602","name":"港口区"},{"code":"450603","name":"防城区"},{"code":"450621","name":"上思县"},{"code":"450681","name":"东兴市"}]},{"code":"450700","name":"钦州市","list":[{"code":"450702","name":"钦南区"},{"code":"450703","name":"钦北区"},{"code":"450721","name":"灵山县"},{"code":"450722","name":"浦北县"}]},{"code":"450800","name":"贵港市","list":[{"code":"450802","name":"港北区"},{"code":"450803","name":"港南区"},{"code":"450804","name":"覃塘区"},{"code":"450821","name":"平南县"},{"code":"450881","name":"桂平市"}]},{"code":"450900","name":"玉林市","list":[{"code":"450902","name":"玉州区"},{"code":"450903","name":"福绵区"},{"code":"450921","name":"容县"},{"code":"450922","name":"陆川县"},{"code":"450923","name":"博白县"},{"code":"450924","name":"兴业县"},{"code":"450981","name":"北流市"}]},{"code":"451000","name":"百色市","list":[{"code":"451002","name":"右江区"},{"code":"451003","name":"田阳区"},{"code":"451022","name":"田东县"},{"code":"451024","name":"德保县"},{"code":"451026","name":"那坡县"},{"code":"451027","name":"凌云县"},{"code":"451028","name":"乐业县"},{"code":"451029","name":"田林县"},{"code":"451030","name":"西林县"},{"code":"451031","name":"隆林各族自治县"},{"code":"451081","name":"靖西市"},{"code":"451082","name":"平果市"}]},{"code":"451100","name":"贺州市","list":[{"code":"451102","name":"八步区"},{"code":"451103","name":"平桂区"},{"code":"451121","name":"昭平县"},{"code":"451122","name":"钟山县"},{"code":"451123","name":"富川瑶族自治县"}]},{"code":"451200","name":"河池市","list":[{"code":"451202","name":"金城江区"},{"code":"451203","name":"宜州区"},{"code":"451221","name":"南丹县"},{"code":"451222","name":"天峨县"},{"code":"451223","name":"凤山县"},{"code":"451224","name":"东兰县"},{"code":"451225","name":"罗城仫佬族自治县"},{"code":"451226","name":"环江毛南族自治县"},{"code":"451227","name":"巴马瑶族自治县"},{"code":"451228","name":"都安瑶族自治县"},{"code":"451229","name":"大化瑶族自治县"}]},{"code":"451300","name":"来宾市","list":[{"code":"451302","name":"兴宾区"},{"code":"451321","name":"忻城县"},{"code":"451322","name":"象州县"},{"code":"451323","name":"武宣县"},{"code":"451324","name":"金秀瑶族自治县"},{"code":"451381","name":"合山市"}]},{"code":"451400","name":"崇左市","list":[{"code":"451402","name":"江州区"},{"code":"451421","name":"扶绥县"},{"code":"451422","name":"宁明县"},{"code":"451423","name":"龙州县"},{"code":"451424","name":"大新县"},{"code":"451425","name":"天等县"},{"code":"451481","name":"凭祥市"}]}]},{"code":"460000","name":"海南省","list":[{"code":"460100","name":"海口市","list":[{"code":"460105","name":"秀英区"},{"code":"460106","name":"龙华区"},{"code":"460107","name":"琼山区"},{"code":"460108","name":"美兰区"}]},{"code":"460200","name":"三亚市","list":[{"code":"460202","name":"海棠区"},{"code":"460203","name":"吉阳区"},{"code":"460204","name":"天涯区"},{"code":"460205","name":"崖州区"}]},{"code":"460300","name":"三沙市","list":[{"code":"460300619","name":"滨湄滩"},{"code":"460300620","name":"玉琢礁"},{"code":"460300621","name":"盘石屿"},{"code":"460300622","name":"羚羊礁"},{"code":"460300624","name":"全富岛"},{"code":"460300625","name":"银屿"},{"code":"460300626","name":"排洪滩"},{"code":"460300627","name":"波洑暗沙"},{"code":"460300628","name":"美溪暗沙"},{"code":"460300629","name":"海鸠暗沙"},{"code":"460300630","name":"中北暗沙"},{"code":"460300631","name":"漫步暗沙"},{"code":"460300632","name":"永兴岛"},{"code":"460300633","name":"浪花礁"},{"code":"460300634","name":"隐矶滩"},{"code":"460300635","name":"比微暗沙"},{"code":"460300636","name":"东岛"},{"code":"460300637","name":"湛涵滩"},{"code":"460300638","name":"华光礁"},{"code":"460300639","name":"中建岛"},{"code":"460300640","name":"金银岛"},{"code":"460300641","name":"甘泉岛"},{"code":"460300642","name":"北礁"},{"code":"460300643","name":"布德暗沙"},{"code":"460300644","name":"指掌暗沙"},{"code":"460300645","name":"鲁班暗沙"},{"code":"460300646","name":"美滨暗沙"},{"code":"460300647","name":"本固暗沙"},{"code":"460300648","name":"西门暗沙"},{"code":"460300649","name":"控湃暗沙"},{"code":"460300650","name":"涛静暗沙"},{"code":"460300651","name":"果淀暗沙"},{"code":"460300652","name":"排波暗沙"},{"code":"460300653","name":"石塘暗沙"},{"code":"460300654","name":"武勇暗沙"},{"code":"460300655","name":"安定连礁"},{"code":"460300656","name":"华夏暗沙"},{"code":"460300657","name":"济猛暗沙"},{"code":"460300658","name":"南扉暗沙"},{"code":"460300659","name":"屏南暗沙"},{"code":"460300660","name":"乐西暗沙"},{"code":"460300661","name":"黄岩岛(民主礁)"},{"code":"460300662","name":"石屿"},{"code":"460300663","name":"七连屿"},{"code":"460300664","name":"小现礁"},{"code":"460300665","name":"永南暗沙"},{"code":"460300666","name":"神狐暗沙"},{"code":"460300667","name":"咸舍屿"},{"code":"460300668","name":"筐仔沙洲"},{"code":"460300669","name":"红草门"},{"code":"460300670","name":"银砾滩"},{"code":"460300671","name":"北边廊"},{"code":"460300672","name":"高尖石"},{"code":"460300673","name":"西渡滩"},{"code":"460300674","name":"嵩焘滩"},{"code":"460300675","name":"鸭公岛"},{"code":"460300676","name":"宪法暗沙"},{"code":"460300677","name":"一统暗沙"},{"code":"460300678","name":"中南暗沙"},{"code":"460300679","name":"珊瑚东暗沙"},{"code":"460300683","name":"彬礁"},{"code":"460300684","name":"南方浅滩"},{"code":"460300685","name":"忠孝滩"},{"code":"460300686","name":"勇士滩"},{"code":"460300687","name":"海马滩"},{"code":"460300691","name":"火星礁"},{"code":"460300692","name":"和平暗沙"},{"code":"460300693","name":"大渊滩"},{"code":"460300694","name":"安塘滩"},{"code":"460300695","name":"马欢岛"},{"code":"460300696","name":"费信岛"},{"code":"460300697","name":"五方西"},{"code":"460300698","name":"五方北"},{"code":"460300699","name":"五方头"},{"code":"460300700","name":"五方尾"},{"code":"460300701","name":"五方南"},{"code":"460300702","name":"南安礁"},{"code":"460300703","name":"康西暗沙"},{"code":"460300704","name":"北安礁"},{"code":"460300705","name":"北康暗沙"},{"code":"460300706","name":"法显暗沙"},{"code":"460300707","name":"盟谊暗沙"},{"code":"460300708","name":"南通礁"},{"code":"460300709","name":"海宁礁"},{"code":"460300710","name":"琼台礁"},{"code":"460300711","name":"海安礁"},{"code":"460300712","name":"谭门礁"},{"code":"460300713","name":"隐波暗沙"},{"code":"460300714","name":"南康暗沙"},{"code":"460300715","name":"欢乐暗沙"},{"code":"460300716","name":"紫滩"},{"code":"460300717","name":"浔江暗沙"},{"code":"460300718","name":"半路礁"},{"code":"460300719","name":"日积礁"},{"code":"460300720","name":"南威岛"},{"code":"460300721","name":"中礁"},{"code":"460300722","name":"东礁"},{"code":"460300723","name":"华阳礁"},{"code":"460300724","name":"永暑礁"},{"code":"460300725","name":"毕生礁"},{"code":"460300726","name":"石盘仔"},{"code":"460300727","name":"奥南暗沙"},{"code":"460300728","name":"蓬勃堡"},{"code":"460300729","name":"金盾暗沙"},{"code":"460300730","name":"常骏暗沙"},{"code":"460300731","name":"南薇滩"},{"code":"460300732","name":"安波沙洲"},{"code":"460300733","name":"鸟鱼锭石"},{"code":"460300734","name":"光星礁"},{"code":"460300735","name":"光星仔礁"},{"code":"460300736","name":"弹丸礁"},{"code":"460300737","name":"安渡滩"},{"code":"460300738","name":"破浪礁"},{"code":"460300739","name":"南海礁"},{"code":"460300740","name":"簸箕礁"},{"code":"460300741","name":"榆亚暗沙"},{"code":"460300742","name":"六门礁"},{"code":"460300743","name":"南华礁"},{"code":"460300744","name":"无乜礁"},{"code":"460300745","name":"司令礁"},{"code":"460300746","name":"南乐暗沙"},{"code":"460300747","name":"半月礁"},{"code":"460300748","name":"舰长礁"},{"code":"460300749","name":"皇路礁"},{"code":"460300750","name":"曾母暗沙"},{"code":"460300751","name":"南屏礁"},{"code":"460300752","name":"永登暗沙"},{"code":"460300763","name":"西礁"},{"code":"460300764","name":"碎浪暗沙"},{"code":"460300765","name":"保卫暗沙"},{"code":"460300766","name":"普宁暗沙"},{"code":"460300767","name":"金吾暗沙"},{"code":"460300768","name":"都护暗沙"},{"code":"460300769","name":"朱应滩"},{"code":"460300770","name":"李准滩"},{"code":"460300771","name":"人骏滩"},{"code":"460300772","name":"广雅滩"},{"code":"460300773","name":"奥援暗沙"},{"code":"460300774","name":"隐遁暗沙"},{"code":"460300775","name":"尹庆群礁"},{"code":"460300776","name":"康泰滩"},{"code":"460300777","name":"玉诺礁"},{"code":"460300778","name":"校尉暗沙"},{"code":"460300779","name":"双礁"},{"code":"460300780","name":"指向礁"},{"code":"460300781","name":"南华水道"},{"code":"460300782","name":"石龙岩"},{"code":"460300783","name":"立新礁"},{"code":"460300784","name":"红石暗沙"},{"code":"460300785","name":"郑和群礁"},{"code":"460300786","name":"北恒礁"},{"code":"460300787","name":"恒礁"},{"code":"460300788","name":"莪兰暗沙"},{"code":"460300789","name":"泛爱暗沙"},{"code":"460300790","name":"孔明礁"},{"code":"460300791","name":"伏波礁"},{"code":"460300792","name":"海康暗沙"},{"code":"460300793","name":"康乐礁"},{"code":"460300794","name":"息波礁"},{"code":"460300795","name":"神仙暗沙"},{"code":"460300796","name":"仙后滩"},{"code":"460300797","name":"逍遥暗沙"},{"code":"460300798","name":"义净礁"},{"code":"460300799","name":"道明群礁"},{"code":"460300800","name":"九章群礁"},{"code":"460300804","name":"澄平礁"},{"code":"460300805","name":"双子群礁"},{"code":"460300806","name":"乐斯暗沙"},{"code":"460300807","name":"铁峙礁"},{"code":"460300808","name":"梅九礁"},{"code":"460300809","name":"铁线礁"},{"code":"460300810","name":"渚碧礁"},{"code":"460300811","name":"双黄沙洲"},{"code":"460300812","name":"库归礁"},{"code":"460300813","name":"西月岛"},{"code":"460300814","name":"长滩"},{"code":"460300815","name":"火艾礁"},{"code":"460300816","name":"南薰礁"},{"code":"460300817","name":"小南薰礁"},{"code":"460300818","name":"鸿庥岛"},{"code":"460300819","name":"安达礁"},{"code":"460300820","name":"舶兰礁"},{"code":"460300821","name":"安乐礁"},{"code":"460300822","name":"长线礁"},{"code":"460300823","name":"主权礁"},{"code":"460300824","name":"牛轭礁"},{"code":"460300825","name":"染青东礁"},{"code":"460300826","name":"染青沙洲"},{"code":"460300827","name":"龙虾礁"},{"code":"460300828","name":"扁参礁"},{"code":"460300829","name":"漳溪礁"},{"code":"460300830","name":"屈原礁"},{"code":"460300831","name":"琼礁"},{"code":"460300832","name":"赤瓜礁"},{"code":"460300833","name":"鬼喊礁"},{"code":"460300834","name":"华礁"},{"code":"460300835","name":"吉阳礁"},{"code":"460300836","name":"东门礁"},{"code":"460300837","name":"西门礁"},{"code":"460300838","name":"景宏岛"},{"code":"460300839","name":"南门礁"},{"code":"460300840","name":"大现礁"},{"code":"460300841","name":"福禄寺礁"},{"code":"460300842","name":"太平岛"},{"code":"460300843","name":"敦谦沙洲"},{"code":"460300844","name":"三角礁"},{"code":"460300845","name":"禄沙礁"},{"code":"460300846","name":"美济礁"},{"code":"460300847","name":"仁爱礁"},{"code":"460300848","name":"牛车轮礁"},{"code":"460300849","name":"仙宾礁"},{"code":"460300850","name":"钟山礁"},{"code":"460300851","name":"片礁"},{"code":"460300852","name":"信义礁"},{"code":"460300853","name":"海口礁"},{"code":"460300854","name":"乙辛石"},{"code":"460300855","name":"仙娥礁"},{"code":"460300856","name":"西卫滩"},{"code":"460300857","name":"万安滩"}]},{"code":"460400","name":"儋州市","list":[{"code":"460400001","name":"三都街道"},{"code":"460400100","name":"那大镇"},{"code":"460400101","name":"和庆镇"},{"code":"460400102","name":"南丰镇"},{"code":"460400103","name":"大成镇"},{"code":"460400104","name":"雅星镇"},{"code":"460400105","name":"兰洋镇"},{"code":"460400106","name":"光村镇"},{"code":"460400107","name":"木棠镇"},{"code":"460400108","name":"海头镇"},{"code":"460400109","name":"峨蔓镇"},{"code":"460400111","name":"王五镇"},{"code":"460400112","name":"白马井镇"},{"code":"460400113","name":"中和镇"},{"code":"460400114","name":"排浦镇"},{"code":"460400115","name":"东成镇"},{"code":"460400116","name":"新州镇"},{"code":"460400400","name":"西培农场"},{"code":"460400404","name":"西联农场"},{"code":"460400405","name":"蓝洋农场"},{"code":"460400407","name":"八一农场"}]},{"code":"469001","name":"五指山市","list":[{"code":"469001100","name":"通什镇"},{"code":"469001101","name":"南圣镇"},{"code":"469001102","name":"毛阳镇"},{"code":"469001103","name":"番阳镇"},{"code":"469001200","name":"畅好乡"},{"code":"469001201","name":"毛道乡"},{"code":"469001202","name":"水满乡"}]},{"code":"469002","name":"琼海市","list":[{"code":"469002100","name":"嘉积镇"},{"code":"469002101","name":"万泉镇"},{"code":"469002102","name":"石壁镇"},{"code":"469002103","name":"中原镇"},{"code":"469002104","name":"博鳌镇"},{"code":"469002105","name":"阳江镇"},{"code":"469002106","name":"龙江镇"},{"code":"469002107","name":"潭门镇"},{"code":"469002108","name":"塔洋镇"},{"code":"469002109","name":"长坡镇"},{"code":"469002110","name":"大路镇"},{"code":"469002111","name":"会山镇"},{"code":"469002400","name":"东太农场"},{"code":"469002402","name":"东红农场"},{"code":"469002403","name":"东升农场"}]},{"code":"469005","name":"文昌市","list":[{"code":"469005100","name":"文城镇"},{"code":"469005101","name":"重兴镇"},{"code":"469005102","name":"蓬莱镇"},{"code":"469005103","name":"会文镇"},{"code":"469005104","name":"东路镇"},{"code":"469005105","name":"潭牛镇"},{"code":"469005106","name":"东阁镇"},{"code":"469005107","name":"文教镇"},{"code":"469005108","name":"东郊镇"},{"code":"469005109","name":"龙楼镇"},{"code":"469005110","name":"昌洒镇"},{"code":"469005111","name":"翁田镇"},{"code":"469005112","name":"抱罗镇"},{"code":"469005113","name":"冯坡镇"},{"code":"469005114","name":"锦山镇"},{"code":"469005115","name":"铺前镇"},{"code":"469005116","name":"公坡镇"},{"code":"469005400","name":"东路农场"},{"code":"469005401","name":"南阳农场"},{"code":"469005402","name":"国营罗豆农场"}]},{"code":"469006","name":"万宁市","list":[{"code":"469006100","name":"万城镇"},{"code":"469006101","name":"龙滚镇"},{"code":"469006102","name":"和乐镇"},{"code":"469006103","name":"后安镇"},{"code":"469006104","name":"大茂镇"},{"code":"469006105","name":"东澳镇"},{"code":"469006106","name":"礼纪镇"},{"code":"469006107","name":"长丰镇"},{"code":"469006108","name":"山根镇"},{"code":"469006109","name":"北大镇"},{"code":"469006110","name":"南桥镇"},{"code":"469006111","name":"三更罗镇"},{"code":"469006400","name":"东兴农场"},{"code":"469006401","name":"东和农场"},{"code":"469006402","name":"兴隆华侨农场"},{"code":"469006403","name":"六连林场"}]},{"code":"469007","name":"东方市","list":[{"code":"469007100","name":"八所镇"},{"code":"469007101","name":"东河镇"},{"code":"469007102","name":"大田镇"},{"code":"469007103","name":"感城镇"},{"code":"469007104","name":"板桥镇"},{"code":"469007105","name":"三家镇"},{"code":"469007106","name":"四更镇"},{"code":"469007107","name":"新龙镇"},{"code":"469007200","name":"天安乡"},{"code":"469007201","name":"江边乡"},{"code":"469007400","name":"广坝农场"},{"code":"469007401","name":"东方华侨农场"}]},{"code":"469021","name":"定安县","list":[{"code":"469021100","name":"定城镇"},{"code":"469021101","name":"新竹镇"},{"code":"469021102","name":"龙湖镇"},{"code":"469021103","name":"黄竹镇"},{"code":"469021104","name":"雷鸣镇"},{"code":"469021105","name":"龙门镇"},{"code":"469021106","name":"龙河镇"},{"code":"469021107","name":"岭口镇"},{"code":"469021108","name":"翰林镇"},{"code":"469021109","name":"富文镇"},{"code":"469021400","name":"中瑞农场"},{"code":"469021401","name":"南海农场"},{"code":"469021402","name":"金鸡岭农场"}]},{"code":"469022","name":"屯昌县","list":[{"code":"469022100","name":"屯城镇"},{"code":"469022101","name":"新兴镇"},{"code":"469022102","name":"枫木镇"},{"code":"469022103","name":"乌坡镇"},{"code":"469022104","name":"南吕镇"},{"code":"469022105","name":"南坤镇"},{"code":"469022106","name":"坡心镇"},{"code":"469022107","name":"西昌镇"}]},{"code":"469023","name":"澄迈县","list":[{"code":"469023100","name":"金江镇"},{"code":"469023101","name":"老城镇"},{"code":"469023102","name":"瑞溪镇"},{"code":"469023103","name":"永发镇"},{"code":"469023104","name":"加乐镇"},{"code":"469023105","name":"文儒镇"},{"code":"469023106","name":"中兴镇"},{"code":"469023107","name":"仁兴镇"},{"code":"469023108","name":"福山镇"},{"code":"469023109","name":"桥头镇"},{"code":"469023110","name":"大丰镇"},{"code":"469023400","name":"红光农场"},{"code":"469023402","name":"西达农场"},{"code":"469023405","name":"国营金安农场"}]},{"code":"469024","name":"临高县","list":[{"code":"469024100","name":"临城镇"},{"code":"469024101","name":"波莲镇"},{"code":"469024102","name":"东英镇"},{"code":"469024103","name":"博厚镇"},{"code":"469024104","name":"皇桐镇"},{"code":"469024105","name":"多文镇"},{"code":"469024106","name":"和舍镇"},{"code":"469024107","name":"南宝镇"},{"code":"469024108","name":"新盈镇"},{"code":"469024109","name":"调楼镇"},{"code":"469024110","name":"加来镇"}]},{"code":"469025","name":"白沙黎族自治县","list":[{"code":"469025100","name":"牙叉镇"},{"code":"469025101","name":"七坊镇"},{"code":"469025102","name":"邦溪镇"},{"code":"469025103","name":"打安镇"},{"code":"469025200","name":"细水乡"},{"code":"469025201","name":"元门乡"},{"code":"469025202","name":"南开乡"},{"code":"469025203","name":"阜龙乡"},{"code":"469025204","name":"青松乡"},{"code":"469025205","name":"金波乡"},{"code":"469025206","name":"荣邦乡"},{"code":"469025401","name":"白沙农场"},{"code":"469025404","name":"龙江农场"},{"code":"469025408","name":"邦溪农场"}]},{"code":"469026","name":"昌江黎族自治县","list":[{"code":"469026100","name":"石碌镇"},{"code":"469026101","name":"叉河镇"},{"code":"469026102","name":"十月田镇"},{"code":"469026103","name":"乌烈镇"},{"code":"469026104","name":"昌化镇"},{"code":"469026105","name":"海尾镇"},{"code":"469026106","name":"七叉镇"},{"code":"469026200","name":"王下乡"}]},{"code":"469027","name":"乐东黎族自治县","list":[{"code":"469027100","name":"抱由镇"},{"code":"469027101","name":"万冲镇"},{"code":"469027102","name":"大安镇"},{"code":"469027103","name":"志仲镇"},{"code":"469027104","name":"千家镇"},{"code":"469027105","name":"九所镇"},{"code":"469027106","name":"利国镇"},{"code":"469027107","name":"黄流镇"},{"code":"469027108","name":"佛罗镇"},{"code":"469027109","name":"尖峰镇"},{"code":"469027110","name":"莺歌海镇"},{"code":"469027401","name":"山荣农场"},{"code":"469027402","name":"乐光农场"},{"code":"469027405","name":"保国农场"}]},{"code":"469028","name":"陵水黎族自治县","list":[{"code":"469028100","name":"椰林镇"},{"code":"469028101","name":"光坡镇"},{"code":"469028102","name":"三才镇"},{"code":"469028103","name":"英州镇"},{"code":"469028104","name":"隆广镇"},{"code":"469028105","name":"文罗镇"},{"code":"469028106","name":"本号镇"},{"code":"469028107","name":"新村镇"},{"code":"469028108","name":"黎安镇"},{"code":"469028200","name":"提蒙乡"},{"code":"469028201","name":"群英乡"},{"code":"469028400","name":"岭门农场"},{"code":"469028401","name":"南平农场"}]},{"code":"469029","name":"保亭黎族苗族自治县","list":[{"code":"469029100","name":"保城镇"},{"code":"469029101","name":"什玲镇"},{"code":"469029102","name":"加茂镇"},{"code":"469029103","name":"响水镇"},{"code":"469029104","name":"新政镇"},{"code":"469029105","name":"三道镇"},{"code":"469029200","name":"六弓乡"},{"code":"469029201","name":"南林乡"},{"code":"469029202","name":"毛感乡"},{"code":"469029401","name":"新星农场"},{"code":"469029403","name":"金江农场"},{"code":"469029405","name":"三道农场"}]},{"code":"469030","name":"琼中黎族苗族自治县","list":[{"code":"469030100","name":"营根镇"},{"code":"469030101","name":"湾岭镇"},{"code":"469030102","name":"黎母山镇"},{"code":"469030103","name":"和平镇"},{"code":"469030104","name":"长征镇"},{"code":"469030105","name":"红毛镇"},{"code":"469030106","name":"中平镇"},{"code":"469030200","name":"吊罗山乡"},{"code":"469030201","name":"上安乡"},{"code":"469030202","name":"什运乡"},{"code":"469030406","name":"加钗农场"},{"code":"469030407","name":"长征农场"}]}]},{"code":"500000","name":"重庆市","list":[{"code":"500100","name":"重庆市","list":[{"code":"500101","name":"万州区"},{"code":"500102","name":"涪陵区"},{"code":"500103","name":"渝中区"},{"code":"500104","name":"大渡口区"},{"code":"500105","name":"江北区"},{"code":"500106","name":"沙坪坝区"},{"code":"500107","name":"九龙坡区"},{"code":"500108","name":"南岸区"},{"code":"500109","name":"北碚区"},{"code":"500110","name":"綦江区"},{"code":"500111","name":"大足区"},{"code":"500112","name":"渝北区"},{"code":"500113","name":"巴南区"},{"code":"500114","name":"黔江区"},{"code":"500115","name":"长寿区"},{"code":"500116","name":"江津区"},{"code":"500117","name":"合川区"},{"code":"500118","name":"永川区"},{"code":"500119","name":"南川区"},{"code":"500120","name":"璧山区"},{"code":"500151","name":"铜梁区"},{"code":"500152","name":"潼南区"},{"code":"500153","name":"荣昌区"},{"code":"500154","name":"开州区"},{"code":"500155","name":"梁平区"},{"code":"500156","name":"武隆区"}]},{"code":"500200","name":"重庆市","list":[{"code":"500229","name":"城口县"},{"code":"500230","name":"丰都县"},{"code":"500231","name":"垫江县"},{"code":"500233","name":"忠县"},{"code":"500235","name":"云阳县"},{"code":"500236","name":"奉节县"},{"code":"500237","name":"巫山县"},{"code":"500238","name":"巫溪县"},{"code":"500240","name":"石柱土家族自治县"},{"code":"500241","name":"秀山土家族苗族自治县"},{"code":"500242","name":"酉阳土家族苗族自治县"},{"code":"500243","name":"彭水苗族土家族自治县"}]}]},{"code":"510000","name":"四川省","list":[{"code":"510100","name":"成都市","list":[{"code":"510104","name":"锦江区"},{"code":"510105","name":"青羊区"},{"code":"510106","name":"金牛区"},{"code":"510107","name":"武侯区"},{"code":"510108","name":"成华区"},{"code":"510112","name":"龙泉驿区"},{"code":"510113","name":"青白江区"},{"code":"510114","name":"新都区"},{"code":"510115","name":"温江区"},{"code":"510116","name":"双流区"},{"code":"510117","name":"郫都区"},{"code":"510118","name":"新津区"},{"code":"510121","name":"金堂县"},{"code":"510129","name":"大邑县"},{"code":"510131","name":"蒲江县"},{"code":"510181","name":"都江堰市"},{"code":"510182","name":"彭州市"},{"code":"510183","name":"邛崃市"},{"code":"510184","name":"崇州市"},{"code":"510185","name":"简阳市"}]},{"code":"510300","name":"自贡市","list":[{"code":"510302","name":"自流井区"},{"code":"510303","name":"贡井区"},{"code":"510304","name":"大安区"},{"code":"510311","name":"沿滩区"},{"code":"510321","name":"荣县"},{"code":"510322","name":"富顺县"}]},{"code":"510400","name":"攀枝花市","list":[{"code":"510402","name":"东区"},{"code":"510403","name":"西区"},{"code":"510411","name":"仁和区"},{"code":"510421","name":"米易县"},{"code":"510422","name":"盐边县"}]},{"code":"510500","name":"泸州市","list":[{"code":"510502","name":"江阳区"},{"code":"510503","name":"纳溪区"},{"code":"510504","name":"龙马潭区"},{"code":"510521","name":"泸县"},{"code":"510522","name":"合江县"},{"code":"510524","name":"叙永县"},{"code":"510525","name":"古蔺县"}]},{"code":"510600","name":"德阳市","list":[{"code":"510603","name":"旌阳区"},{"code":"510604","name":"罗江区"},{"code":"510623","name":"中江县"},{"code":"510681","name":"广汉市"},{"code":"510682","name":"什邡市"},{"code":"510683","name":"绵竹市"}]},{"code":"510700","name":"绵阳市","list":[{"code":"510703","name":"涪城区"},{"code":"510704","name":"游仙区"},{"code":"510705","name":"安州区"},{"code":"510722","name":"三台县"},{"code":"510723","name":"盐亭县"},{"code":"510725","name":"梓潼县"},{"code":"510726","name":"北川羌族自治县"},{"code":"510727","name":"平武县"},{"code":"510781","name":"江油市"}]},{"code":"510800","name":"广元市","list":[{"code":"510802","name":"利州区"},{"code":"510811","name":"昭化区"},{"code":"510812","name":"朝天区"},{"code":"510821","name":"旺苍县"},{"code":"510822","name":"青川县"},{"code":"510823","name":"剑阁县"},{"code":"510824","name":"苍溪县"}]},{"code":"510900","name":"遂宁市","list":[{"code":"510903","name":"船山区"},{"code":"510904","name":"安居区"},{"code":"510921","name":"蓬溪县"},{"code":"510923","name":"大英县"},{"code":"510981","name":"射洪市"}]},{"code":"511000","name":"内江市","list":[{"code":"511002","name":"市中区"},{"code":"511011","name":"东兴区"},{"code":"511024","name":"威远县"},{"code":"511025","name":"资中县"},{"code":"511083","name":"隆昌市"}]},{"code":"511100","name":"乐山市","list":[{"code":"511102","name":"市中区"},{"code":"511111","name":"沙湾区"},{"code":"511112","name":"五通桥区"},{"code":"511113","name":"金口河区"},{"code":"511123","name":"犍为县"},{"code":"511124","name":"井研县"},{"code":"511126","name":"夹江县"},{"code":"511129","name":"沐川县"},{"code":"511132","name":"峨边彝族自治县"},{"code":"511133","name":"马边彝族自治县"},{"code":"511181","name":"峨眉山市"}]},{"code":"511300","name":"南充市","list":[{"code":"511302","name":"顺庆区"},{"code":"511303","name":"高坪区"},{"code":"511304","name":"嘉陵区"},{"code":"511321","name":"南部县"},{"code":"511322","name":"营山县"},{"code":"511323","name":"蓬安县"},{"code":"511324","name":"仪陇县"},{"code":"511325","name":"西充县"},{"code":"511381","name":"阆中市"}]},{"code":"511400","name":"眉山市","list":[{"code":"511402","name":"东坡区"},{"code":"511403","name":"彭山区"},{"code":"511421","name":"仁寿县"},{"code":"511423","name":"洪雅县"},{"code":"511424","name":"丹棱县"},{"code":"511425","name":"青神县"}]},{"code":"511500","name":"宜宾市","list":[{"code":"511502","name":"翠屏区"},{"code":"511503","name":"南溪区"},{"code":"511504","name":"叙州区"},{"code":"511523","name":"江安县"},{"code":"511524","name":"长宁县"},{"code":"511525","name":"高县"},{"code":"511526","name":"珙县"},{"code":"511527","name":"筠连县"},{"code":"511528","name":"兴文县"},{"code":"511529","name":"屏山县"}]},{"code":"511600","name":"广安市","list":[{"code":"511602","name":"广安区"},{"code":"511603","name":"前锋区"},{"code":"511621","name":"岳池县"},{"code":"511622","name":"武胜县"},{"code":"511623","name":"邻水县"},{"code":"511681","name":"华蓥市"}]},{"code":"511700","name":"达州市","list":[{"code":"511702","name":"通川区"},{"code":"511703","name":"达川区"},{"code":"511722","name":"宣汉县"},{"code":"511723","name":"开江县"},{"code":"511724","name":"大竹县"},{"code":"511725","name":"渠县"},{"code":"511781","name":"万源市"}]},{"code":"511800","name":"雅安市","list":[{"code":"511802","name":"雨城区"},{"code":"511803","name":"名山区"},{"code":"511822","name":"荥经县"},{"code":"511823","name":"汉源县"},{"code":"511824","name":"石棉县"},{"code":"511825","name":"天全县"},{"code":"511826","name":"芦山县"},{"code":"511827","name":"宝兴县"}]},{"code":"511900","name":"巴中市","list":[{"code":"511902","name":"巴州区"},{"code":"511903","name":"恩阳区"},{"code":"511921","name":"通江县"},{"code":"511922","name":"南江县"},{"code":"511923","name":"平昌县"}]},{"code":"512000","name":"资阳市","list":[{"code":"512002","name":"雁江区"},{"code":"512021","name":"安岳县"},{"code":"512022","name":"乐至县"}]},{"code":"513200","name":"阿坝藏族羌族自治州","list":[{"code":"513201","name":"马尔康市"},{"code":"513221","name":"汶川县"},{"code":"513222","name":"理县"},{"code":"513223","name":"茂县"},{"code":"513224","name":"松潘县"},{"code":"513225","name":"九寨沟县"},{"code":"513226","name":"金川县"},{"code":"513227","name":"小金县"},{"code":"513228","name":"黑水县"},{"code":"513230","name":"壤塘县"},{"code":"513231","name":"阿坝县"},{"code":"513232","name":"若尔盖县"},{"code":"513233","name":"红原县"}]},{"code":"513300","name":"甘孜藏族自治州","list":[{"code":"513301","name":"康定市"},{"code":"513322","name":"泸定县"},{"code":"513323","name":"丹巴县"},{"code":"513324","name":"九龙县"},{"code":"513325","name":"雅江县"},{"code":"513326","name":"道孚县"},{"code":"513327","name":"炉霍县"},{"code":"513328","name":"甘孜县"},{"code":"513329","name":"新龙县"},{"code":"513330","name":"德格县"},{"code":"513331","name":"白玉县"},{"code":"513332","name":"石渠县"},{"code":"513333","name":"色达县"},{"code":"513334","name":"理塘县"},{"code":"513335","name":"巴塘县"},{"code":"513336","name":"乡城县"},{"code":"513337","name":"稻城县"},{"code":"513338","name":"得荣县"}]},{"code":"513400","name":"凉山彝族自治州","list":[{"code":"513401","name":"西昌市"},{"code":"513402","name":"会理市"},{"code":"513422","name":"木里藏族自治县"},{"code":"513423","name":"盐源县"},{"code":"513424","name":"德昌县"},{"code":"513426","name":"会东县"},{"code":"513427","name":"宁南县"},{"code":"513428","name":"普格县"},{"code":"513429","name":"布拖县"},{"code":"513430","name":"金阳县"},{"code":"513431","name":"昭觉县"},{"code":"513432","name":"喜德县"},{"code":"513433","name":"冕宁县"},{"code":"513434","name":"越西县"},{"code":"513435","name":"甘洛县"},{"code":"513436","name":"美姑县"},{"code":"513437","name":"雷波县"}]}]},{"code":"520000","name":"贵州省","list":[{"code":"520100","name":"贵阳市","list":[{"code":"520102","name":"南明区"},{"code":"520103","name":"云岩区"},{"code":"520111","name":"花溪区"},{"code":"520112","name":"乌当区"},{"code":"520113","name":"白云区"},{"code":"520115","name":"观山湖区"},{"code":"520121","name":"开阳县"},{"code":"520122","name":"息烽县"},{"code":"520123","name":"修文县"},{"code":"520181","name":"清镇市"}]},{"code":"520200","name":"六盘水市","list":[{"code":"520201","name":"钟山区"},{"code":"520203","name":"六枝特区"},{"code":"520204","name":"水城区"},{"code":"520281","name":"盘州市"}]},{"code":"520300","name":"遵义市","list":[{"code":"520302","name":"红花岗区"},{"code":"520303","name":"汇川区"},{"code":"520304","name":"播州区"},{"code":"520322","name":"桐梓县"},{"code":"520323","name":"绥阳县"},{"code":"520324","name":"正安县"},{"code":"520325","name":"道真仡佬族苗族自治县"},{"code":"520326","name":"务川仡佬族苗族自治县"},{"code":"520327","name":"凤冈县"},{"code":"520328","name":"湄潭县"},{"code":"520329","name":"余庆县"},{"code":"520330","name":"习水县"},{"code":"520381","name":"赤水市"},{"code":"520382","name":"仁怀市"}]},{"code":"520400","name":"安顺市","list":[{"code":"520402","name":"西秀区"},{"code":"520403","name":"平坝区"},{"code":"520422","name":"普定县"},{"code":"520423","name":"镇宁布依族苗族自治县"},{"code":"520424","name":"关岭布依族苗族自治县"},{"code":"520425","name":"紫云苗族布依族自治县"}]},{"code":"520500","name":"毕节市","list":[{"code":"520502","name":"七星关区"},{"code":"520521","name":"大方县"},{"code":"520523","name":"金沙县"},{"code":"520524","name":"织金县"},{"code":"520525","name":"纳雍县"},{"code":"520526","name":"威宁彝族回族苗族自治县"},{"code":"520527","name":"赫章县"},{"code":"520581","name":"黔西市"}]},{"code":"520600","name":"铜仁市","list":[{"code":"520602","name":"碧江区"},{"code":"520603","name":"万山区"},{"code":"520621","name":"江口县"},{"code":"520622","name":"玉屏侗族自治县"},{"code":"520623","name":"石阡县"},{"code":"520624","name":"思南县"},{"code":"520625","name":"印江土家族苗族自治县"},{"code":"520626","name":"德江县"},{"code":"520627","name":"沿河土家族自治县"},{"code":"520628","name":"松桃苗族自治县"}]},{"code":"522300","name":"黔西南布依族苗族自治州","list":[{"code":"522301","name":"兴义市"},{"code":"522302","name":"兴仁市"},{"code":"522323","name":"普安县"},{"code":"522324","name":"晴隆县"},{"code":"522325","name":"贞丰县"},{"code":"522326","name":"望谟县"},{"code":"522327","name":"册亨县"},{"code":"522328","name":"安龙县"}]},{"code":"522600","name":"黔东南苗族侗族自治州","list":[{"code":"522601","name":"凯里市"},{"code":"522622","name":"黄平县"},{"code":"522623","name":"施秉县"},{"code":"522624","name":"三穗县"},{"code":"522625","name":"镇远县"},{"code":"522626","name":"岑巩县"},{"code":"522627","name":"天柱县"},{"code":"522628","name":"锦屏县"},{"code":"522629","name":"剑河县"},{"code":"522630","name":"台江县"},{"code":"522631","name":"黎平县"},{"code":"522632","name":"榕江县"},{"code":"522633","name":"从江县"},{"code":"522634","name":"雷山县"},{"code":"522635","name":"麻江县"},{"code":"522636","name":"丹寨县"}]},{"code":"522700","name":"黔南布依族苗族自治州","list":[{"code":"522701","name":"都匀市"},{"code":"522702","name":"福泉市"},{"code":"522722","name":"荔波县"},{"code":"522723","name":"贵定县"},{"code":"522725","name":"瓮安县"},{"code":"522726","name":"独山县"},{"code":"522727","name":"平塘县"},{"code":"522728","name":"罗甸县"},{"code":"522729","name":"长顺县"},{"code":"522730","name":"龙里县"},{"code":"522731","name":"惠水县"},{"code":"522732","name":"三都水族自治县"}]}]},{"code":"530000","name":"云南省","list":[{"code":"530100","name":"昆明市","list":[{"code":"530102","name":"五华区"},{"code":"530103","name":"盘龙区"},{"code":"530111","name":"官渡区"},{"code":"530112","name":"西山区"},{"code":"530113","name":"东川区"},{"code":"530114","name":"呈贡区"},{"code":"530115","name":"晋宁区"},{"code":"530124","name":"富民县"},{"code":"530125","name":"宜良县"},{"code":"530126","name":"石林彝族自治县"},{"code":"530127","name":"嵩明县"},{"code":"530128","name":"禄劝彝族苗族自治县"},{"code":"530129","name":"寻甸回族彝族自治县"},{"code":"530181","name":"安宁市"}]},{"code":"530300","name":"曲靖市","list":[{"code":"530302","name":"麒麟区"},{"code":"530303","name":"沾益区"},{"code":"530304","name":"马龙区"},{"code":"530322","name":"陆良县"},{"code":"530323","name":"师宗县"},{"code":"530324","name":"罗平县"},{"code":"530325","name":"富源县"},{"code":"530326","name":"会泽县"},{"code":"530381","name":"宣威市"}]},{"code":"530400","name":"玉溪市","list":[{"code":"530402","name":"红塔区"},{"code":"530403","name":"江川区"},{"code":"530423","name":"通海县"},{"code":"530424","name":"华宁县"},{"code":"530425","name":"易门县"},{"code":"530426","name":"峨山彝族自治县"},{"code":"530427","name":"新平彝族傣族自治县"},{"code":"530428","name":"元江哈尼族彝族傣族自治县"},{"code":"530481","name":"澄江市"}]},{"code":"530500","name":"保山市","list":[{"code":"530502","name":"隆阳区"},{"code":"530521","name":"施甸县"},{"code":"530523","name":"龙陵县"},{"code":"530524","name":"昌宁县"},{"code":"530581","name":"腾冲市"}]},{"code":"530600","name":"昭通市","list":[{"code":"530602","name":"昭阳区"},{"code":"530621","name":"鲁甸县"},{"code":"530622","name":"巧家县"},{"code":"530623","name":"盐津县"},{"code":"530624","name":"大关县"},{"code":"530625","name":"永善县"},{"code":"530626","name":"绥江县"},{"code":"530627","name":"镇雄县"},{"code":"530628","name":"彝良县"},{"code":"530629","name":"威信县"},{"code":"530681","name":"水富市"}]},{"code":"530700","name":"丽江市","list":[{"code":"530702","name":"古城区"},{"code":"530721","name":"玉龙纳西族自治县"},{"code":"530722","name":"永胜县"},{"code":"530723","name":"华坪县"},{"code":"530724","name":"宁蒗彝族自治县"}]},{"code":"530800","name":"普洱市","list":[{"code":"530802","name":"思茅区"},{"code":"530821","name":"宁洱哈尼族彝族自治县"},{"code":"530822","name":"墨江哈尼族自治县"},{"code":"530823","name":"景东彝族自治县"},{"code":"530824","name":"景谷傣族彝族自治县"},{"code":"530825","name":"镇沅彝族哈尼族拉祜族自治县"},{"code":"530826","name":"江城哈尼族彝族自治县"},{"code":"530827","name":"孟连傣族拉祜族佤族自治县"},{"code":"530828","name":"澜沧拉祜族自治县"},{"code":"530829","name":"西盟佤族自治县"}]},{"code":"530900","name":"临沧市","list":[{"code":"530902","name":"临翔区"},{"code":"530921","name":"凤庆县"},{"code":"530922","name":"云县"},{"code":"530923","name":"永德县"},{"code":"530924","name":"镇康县"},{"code":"530925","name":"双江拉祜族佤族布朗族傣族自治县"},{"code":"530926","name":"耿马傣族佤族自治县"},{"code":"530927","name":"沧源佤族自治县"}]},{"code":"532300","name":"楚雄彝族自治州","list":[{"code":"532301","name":"楚雄市"},{"code":"532302","name":"禄丰市"},{"code":"532322","name":"双柏县"},{"code":"532323","name":"牟定县"},{"code":"532324","name":"南华县"},{"code":"532325","name":"姚安县"},{"code":"532326","name":"大姚县"},{"code":"532327","name":"永仁县"},{"code":"532328","name":"元谋县"},{"code":"532329","name":"武定县"}]},{"code":"532500","name":"红河哈尼族彝族自治州","list":[{"code":"532501","name":"个旧市"},{"code":"532502","name":"开远市"},{"code":"532503","name":"蒙自市"},{"code":"532504","name":"弥勒市"},{"code":"532523","name":"屏边苗族自治县"},{"code":"532524","name":"建水县"},{"code":"532525","name":"石屏县"},{"code":"532527","name":"泸西县"},{"code":"532528","name":"元阳县"},{"code":"532529","name":"红河县"},{"code":"532530","name":"金平苗族瑶族傣族自治县"},{"code":"532531","name":"绿春县"},{"code":"532532","name":"河口瑶族自治县"}]},{"code":"532600","name":"文山壮族苗族自治州","list":[{"code":"532601","name":"文山市"},{"code":"532622","name":"砚山县"},{"code":"532623","name":"西畴县"},{"code":"532624","name":"麻栗坡县"},{"code":"532625","name":"马关县"},{"code":"532626","name":"丘北县"},{"code":"532627","name":"广南县"},{"code":"532628","name":"富宁县"}]},{"code":"532800","name":"西双版纳傣族自治州","list":[{"code":"532801","name":"景洪市"},{"code":"532822","name":"勐海县"},{"code":"532823","name":"勐腊县"}]},{"code":"532900","name":"大理白族自治州","list":[{"code":"532901","name":"大理市"},{"code":"532922","name":"漾濞彝族自治县"},{"code":"532923","name":"祥云县"},{"code":"532924","name":"宾川县"},{"code":"532925","name":"弥渡县"},{"code":"532926","name":"南涧彝族自治县"},{"code":"532927","name":"巍山彝族回族自治县"},{"code":"532928","name":"永平县"},{"code":"532929","name":"云龙县"},{"code":"532930","name":"洱源县"},{"code":"532931","name":"剑川县"},{"code":"532932","name":"鹤庆县"}]},{"code":"533100","name":"德宏傣族景颇族自治州","list":[{"code":"533102","name":"瑞丽市"},{"code":"533103","name":"芒市"},{"code":"533122","name":"梁河县"},{"code":"533123","name":"盈江县"},{"code":"533124","name":"陇川县"}]},{"code":"533300","name":"怒江傈僳族自治州","list":[{"code":"533301","name":"泸水市"},{"code":"533323","name":"福贡县"},{"code":"533324","name":"贡山独龙族怒族自治县"},{"code":"533325","name":"兰坪白族普米族自治县"}]},{"code":"533400","name":"迪庆藏族自治州","list":[{"code":"533401","name":"香格里拉市"},{"code":"533422","name":"德钦县"},{"code":"533423","name":"维西傈僳族自治县"}]}]},{"code":"540000","name":"西藏自治区","list":[{"code":"540100","name":"拉萨市","list":[{"code":"540102","name":"城关区"},{"code":"540103","name":"堆龙德庆区"},{"code":"540104","name":"达孜区"},{"code":"540121","name":"林周县"},{"code":"540122","name":"当雄县"},{"code":"540123","name":"尼木县"},{"code":"540124","name":"曲水县"},{"code":"540127","name":"墨竹工卡县"}]},{"code":"540200","name":"日喀则市","list":[{"code":"540202","name":"桑珠孜区"},{"code":"540221","name":"南木林县"},{"code":"540222","name":"江孜县"},{"code":"540223","name":"定日县"},{"code":"540224","name":"萨迦县"},{"code":"540225","name":"拉孜县"},{"code":"540226","name":"昂仁县"},{"code":"540227","name":"谢通门县"},{"code":"540228","name":"白朗县"},{"code":"540229","name":"仁布县"},{"code":"540230","name":"康马县"},{"code":"540231","name":"定结县"},{"code":"540232","name":"仲巴县"},{"code":"540233","name":"亚东县"},{"code":"540234","name":"吉隆县"},{"code":"540235","name":"聂拉木县"},{"code":"540236","name":"萨嘎县"},{"code":"540237","name":"岗巴县"}]},{"code":"540300","name":"昌都市","list":[{"code":"540302","name":"卡若区"},{"code":"540321","name":"江达县"},{"code":"540322","name":"贡觉县"},{"code":"540323","name":"类乌齐县"},{"code":"540324","name":"丁青县"},{"code":"540325","name":"察雅县"},{"code":"540326","name":"八宿县"},{"code":"540327","name":"左贡县"},{"code":"540328","name":"芒康县"},{"code":"540329","name":"洛隆县"},{"code":"540330","name":"边坝县"}]},{"code":"540400","name":"林芝市","list":[{"code":"540402","name":"巴宜区"},{"code":"540421","name":"工布江达县"},{"code":"540422","name":"米林市"},{"code":"540423","name":"墨脱县"},{"code":"540424","name":"波密县"},{"code":"540425","name":"察隅县"},{"code":"540426","name":"朗县"}]},{"code":"540500","name":"山南市","list":[{"code":"540502","name":"乃东区"},{"code":"540521","name":"扎囊县"},{"code":"540522","name":"贡嘎县"},{"code":"540523","name":"桑日县"},{"code":"540524","name":"琼结县"},{"code":"540525","name":"曲松县"},{"code":"540526","name":"措美县"},{"code":"540527","name":"洛扎县"},{"code":"540528","name":"加查县"},{"code":"540529","name":"隆子县"},{"code":"540530","name":"错那市"},{"code":"540531","name":"浪卡子县"}]},{"code":"540600","name":"那曲市","list":[{"code":"540602","name":"色尼区"},{"code":"540621","name":"嘉黎县"},{"code":"540622","name":"比如县"},{"code":"540623","name":"聂荣县"},{"code":"540624","name":"安多县"},{"code":"540625","name":"申扎县"},{"code":"540626","name":"索县"},{"code":"540627","name":"班戈县"},{"code":"540628","name":"巴青县"},{"code":"540629","name":"尼玛县"},{"code":"540630","name":"双湖县"}]},{"code":"542500","name":"阿里地区","list":[{"code":"542521","name":"普兰县"},{"code":"542522","name":"札达县"},{"code":"542523","name":"噶尔县"},{"code":"542524","name":"日土县"},{"code":"542525","name":"革吉县"},{"code":"542526","name":"改则县"},{"code":"542527","name":"措勤县"}]}]},{"code":"610000","name":"陕西省","list":[{"code":"610100","name":"西安市","list":[{"code":"610102","name":"新城区"},{"code":"610103","name":"碑林区"},{"code":"610104","name":"莲湖区"},{"code":"610111","name":"灞桥区"},{"code":"610112","name":"未央区"},{"code":"610113","name":"雁塔区"},{"code":"610114","name":"阎良区"},{"code":"610115","name":"临潼区"},{"code":"610116","name":"长安区"},{"code":"610117","name":"高陵区"},{"code":"610118","name":"鄠邑区"},{"code":"610122","name":"蓝田县"},{"code":"610124","name":"周至县"}]},{"code":"610200","name":"铜川市","list":[{"code":"610202","name":"王益区"},{"code":"610203","name":"印台区"},{"code":"610204","name":"耀州区"},{"code":"610222","name":"宜君县"}]},{"code":"610300","name":"宝鸡市","list":[{"code":"610302","name":"渭滨区"},{"code":"610303","name":"金台区"},{"code":"610304","name":"陈仓区"},{"code":"610305","name":"凤翔区"},{"code":"610323","name":"岐山县"},{"code":"610324","name":"扶风县"},{"code":"610326","name":"眉县"},{"code":"610327","name":"陇县"},{"code":"610328","name":"千阳县"},{"code":"610329","name":"麟游县"},{"code":"610330","name":"凤县"},{"code":"610331","name":"太白县"}]},{"code":"610400","name":"咸阳市","list":[{"code":"610402","name":"秦都区"},{"code":"610403","name":"杨陵区"},{"code":"610404","name":"渭城区"},{"code":"610422","name":"三原县"},{"code":"610423","name":"泾阳县"},{"code":"610424","name":"乾县"},{"code":"610425","name":"礼泉县"},{"code":"610426","name":"永寿县"},{"code":"610428","name":"长武县"},{"code":"610429","name":"旬邑县"},{"code":"610430","name":"淳化县"},{"code":"610431","name":"武功县"},{"code":"610481","name":"兴平市"},{"code":"610482","name":"彬州市"}]},{"code":"610500","name":"渭南市","list":[{"code":"610502","name":"临渭区"},{"code":"610503","name":"华州区"},{"code":"610522","name":"潼关县"},{"code":"610523","name":"大荔县"},{"code":"610524","name":"合阳县"},{"code":"610525","name":"澄城县"},{"code":"610526","name":"蒲城县"},{"code":"610527","name":"白水县"},{"code":"610528","name":"富平县"},{"code":"610581","name":"韩城市"},{"code":"610582","name":"华阴市"}]},{"code":"610600","name":"延安市","list":[{"code":"610602","name":"宝塔区"},{"code":"610603","name":"安塞区"},{"code":"610621","name":"延长县"},{"code":"610622","name":"延川县"},{"code":"610625","name":"志丹县"},{"code":"610626","name":"吴起县"},{"code":"610627","name":"甘泉县"},{"code":"610628","name":"富县"},{"code":"610629","name":"洛川县"},{"code":"610630","name":"宜川县"},{"code":"610631","name":"黄龙县"},{"code":"610632","name":"黄陵县"},{"code":"610681","name":"子长市"}]},{"code":"610700","name":"汉中市","list":[{"code":"610702","name":"汉台区"},{"code":"610703","name":"南郑区"},{"code":"610722","name":"城固县"},{"code":"610723","name":"洋县"},{"code":"610724","name":"西乡县"},{"code":"610725","name":"勉县"},{"code":"610726","name":"宁强县"},{"code":"610727","name":"略阳县"},{"code":"610728","name":"镇巴县"},{"code":"610729","name":"留坝县"},{"code":"610730","name":"佛坪县"}]},{"code":"610800","name":"榆林市","list":[{"code":"610802","name":"榆阳区"},{"code":"610803","name":"横山区"},{"code":"610822","name":"府谷县"},{"code":"610824","name":"靖边县"},{"code":"610825","name":"定边县"},{"code":"610826","name":"绥德县"},{"code":"610827","name":"米脂县"},{"code":"610828","name":"佳县"},{"code":"610829","name":"吴堡县"},{"code":"610830","name":"清涧县"},{"code":"610831","name":"子洲县"},{"code":"610881","name":"神木市"}]},{"code":"610900","name":"安康市","list":[{"code":"610902","name":"汉滨区"},{"code":"610921","name":"汉阴县"},{"code":"610922","name":"石泉县"},{"code":"610923","name":"宁陕县"},{"code":"610924","name":"紫阳县"},{"code":"610925","name":"岚皋县"},{"code":"610926","name":"平利县"},{"code":"610927","name":"镇坪县"},{"code":"610929","name":"白河县"},{"code":"610981","name":"旬阳市"}]},{"code":"611000","name":"商洛市","list":[{"code":"611002","name":"商州区"},{"code":"611021","name":"洛南县"},{"code":"611022","name":"丹凤县"},{"code":"611023","name":"商南县"},{"code":"611024","name":"山阳县"},{"code":"611025","name":"镇安县"},{"code":"611026","name":"柞水县"}]}]},{"code":"620000","name":"甘肃省","list":[{"code":"620100","name":"兰州市","list":[{"code":"620102","name":"城关区"},{"code":"620103","name":"七里河区"},{"code":"620104","name":"西固区"},{"code":"620105","name":"安宁区"},{"code":"620111","name":"红古区"},{"code":"620121","name":"永登县"},{"code":"620122","name":"皋兰县"},{"code":"620123","name":"榆中县"}]},{"code":"620200","name":"嘉峪关市","list":[{"code":"620200001","name":"雄关街道"},{"code":"620200002","name":"钢城街道"},{"code":"620200100","name":"新城镇"},{"code":"620200101","name":"峪泉镇"},{"code":"620200102","name":"文殊镇"}]},{"code":"620300","name":"金昌市","list":[{"code":"620302","name":"金川区"},{"code":"620321","name":"永昌县"}]},{"code":"620400","name":"白银市","list":[{"code":"620402","name":"白银区"},{"code":"620403","name":"平川区"},{"code":"620421","name":"靖远县"},{"code":"620422","name":"会宁县"},{"code":"620423","name":"景泰县"}]},{"code":"620500","name":"天水市","list":[{"code":"620502","name":"秦州区"},{"code":"620503","name":"麦积区"},{"code":"620521","name":"清水县"},{"code":"620522","name":"秦安县"},{"code":"620523","name":"甘谷县"},{"code":"620524","name":"武山县"},{"code":"620525","name":"张家川回族自治县"}]},{"code":"620600","name":"武威市","list":[{"code":"620602","name":"凉州区"},{"code":"620621","name":"民勤县"},{"code":"620622","name":"古浪县"},{"code":"620623","name":"天祝藏族自治县"}]},{"code":"620700","name":"张掖市","list":[{"code":"620702","name":"甘州区"},{"code":"620721","name":"肃南裕固族自治县"},{"code":"620722","name":"民乐县"},{"code":"620723","name":"临泽县"},{"code":"620724","name":"高台县"},{"code":"620725","name":"山丹县"}]},{"code":"620800","name":"平凉市","list":[{"code":"620802","name":"崆峒区"},{"code":"620821","name":"泾川县"},{"code":"620822","name":"灵台县"},{"code":"620823","name":"崇信县"},{"code":"620825","name":"庄浪县"},{"code":"620826","name":"静宁县"},{"code":"620881","name":"华亭市"}]},{"code":"620900","name":"酒泉市","list":[{"code":"620902","name":"肃州区"},{"code":"620921","name":"金塔县"},{"code":"620922","name":"瓜州县"},{"code":"620923","name":"肃北蒙古族自治县"},{"code":"620924","name":"阿克塞哈萨克族自治县"},{"code":"620981","name":"玉门市"},{"code":"620982","name":"敦煌市"}]},{"code":"621000","name":"庆阳市","list":[{"code":"621002","name":"西峰区"},{"code":"621021","name":"庆城县"},{"code":"621022","name":"环县"},{"code":"621023","name":"华池县"},{"code":"621024","name":"合水县"},{"code":"621025","name":"正宁县"},{"code":"621026","name":"宁县"},{"code":"621027","name":"镇原县"}]},{"code":"621100","name":"定西市","list":[{"code":"621102","name":"安定区"},{"code":"621121","name":"通渭县"},{"code":"621122","name":"陇西县"},{"code":"621123","name":"渭源县"},{"code":"621124","name":"临洮县"},{"code":"621125","name":"漳县"},{"code":"621126","name":"岷县"}]},{"code":"621200","name":"陇南市","list":[{"code":"621202","name":"武都区"},{"code":"621221","name":"成县"},{"code":"621222","name":"文县"},{"code":"621223","name":"宕昌县"},{"code":"621224","name":"康县"},{"code":"621225","name":"西和县"},{"code":"621226","name":"礼县"},{"code":"621227","name":"徽县"},{"code":"621228","name":"两当县"}]},{"code":"622900","name":"临夏回族自治州","list":[{"code":"622901","name":"临夏市"},{"code":"622921","name":"临夏县"},{"code":"622922","name":"康乐县"},{"code":"622923","name":"永靖县"},{"code":"622924","name":"广河县"},{"code":"622925","name":"和政县"},{"code":"622926","name":"东乡族自治县"},{"code":"622927","name":"积石山保安族东乡族撒拉族自治县"}]},{"code":"623000","name":"甘南藏族自治州","list":[{"code":"623001","name":"合作市"},{"code":"623021","name":"临潭县"},{"code":"623022","name":"卓尼县"},{"code":"623023","name":"舟曲县"},{"code":"623024","name":"迭部县"},{"code":"623025","name":"玛曲县"},{"code":"623026","name":"碌曲县"},{"code":"623027","name":"夏河县"}]}]},{"code":"630000","name":"青海省","list":[{"code":"630100","name":"西宁市","list":[{"code":"630102","name":"城东区"},{"code":"630103","name":"城中区"},{"code":"630104","name":"城西区"},{"code":"630105","name":"城北区"},{"code":"630106","name":"湟中区"},{"code":"630121","name":"大通回族土族自治县"},{"code":"630123","name":"湟源县"}]},{"code":"630200","name":"海东市","list":[{"code":"630202","name":"乐都区"},{"code":"630203","name":"平安区"},{"code":"630222","name":"民和回族土族自治县"},{"code":"630223","name":"互助土族自治县"},{"code":"630224","name":"化隆回族自治县"},{"code":"630225","name":"循化撒拉族自治县"}]},{"code":"632200","name":"海北藏族自治州","list":[{"code":"632221","name":"门源回族自治县"},{"code":"632222","name":"祁连县"},{"code":"632223","name":"海晏县"},{"code":"632224","name":"刚察县"}]},{"code":"632300","name":"黄南藏族自治州","list":[{"code":"632301","name":"同仁市"},{"code":"632322","name":"尖扎县"},{"code":"632323","name":"泽库县"},{"code":"632324","name":"河南蒙古族自治县"}]},{"code":"632500","name":"海南藏族自治州","list":[{"code":"632521","name":"共和县"},{"code":"632522","name":"同德县"},{"code":"632523","name":"贵德县"},{"code":"632524","name":"兴海县"},{"code":"632525","name":"贵南县"}]},{"code":"632600","name":"果洛藏族自治州","list":[{"code":"632621","name":"玛沁县"},{"code":"632622","name":"班玛县"},{"code":"632623","name":"甘德县"},{"code":"632624","name":"达日县"},{"code":"632625","name":"久治县"},{"code":"632626","name":"玛多县"}]},{"code":"632700","name":"玉树藏族自治州","list":[{"code":"632701","name":"玉树市"},{"code":"632722","name":"杂多县"},{"code":"632723","name":"称多县"},{"code":"632724","name":"治多县"},{"code":"632725","name":"囊谦县"},{"code":"632726","name":"曲麻莱县"}]},{"code":"632800","name":"海西蒙古族藏族自治州","list":[{"code":"632801","name":"格尔木市"},{"code":"632802","name":"德令哈市"},{"code":"632803","name":"茫崖市"},{"code":"632821","name":"乌兰县"},{"code":"632822","name":"都兰县"},{"code":"632823","name":"天峻县"},{"code":"632857","name":"大柴旦行政区"}]}]},{"code":"640000","name":"宁夏回族自治区","list":[{"code":"640100","name":"银川市","list":[{"code":"640104","name":"兴庆区"},{"code":"640105","name":"西夏区"},{"code":"640106","name":"金凤区"},{"code":"640121","name":"永宁县"},{"code":"640122","name":"贺兰县"},{"code":"640181","name":"灵武市"}]},{"code":"640200","name":"石嘴山市","list":[{"code":"640202","name":"大武口区"},{"code":"640205","name":"惠农区"},{"code":"640221","name":"平罗县"}]},{"code":"640300","name":"吴忠市","list":[{"code":"640302","name":"利通区"},{"code":"640303","name":"红寺堡区"},{"code":"640323","name":"盐池县"},{"code":"640324","name":"同心县"},{"code":"640381","name":"青铜峡市"}]},{"code":"640400","name":"固原市","list":[{"code":"640402","name":"原州区"},{"code":"640422","name":"西吉县"},{"code":"640423","name":"隆德县"},{"code":"640424","name":"泾源县"},{"code":"640425","name":"彭阳县"}]},{"code":"640500","name":"中卫市","list":[{"code":"640502","name":"沙坡头区"},{"code":"640521","name":"中宁县"},{"code":"640522","name":"海原县"}]}]},{"code":"650000","name":"新疆维吾尔自治区","list":[{"code":"650100","name":"乌鲁木齐市","list":[{"code":"650102","name":"天山区"},{"code":"650103","name":"沙依巴克区"},{"code":"650104","name":"新市区"},{"code":"650105","name":"水磨沟区"},{"code":"650106","name":"头屯河区"},{"code":"650107","name":"达坂城区"},{"code":"650109","name":"米东区"},{"code":"650121","name":"乌鲁木齐县"}]},{"code":"650200","name":"克拉玛依市","list":[{"code":"650202","name":"独山子区"},{"code":"650203","name":"克拉玛依区"},{"code":"650204","name":"白碱滩区"},{"code":"650205","name":"乌尔禾区"}]},{"code":"650400","name":"吐鲁番市","list":[{"code":"650402","name":"高昌区"},{"code":"650421","name":"鄯善县"},{"code":"650422","name":"托克逊县"}]},{"code":"650500","name":"哈密市","list":[{"code":"650502","name":"伊州区"},{"code":"650521","name":"巴里坤哈萨克自治县"},{"code":"650522","name":"伊吾县"}]},{"code":"652300","name":"昌吉回族自治州","list":[{"code":"652301","name":"昌吉市"},{"code":"652302","name":"阜康市"},{"code":"652323","name":"呼图壁县"},{"code":"652324","name":"玛纳斯县"},{"code":"652325","name":"奇台县"},{"code":"652327","name":"吉木萨尔县"},{"code":"652328","name":"木垒哈萨克自治县"}]},{"code":"652700","name":"博尔塔拉蒙古自治州","list":[{"code":"652701","name":"博乐市"},{"code":"652702","name":"阿拉山口市"},{"code":"652722","name":"精河县"},{"code":"652723","name":"温泉县"}]},{"code":"652800","name":"巴音郭楞蒙古自治州","list":[{"code":"652801","name":"库尔勒市"},{"code":"652822","name":"轮台县"},{"code":"652823","name":"尉犁县"},{"code":"652824","name":"若羌县"},{"code":"652825","name":"且末县"},{"code":"652826","name":"焉耆回族自治县"},{"code":"652827","name":"和静县"},{"code":"652828","name":"和硕县"},{"code":"652829","name":"博湖县"}]},{"code":"652900","name":"阿克苏地区","list":[{"code":"652901","name":"阿克苏市"},{"code":"652902","name":"库车市"},{"code":"652922","name":"温宿县"},{"code":"652924","name":"沙雅县"},{"code":"652925","name":"新和县"},{"code":"652926","name":"拜城县"},{"code":"652927","name":"乌什县"},{"code":"652928","name":"阿瓦提县"},{"code":"652929","name":"柯坪县"}]},{"code":"653000","name":"克孜勒苏柯尔克孜自治州","list":[{"code":"653001","name":"阿图什市"},{"code":"653022","name":"阿克陶县"},{"code":"653023","name":"阿合奇县"},{"code":"653024","name":"乌恰县"}]},{"code":"653100","name":"喀什地区","list":[{"code":"653101","name":"喀什市"},{"code":"653121","name":"疏附县"},{"code":"653122","name":"疏勒县"},{"code":"653123","name":"英吉沙县"},{"code":"653124","name":"泽普县"},{"code":"653125","name":"莎车县"},{"code":"653126","name":"叶城县"},{"code":"653127","name":"麦盖提县"},{"code":"653128","name":"岳普湖县"},{"code":"653129","name":"伽师县"},{"code":"653130","name":"巴楚县"},{"code":"653131","name":"塔什库尔干塔吉克自治县"}]},{"code":"653200","name":"和田地区","list":[{"code":"653201","name":"和田市"},{"code":"653221","name":"和田县"},{"code":"653222","name":"墨玉县"},{"code":"653223","name":"皮山县"},{"code":"653224","name":"洛浦县"},{"code":"653225","name":"策勒县"},{"code":"653226","name":"于田县"},{"code":"653227","name":"民丰县"}]},{"code":"654000","name":"伊犁哈萨克自治州","list":[{"code":"654002","name":"伊宁市"},{"code":"654003","name":"奎屯市"},{"code":"654004","name":"霍尔果斯市"},{"code":"654021","name":"伊宁县"},{"code":"654022","name":"察布查尔锡伯自治县"},{"code":"654023","name":"霍城县"},{"code":"654024","name":"巩留县"},{"code":"654025","name":"新源县"},{"code":"654026","name":"昭苏县"},{"code":"654027","name":"特克斯县"},{"code":"654028","name":"尼勒克县"}]},{"code":"654200","name":"塔城地区","list":[{"code":"654201","name":"塔城市"},{"code":"654202","name":"乌苏市"},{"code":"654203","name":"沙湾市"},{"code":"654221","name":"额敏县"},{"code":"654224","name":"托里县"},{"code":"654225","name":"裕民县"},{"code":"654226","name":"和布克赛尔蒙古自治县"}]},{"code":"654300","name":"阿勒泰地区","list":[{"code":"654301","name":"阿勒泰市"},{"code":"654321","name":"布尔津县"},{"code":"654322","name":"富蕴县"},{"code":"654323","name":"福海县"},{"code":"654324","name":"哈巴河县"},{"code":"654325","name":"青河县"},{"code":"654326","name":"吉木乃县"}]},{"code":"659001","name":"石河子市","list":[{"code":"659001001","name":"新城街道"},{"code":"659001002","name":"向阳街道"},{"code":"659001003","name":"红山街道"},{"code":"659001004","name":"老街街道"},{"code":"659001005","name":"东城街道"},{"code":"659001100","name":"北泉镇"},{"code":"659001101","name":"石河子镇"},{"code":"659001500","name":"兵团一五二团"}]},{"code":"659002","name":"阿拉尔市","list":[{"code":"659002001","name":"金银川路街道"},{"code":"659002002","name":"幸福路街道"},{"code":"659002003","name":"青松路街道"},{"code":"659002004","name":"南口街道"},{"code":"659002100","name":"金银川镇"},{"code":"659002101","name":"新井子镇"},{"code":"659002102","name":"甘泉镇"},{"code":"659002103","name":"永宁镇"},{"code":"659002201","name":"托喀依乡"},{"code":"659002500","name":"兵团七团"},{"code":"659002501","name":"兵团八团"},{"code":"659002503","name":"兵团十团"},{"code":"659002504","name":"兵团十一团"},{"code":"659002505","name":"兵团十二团"},{"code":"659002506","name":"兵团十三团"},{"code":"659002507","name":"兵团十四团"},{"code":"659002509","name":"兵团十六团"},{"code":"659002510","name":"九团"}]},{"code":"659003","name":"图木舒克市","list":[{"code":"659003002","name":"前海街道"},{"code":"659003100","name":"草湖镇"},{"code":"659003504","name":"兵团四十四团"},{"code":"659003509","name":"兵团四十九团"},{"code":"659003510","name":"兵团五十团"},{"code":"659003511","name":"兵团五十一团"},{"code":"659003513","name":"兵团五十三团"}]},{"code":"659004","name":"五家渠市","list":[{"code":"659004001","name":"军垦路街道"},{"code":"659004002","name":"青湖路街道"},{"code":"659004003","name":"人民路街道"},{"code":"659004500","name":"兵团一零一团"},{"code":"659004501","name":"五家渠经济技术开区"},{"code":"659004502","name":"兵团一零三团"}]},{"code":"659005","name":"北屯市","list":[{"code":"659005100","name":"双渠镇"},{"code":"659005101","name":"丰庆镇"},{"code":"659005102","name":"海川镇"}]},{"code":"659006","name":"铁门关市","list":[{"code":"659006100","name":"博古其镇"},{"code":"659006101","name":"双丰镇"}]},{"code":"659007","name":"双河市","list":[{"code":"659007500","name":"兵团八十九团"},{"code":"659007501","name":"兵团八十六团"},{"code":"659007502","name":"兵团八十四团"},{"code":"659007503","name":"兵团八十一团"},{"code":"659007504","name":"兵团九十团"},{"code":"659007505","name":"兵团八十五团"}]},{"code":"659008","name":"可克达拉市","list":[{"code":"659008500","name":"兵团六十八团"},{"code":"659008501","name":"兵团六十六团(中心团场)"},{"code":"659008502","name":"兵团六十七团"},{"code":"659008503","name":"兵团六十三团"},{"code":"659008504","name":"兵团六十四团"}]},{"code":"659009","name":"昆玉市","list":[{"code":"659009000","name":"昆玉市"}]},{"code":"659100","name":"新星市","list":[{"code":"659011","name":"新星市"}]},{"code":"659200","name":"胡杨河市","list":[{"code":"659210","name":"胡杨河市"}]},{"code":"659300","name":"白杨市","list":[{"code":"659312","name":"白杨市"}]}]},{"code":"990000","name":"苏鲁交界","list":[{"code":"999900","name":"海域","list":[{"code":"999900997","name":"达山岛(达念山)"},{"code":"999900998","name":"车牛山"},{"code":"999900999","name":"平岛(平山岛)"}]}]}] \ No newline at end of file diff --git a/plugin/think-plugs-static/stc/public/static/plugs/jquery/artplayer.min.js b/plugin/think-plugs-static/stc/public/static/plugs/jquery/artplayer.min.js new file mode 100644 index 000000000..c16021c1d --- /dev/null +++ b/plugin/think-plugs-static/stc/public/static/plugs/jquery/artplayer.min.js @@ -0,0 +1,7 @@ +/*! + * artplayer.js v5.0.9 + * Github: https://github.com/zhw2590582/ArtPlayer + * (c) 2017-2023 Harvey Zack + * Released under the MIT License. + */ +!function(e,t,r,a,o){var n="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:"undefined"!=typeof window?window:"undefined"!=typeof global?global:{},i="function"==typeof n[a]&&n[a],s=i.cache||{},l="undefined"!=typeof module&&"function"==typeof module.require&&module.require.bind(module);function c(t,r){if(!s[t]){if(!e[t]){var o="function"==typeof n[a]&&n[a];if(!r&&o)return o(t,!0);if(i)return i(t,!0);if(l&&"string"==typeof t)return l(t);var p=new Error("Cannot find module '"+t+"'");throw p.code="MODULE_NOT_FOUND",p}d.resolve=function(r){var a=e[t][1][r];return null!=a?a:r},d.cache={};var u=s[t]=new c.Module(t);e[t][0].call(u.exports,d,u,u.exports,this)}return s[t].exports;function d(e){var t=d.resolve(e);return!1===t?{}:c(t)}}c.isParcelRequire=!0,c.Module=function(e){this.id=e,this.bundle=c,this.exports={}},c.modules=e,c.cache=s,c.parent=i,c.register=function(t,r){e[t]=[function(e,t){t.exports=r},{}]},Object.defineProperty(c,"root",{get:function(){return n[a]}}),n[a]=c;for(var p=0;pt.call(this,this))),X.DEBUG){const e=e=>console.log(`[ART.${this.id}] -> ${e}`);e("Version@"+X.version),e("Env@"+X.env),e("Build@"+X.build);for(let t=0;te("Event@"+t.type)))}Z.push(this)}static get instances(){return Z}static get version(){return"5.0.9"}static get env(){return"production"}static get build(){return"2023-05-14 11:10:25"}static get config(){return h.default}static get utils(){return p}static get scheme(){return d.default}static get Emitter(){return c.default}static get validator(){return s.default}static get kindOf(){return s.default.kindOf}static get html(){return g.default.html}static get option(){return{id:"",container:"#artplayer",url:"",poster:"",type:"",theme:"#f00",volume:.7,isLive:!1,muted:!1,autoplay:!1,autoSize:!1,autoMini:!1,loop:!1,flip:!1,playbackRate:!1,aspectRatio:!1,screenshot:!1,setting:!1,hotkey:!0,pip:!1,mutex:!0,backdrop:!0,fullscreen:!1,fullscreenWeb:!1,subtitleOffset:!1,miniProgressBar:!1,useSSR:!1,playsInline:!0,lock:!1,fastForward:!1,autoPlayback:!1,autoOrientation:!1,airplay:!1,layers:[],contextmenu:[],controls:[],settings:[],quality:[],highlight:[],plugins:[],thumbnails:{url:"",number:60,column:10,width:0,height:0},subtitle:{url:"",type:"",style:{},escape:!0,encoding:"utf-8",onVttLoad:e=>e},moreVideoAttr:{controls:!1,preload:p.isSafari?"auto":"metadata"},i18n:{},icons:{},cssVar:{},customType:{},lang:navigator.language.toLowerCase()}}get proxy(){return this.events.proxy}get query(){return this.template.query}get video(){return this.template.$video}destroy(e=!0){this.events.destroy(),this.template.destroy(e),Z.splice(Z.indexOf(this),1),this.isDestroy=!0,this.emit("destroy")}}if(r.default=X,X.DEBUG=!1,X.CONTEXTMENU=!0,X.NOTICE_TIME=2e3,X.SETTING_WIDTH=250,X.SETTING_ITEM_WIDTH=200,X.SETTING_ITEM_HEIGHT=35,X.RESIZE_TIME=200,X.SCROLL_TIME=200,X.SCROLL_GAP=50,X.AUTO_PLAYBACK_MAX=10,X.AUTO_PLAYBACK_MIN=5,X.AUTO_PLAYBACK_TIMEOUT=3e3,X.RECONNECT_TIME_MAX=5,X.RECONNECT_SLEEP_TIME=1e3,X.CONTROL_HIDE_TIME=3e3,X.DBCLICK_TIME=300,X.DBCLICK_FULLSCREEN=!0,X.MOBILE_DBCLICK_PLAY=!0,X.MOBILE_CLICK_PLAY=!1,X.AUTO_ORIENTATION_TIME=200,X.INFO_LOOP_TIME=1e3,X.FAST_FORWARD_VALUE=3,X.FAST_FORWARD_TIME=1e3,X.TOUCH_MOVE_RATIO=.5,X.VOLUME_STEP=.1,X.SEEK_STEP=5,X.PLAYBACK_RATE=[.5,.75,1,1.25,1.5,2],X.ASPECT_RATIO=["default","4:3","16:9"],X.FLIP=["normal","horizontal","vertical"],X.FULLSCREEN_WEB_IN_BODY=!1,"undefined"!=typeof document&&!document.getElementById("artplayer-style")){const e=p.createElement("style");e.id="artplayer-style",e.textContent=n.default,document.head.appendChild(e)}"undefined"!=typeof window&&(window.Artplayer=X)},{"bundle-text:./style/index.less":"0016T","option-validator":"bAWi2","./utils/emitter":"66mFZ","./utils":"71aH7","./scheme":"AKEiO","./config":"lyjeQ","./template":"X13Zf","./i18n":"3jKkj","./player":"a90nx","./control":"8Z0Uf","./contextmenu":"2KYsr","./info":"02ajl","./subtitle":"eSWto","./events":"jo4S1","./hotkey":"6NoFy","./layer":"6G6hZ","./loading":"3dsEe","./notice":"dWGTw","./mask":"5POkG","./icons":"6OeNg","./setting":"3eYNH","./storage":"2aaJe","./plugins":"8MTUM","@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],"0016T":[function(e,t,r){t.exports='.art-video-player{--art-theme:red;--art-font-color:#fff;--art-background-color:#000;--art-text-shadow-color:#00000080;--art-transition-duration:.2s;--art-padding:10px;--art-border-radius:3px;--art-progress-height:6px;--art-progress-color:#fff3;--art-hover-color:#ffffff80;--art-loaded-color:#fff3;--art-loop-color:#ffffffbf;--art-state-size:80px;--art-state-opacity:.8;--art-bottom-height:100px;--art-bottom-offset:20px;--art-bottom-gap:5px;--art-highlight-width:8px;--art-highlight-color:#ffffff80;--art-loop-width:2px;--art-control-height:46px;--art-control-opacity:.75;--art-control-icon-size:36px;--art-control-icon-scale:1.1;--art-volume-height:120px;--art-volume-handle-size:14px;--art-lock-size:36px;--art-indicator-scale:0;--art-indicator-size:16px;--art-fullscreen-web-index:9999;--art-settings-icon-size:24px;--art-settings-max-height:300px;--art-selector-max-height:300px;--art-contextmenus-min-width:250px;--art-subtitle-font-size:20px;--art-subtitle-gap:5px;--art-subtitle-bottom:15px;--art-subtitle-border:#000;--art-widget-background:#000000d9;--art-tip-background:#00000080;--art-scrollbar-size:4px;--art-scrollbar-background:#ffffff40;--art-scrollbar-background-hover:#ffffff80;--art-mini-progress-height:2px}.art-bg-cover{background-position:50%;background-repeat:no-repeat;background-size:cover}.art-bottom-gradient{background-image:linear-gradient(#0000,#0006,#000);background-position:bottom;background-repeat:repeat-x}.art-backdrop-filter{-webkit-backdrop-filter:saturate(180%)blur(20px);backdrop-filter:saturate(180%)blur(20px);background-color:#000000bf!important}.art-truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.art-video-player{width:100%;height:100%;zoom:1;text-align:left;direction:ltr;user-select:none;box-sizing:border-box;color:var(--art-font-color);background-color:var(--art-background-color);text-shadow:0 0 2px var(--art-text-shadow-color);-webkit-tap-highlight-color:#0000;-ms-touch-action:manipulation;touch-action:manipulation;-ms-high-contrast-adjust:none;outline:0;margin:0 auto;padding:0;font-family:PingFang SC,Helvetica Neue,Microsoft YaHei,Roboto,Arial,sans-serif;font-size:14px;line-height:1.3;position:relative}.art-video-player *,.art-video-player :before,.art-video-player :after{box-sizing:border-box;margin:0;padding:0}.art-video-player ::-webkit-scrollbar{width:var(--art-scrollbar-size);height:var(--art-scrollbar-size)}.art-video-player ::-webkit-scrollbar-thumb{background-color:var(--art-scrollbar-background)}.art-video-player ::-webkit-scrollbar-thumb:hover{background-color:var(--art-scrollbar-background-hover)}.art-video-player img{max-width:100%;vertical-align:top}.art-video-player svg{fill:var(--art-font-color)}.art-video-player a{color:var(--art-font-color);text-decoration:none}.art-icon{justify-content:center;align-items:center;line-height:1;display:flex}.art-video-player.art-backdrop .art-contextmenus,.art-video-player.art-backdrop .art-info,.art-video-player.art-backdrop .art-settings,.art-video-player.art-backdrop .art-layer-auto-playback,.art-video-player.art-backdrop .art-selector-list,.art-video-player.art-backdrop .art-volume-inner{-webkit-backdrop-filter:saturate(180%)blur(20px);backdrop-filter:saturate(180%)blur(20px);background-color:#000000bf!important}.art-video{z-index:10;width:100%;height:100%;cursor:pointer;position:absolute;inset:0}.art-poster{z-index:11;width:100%;height:100%;pointer-events:none;background-position:50%;background-repeat:no-repeat;background-size:cover;position:absolute;inset:0}.art-video-player .art-subtitle{z-index:20;width:100%;text-align:center;pointer-events:none;justify-content:center;align-items:center;gap:var(--art-subtitle-gap);bottom:var(--art-subtitle-bottom);font-size:var(--art-subtitle-font-size);transition:bottom var(--art-transition-duration)ease;text-shadow:var(--art-subtitle-border)1px 0 1px,var(--art-subtitle-border)0 1px 1px,var(--art-subtitle-border)-1px 0 1px,var(--art-subtitle-border)0 -1px 1px,var(--art-subtitle-border)1px 1px 1px,var(--art-subtitle-border)-1px -1px 1px,var(--art-subtitle-border)1px -1px 1px,var(--art-subtitle-border)-1px 1px 1px;flex-direction:column;padding:0 5%;display:none;position:absolute}.art-video-player.art-subtitle-show .art-subtitle{display:flex}.art-video-player.art-control-show .art-subtitle{bottom:calc(var(--art-control-height) + var(--art-subtitle-bottom))}.art-danmuku{z-index:30;width:100%;height:100%;pointer-events:none;position:absolute;inset:0;overflow:hidden}.art-video-player .art-layers{z-index:40;width:100%;height:100%;pointer-events:none;display:none;position:absolute;inset:0}.art-video-player .art-layers .art-layer{pointer-events:auto}.art-video-player.art-layer-show .art-layers{display:flex}.art-video-player .art-mask{z-index:50;width:100%;height:100%;pointer-events:none;justify-content:center;align-items:center;display:flex;position:absolute;inset:0}.art-video-player .art-mask .art-state{opacity:0;width:var(--art-state-size);height:var(--art-state-size);transition:all var(--art-transition-duration)ease;justify-content:center;align-items:center;display:flex;transform:scale(2)}.art-video-player.art-mask-show .art-state{cursor:pointer;pointer-events:auto;opacity:var(--art-state-opacity);transform:scale(1)}.art-video-player.art-loading-show .art-state{display:none}.art-video-player .art-loading{z-index:70;width:100%;height:100%;pointer-events:none;justify-content:center;align-items:center;display:none;position:absolute;inset:0}.art-video-player.art-loading-show .art-loading{display:flex}.art-video-player .art-bottom{z-index:60;width:100%;height:100%;opacity:0;pointer-events:none;justify-content:flex-end;gap:var(--art-bottom-gap);padding:0 var(--art-padding);transition:opacity var(--art-transition-duration)ease;background-size:100% var(--art-bottom-height);background-image:linear-gradient(#0000,#0006,#000);background-position:bottom;background-repeat:repeat-x;flex-direction:column;display:flex;position:absolute;inset:0;overflow:hidden}.art-video-player .art-bottom .art-controls,.art-video-player .art-bottom .art-progress{transform:translateY(var(--art-bottom-offset));transition:transform var(--art-transition-duration)ease}.art-video-player.art-control-show .art-bottom,.art-video-player.art-hover .art-bottom{opacity:1}.art-video-player.art-control-show .art-bottom .art-controls,.art-video-player.art-hover .art-bottom .art-controls,.art-video-player.art-control-show .art-bottom .art-progress,.art-video-player.art-hover .art-bottom .art-progress{transform:translateY(0)}.art-bottom .art-progress{z-index:0;pointer-events:auto;position:relative}.art-bottom .art-progress .art-control-progress{cursor:pointer;height:var(--art-progress-height);justify-content:center;align-items:center;display:flex;position:relative}.art-bottom .art-progress .art-control-progress .art-control-progress-inner{height:50%;width:100%;transition:height var(--art-transition-duration)ease;background-color:var(--art-progress-color);align-items:center;display:flex;position:relative}.art-bottom .art-progress .art-control-progress .art-control-progress-inner .art-progress-hover{z-index:0;width:100%;height:100%;width:0%;background-color:var(--art-hover-color);display:none;position:absolute;inset:0}.art-bottom .art-progress .art-control-progress .art-control-progress-inner .art-progress-loaded{z-index:10;width:100%;height:100%;width:0%;background-color:var(--art-loaded-color);position:absolute;inset:0}.art-bottom .art-progress .art-control-progress .art-control-progress-inner .art-progress-played{z-index:20;width:100%;height:100%;width:0%;background-color:var(--art-theme);position:absolute;inset:0}.art-bottom .art-progress .art-control-progress .art-control-progress-inner .art-progress-highlight{z-index:30;width:100%;height:100%;pointer-events:none;position:absolute;inset:0}.art-bottom .art-progress .art-control-progress .art-control-progress-inner .art-progress-highlight span{z-index:0;width:100%;height:100%;pointer-events:auto;width:var(--art-highlight-width);transform:translateX(calc(var(--art-highlight-width)/-2));background-color:var(--art-highlight-color);position:absolute;inset:0 auto 0 0}.art-bottom .art-progress .art-control-progress .art-control-progress-inner .art-progress-indicator{z-index:40;width:var(--art-indicator-size);height:var(--art-indicator-size);transform:scale(var(--art-indicator-scale));margin-left:calc(var(--art-indicator-size)/-2);transition:transform var(--art-transition-duration)ease;border-radius:50%;justify-content:center;align-items:center;display:flex;position:absolute;left:0}.art-bottom .art-progress .art-control-progress .art-control-progress-inner .art-progress-indicator .art-icon{width:100%;height:100%;pointer-events:none}.art-bottom .art-progress .art-control-progress .art-control-progress-inner .art-progress-indicator:hover{transform:scale(1.2)!important}.art-bottom .art-progress .art-control-progress .art-control-progress-inner .art-progress-indicator:active{transform:scale(1)!important}.art-bottom .art-progress .art-control-progress .art-control-progress-inner .art-progress-tip{z-index:50;border-radius:var(--art-border-radius);white-space:nowrap;background-color:var(--art-tip-background);padding:3px 5px;font-size:12px;line-height:1;display:none;position:absolute;top:-25px;left:0}.art-bottom .art-progress .art-control-progress:hover .art-control-progress-inner{height:100%}.art-bottom .art-progress .art-control-thumbnails{border-radius:var(--art-border-radius);pointer-events:none;background-color:var(--art-widget-background);display:none;position:absolute;bottom:10px;left:0;box-shadow:0 1px 3px #0003,0 1px 2px -1px #0003}.art-bottom .art-progress .art-control-loop{z-index:0;width:100%;height:100%;pointer-events:none;display:none;position:absolute;inset:0}.art-bottom .art-progress .art-control-loop .art-loop-point{z-index:0;width:100%;height:100%;width:var(--art-loop-width);background-color:var(--art-loop-color);transform:translateX(calc(var(--art-loop-width)/-2))scaleY(1.5);position:absolute;inset:0 0 0 0%}.art-bottom:hover .art-progress .art-control-progress .art-control-progress-inner .art-progress-indicator{transform:scale(1)}.art-controls{z-index:10;pointer-events:auto;height:var(--art-control-height);justify-content:space-between;align-items:center;display:flex;position:relative}.art-controls .art-controls-left,.art-controls .art-controls-right{height:100%;display:flex}.art-controls .art-controls-center{height:100%;flex:1;justify-content:center;align-items:center;padding:0 10px;display:none}.art-controls .art-controls-right{justify-content:flex-end}.art-controls .art-control{cursor:pointer;white-space:nowrap;opacity:var(--art-control-opacity);min-height:var(--art-control-height);min-width:var(--art-control-height);transition:opacity var(--art-transition-duration)ease;flex-shrink:0;justify-content:center;align-items:center;display:flex}.art-controls .art-control .art-icon{height:var(--art-control-icon-size);width:var(--art-control-icon-size);transform:scale(var(--art-control-icon-scale));transition:transform var(--art-transition-duration)ease}.art-controls .art-control .art-icon:active{transform:scale(calc(var(--art-control-icon-scale)*.8))}.art-controls .art-control:hover{opacity:1}.art-control-volume{position:relative}.art-control-volume .art-volume-panel{text-align:center;cursor:default;opacity:0;pointer-events:none;left:0;right:0;bottom:var(--art-control-height);width:var(--art-control-height);height:var(--art-volume-height);transition:all var(--art-transition-duration)ease;justify-content:center;align-items:center;padding:0 5px;font-size:12px;display:flex;position:absolute;transform:translateY(10px)}.art-control-volume .art-volume-panel .art-volume-inner{height:100%;width:100%;border-radius:var(--art-border-radius);background-color:var(--art-widget-background);flex-direction:column;align-items:center;gap:10px;padding:10px 0 12px;display:flex}.art-control-volume .art-volume-panel .art-volume-inner .art-volume-slider{width:100%;cursor:pointer;flex:1;justify-content:center;display:flex;position:relative}.art-control-volume .art-volume-panel .art-volume-inner .art-volume-slider .art-volume-handle{width:2px;border-radius:var(--art-border-radius);background-color:#ffffff40;justify-content:center;display:flex;position:relative;overflow:hidden}.art-control-volume .art-volume-panel .art-volume-inner .art-volume-slider .art-volume-handle .art-volume-loaded{z-index:0;width:100%;height:100%;background-color:var(--art-theme);position:absolute;inset:0}.art-control-volume .art-volume-panel .art-volume-inner .art-volume-slider .art-volume-indicator{width:var(--art-volume-handle-size);height:var(--art-volume-handle-size);margin-top:calc(var(--art-volume-handle-size)/-2);background-color:var(--art-theme);transition:transform var(--art-transition-duration)ease;border-radius:100%;flex-shrink:0;position:absolute;transform:scale(1)}.art-control-volume .art-volume-panel .art-volume-inner .art-volume-slider:active .art-volume-indicator{transform:scale(.9)}.art-control-volume:hover .art-volume-panel{opacity:1;pointer-events:auto;transform:translateY(0)}.art-video-player .art-notice{z-index:80;width:100%;height:100%;height:auto;padding:var(--art-padding);pointer-events:none;display:none;position:absolute;inset:0 0 auto}.art-video-player .art-notice .art-notice-inner{border-radius:var(--art-border-radius);background-color:var(--art-tip-background);padding:5px;line-height:1;display:inline-flex}.art-video-player.art-notice-show .art-notice{display:flex}.art-video-player .art-contextmenus{z-index:120;border-radius:var(--art-border-radius);background-color:var(--art-widget-background);min-width:var(--art-contextmenus-min-width);flex-direction:column;padding:5px 0;font-size:12px;display:none;position:absolute}.art-video-player .art-contextmenus .art-contextmenu{cursor:pointer;border-bottom:1px solid #ffffff1a;padding:10px 15px;display:flex}.art-video-player .art-contextmenus .art-contextmenu span{padding:0 8px}.art-video-player .art-contextmenus .art-contextmenu span:hover,.art-video-player .art-contextmenus .art-contextmenu span.art-current{color:var(--art-theme)}.art-video-player .art-contextmenus .art-contextmenu:hover{background-color:#ffffff1a}.art-video-player .art-contextmenus .art-contextmenu:last-child{border-bottom:none}.art-video-player.art-contextmenu-show .art-contextmenus{display:flex}.art-video-player .art-settings{z-index:90;border-radius:var(--art-border-radius);transform-origin:100% 100%;max-height:var(--art-settings-max-height);left:auto;right:var(--art-padding);bottom:var(--art-control-height);transform:scale(var(--art-settings-scale));transition:all var(--art-transition-duration)ease;background-color:var(--art-widget-background);flex-direction:column;display:none;position:absolute;overflow:hidden auto}.art-video-player .art-settings .art-setting-panel{flex-direction:column;display:none}.art-video-player .art-settings .art-setting-panel.art-current{display:flex}.art-video-player .art-settings .art-setting-panel .art-setting-item{cursor:pointer;transition:background-color var(--art-transition-duration)ease;justify-content:space-between;align-items:center;padding:0 5px;display:flex;overflow:hidden}.art-video-player .art-settings .art-setting-panel .art-setting-item:hover{background-color:#ffffff1a}.art-video-player .art-settings .art-setting-panel .art-setting-item.art-current{color:var(--art-theme)}.art-video-player .art-settings .art-setting-panel .art-setting-item .art-icon-check{visibility:hidden;height:15px}.art-video-player .art-settings .art-setting-panel .art-setting-item.art-current .art-icon-check{visibility:visible}.art-video-player .art-settings .art-setting-panel .art-setting-item .art-setting-item-left{justify-content:center;align-items:center;gap:5px;display:flex}.art-video-player .art-settings .art-setting-panel .art-setting-item .art-setting-item-left .art-setting-item-left-icon{height:var(--art-settings-icon-size);width:var(--art-settings-icon-size);justify-content:center;align-items:center;display:flex}.art-video-player .art-settings .art-setting-panel .art-setting-item .art-setting-item-right{justify-content:center;align-items:center;gap:5px;font-size:12px;display:flex}.art-video-player .art-settings .art-setting-panel .art-setting-item .art-setting-item-right .art-setting-item-right-tooltip{white-space:nowrap;color:#ffffff80}.art-video-player .art-settings .art-setting-panel .art-setting-item .art-setting-item-right .art-setting-item-right-icon{min-width:32px;height:24px;justify-content:center;align-items:center;display:flex}.art-video-player .art-settings .art-setting-panel .art-setting-item .art-setting-item-right .art-setting-range{height:3px;width:80px;appearance:none;background-color:#fff3;outline:none}.art-video-player .art-settings .art-setting-panel .art-setting-item-back{border-bottom:1px solid #ffffff1a}.art-video-player.art-setting-show .art-settings{display:flex}.art-video-player .art-info{left:var(--art-padding);top:var(--art-padding);z-index:100;border-radius:var(--art-border-radius);background-color:var(--art-widget-background);padding:10px;font-size:12px;display:none;position:absolute}.art-video-player .art-info .art-info-panel{flex-direction:column;gap:5px;display:flex}.art-video-player .art-info .art-info-panel .art-info-item{align-items:center;gap:5px;display:flex}.art-video-player .art-info .art-info-panel .art-info-item .art-info-title{width:100px;text-align:right}.art-video-player .art-info .art-info-panel .art-info-item .art-info-content{width:250px;text-overflow:ellipsis;white-space:nowrap;user-select:all;overflow:hidden}.art-video-player .art-info .art-info-close{cursor:pointer;position:absolute;top:5px;right:5px}.art-video-player.art-info-show .art-info{display:flex}.art-hide-cursor *{cursor:none!important}.art-video-player[data-aspect-ratio]{overflow:hidden}.art-video-player[data-aspect-ratio] .art-video{object-fit:fill;box-sizing:content-box}.art-fullscreen{--art-control-height:60px;--art-control-icon-scale:1.3}.art-fullscreen-web{--art-control-height:60px;--art-control-icon-scale:1.3;z-index:var(--art-fullscreen-web-index);width:100%;height:100%;position:fixed;inset:0}.art-mini-popup{z-index:9999;width:320px;height:180px;border-radius:var(--art-border-radius);cursor:move;user-select:none;background:#000;transition:opacity .2s;position:fixed;overflow:hidden;box-shadow:0 0 5px #00000080}.art-mini-popup svg{fill:#fff}.art-mini-popup .art-video{pointer-events:none}.art-mini-popup .art-mini-close{z-index:20;cursor:pointer;opacity:0;transition:opacity .2s;position:absolute;top:10px;right:10px}.art-mini-popup .art-mini-state{z-index:30;width:100%;height:100%;pointer-events:none;opacity:0;background-color:#00000040;justify-content:center;align-items:center;transition:opacity .2s;display:flex;position:absolute;inset:0}.art-mini-popup .art-mini-state .art-icon{opacity:.75;cursor:pointer;pointer-events:auto;transition:transform .2s;transform:scale(3)}.art-mini-popup .art-mini-state .art-icon:active{transform:scale(2.5)}.art-mini-popup.art-mini-droging{opacity:.9}.art-mini-popup:hover .art-mini-close,.art-mini-popup:hover .art-mini-state{opacity:1}.art-video-player[data-flip=horizontal] .art-video{transform:scaleX(-1)}.art-video-player[data-flip=vertical] .art-video{transform:scaleY(-1)}.art-video-player .art-layer-mini-progress-bar{z-index:0;width:100%;height:100%;height:var(--art-mini-progress-height);background-color:var(--art-theme);display:flex;position:absolute;inset:auto 0 0}.art-video-player .art-layer-lock{height:var(--art-lock-size);width:var(--art-lock-size);top:50%;left:var(--art-padding);background-color:var(--art-tip-background);border-radius:50%;justify-content:center;align-items:center;display:none;position:absolute;transform:translateY(-50%)}.art-video-player .art-layer-auto-playback{border-radius:var(--art-border-radius);left:var(--art-padding);bottom:calc(var(--art-control-height) + var(--art-bottom-gap) + 10px);background-color:var(--art-widget-background);align-items:center;gap:10px;padding:10px;line-height:1;display:none;position:absolute}.art-video-player .art-layer-auto-playback .art-auto-playback-close{cursor:pointer;justify-content:center;align-items:center;display:flex}.art-video-player .art-layer-auto-playback .art-auto-playback-close svg{width:15px;height:15px;fill:var(--art-theme)}.art-video-player .art-layer-auto-playback .art-auto-playback-jump{color:var(--art-theme);cursor:pointer}.art-video-player.art-lock .art-bottom{display:none!important}.art-video-player.art-lock .art-subtitle{bottom:var(--art-subtitle-bottom)!important}.art-video-player.art-lock .art-layer-mini-progress-bar{display:flex!important}.art-video-player.art-control-show .art-layer-mini-progress-bar{display:none}.art-video-player.art-control-show .art-layer-lock{display:flex}.art-control-selector{position:relative}.art-control-selector .art-selector-list{text-align:center;border-radius:var(--art-border-radius);opacity:0;pointer-events:none;bottom:var(--art-control-height);max-height:var(--art-selector-max-height);background-color:var(--art-widget-background);transition:all var(--art-transition-duration)ease;flex-direction:column;align-items:center;display:flex;position:absolute;overflow:hidden auto;transform:translateY(10px)}.art-control-selector .art-selector-list .art-selector-item{width:100%;flex-shrink:0;justify-content:center;align-items:center;padding:10px 15px;line-height:1;display:flex}.art-control-selector .art-selector-list .art-selector-item:hover{background-color:#ffffff1a}.art-control-selector .art-selector-list .art-selector-item:hover,.art-control-selector .art-selector-list .art-selector-item.art-current{color:var(--art-theme)}.art-control-selector:hover .art-selector-list{opacity:1;pointer-events:auto;transform:translateY(0)}[class*=hint--]{font-style:normal;display:inline-block;position:relative}[class*=hint--]:before,[class*=hint--]:after{visibility:hidden;opacity:0;z-index:1000000;pointer-events:none;transition:all .3s;position:absolute;transform:translate(0,0)}[class*=hint--]:hover:before,[class*=hint--]:hover:after{visibility:visible;opacity:1;transition-delay:.1s}[class*=hint--]:before{content:"";z-index:1000001;background:0 0;border:6px solid #0000;position:absolute}[class*=hint--]:after{color:#fff;white-space:nowrap;background:#000;padding:8px 10px;font-family:Helvetica Neue,Helvetica,Arial,sans-serif;font-size:12px;line-height:12px}[class*=hint--][aria-label]:after{content:attr(aria-label)}[class*=hint--][data-hint]:after{content:attr(data-hint)}[aria-label=""]:before,[aria-label=""]:after,[data-hint=""]:before,[data-hint=""]:after{display:none!important}.hint--top-left:before,.hint--top-right:before,.hint--top:before{border-top-color:#000}.hint--bottom-left:before,.hint--bottom-right:before,.hint--bottom:before{border-bottom-color:#000}.hint--left:before{border-left-color:#000}.hint--right:before{border-right-color:#000}.hint--top:before{margin-bottom:-11px}.hint--top:before,.hint--top:after{bottom:100%;left:50%}.hint--top:before{left:calc(50% - 6px)}.hint--top:after{transform:translate(-50%)}.hint--top:hover:before{transform:translateY(-8px)}.hint--top:hover:after{transform:translate(-50%)translateY(-8px)}.hint--bottom:before{margin-top:-11px}.hint--bottom:before,.hint--bottom:after{top:100%;left:50%}.hint--bottom:before{left:calc(50% - 6px)}.hint--bottom:after{transform:translate(-50%)}.hint--bottom:hover:before{transform:translateY(8px)}.hint--bottom:hover:after{transform:translate(-50%)translateY(8px)}.hint--right:before{margin-bottom:-6px;margin-left:-11px}.hint--right:after{margin-bottom:-14px}.hint--right:before,.hint--right:after{bottom:50%;left:100%}.hint--right:hover:before,.hint--right:hover:after{transform:translate(8px)}.hint--left:before{margin-bottom:-6px;margin-right:-11px}.hint--left:after{margin-bottom:-14px}.hint--left:before,.hint--left:after{bottom:50%;right:100%}.hint--left:hover:before,.hint--left:hover:after{transform:translate(-8px)}.hint--top-left:before{margin-bottom:-11px}.hint--top-left:before,.hint--top-left:after{bottom:100%;left:50%}.hint--top-left:before{left:calc(50% - 6px)}.hint--top-left:after{margin-left:12px;transform:translate(-100%)}.hint--top-left:hover:before{transform:translateY(-8px)}.hint--top-left:hover:after{transform:translate(-100%)translateY(-8px)}.hint--top-right:before{margin-bottom:-11px}.hint--top-right:before,.hint--top-right:after{bottom:100%;left:50%}.hint--top-right:before{left:calc(50% - 6px)}.hint--top-right:after{margin-left:-12px;transform:translate(0)}.hint--top-right:hover:before,.hint--top-right:hover:after{transform:translateY(-8px)}.hint--bottom-left:before{margin-top:-11px}.hint--bottom-left:before,.hint--bottom-left:after{top:100%;left:50%}.hint--bottom-left:before{left:calc(50% - 6px)}.hint--bottom-left:after{margin-left:12px;transform:translate(-100%)}.hint--bottom-left:hover:before{transform:translateY(8px)}.hint--bottom-left:hover:after{transform:translate(-100%)translateY(8px)}.hint--bottom-right:before{margin-top:-11px}.hint--bottom-right:before,.hint--bottom-right:after{top:100%;left:50%}.hint--bottom-right:before{left:calc(50% - 6px)}.hint--bottom-right:after{margin-left:-12px;transform:translate(0)}.hint--bottom-right:hover:before,.hint--bottom-right:hover:after{transform:translateY(8px)}.hint--small:after,.hint--medium:after,.hint--large:after{white-space:normal;word-wrap:break-word;line-height:1.4em}.hint--small:after{width:80px}.hint--medium:after{width:150px}.hint--large:after{width:300px}[class*=hint--]:after{text-shadow:0 -1px #000;box-shadow:4px 4px 8px #0000004d}.hint--error:after{text-shadow:0 -1px #592726;background-color:#b34e4d}.hint--error.hint--top-left:before,.hint--error.hint--top-right:before,.hint--error.hint--top:before{border-top-color:#b34e4d}.hint--error.hint--bottom-left:before,.hint--error.hint--bottom-right:before,.hint--error.hint--bottom:before{border-bottom-color:#b34e4d}.hint--error.hint--left:before{border-left-color:#b34e4d}.hint--error.hint--right:before{border-right-color:#b34e4d}.hint--warning:after{text-shadow:0 -1px #6c5328;background-color:#c09854}.hint--warning.hint--top-left:before,.hint--warning.hint--top-right:before,.hint--warning.hint--top:before{border-top-color:#c09854}.hint--warning.hint--bottom-left:before,.hint--warning.hint--bottom-right:before,.hint--warning.hint--bottom:before{border-bottom-color:#c09854}.hint--warning.hint--left:before{border-left-color:#c09854}.hint--warning.hint--right:before{border-right-color:#c09854}.hint--info:after{text-shadow:0 -1px #1a3c4d;background-color:#3986ac}.hint--info.hint--top-left:before,.hint--info.hint--top-right:before,.hint--info.hint--top:before{border-top-color:#3986ac}.hint--info.hint--bottom-left:before,.hint--info.hint--bottom-right:before,.hint--info.hint--bottom:before{border-bottom-color:#3986ac}.hint--info.hint--left:before{border-left-color:#3986ac}.hint--info.hint--right:before{border-right-color:#3986ac}.hint--success:after{text-shadow:0 -1px #1a321a;background-color:#458746}.hint--success.hint--top-left:before,.hint--success.hint--top-right:before,.hint--success.hint--top:before{border-top-color:#458746}.hint--success.hint--bottom-left:before,.hint--success.hint--bottom-right:before,.hint--success.hint--bottom:before{border-bottom-color:#458746}.hint--success.hint--left:before{border-left-color:#458746}.hint--success.hint--right:before{border-right-color:#458746}.hint--always:after,.hint--always:before{opacity:1;visibility:visible}.hint--always.hint--top:before{transform:translateY(-8px)}.hint--always.hint--top:after{transform:translate(-50%)translateY(-8px)}.hint--always.hint--top-left:before{transform:translateY(-8px)}.hint--always.hint--top-left:after{transform:translate(-100%)translateY(-8px)}.hint--always.hint--top-right:before,.hint--always.hint--top-right:after{transform:translateY(-8px)}.hint--always.hint--bottom:before{transform:translateY(8px)}.hint--always.hint--bottom:after{transform:translate(-50%)translateY(8px)}.hint--always.hint--bottom-left:before{transform:translateY(8px)}.hint--always.hint--bottom-left:after{transform:translate(-100%)translateY(8px)}.hint--always.hint--bottom-right:before,.hint--always.hint--bottom-right:after{transform:translateY(8px)}.hint--always.hint--left:before,.hint--always.hint--left:after{transform:translate(-8px)}.hint--always.hint--right:before,.hint--always.hint--right:after{transform:translate(8px)}.hint--rounded:after{border-radius:4px}.hint--no-animate:before,.hint--no-animate:after{transition-duration:0s}.hint--bounce:before,.hint--bounce:after{-webkit-transition:opacity .3s,visibility .3s,-webkit-transform .3s cubic-bezier(.71,1.7,.77,1.24);-moz-transition:opacity .3s,visibility .3s,-moz-transform .3s cubic-bezier(.71,1.7,.77,1.24);transition:opacity .3s,visibility .3s,transform .3s cubic-bezier(.71,1.7,.77,1.24)}.hint--no-shadow:before,.hint--no-shadow:after{text-shadow:initial;box-shadow:initial}.hint--no-arrow:before{display:none}.art-video-player.art-mobile{--art-bottom-gap:10px;--art-control-height:38px;--art-control-icon-scale:1;--art-state-size:60px;--art-settings-max-height:180px;--art-selector-max-height:180px;--art-indicator-scale:1;--art-control-opacity:1}.art-video-player.art-mobile .art-controls-left{margin-left:calc(var(--art-padding)/-1)}.art-video-player.art-mobile .art-controls-right{margin-right:calc(var(--art-padding)/-1)}'},{}],bAWi2:[function(e,t,r){t.exports=function(){"use strict";function e(t){return(e="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(t)}var t=Object.prototype.toString,r=function(r){if(void 0===r)return"undefined";if(null===r)return"null";var o=e(r);if("boolean"===o)return"boolean";if("string"===o)return"string";if("number"===o)return"number";if("symbol"===o)return"symbol";if("function"===o)return function(e){return"GeneratorFunction"===a(e)}(r)?"generatorfunction":"function";if(function(e){return Array.isArray?Array.isArray(e):e instanceof Array}(r))return"array";if(function(e){return!(!e.constructor||"function"!=typeof e.constructor.isBuffer)&&e.constructor.isBuffer(e)}(r))return"buffer";if(function(e){try{if("number"==typeof e.length&&"function"==typeof e.callee)return!0}catch(e){if(-1!==e.message.indexOf("callee"))return!0}return!1}(r))return"arguments";if(function(e){return e instanceof Date||"function"==typeof e.toDateString&&"function"==typeof e.getDate&&"function"==typeof e.setDate}(r))return"date";if(function(e){return e instanceof Error||"string"==typeof e.message&&e.constructor&&"number"==typeof e.constructor.stackTraceLimit}(r))return"error";if(function(e){return e instanceof RegExp||"string"==typeof e.flags&&"boolean"==typeof e.ignoreCase&&"boolean"==typeof e.multiline&&"boolean"==typeof e.global}(r))return"regexp";switch(a(r)){case"Symbol":return"symbol";case"Promise":return"promise";case"WeakMap":return"weakmap";case"WeakSet":return"weakset";case"Map":return"map";case"Set":return"set";case"Int8Array":return"int8array";case"Uint8Array":return"uint8array";case"Uint8ClampedArray":return"uint8clampedarray";case"Int16Array":return"int16array";case"Uint16Array":return"uint16array";case"Int32Array":return"int32array";case"Uint32Array":return"uint32array";case"Float32Array":return"float32array";case"Float64Array":return"float64array"}if(function(e){return"function"==typeof e.throw&&"function"==typeof e.return&&"function"==typeof e.next}(r))return"generator";switch(o=t.call(r)){case"[object Object]":return"object";case"[object Map Iterator]":return"mapiterator";case"[object Set Iterator]":return"setiterator";case"[object String Iterator]":return"stringiterator";case"[object Array Iterator]":return"arrayiterator"}return o.slice(8,-1).toLowerCase().replace(/\s/g,"")};function a(e){return e.constructor?e.constructor.name:null}function o(e,t){var a=2n)),a.export(r,"queryAll",(()=>i)),a.export(r,"addClass",(()=>s)),a.export(r,"removeClass",(()=>l)),a.export(r,"hasClass",(()=>c)),a.export(r,"append",(()=>p)),a.export(r,"remove",(()=>u)),a.export(r,"setStyle",(()=>d)),a.export(r,"setStyles",(()=>f)),a.export(r,"getStyle",(()=>h)),a.export(r,"sublings",(()=>m)),a.export(r,"inverseClass",(()=>g)),a.export(r,"tooltip",(()=>v)),a.export(r,"isInViewport",(()=>y)),a.export(r,"includeFromEvent",(()=>b)),a.export(r,"replaceElement",(()=>x)),a.export(r,"createElement",(()=>w)),a.export(r,"getIcon",(()=>j));var o=e("./compatibility");function n(e,t=document){return t.querySelector(e)}function i(e,t=document){return Array.from(t.querySelectorAll(e))}function s(e,t){return e.classList.add(t)}function l(e,t){return e.classList.remove(t)}function c(e,t){return e.classList.contains(t)}function p(e,t){return t instanceof Element?e.appendChild(t):e.insertAdjacentHTML("beforeend",String(t)),e.lastElementChild||e.lastChild}function u(e){return e.parentNode.removeChild(e)}function d(e,t,r){return e.style[t]=r,e}function f(e,t){for(const r in t)d(e,r,t[r]);return e}function h(e,t,r=!0){const a=window.getComputedStyle(e,null).getPropertyValue(t);return r?parseFloat(a):a}function m(e){return Array.from(e.parentElement.children).filter((t=>t!==e))}function g(e,t){m(e).forEach((e=>l(e,t))),s(e,t)}function v(e,t,r="top"){o.isMobile||(e.setAttribute("aria-label",t),s(e,"hint--rounded"),s(e,`hint--${r}`))}function y(e,t=0){const r=e.getBoundingClientRect(),a=window.innerHeight||document.documentElement.clientHeight,o=window.innerWidth||document.documentElement.clientWidth,n=r.top-t<=a&&r.top+r.height+t>=0,i=r.left-t<=o+t&&r.left+r.width+t>=0;return n&&i}function b(e,t){return e.composedPath&&e.composedPath().indexOf(t)>-1}function x(e,t){return t.parentNode.replaceChild(e,t),e}function w(e){return document.createElement(e)}function j(e="",t=""){const r=w("i");return s(r,"art-icon"),s(r,`art-icon-${e}`),p(r,t),r}},{"./compatibility":"6ZTr6","@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],"6ZTr6":[function(e,t,r){var a=e("@parcel/transformer-js/src/esmodule-helpers.js");a.defineInteropFlag(r),a.export(r,"userAgent",(()=>o)),a.export(r,"isSafari",(()=>n)),a.export(r,"isWechat",(()=>i)),a.export(r,"isIE",(()=>s)),a.export(r,"isAndroid",(()=>l)),a.export(r,"isIOS",(()=>c)),a.export(r,"isIOS13",(()=>p)),a.export(r,"isMobile",(()=>u));const o="undefined"!=typeof navigator?navigator.userAgent:"",n=/^((?!chrome|android).)*safari/i.test(o),i=/MicroMessenger/i.test(o),s=/MSIE|Trident/i.test(o),l=/android/i.test(o),c=/iPad|iPhone|iPod/i.test(o)&&!window.MSStream,p=c||o.includes("Macintosh")&&navigator.maxTouchPoints>=1,u=/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(o)||p},{"@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],hwmZz:[function(e,t,r){var a=e("@parcel/transformer-js/src/esmodule-helpers.js");a.defineInteropFlag(r),a.export(r,"ArtPlayerError",(()=>o)),a.export(r,"errorHandle",(()=>n));class o extends Error{constructor(e,t){super(e),"function"==typeof Error.captureStackTrace&&Error.captureStackTrace(this,t||this.constructor),this.name="ArtPlayerError"}}function n(e,t){if(!e)throw new o(t);return e}},{"@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],inzwq:[function(e,t,r){var a=e("@parcel/transformer-js/src/esmodule-helpers.js");function o(e){return"WEBVTT \r\n\r\n".concat((t=e,t.replace(/(\d\d:\d\d:\d\d)[,.](\d+)/g,((e,t,r)=>{let a=r.slice(0,3);return 1===r.length&&(a=r+"00"),2===r.length&&(a=r+"0"),`${t},${a}`}))).replace(/\{\\([ibu])\}/g,"").replace(/\{\\([ibu])1\}/g,"<$1>").replace(/\{([ibu])\}/g,"<$1>").replace(/\{\/([ibu])\}/g,"").replace(/(\d\d:\d\d:\d\d),(\d\d\d)/g,"$1.$2").replace(/{[\s\S]*?}/g,"").concat("\r\n\r\n"));var t}function n(e){return URL.createObjectURL(new Blob([e],{type:"text/vtt"}))}function i(e){const t=new RegExp("Dialogue:\\s\\d,(\\d+:\\d\\d:\\d\\d.\\d\\d),(\\d+:\\d\\d:\\d\\d.\\d\\d),([^,]*),([^,]*),(?:[^,]*,){4}([\\s\\S]*)$","i");function r(e=""){return e.split(/[:.]/).map(((e,t,r)=>{if(t===r.length-1){if(1===e.length)return`.${e}00`;if(2===e.length)return`.${e}0`}else if(1===e.length)return(0===t?"0":":0")+e;return 0===t?e:t===r.length-1?`.${e}`:`:${e}`})).join("")}return`WEBVTT\n\n${e.split(/\r?\n/).map((e=>{const a=e.match(t);return a?{start:r(a[1].trim()),end:r(a[2].trim()),text:a[5].replace(/{[\s\S]*?}/g,"").replace(/(\\N)/g,"\n").trim().split(/\r?\n/).map((e=>e.trim())).join("\n")}:null})).filter((e=>e)).map(((e,t)=>e?`${t+1}\n${e.start} --\x3e ${e.end}\n${e.text}`:"")).filter((e=>e.trim())).join("\n\n")}`}a.defineInteropFlag(r),a.export(r,"srtToVtt",(()=>o)),a.export(r,"vttToBlob",(()=>n)),a.export(r,"assToVtt",(()=>i))},{"@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],"6b7Ip":[function(e,t,r){var a=e("@parcel/transformer-js/src/esmodule-helpers.js");function o(e){return e.includes("?")?o(e.split("?")[0]):e.includes("#")?o(e.split("#")[0]):e.trim().toLowerCase().split(".").pop()}function n(e,t){const r=document.createElement("a");r.style.display="none",r.href=e,r.download=t,document.body.appendChild(r),r.click(),document.body.removeChild(r)}a.defineInteropFlag(r),a.export(r,"getExt",(()=>o)),a.export(r,"download",(()=>n))},{"@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],"5NSdr":[function(e,t,r){var a=e("@parcel/transformer-js/src/esmodule-helpers.js");a.defineInteropFlag(r),a.export(r,"def",(()=>o)),a.export(r,"has",(()=>i)),a.export(r,"get",(()=>s)),a.export(r,"mergeDeep",(()=>l));const o=Object.defineProperty,{hasOwnProperty:n}=Object.prototype;function i(e,t){return n.call(e,t)}function s(e,t){return Object.getOwnPropertyDescriptor(e,t)}function l(...e){const t=e=>e&&"object"==typeof e&&!Array.isArray(e);return e.reduce(((e,r)=>(Object.keys(r).forEach((a=>{const o=e[a],n=r[a];Array.isArray(o)&&Array.isArray(n)?e[a]=o.concat(...n):t(o)&&t(n)?e[a]=l(o,n):e[a]=n})),e)),{})}},{"@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],epmNy:[function(e,t,r){var a=e("@parcel/transformer-js/src/esmodule-helpers.js");function o(e=0){return new Promise((t=>setTimeout(t,e)))}function n(e,t){let r;return function(...a){clearTimeout(r),r=setTimeout((()=>(r=null,e.apply(this,a))),t)}}function i(e,t){let r=!1;return function(...a){r||(e.apply(this,a),r=!0,setTimeout((function(){r=!1}),t))}}a.defineInteropFlag(r),a.export(r,"sleep",(()=>o)),a.export(r,"debounce",(()=>n)),a.export(r,"throttle",(()=>i))},{"@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],gapRl:[function(e,t,r){var a=e("@parcel/transformer-js/src/esmodule-helpers.js");function o(e,t,r){return Math.max(Math.min(e,Math.max(t,r)),Math.min(t,r))}function n(e){return e.charAt(0).toUpperCase()+e.slice(1)}function i(e){return["string","number"].includes(typeof e)}function s(e){const t=Math.floor(e/3600),r=Math.floor((e-3600*t)/60),a=Math.floor(e-3600*t-60*r);return(t>0?[t,r,a]:[r,a]).map((e=>e<10?`0${e}`:String(e))).join(":")}function l(e){return e.replace(/[&<>'"]/g,(e=>({"&":"&","<":"<",">":">","'":"'",'"':"""}[e]||e)))}a.defineInteropFlag(r),a.export(r,"clamp",(()=>o)),a.export(r,"capitalize",(()=>n)),a.export(r,"isStringOrNumber",(()=>i)),a.export(r,"secondToTime",(()=>s)),a.export(r,"escape",(()=>l))},{"@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],AKEiO:[function(e,t,r){var a=e("@parcel/transformer-js/src/esmodule-helpers.js");a.defineInteropFlag(r),a.export(r,"ComponentOption",(()=>d));var o=e("../utils");const n="array",i="boolean",s="string",l="number",c="object",p="function";function u(e,t,r){return(0,o.errorHandle)(t===s||t===l||e instanceof Element,`${r.join(".")} require '${s}' or 'Element' type`)}const d={html:u,disable:`?${i}`,name:`?${s}`,index:`?${l}`,style:`?${c}`,click:`?${p}`,mounted:`?${p}`,tooltip:`?${s}|${l}`,width:`?${l}`,selector:`?${n}`,onSelect:`?${p}`,switch:`?${i}`,onSwitch:`?${p}`,range:`?${n}`,onRange:`?${p}`,onChange:`?${p}`};r.default={id:s,container:u,url:s,poster:s,type:s,theme:s,lang:s,volume:l,isLive:i,muted:i,autoplay:i,autoSize:i,autoMini:i,loop:i,flip:i,playbackRate:i,aspectRatio:i,screenshot:i,setting:i,hotkey:i,pip:i,mutex:i,backdrop:i,fullscreen:i,fullscreenWeb:i,subtitleOffset:i,miniProgressBar:i,useSSR:i,playsInline:i,lock:i,fastForward:i,autoPlayback:i,autoOrientation:i,airplay:i,plugins:[p],layers:[d],contextmenu:[d],settings:[d],controls:[{...d,position:(e,t,r)=>{const a=["top","left","right"];return(0,o.errorHandle)(a.includes(e),`${r.join(".")} only accept ${a.toString()} as parameters`)}}],quality:[{default:`?${i}`,html:s,url:s}],highlight:[{time:l,text:s}],thumbnails:{url:s,number:l,column:l,width:l,height:l},subtitle:{url:s,type:s,style:c,escape:i,encoding:s,onVttLoad:p},moreVideoAttr:c,i18n:c,icons:c,cssVar:c,customType:c}},{"../utils":"71aH7","@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],lyjeQ:[function(e,t,r){e("@parcel/transformer-js/src/esmodule-helpers.js").defineInteropFlag(r),r.default={propertys:["audioTracks","autoplay","buffered","controller","controls","crossOrigin","currentSrc","currentTime","defaultMuted","defaultPlaybackRate","duration","ended","error","loop","mediaGroup","muted","networkState","paused","playbackRate","played","preload","readyState","seekable","seeking","src","startDate","textTracks","videoTracks","volume"],methods:["addTextTrack","canPlayType","load","play","pause"],events:["abort","canplay","canplaythrough","durationchange","emptied","ended","error","loadeddata","loadedmetadata","loadstart","pause","play","playing","progress","ratechange","seeked","seeking","stalled","suspend","timeupdate","volumechange","waiting"],prototypes:["width","height","videoWidth","videoHeight","poster","webkitDecodedFrameCount","webkitDroppedFrameCount","playsInline","webkitSupportsFullscreen","webkitDisplayingFullscreen","onenterpictureinpicture","onleavepictureinpicture","disablePictureInPicture","cancelVideoFrameCallback","requestVideoFrameCallback","getVideoPlaybackQuality","requestPictureInPicture","webkitEnterFullScreen","webkitEnterFullscreen","webkitExitFullScreen","webkitExitFullscreen"]}},{"@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],X13Zf:[function(e,t,r){e("@parcel/transformer-js/src/esmodule-helpers.js").defineInteropFlag(r);var a=e("./utils");class o{constructor(e){this.art=e;const{option:t,constructor:r}=e;t.container instanceof Element?this.$container=t.container:(this.$container=(0,a.query)(t.container),(0,a.errorHandle)(this.$container,`No container element found by ${t.container}`));const o=this.$container.tagName.toLowerCase();(0,a.errorHandle)("div"===o,`Unsupported container element type, only support 'div' but got '${o}'`),(0,a.errorHandle)(r.instances.every((e=>e.template.$container!==this.$container)),"Cannot mount multiple instances on the same dom element"),this.query=this.query.bind(this),this.$container.dataset.artId=e.id,this.$original=this.$container.cloneNode(!0),this.init()}static get html(){return'
    Player version:
    5.0.9
    Video url:
    Video volume:
    Video time:
    Video duration:
    Video resolution:
    x
    [x]
    '}query(e){return(0,a.query)(e,this.$container)}init(){const{option:e}=this.art;e.useSSR||(this.$container.innerHTML=o.html),this.$player=this.query(".art-video-player"),this.$video=this.query(".art-video"),this.$track=this.query("track"),this.$poster=this.query(".art-poster"),this.$subtitle=this.query(".art-subtitle"),this.$danmuku=this.query(".art-danmuku"),this.$bottom=this.query(".art-bottom"),this.$progress=this.query(".art-progress"),this.$controls=this.query(".art-controls"),this.$controlsLeft=this.query(".art-controls-left"),this.$controlsCenter=this.query(".art-controls-center"),this.$controlsRight=this.query(".art-controls-right"),this.$layer=this.query(".art-layers"),this.$loading=this.query(".art-loading"),this.$notice=this.query(".art-notice"),this.$noticeInner=this.query(".art-notice-inner"),this.$mask=this.query(".art-mask"),this.$state=this.query(".art-state"),this.$setting=this.query(".art-settings"),this.$info=this.query(".art-info"),this.$infoPanel=this.query(".art-info-panel"),this.$infoClose=this.query(".art-info-close"),this.$contextmenu=this.query(".art-contextmenus"),e.backdrop&&(0,a.addClass)(this.$player,"art-backdrop"),a.isMobile&&(0,a.addClass)(this.$player,"art-mobile")}destroy(e){e?(0,a.replaceElement)(this.$original,this.$container):(0,a.addClass)(this.$player,"art-destroy")}}r.default=o},{"./utils":"71aH7","@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],"3jKkj":[function(e,t,r){var a=e("@parcel/transformer-js/src/esmodule-helpers.js");a.defineInteropFlag(r);var o=e("../utils"),n=e("./zh-cn.json"),i=a.interopDefault(n),s=e("./zh-tw.json"),l=a.interopDefault(s),c=e("./pl.json"),p=a.interopDefault(c),u=e("./cs.json"),d=a.interopDefault(u),f=e("./es.json"),h=a.interopDefault(f),m=e("./fa.json"),g=a.interopDefault(m),v=e("./fr.json"),y=a.interopDefault(v),b=e("./id.json"),x=a.interopDefault(b),w=e("./ru.json"),j=a.interopDefault(w);r.default=class{constructor(e){this.art=e,this.languages={"zh-cn":i.default,"zh-tw":l.default,pl:p.default,cs:d.default,es:h.default,fa:g.default,fr:y.default,id:x.default,ru:j.default},this.update(e.option.i18n)}init(){const e=this.art.option.lang.toLowerCase();this.language=this.languages[e]||{}}get(e){return this.language[e]||e}update(e){this.languages=(0,o.mergeDeep)(this.languages,e),this.init()}}},{"../utils":"71aH7","./zh-cn.json":"lNQi5","./zh-tw.json":"eRpom","./pl.json":"iEpPa","./cs.json":"dBgp3","./es.json":"dNIrL","./fa.json":"7Plhe","./fr.json":"kGNjI","./id.json":"6MQTw","./ru.json":"7LASr","@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],lNQi5:[function(e,t,r){t.exports=JSON.parse('{"Video Info":"统计信息","Close":"关闭","Video Load Failed":"加载失败","Volume":"音量","Play":"播放","Pause":"暂停","Rate":"速度","Mute":"静音","Video Flip":"画面翻转","Horizontal":"水平","Vertical":"垂直","Reconnect":"重新连接","Show Setting":"显示设置","Hide Setting":"隐藏设置","Screenshot":"截图","Play Speed":"播放速度","Aspect Ratio":"画面比例","Default":"默认","Normal":"正常","Open":"打开","Switch Video":"切换","Switch Subtitle":"切换字幕","Fullscreen":"全屏","Exit Fullscreen":"退出全屏","Web Fullscreen":"网页全屏","Exit Web Fullscreen":"退出网页全屏","Mini Player":"迷你播放器","PIP Mode":"开启画中画","Exit PIP Mode":"退出画中画","PIP Not Supported":"不支持画中画","Fullscreen Not Supported":"不支持全屏","Subtitle Offset":"字幕偏移","Last Seen":"上次看到","Jump Play":"跳转播放","AirPlay":"隔空播放","AirPlay Not Available":"隔空播放不可用"}')},{}],eRpom:[function(e,t,r){t.exports=JSON.parse('{"Video Info":"統計訊息","Close":"關閉","Video Load Failed":"載入失敗","Volume":"音量","Play":"播放","Pause":"暫停","Rate":"速度","Mute":"靜音","Video Flip":"畫面翻轉","Horizontal":"水平","Vertical":"垂直","Reconnect":"重新連接","Show Setting":"顯示设置","Hide Setting":"隱藏设置","Screenshot":"截圖","Play Speed":"播放速度","Aspect Ratio":"畫面比例","Default":"默認","Normal":"正常","Open":"打開","Switch Video":"切換","Switch Subtitle":"切換字幕","Fullscreen":"全屏","Exit Fullscreen":"退出全屏","Web Fullscreen":"網頁全屏","Exit Web Fullscreen":"退出網頁全屏","Mini Player":"迷你播放器","PIP Mode":"開啟畫中畫","Exit PIP Mode":"退出畫中畫","PIP Not Supported":"不支持畫中畫","Fullscreen Not Supported":"不支持全屏","Subtitle Offset":"字幕偏移","Last Seen":"上次看到","Jump Play":"跳轉播放","AirPlay":"隔空播放","AirPlay Not Available":"隔空播放不可用"}')},{}],iEpPa:[function(e,t,r){t.exports=JSON.parse('{"Video Info":"Informacje o wideo","Close":"Zamknij","Video Load Failed":"Błąd ładowania wideo","Volume":"Głośność","Play":"Odtwórz","Pause":"Wstrzymaj","Rate":"Oceń","Mute":"Wycisz","Video Flip":"Rotacja wideo","Horizontal":"Pozioma","Vertical":"Pionowa","Reconnect":"Połącz ponownie","Show Setting":"Pokaż ustawienia","Hide Setting":"Ukryj ustawienia","Screenshot":"Zrzut ekranu","Play Speed":"Prędkość odtwarzania","Aspect Ratio":"Współczynnik proporcji","Default":"Domyślny","Normal":"Normalny","Open":"Otwórz","Switch Video":"Przełącz wideo","Switch Subtitle":"Przełącz napisy","Fullscreen":"Pełny ekran","Exit Fullscreen":"Zamknij pełny ekran","Web Fullscreen":"Tryb pełnej strony","Exit Web Fullscreen":"Zamknij tryb pełnej strony","Mini Player":"Miniodtwarzacz","PIP Mode":"Tryb PiP","Exit PIP Mode":"Zamknij tryb PiP","PIP Not Supported":"Tryb PiP nieobsługiwany","Fullscreen Not Supported":"Pełny ekran nieobsługiwany","Subtitle Offset":"Przesunięcie napisów","Last Seen":"Ostatnio widziany","Jump Play":"Skocz do gry","AirPlay":"AirPlay","AirPlay Not Available":"AirPlay nie jest dostępny"}')},{}],dBgp3:[function(e,t,r){t.exports=JSON.parse('{"Video Info":"Info o videu","Close":"Zavřít","Video Load Failed":"Nahrání videa selhalo","Volume":"Hlasitost","Play":"Přehrát","Pause":"Pozastavit","Rate":"Hodnocení","Mute":"Ztlumit","Video Flip":"Otočit video","Horizontal":"Horizontálně","Vertical":"Vertikálně","Reconnect":"Opětovné připojení","Show Setting":"Zobrazit nastavení","Hide Setting":"Skrýt nastavení","Screenshot":"Snímek obrazovky","Play Speed":"Rychlost přehrávání","Aspect Ratio":"Poměr stran","Default":"Výchozí","Normal":"Normální","Open":"Otevřít","Switch Video":"Přepnout video","Switch Subtitle":"Přepnout titulky","Fullscreen":"Celá obrazovka","Exit Fullscreen":"Opustit režim celé obrazovky","Web Fullscreen":"Celá stránka","Exit Web Fullscreen":"Zavřít režim celé stránky","Mini Player":"Mini přehrávač","PIP Mode":"Režim PIP","Exit PIP Mode":"Opustit režim PIP","PIP Not Supported":"Režim PIP není podporován","Fullscreen Not Supported":"Režim celé obrazovky není podporován","Subtitle Offset":"Posun titulků","Last Seen":"Naposledy viděn","Jump Play":"Hra na skok","AirPlay":"AirPlay","AirPlay Not Available":"AirPlay není k dispozici"}')},{}],dNIrL:[function(e,t,r){t.exports=JSON.parse('{"Video Info":"Información del video","Close":"Cerrar","Video Load Failed":"Falló carga de video","Volume":"Volumen","Play":"Reproduciendo","Pause":"Pausa","Rate":"Velocidad","Mute":"Silencio","Video Flip":"Rotar video","Horizontal":"Horizontal","Vertical":"Vertical","Reconnect":"Reconectando","Show Setting":"Mostrar ajustes","Hide Setting":"Ocultar ajustes","Screenshot":"Captura de Pantalla","Play Speed":"Velocidad de reproducción","Aspect Ratio":"Relación de aspecto","Default":"Por defecto","Normal":"Normal","Open":"Abrir","Switch Video":"Cambiar video","Switch Subtitle":"Cambiar subtítulo","Fullscreen":"Pantalla completa","Exit Fullscreen":"Salir de Pantalla completa","Web Fullscreen":"Pantalla completa Web","Exit Web Fullscreen":"Salir de Pantalla completa","Mini Player":"Mini reproductor","PIP Mode":"Modo PiP","Exit PIP Mode":"Cerrar modo PiP","PIP Not Supported":"Modo PiP no compatible","Fullscreen Not Supported":"Pantalla completa no soportada","Subtitle Offset":"Ajuste subtítulo","Last Seen":"Visto última vez","Jump Play":"Saltar","AirPlay":"AirPlay","AirPlay Not Available":"AirPlay no disponible"}')},{}],"7Plhe":[function(e,t,r){t.exports=JSON.parse('{"Video Info":"اطلاعات ویدیو","Close":"بستن","Video Load Failed":"بارگذاری ناموفق","Play":"پخش","Volume":"میزان صدا","Pause":"توقف","Rate":"نرخ","Mute":"سکوت","Video Flip":"چرخش تصویر","Horizontal":"افقی","Vertical":"عمودی","Reconnect":"اتصال مجدد","Show Setting":"تنظیمات","Hide Setting":"بستن تنظیمات","Screenshot":"عکس از صفحه","Play Speed":"سرعت پخش","Aspect Ratio":"نسبت تصویر","Default":"حالت پیشفرض","Normal":" حالت عادی","Open":"بازکردن","Switch Video":"تغییر ویدیو","Switch Subtitle":"نغییر زیرنویس","Fullscreen":"تمام صفحه","Exit Fullscreen":"کوچک کردن","Web Fullscreen":"حالت تئاتر","Exit Web Fullscreen":"خروج از حالت تئاتر","Mini Player":"حالت پخش کوچک","PIP Mode":" مینی پلیر","Exit PIP Mode":"خروج از مینی پلیر","PIP Not Supported":"عدم پشتیبانی از مینی پلیر","Fullscreen Not Supported":"عدم پشتیبانی از حالت تمام صفحه","Subtitle Offset":"افست زیرنویس","Last Seen":"آخرین بازدید","Jump Play":"جامپ پلی","AirPlay":"ایر پلی","AirPlay Not Available":"عدم پشتیبانی از ایرپلی"}')},{}],kGNjI:[function(e,t,r){t.exports=JSON.parse('{"Video Info":"Informations de la vidéo","Close":"Fermer","Video Load Failed":"Téléchargement de la vidéo échoué","Volume":"Volume","Play":"Lire","Pause":"Pause","Rate":"Vitesse","Mute":"Muet","Video Flip":"Rotation de la vidéo","Horizontal":"Horizontal","Vertical":"Vertical","Reconnect":"Reconnexion","Show Setting":"Afficher les paramètres","Hide Setting":"Cacher les paramètres","Screenshot":"Capture d\'écran","Play Speed":"Vitesse de lecture","Aspect Ratio":"Rapport d\'aspect","Default":"Défaut","Normal":"Normal","Open":"Ouvrir","Switch Video":"Basculer la vidéo","Switch Subtitle":"Basculer le sous-titre","Fullscreen":"Plein écran","Exit Fullscreen":"Quitter le plein écran","Web Fullscreen":"Plein écran Web","Exit Web Fullscreen":"Quitter le plein écran Web","Mini Player":"Mini lecteur","PIP Mode":"Mode PiP","Exit PIP Mode":"Fermer le mode PiP","PIP Not Supported":"Mode PiP non supporté","Fullscreen Not Supported":"Plein écran non supporté","Subtitle Offset":"Réglage des sous-titres","Last Seen":"Dernière position","Jump Play":"Continuer","AirPlay":"AirPlay","AirPlay Not Available":"AirPlay non disponible"}')},{}],"6MQTw":[function(e,t,r){t.exports=JSON.parse('{"Video Info":"Informasi Video","Close":"Tutup","Video Load Failed":"Gagal Memuat Video","Volume":"Volume","Play":"Putar","Pause":"Jeda","Rate":"Kecepatan","Mute":"Senyap","Video Flip":"Memutar Video","Horizontal":"Horizontal","Vertical":"Vertikal","Reconnect":"Menyambung Kembali","Show Setting":"Tampilkan Pengaturan","Hide Setting":"Sembunyikan Pengaturan","Screenshot":"Tangkapan Layar","Play Speed":"Kecepatan Putar","Aspect Ratio":"Rasio Aspek","Default":"Default","Normal":"Normal","Open":"Buka","Switch Video":"Ganti Video","Switch Subtitle":"Ganti Subtitle","Fullscreen":"Layar Penuh","Exit Fullscreen":"Keluar dari Layar Penuh","Web Fullscreen":"Layar Penuh Web","Exit Web Fullscreen":"Keluar dari Layar Penuh Web","Mini Player":"Pemutar Mini","PIP Mode":"Mode PIP","Exit PIP Mode":"Keluar dari Mode PIP","PIP Not Supported":"PIP Tidak Didukung","Fullscreen Not Supported":"Layar Penuh Tidak Didukung","Subtitle Offset":"Pergeseran Subtitle","Last Seen":"Terakhir Dilihat","Jump Play":"Lompat Putar","AirPlay":"AirPlay","AirPlay Not Available":"AirPlay Tidak Tersedia"}')},{}],"7LASr":[function(e,t,r){t.exports=JSON.parse('{"Video Info":"Информация","Close":"Закрыть","Video Load Failed":"Ошибка загрузки видео","Volume":"Громкость","Play":"Играть","Pause":"Пауза","Rate":"Скорость","Mute":"Заглушить","Video Flip":"Развернуть видео","Horizontal":"Горизонтально","Vertical":"Вертикально","Reconnect":"Переподключенине","Show Setting":"Показать настройки","Hide Setting":"Скрыть настройки","Screenshot":"Скриншот","Play Speed":"Скорость воспроизведения","Aspect Ratio":"Соотношение сторон","Default":"По-умолчанию","Normal":"Нормальный","Open":"Открыть","Switch Video":"Переключить видео","Switch Subtitle":"Переключить субтитры","Fullscreen":"Полноэкранный режим","Exit Fullscreen":"Выход из полноэкранного режима","Web Fullscreen":"На все окно браузера","Exit Web Fullscreen":"Выход из режима полного окна","Mini Player":"Мини проигрыватель","PIP Mode":"Картинка в картинке","Exit PIP Mode":"Закрыть картинку в картинке","PIP Not Supported":"Картинка в картинке не поддерживается","Fullscreen Not Supported":"Полноэкранный режим не поддерживается","Subtitle Offset":"Настройка субтитров","Last Seen":"Последнее просмотренное","Jump Play":"Перейти","AirPlay":"AirPlay","AirPlay Not Available":"AirPlay недоступен"}')},{}],a90nx:[function(e,t,r){var a=e("@parcel/transformer-js/src/esmodule-helpers.js");a.defineInteropFlag(r);var o=e("./urlMix"),n=a.interopDefault(o),i=e("./attrMix"),s=a.interopDefault(i),l=e("./playMix"),c=a.interopDefault(l),p=e("./pauseMix"),u=a.interopDefault(p),d=e("./toggleMix"),f=a.interopDefault(d),h=e("./seekMix"),m=a.interopDefault(h),g=e("./volumeMix"),v=a.interopDefault(g),y=e("./currentTimeMix"),b=a.interopDefault(y),x=e("./durationMix"),w=a.interopDefault(x),j=e("./switchMix"),k=a.interopDefault(j),S=e("./playbackRateMix"),I=a.interopDefault(S),C=e("./aspectRatioMix"),P=a.interopDefault(C),$=e("./screenshotMix"),M=a.interopDefault($),T=e("./fullscreenMix"),E=a.interopDefault(T),F=e("./fullscreenWebMix"),A=a.interopDefault(F),z=e("./pipMix"),H=a.interopDefault(z),D=e("./loadedMix"),R=a.interopDefault(D),O=e("./playedMix"),L=a.interopDefault(O),V=e("./playingMix"),N=a.interopDefault(V),Y=e("./autoSizeMix"),_=a.interopDefault(Y),W=e("./rectMix"),q=a.interopDefault(W),B=e("./flipMix"),U=a.interopDefault(B),K=e("./miniMix"),G=a.interopDefault(K),Z=e("./loopMix"),X=a.interopDefault(Z),J=e("./posterMix"),Q=a.interopDefault(J),ee=e("./autoHeightMix"),te=a.interopDefault(ee),re=e("./cssVarMix"),ae=a.interopDefault(re),oe=e("./themeMix"),ne=a.interopDefault(oe),ie=e("./typeMix"),se=a.interopDefault(ie),le=e("./stateMix"),ce=a.interopDefault(le),pe=e("./subtitleOffsetMix"),ue=a.interopDefault(pe),de=e("./airplayMix"),fe=a.interopDefault(de),he=e("./qualityMix"),me=a.interopDefault(he),ge=e("./optionInit"),ve=a.interopDefault(ge),ye=e("./eventInit"),be=a.interopDefault(ye);r.default=class{constructor(e){(0,n.default)(e),(0,s.default)(e),(0,c.default)(e),(0,u.default)(e),(0,f.default)(e),(0,m.default)(e),(0,v.default)(e),(0,b.default)(e),(0,w.default)(e),(0,k.default)(e),(0,I.default)(e),(0,P.default)(e),(0,M.default)(e),(0,E.default)(e),(0,A.default)(e),(0,H.default)(e),(0,R.default)(e),(0,L.default)(e),(0,N.default)(e),(0,_.default)(e),(0,q.default)(e),(0,U.default)(e),(0,G.default)(e),(0,X.default)(e),(0,Q.default)(e),(0,te.default)(e),(0,ae.default)(e),(0,ne.default)(e),(0,se.default)(e),(0,ce.default)(e),(0,ue.default)(e),(0,fe.default)(e),(0,me.default)(e),(0,be.default)(e),(0,ve.default)(e)}}},{"./urlMix":"kQoac","./attrMix":"deCma","./playMix":"fOJuP","./pauseMix":"fzHAy","./toggleMix":"cBHxQ","./seekMix":"koAPr","./volumeMix":"6eyuR","./currentTimeMix":"faaWv","./durationMix":"5y91K","./switchMix":"iceD8","./playbackRateMix":"keKwh","./aspectRatioMix":"jihET","./screenshotMix":"36kPY","./fullscreenMix":"2GYOJ","./fullscreenWebMix":"5aYAP","./pipMix":"7EnIB","./loadedMix":"3N9mP","./playedMix":"et96R","./playingMix":"9DzzM","./autoSizeMix":"i1LDY","./rectMix":"IqARI","./flipMix":"7E7Vs","./miniMix":"gpugx","./loopMix":"f1hVG","./posterMix":"1SuFS","./autoHeightMix":"8x4te","./cssVarMix":"1CaTA","./themeMix":"2FqhO","./typeMix":"1fQQs","./stateMix":"iBOQW","./subtitleOffsetMix":"6vlBV","./airplayMix":"eftqT","./qualityMix":"5SdyX","./optionInit":"fCWZK","./eventInit":"f8Lv3","@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],kQoac:[function(e,t,r){e("@parcel/transformer-js/src/esmodule-helpers.js").defineInteropFlag(r);var a=e("../utils");r.default=function(e){const{option:t,template:{$video:r}}=e;(0,a.def)(e,"url",{get:()=>r.src,async set(o){if(o){const n=e.url,i=t.type||(0,a.getExt)(o),s=t.customType[i];i&&s?(await(0,a.sleep)(),e.loading.show=!0,s.call(e,r,o,e)):(URL.revokeObjectURL(n),r.src=o),n!==e.url&&(e.option.url=o,e.isReady&&n&&e.once("video:canplay",(()=>{e.emit("restart",o)})))}else await(0,a.sleep)(),e.loading.show=!0}})}},{"../utils":"71aH7","@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],deCma:[function(e,t,r){e("@parcel/transformer-js/src/esmodule-helpers.js").defineInteropFlag(r);var a=e("../utils");r.default=function(e){const{template:{$video:t}}=e;(0,a.def)(e,"attr",{value(e,r){if(void 0===r)return t[e];t[e]=r}})}},{"../utils":"71aH7","@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],fOJuP:[function(e,t,r){e("@parcel/transformer-js/src/esmodule-helpers.js").defineInteropFlag(r);var a=e("../utils");r.default=function(e){const{i18n:t,notice:r,option:o,constructor:{instances:n},template:{$video:i}}=e;(0,a.def)(e,"play",{value:async function(){const a=await i.play();if(r.show=t.get("Play"),e.emit("play"),o.mutex)for(let t=0;te.playing?e.pause():e.play()})}},{"../utils":"71aH7","@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],koAPr:[function(e,t,r){e("@parcel/transformer-js/src/esmodule-helpers.js").defineInteropFlag(r);var a=e("../utils");r.default=function(e){const{notice:t}=e;(0,a.def)(e,"seek",{set(r){e.currentTime=r,e.emit("seek",e.currentTime),e.duration&&(t.show=`${(0,a.secondToTime)(e.currentTime)} / ${(0,a.secondToTime)(e.duration)}`)}}),(0,a.def)(e,"forward",{set(t){e.seek=e.currentTime+t}}),(0,a.def)(e,"backward",{set(t){e.seek=e.currentTime-t}})}},{"../utils":"71aH7","@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],"6eyuR":[function(e,t,r){e("@parcel/transformer-js/src/esmodule-helpers.js").defineInteropFlag(r);var a=e("../utils");r.default=function(e){const{template:{$video:t},i18n:r,notice:o,storage:n}=e;(0,a.def)(e,"volume",{get:()=>t.volume||0,set:e=>{t.volume=(0,a.clamp)(e,0,1),o.show=`${r.get("Volume")}: ${parseInt(100*t.volume,10)}`,0!==t.volume&&n.set("volume",t.volume)}}),(0,a.def)(e,"muted",{get:()=>t.muted,set:e=>{t.muted=e}})}},{"../utils":"71aH7","@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],faaWv:[function(e,t,r){e("@parcel/transformer-js/src/esmodule-helpers.js").defineInteropFlag(r);var a=e("../utils");r.default=function(e){const{$video:t}=e.template;(0,a.def)(e,"currentTime",{get:()=>t.currentTime||0,set:r=>{r=parseFloat(r),Number.isNaN(r)||(t.currentTime=(0,a.clamp)(r,0,e.duration))}})}},{"../utils":"71aH7","@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],"5y91K":[function(e,t,r){e("@parcel/transformer-js/src/esmodule-helpers.js").defineInteropFlag(r);var a=e("../utils");r.default=function(e){(0,a.def)(e,"duration",{get:()=>{const{duration:t}=e.template.$video;return t===1/0?0:t||0}})}},{"../utils":"71aH7","@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],iceD8:[function(e,t,r){e("@parcel/transformer-js/src/esmodule-helpers.js").defineInteropFlag(r);var a=e("../utils");r.default=function(e){function t(t,r){return new Promise(((a,o)=>{if(t===e.url)return;const{playing:n,aspectRatio:i,playbackRate:s}=e;e.pause(),e.url=t,e.notice.show="",e.once("video:error",o),e.once("video:canplay",(async()=>{e.playbackRate=s,e.aspectRatio=i,e.currentTime=r,n&&await e.play(),e.notice.show="",a()}))}))}(0,a.def)(e,"switchQuality",{value:r=>t(r,e.currentTime)}),(0,a.def)(e,"switchUrl",{value:e=>t(e,0)}),(0,a.def)(e,"switch",{set:e.switchUrl})}},{"../utils":"71aH7","@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],keKwh:[function(e,t,r){e("@parcel/transformer-js/src/esmodule-helpers.js").defineInteropFlag(r);var a=e("../utils");r.default=function(e){const{template:{$video:t},i18n:r,notice:o}=e;(0,a.def)(e,"playbackRate",{get:()=>t.playbackRate,set(a){if(a){if(a===t.playbackRate)return;t.playbackRate=a,o.show=`${r.get("Rate")}: ${1===a?r.get("Normal"):`${a}x`}`}else e.playbackRate=1}})}},{"../utils":"71aH7","@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],jihET:[function(e,t,r){e("@parcel/transformer-js/src/esmodule-helpers.js").defineInteropFlag(r);var a=e("../utils");r.default=function(e){const{i18n:t,notice:r,template:{$video:o,$player:n}}=e;(0,a.def)(e,"aspectRatio",{get:()=>n.dataset.aspectRatio||"default",set(i){if(i||(i="default"),"default"===i)(0,a.setStyle)(o,"width",null),(0,a.setStyle)(o,"height",null),(0,a.setStyle)(o,"margin",null),delete n.dataset.aspectRatio;else{const e=i.split(":").map(Number),{clientWidth:t,clientHeight:r}=n,s=t/r,l=e[0]/e[1];s>l?((0,a.setStyle)(o,"width",l*r+"px"),(0,a.setStyle)(o,"height","100%"),(0,a.setStyle)(o,"margin","0 auto")):((0,a.setStyle)(o,"width","100%"),(0,a.setStyle)(o,"height",t/l+"px"),(0,a.setStyle)(o,"margin","auto 0")),n.dataset.aspectRatio=i}r.show=`${t.get("Aspect Ratio")}: ${"default"===i?t.get("Default"):i}`,e.emit("aspectRatio",i)}})}},{"../utils":"71aH7","@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],"36kPY":[function(e,t,r){e("@parcel/transformer-js/src/esmodule-helpers.js").defineInteropFlag(r);var a=e("../utils");r.default=function(e){const{notice:t,template:{$video:r}}=e,o=(0,a.createElement)("canvas");(0,a.def)(e,"getDataURL",{value:()=>new Promise(((e,a)=>{try{o.width=r.videoWidth,o.height=r.videoHeight,o.getContext("2d").drawImage(r,0,0),e(o.toDataURL("image/png"))}catch(e){t.show=e,a(e)}}))}),(0,a.def)(e,"getBlobUrl",{value:()=>new Promise(((e,a)=>{try{o.width=r.videoWidth,o.height=r.videoHeight,o.getContext("2d").drawImage(r,0,0),o.toBlob((t=>{e(URL.createObjectURL(t))}))}catch(e){t.show=e,a(e)}}))}),(0,a.def)(e,"screenshot",{value:async()=>{const t=await e.getDataURL();return(0,a.download)(t,`artplayer_${(0,a.secondToTime)(r.currentTime)}.png`),e.emit("screenshot",t),t}})}},{"../utils":"71aH7","@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],"2GYOJ":[function(e,t,r){var a=e("@parcel/transformer-js/src/esmodule-helpers.js");a.defineInteropFlag(r);var o=e("../libs/screenfull"),n=a.interopDefault(o),i=e("../utils");r.default=function(e){const{i18n:t,notice:r,template:{$video:a,$player:o}}=e;e.once("video:loadedmetadata",(()=>{n.default.isEnabled?(e=>{n.default.on("change",(()=>{e.emit("fullscreen",n.default.isFullscreen)})),(0,i.def)(e,"fullscreen",{get:()=>n.default.isFullscreen,async set(t){t?(e.state="fullscreen",await n.default.request(o),(0,i.addClass)(o,"art-fullscreen")):(await n.default.exit(),(0,i.removeClass)(o,"art-fullscreen")),e.emit("resize")}})})(e):document.fullscreenEnabled||a.webkitSupportsFullscreen?(e=>{(0,i.def)(e,"fullscreen",{get:()=>a.webkitDisplayingFullscreen,set(t){t?(e.state="fullscreen",a.webkitEnterFullscreen(),e.emit("fullscreen",!0)):(a.webkitExitFullscreen(),e.emit("fullscreen",!1)),e.emit("resize")}})})(e):(0,i.def)(e,"fullscreen",{get:()=>!1,set(){r.show=t.get("Fullscreen Not Supported")}}),(0,i.def)(e,"fullscreen",(0,i.get)(e,"fullscreen"))}))}},{"../libs/screenfull":"8v40z","../utils":"71aH7","@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],"8v40z":[function(e,t,r){e("@parcel/transformer-js/src/esmodule-helpers.js").defineInteropFlag(r);const a=[["requestFullscreen","exitFullscreen","fullscreenElement","fullscreenEnabled","fullscreenchange","fullscreenerror"],["webkitRequestFullscreen","webkitExitFullscreen","webkitFullscreenElement","webkitFullscreenEnabled","webkitfullscreenchange","webkitfullscreenerror"],["webkitRequestFullScreen","webkitCancelFullScreen","webkitCurrentFullScreenElement","webkitCancelFullScreen","webkitfullscreenchange","webkitfullscreenerror"],["mozRequestFullScreen","mozCancelFullScreen","mozFullScreenElement","mozFullScreenEnabled","mozfullscreenchange","mozfullscreenerror"],["msRequestFullscreen","msExitFullscreen","msFullscreenElement","msFullscreenEnabled","MSFullscreenChange","MSFullscreenError"]],o=(()=>{if("undefined"==typeof document)return!1;const e=a[0],t={};for(const r of a){if(r[1]in document){for(const[a,o]of r.entries())t[e[a]]=o;return t}}return!1})(),n={change:o.fullscreenchange,error:o.fullscreenerror};let i={request:(e=document.documentElement,t)=>new Promise(((r,a)=>{const n=()=>{i.off("change",n),r()};i.on("change",n);const s=e[o.requestFullscreen](t);s instanceof Promise&&s.then(n).catch(a)})),exit:()=>new Promise(((e,t)=>{if(!i.isFullscreen)return void e();const r=()=>{i.off("change",r),e()};i.on("change",r);const a=document[o.exitFullscreen]();a instanceof Promise&&a.then(r).catch(t)})),toggle:(e,t)=>i.isFullscreen?i.exit():i.request(e,t),onchange(e){i.on("change",e)},onerror(e){i.on("error",e)},on(e,t){const r=n[e];r&&document.addEventListener(r,t,!1)},off(e,t){const r=n[e];r&&document.removeEventListener(r,t,!1)},raw:o};Object.defineProperties(i,{isFullscreen:{get:()=>Boolean(document[o.fullscreenElement])},element:{enumerable:!0,get:()=>document[o.fullscreenElement]},isEnabled:{enumerable:!0,get:()=>Boolean(document[o.fullscreenEnabled])}}),o||(i={isEnabled:!1}),r.default=i},{"@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],"5aYAP":[function(e,t,r){e("@parcel/transformer-js/src/esmodule-helpers.js").defineInteropFlag(r);var a=e("../utils");r.default=function(e){const{constructor:t,template:{$container:r,$player:o}}=e;let n="";(0,a.def)(e,"fullscreenWeb",{get:()=>(0,a.hasClass)(o,"art-fullscreen-web"),set(i){i?(n=o.style.cssText,t.FULLSCREEN_WEB_IN_BODY&&(0,a.append)(document.body,o),e.state="fullscreenWeb",(0,a.setStyle)(o,"width","100%"),(0,a.setStyle)(o,"height","100%"),(0,a.addClass)(o,"art-fullscreen-web"),e.emit("fullscreenWeb",!0)):(t.FULLSCREEN_WEB_IN_BODY&&(0,a.append)(r,o),n&&(o.style.cssText=n,n=""),(0,a.removeClass)(o,"art-fullscreen-web"),e.emit("fullscreenWeb",!1)),e.emit("resize")}})}},{"../utils":"71aH7","@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],"7EnIB":[function(e,t,r){e("@parcel/transformer-js/src/esmodule-helpers.js").defineInteropFlag(r);var a=e("../utils");r.default=function(e){const{i18n:t,notice:r,template:{$video:o}}=e;document.pictureInPictureEnabled?function(e){const{template:{$video:t},proxy:r,notice:o}=e;t.disablePictureInPicture=!1,(0,a.def)(e,"pip",{get:()=>document.pictureInPictureElement,set(r){r?(e.state="pip",t.requestPictureInPicture().catch((e=>{throw o.show=e,e}))):document.exitPictureInPicture().catch((e=>{throw o.show=e,e}))}}),r(t,"enterpictureinpicture",(()=>{e.emit("pip",!0)})),r(t,"leavepictureinpicture",(()=>{e.emit("pip",!1)}))}(e):o.webkitSupportsPresentationMode?function(e){const{$video:t}=e.template;t.webkitSetPresentationMode("inline"),(0,a.def)(e,"pip",{get:()=>"picture-in-picture"===t.webkitPresentationMode,set(r){r?(e.state="pip",t.webkitSetPresentationMode("picture-in-picture"),e.emit("pip",!0)):(t.webkitSetPresentationMode("inline"),e.emit("pip",!1))}})}(e):(0,a.def)(e,"pip",{get:()=>!1,set(){r.show=t.get("PIP Not Supported")}})}},{"../utils":"71aH7","@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],"3N9mP":[function(e,t,r){e("@parcel/transformer-js/src/esmodule-helpers.js").defineInteropFlag(r);var a=e("../utils");r.default=function(e){const{$video:t}=e.template;(0,a.def)(e,"loaded",{get:()=>e.loadedTime/t.duration}),(0,a.def)(e,"loadedTime",{get:()=>t.buffered.length?t.buffered.end(t.buffered.length-1):0})}},{"../utils":"71aH7","@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],et96R:[function(e,t,r){e("@parcel/transformer-js/src/esmodule-helpers.js").defineInteropFlag(r);var a=e("../utils");r.default=function(e){(0,a.def)(e,"played",{get:()=>e.currentTime/e.duration})}},{"../utils":"71aH7","@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],"9DzzM":[function(e,t,r){e("@parcel/transformer-js/src/esmodule-helpers.js").defineInteropFlag(r);var a=e("../utils");r.default=function(e){const{$video:t}=e.template;(0,a.def)(e,"playing",{get:()=>!!(t.currentTime>0&&!t.paused&&!t.ended&&t.readyState>2)})}},{"../utils":"71aH7","@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],i1LDY:[function(e,t,r){e("@parcel/transformer-js/src/esmodule-helpers.js").defineInteropFlag(r);var a=e("../utils");r.default=function(e){const{$container:t,$player:r,$video:o}=e.template;(0,a.def)(e,"autoSize",{value(){const{videoWidth:n,videoHeight:i}=o,{width:s,height:l}=t.getBoundingClientRect(),c=n/i;if(s/l>c){const e=l*c/s*100;(0,a.setStyle)(r,"width",`${e}%`),(0,a.setStyle)(r,"height","100%")}else{const e=s/c/l*100;(0,a.setStyle)(r,"width","100%"),(0,a.setStyle)(r,"height",`${e}%`)}e.emit("autoSize",{width:e.width,height:e.height})}})}},{"../utils":"71aH7","@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],IqARI:[function(e,t,r){e("@parcel/transformer-js/src/esmodule-helpers.js").defineInteropFlag(r);var a=e("../utils");r.default=function(e){(0,a.def)(e,"rect",{get:()=>e.template.$player.getBoundingClientRect()});const t=["bottom","height","left","right","top","width"];for(let r=0;re.rect[o]})}(0,a.def)(e,"x",{get:()=>e.left+window.pageXOffset}),(0,a.def)(e,"y",{get:()=>e.top+window.pageYOffset})}},{"../utils":"71aH7","@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],"7E7Vs":[function(e,t,r){e("@parcel/transformer-js/src/esmodule-helpers.js").defineInteropFlag(r);var a=e("../utils");r.default=function(e){const{template:{$player:t},i18n:r,notice:o}=e;(0,a.def)(e,"flip",{get:()=>t.dataset.flip||"normal",set(n){n||(n="normal"),"normal"===n?delete t.dataset.flip:t.dataset.flip=n,o.show=`${r.get("Video Flip")}: ${r.get((0,a.capitalize)(n))}`,e.emit("flip",n)}})}},{"../utils":"71aH7","@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],gpugx:[function(e,t,r){e("@parcel/transformer-js/src/esmodule-helpers.js").defineInteropFlag(r);var a=e("../utils");r.default=function(e){const{icons:t,proxy:r,storage:o,template:{$player:n,$video:i}}=e;let s=!1,l=0,c=0;function p(){const{$mini:t}=e.template;t&&((0,a.removeClass)(n,"art-mini"),(0,a.setStyle)(t,"display","none"),n.prepend(i),e.emit("mini",!1))}function u(t,r){e.playing?((0,a.setStyle)(t,"display","none"),(0,a.setStyle)(r,"display","flex")):((0,a.setStyle)(t,"display","flex"),(0,a.setStyle)(r,"display","none"))}function d(){const{$mini:t}=e.template,r=t.getBoundingClientRect(),n=window.innerHeight-r.height-50,i=window.innerWidth-r.width-50;o.set("top",n),o.set("left",i),(0,a.setStyle)(t,"top",`${n}px`),(0,a.setStyle)(t,"left",`${i}px`)}(0,a.def)(e,"mini",{get:()=>(0,a.hasClass)(n,"art-mini"),set(f){if(f){e.state="mini",(0,a.addClass)(n,"art-mini");const f=function(){const{$mini:n}=e.template;if(n)return(0,a.append)(n,i),(0,a.setStyle)(n,"display","flex");{const n=(0,a.createElement)("div");(0,a.addClass)(n,"art-mini-popup"),(0,a.append)(document.body,n),e.template.$mini=n,(0,a.append)(n,i);const d=(0,a.append)(n,'
    ');(0,a.append)(d,t.close),r(d,"click",p);const f=(0,a.append)(n,'
    '),h=(0,a.append)(f,t.play),m=(0,a.append)(f,t.pause);return r(h,"click",(()=>e.play())),r(m,"click",(()=>e.pause())),u(h,m),e.on("video:playing",(()=>u(h,m))),e.on("video:pause",(()=>u(h,m))),e.on("video:timeupdate",(()=>u(h,m))),r(n,"mousedown",(e=>{s=0===e.button,l=e.pageX,c=e.pageY})),e.on("document:mousemove",(e=>{if(s){(0,a.addClass)(n,"art-mini-droging");const t=e.pageX-l,r=e.pageY-c;(0,a.setStyle)(n,"transform",`translate(${t}px, ${r}px)`)}})),e.on("document:mouseup",(()=>{if(s){s=!1,(0,a.removeClass)(n,"art-mini-droging");const e=n.getBoundingClientRect();o.set("left",e.left),o.set("top",e.top),(0,a.setStyle)(n,"left",`${e.left}px`),(0,a.setStyle)(n,"top",`${e.top}px`),(0,a.setStyle)(n,"transform",null)}})),n}}(),h=o.get("top"),m=o.get("left");h&&m?((0,a.setStyle)(f,"top",`${h}px`),(0,a.setStyle)(f,"left",`${m}px`),(0,a.isInViewport)(f)||d()):d(),e.emit("mini",!0)}else p()}})}},{"../utils":"71aH7","@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],f1hVG:[function(e,t,r){e("@parcel/transformer-js/src/esmodule-helpers.js").defineInteropFlag(r);var a=e("../utils");r.default=function(e){let t=[];(0,a.def)(e,"loop",{get:()=>t,set:r=>{if(Array.isArray(r)&&"number"==typeof r[0]&&"number"==typeof r[1]){const o=(0,a.clamp)(r[0],0,Math.min(r[1],e.duration)),n=(0,a.clamp)(r[1],o,e.duration);t=n-o>=1?[o,n]:[]}else t=[];e.emit("loop",t)}}),e.on("video:timeupdate",(()=>{t.length&&(e.currentTimet[1])&&(e.seek=t[0])}))}},{"../utils":"71aH7","@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],"1SuFS":[function(e,t,r){e("@parcel/transformer-js/src/esmodule-helpers.js").defineInteropFlag(r);var a=e("../utils");r.default=function(e){const{template:{$poster:t}}=e;(0,a.def)(e,"poster",{get:()=>{try{return t.style.backgroundImage.match(/"(.*)"/)[1]}catch(e){return""}},set(e){(0,a.setStyle)(t,"backgroundImage",`url(${e})`)}})}},{"../utils":"71aH7","@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],"8x4te":[function(e,t,r){e("@parcel/transformer-js/src/esmodule-helpers.js").defineInteropFlag(r);var a=e("../utils");r.default=function(e){const{template:{$container:t,$video:r}}=e;(0,a.def)(e,"autoHeight",{value(){const{clientWidth:o}=t,{videoHeight:n,videoWidth:i}=r,s=n*(o/i);(0,a.setStyle)(t,"height",s+"px"),e.emit("autoHeight",s)}})}},{"../utils":"71aH7","@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],"1CaTA":[function(e,t,r){e("@parcel/transformer-js/src/esmodule-helpers.js").defineInteropFlag(r);var a=e("../utils");r.default=function(e){const{$player:t}=e.template;(0,a.def)(e,"cssVar",{value:(e,r)=>r?t.style.setProperty(e,r):getComputedStyle(t).getPropertyValue(e)})}},{"../utils":"71aH7","@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],"2FqhO":[function(e,t,r){e("@parcel/transformer-js/src/esmodule-helpers.js").defineInteropFlag(r);var a=e("../utils");r.default=function(e){(0,a.def)(e,"theme",{get:()=>e.cssVar("--art-theme"),set(t){e.cssVar("--art-theme",t)}})}},{"../utils":"71aH7","@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],"1fQQs":[function(e,t,r){e("@parcel/transformer-js/src/esmodule-helpers.js").defineInteropFlag(r);var a=e("../utils");r.default=function(e){(0,a.def)(e,"type",{get:()=>e.option.type,set(t){e.option.type=t}})}},{"../utils":"71aH7","@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],iBOQW:[function(e,t,r){e("@parcel/transformer-js/src/esmodule-helpers.js").defineInteropFlag(r);var a=e("../utils");r.default=function(e){const t=["mini","pip","fullscreen","fullscreenWeb"];(0,a.def)(e,"state",{get:()=>t.find((t=>e[t]))||"standard",set(r){for(let a=0;a{s=[]})),(0,a.def)(e,"subtitleOffset",{get:()=>i,set(a){if(o.$track&&o.$track.track){const l=Array.from(o.$track.track.cues);i=t(a,-5,5);for(let r=0;r{switch(e.availability){case"available":i=!0;break;case"not-available":i=!1}})):i=!1,(0,a.def)(e,"airplay",{value(){i?(n.webkitShowPlaybackTargetPicker(),e.emit("airplay")):r.show=t.get("AirPlay Not Available")}})}},{"../utils":"71aH7","@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],"5SdyX":[function(e,t,r){e("@parcel/transformer-js/src/esmodule-helpers.js").defineInteropFlag(r);var a=e("../utils");r.default=function(e){(0,a.def)(e,"quality",{set(t){const{controls:r,notice:a,i18n:o}=e,n=t.find((e=>e.default))||t[0];r.update({name:"quality",position:"right",index:10,style:{marginRight:"10px"},html:n?n.html:"",selector:t,async onSelect(t){await e.switchQuality(t.url),a.show=`${o.get("Switch Video")}: ${t.html}`}})}})}},{"../utils":"71aH7","@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],fCWZK:[function(e,t,r){e("@parcel/transformer-js/src/esmodule-helpers.js").defineInteropFlag(r);var a=e("../utils");r.default=function(e){const{option:t,storage:r,template:{$video:o,$poster:n}}=e;for(const r in t.moreVideoAttr)e.attr(r,t.moreVideoAttr[r]);t.muted&&(e.muted=t.muted),t.volume&&(o.volume=(0,a.clamp)(t.volume,0,1));const i=r.get("volume");"number"==typeof i&&(o.volume=(0,a.clamp)(i,0,1)),t.poster&&(0,a.setStyle)(n,"backgroundImage",`url(${t.poster})`),t.autoplay&&(o.autoplay=t.autoplay),t.playsInline&&(o.playsInline=!0,o["webkit-playsinline"]=!0),t.theme&&(t.cssVar["--art-theme"]=t.theme);for(const r in t.cssVar)e.cssVar(r,t.cssVar[r]);e.url=t.url}},{"../utils":"71aH7","@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],f8Lv3:[function(e,t,r){var a=e("@parcel/transformer-js/src/esmodule-helpers.js");a.defineInteropFlag(r);var o=e("../config"),n=a.interopDefault(o),i=e("../utils");r.default=function(e){const{i18n:t,notice:r,option:a,constructor:o,proxy:s,template:{$player:l,$video:c,$poster:p}}=e;let u=0;for(let t=0;t{e.emit(`video:${t.type}`,t)}));e.on("video:canplay",(()=>{u=0,e.loading.show=!1})),e.once("video:canplay",(()=>{e.loading.show=!1,e.controls.show=!0,e.mask.show=!0,e.isReady=!0,e.emit("ready")})),e.on("video:ended",(()=>{a.loop?(e.seek=0,e.play(),e.controls.show=!1,e.mask.show=!1):(e.controls.show=!0,e.mask.show=!0)})),e.on("video:error",(async n=>{u{e.emit("resize"),i.isMobile&&(e.loading.show=!1,e.controls.show=!0,e.mask.show=!0)})),e.on("video:loadstart",(()=>{e.loading.show=!0,e.mask.show=!1,e.controls.show=!0})),e.on("video:pause",(()=>{e.controls.show=!0,e.mask.show=!0})),e.on("video:play",(()=>{e.mask.show=!1,(0,i.setStyle)(p,"display","none")})),e.on("video:playing",(()=>{e.mask.show=!1})),e.on("video:progress",(()=>{e.playing&&(e.loading.show=!1)})),e.on("video:seeked",(()=>{e.loading.show=!1})),e.on("video:seeking",(()=>{e.loading.show=!0,e.mask.show=!1})),e.on("video:timeupdate",(()=>{e.mask.show=!1})),e.on("video:waiting",(()=>{e.loading.show=!0,e.mask.show=!1}))}},{"../config":"lyjeQ","../utils":"71aH7","@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],"8Z0Uf":[function(e,t,r){var a=e("@parcel/transformer-js/src/esmodule-helpers.js");a.defineInteropFlag(r);var o=e("../utils"),n=e("../utils/component"),i=a.interopDefault(n),s=e("./fullscreen"),l=a.interopDefault(s),c=e("./fullscreenWeb"),p=a.interopDefault(c),u=e("./pip"),d=a.interopDefault(u),f=e("./playAndPause"),h=a.interopDefault(f),m=e("./progress"),g=a.interopDefault(m),v=e("./time"),y=a.interopDefault(v),b=e("./volume"),x=a.interopDefault(b),w=e("./setting"),j=a.interopDefault(w),k=e("./thumbnails"),S=a.interopDefault(k),I=e("./screenshot"),C=a.interopDefault(I),P=e("./loop"),$=a.interopDefault(P),M=e("./airplay"),T=a.interopDefault(M);class E extends i.default{constructor(e){super(e),this.name="control";const{proxy:t,constructor:r,template:{$player:a}}=e;let n=Date.now();t(a,["click","mousemove","touchstart","touchmove"],(()=>{this.show=!0,(0,o.removeClass)(a,"art-hide-cursor"),(0,o.addClass)(a,"art-hover"),n=Date.now()})),e.on("video:timeupdate",(()=>{!e.isInput&&e.playing&&this.show&&Date.now()-n>=r.CONTROL_HIDE_TIME&&(this.show=!1,(0,o.addClass)(a,"art-hide-cursor"),(0,o.removeClass)(a,"art-hover"))})),this.init()}init(){const{option:e}=this.art;e.isLive||this.add((0,g.default)({name:"progress",position:"top",index:10})),!e.thumbnails.url||e.isLive||o.isMobile||this.add((0,S.default)({name:"thumbnails",position:"top",index:20})),this.add((0,$.default)({name:"loop",position:"top",index:30})),this.add((0,h.default)({name:"playAndPause",position:"left",index:10})),this.add((0,x.default)({name:"volume",position:"left",index:20})),e.isLive||this.add((0,y.default)({name:"time",position:"left",index:30})),e.quality.length&&(0,o.sleep)().then((()=>{this.art.quality=e.quality})),e.screenshot&&!o.isMobile&&this.add((0,C.default)({name:"screenshot",position:"right",index:20})),e.setting&&this.add((0,j.default)({name:"setting",position:"right",index:30})),e.pip&&this.add((0,d.default)({name:"pip",position:"right",index:40})),e.airplay&&window.WebKitPlaybackTargetAvailabilityEvent&&this.add((0,T.default)({name:"airplay",position:"right",index:50})),e.fullscreenWeb&&this.add((0,p.default)({name:"fullscreenWeb",position:"right",index:60})),e.fullscreen&&this.add((0,l.default)({name:"fullscreen",position:"right",index:70}));for(let t=0;tNumber(e.dataset.index)>=Number(n.dataset.index)));p?p.insertAdjacentElement("beforebegin",n):(0,o.append)(this.$parent,n),t.html&&(0,o.append)(n,t.html),t.style&&(0,o.setStyles)(n,t.style),t.tooltip&&(0,o.tooltip)(n,t.tooltip);const u=[];if(t.click){const e=this.art.events.proxy(n,"click",(e=>{e.preventDefault(),t.click.call(this.art,this,e)}));u.push(e)}return t.selector&&["left","right"].includes(t.position)&&this.addSelector(t,n,u),this[r]=n,this.cache.set(r,{$ref:n,events:u,option:t}),t.mounted&&t.mounted.call(this.art,n),n}addSelector(e,t,r){const{hover:a,proxy:i}=this.art.events;(0,o.addClass)(t,"art-control-selector");const s=(0,o.createElement)("div");(0,o.addClass)(s,"art-selector-value"),(0,o.append)(s,e.html),t.innerText="",(0,o.append)(t,s);const l=e.selector.map(((e,t)=>`
    ${e.html}
    `)).join(""),c=(0,o.createElement)("div");(0,o.addClass)(c,"art-selector-list"),(0,o.append)(c,l),(0,o.append)(t,c);const p=()=>{const e=(0,o.getStyle)(t,"width")/2-(0,o.getStyle)(c,"width")/2;c.style.left=`${e}px`};a(t,p);const u=i(c,"click",(async t=>{const r=(t.composedPath()||[]).find((e=>(0,o.hasClass)(e,"art-selector-item")));if(!r)return;(0,o.inverseClass)(r,"art-current");const a=Number(r.dataset.index),i=e.selector[a]||{};if(s.innerText=r.innerText,e.onSelect){const a=await e.onSelect.call(this.art,i,r,t);(0,n.isStringOrNumber)(a)&&(s.innerHTML=a)}p()}));r.push(u)}remove(e){const t=this.cache.get(e);(0,i.errorHandle)(t,`Can't find [${e}] from the [${this.name}]`),t.option.beforeUnmount&&t.option.beforeUnmount.call(this.art,t.$ref);for(let e=0;e({...e,tooltip:t.i18n.get("Fullscreen"),mounted:e=>{const{proxy:r,icons:o,i18n:n}=t,i=(0,a.append)(e,o.fullscreenOn),s=(0,a.append)(e,o.fullscreenOff);(0,a.setStyle)(s,"display","none"),r(e,"click",(()=>{t.fullscreen=!t.fullscreen})),t.on("fullscreen",(t=>{t?((0,a.tooltip)(e,n.get("Exit Fullscreen")),(0,a.setStyle)(i,"display","none"),(0,a.setStyle)(s,"display","inline-flex")):((0,a.tooltip)(e,n.get("Fullscreen")),(0,a.setStyle)(i,"display","inline-flex"),(0,a.setStyle)(s,"display","none"))}))}})}},{"../utils":"71aH7","@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],"03jeB":[function(e,t,r){e("@parcel/transformer-js/src/esmodule-helpers.js").defineInteropFlag(r);var a=e("../utils");r.default=function(e){return t=>({...e,tooltip:t.i18n.get("Web Fullscreen"),mounted:e=>{const{proxy:r,icons:o,i18n:n}=t,i=(0,a.append)(e,o.fullscreenWebOn),s=(0,a.append)(e,o.fullscreenWebOff);(0,a.setStyle)(s,"display","none"),r(e,"click",(()=>{t.fullscreenWeb=!t.fullscreenWeb})),t.on("fullscreenWeb",(t=>{t?((0,a.tooltip)(e,n.get("Exit Web Fullscreen")),(0,a.setStyle)(i,"display","none"),(0,a.setStyle)(s,"display","inline-flex")):((0,a.tooltip)(e,n.get("Web Fullscreen")),(0,a.setStyle)(i,"display","inline-flex"),(0,a.setStyle)(s,"display","none"))}))}})}},{"../utils":"71aH7","@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],u8l8e:[function(e,t,r){e("@parcel/transformer-js/src/esmodule-helpers.js").defineInteropFlag(r);var a=e("../utils");r.default=function(e){return t=>({...e,tooltip:t.i18n.get("PIP Mode"),mounted:e=>{const{proxy:r,icons:o,i18n:n}=t;(0,a.append)(e,o.pip),r(e,"click",(()=>{t.pip=!t.pip})),t.on("pip",(t=>{(0,a.tooltip)(e,n.get(t?"Exit PIP Mode":"PIP Mode"))}))}})}},{"../utils":"71aH7","@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],ebXtb:[function(e,t,r){e("@parcel/transformer-js/src/esmodule-helpers.js").defineInteropFlag(r);var a=e("../utils");r.default=function(e){return t=>({...e,mounted:e=>{const{proxy:r,icons:o,i18n:n}=t,i=(0,a.append)(e,o.play),s=(0,a.append)(e,o.pause);function l(){(0,a.setStyle)(i,"display","flex"),(0,a.setStyle)(s,"display","none")}function c(){(0,a.setStyle)(i,"display","none"),(0,a.setStyle)(s,"display","flex")}(0,a.tooltip)(i,n.get("Play")),(0,a.tooltip)(s,n.get("Pause")),r(i,"click",(()=>{t.play()})),r(s,"click",(()=>{t.pause()})),t.playing?c():l(),t.on("video:playing",(()=>{c()})),t.on("video:pause",(()=>{l()}))}})}},{"../utils":"71aH7","@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],bgoVP:[function(e,t,r){var a=e("@parcel/transformer-js/src/esmodule-helpers.js");a.defineInteropFlag(r),a.export(r,"getPosFromEvent",(()=>n)),a.export(r,"setCurrentTime",(()=>i));var o=e("../utils");function n(e,t){const{$progress:r}=e.template,{left:a}=r.getBoundingClientRect(),n=o.isMobile?t.touches[0].clientX:t.clientX,i=(0,o.clamp)(n-a,0,r.clientWidth),s=i/r.clientWidth*e.duration;return{second:s,time:(0,o.secondToTime)(s),width:i,percentage:(0,o.clamp)(i/r.clientWidth,0,1)}}function i(e,t){if(e.isRotate){const r=t.touches[0].clientY/e.height,a=r*e.duration;e.emit("setBar","played",r),e.seek=a}else{const{second:r,percentage:a}=n(e,t);e.emit("setBar","played",a),e.seek=r}}r.default=function(e){return t=>{const{icons:r,option:a,proxy:s}=t;return{...e,html:'
    ',mounted:e=>{let l=!1;const c=(0,o.query)(".art-progress-hover",e),p=(0,o.query)(".art-progress-loaded",e),u=(0,o.query)(".art-progress-played",e),d=(0,o.query)(".art-progress-highlight",e),f=(0,o.query)(".art-progress-indicator",e),h=(0,o.query)(".art-progress-tip",e);function m(e,t){"loaded"===e&&(0,o.setStyle)(p,"width",100*t+"%"),"played"===e&&((0,o.setStyle)(u,"width",100*t+"%"),(0,o.setStyle)(f,"left",100*t+"%"))}r.indicator?(0,o.append)(f,r.indicator):(0,o.setStyle)(f,"backgroundColor","var(--art-theme)"),t.on("video:loadedmetadata",(()=>{for(let e=0;e`;(0,o.append)(d,i)}})),m("loaded",t.loaded),t.on("setBar",((e,t)=>{m(e,t)})),t.on("video:progress",(()=>{m("loaded",t.loaded)})),t.on("video:timeupdate",(()=>{m("played",t.played)})),t.on("video:ended",(()=>{m("played",1)})),o.isMobile||(s(e,"click",(e=>{e.target!==f&&i(t,e)})),s(e,"mousemove",(r=>{!function(e){const{width:r}=n(t,e);(0,o.setStyle)(c,"width",`${r}px`),(0,o.setStyle)(c,"display","flex")}(r),(0,o.setStyle)(h,"display","flex"),(0,o.includeFromEvent)(r,d)?function(r){const{width:a}=n(t,r),{text:i}=r.target.dataset;h.innerHTML=i;const s=h.clientWidth;a<=s/2?(0,o.setStyle)(h,"left",0):a>e.clientWidth-s/2?(0,o.setStyle)(h,"left",e.clientWidth-s+"px"):(0,o.setStyle)(h,"left",a-s/2+"px")}(r):function(r){const{width:a,time:i}=n(t,r);h.innerHTML=i;const s=h.clientWidth;a<=s/2?(0,o.setStyle)(h,"left",0):a>e.clientWidth-s/2?(0,o.setStyle)(h,"left",e.clientWidth-s+"px"):(0,o.setStyle)(h,"left",a-s/2+"px")}(r)})),s(e,"mouseleave",(()=>{(0,o.setStyle)(h,"display","none"),(0,o.setStyle)(c,"display","none")})),s(e,"mousedown",(e=>{l=0===e.button})),t.on("document:mousemove",(e=>{if(l){const{second:r,percentage:a}=n(t,e);m("played",a),t.seek=r}})),t.on("document:mouseup",(()=>{l&&(l=!1)})))}}}}},{"../utils":"71aH7","@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],ikc2j:[function(e,t,r){e("@parcel/transformer-js/src/esmodule-helpers.js").defineInteropFlag(r);var a=e("../utils");r.default=function(e){return t=>({...e,style:a.isMobile?{fontSize:"12px",padding:"0 5px"}:{cursor:"auto",padding:"0 10px"},mounted:e=>{function r(){const r=`${(0,a.secondToTime)(t.currentTime)} / ${(0,a.secondToTime)(t.duration)}`;r!==e.innerText&&(e.innerText=r)}r();const o=["video:loadedmetadata","video:timeupdate","video:progress"];for(let e=0;e({...e,mounted:e=>{const{proxy:r,icons:o}=t,n=(0,a.append)(e,o.volume),i=(0,a.append)(e,o.volumeClose),s=(0,a.append)(e,'
    '),l=(0,a.append)(s,'
    '),c=(0,a.append)(l,'
    '),p=(0,a.append)(l,'
    '),u=(0,a.append)(p,'
    '),d=(0,a.append)(u,'
    '),f=(0,a.append)(p,'
    ');function h(e){const{top:t,height:r}=p.getBoundingClientRect();return 1-(e.clientY-t)/r}function m(){if(t.muted||0===t.volume)(0,a.setStyle)(n,"display","none"),(0,a.setStyle)(i,"display","flex"),(0,a.setStyle)(f,"top","100%"),(0,a.setStyle)(d,"top","100%"),c.innerText=0;else{const e=100*t.volume;(0,a.setStyle)(n,"display","flex"),(0,a.setStyle)(i,"display","none"),(0,a.setStyle)(f,"top",100-e+"%"),(0,a.setStyle)(d,"top",100-e+"%"),c.innerText=Math.floor(e)}}if(m(),t.on("video:volumechange",m),r(n,"click",(()=>{t.muted=!0})),r(i,"click",(()=>{t.muted=!1})),a.isMobile)(0,a.setStyle)(s,"display","none");else{let e=!1;r(p,"mousedown",(r=>{e=0===r.button,t.volume=h(r)})),t.on("document:mousemove",(r=>{e&&(t.muted=!1,t.volume=h(r))})),t.on("document:mouseup",(()=>{e&&(e=!1)}))}}})}},{"../utils":"71aH7","@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],"03o9l":[function(e,t,r){e("@parcel/transformer-js/src/esmodule-helpers.js").defineInteropFlag(r);var a=e("../utils");r.default=function(e){return t=>({...e,tooltip:t.i18n.get("Show Setting"),mounted:e=>{const{proxy:r,icons:o,i18n:n}=t;(0,a.append)(e,o.setting),r(e,"click",(()=>{t.setting.toggle(),t.setting.updateStyle()})),t.on("setting",(t=>{(0,a.tooltip)(e,n.get(t?"Hide Setting":"Show Setting"))}))}})}},{"../utils":"71aH7","@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],eCVx2:[function(e,t,r){e("@parcel/transformer-js/src/esmodule-helpers.js").defineInteropFlag(r);var a=e("../utils"),o=e("./progress");r.default=function(e){return t=>({...e,mounted:e=>{const{option:r,template:{$progress:n,$video:i},events:{proxy:s,loadImg:l}}=t;let c=null,p=!1,u=!1;s(n,"mousemove",(async s=>{if(!p){p=!0;const e=await l(r.thumbnails.url);c=e,u=!0}u&&((0,a.setStyle)(e,"display","flex"),function(s){const{width:l}=(0,o.getPosFromEvent)(t,s),{url:p,number:u,column:d,width:f,height:h}=r.thumbnails,m=f||c.naturalWidth/d,g=h||m/(i.videoWidth/i.videoHeight),v=n.clientWidth/u,y=Math.floor(l/v),b=Math.ceil(y/d)-1,x=y%d||d-1;(0,a.setStyle)(e,"backgroundImage",`url(${p})`),(0,a.setStyle)(e,"height",`${g}px`),(0,a.setStyle)(e,"width",`${m}px`),(0,a.setStyle)(e,"backgroundPosition",`-${x*m}px -${b*g}px`),l<=m/2?(0,a.setStyle)(e,"left",0):l>n.clientWidth-m/2?(0,a.setStyle)(e,"left",n.clientWidth-m+"px"):(0,a.setStyle)(e,"left",l-m/2+"px")}(s))})),s(n,"mouseleave",(()=>{(0,a.setStyle)(e,"display","none")})),t.on("hover",(t=>{t||(0,a.setStyle)(e,"display","none")}))}})}},{"../utils":"71aH7","./progress":"bgoVP","@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],"4KCF5":[function(e,t,r){e("@parcel/transformer-js/src/esmodule-helpers.js").defineInteropFlag(r);var a=e("../utils");r.default=function(e){return t=>({...e,tooltip:t.i18n.get("Screenshot"),mounted:e=>{const{proxy:r,icons:o}=t;(0,a.append)(e,o.screenshot),r(e,"click",(()=>{t.screenshot()}))}})}},{"../utils":"71aH7","@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],"2hIff":[function(e,t,r){e("@parcel/transformer-js/src/esmodule-helpers.js").defineInteropFlag(r);var a=e("../utils");r.default=function(e){return t=>({...e,mounted:e=>{const r=(0,a.append)(e,''),o=(0,a.append)(e,'');t.on("loop",(n=>{n&&n.length?((0,a.setStyle)(e,"display","flex"),(0,a.setStyle)(r,"left",`calc(${n[0]/t.duration*100}% - ${r.clientWidth}px)`),(0,a.setStyle)(o,"left",n[1]/t.duration*100+"%")):(0,a.setStyle)(e,"display","none")}))}})}},{"../utils":"71aH7","@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],"4IS2d":[function(e,t,r){e("@parcel/transformer-js/src/esmodule-helpers.js").defineInteropFlag(r);var a=e("../utils");r.default=function(e){return t=>({...e,tooltip:t.i18n.get("AirPlay"),mounted:e=>{const{proxy:r,icons:o}=t;(0,a.append)(e,o.airplay),r(e,"click",(()=>t.airplay()))}})}},{"../utils":"71aH7","@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],"2KYsr":[function(e,t,r){var a=e("@parcel/transformer-js/src/esmodule-helpers.js");a.defineInteropFlag(r);var o=e("../utils"),n=e("../utils/component"),i=a.interopDefault(n),s=e("./playbackRate"),l=a.interopDefault(s),c=e("./aspectRatio"),p=a.interopDefault(c),u=e("./flip"),d=a.interopDefault(u),f=e("./info"),h=a.interopDefault(f),m=e("./version"),g=a.interopDefault(m),v=e("./close"),y=a.interopDefault(v);class b extends i.default{constructor(e){super(e),this.name="contextmenu",this.$parent=e.template.$contextmenu,o.isMobile||this.init()}init(){const{option:e,proxy:t,template:{$player:r,$contextmenu:a}}=this.art;e.playbackRate&&this.add((0,l.default)({name:"playbackRate",index:10})),e.aspectRatio&&this.add((0,p.default)({name:"aspectRatio",index:20})),e.flip&&this.add((0,d.default)({name:"flip",index:30})),this.add((0,h.default)({name:"info",index:40})),this.add((0,g.default)({name:"version",index:50})),this.add((0,y.default)({name:"close",index:60}));for(let t=0;t{if(e.preventDefault(),!this.art.constructor.CONTEXTMENU)return;this.show=!0;const t=e.clientX,n=e.clientY,{height:i,width:s,left:l,top:c}=r.getBoundingClientRect(),{height:p,width:u}=a.getBoundingClientRect();let d=t-l,f=n-c;t+u>l+s&&(d=s-u),n+p>c+i&&(f=i-p),(0,o.setStyles)(a,{top:`${f}px`,left:`${d}px`})})),t(r,"click",(e=>{(0,o.includeFromEvent)(e,a)||(this.show=!1)})),this.art.on("blur",(()=>{this.show=!1}))}}r.default=b},{"../utils":"71aH7","../utils/component":"18nVI","./playbackRate":"69eLi","./aspectRatio":"lUefg","./flip":"kysiM","./info":"gqIgJ","./version":"kRU7C","./close":"jQ8Pm","@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],"69eLi":[function(e,t,r){e("@parcel/transformer-js/src/esmodule-helpers.js").defineInteropFlag(r);var a=e("../utils");r.default=function(e){return t=>{const{i18n:r,constructor:{PLAYBACK_RATE:o}}=t,n=o.map((e=>`${1===e?r.get("Normal"):e.toFixed(1)}`)).join("");return{...e,html:`${r.get("Play Speed")}: ${n}`,click:(e,r)=>{const{value:a}=r.target.dataset;a&&(t.playbackRate=Number(a),e.show=!1)},mounted:e=>{const r=(0,a.query)('[data-value="1"]',e);r&&(0,a.inverseClass)(r,"art-current"),t.on("video:ratechange",(()=>{const r=(0,a.queryAll)("span",e).find((e=>Number(e.dataset.value)===t.playbackRate));r&&(0,a.inverseClass)(r,"art-current")}))}}}}},{"../utils":"71aH7","@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],lUefg:[function(e,t,r){e("@parcel/transformer-js/src/esmodule-helpers.js").defineInteropFlag(r);var a=e("../utils");r.default=function(e){return t=>{const{i18n:r,constructor:{ASPECT_RATIO:o}}=t,n=o.map((e=>`${"default"===e?r.get("Default"):e}`)).join("");return{...e,html:`${r.get("Aspect Ratio")}: ${n}`,click:(e,r)=>{const{value:a}=r.target.dataset;a&&(t.aspectRatio=a,e.show=!1)},mounted:e=>{const r=(0,a.query)('[data-value="default"]',e);r&&(0,a.inverseClass)(r,"art-current"),t.on("aspectRatio",(t=>{const r=(0,a.queryAll)("span",e).find((e=>e.dataset.value===t));r&&(0,a.inverseClass)(r,"art-current")}))}}}}},{"../utils":"71aH7","@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],kysiM:[function(e,t,r){e("@parcel/transformer-js/src/esmodule-helpers.js").defineInteropFlag(r);var a=e("../utils");r.default=function(e){return t=>{const{i18n:r,constructor:{FLIP:o}}=t,n=o.map((e=>`${r.get((0,a.capitalize)(e))}`)).join("");return{...e,html:`${r.get("Video Flip")}: ${n}`,click:(e,r)=>{const{value:a}=r.target.dataset;a&&(t.flip=a.toLowerCase(),e.show=!1)},mounted:e=>{const r=(0,a.query)('[data-value="normal"]',e);r&&(0,a.inverseClass)(r,"art-current"),t.on("flip",(t=>{const r=(0,a.queryAll)("span",e).find((e=>e.dataset.value===t));r&&(0,a.inverseClass)(r,"art-current")}))}}}}},{"../utils":"71aH7","@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],gqIgJ:[function(e,t,r){e("@parcel/transformer-js/src/esmodule-helpers.js").defineInteropFlag(r),r.default=function(e){return t=>({...e,html:t.i18n.get("Video Info"),click:e=>{t.info.show=!0,e.show=!1}})}},{"@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],kRU7C:[function(e,t,r){e("@parcel/transformer-js/src/esmodule-helpers.js").defineInteropFlag(r),r.default=function(e){return{...e,html:'ArtPlayer 5.0.9'}}},{"@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],jQ8Pm:[function(e,t,r){e("@parcel/transformer-js/src/esmodule-helpers.js").defineInteropFlag(r),r.default=function(e){return t=>({...e,html:t.i18n.get("Close"),click:e=>{e.show=!1}})}},{"@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],"02ajl":[function(e,t,r){var a=e("@parcel/transformer-js/src/esmodule-helpers.js");a.defineInteropFlag(r);var o=e("./utils"),n=e("./utils/component"),i=a.interopDefault(n);class s extends i.default{constructor(e){super(e),this.name="info",o.isMobile||this.init()}init(){const{proxy:e,constructor:t,template:{$infoPanel:r,$infoClose:a,$video:n}}=this.art;e(a,"click",(()=>{this.show=!1}));let i=null;const s=(0,o.queryAll)("[data-video]",r)||[];this.art.on("destroy",(()=>clearTimeout(i))),function e(){for(let e=0;enull,this.init(e.option.subtitle);let t=!1;e.on("video:timeupdate",(()=>{if(!this.url)return;const e=this.art.template.$video.webkitDisplayingFullscreen;"boolean"==typeof e&&e!==t&&(t=e,this.createTrack(e?"subtitles":"metadata",this.url))}))}get url(){return this.art.template.$track.src}set url(e){this.switch(e)}get textTrack(){return this.art.template.$video.textTracks[0]}get activeCue(){return this.textTrack.activeCues[0]}style(e,t){const{$subtitle:r}=this.art.template;return"object"==typeof e?(0,o.setStyles)(r,e):(0,o.setStyle)(r,e,t)}update(){const{$subtitle:e}=this.art.template;e.innerHTML="",this.activeCue&&(this.art.option.subtitle.escape?e.innerHTML=this.activeCue.text.split(/\r?\n/).map((e=>`

    ${(0,o.escape)(e)}

    `)).join(""):e.innerHTML=this.activeCue.text,this.art.emit("subtitleUpdate",this.activeCue.text))}async switch(e,t={}){const{i18n:r,notice:a,option:o}=this.art,n={...o.subtitle,...t,url:e},i=await this.init(n);return t.name&&(a.show=`${r.get("Switch Subtitle")}: ${t.name}`),i}createTrack(e,t){const{template:r,proxy:a}=this.art,{$video:n,$track:i}=r,s=(0,o.createElement)("track");s.default=!0,s.kind=e,s.src=t,s.track.mode="hidden",this.eventDestroy(),(0,o.remove)(i),(0,o.append)(n,s),r.$track=s,this.eventDestroy=a(this.textTrack,"cuechange",(()=>this.update()))}async init(e){const{notice:t,template:{$subtitle:r}}=this.art;if((0,l.default)(e,p.default.subtitle),e.url)return this.style(e.style),fetch(e.url).then((e=>e.arrayBuffer())).then((t=>{const r=new TextDecoder(e.encoding).decode(t);switch(this.art.emit("subtitleLoad",e.url),e.type||(0,o.getExt)(e.url)){case"srt":{const t=(0,o.srtToVtt)(r),a=e.onVttLoad(t);return(0,o.vttToBlob)(a)}case"ass":{const t=(0,o.assToVtt)(r),a=e.onVttLoad(t);return(0,o.vttToBlob)(a)}case"vtt":{const t=e.onVttLoad(r);return(0,o.vttToBlob)(t)}default:return e.url}})).then((e=>(r.innerHTML="",this.url===e||(URL.revokeObjectURL(this.url),this.createTrack("metadata",e),this.art.emit("subtitleSwitch",e)),e))).catch((e=>{throw t.show=e,e}))}}r.default=u},{"./utils":"71aH7","./utils/component":"18nVI","option-validator":"bAWi2","./scheme":"AKEiO","@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],jo4S1:[function(e,t,r){var a=e("@parcel/transformer-js/src/esmodule-helpers.js");a.defineInteropFlag(r);var o=e("../utils/error"),n=e("./clickInit"),i=a.interopDefault(n),s=e("./hoverInit"),l=a.interopDefault(s),c=e("./moveInit"),p=a.interopDefault(c),u=e("./resizeInit"),d=a.interopDefault(u),f=e("./gestureInit"),h=a.interopDefault(f),m=e("./viewInit"),g=a.interopDefault(m),v=e("./documentInit"),y=a.interopDefault(v);r.default=class{constructor(e){this.destroyEvents=[],this.proxy=this.proxy.bind(this),this.hover=this.hover.bind(this),this.loadImg=this.loadImg.bind(this),(0,i.default)(e,this),(0,l.default)(e,this),(0,p.default)(e,this),(0,d.default)(e,this),(0,h.default)(e,this),(0,g.default)(e,this),(0,y.default)(e,this)}proxy(e,t,r,a={}){if(Array.isArray(t))return t.map((t=>this.proxy(e,t,r,a)));e.addEventListener(t,r,a);const o=()=>e.removeEventListener(t,r,a);return this.destroyEvents.push(o),o}hover(e,t,r){t&&this.proxy(e,"mouseenter",t),r&&this.proxy(e,"mouseleave",r)}loadImg(e){return new Promise(((t,r)=>{let a;if(e instanceof HTMLImageElement)a=e;else{if("string"!=typeof e)return r(new(0,o.ArtPlayerError)("Unable to get Image"));a=new Image,a.src=e}if(a.complete)return t(a);this.proxy(a,"load",(()=>t(a))),this.proxy(a,"error",(()=>r(new(0,o.ArtPlayerError)(`Failed to load Image: ${a.src}`))))}))}remove(e){const t=this.destroyEvents.indexOf(e);t>-1&&(e(),this.destroyEvents.splice(t,1))}destroy(){for(let e=0;e{(0,a.includeFromEvent)(t,o)?(e.isInput="INPUT"===t.target.tagName,e.isFocus=!0,e.emit("focus",t)):(e.isInput=!1,e.isFocus=!1,e.emit("blur",t))}));let i=0;t.proxy(n,"click",(t=>{const o=Date.now(),{MOBILE_CLICK_PLAY:n,DBCLICK_TIME:s,MOBILE_DBCLICK_PLAY:l,DBCLICK_FULLSCREEN:c}=r;o-i<=s?(e.emit("dblclick",t),a.isMobile?!e.isLock&&l&&e.toggle():c&&(e.fullscreen=!e.fullscreen)):(e.emit("click",t),a.isMobile?!e.isLock&&n&&e.toggle():e.toggle()),i=o}))}},{"../utils":"71aH7","@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],"4jWHi":[function(e,t,r){e("@parcel/transformer-js/src/esmodule-helpers.js").defineInteropFlag(r);var a=e("../utils");r.default=function(e,t){const{$player:r}=e.template;t.hover(r,(t=>{(0,a.addClass)(r,"art-hover"),e.emit("hover",!0,t)}),(t=>{(0,a.removeClass)(r,"art-hover"),e.emit("hover",!1,t)}))}},{"../utils":"71aH7","@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],eqaUm:[function(e,t,r){e("@parcel/transformer-js/src/esmodule-helpers.js").defineInteropFlag(r),r.default=function(e,t){const{$player:r}=e.template;t.proxy(r,"mousemove",(t=>{e.emit("mousemove",t)}))}},{"@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],eDXPO:[function(e,t,r){e("@parcel/transformer-js/src/esmodule-helpers.js").defineInteropFlag(r);var a=e("../utils");r.default=function(e,t){const{option:r,constructor:o}=e;e.on("resize",(()=>{const{aspectRatio:t,notice:a}=e;"standard"===e.state&&r.autoSize&&e.autoSize(),e.aspectRatio=t,a.show=""}));const n=(0,a.debounce)((()=>e.emit("resize")),o.RESIZE_TIME);t.proxy(window,["orientationchange","resize"],(()=>n())),screen&&screen.orientation&&screen.orientation.onchange&&t.proxy(screen.orientation,"change",(()=>n()))}},{"../utils":"71aH7","@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],"95GtS":[function(e,t,r){e("@parcel/transformer-js/src/esmodule-helpers.js").defineInteropFlag(r);var a=e("../utils"),o=e("../control/progress");function n(e,t,r,a){var o=t-a,n=r-e,i=0;if(Math.abs(n)<2&&Math.abs(o)<2)return i;var s=function(e,t){return 180*Math.atan2(t,e)/Math.PI}(n,o);return s>=-45&&s<45?i=4:s>=45&&s<135?i=1:s>=-135&&s<-45?i=2:(s>=135&&s<=180||s>=-180&&s<-135)&&(i=3),i}r.default=function(e,t){if(a.isMobile&&!e.option.isLive){const{$video:r,$progress:i}=e.template;let s=null,l=!1,c=0,p=0,u=0;const d=t=>{if(1===t.touches.length&&!e.isLock){s===i&&(0,o.setCurrentTime)(e,t),l=!0;const{pageX:r,pageY:a}=t.touches[0];c=r,p=a,u=e.currentTime}},f=t=>{if(1===t.touches.length&&l&&e.duration){const{pageX:o,pageY:i}=t.touches[0],l=n(c,p,o,i),d=[3,4].includes(l),f=[1,2].includes(l);if(d&&!e.isRotate||f&&e.isRotate){const t=(0,a.clamp)((o-c)/e.width,-1,1),n=(0,a.clamp)((i-p)/e.height,-1,1),l=e.isRotate?n:t,d=s===r?e.constructor.TOUCH_MOVE_RATIO:1,f=(0,a.clamp)(u+e.duration*l*d,0,e.duration);e.seek=f,e.emit("setBar","played",(0,a.clamp)(f/e.duration,0,1)),e.notice.show=`${(0,a.secondToTime)(f)} / ${(0,a.secondToTime)(e.duration)}`}}},h=()=>{l&&(c=0,p=0,u=0,l=!1,s=null)};t.proxy(i,"touchstart",(e=>{s=i,d(e)})),t.proxy(r,"touchstart",(e=>{s=r,d(e)})),t.proxy(r,"touchmove",f),t.proxy(i,"touchmove",f),t.proxy(document,"touchend",h)}}},{"../utils":"71aH7","../control/progress":"bgoVP","@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],InUBx:[function(e,t,r){e("@parcel/transformer-js/src/esmodule-helpers.js").defineInteropFlag(r);var a=e("../utils");r.default=function(e,t){const{option:r,constructor:o,template:{$container:n}}=e,i=(0,a.throttle)((()=>{e.emit("view",(0,a.isInViewport)(n,o.SCROLL_GAP))}),o.SCROLL_TIME);t.proxy(window,"scroll",(()=>i())),e.on("view",(t=>{r.autoMini&&(e.mini=!t)}))}},{"../utils":"71aH7","@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],hoLfM:[function(e,t,r){e("@parcel/transformer-js/src/esmodule-helpers.js").defineInteropFlag(r),r.default=function(e,t){t.proxy(document,"mousemove",(t=>{e.emit("document:mousemove",t)})),t.proxy(document,"mouseup",(t=>{e.emit("document:mouseup",t)}))}},{"@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],"6NoFy":[function(e,t,r){e("@parcel/transformer-js/src/esmodule-helpers.js").defineInteropFlag(r);var a=e("./utils");r.default=class{constructor(e){this.art=e,this.keys={},e.option.hotkey&&!a.isMobile&&this.init()}init(){const{proxy:e,constructor:t}=this.art;this.add(27,(()=>{this.art.fullscreenWeb&&(this.art.fullscreenWeb=!1)})),this.add(32,(()=>{this.art.toggle()})),this.add(37,(()=>{this.art.backward=t.SEEK_STEP})),this.add(38,(()=>{this.art.volume+=t.VOLUME_STEP})),this.add(39,(()=>{this.art.forward=t.SEEK_STEP})),this.add(40,(()=>{this.art.volume-=t.VOLUME_STEP})),e(window,"keydown",(e=>{if(this.art.isFocus){const t=document.activeElement.tagName.toUpperCase(),r=document.activeElement.getAttribute("contenteditable");if("INPUT"!==t&&"TEXTAREA"!==t&&""!==r&&"true"!==r){const t=this.keys[e.keyCode];if(t){e.preventDefault();for(let r=0;r{o.innerText="",(0,a.removeClass)(r,"art-notice-show")}),t.NOTICE_TIME)):(0,a.removeClass)(r,"art-notice-show")}}},{"./utils":"71aH7","@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],"5POkG":[function(e,t,r){var a=e("@parcel/transformer-js/src/esmodule-helpers.js");a.defineInteropFlag(r);var o=e("./utils"),n=e("./utils/component"),i=a.interopDefault(n);class s extends i.default{constructor(e){super(e),this.name="mask";const{template:t,icons:r,events:a}=e,n=(0,o.append)(t.$state,r.state),i=(0,o.append)(t.$state,r.error);(0,o.setStyle)(i,"display","none"),e.on("destroy",(()=>{(0,o.setStyle)(n,"display","none"),(0,o.setStyle)(i,"display",null)})),a.proxy(t.$state,"click",(()=>e.play()))}}r.default=s},{"./utils":"71aH7","./utils/component":"18nVI","@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],"6OeNg":[function(e,t,r){var a=e("@parcel/transformer-js/src/esmodule-helpers.js");a.defineInteropFlag(r);var o=e("../utils"),n=e("bundle-text:./loading.svg"),i=a.interopDefault(n),s=e("bundle-text:./state.svg"),l=a.interopDefault(s),c=e("bundle-text:./check.svg"),p=a.interopDefault(c),u=e("bundle-text:./play.svg"),d=a.interopDefault(u),f=e("bundle-text:./pause.svg"),h=a.interopDefault(f),m=e("bundle-text:./volume.svg"),g=a.interopDefault(m),v=e("bundle-text:./volume-close.svg"),y=a.interopDefault(v),b=e("bundle-text:./screenshot.svg"),x=a.interopDefault(b),w=e("bundle-text:./setting.svg"),j=a.interopDefault(w),k=e("bundle-text:./arrow-left.svg"),S=a.interopDefault(k),I=e("bundle-text:./arrow-right.svg"),C=a.interopDefault(I),P=e("bundle-text:./playback-rate.svg"),$=a.interopDefault(P),M=e("bundle-text:./aspect-ratio.svg"),T=a.interopDefault(M),E=e("bundle-text:./config.svg"),F=a.interopDefault(E),A=e("bundle-text:./pip.svg"),z=a.interopDefault(A),H=e("bundle-text:./lock.svg"),D=a.interopDefault(H),R=e("bundle-text:./unlock.svg"),O=a.interopDefault(R),L=e("bundle-text:./fullscreen-off.svg"),V=a.interopDefault(L),N=e("bundle-text:./fullscreen-on.svg"),Y=a.interopDefault(N),_=e("bundle-text:./fullscreen-web-off.svg"),W=a.interopDefault(_),q=e("bundle-text:./fullscreen-web-on.svg"),B=a.interopDefault(q),U=e("bundle-text:./switch-on.svg"),K=a.interopDefault(U),G=e("bundle-text:./switch-off.svg"),Z=a.interopDefault(G),X=e("bundle-text:./flip.svg"),J=a.interopDefault(X),Q=e("bundle-text:./error.svg"),ee=a.interopDefault(Q),te=e("bundle-text:./close.svg"),re=a.interopDefault(te),ae=e("bundle-text:./airplay.svg"),oe=a.interopDefault(ae);r.default=class{constructor(e){const t={loading:i.default,state:l.default,play:d.default,pause:h.default,check:p.default,volume:g.default,volumeClose:y.default,screenshot:x.default,setting:j.default,pip:z.default,arrowLeft:S.default,arrowRight:C.default,playbackRate:$.default,aspectRatio:T.default,config:F.default,lock:D.default,flip:J.default,unlock:O.default,fullscreenOff:V.default,fullscreenOn:Y.default,fullscreenWebOff:W.default,fullscreenWebOn:B.default,switchOn:K.default,switchOff:Z.default,error:ee.default,close:re.default,airplay:oe.default,...e.option.icons};for(const e in t)(0,o.def)(this,e,{get:()=>(0,o.getIcon)(e,t[e])})}}},{"../utils":"71aH7","bundle-text:./loading.svg":"7tDub","bundle-text:./state.svg":"1ElZc","bundle-text:./check.svg":"lmgoP","bundle-text:./play.svg":"lVWoQ","bundle-text:./pause.svg":"5Mnax","bundle-text:./volume.svg":"w3eIa","bundle-text:./volume-close.svg":"rHjo1","bundle-text:./screenshot.svg":"2KcqM","bundle-text:./setting.svg":"8rQMV","bundle-text:./arrow-left.svg":"kqGBE","bundle-text:./arrow-right.svg":"aFjpC","bundle-text:./playback-rate.svg":"lx7ZM","bundle-text:./aspect-ratio.svg":"2sEjf","bundle-text:./config.svg":"fQTgE","bundle-text:./pip.svg":"2CaxO","bundle-text:./lock.svg":"aCGnW","bundle-text:./unlock.svg":"bTrAV","bundle-text:./fullscreen-off.svg":"bA3p0","bundle-text:./fullscreen-on.svg":"fTuY8","bundle-text:./fullscreen-web-off.svg":"tvKf4","bundle-text:./fullscreen-web-on.svg":"1F1oB","bundle-text:./switch-on.svg":"7qNHs","bundle-text:./switch-off.svg":"28aV8","bundle-text:./flip.svg":"1uXI6","bundle-text:./error.svg":"9f4dh","bundle-text:./close.svg":"4nTtS","bundle-text:./airplay.svg":"cDPXC","@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],"7tDub":[function(e,t,r){t.exports=''},{}],"1ElZc":[function(e,t,r){t.exports=''},{}],lmgoP:[function(e,t,r){t.exports=''},{}],lVWoQ:[function(e,t,r){t.exports=''},{}],"5Mnax":[function(e,t,r){t.exports=''},{}],w3eIa:[function(e,t,r){t.exports=''},{}],rHjo1:[function(e,t,r){t.exports=''},{}],"2KcqM":[function(e,t,r){t.exports=''},{}],"8rQMV":[function(e,t,r){t.exports=''},{}],kqGBE:[function(e,t,r){t.exports=''},{}],aFjpC:[function(e,t,r){t.exports=''},{}],lx7ZM:[function(e,t,r){t.exports=''},{}],"2sEjf":[function(e,t,r){t.exports=''},{}],fQTgE:[function(e,t,r){t.exports=''},{}],"2CaxO":[function(e,t,r){t.exports=''},{}],aCGnW:[function(e,t,r){t.exports=''},{}],bTrAV:[function(e,t,r){t.exports=''},{}],bA3p0:[function(e,t,r){t.exports=''},{}],fTuY8:[function(e,t,r){t.exports=''},{}],tvKf4:[function(e,t,r){t.exports=''},{}],"1F1oB":[function(e,t,r){t.exports=''},{}],"7qNHs":[function(e,t,r){t.exports=''},{}],"28aV8":[function(e,t,r){t.exports=''},{}],"1uXI6":[function(e,t,r){t.exports=''},{}],"9f4dh":[function(e,t,r){t.exports=''},{}],"4nTtS":[function(e,t,r){t.exports=''},{}],cDPXC:[function(e,t,r){t.exports=''},{}],"3eYNH":[function(e,t,r){var a=e("@parcel/transformer-js/src/esmodule-helpers.js");a.defineInteropFlag(r);var o=e("./flip"),n=a.interopDefault(o),i=e("./aspectRatio"),s=a.interopDefault(i),l=e("./playbackRate"),c=a.interopDefault(l),p=e("./subtitleOffset"),u=a.interopDefault(p),d=e("../utils/component"),f=a.interopDefault(d),h=e("../utils/error"),m=e("../utils");class g extends f.default{constructor(e){super(e);const{option:t,controls:r,template:{$setting:a}}=e;this.name="setting",this.$parent=a,this.option=[],this.events=[],this.cache=new Map,t.setting&&(this.init(),e.on("blur",(()=>{this.show&&(this.show=!1,this.render(this.option))})),e.on("focus",(e=>{const t=(0,m.includeFromEvent)(e,r.setting),a=(0,m.includeFromEvent)(e,this.$parent);!this.show||t||a||(this.show=!1,this.render(this.option))})))}static makeRecursion(e,t,r){for(let a=0;a'),i=(0,m.createElement)("div");(0,m.addClass)(i,"art-setting-item-left-icon"),(0,m.append)(i,t.arrowLeft),(0,m.append)(n,i),(0,m.append)(n,e.$parentItem.html);const s=r(o,"click",(()=>this.render(e.$parentList)));return this.events.push(s),o}creatItem(e,t){const{icons:r,proxy:a,constructor:o}=this.art,n=(0,m.createElement)("div");(0,m.addClass)(n,"art-setting-item"),(0,m.setStyle)(n,"height",`${o.SETTING_ITEM_HEIGHT}px`),(0,m.isStringOrNumber)(t.name)&&(n.dataset.name=t.name),(0,m.isStringOrNumber)(t.value)&&(n.dataset.value=t.value);const i=(0,m.append)(n,'
    '),s=(0,m.append)(n,'
    '),l=(0,m.createElement)("div");switch((0,m.addClass)(l,"art-setting-item-left-icon"),e){case"switch":case"range":(0,m.append)(l,(0,m.isStringOrNumber)(t.icon)||t.icon instanceof Element?t.icon:r.config);break;case"selector":t.selector&&t.selector.length?(0,m.append)(l,(0,m.isStringOrNumber)(t.icon)||t.icon instanceof Element?t.icon:r.config):(0,m.append)(l,r.check)}(0,m.append)(i,l),t.$icon=l,(0,m.def)(t,"icon",{configurable:!0,get:()=>l.innerHTML,set(e){(0,m.isStringOrNumber)(e)&&(l.innerHTML=e)}});const c=(0,m.createElement)("div");(0,m.addClass)(c,"art-setting-item-left-text"),(0,m.append)(c,t.html||""),(0,m.append)(i,c),t.$html=c,(0,m.def)(t,"html",{configurable:!0,get:()=>c.innerHTML,set(e){(0,m.isStringOrNumber)(e)&&(c.innerHTML=e)}});const p=(0,m.createElement)("div");switch((0,m.addClass)(p,"art-setting-item-right-tooltip"),(0,m.append)(p,t.tooltip||""),(0,m.append)(s,p),t.$tooltip=p,(0,m.def)(t,"tooltip",{configurable:!0,get:()=>p.innerHTML,set(e){(0,m.isStringOrNumber)(e)&&(p.innerHTML=e)}}),e){case"switch":{const e=(0,m.createElement)("div");(0,m.addClass)(e,"art-setting-item-right-icon");const a=(0,m.append)(e,r.switchOn),o=(0,m.append)(e,r.switchOff);(0,m.setStyle)(t.switch?o:a,"display","none"),(0,m.append)(s,e),t.$switch=t.switch,(0,m.def)(t,"switch",{configurable:!0,get:()=>t.$switch,set(e){t.$switch=e,e?((0,m.setStyle)(o,"display","none"),(0,m.setStyle)(a,"display",null)):((0,m.setStyle)(o,"display",null),(0,m.setStyle)(a,"display","none"))}});break}case"range":{const e=(0,m.createElement)("div");(0,m.addClass)(e,"art-setting-item-right-icon");const r=(0,m.append)(e,'');r.value=t.range[0]||0,r.min=t.range[1]||0,r.max=t.range[2]||10,r.step=t.range[3]||1,(0,m.addClass)(r,"art-setting-range"),(0,m.append)(s,e),t.$range=r,(0,m.def)(t,"range",{configurable:!0,get:()=>r.valueAsNumber,set(e){r.value=Number(e)}})}break;case"selector":if(t.selector&&t.selector.length){const e=(0,m.createElement)("div");(0,m.addClass)(e,"art-setting-item-right-icon"),(0,m.append)(e,r.arrowRight),(0,m.append)(s,e)}}switch(e){case"switch":if(t.onSwitch){const e=a(n,"click",(async e=>{t.switch=await t.onSwitch.call(this.art,t,n,e)}));this.events.push(e)}break;case"range":if(t.$range){if(t.onRange){const e=a(t.$range,"change",(async e=>{t.tooltip=await t.onRange.call(this.art,t,n,e)}));this.events.push(e)}if(t.onChange){const e=a(t.$range,"input",(async e=>{t.tooltip=await t.onChange.call(this.art,t,n,e)}));this.events.push(e)}}break;case"selector":{const e=a(n,"click",(async e=>{if(t.selector&&t.selector.length)this.render(t.selector,t.width);else{(0,m.inverseClass)(n,"art-current");for(let e=0;ec?((0,m.setStyle)(o,"left",null),(0,m.setStyle)(o,"right",null)):((0,m.setStyle)(o,"left",`${p}px`),(0,m.setStyle)(o,"right","auto"))}}render(e,t){const{constructor:r}=this.art;if(this.cache.has(e)){const t=this.cache.get(e);(0,m.inverseClass)(t,"art-current"),(0,m.setStyle)(this.$parent,"width",`${t.dataset.width}px`),(0,m.setStyle)(this.$parent,"height",`${t.dataset.height}px`),this.updateStyle(Number(t.dataset.width))}else{const a=(0,m.createElement)("div");(0,m.addClass)(a,"art-setting-panel"),a.dataset.width=t||r.SETTING_WIDTH,a.dataset.height=e.length*r.SETTING_ITEM_HEIGHT,e[0]&&e[0].$parentItem&&((0,m.append)(a,this.creatHeader(e[0])),a.dataset.height=Number(a.dataset.height)+r.SETTING_ITEM_HEIGHT);for(let t=0;te.dataset.value===o));n&&(0,a.inverseClass)(n,"art-current")}return{width:o,name:"flip",html:t.get("Video Flip"),tooltip:t.get((0,a.capitalize)(e.flip)),icon:r.flip,selector:n.map((r=>({value:r,name:`aspect-ratio-${r}`,default:r===e.flip,html:t.get((0,a.capitalize)(r))}))),onSelect:t=>(e.flip=t.value,t.html),mounted:(t,r)=>{i(t,r.$tooltip,e.flip),e.on("flip",(()=>{i(t,r.$tooltip,e.flip)}))}}}},{"../utils":"71aH7","@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],"84NBV":[function(e,t,r){e("@parcel/transformer-js/src/esmodule-helpers.js").defineInteropFlag(r);var a=e("../utils");r.default=function(e){const{i18n:t,icons:r,constructor:{SETTING_ITEM_WIDTH:o,ASPECT_RATIO:n}}=e;function i(e){return"default"===e?t.get("Default"):e}function s(e,t,r){t&&(t.innerText=i(r));const o=(0,a.queryAll)(".art-setting-item",e).find((e=>e.dataset.value===r));o&&(0,a.inverseClass)(o,"art-current")}return{width:o,name:"aspect-ratio",html:t.get("Aspect Ratio"),icon:r.aspectRatio,tooltip:i(e.aspectRatio),selector:n.map((t=>({value:t,name:`aspect-ratio-${t}`,default:t===e.aspectRatio,html:i(t)}))),onSelect:t=>(e.aspectRatio=t.value,t.html),mounted:(t,r)=>{s(t,r.$tooltip,e.aspectRatio),e.on("aspectRatio",(()=>{s(t,r.$tooltip,e.aspectRatio)}))}}}},{"../utils":"71aH7","@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],aetWt:[function(e,t,r){e("@parcel/transformer-js/src/esmodule-helpers.js").defineInteropFlag(r);var a=e("../utils");r.default=function(e){const{i18n:t,icons:r,constructor:{SETTING_ITEM_WIDTH:o,PLAYBACK_RATE:n}}=e;function i(e){return 1===e?t.get("Normal"):e.toFixed(1)}function s(e,t,r){t&&(t.innerText=i(r));const o=(0,a.queryAll)(".art-setting-item",e).find((e=>Number(e.dataset.value)===r));o&&(0,a.inverseClass)(o,"art-current")}return{width:o,name:"playback-rate",html:t.get("Play Speed"),tooltip:i(e.playbackRate),icon:r.playbackRate,selector:n.map((t=>({value:t,name:`aspect-ratio-${t}`,default:t===e.playbackRate,html:i(t)}))),onSelect:t=>(e.playbackRate=t.value,t.html),mounted:(t,r)=>{s(t,r.$tooltip,e.playbackRate),e.on("video:ratechange",(()=>{s(t,r.$tooltip,e.playbackRate)}))}}}},{"../utils":"71aH7","@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],fIBkO:[function(e,t,r){e("@parcel/transformer-js/src/esmodule-helpers.js").defineInteropFlag(r),r.default=function(e){const{i18n:t,icons:r,constructor:a}=e;return{width:a.SETTING_ITEM_WIDTH,name:"subtitle-offset",html:t.get("Subtitle Offset"),icon:r.subtitle,tooltip:"0s",range:[0,-5,5,.1],onChange:t=>(e.subtitleOffset=t.range,t.range+"s")}}},{"@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],"2aaJe":[function(e,t,r){e("@parcel/transformer-js/src/esmodule-helpers.js").defineInteropFlag(r);r.default=class{constructor(){this.name="artplayer_settings",this.settings={}}get(e){try{const t=JSON.parse(window.localStorage.getItem(this.name))||{};return e?t[e]:t}catch(t){return e?this.settings[e]:this.settings}}set(e,t){try{const r=Object.assign({},this.get(),{[e]:t});window.localStorage.setItem(this.name,JSON.stringify(r))}catch(r){this.settings[e]=t}}del(e){try{const t=this.get();delete t[e],window.localStorage.setItem(this.name,JSON.stringify(t))}catch(t){delete this.settings[e]}}clear(){try{window.localStorage.removeItem(this.name)}catch(e){this.settings={}}}}},{"@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],"8MTUM":[function(e,t,r){var a=e("@parcel/transformer-js/src/esmodule-helpers.js");a.defineInteropFlag(r);var o=e("../utils"),n=e("./miniProgressBar"),i=a.interopDefault(n),s=e("./autoOrientation"),l=a.interopDefault(s),c=e("./autoPlayback"),p=a.interopDefault(c),u=e("./fastForward"),d=a.interopDefault(u),f=e("./lock"),h=a.interopDefault(f);r.default=class{constructor(e){this.art=e,this.id=0;const{option:t}=e;t.miniProgressBar&&!t.isLive&&this.add(i.default),t.lock&&o.isMobile&&this.add(h.default),t.autoPlayback&&!t.isLive&&this.add(p.default),t.autoOrientation&&o.isMobile&&this.add(l.default),t.fastForward&&o.isMobile&&!t.isLive&&this.add(d.default);for(let e=0;e{e.layers.add({name:"mini-progress-bar",mounted(t){e.on("destroy",(()=>{t.style.display="none"})),e.on("video:timeupdate",(()=>{t.style.width=100*e.played+"%"})),e.on("setBar",((e,r)=>{"played"===e&&(t.style.width=100*r+"%")}))}})})),{name:"mini-progress-bar"}}},{"@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],ePEg5:[function(e,t,r){e("@parcel/transformer-js/src/esmodule-helpers.js").defineInteropFlag(r);var a=e("../utils");r.default=function(e){const{constructor:t,template:{$player:r,$video:o}}=e;return e.on("fullscreenWeb",(n=>{if(n){const{videoWidth:n,videoHeight:i}=o,{clientWidth:s,clientHeight:l}=document.documentElement;(n>i&&sl)&&setTimeout((()=>{(0,a.setStyle)(r,"width",`${l}px`),(0,a.setStyle)(r,"height",`${s}px`),(0,a.setStyle)(r,"transform-origin","0 0"),(0,a.setStyle)(r,"transform",`rotate(90deg) translate(0, -${s}px)`),(0,a.addClass)(r,"art-auto-orientation"),e.isRotate=!0,e.emit("resize")}),t.AUTO_ORIENTATION_TIME)}else(0,a.hasClass)(r,"art-auto-orientation")&&((0,a.removeClass)(r,"art-auto-orientation"),e.isRotate=!1,e.emit("resize"))})),e.on("fullscreen",(async e=>{const t=screen.orientation.type;if(e){const{videoWidth:e,videoHeight:n}=o,{clientWidth:i,clientHeight:s}=document.documentElement;if(e>n&&is){const e=t.startsWith("portrait")?"landscape":"portrait";await screen.orientation.lock(e),(0,a.addClass)(r,"art-auto-orientation-fullscreen")}}else(0,a.hasClass)(r,"art-auto-orientation-fullscreen")&&(await screen.orientation.lock(t),(0,a.removeClass)(r,"art-auto-orientation-fullscreen"))})),{name:"autoOrientation",get state(){return(0,a.hasClass)(r,"art-auto-orientation")}}}},{"../utils":"71aH7","@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],cVO99:[function(e,t,r){e("@parcel/transformer-js/src/esmodule-helpers.js").defineInteropFlag(r);var a=e("../utils");r.default=function(e){const{i18n:t,icons:r,storage:o,constructor:n,proxy:i,template:{$poster:s}}=e,l=e.layers.add({name:"auto-playback",html:'
    '}),c=(0,a.query)(".art-auto-playback-last",l),p=(0,a.query)(".art-auto-playback-jump",l),u=(0,a.query)(".art-auto-playback-close",l);return e.on("video:timeupdate",(()=>{if(e.playing){const t=o.get("times")||{},r=Object.keys(t);r.length>n.AUTO_PLAYBACK_MAX&&delete t[r[0]],t[e.option.id||e.option.url]=e.currentTime,o.set("times",t)}})),e.on("ready",(()=>{const d=(o.get("times")||{})[e.option.id||e.option.url];d&&d>=n.AUTO_PLAYBACK_MIN&&((0,a.append)(u,r.close),(0,a.setStyle)(l,"display","flex"),c.innerText=`${t.get("Last Seen")} ${(0,a.secondToTime)(d)}`,p.innerText=t.get("Jump Play"),i(u,"click",(()=>{(0,a.setStyle)(l,"display","none")})),i(p,"click",(()=>{e.seek=d,e.play(),(0,a.setStyle)(s,"display","none"),(0,a.setStyle)(l,"display","none")})),e.once("video:timeupdate",(()=>{setTimeout((()=>{(0,a.setStyle)(l,"display","none")}),n.AUTO_PLAYBACK_TIMEOUT)})))})),{name:"auto-playback",get times(){return o.get("times")||{}},clear:()=>o.del("times"),delete(e){const t=o.get("times")||{};return delete t[e],o.set("times",t),t}}}},{"../utils":"71aH7","@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],hFDwt:[function(e,t,r){e("@parcel/transformer-js/src/esmodule-helpers.js").defineInteropFlag(r);var a=e("../utils");r.default=function(e){const{constructor:t,proxy:r,template:{$player:o,$video:n}}=e;let i=null,s=!1,l=1;const c=()=>{clearTimeout(i),s&&(s=!1,e.playbackRate=l,(0,a.removeClass)(o,"art-fast-forward"))};return r(n,"touchstart",(r=>{1===r.touches.length&&e.playing&&!e.isLock&&(i=setTimeout((()=>{s=!0,l=e.playbackRate,e.playbackRate=t.FAST_FORWARD_VALUE,(0,a.addClass)(o,"art-fast-forward")}),t.FAST_FORWARD_TIME))})),r(document,"touchmove",c),r(document,"touchend",c),{name:"fastForward",get state(){return(0,a.hasClass)(o,"art-fast-forward")}}}},{"../utils":"71aH7","@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}],"1hsTH":[function(e,t,r){e("@parcel/transformer-js/src/esmodule-helpers.js").defineInteropFlag(r);var a=e("../utils");r.default=function(e){const{layers:t,icons:r,template:{$player:o}}=e;return t.add({name:"lock",mounted(t){const o=(0,a.append)(t,r.lock),n=(0,a.append)(t,r.unlock);(0,a.setStyle)(o,"display","none"),e.on("lock",(e=>{e?((0,a.setStyle)(o,"display","inline-flex"),(0,a.setStyle)(n,"display","none")):((0,a.setStyle)(o,"display","none"),(0,a.setStyle)(n,"display","inline-flex"))}))},click(){(0,a.hasClass)(o,"art-lock")?((0,a.removeClass)(o,"art-lock"),this.isLock=!1,e.emit("lock",!1)):((0,a.addClass)(o,"art-lock"),this.isLock=!0,e.emit("lock",!0))}}),{name:"lock",get state(){return(0,a.hasClass)(o,"art-lock")}}}},{"../utils":"71aH7","@parcel/transformer-js/src/esmodule-helpers.js":"9pCYc"}]},["5lTcX"],"5lTcX","parcelRequire4dc0"); \ No newline at end of file diff --git a/plugin/think-plugs-static/stc/public/static/plugs/jquery/autocompleter.css b/plugin/think-plugs-static/stc/public/static/plugs/jquery/autocompleter.css new file mode 100644 index 000000000..29a66b72d --- /dev/null +++ b/plugin/think-plugs-static/stc/public/static/plugs/jquery/autocompleter.css @@ -0,0 +1,79 @@ +.autocompleter { + left: 0; + top: 37px; + min-width: 100%; + z-index: 100; + background: #fff; + position: absolute; +} + +.autocompleter, .autocompleter-hint { + position: absolute +} + +.autocompleter-focus .autocompleter-list { + border: 1px solid #c9c9c9; + border-bottom-left-radius: 2px; + border-bottom-right-radius: 2px; +} + +.autocompleter-list { + margin: 0; + padding: 0; + text-align: left; + list-style: none; + box-sizing: border-box; + -webkit-box-sizing: border-box; +} + +.autocompleter-item { + color: #444; + cursor: pointer; + padding: 6px 12px; +} + +.autocompleter-item strong { + color: #00B83F; + font-weight: 400; +} + +.autocompleter-item span { + color: #aaa +} + +.autocompleter-item:hover, +.autocompleter-item-selected { + color: #fff; + background: #009688 !important; +} + +.autocompleter-item:hover span, +.autocompleter-item:hover strong, +.autocompleter-item-selected span, +.autocompleter-item-selected strong { + color: #fff; +} + +.autocompleter-hint { + left: 0; + top: -56px; + width: 100%; + color: #ccc; + display: none; + font-size: 24px; + text-align: left; + font-weight: 400; + padding: 12px 12px 12px 13px; +} + +.autocompleter-hint span { + color: transparent +} + +.autocompleter-hint-show { + display: block +} + +.autocompleter-closed { + display: none +} \ No newline at end of file diff --git a/plugin/think-plugs-static/stc/public/static/plugs/jquery/autocompleter.min.js b/plugin/think-plugs-static/stc/public/static/plugs/jquery/autocompleter.min.js new file mode 100644 index 000000000..d92a1cb86 --- /dev/null +++ b/plugin/think-plugs-static/stc/public/static/plugs/jquery/autocompleter.min.js @@ -0,0 +1,8 @@ +/** + * jquery-autocompleter v0.3.0 - 2018-03-08 + * Easy customisable and with localStorage cache support. + * http://github.com/ArtemFitiskin/jquery-autocompleter + * + * @license (c) 2018 Artem Fitiskin MIT Licensed + */ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t():"function"==typeof define&&define.amd?define(t):t()}(0,function(){"use strict";var e="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e};!function(t,o){var a=0,l=[9,13,17,19,20,27,33,34,35,36,37,39,44,92,113,114,115,118,119,120,122,123,144,145],n=["source","empty","limit","cache","cacheExpires","focusOpen","selectFirst","changeWhenSelect","highlightMatches","ignoredKeyCode","customLabel","customValue","template","offset","combine","callback","minLength","delay"],r=o.navigator.userAgent||o.navigator.vendor||o.opera,c=/Firefox/i.test(r),u=/Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(r),i=c&&u,s=null,p=null,m="autocompleterCache",d=function(){var e=void 0!==o.localStorage;if(e)try{localStorage.setItem("autocompleter","autocompleter"),localStorage.removeItem("autocompleter")}catch(t){e=!1}return e}(),f={source:null,asLocal:!1,empty:!0,limit:10,minLength:0,delay:0,customClass:[],cache:!0,cacheExpires:86400,focusOpen:!0,hint:!1,selectFirst:!1,changeWhenSelect:!0,highlightMatches:!1,ignoredKeyCode:[],customLabel:!1,customValue:!1,template:!1,offset:!1,combine:null,callback:t.noop},h={defaults:function(o){return f=t.extend(f,o||{}),"object"!==e(this)||t(this)},option:function(e){return t(this).each(function(o,a){var l=t(a).next(".autocompleter").data("autocompleter");for(var r in e)-1!==t.inArray(r,n)&&(l[r]=e[r])})},open:function(){return t(this).each(function(e,o){var a=t(o).next(".autocompleter").data("autocompleter");a&&S(null,a)})},close:function(){return t(this).each(function(e,o){var a=t(o).next(".autocompleter").data("autocompleter");a&&k(null,a)})},clearCache:function(){O()},destroy:function(){return t(this).each(function(e,o){var a=t(o).next(".autocompleter").data("autocompleter");a&&(a.jqxhr&&a.jqxhr.abort(),a.$autocompleter.hasClass("open")&&a.$autocompleter.find(".autocompleter-selected").trigger("click.autocompleter"),a.originalAutocomplete?a.$node.attr("autocomplete",a.originalAutocomplete):a.$node.removeAttr("autocomplete"),a.$node.off(".autocompleter").removeClass("autocompleter-node"),a.$autocompleter.off(".autocompleter").remove())})}};function v(e,o){if(!e.hasClass("autocompleter-node")){"string"!=typeof(o=t.extend({},o,e.data("autocompleter-options"))).source||".json"!==o.source.slice(-5)&&!0!==o.asLocal||t.ajax({url:o.source,type:"GET",dataType:"json",async:!1}).done(function(e){o.source=e});var l='
    ';o.hint&&(l+='
    '),l+='
      ',l+="
      ",e.addClass("autocompleter-node").after(l);var n=e.next(".autocompleter").eq(0),r=e.attr("autocomplete");e.attr("autocomplete","off");var c=t.extend({$node:e,$autocompleter:n,$selected:null,$list:null,index:-1,hintText:!1,source:!1,jqxhr:!1,response:null,focused:!1,query:"",originalAutocomplete:r,guid:a++},o);c.$autocompleter.on("mousedown.autocompleter",".autocompleter-item",c,E).data("autocompleter",c),c.$node.on("keyup.autocompleter",c,x).on("keydown.autocompleter",c,C).on("focus.autocompleter",c,w).on("blur.autocompleter",c,q).on("mousedown.autocompleter",c,j)}}function y(e){clearTimeout(p),e.query=t.trim(e.$node.val()),!e.empty&&0===e.query.length||e.minLength&&e.query.length1e3*t)&&a)}(this.url,o.cacheExpires);t&&(e.abort(),b(t,o))}}}).done(function(e){o.offset&&(e=function(e,t){t=t.split(".");for(;e&&t.length;)e=e[t.shift()];return e}(e,o.offset)),o.cache&&function(e,t){if(!d)return;if(e&&t){D[e]={value:t,timestamp:+new Date};try{localStorage.setItem(m,JSON.stringify(D))}catch(e){var o=e.code||e.number||e.message;if(22!==o)throw e;O()}}}(this.url,e),b(e,o)}).always(function(){o.$autocompleter.removeClass("autocompleter-ajax")})}}function g(e){e.response=null,e.$list=null,e.$selected=null,e.index=0,e.$autocompleter.find(".autocompleter-list").empty(),e.$autocompleter.find(".autocompleter-hint").removeClass("autocompleter-hint-show").empty(),e.hintText=!1,k(null,e)}function b(e,o){!function(e,o){for(var a="",l=0,n=e.length;l$&"):u;var s=o.customValue&&e[l][o.customValue]?e[l][o.customValue]:e[l].value;if(o.template){var p=o.template.replace(/({{ label }})/gi,u);for(var m in e[l])if(e[l].hasOwnProperty(m)){var d=new RegExp("{{ "+m+" }}","gi");p=p.replace(d,e[l][m])}u=p}a+=s?'
    • '+u+"
    • ":'
    • '+u+"
    • "}if(e.length&&o.hint){var f=o.customLabel&&e[0][o.customLabel]?e[0][o.customLabel]:e[0].label,h=f.substr(0,o.query.length).toUpperCase()===o.query.toUpperCase()&&f;if(h&&o.query!==f){var v=new RegExp(o.query,"i"),y=h.replace(v,""+o.query+"");o.$autocompleter.find(".autocompleter-hint").addClass("autocompleter-hint-show").html(y),o.hintText=y}}o.response=e,o.$autocompleter.find(".autocompleter-list").html(a),o.$selected=o.$autocompleter.find(".autocompleter-item-selected").length?o.$autocompleter.find(".autocompleter-item-selected"):null,o.$list=e.length?o.$autocompleter.find(".autocompleter-item"):null,o.index=o.$selected?o.$list.index(o.$selected):-1,o.$autocompleter.find(".autocompleter-item").each(function(e,a){t(a).data(o.response[e])})}(e,o),o.$autocompleter.hasClass("autocompleter-focus")&&S(null,o)}function x(e){var o=e.data,a=e.keyCode?e.keyCode:e.which;if(40!==a&&38!==a||!o.$autocompleter.hasClass("autocompleter-show"))-1===t.inArray(a,l)&&-1===t.inArray(a,o.ignoredKeyCode)&&y(o);else{var n,r,c=o.$list.length;c&&(c>1?o.index===c-1?(n=o.changeWhenSelect?-1:0,r=o.index-1):0===o.index?(n=o.index+1,r=o.changeWhenSelect?-1:c-1):-1===o.index?(n=0,r=c-1):(n=o.index+1,r=o.index-1):-1===o.index?(n=0,r=0):(r=-1,n=-1),o.index=40===a?n:r,o.$list.removeClass("autocompleter-item-selected"),-1!==o.index&&o.$list.eq(o.index).addClass("autocompleter-item-selected"),o.$selected=o.$autocompleter.find(".autocompleter-item-selected").length?o.$autocompleter.find(".autocompleter-item-selected"):null,o.changeWhenSelect&&A(o))}}function C(e){var t=e.data,o=e.keyCode?e.keyCode:e.which;if(40===o||38===o)e.preventDefault(),e.stopPropagation();else if(39===o){if(t.hint&&t.hintText&&t.$autocompleter.find(".autocompleter-hint").hasClass("autocompleter-hint-show")){e.preventDefault(),e.stopPropagation();var a=!!t.$autocompleter.find(".autocompleter-item").length&&t.$autocompleter.find(".autocompleter-item").eq(0).attr("data-label");a&&(t.query=a,function(e){A(e),T(e),y(e)}(t))}}else 13===o&&t.$autocompleter.hasClass("autocompleter-show")&&t.$selected&&E(e)}function w(e,t){if(!t){var o=e.data;o.$autocompleter.addClass("autocompleter-focus"),o.$node.prop("disabled")||o.$autocompleter.hasClass("autocompleter-show")||o.focusOpen&&(y(o),o.focused=!0,setTimeout(function(){o.focused=!1},500))}}function q(e,t){e.preventDefault(),e.stopPropagation();var o=e.data;t||(o.$autocompleter.removeClass("autocompleter-focus"),k(e))}function j(e){if("mousedown"!==e.type||-1===t.inArray(e.which,[2,3])){var a=e.data;if(a.$list&&!a.focused&&!a.$node.is(":disabled"))if(u&&!i){var l=a.$select[0];if(o.document.createEvent){var n=o.document.createEvent("MouseEvents");n.initMouseEvent("mousedown",!1,!0,o,0,0,0,0,0,!1,!1,!1,!1,0,null),l.dispatchEvent(n)}else l.fireEvent&&l.fireEvent("onmousedown")}else a.$autocompleter.hasClass("autocompleter-closed")?S(e):a.$autocompleter.hasClass("autocompleter-show")&&k(e)}}function S(e,t){var o=e?e.data:t;!o.$node.prop("disabled")&&!o.$autocompleter.hasClass("autocompleter-show")&&o.$list&&o.$list.length&&(o.$autocompleter.removeClass("autocompleter-closed").addClass("autocompleter-show"),s.on("click.autocompleter-"+o.guid,":not(.autocompleter-item)",o,L))}function L(e){t(e.target).hasClass("autocompleter-node")||0===t(e.currentTarget).parents(".autocompleter").length&&k(e)}function k(e,t){var o=e?e.data:t;o.$autocompleter.hasClass("autocompleter-show")&&(o.$autocompleter.removeClass("autocompleter-show").addClass("autocompleter-closed"),s.off(".autocompleter-"+o.guid))}function E(e){if("mousedown"!==e.type||-1===t.inArray(e.which,[2,3])){var o=e.data;e.preventDefault(),e.stopPropagation(),"mousedown"===e.type&&t(this).length&&(o.$selected=t(this),o.index=o.$list.index(o.$selected)),o.$node.prop("disabled")||(k(e),function(e){A(e),T(e),g(e)}(o),"click"===e.type&&o.$node.trigger("focus",[!0]))}}function A(e){e.$selected?(e.hintText&&e.$autocompleter.find(".autocompleter-hint").hasClass("autocompleter-hint-show")&&e.$autocompleter.find(".autocompleter-hint").removeClass("autocompleter-hint-show"),e.$node.val(e.$selected.attr("data-value")?e.$selected.attr("data-value"):e.$selected.attr("data-label"))):(e.hintText&&!e.$autocompleter.find(".autocompleter-hint").hasClass("autocompleter-hint-show")&&e.$autocompleter.find(".autocompleter-hint").addClass("autocompleter-hint-show"),e.$node.val(e.query))}function T(e){e.callback.call(e.$autocompleter,e.$node.val(),e.index,e.response[e.index]),e.$node.trigger("change")}function P(){if(d)return JSON.parse(localStorage.getItem(m)||"{}")}function O(){try{localStorage.removeItem(m),D=P()}catch(e){throw e}}var D=P();t.fn.autocompleter=function(o){return h[o]?h[o].apply(this,Array.prototype.slice.call(arguments,1)):"object"!==(void 0===o?"undefined":e(o))&&o?this:function(e){e=t.extend({},f,e||{}),null===s&&(s=t("body"));for(var o=t(this),a=0,l=o.length;a>8-i%1*8)){if(n=a.charCodeAt(i+=.75),n>255)throw new t("'btoa' failed: The string to be encoded contains characters outside of the Latin1 range.");o=o<<8|n}return d}),r.atob||(r.atob=function(r){var o=String(r).replace(/=+$/,"");if(o.length%4==1)throw new t("'atob' failed: The string to be decoded is not correctly encoded.");for(var n,a,i=0,c=0,d="";a=o.charAt(c++);~a&&(n=i%4?64*n+a:a,i++%4)?d+=String.fromCharCode(255&n>>(-2*i&6)):0)a=e.indexOf(a);return d})}(); \ No newline at end of file diff --git a/plugin/think-plugs-static/stc/public/static/plugs/jquery/compressor.min.js b/plugin/think-plugs-static/stc/public/static/plugs/jquery/compressor.min.js new file mode 100644 index 000000000..8c00bc0d1 --- /dev/null +++ b/plugin/think-plugs-static/stc/public/static/plugs/jquery/compressor.min.js @@ -0,0 +1,10 @@ +/*! + * Compressor.js v1.2.1 + * https://fengyuanchen.github.io/compressorjs + * + * Copyright 2018-present Chen Fengyuan + * Released under the MIT license + * + * Date: 2023-02-28T14:09:41.732Z + */ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).Compressor=t()}(this,function(){"use strict";function t(t,e){var r,i=Object.keys(t);return Object.getOwnPropertySymbols&&(r=Object.getOwnPropertySymbols(t),e&&(r=r.filter(function(e){return Object.getOwnPropertyDescriptor(t,e).enumerable})),i.push.apply(i,r)),i}function n(i){for(var e=1;es.convertSize&&0<=s.convertTypes.indexOf(s.mimeType)&&(s.mimeType="image/jpeg"),"image/jpeg"===s.mimeType);h.fillStyle=m=U?"#fff":m,h.fillRect(0,0,y,w),s.beforeDraw&&s.beforeDraw.call(this,h,c),this.aborted||(h.save(),h.translate(y/2,w/2),h.rotate(t*Math.PI/180),h.scale(r,e),h.drawImage.apply(h,[l].concat(p)),h.restore(),s.drew&&s.drew.call(this,h,c),this.aborted)||(f=function(e){var i,t,r;n.aborted||(i=function(e){return n.done({naturalWidth:a,naturalHeight:o,result:e})},e&&U&&s.retainExif&&n.exif&&0i.size&&a.mimeType===i.type&&!(a.width>t||a.height>r||a.minWidth>t||a.minHeight>r||a.maxWidth=b.status}function e(a){try{a.dispatchEvent(new MouseEvent("click"))}catch(c){var b=document.createEvent("MouseEvents");b.initMouseEvent("click",!0,!0,window,0,0,0,80,20,!1,!1,!1,!1,0,null),a.dispatchEvent(b)}}var f="object"==typeof window&&window.window===window?window:"object"==typeof self&&self.self===self?self:"object"==typeof global&&global.global===global?global:void 0,a=f.navigator&&/Macintosh/.test(navigator.userAgent)&&/AppleWebKit/.test(navigator.userAgent)&&!/Safari/.test(navigator.userAgent),g=f.saveAs||("object"!=typeof window||window!==f?function(){}:"download"in HTMLAnchorElement.prototype&&!a?function(b,g,h){var i=f.URL||f.webkitURL,j=document.createElement("a");g=g||b.name||"download",j.download=g,j.rel="noopener","string"==typeof b?(j.href=b,j.origin===location.origin?e(j):d(j.href)?c(b,g,h):e(j,j.target="_blank")):(j.href=i.createObjectURL(b),setTimeout(function(){i.revokeObjectURL(j.href)},4E4),setTimeout(function(){e(j)},0))}:"msSaveOrOpenBlob"in navigator?function(f,g,h){if(g=g||f.name||"download","string"!=typeof f)navigator.msSaveOrOpenBlob(b(f,h),g);else if(d(f))c(f,g,h);else{var i=document.createElement("a");i.href=f,i.target="_blank",setTimeout(function(){e(i)})}}:function(b,d,e,g){if(g=g||open("","_blank"),g&&(g.document.title=g.document.body.innerText="downloading..."),"string"==typeof b)return c(b,d,e);var h="application/octet-stream"===b.type,i=/constructor/i.test(f.HTMLElement)||f.safari,j=/CriOS\/[\d]+/.test(navigator.userAgent);if((j||h&&i||a)&&"undefined"!=typeof FileReader){var k=new FileReader;k.onloadend=function(){var a=k.result;a=j?a:a.replace(/^data:[^;]*;/,"data:attachment/file;"),g?g.location.href=a:location=a,g=null},k.readAsDataURL(b)}else{var l=f.URL||f.webkitURL,m=l.createObjectURL(b);g?g.location=m:location.href=m,g=null,setTimeout(function(){l.revokeObjectURL(m)},4E4)}});f.saveAs=g.saveAs=g,"undefined"!=typeof module&&(module.exports=g)}); \ No newline at end of file diff --git a/plugin/think-plugs-static/stc/public/static/plugs/jquery/jquery.min.js b/plugin/think-plugs-static/stc/public/static/plugs/jquery/jquery.min.js new file mode 100644 index 000000000..e83647587 --- /dev/null +++ b/plugin/think-plugs-static/stc/public/static/plugs/jquery/jquery.min.js @@ -0,0 +1,5 @@ +/*! jQuery v1.12.4 | (c) jQuery Foundation | jquery.org/license */ +!function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){var c=[],d=a.document,e=c.slice,f=c.concat,g=c.push,h=c.indexOf,i={},j=i.toString,k=i.hasOwnProperty,l={},m="1.12.4",n=function(a,b){return new n.fn.init(a,b)},o=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,p=/^-ms-/,q=/-([\da-z])/gi,r=function(a,b){return b.toUpperCase()};n.fn=n.prototype={jquery:m,constructor:n,selector:"",length:0,toArray:function(){return e.call(this)},get:function(a){return null!=a?0>a?this[a+this.length]:this[a]:e.call(this)},pushStack:function(a){var b=n.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a){return n.each(this,a)},map:function(a){return this.pushStack(n.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(e.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor()},push:g,sort:c.sort,splice:c.splice},n.extend=n.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||n.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(e=arguments[h]))for(d in e)a=g[d],c=e[d],g!==c&&(j&&c&&(n.isPlainObject(c)||(b=n.isArray(c)))?(b?(b=!1,f=a&&n.isArray(a)?a:[]):f=a&&n.isPlainObject(a)?a:{},g[d]=n.extend(j,f,c)):void 0!==c&&(g[d]=c));return g},n.extend({expando:"jQuery"+(m+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===n.type(a)},isArray:Array.isArray||function(a){return"array"===n.type(a)},isWindow:function(a){return null!=a&&a==a.window},isNumeric:function(a){var b=a&&a.toString();return!n.isArray(a)&&b-parseFloat(b)+1>=0},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},isPlainObject:function(a){var b;if(!a||"object"!==n.type(a)||a.nodeType||n.isWindow(a))return!1;try{if(a.constructor&&!k.call(a,"constructor")&&!k.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}if(!l.ownFirst)for(b in a)return k.call(a,b);for(b in a);return void 0===b||k.call(a,b)},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?i[j.call(a)]||"object":typeof a},globalEval:function(b){b&&n.trim(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(p,"ms-").replace(q,r)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b){var c,d=0;if(s(a)){for(c=a.length;c>d;d++)if(b.call(a[d],d,a[d])===!1)break}else for(d in a)if(b.call(a[d],d,a[d])===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(o,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(s(Object(a))?n.merge(c,"string"==typeof a?[a]:a):g.call(c,a)),c},inArray:function(a,b,c){var d;if(b){if(h)return h.call(b,a,c);for(d=b.length,c=c?0>c?Math.max(0,d+c):c:0;d>c;c++)if(c in b&&b[c]===a)return c}return-1},merge:function(a,b){var c=+b.length,d=0,e=a.length;while(c>d)a[e++]=b[d++];if(c!==c)while(void 0!==b[d])a[e++]=b[d++];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,e,g=0,h=[];if(s(a))for(d=a.length;d>g;g++)e=b(a[g],g,c),null!=e&&h.push(e);else for(g in a)e=b(a[g],g,c),null!=e&&h.push(e);return f.apply([],h)},guid:1,proxy:function(a,b){var c,d,f;return"string"==typeof b&&(f=a[b],b=a,a=f),n.isFunction(a)?(c=e.call(arguments,2),d=function(){return a.apply(b||this,c.concat(e.call(arguments)))},d.guid=a.guid=a.guid||n.guid++,d):void 0},now:function(){return+new Date},support:l}),"function"==typeof Symbol&&(n.fn[Symbol.iterator]=c[Symbol.iterator]),n.each("Boolean Number String Function Array Date RegExp Object Error Symbol".split(" "),function(a,b){i["[object "+b+"]"]=b.toLowerCase()});function s(a){var b=!!a&&"length"in a&&a.length,c=n.type(a);return"function"===c||n.isWindow(a)?!1:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var t=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ga(),z=ga(),A=ga(),B=function(a,b){return a===b&&(l=!0),0},C=1<<31,D={}.hasOwnProperty,E=[],F=E.pop,G=E.push,H=E.push,I=E.slice,J=function(a,b){for(var c=0,d=a.length;d>c;c++)if(a[c]===b)return c;return-1},K="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",L="[\\x20\\t\\r\\n\\f]",M="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",N="\\["+L+"*("+M+")(?:"+L+"*([*^$|!~]?=)"+L+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+M+"))|)"+L+"*\\]",O=":("+M+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+N+")*)|.*)\\)|)",P=new RegExp(L+"+","g"),Q=new RegExp("^"+L+"+|((?:^|[^\\\\])(?:\\\\.)*)"+L+"+$","g"),R=new RegExp("^"+L+"*,"+L+"*"),S=new RegExp("^"+L+"*([>+~]|"+L+")"+L+"*"),T=new RegExp("="+L+"*([^\\]'\"]*?)"+L+"*\\]","g"),U=new RegExp(O),V=new RegExp("^"+M+"$"),W={ID:new RegExp("^#("+M+")"),CLASS:new RegExp("^\\.("+M+")"),TAG:new RegExp("^("+M+"|[*])"),ATTR:new RegExp("^"+N),PSEUDO:new RegExp("^"+O),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+L+"*(even|odd|(([+-]|)(\\d*)n|)"+L+"*(?:([+-]|)"+L+"*(\\d+)|))"+L+"*\\)|)","i"),bool:new RegExp("^(?:"+K+")$","i"),needsContext:new RegExp("^"+L+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+L+"*((?:-\\d)?\\d*)"+L+"*\\)|)(?=[^-]|$)","i")},X=/^(?:input|select|textarea|button)$/i,Y=/^h\d$/i,Z=/^[^{]+\{\s*\[native \w/,$=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,_=/[+~]/,aa=/'|\\/g,ba=new RegExp("\\\\([\\da-f]{1,6}"+L+"?|("+L+")|.)","ig"),ca=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:0>d?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},da=function(){m()};try{H.apply(E=I.call(v.childNodes),v.childNodes),E[v.childNodes.length].nodeType}catch(ea){H={apply:E.length?function(a,b){G.apply(a,I.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function fa(a,b,d,e){var f,h,j,k,l,o,r,s,w=b&&b.ownerDocument,x=b?b.nodeType:9;if(d=d||[],"string"!=typeof a||!a||1!==x&&9!==x&&11!==x)return d;if(!e&&((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,p)){if(11!==x&&(o=$.exec(a)))if(f=o[1]){if(9===x){if(!(j=b.getElementById(f)))return d;if(j.id===f)return d.push(j),d}else if(w&&(j=w.getElementById(f))&&t(b,j)&&j.id===f)return d.push(j),d}else{if(o[2])return H.apply(d,b.getElementsByTagName(a)),d;if((f=o[3])&&c.getElementsByClassName&&b.getElementsByClassName)return H.apply(d,b.getElementsByClassName(f)),d}if(c.qsa&&!A[a+" "]&&(!q||!q.test(a))){if(1!==x)w=b,s=a;else if("object"!==b.nodeName.toLowerCase()){(k=b.getAttribute("id"))?k=k.replace(aa,"\\$&"):b.setAttribute("id",k=u),r=g(a),h=r.length,l=V.test(k)?"#"+k:"[id='"+k+"']";while(h--)r[h]=l+" "+qa(r[h]);s=r.join(","),w=_.test(a)&&oa(b.parentNode)||b}if(s)try{return H.apply(d,w.querySelectorAll(s)),d}catch(y){}finally{k===u&&b.removeAttribute("id")}}}return i(a.replace(Q,"$1"),b,d,e)}function ga(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ha(a){return a[u]=!0,a}function ia(a){var b=n.createElement("div");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ja(a,b){var c=a.split("|"),e=c.length;while(e--)d.attrHandle[c[e]]=b}function ka(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&(~b.sourceIndex||C)-(~a.sourceIndex||C);if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function la(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function na(a){return ha(function(b){return b=+b,ha(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function oa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=fa.support={},f=fa.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?"HTML"!==b.nodeName:!1},m=fa.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=n.documentElement,p=!f(n),(e=n.defaultView)&&e.top!==e&&(e.addEventListener?e.addEventListener("unload",da,!1):e.attachEvent&&e.attachEvent("onunload",da)),c.attributes=ia(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ia(function(a){return a.appendChild(n.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=Z.test(n.getElementsByClassName),c.getById=ia(function(a){return o.appendChild(a).id=u,!n.getElementsByName||!n.getElementsByName(u).length}),c.getById?(d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c?[c]:[]}},d.filter.ID=function(a){var b=a.replace(ba,ca);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(ba,ca);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){return"undefined"!=typeof b.getElementsByClassName&&p?b.getElementsByClassName(a):void 0},r=[],q=[],(c.qsa=Z.test(n.querySelectorAll))&&(ia(function(a){o.appendChild(a).innerHTML="",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+L+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+L+"*(?:value|"+K+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ia(function(a){var b=n.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+L+"*[*^$|!~]?="),a.querySelectorAll(":enabled").length||q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=Z.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ia(function(a){c.disconnectedMatch=s.call(a,"div"),s.call(a,"[s!='']:x"),r.push("!=",O)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=Z.test(o.compareDocumentPosition),t=b||Z.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===n||a.ownerDocument===v&&t(v,a)?-1:b===n||b.ownerDocument===v&&t(v,b)?1:k?J(k,a)-J(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,g=[a],h=[b];if(!e||!f)return a===n?-1:b===n?1:e?-1:f?1:k?J(k,a)-J(k,b):0;if(e===f)return ka(a,b);c=a;while(c=c.parentNode)g.unshift(c);c=b;while(c=c.parentNode)h.unshift(c);while(g[d]===h[d])d++;return d?ka(g[d],h[d]):g[d]===v?-1:h[d]===v?1:0},n):n},fa.matches=function(a,b){return fa(a,null,null,b)},fa.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(T,"='$1']"),c.matchesSelector&&p&&!A[b+" "]&&(!r||!r.test(b))&&(!q||!q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return fa(b,n,null,[a]).length>0},fa.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},fa.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&D.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},fa.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},fa.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=fa.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=fa.selectors={cacheLength:50,createPseudo:ha,match:W,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(ba,ca),a[3]=(a[3]||a[4]||a[5]||"").replace(ba,ca),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||fa.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&fa.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return W.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&U.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(ba,ca).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+L+")"+a+"("+L+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=fa.attr(d,a);return null==e?"!="===b:b?(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(P," ")+" ").indexOf(c)>-1:"|="===b?e===c||e.slice(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h,t=!1;if(q){if(f){while(p){m=b;while(m=m[p])if(h?m.nodeName.toLowerCase()===r:1===m.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){m=q,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n&&j[2],m=n&&q.childNodes[n];while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if(1===m.nodeType&&++t&&m===b){k[a]=[w,n,t];break}}else if(s&&(m=b,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n),t===!1)while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if((h?m.nodeName.toLowerCase()===r:1===m.nodeType)&&++t&&(s&&(l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),k[a]=[w,t]),m===b))break;return t-=e,t===d||t%d===0&&t/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||fa.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ha(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=J(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ha(function(a){var b=[],c=[],d=h(a.replace(Q,"$1"));return d[u]?ha(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ha(function(a){return function(b){return fa(a,b).length>0}}),contains:ha(function(a){return a=a.replace(ba,ca),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ha(function(a){return V.test(a||"")||fa.error("unsupported lang: "+a),a=a.replace(ba,ca).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return Y.test(a.nodeName)},input:function(a){return X.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:na(function(){return[0]}),last:na(function(a,b){return[b-1]}),eq:na(function(a,b,c){return[0>c?c+b:c]}),even:na(function(a,b){for(var c=0;b>c;c+=2)a.push(c);return a}),odd:na(function(a,b){for(var c=1;b>c;c+=2)a.push(c);return a}),lt:na(function(a,b,c){for(var d=0>c?c+b:c;--d>=0;)a.push(d);return a}),gt:na(function(a,b,c){for(var d=0>c?c+b:c;++db;b++)d+=a[b].value;return d}function ra(a,b,c){var d=b.dir,e=c&&"parentNode"===d,f=x++;return b.first?function(b,c,f){while(b=b[d])if(1===b.nodeType||e)return a(b,c,f)}:function(b,c,g){var h,i,j,k=[w,f];if(g){while(b=b[d])if((1===b.nodeType||e)&&a(b,c,g))return!0}else while(b=b[d])if(1===b.nodeType||e){if(j=b[u]||(b[u]={}),i=j[b.uniqueID]||(j[b.uniqueID]={}),(h=i[d])&&h[0]===w&&h[1]===f)return k[2]=h[2];if(i[d]=k,k[2]=a(b,c,g))return!0}}}function sa(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function ta(a,b,c){for(var d=0,e=b.length;e>d;d++)fa(a,b[d],c);return c}function ua(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;i>h;h++)(f=a[h])&&(c&&!c(f,d,e)||(g.push(f),j&&b.push(h)));return g}function va(a,b,c,d,e,f){return d&&!d[u]&&(d=va(d)),e&&!e[u]&&(e=va(e,f)),ha(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||ta(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:ua(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=ua(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?J(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=ua(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):H.apply(g,r)})}function wa(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=ra(function(a){return a===b},h,!0),l=ra(function(a){return J(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];f>i;i++)if(c=d.relative[a[i].type])m=[ra(sa(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;f>e;e++)if(d.relative[a[e].type])break;return va(i>1&&sa(m),i>1&&qa(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(Q,"$1"),c,e>i&&wa(a.slice(i,e)),f>e&&wa(a=a.slice(e)),f>e&&qa(a))}m.push(c)}return sa(m)}function xa(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,o,q,r=0,s="0",t=f&&[],u=[],v=j,x=f||e&&d.find.TAG("*",k),y=w+=null==v?1:Math.random()||.1,z=x.length;for(k&&(j=g===n||g||k);s!==z&&null!=(l=x[s]);s++){if(e&&l){o=0,g||l.ownerDocument===n||(m(l),h=!p);while(q=a[o++])if(q(l,g||n,h)){i.push(l);break}k&&(w=y)}c&&((l=!q&&l)&&r--,f&&t.push(l))}if(r+=s,c&&s!==r){o=0;while(q=b[o++])q(t,u,g,h);if(f){if(r>0)while(s--)t[s]||u[s]||(u[s]=F.call(i));u=ua(u)}H.apply(i,u),k&&!f&&u.length>0&&r+b.length>1&&fa.uniqueSort(i)}return k&&(w=y,j=v),t};return c?ha(f):f}return h=fa.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=wa(b[c]),f[u]?d.push(f):e.push(f);f=A(a,xa(e,d)),f.selector=a}return f},i=fa.select=function(a,b,e,f){var i,j,k,l,m,n="function"==typeof a&&a,o=!f&&g(a=n.selector||a);if(e=e||[],1===o.length){if(j=o[0]=o[0].slice(0),j.length>2&&"ID"===(k=j[0]).type&&c.getById&&9===b.nodeType&&p&&d.relative[j[1].type]){if(b=(d.find.ID(k.matches[0].replace(ba,ca),b)||[])[0],!b)return e;n&&(b=b.parentNode),a=a.slice(j.shift().value.length)}i=W.needsContext.test(a)?0:j.length;while(i--){if(k=j[i],d.relative[l=k.type])break;if((m=d.find[l])&&(f=m(k.matches[0].replace(ba,ca),_.test(j[0].type)&&oa(b.parentNode)||b))){if(j.splice(i,1),a=f.length&&qa(j),!a)return H.apply(e,f),e;break}}}return(n||h(a,o))(f,b,!p,e,!b||_.test(a)&&oa(b.parentNode)||b),e},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ia(function(a){return 1&a.compareDocumentPosition(n.createElement("div"))}),ia(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||ja("type|href|height|width",function(a,b,c){return c?void 0:a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ia(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ja("value",function(a,b,c){return c||"input"!==a.nodeName.toLowerCase()?void 0:a.defaultValue}),ia(function(a){return null==a.getAttribute("disabled")})||ja(K,function(a,b,c){var d;return c?void 0:a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),fa}(a);n.find=t,n.expr=t.selectors,n.expr[":"]=n.expr.pseudos,n.uniqueSort=n.unique=t.uniqueSort,n.text=t.getText,n.isXMLDoc=t.isXML,n.contains=t.contains;var u=function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&n(a).is(c))break;d.push(a)}return d},v=function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c},w=n.expr.match.needsContext,x=/^<([\w-]+)\s*\/?>(?:<\/\1>|)$/,y=/^.[^:#\[\.,]*$/;function z(a,b,c){if(n.isFunction(b))return n.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return n.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(y.test(b))return n.filter(b,a,c);b=n.filter(b,a)}return n.grep(a,function(a){return n.inArray(a,b)>-1!==c})}n.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?n.find.matchesSelector(d,a)?[d]:[]:n.find.matches(a,n.grep(b,function(a){return 1===a.nodeType}))},n.fn.extend({find:function(a){var b,c=[],d=this,e=d.length;if("string"!=typeof a)return this.pushStack(n(a).filter(function(){for(b=0;e>b;b++)if(n.contains(d[b],this))return!0}));for(b=0;e>b;b++)n.find(a,d[b],c);return c=this.pushStack(e>1?n.unique(c):c),c.selector=this.selector?this.selector+" "+a:a,c},filter:function(a){return this.pushStack(z(this,a||[],!1))},not:function(a){return this.pushStack(z(this,a||[],!0))},is:function(a){return!!z(this,"string"==typeof a&&w.test(a)?n(a):a||[],!1).length}});var A,B=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,C=n.fn.init=function(a,b,c){var e,f;if(!a)return this;if(c=c||A,"string"==typeof a){if(e="<"===a.charAt(0)&&">"===a.charAt(a.length-1)&&a.length>=3?[null,a,null]:B.exec(a),!e||!e[1]&&b)return!b||b.jquery?(b||c).find(a):this.constructor(b).find(a);if(e[1]){if(b=b instanceof n?b[0]:b,n.merge(this,n.parseHTML(e[1],b&&b.nodeType?b.ownerDocument||b:d,!0)),x.test(e[1])&&n.isPlainObject(b))for(e in b)n.isFunction(this[e])?this[e](b[e]):this.attr(e,b[e]);return this}if(f=d.getElementById(e[2]),f&&f.parentNode){if(f.id!==e[2])return A.find(a);this.length=1,this[0]=f}return this.context=d,this.selector=a,this}return a.nodeType?(this.context=this[0]=a,this.length=1,this):n.isFunction(a)?"undefined"!=typeof c.ready?c.ready(a):a(n):(void 0!==a.selector&&(this.selector=a.selector,this.context=a.context),n.makeArray(a,this))};C.prototype=n.fn,A=n(d);var D=/^(?:parents|prev(?:Until|All))/,E={children:!0,contents:!0,next:!0,prev:!0};n.fn.extend({has:function(a){var b,c=n(a,this),d=c.length;return this.filter(function(){for(b=0;d>b;b++)if(n.contains(this,c[b]))return!0})},closest:function(a,b){for(var c,d=0,e=this.length,f=[],g=w.test(a)||"string"!=typeof a?n(a,b||this.context):0;e>d;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&n.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?n.uniqueSort(f):f)},index:function(a){return a?"string"==typeof a?n.inArray(this[0],n(a)):n.inArray(a.jquery?a[0]:a,this):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(n.uniqueSort(n.merge(this.get(),n(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function F(a,b){do a=a[b];while(a&&1!==a.nodeType);return a}n.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return u(a,"parentNode")},parentsUntil:function(a,b,c){return u(a,"parentNode",c)},next:function(a){return F(a,"nextSibling")},prev:function(a){return F(a,"previousSibling")},nextAll:function(a){return u(a,"nextSibling")},prevAll:function(a){return u(a,"previousSibling")},nextUntil:function(a,b,c){return u(a,"nextSibling",c)},prevUntil:function(a,b,c){return u(a,"previousSibling",c)},siblings:function(a){return v((a.parentNode||{}).firstChild,a)},children:function(a){return v(a.firstChild)},contents:function(a){return n.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:n.merge([],a.childNodes)}},function(a,b){n.fn[a]=function(c,d){var e=n.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=n.filter(d,e)),this.length>1&&(E[a]||(e=n.uniqueSort(e)),D.test(a)&&(e=e.reverse())),this.pushStack(e)}});var G=/\S+/g;function H(a){var b={};return n.each(a.match(G)||[],function(a,c){b[c]=!0}),b}n.Callbacks=function(a){a="string"==typeof a?H(a):n.extend({},a);var b,c,d,e,f=[],g=[],h=-1,i=function(){for(e=a.once,d=b=!0;g.length;h=-1){c=g.shift();while(++h-1)f.splice(c,1),h>=c&&h--}),this},has:function(a){return a?n.inArray(a,f)>-1:f.length>0},empty:function(){return f&&(f=[]),this},disable:function(){return e=g=[],f=c="",this},disabled:function(){return!f},lock:function(){return e=!0,c||j.disable(),this},locked:function(){return!!e},fireWith:function(a,c){return e||(c=c||[],c=[a,c.slice?c.slice():c],g.push(c),b||i()),this},fire:function(){return j.fireWith(this,arguments),this},fired:function(){return!!d}};return j},n.extend({Deferred:function(a){var b=[["resolve","done",n.Callbacks("once memory"),"resolved"],["reject","fail",n.Callbacks("once memory"),"rejected"],["notify","progress",n.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return n.Deferred(function(c){n.each(b,function(b,f){var g=n.isFunction(a[b])&&a[b];e[f[1]](function(){var a=g&&g.apply(this,arguments);a&&n.isFunction(a.promise)?a.promise().progress(c.notify).done(c.resolve).fail(c.reject):c[f[0]+"With"](this===d?c.promise():this,g?[a]:arguments)})}),a=null}).promise()},promise:function(a){return null!=a?n.extend(a,d):d}},e={};return d.pipe=d.then,n.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[1^a][2].disable,b[2][2].lock),e[f[0]]=function(){return e[f[0]+"With"](this===e?d:this,arguments),this},e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=e.call(arguments),d=c.length,f=1!==d||a&&n.isFunction(a.promise)?d:0,g=1===f?a:n.Deferred(),h=function(a,b,c){return function(d){b[a]=this,c[a]=arguments.length>1?e.call(arguments):d,c===i?g.notifyWith(b,c):--f||g.resolveWith(b,c)}},i,j,k;if(d>1)for(i=new Array(d),j=new Array(d),k=new Array(d);d>b;b++)c[b]&&n.isFunction(c[b].promise)?c[b].promise().progress(h(b,j,i)).done(h(b,k,c)).fail(g.reject):--f;return f||g.resolveWith(k,c),g.promise()}});var I;n.fn.ready=function(a){return n.ready.promise().done(a),this},n.extend({isReady:!1,readyWait:1,holdReady:function(a){a?n.readyWait++:n.ready(!0)},ready:function(a){(a===!0?--n.readyWait:n.isReady)||(n.isReady=!0,a!==!0&&--n.readyWait>0||(I.resolveWith(d,[n]),n.fn.triggerHandler&&(n(d).triggerHandler("ready"),n(d).off("ready"))))}});function J(){d.addEventListener?(d.removeEventListener("DOMContentLoaded",K),a.removeEventListener("load",K)):(d.detachEvent("onreadystatechange",K),a.detachEvent("onload",K))}function K(){(d.addEventListener||"load"===a.event.type||"complete"===d.readyState)&&(J(),n.ready())}n.ready.promise=function(b){if(!I)if(I=n.Deferred(),"complete"===d.readyState||"loading"!==d.readyState&&!d.documentElement.doScroll)a.setTimeout(n.ready);else if(d.addEventListener)d.addEventListener("DOMContentLoaded",K),a.addEventListener("load",K);else{d.attachEvent("onreadystatechange",K),a.attachEvent("onload",K);var c=!1;try{c=null==a.frameElement&&d.documentElement}catch(e){}c&&c.doScroll&&!function f(){if(!n.isReady){try{c.doScroll("left")}catch(b){return a.setTimeout(f,50)}J(),n.ready()}}()}return I.promise(b)},n.ready.promise();var L;for(L in n(l))break;l.ownFirst="0"===L,l.inlineBlockNeedsLayout=!1,n(function(){var a,b,c,e;c=d.getElementsByTagName("body")[0],c&&c.style&&(b=d.createElement("div"),e=d.createElement("div"),e.style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px",c.appendChild(e).appendChild(b),"undefined"!=typeof b.style.zoom&&(b.style.cssText="display:inline;margin:0;border:0;padding:1px;width:1px;zoom:1",l.inlineBlockNeedsLayout=a=3===b.offsetWidth,a&&(c.style.zoom=1)),c.removeChild(e))}),function(){var a=d.createElement("div");l.deleteExpando=!0;try{delete a.test}catch(b){l.deleteExpando=!1}a=null}();var M=function(a){var b=n.noData[(a.nodeName+" ").toLowerCase()],c=+a.nodeType||1;return 1!==c&&9!==c?!1:!b||b!==!0&&a.getAttribute("classid")===b},N=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,O=/([A-Z])/g;function P(a,b,c){if(void 0===c&&1===a.nodeType){var d="data-"+b.replace(O,"-$1").toLowerCase();if(c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:N.test(c)?n.parseJSON(c):c}catch(e){}n.data(a,b,c)}else c=void 0; +}return c}function Q(a){var b;for(b in a)if(("data"!==b||!n.isEmptyObject(a[b]))&&"toJSON"!==b)return!1;return!0}function R(a,b,d,e){if(M(a)){var f,g,h=n.expando,i=a.nodeType,j=i?n.cache:a,k=i?a[h]:a[h]&&h;if(k&&j[k]&&(e||j[k].data)||void 0!==d||"string"!=typeof b)return k||(k=i?a[h]=c.pop()||n.guid++:h),j[k]||(j[k]=i?{}:{toJSON:n.noop}),"object"!=typeof b&&"function"!=typeof b||(e?j[k]=n.extend(j[k],b):j[k].data=n.extend(j[k].data,b)),g=j[k],e||(g.data||(g.data={}),g=g.data),void 0!==d&&(g[n.camelCase(b)]=d),"string"==typeof b?(f=g[b],null==f&&(f=g[n.camelCase(b)])):f=g,f}}function S(a,b,c){if(M(a)){var d,e,f=a.nodeType,g=f?n.cache:a,h=f?a[n.expando]:n.expando;if(g[h]){if(b&&(d=c?g[h]:g[h].data)){n.isArray(b)?b=b.concat(n.map(b,n.camelCase)):b in d?b=[b]:(b=n.camelCase(b),b=b in d?[b]:b.split(" ")),e=b.length;while(e--)delete d[b[e]];if(c?!Q(d):!n.isEmptyObject(d))return}(c||(delete g[h].data,Q(g[h])))&&(f?n.cleanData([a],!0):l.deleteExpando||g!=g.window?delete g[h]:g[h]=void 0)}}}n.extend({cache:{},noData:{"applet ":!0,"embed ":!0,"object ":"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"},hasData:function(a){return a=a.nodeType?n.cache[a[n.expando]]:a[n.expando],!!a&&!Q(a)},data:function(a,b,c){return R(a,b,c)},removeData:function(a,b){return S(a,b)},_data:function(a,b,c){return R(a,b,c,!0)},_removeData:function(a,b){return S(a,b,!0)}}),n.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=n.data(f),1===f.nodeType&&!n._data(f,"parsedAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=n.camelCase(d.slice(5)),P(f,d,e[d])));n._data(f,"parsedAttrs",!0)}return e}return"object"==typeof a?this.each(function(){n.data(this,a)}):arguments.length>1?this.each(function(){n.data(this,a,b)}):f?P(f,a,n.data(f,a)):void 0},removeData:function(a){return this.each(function(){n.removeData(this,a)})}}),n.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=n._data(a,b),c&&(!d||n.isArray(c)?d=n._data(a,b,n.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=n.queue(a,b),d=c.length,e=c.shift(),f=n._queueHooks(a,b),g=function(){n.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return n._data(a,c)||n._data(a,c,{empty:n.Callbacks("once memory").add(function(){n._removeData(a,b+"queue"),n._removeData(a,c)})})}}),n.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.lengthh;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f},Z=/^(?:checkbox|radio)$/i,$=/<([\w:-]+)/,_=/^$|\/(?:java|ecma)script/i,aa=/^\s+/,ba="abbr|article|aside|audio|bdi|canvas|data|datalist|details|dialog|figcaption|figure|footer|header|hgroup|main|mark|meter|nav|output|picture|progress|section|summary|template|time|video";function ca(a){var b=ba.split("|"),c=a.createDocumentFragment();if(c.createElement)while(b.length)c.createElement(b.pop());return c}!function(){var a=d.createElement("div"),b=d.createDocumentFragment(),c=d.createElement("input");a.innerHTML="
      a",l.leadingWhitespace=3===a.firstChild.nodeType,l.tbody=!a.getElementsByTagName("tbody").length,l.htmlSerialize=!!a.getElementsByTagName("link").length,l.html5Clone="<:nav>"!==d.createElement("nav").cloneNode(!0).outerHTML,c.type="checkbox",c.checked=!0,b.appendChild(c),l.appendChecked=c.checked,a.innerHTML="",l.noCloneChecked=!!a.cloneNode(!0).lastChild.defaultValue,b.appendChild(a),c=d.createElement("input"),c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),a.appendChild(c),l.checkClone=a.cloneNode(!0).cloneNode(!0).lastChild.checked,l.noCloneEvent=!!a.addEventListener,a[n.expando]=1,l.attributes=!a.getAttribute(n.expando)}();var da={option:[1,""],legend:[1,"
      ","
      "],area:[1,"",""],param:[1,"",""],thead:[1,"","
      "],tr:[2,"","
      "],col:[2,"","
      "],td:[3,"","
      "],_default:l.htmlSerialize?[0,"",""]:[1,"X
      ","
      "]};da.optgroup=da.option,da.tbody=da.tfoot=da.colgroup=da.caption=da.thead,da.th=da.td;function ea(a,b){var c,d,e=0,f="undefined"!=typeof a.getElementsByTagName?a.getElementsByTagName(b||"*"):"undefined"!=typeof a.querySelectorAll?a.querySelectorAll(b||"*"):void 0;if(!f)for(f=[],c=a.childNodes||a;null!=(d=c[e]);e++)!b||n.nodeName(d,b)?f.push(d):n.merge(f,ea(d,b));return void 0===b||b&&n.nodeName(a,b)?n.merge([a],f):f}function fa(a,b){for(var c,d=0;null!=(c=a[d]);d++)n._data(c,"globalEval",!b||n._data(b[d],"globalEval"))}var ga=/<|&#?\w+;/,ha=/r;r++)if(g=a[r],g||0===g)if("object"===n.type(g))n.merge(q,g.nodeType?[g]:g);else if(ga.test(g)){i=i||p.appendChild(b.createElement("div")),j=($.exec(g)||["",""])[1].toLowerCase(),m=da[j]||da._default,i.innerHTML=m[1]+n.htmlPrefilter(g)+m[2],f=m[0];while(f--)i=i.lastChild;if(!l.leadingWhitespace&&aa.test(g)&&q.push(b.createTextNode(aa.exec(g)[0])),!l.tbody){g="table"!==j||ha.test(g)?""!==m[1]||ha.test(g)?0:i:i.firstChild,f=g&&g.childNodes.length;while(f--)n.nodeName(k=g.childNodes[f],"tbody")&&!k.childNodes.length&&g.removeChild(k)}n.merge(q,i.childNodes),i.textContent="";while(i.firstChild)i.removeChild(i.firstChild);i=p.lastChild}else q.push(b.createTextNode(g));i&&p.removeChild(i),l.appendChecked||n.grep(ea(q,"input"),ia),r=0;while(g=q[r++])if(d&&n.inArray(g,d)>-1)e&&e.push(g);else if(h=n.contains(g.ownerDocument,g),i=ea(p.appendChild(g),"script"),h&&fa(i),c){f=0;while(g=i[f++])_.test(g.type||"")&&c.push(g)}return i=null,p}!function(){var b,c,e=d.createElement("div");for(b in{submit:!0,change:!0,focusin:!0})c="on"+b,(l[b]=c in a)||(e.setAttribute(c,"t"),l[b]=e.attributes[c].expando===!1);e=null}();var ka=/^(?:input|select|textarea)$/i,la=/^key/,ma=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,na=/^(?:focusinfocus|focusoutblur)$/,oa=/^([^.]*)(?:\.(.+)|)/;function pa(){return!0}function qa(){return!1}function ra(){try{return d.activeElement}catch(a){}}function sa(a,b,c,d,e,f){var g,h;if("object"==typeof b){"string"!=typeof c&&(d=d||c,c=void 0);for(h in b)sa(a,h,c,d,b[h],f);return a}if(null==d&&null==e?(e=c,d=c=void 0):null==e&&("string"==typeof c?(e=d,d=void 0):(e=d,d=c,c=void 0)),e===!1)e=qa;else if(!e)return a;return 1===f&&(g=e,e=function(a){return n().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=n.guid++)),a.each(function(){n.event.add(this,b,e,d,c)})}n.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=n._data(a);if(r){c.handler&&(i=c,c=i.handler,e=i.selector),c.guid||(c.guid=n.guid++),(g=r.events)||(g=r.events={}),(k=r.handle)||(k=r.handle=function(a){return"undefined"==typeof n||a&&n.event.triggered===a.type?void 0:n.event.dispatch.apply(k.elem,arguments)},k.elem=a),b=(b||"").match(G)||[""],h=b.length;while(h--)f=oa.exec(b[h])||[],o=q=f[1],p=(f[2]||"").split(".").sort(),o&&(j=n.event.special[o]||{},o=(e?j.delegateType:j.bindType)||o,j=n.event.special[o]||{},l=n.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&n.expr.match.needsContext.test(e),namespace:p.join(".")},i),(m=g[o])||(m=g[o]=[],m.delegateCount=0,j.setup&&j.setup.call(a,d,p,k)!==!1||(a.addEventListener?a.addEventListener(o,k,!1):a.attachEvent&&a.attachEvent("on"+o,k))),j.add&&(j.add.call(a,l),l.handler.guid||(l.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,l):m.push(l),n.event.global[o]=!0);a=null}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=n.hasData(a)&&n._data(a);if(r&&(k=r.events)){b=(b||"").match(G)||[""],j=b.length;while(j--)if(h=oa.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=n.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,m=k[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),i=f=m.length;while(f--)g=m[f],!e&&q!==g.origType||c&&c.guid!==g.guid||h&&!h.test(g.namespace)||d&&d!==g.selector&&("**"!==d||!g.selector)||(m.splice(f,1),g.selector&&m.delegateCount--,l.remove&&l.remove.call(a,g));i&&!m.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||n.removeEvent(a,o,r.handle),delete k[o])}else for(o in k)n.event.remove(a,o+b[j],c,d,!0);n.isEmptyObject(k)&&(delete r.handle,n._removeData(a,"events"))}},trigger:function(b,c,e,f){var g,h,i,j,l,m,o,p=[e||d],q=k.call(b,"type")?b.type:b,r=k.call(b,"namespace")?b.namespace.split("."):[];if(i=m=e=e||d,3!==e.nodeType&&8!==e.nodeType&&!na.test(q+n.event.triggered)&&(q.indexOf(".")>-1&&(r=q.split("."),q=r.shift(),r.sort()),h=q.indexOf(":")<0&&"on"+q,b=b[n.expando]?b:new n.Event(q,"object"==typeof b&&b),b.isTrigger=f?2:3,b.namespace=r.join("."),b.rnamespace=b.namespace?new RegExp("(^|\\.)"+r.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=e),c=null==c?[b]:n.makeArray(c,[b]),l=n.event.special[q]||{},f||!l.trigger||l.trigger.apply(e,c)!==!1)){if(!f&&!l.noBubble&&!n.isWindow(e)){for(j=l.delegateType||q,na.test(j+q)||(i=i.parentNode);i;i=i.parentNode)p.push(i),m=i;m===(e.ownerDocument||d)&&p.push(m.defaultView||m.parentWindow||a)}o=0;while((i=p[o++])&&!b.isPropagationStopped())b.type=o>1?j:l.bindType||q,g=(n._data(i,"events")||{})[b.type]&&n._data(i,"handle"),g&&g.apply(i,c),g=h&&i[h],g&&g.apply&&M(i)&&(b.result=g.apply(i,c),b.result===!1&&b.preventDefault());if(b.type=q,!f&&!b.isDefaultPrevented()&&(!l._default||l._default.apply(p.pop(),c)===!1)&&M(e)&&h&&e[q]&&!n.isWindow(e)){m=e[h],m&&(e[h]=null),n.event.triggered=q;try{e[q]()}catch(s){}n.event.triggered=void 0,m&&(e[h]=m)}return b.result}},dispatch:function(a){a=n.event.fix(a);var b,c,d,f,g,h=[],i=e.call(arguments),j=(n._data(this,"events")||{})[a.type]||[],k=n.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=n.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,c=0;while((g=f.handlers[c++])&&!a.isImmediatePropagationStopped())a.rnamespace&&!a.rnamespace.test(g.namespace)||(a.handleObj=g,a.data=g.data,d=((n.event.special[g.origType]||{}).handle||g.handler).apply(f.elem,i),void 0!==d&&(a.result=d)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&("click"!==a.type||isNaN(a.button)||a.button<1))for(;i!=this;i=i.parentNode||this)if(1===i.nodeType&&(i.disabled!==!0||"click"!==a.type)){for(d=[],c=0;h>c;c++)f=b[c],e=f.selector+" ",void 0===d[e]&&(d[e]=f.needsContext?n(e,this).index(i)>-1:n.find(e,this,null,[i]).length),d[e]&&d.push(f);d.length&&g.push({elem:i,handlers:d})}return h]","i"),va=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:-]+)[^>]*)\/>/gi,wa=/\s*$/g,Aa=ca(d),Ba=Aa.appendChild(d.createElement("div"));function Ca(a,b){return n.nodeName(a,"table")&&n.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function Da(a){return a.type=(null!==n.find.attr(a,"type"))+"/"+a.type,a}function Ea(a){var b=ya.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function Fa(a,b){if(1===b.nodeType&&n.hasData(a)){var c,d,e,f=n._data(a),g=n._data(b,f),h=f.events;if(h){delete g.handle,g.events={};for(c in h)for(d=0,e=h[c].length;e>d;d++)n.event.add(b,c,h[c][d])}g.data&&(g.data=n.extend({},g.data))}}function Ga(a,b){var c,d,e;if(1===b.nodeType){if(c=b.nodeName.toLowerCase(),!l.noCloneEvent&&b[n.expando]){e=n._data(b);for(d in e.events)n.removeEvent(b,d,e.handle);b.removeAttribute(n.expando)}"script"===c&&b.text!==a.text?(Da(b).text=a.text,Ea(b)):"object"===c?(b.parentNode&&(b.outerHTML=a.outerHTML),l.html5Clone&&a.innerHTML&&!n.trim(b.innerHTML)&&(b.innerHTML=a.innerHTML)):"input"===c&&Z.test(a.type)?(b.defaultChecked=b.checked=a.checked,b.value!==a.value&&(b.value=a.value)):"option"===c?b.defaultSelected=b.selected=a.defaultSelected:"input"!==c&&"textarea"!==c||(b.defaultValue=a.defaultValue)}}function Ha(a,b,c,d){b=f.apply([],b);var e,g,h,i,j,k,m=0,o=a.length,p=o-1,q=b[0],r=n.isFunction(q);if(r||o>1&&"string"==typeof q&&!l.checkClone&&xa.test(q))return a.each(function(e){var f=a.eq(e);r&&(b[0]=q.call(this,e,f.html())),Ha(f,b,c,d)});if(o&&(k=ja(b,a[0].ownerDocument,!1,a,d),e=k.firstChild,1===k.childNodes.length&&(k=e),e||d)){for(i=n.map(ea(k,"script"),Da),h=i.length;o>m;m++)g=k,m!==p&&(g=n.clone(g,!0,!0),h&&n.merge(i,ea(g,"script"))),c.call(a[m],g,m);if(h)for(j=i[i.length-1].ownerDocument,n.map(i,Ea),m=0;h>m;m++)g=i[m],_.test(g.type||"")&&!n._data(g,"globalEval")&&n.contains(j,g)&&(g.src?n._evalUrl&&n._evalUrl(g.src):n.globalEval((g.text||g.textContent||g.innerHTML||"").replace(za,"")));k=e=null}return a}function Ia(a,b,c){for(var d,e=b?n.filter(b,a):a,f=0;null!=(d=e[f]);f++)c||1!==d.nodeType||n.cleanData(ea(d)),d.parentNode&&(c&&n.contains(d.ownerDocument,d)&&fa(ea(d,"script")),d.parentNode.removeChild(d));return a}n.extend({htmlPrefilter:function(a){return a.replace(va,"<$1>")},clone:function(a,b,c){var d,e,f,g,h,i=n.contains(a.ownerDocument,a);if(l.html5Clone||n.isXMLDoc(a)||!ua.test("<"+a.nodeName+">")?f=a.cloneNode(!0):(Ba.innerHTML=a.outerHTML,Ba.removeChild(f=Ba.firstChild)),!(l.noCloneEvent&&l.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||n.isXMLDoc(a)))for(d=ea(f),h=ea(a),g=0;null!=(e=h[g]);++g)d[g]&&Ga(e,d[g]);if(b)if(c)for(h=h||ea(a),d=d||ea(f),g=0;null!=(e=h[g]);g++)Fa(e,d[g]);else Fa(a,f);return d=ea(f,"script"),d.length>0&&fa(d,!i&&ea(a,"script")),d=h=e=null,f},cleanData:function(a,b){for(var d,e,f,g,h=0,i=n.expando,j=n.cache,k=l.attributes,m=n.event.special;null!=(d=a[h]);h++)if((b||M(d))&&(f=d[i],g=f&&j[f])){if(g.events)for(e in g.events)m[e]?n.event.remove(d,e):n.removeEvent(d,e,g.handle);j[f]&&(delete j[f],k||"undefined"==typeof d.removeAttribute?d[i]=void 0:d.removeAttribute(i),c.push(f))}}}),n.fn.extend({domManip:Ha,detach:function(a){return Ia(this,a,!0)},remove:function(a){return Ia(this,a)},text:function(a){return Y(this,function(a){return void 0===a?n.text(this):this.empty().append((this[0]&&this[0].ownerDocument||d).createTextNode(a))},null,a,arguments.length)},append:function(){return Ha(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ca(this,a);b.appendChild(a)}})},prepend:function(){return Ha(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ca(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return Ha(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return Ha(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},empty:function(){for(var a,b=0;null!=(a=this[b]);b++){1===a.nodeType&&n.cleanData(ea(a,!1));while(a.firstChild)a.removeChild(a.firstChild);a.options&&n.nodeName(a,"select")&&(a.options.length=0)}return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return n.clone(this,a,b)})},html:function(a){return Y(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a)return 1===b.nodeType?b.innerHTML.replace(ta,""):void 0;if("string"==typeof a&&!wa.test(a)&&(l.htmlSerialize||!ua.test(a))&&(l.leadingWhitespace||!aa.test(a))&&!da[($.exec(a)||["",""])[1].toLowerCase()]){a=n.htmlPrefilter(a);try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(n.cleanData(ea(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=[];return Ha(this,arguments,function(b){var c=this.parentNode;n.inArray(this,a)<0&&(n.cleanData(ea(this)),c&&c.replaceChild(b,this))},a)}}),n.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){n.fn[a]=function(a){for(var c,d=0,e=[],f=n(a),h=f.length-1;h>=d;d++)c=d===h?this:this.clone(!0),n(f[d])[b](c),g.apply(e,c.get());return this.pushStack(e)}});var Ja,Ka={HTML:"block",BODY:"block"};function La(a,b){var c=n(b.createElement(a)).appendTo(b.body),d=n.css(c[0],"display");return c.detach(),d}function Ma(a){var b=d,c=Ka[a];return c||(c=La(a,b),"none"!==c&&c||(Ja=(Ja||n("';break;case 3:delete o.title,delete o.closeBtn,-1===o.icon&&o.icon,x.closeAll("loading");break;case 4:l||(o.content=[o.content,"body"]),o.follow=o.content[1],o.content=o.content[0]+'',delete o.title,o.tips="object"==typeof o.tips?o.tips:[o.tips,!0],o.tipsMore||x.closeAll("tips")}a.vessel(l,function(e,t,i){c.append(e[0]),l?2==o.type||4==o.type?m("body").append(e[1]):r.parents("."+u[0])[0]||(r.data("display",r.css("display")).show().addClass("layui-layer-wrap").wrap(e[1]),m("#"+u[0]+s).find("."+u[5]).before(t)):c.append(e[1]),m("#"+u.MOVE)[0]||c.append(d.moveElem=i),a.layero=m("#"+u[0]+s),a.shadeo=m("#"+u.SHADE+s),o.scrollbar||d.setScrollbar(s)}).auto(s),a.shadeo.css({"background-color":o.shade[1]||"#000",opacity:o.shade[0]||o.shade,transition:o.shade[2]||""}),a.shadeo.data(y,o.shade[0]||o.shade),2==o.type&&6==x.ie&&a.layero.find("iframe").attr("src",r[0]),4==o.type?a.tips():(a.offset(),parseInt(d.getStyle(document.getElementById(u.MOVE),"z-index"))||(a.layero.css("visibility","hidden"),x.ready(function(){a.offset(),a.layero.css("visibility","visible")}))),!o.fixed||d.events.resize[a.index]||(d.events.resize[a.index]=function(){a.resize()},h.on("resize",d.events.resize[a.index])),a.layero.data("config",o),o.time<=0||setTimeout(function(){x.close(a.index)},o.time),a.move().callback(),f(a.layero)}},i.pt.resize=function(){var e=this,t=e.config;e.offset(),(/^\d+%$/.test(t.area[0])||/^\d+%$/.test(t.area[1]))&&e.auto(e.index),4==t.type&&e.tips()},i.pt.auto=function(e){var t=this.config,i=m("#"+u[0]+e),n=((""===t.area[0]||"auto"===t.area[0])&&0t.maxWidth)&&i.width(t.maxWidth),[i.innerWidth(),i.innerHeight()]),a=i.find(u[1]).outerHeight()||0,o=i.find("."+u[6]).outerHeight()||0,e=function(e){(e=i.find(e)).height(n[1]-a-o-2*(0|parseFloat(e.css("padding-top"))))};return 2===t.type?e("iframe"):""===t.area[1]||"auto"===t.area[1]?0t.maxHeight?(n[1]=t.maxHeight,e("."+u[5])):t.fixed&&n[1]>=h.height()&&(n[1]=h.height(),e("."+u[5])):e("."+u[5]),this},i.pt.offset=function(){var e=this.config,t=this.layero,t=d.updatePosition(t,e);this.offsetTop=t.offsetTop,this.offsetLeft=t.offsetLeft},i.pt.tips=function(){var e=this.config,t=this.layero,i=[t.outerWidth(),t.outerHeight()],n=m(e.follow),a={width:(n=n[0]?n:m("body")).outerWidth(),height:n.outerHeight(),top:n.offset().top,left:n.offset().left},o=t.find(".layui-layer-TipsG"),n=e.tips[0];e.tips[1]||o.remove(),a.autoLeft=function(){0'):e.removeClass("layui-layer-btn-is-loading").removeAttr("disabled").find(".layui-layer-btn-loading-icon").remove()},i.pt.callback=function(){var n=this,a=n.layero,o=n.config;n.openLayer(),o.success&&(2==o.type?a.find("iframe").on("load",function(){o.success(a,n.index,n)}):o.success(a,n.index,n)),6==x.ie&&n.IE6(a),a.find("."+u[6]).children("a").on("click",function(){var e,t=m(this),i=t.index();t.attr("disabled")||(o.btnAsync?(e=0===i?o.yes||o.btn1:o["btn"+(i+1)],n.loading=function(e){n.btnLoading(t,e)},e?d.promiseLikeResolve(e.call(o,n.index,a,n)).then(function(e){!1!==e&&x.close(n.index)},function(e){e!==undefined&&p.console&&p.console.error("layer error hint: "+e)}):x.close(n.index)):0===i?o.yes?o.yes(n.index,a,n):o.btn1?o.btn1(n.index,a,n):x.close(n.index):!1!==(o["btn"+(i+1)]&&o["btn"+(i+1)](n.index,a,n))&&x.close(n.index))}),a.find("."+u[7]).on("click",function(){!1!==(o.cancel&&o.cancel(n.index,a,n))&&x.close(n.index)}),o.shadeClose&&n.shadeo.on("click",function(){x.close(n.index)}),a.find(".layui-layer-min").on("click",function(){!1!==(o.min&&o.min(a,n.index,n))&&x.min(n.index,o)}),a.find(".layui-layer-max").on("click",function(){m(this).hasClass("layui-layer-maxmin")?(x.restore(n.index),o.restore&&o.restore(a,n.index,n)):(x.full(n.index,o),setTimeout(function(){o.full&&o.full(a,n.index,n)},100))}),o.end&&(d.end[n.index]=o.end),o.beforeEnd&&(d.beforeEnd[n.index]=m.proxy(o.beforeEnd,o,a,n.index,n))},d.reselect=function(){m.each(m("select"),function(e,t){var i=m(this);i.parents("."+u[0])[0]||1==i.attr("layer")&&m("."+u[0]).length<1&&i.removeAttr("layer").show()})},i.pt.IE6=function(e){m("select").each(function(e,t){var i=m(this);i.parents("."+u[0])[0]||"none"!==i.css("display")&&i.attr({layer:"1"}).hide()})},i.pt.openLayer=function(){x.zIndex=this.config.zIndex,x.setTop=function(e){return x.zIndex=parseInt(e[0].style.zIndex),e.on("mousedown",function(){x.zIndex++,e.css("z-index",x.zIndex+1)}),x.zIndex}},d.record=function(e){if(!e[0])return p.console&&console.error("index error");var t=e.attr("type"),i=e.find(".layui-layer-content"),t=t===d.type[2]?i.children("iframe"):i,n=[e[0].style.width||d.getStyle(e[0],"width"),e[0].style.height||d.getStyle(e[0],"height"),e.position().top,e.position().left+parseFloat(e.css("margin-left"))];e.find(".layui-layer-max").addClass("layui-layer-maxmin"),e.attr({area:n}),i.data(l,d.getStyle(t[0],"height"))},d.setScrollbar=function(e){u.html.css("overflow","hidden")},d.restScrollbar=function(t){u.html.css("overflow")&&0===m("."+u[0]).filter(function(){var e=m(this);return!1===(e.data("config")||{}).scrollbar&&"min"!==e.data("maxminStatus")&&e.attr("times")!==String(t)}).length&&u.html.css("overflow","")},d.promiseLikeResolve=function(e){var t=m.Deferred();return e&&"function"==typeof e.then?e.then(t.resolve,t.reject):t.resolve(e),t.promise()},d.updatePosition=function(e,t){var i=[e.outerWidth(),e.outerHeight()],n={offsetTop:(h.height()-i[1])/2,offsetLeft:(h.width()-i[0])/2};return"object"==typeof t.offset?(n.offsetTop=t.offset[0],n.offsetLeft=t.offset[1]||n.offsetLeft):"auto"!==t.offset&&("t"===t.offset?n.offsetTop=0:"r"===t.offset?n.offsetLeft=h.width()-i[0]:"b"===t.offset?n.offsetTop=h.height()-i[1]:"l"===t.offset?n.offsetLeft=0:"lt"===t.offset?(n.offsetTop=0,n.offsetLeft=0):"lb"===t.offset?(n.offsetTop=h.height()-i[1],n.offsetLeft=0):"rt"===t.offset?(n.offsetTop=0,n.offsetLeft=h.width()-i[0]):"rb"===t.offset?(n.offsetTop=h.height()-i[1],n.offsetLeft=h.width()-i[0]):n.offsetTop=t.offset),t.fixed||(n.offsetTop=/%$/.test(n.offsetTop)?h.height()*parseFloat(n.offsetTop)/100:parseFloat(n.offsetTop),n.offsetLeft=/%$/.test(n.offsetLeft)?h.width()*parseFloat(n.offsetLeft)/100:parseFloat(n.offsetLeft),n.offsetTop+=h.scrollTop(),n.offsetLeft+=h.scrollLeft()),"min"===e.data("maxminStatus")&&(n.offsetTop=h.height()-(e.find(u[1]).outerHeight()||0),n.offsetLeft=e.css("left")),e.css({top:n.offsetTop,left:n.offsetLeft}),n},(p.layer=x).getChildFrame=function(e,t){return t=t||m("."+u[4]).attr("times"),m("#"+u[0]+t).find("iframe").contents().find(e)},x.getFrameIndex=function(e){if(e)return m("#"+e).parents("."+u[4]).attr("times")},x.iframeAuto=function(e){var t,i,n,a,o;e&&(i=(t=m("#"+u[0]+e)).data("config"),e=x.getChildFrame("html",e).outerHeight(),n=t.find(u[1]).outerHeight()||0,a=t.find("."+u[6]).outerHeight()||0,(o="maxHeight"in i?i.maxHeight:h.height())&&(e=Math.min(e,o-n-a)),t.css({height:e+n+a}),t.find("iframe").css({height:e}),d.updatePosition(t,i))},x.iframeSrc=function(e,t){m("#"+u[0]+e).find("iframe").attr("src",t)},x.style=function(e,t,i){var e=m("#"+u[0]+e),n=e.find(".layui-layer-content"),a=e.attr("type"),o=e.find(u[1]).outerHeight()||0,s=e.find("."+u[6]).outerHeight()||0;a!==d.type[3]&&a!==d.type[4]&&(i||(parseFloat(t.width)<=260&&(t.width=260),parseFloat(t.height)-o-s<=64&&(t.height=64+o+s)),e.css(t),s=e.find("."+u[6]).outerHeight()||0,a===d.type[2]?e.find("iframe").css({height:("number"==typeof t.height?t.height:e.height())-o-s}):n.css({height:("number"==typeof t.height?t.height:e.height())-o-s-parseFloat(n.css("padding-top"))-parseFloat(n.css("padding-bottom"))}))},x.min=function(e,t){var i,n,a,o,s,r,l=m("#"+u[0]+e),c=l.data("maxminStatus");"min"!==c&&("max"===c&&x.restore(e),l.data("maxminStatus","min"),t=t||l.data("config")||{},c=m("#"+u.SHADE+e),i=l.find(".layui-layer-min"),n=l.find(u[1]).outerHeight()||0,o=(a="string"==typeof(o=l.attr("minLeft")))?o:181*d.minStackIndex+"px",s=l.css("position"),r={width:180,height:n,position:"fixed",overflow:"hidden"},d.record(l),0h.width()&&(o=h.width()-180-(d.minStackArr.edgeIndex=d.minStackArr.edgeIndex||0,d.minStackArr.edgeIndex+=3))<0&&(o=0),t.minStack&&(r.left=o,r.top=h.height()-n,a||d.minStackIndex++,l.attr("minLeft",o)),l.attr("position",s),x.style(e,r,!0),i.hide(),"page"===l.attr("type")&&l.find(u[4]).hide(),d.restScrollbar(e),c.hide())},x.restore=function(e){var t=m("#"+u[0]+e),i=m("#"+u.SHADE+e),n=t.find(".layui-layer-content"),a=t.attr("area").split(","),o=t.attr("type"),s=t.data("config")||{},r=n.data(l);t.removeData("maxminStatus"),x.style(e,{width:a[0],height:a[1],top:parseFloat(a[2]),left:parseFloat(a[3]),position:t.attr("position"),overflow:"visible"},!0),t.find(".layui-layer-max").removeClass("layui-layer-maxmin"),t.find(".layui-layer-min").show(),"page"===o&&t.find(u[4]).show(),s.scrollbar?d.restScrollbar(e):d.setScrollbar(e),r!==undefined&&(n.removeData(l),(o===d.type[2]?n.children("iframe"):n).css({height:r})),i.show()},x.full=function(t){var i=m("#"+u[0]+t),e=i.data("maxminStatus");"max"!==e&&("min"===e&&x.restore(t),i.data("maxminStatus","max"),d.record(i),d.setScrollbar(t),setTimeout(function(){var e="fixed"===i.css("position");x.style(t,{top:e?0:h.scrollTop(),left:e?0:h.scrollLeft(),width:"100%",height:"100%"},!0),i.find(".layui-layer-min").hide()},100))},x.title=function(e,t){m("#"+u[0]+(t||x.index)).find(u[1]).html(e)},x.close=function(s,r){var e,t,l=(e=m("."+u[0]).children("#"+s).closest("."+u[0]))[0]?(s=e.attr("times"),e):m("#"+u[0]+s),c=l.attr("type"),i=l.data("config")||{},f=i.id&&i.hideOnClose;l[0]&&(t=function(){var o={slideDown:"layer-anim-slide-down-out",slideLeft:"layer-anim-slide-left-out",slideUp:"layer-anim-slide-up-out",slideRight:"layer-anim-slide-right-out"}[i.anim]||"layer-anim-close",e=function(){var e="layui-layer-wrap";if(f)return l.removeClass("layer-anim "+o),l.hide();if(c===d.type[1]&&"object"===l.attr("conType")){l.children(":not(."+u[5]+")").remove();for(var t=l.find("."+e),i=0;i<2;i++)t.unwrap();t.css("display",t.data("display")).removeClass(e)}else{if(c===d.type[2])try{var n=m("#"+u[4]+s)[0];n.contentWindow.document.write(""),n.contentWindow.close(),l.find("."+u[5])[0].removeChild(n)}catch(a){}l[0].innerHTML="",l.remove()}"function"==typeof d.end[s]&&d.end[s](),delete d.end[s],"function"==typeof r&&r(),d.events.resize[s]&&(h.off("resize",d.events.resize[s]),delete d.events.resize[s])},t=m("#"+u.SHADE+s);x.ie&&x.ie<10||!i.isOutAnim?t[f?"hide":"remove"]():(t.css({opacity:0}),setTimeout(function(){t[f?"hide":"remove"]()},350)),i.isOutAnim&&l.addClass("layer-anim "+o),6==x.ie&&d.reselect(),d.restScrollbar(s),"string"==typeof l.attr("minLeft")&&(d.minStackIndex--,d.minStackArr.push(l.attr("minLeft"))),x.ie&&x.ie<10||!i.isOutAnim?e():setTimeout(function(){e()},200)},f||"function"!=typeof d.beforeEnd[s]?(delete d.beforeEnd[s],t()):d.promiseLikeResolve(d.beforeEnd[s]()).then(function(e){!1!==e&&(delete d.beforeEnd[s],t())},function(e){e!==undefined&&p.console&&p.console.error("layer error hint: "+e)}))},x.closeAll=function(n,a){"function"==typeof n&&(a=n,n=null);var o=m("."+u[0]);m.each(o,function(e){var t=m(this),i=n?t.attr("type")===n:1;i&&x.close(t.attr("times"),e===o.length-1?a:null)}),0===o.length&&"function"==typeof a&&a()},x.closeLast=function(i,e){var t,n=[],a=m.isArray(i);m("string"==typeof i?".layui-layer-"+i:".layui-layer").each(function(e,t){t=m(t);if(a&&-1===i.indexOf(t.attr("type"))||"none"===t.css("display"))return!0;n.push(Number(t.attr("times")))}),0":'"),s=i.success;return delete i.success,x.open(m.extend({type:1,btn:[v.$t("layer.confirm"),v.$t("layer.cancel")],content:o,skin:"layui-layer-prompt"+b("prompt"),maxWidth:h.width(),success:function(e){(a=e.find(".layui-layer-input")).val(i.value||"").focus(),"function"==typeof s&&s(e)},resize:!1,yes:function(e){var t=a.val();t.length>(i.maxlength||500)?x.tips(v.$t("layer.prompt.inputLengthPrompt",{length:i.maxlength||500}),a,{tips:1}):n&&n(t,e,a)}},i))},x.tab=function(n){var a=(n=n||{}).tab||{},o="layui-this",s=n.success;return delete n.success,x.open(m.extend({type:1,skin:"layui-layer-tab"+b("tab"),resize:!1,title:function(){var e=a.length,t=1,i="";if(0'+a[0].title+"";t"+a[t].title+"";return i}(),content:'
        '+function(){var e=a.length,t=1,i="";if(0'+(a[0].content||"no content")+"";t'+(a[t].content||"no content")+"";return i}()+"
      ",success:function(e){var t=e.find(".layui-layer-title").children(),i=e.find(".layui-layer-tabmain").children();t.on("mousedown",function(e){e.stopPropagation?e.stopPropagation():e.cancelBubble=!0;var e=m(this),t=e.index();e.addClass(o).siblings().removeClass(o),i.eq(t).show().siblings().hide(),"function"==typeof n.change&&n.change(t)}),"function"==typeof s&&s(e)}},n))},x.photos=function(n,e,a){var s={};if((n=m.extend(!0,{toolbar:!0,footer:!0},n)).photos){var t=!("string"==typeof n.photos||n.photos instanceof m),i=t?n.photos:{},o=i.data||[],r=i.start||0,l=n.success;if(s.imgIndex=1+(0|r),n.img=n.img||"img",delete n.success,t){if(0===o.length)return x.msg(v.$t("layer.photos.noData"))}else{var c=m(n.photos),f=function(){o=[],c.find(n.img).each(function(e){var t=m(this);t.attr("layer-index",e),o.push({alt:t.attr("alt"),pid:t.attr("layer-pid"),src:t.attr("lay-src")||t.attr("layer-src")||t.attr("src"),thumb:t.attr("src")})})};if(f(),e||c.on("click",n.img,function(){f();var e=m(this).attr("layer-index");x.photos(m.extend(n,{photos:{start:e,data:o,tab:n.tab},full:n.full}),!0)}),!e)return}s.imgprev=function(e){s.imgIndex--,s.imgIndex<1&&(s.imgIndex=o.length),s.tabimg(e)},s.imgnext=function(e,t){s.imgIndex++,s.imgIndex>o.length&&(s.imgIndex=1,t)||s.tabimg(e)},s.keyup=function(e){var t;s.end||(t=e.keyCode,e.preventDefault(),37===t?s.imgprev(!0):39===t?s.imgnext(!0):27===t&&x.close(s.index))},s.tabimg=function(e){if(!(o.length<=1))return i.start=s.imgIndex-1,x.close(s.index),x.photos(n,!0,e)},s.isNumber=function(e){return"number"==typeof e&&!isNaN(e)},s.image={},s.getTransform=function(e){var t=[],i=e.rotate,n=e.scaleX,e=e.scale;return s.isNumber(i)&&0!==i&&t.push("rotate("+i+"deg)"),s.isNumber(n)&&1!==n&&t.push("scaleX("+n+")"),s.isNumber(e)&&t.push("scale("+e+")"),t.length?t.join(" "):"none"},s.event=function(e,i,n){var a,o;s.main.find(".layui-layer-photos-prev").on("click",function(e){e.preventDefault(),s.imgprev(!0)}),s.main.find(".layui-layer-photos-next").on("click",function(e){e.preventDefault(),s.imgnext(!0)}),m(document).on("keyup",s.keyup),e.off("click").on("click","*[toolbar-event]",function(){var e=m(this);switch(e.attr("toolbar-event")){case"rotate":s.image.rotate=((s.image.rotate||0)+Number(e.attr("data-option")))%360,s.imgElem.css({transform:s.getTransform(s.image)});break;case"scalex":s.image.scaleX=-1===s.image.scaleX?1:-1,s.imgElem.css({transform:s.getTransform(s.image)});break;case"zoom":var t=Number(e.attr("data-option"));s.image.scale=(s.image.scale||1)+t,t<0&&s.image.scale<0-t&&(s.image.scale=0-t),s.imgElem.css({transform:s.getTransform(s.image)});break;case"reset":s.image.scaleX=1,s.image.scale=1,s.image.rotate=0,s.imgElem.css({transform:"none"});break;case"close":x.close(i)}n.offset(),n.auto(i)}),s.main.on("mousewheel DOMMouseScroll",function(e){var t=e.originalEvent.wheelDelta||-e.originalEvent.detail,i=s.main.find('[toolbar-event="zoom"]');(0n)&&("left"===t.direction?s.imgnext(!0):"right"===t.direction&&s.imgprev(!0))},m.each([n.shadeo,s.main],function(e,t){a.touchSwipe(t,{onTouchEnd:o})}))},s.loadi=x.load(1,{shade:!("shade"in n)&&[.9,undefined,"unset"],scrollbar:!1});var t=o[r].src,d=function(e){x.close(s.loadi);var t,i=o[r].alt||"";a&&(n.anim=-1),s.index=x.open(m.extend({type:1,id:"layui-layer-photos",area:(e=[e.width,e.height],t=[m(p).width()-100,m(p).height()-100],!n.full&&(t[0]'+i+''+(t=['
      '],1','','',"
      "].join("")),n.toolbar&&t.push(['
      ','','','','','','',"
      "].join("")),n.footer&&t.push(['"].join("")),t.push(""),t.join(""))+"",success:function(e,t,i){s.main=e.find(".layer-layer-photos-main"),s.footer=e.find(".layui-layer-photos-footer"),s.imgElem=s.main.children("img"),s.event(e,t,i),n.tab&&n.tab(o[r],e),"function"==typeof l&&l(e)},end:function(){s.end=!0,m(document).off("keyup",s.keyup)}},n))},u=function(){x.close(s.loadi),x.msg(''+v.$t("layer.photos.urlError.prompt")+"",{time:3e4,btn:[v.$t("layer.photos.urlError.confirm"),v.$t("layer.photos.urlError.cancel")],yes:function(){1").addClass(r));layui.each(i.bars,function(t,e){var n=s('
    • ');n.addClass(e.icon).attr({"lay-type":e.type,style:e.style||(i.bgcolor?"background-color: "+i.bgcolor:"")}).html(e.content),n.on("click",function(){var t=s(this).attr("lay-type");"top"===t&&("body"===i.target?s("html,body"):c).animate({scrollTop:0},i.duration),"function"==typeof i.click&&i.click.call(this,t)}),"object"===layui.type(i.on)&&layui.each(i.on,function(t,e){n.on(t,function(){var t=s(this).attr("lay-type");"function"==typeof e&&e.call(this,t)})}),"top"===e.type&&(n.addClass("layui-fixbar-top"),o=n),l.append(n)}),u.find("."+r).remove(),"object"==typeof i.css&&l.css(i.css),u.append(l),o&&(e=function e(){return c.scrollTop()>=i.margin?t||(o.show(),t=1):t&&(o.hide(),t=0),e}()),c.on("scroll",function(){e&&(clearTimeout(n),n=setTimeout(function(){e()},100))})},countdown:function(i){i=s.extend(!0,{date:new Date,now:new Date},i);var o=arguments,r=(1r-t.margin||u."+w,O=function(e){var i=this;i.index=++a.index,i.config=c.extend({},i.config,a.config,e),i.stopClickOutsideEvent=c.noop,i.stopResizeEvent=c.noop,i.init()};O.prototype.config={trigger:"click",content:"",className:"",style:"",show:!1,isAllowSpread:!0,isSpreadItem:!0,data:[],delay:[200,300],shade:0,accordion:!1,closeOnClick:!0},O.prototype.reload=function(e,i){var t=this;t.config=c.extend({},t.config,e),t.init(!0,i)},O.prototype.init=function(e,i){var t=this,n=t.config,o=c(n.elem);return 1",(t="href"in i?''+a+"":a,n?'
      '+t+("parent"===l?'':"group"===l&&s.isAllowSpread?'':"")+"
      ":'
      '+t+"
      "),"
    • "].join(""))).data("item",i),n&&(o=c('
      '),t=c("
        "),"parent"===l?(o.append(u(t,i[d.children])),a.append(o)):a.append(u(t,i[d.children]))),r.append(a))}),r},t=['
        ',"
        "].join(""),n=s.content||(n=c('
          '),0'+r.$t("dropdown.noData")+""),n),o=v.findMainElem(s.id);"reloadData"===e&&o.length?(i=a.mainElem=o).html(n):((i=a.mainElem=c(t)).append(n),i.addClass(s.className),i.attr("style",s.style),a.remove(s.id),s.target.append(i),s.elem.data(y,!0),e=s.shade?'
          ':"",o=c(e),"touchstart"==f&&o.on(f,function(e){e.preventDefault()}),i.before(o),"mouseenter"===s.trigger&&i.on("mouseenter",function(){clearTimeout(a.timer)}).on("mouseleave",function(){a.delayRemove()})),a.position(),i.find(".layui-menu").on(f,function(e){layui.stope(e)}),i.find(".layui-menu li").on("click",function(e){var i=c(this),t=i.data("item")||{},n=t[d.children]&&0n.width()&&(t.addClass(b),(i=t[0].getBoundingClientRect()).left<0)&&t.removeClass(b),i.bottom>n.height())&&t.eq(0).css("margin-top",-(i.bottom-n.height()+5))}).on("mouseleave",t,function(e){var i=c(this).children("."+o);i.removeClass(b),i.css("margin-top",0)}),a.close=function(e){e=v.getThis(e);return e?(e.remove(),v.call(e)):this},a.open=function(e){e=v.getThis(e);return e?(e.render(),v.call(e)):this},a.reload=function(e,i,t){e=v.getThis(e);return e?(e.reload(i,t),v.call(e)):this},a.reloadData=function(){var t=c.extend([],arguments),n=(t[2]="reloadData",new RegExp("^("+["data","templet","content"].join("|")+")$"));return layui.each(t[1],function(e,i){n.test(e)||delete t[1][e]}),a.reload.apply(null,t)},a.render=function(e){e=new O(e);return v.call(e)},e(s,a)});layui.define("component",function(e){"use strict";var E=layui.$,I=layui.lay,t=layui.component({name:"slider",config:{type:"default",min:0,max:100,value:0,step:1,showstep:!1,tips:!0,tipsAlways:!1,input:!1,range:!1,height:200,disabled:!1,theme:"#16baaa"},CONST:{ELEM_VIEW:"layui-slider",SLIDER_BAR:"layui-slider-bar",SLIDER_WRAP:"layui-slider-wrap",SLIDER_WRAP_BTN:"layui-slider-wrap-btn",SLIDER_TIPS:"layui-slider-tips",SLIDER_INPUT:"layui-slider-input",SLIDER_INPUT_TXT:"layui-slider-input-txt",SLIDER_INPUT_BTN:"layui-slider-input-btn",ELEM_HOVER:"layui-slider-hover"},render:function(e){var t,a=this,n=a.config,i=(n.step<=0&&(n.step=1),n.maxn.max&&(n.value=n.max),s=(n.value-n.min)/(n.max-n.min)*100+"%"),n.disabled?"#c2c2c2":n.theme),l='
          '+(n.tips?'
          ":"")+'
          '+(n.range?'
          ':"")+"
          ",s=E(n.elem),o=s.next("."+S.ELEM_VIEW);if(o[0]&&o.remove(),a.elemTemp=E(l),n.range?(a.elemTemp.find("."+S.SLIDER_WRAP).eq(0).data("value",n.value[0]),a.elemTemp.find("."+S.SLIDER_WRAP).eq(1).data("value",n.value[1])):a.elemTemp.find("."+S.SLIDER_WRAP).data("value",n.value),s.html(a.elemTemp),"vertical"===n.type&&a.elemTemp.height(n.height+"px"),n.showstep){for(var r=(n.max-n.min)/n.step,u="",c=1;c<1+r;c++){var d=100*c/r;d<100&&(u+='
          ')}a.elemTemp.append(u)}function p(e){e=e.parent().data("value"),e=n.setTips?n.setTips(e):e;a.elemTemp.find("."+S.SLIDER_TIPS).html(e)}function m(e){var t="vertical"===n.type?n.height:a.elemTemp[0].offsetWidth,i=a.elemTemp.find("."+S.SLIDER_WRAP);return("vertical"===n.type?t-e.parent()[0].offsetTop-i.height():e.parent()[0].offsetLeft)/t*100}function v(e){"vertical"===n.type?a.elemTemp.find("."+S.SLIDER_TIPS).css({bottom:e+"%","margin-bottom":"20px",display:"inline-block"}):a.elemTemp.find("."+S.SLIDER_TIPS).css({left:e+"%",display:"inline-block"})}n.input&&!n.range&&(i=E('
          '),s.css("position","relative"),s.append(i),s.find("."+S.SLIDER_INPUT_TXT).children("input").val(n.value),"vertical"===n.type?i.css({left:0,top:-48}):a.elemTemp.css("margin-right",i.outerWidth()+15)),n.disabled?(a.elemTemp.addClass(S.CLASS_DISABLED),a.elemTemp.find("."+S.SLIDER_WRAP_BTN).addClass(S.CLASS_DISABLED)):a.slide(),n.tips&&(n.tipsAlways?(p(o=a.elemTemp.find("."+S.SLIDER_WRAP_BTN)),v(m(o))):a.elemTemp.find("."+S.SLIDER_WRAP_BTN).on("mouseover",function(){p(E(this));var e=m(E(this));clearTimeout(t),t=setTimeout(function(){v(e)},300)}).on("mouseout",function(){clearTimeout(t),n.tipsAlways||a.elemTemp.find("."+S.SLIDER_TIPS).css("display","none")}))},extendsInstance:function(){var i=this,a=i.config;return{setValue:function(e,t){return e=(e=e>a.max?a.max:e)a[1]&&a.reverse(),u.value=c.range?a:l,c.change&&c.change(u.value),"done"===i&&c.done&&c.done(u.value)},y=function(e){var t=e/p()*100/v,i=Math.round(t)*v;return i=e==p()?Math.ceil(t)*v:i},T=E(['
          p()?p():t)/p()*100/v;h(t,o),r.addClass(S.ELEM_HOVER),d.find("."+S.SLIDER_TIPS).show(),e.preventDefault()},a=function(e){r.removeClass(S.ELEM_HOVER),c.tipsAlways||setTimeout(function(){d.find("."+S.SLIDER_TIPS).hide()},e)},n=function(){a&&a(I.touchEventsSupported()?1e3:0),T.remove(),c.done&&c.done(u.value),I.touchEventsSupported()&&(t[0].removeEventListener("touchmove",i,!!I.passiveSupported&&{passive:!1}),t[0].removeEventListener("touchend",n),t[0].removeEventListener("touchcancel",n))},E("#LAY-slider-moving")[0]||E("body").append(T),T.on("mousemove",i),T.on("mouseup",n).on("mouseleave",n),I.touchEventsSupported()&&(t[0].addEventListener("touchmove",i,!!I.passiveSupported&&{passive:!1}),t[0].addEventListener("touchend",n),t[0].addEventListener("touchcancel",n))})}),d.on("click",function(e){var t=E("."+S.SLIDER_WRAP_BTN),i=E(this);!t.is(e.target)&&0===t.has(e.target).length&&t.length&&(i=(t=(t=(t="vertical"===c.type?p()-e.clientY+i.offset().top-E(window).scrollTop():e.clientX-i.offset().left-E(window).scrollLeft())<0?0:t)>p()?p():t)/p()*100/v,t=c.range?"vertical"===c.type?Math.abs(t-parseInt(E(m[0]).css("bottom")))>Math.abs(t-parseInt(E(m[1]).css("bottom")))?1:0:Math.abs(t-m[0].offsetLeft)>Math.abs(t-m[1].offsetLeft)?1:0:0,h(i,t,"done"),e.preventDefault())}),o.children("."+S.SLIDER_INPUT_BTN).children("i").each(function(t){E(this).on("click",function(){r=o.children("."+S.SLIDER_INPUT_TXT).children("input").val();var e=((r=1==t?r-c.stepc.max?c.max:Number(r)+c.step)-c.min)/(c.max-c.min)*100/v;h(e,0,"done")})});var a=function(){var e=this.value,e=(e=(e=(e=isNaN(e)?0:e)c.max?c.max:e,((this.value=e)-c.min)/(c.max-c.min)*100/v);h(e,0,"done")};o.children("."+S.SLIDER_INPUT_TXT).children("input").on("keydown",function(e){13===e.keyCode&&(e.preventDefault(),a.call(this))}).on("change",a)},e(S.MOD_NAME,t)});layui.define(["i18n","component"],function(e){"use strict";var P=layui.$,I=layui.lay,t=layui.i18n,i=layui.device().mobile?"click":"mousedown",o=layui.component({name:"colorpicker",config:{color:"",size:null,alpha:!1,format:"hex",predefine:!1,colors:["#16baaa","#16b777","#1E9FFF","#FF5722","#FFB800","#01AAED","#999","#c00","#ff8c00","#ffd700","#90ee90","#00ced1","#1e90ff","#c71585","#393D49","rgb(0, 186, 189)","rgb(255, 120, 0)","rgb(250, 212, 0)","rgba(0,0,0,.5)","rgba(255, 69, 0, 0.68)","rgba(144, 240, 144, 0.5)","rgba(31, 147, 255, 0.73)"]},CONST:{ELEM:"layui-colorpicker",ELEM_MAIN:".layui-colorpicker-main",ICON_PICKER_DOWN:"layui-icon-down",ICON_PICKER_CLOSE:"layui-icon-close",PICKER_TRIG_SPAN:"layui-colorpicker-trigger-span",PICKER_TRIG_I:"layui-colorpicker-trigger-i",PICKER_SIDE:"layui-colorpicker-side",PICKER_SIDE_SLIDER:"layui-colorpicker-side-slider",PICKER_BASIS:"layui-colorpicker-basis",PICKER_ALPHA_BG:"layui-colorpicker-alpha-bgcolor",PICKER_ALPHA_SLIDER:"layui-colorpicker-alpha-slider",PICKER_BASIS_CUR:"layui-colorpicker-basis-cursor",PICKER_INPUT:"layui-colorpicker-main-input"},beforeInit:function(){this.stopClickOutsideEvent=P.noop,this.stopResizeEvent=P.noop,S.PICKER_OPENED=S.MOD_ID+"-opened"},beforeRender:function(){this.config.target=P("body")},render:function(){var e=this,i=e.config,o=P(e.buildColorBoxTemplate(i)),t=i.elem;i.size&&o.addClass("layui-colorpicker-"+i.size),t.addClass("layui-inline").html(e.elemColorBox=o),e.color=e.elemColorBox.find("."+S.PICKER_TRIG_SPAN)[0].style.background}}),k=function(e){var i={h:0,s:0,b:0},o=Math.min(e.r,e.g,e.b),t=Math.max(e.r,e.g,e.b),r=t-o;return i.b=t,i.s=0!==t?255*r/t:0,0!==i.s?e.r==t?i.h=(e.g-e.b)/r:e.g==t?i.h=2+(e.b-e.r)/r:i.h=4+(e.r-e.g)/r:i.h=-1,t===o&&(i.h=0),i.h*=60,i.h<0&&(i.h+=360),i.s*=100/255,i.b*=100/255,i},E=function(e){var i,o={},t=e.h,r=255*e.s/100,e=255*e.b/100;return 0==r?o.r=o.g=o.b=e:(e=t%60*((i=e)-(r=(255-r)*e/255))/60,(t=360===t?0:t)<60?(o.r=i,o.b=r,o.g=r+e):t<120?(o.g=i,o.b=r,o.r=i-e):t<180?(o.g=i,o.r=r,o.b=r+e):t<240?(o.b=i,o.r=r,o.g=i-e):t<300?(o.b=i,o.g=r,o.r=r+e):t<360?(o.r=i,o.g=r,o.b=i-e):(o.r=0,o.g=0,o.b=0)),{r:Math.round(o.r),g:Math.round(o.g),b:Math.round(o.b)}},_=function(e){var e=E(e),o=[e.r.toString(16),e.g.toString(16),e.b.toString(16)];return P.each(o,function(e,i){1===i.length&&(o[e]="0"+i)}),o.join("")},R=function(e){e=e.match(/[0-9]{1,3}/g)||[];return{r:e[0],g:e[1],b:e[2]}},K=P(window),S=o.CONST,r=o.Class;r.prototype.buildColorBoxTemplate=function(e){var i;return['
          '," ",' ',' '," "," ","
          "].join("")},r.prototype.buildColorPickerTemplate=function(e,i){var o;return['
          ','
          ','
          ','
          ','
          ','
          ',"
          ",'
          ','
          ',"
          ","
          ",'
          ','
          ','
          ',"
          ","
          ",e.predefine?(o=['
          '],layui.each(e.colors,function(e,i){o.push(['
          ','
          ',"
          "].join(""))}),o.push("
          "),o.join("")):"",'
          ','
          ',' ',"
          ",'
          ',' ",' ","
          ","
          ","
          "].join("")},r.prototype.renderPicker=function(){var e=this,i=e.config,o=e.elemPicker=P(e.buildColorPickerTemplate(i,e));e.removePicker(i.id),i.target.append(o),i.elem.data(S.PICKER_OPENED,!0),e.position(),e.pickerEvents(),e.onClickOutside(),e.autoUpdatePosition()},r.prototype.removePicker=function(e){var i=this,o=i.config,e=P("#layui-colorpicker"+(e||i.index));return i.stopClickOutsideEvent(),i.stopResizeEvent(),e[0]&&(e.remove(),o.elem.removeData(S.PICKER_OPENED),"function"==typeof o.close)&&o.close(i.color),i},r.prototype.position=function(){var e=this,i=e.config;return I.position(e.bindElem||e.elemColorBox[0],e.elemPicker[0],{position:i.position,align:"center"}),e},r.prototype.val=function(){var e,i=this,o=i.elemColorBox.find("."+S.PICKER_TRIG_SPAN),t=i.elemPicker.find("."+S.PICKER_INPUT),r=o[0].style.backgroundColor;r?(e=k(R(r)),o=o.attr("lay-type"),i.select(e.h,e.s,e.b),"torgb"===o?t.find("input").val(r):"rgba"===o?(o=R(r),3===(r.match(/[0-9]{1,3}/g)||[]).length?(t.find("input").val("rgba("+o.r+", "+o.g+", "+o.b+", 1)"),i.elemPicker.find("."+S.PICKER_ALPHA_SLIDER).css("left",280)):(t.find("input").val(r),r=280*r.slice(r.lastIndexOf(",")+1,r.length-1),i.elemPicker.find("."+S.PICKER_ALPHA_SLIDER).css("left",r)),i.elemPicker.find("."+S.PICKER_ALPHA_BG)[0].style.background="linear-gradient(to right, rgba("+o.r+", "+o.g+", "+o.b+", 0), rgb("+o.r+", "+o.g+", "+o.b+"))"):t.find("input").val("#"+_(e))):(i.select(0,100,100),t.find("input").val(""),i.elemPicker.find("."+S.PICKER_ALPHA_BG)[0].style.background="",i.elemPicker.find("."+S.PICKER_ALPHA_SLIDER).css("left",280))},r.prototype.side=function(){var n=this,l=n.config,c=n.elemColorBox.find("."+S.PICKER_TRIG_SPAN),a=c.attr("lay-type"),s=n.elemPicker.find("."+S.PICKER_SIDE),o=n.elemPicker.find("."+S.PICKER_SIDE_SLIDER),u=n.elemPicker.find("."+S.PICKER_BASIS),t=n.elemPicker.find("."+S.PICKER_BASIS_CUR),d=n.elemPicker.find("."+S.PICKER_ALPHA_BG),f=n.elemPicker.find("."+S.PICKER_ALPHA_SLIDER),p=o[0].offsetTop/180*360,g=100-t[0].offsetTop/180*100,v=t[0].offsetLeft/260*100,h=Math.round(f[0].offsetLeft/280*100)/100,m=n.elemColorBox.find("."+S.PICKER_TRIG_I),e=n.elemPicker.find(".layui-colorpicker-pre").children("div"),b=function(e,i,o,t){n.select(e,i,o);var r=E({h:e,s:i,b:o}),e=_({h:e,s:i,b:o}),i=n.elemPicker.find("."+S.PICKER_INPUT).find("input");m.addClass(S.ICON_PICKER_DOWN).removeClass(S.ICON_PICKER_CLOSE),c[0].style.background="rgb("+r.r+", "+r.g+", "+r.b+")","torgb"===a?i.val("rgb("+r.r+", "+r.g+", "+r.b+")"):"rgba"===a?(f.css("left",280*t),i.val("rgba("+r.r+", "+r.g+", "+r.b+", "+t+")"),c[0].style.background="rgba("+r.r+", "+r.g+", "+r.b+", "+t+")",d[0].style.background="linear-gradient(to right, rgba("+r.r+", "+r.g+", "+r.b+", 0), rgb("+r.r+", "+r.g+", "+r.b+"))"):i.val("#"+e),l.change&&l.change(P.trim(n.elemPicker.find("."+S.PICKER_INPUT).find("input").val()))},i=P('
          '),y=function(e){P("#LAY-colorpicker-moving")[0]||P("body").append(i),i.on("mousemove",e),i.on("mouseup",function(){i.remove()}).on("mouseleave",function(){i.remove()})},r=!0,C=!0;o.on("mousedown",function(e,i){var t=this.offsetTop,r=(e.clientY===undefined?i:e).clientY;C&&layui.stope(e),y(function(e){var i=t+(e.clientY-r),o=s[0].offsetHeight,o=(i=o<(i=i<0?0:i)?o:i)/180*360;b(p=o,v,g,h),e.preventDefault()}),e.preventDefault()}),s.on("mousedown",function(e){var i=e.clientY-P(this).offset().top+K.scrollTop(),i=(i=(i=i<0?0:i)>this.offsetHeight?this.offsetHeight:i)/180*360;b(p=i,v,g,h),e.preventDefault(),r&&o.trigger("mousedown",e)}),t.on("mousedown",function(e,i){var n=this.offsetTop,l=this.offsetLeft,c=(e.clientY===undefined?i:e).clientY,a=(e.clientX===undefined?i:e).clientX;C&&layui.stope(e),y(function(e){var i=n+(e.clientY-c),o=l+(e.clientX-a),t=u[0].offsetHeight,r=u[0].offsetWidth,r=(o=r<(o=o<0?0:o)?r:o)/260*100,o=100-(i=t<(i=i<0?0:i)?t:i)/180*100;b(p,v=r,g=o,h),e.preventDefault()}),e.preventDefault()}),u.on("mousedown",function(e){var i=e.clientY-P(this).offset().top+K.scrollTop(),o=e.clientX-P(this).offset().left+K.scrollLeft(),o=((i=i<0?0:i)>this.offsetHeight&&(i=this.offsetHeight),(o=(o=o<0?0:o)>this.offsetWidth?this.offsetWidth:o)/260*100),i=100-i/180*100;b(p,v=o,g=i,h),layui.stope(e),e.preventDefault(),r&&t.trigger("mousedown",e)}),f.on("mousedown",function(e,i){var t=this.offsetLeft,r=(e.clientX===undefined?i:e).clientX;C&&layui.stope(e),y(function(e){var i=t+(e.clientX-r),o=d[0].offsetWidth,o=(o<(i=i<0?0:i)&&(i=o),Math.round(i/280*100)/100);b(p,v,g,h=o),e.preventDefault()}),e.preventDefault()}),d.on("mousedown",function(e){var i=e.clientX-P(this).offset().left,i=((i=i<0?0:i)>this.offsetWidth&&(i=this.offsetWidth),Math.round(i/280*100)/100);b(p,v,g,h=i),e.preventDefault(),r&&f.trigger("mousedown",e)}),e.each(function(){P(this).on("click",function(){P(this).parent(".layui-colorpicker-pre").addClass("selected").siblings().removeClass("selected");var e=this.style.backgroundColor,i=k(R(e)),o=e.slice(e.lastIndexOf(",")+1,e.length-1);p=i.h,v=i.s,g=i.b,3===(e.match(/[0-9]{1,3}/g)||[]).length&&(o=1),h=o,b(i.h,i.s,i.b,o)})}),I.touchEventsSupported()&&layui.each([{elem:s,eventType:"mousedown"},{elem:d,eventType:"mousedown"},{elem:u,eventType:"mousedown"}],function(e,t){I.touchSwipe(t.elem,{onTouchStart:function(){C=r=!1},onTouchMove:function(e){var i,o;e=e,i=t.eventType,e=e.touches[0],(o=document.createEvent("MouseEvent")).initMouseEvent(i,!0,!0,window,1,e.screenX,e.screenY,e.clientX,e.clientY,!1,!1,!1,!1,0,null),e.target.dispatchEvent(o)},onTouchEnd:function(){i.remove(),C=r=!0}})})},r.prototype.select=function(e,i,o,t){var r=_({h:e,s:100,b:100}),e=e/360*180,o=180-o/100*180,i=i/100*260,n=this.elemPicker.find("."+S.PICKER_BASIS)[0];this.elemPicker.find("."+S.PICKER_SIDE_SLIDER).css("top",e),n.style.background="#"+r,this.elemPicker.find("."+S.PICKER_BASIS_CUR).css({top:o/n.offsetHeight*100+"%",left:i/n.offsetWidth*100+"%"})},r.prototype.pickerEvents=function(){var c=this,a=c.config,s=c.elemColorBox.find("."+S.PICKER_TRIG_SPAN),u=c.elemPicker.find("."+S.PICKER_INPUT+" input"),o={clear:function(e){s[0].style.background="",c.elemColorBox.find("."+S.PICKER_TRIG_I).removeClass(S.ICON_PICKER_DOWN).addClass(S.ICON_PICKER_CLOSE),c.color="",a.done&&a.done(""),c.removePicker()},confirm:function(e,i){var o,t,r,n,l=P.trim(u.val());-1>16,g:(65280&r)>>8,b:255&r},t=k(n),s[0].style.background=o="#"+_(t),c.elemColorBox.find("."+S.PICKER_TRIG_I).removeClass(S.ICON_PICKER_CLOSE).addClass(S.ICON_PICKER_DOWN)),"change"===i?(c.select(t.h,t.s,t.b,i),a.change&&a.change(o)):(c.color=l,a.done&&a.done(l),c.removePicker())}};c.elemPicker.on("click","*[colorpicker-events]",function(){var e=P(this),i=e.attr("colorpicker-events");o[i]&&o[i].call(this,e)}),u.on("keyup",function(e){var i=P(this);o.confirm.call(this,i,13===e.keyCode?null:"change")})},r.prototype.events=function(){var e=this,i=e.config;e.elemColorBox.on("click",function(){i.elem.data(S.PICKER_OPENED)?e.removePicker():(e.renderPicker(),e.val(),e.side())})},r.prototype.onClickOutside=function(){var t=this,r=t.config,e=(t.stopClickOutsideEvent(),I.onClickOutside(t.elemPicker[0],function(e){var i,o=t.elemColorBox.find("."+S.PICKER_TRIG_SPAN);t.color?(i=k(R(t.color)),t.select(i.h,i.s,i.b)):t.elemColorBox.find("."+S.PICKER_TRIG_I).removeClass(S.ICON_PICKER_DOWN).addClass(S.ICON_PICKER_CLOSE),o[0].style.background=t.color||"","function"==typeof r.cancel&&r.cancel(t.color),t.removePicker()},{ignore:[r.elem[0]],event:i,capture:!1}));t.stopClickOutsideEvent=function(){e(),t.stopClickOutsideEvent=P.noop}},r.prototype.autoUpdatePosition=function(){var e=this,i="resize.lay_colorpicker_resize",o=(e.stopResizeEvent(),function(){e.position()});K.on(i,o),e.stopResizeEvent=function(){K.off(i,o),e.stopResizeEvent=P.noop}},e(S.MOD_NAME,o)});layui.define("component",function(e){"use strict";var d=layui.$,s="element",t=layui.component({name:"tab",config:{elem:".layui-tab"},CONST:{ELEM:"layui-tab",HEADER:"layui-tab-title",CLOSE:"layui-tab-close",MORE:"layui-tab-more",BAR:"layui-tab-bar"},render:function(){var e=this.config;o.tabAuto(null,e.elem)}}),u=t.CONST,a=d(window),i=d(document),o={tabClick:function(e){var t=(e=e||{}).options||{},a=e.liElem||d(this),i=t.headerElem?a.parent():a.parents(".layui-tab").eq(0),t=t.bodyElem?d(t.bodyElem):i.children(".layui-tab-content").children(".layui-tab-item"),l=a.find("a"),l="javascript:;"!==l.attr("href")&&"_blank"===l.attr("target"),n="string"==typeof a.attr("lay-unselect"),r=i.attr("lay-filter"),c=a.attr("lay-id"),o="index"in e?e.index:a.parent().children("li").index(a);if(!e.force){var e=a.siblings("."+u.CLASS_THIS);if(!1===layui.event.call(this,s,"tabBeforeChange("+r+")",{elem:i,from:{index:a.parent().children("li").index(e),id:e.attr("lay-id")},to:{index:o,id:c}}))return}l||n||(a.addClass(u.CLASS_THIS).siblings().removeClass(u.CLASS_THIS),(c?e=(e=t.filter('[lay-id="'+c+'"]')).length?e:t.eq(o):t.eq(o)).addClass(u.CLASS_SHOW).siblings().removeClass(u.CLASS_SHOW)),layui.event.call(this,s,"tab("+r+")",{elem:i,index:o,id:c})},tabDelete:function(e){var t=(e=e||{}).liElem||d(this).parent(),a=t.parent().children("li").index(t),i=t.closest(".layui-tab"),l=i.children(".layui-tab-content").children(".layui-tab-item"),n=i.attr("lay-filter"),r=t.attr("lay-id");if(!e.force&&!1===layui.event.call(t[0],s,"tabBeforeDelete("+n+")",{elem:i,index:a,id:r}))return;t.hasClass(u.CLASS_THIS)&&(t.next()[0]&&t.next().is("li")?o.tabClick.call(t.next()[0],{index:a+1}):t.prev()[0]&&t.prev().is("li")&&o.tabClick.call(t.prev()[0],null,a-1)),t.remove(),(r?e=(e=l.filter('[lay-id="'+r+'"]')).length?e:l.eq(a):l.eq(a)).remove(),setTimeout(function(){o.tabAuto(null,i)},50),layui.event.call(this,s,"tabDelete("+n+")",{elem:i,index:a,id:r})},tabAuto:function(l,e){(e||d(".layui-tab")).each(function(){var e=d(this),a=e.children("."+u.HEADER),t='lay-stope="tabmore"',t=d(''),i=e.attr("lay-allowclose");i&&"false"!==i&&a.find("li").each(function(){var e,t=d(this);t.find("."+u.CLOSE)[0]||"false"===t.attr("lay-allowclose")||((e=d('')).on("click",function(e){o.tabDelete.call(this,{e:e})}),t.append(e))}),"string"!=typeof e.attr("lay-unauto")&&(a.prop("scrollWidth")>a.outerWidth()+1||a.find("li").length&&a.height()>(i=a.find("li").eq(0).height())+i/2?("change"===l&&a.data("LAY_TAB_CHANGE")&&a.addClass(u.MORE),a.find("."+u.BAR)[0]||(a.append(t),e.attr("overflow",""),t.on("click",function(e){var t=a.hasClass(u.MORE);a[t?"removeClass":"addClass"](u.MORE)}))):(a.find("."+u.BAR).remove(),e.removeAttr("overflow")))})},hideTabMore:function(e){var t=d("."+u.HEADER);!0!==e&&"tabmore"===d(e.target).attr("lay-stope")||(t.removeClass(u.MORE),t.find("."+u.BAR).attr("title",""))}};d.extend(t,{tabAdd:function(e,t){var a,i=d(".layui-tab[lay-filter="+e+"]"),l=i.children("."+u.HEADER),n=l.children("."+u.BAR),r=i.children(".layui-tab-content"),c=""+(t.title||"unnaming")+"";return n[0]?n.before(c):l.append(c),r.append('
          "+(t.content||"")+"
          "),t.change&&this.tabChange(e,t.id),l.data("LAY_TAB_CHANGE",t.change),o.tabAuto(t.change?"change":null,i),this},tabDelete:function(e,t,a){e=d(".layui-tab[lay-filter="+e+"]").children("."+u.HEADER).find('>li[lay-id="'+t+'"]');return o.tabDelete.call(e[0],{liElem:e,force:a}),this},tabChange:function(e,t,a){e=d(".layui-tab[lay-filter="+e+"]").children("."+u.HEADER).find('>li[lay-id="'+t+'"]');return o.tabClick.call(e[0],{liElem:e,force:a}),this},tab:function(a){a=a||{},i.on("click",a.headerElem,function(e){var t=d(a.headerElem).index(d(this));o.tabClick.call(this,{index:t,options:a})})}}),i.on("click","."+u.HEADER+" li",o.tabClick),a.on("resize.lay_tab_auto_resize",o.tabAuto),e(u.MOD_NAME,t)});layui.define("component",function(i){"use strict";var _=layui.$,f=layui.device(),a=layui.component({name:"nav",config:{elem:".layui-nav"},CONST:{NAV_ELEM:".layui-nav",NAV_ITEM:"layui-nav-item",NAV_BAR:"layui-nav-bar",NAV_TREE:"layui-nav-tree",NAV_CHILD:"layui-nav-child",NAV_CHILD_C:"layui-nav-child-c",NAV_MORE:"layui-nav-more",NAV_DOWN:"layui-icon-down",NAV_ANIM:"layui-anim layui-anim-upbit"},render:function(){var i=this.config,l={},o={},c={};i.elem.each(function(i){var a=_(this),s=_(''),e=a.find("."+r.NAV_ITEM),t=a.find("."+r.NAV_BAR);t[0]&&t.remove(),a.append(s),(a.hasClass(r.NAV_TREE)?e.find("dd,>."+r.NAV_TITLE):e).off("mouseenter.lay_nav").on("mouseenter.lay_nav",function(){!function(i,a,s){var e,t=_(this),n=t.find("."+r.NAV_CHILD);a.hasClass(r.NAV_TREE)?n[0]||(e=t.children(".layui-nav-title"),i.css({top:t.offset().top-a.offset().top+a.scrollTop(),height:(e[0]?e:t).outerHeight(),opacity:1})):(n.addClass(r.NAV_ANIM),n.hasClass(r.NAV_CHILD_C)&&n.css({left:-(n.outerWidth()-t.width())/2}),n[0]?i.css({left:i.position().left+i.width()/2,width:0,opacity:0}):i.css({left:t.position().left+parseFloat(t.css("marginLeft")),top:t.position().top+t.height()-i.height()}),l[s]=setTimeout(function(){i.css({width:n[0]?0:t.width(),opacity:n[0]?0:1})},f.ie&&f.ie<10?0:200),clearTimeout(c[s]),"block"===n.css("display")&&clearTimeout(o[s]),o[s]=setTimeout(function(){n.addClass(r.CLASS_SHOW),t.find("."+r.NAV_MORE).addClass(r.NAV_MORE+"d")},300))}.call(this,s,a,i)}).off("mouseleave.lay_nav").on("mouseleave.lay_nav",function(){a.hasClass(r.NAV_TREE)?s.css({height:0,opacity:0}):(clearTimeout(o[i]),o[i]=setTimeout(function(){a.find("."+r.NAV_CHILD).removeClass(r.CLASS_SHOW),a.find("."+r.NAV_MORE).removeClass(r.NAV_MORE+"d")},300))}),a.off("mouseleave.lay_nav").on("mouseleave.lay_nav",function(){clearTimeout(l[i]),c[i]=setTimeout(function(){a.hasClass(r.NAV_TREE)||s.css({width:0,left:s.position().left+s.width()/2,opacity:0})},200)}),e.find("a").each(function(){var i=_(this),a="click.lay_nav_click";i.siblings("."+r.NAV_CHILD)[0]&&!i.children("."+r.NAV_MORE)[0]&&i.append(''),i.off(a,n.clickThis).on(a,n.clickThis)})})}}),n={clickThis:function(){var i=_(this),a=i.closest(r.NAV_ELEM),s=a.attr("lay-filter"),e=i.parent(),t=i.siblings("."+r.NAV_CHILD),n="string"==typeof e.attr("lay-unselect");if("javascript:;"!==i.attr("href")&&"_blank"===i.attr("target")||n||t[0]||(a.find("."+r.CLASS_THIS).removeClass(r.CLASS_THIS),e.addClass(r.CLASS_THIS)),a.hasClass(r.NAV_TREE)){var n=r.NAV_ITEM+"ed",l=!e.hasClass(n),o=function(){_(this).css({display:""}),a.children("."+r.NAV_BAR).css({opacity:0})};if(t.is(":animated"))return;t.removeClass(r.NAV_ANIM),t[0]&&(l?(t.slideDown(200,o),e.addClass(n)):(e.removeClass(n),t.show().slideUp(200,o)),"string"!=typeof a.attr("lay-accordion")&&"all"!==a.attr("lay-shrink")||((l=e.siblings("."+n)).removeClass(n),l.children("."+r.NAV_CHILD).show().stop().slideUp(200,o)))}layui.event.call(this,"element","nav("+s+")",i)}},r=a.CONST;i(r.MOD_NAME,a)});layui.define("component",function(n){"use strict";var t=layui.$,i=layui.component({name:"breadcrumb",config:{elem:".layui-breadcrumb"},render:function(){this.config.elem.each(function(){var n=t(this),i="lay-separator",e=n.attr(i)||"/",a=n.find("a");a.next("span["+i+"]")[0]||(a.each(function(n){n!==a.length-1&&t(this).after(""+e+"")}),n.css("visibility","visible"))})}});n(i.CONST.MOD_NAME,i)});layui.define("component",function(t){"use strict";var r=layui.$,e=layui.component({name:"progress",config:{elem:".layui-progress"},CONST:{ELEM:"layui-progress"},render:function(){this.config.elem.each(function(){var t=r(this),e=t.find(".layui-progress-bar"),n=e.attr("lay-percent");e.css("width",function(){return/^.+\/.+$/.test(n)?100*new Function("return "+n)()+"%":n}),t.attr("lay-showpercent")&&setTimeout(function(){e.html(''+n+"")},350)})}}),i=e.CONST;r.extend(e,{setValue:function(t,e){var n="layui-progress",t=r("."+n+"[lay-filter="+t+"]").find("."+n+"-bar"),n=t.find("."+n+"-text");return t.css("width",function(){return/^.+\/.+$/.test(e)?100*new Function("return "+e)()+"%":e}).attr("lay-percent",e),n.text(e),this}}),t(i.MOD_NAME,e)});layui.define("component",function(l){"use strict";var t=layui.$,i=layui.component({name:"collapse",config:{elem:".layui-collapse"},render:function(){this.config.elem.each(function(){t(this).find(".layui-colla-item").each(function(){var l=t(this),i=l.find(".layui-colla-title"),a=l.find(".layui-colla-content"),e="none"===a.css("display"),s="click.lay_collapse_click";i.find(".layui-colla-icon").remove(),i.append(''),l[e?"removeClass":"addClass"](u.CLASS_SHOW),a.hasClass(u.CLASS_SHOW)&&a.removeClass(u.CLASS_SHOW),i.off(s,n.titleClick).on(s,n.titleClick)})})}}),n={titleClick:function(){var l=t(this),i=l.closest(".layui-collapse"),a=i.attr("lay-filter"),e=".layui-colla-content",s=l.parent(".layui-colla-item"),n=l.siblings(e),c="none"===n.css("display"),i="string"==typeof i.attr("lay-accordion"),o=function(){t(this).css("display","")};n.is(":animated")||(c?(n.slideDown(200,o),s.addClass(u.CLASS_SHOW)):(s.removeClass(u.CLASS_SHOW),n.show().slideUp(200,o)),i&&((i=s.siblings("."+u.CLASS_SHOW)).removeClass(u.CLASS_SHOW),i.children(e).show().slideUp(200,o)),layui.event.call(this,"element","collapse("+a+")",{title:l,content:n,show:c}))}},u=i.CONST;l(u.MOD_NAME,i)});layui.define(["component","tab","nav","breadcrumb","progress","collapse"],function(e){"use strict";var n=layui.$,a=layui.tab,r=layui.progress,t=layui.component({name:"element",CONST:{MOD_NAME:"element"}}),l=t.CONST;n.extend(t,{render:function(e,a){var r="string"==typeof a&&a?'[lay-filter="'+a+'"]':"",t={tab:".layui-tab"+r,nav:".layui-nav"+r,breadcrumb:".layui-breadcrumb"+r,progress:".layui-progress"+r,collapse:".layui-collapse"+r};if(!e||t[e])return e&&"object"==typeof a&&a instanceof n?layui[e].render({elem:a}):t[e]?layui[e].render({elem:t[e]}):layui.each(t,function(e){layui[e].render({elem:t[e]})})},tabAdd:a.tabAdd,tabDelete:a.tabDelete,tabChange:a.tabChange,tab:a.tab,progress:r.setValue}),t.init=t.render,n(function(){t.render()}),e(l.MOD_NAME,t)});layui.define(["lay","i18n","layer"],function(e){"use strict";var F=layui.$,a=layui.lay,i=layui.layer,R=layui.i18n,T=layui.device(),w=layui.hint(),t="upload",f="layui_"+t+"_index",L={config:{},index:layui[t]?layui[t].index+1e4:0,set:function(e){var i=this;return i.config=F.extend({},i.config,e),i},on:function(e,i){return layui.onevent.call(this,t,e,i)}},o=function(){var i=this,e=i.config.id;return{upload:function(e){i.upload.call(i,e)},reload:function(e){i.reload.call(i,e)},config:(o.that[e]=i).config}},l="layui-upload-file",r="layui-upload-form",E="layui-upload-iframe",O="layui-upload-choose",D="UPLOADING",M=function(e){var i=this;i.index=++L.index,i.config=F.extend({},i.config,L.config,e),i.render()};M.prototype.config={accept:"images",exts:"",auto:!0,bindAction:"",url:"",force:"",field:"file",acceptMime:"",method:"post",data:{},drag:!0,size:0,number:0,multiple:!1,text:{"cross-domain":"Cross-domain requests are not supported","data-format-error":"Please return JSON data format","check-error":"",error:"","limit-number":null,"limit-size":null}},M.prototype.reload=function(e){var i=this;i.config=F.extend({},i.config,e),i.render(!0)},M.prototype.render=function(e){var i=this,t=i.config,n=F(t.elem);return 1"].join("")),n=i.elem.next();(n.hasClass(l)||n.hasClass(r))&&n.remove(),T.ie&&T.ie<10&&i.elem.wrap('
          '),e.isFile()?(e.elemFile=i.elem,i.field=i.elem[0].name):i.elem.after(t),T.ie&&T.ie<10&&e.initIE()},M.prototype.initIE=function(){var t,e=this.config,i=F(''),n=F(['
          ',""].join(""));F("#"+E)[0]||F("body").append(i),e.elem.next().hasClass(r)||(this.elemFile.wrap(n),e.elem.next("."+r).append((t=[],layui.each(e.data,function(e,i){i="function"==typeof i?i():i,t.push('')}),t.join(""))))},M.prototype.msg=function(e){return i.msg(e,{icon:2,shift:6})},M.prototype.isFile=function(){var e=this.config.elem[0];if(e)return"input"===e.tagName.toLocaleLowerCase()&&"file"===e.type},M.prototype.preview=function(n){window.FileReader&&layui.each(this.chooseFiles,function(e,i){var t=new FileReader;t.readAsDataURL(i),t.onload=function(){n&&n(e,i,this.result)}})},M.prototype.upload=function(e,i){var t,n,a,o,l,u=this,s=u.config,f=s.text||{},r=u.elemFile[0],c=function(){return e||u.files||u.chooseFiles||r.files},d=function(){var a=0,o=0,l=c(),r=function(){s.multiple&&a+o===u.fileLength&&"function"==typeof s.allDone&&s.allDone({total:u.fileLength,successful:a,failed:o})},t=function(t){var n=new FormData,i=function(e){t.unified?layui.each(l,function(e,i){delete i[D]}):delete e[D]};if(layui.each(s.data,function(e,i){i="function"==typeof i?t.unified?i():i(t.index,t.file):i,n.append(e,i)}),t.unified)layui.each(l,function(e,i){i[D]||(i[D]=!0,n.append(s.field,i))});else{if(t.file[D])return;n.append(s.field,t.file),t.file[D]=!0}var e={url:s.url,type:"post",data:n,dataType:s.dataType||"json",contentType:!1,processData:!1,headers:s.headers||{},success:function(e){s.unified?a+=u.fileLength:a++,m(t.index,e),r(t.index),i(t.file)},error:function(e){s.unified?o+=u.fileLength:o++,u.msg(f.error||["Upload failed, please try again.","status: "+(e.status||"")+" - "+(e.statusText||"error")].join("
          ")),g(t.index,e.responseText,e),r(t.index),i(t.file)}};"function"==typeof s.progress&&(e.xhr=function(){var e=F.ajaxSettings.xhr();return e.upload.addEventListener("progress",function(e){var i;e.lengthComputable&&(i=Math.floor(e.loaded/e.total*100),s.progress(i,(s.item||s.elem)[0],e,t.index))}),e}),F.ajax(e)};s.unified?t({unified:!0,index:0}):layui.each(l,function(e,i){t({index:e,file:i})})},p=function(){var n=F("#"+E);u.elemFile.parent().submit(),clearInterval(M.timer),M.timer=setInterval(function(){var e,i=n.contents().find("body");try{e=i.text()}catch(t){u.msg(f["cross-domain"]),clearInterval(M.timer),g()}e&&(clearInterval(M.timer),i.html(""),m(0,e))},30)},h=function(e){if("json"===s.force&&"object"!=typeof e)try{return{status:"CONVERTED",data:JSON.parse(e)}}catch(i){return u.msg(f["data-format-error"]),{status:"FORMAT_ERROR",data:{}}}return{status:"DO_NOTHING",data:{}}},m=function(e,i){u.elemFile.next("."+O).remove(),r.value="";var t=h(i);switch(t.status){case"CONVERTED":i=t.data;break;case"FORMAT_ERROR":return}"function"==typeof s.done&&s.done(i,e||0,function(e){u.upload(e)})},g=function(e,i,t){s.auto&&(r.value="");var n=h(i);switch(n.status){case"CONVERTED":i=n.data;break;case"FORMAT_ERROR":return}"function"==typeof s.error&&s.error(e||0,function(e){u.upload(e)},i,t)},v=s.exts,y=(n=[],layui.each(e||u.chooseFiles,function(e,i){n.push(i.name)}),n),x={preview:function(e){u.preview(e)},upload:function(e,i){var t={};t[e]=i,u.upload(t)},pushFile:function(){return u.files=u.files||{},layui.each(u.chooseFiles,function(e,i){u.files[e]=i}),u.files},resetFile:function(e,i,t){i=new File([i],t);u.files=u.files||{},u.files[e]=i},getChooseFiles:function(){return u.chooseFiles}},b={file:R.$t("upload.fileType.file"),images:R.$t("upload.fileType.image"),video:R.$t("upload.fileType.video"),audio:R.$t("upload.fileType.audio")}[s.accept]||R.$t("upload.fileType.file"),y=0===y.length?r.value.match(/[^/\\]+\..+/g)||[]:y;if(0!==y.length){switch(s.accept){case"file":layui.each(y,function(e,i){if(v&&!RegExp(".\\.("+v+")$","i").test(escape(i)))return t=!0});break;case"video":layui.each(y,function(e,i){if(!RegExp(".\\.("+(v||"avi|mp4|wma|rmvb|rm|flash|3gp|flv")+")$","i").test(escape(i)))return t=!0});break;case"audio":layui.each(y,function(e,i){if(!RegExp(".\\.("+(v||"mp3|wav|mid")+")$","i").test(escape(i)))return t=!0});break;default:layui.each(y,function(e,i){if(!RegExp(".\\.("+(v||"jpg|png|gif|bmp|jpeg|svg|webp")+")$","i").test(escape(i)))return t=!0})}if(t)return u.msg(f["check-error"]||R.$t("upload.validateMessages.fileExtensionError",{fileType:b})),r.value="";if("choose"!==i&&!s.auto||(s.choose&&s.choose(x),"choose"!==i)){if(u.fileLength=(a=0,b=c(),layui.each(b,function(){a++}),a),s.number&&u.fileLength>s.number)return u.msg("function"==typeof f["limit-number"]?f["limit-number"](s,u.fileLength):R.$t("upload.validateMessages.filesOverLengthLimit",{length:s.number})+"
          "+R.$t("upload.validateMessages.currentFilesLength",{length:u.fileLength}));if(01024*s.size&&(i=1<=(i=s.size/1024)?i.toFixed(2)+"MB":s.size+"KB",r.value="",o=i)}),o)return u.msg("function"==typeof f["limit-size"]?f["limit-size"](s,o):R.$t("upload.validateMessages.fileOverSizeLimit",{size:o}));l=function(){if(T.ie)return(9'+e+"")},r=function(t){var n=!0;return layui.each(a.files,function(e,i){if(!(n=!(i.name===t.name)))return!0}),n},u=function(e){var t=function(e){e.ext=e.name.substr(e.name.lastIndexOf(".")+1).toLowerCase(),e.sizes=L.util.parseSize(e.size)};return e instanceof FileList?layui.each(e,function(e,i){t(i)}):t(e),e},s=function(e){var t;return(e=e||[]).length?a.files?(t=[],layui.each(e,function(e,i){r(i)&&t.push(u(i))}),t):u(e):[]};n.elem.off("upload.start").on("upload.start",function(){var e=F(this);a.config.item=e,a.elemFile[0].click()}),T.ie&&T.ie<10||n.elem.off("upload.over").on("upload.over",function(){F(this).attr("lay-over","")}).off("upload.leave").on("upload.leave",function(){F(this).removeAttr("lay-over")}).off("upload.drop").on("upload.drop",function(e,i){var t=F(this),i=s(i.originalEvent.dataTransfer.files);t.removeAttr("lay-over"),o(i),n.auto?a.upload():l(i)}),a.elemFile.on("change",function(){var e=s(this.files);0!==e.length&&(o(e),n.auto?a.upload():l(e))}),n.bindAction.off("upload.action").on("upload.action",function(){a.upload()}),n.elem.data(f)||(n.elem.on("click",function(){a.isFile()||F(this).trigger("upload.start")}),n.drag&&n.elem.on("dragover",function(e){e.preventDefault(),F(this).trigger("upload.over")}).on("dragleave",function(e){F(this).trigger("upload.leave")}).on("drop",function(e){e.preventDefault(),F(this).trigger("upload.drop",e)}),n.bindAction.on("click",function(){F(this).trigger("upload.action")}),n.elem.data(f,n.id))},L.util={parseSize:function(e,i){var t,n;return i=i||2,null!=e&&e?(t="string"==typeof e?parseFloat(e):e,n=Math.floor(Math.log(t)/Math.log(1024)),(e=(e=t/Math.pow(1024,n))%1==0?e:parseFloat(e.toFixed(i)))+["Bytes","Kb","Mb","Gb","Tb","Pb","Eb","Zb","Yb"][n]):"0"},promiseLikeResolve:function(e){var i=F.Deferred();return e&&"function"==typeof e.then?e.then(i.resolve,i.reject):i.resolve(e),i.promise()}},o.that={},o.getThis=function(e){var i=o.that[e];return i||w.error(e?t+" instance with ID '"+e+"' not found":"ID argument required"),i},L.render=function(e){e=new M(e);return o.call(e)},e(t,L)});layui.define(["lay","i18n","layer","util"],function(e){"use strict";var _=layui.$,h=layui.layer,p=layui.util,O=layui.lay,l=layui.hint(),$=layui.i18n,T="form",f=".layui-form",M="layui-this",E="layui-hide",A="layui-disabled",y="layui-input-number-invalid",I=O.createSharedResizeObserver(T),v=O.ie&&8===parseFloat(O.ie)||void 0===Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,"checked"),t=function(){this.config={verify:{required:function(e){if(!/[\S]+/.test(e)||e===undefined||null===e)return $.$t("form.validateMessages.required")},phone:function(e){if(e&&!/^1\d{10}$/.test(e))return $.$t("form.validateMessages.phone")},email:function(e){if(e&&!/^([a-zA-Z0-9_.-])+@(([a-zA-Z0-9-])+\.)+([a-zA-Z0-9]{2,4})+$/.test(e))return $.$t("form.validateMessages.email")},url:function(e){if(e&&!/^(#|(http(s?)):\/\/|\/\/)[^\s]+\.[^\s]+$/.test(e))return $.$t("form.validateMessages.url")},number:function(e){if(e&&isNaN(e))return $.$t("form.validateMessages.number")},date:function(e){if(e&&!/^(\d{4})[-/](\d{1}|0\d{1}|1[0-2])([-/](\d{1}|0\d{1}|[1-2][0-9]|3[0-1]))*$/.test(e))return $.$t("form.validateMessages.date")},identity:function(e){if(e&&!/(^\d{15}$)|(^\d{17}(x|X|\d)$)/.test(e))return $.$t("form.validateMessages.identity")}},autocomplete:null}},i=(t.prototype.set=function(e){return _.extend(!0,this.config,e),this},t.prototype.verify=function(e){return _.extend(!0,this.config.verify,e),this},t.prototype.getFormElem=function(e){return _(f+(e?'[lay-filter="'+e+'"]':""))},t.prototype.on=function(e,t){return layui.onevent.call(this,T,e,t)},t.prototype.val=function(e,o){return this.getFormElem(e).each(function(e,t){var i,a,n,l,r=_(this);for(i in o)O.hasOwn(o,i)&&(n=o[i],(l=r.find('[name="'+i+'"]'))[0])&&("checkbox"===(a=l[0].type)?l[0].checked=n:"radio"===a?l.each(function(){this.checked=this.value==n+""}):l.val(n))}),r.render(null,e),this.getValue(e)},t.prototype.getValue=function(e,t){t=t||this.getFormElem(e);var n={},l={},e=t.find("input,select,textarea");return layui.each(e,function(e,t){var i,a=_(this);t.name=(t.name||"").replace(/^\s*|\s*&/,""),t.name&&(/^.*\[\]$/.test(t.name)&&(i=t.name.match(/^(.*)\[\]$/g)[0],n[i]=0|n[i],i=t.name.replace(/^(.*)\[\]$/,"$1["+n[i]+++"]")),/^(checkbox|radio)$/.test(t.type)&&!t.checked||(l[i||t.name]="SELECT"===this.tagName&&"string"==typeof this.getAttribute("multiple")?a.val()||[]:this.value))}),l},t.prototype.render=function(e,t){var d=this,i=d.config,a=_(f+(t?'[lay-filter="'+t+'"]':"")),n={input:function(e){var e=e||a.find("input,textarea"),h=(i.autocomplete&&e.attr("autocomplete",i.autocomplete),function(e,t){var i=e.val(),a=Number(i),n=Number(e.attr("step"))||1,l=Number(e.attr("min")),r=Number(e.attr("max")),o=Number(e.attr("lay-precision")),s="click"!==t&&""===i,c="init"===t,u=isNaN(a),d="string"==typeof e.attr("lay-step-strictly");if(e.toggleClass(y,u),!u){if("click"===t){if("text"===e[0].type&&"string"==typeof e.attr("readonly"))return;a=!!_(this).index()?a-n:a+n}u=function(e){return((e.toString().match(/\.(\d+$)/)||[])[1]||"").length},o=0<=o?o:Math.max(u(n),u(i));s||(c||r<=(a=(a=d?Math.round(a/n)*n:a)<=l?l:a)&&(a=r),0===o?a=parseInt(a):0'),e=layui.isArray(i.value)?i.value:[i.value],e=_((a=[],layui.each(e,function(e,t){a.push('')}),a.join(""))),n=(t.append(e),i.split&&t.addClass("layui-input-split"),i.className&&t.addClass(i.className),r.next("."+u)),l=(n[0]&&n.remove(),r.parent().hasClass(s)||r.wrap('
          '),r.next("."+c));l[0]?((n=l.find("."+u))[0]&&n.remove(),l.prepend(t),r.css("padding-right",function(){return(r.closest(".layui-input-group")[0]?0:l.outerWidth())+t.outerWidth()})):(t.addClass(c),r.after(t)),"auto"===i.show&&d(t,r.val()),"function"==typeof i.init&&i.init.call(this,r,i),r.on("input propertychange",function(){var e=this.value;"auto"===i.show&&d(t,e)}),r.on("blur",function(){"function"==typeof i.blur&&i.blur.call(this,r,i)}),e.on("click",function(){var e=r.attr("lay-filter");_(this).hasClass(A)||("function"==typeof i.click&&i.click.call(this,r,i),layui.event.call(this,T,"input-affix("+e+")",{elem:r[0],affix:o,options:i}))})},p={eye:{value:"eye-invisible",click:function(e,t){var i="LAY_FORM_INPUT_AFFIX_SHOW",a=e.data(i);e.attr("type",a?"password":"text").data(i,!a),n({value:a?"eye-invisible":"eye"})}},clear:{value:"clear",click:function(e){e.val("").focus(),d(_(this).parent(),null)},show:"auto",disabled:e},number:{value:["up","down"],split:!0,className:"layui-input-number",disabled:r.is("[disabled]"),init:function(a){var e,n,l,t,i,r;"text"!==a.attr("type")&&"text"!==a[0].type||(l=n=!(e=".lay_input_number"),t="string"==typeof a.attr("readonly"),i="string"==typeof a.attr("lay-wheel"),r=a.next(".layui-input-number").children("i"),a.attr("lay-input-mirror",a.val()),a.off(e),a.on("keydown"+e,function(e){n=!1,8!==e.keyCode&&46!==e.keyCode||(n=!0),t||2!==r.length||38!==e.keyCode&&40!==e.keyCode||(e.preventDefault(),r.eq(38===e.keyCode?0:1).click())}),a.on("input"+e+" propertychange"+e,function(e){var t,i;l||"propertychange"===e.type&&"value"!==e.originalEvent.propertyName||(n||""===(e=this.value)||"00"!==e.slice(0,2)&&!e.match(/\s/g)&&!((t=e.match(/\./g))&&1=Math.abs(e.deltaY)?e.deltaX:e.deltaY):"mousewheel"===e.type?t=-e.originalEvent.wheelDelta:"DOMMouseScroll"===e.type&&(t=e.originalEvent.detail),r.eq(0S.height()&&t<=e&&l.addClass(x+"up"),h(),s&&g.off("mousedown.lay_select_ieph").on("mousedown.lay_select_ieph",function(){m[0].__ieph=!0,setTimeout(function(){m[0].__ieph=!1},60)}),n=O.onClickOutside((a?l:g)[0],function(){p(),k&&m.val(k)},{ignore:v,detectIframe:!0,capture:!1})},p=function(e){v.parent().removeClass(x+"ed "+x+"up"),m.blur(),u&&g.children("."+N).remove(),"function"==typeof n&&(n(),n=null),a&&(l.detach(),_(window).off("resize.lay_select_resize"),I)&&I.unobserve(l[0]),e||f(m.val(),function(e){var t=y[0].selectedIndex;e&&(k=_(y[0].options[t]).prop("text"),0===t&&k===m.attr("placeholder")&&(k=""),m.val(k||""))})},h=function(){var e,t,i=g.children("dd."+M);i[0]&&(e=i.position().top,t=g.height(),i=i.height(),t").addClass(N).attr("lay-value",n).text(n),a=(i=g.children().eq(0)).hasClass("layui-select-tips"),i[a?"after":"before"](t)):e?g.find("."+w)[0]||g.append('

          '+$.$t("form.select.noMatch")+"

          "):g.find("."+w).remove()},"keyup"),""===n&&(y.val(""),g.find("."+M).removeClass(M),(y[0].options[0]||{}).value||g.children("dd:eq(0)").addClass(M),g.find("."+w).remove(),u)&&g.children("."+N).remove(),void h()))},50)).on("blur",function(e){var t=y[0].selectedIndex;k=_(y[0].options[t]).prop("text"),0===t&&k===m.attr("placeholder")&&(k=""),setTimeout(function(){f(m.val(),function(e){k||m.val("")},"blur")},200)}),g.on("click","dd",function(){var e,t,i=_(this),a=i.attr("lay-value"),n=y.attr("lay-filter");return i.hasClass(A)||(u&&i.hasClass(N)&&(t=(e=_("
          "].join(""));i.after(l),function(i,a){var n=_(this),e=n.attr("lay-skin")||"primary",t="switch"===e,e="primary"===e;n.off(u).on(u,function(e){var t=n.attr("lay-filter");n[0].disabled||(n[0].indeterminate=!!n[0].indeterminate,n[0].checked=!!n[0].checked,layui.event.call(n[0],T,a[2]+"("+t+")",{elem:n[0],value:n[0].value,othis:i}))}),i.on("click",function(){n.closest("label").length||n.trigger("click")}),d.syncAppearanceOnPropChanged(this,"checked",function(){var e;t&&(e=(i.next("*[lay-checkbox]")[0]?i.next().html():n.attr("title")||"").split("|"),i.children("div").html(!this.checked&&e[1]||e[0])),i.toggleClass(a[1],this.checked)}),e&&d.syncAppearanceOnPropChanged(this,"indeterminate",function(){this.indeterminate?i.children("."+c.ICON).removeClass(c.ICON_OK).addClass(c.SUBTRA):i.children("."+c.ICON).removeClass(c.SUBTRA).addClass(c.ICON_OK)})}.call(this,l,r)})},radio:function(e){var s="layui-form-radio",c=["layui-icon-radio","layui-icon-circle"],e=e||a.find("input[type=radio]"),u="click.lay_radio_click";e.each(function(e,t){var i=_(this),a=i.next("."+s),n=this.disabled,l=i.attr("lay-skin");if(i.closest("[lay-ignore]").length)return i.show();v&&m.call(t,"lay-form-sync-checked",t.checked),a[0]&&a.remove();var a=p.escape(t.title||""),r=[],o=(i.next("[lay-radio]")[0]&&(a=(o=i.next()).html()||"",1",'',"
          "+a+"
          ",""].join("")));i.after(o),function(i){var a=_(this),n="layui-anim-scaleSpring";a.off(u).on(u,function(){var e=a.attr("lay-filter");a[0].disabled||(a[0].checked=!0,layui.event.call(a[0],T,"radio("+e+")",{elem:a[0],value:a[0].value,othis:i}))}),i.on("click",function(){a.closest("label").length||a.trigger("click")}),d.syncAppearanceOnPropChanged(this,"checked",function(){var e,t=this;t.checked?(i.addClass(s+"ed"),i.children(".layui-icon").addClass(n+" "+c[0]),e=a.parents(f).find("input[name="+t.name.replace(/(\.|#|\[|\])/g,"\\$1")+"]"),layui.each(e,function(){t!==this&&(this.checked=!1)})):(i.removeClass(s+"ed"),i.children(".layui-icon").removeClass(n+" "+c[0]).addClass(c[1]))})}.call(this,o)})}},t=function(){layui.each(n,function(e,t){t()})};return"object"===layui.type(e)?_(e).is(f)?(a=_(e),t()):e.each(function(e,t){var i=_(t);i.closest(f).length&&("SELECT"===t.tagName?n.select(i):"INPUT"===t.tagName&&("checkbox"===(t=t.type)||"radio"===t?n[t](i):n.input(i)))}):e?n[e]?n[e]():l.error('[form] "'+e+'" is an unsupported form element type'):t(),d},t.prototype.syncAppearanceOnPropChanged=v?function(e,t,i){var a=Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,t);Object.defineProperty(e,t,O.extend({},a,{get:function(){return"string"==typeof this.getAttribute("lay-form-sync-"+t)},set:function(e){m.call(this,"lay-form-sync-"+t,e),i.call(this)}}))}:function(e,t,i){var a=Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,t);Object.defineProperty(e,t,O.extend({},a,{get:function(){return a.get.call(this)},set:function(e){a.set.call(this,e),i.call(this)}}))},t.prototype.validate=function(e){var u,d=this.config.verify,p="layui-form-danger";return!(e=_(e))[0]||(e.attr("lay-verify")!==undefined||!1!==this.validate(e.find("*[lay-verify]")))&&(layui.each(e,function(e,r){var o=_(this),t=(o.attr("lay-verify")||"").split("|"),s=o.attr("lay-vertype"),c="string"==typeof(c=o.val())?_.trim(c):c;if(o.removeClass(p),layui.each(t,function(e,t){var i="",a=d[t];if(a){var n="function"==typeof a?i=a(c,r):!a[0].test(c),l="select"===r.tagName.toLowerCase()||/^(checkbox|radio)$/.test(r.type),i=i||a[1];if("required"===t&&(i=o.attr("lay-reqtext")||i),n)return"tips"===s?h.tips(i,!o.closest("[lay-ignore]").length&&l?o.next():o,{tips:1}):"alert"===s?h.alert(i,{title:$.$t("form.verifyErrorPromptTitle"),shadeClose:!0}):/\b(string|number)\b/.test(typeof i)&&h.msg(i,{icon:5,shift:6}),setTimeout(function(){(l?o.next().find("input"):r).focus()},7),o.addClass(p),u=!0}}),u)return u}),!u)},t.prototype.submit=function(e,t){var i={},a=_(this),e="string"==typeof e?e:a.attr("lay-filter"),n=this.getFormElem?this.getFormElem(e):a.parents(f).eq(0),l=n.find("*[lay-verify]");return!!r.validate(l)&&(i=r.getValue(null,n),l={elem:this.getFormElem?window.event&&window.event.target:this,form:(this.getFormElem?n:a.parents("form"))[0],field:i},"function"==typeof t&&t(l),layui.event.call(this,T,"submit("+e+")",l))});function m(e,t){var i=!!t,t=2===arguments.length&&!t;return null!==this.getAttribute(e)?i||(this.removeAttribute(e),!1):!t&&(this.setAttribute(e,""),!0)}var g=["-",".","e","E","+"];var r=new t,t=_(document),S=_(window);_(function(){r.render()}),t.on("reset",f,function(){var e=_(this).attr("lay-filter");setTimeout(function(){r.render(null,e)},50)}),t.on("submit",f,i).on("click","*[lay-submit]",i),e(T,r)});layui.define(["lay","i18n","laytpl","laypage","form","util"],function(c){"use strict";var f=layui.$,d=layui.lay,m=layui.laytpl,p=layui.laypage,g=layui.layer,i=layui.form,v=layui.util,b=layui.hint(),x=layui.device(),s=layui.i18n,w={config:{checkName:"LAY_CHECKED",indexName:"LAY_INDEX",initIndexName:"LAY_INDEX_INIT",numbersName:"LAY_NUM",disabledName:"LAY_DISABLED"},cache:{},index:layui.table?layui.table.index+1e4:0,set:function(e){return this.config=f.extend({},this.config,e),this},on:function(e,t){return layui.onevent.call(this,R,e,t)}},k=function(){var a=this,e=a.config,i=e.id||e.index;return{config:e,reload:function(e,t){a.reload.call(a,e,t)},reloadData:function(e,t){w.reloadData(i,e,t)},setColsWidth:function(){a.setColsWidth.call(a)},resize:function(){a.resize.call(a)}}},C=function(e){var t=k.that[e];return t||b.error(e?"The table instance with ID '"+e+"' not found":"ID argument required"),t||null},l=function(e){var t=k.config[e];return t||b.error(e?"The table instance with ID '"+e+"' not found":"ID argument required"),t||null},T=function(e){var t=this.config||{},a=(e=e||{}).item3,i=e.content;"numbers"===a.type&&(i=e.tplData[w.config.numbersName]);("escape"in a?a:t).escape&&(i=v.escape(i));t=e.text&&a.exportTemplet||a.templet||a.toolbar;return t&&(i="function"==typeof t?t.call(a,e.tplData,e.obj):m(function(e){try{return d(e).html()}catch(t){return e}}(t)||String(i)).render(f.extend({LAY_COL:a},e.tplData))),e.text?f("
          "+i+"
          ").text():i},R="table",_="lay-"+R+"-id",u=".layui-table",N="layui-hide",h="layui-hide-v",y="layui-none",F="layui-table-view",P=".layui-table-header",W=".layui-table-body",I=".layui-table-fixed",O=".layui-table-fixed-r",B=".layui-table-pageview",D=".layui-table-sort",S="layui-table-checked",H="layui-table-edit",E="layui-table-hover",K="laytable-cell-group",L="layui-table-col-special",M="layui-table-tool-panel",A="layui-table-expanded",G="layui-table-disabled-transition",r="layui-table-fixed-height-patch",z="LAY_TABLE_MOVE_DICT",n=d.createSharedResizeObserver(R),e=function(e){var t=["{{# var colspan = layui.type(item2.colspan2) === 'number' ? item2.colspan2 : item2.colspan; }}",'{{# if(colspan){ }} colspan="{{=colspan}}"{{# } }}','{{# if(item2.rowspan){ }} rowspan="{{=item2.rowspan}}"{{# } }}'].join("");return['
          "," "," {{# layui.each(d.data.cols, function(i1, item1){ }}"," "," {{# layui.each(item1, function(i2, item2){ }}",' {{# if(item2.fixed && item2.fixed !== "right"){ left = true; } }}',' {{# if(item2.fixed === "right"){ right = true; } }}',(e=e||{}).fixed&&"right"!==e.fixed?'{{# if(item2.fixed && item2.fixed !== "right"){ }}':"right"===e.fixed?'{{# if(item2.fixed === "right"){ }}':""," {{# var isSort = !(item2.colGroup) && item2.sort; }}",' ",e.fixed?" {{# } }}":""," {{# }); }}"," "," {{# }); }}"," ","
          ' + item2.title + '').text() }}\"{{# } }}>",'
          ',' {{# if(item2.type === "checkbox"){ }}',' "," {{# } else { }}",' {{- item2.title || "" }}'," {{# if(isSort){ }}",' ',' ',' '," "," {{# } }}"," {{# } }}","
          ","
          "].join("\n")},t=['"," ","
          "].join("\n"),Y=["{{# if(d.data.toolbar){ }}",'
          ','
          ','
          ',"
          ","{{# } }}","",'
          '," {{# if(d.data.loading){ }}",'
          ','
          ',' {{# if(typeof d.data.loading === "string"){ }}'," {{- d.data.loading }}"," {{# } else { }}",' '," {{# } }}","
          ","
          "," {{# } }}",""," {{# var left, right; }}",""," \x3c!-- \u8868\u5934\u533a\u57df --\x3e",'
          ',e(),"
          ",""," \x3c!-- \u8868\u4f53\u533a\u57df --\x3e",'
          ',t,"
          ",""," {{# if(left){ }}"," \x3c!-- \u5de6\u56fa\u5b9a\u5217 --\x3e",'
          ','
          ',e({fixed:!0}),"
          ",'
          ',t,"
          ","
          "," {{# } }}",""," {{# if(right){ }}"," \x3c!-- \u53f3\u56fa\u5b9a\u5217 --\x3e",'
          ','
          ',e({fixed:"right"}),'
          ',"
          ",'
          ',t,"
          ","
          "," {{# } }}","","
          ","","{{# if(d.data.totalRow){ }}",'
          ',' "," "," "," "," "," ","
          ",' ',"
          ","
          ","{{# } }}","",'
          ','
          ',"
          "].join("\n"),o=f(window),j=f(document),a=function(e){var t=this;t.index=++w.index,t.config=f.extend({},t.config,w.config,e),t.unobserveResize=f.noop,t.render()},$=(a.prototype.config={limit:10,loading:!0,escape:!0,cellMinWidth:60,cellMaxWidth:Number.MAX_VALUE,editTrigger:"click",defaultToolbar:["filter","exports","print"],defaultContextmenu:!0,autoSort:!0,cols:[]},a.prototype.render=function(e){var t=this,a=t.config,i=(a.elem=f(a.elem),a.where=a.where||{},a.id="id"in a?a.id:a.elem.attr("id")||t.index);if(k.that[i]&&k.that[i]!==t&&k.that[i].dispose(),k.that[i]=t,(k.config[i]=a).request=f.extend({pageName:"page",limitName:"limit"},a.request),a.response=f.extend({statusName:"code",statusCode:0,msgName:"msg",dataName:"data",totalRowName:"totalRow",countName:"count"},a.response),null!==a.page&&"object"==typeof a.page&&(a.limit=a.page.limit||a.limit,a.limits=a.page.limits||a.limits,t.page=a.page.curr=a.page.curr||1,delete a.page.elem,delete a.page.jump),a.text=f.extend(!0,{none:s.$t("table.noData")},a.text),!a.elem[0])return t;if(a.elem.attr("lay-filter")||a.elem.attr("lay-filter",a.id),"reloadData"===e)return t.pullData(t.page,{type:"reloadData"});a.index=t.index,t.key=a.id||a.index,t.setInit(),a.height&&/^full-.+$/.test(a.height)?(t.fullHeightGap=a.height.split("-")[1],a.height=o.height()-(parseFloat(t.fullHeightGap)||0)):a.height&&/^#\w+\S*-.+$/.test(a.height)?(i=a.height.split("-"),t.parentHeightGap=i.pop(),t.parentDiv=i.join("-"),a.height=f(t.parentDiv).height()-(parseFloat(t.parentHeightGap)||0)):"function"==typeof a.height&&(t.customHeightFunc=a.height,a.height=t.customHeightFunc());var l,e=a.elem,i=e.next("."+F),n=t.elem=f("
          ");n.addClass((l=[F,F+"-"+t.index,"layui-form","layui-border-box"],a.className&&l.push(a.className),l.join(" "))).attr(((l={"lay-filter":"LAY-TABLE-FORM-DF-"+t.index,style:(l=[],a.width&&l.push("width:"+a.width+"px;"),l.join(""))})[_]=a.id,l)).html(m(Y,{open:"{{",close:"}}",tagStyle:"legacy"}).render({data:a,index:t.index,i18nMessages:{table_sort_asc:s.$t("table.sort.asc"),table_sort_desc:s.$t("table.sort.desc")}})),t.renderStyle(),i[0]&&i.remove(),e.after(n),t.layTool=n.find(".layui-table-tool"),t.layBox=n.find(".layui-table-box"),t.layHeader=n.find(P),t.layMain=n.find(".layui-table-main"),t.layBody=n.find(W),t.layFixed=n.find(I),t.layFixLeft=n.find(".layui-table-fixed-l"),t.layFixRight=n.find(O),t.layTotal=n.find(".layui-table-total"),t.layPage=n.find(".layui-table-page"),t.renderToolbar(),t.renderPagebar(),t.fullSize(),t.setColsWidth({isInit:!0}),t.pullData(t.page,{done:function(){t.observeResize()}}),t.events()},a.prototype.initOpts=function(e){e.checkbox&&(e.type="checkbox"),e.space&&(e.type="space"),e.type||(e.type="normal"),"normal"!==e.type&&(e.unresize=!0,e.width=e.width||{checkbox:50,radio:50,space:30,numbers:60}[e.type])},a.prototype.setInit=function(e){var n,a,r=this,d=r.config;if(d.clientWidth=d.width||(n=function(e){var t,a;e=e||d.elem.parent(),t=r.getContentWidth(e);try{a="none"===e.css("display")}catch(l){}var i=e.parent();return e[0]&&i&&i[0]&&(!t||a)?n(i):t})(),"width"===e)return d.clientWidth;d.height=d.maxHeight||d.height,d.css&&-1===d.css.indexOf(F)&&(a=d.css.split("}"),layui.each(a,function(e,t){t&&(a[e]="."+F+"-"+r.index+" "+t)}),d.css=a.join("}"));var c=function(a,e,i,l){var n,o;l?(l.key=[d.index,a,i].join("-"),l.colspan=l.colspan||0,l.rowspan=l.rowspan||0,r.initOpts(l),(n=a+(parseInt(l.rowspan)||1)) td:hover > .layui-table-cell{overflow: auto;}"].concat(x.ie?[".layui-table-edit{height: "+i+";}","td[data-edit]:hover:after{height: "+i+";}"]:[]),function(e,t){t&&o.push(a+" "+t)})),l.css&&o.push(l.css),o.push("."+r+"{height:auto;}"),d.style({target:this.elem[0],text:o.join(""),id:"DF-table-"+n})},a.prototype.renderToolbar=function(){var l,o=this,e=o.config,r=e.elem.attr("lay-filter"),t=['
          ','
          ','
          '].join(""),a=o.layTool.find(".layui-table-tool-temp"),n=("default"===e.toolbar?a.html(t):"string"==typeof e.toolbar&&(t=f(e.toolbar).html()||"")&&a.html(m(t).render(e)),{filter:{title:s.$t("table.tools.filter.title"),layEvent:"LAYTABLE_COLS",icon:"layui-icon-cols",onClick:function(e){var a,n=e.config;(0,e.openPanel)({list:(a=[],o.eachCols(function(e,t){t.field&&"normal"==t.type&&a.push('
        • "+(t.fieldTitle||t.title||t.field)+"").text())+'" lay-filter="LAY_TABLE_TOOL_COLS">
        • ')}),a.join("")),done:function(){i.on("checkbox(LAY_TABLE_TOOL_COLS)",function(e){var e=f(e.elem),t=this.checked,a=e.data("key"),i=o.col(a),l=i.hide,e=e.data("parentkey");i.key&&(i.hide=!t,o.elem.find('*[data-key="'+a+'"]')[t?"removeClass":"addClass"](N),l!=i.hide&&o.setParentCol(!t,e),o.resize(),layui.event.call(this,R,"colToggled("+r+")",{col:i,config:n}))})}})}},exports:{title:s.$t("table.tools.export.title"),layEvent:"LAYTABLE_EXPORT",icon:"layui-icon-export",onClick:function(e){var t=e.data,a=e.config,i=e.openPanel,e=e.elem;if(!t.length)return g.tips(s.$t("table.tools.export.noDataPrompt"),e,{tips:3});x.ie?g.tips(s.$t("table.tools.export.compatPrompt"),e,{tips:3}):i({list:['
        • '+s.$t("table.tools.export.csvText")+"
        • "].join(""),done:function(e,t){t.on("click",function(){var e=f(this).data("type");w.exportFile.call(o,a.id,null,e)})}})}},print:{title:s.$t("table.tools.print.title"),layEvent:"LAYTABLE_PRINT",icon:"layui-icon-print",onClick:function(e){var t=e.data,e=e.elem;if(!t.length)return g.tips(s.$t("table.tools.print.noDataPrompt"),e,{tips:3});var t=window.open("about:blank","_blank"),e=[""].join(""),a=f(o.layHeader.html());a.append(o.layMain.find("table").html()),a.append(o.layTotal.find("table").html()),a.find("th.layui-table-patch").remove(),a.find("thead>tr>th."+L).filter(function(e,t){return!f(t).children("."+K).length}).remove(),a.find("tbody>tr>td."+L).remove(),t.document.write(e+a.prop("outerHTML")),t.document.close(),layui.device("edg").edg?(t.onafterprint=t.close,t.print()):(t.print(),t.close())}}});"object"==typeof e.defaultToolbar&&(l=[],e.defaultToolbar=f.map(e.defaultToolbar,function(e,t){var a="string"==typeof e,i=a?n[e]:e;return i&&(!(i=i.name&&n[i.name]?f.extend({},n[i.name],i):i).name&&a&&(i.name=e),l.push('
          ')),i}),o.layTool.find(".layui-table-tool-self").html(l.join("")))},a.prototype.renderPagebar=function(){var e,t=this.config,a=this.layPagebar=f('
          ');t.pagebar&&((e=f(t.pagebar).html()||"")&&a.append(m(e).render(t)),this.layPage.append(a))},a.prototype.setParentCol=function(e,t){var a=this.config,i=this.layHeader.find('th[data-key="'+t+'"]'),l=parseInt(i.attr("colspan"))||0;i[0]&&(t=t.split("-"),t=a.cols[t[1]][t[2]],e?l--:l++,i.attr("colspan",l),i[l?"removeClass":"addClass"](N),t.colspan2=l,t.hide=l<1,a=i.data("parentkey"))&&this.setParentCol(e,a)},a.prototype.setColsPatch=function(){var a=this,e=a.config;layui.each(e.cols,function(e,t){layui.each(t,function(e,t){t.hide&&a.setParentCol(t.hide,t.parentKey)})})},a.prototype.setGroupWidth=function(i){var e,l=this;l.config.cols.length<=1||((e=l.layHeader.find((i?"th[data-key="+i.data("parentkey")+"]>":"")+"."+K)).css("width",0),layui.each(e.get().reverse(),function(){var e=f(this),t=e.parent().data("key"),a=0;l.layHeader.eq(0).find("th[data-parentkey="+t+"]").width(function(e,t){f(this).hasClass(N)||0o.layMain.prop("clientHeight")&&(e.style.width=parseFloat(e.style.width)-i+"px")}),!p&&y?h.width(o.getContentWidth(l)):h.width("auto"),o.setGroupWidth()},a.prototype.resize=function(e){var t=this;if(e){if(0===e.contentRect.height&&0===e.contentRect.width)return;if(e.target._lay_lastSize&&Math.abs(e.target._lay_lastSize.height-e.contentRect.height)<2&&Math.abs(e.target._lay_lastSize.width-e.contentRect.width)<2)return;e.target._lay_lastSize={height:e.contentRect.height,width:e.contentRect.width}}t.layMain&&("isConnected"in t.layMain[0]?t.layMain[0].isConnected:f.contains(document.body,t.layMain[0]))&&(t.fullSize(),t.setColsWidth(),t.scrollPatch())},a.prototype.reload=function(e,t,a){var i=this;e=e||{},delete i.haveInit,layui.each(e,function(e,t){"array"===layui.type(t)&&delete i.config[e]}),i.config=f.extend(t,{},i.config,e),"reloadData"!==a&&(layui.each(i.config.cols,function(e,t){layui.each(t,function(e,t){delete t.colspan2})}),delete i.config.HAS_SET_COLS_PATCH),i.render(a)},a.prototype.errorView=function(e){var t=this,a=t.layMain.find("."+y),e=f('
          '+(e||"Error")+"
          ");a[0]&&(t.layNone.remove(),a.remove()),t.layFixed.addClass(N),t.layMain.find("tbody").html(""),t.layMain.append(t.layNone=e),t.layTotal.addClass(h),t.layPage.find(B).addClass(h),w.cache[t.key]=[],t.syncCheckAll(),t.renderForm(),t.setColsWidth(),t.loading(!1)},a.prototype.page=1,a.prototype.pullData=function(i,l){var e,t,n=this,o=n.config,a=(o.HAS_SET_COLS_PATCH||n.setColsPatch(),o.HAS_SET_COLS_PATCH=!0,o.request),r=o.response,d=function(){"object"==typeof o.initSort&&n.sort({field:o.initSort.field,type:o.initSort.type,reloadType:l.type})},c=function(e,t){n.setColsWidth(),n.loading(!1),"function"==typeof o.done&&o.done(e,i,e[r.countName],t),"function"==typeof l.done&&l.done()};l=l||{},"function"==typeof o.before&&o.before(o),n.startTime=(new Date).getTime(),l.renderData?((e={})[r.dataName]=w.cache[n.key],e[r.countName]=o.url?"object"===layui.type(o.page)?o.page.count:e[r.dataName].length:o.data.length,"object"==typeof o.totalRow&&(e[r.totalRowName]=f.extend({},n.totalRow)),n.renderData({res:e,curr:i,count:e[r.countName],type:l.type,sort:!0}),c(e,"renderData")):o.url?(t={},o.page&&(t[a.pageName]=i,t[a.limitName]=o.limit),a=f.extend(t,o.where),o.contentType&&0==o.contentType.indexOf("application/json")&&(a=JSON.stringify(a)),n.loading(!0),t={type:o.method||"get",url:o.url,contentType:o.contentType,data:a,dataType:o.dataType||"json",jsonpCallback:o.jsonpCallback,headers:o.headers||{},complete:"function"==typeof o.complete?o.complete:undefined,success:function(e){var t,a;(e="function"==typeof o.parseData?o.parseData(e)||e:e)[r.statusName]!=r.statusCode?n.errorView(e[r.msgName]||s.$t("table.dataFormatError",{statusName:r.statusName,statusCode:r.statusCode})):(t=e[r.countName],(a=Math.ceil(t/o.limit)||1)','
          "+function(){var e,t=f.extend(!0,{LAY_COL:l},o),a=w.config.checkName,i=w.config.disabledName;switch(l.type){case"checkbox":return'';case"radio":return'';case"numbers":return c}return l.toolbar?m(f(l.toolbar).html()||"").render(t):T.call(s,{item3:l,content:n,tplData:t})}(),"
          "].join(""),i.push(e),l.fixed&&"right"!==l.fixed&&r.push(e),"right"===l.fixed&&d.push(e))}),e=['data-index="'+e+'"'],o[w.config.checkName]&&e.push('class="'+S+'"'),e=e.join(" "),h.push(""+i.join("")+""),y.push(""+r.join("")+""),p.push(""+d.join("")+""))}),{trs:h,trs_fixed:y,trs_fixed_r:p}},w.getTrHtml=function(e,t){e=C(e);return e.getTrHtml(t,null,e.page)},a.prototype.renderData=function(e){var a=this,i=a.config,t=e.res,l=e.curr,n=a.count=e.count,o=e.sort,r=t[i.response.dataName]||[],t=t[i.response.totalRowName],d=[],c=[],s=[],u=function(){if(!o&&a.sortKey)return a.sort({field:a.sortKey.field,type:a.sortKey.sort,pull:!0,reloadType:e.type});a.getTrHtml(r,o,l,{trs:d,trs_fixed:c,trs_fixed_r:s}),"fixed"===i.scrollPos&&"reloadData"===e.type||a.layBody.scrollTop(0),"reset"===i.scrollPos&&a.layBody.scrollLeft(0),a.layMain.find("."+y).remove(),a.layMain.find("tbody").html(d.join("")),a.layFixLeft.find("tbody").html(c.join("")),a.layFixRight.find("tbody").html(s.join("")),a.syncCheckAll(),a.renderForm(),a.fullSize(),a.haveInit?a.scrollPatch():setTimeout(function(){a.scrollPatch()},50),a.haveInit=!0,a.needSyncFixedRowHeight&&a.calcFixedRowHeight(),g.close(a.tipsIndex)};return w.cache[a.key]=r,a.layTotal[0==r.length?"addClass":"removeClass"](h),a.layPage[i.page||i.pagebar?"removeClass":"addClass"](N),a.layPage.find(B)[!i.page||0==n||0===r.length&&1==l?"addClass":"removeClass"](h),0===r.length?a.errorView(i.text.none):(a.layFixLeft.removeClass(N),o?u():(u(),a.renderTotal(r,t),a.layTotal&&a.layTotal.removeClass(N),void(i.page&&(i.page=f.extend({elem:"layui-table-page"+i.index,count:n,limit:i.limit,limits:i.limits||[10,20,30,40,50,60,70,80,90],groups:3,layout:["prev","page","next","skip","count","limit"],prev:'',next:'',jump:function(e,t){t||(a.page=e.curr,i.limit=e.limit,a.pullData(e.curr))}},i.page),i.page.count=n,p.render(i.page)))))},w.renderData=function(e){e=C(e);e&&e.pullData(e.page,{renderData:!0,type:"reloadData"})},a.prototype.renderTotal=function(e,o){var r,d=this,c=d.config,s={};c.totalRow&&(layui.each(e,function(e,i){"array"===layui.type(i)&&0===i.length||d.eachCols(function(e,t){var e=t.field||e,a=i[e];t.totalRow&&(s[e]=(s[e]||0)+(parseFloat(a)||0))})}),d.dataTotal=[],r=[],d.eachCols(function(e,t){var e=t.field||e,a=o&&o[t.field],i="totalRowDecimals"in t?t.totalRowDecimals:2,i=s[e]?parseFloat(s[e]||0).toFixed(i):"",i=(n=t.totalRowText||"",(l={LAY_COL:t})[e]=i,l=t.totalRow&&T.call(d,{item3:t,content:i,tplData:l})||n,a||l),l="string"==typeof(n=t.totalRow||c.totalRow)?m(n).render(f.extend({TOTAL_NUMS:a||s[e],TOTAL_ROW:o||{},LAY_COL:t},t)):i,n=(t.field&&d.dataTotal.push({field:t.field,total:f("
          "+l+"
          ").text()}),['','
          "+l,"
          "].join(""));r.push(n)}),e=d.layTotal.find(".layui-table-patch"),d.layTotal.find("tbody").html(""+r.join("")+(e.length?e.get(0).outerHTML:"")+""))},a.prototype.getColElem=function(e,t){return e.eq(0).find(".laytable-cell-"+t+":eq(0)")},a.prototype.renderForm=function(e){var t=this.elem.attr("lay-filter");i.render(e,t)},a.prototype.renderFormByElem=function(a){layui.each(["input","select"],function(e,t){i.render(a.find(t))})},a.prototype.syncCheckAll=function(){var a,e=this,i=e.config,t=e.layHeader.find('input[name="layTableCheckbox"]'),l=w.checkStatus(e.key);t[0]&&(a=l.isAll,e.eachCols(function(e,t){"checkbox"===t.type&&(t[i.checkName]=a)}),t.prop({checked:l.isAll,indeterminate:!l.isAll&&l.data.length}))},a.prototype.setRowActive=function(e,t,a){e=this.layBody.find('tr[data-index="'+e+'"]');if(t=t||"layui-table-click",a)return e.removeClass(t);e.addClass(t),e.siblings("tr").removeClass(t)},a.prototype.setRowChecked=function(i){var a,e,l,t,n,o,r,d=this,c=d.config,s="all"===i.index,u="array"===layui.type(i.index),h=s||u;c.tree&&c.tree.view||h&&(d.layBox.addClass(G),"radio"===i.type)||(u&&(a={},layui.each(i.index,function(e,t){a[t]=!0}),i.index=a),e=d.layBody.children(".layui-table").children("tbody"),r=h?"tr":'tr[data-index="'+i.index+'"]',r=e.children(r),e=s?r:r.filter(u?function(){var e=f(this).data("index");return i.index[e]}:'[data-index="'+i.index+'"]'),i=f.extend({type:"checkbox"},i),l=w.cache[d.key],t="checked"in i,n=function(e){return"radio"===i.type||(t?i.checked:!e)},e.each(function(){var e=f(this),t=e.attr("data-index"),a=l[t];t&&"array"!==layui.type(a)&&!a[c.disabledName]&&(a=a[c.checkName]=n(e.hasClass(S)),e.toggleClass(S,!!a),"radio"===i.type)&&(o=t,e.siblings().removeClass(S))}),o&&layui.each(l,function(e,t){Number(o)!==Number(e)&&delete t[c.checkName]}),r=(u=(s=e.children("td").children(".layui-table-cell").children('input[lay-type="'+({radio:"layTableRadio",checkbox:"layTableCheckbox"}[i.type]||"checkbox")+'"]:not(:disabled)')).last()).closest(O),("radio"===i.type&&r.hasClass(N)?s.first():s).prop("checked",n(u.prop("checked"))),d.syncCheckAll(),h&&setTimeout(function(){d.layBox.removeClass(G)},100))},a.prototype.sort=function(l){var e,t=this,a={},i=t.config,n=i.elem.attr("lay-filter"),o=w.cache[t.key];"string"==typeof(l=l||{}).field&&(r=l.field,t.layHeader.find("th").each(function(e,t){var a=f(this),i=a.data("field");if(i===l.field)return l.field=a,r=i,!1}));try{var r=r||l.field.data("field"),d=l.field.data("key");if(t.sortKey&&!l.pull&&r===t.sortKey.field&&l.type===t.sortKey.sort)return;var c=t.layHeader.find("th .laytable-cell-"+d).find(D);t.layHeader.find("th").find(D).removeAttr("lay-sort"),c.attr("lay-sort",l.type||null),t.layFixed.find("th")}catch(s){b.error("Table modules: sort field '"+r+"' not matched")}t.sortKey={field:r,sort:l.type},i.autoSort&&("asc"===l.type?e=layui.sort(o,r,null,!0):"desc"===l.type?e=layui.sort(o,r,!0,!0):(e=layui.sort(o,w.config.initIndexName,null,!0),delete t.sortKey,delete i.initSort)),a[i.response.dataName]=e||o,t.renderData({res:a,curr:t.page,count:t.count,sort:!0,type:l.reloadType}),l.fromEvent&&(i.initSort={field:r,type:l.type},layui.event.call(l.field,R,"sort("+n+")",f.extend({config:i},i.initSort)))},a.prototype.loading=function(e){this.config.loading&&this.layBox.find(".layui-table-init").toggleClass(N,!e)},a.prototype.cssRules=function(t,a){var e=this.elem.children("style")[0];d.getStyleRules(e,function(e){if(e.selectorText===".laytable-cell-"+t)return a(e),!0})},a.prototype.fullSize=function(){var e,a,i=this,t=i.config,l=t.height;i.fullHeightGap?(l=o.height()-i.fullHeightGap)<135&&(l=135):i.parentDiv&&i.parentHeightGap?(l=f(i.parentDiv).height()-i.parentHeightGap)<135&&(l=135):i.customHeightFunc&&(l=i.customHeightFunc())<135&&(l=135),1
          ')).find("div").css({width:a}),e.find("tr").append(t)):e.find(".layui-table-patch").remove()};n(e.layHeader),n(e.layTotal);n=e.layMain.height()-i;e.layFixed.find(W).css("height",t.height()>=n?n:"auto").scrollTop(e.layMain.scrollTop()),e.layFixRight[w.cache[e.key]&&w.cache[e.key].length&&0');a.html(t),s.height&&a.css("max-height",s.height-(c.layTool.outerHeight()||50)),i.find("."+M)[0]||i.append(a),c.renderForm(),a.on("click",function(e){layui.stope(e)}),e.done&&e.done(a,t)};layui.stope(e),j.trigger("table.tool.panel.remove"),g.close(c.tipsIndex),layui.each(s.defaultToolbar,function(e,t){if(t.layEvent===a)return"function"==typeof t.onClick&&t.onClick({data:l,config:s,openPanel:n,elem:i}),!0}),layui.event.call(this,R,"toolbar("+o+")",f.extend({event:a,config:s},{}))}),c.layHeader.on("click","*[lay-event]",function(e){var t=f(this),a=t.attr("lay-event"),t=t.closest("th").data("key"),t=c.col(t);layui.event.call(this,R,"colTool("+o+")",f.extend({event:a,config:s,col:t},{}))}),c.layPagebar.on("click","*[lay-event]",function(e){var t=f(this).attr("lay-event");layui.event.call(this,R,"pagebar("+o+")",f.extend({event:t,config:s},{}))}),e.on("mousemove",function(e){var t=f(this),a=t.offset().left,e=e.clientX-a;t.data("unresize")||k.eventMoveElem||(d.allowResize=t.width()-e<=10,r.css("cursor",d.allowResize?"col-resize":""))}).on("mouseleave",function(){k.eventMoveElem||(d.allowResize=!1,r.css("cursor",""))}).on("mousedown",function(e){var t,a=f(this);d.allowResize&&(t=a.data("key"),e.preventDefault(),d.offset=[e.clientX,e.clientY],c.cssRules(t,function(e){var t=e.style.width||a.outerWidth();d.rule=e,d.ruleWidth=parseFloat(t),d.minWidth=a.data("minwidth")||s.cellMinWidth,d.maxWidth=a.data("maxwidth")||s.cellMaxWidth}),a.data(z,d),k.eventMoveElem=a)}),k.docEvent||j.on("mousemove",function(e){var t,a;k.eventMoveElem&&(t=k.eventMoveElem.data(z)||{},k.eventMoveElem.data("resizing",1),e.preventDefault(),t.rule)&&(e=t.ruleWidth+e.clientX-t.offset[0],a=k.eventMoveElem.closest("."+F).attr(_),a=C(a))&&((e=et.maxWidth&&(e=t.maxWidth),t.rule.style.width=e+"px",a.setGroupWidth(k.eventMoveElem),g.close(c.tipsIndex))}).on("mouseup",function(e){var t,a,i,l,n;k.eventMoveElem&&(i=(t=k.eventMoveElem).closest("."+F).attr(_),a=C(i))&&(i=t.data("key"),l=a.col(i),n=a.config.elem.attr("lay-filter"),d={},r.css("cursor",""),a.scrollPatch(),t.removeData(z),delete k.eventMoveElem,a.cssRules(i,function(e){l.width=parseFloat(e.style.width),layui.event.call(t[0],R,"colResized("+n+")",{col:l,config:a.config})}))}),k.docEvent=!0,e.on("click",function(e){var t=f(this),a=t.find(D),i=a.attr("lay-sort");if(!f(e.target).closest("[lay-event]")[0]){if(!a[0]||1===t.data("resizing"))return t.removeData("resizing");c.sort({field:t,type:"asc"===i?"desc":"desc"===i?null:"asc",fromEvent:!0})}}).find(D+" .layui-edge ").on("click",function(e){var t=f(this),a=t.index(),t=t.parents("th").eq(0).data("field");layui.stope(e),0===a?c.sort({field:t,type:"asc",fromEvent:!0}):c.sort({field:t,type:"desc",fromEvent:!0})}),c.commonMember=function(e){var a=f(this).parents("tr").eq(0).data("index"),t=c.layBody.find('tr[data-index="'+a+'"]'),i=(w.cache[c.key]||[])[a]||{},l={tr:t,config:s,data:w.clearCacheKey(i),dataCache:i,index:a,del:function(){w.cache[c.key][a]=[],t.remove(),c.scrollPatch()},update:function(e,t){c.updateRow({index:a,data:e=e||{},related:t},function(e,t){l.data[e]=t})},setRowChecked:function(e){c.setRowChecked(f.extend({index:a},e))}};return f.extend(l,e)}),t=(c.elem.on("click",'input[name="layTableCheckbox"]+',function(e){var t=f(this),a=t.closest("td"),t=t.prev(),i=t.parents("tr").eq(0).data("index"),l=t[0].checked,n="layTableAllChoose"===t.attr("lay-filter");t[0].disabled||(n?c.setRowChecked({index:"all",checked:l}):c.setRowChecked({index:i,checked:l}),layui.stope(e),layui.event.call(t[0],R,"checkbox("+o+")",h.call(t[0],{checked:l,type:n?"all":"one",getCol:function(){return c.col(a.data("key"))}})))}),c.elem.on("click",'input[lay-type="layTableRadio"]+',function(e){var t=f(this),a=t.closest("td"),t=t.prev(),i=t[0].checked,l=t.parents("tr").eq(0).data("index");if(layui.stope(e),t[0].disabled)return!1;c.setRowChecked({type:"radio",index:l}),layui.event.call(t[0],R,"radio("+o+")",h.call(t[0],{checked:i,getCol:function(){return c.col(a.data("key"))}}))}),c.layBody.on("mouseenter","tr",function(){var e=f(this),t=e.index();e.data("off")||((e=c.layBody.find("tr:eq("+t+")")).addClass(E),c.needSyncFixedRowHeight&&c.fixedRowHeightPatchOnHover(this,e,!0))}).on("mouseleave","tr",function(){var e=f(this),t=e.index();e.data("off")||((e=c.layBody.find("tr:eq("+t+")")).removeClass(E),c.needSyncFixedRowHeight&&c.fixedRowHeightPatchOnHover(this,e,!1))}).on("click","tr",function(e){t.call(this,"row",e)}).on("dblclick","tr",function(e){t.call(this,"rowDouble",e)}).on("contextmenu","tr",function(e){s.defaultContextmenu||e.preventDefault(),t.call(this,"rowContextmenu",e)}),function(e,t){var a=f(this);if(!a.data("off")){if("rowContextmenu"!==e){var i=[".layui-form-checkbox",".layui-form-switch",".layui-form-radio","[lay-unrow]",'[lay-type="layTableCheckbox"]','[lay-type="layTableRadio"]'].join(",");if(f(t.target).is(i)||f(t.target).closest(i)[0])return}layui.event.call(this,R,e+"("+o+")",h.call(a.children("td")[0],{e:t}))}}),n=function(e,t){var a,i,l;(e=f(e)).data("off")||(l=e.data("field"),i=e.data("key"),i=c.col(i),a=e.closest("tr").data("index"),a=w.cache[c.key][a],(i="function"==typeof i.edit?i.edit(a):i.edit)&&((i=f("textarea"===i?'':''))[0].value=(l=e.data("content")||a[l])===undefined||null===l?"":l,e.find("."+H)[0]||e.append(i),i.focus(),t)&&layui.stope(t))},i=(c.layBody.on("change","."+H,function(){var e=f(this),t=e.parent(),a=this.value,i=e.parent().data("field"),e=e.closest("tr").data("index"),e=w.cache[c.key][e],l=h.call(t[0],{value:a,field:i,oldValue:e[i],td:t,reedit:function(){setTimeout(function(){n(l.td);var e={};e[i]=l.oldValue,l.update(e)})},getCol:function(){return c.col(t.data("key"))}}),e={};e[i]=a,l.update(e),layui.event.call(t[0],R,"edit("+o+")",l)}).on("blur","."+H,function(){f(this).remove()}),c.layBody.on(s.editTrigger,"td",function(e){n(this,e)}).on("mouseenter","td",function(){a.call(this)}).on("mouseleave","td",function(){a.call(this,"hide")}),c.layTotal.on("mouseenter","td",function(){a.call(this)}).on("mouseleave","td",function(){a.call(this,"hide")}),"layui-table-grid-down"),a=function(e){var t=f(this),a=t.children(u);t.data("off")||t.parent().hasClass(A)||(e?t.find(".layui-table-grid-down").remove():!(a.prop("scrollWidth")>a.prop("clientWidth")||0'))},l=function(e,t){var a=f(this),i=a.parent(),l=i.data("key"),n=c.col(l),o=i.parent().data("index"),r=i.children(u),i="layui-table-cell-c",d=f('');"tips"===(t=t||n.expandedMode||s.cellExpandedMode)?c.tipsIndex=g.tips(['
          ',r.html(),"
          ",''].join(""),r[0],{tips:[3,""],time:-1,anim:-1,maxWidth:x.ios||x.android?300:c.elem.width()/2,isOutAnim:!1,skin:"layui-table-tips",success:function(e,t){e.find(".layui-table-tips-c").on("click",function(){g.close(t)})}}):(c.elem.find("."+i).trigger("click"),c.cssRules(l,function(e){var t=e.style.width,a=n.expandedWidth||s.cellExpandedWidth;a.layui-table-body>table>tbody>tr"),i=e.layFixRight.find(">.layui-table-body>table>tbody>tr"),t=t.find(">tbody>tr"),l=[];t.each(function(){l.push(e.getElementSize(this).height)}),a.length&&a.each(function(e){l[e]&&(this.style.height=l[e]+"px")}),i.length&&i.each(function(e){l[e]&&(this.style.height=l[e]+"px")})},a.prototype.fixedRowHeightPatchOnHover=function(t,e,a){var i,l=this,n=l.elem.children("style")[0],o="."+r;e.toggleClass(r,a),a?d.getStyleRules(n,function(e){e.selectorText===o&&e.style.setProperty("height",l.getElementSize(t).height+"px","important")}):(d.getStyleRules(n,function(e){e.selectorText===o&&e.style.setProperty("height","auto")}),(e=e.filter(function(){var e=f(this),t=0tr").each(function(i){n.cols[i]=[],f(this).children().each(function(e){var t=f(this),a=t.attr("lay-data"),a=d.options(this,{attr:a?"lay-data":null,errorText:r+(a||t.attr("lay-options"))}),t=f.extend({title:t.text(),colspan:parseInt(t.attr("colspan"))||1,rowspan:parseInt(t.attr("rowspan"))||1},a);n.cols[i].push(t)})}),e.find("tbody>tr")),t=w.render(n);!a.length||o.data||t.config.url||(l=0,w.eachCols(t.config.id,function(e,i){a.each(function(e){n.data[e]=n.data[e]||{};var t=f(this),a=i.field;n.data[e][a]=t.children("td").eq(l).html()}),l++}),t.reloadData({data:n.data}))}),this},k.that={},k.config={},function(a,i,e,l){var n,o;l.colGroup&&(n=0,a++,l.CHILD_COLS=[],o=e+(parseInt(l.rowspan)||1),layui.each(i[o],function(e,t){t.parentKey?t.parentKey===l.key&&(t.PARENT_COL_INDEX=a,l.CHILD_COLS.push(t),$(a,i,o,t)):t.PARENT_COL_INDEX||1<=n&&n==(l.colspan||1)||(t.PARENT_COL_INDEX=a,l.CHILD_COLS.push(t),n+=parseInt(1td').filter('[data-field="'+e+'"]')}}})).replace(/"/g,'""'),n.push(a='"'+a+'"')):t.field&&"normal"!==t.type&&0==i&&(u[t.field]=!0)}),c.push(n.join(","))}),o&&layui.each(o.dataTotal,function(e,t){u[t.field]||s.push('"'+(t.total||"")+'"')}),d.join(",")+"\r\n"+c.join("\r\n")+"\r\n"+s.join(","))),r.download=(a.title||l.title||"table_"+(l.index||""))+"."+i,document.body.appendChild(r),r.click(),document.body.removeChild(r)},w.getOptions=l,w.hideCol=function(e,l){var n=C(e);n&&("boolean"===layui.type(l)?n.eachCols(function(e,t){var a=t.key,i=n.col(a),t=t.parentKey;i.hide!=l&&(i=i.hide=l,n.elem.find('*[data-key="'+a+'"]')[i?"addClass":"removeClass"](N),n.setParentCol(i,t))}):(l=layui.isArray(l)?l:[l],layui.each(l,function(e,l){n.eachCols(function(e,t){var a,i;l.field===t.field&&(a=t.key,i=n.col(a),t=t.parentKey,"hide"in l)&&i.hide!=l.hide&&(i=i.hide=!!l.hide,n.elem.find('*[data-key="'+a+'"]')[i?"addClass":"removeClass"](N),n.setParentCol(i,t))})})),f("."+M).remove(),n.resize())},w.reload=function(e,t,a,i){if(l(e))return(e=C(e)).reload(t,a,i),k.call(e)},w.reloadData=function(){var a=f.extend([],arguments),i=(a[3]="reloadData",new RegExp("^("+["elem","id","cols","width","height","maxHeight","toolbar","defaultToolbar","className","css","pagebar"].join("|")+")$"));return layui.each(a[1],function(e,t){i.test(e)&&delete a[1][e]}),w.reload.apply(null,a)},w.render=function(e){e=new a(e);return k.call(e)},w.clearCacheKey=function(e){return delete(e=f.extend({},e))[w.config.checkName],delete e[w.config.indexName],delete e[w.config.initIndexName],delete e[w.config.numbersName],delete e[w.config.disabledName],e},f(function(){w.init()}),c(R,w)});layui.define(["table"],function(e){"use strict";var P=layui.$,h=layui.form,B=layui.table,y=layui.hint(),j={config:{},on:B.on,eachCols:B.eachCols,index:B.index,set:function(e){var t=this;return t.config=P.extend({},t.config,e),t},resize:B.resize,getOptions:B.getOptions,hideCol:B.hideCol,renderData:B.renderData},i=function(){var a=this,e=a.config,n=e.id||e.index;return{config:e,reload:function(e,t){a.reload.call(a,e,t)},reloadData:function(e,t){j.reloadData(n,e,t)}}},F=function(e){var t=i.that[e];return t||y.error(e?"The treeTable instance with ID '"+e+"' not found":"ID argument required"),t||null},L="lay-table-id",q="layui-hide",s=".layui-table-body",R=".layui-table-main",Y=".layui-table-fixed-l",z=".layui-table-fixed-r",n="layui-table-checked",m="layui-table-tree",H="LAY_DATA_INDEX",b="LAY_DATA_INDEX_HISTORY",f="LAY_PARENT_INDEX",g="LAY_CHECKBOX_HALF",X="LAY_EXPAND",V="LAY_HAS_EXPANDED",U="LAY_ASYNC_STATUS",l=["all","parent","children","none"],t=/<[^>]+?>/,p=["flexIconClose","flexIconOpen","iconClose","iconOpen","iconLeaf","icon"],a=function(e){var t=this;t.index=++j.index,t.config=P.extend(!0,{},t.config,j.config,e),t.init(),t.render()},x=function(n,i,e){var l=B.cache[n];layui.each(e||l,function(e,t){var a=t[H]||"";-1!==a.indexOf("-")&&(l[a]=t),t[i]&&x(n,i,t[i])})},d=function(d,a,e){var r=F(d),o=("reloadData"!==e&&(r.status={expand:{}}),P.extend(!0,{},r.getOptions(),a)),n=o.tree,c=n.customName.children,i=n.customName.id,l=(delete a.hasNumberCol,delete a.hasChecboxCol,delete a.hasRadioCol,B.eachCols(null,function(e,t){"numbers"===t.type?a.hasNumberCol=!0:"checkbox"===t.type?a.hasChecboxCol=!0:"radio"===t.type&&(a.hasRadioCol=!0)},o.cols),a.parseData),u=a.done;"reloadData"===e&&"fixed"===o.scrollPos&&(r.scrollTopCache=r.config.elem.next().find(s).scrollTop()),o.url?e&&(!l||l.mod)||(a.parseData=function(){var e=this,t=arguments,a=t[0],t=("function"===layui.type(l)&&(a=l.apply(e,t)||t[0]),e.response.dataName);return n.data.isSimpleData&&!n["async"].enable&&(a[t]=r.flatToTree(a[t])),N(a[t],function(e){e[X]=X in e?e[X]:e[i]!==undefined&&r.status.expand[e[i]]},c),e.autoSort&&e.initSort&&e.initSort.type&&layui.sort(a[t],e.initSort.field,"desc"===e.initSort.type,!0),r.initData(a[t]),a},a.parseData.mod=!0):a.data!==undefined&&(a.data=a.data||[],n.data.isSimpleData&&(a.data=r.flatToTree(a.data)),r.initData(a.data)),e&&(!u||u.mod)||(a.done=function(){var e,t=arguments,a=t[3],n="renderData"===a,i=(n||delete r.isExpandAll,this.elem.next()),l=(r.updateStatus(null,{LAY_HAS_EXPANDED:!1}),x(d,c),i.find('[name="layTableCheckbox"][lay-filter="layTableAllChoose"]'));if(l.length&&(e=j.checkStatus(d),l.prop({checked:e.isAll&&e.data.length,indeterminate:!e.isAll&&e.data.length})),!n&&o.autoSort&&o.initSort&&o.initSort.type&&j.sort(d),r.renderTreeTable(i),"reloadData"===a&&"fixed"===this.scrollPos&&i.find(s).scrollTop(r.scrollTopCache),"function"===layui.type(u))return u.apply(this,t)},a.done.mod=!0),a&&a.tree&&a.tree.view&&layui.each(p,function(e,t){a.tree.view[t]!==undefined&&(a.tree.view[t]=r.normalizedIcon(a.tree.view[t]))})};a.prototype.init=function(){var e=this.config,t=e.tree.data.cascade,t=(-1===l.indexOf(t)&&(e.tree.data.cascade="all"),B.render(P.extend({},e,{data:[],url:"",done:null}))),a=t.config.id;(i.that[a]=this).tableIns=t,d(a,e)},a.prototype.config={tree:{customName:{children:"children",isParent:"isParent",name:"name",id:"id",pid:"parentId",icon:"icon"},view:{indent:14,flexIconClose:'',flexIconOpen:'',showIcon:!0,icon:"",iconClose:'',iconOpen:'',iconLeaf:'',showFlexIconIfNotParent:!1,dblClickExpand:!0,expandAllDefault:!1},data:{isSimpleData:!1,rootPid:null,cascade:"all"},"async":{enable:!1,url:"",type:null,contentType:null,headers:null,where:null,autoParam:[]},callback:{beforeExpand:null,onExpand:null}}},a.prototype.normalizedIcon=function(e){return e?t.test(e)?e:'':""},a.prototype.getOptions=function(){return this.tableIns?B.getOptions(this.tableIns.config.id):this.config},a.prototype.flatToTree=function(e){var n,i,l,d,r,o,c,u,t=this.getOptions(),a=t.tree,s=a.customName;return e=e||B.cache[t.id],t=e,n=s.id,i=s.pid,l=s.children,d=a.data.rootPid,n=n||"id",i=i||"parentId",l=l||"children",c={},u=[],layui.each(t,function(e,t){r=n+t[n],o=n+t[i],c[r]||(c[r]={},c[r][l]=[]);var a={};a[l]=c[r][l],c[r]=P.extend({},t,a),((d?c[r][i]===d:!c[r][i])?u:(c[o]||(c[o]={},c[o][l]=[]),c[o][l])).push(c[r])}),u},a.prototype.treeToFlat=function(e,n,i){var l=this,d=l.getOptions().tree.customName,r=d.children,o=d.pid,c=[];return layui.each(e,function(e,t){var e=(i?i+"-":"")+e,a=P.extend({},t);a[o]="undefined"!=typeof t[o]?t[o]:n,c.push(a),c=c.concat(l.treeToFlat(t[r],t[d.id],e))}),c},a.prototype.getTreeNode=function(e){var t=this;return e?{data:e,dataIndex:e[H],getParentNode:function(){return t.getNodeByIndex(e[f])}}:y.error("Node data not found")},a.prototype.getNodeByIndex=function(t){var a,e,n=this,i=n.getNodeDataByIndex(t);return i?(a=n.getOptions().id,(e={data:i,dataIndex:i[H],getParentNode:function(){return n.getNodeByIndex(i[f])},update:function(e){return j.updateNode(a,t,e)},remove:function(){return j.removeNode(a,t)},expand:function(e){return j.expandNode(a,P.extend({},e,{index:t}))},setChecked:function(e){return j.setRowChecked(a,P.extend({},e,{index:t}))}}).dataIndex=t,e):y.error("Node data not found by index: "+t)},a.prototype.getNodeById=function(a){var e=this.getOptions(),n=e.tree.customName.id,i="",e=j.getData(e.id,!0);if(layui.each(e,function(e,t){if(t[n]===a)return i=t[H],!0}),i)return this.getNodeByIndex(i)},a.prototype.getNodeDataByIndex=function(e,t,a){var n=this.getOptions(),i=n.tree,n=B.cache[n.id],l=n[e];if("delete"!==a&&l)return P.extend(l,a),t?P.extend({},l):l;for(var d=n,r=String(e).split("-"),o=0,c=i.customName.children;o
          '),I=function(e){p[U]="success",p[f.children]=e,u.initData(p[f.children],p[H]),K(t,!0,!x&&n,i,l,d)},D=b.format,"function"===layui.type(D)?D(p,c,I):(C=P.extend({},b.where||c.where),D=b.autoParam,layui.each(D,function(e,t){t=t.split("=");C[t[0].trim()]=p[(t[1]||t[0]).trim()]}),(D=b.contentType||c.contentType)&&0==D.indexOf("application/json")&&(C=JSON.stringify(C)),S=b.method||c.method,T=b.dataType||c.dataType,_=b.jsonpCallback||c.jsonpCallback,k=b.headers||c.headers,w=b.parseData||c.parseData,O=b.response||c.response,b={type:S||"get",url:g,contentType:D,data:C,dataType:T||"json",jsonpCallback:_,headers:k||{},success:function(e){(e="function"==typeof w?w.call(c,e)||e:e)[O.statusName]!=O.statusCode?(p[U]="error",p[X]=!1,v.html('')):I(e[O.dataName])},error:function(e,t){p[U]="error",p[X]=!1,"function"==typeof c.error&&c.error(e,t)}},c.ajax?c.ajax(b,"treeNodes"):P.ajax(b)),m;p[V]=!0,N.length&&(!c.initSort||c.url&&!c.autoSort||((S=c.initSort).type?layui.sort(N,S.field,"desc"===S.type,!0):layui.sort(N,B.config.indexName,null,!0)),u.initData(p[f.children],p[H]),g=B.getTrHtml(o,N,null,null,e),E={trs:P(g.trs.join("")),trs_fixed:P(g.trs_fixed.join("")),trs_fixed_r:P(g.trs_fixed_r.join(""))},A=(e.split("-").length-1||0)+1,layui.each(N,function(e,t){E.trs.eq(e).attr({"data-index":t[H],"lay-data-index":t[H],"data-level":A}).data("index",t[H]),E.trs_fixed.eq(e).attr({"data-index":t[H],"lay-data-index":t[H],"data-level":A}).data("index",t[H]),E.trs_fixed_r.eq(e).attr({"data-index":t[H],"lay-data-index":t[H],"data-level":A}).data("index",t[H])}),r.find(R).find('tbody tr[lay-data-index="'+e+'"]').after(E.trs),r.find(Y).find('tbody tr[lay-data-index="'+e+'"]').after(E.trs_fixed),r.find(z).find('tbody tr[lay-data-index="'+e+'"]').after(E.trs_fixed_r),u.renderTreeTable(E.trs,A),n)&&!x&&layui.each(N,function(e,t){K({dataIndex:t[H],trElem:r.find('tr[lay-data-index="'+t[H]+'"]').first(),tableViewElem:r,tableId:o,options:c},a,n,i,l,d)})}else u.isExpandAll=!1,(n&&!x?(layui.each(N,function(e,t){K({dataIndex:t[H],trElem:r.find('tr[lay-data-index="'+t[H]+'"]').first(),tableViewElem:r,tableId:o,options:c},a,n,i,l,d)}),r.find(N.map(function(e,t,a){return'tr[lay-data-index="'+e[H]+'"]'}).join(","))):(D=u.treeToFlat(N,p[f.id],e),r.find(D.map(function(e,t,a){return'tr[lay-data-index="'+e[H]+'"]'}).join(",")))).addClass(q);J("resize-"+o,function(){j.resize(o)},0)(),l&&"loading"!==p[U]&&(T=s.callback.onExpand,"function"===layui.type(T))&&T(o,p,h),"function"===layui.type(d)&&"loading"!==p[U]&&d(o,p,h)}return m},v=(j.expandNode=function(e,t){var a,n,i,l,e=F(e);if(e)return a=(t=t||{}).index,n=t.expandFlag,i=t.inherit,l=t.callbackFlag,e=e.getOptions().elem.next(),K({trElem:e.find('tr[lay-data-index="'+a+'"]').first()},n,i,null,l,t.done)},j.expandAll=function(a,e){if("boolean"!==layui.type(e))return y.error('treeTable.expandAll param "expandFlag" must be a boolean value.');var t=F(a);if(t){t.isExpandAll=e;var n=t.getOptions(),i=n.tree,l=n.elem.next(),d=i.customName.isParent,r=i.customName.id,o=i.view.showFlexIconIfNotParent;if(e){e=j.getData(a,!0);if(i["async"].enable){var c=!0;if(layui.each(e,function(e,t){if(t[d]&&!t[U])return!(c=!1)}),!c)return void layui.each(j.getData(a),function(e,t){j.expandNode(a,{index:t[H],expandFlag:!0,inherit:!0})})}var u=!0;if(layui.each(e,function(e,t){if(t[d]&&!t[V])return!(u=!1)}),u)t.updateStatus(null,function(e){(e[d]||o)&&(e[X]=!0,e[r]!==undefined)&&(t.status.expand[e[r]]=!0)}),l.find('tbody tr[data-level!="0"]').removeClass(q),l.find(".layui-table-tree-flexIcon").html(i.view.flexIconOpen),i.view.showIcon&&l.find(".layui-table-tree-nodeIcon:not(.layui-table-tree-iconCustom,.layui-table-tree-iconLeaf)").html(i.view.iconOpen);else{if(t.updateStatus(null,function(e){(e[d]||o)&&(e[X]=!0,e[V]=!0,e[r]!==undefined)&&(t.status.expand[e[r]]=!0)}),n.initSort&&n.initSort.type&&n.autoSort)return j.sort(a);var s,n=B.getTrHtml(a,e),f={trs:P(n.trs.join("")),trs_fixed:P(n.trs_fixed.join("")),trs_fixed_r:P(n.trs_fixed_r.join(""))};layui.each(e,function(e,t){var a=t[H].split("-").length-1;s={"data-index":t[H],"lay-data-index":t[H],"data-level":a},f.trs.eq(e).attr(s).data("index",t[H]),f.trs_fixed.eq(e).attr(s).data("index",t[H]),f.trs_fixed_r.eq(e).attr(s).data("index",t[H])}),layui.each(["main","fixed-l","fixed-r"],function(e,t){l.find(".layui-table-"+t+" tbody").html(f[["trs","trs_fixed","trs_fixed_r"][e]])}),t.renderTreeTable(l,0,!1)}}else t.updateStatus(null,function(e){(e[d]||o)&&(e[X]=!1,e[r]!==undefined)&&(t.status.expand[e[r]]=!1)}),l.find('.layui-table-box tbody tr[data-level!="0"]').addClass(q),l.find(".layui-table-tree-flexIcon").html(i.view.flexIconClose),i.view.showIcon&&l.find(".layui-table-tree-nodeIcon:not(.layui-table-tree-iconCustom,.layui-table-tree-iconLeaf)").html(i.view.iconClose);j.resize(a)}},a.prototype.updateNodeIcon=function(e){var t=this.getOptions().tree||{},a=e.scopeEl,n=e.isExpand,e=e.isParent;a.find(".layui-table-tree-flexIcon").css("visibility",e||t.view.showFlexIconIfNotParent?"visible":"hidden").html(n?t.view.flexIconOpen:t.view.flexIconClose),t.view.showIcon&&(a=a.find(".layui-table-tree-nodeIcon:not(.layui-table-tree-iconCustom)"),n=e?n?t.view.iconOpen:t.view.iconClose:t.view.iconLeaf,a.toggleClass("layui-table-tree-iconLeaf",!e).html(n))},a.prototype.renderTreeTable=function(e,t,a){var l=this,n=l.getOptions(),d=n.elem.next(),i=(d.hasClass(m)||d.addClass(m),n.id),r=n.tree||{},o=r.view||{},c=r.customName||{},u=c.isParent,s=l,f=n.data.length,y=((t=t||0)||(d.find(".layui-table-body tr:not([data-level])").attr("data-level",t),layui.each(B.cache[i],function(e,t){f&&(t[H]=String(e));t=t[H];d.find('.layui-table-main tbody tr[data-level="0"]:eq('+e+")").attr("lay-data-index",t),d.find('.layui-table-fixed-l tbody tr[data-level="0"]:eq('+e+")").attr("lay-data-index",t),d.find('.layui-table-fixed-r tbody tr[data-level="0"]:eq('+e+")").attr("lay-data-index",t)})),null),p=c.name,x=o.indent||14;if(layui.each(e.find('td[data-field="'+p+'"]'),function(e,t){var a,n,i=(t=P(t)).closest("tr"),t=t.children(".layui-table-cell");t.hasClass("layui-table-tree-item")||(n=i.attr("lay-data-index"))&&(i=d.find('tr[lay-data-index="'+n+'"]'),(a=s.getNodeDataByIndex(n))[X]&&a[u]&&((y=y||{})[n]=!0),a[g]&&i.find('input[type="checkbox"][name="layTableCheckbox"]').prop("indeterminate",!0),n=t.html(),(t=i.find('td[data-field="'+p+'"]>div.layui-table-cell')).addClass("layui-table-tree-item"),t.html(['
          ',a[X]?o.flexIconOpen:o.flexIconClose,"
          ",o.showIcon?'
          '+(l.normalizedIcon(a[c.icon])||o.icon||(a[u]?a[X]?o.iconOpen:o.iconClose:o.iconLeaf)||"")+"
          ":"",n].join("")).find(".layui-table-tree-flexIcon").on("click",function(e){layui.stope(e),K({trElem:i},null,null,null,!0)}))}),!t&&r.view.expandAllDefault&&l.isExpandAll===undefined)return j.expandAll(i,!0);(!1!==a&&y?(layui.each(y,function(e,t){e=d.find('tr[lay-data-index="'+e+'"]');e.find(".layui-table-tree-flexIcon").html(o.flexIconOpen),K({trElem:e.first()},!0)}),J("renderTreeTable2-"+i,function(){h.render(P(".layui-table-tree["+L+'="'+i+'"]'))},0)):J("renderTreeTable-"+i,function(){n.hasNumberCol&&v(l),h.render(P(".layui-table-tree["+L+'="'+i+'"]'))},0))()},function(a){var e=a.getOptions(),t=e.elem.next(),n=0,i=t.find(".layui-table-main tbody tr"),l=t.find(".layui-table-fixed-l tbody tr"),d=t.find(".layui-table-fixed-r tbody tr");layui.each(a.treeToFlat(B.cache[e.id]),function(e,t){t.LAY_HIDE||(a.getNodeDataByIndex(t[H]).LAY_NUM=++n,i.eq(e).find(".laytable-cell-numbers").html(n),l.eq(e).find(".laytable-cell-numbers").html(n),d.eq(e).find(".laytable-cell-numbers").html(n))})}),N=(a.prototype.render=function(e){var t=this;t.tableIns=B["reloadData"===e?"reloadData":"reload"](t.tableIns.config.id,P.extend(!0,{},t.config)),t.config=t.tableIns.config},a.prototype.reload=function(e,t,a){var n=this;e=e||{},delete n.haveInit,layui.each(e,function(e,t){"array"===layui.type(t)&&delete n.config[e]}),d(n.getOptions().id,e,a||!0),n.config=P.extend(t,{},n.config,e),n.render(a)},j.reloadData=function(){var e=P.extend(!0,[],arguments);return e[3]="reloadData",j.reload.apply(null,e)},function(e,a,n,i){var l=[];return layui.each(e,function(e,t){"function"===layui.type(a)?a(t):P.extend(t,a),l.push(P.extend({},t)),i||(l=l.concat(N(t[n],a,n,i)))}),l}),o=(a.prototype.updateStatus=function(e,t,a){var n=this.getOptions(),i=n.tree;return e=e||B.cache[n.id],N(e,t,i.customName.children,a)},a.prototype.getTableData=function(){var e=this.getOptions();return B.cache[e.id]},j.updateStatus=function(e,t,a){var e=F(e),n=e.getOptions();return a=a||(n.url?B.cache[n.id]:n.data),e.updateStatus(a,t)},j.sort=function(e){var t,a,i,l,n,d=F(e);d&&(n=(t=d.getOptions()).tree,a=j.getData(e),i=n.customName.children,l=function(e,a,n){layui.sort(e,a,n,!0),layui.each(e,function(e,t){l(t[i]||[],a,n)})},t.autoSort)&&((n=t.initSort).type?l(a,n.field,"desc"===n.type):l(a,B.config.indexName,null),B.cache[e]=a,d.initData(a),j.renderData(e))},function(n){var t=n.config.id,i=F(t),a=n.data=j.getNodeDataByIndex(t,n.index),l=a[H],d=(n.dataIndex=l,n.update);n.update=function(){var e=arguments,t=(P.extend(i.getNodeDataByIndex(l),e[0]),d.apply(this,e)),a=n.config.tree.customName.name;return a in e[0]&&n.tr.find('td[data-field="'+a+'"]').children("div.layui-table-cell").removeClass("layui-table-tree-item"),i.renderTreeTable(n.tr,n.tr.attr("data-level"),!1),t},n.del=function(){j.removeNode(t,a)},n.setRowChecked=function(e){j.setRowChecked(t,{index:a,checked:e})}}),u=(j.updateNode=function(e,a,t){var n,i,l,d,r,o=F(e);o&&(d=(n=o.getOptions().elem.next()).find('tr[lay-data-index="'+a+'"]'),i=d.attr("data-index"),l=d.attr("data-level"),t)&&(d=o.getNodeDataByIndex(a,!1,t),r=B.getTrHtml(e,[d]),layui.each(["main","fixed-l","fixed-r"],function(e,t){n.find(".layui-table-"+t+' tbody tr[lay-data-index="'+a+'"]').replaceWith(P(r[["trs","trs_fixed","trs_fixed_r"][e]].join("")).attr({"data-index":i,"lay-data-index":a,"data-level":l}).data("index",i))}),o.renderTreeTable(n.find('tr[lay-data-index="'+a+'"]'),l))},j.removeNode=function(e,t,a){var n,i,l,d,r,o,c,u,s=F(e);s&&(i=(u=(n=s.getOptions()).tree).customName.isParent,l=u.customName.children,d=n.elem.next(),r=[],o=B.cache[e],t=s.getNodeDataByIndex("string"===layui.type(t)?t:t[H],!1,"delete"),c=s.getNodeDataByIndex(t[f]),s.updateCheckStatus(c),u=s.treeToFlat([t],t[u.customName.pid],t[f]),layui.each(u,function(e,t){t=t[H];r.push('tr[lay-data-index="'+t+'"]'),-1!==t.indexOf("-")&&delete o[t]}),d.find(r.join(",")).remove(),t=s.initData(),function(){for(var e in o)-1!==e.indexOf("-")&&e!==o[e][H]&&delete o[e]}(),layui.each(s.treeToFlat(t),function(e,t){t[b]&&t[b]!==t[H]&&d.find('tr[lay-data-index="'+t[b]+'"]').attr({"data-index":t[H],"lay-data-index":t[H]}).data("index",t[H])}),layui.each(o,function(e,t){d.find('tr[data-level="0"][lay-data-index="'+t[H]+'"]').attr("data-index",e).data("index",e)}),n.hasNumberCol&&v(s),c&&(u=d.find('tr[lay-data-index="'+c[H]+'"]'),a||(c[i]=!(!c[l]||!c[l].length)),s.updateNodeIcon({scopeEl:u,isExpand:c[X],isParent:c[i]})),j.resize(e))},j.addNodes=function(e,t){var a=F(e);if(a){var n=a.getOptions(),i=n.tree,l=n.elem.next(),d=B.config.checkName,r=(t=t||{}).parentIndex,o=t.index,c=t.data,t=t.focus,u=(r="number"===layui.type(r)?r.toString():r)?a.getNodeDataByIndex(r):null,o="number"===layui.type(o)?o:-1,c=P.extend(!0,[],layui.isArray(c)?c:[c]);if(layui.each(c,function(e,t){d in t||!u||(t[d]=u[d])}),u){var s=i.customName.isParent,f=i.customName.children;u[s]=!0;var y=(y=u[f])?(p=y.splice(-1===o?y.length:o),u[f]=y.concat(c,p)):u[f]=c,f=(a.updateStatus(y,function(e){(e[s]||i.view.showFlexIconIfNotParent)&&(e[V]=!1)}),a.treeToFlat(y));l.find(f.map(function(e){return'tr[lay-data-index="'+e[H]+'"]'}).join(",")).remove(),u[V]=!1,u[U]="local",K({trElem:l.find('tr[lay-data-index="'+r+'"]')},!0)}else{var p=B.cache[e].splice(-1===o?B.cache[e].length:o);if(B.cache[e]=B.cache[e].concat(c,p),n.url||(n.page?(y=n.page,n.data.splice.apply(n.data,[y.limit*(y.curr-1),y.limit].concat(B.cache[e]))):n.data=B.cache[e]),l.find(".layui-none").length)return B.renderData(e),c;var x,f=B.getTrHtml(e,c),h={trs:P(f.trs.join("")),trs_fixed:P(f.trs_fixed.join("")),trs_fixed_r:P(f.trs_fixed_r.join(""))},r=(layui.each(c,function(e,t){x={"data-index":t[H],"lay-data-index":t[H],"data-level":"0"},h.trs.eq(e).attr(x).data("index",t[H]),h.trs_fixed.eq(e).attr(x).data("index",t[H]),h.trs_fixed_r.eq(e).attr(x).data("index",t[H])}),parseInt(c[0][H])-1),y=l.find(R),n=l.find(Y),f=l.find(z);-1==r?y.find('tr[data-level="0"][data-index="0"]')[0]?(y.find('tr[data-level="0"][data-index="0"]').before(h.trs),n.find('tr[data-level="0"][data-index="0"]').before(h.trs_fixed),f.find('tr[data-level="0"][data-index="0"]').before(h.trs_fixed_r)):(y.find("tbody").prepend(h.trs),n.find("tbody").prepend(h.trs_fixed),f.find("tbody").prepend(h.trs_fixed_r)):-1===o?(y.find("tbody").append(h.trs),n.find("tbody").append(h.trs_fixed),f.find("tbody").append(h.trs_fixed_r)):(r=p[0][b],y.find('tr[data-level="0"][data-index="'+r+'"]').before(h.trs),n.find('tr[data-level="0"][data-index="'+r+'"]').before(h.trs_fixed),f.find('tr[data-level="0"][data-index="'+r+'"]').before(h.trs_fixed_r)),layui.each(B.cache[e],function(e,t){l.find('tr[data-level="0"][lay-data-index="'+t[H]+'"]').attr("data-index",e).data("index",e)}),a.renderTreeTable(l.find(c.map(function(e,t,a){return'tr[lay-data-index="'+e[H]+'"]'}).join(",")))}return a.updateCheckStatus(u),u&&(o=l.find('tr[lay-data-index="'+u[H]+'"]'),a.updateNodeIcon({scopeEl:o,isExpand:u[X],isParent:u[s]})),j.resize(e),t&&l.find(R).find('tr[lay-data-index="'+c[0][H]+'"]').get(0).scrollIntoViewIfNeeded(),c}},j.checkStatus=function(e,n){var i,t,a,l=F(e);if(l)return l=l.getOptions().tree,i=B.config.checkName,t=j.getData(e,!0).filter(function(e,t,a){return e[i]||n&&e[g]}),a=!0,layui.each("all"===l.data.cascade?B.cache[e]:j.getData(e,!0),function(e,t){if(!t[i])return!(a=!1)}),{data:t,isAll:a}},j.on("sort",function(e){var e=e.config,t=e.elem.next(),e=e.id;t.hasClass(m)&&j.sort(e)}),j.on("row",function(e){e.config.elem.next().hasClass(m)&&o(e)}),j.on("rowDouble",function(e){var t=e.config;t.elem.next().hasClass(m)&&(o(e),(t.tree||{}).view.dblClickExpand)&&K({trElem:e.tr.first()},null,null,null,!0)}),j.on("rowContextmenu",function(e){e.config.elem.next().hasClass(m)&&o(e)}),j.on("tool",function(e){e.config.elem.next().hasClass(m)&&o(e)}),j.on("edit",function(e){var t=e.config;t.elem.next().hasClass(m)&&(o(e),e.field===t.tree.customName.name)&&((t={})[e.field]=e.value,e.update(t))}),j.on("radio",function(e){var t=e.config,a=t.elem.next(),t=t.id;a.hasClass(m)&&(a=F(t),o(e),u.call(a,e.tr,e.checked))}),a.prototype.setRowCheckedClass=function(e,t){var a=this.getOptions().elem.next();e[t?"addClass":"removeClass"](n),e.each(function(){var e=P(this).data("index");a.find('.layui-table-fixed-r tbody tr[data-index="'+e+'"]')[t?"addClass":"removeClass"](n)})},a.prototype.updateCheckStatus=function(e,t){var a,n,i,l,d,r,o,c=this,u=c.getOptions();return!!u.hasChecboxCol&&(a=u.tree,n=u.id,i=u.elem.next(),l=B.config.checkName,"all"!==(d=a.data.cascade)&&"parent"!==d||!e||(d=c.updateParentCheckStatus(e,"boolean"===layui.type(t)?t:null),layui.each(d,function(e,t){var a=i.find('tr[lay-data-index="'+t[H]+'"] input[name="layTableCheckbox"]:not(:disabled)'),n=t[l];c.setRowCheckedClass(a.closest("tr"),n),a.prop({checked:n,indeterminate:t[g]})})),o=!(r=!0),0<(e=(e="all"===a.data.cascade?B.cache[n]:j.getData(n,!0)).filter(function(e){return!e[u.disabledName]})).length?layui.each(e,function(e,t){if((t[l]||t[g])&&(o=!0),t[l]||(r=!1),o&&!r)return!0}):r=!1,o=o&&!r,i.find('input[name="layTableCheckbox"][lay-filter="layTableAllChoose"]').prop({checked:r,indeterminate:o}),r)},a.prototype.updateParentCheckStatus=function(a,n){var i,e=this.getOptions(),t=e.tree,e=e.id,l=B.config.checkName,t=t.customName.children,d=[];return!(a[g]=!1)===n?a[t].length?layui.each(a[t],function(e,t){if(!t[l])return n=!1,a[g]=!0}):n=!1:!1===n?layui.each(a[t],function(e,t){if(t[l]||t[g])return a[g]=!0}):(n=!1,i=0,layui.each(a[t],function(e,t){t[l]&&i++}),n=a[t].length?a[t].length===i:a[l],a[g]=!n&&0li"],n.bodyElem=["."+C.CONST.BODY+":eq(0)",">."+C.CONST.ITEM],n.getContainer=function(){var e=n.documentElem||t.elem;return{header:{elem:e.find(n.headerElem[0]),items:e.find(n.headerElem.join(""))},body:{elem:e.find(n.bodyElem[0]),items:e.find(n.bodyElem.join(""))}}},"array"===layui.type(t.header)?"string"==typeof t.header[0]?(n.headerElem=t.header.concat(),n.documentElem=p(document)):(n.elemView=p('
          '),t.className&&n.elemView.addClass(t.className),a=p('
            '),i=p('
            '),layui.each(t.header,function(e,t){t=n.renderHeaderItem(t);a.append(t)}),layui.each(t.body,function(e,t){t=n.renderBodyItem(t);i.append(t)}),n.elemView.append(a).append(i),t.elem.html(n.elemView)):n.renderClose(),"array"===layui.type(t.body)&&"string"==typeof t.body[0]&&(n.documentElem=p(document),n.bodyElem=t.body.concat()),n.data());"index"in t&&e.index!=t.index?n.change(n.findHeaderItem(t.index),!0):-1===e.index&&n.change(n.findHeaderItem(0),!0),n.roll("auto"),t.elem.hasClass(C.CONST.CLASS_HIDEV)&&t.elem.removeClass(C.CONST.CLASS_HIDEV),"function"==typeof t.afterRender&&t.afterRender(e),layui.event.call(t.elem[0],C.CONST.MOD_NAME,"afterRender("+t.id+")",e)},events:function(){var e,t=this,a=t.config,i=t.getContainer(),n=C.CONST.MOD_NAME,i=(t.documentElem?i.header:a).elem,a=a.trigger+(".lay_"+n+"_trigger"),n=t.documentElem?t.headerElem[1]:t.headerElem.join("");i.off(a).on(a,n,function(){t.change(p(this))}),r.onresize||(p(window).on("resize",function(){clearTimeout(e),e=setTimeout(function(){layui.each(C.cache.id,function(e){e=C.getInst(e);e&&e.roll("init")})},50)}),r.onresize=!0)}}),r={},t=C.Class;t.prototype.add=function(e){var t,a,i=this,n=i.getContainer(),r=i.renderHeaderItem(e),d=i.renderBodyItem(e),l=i.data();e=p.extend({active:!0},e),/(before|after)/.test(e.mode)?(a=(t=Object.prototype.hasOwnProperty.call(e,"index"))?i.findHeaderItem(e.index):l.thisHeaderItem,t=t?i.findBodyItem(e.index):l.thisHeaderItem,a[e.mode](r),t[e.mode](d)):(a={prepend:"prepend",append:"append"}[e.mode||"append"]||"append",n.header.elem[a](r),n.body.elem[a](d)),e.active?i.change(r,!0):i.roll("auto"),"function"==typeof e.done&&e.done(p.extend(l,{headerItem:r,bodyItem:d}))},t.prototype.close=function(e,t){if(e&&e[0]){var a=this,i=a.config,n=e.attr("lay-id"),r=e.index();if("false"!==e.attr("lay-closable")){var d=a.data();if(!t)if(!1===layui.event.call(e[0],C.CONST.MOD_NAME,"beforeClose("+i.id+")",p.extend(d,{index:r})))return;e.hasClass(C.CONST.CLASS_THIS)&&(e.next()[0]?a.change(e.next(),!0):e.prev()[0]&&a.change(e.prev(),!0)),a.findBodyItem(n||r).remove(),e.remove(),a.roll("auto",r),d=a.data(),layui.event.call(d.thisHeaderItem[0],C.CONST.MOD_NAME,"afterClose("+i.id+")",d)}}},t.prototype.closeMult=function(i,e){var n=this,t=n.config,a=n.getContainer(),r=n.data(),a=a.header.items,d='[lay-closable="false"]',l=(e=e===undefined?r.index:e,n.findHeaderItem(e)),o=l.index();"false"!==r.thisHeaderItem.attr("lay-closable")&&("all"!==i&&i?e!==r.index&&n.change(l,!0):(e=a.filter(":gt("+r.index+")"+d).eq(0),l=p(a.filter(":lt("+r.index+")"+d).get().reverse()).eq(0),e[0]?n.change(e,!0):l[0]&&n.change(l,!0))),a.each(function(e){var t=p(this),a=t.attr("lay-id"),a=n.findBodyItem(a||e);"false"!==t.attr("lay-closable")&&("other"===i&&e!==o||"right"===i&&o");return t.html(e.title||"New Tab").attr("lay-id",e.id),this.appendClose(t,e),t},t.prototype.renderBodyItem=function(e){var t=this.config,t=p(e.bodyItem||t.bodyItem||'
            ');return t.html(e.content||"").attr("lay-id",e.id),t},t.prototype.appendClose=function(e,t){var a=this;a.config.closable&&(0==(t=t||{}).closable&&e.attr("lay-closable","false"),"false"===e.attr("lay-closable")||e.find("."+C.CONST.CLOSE)[0]||((t=p('')).on("click",function(){return a.close(p(this).parent()),!1}),e.append(t)))},t.prototype.renderClose=function(){var t=this,a=t.config;t.getContainer().header.items.each(function(){var e=p(this);a.closable?t.appendClose(e):e.find("."+C.CONST.CLOSE).remove()})},t.prototype.roll=function(e,i){var n=this,t=n.config,a=n.getContainer(),r=a.header.elem,d=a.header.items,a=r.prop("scrollWidth"),l=Math.ceil(r.outerWidth()),o=r.data("left")||0,s="scroll"===t.headerMode,c="layui-tabs-scroll",f="layui-tabs-bar",u=["layui-icon-prev","layui-icon-next"],m={elem:p('
            '),bar:p(['
            ','','',"
            "].join(""))};if("normal"!==t.headerMode){var h,y=r.parent("."+c);if(s||!s&&l=l-o)return r.css("left",-a).data("left",-a),!1}),o=r.data("left")||0,y.find("."+u[0])[o<0?"removeClass":"addClass"](C.CONST.CLASS_DISABLED),y.find("."+u[1])[0')),n=(e.tree(a),i.elem);if(n[0]){if(e.elem=a,e.elemNone=L('
            '+i.text.none+"
            "),n.html(e.elem),0==e.elem.find("."+C.ELEM_SET).length)return e.elem.append(e.elemNone);i.showCheckbox&&e.renderForm("checkbox"),e.elem.find("."+C.ELEM_SET).each(function(){var e=L(this);e.parent(".layui-tree-pack")[0]||e.addClass("layui-tree-setHide"),!e.next()[0]&&e.parents(".layui-tree-pack").eq(1).hasClass("layui-tree-lineExtend")&&e.addClass(C.ELEM_LINE_SHORT),e.next()[0]||e.parents("."+C.ELEM_SET).eq(0).next()[0]||e.addClass(C.ELEM_LINE_SHORT)})}},extendsInstance:function(){var i=this;return{getChecked:function(){return i.getChecked.call(i)},setChecked:function(e){return i.setChecked.call(i,e)}}}}),C=a.CONST,n=a.Class;n.prototype.reload=function(e,i){var a=this;layui.each(e,function(e,i){"array"===layui.type(i)&&delete a.config[e]}),a.config=L.extend(!0,{},a.config,e),a.init(!0,i)},n.prototype.renderForm=function(e){i.render(e,"LAY-tree-"+this.index)},n.prototype.tree=function(d,e){var r=this,E=r.config,s=E.customName,e=e||E.data;layui.each(e,function(e,i){var a,n,t=i[s.children]&&0"),c=L(['
            ','
            ','
            ',E.showLine?t?'':'':'',E.showCheckbox?'':"",E.isJump&&i.href?''+(i[s.title]||i.label||E.text.defaultNodeName)+"":''+(i[s.title]||i.label||E.text.defaultNodeName)+"","
            ",E.edit?(a={add:'',update:'',del:''},n=['
            '],!0===E.edit&&(E.edit=["update","del"]),"object"==typeof E.edit?(layui.each(E.edit,function(e,i){n.push(a[i]||"")}),n.join("")+"
            "):void 0):"","
            ","
            "].join(""));t&&(c.append(l),r.tree(l,i[s.children])),d.append(c),c.prev("."+C.ELEM_SET)[0]&&c.prev().children(".layui-tree-pack").addClass("layui-tree-showLine"),t||c.parent(".layui-tree-pack").addClass("layui-tree-lineExtend"),r.spread(c,i),E.showCheckbox&&(i.checked&&r.checkids.push(i[s.id]),r.checkClick(c,i)),E.edit&&r.operate(c,i)})},n.prototype.spread=function(n,t){var l=this,c=l.config,e=n.children("."+C.ELEM_ENTRY),i=e.children("."+C.ELEM_MAIN),a=i.find('input[same="layuiTreeCheck"]'),d=e.find("."+C.ICON_CLICK),e=e.find("."+C.ELEM_TEXT),r=c.onlyIconControl?d:i,E="";r.on("click",function(e){var i=n.children("."+C.ELEM_PACK),a=(r.children(".layui-icon")[0]?r:r.find(".layui-tree-icon")).children(".layui-icon");i[0]?n.hasClass(C.ELEM_SPREAD)?(n.removeClass(C.ELEM_SPREAD),i.slideUp(200),a.removeClass(C.ICON_SUB).addClass(C.ICON_ADD),l.updateFieldValue(t,"spread",!1)):(n.addClass(C.ELEM_SPREAD),i.slideDown(200),a.addClass(C.ICON_SUB).removeClass(C.ICON_ADD),l.updateFieldValue(t,"spread",!0),c.accordion&&((i=n.siblings("."+C.ELEM_SET)).removeClass(C.ELEM_SPREAD),i.children("."+C.ELEM_PACK).slideUp(200),i.find(".layui-tree-icon").children(".layui-icon").removeClass(C.ICON_SUB).addClass(C.ICON_ADD))):E="normal"}),e.on("click",function(){L(this).hasClass(C.CLASS_DISABLED)||(E=n.hasClass(C.ELEM_SPREAD)?c.onlyIconControl?"open":"close":c.onlyIconControl?"close":"open",a[0]&&l.updateFieldValue(t,"checked",a.prop("checked")),c.click&&c.click({elem:n,state:E,data:t}))})},n.prototype.updateFieldValue=function(e,i,a){i in e&&(e[i]=a)},n.prototype.syncCheckedState=function(e,i,l){var c,t,d=this,r=d.config.customName,E=e.prop("checked"),a=e.closest("."+C.ELEM_SET);e.prop("disabled")||(t=function(e){var i,a,n;e.parents("."+C.ELEM_SET)[0]&&(a=(e=e.parent("."+C.ELEM_PACK)).parent(),(n=e.prev().find('input[same="layuiTreeCheck"]')).prop("disabled")||(E?n.prop("checked",E):(e.find('input[same="layuiTreeCheck"]').each(function(){this.checked&&(i=!0)}),i||n.prop("checked",!1)),t(a)))},(c=function(e,i){var n,t=i[r.children];t&&0!==t.length&&(n=e.children("."+C.ELEM_PACK).children("."+C.ELEM_SET)).children("."+C.ELEM_ENTRY).find('input[same="layuiTreeCheck"]').each(function(e){var i,a;this.disabled||(i=t[e],a=!l&&"checked"in i?i.checked:E,this.checked=a,d.updateFieldValue(i,"checked",a),i[r.children]&&c(n.eq(e),i))})})(a,i),t(a),d.renderForm("checkbox"))},n.prototype.checkClick=function(a,n){var t=this,l=t.config,e=a.children("."+C.ELEM_ENTRY).children("."+C.ELEM_MAIN);e.on("click",'input[same="layuiTreeCheck"]',layui.stope),e.on("click",'input[same="layuiTreeCheck"]+',function(e){layui.stope(e);var e=L(this).prev(),i=e.prop("checked");e.prop("disabled")||(t.syncCheckedState(e,n,"manual"),t.updateFieldValue(n,"checked",i),l.oncheck&&l.oncheck({elem:a,checked:i,data:n}))})},n.prototype.operate=function(r,l){var E=this,s=E.config,o=s.customName,e=r.children("."+C.ELEM_ENTRY),h=e.children("."+C.ELEM_MAIN);e.children(".layui-tree-btnGroup").on("click",".layui-icon",function(e){layui.stope(e);var a,i,n,t,e=L(this).data("type"),c=r.children("."+C.ELEM_PACK),d={data:l,type:e,elem:r};"add"==e?(c[0]||(s.showLine?(h.find("."+C.ICON_CLICK).addClass("layui-tree-icon"),h.find("."+C.ICON_CLICK).children(".layui-icon").addClass(C.ICON_ADD).removeClass("layui-icon-leaf")):h.find(".layui-tree-iconArrow").removeClass(C.CLASS_HIDE),r.append('
            ')),i=s.operate&&s.operate(d),(t={})[o.title]=s.text.defaultNodeName,t[o.id]=i,E.tree(r.children("."+C.ELEM_PACK),[t]),s.showLine&&(c[0]?(c.hasClass(C.ELEM_EXTEND)||c.addClass(C.ELEM_EXTEND),r.find("."+C.ELEM_PACK).each(function(){L(this).children("."+C.ELEM_SET).last().addClass(C.ELEM_LINE_SHORT)}),(c.children("."+C.ELEM_SET).last().prev().hasClass(C.ELEM_LINE_SHORT)?c.children("."+C.ELEM_SET).last().prev():c.children("."+C.ELEM_SET).last()).removeClass(C.ELEM_LINE_SHORT),!r.parent("."+C.ELEM_PACK)[0]&&r.next()[0]&&c.children("."+C.ELEM_SET).last().removeClass(C.ELEM_LINE_SHORT)):(i=r.siblings("."+C.ELEM_SET),a=1,t=r.parent("."+C.ELEM_PACK),layui.each(i,function(e,i){L(i).children("."+C.ELEM_PACK)[0]||(a=0)}),(1==a?(i.children("."+C.ELEM_PACK).addClass(C.ELEM_SHOW),i.children("."+C.ELEM_PACK).children("."+C.ELEM_SET).removeClass(C.ELEM_LINE_SHORT),r.children("."+C.ELEM_PACK).addClass(C.ELEM_SHOW),t.removeClass(C.ELEM_EXTEND),t.children("."+C.ELEM_SET).last().children("."+C.ELEM_PACK).children("."+C.ELEM_SET).last()):r.children("."+C.ELEM_PACK).children("."+C.ELEM_SET)).addClass(C.ELEM_LINE_SHORT))),s.showCheckbox&&(h.find('input[same="layuiTreeCheck"]')[0].checked&&(r.children("."+C.ELEM_PACK).children("."+C.ELEM_SET).last().find('input[same="layuiTreeCheck"]')[0].checked=!0),E.renderForm("checkbox"))):"update"==e?(i=h.children("."+C.ELEM_TEXT).html(),h.children("."+C.ELEM_TEXT).html(""),h.append(''),h.children(".layui-tree-editInput").val(u.unescape(i)).focus(),n=function(e){var i=u.escape(e.val().trim())||s.text.defaultNodeName;e.remove(),h.children("."+C.ELEM_TEXT).html(i),d.data[o.title]=i,s.operate&&s.operate(d)},h.children(".layui-tree-editInput").blur(function(){n(L(this))}),h.children(".layui-tree-editInput").on("keydown",function(e){13===e.keyCode&&(e.preventDefault(),n(L(this)))})):(t=p.$t("tree.deleteNodePrompt",{name:l[o.title]||""}),_.confirm(t,function(e){var l,a,i;s.operate&&s.operate(d),d.status="remove",_.close(e),r.prev("."+C.ELEM_SET)[0]||r.next("."+C.ELEM_SET)[0]||r.parent("."+C.ELEM_PACK)[0]?(r.siblings("."+C.ELEM_SET).children("."+C.ELEM_ENTRY)[0]?(s.showCheckbox&&(l=function(e){var i,a,n,t;e.parents("."+C.ELEM_SET)[0]&&(i=e.siblings("."+C.ELEM_SET).children("."+C.ELEM_ENTRY),a=(e=e.parent("."+C.ELEM_PACK).prev()).find('input[same="layuiTreeCheck"]')[0],n=1,(t=0)==a.checked)&&(i.each(function(e,i){i=L(i).find('input[same="layuiTreeCheck"]')[0];0!=i.checked||i.disabled||(n=0),i.disabled||(t=1)}),1==n)&&1==t&&(a.checked=!0,E.renderForm("checkbox"),l(e.parent("."+C.ELEM_SET)))})(r),s.showLine&&(e=r.siblings("."+C.ELEM_SET),a=1,i=r.parent("."+C.ELEM_PACK),layui.each(e,function(e,i){L(i).children("."+C.ELEM_PACK)[0]||(a=0)}),1==a?(c[0]||(i.removeClass(C.ELEM_EXTEND),e.children("."+C.ELEM_PACK).addClass(C.ELEM_SHOW),e.children("."+C.ELEM_PACK).children("."+C.ELEM_SET).removeClass(C.ELEM_LINE_SHORT)),(r.next()[0]?i.children("."+C.ELEM_SET).last():r.prev()).children("."+C.ELEM_PACK).children("."+C.ELEM_SET).last().addClass(C.ELEM_LINE_SHORT),r.next()[0]||r.parents("."+C.ELEM_SET)[1]||r.parents("."+C.ELEM_SET).eq(0).next()[0]||r.prev("."+C.ELEM_SET).addClass(C.ELEM_LINE_SHORT)):!r.next()[0]&&r.hasClass(C.ELEM_LINE_SHORT)&&r.prev().addClass(C.ELEM_LINE_SHORT))):(e=r.parent("."+C.ELEM_PACK).prev(),s.showLine?(e.find("."+C.ICON_CLICK).removeClass("layui-tree-icon"),e.find("."+C.ICON_CLICK).children(".layui-icon").removeClass(C.ICON_SUB).addClass("layui-icon-leaf"),(i=e.parents("."+C.ELEM_PACK).eq(0)).addClass(C.ELEM_EXTEND),i.children("."+C.ELEM_SET).each(function(){L(this).children("."+C.ELEM_PACK).children("."+C.ELEM_SET).last().addClass(C.ELEM_LINE_SHORT)})):e.find(".layui-tree-iconArrow").addClass(C.CLASS_HIDE),r.parents("."+C.ELEM_SET).eq(0).removeClass(C.ELEM_SPREAD),r.parent("."+C.ELEM_PACK).remove()),r.remove()):(r.remove(),E.elem.append(E.elemNone))}))})},n.prototype.events=function(){var i=this,t=i.config;i.setChecked(i.checkids),i.elem.find(".layui-tree-search").on("keyup",function(){var e=L(this),a=e.val(),e=e.nextAll(),n=[];e.find("."+C.ELEM_TEXT).each(function(){var i,e=L(this).parents("."+C.ELEM_ENTRY);-1!=L(this).html().indexOf(a)&&(n.push(L(this).parent()),(i=function(e){e.addClass("layui-tree-searchShow"),e.parent("."+C.ELEM_PACK)[0]&&i(e.parent("."+C.ELEM_PACK).parent("."+C.ELEM_SET))})(e.parent("."+C.ELEM_SET)))}),e.find("."+C.ELEM_ENTRY).each(function(){var e=L(this).parent("."+C.ELEM_SET);e.hasClass("layui-tree-searchShow")||e.addClass(C.CLASS_HIDE)}),0==e.find(".layui-tree-searchShow").length&&i.elem.append(i.elemNone),t.onsearch&&t.onsearch({elem:n})}),i.elem.find(".layui-tree-search").on("keydown",function(){L(this).nextAll().find("."+C.ELEM_ENTRY).each(function(){L(this).parent("."+C.ELEM_SET).removeClass("layui-tree-searchShow "+C.CLASS_HIDE)}),L(".layui-tree-emptyText")[0]&&L(".layui-tree-emptyText").remove()})},n.prototype.getChecked=function(){var t=this,e=t.config,l=e.customName,i=[],a=[],c=(t.elem.find(".layui-form-checked").each(function(){i.push(L(this).prev()[0].value)}),function(e,n){layui.each(e,function(e,a){layui.each(i,function(e,i){if(a[l.id]==i)return t.updateFieldValue(a,"checked",!0),delete(i=L.extend({},a))[l.children],n.push(i),a[l.children]&&(i[l.children]=[],c(a[l.children],i[l.children])),!0})})});return c(L.extend({},e.data),a),a},n.prototype.setChecked=function(e){var c=this,d=c.config.flatData;"object"!=typeof e&&(e=[e]),c.elem.find("."+C.ELEM_SET).each(function(a){var n=L(this).data("id"),t=L(this).children("."+C.ELEM_ENTRY).find('input[same="layuiTreeCheck"]'),l=t.prop("checked");layui.each(e,function(e,i){return n!=i||t.prop("disabled")||l?void 0:(t.prop("checked",!0),c.syncCheckedState(t,d[a]),!0)})})},L.extend(a,{getChecked:function(e){e=a.getInst(e);if(e)return e.getChecked()},setChecked:function(e,i){e=a.getInst(e);if(e)return e.setChecked(i)}}),e(C.MOD_NAME,a)});layui.define(["i18n","laytpl","component","form"],function(e){"use strict";var i=layui.i18n,l=layui.laytpl,o=layui.$,a=layui.form,t=layui.component({name:"transfer",config:{width:200,height:360,data:[],value:[],showSearch:!1,id:""},CONST:{ELEM:"layui-transfer",ELEM_BOX:"layui-transfer-box",ELEM_HEADER:"layui-transfer-header",ELEM_SEARCH:"layui-transfer-search",ELEM_ACTIVE:"layui-transfer-active",ELEM_DATA:"layui-transfer-data",BTN_DISABLED:"layui-btn-disabled"},beforeRender:function(e){this.config=o.extend({title:i.$t("transfer.title"),text:{none:i.$t("transfer.noData"),searchNone:i.$t("transfer.noMatch")}},this.config,e)},render:function(){var e=this,a=e.config,t=function(e){return['
            ','
            ',' ","
            ",""," {{ if(d.data.showSearch){ }}",' "," {{ } }}","",'
              ',"","
              "].join("\n")},t=['
              ',""," \x3c!-- \u5de6\u4fa7\u7a7f\u68ad\u6846 --\x3e",t({index:0,checkAllName:"layTransferLeftCheckAll"}),""," \x3c!-- \u4e2d\u95f4\u64cd\u4f5c\u6309\u94ae --\x3e",'
              '," \x3c!-- \u5411\u53f3\u7a7f\u68ad\u6309\u94ae --\x3e",' ",""," \x3c!-- \u5411\u5de6\u7a7f\u68ad\u6309\u94ae --\x3e",' ","
              ",""," \x3c!-- \u53f3\u4fa7\u7a7f\u68ad\u6846 --\x3e",t({index:1,checkAllName:"layTransferRightCheckAll"}),"","
              "].join("\n"),t=e.elem=o(l(t,{open:"{{",close:"}}",tagStyle:"modern"}).render({data:a,index:e.index})),n=a.elem;n[0]&&(a.data=a.data||[],a.value=a.value||[],n.html(e.elem),e.layBox=e.elem.find("."+d.ELEM_BOX),e.layHeader=e.elem.find("."+d.ELEM_HEADER),e.laySearch=e.elem.find("."+d.ELEM_SEARCH),e.layData=t.find("."+d.ELEM_DATA),e.layBtn=t.find("."+d.ELEM_ACTIVE+" .layui-btn"),e.layBox.css({width:a.width,height:a.height}),e.layData.css({height:(n=a.height-e.layHeader.outerHeight(),a.showSearch&&(n-=e.laySearch.outerHeight()),n-2)}),e.renderData(),e.events())},extendsInstance:function(){var e=this;return{getData:function(){return e.getData.call(e)}}}}),d=t.CONST,n=t.Class;n.prototype.renderData=function(){var e=this,a=e.config,l=[{checkName:"layTransferLeftCheck",views:[]},{checkName:"layTransferRightCheck",views:[]}];e.parseData(function(t){var n=t.selected?1:0,i=["
            • ",'',"
            • "].join("");n?layui.each(a.value,function(e,a){a==t.value&&t.selected&&(l[n].views[e]=i)}):l[n].views.push(i),delete t.selected}),e.layData.eq(0).html(l[0].views.join("")),e.layData.eq(1).html(l[1].views.join("")),e.renderCheckBtn()},n.prototype.renderForm=function(e){a.render(e,"LAY-transfer-"+this.index)},n.prototype.renderCheckBtn=function(c){var r=this,s=r.config;c=c||{},r.layBox.each(function(e){var a=o(this),t=a.find("."+d.ELEM_DATA),a=a.find("."+d.ELEM_HEADER).find('input[type="checkbox"]'),n=t.find('input[type="checkbox"]'),i=0,l=!1;n.each(function(){var e=o(this).data("hide");(this.checked||this.disabled||e)&&i++,this.checked&&!e&&(l=!0)}),a.prop("checked",l&&i===n.length),r.layBtn.eq(e)[l?"removeClass":"addClass"](d.BTN_DISABLED),c.stopNone||(n=t.children("li:not(."+d.CLASS_HIDE+")").length,r.noneView(t,n?"":s.text.none))}),r.renderForm("checkbox")},n.prototype.noneView=function(e,a){var t=o('

              '+(a||"")+"

              ");e.find("."+d.CLASS_NONE)[0]&&e.find("."+d.CLASS_NONE).remove(),a.replace(/\s/g,"")&&e.append(t)},n.prototype.setValue=function(){var e=this,a=e.config,t=[];return e.layBox.eq(1).find("."+d.ELEM_DATA+' input[type="checkbox"]').each(function(){o(this).data("hide")||t.push(this.value)}),a.value=t,e},n.prototype.parseData=function(a){var n=this.config,i=[];return layui.each(n.data,function(e,t){t=("function"==typeof n.parseData?n.parseData(t):t)||t,i.push(t=o.extend({},t)),layui.each(n.value,function(e,a){a==t.value&&(t.selected=!0)}),a&&a(t)}),n.data=i,this},n.prototype.getData=function(e){var a=this.config,n=[];return this.setValue(),layui.each(e||a.value,function(e,t){layui.each(a.data,function(e,a){delete a.selected,t==a.value&&n.push(a)})}),n},n.prototype.transfer=function(e,a){var t,n=this,i=n.config,l=n.layBox.eq(e),c=[],a=(a?((t=(a=a).find('input[type="checkbox"]'))[0].checked=!1,l.siblings("."+d.ELEM_BOX).find("."+d.ELEM_DATA).append(a.clone()),a.remove(),c.push(t[0].value),n.setValue()):l.each(function(e){o(this).find("."+d.ELEM_DATA).children("li").each(function(){var e=o(this),a=e.find('input[type="checkbox"]'),t=a.data("hide");a[0].checked&&!t&&(a[0].checked=!1,l.siblings("."+d.ELEM_BOX).find("."+d.ELEM_DATA).append(e.clone()),e.remove(),c.push(a[0].value)),n.setValue()})}),n.renderCheckBtn(),l.siblings("."+d.ELEM_BOX).find("."+d.ELEM_SEARCH+" input"));""!==a.val()&&a.trigger("keyup"),i.onchange&&i.onchange(n.getData(c),e)},n.prototype.events=function(){var i=this,l=i.config;i.elem.on("click",'input[lay-filter="layTransferCheckbox"]+',function(){var e=o(this).prev(),a=e[0].checked,t=e.parents("."+d.ELEM_BOX).eq(0).find("."+d.ELEM_DATA);e[0].disabled||("all"===e.attr("lay-type")&&t.find('input[type="checkbox"]').each(function(){this.disabled||(this.checked=a)}),setTimeout(function(){i.renderCheckBtn({stopNone:!0})},0))}),i.elem.on("dblclick","."+d.ELEM_DATA+">li",function(e){var a=o(this),t=a.children('input[type="checkbox"]'),n=a.parent().parent().data("index");t[0].disabled||!1!==("function"==typeof l.dblclick?l.dblclick({elem:a,data:i.getData([t[0].value])[0],index:n}):null)&&i.transfer(n,a)}),i.layBtn.on("click",function(){var e=o(this),a=e.data("index");e.hasClass(d.BTN_DISABLED)||i.transfer(a)}),i.laySearch.find("input").on("keyup",function(){var n=this.value,e=o(this).parents("."+d.ELEM_SEARCH).eq(0).siblings("."+d.ELEM_DATA),a=e.children("li"),a=(a.each(function(){var e=o(this),a=e.find('input[type="checkbox"]'),t=a[0].title,t=("cs"!==l.showSearch&&(t=t.toLowerCase(),n=n.toLowerCase()),-1!==t.indexOf(n));e[t?"removeClass":"addClass"](d.CLASS_HIDE),a.data("hide",!t)}),i.renderCheckBtn(),a.length===e.children("li."+d.CLASS_HIDE).length);i.noneView(e,a?l.text.searchNone:"")})},o.extend(t,{getData:function(e){e=t.getInst(e);if(e)return e.getData()}}),e(d.MOD_NAME,t)});layui.define("component",function(e){"use strict";var o=layui.$,i=layui.lay,t=layui.component({name:"carousel",config:{width:"600px",height:"280px",full:!1,arrow:"hover",indicator:"inside",autoplay:!0,interval:3e3,anim:"",trigger:"click",index:0},CONST:{ELEM:"layui-carousel",ELEM_ITEM:">*[carousel-item]>*",ELEM_LEFT:"layui-carousel-left",ELEM_RIGHT:"layui-carousel-right",ELEM_PREV:"layui-carousel-prev",ELEM_NEXT:"layui-carousel-next",ELEM_ARROW:"layui-carousel-arrow",ELEM_IND:"layui-carousel-ind"},render:function(){var e=this,i=e.config;e.elemItem=i.elem.find(s.ELEM_ITEM),i.index<0&&(i.index=0),i.index>=e.elemItem.length&&(i.index=e.elemItem.length-1),i.interval<800&&(i.interval=800),i.full?i.elem.css({position:"fixed",width:"100%",height:"100%",zIndex:9999}):i.elem.css({width:i.width,height:i.height}),i.elem.attr("lay-anim",i.anim),e.elemItem.eq(i.index).addClass(s.CLASS_THIS),e.indicator(),e.arrow(),e.autoplay()},extendsInstance:function(){var i=this;return{elemInd:i.elemInd,elemItem:i.elemItem,timer:i.timer,"goto":function(e){i["goto"](e)}}}}),s=t.CONST,n=t.Class;n.prototype.prevIndex=function(){var e=this.config.index-1;return e=e<0?this.elemItem.length-1:e},n.prototype.nextIndex=function(){var e=this.config.index+1;return e=e>=this.elemItem.length?0:e},n.prototype.addIndex=function(e){var i=this.config;i.index=i.index+(e=e||1),i.index>=this.elemItem.length&&(i.index=0)},n.prototype.subIndex=function(e){var i=this.config;i.index=i.index-(e=e||1),i.index<0&&(i.index=this.elemItem.length-1)},n.prototype.autoplay=function(){var e=this,i=e.config,t=e.elemItem.length;i.autoplay&&(clearInterval(e.timer),1',''].join(""));e.elem.attr("lay-arrow",e.arrow),e.elem.find("."+s.ELEM_ARROW)[0]&&e.elem.find("."+s.ELEM_ARROW).remove(),1t.index?i.slide("add",e-t.index):e
                ',(i=[],layui.each(e.elemItem,function(e){i.push("")}),i.join("")),"
              "].join(""));t.elem.attr("lay-indicator",t.indicator),t.elem.find("."+s.ELEM_IND)[0]&&t.elem.find("."+s.ELEM_IND).remove(),1n[a?"height":"width"]()/3)&&o.slide(0"),i=1;i<=a.length;i++){var n='
            • ";a.half&&parseInt(a.value)!==a.value&&i==Math.ceil(a.value)?t=t+'
            • ":t+=n}t+="",a.text&&(t+=''+a.value+"");var s=a.elem,o=s.next("."+c.ELEM);o[0]&&o.remove(),e.elemTemplate=u(t),a.span=e.elemTemplate.next("span"),a.setText&&a.setText(a.value),s.html(e.elemTemplate),s.addClass("layui-inline"),a.readonly||e.action()},extendsInstance:function(){var a=this,l=a.config;return{setvalue:function(e){l.value=e,a.render()}}}}),c=l.CONST;l.Class.prototype.action=function(){var n=this.config,t=this.elemTemplate,i=t.find("i").width(),l=t.children("li");l.each(function(e){var a=e+1,l=u(this);l.on("click",function(e){n.value=a,n.half&&e.pageX-u(this).offset().left<=i/2&&(n.value=n.value-.5),n.text&&t.next("span").text(n.value),n.choose&&n.choose(n.value),n.setText&&n.setText(n.value)}),l.on("mousemove",function(e){t.find("i").each(function(){u(this).addClass(c.ICON_RATE).removeClass(c.ICON_SOLID_HALF)}),t.find("i:lt("+a+")").each(function(){u(this).addClass(c.ICON_RATE_SOLID).removeClass(c.ICON_HALF_RATE)}),n.half&&e.pageX-u(this).offset().left<=i/2&&l.children("i").addClass(c.ICON_RATE_HALF).removeClass(c.ICON_RATE_SOLID)}),l.on("mouseleave",function(){t.find("i").each(function(){u(this).addClass(c.ICON_RATE).removeClass(c.ICON_SOLID_HALF)}),t.find("i:lt("+Math.floor(n.value)+")").each(function(){u(this).addClass(c.ICON_RATE_SOLID).removeClass(c.ICON_HALF_RATE)}),n.half&&parseInt(n.value)!==n.value&&t.children("li:eq("+Math.floor(n.value)+")").children("i").addClass(c.ICON_RATE_HALF).removeClass(c.ICON_SOLID_RATE)})}),a.touchSwipe(t,{onTouchMove:function(e,a){var i;Date.now()-a.timeStart<=200||(a=e.touches[0].pageX,e=t.width()/n.length,a=(a-t.offset().left)/e,(i=(i=(e=a%1)<=.5&&n.half?.5+(a-e):Math.ceil(a))>n.length?n.length:i)<0&&(i=0),l.each(function(e){var a=u(this).children("i"),l=Math.ceil(i)-e==1,t=Math.ceil(i)>e,e=i-e==.5;t?(a.addClass(c.ICON_RATE_SOLID).removeClass(c.ICON_HALF_RATE),n.half&&e&&a.addClass(c.ICON_RATE_HALF).removeClass(c.ICON_RATE_SOLID)):a.addClass(c.ICON_RATE).removeClass(c.ICON_SOLID_HALF),a.toggleClass("layui-rate-hover",l)}),n.value=i,n.text&&t.next("span").text(n.value),n.setText&&n.setText(n.value))},onTouchEnd:function(e,a){Date.now()-a.timeStart<=200||(t.find("i").removeClass("layui-rate-hover"),n.choose&&n.choose(n.value),n.setText&&n.setText(n.value))}})},e(c.MOD_NAME,l)});layui.define(["i18n","component"],function(o){"use strict";var L=layui.$,g=layui.i18n,v=layui.component({name:"flow",CONST:{ELEM_LOAD:'',ELEM_MORE:"layui-flow-more",FLOW_SCROLL_EVENTS:"scroll.lay_flow_scroll",LAZYIMG_SCROLL_EVENTS:"scroll.lay_flow_lazyimg_scroll"},render:function(){var i,n,r=this.config,c=0,o=r.elem;if(o[0]){var e,a=L(r.scrollElem||document),m="mb"in r?r.mb:50,l=!("isAuto"in r)||r.isAuto,t=r.moreText||g.$t("flow.loadMore"),f=r.end||g.$t("flow.noMore"),s="top"===(r.direction||"bottom"),u=r.scrollElem&&r.scrollElem!==document,d=""+t+"",E=L('"),p=(o.find("."+T.ELEM_MORE).remove(),o[s?"prepend":"append"](E),function(o,e){var l=u?a.prop("scrollHeight"):document.documentElement.scrollHeight,t=a.scrollTop();E[s?"after":"before"](o),(e=0==e||null)?E.html(f):y.html(d),n=e,i=null,r.isLazyimg&&v.lazyimg({elem:r.elem.find("img[lay-src]"),scrollElem:r.scrollElem,direction:r.direction,id:r.id}),s&&(o=u?a.prop("scrollHeight"):document.documentElement.scrollHeight,1===c?a.scrollTop(o):1=i&&o<=n&&e.attr("lay-src")&&(l=e.attr("lay-src"),layui.img(l,function(){var o=t.eq(u);e.attr("src",l).removeAttr("lay-src"),o[0]&&E(o),u++},function(){e.removeAttr("lay-src")}))};if(o)l(o);else for(var r=0;r"),preview:"Preview"},wordWrap:!0,lang:"text",highlighter:!1,langMarker:!1,highlightLine:{focus:{range:"",comment:!1,classActiveLine:"layui-code-line-has-focus",classActivePre:"layui-code-has-focused-lines"},hl:{comment:!1,classActiveLine:"layui-code-line-highlighted"},"++":{comment:!1,classActiveLine:"layui-code-line-diff-add"},"--":{comment:!1,classActiveLine:"layui-code-line-diff-remove"}}},O=layui.code?layui.code.index+1e4:0,P=function(e){return String(e).replace(/\s+$/,"").replace(/^\n|\n$/,"")},R=function(e){return"string"!=typeof e?[]:A.map(e.split(","),function(e){var e=e.split("-"),i=parseInt(e[0],10),e=parseInt(e[1],10);return i&&e?A.map(new Array(e-i+1),function(e,t){return i+t}):i||undefined})},H=/(?:\/\/|\/\*{1,2}||-->)?/;e("code",function(r,e){var u,a,t,i,l,n,o,c,s,d,y,p,E,h,f,v,m,L,g,M,_,C={config:r=A.extend(!0,{},j,r),reload:function(e){layui.code(this.updateOptions(e))},updateOptions:function(e){return delete(e=e||{}).elem,A.extend(!0,r,e)},reloadCode:function(e){layui.code(this.updateOptions(e),"reloadCode")}},w=A(r.elem);return 1',r.ln?['
              ',x.digit(t+1)+".","
              "].join(""):"",'
              ',(d.needParseComment?e.replace(H,""):e)||" ","
              ",""].join("")}),d.preClass&&u.addClass(d.preClass),{lines:s,html:e}},i=r.code,l=function(e){return"function"==typeof r.codeParse?r.codeParse(e,r):e},"reloadCode"===e?u.children(".layui-code-wrap").html(w(l(i)).html):(n=layui.code.index=++O,u.attr("lay-code-index",n),(M=T.CDDE_DATA_CLASS in u.data())&&u.attr("class",u.data(T.CDDE_DATA_CLASS)||""),M||u.data(T.CDDE_DATA_CLASS,u.attr("class")),o={copy:{className:"file-b",title:[W.$t("code.copy")],event:function(e){var t=x.unescape(l(r.code)),i="function"==typeof r.onCopy;lay.clipboard.writeText({text:t,done:function(){if(i&&!1===r.onCopy(t,!0))return;N.msg(W.$t("code.copied"),{icon:1})},error:function(){if(i&&!1===r.onCopy(t,!1))return;N.msg(W.$t("code.copyError"),{icon:2})}})}}},function b(){var e=u.parent("."+T.ELEM_PREVIEW),t=e.children("."+T.ELEM_TAB),i=e.children("."+T.ELEM_ITEM+"-preview");return t.remove(),i.remove(),e[0]&&u.unwrap(),b}(),r.preview&&(M="LAY-CODE-DF-"+n,h=r.layout||["code","preview"],c="iframe"===r.preview,E=A('
              '),_=A('
              '),s=A('
              '),g=A('
              '),d=A('
              '),r.id&&E.attr("id",r.id),E.addClass(r.className),_.attr("lay-filter",M),layui.each(h,function(e,t){var i=A('
            • ');0===e&&i.addClass("layui-this"),i.html(r.text[t]),s.append(i)}),A.extend(o,{full:{className:"screen-full",title:[W.$t("code.maximize"),W.$t("code.restore")],event:function(e){var e=e.elem,t=e.closest("."+T.ELEM_PREVIEW),i="layui-icon-"+this.className,a="layui-icon-screen-restore",l=this.title,n=A("html,body"),o="layui-scrollbar-hide";e.hasClass(i)?(t.addClass(T.ELEM_FULL),e.removeClass(i).addClass(a),e.attr("title",l[1]),n.addClass(o)):(t.removeClass(T.ELEM_FULL),e.removeClass(a).addClass(i),e.attr("title",l[0]),n.removeClass(o))}},window:{className:"release",title:[W.$t("code.preview")],event:function(e){x.openWin({content:l(r.code)})}}}),r.copy&&("array"===layui.type(r.tools)?-1===r.tools.indexOf("copy")&&r.tools.unshift("copy"):r.tools=["copy"]),d.on("click",">i",function(){var e=A(this),t=e.data("type"),e={elem:e,type:t,options:r,rawCode:r.code,finalCode:x.unescape(l(r.code))};o[t]&&"function"==typeof o[t].event&&o[t].event(e),"function"==typeof r.toolsEvent&&r.toolsEvent(e)}),r.addTools&&r.tools&&(r.tools=[].concat(r.tools,r.addTools)),layui.each(r.tools,function(e,t){var i="object"==typeof t,a=i?t:o[t]||{className:t,title:[t]},l=a.className||a.type,n=a.title||[""],i=i?a.type||l:t;i&&(o[i]||((t={})[i]=a,A.extend(o,t)),d.append(''))}),u.addClass(T.ELEM_ITEM).wrap(E),_.append(s),r.tools&&_.append(d),u.before(_),c&&g.html(''),y=function(e){var t=e.children("iframe")[0];c&&t?t.srcdoc=l(r.code):e.html(r.code),setTimeout(function(){"function"==typeof r.done&&r.done({container:e,options:r,render:function(){S.render(e.find(".layui-form")),I.render(),D.render({elem:["."+T.ELEM_PREVIEW,".layui-tabs"].join(" ")})}})},3)},"preview"===h[0]?(g.addClass(T.ELEM_SHOW),u.before(g),y(g)):u.addClass(T.ELEM_SHOW).after(g),r.previewStyle=[r.style,r.previewStyle].join(""),g.attr("style",r.previewStyle),I.on("tab("+M+")",function(e){var t=A(this),i=A(e.elem).closest("."+T.ELEM_PREVIEW).find("."+T.ELEM_ITEM),e=i.eq(e.index);i.removeClass(T.ELEM_SHOW),e.addClass(T.ELEM_SHOW),"preview"===t.attr("lay-id")&&y(e),L()})),p=A(''),u.addClass((E=["layui-code-view layui-border-box"],r.wordWrap||E.push("layui-code-nowrap"),E.join(" "))),(_=r.theme||r.skin)&&(u.removeClass("layui-code-theme-dark layui-code-theme-light"),u.addClass("layui-code-theme-"+_)),r.highlighter&&u.addClass([r.highlighter,"language-"+r.lang,"layui-code-hl"].join(" ")),h=w(r.encode?x.escape(l(i)):i),f=h.lines,u.html(p.html(h.html)),r.ln&&u.append('
              '),r.height&&p.css("max-height",r.height),r.codeStyle=[r.style,r.codeStyle].join(""),r.codeStyle&&p.attr("style",function(e,t){return(t||"")+r.codeStyle}),v=[{selector:">.layui-code-wrap>.layui-code-line{}",setValue:function(e,t){e.style["padding-left"]=t+"px"}},{selector:">.layui-code-wrap>.layui-code-line>.layui-code-line-number{}",setValue:function(e,t){e.style.width=t+"px"}},{selector:">.layui-code-ln-side{}",setValue:function(e,t){e.style.width=t+"px"}}],m=lay.style({target:u[0],id:"DF-code-"+n,text:A.map(A.map(v,function(e){return e.selector}),function(e,t){return['.layui-code-view[lay-code-index="'+n+'"]',e].join(" ")}).join("")}),L=function b(){var e,a;return r.ln&&(e=Math.floor(f.length/100),a=p.children("."+T.ELEM_LINE).last().children("."+T.ELEM_LINE_NUM).outerWidth(),u.addClass(T.ELEM_LN_MODE),e)&&a>T.LINE_RAW_WIDTH&&lay.getStyleRules(m,function(e,t){try{v[t].setValue(e,a)}catch(i){}}),b}(),r.header&&((g=A('
              ')).html(r.title||r.text.code),u.prepend(g)),M=A('
              '),r.copy&&!r.preview&&((_=A(['','',""].join(""))).on("click",function(){o.copy.event()}),M.append(_)),r.langMarker&&M.append(''+r.lang+""),r.about&&M.append(r.about),u.append(M),r.preview||setTimeout(function(){"function"==typeof r.done&&r.done({})},3),r.elem.length===1+n&&"function"==typeof r.allDone&&r.allDone())),C})}),layui["layui.all"]||layui.addcss("modules/code.css?v=6","skincodecss"); \ No newline at end of file diff --git a/plugin/think-plugs-static/stc/public/static/plugs/layui_exts/excel.js b/plugin/think-plugs-static/stc/public/static/plugs/layui_exts/excel.js new file mode 100644 index 000000000..34f91861e --- /dev/null +++ b/plugin/think-plugs-static/stc/public/static/plugs/layui_exts/excel.js @@ -0,0 +1,10 @@ +!function(e){var t={};function r(n){if(t[n])return t[n].exports;var a=t[n]={i:n,l:!1,exports:{}};return e[n].call(a.exports,a,a.exports,r),a.l=!0,a.exports}r.m=e,r.c=t,r.d=function(e,t,n){r.o(e,t)||Object.defineProperty(e,t,{configurable:!1,enumerable:!0,get:n})},r.r=function(e){Object.defineProperty(e,"__esModule",{value:!0})},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p="",r(r.s=343)}([function(e,t,r){var n=r(2),a=r(22),i=r(13),s=r(12),o=r(21),c=function(e,t,r){var l,f,u,h,d=e&c.F,p=e&c.G,g=e&c.S,m=e&c.P,v=e&c.B,b=p?n:g?n[t]||(n[t]={}):(n[t]||{}).prototype,w=p?a:a[t]||(a[t]={}),E=w.prototype||(w.prototype={});for(l in p&&(r=t),r)u=((f=!d&&b&&void 0!==b[l])?b:r)[l],h=v&&f?o(u,n):m&&"function"==typeof u?o(Function.call,u):u,b&&s(b,l,u,e&c.U),w[l]!=u&&i(w,l,h),m&&E[l]!=u&&(E[l]=u)};n.core=a,c.F=1,c.G=2,c.S=4,c.P=8,c.B=16,c.W=32,c.U=64,c.R=128,e.exports=c},function(e,t,r){var n=r(4);e.exports=function(e){if(!n(e))throw TypeError(e+" is not an object!");return e}},function(e,t){var r=e.exports="undefined"!=typeof window&&window.Math==Math?window:"undefined"!=typeof self&&self.Math==Math?self:Function("return this")();"number"==typeof __g&&(__g=r)},function(e,t){e.exports=function(e){try{return!!e()}catch(e){return!0}}},function(e,t){e.exports=function(e){return"object"==typeof e?null!==e:"function"==typeof e}},function(e,t,r){var n=r(52)("wks"),a=r(41),i=r(2).Symbol,s="function"==typeof i;(e.exports=function(e){return n[e]||(n[e]=s&&i[e]||(s?i:a)("Symbol."+e))}).store=n},function(e,t,r){var n=r(19),a=Math.min;e.exports=function(e){return e>0?a(n(e),9007199254740991):0}},function(e,t,r){e.exports=!r(3)(function(){return 7!=Object.defineProperty({},"a",{get:function(){return 7}}).a})},function(e,t,r){var n=r(1),a=r(130),i=r(26),s=Object.defineProperty;t.f=r(7)?Object.defineProperty:function(e,t,r){if(n(e),t=i(t,!0),n(r),a)try{return s(e,t,r)}catch(e){}if("get"in r||"set"in r)throw TypeError("Accessors not supported!");return"value"in r&&(e[t]=r.value),e}},function(e,t,r){var n=r(25);e.exports=function(e){return Object(n(e))}},function(e,t){e.exports=function(e){if("function"!=typeof e)throw TypeError(e+" is not a function!");return e}},function(e,t,r){var n=r(0),a=r(3),i=r(25),s=/"/g,o=function(e,t,r,n){var a=String(i(e)),o="<"+t;return""!==r&&(o+=" "+r+'="'+String(n).replace(s,""")+'"'),o+">"+a+""};e.exports=function(e,t){var r={};r[e]=t(o),n(n.P+n.F*a(function(){var t=""[e]('"');return t!==t.toLowerCase()||t.split('"').length>3}),"String",r)}},function(e,t,r){var n=r(2),a=r(13),i=r(17),s=r(41)("src"),o=r(339),c=(""+o).split("toString");r(22).inspectSource=function(e){return o.call(e)},(e.exports=function(e,t,r,o){var l="function"==typeof r;l&&(i(r,"name")||a(r,"name",t)),e[t]!==r&&(l&&(i(r,s)||a(r,s,e[t]?""+e[t]:c.join(String(t)))),e===n?e[t]=r:o?e[t]?e[t]=r:a(e,t,r):(delete e[t],a(e,t,r)))})(Function.prototype,"toString",function(){return"function"==typeof this&&this[s]||o.call(this)})},function(e,t,r){var n=r(8),a=r(42);e.exports=r(7)?function(e,t,r){return n.f(e,t,a(1,r))}:function(e,t,r){return e[t]=r,e}},function(e,t,r){var n=r(17),a=r(9),i=r(91)("IE_PROTO"),s=Object.prototype;e.exports=Object.getPrototypeOf||function(e){return e=a(e),n(e,i)?e[i]:"function"==typeof e.constructor&&e instanceof e.constructor?e.constructor.prototype:e instanceof Object?s:null}},function(e,t,r){var n=r(50),a=r(42),i=r(16),s=r(26),o=r(17),c=r(130),l=Object.getOwnPropertyDescriptor;t.f=r(7)?l:function(e,t){if(e=i(e),t=s(t,!0),c)try{return l(e,t)}catch(e){}if(o(e,t))return a(!n.f.call(e,t),e[t])}},function(e,t,r){var n=r(51),a=r(25);e.exports=function(e){return n(a(e))}},function(e,t){var r={}.hasOwnProperty;e.exports=function(e,t){return r.call(e,t)}},function(e,t,r){"use strict";var n=r(3);e.exports=function(e,t){return!!e&&n(function(){t?e.call(null,function(){},1):e.call(null)})}},function(e,t){var r=Math.ceil,n=Math.floor;e.exports=function(e){return isNaN(e=+e)?0:(e>0?n:r)(e)}},function(e,t){var r={}.toString;e.exports=function(e){return r.call(e).slice(8,-1)}},function(e,t,r){var n=r(10);e.exports=function(e,t,r){if(n(e),void 0===t)return e;switch(r){case 1:return function(r){return e.call(t,r)};case 2:return function(r,n){return e.call(t,r,n)};case 3:return function(r,n,a){return e.call(t,r,n,a)}}return function(){return e.apply(t,arguments)}}},function(e,t){var r=e.exports={version:"2.6.11"};"number"==typeof __e&&(__e=r)},function(e,t,r){var n=r(21),a=r(51),i=r(9),s=r(6),o=r(75);e.exports=function(e,t){var r=1==e,c=2==e,l=3==e,f=4==e,u=6==e,h=5==e||u,d=t||o;return function(t,o,p){for(var g,m,v=i(t),b=a(v),w=n(o,p,3),E=s(b.length),S=0,y=r?d(t,E):c?d(t,0):void 0;E>S;S++)if((h||S in b)&&(m=w(g=b[S],S,v),e))if(r)y[S]=m;else if(m)switch(e){case 3:return!0;case 5:return g;case 6:return S;case 2:y.push(g)}else if(f)return!1;return u?-1:l||f?f:y}}},function(e,t,r){var n=r(0),a=r(22),i=r(3);e.exports=function(e,t){var r=(a.Object||{})[e]||Object[e],s={};s[e]=t(r),n(n.S+n.F*i(function(){r(1)}),"Object",s)}},function(e,t){e.exports=function(e){if(void 0==e)throw TypeError("Can't call method on "+e);return e}},function(e,t,r){var n=r(4);e.exports=function(e,t){if(!n(e))return e;var r,a;if(t&&"function"==typeof(r=e.toString)&&!n(a=r.call(e)))return a;if("function"==typeof(r=e.valueOf)&&!n(a=r.call(e)))return a;if(!t&&"function"==typeof(r=e.toString)&&!n(a=r.call(e)))return a;throw TypeError("Can't convert object to primitive value")}},function(e,t,r){var n=r(107),a=r(0),i=r(52)("metadata"),s=i.store||(i.store=new(r(104))),o=function(e,t,r){var a=s.get(e);if(!a){if(!r)return;s.set(e,a=new n)}var i=a.get(t);if(!i){if(!r)return;a.set(t,i=new n)}return i};e.exports={store:s,map:o,has:function(e,t,r){var n=o(t,r,!1);return void 0!==n&&n.has(e)},get:function(e,t,r){var n=o(t,r,!1);return void 0===n?void 0:n.get(e)},set:function(e,t,r,n){o(r,n,!0).set(e,t)},keys:function(e,t){var r=o(e,t,!1),n=[];return r&&r.forEach(function(e,t){n.push(t)}),n},key:function(e){return void 0===e||"symbol"==typeof e?e:String(e)},exp:function(e){a(a.S,"Reflect",e)}}},function(e,t,r){"use strict";if(r(7)){var n=r(31),a=r(2),i=r(3),s=r(0),o=r(56),c=r(67),l=r(21),f=r(35),u=r(42),h=r(13),d=r(33),p=r(19),g=r(6),m=r(102),v=r(39),b=r(26),w=r(17),E=r(45),S=r(4),y=r(9),_=r(78),C=r(38),x=r(14),T=r(37).f,B=r(76),k=r(41),A=r(5),I=r(23),R=r(66),O=r(48),F=r(73),P=r(43),D=r(61),N=r(36),M=r(74),L=r(113),U=r(8),W=r(15),V=U.f,H=W.f,z=a.RangeError,X=a.TypeError,G=a.Uint8Array,j=Array.prototype,Y=c.ArrayBuffer,$=c.DataView,K=I(0),Z=I(2),J=I(3),Q=I(4),q=I(5),ee=I(6),te=R(!0),re=R(!1),ne=F.values,ae=F.keys,ie=F.entries,se=j.lastIndexOf,oe=j.reduce,ce=j.reduceRight,le=j.join,fe=j.sort,ue=j.slice,he=j.toString,de=j.toLocaleString,pe=A("iterator"),ge=A("toStringTag"),me=k("typed_constructor"),ve=k("def_constructor"),be=o.CONSTR,we=o.TYPED,Ee=o.VIEW,Se=I(1,function(e,t){return Te(O(e,e[ve]),t)}),ye=i(function(){return 1===new G(new Uint16Array([1]).buffer)[0]}),_e=!!G&&!!G.prototype.set&&i(function(){new G(1).set({})}),Ce=function(e,t){var r=p(e);if(r<0||r%t)throw z("Wrong offset!");return r},xe=function(e){if(S(e)&&we in e)return e;throw X(e+" is not a typed array!")},Te=function(e,t){if(!(S(e)&&me in e))throw X("It is not a typed array constructor!");return new e(t)},Be=function(e,t){return ke(O(e,e[ve]),t)},ke=function(e,t){for(var r=0,n=t.length,a=Te(e,n);n>r;)a[r]=t[r++];return a},Ae=function(e,t,r){V(e,t,{get:function(){return this._d[r]}})},Ie=function(e){var t,r,n,a,i,s,o=y(e),c=arguments.length,f=c>1?arguments[1]:void 0,u=void 0!==f,h=B(o);if(void 0!=h&&!_(h)){for(s=h.call(o),n=[],t=0;!(i=s.next()).done;t++)n.push(i.value);o=n}for(u&&c>2&&(f=l(f,arguments[2],2)),t=0,r=g(o.length),a=Te(this,r);r>t;t++)a[t]=u?f(o[t],t):o[t];return a},Re=function(){for(var e=0,t=arguments.length,r=Te(this,t);t>e;)r[e]=arguments[e++];return r},Oe=!!G&&i(function(){de.call(new G(1))}),Fe=function(){return de.apply(Oe?ue.call(xe(this)):xe(this),arguments)},Pe={copyWithin:function(e,t){return L.call(xe(this),e,t,arguments.length>2?arguments[2]:void 0)},every:function(e){return Q(xe(this),e,arguments.length>1?arguments[1]:void 0)},fill:function(e){return M.apply(xe(this),arguments)},filter:function(e){return Be(this,Z(xe(this),e,arguments.length>1?arguments[1]:void 0))},find:function(e){return q(xe(this),e,arguments.length>1?arguments[1]:void 0)},findIndex:function(e){return ee(xe(this),e,arguments.length>1?arguments[1]:void 0)},forEach:function(e){K(xe(this),e,arguments.length>1?arguments[1]:void 0)},indexOf:function(e){return re(xe(this),e,arguments.length>1?arguments[1]:void 0)},includes:function(e){return te(xe(this),e,arguments.length>1?arguments[1]:void 0)},join:function(e){return le.apply(xe(this),arguments)},lastIndexOf:function(e){return se.apply(xe(this),arguments)},map:function(e){return Se(xe(this),e,arguments.length>1?arguments[1]:void 0)},reduce:function(e){return oe.apply(xe(this),arguments)},reduceRight:function(e){return ce.apply(xe(this),arguments)},reverse:function(){for(var e,t=xe(this).length,r=Math.floor(t/2),n=0;n1?arguments[1]:void 0)},sort:function(e){return fe.call(xe(this),e)},subarray:function(e,t){var r=xe(this),n=r.length,a=v(e,n);return new(O(r,r[ve]))(r.buffer,r.byteOffset+a*r.BYTES_PER_ELEMENT,g((void 0===t?n:v(t,n))-a))}},De=function(e,t){return Be(this,ue.call(xe(this),e,t))},Ne=function(e){xe(this);var t=Ce(arguments[1],1),r=this.length,n=y(e),a=g(n.length),i=0;if(a+t>r)throw z("Wrong length!");for(;i255?255:255&n),a.v[d](r*t+a.o,n,ye)}(this,r,e)},enumerable:!0})};w?(p=r(function(e,r,n,a){f(e,p,l,"_d");var i,s,o,c,u=0,d=0;if(S(r)){if(!(r instanceof Y||"ArrayBuffer"==(c=E(r))||"SharedArrayBuffer"==c))return we in r?ke(p,r):Ie.call(p,r);i=r,d=Ce(n,t);var v=r.byteLength;if(void 0===a){if(v%t)throw z("Wrong length!");if((s=v-d)<0)throw z("Wrong length!")}else if((s=g(a)*t)+d>v)throw z("Wrong length!");o=s/t}else o=m(r),i=new Y(s=o*t);for(h(e,"_d",{b:i,o:d,l:s,e:o,v:new $(i)});uw;w++)if((m=t?b(s(p=e[w])[0],p[1]):b(e[w]))===l||m===f)return m}else for(g=v.call(e);!(p=g.next()).done;)if((m=a(g,b,p.value,t))===l||m===f)return m}).BREAK=l,t.RETURN=f},function(e,t){e.exports=function(e,t,r,n){if(!(e instanceof t)||void 0!==n&&n in e)throw TypeError(r+": incorrect invocation!");return e}},function(e,t,r){"use strict";var n=r(2),a=r(8),i=r(7),s=r(5)("species");e.exports=function(e){var t=n[e];i&&t&&!t[s]&&a.f(t,s,{configurable:!0,get:function(){return this}})}},function(e,t,r){var n=r(128),a=r(90).concat("length","prototype");t.f=Object.getOwnPropertyNames||function(e){return n(e,a)}},function(e,t,r){var n=r(1),a=r(127),i=r(90),s=r(91)("IE_PROTO"),o=function(){},c=function(){var e,t=r(93)("iframe"),n=i.length;for(t.style.display="none",r(89).appendChild(t),t.src="javascript:",(e=t.contentWindow.document).open(),e.write(" + Excel.prototype.bind = function (done, filename, selector, options) { + let that = this; + this.options = $.extend(this.options, options || {}); + this.bindLoadDone(function (data, button) { + that.export(done.call(that, data, []), button.dataset.filename || filename); + }, selector); + }; + + /*! 加载所有数据 */ + Excel.prototype.bindLoadDone = function (done, selector) { + let that = this; + $('body').off('click', selector || '[data-form-export]').on('click', selector || '[data-form-export]', function () { + let button = this, form = $(this).parents('form'); + let method = this.dataset.method || form.attr('method') || 'get'; + let location = this.dataset.excel || this.dataset.formExport || form.attr('action') || ''; + let sortType = $(this).attr('data-sort-type') || '', sortField = $(this).attr('data-sort-field') || ''; + if (sortField.length > 0 && sortType.length > 0) { + location += (location.indexOf('?') > -1 ? '&' : '?') + '_order_=' + sortType + '&_field_=' + sortField; + } + that.load(location, form.serialize(), method).then((data) => done.call(that, data, button)).fail(function (ret) { + $.msg.tips(ret || '数据加载失败'); + }); + }); + } + + /*! 加载导出的文档 */ + Excel.prototype.load = function (url, data, method) { + return (function (defer, lists, loaded) { + loaded = $.msg.loading('正在加载 0.00%'); + return (lists = []), LoadNextPage(1, 1), defer; + + function LoadNextPage(curPage, maxPage, urlParams) { + let proc = (curPage / maxPage * 100).toFixed(2); + $('[data-upload-count]').html(proc > 100 ? '100.00' : proc); + if (curPage > maxPage) return $.msg.close(loaded), defer.resolve(lists); + urlParams = (url.indexOf('?') > -1 ? '&' : '?') + 'output=json¬_cache_limit=1&limit=100&page=' + curPage; + $.form.load(url + urlParams, data, method, function (ret) { + if (parseInt(ret.code, 10) === 200) { + lists = lists.concat(ret.data.list); + if (ret.data.page) LoadNextPage((ret.data.page.current || 1) + 1, ret.data.page.pages || 1); + } else { + defer.reject('数据加载异常'); + } + return false; + }, false); + } + })($.Deferred()); + }; + + /*! 设置表格导出样式 */ + // this.withStyle(data, {A: 60, B: 80, C: 99, E: 120, G: 120}, 99, 28) + Excel.prototype.withStyle = function (data, colsWidth, defaultWidth, defaultHeight) { + // 自动计算列序 + let idx, colN = 0, defaC = {}, lastCol; + for (idx in data[0]) defaC[lastCol = layui.excel.numToTitle(++colN)] = defaultWidth || 99; + defaC[lastCol] = 160; + + // 设置表头样式 + layui.excel.setExportCellStyle(data, 'A1:' + lastCol + '1', { + s: { + font: {sz: 12, bold: true, color: {rgb: "FFFFFF"}, name: '微软雅黑', shadow: true}, + fill: {bgColor: {indexed: 64}, fgColor: {rgb: '5FB878'}}, + alignment: {vertical: 'center', horizontal: 'center'} + } + }); + + // 设置内容样式 + (function (style1, style2) { + layui.excel.setExportCellStyle(data, 'A2:' + lastCol + data.length, {s: style1}, function (rawCell, newCell, row, config, curRow) { + typeof rawCell !== 'object' && (rawCell = {v: rawCell}); + rawCell.s = Object.assign({}, style2, rawCell.s || {}); + return (curRow % 2 === 0) ? newCell : rawCell; + }); + })({ + font: {sz: 10, shadow: true, name: '微软雅黑'}, + fill: {bgColor: {indexed: 64}, fgColor: {rgb: "EAEAEA"}}, + alignment: {vertical: 'center', horizontal: 'center'} + }, { + font: {sz: 10, shadow: true, name: '微软雅黑'}, + fill: {bgColor: {indexed: 64}, fgColor: {rgb: "FFFFFF"}}, + alignment: {vertical: 'center', horizontal: 'center'} + }); + + // 设置表格行宽高,需要设置最后的行或列宽高,否则部分不生效 ??? + let rowsC = {1: 33}, colsC = Object.assign({}, defaC, {A: 60}, colsWidth || {}); + rowsC[data.length] = defaultHeight || 28, this.options.extend = Object.assign({}, { + '!cols': layui.excel.makeColConfig(colsC, defaultWidth || 99), + '!rows': layui.excel.makeRowConfig(rowsC, defaultHeight || 28), + }, this.options.extend || {}); + return data; + } + + /*! 直接推送表格内容 */ + // url: 记录推送地址 + // sheet: 表格 Sheet 名称 + // cols: { _: 1, 表头名1: 字段名1, 表头名2: 字段名2 },其中字段 _ 配置起始行 + // filter: 数据过滤处理,如果返回 false 不上传记录 + // Excel.push(ACTION, '用户信息', {_:1, username: "用户名称", phone: "联系手机"}) + Excel.prototype.push = function (url, sheet, cols, filter) { + let loaded, $input; + $input = $(''); + $input.appendTo($('body')).click().on('change', function (event) { + if (!event.target.files || event.target.files.length < 1) return $.msg.tips('没有可操作文件'); + loaded = $.msg.loading('读取 0.00%'); + try { + // 导入Excel数据,并逐行上传处理 + layui.excel.importExcel(event.target.files, {}, function (data) { + if (!data[0][sheet]) return $.msg.tips('未读取到表[' + sheet + ']的数据'); + let _cols = {}, _data = data[0][sheet], items = [], row, col, key, item; + for (row in _data) if (parseInt(row) + 1 === parseInt(cols._ || '1')) { + for (col in _data[row]) for (key in cols) if (_data[row][col] === cols[key]) _cols[key] = col; + } else if (parseInt(row) + 1 > cols._ || 1) { + item = {}; + for (key in _cols) item[key] = CellToValue(_data[row][_cols[key]]); + items.push(item); + } + PushQueue(items, items.length, 0, 0, 1); + }); + } catch (e) { + $.msg.error('读取 Excel 文件失败!') + } + }); + + /*! 单项推送数据 */ + function PushQueue(items, total, ers, oks, idx) { + if ((total = items.length) < 1) return CleanAll(), $.msg.tips('未读取到有效数据'); + return (ers = 0, oks = 0, idx = 0), $('[data-load-name]').html('更新'), DoPostItem(idx, items[idx]); + + /*! 执行导入的数据 */ + function DoPostItem(idx, item, data) { + if (idx >= total) { + return CleanAll(), $.msg.success('共处理' + total + '条记录( 成功 ' + oks + ' 条, 失败 ' + ers + ' 条 )', 3, function () { + $.form.reload(); + }); + } else { + let proc = (idx * 100 / total).toFixed(2); + $('[data-load-count]').html((proc > 100 ? '100.00' : proc) + '%( 成功 ' + oks + ' 条, 失败 ' + ers + ' 条 )'); + /*! 单元数据过滤 */ + data = item; + if (filter && (data = filter(item)) === false) { + return (ers++), DoPostItem(idx + 1, items[idx + 1]); + } + /*! 提交单个数据 */ + DoUpdate(url, data).then(function (ret) { + (parseInt(ret.code, 10) === 200 ? oks++ : ers++), DoPostItem(idx + 1, items[idx + 1]); + }); + } + } + } + + /*! 清理文件选择器 */ + function CleanAll() { + $input.remove(); + if (loaded) $.msg.close(loaded); + } + + /*! 表格单元内容转换 */ + function CellToValue(v) { + if (typeof v !== 'undefined' && /^\d+\.\d{12}$/.test(v)) { + return LAY_EXCEL.dateCodeFormat(v, 'YYYY-MM-DD HH:ii:ss'); + } else { + return typeof v !== 'undefined' ? v : ''; + } + } + + /*! 队列方式上传数据 */ + function DoUpdate(url, item) { + return (function (defer) { + return $.form.load(url, item, 'post', function (ret) { + return defer.resolve(ret), false; + }, false), defer.promise(); + })($.Deferred()); + } + } + + /*! 返回对象实例 */ + exports('taExcel', new Excel); +}); diff --git a/plugin/think-plugs-static/stc/public/static/plugs/system/queue.js b/plugin/think-plugs-static/stc/public/static/plugs/system/queue.js new file mode 100644 index 000000000..f7a56ba83 --- /dev/null +++ b/plugin/think-plugs-static/stc/public/static/plugs/system/queue.js @@ -0,0 +1,114 @@ +// +---------------------------------------------------------------------- +// | Static Plugin for ThinkAdmin +// +---------------------------------------------------------------------- +// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ] +// +---------------------------------------------------------------------- +// | 官方网站: https://thinkadmin.top +// +---------------------------------------------------------------------- +// | 开源协议 ( https://mit-license.org ) +// | 免责声明 ( https://thinkadmin.top/disclaimer ) +// +---------------------------------------------------------------------- +// | gitee 代码仓库:https://gitee.com/zoujingli/think-plugs-static +// | github 代码仓库:https://github.com/zoujingli/think-plugs-static +// +---------------------------------------------------------------------- + +layui.define(function (exports) { + + let template = '
              ...
              ' + '
              '; + + function Queue(code, doScript, element) { + let queue = this; + (this.doAjax = true) && (this.doReload = false) || layer.open({ + type: 1, title: false, area: ['560px', '315px'], anim: 2, shadeClose: false, end: function () { + queue.doAjax = queue.doReload && doScript && $.layTable.reload(((element || {}).dataset || {}).tableId || true) && false; + }, content: laytpl(template).render({code: code}), success: function ($elem) { + new Progress($elem, code, queue, doScript); + } + }); + } + + function Progress($elem, code, queue, doScript) { + let that = this; + + this.$box = $elem.find('[data-queue-load=' + code + ']'); + if (queue.doAjax === false || this.$box.length < 1) return false; + + this.$code = this.$box.find('code'); + this.$title = this.$box.find('[data-message-title]'); + this.$percent = this.$box.find('.layui-progress div'); + + // 设置数据缓存 + this.SetCache = function (code, index, value) { + let ckey = code + '_' + index, ctype = 'admin-queue-script'; + return value !== undefined ? layui.data(ctype, {key: ckey, value: value}) : layui.data(ctype)[ckey] || 0; + }; + + // 更新任务显示状态 + this.SetState = function (status, message) { + if (message.indexOf('javascript:') === -1) if (status === 1) { + that.$title.html('' + message + '').addClass('text-center'); + that.$percent.addClass('layui-bg-blue').removeClass('layui-bg-green layui-bg-red'); + } else if (status === 2) { + if (message.indexOf('>>>') > -1) { + that.$title.html('' + message + '').addClass('text-center'); + } else { + that.$title.html('正在处理:' + message).removeClass('text-center'); + } + that.$percent.addClass('layui-bg-blue').removeClass('layui-bg-green layui-bg-red'); + } else if (status === 3) { + queue.doReload = true; + that.$title.html('' + message + '').addClass('text-center'); + that.$percent.addClass('layui-bg-green').removeClass('layui-bg-blue layui-bg-red'); + } else if (status === 4) { + that.$title.html('' + message + '').addClass('text-center'); + that.$percent.addClass('layui-bg-red').removeClass('layui-bg-blue layui-bg-green'); + } + }; + + // 读取任务进度信息 + this.LoadProgress = function () { + if (queue.doAjax === false || that.$box.length < 1) return false; + $.form.load(tapiRoot + '/queue/progress', {code: code}, 'post', function (ret) { + if (parseInt(ret.code, 10) === 200) { + let data = ret && typeof ret.data === 'object' && ret.data ? ret.data : {}; + let status = parseInt(data.status || '0'); + let progress = parseFloat(data.progress || '0.00'); + let message = typeof data.message === 'string' && data.message.length ? data.message : '>>> 等待任务状态更新 <<<'; + let history = Array.isArray(data.history) ? data.history : []; + let lines = []; + for (let idx in history) { + let line = history[idx] || {}, text = String(line.message || ''), percent = '[ ' + (line.progress || '0.00') + '% ] '; + if (!text.length) { + continue; + } + if (text.indexOf('javascript:') === -1) { + lines.push(text.indexOf('>>>') > -1 ? text : percent + text); + } else if (!that.SetCache(code, idx) && doScript !== false) { + that.SetCache(code, idx, 1) + $.form.goto(text); + } + } + if (!isFinite(progress)) { + progress = 0; + } + that.$code.html(lines.length ? '

              ' + lines.join('

              ') + '

              ' : '

              暂无执行日志

              ').animate({scrollTop: that.$code[0].scrollHeight + 'px'}, 200); + if (status > 0) { + that.SetState(status, message); + that.$percent.attr('lay-percent', progress.toFixed(2) + '%') && layui.element.render(); + status === 3 || status === 4 || setTimeout(that.LoadProgress, Math.floor(Math.random() * 200)); + } else { + that.$title.html('' + message + '').addClass('text-center'); + that.$percent.attr('lay-percent', progress.toFixed(2) + '%') && layui.element.render(); + setTimeout(that.LoadProgress, Math.floor(Math.random() * 500) + 200); + } + return false; + } + }, false); + }; + + // 首页加载进度信息 + this.LoadProgress(); + } + + exports('taQueue', Queue); +}); diff --git a/plugin/think-plugs-static/stc/public/static/plugs/system/validate.js b/plugin/think-plugs-static/stc/public/static/plugs/system/validate.js new file mode 100644 index 000000000..a5101185b --- /dev/null +++ b/plugin/think-plugs-static/stc/public/static/plugs/system/validate.js @@ -0,0 +1,126 @@ +// +---------------------------------------------------------------------- +// | Static Plugin for ThinkAdmin +// +---------------------------------------------------------------------- +// | 版权所有 2014~2024 ThinkAdmin [ thinkadmin.top ] +// +---------------------------------------------------------------------- +// | 官方网站: https://thinkadmin.top +// +---------------------------------------------------------------------- +// | 开源协议 ( https://mit-license.org ) +// | 免责声明 ( https://thinkadmin.top/disclaimer ) +// +---------------------------------------------------------------------- +// | gitee 代码仓库:https://gitee.com/zoujingli/think-plugs-static +// | github 代码仓库:https://github.com/zoujingli/think-plugs-static +// +---------------------------------------------------------------------- + +layui.define(function (exports) { + + function Validate(form) { + let that = this; + // 绑定表单元素 + this.form = $(form); + // 绑定元素事件 + this.evts = 'blur change'; + // 检测表单元素 + this.tags = 'input,textarea'; + // 验证成功回调 + this.dones = []; + // 预设检测规则 + this.patterns = { + qq: '^[1-9][0-9]{4,11}$', + ip: '^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$', + url: '^https?://([a-zA-Z0-9-]+\\.)+[-_a-zA-Z0-9-]+', + phone: '^1[3-9][0-9]{9}$', + mobile: '^1[3-9][0-9]{9}$', + email: '^([a-zA-Z0-9_\\.-])+@(([a-zA-Z0-9-])+\\.)+([a-zA-Z0-9]{2,4})+$', + wechat: '^[a-zA-Z]([-_a-zA-Z0-9]{5,19})+$', + cardid: '^[1-9]\d{5}(18|19|([23]\d))\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$', + userame: '^[a-zA-Z0-9_-]{4,16}$', + }; + // 设置完成回调 + this.addDoneEvent = function (done) { + if (typeof done === 'function') this.dones.push(done); + }; + this.isRegex = function (el, value, pattern) { + pattern = pattern || el.getAttribute('pattern'); + if ((value = value || $.trim($(el).val())) === '') return true; + if (!(pattern = this.patterns[pattern] || pattern)) return true; + return new RegExp(pattern, 'i').test(value); + }; + this.hasProp = function (el, prop) { + let attrProp = el.getAttribute(prop); + return typeof attrProp !== 'undefined' && attrProp !== null && attrProp !== false; + }; + this.hasCheck = function (el, type) { + if (this.hasProp(el, 'data-auto-none')) return false; + type = (el.getAttribute('type') || '').replace(/\W+/, '').toLowerCase(); + return $.inArray(type, ['file', 'reset', 'image', 'radio', 'checkbox', 'submit', 'hidden']) < 0; + }; + this.checkAllInput = function () { + let status = true; + return this.form.find(this.tags).each(function () { + !that.checkInput(this) && status && (status = !$(this).focus()); + }) && status; + }; + this.checkInput = function (el) { + if (!this.hasCheck(el = typeof el === 'string' ? form[el] : el)) return true; + if (this.hasProp(el, 'required') && $.trim($(el).val()) === '') return this.remind(el, 'required'); + return this.isRegex(el) ? !!this.hideError(el) : this.remind(el, 'pattern'); + }; + this.remind = function (el, type, tips) { + return $(el).is(':visible') ? this.showError(el, tips || el.getAttribute(type + '-error') || function (name, tips) { + return name ? name + (type === 'required' ? '不能为空' : "格式错误") : (tips || el.getAttribute('placeholder') || '输入格式错误'); + }(el.getAttribute('vali-name') || el.getAttribute('data-vali-name'), el.getAttribute('title'))) && false : true; + }; + this.showError = function (el, tip) { + return this.insertError($(el).addClass('validate-error')).addClass('layui-anim-fadein').css({width: 'auto'}).html(tip); + }; + this.hideError = function (el) { + return this.insertError($(el).removeClass('validate-error')).removeClass('layui-anim-fadein').css({width: '30px'}).html(''); + }; + this.insertError = function ($el) { + return (function ($icon) { + return $el.data('vali-tags').css({ + top: $el.position().top + 'px', right: (($icon ? $icon.width() + parseFloat($icon.css('right') || 0) : 0) + 10) + 'px', + paddingTop: $el.css('marginTop'), lineHeight: ($el.get(0).nodeName || '') === 'TEXTAREA' ? '32px' : $el.css('height'), + }); + })($el.nextAll('.input-right-icon'), $el.data('vali-tags') || function () { + let css = 'display:block;position:absolute;text-align:center;color:#c44;font-size:12px;z-index:2;right:8px'; + $el.data('vali-tags', $('').insertAfter($el)); + }()); + }; + /*! 预埋异常标签*/ + this.form.find(this.tags).each(function (i, el) { + that.hasCheck(this) && that.hideError(el, ''); + }); + /*! 表单元素验证 */ + this.form.attr({onsubmit: 'return false', novalidate: 'novalidate', autocomplete: 'off'}).on('keydown', this.tags, function () { + that.hideError(this) + }).off(this.evts, this.tags).on(this.evts, this.tags, function () { + that.checkInput(this); + }).data('validate', this).bind('submit', function (evt) { + evt.preventDefault(); + /* 检查所有表单元素是否通过H5的规则验证 */ + if (that.checkAllInput() && that.dones.length > 0) { + if (typeof CKEDITOR === 'object' && typeof CKEDITOR.instances === 'object') { + for (let i in CKEDITOR.instances) CKEDITOR.instances[i].updateElement(); + } + /* 触发表单提交后,锁定三秒不能再次提交表单 */ + if (that.form.attr('submit-locked')) return false; + evt.submit = that.form.find('button[type=submit],button:not([type=button])'); + $.base.onConfirm(evt.submit.attr('data-confirm'), function () { + that.form.attr('submit-locked', 1) && evt.submit.addClass('submit-button-loading'); + setTimeout(function () { + that.form.removeAttr('submit-locked') && evt.submit.removeClass('submit-button-loading'); + }, 3000) && that.dones.forEach(function (done) { + done.call(form, that.form.formToJson(), []); + }); + }); + } + }).find('[data-form-loaded]').map(function () { + $(this).html(this.dataset.formLoaded || this.innerHTML); + $(this).removeAttr('data-form-loaded').removeClass('layui-disabled'); + }); + } + + exports('taValidate', Validate); +}); diff --git a/plugin/think-plugs-static/stc/public/static/plugs/vue/vue.min.js b/plugin/think-plugs-static/stc/public/static/plugs/vue/vue.min.js new file mode 100644 index 000000000..d285e55a1 --- /dev/null +++ b/plugin/think-plugs-static/stc/public/static/plugs/vue/vue.min.js @@ -0,0 +1,6 @@ +/*! + * Vue.js v2.7.14 + * (c) 2014-2022 Evan You + * Released under the MIT License. + */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).Vue=e()}(this,(function(){"use strict";var t=Object.freeze({}),e=Array.isArray;function n(t){return null==t}function r(t){return null!=t}function o(t){return!0===t}function i(t){return"string"==typeof t||"number"==typeof t||"symbol"==typeof t||"boolean"==typeof t}function a(t){return"function"==typeof t}function s(t){return null!==t&&"object"==typeof t}var c=Object.prototype.toString;function u(t){return"[object Object]"===c.call(t)}function l(t){var e=parseFloat(String(t));return e>=0&&Math.floor(e)===e&&isFinite(t)}function f(t){return r(t)&&"function"==typeof t.then&&"function"==typeof t.catch}function d(t){return null==t?"":Array.isArray(t)||u(t)&&t.toString===c?JSON.stringify(t,null,2):String(t)}function p(t){var e=parseFloat(t);return isNaN(e)?t:e}function v(t,e){for(var n=Object.create(null),r=t.split(","),o=0;o-1)return t.splice(r,1)}}var y=Object.prototype.hasOwnProperty;function _(t,e){return y.call(t,e)}function b(t){var e=Object.create(null);return function(n){return e[n]||(e[n]=t(n))}}var $=/-(\w)/g,w=b((function(t){return t.replace($,(function(t,e){return e?e.toUpperCase():""}))})),x=b((function(t){return t.charAt(0).toUpperCase()+t.slice(1)})),C=/\B([A-Z])/g,k=b((function(t){return t.replace(C,"-$1").toLowerCase()}));var S=Function.prototype.bind?function(t,e){return t.bind(e)}:function(t,e){function n(n){var r=arguments.length;return r?r>1?t.apply(e,arguments):t.call(e,n):t.call(e)}return n._length=t.length,n};function O(t,e){e=e||0;for(var n=t.length-e,r=new Array(n);n--;)r[n]=t[n+e];return r}function T(t,e){for(var n in e)t[n]=e[n];return t}function A(t){for(var e={},n=0;n0,G=q&&q.indexOf("edge/")>0;q&&q.indexOf("android");var X=q&&/iphone|ipad|ipod|ios/.test(q);q&&/chrome\/\d+/.test(q),q&&/phantomjs/.test(q);var Y,Q=q&&q.match(/firefox\/(\d+)/),tt={}.watch,et=!1;if(J)try{var nt={};Object.defineProperty(nt,"passive",{get:function(){et=!0}}),window.addEventListener("test-passive",null,nt)}catch(t){}var rt=function(){return void 0===Y&&(Y=!J&&"undefined"!=typeof global&&(global.process&&"server"===global.process.env.VUE_ENV)),Y},ot=J&&window.__VUE_DEVTOOLS_GLOBAL_HOOK__;function it(t){return"function"==typeof t&&/native code/.test(t.toString())}var at,st="undefined"!=typeof Symbol&&it(Symbol)&&"undefined"!=typeof Reflect&&it(Reflect.ownKeys);at="undefined"!=typeof Set&&it(Set)?Set:function(){function t(){this.set=Object.create(null)}return t.prototype.has=function(t){return!0===this.set[t]},t.prototype.add=function(t){this.set[t]=!0},t.prototype.clear=function(){this.set=Object.create(null)},t}();var ct=null;function ut(t){void 0===t&&(t=null),t||ct&&ct._scope.off(),ct=t,t&&t._scope.on()}var lt=function(){function t(t,e,n,r,o,i,a,s){this.tag=t,this.data=e,this.children=n,this.text=r,this.elm=o,this.ns=void 0,this.context=i,this.fnContext=void 0,this.fnOptions=void 0,this.fnScopeId=void 0,this.key=e&&e.key,this.componentOptions=a,this.componentInstance=void 0,this.parent=void 0,this.raw=!1,this.isStatic=!1,this.isRootInsert=!0,this.isComment=!1,this.isCloned=!1,this.isOnce=!1,this.asyncFactory=s,this.asyncMeta=void 0,this.isAsyncPlaceholder=!1}return Object.defineProperty(t.prototype,"child",{get:function(){return this.componentInstance},enumerable:!1,configurable:!0}),t}(),ft=function(t){void 0===t&&(t="");var e=new lt;return e.text=t,e.isComment=!0,e};function dt(t){return new lt(void 0,void 0,void 0,String(t))}function pt(t){var e=new lt(t.tag,t.data,t.children&&t.children.slice(),t.text,t.elm,t.context,t.componentOptions,t.asyncFactory);return e.ns=t.ns,e.isStatic=t.isStatic,e.key=t.key,e.isComment=t.isComment,e.fnContext=t.fnContext,e.fnOptions=t.fnOptions,e.fnScopeId=t.fnScopeId,e.asyncMeta=t.asyncMeta,e.isCloned=!0,e}var vt=0,ht=[],mt=function(){function t(){this._pending=!1,this.id=vt++,this.subs=[]}return t.prototype.addSub=function(t){this.subs.push(t)},t.prototype.removeSub=function(t){this.subs[this.subs.indexOf(t)]=null,this._pending||(this._pending=!0,ht.push(this))},t.prototype.depend=function(e){t.target&&t.target.addDep(this)},t.prototype.notify=function(t){for(var e=this.subs.filter((function(t){return t})),n=0,r=e.length;n0&&(Yt((c=Qt(c,"".concat(a||"","_").concat(s)))[0])&&Yt(l)&&(f[u]=dt(l.text+c[0].text),c.shift()),f.push.apply(f,c)):i(c)?Yt(l)?f[u]=dt(l.text+c):""!==c&&f.push(dt(c)):Yt(c)&&Yt(l)?f[u]=dt(l.text+c.text):(o(t._isVList)&&r(c.tag)&&n(c.key)&&r(a)&&(c.key="__vlist".concat(a,"_").concat(s,"__")),f.push(c)));return f}function te(t,n,c,u,l,f){return(e(c)||i(c))&&(l=u,u=c,c=void 0),o(f)&&(l=2),function(t,n,o,i,c){if(r(o)&&r(o.__ob__))return ft();r(o)&&r(o.is)&&(n=o.is);if(!n)return ft();e(i)&&a(i[0])&&((o=o||{}).scopedSlots={default:i[0]},i.length=0);2===c?i=Xt(i):1===c&&(i=function(t){for(var n=0;n0,s=n?!!n.$stable:!a,c=n&&n.$key;if(n){if(n._normalized)return n._normalized;if(s&&o&&o!==t&&c===o.$key&&!a&&!o.$hasNormal)return o;for(var u in i={},n)n[u]&&"$"!==u[0]&&(i[u]=$e(e,r,u,n[u]))}else i={};for(var l in r)l in i||(i[l]=we(r,l));return n&&Object.isExtensible(n)&&(n._normalized=i),z(i,"$stable",s),z(i,"$key",c),z(i,"$hasNormal",a),i}function $e(t,n,r,o){var i=function(){var n=ct;ut(t);var r=arguments.length?o.apply(null,arguments):o({}),i=(r=r&&"object"==typeof r&&!e(r)?[r]:Xt(r))&&r[0];return ut(n),r&&(!i||1===r.length&&i.isComment&&!_e(i))?void 0:r};return o.proxy&&Object.defineProperty(n,r,{get:i,enumerable:!0,configurable:!0}),i}function we(t,e){return function(){return t[e]}}function xe(e){return{get attrs(){if(!e._attrsProxy){var n=e._attrsProxy={};z(n,"_v_attr_proxy",!0),Ce(n,e.$attrs,t,e,"$attrs")}return e._attrsProxy},get listeners(){e._listenersProxy||Ce(e._listenersProxy={},e.$listeners,t,e,"$listeners");return e._listenersProxy},get slots(){return function(t){t._slotsProxy||Se(t._slotsProxy={},t.$scopedSlots);return t._slotsProxy}(e)},emit:S(e.$emit,e),expose:function(t){t&&Object.keys(t).forEach((function(n){return Bt(e,t,n)}))}}}function Ce(t,e,n,r,o){var i=!1;for(var a in e)a in t?e[a]!==n[a]&&(i=!0):(i=!0,ke(t,a,r,o));for(var a in t)a in e||(i=!0,delete t[a]);return i}function ke(t,e,n,r){Object.defineProperty(t,e,{enumerable:!0,configurable:!0,get:function(){return n[r][e]}})}function Se(t,e){for(var n in e)t[n]=e[n];for(var n in t)n in e||delete t[n]}function Oe(){var t=ct;return t._setupContext||(t._setupContext=xe(t))}var Te,Ae=null;function je(t,e){return(t.__esModule||st&&"Module"===t[Symbol.toStringTag])&&(t=t.default),s(t)?e.extend(t):t}function Ee(t){if(e(t))for(var n=0;ndocument.createEvent("Event").timeStamp&&(Ze=function(){return Ge.now()})}var Xe=function(t,e){if(t.post){if(!e.post)return 1}else if(e.post)return-1;return t.id-e.id};function Ye(){var t,e;for(We=Ze(),Je=!0,Ue.sort(Xe),qe=0;qeqe&&Ue[n].id>t.id;)n--;Ue.splice(n+1,0,t)}else Ue.push(t);Ke||(Ke=!0,Cn(Ye))}}var tn="watcher",en="".concat(tn," callback"),nn="".concat(tn," getter"),rn="".concat(tn," cleanup");function on(t,e){return cn(t,null,{flush:"post"})}var an,sn={};function cn(n,r,o){var i=void 0===o?t:o,s=i.immediate,c=i.deep,u=i.flush,l=void 0===u?"pre":u;i.onTrack,i.onTrigger;var f,d,p=ct,v=function(t,e,n){return void 0===n&&(n=null),dn(t,null,n,p,e)},h=!1,m=!1;if(Ft(n)?(f=function(){return n.value},h=It(n)):Mt(n)?(f=function(){return n.__ob__.dep.depend(),n},c=!0):e(n)?(m=!0,h=n.some((function(t){return Mt(t)||It(t)})),f=function(){return n.map((function(t){return Ft(t)?t.value:Mt(t)?Bn(t):a(t)?v(t,nn):void 0}))}):f=a(n)?r?function(){return v(n,nn)}:function(){if(!p||!p._isDestroyed)return d&&d(),v(n,tn,[y])}:j,r&&c){var g=f;f=function(){return Bn(g())}}var y=function(t){d=_.onStop=function(){v(t,rn)}};if(rt())return y=j,r?s&&v(r,en,[f(),m?[]:void 0,y]):f(),j;var _=new Vn(ct,f,j,{lazy:!0});_.noRecurse=!r;var b=m?[]:sn;return _.run=function(){if(_.active)if(r){var t=_.get();(c||h||(m?t.some((function(t,e){return I(t,b[e])})):I(t,b)))&&(d&&d(),v(r,en,[t,b===sn?void 0:b,y]),b=t)}else _.get()},"sync"===l?_.update=_.run:"post"===l?(_.post=!0,_.update=function(){return Qe(_)}):_.update=function(){if(p&&p===ct&&!p._isMounted){var t=p._preWatchers||(p._preWatchers=[]);t.indexOf(_)<0&&t.push(_)}else Qe(_)},r?s?_.run():b=_.get():"post"===l&&p?p.$once("hook:mounted",(function(){return _.get()})):_.get(),function(){_.teardown()}}var un=function(){function t(t){void 0===t&&(t=!1),this.detached=t,this.active=!0,this.effects=[],this.cleanups=[],this.parent=an,!t&&an&&(this.index=(an.scopes||(an.scopes=[])).push(this)-1)}return t.prototype.run=function(t){if(this.active){var e=an;try{return an=this,t()}finally{an=e}}},t.prototype.on=function(){an=this},t.prototype.off=function(){an=this.parent},t.prototype.stop=function(t){if(this.active){var e=void 0,n=void 0;for(e=0,n=this.effects.length;e1)return n&&a(e)?e.call(r):e}},h:function(t,e,n){return te(ct,t,e,n,2,!0)},getCurrentInstance:function(){return ct&&{proxy:ct}},useSlots:function(){return Oe().slots},useAttrs:function(){return Oe().attrs},useListeners:function(){return Oe().listeners},mergeDefaults:function(t,n){var r=e(t)?t.reduce((function(t,e){return t[e]={},t}),{}):t;for(var o in n){var i=r[o];i?e(i)||a(i)?r[o]={type:i,default:n[o]}:i.default=n[o]:null===i&&(r[o]={default:n[o]})}return r},nextTick:Cn,set:jt,del:Et,useCssModule:function(e){return t},useCssVars:function(t){if(J){var e=ct;e&&on((function(){var n=e.$el,r=t(e,e._setupProxy);if(n&&1===n.nodeType){var o=n.style;for(var i in r)o.setProperty("--".concat(i),r[i])}}))}},defineAsyncComponent:function(t){a(t)&&(t={loader:t});var e=t.loader,n=t.loadingComponent,r=t.errorComponent,o=t.delay,i=void 0===o?200:o,s=t.timeout;t.suspensible;var c=t.onError,u=null,l=0,f=function(){var t;return u||(t=u=e().catch((function(t){if(t=t instanceof Error?t:new Error(String(t)),c)return new Promise((function(e,n){c(t,(function(){return e((l++,u=null,f()))}),(function(){return n(t)}),l+1)}));throw t})).then((function(e){return t!==u&&u?u:(e&&(e.__esModule||"Module"===e[Symbol.toStringTag])&&(e=e.default),e)})))};return function(){return{component:f(),delay:i,timeout:s,error:r,loading:n}}},onBeforeMount:Sn,onMounted:On,onBeforeUpdate:Tn,onUpdated:An,onBeforeUnmount:jn,onUnmounted:En,onActivated:Nn,onDeactivated:Pn,onServerPrefetch:Dn,onRenderTracked:Mn,onRenderTriggered:In,onErrorCaptured:function(t,e){void 0===e&&(e=ct),Ln(t,e)}}),Hn=new at;function Bn(t){return Un(t,Hn),Hn.clear(),t}function Un(t,n){var r,o,i=e(t);if(!(!i&&!s(t)||t.__v_skip||Object.isFrozen(t)||t instanceof lt)){if(t.__ob__){var a=t.__ob__.dep.id;if(n.has(a))return;n.add(a)}if(i)for(r=t.length;r--;)Un(t[r],n);else if(Ft(t))Un(t.value,n);else for(r=(o=Object.keys(t)).length;r--;)Un(t[o[r]],n)}}var zn=0,Vn=function(){function t(t,e,n,r,o){!function(t,e){void 0===e&&(e=an),e&&e.active&&e.effects.push(t)}(this,an&&!an._vm?an:t?t._scope:void 0),(this.vm=t)&&o&&(t._watcher=this),r?(this.deep=!!r.deep,this.user=!!r.user,this.lazy=!!r.lazy,this.sync=!!r.sync,this.before=r.before):this.deep=this.user=this.lazy=this.sync=!1,this.cb=n,this.id=++zn,this.active=!0,this.post=!1,this.dirty=this.lazy,this.deps=[],this.newDeps=[],this.depIds=new at,this.newDepIds=new at,this.expression="",a(e)?this.getter=e:(this.getter=function(t){if(!V.test(t)){var e=t.split(".");return function(t){for(var n=0;n-1)if(i&&!_(o,"default"))s=!1;else if(""===s||s===k(t)){var u=xr(String,o.type);(u<0||c-1:"string"==typeof t?t.split(",").indexOf(n)>-1:(r=t,"[object RegExp]"===c.call(r)&&t.test(n));var r}function Tr(t,e){var n=t.cache,r=t.keys,o=t._vnode;for(var i in n){var a=n[i];if(a){var s=a.name;s&&!e(s)&&Ar(n,i,r,o)}}}function Ar(t,e,n,r){var o=t[e];!o||r&&o.tag===r.tag||o.componentInstance.$destroy(),t[e]=null,g(n,e)}!function(e){e.prototype._init=function(e){var n=this;n._uid=tr++,n._isVue=!0,n.__v_skip=!0,n._scope=new un(!0),n._scope._vm=!0,e&&e._isComponent?function(t,e){var n=t.$options=Object.create(t.constructor.options),r=e._parentVnode;n.parent=e.parent,n._parentVnode=r;var o=r.componentOptions;n.propsData=o.propsData,n._parentListeners=o.listeners,n._renderChildren=o.children,n._componentTag=o.tag,e.render&&(n.render=e.render,n.staticRenderFns=e.staticRenderFns)}(n,e):n.$options=gr(er(n.constructor),e||{},n),n._renderProxy=n,n._self=n,function(t){var e=t.$options,n=e.parent;if(n&&!e.abstract){for(;n.$options.abstract&&n.$parent;)n=n.$parent;n.$children.push(t)}t.$parent=n,t.$root=n?n.$root:t,t.$children=[],t.$refs={},t._provided=n?n._provided:Object.create(null),t._watcher=null,t._inactive=null,t._directInactive=!1,t._isMounted=!1,t._isDestroyed=!1,t._isBeingDestroyed=!1}(n),function(t){t._events=Object.create(null),t._hasHookEvent=!1;var e=t.$options._parentListeners;e&&Me(t,e)}(n),function(e){e._vnode=null,e._staticTrees=null;var n=e.$options,r=e.$vnode=n._parentVnode,o=r&&r.context;e.$slots=ge(n._renderChildren,o),e.$scopedSlots=r?be(e.$parent,r.data.scopedSlots,e.$slots):t,e._c=function(t,n,r,o){return te(e,t,n,r,o,!1)},e.$createElement=function(t,n,r,o){return te(e,t,n,r,o,!0)};var i=r&&r.data;At(e,"$attrs",i&&i.attrs||t,null,!0),At(e,"$listeners",n._parentListeners||t,null,!0)}(n),Be(n,"beforeCreate",void 0,!1),function(t){var e=Qn(t.$options.inject,t);e&&(kt(!1),Object.keys(e).forEach((function(n){At(t,n,e[n])})),kt(!0))}(n),qn(n),function(t){var e=t.$options.provide;if(e){var n=a(e)?e.call(t):e;if(!s(n))return;for(var r=ln(t),o=st?Reflect.ownKeys(n):Object.keys(n),i=0;i1?O(n):n;for(var r=O(arguments,1),o='event handler for "'.concat(t,'"'),i=0,a=n.length;iparseInt(this.max)&&Ar(e,n[0],n,this._vnode),this.vnodeToCache=null}}},created:function(){this.cache=Object.create(null),this.keys=[]},destroyed:function(){for(var t in this.cache)Ar(this.cache,t,this.keys)},mounted:function(){var t=this;this.cacheVNode(),this.$watch("include",(function(e){Tr(t,(function(t){return Or(e,t)}))})),this.$watch("exclude",(function(e){Tr(t,(function(t){return!Or(e,t)}))}))},updated:function(){this.cacheVNode()},render:function(){var t=this.$slots.default,e=Ee(t),n=e&&e.componentOptions;if(n){var r=Sr(n),o=this.include,i=this.exclude;if(o&&(!r||!Or(o,r))||i&&r&&Or(i,r))return e;var a=this.cache,s=this.keys,c=null==e.key?n.Ctor.cid+(n.tag?"::".concat(n.tag):""):e.key;a[c]?(e.componentInstance=a[c].componentInstance,g(s,c),s.push(c)):(this.vnodeToCache=e,this.keyToCache=c),e.data.keepAlive=!0}return e||t&&t[0]}},Nr={KeepAlive:Er};!function(t){var e={get:function(){return H}};Object.defineProperty(t,"config",e),t.util={warn:lr,extend:T,mergeOptions:gr,defineReactive:At},t.set=jt,t.delete=Et,t.nextTick=Cn,t.observable=function(t){return Tt(t),t},t.options=Object.create(null),R.forEach((function(e){t.options[e+"s"]=Object.create(null)})),t.options._base=t,T(t.options.components,Nr),function(t){t.use=function(t){var e=this._installedPlugins||(this._installedPlugins=[]);if(e.indexOf(t)>-1)return this;var n=O(arguments,1);return n.unshift(this),a(t.install)?t.install.apply(t,n):a(t)&&t.apply(null,n),e.push(t),this}}(t),function(t){t.mixin=function(t){return this.options=gr(this.options,t),this}}(t),kr(t),function(t){R.forEach((function(e){t[e]=function(t,n){return n?("component"===e&&u(n)&&(n.name=n.name||t,n=this.options._base.extend(n)),"directive"===e&&a(n)&&(n={bind:n,update:n}),this.options[e+"s"][t]=n,n):this.options[e+"s"][t]}}))}(t)}(Cr),Object.defineProperty(Cr.prototype,"$isServer",{get:rt}),Object.defineProperty(Cr.prototype,"$ssrContext",{get:function(){return this.$vnode&&this.$vnode.ssrContext}}),Object.defineProperty(Cr,"FunctionalRenderContext",{value:nr}),Cr.version=Rn;var Pr=v("style,class"),Dr=v("input,textarea,option,select,progress"),Mr=function(t,e,n){return"value"===n&&Dr(t)&&"button"!==e||"selected"===n&&"option"===t||"checked"===n&&"input"===t||"muted"===n&&"video"===t},Ir=v("contenteditable,draggable,spellcheck"),Lr=v("events,caret,typing,plaintext-only"),Rr=v("allowfullscreen,async,autofocus,autoplay,checked,compact,controls,declare,default,defaultchecked,defaultmuted,defaultselected,defer,disabled,enabled,formnovalidate,hidden,indeterminate,inert,ismap,itemscope,loop,multiple,muted,nohref,noresize,noshade,novalidate,nowrap,open,pauseonexit,readonly,required,reversed,scoped,seamless,selected,sortable,truespeed,typemustmatch,visible"),Fr="http://www.w3.org/1999/xlink",Hr=function(t){return":"===t.charAt(5)&&"xlink"===t.slice(0,5)},Br=function(t){return Hr(t)?t.slice(6,t.length):""},Ur=function(t){return null==t||!1===t};function zr(t){for(var e=t.data,n=t,o=t;r(o.componentInstance);)(o=o.componentInstance._vnode)&&o.data&&(e=Vr(o.data,e));for(;r(n=n.parent);)n&&n.data&&(e=Vr(e,n.data));return function(t,e){if(r(t)||r(e))return Kr(t,Jr(e));return""}(e.staticClass,e.class)}function Vr(t,e){return{staticClass:Kr(t.staticClass,e.staticClass),class:r(t.class)?[t.class,e.class]:e.class}}function Kr(t,e){return t?e?t+" "+e:t:e||""}function Jr(t){return Array.isArray(t)?function(t){for(var e,n="",o=0,i=t.length;o-1?_o(t,e,n):Rr(e)?Ur(n)?t.removeAttribute(e):(n="allowfullscreen"===e&&"EMBED"===t.tagName?"true":e,t.setAttribute(e,n)):Ir(e)?t.setAttribute(e,function(t,e){return Ur(e)||"false"===e?"false":"contenteditable"===t&&Lr(e)?e:"true"}(e,n)):Hr(e)?Ur(n)?t.removeAttributeNS(Fr,Br(e)):t.setAttributeNS(Fr,e,n):_o(t,e,n)}function _o(t,e,n){if(Ur(n))t.removeAttribute(e);else{if(W&&!Z&&"TEXTAREA"===t.tagName&&"placeholder"===e&&""!==n&&!t.__ieph){var r=function(e){e.stopImmediatePropagation(),t.removeEventListener("input",r)};t.addEventListener("input",r),t.__ieph=!0}t.setAttribute(e,n)}}var bo={create:go,update:go};function $o(t,e){var o=e.elm,i=e.data,a=t.data;if(!(n(i.staticClass)&&n(i.class)&&(n(a)||n(a.staticClass)&&n(a.class)))){var s=zr(e),c=o._transitionClasses;r(c)&&(s=Kr(s,Jr(c))),s!==o._prevClass&&(o.setAttribute("class",s),o._prevClass=s)}}var wo,xo,Co,ko,So,Oo,To={create:$o,update:$o},Ao=/[\w).+\-_$\]]/;function jo(t){var e,n,r,o,i,a=!1,s=!1,c=!1,u=!1,l=0,f=0,d=0,p=0;for(r=0;r=0&&" "===(h=t.charAt(v));v--);h&&Ao.test(h)||(u=!0)}}else void 0===o?(p=r+1,o=t.slice(0,r).trim()):m();function m(){(i||(i=[])).push(t.slice(p,r).trim()),p=r+1}if(void 0===o?o=t.slice(0,r).trim():0!==p&&m(),i)for(r=0;r-1?{exp:t.slice(0,ko),key:'"'+t.slice(ko+1)+'"'}:{exp:t,key:null};xo=t,ko=So=Oo=0;for(;!qo();)Wo(Co=Jo())?Go(Co):91===Co&&Zo(Co);return{exp:t.slice(0,So),key:t.slice(So+1,Oo)}}(t);return null===n.key?"".concat(t,"=").concat(e):"$set(".concat(n.exp,", ").concat(n.key,", ").concat(e,")")}function Jo(){return xo.charCodeAt(++ko)}function qo(){return ko>=wo}function Wo(t){return 34===t||39===t}function Zo(t){var e=1;for(So=ko;!qo();)if(Wo(t=Jo()))Go(t);else if(91===t&&e++,93===t&&e--,0===e){Oo=ko;break}}function Go(t){for(var e=t;!qo()&&(t=Jo())!==e;);}var Xo,Yo="__r";function Qo(t,e,n){var r=Xo;return function o(){var i=e.apply(null,arguments);null!==i&&ni(t,o,n,r)}}var ti=mn&&!(Q&&Number(Q[1])<=53);function ei(t,e,n,r){if(ti){var o=We,i=e;e=i._wrapper=function(t){if(t.target===t.currentTarget||t.timeStamp>=o||t.timeStamp<=0||t.target.ownerDocument!==document)return i.apply(this,arguments)}}Xo.addEventListener(t,e,et?{capture:n,passive:r}:n)}function ni(t,e,n,r){(r||Xo).removeEventListener(t,e._wrapper||e,n)}function ri(t,e){if(!n(t.data.on)||!n(e.data.on)){var o=e.data.on||{},i=t.data.on||{};Xo=e.elm||t.elm,function(t){if(r(t.__r)){var e=W?"change":"input";t[e]=[].concat(t.__r,t[e]||[]),delete t.__r}r(t.__c)&&(t.change=[].concat(t.__c,t.change||[]),delete t.__c)}(o),Wt(o,i,ei,ni,Qo,e.context),Xo=void 0}}var oi,ii={create:ri,update:ri,destroy:function(t){return ri(t,io)}};function ai(t,e){if(!n(t.data.domProps)||!n(e.data.domProps)){var i,a,s=e.elm,c=t.data.domProps||{},u=e.data.domProps||{};for(i in(r(u.__ob__)||o(u._v_attr_proxy))&&(u=e.data.domProps=T({},u)),c)i in u||(s[i]="");for(i in u){if(a=u[i],"textContent"===i||"innerHTML"===i){if(e.children&&(e.children.length=0),a===c[i])continue;1===s.childNodes.length&&s.removeChild(s.childNodes[0])}if("value"===i&&"PROGRESS"!==s.tagName){s._value=a;var l=n(a)?"":String(a);si(s,l)&&(s.value=l)}else if("innerHTML"===i&&Zr(s.tagName)&&n(s.innerHTML)){(oi=oi||document.createElement("div")).innerHTML="".concat(a,"");for(var f=oi.firstChild;s.firstChild;)s.removeChild(s.firstChild);for(;f.firstChild;)s.appendChild(f.firstChild)}else if(a!==c[i])try{s[i]=a}catch(t){}}}}function si(t,e){return!t.composing&&("OPTION"===t.tagName||function(t,e){var n=!0;try{n=document.activeElement!==t}catch(t){}return n&&t.value!==e}(t,e)||function(t,e){var n=t.value,o=t._vModifiers;if(r(o)){if(o.number)return p(n)!==p(e);if(o.trim)return n.trim()!==e.trim()}return n!==e}(t,e))}var ci={create:ai,update:ai},ui=b((function(t){var e={},n=/:(.+)/;return t.split(/;(?![^(]*\))/g).forEach((function(t){if(t){var r=t.split(n);r.length>1&&(e[r[0].trim()]=r[1].trim())}})),e}));function li(t){var e=fi(t.style);return t.staticStyle?T(t.staticStyle,e):e}function fi(t){return Array.isArray(t)?A(t):"string"==typeof t?ui(t):t}var di,pi=/^--/,vi=/\s*!important$/,hi=function(t,e,n){if(pi.test(e))t.style.setProperty(e,n);else if(vi.test(n))t.style.setProperty(k(e),n.replace(vi,""),"important");else{var r=gi(e);if(Array.isArray(n))for(var o=0,i=n.length;o-1?e.split(bi).forEach((function(e){return t.classList.add(e)})):t.classList.add(e);else{var n=" ".concat(t.getAttribute("class")||""," ");n.indexOf(" "+e+" ")<0&&t.setAttribute("class",(n+e).trim())}}function wi(t,e){if(e&&(e=e.trim()))if(t.classList)e.indexOf(" ")>-1?e.split(bi).forEach((function(e){return t.classList.remove(e)})):t.classList.remove(e),t.classList.length||t.removeAttribute("class");else{for(var n=" ".concat(t.getAttribute("class")||""," "),r=" "+e+" ";n.indexOf(r)>=0;)n=n.replace(r," ");(n=n.trim())?t.setAttribute("class",n):t.removeAttribute("class")}}function xi(t){if(t){if("object"==typeof t){var e={};return!1!==t.css&&T(e,Ci(t.name||"v")),T(e,t),e}return"string"==typeof t?Ci(t):void 0}}var Ci=b((function(t){return{enterClass:"".concat(t,"-enter"),enterToClass:"".concat(t,"-enter-to"),enterActiveClass:"".concat(t,"-enter-active"),leaveClass:"".concat(t,"-leave"),leaveToClass:"".concat(t,"-leave-to"),leaveActiveClass:"".concat(t,"-leave-active")}})),ki=J&&!Z,Si="transition",Oi="animation",Ti="transition",Ai="transitionend",ji="animation",Ei="animationend";ki&&(void 0===window.ontransitionend&&void 0!==window.onwebkittransitionend&&(Ti="WebkitTransition",Ai="webkitTransitionEnd"),void 0===window.onanimationend&&void 0!==window.onwebkitanimationend&&(ji="WebkitAnimation",Ei="webkitAnimationEnd"));var Ni=J?window.requestAnimationFrame?window.requestAnimationFrame.bind(window):setTimeout:function(t){return t()};function Pi(t){Ni((function(){Ni(t)}))}function Di(t,e){var n=t._transitionClasses||(t._transitionClasses=[]);n.indexOf(e)<0&&(n.push(e),$i(t,e))}function Mi(t,e){t._transitionClasses&&g(t._transitionClasses,e),wi(t,e)}function Ii(t,e,n){var r=Ri(t,e),o=r.type,i=r.timeout,a=r.propCount;if(!o)return n();var s=o===Si?Ai:Ei,c=0,u=function(){t.removeEventListener(s,l),n()},l=function(e){e.target===t&&++c>=a&&u()};setTimeout((function(){c0&&(n=Si,l=a,f=i.length):e===Oi?u>0&&(n=Oi,l=u,f=c.length):f=(n=(l=Math.max(a,u))>0?a>u?Si:Oi:null)?n===Si?i.length:c.length:0,{type:n,timeout:l,propCount:f,hasTransform:n===Si&&Li.test(r[Ti+"Property"])}}function Fi(t,e){for(;t.length1}function Ki(t,e){!0!==e.data.show&&Bi(e)}var Ji=function(t){var a,s,c={},u=t.modules,l=t.nodeOps;for(a=0;av?b(t,n(o[g+1])?null:o[g+1].elm,o,p,g,i):p>g&&w(e,f,v)}(f,h,m,i,u):r(m)?(r(t.text)&&l.setTextContent(f,""),b(f,null,m,0,m.length-1,i)):r(h)?w(h,0,h.length-1):r(t.text)&&l.setTextContent(f,""):t.text!==e.text&&l.setTextContent(f,e.text),r(v)&&r(p=v.hook)&&r(p=p.postpatch)&&p(t,e)}}}function S(t,e,n){if(o(n)&&r(t.parent))t.parent.data.pendingInsert=e;else for(var i=0;i-1,a.selected!==i&&(a.selected=i);else if(P(Xi(a),r))return void(t.selectedIndex!==s&&(t.selectedIndex=s));o||(t.selectedIndex=-1)}}function Gi(t,e){return e.every((function(e){return!P(e,t)}))}function Xi(t){return"_value"in t?t._value:t.value}function Yi(t){t.target.composing=!0}function Qi(t){t.target.composing&&(t.target.composing=!1,ta(t.target,"input"))}function ta(t,e){var n=document.createEvent("HTMLEvents");n.initEvent(e,!0,!0),t.dispatchEvent(n)}function ea(t){return!t.componentInstance||t.data&&t.data.transition?t:ea(t.componentInstance._vnode)}var na={bind:function(t,e,n){var r=e.value,o=(n=ea(n)).data&&n.data.transition,i=t.__vOriginalDisplay="none"===t.style.display?"":t.style.display;r&&o?(n.data.show=!0,Bi(n,(function(){t.style.display=i}))):t.style.display=r?i:"none"},update:function(t,e,n){var r=e.value;!r!=!e.oldValue&&((n=ea(n)).data&&n.data.transition?(n.data.show=!0,r?Bi(n,(function(){t.style.display=t.__vOriginalDisplay})):Ui(n,(function(){t.style.display="none"}))):t.style.display=r?t.__vOriginalDisplay:"none")},unbind:function(t,e,n,r,o){o||(t.style.display=t.__vOriginalDisplay)}},ra={model:qi,show:na},oa={name:String,appear:Boolean,css:Boolean,mode:String,type:String,enterClass:String,leaveClass:String,enterToClass:String,leaveToClass:String,enterActiveClass:String,leaveActiveClass:String,appearClass:String,appearActiveClass:String,appearToClass:String,duration:[Number,String,Object]};function ia(t){var e=t&&t.componentOptions;return e&&e.Ctor.options.abstract?ia(Ee(e.children)):t}function aa(t){var e={},n=t.$options;for(var r in n.propsData)e[r]=t[r];var o=n._parentListeners;for(var r in o)e[w(r)]=o[r];return e}function sa(t,e){if(/\d-keep-alive$/.test(e.tag))return t("keep-alive",{props:e.componentOptions.propsData})}var ca=function(t){return t.tag||_e(t)},ua=function(t){return"show"===t.name},la={name:"transition",props:oa,abstract:!0,render:function(t){var e=this,n=this.$slots.default;if(n&&(n=n.filter(ca)).length){var r=this.mode,o=n[0];if(function(t){for(;t=t.parent;)if(t.data.transition)return!0}(this.$vnode))return o;var a=ia(o);if(!a)return o;if(this._leaving)return sa(t,o);var s="__transition-".concat(this._uid,"-");a.key=null==a.key?a.isComment?s+"comment":s+a.tag:i(a.key)?0===String(a.key).indexOf(s)?a.key:s+a.key:a.key;var c=(a.data||(a.data={})).transition=aa(this),u=this._vnode,l=ia(u);if(a.data.directives&&a.data.directives.some(ua)&&(a.data.show=!0),l&&l.data&&!function(t,e){return e.key===t.key&&e.tag===t.tag}(a,l)&&!_e(l)&&(!l.componentInstance||!l.componentInstance._vnode.isComment)){var f=l.data.transition=T({},c);if("out-in"===r)return this._leaving=!0,Zt(f,"afterLeave",(function(){e._leaving=!1,e.$forceUpdate()})),sa(t,o);if("in-out"===r){if(_e(a))return u;var d,p=function(){d()};Zt(c,"afterEnter",p),Zt(c,"enterCancelled",p),Zt(f,"delayLeave",(function(t){d=t}))}}return o}}},fa=T({tag:String,moveClass:String},oa);delete fa.mode;var da={props:fa,beforeMount:function(){var t=this,e=this._update;this._update=function(n,r){var o=Le(t);t.__patch__(t._vnode,t.kept,!1,!0),t._vnode=t.kept,o(),e.call(t,n,r)}},render:function(t){for(var e=this.tag||this.$vnode.data.tag||"span",n=Object.create(null),r=this.prevChildren=this.children,o=this.$slots.default||[],i=this.children=[],a=aa(this),s=0;s-1?Yr[t]=e.constructor===window.HTMLUnknownElement||e.constructor===window.HTMLElement:Yr[t]=/HTMLUnknownElement/.test(e.toString())},T(Cr.options.directives,ra),T(Cr.options.components,ma),Cr.prototype.__patch__=J?Ji:j,Cr.prototype.$mount=function(t,e){return function(t,e,n){var r;t.$el=e,t.$options.render||(t.$options.render=ft),Be(t,"beforeMount"),r=function(){t._update(t._render(),n)},new Vn(t,r,j,{before:function(){t._isMounted&&!t._isDestroyed&&Be(t,"beforeUpdate")}},!0),n=!1;var o=t._preWatchers;if(o)for(var i=0;i\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/,Ta=/^\s*((?:v-[\w-]+:|@|:|#)\[[^=]+?\][^\s"'<>\/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/,Aa="[a-zA-Z_][\\-\\.0-9_a-zA-Z".concat(B.source,"]*"),ja="((?:".concat(Aa,"\\:)?").concat(Aa,")"),Ea=new RegExp("^<".concat(ja)),Na=/^\s*(\/?)>/,Pa=new RegExp("^<\\/".concat(ja,"[^>]*>")),Da=/^]+>/i,Ma=/^",""":'"',"&":"&"," ":"\n"," ":"\t","'":"'"},Ha=/&(?:lt|gt|quot|amp|#39);/g,Ba=/&(?:lt|gt|quot|amp|#39|#10|#9);/g,Ua=v("pre,textarea",!0),za=function(t,e){return t&&Ua(t)&&"\n"===e[0]};function Va(t,e){var n=e?Ba:Ha;return t.replace(n,(function(t){return Fa[t]}))}function Ka(t,e){for(var n,r,o=[],i=e.expectHTML,a=e.isUnaryTag||E,s=e.canBeLeftOpenTag||E,c=0,u=function(){if(n=t,r&&La(r)){var u=0,d=r.toLowerCase(),p=Ra[d]||(Ra[d]=new RegExp("([\\s\\S]*?)(]*>)","i"));w=t.replace(p,(function(t,n,r){return u=r.length,La(d)||"noscript"===d||(n=n.replace(//g,"$1").replace(//g,"$1")),za(d,n)&&(n=n.slice(1)),e.chars&&e.chars(n),""}));c+=t.length-w.length,t=w,f(d,c-u,c)}else{var v=t.indexOf("<");if(0===v){if(Ma.test(t)){var h=t.indexOf("--\x3e");if(h>=0)return e.shouldKeepComment&&e.comment&&e.comment(t.substring(4,h),c,c+h+3),l(h+3),"continue"}if(Ia.test(t)){var m=t.indexOf("]>");if(m>=0)return l(m+2),"continue"}var g=t.match(Da);if(g)return l(g[0].length),"continue";var y=t.match(Pa);if(y){var _=c;return l(y[0].length),f(y[1],_,c),"continue"}var b=function(){var e=t.match(Ea);if(e){var n={tagName:e[1],attrs:[],start:c};l(e[0].length);for(var r=void 0,o=void 0;!(r=t.match(Na))&&(o=t.match(Ta)||t.match(Oa));)o.start=c,l(o[0].length),o.end=c,n.attrs.push(o);if(r)return n.unarySlash=r[1],l(r[0].length),n.end=c,n}}();if(b)return function(t){var n=t.tagName,c=t.unarySlash;i&&("p"===r&&Sa(n)&&f(r),s(n)&&r===n&&f(n));for(var u=a(n)||!!c,l=t.attrs.length,d=new Array(l),p=0;p=0){for(w=t.slice(v);!(Pa.test(w)||Ea.test(w)||Ma.test(w)||Ia.test(w)||(x=w.indexOf("<",1))<0);)v+=x,w=t.slice(v);$=t.substring(0,v)}v<0&&($=t),$&&l($.length),e.chars&&$&&e.chars($,c-$.length,c)}if(t===n)return e.chars&&e.chars(t),"break"};t;){if("break"===u())break}function l(e){c+=e,t=t.substring(e)}function f(t,n,i){var a,s;if(null==n&&(n=c),null==i&&(i=c),t)for(s=t.toLowerCase(),a=o.length-1;a>=0&&o[a].lowerCasedTag!==s;a--);else a=0;if(a>=0){for(var u=o.length-1;u>=a;u--)e.end&&e.end(o[u].tag,n,i);o.length=a,r=a&&o[a-1].tag}else"br"===s?e.start&&e.start(t,[],!0,n,i):"p"===s&&(e.start&&e.start(t,[],!1,n,i),e.end&&e.end(t,n,i))}f()}var Ja,qa,Wa,Za,Ga,Xa,Ya,Qa,ts=/^@|^v-on:/,es=/^v-|^@|^:|^#/,ns=/([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/,rs=/,([^,\}\]]*)(?:,([^,\}\]]*))?$/,os=/^\(|\)$/g,is=/^\[.*\]$/,as=/:(.*)$/,ss=/^:|^\.|^v-bind:/,cs=/\.[^.\]]+(?=[^\]]*$)/g,us=/^v-slot(:|$)|^#/,ls=/[\r\n]/,fs=/[ \f\t\r\n]+/g,ds=b(xa),ps="_empty_";function vs(t,e,n){return{type:1,tag:t,attrsList:e,attrsMap:$s(e),rawAttrsMap:{},parent:n,children:[]}}function hs(t,e){Ja=e.warn||No,Xa=e.isPreTag||E,Ya=e.mustUseProp||E,Qa=e.getTagNamespace||E,e.isReservedTag,Wa=Po(e.modules,"transformNode"),Za=Po(e.modules,"preTransformNode"),Ga=Po(e.modules,"postTransformNode"),qa=e.delimiters;var n,r,o=[],i=!1!==e.preserveWhitespace,a=e.whitespace,s=!1,c=!1;function u(t){if(l(t),s||t.processed||(t=ms(t,e)),o.length||t===n||n.if&&(t.elseif||t.else)&&ys(n,{exp:t.elseif,block:t}),r&&!t.forbidden)if(t.elseif||t.else)a=t,u=function(t){for(var e=t.length;e--;){if(1===t[e].type)return t[e];t.pop()}}(r.children),u&&u.if&&ys(u,{exp:a.elseif,block:a});else{if(t.slotScope){var i=t.slotTarget||'"default"';(r.scopedSlots||(r.scopedSlots={}))[i]=t}r.children.push(t),t.parent=r}var a,u;t.children=t.children.filter((function(t){return!t.slotScope})),l(t),t.pre&&(s=!1),Xa(t.tag)&&(c=!1);for(var f=0;fc&&(s.push(i=t.slice(c,o)),a.push(JSON.stringify(i)));var u=jo(r[1].trim());a.push("_s(".concat(u,")")),s.push({"@binding":u}),c=o+r[0].length}return c-1")+("true"===i?":(".concat(e,")"):":_q(".concat(e,",").concat(i,")"))),Fo(t,"change","var $$a=".concat(e,",")+"$$el=$event.target,"+"$$c=$$el.checked?(".concat(i,"):(").concat(a,");")+"if(Array.isArray($$a)){"+"var $$v=".concat(r?"_n("+o+")":o,",")+"$$i=_i($$a,$$v);"+"if($$el.checked){$$i<0&&(".concat(Ko(e,"$$a.concat([$$v])"),")}")+"else{$$i>-1&&(".concat(Ko(e,"$$a.slice(0,$$i).concat($$a.slice($$i+1))"),")}")+"}else{".concat(Ko(e,"$$c"),"}"),null,!0)}(t,r,o);else if("input"===i&&"radio"===a)!function(t,e,n){var r=n&&n.number,o=Ho(t,"value")||"null";o=r?"_n(".concat(o,")"):o,Do(t,"checked","_q(".concat(e,",").concat(o,")")),Fo(t,"change",Ko(e,o),null,!0)}(t,r,o);else if("input"===i||"textarea"===i)!function(t,e,n){var r=t.attrsMap.type,o=n||{},i=o.lazy,a=o.number,s=o.trim,c=!i&&"range"!==r,u=i?"change":"range"===r?Yo:"input",l="$event.target.value";s&&(l="$event.target.value.trim()");a&&(l="_n(".concat(l,")"));var f=Ko(e,l);c&&(f="if($event.target.composing)return;".concat(f));Do(t,"value","(".concat(e,")")),Fo(t,u,f,null,!0),(s||a)&&Fo(t,"blur","$forceUpdate()")}(t,r,o);else if(!H.isReservedTag(i))return Vo(t,r,o),!1;return!0},text:function(t,e){e.value&&Do(t,"textContent","_s(".concat(e.value,")"),e)},html:function(t,e){e.value&&Do(t,"innerHTML","_s(".concat(e.value,")"),e)}},As={expectHTML:!0,modules:ks,directives:Ts,isPreTag:function(t){return"pre"===t},isUnaryTag:Ca,mustUseProp:Mr,canBeLeftOpenTag:ka,isReservedTag:Gr,getTagNamespace:Xr,staticKeys:function(t){return t.reduce((function(t,e){return t.concat(e.staticKeys||[])}),[]).join(",")}(ks)},js=b((function(t){return v("type,tag,attrsList,attrsMap,plain,parent,children,attrs,start,end,rawAttrsMap"+(t?","+t:""))}));function Es(t,e){t&&(Ss=js(e.staticKeys||""),Os=e.isReservedTag||E,Ns(t),Ps(t,!1))}function Ns(t){if(t.static=function(t){if(2===t.type)return!1;if(3===t.type)return!0;return!(!t.pre&&(t.hasBindings||t.if||t.for||h(t.tag)||!Os(t.tag)||function(t){for(;t.parent;){if("template"!==(t=t.parent).tag)return!1;if(t.for)return!0}return!1}(t)||!Object.keys(t).every(Ss)))}(t),1===t.type){if(!Os(t.tag)&&"slot"!==t.tag&&null==t.attrsMap["inline-template"])return;for(var e=0,n=t.children.length;e|^function(?:\s+[\w$]+)?\s*\(/,Ms=/\([^)]*?\);*$/,Is=/^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\['[^']*?']|\["[^"]*?"]|\[\d+]|\[[A-Za-z_$][\w$]*])*$/,Ls={esc:27,tab:9,enter:13,space:32,up:38,left:37,right:39,down:40,delete:[8,46]},Rs={esc:["Esc","Escape"],tab:"Tab",enter:"Enter",space:[" ","Spacebar"],up:["Up","ArrowUp"],left:["Left","ArrowLeft"],right:["Right","ArrowRight"],down:["Down","ArrowDown"],delete:["Backspace","Delete","Del"]},Fs=function(t){return"if(".concat(t,")return null;")},Hs={stop:"$event.stopPropagation();",prevent:"$event.preventDefault();",self:Fs("$event.target !== $event.currentTarget"),ctrl:Fs("!$event.ctrlKey"),shift:Fs("!$event.shiftKey"),alt:Fs("!$event.altKey"),meta:Fs("!$event.metaKey"),left:Fs("'button' in $event && $event.button !== 0"),middle:Fs("'button' in $event && $event.button !== 1"),right:Fs("'button' in $event && $event.button !== 2")};function Bs(t,e){var n=e?"nativeOn:":"on:",r="",o="";for(var i in t){var a=Us(t[i]);t[i]&&t[i].dynamic?o+="".concat(i,",").concat(a,","):r+='"'.concat(i,'":').concat(a,",")}return r="{".concat(r.slice(0,-1),"}"),o?n+"_d(".concat(r,",[").concat(o.slice(0,-1),"])"):n+r}function Us(t){if(!t)return"function(){}";if(Array.isArray(t))return"[".concat(t.map((function(t){return Us(t)})).join(","),"]");var e=Is.test(t.value),n=Ds.test(t.value),r=Is.test(t.value.replace(Ms,""));if(t.modifiers){var o="",i="",a=[],s=function(e){if(Hs[e])i+=Hs[e],Ls[e]&&a.push(e);else if("exact"===e){var n=t.modifiers;i+=Fs(["ctrl","shift","alt","meta"].filter((function(t){return!n[t]})).map((function(t){return"$event.".concat(t,"Key")})).join("||"))}else a.push(e)};for(var c in t.modifiers)s(c);a.length&&(o+=function(t){return"if(!$event.type.indexOf('key')&&"+"".concat(t.map(zs).join("&&"),")return null;")}(a)),i&&(o+=i);var u=e?"return ".concat(t.value,".apply(null, arguments)"):n?"return (".concat(t.value,").apply(null, arguments)"):r?"return ".concat(t.value):t.value;return"function($event){".concat(o).concat(u,"}")}return e||n?t.value:"function($event){".concat(r?"return ".concat(t.value):t.value,"}")}function zs(t){var e=parseInt(t,10);if(e)return"$event.keyCode!==".concat(e);var n=Ls[t],r=Rs[t];return"_k($event.keyCode,"+"".concat(JSON.stringify(t),",")+"".concat(JSON.stringify(n),",")+"$event.key,"+"".concat(JSON.stringify(r))+")"}var Vs={on:function(t,e){t.wrapListeners=function(t){return"_g(".concat(t,",").concat(e.value,")")}},bind:function(t,e){t.wrapData=function(n){return"_b(".concat(n,",'").concat(t.tag,"',").concat(e.value,",").concat(e.modifiers&&e.modifiers.prop?"true":"false").concat(e.modifiers&&e.modifiers.sync?",true":"",")")}},cloak:j},Ks=function(t){this.options=t,this.warn=t.warn||No,this.transforms=Po(t.modules,"transformCode"),this.dataGenFns=Po(t.modules,"genData"),this.directives=T(T({},Vs),t.directives);var e=t.isReservedTag||E;this.maybeComponent=function(t){return!!t.component||!e(t.tag)},this.onceId=0,this.staticRenderFns=[],this.pre=!1};function Js(t,e){var n=new Ks(e),r=t?"script"===t.tag?"null":qs(t,n):'_c("div")';return{render:"with(this){return ".concat(r,"}"),staticRenderFns:n.staticRenderFns}}function qs(t,e){if(t.parent&&(t.pre=t.pre||t.parent.pre),t.staticRoot&&!t.staticProcessed)return Ws(t,e);if(t.once&&!t.onceProcessed)return Zs(t,e);if(t.for&&!t.forProcessed)return Ys(t,e);if(t.if&&!t.ifProcessed)return Gs(t,e);if("template"!==t.tag||t.slotTarget||e.pre){if("slot"===t.tag)return function(t,e){var n=t.slotName||'"default"',r=nc(t,e),o="_t(".concat(n).concat(r?",function(){return ".concat(r,"}"):""),i=t.attrs||t.dynamicAttrs?ic((t.attrs||[]).concat(t.dynamicAttrs||[]).map((function(t){return{name:w(t.name),value:t.value,dynamic:t.dynamic}}))):null,a=t.attrsMap["v-bind"];!i&&!a||r||(o+=",null");i&&(o+=",".concat(i));a&&(o+="".concat(i?"":",null",",").concat(a));return o+")"}(t,e);var n=void 0;if(t.component)n=function(t,e,n){var r=e.inlineTemplate?null:nc(e,n,!0);return"_c(".concat(t,",").concat(Qs(e,n)).concat(r?",".concat(r):"",")")}(t.component,t,e);else{var r=void 0,o=e.maybeComponent(t);(!t.plain||t.pre&&o)&&(r=Qs(t,e));var i=void 0,a=e.options.bindings;o&&a&&!1!==a.__isScriptSetup&&(i=function(t,e){var n=w(e),r=x(n),o=function(o){return t[e]===o?e:t[n]===o?n:t[r]===o?r:void 0},i=o("setup-const")||o("setup-reactive-const");if(i)return i;var a=o("setup-let")||o("setup-ref")||o("setup-maybe-ref");if(a)return a}(a,t.tag)),i||(i="'".concat(t.tag,"'"));var s=t.inlineTemplate?null:nc(t,e,!0);n="_c(".concat(i).concat(r?",".concat(r):"").concat(s?",".concat(s):"",")")}for(var c=0;c>>0}(a)):"",")")}(t,t.scopedSlots,e),",")),t.model&&(n+="model:{value:".concat(t.model.value,",callback:").concat(t.model.callback,",expression:").concat(t.model.expression,"},")),t.inlineTemplate){var i=function(t,e){var n=t.children[0];if(n&&1===n.type){var r=Js(n,e.options);return"inlineTemplate:{render:function(){".concat(r.render,"},staticRenderFns:[").concat(r.staticRenderFns.map((function(t){return"function(){".concat(t,"}")})).join(","),"]}")}}(t,e);i&&(n+="".concat(i,","))}return n=n.replace(/,$/,"")+"}",t.dynamicAttrs&&(n="_b(".concat(n,',"').concat(t.tag,'",').concat(ic(t.dynamicAttrs),")")),t.wrapData&&(n=t.wrapData(n)),t.wrapListeners&&(n=t.wrapListeners(n)),n}function tc(t){return 1===t.type&&("slot"===t.tag||t.children.some(tc))}function ec(t,e){var n=t.attrsMap["slot-scope"];if(t.if&&!t.ifProcessed&&!n)return Gs(t,e,ec,"null");if(t.for&&!t.forProcessed)return Ys(t,e,ec);var r=t.slotScope===ps?"":String(t.slotScope),o="function(".concat(r,"){")+"return ".concat("template"===t.tag?t.if&&n?"(".concat(t.if,")?").concat(nc(t,e)||"undefined",":undefined"):nc(t,e)||"undefined":qs(t,e),"}"),i=r?"":",proxy:true";return"{key:".concat(t.slotTarget||'"default"',",fn:").concat(o).concat(i,"}")}function nc(t,e,n,r,o){var i=t.children;if(i.length){var a=i[0];if(1===i.length&&a.for&&"template"!==a.tag&&"slot"!==a.tag){var s=n?e.maybeComponent(a)?",1":",0":"";return"".concat((r||qs)(a,e)).concat(s)}var c=n?function(t,e){for(var n=0,r=0;r':'
              ',lc.innerHTML.indexOf(" ")>0}var vc=!!J&&pc(!1),hc=!!J&&pc(!0),mc=b((function(t){var e=to(t);return e&&e.innerHTML})),gc=Cr.prototype.$mount;return Cr.prototype.$mount=function(t,e){if((t=t&&to(t))===document.body||t===document.documentElement)return this;var n=this.$options;if(!n.render){var r=n.template;if(r)if("string"==typeof r)"#"===r.charAt(0)&&(r=mc(r));else{if(!r.nodeType)return this;r=r.innerHTML}else t&&(r=function(t){if(t.outerHTML)return t.outerHTML;var e=document.createElement("div");return e.appendChild(t.cloneNode(!0)),e.innerHTML}(t));if(r){var o=dc(r,{outputSourceRange:!1,shouldDecodeNewlines:vc,shouldDecodeNewlinesForHref:hc,delimiters:n.delimiters,comments:n.comments},this),i=o.render,a=o.staticRenderFns;n.render=i,n.staticRenderFns=a}}return gc.call(this,t,e)},Cr.compile=dc,T(Cr,Fn),Cr.effect=function(t,e){var n=new Vn(ct,t,j,{sync:!0});e&&(n.update=function(){e((function(){return n.run()}))})},Cr})); \ No newline at end of file diff --git a/plugin/think-plugs-static/stc/public/static/plugs/ztree/zTreeStyle/img/diy/1_close.png b/plugin/think-plugs-static/stc/public/static/plugs/ztree/zTreeStyle/img/diy/1_close.png new file mode 100644 index 0000000000000000000000000000000000000000..68ccb3c3b90170df7cddab1fe6e8e455c3854573 GIT binary patch literal 601 zcmV-f0;c_mP)(_`g8%^e{{R4h=>PzAFaQARU;qF*m;eA5Z<1fd zMgRZ--$_J4RCwB?l0Qh?U>Jv=3mDW`xPoRlZg47s^hk#c>Tuv92pw){9g3YCdL5hu zPl^tKlA{iS!zGjsQlXPxhk_ub9SW5~g#>Fr4ipy;F_I1%TS(#S(A+iGdab2D_=ey2 zzVGw>dEXSV48y1Z#n@V})Z4_~`uO11LoSgJ;-@hTLj*w}f*=rgmR<_oc7z)Vq1%qo zZKu)>ASE|^-{;BuBM!UznV-=3a02ra8bGlfqy_NQ3wd+&joE_6egI#-!hQg=h4zpp z;M296ygNxTJ+86mLvIJXc(C!M=TaJWLajhppjQ$b;~2$BXMvVGB=iMv=}M6I9o9{cNofcv$kC$ufoz@ zz2&1CkhSd^L)mW3%^jkPvz10%8Ca;U>$W3is*d=RW~z?RZAT0&RM!C|IY^3Wnk~te zWi?k)rfCX5`E`)ObUMvd6z7G_E(jr7XZi|C)=2!PQos#>QmOPvQIz|CD+t5zdAVGE n9N(mtO2jh%aB#ixl(_`g8%^e{{R4h=>PzAFaQARU;qF*m;eA5Z<1fd zMgRZ-%1J~)RCwB?lCeu0Q5?rVIibtp5L74?1Q$6a(r!ngUGf(c7duF)D8;MGL5C9B zI3&~?dP1k@kYI4B4iOhq=+db}N8|9$61XK6ONK(|@rtRu4tGiK#I%-v;rQ`=zu)ik z?!EU*5y^3!5@1Dk^TF2xBELEP=F?NIkW%WmaU4fUDV3B`)pHM(uuY{pCra3+61G_y z1kmCO?RJ~i+jSSlaFBpYY-#>`2McM&&GR>sLTD0AZV@N$F$EuYi9XHdi68G}lp77xZ62 zDV3b*Ybf5M@t;ZoQvmsV{)MjVkN(yW1i?&rock3TE{?a?juK@rdhYW4p S$5p2Q0000%A_P)(_`g8%^e{{R4h=>PzAFaQARU;qF*m;eA5Z<1fd zMgRZ-z)3_wRCwBqlR;<`Q4of|=SoN-iXiB%SaR=4qz6IqX2`+Af>f~AQhE}Eig@Tj zBzy8wOX*!fcWJ>?1hHVChd?QzUbR+3TN0sbpvl_ZyxsAj8?v~O{P36=X8!-p%;PB$ z!LqDbphyY|g+j?RO~o*X2<39QB12%?w%E2UB0}0Z#RxD>6UT8REjN`U>s( zhd*TZkOSA{aI4BsFL;p{AtuIc`@T=3vYbjjXnU*0#f_PSKsU=kNV^|)04A(_`g8%^e{{R4h=>PzAFaQARU;qF*m;eA5Z<1fd zMgRZ;fJsC_RCwBqlif?xQ5eTRd)1}e+J1_fPBt@8o9(K!Cc-2TCJ0g&UIbkV;>Ciz z_7AWNMc93KlTaWAV@cSBY1&5W$R<+0;PS1UKbI)(<#)PpMy`wwJe&j1_w(gE&v}k2 z65wB}F1vraNT9wbk&tm3Ei4)>EGA@}|6`y5dVqQWwVDpA&5PCMMXjbItEtawG=CVJ z9MMx*=N>XjM3f}MIxTG`sST6VhE7YHl4RH@5viMYbvIx89MA zzj*L%?Fr~H5Uq-EOAeFRoxIe*OY>I;&u4=cxVz8Ei5NVXm57J4Q z%*l69;yLe?MuUu!(IG?{jS9BEVo(7(cLj~!Rvun=;&C-nRbsCponAV}Yqd4(#Fkii z%gW+M-UMFpbSlOSfC?}G#X&zoIo2)`n=Wy%AqjaEA+I71HYAbQbO|}uE`ol7Kyj8) zX(IsyaD14cwxpa^gVDob{3VI)1uSLNpwSYKEnzBkQE2sWt$wU^`Wd!D6Ccjafuhiq zXO>uxJmU2jp|UGjJooFW0p@3xK5uPK?B8vlxOvGep=|6m!Knd}z-a&l{$Bd>p%ZZU zdl}q{+QnAXF8sX=0*-v>^!L&S6#U|TGtdDA+1k`yEi^ArB9(_`g8%^e{{R4h=>PzAFaQARU;qF*m;eA5Z<1fd zMgRZ-6-h)vRCwB)kwHqtFc^hDO9exv%xrssDR_YP3KH}P>AHeo4 z!n8_44a7<*fOz{5=x+MFN_r=}XHqLH?*Vg2Dj@Q<( zsf^{d?t$;DLEs_45(LYZkIyZa%L_PnjWytw_!&?yR*DYpK|Z9fLl(~*8t4{WR}t?Us?bF002ovPDHLkV1l&kqKg0k literal 0 HcmV?d00001 diff --git a/plugin/think-plugs-static/stc/public/static/plugs/ztree/zTreeStyle/img/diy/5.png b/plugin/think-plugs-static/stc/public/static/plugs/ztree/zTreeStyle/img/diy/5.png new file mode 100644 index 0000000000000000000000000000000000000000..0c5eccd562c303cf5197629ef5f2666b6180bd48 GIT binary patch literal 710 zcmV;%0y+JOP)(_`g8%^e{{R4h=>PzAFaQARU;qF*m;eA5Z<1fd zMgRZ;Oi4sRRCwB?l21sJaTv!xzis}To8;O?C>{dC{zy=SO6rn?>JoVfJ9PB0!cw|< z5rY2EH4^INrK0lE$(TB`L1J|?w{{RVwVX+{e0A>6yZ3#69o{*#LV~{V{P^I25M99V?Z$DfCOu%f*iEi)VM!&PQ0~X={*pIhRm&^JADx2Ei zO9FtZ)17!zihou!LclW{fUsJ6-KRQWX%RLy%#rdc&iw~WzZ8c|*7UlD+tm4dPcnr0SiDoFEg5E2Y&dPN717pvrG@d&~qN)SLk2YQ}eWhnzS2NsMXU@e;q z$#1J?MTrek>$@}ysIbH4Pn4|s4!{BsRg~ey@o{jwK^l-v|2=daTrL(~1&Auja1qcm zA0Pb~i;esUhr#VZ*EO_EW=H6T4sH+MBVlG^vFAPW@zEjxNMD$XUVj}71~yd{>Do)) z&``4()NF>lp&_eXY~*rG1t$YtbJ2bvy&WKdWa#Vaz0czlsCFm9?LkALArT%gs@=iz zM3Aw>%3UB?=s|4%%0TI@ntc}!?y2fKbE&)8wwW?tw9>)Yi;(_`g8%^e{{R4h=>PzAFaQARU;qF*m;eA5Z<1fd zMgRZ-Hc3Q5RCwBylf6#EFcgKqR*|SEf;KdQN{mQMNM#6bz*E3p!3*#TJOP9q=I%T} z)g_1#i3ub?3{8Itwu2ecv`#4@J&R@g=<~Vv$_3Z+FM(5**Hx$4IK%aP-43;Gb+vXq zUuVhneBJ75-43T-EP$6WJoRVj!|QthI?cwfXCHeI>Dj&rpcKd`Uv3%2{zO&+5OQ|7 z=;cI0rUAu-Dnz~nLZs7L08+*mb;p_3zesh*i2-sBFdS=wK}gNEso8dBf?)8;uvD5$ zpcK!qZv_3`za>=GH`uH0qya)Hf_^WvdFF_P+D(_`g8%^e{{R4h=>PzAFaQARU;qF*m;eA5Z<1fd zMgRZ-oJmAMRCwCNld)>sP!xtg$;P&DX##d?FjXy#1Btu@<1TrIE}b(6+V0u<0C|G+ z1!6E{h#^zPfVNUH1)Si?mq44L)RLtur52}yfrAIB@yT&wtK6#}E;o1CwW> zs0MEifRF!B`^DaweqwrPizJsaJ+ws@N@V7V{5<_TW}e7G$pgN~T?_h(db5KXZbM-X z@i~OveZ=Qbn4^Z<)SDg9SGgG&F_ybUT3JJlcA@4#C4%DjEY}>=XqU9IhUIR75$6O7 z8YIQ(_la3W9qobDhNK8q8+Eit%qmX550Ym5hYkYx{R_;P^`1+ZR#BsUc>NAF+9yn_ ztoK|n4k(!k^_NgGljU2p)j*9W zaI13;R$T12?;o{2_N$D)6u7>L2Aq7}dD;>lbA1yHY`_4jK;tey79sFML_*`gU*n$v Y0CUjIYVM#Hl>h($07*qoM6N<$g3L4G@&Et; literal 0 HcmV?d00001 diff --git a/plugin/think-plugs-static/stc/public/static/plugs/ztree/zTreeStyle/img/diy/8.png b/plugin/think-plugs-static/stc/public/static/plugs/ztree/zTreeStyle/img/diy/8.png new file mode 100644 index 0000000000000000000000000000000000000000..a8f3a86e7091de4acdd38745f74b30f0f3d40f9e GIT binary patch literal 529 zcmV+s0`C2ZP)(_`g8%^e{{R4h=>PzAFaQARU;qF*m;eA5Z<1fd zMgRZ-mq|oHRCwB?ld(!1Q51&1(I{htpn+Xo2nhiLK7gQ=rAta1-4c z23I~Dy#=Bx=-sES#qFU9A~NtT@3!SYME0%7fru>cwq@9yhz#EV;MBm^6uNgtR8Xr= z2i9fl6|Mue0M{9PgJ@>&J^Va_d-JF#FCqZvz~tbj{^y9_PT}y_|FTqGfU(|32N(ld zdoN`zWX3zdQtSRe)z51`wtYaRAywY{0S-(qSQqqB^m^?LF~6wMV5jvm0?L4k3s8yo zD*>6YSQl38yF2Ej`-O+&`dWo7gEaA8Rp?@@!`g|8C0WWJ_m{hqX~o6>&}D$W3~-$S zV;!z@c}1laaT=qvy0*ci2asppOD)WX6h)7P*^pMr>b(YuB-DG2YA$FmJ^na9HNDvf zXGS+Sp6w9M)GC&%xasQa+^&Tc%i_FwxOx6O0H%St(lzkAoEgbPL(_`g8%^e{{R4h=>PzAFaQARU;qF*m;eA5Z<1fd zMgRZ-SxH1eRCwCFlD%phK@^3*HCYugNa+j-+qg=ez>lIrt6Er<-ysj+R54r6Ea(-| zq;X*c?zeMmf*6eCC_6h695=JJ71{U?e45KW_s-#5&LjXuQTzlBfs@f_G?s+5w&eg$ zhQs057_-6Uc|IlzNRo=85P&3%F=}Cq5wOKcVuv^4*#fPL|MB6ehpW|!JkK}!G)eCUA?`kDYU+wRQl|@AsL{=k$6#thE3*=Sl#oqXX&kO!|GS z@au=FqXS6-*ah~DF{o$>nU{_K+;olW3X86D1zL_O(0T4pKdrt_r^NSay z@PGjCE6_9z-g}y+dE_5_`V3#b@v&aP`RUeDmSwEhYqBic7MNZ8lJBEIOU$l)`Ssp{ zbFQ3BCIf)!bb1$GL=xlLi**V7k+e*bq}|z?$BHD9nE9)5GXRrS-uaN7-+2H4002ov JPDHLkV1iKO%vb;b literal 0 HcmV?d00001 diff --git a/plugin/think-plugs-static/stc/public/static/plugs/ztree/zTreeStyle/img/line_conn.gif b/plugin/think-plugs-static/stc/public/static/plugs/ztree/zTreeStyle/img/line_conn.gif new file mode 100644 index 0000000000000000000000000000000000000000..d561d36a915776730eb3069cee4c949f027667ed GIT binary patch literal 45 xcmZ?wbhEHbT_)p@)n z{^qIIq1(T$e$~zMQ}6t1w^a|Cj;Amo3}FHq!p^`7G=x7ROJIXqM}S9S9}m;(SWbi* zHiVjpkZW$u7K%QH zEX1o>_L}Xs`W>m5xx>h#V7OG2< zoq3~FS4nm^l$rbe{C>~B&mZUUIR8A)oj29ga;5>!z_lohQ{m)OUUypp5`9Pnjaj*H{9dv7b zb!Kn=h4ov*xw+ZYSx)RU$G-TzW2@@?CTFkTlg(b+`gP~mD0QwoAbriBy|&~LU%dCN zdV71T>qi%7ucflFoi>&q921w@m;NRFM%>$#udO8N!1~VGo88~Tt8bk1^Pc|e%syz2 z4QFz?ra8MC(*Y@!cS6e-r*F5iN`J1dS5%j;O{K4VFKuqBNo`xX%UF%euIRpJy)Y1c z=V8I84^^er&D6Yszq_2#vBEh<2><}2L!Cc=E*KRsoAdb>7Jkev{Yq{O-5U*F+gO#b zp>Y2HWOiU607L@E|HlLWuL*$UP{N~QVq+rX?ng%^MY^bo9Tih^NzQnflA4~8`!p~A z8LgnOs5n1dy-ZzLSSMK0&@h}@UQty;uTSC%H?vY#GYjX+ZGZW?^G!+rz+eH)--egh z#vk?!=8V$UM>#W-OUo*&YYn;UXPf5eKikBtzSlhJi{$$CTXJJ)Z%E6S1Kgpceg}*) zc@Ec;$hcD5Y-zZ$nUD?ZoGAI5Z-(QDiiDnwgl50MI5OYVUTv2s>+PSHlAg2OC)5<5 zFqG@o7Mxmya2EFF<*o3xU!3hOl0B}X{bzIra%ADss{K0|`3ZeSj{l}AR5|tb=>W|| zdeib?!!w^RjovN~99zzFgm=&tBT>)V4bEt8|D`hc5=@g92w3}Xd^f@y^~3ygzd8$lrrYyp=J~^wKBp!(@ZDM(&0@(_DoIZ5R0k>8=Cu*W4aU#5mw*CjCF) z5QULl#u(o@mD`CTf{xcM0m;e=T;+wE2R~sCex!*!EsJ>h@Zqnq46kR4U3}p0b0mB6 z@ACZ}_~M74khAR(tNJ$ebIet$KH`LfaVN?-N{0648Uis7gHTDOFmU3}sJYv$fnt(; zi{|NT9-?~G`-7c`qH|M@548@ud!67y zrQ7~USx^jBY*-s4mYm(3l3Oj9$p2v&b_d1=Bt4_Wot}J+Z|_A3 z7FVvdEk3080yZRE6fF32f`Jee4}nAAm?T2ozARu;Z*FMj)&Q4VQj}{CE?_{of@LtL z!Zk5-M5>vLA)g;gF3VW;{L%BC(M)NTbc|}hvU#}bWNulr)xhmk#gA8?;LNTSR`8Dd3VWM&ZC+ zo~|YcNRF@xfO-Jw;9}dxnhv|`4CTW9yK(8*e{0!~IZ~KbgCm86td2T_2oExQn zK3xLt-yjL-b%7|o{zz3b2yZG@7g1KsAI_j5v_xTisW%0`aGP)$)EPd(;t(2U23)>a z=!oC9xKmG^d6Y+yU-G&ogmq!Rp05| zQam=ZVZ*`mkD?|fR2|Cdh-L04w0;|7R&b_L04vp{(de+)sXp^{7ANhAud8%eR&gnT z@Q})u43I5~we9}~GgdT^eiGcj%D9;MbrFLk6D^Jp&pkflJ>ba1X1yw=L39^DARY!t zUBbb`WEiK?&=e{Ah9~ch2Y+EE`?+b~gM?g!U>#b$XW5Sv+$aQDH3E1iqD+Zs<|%u) z9AoNnQfRaMk?)!yRGm%Yc5JmE{G-FAXwGT>_B{z;+hDZRCYEuB8KXiN)-z7e5Ipje zKkCc?;y+e`{XGb~vHQrQ=!vZT-cmj92Grq368CXZ=QXz_q_A%RT!R#A;jCmUe7xcL znW1u%81gX)QAn~=4OxT>xP(ZERQ8QfA;O_4on34eXNJk%DtVE6sF(h> z{vZZ}Znfrez}n_CpMHMA43uMjWajXqYy4M@{^5en6E!Z^?tNd@{FlQGpP_)!LrRdq z+f&^LZ0Hc#I%%#!cEeKRKR0}*gT@U(*=}$C8{tu2zt*EI5;bb>_(gd+v5?Hvzwok2 zW!QI3`z|S2dgGrPwt=J6Hx(O*F~|H{P|=y{=xCEdf1A)RYBl7`G7ZXcR*?I6WohCp z9D!$CYg|S`J1AUwq}af^%Fin|x7ueXpUO_;CYc1U#je47&?$8F#^Obkt5RC8@a?as z~s!*;4>d|mSGo-c*Y!vgCiwW9lsge^Tk$EC$9 zFFvZNq&?wA+D?6JV~m`}P8o2WcvTD*laSaBYxI>K|A#FzKN|jk++G*%-{^qVs{}LtuhcRb{CpnO40s0%@Y!Toa>x{s(9Fn+g{wq9D+jqG1%IuT+0apTxK3Q_`(6E4lkJ~b;6RVJ9KMY|4} z>%+#6O)P0`xhz4XjruvSVmUz}9-oe#xw$1QmPi*w^Lio~hbLa4isao7>u(b-vWCx_ z%Wk9%S5XwQTzt}kW)`JAThlwRkTg4q#478p%t6ab7_&On40mvVUK58I!KnPzh&7C~md+_(r$um7M5Ll3m z4wm^It`a5#GZDLRd!Ha7hohz)z!sAJ{2EdF+qvT#&pTCm6C*+iF93W;VB8{}ia+s8 z0)Q%`FqsrCl2MBY7Y=`yOphS2K&kDB3<56dwn%m<4l{P~U407))Y)$#p#sb-nHD8WUQDX=vf#sDhMOiK!?{RJ8uLqc)0K(S~u6Scf7LhibT^ zzkB>X@s9oIGxl%YKJgEJXcBy6@YH+N-}W-^v53;ghh=Xr{|aCW()U zd@HBlE2rBXqyGwu?Jwd!I-+X~oDDU_^qGMJ+pwWt!&nd5{wCSMzz4Z3__E#o_=N}$ zB3fIHlVsd=QlH?e(%^D6-u0A*E0vJk*{Z+gs{6L=rU~2U!03uG7A8cu|3e_C(jM(E zJd&4HIFjb|FEAyK4fp9y{Avcdzm4>6(9m;zc&hXUYgxccG2aV1$Oy}Zt=Q34QLZ1?@2lX;5 z-Fycw&koICYd)6K`bgAzxZ#~G?DMo)s6)^3dat{7wH0h%LJEStT8`_MWxP==jFId(&w4y6l%^+r0) zym{zI*to`p1y=5Cw~rm+!iq7>kCoN4^Q7QBEDkAaLCR4xxOHcmdM(g|pXB#<5o1zn zsev#Y&<%G%gqe$l&w@2lUQKnQWi>hPyJ>E+sFa=~;bzmpNYbr;!s-}U6(^BpK>WTyLP zc!P*=U7|tV5(SAL0mVtYuRTFNMxAIIl#c{P;u?)V)t%eRkJ`R$J%ASJ=QkVykLALh z1mOgt>r%F>L$hl#$#r?VX|1&;u>huhgZnJEul4WH6=+3}jY1^5IRXch+5_;|7LXv7 zX_APVZYeXXh7p@|@bDl!+`bawGs5u}JLa*{>n@q_*tz*}N~nf7x!KFH%1t%<#z6BE z3#5f(V*ndT3l-5B0SzsX&vn{JE)U$IQ>M4uCVZ}%laUH!q#O-lg99#md)rNu3YqOR zM90j$Xex}3G(mQVzlvS_pPNKz0&w>~* zH!{)(2Yj^9Onw4+&qVjkLwe_5J=DrB2~b?pKJ=!aAG;n}!rh{SZ{zF%xT3ZQEMv|l zUE0V+qFISMEM4>}vXTU1$jBfD0Hz=t5*{y(-`e?$ZrbTgrh!|Z6qgc_HQ1{C0r{I2 zQ1UX;-=YQ2sJh3^c@xU5F!Rmrp7-elCI&`?U#34Rp@B1Q_y}4}Eg5D=1C_%>40IZ4 zK8MD=P#lPq0Y@p|d0MYi$1Nq`uoAtK8d6M?5Ah4A z>T#&*u&hc3s>MgD%m&b1mV@$18vZyCD1;{_gXk!1(u~LpXtz6lK!FBv2f8nl;9qeZ zs~aIABi-y7lIyZgeayJcWgS?X@VkCk!| zE7Tkf(Os8bI)3s&3{)uvuuK8|c0m5Dj0n;onv`f=6I*~)Q7%4GR{^spVnvBZbqj~L z{YM4Fka7UxPX|Qh#HbN~u*D+hjqG0K#5su8Nbyw4oE?+@C#`QYC|dr%4*@y3I<_?k zh_m$q?5ROsH99<%+zdc~HLmZc%u5+dW&?7pG53`*IW~}sPe51FS%)U`&3gl7HCj^T z(6hjFI=y@$QZ(uVD6XBh=4Ri z&I8E3Lh12A6s;P9!9pZQKo8=C66It5e7jw__uEe&1JJEYzgz0|WXS|*qo1-2?z5^O z++pZh_pyodhan&->bK78UzIZhl>d&(wojp6c^U$NWMD-1ossiEcqiIWqKjkt<%eI& z?B2`b)~{*?KpC!ko9B$Ia5}1-A$?a^oeUreQ0SUP;Jqx=w422|`eF*?B904&M>J?4 zAP20u$2s?#b6#h99RpyZQe5U_;NM+fo#)#t;a-P9j??4|l!)Hz{H$+`Gqo#%AQB7e zNrCPHwfF<|+B|U^9jX*QC|^EAY#61(VA(TR9_%a++z-r!Y?HOxcj;xA`~$-oDNAFgD;nm!S&SN(1RdWp1n9H`NtWhb!#YxV+Wc)=RSOw+5*(0HxPgsOEf zCec|CoFC8irSUdkvrX?^<5C_0sP;s-(ibFD{wFY7f4v~1c`<(kv?ux}9l)%~{`RcC zH+su~j3sC8MI;F!NrnZ#T38SjTg|A3`K+;$3D7T+3tyy0gcVnQzFJGdq5z%$10c8r Ap8x;= literal 0 HcmV?d00001 diff --git a/plugin/think-plugs-static/stc/public/static/plugs/ztree/zTreeStyle/img/zTreeStandard.png b/plugin/think-plugs-static/stc/public/static/plugs/ztree/zTreeStyle/img/zTreeStandard.png new file mode 100644 index 0000000000000000000000000000000000000000..ffda01ef1cccc398ee4e2327f4093ba1130a4961 GIT binary patch literal 11173 zcmYLvWmFwav?YFV4Z+>r9WEN&f;$&YfZ*=#esOnqhu{tYf=h6BcW3zCn>F*JyMA=9 zI$Hbes#D?0ic%`eCMGW@PGaKWg^*6N8kDgs7U^@~Jn1m)iW@N18WyvpNUqmNcrPI3Y#Ri>q7dfcjA0`CQJQPRv z93Ju@g2iM|VQG_AnX(C!YE<|otj>Y%t~3wFd|9$=oJj#ew<0i%PNM0mtqXpZtV?GLkh`_pJ{z_ z%S*3y3oPMI!)ik|n^y83JT7B+t-_z>NO8=nf2}0%w>JWyz9~+$^BAK}qdy})aRN5z zuO_<0-qB+0!4d6A6NMYS%lL}2%@*5Ie#~dtpp8%(Q-{Ih+vx<->Pb?+R+#h3JRk6u z!*pT9&Pz%Ivw2=d7+7-TAxNxPD=R4(x-;QLRNjkGXk%`Q_ej=xw9xUnU`!?_I4H=f zLl9I;*|!23DN!jq+=(}g4&ivnx$fJ#4JVeDC*QhJ_;W62Teq>L-fd>DgM#f^fi>m8 zMS>X;9skRUnF&f)l|=)KgLm^~(^xOE-(*|E^si5%S1z(&8=lMiJC)MQ?T0&H8*Uvg zIAfujn+Gq^A^27N3iqbjlqqIPqAC@b&f7%`#|R3t{JAUrwZ`A)3Od)YMt;ft^=7`X z#-YFS;1KuvK2GU$)Q!+MQ8o$fsTH`+tJl7DdlW0-^)X8E++T?x{?nI^Q1L5(39zLJ z+a189uG>4bOl9BlyA7A7midT+kqjd}QaJGUPEA8!!QCH?t!MozI z%5?x4NR`70iEVeV-cD*eL>7SsR+3g(XSM5T(q*0GaeM`7wVY6nXMxWG1%3*`=d8R# zn-?J@>$@SHx(JMuyw;*sH~gCQcl@;;?9hF-6LK5y&((C4-;;320+3uM$8y=<7#GL2 zdf;_dCXW8kle*K^l|Aw~sxtw-5;pgWzS@BHsMOjEd$UW}A(p30@(2tT!eAy@X$cd& zWIWDtSp@2w;LX~kl(1W}-gH_TN$G|2UDF8~IJ&jREkV&!v}!jHFouB~ z5U;cFhR`{NApCj}gWB7=B>4>5sDHI3#>7-s96H2HK~-3Qt8J@PR#WNWYTe1;fGs&Z z%k>HLcjkt;PfFPn+9dki) zk*LTyV!c|NW`JA&a*|m%VN5F<>Wa$j)M|?pOG$TuhI+fss7Q?k4OGZTiYUjZHTAL5 zCbKrRQ?_nnCO;-iU@4sf*tvs^gLXy}>%Y@SV@B807Zcn=dKZ-NXD+syb2#+AG3WqANRB2wT-5SW#(fu-pa@+HQIx}om z2d3NDL+?ffJQ4ijMoNoj^f6>;{uf5mqLeing1qf&vjWLzFsvhk*4iOOsWb;1UcA6ggXwBlossE8uTPGR9bt43z#u2;u^m@|e2!;PIf@iz*5}$8CRAoS=O8AqWc>rtz@UMsOUOH| z)g10Vhdy}2yB5!<>69Z?*!xRZ!f4)hz?ENKs$11iLUE^Rt=qEoB(E3xOt$zTB#teu zj<#VOn!Fp7pCkY^6uNw4m)sPHU+rV{)w&iML;|o{NyLN-&bfvjXa}Zq zTe%Sw{!(7}hq32Sr;)fdkOX;o_hPdIC5ZsTG1)W-lydr?^^1XPgWhcO4om~3SfmE= zP$l$eQ1{=E?;=`B9>lqC{qo{!D|R8rZc-XFS*&dqt&|8^kvyW^b%rirV;@O{jpxUs zG>7iwbTD~H2{@8f1@p zrcz5jL%5w#^PWx{^|5U0kNI(5H~(GDc3l)l??Lz=zsKyOk?Du3c9k4FFrg5gkj5g( z%3XBv%+377i@~z)lje2pmlUrcze*TLA(JSNx?zrRXmXlwB_XqHI_2X<&N#XcS#Irr zIp9jZ*+pW|1F12P#Pdl?;)8aXOx5MTf=$^FkV95A&KJVZG!7dWJEN7!wx_`Cj;RRU zK4|8#5dl(?bX)x|jd89=?7x6^2+5UdAW|d8kMBS+xYd(ys|7GEGdRP|t|&|K7OfRA zv&7io4pRerW041!|=7h57sX++!R)A(3#&)3*NAfp*!m58%q9 zF-cK%z+IJ^qShH|vON4)V?~FRJ$}zO7`^k6$E3qV-}Z4_{I?V$hn_V~5qQ)io8o zotdtN_9R%CY}JFumAUQkVp#N6WG4;G!{4u%siCybN&}oC?&K1)GFt`hZZ+l-*KVfo zv}rAFr`nwzm-uiJh9E~lQ#?rnA9a*seGiU~_3XX+D{)%rPckwKWHMum1Q zzX2V@9wJF%b=%Vd9KAhpJ@we2F-W1)M~n0Wt!_=F|DXako_J~lw{H_#qZzWWpxqb6+|Umd?5`V3c{>7{im;!5wONI&^^91uSwff=EnxG`JZ8M*Rp0UMgRUwS96(2sO;y_ip zSQ&u~t)Q2ff-73B9HUZ}G&Q#gMRP)guVu~b#;+82OS;ZRWgx@WXa5%M@0RWL>bA|+ zO1qR*b5p4E*yHbpg(I{|>A<#5tQ^E}>`hP`dVvcn~*Rs&4ihO7`s_`v_mA z@m(yv5Wl#y_3yPFj}C@)wGd*w1wt;e66!M-kAWbPL&LNb+E1EFHmt=(Nn&Q-sDs=2 z)n;2A2SLQ|qhG3^aB6`bB4upjb6Fuxo#xkCf*}HEhl*b#M~0Phh%xJ;Xwh)-W8^SD z0`~7%%>uq#Db@#9uH_$oa(QGK%Aybi*hOvk7J{;{g@G-db5e=BV}je8QgVu3%hLPw zfGmw0cgzv9f`C8956R!PM~q`0K*B52i1C(uWbs&f8e`vV@ug$^-3GiplnM{(+>m8O zuDG>pztYFhBuLFW9104y^>>zdPAcDdm~Xq7XI$OYW1CjCzW` z{JdW2g@n04`ydNePS5ils|fhTfUd4I+(2H5;`O^bp)Iu1W5jZHCG@*OYtZ>%QRT|RDifu|=3Yslr=Y3u|qN{9cUf#iYjoNBb zq};$LV@u2{*AW{;5X6(8ctf}|Z4ajzW3czE=jK92+|pCYKJ_R!vN8YWq_&);?Bj&? zlOkY|*w`=xgK>+|86^bFVSB2=V3@(qihb+w^;jOY^}x_$nNYEK{7V+%OnzeRNdR#z zi*d(+I44<0E|dl9*mWitUjvv)k8w zc~oZmwQZ9=5UNszeTjKOn|19k0bN7zPd|`A|CX&U7gQZnm-`CXuf9aJ;-vVB+PA^9 zEa41aw}Of{V=5tS8_D?@i*urkoKA~6Uhky=r0n*o2h*wQG-prU#8)vpN9yceKqLUM%C^e?>&ZANywU-szlXptd^4>~X4 z*D}!NhwfH)5kE;`kqyqU`zhVUcHYT54aLd)E_C{>U{*ex!fgobuDRYx3DW60;SQJr<(O|gsslMh2Q+$XDF~Zn-AS>(8eYvX0I;{%aS*%wzk&7>-?1f zt92*o-g&;SUcnMl7|Ev8*(Tdl-{(s6;YqGW*wTp zBZW=y3D+TXtC=g;b((A{MNM2()IagyrtGZ6XGo@I$9DE!X5k)7v7KlJ8cq&)C9w0$ zJ|G*0I5Ehz=1vYsW+SE{{CKa2&8dS2kG9xWLF>i>+1JmM)aN7fpU7_Lm3Ta5`B>kGfXAYUNjDMrq&D0cczaFWCV)Q4$k(kQWUiqRVKI3xz^adofQ^oAyP2vou8O@t`^z|1}n@pg1sUIxE74bg8uZ#B5%DV zvo|gv*;8(Pu%#A^p_dMS_>Tj2ck8=d^&cqTmG2O$L#Dm++p7Gcb6@Ds7w-KAO!7kQ zTc3h=8~9rTn<@D8-53-^jI4z-k}69kqO-Yu5Z~RFA(unE)r{zstcB6S-GxY>R=?kb z0&Jzl-r3fTNt$#D0bgtrV6F1DY15dqR4NUhVXC4olS`ia%vT!+r&M66P2ny1v99W-U^NU4Sm zoj_6D`E8TLA}ojvUU7eH!9F&{Z51V`q$q6ztCd?xmlivz&tJj}+U28vwxf*O*o2RU z9y~dwR>xD4Xs)mjY^X8pbUyrYNbMvJrR(e6{#9{ype+SA^(CKpszH~k2=Ll9&kE%L zt4<&(Ew;`tjxuX-LeG`2f1XQ*{&bl+MyJpt()3o&(&EjAsp9!OcYou?E9}w3mvI8y zN&cx?mIgsFhT7x#COK?A6PE!mCH6u{T5DJZN&~16GkFp<(1gmY_6)?<(IE?2VUYTR z;*D~i6=`G3vR_clj^uH>i-${IvlQcu{CjQxksNSxZp7|H#8;Jd;VANj^LXAKV>S;D z88k4dR13Fz7ToYKyX@nRBG9(%6TsVgxHiaOGUL+4FCYMQJc=CS(UbO3cJD!KpqiR% z1X2|h8tfrJb5gX%rQ8TUyvjZU_a=Q@1(1aU*E)zaKFEpAPlx?x7sR^qTt<%0g07A< zQrV^uPI$bKZw`d^3;u^wS+!IvQ>6FOrruGLVE*lGgeAL&fJrPrYk7QJ`vemY`j^MK zd#emxfl|P(%PmP22npQfpdO6x^T} z%U?kVm*<9KtOtTn#MAZieL=hp#1YNcWPP5?Km32`;$Deoo1 z71hc;9y@yL4j_w9i>vuAgeh+hSc=*upgBR%5gSYEr0jf%Q4~bPqgA<)2r1<7C++t_P6JNgkUgcRNAIFEf&vmc zz8mGL-2;-UFwf7e{Cr8eM8YWt@(ylFLh4S;3|jv-euj&mi%VTHCgz=vZ2lh0y?5%) z?{%4JD!W$A8rKe22)y8DiECtyi+N_;<8Q^2=`wwjrl7%!&vg5A+17938JNKMOOl$a zx1qiIadYam+(_cTS#!hAS#S82S7KXGPavQCC^r9RI{qd&jeg{|TQUh9W&9+WNt1He z|Bfu}CPHVc$EzGSyCEEA%{=t4T<7_5c^X^LP$zQmC8JIlPv|2K&Bl_A1k)MjC5u-* z56085?1)ymj-h`StbOin!pJyLtlacYMrjgy--yTccPpKJ`44cYzSe$?H!^j||GCKk~mh7a} z7DWQ=X(U+p?DAEhSD0z~&jOZN1(`d~Atbm`Jj;yw2$;oXpT3f;qOHL{(IP^wRH!&6_68fKyW@LZL z&T+FOC%={PGHn0^lxj!~PA~HPD0kU*&nG%i=R?Aq)%xQYKK#gve64hEV0(fNp%e1JBw9N76 z`1qJMcSo*g=v=M=|jfk(2a1 zAWH71iIuLi>QT-wTKb<1!vyrqRg0SxRZJ*5=@}=YC6OUvBfOfE*=5)1>9Q$n=E*xx zhpEKBOx3)yyE;H#Que}>P}eQ5*rC$E{_rf;^AaT}RZTc9^NgQ(7gc*$#}ALceO}w{ z=Kx<8VRsciLA90={8A=P6TD*%vEVqGI~6BED;g24;8I-lpmBYa+t|2%t6}*4{Fil= zrg8U)|4SY^`i51Ak>KvEtv0j*jjQ7>hni?nFNFHjOWyP^!;33L@ExR(lmAQiHmA44 zwR>|7FxORIWS#QPuNNHqZXk`mn~P++y;w3;) zmpzR&JXE&Ny7PC%{>CXhL4MVWG}E?{PDCO)R@MpJt@y?6%lBjAJ^0iCm7!>FhlON| z%iGIZxGp)iB_=BG7uwT6cGSBM{7HTce7CdlwY_(TeXC3V(JV>0psDOhQR9mIXHx>z zH@)}gFW7{#;7VwwxVTiPa3K|MyDT&PZx|Dv!j#0xoV{c841c>J;ZEQVofqXnIs}5d z49yl+Un`%UG0^FC_;dH#iA~_)-K?)=u$1APxy($xxkp01#2h@y$uW61mnRyKuzKdw z?KI`&0`(Aoa5l%e5IyCdf*SDie*ccbRE|24KS7N6$@yA&Iit~}qrl}`a!0T!h3123 z=zGsmE726dkrR~dEh#QjwN2S0$-<|eQi}H3VRMYCW`hF+Ia zkKQgtSO2HGDSKP&$?(s`oP(Xw&lXMB=k&c^JxgdWa$A^~2}g}%2`qek;+TtRE(Kqv zVUTF>Jo&@7dt9H)_yJm`f2*C^k29atfeCUG#iSXF;!471~5mlb}FYLlFLb!=h>BYh>Gz?y@2*yMcb+ z{@8a$vP58s^u=h4A?Uyz%8?Q7+8t6#jDR{4o5K0K#YZs==kWnA-hQc0|6M$N?Ua!a zh;|oiCoX7_oMdgRiY~#Z9{0xiKk@1j5==~YveDF;_ZRc+z9XN7l%Ocg#W5z*@9f@b z$o$>=G;AyOr)ObMmUpG$D$IXq@s_JhrGyy^eCp~@pD&p zZ*{2a!v)3h1VK7s_KW@ zXBqLRU__4^!@Aq*2L)F~YU9odTN!O%vgIlq!(tm&={q_2=oHe+yDI&OY775eYB%C` zi9q|YUC<(LhC`7Q&sHOSfN3w8fU(`66RpD~B*-7?K}llKQ|(psa}v1yX0|BMwvgwqW;QZH@^{^?%or+sDOBDeim1*`QGUdY;@ARs0t>i&+ETx}lSVwPDIrI$B5aGRQ$J8$3ZW5R`(R{Y!cSU+mCSXq+Z zw8w8k#fXQ8XO+i^UJeIb0OY;TpwsqVKUt)~D2JV?;9>WdZF7)pw15N4=;=8;(Vq&q zKDE$f1mXz z)c$(Oh}^0eYG2;l5bDHBuB}uIg-C71gviph&}c8-d2MA)ToW5J-HpfgsEP=HK^o!| zBjh`N&P#2q^HITYgsY0YU{*#r{K>3<5a<=4T8I=->F*TEa>{zhw!Q%JB=Jgv;G}YA zQc{B?C=F4p>t=vp!5+{Sdr*t=0x}#RUBE(;o&zM7V;r^sK`m=ulKBcJG2s0c9A6)e zdmF10-Ij?=BzZZCH0d)p?3Q+Bp0*^DZAFwOK(oJIkq(2Z1)~4n$oCb9`yU%lZwzeB z<-0AhUiG_m!=W2kNrLi6eSHVWEh)^Z5&?Pz5aLfbe`7y~MUdWw4 z4fac#wSIm3$own9Io?P4I>FuX1?Ms`Uj;7 z(X5I~5B?(C#gY_cBm-Cdg#d70xOVt#d0CXn8o`##4p5bh`afI|{%lo49rELFTN?$j|r%XqS}ra zpIY{02^cMw<}O0aVfmoi-U3X9_q)Z;-EmU+t1T@)ijhDdM7Jjt=X5xK)3n5E31hN+ z0%7;5R%Te{*kOfg&0~V-%>ft0w-m^jW_6pJ3czg@#$m<(HMgq>xu~ly;bl+=+2lvy zs4-+kUdVam&c*ep6Pt(_60>%6J*O&`$T!F!sv@W~1FTW)->&y3PY@)X(3iUfxi0m; zN8_(dJj$C!gD8b^%bx48&^rEqbQ5AcU9bMU)_auS&Bk92nY;Q8{+Wj#&=tJ=AIHeu z`K`fp@J;O6^|G`i&c9y#+qYc`=qFtq3}6)eihFHuISUA!?}buz#xw1No4q3$pb3;C zKvMtEwjjG5Kzk>Qxmg&a6rmn#EQFChipPzwK)EPr%CIE!{a8Qu4Haq;E7|ARGPMcBxMJr2jFgLqTFN{TwR~c68WeT{|b32o! zu_%!8aEl&|7F0Lv-5vORCFYY)b{62(#m_w-xBtn+qD`erpR+T}P+`Xc134$_9J#FW zx{;itRXII&aP({5Y{DCjViNk5_?Od z(A}e-kX`LQIC~GyZa32@+N&eOqY6~~+Is|+rR}=muHJgoe!MVzZ@F6K0T6^5(gu=O zVEu7Z4tIsDM}0T2YjGa2iQ~tYaoh9ls`6ev<$W(P#3!oTV!9)^KVc#`(y^Di>sLrt zuwCW(?VHC>{%pMs9`DPlnc@Bsxqhf8O7{hK5H=aHN4Wa$FdRd*P{ zEpTo+NI6QN?iFd0PiX?GM{QXAJ=^MPd+-*gJad=4GJ?!rL53HWX1)#lsL}W z;h2UmX>03_ro*}86A+x20=`RyvZLDe+4GxxWG^~z4=U`wj|>b*lmjZu%F@LD5)Ha^ zP2M%yKpD^=NfNEC+rO3-2#Jmk2fv4imSJfK_k=B0Wa_M&ELN@*kV#|+c-;Oh2fRL? z)ErW=oaBApET}sY_Ut`-(jH8F-06C8)Rl7*xFuG8JU?CagdXfCs#fggTdfKG#!Fy$!+(u9xUaOS-Sd$h6HKhXWhR| zO-;4lFZa{~E}lI$&>Uo175{t%u+=eQA5#DvTmL2zJO9+5t#`Jk$~R1=PwWR{QOl3? zZV-xMVqWZPUyzX%h4TYf(IF%$p=zi=QzcS$mTWAxJvqnZ`-_a?(J&RCbBHa03rvhG7XuD!1! zyW$z#*7(H+wUxTErlzLMR}MPJA~D25Ite*B-PkYawmcj(G$l06gF{36&CSimWQFVl zE?BiIQ0|g*Z)vabXjNrp;f4ogqx?z=9XexejtBRIymp&d+puNCag}e)i76?<;G`t3 zB#9AA%*;!^{*!B+b{6>$NL@WWLSLMcP5*&0l=A9o;M&rX)@nP?gGsDrwI1a5(=!bX z4O+Lyoh1f>qOy@V1&E!>^;0hY<<`r>c`sgk^;?i=SnC9qm?7ZY>GOk4>01s2G=xQn X3wf1`XyoU;TL_tNiW1dgpn(4aGkUu% literal 0 HcmV?d00001 diff --git a/plugin/think-plugs-static/stc/public/static/plugs/ztree/zTreeStyle/zTreeStyle.css b/plugin/think-plugs-static/stc/public/static/plugs/ztree/zTreeStyle/zTreeStyle.css new file mode 100644 index 000000000..4a1705b14 --- /dev/null +++ b/plugin/think-plugs-static/stc/public/static/plugs/ztree/zTreeStyle/zTreeStyle.css @@ -0,0 +1,97 @@ +/*------------------------------------- +zTree Style + +version: 3.5.19 +author: Hunter.z +email: hunter.z@263.net +website: http://code.google.com/p/jquerytree/ + +-------------------------------------*/ + +.ztree * {padding:0; margin:0; font-size:12px; font-family: Verdana, Arial, Helvetica, AppleGothic, sans-serif} +.ztree {margin:0; padding:5px; color:#333} +.ztree li{padding:0; margin:0; list-style:none; line-height:14px; text-align:left; white-space:nowrap; outline:0} +.ztree li ul{ margin:0; padding:0 0 0 18px} +.ztree li ul.line{ background:url(./img/line_conn.gif) 0 0 repeat-y;} + +.ztree li a {padding:1px 3px 0 0; margin:0; cursor:pointer; height:17px; color:#333; background-color: transparent; + text-decoration:none; vertical-align:top; display: inline-block} +.ztree li a:hover {text-decoration:underline} +.ztree li a.curSelectedNode {padding-top:0px; background-color:#FFE6B0; color:black; height:16px; border:1px #FFB951 solid; opacity:0.8;} +.ztree li a.curSelectedNode_Edit {padding-top:0px; background-color:#FFE6B0; color:black; height:16px; border:1px #FFB951 solid; opacity:0.8;} +.ztree li a.tmpTargetNode_inner {padding-top:0px; background-color:#316AC5; color:white; height:16px; border:1px #316AC5 solid; + opacity:0.8; filter:alpha(opacity=80)} +.ztree li a.tmpTargetNode_prev {} +.ztree li a.tmpTargetNode_next {} +.ztree li a input.rename {height:14px; width:80px; padding:0; margin:0; + font-size:12px; border:1px #7EC4CC solid; *border:0px} +.ztree li span {line-height:16px; margin-right:2px} +.ztree li span.button {line-height:0; margin:0; width:16px; height:16px; display: inline-block; vertical-align:middle; + border:0 none; cursor: pointer;outline:none; + background-color:transparent; background-repeat:no-repeat; background-attachment: scroll; + background-image:url("./img/zTreeStandard.png"); *background-image:url("./img/zTreeStandard.gif")} + +.ztree li span.button.chk {width:13px; height:13px; margin:0 3px 0 0; cursor: auto} +.ztree li span.button.chk.checkbox_false_full {background-position:0 0} +.ztree li span.button.chk.checkbox_false_full_focus {background-position:0 -14px} +.ztree li span.button.chk.checkbox_false_part {background-position:0 -28px} +.ztree li span.button.chk.checkbox_false_part_focus {background-position:0 -42px} +.ztree li span.button.chk.checkbox_false_disable {background-position:0 -56px} +.ztree li span.button.chk.checkbox_true_full {background-position:-14px 0} +.ztree li span.button.chk.checkbox_true_full_focus {background-position:-14px -14px} +.ztree li span.button.chk.checkbox_true_part {background-position:-14px -28px} +.ztree li span.button.chk.checkbox_true_part_focus {background-position:-14px -42px} +.ztree li span.button.chk.checkbox_true_disable {background-position:-14px -56px} +.ztree li span.button.chk.radio_false_full {background-position:-28px 0} +.ztree li span.button.chk.radio_false_full_focus {background-position:-28px -14px} +.ztree li span.button.chk.radio_false_part {background-position:-28px -28px} +.ztree li span.button.chk.radio_false_part_focus {background-position:-28px -42px} +.ztree li span.button.chk.radio_false_disable {background-position:-28px -56px} +.ztree li span.button.chk.radio_true_full {background-position:-42px 0} +.ztree li span.button.chk.radio_true_full_focus {background-position:-42px -14px} +.ztree li span.button.chk.radio_true_part {background-position:-42px -28px} +.ztree li span.button.chk.radio_true_part_focus {background-position:-42px -42px} +.ztree li span.button.chk.radio_true_disable {background-position:-42px -56px} + +.ztree li span.button.switch {width:18px; height:18px} +.ztree li span.button.root_open{background-position:-92px -54px} +.ztree li span.button.root_close{background-position:-74px -54px} +.ztree li span.button.roots_open{background-position:-92px 0} +.ztree li span.button.roots_close{background-position:-74px 0} +.ztree li span.button.center_open{background-position:-92px -18px} +.ztree li span.button.center_close{background-position:-74px -18px} +.ztree li span.button.bottom_open{background-position:-92px -36px} +.ztree li span.button.bottom_close{background-position:-74px -36px} +.ztree li span.button.noline_open{background-position:-92px -72px} +.ztree li span.button.noline_close{background-position:-74px -72px} +.ztree li span.button.root_docu{ background:none;} +.ztree li span.button.roots_docu{background-position:-56px 0} +.ztree li span.button.center_docu{background-position:-56px -18px} +.ztree li span.button.bottom_docu{background-position:-56px -36px} +.ztree li span.button.noline_docu{ background:none;} + +.ztree li span.button.ico_open{margin-right:2px; background-position:-110px -16px; vertical-align:top; *vertical-align:middle} +.ztree li span.button.ico_close{margin-right:2px; background-position:-110px 0; vertical-align:top; *vertical-align:middle} +.ztree li span.button.ico_docu{margin-right:2px; background-position:-110px -32px; vertical-align:top; *vertical-align:middle} +.ztree li span.button.edit {margin-right:2px; background-position:-110px -48px; vertical-align:top; *vertical-align:middle} +.ztree li span.button.remove {margin-right:2px; background-position:-110px -64px; vertical-align:top; *vertical-align:middle} + +.ztree li span.button.ico_loading{margin-right:2px; background:url(./img/loading.gif) no-repeat scroll 0 0 transparent; vertical-align:top; *vertical-align:middle} + +ul.tmpTargetzTree {background-color:#FFE6B0; opacity:0.8; filter:alpha(opacity=80)} + +span.tmpzTreeMove_arrow {width:16px; height:16px; display: inline-block; padding:0; margin:2px 0 0 1px; border:0 none; position:absolute; + background-color:transparent; background-repeat:no-repeat; background-attachment: scroll; + background-position:-110px -80px; background-image:url("./img/zTreeStandard.png"); *background-image:url("./img/zTreeStandard.gif")} + +ul.ztree.zTreeDragUL {margin:0; padding:0; position:absolute; width:auto; height:auto;overflow:hidden; background-color:#cfcfcf; border:1px #00B83F dotted; opacity:0.8; filter:alpha(opacity=80)} +.zTreeMask {z-index:10000; background-color:#cfcfcf; opacity:0.0; filter:alpha(opacity=0); position:absolute} + +/* level style*/ +/*.ztree li span.button.level0 { + display:none; +} +.ztree li ul.level0 { + padding:0; + background:none; +}*/ \ No newline at end of file diff --git a/plugin/think-plugs-static/stc/public/static/plugs/ztree/ztree.all.min.js b/plugin/think-plugs-static/stc/public/static/plugs/ztree/ztree.all.min.js new file mode 100644 index 000000000..274165d44 --- /dev/null +++ b/plugin/think-plugs-static/stc/public/static/plugs/ztree/ztree.all.min.js @@ -0,0 +1,3 @@ +!function($){var settings={},roots={},caches={},_consts={className:{BUTTON:"button",LEVEL:"level",ICO_LOADING:"ico_loading",SWITCH:"switch",NAME:"node_name"},event:{NODECREATED:"ztree_nodeCreated",CLICK:"ztree_click",EXPAND:"ztree_expand",COLLAPSE:"ztree_collapse",ASYNC_SUCCESS:"ztree_async_success",ASYNC_ERROR:"ztree_async_error",REMOVE:"ztree_remove",SELECTED:"ztree_selected",UNSELECTED:"ztree_unselected"},id:{A:"_a",ICON:"_ico",SPAN:"_span",SWITCH:"_switch",UL:"_ul"},line:{ROOT:"root",ROOTS:"roots",CENTER:"center",BOTTOM:"bottom",NOLINE:"noline",LINE:"line"},folder:{OPEN:"open",CLOSE:"close",DOCU:"docu"},node:{CURSELECTED:"curSelectedNode"}},_setting={treeId:"",treeObj:null,view:{addDiyDom:null,autoCancelSelected:!0,dblClickExpand:!0,expandSpeed:"fast",fontCss:{},nameIsHTML:!1,selectedMulti:!0,showIcon:!0,showLine:!0,showTitle:!0,txtSelectedEnable:!1},data:{key:{isParent:"isParent",children:"children",name:"name",title:"",url:"url",icon:"icon"},simpleData:{enable:!1,idKey:"id",pIdKey:"pId",rootPId:null},keep:{parent:!1,leaf:!1}},async:{enable:!1,contentType:"application/x-www-form-urlencoded",type:"post",dataType:"text",headers:{},xhrFields:{},url:"",autoParam:[],otherParam:[],dataFilter:null},callback:{beforeAsync:null,beforeClick:null,beforeDblClick:null,beforeRightClick:null,beforeMouseDown:null,beforeMouseUp:null,beforeExpand:null,beforeCollapse:null,beforeRemove:null,onAsyncError:null,onAsyncSuccess:null,onNodeCreated:null,onClick:null,onDblClick:null,onRightClick:null,onMouseDown:null,onMouseUp:null,onExpand:null,onCollapse:null,onRemove:null}},_initRoot=function(e){var t=data.getRoot(e);t||(t={},data.setRoot(e,t)),data.nodeChildren(e,t,[]),t.expandTriggerFlag=!1,t.curSelectedList=[],t.noSelection=!0,t.createdNodes=[],t.zId=0,t._ver=(new Date).getTime()},_initCache=function(e){var t=data.getCache(e);t||(t={},data.setCache(e,t)),t.nodes=[],t.doms=[]},_bindEvent=function(d){var e=d.treeObj,t=consts.event;e.bind(t.NODECREATED,function(e,t,n){tools.apply(d.callback.onNodeCreated,[e,t,n])}),e.bind(t.CLICK,function(e,t,n,o,a){tools.apply(d.callback.onClick,[t,n,o,a])}),e.bind(t.EXPAND,function(e,t,n){tools.apply(d.callback.onExpand,[e,t,n])}),e.bind(t.COLLAPSE,function(e,t,n){tools.apply(d.callback.onCollapse,[e,t,n])}),e.bind(t.ASYNC_SUCCESS,function(e,t,n,o){tools.apply(d.callback.onAsyncSuccess,[e,t,n,o])}),e.bind(t.ASYNC_ERROR,function(e,t,n,o,a,r){tools.apply(d.callback.onAsyncError,[e,t,n,o,a,r])}),e.bind(t.REMOVE,function(e,t,n){tools.apply(d.callback.onRemove,[e,t,n])}),e.bind(t.SELECTED,function(e,t,n){tools.apply(d.callback.onSelected,[t,n])}),e.bind(t.UNSELECTED,function(e,t,n){tools.apply(d.callback.onUnSelected,[t,n])})},_unbindEvent=function(e){var t=e.treeObj,n=consts.event;t.unbind(n.NODECREATED).unbind(n.CLICK).unbind(n.EXPAND).unbind(n.COLLAPSE).unbind(n.ASYNC_SUCCESS).unbind(n.ASYNC_ERROR).unbind(n.REMOVE).unbind(n.SELECTED).unbind(n.UNSELECTED)},_eventProxy=function(e){var t=e.target,n=data.getSetting(e.data.treeId),o="",a=null,r="",d="",i=null,s=null,l=null;if(tools.eqs(e.type,"mousedown")?d="mousedown":tools.eqs(e.type,"mouseup")?d="mouseup":tools.eqs(e.type,"contextmenu")?d="contextmenu":tools.eqs(e.type,"click")?tools.eqs(t.tagName,"span")&&null!==t.getAttribute("treeNode"+consts.id.SWITCH)?(o=tools.getNodeMainDom(t).id,r="switchNode"):(l=tools.getMDom(n,t,[{tagName:"a",attrName:"treeNode"+consts.id.A}]))&&(o=tools.getNodeMainDom(l).id,r="clickNode"):tools.eqs(e.type,"dblclick")&&(d="dblclick",(l=tools.getMDom(n,t,[{tagName:"a",attrName:"treeNode"+consts.id.A}]))&&(o=tools.getNodeMainDom(l).id,r="switchNode")),0=r.length&&(n=-1):(r=data.nodeChildren(e,t,[]),n=-1),0=u.length-n.length)&&(a=-1);for(var p=0,f=n.length;p/g,">");e.push("",a,"")},makeDOMNodeLine:function(e,t,n){e.push("")},makeDOMNodeMainAfter:function(e,t,n){e.push("
            • ")},makeDOMNodeMainBefore:function(e,t,n){e.push("
            • ")},makeDOMNodeNameAfter:function(e,t,n){e.push("")},makeDOMNodeNameBefore:function(e,t,n){var o=data.nodeTitle(t,n),a=view.makeNodeUrl(t,n),r=view.makeNodeFontCss(t,n),d=[];for(var i in r)d.push(i,":",r[i],";");e.push("/g,">"),"'"),e.push(">")},makeNodeFontCss:function(e,t){var n=tools.apply(e.view.fontCss,[e.treeId,t],e.view.fontCss);return n&&"function"!=typeof n?n:{}},makeNodeIcoClass:function(e,t){var n=["ico"];if(!t.isAjaxing){var o=data.nodeIsParent(e,t);n[0]=(t.iconSkin?t.iconSkin+"_":"")+n[0],o?n.push(t.open?consts.folder.OPEN:consts.folder.CLOSE):n.push(consts.folder.DOCU)}return consts.className.BUTTON+" "+n.join("_")},makeNodeIcoStyle:function(e,t){var n=[];if(!t.isAjaxing){var o=data.nodeIsParent(e,t)&&t.iconOpen&&t.iconClose?t.open?t.iconOpen:t.iconClose:t[e.data.key.icon];o&&n.push("background:url(",o,") 0 0 no-repeat;"),0!=e.view.showIcon&&tools.apply(e.view.showIcon,[e.treeId,t],!0)||n.push("display:none;")}return n.join("")},makeNodeLineClass:function(e,t){var n=[];return e.view.showLine?0==t.level&&t.isFirstNode&&t.isLastNode?n.push(consts.line.ROOT):0==t.level&&t.isFirstNode?n.push(consts.line.ROOTS):t.isLastNode?n.push(consts.line.BOTTOM):n.push(consts.line.CENTER):n.push(consts.line.NOLINE),data.nodeIsParent(e,t)?n.push(t.open?consts.folder.OPEN:consts.folder.CLOSE):n.push(consts.folder.DOCU),view.makeNodeLineClassEx(t)+n.join("_")},makeNodeLineClassEx:function(e){return consts.className.BUTTON+" "+consts.className.LEVEL+e.level+" "+consts.className.SWITCH+" "},makeNodeTarget:function(e){return e.target||"_blank"},makeNodeUrl:function(e,t){var n=e.data.key.url;return t[n]?t[n]:null},makeUlHtml:function(e,t,n,o){n.push("
                "),n.push(o),n.push("
              ")},makeUlLineClass:function(e,t){return e.view.showLine&&!t.isLastNode?consts.line.LINE:""},removeChildNodes:function(e,t){if(t){var n=data.nodeChildren(e,t);if(n){for(var o=0,a=n.length;on.bottom||o.right>n.right||o.left
            • ",Z)).append(Pe(d,he.id.A,Z).clone()),r.css("padding","0"),r.children("#"+d.tId+he.id.A).removeClass(he.node.CURSELECTED),te.append(r),t==Z.edit.drag.maxShowNodeNum-1&&(r=Pe("
            • ...
            • ",Z),te.append(r)));te.attr("id",ee[0].tId+he.id.UL+"_tmp"),te.addClass(Z.treeObj.attr("class")),te.appendTo(ie),(oe=Pe("",Z)).attr("id","zTreeMove_arrow_tmp"),oe.appendTo(ie),Z.treeObj.trigger(he.event.DRAG,[e,Z.treeId,ee])}if(1==$.dragFlag){if(de&&oe.attr("id")==e.target.id&&ue&&e.clientX+ae.scrollLeft()+2>fe("#"+ue+he.id.A,de).offset().left){var s=fe("#"+ue+he.id.A,de);e.target=0Z.edit.drag.borderMin,b=fZ.edit.drag.borderMin,R=EZ.edit.drag.borderMin,P=IZ.edit.drag.borderMin,C=T>Z.edit.drag.borderMin&&f>Z.edit.drag.borderMin&&E>Z.edit.drag.borderMin&&I>Z.edit.drag.borderMin,w=h&&se.treeObj.scrollTop()<=0,M=b&&se.treeObj.scrollTop()+se.treeObj.height()+10>=m,_=R&&se.treeObj.scrollLeft()<=0,O=P&&se.treeObj.scrollLeft()+se.treeObj.width()+10>=p;if(e.target&&Ie.isChildOrSelf(e.target,se.treeId)){for(var D=e.target;D&&D.tagName&&!Ie.eqs(D.tagName,"li")&&D.id!=se.treeId;)D=D.parentNode;var y=!0;for(t=0,o=ee.length;tse.edit.drag.autoOpenTime&&Ie.apply(se.callback.beforeDragOpen,[se.treeId,A],!0)&&(be.switchNode(se,A),se.edit.drag.autoExpandTrigger&&se.treeObj.trigger(he.event.EXPAND,[se.treeId,A]))},se.edit.drag.autoOpenTime+50),window.zTreeMoveTargetNodeTId=A.tId)}}else F()}else ge=he.move.TYPE_INNER,de&&Ie.apply(se.edit.drag.inner,[se.treeId,ee,null],!!se.edit.drag.inner)?de.addClass(he.node.TMPTARGET_TREE):de=null,oe.css({display:"none"}),window.zTreeMoveTimer&&(clearTimeout(window.zTreeMoveTimer),window.zTreeMoveTargetNodeTId=null);ce=ue,Ne=ge,Z.treeObj.trigger(he.event.DRAGMOVE,[e,Z.treeId,ee])}return!1}function Te(d){if(window.zTreeMoveTimer&&(clearTimeout(window.zTreeMoveTimer),window.zTreeMoveTargetNodeTId=null),Ne=ce=null,ae.unbind("mousemove",s),ae.unbind("mouseup",Te),ae.unbind("selectstart",c),ie.css("cursor",""),de&&(de.removeClass(he.node.TMPTARGET_TREE),ue&&fe("#"+ue+he.id.A,de).removeClass(he.node.TMPTARGET_NODE+"_"+he.move.TYPE_PREV).removeClass(he.node.TMPTARGET_NODE+"_"+Ee.move.TYPE_NEXT).removeClass(he.node.TMPTARGET_NODE+"_"+Ee.move.TYPE_INNER)),Ie.showIfameMask(Z,!1),J.showHoverDom=!0,0!=$.dragFlag){var e,t,o;for(e=$.dragFlag=0,t=ee.length;e
              ",e);l.appendTo(Pe("body",e)),o.dragMaskList.push(l)}}},view:{addEditBtn:function(e,t){if(!(t.editNameFlag||0