[
  {
    "path": ".gitignore",
    "content": "/.idea/\n/.vscode/\n/.hbuilderx/\n/test"
  },
  {
    "path": "LICENSE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "MineAdmin/php/app/Ai/Api/BaseApi.php",
    "content": "<?php\ndeclare(strict_types=1);\n\nnamespace App\\Ai\\Api;\n\nuse Hyperf\\DbConnection\\Db;\nuse Mine\\MineController;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Http\\Message\\ResponseInterface;\n\nclass BaseApi extends MineController\n{\n    public function checkParameter(array $params, array $checkData = [], bool $is_set = false): bool\n    {\n        if (empty($checkData)) {\n            return true;\n        }\n\n        if ($is_set) {\n            foreach ($checkData as $v) {\n                if (!isset($params[$v])) {\n                    return false;\n                }\n            }\n        } else {\n            foreach ($checkData as $v) {\n                if (empty($params[$v])) {\n                    return false;\n                }\n            }\n        }\n        return true;\n    }\n\n    /**\n     * @throws ContainerExceptionInterface\n     * @throws \\Throwable\n     * @throws NotFoundExceptionInterface\n     */\n    public function funCallbackRes(callable $callback): ResponseInterface\n    {\n        return $this->success($this->funCallback($callback));\n    }\n\n    public function funCallback(callable $callback)\n    {\n        Db::beginTransaction();\n        try {\n            $res = $callback();\n            Db::commit();\n        } catch (\\Throwable $throwable) {\n            Db::rollBack();\n            throw $throwable;\n        }\n        return $res;\n    }\n}"
  },
  {
    "path": "MineAdmin/php/app/Ai/Api/Chat.php",
    "content": "<?php\ndeclare(strict_types=1);\n\nnamespace App\\Ai\\Api;\n\nuse App\\Ai\\Middleware\\AuthMiddleware;\nuse App\\Ai\\Service\\AiChatgptPromptsService;\nuse App\\Ai\\Service\\AiChatMessageService;\nuse App\\Ai\\Service\\AiChatSessionService;\nuse App\\Ai\\Service\\AiLoginService;\nuse App\\Ai\\Service\\AiOpenaiKeyService;\nuse App\\Ai\\Service\\AiQuickIssueService;\nuse Hyperf\\Context\\ApplicationContext;\nuse Hyperf\\Di\\Annotation\\Inject;\nuse Hyperf\\Engine\\Http\\EventStream;\nuse Hyperf\\HttpServer\\Annotation\\Controller;\nuse Hyperf\\HttpServer\\Annotation\\GetMapping;\nuse Hyperf\\HttpServer\\Annotation\\Middleware;\nuse Hyperf\\HttpServer\\Annotation\\PostMapping;\nuse Orhanerday\\OpenAi\\OpenAi;\nuse Psr\\Http\\Message\\ResponseInterface;\n\n#[Controller(prefix: \"ai/api/chat\")]\n#[Middleware(AuthMiddleware::class)]\nclass Chat extends BaseApi\n{\n    #[Inject]\n    protected AiLoginService $loginService;\n\n    #[Inject]\n    protected AiChatgptPromptsService $chatgptPromptsService;\n\n    #[Inject]\n    protected AiChatSessionService $chatSessionService;\n\n    #[Inject]\n    protected AiChatMessageService $chatMessageService;\n\n    #[Inject]\n    protected AiQuickIssueService $quickIssueService;\n\n    #[GetMapping(\"roles\")]\n    public function roles(): ResponseInterface\n    {\n        return $this->success([\n            'list' => $this->chatgptPromptsService->getList([\n                'select'    => 'id,act,prompt',\n                'orderBy'   => 'sort',\n                'orderType' => 'desc',\n            ], false)\n        ]);\n    }\n\n    #[GetMapping(\"model-list\")]\n    public function modelList(): ResponseInterface\n    {\n        return $this->success(AiChatgptPromptsService::MODEL_LIST);\n    }\n\n    #[GetMapping(\"session\")]\n    public function session(): ResponseInterface\n    {\n        return $this->success($this->chatSessionService->session($this->request->all()));\n    }\n\n    #[PostMapping(\"session-close\")]\n    public function sessionClose(): ResponseInterface\n    {\n        return $this->success([\n            'sid' => $this->chatSessionService->sessionClose($this->request->all())\n        ]);\n    }\n\n    #[GetMapping(\"messages\")]\n    public function messages(): ResponseInterface\n    {\n        // next、prev\n        $sid     = $this->request->query('sid');\n        $session = $this->chatSessionService->mapper->session([\n            'id'  => $sid,\n            'uid' => $this->loginService->getId()\n        ]);\n        if (empty($session)) {\n            return $this->success([\n                'list' => []\n            ]);\n        }\n        return $this->success([\n            'list' => $this->chatMessageService->messages($this->request->all())\n        ]);\n    }\n\n    #[GetMapping(\"session-history\")]\n    public function sessionHistory(): ResponseInterface\n    {\n        return $this->success([\n            'list' => $this->chatSessionService->sessionHistory($this->request->all())\n        ]);\n    }\n\n    #[PostMapping(\"session-share\")]\n    public function sessionShare(): ResponseInterface\n    {\n        $model = $this->chatSessionService->mapper->session([\n            'id'  => $this->request->post('id'),\n            'uid' => $this->loginService->getId()\n        ]);\n\n        if (empty($model) || ($this->request->post('is_share', false) && $model->share === 2)) {\n            return $this->success();\n        }\n        $model->share = $model->share === 1 ? 2 : 1;\n        $model->save();\n        return $this->success();\n    }\n\n    #[PostMapping(\"session-delete\")]\n    public function sessionDelete(): ResponseInterface\n    {\n        $model = $this->chatSessionService->mapper->session([\n            'id'  => $this->request->post('id'),\n            'uid' => $this->loginService->getId()\n        ]);\n        if (empty($model)) {\n            return $this->success();\n        }\n        $model->delete();\n        $this->chatMessageService->mapper->getModel()::where([\n            'sid' => $this->request->post('id')\n        ])->delete();\n        return $this->success();\n    }\n\n    #[GetMapping(\"session-share-list\")]\n    public function sessionShareList(): ResponseInterface\n    {\n        return $this->success([\n            'list' => $this->chatSessionService->sessionShareList($this->request->all())\n        ]);\n    }\n\n    #[GetMapping(\"session-share-message-list\")]\n    public function sessionShareMessageList(): ResponseInterface\n    {\n        $sid     = (int)$this->request->query('sid');\n        $session = $this->chatSessionService->read($sid);\n        if (empty($session) || $session->share === 1) {\n            return $this->success(['list' => []]);\n        }\n        return $this->success([\n            'list' => $this->chatMessageService->shareMessageList($this->request->all())\n        ]);\n    }\n\n    #[GetMapping(\"quick-issue\")]\n    public function quickIssue(): ResponseInterface\n    {\n        return $this->success([\n            'list' => $this->quickIssueService->getList([\n                'select'    => 'id,title,content',\n                'orderBy'   => 'id',\n                'orderType' => 'desc',\n            ], false)\n        ]);\n    }\n\n//    #[GetMapping(\"test\")]\n    public function test()\n    {\n\n        $token = $this->request->query('x-token', null);\n        if ($token) {\n            $token = 'Bearer ' . $token;\n            //todo 验证\n        }\n\n        $opts = [\n            'model'             => 'gpt-3.5-turbo',\n            'messages'          => [],\n            'temperature'       => 1.0,\n            'max_tokens'        => 150,\n            'frequency_penalty' => 0,\n            'presence_penalty'  => 0,\n            'stream'            => true,\n        ];\n        $opts['messages'][] = ['role' => 'system', 'content' => \"你是一个AI助手，我需要你模拟一名温柔贴心的女朋友来回答我的问题。\"];\n        $opts['messages'][] = [\n            'role'    => 'user',\n            'content' => 'php如何计算1+1?',\n        ];\n        $response      = ApplicationContext::getContainer()->get(\\Hyperf\\HttpServer\\Contract\\ResponseInterface::class);\n        $eventStream   = new EventStream($response->getConnection(), $response);\n        $openAiService = ApplicationContext::getContainer()->get(AiOpenaiKeyService::class);\n        $key           = $openAiService->openAiKey();\n        $openAI        = new OpenAi($key);\n        $baseUrl       = $openAiService->openaiProxy();\n        $baseUrl && $openAI->setBaseURL($baseUrl);\n        $openAI->setHeader([\"Content-Type\" => \"text/event-stream\"]);\n        // 本次所有结果\n        $replyContent = '';\n        $openAI->chat($opts, function ($curl_info, $data) use ($eventStream, &$replyContent) {\n            $datas = explode('data: ', $data);\n            foreach ($datas as $dataStr) {\n                $arrayData = json_decode($dataStr, true);\n                if ($arrayData) {\n                    if (isset($arrayData['choices'][0]['delta']['content'])) {\n                        $replyContent .= ($content = $arrayData['choices'][0]['delta']['content'] ?? '');\n                        $eventStream->write(\"data: \" . json_encode([\n                                'status' => 1,\n                                'content' => $content,\n                            ]) . PHP_EOL . PHP_EOL\n                        );\n                    } elseif (!empty($arrayData['choices'][0]['finish_reason'])) {\n                        $eventStream->write(\"data: \" . json_encode([\n                                'status' => 2,\n                                'content' => '',\n                            ]) . PHP_EOL . PHP_EOL\n                        );\n                    } elseif (isset($arrayData['error'])) {\n                        $eventStream->write(\"data: \" . json_encode([\n                                'status' => 0,\n                                'content' => $arrayData['error']['message'],\n                            ]) . PHP_EOL . PHP_EOL\n                        );\n                    }\n                }\n            }\n            return \\strlen($data);\n        });\n        return \"data: \" . PHP_EOL . PHP_EOL;\n    }\n}"
  },
  {
    "path": "MineAdmin/php/app/Ai/Api/Common.php",
    "content": "<?php\ndeclare(strict_types=1);\n\nnamespace App\\Ai\\Api;\n\nuse App\\Ai\\Constants\\VipConst;\nuse App\\Ai\\Service\\AiLoginService;\nuse App\\Ai\\Service\\AiSettingService;\nuse App\\Ai\\Service\\AiVipService;\nuse App\\Ai\\Middleware\\AuthMiddleware;\nuse App\\Ai\\Constants\\UploadSceneConst;\nuse App\\Ai\\Service\\QiniuService;\nuse Hyperf\\Di\\Annotation\\Inject;\nuse Hyperf\\HttpServer\\Annotation\\Controller;\nuse Hyperf\\HttpServer\\Annotation\\GetMapping;\nuse Hyperf\\HttpServer\\Annotation\\Middleware;\nuse Psr\\Http\\Message\\ResponseInterface;\n\n#[Controller(prefix: \"ai/api/public\")]\n#[Middleware(AuthMiddleware::class)]\nclass Common extends BaseApi\n{\n    #[Inject]\n    protected QiniuService $qiniuService;\n\n    #[Inject]\n    protected AiLoginService $loginService;\n\n    #[Inject]\n    protected AiVipService $vipService;\n\n    #[Inject]\n    protected AiSettingService $settingService;\n\n    /**\n     * 全局场景值\n     */\n    #[GetMapping(\"init\")]\n    public function init(): ResponseInterface\n    {\n        $data = [];\n        // 图片资源上传\n        $data['scene'] = [\n            'upload' => [\n                'image'  => array_filter(UploadSceneConst::ImageScene, function ($key) {\n                    return str_starts_with($key, 'ai_');\n                }, ARRAY_FILTER_USE_KEY),\n                // 远端访问主域名, 用于前端上传时判断是否本地文件\n                'domain' => config('file.storage.qiniu.host'),\n            ],\n            'tutorials' => [],\n            'agreement' => [\n                'user' => $this->settingService->agreementUser(),\n            ],\n            'ads' => [],\n        ];\n\n        $data['other'] = [\n            'copyright'     => 'Copyright © 1999 - ' . date('Y') . ' ByAi',\n            'version'       => Login::VERSIONS,\n            'customer_info' => $this->settingService->customer(),\n        ];\n        return $this->success($data);\n    }\n\n    #[GetMapping(\"upload-token\")]\n    public function uploadToken(): ResponseInterface\n    {\n        return $this->success($this->qiniuService->token($this->request->query('scenes')));\n    }\n\n    #[GetMapping(\"vip-config\")]\n    public function vipConfig(): ResponseInterface\n    {\n        [$config, $user] = $this->vipService->config($this->loginService->getId());\n        $configCp = array_column(VipConst::config(), null, 'level');\n        $data = [\n            'equity' => [\n                [\n                    [\n                        'text'  => '特权',\n                        'color' => '',\n                    ],\n                    [\n                        'text'  => $configCp[VipConst::VIP]['name'],\n                        'color' => '',\n                    ],\n                    [\n                        'text'  => $configCp[VipConst::VIP_ONE]['name'],\n                        'color' => '',\n                    ],\n                    [\n                        'text'  => $configCp[VipConst::VIP_TWO]['name'],\n                        'color' => '',\n                    ],\n                ],\n                [\n                    [\n                        'text'  => '使用时长(月)',\n                        'color' => '',\n                    ],\n                    [\n                        'text'  => $configCp[VipConst::VIP]['length'],\n                        'color' => '',\n                    ],\n                    [\n                        'text'  => $configCp[VipConst::VIP_ONE]['length'],\n                        'color' => '',\n                    ],\n                    [\n                        'text'  => $configCp[VipConst::VIP_TWO]['length'],\n                        'color' => 'red',\n                    ],\n                ],\n                [\n                    [\n                        'text'  => 'VIP抵扣包',\n                        'color' => '',\n                    ],\n                    [\n                        'text'  => $configCp[VipConst::VIP]['wrap_vip'],\n                        'color' => '',\n                    ],\n                    [\n                        'text'  => $configCp[VipConst::VIP_ONE]['wrap_vip'],\n                        'color' => '',\n                    ],\n                    [\n                        'text'  => $configCp[VipConst::VIP_TWO]['wrap_vip'],\n                        'color' => 'red',\n                    ],\n                ],\n                [\n                    [\n                        'text'  => '推广赚(%)',\n                        'color' => '',\n                    ],\n                    [\n                        'text'  => $configCp[VipConst::VIP]['income'],\n                        'color' => '',\n                    ],\n                    [\n                        'text'  => $configCp[VipConst::VIP_ONE]['income'],\n                        'color' => '',\n                    ],\n                    [\n                        'text'  => $configCp[VipConst::VIP_TWO]['income'],\n                        'color' => 'red',\n                    ],\n                ],\n                [\n                    [\n                        'text'  => '聊天上下文',\n                        'color' => '',\n                    ],\n                    [\n                        'text'  => '不支持',\n                        'color' => '',\n                    ],\n                    [\n                        'text'  => '支持',\n                        'color' => '',\n                    ],\n                    [\n                        'text'  => '支持',\n                        'color' => 'red',\n                    ],\n                ],\n                [\n                    [\n                        'text'  => '每日次数',\n                        'color' => '',\n                    ],\n                    [\n                        'text'  => '19',\n                        'color' => '',\n                    ],\n                    [\n                        'text'  => '无限',\n                        'color' => '',\n                    ],\n                    [\n                        'text'  => '无限',\n                        'color' => 'red',\n                    ],\n                ],\n                [\n                    [\n                        'text'  => '总价值',\n                        'color' => '',\n                    ],\n                    [\n                        'text'  => '1000+',\n                        'color' => '',\n                    ],\n                    [\n                        'text'  => '3000+',\n                        'color' => '',\n                    ],\n                    [\n                        'text'  => '5000+',\n                        'color' => 'red',\n                    ],\n                ],\n            ],\n            'config' => $config,\n            'user'   => $user->toArray(),\n        ];\n\n        return $this->success($data);\n    }\n}"
  },
  {
    "path": "MineAdmin/php/app/Ai/Api/Login.php",
    "content": "<?php\ndeclare(strict_types=1);\n\nnamespace App\\Ai\\Api;\n\nuse App\\Ai\\Model\\AiUser;\nuse App\\Ai\\Model\\AiUserRelation;\nuse App\\Ai\\Model\\AiUserWallet;\nuse App\\Ai\\Service\\AiLoginService;\nuse Hyperf\\DbConnection\\Db;\nuse Hyperf\\Di\\Annotation\\Inject;\nuse Hyperf\\HttpServer\\Annotation\\Controller;\nuse Hyperf\\HttpServer\\Annotation\\GetMapping;\n\n#[Controller(prefix: \"ai/api/login\")]\nclass Login extends BaseApi\n{\n    public const VERSIONS = '0.0.1';\n\n    #[Inject]\n    protected AiLoginService $loginService;\n\n    #[GetMapping(\"index\")]\n    public function index()\n    {\n        try {\n            $user_name = $this->request->query('user_name');\n            $vid       = (int)$this->request->query('vid');\n            $password  = $this->request->query('password');\n            if (!$user_name || !$password){\n                return $this->error('参数错误！');\n            }\n            /**\n             * @var AiUser $user\n             */\n            $user     = AiUser::where(['mobile'=>$user_name, 'is_lock'=>1])->first();\n            $password = md5($password);\n            if (!empty($user) && $password !== $user->password){\n                return $this->error('密码错误！');\n            }\n\n            if (empty($user)){\n                if (empty($vid)) {\n                    return $this->error('账号不存在！');\n                }else{\n                    /**\n                     * @var AiUser $pUser\n                     */\n                    $pUser = AiUser::where(['id'=>$vid, 'is_lock'=>1])->select(['id'])->first();\n                    if (empty($pUser)) {\n                        return $this->error('邀请用户有误！');\n                    }\n                    // 注册\n                    Db::beginTransaction();\n                    try {\n                        $user            = new AiUser();\n                        $user->nick_name = '';\n                        $user->head_img  = '';\n                        $user->mobile    = $user_name;\n                        $user->password  = $password;\n                        $user->save();\n                        AiUserRelation::insert([\n                            'uid'       => $user->id,\n                            'from_uid' => $pUser->id,\n                        ]);\n                        AiUserWallet::insert([\n                            'uid' => $user->id,\n                        ]);\n                        Db::commit();\n                    }catch (\\Throwable $exception){\n                        Db::rollBack();\n                        return $this->error('注册失败！');\n                    }\n                }\n            }\n\n            $data            = [];\n            $data['version'] = self::VERSIONS;\n            $data['id']      = $user->id;\n            $data['vip']     = $user->vip;\n            $token           = $this->loginService->getToken($data);\n            return $this->success([\n                'token'        => $token,\n                'version'      => self::VERSIONS,\n                'check_status' => false,\n                'login_time'   => time(),\n            ]);\n        }catch (\\Throwable $exception){\n            return $this->error($exception->getMessage());\n        }\n    }\n}"
  },
  {
    "path": "MineAdmin/php/app/Ai/Api/Order.php",
    "content": "<?php\ndeclare(strict_types=1);\n\nnamespace App\\Ai\\Api;\n\nuse App\\Ai\\Constants\\OrderConst;\nuse App\\Ai\\Middleware\\AuthMiddleware;\nuse App\\Ai\\Service\\AiOrderService;\nuse Hyperf\\Di\\Annotation\\Inject;\nuse Hyperf\\HttpServer\\Annotation\\Controller;\nuse Hyperf\\HttpServer\\Annotation\\GetMapping;\nuse Hyperf\\HttpServer\\Annotation\\Middleware;\nuse Hyperf\\HttpServer\\Annotation\\PostMapping;\nuse Psr\\Http\\Message\\ResponseInterface;\n\n#[Controller(prefix: \"ai/api/order\")]\n#[Middleware(AuthMiddleware::class)]\nclass Order extends BaseApi\n{\n    #[Inject]\n    protected AiOrderService $orderService;\n\n    #[GetMapping(\"list\")]\n    public function index(): ResponseInterface\n    {\n        $param = $this->request->all();\n        $param['ord_type'] = [OrderConst::OPEN_VIP, OrderConst::MARKET];\n        return $this->success([\n            'list' => $this->orderService->orderList($param)\n        ]);\n    }\n\n    #[PostMapping(\"kami-open-vip\")]\n    public function kamiOpenVip(): ResponseInterface\n    {\n        $this->orderService->kamiOpenVip($this->request->all());\n        return $this->success();\n    }\n}"
  },
  {
    "path": "MineAdmin/php/app/Ai/Api/User.php",
    "content": "<?php\ndeclare(strict_types=1);\n\nnamespace App\\Ai\\Api;\n\nuse App\\Ai\\Constants\\ResponseCodeConst;\nuse App\\Ai\\Constants\\VipConst;\nuse App\\Ai\\Model\\AiUser;\nuse App\\Ai\\Service\\AiImageMaterialService;\nuse App\\Ai\\Service\\AiLoginService;\nuse App\\Ai\\Middleware\\AuthMiddleware;\nuse App\\Ai\\Service\\AiMineMenuGroupService;\nuse App\\Ai\\Service\\AiUserService;\nuse App\\Ai\\Service\\HelperService;\nuse Hyperf\\DbConnection\\Db;\nuse Hyperf\\Di\\Annotation\\Inject;\nuse Hyperf\\HttpServer\\Annotation\\Controller;\nuse Hyperf\\HttpServer\\Annotation\\GetMapping;\nuse Hyperf\\HttpServer\\Annotation\\Middleware;\nuse Hyperf\\HttpServer\\Annotation\\PostMapping;\nuse Mine\\Exception\\NormalStatusException;\nuse Mine\\Helper\\MineCode;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Http\\Message\\ResponseInterface;\n\n#[Controller(prefix: \"ai/api/user\")]\n#[Middleware(AuthMiddleware::class)]\nclass User extends BaseApi\n{\n    #[Inject]\n    protected AiLoginService $loginService;\n\n    #[Inject]\n    protected AiUserService $userService;\n\n    #[Inject]\n    protected AiMineMenuGroupService $mineMenuGroupService;\n\n    #[Inject]\n    protected AiImageMaterialService $imageMaterialService;\n\n    #[GetMapping(\"mine\")]\n    public function mine()\n    {\n        /**\n         * @var AiUser $user\n         */\n        $user  = $this->userService->read($this->loginService->getId());\n        return $this->success([\n            'head_img'   => $user->head_img,\n            'nick_name'  => $user->nick_name,\n            'uid'        => $user->id,\n            'mobile'     => $user->mobile,\n            'vip'        => $user->vip,\n            'vip_name'   => VipConst::getDesc($user->vip),\n            'vip_ent_at' => $user->vip_ent_at ? strtotime($user->vip_ent_at) : 0,\n            'amount'     => $user->wallet->balance ? HelperService::decode100($user->wallet->balance) : 0,\n            'friend_num' => $this->userService->friendNum($user->id),\n            'menus'      => $this->mineMenuGroupService->mineMenus(),\n            'banner'     => $this->imageMaterialService->mine(1),\n        ]);\n    }\n\n    /**\n     * @throws ContainerExceptionInterface\n     * @throws NotFoundExceptionInterface\n     * @throws \\Throwable\n     */\n    #[PostMapping(\"edit\")]\n    public function edit(): ResponseInterface\n    {\n        $post = $this->request->post();\n        if (!$this->checkParameter($post, ['mobile', 'head_img', 'nick_name'])) {\n            throw new NormalStatusException('参数错误', ResponseCodeConst::PARAM_FAILED);\n        }\n        return $this->funCallbackRes(function () use ($post) {\n            $this->userService->update($this->loginService->getId(), [\n                'nick_name' => $post['nick_name'],\n                'head_img' => HelperService::buildSavePath($post['head_img']),\n                'mobile' => $post['mobile'],\n            ]);\n            return '';\n        });\n    }\n\n    #[GetMapping(\"friends\")]\n    public function friends(): ResponseInterface\n    {\n        return $this->success([\n            'list'=>$this->userService->friendList($this->request->all())\n        ]);\n    }\n}"
  },
  {
    "path": "MineAdmin/php/app/Ai/Api/Wallet.php",
    "content": "<?php\ndeclare(strict_types=1);\n\nnamespace App\\Ai\\Api;\n\nuse App\\Ai\\Constants\\OrderConst;\nuse App\\Ai\\Middleware\\AuthMiddleware;\nuse App\\Ai\\Service\\AiOrderService;\nuse App\\Ai\\Service\\AiWalletService;\nuse Hyperf\\Di\\Annotation\\Inject;\nuse Hyperf\\HttpServer\\Annotation\\Controller;\nuse Hyperf\\HttpServer\\Annotation\\GetMapping;\nuse Hyperf\\HttpServer\\Annotation\\Middleware;\nuse Hyperf\\HttpServer\\Annotation\\PostMapping;\nuse Psr\\Http\\Message\\ResponseInterface;\n\n#[Controller(prefix: \"ai/api/wallet\")]\n#[Middleware(AuthMiddleware::class)]\nclass Wallet extends BaseApi\n{\n    #[Inject]\n    protected AiWalletService $walletService;\n\n    #[Inject]\n    protected AiOrderService $orderService;\n\n    #[GetMapping(\"index\")]\n    public function index(){\n        return $this->success($this->walletService->info());\n    }\n\n    #[GetMapping(\"change-log-list\")]\n    public function changeLogList(): ResponseInterface\n    {\n        return $this->success([\n            'list'=>$this->walletService->changeLogList($this->request->all())\n        ]);\n    }\n\n    #[GetMapping(\"withdrawal-list\")]\n    public function withdrawalList(): ResponseInterface\n    {\n        $param = $this->request->all();\n        $param['ord_type'] = [OrderConst::WITHDRAWAL];\n        return $this->success([\n            'list'=>$this->orderService->orderList($param)\n        ]);\n    }\n\n    #[PostMapping(\"withdrawal\")]\n    public function withdrawal(): ResponseInterface\n    {\n        $this->walletService->withdrawal($this->request->all());\n        return $this->success();\n    }\n}"
  },
  {
    "path": "MineAdmin/php/app/Ai/Api/Websocket.php",
    "content": "<?php\ndeclare(strict_types=1);\n\nnamespace App\\Ai\\Api;\n\nuse App\\Ai\\Constants\\RedisConst;\nuse App\\Ai\\Constants\\VipConst;\nuse App\\Ai\\Factory\\AiRedisFactory;\nuse App\\Ai\\Service\\AiChatgptPromptsService;\nuse App\\Ai\\Service\\AiChatMessageService;\nuse App\\Ai\\Service\\AiLoginService;\nuse App\\Ai\\Service\\AiOpenaiKeyService;\nuse App\\Ai\\Service\\AiSettingService;\nuse Hyperf\\Contract\\OnCloseInterface;\nuse Hyperf\\Contract\\OnMessageInterface;\nuse Hyperf\\Contract\\OnOpenInterface;\nuse Hyperf\\Di\\Annotation\\Inject;\nuse Hyperf\\Engine\\WebSocket\\Opcode;\nuse Hyperf\\Server\\ServerFactory;\nuse Orhanerday\\OpenAi\\OpenAi;\nuse Swoole\\Http\\Request;\nuse Swoole\\Http\\Response;\nuse Swoole\\WebSocket\\Server;\nuse Swoole\\WebSocket\\Server as WebSocketServer;\nuse Psr\\Container\\ContainerInterface;\n\nclass Websocket implements OnMessageInterface, OnOpenInterface, OnCloseInterface\n{\n    #[Inject]\n    protected AiLoginService $loginService;\n\n    #[Inject]\n    protected ContainerInterface $container;\n\n    #[Inject]\n    protected AiRedisFactory $redis;\n\n    #[Inject]\n    protected AiOpenaiKeyService $openAiService;\n\n    #[Inject]\n    protected AiSettingService $settingService;\n\n    #[Inject]\n    protected AiChatgptPromptsService $chatgptPromptsService;\n\n    #[Inject]\n    protected AiChatMessageService $chatMessageService;\n\n    public function onMessage($server, $frame): void\n    {\n        if($frame->opcode == Opcode::PING) {\n            // 如果使用协程 Server，在判断是 PING 帧后，需要手动处理，返回 PONG 帧。\n            // 异步风格 Server，可以直接通过 Swoole 配置处理，详情请见 https://wiki.swoole.com/#/websocket_server?id=open_websocket_ping_frame\n            $server->push('', Opcode::PONG);\n            return;\n        }\n        $data = \\json_decode($frame->data);\n        // todo {model: \"1:gpt3.5问答模式 2文心一言问答模式 3通义千问问答模式\", role_id: 1, message:\"\",}\n        switch ($data->type){\n            case 'ping':\n                $server->push($frame->fd, '{\"type\":\"ping\",\"content\":\"ok\"}');\n                break;\n            case 'message':\n                $this->messageHandle($frame->fd, $data);\n                break;\n        }\n    }\n\n    public function messageHandle($fd, $data)\n    {\n        switch ($data->model->index){\n            case 0:\n            case 1:\n                // chatgpt 3.5\n                // todo 存入消息，请求openapi 返回结果 结果入表\n                \\Hyperf\\Coroutine\\go(function () use($fd, $data){\n                    $this->chatgpt($fd, $data);\n                });\n                break;\n        }\n\n    }\n\n    protected function chatgpt($fd, $data){\n        try {\n            /**\n             * @var WebSocketServer $server\n             */\n            $server  = $this->container->get(ServerFactory::class)->getServer()->getServer();\n            // todo 验证会话id是否属于当前用户uid\n            if (mb_strlen($data->content) > 2048) {\n                $server->push($fd, '{\"type\":\"error\",\"content\":\"您输入的内容过长\",\"status\":true}');\n                return;\n            }\n            $user = $this->redis->hGetAll(RedisConst::FD_TO_USER . $fd);\n            // 免费会员限制会话\n            $time = time();\n            $todayKey = RedisConst::USER_SPOKE_TODAY . date('Ymd');\n            $incr = $this->redis->zIncrBy($todayKey, 1, $user['uid']);\n            $this->redis->expire($todayKey, 86400);\n            if ($user['vip'] === VipConst::FREE && $incr > 10) {\n                $server->push($fd, '{\"type\":\"error\",\"content\":\"您今日已经超过免费会员次数了哦！\",\"status\":true}');\n                return;\n            }\n\n            $prompt = $this->chatgptPromptsService->mapper->first(['id'=>$data->prompt_id], ['id', 'act', 'prompt']);\n\n            $opts = [\n                'model'             => AiChatgptPromptsService::MODEL_LIST[$data->model->index]['text'],\n                'messages'          => [],\n                'temperature'       => $data->model->temperature,\n                'frequency_penalty' => $data->model->frequency_penalty,\n                'presence_penalty'  => $data->model->presence_penalty,\n                'stream'            => true,\n            ];\n\n            if (AiChatgptPromptsService::MODEL_LIST[$data->model->index]['is_vip'] && !$user['vip']) {\n                $opts['model'] = AiChatgptPromptsService::MODEL_LIST[0]['text'];\n                $opts['max_tokens'] = 2048;\n            } else if (!$user['vip']) {\n                $opts['max_tokens'] = 2048;\n            }\n\n            $opts['messages'][] = ['role' => 'system', 'content' => $prompt->prompt];\n\n            if (!empty($data->context) && $user['vip'] > VipConst::FREE){\n                $i = 0;\n                foreach ($data->context as $msg) {\n                    $opts['messages'][] = [\n                        'role'    => 'user',\n                        'content' => $msg->content,\n                    ];\n                    $opts['messages'][] = [\n                        'role'    => 'assistant',\n                        'content' => $msg->reply_content,\n                    ];\n                    if (++$i > 6) {\n                        break;\n                    }\n                }\n            }\n\n            $opts['messages'][] = [\n                'role'    => 'user',\n                'content' => $data->content,\n            ];\n            $key     = $this->openAiService->openAiKey();\n            $openAI  = new OpenAi($key);\n            $baseUrl = $this->settingService->openaiProxy();\n            $baseUrl && $openAI->setBaseURL($baseUrl);\n            $openAI->setHeader([\"Content-Type\"=>\"text/event-stream\"]);\n            // 本次所有结果\n            $replyContent = '';\n            $openAI->chat($opts, function ($curl_info, $data) use ($fd, $server, &$replyContent) {\n                $datas = explode('data: ', $data);\n                foreach ($datas as $dataStr) {\n                    $arrayData = json_decode($dataStr, true);\n                    if ($arrayData) {\n                        if (isset($arrayData['choices'][0]['delta']['content'])) {\n                            $replyContent .= ($content = $arrayData['choices'][0]['delta']['content'] ?? '');\n                            $server->push($fd, json_encode([\n                                'type'    => 'message',\n                                'content' => $content,\n                                'status'  => false,\n                            ], JSON_UNESCAPED_UNICODE));\n                        } elseif (!empty($arrayData['choices'][0]['finish_reason'])) {\n                            $server->push($fd, '{\"type\":\"message\",\"content\":\"\",\"status\":true}');\n                        } elseif (isset($arrayData['error'])) {\n                            $server->push($fd, '{\"type\":\"error\",\"content\":\"' . ($arrayData['error']['message'] ?? '') . '\",\"status\":true}');\n                        }\n                    }\n                }\n                return \\strlen($data);\n            });\n\n            $date = date('Y-m-d H:i:s', $time);\n            $this->chatMessageService->save([\n                'role'             => 2,\n                'sid'              => $data->sid ?: 1,\n                'content'          => htmlspecialchars($data->content),\n                'reply_content'    => $replyContent  ? htmlspecialchars($replyContent) : '',\n                'reply_at'         => $date,\n                'created_at'       => $date,\n            ]);\n\n            $server->push($fd, '{\"type\":\"message\",\"content\":\"\",\"status\":true}');\n        }catch (\\Throwable $exception){\n            $server->push($fd, '{\"type\":\"error\",\"content\":\"'.$exception->getMessage().'\",\"status\":true}');\n        }\n    }\n\n    public function onClose($server, int $fd, int $reactorId): void\n    {\n        try {\n            // 解除绑定fd\n            $user = $this->redis->hGetAll(RedisConst::FD_TO_USER . $fd);\n            if (!empty($user['uid'])) {\n                $this->redis->zRem(RedisConst::USER_TO_FD, $user['uid']);\n            }\n            $this->redis->del(RedisConst::FD_TO_USER . $fd);\n        }catch (\\Throwable $exception){\n\n        }\n    }\n\n    /**\n     * @param Response|Server $server\n     * @param Request $request\n     * @throws \\RedisException\n     */\n    public function onOpen($server, $request): void\n    {\n        try {\n\n            $token = $request->get['token'] ?? '';\n            if (empty($token)){\n                $server->push($request->fd, '{\"type\":\"login\",\"content\":\"token无效1\"}');\n                $server->close();\n                return;\n            }\n\n            if (false === $this->loginService->check($token, 'ai')) {\n                $server->push($request->fd, '{\"type\":\"login\",\"content\":\"token无效2\"}');\n                $server->close();\n                return;\n            }\n\n            $userInfo = $this->loginService->getInfo($token);\n            if (empty($userInfo['id'])){\n                $server->push($request->fd, '{\"type\":\"login\",\"content\":\"token无效3\"}');\n                $server->close();\n                return;\n            }\n\n            $this->redis->zAdd(RedisConst::USER_TO_FD, $request->fd, $userInfo['id']);\n            $this->redis->hMSet(RedisConst::FD_TO_USER . $request->fd, [\n                'uid' => $userInfo['id'],\n                'vip' => $userInfo['vip'] ?? 0,\n            ]);\n            $this->redis->expire(RedisConst::USER_TO_FD, 86400);\n            $this->redis->expire(RedisConst::FD_TO_USER . $request->fd, 86400);\n            $server->push($request->fd, '{\"type\":\"opened\",\"content\":\"ok\"}');\n        } catch (\\Throwable $exception) {\n            $server->push($request->fd, '{\"type\":\"error\",\"content\":\"' . $exception->getMessage() . '\"}');\n        }\n    }\n}"
  },
  {
    "path": "MineAdmin/php/app/Ai/Command/InitMenuCommand.php",
    "content": "<?php\ndeclare(strict_types=1);\n\nnamespace App\\Ai\\Command;\n\nuse App\\System\\Model\\SystemMenu;\nuse Hyperf\\Command\\Command as HyperfCommand;\nuse Hyperf\\Command\\Annotation\\Command;\nuse Hyperf\\DbConnection\\Db;\n\n#[Command]\nclass InitMenuCommand extends HyperfCommand\n{\n\n    /**\n     * 执行的命令行\n     */\n    protected ?string $name = 'ai:init-menu';\n\n    public function handle()\n    {\n        // TODO: Implement handle() method.\n        $isInit = SystemMenu::where(['parent_id' => 0, 'level' => 0, 'code' => 'aiapp',])->first();\n        if (!empty($isInit)) {\n            $this->line('已经执行过了', 'info');\n            return;\n        }\n        $tableName = env('DB_PREFIX') . SystemMenu::getModel()->getTable();\n\n        $sql = \"INSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (0, '0', 'AI系统', 'aiapp', 'IconFire', 'ai', NULL, NULL, 2, 'M', 1, 96, 1, 1, now(), now(), NULL, NULL);\nSET @topid := LAST_INSERT_ID();\nSET @toplevel := CONCAT('0', ',', @topid);\n\n-- chatgptPrompts --\nINSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@topid, @toplevel, 'chatgpt角色', 'ai:chatgptPrompts', 'IconUser', 'ai/chatgptPrompts', 'ai/chatgptPrompts/index', NULL, 2, 'M', 1, 0, 1, 1, now(), now(), NULL, NULL);\nSET @id := LAST_INSERT_ID();\nSET @level := CONCAT('0',',', @topid, ',', @id);\nINSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, 'chatgpt角色列表', 'ai:chatgptPrompts:index', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL);\nINSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, 'chatgpt角色更新', 'ai:chatgptPrompts:update', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL);\nINSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, 'chatgpt角色保存', 'ai:chatgptPrompts:save', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL);\nINSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, 'chatgpt角色读取', 'ai:chatgptPrompts:read', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL);\nINSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, 'chatgpt角色删除', 'ai:chatgptPrompts:delete', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL);\n\n-- chatMessage --\nINSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@topid, @toplevel, '聊天数据', 'ai:chatMessage', 'IconNotification', 'ai/chatMessage', 'ai/chatMessage/index', NULL, 2, 'M', 1, 0, 1, 1, now(), now(), NULL, NULL);\nSET @id := LAST_INSERT_ID();\nSET @level := CONCAT('0',',', @topid, ',', @id);\nINSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, '聊天数据列表', 'ai:chatMessage:index', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL);\nINSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, '聊天数据读取', 'ai:chatMessage:read', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL);\nINSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, '聊天数据删除', 'ai:chatMessage:delete', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL);\n\n-- chatSession -- \nINSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@topid, @toplevel, '问答会话', 'ai:chatSession', 'IconSend', 'ai/chatSession', 'ai/chatSession/index', NULL, 2, 'M', 1, 0, 1, 1, now(), now(), NULL, NULL);\nSET @id := LAST_INSERT_ID();\nSET @level := CONCAT('0',',', @topid, ',', @id);\nINSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, '问答会话列表', 'ai:chatSession:index', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL);\nINSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, '问答会话读取', 'ai:chatSession:read', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL);\nINSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, '问答会话删除', 'ai:chatSession:delete', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL);\n\n-- mineMenuGroup --\nINSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@topid, @toplevel, 'Mine菜单分组', 'ai:mineMenuGroup', 'IconMenuUnfold', 'ai/mineMenuGroup', 'ai/mineMenuGroup/index', NULL, 2, 'M', 1, 0, 1, 1, now(), now(), NULL, NULL);\nSET @id := LAST_INSERT_ID();\nSET @level := CONCAT('0',',', @topid, ',', @id);\nINSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, '个人中心菜单分组列表', 'ai:mineMenuGroup:index', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL);\nINSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, '个人中心菜单分组保存', 'ai:mineMenuGroup:save', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL);\nINSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, '个人中心菜单分组更新', 'ai:mineMenuGroup:update', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL);\nINSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, '个人中心菜单分组读取', 'ai:mineMenuGroup:read', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL);\nINSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, '个人中心菜单分组删除', 'ai:mineMenuGroup:delete', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL);\n\n-- mineMenu --\nINSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@topid, @toplevel, 'Mine菜单', 'ai:mineMenu', 'IconSelectAll', 'ai/mineMenu', 'ai/mineMenu/index', NULL, 2, 'M', 1, 0, 1, 1, now(), now(), NULL, NULL);\nSET @id := LAST_INSERT_ID();\nSET @level := CONCAT('0',',', @topid, ',', @id);\nINSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, '个人中心菜单列表', 'ai:mineMenu:index', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL);\nINSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, '个人中心菜单保存', 'ai:mineMenu:save', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL);\nINSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, '个人中心菜单更新', 'ai:mineMenu:update', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL);\nINSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, '个人中心菜单读取', 'ai:mineMenu:read', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL);\nINSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, '个人中心菜单删除', 'ai:mineMenu:delete', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL);\n\n-- order --\nINSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@topid, @toplevel, '订单列表', 'ai:order', 'icon-home', 'ai/order', 'ai/order/index', NULL, 2, 'M', 1, 0, 1, 1, now(), now(), NULL, NULL);\nSET @id := LAST_INSERT_ID();\nSET @level := CONCAT('0',',', @topid, ',', @id);\nINSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, '订单表列表', 'ai:order:index', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL);\nINSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, '订单表读取', 'ai:order:read', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL);\nINSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, '订单表删除', 'ai:order:delete', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL);\n\n-- quickIssue --\nINSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@topid, @toplevel, '快捷问题', 'ai:quickIssue', 'IconExclamationCircle', 'ai/quickIssue', 'ai/quickIssue/index', NULL, 2, 'M', 1, 0, 1, 1, now(), now(), NULL, NULL);\nSET @id := LAST_INSERT_ID();\nSET @level := CONCAT('0',',', @topid, ',', @id);\nINSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, '快捷问题列表', 'ai:quickIssue:index', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL);\nINSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, '快捷问题更新', 'ai:quickIssue:update', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL);\nINSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, '快捷问题保存', 'ai:quickIssue:save', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL);\nINSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, '快捷问题读取', 'ai:quickIssue:read', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL);\nINSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, '快捷问题删除', 'ai:quickIssue:delete', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL);\n\n-- user --\nINSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@topid, @toplevel, '用户列表', 'ai:user', 'IconUserGroup', 'ai/user', 'ai/user/index', NULL, 2, 'M', 1, 0, 1, 1, now(), now(), NULL, NULL);\nSET @id := LAST_INSERT_ID();\nSET @level := CONCAT('0',',', @topid, ',', @id);\nINSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, '用户主表列表', 'ai:user:index', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL);\nINSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, '用户主表更新', 'ai:user:update', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL);\nINSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, '用户主表读取', 'ai:user:read', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL);\nINSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, '用户主表删除', 'ai:user:delete', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL);\nINSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, '用户开通VIP', 'ai:user:open-vip', NULL, NULL, NULL, NULL, 2, 'B', 1, 1, 1, 1, now(), now(), NULL, NULL);\nINSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, '用户锁定', 'ai:user:lock', NULL, NULL, NULL, NULL, 2, 'B', 1, 1, 1, 1, now(), now(), NULL, NULL);\n\n-- payKami --\nINSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@topid, @toplevel, '卡密', 'ai:payKami', 'icon-home', 'ai/payKami', 'ai/payKami/index', NULL, 2, 'M', 1, 0, 1, 1, now(), now(), NULL, NULL);\nSET @id := LAST_INSERT_ID();\nSET @level := CONCAT('0',',', @topid, ',', @id);\nINSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, '卡密列表', 'ai:payKami:index', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL);\nINSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, '卡密读取', 'ai:payKami:read', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL);\nINSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, '卡密更新', 'ai:payKami:update', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL);\nINSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, '卡密删除', 'ai:payKami:delete', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL);\nINSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, '创建卡密', 'ai:payKami:add', NULL, NULL, NULL, NULL, 2, 'B', 1, 1, 1, 1, now(), now(), NULL, NULL);\n\n-- setting --\nINSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@topid, @toplevel, '设置', 'ai:setting', 'IconSettings', 'ai/setting', 'ai/setting/index', NULL, 2, 'M', 1, 0, 1, 1, now(), now(), NULL, NULL);\n\n-- openKey --\nINSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@topid, @toplevel, 'openai_key', 'ai:openKey', 'icon-home', 'ai/openKey', 'ai/openKey/index', NULL, 2, 'M', 1, 0, 1, NULL, now(), now(), NULL, NULL);\nSET @id := LAST_INSERT_ID();\nSET @level := CONCAT('0',',', @topid, ',', @id);\nINSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, 'openai_key列表', 'ai:openKey:index', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL);\nINSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, 'openai_key保存', 'ai:openKey:save', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL);\nINSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, 'openai_key更新', 'ai:openKey:update', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL);\nINSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, 'openai_key读取', 'ai:openKey:read', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL);\nINSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, 'openai_key删除', 'ai:openKey:delete', NULL, NULL, NULL, NULL, 2, 'B', 1, 0, 1, NULL, now(), now(), NULL, NULL);\nINSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, 'openai_key刷新缓存', 'ai:openKey:refresh-cache-list', NULL, NULL, NULL, NULL, 2, 'B', 1, 1, 1, 1, now(), now(), NULL, NULL);\nINSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, 'openai_key批量添加', 'ai:openKey:batch-add', NULL, NULL, NULL, NULL, 2, 'B', 1, 1, 1, 1, now(), now(), NULL, NULL);\n\n-- imageMaterial --\nINSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@topid, @toplevel, '图片素材', 'ai:imageMaterial', 'icon-home', 'ai/imageMaterial', 'ai/imageMaterial/index', NULL, '2', 'M', '1', 0, 1, NULL, now(), now(), NULL, NULL);\nSET @id := LAST_INSERT_ID();\nSET @level := CONCAT('0',',', @topid, ',', @id);\nINSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, CONCAT('图片素材', '列表'), CONCAT('ai:imageMaterial',':index'), NULL, NULL, NULL, NULL, '2', 'B', '1', 0, 1, NULL, now(), now(), NULL, NULL);\nINSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, CONCAT('图片素材', '保存'), CONCAT('ai:imageMaterial',':save'), NULL, NULL, NULL, NULL, '2', 'B', '1', 0, 1, NULL, now(), now(), NULL, NULL);\nINSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, CONCAT('图片素材', '更新'), CONCAT('ai:imageMaterial',':update'), NULL, NULL, NULL, NULL, '2', 'B', '1', 0, 1, NULL, now(), now(), NULL, NULL);\nINSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, CONCAT('图片素材', '读取'), CONCAT('ai:imageMaterial',':read'), NULL, NULL, NULL, NULL, '2', 'B', '1', 0, 1, NULL, now(), now(), NULL, NULL);\nINSERT INTO `{$tableName}`(`parent_id`, `level`, `name`, `code`, `icon`, `route`, `component`, `redirect`, `is_hidden`, `type`, `status`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (@id, @level, CONCAT('图片素材', '删除'), CONCAT('ai:imageMaterial',':delete'), NULL, NULL, NULL, NULL, '2', 'B', '1', 0, 1, NULL, now(), now(), NULL, NULL);\n\";\n        Db::unprepared($sql);\n        $this->line('大功告成', 'info');\n    }\n}"
  },
  {
    "path": "MineAdmin/php/app/Ai/Constants/OrderConst.php",
    "content": "<?php\n\nnamespace App\\Ai\\Constants;\n\nuse Pt\\Constants\\DescConst;\n\nclass OrderConst extends DescConst\n{\n    /**\n     * @Desc(\"购买VIP\")\n     * @Group(\"ord_type\")\n     */\n    const OPEN_VIP = 1;\n\n    /**\n     * @Desc(\"提现\")\n     * @Group(\"ord_type\")\n     */\n    const WITHDRAWAL = 2;\n\n    /**\n     * @Desc(\"成为营销部\")\n     * @Group(\"ord_type\")\n     */\n    const MARKET = 3;\n\n    /**\n     * @Desc(\"微信支付\")\n     * @Group(\"pay_type\")\n     */\n    const WX_PAY = 1;\n\n    /**\n     * @Desc(\"后台免费\")\n     * @Group(\"pay_type\")\n     */\n    const ADMIN_FREE = 2;\n\n    /**\n     * @Desc(\"后台付费\")\n     * @Group(\"pay_type\")\n     */\n    const ADMIN_PAY = 3;\n\n    /**\n     * @Desc(\"免费卡密\")\n     * @Group(\"pay_type\")\n     */\n    const KAMI_FREE = 4;\n\n    /**\n     * @Desc(\"付费卡密\")\n     * @Group(\"pay_type\")\n     */\n    const KAMI_PAY = 5;\n\n    /**\n     * @Desc(\"待支付-待处理\")\n     * @Group(\"status\")\n     */\n    const WAIT_PAY = 1;\n\n    /**\n     * @Desc(\"已支付-已完成\")\n     * @Group(\"status\")\n     */\n    const SUCCESS_PAY = 2;\n\n    /**\n     * @Desc(\"支付失败-失败\")\n     * @Group(\"status\")\n     */\n    const SUCCESS_ERR = 3;\n}"
  },
  {
    "path": "MineAdmin/php/app/Ai/Constants/RedisConst.php",
    "content": "<?php\ndeclare(strict_types=1);\n\nnamespace App\\Ai\\Constants;\n\nclass RedisConst\n{\n    /**\n     * 用户uid绑定fd\n     */\n    const USER_TO_FD = 'ai_user_fd';\n\n    /**\n     * fa 绑定用户信息\n     */\n    const FD_TO_USER = 'ai_fd_user:';\n\n    /**\n     * 用户今天发言了多少次\n     */\n    const USER_SPOKE_TODAY = 'ai_user_spoke_today:';\n}"
  },
  {
    "path": "MineAdmin/php/app/Ai/Constants/ResponseCodeConst.php",
    "content": "<?php\ndeclare(strict_types=1);\n\nnamespace App\\Ai\\Constants;\n\nclass ResponseCodeConst\n{\n    const INVALID_TOKEN   = 10001; // token 无效\n    const ACCOUNT_LOCK    = 10002; // 账号被锁定\n    const REDIRECT_LOGIN  = 10003; // 跳转到登录页\n    const APP_CLOSE       = 10004; // 关站维护\n    const CLEAR_ALL_CACHE = 10005; // 清空所有缓存数据\n    const PARAM_FAILED   = 10006; // 参数错误\n}"
  },
  {
    "path": "MineAdmin/php/app/Ai/Constants/UploadSceneConst.php",
    "content": "<?php\nnamespace App\\Ai\\Constants;\n\nclass UploadSceneConst\n{\n    // 1开头，例如 1001 1002\n    const ImageScene = [\n        'ai_head_img'          => '1010',\n        'ai_mine_menu_icon'    => '1011',\n        'ai_customer_wx_img'   => '1012',\n        'ai_customer_head_img' => '1013',\n        'ai_image_materialg'   => '1014',\n    ];\n\n    // 2开头，例如 2001 2002\n    const AudioScene = [\n    ];\n\n    // 3开头，例如 3001 3002\n    const VideoScene = [\n    ];\n\n    public static function hasScene(string $scene): bool\n    {\n        if (in_array($scene, self::ImageScene) || in_array($scene, self::AudioScene) || in_array($scene, self::VideoScene)) {\n            return true;\n        }\n        return false;\n    }\n\n    public static function isOnly(string $scene): bool\n    {\n        return in_array($scene, ['1010']);\n    }\n}"
  },
  {
    "path": "MineAdmin/php/app/Ai/Constants/VipConst.php",
    "content": "<?php\ndeclare(strict_types=1);\n\nnamespace App\\Ai\\Constants;\n\nuse Pt\\Constants\\DescConst;\n\nclass VipConst extends DescConst\n{\n    /**\n     * @Desc(\"免费会员\")\n     */\n    const FREE = 0;\n\n    /**\n     * @Desc(\"VIP\")\n     */\n    const VIP = 10;\n\n    /**\n     * @Desc(\"1星VIP\")\n     */\n    const VIP_ONE = 20;\n\n    /**\n     * @Desc(\"2星VIP\")\n     */\n    const VIP_TWO = 30;\n\n    public static function config(): array\n    {\n        return [\n            [\n                'name' => 'VIP', // VIP名称\n                'price' => '199', // 现价\n                'price_pay' => '199', // 支付金额\n                'price_old' => '198', // 原价\n                'level' => 10, // 等级\n                'length' => 12, // 时长(月)\n                'income' => 0, // 收益%\n                'wrap_vip' => 0, // VIP抵扣包\n                'is_default' => false,// 默认选中\n                'is_choose' => false,// 是否可选\n                'btn_text' => '立即开通',\n            ], [\n                'name' => '一星',\n                'price' => '299',\n                'price_pay' => '299',\n                'price_old' => '1198',\n                'level' => 20,\n                'length' => 36,\n                'income' => 35,\n                'wrap_vip' => 10,\n                'is_default' => false,\n                'is_choose' => false,\n                'btn_text' => '立即开通',\n            ], [\n                'name' => '二星',\n                'price' => '399',\n                'price_pay' => '399',\n                'price_old' => '2198',\n                'level' => 30,\n                'length' => 60,\n                'income' => 50,\n                'wrap_vip' => 20,\n                'is_default' => false,\n                'is_choose' => false,\n                'btn_text' => '立即开通',\n            ],\n        ];\n    }\n}"
  },
  {
    "path": "MineAdmin/php/app/Ai/Constants/WalletConst.php",
    "content": "<?php\ndeclare(strict_types=1);\n\nnamespace App\\Ai\\Constants;\n\nuse Pt\\Constants\\DescConst;\n\nclass WalletConst extends DescConst\n{\n    /**\n     * @Desc(\"收入\")\n     * @Group(\"direction\")\n     */\n    const IN = 1;\n\n    /**\n     * @Desc(\"支出\")\n     * @Group(\"direction\")\n     */\n    const OUT = 2;\n\n    /**\n     * @Desc(\"推广获益\")\n     * @Group(\"scene\")\n     */\n    const PROMOTION = 1;\n\n    /**\n     * @Desc(\"提现\")\n     * @Group(\"scene\")\n     */\n    const WITHDRAWAL = 2;\n}"
  },
  {
    "path": "MineAdmin/php/app/Ai/Controller/AiChatMessageController.php",
    "content": "<?php\ndeclare(strict_types=1);\n\n\nnamespace App\\Ai\\Controller;\n\nuse App\\Ai\\Service\\AiChatMessageService;\nuse App\\Ai\\Request\\AiChatMessageRequest;\nuse Hyperf\\Di\\Annotation\\Inject;\nuse Hyperf\\HttpServer\\Annotation\\Controller;\nuse Hyperf\\HttpServer\\Annotation\\DeleteMapping;\nuse Hyperf\\HttpServer\\Annotation\\GetMapping;\nuse Hyperf\\HttpServer\\Annotation\\PostMapping;\nuse Hyperf\\HttpServer\\Annotation\\PutMapping;\nuse Mine\\Annotation\\Auth;\nuse Mine\\Annotation\\OperationLog;\nuse Mine\\Annotation\\Permission;\nuse Mine\\MineController;\nuse Psr\\Http\\Message\\ResponseInterface;\n\n/**\n * 聊天数据控制器\n * Class AiChatMessageController\n */\n#[Controller(prefix: \"ai/chatMessage\"), Auth]\nclass AiChatMessageController extends MineController\n{\n    /**\n     * 业务处理服务\n     * AiChatMessageService\n     */\n    #[Inject]\n    protected AiChatMessageService $service;\n\n    \n    /**\n     * 列表\n     * @return ResponseInterface\n     * @throws \\Psr\\Container\\ContainerExceptionInterface\n     * @throws \\Psr\\Container\\NotFoundExceptionInterface\n     */\n    #[GetMapping(\"index\"), Permission(\"ai:chatMessage, ai:chatMessage:index\")]\n    public function index(): ResponseInterface\n    {\n        return $this->success($this->service->getPageList($this->request->all()));\n    }\n\n    /**\n     * 读取数据\n     * @param int $id\n     * @return ResponseInterface\n     * @throws \\Psr\\Container\\ContainerExceptionInterface\n     * @throws \\Psr\\Container\\NotFoundExceptionInterface\n     */\n    #[GetMapping(\"read/{id}\"), Permission(\"ai:chatMessage:read\")]\n    public function read(int $id): ResponseInterface\n    {\n        return $this->success($this->service->read($id));\n    }\n\n    /**\n     * 单个或批量删除数据到回收站\n     * @return ResponseInterface\n     * @throws \\Psr\\Container\\ContainerExceptionInterface\n     * @throws \\Psr\\Container\\NotFoundExceptionInterface\n     */\n    #[DeleteMapping(\"delete\"), Permission(\"ai:chatMessage:delete\"), OperationLog]\n    public function delete(): ResponseInterface\n    {\n        return $this->service->delete((array) $this->request->input('ids', [])) ? $this->success() : $this->error();\n    }\n\n}"
  },
  {
    "path": "MineAdmin/php/app/Ai/Controller/AiChatSessionController.php",
    "content": "<?php\ndeclare(strict_types=1);\n\n\nnamespace App\\Ai\\Controller;\n\nuse App\\Ai\\Service\\AiChatSessionService;\nuse App\\Ai\\Request\\AiChatSessionRequest;\nuse Hyperf\\Di\\Annotation\\Inject;\nuse Hyperf\\HttpServer\\Annotation\\Controller;\nuse Hyperf\\HttpServer\\Annotation\\DeleteMapping;\nuse Hyperf\\HttpServer\\Annotation\\GetMapping;\nuse Hyperf\\HttpServer\\Annotation\\PostMapping;\nuse Hyperf\\HttpServer\\Annotation\\PutMapping;\nuse Mine\\Annotation\\Auth;\nuse Mine\\Annotation\\OperationLog;\nuse Mine\\Annotation\\Permission;\nuse Mine\\MineController;\nuse Psr\\Http\\Message\\ResponseInterface;\n\n/**\n * 问答会话控制器\n * Class AiChatSessionController\n */\n#[Controller(prefix: \"ai/chatSession\"), Auth]\nclass AiChatSessionController extends MineController\n{\n    /**\n     * 业务处理服务\n     * AiChatSessionService\n     */\n    #[Inject]\n    protected AiChatSessionService $service;\n\n    \n    /**\n     * 列表\n     * @return ResponseInterface\n     * @throws \\Psr\\Container\\ContainerExceptionInterface\n     * @throws \\Psr\\Container\\NotFoundExceptionInterface\n     */\n    #[GetMapping(\"index\"), Permission(\"ai:chatSession, ai:chatSession:index\")]\n    public function index(): ResponseInterface\n    {\n        return $this->success($this->service->getPageList($this->request->all()));\n    }\n\n    /**\n     * 读取数据\n     * @param int $id\n     * @return ResponseInterface\n     * @throws \\Psr\\Container\\ContainerExceptionInterface\n     * @throws \\Psr\\Container\\NotFoundExceptionInterface\n     */\n    #[GetMapping(\"read/{id}\"), Permission(\"ai:chatSession:read\")]\n    public function read(int $id): ResponseInterface\n    {\n        return $this->success($this->service->read($id));\n    }\n\n    /**\n     * 单个或批量删除数据到回收站\n     * @return ResponseInterface\n     * @throws \\Psr\\Container\\ContainerExceptionInterface\n     * @throws \\Psr\\Container\\NotFoundExceptionInterface\n     */\n    #[DeleteMapping(\"delete\"), Permission(\"ai:chatSession:delete\"), OperationLog]\n    public function delete(): ResponseInterface\n    {\n        return $this->service->delete((array) $this->request->input('ids', [])) ? $this->success() : $this->error();\n    }\n\n}"
  },
  {
    "path": "MineAdmin/php/app/Ai/Controller/AiChatgptPromptsController.php",
    "content": "<?php\ndeclare(strict_types=1);\n\n\nnamespace App\\Ai\\Controller;\n\nuse App\\Ai\\Service\\AiChatgptPromptsService;\nuse App\\Ai\\Request\\AiChatgptPromptsRequest;\nuse Hyperf\\Di\\Annotation\\Inject;\nuse Hyperf\\HttpServer\\Annotation\\Controller;\nuse Hyperf\\HttpServer\\Annotation\\DeleteMapping;\nuse Hyperf\\HttpServer\\Annotation\\GetMapping;\nuse Hyperf\\HttpServer\\Annotation\\PostMapping;\nuse Hyperf\\HttpServer\\Annotation\\PutMapping;\nuse Mine\\Annotation\\Auth;\nuse Mine\\Annotation\\OperationLog;\nuse Mine\\Annotation\\Permission;\nuse Mine\\MineController;\nuse Psr\\Http\\Message\\ResponseInterface;\n\n/**\n * chatgpt角色控制器\n * Class AiChatgptPromptsController\n */\n#[Controller(prefix: \"ai/chatgptPrompts\"), Auth]\nclass AiChatgptPromptsController extends MineController\n{\n    /**\n     * 业务处理服务\n     * AiChatgptPromptsService\n     */\n    #[Inject]\n    protected AiChatgptPromptsService $service;\n\n    \n    /**\n     * 列表\n     * @return ResponseInterface\n     * @throws \\Psr\\Container\\ContainerExceptionInterface\n     * @throws \\Psr\\Container\\NotFoundExceptionInterface\n     */\n    #[GetMapping(\"index\"), Permission(\"ai:chatgptPrompts, ai:chatgptPrompts:index\")]\n    public function index(): ResponseInterface\n    {\n        return $this->success($this->service->getPageList($this->request->all()));\n    }\n\n    /**\n     * 更新\n     * @param int $id\n     * @param AiChatgptPromptsRequest $request\n     * @return ResponseInterface\n     * @throws \\Psr\\Container\\ContainerExceptionInterface\n     * @throws \\Psr\\Container\\NotFoundExceptionInterface\n     */\n    #[PutMapping(\"update/{id}\"), Permission(\"ai:chatgptPrompts:update\"), OperationLog]\n    public function update(int $id, AiChatgptPromptsRequest $request): ResponseInterface\n    {\n        return $this->service->update($id, $request->all()) ? $this->success() : $this->error();\n    }\n\n    /**\n     * 新增\n     * @param AiChatgptPromptsRequest $request\n     * @return ResponseInterface\n     * @throws \\Psr\\Container\\ContainerExceptionInterface\n     * @throws \\Psr\\Container\\NotFoundExceptionInterface\n     */\n    #[PostMapping(\"save\"), Permission(\"ai:chatgptPrompts:save\"), OperationLog]\n    public function save(AiChatgptPromptsRequest $request): ResponseInterface\n    {\n        return $this->success(['id' => $this->service->save($request->all())]);\n    }\n\n    /**\n     * 读取数据\n     * @param int $id\n     * @return ResponseInterface\n     * @throws \\Psr\\Container\\ContainerExceptionInterface\n     * @throws \\Psr\\Container\\NotFoundExceptionInterface\n     */\n    #[GetMapping(\"read/{id}\"), Permission(\"ai:chatgptPrompts:read\")]\n    public function read(int $id): ResponseInterface\n    {\n        return $this->success($this->service->read($id));\n    }\n\n    /**\n     * 单个或批量删除数据到回收站\n     * @return ResponseInterface\n     * @throws \\Psr\\Container\\ContainerExceptionInterface\n     * @throws \\Psr\\Container\\NotFoundExceptionInterface\n     */\n    #[DeleteMapping(\"delete\"), Permission(\"ai:chatgptPrompts:delete\"), OperationLog]\n    public function delete(): ResponseInterface\n    {\n        $ids = $this->request->input('ids', []);\n        if (in_array(1, $ids)) {\n            $this->error('id为1的角色不能删除！');\n        }\n        return $this->service->delete((array) $this->request->input('ids', [])) ? $this->success() : $this->error();\n    }\n\n}"
  },
  {
    "path": "MineAdmin/php/app/Ai/Controller/AiImageMaterialController.php",
    "content": "<?php\ndeclare(strict_types=1);\n\n\nnamespace App\\Ai\\Controller;\n\nuse App\\Ai\\Service\\AiImageMaterialService;\nuse App\\Ai\\Request\\AiImageMaterialRequest;\nuse Hyperf\\Di\\Annotation\\Inject;\nuse Hyperf\\HttpServer\\Annotation\\Controller;\nuse Hyperf\\HttpServer\\Annotation\\DeleteMapping;\nuse Hyperf\\HttpServer\\Annotation\\GetMapping;\nuse Hyperf\\HttpServer\\Annotation\\PostMapping;\nuse Hyperf\\HttpServer\\Annotation\\PutMapping;\nuse Mine\\Annotation\\Auth;\nuse Mine\\Annotation\\OperationLog;\nuse Mine\\Annotation\\Permission;\nuse Mine\\MineController;\nuse Psr\\Http\\Message\\ResponseInterface;\n\n/**\n * 图片素材控制器\n * Class AiImageMaterialController\n */\n#[Controller(prefix: \"ai/imageMaterial\"), Auth]\nclass AiImageMaterialController extends MineController\n{\n    /**\n     * 业务处理服务\n     * AiImageMaterialService\n     */\n    #[Inject]\n    protected AiImageMaterialService $service;\n\n    \n    /**\n     * 列表\n     * @return ResponseInterface\n     * @throws \\Psr\\Container\\ContainerExceptionInterface\n     * @throws \\Psr\\Container\\NotFoundExceptionInterface\n     */\n    #[GetMapping(\"index\"), Permission(\"ai:imageMaterial, ai:imageMaterial:index\")]\n    public function index(): ResponseInterface\n    {\n        return $this->success($this->service->getPageList($this->request->all()));\n    }\n\n    /**\n     * 新增\n     * @param AiImageMaterialRequest $request\n     * @return ResponseInterface\n     * @throws \\Psr\\Container\\ContainerExceptionInterface\n     * @throws \\Psr\\Container\\NotFoundExceptionInterface\n     */\n    #[PostMapping(\"save\"), Permission(\"ai:imageMaterial:save\"), OperationLog]\n    public function save(AiImageMaterialRequest $request): ResponseInterface\n    {\n        return $this->success(['id' => $this->service->save($request->all())]);\n    }\n\n    /**\n     * 更新\n     * @param int $id\n     * @param AiImageMaterialRequest $request\n     * @return ResponseInterface\n     * @throws \\Psr\\Container\\ContainerExceptionInterface\n     * @throws \\Psr\\Container\\NotFoundExceptionInterface\n     */\n    #[PutMapping(\"update/{id}\"), Permission(\"ai:imageMaterial:update\"), OperationLog]\n    public function update(int $id, AiImageMaterialRequest $request): ResponseInterface\n    {\n        return $this->service->update($id, $request->all()) ? $this->success() : $this->error();\n    }\n\n    /**\n     * 读取数据\n     * @param int $id\n     * @return ResponseInterface\n     * @throws \\Psr\\Container\\ContainerExceptionInterface\n     * @throws \\Psr\\Container\\NotFoundExceptionInterface\n     */\n    #[GetMapping(\"read/{id}\"), Permission(\"ai:imageMaterial:read\")]\n    public function read(int $id): ResponseInterface\n    {\n        return $this->success($this->service->read($id));\n    }\n\n    /**\n     * 单个或批量删除数据到回收站\n     * @return ResponseInterface\n     * @throws \\Psr\\Container\\ContainerExceptionInterface\n     * @throws \\Psr\\Container\\NotFoundExceptionInterface\n     */\n    #[DeleteMapping(\"delete\"), Permission(\"ai:imageMaterial:delete\"), OperationLog]\n    public function delete(): ResponseInterface\n    {\n        return $this->service->delete((array) $this->request->input('ids', [])) ? $this->success() : $this->error();\n    }\n\n}"
  },
  {
    "path": "MineAdmin/php/app/Ai/Controller/AiMineMenuController.php",
    "content": "<?php\ndeclare(strict_types=1);\n\n\nnamespace App\\Ai\\Controller;\n\nuse App\\Ai\\Service\\AiMineMenuService;\nuse App\\Ai\\Request\\AiMineMenuRequest;\nuse Hyperf\\Di\\Annotation\\Inject;\nuse Hyperf\\HttpServer\\Annotation\\Controller;\nuse Hyperf\\HttpServer\\Annotation\\DeleteMapping;\nuse Hyperf\\HttpServer\\Annotation\\GetMapping;\nuse Hyperf\\HttpServer\\Annotation\\PostMapping;\nuse Hyperf\\HttpServer\\Annotation\\PutMapping;\nuse Mine\\Annotation\\Auth;\nuse Mine\\Annotation\\OperationLog;\nuse Mine\\Annotation\\Permission;\nuse Mine\\MineController;\nuse Psr\\Http\\Message\\ResponseInterface;\n\n/**\n * 个人中心菜单控制器\n * Class AiMineMenuController\n */\n#[Controller(prefix: \"ai/mineMenu\"), Auth]\nclass AiMineMenuController extends MineController\n{\n    /**\n     * 业务处理服务\n     * AiMineMenuService\n     */\n    #[Inject]\n    protected AiMineMenuService $service;\n\n    \n    /**\n     * 列表\n     * @return ResponseInterface\n     * @throws \\Psr\\Container\\ContainerExceptionInterface\n     * @throws \\Psr\\Container\\NotFoundExceptionInterface\n     */\n    #[GetMapping(\"index\"), Permission(\"ai:mineMenu, ai:mineMenu:index\")]\n    public function index(): ResponseInterface\n    {\n        return $this->success($this->service->getPageList($this->request->all()));\n    }\n\n    /**\n     * 新增\n     * @param AiMineMenuRequest $request\n     * @return ResponseInterface\n     * @throws \\Psr\\Container\\ContainerExceptionInterface\n     * @throws \\Psr\\Container\\NotFoundExceptionInterface\n     */\n    #[PostMapping(\"save\"), Permission(\"ai:mineMenu:save\"), OperationLog]\n    public function save(AiMineMenuRequest $request): ResponseInterface\n    {\n        return $this->success(['id' => $this->service->save($request->all())]);\n    }\n\n    /**\n     * 更新\n     * @param int $id\n     * @param AiMineMenuRequest $request\n     * @return ResponseInterface\n     * @throws \\Psr\\Container\\ContainerExceptionInterface\n     * @throws \\Psr\\Container\\NotFoundExceptionInterface\n     */\n    #[PutMapping(\"update/{id}\"), Permission(\"ai:mineMenu:update\"), OperationLog]\n    public function update(int $id, AiMineMenuRequest $request): ResponseInterface\n    {\n        return $this->service->update($id, $request->all()) ? $this->success() : $this->error();\n    }\n\n    /**\n     * 读取数据\n     * @param int $id\n     * @return ResponseInterface\n     * @throws \\Psr\\Container\\ContainerExceptionInterface\n     * @throws \\Psr\\Container\\NotFoundExceptionInterface\n     */\n    #[GetMapping(\"read/{id}\"), Permission(\"ai:mineMenu:read\")]\n    public function read(int $id): ResponseInterface\n    {\n        return $this->success($this->service->read($id));\n    }\n\n    /**\n     * 单个或批量删除数据到回收站\n     * @return ResponseInterface\n     * @throws \\Psr\\Container\\ContainerExceptionInterface\n     * @throws \\Psr\\Container\\NotFoundExceptionInterface\n     */\n    #[DeleteMapping(\"delete\"), Permission(\"ai:mineMenu:delete\"), OperationLog]\n    public function delete(): ResponseInterface\n    {\n        return $this->service->delete((array) $this->request->input('ids', [])) ? $this->success() : $this->error();\n    }\n\n}"
  },
  {
    "path": "MineAdmin/php/app/Ai/Controller/AiMineMenuGroupController.php",
    "content": "<?php\ndeclare(strict_types=1);\n\n\nnamespace App\\Ai\\Controller;\n\nuse App\\Ai\\Service\\AiMineMenuGroupService;\nuse App\\Ai\\Request\\AiMineMenuGroupRequest;\nuse Hyperf\\Di\\Annotation\\Inject;\nuse Hyperf\\HttpServer\\Annotation\\Controller;\nuse Hyperf\\HttpServer\\Annotation\\DeleteMapping;\nuse Hyperf\\HttpServer\\Annotation\\GetMapping;\nuse Hyperf\\HttpServer\\Annotation\\PostMapping;\nuse Hyperf\\HttpServer\\Annotation\\PutMapping;\nuse Mine\\Annotation\\Auth;\nuse Mine\\Annotation\\OperationLog;\nuse Mine\\Annotation\\Permission;\nuse Mine\\MineController;\nuse Psr\\Http\\Message\\ResponseInterface;\n\n/**\n * 个人中心菜单分组控制器\n * Class AiMineMenuGroupController\n */\n#[Controller(prefix: \"ai/mineMenuGroup\"), Auth]\nclass AiMineMenuGroupController extends MineController\n{\n    /**\n     * 业务处理服务\n     * AiMineMenuGroupService\n     */\n    #[Inject]\n    protected AiMineMenuGroupService $service;\n\n    \n    /**\n     * 列表\n     * @return ResponseInterface\n     * @throws \\Psr\\Container\\ContainerExceptionInterface\n     * @throws \\Psr\\Container\\NotFoundExceptionInterface\n     */\n    #[GetMapping(\"index\"), Permission(\"ai:mineMenuGroup, ai:mineMenuGroup:index\")]\n    public function index(): ResponseInterface\n    {\n        return $this->success($this->service->getPageList($this->request->all()));\n    }\n\n    /**\n     * 新增\n     * @param AiMineMenuGroupRequest $request\n     * @return ResponseInterface\n     * @throws \\Psr\\Container\\ContainerExceptionInterface\n     * @throws \\Psr\\Container\\NotFoundExceptionInterface\n     */\n    #[PostMapping(\"save\"), Permission(\"ai:mineMenuGroup:save\"), OperationLog]\n    public function save(AiMineMenuGroupRequest $request): ResponseInterface\n    {\n        return $this->success(['id' => $this->service->save($request->all())]);\n    }\n\n    /**\n     * 更新\n     * @param int $id\n     * @param AiMineMenuGroupRequest $request\n     * @return ResponseInterface\n     * @throws \\Psr\\Container\\ContainerExceptionInterface\n     * @throws \\Psr\\Container\\NotFoundExceptionInterface\n     */\n    #[PutMapping(\"update/{id}\"), Permission(\"ai:mineMenuGroup:update\"), OperationLog]\n    public function update(int $id, AiMineMenuGroupRequest $request): ResponseInterface\n    {\n        return $this->service->update($id, $request->all()) ? $this->success() : $this->error();\n    }\n\n    /**\n     * 读取数据\n     * @param int $id\n     * @return ResponseInterface\n     * @throws \\Psr\\Container\\ContainerExceptionInterface\n     * @throws \\Psr\\Container\\NotFoundExceptionInterface\n     */\n    #[GetMapping(\"read/{id}\"), Permission(\"ai:mineMenuGroup:read\")]\n    public function read(int $id): ResponseInterface\n    {\n        return $this->success($this->service->read($id));\n    }\n\n    /**\n     * 单个或批量删除数据到回收站\n     * @return ResponseInterface\n     * @throws \\Psr\\Container\\ContainerExceptionInterface\n     * @throws \\Psr\\Container\\NotFoundExceptionInterface\n     */\n    #[DeleteMapping(\"delete\"), Permission(\"ai:mineMenuGroup:delete\"), OperationLog]\n    public function delete(): ResponseInterface\n    {\n        return $this->service->delete((array) $this->request->input('ids', [])) ? $this->success() : $this->error();\n    }\n\n}"
  },
  {
    "path": "MineAdmin/php/app/Ai/Controller/AiOpenaiKeyController.php",
    "content": "<?php\ndeclare(strict_types=1);\n\n\nnamespace App\\Ai\\Controller;\n\nuse App\\Ai\\Service\\AiOpenaiKeyService;\nuse App\\Ai\\Request\\AiOpenaiKeyRequest;\nuse Hyperf\\Di\\Annotation\\Inject;\nuse Hyperf\\HttpServer\\Annotation\\Controller;\nuse Hyperf\\HttpServer\\Annotation\\DeleteMapping;\nuse Hyperf\\HttpServer\\Annotation\\GetMapping;\nuse Hyperf\\HttpServer\\Annotation\\PostMapping;\nuse Hyperf\\HttpServer\\Annotation\\PutMapping;\nuse Mine\\Annotation\\Auth;\nuse Mine\\Annotation\\OperationLog;\nuse Mine\\Annotation\\Permission;\nuse Mine\\MineController;\nuse Psr\\Http\\Message\\ResponseInterface;\n\n/**\n * openai_key控制器\n * Class AiOpenaiKeyController\n */\n#[Controller(prefix: \"ai/openKey\"), Auth]\nclass AiOpenaiKeyController extends MineController\n{\n    /**\n     * 业务处理服务\n     * AiOpenaiKeyService\n     */\n    #[Inject]\n    protected AiOpenaiKeyService $service;\n\n    \n    /**\n     * 列表\n     * @return ResponseInterface\n     * @throws \\Psr\\Container\\ContainerExceptionInterface\n     * @throws \\Psr\\Container\\NotFoundExceptionInterface\n     */\n    #[GetMapping(\"index\"), Permission(\"ai:openKey, ai:openKey:index\")]\n    public function index(): ResponseInterface\n    {\n        return $this->success($this->service->getPageList($this->request->all()));\n    }\n\n    /**\n     * 新增\n     * @param AiOpenaiKeyRequest $request\n     * @return ResponseInterface\n     * @throws \\Psr\\Container\\ContainerExceptionInterface\n     * @throws \\Psr\\Container\\NotFoundExceptionInterface\n     */\n    #[PostMapping(\"save\"), Permission(\"ai:openKey:save\"), OperationLog]\n    public function save(AiOpenaiKeyRequest $request): ResponseInterface\n    {\n        return $this->success(['id' => $this->service->save($request->all())]);\n    }\n\n    /**\n     * 更新\n     * @param int $id\n     * @param AiOpenaiKeyRequest $request\n     * @return ResponseInterface\n     * @throws \\Psr\\Container\\ContainerExceptionInterface\n     * @throws \\Psr\\Container\\NotFoundExceptionInterface\n     */\n    #[PutMapping(\"update/{id}\"), Permission(\"ai:openKey:update\"), OperationLog]\n    public function update(int $id, AiOpenaiKeyRequest $request): ResponseInterface\n    {\n        return $this->service->update($id, $request->all()) ? $this->success() : $this->error();\n    }\n\n    /**\n     * 读取数据\n     * @param int $id\n     * @return ResponseInterface\n     * @throws \\Psr\\Container\\ContainerExceptionInterface\n     * @throws \\Psr\\Container\\NotFoundExceptionInterface\n     */\n    #[GetMapping(\"read/{id}\"), Permission(\"ai:openKey:read\")]\n    public function read(int $id): ResponseInterface\n    {\n        return $this->success($this->service->read($id));\n    }\n\n    /**\n     * 单个或批量删除数据到回收站\n     * @return ResponseInterface\n     * @throws \\Psr\\Container\\ContainerExceptionInterface\n     * @throws \\Psr\\Container\\NotFoundExceptionInterface\n     */\n    #[DeleteMapping(\"delete\"), Permission(\"ai:openKey:delete\"), OperationLog]\n    public function delete(): ResponseInterface\n    {\n        return $this->service->delete((array) $this->request->input('ids', [])) ? $this->success() : $this->error();\n    }\n\n    /**\n     * 批量添加\n     * @return ResponseInterface\n     * @throws \\Psr\\Container\\ContainerExceptionInterface\n     * @throws \\Psr\\Container\\NotFoundExceptionInterface\n     */\n    #[PostMapping(\"batch-add\"), Permission(\"ai:openKey:batch-add\"), OperationLog]\n    public function batchAdd(): ResponseInterface\n    {\n        return $this->service->batchAdd($this->request->all()) ? $this->success() : $this->error();\n    }\n\n    /**\n     * 缓存列表\n     * @return ResponseInterface\n     * @throws \\Psr\\Container\\ContainerExceptionInterface\n     * @throws \\Psr\\Container\\NotFoundExceptionInterface\n     */\n    #[PostMapping(\"refresh-cache-list\"), Permission(\"ai:openKey:refresh-cache-list\"), OperationLog]\n    public function refreshCache(): ResponseInterface\n    {\n        return $this->service->cacheAll() ? $this->success() : $this->error();\n    }\n}"
  },
  {
    "path": "MineAdmin/php/app/Ai/Controller/AiOrderController.php",
    "content": "<?php\ndeclare(strict_types=1);\n\n\nnamespace App\\Ai\\Controller;\n\nuse App\\Ai\\Service\\AiOrderService;\nuse App\\Ai\\Request\\AiOrderRequest;\nuse Hyperf\\Di\\Annotation\\Inject;\nuse Hyperf\\HttpServer\\Annotation\\Controller;\nuse Hyperf\\HttpServer\\Annotation\\DeleteMapping;\nuse Hyperf\\HttpServer\\Annotation\\GetMapping;\nuse Hyperf\\HttpServer\\Annotation\\PostMapping;\nuse Hyperf\\HttpServer\\Annotation\\PutMapping;\nuse Mine\\Annotation\\Auth;\nuse Mine\\Annotation\\OperationLog;\nuse Mine\\Annotation\\Permission;\nuse Mine\\MineController;\nuse Psr\\Http\\Message\\ResponseInterface;\n\n/**\n * 订单表控制器\n * Class AiOrderController\n */\n#[Controller(prefix: \"ai/order\"), Auth]\nclass AiOrderController extends MineController\n{\n    /**\n     * 业务处理服务\n     * AiOrderService\n     */\n    #[Inject]\n    protected AiOrderService $service;\n\n    \n    /**\n     * 列表\n     * @return ResponseInterface\n     * @throws \\Psr\\Container\\ContainerExceptionInterface\n     * @throws \\Psr\\Container\\NotFoundExceptionInterface\n     */\n    #[GetMapping(\"index\"), Permission(\"ai:order, ai:order:index\")]\n    public function index(): ResponseInterface\n    {\n        return $this->success($this->service->getPageList($this->request->all()));\n    }\n\n    /**\n     * 读取数据\n     * @param int $id\n     * @return ResponseInterface\n     * @throws \\Psr\\Container\\ContainerExceptionInterface\n     * @throws \\Psr\\Container\\NotFoundExceptionInterface\n     */\n    #[GetMapping(\"read/{id}\"), Permission(\"ai:order:read\")]\n    public function read(int $id): ResponseInterface\n    {\n        return $this->success($this->service->read($id));\n    }\n\n    /**\n     * 单个或批量删除数据到回收站\n     * @return ResponseInterface\n     * @throws \\Psr\\Container\\ContainerExceptionInterface\n     * @throws \\Psr\\Container\\NotFoundExceptionInterface\n     */\n    #[DeleteMapping(\"delete\"), Permission(\"ai:order:delete\"), OperationLog]\n    public function delete(): ResponseInterface\n    {\n        return $this->service->delete((array) $this->request->input('ids', [])) ? $this->success() : $this->error();\n    }\n\n}"
  },
  {
    "path": "MineAdmin/php/app/Ai/Controller/AiPayKamiController.php",
    "content": "<?php\ndeclare(strict_types=1);\n\n\nnamespace App\\Ai\\Controller;\n\nuse App\\Ai\\Service\\AiPayKamiService;\nuse App\\Ai\\Request\\AiPayKamiRequest;\nuse Hyperf\\Di\\Annotation\\Inject;\nuse Hyperf\\HttpServer\\Annotation\\Controller;\nuse Hyperf\\HttpServer\\Annotation\\DeleteMapping;\nuse Hyperf\\HttpServer\\Annotation\\GetMapping;\nuse Hyperf\\HttpServer\\Annotation\\PostMapping;\nuse Hyperf\\HttpServer\\Annotation\\PutMapping;\nuse Mine\\Annotation\\Auth;\nuse Mine\\Annotation\\OperationLog;\nuse Mine\\Annotation\\Permission;\nuse Mine\\MineController;\nuse Psr\\Http\\Message\\ResponseInterface;\n\n/**\n * 卡密控制器\n * Class AiPayKamiController\n */\n#[Controller(prefix: \"ai/payKami\"), Auth]\nclass AiPayKamiController extends MineController\n{\n    /**\n     * 业务处理服务\n     * AiPayKamiService\n     */\n    #[Inject]\n    protected AiPayKamiService $service;\n\n    \n    /**\n     * 列表\n     * @return ResponseInterface\n     * @throws \\Psr\\Container\\ContainerExceptionInterface\n     * @throws \\Psr\\Container\\NotFoundExceptionInterface\n     */\n    #[GetMapping(\"index\"), Permission(\"ai:payKami, ai:payKami:index\")]\n    public function index(): ResponseInterface\n    {\n        return $this->success($this->service->getPageList($this->request->all()));\n    }\n\n    /**\n     * 读取数据\n     * @param int $id\n     * @return ResponseInterface\n     * @throws \\Psr\\Container\\ContainerExceptionInterface\n     * @throws \\Psr\\Container\\NotFoundExceptionInterface\n     */\n    #[GetMapping(\"read/{id}\"), Permission(\"ai:payKami:read\")]\n    public function read(int $id): ResponseInterface\n    {\n        return $this->success($this->service->read($id));\n    }\n\n    /**\n     * 更新\n     * @param int $id\n     * @param AiPayKamiRequest $request\n     * @return ResponseInterface\n     * @throws \\Psr\\Container\\ContainerExceptionInterface\n     * @throws \\Psr\\Container\\NotFoundExceptionInterface\n     */\n    #[PutMapping(\"update/{id}\"), Permission(\"ai:payKami:update\"), OperationLog]\n    public function update(int $id, AiPayKamiRequest $request): ResponseInterface\n    {\n        return $this->service->update($id, $request->all()) ? $this->success() : $this->error();\n    }\n\n    /**\n     * 单个或批量删除数据到回收站\n     * @return ResponseInterface\n     * @throws \\Psr\\Container\\ContainerExceptionInterface\n     * @throws \\Psr\\Container\\NotFoundExceptionInterface\n     */\n    #[DeleteMapping(\"delete\"), Permission(\"ai:payKami:delete\"), OperationLog]\n    public function delete(): ResponseInterface\n    {\n        return $this->service->delete((array) $this->request->input('ids', [])) ? $this->success() : $this->error();\n    }\n\n    /**\n     * 单个或批量删除数据到回收站\n     * @return ResponseInterface\n     * @throws \\Psr\\Container\\ContainerExceptionInterface\n     * @throws \\Psr\\Container\\NotFoundExceptionInterface\n     */\n    #[PostMapping(\"add\"), Permission(\"ai:payKami:add\"), OperationLog]\n    public function add(): ResponseInterface\n    {\n        $this->service->add($this->request->all());\n        return $this->success();\n    }\n}"
  },
  {
    "path": "MineAdmin/php/app/Ai/Controller/AiQuickIssueController.php",
    "content": "<?php\ndeclare(strict_types=1);\n\n\nnamespace App\\Ai\\Controller;\n\nuse App\\Ai\\Service\\AiQuickIssueService;\nuse App\\Ai\\Request\\AiQuickIssueRequest;\nuse Hyperf\\Di\\Annotation\\Inject;\nuse Hyperf\\HttpServer\\Annotation\\Controller;\nuse Hyperf\\HttpServer\\Annotation\\DeleteMapping;\nuse Hyperf\\HttpServer\\Annotation\\GetMapping;\nuse Hyperf\\HttpServer\\Annotation\\PostMapping;\nuse Hyperf\\HttpServer\\Annotation\\PutMapping;\nuse Mine\\Annotation\\Auth;\nuse Mine\\Annotation\\OperationLog;\nuse Mine\\Annotation\\Permission;\nuse Mine\\MineController;\nuse Psr\\Http\\Message\\ResponseInterface;\n\n/**\n * 快捷问题\n控制器\n * Class AiQuickIssueController\n */\n#[Controller(prefix: \"ai/quickIssue\"), Auth]\nclass AiQuickIssueController extends MineController\n{\n    /**\n     * 业务处理服务\n     * AiQuickIssueService\n     */\n    #[Inject]\n    protected AiQuickIssueService $service;\n\n    \n    /**\n     * 列表\n     * @return ResponseInterface\n     * @throws \\Psr\\Container\\ContainerExceptionInterface\n     * @throws \\Psr\\Container\\NotFoundExceptionInterface\n     */\n    #[GetMapping(\"index\"), Permission(\"ai:quickIssue, ai:quickIssue:index\")]\n    public function index(): ResponseInterface\n    {\n        return $this->success($this->service->getPageList($this->request->all()));\n    }\n\n    /**\n     * 更新\n     * @param int $id\n     * @param AiQuickIssueRequest $request\n     * @return ResponseInterface\n     * @throws \\Psr\\Container\\ContainerExceptionInterface\n     * @throws \\Psr\\Container\\NotFoundExceptionInterface\n     */\n    #[PutMapping(\"update/{id}\"), Permission(\"ai:quickIssue:update\"), OperationLog]\n    public function update(int $id, AiQuickIssueRequest $request): ResponseInterface\n    {\n        return $this->service->update($id, $request->all()) ? $this->success() : $this->error();\n    }\n\n    /**\n     * 新增\n     * @param AiQuickIssueRequest $request\n     * @return ResponseInterface\n     * @throws \\Psr\\Container\\ContainerExceptionInterface\n     * @throws \\Psr\\Container\\NotFoundExceptionInterface\n     */\n    #[PostMapping(\"save\"), Permission(\"ai:quickIssue:save\"), OperationLog]\n    public function save(AiQuickIssueRequest $request): ResponseInterface\n    {\n        return $this->success(['id' => $this->service->save($request->all())]);\n    }\n\n    /**\n     * 读取数据\n     * @param int $id\n     * @return ResponseInterface\n     * @throws \\Psr\\Container\\ContainerExceptionInterface\n     * @throws \\Psr\\Container\\NotFoundExceptionInterface\n     */\n    #[GetMapping(\"read/{id}\"), Permission(\"ai:quickIssue:read\")]\n    public function read(int $id): ResponseInterface\n    {\n        return $this->success($this->service->read($id));\n    }\n\n    /**\n     * 单个或批量删除数据到回收站\n     * @return ResponseInterface\n     * @throws \\Psr\\Container\\ContainerExceptionInterface\n     * @throws \\Psr\\Container\\NotFoundExceptionInterface\n     */\n    #[DeleteMapping(\"delete\"), Permission(\"ai:quickIssue:delete\"), OperationLog]\n    public function delete(): ResponseInterface\n    {\n        return $this->service->delete((array) $this->request->input('ids', [])) ? $this->success() : $this->error();\n    }\n\n}"
  },
  {
    "path": "MineAdmin/php/app/Ai/Controller/AiSettingController.php",
    "content": "<?php\ndeclare(strict_types=1);\n\nnamespace App\\Ai\\Controller;\n\nuse App\\Ai\\Service\\AiSettingService;\nuse App\\Ai\\Service\\QiniuService;\nuse Hyperf\\Di\\Annotation\\Inject;\nuse Hyperf\\HttpServer\\Annotation\\Controller;\nuse Hyperf\\HttpServer\\Annotation\\GetMapping;\nuse Hyperf\\HttpServer\\Annotation\\PostMapping;\nuse Mine\\Annotation\\Auth;\nuse Mine\\Annotation\\Permission;\nuse Mine\\MineController;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Http\\Message\\ResponseInterface;\n\n#[Controller(prefix: \"ai/setting\"), Auth]\nclass AiSettingController extends MineController\n{\n    #[Inject]\n    protected AiSettingService $settingService;\n\n    /**\n     * @throws ContainerExceptionInterface\n     * @throws NotFoundExceptionInterface\n     */\n    #[GetMapping(\"index\"), Permission(\"ai:setting, ai:setting:index\")]\n    public function index(): ResponseInterface\n    {\n        return $this->success(array_merge([\n            'app_close_message' => $this->settingService->appClose(),\n            'agreement_user' => $this->settingService->agreementUser(),\n            'openai_proxy' => $this->settingService->openaiProxy(),\n\n        ], $this->settingService->customer()));\n    }\n\n    /**\n     * @throws ContainerExceptionInterface\n     * @throws NotFoundExceptionInterface\n     */\n    #[PostMapping(\"save\"), Permission(\"ai:setting, ai:setting:save\")]\n    public function save(): ResponseInterface\n    {\n        $this->settingService->setAppClose($this->request->post('app_close_message', ''));\n        $this->settingService->setAgreementUser($this->request->post('agreement_user', ''));\n        $this->settingService->setCustomer($this->request->post('customer'));\n        $this->settingService->setOpenaiProxy($this->request->post('openai_proxy'));\n        return $this->success();\n    }\n\n    #[Inject]\n    protected QiniuService $qiniuService;\n\n    #[GetMapping(\"upload-token\")]\n    public function uploadToken(): ResponseInterface\n    {\n        return $this->success($this->qiniuService->token($this->request->query('scenes')));\n    }\n}"
  },
  {
    "path": "MineAdmin/php/app/Ai/Controller/AiUserController.php",
    "content": "<?php\ndeclare(strict_types=1);\n\n\nnamespace App\\Ai\\Controller;\n\nuse App\\Ai\\Service\\AiOrderService;\nuse App\\Ai\\Service\\AiUserService;\nuse App\\Ai\\Request\\AiUserRequest;\nuse Hyperf\\Di\\Annotation\\Inject;\nuse Hyperf\\HttpServer\\Annotation\\Controller;\nuse Hyperf\\HttpServer\\Annotation\\DeleteMapping;\nuse Hyperf\\HttpServer\\Annotation\\GetMapping;\nuse Hyperf\\HttpServer\\Annotation\\PostMapping;\nuse Hyperf\\HttpServer\\Annotation\\PutMapping;\nuse Mine\\Annotation\\Auth;\nuse Mine\\Annotation\\OperationLog;\nuse Mine\\Annotation\\Permission;\nuse Mine\\MineController;\nuse Psr\\Http\\Message\\ResponseInterface;\n\n/**\n * 用户主表控制器\n * Class AiUserController\n */\n#[Controller(prefix: \"ai/user\"), Auth]\nclass AiUserController extends MineController\n{\n    /**\n     * 业务处理服务\n     * AiUserService\n     */\n    #[Inject]\n    protected AiUserService $service;\n\n    #[Inject]\n    protected AiOrderService $orderService;\n    /**\n     * 列表\n     * @return ResponseInterface\n     * @throws \\Psr\\Container\\ContainerExceptionInterface\n     * @throws \\Psr\\Container\\NotFoundExceptionInterface\n     */\n    #[GetMapping(\"index\"), Permission(\"ai:user, ai:user:index\")]\n    public function index(): ResponseInterface\n    {\n        return $this->success($this->service->userList($this->request->all()));\n    }\n\n    /**\n     * 更新\n     * @param int $id\n     * @param AiUserRequest $request\n     * @return ResponseInterface\n     * @throws \\Psr\\Container\\ContainerExceptionInterface\n     * @throws \\Psr\\Container\\NotFoundExceptionInterface\n     */\n    #[PutMapping(\"update/{id}\"), Permission(\"ai:user:update\"), OperationLog]\n    public function update(int $id, AiUserRequest $request): ResponseInterface\n    {\n        return $this->service->update($id, $request->all()) ? $this->success() : $this->error();\n    }\n\n    /**\n     * 读取数据\n     * @param int $id\n     * @return ResponseInterface\n     * @throws \\Psr\\Container\\ContainerExceptionInterface\n     * @throws \\Psr\\Container\\NotFoundExceptionInterface\n     */\n    #[GetMapping(\"read/{id}\"), Permission(\"ai:user:read\")]\n    public function read(int $id): ResponseInterface\n    {\n        return $this->success($this->service->read($id));\n    }\n\n    /**\n     * 单个或批量删除数据到回收站\n     * @return ResponseInterface\n     * @throws \\Psr\\Container\\ContainerExceptionInterface\n     * @throws \\Psr\\Container\\NotFoundExceptionInterface\n     */\n    #[DeleteMapping(\"delete\"), Permission(\"ai:user:delete\"), OperationLog]\n    public function delete(): ResponseInterface\n    {\n        return $this->service->delete((array) $this->request->input('ids', [])) ? $this->success() : $this->error();\n    }\n\n    /**\n     * 更新\n     * @param int $id\n     * @param AiUserRequest $request\n     * @return ResponseInterface\n     * @throws \\Psr\\Container\\ContainerExceptionInterface\n     * @throws \\Psr\\Container\\NotFoundExceptionInterface\n     */\n    #[PostMapping(\"open-vip/{id}\"), Permission(\"ai:user:open-vip\"), OperationLog]\n    public function openVip(): ResponseInterface\n    {\n        $this->orderService->adminOpenVip($this->request->all());\n        return $this->success();\n    }\n\n    #[PostMapping(\"lock/{id}\"), Permission(\"ai:user:lock\"), OperationLog]\n    public function lock(int $id): ResponseInterface\n    {\n        $this->service->lock($id);\n        return $this->success();\n    }\n}"
  },
  {
    "path": "MineAdmin/php/app/Ai/Crontab/CheckVipOver.php",
    "content": "<?php\ndeclare(strict_types=1);\n\nnamespace App\\Ai\\Crontab;\n\nuse App\\Ai\\Model\\AiUser;\nuse Mine\\Annotation\\Transaction;\n\nclass CheckVipOver\n{\n    #[Transaction]\n    public function execute(): string\n    {\n        // todo\n        try {\n            $lastId = 0;\n            $time = time();\n            while (true){\n                $userList = AiUser::where('id', '>', $lastId)->where('vip','>', 0)->limit(1000)->get();\n                /**\n                 * @var AiUser $user\n                 */\n                $uidArr = [];\n                foreach ($userList as $user) {\n                    if ($user->vip_ent_at && $time > strtotime($user->vip_ent_at)) {\n                        $uidArr[] = $user->id;\n                    }\n                    $lastId = $user->id;\n                }\n                $uidArr && AiUser::whereIn('id', $uidArr)->update([\n                    'vip' => 0\n                ]);\n                if (count($userList) < 1000) {\n                    break;\n                }\n            }\n        } catch (\\Throwable $exception){\n\n        }\n        return 'success';\n    }\n}"
  },
  {
    "path": "MineAdmin/php/app/Ai/Database/Migrations/2023_05_04_145048_create_ai_user_table.php",
    "content": "<?php\n\n\nuse Hyperf\\Database\\Schema\\Schema;\nuse Hyperf\\Database\\Schema\\Blueprint;\nuse Hyperf\\Database\\Migrations\\Migration;\n\nclass CreateAiUserTable extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::create('ai_user', function (Blueprint $table) {\n            $table->engine = 'Innodb';\n            $table->comment('用户主表');\n            $table->increments('id')->comment('主键');\n            $table->addColumn('string', 'nick_name', ['length' => 50, 'comment' => '昵称'])->default('')->nullable(false);\n            $table->addColumn('string', 'head_img', ['length' => 100, 'comment' => '头像'])->default('')->nullable(false);\n            $table->addColumn('string', 'mobile', ['length' => 20, 'comment' => '手机号'])->default('')->nullable(false);\n            $table->addColumn('tinyInteger', 'vip', ['length' => 1, 'comment' => 'vip等级'])->index()->default(0)->nullable(false);\n            $table->addColumn('timestamp', 'vip_ent_at', ['precision' => 0, 'comment' => 'vip到期时间'])->nullable();\n            $table->addColumn('tinyInteger', 'is_lock', ['length' => 1, 'comment' => '是否锁定:1正常,2锁定'])->default(1)->nullable(false);\n            $table->addColumn('string', 'password', ['length' => 32, 'comment' => '密码'])->default('')->nullable(false);\n            $table->addColumn('integer', 'updated_by', ['comment' => '更新者'])->default(0)->nullable();\n            $table->addColumn('timestamp', 'created_at', ['precision' => 0, 'comment' => '创建时间'])->nullable();\n            $table->addColumn('timestamp', 'updated_at', ['precision' => 0, 'comment' => '更新时间'])->nullable();\n            $table->addColumn('timestamp', 'deleted_at', ['precision' => 0, 'comment' => '删除时间'])->nullable();\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::dropIfExists('ai_user');\n    }\n}\n"
  },
  {
    "path": "MineAdmin/php/app/Ai/Database/Migrations/2023_05_09_095456_create_ai_user_wallet_table.php",
    "content": "<?php\n\n\nuse Hyperf\\Database\\Schema\\Schema;\nuse Hyperf\\Database\\Schema\\Blueprint;\nuse Hyperf\\Database\\Migrations\\Migration;\n\nclass CreateAiUserWalletTable extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::create('ai_user_wallet', function (Blueprint $table) {\n            $table->engine = 'Innodb';\n            $table->comment('用户钱包');\n            $table->addColumn('integer', 'uid')->primary()->comment('用户UID');\n            $table->addColumn('integer', 'balance')->comment('余额')->unsigned()->default(0)->nullable(false);\n            $table->addColumn('integer', 'balance_total')->comment('总收入')->unsigned()->default(0)->nullable(false);\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::dropIfExists('ai_user_wallet');\n    }\n}\n"
  },
  {
    "path": "MineAdmin/php/app/Ai/Database/Migrations/2023_05_09_095504_create_ai_user_wallet_log_table.php",
    "content": "<?php\n\n\nuse Hyperf\\Database\\Schema\\Schema;\nuse Hyperf\\Database\\Schema\\Blueprint;\nuse Hyperf\\Database\\Migrations\\Migration;\n\nclass CreateAiUserWalletLogTable extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::create('ai_user_wallet_log', function (Blueprint $table) {\n            $table->engine = 'Innodb';\n            $table->comment('钱包变动记录');\n            $table->increments('id')->comment('主键');\n            $table->addColumn('integer', 'uid')->index()->comment('用户UID')->nullable(false);\n            $table->addColumn('integer', 'oid')->comment('订单ID')->index()->default(0)->nullable(false);\n            $table->addColumn('tinyInteger', 'direction', ['length' => 1, 'comment' => '类型:1收入,2支出'])->default(1)->nullable(false);\n            $table->addColumn('integer', 'balance')->comment('金额')->default(0)->nullable(false);\n            $table->addColumn('tinyInteger', 'scene', ['length' => 1, 'comment' => '变动场景: 1推广获益'])->default(1)->nullable(false);\n            $table->addColumn('string', 'remark', ['length' => 255, 'comment' => '备注'])->default(\"\")->nullable(false);\n            $table->addColumn('timestamp', 'created_at', ['precision' => 0, 'comment' => '创建时间'])->nullable();\n            $table->addColumn('timestamp', 'deleted_at', ['precision' => 0, 'comment' => '删除时间'])->nullable();\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::dropIfExists('ai_user_wallet_log');\n    }\n}\n"
  },
  {
    "path": "MineAdmin/php/app/Ai/Database/Migrations/2023_05_09_095525_create_ai_user_relation_table.php",
    "content": "<?php\n\n\nuse Hyperf\\Database\\Schema\\Schema;\nuse Hyperf\\Database\\Schema\\Blueprint;\nuse Hyperf\\Database\\Migrations\\Migration;\n\nclass CreateAiUserRelationTable extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::create('ai_user_relation', function (Blueprint $table) {\n            $table->engine = 'Innodb';\n            $table->comment('用户关系');\n            $table->addColumn('integer', 'uid')->primary()->comment('用户UID');\n            $table->addColumn('integer', 'from_uid', ['comment' => '上级'])->index()->default(1)->nullable(false);\n            $table->addColumn('integer', 'market_uid', ['comment' => '上级营销部'])->index()->default(0)->nullable(false);\n            $table->addColumn('timestamp', 'created_at', ['precision' => 0, 'comment' => '创建时间'])->nullable();\n            $table->addColumn('timestamp', 'updated_at', ['precision' => 0, 'comment' => '更新时间'])->nullable();\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::dropIfExists('ai_user_relation');\n    }\n}\n"
  },
  {
    "path": "MineAdmin/php/app/Ai/Database/Migrations/2023_05_09_095540_create_ai_order_table.php",
    "content": "<?php\n\n\nuse Hyperf\\Database\\Schema\\Schema;\nuse Hyperf\\Database\\Schema\\Blueprint;\nuse Hyperf\\Database\\Migrations\\Migration;\n\nclass CreateAiOrderTable extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::create('ai_order', function (Blueprint $table) {\n            $table->engine = 'Innodb';\n            $table->comment('订单表');\n            $table->increments('id')->comment('主键');\n            $table->addColumn('integer', 'uid', ['comment' => '用户UID'])->index()->default(0)->nullable(false);\n            $table->addColumn('integer', 'from_uid', ['comment' => '上级UID'])->default(0)->nullable(false);\n            $table->addColumn('integer', 'market_uid', ['comment' => '营销部UID'])->default(0)->nullable(false);\n            $table->addColumn('string', 'ord_sn', ['length' => 32, 'comment' => '订单号'])->unique()->default('')->nullable(false);\n            $table->addColumn('tinyInteger', 'ord_type', ['length' => 1, 'comment' => '订单类型: 1开通VIP 2提现 3成为营销部'])->default(1)->nullable(false);\n            $table->addColumn('tinyInteger', 'pay_type', ['length' => 1, 'comment' => '支付方式: 1微信 2后台付费'])->default(1)->nullable(false);\n            $table->addColumn('tinyInteger', 'status', ['length' => 1, 'comment' => '订单状态 1未支付(待处理) 2已支付(已完成) 3失败'])->default(1)->nullable(false);\n            $table->addColumn('integer', 'total_price', ['length' => 8, 'comment' => '总金额'])->default(0)->nullable(false);\n            $table->addColumn('integer', 'amount_price', ['length' => 8, 'comment' => '实际金额'])->default(0)->nullable(false);\n            $table->addColumn('string', 'content', ['length' => 255, 'comment' => '订单描述'])->default('')->nullable(false);\n            $table->addColumn('string', 'remark', ['length' => 255, 'comment' => '备注'])->default(\"\")->nullable(false);\n            $table->addColumn('bigInteger', 'created_by', ['comment' => '创建者'])->default(0)->nullable();\n            $table->addColumn('bigInteger', 'updated_by', ['comment' => '更新者'])->default(0)->nullable();\n            $table->addColumn('timestamp', 'created_at', ['precision' => 0, 'comment' => '创建时间'])->nullable();\n            $table->addColumn('timestamp', 'pay_at', ['precision' => 0, 'comment' => '订单完成时间'])->nullable();\n            $table->addColumn('timestamp', 'updated_at', ['precision' => 0, 'comment' => '更新时间'])->nullable();\n            $table->addColumn('timestamp', 'deleted_at', ['precision' => 0, 'comment' => '删除时间'])->nullable();\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::dropIfExists('ai_order');\n    }\n}\n"
  },
  {
    "path": "MineAdmin/php/app/Ai/Database/Migrations/2023_05_09_095543_create_ai_order_vip_table.php",
    "content": "<?php\n\n\nuse Hyperf\\Database\\Schema\\Schema;\nuse Hyperf\\Database\\Schema\\Blueprint;\nuse Hyperf\\Database\\Migrations\\Migration;\n\nclass CreateAiOrderVipTable extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::create('ai_order_vip', function (Blueprint $table) {\n            $table->engine = 'Innodb';\n            $table->comment('vip订单');\n            $table->integer('oid')->primary()->comment('订单ID');\n            $table->addColumn('tinyInteger', 'vip_level', ['length'=>1, 'comment' => 'vip等级'])->default(1)->nullable(false);\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::dropIfExists('ai_order_vip');\n    }\n}\n"
  },
  {
    "path": "MineAdmin/php/app/Ai/Database/Migrations/2023_05_09_102806_create_ai_mine_menu_group_table.php",
    "content": "<?php\n\n\nuse Hyperf\\Database\\Schema\\Schema;\nuse Hyperf\\Database\\Schema\\Blueprint;\nuse Hyperf\\Database\\Migrations\\Migration;\n\nclass CreateAiMineMenuGroupTable extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::create('ai_mine_menu_group', function (Blueprint $table) {\n            $table->engine = 'Innodb';\n            $table->comment('个人中心菜单分组');\n            $table->bigIncrements('id')->comment('主键');\n            $table->addColumn('string', 'name', ['length' => 100, 'comment' => '分组名称'])->default('')->nullable(false);\n            $table->addColumn('integer', 'sort', ['comment' => '排序'])->default(0)->nullable(false);\n            $table->addColumn('tinyInteger', 'is_lock', ['length' => 1, 'comment' => '是否锁定:1正常,锁定'])->default(1)->nullable(false);\n            $table->addColumn('timestamp', 'created_at', ['precision' => 0, 'comment' => '创建时间'])->nullable();\n            $table->addColumn('timestamp', 'updated_at', ['precision' => 0, 'comment' => '更新时间'])->nullable();\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::dropIfExists('ai_mine_menu_group');\n    }\n}\n"
  },
  {
    "path": "MineAdmin/php/app/Ai/Database/Migrations/2023_05_09_102817_create_ai_mine_menu_table.php",
    "content": "<?php\n\n\nuse Hyperf\\Database\\Schema\\Schema;\nuse Hyperf\\Database\\Schema\\Blueprint;\nuse Hyperf\\Database\\Migrations\\Migration;\n\nclass CreateAiMineMenuTable extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::create('ai_mine_menu', function (Blueprint $table) {\n            $table->engine = 'Innodb';\n            $table->comment('个人中心菜单');\n            $table->bigIncrements('id')->comment('主键');\n            $table->addColumn('integer', 'gid', ['comment' => '分组ID'])->index()->default(0)->nullable(false);\n            $table->addColumn('string', 'name', ['length' => 50, 'comment' => '菜单名称'])->default('')->nullable(false);\n            $table->addColumn('integer', 'sort', ['comment' => '排序'])->default(0)->nullable(false);\n            $table->addColumn('tinyInteger', 'use_vip', ['length' => 2, 'comment' => '使用权限限制 0全部'])->default(0)->nullable(false);\n            $table->addColumn('tinyInteger', 'click_type', ['length' => 1, 'comment' => '点击类型 1跳转 2调用函数'])->default(1)->nullable(false);\n            $table->addColumn('string', 'click_func', ['length' => 50, 'comment' => '函数标识 小程序端提前封装'])->default('')->nullable(false);\n            $table->addColumn('string', 'path', ['length' => 100, 'comment' => '打开的页面路径'])->default('')->nullable(false);\n            $table->addColumn('string', 'app_id', ['length' => 100, 'comment' => '小程序appid'])->default('')->nullable(false);\n            $table->addColumn('string', 'extra_data', ['length' => 100, 'comment' => '需要传递给目标小程序的数据 json'])->default('')->nullable(false);\n            $table->addColumn('string', 'env_version', ['length' => 100, 'comment' => '要打开的小程序版本'])->default('')->nullable(false);\n            $table->addColumn('string', 'short_link', ['length' => 100, 'comment' => '小程序链接'])->default('')->nullable(false);\n            $table->addColumn('string', 'icon', ['length' => 255, 'comment' => '菜单图标'])->default('')->nullable(false);\n            $table->addColumn('tinyInteger', 'is_lock', ['length' => 1, 'comment' => '是否锁定:1正常,2锁定'])->default(1)->nullable(false);\n            $table->addColumn('timestamp', 'created_at', ['precision' => 0, 'comment' => '创建时间'])->nullable();\n            $table->addColumn('timestamp', 'updated_at', ['precision' => 0, 'comment' => '更新时间'])->nullable();\n            $table->addColumn('timestamp', 'deleted_at', ['precision' => 0, 'comment' => '删除时间'])->nullable();\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::dropIfExists('ai_mine_menu');\n    }\n}\n"
  },
  {
    "path": "MineAdmin/php/app/Ai/Database/Migrations/2023_05_09_154733_create_ai_chatgpt_prompts_table.php",
    "content": "<?php\n\n\nuse Hyperf\\Database\\Schema\\Schema;\nuse Hyperf\\Database\\Schema\\Blueprint;\nuse Hyperf\\Database\\Migrations\\Migration;\n\nclass CreateAiChatgptPromptsTable extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::create('ai_chatgpt_prompts', function (Blueprint $table) {\n            $table->engine = 'Innodb';\n            $table->comment('chatgpt角色');\n            $table->increments('id')->comment('主键');\n            $table->addColumn('string', 'act', ['length'=>100, 'comment' => '角色名称'])->default('')->nullable(false);\n            $table->addColumn('text', 'prompt', ['comment' => '角色说明'])->nullable(false);\n            $table->addColumn('integer', 'sort', ['comment' => '排序'])->default(0)->nullable(false);\n            $table->addColumn('string', 'remark', ['length' => 255, 'comment' => '备注'])->default('')->nullable(false);\n            $table->addColumn('integer', 'created_by', ['comment' => '创建者'])->default(0)->nullable(false);\n            $table->addColumn('integer', 'updated_by', ['comment' => '更新者'])->default(0)->nullable(false);\n            $table->addColumn('timestamp', 'created_at', ['precision' => 0, 'comment' => '创建时间'])->nullable();\n            $table->addColumn('timestamp', 'updated_at', ['precision' => 0, 'comment' => '更新时间'])->nullable();\n            $table->addColumn('timestamp', 'deleted_at', ['precision' => 0, 'comment' => '删除时间'])->nullable();\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::dropIfExists('ai_chatgpt_prompts');\n    }\n}\n"
  },
  {
    "path": "MineAdmin/php/app/Ai/Database/Migrations/2023_05_10_165842_create_ai_chat_message_table.php",
    "content": "<?php\n\n\nuse Hyperf\\Database\\Schema\\Schema;\nuse Hyperf\\Database\\Schema\\Blueprint;\nuse Hyperf\\Database\\Migrations\\Migration;\n\nclass CreateAiChatMessageTable extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::create('ai_chat_message', function (Blueprint $table) {\n            $table->engine = 'Innodb';\n            $table->comment('聊天数据');\n            $table->bigIncrements('id')->comment('主键');\n            $table->addColumn('integer', 'sid', ['comment' => '会话ID'])->index()->default(0)->nullable(false);\n            $table->addColumn('text', 'content', ['comment' => '内容'])->nullable(false);\n            $table->addColumn('text', 'reply_content', ['comment' => '回复内容'])->nullable(false);\n            $table->addColumn('timestamp', 'reply_at', ['precision' => 0, 'comment' => '回复时间'])->nullable();\n            $table->addColumn('timestamp', 'created_at', ['precision' => 0, 'comment' => '创建时间'])->nullable();\n            $table->addColumn('timestamp', 'deleted_at', ['precision' => 0, 'comment' => '删除时间'])->nullable();\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::dropIfExists('ai_chat_message');\n    }\n}\n"
  },
  {
    "path": "MineAdmin/php/app/Ai/Database/Migrations/2023_05_10_171603_create_ai_chat_session_table.php",
    "content": "<?php\n\n\nuse Hyperf\\Database\\Schema\\Schema;\nuse Hyperf\\Database\\Schema\\Blueprint;\nuse Hyperf\\Database\\Migrations\\Migration;\n\nclass CreateAiChatSessionTable extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::create('ai_chat_session', function (Blueprint $table) {\n            $table->engine = 'Innodb';\n            $table->comment('问答会话');\n            $table->increments('id')->comment('主键');\n            $table->addColumn('integer', 'uid', ['comment' => '用户uid'])->index()->default(0)->nullable(false);\n            $table->addColumn('integer', 'prompt_id', ['comment' => '模型ID'])->index()->default(0)->nullable(false);\n            $table->addColumn('tinyInteger', 'close', ['length' => 1, 'comment' => '是否关闭:1正常,2关闭'])->default(1)->nullable(false);\n            $table->addColumn('tinyInteger', 'share', ['length' => 1, 'comment' => '是否分享:1关闭,2公开'])->default(1)->nullable(false);\n            $table->addColumn('timestamp', 'created_at', ['precision' => 0, 'comment' => '创建时间'])->nullable();\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::dropIfExists('ai_chat_session');\n    }\n}\n"
  },
  {
    "path": "MineAdmin/php/app/Ai/Database/Migrations/2023_05_12_152504_create_ai_quick_issue_table.php",
    "content": "<?php\n\n\nuse Hyperf\\Database\\Schema\\Schema;\nuse Hyperf\\Database\\Schema\\Blueprint;\nuse Hyperf\\Database\\Migrations\\Migration;\n\nclass CreateAiQuickIssueTable extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::create('ai_quick_issue', function (Blueprint $table) {\n            $table->engine = 'Innodb';\n            $table->comment('快捷问题');\n            $table->increments('id')->comment('主键');\n            $table->addColumn('string', 'title', ['length' => 100, 'comment' => '问题标题'])->default('')->nullable(false);\n            $table->addColumn('string', 'content', ['length' => 255, 'comment' => '问题描述'])->default('')->nullable(false);\n            $table->addColumn('integer', 'sort', ['comment' => '排序'])->default(0)->nullable(false);\n            $table->addColumn('timestamp', 'created_at', ['precision' => 0, 'comment' => '创建时间'])->nullable();\n            $table->addColumn('timestamp', 'updated_at', ['precision' => 0, 'comment' => '更新时间'])->nullable();\n            $table->addColumn('timestamp', 'deleted_at', ['precision' => 0, 'comment' => '删除时间'])->nullable();\n\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::dropIfExists('ai_quick_issue');\n    }\n}\n"
  },
  {
    "path": "MineAdmin/php/app/Ai/Database/Migrations/2023_05_19_164456_create_ai_pay_kami_table.php",
    "content": "<?php\n\n\nuse Hyperf\\Database\\Schema\\Schema;\nuse Hyperf\\Database\\Schema\\Blueprint;\nuse Hyperf\\Database\\Migrations\\Migration;\n\nclass CreateAiPayKamiTable extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::create('ai_pay_kami', function (Blueprint $table) {\n            $table->engine = 'Innodb';\n            $table->comment('卡密');\n            $table->increments('id')->comment('主键');\n            $table->addColumn('integer', 'uid', ['comment' => '绑定用户'])->default(0)->nullable(false);\n            $table->addColumn('integer', 'price', ['comment' => '价格'])->default(0)->nullable(false);\n            $table->addColumn('string', 'code', ['length' => 32, 'comment' => '卡密号'])->unique()->default('')->nullable(false);\n            $table->addColumn('tinyInteger', 'status', ['length' => 1, 'comment' => '状态 1未使用 2已使用'])->default(1)->nullable(false);\n            $table->addColumn('string', 'remark', ['length' => 255, 'comment' => '备注'])->default('')->nullable(false);\n            $table->addColumn('bigInteger', 'created_by', ['comment' => '创建者'])->default(0)->nullable(false);\n            $table->addColumn('bigInteger', 'updated_by', ['comment' => '更新者'])->default(0)->nullable(false);\n            $table->addColumn('timestamp', 'created_at', ['precision' => 0, 'comment' => '创建时间'])->nullable();\n            $table->addColumn('timestamp', 'use_at', ['precision' => 0, 'comment' => '使用时间'])->nullable();\n            $table->addColumn('timestamp', 'updated_at', ['precision' => 0, 'comment' => '更新时间'])->nullable();\n            $table->addColumn('timestamp', 'deleted_at', ['precision' => 0, 'comment' => '删除时间'])->nullable();\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::dropIfExists('ai_pay_carmi');\n    }\n}\n"
  },
  {
    "path": "MineAdmin/php/app/Ai/Database/Migrations/2023_05_25_152855_create_ai_openai_key_table.php",
    "content": "<?php\n\n\nuse Hyperf\\Database\\Schema\\Schema;\nuse Hyperf\\Database\\Schema\\Blueprint;\nuse Hyperf\\Database\\Migrations\\Migration;\n\nclass CreateAiOpenaiKeyTable extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::create('ai_openai_key', function (Blueprint $table) {\n            $table->engine = 'Innodb';\n            $table->comment('openai_key');\n            $table->increments('id')->comment('主键');\n            $table->addColumn('string', 'openai_key', ['length' => 255, 'comment' => 'openai_key'])->nullable();\n            $table->addColumn('string', 'remark', ['length' => 255, 'comment' => '备注'])->nullable();\n            $table->addColumn('bigInteger', 'created_by', ['comment' => '创建者'])->nullable();\n            $table->addColumn('bigInteger', 'updated_by', ['comment' => '更新者'])->nullable();\n            $table->addColumn('timestamp', 'created_at', ['precision' => 0, 'comment' => '创建时间'])->nullable();\n            $table->addColumn('timestamp', 'updated_at', ['precision' => 0, 'comment' => '更新时间'])->nullable();\n            $table->addColumn('timestamp', 'deleted_at', ['precision' => 0, 'comment' => '删除时间'])->nullable();\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::dropIfExists('ai_openai_key');\n    }\n}\n"
  },
  {
    "path": "MineAdmin/php/app/Ai/Database/Migrations/2023_05_26_111034_create_ai_order_kami_table.php",
    "content": "<?php\n\n\nuse Hyperf\\Database\\Schema\\Schema;\nuse Hyperf\\Database\\Schema\\Blueprint;\nuse Hyperf\\Database\\Migrations\\Migration;\n\nclass CreateAiOrderKamiTable extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::create('ai_order_kami', function (Blueprint $table) {\n            $table->engine = 'Innodb';\n            $table->comment('订单关联卡密');\n            $table->integer('oid')->primary()->comment('订单ID');\n            $table->integer('kid')->comment('卡密ID');\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::dropIfExists('ai_order_kami');\n    }\n}\n"
  },
  {
    "path": "MineAdmin/php/app/Ai/Database/Migrations/2023_06_14_141605_create_ai_image_material_table.php",
    "content": "<?php\n\n\nuse Hyperf\\Database\\Schema\\Schema;\nuse Hyperf\\Database\\Schema\\Blueprint;\nuse Hyperf\\Database\\Migrations\\Migration;\n\nclass CreateAiImageMaterialTable extends Migration\n{\n    /**\n     * Run the migrations.\n     */\n    public function up(): void\n    {\n        Schema::create('ai_image_material', function (Blueprint $table) {\n            $table->engine = 'Innodb';\n            $table->comment('图片素材');\n            $table->increments('id')->comment('主键');\n            $table->addColumn('tinyInteger', 'scene', ['length' => 1, 'comment' => '使用场景'])->default(1)->nullable(false);\n            $table->addColumn('string', 'img_url', ['length' => 255, 'comment' => '图片地址'])->default('')->nullable(false);\n            $table->addColumn('string', 'url', ['length' => 255, 'comment' => '跳转地址'])->default('')->nullable(false);\n            $table->addColumn('string', 'remark', ['length' => 255, 'comment' => '备注'])->default('')->nullable(false);\n            $table->addColumn('integer', 'sort', ['comment' => '排序'])->default(0)->nullable(false);\n            $table->addColumn('integer', 'created_by', ['comment' => '创建者'])->default(0)->nullable(false);\n            $table->addColumn('integer', 'updated_by', ['comment' => '更新者'])->default(0)->nullable(false);\n            $table->addColumn('timestamp', 'start_at', ['precision' => 0, 'comment' => '使用开始时间'])->nullable();\n            $table->addColumn('timestamp', 'end_at', ['precision' => 0, 'comment' => '使用结束时间'])->nullable();\n            $table->addColumn('timestamp', 'created_at', ['precision' => 0, 'comment' => '创建时间'])->nullable();\n            $table->addColumn('timestamp', 'updated_at', ['precision' => 0, 'comment' => '更新时间'])->nullable();\n            $table->addColumn('timestamp', 'deleted_at', ['precision' => 0, 'comment' => '删除时间'])->nullable();\n        });\n    }\n\n    /**\n     * Reverse the migrations.\n     */\n    public function down(): void\n    {\n        Schema::dropIfExists('ai_image_material');\n    }\n}\n"
  },
  {
    "path": "MineAdmin/php/app/Ai/Database/Seeders/ai_chatgpt_prompts.php",
    "content": "<?php\ndeclare(strict_types=1);\n\nuse Hyperf\\Database\\Seeders\\Seeder;\nuse Hyperf\\DbConnection\\Db;\n\nclass AiChatgptPrompts extends Seeder\n{\n    /**\n     * Run the database seeds.\n     *\n     * @return void\n     */\n    public function run()\n    {\n        $isInit = \\App\\Ai\\Model\\AiChatgptPrompts::where(['id'=>1])->first();\n        if (!empty($isInit)){\n            echo '大功告成'.PHP_EOL;\n            return;\n        }\n\n        $tableName = env('DB_PREFIX') . \\App\\Ai\\Model\\AiChatgptPrompts::getModel()->getTable();\n        $sql = [\n            \"INSERT INTO `{$tableName}`(`id`, `act`, `prompt`, `sort`, `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at`, `remark`) VALUES (1, 'Ai助手', '你是一个Ai智能助手，我需要你模拟一名温柔贴心的女朋友来回答我的问题。', 0, 1, 1, now(), now(), NULL, '')\",\n        ];\n        foreach ($sql as $item) {\n            Db::insert($item);\n        }\n        echo '大功告成'.PHP_EOL;\n    }\n}\n"
  },
  {
    "path": "MineAdmin/php/app/Ai/Database/Seeders/ai_mine_menu.php",
    "content": "<?php\ndeclare(strict_types=1);\n\nuse Hyperf\\Database\\Seeders\\Seeder;\nuse Hyperf\\DbConnection\\Db;\n\nclass AiMineMenu extends Seeder\n{\n    /**\n     * Run the database seeds.\n     *\n     * @return void\n     */\n    public function run()\n    {\n        Db::table('ai_mine_menu')->truncate();\n        $tableName = env('DB_PREFIX') . \\App\\Ai\\Model\\AiMineMenu::getModel()->getTable();\n        $sql = [\n            \"INSERT INTO `{$tableName}`(`id`, `gid`, `name`, `sort`, `use_vip`, `click_type`, `click_func`, `path`, `app_id`, `extra_data`, `env_version`, `short_link`, `icon`, `is_lock`, `created_at`, `updated_at`, `deleted_at`) VALUES (1, 1, '会员特权', 4, 0, 2, '1002', '', '', '', '', '', 'https://www.putyy.com/uploads/images/vip.png', 1, now(), now(), NULL);\",\n            \"INSERT INTO `{$tableName}`(`id`, `gid`, `name`, `sort`, `use_vip`, `click_type`, `click_func`, `path`, `app_id`, `extra_data`, `env_version`, `short_link`, `icon`, `is_lock`, `created_at`, `updated_at`, `deleted_at`) VALUES (2, 1, '联系客服', 3, 0, 2, '1001', '', '', '', '', '', 'https://www.putyy.com/uploads/images/kf.png', 2, now(), now(), NULL);\",\n            \"INSERT INTO `{$tableName}`(`id`, `gid`, `name`, `sort`, `use_vip`, `click_type`, `click_func`, `path`, `app_id`, `extra_data`, `env_version`, `short_link`, `icon`, `is_lock`, `created_at`, `updated_at`, `deleted_at`) VALUES (3, 1, '清空缓存', 0, 0, 2, '1003', '', '', '', '', '', 'https://www.putyy.com/uploads/images/clear.png', 1, now(), now(), NULL);\",\n            \"INSERT INTO `{$tableName}`(`id`, `gid`, `name`, `sort`, `use_vip`, `click_type`, `click_func`, `path`, `app_id`, `extra_data`, `env_version`, `short_link`, `icon`, `is_lock`, `created_at`, `updated_at`, `deleted_at`) VALUES (4, 1, '订单列表', 0, 0, 1, '', '/pages/user/order', '', '', '', '', 'https://www.putyy.com/uploads/images/list.png', 1, now(), now(), NULL);\",\n        ];\n        foreach ($sql as $item){\n            Db::insert($item);\n        }\n        echo '大功告成'.PHP_EOL;\n    }\n}\n"
  },
  {
    "path": "MineAdmin/php/app/Ai/Database/Seeders/ai_mine_menu_group.php",
    "content": "<?php\ndeclare(strict_types=1);\n\nuse Hyperf\\Database\\Seeders\\Seeder;\nuse Hyperf\\DbConnection\\Db;\n\nclass AiMineMenuGroup extends Seeder\n{\n    /**\n     * Run the database seeds.\n     *\n     * @return void\n     */\n    public function run()\n    {\n        Db::table('ai_mine_menu_group')->truncate();\n        $tableName = env('DB_PREFIX') . \\App\\Ai\\Model\\AiMineMenuGroup::getModel()->getTable();\n        Db::insert(\"INSERT INTO `$tableName`(`id`, `name`, `sort`, `is_lock`, `created_at`, `updated_at`) VALUES (1, '会员服务', 1, 1, now(), now());\");\n        echo '大功告成'.PHP_EOL;\n    }\n}\n"
  },
  {
    "path": "MineAdmin/php/app/Ai/Database/Seeders/ai_user.php",
    "content": "<?php\n/**\n * MineAdmin is committed to providing solutions for quickly building web applications\n * Please view the LICENSE file that was distributed with this source code,\n * For the full copyright and license information.\n * Thank you very much for using MineAdmin.\n *\n * @Author X.Mo<root@imoi.cn>\n * @Link   https://gitee.com/xmo/MineAdmin\n */\n\ndeclare(strict_types=1);\n\nuse Hyperf\\Database\\Seeders\\Seeder;\nuse Hyperf\\DbConnection\\Db;\n\nclass AiUser extends Seeder\n{\n    /**\n     * Run the database seeds.\n     *\n     * @return void\n     */\n    public function run()\n    {\n        $isInit = \\App\\Ai\\Model\\AiUser::where(['id'=>1])->first();\n        if (!empty($isInit)){\n            echo '大功告成'.PHP_EOL;\n            return;\n        }\n\n        $tableName = env('DB_PREFIX') . \\App\\Ai\\Model\\AiUser::getModel()->getTable();\n\n        Db::insert(\"INSERT INTO `{$tableName}`(`id`, `nick_name`, `head_img`, `mobile`, `vip`, `vip_ent_at`, `password`, `is_lock`, `updated_by`, `created_at`, `updated_at`, `deleted_at`) VALUES (1, 'ChatGPT', '', '123456789', 30, '2036-03-11 16:02:58', 'e10adc3949ba59abbe56e057f20f883e', 1, 1, now(), now(), NULL);\");\n\n        \\App\\Ai\\Model\\AiUserRelation::insert([\n            'uid'       => 1,\n            'from_uid'  => 1,\n        ]);\n\n        \\App\\Ai\\Model\\AiUserWallet::insert([\n            'uid' => 1,\n        ]);\n        echo '大功告成'.PHP_EOL;\n    }\n}\n"
  },
  {
    "path": "MineAdmin/php/app/Ai/Dto/AiChatMessageDto.php",
    "content": "<?php\nnamespace App\\Ai\\Dto;\n\nuse Mine\\Interfaces\\MineModelExcel;\nuse Mine\\Annotation\\ExcelData;\nuse Mine\\Annotation\\ExcelProperty;\n\n/**\n * 聊天数据Dto （导入导出）\n */\n#[ExcelData]\nclass AiChatMessageDto implements MineModelExcel\n{\n    #[ExcelProperty(value: \"主键\", index: 0)]\n    public string $id;\n\n    #[ExcelProperty(value: \"会话ID\", index: 1)]\n    public string $sid;\n\n    #[ExcelProperty(value: \"内容\", index: 2)]\n    public string $content;\n\n    #[ExcelProperty(value: \"回复内容\", index: 3)]\n    public string $reply_content;\n\n    #[ExcelProperty(value: \"回复时间\", index: 4)]\n    public string $reply_at;\n\n    #[ExcelProperty(value: \"创建时间\", index: 4)]\n    public string $created_at;\n\n    #[ExcelProperty(value: \"删除时间\", index: 5)]\n    public string $deleted_at;\n\n\n}"
  },
  {
    "path": "MineAdmin/php/app/Ai/Dto/AiChatSessionDto.php",
    "content": "<?php\nnamespace App\\Ai\\Dto;\n\nuse Mine\\Interfaces\\MineModelExcel;\nuse Mine\\Annotation\\ExcelData;\nuse Mine\\Annotation\\ExcelProperty;\n\n/**\n * 问答会话Dto （导入导出）\n */\n#[ExcelData]\nclass AiChatSessionDto implements MineModelExcel\n{\n    #[ExcelProperty(value: \"主键\", index: 0)]\n    public string $id;\n\n    #[ExcelProperty(value: \"用户uid\", index: 1)]\n    public string $uid;\n\n    #[ExcelProperty(value: \"模型ID\", index: 2)]\n    public string $prompt_id;\n\n    #[ExcelProperty(value: \"是否关闭:1正常,2关闭\", index: 3)]\n    public string $close;\n\n    #[ExcelProperty(value: \"是否分享:1关闭,2公开\", index: 4)]\n    public string $share;\n\n    #[ExcelProperty(value: \"创建时间\", index: 5)]\n    public string $created_at;\n\n\n}"
  },
  {
    "path": "MineAdmin/php/app/Ai/Dto/AiChatgptPromptsDto.php",
    "content": "<?php\nnamespace App\\Ai\\Dto;\n\nuse Mine\\Interfaces\\MineModelExcel;\nuse Mine\\Annotation\\ExcelData;\nuse Mine\\Annotation\\ExcelProperty;\n\n/**\n * chatgpt角色Dto （导入导出）\n */\n#[ExcelData]\nclass AiChatgptPromptsDto implements MineModelExcel\n{\n    #[ExcelProperty(value: \"主键\", index: 0)]\n    public string $id;\n\n    #[ExcelProperty(value: \"角色名称\", index: 1)]\n    public string $act;\n\n    #[ExcelProperty(value: \"角色说明\", index: 2)]\n    public string $prompt;\n\n    #[ExcelProperty(value: \"排序\", index: 3)]\n    public string $sort;\n\n    #[ExcelProperty(value: \"创建者\", index: 4)]\n    public string $created_by;\n\n    #[ExcelProperty(value: \"更新者\", index: 5)]\n    public string $updated_by;\n\n    #[ExcelProperty(value: \"创建时间\", index: 6)]\n    public string $created_at;\n\n    #[ExcelProperty(value: \"更新时间\", index: 7)]\n    public string $updated_at;\n\n    #[ExcelProperty(value: \"删除时间\", index: 8)]\n    public string $deleted_at;\n\n    #[ExcelProperty(value: \"备注\", index: 9)]\n    public string $remark;\n\n\n}"
  },
  {
    "path": "MineAdmin/php/app/Ai/Dto/AiImageMaterialDto.php",
    "content": "<?php\nnamespace App\\Ai\\Dto;\n\nuse Mine\\Interfaces\\MineModelExcel;\nuse Mine\\Annotation\\ExcelData;\nuse Mine\\Annotation\\ExcelProperty;\n\n/**\n * 图片素材Dto （导入导出）\n */\n#[ExcelData]\nclass AiImageMaterialDto implements MineModelExcel\n{\n    #[ExcelProperty(value: \"主键\", index: 0)]\n    public string $id;\n\n    #[ExcelProperty(value: \"使用场景\", index: 1)]\n    public string $scene;\n\n    #[ExcelProperty(value: \"图片地址\", index: 2)]\n    public string $img_url;\n\n    #[ExcelProperty(value: \"跳转地址\", index: 3)]\n    public string $url;\n\n    #[ExcelProperty(value: \"备注\", index: 4)]\n    public string $remark;\n\n    #[ExcelProperty(value: \"排序\", index: 5)]\n    public string $sort;\n\n    #[ExcelProperty(value: \"创建者\", index: 6)]\n    public string $created_by;\n\n    #[ExcelProperty(value: \"更新者\", index: 7)]\n    public string $updated_by;\n\n    #[ExcelProperty(value: \"使用开始时间\", index: 8)]\n    public string $start_at;\n\n    #[ExcelProperty(value: \"使用结束时间\", index: 9)]\n    public string $end_at;\n\n    #[ExcelProperty(value: \"创建时间\", index: 10)]\n    public string $created_at;\n\n    #[ExcelProperty(value: \"更新时间\", index: 11)]\n    public string $updated_at;\n\n    #[ExcelProperty(value: \"删除时间\", index: 12)]\n    public string $deleted_at;\n\n\n}"
  },
  {
    "path": "MineAdmin/php/app/Ai/Dto/AiMineMenuDto.php",
    "content": "<?php\nnamespace App\\Ai\\Dto;\n\nuse Mine\\Interfaces\\MineModelExcel;\nuse Mine\\Annotation\\ExcelData;\nuse Mine\\Annotation\\ExcelProperty;\n\n/**\n * 个人中心菜单Dto （导入导出）\n */\n#[ExcelData]\nclass AiMineMenuDto implements MineModelExcel\n{\n    #[ExcelProperty(value: \"主键\", index: 0)]\n    public string $id;\n\n    #[ExcelProperty(value: \"分组ID\", index: 1)]\n    public string $gid;\n\n    #[ExcelProperty(value: \"分组名称\", index: 2)]\n    public string $name;\n\n    #[ExcelProperty(value: \"排序\", index: 3)]\n    public string $sort;\n\n    #[ExcelProperty(value: \"使用权限限制 0全部\", index: 4)]\n    public string $use_vip;\n\n    #[ExcelProperty(value: \"点击类型 1跳转 2调用函数\", index: 5)]\n    public string $click_type;\n\n    #[ExcelProperty(value: \"函数标识 小程序端提前封装\", index: 6)]\n    public string $click_func;\n\n    #[ExcelProperty(value: \"打开的页面路径\", index: 7)]\n    public string $path;\n\n    #[ExcelProperty(value: \"小程序appid\", index: 8)]\n    public string $app_id;\n\n    #[ExcelProperty(value: \"需要传递给目标小程序的数据 json\", index: 9)]\n    public string $extra_data;\n\n    #[ExcelProperty(value: \"要打开的小程序版本\", index: 10)]\n    public string $env_version;\n\n    #[ExcelProperty(value: \"小程序链接\", index: 11)]\n    public string $short_link;\n\n    #[ExcelProperty(value: \"icon\", index: 12)]\n    public string $icon;\n\n    #[ExcelProperty(value: \"是否锁定:1正常,2锁定\", index: 13)]\n    public string $is_lock;\n\n    #[ExcelProperty(value: \"创建时间\", index: 14)]\n    public string $created_at;\n\n    #[ExcelProperty(value: \"更新时间\", index: 15)]\n    public string $updated_at;\n\n    #[ExcelProperty(value: \"删除时间\", index: 16)]\n    public string $deleted_at;\n\n\n}"
  },
  {
    "path": "MineAdmin/php/app/Ai/Dto/AiMineMenuGroupDto.php",
    "content": "<?php\nnamespace App\\Ai\\Dto;\n\nuse Mine\\Interfaces\\MineModelExcel;\nuse Mine\\Annotation\\ExcelData;\nuse Mine\\Annotation\\ExcelProperty;\n\n/**\n * 个人中心菜单分组Dto （导入导出）\n */\n#[ExcelData]\nclass AiMineMenuGroupDto implements MineModelExcel\n{\n    #[ExcelProperty(value: \"主键\", index: 0)]\n    public string $id;\n\n    #[ExcelProperty(value: \"分组名称\", index: 1)]\n    public string $name;\n\n    #[ExcelProperty(value: \"排序\", index: 2)]\n    public string $sort;\n\n    #[ExcelProperty(value: \"是否锁定:1正常,锁定\", index: 3)]\n    public string $is_lock;\n\n    #[ExcelProperty(value: \"创建时间\", index: 4)]\n    public string $created_at;\n\n    #[ExcelProperty(value: \"更新时间\", index: 5)]\n    public string $updated_at;\n\n\n}"
  },
  {
    "path": "MineAdmin/php/app/Ai/Dto/AiOpenaiKeyDto.php",
    "content": "<?php\nnamespace App\\Ai\\Dto;\n\nuse Mine\\Interfaces\\MineModelExcel;\nuse Mine\\Annotation\\ExcelData;\nuse Mine\\Annotation\\ExcelProperty;\n\n/**\n * openai_keyDto （导入导出）\n */\n#[ExcelData]\nclass AiOpenaiKeyDto implements MineModelExcel\n{\n    #[ExcelProperty(value: \"openai_key\", index: 0)]\n    public string $openai_key;\n\n    #[ExcelProperty(value: \"备注\", index: 1)]\n    public string $remark;\n\n    #[ExcelProperty(value: \"主键\", index: 2)]\n    public string $id;\n\n    #[ExcelProperty(value: \"创建者\", index: 3)]\n    public string $created_by;\n\n    #[ExcelProperty(value: \"更新者\", index: 4)]\n    public string $updated_by;\n\n    #[ExcelProperty(value: \"创建时间\", index: 5)]\n    public string $created_at;\n\n    #[ExcelProperty(value: \"更新时间\", index: 6)]\n    public string $updated_at;\n\n    #[ExcelProperty(value: \"删除时间\", index: 7)]\n    public string $deleted_at;\n\n\n}"
  },
  {
    "path": "MineAdmin/php/app/Ai/Dto/AiOrderDto.php",
    "content": "<?php\nnamespace App\\Ai\\Dto;\n\nuse Mine\\Interfaces\\MineModelExcel;\nuse Mine\\Annotation\\ExcelData;\nuse Mine\\Annotation\\ExcelProperty;\n\n/**\n * 订单表Dto （导入导出）\n */\n#[ExcelData]\nclass AiOrderDto implements MineModelExcel\n{\n    #[ExcelProperty(value: \"主键\", index: 0)]\n    public string $id;\n\n    #[ExcelProperty(value: \"用户UID\", index: 1)]\n    public string $uid;\n\n    #[ExcelProperty(value: \"上级UID\", index: 2)]\n    public string $from_uid;\n\n    #[ExcelProperty(value: \"营销部UID\", index: 3)]\n    public string $market_uid;\n\n    #[ExcelProperty(value: \"订单号\", index: 4)]\n    public string $ord_sn;\n\n    #[ExcelProperty(value: \"订单类型: 1开通VIP 2提现 3成为营销部\", index: 5)]\n    public string $ord_type;\n\n    #[ExcelProperty(value: \"支付方式: 1微信 2后台付费\", index: 6)]\n    public string $pay_type;\n\n    #[ExcelProperty(value: \"订单状态 1未支付(待处理) 2已支付(已完成) 3失败\", index: 7)]\n    public string $status;\n\n    #[ExcelProperty(value: \"总金额\", index: 8)]\n    public string $total_price;\n\n    #[ExcelProperty(value: \"实际金额\", index: 9)]\n    public string $amount_price;\n\n    #[ExcelProperty(value: \"订单描述\", index: 10)]\n    public string $content;\n\n    #[ExcelProperty(value: \"备注\", index: 11)]\n    public string $remark;\n\n    #[ExcelProperty(value: \"创建者\", index: 12)]\n    public string $created_by;\n\n    #[ExcelProperty(value: \"更新者\", index: 13)]\n    public string $updated_by;\n\n    #[ExcelProperty(value: \"创建时间\", index: 14)]\n    public string $created_at;\n\n    #[ExcelProperty(value: \"订单完成时间\", index: 15)]\n    public string $pay_at;\n\n    #[ExcelProperty(value: \"更新时间\", index: 16)]\n    public string $updated_at;\n\n    #[ExcelProperty(value: \"删除时间\", index: 17)]\n    public string $deleted_at;\n\n\n}"
  },
  {
    "path": "MineAdmin/php/app/Ai/Dto/AiPayKamiDto.php",
    "content": "<?php\nnamespace App\\Ai\\Dto;\n\nuse Mine\\Interfaces\\MineModelExcel;\nuse Mine\\Annotation\\ExcelData;\nuse Mine\\Annotation\\ExcelProperty;\n\n/**\n * 卡密Dto （导入导出）\n */\n#[ExcelData]\nclass AiPayKamiDto implements MineModelExcel\n{\n    #[ExcelProperty(value: \"主键\", index: 0)]\n    public string $id;\n\n    #[ExcelProperty(value: \"绑定用户\", index: 1)]\n    public string $uid;\n\n    #[ExcelProperty(value: \"价格\", index: 2)]\n    public string $price;\n\n    #[ExcelProperty(value: \"卡密号\", index: 3)]\n    public string $code;\n\n    #[ExcelProperty(value: \"状态 1未使用 2已使用\", index: 4)]\n    public string $status;\n\n    #[ExcelProperty(value: \"备注\", index: 5)]\n    public string $remark;\n\n    #[ExcelProperty(value: \"创建者\", index: 6)]\n    public string $created_by;\n\n    #[ExcelProperty(value: \"更新者\", index: 7)]\n    public string $updated_by;\n\n    #[ExcelProperty(value: \"创建时间\", index: 8)]\n    public string $created_at;\n\n    #[ExcelProperty(value: \"使用时间\", index: 9)]\n    public string $use_at;\n\n    #[ExcelProperty(value: \"更新时间\", index: 10)]\n    public string $updated_at;\n\n    #[ExcelProperty(value: \"删除时间\", index: 11)]\n    public string $deleted_at;\n\n\n}"
  },
  {
    "path": "MineAdmin/php/app/Ai/Dto/AiQuickIssueDto.php",
    "content": "<?php\nnamespace App\\Ai\\Dto;\n\nuse Mine\\Interfaces\\MineModelExcel;\nuse Mine\\Annotation\\ExcelData;\nuse Mine\\Annotation\\ExcelProperty;\n\n/**\n * 快捷问题\nDto （导入导出）\n */\n#[ExcelData]\nclass AiQuickIssueDto implements MineModelExcel\n{\n    #[ExcelProperty(value: \"主键\", index: 0)]\n    public string $id;\n\n    #[ExcelProperty(value: \"问题标题\", index: 1)]\n    public string $title;\n\n    #[ExcelProperty(value: \"问题描述\", index: 2)]\n    public string $content;\n\n    #[ExcelProperty(value: \"sort\", index: 3)]\n    public string $sort;\n\n    #[ExcelProperty(value: \"创建时间\", index: 4)]\n    public string $created_at;\n\n    #[ExcelProperty(value: \"更新时间\", index: 5)]\n    public string $updated_at;\n\n    #[ExcelProperty(value: \"删除时间\", index: 6)]\n    public string $deleted_at;\n\n\n}"
  },
  {
    "path": "MineAdmin/php/app/Ai/Dto/AiUserDto.php",
    "content": "<?php\nnamespace App\\Ai\\Dto;\n\nuse Mine\\Interfaces\\MineModelExcel;\nuse Mine\\Annotation\\ExcelData;\nuse Mine\\Annotation\\ExcelProperty;\n\n/**\n * 用户主表Dto （导入导出）\n */\n#[ExcelData]\nclass AiUserDto implements MineModelExcel\n{\n    #[ExcelProperty(value: \"主键\", index: 0)]\n    public string $id;\n\n    #[ExcelProperty(value: \"昵称\", index: 1)]\n    public string $nick_name;\n\n    #[ExcelProperty(value: \"头像\", index: 2)]\n    public string $head_img;\n\n    #[ExcelProperty(value: \"手机号\", index: 3)]\n    public string $mobile;\n\n    #[ExcelProperty(value: \"vip等级\", index: 4)]\n    public string $vip;\n\n    #[ExcelProperty(value: \"vip到期时间\", index: 5)]\n    public string $vip_ent_at;\n\n    #[ExcelProperty(value: \"password\", index: 6)]\n    public string $password;\n\n    #[ExcelProperty(value: \"是否锁定:1正常,2锁定\", index: 7)]\n    public string $is_lock;\n\n    #[ExcelProperty(value: \"更新者\", index: 8)]\n    public string $updated_by;\n\n    #[ExcelProperty(value: \"创建时间\", index: 9)]\n    public string $created_at;\n\n    #[ExcelProperty(value: \"更新时间\", index: 10)]\n    public string $updated_at;\n\n    #[ExcelProperty(value: \"删除时间\", index: 11)]\n    public string $deleted_at;\n\n\n}"
  },
  {
    "path": "MineAdmin/php/app/Ai/Factory/AiRedisFactory.php",
    "content": "<?php\ndeclare(strict_types=1);\n\nnamespace App\\Ai\\Factory;\n\nuse Hyperf\\Redis\\Redis;\n\nclass AiRedisFactory extends Redis\n{\n    protected string $poolName = 'default';\n}"
  },
  {
    "path": "MineAdmin/php/app/Ai/Mapper/AiChatMessageMapper.php",
    "content": "<?php\ndeclare(strict_types=1);\n\n\nnamespace App\\Ai\\Mapper;\n\nuse App\\Ai\\Model\\AiChatMessage;\nuse Hyperf\\Database\\Model\\Builder;\nuse Mine\\Abstracts\\AbstractMapper;\n\n/**\n * 聊天数据Mapper类\n */\nclass AiChatMessageMapper extends AbstractMapper\n{\n    /**\n     * @var AiChatMessage\n     */\n    public $model;\n\n    public function assignModel()\n    {\n        $this->model = AiChatMessage::class;\n    }\n\n    /**\n     * 搜索处理器\n     * @param Builder $query\n     * @param array $params\n     * @return Builder\n     */\n    public function handleSearch(Builder $query, array $params): Builder\n    {\n        \n        // 会话ID\n        if (isset($params['sid']) && $params['sid'] !== '') {\n            $query->where('sid', '=', $params['sid']);\n        }\n\n        // 内容\n        if (isset($params['content']) && $params['content'] !== '') {\n            $query->where('content', 'like', \"%{$params['content']}%\");\n        }\n\n        // 内容\n        if (isset($params['reply_content']) && $params['reply_content'] !== '') {\n            $query->where('reply_content', 'like', \"%{$params['reply_content']}%\");\n        }\n\n        // 创建时间\n        if (isset($params['created_at']) && is_array($params['created_at']) && count($params['created_at']) == 2) {\n            $query->whereBetween(\n                'created_at',\n                [ $params['created_at'][0], $params['created_at'][1] ]\n            );\n        }\n\n        return $query;\n    }\n}"
  },
  {
    "path": "MineAdmin/php/app/Ai/Mapper/AiChatSessionMapper.php",
    "content": "<?php\ndeclare(strict_types=1);\n\n\nnamespace App\\Ai\\Mapper;\n\nuse App\\Ai\\Model\\AiChatSession;\nuse Hyperf\\Database\\Model\\Builder;\nuse Hyperf\\Database\\Model\\Model;\nuse Mine\\Abstracts\\AbstractMapper;\n\n/**\n * 问答会话Mapper类\n */\nclass AiChatSessionMapper extends AbstractMapper\n{\n    /**\n     * @var AiChatSession\n     */\n    public $model;\n\n    public function assignModel()\n    {\n        $this->model = AiChatSession::class;\n    }\n\n    /**\n     * 搜索处理器\n     * @param Builder $query\n     * @param array $params\n     * @return Builder\n     */\n    public function handleSearch(Builder $query, array $params): Builder\n    {\n        \n        // 主键\n        if (isset($params['id']) && $params['id'] !== '') {\n            $query->where('id', '=', $params['id']);\n        }\n\n        // 用户uid\n        if (isset($params['uid']) && $params['uid'] !== '') {\n            $query->where('uid', '=', $params['uid']);\n        }\n\n        // 模型ID\n        if (isset($params['prompt_id']) && $params['prompt_id'] !== '') {\n            $query->where('prompt_id', '=', $params['prompt_id']);\n        }\n\n        // 是否关闭:1正常,2关闭\n        if (isset($params['close']) && $params['close'] !== '') {\n            $query->where('close', '=', $params['close']);\n        }\n\n        // 是否分享:1关闭,2公开\n        if (isset($params['share']) && $params['share'] !== '') {\n            $query->where('share', '=', $params['share']);\n        }\n\n        return $query;\n    }\n\n    public function session($where): Model|Builder|null\n    {\n        return $this->model::where($where)->orderBy('id', 'desc')->first();\n    }\n}"
  },
  {
    "path": "MineAdmin/php/app/Ai/Mapper/AiChatgptPromptsMapper.php",
    "content": "<?php\ndeclare(strict_types=1);\n\n\nnamespace App\\Ai\\Mapper;\n\nuse App\\Ai\\Model\\AiChatgptPrompts;\nuse Hyperf\\Database\\Model\\Builder;\nuse Mine\\Abstracts\\AbstractMapper;\n\n/**\n * chatgpt角色Mapper类\n */\nclass AiChatgptPromptsMapper extends AbstractMapper\n{\n    /**\n     * @var AiChatgptPrompts\n     */\n    public $model;\n\n    public function assignModel()\n    {\n        $this->model = AiChatgptPrompts::class;\n    }\n\n    /**\n     * 搜索处理器\n     * @param Builder $query\n     * @param array $params\n     * @return Builder\n     */\n    public function handleSearch(Builder $query, array $params): Builder\n    {\n        \n        // 角色名称\n        if (isset($params['act']) && $params['act'] !== '') {\n            $query->where('act', 'like', '%'.$params['act'].'%');\n        }\n\n        // 角色说明\n        if (isset($params['prompt']) && $params['prompt'] !== '') {\n            $query->where('prompt', 'like', '%'.$params['prompt'].'%');\n        }\n\n        return $query;\n    }\n}"
  },
  {
    "path": "MineAdmin/php/app/Ai/Mapper/AiImageMaterialMapper.php",
    "content": "<?php\ndeclare(strict_types=1);\n\n\nnamespace App\\Ai\\Mapper;\n\nuse App\\Ai\\Model\\AiImageMaterial;\nuse Hyperf\\Database\\Model\\Builder;\nuse Mine\\Abstracts\\AbstractMapper;\n\n/**\n * 图片素材Mapper类\n */\nclass AiImageMaterialMapper extends AbstractMapper\n{\n    /**\n     * @var AiImageMaterial\n     */\n    public $model;\n\n    public function assignModel()\n    {\n        $this->model = AiImageMaterial::class;\n    }\n\n    /**\n     * 搜索处理器\n     * @param Builder $query\n     * @param array $params\n     * @return Builder\n     */\n    public function handleSearch(Builder $query, array $params): Builder\n    {\n        \n        // 使用场景\n        if (isset($params['scene']) && $params['scene'] !== '') {\n            $query->where('scene', '=', $params['scene']);\n        }\n\n        // 备注\n        if (isset($params['remark']) && $params['remark'] !== '') {\n            $query->where('remark', 'like', '%'.$params['remark'].'%');\n        }\n\n        // 使用开始时间\n        if (isset($params['start_at']) && is_array($params['start_at']) && count($params['start_at']) == 2) {\n            $query->whereBetween(\n                'start_at',\n                [ $params['start_at'][0], $params['start_at'][1] ]\n            );\n        }\n\n        // 使用结束时间\n        if (isset($params['end_at']) && is_array($params['end_at']) && count($params['end_at']) == 2) {\n            $query->whereBetween(\n                'end_at',\n                [ $params['end_at'][0], $params['end_at'][1] ]\n            );\n        }\n\n        // 创建时间\n        if (isset($params['created_at']) && is_array($params['created_at']) && count($params['created_at']) == 2) {\n            $query->whereBetween(\n                'created_at',\n                [ $params['created_at'][0], $params['created_at'][1] ]\n            );\n        }\n\n        return $query;\n    }\n}"
  },
  {
    "path": "MineAdmin/php/app/Ai/Mapper/AiMineMenuGroupMapper.php",
    "content": "<?php\ndeclare(strict_types=1);\n\n\nnamespace App\\Ai\\Mapper;\n\nuse App\\Ai\\Model\\AiMineMenuGroup;\nuse Hyperf\\Database\\Model\\Builder;\nuse Mine\\Abstracts\\AbstractMapper;\n\n/**\n * 个人中心菜单分组Mapper类\n */\nclass AiMineMenuGroupMapper extends AbstractMapper\n{\n    /**\n     * @var AiMineMenuGroup\n     */\n    public $model;\n\n    public function assignModel()\n    {\n        $this->model = AiMineMenuGroup::class;\n    }\n\n    /**\n     * 搜索处理器\n     * @param Builder $query\n     * @param array $params\n     * @return Builder\n     */\n    public function handleSearch(Builder $query, array $params): Builder\n    {\n        \n        // 分组名称\n        if (isset($params['name']) && $params['name'] !== '') {\n            $query->where('name', 'like', '%'.$params['name'].'%');\n        }\n\n        // 排序\n        if (isset($params['sort']) && $params['sort'] !== '') {\n            $query->where('sort', '=', $params['sort']);\n        }\n\n        // 是否锁定:1正常,锁定\n        if (isset($params['is_lock']) && $params['is_lock'] !== '') {\n            $query->where('is_lock', '=', $params['is_lock']);\n        }\n\n        // 创建时间\n        if (isset($params['created_at']) && is_array($params['created_at']) && count($params['created_at']) == 2) {\n            $query->whereBetween(\n                'created_at',\n                [ $params['created_at'][0], $params['created_at'][1] ]\n            );\n        }\n\n        // 更新时间\n        if (isset($params['updated_at']) && is_array($params['updated_at']) && count($params['updated_at']) == 2) {\n            $query->whereBetween(\n                'updated_at',\n                [ $params['updated_at'][0], $params['updated_at'][1] ]\n            );\n        }\n\n        return $query;\n    }\n}"
  },
  {
    "path": "MineAdmin/php/app/Ai/Mapper/AiMineMenuMapper.php",
    "content": "<?php\ndeclare(strict_types=1);\n\n\nnamespace App\\Ai\\Mapper;\n\nuse App\\Ai\\Model\\AiMineMenu;\nuse Hyperf\\Database\\Model\\Builder;\nuse Mine\\Abstracts\\AbstractMapper;\n\n/**\n * 个人中心菜单Mapper类\n */\nclass AiMineMenuMapper extends AbstractMapper\n{\n    /**\n     * @var AiMineMenu\n     */\n    public $model;\n\n    public function assignModel()\n    {\n        $this->model = AiMineMenu::class;\n    }\n\n    /**\n     * 搜索处理器\n     * @param Builder $query\n     * @param array $params\n     * @return Builder\n     */\n    public function handleSearch(Builder $query, array $params): Builder\n    {\n        \n        // 分组名称\n        if (isset($params['name']) && $params['name'] !== '') {\n            $query->where('name', 'like', '%'.$params['name'].'%');\n        }\n\n        // 是否锁定:1正常,2锁定\n        if (isset($params['is_lock']) && $params['is_lock'] !== '') {\n            $query->where('is_lock', '=', $params['is_lock']);\n        }\n\n        // 创建时间\n        if (isset($params['created_at']) && is_array($params['created_at']) && count($params['created_at']) == 2) {\n            $query->whereBetween(\n                'created_at',\n                [ $params['created_at'][0], $params['created_at'][1] ]\n            );\n        }\n\n        // 更新时间\n        if (isset($params['updated_at']) && is_array($params['updated_at']) && count($params['updated_at']) == 2) {\n            $query->whereBetween(\n                'updated_at',\n                [ $params['updated_at'][0], $params['updated_at'][1] ]\n            );\n        }\n\n        return $query;\n    }\n}"
  },
  {
    "path": "MineAdmin/php/app/Ai/Mapper/AiOpenaiKeyMapper.php",
    "content": "<?php\ndeclare(strict_types=1);\n\n\nnamespace App\\Ai\\Mapper;\n\nuse App\\Ai\\Model\\AiOpenaiKey;\nuse Hyperf\\Database\\Model\\Builder;\nuse Mine\\Abstracts\\AbstractMapper;\n\n/**\n * openai_keyMapper类\n */\nclass AiOpenaiKeyMapper extends AbstractMapper\n{\n    /**\n     * @var AiOpenaiKey\n     */\n    public $model;\n\n    public function assignModel()\n    {\n        $this->model = AiOpenaiKey::class;\n    }\n\n    /**\n     * 搜索处理器\n     * @param Builder $query\n     * @param array $params\n     * @return Builder\n     */\n    public function handleSearch(Builder $query, array $params): Builder\n    {\n        \n        // openai_key\n        if (isset($params['openai_key']) && $params['openai_key'] !== '') {\n            $query->where('openai_key', 'like', '%'.$params['openai_key'].'%');\n        }\n\n        // 备注\n        if (isset($params['remark']) && $params['remark'] !== '') {\n            $query->where('remark', 'like', '%'.$params['remark'].'%');\n        }\n\n        // 主键\n        if (isset($params['id']) && $params['id'] !== '') {\n            $query->where('id', '=', $params['id']);\n        }\n\n        // 创建时间\n        if (isset($params['created_at']) && is_array($params['created_at']) && count($params['created_at']) == 2) {\n            $query->whereBetween(\n                'created_at',\n                [ $params['created_at'][0], $params['created_at'][1] ]\n            );\n        }\n\n        // 更新时间\n        if (isset($params['updated_at']) && is_array($params['updated_at']) && count($params['updated_at']) == 2) {\n            $query->whereBetween(\n                'updated_at',\n                [ $params['updated_at'][0], $params['updated_at'][1] ]\n            );\n        }\n\n        return $query;\n    }\n}"
  },
  {
    "path": "MineAdmin/php/app/Ai/Mapper/AiOrderMapper.php",
    "content": "<?php\ndeclare(strict_types=1);\n\n\nnamespace App\\Ai\\Mapper;\n\nuse App\\Ai\\Model\\AiOrder;\nuse Hyperf\\Database\\Model\\Builder;\nuse Mine\\Abstracts\\AbstractMapper;\n\n/**\n * 订单表Mapper类\n */\nclass AiOrderMapper extends AbstractMapper\n{\n    /**\n     * @var AiOrder\n     */\n    public $model;\n\n    public function assignModel()\n    {\n        $this->model = AiOrder::class;\n    }\n\n    /**\n     * 搜索处理器\n     * @param Builder $query\n     * @param array $params\n     * @return Builder\n     */\n    public function handleSearch(Builder $query, array $params): Builder\n    {\n        \n        // 用户UID\n        if (isset($params['uid']) && $params['uid'] !== '') {\n            $query->where('uid', '=', $params['uid']);\n        }\n\n        // 订单号\n        if (isset($params['ord_sn']) && $params['ord_sn'] !== '') {\n            $query->where('ord_sn', 'like', '%'.$params['ord_sn'].'%');\n        }\n\n        // 订单类型: 1开通VIP 2提现 3成为营销部\n        if (isset($params['ord_type']) && $params['ord_type'] !== '') {\n            $query->where('ord_type', '=', $params['ord_type']);\n        }\n\n        // 支付方式: 1微信 2后台付费\n        if (isset($params['pay_type']) && $params['pay_type'] !== '') {\n            $query->where('pay_type', '=', $params['pay_type']);\n        }\n\n        // 订单状态 1未支付(待处理) 2已支付(已完成) 3失败\n        if (isset($params['status']) && $params['status'] !== '') {\n            $query->where('status', '=', $params['status']);\n        }\n\n        // 创建时间\n        if (isset($params['created_at']) && is_array($params['created_at']) && count($params['created_at']) == 2) {\n            $query->whereBetween(\n                'created_at',\n                [ $params['created_at'][0], $params['created_at'][1] ]\n            );\n        }\n\n        // 订单完成时间\n        if (isset($params['pay_at']) && is_array($params['pay_at']) && count($params['pay_at']) == 2) {\n            $query->whereBetween(\n                'pay_at',\n                [ $params['pay_at'][0], $params['pay_at'][1] ]\n            );\n        }\n\n        // 更新时间\n        if (isset($params['updated_at']) && is_array($params['updated_at']) && count($params['updated_at']) == 2) {\n            $query->whereBetween(\n                'updated_at',\n                [ $params['updated_at'][0], $params['updated_at'][1] ]\n            );\n        }\n\n        return $query;\n    }\n}"
  },
  {
    "path": "MineAdmin/php/app/Ai/Mapper/AiPayKamiMapper.php",
    "content": "<?php\ndeclare(strict_types=1);\n\n\nnamespace App\\Ai\\Mapper;\n\nuse App\\Ai\\Model\\AiPayKami;\nuse Hyperf\\Database\\Model\\Builder;\nuse Mine\\Abstracts\\AbstractMapper;\n\n/**\n * 卡密Mapper类\n */\nclass AiPayKamiMapper extends AbstractMapper\n{\n    /**\n     * @var AiPayKami\n     */\n    public $model;\n\n    public function assignModel()\n    {\n        $this->model = AiPayKami::class;\n    }\n\n    /**\n     * 搜索处理器\n     * @param Builder $query\n     * @param array $params\n     * @return Builder\n     */\n    public function handleSearch(Builder $query, array $params): Builder\n    {\n        \n        // 主键\n        if (isset($params['id']) && $params['id'] !== '') {\n            $query->where('id', '=', $params['id']);\n        }\n\n        // 绑定用户\n        if (isset($params['uid']) && $params['uid'] !== '') {\n            $query->where('uid', '=', $params['uid']);\n        }\n\n        // 卡密号\n        if (isset($params['code']) && $params['code'] !== '') {\n            $query->where('code', 'like', '%'.$params['code'].'%');\n        }\n\n        // 状态 1未使用 2已使用\n        if (isset($params['status']) && $params['status'] !== '') {\n            $query->where('status', '=', $params['status']);\n        }\n\n        // 备注\n        if (isset($params['remark']) && $params['remark'] !== '') {\n            $query->where('remark', 'like', '%'.$params['remark'].'%');\n        }\n\n        // 创建时间\n        if (isset($params['created_at']) && is_array($params['created_at']) && count($params['created_at']) == 2) {\n            $query->whereBetween(\n                'created_at',\n                [ $params['created_at'][0], $params['created_at'][1] ]\n            );\n        }\n\n        // 使用时间\n        if (isset($params['use_at']) && is_array($params['use_at']) && count($params['use_at']) == 2) {\n            $query->whereBetween(\n                'use_at',\n                [ $params['use_at'][0], $params['use_at'][1] ]\n            );\n        }\n\n        // 更新时间\n        if (isset($params['updated_at']) && is_array($params['updated_at']) && count($params['updated_at']) == 2) {\n            $query->whereBetween(\n                'updated_at',\n                [ $params['updated_at'][0], $params['updated_at'][1] ]\n            );\n        }\n\n        return $query;\n    }\n}"
  },
  {
    "path": "MineAdmin/php/app/Ai/Mapper/AiQuickIssueMapper.php",
    "content": "<?php\ndeclare(strict_types=1);\n\n\nnamespace App\\Ai\\Mapper;\n\nuse App\\Ai\\Model\\AiQuickIssue;\nuse Hyperf\\Database\\Model\\Builder;\nuse Mine\\Abstracts\\AbstractMapper;\n\n/**\n * 快捷问题\nMapper类\n */\nclass AiQuickIssueMapper extends AbstractMapper\n{\n    /**\n     * @var AiQuickIssue\n     */\n    public $model;\n\n    public function assignModel()\n    {\n        $this->model = AiQuickIssue::class;\n    }\n\n    /**\n     * 搜索处理器\n     * @param Builder $query\n     * @param array $params\n     * @return Builder\n     */\n    public function handleSearch(Builder $query, array $params): Builder\n    {\n        \n        // 问题标题\n        if (isset($params['title']) && $params['title'] !== '') {\n            $query->where('title', 'like', '%'.$params['title'].'%');\n        }\n\n        // 问题描述\n        if (isset($params['content']) && $params['content'] !== '') {\n            $query->where('content', 'like', '%'.$params['content'].'%');\n        }\n\n        // 创建时间\n        if (isset($params['created_at']) && is_array($params['created_at']) && count($params['created_at']) == 2) {\n            $query->whereBetween(\n                'created_at',\n                [ $params['created_at'][0], $params['created_at'][1] ]\n            );\n        }\n\n        return $query;\n    }\n}"
  },
  {
    "path": "MineAdmin/php/app/Ai/Mapper/AiUserMapper.php",
    "content": "<?php\ndeclare(strict_types=1);\n\n\nnamespace App\\Ai\\Mapper;\n\nuse App\\Ai\\Model\\AiUser;\nuse App\\Ai\\Model\\AiUserRelation;\nuse Hyperf\\Database\\Model\\Builder;\nuse Mine\\Abstracts\\AbstractMapper;\n\n/**\n * 用户主表Mapper类\n */\nclass AiUserMapper extends AbstractMapper\n{\n    /**\n     * @var AiUser\n     */\n    public $model;\n\n    public function assignModel()\n    {\n        $this->model = AiUser::class;\n    }\n\n    /**\n     * 搜索处理器\n     * @param Builder $query\n     * @param array $params\n     * @return Builder\n     */\n    public function handleSearch(Builder $query, array $params): Builder\n    {\n\n        // 昵称\n        if (isset($params['nick_name']) && $params['nick_name'] !== '') {\n            $query->where('nick_name', 'like', '%'.$params['nick_name'].'%');\n        }\n\n        // 手机号\n        if (isset($params['mobile']) && $params['mobile'] !== '') {\n            $query->where('mobile', 'like', '%'.$params['mobile'].'%');\n        }\n\n        // vip等级\n        if (isset($params['vip']) && $params['vip'] !== '') {\n            $query->where('vip', '=', $params['vip']);\n        }\n\n        // vip到期时间\n        if (isset($params['vip_ent_at']) && is_array($params['vip_ent_at']) && count($params['vip_ent_at']) == 2) {\n            $query->whereBetween(\n                'vip_ent_at',\n                [ $params['vip_ent_at'][0], $params['vip_ent_at'][1] ]\n            );\n        }\n\n        // 是否锁定:1正常,2锁定\n        if (isset($params['is_lock']) && $params['is_lock'] !== '') {\n            $query->where('is_lock', '=', $params['is_lock']);\n        }\n\n        // 创建时间\n        if (isset($params['created_at']) && is_array($params['created_at']) && count($params['created_at']) == 2) {\n            $query->whereBetween(\n                'created_at',\n                [ $params['created_at'][0], $params['created_at'][1] ]\n            );\n        }\n\n        // 更新时间\n        if (isset($params['updated_at']) && is_array($params['updated_at']) && count($params['updated_at']) == 2) {\n            $query->whereBetween(\n                'updated_at',\n                [ $params['updated_at'][0], $params['updated_at'][1] ]\n            );\n        }\n        return $query;\n    }\n}"
  },
  {
    "path": "MineAdmin/php/app/Ai/Middleware/AuthMiddleware.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace App\\Ai\\Middleware;\n\nuse App\\Ai\\Service\\AiLoginService;\nuse App\\Ai\\Api\\Login;\nuse App\\Ai\\Constants\\ResponseCodeConst;\nuse App\\Ai\\Service\\AiSettingService;\nuse App\\Ai\\Service\\AiUserService;\nuse Hyperf\\Context\\Context;\nuse Hyperf\\Contract\\StdoutLoggerInterface;\nuse Hyperf\\HttpServer\\Contract\\RequestInterface;\nuse Hyperf\\HttpServer\\Contract\\ResponseInterface as HttpResponse;\nuse Mine\\Exception\\NormalStatusException;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\ContainerInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Http\\Message\\ResponseInterface;\nuse Psr\\Http\\Message\\ServerRequestInterface;\nuse Psr\\Http\\Server\\MiddlewareInterface;\nuse Psr\\Http\\Server\\RequestHandlerInterface;\n\nclass AuthMiddleware implements MiddlewareInterface\n{\n    protected ContainerInterface $container;\n\n    protected RequestInterface $request;\n\n    protected HttpResponse $response;\n\n    protected StdoutLoggerInterface $logger;\n\n    protected AiLoginService $loginService;\n\n    protected AiSettingService $settingService;\n\n    protected AiUserService $userService;\n\n    public function __construct(ContainerInterface $container, HttpResponse $response, RequestInterface $request)\n    {\n        $this->container = $container;\n        $this->response = $response;\n        $this->request = $request;\n        $this->loginService = $container->get(AiLoginService::class);\n        $this->settingService = $container->get(AiSettingService::class);\n        $this->logger = $container->get(StdoutLoggerInterface::class);\n        $this->userService = $container->get(AiUserService::class);\n    }\n\n    /**\n     * @throws ContainerExceptionInterface\n     * @throws NotFoundExceptionInterface\n     */\n    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface\n    {\n        if (false === $this->loginService->check(null, 'ai')) {\n            throw new NormalStatusException('error', ResponseCodeConst::INVALID_TOKEN);\n        }\n\n        $data = $this->loginService->getInfo();\n        if (($data['version'] ?? '') != Login::VERSIONS) {\n            throw new NormalStatusException('error', ResponseCodeConst::CLEAR_ALL_CACHE);\n        }\n\n        // 检测系统是否关站\n        if ($closeMsg = $this->settingService->appClose()) {\n            throw new NormalStatusException($closeMsg, ResponseCodeConst::APP_CLOSE);\n        }\n\n        // 拦截刚删除的用户、锁定的账户\n        if ($this->userService->isJustDelete($data['id'])) {\n            throw new NormalStatusException('账号不存在', ResponseCodeConst::CLEAR_ALL_CACHE);\n        }\n\n        if ($this->userService->isJustLock($data['id'])) {\n            throw new NormalStatusException('账号被锁定', ResponseCodeConst::ACCOUNT_LOCK);\n        }\n\n        Context::set(self::class . '_login_data', $data);\n        return $handler->handle($request);\n    }\n}\n"
  },
  {
    "path": "MineAdmin/php/app/Ai/Model/AiChatMessage.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace App\\Ai\\Model;\n\nuse Hyperf\\Database\\Model\\SoftDeletes;\nuse Mine\\MineModel;\n\n/**\n * @property int $id 主键\n * @property int $sid 会话ID\n * @property string $content 内容\n * @property string $reply_content 内容\n * @property string $reply_at 回复时间\n * @property string $created_at 创建时间\n * @property string $deleted_at 删除时间\n */\nclass AiChatMessage extends MineModel\n{\n    use SoftDeletes;\n    public bool $timestamps = false;\n    /**\n     * The table associated with the model.\n     */\n    protected ?string $table = 'ai_chat_message';\n\n    /**\n     * The attributes that are mass assignable.\n     */\n    protected array $fillable = ['id', 'sid', 'content', 'reply_content', 'reply_at', 'created_at', 'deleted_at'];\n\n    /**\n     * The attributes that should be cast to native types.\n     */\n    protected array $casts = ['id' => 'integer', 'sid' => 'integer'];\n}\n"
  },
  {
    "path": "MineAdmin/php/app/Ai/Model/AiChatSession.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace App\\Ai\\Model;\n\nuse Hyperf\\Database\\Model\\Relations\\HasOne;\nuse Mine\\MineModel;\n\n/**\n * @property int $id 主键\n * @property int $uid 用户uid\n * @property int $prompt_id 模型ID\n * @property int $close 是否关闭:1正常,2关闭\n * @property int $share 是否分享:1关闭,2公开\n * @property string $created_at 创建时间\n * @property-read AiChatgptPrompts $prompt\n * @property-read AiChatMessage $firstMessage \n * @property-read AiUser $user \n */\nclass AiChatSession extends MineModel\n{\n    public bool $timestamps = false;\n    /**\n     * The table associated with the model.\n     */\n    protected ?string $table = 'ai_chat_session';\n\n    /**\n     * The attributes that are mass assignable.\n     */\n    protected array $fillable = ['id', 'uid', 'prompt_id', 'close', 'share', 'created_at'];\n\n    /**\n     * The attributes that should be cast to native types.\n     */\n    protected array $casts = ['id' => 'integer', 'uid' => 'integer', 'prompt_id' => 'integer', 'close' => 'integer', 'share' => 'integer'];\n\n    public function prompt(): HasOne\n    {\n        return $this->hasOne(AiChatgptPrompts::class, 'id', 'prompt_id');\n    }\n\n    public function firstMessage(): HasOne\n    {\n        return $this->hasOne(AiChatMessage::class, 'sid', 'id');\n    }\n\n    public function user(): HasOne\n    {\n        return $this->hasOne(AiUser::class, 'id', 'uid');\n    }\n}\n"
  },
  {
    "path": "MineAdmin/php/app/Ai/Model/AiChatgptPrompts.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace App\\Ai\\Model;\n\nuse Hyperf\\Database\\Model\\SoftDeletes;\nuse Mine\\MineModel;\n\n/**\n * @property int $id 主键\n * @property string $act 角色名称\n * @property string $prompt 角色说明\n * @property int $sort 排序\n * @property int $created_by 创建者\n * @property int $updated_by 更新者\n * @property \\Carbon\\Carbon $created_at 创建时间\n * @property \\Carbon\\Carbon $updated_at 更新时间\n * @property string $deleted_at 删除时间\n * @property string $remark 备注\n */\nclass AiChatgptPrompts extends MineModel\n{\n    use SoftDeletes;\n    /**\n     * The table associated with the model.\n     */\n    protected ?string $table = 'ai_chatgpt_prompts';\n\n    /**\n     * The attributes that are mass assignable.\n     */\n    protected array $fillable = ['id', 'act', 'prompt', 'sort', 'created_by', 'updated_by', 'created_at', 'updated_at', 'deleted_at', 'remark'];\n\n    /**\n     * The attributes that should be cast to native types.\n     */\n    protected array $casts = ['id' => 'integer', 'sort' => 'integer', 'created_by' => 'integer', 'updated_by' => 'integer', 'created_at' => 'datetime', 'updated_at' => 'datetime'];\n}\n"
  },
  {
    "path": "MineAdmin/php/app/Ai/Model/AiImageMaterial.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace App\\Ai\\Model;\n\nuse App\\Ai\\Service\\HelperService;\nuse Hyperf\\Database\\Model\\SoftDeletes;\nuse Mine\\MineModel;\n\n/**\n * @property int $id 主键\n * @property int $scene 使用场景\n * @property string $img_url 图片地址\n * @property string $url 跳转地址\n * @property string $remark 备注\n * @property int $sort 排序\n * @property int $created_by 创建者\n * @property int $updated_by 更新者\n * @property string $start_at 使用开始时间\n * @property string $end_at 使用结束时间\n * @property \\Carbon\\Carbon $created_at 创建时间\n * @property \\Carbon\\Carbon $updated_at 更新时间\n * @property string $deleted_at 删除时间\n */\nclass AiImageMaterial extends MineModel\n{\n    use SoftDeletes;\n    /**\n     * The table associated with the model.\n     */\n    protected ?string $table = 'ai_image_material';\n\n    /**\n     * The attributes that are mass assignable.\n     */\n    protected array $fillable = ['id', 'scene', 'img_url', 'url', 'remark', 'sort', 'created_by', 'updated_by', 'start_at', 'end_at', 'created_at', 'updated_at', 'deleted_at'];\n\n    /**\n     * The attributes that should be cast to native types.\n     */\n    protected array $casts = ['id' => 'integer', 'scene' => 'integer', 'sort' => 'integer', 'created_by' => 'integer', 'updated_by' => 'integer', 'created_at' => 'datetime', 'updated_at' => 'datetime'];\n\n    public function setImgUrlAttribute($value)\n    {\n        $this->attributes['img_url'] = HelperService::buildSavePath($value);\n    }\n\n    public function getImgUrlAttribute($value)\n    {\n        return HelperService::buildSourceUrl($value);\n    }\n}\n"
  },
  {
    "path": "MineAdmin/php/app/Ai/Model/AiMineMenu.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace App\\Ai\\Model;\n\nuse App\\Ai\\Service\\HelperService;\nuse Hyperf\\Database\\Model\\SoftDeletes;\nuse Mine\\MineModel;\n\n/**\n * @property int $id 主键\n * @property int $gid 分组ID\n * @property string $name 分组名称\n * @property int $sort 排序\n * @property int $use_vip 使用权限限制 0全部\n * @property int $click_type 点击类型 1跳转 2调用函数\n * @property string $click_func 函数标识 小程序端提前封装\n * @property string $path 打开的页面路径\n * @property string $app_id 小程序appid\n * @property string $extra_data 需要传递给目标小程序的数据 json\n * @property string $env_version 要打开的小程序版本\n * @property string $short_link 小程序链接\n * @property int $is_lock 是否锁定:1正常,2锁定\n * @property \\Carbon\\Carbon $created_at 创建时间\n * @property \\Carbon\\Carbon $updated_at 更新时间\n * @property string $deleted_at 删除时间\n * @property mixed $icon \n */\nclass AiMineMenu extends MineModel\n{\n    use SoftDeletes;\n    /**\n     * The table associated with the model.\n     */\n    protected ?string $table = 'ai_mine_menu';\n\n    /**\n     * The attributes that are mass assignable.\n     */\n    protected array $fillable = ['id', 'gid', 'name', 'sort', 'use_vip', 'click_type', 'click_func', 'path', 'app_id', 'extra_data', 'env_version', 'short_link', 'icon', 'is_lock', 'created_at', 'updated_at', 'deleted_at'];\n\n    /**\n     * The attributes that should be cast to native types.\n     */\n    protected array $casts = ['id' => 'integer', 'gid' => 'integer', 'sort' => 'integer', 'use_vip' => 'integer', 'click_type' => 'integer', 'is_lock' => 'integer', 'created_at' => 'datetime', 'updated_at' => 'datetime'];\n\n    public function setIconAttribute($value)\n    {\n        $this->attributes['icon'] = HelperService::buildSavePath($value);\n    }\n\n    public function getIconAttribute($value)\n    {\n        return HelperService::buildSourceUrl($value);\n    }\n}\n"
  },
  {
    "path": "MineAdmin/php/app/Ai/Model/AiMineMenuGroup.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace App\\Ai\\Model;\n\nuse \\App\\Ai\\Model\\AiMineMenu;\nuse Hyperf\\Database\\Model\\Relations\\HasMany;\nuse Mine\\MineModel;\n\n/**\n * @property int $id 主键\n * @property string $name 分组名称\n * @property int $sort 排序\n * @property int $is_lock 是否锁定:1正常,锁定\n * @property \\Carbon\\Carbon $created_at 创建时间\n * @property \\Carbon\\Carbon $updated_at 更新时间\n * @property-read \\Hyperf\\Database\\Model\\Collection|AiMineMenu[] $menus \n */\nclass AiMineMenuGroup extends MineModel\n{\n    /**\n     * The table associated with the model.\n     */\n    protected ?string $table = 'ai_mine_menu_group';\n\n    /**\n     * The attributes that are mass assignable.\n     */\n    protected array $fillable = ['id', 'name', 'sort', 'is_lock', 'created_at', 'updated_at'];\n\n    /**\n     * The attributes that should be cast to native types.\n     */\n    protected array $casts = ['id' => 'integer', 'sort' => 'integer', 'is_lock' => 'integer', 'created_at' => 'datetime', 'updated_at' => 'datetime'];\n\n\n    public function menus(): HasMany\n    {\n        return $this->hasMany(AiMineMenu::class, 'gid', 'id');\n    }\n}\n"
  },
  {
    "path": "MineAdmin/php/app/Ai/Model/AiOpenaiKey.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace App\\Ai\\Model;\n\nuse Hyperf\\Database\\Model\\SoftDeletes;\nuse Mine\\MineModel;\n\n/**\n * @property string $openai_key openai_key\n * @property string $remark 备注\n * @property int $id 主键\n * @property int $created_by 创建者\n * @property int $updated_by 更新者\n * @property \\Carbon\\Carbon $created_at 创建时间\n * @property \\Carbon\\Carbon $updated_at 更新时间\n * @property string $deleted_at 删除时间\n */\nclass AiOpenaiKey extends MineModel\n{\n    use SoftDeletes;\n    /**\n     * The table associated with the model.\n     */\n    protected ?string $table = 'ai_openai_key';\n\n    /**\n     * The attributes that are mass assignable.\n     */\n    protected array $fillable = ['openai_key', 'remark', 'id', 'created_by', 'updated_by', 'created_at', 'updated_at', 'deleted_at'];\n\n    /**\n     * The attributes that should be cast to native types.\n     */\n    protected array $casts = ['id' => 'integer', 'created_by' => 'integer', 'updated_by' => 'integer', 'created_at' => 'datetime', 'updated_at' => 'datetime'];\n}\n"
  },
  {
    "path": "MineAdmin/php/app/Ai/Model/AiOrder.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace App\\Ai\\Model;\n\nuse Hyperf\\Database\\Model\\SoftDeletes;\nuse Mine\\MineModel;\n\n/**\n * @property int $id 主键\n * @property int $uid 用户UID\n * @property int $from_uid 上级UID\n * @property int $market_uid 营销部UID\n * @property string $ord_sn 订单号\n * @property int $ord_type 订单类型: 1开通VIP 2提现 3成为营销部\n * @property int $pay_type 支付方式: 1微信 2后台付费\n * @property int $status 订单状态 1未支付(待处理) 2已支付(已完成) 3失败\n * @property int $total_price 总金额\n * @property int $amount_price 实际金额\n * @property string $content 订单描述\n * @property string $remark 备注\n * @property int $created_by 创建者\n * @property int $updated_by 更新者\n * @property \\Carbon\\Carbon $created_at 创建时间\n * @property string $pay_at 订单完成时间\n * @property \\Carbon\\Carbon $updated_at 更新时间\n * @property string $deleted_at 删除时间\n */\nclass AiOrder extends MineModel\n{\n    use SoftDeletes;\n    /**\n     * The table associated with the model.\n     */\n    protected ?string $table = 'ai_order';\n\n    /**\n     * The attributes that are mass assignable.\n     */\n    protected array $fillable = ['id', 'uid', 'from_uid', 'market_uid', 'ord_sn', 'ord_type', 'pay_type', 'status', 'total_price', 'amount_price', 'content', 'remark', 'created_by', 'updated_by', 'created_at', 'pay_at', 'updated_at', 'deleted_at'];\n\n    /**\n     * The attributes that should be cast to native types.\n     */\n    protected array $casts = ['id' => 'integer', 'uid' => 'integer', 'from_uid' => 'integer', 'market_uid' => 'integer', 'ord_type' => 'integer', 'pay_type' => 'integer', 'status' => 'integer', 'total_price' => 'integer', 'amount_price' => 'integer', 'created_by' => 'integer', 'updated_by' => 'integer', 'created_at' => 'datetime', 'updated_at' => 'datetime'];\n}\n"
  },
  {
    "path": "MineAdmin/php/app/Ai/Model/AiOrderKami.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace App\\Ai\\Model;\n\nuse Mine\\MineModel;\n\n/**\n * @property int $oid 订单ID\n * @property int $kid 卡密ID\n */\nclass AiOrderKami extends MineModel\n{\n    public bool $incrementing = false;\n    protected string $primaryKey = 'oid';\n    public bool $timestamps = false;\n    /**\n     * The table associated with the model.\n     */\n    protected ?string $table = 'ai_order_kami';\n\n    /**\n     * The attributes that are mass assignable.\n     */\n    protected array $fillable = ['oid', 'kid'];\n\n    /**\n     * The attributes that should be cast to native types.\n     */\n    protected array $casts = ['oid' => 'integer', 'kid' => 'integer'];\n}\n"
  },
  {
    "path": "MineAdmin/php/app/Ai/Model/AiOrderVip.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace App\\Ai\\Model;\n\nuse Mine\\MineModel;\n\n/**\n * @property int $oid 订单ID\n * @property int $vip_level vip等级\n * @property string $wx_sn 微信订单号\n * @property string $mch_id 商户ID\n * @property \\Carbon\\Carbon $created_at 创建时间\n */\nclass AiOrderVip extends MineModel\n{\n    public bool $incrementing = false;\n    protected string $primaryKey = 'oid';\n    public bool $timestamps = false;\n    /**\n     * The table associated with the model.\n     */\n    protected ?string $table = 'ai_order_vip';\n\n    /**\n     * The attributes that are mass assignable.\n     */\n    protected array $fillable = ['oid', 'vip_level', 'wx_sn', 'mch_id', 'created_at'];\n\n    /**\n     * The attributes that should be cast to native types.\n     */\n    protected array $casts = ['oid' => 'integer', 'vip_level' => 'integer', 'created_at' => 'datetime'];\n}\n"
  },
  {
    "path": "MineAdmin/php/app/Ai/Model/AiPayKami.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace App\\Ai\\Model;\n\nuse Hyperf\\Database\\Model\\SoftDeletes;\nuse Mine\\MineModel;\n\n/**\n * @property int $id 主键\n * @property int $uid 绑定用户\n * @property int $price 价格\n * @property string $code 卡密号\n * @property int $status 状态 1未使用 2已使用\n * @property string $remark 备注\n * @property int $created_by 创建者\n * @property int $updated_by 更新者\n * @property \\Carbon\\Carbon $created_at 创建时间\n * @property string $use_at 使用时间\n * @property \\Carbon\\Carbon $updated_at 更新时间\n * @property string $deleted_at 删除时间\n */\nclass AiPayKami extends MineModel\n{\n    use SoftDeletes;\n    /**\n     * The table associated with the model.\n     */\n    protected ?string $table = 'ai_pay_kami';\n\n    /**\n     * The attributes that are mass assignable.\n     */\n    protected array $fillable = ['id', 'uid', 'price', 'code', 'status', 'remark', 'created_by', 'updated_by', 'created_at', 'use_at', 'updated_at', 'deleted_at'];\n\n    /**\n     * The attributes that should be cast to native types.\n     */\n    protected array $casts = ['id' => 'integer', 'uid' => 'integer', 'price' => 'integer', 'status' => 'integer', 'created_by' => 'integer', 'updated_by' => 'integer', 'created_at' => 'datetime', 'updated_at' => 'datetime'];\n}\n"
  },
  {
    "path": "MineAdmin/php/app/Ai/Model/AiQuickIssue.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace App\\Ai\\Model;\n\nuse Hyperf\\Database\\Model\\SoftDeletes;\nuse Mine\\MineModel;\n\n/**\n * @property int $id 主键\n * @property string $title 问题标题\n * @property string $content 问题描述\n * @property int $sort \n * @property \\Carbon\\Carbon $created_at 创建时间\n * @property \\Carbon\\Carbon $updated_at 更新时间\n * @property string $deleted_at 删除时间\n */\nclass AiQuickIssue extends MineModel\n{\n    use SoftDeletes;\n    /**\n     * The table associated with the model.\n     */\n    protected ?string $table = 'ai_quick_issue';\n\n    /**\n     * The attributes that are mass assignable.\n     */\n    protected array $fillable = ['id', 'title', 'content', 'sort', 'created_at', 'updated_at', 'deleted_at'];\n\n    /**\n     * The attributes that should be cast to native types.\n     */\n    protected array $casts = ['id' => 'integer', 'sort' => 'integer', 'created_at' => 'datetime', 'updated_at' => 'datetime'];\n}\n"
  },
  {
    "path": "MineAdmin/php/app/Ai/Model/AiUser.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace App\\Ai\\Model;\n\nuse App\\Ai\\Service\\HelperService;\nuse Hyperf\\Database\\Model\\Relations\\HasOne;\nuse Hyperf\\Database\\Model\\SoftDeletes;\nuse Mine\\MineModel;\n\n/**\n * @property int $id 主键\n * @property string $nick_name 昵称\n * @property string $mobile 手机号\n * @property int $vip vip等级\n * @property string $vip_ent_at vip到期时间\n * @property string $password \n * @property int $is_lock 是否锁定:1正常,2锁定\n * @property int $updated_by 更新者\n * @property \\Carbon\\Carbon $created_at 创建时间\n * @property \\Carbon\\Carbon $updated_at 更新时间\n * @property string $deleted_at 删除时间\n * @property-read AiUserRelation $parentRelation \n * @property-read AiUserWallet $wallet \n * @property mixed $head_img 头像\n */\nclass AiUser extends MineModel\n{\n    use SoftDeletes;\n    /**\n     * The table associated with the model.\n     */\n    protected ?string $table = 'ai_user';\n\n    /**\n     * The attributes that are mass assignable.\n     */\n    protected array $fillable = ['id', 'nick_name', 'head_img', 'mobile', 'vip', 'vip_ent_at', 'password', 'is_lock', 'updated_by', 'created_at', 'updated_at', 'deleted_at'];\n\n    /**\n     * The attributes that should be cast to native types.\n     */\n    protected array $casts = ['id' => 'integer', 'vip' => 'integer', 'is_lock' => 'integer', 'updated_by' => 'integer', 'created_at' => 'datetime', 'updated_at' => 'datetime'];\n\n    public function parentRelation(): HasOne\n    {\n        return $this->hasOne(AiUserRelation::class, 'uid', 'id');\n    }\n\n    public function wallet(): HasOne\n    {\n        return $this->hasOne(AiUserWallet::class, 'uid', 'id');\n    }\n\n    public function setHeadImgAttribute($value)\n    {\n        $this->attributes['head_img'] = HelperService::buildSavePath($value);\n    }\n\n    public function getHeadImgAttribute($value)\n    {\n        return HelperService::buildSourceUrl($value);\n    }\n}\n"
  },
  {
    "path": "MineAdmin/php/app/Ai/Model/AiUserRelation.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace App\\Ai\\Model;\n\nuse Mine\\MineModel;\n\n/**\n * @property int $uid 用户UID\n * @property int $from_uid 上级\n * @property int $market_uid 上级营销部\n * @property \\Carbon\\Carbon $created_at 创建时间\n * @property \\Carbon\\Carbon $updated_at 更新时间\n */\nclass AiUserRelation extends MineModel\n{\n    public bool $incrementing = false;\n    protected string $primaryKey = 'uid';\n    /**\n     * The table associated with the model.\n     */\n    protected ?string $table = 'ai_user_relation';\n\n    /**\n     * The attributes that are mass assignable.\n     */\n    protected array $fillable = ['uid', 'from_uid', 'market_uid', 'created_at', 'updated_at'];\n\n    /**\n     * The attributes that should be cast to native types.\n     */\n    protected array $casts = ['uid' => 'integer', 'from_uid' => 'integer', 'market_uid' => 'integer', 'created_at' => 'datetime', 'updated_at' => 'datetime'];\n\n\n}\n"
  },
  {
    "path": "MineAdmin/php/app/Ai/Model/AiUserWallet.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace App\\Ai\\Model;\n\nuse Mine\\MineModel;\n\n/**\n * @property int $uid 用户UID\n * @property int $balance 余额\n * @property int $balance_total 总收入\n */\nclass AiUserWallet extends MineModel\n{\n    public bool $incrementing = false;\n    protected string $primaryKey = 'uid';\n    public bool $timestamps = false;\n    /**\n     * The table associated with the model.\n     */\n    protected ?string $table = 'ai_user_wallet';\n\n    /**\n     * The attributes that are mass assignable.\n     */\n    protected array $fillable = ['uid', 'balance', 'balance_total'];\n\n    /**\n     * The attributes that should be cast to native types.\n     */\n    protected array $casts = ['uid' => 'integer'];\n}\n"
  },
  {
    "path": "MineAdmin/php/app/Ai/Model/AiUserWalletLog.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace App\\Ai\\Model;\n\nuse Hyperf\\Database\\Model\\SoftDeletes;\nuse Mine\\MineModel;\n\n/**\n * @property int $id 主键\n * @property int $uid 用户UID\n * @property int $oid 订单ID\n * @property int $direction 类型:1收入,2支出\n * @property int $balance 余额\n * @property int $scene 变动场景: 1推广获益\n * @property string $remark 备注\n * @property \\Carbon\\Carbon $created_at 创建时间\n * @property string $deleted_at 删除时间\n */\nclass AiUserWalletLog extends MineModel\n{\n    use SoftDeletes;\n    public bool $timestamps = false;\n    /**\n     * The table associated with the model.\n     */\n    protected ?string $table = 'ai_user_wallet_log';\n\n    /**\n     * The attributes that are mass assignable.\n     */\n    protected array $fillable = ['id', 'uid', 'oid', 'direction', 'balance', 'scene', 'remark', 'created_at', 'deleted_at'];\n\n    /**\n     * The attributes that should be cast to native types.\n     */\n    protected array $casts = ['id' => 'integer', 'uid' => 'integer', 'oid' => 'integer', 'direction' => 'integer', 'balance' => 'integer', 'scene' => 'integer', 'created_at' => 'datetime'];\n}\n"
  },
  {
    "path": "MineAdmin/php/app/Ai/Request/AiChatMessageRequest.php",
    "content": "<?php\ndeclare(strict_types=1);\n\nnamespace App\\Ai\\Request;\n\nuse Mine\\MineFormRequest;\n\n/**\n * 聊天数据验证数据类\n */\nclass AiChatMessageRequest extends MineFormRequest\n{\n    /**\n     * 公共规则\n     */\n    public function commonRules(): array\n    {\n        return [];\n    }\n\n    \n    /**\n     * 新增数据验证规则\n     * return array\n     */\n    public function saveRules(): array\n    {\n        return [\n\n        ];\n    }\n    /**\n     * 更新数据验证规则\n     * return array\n     */\n    public function updateRules(): array\n    {\n        return [\n\n        ];\n    }\n\n    \n    /**\n     * 字段映射名称\n     * return array\n     */\n    public function attributes(): array\n    {\n        return [\n            'id' => '主键',\n            'sid' => '会话ID',\n        ];\n    }\n\n}"
  },
  {
    "path": "MineAdmin/php/app/Ai/Request/AiChatSessionRequest.php",
    "content": "<?php\ndeclare(strict_types=1);\n\nnamespace App\\Ai\\Request;\n\nuse Mine\\MineFormRequest;\n\n/**\n * 问答会话验证数据类\n */\nclass AiChatSessionRequest extends MineFormRequest\n{\n    /**\n     * 公共规则\n     */\n    public function commonRules(): array\n    {\n        return [];\n    }\n\n    \n    /**\n     * 新增数据验证规则\n     * return array\n     */\n    public function saveRules(): array\n    {\n        return [\n\n        ];\n    }\n    /**\n     * 更新数据验证规则\n     * return array\n     */\n    public function updateRules(): array\n    {\n        return [\n\n        ];\n    }\n\n    \n    /**\n     * 字段映射名称\n     * return array\n     */\n    public function attributes(): array\n    {\n        return [\n\n        ];\n    }\n\n}"
  },
  {
    "path": "MineAdmin/php/app/Ai/Request/AiChatgptPromptsRequest.php",
    "content": "<?php\ndeclare(strict_types=1);\n\nnamespace App\\Ai\\Request;\n\nuse Mine\\MineFormRequest;\n\n/**\n * chatgpt角色验证数据类\n */\nclass AiChatgptPromptsRequest extends MineFormRequest\n{\n    /**\n     * 公共规则\n     */\n    public function commonRules(): array\n    {\n        return [];\n    }\n\n    \n    /**\n     * 新增数据验证规则\n     * return array\n     */\n    public function saveRules(): array\n    {\n        return [\n            //角色名称 验证\n            'act' => 'required',\n            //角色说明 验证\n            'prompt' => 'required',\n            //排序 验证\n            'sort' => 'required',\n\n        ];\n    }\n    /**\n     * 更新数据验证规则\n     * return array\n     */\n    public function updateRules(): array\n    {\n        return [\n            //角色名称 验证\n            'act' => 'required',\n            //角色说明 验证\n            'prompt' => 'required',\n            //排序 验证\n            'sort' => 'required',\n\n        ];\n    }\n\n    \n    /**\n     * 字段映射名称\n     * return array\n     */\n    public function attributes(): array\n    {\n        return [\n            'id' => '主键',\n            'act' => '角色名称',\n            'prompt' => '角色说明',\n            'sort' => '排序',\n\n        ];\n    }\n\n}"
  },
  {
    "path": "MineAdmin/php/app/Ai/Request/AiImageMaterialRequest.php",
    "content": "<?php\ndeclare(strict_types=1);\n\nnamespace App\\Ai\\Request;\n\nuse Mine\\MineFormRequest;\n\n/**\n * 图片素材验证数据类\n */\nclass AiImageMaterialRequest extends MineFormRequest\n{\n    /**\n     * 公共规则\n     */\n    public function commonRules(): array\n    {\n        return [];\n    }\n\n    \n    /**\n     * 新增数据验证规则\n     * return array\n     */\n    public function saveRules(): array\n    {\n        return [\n            //使用场景 验证\n            'scene' => 'required',\n            //图片地址 验证\n            'img_url' => 'required',\n            //跳转地址 验证\n            'url' => 'required',\n            //备注 验证\n            'remark' => 'required',\n            //排序 验证\n            'sort' => 'required',\n            //使用开始时间 验证\n            'start_at' => 'required',\n            //使用结束时间 验证\n            'end_at' => 'required',\n\n        ];\n    }\n    /**\n     * 更新数据验证规则\n     * return array\n     */\n    public function updateRules(): array\n    {\n        return [\n            //使用场景 验证\n            'scene' => 'required',\n            //图片地址 验证\n            'img_url' => 'required',\n            //跳转地址 验证\n            'url' => 'required',\n            //备注 验证\n            'remark' => 'required',\n            //排序 验证\n            'sort' => 'required',\n            //使用开始时间 验证\n            'start_at' => 'required',\n            //使用结束时间 验证\n            'end_at' => 'required',\n\n        ];\n    }\n\n    \n    /**\n     * 字段映射名称\n     * return array\n     */\n    public function attributes(): array\n    {\n        return [\n            'id' => '主键',\n            'scene' => '使用场景',\n            'img_url' => '图片地址',\n            'url' => '跳转地址',\n            'remark' => '备注',\n            'sort' => '排序',\n            'created_by' => '创建者',\n            'updated_by' => '更新者',\n            'start_at' => '使用开始时间',\n            'end_at' => '使用结束时间',\n\n        ];\n    }\n\n}"
  },
  {
    "path": "MineAdmin/php/app/Ai/Request/AiMineMenuGroupRequest.php",
    "content": "<?php\ndeclare(strict_types=1);\n\nnamespace App\\Ai\\Request;\n\nuse Mine\\MineFormRequest;\n\n/**\n * 个人中心菜单分组验证数据类\n */\nclass AiMineMenuGroupRequest extends MineFormRequest\n{\n    /**\n     * 公共规则\n     */\n    public function commonRules(): array\n    {\n        return [];\n    }\n\n    \n    /**\n     * 新增数据验证规则\n     * return array\n     */\n    public function saveRules(): array\n    {\n        return [\n\n        ];\n    }\n    /**\n     * 更新数据验证规则\n     * return array\n     */\n    public function updateRules(): array\n    {\n        return [\n\n        ];\n    }\n\n    \n    /**\n     * 字段映射名称\n     * return array\n     */\n    public function attributes(): array\n    {\n        return [\n            'id' => '主键',\n\n        ];\n    }\n\n}"
  },
  {
    "path": "MineAdmin/php/app/Ai/Request/AiMineMenuRequest.php",
    "content": "<?php\ndeclare(strict_types=1);\n\nnamespace App\\Ai\\Request;\n\nuse Mine\\MineFormRequest;\n\n/**\n * 个人中心菜单验证数据类\n */\nclass AiMineMenuRequest extends MineFormRequest\n{\n    /**\n     * 公共规则\n     */\n    public function commonRules(): array\n    {\n        return [];\n    }\n\n    \n    /**\n     * 新增数据验证规则\n     * return array\n     */\n    public function saveRules(): array\n    {\n        return [\n\n        ];\n    }\n    /**\n     * 更新数据验证规则\n     * return array\n     */\n    public function updateRules(): array\n    {\n        return [\n\n        ];\n    }\n\n    \n    /**\n     * 字段映射名称\n     * return array\n     */\n    public function attributes(): array\n    {\n        return [\n            'id' => '主键',\n\n        ];\n    }\n\n}"
  },
  {
    "path": "MineAdmin/php/app/Ai/Request/AiOpenaiKeyRequest.php",
    "content": "<?php\ndeclare(strict_types=1);\n\nnamespace App\\Ai\\Request;\n\nuse Mine\\MineFormRequest;\n\n/**\n * openai_key验证数据类\n */\nclass AiOpenaiKeyRequest extends MineFormRequest\n{\n    /**\n     * 公共规则\n     */\n    public function commonRules(): array\n    {\n        return [];\n    }\n\n    \n    /**\n     * 新增数据验证规则\n     * return array\n     */\n    public function saveRules(): array\n    {\n        return [\n\n        ];\n    }\n    /**\n     * 更新数据验证规则\n     * return array\n     */\n    public function updateRules(): array\n    {\n        return [\n\n        ];\n    }\n\n    \n    /**\n     * 字段映射名称\n     * return array\n     */\n    public function attributes(): array\n    {\n        return [\n            'id' => '主键',\n\n        ];\n    }\n\n}"
  },
  {
    "path": "MineAdmin/php/app/Ai/Request/AiOrderRequest.php",
    "content": "<?php\ndeclare(strict_types=1);\n\nnamespace App\\Ai\\Request;\n\nuse Mine\\MineFormRequest;\n\n/**\n * 订单表验证数据类\n */\nclass AiOrderRequest extends MineFormRequest\n{\n    /**\n     * 公共规则\n     */\n    public function commonRules(): array\n    {\n        return [];\n    }\n\n    \n    /**\n     * 新增数据验证规则\n     * return array\n     */\n    public function saveRules(): array\n    {\n        return [\n\n        ];\n    }\n    /**\n     * 更新数据验证规则\n     * return array\n     */\n    public function updateRules(): array\n    {\n        return [\n\n        ];\n    }\n\n    \n    /**\n     * 字段映射名称\n     * return array\n     */\n    public function attributes(): array\n    {\n        return [\n            'id' => '主键',\n\n        ];\n    }\n\n}"
  },
  {
    "path": "MineAdmin/php/app/Ai/Request/AiPayKamiRequest.php",
    "content": "<?php\ndeclare(strict_types=1);\n\nnamespace App\\Ai\\Request;\n\nuse Mine\\MineFormRequest;\n\n/**\n * 卡密验证数据类\n */\nclass AiPayKamiRequest extends MineFormRequest\n{\n    /**\n     * 公共规则\n     */\n    public function commonRules(): array\n    {\n        return [];\n    }\n\n    \n    /**\n     * 新增数据验证规则\n     * return array\n     */\n    public function saveRules(): array\n    {\n        return [\n\n        ];\n    }\n    /**\n     * 更新数据验证规则\n     * return array\n     */\n    public function updateRules(): array\n    {\n        return [\n\n        ];\n    }\n\n    \n    /**\n     * 字段映射名称\n     * return array\n     */\n    public function attributes(): array\n    {\n        return [\n            'id' => '主键',\n\n        ];\n    }\n\n}"
  },
  {
    "path": "MineAdmin/php/app/Ai/Request/AiQuickIssueRequest.php",
    "content": "<?php\ndeclare(strict_types=1);\n\nnamespace App\\Ai\\Request;\n\nuse Mine\\MineFormRequest;\n\n/**\n * 快捷问题\n验证数据类\n */\nclass AiQuickIssueRequest extends MineFormRequest\n{\n    /**\n     * 公共规则\n     */\n    public function commonRules(): array\n    {\n        return [];\n    }\n\n    \n    /**\n     * 新增数据验证规则\n     * return array\n     */\n    public function saveRules(): array\n    {\n        return [\n            //问题标题 验证\n            'title' => 'required',\n            //问题描述 验证\n            'content' => 'required',\n            // 验证\n            'sort' => 'required',\n\n        ];\n    }\n    /**\n     * 更新数据验证规则\n     * return array\n     */\n    public function updateRules(): array\n    {\n        return [\n            //问题标题 验证\n            'title' => 'required',\n            //问题描述 验证\n            'content' => 'required',\n            // 验证\n            'sort' => 'required',\n\n        ];\n    }\n\n    \n    /**\n     * 字段映射名称\n     * return array\n     */\n    public function attributes(): array\n    {\n        return [\n            'id' => '主键',\n            'title' => '问题标题',\n            'content' => '问题描述',\n            'sort' => '',\n\n        ];\n    }\n\n}"
  },
  {
    "path": "MineAdmin/php/app/Ai/Request/AiUserRequest.php",
    "content": "<?php\ndeclare(strict_types=1);\n\nnamespace App\\Ai\\Request;\n\nuse Mine\\MineFormRequest;\n\n/**\n * 用户主表验证数据类\n */\nclass AiUserRequest extends MineFormRequest\n{\n    /**\n     * 公共规则\n     */\n    public function commonRules(): array\n    {\n        return [];\n    }\n\n    \n    /**\n     * 新增数据验证规则\n     * return array\n     */\n    public function saveRules(): array\n    {\n        return [\n\n        ];\n    }\n    /**\n     * 更新数据验证规则\n     * return array\n     */\n    public function updateRules(): array\n    {\n        return [\n            //昵称 验证\n            'nick_name' => 'required',\n            //头像 验证\n            'head_img' => 'required',\n            //手机号 验证\n            'mobile' => 'required',\n            //vip等级 验证\n            'vip' => 'required',\n\n        ];\n    }\n\n    \n    /**\n     * 字段映射名称\n     * return array\n     */\n    public function attributes(): array\n    {\n        return [\n            'id' => '主键',\n            'nick_name' => '昵称',\n            'head_img' => '头像',\n            'mobile' => '手机号',\n            'vip' => 'vip等级'\n        ];\n    }\n\n}"
  },
  {
    "path": "MineAdmin/php/app/Ai/Service/AiChatMessageService.php",
    "content": "<?php\ndeclare(strict_types=1);\n\nnamespace App\\Ai\\Service;\n\nuse App\\Ai\\Mapper\\AiChatMessageMapper;\nuse App\\Ai\\Model\\AiChatMessage;\nuse Mine\\Abstracts\\AbstractService;\n\n/**\n * 聊天数据服务类\n */\nclass AiChatMessageService extends AbstractService\n{\n    /**\n     * @var AiChatMessageMapper\n     */\n    public $mapper;\n\n    public function __construct(AiChatMessageMapper $mapper)\n    {\n        $this->mapper = $mapper;\n    }\n\n    public function messages(array $param): array\n    {\n        $slide   = $param['slide'] ?? 'prev';\n        $last_id = $param['last_id'] ?? 0;\n        $model   = $this->mapper->getModel();\n        $model   = $model->where('sid', '=', $param['sid']);\n        if ($slide === 'prev') {\n            // 上一页\n            if ($last_id) {\n                $model = $model->where('id', '<', $last_id);\n            }\n        } else {\n            if ($last_id) {\n                $model = $model->where('id', '>', $last_id);\n            }\n        }\n        return array_values($model->orderBy('id', 'desc')->limit(10)->get()->sortBy('id', SORT_ASC)->toArray());\n    }\n\n    public function shareMessageList(array $param): array\n    {\n        $model   = $this->mapper->getModel();\n        $model   = $model->where('sid', '=', $param['sid']);\n        $last_id = $param['last_id'] ?? 0;\n        if ($last_id) {\n            $model = $model->where('id', '>', $last_id);\n        }\n        return $model->orderBy('id')->limit(10)->get()->toArray();\n    }\n}"
  },
  {
    "path": "MineAdmin/php/app/Ai/Service/AiChatSessionService.php",
    "content": "<?php\ndeclare(strict_types=1);\n\n\nnamespace App\\Ai\\Service;\n\nuse App\\Ai\\Constants\\ResponseCodeConst;\nuse App\\Ai\\Mapper\\AiChatSessionMapper;\nuse App\\Ai\\Model\\AiChatSession;\nuse Mine\\Abstracts\\AbstractService;\nuse Mine\\Exception\\NormalStatusException;\n\n/**\n * 问答会话服务类\n */\nclass AiChatSessionService extends AbstractService\n{\n    /**\n     * @var AiChatSessionMapper\n     */\n    public $mapper;\n\n    protected AiLoginService $loginService;\n\n    public function __construct(AiChatSessionMapper $mapper, AiLoginService $loginService)\n    {\n        $this->mapper = $mapper;\n        $this->loginService = $loginService;\n    }\n\n    /**\n     * 获取会话\n     * @param array $param\n     * @return array\n     */\n    public function session(array $param): array\n    {\n        $where          = [];\n        $where['uid']   = $this->loginService->getId();\n        $where['close'] = 1;\n        if ($prompt_id = ($param['prompt_id'] ?? 0)) {\n            $where['prompt_id'] = $prompt_id;\n        }\n\n        /**\n         * @var $res AiChatSession\n         */\n        $res = $this->mapper->session($where);\n        $isNew = false;\n        if (empty($res)) {\n            // 没有回话则创建会话\n            $res      = new \\stdClass();\n            $res->prompt_id = $prompt_id ?: 1;\n            $res->id = $this->mapper->save([\n                'uid' => $this->loginService->getId(),\n                'prompt_id' => $res->prompt_id,\n                'created_at' => date(\"Y-m-d H:i:s\")\n            ]);\n            $isNew = true;\n        }\n        return [\n            'sid'    => $res->id,\n            'prompt_id'    => $res->prompt_id,\n            'is_new' => $isNew\n        ];\n    }\n\n    public function sessionClose(array $param): int\n    {\n        if (empty($param['sid']) || empty($param['prompt_id'])) {\n            throw new NormalStatusException('参数错误', ResponseCodeConst::PARAM_FAILED);\n        }\n\n        $this->mapper->updateByCondition(['id' => $param['sid'], 'uid' => $this->loginService->getId()], [\n            'close' => 2\n        ]);\n        return $this->mapper->save([\n            'uid' => $this->loginService->getId(),\n            'prompt_id' => $param['prompt_id'],\n            'created_at' => date(\"Y-m-d H:i:s\")\n        ]);\n    }\n\n    public function sessionHistory(array $param): array\n    {\n        $where        = [];\n        $where['uid'] = $this->loginService->getId();\n        $model        = $this->mapper->getModel();\n        $id           = $param['last_id'] ?? 0;\n        if ($id) {\n            $model = $model->where('id', '<', $id);\n        }\n        return $model->with('firstMessage')->where($where)->orderBy('id', 'desc')->limit(15)->get()->toArray();\n    }\n\n    public function sessionShareList(array $param): array\n    {\n        $where          = [];\n        $where['share'] = 2;\n        $model          = $this->mapper->getModel();\n        $id             = $param['last_id'] ?? 0;\n        if ($id) {\n            $model = $model->where('id', '<', $id);\n        }\n       return $model->with('firstMessage')->with(['user' => function ($query) {\n                  $query->select(['id', 'nick_name', 'head_img']);\n              }])\n              ->where($where)\n              ->orderBy('id', 'desc')\n              ->limit(10)\n              ->get()\n              ->toArray();\n    }\n}"
  },
  {
    "path": "MineAdmin/php/app/Ai/Service/AiChatgptPromptsService.php",
    "content": "<?php\ndeclare(strict_types=1);\n\nnamespace App\\Ai\\Service;\n\nuse App\\Ai\\Mapper\\AiChatgptPromptsMapper;\nuse Mine\\Abstracts\\AbstractService;\n\n/**\n * chatgpt角色服务类\n */\nclass AiChatgptPromptsService extends AbstractService\n{\n    /**\n     * @var AiChatgptPromptsMapper\n     */\n    public $mapper;\n\n    const MODEL_LIST = [\n        [\n            'text'              => 'gpt-3.5-turbo-0613',\n            'value'             => 0,\n            'is_vip'            => false,\n            // 每次对话上下文最多包含1500单词左右\n            'max_tokens'        => 2048,\n            'max_tokens_text'   => '每次对话上下文最多包含1500单词左右',\n            // 温度设置（较高的温度会导致更多种类和不可预测的输出）\n            'temperature'       => 0.5,\n            // 在生成句子的时候加入惩罚项来限制重复单词\n            'frequency_penalty' => 0,\n            // 减少总体上使用频率较高的单词/短语的概率\n            'presence_penalty'  => 0,\n            // 上下文长度\n            'context_length'  => 3,\n        ],\n        [\n            'text'              => 'gpt-3.5-turbo-16k',\n            'value'             => 1,\n            'is_vip'            => true,\n            'temperature'       => 1.0,\n            'max_tokens'        => -1,\n            'max_tokens_text'   => '每次对话上下文最多包含12000单词左右',\n            'frequency_penalty' => 0,\n            'presence_penalty'  => 0,\n            'context_length'  => 3,\n        ],\n    ];\n\n    public function __construct(AiChatgptPromptsMapper $mapper)\n    {\n        $this->mapper = $mapper;\n    }\n}"
  },
  {
    "path": "MineAdmin/php/app/Ai/Service/AiImageMaterialService.php",
    "content": "<?php\ndeclare(strict_types=1);\n\n\nnamespace App\\Ai\\Service;\n\nuse App\\Ai\\Mapper\\AiImageMaterialMapper;\nuse Mine\\Abstracts\\AbstractService;\n\n/**\n * 图片素材服务类\n */\nclass AiImageMaterialService extends AbstractService\n{\n    /**\n     * @var AiImageMaterialMapper\n     */\n    public $mapper;\n\n    public function __construct(AiImageMaterialMapper $mapper)\n    {\n        $this->mapper = $mapper;\n    }\n\n    public function mine(int $scene): array\n    {\n        $time = time();\n        return $this->mapper->getModel()\n             ->where('scene', '=', $scene)\n             ->orderByDesc('sort')\n             ->orderByDesc('id')\n             ->select(['id','img_url','start_at','end_at','url'])\n             ->get()\n             ->filter(function ($item) use ($time){\n                 return strtotime($item->start_at) < $time && strtotime($item->end_at) > $time;\n             })\n             ->toArray();\n    }\n}"
  },
  {
    "path": "MineAdmin/php/app/Ai/Service/AiLoginService.php",
    "content": "<?php\ndeclare(strict_types=1);\n\nnamespace App\\Ai\\Service;\n\nuse Hyperf\\Context\\Context;\nuse Psr\\SimpleCache\\InvalidArgumentException;\nuse Xmo\\JWTAuth\\JWT;\n\nclass AiLoginService\n{\n    /**\n     * @var JWT\n     */\n    protected JWT $jwt;\n\n    /**\n     * LoginUser constructor.\n     * @param string $scene 场景，默认为default\n     */\n    public function __construct()\n    {\n        /* @var JWT $this- >jwt */\n        $this->jwt = make(JWT::class)->setScene('ai');\n    }\n\n    /**\n     * 获取Token\n     * @param array $claims\n     * @return string\n     * @throws InvalidArgumentException\n     */\n    public function getToken(array $claims): string\n    {\n        return $this->jwt->getToken($claims);\n    }\n\n    /**\n     * 验证token\n     * @param string|null $token\n     * @param string $scene\n     * @return bool\n     */\n    public function check(?string $token = null, string $scene = 'default'): bool\n    {\n        try {\n            if ($this->jwt->checkToken($token, $scene, true, true, true)) {\n                return true;\n            }\n        } catch (InvalidArgumentException $e) {\n//            throw new TokenException(t('jwt.no_token'));\n            return false;\n        } catch (\\Throwable $e) {\n//            throw new TokenException(t('jwt.no_login'));\n            return false;\n        }\n\n        return false;\n    }\n\n    /**\n     * 获取JWT对象\n     * @return Jwt\n     */\n    public function getJwt(): Jwt\n    {\n        return $this->jwt;\n    }\n\n    /**\n     * 获取当前登录用户信息\n     * @param string|null $token\n     * @return array\n     */\n    public function getInfo(?string $token = null): array\n    {\n        $info = Context::get(self::class.'_info');\n        if (empty($info)){\n            $info = $this->jwt->getParserData($token);\n            Context::set(self::class.'_info', $info);\n        }\n\n        return $info;\n    }\n\n    /**\n     * 获取当前登录用户ID\n     * @return int\n     */\n    public function getId(): int\n    {\n        return $this->getInfo()['id'];\n    }\n\n    /**\n     * 刷新token\n     * @return string\n     */\n    public function refresh(): string\n    {\n        return $this->jwt->refreshToken();\n    }\n}"
  },
  {
    "path": "MineAdmin/php/app/Ai/Service/AiMineMenuGroupService.php",
    "content": "<?php\ndeclare(strict_types=1);\n\n\nnamespace App\\Ai\\Service;\n\nuse App\\Ai\\Mapper\\AiMineMenuGroupMapper;\nuse App\\Ai\\Model\\AiMineMenuGroup;\nuse Mine\\Abstracts\\AbstractService;\n\n/**\n * 个人中心菜单分组服务类\n */\nclass AiMineMenuGroupService extends AbstractService\n{\n    /**\n     * @var AiMineMenuGroupMapper\n     */\n    public $mapper;\n\n    public function __construct(AiMineMenuGroupMapper $mapper)\n    {\n        $this->mapper = $mapper;\n    }\n\n    public function mineMenus(): array\n    {\n        return $this->mapper->getModel()::with(['menus' => function ($query) {\n                   $query->orderByDesc('sort')->orderByDesc('id');\n               }])\n               ->orderByDesc('sort')\n               ->orderByDesc('id')\n               ->get()\n               ->toArray();\n    }\n}"
  },
  {
    "path": "MineAdmin/php/app/Ai/Service/AiMineMenuService.php",
    "content": "<?php\ndeclare(strict_types=1);\n\n\nnamespace App\\Ai\\Service;\n\nuse App\\Ai\\Mapper\\AiMineMenuMapper;\nuse Mine\\Abstracts\\AbstractService;\n\n/**\n * 个人中心菜单服务类\n */\nclass AiMineMenuService extends AbstractService\n{\n    /**\n     * @var AiMineMenuMapper\n     */\n    public $mapper;\n\n    public function __construct(AiMineMenuMapper $mapper)\n    {\n        $this->mapper = $mapper;\n    }\n}"
  },
  {
    "path": "MineAdmin/php/app/Ai/Service/AiOpenaiKeyService.php",
    "content": "<?php\ndeclare(strict_types=1);\n\n\nnamespace App\\Ai\\Service;\n\nuse App\\Ai\\Factory\\AiRedisFactory;\nuse App\\Ai\\Mapper\\AiOpenaiKeyMapper;\nuse App\\Ai\\Model\\AiOpenaiKey;\nuse Mine\\Abstracts\\AbstractService;\n\n/**\n * openai_key服务类\n */\nclass AiOpenaiKeyService extends AbstractService\n{\n    /**\n     * @var AiOpenaiKeyMapper\n     */\n    public $mapper;\n\n    protected AiRedisFactory $redis;\n\n    public function __construct(AiOpenaiKeyMapper $mapper, AiRedisFactory $redis)\n    {\n        $this->mapper = $mapper;\n        $this->redis = $redis;\n    }\n\n    public function batchAdd(array $data): bool\n    {\n        $keys = explode(\"\\n\", $data['content']);\n        if (empty($keys)) {\n            return false;\n        }\n        $saveAll = [];\n        $time = date('Y-m-d H:i:s');\n        foreach ($keys as $key=>$v) {\n            $saveAll[$key]['openai_key'] = trim($v);\n            $saveAll[$key]['remark']     = $data['remark'];\n            $saveAll[$key]['created_at'] = $time;\n            $saveAll[$key]['updated_at'] = $time;\n        }\n        $model = new AiOpenaiKey();\n        $model->insert($saveAll);\n        return true;\n    }\n\n    public function cacheAll(): bool\n    {\n        $this->redis->del('ai_openai_key_cache');\n        $all = $this->mapper->get();\n        if (empty($all)) return false;\n        for ($i = 0; $i < 10; $i++) {\n            foreach ($all as $item) {\n                $this->redis->rPush('ai_openai_key_cache', $item['openai_key']);\n            }\n        }\n        return true;\n    }\n\n    public function openAiKey()\n    {\n        $key = $this->redis->lPop('ai_openai_key_cache');\n        $this->redis->rPush('ai_openai_key_cache', $key);\n        return $key;\n    }\n}"
  },
  {
    "path": "MineAdmin/php/app/Ai/Service/AiOrderService.php",
    "content": "<?php\ndeclare(strict_types=1);\n\n\nnamespace App\\Ai\\Service;\n\nuse App\\Ai\\Constants\\OrderConst;\nuse App\\Ai\\Constants\\ResponseCodeConst;\nuse App\\Ai\\Constants\\VipConst;\nuse App\\Ai\\Constants\\WalletConst;\nuse App\\Ai\\Mapper\\AiOrderMapper;\nuse App\\Ai\\Model\\AiOrder;\nuse App\\Ai\\Model\\AiOrderKami;\nuse App\\Ai\\Model\\AiOrderVip;\nuse App\\Ai\\Model\\AiPayKami;\nuse App\\Ai\\Model\\AiUser;\nuse App\\Ai\\Model\\AiUserWallet;\nuse App\\Ai\\Model\\AiUserWalletLog;\nuse Hyperf\\DbConnection\\Db;\nuse Mine\\Abstracts\\AbstractService;\nuse Mine\\Exception\\NormalStatusException;\n\n/**\n * 订单表服务类\n */\nclass AiOrderService extends AbstractService\n{\n    /**\n     * @var AiOrderMapper\n     */\n    public $mapper;\n\n    protected AiVipService $vipService;\n\n    protected AiLoginService $loginService;\n\n    public function __construct(AiOrderMapper $mapper, AiVipService $vipService, AiLoginService $loginService)\n    {\n        $this->mapper = $mapper;\n        $this->vipService   = $vipService;\n        $this->loginService = $loginService;\n    }\n\n    public function orderList(array $param): array\n    {\n        $create_time1 =\n        $create_time2 = '';\n        if (!empty($param['create_time'])) {\n            list($create_time1, $create_time2) = explode(',', $param['create_time']);\n        }\n\n        $last_id      = $param['last_id'] ?? 0;\n        $keywords     = $param['keyword'] ?? '';\n        $model        = new AiOrder();\n        $model = $model->where([\n            'uid' => $this->loginService->getId(),\n        ])->whereIn('ord_type', $param['ord_type']);\n\n        if ($create_time1 && $create_time2) {\n            $model = $model->where('created_at', '>', $create_time1 . ' 00:00:00')\n                ->where('created_at', '<', $create_time2 . ' 00:00:00');\n        }\n\n        if ($last_id) {\n            $model = $model->where('id', '<', $last_id);\n        }\n\n        if ($keywords) {\n            $model = $model->where('content', 'like', \"%$keywords%\");\n        }\n\n        $ord_type = OrderConst::getGroupValueDescArr('ord_type');\n        $status   = OrderConst::getGroupValueDescArr('status');\n        $pay_type = OrderConst::getGroupValueDescArr('pay_type');\n        return $model->limit(15)\n            ->select(explode(',', 'id,uid,ord_sn,ord_type,pay_type,status,amount_price,content,created_at,pay_at'))\n            ->orderBy('id', 'desc')->get()\n            ->each(function ($item) use ($ord_type, $status, $pay_type) {\n                $item->status_text       = $status[$item->status];\n                $item->ord_type_text     = $ord_type[$item->ord_type];\n                $item->pay_type_text     = $pay_type[$item->pay_type];\n                $item->amount_price_text = HelperService::decode100($item->amount_price);\n            })\n            ->toArray();\n    }\n\n    public function kamiOpenVip(array $param)\n    {\n        /**\n         * @var AiPayKami $kami\n         */\n        $kami = AiPayKami::where(['code' => $param['kami_code']])->first();\n        if (empty($kami)) {\n            throw new NormalStatusException('卡密不存在', ResponseCodeConst::PARAM_FAILED);\n        }\n\n        if (2 === $kami->status) {\n            throw new NormalStatusException('卡密已失效', ResponseCodeConst::PARAM_FAILED);\n        }\n\n        if ($kami->uid && $kami->uid != $this->loginService->getId()) {\n            throw new NormalStatusException('您无权使用该卡密', ResponseCodeConst::PARAM_FAILED);\n        }\n\n        /**\n         * @var AiUser $user\n         */\n        [$vipConfigAll, $user] = $this->vipService->config($this->loginService->getId());\n        unset($user->vip_name);\n        $vipConfigAll = array_column($vipConfigAll, null, 'level');\n\n        if (empty($vipConfigAll[$param['level']])) {\n            throw new NormalStatusException('VIP信息有误', ResponseCodeConst::PARAM_FAILED);\n        }\n\n        $vipConfig = $vipConfigAll[$param['level']];\n\n        if ($kami->price && $kami->price < $vipConfig['price_pay']) {\n            throw new NormalStatusException('卡密金额不足', ResponseCodeConst::PARAM_FAILED);\n        }\n\n        if ($user->vip > $vipConfig['level']) {\n            throw new NormalStatusException('VIP信息有误1', ResponseCodeConst::PARAM_FAILED);\n        }\n        Db::beginTransaction();\n        try {\n            $price = HelperService::encode100($vipConfig['price_pay']);\n            $aiOrder = AiOrder::create([\n                'uid'          => $user->id,\n                'from_uid'     => $user->parentRelation->from_uid,\n                'market_uid'   => $user->parentRelation->market_uid,\n                'ord_sn'       => HelperService::createOrderCode($user->parentRelation->from_uid),\n                'ord_type'     => OrderConst::OPEN_VIP,\n                'pay_type'     => $price ? OrderConst::KAMI_PAY : OrderConst::KAMI_FREE,\n                'status'       => OrderConst::SUCCESS_PAY,\n                'total_price'  => $price,\n                'amount_price' => $price,\n                'content'      => '购买<<' . $vipConfig['name'] . '>>',\n                'remark'       => '卡密支付',\n            ]);\n            $oid = $aiOrder->id;\n            AiOrderVip::create([\n                'oid' => $oid,\n                'vip_level' => $vipConfig['level'],\n            ]);\n            AiOrderKami::create([\n                'oid' => $oid,\n                'kid' => $kami->id,\n            ]);\n            if ($price && !empty($user->parentRelation->from_uid)) {\n                /**\n                 * @var AiUser $parentInfo\n                 */\n                $parentInfo = AiUser::first(['id' => $user->parentRelation->from_uid]);\n                $pVipConfig = $vipConfigAll[$parentInfo->vip] ?? [];\n                if (!empty($pVipConfig) && ($parentInfo->vip ?? 0) > VipConst::VIP && $parentInfo->vip >= $vipConfig['level'] && $pVipConfig['income'] > 0) {\n                    $income = HelperService::encode100($vipConfig['price_pay'] * ($pVipConfig['income'] / 100));\n                    AiUserWallet::where(['uid' => $user->parentRelation->from_uid])->update([\n                        'balance'       => Db::raw('`balance`+' . $income),\n                        'balance_total' => Db::raw('`balance_total`+' . $income),\n                    ]);\n\n                    AiUserWalletLog::create([\n                        'uid'        => $user->parentRelation->from_uid,\n                        'oid'        => $oid,\n                        'direction'  => WalletConst::IN,\n                        'scene'      => WalletConst::PROMOTION,\n                        'balance'    => $income,\n                        'remark'     => 'user-'.$user->id.':购买<<' . $vipConfig['name'] . '>>获得' . $vipConfig['income'] . '%',\n                        'created_at' => date('Y-m-d H:i:s'),\n                    ]);\n                }\n            }\n\n            $user->vip  = $vipConfig['level'];\n            $vip_ent_at = $user->vip_ent_at ? strtotime($user->vip_ent_at) : 0;\n            $time       = time();\n            if ($vip_ent_at && $vip_ent_at > $time) {\n                $vip_ent_at += $vipConfig['length'] * 2592000;\n            } else {\n                $vip_ent_at = $time + $vipConfig['length'] * 2592000;\n            }\n            $user->vip_ent_at = date('Y-m-d H:i:s', $vip_ent_at);\n            $user->save();\n            Db::commit();\n        }catch (\\Throwable $exception){\n            Db::rollBack();\n            throw new NormalStatusException($exception->getMessage(), ResponseCodeConst::PARAM_FAILED);\n        }\n    }\n\n    public function adminOpenVip(array $data)\n    {\n        /**\n         * @var AiUser $user\n         */\n        $user = $this->mapper->first(['id' => $data['id']]);\n        if (empty($user)) {\n            throw new NormalStatusException('用户不存在', ResponseCodeConst::PARAM_FAILED);\n        }\n\n        if ($user->vip > (int)$data['vip']) {\n            throw new NormalStatusException('不能开通比用户原等级小的VIP', ResponseCodeConst::PARAM_FAILED);\n        }\n\n        $vipConfigAll = array_column(VipConst::config(), null, 'level');\n        $vipConfig = $vipConfigAll[$data['vip']] ?? [];\n        if (empty($vipConfig)) {\n            throw new NormalStatusException('vip信息有误', ResponseCodeConst::PARAM_FAILED);\n        }\n        Db::beginTransaction();\n        try {\n            $price   = $data['price'] ? HelperService::encode100($data['price']) : 0;\n            $aiOrder = AiOrder::create([\n                'uid'          => $user->id,\n                'from_uid'     => $user->parentRelation->from_uid,\n                'market_uid'   => $user->parentRelation->market_uid,\n                'ord_sn'       => HelperService::createOrderCode($user->parentRelation->from_uid),\n                'ord_type'     => OrderConst::OPEN_VIP,\n                'pay_type'     => $price ? OrderConst::ADMIN_PAY : OrderConst::ADMIN_FREE,\n                'status'       => OrderConst::SUCCESS_PAY,\n                'total_price'  => $price,\n                'amount_price' => $price,\n                'content'      => '购买<<' . $vipConfig['name'] . '>>',\n                'remark'       => $data['remark'],\n            ]);\n            $oid = $aiOrder->id;\n            AiOrderVip::create([\n                'oid'       => $oid,\n                'vip_level' => $vipConfig['level'],\n            ]);\n            if ($price && !empty($user->parentRelation->from_uid)) {\n                /**\n                 * @var AiUser $parentInfo\n                 */\n                $parentInfo = AiUser::first(['id' => $user->parentRelation->from_uid]);\n                $pVipConfig = $vipConfigAll[$parentInfo->vip] ?? [];\n                if (!empty($pVipConfig) && ($parentInfo->vip ?? 0) > VipConst::VIP && $parentInfo->vip >= $vipConfig['level'] && $pVipConfig['income'] > 0) {\n                    $income = HelperService::encode100($data['price'] * ($pVipConfig['income'] / 100));\n                    AiUserWallet::where(['uid' => $user->parentRelation->from_uid])->update([\n                        'balance'       => Db::raw('`balance`+' . $income),\n                        'balance_total' => Db::raw('`balance_total`+' . $income),\n                    ]);\n\n                    AiUserWalletLog::create([\n                        'uid'        => $user->parentRelation->from_uid,\n                        'oid'        => $oid,\n                        'direction'  => WalletConst::IN,\n                        'scene'      => WalletConst::PROMOTION,\n                        'balance'    => $income,\n                        'remark'     => 'user-'.$user->id.':购买<<' . $vipConfig['name'] . '>>获得' . $vipConfig['income'] . '%',\n                        'created_at' => date('Y-m-d H:i:s'),\n                    ]);\n                }\n            }\n\n            $user->vip  = $vipConfig['level'];\n            $vip_ent_at = $user->vip_ent_at ? strtotime($user->vip_ent_at) : 0;\n            $time       = time();\n            if ($vip_ent_at && $vip_ent_at > $time) {\n                $vip_ent_at += $vipConfig['length'] * 2592000;\n            } else {\n                $vip_ent_at = $time + $vipConfig['length'] * 2592000;\n            }\n            $user->vip_ent_at = date('Y-m-d H:i:s', $vip_ent_at);\n            $user->save();\n            Db::commit();\n        }catch (\\Throwable $exception){\n            Db::rollBack();\n            throw new NormalStatusException($exception->getMessage(), ResponseCodeConst::PARAM_FAILED);\n        }\n    }\n}"
  },
  {
    "path": "MineAdmin/php/app/Ai/Service/AiPayKamiService.php",
    "content": "<?php\ndeclare(strict_types=1);\n\n\nnamespace App\\Ai\\Service;\n\nuse App\\Ai\\Constants\\ResponseCodeConst;\nuse App\\Ai\\Mapper\\AiPayKamiMapper;\nuse App\\Ai\\Model\\AiPayKami;\nuse Mine\\Abstracts\\AbstractService;\nuse Mine\\Exception\\NormalStatusException;\n\n/**\n * 卡密服务类\n */\nclass AiPayKamiService extends AbstractService\n{\n    /**\n     * @var AiPayKamiMapper\n     */\n    public $mapper;\n\n    public function __construct(AiPayKamiMapper $mapper)\n    {\n        $this->mapper = $mapper;\n    }\n\n    public function add(array $data){\n        $num = $data['number'];\n        $num = $num ?? 100;\n        if ($num <= 0 || $num >= 100) {\n            throw new NormalStatusException('数量必须小于或等于100', ResponseCodeConst::PARAM_FAILED);\n        }\n        $time     = date('Y-m-d H:i:s');\n        $code_arr = HelperService::randStrArr(32, (int)$num);\n        $saveAll  = [];\n        $data['price']  = $data['price'] ? HelperService::encode100($data['price']) : 0;\n        $data['uid']    = $data['uid'] ?: 0;\n        $data['remark'] = $data['remark'] ?: '';\n        foreach ($code_arr as $key=>$code){\n            $saveAll[$key]['uid']        = $data['uid'];\n            $saveAll[$key]['price']      = $data['price'];\n            $saveAll[$key]['code']       = $code;\n            $saveAll[$key]['status']     = 1;\n            $saveAll[$key]['remark']     = $data['remark'];\n            $saveAll[$key]['created_at'] = $time;\n            $saveAll[$key]['updated_at'] = $time;\n        }\n        $model = new AiPayKami();\n        $model->insert($saveAll);\n    }\n}"
  },
  {
    "path": "MineAdmin/php/app/Ai/Service/AiQuickIssueService.php",
    "content": "<?php\ndeclare(strict_types=1);\n\n\nnamespace App\\Ai\\Service;\n\nuse App\\Ai\\Mapper\\AiQuickIssueMapper;\nuse Mine\\Abstracts\\AbstractService;\n\n/**\n * 快捷问题\n服务类\n */\nclass AiQuickIssueService extends AbstractService\n{\n    /**\n     * @var AiQuickIssueMapper\n     */\n    public $mapper;\n\n    public function __construct(AiQuickIssueMapper $mapper)\n    {\n        $this->mapper = $mapper;\n    }\n}"
  },
  {
    "path": "MineAdmin/php/app/Ai/Service/AiSettingService.php",
    "content": "<?php\ndeclare(strict_types=1);\n\nnamespace App\\Ai\\Service;\n\nuse App\\Ai\\Factory\\AiRedisFactory;\n\nclass AiSettingService\n{\n    protected AiRedisFactory $redis;\n\n    public function __construct(AiRedisFactory $redis){\n        $this->redis = $redis;\n    }\n\n    const CACHE_KEY = 'ai_setting_';\n\n    protected array $customer = [\n        'head_img'   => '',\n        'mobile'     => '',\n        'user_name'  => '',\n        'work_time'  => '',\n        'wx_img_url' => '',\n        'wx_no'      => ''\n    ];\n\n    public function customer(): array\n    {\n        $data = $this->redis->get(self::CACHE_KEY . 'customer');\n        if ($data) {\n            $data = \\json_decode($data, true);\n        }\n        $data = array_merge($this->customer, $data ?: []);\n        return $data;\n    }\n\n    public function setCustomer($data): array\n    {\n        $this->redis->set(self::CACHE_KEY . 'customer', \\json_encode($data, JSON_UNESCAPED_UNICODE));\n        return array_merge($this->customer, $data);\n    }\n\n    public function appClose()\n    {\n        return $this->redis->get(self::CACHE_KEY . 'app_close_message') ?: '' ;\n    }\n\n    public function setAppClose(string $v)\n    {\n        // 关站消息，有则为关站\n        return $this->redis->set(self::CACHE_KEY . 'app_close_message', $v);\n    }\n\n    public function agreementUser()\n    {\n        return $this->redis->get(self::CACHE_KEY . 'agreement') ?: '';\n    }\n\n    public function setAgreementUser($v)\n    {\n        return $this->redis->set(self::CACHE_KEY . 'agreement', $v);\n    }\n\n    public function openaiProxy()\n    {\n        return $this->redis->get(self::CACHE_KEY . 'openai_proxy') ?: '';\n    }\n\n    public function setOpenaiProxy($v)\n    {\n        return $this->redis->set(self::CACHE_KEY . 'openai_proxy', $v);\n    }\n}"
  },
  {
    "path": "MineAdmin/php/app/Ai/Service/AiUserService.php",
    "content": "<?php\ndeclare(strict_types=1);\n\n\nnamespace App\\Ai\\Service;\n\nuse App\\Ai\\Constants\\ResponseCodeConst;\nuse App\\Ai\\Constants\\VipConst;\nuse App\\Ai\\Factory\\AiRedisFactory;\nuse App\\Ai\\Mapper\\AiUserMapper;\nuse App\\Ai\\Model\\AiUser;\nuse App\\Ai\\Model\\AiUserRelation;\nuse App\\Ai\\Model\\AiUserWallet;\nuse Hyperf\\DbConnection\\Db;\nuse Mine\\Abstracts\\AbstractService;\nuse Mine\\Exception\\NormalStatusException;\n\n/**\n * 用户主表服务类\n */\nclass AiUserService extends AbstractService\n{\n    /**\n     * @var AiUserMapper\n     */\n    public $mapper;\n\n    protected AiRedisFactory $redis;\n\n    protected AiLoginService $loginService;\n\n    protected AiVipService $vipService;\n\n    public function __construct(AiUserMapper $mapper, AiRedisFactory $redis, AiLoginService $loginService,  AiVipService $vipService)\n    {\n        $this->mapper       = $mapper;\n        $this->redis        = $redis;\n        $this->loginService = $loginService;\n        $this->vipService   = $vipService;\n    }\n\n    public function userList(array $params): array\n    {\n        $relation = new AiUserRelation();\n        $wallet   = new AiUserWallet();\n        $user     = $this->mapper->getModel();\n        $query    = Db::table($user->getTable().' as a')\n                ->leftJoin($relation->getTable().' as b', 'a.id', '=', 'b.uid')\n                ->leftJoin($wallet->getTable().' as c', 'a.id', '=', 'c.uid')\n                ->leftJoin($user->getTable().' as d', 'b.from_uid', '=', 'd.id')\n                ->select(['a.*', 'b.from_uid', 'b.market_uid', 'c.balance', 'c.balance_total', 'd.nick_name as parent_nick_name']);\n        // 昵称\n        if (isset($params['nick_name']) && $params['nick_name'] !== '') {\n            $query->where('a.nick_name', 'like', '%'.$params['nick_name'].'%');\n        }\n\n        // 手机号\n        if (isset($params['mobile']) && $params['mobile'] !== '') {\n            $query->where('a.mobile', 'like', '%'.$params['mobile'].'%');\n        }\n\n        // vip等级\n        if (isset($params['vip']) && $params['vip'] !== '') {\n            $query->where('a.vip', '=', $params['vip']);\n        }\n\n        // vip到期时间\n        if (isset($params['vip_ent_at']) && is_array($params['vip_ent_at']) && count($params['vip_ent_at']) == 2) {\n            $query->whereBetween(\n                'a.vip_ent_at',\n                [ $params['vip_ent_at'][0], $params['vip_ent_at'][1] ]\n            );\n        }\n\n        // 是否锁定:1正常,2锁定\n        if (isset($params['is_lock']) && $params['is_lock'] !== '') {\n            $query->where('a.is_lock', '=', $params['is_lock']);\n        }\n\n        // 创建时间\n        if (isset($params['created_at']) && is_array($params['created_at']) && count($params['created_at']) == 2) {\n            $query->whereBetween(\n                'a.created_at',\n                [ $params['created_at'][0], $params['created_at'][1] ]\n            );\n        }\n\n        // 更新时间\n        if (isset($params['updated_at']) && is_array($params['updated_at']) && count($params['updated_at']) == 2) {\n            $query->whereBetween(\n                'a.updated_at',\n                [ $params['updated_at'][0], $params['updated_at'][1] ]\n            );\n        }\n\n        // 昵称\n        if (isset($params['from_uid']) && $params['from_uid'] !== '') {\n            $query->where('b.from_uid', '=', $params['from_uid']);\n        }\n\n        if ($params['orderBy'] ?? false) {\n            if (is_array($params['orderBy'])) {\n                foreach ($params['orderBy'] as $key => $order) {\n                    $query->orderBy($order, 'a.'.$params['orderType'][$key] ?? 'asc');\n                }\n            } else {\n                $query->orderBy($params['orderBy'], 'a.'.$params['orderType'] ?? 'asc');\n            }\n        }\n\n        $query = $query->paginate((int)$params['pageSize'] ?? $this->mapper->getModel()::PAGE_SIZE, ['*'], 'page', (int)$params['page'] ?? 1);\n        $res   = $this->mapper->setPaginate($query);\n\n        foreach ($res['items'] as $k=>$v) {\n            $res['items'][$k]->head_img      = HelperService::buildSourceUrl($v->head_img);\n            $res['items'][$k]->balance       = $v->balance ? HelperService::decode100($v->balance) : '0';\n            $res['items'][$k]->balance_total = $v->balance_total ? HelperService::decode100($v->balance_total) : '0';\n        }\n\n        return $res;\n    }\n\n    public function update(mixed $id, array $data): bool\n    {\n        /**\n         * @var AiUser $user\n         */\n        $user = $this->mapper->first(['mobile' => $data['mobile']]);\n        if (!empty($user) && $user->id != $id) {\n            throw new NormalStatusException('手机号已有用户使用', ResponseCodeConst::PARAM_FAILED);\n        }\n        return $this->mapper->update($id, $data);\n    }\n\n    /**\n     * 单个或批量软删除数据\n     * @param array $ids\n     * @return bool\n     */\n    public function delete(array $ids): bool\n    {\n        if (!empty($ids)){\n            $this->mapper->delete($ids);\n            foreach ($ids as $id) {\n                $this->redis->sAdd('ai_user_delete', $id);\n            }\n            $this->redis->expire('ai_user_delete', 86400);\n        }\n        return true;\n    }\n\n    public function isJustDelete(int $id): bool\n    {\n        return $this->redis->sIsMember('ai_user_delete', $id);\n    }\n\n    public function lock(int $id)\n    {\n        $user = $this->read($id);\n        if ($user) {\n            $user->is_lock = $user->is_lock === 1 ? 2 : 1;\n            $user->save();\n            if ($user->is_lock === 2) {\n                $this->redis->sAdd('ai_user_lock', $id);\n                $this->redis->expire('ai_user_lock', 86400);\n            } else {\n                $this->redis->sRem('ai_user_lock', $id);\n            }\n        }\n    }\n\n    public function isJustLock(int $id): bool\n    {\n        return $this->redis->sIsMember('ai_user_lock', $id);\n    }\n\n    public function friendNum(int $uid): int\n    {\n        return AiUserRelation::where(['from_uid' => $uid])->count('uid');\n    }\n\n    public function friendList(array $param){\n        $register_time1 =\n        $register_time2 = '';\n        if (!empty($param['register_time'])) {\n            list($register_time1, $register_time2) = explode(',', $param['register_time']);\n        }\n        $keywords       = $param['keyword'] ?? '';\n        $last_id        = $param['last_id'] ?? 0;\n        $uid            = $this->loginService->getId();\n        $model          = new AiUserRelation();\n        $userModel      = new AiUser();\n\n        if (($param['cate_id'] ?? '1') === '2') {\n            $model = $model->where('market_uid', '=', $uid)->where('from_uid','<>',$uid);\n        }else{\n            $model = $model->where('from_uid','=',$uid);\n        }\n\n        $model = $model->leftJoin($userModel->getTable().' as u', 'uid', '=', 'u.id');\n\n        if ($last_id) {\n            $model = $model->where('u.id', '<', $last_id);\n        }\n\n        $vip = $param['vip'] ?? -1;\n        if ($vip > -1) {\n            $model = $model->where('u.vip', '=', $vip);\n        }\n\n        if ($register_time1 && $register_time2) {\n            $model = $model->where('u.created_at', '>', $register_time1.' 00:00:00')->where('u.created_at', '<', $register_time2.' 00:00:00');\n        }\n\n        if ($keywords) {\n            $model = $model->where('u.nick_name', 'like', \"%$keywords%\");\n        }\n\n        return $model->orderBy('u.id', 'desc')\n               ->where('u.is_lock','=', 1)\n               ->select(['u.id','u.nick_name','u.head_img','u.mobile','u.vip','u.created_at'])\n               ->limit(10)\n               ->get()\n               ->each(function ($user) {\n                   $user->head_img = HelperService::buildSourceUrl($user->head_img);\n                   $user->vip_name = VipConst::getDesc($user->vip);\n               })\n               ->toArray();\n    }\n}"
  },
  {
    "path": "MineAdmin/php/app/Ai/Service/AiVipService.php",
    "content": "<?php\ndeclare(strict_types=1);\n\nnamespace App\\Ai\\Service;\n\nuse App\\Ai\\Constants\\OrderConst;\nuse App\\Ai\\Model\\AiUser;\nuse App\\Ai\\Constants\\VipConst;\nuse App\\Ai\\Model\\AiOrder;\n\nclass AiVipService\n{\n    public function config(int $uid): array\n    {\n        $order = AiOrder::where([\n                   'uid'      => $uid,\n                   'ord_type' => 1,\n                   'status'   => 2\n               ])\n               ->whereIn('pay_type', [OrderConst::WX_PAY, OrderConst::ADMIN_PAY, OrderConst::KAMI_PAY])\n               ->select(['amount_price'])\n               ->orderBy('id', 'desc')\n               ->first();\n        /**\n         * @var AiUser $user\n         */\n        $user = AiUser::where(['id'=>$uid])->select(['id', 'vip', 'vip_ent_at'])->first();\n        $user->vip_name = VipConst::getDesc($user->vip);\n        $config = VipConst::config();\n\n        foreach ($config as $k=>&$item) {\n            if ($user->vip <= $item['level']) {\n                // 当前等级小于等于配置等级低\n                $item['is_choose'] = true;\n            }\n\n            if ($user->vip === $item['level']) {\n                // 当前用户等级等于配置等级时为续费\n                $item['btn_text'] = '立即续费';\n            }\n\n            if ($user->vip >0 && $user->vip < $item['level']) {\n                if (!empty($order)) {\n                    // 升级补差价 抵扣最近订单金额\n                    $item['price_pay'] = HelperService::decode100($order->amount_price) > $item['price'] ? '￥1' : $item['price'];\n                }\n                $item['btn_text'] = '立即升级';\n            }\n\n            if ($user->vip === 0 && $k === 1){\n                $item['is_default'] = true;\n            }\n\n            if ($user->vip === 10 && $k === 1){\n                $item['is_default'] = true;\n            }\n\n            if ($user->vip === 20 && $k === 2){\n                $item['is_default'] = true;\n            }\n\n            if ($user->vip === 30 && $k === 2) {\n                $item['is_default'] = true;\n            }\n        }\n        return [$config, $user];\n    }\n\n}"
  },
  {
    "path": "MineAdmin/php/app/Ai/Service/AiWalletService.php",
    "content": "<?php\ndeclare(strict_types=1);\n\nnamespace App\\Ai\\Service;\n\nuse App\\Ai\\Constants\\OrderConst;\nuse App\\Ai\\Constants\\ResponseCodeConst;\nuse App\\Ai\\Constants\\WalletConst;\nuse App\\Ai\\Model\\AiOrder;\nuse App\\Ai\\Model\\AiUser;\nuse App\\Ai\\Model\\AiUserWallet;\nuse App\\Ai\\Model\\AiUserWalletLog;\nuse Hyperf\\DbConnection\\Db;\nuse Mine\\Exception\\NormalStatusException;\n\nclass AiWalletService\n{\n    protected AiLoginService $loginService;\n\n    public function __construct(AiLoginService $loginService)\n    {\n        $this->loginService = $loginService;\n    }\n\n    public function info(): array\n    {\n        $res = AiUserWallet::where(['uid' => $this->loginService->getId()])->first();\n        return [\n            'balance' => isset($res->balance) ? HelperService::decode100($res->balance) : 0,\n            'balance_total' => isset($res->balance_total) ? HelperService::decode100($res->balance_total) : 0,\n        ];\n    }\n\n    public function withdrawal(array $param): void\n    {\n        /**\n         * @var AiUserWallet $userWallet\n         */\n        $userWallet = AiUserWallet::where(['uid' => $this->loginService->getId()])->first();\n        if (empty($userWallet)) {\n            throw new NormalStatusException('账户信息错误', ResponseCodeConst::PARAM_FAILED);\n        }\n        $amount_price = HelperService::encode100((int)$param['amount']);\n        if ($userWallet->balance < $amount_price){\n            throw new NormalStatusException('账户余额不足', ResponseCodeConst::PARAM_FAILED);\n        }\n\n        /**\n         * @var AiUser $user\n         */\n        $user = AiUser::where(['id'=>$userWallet->uid])->first();\n\n        Db::beginTransaction();\n        try {\n            AiOrder::create([\n                'uid'          => $userWallet->uid,\n                'from_uid'     => $user->parentRelation->from_uid,\n                'market_uid'   => $user->parentRelation->market_uid,\n                'ord_sn'       => HelperService::createOrderCode($user->parentRelation->from_uid),\n                'ord_type'     => OrderConst::WITHDRAWAL,\n                'pay_type'     => OrderConst::WX_PAY,\n                'status'       => OrderConst::WAIT_PAY,\n                'total_price'  => $amount_price,\n                'amount_price' => $amount_price,\n                'content'      => '提现<<' . HelperService::decode100($amount_price) . '元>>',\n                'remark'       => '',\n            ]);\n            AiUserWallet::where(['uid' => $userWallet->uid])->update([\n                'balance' => Db::raw('`balance`-' . $amount_price),\n            ]);\n            Db::commit();\n        }catch (\\Throwable $exception){\n            Db::rollBack();\n            throw new NormalStatusException($exception->getMessage(), ResponseCodeConst::PARAM_FAILED);\n        }\n    }\n\n    public function changeLogList(array $param): array\n    {\n        $create_time1 =\n        $create_time2 = '';\n        if (!empty($param['create_time'])) {\n            list($create_time1, $create_time2) = explode(',', $param['create_time']);\n        }\n        $keywords     = $param['keyword'] ?? '';\n        $direction    = (int)$param['direction'] ?? 0;\n        $last_id      = $param['last_id'] ?? 0;\n        $model        = new AiUserWalletLog();\n        $model = $model->where([\n            'uid' => $this->loginService->getId()\n        ]);\n\n        if ($direction){\n            $model = $model->where('direction', '=', $direction === WalletConst::IN ? WalletConst::IN : WalletConst::OUT);\n        }\n\n        if ($create_time1 && $create_time2) {\n            $model = $model->where('created_at', '>', $create_time1.' 00:00:00')\n                ->where('created_at', '<', $create_time2.' 00:00:00');\n        }\n\n        if ($keywords) {\n            $model = $model->where('remark', 'like', \"%$keywords%\");\n        }\n\n        if ($last_id) {\n            $model = $model->where('id', '<', $last_id);\n        }\n\n        $scene = WalletConst::getGroupValueDescArr('scene');\n        return $model->limit(15)->orderBy('id', 'desc')->get()\n            ->each(function ($item) use ($scene){\n                $item->scene_text = $scene[$item->scene];\n                $item->balance_text = HelperService::decode100($item->balance);\n            })\n            ->toArray();\n    }\n}"
  },
  {
    "path": "MineAdmin/php/app/Ai/Service/HelperService.php",
    "content": "<?php\ndeclare(strict_types=1);\n\nnamespace App\\Ai\\Service;\n\nuse App\\Ai\\Middleware\\AuthMiddleware;\nuse Hyperf\\Context\\Context;\n\nclass HelperService\n{\n    public static function buildSavePath(string $url, $needle = 'upload'): string\n    {\n        if (empty($url)) {\n            return '';\n        }\n        $pathInfo = parse_url($url);\n        $start = strpos($pathInfo['path'], $needle);\n        $str = '';\n        if ($start !== false) {\n            $str = substr($pathInfo['path'], $start);\n        }\n\n        parse_str($pathInfo['query'] ?? '', $query_arr);\n        return isset($query_arr['v']) ? $str . '?v=' . $query_arr['v'] : $str;\n    }\n\n    public static function makeFileName(string $scene, string $suffix, int $uid = 0): string\n    {\n        if ($uid > 0) {\n            // 用户唯一\n            $filename = 'upload/' . $scene . '/' . md5(config('app_name') . '-' . $scene . '-' . $uid);\n        } else {\n            $filename = 'upload/' . $scene . '/' . date('Ymd') . uniqid() . mt_rand(100000000, 999999999);\n        }\n\n        return $filename . $suffix;\n    }\n\n    public static function buildSourceUrl(?string $filename): string\n    {\n        if (empty($filename)) {\n            return '';\n        }\n        if (strpos($filename, 'http') === 0 || strpos($filename, '//') === 0) {\n            return $filename;\n        }\n        // upload/1000/a.png\n        $type = (int)substr($filename, 7, 1) - 1;\n        $config = config('file.storage.qiniu');\n        $domain = [$config['image_domain'], $config['audio_domain'], $config['video_domain']];\n        $schema = self::isMini() ? 'https' : 'http';\n        switch ($type) {\n            case 2:\n                return $schema . '://' . $domain[$type - 1] . '/' . config('app_env') . '/' . $filename;\n            case 1:\n            default:\n                return $schema . '://' . ($domain[$type - 1] ?? $domain[0]) . '/' . config('app_env') . '/' . $filename;\n        }\n    }\n\n    /**\n     * 是否小程序请求\n     * @return bool\n     */\n    public static function isMini(): bool\n    {\n        return isset(Context::get(AuthMiddleware::class . '_login_data')['mini_openid']);\n    }\n\n    public static function decode100(string|int $price, string $percent = '0.01') : string\n    {\n        return bcmul((string)$price, $percent, 2);\n    }\n\n    public static function encode100(string|int|float $price, string $hundred = '100') : string\n    {\n        return bcmul((string)$price, $hundred, 0);\n    }\n\n    public static function createOrderCode(string|int $uid=0) : string\n    {\n        mt_srand();\n        return date(\"YmdHis\") . $uid . rand(100000, 999999);\n    }\n\n    public static function randStrArr(int $len = 6, int $number = 1): array\n    {\n        mt_srand();\n        $res = [];\n        $str = 'a b c d e f g h i j k l m n o p q r s t u v w x y z A B C D E F G H I J K L M N O P Q R S T U V W X Y Z 0 1 2 3 4 5 6 7 8 9';\n        $arr = explode(' ', $str);\n        do {\n            $rand_keys = array_rand($arr, $len);\n            shuffle($rand_keys);\n            $code = '';\n            foreach ($rand_keys as $index => $key) {\n                $code .= $arr[$key];\n            }\n            array_push($res, $code);\n            --$number;\n        } while ($number);\n        return $res;\n    }\n}"
  },
  {
    "path": "MineAdmin/php/app/Ai/Service/QiniuService.php",
    "content": "<?php\ndeclare(strict_types=1);\n\nnamespace App\\Ai\\Service;\n\nuse App\\Ai\\Constants\\ResponseCodeConst;\nuse App\\Ai\\Middleware\\AuthMiddleware;\nuse App\\Ai\\Constants\\UploadSceneConst;\nuse EasySwoole\\Oss\\QiNiu\\Auth;\nuse EasySwoole\\Oss\\QiNiu\\Config;\nuse Hyperf\\Context\\Context;\nuse Mine\\Exception\\NormalStatusException;\n\nclass QiniuService\n{\n    public function __construct(){\n        Config::setTimeout(3);\n        Config::setConnectTimeout(5);\n    }\n\n    public function token(string $scenes): array\n    {\n        $sceneArr = explode('-', $scenes);\n        $list = [];\n        if (is_array($sceneArr) && array_filter($sceneArr)) {\n            $config = config('file.storage.qiniu');\n            $bucket = [$config['image_bucket'], $config['audio_bucket'], $config['video_bucket']];\n            $domain = [$config['image_domain'], $config['audio_domain'], $config['video_domain']];\n            // 小程序判断\n            $schema = HelperService::isMini() ? 'https' : 'http';\n            $suffix = ['.png', '.mp3', '.mp4'];\n            $auth   = new Auth($config['accessKey'], $config['secretKey']);\n            $time   = time();\n            $env    = config('app_env');\n            $uid    = Context::get(AuthMiddleware::class . '_login_data')['id'] ?? 0;\n            foreach ($sceneArr as $key => $scene) {\n                if (!UploadSceneConst::hasScene($scene)) {\n                    throw new NormalStatusException('场景不存在~~', ResponseCodeConst::PARAM_FAILED);\n                }\n                $i                         = substr($scene, 0, 1) - 1;\n                $list[$key]['path']        = $env . '/' . HelperService::makeFileName($scene, $suffix[$i] ?? $suffix[0], UploadSceneConst::isOnly($scene) ? $uid  : 0);\n                $list[$key]['domain']      = $schema . '://' . ($domain[$i] ?? $domain[0]);\n                $list[$key]['scene']       = $scene;\n\n                /**\n                 * https://developer.qiniu.com/kodo/1671/region-endpoint-fq\n                 */\n                $list[$key]['service_url'] = $config['upload_domain'];\n                $returnBody                = '{\"key\":\"$(key)\",\"hash\":\"$(etag)\",\"fsize\":$(fsize),\"bucket\":\"$(bucket)\",\"name\":\"$(x:name)\",\"url\":\"' . $list[$key]['domain'] . '/$(key)' . (UploadSceneConst::isOnly($scene) ? '?v=' . $time : '') . '\"}';\n                $list[$key]['token']       = $auth->uploadToken($bucket[$i] ?? $bucket[0], $list[$key]['path'], 900, ['returnBody' => $returnBody]);\n            }\n        }\n        return $list;\n    }\n}"
  },
  {
    "path": "MineAdmin/php/app/Ai/config.json",
    "content": "{\n    \"name\": \"Ai\",\n    \"label\": \"ai\",\n    \"description\": \"ai\",\n    \"installed\": true,\n    \"enabled\": true,\n    \"version\": \"1.0.0\",\n    \"order\": 0\n}"
  },
  {
    "path": "MineAdmin/vue/src/api/ai/aiChatMessage.js",
    "content": "import { request } from '@/utils/request.js'\n\n/**\n * 聊天数据 API JS\n */\n\nexport default {\n\n  /**\n   * 获取聊天数据分页列表\n   * @returns\n   */\n  getList (params = {}) {\n    return request({\n      url: 'ai/chatMessage/index',\n      method: 'get',\n      params\n    })\n  },\n\n  /**\n   * 读取聊天数据\n   * @returns\n   */\n  read (data = {}) {\n    return request({\n      url: 'ai/chatMessage/read',\n      method: 'get',\n      data\n    })\n  },\n\n  /**\n   * 将聊天数据删除，有软删除则移动到回收站\n   * @returns\n   */\n  deletes (data) {\n    return request({\n      url: 'ai/chatMessage/delete',\n      method: 'delete',\n      data\n    })\n  },\n\n\n}"
  },
  {
    "path": "MineAdmin/vue/src/api/ai/aiChatSession.js",
    "content": "import { request } from '@/utils/request.js'\n\n/**\n * 问答会话 API JS\n */\n\nexport default {\n\n  /**\n   * 获取问答会话分页列表\n   * @returns\n   */\n  getList (params = {}) {\n    return request({\n      url: 'ai/chatSession/index',\n      method: 'get',\n      params\n    })\n  },\n\n  /**\n   * 读取问答会话\n   * @returns\n   */\n  read (data = {}) {\n    return request({\n      url: 'ai/chatSession/read',\n      method: 'get',\n      data\n    })\n  },\n\n  /**\n   * 将问答会话删除，有软删除则移动到回收站\n   * @returns\n   */\n  deletes (data) {\n    return request({\n      url: 'ai/chatSession/delete',\n      method: 'delete',\n      data\n    })\n  },\n\n\n}"
  },
  {
    "path": "MineAdmin/vue/src/api/ai/aiChatgptPrompts.js",
    "content": "import { request } from '@/utils/request.js'\n\n/**\n * chatgpt角色 API JS\n */\n\nexport default {\n\n  /**\n   * 获取chatgpt角色分页列表\n   * @returns\n   */\n  getList (params = {}) {\n    return request({\n      url: 'ai/chatgptPrompts/index',\n      method: 'get',\n      params\n    })\n  },\n\n  /**\n   * 更新chatgpt角色数据\n   * @returns\n   */\n  update (id, data = {}) {\n    return request({\n      url: 'ai/chatgptPrompts/update/' + id,\n      method: 'put',\n      data\n    })\n  },\n\n  /**\n   * 添加chatgpt角色\n   * @returns\n   */\n  save (data = {}) {\n    return request({\n      url: 'ai/chatgptPrompts/save',\n      method: 'post',\n      data\n    })\n  },\n\n  /**\n   * 读取chatgpt角色\n   * @returns\n   */\n  read (data = {}) {\n    return request({\n      url: 'ai/chatgptPrompts/read',\n      method: 'get',\n      data\n    })\n  },\n\n  /**\n   * 将chatgpt角色删除，有软删除则移动到回收站\n   * @returns\n   */\n  deletes (data) {\n    return request({\n      url: 'ai/chatgptPrompts/delete',\n      method: 'delete',\n      data\n    })\n  },\n\n\n}"
  },
  {
    "path": "MineAdmin/vue/src/api/ai/aiImageMaterial.js",
    "content": "import { request } from '@/utils/request.js'\n\n/**\n * 图片素材 API JS\n */\n\nexport default {\n\n  /**\n   * 获取图片素材分页列表\n   * @returns\n   */\n  getList (params = {}) {\n    return request({\n      url: 'ai/imageMaterial/index',\n      method: 'get',\n      params\n    })\n  },\n\n  /**\n   * 添加图片素材\n   * @returns\n   */\n  save (data = {}) {\n    return request({\n      url: 'ai/imageMaterial/save',\n      method: 'post',\n      data\n    })\n  },\n\n  /**\n   * 更新图片素材数据\n   * @returns\n   */\n  update (id, data = {}) {\n    return request({\n      url: 'ai/imageMaterial/update/' + id,\n      method: 'put',\n      data\n    })\n  },\n\n  /**\n   * 读取图片素材\n   * @returns\n   */\n  read (data = {}) {\n    return request({\n      url: 'ai/imageMaterial/read',\n      method: 'get',\n      data\n    })\n  },\n\n  /**\n   * 将图片素材删除，有软删除则移动到回收站\n   * @returns\n   */\n  deletes (data) {\n    return request({\n      url: 'ai/imageMaterial/delete',\n      method: 'delete',\n      data\n    })\n  },\n\n\n}"
  },
  {
    "path": "MineAdmin/vue/src/api/ai/aiMineMenu.js",
    "content": "import { request } from '@/utils/request.js'\n\n/**\n * 个人中心菜单 API JS\n */\n\nexport default {\n\n  /**\n   * 获取个人中心菜单分页列表\n   * @returns\n   */\n  getList (params = {}) {\n    return request({\n      url: 'ai/mineMenu/index',\n      method: 'get',\n      params\n    })\n  },\n\n  /**\n   * 添加个人中心菜单\n   * @returns\n   */\n  save (data = {}) {\n    return request({\n      url: 'ai/mineMenu/save',\n      method: 'post',\n      data\n    })\n  },\n\n  /**\n   * 更新个人中心菜单数据\n   * @returns\n   */\n  update (id, data = {}) {\n    return request({\n      url: 'ai/mineMenu/update/' + id,\n      method: 'put',\n      data\n    })\n  },\n\n  /**\n   * 读取个人中心菜单\n   * @returns\n   */\n  read (data = {}) {\n    return request({\n      url: 'ai/mineMenu/read',\n      method: 'get',\n      data\n    })\n  },\n\n  /**\n   * 将个人中心菜单删除，有软删除则移动到回收站\n   * @returns\n   */\n  deletes (data) {\n    return request({\n      url: 'ai/mineMenu/delete',\n      method: 'delete',\n      data\n    })\n  },\n\n\n}"
  },
  {
    "path": "MineAdmin/vue/src/api/ai/aiMineMenuGroup.js",
    "content": "import { request } from '@/utils/request.js'\n\n/**\n * 个人中心菜单分组 API JS\n */\n\nexport default {\n\n  /**\n   * 获取个人中心菜单分组分页列表\n   * @returns\n   */\n  getList (params = {}) {\n    return request({\n      url: 'ai/mineMenuGroup/index',\n      method: 'get',\n      params\n    })\n  },\n\n  /**\n   * 添加个人中心菜单分组\n   * @returns\n   */\n  save (data = {}) {\n    return request({\n      url: 'ai/mineMenuGroup/save',\n      method: 'post',\n      data\n    })\n  },\n\n  /**\n   * 更新个人中心菜单分组数据\n   * @returns\n   */\n  update (id, data = {}) {\n    return request({\n      url: 'ai/mineMenuGroup/update/' + id,\n      method: 'put',\n      data\n    })\n  },\n\n  /**\n   * 读取个人中心菜单分组\n   * @returns\n   */\n  read (data = {}) {\n    return request({\n      url: 'ai/mineMenuGroup/read',\n      method: 'get',\n      data\n    })\n  },\n\n  /**\n   * 将个人中心菜单分组删除，有软删除则移动到回收站\n   * @returns\n   */\n  deletes (data) {\n    return request({\n      url: 'ai/mineMenuGroup/delete',\n      method: 'delete',\n      data\n    })\n  },\n\n\n}"
  },
  {
    "path": "MineAdmin/vue/src/api/ai/aiOpenaiKey.js",
    "content": "import { request } from '@/utils/request.js'\n\n/**\n * openai_key API JS\n */\n\nexport default {\n\n  /**\n   * 获取openai_key分页列表\n   * @returns\n   */\n  getList (params = {}) {\n    return request({\n      url: 'ai/openKey/index',\n      method: 'get',\n      params\n    })\n  },\n\n  /**\n   * 添加openai_key\n   * @returns\n   */\n  save (data = {}) {\n    return request({\n      url: 'ai/openKey/save',\n      method: 'post',\n      data\n    })\n  },\n\n  /**\n   * 更新openai_key数据\n   * @returns\n   */\n  update (id, data = {}) {\n    return request({\n      url: 'ai/openKey/update/' + id,\n      method: 'put',\n      data\n    })\n  },\n\n  /**\n   * 读取openai_key\n   * @returns\n   */\n  read (data = {}) {\n    return request({\n      url: 'ai/openKey/read',\n      method: 'get',\n      data\n    })\n  },\n\n  /**\n   * 将openai_key删除，有软删除则移动到回收站\n   * @returns\n   */\n  deletes (data) {\n    return request({\n      url: 'ai/openKey/delete',\n      method: 'delete',\n      data\n    })\n  },\n\n  batchAdd (data) {\n    return request({\n      url: 'ai/openKey/batch-add',\n      method: 'post',\n      data\n    })\n  },\n\n  refreshCache () {\n    return request({\n      url: 'ai/openKey/refresh-cache-list',\n      method: 'post'\n    })\n  },\n\n}"
  },
  {
    "path": "MineAdmin/vue/src/api/ai/aiOrder.js",
    "content": "import { request } from '@/utils/request.js'\n\n/**\n * 订单表 API JS\n */\n\nexport default {\n\n  /**\n   * 获取订单表分页列表\n   * @returns\n   */\n  getList (params = {}) {\n    return request({\n      url: 'ai/order/index',\n      method: 'get',\n      params\n    })\n  },\n\n  /**\n   * 读取订单表\n   * @returns\n   */\n  read (data = {}) {\n    return request({\n      url: 'ai/order/read',\n      method: 'get',\n      data\n    })\n  },\n\n  /**\n   * 将订单表删除，有软删除则移动到回收站\n   * @returns\n   */\n  deletes (data) {\n    return request({\n      url: 'ai/order/delete',\n      method: 'delete',\n      data\n    })\n  },\n\n\n}"
  },
  {
    "path": "MineAdmin/vue/src/api/ai/aiPayKami.js",
    "content": "import { request } from '@/utils/request.js'\n\n/**\n * 卡密 API JS\n */\n\nexport default {\n\n    /**\n     * 获取卡密分页列表\n     * @returns\n     */\n    getList (params = {}) {\n        return request({\n            url: 'ai/payKami/index',\n            method: 'get',\n            params\n        })\n    },\n\n    /**\n     * 读取卡密\n     * @returns\n     */\n    read (data = {}) {\n        return request({\n            url: 'ai/payKami/read',\n            method: 'get',\n            data\n        })\n    },\n\n    /**\n     * 更新卡密数据\n     * @returns\n     */\n    update (id, data = {}) {\n        return request({\n            url: 'ai/payKami/update/' + id,\n            method: 'put',\n            data\n        })\n    },\n\n    /**\n     * 将卡密删除，有软删除则移动到回收站\n     * @returns\n     */\n    deletes (data) {\n        return request({\n            url: 'ai/payKami/delete',\n            method: 'delete',\n            data\n        })\n    },\n\n    /**\n     * 将卡密删除，有软删除则移动到回收站\n     * @returns\n     */\n    add (data) {\n        return request({\n            url: 'ai/payKami/add',\n            method: 'post',\n            data\n        })\n    },\n}"
  },
  {
    "path": "MineAdmin/vue/src/api/ai/aiQuickIssue.js",
    "content": "import { request } from '@/utils/request.js'\n\n/**\n * 快捷问题\n API JS\n */\n\nexport default {\n\n  /**\n   * 获取快捷问题\n分页列表\n   * @returns\n   */\n  getList (params = {}) {\n    return request({\n      url: 'ai/quickIssue/index',\n      method: 'get',\n      params\n    })\n  },\n\n  /**\n   * 更新快捷问题\n数据\n   * @returns\n   */\n  update (id, data = {}) {\n    return request({\n      url: 'ai/quickIssue/update/' + id,\n      method: 'put',\n      data\n    })\n  },\n\n  /**\n   * 添加快捷问题\n\n   * @returns\n   */\n  save (data = {}) {\n    return request({\n      url: 'ai/quickIssue/save',\n      method: 'post',\n      data\n    })\n  },\n\n  /**\n   * 读取快捷问题\n\n   * @returns\n   */\n  read (data = {}) {\n    return request({\n      url: 'ai/quickIssue/read',\n      method: 'get',\n      data\n    })\n  },\n\n  /**\n   * 将快捷问题\n删除，有软删除则移动到回收站\n   * @returns\n   */\n  deletes (data) {\n    return request({\n      url: 'ai/quickIssue/delete',\n      method: 'delete',\n      data\n    })\n  },\n\n\n}"
  },
  {
    "path": "MineAdmin/vue/src/api/ai/aiSetting.js",
    "content": "import { request } from '@/utils/request.js'\n\n/**\n * 用户主表 API JS\n */\n\nexport default {\n\n    /**\n     * 获取用户主表分页列表\n     * @returns\n     */\n    getList (params = {}) {\n        return request({\n            url: 'ai/setting/index',\n            method: 'get',\n            params\n        })\n    },\n\n    /**\n     * 更新用户主表数据\n     * @returns\n     */\n    save (data = {}) {\n        return request({\n            url: 'ai/setting/save',\n            method: 'post',\n            data\n        })\n    },\n\n    getQiniuToken(scenes= \"\") {\n        let params = {scenes: scenes}\n        return request({\n            url: 'ai/setting/upload-token',\n            method: 'get',\n            params\n        })\n    }\n}"
  },
  {
    "path": "MineAdmin/vue/src/api/ai/aiUser.js",
    "content": "import { request } from '@/utils/request.js'\n\n/**\n * 用户主表 API JS\n */\n\nexport default {\n\n  /**\n   * 获取用户主表分页列表\n   * @returns\n   */\n  getList (params = {}) {\n    return request({\n      url: 'ai/user/index',\n      method: 'get',\n      params\n    })\n  },\n\n  /**\n   * 更新用户主表数据\n   * @returns\n   */\n  update (id, data = {}) {\n    return request({\n      url: 'ai/user/update/' + id,\n      method: 'put',\n      data\n    })\n  },\n\n  /**\n   * 读取用户主表\n   * @returns\n   */\n  read (data = {}) {\n    return request({\n      url: 'ai/user/read',\n      method: 'get',\n      data\n    })\n  },\n\n  /**\n   * 将用户主表删除，有软删除则移动到回收站\n   * @returns\n   */\n  deletes (data) {\n    return request({\n      url: 'ai/user/delete',\n      method: 'delete',\n      data\n    })\n  },\n\n\n  /**\n   * 将用户主表删除，有软删除则移动到回收站\n   * @returns\n   */\n  openVip(id, data) {\n    return request({\n      url: 'ai/user/open-vip/' + id,\n      method: 'post',\n      data\n    })\n  },\n\n  /**\n   * 锁定 or 解锁\n   * @returns\n   */\n  lock(id) {\n    return request({\n      url: 'ai/user/lock/' + id,\n      method: 'post'\n    })\n  },\n\n}"
  },
  {
    "path": "MineAdmin/vue/src/components/putyy/pt-upload.vue",
    "content": "<template>\n  <a-space direction=\"vertical\" :style=\"{ width: '100%' }\">\n    <a-upload v-if=\"uploadType === 'image'\" :custom-request=\"customRequest\" :accept=\"props.component.accept\" @before-remove=\"beforeRemove\" :limit=\"1\" :file-list=\"ptFileList\" list-type=\"picture-card\" image-preview/>\n    <a-upload v-else-if=\"uploadType === 'audio'\" :custom-request=\"customRequest\" :accept=\"props.component.accept\" @before-remove=\"beforeRemove\" :file-list=\"ptFileList\" :limit=\"1\" :show-cancel-button=\"false\">\n\n    </a-upload>\n\n    <a-upload v-else-if=\"uploadType === 'video'\" :custom-request=\"customRequest\" :accept=\"props.component.accept\" @before-remove=\"beforeRemove\" :file-list=\"ptFileList\" :limit=\"1\" :show-cancel-button=\"false\">\n\n    </a-upload>\n  </a-space>\n</template>\n\n<script setup>\nimport {ref, inject, onMounted, watch} from 'vue'\nimport {Message} from '@arco-design/web-vue'\n\nconst props = defineProps({\n  component: Object,\n  customField: { type: String, default: undefined }\n})\n\nconst formModel = inject('formModel')\n\n// 当前字段名\nlet dataIndex = props.customField ?? props.component.dataIndex\n\nconst maxFileSize = props.component.hasOwnProperty('ptMaxFileSize') ? props.component['ptMaxFileSize'] : 0\n\nlet uploadType = props.component.accept.slice(0, 5)\n\nlet ptFileList = ref([])\nlet sourceUrl = ref('')\n\nonMounted(()=>{\n  // 追加fileList属性\n  formModel.value.ptFileList = formModel.value.ptFileList ? formModel.value.ptFileList : []\n\n  if (formModel.value[dataIndex]) {\n    ptFileList.value = [{\n      name: formModel.value[dataIndex],\n      url: formModel.value[dataIndex]\n    }]\n    sourceUrl.value = formModel.value[dataIndex]\n  }\n})\n\nwatch(formModel, newValue => {\n  ptFileList.value = [{\n    name: formModel.value[dataIndex],\n    url: formModel.value[dataIndex]\n  }]\n  sourceUrl.value = formModel.value[dataIndex]\n})\n\n\nconst duration = ref(0)\n\nlet customRequest = option => {\n  if (!props.component.ptScene) {\n    Message.error('请填入场景值')\n  }\n  if (maxFileSize && option.fileItem.file.size > maxFileSize) {\n    Message.error('文件大小不能超过' + (maxFileSize / 1024 / 1024) + 'M')\n    return\n  }\n\n  formModel.value.ptFileList[dataIndex] = {\n    file: option.fileItem.file,\n    url: window.URL.createObjectURL(option.fileItem.file),\n    name: dataIndex,\n    dataIndex: dataIndex,\n    size: option.fileItem.file.size,\n    duration: 0,\n    type: 'image',\n    ptScene: props.component.ptScene\n  }\n\n  ptFileList.value = [{\n    name: dataIndex,\n    url: formModel.value.ptFileList[dataIndex].url\n  }]\n  sourceUrl.value = formModel.value.ptFileList[dataIndex].url\n  if (uploadType.value !== 'image') {\n    let videoElement = new Audio(formModel.value.ptFileList[dataIndex].url)\n    videoElement.addEventListener('loadedmetadata', event => {\n      duration.value = videoElement.duration\n    })\n  }\n}\n\nlet beforeRemove = file => {\n  formModel.value.ptFileList[dataIndex] = []\n  ptFileList.value = []\n  sourceUrl.value = ''\n}\n</script>\n\n<style scoped lang=\"less\">\n</style>\n"
  },
  {
    "path": "MineAdmin/vue/src/config/pt-const.js",
    "content": "export default {\n    is_lock: [\n        {\n            label: \"正常\",\n            value: 1\n        },\n        {\n            label: \"锁定\",\n            value: 2\n        },\n    ],\n    ai_vip: [\n        {\n            label: \"免费会员\",\n            value: 0\n        }, {\n            label: \"VIP\",\n            value: 10\n        }, {\n            label: \"1星VIP\",\n            value: 20\n        }, {\n            label: \"2星VIP\",\n            value: 30\n        },\n    ],\n};"
  },
  {
    "path": "MineAdmin/vue/src/config/pt-scene.js",
    "content": "export default {\n    ai_head_img: '1010',\n    ai_mine_menu_icon: '1011',\n    ai_customer_wx_img: '1012',\n    ai_customer_head_img: '1013',\n    ai_image_materialg: '1014',\n};"
  },
  {
    "path": "MineAdmin/vue/src/utils/pt-upload.js",
    "content": "import ai from \"@/api/ai/aiSetting\";\nimport * as qiniu from \"qiniu-js\";\n\n\nlet uploadQiniu  = (blob,key,token)=>{\n    return new Promise(function (resolve, reject) {\n        let observable = qiniu.upload(blob, key, token)\n        let observer = {\n            next (res) {\n            },\n            error (err) {\n                reject(err)\n            },\n            complete (data) {\n                resolve(data)\n            }\n        }\n        observable.subscribe(observer)\n    })\n}\n\nexport const uploadQiniuHandler = async (files) => {\n    let sceneArr = []\n    for (let dataIndex in files) {\n        sceneArr.push(files[dataIndex].ptScene)\n    }\n\n    if (sceneArr.length <= 0) {\n        return true\n    }\n\n    // 获取token\n    let sceneResArr = []\n    await ai.getQiniuToken(sceneArr.join(\"-\")).then((res)=>{\n        sceneResArr = res.data\n    })\n    for (let key in sceneResArr) {\n        for (let dataIndex in files) {\n            if (sceneResArr[key].scene === files[dataIndex].ptScene) {\n                await uploadQiniu(files[dataIndex].file, sceneResArr[key].path,sceneResArr[key].token).then((res) => {\n                    files[dataIndex].source_url = res.url\n                })\n            }\n        }\n    }\n    return files\n}"
  },
  {
    "path": "MineAdmin/vue/src/views/ai/chatMessage/index.vue",
    "content": "<template>\n  <div class=\"ma-content-block lg:flex justify-between p-4\">\n    <!-- CRUD 组件 -->\n    <ma-crud :options=\"options\" :columns=\"columns\" ref=\"crudRef\">\n    </ma-crud>\n  </div>\n</template>\n<script setup>\nimport { ref, reactive } from 'vue'\nimport aiChatMessage from '@/api/ai/aiChatMessage'\nimport { Message } from '@arco-design/web-vue'\nimport tool from '@/utils/tool'\nimport * as common from '@/utils/common'\n\nconst crudRef = ref()\n\nconst options = reactive({\n  id: 'by_ai_chat_message',\n  rowSelection: {\n    showCheckedAll: true\n  },\n  pk: 'id',\n  operationColumn: true,\n  operationWidth: 160,\n  formOption: {\n    viewType: 'modal',\n    width: 600\n  },\n  api: aiChatMessage.getList,\n  delete: {\n    show: true,\n    api: aiChatMessage.deletes,\n    auth: ['ai:chatMessage:delete']\n  }\n})\n\nconst columns = reactive([\n  {\n    title: \"主键\",\n    dataIndex: \"id\",\n    formType: \"input\",\n    addDisplay: false,\n    editDisplay: false,\n    commonRules: {\n      required: true,\n      message: \"请输入主键\"\n    }\n  },\n  {\n    title: \"会话ID\",\n    dataIndex: \"sid\",\n    formType: \"input\",\n    search: true,\n    addDisplay: false,\n    editDisplay: false,\n    commonRules: {\n      required: true,\n      message: \"请输入会话ID\"\n    }\n  },\n  {\n    title: \"内容\",\n    dataIndex: \"content\",\n    formType: \"editor\",\n    search: true,\n    addDisplay: false,\n    editDisplay: false\n  },\n  {\n    title: \"回复内容\",\n    dataIndex: \"reply_content\",\n    formType: \"editor\",\n    search: true,\n    addDisplay: false,\n    editDisplay: false\n  },\n  {\n    title: \"回复时间\",\n    dataIndex: \"reply_at\",\n    formType: \"date\",\n    search: true,\n    addDisplay: false,\n    editDisplay: false,\n    showTime: true\n  },\n  {\n    title: \"创建时间\",\n    dataIndex: \"created_at\",\n    formType: \"date\",\n    search: true,\n    addDisplay: false,\n    editDisplay: false,\n    showTime: true\n  },\n  {\n    title: \"删除时间\",\n    dataIndex: \"deleted_at\",\n    formType: \"date\",\n    addDisplay: false,\n    editDisplay: false,\n    hide: true,\n    showTime: true\n  }\n])\n</script>\n<script> export default { name: 'ai:chatMessage' } </script>"
  },
  {
    "path": "MineAdmin/vue/src/views/ai/chatSession/index.vue",
    "content": "<template>\n  <div class=\"ma-content-block lg:flex justify-between p-4\">\n    <!-- CRUD 组件 -->\n    <ma-crud :options=\"options\" :columns=\"columns\" ref=\"crudRef\">\n    </ma-crud>\n  </div>\n</template>\n<script setup>\nimport { ref, reactive } from 'vue'\nimport aiChatSession from '@/api/ai/aiChatSession'\nimport { Message } from '@arco-design/web-vue'\nimport tool from '@/utils/tool'\nimport * as common from '@/utils/common'\n\nconst crudRef = ref()\n\n\n\n\nconst options = reactive({\n  id: 'by_ai_chat_session',\n  rowSelection: {\n    showCheckedAll: true\n  },\n  pk: 'id',\n  operationColumn: true,\n  operationWidth: 160,\n  formOption: {\n    viewType: 'modal',\n    width: 600\n  },\n  api: aiChatSession.getList,\n  delete: {\n    show: true,\n    api: aiChatSession.deletes,\n    auth: ['ai:chatSession:delete']\n  }\n})\n\nconst columns = reactive([\n  {\n    title: \"主键\",\n    dataIndex: \"id\",\n    formType: \"input\",\n    search: true,\n    addDisplay: false,\n    editDisplay: false,\n    sortable: {\n      sortDirections: [\n        \"ascend\",\n        \"descend\"\n      ],\n      sorter: true\n    }\n  },\n  {\n    title: \"用户uid\",\n    dataIndex: \"uid\",\n    formType: \"input\",\n    search: true,\n    addDisplay: false,\n    editDisplay: false\n  },\n  {\n    title: \"模型ID\",\n    dataIndex: \"prompt_id\",\n    formType: \"input\",\n    search: true,\n    addDisplay: false,\n    editDisplay: false\n  },\n  {\n    title: \"是否关闭\",\n    dataIndex: \"share\",\n    formType: \"select\",\n    search: true,\n    addDisplay: false,\n    editDisplay: false,\n    dict: {\n      data: [\n        {\n          label: \"正常\",\n          value: 1\n        }, {\n          label: \"关闭\",\n          value: 2\n        }\n      ],\n      translation: true\n    },\n  },\n  {\n    title: \"是否分享\",\n    dataIndex: \"share\",\n    formType: \"select\",\n    search: true,\n    addDisplay: false,\n    editDisplay: false,\n    dict: {\n      data: [\n        {\n          label: \"关闭\",\n          value: 1\n        }, {\n          label: \"公开\",\n          value: 2\n        }\n      ],\n      translation: true\n    },\n  },\n  {\n    title: \"创建时间\",\n    dataIndex: \"created_at\",\n    formType: \"date\",\n    addDisplay: false,\n    editDisplay: false,\n    hide: true,\n    showTime: true\n  }\n])\n</script>\n<script> export default { name: 'ai:chatSession' } </script>"
  },
  {
    "path": "MineAdmin/vue/src/views/ai/chatgptPrompts/index.vue",
    "content": "<template>\n  <div class=\"ma-content-block lg:flex justify-between p-4\">\n    <!-- CRUD 组件 -->\n    <ma-crud :options=\"options\" :columns=\"columns\" ref=\"crudRef\">\n    </ma-crud>\n  </div>\n</template>\n<script setup>\nimport { ref, reactive } from 'vue'\nimport aiChatgptPrompts from '@/api/ai/aiChatgptPrompts'\nimport { Message } from '@arco-design/web-vue'\nimport tool from '@/utils/tool'\nimport * as common from '@/utils/common'\n\nconst crudRef = ref()\n\n\n\n\nconst options = reactive({\n  id: 'by_ai_chatgpt_prompts',\n  rowSelection: {\n    showCheckedAll: true\n  },\n  pk: 'id',\n  operationColumn: true,\n  operationWidth: 160,\n  formOption: {\n    viewType: 'modal',\n    width: 600\n  },\n  api: aiChatgptPrompts.getList,\n  add: {\n    show: true,\n    api: aiChatgptPrompts.save,\n    auth: ['ai:chatgptPrompts:save']\n  },\n  edit: {\n    show: true,\n    api: aiChatgptPrompts.update,\n    auth: ['ai:chatgptPrompts:update']\n  },\n  delete: {\n    show: true,\n    api: aiChatgptPrompts.deletes,\n    auth: ['ai:chatgptPrompts:delete']\n  }\n})\n\nconst columns = reactive([\n  {\n    title: \"主键\",\n    dataIndex: \"id\",\n    formType: \"input\",\n    addDisplay: false,\n    editDisplay: false,\n    hide: true,\n    commonRules: {\n      required: true,\n      message: \"请输入主键\"\n    },\n    sortable: {\n      sortDirections: [\n        \"ascend\",\n        \"descend\"\n      ],\n      sorter: true\n    }\n  },\n  {\n    title: \"角色名称\",\n    dataIndex: \"act\",\n    formType: \"input\",\n    search: true,\n    commonRules: {\n      required: true,\n      message: \"请输入角色名称\"\n    }\n  },\n  {\n    title: \"角色说明\",\n    dataIndex: \"prompt\",\n    formType: \"textarea\",\n    search: true,\n    commonRules: {\n      required: true,\n      message: \"请输入角色说明\"\n    }\n  },\n  {\n    title: \"排序\",\n    dataIndex: \"sort\",\n    formType: \"input\",\n    commonRules: {\n      required: true,\n      message: \"请输入排序\"\n    },\n    sortable: {\n      sortDirections: [\n        \"ascend\",\n        \"descend\"\n      ],\n      sorter: true\n    }\n  },\n  {\n    title: \"创建者\",\n    dataIndex: \"created_by\",\n    formType: \"input\",\n    addDisplay: false,\n    editDisplay: false,\n    hide: true\n  },\n  {\n    title: \"更新者\",\n    dataIndex: \"updated_by\",\n    formType: \"input\",\n    addDisplay: false,\n    editDisplay: false,\n    hide: true\n  },\n  {\n    title: \"创建时间\",\n    dataIndex: \"created_at\",\n    formType: \"date\",\n    addDisplay: false,\n    editDisplay: false,\n    showTime: true\n  },\n  {\n    title: \"更新时间\",\n    dataIndex: \"updated_at\",\n    formType: \"date\",\n    addDisplay: false,\n    editDisplay: false,\n    showTime: true\n  },\n  {\n    title: \"删除时间\",\n    dataIndex: \"deleted_at\",\n    formType: \"date\",\n    addDisplay: false,\n    editDisplay: false,\n    hide: true,\n    showTime: true\n  },\n  {\n    title: \"备注\",\n    dataIndex: \"remark\",\n    formType: \"input\",\n    addDisplay: false,\n    editDisplay: false,\n    hide: true\n  }\n])\n</script>\n<script> export default { name: 'ai:chatgptPrompts' } </script>"
  },
  {
    "path": "MineAdmin/vue/src/views/ai/imageMaterial/index.vue",
    "content": "<template>\n  <div class=\"ma-content-block lg:flex justify-between p-4\">\n    <!-- CRUD 组件 -->\n    <ma-crud :options=\"options\" :columns=\"columns\" ref=\"crudRef\">\n      <template #img_url=\"{ record }\">\n        <img :src=\"record.img_url\" style=\"object-fit: cover\"/>\n      </template>\n    </ma-crud>\n  </div>\n</template>\n<script setup>\nimport {ref, reactive, shallowRef} from 'vue'\nimport aiImageMaterial from '@/api/ai/aiImageMaterial'\nimport { Message } from '@arco-design/web-vue'\nimport tool from '@/utils/tool'\nimport * as common from '@/utils/common'\nimport {uploadQiniuHandler} from \"@/utils/pt-upload\";\nimport ptScene from \"@/config/pt-scene\";\nimport ptUpload from '@/components/putyy/pt-upload.vue'\n\nconst crudRef = ref()\n\nconst options = reactive({\n  id: 'by_ai_image_material',\n  rowSelection: {\n    showCheckedAll: true\n  },\n  pk: 'id',\n  operationColumn: true,\n  operationWidth: 160,\n  formOption: {\n    viewType: 'modal',\n    width: 600\n  },\n  api: aiImageMaterial.getList,\n  add: {\n    show: true,\n    api: aiImageMaterial.save,\n    auth: ['ai:imageMaterial:save']\n  },\n  edit: {\n    show: true,\n    api: aiImageMaterial.update,\n    auth: ['ai:imageMaterial:update']\n  },\n  delete: {\n    show: true,\n    api: aiImageMaterial.deletes,\n    auth: ['ai:imageMaterial:delete']\n  },\n\n  beforeAdd: async (form) => {\n    let files = await uploadQiniuHandler(form.ptFileList)\n    if (files === true) {\n      return\n    }\n    for (let dataIndex in files) {\n      if (form.hasOwnProperty(dataIndex)) {\n        form[dataIndex] = files[dataIndex].source_url\n      }\n    }\n    delete form.ptFileList\n  },\n  beforeEdit: async (form) => {\n    let files = await uploadQiniuHandler(form.ptFileList)\n    if (files === true) {\n      return\n    }\n    for (let dataIndex in files) {\n      if (form.hasOwnProperty(dataIndex)) {\n        form[dataIndex] = files[dataIndex].source_url\n      }\n    }\n    delete form.ptFileList\n  },\n})\n\nconst columns = reactive([\n  {\n    title: \"主键\",\n    dataIndex: \"id\",\n    formType: \"input\",\n    addDisplay: false,\n    editDisplay: false,\n    hide: true,\n    commonRules: {\n      required: true,\n      message: \"请输入主键\"\n    },\n    sortable: {\n      sortDirections: [\n        \"ascend\",\n        \"descend\"\n      ],\n      sorter: true\n    }\n  },\n  {\n    title: \"使用场景\",\n    dataIndex: \"scene\",\n    formType: \"select\",\n    search: true,\n    dict: {\n      data: [\n        {\n          label: \"个人中心\",\n          value: 1\n        },\n      ],\n      translation: true\n    },\n  },\n  {\n    title: \"图片地址\",\n    dataIndex: \"img_url\",\n    formType: 'component',\n    component: shallowRef(ptUpload),\n    accept: 'image/*',\n    ptScene: ptScene.ai_image_materialg\n  },\n  {\n    title: \"跳转地址\",\n    dataIndex: \"url\",\n    formType: \"input\",\n    commonRules: {\n      required: true,\n      message: \"请输入跳转地址\"\n    }\n  },\n  {\n    title: \"备注\",\n    dataIndex: \"remark\",\n    formType: \"input\",\n    search: true,\n    hide: true,\n    commonRules: {\n      required: true,\n      message: \"请输入备注\"\n    }\n  },\n  {\n    title: \"排序\",\n    dataIndex: \"sort\",\n    formType: \"input\",\n    hide: false,\n    commonRules: {\n      required: true,\n      message: \"请输入排序\"\n    },\n    sortable: {\n      sortDirections: [\n        \"ascend\",\n        \"descend\"\n      ],\n      sorter: true\n    }\n  },\n  {\n    title: \"创建者\",\n    dataIndex: \"created_by\",\n    formType: \"input\",\n    addDisplay: false,\n    editDisplay: false,\n    hide: true,\n    commonRules: {\n      required: true,\n      message: \"请输入创建者\"\n    }\n  },\n  {\n    title: \"更新者\",\n    dataIndex: \"updated_by\",\n    formType: \"input\",\n    addDisplay: false,\n    editDisplay: false,\n    hide: true,\n    commonRules: {\n      required: true,\n      message: \"请输入更新者\"\n    }\n  },\n  {\n    title: \"使用开始时间\",\n    dataIndex: \"start_at\",\n    formType: \"date\",\n    search: true,\n    commonRules: {\n      required: true,\n      message: \"请输入使用开始时间\"\n    },\n    showTime: true\n  },\n  {\n    title: \"使用结束时间\",\n    dataIndex: \"end_at\",\n    formType: \"date\",\n    search: true,\n    commonRules: {\n      required: true,\n      message: \"请输入使用结束时间\"\n    },\n    showTime: true\n  },\n  {\n    title: \"创建时间\",\n    dataIndex: \"created_at\",\n    formType: \"date\",\n    search: true,\n    addDisplay: false,\n    editDisplay: false,\n    showTime: true\n  },\n  {\n    title: \"更新时间\",\n    dataIndex: \"updated_at\",\n    formType: \"date\",\n    addDisplay: false,\n    editDisplay: false,\n    hide: true,\n    showTime: true\n  },\n  {\n    title: \"删除时间\",\n    dataIndex: \"deleted_at\",\n    formType: \"date\",\n    addDisplay: false,\n    editDisplay: false,\n    hide: true,\n    showTime: true\n  }\n])\n</script>\n<script> export default { name: 'ai:imageMaterial' } </script>"
  },
  {
    "path": "MineAdmin/vue/src/views/ai/mineMenu/index.vue",
    "content": "<template>\n  <div class=\"ma-content-block lg:flex justify-between p-4\">\n    <!-- CRUD 组件 -->\n    <ma-crud :options=\"options\" :columns=\"columns\" ref=\"crudRef\">\n      <template #icon=\"{ record }\">\n        <img :src=\"record.icon\" style=\"object-fit: cover\"/>\n      </template>\n    </ma-crud>\n  </div>\n</template>\n<script setup>\nimport {ref, reactive, shallowRef, onMounted, onBeforeMount} from 'vue'\nimport aiMineMenu from '@/api/ai/aiMineMenu'\nimport { Message } from '@arco-design/web-vue'\nimport tool from '@/utils/tool'\nimport * as common from '@/utils/common'\nimport ptUpload from '@/components/putyy/pt-upload.vue'\nimport ptScene from \"@/config/pt-scene\";\nimport ptConst from \"@/config/pt-const\";\nimport aiMineMenuGroup from \"@/api/ai/aiMineMenuGroup\";\nimport {uploadQiniuHandler} from \"@/utils/pt-upload\";\n\nconst crudRef = ref()\n\nconst options = reactive({\n  id: 'by_ai_mine_menu',\n  rowSelection: {\n    showCheckedAll: true\n  },\n  pk: 'id',\n  operationColumn: true,\n  operationWidth: 160,\n  formOption: {\n    viewType: 'modal',\n    width: 600\n  },\n  api: aiMineMenu.getList,\n  add: {\n    show: true,\n    api: aiMineMenu.save,\n    auth: ['ai:mineMenu:save']\n  },\n  edit: {\n    show: true,\n    api: aiMineMenu.update,\n    auth: ['ai:mineMenu:update']\n  },\n  delete: {\n    show: true,\n    api: aiMineMenu.deletes,\n    auth: ['ai:mineMenu:delete']\n  },\n\n  beforeAdd: async (form) => {\n    let files = await uploadQiniuHandler(form.ptFileList)\n    if (files === true) {\n      return\n    }\n    for (let dataIndex in files) {\n      if (form.hasOwnProperty(dataIndex)) {\n        form[dataIndex] = files[dataIndex].source_url\n      }\n    }\n    delete form.ptFileList\n  },\n  beforeEdit: async (form) => {\n    let files = await uploadQiniuHandler(form.ptFileList)\n    if (files === true) {\n      return\n    }\n    for (let dataIndex in files) {\n      if (form.hasOwnProperty(dataIndex)) {\n        form[dataIndex] = files[dataIndex].source_url\n      }\n    }\n    delete form.ptFileList\n  },\n})\n\nconst columns = reactive([\n  {\n    title: \"主键\",\n    dataIndex: \"id\",\n    formType: \"input\",\n    addDisplay: false,\n    editDisplay: false,\n    commonRules: {\n      required: true,\n      message: \"请输入主键\"\n    },\n    sortable: {\n      sortDirections: [\n        \"ascend\",\n        \"descend\"\n      ],\n      sorter: true\n    }\n  },\n  {\n    title: \"分组\",\n    dataIndex: \"gid\",\n    formType: \"select\",\n    dict: {\n      props:  { label: 'name', value: 'id' },\n      data: async () => {\n        let res = await aiMineMenuGroup.getList({pageSize: 999})\n        return res.data.items\n      },\n      translation: true\n    }\n  },\n  {\n    title: \"菜单名称\",\n    dataIndex: \"name\",\n    formType: \"input\",\n    search: true\n  },\n  {\n    title: \"排序\",\n    dataIndex: \"sort\",\n    formType: \"input\",\n    sortable: {\n      sortDirections: [\n        \"ascend\",\n        \"descend\"\n      ],\n      sorter: true\n    }\n  },\n  {\n    title: \"使用权限限制 0全部\",\n    dataIndex: \"use_vip\",\n    formType: \"select\",\n    dict: {\n      data: ptConst.ai_vip,\n      translation: true\n    },\n  },\n  {\n    title: \"点击类型 1跳转 2调用函数\",\n    dataIndex: \"click_type\",\n    formType: \"select\",\n    dict: {\n      data: [\n        {\n          label: \"跳转\",\n          value: 1\n        }, {\n          label: \"调用函数\",\n          value: 2\n        },\n      ],\n      translation: true\n    },\n  },\n  {\n    title: \"函数标识 小程序端提前封装\",\n    dataIndex: \"click_func\",\n    formType: \"input\"\n  },\n  {\n    title: \"打开的页面路径\",\n    dataIndex: \"path\",\n    formType: \"input\"\n  },\n  {\n    title: \"小程序appid\",\n    dataIndex: \"app_id\",\n    formType: \"input\"\n  },\n  {\n    title: \"需要传递给目标小程序的数据 json\",\n    dataIndex: \"extra_data\",\n    formType: \"input\"\n  },\n  {\n    title: \"要打开的小程序版本\",\n    dataIndex: \"env_version\",\n    formType: \"input\"\n  },\n  {\n    title: \"小程序链接\",\n    dataIndex: \"short_link\",\n    formType: \"input\"\n  },\n  {\n    title: \"图标\",\n    dataIndex: \"icon\",\n    formType: 'component',\n    component: shallowRef(ptUpload),\n    accept: 'image/*',\n    ptScene: ptScene.ai_mine_menu_icon\n  },\n  {\n    title: \"是否锁定\",\n    dataIndex: \"is_lock\",\n    formType: \"select\",\n    search: true,\n    dict: {\n      data: ptConst.is_lock,\n      translation: true\n    },\n  },\n  {\n    title: \"创建时间\",\n    dataIndex: \"created_at\",\n    formType: \"date\",\n    search: true,\n    addDisplay: false,\n    editDisplay: false,\n    showTime: true\n  },\n  {\n    title: \"更新时间\",\n    dataIndex: \"updated_at\",\n    formType: \"date\",\n    search: true,\n    addDisplay: false,\n    editDisplay: false,\n    showTime: true\n  },\n  {\n    title: \"删除时间\",\n    dataIndex: \"deleted_at\",\n    formType: \"date\",\n    addDisplay: false,\n    editDisplay: false,\n    hide: true,\n    showTime: true\n  }\n])\n</script>\n<script> export default { name: 'ai:mineMenu' } </script>"
  },
  {
    "path": "MineAdmin/vue/src/views/ai/mineMenuGroup/index.vue",
    "content": "<template>\n  <div class=\"ma-content-block lg:flex justify-between p-4\">\n    <!-- CRUD 组件 -->\n    <ma-crud :options=\"options\" :columns=\"columns\" ref=\"crudRef\">\n    </ma-crud>\n  </div>\n</template>\n<script setup>\nimport { ref, reactive } from 'vue'\nimport aiMineMenuGroup from '@/api/ai/aiMineMenuGroup'\nimport { Message } from '@arco-design/web-vue'\nimport tool from '@/utils/tool'\nimport * as common from '@/utils/common'\nimport ptConst from \"@/config/pt-const\";\n\nconst crudRef = ref()\n\n\n\n\nconst options = reactive({\n  id: 'by_ai_mine_menu_group',\n  rowSelection: {\n    showCheckedAll: true\n  },\n  pk: 'id',\n  operationColumn: true,\n  operationWidth: 160,\n  formOption: {\n    viewType: 'modal',\n    width: 600\n  },\n  api: aiMineMenuGroup.getList,\n  add: {\n    show: true,\n    api: aiMineMenuGroup.save,\n    auth: ['ai:mineMenuGroup:save']\n  },\n  edit: {\n    show: true,\n    api: aiMineMenuGroup.update,\n    auth: ['ai:mineMenuGroup:update']\n  },\n  delete: {\n    show: true,\n    api: aiMineMenuGroup.deletes,\n    auth: ['ai:mineMenuGroup:delete']\n  }\n})\n\nconst columns = reactive([\n  {\n    title: \"主键\",\n    dataIndex: \"id\",\n    formType: \"input\",\n    addDisplay: false,\n    editDisplay: false,\n    hide: true,\n    commonRules: {\n      required: true,\n      message: \"请输入主键\"\n    }\n  },\n  {\n    title: \"分组名称\",\n    dataIndex: \"name\",\n    formType: \"input\",\n    search: true\n  },\n  {\n    title: \"排序\",\n    dataIndex: \"sort\",\n    formType: \"input\",\n    search: true\n  },\n  {\n    title: \"是否锁定\",\n    dataIndex: \"is_lock\",\n    addDisplay: false,\n    formType: \"select\",\n    search: true,\n    dict: {\n      data: ptConst.is_lock,\n      translation: true\n    },\n  },\n  {\n    title: \"创建时间\",\n    dataIndex: \"created_at\",\n    formType: \"date\",\n    search: true,\n    addDisplay: false,\n    editDisplay: false,\n    showTime: true\n  },\n  {\n    title: \"更新时间\",\n    dataIndex: \"updated_at\",\n    formType: \"date\",\n    search: true,\n    addDisplay: false,\n    editDisplay: false,\n    showTime: true\n  }\n])\n</script>\n<script> export default { name: 'ai:mineMenuGroup' } </script>"
  },
  {
    "path": "MineAdmin/vue/src/views/ai/openKey/components/add.vue",
    "content": "<template>\n  <a-modal v-model:visible=\"visible\" :footer=\"false\" draggable width=\"600px\">\n    <template #title>批量添加openai key</template>\n    <ma-form v-model=\"form\" v-model:columns=\"columns\" @onSubmit=\"submit\" ref=\"maformRef\">\n    </ma-form>\n  </a-modal>\n</template>\n\n<script setup>\nimport {reactive, ref} from 'vue'\nimport aiOpenaiKey from '@/api/ai/aiOpenaiKey'\nimport {Message} from \"@arco-design/web-vue\";\n\nconst form = ref({\n  content: '',\n  remark: ''\n})\n\nconst maformRef = ref()\nconst visible = ref(false)\nconst emit = defineEmits(['success'])\n\nconst open = () => {\n  visible.value = true\n}\n\nconst submit = async (data) => {\n  if (data) {\n    let response = await aiOpenaiKey.batchAdd(data)\n    if (response.success) {\n      Message.success('创建成功')\n      emit('success', true)\n      visible.value = false\n    } else {\n      emit('success', false)\n    }\n  }\n}\n\nconst columns = reactive([\n  {\n    title: '卡密内容',\n    dataIndex: 'content',\n    formType: 'textarea'\n  }, {\n    title: '备注',\n    dataIndex: 'remark',\n    formType: 'textarea'\n  }\n])\n\ndefineExpose({open})\n</script>\n"
  },
  {
    "path": "MineAdmin/vue/src/views/ai/openKey/index.vue",
    "content": "<template>\n  <div class=\"ma-content-block lg:flex justify-between p-4\">\n    <!-- CRUD 组件 -->\n    <ma-crud :options=\"options\" :columns=\"columns\" ref=\"crudRef\">\n      <template #tableAfterButtons=\"{ record }\">\n        <a-button class=\"w-full lg:w-auto mt-2 lg:mt-0\" type=\"primary\" @click=\"batchAdd\" >批量添加</a-button>\n        <a-button class=\"w-full lg:w-auto mt-2 lg:mt-0\" type=\"primary\" @click=\"refreshCache\" >刷新缓存</a-button>\n      </template>\n    </ma-crud>\n    <add ref=\"addRef\" @success=\"addSuccess\"/>\n  </div>\n</template>\n<script setup>\nimport { ref, reactive } from 'vue'\nimport aiOpenaiKey from '@/api/ai/aiOpenaiKey'\nimport { Message } from '@arco-design/web-vue'\nimport Add from './components/add.vue'\n\nconst crudRef = ref()\n\nconst addRef = ref()\n\nconst options = reactive({\n  id: 'by_ai_openai_key',\n  rowSelection: {\n    showCheckedAll: true\n  },\n  pk: 'id',\n  operationColumn: true,\n  operationWidth: 160,\n  formOption: {\n    viewType: 'modal',\n    width: 600\n  },\n  api: aiOpenaiKey.getList,\n  add: {\n    show: true,\n    api: aiOpenaiKey.save,\n    auth: ['ai:openKey:save']\n  },\n  edit: {\n    show: true,\n    api: aiOpenaiKey.update,\n    auth: ['ai:openKey:update']\n  },\n  delete: {\n    show: true,\n    api: aiOpenaiKey.deletes,\n    auth: ['ai:openKey:delete']\n  }\n})\n\nconst columns = reactive([\n  {\n    title: \"主键\",\n    dataIndex: \"id\",\n    formType: \"input\",\n    search: true,\n    addDisplay: false,\n    editDisplay: false,\n    commonRules: {\n      required: true,\n      message: \"请输入主键\"\n    },\n    sortable: {\n      sortDirections: [\n        \"ascend\",\n        \"descend\"\n      ],\n      sorter: true\n    }\n  },\n  {\n    title: \"openai_key\",\n    dataIndex: \"openai_key\",\n    formType: \"input\",\n    search: true\n  },\n  {\n    title: \"备注\",\n    dataIndex: \"remark\",\n    formType: \"input\",\n    search: true\n  },\n  {\n    title: \"创建者\",\n    dataIndex: \"created_by\",\n    formType: \"input\",\n    addDisplay: false,\n    editDisplay: false,\n    hide: true\n  },\n  {\n    title: \"更新者\",\n    dataIndex: \"updated_by\",\n    formType: \"input\",\n    addDisplay: false,\n    editDisplay: false,\n    hide: true\n  },\n  {\n    title: \"创建时间\",\n    dataIndex: \"created_at\",\n    formType: \"date\",\n    search: true,\n    addDisplay: false,\n    editDisplay: false,\n    showTime: true\n  },\n  {\n    title: \"更新时间\",\n    dataIndex: \"updated_at\",\n    formType: \"date\",\n    search: true,\n    addDisplay: false,\n    editDisplay: false,\n    showTime: true\n  },\n  {\n    title: \"删除时间\",\n    dataIndex: \"deleted_at\",\n    formType: \"date\",\n    addDisplay: false,\n    editDisplay: false,\n    hide: true,\n    showTime: true\n  }\n])\n\nconst batchAdd = ()=>{\n  addRef.value.open()\n}\n\nconst refreshCache = ()=>{\n  aiOpenaiKey.refreshCache().then((res)=>{\n    Message.success('操作成功')\n  })\n}\n\nconst addSuccess = (res)=>{\n  res && crudRef.value.refresh()\n}\n</script>\n<script> export default { name: 'ai:openKey' } </script>"
  },
  {
    "path": "MineAdmin/vue/src/views/ai/order/index.vue",
    "content": "<template>\n  <div class=\"ma-content-block lg:flex justify-between p-4\">\n    <!-- CRUD 组件 -->\n    <ma-crud :options=\"options\" :columns=\"columns\" ref=\"crudRef\">\n    </ma-crud>\n  </div>\n</template>\n<script setup>\nimport { ref, reactive } from 'vue'\nimport aiOrder from '@/api/ai/aiOrder'\nimport { Message } from '@arco-design/web-vue'\nimport tool from '@/utils/tool'\nimport * as common from '@/utils/common'\nimport ptConst from \"@/config/pt-const\";\n\nconst crudRef = ref()\n\nconst options = reactive({\n  id: 'by_ai_order',\n  rowSelection: {\n    showCheckedAll: true\n  },\n  pk: 'id',\n  operationColumn: true,\n  operationWidth: 160,\n  formOption: {\n    viewType: 'modal',\n    width: 600\n  },\n  api: aiOrder.getList,\n  delete: {\n    show: true,\n    api: aiOrder.deletes,\n    auth: ['ai:order:delete']\n  }\n})\n\nconst columns = reactive([\n  {\n    title: \"主键\",\n    dataIndex: \"id\",\n    formType: \"input\",\n    addDisplay: false,\n    editDisplay: false,\n    commonRules: {\n      required: true,\n      message: \"请输入主键\"\n    }\n  },\n  {\n    title: \"用户UID\",\n    dataIndex: \"uid\",\n    formType: \"input\",\n    search: true,\n    addDisplay: false,\n    editDisplay: false\n  },\n  {\n    title: \"上级UID\",\n    dataIndex: \"from_uid\",\n    formType: \"input\",\n    addDisplay: false,\n    editDisplay: false\n  },\n  {\n    title: \"营销部UID\",\n    dataIndex: \"market_uid\",\n    formType: \"input\",\n    addDisplay: false,\n    editDisplay: false\n  },\n  {\n    title: \"订单号\",\n    dataIndex: \"ord_sn\",\n    formType: \"input\",\n    search: true,\n    addDisplay: false,\n    editDisplay: false\n  },\n  {\n    title: \"订单类型\",\n    dataIndex: \"ord_type\",\n    formType: \"select\",\n    search: true,\n    addDisplay: false,\n    editDisplay: false,\n    dict: {\n      data: [\n        {\n          label: \"开通VIP\",\n          value: 1\n        }, {\n          label: \"提现\",\n          value: 2\n        }, {\n          label: \"成为营销部\",\n          value: 3\n        }\n      ],\n      translation: true\n    },\n  },\n  {\n    title: \"支付方式\",\n    dataIndex: \"pay_type\",\n    formType: \"select\",\n    search: true,\n    addDisplay: false,\n    editDisplay: false,\n    dict: {\n      data: [\n        {\n          label: \"微信\",\n          value: 1\n        }, {\n          label: \"后台免费\",\n          value: 2\n        }, {\n          label: \"后台付费\",\n          value: 3\n        }, {\n          label: \"免费卡密\",\n          value: 4\n        }, {\n          label: \"付费卡密\",\n          value: 5\n        }\n      ],\n      translation: true\n    },\n  },\n  {\n    title: \"订单状态\",\n    dataIndex: \"status\",\n    formType: \"select\",\n    search: true,\n    addDisplay: false,\n    editDisplay: false,\n    dict: {\n      data: [\n        {\n          label: \"未支付(待处理)\",\n          value: 1\n        }, {\n          label: \"已支付(已完成)\",\n          value: 2\n        }, {\n          label: \"支付失败\",\n          value: 3\n        }\n      ],\n      translation: true\n    },\n  },\n  {\n    title: \"总金额\",\n    dataIndex: \"total_price\",\n    formType: \"input\",\n    addDisplay: false,\n    editDisplay: false\n  },\n  {\n    title: \"实际金额\",\n    dataIndex: \"amount_price\",\n    formType: \"input\",\n    addDisplay: false,\n    editDisplay: false\n  },\n  {\n    title: \"订单描述\",\n    dataIndex: \"content\",\n    formType: \"input\",\n    addDisplay: false,\n    editDisplay: false\n  },\n  {\n    title: \"备注\",\n    dataIndex: \"remark\",\n    formType: \"input\",\n    addDisplay: false,\n    editDisplay: false\n  },\n  {\n    title: \"创建者\",\n    dataIndex: \"created_by\",\n    formType: \"input\",\n    addDisplay: false,\n    editDisplay: false,\n    hide: true\n  },\n  {\n    title: \"更新者\",\n    dataIndex: \"updated_by\",\n    formType: \"input\",\n    addDisplay: false,\n    editDisplay: false,\n    hide: true\n  },\n  {\n    title: \"创建时间\",\n    dataIndex: \"created_at\",\n    formType: \"date\",\n    search: true,\n    addDisplay: false,\n    editDisplay: false,\n    showTime: true\n  },\n  {\n    title: \"订单完成时间\",\n    dataIndex: \"pay_at\",\n    formType: \"date\",\n    search: true,\n    addDisplay: false,\n    editDisplay: false,\n    showTime: true\n  },\n  {\n    title: \"更新时间\",\n    dataIndex: \"updated_at\",\n    formType: \"date\",\n    search: true,\n    addDisplay: false,\n    editDisplay: false,\n    showTime: true\n  },\n  {\n    title: \"删除时间\",\n    dataIndex: \"deleted_at\",\n    formType: \"date\",\n    addDisplay: false,\n    editDisplay: false,\n    hide: true,\n    showTime: true\n  }\n])\n</script>\n<script> export default { name: 'ai:order' } </script>"
  },
  {
    "path": "MineAdmin/vue/src/views/ai/payKami/components/add.vue",
    "content": "<template>\n  <a-modal v-model:visible=\"visible\" :footer=\"false\" draggable width=\"600px\">\n    <template #title>生成卡密</template>\n    <ma-form v-model=\"form\" v-model:columns=\"columns\" @onSubmit=\"submit\" ref=\"maformRef\">\n    </ma-form>\n  </a-modal>\n</template>\n\n<script setup>\nimport {reactive, ref} from 'vue'\nimport ptConst from \"@/config/pt-const\";\nimport ai from \"@/api/ai/aiPayKami\";\nimport {Message} from \"@arco-design/web-vue\";\n\nconst form = ref({\n  uid: 0,\n  price: 0,\n  remark: '',\n  number: 1,\n})\n\nconst maformRef = ref()\nconst visible = ref(false)\nconst emit = defineEmits(['success'])\n\nconst open = () => {\n  visible.value = true\n}\n\nconst submit = async (data) => {\n  if (data) {\n    let response = await ai.add(data)\n    if (response.success) {\n      Message.success('创建成功')\n      emit('success', true)\n      visible.value = false\n    } else {\n      emit('success', false)\n    }\n  }\n}\n\nconst columns = reactive([\n  {\n    title: '用户UID',\n    dataIndex: 'uid',\n    formType: \"input\"\n  },\n  {\n    title: \"金额\",\n    dataIndex: \"price\",\n    formType: \"input\"\n  }, {\n    title: \"数量\",\n    dataIndex: \"number\",\n    formType: \"input\"\n  },\n  {\n    title: '备注',\n    dataIndex: 'remark',\n    formType: 'textarea'\n  }\n])\n\ndefineExpose({open})\n</script>\n"
  },
  {
    "path": "MineAdmin/vue/src/views/ai/payKami/index.vue",
    "content": "<template>\n  <div class=\"ma-content-block lg:flex justify-between p-4\">\n    <!-- CRUD 组件 -->\n    <ma-crud :options=\"options\" :columns=\"columns\" ref=\"crudRef\">\n      <template #tableAfterButtons=\"{ record }\">\n        <a-button class=\"w-full lg:w-auto mt-2 lg:mt-0\" type=\"primary\" @click=\"addRef.open()\" ><icon-plus />&nbsp;&nbsp;新增</a-button>\n      </template>\n    </ma-crud>\n    <add ref=\"addRef\" @success=\"addSuccess\"/>\n  </div>\n</template>\n<script setup>\nimport { ref, reactive } from 'vue'\nimport aiPayKami from '@/api/ai/aiPayKami'\nimport Add from './components/add.vue'\n\nconst crudRef = ref()\nconst addRef = ref()\n\n\nconst options = reactive({\n  id: 'by_ai_pay_kami',\n  rowSelection: {\n    showCheckedAll: true\n  },\n  pk: 'id',\n  operationColumn: true,\n  operationWidth: 160,\n  formOption: {\n    viewType: 'modal',\n    width: 600\n  },\n  api: aiPayKami.getList,\n  edit: {\n    show: true,\n    api: aiPayKami.update,\n    auth: ['ai:payKami:update']\n  },\n  delete: {\n    show: true,\n    api: aiPayKami.deletes,\n    auth: ['ai:payKami:delete']\n  }\n})\n\nconst columns = reactive([\n  {\n    title: \"主键\",\n    dataIndex: \"id\",\n    formType: \"input\",\n    search: true,\n    addDisplay: false,\n    editDisplay: false,\n    commonRules: {\n      required: true,\n      message: \"请输入主键\"\n    }\n  },\n  {\n    title: \"绑定用户\",\n    dataIndex: \"uid\",\n    formType: \"input\",\n    search: true\n  },\n  {\n    title: \"价格\",\n    dataIndex: \"price\",\n    formType: \"input\"\n  },\n  {\n    title: \"卡密号\",\n    dataIndex: \"code\",\n    formType: \"input\",\n    search: true,\n    addDisplay: false,\n    editDisplay: false\n  },\n  {\n    title: \"状态\",\n    dataIndex: \"status\",\n    formType: \"select\",\n    search: true,\n    addDisplay: false,\n    editDisplay: false,\n    dict: {\n      data: [\n        {\n          label: \"待使用\",\n          value: 1\n        }, {\n          label: \"已使用\",\n          value: 2\n        }\n      ],\n      translation: true\n    },\n  },\n  {\n    title: \"备注\",\n    dataIndex: \"remark\",\n    formType: \"input\",\n    search: true,\n    addDisplay: false,\n    editDisplay: false\n  },\n  {\n    title: \"创建者\",\n    dataIndex: \"created_by\",\n    formType: \"input\",\n    addDisplay: false,\n    editDisplay: false\n  },\n  {\n    title: \"更新者\",\n    dataIndex: \"updated_by\",\n    formType: \"input\",\n    addDisplay: false,\n    editDisplay: false,\n    hide: true\n  },\n  {\n    title: \"创建时间\",\n    dataIndex: \"created_at\",\n    formType: \"date\",\n    search: true,\n    addDisplay: false,\n    editDisplay: false,\n    showTime: true\n  },\n  {\n    title: \"使用时间\",\n    dataIndex: \"use_at\",\n    formType: \"date\",\n    search: true,\n    addDisplay: false,\n    editDisplay: false,\n    showTime: true\n  },\n  {\n    title: \"更新时间\",\n    dataIndex: \"updated_at\",\n    formType: \"date\",\n    search: true,\n    addDisplay: false,\n    editDisplay: false,\n    showTime: true\n  },\n  {\n    title: \"删除时间\",\n    dataIndex: \"deleted_at\",\n    formType: \"date\",\n    addDisplay: false,\n    editDisplay: false,\n    hide: true,\n    showTime: true\n  }\n])\n\nconst addSuccess = (res)=>{\n  res && crudRef.value.refresh()\n}\n</script>\n<script> export default { name: 'ai:payKami' } </script>"
  },
  {
    "path": "MineAdmin/vue/src/views/ai/quickIssue/index.vue",
    "content": "<template>\n  <div class=\"ma-content-block lg:flex justify-between p-4\">\n    <!-- CRUD 组件 -->\n    <ma-crud :options=\"options\" :columns=\"columns\" ref=\"crudRef\">\n    </ma-crud>\n  </div>\n</template>\n<script setup>\nimport { ref, reactive } from 'vue'\nimport aiQuickIssue from '@/api/ai/aiQuickIssue'\nimport { Message } from '@arco-design/web-vue'\nimport tool from '@/utils/tool'\nimport * as common from '@/utils/common'\n\nconst crudRef = ref()\n\n\n\n\nconst options = reactive({\n  id: 'by_ai_quick_issue',\n  rowSelection: {\n    showCheckedAll: true\n  },\n  pk: 'id',\n  operationColumn: true,\n  operationWidth: 160,\n  formOption: {\n    viewType: 'modal',\n    width: 600\n  },\n  api: aiQuickIssue.getList,\n  add: {\n    show: true,\n    api: aiQuickIssue.save,\n    auth: ['ai:quickIssue:save']\n  },\n  edit: {\n    show: true,\n    api: aiQuickIssue.update,\n    auth: ['ai:quickIssue:update']\n  },\n  delete: {\n    show: true,\n    api: aiQuickIssue.deletes,\n    auth: ['ai:quickIssue:delete']\n  }\n})\n\nconst columns = reactive([\n  {\n    title: \"主键\",\n    dataIndex: \"id\",\n    formType: \"input\",\n    addDisplay: false,\n    editDisplay: false,\n    hide: true,\n    commonRules: {\n      required: true,\n      message: \"请输入主键\"\n    }\n  },\n  {\n    title: \"问题标题\",\n    dataIndex: \"title\",\n    formType: \"input\",\n    search: true,\n    commonRules: {\n      required: true,\n      message: \"请输入问题标题\"\n    }\n  },\n  {\n    title: \"问题描述\",\n    dataIndex: \"content\",\n    formType: \"input\",\n    search: true,\n    commonRules: {\n      required: true,\n      message: \"请输入问题描述\"\n    }\n  },\n  {\n    title: \"排序\",\n    dataIndex: \"sort\",\n    formType: \"input\",\n    commonRules: {\n      required: true,\n      message: \"请输入\"\n    }\n  },\n  {\n    title: \"创建时间\",\n    dataIndex: \"created_at\",\n    formType: \"date\",\n    search: true,\n    addDisplay: false,\n    editDisplay: false,\n    showTime: true\n  },\n  {\n    title: \"更新时间\",\n    dataIndex: \"updated_at\",\n    formType: \"date\",\n    addDisplay: false,\n    editDisplay: false,\n    showTime: true\n  },\n  {\n    title: \"删除时间\",\n    dataIndex: \"deleted_at\",\n    formType: \"date\",\n    addDisplay: false,\n    editDisplay: false,\n    hide: true,\n    showTime: true\n  }\n])\n</script>\n<script> export default { name: 'ai:quickIssue' } </script>"
  },
  {
    "path": "MineAdmin/vue/src/views/ai/setting/index.vue",
    "content": "<template>\n  <div class=\"ma-content-block lg:flex justify-between p-4\">\n    <ma-form v-model=\"form\" v-model:columns=\"columns\" @onSubmit=\"submit\" ref=\"maformRef\">\n    </ma-form>\n  </div>\n</template>\n\n<script setup>\nimport {onMounted, reactive, ref, shallowRef, watch} from 'vue'\nimport {Message} from \"@arco-design/web-vue\"\nimport ptUpload from '@/components/putyy/pt-upload.vue'\nimport ptScene from \"@/config/pt-scene\"\nimport {uploadQiniuHandler} from \"@/utils/pt-upload\"\nimport ai from \"@/api/ai/aiSetting\"\nimport { useFormStore } from '@/store/index'\n\nconst formStore = useFormStore()\n\nconst form = ref({\n  openai_proxy: '',\n  app_close_message: '',\n  agreement_user: '',\n  head_img: '',\n  mobile: '',\n  user_name: '',\n  work_time: '',\n  wx_img_url: '',\n  wx_no: '',\n})\n\nwatch(\n    () => formStore.crudList['ai_setting_index'],\n    async vl => {\n      vl === true && await  ai.getList().then((res) => {\n        form.value = Object.assign({}, form.value, res.data)\n      })\n      formStore.crudList['ai_setting_index'] = false\n    }\n)\n\nconst maformRef = ref()\n\nconst emit = defineEmits(['success'])\n\nconst columns = reactive([\n  {\n    title: \"openai api地址\",\n    dataIndex: \"openai_proxy\",\n    formType: \"input\",\n    placeholder: \"默认: https://api.openai.com\",\n  },{\n    title: \"关站信息\",\n    dataIndex: \"app_close_message\",\n    formType: \"textarea\",\n    placeholder: \"不为空则为关站中\",\n  }, {\n    title: '用户协议',\n    dataIndex: 'agreement_user',\n    formType: 'textarea'\n  }, {\n    title: '客服手机',\n    dataIndex: 'mobile',\n    formType: 'input'\n  }, {\n    title: '客服名字',\n    dataIndex: 'user_name',\n    formType: 'input'\n  }, {\n    title: '客服工作时间',\n    dataIndex: 'work_time',\n    formType: 'input'\n  }, {\n    title: '客服微信号',\n    dataIndex: 'wx_no',\n    formType: 'input'\n  }, {\n    addDisplay: false,\n    title: \"客服头像\",\n    dataIndex: \"head_img\",\n    formType: 'component',\n    component: shallowRef(ptUpload),\n    accept: 'image/*',\n    ptScene: ptScene.ai_customer_head_img\n  },\n  {\n    addDisplay: false,\n    title: \"客服微信二维码\",\n    dataIndex: \"wx_img_url\",\n    formType: 'component',\n    component: shallowRef(ptUpload),\n    accept: 'image/*',\n    ptScene: ptScene.ai_customer_wx_img\n  },\n])\n\nonMounted(() => {\n  ai.getList().then((res) => {\n    form.value = Object.assign({}, form.value, res.data)\n  })\n})\n\nconst submit = async (form) => {\n  if (form.hasOwnProperty('ptFileList')) {\n    let files = await uploadQiniuHandler(form.ptFileList)\n    for (let dataIndex in files) {\n      if (form.hasOwnProperty(dataIndex)) {\n        form[dataIndex] = files[dataIndex].source_url\n      }\n    }\n    delete form.ptFileList\n  }\n  ai.save({\n    app_close_message: form.app_close_message,\n    agreement_user: form.agreement_user,\n    openai_proxy: form.openai_proxy,\n    customer: {\n      head_img: form.head_img,\n      mobile: form.mobile,\n      user_name: form.user_name,\n      work_time: form.work_time,\n      wx_img_url: form.wx_img_url,\n      wx_no: form.wx_no,\n    },\n  }).then((res)=>{\n    Message.success('设置成功')\n  })\n}\n</script>"
  },
  {
    "path": "MineAdmin/vue/src/views/ai/user/components/openVip.vue",
    "content": "<template>\n  <a-modal v-model:visible=\"visible\" :footer=\"false\" draggable width=\"600px\">\n    <template #title>开通VIP</template>\n\n    <ma-form v-model=\"form\" v-model:columns=\"columns\" @onSubmit=\"submit\" ref=\"maformRef\">\n      <template #form-vip>\n        <a-select\n            style=\"width: 100%\"\n            v-model=\"form['vip']\"\n            allow-clear\n            allow-search\n            placeholder=\"请选择vip等级\"\n        >\n          <a-option\n              class=\"w-full\"\n              v-for=\"(item, index) in ptConst.ai_vip\"\n              :label=\"item.label\"\n              :value=\"item.value\"\n              :key=\"index\"\n          >\n          </a-option>\n        </a-select>\n      </template>\n    </ma-form>\n  </a-modal>\n</template>\n\n<script setup>\nimport {reactive, ref} from 'vue'\nimport ptConst from \"@/config/pt-const\"\nimport ai from \"@/api/ai/aiUser\"\nimport {Message} from \"@arco-design/web-vue\"\n\nconst form = ref({\n  id: 0,\n  vip: 0,\n  price: 0,\n  remark: '',\n})\n\nconst maformRef = ref()\nconst visible = ref(false)\nconst emit = defineEmits(['success'])\n\nconst open = (value) => {\n  form.value.id = value.id\n  form.value.vip = value.vip\n  form.value.price = 0\n  form.value.remark = ''\n  visible.value = true\n}\n\nconst submit = async (data) => {\n  if (data) {\n    let response = await ai.openVip(data.id, data)\n    if (response.success) {\n      Message.success('开通成功')\n      emit('success', true)\n      visible.value = false\n    } else {\n      emit('success', false)\n    }\n  }\n}\n\nconst columns = reactive([\n  {\n    title: '用户UID',\n    dataIndex: 'id',\n    disabled: true\n  },\n  {\n    title: \"vip等级\",\n    dataIndex: \"vip\",\n    formType: \"select\",\n    commonRules: {\n      required: true,\n      message: \"vip等级\"\n    },\n    dict: {\n      data: ptConst.ai_vip,\n      translation: true\n    },\n  },\n  {\n    title: \"金额\",\n    dataIndex: \"price\",\n    formType: \"input\"\n  },\n  {\n    title: '备注',\n    dataIndex: 'remark',\n    formType: 'textarea'\n  }\n])\n\ndefineExpose({open})\n</script>\n"
  },
  {
    "path": "MineAdmin/vue/src/views/ai/user/index.vue",
    "content": "<template>\n  <div class=\"ma-content-block lg:flex justify-between p-4\">\n    <!-- CRUD 组件 -->\n    <ma-crud :options=\"options\" :columns=\"columns\" ref=\"crudRef\">\n      <template #nick_name=\"{ record }\">\n        <div>{{record.nick_name}}</div>\n        <div>{{record.mobile}}</div>\n      </template>\n      <template #head_img=\"{ record }\">\n        <img :src=\"record.head_img\" style=\"object-fit: cover\"/>\n      </template>\n      <template #from_uid=\"{ record }\">\n        <sapn>{{record.from_uid ? record.parent_nick_name+'('+record.from_uid+')' : ''}}</sapn>\n      </template>\n      <template #balance=\"{ record }\">\n        <div>余额: {{record.balance}}</div>\n        <div>总收入: {{record.balance_total}}</div>\n      </template>\n      <template #operationAfterExtend=\"{ record }\">\n        <a-link v-auth=\"['ai:user:open-vip']\" @click=\"showOpenVip(record)\">\n          <icon-edit/>\n          开通VIP\n        </a-link>\n        <a-link v-auth=\"['ai:user:lock']\" @click=\"lock(record)\">\n          <icon-edit/>\n          {{record.is_lock === 1 ? \"锁定\" : \"解除锁定\" }}\n        </a-link>\n      </template>\n    </ma-crud>\n    <open-vip ref=\"openVipRef\" @success=\"openVipSuccess\"/>\n  </div>\n</template>\n<script setup>\nimport {ref, reactive, shallowRef} from 'vue'\nimport aiUser from '@/api/ai/aiUser'\nimport { Message } from '@arco-design/web-vue'\nimport ptUpload from '@/components/putyy/pt-upload.vue'\nimport ptScene from \"@/config/pt-scene\";\nimport ptConst from \"@/config/pt-const\";\nimport {uploadQiniuHandler} from \"@/utils/pt-upload\";\nimport OpenVip from './components/openVip.vue'\nimport user from \"@/api/ai/aiUser\"\n\nconst openVipRef = ref()\n\nconst crudRef = ref()\n\nconst options = reactive({\n  id: 'by_ai_user',\n  rowSelection: {\n    showCheckedAll: true\n  },\n  pk: 'id',\n  operationColumn: true,\n  operationWidth: 160,\n  formOption: {\n    viewType: 'modal',\n    width: 600\n  },\n  api: aiUser.getList,\n  edit: {\n    show: true,\n    api: aiUser.update,\n    auth: ['ai:user:update']\n  },\n  delete: {\n    show: true,\n    api: aiUser.deletes,\n    auth: ['ai:user:delete']\n  },\n  beforeAdd: async (form) => {\n    let files = await uploadQiniuHandler(form.ptFileList)\n    if (files === true) {\n      return\n    }\n    for (let dataIndex in files) {\n      if (form.hasOwnProperty(dataIndex)) {\n        form[dataIndex] = files[dataIndex].source_url\n      }\n    }\n    delete form.ptFileList\n  },\n  beforeEdit: async (form) => {\n    let files = await uploadQiniuHandler(form.ptFileList)\n    if (files === true) {\n      return\n    }\n    for (let dataIndex in files) {\n      if (form.hasOwnProperty(dataIndex)) {\n        form[dataIndex] = files[dataIndex].source_url\n      }\n    }\n    delete form.ptFileList\n  },\n})\n\nconst columns = reactive([\n  {\n    title: \"Uid\",\n    dataIndex: \"id\",\n    formType: \"input\",\n    search: true,\n    addDisplay: false,\n    editDisplay: false,\n    commonRules: {\n      required: true,\n      message: \"请输入主键\"\n    }\n  },\n  {\n    title: \"昵称\",\n    dataIndex: \"nick_name\",\n    formType: \"input\",\n    search: true,\n    addDisplay: false,\n    commonRules: {\n      required: true,\n      message: \"请输入昵称\"\n    }\n  },\n  {\n    addDisplay: false,\n    title: \"头像\",\n    dataIndex: \"head_img\",\n    formType: 'component',\n    component: shallowRef(ptUpload),\n    accept: 'image/*',\n    ptScene: ptScene.ai_head_img\n  },\n  {\n    title: \"手机号\",\n    dataIndex: \"mobile\",\n    formType: \"input\",\n    hide: true,\n    search: true,\n    addDisplay: false,\n    commonRules: {\n      required: true,\n      message: \"请输入手机号\"\n    }\n  },\n  {\n    title: \"账户\",\n    dataIndex: \"balance\",\n    formType: \"input\",\n    search: false,\n    addDisplay: false,\n    editDisplay: false,\n    commonRules: {\n      required: true,\n      message: \"请输入手机号\"\n    }\n  },\n  {\n    title: \"总收入\",\n    dataIndex: \"balance_total\",\n    formType: \"input\",\n    hide: true,\n    search: false,\n    addDisplay: false,\n    editDisplay: false,\n    commonRules: {\n      required: true,\n      message: \"请输入手机号\"\n    }\n  },\n  {\n    title: \"上级\",\n    dataIndex: \"from_uid\",\n    formType: \"input\",\n    search: true,\n    addDisplay: false,\n    editDisplay: false,\n    commonRules: {\n      required: true,\n      message: \"请输入主键\"\n    }\n  },\n  {\n    title: \"vip等级\",\n    dataIndex: \"vip\",\n    formType: \"select\",\n    search: true,\n    addDisplay: false,\n    disabled: true,\n    commonRules: {\n      required: true,\n      message: \"vip等级\"\n    },\n    dict: {\n      data: ptConst.ai_vip,\n      translation: true\n    },\n  },\n  {\n    title: \"vip到期时间\",\n    dataIndex: \"vip_ent_at\",\n    formType: \"date\",\n    search: true,\n    addDisplay: false,\n    showTime: true\n  },\n  {\n    title: \"是否锁定\",\n    dataIndex: \"is_lock\",\n    formType: \"select\",\n    search: true,\n    addDisplay: false,\n    editDisplay: false,\n    commonRules: {\n      required: true,\n      message: \"是否锁定\"\n    },\n    dict: {\n      data:ptConst.is_lock,\n      translation: true\n    },\n  },\n  {\n    title: \"更新者\",\n    dataIndex: \"updated_by\",\n    formType: \"input\",\n    addDisplay: false,\n    editDisplay: false,\n    hide: true\n  },\n  {\n    title: \"创建时间\",\n    dataIndex: \"created_at\",\n    formType: \"date\",\n    search: true,\n    addDisplay: false,\n    editDisplay: false,\n    showTime: true\n  },\n  {\n    title: \"更新时间\",\n    dataIndex: \"updated_at\",\n    formType: \"date\",\n    search: true,\n    addDisplay: false,\n    editDisplay: false,\n    showTime: true\n  },\n  {\n    title: \"删除时间\",\n    dataIndex: \"deleted_at\",\n    formType: \"date\",\n    addDisplay: false,\n    editDisplay: false,\n    hide: true,\n    showTime: true\n  }\n])\n\nconst showOpenVip = (record)=>{\n  openVipRef.value.open(record)\n}\n\nconst openVipSuccess = (res)=>{\n  res && crudRef.value.refresh()\n}\n\nconst lock = (record)=>{\n  user.lock(record.id).then((res)=>{\n    Message.success('操作成功')\n    crudRef.value.refresh()\n  })\n}\n</script>\n<script> export default { name: 'ai:user' } </script>"
  },
  {
    "path": "README.md",
    "content": "# uniapp、hyperf MineAdmin 实现的 chatgpt应用，支持小程序、H5、App！\n## 效果图\n#### App\n![](images/1.jpg)\n\n![](images/2.jpg)\n\n![](images/3.jpg)\n\n#### 后台系统\n.\n![admin.png](images/admin.png)\n\n## 技术栈 具体依赖看项目代码吧！\n### 前端\n> uniapp vue3 pug scss 等\n### 后端\n> swoole hyperf MineAdmin 等\n\n## 功能说明\n> 客户端： 问答上下文、快捷提问、角色自定义、历史会话、公开频道、模型设置、VIP系统、邀请好友、分佣系统、联系客服、钱包系统、提现、订单、好友管理等\n\n> 后台：chatgpt角色自定义、快捷提问管理、聊天数据管理、个人中心菜单管理、订单管理、用户管理、卡密管理、设置(openai地址、客服信息、用户协议等设置)、图片素材(个人中心banner)、openai_key管理(自动轮训)\n\n\n## 开始安装\n> 下载本项目\n```shell\ngit clone https://github.com/putyy/chatgpt.git\n```\n\n## 安装 MineAdmin\n#### 1. 按照官方文档进行安装 [安装文档](https://doc.mineadmin.com/guide/install/)\n\n#### 2. 安装本项目mineadmin-php\n\n> 安装composer依赖包\n>> composer require easyswoole/oss putyy/php-constants orhanerday/open-ai --ignore-platform-reqs\n\n>复制本项目 ./MineAdmin/php/app/ai 下所有子文件夹，粘贴到到mineadmin-php app/ai目录下\n>> cp -r ./MineAdmin/php/app/ai/ ./you-mineadmin-php/app/ai\n\n> 执行以下命令添加本项目需要的数据表及初始化数据\n>> php bin/hyperf.php mine:migrate-run ai\n>>\n>> php bin/hyperf.php mine:seeder-run ai\n>>\n>> php bin/hyperf.php ai:init-menu\n\n> 生成Ai api需要的jwt key\n>>php bin/hyperf.php mine:jwt-gen --jwtSecret=JWT_AI_SECRET\n\n> 修改mineadmin后端 jwt 配置文件，位置: config/autoload/jwt.php,新增如下内容:\n> ```php\n> return [\n>     // ......\n>     'scene' => [\n>          // 新增如下\n>          'ai' => [\n>             'secret' => env('JWT_AI_SECRET', ''), // 非对称加密使用字符串,请使用自己加密的字符串\n>             'login_type' => 'sso',\n>             'sso_key' => 'id',\n>             'ttl' => 86400,\n>             'blacklist_cache_ttl' => 86400, \n>          ],\n>          // ......\n>     ]\n>     // ......\n> ]\n> ```\n\n> 修改mineadmin后端 route 配置文件，位置: config/routes.php,新增如下内容:\n> ```php\n> Router::addServer('message', function () {\n>    // ......\n>    // 新增如下内容\n>    Router::get('/ws-chat', 'App\\Ai\\Api\\Websocket', [\n>        'middleware' => [  ]\n>    ]);\n>    // ......\n> })\n> ```\n> \n> \n> 修改mineadmin后端 file 配置文件，位置: config/autoload/file.php,新增如下内容:\n> ```php\n> return [\n>   'storage'=>[\n>       'qiniu'=>[\n>           // ......\n>           'accessKey' => '七牛云accessKey',   \n>           'secretKey' => '七牛云secretKey',   \n>           'host' => env('QINIU_HOST', '你的七牛云访问主域名,例: baidu.com'),\n>           // 具体查看 https://developer.qiniu.com/kodo/1671/region-endpoint-fq\n>           'upload_domain' => env('QINIU_UPLOAD_DOMAIN', 'https://up-cn-east-2.qiniup.com'),\n>           'image_bucket' => env('QINIU_IMAGE_BUCKET', '七牛云图片空间'),\n>           'image_domain' => env('QINIU_IMAGE_DOMAIN', '七牛云图片域名'),\n>           // 以下未用到 不用配置\n>           'audio_bucket' => env('QINIU_AUDIO_BUCKET'),\n>           'video_bucket' => env('QINIU_VIDEO_BUCKET'),\n>           'audio_domain' => env('QINIU_AUDIO_DOMAIN'),\n>           'video_domain' => env('QINIU_VIDEO_DOMAIN'),\n>           // ......\n>       ]\n>   ]\n> ]\n>```\n\n> 4. 安装本项目mineadmin-vue\n>> 复制本项目 ./MineAdmin/vue/src 下所有子文件夹，粘贴到到mineadmin-vue src目录下\n>>> cp -r ./MineAdmin/vue/src/ ./you-mineadmin-vue/src\n>>\n>> 安装 qiniu-js\n>>> yarn add qiniu-js --save\n>>\n>> 运行\n>>> yarn run dev\n\n#### 打开后台系统\n> 添加openai api key： Ai系统->openai_key->新增, 返回列表点击顶部刷新缓存\n> \n> 设置站点相关信息： Ai系统->设置\n\n\n\n### 大功告成，其他功能自行探索！\n\n.\n### 安装 uniapp\n#### 1. 按照uniapp官方文档安装好环境\n#### 2. 用HBuilderX打开本项目UniApp文件夹\n#### 3. 按照以下说明修改配置文件(相关文件都在UniApp文件夹内)\n> uniapp开发者中心  [点击获取appid](https://dev.dcloud.net.cn/pages/app/list)\n```shell\n# 修改 ./manifest.json 文件中的appid\n{\n    \"name\" : \"应用名称\",\n    \"appid\" : \"你的应用ID\",\n    \"description\" : \"\",\n    ......\n}\n\n# 复制 ./config.example.ts => ./common/config.ts 文件, 修改对应配置\nlet config = [\n    {\n        wsUrl: 'ws://开发环境的域名ws/ws-chat',\n        baseURL: 'https://开发环境的域名/api/ai/api/'\n    },\n     {\n         wsUrl: 'ws://线上域名/ws/ws-chat',\n         baseURL: 'https://线上域名/api/ai/api/'\n    }\n]\n```\n.\n#### 4. 安装以下插件(点击打开，页面最右侧导入HBuilderX)，已安装过则忽略\n> [uni-ui](https://ext.dcloud.net.cn/plugin?id=55)\n> \n> [compile-typescript](https://ext.dcloud.net.cn/plugin?name=compile-typescript) \n> \n> [compile-node-sass](https://ext.dcloud.net.cn/plugin?name=compile-node-sass) \n> \n> [pug-language](https://ext.dcloud.net.cn/plugin?name=pug-language) \n> \n> [compile-pug-cli](https://ext.dcloud.net.cn/plugin?name=compile-pug-cli)\n\n#### 6. HBuilderX最顶部： 运行->运行到浏览器\n#### 大功告成！\n\n## 其他\n## uniapp打包app、h5、小程序参考uniapp官方文档，使用HBuilderX配置manifest.json 之后进行打包操作！\n.\n## nginx配置文件可以参考如下\n```nginx\n# 前端\nlocation / {\n  index  index.html index.htm;\n  try_files $uri $uri/ /index.html;\n}\n\n # PHP后端代理，这里的 /prod/ 要跟前端 .env.production 的 VITE_APP_PROXY_PREFIX 值一致\nlocation /api/ {\n  \n  if ($request_method = 'OPTIONS') {\n    add_header Access-Control-Allow-Origin *;\n    add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS, DELETE';\n    add_header Access-Control-Allow-Headers 'DNT,Keep-Alive,User-Agent,Cache-Control,Content-Type,Authorization,X-Token';\n    return 204;\n  }\n  \n  # 将客户端的 Host 和 IP 信息一并转发到对应节点\n  proxy_set_header Host $http_host;\n  proxy_set_header X-Real-IP $remote_addr;\n  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n  # 将协议架构转发到对应节点，如果使用非https请改为http\n  proxy_set_header X-scheme https;\n\n  # 执行代理访问真实服务器\n  proxy_pass http://127.0.0.1:9501/;\n}\n\nlocation /ws/ {\n    # WebSocket Header\n    proxy_http_version 1.1;\n    proxy_set_header Upgrade websocket;\n    proxy_set_header Connection \"Upgrade\";\n\n    # 将客户端的 Host 和 IP 信息一并转发到对应节点\n    proxy_set_header X-Real-IP $remote_addr;\n    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n    proxy_set_header Host $http_host;\n\n    # 客户端与服务端无交互 60s 后自动断开连接，请根据实际业务场景设置\n    proxy_read_timeout 60s ;\n\n    # 执行代理访问真实服务器\n    proxy_pass http://127.0.0.1:9502/;\n}\n  \n# ^~ 不能去掉，/upload/ 中的 upload 可以改成其他名称\nlocation ^~ /upload/ {\n    # 将客户端的 Host 和 IP 信息一并转发到对应节点\n    proxy_set_header Host $http_host;\n    proxy_set_header X-Real-IP $remote_addr;\n    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n    # 将协议架构转发到对应节点，如果使用非https请改为http\n    proxy_set_header X-scheme https;\n\n    # 执行代理访问真实服务器\n    proxy_pass http://127.0.0.1:9501/;\n}\n```\n#### MineAdmin-vue下的\n> .env.development\n```env\nVITE_APP_BASE_URL = http://you.domain.com/api\n\nVITE_APP_UPLOAD_URL = http://you.domain.com/upload\n\nVITE_APP_WS_URL = ws://you.domain.com/ws/message.io\n```\n\n> .env.production\n```env\nVITE_APP_BASE_URL = /\n\nVITE_APP_UPLOAD_URL = http://you.domain.com/upload\n\nVITE_APP_WS_URL = ws://you.domain.com/ws/message.io\n```\n\n#### uniapp下的./common/config.ts文件如下\n```\nlet config = [\n    {\n        wsUrl: 'ws://you.domain.com/ws/ws-chat',\n        baseURL: 'https://you.domain.com/api/ai/api/'\n    },\n     {\n         wsUrl: 'ws://you.domain.com/ws/ws-chat',\n         baseURL: 'https://you.domain.com/api/ai/api/'\n    }\n]\n```\n## 免责声明\n#### 使用本软件不得用于开发违反国家有关政策的相关软件和应用，若因使用本软件造成的一切法律责任均与本人无关！\n\n"
  },
  {
    "path": "UniApp/.gitignore",
    "content": "/.idea/\n/.vscode/\n/.hbuilderx/\n/uni_modules/*\n!/uni_modules/bt-cropper_3.0.1/\n!/uni_modules/mp-html/\n/node_modules/\n/unpackage/\n/common/config.ts\n/.vite/"
  },
  {
    "path": "UniApp/App.vue",
    "content": "<script>\r\n\timport {AppInitOptionCacheKey} from \"./common/const\"\r\n\timport {useIndexStore} from './store/index'\r\n\texport default {\r\n\t\tonLaunch: function(options) {\r\n\t\t\tconsole.log('App Launch')\r\n\t\t\tuni.setStorageSync(AppInitOptionCacheKey, options)\r\n\t\t\tuseIndexStore().authorization(false)\r\n\t\t},\r\n\t\tonShow: function() {\r\n\t\t\tconsole.log('App Show')\r\n\t\t},\r\n\t\tonHide: function() {\r\n\t\t\tconsole.log('App Hide')\r\n\t\t}\r\n\t}\r\n</script>\r\n\r\n<style>\r\n\t/*每个页面公共css */\r\n  /* #ifdef H5 */\r\n  uni-page-head {\r\n    display: none;\r\n  }\r\n  /* #endif */\r\n</style>\r\n"
  },
  {
    "path": "UniApp/README.md",
    "content": "# chatGPT UniApp\n"
  },
  {
    "path": "UniApp/androidPrivacy.json",
    "content": "{\n    \"prompt\" : \"template\"\n}\n"
  },
  {
    "path": "UniApp/common/api.ts",
    "content": "import request from './utils/services'\n\nexport function getLogin(data: ptAny) {\n    return request.get('login/index', data)\n}\n\nexport function getInit() {\n    return request.get('public/init')\n}\n\nexport function getMine() {\n    return request.get('user/mine')\n}\n\nexport function getFriends(data: ptAny) {\n    return request.get('user/friends', data)\n}\n\nexport function postUserDataInfo(data: ptAny) {\n    return request.post('user/edit', data)\n}\n\nexport function getQiniuToken(scenes: string) {\n    return request.get('public/upload-token', {scenes: scenes})\n}\n\nexport function getAiRoles() {\n    return request.get('chat/roles')\n}\n\nexport function getAiModelList() {\n    return request.get('chat/model-list')\n}\n\nexport function getAiSession(data: ptAny) {\n    return request.get('chat/session', data)\n}\n\nexport function getAiSessionHistory(data: ptAny) {\n    return request.get('chat/session-history', data)\n}\n\nexport function postAiSessionClose(data: ptAny) {\n    return request.post('chat/session-close', data)\n}\n\nexport function postAiSessionShare(data: ptAny) {\n    return request.post('chat/session-share', data)\n}\n\nexport function postAiSessionDelete(data: ptAny) {\n    return request.post('chat/session-delete', data)\n}\n\nexport function getAiSessionShareList(data: ptAny) {\n    return request.get('chat/session-share-list', data)\n}\n\nexport function getAiSessionShareMessageList(data: ptAny) {\n    return request.get('chat/session-share-message-list', data)\n}\n\nexport function getAiMessages(data: ptAny) {\n    return request.get('chat/messages', data)\n}\n\nexport function getAiQuickIssue() {\n    return request.get('chat/quick-issue')\n}\n\nexport function getVipConfig() {\n    return request.get('public/vip-config')\n}\n\nexport function getOrderList(data: ptAny) {\n    return request.get('order/list', data)\n}\n\nexport function postKamiOpenVip(data: ptAny) {\n    return request.post('order/kami-open-vip', data)\n}\n\nexport function getWalletInfo() {\n    return request.get('wallet/index')\n}\n\nexport function getChangeLogList(data: ptAny) {\n    return request.get('wallet/change-log-list', data)\n}\n\nexport function getWithdrawalList(data: ptAny) {\n    return request.get('wallet/withdrawal-list', data)\n}\n\nexport function postWithdrawal(data: ptAny) {\n    return request.post('wallet/withdrawal', data)\n}"
  },
  {
    "path": "UniApp/common/const.ts",
    "content": "export const xTokenCacheKey = \"app-x-token\"; // 缓存登录token\nexport const AppNoticeCacheKey = \"app-notice\"; // 缓存提示信息\nexport const AppInitCacheKey = \"app-init\"; // 缓存公共信息\nexport const AppInitOptionCacheKey = \"app-init-options\"; // 缓存初始化参数\nexport const UserMineCacheKey = \"app-user-mine\"; // 个人中心缓存\nexport const AiRolesCacheKey = \"app-ai-room-roles\"; // ai问答角色缓存\nexport const AiBaseCacheKey = \"app-ai-room-base\"; // ai问答基础信息缓存\nexport const AiQuickIssueCacheKey = \"app-ai-room-quick-issue\"; // ai问答快捷问题列表缓存\nexport const AiLoginCacheKey = \"app-ai-login\"; // 缓存登录名密码\nexport const AiModelCacheKey = \"app-ai-model\"; // 模型设置缓存\nexport const AiModelListCacheKey = \"app-ai-model-list\"; // 模型设置缓存"
  },
  {
    "path": "UniApp/common/func.ts",
    "content": "/**\n * uploadCos.\n * 上传到七牛云\n * @param {any} qiniuInfo 七牛云预生成的信息\n * @param {String} filePath 文件的临时路径\n * @returns {Promise<any>}\n */\nexport function uploadQiniu(qiniuInfo: utilsType.qiniuInfo, filePath: string) {\n    return new Promise(async (resolve, reject) => {\n        // @ts-ignore\n        await uni.uploadFile({\n            url: qiniuInfo.service_url,\n            filePath: filePath,\n            name: 'file',\n            formData: {\n                token: qiniuInfo.token,\n                key: qiniuInfo.path\n            },\n            success: (res: ptAny) => {\n                resolve(JSON.parse(res.data))\n            },\n            fail: (err: ptAny) => {\n                reject(err)\n            }\n        })\n    })\n}"
  },
  {
    "path": "UniApp/common/utils/jump.ts",
    "content": "export const byNavigateTo = (page: string, params?: ptAny) => {\n    // @ts-ignore\n    uni.navigateTo({\n        url: page\n    }).then((r: ptAny) => {\n    })\n}\n\nexport const byRedirectTo = (page: string, params?: ptAny) => {\n    // @ts-ignore\n    uni.redirectTo({\n        url: page\n    }).then((r: ptAny) => {\n    })\n}\n\nexport const byReLaunch = (page: string, params?: ptAny) => {\n    // @ts-ignore\n    uni.reLaunch({\n        url: page\n    }).then((r: ptAny) => {\n    })\n}\n\nexport const byNavigateBack = (delta: number) => {\n    // @ts-ignore\n    uni.navigateBack({\n        delta: delta\n    }).then((r: ptAny) => {\n    })\n}\n\n"
  },
  {
    "path": "UniApp/common/utils/request.ts",
    "content": "class RequestService {\n    private before: ptAny[];\n    private after: ptAny[];\n\n    constructor() {\n        // @ts-ignore\n        this.before = []\n        this.after = []\n    }\n\n    static handleIntercept(handles: ptAny, data: utilsType.requestConfig | ptAny) {\n        return handles.reduce((old: ptAny, current: ptAny) => {\n            return current(old)\n        }, data)\n    }\n\n    use(before: ptAny, after: ptAny) {\n        typeof before === 'function' && this.before.push(before)\n        typeof after === 'function' && this.after.push(after)\n    }\n\n    get(url: ptAny, data?: ptAny) {\n        return this.request({\n            url,\n            data,\n            method: 'GET'\n        })\n    }\n\n    post(url: ptAny, data?: ptAny) {\n        return this.request({\n            url,\n            data,\n            method: 'POST'\n        })\n    }\n\n    request(config: utilsType.requestConfig) {\n        let _config = RequestService.handleIntercept(this.before, config)\n        // @ts-ignore\n        return new Promise((resolve, reject) => {\n            // @ts-ignore\n            uni.request({\n                ..._config,\n                ...{\n                    success: (res: ptAny) => {\n                        let response = RequestService.handleIntercept(this.after, res)\n                        if (response && response.statusCode === 200) {\n                            resolve(response.data)\n                        } else {\n                            reject(response.data)\n                        }\n                    },\n                    fail: (err: any) => {\n                        console.log(\"fail\", err)\n                        reject(err)\n                    }\n                }\n            })\n        })\n    }\n}\n\nconst request = new RequestService()\nexport default request"
  },
  {
    "path": "UniApp/common/utils/services.ts",
    "content": "import Request from './request'\nimport {AppNoticeCacheKey, xTokenCacheKey} from \"../const\";\nimport {useIndexStore} from '../../store'\nimport {getConfig} from \"../config\";\n\nRequest.use( (config: utilsType.requestConfig) => {\n\tif (!config.header) {\n\t\tconfig.header = {}\n\t}\n\t// @ts-ignore\n\tlet authorization = uni.getStorageSync(xTokenCacheKey)\n\n\tconfig.header['Authorization'] = 'Bearer ' + (authorization ? authorization.token : '')\n\n\tif (config.url.slice(0, 8) !== \"https://\") {\n\t\tconfig.url = getConfig().baseURL + config.url\n\t}\n\n\treturn config\n},  (response: ptAny) => {\n\tswitch (response.data.code) {\n\t\tcase 10006: // 参数错误\n\t\t\t// @ts-ignore\n\t\t\tuni.showToast({\n\t\t\t\ttitle: response.data.message,\n\t\t\t\ticon: 'none',\n\t\t\t\tduration: 1500\n\t\t\t})\n\t\t\tbreak\n\t\tcase 10001: // token 无效\n\t\t\tuseIndexStore().authorization(true)\n\t\t\tbreak\n\t\tcase 10002: // 账号被锁定\n\t\t\t// @ts-ignore\n\t\t\tuni.setStorageSync(AppNoticeCacheKey, response.data)\n\t\t\t// @ts-ignore\n\t\t\tuni.redirectTo({\n\t\t\t\turl: '/pages/notice'\n\t\t\t})\n\t\t\tbreak\n\t\tcase 10003: // 跳转到登录页\n\t\t\t// @ts-ignore\n\t\t\tuni.redirectTo({\n\t\t\t\turl: '/pages/login'\n\t\t\t})\n\t\t\tbreak\n\t\tcase 10004: // 关站维护\n\t\t\t// @ts-ignore\n\t\t\tuni.setStorageSync(AppNoticeCacheKey, response.data)\n\t\t\t// @ts-ignore\n\t\t\tuni.redirectTo({\n\t\t\t\turl: '/pages/notice'\n\t\t\t})\n\t\t\tbreak\n\t\tcase 10005: // 清空所有缓存数据\n\t\t\t// @ts-ignore\n\t\t\tuni.clearStorageSync()\n\t\t\tbreak\n\t}\n\treturn response\n})\n\nexport default Request\n"
  },
  {
    "path": "UniApp/components/AgreementPopup.vue",
    "content": "<template lang=\"pug\">\nuni-popup(ref=\"popupObj\" type=\"bottom\")\n  scroll-view.agreement_container(scroll-y=\"true\")\n    view.title {{agreementData.title}}\n    view.content {{agreementData.content}}\n    div.close(@click=\"emit('close')\")\n      button.btn 关闭\n</template>\n\n<script setup lang=\"ts\">\n// @ts-ignore\nimport {computed, ref, watch} from \"vue\"\nimport {useIndexStore} from '../store'\n// @ts-ignore\nconst props = defineProps([\"isShow\", \"scene\"])\n// @ts-ignore\nconst emit = defineEmits(['close'])\n\nconst popupObj = ref()\nconst agreementData = ref({title:'', content: '', scene: 0})\n\nconst isShowCp = computed(() => {\n  return props.isShow\n})\n\nwatch(isShowCp, (v: ptAny)=>{\n  if (v){\n    if (agreementData.value.scene !== props.scene) {\n      // todo 根据场景值获取协议\n      agreementData.value.scene = props.scene\n      if (props.scene === 1000 ) {\n        agreementData.value.title = \"用户协议\"\n        agreementData.value.content = useIndexStore().scene.agreement.user\n      }\n    }\n    popupObj.value.open()\n  }else{\n    popupObj.value.close()\n  }\n})\n</script>\n\n<style scoped lang=\"scss\">\n.agreement_container{\n  background-color: #ffffff;\n  height: 100vh;\n  .title{\n    text-align: center;\n    padding: 15rpx;\n    font-weight: bold;\n    font-size: 36rpx;\n  }\n  .content{\n    white-space: pre-wrap;\n    padding: 15rpx;\n  }\n  .close{\n    padding-bottom: 20rpx;\n  }\n  .btn{\n    width: 170rpx;\n    font-size: 30rpx;\n    color: #fff;\n    background-color: $theme-color;\n  }\n}\n</style>\n"
  },
  {
    "path": "UniApp/components/FooterCommon.vue",
    "content": "<template lang=\"pug\">\ndiv.footer\n  div.item(v-for=\"(item, key) in footer\"\n    :class=\"{active: footerIndex === key}\"\n    @click=\"operationFooter(key)\"\n  )\n    span.icon\n      uni-icons( :type=\"item.icon\" size=\"35\" :color=\"footerIndex === key ? '#55aaef' : '#ccc'\")\n    span.text {{item.name}}\n</template>\n\n<script setup>\n// @ts-ignore\nimport {ref} from \"vue\"\nimport {byRedirectTo} from \"../common/utils/jump\"\n\nconst props = defineProps({\n  footerIndex: {\n    type: Number,\n    required: false,\n    default: 0\n  }\n})\n\nconst footer = ref([\n  {\n    icon: 'chatbubble',\n    name: \"Ai问答\"\n  },\n  {\n    icon: 'medal',\n    name: \"公开频道\"\n  },\n  {\n    icon: 'home',\n    name: \"我的\"\n  },\n])\n\nconst operationFooter = (index) => {\n  if (props.footerIndex === index) {\n    return\n  }\n  switch (index) {\n    case 0:\n      byRedirectTo(\"/pages/chatgpt/room\")\n      break\n    case 1:\n      byRedirectTo(\"/pages/chatgpt/channel\")\n      break\n    case 2:\n      byRedirectTo(\"/pages/user/mine\")\n      break\n  }\n}\n\n</script>\n\n<style scoped lang=\"scss\">\n.footer {\n  display: flex;\n  flex-direction: row;\n  position: fixed;\n  bottom: 0;\n  left: 0;\n  right: 0;\n  z-index: 2;\n  width: 100%;\n  border-top: 1rpx solid #e8e8e8;\n  background-color: #fff;\n  justify-content: space-around;\n  text-align: center;\n}\n\n.item {\n  display: flex;\n  flex-direction: column;\n}\n\n.text {\n  font-size: 24rpx;\n  transform: scale(.83);\n  transform-origin: center;\n  color: #ccc;\n}\n\n.active .text {\n  color: $theme-color !important;\n}\n</style>\n"
  },
  {
    "path": "UniApp/components/Nothing.vue",
    "content": "<template lang=\"pug\">\ndiv.nothing\n  div.tips {{config.tips}}\n  div.entry(v-if=\"config.entry\" @tap=\"emit('entry', config)\") {{config.entry}}\n</template>\n\n<script setup lang=\"ts\">\n// @ts-ignore\nconst props = defineProps({\n  config:{\n    type: Object,\n    default: {\n      tips: \"暂无数据\",\n      entry: null,\n    },\n  }\n})\n// @ts-ignore\nconst emit = defineEmits(['entry'])\n</script>\n\n<style lang=\"scss\">\n.nothing {\n  text-align: center;\n  padding-top: 400rpx;\n  background-image: url('../../static/images/nothing.png');\n  background-position: center;\n  background-repeat: no-repeat;\n  background-size: auto 267rpx;\n}\n\n.tips {\n  padding: 50rpx;\n  font-size: 30rpx;\n  color: #9e9e9e;\n}\n\n.entry {\n  width: 280rpx;\n  height: 80rpx;\n  color: #333;\n  font-size: 30rpx;\n  text-align: center;\n  line-height: 80rpx;\n  border-radius: 99rpx;\n  border: 1px solid #ccc;\n  margin: auto;\n\n  &:active {\n    background: darken(#fff, 5%);\n  }\n}\n</style>"
  },
  {
    "path": "UniApp/components/OpenVipPopup.vue",
    "content": "<template lang=\"pug\">\nuni-popup(ref=\"popupObj\" type=\"bottom\" @change=\"change\")\n  scroll-view.open_vip_popup(scroll-y=\"true\")\n    div.open_vip\n      div.vip__title\n        span 原创不易，支持作者\n      uni-table(border stripe)\n        uni-tr(v-for=\"(item ,index) in equity\" :key=\"index\")\n          uni-th(align=\"center\" v-for=\"(item1 ,index1) in item\" :key=\"index1\" :style=\"{color: item1.color}\") {{item1.text}}\n      div.use_vip 当前等级: {{user.vip_name}}\n      div.notice ps：VIP有效期内,支持补差价升级，例如: VIP升级一星抵扣上一次一次订单金额\n      div.vip_list\n        div.item(v-for=\"(item ,index) in vipConfig\" :key=\"index\" @click=\"chooseVip(index)\" :class=\"[chooseItem.level === item.level ? 'vip__choose' : '',  item.is_choose ? '' : ' vip_not_choose' ]\")\n          div.box\n            span.level {{item.name}}\n            span.rmb ——RMB——\n            span.price 限时{{item.price}}\n          span.old_price 原价{{item.price_old}}\n      div.submit\n        input.kami_code(placeholder='输入卡密(暂时只支持卡密支付)' type=\"input\" :value=\"kami_code\" @input=\"(res)=>{kami_code= res.detail.value}\")\n        button.go_pay(@click=\"goPay\") {{chooseItem.btn_text}}{{chooseItem.price_pay ? '(￥'+chooseItem.price_pay+')' : ''}}\n      div.user_agreement(@click=\"agreement\")\n        span 支付即表示同意\n        span.aa 《用户协议》\n        span 购买后不支持退款\n  AgreementPopup(:isShow=\"isShowAgreement\" :scene=\"1000\" @close=\"isShowAgreement=false\")\n</template>\n\n<script lang=\"ts\" setup>\n// @ts-ignore\nimport {computed, ref, watch} from \"vue\"\nimport {getVipConfig, postKamiOpenVip} from \"../common/api\"\nimport AgreementPopup from './AgreementPopup.vue'\n// @ts-ignore\nconst props = defineProps([\"isShow\"]);\n// @ts-ignore\nconst emit = defineEmits(['close'])\nconst isShowAgreement = ref(false)\nconst popupObj = ref()\nconst equity: ptAny = ref([])\nconst user = ref({\n  vip:0,\n  vip_name: \"免费会员\"\n})\nconst vipConfig = ref<{is_choose: boolean,is_default: boolean,price_pay: number, level: number, btn_text: string}[]>([])\nconst kami_code = ref('')\nconst isShowCp = computed(() => {\n  return props.isShow\n})\n\nconst chooseItem = ref({\n  price_pay: 0,\n  level: 0,\n  btn_text: ''\n})\n\nconst agreement = ()=>{\n  isShowAgreement.value = true\n}\n\nconst change = (res: ptAny)=>{\n  if (res.show === false){\n    emit('close')\n  }\n}\n\nconst chooseVip = (index: number)=>{\n  if (!vipConfig.value[index].is_choose) {\n    // @ts-ignore\n    uni.showToast({\n      title: '不能选择低于自身等级的VIP',\n      icon: 'none',\n      duration: 1000\n    })\n    return\n  }\n  chooseItem.value = Object.assign(chooseItem.value, vipConfig.value[index])\n}\n\nconst goPay = () => {\n  if (!kami_code.value) {\n    // @ts-ignore\n    uni.showToast({\n      title: '请填写卡密',\n      icon: 'none',\n      duration: 1500\n    })\n    return\n  }\n\n  if (!chooseItem.value.level) {\n    // @ts-ignore\n    uni.showToast({\n      title: '请选择需要开通的VIP等级',\n      icon: 'none',\n      duration: 1500\n    })\n    return\n  }\n\n  postKamiOpenVip({kami_code: kami_code.value, level: chooseItem.value.level}).then((res: ptAny)=>{\n    if (res.code === 200) {\n      // @ts-ignore\n      uni.showToast({\n        title: '开通成功',\n        icon: 'success',\n        duration: 1500\n      })\n      // @ts-ignore\n      uni.clearStorageSync()\n      // @ts-ignore\n      uni.redirectTo({\n        url: '/pages/login'\n      })\n      return\n    }\n  })\n}\n\nwatch(isShowCp, v=>{\n  if (v){\n    if (vipConfig.value.length <= 0){\n      getVipConfig().then((res: ptAny)=>{\n        vipConfig.value = res.data.config\n        equity.value = res.data.equity\n        user.value = res.data.user\n        let tLen = vipConfig.value.length\n        for (let i=0; i< tLen; i++) {\n          if (vipConfig.value[i].is_default){\n            chooseItem.value = Object.assign(chooseItem.value, vipConfig.value[i])\n          }\n        }\n        popupObj.value.open()\n      })\n      return\n    }\n    popupObj.value.open()\n  }else{\n    popupObj.value.close()\n  }\n})\n</script>\n\n<style scoped lang=\"scss\">\n.open_vip_popup{\n  max-height: 80vh;\n  border-radius: 15rpx 15rpx 0 0;\n  padding-top: 15rpx;\n}\n.open_vip{\n  width: 100%;\n  background-color: #fff;\n  border-radius: 15rpx 15rpx 0 0;\n  .vip__title{\n    display: flex;\n    align-items: center;\n    padding: 15rpx;\n    justify-content: center;\n    font-weight: bold;\n    font-size: 36rpx;\n  }\n\n  .vip_list{\n    display: flex;\n    flex-direction: row;\n    justify-content: space-evenly;\n  }\n\n  .item{\n    display: flex;\n    flex-direction: column;\n    text-align: center;\n    border: .3rpx solid #efe8e8;\n    margin: 15rpx;\n    border-radius: 10%;\n    .box{\n      color: #fff;\n      background-color: #b1765f;\n      border-radius: 10% 10% 0 0;\n      display: flex;\n      flex-direction: column;\n      padding: 30rpx;\n    }\n    .level{\n      font-weight: bold;\n      font-size: 30rpx;\n    }\n    .rmb{\n      font-size: 24rpx;\n      color: #353131;\n    }\n    .price {\n      font-size: 34rpx;\n    }\n    .old_price{\n      color: #fa8806;\n      font-size: 26rpx;\n      text-decoration: line-through;\n      padding: 15rpx;\n    }\n  }\n\n  .vip_not_choose{\n    background-color: #efefef !important;\n    .box{\n      background-color: #a0a0a0 !important\n    }\n  }\n\n  .vip__choose{\n    border: .3rpx solid #fa8806;\n    .box{\n      background: linear-gradient(#FF9800 0%, #FF5722 20%, #e0562b 100%) !important;\n    }\n  }\n\n  .submit{\n    padding: 15rpx 0;\n    display: flex;\n    justify-content: center;\n    flex-direction: column;\n    align-items: center;\n    .kami_code{\n      border: #c8c7cc solid 1rpx;\n      padding: 15rpx;\n      margin: 10rpx 0;\n      width: 280px;\n    }\n  }\n\n  .go_pay{\n    width: 330rpx;\n    font-size: 30rpx;\n    color: #fff;\n    background: linear-gradient(#FF9800 0%, #FF5722 20%, #e0562b 100%) !important;\n  }\n\n  .user_agreement{\n    text-align: center;\n    font-size: 23rpx;\n    color: #8c8c8c;\n    width: 100%;\n    padding-bottom: 15rpx;\n    .aa{\n      color: #00c6ff;\n    }\n  }\n  .use_vip{\n    color: $theme-color;\n    padding: 10rpx 20rpx;\n  }\n  .notice{\n    font-size: 23rpx;\n    color: red;\n    padding: 0 20rpx;\n  }\n}\n</style>"
  },
  {
    "path": "UniApp/components/QrCodePopup.vue",
    "content": "<template lang=\"pug\">\nuni-popup(ref=\"popupObj\" :mask-click=\"false\")\n  div.code\n    div.code_content\n      div.code_middle\n        p.nickname(v-if=\"codeData.nickname\")\n          span.nickname_text {{codeData.nickname}}\n        p.text(v-if=\"codeData.text\") {{codeData.text}}\n        img.img(:src=\"codeData.codeSrc\")\n        p.prompt(v-if=\"codeData.promptText\") {{codeData.promptText}}\n        p.hint(v-if=\"codeData.hint\") {{codeData.hint}}\n    div.code_following(@click=\"emit('close')\")\n        uni-icons(type=\"close\" size=\"35\")\n</template>\n\n<script setup lang=\"ts\">\n// @ts-ignore\nimport {computed, ref, watch} from \"vue\"\n// @ts-ignore\nconst props = defineProps([\"codeData\", \"isShow\"]);\n// @ts-ignore\nconst emit = defineEmits(['close'])\n\nconst popupObj = ref()\n\nconst isShowCp = computed(() => {\n  return props.isShow\n})\n\nwatch(isShowCp, (v: ptAny) => {\n  if (v) {\n    popupObj.value.open()\n  } else {\n    popupObj.value.close()\n  }\n})\n</script>\n\n<style scoped lang=\"scss\">\n  .code {\n    width: 570rpx;\n\n    &_content {\n      width: 570rpx;\n    }\n\n    .image-bg{\n      position: absolute;\n      z-index: -1;\n      left: 0;\n      right: 0;\n      bottom: 0;\n      width: 100%;\n      height: 100%;\n    }\n\n    &_middle {\n      display: flex;\n      flex-direction: column;\n      align-items: center;\n      justify-content: center;\n      padding: 40rpx;\n      border-radius:32rpx 32rpx;\n      background-color: $uni-text-color-inverse;\n\n      .nickname {\n        color: $uni-text-color;\n        font-size: 34rpx;\n        //.ellipsisLn(1);\n\n        &_text {\n          font-weight: bold;\n        }\n      }\n\n      .text {\n        margin-top: 14rpx;\n        color: $uni-color-primary;\n        font-size: 32rpx;\n      }\n\n      .img {\n        width: 222rpx;\n        height: 222rpx;\n        margin: 20rpx 0;\n        border: 10rpx solid $uni-text-color-inverse;\n      }\n\n      .prompt {\n        color: $uni-text-color;\n        font-size: 30rpx;\n      }\n\n      .hint {\n        width: 490rpx;\n        height: 62rpx;\n        margin-top: 30rpx;\n        background: #f3f3f3;\n        border-radius: 80rpx;\n        text-align: center;\n        font-size: 22rpx;\n        color: $uni-text-color;\n        line-height: 62rpx;\n      }\n    }\n\n    &_following {\n      width: 80rpx;\n      height: 80rpx;\n      margin: 60rpx auto 0 auto;\n    }\n  }\n</style>\n"
  },
  {
    "path": "UniApp/components/Search.vue",
    "content": "<template lang=\"pug\">\ndiv.container\n  div.search_box\n    div.top(v-if=\"props.columns.length >= 0\")\n      template(v-for=\"(item, index) in props.columns\" :key=\"index\")\n        div.keyword(v-if=\"item.index === 'keyword'\")\n          div.content\n            input.input(placeholder-class=\"remind\" type=\"text\" placeholder=\"搜索内容\" v-model=\"searchForm[item.index]\" confirm-type=\"search\")\n            uni-icons.icon_search(type=\"search\" size=\"30\" color=\"#4d86b5\" @tap=\"submit\")\n      div.choose_where(v-if=\"props.columns.length >= 1\" @click=\"searchFormPopupOb.open()\")\n        uni-icons(type=\"settings\" size=\"25\" color=\"#fff\")\n        span 筛选\n  slot(:result=\"resultList\")\n  nothing(v-if=\"!resultList.length\")\n  uni-popup(ref=\"searchFormPopupOb\" type=\"bottom\")\n    div.search_form\n      template(v-for=\"(item, index) in props.columns\" :key=\"index\")\n        template(v-if=\"item.formType === 'time'\" )\n          uni-section(:title=\"item.title\" titleFontSize=\"36rpx\")\n          div.select_time\n            uni-datetime-picker(v-model=\"searchForm[item.index]\" type=\"daterange\" rangeSeparator=\"至\")\n        template(v-if=\"item.formType === 'select'\" )\n          uni-section(:title=\"item.title\" titleFontSize=\"36rpx\")\n          uni-data-checkbox.select_vip(v-model=\"searchForm[item.index]\" :localdata=\"item.data\" selectedColor=\"#fa8806\" selectedTextColor=\"#fa8806\")\n      uni-section.select_action(title=\"\")\n        button.btn(type=\"primary\" size=\"mini\" @click=\"searchFormPopupOb.close()\") 取消\n        button.btn(type=\"primary\" size=\"mini\" @click=\"searchFormPopupOb.close() & submit()\") 确定\n</template>\n\n<script setup lang=\"ts\">\n// @ts-ignore\nimport {onMounted, ref} from \"vue\"\nimport Nothing from \"../components/Nothing.vue\"\n// @ts-ignore\nconst props = defineProps({\n  // 组件设置\n  options: {\n    type: Object\n  },\n  // 字段列表\n  columns: {type: Array, default: []}\n})\n\nconst options = ref({\n  api: () => {\n  },\n  // 关键字搜索\n  isKeywordSearch: true,\n  // 每页记录数\n  pageSize: 10,\n})\n\nconst resultList = ref<Object[]>([])\n\nconst isLoadingMore = ref(false)\nconst isLastPage = ref(false)\n\n\nconst searchFormPopupOb = ref()\n\nconst searchForm = ref({last_id: 0})\n\nconst requestApi = ref()\n\nonMounted(() => {\n  options.value = Object.assign({}, options.value, props.options)\n  requestApi.value = options.value.api\n\n  let isKeyword = false\n\n  props.columns.map((item: { index: string, value: ptAny }) => {\n    searchForm.value[item.index] = item.value\n    item.index === \"keyword\" && (isKeyword = true)\n  })\n\n  if (!isKeyword && options.value.isKeywordSearch) {\n    props.columns.push({\n      index: \"keyword\",\n      title: \"搜索内容\",\n      value: \"\",\n      formType: \"input\"\n    })\n  }\n\n  let param = Object.assign({}, searchForm.value)\n\n  for (let prop in param) {\n    for (let i in props.columns) {\n      if (props.columns[i].index === prop && props.columns[i].formType === 'time') {\n        param[prop] = param[prop].length > 0 ? param[prop][0] + ',' + param[prop][1] : ''\n        break;\n      }\n    }\n  }\n\n  requestApi.value(param).then((res: ptAny) => {\n    resultList.value = res.data.list\n  })\n\n})\n\nconst loadMore = () => {\n  if (isLoadingMore.value || !resultList.value.length || isLastPage.value) {\n    return\n  }\n  isLoadingMore.value = true\n  searchForm.value.last_id = resultList.value[resultList.value.length - 1].id\n  requestApi.value(searchForm.value).then((res: ptAny) => {\n    isLoadingMore.value = false\n    if (res.data.list.length <= 0) {\n      isLastPage.value = true\n      return\n    }\n    resultList.value.push(...res.data.list)\n  })\n}\n\nconst submit = () => {\n  isLastPage.value = false\n  searchForm.value.last_id = 0\n  resultList.value = []\n\n  let param = Object.assign({}, searchForm.value)\n  for (let prop in param) {\n    for (let i in props.columns) {\n      if (props.columns[i].index === prop && props.columns[i].formType === 'time') {\n        param[prop] = param[prop].length > 0 ? param[prop][0] + ',' + param[prop][1] : ''\n        break;\n      }\n    }\n  }\n\n  requestApi.value(param).then((res: ptAny) => {\n    if (res.data.list.length <= 0) {\n      isLastPage.value = true\n      return\n    }\n    resultList.value.push(...res.data.list)\n  })\n}\n// @ts-ignore\ndefineExpose({\n  loadMore\n})\n</script>\n\n<style scoped lang=\"scss\">\n.container{\n  height: 100vh;\n  position: relative;\n}\n\n.search_box {\n  position: -webkit-sticky;\n  position: sticky;\n  background-color: #fff;\n  z-index: 1;\n  top: var(--window-top);\n}\n\n.top {\n display: flex;\n flex-direction: row;\n width: 100%;\n justify-content: center;\n background-color: $theme-color;\n align-items: center;\n}\n\n.keyword {\n display: flex;\n align-items: center;\n height: 108rpx;\n position: sticky;\n top: 0;\n z-index: 2;\n width: 70vw;\n\n .content {\n   flex: 1;\n   display: flex;\n   align-items: center;\n   height: 70rpx;\n   background-color: #f3f3f3;\n   border-radius: 16rpx;\n   padding: 0 20rpx;\n }\n\n .from {\n   flex: 1;\n }\n\n .input {\n   color: $uni-text-color;\n   font-size: 30rpx;\n   font-weight: bold;\n   width: 100%;\n }\n\n .remind {\n   color: #9e9e9e;\n   font-size: 28rpx;\n }\n}\n\n.choose_where {\n display: flex;\n align-items: center;\n justify-content: center;\n padding-left: 15rpx;\n font-size: 40rpx;\n color: $uni-text-color-inverse;\n}\n\n.search_form {\n display: flex;\n flex-direction: column;\n width: 100%;\n background: $uni-text-color-inverse;\n border-radius: 15rpx 10rpx 0 0;\n padding: 10rpx 20rpx 30rpx 20rpx;\n\n .select_vip, .select_time {\n   padding: 0 22rpx;\n }\n\n .select_action {\n   text-align: center;\n\n   .btn {\n     margin: 0 50rpx;\n     width: 200rpx;\n     background-color: $theme-color;\n   }\n }\n}\n</style>"
  },
  {
    "path": "UniApp/config.example.ts",
    "content": "let config = [\n    {\n        wsUrl: 'ws://开发环境的域名ws/ws-chat',\n        baseURL: 'https://开发环境的域名/api/ai/api/'\n    },\n    {\n        wsUrl: 'ws://线上域名/ws/ws-chat',\n        baseURL: 'https://线上域名/api/ai/api/'\n    }\n]\n\n// @ts-ignore\nlet index = process.env.NODE_ENV === 'development' ? 0 : 1\n\nexport const getConfig = () => {\n    return config[index]\n}"
  },
  {
    "path": "UniApp/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <script>\n      var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') ||\n        CSS.supports('top: constant(a)'))\n      document.write(\n        '<meta name=\"viewport\" content=\"width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +\n        (coverSupport ? ', viewport-fit=cover' : '') + '\" />')\n    </script>\n    <title></title>\n    <!--preload-links-->\n    <!--app-context-->\n  </head>\n  <body>\n    <div id=\"app\"><!--app-html--></div>\n    <script type=\"module\" src=\"/main.js\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "UniApp/logic/user.ts",
    "content": "import {UserMineCacheKey} from \"../common/const\"\nimport {getMine} from \"../common/api\"\n\nexport function userService() {\n    const initMine = async () => {\n        // @ts-ignore\n        let cache = uni.getStorageSync(UserMineCacheKey)\n        if (cache) {\n            return cache\n        }\n        let data = {}\n        await getMine().then((res: ptAny) => {\n            if (res.code === 200) {\n                data = res.data\n                // @ts-ignore\n                uni.setStorageSync(UserMineCacheKey, res.data)\n            }\n        })\n        return data\n    }\n\n    const updateMineUserCache = (data: ptAny) => {\n        // @ts-ignore\n        let cache = uni.getStorageSync(UserMineCacheKey)\n        if (cache) {\n            cache.head_img = data.head_img\n            cache.nick_name = data.nick_name\n            cache.mobile = data.mobile\n            // @ts-ignore\n            uni.setStorageSync(UserMineCacheKey, cache)\n        }\n    }\n\n    return {initMine, updateMineUserCache}\n}\n"
  },
  {
    "path": "UniApp/main.js",
    "content": "import App from './App'\n\n// #ifndef VUE3\nimport Vue from 'vue'\nimport './uni.promisify.adaptor'\nVue.config.productionTip = false\nApp.mpType = 'app'\nconst app = new Vue({\n  ...App\n})\napp.$mount()\n// #endif\n\n// #ifdef VUE3\nimport { createSSRApp } from 'vue'\nimport * as Pinia from 'pinia';\nexport function createApp() {\n  const app = createSSRApp(App)\n  app.use(Pinia.createPinia())\n  return {\n    app,\n\tPinia,\n  }\n}\n// #endif"
  },
  {
    "path": "UniApp/manifest.json",
    "content": "{\n    \"name\" : \"ChatGPT\",\n    \"appid\" : \"\",\n    \"description\" : \"ChatGPT AI问答\",\n    \"versionName\" : \"1.0.3\",\n    \"versionCode\" : \"100\",\n    \"transformPx\" : false,\n    /* 5+App特有相关 */\n    \"app-plus\" : {\n        \"usingComponents\" : true,\n        \"nvueStyleCompiler\" : \"uni-app\",\n        \"compilerVersion\" : 3,\n        \"splashscreen\" : {\n            \"alwaysShowBeforeRender\" : true,\n            \"waiting\" : true,\n            \"autoclose\" : true,\n            \"delay\" : 0\n        },\n        /* 模块配置 */\n        \"modules\" : {},\n        /* 应用发布信息 */\n        \"distribute\" : {\n            /* android打包配置 */\n            \"android\" : {\n                \"permissions\" : [\n                    \"<uses-permission android:name=\\\"android.permission.CHANGE_NETWORK_STATE\\\"/>\",\n                    \"<uses-permission android:name=\\\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\\\"/>\",\n                    \"<uses-permission android:name=\\\"android.permission.VIBRATE\\\"/>\",\n                    \"<uses-permission android:name=\\\"android.permission.READ_LOGS\\\"/>\",\n                    \"<uses-permission android:name=\\\"android.permission.ACCESS_WIFI_STATE\\\"/>\",\n                    \"<uses-feature android:name=\\\"android.hardware.camera.autofocus\\\"/>\",\n                    \"<uses-permission android:name=\\\"android.permission.ACCESS_NETWORK_STATE\\\"/>\",\n                    \"<uses-permission android:name=\\\"android.permission.CAMERA\\\"/>\",\n                    \"<uses-permission android:name=\\\"android.permission.GET_ACCOUNTS\\\"/>\",\n                    \"<uses-permission android:name=\\\"android.permission.READ_PHONE_STATE\\\"/>\",\n                    \"<uses-permission android:name=\\\"android.permission.CHANGE_WIFI_STATE\\\"/>\",\n                    \"<uses-permission android:name=\\\"android.permission.WAKE_LOCK\\\"/>\",\n                    \"<uses-permission android:name=\\\"android.permission.FLASHLIGHT\\\"/>\",\n                    \"<uses-feature android:name=\\\"android.hardware.camera\\\"/>\",\n                    \"<uses-permission android:name=\\\"android.permission.WRITE_SETTINGS\\\"/>\"\n                ]\n            },\n            /* ios打包配置 */\n            \"ios\" : {\n                \"dSYMs\" : false\n            },\n            /* SDK配置 */\n            \"sdkConfigs\" : {\n                \"ad\" : {}\n            },\n            \"icons\" : {\n                \"android\" : {\n                    \"hdpi\" : \"./static/icon/72x72.png\",\n                    \"xhdpi\" : \"./static/icon/96x96.png\",\n                    \"xxhdpi\" : \"./static/icon/144x144.png\",\n                    \"xxxhdpi\" : \"./static/icon/192x192.png\"\n                },\n                \"ios\" : {\n                    \"appstore\" : \"./static/icon/1024x1024.png\",\n                    \"ipad\" : {\n                        \"app\" : \"./static/icon/76x76.png\",\n                        \"app@2x\" : \"./static/icon/152x152.png\",\n                        \"notification\" : \"./static/icon/20x20.png\",\n                        \"notification@2x\" : \"./static/icon/40x40.png\",\n                        \"proapp@2x\" : \"./static/icon/167x167.png\",\n                        \"settings\" : \"./static/icon/29x29.png\",\n                        \"settings@2x\" : \"./static/icon/58x58.png\",\n                        \"spotlight\" : \"./static/icon/40x40.png\",\n                        \"spotlight@2x\" : \"./static/icon/80x80.png\"\n                    },\n                    \"iphone\" : {\n                        \"app@2x\" : \"./static/icon/120x120.png\",\n                        \"app@3x\" : \"./static/icon/180x180.png\",\n                        \"notification@2x\" : \"./static/icon/40x40.png\",\n                        \"notification@3x\" : \"./static/icon/60x60.png\",\n                        \"settings@2x\" : \"./static/icon/58x58.png\",\n                        \"settings@3x\" : \"./static/icon/87x87.png\",\n                        \"spotlight@2x\" : \"./static/icon/80x80.png\",\n                        \"spotlight@3x\" : \"./static/icon/120x120.png\"\n                    }\n                }\n            },\n            \"splashscreen\" : {\n                \"androidStyle\" : \"default\",\n                \"android\" : {\n                    \"hdpi\" : \"./static/icon/init-app.9.png\",\n                    \"xhdpi\" : \"./static/icon/init-app.9.png\",\n                    \"xxhdpi\" : \"./static/icon/init-app.9.png\"\n                },\n                \"iosStyle\" : \"common\",\n                \"useOriginalMsgbox\" : true\n            }\n        }\n    },\n    /* 快应用特有相关 */\n    \"quickapp\" : {},\n    /* 小程序特有相关 */\n    \"mp-weixin\" : {\n        \"appid\" : \"\",\n        \"setting\" : {\n            \"urlCheck\" : false\n        },\n        \"usingComponents\" : true\n    },\n    \"mp-alipay\" : {\n        \"usingComponents\" : true\n    },\n    \"mp-baidu\" : {\n        \"usingComponents\" : true\n    },\n    \"mp-toutiao\" : {\n        \"usingComponents\" : true\n    },\n    \"uniStatistics\" : {\n        \"enable\" : false\n    },\n    \"vueVersion\" : \"3\",\n    \"h5\" : {\n        \"devServer\" : {\n            \"https\" : false\n        }\n    },\n    \"fallbackLocale\" : \"zh-Hans\"\n}\n"
  },
  {
    "path": "UniApp/pages/chatgpt/channel.vue",
    "content": "<template lang=\"pug\">\ndiv.sessions\n  div.item(v-for=\"item in sessions\")\n    div.user\n      image.head(:src=\"item.user?.head_img? item.user.head_img : '../../static/images/chatgpt.png'\")\n      span.name {{item.user?.nick_name}}\n    div.info\n      span.message {{item.first_message.content}}\n      span.time {{item.created_at}}\n      span.all(@click=\"loadMessage(item)\") 查看全文\nuni-popup(ref=\"popupMessages\" type=\"bottom\")\n  scroll-view.message_list(@scrolltolower=\"moreMessage\" scroll-y=\"true\")\n    template(v-for=\"item in messageList\" )\n      div.content_box.user\n        image.head(:src=\"useSessionItem.user.head_img ? useSessionItem.user.head_img : '../../static/images/chatgpt.png'\")\n        div.message_box\n          div.message\n            mp-html(v-if=\"item.content\" :markdown=\"true\" :content=\"item.content\" :selectable=\"true\")\n          span.time {{item.created_at}}\n      div.content_box.ai\n        image.head(src=\"../../static/images/chatgpt.png\")\n        div.message_box\n          div.message\n            mp-html(:markdown=\"true\" :content=\"item.reply_content ? item.reply_content : '...'\" :selectable=\"true\")\n          span.time {{item.reply_at}}\nFooterCommon(:footerIndex=\"1\")\n</template>\n\n<script setup lang=\"ts\">\nimport FooterCommon from '../../components/FooterCommon.vue'\n// @ts-ignore\nimport {ref, onMounted} from 'vue'\nimport {getAiSessionShareList, getAiSessionShareMessageList} from \"../../common/api\";\n// @ts-ignore\nimport {onReachBottom} from \"@dcloudio/uni-app\"\n\nconst sessions = ref([])\nconst isOverSessions = ref(false)\nconst isLoading = ref(false)\nconst popupMessages = ref()\nconst useSessionItem = ref({\n  id: 0,\n  user: {\n    nick_name: '',\n    head_img: '',\n  }\n})\nconst isOverMessages = ref(false)\nconst messageList = ref([])\n\nonMounted(()=>{\n  getAiSessionShareList({last_id: 0}).then((res: ptAny)=>{\n    sessions.value = res.data.list\n  })\n})\n\nonReachBottom(() => {\n  if (sessions.value.length <= 0) {\n    return\n  }\n  if (isOverSessions.value || isLoading.value) {\n    return\n  }\n  getAiSessionShareList({last_id: sessions.value[sessions.value.length - 1].id}).then((res: ptAny) => {\n    if (res.data.list.length > 0) {\n      sessions.value.push(...res.data.list)\n    } else {\n      isOverSessions.value = false\n    }\n  })\n})\n\nconst loadMessage = (item: any) => {\n  if (useSessionItem.value.id != item.id) {\n    useSessionItem.value = item\n    isOverMessages.value = false\n    getAiSessionShareMessageList({sid: useSessionItem.value.id, last_id: 0}).then((res: ptAny) => {\n      messageList.value = res.data.list\n      popupMessages.value.open()\n    })\n  } else {\n    popupMessages.value.open()\n  }\n}\n\nconst moreMessage = ()=>{\n  if (isOverMessages.value){\n    return\n  }\n  getAiSessionShareMessageList({sid: useSessionItem.value.id, last_id: messageList.value[messageList.value.length-1].id}).then((res: ptAny) => {\n    if (res.data.list.length <= 0) {\n      isOverMessages.value = true\n      return\n    }\n    messageList.value.push(...res.data.list)\n  })\n}\n</script>\n\n<style scoped lang=\"scss\">\n.sessions{\n  display: flex;\n  flex-direction: column;\n  padding-bottom: 114rpx;\n  width: 100%;\n  .item{\n    width: 100%;\n    display: flex;\n    flex-direction: column;\n    border-bottom: 1rpx solid #f1f1f1;\n    margin: 15rpx auto;\n  }\n  .info{\n    display: flex;\n    flex-direction: column;\n    margin-left: 80rpx;\n    padding: 0 20rpx;\n    .message{\n      padding: 15rpx 0;\n      font-size: 30rpx;\n      color: #3F51B5;\n    }\n    .time{\n      font-size: 18rpx;\n      color: #c8c7cc;\n    }\n    .all{\n      color: #2196F3;\n      padding: 15rpx 0;\n    }\n  }\n  .user{\n    display: flex;\n    flex-direction: row;\n    align-items: center;\n    padding: 0 20rpx;\n    .head {\n      width: 70rpx;\n      height: 70rpx;\n      border-radius: 50%;\n      box-shadow: 0 15rpx 30rpx rgba(0, 0, 0, .4);\n    }\n    .name{\n      font-size: 30rpx;\n      padding-left: 15rpx;\n      max-width: 70%;\n      overflow-y: hidden;\n      white-space: nowrap;\n      text-overflow: ellipsis;\n    }\n  }\n}\n\n.message_list{\n  max-height: 80vh;\n  background-color: white;\n  border-radius: 15rpx 15rpx 0 0;\n  padding-top: 20rpx;\n  .content_box {\n    display: flex;\n    padding: 15rpx;\n    .message_box{\n      display: flex;\n      flex-direction: column;\n      .time{\n        color:  #c1c1c1;\n        font-size: 20rpx;\n        text-align: center;\n      }\n    }\n    .message {\n      max-width: 470rpx;\n      padding: 0 20rpx;\n      margin: 5rpx;\n      border-radius: 30rpx;\n      font-size: 26rpx;\n      box-shadow: 0 15rpx 30rpx rgba(0, 0, 0, .4);\n    }\n\n    .head {\n      width: 75rpx;\n      height: 75rpx;\n      border-radius: 50%;\n      background: #c1c1c1;\n      box-shadow: 0 15rpx 30rpx rgba(0, 0, 0, .4);\n    }\n  }\n\n  .ai {\n    flex-direction: row;\n\n    .message {\n      background-color: #e6edf3;\n    }\n  }\n\n  .user {\n    flex-direction: row-reverse;\n\n    .message {\n      color: #fff;\n      background-color: #8583fd;\n    }\n  }\n}\n</style>"
  },
  {
    "path": "UniApp/pages/chatgpt/room.vue",
    "content": "<template lang=\"pug\">\ndiv.room\n  div.message_list\n    template(v-for=\"item in messageList\" )\n      div.content_box.user\n        image.head(:src=\"user.head ? user.head : '../../static/images/chatgpt.png'\")\n        div.message_box\n          div.message\n            mp-html(v-if=\"item.content\" :markdown=\"true\" :content=\"item.content\" :selectable=\"true\")\n            div.dot_flashing(v-if=\"!item.content\")\n          span.time {{item.created_at}}\n      div.content_box.ai\n        image.head(src=\"../../static/images/chatgpt.png\")\n        div.message_box\n          div.message\n            mp-html(v-if=\"item.reply_content\" :markdown=\"true\" :content=\"item.reply_content\" :selectable=\"true\")\n            div.dot_flashing(v-if=\"!item.reply_content\")\n          span.time {{item.reply_at}}\n  div.fast\n    div.action(:class=\"isContext ? 'choose' : ''\" @click=\"setContext()\") 上下文\n    div.action(@click=\"showPopupIssue\") 快 问\n  div.send_line\n    div.more\n      image.icon(src=\"../../static/images/more.png\" @tap=\"showMore\")\n    div.input_message\n      uni-easyinput(type=\"textarea\" v-model=\"messageContent\" placeholder=\"请输入内容\")\n    div.send\n      uni-icons.icon(v-if=\"!isHistoryClose\" @click=\"send\" type=\"paperplane\" size=\"35\" color=\"rgb(85 170 239)\")\n      uni-icons.icon(v-if=\"isHistoryClose\" type=\"paperplane\" size=\"35\" color=\"#c1c1c1\")\nuni-popup(ref=\"popupMore\" type=\"bottom\")\n  div.popup_more\n    div.item\n      span.icon\n        uni-icons( type=\"folder-add\" size=\"35\" color=\"rgb(85 170 239)\" @click=\"newSession\")\n      span.text 新建会话\n    div.item\n      span.icon\n        uni-icons( type=\"refresh\" size=\"35\" color=\"rgb(85 170 239)\" @click=\"resetSession\")\n      span.text 重置会话\n    div.item\n      span.icon\n        uni-icons( type=\"paperplane\" size=\"35\" color=\"rgb(85 170 239)\" @click=\"shareSession(null, true)\")\n      span.text 分享会话\n    div.item\n      span.icon\n        uni-icons( type=\"list\" size=\"35\" color=\"rgb(85 170 239)\"  @click=\"historySession\")\n      span.text 历史会话\n    div.item\n      span.icon\n        uni-icons( type=\"gear\" size=\"35\" color=\"rgb(85 170 239)\"  @click=\"setModel(1)\")\n      span.text 模型设置\n    div.item\n      span.icon\n        uni-icons( type=\"vip\" size=\"35\" color=\"rgb(85 170 239)\"  @click=\"showOpenVip\")\n      span.text 解锁VIP\n    div.item\n      span.icon\n        uni-icons( type=\"medal\" size=\"35\" color=\"rgb(85 170 239)\" @click=\"byRedirectTo('/pages/chatgpt/channel')\")\n      span.text 公开频道\n    div.item\n      span.icon\n        uni-icons( type=\"home\" size=\"35\" color=\"rgb(85 170 239)\" @click=\"byRedirectTo('/pages/user/mine')\")\n      span.text 个人中心\nuni-popup(ref=\"popupIssue\" type=\"center\")\n  scroll-view.issue_scroll(scroll-y=\"true\")\n    div.issue(v-for=\"issue in quickIssueList\")\n      div.info\n        span.title {{issue.title}}\n        span.content {{issue.content}}\n      div.action(@click=\"chooseIssue(issue)\") 使用\nuni-popup(ref=\"popupSetModel\" type=\"bottom\")\n  div.set_model\n    uni-forms-item( label=\"模型名称\")\n      uni-data-select.select(v-model=\"modelCache['index']\" :localdata=\"modelList\" :clear=\"false\")\n      span.desc {{modelList[modelCache.index]?.max_tokens_text}}\n    uni-forms-item( label=\"上下文长度\")\n      uni-number-box(:min=\"1\" :max=\"6\" v-model=\"modelCache['context_length']\" :step=\"3\")\n    uni-forms-item( label=\"温  度\")\n      uni-number-box(:min=\"0\" :max=\"1\" v-model=\"modelCache['temperature']\" :step=\"0.1\")\n      span.desc 较高的温度会导致更多种类和不可预测的输出）\n    uni-forms-item( label=\"重  复\")\n      uni-number-box(:min=\"0\" :max=\"1\" v-model=\"modelCache['frequency_penalty']\" :step=\"0.1\")\n      span.desc 减少总体上使用频率较高的单词/短语的概率\n    uni-forms-item( label=\"概  率\")\n      uni-number-box(:min=\"-2\" :max=\"2\" v-model=\"modelCache['presence_penalty']\" :step=\"0.1\")\n      span.desc 减少总体上使用频率较高的单词/短语的概率\n    button.save_btn(type=\"primary\" size=\"mini\" @click=\"setModel(2)\") 确定\nuni-drawer(ref=\"popupHistory\" mode=\"left\" :width=\"320\")\n  scroll-view.history_drawer(@scrolltolower=\"moreHistorySession\" scroll-y=\"true\")\n    div.session_list\n      div.item(v-for=\"(item, key) in historySessionList\" :class=\"item.id === messageListParam.data.sid ? 'choose': ''\")\n        div.info\n          span.first_message {{item?.first_message?.content}}\n          span.created_at {{item?.created_at}}\n            span.close {{item?.close===2 ? \"已关闭\" : \"\"}}\n        div.actions(v-show=\"item.id !== messageListParam.data.sid\" )\n          span.action(@click=\"chooseHistorySession(item)\") 进入\n          span.action(@click=\"shareSession(item)\") {{item.share === 1 ? '分享' : '取消'}}\n          span.action(@click=\"deleteHistorySession(item)\") 删除\nuni-drawer(ref=\"popupNewSession\" mode=\"left\" :width=\"320\")\n  div.choose_search\n    div.content\n      input.keyword(placeholder-class=\"remind\" type=\"text\" placeholder=\"搜索内容\" v-model=\"keywords\" confirm-type=\"search\")\n      uni-icons.icon_search(type=\"search\" size=\"30\" color=\"#4d86b5\" @tap=\"confirmSearch\")\n  scroll-view.roles_scroll(:scroll-top=\"scrollTop\" scroll-y=\"true\")\n    div.roles\n      div.role(v-for=\"role in roles\")\n        span.title {{role.act}}\n        span.desc {{role.prompt}}\n        span.use_model(@click=\"chooseRole(role)\")  使用\nopen-vip(:isShow=\"isShowOpenVip\" @close=\"isShowOpenVip=false\")\n</template>\n\n<script setup lang=\"ts\">\n// @ts-ignore\nimport {ref, onMounted, nextTick, reactive} from 'vue'\n// @ts-ignore\nimport {onPullDownRefresh, onReachBottom} from \"@dcloudio/uni-app\"\nimport {\n  AiBaseCacheKey, AiModelCacheKey, AiModelListCacheKey,\n  AiQuickIssueCacheKey,\n  AiRolesCacheKey,\n  xTokenCacheKey\n} from '../../common/const'\nimport {byRedirectTo} from \"../../common/utils/jump\"\nimport {getConfig} from \"../../common/config\"\nimport {\n  getAiRoles,\n  getAiMessages,\n  getAiSession,\n  postAiSessionClose,\n  getAiQuickIssue,\n  getAiSessionHistory, postAiSessionShare, postAiSessionDelete, getAiModelList\n} from '../../common/api'\nimport {useWebsocketStore} from '../../store/websocket'\nimport {userService} from \"../../logic/user\"\nimport OpenVip from \"../../components/OpenVipPopup.vue\"\n\nconst {initMine} = userService()\n\n// 输入内容\nconst messageContent = ref('')\n// 消息列表\nconst messageList = ref<{ content: string, created_at: string, reply_content: string, reply_at: string}[]>([])\n\nconst loadMessage = ref({\n  // 上一页是否加载完\n  prev: false,\n  next: false,\n  isLoading: false\n})\nconst messageListParam = reactive({\n  data: {\n    slide: 'next',\n    sid: 0,\n    last_id: 0\n  },\n})\n\nconst modelData = ref({\n  isCache: false,\n  index: 0,\n  temperature: 0,\n  frequency_penalty: 0,\n  presence_penalty: 0,\n  context_length: 3,\n})\n\n// 更多操作\nconst popupMore = ref()\n// 历史会话\nconst popupHistory = ref()\n// 角色弹窗 新建会话\nconst popupNewSession = ref()\n// 快捷问题弹窗\nconst popupIssue = ref()\n// 角色列表\nconst roles = ref([])\nconst rolesCp = ref([])\n// websocket句柄\nconst websocketStore = useWebsocketStore()\n// 是否显示VIP弹窗\nconst isShowOpenVip = ref(false)\n// 基础缓存\nconst baseInfoCache = ref()\n// 用户基础信息\nconst user = ref({\n  head: \"\",\n  name: \"\",\n  userUid: 0,\n  vip: 0,\n})\n// 角色搜索\nconst keywords = ref(\"\")\n// 角色内容滑块\nconst scrollTop = ref(0)\n// 定时器 每3秒计算展示底部\nconst isScrollTopTime = ref(0);\n// 是否启动上下文\nconst isContext = ref(false)\n// 快捷问题列表\nconst quickIssueList = ref([])\n// 是否发送中\nconst isSend = ref(false)\n// 是否第第一次下拉\nconst isInitMoreMessage = ref(false)\n\n// 是否处于已关闭的历史对话\nconst isHistoryClose = ref(false)\nconst isLastHistory = ref(false)\n\nconst historySessionList = ref([])\n\n\nconst popupSetModel = ref()\nconst modelCache = ref({\n  index: 0,\n  temperature: 0,\n  frequency_penalty: 0,\n  presence_penalty: 0,\n  context_length: 3,\n})\n\nconst modelList = ref<{ name: string, is_vip: boolean, max_tokens: number, max_tokens_text: string, temperature: number, frequency_penalty: number, presence_penalty: number, context_length: number }[]>([])\n\nonMounted(async () => {\n  let userCache: ptAny = await initMine()\n  if (userCache) {\n    user.value.head = userCache?.head_img\n    user.value.name = userCache?.nick_name\n    user.value.userUid = userCache?.uid\n    user.value.vip = userCache?.vip\n  }\n\n  let modelCacheTemp = uni.getStorageSync(AiModelCacheKey)\n  if (modelCacheTemp) {\n    modelData.value = Object.assign({}, modelData.value, modelCacheTemp)\n    modelData.value.isCache = true\n  }\n\n  // todo 缓存角色ID 会话id\n  // @ts-ignore\n  baseInfoCache.value = uni.getStorageSync(AiBaseCacheKey)\n  if (baseInfoCache.value.sid) {\n    messageListParam.data.sid = baseInfoCache.value.sid\n    getAiMessages(messageListParam.data).then((res: ptAny) => {\n      messageList.value = res.data.list\n    })\n  } else {\n    await getAiSession({prompt_id: baseInfoCache.value.prompt_id ? baseInfoCache.value.prompt_id : 0}).then((session: ptAny) => {\n      baseInfoCache.value = {sid: session.data.sid, prompt_id: session.data.prompt_id}\n      // @ts-ignore\n      uni.setStorageSync(AiBaseCacheKey, baseInfoCache.value)\n      if (!session.data.is_new) {\n        messageListParam.data.sid = baseInfoCache.value.sid\n        getAiMessages(messageListParam.data).then((res: ptAny) => {\n          messageList.value = res.data.list\n        })\n      }\n    })\n  }\n\n  // @ts-ignore\n  let cache = uni.getStorageSync(AiRolesCacheKey)\n  if (cache) {\n    roles.value = cache\n    rolesCp.value = cache\n    for (let key in rolesCp.value) {\n      if (rolesCp.value[key].id === baseInfoCache.value.prompt_id) {\n        // @ts-ignore\n        uni.setNavigationBarTitle({\n          title: \"角色:\" + rolesCp.value[key].act\n        });\n        break;\n      }\n    }\n  } else {\n    getAiRoles().then((res: ptAny) => {\n      if (res.code === 200) {\n        // @ts-ignore\n        uni.setStorageSync(AiRolesCacheKey, res.data.list)\n        roles.value = res.data.list\n        rolesCp.value = res.data.list\n        for (let key in rolesCp.value) {\n          if (rolesCp.value[key].id === baseInfoCache.value.prompt_id) {\n            // @ts-ignore\n            uni.setNavigationBarTitle({\n              title: \"角色:\" + rolesCp.value[key].act\n            });\n            break;\n          }\n        }\n      }\n    })\n  }\n\n  try {\n    websocketStore.bindMessageHandle({\n      type: \"message\",\n      event: (res: ptAny) => {\n        let l = messageList.value.length - 1\n        messageList.value[l].reply_content += res.content\n        if (res.status) {\n          isSend.value = false\n          isScrollTopTime.value && clearInterval(isScrollTopTime.value)\n          setTimeout(() => {\n            scrollTopButtom()\n          }, 100)\n        }\n      }\n    })\n\n    websocketStore.bindMessageHandle({\n      type: \"error\",\n      event: (res: ptAny) => {\n        isSend.value = false\n        isScrollTopTime.value && clearInterval(isScrollTopTime.value)\n        // @ts-ignore\n        uni.showToast({\n          title: res.content,\n          icon: 'none',\n          duration: 1500\n        })\n      }\n    })\n\n    websocketStore.websocketInit()\n  } catch (e) {\n    // @ts-ignore\n    uni.showToast({\n      title: '初始化失败，请重试！',\n      icon: 'none',\n      duration: 1500\n    })\n  }\n})\n\nnextTick(() => {\n  setTimeout(() => {\n    scrollTopButtom()\n  }, 500)\n})\n\nonPullDownRefresh(() => {\n  if (isInitMoreMessage.value) {\n    moreMessage(\"prev\")\n  } else {\n    // @ts-ignore\n    uni.stopPullDownRefresh()\n    isInitMoreMessage.value = true\n  }\n})\n\nonReachBottom(() => {\n  if (isInitMoreMessage.value) {\n    moreMessage(\"next\")\n  } else {\n    isInitMoreMessage.value = true\n  }\n})\n\nconst setContext = () => {\n  if (user.value.vip === 0) {\n    // @ts-ignore\n    uni.showToast({\n      title: '连接上下文可以获得更好的聊天体验，目前只会连接最近4条消息，只针对付费用户使用！',\n      icon: 'none',\n      duration: 4500\n    })\n    return\n  }\n  isContext.value = !isContext.value\n}\n\nconst moreMessage = (slide: string) => {\n  if (slide === \"prev\" && loadMessage.value.prev) {\n    // @ts-ignore\n    uni.stopPullDownRefresh()\n    return\n  }\n  if (slide === \"next\" && loadMessage.value.next) {\n    return\n  }\n\n  if (loadMessage.value.isLoading) {\n    // @ts-ignore\n    slide === \"prev\" && uni.stopPullDownRefresh()\n    return\n  }\n  loadMessage.value.isLoading = true\n  if (slide === \"next\") {\n    let last_id = messageList.value[messageList.value.length - 1]?.id\n    if (!last_id) {\n      // 属于新建聊天内容 已经是最后一条\n      loadMessage.value.next = true\n      loadMessage.value.isLoading = false\n      return\n    }\n    messageListParam.data.last_id = last_id\n  } else {\n    messageListParam.data.last_id = messageList.value.length > 0 ? messageList.value[0].id : 0\n  }\n\n  messageListParam.data.slide = slide\n  getAiMessages(messageListParam.data).then((res: ptAny) => {\n    if (res.data.list.length > 0) {\n      if (slide === \"next\") {\n        messageList.value.push(...res.data.list)\n      } else {\n        messageList.value = res.data.list.concat(messageList.value)\n      }\n    } else {\n      loadMessage.value[slide] = true\n    }\n    loadMessage.value.isLoading = false\n    // @ts-ignore\n    slide === \"prev\" && uni.stopPullDownRefresh()\n  })\n}\n\nconst newSession = () => {\n  popupNewSession.value.open()\n}\n\nconst chooseIssue = (issue: ptAny) => {\n  messageContent.value = issue.content\n  popupIssue.value.close()\n}\n\nconst setModel = async (mark: number) => {\n  if (1 === mark) {\n    if (modelList.value.length === 0){\n      // @ts-ignore\n      let cache = uni.getStorageSync(AiModelListCacheKey)\n      if (!cache) {\n        await getAiModelList().then((res: ptAny) => {\n          cache = res.data\n          // @ts-ignore\n          uni.setStorageSync(AiModelListCacheKey, cache)\n        })\n      }\n      modelList.value = cache\n    }\n\n    if (modelData.value.isCache) {\n      modelCache.value = Object.assign({}, modelCache.value, modelData.value)\n    } else {\n      modelCache.value = Object.assign({}, modelCache.value, modelList.value[modelData.value.index])\n    }\n    popupSetModel.value.open()\n    return\n  }\n\n  if (modelList.value[modelData.value.index].is_vip && !user.value.vip) {\n    // @ts-ignore\n    uni.showToast({\n      title: '该模型只有VIP才能使用哦！',\n      icon: 'none',\n      duration: 4500\n    })\n    return\n  }\n  // @ts-ignore\n  uni.setStorageSync(AiModelCacheKey, modelCache.value)\n  for (let prop in modelData.value) {\n    if (modelCache.value.hasOwnProperty(prop)) {\n      modelData.value[prop] = modelCache.value[prop];\n    }\n  }\n\n  popupMore.value.close()\n  popupSetModel.value.close()\n}\n\nconst showOpenVip = () => {\n  isShowOpenVip.value = true\n  popupMore.value.close()\n}\n\nconst showPopupIssue = () => {\n  if (quickIssueList.value.length > 0) {\n    popupIssue.value.open()\n    return\n  }\n  // @ts-ignore\n  let cache = uni.getStorageSync(AiQuickIssueCacheKey)\n  if (cache) {\n    quickIssueList.value = cache\n    popupIssue.value.open()\n    return\n  }\n  getAiQuickIssue().then((res: ptAny) => {\n    quickIssueList.value = res.data.list\n    popupIssue.value.open()\n    // @ts-ignore\n    uni.setStorageSync(AiQuickIssueCacheKey, quickIssueList.value)\n  })\n}\n\nconst resetSession = () => {\n  if (isHistoryClose.value) {\n    // @ts-ignore\n    uni.showToast({\n      title: '历史会话无法重置',\n      icon: 'none',\n      duration: 1500\n    })\n    return\n  }\n  // @ts-ignore\n  uni.showModal({\n    title: '重置会话',\n    content: '重置会话会将当前会话存入历史记录，然后根据当前所选角色重新创建一个新的会话',\n    success:  (res: ptAny)=>{\n      if (res.confirm) {\n        postAiSessionClose({sid: baseInfoCache.value.sid, prompt_id: baseInfoCache.value.prompt_id,}).then((res: ptAny) => {\n          if(res.code === 200) {\n            messageList.value = []\n            messageListParam.data.sid = res.data.sid\n            messageListParam.data.last_id = 0\n            baseInfoCache.value = {sid: res.data.sid, prompt_id: baseInfoCache.value.prompt_id}\n            // @ts-ignore\n            uni.setStorageSync(AiBaseCacheKey, baseInfoCache.value)\n            popupMore.value.close()\n          }\n        })\n      } else if (res.cancel) {\n      }\n    }\n  });\n\n}\n\nconst chooseRole = (role: ptAny) => {\n  // 查找会话\n  getAiSession({prompt_id: role.id}).then((session: ptAny) => {\n    // @ts-ignore\n    uni.setNavigationBarTitle({\n      title: \"角色:\" + role.act\n    });\n\n    baseInfoCache.value = {sid: session.data.sid, prompt_id: session.data.prompt_id}\n    // @ts-ignore\n    uni.setStorageSync(AiBaseCacheKey, baseInfoCache.value)\n    if (session.data.is_new) {\n      messageList.value = []\n    } else {\n      messageListParam.data.sid = baseInfoCache.value.sid\n      messageListParam.data.slide = \"next\"\n      messageListParam.data.last_id = 0\n      getAiMessages(messageListParam.data).then((res: ptAny) => {\n        loadMessage.value.next = false\n        loadMessage.value.prev = false\n        messageList.value = res.data.list\n      })\n    }\n    isHistoryClose.value = false\n    popupNewSession.value.close()\n    popupMore.value.close()\n  })\n}\n\nconst historySession = () => {\n  if (historySessionList.value.length) {\n    popupHistory.value.open()\n    return\n  }\n  getAiSessionHistory({}).then((res: ptAny) => {\n    historySessionList.value = res.data.list\n    popupHistory.value.open()\n  })\n}\n\nconst moreHistorySession = () => {\n  // 加载更多\n  if (isLastHistory.value) {\n    return\n  }\n  let last_id = 0\n  if (historySessionList.value.length > 0) {\n    last_id = historySessionList.value[historySessionList.value.length - 1].id\n  }\n  getAiSessionHistory({last_id: last_id}).then((res: ptAny) => {\n    if (res.data.list.length > 0) {\n      historySessionList.value.push(...res.data.list)\n    } else {\n      isLastHistory.value = true\n    }\n  })\n}\n\nconst chooseHistorySession = (item: ptAny) => {\n  if (item.close === 2) {\n    isHistoryClose.value = true\n  } else {\n    baseInfoCache.value = {sid: item.id, prompt_id: item.prompt_id}\n    // @ts-ignore\n    uni.setStorageSync(AiBaseCacheKey, baseInfoCache.value)\n    isHistoryClose.value = false\n  }\n  for (let key in rolesCp.value) {\n    if (rolesCp.value[key].id === item.prompt_id) {\n      // @ts-ignore\n      uni.setNavigationBarTitle({\n        title: \"角色:\" + rolesCp.value[key].act\n      });\n      break;\n    }\n  }\n  messageListParam.data.sid = item.id\n  messageListParam.data.slide = \"next\"\n  messageListParam.data.last_id = 0\n  getAiMessages(messageListParam.data).then((res: ptAny) => {\n    loadMessage.value.next = false\n    loadMessage.value.prev = false\n    messageList.value = res.data.list\n    popupMore.value.close()\n    popupHistory.value.close()\n    setTimeout(() => {\n      scrollTopButtom()\n    }, 100)\n  })\n}\n\nconst deleteHistorySession = (item: ptAny) => {\n  // @ts-ignore\n  uni.showModal({\n    title: '删除会话',\n    content: '删除会话会删除当前选择的会话，一旦删除将无法找回!',\n    success: (res: ptAny) => {\n      if (res.confirm) {\n        postAiSessionDelete({id: item.id}).then((res: ptAny) => {\n          if (res.code === 200 && historySessionList.value.length > 0) {\n            historySessionList.value = historySessionList.value.filter((i: ptAny) => {\n              return i.id != item.id\n            })\n          }\n        })\n\n      } else if (res.cancel) {\n      }\n    }\n  })\n}\n\nconst shareSession = (item: ptAny, isShare: false) => {\n  // @ts-ignore\n  uni.showModal({\n    title: '分享会话',\n    content: '将会推送到公开频道!',\n    success: (res: ptAny) => {\n      if (res.confirm) {\n        let id = item?.id ? item?.id : baseInfoCache.value.sid\n        // todo\n        postAiSessionShare({id: id, is_share: isShare}).then((res: ptAny) => {\n          if (res.code === 200 && historySessionList.value.length > 0) {\n            // 更改session列表中的状态\n            for (let key in historySessionList.value) {\n              if (historySessionList.value[key].id === id) {\n                historySessionList.value[key].share = historySessionList.value[key].share === 1 ? 2 : 1\n                break;\n              }\n            }\n          }\n        })\n\n      } else if (res.cancel) {\n      }\n    }\n  })\n}\n\nconst confirmSearch = () => {\n  if (!keywords.value) {\n    roles.value = rolesCp.value\n    return\n  }\n  let res = []\n  for (let key in rolesCp.value) {\n    if ((new RegExp(keywords.value, 'i')).test(rolesCp.value[key].prompt)) {\n      res.push(rolesCp.value[key])\n    }\n  }\n  roles.value = res\n}\n\nconst showMore = () => {\n  popupMore.value.open()\n}\n\nconst send = () => {\n  if (isHistoryClose.value) {\n    // @ts-ignore\n    uni.showToast({\n      title: '当前处于历史会话哦！',\n      icon: 'none',\n      duration: 1500\n    })\n    return\n  }\n  if (isSend.value) {\n    // @ts-ignore\n    uni.showToast({\n      title: '上一次提问还未结束，请等待！',\n      icon: 'none',\n      duration: 1500\n    })\n    return\n  }\n\n  if (!messageContent.value || messageContent.value.length < 5) {\n    // @ts-ignore\n    uni.showToast({\n      title: '内容过短！',\n      icon: 'none',\n      duration: 1500\n    })\n    return\n  }\n\n  if (messageContent.value && messageContent.value.length > 1000) {\n    // @ts-ignore\n    uni.showToast({\n      title: '内容过长！',\n      icon: 'none',\n      duration: 1500\n    })\n    return\n  }\n\n  let context = isContext.value ? messageList.value.slice(-modelData.value.context_length) : []\n\n  isSend.value = true\n  messageList.value.push({\n    content: messageContent.value,\n    reply_content: '',\n    created_at: '',\n    reply_at: '',\n  })\n\n  websocketStore.send({\n    type: 'message',\n    prompt_id: baseInfoCache.value.prompt_id,\n    sid: baseInfoCache.value.sid,\n    content: messageContent.value,\n    context: context,\n    model: modelData.value\n  })\n\n  messageContent.value = \"\"\n  isScrollTopTime.value && clearInterval(isScrollTopTime.value)\n  isScrollTopTime.value = setInterval(() => {\n    scrollTopButtom()\n  }, 1000)\n}\n\nconst scrollTopButtom = () => {\n  // @ts-ignore\n  let query = uni.createSelectorQuery()\n  query.select('.message_list').boundingClientRect()\n  query.select('.send_line').boundingClientRect()\n  query.exec((res: ptAny) => {\n    // @ts-ignore\n    uni.pageScrollTo({\n      duration: 100,// 过渡时间\n      scrollTop: res[0].bottom + res[0].height + res[1].height + res[1].bottom + 1000,// 滚动的实际距离\n    })\n  })\n}\n\nconst send1 = () => {\n  // @ts-ignore\n  let authorization = uni.getStorageSync(xTokenCacheKey)\n  let eventSource = new EventSource(getConfig().baseURL + 'test');\n  let msg = \"\"\n  let index = messageList.value.push({\n    id: 1,\n    user_id: 0,\n    content: \"\"\n  }) - 1\n\n  eventSource.onmessage = (e: ptAny) => {\n    let data = JSON.parse(e.data);\n    if (data.status === 0) {\n      eventSource.close();\n    } else if (data.status === 1) {\n      msg += data.content\n      messageList.value[index].content += data.content\n    } else if (data.status === 2) {\n      eventSource.close();\n    }\n  };\n  eventSource.onerror = (e: ptAny) => {\n    console.log(\"onerror\", e);\n    eventSource.close();\n  };\n\n}\n</script>\n\n<style scoped lang=\"scss\">\n:deep(.open_vip .uni-table) {\n  min-width: unset !important;\n}\n\n:deep(.open_vip .uni-table-th) {\n  font-size: 23rpx;\n}\n\n:deep(.uni-easyinput__content-textarea) {\n  height: 92rpx !important;\n  min-height: 92rpx !important;\n}\n\n:deep(.roles_scroll .uni-scroll-view-content) {\n  width: 100% !important;\n  height: 90vh !important;\n  display: flex !important;\n  flex-direction: row !important;\n  flex-wrap: wrap !important;\n  justify-content: center !important;\n  align-content: flex-start !important;\n}\n\n.dot_flashing {\n  margin: 15rpx 40rpx;\n  position: relative;\n  width: 10px;\n  height: 10px;\n  border-radius: 5px;\n  background-color: #9880ff;\n  color: #9880ff;\n  animation: dotFlashing 1s infinite linear alternate;\n  animation-delay: .5s;\n}\n\n.dot_flashing::before, .dot_flashing::after {\n  content: '';\n  display: inline-block;\n  position: absolute;\n  top: 0;\n}\n\n.dot_flashing::before {\n  left: -15px;\n  width: 10px;\n  height: 10px;\n  border-radius: 5px;\n  background-color: #9880ff;\n  color: #9880ff;\n  animation: dotFlashing 1s infinite alternate;\n  animation-delay: 0s;\n}\n\n.dot_flashing::after {\n  left: 15px;\n  width: 10px;\n  height: 10px;\n  border-radius: 5px;\n  background-color: #9880ff;\n  color: #9880ff;\n  animation: dotFlashing 1s infinite alternate;\n  animation-delay: 1s;\n}\n\n@keyframes dotFlashing {\n  0% {\n    background-color: #9880ff;\n  }\n  50%,\n  100% {\n    background-color: #ebe6ff;\n  }\n}\n\n.message_list {\n  width: 100%;\n  padding-bottom: 160rpx;\n  //max-height: 100vh;\n\n  .content_box {\n    display: flex;\n    padding: 15rpx;\n    .message_box{\n      display: flex;\n      flex-direction: column;\n      .time{\n        color:  #c1c1c1;\n        font-size: 20rpx;\n        text-align: center;\n      }\n    }\n    .message {\n      max-width: 470rpx;\n      padding: 0 20rpx;\n      margin: 5rpx;\n      border-radius: 30rpx;\n      font-size: 26rpx;\n      box-shadow: 0 15rpx 30rpx rgba(0, 0, 0, .4);\n      display: flex;\n      align-items: center;\n    }\n\n    .head {\n      width: 75rpx;\n      height: 75rpx;\n      border-radius: 50%;\n      background: #c1c1c1;\n      box-shadow: 0 15rpx 30rpx rgba(0, 0, 0, .4);\n    }\n  }\n\n  .ai {\n    flex-direction: row;\n\n    .message {\n      background-color: #e6edf3;\n    }\n  }\n\n  .user {\n    flex-direction: row-reverse;\n\n    .message {\n      color: #fff;\n      background-color: #8583fd;\n    }\n  }\n}\n\n.fast {\n  position: fixed;\n  left: 0;\n  z-index: 5;\n  width: auto;\n  bottom: 160rpx;\n\n  .action {\n    font-size: 23rpx;\n    padding: 5rpx;\n    margin: 10rpx 0;\n    background-color: #c1c1c1;\n    border-radius: 0 15rpx 15rpx 0;\n    opacity: 0.7;\n  }\n\n  .choose {\n    color: red;\n  }\n}\n\n.send_line {\n  width: 100%;\n  position: fixed;\n  bottom: 0;\n  display: flex;\n  flex-direction: row;\n  padding: 15rpx 0;\n  align-items: center;\n  justify-content: center;\n  background-color: aliceblue;\n\n  .input_message {\n    width: calc(100% - 215rpx);\n  }\n\n  .more {\n    padding: 10rpx;\n\n    .icon {\n      width: 80rpx;\n      height: 80rpx;\n    }\n  }\n\n  .send {\n    width: 80rpx;\n    padding: 10rpx;\n  }\n}\n\n.popup_more {\n  background-color: #f1f1f1;\n  display: flex;\n  flex-direction: row;\n  flex-wrap: wrap;\n  justify-content: space-between;\n  border-radius: 30rpx 30rpx 0 0;\n  padding: 20rpx;\n\n  .item {\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n    border: 1rpx solid #e5e4e7;\n    border-radius: 15rpx;\n    padding: 15rpx;\n    margin: 15rpx;\n\n    .text {\n      font-size: 24rpx;\n      transform: scale(.83);\n      transform-origin: center;\n      color: #282626;\n    }\n  }\n}\n\n.issue_scroll {\n  max-height: 70vh;\n  display: flex;\n  flex-direction: column;\n  background-color: #f6f6f7;\n  border-radius: 15rpx;\n  width: 80%;\n  margin: auto;\n  padding: 10rpx 0;\n  .issue {\n    padding: 20rpx;\n    display: flex;\n    flex-direction: row;\n    justify-content: space-around;\n    background-color: #fff;\n    margin: 10px;\n    border-radius: 15rpx;\n\n    .info {\n      width: 80%;\n      display: flex;\n      flex-direction: column;\n    }\n\n    .title {\n      font-size: 26rpx;\n      font-weight: bold;\n    }\n\n    .content {\n      font-size: 20rpx;\n      color: #999999;\n    }\n\n    .action {\n      color: #2196F3;\n    }\n  }\n}\n\n.roles {\n  width: 100%;\n  height: 90vh;\n  display: flex;\n  flex-direction: row;\n  flex-wrap: wrap;\n  justify-content: center;\n  align-content: flex-start;\n\n  .role {\n    margin: 10rpx;\n    width: 40%;\n    display: flex;\n    flex-direction: column;\n    background-color: #8583fd;\n    padding: 15rpx;\n    border-radius: 20rpx;\n    justify-content: space-evenly;\n    height: auto;\n\n    .title {\n      color: #fff;\n      font-size: 29rpx;\n    }\n\n    .desc {\n      color: #fff;\n      font-size: 23rpx;\n      padding: 10rpx 0 5rpx 0;\n      overflow-x: hidden;\n    }\n\n    .use_model {\n      color: #FFC107;\n      margin: 5px 0 0 0;\n      text-align: center;\n      border: 1rpx solid #fff;\n    }\n  }\n}\n\n.choose_search {\n  display: flex;\n  align-items: center;\n  position: sticky;\n  padding: 0 30rpx;\n  top: 0;\n  z-index: 2;\n  height: 10vh;\n  .content {\n    flex: 1;\n    display: flex;\n    align-items: center;\n    height: 70rpx;\n    background-color: #f3f3f3;\n    border-radius: 16rpx;\n    padding: 0 20rpx;\n  }\n\n  .from {\n    flex: 1;\n  }\n\n  .keyword {\n    color: $uni-text-color;\n    font-size: 30rpx;\n    font-weight: bold;\n    width: 100%;\n  }\n\n  .remind {\n    color: #9e9e9e;\n    font-size: 28rpx;\n  }\n}\n\n.history_drawer {\n  background-color: #f6f6f7;\n  max-height: 100vh;\n  padding: 10rpx 0;\n\n  .session_list {\n    display: flex;\n    flex-direction: column;\n\n    .choose {\n      background-color: #cce0ef !important;\n    }\n\n    .item {\n      display: flex;\n      flex-direction: row;\n      justify-content: space-between;\n      align-items: center;\n      padding: 15rpx 30rpx;\n      margin: 15rpx 0;\n      background-color: #fff;\n    }\n\n    .info {\n      display: flex;\n      flex-direction: column;\n      width: 80%;\n    }\n\n    .first_message {\n      font-size: 26rpx;\n      font-weight: bold;\n      padding: 5rpx;\n    }\n\n    .created_at {\n      font-size: 20rpx;\n      color: #999999;\n      padding: 5rpx;\n    }\n\n    .close {\n      padding-left: 10rpx;\n      color: #f0ad4e;\n    }\n\n    .actions {\n      color: #2196F3;\n      display: flex;\n      flex-direction: column;\n\n      .action {\n        margin: 3rpx;\n      }\n    }\n  }\n}\n\n:deep(.set_model .uni-forms-item){\n  padding: 0 15rpx;\n  margin-bottom: 5rpx;\n}\n:deep(.set_model .uni-forms-item__label){\n  width: 160rpx!important;\n}\n\n.set_model{\n  display: flex;\n  flex-direction: column;\n  flex-wrap: wrap;\n  justify-content: space-between;\n  border-radius: 30rpx 30rpx 0 0;\n  width: 100%;\n  background: $uni-text-color-inverse;\n  padding-top: 30rpx;\n  .desc{\n    font-size: 20rpx;\n    color: red;\n  }\n  .save_btn{\n    width: 60%;\n    height: 78rpx;\n    background: $theme-color;\n    box-shadow: 0 6rpx 12rpx rgba(254, 27, 27, .3);\n    border-radius: 48rpx;\n    font-size: 36rpx;\n    font-weight: bold;\n    color: $uni-text-color-inverse;\n    margin-left: 20%;\n    margin-top: 25rpx;\n    margin-bottom: 25rpx;\n  }\n}\n</style>"
  },
  {
    "path": "UniApp/pages/login.vue",
    "content": "<template lang=\"pug\">\r\ndiv.login\r\n  div.header\r\n    div.hello\r\n      text {{type === \"login\" ? \"Hi，欢迎使用\" : \"Hi，欢迎注册\"}}\r\n  div.content\r\n    div.tip 账号\r\n    input.input(:value=\"loginData.user_name\" @input=\"(res)=>{loginData.user_name = res.detail.value}\" type=\"text\" placeholder=\"请输入账号\" placeholder-class=\"placeholder\")\r\n\r\n    div.tip 密码\r\n    input.input(:value=\"loginData.password\" @input=\"(res)=>{loginData.password = res.detail.value}\" type=\"password\" placeholder=\"请输入密码\" placeholder-class=\"placeholder\")\r\n\r\n    div.tip(v-show=\"type === 'register'\") 邀请码\r\n    input.input(:value=\"loginData.vid\" v-show=\"type === 'register'\" @input=\"(res)=>{loginData.vid = res.detail.value}\" type=\"text\" placeholder=\"必填\" placeholder-class=\"placeholder\")\r\n\r\n    div.tip.link( @tap=\"changeType\") {{type === \"login\" ? \"还没有账号？快去注册吧\" : \"已有账号，前往登录\"}}\r\n    button.submit(type=\"button\" @click=\"loginHandle\") {{type === \"login\" ? \"登录\" : \"注册\"}}\r\n</template>\r\n\r\n<script lang=\"ts\" setup>\r\n// @ts-ignore\r\nimport {onMounted, ref} from \"vue\"\r\nimport {getLogin} from \"../common/api\"\r\nimport {AiLoginCacheKey, xTokenCacheKey} from \"../common/const\"\r\nimport {useIndexStore} from '../store'\r\n\r\nconst type = ref(\"login\")\r\n\r\nconst loginData = ref({\r\n  user_name: '',\r\n  password: '',\r\n  vid: '',\r\n})\r\n\r\nconst route: any = ref('/pages/user/mine')\r\n\r\nconst store = useIndexStore()\r\n\r\nonMounted(() => {\r\n  // @ts-ignore\r\n  let pages = getCurrentPages()\r\n  if (pages.length) {\r\n    let tmp = pages[pages.length - 1].route\r\n    if (/^pages/.test(tmp) && '/' + tmp != \"/pages/login\") {\r\n      route.value = '/' + tmp\r\n    }\r\n  }\r\n  // @ts-ignore\r\n  let cache = uni.getStorageSync(AiLoginCacheKey)\r\n  if (cache) {\r\n    loginData.value = cache\r\n  }\r\n})\r\n\r\nconst changeType = () => {\r\n  type.value = type.value === \"register\" ? \"login\" : \"register\"\r\n}\r\n\r\nconst loginHandle = () => {\r\n  if (loginData.value.user_name === \"\" || !/[a-zA-Z0-9]{6,18}$/.exec(loginData.value.user_name)) {\r\n    // @ts-ignore\r\n    uni.showToast({\r\n      title: '用户名只能是6-18位大小写字母、数字(可组合)',\r\n      icon: 'none',\r\n      duration: 1500\r\n    })\r\n    return\r\n  }\r\n\r\n  if (loginData.value.password === \"\" || !/[a-zA-Z0-9]{6,18}$/.exec(loginData.value.password)) {\r\n    // @ts-ignore\r\n    uni.showToast({\r\n      title: '密码只能是6-18位大小写字母、数字(可组合)',\r\n      icon: 'none',\r\n      duration: 1500\r\n    })\r\n    return\r\n  }\r\n\r\n  if (type.value === \"register\" && loginData.value.vid === \"\") {\r\n    // @ts-ignore\r\n    uni.showToast({\r\n      title: '邀请人ID必须',\r\n      icon: 'none',\r\n      duration: 1500\r\n    })\r\n    return\r\n  }\r\n\r\n  getLogin(loginData.value).then(async (res: ptAny) => {\r\n    if (res.code === 200) {\r\n      // @ts-ignore\r\n      uni.clearStorageSync()\r\n      // @ts-ignore\r\n      uni.setStorageSync(xTokenCacheKey, {\r\n        token: res.data.token,\r\n        check_status: res.data.check_status,\r\n        login_time: res.data.login_time,\r\n        version: res.data.version,\r\n      })\r\n      // @ts-ignore\r\n      uni.setStorageSync(AiLoginCacheKey, loginData.value)\r\n      await store.setInit().then(async (r: ptAny) => {\r\n      })\r\n      // @ts-ignore\r\n      uni.reLaunch({url: route.value})\r\n    } else {\r\n      // @ts-ignore\r\n      uni.showToast({\r\n        title: res.message,\r\n        icon: 'none',\r\n        duration: 1500\r\n      })\r\n    }\r\n  })\r\n}\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n\t.login {\r\n    height: 80vh;\r\n    display: flex;\r\n    flex-direction: column;\r\n    justify-content: center;\r\n    align-items: center;\r\n\r\n    .header{\r\n      padding-bottom: 50rpx;\r\n      width: 60%;\r\n      .hello {\r\n        font-size: 50rpx;\r\n        font-weight: 500;\r\n        display: flex;\r\n        color: var(--black);\r\n        align-items: center;\r\n      }\r\n    }\r\n    .content{\r\n      width: 60%;\r\n      .input {\r\n        border: 0;\r\n        color: var(--black);\r\n        line-height: 70rpx;\r\n        border-bottom: 1px solid #ccc;\r\n        margin-top: 30rpx;\r\n        padding-bottom: 20rpx;\r\n      }\r\n\r\n      .placeholder {\r\n        color: #c0c3cb;\r\n        font-size: 32rpx;\r\n      }\r\n\r\n      .tip{\r\n        font-size: 28rpx;\r\n        margin: 25rpx 0 0 0rpx;\r\n        color: var(--black);\r\n      }\r\n\r\n      .link {\r\n        color: $uni-color-primary;\r\n      }\r\n      .submit {\r\n        margin-top: 50rpx;\r\n        height: 80rpx;\r\n        background: $theme-color;\r\n        color: #fff;\r\n        border-radius: 45rpx;\r\n        font-size: 32rpx;\r\n        line-height: 80rpx;\r\n      }\r\n    }\r\n\t}\r\n\r\n</style>"
  },
  {
    "path": "UniApp/pages/notice.vue",
    "content": "<template lang=\"pug\">\nview {{content}}\n</template>\n\n<script setup lang=\"ts\">\n// @ts-ignore\nimport {ref, onMounted} from 'vue'\nimport {useIndexStore} from '../store/index'\nimport {AppNoticeCacheKey} from \"../common/const\"\n\nconst store = useIndexStore()\nconst content = ref([]) // 内容\n\nonMounted(() => {\n  // @ts-ignore\n  if (uni.getStorageSync(AppNoticeCacheKey)) {\n    // @ts-ignore\n    content.value = uni.getStorageSync(AppNoticeCacheKey).message\n  }\n})\n</script>\n\n<style scoped>\n\n</style>\n"
  },
  {
    "path": "UniApp/pages/user/friend.vue",
    "content": "<template lang=\"pug\">\nSearch(ref=\"searchCps\" :columns=\"columns\" :options=\"options\")\n  template(v-slot=\"{result}\")\n    div.friend_list\n      div.item(v-for=\"item in result\")\n        div.left\n          div.user\n            image.head(:src=\"item.head_img\")\n            span.vip {{item.vip_name}}\n          div.info\n            span.nick_name {{item.nick_name}}\n            span.mobile {{item.mobile}}\n            span.register_time {{item.created_at ? item.created_at : ''}}\n        div.actions\n          span.copy_mobile(@click=\"copyMobile(item)\") 复制手机号\n</template>\n\n<script setup lang=\"ts\">\n// @ts-ignore\nimport {ref} from \"vue\"\nimport Search from '../../components/Search.vue'\nimport {getFriends} from \"../../common/api\"\n// @ts-ignore\nimport {onReachBottom} from \"@dcloudio/uni-app\"\n\nconst searchCps = ref()\n\nconst options = ref({\n  api: getFriends,\n});\n\nconst columns = ref([\n  {\n    index: \"register_time\",\n    title: \"注册时间\",\n    value: [],\n    formType: \"time\",\n  },\n  {\n    index:\"vip\",\n    title: \"vip等级\",\n    value: -1,\n    formType: \"select\",\n    data: [\n      {\n        value: -1,\n        text: \"全部\"\n      }, {\n        value: 0,\n        text: \"免费\"\n      }, {\n        value: 10,\n        text: \"VIP\"\n      }, {\n        value: 20,\n        text: \"一星\"\n      }, {\n        value: 30,\n        text: \"二星\"\n      },\n    ]\n  },\n]);\n\nconst copyMobile = (item: ptAny) => {\n  // @ts-ignore\n  uni.setClipboardData({\n    data: item.mobile,\n    success: () => {\n    }\n  });\n}\n\nonReachBottom(() => {\n  searchCps.value.loadMore()\n})\n\n</script>\n\n<style scoped lang=\"scss\">\n.friend_list{\n  display: flex;\n  flex-direction: column;\n  padding-top: 25rpx;\n  background-color: #f7f7f7;\n  .item{\n    display: flex;\n    flex-direction: row;\n    padding: 15rpx 30rpx;\n    margin: 5rpx 0;\n    background-color: $uni-text-color-inverse;\n    justify-content: space-between;\n    .left{\n      display: flex;\n      flex-direction: row;\n    }\n  }\n  .copy_mobile{\n    padding: 4rpx 5rpx;\n    border-radius: 5%;\n    background-color: $theme-color;\n    color: $uni-text-color-inverse;\n    font-size: 23rpx;\n    border: 1rpx solid $theme-color;\n  }\n  .user,.info,.actions{\n    display: flex;\n    flex-direction: column;\n    justify-content: center;\n    align-items: center;\n  }\n  .info{\n    margin: 0 15rpx;\n    display: flex;\n    flex-direction: column;\n    align-items:  flex-start;\n    font-size: 33rpx;\n    .nick_name{\n      width: 250rpx;\n      overflow-y: hidden;\n      white-space: nowrap;\n      text-overflow: ellipsis;\n    }\n    .register_time, .mobile{\n      font-size: 24rpx;\n    }\n  }\n  .user{\n    width: 100rpx;\n    position: relative;\n    .head{\n      width: 100rpx;\n      height: 100rpx;\n      box-shadow: 0 0 .12 rgba(249, 71, 71, .1);\n      border-radius: 50%;\n      background: #c1c1c1;\n    }\n\n    .vip {\n      position: absolute;\n      font-size: 15rpx;\n      background: $theme-color;\n      border-radius: 20%;\n      /* height: 40rpx; */\n      bottom: 0;\n      right: 0;\n      color: uni-text-color-inverse;\n      //width: 40rpx;\n      text-align: center;\n    }\n  }\n}\n</style>"
  },
  {
    "path": "UniApp/pages/user/mine.vue",
    "content": "<template lang=\"pug\">\ndiv.mine(v-show=\"personal.userUid > 0\")\n  div.mine_personal\n    image.head(:src=\"personal.head ? personal.head : '../../static/images/default_head_img.png'\")\n    div.name-box\n      p.name {{personal.name}}\n      p.rank(@click.stop=\"isShowOpenVip = true\")\n        span.grade {{personal.rank}}\n    div.action\n      span.setting(@click.stop=\"notice\") 偏好设置\n      span.edit(@click.stop=\"byNavigateTo('/pages/user/perfect')\") 编辑资料\n  div.mine_my\n    div.friend(@click.stop=\"byNavigateTo('/pages/user/friend')\")\n      span 我的好友\n      span.num {{personal.friendNum}}\n    div.friend(@click=\"byNavigateTo('/pages/user/wallet')\")\n      span 我的钱包\n      span.num ￥{{personal.moneyNum}}\n  div.mine_ad(v-if=\"banner.length > 0\")\n    uni-swiper-dot(mode=\"round\")\n      swiper.swiper-box \n        swiper-item(v-for=\"item in banner\")\n          image.img(:src=\"item.img_url\" @click=\"clickBanner(item)\")\n  div.menus\n    div.menu_group(v-for=\"item in menus\")\n      label.group_name {{item.name}}\n      div.menu_list\n        div.menu__box(v-for=\"menu in item.menus\" @click=\"clickMenu(menu)\")\n          image.menu_icon(:src=\"menu.icon ? menu.icon : '../../static/images/default_head_img.png'\")\n          span.menu_name {{menu.name}}\n  // 底部导航\n  FooterCommon(:footerIndex=\"2\")\n  // 客服弹窗\n  QrCodePopup(:isShow=\"isShowCode\" :codeData=\"publicCodeData\" @close=\"isShowCode=false\")\n  open-vip(:isShow=\"isShowOpenVip\" @close=\"isShowOpenVip=false\")\n</template>\n\n<script setup lang=\"ts\">\n// @ts-ignore\nimport {onMounted, reactive, ref} from 'vue'\nimport {useIndexStore} from '../../store'\nimport FooterCommon from '../../components/FooterCommon.vue'\nimport {userService} from \"../../logic/user\"\nimport {byNavigateTo} from \"../../common/utils/jump\"\nimport QrCodePopup from '../../components/QrCodePopup.vue'\nimport OpenVip from \"../../components/OpenVipPopup.vue\"\n\nconst {initMine} = userService()\nlet isShowOpenVip = ref(false)\nconst store = useIndexStore()\n\nconst personal = reactive({\n  head: '', // 头像\n  name: '', // 名字\n  userUid: '', // 用户uid\n  rank: '', // 等级级别\n  friendNum: '', // 声友数量\n  moneyNum: '', // 我的钱包数量\n  mobile: '', // mobile\n})\n\nconst menus = ref([])\n\n// 联系客服弹窗\nconst publicCodeData = reactive({\n  nickname: '',\n  portraitSrc: '', // 头像\n  codeSrc: '', // 二维码\n  text: '~', // 描述\n  hint: '', // 描述\n  promptText: '' // 提醒文字\n})\n\nconst isShowCode = ref(false) // 联系客服弹窗\n\nconst banner = ref([])\n\nconst clickBanner = (item: ptAny)=>{\n  if (item.url) {\n    /* #ifdef H5 */\n    window.location.href = item.url\n    /* #endif */\n\n    /* #ifdef APP-PLUS */\n    // @ts-ignore\n    plus.runtime.openURL(item.url)\n    /* #endif */\n  }\n}\n\nconst notice = () => {\n  // @ts-ignore\n  uni.showToast({\n    title: '马不停蹄,开发中!',\n    icon: 'none',\n    duration: 1500\n  })\n}\n\n/**\n * 快捷入口跳转到对应页面\n * @param item {Object} 对应数据\n */\nconst clickMenu = (item: ptAny) => {\n  if (item.click_type === 1) {\n    byNavigateTo(item.path)\n    return;\n  }\n  switch (item.click_func) {\n    case \"1001\":\n      let data = store.customerInfo\n      publicCodeData.nickname = \"我是【\" + data.user_name + \"】\"\n      publicCodeData.codeSrc = data.wx_img_url // 二维码\n      publicCodeData.text = '可通过微信联系我哦~' // 描述\n      publicCodeData.promptText = '长按识别二维码添加' // 提醒文字\n      publicCodeData.hint = '工作时间: ' + data.work_time\n      isShowCode.value = true\n      return;\n    case \"1002\":\n      isShowOpenVip.value = true\n      return;\n    case \"1003\":\n      // @ts-ignore\n      uni.clearStorageSync()\n      byNavigateTo('/pages/login')\n      return;\n  }\n\n}\n\nonMounted(async () => {\n  let cache: ptAny = await initMine()\n  if (cache) {\n    personal.moneyNum = cache?.amount\n    personal.friendNum = cache?.friend_num\n    personal.head = cache?.head_img\n    personal.name = cache?.nick_name\n    personal.userUid = cache?.uid\n    personal.rank = cache?.vip_name\n    personal.mobile = cache?.mobile\n    banner.value = cache?.banner\n    menus.value = cache?.menus\n  }\n})\n\n</script>\n\n<style scoped lang=\"scss\">\n  :deep(.open_vip .uni-table){\n    min-width: unset !important;\n  }\n\n  :deep(.open_vip .uni-table-th){\n    font-size: 23rpx;\n  }\n  .swiper-box{\n    height: 182rpx;\n  }\n\n  .mine {\n    width: 92vw;\n    background: linear-gradient(180deg, $uni-bg-color, #f6f6f6);\n    padding: 40rpx 30rpx 140rpx 30rpx;\n  }\n\n  .mine_personal {\n      width: 100%;\n      height: 106rpx;\n      margin-top: 20rpx;\n      display: flex;\n\n      .head {\n        width: 106rpx;\n        height: 106rpx;\n        box-shadow: 0 0 .12 rgba(249, 71, 71, .1);\n        border-radius: 50%;\n        background: #c1c1c1;\n      }\n\n      .name-box {\n        margin-left: 26rpx;\n        display: flex;\n        flex-direction: column;\n\n        .name {\n          width: 100%;\n          font-size: 38rpx;\n          font-weight: bold;\n          color: $uni-text-color;\n        }\n\n        .rank {\n          max-width: 180rpx;\n          padding: 0 10rpx;\n          border-radius: 8rpx;\n          font-size: 22rpx;\n          line-height: 44rpx;\n          color: $uni-text-color-inverse;\n          background-color: $theme-color;\n          width: fit-content;\n        }\n\n      }\n\n      .action {\n\n        font-size: 26rpx;\n        color: $uni-text-color-placeholder;\n        margin-left: auto;\n\n        padding-right: 30rpx;\n        display: flex;\n        flex-direction: column;\n      }\n      .setting, .edit{\n        height: 40rpx;\n        margin: 10rpx 0;\n        background-size: 13rpx 25rpx;\n      }\n  }\n\n  .mine_my{\n      height: 166rpx;\n      margin-top: 30rpx;\n      background: rgba(255, 255, 255, 0.39);\n      box-shadow: 0 0 20rpx rgba(0, 0, 0, .1);\n      border-radius: 16rpx;\n      display: flex;\n      align-items: center;\n\n      .friend {\n        flex: 1;\n        text-align: center;\n        font-size: 30rpx;\n        color: $uni-text-color;\n        display: flex;\n        flex-direction: column;\n      }\n\n      .num {\n        font-size: 38rpx;\n        font-weight: 600;\n        color: $uni-text-color;\n      }\n    }\n\n  .mine_ad {\n    height: 182rpx;\n    margin-top: 30rpx;\n    box-shadow: 0 0 20rpx rgba(254, 27, 27, .35);\n    border-radius: 16rpx;\n\n    .img {\n      width: 100%;\n      height: 100%;\n      border-radius: 16rpx;\n    }\n  }\n\n  .menus{\n    margin-top: 25rpx;\n    border-top: #f1f1f1 5rpx dashed;\n  }\n\n  .menu_group{\n    width: 100%;\n    display: flex;\n    flex-direction: column;\n    margin-top: 18rpx;\n    .group__name{\n      padding: 10rpx;\n      font-size: 34rpx;\n      font-weight: 700;\n      color: #090909;\n    }\n    .menu_list {\n      width: 100%;\n      display: flex;\n      flex-wrap: wrap;\n      padding: 30rpx 0;\n      border: solid #f1f1f1 1rpx;\n      border-radius: 26rpx;\n      box-shadow: 0 2px 6px 0 rgb(186 197 214 / 50%);\n    }\n    .menu__box{\n      display: flex;\n      flex-direction: column;\n      align-items: center;\n      width: 33.33%;\n      margin-top: 15rpx;\n      margin-bottom: 15rpx;\n    }\n    .menu_icon{\n      width: 80rpx;\n      height: 80rpx;\n    }\n    .menu_name{\n      margin-top: 7rpx;\n      font-size: 28rpx;\n      color: #545454;\n    }\n  }\n\n</style>\n"
  },
  {
    "path": "UniApp/pages/user/order.vue",
    "content": "<template lang=\"pug\">\nSearch(ref=\"searchCps\" :columns=\"columns\" :options=\"options\")\n  template(v-slot=\"{result}\")\n    div.order_list\n      div.item(v-for=\"item in result\")\n        div.left\n          span.content {{item.content}}\n          span.price 订单金额: {{item.amount_price_text}}  支付方式: {{item.pay_type_text}}\n          span.status 订单状态: {{item.status_text}}\n          span.ord_sn 订单号: {{item.ord_sn}}\n          span.created_at 订单时间: {{item.created_at ? item.created_at : ''}}\n        div.actions\n          span.action.copy(@click=\"copySn(item)\") 复制订单号\n</template>\n\n<script setup lang=\"ts\">\n// @ts-ignore\nimport {ref} from \"vue\"\nimport Search from '../../components/Search.vue'\nimport {getOrderList} from \"../../common/api\"\n// @ts-ignore\nimport {onReachBottom} from \"@dcloudio/uni-app\"\n\nconst searchCps = ref()\n\nconst options = ref({\n  api: getOrderList,\n});\n\nconst columns = ref([\n  {\n    index: \"create_time\",\n    title: \"创建时间\",\n    value: [],\n    formType: \"time\",\n  }\n]);\n\nonReachBottom(() => {\n  searchCps.value.loadMore()\n})\n\nconst copySn = (item: ptAny) => {\n  // @ts-ignore\n  uni.setClipboardData({\n    data: item.ord_sn,\n    success: () => {\n    }\n  });\n}\n\n</script>\n\n<style scoped lang=\"scss\">\n.order_list{\n  display: flex;\n  flex-direction: column;\n  padding-top: 25rpx;\n  background-color: #f7f7f7;\n  .item{\n    display: flex;\n    flex-direction: row;\n    padding: 15rpx 30rpx;\n    margin: 5rpx 0;\n    background-color: $uni-text-color-inverse;\n    justify-content: space-between;\n  }\n  .left{\n    display: flex;\n    flex-direction: column;\n    width: 80%;\n    .price{\n      color: #e18c8c;\n      font-size: 26rpx;\n    }\n    .ord_sn, .created_at, .status{\n      font-size: 26rpx;\n      color: #c8c7cc;\n    }\n  }\n\n  .actions{\n    display: flex;\n    flex-direction: column;\n    justify-content: center;\n    align-items: center;\n    .action{\n      padding: 4rpx 5rpx;\n      border-radius: 5%;\n      font-size: 23rpx;\n      margin: 5rpx 0;\n    }\n    .copy{\n      background-color: $theme-color;\n      border: 1rpx solid $theme-color;\n      color: $uni-text-color-inverse;\n    }\n  }\n}\n</style>"
  },
  {
    "path": "UniApp/pages/user/perfect.vue",
    "content": "<template lang=\"pug\">\ndiv.edit(v-show=\"!showCrop\" )\n  div.edit_top\n    div.head_box(@click=\"chooseImage(0)\")\n      image.head(:src=\"basicsInfo.head\")\n      div.camera\n    div.table\n      span.text 用户昵称\n      input.value(placeholder='请输入昵称' :value=\"basicsInfo.name\" @input=\"(res)=>{basicsInfo.name= res.detail.value}\")\n    div.table\n      span.text 手机号\n      input.value(placeholder='请输入手机号' type=\"number\" :value=\"basicsInfo.mobile\" @input=\"(res)=>{basicsInfo.mobile= res.detail.value}\")\n  button.edit_btn(@click=\"clickSave\") 保存\ndiv.cropper_container(v-show=\"showCrop\" )\n  BtCropper(ref=\"cropper\" :ratio=\"cropperRatio\" :imageSrc=\"cropperImgSrc\")\n  div.cropper_operate\n    button(@click=\"cropClose\") 取消\n    button(@click=\"cropSave\") 保存\n</template>\n<script setup lang=\"ts\">\n// @ts-ignore\nimport {ref, onMounted} from 'vue'\nimport {useIndexStore} from '../../store'\n\nimport {\n  getQiniuToken,\n  postUserDataInfo,\n} from '../../common/api'\nimport {uploadQiniu} from \"../../common/func\"\nimport {userService} from \"../../logic/user\"\nimport BtCropper from \"../../uni_modules/bt-cropper_3.0.1/components/bt-cropper/bt-cropper.vue\"\nimport {byNavigateTo} from \"../../common/utils/jump\"\n\nconst {initMine, updateMineUserCache} = userService()\n\nconst cropper = ref()\nconst cropperImgSrc = ref(\"\")\nconst cropperRatio = ref(1)\nconst tempIndex = ref(0)\nconst showCrop = ref(false)\n\nconst store = useIndexStore()\n\nconst basicsInfo = ref({\n  head: '', // 头像\n  mobile: '', // 手机号\n  name: '', // 昵称\n  nameLen: 0, // 昵称长度\n})\n\nonMounted(async () => {\n  let cache = await initMine()\n  basicsInfo.value.head = cache.head_img\n  basicsInfo.value.mobile = cache.mobile\n  basicsInfo.value.name = cache.nick_name\n})\n\nconst setImgData = (index: number, res: string) => {\n  switch (index) {\n    case 0:\n      basicsInfo.value.head = res\n      break;\n  }\n}\n\nconst cropClose = () => {\n  setImgData(tempIndex.value, cropperImgSrc.value)\n  showCrop.value = false\n}\n\nconst cropSave = () => {\n  cropper.value.crop().then((res: string) => {\n    if (\"tempFilePath\" !== res) {\n      setImgData(tempIndex.value, res)\n    }\n    showCrop.value = false\n  })\n}\n\nconst chooseImage = (index: number) => {\n  // @ts-ignore\n  uni.chooseImage({\n    count: 1,\n    sizeType: ['original', 'compressed'], //可以指定是原图还是压缩图，默认二者都有\n    sourceType: ['album'], //从相册选择\n    success: (res: ptAny) => {\n      cropperRatio.value = 1\n      tempIndex.value = index\n      cropperImgSrc.value = res.tempFilePaths[0]\n      showCrop.value = true\n    }\n  })\n}\n\n/**\n * 点击保存\n */\nconst clickSave = () => {\n  basicsInfo.value.nameLen = 0\n  let str = basicsInfo.value.name\n  let l = str.length\n  // 判断昵称长度\n  for (let i = 0; i < l; i++) {\n    if ((str.charCodeAt(i) & 0xff00) !== 0) {\n      basicsInfo.value.nameLen++\n    }\n    basicsInfo.value.nameLen++\n  }\n  if (!basicsInfo.value.name) {\n    // @ts-ignore\n    uni.showToast({\n      title: '昵称不能为空',\n      icon: 'none',\n      duration: 1000\n    })\n    return\n  }\n  if (basicsInfo.value.nameLen > 32) {\n    // @ts-ignore\n    uni.showToast({\n      title: '昵称最多只能16个字哦',\n      icon: 'none',\n      duration: 1000\n    })\n    return\n  }\n  if (!basicsInfo.value.head) {\n    // @ts-ignore\n    uni.showToast({\n      title: '头像不能为空',\n      icon: 'none',\n      duration: 1000\n    })\n    return\n  }\n\n  if (!basicsInfo.value.mobile) {\n    // @ts-ignore\n    uni.showToast({\n      title: '手机号不能为空',\n      icon: 'none',\n      duration: 1000\n    })\n    return\n  }\n  if (basicsInfo.value.mobile.length > 20 || basicsInfo.value.mobile.length < 5) {\n    // @ts-ignore\n    uni.showToast({\n      title: '手机号格式不正确',\n      icon: 'none',\n      duration: 1000\n    })\n    return\n  }\n  uploadAvatarImg().then(() => {\n    let data = {\n      head_img: basicsInfo.value.head,\n      mobile: basicsInfo.value.mobile,\n      nick_name: basicsInfo.value.name,\n    }\n    postUserDataInfo(data).then((res: ptAny) => {\n      if (res.code === 200) {\n        // @ts-ignore\n        uni.showToast({\n          title: '保存成功',\n          icon: 'none',\n          duration: 1000\n        })\n        updateMineUserCache(data)\n        byNavigateTo(\"/pages/user/mine\")\n      }\n    })\n  })\n}\n\n/**\n * 上传所有图片\n */\nconst uploadAvatarImg = async () => {\n  let scene = []\n  let reg = RegExp(store.scene.upload.domain)\n  if (!reg.test(basicsInfo.value.head)) {\n    scene.push(store.scene.upload.image.ai_head_img)\n  }\n\n  if (scene.length <= 0) {\n    return\n  }\n\n  let sceneArr: utilsType.qiniuInfo[] = []\n  await getQiniuToken(scene.join(\"-\")).then((res: ptAny) => {\n    sceneArr = res.data\n  })\n\n  if (sceneArr.length <= 0) {\n    return\n  }\n\n  for (let key in sceneArr) {\n    switch (sceneArr[key].scene) {\n      case store.scene.upload.image.ai_head_img:\n        await uploadQiniu(sceneArr[key], basicsInfo.value.head).then((res: ptAny) => {\n          basicsInfo.value.head = res.url\n        })\n        break;\n    }\n  }\n}\n\n</script>\n<style lang=\"scss\" scoped>\n.cropper_container{\n  height:100vh;\n  width: 100vw;\n  position: absolute;\n  top: 0;\n  left: 0;\n  background-color: #999999 !important;\n  z-index: 1;\n  .cropper_operate{\n    display: flex;\n    width: 100vw;\n    position: absolute;\n    bottom: 10rpx;\n  }\n}\n\n.edit {\n  width: 100vw;\n  min-height: 100vh;\n}\n\n.edit_top {\n  background-color: $uni-text-color-inverse;\n  padding: 80rpx 30rpx 0;\n\n  .head_box {\n    width: 100%;\n    height: 226rpx;\n    position: relative;\n    margin-bottom: 40rpx;\n\n    .head {\n      width: 226rpx;\n      height: 226rpx;\n      border-radius: 50%;\n      box-shadow: 0 0 120rpx rgba(249, 71, 71, .1);\n      margin-left: 50%;\n      transform: translate(-50%);\n    }\n\n    .camera {\n      position: absolute;\n      width: 60rpx;\n      height: 60rpx;\n      border-radius: 50%;\n      top: 166rpx;\n      left: 408rpx;\n      background: $theme-color url(\"../../static/images/white-camera.png\") no-repeat center;\n      background-size: 31rpx 26rpx;\n    }\n  }\n\n  .table {\n    height: 116rpx;\n    width: 100%;\n    border-bottom: 1rpx solid #ebe8e8;\n    line-height: 116rpx;\n    color: $uni-text-color;\n    display: flex;\n    justify-content: space-between;\n    align-items: center;\n\n    .text {\n      width: 140rpx;\n      font-size: 34rpx;\n      font-weight: bold;\n\n    }\n\n    .value {\n      width: 300rpx;\n      font-size: 30rpx;\n      font-weight: bold;\n      text-align: right;\n      margin-left: auto;\n      margin-right: 20rpx;\n    }\n\n  }\n}\n\n.edit_btn {\n  width: 498rpx;\n  height: 78rpx;\n  background: $theme-color;\n  box-shadow: 0 6rpx 12rpx rgba(254, 27, 27, .3);\n  border-radius: 48rpx;\n  font-size: 36rpx;\n  font-weight: bold;\n  color: $uni-text-color-inverse;\n  margin-top: 50rpx;\n  margin-left: 126rpx;\n}\n</style>"
  },
  {
    "path": "UniApp/pages/user/wallet.vue",
    "content": "<template lang=\"pug\">\ndiv.wallet\n  div.back_box\n    div.top\n    div.bottom\n  div.info_box\n    div.prices\n      div.can\n        span.desc 可提现\n        span.price ￥{{wallet.balance}}\n      div.more\n        div.item\n          span.desc(@click=\"byNavigateTo('/pages/user/walletList')\") 累计收益\n          span.price ￥{{wallet.balance_total}}\n        div.item\n          span.desc(@click=\"byNavigateTo('/pages/user/withdrawalList')\") 已提现\n          span.price ￥{{wallet.balance_total-wallet.balance}}\n    div.pay_box\n      input.price(placeholder-class=\"remind\" type=\"text\" placeholder=\"请输入提现金额\" v-model=\"price\" confirm-type=\"search\")\n      button.submit(@click=\"submit\") 提现\n      span.desc 提现相关问题请联系客服！\n</template>\n\n<script setup lang=\"ts\">\n// @ts-ignore\nimport {ref, onMounted} from 'vue'\nimport {getWalletInfo, postWithdrawal} from \"../../common/api\"\nimport {byNavigateTo} from \"../../common/utils/jump\"\n\nconst price = ref(0)\n\nconst wallet = ref({\n  balance: 0,\n  balance_total: 0,\n})\n\nonMounted(()=>{\n  getWalletInfo().then((res: ptAny)=>{\n    wallet.value = res.data\n    price.value = res.data.balance\n  })\n})\n\nconst submit = () => {\n  if (wallet.value.balance <= 0) {\n    // @ts-ignore\n    uni.showToast({\n      title: '余额不足',\n      icon: 'error',\n      duration: 1000\n    })\n    return\n  }\n\n  if (parseFloat(price.value) > wallet.value.balance) {\n    // @ts-ignore\n    uni.showToast({\n      title: '余额不足',\n      icon: 'error',\n      duration: 1000\n    })\n    return\n  }\n  postWithdrawal({amount: price.value}).then((res: ptAny) => {\n    if (res.code === 200) {\n      wallet.value.balance = wallet.value.balance - parseFloat(price.value)\n      // @ts-ignore\n      uni.showToast({\n        title: '申请成功',\n        icon: 'success',\n        duration: 1000\n      })\n    }\n  })\n}\n\n</script>\n\n<style scoped lang=\"scss\">\n.wallet{\n  width: 100%;\n  height: 100%;\n  position: fixed;\n}\n.back_box{\n  display: flex;\n  flex-direction: column;\n  height: 100%;\n  .top{\n    height: 350rpx;\n    background-color: rgb(78, 129, 176);\n  }\n  .bottom{\n    height: calc(100% - 350rpx);\n    background-color: #e6edee;\n  }\n}\n\n.info_box {\n  display: flex;\n  width: 600rpx;\n  flex-direction: column;\n  position: absolute;\n  background-color: #ffffff;\n  top: 450rpx;\n  left:50%;\n  transform: translate(-50%,-50%);\n  border-radius: 15rpx;\n  padding: 30rpx;\n  .prices{\n    display: flex;\n    flex-direction: column;\n    padding-bottom: 40rpx;\n    .can{\n      display: flex;\n      flex-direction: column;\n      justify-content: center;\n      align-items: center;\n      padding: 15rpx 0;\n      font-weight: bold;\n      .desc{\n        font-size: 36rpx;\n      }\n      .price{\n        color: #ec8d13;\n        font-size: 40rpx;\n        padding: 15rpx 0;\n      }\n    }\n    .more{\n      display: flex;\n      flex-direction: row;\n      justify-content: space-around;\n      padding: 15rpx 0;\n      .desc{\n        font-size: 32rpx;\n        color: #5e80a6;\n      }\n      .price{\n        color: #ec8d13;\n        font-size: 32rpx;\n        padding: 10rpx 0;\n      }\n      .item{\n        display: flex;\n        flex-direction: column;\n        align-items: flex-start;\n      }\n    }\n  }\n\n  .pay_box{\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n    .price{\n      color: $uni-text-color;\n      font-size: 30rpx;\n      font-weight: bold;\n      width: 60%;\n      border: #efecec solid 1rpx;\n      padding: 15rpx;\n      border-radius: 15rpx;\n    }\n    .submit{\n      width: 498rpx;\n      height: 78rpx;\n      background: $theme-color;\n      box-shadow: 0 6rpx 12rpx rgba(254, 27, 27, .3);\n      border-radius: 48rpx;\n      font-size: 36rpx;\n      font-weight: bold;\n      color: $uni-text-color-inverse;\n      margin-top: 50rpx;\n    }\n\n    .desc{\n      color: #999999;\n      font-size: 24rpx;\n      padding-top: 20rpx;\n    }\n  }\n}\n</style>"
  },
  {
    "path": "UniApp/pages/user/walletList.vue",
    "content": "<template lang=\"pug\">\nSearch(ref=\"searchCps\" :columns=\"columns\" :options=\"options\")\n  template(v-slot=\"{result}\")\n    div.order_list\n      div.item(v-for=\"item in result\")\n        span.content {{item.remark}}\n        span.price 金额: {{item.balance_text}}\n        span.status 场景: {{item.scene_text}}\n        span.created_at 发生时间: {{item.created_at ? item.created_at : ''}}\n</template>\n\n<script setup lang=\"ts\">\n// @ts-ignore\nimport {ref} from \"vue\"\nimport Search from '../../components/Search.vue'\nimport {getChangeLogList} from \"../../common/api\"\n// @ts-ignore\nimport {onReachBottom} from \"@dcloudio/uni-app\"\n\nconst searchCps = ref()\n\nconst options = ref({\n  api: getChangeLogList,\n});\n\nconst columns = ref([\n  {\n    index: \"create_time\",\n    title: \"创建时间\",\n    value: [],\n    formType: \"time\",\n  },{\n    index:\"direction\",\n    title: \"类型\",\n    value: 0,\n    formType: \"select\",\n    data: [\n      {\n        value: 0,\n        text: \"全部\"\n      }, {\n        value: 1,\n        text: \"收入\"\n      }, {\n        value: 2,\n        text: \"支出\"\n      }\n    ]\n  },\n]);\n\nonReachBottom(() => {\n  searchCps.value.loadMore()\n})\n\nconst copySn = (item: ptAny) => {\n  // @ts-ignore\n  uni.setClipboardData({\n    data: item.ord_sn,\n    success: () => {\n    }\n  });\n}\n\n</script>\n\n<style scoped lang=\"scss\">\n.order_list{\n  display: flex;\n  flex-direction: column;\n  padding-top: 25rpx;\n  background-color: #f7f7f7;\n  .item{\n    display: flex;\n    flex-direction: column;\n    padding: 15rpx 30rpx;\n    margin: 5rpx 0;\n    background-color: $uni-text-color-inverse;\n    justify-content: space-between;\n    .price{\n      color: #e18c8c;\n      font-size: 26rpx;\n    }\n    .ord_sn, .created_at, .status{\n      font-size: 26rpx;\n      color: #c8c7cc;\n    }\n  }\n}\n</style>"
  },
  {
    "path": "UniApp/pages/user/withdrawalList.vue",
    "content": "<template lang=\"pug\">\nSearch(ref=\"searchCps\" :columns=\"columns\" :options=\"options\")\n  template(v-slot=\"{result}\")\n    div.order_list\n      div.item(v-for=\"item in result\")\n        div.left\n          span.content {{item.content}}\n          span.price 订单金额: {{item.amount_price_text}}\n          span.status 订单状态: {{item.status_text}}\n          span.ord_sn 订单号: {{item.ord_sn}}\n          span.created_at 订单时间: {{item.created_at ? item.created_at : ''}}\n        div.actions\n          span.action.copy(@click=\"copySn(item)\") 复制订单号\n</template>\n\n<script setup lang=\"ts\">\n// @ts-ignore\nimport {ref} from \"vue\"\nimport Search from '../../components/Search.vue'\nimport {getWithdrawalList} from \"../../common/api\"\n// @ts-ignore\nimport {onReachBottom} from \"@dcloudio/uni-app\"\n\nconst searchCps = ref()\n\nconst options = ref({\n  api: getWithdrawalList,\n});\n\nconst columns = ref([\n  {\n    index: \"create_time\",\n    title: \"创建时间\",\n    value: [],\n    formType: \"time\",\n  }\n]);\n\nonReachBottom(() => {\n  searchCps.value.loadMore()\n})\n\nconst copySn = (item: ptAny) => {\n  // @ts-ignore\n  uni.setClipboardData({\n    data: item.ord_sn,\n    success: () => {\n    }\n  });\n}\n\n</script>\n\n<style scoped lang=\"scss\">\n.order_list{\n  display: flex;\n  flex-direction: column;\n  padding-top: 25rpx;\n  background-color: #f7f7f7;\n  .item{\n    display: flex;\n    flex-direction: row;\n    padding: 15rpx 30rpx;\n    margin: 5rpx 0;\n    background-color: $uni-text-color-inverse;\n    justify-content: space-between;\n  }\n  .left{\n    display: flex;\n    flex-direction: column;\n    width: 80%;\n    .price{\n      color: #e18c8c;\n      font-size: 26rpx;\n    }\n    .ord_sn, .created_at, .status{\n      font-size: 26rpx;\n      color: #c8c7cc;\n    }\n  }\n  .copy{\n    padding: 4rpx 5rpx;\n    border-radius: 5%;\n    background-color: $theme-color;\n    color: $uni-text-color-inverse;\n    font-size: 23rpx;\n    border: 1rpx solid $theme-color;\n  }\n  .actions{\n    display: flex;\n    flex-direction: column;\n    justify-content: center;\n    align-items: center;\n    .action{\n      padding: 4rpx 5rpx;\n      border-radius: 5%;\n      font-size: 23rpx;\n      margin: 5rpx 0;\n    }\n    .copy{\n      background-color: $theme-color;\n      border: 1rpx solid $theme-color;\n      color: $uni-text-color-inverse;\n    }\n  }\n}\n</style>"
  },
  {
    "path": "UniApp/pages.json",
    "content": "{\n\t\"pages\": [ //pages数组中第一项表示应用启动页，参考：https://uniapp.dcloud.io/collocation/pages\n\t\t{\n\t\t\t\"path\": \"pages/user/mine\",\n\t\t\t\"style\": {\n\t\t\t\t\"navigationBarTitleText\": \"个人中心\"\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t\"path\": \"pages/user/perfect\",\n\t\t\t\"style\": {\n\t\t\t\t\"navigationBarTitleText\": \"编辑资料\"\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t\"path\": \"pages/user/friend\",\n\t\t\t\"style\": {\n\t\t\t\t\"navigationBarTitleText\": \"我的好友\"\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t\"path\": \"pages/user/order\",\n\t\t\t\"style\": {\n\t\t\t\t\"navigationBarTitleText\": \"我的订单\"\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t\"path\": \"pages/user/wallet\",\n\t\t\t\"style\": {\n\t\t\t\t\"navigationBarTitleText\": \"钱包\"\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t\"path\": \"pages/user/walletList\",\n\t\t\t\"style\": {\n\t\t\t\t\"navigationBarTitleText\": \"收支明细\"\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t\"path\": \"pages/user/withdrawalList\",\n\t\t\t\"style\": {\n\t\t\t\t\"navigationBarTitleText\": \"提现列表\"\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t\"path\": \"pages/chatgpt/room\",\n\t\t\t\"style\": {\n\t\t\t\t\"navigationBarTitleText\": \"Ai问答\",\n\t\t\t\t\"enablePullDownRefresh\":true\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t\"path\": \"pages/chatgpt/channel\",\n\t\t\t\"style\": {\n\t\t\t\t\"navigationBarTitleText\": \"公开频道\"\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t\"path\": \"pages/login\",\n\t\t\t\"style\": {\n\t\t\t\t\"navigationBarTitleText\": \"login\"\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\t\"path\": \"pages/notice\",\n\t\t\t\"style\": {\n\t\t\t\t\"navigationBarTitleText\": \"notice\"\n\t\t\t}\n\t\t}\n\t],\n\t\"globalStyle\": {\n\t\t\"navigationBarTextStyle\": \"black\",\n\t\t\"navigationBarTitleText\": \"AI大脑\",\n\t\t\"navigationBarBackgroundColor\": \"#F8F8F8\",\n\t\t// #ifdef H5\n\t\t\"navigationStyle\": \"custom\",\n\t\t// #endif\n\t\t// #ifndef H5\n\t\t\"navigationStyle\": \"default\",\n\t\t// #endif\n\t\t\"backgroundColor\": \"#F8F8F8\"\n\t},\n\t\"uniIdRouter\": {}\n}\n"
  },
  {
    "path": "UniApp/store/index.ts",
    "content": "// @ts-ignore\nimport {defineStore} from 'pinia'\n// @ts-ignore\nimport {ref} from \"vue\"\nimport {\n    getInit\n} from \"../common/api\"\nimport {AppInitCacheKey, xTokenCacheKey} from \"../common/const\"\n\nexport const useIndexStore = defineStore('index', () => {\n    const scene = ref({ // 场景值\n        ads: { // 广告\n        },\n        agreement: {  // 协议\n            user: ''\n        },\n        upload: {  // 上传\n            image : {\n                byv_default: \"1000\",\n                byv_head_img: \"1002\",\n                byv_home_back: \"1005\",\n                byv_real_name: \"1001\",\n                byv_wx_er: \"1003\",\n                byv_wx_video_er: \"1004\",\n            },\n\t\t\tdomain: '',\n        },\n        tutorials: { // 教程\n        },\n    })\n\n    const appInfo = ref({\n        copyright: '', // 版权信息\n        version: '',\n    })\n\n    const customerInfo = ref({ // 客服信息\n        customer_id: 0, // id\n        head_img: '', // 头像\n        mobile: '', // 头像\n        user_name: '', // 头像\n        work_time: '', // 头像\n        wx_img_url: '', // 昵称\n        wx_no: '', // 昵称\n    })\n\n    const setInit = async ()=>{\n        // @ts-ignore\n        let initInfo = uni.getStorageSync(AppInitCacheKey)\n        if (initInfo) {\n            scene.value = initInfo.scene\n            customerInfo.value = initInfo.other.customer_info\n            appInfo.value.copyright = initInfo.other.copyright\n            appInfo.value.version = initInfo.other.version\n            return Promise.resolve()\n        } else {\n            return getInit().then((res: ptAny) => {\n                if (res.code === 200) {\n                    // @ts-ignore\n                    uni.setStorageSync(AppInitCacheKey, res.data)\n                    scene.value = res.data.scene\n                    customerInfo.value = res.data.other.customer_info\n                    appInfo.value.copyright = res.data.other.copyright\n                    appInfo.value.version = res.data.other.version\n                }\n            })\n        }\n    }\n\n    const authorization = async (isForce?: boolean) => {\n        // @ts-ignore\n        let xTokenCache = uni.getStorageSync(xTokenCacheKey)\n        if (xTokenCache && !isForce) {\n            await setInit().then(async (r: ptAny) => {\n            })\n            return\n        }\n        // @ts-ignore\n        uni.reLaunch({url: \"/pages/login\"})\n    }\n\n    return {\n        scene, appInfo,customerInfo,setInit,\n        authorization\n    }\n});"
  },
  {
    "path": "UniApp/store/websocket.ts",
    "content": "// @ts-ignore\nimport {defineStore} from 'pinia'\n// @ts-ignore\nimport {ref} from \"vue\"\nimport {getConfig} from '../common/config'\nimport { xTokenCacheKey } from '../common/const'\n\nexport const useWebsocketStore = defineStore('websocket', () => {\n    // @ts-ignore\n    const websocketTask = ref<UniNamespace.SocketTask>()\n    const heartbeatTime = ref(0)\n    const isOpenSocket = ref(false)\n    const isReconnect = ref(true)\n    const onMessageHandles = ref({\n        ping: (res: ptAny) => {\n            console.log(\"websocket-ping\", res)\n        },\n        opened: (res: ptAny) => {\n            console.log(\"websocket-opened\", res)\n        },\n        close: (res: ptAny) => {\n            console.log(\"websocket-close\", res)\n        },\n        error: (res: ptAny) => {\n            console.log(\"websocket-error\", res)\n        },\n        login: (res: ptAny) => {\n            // @ts-ignore\n            uni.reLaunch({url: \"/pages/login\"})\n        },\n    })\n\n    const websocketInit = () => {\n        if (isOpenSocket.value) {\n            return\n        }\n        // @ts-ignore\n\t\tlet authorization = uni.getStorageSync(xTokenCacheKey)\n        // @ts-ignore\n\t\twebsocketTask.value = uni.connectSocket({\n\t\t\turl: getConfig().wsUrl + \"?token=\" + (authorization ? authorization.token : ''),\n\t\t\tcomplete: () => {\n\t\t\t}\n\t\t})\n\n        websocketTask.value.onOpen((res: ptAny) => {\n            heartbeatTime.value && clearInterval(heartbeatTime.value)\n            isOpenSocket.value = true\n            heartbeatTime.value = setInterval(() => {\n                websocketTask.value.send({data: \"{\\\"type\\\":\\\"ping\\\"}\"});\n            }, 55000)\n        })\n\t\t\n\t\twebsocketTask.value.onMessage((res: ptAny) => {\n\t\t  let data = JSON.parse(res.data)\n\t\t  if(onMessageHandles.value.hasOwnProperty(data.type)){\n              onMessageHandles.value[data.type](data)\n\t\t  }else{\n              console.log(\"找不到onMessageHandles\")\n          }\n\t\t})\n\n        websocketTask.value.onClose(() => {\n            isOpenSocket.value = false\n            isReconnect.value && reconnect();\n        })\n    }\n\n    const reconnect = () => {\n        websocketTask.value.close({})\n        if (!isOpenSocket.value) {\n            setTimeout(() => {\n                websocketInit();\n            }, 2000)\n        }\n    }\n\n    const send = (data: ptAny) => {\n        websocketTask.value.send({data:JSON.stringify(data)})\n    }\n\n    const close = () => {\n        websocketTask.value.close({})\n        isOpenSocket.value = false\n        websocketTask.value = null\n    }\n\n    const bindMessageHandle = (handle: ptAny)=>{\n\t\tonMessageHandles.value[handle.type] = handle.event\n\t}\n\t\n    return {\n        websocketInit, send, close, bindMessageHandle\n    }\n})"
  },
  {
    "path": "UniApp/tsconfig.json",
    "content": "// tsconfig.json\n{\n  \"compilerOptions\": {\n    \"target\": \"esnext\",\n    \"module\": \"esnext\",\n    \"strict\": true,\n    \"jsx\": \"preserve\",\n    \"moduleResolution\": \"node\",\n    \"esModuleInterop\": true,\n    \"sourceMap\": true,\n    \"skipLibCheck\": true,\n    \"importHelpers\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"useDefineForClassFields\": true,\n    \"resolveJsonModule\": true,\n    \"lib\": [\n      \"esnext\",\n      \"dom\"\n    ],\n    \"types\": [\n      \"@dcloudio/types\",\n      \"./types\"\n    ]\n  },\n  \"exclude\": [\n    \"node_modules\",\n    \"unpackage\",\n    \"src/**/*.nvue\"\n  ]\n}"
  },
  {
    "path": "UniApp/types/global.d.ts",
    "content": "type ptAny = any\n\ndeclare namespace utilsType {\n    interface result{\n        code: number;\n        data: any;\n        message: string;\n    }\n\n    interface requestConfig {\n        url: string\n        header?: Record<string, string>\n        data?: any\n        method: 'GET' | 'POST' | 'PUT' | 'DELETE'\n    }\n\n    interface qiniuInfo {\n        domain: string\n        service_url: string\n        scene: string | number\n        token: string\n        path: string\n    }\n}"
  },
  {
    "path": "UniApp/uni.promisify.adaptor.js",
    "content": "uni.addInterceptor({\n  returnValue (res) {\n    if (!(!!res && (typeof res === \"object\" || typeof res === \"function\") && typeof res.then === \"function\")) {\n      return res;\n    }\n    return new Promise((resolve, reject) => {\n      res.then((res) => res[0] ? reject(res[0]) : resolve(res[1]));\n    });\n  },\n});"
  },
  {
    "path": "UniApp/uni.scss",
    "content": "/**\r\n * 这里是uni-app内置的常用样式变量\r\n *\r\n * uni-app 官方扩展插件及插件市场（https://ext.dcloud.net.cn）上很多三方插件均使用了这些样式变量\r\n * 如果你是插件开发者，建议你使用scss预处理，并在插件代码中直接使用这些变量（无需 import 这个文件），方便用户通过搭积木的方式开发整体风格一致的App\r\n *\r\n */\r\n\r\n/**\r\n * 如果你是App开发者（插件使用者），你可以通过修改这些变量来定制自己的插件主题，实现自定义主题功能\r\n *\r\n * 如果你的项目同样使用了scss预处理，你也可以直接在你的 scss 代码中使用如下变量，同时无需 import 这个文件\r\n */\r\n\r\n/* 颜色变量 */\r\n\r\n/* 行为相关颜色 */\r\n$uni-color-primary: #007aff;\r\n$uni-color-success: #4cd964;\r\n$uni-color-warning: #f0ad4e;\r\n$uni-color-error: #dd524d;\r\n\r\n/* 文字基本颜色 */\r\n$uni-text-color:#333;//基本色\r\n$uni-text-color-inverse:#fff;//反色\r\n$uni-text-color-grey:#999;//辅助灰色，如加载更多的提示信息\r\n$uni-text-color-placeholder: #808080;\r\n$uni-text-color-disable:#c0c0c0;\r\n\r\n/* 背景颜色 */\r\n$uni-bg-color:#ffffff;\r\n$uni-bg-color-grey:#f8f8f8;\r\n$uni-bg-color-hover:#f1f1f1;//点击状态颜色\r\n$uni-bg-color-mask:rgba(0, 0, 0, 0.4);//遮罩颜色\r\n\r\n/* 边框颜色 */\r\n$uni-border-color:#c8c7cc;\r\n\r\n/* 尺寸变量 */\r\n\r\n/* 文字尺寸 */\r\n$uni-font-size-sm:12px;\r\n$uni-font-size-base:14px;\r\n$uni-font-size-lg:16;\r\n\r\n/* 图片尺寸 */\r\n$uni-img-size-sm:20px;\r\n$uni-img-size-base:26px;\r\n$uni-img-size-lg:40px;\r\n\r\n/* Border Radius */\r\n$uni-border-radius-sm: 2px;\r\n$uni-border-radius-base: 3px;\r\n$uni-border-radius-lg: 6px;\r\n$uni-border-radius-circle: 50%;\r\n\r\n/* 水平间距 */\r\n$uni-spacing-row-sm: 5px;\r\n$uni-spacing-row-base: 10px;\r\n$uni-spacing-row-lg: 15px;\r\n\r\n/* 垂直间距 */\r\n$uni-spacing-col-sm: 4px;\r\n$uni-spacing-col-base: 8px;\r\n$uni-spacing-col-lg: 12px;\r\n\r\n/* 透明度 */\r\n$uni-opacity-disabled: 0.3; // 组件禁用态的透明度\r\n\r\n/* 文章场景相关 */\r\n$uni-color-title: #2C405A; // 文章标题颜色\r\n$uni-font-size-title:20px;\r\n$uni-color-subtitle: #555555; // 二级标题颜色\r\n$uni-font-size-subtitle:26px;\r\n$uni-color-paragraph: #3F536E; // 文章段落颜色\r\n$uni-font-size-paragraph:15px;\r\n\r\n$theme-color: rgb(85 170 239);"
  },
  {
    "path": "UniApp/uni_modules/bt-cropper_3.0.1/changelog.md",
    "content": "\r\n## 3.0.1（2022-11-03）\n修复 撤销和重做不生效的问题\n## 3.0.0（2022-11-03）\r\n使用wxs重构代码，性能大提升\r\n新增 支持蒙版裁剪，可以裁剪任何形状的图形（详情见demo示例）\r\n新增 支持在弹窗中使用（详情见demo示例）\r\n移除 由于插槽会导致许多问题，实际上开发者自己封装组件反而更简单，所以3.0版本以后移除插槽，2.0迁移教程见 demo:全屏裁剪\r\n## 2.0.3（2022-08-21）\r\n修复 在vue3 程序中报错的问题\r\n新增 新增了图片初始化完成和加载失败的事件\r\n## 2.0.2（2022-08-18）\r\n新增 增加了原像素裁剪功能，即使用用户在裁剪框取景的大小作为输出像素，换句话说，输出的图片分辨率与输入图片分辨率一样\r\n新增 增加了change事件，会在图像和裁剪框位置变化后触发\r\n新增 增加了compress参数 压缩图片，压缩图片是为了提升流畅度，所以只会针对用户拖动的那张图片进行压缩，最终输出的图像品质并不会受到影响\r\n修复 用户在没有传入图像时报错的问题\r\n修复 ios在某些机型上拖动出现残留的问题\r\n## 2.0.1（2022-07-20）\r\n修复：ios打包成app的时候有几率裁剪不成功的问题\r\n## 2.0.0（2022-07-13）\r\n更新了2.0版本，增加了图片放大功能\r\n## bt-cropper 图片裁切\r\n========\r\n### 2022年7月13日 发布2.0版本\r\n* 1.完全重构了代码，并且优化了性能\r\n* 2.支持app\r\n* 3.修复demo在H5的情况下高度错误的问题\r\n"
  },
  {
    "path": "UniApp/uni_modules/bt-cropper_3.0.1/components/bt-cropper/bt-cropper.vue",
    "content": "<template>\r\n\t<view class=\"bt-container\" :style=\"[containerStyle]\">\r\n\t\t<!-- #ifdef APP-VUE || MP-WEIXIN || MP-QQ || H5 -->\r\n\t\t<view class=\"mainContent\" data-type=\"image\" @touchstart=\"wxsModule.touchStart\" @touchmove=\"wxsModule.touchMove\" @touchend=\"wxsModule.touchEnd\">\r\n\t\t<!-- #endif -->\r\n\t\t<!-- #ifndef APP-VUE || MP-WEIXIN || MP-QQ || H5 -->\r\n\t\t<view class=\"mainContent\" data-type=\"image\" @touchstart=\"touchStart\" @touchmove=\"touchMove\" @touchend=\"touchEnd\">\r\n\t\t<!-- #endif -->\r\n\t\t\t<template v-if=\"imageRect && cropperRect\">\r\n\t\t\t\t<!-- #ifdef APP-VUE || MP-WEIXIN || MP-QQ || H5 -->\r\n\t\t\t\t<image mode=\"aspectFit\" :src=\"imageSrc\" class=\"image\" :change:imageRect=\"wxsModule.changeImageRect\" :imageRect=\"imageRect\" :class=\"{ anim }\">\r\n\t\t\t\t<!-- #endif -->\r\n\t\t\t\t<!-- #ifndef APP-VUE || MP-WEIXIN || MP-QQ || H5 -->\r\n\t\t\t\t<image mode=\"aspectFit\" :src=\"imageSrc\" class=\"image\" :style=\"[imageStyle]\" :class=\"{ anim }\">\r\n\t\t\t\t<!-- #endif -->\r\n\t\t\t\t</image>\r\n\t\t\t\t<!-- #ifdef APP-VUE || MP-WEIXIN || MP-QQ || H5 -->\r\n\t\t\t\t<view class=\"cropper\" :class=\"{ anim }\" :change:cropperRect=\"wxsModule.changeCropper\" :cropperRect=\"cropperRect\" :change:ratio=\"wxsModule.changeRatio\" :ratio=\"ratio\" >\r\n\t\t\t\t<!-- #endif -->\r\n\t\t\t\t<!-- #ifndef APP-VUE || MP-WEIXIN || MP-QQ || H5 -->\r\n\t\t\t\t<view class=\"cropper\" :class=\"{ anim }\"  :style=\"[cropperStyle]\">\r\n\t\t\t\t<!-- #endif -->\r\n\t\t\t\t\t<image class=\"mask\" :src=\"mask\"></image>\r\n\t\t\t\t\t<template v-if=\"showGrid\">\r\n\t\t\t\t\t\t<view class=\"line row row1\"></view>\r\n\t\t\t\t\t\t<view class=\"line row row2\"></view>\r\n\t\t\t\t\t\t<view class=\"line col col1\"></view>\r\n\t\t\t\t\t\t<view class=\"line col col2\"></view>\r\n\t\t\t\t\t</template>\r\n\t\t\t\t\t<!-- #ifdef APP-VUE || MP-WEIXIN || MP-QQ || H5  -->\r\n\t\t\t\t\t<view class=\"controller vertical left\" @touchstart=\"wxsModule.touchStart\" data-type=\"controller\" />\r\n\t\t\t\t\t<view class=\"controller vertical right\" @touchstart=\"wxsModule.touchStart\" data-type=\"controller\" />\r\n\t\t\t\t\t<view class=\"controller horizon top\" @touchstart=\"wxsModule.touchStart\" data-type=\"controller\" />\r\n\t\t\t\t\t<view class=\"controller horizon bottom\" @touchstart=\"wxsModule.touchStart\" data-type=\"controller\" />\r\n\t\t\t\t\t<view class=\"controller left top\" @touchstart=\"wxsModule.touchStart\" data-type=\"controller\" />\r\n\t\t\t\t\t<view class=\"controller left bottom\" @touchstart=\"wxsModule.touchStart\" data-type=\"controller\" />\r\n\t\t\t\t\t<view class=\"controller right top\" @touchstart=\"wxsModule.touchStart\" data-type=\"controller\" />\r\n\t\t\t\t\t<view class=\"controller right bottom\" @touchstart=\"wxsModule.touchStart\" data-type=\"controller\" />\r\n\t\t\t\t\t<!-- #endif -->\r\n\t\t\t\t\t<!-- #ifndef APP-VUE || MP-WEIXIN || MP-QQ || H5  -->\r\n\t\t\t\t\t<view class=\"controller vertical left\" @touchstart.stop=\"touchStart(-1, 0, $event)\" />\r\n\t\t\t\t\t<view class=\"controller vertical right\" @touchstart.stop=\"touchStart(1, 0, $event)\" />\r\n\t\t\t\t\t<view class=\"controller horizon top\" @touchstart.stop=\"touchStart(0, -1, $event)\" />\r\n\t\t\t\t\t<view class=\"controller horizon bottom\" @touchstart.stop=\"touchStart(0, 1, $event)\" />\r\n\t\t\t\t\t<view class=\"controller left top\" @touchstart.stop=\"touchStart(-1, -1, $event)\" />\r\n\t\t\t\t\t<view class=\"controller left bottom\" @touchstart.stop=\"touchStart(-1, 1, $event)\" />\r\n\t\t\t\t\t<view class=\"controller right top\" @touchstart.stop=\"touchStart(1, -1, $event)\" />\r\n\t\t\t\t\t<view class=\"controller right bottom\" @touchstart.stop=\"touchStart(1, 1, $event)\" />\r\n\t\t\t\t\t<!-- #endif -->\r\n\t\t\t\t</view>\r\n\t\t\t</template>\r\n\t\t</view>\r\n\t\t\t<canvas v-if=\"type2d\" type=\"2d\" class=\"bt-canvas\" :width=\"target.width\" :height=\"target.height\"></canvas>\r\n\t\t\t<canvas\r\n\t\t\t\tv-else\r\n\t\t\t\t:canvas-id=\"canvasId\"\r\n\t\t\t\tclass=\"bt-canvas\"\r\n\t\t\t\t:style=\"{\r\n\t\t\t\t\twidth: target.width + 'px',\r\n\t\t\t\t\theight: target.height + 'px'\r\n\t\t\t\t}\"\r\n\t\t\t\t:width=\"target.width * pixel\"\r\n\t\t\t\t:height=\"target.height * pixel\"\r\n\t\t\t></canvas>\r\n\t</view>\r\n</template>\r\n\r\n<script>\r\n// #ifndef APP-VUE || MP-WEIXIN || MP-QQ || H5\r\nimport touches from './js/touchs.js';\r\n// #endif\r\nimport { parseUnit,sleep } from './utils/tools.js';\r\n/**\r\n * better-cropper 图片裁切插件\r\n */\r\nexport default {\r\n\tname: 'bt-cropper',\r\n\tprops: {\r\n\t\t// 图片路径，支持网络路径和本地路径\r\n\t\timageSrc: {\r\n\t\t\ttype: String,\r\n\t\t\tdefault: '',\r\n\t\t\trequired: true\r\n\t\t},\r\n\t\tmask: {\r\n\t\t\ttype: String,\r\n\t\t\tdefault: ''\r\n\t\t},\r\n\t\t// 手动指定容器的大小\r\n\t\tcontainerSize: {\r\n\t\t\ttype: Object,\r\n\t\t\tdefault: null\r\n\t\t},\r\n\t\t// 输出图片的格式，默认jpg\r\n\t\tfileType: {\r\n\t\t\ttype: String,\r\n\t\t\tdefault: 'png'\r\n\t\t},\r\n\t\t// 生成的图片的宽度,不传或者传0表示按照原始分辨率裁切\r\n\t\tdWidth: Number,\r\n\t\tmaxWidth: {\r\n\t\t\ttype: Number,\r\n\t\t\tdefault: 2000\r\n\t\t},\r\n\t\t// 裁切比例，0表示自由\r\n\t\tratio: {\r\n\t\t\ttype: Number,\r\n\t\t\tdefault: 0,\r\n\t\t\tvalidator(value) {\r\n\t\t\t\tif (typeof value === 'number') {\r\n\t\t\t\t\tif (value < 0) {\r\n\t\t\t\t\t\treturn false;\r\n\t\t\t\t\t}\r\n\t\t\t\t} else {\r\n\t\t\t\t\treturn false;\r\n\t\t\t\t}\r\n\t\t\t\treturn true;\r\n\t\t\t}\r\n\t\t},\r\n\t\t// 是否展示网格\r\n\t\tshowGrid: {\r\n\t\t\ttype: Boolean,\r\n\t\t\tdefault: false\r\n\t\t},\r\n\t\t// 图片质量，0-1 越大质量越好\r\n\t\tquality: {\r\n\t\t\ttype: Number,\r\n\t\t\tdefault: 1\r\n\t\t},\r\n\t\tcanvas2d: {\r\n\t\t\ttype: Boolean,\r\n\t\t\tdefault: false\r\n\t\t},\r\n\t\t// 初始的图片位置\r\n\t\tinitPosition: {\r\n\t\t\ttype: Object,\r\n\t\t\tdefault() {\r\n\t\t\t\treturn null;\r\n\t\t\t}\r\n\t\t},\r\n\t\t// 是否开启操作结束后自动放大\r\n\t\tautoZoom: {\r\n\t\t\ttype: Boolean,\r\n\t\t\tdefault: true\r\n\t\t}\r\n\t},\r\n\t// #ifndef APP-VUE || MP-WEIXIN || MP-QQ || H5\r\n\tmixins: [touches],\r\n\t// #endif\r\n\tdata() {\r\n\t\treturn {\r\n\t\t\tcanvasId: 'bt-cropper',\r\n\t\t\tcontainerRect: null,\r\n\t\t\timageInfo: null,\r\n\t\t\toperationHistory: [],\r\n\t\t\toperationIndex: 0,\r\n\t\t\tanim: false,\r\n\t\t\ttimer: null,\r\n\t\t\t// 是否使用2D canvas\r\n\t\t\ttype2d: false,\r\n\t\t\tpixel: 1,\r\n\t\t\timageRect: null,\r\n\t\t\tcropperRect: null,\r\n\t\t\ttarget:{\r\n\t\t\t\twidth:0,\r\n\t\t\t\theight:0\r\n\t\t\t}\r\n\t\t};\r\n\t},\r\n\twatch: {\r\n\t\timageSrc: {\r\n\t\t\thandler(src) {\r\n\t\t\t\tif (typeof src === 'string' && src !== '') {\r\n\t\t\t\t\tthis.imageInit(src);\r\n\t\t\t\t} else {\r\n\t\t\t\t\tthis.imageInfo = null;\r\n\t\t\t\t}\r\n\t\t\t},\r\n\t\t\timmediate: true\r\n\t\t},\r\n\t\tratio() {\r\n\t\t\tif (this.ratio != 0) {\r\n\t\t\t\tthis.startAnim();\r\n\t\t\t\tthis.init();\r\n\t\t\t}\r\n\t\t}\r\n\t},\r\n\tcomputed: {\r\n\t\tcontainerStyle() {\r\n\t\t\tif (this.containerSize && this.containerRect) {\r\n\t\t\t\treturn {\r\n\t\t\t\t\twidth: this.containerRect.width + 'px',\r\n\t\t\t\t\theight: this.containerRect.height + 'px'\r\n\t\t\t\t};\r\n\t\t\t}\r\n\t\t\treturn {};\r\n\t\t}\r\n\t},\r\n\tmethods: {\r\n\t\tstartAnim() {\r\n\t\t\tthis.stopAnim();\r\n\t\t\tthis.anim = true;\r\n\t\t\tthis.timer = setTimeout(() => {\r\n\t\t\t\tthis.anim = false;\r\n\t\t\t}, 200);\r\n\t\t},\r\n\t\tstopAnim() {\r\n\t\t\tthis.anim = false;\r\n\t\t\tclearTimeout(this.timer);\r\n\t\t},\r\n\t\timageInit(src) {\r\n\t\t\tuni.showLoading({\r\n\t\t\t\ttitle: '载入中...'\r\n\t\t\t});\r\n\t\t\tuni.getImageInfo({\r\n\t\t\t\tsrc,\r\n\t\t\t\tsuccess: res => {\r\n\t\t\t\t\tthis.imageInfo = res;\r\n\t\t\t\t\tthis.$nextTick(() => {\r\n\t\t\t\t\t\tthis.getContainer().then(rect => {\r\n\t\t\t\t\t\t\tthis.containerRect = rect;\r\n\t\t\t\t\t\t\tthis.init();\r\n\t\t\t\t\t\t});\r\n\t\t\t\t\t});\r\n\t\t\t\t},\r\n\t\t\t\tfail: (err) => {\r\n\t\t\t\t\tthis.$emit('loadFail',err);\r\n\t\t\t\t\tuni.showToast({\r\n\t\t\t\t\t\ttitle: '图片下载失败!',\r\n\t\t\t\t\t\ticon: 'none'\r\n\t\t\t\t\t});\r\n\t\t\t\t},\r\n\t\t\t\tcomplete(res) {\r\n\t\t\t\t\tuni.hideLoading();\r\n\t\t\t\t}\r\n\t\t\t});\r\n\t\t},\r\n\t\tinitCropper() {\r\n\t\t\tconst imageRate = this.imageInfo.width / this.imageInfo.height;\r\n\t\t\tconst containerRate = this.containerRect.width / this.containerRect.height;\r\n\t\t\tconst imageRect = {};\r\n\t\t\tlet cropperRate = this.ratio;\r\n\t\t\tif (cropperRate == 0) {\r\n\t\t\t\tif (this.cropperRect) {\r\n\t\t\t\t\tcropperRate = this.cropperRect.width / this.cropperRect.height;\r\n\t\t\t\t} else {\r\n\t\t\t\t\tcropperRate = 1;\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t\tconst cropperRect = {};\r\n\t\t\tif (containerRate > cropperRate) {\r\n\t\t\t\tcropperRect.height = this.containerRect.height * 0.85;\r\n\t\t\t\tcropperRect.width = cropperRect.height * cropperRate;\r\n\t\t\t} else {\r\n\t\t\t\tcropperRect.width = this.containerRect.width * 0.85;\r\n\t\t\t\tcropperRect.height = cropperRect.width / cropperRate;\r\n\t\t\t}\r\n\t\t\tif (cropperRate > imageRate) {\r\n\t\t\t\timageRect.width = cropperRect.width;\r\n\t\t\t\timageRect.height = imageRect.width / imageRate;\r\n\t\t\t} else {\r\n\t\t\t\timageRect.height = cropperRect.height;\r\n\t\t\t\timageRect.width = imageRect.height * imageRate;\r\n\t\t\t}\r\n\t\t\timageRect.left = (this.containerRect.width - imageRect.width) / 2;\r\n\t\t\timageRect.top = (this.containerRect.height - imageRect.height) / 2;\r\n\t\t\tcropperRect.left = imageRect.left + (imageRect.width - cropperRect.width) / 2;\r\n\t\t\tcropperRect.top = imageRect.top + (imageRect.height - cropperRect.height) / 2;\r\n\t\t\treturn {\r\n\t\t\t\timageRect,\r\n\t\t\t\tcropperRect\r\n\t\t\t};\r\n\t\t},\r\n\t\tinit() {\r\n\t\t\tconst {imageRect,cropperRect} = this.initCropper()\r\n\t\t\tif(this.initPosition){\r\n\t\t\t\tconst scale = this.imageInfo.width/imageRect.width;\r\n\t\t\t\tconst {left,top,width,height} = this.initPosition;\r\n\t\t\t\tif(left!==undefined&&top!==undefined&&width!==undefined&&height!==undefined){\r\n\t\t\t\t\tcropperRect.width = width/scale;\r\n\t\t\t\t\tcropperRect.height = height/scale;\r\n\t\t\t\t\tcropperRect.left = left/scale;\r\n\t\t\t\t\tcropperRect.top = top/scale;\r\n\t\t\t\t\tthis.$nextTick(this.zoomToFill);\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t\tthis.imageRect = imageRect\r\n\t\t\tthis.cropperRect = cropperRect\r\n\t\t\tthis.operationHistory = [{imageRect,cropperRect}];\r\n\t\t\tthis.operationIndex = 0;\r\n\t\t\tthis.setTarget();\r\n\t\t\t// #ifdef MP-WEIXIN\r\n\t\t\tconst systemInfo = uni.getSystemInfoSync();\r\n\t\t\tif (this.canvas2d === false || systemInfo.platform === 'windows' || systemInfo.platform === 'mac') {\r\n\t\t\t\tthis.type2d = false;\r\n\t\t\t} else {\r\n\t\t\t\tthis.type2d = true;\r\n\t\t\t\tthis.pixel = systemInfo.pixelRatio;\r\n\t\t\t}\r\n\t\t\t// #endif\r\n\t\t\t//非微信小程序端强制关闭canvas2d模式\r\n\t\t\t// #ifndef MP-WEIXIN\r\n\t\t\tthis.type2d = false;\r\n\t\t\t// #endif\r\n\t\t\t// #ifdef  MP-TOUTIAO || MP-LARK || MP-ALIPAY\r\n\t\t\tthis.type2d = this.canvas2d;\r\n\t\t\t// #endif\r\n\t\t},\r\n\t\t// 设置目标图像的大小\r\n\t\tsetTarget(){\r\n\t\t\tconst ratio = this.cropperRect.width / this.cropperRect.height;\r\n\t\t\tif (!!this.dWidth) {\r\n\t\t\t\tthis.target = {\r\n\t\t\t\t\twidth: this.dWidth,\r\n\t\t\t\t\theight: this.dWidth / (ratio || 1)\r\n\t\t\t\t}\r\n\t\t\t} else {\r\n\t\t\t\tconst width = Math.min(this.maxWidth, this.cropperRect.width * (this.imageInfo.width / this.imageRect.width));\r\n\t\t\t\tthis.target = {\r\n\t\t\t\t\twidth,\r\n\t\t\t\t\theight: width / (ratio || 1)\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t},\r\n\t\taddHistory({imageRect,cropperRect}){\r\n\t\t\tif(this.operationIndex!==this.operationHistory.length-1){\r\n\t\t\t\tthis.operationHistory = this.operationHistory.slice(0,this.operationIndex)\r\n\t\t\t}\r\n\t\t\tthis.operationHistory.push({\r\n\t\t\t\timageRect,\r\n\t\t\t\tcropperRect\r\n\t\t\t});\r\n\t\t\tif (this.operationHistory.length > 10) {\r\n\t\t\t\tthis.operationHistory.shift();\r\n\t\t\t}\r\n\t\t\tthis.operationIndex = this.operationHistory.length - 1;\r\n\t\t},\r\n\t\tupdateData(data) {\r\n\t\t\tthis.imageRect = data.imageRect\r\n\t\t\tthis.cropperRect = data.cropperRect\r\n\t\t\tthis.addHistory(data);\r\n\t\t\tthis.setTarget();\r\n\t\t\tif (this.autoZoom) {\r\n\t\t\t\tthis.timer = setTimeout(() => {\r\n\t\t\t\t\tthis.zoomToFill();\r\n\t\t\t\t}, 600);\r\n\t\t\t}\r\n\t\t\tconst {imageRect,cropperRect} = data\r\n\t\t\tconst scale = imageRect.width/this.imageInfo.width\r\n\t\t\tthis.$emit('change', {\r\n\t\t\t\tleft: (cropperRect.left - imageRect.left) /scale,\r\n\t\t\t\ttop: (cropperRect.top - imageRect.top) / scale,\r\n\t\t\t\twidth: cropperRect.width / scale,\r\n\t\t\t\theight: cropperRect.height / scale\r\n\t\t\t});\r\n\t\t},\r\n\t\tgetContainer() {\r\n\t\t\tif (this.containerSize !== null && typeof this.containerSize == 'object') {\r\n\t\t\t\tconst { width, height } = this.containerSize;\r\n\t\t\t\treturn Promise.resolve({\r\n\t\t\t\t\twidth: parseUnit(width),\r\n\t\t\t\t\theight: parseUnit(height)\r\n\t\t\t\t});\r\n\t\t\t} else {\r\n\t\t\t\treturn new Promise(resolve => {\r\n\t\t\t\t\tconst query = uni.createSelectorQuery().in(this);\r\n\t\t\t\t\tquery\r\n\t\t\t\t\t\t.select('.mainContent')\r\n\t\t\t\t\t\t.boundingClientRect(rect => {\r\n\t\t\t\t\t\t\tresolve(rect);\r\n\t\t\t\t\t\t})\r\n\t\t\t\t\t\t.exec();\r\n\t\t\t\t});\r\n\t\t\t}\r\n\t\t},\r\n\t\tzoomToFill() {\r\n\t\t\tthis.startAnim();\r\n\t\t\tconst beforeCropper = {\r\n\t\t\t\t...this.cropperRect\r\n\t\t\t};\r\n\t\t\tconst operation = {\r\n\t\t\t\timageRect:this.imageRect,\r\n\t\t\t\tcropperRect:this.cropperRect\r\n\t\t\t};\r\n\t\t\tthis.cropperRect = this.initCropper().cropperRect;\r\n\t\t\tconst scale = this.cropperRect.width / beforeCropper.width;\r\n\t\t\tconst ox = beforeCropper.left - this.imageRect.left;\r\n\t\t\tconst oy = beforeCropper.top - this.imageRect.top;\r\n\t\t\tthis.imageRect = {\r\n\t\t\t\twidth: this.imageRect.width * scale,\r\n\t\t\t\theight: this.imageRect.height * scale,\r\n\t\t\t\tleft: this.imageRect.left + (this.cropperRect.left - beforeCropper.left) - (scale - 1) * ox,\r\n\t\t\t\ttop: this.imageRect.top + (this.cropperRect.top - beforeCropper.top) - (scale - 1) * oy\r\n\t\t\t};\r\n\t\t},\r\n\t\tonTouchStart() {\r\n\t\t\tthis.stopAnim();\r\n\t\t},\r\n\t\t// 撤销\r\n\t\tundo() {\r\n\t\t\tif (this.operationIndex > 0) {\r\n\t\t\t\tthis.operationIndex--;\r\n\t\t\t\tthis.imageRect = this.operationHistory[this.operationIndex].imageRect;\r\n\t\t\t\tthis.cropperRect = this.operationHistory[this.operationIndex].cropperRect;\r\n\t\t\t\treturn true;\r\n\t\t\t}\r\n\t\t\treturn false;\r\n\t\t},\r\n\t\t// 重做\r\n\t\tresume() {\r\n\t\t\tif (this.operationIndex < this.operationHistory.length - 1) {\r\n\t\t\t\tthis.operationIndex++;\r\n\t\t\t\tthis.imageRect = this.operationHistory[this.operationIndex].imageRect;\r\n\t\t\t\tthis.cropperRect = this.operationHistory[this.operationIndex].cropperRect;\r\n\t\t\t\treturn true;\r\n\t\t\t}\r\n\t\t\treturn false;\r\n\t\t},\r\n\t\tasync drawImage(ctx,image,x,y,w,h){\r\n\t\t\tif (this.type2d) {\r\n\t\t\t\tawait new Promise(resolve => (image.onload = resolve));\r\n\t\t\t\tctx.drawImage(image, x * this.pixel, y * this.pixel, w * this.pixel, h * this.pixel);\r\n\t\t\t} else {\r\n\t\t\t\tconst path = await new Promise((resolve)=>{\r\n\t\t\t\t\tuni.getImageInfo({\r\n\t\t\t\t\t\tsrc:image,\r\n\t\t\t\t\t\tsuccess({path}){\r\n\t\t\t\t\t\t\tresolve(path)\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t})\r\n\t\t\t\t})\r\n\t\t\t\tctx.drawImage(path, x * this.pixel, y * this.pixel, w * this.pixel, h * this.pixel);\r\n\t\t\t\tawait new Promise((resolve) => ctx.draw(false,resolve));\r\n\t\t\t}\r\n\t\t},\r\n\t\tasync crop() {\r\n\t\t\tlet ctx;\r\n\t\t\tlet canvas;\r\n\t\t\tthis.$emit('cropStart')\r\n\t\t\tthis.setTarget()\r\n\t\t\tif (this.type2d) {\r\n\t\t\t\tconst query = uni.createSelectorQuery().in(this);\r\n\t\t\t\tcanvas = await new Promise(resolve =>\r\n\t\t\t\t\tquery\r\n\t\t\t\t\t\t.select('.bt-canvas')\r\n\t\t\t\t\t\t.node(({ node }) => resolve(node))\r\n\t\t\t\t\t\t.exec()\r\n\t\t\t\t);\r\n\t\t\t\tcanvas.width = this.target.width * this.pixel;\r\n\t\t\t\tcanvas.height = this.target.height * this.pixel;\r\n\t\t\t\tctx = canvas.getContext('2d');\r\n\t\t\t\t// #ifdef MP-TOUTIAO\r\n\t\t\t\tif(this.type2d){\r\n\t\t\t\t\tconsole.warn(\"请注意：目前头条系小程序暂时无法使用2d canvas保存图片，建议换成V1版本\")\r\n\t\t\t\t}\r\n\t\t\t\t// #endif\r\n\t\t\t} else {\r\n\t\t\t\tctx = uni.createCanvasContext(this.canvasId,this);\r\n\t\t\t}\r\n\t\t\tconst scale = this.cropperRect.width / this.target.width;\r\n\t\t\tconst dx = (this.cropperRect.left - this.imageRect.left) / scale;\r\n\t\t\tconst dy = (this.cropperRect.top - this.imageRect.top) / scale;\r\n\t\t\tlet image;\r\n\t\t\tif(this.type2d){\r\n\t\t\t\timage = canvas.createImage()\r\n\t\t\t\timage.src = this.imageSrc;\r\n\t\t\t}else{\r\n\t\t\t\timage = this.imageSrc;\r\n\t\t\t}\r\n\t\t\tawait this.drawImage(ctx,image, -dx, -dy, this.imageRect.width / scale, this.imageRect.height / scale);\r\n\t\t\tif (this.mask !== '') {\r\n\t\t\t\tlet imageData;\r\n\t\t\t\tif (this.type2d) {\r\n\t\t\t\t\timageData = ctx.getImageData(0, 0, this.target.width, this.target.height);\r\n\t\t\t\t} else {\r\n\t\t\t\t\timageData = await new Promise(resolve => {\r\n\t\t\t\t\t\tuni.canvasGetImageData({\r\n\t\t\t\t\t\t\tcanvasId: this.canvasId,\r\n\t\t\t\t\t\t\tx: 0,\r\n\t\t\t\t\t\t\ty: 0,\r\n\t\t\t\t\t\t\twidth: this.target.width,\r\n\t\t\t\t\t\t\theight: this.target.height,\r\n\t\t\t\t\t\t\tsuccess(res) {\r\n\t\t\t\t\t\t\t\tresolve(res);\r\n\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t},this);\r\n\t\t\t\t\t});\r\n\t\t\t\t}\r\n\t\t\t\tctx.clearRect(0, 0, this.target.width, this.target.height);\r\n\t\t\t\tif(this.type2d){\r\n\t\t\t\t\timage.src = this.mask;\r\n\t\t\t\t}else{\r\n\t\t\t\t\timage = this.mask;\r\n\t\t\t\t}\r\n\t\t\t\tawait this.drawImage(ctx,image, 0, 0, this.target.width, this.target.height);\r\n\t\t\t\tlet maskData;\r\n\t\t\t\tif (this.type2d) {\r\n\t\t\t\t\tmaskData = ctx.getImageData(0, 0, this.target.width, this.target.height);\r\n\t\t\t\t} else {\r\n\t\t\t\t\tmaskData = await new Promise((resolve,reject) => {\r\n\t\t\t\t\t\tuni.canvasGetImageData({\r\n\t\t\t\t\t\t\tcanvasId: this.canvasId,\r\n\t\t\t\t\t\t\tx: 0,\r\n\t\t\t\t\t\t\ty: 0,\r\n\t\t\t\t\t\t\twidth: this.target.width,\r\n\t\t\t\t\t\t\theight: this.target.height,\r\n\t\t\t\t\t\t\tsuccess(res) {\r\n\t\t\t\t\t\t\t\tresolve(res);\r\n\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t},this);\r\n\t\t\t\t\t});\r\n\t\t\t\t}\r\n\t\t\t\tctx.clearRect(0, 0, this.target.width, this.target.height);\r\n\t\t\t\tfor (let index = 3; index < maskData.data.length; index += 4) {\r\n\t\t\t\t\tconst alpha = maskData.data[index];\r\n\t\t\t\t\tif (alpha !== 0) {\r\n\t\t\t\t\t\timageData.data[index] = 0;\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t\tif (this.type2d) {\r\n\t\t\t\t\tctx.putImageData(imageData, 0, 0);\r\n\t\t\t\t} else {\r\n\t\t\t\t\tawait new Promise(resolve => {\r\n\t\t\t\t\t\tuni.canvasPutImageData({\r\n\t\t\t\t\t\t\tcanvasId: this.canvasId,\r\n\t\t\t\t\t\t\tx: 0,\r\n\t\t\t\t\t\t\ty: 0,\r\n\t\t\t\t\t\t\twidth: imageData.width,\r\n\t\t\t\t\t\t\theight: imageData.height,\r\n\t\t\t\t\t\t\tdata: imageData.data,\r\n\t\t\t\t\t\t\tcomplete: res => {\r\n\t\t\t\t\t\t\t\tresolve(res);\r\n\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t},this);\r\n\t\t\t\t\t});\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t\treturn new Promise(resolve => {\r\n\t\t\t\tconst params = {};\r\n\t\t\t\tif (this.type2d) {\r\n\t\t\t\t\tparams.canvas = canvas;\r\n\t\t\t\t} else {\r\n\t\t\t\t\tparams.canvasId = this.canvasId;\r\n\t\t\t\t}\r\n\t\t\t\t\r\n\t\t\t\tuni.canvasToTempFilePath({\r\n\t\t\t\t\t...params,\r\n\t\t\t\t\tdestWidth: this.target.width,\r\n\t\t\t\t\tdestHeight: this.target.height,\r\n\t\t\t\t\tquality: Number(this.quality) || 1,\r\n\t\t\t\t\tfileType: this.fileType,\r\n\t\t\t\t\tsuccess: ({ tempFilePath }) => {\r\n\t\t\t\t\t\t// #ifdef H5\r\n\t\t\t\t\t\tvar arr = tempFilePath.split(',');\r\n\t\t\t\t\t\tvar mime = arr[0].match(/:(.*?);/)[1];\r\n\t\t\t\t\t\tvar bstr = atob(arr[1]);\r\n\t\t\t\t\t\tvar n = bstr.length;\r\n\t\t\t\t\t\tvar u8arr = new Uint8Array(n);\r\n\t\t\t\t\t\tfor (var i = 0; i < n; i++) {\r\n\t\t\t\t\t\t\tu8arr[i] = bstr.charCodeAt(i);\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t\tvar url = URL || webkitURL;\r\n\t\t\t\t\t\tresolve(\r\n\t\t\t\t\t\t\turl.createObjectURL(\r\n\t\t\t\t\t\t\t\tnew Blob([u8arr], {\r\n\t\t\t\t\t\t\t\t\ttype: mime\r\n\t\t\t\t\t\t\t\t})\r\n\t\t\t\t\t\t\t)\r\n\t\t\t\t\t\t);\r\n\t\t\t\t\t\t// #endif\r\n\t\t\t\t\t\tresolve(tempFilePath);\r\n\t\t\t\t\t},\r\n\t\t\t\t\tfail(err) {\r\n\t\t\t\t\t\tconsole.log(err);\r\n\t\t\t\t\t\tresolve('tempFilePath');\r\n\t\t\t\t\t}\r\n\t\t\t\t},this);\r\n\t\t\t});\r\n\t\t}\r\n\t}\r\n};\r\n</script>\r\n<!-- #ifdef APP-VUE || MP-WEIXIN || MP-QQ || H5 -->\r\n<script module=\"wxsModule\" lang=\"wxs\">\r\nvar startTouchs = [];\r\nvar startDistance = 0;\r\nvar touchCenter = [];\r\nvar ratio = 0;\r\nvar imageInstance = null;\r\nvar cropperInstance = null;\r\nvar touchType = \"\";\r\nvar touchInstance = null;\r\nvar cropperRect = null;\r\nvar imageRect = null;\r\n// 操作时改变的对象\r\nvar changes = {\r\n\timageRect: null,\r\n\tcropperRect: null\r\n}\r\n\r\nfunction updateImageStyle() {\r\n\tvar imageRect = changes.imageRect\r\n\timageInstance.setStyle({\r\n\t\tleft: imageRect.left + 'px',\r\n\t\ttop: imageRect.top + 'px',\r\n\t\twidth: imageRect.width + 'px',\r\n\t\theight: imageRect.height + 'px'\r\n\t})\r\n}\r\n\r\nfunction updateCopperStyle() {\r\n\tvar cropperRect = changes.cropperRect\r\n\tcropperInstance.setStyle({\r\n\t\tleft: cropperRect.left + \"px\",\r\n\t\ttop: cropperRect.top + \"px\",\r\n\t\twidth: cropperRect.width + \"px\",\r\n\t\theight: cropperRect.height + \"px\"\r\n\t})\r\n}\r\n\r\nfunction imageScale(scaleRate) {\r\n\tvar cw = imageRect.width * (scaleRate - 1)\r\n\tvar ch = imageRect.height * (scaleRate - 1)\r\n\tchanges.imageRect = {\r\n\t\twidth: imageRect.width + cw,\r\n\t\theight: imageRect.height + ch,\r\n\t\tleft: imageRect.left - cw * (touchCenter[0]),\r\n\t\ttop: imageRect.top - ch * (touchCenter[1])\r\n\t}\r\n}\r\nmodule.exports = {\r\n\ttouchStart: function (ev, oi) {\r\n\t\t// #ifdef H5\r\n\t\tev.preventDefault();\r\n\t\tev.stopPropagation();\r\n\t\t// #endif\r\n\t\ttouchInstance = ev.instance;\r\n\t\tvar dataSet = ev.instance.getDataset()\r\n\t\ttouchType = dataSet.type;\r\n\t\tstartTouchs = ev.touches;\r\n\t\toi.callMethod('onTouchStart')\r\n\t\tif (startTouchs.length == 2) {\r\n\t\t\ttouchType = \"image\"\r\n\t\t\tvar x1 = startTouchs[0].clientX\r\n\t\t\tvar y1 = startTouchs[0].clientY\r\n\t\t\tvar x2 = startTouchs[1].clientX\r\n\t\t\tvar y2 = startTouchs[1].clientY\r\n\t\t\tvar distance = Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2)\r\n\t\t\tstartDistance = Math.sqrt(distance)\r\n\t\t\tvar leftPercent = ((x1 + x2) / 2 - imageRect.left) / imageRect.width\r\n\t\t\tvar topPercent = ((y1 + y2) / 2 - imageRect.top) / imageRect.height\r\n\t\t\ttouchCenter = [leftPercent, topPercent]\r\n\t\t}\r\n\t\treturn false;\r\n\t},\r\n\ttouchMove: function (ev, io) {\r\n\t\tif (touchType == \"\") return false\r\n\t\t// #ifdef H5\r\n\t\tev.preventDefault();\r\n\t\tev.stopPropagation();\r\n\t\t// #endif\r\n\t\tvar touches = ev.touches;\r\n\t\tvar changeX1 = touches[0].clientX - startTouchs[0].clientX;\r\n\t\tvar changeY1 = touches[0].clientY - startTouchs[0].clientY;\r\n\t\tif (startTouchs.length == 1) {\r\n\t\t\tif (touchType === 'image') {\r\n\t\t\t\tchanges.imageRect.left = imageRect.left + changeX1;\r\n\t\t\t\tchanges.imageRect.top = imageRect.top + changeY1;\r\n\t\t\t\tupdateImageStyle()\r\n\t\t\t} else if (touchType === 'controller') {\r\n\t\t\t\tvar directionX = 0;\r\n\t\t\t\tif (touchInstance.hasClass('left')) {\r\n\t\t\t\t\tdirectionX = -1;\r\n\t\t\t\t}\r\n\t\t\t\tif (touchInstance.hasClass('right')) {\r\n\t\t\t\t\tdirectionX = 1;\r\n\t\t\t\t}\r\n\t\t\t\tvar directionY = 0;\r\n\t\t\t\tif (touchInstance.hasClass('top')) {\r\n\t\t\t\t\tdirectionY = -1\r\n\t\t\t\t}\r\n\t\t\t\tif (touchInstance.hasClass('bottom')) {\r\n\t\t\t\t\tdirectionY = 1\r\n\t\t\t\t}\r\n\t\t\t\tvar changeX = changeX1 * directionX;\r\n\t\t\t\tvar changeY = changeY1 * directionY;\r\n\t\t\t\t// 比例缩放控制\r\n\t\t\t\tif (ratio !== 0) {\r\n\t\t\t\t\tif (directionX * directionY !== 0) {\r\n\t\t\t\t\t\tif (changeX / ratio > changeY) {\r\n\t\t\t\t\t\t\tchangeY = changeX / ratio\r\n\t\t\t\t\t\t\tchangeX = changeY * ratio\r\n\t\t\t\t\t\t} else {\r\n\t\t\t\t\t\t\tchangeX = changeY * ratio\r\n\t\t\t\t\t\t\tchangeY = changeX / ratio\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t} else {\r\n\t\t\t\t\t\tif (directionX == 0) {\r\n\t\t\t\t\t\t\tchangeX = changeY * ratio\r\n\t\t\t\t\t\t} else {\r\n\t\t\t\t\t\t\tchangeY = changeX / ratio\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\r\n\t\t\t\tvar width = cropperRect.width + changeX\r\n\t\t\t\tvar height = cropperRect.height + changeY\r\n\t\t\t\tvar imageRight = imageRect.left + imageRect.width\r\n\t\t\t\tvar imageBottom = imageRect.top + imageRect.height\r\n\t\t\t\tif (directionX != -1) {\r\n\t\t\t\t\tif (cropperRect.left + width > imageRight) {\r\n\t\t\t\t\t\twidth = imageRight - cropperRect.left\r\n\t\t\t\t\t\tif (ratio !== 0) {\r\n\t\t\t\t\t\t\theight = width / ratio\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}\r\n\t\t\t\t} else {\r\n\t\t\t\t\tvar cLeft = cropperRect.left - changeX\r\n\t\t\t\t\tif (cLeft < imageRect.left) {\r\n\t\t\t\t\t\twidth = cropperRect.left + cropperRect.width - imageRect.left\r\n\t\t\t\t\t\tif (ratio !== 0) {\r\n\t\t\t\t\t\t\theight = width / ratio\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t\t// 判断是否触底\r\n\t\t\t\tif (directionY != -1) {\r\n\t\t\t\t\tif (cropperRect.top + height > imageBottom) {\r\n\t\t\t\t\t\theight = imageBottom - cropperRect.top\r\n\t\t\t\t\t\tif (ratio !== 0) {\r\n\t\t\t\t\t\t\twidth = height * ratio\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}\r\n\t\t\t\t} else {\r\n\t\t\t\t\tvar cTop = cropperRect.top - changeY\r\n\t\t\t\t\tif (cTop < imageRect.top) {\r\n\t\t\t\t\t\theight = cropperRect.top + cropperRect.height - imageRect.top\r\n\t\t\t\t\t\tif (ratio !== 0) {\r\n\t\t\t\t\t\t\twidth = height * ratio\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}\r\n\t\t\t\t}\r\n\t\t\t\tif (directionX == -1) {\r\n\t\t\t\t\tchanges.cropperRect.left = cropperRect.left + cropperRect.width - width\r\n\t\t\t\t}\r\n\t\t\t\tif (directionY == -1) {\r\n\t\t\t\t\tchanges.cropperRect.top = cropperRect.top + cropperRect.height - height\r\n\t\t\t\t}\r\n\t\t\t\t// 边界控制\r\n\t\t\t\tchanges.cropperRect.width = width\r\n\t\t\t\tchanges.cropperRect.height = height\r\n\t\t\t\tupdateCopperStyle()\r\n\t\t\t}\r\n\t\t} else if (touches.length == 2 && startTouchs.length == 2) {\r\n\t\t\tvar changeX2 = touches[0].clientX - touches[1].clientX;\r\n\t\t\tvar changeY2 = touches[0].clientY - touches[1].clientY;\r\n\t\t\tvar distance = Math.pow(changeX2, 2) + Math.pow(changeY2, 2)\r\n\t\t\tdistance = Math.sqrt(distance)\r\n\t\t\t// 放大比例\r\n\t\t\tvar scaleRate = distance / startDistance\r\n\t\t\timageScale(scaleRate)\r\n\t\t\tupdateImageStyle()\r\n\t\t}\r\n\t\treturn false;\r\n\t},\r\n\ttouchEnd: function (ev, oi) {\r\n\t\tif (touchType === \"image\") {\r\n\t\t\tvar cropperLeft = cropperRect.left\r\n\t\t\tvar cropperRight = cropperRect.left + cropperRect.width\r\n\t\t\tvar cropperTop = cropperRect.top\r\n\t\t\tvar cropperBottom = cropperTop + cropperRect.height\r\n\t\t\tvar rate = changes.imageRect.width / changes.imageRect.height\r\n\t\t\tvar cropperRate = cropperRect.width / cropperRect.height\r\n\t\t\tif (changes.imageRect.width < cropperRect.width || changes.imageRect.height < cropperRect.height) {\r\n\t\t\t\tvar scale = 1\r\n\t\t\t\tif (rate < cropperRate) {\r\n\t\t\t\t\tscale = cropperRect.width / changes.imageRect.width\r\n\t\t\t\t} else {\r\n\t\t\t\t\tscale = cropperRect.height / changes.imageRect.height\r\n\t\t\t\t}\r\n\t\t\t\timageRect.width = changes.imageRect.width\r\n\t\t\t\timageRect.height = changes.imageRect.height\r\n\t\t\t\timageScale(scale)\r\n\t\t\t}\r\n\t\t\t// 边界控制start\r\n\t\t\tif (cropperLeft < changes.imageRect.left) {\r\n\t\t\t\tchanges.imageRect.left = cropperLeft\r\n\t\t\t}\r\n\t\t\tif (cropperRight > changes.imageRect.left + changes.imageRect.width) {\r\n\t\t\t\tchanges.imageRect.left = cropperRight - changes.imageRect.width\r\n\t\t\t}\r\n\t\t\tif (cropperTop < changes.imageRect.top) {\r\n\t\t\t\tchanges.imageRect.top = cropperTop\r\n\t\t\t}\r\n\t\t\tif (cropperBottom > changes.imageRect.top + changes.imageRect.height) {\r\n\t\t\t\tchanges.imageRect.top = cropperBottom - changes.imageRect.height\r\n\t\t\t}\r\n\t\t\t// 边界控制end\r\n\t\t\tupdateImageStyle()\r\n\t\t}\r\n\t\toi.callMethod('updateData', {\r\n\t\t\tcropperRect: changes.cropperRect,\r\n\t\t\timageRect: changes.imageRect,\r\n\t\t})\r\n\t\ttouchType = \"\"\r\n\t\tstartTouchs = []\r\n\t\treturn false;\r\n\t},\r\n\t// 将逻辑层的图像变换同步过来\r\n\t// 裁剪比例变化\r\n\tchangeRatio: function (value) {\r\n\t\tratio = value\r\n\t},\r\n\tchangeImageRect: function (value, oldValue, oi) {\r\n\t\tif (value) {\r\n\t\t\timageRect = value;\r\n\t\t\tchanges.imageRect = {\r\n\t\t\t\tleft: value.left,\r\n\t\t\t\ttop: value.top,\r\n\t\t\t\twidth: value.width,\r\n\t\t\t\theight: value.height\r\n\t\t\t};\r\n\t\t\t// #ifndef MP-WEIXIN || MP-QQ\r\n\t\t\tsetTimeout(function() {\r\n\t\t\t\timageInstance = oi.selectComponent('.mainContent > .image')\r\n\t\t\t\tupdateImageStyle();\r\n\t\t\t});\r\n\t\t\t// #endif\r\n\t\t\t// #ifdef MP-WEIXIN || MP-QQ\r\n\t\t\timageInstance = oi.selectComponent('.mainContent > .image')\r\n\t\t\tupdateImageStyle();\r\n\t\t\t// #endif\r\n\t\t}\r\n\t},\r\n\tchangeCropper: function (value, oldValue, oi) {\r\n\t\tif (value) {\r\n\t\t\tcropperRect = value\r\n\t\t\tchanges.cropperRect = {\r\n\t\t\t\tleft: value.left,\r\n\t\t\t\ttop: value.top,\r\n\t\t\t\twidth: value.width,\r\n\t\t\t\theight: value.height\r\n\t\t\t}\r\n\t\t\t// #ifdef H5 || APP-VUE\r\n\t\t\tsetTimeout(function() {\r\n\t\t\t// #endif\r\n\t\t\t\tcropperInstance = oi.selectComponent('.mainContent > .cropper')\r\n\t\t\t\tupdateCopperStyle()\r\n\t\t\t// #ifdef H5 || APP-VUE\r\n\t\t\t});\r\n\t\t\t// #endif\r\n\t\t}\r\n\t}\r\n}\r\n</script>\r\n<!-- #endif -->\r\n<style lang=\"scss\" scoped>\r\n.bt-container {\r\n\t// display: flex;\r\n\t// flex-direction: column;\r\n\t// justify-content: space-between;\r\n\theight: 100%;\r\n\tbox-sizing: border-box;\r\n\t// background-color: #0e1319;\r\n\tposition: relative;\r\n\toverflow: hidden;\r\n\r\n\t.bt-canvas {\r\n\t\tposition: fixed;\r\n\t\tleft: 100%;\r\n\t\ttop: 0;\r\n\t}\r\n\r\n\t.mainContent {\r\n\t\t// flex: 1;\r\n\t\t// flex-shrink:0;\r\n\t\t// margin: 60rpx;\r\n\t\twidth: 100%;\r\n\t\theight: 100%;\r\n\t\tposition: relative;\r\n\t\tdisplay: flex;\r\n\t\tjustify-content: center;\r\n\t\talign-items: center;\r\n\t\t// touch-action: none;\r\n\r\n\t\t.image {\r\n\t\t\tposition: absolute;\r\n\t\t\t// will-change: transform;\r\n\t\t\t// transform-origin: center center;\r\n\t\t\twidth: 85%;\r\n\t\t\theight: 85%;\r\n\t\t\twill-change: left, top, width, height;\r\n\t\t}\r\n\r\n\t\t.controller {\r\n\t\t\tposition: absolute;\r\n\t\t\tz-index: 99;\r\n\t\t\tpadding: 10rpx;\r\n\t\t\t$offset: -20rpx;\r\n\r\n\t\t\t&::after {\r\n\t\t\t\tdisplay: block;\r\n\t\t\t\tcontent: '';\r\n\t\t\t\tfilter: drop-shadow(0 0px 10rpx rgba(0, 0, 0, 0.3));\r\n\t\t\t}\r\n\r\n\t\t\t&.vertical {\r\n\t\t\t\ttop: calc(50% - 30rpx);\r\n\t\t\t}\r\n\r\n\t\t\t&.horizon {\r\n\t\t\t\tleft: calc(50% - 30rpx);\r\n\t\t\t}\r\n\r\n\t\t\t&.left {\r\n\t\t\t\t&::after {\r\n\t\t\t\t\theight: 40rpx;\r\n\t\t\t\t\tborder-left: 10rpx solid #fff;\r\n\t\t\t\t}\r\n\r\n\t\t\t\tleft: $offset;\r\n\t\t\t}\r\n\r\n\t\t\t&.right {\r\n\t\t\t\t&::after {\r\n\t\t\t\t\theight: 40rpx;\r\n\t\t\t\t\tborder-right: 10rpx solid #fff;\r\n\t\t\t\t}\r\n\r\n\t\t\t\tright: $offset;\r\n\t\t\t}\r\n\r\n\t\t\t&.top {\r\n\t\t\t\ttop: $offset;\r\n\r\n\t\t\t\t&::after {\r\n\t\t\t\t\twidth: 40rpx;\r\n\t\t\t\t\tborder-top: 10rpx solid #fff;\r\n\t\t\t\t}\r\n\t\t\t}\r\n\r\n\t\t\t&.bottom {\r\n\t\t\t\tbottom: $offset;\r\n\r\n\t\t\t\t&::after {\r\n\t\t\t\t\twidth: 40rpx;\r\n\t\t\t\t\tborder-bottom: 10rpx solid #fff;\r\n\t\t\t\t}\r\n\t\t\t}\r\n\r\n\t\t\t&.left.bottom,\r\n\t\t\t&.right.bottom,\r\n\t\t\t&.left.top,\r\n\t\t\t&.left.bottom {\r\n\t\t\t\t&::after {\r\n\t\t\t\t\twidth: 30rpx;\r\n\t\t\t\t\theight: 30rpx;\r\n\t\t\t\t\tbackground-color: transparent;\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\t.cropper {\r\n\t\t\tposition: absolute;\r\n\t\t\tborder: 1px solid #eee;\r\n\t\t\tbox-sizing: content-box;\r\n\t\t\t// transform-origin: center center;\r\n\t\t\toutline: 999px solid rgba(0, 0, 0, 0.5);\r\n\t\t\twill-change: left, top, width, height;\r\n\r\n\t\t\t// display: contain;\r\n\t\t\t// pointer-events: none;\r\n\t\t\t.mask {\r\n\t\t\t\tposition: absolute;\r\n\t\t\t\tleft: 0;\r\n\t\t\t\ttop: 0;\r\n\t\t\t\twidth: 100%;\r\n\t\t\t\theight: 100%;\r\n\t\t\t\topacity: 0.5;\r\n\t\t\t}\r\n\r\n\t\t\t.line {\r\n\t\t\t\tposition: absolute;\r\n\t\t\t\t// background-color: #eee;\r\n\t\t\t}\r\n\r\n\t\t\t.row {\r\n\t\t\t\twidth: 100%;\r\n\t\t\t\theight: 0px;\r\n\t\t\t\tleft: 0;\r\n\t\t\t\tborder-top: 1px dashed #007aff;\r\n\t\t\t}\r\n\r\n\t\t\t.col {\r\n\t\t\t\theight: 100%;\r\n\t\t\t\twidth: 0px;\r\n\t\t\t\tborder-left: 1px dashed #007aff;\r\n\t\t\t}\r\n\r\n\t\t\t.row1 {\r\n\t\t\t\ttop: 33%;\r\n\t\t\t}\r\n\r\n\t\t\t.row2 {\r\n\t\t\t\ttop: 66%;\r\n\t\t\t}\r\n\r\n\t\t\t.col1 {\r\n\t\t\t\tleft: 33%;\r\n\t\t\t}\r\n\r\n\t\t\t.col2 {\r\n\t\t\t\tleft: 66%;\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\t// .slot {\r\n\t// \tposition: relative;\r\n\t// \tpadding-top: 20rpx;\r\n\t// }\r\n}\r\n\r\n.anim {\r\n\ttransition: 0.2s;\r\n}\r\n</style>\r\n"
  },
  {
    "path": "UniApp/uni_modules/bt-cropper_3.0.1/components/bt-cropper/iconfont.css",
    "content": "@font-face {\r\n  font-family: \"iconfont\"; /* Project id 3311610 */\r\n  src: url('//at.alicdn.com/t/font_3311610_7wh8injedpd.woff2?t=1649382821379') format('woff2'),\r\n       url('//at.alicdn.com/t/font_3311610_7wh8injedpd.woff?t=1649382821379') format('woff'),\r\n       url('//at.alicdn.com/t/font_3311610_7wh8injedpd.ttf?t=1649382821379') format('truetype');\r\n}\r\n\r\n.iconfont {\r\n  font-family: \"iconfont\" !important;\r\n  font-size: 16px;\r\n  font-style: normal;\r\n  -webkit-font-smoothing: antialiased;\r\n  -moz-osx-font-smoothing: grayscale;\r\n}\r\n\r\n.icon-reset:before {\r\n  content: \"\\e611\";\r\n}\r\n\r\n.icon-move:before {\r\n  content: \"\\e67b\";\r\n}\r\n"
  },
  {
    "path": "UniApp/uni_modules/bt-cropper_3.0.1/components/bt-cropper/js/touchs.js",
    "content": "var startTouchs = [];\r\nvar touchType = ''\r\nvar startDistance = 0;\r\nvar touchCenter = [];\r\nvar cropperRect = null;\r\nvar imageRect = null;\r\nvar directionX = 0;\r\nvar directionY = 0;\r\nvar ratio = 0;\r\n// 操作时改变的对象\r\nvar changes = {\r\n\timageRect: null,\r\n\tcropperRect: null\r\n}\r\nexport default {\r\n\tcomputed: {\r\n\t\timageStyle() {\r\n\t\t\tconst imageRect = this.imageRect\r\n\t\t\tif (imageRect) {\r\n\t\t\t\treturn {\r\n\t\t\t\t\tleft: imageRect.left + 'px',\r\n\t\t\t\t\ttop: imageRect.top + 'px',\r\n\t\t\t\t\twidth: imageRect.width + 'px',\r\n\t\t\t\t\theight: imageRect.height + 'px'\r\n\t\t\t\t}\r\n\t\t\t} else {\r\n\t\t\t\treturn {}\r\n\t\t\t}\r\n\t\t},\r\n\t\tcropperStyle() {\r\n\t\t\tconst cropperRect = this.cropperRect\r\n\t\t\tif (cropperRect) {\r\n\t\t\t\treturn {\r\n\t\t\t\t\tleft: cropperRect.left + 'px',\r\n\t\t\t\t\ttop: cropperRect.top + 'px',\r\n\t\t\t\t\twidth: cropperRect.width + 'px',\r\n\t\t\t\t\theight: cropperRect.height + 'px'\r\n\t\t\t\t}\r\n\t\t\t} else {\r\n\t\t\t\treturn {}\r\n\t\t\t}\r\n\t\t}\r\n\t},\r\n\tmethods: {\r\n\t\ttouchStart() {\r\n\t\t\tlet ev;\r\n\t\t\tif (arguments.length == 3) {\r\n\t\t\t\tdirectionX = arguments[0];\r\n\t\t\t\tdirectionY = arguments[1];\r\n\t\t\t\tev = arguments[2];\r\n\t\t\t\ttouchType = \"controller\";\r\n\t\t\t} else {\r\n\t\t\t\ttouchType = \"image\";\r\n\t\t\t\tev = arguments[0];\r\n\t\t\t}\r\n\t\t\tstartTouchs = ev.touches;\r\n\t\t\tchanges = {\r\n\t\t\t\timageRect: this.imageRect,\r\n\t\t\t\tcropperRect: this.cropperRect\r\n\t\t\t};\r\n\t\t\tratio = this.ratio;\r\n\t\t\tcropperRect = {\r\n\t\t\t\t...changes.cropperRect\r\n\t\t\t}\r\n\t\t\timageRect = {\r\n\t\t\t\t...changes.imageRect\r\n\t\t\t}\r\n\t\t\tif (startTouchs.length == 2) {\r\n\t\t\t\tconst imageRect = this.imageRect\r\n\t\t\t\tvar x1 = startTouchs[0].clientX\r\n\t\t\t\tvar y1 = startTouchs[0].clientY\r\n\t\t\t\tvar x2 = startTouchs[1].clientX\r\n\t\t\t\tvar y2 = startTouchs[1].clientY\r\n\t\t\t\tvar distance = Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2)\r\n\t\t\t\tstartDistance = Math.sqrt(distance)\r\n\t\t\t\tvar leftPercent = ((x1 + x2) / 2 - imageRect.left) / imageRect.width\r\n\t\t\t\tvar topPercent = ((y1 + y2) / 2 - imageRect.top) / imageRect.height\r\n\t\t\t\ttouchCenter = [leftPercent, topPercent]\r\n\t\t\t}\r\n\t\t},\r\n\t\ttouchMove(ev) {\r\n\t\t\tif(startTouchs.length!==ev.touches.length) return\r\n\t\t\tvar touches = ev.touches;\r\n\t\t\tvar changeX1 = touches[0].clientX - startTouchs[0].clientX;\r\n\t\t\tvar changeY1 = touches[0].clientY - startTouchs[0].clientY;\r\n\t\t\tif (startTouchs.length == 1) {\r\n\t\t\t\tif (touchType === 'image') {\r\n\t\t\t\t\tchanges.imageRect.left = imageRect.left + changeX1;\r\n\t\t\t\t\tchanges.imageRect.top = imageRect.top + changeY1;\r\n\t\t\t\t\t// console.log(startTouchs.length,ev.touches.length)\r\n\t\t\t\t} else if (touchType === 'controller') {\r\n\t\t\t\t\tvar changeX = changeX1 * directionX;\r\n\t\t\t\t\tvar changeY = changeY1 * directionY;\r\n\t\t\t\t\t// 比例缩放控制\r\n\t\t\t\t\tif (ratio !== 0) {\r\n\t\t\t\t\t\tif (directionX * directionY !== 0) {\r\n\t\t\t\t\t\t\tif (changeX / ratio > changeY) {\r\n\t\t\t\t\t\t\t\tchangeY = changeX / ratio\r\n\t\t\t\t\t\t\t\tchangeX = changeY * ratio\r\n\t\t\t\t\t\t\t} else {\r\n\t\t\t\t\t\t\t\tchangeX = changeY * ratio\r\n\t\t\t\t\t\t\t\tchangeY = changeX / ratio\r\n\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t} else {\r\n\t\t\t\t\t\t\tif (directionX == 0) {\r\n\t\t\t\t\t\t\t\tchangeX = changeY * ratio\r\n\t\t\t\t\t\t\t} else {\r\n\t\t\t\t\t\t\t\tchangeY = changeX / ratio\r\n\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}\r\n\t\t\t\t\r\n\t\t\t\t\tvar width = cropperRect.width + changeX\r\n\t\t\t\t\tvar height = cropperRect.height + changeY\r\n\t\t\t\t\tvar imageRight = imageRect.left + imageRect.width\r\n\t\t\t\t\tvar imageBottom = imageRect.top + imageRect.height\r\n\t\t\t\t\tif (directionX != -1) {\r\n\t\t\t\t\t\tif (cropperRect.left + width > imageRight) {\r\n\t\t\t\t\t\t\twidth = imageRight - cropperRect.left\r\n\t\t\t\t\t\t\tif (ratio !== 0) {\r\n\t\t\t\t\t\t\t\theight = width / ratio\r\n\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t} else {\r\n\t\t\t\t\t\tvar cLeft = cropperRect.left - changeX\r\n\t\t\t\t\t\tif (cLeft < imageRect.left) {\r\n\t\t\t\t\t\t\twidth = cropperRect.left + cropperRect.width - imageRect.left\r\n\t\t\t\t\t\t\tif (ratio !== 0) {\r\n\t\t\t\t\t\t\t\theight = width / ratio\r\n\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}\r\n\t\t\t\t\t// 判断是否触底\r\n\t\t\t\t\tif (directionY != -1) {\r\n\t\t\t\t\t\tif (cropperRect.top + height > imageBottom) {\r\n\t\t\t\t\t\t\theight = imageBottom - cropperRect.top\r\n\t\t\t\t\t\t\tif (ratio !== 0) {\r\n\t\t\t\t\t\t\t\twidth = height * ratio\r\n\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t} else {\r\n\t\t\t\t\t\tvar cTop = cropperRect.top - changeY\r\n\t\t\t\t\t\tif (cTop < imageRect.top) {\r\n\t\t\t\t\t\t\theight = cropperRect.top + cropperRect.height - imageRect.top\r\n\t\t\t\t\t\t\tif (ratio !== 0) {\r\n\t\t\t\t\t\t\t\twidth = height * ratio\r\n\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}\r\n\t\t\t\t\tif (directionX == -1) {\r\n\t\t\t\t\t\tchanges.cropperRect.left = cropperRect.left + cropperRect.width - width\r\n\t\t\t\t\t}\r\n\t\t\t\t\tif (directionY == -1) {\r\n\t\t\t\t\t\tchanges.cropperRect.top = cropperRect.top + cropperRect.height - height\r\n\t\t\t\t\t}\r\n\t\t\t\t\t// 边界控制\r\n\t\t\t\t\tchanges.cropperRect.width = width\r\n\t\t\t\t\tchanges.cropperRect.height = height\r\n\t\t\t\t}\r\n\t\t\t} else if (touches.length == 2 && startTouchs.length == 2) {\r\n\t\t\t\tvar changeX2 = touches[0].clientX - touches[1].clientX;\r\n\t\t\t\tvar changeY2 = touches[0].clientY - touches[1].clientY;\r\n\t\t\t\tvar distance = Math.pow(changeX2, 2) + Math.pow(changeY2, 2)\r\n\t\t\t\tdistance = Math.sqrt(distance)\r\n\t\t\t\t// 放大比例\r\n\t\t\t\tvar scaleRate = distance / startDistance\r\n\t\t\t\tthis.imageScale(scaleRate)\r\n\t\t\t}\r\n\t\t},\r\n\t\ttouchEnd(ev) {\r\n\t\t\t// console.log('end',ev)\r\n\t\t\tif(ev.touches.length!==0) return\r\n\t\t\tif (touchType === \"image\") {\r\n\t\t\t\tvar cropperLeft = cropperRect.left\r\n\t\t\t\tvar cropperRight = cropperRect.left + cropperRect.width\r\n\t\t\t\tvar cropperTop = cropperRect.top\r\n\t\t\t\tvar cropperBottom = cropperTop + cropperRect.height\r\n\t\t\t\tvar rate = changes.imageRect.width / changes.imageRect.height\r\n\t\t\t\tvar cropperRate = cropperRect.width / cropperRect.height\r\n\t\t\t\tif (changes.imageRect.width < cropperRect.width || changes.imageRect.height < cropperRect.height) {\r\n\t\t\t\t\tvar scale = 1\r\n\t\t\t\t\tif (rate < cropperRate) {\r\n\t\t\t\t\t\tscale = cropperRect.width / changes.imageRect.width\r\n\t\t\t\t\t} else {\r\n\t\t\t\t\t\tscale = cropperRect.height / changes.imageRect.height\r\n\t\t\t\t\t}\r\n\t\t\t\t\timageRect.width = changes.imageRect.width\r\n\t\t\t\t\timageRect.height = changes.imageRect.height\r\n\t\t\t\t\tthis.imageScale(scale)\r\n\t\t\t\t}\r\n\t\t\t\t// 边界控制start\r\n\t\t\t\tif (cropperLeft < changes.imageRect.left) {\r\n\t\t\t\t\tchanges.imageRect.left = cropperLeft\r\n\t\t\t\t}\r\n\t\t\t\tif (cropperRight > changes.imageRect.left + changes.imageRect.width) {\r\n\t\t\t\t\tchanges.imageRect.left = cropperRight - changes.imageRect.width\r\n\t\t\t\t}\r\n\t\t\t\tif (cropperTop < changes.imageRect.top) {\r\n\t\t\t\t\tchanges.imageRect.top = cropperTop\r\n\t\t\t\t}\r\n\t\t\t\tif (cropperBottom > changes.imageRect.top + changes.imageRect.height) {\r\n\t\t\t\t\tchanges.imageRect.top = cropperBottom - changes.imageRect.height\r\n\t\t\t\t}\r\n\t\t\t\t// 边界控制end\r\n\t\t\t}\r\n\t\t\tthis.updateData({\r\n\t\t\t\tcropperRect: changes.cropperRect,\r\n\t\t\t\timageRect: changes.imageRect,\r\n\t\t\t})\r\n\t\t\ttouchType = \"\"\r\n\t\t\tstartTouchs = []\r\n\t\t\treturn false;\r\n\t\t},\r\n\t\timageScale(scaleRate) {\r\n\t\t\tvar cw = imageRect.width * (scaleRate - 1)\r\n\t\t\tvar ch = imageRect.height * (scaleRate - 1)\r\n\t\t\tchanges.imageRect = {\r\n\t\t\t\twidth: imageRect.width + cw,\r\n\t\t\t\theight: imageRect.height + ch,\r\n\t\t\t\tleft: imageRect.left - cw * (touchCenter[0]),\r\n\t\t\t\ttop: imageRect.top - ch * (touchCenter[1])\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n}\r\n"
  },
  {
    "path": "UniApp/uni_modules/bt-cropper_3.0.1/components/bt-cropper/utils/tools.js",
    "content": "export function getTouchPoints(touchs) {\r\n\treturn Array.from(touchs).map(ev => {\r\n\t\treturn [ev.clientX, ev.clientY]\r\n\t})\r\n}\r\n// 函数防抖\r\nexport function debounce(fn, wait = 200) {\r\n\tvar timer = null;\r\n\treturn function (){\r\n\t\tif (timer !== null) {\r\n\t\t\tclearTimeout(timer);\r\n\t\t}\r\n\t\ttimer = setTimeout(fn.bind(this), wait);\r\n\t}\r\n}\r\n\r\n/**\r\n * @description 睡眠\r\n * @param {number} time 等待时间毫秒数\r\n */\r\nexport function sleep(time = 200) {\r\n\treturn new Promise(resolve => {\r\n\t\tsetTimeout(resolve, time)\r\n\t})\r\n}\r\nconst systemInfo = uni.getSystemInfoSync();\r\n\r\nexport function parseUnit(size){\r\n\tif(typeof size == 'number' || !isNaN(Number(size))){\r\n\t\treturn uni.upx2px(size)\r\n\t}else if(typeof size === 'string') {\r\n\t\tif(size.endsWith('rpx')){\r\n\t\t\treturn parseUnit(size.replace('rpx',''))\r\n\t\t}else if(size.endsWith('px')){\r\n\t\t\treturn Number(size.replace('px',''))\r\n\t\t}else if(size.endsWith('vw')){\r\n\t\t\treturn Number(size.replace('vw',''))*systemInfo.screenWidth/100\r\n\t\t}else if(size.endsWith('vh')){\r\n\t\t\treturn Number(size.replace('vh',''))*systemInfo.screenHeight/100\r\n\t\t}\r\n\t}\r\n\treturn 0\r\n}\r\n"
  },
  {
    "path": "UniApp/uni_modules/bt-cropper_3.0.1/components/bt-cropper/{ages.json",
    "content": ""
  },
  {
    "path": "UniApp/uni_modules/bt-cropper_3.0.1/package.json",
    "content": "{\r\n\t\"id\": \"bt-cropper\",\r\n\t\"displayName\": \"bt-cropper图片裁剪插件\",\r\n\t\"version\": \"3.0.1\",\r\n\t\"description\": \"一款好用的图片裁剪插件\",\r\n\t\"keywords\": [\r\n        \"图片\",\r\n        \"图片裁剪\",\r\n        \"图片裁剪\",\r\n        \"头像裁剪\",\r\n        \"cropper\"\r\n    ],\r\n\t\"repository\": \"\",\r\n\t\"engines\": {\r\n\t\t\"HBuilderX\": \"^3.2.1\"\r\n\t},\r\n    \"dcloudext\": {\r\n        \"sale\": {\r\n\t\t\t\"regular\": {\r\n\t\t\t\t\"price\": \"0.00\"\r\n\t\t\t},\r\n\t\t\t\"sourcecode\": {\r\n\t\t\t\t\"price\": \"0.00\"\r\n\t\t\t}\r\n\t\t},\r\n\t\t\"contact\": {\r\n\t\t\t\"qq\": \"1097122362\"\r\n\t\t},\r\n\t\t\"declaration\": {\r\n\t\t\t\"ads\": \"无\",\r\n\t\t\t\"data\": \"插件不采集任何数据\",\r\n\t\t\t\"permissions\": \"无\"\r\n\t\t},\r\n        \"npmurl\": \"\",\r\n        \"type\": \"component-vue\"\r\n\t},\r\n\t\"uni_modules\": {\r\n\t\t\"dependencies\": [],\r\n\t\t\"encrypt\": [],\r\n\t\t\"platforms\": {\r\n\t\t\t\"cloud\": {\r\n\t\t\t\t\"tcb\": \"y\",\r\n\t\t\t\t\"aliyun\": \"y\"\r\n\t\t\t},\r\n\t\t\t\"client\": {\r\n\t\t\t\t\"Vue\": {\r\n\t\t\t\t\t\"vue2\": \"y\",\r\n\t\t\t\t\t\"vue3\": \"y\"\r\n\t\t\t\t},\r\n\t\t\t\t\"App\": {\r\n\t\t\t\t\t\"app-vue\": \"y\",\r\n\t\t\t\t\t\"app-nvue\": \"n\"\r\n\t\t\t\t},\r\n\t\t\t\t\"H5-mobile\": {\r\n\t\t\t\t\t\"Safari\": \"y\",\r\n\t\t\t\t\t\"Android Browser\": \"y\",\r\n\t\t\t\t\t\"微信浏览器(Android)\": \"y\",\r\n\t\t\t\t\t\"QQ浏览器(Android)\": \"y\"\r\n\t\t\t\t},\r\n\t\t\t\t\"H5-pc\": {\r\n\t\t\t\t\t\"Chrome\": \"n\",\r\n\t\t\t\t\t\"IE\": \"n\",\r\n\t\t\t\t\t\"Edge\": \"n\",\r\n\t\t\t\t\t\"Firefox\": \"n\",\r\n\t\t\t\t\t\"Safari\": \"n\"\r\n\t\t\t\t},\r\n\t\t\t\t\"小程序\": {\r\n\t\t\t\t\t\"微信\": \"y\",\r\n\t\t\t\t\t\"阿里\": \"u\",\r\n\t\t\t\t\t\"百度\": \"u\",\r\n\t\t\t\t\t\"字节跳动\": \"y\",\r\n\t\t\t\t\t\"QQ\": \"y\"\r\n\t\t\t\t},\r\n\t\t\t\t\"快应用\": {\r\n\t\t\t\t\t\"华为\": \"n\",\r\n\t\t\t\t\t\"联盟\": \"n\"\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n}\r\n"
  },
  {
    "path": "UniApp/uni_modules/bt-cropper_3.0.1/readme.md",
    "content": "\r\n\r\n## bt-cropper 图片裁切\r\n> **组件名：bt-cropper**\r\n\r\n图片裁切组件，在页面中裁切图片，输出裁切后的图片，支持app，小程序，H5\r\n### [在线体验](https://static-a3b890b4-7cb2-4b29-aa78-e652572bdef6.bspapp.com/#/)\r\n\r\n> **注意事项**\r\n> 为了避免错误使用，给大家带来不好的开发体验，请在使用组件前仔细阅读下面的注意事项，可以帮你避免一些错误。\r\n> - 组件需要依赖 `sass` 插件 ，请自行手动安装\r\n> - 只测试了头条小程序，app-vue 安卓，微信小程序和H5 大部分平台应该都没问题了\r\n> - 包裹层或裁剪器需要手动指定高度和宽度，推荐手动指定裁剪器的大小，尤其是头条小程序，js有时候获取不到容器的大小\r\n> - 如使用过程中有任何问题，或者您有一些好的建议，欢迎联系作者微信:1097122362\r\n\r\n\r\n\r\n### 安装方式\r\n\r\n本组件符合[easycom](https://uniapp.dcloud.io/collocation/pages?id=easycom)规范，`HBuilderX 2.5.5`起，只需将本组件导入项目，在页面`template`中即可直接使用，无需在页面中`import`和注册`components`。\r\n\r\n### 基本用法 \r\n\r\n**示例**\r\n\r\n```html\r\n<template>\r\n\t<view class=\"container\">\r\n\t\t<bt-cropper ref=\"cropper\" :imageSrc=\"imageSrc\"></bt-cropper>\r\n\t\t<button @click=\"crop\">裁切</button>\r\n\t</view>\r\n</template>\r\n<style>\r\n\t.container{\r\n\t\t/** 外层一定要指定大小 */\r\n\t\theight:100vh;\r\n\t}\r\n</style>\r\n```\r\n\r\n\r\n```javascript\r\nexport default {\r\n   methods:{\r\n      crop(){\r\n        // 通过组件定义的ref调用cropper方法，返回一个promise对象\r\n        this.$refs.cropper.crop().then(([err,res])=>{\r\n\t\t\tif(!err){\r\n\t\t\t\tconsole.log(res)\r\n\t\t\t}else{\r\n\t\t\t\tconsole.err(err)\r\n\t\t\t}\r\n\t\t})\r\n      }\r\n   }\r\n}\r\n\r\n```\r\n\r\n### 限定裁切比例\r\n\r\nbt-cropper，指定ratio即可设置裁切框的宽高比，如果你想让用户自由缩放，将ratio设置为0即可\r\n\r\n**示例**\r\n\r\n```html\r\n<bt-cropper ref=\"cropper\" :ratio=\"16/9\":imageSrc=\"imageSrc\"></bt-cropper>\r\n```\r\n\r\n## API\r\n\r\n### cropper Props \r\n\r\n|属性名|类型|默认值|说明|\r\n|:-:|:-:|:-:|:-:|\r\n|ratio|number|0|裁切图像的宽高比，0表示自由比例|\r\n|dWidth|number|0|生成的图片的宽度,单位：px,如果传入0的话就是按原像素的比例裁剪，也就是说，输出图片的清晰度和输入图片的清晰度一样|\r\n|imageSrc|String|''|原图的路径，支持本地路径和网络路径，如果是网络路径，小程序要注意配置下载域名，H5要注意跨域问题|\r\n|mask|String|''| 裁剪的蒙版url，配合蒙版可以裁剪出任何形状的图形 (示例见全屏裁剪demo) |\r\n|fileType|String|'jpg'|目标文件的类型，只支持 'jpg' 或 'png'。默认为 'jpg'|\r\n|quality|Number|1|图片的质量，取值范围为 (0, 1]，不在范围内时当作1.0处理|\r\n|showGrid|Boolean|false|是否显示中心网格线，默认不显示|\r\n|initPosition|object|null|图片自定义的初始的位置，内容格式见change事件|\r\n|autoZoom|Boolean|true|是否开启操作结束后自动放大到窗口大小|\r\n|containerSize|object|null|手动指定容器大小，如果裁剪器放在大小会移动或缩放的dom中，则必须手动指定大小，可以带上单位，如果不带单位默认px，支持的单位有：rpx，px，vw，vm,示例：{width:100,height:1100}或者：{width:'100rpx',height:'100rpx'}|\r\n|canvas2d|Boolean|false| 是开启新版的canvas |\r\n\r\n\r\n\r\n### cropper Methods\r\n\r\n|方法名称|说明|参数|\r\n|:-:|:-:|:-:|\r\n|crop|裁剪图片|开始绘制并开始裁剪图片，返回Promise对象|\r\n|init|初始化|-|\r\n|resetImage|重置裁剪框和图片的位置和大小到初始的位置和大小|-|\r\n|redo|撤销，最多可以回退10步|-|\r\n|resume|重做|-|\r\n\r\n\r\n### cropper Events\r\n\r\n|方法名称|说明|返回值|\r\n|:-:|:-:|:-:|\r\n|change|当裁剪框和图片的相对位置发生变化的时候触发，返回裁剪框与图片的相对位置|ev={left:number,top:number,width:number,height:number}|\r\n|loadFail|当图片加载失败时触发| - |\r\n|cropStart|当裁开始时触发| - |\r\n## 帮助\r\n在使用中如遇到无法解决的问题，请提 [Issues](https://gitee.com/xiaojiang1996/better-uni-cropper/issues) 或者加我 微信:1097122362。\r\n"
  },
  {
    "path": "UniApp/uni_modules/mp-html/README.md",
    "content": "## 为减小组件包的大小，默认组件包中不包含编辑、latex 公式等扩展功能，需要使用扩展功能的请参考下方的 插件扩展 栏的说明\n\n## 功能介绍\n- 全端支持（含 `v3、NVUE`）\n- 支持丰富的标签（包括 `table`、`video`、`svg` 等）\n- 支持丰富的事件效果（自动预览图片、链接处理等）\n- 支持设置占位图（加载中、出错时、预览时）\n- 支持锚点跳转、长按复制等丰富功能\n- 支持大部分 *html* 实体\n- 丰富的插件（关键词搜索、内容编辑、`latex` 公式等）\n- 效率高、容错性强且轻量化\n\n查看 [功能介绍](https://jin-yufeng.gitee.io/mp-html/#/overview/feature) 了解更多\n\n## 使用方法\n- `uni_modules` 方式  \n  1. 点击右上角的 `使用 HBuilder X 导入插件` 按钮直接导入项目或点击 `下载插件 ZIP` 按钮下载插件包并解压到项目的 `uni_modules/mp-html` 目录下  \n  2. 在需要使用页面的 `(n)vue` 文件中添加  \n     ```html\n     <!-- 不需要引入，可直接使用 -->\n     <mp-html :content=\"html\" />\n     ```\n     ```javascript\n     export default {\n       data() {\n         return {\n           html: '<div>Hello World!</div>'\n         }\n       }\n     }\n     ```\n  3. 需要更新版本时在 `HBuilder X` 中右键 `uni_modules/mp-html` 目录选择 `从插件市场更新` 即可  \n\n- 源码方式  \n  1. 从 [github](https://github.com/jin-yufeng/mp-html/tree/master/dist/uni-app) 或 [gitee](https://gitee.com/jin-yufeng/mp-html/tree/master/dist/uni-app) 下载源码  \n     插件市场的 **非 uni_modules 版本** 无法更新，不建议从插件市场获取  \n  2. 在需要使用页面的 `(n)vue` 文件中添加  \n     ```html\n     <mp-html :content=\"html\" />\n     ```\n     ```javascript\n     import mpHtml from '@/components/mp-html/mp-html'\n     export default {\n       // HBuilderX 2.5.5+ 可以通过 easycom 自动引入\n       components: {\n         mpHtml\n       },\n       data() {\n         return {\n           html: '<div>Hello World!</div>'\n         }\n       }\n     }\n     ```\n\n- npm 方式  \n  1. 在项目根目录下执行  \n     ```bash\n     npm install mp-html\n     ```\n  2. 在需要使用页面的 `(n)vue` 文件中添加  \n     ```html\n     <mp-html :content=\"html\" />\n     ```\n     ```javascript\n     import mpHtml from 'mp-html/dist/uni-app/components/mp-html/mp-html'\n     export default {\n       // 不可省略\n       components: {\n         mpHtml\n       },\n       data() {\n         return {\n           html: '<div>Hello World!</div>'\n         }\n       }\n     }\n     ```\n  3. 需要更新版本时执行以下命令即可  \n     ```bash\n     npm update mp-html\n     ```\n  \n  使用 *cli* 方式运行的项目，通过 *npm* 方式引入时，需要在 *vue.config.js* 中配置 *transpileDependencies*，详情可见 [#330](https://github.com/jin-yufeng/mp-html/issues/330#issuecomment-913617687)  \n  如果在 **nvue** 中使用还要将 `dist/uni-app/static` 目录下的内容拷贝到项目的 `static` 目录下，否则无法运行  \n\n查看 [快速开始](https://jin-yufeng.gitee.io/mp-html/#/overview/quickstart) 了解更多\n\n## 组件属性\n\n| 属性 | 类型 | 默认值 | 说明 |\n|:---:|:---:|:---:|---|\n| container-style | String |  | 容器的样式（[2.1.0+](https://jin-yufeng.gitee.io/mp-html/#/changelog/changelog#v210)） |\n| content | String |  | 用于渲染的 html 字符串 |\n| copy-link | Boolean | true | 是否允许外部链接被点击时自动复制 |\n| domain | String |  | 主域名（用于链接拼接） |\n| error-img | String |  | 图片出错时的占位图链接 |\n| lazy-load | Boolean | false | 是否开启图片懒加载 |\n| loading-img | String |  | 图片加载过程中的占位图链接 |\n| pause-video | Boolean | true | 是否在播放一个视频时自动暂停其他视频 |\n| preview-img | Boolean | true | 是否允许图片被点击时自动预览 |\n| scroll-table | Boolean | false | 是否给每个表格添加一个滚动层使其能单独横向滚动 |\n| selectable | Boolean | false | 是否开启文本长按复制 |\n| set-title | Boolean | true | 是否将 title 标签的内容设置到页面标题 |\n| show-img-menu | Boolean | true | 是否允许图片被长按时显示菜单 |\n| tag-style | Object |  | 设置标签的默认样式 |\n| use-anchor | Boolean | false | 是否使用锚点链接 |\n\n查看 [属性](https://jin-yufeng.gitee.io/mp-html/#/basic/prop) 了解更多\n\n## 组件事件\n\n| 名称 | 触发时机 |\n|:---:|---|\n| load | dom 树加载完毕时 |\n| ready | 图片加载完毕时 |\n| error | 发生渲染错误时 |\n| imgtap | 图片被点击时 |\n| linktap | 链接被点击时 |\n| play | 音视频播放时 |\n\n查看 [事件](https://jin-yufeng.gitee.io/mp-html/#/basic/event) 了解更多\n\n## api\n组件实例上提供了一些 `api` 方法可供调用\n\n| 名称 | 作用 |\n|:---:|---|\n| in | 将锚点跳转的范围限定在一个 scroll-view 内 |\n| navigateTo | 锚点跳转 |\n| getText | 获取文本内容 |\n| getRect | 获取富文本内容的位置和大小 |\n| setContent | 设置富文本内容 |\n| imgList | 获取所有图片的数组 |\n| pauseMedia | 暂停播放音视频（[2.2.2+](https://jin-yufeng.gitee.io/mp-html/#/changelog/changelog#v222)） |\n| setPlaybackRate | 设置音视频播放速率（[2.4.0+](https://jin-yufeng.gitee.io/mp-html/#/changelog/changelog#v240)） |\n\n查看 [api](https://jin-yufeng.gitee.io/mp-html/#/advanced/api) 了解更多\n\n## 插件扩展  \n除基本功能外，本组件还提供了丰富的扩展，可按照需要选用\n\n| 名称 | 作用 |\n|:---:|---|\n| audio | 音乐播放器 |\n| editable | 富文本 **编辑**（[示例项目](https://mp-html.oss-cn-hangzhou.aliyuncs.com/editable.zip)） |\n| emoji | 解析 emoji |\n| highlight | 代码块高亮显示 |\n| markdown | 渲染 markdown |\n| search | 关键词搜索 |\n| style | 匹配 style 标签中的样式 |\n| txv-video | 使用腾讯视频 |\n| img-cache | 图片缓存 by [@PentaTea](https://github.com/PentaTea) |\n| latex | 渲染 latex 公式 by [@Zeng-J](https://github.com/Zeng-J) |\n\n从插件市场导入的包中 **不含有** 扩展插件，使用插件需通过微信小程序 `富文本插件` 获取或参考以下方法进行打包：  \n1. 获取完整组件包  \n   ```bash\n   npm install mp-html\n   ```\n2. 编辑 `tools/config.js` 中的 `plugins` 项，选择需要的插件  \n3. 生成新的组件包  \n   在 `node_modules/mp-html` 目录下执行  \n   ```bash\n   npm install\n   npm run build:uni-app\n   ```\n4. 拷贝 `dist/uni-app` 中的内容到项目根目录  \n\n查看 [插件](https://jin-yufeng.gitee.io/mp-html/#/advanced/plugin) 了解更多\n\n## 关于 nvue\n`nvue` 使用原生渲染，不支持部分 `css` 样式，为实现和 `html` 相同的效果，组件内部通过 `web-view` 进行渲染，性能上差于原生，根据 `weex` 官方建议，`web` 标签仅应用在非常规的降级场景。因此，如果通过原生的方式（如 `richtext`）能够满足需要，则不建议使用本组件，如果有较多的富文本内容，则可以直接使用 `vue` 页面  \n由于渲染方式与其他端不同，有以下限制：  \n1. 不支持 `lazy-load` 属性\n2. 视频不支持全屏播放\n3. 如果在 `flex-direction: row` 的容器中使用，需要给组件设置宽度或设置 `flex: 1` 占满剩余宽度\n\n纯 `nvue` 模式下，[此问题](https://ask.dcloud.net.cn/question/119678) 修复前，不支持通过 `uni_modules` 引入，需要本地引入（将 [dist/uni-app](https://github.com/jin-yufeng/mp-html/tree/master/dist/uni-app) 中的内容拷贝到项目根目录下）  \n\n## 立即体验\n![富文本插件](https://mp-html.oss-cn-hangzhou.aliyuncs.com/qrcode.jpg)\n\n## 问题反馈\n遇到问题时，请先查阅 [常见问题](https://jin-yufeng.gitee.io/mp-html/#/question/faq) 和 [issue](https://github.com/jin-yufeng/mp-html/issues) 中是否已有相同的问题  \n可通过 [issue](https://github.com/jin-yufeng/mp-html/issues/new/choose) 、插件问答或发送邮件到 [mp_html@126.com](mailto:mp_html@126.com) 提问，不建议在评论区提问（不方便回复）  \n提问请严格按照 [issue 模板](https://github.com/jin-yufeng/mp-html/issues/new/choose) ，描述清楚使用环境、`html` 内容或可复现的 `demo` 项目以及复现方式，对于 **描述不清**、**无法复现** 或重复的问题将不予回复  \n\n欢迎加入 `QQ` 交流群：  \n群1（已满）：`699734691`  \n群2：`778239129`  \n\n查看 [问题反馈](https://jin-yufeng.gitee.io/mp-html/#/question/feedback) 了解更多\n"
  },
  {
    "path": "UniApp/uni_modules/mp-html/changelog.md",
    "content": "## v2.4.2（2023-05-14）\n1. `A` `editable` 插件支持修改文字颜色 [详细](https://github.com/jin-yufeng/mp-html/issues/254)\n2. `F` 修复了 `svg` 中有 `style` 不生效的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/505)\n3. `F` 修复了使用旧版编译器可能报错 `Bad attr nodes` 的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/472)\n4. `F` 修复了 `app` 端可能出现无法读取 `lazyLoad` 的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/513)\n5. `F` 修复了 `editable` 插件在点击换图时未拼接 `domain` 的问题 [详细](https://github.com/jin-yufeng/mp-html/pull/497) by [@TwoKe945](https://github.com/TwoKe945)\n6. `F` 修复了 `latex` 插件部分情况下不显示的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/515) \n7. `F` 修复了 `editable` 插件点击音视频时其他标签框不消失的问题\n## v2.4.1（2022-12-25）\n1. `F` 修复了没有图片时 `ready` 事件可能不触发的问题\n2. `F` 修复了加载过程中可能出现 `Root label not found` 错误的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/470)\n3. `F` 修复了 `audio` 插件退出页面可能会报错的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/457)\n4. `F` 修复了 `vue3` 运行到 `app` 在 `HBuilder X 3.6.10` 以上报错的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/480)\n5. `F` 修复了 `nvue` 端链接中包含 `%22` 时可能无法显示的问题\n6. `F` 修复了 `vue3` 使用 `highlight` 插件可能报错的问题\n## v2.4.0（2022-08-27）\n1. `A` 增加了 [setPlaybackRate](https://jin-yufeng.gitee.io/mp-html/#/advanced/api#setPlaybackRate) 的 `api`，可以设置音视频的播放速率 [详细](https://github.com/jin-yufeng/mp-html/issues/452)\n2. `A` 示例小程序代码开源 [详细](https://github.com/jin-yufeng/mp-html-demo)\n3. `U` 优化 `ready` 事件触发时机，未设置懒加载的情况下基本可以准确触发 [详细](https://github.com/jin-yufeng/mp-html/issues/195)\n4. `U` `highlight` 插件在编辑状态下不进行高亮处理，便于编辑\n5. `F` 修复了 `flex` 布局下图片大小可能不正确的问题\n6. `F` 修复了 `selectable` 属性没有设置 `force` 也可能出现渲染异常的问题\n7. `F` 修复了表格中的图片大小可能不正确的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/448)\n8. `F` 修复了含有合并单元格的表格可能无法设置竖直对齐的问题\n9. `F` 修复了 `editable` 插件在 `scroll-view` 中使用时工具条位置可能不正确的问题\n10. `F` 修复了 `vue3` 使用 [search](advanced/plugin#search) 插件可能导致错误换行的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/449)\n## v2.3.2（2022-08-13）\n1. `A` 增加 [latex](https://jin-yufeng.gitee.io/mp-html/#/advanced/plugin#latex) 插件，可以渲染数学公式 [详细](https://github.com/jin-yufeng/mp-html/pull/447) by [@Zeng-J](https://github.com/Zeng-J)\n2. `U` 优化根节点下有很多标签的长内容渲染速度\n3. `U` `highlight` 插件适配 `lang-xxx` 格式\n4. `F` 修复了 `table` 标签设置 `border` 属性后可能无法修改边框样式的问题 [详细](https://github.com/jin-yufeng/mp-html/pull/439) by [@zouxingjie](https://github.com/zouxingjie)\n5. `F` 修复了 `editable` 插件输入连续空格无效的问题\n6. `F` 修复了 `vue3` 图片设置 `inline` 会报错的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/438)\n7. `F` 修复了 `vue3` 使用 `table` 可能报错的问题\n## v2.3.1（2022-05-20）\n1. `U` `app` 端支持使用本地图片\n2. `U` 优化了微信小程序 `selectable` 属性在 `ios` 端的处理 [详细](https://jin-yufeng.gitee.io/mp-html/#/basic/prop#selectable)\n3. `F` 修复了 `editable` 插件不在顶部时 `tooltip` 位置可能错误的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/430)\n4. `F` 修复了 `vue3` 运行到微信小程序可能报错丢失内容的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/414)\n5. `F` 修复了 `vue3` 部分标签可能被错误换行的问题\n6. `F` 修复了 `editable` 插件 `app` 端插入视频无法预览的问题\n## v2.3.0（2022-04-01）\n1. `A` 增加了 `play` 事件，音视频播放时触发，可用于与页面其他音视频进行互斥播放 [详细](basic/event#play)\n2. `U` `show-img-menu` 属性支持控制预览时是否长按弹出菜单\n3. `U` 优化 `wxs` 处理，提高渲染性能 [详细](https://developers.weixin.qq.com/community/develop/article/doc/0006cc2b204740f601bd43fa25a413)  \n4. `U` `video` 标签支持 `object-fit` 属性\n5. `U` 增加支持一些常用实体编码 [详细](https://github.com/jin-yufeng/mp-html/issues/418)\n6. `F` 修复了图片仅设置高度可能不显示的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/410)\n7. `F` 修复了 `video` 标签高度设置为 `auto` 不显示的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/411)\n8. `F` 修复了使用 `grid` 布局时可能样式错误的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/413)\n9. `F` 修复了含有合并单元格的表格部分情况下显示异常的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/417)\n10. `F` 修复了 `editable` 插件连续插入内容时顺序不正确的问题\n11. `F` 修复了 `uni-app` 包 `vue3` 使用 `audio` 插件报错的问题\n12. `F` 修复了 `uni-app` 包 `highlight` 插件使用自定义的 `prism.min.js` 报错的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/416)\n## v2.2.2（2022-02-26）\n1. `A` 增加了 [pauseMedia](https://jin-yufeng.gitee.io/mp-html/#/advanced/api#pauseMedia) 的 `api`，可用于暂停播放音视频 [详细](https://github.com/jin-yufeng/mp-html/issues/317)\n2. `U` 优化了长内容的加载速度  \n3. `U` 适配 `vue3` [#389](https://github.com/jin-yufeng/mp-html/issues/389)、[#398](https://github.com/jin-yufeng/mp-html/pull/398) by [@zhouhuafei](https://github.com/zhouhuafei)、[#400](https://github.com/jin-yufeng/mp-html/issues/400)\n4. `F` 修复了小程序端图片高度设置为百分比时可能不显示的问题\n5. `F` 修复了 `highlight` 插件部分情况下可能显示不完整的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/403)\n## v2.2.1（2021-12-24）\n1. `A` `editable` 插件增加上下移动标签功能\n2. `U` `editable` 插件支持在文本中间光标处插入内容\n3. `F` 修复了 `nvue` 端设置 `margin` 后可能导致高度不正确的问题\n4. `F` 修复了 `highlight` 插件使用压缩版的 `prism.css` 可能导致背景失效的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/367)\n5. `F` 修复了编辑状态下使用 `emoji` 插件内容为空时可能报错的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/371)\n6. `F` 修复了使用 `editable` 插件后将 `selectable` 属性设置为 `force` 不生效的问题\n## v2.2.0（2021-10-12）\n1. `A` 增加 `customElements` 配置项，便于添加自定义功能性标签 [详细](https://github.com/jin-yufeng/mp-html/issues/350)\n2. `A` `editable` 插件增加切换音视频自动播放状态的功能 [详细](https://github.com/jin-yufeng/mp-html/pull/341) by [@leeseett](https://github.com/leeseett)\n3. `A` `editable` 插件删除媒体标签时触发 `remove` 事件，便于删除已上传的文件\n4. `U` `editable` 插件 `insertImg` 方法支持同时插入多张图片 [详细](https://github.com/jin-yufeng/mp-html/issues/342)\n5. `U` `editable` 插入图片和音视频时支持拼接 `domian` 主域名\n6. `F` 修复了内部链接参数中包含 `://` 时被认为是外部链接的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/356)\n7. `F` 修复了部分 `svg` 标签名或属性名大小写不正确时不生效的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/351)\n8. `F` 修复了 `nvue` 页面运行到非 `app` 平台时可能样式错误的问题\n## v2.1.5（2021-08-13）\n1. `A` 增加支持标签的 `dir` 属性\n2. `F` 修复了 `ruby` 标签文字与拼音没有居中对齐的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/325)\n3. `F` 修复了音视频标签内有 `a` 标签时可能无法播放的问题\n4. `F` 修复了 `externStyle` 中的 `class` 名包含下划线或数字时可能失效的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/326)\n5. `F` 修复了 `h5` 端引入 `externStyle` 可能不生效的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/326)\n## v2.1.4（2021-07-14）\n1. `F` 修复了 `rt` 标签无法设置样式的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/318)\n2. `F` 修复了表格中有单元格同时合并行和列时可能显示不正确的问题\n3. `F` 修复了 `app` 端无法关闭图片长按菜单的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/322)\n4. `F` 修复了 `editable` 插件只能添加图片链接不能修改的问题 [详细](https://github.com/jin-yufeng/mp-html/pull/312) by [@leeseett](https://github.com/leeseett)\n## v2.1.3（2021-06-12）\n1. `A` `editable` 插件增加 `insertTable` 方法\n2. `U` `editable` 插件支持编辑表格中的空白单元格 [详细](https://github.com/jin-yufeng/mp-html/issues/310)\n3. `F` 修复了 `externStyle` 中使用伪类可能失效的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/298)\n4. `F` 修复了多个组件同时使用时 `tag-style` 属性时可能互相影响的问题 [详细](https://github.com/jin-yufeng/mp-html/pull/305) by [@woodguoyu](https://github.com/woodguoyu)\n5. `F` 修复了包含 `linearGradient` 的 `svg` 可能无法显示的问题\n6. `F` 修复了编译到头条小程序时可能报错的问题\n7. `F` 修复了 `nvue` 端不触发 `click` 事件的问题\n8. `F` 修复了 `editable` 插件尾部插入时无法撤销的问题\n9. `F` 修复了 `editable` 插件的 `insertHtml` 方法只能在末尾插入的问题\n10. `F` 修复了 `editable` 插件插入音频不显示的问题\n## v2.1.2（2021-04-24）\n1. `A` 增加了 [img-cache](https://jin-yufeng.gitee.io/mp-html/#/advanced/plugin#img-cache) 插件，可以在 `app` 端缓存图片 [详细](https://github.com/jin-yufeng/mp-html/issues/292) by [@PentaTea](https://github.com/PentaTea)\n2. `U` 支持通过 `container-style` 属性设置 `white-space` 来保留连续空格和换行符 [详细](https://jin-yufeng.gitee.io/mp-html/#/question/faq#space)\n3. `U` 代码风格符合 [standard](https://standardjs.com) 标准\n4. `U` `editable` 插件编辑状态下支持预览视频 [详细](https://github.com/jin-yufeng/mp-html/issues/286)\n5. `F` 修复了 `svg` 标签内嵌 `svg` 时无法显示的问题\n6. `F` 修复了编译到支付宝和头条小程序时部分区域不可复制的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/291)\n## v2.1.1（2021-04-09）\n1. 修复了对 `p` 标签设置 `tag-style` 可能不生效的问题\n2. 修复了 `svg` 标签中的文本无法显示的问题\n3. 修复了使用 `editable` 插件编辑表格时可能报错的问题\n4. 修复了使用 `highlight` 插件运行到头条小程序时可能没有样式的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/280)\n5. 修复了使用 `editable` 插件 `editable` 属性为 `false` 时会报错的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/284)\n6. 修复了 `style` 插件连续子选择器失效的问题\n7. 修复了 `editable` 插件无法修改图片和字体大小的问题\n## v2.1.0.2（2021-03-21）\n修复了 `nvue` 端使用可能报错的问题\n## v2.1.0（2021-03-20）\n1. `A` 增加了 [container-style](https://jin-yufeng.gitee.io/mp-html/#/basic/prop#container-style) 属性 [详细](https://gitee.com/jin-yufeng/mp-html/pulls/1)\n2. `A` 增加支持 `strike` 标签\n3. `A` `editable` 插件增加 `placeholder` 属性 [详细](https://jin-yufeng.gitee.io/mp-html/#/advanced/plugin#editable)\n4. `A` `editable` 插件增加 `insertHtml` 方法 [详细](https://jin-yufeng.gitee.io/mp-html/#/advanced/plugin#editable)\n5. `U` 外部样式支持标签名选择器 [详细](https://jin-yufeng.gitee.io/mp-html/#/overview/quickstart#setting)\n6. `F` 修复了 `nvue` 端部分情况下可能不显示的问题\n## v2.0.5（2021-03-12）\n1. `U` [linktap](https://jin-yufeng.gitee.io/mp-html/#/basic/event#linktap) 事件增加返回内部文本内容 `innerText` [详细](https://github.com/jin-yufeng/mp-html/issues/271)\n2. `U` [selectable](https://jin-yufeng.gitee.io/mp-html/#/basic/prop#selectable) 属性设置为 `force` 时能够在微信 `iOS` 端生效（文本块会变成 `inline-block`） [详细](https://github.com/jin-yufeng/mp-html/issues/267)\n3. `F` 修复了部分情况下竖向无法滚动的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/182)\n4. `F` 修复了多次修改富文本数据时部分内容可能不显示的问题\n5. `F` 修复了 [腾讯视频](https://jin-yufeng.gitee.io/mp-html/#/advanced/plugin#txv-video) 插件可能无法播放的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/265)\n6. `F` 修复了 [highlight](https://jin-yufeng.gitee.io/mp-html/#/advanced/plugin#highlight) 插件没有设置高亮语言时没有应用默认样式的问题 [详细](https://github.com/jin-yufeng/mp-html/issues/276) by [@fuzui](https://github.com/fuzui)\n"
  },
  {
    "path": "UniApp/uni_modules/mp-html/components/mp-html/emoji/index.js",
    "content": "/**\r\n * @fileoverview emoji 插件\r\n */\r\nconst reg = /\\[(\\S+?)\\]/g\r\nconst data = {\r\n  笑脸: '😄',\r\n  生病: '😷',\r\n  破涕为笑: '😂',\r\n  吐舌: '😝',\r\n  脸红: '😳',\r\n  恐惧: '😱',\r\n  失望: '😔',\r\n  无语: '😒',\r\n  眨眼: '😉',\r\n  酷: '😎',\r\n  哭: '😭',\r\n  痴迷: '😍',\r\n  吻: '😘',\r\n  思考: '🤔',\r\n  困惑: '😕',\r\n  颠倒: '🙃',\r\n  钱: '🤑',\r\n  惊讶: '😲',\r\n  白眼: '🙄',\r\n  叹气: '😤',\r\n  睡觉: '😴',\r\n  书呆子: '🤓',\r\n  愤怒: '😡',\r\n  面无表情: '😑',\r\n  张嘴: '😮',\r\n  量体温: '🤒',\r\n  呕吐: '🤮',\r\n  光环: '😇',\r\n  幽灵: '👻',\r\n  外星人: '👽',\r\n  机器人: '🤖',\r\n  捂眼镜: '🙈',\r\n  捂耳朵: '🙉',\r\n  捂嘴: '🙊',\r\n  婴儿: '👶',\r\n  男孩: '👦',\r\n  女孩: '👧',\r\n  男人: '👨',\r\n  女人: '👩',\r\n  老人: '👴',\r\n  老妇人: '👵',\r\n  警察: '👮',\r\n  王子: '🤴',\r\n  公主: '🤴',\r\n  举手: '🙋',\r\n  跑步: '🏃',\r\n  家庭: '👪',\r\n  眼睛: '👀',\r\n  鼻子: '👃',\r\n  耳朵: '👂',\r\n  舌头: '👅',\r\n  嘴: '👄',\r\n  心: '❤️',\r\n  心碎: '💔',\r\n  雪人: '☃️',\r\n  情书: '💌',\r\n  大便: '💩',\r\n  闹钟: '⏰',\r\n  眼镜: '👓',\r\n  雨伞: '☂️',\r\n  音乐: '🎵',\r\n  话筒: '🎤',\r\n  游戏机: '🎮',\r\n  喇叭: '📢',\r\n  耳机: '🎧',\r\n  礼物: '🎁',\r\n  电话: '📞',\r\n  电脑: '💻',\r\n  打印机: '🖨️',\r\n  手电筒: '🔦',\r\n  灯泡: '💡',\r\n  书本: '📖',\r\n  信封: '✉️',\r\n  药丸: '💊',\r\n  口红: '💄',\r\n  手机: '📱',\r\n  相机: '📷',\r\n  电视: '📺',\r\n  中: '🀄',\r\n  垃圾桶: '🚮',\r\n  厕所: '🚾',\r\n  感叹号: '❗',\r\n  禁: '🈲',\r\n  可: '🉑',\r\n  彩虹: '🌈',\r\n  旋风: '🌀',\r\n  雷电: '⚡',\r\n  雪花: '❄️',\r\n  星星: '⭐',\r\n  水滴: '💧',\r\n  玫瑰: '🌹',\r\n  加油: '💪',\r\n  左: '👈',\r\n  右: '👉',\r\n  上: '👆',\r\n  下: '👇',\r\n  手掌: '🖐️',\r\n  好的: '👌',\r\n  好: '👍',\r\n  差: '👎',\r\n  胜利: '✌',\r\n  拳头: '👊',\r\n  挥手: '👋',\r\n  鼓掌: '👏',\r\n  猴子: '🐒',\r\n  狗: '🐶',\r\n  狼: '🐺',\r\n  猫: '🐱',\r\n  老虎: '🐯',\r\n  马: '🐎',\r\n  独角兽: '🦄',\r\n  斑马: '🦓',\r\n  鹿: '🦌',\r\n  牛: '🐮',\r\n  猪: '🐷',\r\n  羊: '🐏',\r\n  长颈鹿: '🦒',\r\n  大象: '🐘',\r\n  老鼠: '🐭',\r\n  蝙蝠: '🦇',\r\n  刺猬: '🦔',\r\n  熊猫: '🐼',\r\n  鸽子: '🕊️',\r\n  鸭子: '🦆',\r\n  兔子: '🐇',\r\n  老鹰: '🦅',\r\n  青蛙: '🐸',\r\n  蛇: '🐍',\r\n  龙: '🐉',\r\n  鲸鱼: '🐳',\r\n  海豚: '🐬',\r\n  足球: '⚽',\r\n  棒球: '⚾',\r\n  篮球: '🏀',\r\n  排球: '🏐',\r\n  橄榄球: '🏉',\r\n  网球: '🎾',\r\n  骰子: '🎲',\r\n  鸡腿: '🍗',\r\n  蛋糕: '🎂',\r\n  啤酒: '🍺',\r\n  饺子: '🥟',\r\n  汉堡: '🍔',\r\n  薯条: '🍟',\r\n  意大利面: '🍝',\r\n  干杯: '🥂',\r\n  筷子: '🥢',\r\n  糖果: '🍬',\r\n  奶瓶: '🍼',\r\n  爆米花: '🍿',\r\n  邮局: '🏤',\r\n  医院: '🏥',\r\n  银行: '🏦',\r\n  酒店: '🏨',\r\n  学校: '🏫',\r\n  城堡: '🏰',\r\n  火车: '🚂',\r\n  高铁: '🚄',\r\n  地铁: '🚇',\r\n  公交: '🚌',\r\n  救护车: '🚑',\r\n  消防车: '🚒',\r\n  警车: '🚓',\r\n  出租车: '🚕',\r\n  汽车: '🚗',\r\n  货车: '🚛',\r\n  自行车: '🚲',\r\n  摩托: '🛵',\r\n  红绿灯: '🚥',\r\n  帆船: '⛵',\r\n  游轮: '🛳️',\r\n  轮船: '⛴️',\r\n  飞机: '✈️',\r\n  直升机: '🚁',\r\n  缆车: '🚠',\r\n  警告: '⚠️',\r\n  禁止: '⛔'\r\n}\r\n\r\nfunction Emoji () {\r\n\r\n}\r\n\r\nEmoji.prototype.onUpdate = function (content) {\r\n  return content.replace(reg, ($, $1) => {\r\n    if (data[$1]) return data[$1]\r\n    return $\r\n  })\r\n}\r\n\r\nEmoji.prototype.onGetContent = function (content) {\r\n  for (const item in data) {\r\n    content = content.replace(new RegExp(data[item], 'g'), '[' + item + ']')\r\n  }\r\n  return content\r\n}\r\n\r\nexport default Emoji\r\n"
  },
  {
    "path": "UniApp/uni_modules/mp-html/components/mp-html/highlight/config.js",
    "content": "export default {\r\n  copyByLongPress: false, // 是否需要长按代码块时显示复制代码内容菜单\r\n  showLanguageName: false, // 是否在代码块右上角显示语言的名称\r\n  showLineNumber: false // 是否显示行号\r\n}\n"
  },
  {
    "path": "UniApp/uni_modules/mp-html/components/mp-html/highlight/index.js",
    "content": "/**\r\n * @fileoverview highlight 插件\r\n * Include prismjs (https://prismjs.com)\r\n */\r\nimport prism from './prism.min'\r\nimport config from './config'\r\nimport Parser from '../parser'\r\n\r\nfunction Highlight (vm) {\r\n  this.vm = vm\r\n}\r\n\r\nHighlight.prototype.onParse = function (node, vm) {\r\n  if (node.name === 'pre') {\r\n    if (vm.options.editable) {\r\n      node.attrs.class = (node.attrs.class || '') + ' hl-pre'\r\n      return\r\n    }\r\n    let i\r\n    for (i = node.children.length; i--;) {\r\n      if (node.children[i].name === 'code') break\r\n    }\r\n    if (i === -1) return\r\n    const code = node.children[i]\r\n    let className = code.attrs.class + ' ' + node.attrs.class\r\n    i = className.indexOf('language-')\r\n    if (i === -1) {\r\n      i = className.indexOf('lang-')\r\n      if (i === -1) {\r\n        className = 'language-text'\r\n        i = 9\r\n      } else {\r\n        i += 5\r\n      }\r\n    } else {\r\n      i += 9\r\n    }\r\n    let j\r\n    for (j = i; j < className.length; j++) {\r\n      if (className[j] === ' ') break\r\n    }\r\n    const lang = className.substring(i, j)\r\n    if (code.children.length) {\r\n      const text = this.vm.getText(code.children).replace(/&amp;/g, '&')\r\n      if (!text) return\r\n      if (node.c) {\r\n        node.c = undefined\r\n      }\r\n      if (prism.languages[lang]) {\r\n        code.children = (new Parser(this.vm).parse(\r\n          // 加一层 pre 保留空白符\r\n          '<pre>' + prism.highlight(text, prism.languages[lang], lang).replace(/token /g, 'hl-') + '</pre>'))[0].children\r\n      }\r\n      node.attrs.class = 'hl-pre'\r\n      code.attrs.class = 'hl-code'\r\n      if (config.showLanguageName) {\r\n        node.children.push({\r\n          name: 'div',\r\n          attrs: {\r\n            class: 'hl-language',\r\n            style: 'user-select:none'\r\n          },\r\n          children: [{\r\n            type: 'text',\r\n            text: lang\r\n          }]\r\n        })\r\n      }\r\n      if (config.copyByLongPress) {\r\n        node.attrs.style += (node.attrs.style || '') + ';user-select:none'\r\n        node.attrs['data-content'] = text\r\n        vm.expose()\r\n      }\r\n      if (config.showLineNumber) {\r\n        const line = text.split('\\n').length; const children = []\r\n        for (let k = line; k--;) {\r\n          children.push({\r\n            name: 'span',\r\n            attrs: {\r\n              class: 'span'\r\n            }\r\n          })\r\n        }\r\n        node.children.push({\r\n          name: 'span',\r\n          attrs: {\r\n            class: 'line-numbers-rows'\r\n          },\r\n          children\r\n        })\r\n      }\r\n    }\r\n  }\r\n}\r\n\r\nexport default Highlight\r\n"
  },
  {
    "path": "UniApp/uni_modules/mp-html/components/mp-html/markdown/index.js",
    "content": "/**\r\n * @fileoverview markdown 插件\r\n * Include marked (https://github.com/markedjs/marked)\r\n * Include github-markdown-css (https://github.com/sindresorhus/github-markdown-css)\r\n */\r\nimport marked from './marked.min'\r\nlet index = 0\r\n\r\nfunction Markdown (vm) {\r\n  this.vm = vm\r\n  vm._ids = {}\r\n}\r\n\r\nMarkdown.prototype.onUpdate = function (content) {\r\n  if (this.vm.markdown) {\r\n    return marked(content)\r\n  }\r\n}\r\n\r\nMarkdown.prototype.onParse = function (node, vm) {\r\n  if (vm.options.markdown) {\r\n    // 中文 id 需要转换，否则无法跳转\r\n    if (vm.options.useAnchor && node.attrs && /[\\u4e00-\\u9fa5]/.test(node.attrs.id)) {\r\n      const id = 't' + index++\r\n      this.vm._ids[node.attrs.id] = id\r\n      node.attrs.id = id\r\n    }\r\n    if (node.name === 'p' || node.name === 'table' || node.name === 'tr' || node.name === 'th' || node.name === 'td' || node.name === 'blockquote' || node.name === 'pre' || node.name === 'code') {\r\n      node.attrs.class = `md-${node.name} ${node.attrs.class || ''}`\r\n    }\r\n  }\r\n}\r\n\r\nexport default Markdown\r\n"
  },
  {
    "path": "UniApp/uni_modules/mp-html/components/mp-html/mp-html.vue",
    "content": "<template>\r\n  <view id=\"_root\" :class=\"(selectable?'_select ':'')+'_root'\" :style=\"containerStyle\">\r\n    <slot v-if=\"!nodes[0]\" />\r\n    <!-- #ifndef APP-PLUS-NVUE -->\r\n    <node v-else :childs=\"nodes\" :opts=\"[lazyLoad,loadingImg,errorImg,showImgMenu,selectable]\" name=\"span\" />\r\n    <!-- #endif -->\r\n    <!-- #ifdef APP-PLUS-NVUE -->\r\n    <web-view ref=\"web\" src=\"/static/app-plus/mp-html/local.html\" :style=\"'margin-top:-2px;height:' + height + 'px'\" @onPostMessage=\"_onMessage\" />\r\n    <!-- #endif -->\r\n  </view>\r\n</template>\r\n\r\n<script>\r\n/**\r\n * mp-html v2.4.2\r\n * @description 富文本组件\r\n * @tutorial https://github.com/jin-yufeng/mp-html\r\n * @property {String} container-style 容器的样式\r\n * @property {String} content 用于渲染的 html 字符串\r\n * @property {Boolean} copy-link 是否允许外部链接被点击时自动复制\r\n * @property {String} domain 主域名，用于拼接链接\r\n * @property {String} error-img 图片出错时的占位图链接\r\n * @property {Boolean} lazy-load 是否开启图片懒加载\r\n * @property {string} loading-img 图片加载过程中的占位图链接\r\n * @property {Boolean} pause-video 是否在播放一个视频时自动暂停其他视频\r\n * @property {Boolean} preview-img 是否允许图片被点击时自动预览\r\n * @property {Boolean} scroll-table 是否给每个表格添加一个滚动层使其能单独横向滚动\r\n * @property {Boolean | String} selectable 是否开启长按复制\r\n * @property {Boolean} set-title 是否将 title 标签的内容设置到页面标题\r\n * @property {Boolean} show-img-menu 是否允许图片被长按时显示菜单\r\n * @property {Object} tag-style 标签的默认样式\r\n * @property {Boolean | Number} use-anchor 是否使用锚点链接\r\n * @event {Function} load dom 结构加载完毕时触发\r\n * @event {Function} ready 所有图片加载完毕时触发\r\n * @event {Function} imgtap 图片被点击时触发\r\n * @event {Function} linktap 链接被点击时触发\r\n * @event {Function} play 音视频播放时触发\r\n * @event {Function} error 媒体加载出错时触发\r\n */\r\n// #ifndef APP-PLUS-NVUE\r\nimport node from './node/node'\r\n// #endif\r\nimport Parser from './parser'\r\nimport markdown from './markdown/index.js'\nimport emoji from './emoji/index.js'\nimport highlight from './highlight/index.js'\nconst plugins=[markdown,emoji,highlight,]\r\n// #ifdef APP-PLUS-NVUE\r\nconst dom = weex.requireModule('dom')\r\n// #endif\r\nexport default {\r\n  name: 'mp-html',\r\n  data () {\r\n    return {\r\n      nodes: [],\r\n      // #ifdef APP-PLUS-NVUE\r\n      height: 3\r\n      // #endif\r\n    }\r\n  },\r\n  props: {\n    markdown: Boolean,\r\n    containerStyle: {\r\n      type: String,\r\n      default: ''\r\n    },\r\n    content: {\r\n      type: String,\r\n      default: ''\r\n    },\r\n    copyLink: {\r\n      type: [Boolean, String],\r\n      default: true\r\n    },\r\n    domain: String,\r\n    errorImg: {\r\n      type: String,\r\n      default: ''\r\n    },\r\n    lazyLoad: {\r\n      type: [Boolean, String],\r\n      default: false\r\n    },\r\n    loadingImg: {\r\n      type: String,\r\n      default: ''\r\n    },\r\n    pauseVideo: {\r\n      type: [Boolean, String],\r\n      default: true\r\n    },\r\n    previewImg: {\r\n      type: [Boolean, String],\r\n      default: true\r\n    },\r\n    scrollTable: [Boolean, String],\r\n    selectable: [Boolean, String],\r\n    setTitle: {\r\n      type: [Boolean, String],\r\n      default: true\r\n    },\r\n    showImgMenu: {\r\n      type: [Boolean, String],\r\n      default: true\r\n    },\r\n    tagStyle: Object,\r\n    useAnchor: [Boolean, Number]\r\n  },\r\n  // #ifdef VUE3\r\n  emits: ['load', 'ready', 'imgtap', 'linktap', 'play', 'error'],\r\n  // #endif\r\n  // #ifndef APP-PLUS-NVUE\r\n  components: {\r\n    node\r\n  },\r\n  // #endif\r\n  watch: {\r\n    content (content) {\r\n      this.setContent(content)\r\n    }\r\n  },\r\n  created () {\r\n    this.plugins = []\r\n    for (let i = plugins.length; i--;) {\r\n      this.plugins.push(new plugins[i](this))\r\n    }\r\n  },\r\n  mounted () {\r\n    if (this.content && !this.nodes.length) {\r\n      this.setContent(this.content)\r\n    }\r\n  },\r\n  beforeDestroy () {\r\n    this._hook('onDetached')\r\n  },\r\n  methods: {\r\n    /**\r\n     * @description 将锚点跳转的范围限定在一个 scroll-view 内\r\n     * @param {Object} page scroll-view 所在页面的示例\r\n     * @param {String} selector scroll-view 的选择器\r\n     * @param {String} scrollTop scroll-view scroll-top 属性绑定的变量名\r\n     */\r\n    in (page, selector, scrollTop) {\r\n      // #ifndef APP-PLUS-NVUE\r\n      if (page && selector && scrollTop) {\r\n        this._in = {\r\n          page,\r\n          selector,\r\n          scrollTop\r\n        }\r\n      }\r\n      // #endif\r\n    },\r\n\r\n    /**\r\n     * @description 锚点跳转\r\n     * @param {String} id 要跳转的锚点 id\r\n     * @param {Number} offset 跳转位置的偏移量\r\n     * @returns {Promise}\r\n     */\r\n    navigateTo (id, offset) {\n      id = this._ids[decodeURI(id)] || id\r\n      return new Promise((resolve, reject) => {\r\n        if (!this.useAnchor) {\r\n          reject(Error('Anchor is disabled'))\r\n          return\r\n        }\r\n        offset = offset || parseInt(this.useAnchor) || 0\r\n        // #ifdef APP-PLUS-NVUE\r\n        if (!id) {\r\n          dom.scrollToElement(this.$refs.web, {\r\n            offset\r\n          })\r\n          resolve()\r\n        } else {\r\n          this._navigateTo = {\r\n            resolve,\r\n            reject,\r\n            offset\r\n          }\r\n          this.$refs.web.evalJs('uni.postMessage({data:{action:\"getOffset\",offset:(document.getElementById(' + id + ')||{}).offsetTop}})')\r\n        }\r\n        // #endif\r\n        // #ifndef APP-PLUS-NVUE\r\n        let deep = ' '\r\n        // #ifdef MP-WEIXIN || MP-QQ || MP-TOUTIAO\r\n        deep = '>>>'\r\n        // #endif\r\n        const selector = uni.createSelectorQuery()\r\n          // #ifndef MP-ALIPAY\r\n          .in(this._in ? this._in.page : this)\r\n          // #endif\r\n          .select((this._in ? this._in.selector : '._root') + (id ? `${deep}#${id}` : '')).boundingClientRect()\r\n        if (this._in) {\r\n          selector.select(this._in.selector).scrollOffset()\r\n            .select(this._in.selector).boundingClientRect()\r\n        } else {\r\n          // 获取 scroll-view 的位置和滚动距离\r\n          selector.selectViewport().scrollOffset() // 获取窗口的滚动距离\r\n        }\r\n        selector.exec(res => {\r\n          if (!res[0]) {\r\n            reject(Error('Label not found'))\r\n            return\r\n          }\r\n          const scrollTop = res[1].scrollTop + res[0].top - (res[2] ? res[2].top : 0) + offset\r\n          if (this._in) {\r\n            // scroll-view 跳转\r\n            this._in.page[this._in.scrollTop] = scrollTop\r\n          } else {\r\n            // 页面跳转\r\n            uni.pageScrollTo({\r\n              scrollTop,\r\n              duration: 300\r\n            })\r\n          }\r\n          resolve()\r\n        })\r\n        // #endif\r\n      })\r\n    },\r\n\r\n    /**\r\n     * @description 获取文本内容\r\n     * @return {String}\r\n     */\r\n    getText (nodes) {\r\n      let text = '';\r\n      (function traversal (nodes) {\r\n        for (let i = 0; i < nodes.length; i++) {\r\n          const node = nodes[i]\r\n          if (node.type === 'text') {\r\n            text += node.text.replace(/&amp;/g, '&')\r\n          } else if (node.name === 'br') {\r\n            text += '\\n'\r\n          } else {\r\n            // 块级标签前后加换行\r\n            const isBlock = node.name === 'p' || node.name === 'div' || node.name === 'tr' || node.name === 'li' || (node.name[0] === 'h' && node.name[1] > '0' && node.name[1] < '7')\r\n            if (isBlock && text && text[text.length - 1] !== '\\n') {\r\n              text += '\\n'\r\n            }\r\n            // 递归获取子节点的文本\r\n            if (node.children) {\r\n              traversal(node.children)\r\n            }\r\n            if (isBlock && text[text.length - 1] !== '\\n') {\r\n              text += '\\n'\r\n            } else if (node.name === 'td' || node.name === 'th') {\r\n              text += '\\t'\r\n            }\r\n          }\r\n        }\r\n      })(nodes || this.nodes)\r\n      return text\r\n    },\r\n\r\n    /**\r\n     * @description 获取内容大小和位置\r\n     * @return {Promise}\r\n     */\r\n    getRect () {\r\n      return new Promise((resolve, reject) => {\r\n        uni.createSelectorQuery()\r\n          // #ifndef MP-ALIPAY\r\n          .in(this)\r\n          // #endif\r\n          .select('#_root').boundingClientRect().exec(res => res[0] ? resolve(res[0]) : reject(Error('Root label not found')))\r\n      })\r\n    },\r\n\r\n    /**\r\n     * @description 暂停播放媒体\r\n     */\r\n    pauseMedia () {\r\n      for (let i = (this._videos || []).length; i--;) {\r\n        this._videos[i].pause()\r\n      }\r\n      // #ifdef APP-PLUS\r\n      const command = 'for(var e=document.getElementsByTagName(\"video\"),i=e.length;i--;)e[i].pause()'\r\n      // #ifndef APP-PLUS-NVUE\r\n      let page = this.$parent\r\n      while (!page.$scope) page = page.$parent\r\n      page.$scope.$getAppWebview().evalJS(command)\r\n      // #endif\r\n      // #ifdef APP-PLUS-NVUE\r\n      this.$refs.web.evalJs(command)\r\n      // #endif\r\n      // #endif\r\n    },\r\n\r\n    /**\r\n     * @description 设置媒体播放速率\r\n     * @param {Number} rate 播放速率\r\n     */\r\n    setPlaybackRate (rate) {\r\n      this.playbackRate = rate\r\n      for (let i = (this._videos || []).length; i--;) {\r\n        this._videos[i].playbackRate(rate)\r\n      }\r\n      // #ifdef APP-PLUS\r\n      const command = 'for(var e=document.getElementsByTagName(\"video\"),i=e.length;i--;)e[i].playbackRate=' + rate\r\n      // #ifndef APP-PLUS-NVUE\r\n      let page = this.$parent\r\n      while (!page.$scope) page = page.$parent\r\n      page.$scope.$getAppWebview().evalJS(command)\r\n      // #endif\r\n      // #ifdef APP-PLUS-NVUE\r\n      this.$refs.web.evalJs(command)\r\n      // #endif\r\n      // #endif\r\n    },\r\n\r\n    /**\r\n     * @description 设置内容\r\n     * @param {String} content html 内容\r\n     * @param {Boolean} append 是否在尾部追加\r\n     */\r\n    setContent (content, append) {\r\n      if (!append || !this.imgList) {\r\n        this.imgList = []\r\n      }\r\n      const nodes = new Parser(this).parse(content)\r\n      // #ifdef APP-PLUS-NVUE\r\n      if (this._ready) {\r\n        this._set(nodes, append)\r\n      }\r\n      // #endif\r\n      this.$set(this, 'nodes', append ? (this.nodes || []).concat(nodes) : nodes)\r\n\r\n      // #ifndef APP-PLUS-NVUE\r\n      this._videos = []\r\n      this.$nextTick(() => {\r\n        this._hook('onLoad')\r\n        this.$emit('load')\r\n      })\r\n\r\n      if (this.lazyLoad || this.imgList._unloadimgs < this.imgList.length / 2) {\r\n        // 设置懒加载，每 350ms 获取高度，不变则认为加载完毕\r\n        let height = 0\r\n        const callback = rect => {\r\n          if (!rect || !rect.height) rect = {}\r\n          // 350ms 总高度无变化就触发 ready 事件\r\n          if (rect.height === height) {\r\n            this.$emit('ready', rect)\r\n          } else {\r\n            height = rect.height\r\n            setTimeout(() => {\r\n              this.getRect().then(callback).catch(callback)\r\n            }, 350)\r\n          }\r\n        }\r\n        this.getRect().then(callback).catch(callback)\r\n      } else {\r\n        // 未设置懒加载，等待所有图片加载完毕\r\n        if (!this.imgList._unloadimgs) {\r\n          this.getRect().then(rect => {\r\n            this.$emit('ready', rect)\r\n          }).catch(() => {\r\n            this.$emit('ready', {})\r\n          })\r\n        }\r\n      }\r\n      // #endif\r\n    },\r\n\r\n    /**\r\n     * @description 调用插件钩子函数\r\n     */\r\n    _hook (name) {\r\n      for (let i = plugins.length; i--;) {\r\n        if (this.plugins[i][name]) {\r\n          this.plugins[i][name]()\r\n        }\r\n      }\r\n    },\r\n\r\n    // #ifdef APP-PLUS-NVUE\r\n    /**\r\n     * @description 设置内容\r\n     */\r\n    _set (nodes, append) {\r\n      this.$refs.web.evalJs('setContent(' + JSON.stringify(nodes).replace(/%22/g, '') + ',' + JSON.stringify([this.containerStyle.replace(/(?:margin|padding)[^;]+/g, ''), this.errorImg, this.loadingImg, this.pauseVideo, this.scrollTable, this.selectable]) + ',' + append + ')')\r\n    },\r\n\r\n    /**\r\n     * @description 接收到 web-view 消息\r\n     */\r\n    _onMessage (e) {\r\n      const message = e.detail.data[0]\r\n      switch (message.action) {\r\n        // web-view 初始化完毕\r\n        case 'onJSBridgeReady':\r\n          this._ready = true\r\n          if (this.nodes) {\r\n            this._set(this.nodes)\r\n          }\r\n          break\r\n        // 内容 dom 加载完毕\r\n        case 'onLoad':\r\n          this.height = message.height\r\n          this._hook('onLoad')\r\n          this.$emit('load')\r\n          break\r\n        // 所有图片加载完毕\r\n        case 'onReady':\r\n          this.getRect().then(res => {\r\n            this.$emit('ready', res)\r\n          }).catch(() => {\r\n            this.$emit('ready', {})\r\n          })\r\n          break\r\n        // 总高度发生变化\r\n        case 'onHeightChange':\r\n          this.height = message.height\r\n          break\r\n        // 图片点击\r\n        case 'onImgTap':\r\n          this.$emit('imgtap', message.attrs)\r\n          if (this.previewImg) {\r\n            uni.previewImage({\r\n              current: parseInt(message.attrs.i),\r\n              urls: this.imgList\r\n            })\r\n          }\r\n          break\r\n        // 链接点击\r\n        case 'onLinkTap': {\r\n          const href = message.attrs.href\r\n          this.$emit('linktap', message.attrs)\r\n          if (href) {\r\n            // 锚点跳转\r\n            if (href[0] === '#') {\r\n              if (this.useAnchor) {\r\n                dom.scrollToElement(this.$refs.web, {\r\n                  offset: message.offset\r\n                })\r\n              }\r\n            } else if (href.includes('://')) {\r\n              // 打开外链\r\n              if (this.copyLink) {\r\n                plus.runtime.openWeb(href)\r\n              }\r\n            } else {\r\n              uni.navigateTo({\r\n                url: href,\r\n                fail () {\r\n                  uni.switchTab({\r\n                    url: href\r\n                  })\r\n                }\r\n              })\r\n            }\r\n          }\r\n          break\r\n        }\r\n        case 'onPlay':\r\n          this.$emit('play')\r\n          break\r\n        // 获取到锚点的偏移量\r\n        case 'getOffset':\r\n          if (typeof message.offset === 'number') {\r\n            dom.scrollToElement(this.$refs.web, {\r\n              offset: message.offset + this._navigateTo.offset\r\n            })\r\n            this._navigateTo.resolve()\r\n          } else {\r\n            this._navigateTo.reject(Error('Label not found'))\r\n          }\r\n          break\r\n        // 点击\r\n        case 'onClick':\r\n          this.$emit('tap')\r\n          this.$emit('click')\r\n          break\r\n        // 出错\r\n        case 'onError':\r\n          this.$emit('error', {\r\n            source: message.source,\r\n            attrs: message.attrs\r\n          })\r\n      }\r\n    }\r\n    // #endif\r\n  }\r\n}\r\n</script>\r\n\r\n<style>\r\n/* #ifndef APP-PLUS-NVUE */\r\n/* 根节点样式 */\r\n._root {\r\n  padding: 1px 0;\r\n  overflow-x: auto;\r\n  overflow-y: hidden;\r\n  -webkit-overflow-scrolling: touch;\r\n}\r\n\r\n/* 长按复制 */\r\n._select {\r\n  user-select: text;\r\n}\r\n/* #endif */\r\n</style>\r\n"
  },
  {
    "path": "UniApp/uni_modules/mp-html/components/mp-html/node/node.vue",
    "content": "<template>\r\n  <view :id=\"attrs.id\" :class=\"'_block _'+name+' '+attrs.class\" :style=\"attrs.style\">\r\n    <block v-for=\"(n, i) in childs\" v-bind:key=\"i\">\r\n      <!-- 图片 -->\r\n      <!-- 占位图 -->\r\n      <image v-if=\"n.name==='img'&&!n.t&&((opts[1]&&!ctrl[i])||ctrl[i]<0)\" class=\"_img\" :style=\"n.attrs.style\" :src=\"ctrl[i]<0?opts[2]:opts[1]\" mode=\"widthFix\" />\r\n      <!-- 显示图片 -->\r\n      <!-- #ifdef H5 || (APP-PLUS && VUE2) -->\r\n      <img v-if=\"n.name==='img'\" :id=\"n.attrs.id\" :class=\"'_img '+n.attrs.class\" :style=\"(ctrl[i]===-1?'display:none;':'')+n.attrs.style\" :src=\"n.attrs.src||(ctrl.load?n.attrs['data-src']:'')\" :data-i=\"i\" @load=\"imgLoad\" @error=\"mediaError\" @tap.stop=\"imgTap\" @longpress=\"imgLongTap\" />\r\n      <!-- #endif -->\r\n      <!-- #ifndef H5 || (APP-PLUS && VUE2) -->\r\n      <!-- 表格中的图片，使用 rich-text 防止大小不正确 -->\r\n      <rich-text v-if=\"n.name==='img'&&n.t\" :style=\"'display:'+n.t\" :nodes=\"[{attrs:{style:n.attrs.style,src:n.attrs.src},name:'img'}]\" :data-i=\"i\" @tap.stop=\"imgTap\" />\r\n      <!-- #endif -->\r\n      <!-- #ifndef H5 || APP-PLUS -->\r\n      <image v-else-if=\"n.name==='img'\" :id=\"n.attrs.id\" :class=\"'_img '+n.attrs.class\" :style=\"(ctrl[i]===-1?'display:none;':'')+'width:'+(ctrl[i]||1)+'px;height:1px;'+n.attrs.style\" :src=\"n.attrs.src\" :mode=\"!n.h?'widthFix':(!n.w?'heightFix':'')\" :lazy-load=\"opts[0]\" :webp=\"n.webp\" :show-menu-by-longpress=\"opts[3]&&!n.attrs.ignore\" :image-menu-prevent=\"!opts[3]||n.attrs.ignore\" :data-i=\"i\" @load=\"imgLoad\" @error=\"mediaError\" @tap.stop=\"imgTap\" @longpress=\"imgLongTap\" />\r\n      <!-- #endif -->\r\n      <!-- #ifdef APP-PLUS && VUE3 -->\r\n      <image v-else-if=\"n.name==='img'\" :id=\"n.attrs.id\" :class=\"'_img '+n.attrs.class\" :style=\"(ctrl[i]===-1?'display:none;':'')+'width:'+(ctrl[i]||1)+'px;'+n.attrs.style\" :src=\"n.attrs.src||(ctrl.load?n.attrs['data-src']:'')\" :mode=\"!n.h?'widthFix':(!n.w?'heightFix':'')\" :data-i=\"i\" @load=\"imgLoad\" @error=\"mediaError\" @tap.stop=\"imgTap\" @longpress=\"imgLongTap\" />\r\n      <!-- #endif -->\r\n      <!-- 文本 -->\r\n      <!-- #ifdef MP-WEIXIN -->\r\n      <text v-else-if=\"n.text\" :user-select=\"opts[4]=='force'&&isiOS\" decode>{{n.text}}</text>\r\n      <!-- #endif -->\r\n      <!-- #ifndef MP-WEIXIN || MP-BAIDU || MP-ALIPAY || MP-TOUTIAO -->\r\n      <text v-else-if=\"n.text\" decode>{{n.text}}</text>\r\n      <!-- #endif -->\r\n      <text v-else-if=\"n.name==='br'\">\\n</text>\r\n      <!-- 链接 -->\r\n      <view v-else-if=\"n.name==='a'\" :id=\"n.attrs.id\" :class=\"(n.attrs.href?'_a ':'')+n.attrs.class\" hover-class=\"_hover\" :style=\"'display:inline;'+n.attrs.style\" :data-i=\"i\" @tap.stop=\"linkTap\">\r\n        <node name=\"span\" :childs=\"n.children\" :opts=\"opts\" style=\"display:inherit\" />\r\n      </view>\r\n      <!-- 视频 -->\r\n      <!-- #ifdef APP-PLUS -->\r\n      <view v-else-if=\"n.html\" :id=\"n.attrs.id\" :class=\"'_video '+n.attrs.class\" :style=\"n.attrs.style\" v-html=\"n.html\" @vplay.stop=\"play\" />\r\n      <!-- #endif -->\r\n      <!-- #ifndef APP-PLUS -->\r\n      <video v-else-if=\"n.name==='video'\" :id=\"n.attrs.id\" :class=\"n.attrs.class\" :style=\"n.attrs.style\" :autoplay=\"n.attrs.autoplay\" :controls=\"n.attrs.controls\" :loop=\"n.attrs.loop\" :muted=\"n.attrs.muted\" :object-fit=\"n.attrs['object-fit']\" :poster=\"n.attrs.poster\" :src=\"n.src[ctrl[i]||0]\" :data-i=\"i\" @play=\"play\" @error=\"mediaError\" />\r\n      <!-- #endif -->\r\n      <!-- #ifdef H5 || APP-PLUS -->\r\n      <iframe v-else-if=\"n.name==='iframe'\" :style=\"n.attrs.style\" :allowfullscreen=\"n.attrs.allowfullscreen\" :frameborder=\"n.attrs.frameborder\" :src=\"n.attrs.src\" />\r\n      <embed v-else-if=\"n.name==='embed'\" :style=\"n.attrs.style\" :src=\"n.attrs.src\" />\r\n      <!-- #endif -->\r\n      <!-- #ifndef MP-TOUTIAO || ((H5 || APP-PLUS) && VUE3) -->\r\n      <!-- 音频 -->\r\n      <audio v-else-if=\"n.name==='audio'\" :id=\"n.attrs.id\" :class=\"n.attrs.class\" :style=\"n.attrs.style\" :author=\"n.attrs.author\" :controls=\"n.attrs.controls\" :loop=\"n.attrs.loop\" :name=\"n.attrs.name\" :poster=\"n.attrs.poster\" :src=\"n.src[ctrl[i]||0]\" :data-i=\"i\" @play=\"play\" @error=\"mediaError\" />\r\n      <!-- #endif -->\r\n      <view v-else-if=\"(n.name==='table'&&n.c)||n.name==='li'\" :id=\"n.attrs.id\" :class=\"'_'+n.name+' '+n.attrs.class\" :style=\"n.attrs.style\">\r\n        <node v-if=\"n.name==='li'\" :childs=\"n.children\" :opts=\"opts\" />\r\n        <view v-else v-for=\"(tbody, x) in n.children\" v-bind:key=\"x\" :class=\"'_'+tbody.name+' '+tbody.attrs.class\" :style=\"tbody.attrs.style\">\r\n          <node v-if=\"tbody.name==='td'||tbody.name==='th'\" :childs=\"tbody.children\" :opts=\"opts\" />\r\n          <block v-else v-for=\"(tr, y) in tbody.children\" v-bind:key=\"y\">\r\n            <view v-if=\"tr.name==='td'||tr.name==='th'\" :class=\"'_'+tr.name+' '+tr.attrs.class\" :style=\"tr.attrs.style\">\r\n              <node :childs=\"tr.children\" :opts=\"opts\" />\r\n            </view>\r\n            <view v-else :class=\"'_'+tr.name+' '+tr.attrs.class\" :style=\"tr.attrs.style\">\r\n              <view v-for=\"(td, z) in tr.children\" v-bind:key=\"z\" :class=\"'_'+td.name+' '+td.attrs.class\" :style=\"td.attrs.style\">\r\n                <node :childs=\"td.children\" :opts=\"opts\" />\r\n              </view>\r\n            </view>\r\n          </block>\r\n        </view>\r\n      </view>\r\n      \r\n      <!-- 富文本 -->\r\n      <!-- #ifdef H5 || ((MP-WEIXIN || MP-QQ || APP-PLUS || MP-360) && VUE2) -->\r\n      <rich-text v-else-if=\"!n.c&&!handler.isInline(n.name, n.attrs.style)\" :id=\"n.attrs.id\" :style=\"n.f\" :user-select=\"opts[4]\" :nodes=\"[n]\" />\r\n      <!-- #endif -->\r\n      <!-- #ifndef H5 || ((MP-WEIXIN || MP-QQ || APP-PLUS || MP-360) && VUE2) -->\r\n      <rich-text v-else-if=\"!n.c\" :id=\"n.attrs.id\" :style=\"'display:inline;'+n.f\" :preview=\"false\" :selectable=\"opts[4]\" :user-select=\"opts[4]\" :nodes=\"[n]\" />\r\n      <!-- #endif -->\r\n      <!-- 继续递归 -->\r\n      <view v-else-if=\"n.c===2\" :id=\"n.attrs.id\" :class=\"'_block _'+n.name+' '+n.attrs.class\" :style=\"n.f+';'+n.attrs.style\">\r\n        <node v-for=\"(n2, j) in n.children\" v-bind:key=\"j\" :style=\"n2.f\" :name=\"n2.name\" :attrs=\"n2.attrs\" :childs=\"n2.children\" :opts=\"opts\" />\r\n      </view>\r\n      <node v-else :style=\"n.f\" :name=\"n.name\" :attrs=\"n.attrs\" :childs=\"n.children\" :opts=\"opts\" />\r\n    </block>\r\n  </view>\r\n</template>\r\n<script module=\"handler\" lang=\"wxs\">\r\n// 行内标签列表\r\nvar inlineTags = {\r\n  abbr: true,\r\n  b: true,\r\n  big: true,\r\n  code: true,\r\n  del: true,\r\n  em: true,\r\n  i: true,\r\n  ins: true,\r\n  label: true,\r\n  q: true,\r\n  small: true,\r\n  span: true,\r\n  strong: true,\r\n  sub: true,\r\n  sup: true\r\n}\r\n/**\r\n * @description 判断是否为行内标签\r\n */\r\nmodule.exports = {\r\n  isInline: function (tagName, style) {\r\n    return inlineTags[tagName] || (style || '').indexOf('display:inline') !== -1\r\n  }\r\n}\r\n</script>\r\n<script>\n\r\nimport node from './node'\r\nexport default {\r\n  name: 'node',\r\n  options: {\r\n    // #ifdef MP-WEIXIN\r\n    virtualHost: true,\r\n    // #endif\r\n    // #ifdef MP-TOUTIAO\r\n    addGlobalClass: false\r\n    // #endif\r\n  },\r\n  data () {\r\n    return {\r\n      ctrl: {},\r\n      // #ifdef MP-WEIXIN\r\n      isiOS: uni.getSystemInfoSync().system.includes('iOS')\r\n      // #endif\r\n    }\r\n  },\r\n  props: {\r\n    name: String,\r\n    attrs: {\r\n      type: Object,\r\n      default () {\r\n        return {}\r\n      }\r\n    },\r\n    childs: Array,\r\n    opts: Array\r\n  },\r\n  components: {\n\r\n    // #ifndef (H5 || APP-PLUS) && VUE3\r\n    node\r\n    // #endif\r\n  },\r\n  mounted () {\r\n    this.$nextTick(() => {\r\n      for (this.root = this.$parent; this.root.$options.name !== 'mp-html'; this.root = this.root.$parent);\r\n    })\r\n    // #ifdef H5 || APP-PLUS\r\n    if (this.opts[0]) {\r\n      let i\r\n      for (i = this.childs.length; i--;) {\r\n        if (this.childs[i].name === 'img') break\r\n      }\r\n      if (i !== -1) {\r\n        this.observer = uni.createIntersectionObserver(this).relativeToViewport({\r\n          top: 500,\r\n          bottom: 500\r\n        })\r\n        this.observer.observe('._img', res => {\r\n          if (res.intersectionRatio) {\r\n            this.$set(this.ctrl, 'load', 1)\r\n            this.observer.disconnect()\r\n          }\r\n        })\r\n      }\r\n    }\r\n    // #endif\r\n  },\r\n  beforeDestroy () {\r\n    // #ifdef H5 || APP-PLUS\r\n    if (this.observer) {\r\n      this.observer.disconnect()\r\n    }\r\n    // #endif\r\n  },\r\n  methods:{\r\n    // #ifdef MP-WEIXIN\r\n    toJSON () { return this },\r\n    // #endif\r\n    /**\r\n     * @description 播放视频事件\r\n     * @param {Event} e\r\n     */\r\n    play (e) {\r\n      this.root.$emit('play')\r\n      // #ifndef APP-PLUS\r\n      if (this.root.pauseVideo) {\r\n        let flag = false\r\n        const id = e.target.id\r\n        for (let i = this.root._videos.length; i--;) {\r\n          if (this.root._videos[i].id === id) {\r\n            flag = true\r\n          } else {\r\n            this.root._videos[i].pause() // 自动暂停其他视频\r\n          }\r\n        }\r\n        // 将自己加入列表\r\n        if (!flag) {\r\n          const ctx = uni.createVideoContext(id\r\n            // #ifndef MP-BAIDU\r\n            , this\r\n            // #endif\r\n          )\r\n          ctx.id = id\r\n          if (this.root.playbackRate) {\r\n            ctx.playbackRate(this.root.playbackRate)\r\n          }\r\n          this.root._videos.push(ctx)\r\n        }\r\n      }\r\n      // #endif\r\n    },\r\n\r\n    /**\r\n     * @description 图片点击事件\r\n     * @param {Event} e\r\n     */\r\n    imgTap (e) {\r\n      const node = this.childs[e.currentTarget.dataset.i]\r\n      if (node.a) {\r\n        this.linkTap(node.a)\r\n        return\r\n      }\r\n      if (node.attrs.ignore) return\r\n      // #ifdef H5 || APP-PLUS\r\n      node.attrs.src = node.attrs.src || node.attrs['data-src']\r\n      // #endif\r\n      this.root.$emit('imgtap', node.attrs)\r\n      // 自动预览图片\r\n      if (this.root.previewImg) {\r\n        uni.previewImage({\r\n          // #ifdef MP-WEIXIN\r\n          showmenu: this.root.showImgMenu,\r\n          // #endif\r\n          // #ifdef MP-ALIPAY\r\n          enablesavephoto: this.root.showImgMenu,\r\n          enableShowPhotoDownload: this.root.showImgMenu,\r\n          // #endif\r\n          current: parseInt(node.attrs.i),\r\n          urls: this.root.imgList\r\n        })\r\n      }\r\n    },\r\n\r\n    /**\r\n     * @description 图片长按\r\n     */\r\n    imgLongTap (e) {\r\n      // #ifdef APP-PLUS\r\n      const attrs = this.childs[e.currentTarget.dataset.i].attrs\r\n      if (this.opts[3] && !attrs.ignore) {\r\n        uni.showActionSheet({\r\n          itemList: ['保存图片'],\r\n          success: () => {\r\n            const save = path => {\r\n              uni.saveImageToPhotosAlbum({\r\n                filePath: path,\r\n                success () {\r\n                  uni.showToast({\r\n                    title: '保存成功'\r\n                  })\r\n                }\r\n              })\r\n            }\r\n            if (this.root.imgList[attrs.i].startsWith('http')) {\r\n              uni.downloadFile({\r\n                url: this.root.imgList[attrs.i],\r\n                success: res => save(res.tempFilePath)\r\n              })\r\n            } else {\r\n              save(this.root.imgList[attrs.i])\r\n            }\r\n          }\r\n        })\r\n      }\r\n      // #endif\r\n    },\r\n\r\n    /**\r\n     * @description 图片加载完成事件\r\n     * @param {Event} e\r\n     */\r\n    imgLoad (e) {\r\n      const i = e.currentTarget.dataset.i\r\n      /* #ifndef H5 || (APP-PLUS && VUE2) */\r\n      if (!this.childs[i].w) {\r\n        // 设置原宽度\r\n        this.$set(this.ctrl, i, e.detail.width)\r\n      } else /* #endif */ if ((this.opts[1] && !this.ctrl[i]) || this.ctrl[i] === -1) {\r\n        // 加载完毕，取消加载中占位图\r\n        this.$set(this.ctrl, i, 1)\r\n      }\r\n      this.checkReady()\r\n    },\r\n\r\n    /**\r\n     * @description 检查是否所有图片加载完毕\r\n     */\r\n    checkReady () {\r\n      if (this.root && !this.root.lazyLoad) {\r\n        this.root._unloadimgs -= 1\r\n        if (!this.root._unloadimgs) {\r\n          setTimeout(() => {\r\n            this.root.getRect().then(rect => {\r\n              this.root.$emit('ready', rect)\r\n            }).catch(() => {\r\n              this.root.$emit('ready', {})\r\n            })\r\n          }, 350)\r\n        }\r\n      }\r\n    },\r\n\r\n    /**\r\n     * @description 链接点击事件\r\n     * @param {Event} e\r\n     */\r\n    linkTap (e) {\r\n      const node = e.currentTarget ? this.childs[e.currentTarget.dataset.i] : {}\r\n      const attrs = node.attrs || e\r\n      const href = attrs.href\r\n      this.root.$emit('linktap', Object.assign({\r\n        innerText: this.root.getText(node.children || []) // 链接内的文本内容\r\n      }, attrs))\r\n      if (href) {\r\n        if (href[0] === '#') {\r\n          // 跳转锚点\r\n          this.root.navigateTo(href.substring(1)).catch(() => { })\r\n        } else if (href.split('?')[0].includes('://')) {\r\n          // 复制外部链接\r\n          if (this.root.copyLink) {\r\n            // #ifdef H5\r\n            window.open(href)\r\n            // #endif\r\n            // #ifdef MP\r\n            uni.setClipboardData({\r\n              data: href,\r\n              success: () =>\r\n                uni.showToast({\r\n                  title: '链接已复制'\r\n                })\r\n            })\r\n            // #endif\r\n            // #ifdef APP-PLUS\r\n            plus.runtime.openWeb(href)\r\n            // #endif\r\n          }\r\n        } else {\r\n          // 跳转页面\r\n          uni.navigateTo({\r\n            url: href,\r\n            fail () {\r\n              uni.switchTab({\r\n                url: href,\r\n                fail () { }\r\n              })\r\n            }\r\n          })\r\n        }\r\n      }\r\n    },\r\n\r\n    /**\r\n     * @description 错误事件\r\n     * @param {Event} e\r\n     */\r\n    mediaError (e) {\r\n      const i = e.currentTarget.dataset.i\r\n      const node = this.childs[i]\r\n      // 加载其他源\r\n      if (node.name === 'video' || node.name === 'audio') {\r\n        let index = (this.ctrl[i] || 0) + 1\r\n        if (index > node.src.length) {\r\n          index = 0\r\n        }\r\n        if (index < node.src.length) {\r\n          this.$set(this.ctrl, i, index)\r\n          return\r\n        }\r\n      } else if (node.name === 'img') {\r\n        // #ifdef H5 && VUE3\r\n        if (this.opts[0] && !this.ctrl.load) return\r\n        // #endif\r\n        // 显示错误占位图\r\n        if (this.opts[2]) {\r\n          this.$set(this.ctrl, i, -1)\r\n        }\r\n        this.checkReady()\r\n      }\r\n      if (this.root) {\r\n        this.root.$emit('error', {\r\n          source: node.name,\r\n          attrs: node.attrs,\r\n          // #ifndef H5 && VUE3\r\n          errMsg: e.detail.errMsg\r\n          // #endif\r\n        })\r\n      }\r\n    }\r\n  }\r\n}\r\n</script>\r\n<style>/deep/ .hl-code,/deep/ .hl-pre{color:#ccc;background:0 0;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}/deep/ .hl-pre{padding:1em;margin:.5em 0;overflow:auto}/deep/ .hl-pre{background:#2d2d2d}/deep/ .hl-block-comment,/deep/ .hl-cdata,/deep/ .hl-comment,/deep/ .hl-doctype,/deep/ .hl-prolog{color:#999}/deep/ .hl-punctuation{color:#ccc}/deep/ .hl-attr-name,/deep/ .hl-deleted,/deep/ .hl-namespace,/deep/ .hl-tag{color:#e2777a}/deep/ .hl-function-name{color:#6196cc}/deep/ .hl-boolean,/deep/ .hl-function,/deep/ .hl-number{color:#f08d49}/deep/ .hl-class-name,/deep/ .hl-constant,/deep/ .hl-property,/deep/ .hl-symbol{color:#f8c555}/deep/ .hl-atrule,/deep/ .hl-builtin,/deep/ .hl-important,/deep/ .hl-keyword,/deep/ .hl-selector{color:#cc99cd}/deep/ .hl-attr-value,/deep/ .hl-char,/deep/ .hl-regex,/deep/ .hl-string,/deep/ .hl-variable{color:#7ec699}/deep/ .hl-entity,/deep/ .hl-operator,/deep/ .hl-url{color:#67cdcc}/deep/ .hl-bold,/deep/ .hl-important{font-weight:700}/deep/ .hl-italic{font-style:italic}/deep/ .hl-entity{cursor:help}/deep/ .hl-inserted{color:green}/deep/ .md-p {\n  margin-block-start: 1em;\n  margin-block-end: 1em;\n}\n\n/deep/ .md-table,\n/deep/ .md-blockquote {\n  margin-bottom: 16px;\n}\n\n/deep/ .md-table {\n  box-sizing: border-box;\n  width: 100%;\n  overflow: auto;\n  border-spacing: 0;\n  border-collapse: collapse;\n}\n\n/deep/ .md-tr {\n  background-color: #fff;\n  border-top: 1px solid #c6cbd1;\n}\n\n.md-table .md-tr:nth-child(2n) {\n  background-color: #f6f8fa;\n}\n\n/deep/ .md-th,\n/deep/ .md-td {\n  padding: 6px 13px !important;\n  border: 1px solid #dfe2e5;\n}\n\n/deep/ .md-th {\n  font-weight: 600;\n}\n\n/deep/ .md-blockquote {\n  padding: 0 1em;\n  color: #6a737d;\n  border-left: 0.25em solid #dfe2e5;\n}\n\n/deep/ .md-code {\n  padding: 0.2em 0.4em;\n  font-family: SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace;\n  font-size: 85%;\n  background-color: rgba(27, 31, 35, 0.05);\n  border-radius: 3px;\n}\n\n/deep/ .md-pre .md-code {\n  padding: 0;\n  font-size: 100%;\n  background: transparent;\n  border: 0;\n}\r\n/* a 标签默认效果 */\r\n._a {\r\n  padding: 1.5px 0 1.5px 0;\r\n  color: #366092;\r\n  word-break: break-all;\r\n}\r\n\r\n/* a 标签点击态效果 */\r\n._hover {\r\n  text-decoration: underline;\r\n  opacity: 0.7;\r\n}\r\n\r\n/* 图片默认效果 */\r\n._img {\r\n  max-width: 100%;\r\n  -webkit-touch-callout: none;\r\n}\r\n\r\n/* 内部样式 */\r\n\r\n._block {\r\n  display: block;\r\n}\r\n\r\n._b,\r\n._strong {\r\n  font-weight: bold;\r\n}\r\n\r\n._code {\r\n  font-family: monospace;\r\n}\r\n\r\n._del {\r\n  text-decoration: line-through;\r\n}\r\n\r\n._em,\r\n._i {\r\n  font-style: italic;\r\n}\r\n\r\n._h1 {\r\n  font-size: 2em;\r\n}\r\n\r\n._h2 {\r\n  font-size: 1.5em;\r\n}\r\n\r\n._h3 {\r\n  font-size: 1.17em;\r\n}\r\n\r\n._h5 {\r\n  font-size: 0.83em;\r\n}\r\n\r\n._h6 {\r\n  font-size: 0.67em;\r\n}\r\n\r\n._h1,\r\n._h2,\r\n._h3,\r\n._h4,\r\n._h5,\r\n._h6 {\r\n  display: block;\r\n  font-weight: bold;\r\n}\r\n\r\n._image {\r\n  height: 1px;\r\n}\r\n\r\n._ins {\r\n  text-decoration: underline;\r\n}\r\n\r\n._li {\r\n  display: list-item;\r\n}\r\n\r\n._ol {\r\n  list-style-type: decimal;\r\n}\r\n\r\n._ol,\r\n._ul {\r\n  display: block;\r\n  padding-left: 40px;\r\n  margin: 1em 0;\r\n}\r\n\r\n._q::before {\r\n  content: '\"';\r\n}\r\n\r\n._q::after {\r\n  content: '\"';\r\n}\r\n\r\n._sub {\r\n  font-size: smaller;\r\n  vertical-align: sub;\r\n}\r\n\r\n._sup {\r\n  font-size: smaller;\r\n  vertical-align: super;\r\n}\r\n\r\n._thead,\r\n._tbody,\r\n._tfoot {\r\n  display: table-row-group;\r\n}\r\n\r\n._tr {\r\n  display: table-row;\r\n}\r\n\r\n._td,\r\n._th {\r\n  display: table-cell;\r\n  vertical-align: middle;\r\n}\r\n\r\n._th {\r\n  font-weight: bold;\r\n  text-align: center;\r\n}\r\n\r\n._ul {\r\n  list-style-type: disc;\r\n}\r\n\r\n._ul ._ul {\r\n  margin: 0;\r\n  list-style-type: circle;\r\n}\r\n\r\n._ul ._ul ._ul {\r\n  list-style-type: square;\r\n}\r\n\r\n._abbr,\r\n._b,\r\n._code,\r\n._del,\r\n._em,\r\n._i,\r\n._ins,\r\n._label,\r\n._q,\r\n._span,\r\n._strong,\r\n._sub,\r\n._sup {\r\n  display: inline;\r\n}\r\n\r\n/* #ifdef APP-PLUS */\r\n._video {\r\n  width: 300px;\r\n  height: 225px;\r\n}\r\n/* #endif */\r\n</style>"
  },
  {
    "path": "UniApp/uni_modules/mp-html/components/mp-html/parser.js",
    "content": "/**\r\n * @fileoverview html 解析器\r\n */\r\n\r\n// 配置\r\nconst config = {\r\n  // 信任的标签（保持标签名不变）\r\n  trustTags: makeMap('a,abbr,ad,audio,b,blockquote,br,code,col,colgroup,dd,del,dl,dt,div,em,fieldset,h1,h2,h3,h4,h5,h6,hr,i,img,ins,label,legend,li,ol,p,q,ruby,rt,source,span,strong,sub,sup,table,tbody,td,tfoot,th,thead,tr,title,ul,video'),\r\n\r\n  // 块级标签（转为 div，其他的非信任标签转为 span）\r\n  blockTags: makeMap('address,article,aside,body,caption,center,cite,footer,header,html,nav,pre,section'),\r\n\r\n  // #ifdef (MP-WEIXIN || MP-QQ || APP-PLUS || MP-360) && VUE3\r\n  // 行内标签\r\n  inlineTags: makeMap('abbr,b,big,code,del,em,i,ins,label,q,small,span,strong,sub,sup'),\r\n  // #endif\r\n\r\n  // 要移除的标签\r\n  ignoreTags: makeMap('area,base,canvas,embed,frame,head,iframe,input,link,map,meta,param,rp,script,source,style,textarea,title,track,wbr'),\r\n\r\n  // 自闭合的标签\r\n  voidTags: makeMap('area,base,br,col,circle,ellipse,embed,frame,hr,img,input,line,link,meta,param,path,polygon,rect,source,track,use,wbr'),\r\n\r\n  // html 实体\r\n  entities: {\r\n    lt: '<',\r\n    gt: '>',\r\n    quot: '\"',\r\n    apos: \"'\",\r\n    ensp: '\\u2002',\r\n    emsp: '\\u2003',\r\n    nbsp: '\\xA0',\r\n    semi: ';',\r\n    ndash: '–',\r\n    mdash: '—',\r\n    middot: '·',\r\n    lsquo: '‘',\r\n    rsquo: '’',\r\n    ldquo: '“',\r\n    rdquo: '”',\r\n    bull: '•',\r\n    hellip: '…',\r\n    larr: '←',\r\n    uarr: '↑',\r\n    rarr: '→',\r\n    darr: '↓'\r\n  },\r\n\r\n  // 默认的标签样式\r\n  tagStyle: {\r\n    // #ifndef APP-PLUS-NVUE\r\n    address: 'font-style:italic',\r\n    big: 'display:inline;font-size:1.2em',\r\n    caption: 'display:table-caption;text-align:center',\r\n    center: 'text-align:center',\r\n    cite: 'font-style:italic',\r\n    dd: 'margin-left:40px',\r\n    mark: 'background-color:yellow',\r\n    pre: 'font-family:monospace;white-space:pre',\r\n    s: 'text-decoration:line-through',\r\n    small: 'display:inline;font-size:0.8em',\r\n    strike: 'text-decoration:line-through',\r\n    u: 'text-decoration:underline'\r\n    // #endif\r\n  },\r\n\r\n  // svg 大小写对照表\r\n  svgDict: {\r\n    animatetransform: 'animateTransform',\r\n    lineargradient: 'linearGradient',\r\n    viewbox: 'viewBox',\r\n    attributename: 'attributeName',\r\n    repeatcount: 'repeatCount',\r\n    repeatdur: 'repeatDur'\r\n  }\r\n}\r\nconst tagSelector={}\r\nconst {\r\n  windowWidth,\r\n  // #ifdef MP-WEIXIN\r\n  system\r\n  // #endif\r\n} = uni.getSystemInfoSync()\r\nconst blankChar = makeMap(' ,\\r,\\n,\\t,\\f')\r\nlet idIndex = 0\r\n\r\n// #ifdef H5 || APP-PLUS\r\nconfig.ignoreTags.iframe = undefined\r\nconfig.trustTags.iframe = true\r\nconfig.ignoreTags.embed = undefined\r\nconfig.trustTags.embed = true\r\n// #endif\r\n// #ifdef APP-PLUS-NVUE\r\nconfig.ignoreTags.source = undefined\r\nconfig.ignoreTags.style = undefined\r\n// #endif\r\n\r\n/**\r\n * @description 创建 map\r\n * @param {String} str 逗号分隔\r\n */\r\nfunction makeMap (str) {\r\n  const map = Object.create(null)\r\n  const list = str.split(',')\r\n  for (let i = list.length; i--;) {\r\n    map[list[i]] = true\r\n  }\r\n  return map\r\n}\r\n\r\n/**\r\n * @description 解码 html 实体\r\n * @param {String} str 要解码的字符串\r\n * @param {Boolean} amp 要不要解码 &amp;\r\n * @returns {String} 解码后的字符串\r\n */\r\nfunction decodeEntity (str, amp) {\r\n  let i = str.indexOf('&')\r\n  while (i !== -1) {\r\n    const j = str.indexOf(';', i + 3)\r\n    let code\r\n    if (j === -1) break\r\n    if (str[i + 1] === '#') {\r\n      // &#123; 形式的实体\r\n      code = parseInt((str[i + 2] === 'x' ? '0' : '') + str.substring(i + 2, j))\r\n      if (!isNaN(code)) {\r\n        str = str.substr(0, i) + String.fromCharCode(code) + str.substr(j + 1)\r\n      }\r\n    } else {\r\n      // &nbsp; 形式的实体\r\n      code = str.substring(i + 1, j)\r\n      if (config.entities[code] || (code === 'amp' && amp)) {\r\n        str = str.substr(0, i) + (config.entities[code] || '&') + str.substr(j + 1)\r\n      }\r\n    }\r\n    i = str.indexOf('&', i + 1)\r\n  }\r\n  return str\r\n}\r\n\r\n/**\r\n * @description 合并多个块级标签，加快长内容渲染\r\n * @param {Array} nodes 要合并的标签数组\r\n */\r\nfunction mergeNodes (nodes) {\r\n  let i = nodes.length - 1\r\n  for (let j = i; j >= -1; j--) {\r\n    if (j === -1 || nodes[j].c || !nodes[j].name || (nodes[j].name !== 'div' && nodes[j].name !== 'p' && nodes[j].name[0] !== 'h') || (nodes[j].attrs.style || '').includes('inline')) {\r\n      if (i - j >= 5) {\r\n        nodes.splice(j + 1, i - j, {\r\n          name: 'div',\r\n          attrs: {},\r\n          children: nodes.slice(j + 1, i + 1)\r\n        })\r\n      }\r\n      i = j - 1\r\n    }\r\n  }\r\n}\r\n\r\n/**\r\n * @description html 解析器\r\n * @param {Object} vm 组件实例\r\n */\r\nfunction Parser (vm) {\r\n  this.options = vm || {}\r\n  this.tagStyle = Object.assign({}, config.tagStyle, this.options.tagStyle)\r\n  this.imgList = vm.imgList || []\r\n  this.imgList._unloadimgs = 0\r\n  this.plugins = vm.plugins || []\r\n  this.attrs = Object.create(null)\r\n  this.stack = []\r\n  this.nodes = []\r\n  this.pre = (this.options.containerStyle || '').includes('white-space') && this.options.containerStyle.includes('pre') ? 2 : 0\r\n}\r\n\r\n/**\r\n * @description 执行解析\r\n * @param {String} content 要解析的文本\r\n */\r\nParser.prototype.parse = function (content) {\r\n  // 插件处理\r\n  for (let i = this.plugins.length; i--;) {\r\n    if (this.plugins[i].onUpdate) {\r\n      content = this.plugins[i].onUpdate(content, config) || content\r\n    }\r\n  }\r\n\r\n  new Lexer(this).parse(content)\r\n  // 出栈未闭合的标签\r\n  while (this.stack.length) {\r\n    this.popNode()\r\n  }\r\n  if (this.nodes.length > 50) {\r\n    mergeNodes(this.nodes)\r\n  }\r\n  return this.nodes\r\n}\r\n\r\n/**\r\n * @description 将标签暴露出来（不被 rich-text 包含）\r\n */\r\nParser.prototype.expose = function () {\r\n  // #ifndef APP-PLUS-NVUE\r\n  for (let i = this.stack.length; i--;) {\r\n    const item = this.stack[i]\r\n    if (item.c || item.name === 'a' || item.name === 'video' || item.name === 'audio') return\r\n    item.c = 1\r\n  }\r\n  // #endif\r\n}\r\n\r\n/**\r\n * @description 处理插件\r\n * @param {Object} node 要处理的标签\r\n * @returns {Boolean} 是否要移除此标签\r\n */\r\nParser.prototype.hook = function (node) {\r\n  for (let i = this.plugins.length; i--;) {\r\n    if (this.plugins[i].onParse && this.plugins[i].onParse(node, this) === false) {\r\n      return false\r\n    }\r\n  }\r\n  return true\r\n}\r\n\r\n/**\r\n * @description 将链接拼接上主域名\r\n * @param {String} url 需要拼接的链接\r\n * @returns {String} 拼接后的链接\r\n */\r\nParser.prototype.getUrl = function (url) {\r\n  const domain = this.options.domain\r\n  if (url[0] === '/') {\r\n    if (url[1] === '/') {\r\n      // // 开头的补充协议名\r\n      url = (domain ? domain.split('://')[0] : 'http') + ':' + url\r\n    } else if (domain) {\r\n      // 否则补充整个域名\r\n      url = domain + url\r\n    } /* #ifdef APP-PLUS */ else {\r\n      url = plus.io.convertLocalFileSystemURL(url)\r\n    } /* #endif */\r\n  } else if (!url.includes('data:') && !url.includes('://')) {\r\n    if (domain) {\r\n      url = domain + '/' + url\r\n    } /* #ifdef APP-PLUS */ else {\r\n      url = plus.io.convertLocalFileSystemURL(url)\r\n    } /* #endif */\r\n  }\r\n  return url\r\n}\r\n\r\n/**\r\n * @description 解析样式表\r\n * @param {Object} node 标签\r\n * @returns {Object}\r\n */\r\nParser.prototype.parseStyle = function (node) {\r\n  const attrs = node.attrs\r\n  const list = (this.tagStyle[node.name] || '').split(';').concat((attrs.style || '').split(';'))\r\n  const styleObj = {}\r\n  let tmp = ''\r\n\r\n  if (attrs.id && !this.xml) {\r\n    // 暴露锚点\r\n    if (this.options.useAnchor) {\r\n      this.expose()\r\n    } else if (node.name !== 'img' && node.name !== 'a' && node.name !== 'video' && node.name !== 'audio') {\r\n      attrs.id = undefined\r\n    }\r\n  }\r\n\r\n  // 转换 width 和 height 属性\r\n  if (attrs.width) {\r\n    styleObj.width = parseFloat(attrs.width) + (attrs.width.includes('%') ? '%' : 'px')\r\n    attrs.width = undefined\r\n  }\r\n  if (attrs.height) {\r\n    styleObj.height = parseFloat(attrs.height) + (attrs.height.includes('%') ? '%' : 'px')\r\n    attrs.height = undefined\r\n  }\r\n\r\n  for (let i = 0, len = list.length; i < len; i++) {\r\n    const info = list[i].split(':')\r\n    if (info.length < 2) continue\r\n    const key = info.shift().trim().toLowerCase()\r\n    let value = info.join(':').trim()\r\n    if ((value[0] === '-' && value.lastIndexOf('-') > 0) || value.includes('safe')) {\r\n      // 兼容性的 css 不压缩\r\n      tmp += `;${key}:${value}`\r\n    } else if (!styleObj[key] || value.includes('import') || !styleObj[key].includes('import')) {\r\n      // 重复的样式进行覆盖\r\n      if (value.includes('url')) {\r\n        // 填充链接\r\n        let j = value.indexOf('(') + 1\r\n        if (j) {\r\n          while (value[j] === '\"' || value[j] === \"'\" || blankChar[value[j]]) {\r\n            j++\r\n          }\r\n          value = value.substr(0, j) + this.getUrl(value.substr(j))\r\n        }\r\n      } else if (value.includes('rpx')) {\r\n        // 转换 rpx（rich-text 内部不支持 rpx）\r\n        value = value.replace(/[0-9.]+\\s*rpx/g, $ => parseFloat($) * windowWidth / 750 + 'px')\r\n      }\r\n      styleObj[key] = value\r\n    }\r\n  }\r\n\r\n  node.attrs.style = tmp\r\n  return styleObj\r\n}\r\n\r\n/**\r\n * @description 解析到标签名\r\n * @param {String} name 标签名\r\n * @private\r\n */\r\nParser.prototype.onTagName = function (name) {\r\n  this.tagName = this.xml ? name : name.toLowerCase()\r\n  if (this.tagName === 'svg') {\r\n    this.xml = (this.xml || 0) + 1 // svg 标签内大小写敏感\r\n    config.ignoreTags.style = undefined // svg 标签内 style 可用\r\n  }\r\n}\r\n\r\n/**\r\n * @description 解析到属性名\r\n * @param {String} name 属性名\r\n * @private\r\n */\r\nParser.prototype.onAttrName = function (name) {\r\n  name = this.xml ? name : name.toLowerCase()\r\n  if (name.substr(0, 5) === 'data-') {\r\n    if (name === 'data-src' && !this.attrs.src) {\r\n      // data-src 自动转为 src\r\n      this.attrName = 'src'\r\n    } else if (this.tagName === 'img' || this.tagName === 'a') {\r\n      // a 和 img 标签保留 data- 的属性，可以在 imgtap 和 linktap 事件中使用\r\n      this.attrName = name\r\n    } else {\r\n      // 剩余的移除以减小大小\r\n      this.attrName = undefined\r\n    }\r\n  } else {\r\n    this.attrName = name\r\n    this.attrs[name] = 'T' // boolean 型属性缺省设置\r\n  }\r\n}\r\n\r\n/**\r\n * @description 解析到属性值\r\n * @param {String} val 属性值\r\n * @private\r\n */\r\nParser.prototype.onAttrVal = function (val) {\r\n  const name = this.attrName || ''\r\n  if (name === 'style' || name === 'href') {\r\n    // 部分属性进行实体解码\r\n    this.attrs[name] = decodeEntity(val, true)\r\n  } else if (name.includes('src')) {\r\n    // 拼接主域名\r\n    this.attrs[name] = this.getUrl(decodeEntity(val, true))\r\n  } else if (name) {\r\n    this.attrs[name] = val\r\n  }\r\n}\r\n\r\n/**\r\n * @description 解析到标签开始\r\n * @param {Boolean} selfClose 是否有自闭合标识 />\r\n * @private\r\n */\r\nParser.prototype.onOpenTag = function (selfClose) {\r\n  // 拼装 node\r\n  const node = Object.create(null)\r\n  node.name = this.tagName\r\n  node.attrs = this.attrs\r\n  // 避免因为自动 diff 使得 type 被设置为 null 导致部分内容不显示\r\n  if (this.options.nodes.length) {\r\n    node.type = 'node'\r\n  }\r\n  this.attrs = Object.create(null)\r\n\r\n  const attrs = node.attrs\r\n  const parent = this.stack[this.stack.length - 1]\r\n  const siblings = parent ? parent.children : this.nodes\r\n  const close = this.xml ? selfClose : config.voidTags[node.name]\r\n\r\n  // 替换标签名选择器\r\n  if (tagSelector[node.name]) {\r\n    attrs.class = tagSelector[node.name] + (attrs.class ? ' ' + attrs.class : '')\r\n  }\r\n\r\n  // 转换 embed 标签\r\n  if (node.name === 'embed') {\r\n    // #ifndef H5 || APP-PLUS\r\n    const src = attrs.src || ''\r\n    // 按照后缀名和 type 将 embed 转为 video 或 audio\r\n    if (src.includes('.mp4') || src.includes('.3gp') || src.includes('.m3u8') || (attrs.type || '').includes('video')) {\r\n      node.name = 'video'\r\n    } else if (src.includes('.mp3') || src.includes('.wav') || src.includes('.aac') || src.includes('.m4a') || (attrs.type || '').includes('audio')) {\r\n      node.name = 'audio'\r\n    }\r\n    if (attrs.autostart) {\r\n      attrs.autoplay = 'T'\r\n    }\r\n    attrs.controls = 'T'\r\n    // #endif\r\n    // #ifdef H5 || APP-PLUS\r\n    this.expose()\r\n    // #endif\r\n  }\r\n\r\n  // #ifndef APP-PLUS-NVUE\r\n  // 处理音视频\r\n  if (node.name === 'video' || node.name === 'audio') {\r\n    // 设置 id 以便获取 context\r\n    if (node.name === 'video' && !attrs.id) {\r\n      attrs.id = 'v' + idIndex++\r\n    }\r\n    // 没有设置 controls 也没有设置 autoplay 的自动设置 controls\r\n    if (!attrs.controls && !attrs.autoplay) {\r\n      attrs.controls = 'T'\r\n    }\r\n    // 用数组存储所有可用的 source\r\n    node.src = []\r\n    if (attrs.src) {\r\n      node.src.push(attrs.src)\r\n      attrs.src = undefined\r\n    }\r\n    this.expose()\r\n  }\r\n  // #endif\r\n\r\n  // 处理自闭合标签\r\n  if (close) {\r\n    if (!this.hook(node) || config.ignoreTags[node.name]) {\r\n      // 通过 base 标签设置主域名\r\n      if (node.name === 'base' && !this.options.domain) {\r\n        this.options.domain = attrs.href\r\n      } /* #ifndef APP-PLUS-NVUE */ else if (node.name === 'source' && parent && (parent.name === 'video' || parent.name === 'audio') && attrs.src) {\r\n        // 设置 source 标签（仅父节点为 video 或 audio 时有效）\r\n        parent.src.push(attrs.src)\r\n      } /* #endif */\r\n      return\r\n    }\r\n\r\n    // 解析 style\r\n    const styleObj = this.parseStyle(node)\r\n\r\n    // 处理图片\r\n    if (node.name === 'img') {\r\n      if (attrs.src) {\r\n        // 标记 webp\r\n        if (attrs.src.includes('webp')) {\r\n          node.webp = 'T'\r\n        }\r\n        // data url 图片如果没有设置 original-src 默认为不可预览的小图片\r\n        if (attrs.src.includes('data:') && !attrs['original-src']) {\r\n          attrs.ignore = 'T'\r\n        }\r\n        if (!attrs.ignore || node.webp || attrs.src.includes('cloud://')) {\r\n          for (let i = this.stack.length; i--;) {\r\n            const item = this.stack[i]\r\n            if (item.name === 'a') {\r\n              node.a = item.attrs\r\n            }\r\n            if (item.name === 'table' && !node.webp && !attrs.src.includes('cloud://')) {\r\n              if (!styleObj.display || styleObj.display.includes('inline')) {\r\n                node.t = 'inline-block'\r\n              } else {\r\n                node.t = styleObj.display\r\n              }\r\n              styleObj.display = undefined\r\n            }\r\n            // #ifndef H5 || APP-PLUS\r\n            const style = item.attrs.style || ''\r\n            if (style.includes('flex:') && !style.includes('flex:0') && !style.includes('flex: 0') && (!styleObj.width || parseInt(styleObj.width) > 100)) {\r\n              styleObj.width = '100% !important'\r\n              styleObj.height = ''\r\n              for (let j = i + 1; j < this.stack.length; j++) {\r\n                this.stack[j].attrs.style = (this.stack[j].attrs.style || '').replace('inline-', '')\r\n              }\r\n            } else if (style.includes('flex') && styleObj.width === '100%') {\r\n              for (let j = i + 1; j < this.stack.length; j++) {\r\n                const style = this.stack[j].attrs.style || ''\r\n                if (!style.includes(';width') && !style.includes(' width') && style.indexOf('width') !== 0) {\r\n                  styleObj.width = ''\r\n                  break\r\n                }\r\n              }\r\n            } else if (style.includes('inline-block')) {\r\n              if (styleObj.width && styleObj.width[styleObj.width.length - 1] === '%') {\r\n                item.attrs.style += ';max-width:' + styleObj.width\r\n                styleObj.width = ''\r\n              } else {\r\n                item.attrs.style += ';max-width:100%'\r\n              }\r\n            }\r\n            // #endif\r\n            item.c = 1\r\n          }\r\n          attrs.i = this.imgList.length.toString()\r\n          let src = attrs['original-src'] || attrs.src\r\n          // #ifndef H5 || MP-ALIPAY || APP-PLUS || MP-360\r\n          if (this.imgList.includes(src)) {\r\n            // 如果有重复的链接则对域名进行随机大小写变换避免预览时错位\r\n            let i = src.indexOf('://')\r\n            if (i !== -1) {\r\n              i += 3\r\n              let newSrc = src.substr(0, i)\r\n              for (; i < src.length; i++) {\r\n                if (src[i] === '/') break\r\n                newSrc += Math.random() > 0.5 ? src[i].toUpperCase() : src[i]\r\n              }\r\n              newSrc += src.substr(i)\r\n              src = newSrc\r\n            }\r\n          }\r\n          // #endif\r\n          this.imgList.push(src)\r\n          if (!node.t) {\r\n            this.imgList._unloadimgs += 1\r\n          }\r\n          // #ifdef H5 || APP-PLUS\r\n          if (this.options.lazyLoad) {\r\n            attrs['data-src'] = attrs.src\r\n            attrs.src = undefined\r\n          }\r\n          // #endif\r\n        }\r\n      }\r\n      if (styleObj.display === 'inline') {\r\n        styleObj.display = ''\r\n      }\r\n      // #ifndef APP-PLUS-NVUE\r\n      if (attrs.ignore) {\r\n        styleObj['max-width'] = styleObj['max-width'] || '100%'\r\n        attrs.style += ';-webkit-touch-callout:none'\r\n      }\r\n      // #endif\r\n      // 设置的宽度超出屏幕，为避免变形，高度转为自动\r\n      if (parseInt(styleObj.width) > windowWidth) {\r\n        styleObj.height = undefined\r\n      }\r\n      // 记录是否设置了宽高\r\n      if (!isNaN(parseInt(styleObj.width))) {\r\n        node.w = 'T'\r\n      }\r\n      if (!isNaN(parseInt(styleObj.height)) && (!styleObj.height.includes('%') || (parent && (parent.attrs.style || '').includes('height')))) {\r\n        node.h = 'T'\r\n      }\r\n    } else if (node.name === 'svg') {\r\n      siblings.push(node)\r\n      this.stack.push(node)\r\n      this.popNode()\r\n      return\r\n    }\r\n    for (const key in styleObj) {\r\n      if (styleObj[key]) {\r\n        attrs.style += `;${key}:${styleObj[key].replace(' !important', '')}`\r\n      }\r\n    }\r\n    attrs.style = attrs.style.substr(1) || undefined\r\n    // #ifdef (MP-WEIXIN || MP-QQ) && VUE3\r\n    if (!attrs.style) {\r\n      delete attrs.style\r\n    }\r\n    // #endif\r\n  } else {\r\n    if ((node.name === 'pre' || ((attrs.style || '').includes('white-space') && attrs.style.includes('pre'))) && this.pre !== 2) {\r\n      this.pre = node.pre = 1\r\n    }\r\n    node.children = []\r\n    this.stack.push(node)\r\n  }\r\n\r\n  // 加入节点树\r\n  siblings.push(node)\r\n}\r\n\r\n/**\r\n * @description 解析到标签结束\r\n * @param {String} name 标签名\r\n * @private\r\n */\r\nParser.prototype.onCloseTag = function (name) {\r\n  // 依次出栈到匹配为止\r\n  name = this.xml ? name : name.toLowerCase()\r\n  let i\r\n  for (i = this.stack.length; i--;) {\r\n    if (this.stack[i].name === name) break\r\n  }\r\n  if (i !== -1) {\r\n    while (this.stack.length > i) {\r\n      this.popNode()\r\n    }\r\n  } else if (name === 'p' || name === 'br') {\r\n    const siblings = this.stack.length ? this.stack[this.stack.length - 1].children : this.nodes\r\n    siblings.push({\r\n      name,\r\n      attrs: {\r\n        class: tagSelector[name] || '',\r\n        style: this.tagStyle[name] || ''\r\n      }\r\n    })\r\n  }\r\n}\r\n\r\n/**\r\n * @description 处理标签出栈\r\n * @private\r\n */\r\nParser.prototype.popNode = function () {\r\n  const node = this.stack.pop()\r\n  let attrs = node.attrs\r\n  const children = node.children\r\n  const parent = this.stack[this.stack.length - 1]\r\n  const siblings = parent ? parent.children : this.nodes\r\n\r\n  if (!this.hook(node) || config.ignoreTags[node.name]) {\r\n    // 获取标题\r\n    if (node.name === 'title' && children.length && children[0].type === 'text' && this.options.setTitle) {\r\n      uni.setNavigationBarTitle({\r\n        title: children[0].text\r\n      })\r\n    }\r\n    siblings.pop()\r\n    return\r\n  }\r\n\r\n  if (node.pre && this.pre !== 2) {\r\n    // 是否合并空白符标识\r\n    this.pre = node.pre = undefined\r\n    for (let i = this.stack.length; i--;) {\r\n      if (this.stack[i].pre) {\r\n        this.pre = 1\r\n      }\r\n    }\r\n  }\r\n\r\n  const styleObj = {}\r\n\r\n  // 转换 svg\r\n  if (node.name === 'svg') {\r\n    if (this.xml > 1) {\r\n      // 多层 svg 嵌套\r\n      this.xml--\r\n      return\r\n    }\r\n    // #ifdef APP-PLUS-NVUE\r\n    (function traversal (node) {\r\n      if (node.name) {\r\n        // 调整 svg 的大小写\r\n        node.name = config.svgDict[node.name] || node.name\r\n        for (const item in node.attrs) {\r\n          if (config.svgDict[item]) {\r\n            node.attrs[config.svgDict[item]] = node.attrs[item]\r\n            node.attrs[item] = undefined\r\n          }\r\n        }\r\n        for (let i = 0; i < (node.children || []).length; i++) {\r\n          traversal(node.children[i])\r\n        }\r\n      }\r\n    })(node)\r\n    // #endif\r\n    // #ifndef APP-PLUS-NVUE\r\n    let src = ''\r\n    const style = attrs.style\r\n    attrs.style = ''\r\n    attrs.xmlns = 'http://www.w3.org/2000/svg';\r\n    (function traversal (node) {\r\n      if (node.type === 'text') {\r\n        src += node.text\r\n        return\r\n      }\r\n      const name = config.svgDict[node.name] || node.name\r\n      src += '<' + name\r\n      for (const item in node.attrs) {\r\n        const val = node.attrs[item]\r\n        if (val) {\r\n          src += ` ${config.svgDict[item] || item}=\"${val}\"`\r\n        }\r\n      }\r\n      if (!node.children) {\r\n        src += '/>'\r\n      } else {\r\n        src += '>'\r\n        for (let i = 0; i < node.children.length; i++) {\r\n          traversal(node.children[i])\r\n        }\r\n        src += '</' + name + '>'\r\n      }\r\n    })(node)\r\n    node.name = 'img'\r\n    node.attrs = {\r\n      src: 'data:image/svg+xml;utf8,' + src.replace(/#/g, '%23'),\r\n      style,\r\n      ignore: 'T'\r\n    }\r\n    node.children = undefined\r\n    // #endif\r\n    this.xml = false\r\n    config.ignoreTags.style = true\r\n    return\r\n  }\r\n\r\n  // #ifndef APP-PLUS-NVUE\r\n  // 转换 align 属性\r\n  if (attrs.align) {\r\n    if (node.name === 'table') {\r\n      if (attrs.align === 'center') {\r\n        styleObj['margin-inline-start'] = styleObj['margin-inline-end'] = 'auto'\r\n      } else {\r\n        styleObj.float = attrs.align\r\n      }\r\n    } else {\r\n      styleObj['text-align'] = attrs.align\r\n    }\r\n    attrs.align = undefined\r\n  }\r\n\r\n  // 转换 dir 属性\r\n  if (attrs.dir) {\r\n    styleObj.direction = attrs.dir\r\n    attrs.dir = undefined\r\n  }\r\n\r\n  // 转换 font 标签的属性\r\n  if (node.name === 'font') {\r\n    if (attrs.color) {\r\n      styleObj.color = attrs.color\r\n      attrs.color = undefined\r\n    }\r\n    if (attrs.face) {\r\n      styleObj['font-family'] = attrs.face\r\n      attrs.face = undefined\r\n    }\r\n    if (attrs.size) {\r\n      let size = parseInt(attrs.size)\r\n      if (!isNaN(size)) {\r\n        if (size < 1) {\r\n          size = 1\r\n        } else if (size > 7) {\r\n          size = 7\r\n        }\r\n        styleObj['font-size'] = ['x-small', 'small', 'medium', 'large', 'x-large', 'xx-large', 'xxx-large'][size - 1]\r\n      }\r\n      attrs.size = undefined\r\n    }\r\n  }\r\n  // #endif\r\n\r\n  // 一些编辑器的自带 class\r\n  if ((attrs.class || '').includes('align-center')) {\r\n    styleObj['text-align'] = 'center'\r\n  }\r\n\r\n  Object.assign(styleObj, this.parseStyle(node))\r\n\r\n  if (node.name !== 'table' && parseInt(styleObj.width) > windowWidth) {\r\n    styleObj['max-width'] = '100%'\r\n    styleObj['box-sizing'] = 'border-box'\r\n  }\r\n\r\n  // #ifndef APP-PLUS-NVUE\r\n  if (config.blockTags[node.name]) {\r\n    node.name = 'div'\r\n  } else if (!config.trustTags[node.name] && !this.xml) {\r\n    // 未知标签转为 span，避免无法显示\r\n    node.name = 'span'\r\n  }\r\n\r\n  if (node.name === 'a' || node.name === 'ad'\r\n    // #ifdef H5 || APP-PLUS\r\n    || node.name === 'iframe' // eslint-disable-line\r\n    // #endif\r\n  ) {\r\n    this.expose()\r\n  } else if (node.name === 'video') {\r\n    if ((styleObj.height || '').includes('auto')) {\r\n      styleObj.height = undefined\r\n    }\r\n    /* #ifdef APP-PLUS */\r\n    let str = '<video style=\"width:100%;height:100%\"'\r\n    for (const item in attrs) {\r\n      if (attrs[item]) {\r\n        str += ' ' + item + '=\"' + attrs[item] + '\"'\r\n      }\r\n    }\r\n    if (this.options.pauseVideo) {\r\n      str += ' onplay=\"this.dispatchEvent(new CustomEvent(\\'vplay\\',{bubbles:!0}));for(var e=document.getElementsByTagName(\\'video\\'),t=0;t<e.length;t++)e[t]!=this&&e[t].pause()\"'\r\n    }\r\n    str += '>'\r\n    for (let i = 0; i < node.src.length; i++) {\r\n      str += '<source src=\"' + node.src[i] + '\">'\r\n    }\r\n    str += '</video>'\r\n    node.html = str\r\n    /* #endif */\r\n  } else if ((node.name === 'ul' || node.name === 'ol') && node.c) {\r\n    // 列表处理\r\n    const types = {\r\n      a: 'lower-alpha',\r\n      A: 'upper-alpha',\r\n      i: 'lower-roman',\r\n      I: 'upper-roman'\r\n    }\r\n    if (types[attrs.type]) {\r\n      attrs.style += ';list-style-type:' + types[attrs.type]\r\n      attrs.type = undefined\r\n    }\r\n    for (let i = children.length; i--;) {\r\n      if (children[i].name === 'li') {\r\n        children[i].c = 1\r\n      }\r\n    }\r\n  } else if (node.name === 'table') {\r\n    // 表格处理\r\n    // cellpadding、cellspacing、border 这几个常用表格属性需要通过转换实现\r\n    let padding = parseFloat(attrs.cellpadding)\r\n    let spacing = parseFloat(attrs.cellspacing)\r\n    const border = parseFloat(attrs.border)\r\n    const bordercolor = styleObj['border-color']\r\n    const borderstyle = styleObj['border-style']\r\n    if (node.c) {\r\n      // padding 和 spacing 默认 2\r\n      if (isNaN(padding)) {\r\n        padding = 2\r\n      }\r\n      if (isNaN(spacing)) {\r\n        spacing = 2\r\n      }\r\n    }\r\n    if (border) {\r\n      attrs.style += `;border:${border}px ${borderstyle || 'solid'} ${bordercolor || 'gray'}`\r\n    }\r\n    if (node.flag && node.c) {\r\n      // 有 colspan 或 rowspan 且含有链接的表格通过 grid 布局实现\r\n      styleObj.display = 'grid'\r\n      if (spacing) {\r\n        styleObj['grid-gap'] = spacing + 'px'\r\n        styleObj.padding = spacing + 'px'\r\n      } else if (border) {\r\n        // 无间隔的情况下避免边框重叠\r\n        attrs.style += ';border-left:0;border-top:0'\r\n      }\r\n\r\n      const width = [] // 表格的列宽\r\n      const trList = [] // tr 列表\r\n      const cells = [] // 保存新的单元格\r\n      const map = {}; // 被合并单元格占用的格子\r\n\r\n      (function traversal (nodes) {\r\n        for (let i = 0; i < nodes.length; i++) {\r\n          if (nodes[i].name === 'tr') {\r\n            trList.push(nodes[i])\r\n          } else {\r\n            traversal(nodes[i].children || [])\r\n          }\r\n        }\r\n      })(children)\r\n\r\n      for (let row = 1; row <= trList.length; row++) {\r\n        let col = 1\r\n        for (let j = 0; j < trList[row - 1].children.length; j++) {\r\n          const td = trList[row - 1].children[j]\r\n          if (td.name === 'td' || td.name === 'th') {\r\n            // 这个格子被上面的单元格占用，则列号++\r\n            while (map[row + '.' + col]) {\r\n              col++\r\n            }\r\n            let style = td.attrs.style || ''\r\n            let start = style.indexOf('width') ? style.indexOf(';width') : 0\r\n            // 提取出 td 的宽度\r\n            if (start !== -1) {\r\n              let end = style.indexOf(';', start + 6)\r\n              if (end === -1) {\r\n                end = style.length\r\n              }\r\n              if (!td.attrs.colspan) {\r\n                width[col] = style.substring(start ? start + 7 : 6, end)\r\n              }\r\n              style = style.substr(0, start) + style.substr(end)\r\n            }\r\n            // 设置竖直对齐\r\n            style += ';display:flex'\r\n            start = style.indexOf('vertical-align')\r\n            if (start !== -1) {\r\n              const val = style.substr(start + 15, 10)\r\n              if (val.includes('middle')) {\r\n                style += ';align-items:center'\r\n              } else if (val.includes('bottom')) {\r\n                style += ';align-items:flex-end'\r\n              }\r\n            } else {\r\n              style += ';align-items:center'\r\n            }\r\n            // 设置水平对齐\r\n            start = style.indexOf('text-align')\r\n            if (start !== -1) {\r\n              const val = style.substr(start + 11, 10)\r\n              if (val.includes('center')) {\r\n                style += ';justify-content: center'\r\n              } else if (val.includes('right')) {\r\n                style += ';justify-content: right'\r\n              }\r\n            }\r\n            style = (border ? `;border:${border}px ${borderstyle || 'solid'} ${bordercolor || 'gray'}` + (spacing ? '' : ';border-right:0;border-bottom:0') : '') + (padding ? `;padding:${padding}px` : '') + ';' + style\r\n            // 处理列合并\r\n            if (td.attrs.colspan) {\r\n              style += `;grid-column-start:${col};grid-column-end:${col + parseInt(td.attrs.colspan)}`\r\n              if (!td.attrs.rowspan) {\r\n                style += `;grid-row-start:${row};grid-row-end:${row + 1}`\r\n              }\r\n              col += parseInt(td.attrs.colspan) - 1\r\n            }\r\n            // 处理行合并\r\n            if (td.attrs.rowspan) {\r\n              style += `;grid-row-start:${row};grid-row-end:${row + parseInt(td.attrs.rowspan)}`\r\n              if (!td.attrs.colspan) {\r\n                style += `;grid-column-start:${col};grid-column-end:${col + 1}`\r\n              }\r\n              // 记录下方单元格被占用\r\n              for (let rowspan = 1; rowspan < td.attrs.rowspan; rowspan++) {\r\n                for (let colspan = 0; colspan < (td.attrs.colspan || 1); colspan++) {\r\n                  map[(row + rowspan) + '.' + (col - colspan)] = 1\r\n                }\r\n              }\r\n            }\r\n            if (style) {\r\n              td.attrs.style = style\r\n            }\r\n            cells.push(td)\r\n            col++\r\n          }\r\n        }\r\n        if (row === 1) {\r\n          let temp = ''\r\n          for (let i = 1; i < col; i++) {\r\n            temp += (width[i] ? width[i] : 'auto') + ' '\r\n          }\r\n          styleObj['grid-template-columns'] = temp\r\n        }\r\n      }\r\n      node.children = cells\r\n    } else {\r\n      // 没有使用合并单元格的表格通过 table 布局实现\r\n      if (node.c) {\r\n        styleObj.display = 'table'\r\n      }\r\n      if (!isNaN(spacing)) {\r\n        styleObj['border-spacing'] = spacing + 'px'\r\n      }\r\n      if (border || padding) {\r\n        // 遍历\r\n        (function traversal (nodes) {\r\n          for (let i = 0; i < nodes.length; i++) {\r\n            const td = nodes[i]\r\n            if (td.name === 'th' || td.name === 'td') {\r\n              if (border) {\r\n                td.attrs.style = `border:${border}px ${borderstyle || 'solid'} ${bordercolor || 'gray'};${td.attrs.style || ''}`\r\n              }\r\n              if (padding) {\r\n                td.attrs.style = `padding:${padding}px;${td.attrs.style || ''}`\r\n              }\r\n            } else if (td.children) {\r\n              traversal(td.children)\r\n            }\r\n          }\r\n        })(children)\r\n      }\r\n    }\r\n    // 给表格添加一个单独的横向滚动层\r\n    if (this.options.scrollTable && !(attrs.style || '').includes('inline')) {\r\n      const table = Object.assign({}, node)\r\n      node.name = 'div'\r\n      node.attrs = {\r\n        style: 'overflow:auto'\r\n      }\r\n      node.children = [table]\r\n      attrs = table.attrs\r\n    }\r\n  } else if ((node.name === 'td' || node.name === 'th') && (attrs.colspan || attrs.rowspan)) {\r\n    for (let i = this.stack.length; i--;) {\r\n      if (this.stack[i].name === 'table') {\r\n        this.stack[i].flag = 1 // 指示含有合并单元格\r\n        break\r\n      }\r\n    }\r\n  } else if (node.name === 'ruby') {\r\n    // 转换 ruby\r\n    node.name = 'span'\r\n    for (let i = 0; i < children.length - 1; i++) {\r\n      if (children[i].type === 'text' && children[i + 1].name === 'rt') {\r\n        children[i] = {\r\n          name: 'div',\r\n          attrs: {\r\n            style: 'display:inline-block;text-align:center'\r\n          },\r\n          children: [{\r\n            name: 'div',\r\n            attrs: {\r\n              style: 'font-size:50%;' + (children[i + 1].attrs.style || '')\r\n            },\r\n            children: children[i + 1].children\r\n          }, children[i]]\r\n        }\r\n        children.splice(i + 1, 1)\r\n      }\r\n    }\r\n  } else if (node.c) {\r\n    (function traversal (node) {\r\n      node.c = 2\r\n      for (let i = node.children.length; i--;) {\r\n        const child = node.children[i]\r\n        // #ifdef (MP-WEIXIN || MP-QQ || APP-PLUS || MP-360) && VUE3\r\n        if (child.name && (config.inlineTags[child.name] || ((child.attrs.style || '').includes('inline') && child.children)) && !child.c) {\r\n          traversal(child)\r\n        }\r\n        // #endif\r\n        if (!child.c || child.name === 'table') {\r\n          node.c = 1\r\n        }\r\n      }\r\n    })(node)\r\n  }\r\n\r\n  if ((styleObj.display || '').includes('flex') && !node.c) {\r\n    for (let i = children.length; i--;) {\r\n      const item = children[i]\r\n      if (item.f) {\r\n        item.attrs.style = (item.attrs.style || '') + item.f\r\n        item.f = undefined\r\n      }\r\n    }\r\n  }\r\n  // flex 布局时部分样式需要提取到 rich-text 外层\r\n  const flex = parent && ((parent.attrs.style || '').includes('flex') || (parent.attrs.style || '').includes('grid'))\r\n    // #ifdef MP-WEIXIN\r\n    // 检查基础库版本 virtualHost 是否可用\r\n    && !(node.c && wx.getNFCAdapter) // eslint-disable-line\r\n    // #endif\r\n    // #ifndef MP-WEIXIN || MP-QQ || MP-BAIDU || MP-TOUTIAO\r\n    && !node.c // eslint-disable-line\r\n  // #endif\r\n  if (flex) {\r\n    node.f = ';max-width:100%'\r\n  }\r\n\r\n  if (children.length >= 50 && node.c && !(styleObj.display || '').includes('flex')) {\r\n    mergeNodes(children)\r\n  }\r\n  // #endif\r\n\r\n  for (const key in styleObj) {\r\n    if (styleObj[key]) {\r\n      const val = `;${key}:${styleObj[key].replace(' !important', '')}`\r\n      /* #ifndef APP-PLUS-NVUE */\r\n      if (flex && ((key.includes('flex') && key !== 'flex-direction') || key === 'align-self' || key.includes('grid') || styleObj[key][0] === '-' || (key.includes('width') && val.includes('%')))) {\r\n        node.f += val\r\n        if (key === 'width') {\r\n          attrs.style += ';width:100%'\r\n        }\r\n      } else /* #endif */ {\r\n        attrs.style += val\r\n      }\r\n    }\r\n  }\r\n  attrs.style = attrs.style.substr(1) || undefined\r\n  // #ifdef (MP-WEIXIN || MP-QQ) && VUE3\r\n  for (const key in attrs) {\r\n    if (!attrs[key]) {\r\n      delete attrs[key]\r\n    }\r\n  }\r\n  // #endif\r\n}\r\n\r\n/**\r\n * @description 解析到文本\r\n * @param {String} text 文本内容\r\n */\r\nParser.prototype.onText = function (text) {\r\n  if (!this.pre) {\r\n    // 合并空白符\r\n    let trim = ''\r\n    let flag\r\n    for (let i = 0, len = text.length; i < len; i++) {\r\n      if (!blankChar[text[i]]) {\r\n        trim += text[i]\r\n      } else {\r\n        if (trim[trim.length - 1] !== ' ') {\r\n          trim += ' '\r\n        }\r\n        if (text[i] === '\\n' && !flag) {\r\n          flag = true\r\n        }\r\n      }\r\n    }\r\n    // 去除含有换行符的空串\r\n    if (trim === ' ') {\r\n      if (flag) return\r\n      // #ifdef VUE3\r\n      else {\r\n        const parent = this.stack[this.stack.length - 1]\r\n        if (parent && parent.name[0] === 't') return\r\n      }\r\n      // #endif\r\n    }\r\n    text = trim\r\n  }\r\n  const node = Object.create(null)\r\n  node.type = 'text'\r\n  // #ifdef (MP-BAIDU || MP-ALIPAY || MP-TOUTIAO) && VUE3\r\n  node.attrs = {}\r\n  // #endif\r\n  node.text = decodeEntity(text)\r\n  if (this.hook(node)) {\r\n    // #ifdef MP-WEIXIN\r\n    if (this.options.selectable === 'force' && system.includes('iOS') && !uni.canIUse('rich-text.user-select')) {\r\n      this.expose()\r\n    }\r\n    // #endif\r\n    const siblings = this.stack.length ? this.stack[this.stack.length - 1].children : this.nodes\r\n    siblings.push(node)\r\n  }\r\n}\r\n\r\n/**\r\n * @description html 词法分析器\r\n * @param {Object} handler 高层处理器\r\n */\r\nfunction Lexer (handler) {\r\n  this.handler = handler\r\n}\r\n\r\n/**\r\n * @description 执行解析\r\n * @param {String} content 要解析的文本\r\n */\r\nLexer.prototype.parse = function (content) {\r\n  this.content = content || ''\r\n  this.i = 0 // 标记解析位置\r\n  this.start = 0 // 标记一个单词的开始位置\r\n  this.state = this.text // 当前状态\r\n  for (let len = this.content.length; this.i !== -1 && this.i < len;) {\r\n    this.state()\r\n  }\r\n}\r\n\r\n/**\r\n * @description 检查标签是否闭合\r\n * @param {String} method 如果闭合要进行的操作\r\n * @returns {Boolean} 是否闭合\r\n * @private\r\n */\r\nLexer.prototype.checkClose = function (method) {\r\n  const selfClose = this.content[this.i] === '/'\r\n  if (this.content[this.i] === '>' || (selfClose && this.content[this.i + 1] === '>')) {\r\n    if (method) {\r\n      this.handler[method](this.content.substring(this.start, this.i))\r\n    }\r\n    this.i += selfClose ? 2 : 1\r\n    this.start = this.i\r\n    this.handler.onOpenTag(selfClose)\r\n    if (this.handler.tagName === 'script') {\r\n      this.i = this.content.indexOf('</', this.i)\r\n      if (this.i !== -1) {\r\n        this.i += 2\r\n        this.start = this.i\r\n      }\r\n      this.state = this.endTag\r\n    } else {\r\n      this.state = this.text\r\n    }\r\n    return true\r\n  }\r\n  return false\r\n}\r\n\r\n/**\r\n * @description 文本状态\r\n * @private\r\n */\r\nLexer.prototype.text = function () {\r\n  this.i = this.content.indexOf('<', this.i) // 查找最近的标签\r\n  if (this.i === -1) {\r\n    // 没有标签了\r\n    if (this.start < this.content.length) {\r\n      this.handler.onText(this.content.substring(this.start, this.content.length))\r\n    }\r\n    return\r\n  }\r\n  const c = this.content[this.i + 1]\r\n  if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) {\r\n    // 标签开头\r\n    if (this.start !== this.i) {\r\n      this.handler.onText(this.content.substring(this.start, this.i))\r\n    }\r\n    this.start = ++this.i\r\n    this.state = this.tagName\r\n  } else if (c === '/' || c === '!' || c === '?') {\r\n    if (this.start !== this.i) {\r\n      this.handler.onText(this.content.substring(this.start, this.i))\r\n    }\r\n    const next = this.content[this.i + 2]\r\n    if (c === '/' && ((next >= 'a' && next <= 'z') || (next >= 'A' && next <= 'Z'))) {\r\n      // 标签结尾\r\n      this.i += 2\r\n      this.start = this.i\r\n      this.state = this.endTag\r\n      return\r\n    }\r\n    // 处理注释\r\n    let end = '-->'\r\n    if (c !== '!' || this.content[this.i + 2] !== '-' || this.content[this.i + 3] !== '-') {\r\n      end = '>'\r\n    }\r\n    this.i = this.content.indexOf(end, this.i)\r\n    if (this.i !== -1) {\r\n      this.i += end.length\r\n      this.start = this.i\r\n    }\r\n  } else {\r\n    this.i++\r\n  }\r\n}\r\n\r\n/**\r\n * @description 标签名状态\r\n * @private\r\n */\r\nLexer.prototype.tagName = function () {\r\n  if (blankChar[this.content[this.i]]) {\r\n    // 解析到标签名\r\n    this.handler.onTagName(this.content.substring(this.start, this.i))\r\n    while (blankChar[this.content[++this.i]]);\r\n    if (this.i < this.content.length && !this.checkClose()) {\r\n      this.start = this.i\r\n      this.state = this.attrName\r\n    }\r\n  } else if (!this.checkClose('onTagName')) {\r\n    this.i++\r\n  }\r\n}\r\n\r\n/**\r\n * @description 属性名状态\r\n * @private\r\n */\r\nLexer.prototype.attrName = function () {\r\n  let c = this.content[this.i]\r\n  if (blankChar[c] || c === '=') {\r\n    // 解析到属性名\r\n    this.handler.onAttrName(this.content.substring(this.start, this.i))\r\n    let needVal = c === '='\r\n    const len = this.content.length\r\n    while (++this.i < len) {\r\n      c = this.content[this.i]\r\n      if (!blankChar[c]) {\r\n        if (this.checkClose()) return\r\n        if (needVal) {\r\n          // 等号后遇到第一个非空字符\r\n          this.start = this.i\r\n          this.state = this.attrVal\r\n          return\r\n        }\r\n        if (this.content[this.i] === '=') {\r\n          needVal = true\r\n        } else {\r\n          this.start = this.i\r\n          this.state = this.attrName\r\n          return\r\n        }\r\n      }\r\n    }\r\n  } else if (!this.checkClose('onAttrName')) {\r\n    this.i++\r\n  }\r\n}\r\n\r\n/**\r\n * @description 属性值状态\r\n * @private\r\n */\r\nLexer.prototype.attrVal = function () {\r\n  const c = this.content[this.i]\r\n  const len = this.content.length\r\n  if (c === '\"' || c === \"'\") {\r\n    // 有冒号的属性\r\n    this.start = ++this.i\r\n    this.i = this.content.indexOf(c, this.i)\r\n    if (this.i === -1) return\r\n    this.handler.onAttrVal(this.content.substring(this.start, this.i))\r\n  } else {\r\n    // 没有冒号的属性\r\n    for (; this.i < len; this.i++) {\r\n      if (blankChar[this.content[this.i]]) {\r\n        this.handler.onAttrVal(this.content.substring(this.start, this.i))\r\n        break\r\n      } else if (this.checkClose('onAttrVal')) return\r\n    }\r\n  }\r\n  while (blankChar[this.content[++this.i]]);\r\n  if (this.i < len && !this.checkClose()) {\r\n    this.start = this.i\r\n    this.state = this.attrName\r\n  }\r\n}\r\n\r\n/**\r\n * @description 结束标签状态\r\n * @returns {String} 结束的标签名\r\n * @private\r\n */\r\nLexer.prototype.endTag = function () {\r\n  const c = this.content[this.i]\r\n  if (blankChar[c] || c === '>' || c === '/') {\r\n    this.handler.onCloseTag(this.content.substring(this.start, this.i))\r\n    if (c !== '>') {\r\n      this.i = this.content.indexOf('>', this.i)\r\n      if (this.i === -1) return\r\n    }\r\n    this.start = ++this.i\r\n    this.state = this.text\r\n  } else {\r\n    this.i++\r\n  }\r\n}\r\n\r\nexport default Parser\r\n"
  },
  {
    "path": "UniApp/uni_modules/mp-html/package.json",
    "content": "{\r\n    \"id\": \"mp-html\",\r\n    \"displayName\": \"mp-html 富文本组件【全端支持，支持编辑、latex等扩展】\",\r\n    \"version\": \"v2.4.2\",\r\n    \"description\": \"一个强大的富文本组件，高效轻量，功能丰富\",\r\n    \"keywords\": [\r\n        \"富文本\",\r\n        \"编辑器\",\r\n        \"html\",\r\n        \"rich-text\",\r\n        \"editor\"\r\n    ],\r\n    \"repository\": \"https://github.com/jin-yufeng/mp-html\",\r\n    \"dcloudext\": {\r\n        \"sale\": {\r\n            \"regular\": {\r\n                \"price\": \"0.00\"\r\n            },\r\n            \"sourcecode\": {\r\n                \"price\": \"0.00\"\r\n            }\r\n        },\r\n        \"contact\": {\r\n            \"qq\": \"\"\r\n        },\r\n        \"declaration\": {\r\n            \"ads\": \"无\",\r\n            \"data\": \"无\",\r\n            \"permissions\": \"无\"\r\n        },\r\n        \"npmurl\": \"https://www.npmjs.com/package/mp-html\",\r\n        \"type\": \"component-vue\"\r\n    },\r\n    \"uni_modules\": {\r\n        \"platforms\": {\r\n            \"cloud\": {\r\n                \"tcb\": \"y\",\r\n                \"aliyun\": \"y\"\r\n            },\r\n            \"client\": {\r\n                \"App\": {\r\n                    \"app-vue\": \"y\",\r\n                    \"app-nvue\": \"y\"\r\n                },\r\n                \"H5-mobile\": {\r\n                    \"Safari\": \"y\",\r\n                    \"Android Browser\": \"y\",\r\n                    \"微信浏览器(Android)\": \"y\",\r\n                    \"QQ浏览器(Android)\": \"y\"\r\n                },\r\n                \"H5-pc\": {\r\n                    \"Chrome\": \"y\",\r\n                    \"IE\": \"u\",\r\n                    \"Edge\": \"y\",\r\n                    \"Firefox\": \"y\",\r\n                    \"Safari\": \"y\"\r\n                },\r\n                \"小程序\": {\r\n                    \"微信\": \"y\",\r\n                    \"阿里\": \"y\",\r\n                    \"百度\": \"y\",\r\n                    \"字节跳动\": \"y\",\r\n                    \"QQ\": \"y\"\r\n                },\r\n                \"快应用\": {\r\n                    \"华为\": \"y\",\r\n                    \"联盟\": \"y\"\r\n                },\r\n                \"Vue\": {\r\n                    \"vue2\": \"y\",\r\n                    \"vue3\": \"y\"\r\n                }\r\n            }\r\n        }\r\n    }\r\n}"
  },
  {
    "path": "UniApp/uni_modules/mp-html/static/app-plus/mp-html/js/handler.js",
    "content": "\"use strict\";function t(t){for(var e=Object.create(null),n=t.attributes.length;n--;)e[t.attributes[n].name]=t.attributes[n].value;return e}function e(){a[1]&&(this.src=a[1],this.onerror=null),this.onclick=null,this.ontouchstart=null,uni.postMessage({data:{action:\"onError\",source:\"img\",attrs:t(this)}})}function n(){window.unloadimgs-=1,0===window.unloadimgs&&uni.postMessage({data:{action:\"onReady\"}})}function o(r,s,c){for(var d=0;d<r.length;d++)!function(d){var u=r[d],l=void 0;if(u.type&&\"node\"!==u.type)l=document.createTextNode(u.text.replace(/&amp;/g,\"&\"));else{var g=u.name;\"svg\"===g&&(c=\"http://www.w3.org/2000/svg\"),\"html\"!==g&&\"body\"!==g||(g=\"div\"),l=c?document.createElementNS(c,g):document.createElement(g);for(var p in u.attrs)l.setAttribute(p,u.attrs[p]);if(u.children&&o(u.children,l,c),\"img\"===g){if(window.unloadimgs+=1,l.onload=n,l.onerror=n,!l.src&&l.getAttribute(\"data-src\")&&(l.src=l.getAttribute(\"data-src\")),u.attrs.ignore||(l.onclick=function(e){e.stopPropagation(),uni.postMessage({data:{action:\"onImgTap\",attrs:t(this)}})}),a[2]){var h=new Image;h.src=l.src,l.src=a[2],h.onload=function(){l.src=this.src},h.onerror=function(){l.onerror()}}l.onerror=e}else if(\"a\"===g)l.addEventListener(\"click\",function(e){e.stopPropagation(),e.preventDefault();var n,o=this.getAttribute(\"href\");o&&\"#\"===o[0]&&(n=(document.getElementById(o.substr(1))||{}).offsetTop),uni.postMessage({data:{action:\"onLinkTap\",attrs:t(this),offset:n}})},!0);else if(\"video\"===g||\"audio\"===g)i.push(l),u.attrs.autoplay||u.attrs.controls||l.setAttribute(\"controls\",\"true\"),l.onplay=function(){if(uni.postMessage({data:{action:\"onPlay\"}}),a[3])for(var t=0;t<i.length;t++)i[t]!==this&&i[t].pause()},l.onerror=function(){uni.postMessage({data:{action:\"onError\",source:g,attrs:t(this)}})};else if(\"table\"===g&&a[4]&&!l.style.cssText.includes(\"inline\")){var f=document.createElement(\"div\");f.style.overflow=\"auto\",f.appendChild(l),l=f}else\"svg\"===g&&(c=void 0)}s.appendChild(l)}(d)}document.addEventListener(\"UniAppJSBridgeReady\",function(){document.body.onclick=function(){return uni.postMessage({data:{action:\"onClick\"}})},uni.postMessage({data:{action:\"onJSBridgeReady\"}})});var a,i=[];window.setContent=function(t,e,n){var r=document.getElementById(\"content\");e[0]&&(document.body.style.cssText=e[0]),e[5]||(r.style.userSelect=\"none\"),n||(r.innerHTML=\"\",i=[]),a=e,window.unloadimgs=0;var s=document.createDocumentFragment();o(t,s),r.appendChild(s);var c=r.scrollHeight;uni.postMessage({data:{action:\"onLoad\",height:c}}),window.unloadimgs||uni.postMessage({data:{action:\"onReady\",height:c}}),clearInterval(window.timer),window.timer=setInterval(function(){r.scrollHeight!==c&&(c=r.scrollHeight,uni.postMessage({data:{action:\"onHeightChange\",height:c}}))},350)},window.onunload=function(){clearInterval(window.timer)};"
  },
  {
    "path": "UniApp/uni_modules/mp-html/static/app-plus/mp-html/local.html",
    "content": "<head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no\"><style>body,html{width:100%;height:100%;overflow-x:scroll;overflow-y:hidden}body{margin:0}video{width:300px;height:225px}img{max-width:100%;-webkit-touch-callout:none}</style></head><body><div id=\"content\" style=\"overflow:hidden\"></div><script type=\"text/javascript\" src=\"./js/uni.webview.min.js\"></script><script type=\"text/javascript\" src=\"./js/handler.js\"></script></body>"
  }
]