├── .editorconfig ├── .env ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .prettierignore ├── .prettierrc.js ├── LICENSE ├── README.md ├── config ├── config.ts ├── defaultSettings.ts ├── oneapi.json ├── proxy.ts └── routes.ts ├── jest.config.ts ├── jsconfig.json ├── package.json ├── pnpm-lock.yaml ├── public ├── CNAME ├── favicon.ico ├── insightface.webp ├── logo.png ├── logo.svg ├── midjourney.webp ├── niji.webp └── scripts │ └── loading.js ├── src ├── access.ts ├── app.tsx ├── components │ ├── HeaderDropdown │ │ └── index.tsx │ ├── JsonEditor │ │ └── index.tsx │ └── RightContent │ │ ├── AvatarDropdown.tsx │ │ └── index.tsx ├── global.less ├── global.tsx ├── locales │ ├── en-US.ts │ ├── en-US │ │ ├── component.ts │ │ ├── globalHeader.ts │ │ ├── menu.ts │ │ ├── pages.ts │ │ ├── pwa.ts │ │ ├── settingDrawer.ts │ │ └── settings.ts │ ├── zh-CN.ts │ └── zh-CN │ │ ├── component.ts │ │ ├── globalHeader.ts │ │ ├── menu.ts │ │ ├── pages.ts │ │ ├── pwa.ts │ │ ├── settingDrawer.ts │ │ └── settings.ts ├── manifest.json ├── pages │ ├── 404.tsx │ ├── AccountList │ │ ├── components │ │ │ ├── button │ │ │ │ ├── DelButton.tsx │ │ │ │ └── SyncButton.tsx │ │ │ └── contents │ │ │ │ ├── AddContent.tsx │ │ │ │ ├── CfContent.tsx │ │ │ │ ├── MoreContent.tsx │ │ │ │ ├── ReconnectContent.tsx │ │ │ │ └── UpdateContent.tsx │ │ ├── index.less │ │ └── index.tsx │ ├── BannedWordList │ │ ├── List │ │ │ ├── index.less │ │ │ └── index.tsx │ │ └── components │ │ │ └── AddContent.tsx │ ├── DomainList │ │ ├── List │ │ │ ├── index.less │ │ │ └── index.tsx │ │ └── components │ │ │ └── AddContent.tsx │ ├── Draw │ │ ├── index.less │ │ └── index.tsx │ ├── Probe │ │ ├── index.less │ │ └── index.tsx │ ├── Setting │ │ ├── index.less │ │ └── index.tsx │ ├── Task │ │ ├── List │ │ │ ├── index.less │ │ │ └── index.tsx │ │ └── components │ │ │ └── TaskContent.tsx │ ├── User │ │ └── Login │ │ │ └── index.tsx │ ├── UserList │ │ ├── List │ │ │ ├── index.less │ │ │ └── index.tsx │ │ └── components │ │ │ └── AddContent.tsx │ ├── Welcome.tsx │ └── components │ │ └── Modal.tsx ├── requestErrorConfig.ts ├── service-worker.js ├── services │ ├── mj │ │ ├── api.ts │ │ ├── index.ts │ │ └── typings.d.ts │ └── swagger │ │ ├── index.ts │ │ ├── pet.ts │ │ ├── store.ts │ │ ├── typings.d.ts │ │ └── user.ts └── typings.d.ts ├── tests └── setupTests.jsx ├── tsconfig.json └── types └── cache ├── cache.json └── mock └── mock.cache.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [Makefile] 16 | indent_style = tab 17 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | # MJ-SERVER 2 | MJ_SERVER=http://127.0.0.1:8080 3 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /lambda/ 2 | /scripts 3 | /config 4 | .history 5 | public 6 | dist 7 | .umi 8 | mock -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [require.resolve('@umijs/lint/dist/config/eslint')], 3 | globals: { 4 | page: true, 5 | REACT_APP_ENV: true, 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | **/node_modules 5 | # roadhog-api-doc ignore 6 | /src/utils/request-temp.js 7 | _roadhog-api-doc 8 | 9 | # production 10 | /dist 11 | 12 | # misc 13 | .DS_Store 14 | npm-debug.log* 15 | yarn-error.log 16 | 17 | /coverage 18 | .idea 19 | yarn.lock 20 | package-lock.json 21 | *bak 22 | .vscode 23 | 24 | 25 | # visual studio code 26 | .history 27 | *.log 28 | functions/* 29 | .temp/** 30 | 31 | # umi 32 | .umi 33 | .umi-production 34 | .umi-test 35 | 36 | # screenshot 37 | screenshot 38 | .firebase 39 | .eslintcache 40 | 41 | build 42 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/*.svg 2 | .umi 3 | .umi-production 4 | /dist 5 | .dockerignore 6 | .DS_Store 7 | .eslintignore 8 | *.png 9 | *.toml 10 | docker 11 | .editorconfig 12 | Dockerfile* 13 | .gitignore 14 | .prettierignore 15 | LICENSE 16 | .eslintcache 17 | *.lock 18 | yarn-error.log 19 | .history 20 | CNAME 21 | /build 22 | /public 23 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: true, 3 | trailingComma: 'all', 4 | printWidth: 100, 5 | proseWrap: 'never', 6 | endOfLine: 'lf', 7 | overrides: [ 8 | { 9 | files: '.prettierrc', 10 | options: { 11 | parser: 'json', 12 | }, 13 | }, 14 | { 15 | files: 'document.ejs', 16 | options: { 17 | parser: 'html', 18 | }, 19 | }, 20 | ], 21 | }; 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2024 True AI Organization 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # midjourney-proxy-admin 2 | 3 | [midjourney-proxy](https://github.com/trueai-org/midjourney-proxy) 的管理后台 4 | 5 | # 主要功能 6 | 7 | - [x] 支持MJ账号的增删改查功能 8 | - [x] 支持MJ账号的详细信息查询和账号同步操作 9 | - [x] 支持MJ账号的并发队列设置 10 | - [x] 支持MJ的账号settings设置 11 | - [x] 支持MJ的任务查询 12 | - [x] 提供功能齐全的绘图测试页面 13 | 14 | # 部署方式 15 | 16 | ## 1.运行环境 17 | 18 | 支持 Linux、MacOS、Windows 系统(可在Linux服务器上长期运行),同时需安装 `node18`。 19 | 20 | **(1) 克隆项目代码:** 21 | 22 | ```bash 23 | git clone https://github.com/trueai-org/midjourney-proxy-webui 24 | cd midjourney-proxy-webui/ 25 | ``` 26 | 27 | **(2) 安装依赖 :** 28 | 29 | ```bash 30 | yarn 31 | ``` 32 | 33 | ## 2.配置 34 | 35 | 配置文件在根目录的`.env`中: 36 | 37 | ```shell 38 | # MJ-SERVER 39 | MJ_SERVER=http://127.0.0.1:8080 40 | ``` 41 | 42 | ## 3.运行 43 | 44 | ``` 45 | yarn dev 46 | ``` 47 | 48 | -------------------------------------------------------------------------------- /config/config.ts: -------------------------------------------------------------------------------- 1 | // https://umijs.org/config/ 2 | import { defineConfig } from '@umijs/max'; 3 | import defaultSettings from './defaultSettings'; 4 | import proxy from './proxy'; 5 | import routes from './routes'; 6 | 7 | const { REACT_APP_ENV = 'dev' } = process.env; 8 | 9 | export default defineConfig({ 10 | /** 11 | * @name 开启 base 前缀配置 12 | * @description base 配置允许你为应用程序设置路由前缀 13 | * @doc https://umijs.org/docs/api/config#base 14 | */ 15 | base: "", 16 | /** 17 | * @name publicPath 静态资源配置 18 | * @description publicPath 配置构建后的静态资源存放位置 19 | * @doc https://umijs.org/docs/api/config#publicPath 20 | */ 21 | publicPath: '/', 22 | esbuildMinifyIIFE: true, 23 | /** 24 | * @name 开启 hash 模式 25 | * @description 让 build 之后的产物包含 hash 后缀。通常用于增量发布和避免浏览器加载缓存。 26 | * @doc https://umijs.org/docs/api/config#hash 27 | */ 28 | hash: true, 29 | history: { type: 'hash' }, 30 | 31 | /** 32 | * @name 兼容性设置 33 | * @description 设置 ie11 不一定完美兼容,需要检查自己使用的所有依赖 34 | * @doc https://umijs.org/docs/api/config#targets 35 | */ 36 | // targets: { 37 | // ie: 11, 38 | // }, 39 | /** 40 | * @name 路由的配置,不在路由中引入的文件不会编译 41 | * @description 只支持 path,component,routes,redirect,wrappers,title 的配置 42 | * @doc https://umijs.org/docs/guides/routes 43 | */ 44 | // umi routes: https://umijs.org/docs/routing 45 | routes, 46 | /** 47 | * @name 主题的配置 48 | * @description 虽然叫主题,但是其实只是 less 的变量设置 49 | * @doc antd的主题设置 https://ant.design/docs/react/customize-theme-cn 50 | * @doc umi 的theme 配置 https://umijs.org/docs/api/config#theme 51 | */ 52 | theme: { 53 | // 如果不想要 configProvide 动态设置主题需要把这个设置为 default 54 | // 只有设置为 variable, 才能使用 configProvide 动态设置主色调 55 | 'root-entry-name': 'variable', 56 | }, 57 | /** 58 | * @name moment 的国际化配置 59 | * @description 如果对国际化没有要求,打开之后能减少js的包大小 60 | * @doc https://umijs.org/docs/api/config#ignoremomentlocale 61 | */ 62 | ignoreMomentLocale: true, 63 | /** 64 | * @name 代理配置 65 | * @description 可以让你的本地服务器代理到你的服务器上,这样你就可以访问服务器的数据了 66 | * @see 要注意以下 代理只能在本地开发时使用,build 之后就无法使用了。 67 | * @doc 代理介绍 https://umijs.org/docs/guides/proxy 68 | * @doc 代理配置 https://umijs.org/docs/api/config#proxy 69 | */ 70 | proxy: proxy[REACT_APP_ENV as keyof typeof proxy], 71 | /** 72 | * @name 快速热更新配置 73 | * @description 一个不错的热更新组件,更新时可以保留 state 74 | */ 75 | fastRefresh: true, 76 | //============== 以下都是max的插件配置 =============== 77 | /** 78 | * @name 数据流插件 79 | * @@doc https://umijs.org/docs/max/data-flow 80 | */ 81 | model: {}, 82 | /** 83 | * 一个全局的初始数据流,可以用它在插件之间共享数据 84 | * @description 可以用来存放一些全局的数据,比如用户信息,或者一些全局的状态,全局初始状态在整个 Umi 项目的最开始创建。 85 | * @doc https://umijs.org/docs/max/data-flow#%E5%85%A8%E5%B1%80%E5%88%9D%E5%A7%8B%E7%8A%B6%E6%80%81 86 | */ 87 | initialState: {}, 88 | /** 89 | * @name layout 插件 90 | * @doc https://umijs.org/docs/max/layout-menu 91 | */ 92 | title: 'Midjourney Proxy Admin', 93 | layout: { 94 | locale: true, 95 | ...defaultSettings, 96 | }, 97 | /** 98 | * @name moment2dayjs 插件 99 | * @description 将项目中的 moment 替换为 dayjs 100 | * @doc https://umijs.org/docs/max/moment2dayjs 101 | */ 102 | moment2dayjs: { 103 | preset: 'antd', 104 | plugins: ['duration'], 105 | }, 106 | /** 107 | * @name 国际化插件 108 | * @doc https://umijs.org/docs/max/i18n 109 | */ 110 | locale: { 111 | // default zh-CN 112 | default: 'zh-CN', 113 | antd: true, 114 | // default true, when it is true, will use `navigator.language` overwrite default 115 | baseNavigator: true, 116 | }, 117 | /** 118 | * @name antd 插件 119 | * @description 内置了 babel import 插件 120 | * @doc https://umijs.org/docs/max/antd#antd 121 | */ 122 | antd: {}, 123 | /** 124 | * @name 网络请求配置 125 | * @description 它基于 axios 和 ahooks 的 useRequest 提供了一套统一的网络请求和错误处理方案。 126 | * @doc https://umijs.org/docs/max/request 127 | */ 128 | request: {}, 129 | /** 130 | * @name 权限插件 131 | * @description 基于 initialState 的权限插件,必须先打开 initialState 132 | * @doc https://umijs.org/docs/max/access 133 | */ 134 | access: {}, 135 | /** 136 | * @name 中额外的 script 137 | * @description 配置 中额外的 script 138 | */ 139 | headScripts: [ 140 | // 解决首次加载时白屏的问题 141 | { src: '/scripts/loading.js', async: true }, 142 | ], 143 | //================ pro 插件配置 ================= 144 | presets: ['umi-presets-pro'], 145 | /** 146 | * @name openAPI 插件的配置 147 | * @description 基于 openapi 的规范生成serve 和mock,能减少很多样板代码 148 | * @doc https://pro.ant.design/zh-cn/docs/openapi/ 149 | */ 150 | mfsu: { 151 | strategy: 'normal', 152 | }, 153 | requestRecord: {}, 154 | }); 155 | -------------------------------------------------------------------------------- /config/defaultSettings.ts: -------------------------------------------------------------------------------- 1 | import { ProLayoutProps } from '@ant-design/pro-components'; 2 | 3 | /** 4 | * @name 5 | */ 6 | const Settings: ProLayoutProps & { 7 | pwa?: boolean; 8 | logo?: string; 9 | } = { 10 | navTheme: 'light', 11 | // 拂晓蓝 12 | colorPrimary: '#1890ff', 13 | layout: 'mix', 14 | contentWidth: 'Fluid', 15 | fixedHeader: false, 16 | fixSiderbar: true, 17 | colorWeak: false, 18 | title: 'Midjourney Proxy Admin', 19 | pwa: true, 20 | logo: './logo.svg', 21 | iconfontUrl: '', 22 | token: { 23 | // 参见ts声明,demo 见文档,通过token 修改样式 24 | //https://procomponents.ant.design/components/layout#%E9%80%9A%E8%BF%87-token-%E4%BF%AE%E6%94%B9%E6%A0%B7%E5%BC%8F 25 | }, 26 | }; 27 | 28 | export default Settings; 29 | -------------------------------------------------------------------------------- /config/oneapi.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.1", 3 | "info": { 4 | "title": "Ant Design Pro", 5 | "version": "1.0.0" 6 | }, 7 | "servers": [ 8 | { 9 | "url": "http://localhost:8000/" 10 | }, 11 | { 12 | "url": "https://localhost:8000/" 13 | } 14 | ], 15 | "paths": { 16 | "/swagger": { 17 | "x-swagger-pipe": "swagger_raw" 18 | } 19 | }, 20 | "components": { 21 | "schemas": { 22 | "LoginResult": { 23 | "type": "object", 24 | "properties": { 25 | "status": { 26 | "type": "string" 27 | }, 28 | "type": { 29 | "type": "string" 30 | }, 31 | "currentAuthority": { 32 | "type": "string" 33 | } 34 | } 35 | }, 36 | "PageParams": { 37 | "type": "object", 38 | "properties": { 39 | "current": { 40 | "type": "number" 41 | }, 42 | "pageSize": { 43 | "type": "number" 44 | } 45 | } 46 | }, 47 | "NoticeIconItem": { 48 | "type": "object", 49 | "properties": { 50 | "id": { 51 | "type": "string" 52 | }, 53 | "extra": { 54 | "type": "string", 55 | "format": "any" 56 | }, 57 | "key": { "type": "string" }, 58 | "read": { 59 | "type": "boolean" 60 | }, 61 | "avatar": { 62 | "type": "string" 63 | }, 64 | "title": { 65 | "type": "string" 66 | }, 67 | "status": { 68 | "type": "string" 69 | }, 70 | "datetime": { 71 | "type": "string", 72 | "format": "date" 73 | }, 74 | "description": { 75 | "type": "string" 76 | }, 77 | "type": { 78 | "extensions": { 79 | "x-is-enum": true 80 | }, 81 | "$ref": "#/components/schemas/NoticeIconItemType" 82 | } 83 | } 84 | } 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /config/proxy.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @name 代理的配置 3 | * @see 在生产环境 代理是无法生效的,所以这里没有生产环境的配置 4 | * ------------------------------- 5 | * The agent cannot take effect in the production environment 6 | * so there is no configuration of the production environment 7 | * For details, please see 8 | * https://pro.ant.design/docs/deploy 9 | * 10 | * @doc https://umijs.org/docs/guides/proxy 11 | */ 12 | 13 | const MJ_SERVER = process.env.MJ_SERVER; 14 | 15 | export default { 16 | // 如果需要自定义本地开发服务器 请取消注释按需调整 17 | dev: { 18 | // localhost:8000/api/** -> https://preview.pro.ant.design/api/** 19 | '/mj/': { 20 | // 要代理的地址 21 | target: MJ_SERVER, 22 | // 配置了这个可以从 http 代理到 https 23 | // 依赖 origin 的功能可能需要这个,比如 cookie 24 | changeOrigin: true, 25 | }, 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /config/routes.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @name umi 的路由配置 3 | * @description 只支持 path,component,routes,redirect,wrappers,name,icon 的配置 4 | * @param path path 只支持两种占位符配置,第一种是动态参数 :id 的形式,第二种是 * 通配符,通配符只能出现路由字符串的最后。 5 | * @param component 配置 location 和 path 匹配后用于渲染的 React 组件路径。可以是绝对路径,也可以是相对路径,如果是相对路径,会从 src/pages 开始找起。 6 | * @param routes 配置子路由,通常在需要为多个路径增加 layout 组件时使用。 7 | * @param redirect 配置路由跳转 8 | * @param wrappers 配置路由组件的包装组件,通过包装组件可以为当前的路由组件组合进更多的功能。 比如,可以用于路由级别的权限校验 9 | * @param name 配置路由的标题,默认读取国际化文件 menu.ts 中 menu.xxxx 的值,如配置 name 为 login,则读取 menu.ts 中 menu.login 的取值作为标题 10 | * @param icon 配置路由的图标,取值参考 https://ant.design/components/icon-cn, 注意去除风格后缀和大小写,如想要配置图标为 则取值应为 stepBackward 或 StepBackward,如想要配置图标为 则取值应为 user 或者 User 11 | * @doc https://umijs.org/docs/guides/routes 12 | */ 13 | export default [ 14 | { 15 | path: '/user', 16 | layout: false, 17 | routes: [ 18 | { 19 | name: 'login', 20 | path: 'login', 21 | component: './User/Login', 22 | }, 23 | ], 24 | }, 25 | { 26 | path: '/welcome', 27 | name: 'welcome', 28 | icon: 'smile', 29 | component: './Welcome', 30 | }, 31 | { 32 | path: '/', 33 | redirect: '/welcome', 34 | }, 35 | { 36 | name: 'list.account-list', 37 | icon: 'crown', 38 | path: '/account', 39 | component: './AccountList', 40 | }, 41 | { 42 | name: 'list.domain-list', 43 | icon: 'tag', 44 | path: '/domain', 45 | component: './DomainList/List', 46 | }, 47 | { 48 | name: 'list.user-list', 49 | icon: 'user', 50 | path: '/user-list', 51 | component: './UserList/List', 52 | }, 53 | { 54 | name: 'task-list', 55 | icon: 'bars', 56 | path: '/task', 57 | component: './Task/List', 58 | }, 59 | { 60 | name: 'draw-test', 61 | icon: 'experiment', 62 | path: '/draw-test', 63 | component: './Draw', 64 | }, 65 | { 66 | name: 'list.banned-word-list', 67 | icon: 'stop', 68 | path: '/banned-word', 69 | component: './BannedWordList/List', 70 | }, 71 | { 72 | name: 'setting', 73 | icon: 'setting', 74 | path: '/setting', 75 | component: './Setting', 76 | }, 77 | { 78 | name: 'probe', 79 | icon: 'profile', 80 | path: '/probe', 81 | component: './Probe', 82 | }, 83 | { 84 | path: '*', 85 | layout: false, 86 | component: './404', 87 | }, 88 | ]; 89 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import { configUmiAlias, createConfig } from '@umijs/max/test'; 2 | 3 | export default async () => { 4 | const config = await configUmiAlias({ 5 | ...createConfig({ 6 | target: 'browser', 7 | }), 8 | }); 9 | 10 | console.log(); 11 | return { 12 | ...config, 13 | testEnvironmentOptions: { 14 | ...(config?.testEnvironmentOptions || {}), 15 | url: 'http://localhost:8000', 16 | }, 17 | setupFiles: [...(config.setupFiles || []), './tests/setupTests.jsx'], 18 | globals: { 19 | ...config.globals, 20 | localStorage: null, 21 | }, 22 | }; 23 | }; 24 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react-jsx", 4 | "emitDecoratorMetadata": true, 5 | "experimentalDecorators": true, 6 | "baseUrl": ".", 7 | "paths": { 8 | "@/*": ["./src/*"] 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "midjourney-proxy-admin", 3 | "version": "1.0", 4 | "private": true, 5 | "description": "An out-of-box UI solution for enterprise applications", 6 | "scripts": { 7 | "analyze": "cross-env ANALYZE=1 max build", 8 | "build": "max build", 9 | "deploy": "npm run build && npm run gh-pages", 10 | "dev": "npm run start:dev", 11 | "gh-pages": "gh-pages -d dist", 12 | "i18n-remove": "pro i18n-remove --locale=zh-CN --write", 13 | "postinstall": "max setup", 14 | "jest": "jest", 15 | "lint": "npm run lint:js && npm run lint:prettier && npm run tsc", 16 | "lint-staged": "lint-staged", 17 | "lint-staged:js": "eslint --ext .js,.jsx,.ts,.tsx ", 18 | "lint:fix": "eslint --fix --cache --ext .js,.jsx,.ts,.tsx --format=pretty ./src ", 19 | "lint:js": "eslint --cache --ext .js,.jsx,.ts,.tsx --format=pretty ./src", 20 | "lint:prettier": "prettier -c --write \"**/**.{js,jsx,tsx,ts,less,md,json}\" --end-of-line auto", 21 | "openapi": "max openapi", 22 | "prettier": "prettier -c --write \"**/**.{js,jsx,tsx,ts,less,md,json}\"", 23 | "preview": "npm run build && max preview --port 8000", 24 | "record": "cross-env NODE_ENV=development REACT_APP_ENV=test max record --scene=login", 25 | "serve": "umi-serve", 26 | "start": "cross-env UMI_ENV=dev max dev", 27 | "start:dev": "cross-env REACT_APP_ENV=dev MOCK=none UMI_ENV=dev max dev", 28 | "start:no-mock": "cross-env MOCK=none UMI_ENV=dev max dev", 29 | "start:pre": "cross-env REACT_APP_ENV=pre UMI_ENV=dev max dev", 30 | "start:test": "cross-env REACT_APP_ENV=test MOCK=none UMI_ENV=dev max dev", 31 | "test": "jest", 32 | "test:coverage": "npm run jest -- --coverage", 33 | "test:update": "npm run jest -- -u", 34 | "tsc": "tsc --noEmit" 35 | }, 36 | "lint-staged": { 37 | "**/*.{js,jsx,ts,tsx}": "npm run lint-staged:js", 38 | "**/*.{js,jsx,tsx,ts,less,md,json}": [ 39 | "prettier --write" 40 | ] 41 | }, 42 | "browserslist": [ 43 | "> 1%", 44 | "last 2 versions", 45 | "not ie <= 10" 46 | ], 47 | "dependencies": { 48 | "@ant-design/icons": "^4.8.0", 49 | "@ant-design/pro-components": "^2.3.57", 50 | "@ant-design/use-emotion-css": "1.0.4", 51 | "@umijs/route-utils": "^2.2.2", 52 | "ace-builds": "^1.35.4", 53 | "antd": "^5.20.0", 54 | "classnames": "^2.3.2", 55 | "lodash": "^4.17.21", 56 | "moment": "^2.29.4", 57 | "omit.js": "^2.0.2", 58 | "rc-menu": "^9.8.2", 59 | "rc-util": "^5.27.2", 60 | "react": "^18.2.0", 61 | "react-ace": "^12.0.0", 62 | "react-dev-inspector": "^1.8.4", 63 | "react-dom": "^18.2.0", 64 | "react-helmet-async": "^1.3.0", 65 | "react-markdown": "^9.0.0" 66 | }, 67 | "devDependencies": { 68 | "@ant-design/pro-cli": "^2.1.5", 69 | "@testing-library/react": "^13.4.0", 70 | "@types/classnames": "^2.3.1", 71 | "@types/express": "^4.17.17", 72 | "@types/history": "^4.7.11", 73 | "@types/jest": "^29.4.0", 74 | "@types/lodash": "^4.14.191", 75 | "@types/react": "^18.0.28", 76 | "@types/react-dom": "^18.0.11", 77 | "@types/react-helmet": "^6.1.6", 78 | "@umijs/fabric": "^2.14.1", 79 | "@umijs/lint": "^4.0.52", 80 | "@umijs/max": "^4.0.52", 81 | "cross-env": "^7.0.3", 82 | "eslint": "^8.34.0", 83 | "express": "^4.18.2", 84 | "gh-pages": "^3.2.3", 85 | "husky": "^7.0.4", 86 | "jest": "^29.4.3", 87 | "jest-environment-jsdom": "^29.4.3", 88 | "lint-staged": "^10.5.4", 89 | "mockjs": "^1.1.0", 90 | "prettier": "^2.8.4", 91 | "swagger-ui-dist": "^4.15.5", 92 | "ts-node": "^10.9.1", 93 | "typescript": "^4.9.5", 94 | "umi-presets-pro": "^2.0.2" 95 | }, 96 | "engines": { 97 | "node": ">=12.0.0" 98 | }, 99 | "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" 100 | } 101 | -------------------------------------------------------------------------------- /public/CNAME: -------------------------------------------------------------------------------- 1 | preview.pro.ant.design -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trueai-org/midjourney-proxy-webui/1ceaa67750eb8e4db1e4cb38a3b8f0aca6e53cad/public/favicon.ico -------------------------------------------------------------------------------- /public/insightface.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trueai-org/midjourney-proxy-webui/1ceaa67750eb8e4db1e4cb38a3b8f0aca6e53cad/public/insightface.webp -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trueai-org/midjourney-proxy-webui/1ceaa67750eb8e4db1e4cb38a3b8f0aca6e53cad/public/logo.png -------------------------------------------------------------------------------- /public/midjourney.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trueai-org/midjourney-proxy-webui/1ceaa67750eb8e4db1e4cb38a3b8f0aca6e53cad/public/midjourney.webp -------------------------------------------------------------------------------- /public/niji.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trueai-org/midjourney-proxy-webui/1ceaa67750eb8e4db1e4cb38a3b8f0aca6e53cad/public/niji.webp -------------------------------------------------------------------------------- /public/scripts/loading.js: -------------------------------------------------------------------------------- 1 | /** 2 | * loading 占位 3 | * 解决首次加载时白屏的问题 4 | */ 5 | (function () { 6 | const _root = document.querySelector('#root'); 7 | if (_root && _root.innerHTML === '') { 8 | _root.innerHTML = ` 9 | 174 | 175 |
183 |
184 |
185 | 186 | 187 | 188 | 189 | 190 | 191 |
192 |
193 |
194 | 正在加载资源 195 |
196 |
197 | 初次加载资源可能需要较多时间 请耐心等待 198 |
199 |
200 | `; 201 | } 202 | })(); 203 | -------------------------------------------------------------------------------- /src/access.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @see https://umijs.org/zh-CN/plugins/plugin-access 3 | * */ 4 | export default function access(initialState: { currentUser?: API.CurrentUser } | undefined) { 5 | const { currentUser } = initialState ?? {}; 6 | return { 7 | canAdmin: currentUser && currentUser.access === 'admin', 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /src/app.tsx: -------------------------------------------------------------------------------- 1 | import { SelectLang } from '@/components/RightContent'; 2 | import type { Settings as LayoutSettings } from '@ant-design/pro-components'; 3 | import { SettingDrawer } from '@ant-design/pro-components'; 4 | import type { RequestConfig, RunTimeLayoutConfig } from '@umijs/max'; 5 | import { history } from '@umijs/max'; 6 | import defaultSettings from '../config/defaultSettings'; 7 | import { AvatarDropdown, AvatarName } from './components/RightContent/AvatarDropdown'; 8 | import { errorConfig } from './requestErrorConfig'; 9 | import { currentUser as queryCurrentUser } from './services/mj/api'; 10 | 11 | const isDev = process.env.NODE_ENV === 'development'; 12 | const loginPath = '/user/login'; 13 | 14 | /** 15 | * @see https://umijs.org/zh-CN/plugins/plugin-initial-state 16 | * */ 17 | export async function getInitialState(): Promise<{ 18 | settings?: Partial; 19 | currentUser?: API.CurrentUser; 20 | loading?: boolean; 21 | fetchUserInfo?: () => Promise; 22 | }> { 23 | const fetchUserInfo = async () => { 24 | try { 25 | const msg = await queryCurrentUser({ 26 | skipErrorHandler: true, 27 | }); 28 | return msg; 29 | } catch (error) { 30 | history.push(loginPath); 31 | } 32 | return undefined; 33 | }; 34 | // 如果不是登录页面,执行 35 | const { location } = history; 36 | if (location.pathname !== loginPath) { 37 | const currentUser = await fetchUserInfo(); 38 | return { 39 | fetchUserInfo, 40 | currentUser, 41 | settings: defaultSettings as Partial, 42 | }; 43 | } 44 | return { 45 | fetchUserInfo, 46 | settings: defaultSettings as Partial, 47 | }; 48 | } 49 | 50 | // ProLayout 支持的api https://procomponents.ant.design/components/layout 51 | export const layout: RunTimeLayoutConfig = ({ initialState, setInitialState }) => { 52 | return { 53 | actionsRender: () => [], 54 | avatarProps: { 55 | src: initialState?.currentUser?.avatar, 56 | title: , 57 | render: (_, avatarChildren) => { 58 | return {avatarChildren}; 59 | }, 60 | }, 61 | 62 | onPageChange: () => { 63 | const { location } = history; 64 | // 如果没有登录,重定向到 login 65 | if (!initialState?.currentUser && location.pathname !== loginPath) { 66 | history.push(loginPath); 67 | } 68 | }, 69 | layoutBgImgList: [ 70 | { 71 | src: 'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/D2LWSqNny4sAAAAAAAAAAAAAFl94AQBr', 72 | left: 85, 73 | bottom: 100, 74 | height: '303px', 75 | }, 76 | { 77 | src: 'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/C2TWRpJpiC0AAAAAAAAAAAAAFl94AQBr', 78 | bottom: -68, 79 | right: -45, 80 | height: '303px', 81 | }, 82 | { 83 | src: 'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/F6vSTbj8KpYAAAAAAAAAAAAAFl94AQBr', 84 | bottom: 0, 85 | left: 0, 86 | width: '331px', 87 | }, 88 | ], 89 | links: [], 90 | menuHeaderRender: undefined, 91 | // 自定义 403 页面 92 | // unAccessible:
unAccessible
, 93 | // 增加一个 loading 的状态 94 | childrenRender: (children) => { 95 | // if (initialState?.loading) return ; 96 | return ( 97 | <> 98 | {children} 99 | { 104 | setInitialState((preInitialState) => ({ 105 | ...preInitialState, 106 | settings, 107 | })); 108 | }} 109 | /> 110 | 111 | ); 112 | }, 113 | ...initialState?.settings, 114 | }; 115 | }; 116 | 117 | /** 118 | * @name request 配置,可以配置错误处理 119 | * 它基于 axios 和 ahooks 的 useRequest 提供了一套统一的网络请求和错误处理方案。 120 | * @doc https://umijs.org/docs/max/request#配置 121 | */ 122 | export const request = { 123 | ...errorConfig, 124 | }; -------------------------------------------------------------------------------- /src/components/HeaderDropdown/index.tsx: -------------------------------------------------------------------------------- 1 | import { Dropdown, Tag } from 'antd'; 2 | import type { DropDownProps } from 'antd/es/dropdown'; 3 | import React from 'react'; 4 | import { useEmotionCss } from '@ant-design/use-emotion-css'; 5 | import classNames from 'classnames'; 6 | import { useModel } from '@umijs/max'; 7 | 8 | export type HeaderDropdownProps = { 9 | overlayClassName?: string; 10 | placement?: 'bottomLeft' | 'bottomRight' | 'topLeft' | 'topCenter' | 'topRight' | 'bottomCenter'; 11 | } & Omit; 12 | 13 | const HeaderDropdown: React.FC = ({ overlayClassName: cls, ...restProps }) => { 14 | const { initialState } = useModel('@@initialState'); 15 | const { currentUser } = initialState || {}; 16 | const className = useEmotionCss(({ token }) => { 17 | return { 18 | [`@media screen and (max-width: ${token.screenXS})`]: { 19 | width: '100%', 20 | }, 21 | }; 22 | }); 23 | return <>{currentUser?.version}; 24 | }; 25 | 26 | export default HeaderDropdown; 27 | -------------------------------------------------------------------------------- /src/components/JsonEditor/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import AceEditor from 'react-ace'; 3 | 4 | import 'ace-builds/src-noconflict/mode-json'; 5 | import 'ace-builds/src-noconflict/theme-textmate'; 6 | 7 | interface JsonEditorFormItemProps { 8 | value?: object; 9 | onChange?: (value: object) => void; 10 | } 11 | 12 | const JsonEditor: React.FC = ({ value = {}, onChange }) => { 13 | const [textValue, setTextValue] = React.useState(JSON.stringify(value, null, 2)); 14 | const [isValidJson, setIsValidJson] = React.useState(true); 15 | 16 | useEffect(() => { 17 | // 只有不等时才更新 18 | const json1 = JSON.stringify(value); 19 | const json2 = JSON.stringify(JSON.parse(textValue)); 20 | if (json1 !== json2) { 21 | setTextValue(JSON.stringify(value, null, 2)); 22 | } 23 | }, [value]); 24 | 25 | const handleChange = (newValue: string) => { 26 | setTextValue(newValue); 27 | try { 28 | const parsedValue = JSON.parse(newValue); 29 | setIsValidJson(true); 30 | if (onChange) { 31 | onChange(parsedValue); 32 | } 33 | } catch (error) { 34 | setIsValidJson(false); 35 | } 36 | }; 37 | return ( 38 |
39 | 63 | {!isValidJson && ( 64 |
JSON 格式错误,请检查输入!
65 | )} 66 |
67 | ); 68 | }; 69 | 70 | export default JsonEditor; 71 | 72 | // import React from 'react'; 73 | // import ReactJson, { InteractionProps } from 'react-json-view'; 74 | 75 | // interface JsonEditorFormItemProps { 76 | // value?: object; 77 | // onChange?: (value: object) => void; 78 | // } 79 | 80 | // const JsonEditor: React.FC = ({ value = {}, onChange }) => { 81 | // const handleEdit = (edit: InteractionProps) => { 82 | // if (onChange) { 83 | // onChange(edit.updated_src); 84 | // } 85 | // }; 86 | 87 | // const handleAdd = (add: InteractionProps) => { 88 | // if (onChange) { 89 | // onChange(add.updated_src); 90 | // } 91 | // }; 92 | 93 | // const handleDelete = (del: InteractionProps) => { 94 | // if (onChange) { 95 | // onChange(del.updated_src); 96 | // } 97 | // }; 98 | 99 | // return ( 100 | // 113 | // ); 114 | // }; 115 | 116 | // export default JsonEditor; 117 | -------------------------------------------------------------------------------- /src/components/RightContent/AvatarDropdown.tsx: -------------------------------------------------------------------------------- 1 | import { outLogin } from '@/services/mj/api'; 2 | import { LogoutOutlined, SettingOutlined, UserOutlined } from '@ant-design/icons'; 3 | import { useEmotionCss } from '@ant-design/use-emotion-css'; 4 | import { history, useModel, useIntl } from '@umijs/max'; 5 | import { Spin } from 'antd'; 6 | import { stringify } from 'querystring'; 7 | import type { MenuInfo } from 'rc-menu/lib/interface'; 8 | import React, { useCallback } from 'react'; 9 | import { flushSync } from 'react-dom'; 10 | import HeaderDropdown from '../HeaderDropdown'; 11 | 12 | export type GlobalHeaderRightProps = { 13 | menu?: boolean; 14 | children?: React.ReactNode; 15 | }; 16 | 17 | export const AvatarName = () => { 18 | const { initialState } = useModel('@@initialState'); 19 | const { currentUser } = initialState || {}; 20 | return {currentUser?.name}; 21 | }; 22 | 23 | export const AvatarDropdown: React.FC = ({ menu, children }) => { 24 | const intl = useIntl(); 25 | /** 26 | * 退出登录,并且将当前的 url 保存 27 | */ 28 | const loginOut = async () => { 29 | await outLogin(); 30 | const { search, pathname } = window.location; 31 | const urlParams = new URL(window.location.href).searchParams; 32 | /** 此方法会跳转到 redirect 参数所在的位置 */ 33 | const redirect = urlParams.get('redirect'); 34 | // Note: There may be security issues, please note 35 | if (window.location.pathname !== '/user/login' && !redirect) { 36 | history.replace({ 37 | pathname: '/user/login', 38 | search: stringify({ 39 | redirect: pathname + search, 40 | }), 41 | }); 42 | } 43 | }; 44 | const actionClassName = useEmotionCss(({ token }) => { 45 | return { 46 | display: 'flex', 47 | height: '48px', 48 | marginLeft: 'auto', 49 | overflow: 'hidden', 50 | alignItems: 'center', 51 | padding: '0 8px', 52 | cursor: 'pointer', 53 | borderRadius: token.borderRadius, 54 | '&:hover': { 55 | backgroundColor: token.colorBgTextHover, 56 | }, 57 | }; 58 | }); 59 | const { initialState, setInitialState } = useModel('@@initialState'); 60 | 61 | const onMenuClick = useCallback( 62 | (event: MenuInfo) => { 63 | const { key } = event; 64 | if (key === 'logout') { 65 | flushSync(() => { 66 | setInitialState((s) => ({ ...s, currentUser: undefined })); 67 | }); 68 | loginOut(); 69 | return; 70 | } 71 | history.push(`/account/${key}`); 72 | }, 73 | [setInitialState], 74 | ); 75 | 76 | const loading = ( 77 | 78 | 85 | 86 | ); 87 | 88 | if (!initialState) { 89 | return loading; 90 | } 91 | 92 | const { currentUser } = initialState; 93 | 94 | if (!currentUser || !currentUser.name) { 95 | return loading; 96 | } 97 | 98 | const menuItems = [ 99 | ...(menu 100 | ? [ 101 | { 102 | key: 'center', 103 | icon: , 104 | label: '个人中心', 105 | }, 106 | { 107 | key: 'settings', 108 | icon: , 109 | label: '个人设置', 110 | }, 111 | { 112 | type: 'divider' as const, 113 | }, 114 | ] 115 | : []), 116 | { 117 | key: 'logout', 118 | icon: , 119 | label: intl.formatMessage({ id: 'menu.account.logout' }), 120 | }, 121 | ]; 122 | 123 | return ( 124 | 131 | {children} 132 | 133 | ); 134 | }; 135 | -------------------------------------------------------------------------------- /src/components/RightContent/index.tsx: -------------------------------------------------------------------------------- 1 | import { QuestionCircleOutlined } from '@ant-design/icons'; 2 | import { SelectLang as UmiSelectLang } from '@umijs/max'; 3 | import React from 'react'; 4 | 5 | export type SiderTheme = 'light' | 'dark'; 6 | 7 | export const SelectLang = () => { 8 | return ( 9 | 14 | ); 15 | }; 16 | 17 | export const Question = () => { 18 | return ( 19 |
{ 25 | window.open('https://pro.ant.design/docs/getting-started'); 26 | }} 27 | > 28 | 29 |
30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /src/global.less: -------------------------------------------------------------------------------- 1 | html, 2 | body, 3 | #root { 4 | height: 100%; 5 | margin: 0; 6 | padding: 0; 7 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 8 | 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 9 | 'Noto Color Emoji'; 10 | } 11 | 12 | .colorWeak { 13 | filter: invert(80%); 14 | } 15 | 16 | .ant-layout { 17 | min-height: 100vh; 18 | } 19 | 20 | .ant-pro-sider.ant-layout-sider.ant-pro-sider-fixed { 21 | left: unset; 22 | } 23 | 24 | canvas { 25 | display: block; 26 | } 27 | 28 | body { 29 | text-rendering: optimizeLegibility; 30 | -webkit-font-smoothing: antialiased; 31 | -moz-osx-font-smoothing: grayscale; 32 | } 33 | 34 | ul, 35 | ol { 36 | list-style: none; 37 | } 38 | 39 | @media (max-width: 768px) { 40 | .ant-table { 41 | width: 100%; 42 | overflow-x: auto; 43 | 44 | &-thead>tr, 45 | &-tbody>tr { 46 | 47 | >th, 48 | >td { 49 | white-space: pre; 50 | 51 | >span { 52 | display: block; 53 | } 54 | } 55 | } 56 | } 57 | } 58 | 59 | // .ant-pro-form-login-top{ 60 | // padding: 0 30vw; 61 | // } 62 | 63 | .ant-pro-form-login-header { 64 | height: unset !important; 65 | } 66 | 67 | 68 | #subChannels_extra { 69 | position: absolute !important; 70 | right: 0; 71 | top: 0; 72 | } -------------------------------------------------------------------------------- /src/global.tsx: -------------------------------------------------------------------------------- 1 | import { useIntl } from '@umijs/max'; 2 | import { Button, message, notification } from 'antd'; 3 | import defaultSettings from '../config/defaultSettings'; 4 | 5 | const { pwa } = defaultSettings; 6 | const isHttps = document.location.protocol === 'https:'; 7 | 8 | const clearCache = () => { 9 | // remove all caches 10 | if (window.caches) { 11 | caches 12 | .keys() 13 | .then((keys) => { 14 | keys.forEach((key) => { 15 | caches.delete(key); 16 | }); 17 | }) 18 | .catch((e) => console.log(e)); 19 | } 20 | }; 21 | 22 | // if pwa is true 23 | if (pwa) { 24 | // Notify user if offline now 25 | window.addEventListener('sw.offline', () => { 26 | message.warning(useIntl().formatMessage({ id: 'app.pwa.offline' })); 27 | }); 28 | 29 | // Pop up a prompt on the page asking the user if they want to use the latest version 30 | window.addEventListener('sw.updated', (event: Event) => { 31 | const e = event as CustomEvent; 32 | const reloadSW = async () => { 33 | // Check if there is sw whose state is waiting in ServiceWorkerRegistration 34 | // https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration 35 | const worker = e.detail && e.detail.waiting; 36 | if (!worker) { 37 | return true; 38 | } 39 | // Send skip-waiting event to waiting SW with MessageChannel 40 | await new Promise((resolve, reject) => { 41 | const channel = new MessageChannel(); 42 | channel.port1.onmessage = (msgEvent) => { 43 | if (msgEvent.data.error) { 44 | reject(msgEvent.data.error); 45 | } else { 46 | resolve(msgEvent.data); 47 | } 48 | }; 49 | worker.postMessage({ type: 'skip-waiting' }, [channel.port2]); 50 | }); 51 | 52 | clearCache(); 53 | window.location.reload(); 54 | return true; 55 | }; 56 | const key = `open${Date.now()}`; 57 | const btn = ( 58 | 67 | ); 68 | notification.open({ 69 | message: useIntl().formatMessage({ id: 'app.pwa.serviceworker.updated' }), 70 | description: useIntl().formatMessage({ id: 'app.pwa.serviceworker.updated.hint' }), 71 | btn, 72 | key, 73 | onClose: async () => null, 74 | }); 75 | }); 76 | } else if ('serviceWorker' in navigator && isHttps) { 77 | // unregister service worker 78 | const { serviceWorker } = navigator; 79 | if (serviceWorker.getRegistrations) { 80 | serviceWorker.getRegistrations().then((sws) => { 81 | sws.forEach((sw) => { 82 | sw.unregister(); 83 | }); 84 | }); 85 | } 86 | serviceWorker.getRegistration().then((sw) => { 87 | if (sw) sw.unregister(); 88 | }); 89 | 90 | clearCache(); 91 | } 92 | -------------------------------------------------------------------------------- /src/locales/en-US.ts: -------------------------------------------------------------------------------- 1 | import component from './en-US/component'; 2 | import globalHeader from './en-US/globalHeader'; 3 | import menu from './en-US/menu'; 4 | import pages from './en-US/pages'; 5 | import pwa from './en-US/pwa'; 6 | import settingDrawer from './en-US/settingDrawer'; 7 | import settings from './en-US/settings'; 8 | 9 | export default { 10 | 'navBar.lang': 'Languages', 11 | 'layout.user.link.help': 'Help', 12 | 'layout.user.link.privacy': 'Privacy', 13 | 'layout.user.link.terms': 'Terms', 14 | 'app.copyright.produced': 'Midjourney Proxy Admin', 15 | 'app.preview.down.block': 'Download this page to your local project', 16 | 'app.welcome.link.fetch-blocks': 'Get all block', 17 | 'app.welcome.link.block-list': 'Quickly build standard, pages based on `block` development', 18 | ...globalHeader, 19 | ...menu, 20 | ...settingDrawer, 21 | ...settings, 22 | ...pwa, 23 | ...component, 24 | ...pages, 25 | }; 26 | -------------------------------------------------------------------------------- /src/locales/en-US/component.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'component.tagSelect.expand': 'Expand', 3 | 'component.tagSelect.collapse': 'Collapse', 4 | 'component.tagSelect.all': 'All', 5 | }; 6 | -------------------------------------------------------------------------------- /src/locales/en-US/globalHeader.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'component.globalHeader.search': 'Search', 3 | 'component.globalHeader.search.example1': 'Search example 1', 4 | 'component.globalHeader.search.example2': 'Search example 2', 5 | 'component.globalHeader.search.example3': 'Search example 3', 6 | 'component.globalHeader.help': 'Help', 7 | 'component.globalHeader.notification': 'Notification', 8 | 'component.globalHeader.notification.empty': 'You have viewed all notifications.', 9 | 'component.globalHeader.message': 'Message', 10 | 'component.globalHeader.message.empty': 'You have viewed all messsages.', 11 | 'component.globalHeader.event': 'Event', 12 | 'component.globalHeader.event.empty': 'You have viewed all events.', 13 | 'component.noticeIcon.clear': 'Clear', 14 | 'component.noticeIcon.cleared': 'Cleared', 15 | 'component.noticeIcon.empty': 'No notifications', 16 | 'component.noticeIcon.view-more': 'View more', 17 | }; 18 | -------------------------------------------------------------------------------- /src/locales/en-US/menu.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'menu.welcome': 'Welcome', 3 | 'menu.more-blocks': 'More Blocks', 4 | 'menu.home': 'Home', 5 | 'menu.admin': 'Admin', 6 | 'menu.admin.sub-page': 'Sub-Page', 7 | 'menu.login': 'Login', 8 | 'menu.register': 'Register', 9 | 'menu.register-result': 'Register Result', 10 | 'menu.dashboard': 'Dashboard', 11 | 'menu.dashboard.analysis': 'Analysis', 12 | 'menu.dashboard.monitor': 'Monitor', 13 | 'menu.dashboard.workplace': 'Workplace', 14 | 'menu.exception.403': '403', 15 | 'menu.exception.404': '404', 16 | 'menu.exception.500': '500', 17 | 'menu.form': 'Form', 18 | 'menu.form.basic-form': 'Basic Form', 19 | 'menu.form.step-form': 'Step Form', 20 | 'menu.form.step-form.info': 'Step Form(write transfer information)', 21 | 'menu.form.step-form.confirm': 'Step Form(confirm transfer information)', 22 | 'menu.form.step-form.result': 'Step Form(finished)', 23 | 'menu.form.advanced-form': 'Advanced Form', 24 | 'menu.list': 'List', 25 | 'menu.list.table-list': 'Search Table', 26 | 'menu.list.account-list': 'Account Manage', 27 | 'menu.list.domain-list': 'Domain Manage', 28 | 'menu.list.banned-word-list': 'Banned Word Manage', 29 | 'menu.list.user-list': 'User Manage', 30 | 'menu.api-doc': 'API DOC', 31 | 'menu.task-list': 'Task Search', 32 | 'menu.draw-test': 'Draw Test', 33 | 'menu.setting': 'System Setting', 34 | 'menu.probe': 'Log Probe', 35 | 'menu.activate': 'System Activate', 36 | 'menu.list.basic-list': 'Basic List', 37 | 'menu.list.card-list': 'Card List', 38 | 'menu.list.search-list': 'Search List', 39 | 'menu.list.search-list.articles': 'Search List(articles)', 40 | 'menu.list.search-list.projects': 'Search List(projects)', 41 | 'menu.list.search-list.applications': 'Search List(applications)', 42 | 'menu.profile': 'Profile', 43 | 'menu.profile.basic': 'Basic Profile', 44 | 'menu.profile.advanced': 'Advanced Profile', 45 | 'menu.result': 'Result', 46 | 'menu.result.success': 'Success', 47 | 'menu.result.fail': 'Fail', 48 | 'menu.exception': 'Exception', 49 | 'menu.exception.not-permission': '403', 50 | 'menu.exception.not-find': '404', 51 | 'menu.exception.server-error': '500', 52 | 'menu.exception.trigger': 'Trigger', 53 | 'menu.account': 'Account', 54 | 'menu.account.center': 'Account Center', 55 | 'menu.account.settings': 'Account Settings', 56 | 'menu.account.trigger': 'Trigger Error', 57 | 'menu.account.logout': 'Logout', 58 | 'menu.editor': 'Graphic Editor', 59 | 'menu.editor.flow': 'Flow Editor', 60 | 'menu.editor.mind': 'Mind Editor', 61 | 'menu.editor.koni': 'Koni Editor', 62 | }; 63 | -------------------------------------------------------------------------------- /src/locales/en-US/pwa.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.pwa.offline': 'You are offline now', 3 | 'app.pwa.serviceworker.updated': 'New content is available', 4 | 'app.pwa.serviceworker.updated.hint': 'Please press the "Refresh" button to reload current page', 5 | 'app.pwa.serviceworker.updated.ok': 'Refresh', 6 | }; 7 | -------------------------------------------------------------------------------- /src/locales/en-US/settingDrawer.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.setting.pagestyle': 'Page style setting', 3 | 'app.setting.pagestyle.dark': 'Dark style', 4 | 'app.setting.pagestyle.light': 'Light style', 5 | 'app.setting.content-width': 'Content Width', 6 | 'app.setting.content-width.fixed': 'Fixed', 7 | 'app.setting.content-width.fluid': 'Fluid', 8 | 'app.setting.themecolor': 'Theme Color', 9 | 'app.setting.themecolor.dust': 'Dust Red', 10 | 'app.setting.themecolor.volcano': 'Volcano', 11 | 'app.setting.themecolor.sunset': 'Sunset Orange', 12 | 'app.setting.themecolor.cyan': 'Cyan', 13 | 'app.setting.themecolor.green': 'Polar Green', 14 | 'app.setting.themecolor.daybreak': 'Daybreak Blue (default)', 15 | 'app.setting.themecolor.geekblue': 'Geek Glue', 16 | 'app.setting.themecolor.purple': 'Golden Purple', 17 | 'app.setting.navigationmode': 'Navigation Mode', 18 | 'app.setting.sidemenu': 'Side Menu Layout', 19 | 'app.setting.topmenu': 'Top Menu Layout', 20 | 'app.setting.fixedheader': 'Fixed Header', 21 | 'app.setting.fixedsidebar': 'Fixed Sidebar', 22 | 'app.setting.fixedsidebar.hint': 'Works on Side Menu Layout', 23 | 'app.setting.hideheader': 'Hidden Header when scrolling', 24 | 'app.setting.hideheader.hint': 'Works when Hidden Header is enabled', 25 | 'app.setting.othersettings': 'Other Settings', 26 | 'app.setting.weakmode': 'Weak Mode', 27 | 'app.setting.copy': 'Copy Setting', 28 | 'app.setting.copyinfo': 'copy success,please replace defaultSettings in src/models/setting.js', 29 | 'app.setting.production.hint': 30 | 'Setting panel shows in development environment only, please manually modify', 31 | }; 32 | -------------------------------------------------------------------------------- /src/locales/en-US/settings.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.settings.menuMap.basic': 'Basic Settings', 3 | 'app.settings.menuMap.security': 'Security Settings', 4 | 'app.settings.menuMap.binding': 'Account Binding', 5 | 'app.settings.menuMap.notification': 'New Message Notification', 6 | 'app.settings.basic.avatar': 'Avatar', 7 | 'app.settings.basic.change-avatar': 'Change avatar', 8 | 'app.settings.basic.email': 'Email', 9 | 'app.settings.basic.email-message': 'Please input your email!', 10 | 'app.settings.basic.nickname': 'Nickname', 11 | 'app.settings.basic.nickname-message': 'Please input your Nickname!', 12 | 'app.settings.basic.profile': 'Personal profile', 13 | 'app.settings.basic.profile-message': 'Please input your personal profile!', 14 | 'app.settings.basic.profile-placeholder': 'Brief introduction to yourself', 15 | 'app.settings.basic.country': 'Country/Region', 16 | 'app.settings.basic.country-message': 'Please input your country!', 17 | 'app.settings.basic.geographic': 'Province or city', 18 | 'app.settings.basic.geographic-message': 'Please input your geographic info!', 19 | 'app.settings.basic.address': 'Street Address', 20 | 'app.settings.basic.address-message': 'Please input your address!', 21 | 'app.settings.basic.phone': 'Phone Number', 22 | 'app.settings.basic.phone-message': 'Please input your phone!', 23 | 'app.settings.basic.update': 'Update Information', 24 | 'app.settings.security.strong': 'Strong', 25 | 'app.settings.security.medium': 'Medium', 26 | 'app.settings.security.weak': 'Weak', 27 | 'app.settings.security.password': 'Account Password', 28 | 'app.settings.security.password-description': 'Current password strength', 29 | 'app.settings.security.phone': 'Security Phone', 30 | 'app.settings.security.phone-description': 'Bound phone', 31 | 'app.settings.security.question': 'Security Question', 32 | 'app.settings.security.question-description': 33 | 'The security question is not set, and the security policy can effectively protect the account security', 34 | 'app.settings.security.email': 'Backup Email', 35 | 'app.settings.security.email-description': 'Bound Email', 36 | 'app.settings.security.mfa': 'MFA Device', 37 | 'app.settings.security.mfa-description': 38 | 'Unbound MFA device, after binding, can be confirmed twice', 39 | 'app.settings.security.modify': 'Modify', 40 | 'app.settings.security.set': 'Set', 41 | 'app.settings.security.bind': 'Bind', 42 | 'app.settings.binding.taobao': 'Binding Taobao', 43 | 'app.settings.binding.taobao-description': 'Currently unbound Taobao account', 44 | 'app.settings.binding.alipay': 'Binding Alipay', 45 | 'app.settings.binding.alipay-description': 'Currently unbound Alipay account', 46 | 'app.settings.binding.dingding': 'Binding DingTalk', 47 | 'app.settings.binding.dingding-description': 'Currently unbound DingTalk account', 48 | 'app.settings.binding.bind': 'Bind', 49 | 'app.settings.notification.password': 'Account Password', 50 | 'app.settings.notification.password-description': 51 | 'Messages from other users will be notified in the form of a station letter', 52 | 'app.settings.notification.messages': 'System Messages', 53 | 'app.settings.notification.messages-description': 54 | 'System messages will be notified in the form of a station letter', 55 | 'app.settings.notification.todo': 'To-do Notification', 56 | 'app.settings.notification.todo-description': 57 | 'The to-do list will be notified in the form of a letter from the station', 58 | 'app.settings.open': 'Open', 59 | 'app.settings.close': 'Close', 60 | }; 61 | -------------------------------------------------------------------------------- /src/locales/zh-CN.ts: -------------------------------------------------------------------------------- 1 | import component from './zh-CN/component'; 2 | import globalHeader from './zh-CN/globalHeader'; 3 | import menu from './zh-CN/menu'; 4 | import pages from './zh-CN/pages'; 5 | import pwa from './zh-CN/pwa'; 6 | import settingDrawer from './zh-CN/settingDrawer'; 7 | import settings from './zh-CN/settings'; 8 | 9 | export default { 10 | 'navBar.lang': '语言', 11 | 'layout.user.link.help': '帮助', 12 | 'layout.user.link.privacy': '隐私', 13 | 'layout.user.link.terms': '条款', 14 | 'app.copyright.produced': 'Midjourney Proxy Admin', 15 | 'app.preview.down.block': '下载此页面到本地项目', 16 | 'app.welcome.link.fetch-blocks': '获取全部区块', 17 | 'app.welcome.link.block-list': '基于 block 开发,快速构建标准页面', 18 | ...pages, 19 | ...globalHeader, 20 | ...menu, 21 | ...settingDrawer, 22 | ...settings, 23 | ...pwa, 24 | ...component, 25 | }; 26 | -------------------------------------------------------------------------------- /src/locales/zh-CN/component.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'component.tagSelect.expand': '展开', 3 | 'component.tagSelect.collapse': '收起', 4 | 'component.tagSelect.all': '全部', 5 | }; 6 | -------------------------------------------------------------------------------- /src/locales/zh-CN/globalHeader.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'component.globalHeader.search': '站内搜索', 3 | 'component.globalHeader.search.example1': '搜索提示一', 4 | 'component.globalHeader.search.example2': '搜索提示二', 5 | 'component.globalHeader.search.example3': '搜索提示三', 6 | 'component.globalHeader.help': '使用文档', 7 | 'component.globalHeader.notification': '通知', 8 | 'component.globalHeader.notification.empty': '你已查看所有通知', 9 | 'component.globalHeader.message': '消息', 10 | 'component.globalHeader.message.empty': '您已读完所有消息', 11 | 'component.globalHeader.event': '待办', 12 | 'component.globalHeader.event.empty': '你已完成所有待办', 13 | 'component.noticeIcon.clear': '清空', 14 | 'component.noticeIcon.cleared': '清空了', 15 | 'component.noticeIcon.empty': '暂无数据', 16 | 'component.noticeIcon.view-more': '查看更多', 17 | }; 18 | -------------------------------------------------------------------------------- /src/locales/zh-CN/menu.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'menu.welcome': '欢迎', 3 | 'menu.more-blocks': '更多区块', 4 | 'menu.home': '首页', 5 | 'menu.admin': '管理页', 6 | 'menu.admin.sub-page': '二级管理页', 7 | 'menu.login': '登录', 8 | 'menu.register': '注册', 9 | 'menu.register-result': '注册结果', 10 | 'menu.dashboard': 'Dashboard', 11 | 'menu.dashboard.analysis': '分析页', 12 | 'menu.dashboard.monitor': '监控页', 13 | 'menu.dashboard.workplace': '工作台', 14 | 'menu.exception.403': '403', 15 | 'menu.exception.404': '404', 16 | 'menu.exception.500': '500', 17 | 'menu.form': '表单页', 18 | 'menu.form.basic-form': '基础表单', 19 | 'menu.form.step-form': '分步表单', 20 | 'menu.form.step-form.info': '分步表单(填写转账信息)', 21 | 'menu.form.step-form.confirm': '分步表单(确认转账信息)', 22 | 'menu.form.step-form.result': '分步表单(完成)', 23 | 'menu.form.advanced-form': '高级表单', 24 | 'menu.list': '列表页', 25 | 'menu.list.table-list': '查询表格', 26 | 'menu.list.account-list': '账号管理', 27 | 'menu.list.domain-list': '领域管理', 28 | 'menu.list.banned-word-list': '违规词条', 29 | 'menu.list.user-list': '用户管理', 30 | 'menu.task-list': '任务查询', 31 | 'menu.draw-test': '绘画测试', 32 | 'menu.setting': '系统设置', 33 | 'menu.probe': '日志查看', 34 | 'menu.activate': '服务激活', 35 | 'menu.api-doc': 'API文档', 36 | 'menu.list.basic-list': '标准列表', 37 | 'menu.list.card-list': '卡片列表', 38 | 'menu.list.search-list': '搜索列表', 39 | 'menu.list.search-list.articles': '搜索列表(文章)', 40 | 'menu.list.search-list.projects': '搜索列表(项目)', 41 | 'menu.list.search-list.applications': '搜索列表(应用)', 42 | 'menu.profile': '详情页', 43 | 'menu.profile.basic': '基础详情页', 44 | 'menu.profile.advanced': '高级详情页', 45 | 'menu.result': '结果页', 46 | 'menu.result.success': '成功页', 47 | 'menu.result.fail': '失败页', 48 | 'menu.exception': '异常页', 49 | 'menu.exception.not-permission': '403', 50 | 'menu.exception.not-find': '404', 51 | 'menu.exception.server-error': '500', 52 | 'menu.exception.trigger': '触发错误', 53 | 'menu.account': '个人页', 54 | 'menu.account.center': '个人中心', 55 | 'menu.account.settings': '个人设置', 56 | 'menu.account.trigger': '触发报错', 57 | 'menu.account.logout': '退出登录', 58 | 'menu.editor': '图形编辑器', 59 | 'menu.editor.flow': '流程编辑器', 60 | 'menu.editor.mind': '脑图编辑器', 61 | 'menu.editor.koni': '拓扑编辑器', 62 | }; 63 | -------------------------------------------------------------------------------- /src/locales/zh-CN/pwa.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.pwa.offline': '当前处于离线状态', 3 | 'app.pwa.serviceworker.updated': '有新内容', 4 | 'app.pwa.serviceworker.updated.hint': '请点击“刷新”按钮或者手动刷新页面', 5 | 'app.pwa.serviceworker.updated.ok': '刷新', 6 | }; 7 | -------------------------------------------------------------------------------- /src/locales/zh-CN/settingDrawer.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.setting.pagestyle': '整体风格设置', 3 | 'app.setting.pagestyle.dark': '暗色菜单风格', 4 | 'app.setting.pagestyle.light': '亮色菜单风格', 5 | 'app.setting.content-width': '内容区域宽度', 6 | 'app.setting.content-width.fixed': '定宽', 7 | 'app.setting.content-width.fluid': '流式', 8 | 'app.setting.themecolor': '主题色', 9 | 'app.setting.themecolor.dust': '薄暮', 10 | 'app.setting.themecolor.volcano': '火山', 11 | 'app.setting.themecolor.sunset': '日暮', 12 | 'app.setting.themecolor.cyan': '明青', 13 | 'app.setting.themecolor.green': '极光绿', 14 | 'app.setting.themecolor.daybreak': '拂晓蓝(默认)', 15 | 'app.setting.themecolor.geekblue': '极客蓝', 16 | 'app.setting.themecolor.purple': '酱紫', 17 | 'app.setting.navigationmode': '导航模式', 18 | 'app.setting.sidemenu': '侧边菜单布局', 19 | 'app.setting.topmenu': '顶部菜单布局', 20 | 'app.setting.fixedheader': '固定 Header', 21 | 'app.setting.fixedsidebar': '固定侧边菜单', 22 | 'app.setting.fixedsidebar.hint': '侧边菜单布局时可配置', 23 | 'app.setting.hideheader': '下滑时隐藏 Header', 24 | 'app.setting.hideheader.hint': '固定 Header 时可配置', 25 | 'app.setting.othersettings': '其他设置', 26 | 'app.setting.weakmode': '色弱模式', 27 | 'app.setting.copy': '拷贝设置', 28 | 'app.setting.copyinfo': '拷贝成功,请到 config/defaultSettings.js 中替换默认配置', 29 | 'app.setting.production.hint': 30 | '配置栏只在开发环境用于预览,生产环境不会展现,请拷贝后手动修改配置文件', 31 | }; 32 | -------------------------------------------------------------------------------- /src/locales/zh-CN/settings.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.settings.menuMap.basic': '基本设置', 3 | 'app.settings.menuMap.security': '安全设置', 4 | 'app.settings.menuMap.binding': '账号绑定', 5 | 'app.settings.menuMap.notification': '新消息通知', 6 | 'app.settings.basic.avatar': '头像', 7 | 'app.settings.basic.change-avatar': '更换头像', 8 | 'app.settings.basic.email': '邮箱', 9 | 'app.settings.basic.email-message': '请输入您的邮箱!', 10 | 'app.settings.basic.nickname': '昵称', 11 | 'app.settings.basic.nickname-message': '请输入您的昵称!', 12 | 'app.settings.basic.profile': '个人简介', 13 | 'app.settings.basic.profile-message': '请输入个人简介!', 14 | 'app.settings.basic.profile-placeholder': '个人简介', 15 | 'app.settings.basic.country': '国家/地区', 16 | 'app.settings.basic.country-message': '请输入您的国家或地区!', 17 | 'app.settings.basic.geographic': '所在省市', 18 | 'app.settings.basic.geographic-message': '请输入您的所在省市!', 19 | 'app.settings.basic.address': '街道地址', 20 | 'app.settings.basic.address-message': '请输入您的街道地址!', 21 | 'app.settings.basic.phone': '联系电话', 22 | 'app.settings.basic.phone-message': '请输入您的联系电话!', 23 | 'app.settings.basic.update': '更新基本信息', 24 | 'app.settings.security.strong': '强', 25 | 'app.settings.security.medium': '中', 26 | 'app.settings.security.weak': '弱', 27 | 'app.settings.security.password': '账户密码', 28 | 'app.settings.security.password-description': '当前密码强度', 29 | 'app.settings.security.phone': '密保手机', 30 | 'app.settings.security.phone-description': '已绑定手机', 31 | 'app.settings.security.question': '密保问题', 32 | 'app.settings.security.question-description': '未设置密保问题,密保问题可有效保护账户安全', 33 | 'app.settings.security.email': '备用邮箱', 34 | 'app.settings.security.email-description': '已绑定邮箱', 35 | 'app.settings.security.mfa': 'MFA 设备', 36 | 'app.settings.security.mfa-description': '未绑定 MFA 设备,绑定后,可以进行二次确认', 37 | 'app.settings.security.modify': '修改', 38 | 'app.settings.security.set': '设置', 39 | 'app.settings.security.bind': '绑定', 40 | 'app.settings.binding.taobao': '绑定淘宝', 41 | 'app.settings.binding.taobao-description': '当前未绑定淘宝账号', 42 | 'app.settings.binding.alipay': '绑定支付宝', 43 | 'app.settings.binding.alipay-description': '当前未绑定支付宝账号', 44 | 'app.settings.binding.dingding': '绑定钉钉', 45 | 'app.settings.binding.dingding-description': '当前未绑定钉钉账号', 46 | 'app.settings.binding.bind': '绑定', 47 | 'app.settings.notification.password': '账户密码', 48 | 'app.settings.notification.password-description': '其他用户的消息将以站内信的形式通知', 49 | 'app.settings.notification.messages': '系统消息', 50 | 'app.settings.notification.messages-description': '系统消息将以站内信的形式通知', 51 | 'app.settings.notification.todo': '待办任务', 52 | 'app.settings.notification.todo-description': '待办任务将以站内信的形式通知', 53 | 'app.settings.open': '开', 54 | 'app.settings.close': '关', 55 | }; 56 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Ant Design Pro", 3 | "short_name": "Ant Design Pro", 4 | "display": "standalone", 5 | "start_url": "./?utm_source=homescreen", 6 | "theme_color": "#002140", 7 | "background_color": "#001529", 8 | "icons": [ 9 | { 10 | "src": "icons/icon-192x192.png", 11 | "sizes": "192x192" 12 | }, 13 | { 14 | "src": "icons/icon-128x128.png", 15 | "sizes": "128x128" 16 | }, 17 | { 18 | "src": "icons/icon-512x512.png", 19 | "sizes": "512x512" 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /src/pages/404.tsx: -------------------------------------------------------------------------------- 1 | import { history } from '@umijs/max'; 2 | import { Button, Result } from 'antd'; 3 | import React from 'react'; 4 | 5 | const NoFoundPage: React.FC = () => ( 6 | history.push('/')}> 12 | Back Home 13 | 14 | } 15 | /> 16 | ); 17 | 18 | export default NoFoundPage; 19 | -------------------------------------------------------------------------------- /src/pages/AccountList/components/button/DelButton.tsx: -------------------------------------------------------------------------------- 1 | import { deleteAccount } from '@/services/mj/api'; 2 | import { Button, Popconfirm, notification } from 'antd'; 3 | import { DeleteOutlined } from '@ant-design/icons'; 4 | import React, { useState } from 'react'; 5 | import { useIntl } from '@umijs/max'; 6 | 7 | // 定义 DelButton 接受的 props 类型 8 | interface DelButtonProps { 9 | record: Record; 10 | onSuccess: () => void; 11 | } 12 | 13 | const DelButton: React.FC = ({ record, onSuccess }) => { 14 | const [open, setOpen] = useState(false); 15 | const [confirmLoading, setConfirmLoading] = useState(false); 16 | const [api, contextHolder] = notification.useNotification(); 17 | const intl = useIntl(); 18 | 19 | const showPopconfirm = () => { 20 | setOpen(true); 21 | }; 22 | 23 | const handleOk = async () => { 24 | setConfirmLoading(true); 25 | try { 26 | const res = await deleteAccount(record.id); 27 | setOpen(false); 28 | if (res.success) { 29 | api.success({ 30 | message: 'success', 31 | description: intl.formatMessage({ id: 'pages.account.deleteSuccess' }) 32 | }); 33 | onSuccess(); 34 | } else { 35 | api.error({ 36 | message: 'error', 37 | description: res.message 38 | }); 39 | } 40 | } catch (error) { 41 | console.error(error); 42 | } finally { 43 | setConfirmLoading(false); 44 | } 45 | }; 46 | 47 | const handleCancel = () => { 48 | setOpen(false); 49 | }; 50 | 51 | return ( 52 | 60 | {contextHolder} 61 | 63 | 64 | ); 65 | }; 66 | 67 | export default DelButton; 68 | -------------------------------------------------------------------------------- /src/pages/AccountList/components/button/SyncButton.tsx: -------------------------------------------------------------------------------- 1 | import { refreshAccount } from '@/services/mj/api'; 2 | import { SyncOutlined } from '@ant-design/icons'; 3 | import { useIntl } from '@umijs/max'; 4 | import { Button, notification, Popconfirm } from 'antd'; 5 | import React, { useState } from 'react'; 6 | 7 | interface SyncButtonProps { 8 | record: Record; 9 | onSuccess: () => void; 10 | } 11 | 12 | const SyncButton: React.FC = ({ record, onSuccess }) => { 13 | const [open, setOpen] = useState(false); 14 | const [confirmLoading, setConfirmLoading] = useState(false); 15 | const [api, contextHolder] = notification.useNotification(); 16 | const intl = useIntl(); 17 | 18 | const showPopconfirm = () => { 19 | setOpen(true); 20 | }; 21 | 22 | const handleOk = async () => { 23 | setConfirmLoading(true); 24 | try { 25 | const res = await refreshAccount(record.id); 26 | console.log('resss', res); 27 | 28 | setOpen(false); 29 | if (res.success) { 30 | api.success({ 31 | message: 'success', 32 | description: intl.formatMessage({ id: 'pages.account.syncSuccess' }), 33 | }); 34 | onSuccess(); 35 | } else { 36 | api.error({ 37 | message: 'error', 38 | description: res.message, 39 | }); 40 | onSuccess(); 41 | } 42 | } catch (error) { 43 | console.error(error); 44 | } finally { 45 | setConfirmLoading(false); 46 | } 47 | }; 48 | 49 | const handleCancel = () => { 50 | setOpen(false); 51 | }; 52 | 53 | return ( 54 | 62 | {contextHolder} 63 | 66 | 67 | ); 68 | }; 69 | 70 | export default SyncButton; 71 | -------------------------------------------------------------------------------- /src/pages/AccountList/components/contents/CfContent.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Col, Form, FormInstance, notification, Row, Space } from 'antd'; 2 | import { useEffect, useState } from 'react'; 3 | 4 | import { accountCfOk, accountCfUrl } from '@/services/mj/api'; 5 | import { useIntl } from '@umijs/max'; 6 | 7 | const CfContent = ({ 8 | form, 9 | onSubmit, 10 | record, 11 | }: { 12 | form: FormInstance; 13 | onSubmit: (values: any) => void; 14 | record: Record; 15 | }) => { 16 | const intl = useIntl(); 17 | 18 | const [api, contextHolder] = notification.useNotification(); 19 | const [data, setData] = useState>(); 20 | const [loading, setLoading] = useState(false); 21 | 22 | // 当组件挂载或者record更新时,设置表单的初始值 23 | useEffect(() => { 24 | setData(record); 25 | }, []); 26 | 27 | const updateUrl = async () => { 28 | setLoading(true); 29 | const res = await accountCfUrl(record.id); 30 | setLoading(false); 31 | if (res.success) { 32 | setData(res.data); 33 | } 34 | }; 35 | 36 | const updateOk = async () => { 37 | setLoading(true); 38 | const res = await accountCfOk(record.id); 39 | setLoading(false); 40 | if (res.success) { 41 | api.success({ 42 | message: 'success', 43 | description: 'Success', 44 | }); 45 | onSubmit(data); 46 | } 47 | }; 48 | 49 | return ( 50 |
58 | {contextHolder} 59 | 60 | 61 | {data && ( 62 | 63 | {data?.cfUrl || '-'} 64 | 65 | )} 66 | 67 |
68 | 69 | 72 | 73 | 76 | 77 | 78 |
79 |
80 | ); 81 | }; 82 | 83 | export default CfContent; 84 | -------------------------------------------------------------------------------- /src/pages/AccountList/components/contents/MoreContent.tsx: -------------------------------------------------------------------------------- 1 | import { accountAction, accountChangeVersion } from '@/services/mj/api'; 2 | import { useIntl } from '@umijs/max'; 3 | import { Button, Card, Descriptions, notification, Select, Space, Tag, Tooltip } from 'antd'; 4 | import moment from 'moment'; 5 | import React, { useEffect, useState } from 'react'; 6 | 7 | const { Option } = Select; 8 | 9 | interface MoreContentProps { 10 | record: Record; 11 | onSuccess: () => void; 12 | } 13 | 14 | const MoreContent: React.FC = ({ record, onSuccess }) => { 15 | const [api, contextHolder] = notification.useNotification(); 16 | const [version, setVersion] = useState(''); 17 | const [buttons, setButtons] = useState>([]); 18 | const [nijiButtons, setNijiButtons] = useState>([]); 19 | const [loading, setLoading] = useState(false); 20 | const [loadingButton, setLoadingButton] = useState(''); 21 | const intl = useIntl(); 22 | 23 | useEffect(() => { 24 | setVersion(record.version); 25 | setButtons(record.buttons); 26 | setNijiButtons(record.nijiButtons); 27 | }, [record]); 28 | 29 | const getStatusTag = (enable: boolean, enableText: string, disableText: string) => { 30 | let color = enable ? 'green' : 'volcano'; 31 | let text = enable ? enableText : disableText; 32 | return {text}; 33 | }; 34 | 35 | const getTooltip = (text: string) => { 36 | if (!text || text.length < 25) return text; 37 | return {text.substring(0, 25) + '...'}; 38 | }; 39 | 40 | const changeDate = (date: string) => { 41 | return moment(date).format('YYYY-MM-DD HH:mm'); 42 | }; 43 | 44 | const versionSelectorOptions = () => { 45 | return record.versionSelector?.map((option: any) => { 46 | return ( 47 | 50 | ); 51 | }); 52 | }; 53 | 54 | const accountButtons = () => { 55 | return buttons.map((button: any) => { 56 | return ( 57 | 68 | ); 69 | }); 70 | }; 71 | 72 | const accountNijiButtons = () => { 73 | return nijiButtons.map((button: any) => { 74 | return ( 75 | 86 | ); 87 | }); 88 | }; 89 | 90 | const versionChange = async (value: string) => { 91 | setVersion(value); 92 | setLoading(true); 93 | const res = await accountChangeVersion(record.id, value); 94 | if (res.success) { 95 | setLoading(false); 96 | api.success({ 97 | message: 'success', 98 | description: intl.formatMessage({ id: 'pages.account.mjVersionSuccess' }), 99 | }); 100 | onSuccess(); 101 | } else { 102 | setVersion(record.version); 103 | setLoading(false); 104 | api.error({ 105 | message: 'error', 106 | description: res.message, 107 | }); 108 | } 109 | }; 110 | 111 | const action = async (botType: string, customId: string) => { 112 | if (loadingButton !== '') return; 113 | setLoadingButton(botType + ':' + customId); 114 | const res = await accountAction(record.id, botType, customId); 115 | setLoadingButton(''); 116 | if (res.success) { 117 | onSuccess(); 118 | } else { 119 | api.error({ 120 | message: 'error', 121 | description: res.message, 122 | }); 123 | } 124 | }; 125 | 126 | return ( 127 | <> 128 | {contextHolder} 129 | 134 | 135 | 136 | {record.guildId} 137 | 138 | 139 | {record.channelId} 140 | 141 | {/* 142 | {record.name} 143 | */} 144 | 145 | {getTooltip(record.userToken)} 146 | 147 | 148 | {getTooltip(record.botToken)} 149 | 150 | {getTooltip(record.userAgent)} 151 | 152 | {getStatusTag( 153 | record.remixAutoSubmit, 154 | intl.formatMessage({ id: 'pages.yes' }), 155 | intl.formatMessage({ id: 'pages.no' }), 156 | )} 157 | 158 | 159 | {record.privateChannelId} 160 | 161 | 162 | {record.nijiBotChannelId} 163 | 164 | 165 | 166 | 167 | 172 | 173 | 174 | {getStatusTag( 175 | record.enable, 176 | intl.formatMessage({ id: 'pages.enable' }), 177 | intl.formatMessage({ id: 'pages.disable' }), 178 | )} 179 | 180 | 181 | {record['displays']['mode']} 182 | 183 | 184 | {record['displays']['nijiMode']} 185 | 186 | 187 | {record['displays']['subscribePlan']} 188 | 189 | 190 | {record['displays']['billedWay']} 191 | 192 | 193 | {record['displays']['renewDate']} 194 | 195 | 196 | {record.fastTimeRemaining} 197 | 198 | 199 | {record.relaxedUsage} 200 | 201 | 202 | {record.fastUsage} 203 | 204 | 205 | {record.turboUsage} 206 | 207 | 208 | {record.lifetimeUsage} 209 | 210 | 211 | 212 | 213 | 218 | 219 | 220 | {record.coreSize} 221 | 222 | 223 | {record.queueSize} 224 | 225 | 226 | {record.timeoutMinutes} {intl.formatMessage({ id: 'pages.minutes' })} 227 | 228 | 229 | {record.weight} 230 | 231 | 232 | {changeDate(record.dateCreated)} 233 | 234 | 235 | {getTooltip(record.remark)} 236 | 237 | 238 | {getTooltip(record.disabledReason)} 239 | 240 | 241 | 242 | 243 | 248 | 257 | 258 | {accountButtons()} 259 | 260 | 261 | 266 | 267 | {accountNijiButtons()} 268 | 269 | 270 | 271 | ); 272 | }; 273 | 274 | export default MoreContent; 275 | -------------------------------------------------------------------------------- /src/pages/AccountList/components/contents/UpdateContent.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Alert, 3 | Button, 4 | Card, 5 | Col, 6 | Form, 7 | FormInstance, 8 | Input, 9 | InputNumber, 10 | Modal, 11 | Row, 12 | Select, 13 | Switch, 14 | } from 'antd'; 15 | import { useEffect, useState } from 'react'; 16 | 17 | import { allDomain } from '@/services/mj/api'; 18 | import { FullscreenOutlined } from '@ant-design/icons'; 19 | import { useIntl } from '@umijs/max'; 20 | 21 | const UpdateContent = ({ 22 | form, 23 | onSubmit, 24 | record, 25 | r, 26 | }: { 27 | form: FormInstance; 28 | onSubmit: (values: any) => void; 29 | record: Record; 30 | r: any; 31 | }) => { 32 | const intl = useIntl(); 33 | 34 | // // 当组件挂载或者record更新时,设置表单的初始值 35 | // useEffect(() => { 36 | // form.setFieldsValue(record); 37 | // }); 38 | 39 | useEffect(() => { 40 | form.setFieldsValue(record); 41 | }, []); 42 | 43 | useEffect(() => { 44 | form.setFieldsValue(record); 45 | }, [r]); 46 | 47 | const [opts, setOpts] = useState([]); 48 | useEffect(() => { 49 | allDomain().then((res) => { 50 | if (res.success) { 51 | setOpts(res.data); 52 | } 53 | }); 54 | }, []); 55 | const [isModalVisible, setIsModalVisible] = useState(false); 56 | const [subChannels, setSubChannels] = useState(''); 57 | 58 | const showModal = () => { 59 | setIsModalVisible(true); 60 | }; 61 | 62 | const handleOk = () => { 63 | record.subChannels = subChannels.split('\n'); 64 | form.setFieldsValue({ subChannels: subChannels.split('\n') }); 65 | setIsModalVisible(false); 66 | }; 67 | 68 | const handleCancel = () => { 69 | setIsModalVisible(false); 70 | }; 71 | 72 | return ( 73 |
81 | 82 | 83 | 84 | 87 | 91 | 92 | 93 | 97 | 98 | 99 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 114 | 115 | 116 | 117 | 118 | 119 | 123 | 124 | 125 | 129 | 130 | 131 | 0 && ( 136 | 137 | {intl.formatMessage({ id: 'pages.account.dayDrawCount' })} {record.dayDrawCount} 138 | 139 | ) 140 | } 141 | > 142 | 143 | 144 | 145 | 146 | 147 | 148 | 152 | 153 | 154 | 158 | 159 | 160 | 164 | 165 | 166 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | { 192 | setSubChannels(form.getFieldValue('subChannels').join('\n')); 193 | showModal(); 194 | }} 195 | icon={} 196 | > 197 | } 198 | > 199 | 200 | 201 | 205 | 206 | 207 | 211 | 212 | 213 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 230 |
231 | 236 |
237 | { 243 | // 设置 form 的值 244 | setSubChannels(e.target.value); 245 | }} 246 | /> 247 |
248 |
249 | ); 250 | }; 251 | 252 | export default UpdateContent; 253 | -------------------------------------------------------------------------------- /src/pages/AccountList/index.less: -------------------------------------------------------------------------------- 1 | .tableToolbar { 2 | text-align: right; 3 | padding: 15px 0; 4 | } -------------------------------------------------------------------------------- /src/pages/BannedWordList/List/index.less: -------------------------------------------------------------------------------- 1 | .tableToolbar{ 2 | text-align: right; 3 | padding: 15px 0; 4 | } 5 | -------------------------------------------------------------------------------- /src/pages/BannedWordList/List/index.tsx: -------------------------------------------------------------------------------- 1 | import MyModal from '@/pages/components/Modal'; 2 | import AddContent from '@/pages/DomainList/components/AddContent'; 3 | import { createIllegalWord, deleteIllegalWord, queryIllegalWord } from '@/services/mj/api'; 4 | import { DeleteOutlined, EditOutlined, PlusOutlined } from '@ant-design/icons'; 5 | import { PageContainer, ProTable } from '@ant-design/pro-components'; 6 | import { useIntl } from '@umijs/max'; 7 | import { Button, Card, Form, notification, Popconfirm, Space, Switch } from 'antd'; 8 | import React, { useRef, useState } from 'react'; 9 | 10 | const UserList: React.FC = () => { 11 | // 初始化 dataSource 状态为空数组 12 | const [modalVisible, setModalVisible] = useState(false); 13 | const [modalContent, setModalContent] = useState({}); 14 | const [title, setTitle] = useState(''); 15 | const [footer, setFooter] = useState({}); 16 | const [modalWidth, setModalWidth] = useState(1000); 17 | const [form] = Form.useForm(); 18 | const intl = useIntl(); 19 | 20 | const [api, contextHolder] = notification.useNotification(); 21 | 22 | const actionRef = useRef(); 23 | 24 | const hideModal = () => { 25 | setModalContent({}); 26 | setFooter({}); 27 | setModalVisible(false); 28 | }; 29 | const [modalSubmitLoading, setModalSubmitLoading] = useState(false); 30 | const modalFooter = ( 31 | <> 32 | 35 | 43 | 44 | ); 45 | 46 | const handleAdd = async (values: Record) => { 47 | setModalSubmitLoading(true); 48 | 49 | // 判断如果 keywords 为空,则设置为空数组 50 | if (!values.keywords) { 51 | values.keywords = []; 52 | } 53 | 54 | // 如果是字符串,则转换为数组 55 | if (typeof values.keywords === 'string') { 56 | values.keywords = values.keywords.split(','); 57 | } 58 | 59 | const res = await createIllegalWord(values); 60 | if (res.success) { 61 | api.success({ 62 | message: 'success', 63 | description: res.message, 64 | }); 65 | hideModal(); 66 | actionRef.current?.reload(); 67 | } else { 68 | api.error({ 69 | message: 'error', 70 | description: res.message, 71 | }); 72 | } 73 | setModalSubmitLoading(false); 74 | }; 75 | 76 | const openModal = (title: string, content: any, footer: any, modalWidth: number) => { 77 | form.resetFields(); 78 | setTitle(title); 79 | setModalContent(content); 80 | setFooter(footer); 81 | setModalWidth(modalWidth); 82 | setModalVisible(true); 83 | }; 84 | 85 | const onDelete = async (id: string) => { 86 | try { 87 | const res = await deleteIllegalWord(id); 88 | if (res.success) { 89 | api.success({ 90 | message: 'success', 91 | description: intl.formatMessage({ id: 'pages.task.deleteSuccess' }), 92 | }); 93 | actionRef.current?.reload(); 94 | } else { 95 | api.error({ 96 | message: 'error', 97 | description: res.message, 98 | }); 99 | } 100 | } catch (error) { 101 | console.error(error); 102 | } finally { 103 | } 104 | }; 105 | 106 | const columns = [ 107 | { 108 | title: intl.formatMessage({ id: 'pages.word.name' }), 109 | dataIndex: 'name', 110 | width: 160, 111 | align: 'left', 112 | fixed: 'left', 113 | }, 114 | { 115 | title: intl.formatMessage({ id: 'pages.word.description' }), 116 | dataIndex: 'description', 117 | width: 160, 118 | align: 'left', 119 | }, 120 | { 121 | title: intl.formatMessage({ id: 'pages.word.keywords' }), 122 | dataIndex: 'keywords', 123 | width: 120, 124 | align: 'left', 125 | // 弹出显示纯文本 126 | render: (text, record) => { 127 | return {record?.keywords.length || '0'} {intl.formatMessage({ id: 'pages.word.keywords' })}; 128 | }, 129 | }, 130 | { 131 | title: intl.formatMessage({ id: 'pages.word.enable' }), 132 | dataIndex: 'enable', 133 | width: 80, 134 | showInfo: false, 135 | hideInSearch: true, 136 | align: 'left', 137 | render: (text, record) => { 138 | return ; 139 | }, 140 | }, 141 | { 142 | title: intl.formatMessage({ id: 'pages.word.weight' }), 143 | dataIndex: 'weight', 144 | width: 80, 145 | align: 'left', 146 | hideInSearch: true, 147 | }, 148 | { 149 | title: intl.formatMessage({ id: 'pages.word.createTime' }), 150 | dataIndex: 'createTimeFormat', 151 | width: 140, 152 | ellipsis: true, 153 | hideInSearch: true, 154 | }, 155 | { 156 | title: intl.formatMessage({ id: 'pages.word.updateTime' }), 157 | dataIndex: 'updateTimeFormat', 158 | width: 140, 159 | ellipsis: true, 160 | hideInSearch: true, 161 | }, 162 | { 163 | title: intl.formatMessage({ id: 'pages.operation' }), 164 | dataIndex: 'operation', 165 | width: 140, 166 | key: 'operation', 167 | fixed: 'right', 168 | align: 'center', 169 | hideInSearch: true, 170 | render: (_, record) => { 171 | return ( 172 | 173 | 191 | 192 | 193 | ); 194 | }, 195 | }, 196 | ]; 197 | 198 | return ( 199 | 200 | {contextHolder} 201 | 202 | [ 214 | , 229 | ]} 230 | request={async (params) => { 231 | // 如果 params.keywords 是 string 转为 array 232 | if (params.keywords) { 233 | if (typeof params.keywords === 'string') { 234 | params.keywords = params.keywords.split(','); 235 | } 236 | } 237 | const res = await queryIllegalWord({ ...params, pageNumber: params.current! - 1 }); 238 | return { 239 | data: res.list, 240 | total: res.pagination.total, 241 | success: true, 242 | }; 243 | }} 244 | /> 245 | 246 | 254 | 255 | ); 256 | }; 257 | 258 | export default UserList; 259 | -------------------------------------------------------------------------------- /src/pages/BannedWordList/components/AddContent.tsx: -------------------------------------------------------------------------------- 1 | import { Card, Col, Form, FormInstance, Input, InputNumber, Row, Switch } from 'antd'; 2 | import { useEffect, useState } from 'react'; 3 | 4 | import { useIntl } from '@umijs/max'; 5 | 6 | const UpdateContent = ({ 7 | form, 8 | onSubmit, 9 | record, 10 | }: { 11 | form: FormInstance; 12 | onSubmit: (values: any) => void; 13 | record: Record; 14 | }) => { 15 | const intl = useIntl(); 16 | const [disabledRole, setDisabledRole] = useState(false); 17 | 18 | // 当组件挂载或者record更新时,设置表单的初始值 19 | useEffect(() => { 20 | form.setFieldsValue(record); 21 | 22 | // 如果是管理员,则禁用角色选择 23 | if (record.id == 'admin') { 24 | setDisabledRole(true); 25 | } else { 26 | setDisabledRole(false); 27 | } 28 | console.log('record', record); 29 | }); 30 | 31 | return ( 32 |
40 | 41 | 42 | 43 | 46 | 47 | 48 | 49 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 67 | 72 | 73 | 74 | 75 | 76 |
77 | ); 78 | }; 79 | 80 | export default UpdateContent; 81 | -------------------------------------------------------------------------------- /src/pages/DomainList/List/index.less: -------------------------------------------------------------------------------- 1 | .tableToolbar{ 2 | text-align: right; 3 | padding: 15px 0; 4 | } 5 | -------------------------------------------------------------------------------- /src/pages/DomainList/List/index.tsx: -------------------------------------------------------------------------------- 1 | import MyModal from '@/pages/components/Modal'; 2 | import AddContent from '@/pages/DomainList/components/AddContent'; 3 | import { createDomain, deleteDomain, queryDomain } from '@/services/mj/api'; 4 | import { DeleteOutlined, EditOutlined, PlusOutlined } from '@ant-design/icons'; 5 | import { PageContainer, ProTable } from '@ant-design/pro-components'; 6 | import { useIntl } from '@umijs/max'; 7 | import { Button, Card, Form, notification, Popconfirm, Space, Switch } from 'antd'; 8 | import React, { useRef, useState } from 'react'; 9 | 10 | const UserList: React.FC = () => { 11 | // 初始化 dataSource 状态为空数组 12 | const [modalVisible, setModalVisible] = useState(false); 13 | const [modalContent, setModalContent] = useState({}); 14 | const [title, setTitle] = useState(''); 15 | const [footer, setFooter] = useState({}); 16 | const [modalWidth, setModalWidth] = useState(1000); 17 | const [form] = Form.useForm(); 18 | const intl = useIntl(); 19 | 20 | const [api, contextHolder] = notification.useNotification(); 21 | 22 | const actionRef = useRef(); 23 | 24 | const hideModal = () => { 25 | setModalContent({}); 26 | setFooter({}); 27 | setModalVisible(false); 28 | }; 29 | const [modalSubmitLoading, setModalSubmitLoading] = useState(false); 30 | const modalFooter = ( 31 | <> 32 | 35 | 43 | 44 | ); 45 | 46 | const handleAdd = async (values: Record) => { 47 | setModalSubmitLoading(true); 48 | 49 | // 判断如果 keywords 为空,则设置为空数组 50 | if (!values.keywords) { 51 | values.keywords = []; 52 | } 53 | 54 | // 如果是字符串,则转换为数组 55 | if (typeof values.keywords === 'string') { 56 | values.keywords = values.keywords.split(','); 57 | } 58 | 59 | const res = await createDomain(values); 60 | if (res.success) { 61 | api.success({ 62 | message: 'success', 63 | description: res.message, 64 | }); 65 | hideModal(); 66 | actionRef.current?.reload(); 67 | } else { 68 | api.error({ 69 | message: 'error', 70 | description: res.message, 71 | }); 72 | } 73 | setModalSubmitLoading(false); 74 | }; 75 | 76 | const openModal = (title: string, content: any, footer: any, modalWidth: number) => { 77 | form.resetFields(); 78 | setTitle(title); 79 | setModalContent(content); 80 | setFooter(footer); 81 | setModalWidth(modalWidth); 82 | setModalVisible(true); 83 | }; 84 | 85 | const onDelete = async (id: string) => { 86 | try { 87 | const res = await deleteDomain(id); 88 | if (res.success) { 89 | api.success({ 90 | message: 'success', 91 | description: intl.formatMessage({ id: 'pages.task.deleteSuccess' }), 92 | }); 93 | actionRef.current?.reload(); 94 | } else { 95 | api.error({ 96 | message: 'error', 97 | description: res.message, 98 | }); 99 | } 100 | } catch (error) { 101 | console.error(error); 102 | } finally { 103 | } 104 | }; 105 | 106 | const columns = [ 107 | { 108 | title: intl.formatMessage({ id: 'pages.domain.name' }), 109 | dataIndex: 'name', 110 | width: 160, 111 | align: 'left', 112 | fixed: 'left', 113 | }, 114 | { 115 | title: intl.formatMessage({ id: 'pages.domain.description' }), 116 | dataIndex: 'description', 117 | width: 160, 118 | align: 'left', 119 | }, 120 | { 121 | title: intl.formatMessage({ id: 'pages.domain.keywords' }), 122 | dataIndex: 'keywords', 123 | width: 120, 124 | align: 'left', 125 | // 弹出显示纯文本 126 | render: (text, record) => { 127 | return {record?.keywords.length || '0'} {intl.formatMessage({ id: 'pages.domain.keywords' })}; 128 | }, 129 | }, 130 | { 131 | title: intl.formatMessage({ id: 'pages.domain.enable' }), 132 | dataIndex: 'enable', 133 | width: 80, 134 | showInfo: false, 135 | hideInSearch: true, 136 | align: 'left', 137 | render: (text, record) => { 138 | return ; 139 | }, 140 | }, 141 | { 142 | title: intl.formatMessage({ id: 'pages.domain.weight' }), 143 | dataIndex: 'weight', 144 | width: 80, 145 | align: 'left', 146 | hideInSearch: true, 147 | }, 148 | { 149 | title: intl.formatMessage({ id: 'pages.domain.createTime' }), 150 | dataIndex: 'createTimeFormat', 151 | width: 140, 152 | ellipsis: true, 153 | hideInSearch: true, 154 | }, 155 | { 156 | title: intl.formatMessage({ id: 'pages.domain.updateTime' }), 157 | dataIndex: 'updateTimeFormat', 158 | width: 140, 159 | ellipsis: true, 160 | hideInSearch: true, 161 | }, 162 | { 163 | title: intl.formatMessage({ id: 'pages.operation' }), 164 | dataIndex: 'operation', 165 | width: 140, 166 | key: 'operation', 167 | fixed: 'right', 168 | align: 'center', 169 | hideInSearch: true, 170 | render: (_, record) => { 171 | return ( 172 | 173 | 191 | 192 | 193 | ); 194 | }, 195 | }, 196 | ]; 197 | 198 | return ( 199 | 200 | {contextHolder} 201 | 202 | [ 214 | , 229 | ]} 230 | request={async (params) => { 231 | // 如果 params.keywords 是 string 转为 array 232 | if (params.keywords) { 233 | if (typeof params.keywords === 'string') { 234 | params.keywords = params.keywords.split(','); 235 | } 236 | } 237 | const res = await queryDomain({ ...params, pageNumber: params.current! - 1 }); 238 | return { 239 | data: res.list, 240 | total: res.pagination.total, 241 | success: true, 242 | }; 243 | }} 244 | /> 245 | 246 | 254 | 255 | ); 256 | }; 257 | 258 | export default UserList; 259 | -------------------------------------------------------------------------------- /src/pages/DomainList/components/AddContent.tsx: -------------------------------------------------------------------------------- 1 | import { Card, Col, Form, FormInstance, Input, InputNumber, Row, Switch } from 'antd'; 2 | import { useEffect, useState } from 'react'; 3 | 4 | import { useIntl } from '@umijs/max'; 5 | 6 | const UpdateContent = ({ 7 | form, 8 | onSubmit, 9 | record, 10 | }: { 11 | form: FormInstance; 12 | onSubmit: (values: any) => void; 13 | record: Record; 14 | }) => { 15 | const intl = useIntl(); 16 | const [disabledRole, setDisabledRole] = useState(false); 17 | 18 | // 当组件挂载或者record更新时,设置表单的初始值 19 | useEffect(() => { 20 | form.setFieldsValue(record); 21 | 22 | // 如果是管理员,则禁用角色选择 23 | if (record.id == 'admin') { 24 | setDisabledRole(true); 25 | } else { 26 | setDisabledRole(false); 27 | } 28 | console.log('record', record); 29 | }); 30 | 31 | return ( 32 |
40 | 41 | 42 | 43 | 46 | 47 | 48 | 49 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 67 | 72 | 73 | 74 | 75 | 76 |
77 | ); 78 | }; 79 | 80 | export default UpdateContent; 81 | -------------------------------------------------------------------------------- /src/pages/Draw/index.less: -------------------------------------------------------------------------------- 1 | .cardTitleTime { 2 | font-size: 13px; 3 | font-weight: normal; 4 | margin-left: 10px; 5 | color: gray; 6 | } 7 | 8 | .taskErrorTip { 9 | color: red; 10 | font-size: 15px; 11 | } 12 | -------------------------------------------------------------------------------- /src/pages/Probe/index.less: -------------------------------------------------------------------------------- 1 | // index.less 2 | .logCard { 3 | border-radius: 4px; // 设置边框圆角 4 | padding: 16px; // 设置内边距 5 | height: 75vh; // 设置卡片高度 6 | overflow-y: auto; // 设置垂直滚动 7 | } 8 | 9 | .logContent { 10 | white-space: pre-wrap; // 保留空白符和换行符 11 | font-family: monospace; // 设置字体为等宽字体 12 | font-size: 14px; // 设置字体大小 13 | color: #333; // 设置字体颜色 14 | } 15 | -------------------------------------------------------------------------------- /src/pages/Probe/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState, useRef } from 'react'; 2 | import { Card, Button, Select, Space } from 'antd'; 3 | import { FullscreenOutlined, FullscreenExitOutlined } from '@ant-design/icons'; 4 | import { PageContainer } from '@ant-design/pro-components'; 5 | import { probe } from "@/services/mj/api"; 6 | import styles from './index.less'; 7 | import { useIntl } from '@umijs/max'; 8 | 9 | const { Option } = Select; 10 | 11 | 12 | const Probe: React.FC = () => { 13 | const [logContent, setLogContent] = useState(''); 14 | const [refreshInterval, setRefreshInterval] = useState(5); 15 | const logCardRef = useRef(null); 16 | const intervalIdRef = useRef(null); 17 | const intl = useIntl(); 18 | const [level, setLevel] = useState(0); 19 | 20 | const fetchLogContent = async (l = 0) => { 21 | return await probe(1000, l); 22 | }; 23 | 24 | useEffect(() => { 25 | const tailLog = async () => { 26 | const res = await fetchLogContent(level); 27 | const logCardElement = logCardRef.current || document.body; 28 | const scrollN = logCardElement.scrollHeight - logCardElement.scrollTop - logCardElement.clientHeight; 29 | setLogContent(res); 30 | if (scrollN < 50) { 31 | logCardElement.scrollTop = logCardElement.scrollHeight - logCardElement.clientHeight; 32 | } 33 | }; 34 | 35 | tailLog().then(() => { 36 | setTimeout(() => { 37 | const logCardElement = logCardRef.current || document.body; 38 | logCardElement.scrollTop = logCardElement.scrollHeight - logCardElement.clientHeight; 39 | }, 100); 40 | }); 41 | 42 | if (intervalIdRef.current) { 43 | clearInterval(intervalIdRef.current); 44 | } 45 | 46 | if (refreshInterval > 0) { 47 | const newIntervalId = setInterval(tailLog, refreshInterval * 1000); 48 | intervalIdRef.current = newIntervalId; 49 | } 50 | 51 | return () => { 52 | if (intervalIdRef.current) { 53 | clearInterval(intervalIdRef.current); 54 | intervalIdRef.current = null; 55 | } 56 | }; 57 | }, [refreshInterval, level]); 58 | 59 | const handleRefreshChange = (value: number) => { 60 | setRefreshInterval(value); 61 | }; 62 | 63 | const handleLevelChange = (value: number) => { 64 | setLevel(value) 65 | }; 66 | 67 | const toggleFullscreen = () => { 68 | if (logCardRef.current) { 69 | if (document.fullscreenElement) { 70 | document.exitFullscreen(); 71 | } else { 72 | logCardRef.current.requestFullscreen(); 73 | } 74 | } 75 | }; 76 | 77 | return ( 78 | 79 | 80 | 84 | 90 | 94 | 95 | 96 |
97 |
{logContent}
98 |
99 |
100 |
101 | ); 102 | }; 103 | 104 | export default Probe; 105 | -------------------------------------------------------------------------------- /src/pages/Setting/index.less: -------------------------------------------------------------------------------- 1 | // index.less 2 | -------------------------------------------------------------------------------- /src/pages/Task/List/index.less: -------------------------------------------------------------------------------- 1 | .tableToolbar{ 2 | text-align: right; 3 | padding: 15px 0; 4 | } 5 | -------------------------------------------------------------------------------- /src/pages/Task/List/index.tsx: -------------------------------------------------------------------------------- 1 | import MyModal from '@/pages/components/Modal'; 2 | import TaskContent from '@/pages/Task/components/TaskContent'; 3 | import { deleteTask, queryTask } from '@/services/mj/api'; 4 | import { DeleteOutlined } from '@ant-design/icons'; 5 | import { PageContainer, ProTable } from '@ant-design/pro-components'; 6 | import { useIntl } from '@umijs/max'; 7 | import { Button, Card, Form, Image, notification, Popconfirm, Progress, Spin, Tag } from 'antd'; 8 | import React, { useRef, useState } from 'react'; 9 | 10 | const List: React.FC = () => { 11 | // 初始化 dataSource 状态为空数组 12 | const [modalVisible, setModalVisible] = useState(false); 13 | const [modalContent, setModalContent] = useState({}); 14 | const [title, setTitle] = useState(''); 15 | const [footer, setFooter] = useState({}); 16 | const [modalWidth, setModalWidth] = useState(1000); 17 | const [form] = Form.useForm(); 18 | const intl = useIntl(); 19 | 20 | const imagePrefix = sessionStorage.getItem('mj-image-prefix') || ''; 21 | 22 | const [api, contextHolder] = notification.useNotification(); 23 | 24 | const actionRef = useRef(); 25 | 26 | const hideModal = () => { 27 | setModalContent({}); 28 | setFooter({}); 29 | setModalVisible(false); 30 | }; 31 | 32 | const openModal = (title: string, content: any, footer: any, modalWidth: number) => { 33 | form.resetFields(); 34 | setTitle(title); 35 | setModalContent(content); 36 | setFooter(footer); 37 | setModalWidth(modalWidth); 38 | setModalVisible(true); 39 | }; 40 | 41 | const onDelete = async (id: string) => { 42 | try { 43 | const res = await deleteTask(id); 44 | if (res.success) { 45 | api.success({ 46 | message: 'success', 47 | description: intl.formatMessage({ id: 'pages.task.deleteSuccess' }), 48 | }); 49 | actionRef.current?.reload(); 50 | } else { 51 | api.error({ 52 | message: 'error', 53 | description: res.message, 54 | }); 55 | } 56 | } catch (error) { 57 | console.error(error); 58 | } finally { 59 | } 60 | }; 61 | 62 | const [loading, setLoading] = useState(true); 63 | 64 | const handleImageLoad = () => { 65 | setLoading(false); 66 | }; 67 | 68 | // get Video 69 | const getVideo = (url: string) => { 70 | if (!url) return url; 71 | return ( 72 | 78 | ); 79 | }; 80 | 81 | const columns = [ 82 | { 83 | title: 'ID', 84 | dataIndex: 'id', 85 | width: 160, 86 | align: 'center', 87 | fixed: 'left', 88 | render: (text, record) => ( 89 | 91 | openModal( 92 | intl.formatMessage({ id: 'pages.task.info' }), 93 | , 94 | null, 95 | 1100, 96 | ) 97 | } 98 | > 99 | {text} 100 | 101 | ), 102 | }, 103 | { 104 | title: intl.formatMessage({ id: 'pages.task.type' }), 105 | dataIndex: 'action', 106 | width: 140, 107 | align: 'center', 108 | request: async () => [ 109 | { 110 | label: 'Imagine', 111 | value: 'IMAGINE', 112 | }, 113 | { 114 | label: 'Upscale', 115 | value: 'UPSCALE', 116 | }, 117 | { 118 | label: 'Variation', 119 | value: 'VARIATION', 120 | }, 121 | { 122 | label: 'Zoom', 123 | value: 'ZOOM', 124 | }, 125 | { 126 | label: 'Pan', 127 | value: 'PAN', 128 | }, 129 | { 130 | label: 'Describe', 131 | value: 'DESCRIBE', 132 | }, 133 | { 134 | label: 'Blend', 135 | value: 'BLEND', 136 | }, 137 | { 138 | label: 'Shorten', 139 | value: 'SHORTEN', 140 | }, 141 | { 142 | label: 'SwapFace', 143 | value: 'SWAP_FACE', 144 | }, 145 | { 146 | label: 'SwapVideoFace', 147 | value: 'SWAP_VIDEO_FACE', 148 | }, 149 | ], 150 | render: (text, record) => record['displays']['action'], 151 | }, 152 | { 153 | title: intl.formatMessage({ id: 'pages.task.preview' }), 154 | dataIndex: 'imageUrl', 155 | width: 80, 156 | align: 'center', 157 | hideInSearch: true, 158 | render: (text, record, index) => { 159 | if (record.action === 'SWAP_VIDEO_FACE') { 160 | // 点击图片显示视频 161 | return ( 162 | record.thumbnailUrl && ( 163 | { 170 | window.open(record.imageUrl); 171 | }} 172 | loading="lazy" 173 | onLoad={handleImageLoad} 174 | placeholder={ 175 |
184 | 185 |
186 | } 187 | /> 188 | ) 189 | ); 190 | } 191 | 192 | return ( 193 | (record.thumbnailUrl || record.imageUrl) && ( 194 | 195 | , 203 | }} 204 | loading="lazy" 205 | onLoad={handleImageLoad} 206 | placeholder={ 207 |
216 | 217 |
218 | } 219 | /> 220 |
221 | ) 222 | ); 223 | }, 224 | }, 225 | { 226 | title: intl.formatMessage({ id: 'pages.task.instanceId' }), 227 | dataIndex: 'instanceId', 228 | width: 180, 229 | align: 'center', 230 | render: (text, record) => record['properties']['discordInstanceId'], 231 | }, 232 | { 233 | title: intl.formatMessage({ id: 'pages.task.submitTime' }), 234 | dataIndex: 'submitTime', 235 | width: 160, 236 | hideInSearch: true, 237 | align: 'center', 238 | render: (text, record) => record['displays']['submitTime'], 239 | }, 240 | 241 | { 242 | title: intl.formatMessage({ id: 'pages.task.status' }), 243 | dataIndex: 'status', 244 | width: 90, 245 | align: 'center', 246 | request: async () => [ 247 | { 248 | label: intl.formatMessage({ id: 'pages.task.NOT_START' }), 249 | value: 'NOT_START', 250 | }, 251 | { 252 | label: intl.formatMessage({ id: 'pages.task.SUBMITTED' }), 253 | value: 'SUBMITTED', 254 | }, 255 | { 256 | label: intl.formatMessage({ id: 'pages.task.MODAL' }), 257 | value: 'MODAL', 258 | }, 259 | { 260 | label: intl.formatMessage({ id: 'pages.task.IN_PROGRESS' }), 261 | value: 'IN_PROGRESS', 262 | }, 263 | { 264 | label: intl.formatMessage({ id: 'pages.task.FAILURE' }), 265 | value: 'FAILURE', 266 | }, 267 | { 268 | label: intl.formatMessage({ id: 'pages.task.SUCCESS' }), 269 | value: 'SUCCESS', 270 | }, 271 | { 272 | label: intl.formatMessage({ id: 'pages.task.CANCEL' }), 273 | value: 'CANCEL', 274 | }, 275 | ], 276 | render: (text, record) => { 277 | let color = 'default'; 278 | if (text == 'NOT_START') { 279 | color = 'default'; 280 | } else if (text == 'SUBMITTED') { 281 | color = 'lime'; 282 | } else if (text == 'MODAL') { 283 | color = 'warning'; 284 | } else if (text == 'IN_PROGRESS') { 285 | color = 'processing'; 286 | } else if (text == 'FAILURE') { 287 | color = 'error'; 288 | } else if (text == 'SUCCESS') { 289 | color = 'success'; 290 | } else if (text == 'CANCEL') { 291 | color = 'magenta'; 292 | } 293 | return {record['displays']['status']}; 294 | }, 295 | }, 296 | { 297 | title: intl.formatMessage({ id: 'pages.task.progress' }), 298 | dataIndex: 'progress', 299 | width: 130, 300 | showInfo: false, 301 | hideInSearch: true, 302 | render: (text, record) => { 303 | let percent = 0; 304 | if (text && text.indexOf('%') > 0) { 305 | percent = parseInt(text.substring(0, text.indexOf('%'))); 306 | } 307 | let status = 'normal'; 308 | if (record['status'] == 'SUCCESS') { 309 | status = 'success'; 310 | } else if (record['status'] == 'FAILURE') { 311 | status = 'exception'; 312 | } 313 | return ; 314 | }, 315 | }, 316 | { 317 | title: intl.formatMessage({ id: 'pages.task.description' }), 318 | dataIndex: 'description', 319 | width: 250, 320 | ellipsis: true, 321 | }, 322 | { 323 | title: intl.formatMessage({ id: 'pages.task.failReason' }), 324 | dataIndex: 'failReason', 325 | width: 220, 326 | ellipsis: true, 327 | }, 328 | { 329 | title: intl.formatMessage({ id: 'pages.operation' }), 330 | dataIndex: 'operation', 331 | width: 100, 332 | key: 'operation', 333 | fixed: 'right', 334 | align: 'center', 335 | hideInSearch: true, 336 | render: (_, record) => { 337 | return ( 338 | onDelete(record.id)} 342 | > 343 | 344 | 345 | ); 346 | }, 347 | }, 348 | ]; 349 | 350 | return ( 351 | 352 | {contextHolder} 353 | 354 | { 366 | const res = await queryTask({ ...params, pageNumber: params.current - 1 }); 367 | const images = res.list.map((item) => item.imageUrl || '').filter((item) => item != ''); 368 | const list = res.list; 369 | list.forEach((item, index) => { 370 | item.images = images; 371 | }); 372 | return { 373 | data: list, 374 | total: res.pagination.total, 375 | success: true, 376 | }; 377 | }} 378 | /> 379 | 380 | 388 | 389 | ); 390 | }; 391 | 392 | export default List; 393 | -------------------------------------------------------------------------------- /src/pages/Task/components/TaskContent.tsx: -------------------------------------------------------------------------------- 1 | import { useIntl } from '@umijs/max'; 2 | import { Card, Descriptions, Flex, Image, Progress, Spin, Tag, Tooltip } from 'antd'; 3 | 4 | const TaskContent = ({ record }: { record: Record }) => { 5 | const intl = useIntl(); 6 | const imagePrefix = sessionStorage.getItem('mj-image-prefix') || ''; 7 | 8 | const getStatusTag = (text: string) => { 9 | let color = 'default'; 10 | if (text == 'NOT_START') { 11 | color = 'default'; 12 | } else if (text == 'SUBMITTED') { 13 | color = 'lime'; 14 | } else if (text == 'MODAL') { 15 | color = 'warning'; 16 | } else if (text == 'IN_PROGRESS') { 17 | color = 'processing'; 18 | } else if (text == 'FAILURE') { 19 | color = 'error'; 20 | } else if (text == 'SUCCESS') { 21 | color = 'success'; 22 | } else if (text == 'CANCEL') { 23 | color = 'magenta'; 24 | } 25 | return {record['displays']['status']}; 26 | }; 27 | 28 | const getProgress = (text: string) => { 29 | let percent = 0; 30 | if (text && text.indexOf('%') > 0) { 31 | percent = parseInt(text.substring(0, text.indexOf('%'))); 32 | } 33 | let status = 'normal'; 34 | if (record['status'] == 'SUCCESS') { 35 | status = 'success'; 36 | } else if (record['status'] == 'FAILURE') { 37 | status = 'exception'; 38 | } 39 | return ( 40 |
41 | 42 |
43 | ); 44 | }; 45 | 46 | const getTooltip = (text: string) => { 47 | if (!text || text.length < 30) return text; 48 | return {text.substring(0, 30) + '...'}; 49 | }; 50 | 51 | const getImage = (url: string) => { 52 | if (!url) return url; 53 | return ( 54 | } 58 | /> 59 | ); 60 | }; 61 | 62 | // get Video 63 | const getVideo = (url: string) => { 64 | if (!url) return url; 65 | return ( 66 | 72 | ); 73 | }; 74 | 75 | const getBotTypeTag = (botType: string) => { 76 | if (botType == 'NIJI_JOURNEY') { 77 | return niji・journey; 78 | } else if ( 79 | botType == 'INSIGHT_FACE' || 80 | botType == 'FACE_SWAP' || 81 | botType == 'FACE_SWAP_VIDEO' 82 | ) { 83 | return InsightFace; 84 | } else if (botType == 'MID_JOURNEY') { 85 | return Midjourney; 86 | } else { 87 | return '-'; 88 | } 89 | }; 90 | 91 | const getModalTag = (enable: boolean) => { 92 | if (enable == null || !enable) return; 93 | return {intl.formatMessage({ id: 'pages.yes' })}; 94 | }; 95 | 96 | return ( 97 | <> 98 | 103 | 104 | {record.id} 105 | 106 | {record['displays']['action']} 107 | 108 | 109 | {getStatusTag(record.status)} 110 | 111 | 112 | {getProgress(record.progress)} 113 | 114 | 115 | {getTooltip(record.prompt)} 116 | 117 | 118 | {getTooltip(record.promptEn)} 119 | 120 | 121 | {getTooltip(record.description)} 122 | 123 | 124 | {record['displays']['submitTime']} 125 | 126 | 127 | {record['displays']['startTime']} 128 | 129 | 130 | {record['displays']['finishTime']} 131 | 132 | 133 | {getTooltip(record.failReason)} 134 | 135 | 136 | {getTooltip(record.state)} 137 | 138 | 139 | {record.action === 'SWAP_VIDEO_FACE' ? ( 140 | 141 | 142 | {/* 143 |
{getImage(record.replicateSource)}
144 |
{getVideo(record.replicateTarget)}
145 |
*/} 146 | {getVideo(record.imageUrl)} 147 |
148 |
149 | ) : ( 150 | 151 | {getImage(record.imageUrl)} 152 | 153 | )} 154 |
155 |
156 | 161 | 162 | 163 | {getBotTypeTag(record.botType)} 164 | 165 | {record.nonce} 166 | 167 | {record['properties']['discordInstanceId']} 168 | 169 | 170 | {record['properties']['discordInstanceId']} 171 | 172 | {record['properties']['messageHash']} 173 | 174 | {getTooltip(record['properties']['messageContent'])} 175 | 176 | 177 | {getTooltip(record['properties']['finalPrompt'])} 178 | 179 | 180 | {getTooltip(record.promptCn || '-')} 181 | 182 | 183 | {getTooltip(record['properties']['custom_id'] || '-')} 184 | 185 | 186 | {getModalTag(record['properties']['needModel']) || '-'} 187 | 188 | 189 | {record.seed || '-'} 190 | 191 | 192 | {getTooltip(record['properties']['notifyHook'] || '-')} 193 | 194 | 195 | {record.clientIp || '-'} 196 | 197 | 198 | 199 | 200 | ); 201 | }; 202 | 203 | export default TaskContent; 204 | -------------------------------------------------------------------------------- /src/pages/User/Login/index.tsx: -------------------------------------------------------------------------------- 1 | import { getIndex, login, register } from '@/services/mj/api'; 2 | import { LockOutlined, MailOutlined } from '@ant-design/icons'; 3 | import { LoginForm, ProFormText } from '@ant-design/pro-components'; 4 | import { useEmotionCss } from '@ant-design/use-emotion-css'; 5 | import { Helmet, SelectLang, useIntl, useModel } from '@umijs/max'; 6 | import { Button, Input, message, Space } from 'antd'; 7 | import React, { useEffect, useState } from 'react'; 8 | import { flushSync } from 'react-dom'; 9 | import Settings from '../../../../config/defaultSettings'; 10 | 11 | const Lang = () => { 12 | const langClassName = useEmotionCss(({ token }) => { 13 | return { 14 | width: 42, 15 | height: 42, 16 | lineHeight: '42px', 17 | position: 'fixed', 18 | right: 16, 19 | borderRadius: token.borderRadius, 20 | ':hover': { 21 | backgroundColor: token.colorBgTextHover, 22 | }, 23 | }; 24 | }); 25 | 26 | return ( 27 |
28 | {SelectLang && } 29 |
30 | ); 31 | }; 32 | 33 | const Login: React.FC = () => { 34 | const { initialState, setInitialState } = useModel('@@initialState'); 35 | 36 | const intl = useIntl(); 37 | 38 | const [isRegister, setIsRegister] = useState(false); 39 | const [mail, setMail] = useState(); 40 | const [registering, setRegistering] = useState(false); 41 | 42 | const containerClassName = useEmotionCss(() => { 43 | return { 44 | display: 'flex', 45 | flexDirection: 'column', 46 | height: '100vh', 47 | overflow: 'auto', 48 | backgroundImage: 49 | "url('https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/V-_oS6r-i7wAAAAAAAAAAAAAFl94AQBr')", 50 | backgroundSize: '100% 100%', 51 | }; 52 | }); 53 | 54 | // register 55 | const onRegister = () => { 56 | if (!mail) { 57 | message.error('Please input email'); 58 | return; 59 | } 60 | 61 | setRegistering(true); 62 | register(JSON.stringify({ email: mail })).then((res) => { 63 | setRegistering(false); 64 | if (res.success) { 65 | message.success( 66 | intl.formatMessage({ 67 | id: 'pages.login.registerSuccess', 68 | }), 69 | ); 70 | setIsRegister(false); 71 | } else { 72 | message.error(res.message); 73 | } 74 | }); 75 | }; 76 | 77 | const handleSubmit = async (values: API.LoginParams) => { 78 | try { 79 | // 登录 80 | const msg = await login(JSON.stringify(values.password || '')); 81 | if (msg.code === 1) { 82 | if (msg && msg.apiSecret) { 83 | sessionStorage.setItem('mj-api-secret', msg.apiSecret); 84 | } 85 | 86 | message.success( 87 | intl.formatMessage({ 88 | id: 'pages.login.success', 89 | }), 90 | ); 91 | 92 | const userInfo = await initialState?.fetchUserInfo?.(); 93 | if (userInfo) { 94 | flushSync(() => { 95 | setInitialState((s) => ({ 96 | ...s, 97 | currentUser: userInfo, 98 | })); 99 | }); 100 | location.hash = '#/welcome'; 101 | } 102 | return; 103 | } 104 | message.error(msg.description); 105 | } catch (error) { 106 | const defaultLoginFailureMessage = intl.formatMessage({ 107 | id: 'pages.login.failure', 108 | }); 109 | message.error(defaultLoginFailureMessage); 110 | } 111 | }; 112 | 113 | // 是否显示注册 114 | const [showRegister, setShowRegister] = useState(false); 115 | 116 | useEffect(() => { 117 | getIndex().then((res) => { 118 | if (res.success) { 119 | if (res.data) { 120 | setShowRegister(res.data.isRegister); 121 | } 122 | } 123 | }); 124 | }, []); 125 | 126 | return ( 127 |
128 | 129 | 130 | {intl.formatMessage({ id: 'menu.login' })} - {Settings.title} 131 | 132 | 133 | 134 |
140 | {isRegister && showRegister ? ( 141 |
151 | } 155 | placeholder={intl.formatMessage({ 156 | id: 'pages.register.email.placeholder', 157 | defaultMessage: 'Email', 158 | })} 159 | value={mail} 160 | onChange={(e) => { 161 | setMail(e.target.value); 162 | }} 163 | /> 164 | 165 | 174 | 182 | 183 |
184 | ) : ( 185 | { 197 | await handleSubmit(values as API.LoginParams); 198 | }} 199 | actions={ 200 | showRegister && ( 201 | 210 | ) 211 | } 212 | > 213 | , 218 | }} 219 | placeholder={intl.formatMessage({ 220 | id: 'pages.login.password.placeholder', 221 | defaultMessage: 'Admin Token', 222 | })} 223 | /> 224 | 225 | )} 226 |
227 |
228 | ); 229 | }; 230 | 231 | export default Login; 232 | -------------------------------------------------------------------------------- /src/pages/UserList/List/index.less: -------------------------------------------------------------------------------- 1 | .tableToolbar{ 2 | text-align: right; 3 | padding: 15px 0; 4 | } 5 | -------------------------------------------------------------------------------- /src/pages/UserList/List/index.tsx: -------------------------------------------------------------------------------- 1 | import MyModal from '@/pages/components/Modal'; 2 | import AddContent from '@/pages/UserList/components/AddContent'; 3 | import { createUser, deleteUser, queryUser } from '@/services/mj/api'; 4 | import { DeleteOutlined, EditOutlined, UserAddOutlined } from '@ant-design/icons'; 5 | import { PageContainer, ProTable } from '@ant-design/pro-components'; 6 | import { useIntl } from '@umijs/max'; 7 | import { Button, Card, Checkbox, Form, notification, Popconfirm, Space, Tag } from 'antd'; 8 | import moment from 'moment'; 9 | import React, { useRef, useState } from 'react'; 10 | 11 | const UserList: React.FC = () => { 12 | // 初始化 dataSource 状态为空数组 13 | const [modalVisible, setModalVisible] = useState(false); 14 | const [modalContent, setModalContent] = useState({}); 15 | const [title, setTitle] = useState(''); 16 | const [footer, setFooter] = useState({}); 17 | const [modalWidth, setModalWidth] = useState(1000); 18 | const [form] = Form.useForm(); 19 | const intl = useIntl(); 20 | 21 | const [api, contextHolder] = notification.useNotification(); 22 | 23 | const actionRef = useRef(); 24 | 25 | const hideModal = () => { 26 | setModalContent({}); 27 | setFooter({}); 28 | setModalVisible(false); 29 | }; 30 | const [modalSubmitLoading, setModalSubmitLoading] = useState(false); 31 | const modalFooter = ( 32 | <> 33 | 36 | 44 | 45 | ); 46 | 47 | const handleAdd = async (values: Record) => { 48 | setModalSubmitLoading(true); 49 | 50 | // datepicker 传递的是 moment 对象,需要转换为字符串 51 | if (values.validStartTime) { 52 | values.validStartTime = moment(values.validStartTime).format('YYYY-MM-DD'); 53 | } 54 | if (values.validEndTime) { 55 | values.validEndTime = moment(values.validEndTime).format('YYYY-MM-DD'); 56 | } 57 | 58 | const res = await createUser(values); 59 | if (res.success) { 60 | api.success({ 61 | message: 'success', 62 | description: res.message, 63 | }); 64 | hideModal(); 65 | actionRef.current?.reload(); 66 | } else { 67 | api.error({ 68 | message: 'error', 69 | description: res.message, 70 | }); 71 | } 72 | setModalSubmitLoading(false); 73 | }; 74 | 75 | const openModal = (title: string, content: any, footer: any, modalWidth: number) => { 76 | form.resetFields(); 77 | setTitle(title); 78 | setModalContent(content); 79 | setFooter(footer); 80 | setModalWidth(modalWidth); 81 | setModalVisible(true); 82 | }; 83 | 84 | const onDelete = async (id: string) => { 85 | try { 86 | const res = await deleteUser(id); 87 | if (res.success) { 88 | api.success({ 89 | message: 'success', 90 | description: intl.formatMessage({ id: 'pages.task.deleteSuccess' }), 91 | }); 92 | actionRef.current?.reload(); 93 | } else { 94 | api.error({ 95 | message: 'error', 96 | description: res.message, 97 | }); 98 | } 99 | } catch (error) { 100 | console.error(error); 101 | } finally { 102 | } 103 | }; 104 | 105 | const columns = [ 106 | { 107 | title: intl.formatMessage({ id: 'pages.user.name' }), 108 | dataIndex: 'name', 109 | width: 140, 110 | align: 'center', 111 | fixed: 'left', 112 | }, 113 | { 114 | title: intl.formatMessage({ id: 'pages.user.email' }), 115 | dataIndex: 'email', 116 | width: 140, 117 | align: 'center', 118 | }, 119 | { 120 | title: intl.formatMessage({ id: 'pages.user.isWhite' }), 121 | dataIndex: 'isWhite', 122 | width: 90, 123 | align: 'center', 124 | render: (text) => { 125 | return ; 126 | }, 127 | }, 128 | { 129 | title: intl.formatMessage({ id: 'pages.user.status' }), 130 | dataIndex: 'status', 131 | width: 90, 132 | align: 'center', 133 | request: async () => [ 134 | { 135 | label: intl.formatMessage({ id: 'pages.user.normal' }), 136 | value: 'NORMAL', 137 | }, 138 | { 139 | label: intl.formatMessage({ id: 'pages.user.disabled' }), 140 | value: 'DISABLED', 141 | }, 142 | ], 143 | render: (text, record) => { 144 | let color = 'default'; 145 | if (text == 'NORMAL') { 146 | color = 'default'; 147 | } else if (text == 'DISABLED') { 148 | color = 'error'; 149 | } 150 | return {record.status}; 151 | }, 152 | }, 153 | { 154 | title: intl.formatMessage({ id: 'pages.user.role' }), 155 | dataIndex: 'role', 156 | width: 140, 157 | showInfo: false, 158 | align: 'center', 159 | request: async () => [ 160 | { 161 | label: intl.formatMessage({ id: 'pages.user.user' }), 162 | value: 'USER', 163 | }, 164 | { 165 | label: intl.formatMessage({ id: 'pages.user.admin' }), 166 | value: 'ADMIN', 167 | }, 168 | ], 169 | render: (text, record) => { 170 | let color = 'default'; 171 | if (text == 'USER') { 172 | color = 'default'; 173 | } else if (text == 'ADMIN') { 174 | color = 'success'; 175 | } 176 | return {record.role}; 177 | }, 178 | }, 179 | { 180 | title: intl.formatMessage({ id: 'pages.user.dayDrawLimit' }), 181 | dataIndex: 'dayDrawLimit', 182 | width: 140, 183 | align: 'center', 184 | hideInSearch: true, 185 | render: (text, record) => { 186 | if (text <= 0) { 187 | return intl.formatMessage({ id: 'pages.user.unlimited' }); 188 | } else { 189 | return text; 190 | } 191 | }, 192 | }, 193 | { 194 | title: intl.formatMessage({ id: 'pages.user.totalDrawLimit' }), 195 | dataIndex: 'totalDrawLimit', 196 | width: 140, 197 | align: 'center', 198 | hideInSearch: true, 199 | render: (text, record) => { 200 | if (text <= 0) { 201 | return intl.formatMessage({ id: 'pages.user.unlimited' }); 202 | } else { 203 | return text; 204 | } 205 | }, 206 | }, 207 | { 208 | title: intl.formatMessage({ id: 'pages.user.dayDrawCount' }), 209 | dataIndex: 'dayDrawCount', 210 | width: 140, 211 | ellipsis: true, 212 | hideInSearch: true, 213 | }, 214 | { 215 | title: intl.formatMessage({ id: 'pages.user.totalDrawCount' }), 216 | dataIndex: 'totalDrawCount', 217 | width: 140, 218 | ellipsis: true, 219 | hideInSearch: true, 220 | }, 221 | // { 222 | // title: intl.formatMessage({ id: 'pages.user.lastLoginIp' }), 223 | // dataIndex: 'lastLoginIp', 224 | // width: 140, 225 | // ellipsis: true, 226 | // hideInSearch: true, 227 | // }, 228 | // { 229 | // title: intl.formatMessage({ id: 'pages.user.lastLoginTime' }), 230 | // dataIndex: 'lastLoginTimeFormat', 231 | // width: 140, 232 | // ellipsis: true, 233 | // hideInSearch: true, 234 | // }, 235 | // { 236 | // title: intl.formatMessage({ id: 'pages.user.registerIp' }), 237 | // dataIndex: 'registerIp', 238 | // width: 140, 239 | // ellipsis: true, 240 | // hideInSearch: true, 241 | // }, 242 | // { 243 | // title: intl.formatMessage({ id: 'pages.user.registerTime' }), 244 | // dataIndex: 'registerTimeFormat', 245 | // width: 140, 246 | // ellipsis: true, 247 | // hideInSearch: true, 248 | // }, 249 | { 250 | title: intl.formatMessage({ id: 'pages.operation' }), 251 | dataIndex: 'operation', 252 | width: 140, 253 | key: 'operation', 254 | fixed: 'right', 255 | align: 'center', 256 | hideInSearch: true, 257 | render: (_, record: any) => { 258 | if (record.validStartTime) { 259 | record.validStartTime = moment(record.validStartTimeFormat, 'YYYY-MM-DD'); 260 | } 261 | if (record.validEndTime) { 262 | record.validEndTime = moment(record.validEndTimeFormat, 'YYYY-MM-DD'); 263 | } 264 | 265 | return ( 266 | 267 | 285 | 286 | 287 | ); 288 | }, 289 | }, 290 | ]; 291 | 292 | return ( 293 | 294 | {contextHolder} 295 | 296 | [ 308 | , 323 | ]} 324 | request={async (params) => { 325 | const res = await queryUser({ ...params, pageNumber: params.current! - 1 }); 326 | return { 327 | data: res.list, 328 | total: res.pagination.total, 329 | success: true, 330 | }; 331 | }} 332 | /> 333 | 334 | 342 | 343 | ); 344 | }; 345 | 346 | export default UserList; 347 | -------------------------------------------------------------------------------- /src/pages/UserList/components/AddContent.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Card, 3 | Col, 4 | DatePicker, 5 | Form, 6 | FormInstance, 7 | Input, 8 | InputNumber, 9 | Row, 10 | Select, 11 | Switch, 12 | } from 'antd'; 13 | import { useEffect, useState } from 'react'; 14 | 15 | import { useIntl } from '@umijs/max'; 16 | 17 | const UpdateContent = ({ 18 | form, 19 | onSubmit, 20 | record, 21 | }: { 22 | form: FormInstance; 23 | onSubmit: (values: any) => void; 24 | record: Record; 25 | }) => { 26 | const intl = useIntl(); 27 | const [disabledRole, setDisabledRole] = useState(false); 28 | 29 | // 当组件挂载或者record更新时,设置表单的初始值 30 | useEffect(() => { 31 | form.setFieldsValue(record); 32 | 33 | // 如果是管理员,则禁用角色选择 34 | if (record.id == 'admin') { 35 | setDisabledRole(true); 36 | } else { 37 | setDisabledRole(false); 38 | } 39 | console.log('record', record); 40 | }); 41 | 42 | return ( 43 |
51 | 52 | 53 | 54 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 72 | 80 | 81 | 82 | 83 | 91 | 92 | { 99 | // 128 位随机字符串 100 | const token = Array.from({ length: 32 }, () => 101 | Math.random().toString(36).charAt(2), 102 | ).join(''); 103 | form.setFieldsValue({ token }); 104 | }} 105 | > 106 | {intl.formatMessage({ id: 'pages.user.refreshToken' })} 107 | 108 | } 109 | > 110 | 111 | 112 | 113 | 117 | 118 | 119 | 123 | 124 | 125 | 129 | 130 | 131 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 146 | 147 | 148 | 149 | 153 | 154 | 155 | 156 | 160 | 161 | 162 | 163 | 167 | 168 | 169 | 170 | 174 | 175 | 176 | 177 | 178 | 182 | 183 | 184 | 185 | 189 | 190 | 191 | 192 | 196 | 197 | 198 | 199 | 203 | 204 | 205 | 206 | 207 | 208 |
209 | ); 210 | }; 211 | 212 | export default UpdateContent; 213 | -------------------------------------------------------------------------------- /src/pages/Welcome.tsx: -------------------------------------------------------------------------------- 1 | import { getIndex } from '@/services/mj/api'; 2 | import { PageContainer } from '@ant-design/pro-components'; 3 | import { useIntl, useModel } from '@umijs/max'; 4 | import { Alert, Card, Col, Divider, List, Row, Statistic, Tag, theme } from 'antd'; 5 | import React, { useEffect, useState } from 'react'; 6 | 7 | /** 8 | * 每个单独的卡片,为了复用样式抽成了组件 9 | * @param param0 10 | * @returns 11 | */ 12 | const InfoCard: React.FC<{ 13 | title: string; 14 | index: number; 15 | desc: string; 16 | href: string; 17 | }> = ({ title, href, index, desc }) => { 18 | const intl = useIntl(); 19 | const { useToken } = theme; 20 | 21 | const { token } = useToken(); 22 | 23 | return ( 24 |
37 |
44 |
58 | {index} 59 |
60 |
67 | {title} 68 |
69 |
70 |
79 | {desc} 80 |
81 | 82 | {intl.formatMessage({ id: 'pages.welcome.learnMore' })} {'>'} 83 | 84 |
85 | ); 86 | }; 87 | 88 | const Welcome: React.FC = () => { 89 | const { token } = theme.useToken(); 90 | const { initialState } = useModel('@@initialState'); 91 | const intl = useIntl(); 92 | 93 | // 是否显示注册 94 | const [data, setData] = useState(); 95 | const [tops, setTops] = useState(); 96 | 97 | useEffect(() => { 98 | getIndex().then((res) => { 99 | if (res.success) { 100 | if (res.data) { 101 | setData(res.data); 102 | const vs = Object.keys(res.data.tops).map((x) => { 103 | return { 104 | ip: x, 105 | count: res.data.tops[x], 106 | }; 107 | }); 108 | setTops(vs); 109 | } 110 | } 111 | }); 112 | }, []); 113 | 114 | return ( 115 | 116 | {data && data.notify && ( 117 | 125 | )} 126 | 127 | 138 |
147 |
153 | {intl.formatMessage({ id: 'pages.welcome.link' })} Midjourney Proxy Admin 154 |
155 |

165 | {intl.formatMessage({ id: 'pages.welcome.description' })} 166 |

167 |
174 | 180 | 186 | 192 |
193 |
194 |
195 | 196 | {data && ( 197 | 209 | 210 | 211 | 215 | 216 | 217 | 221 | 222 | 223 | 227 | 228 | 229 | 230 | {intl.formatMessage({ id: 'pages.welcome.top5' })} 231 | ( 234 | 235 | {item.ip} {item.count} {intl.formatMessage({ id: 'pages.welcome.unit' })} 236 | 237 | )} 238 | /> 239 | 240 | )} 241 |
242 | ); 243 | }; 244 | 245 | export default Welcome; 246 | -------------------------------------------------------------------------------- /src/pages/components/Modal.tsx: -------------------------------------------------------------------------------- 1 | import { Modal } from 'antd'; 2 | 3 | const MyModal = ({ 4 | title, 5 | modalVisible, 6 | hideModal, 7 | modalContent, 8 | footer, 9 | modalWidth, 10 | }: { 11 | title: string; 12 | modalVisible: boolean; 13 | hideModal: () => void; 14 | modalContent: any; 15 | footer: any; 16 | modalWidth: number; 17 | }) => { 18 | return ( 19 | 26 | {modalContent} 27 | 28 | ); 29 | }; 30 | 31 | export default MyModal; 32 | -------------------------------------------------------------------------------- /src/requestErrorConfig.ts: -------------------------------------------------------------------------------- 1 | import type { RequestOptions } from '@@/plugin-request/request'; 2 | import type { RequestConfig } from '@umijs/max'; 3 | import { getLocale } from '@umijs/max'; 4 | import { message, notification } from 'antd'; 5 | import { history } from '@umijs/max'; 6 | 7 | const loginPath = '/user/login'; 8 | 9 | 10 | // 错误处理方案: 错误类型 11 | enum ErrorShowType { 12 | SILENT = 0, 13 | WARN_MESSAGE = 1, 14 | ERROR_MESSAGE = 2, 15 | NOTIFICATION = 3, 16 | REDIRECT = 9, 17 | } 18 | // 与后端约定的响应数据格式 19 | interface ResponseStructure { 20 | success: boolean; 21 | data: any; 22 | errorCode?: number; 23 | errorMessage?: string; 24 | showType?: ErrorShowType; 25 | } 26 | 27 | /** 28 | * @name 错误处理 29 | * pro 自带的错误处理, 可以在这里做自己的改动 30 | * @doc https://umijs.org/docs/max/request#配置 31 | */ 32 | export const errorConfig: RequestConfig = { 33 | // 错误处理: umi@3 的错误处理方案。 34 | errorConfig: { 35 | // 错误抛出 36 | errorThrower: (res) => { 37 | const { success, data, errorCode, errorMessage, showType } = 38 | res as unknown as ResponseStructure; 39 | if (!success) { 40 | const error: any = new Error(errorMessage); 41 | error.name = 'BizError'; 42 | error.info = { errorCode, errorMessage, showType, data }; 43 | console.error('BizError', success, data, errorCode, errorMessage, showType, error); 44 | } 45 | }, 46 | // 错误接收及处理 47 | errorHandler: (error: any, opts: any) => { 48 | if (opts?.skipErrorHandler) throw error; 49 | // 我们的 errorThrower 抛出的错误。 50 | if (error.name === 'BizError') { 51 | const errorInfo: ResponseStructure | undefined = error.info; 52 | if (errorInfo) { 53 | const { errorMessage, errorCode } = errorInfo; 54 | switch (errorInfo.showType) { 55 | case ErrorShowType.SILENT: 56 | // do nothing 57 | break; 58 | case ErrorShowType.WARN_MESSAGE: 59 | message.warning(errorMessage); 60 | break; 61 | case ErrorShowType.ERROR_MESSAGE: 62 | message.error(errorMessage); 63 | break; 64 | case ErrorShowType.NOTIFICATION: 65 | notification.open({ 66 | description: errorMessage, 67 | message: errorCode, 68 | }); 69 | break; 70 | case ErrorShowType.REDIRECT: 71 | // TODO: redirect 72 | break; 73 | default: 74 | if (errorMessage) { 75 | message.error(errorMessage); 76 | } 77 | } 78 | } 79 | } else if (error.response) { 80 | // Axios 的错误 81 | // 请求成功发出且服务器也响应了状态码,但状态代码超出了 2xx 的范围 82 | if (error.response.status === 403) { 83 | message.error('Forbidden, please login again.'); 84 | } 85 | else if (error.response.status === 401) { 86 | message.error('Unauthorized, please login again.'); 87 | // 去登录 88 | history.push(loginPath); 89 | } 90 | else if (error.response.status === 429) { 91 | message.error('Too many requests, please retry later.'); 92 | } else { 93 | message.error(`Response status:${error.response.status}`); 94 | } 95 | } else if (error.request) { 96 | // 请求已经成功发起,但没有收到响应 97 | // \`error.request\` 在浏览器中是 XMLHttpRequest 的实例, 98 | // 而在node.js中是 http.ClientRequest 的实例 99 | message.error('None response! Please retry.'); 100 | } else { 101 | // 发送请求时出了点问题 102 | message.error('Request error, please retry.'); 103 | } 104 | }, 105 | }, 106 | 107 | // 请求拦截器 108 | requestInterceptors: [ 109 | (config: RequestOptions) => { 110 | const MJ_API_SECRET = sessionStorage.getItem('mj-api-secret') || ''; // 获取保存的密码 111 | const locale = getLocale(); 112 | config.headers = { 113 | ...config.headers, 114 | 'mj-api-secret': MJ_API_SECRET, // 将密码作为自定义头部 115 | 'Accept-Language': locale === 'zh-CN' ? 'zh-CN' : 'en-US', 116 | }; 117 | return { ...config }; 118 | }, 119 | ], 120 | 121 | timeout: 1000 * 60, 122 | 123 | responseInterceptors: [ 124 | (response) => { 125 | // 你可以在这里添加响应拦截逻辑,移除默认的拦截逻辑 126 | const { data } = response; 127 | // 自定义拦截逻辑 128 | if (data && data?.success === false) { 129 | // 这里是你的自定义处理逻辑,可以根据需求进行调整 130 | message.error(data?.message || 'Request failed'); 131 | } 132 | return response; 133 | }, 134 | ], 135 | }; 136 | -------------------------------------------------------------------------------- /src/service-worker.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-restricted-globals */ 2 | /* eslint-disable no-underscore-dangle */ 3 | /* globals workbox */ 4 | workbox.core.setCacheNameDetails({ 5 | prefix: 'antd-pro', 6 | suffix: 'v5', 7 | }); 8 | // Control all opened tabs ASAP 9 | workbox.clientsClaim(); 10 | 11 | /** 12 | * Use precaching list generated by workbox in build process. 13 | * https://developers.google.com/web/tools/workbox/reference-docs/latest/workbox.precaching 14 | */ 15 | workbox.precaching.precacheAndRoute(self.__precacheManifest || []); 16 | 17 | /** 18 | * Register a navigation route. 19 | * https://developers.google.com/web/tools/workbox/modules/workbox-routing#how_to_register_a_navigation_route 20 | */ 21 | workbox.routing.registerNavigationRoute('/index.html'); 22 | 23 | /** 24 | * Use runtime cache: 25 | * https://developers.google.com/web/tools/workbox/reference-docs/latest/workbox.routing#.registerRoute 26 | * 27 | * Workbox provides all common caching strategies including CacheFirst, NetworkFirst etc. 28 | * https://developers.google.com/web/tools/workbox/reference-docs/latest/workbox.strategies 29 | */ 30 | 31 | /** Handle API requests */ 32 | workbox.routing.registerRoute(/\/api\//, workbox.strategies.networkFirst()); 33 | 34 | /** Handle third party requests */ 35 | workbox.routing.registerRoute( 36 | /^https:\/\/gw\.alipayobjects\.com\//, 37 | workbox.strategies.networkFirst(), 38 | ); 39 | workbox.routing.registerRoute( 40 | /^https:\/\/cdnjs\.cloudflare\.com\//, 41 | workbox.strategies.networkFirst(), 42 | ); 43 | workbox.routing.registerRoute(/\/color.less/, workbox.strategies.networkFirst()); 44 | 45 | /** Response to client after skipping waiting with MessageChannel */ 46 | addEventListener('message', (event) => { 47 | const replyPort = event.ports[0]; 48 | const message = event.data; 49 | if (replyPort && message && message.type === 'skip-waiting') { 50 | event.waitUntil( 51 | self.skipWaiting().then( 52 | () => { 53 | replyPort.postMessage({ 54 | error: null, 55 | }); 56 | }, 57 | (error) => { 58 | replyPort.postMessage({ 59 | error, 60 | }); 61 | }, 62 | ), 63 | ); 64 | } 65 | }); 66 | -------------------------------------------------------------------------------- /src/services/mj/api.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | /* eslint-disable */ 3 | import { request } from '@umijs/max'; 4 | 5 | /** 获取当前的用户 GET /mj/admin/current */ 6 | export async function currentUser(options?: { [key: string]: any }) { 7 | return request('/mj/admin/current', { 8 | method: 'GET', 9 | ...(options || {}), 10 | }).then((response) => { 11 | if (response && response.apiSecret) { 12 | sessionStorage.setItem('mj-api-secret', response.apiSecret); 13 | } 14 | if (response.imagePrefix) { 15 | sessionStorage.setItem('mj-image-prefix', response.imagePrefix); 16 | } else { 17 | sessionStorage.removeItem('mj-image-prefix'); 18 | } 19 | return response; 20 | }); 21 | } 22 | 23 | /** 退出登录接口 POST /mj/admin/logout */ 24 | export async function outLogin(options?: { [key: string]: any }) { 25 | return request('/mj/admin/logout', { 26 | method: 'POST', 27 | ...(options || {}), 28 | }).then((response) => { 29 | sessionStorage.removeItem('mj-api-secret'); 30 | sessionStorage.removeItem('mj-image-prefix'); 31 | return response; 32 | }); 33 | } 34 | 35 | /** 登录接口 POST /mj/admin/login */ 36 | export async function login(body: any, options?: { [key: string]: any }) { 37 | return request('/mj/admin/login', { 38 | method: 'POST', 39 | headers: { 40 | 'Content-Type': 'application/json', 41 | }, 42 | data: body, 43 | ...(options || {}), 44 | }); 45 | } 46 | 47 | // 通过邮件注册接口 48 | export async function register(body: any, options?: { [key: string]: any }) { 49 | return request('/mj/admin/register', { 50 | method: 'POST', 51 | headers: { 52 | 'Content-Type': 'application/json', 53 | }, 54 | data: body, 55 | ...(options || {}), 56 | }); 57 | } 58 | 59 | // 首页信息 60 | export async function getIndex(options?: { [key: string]: any }) { 61 | return request('/mj/home', { 62 | method: 'GET', 63 | ...(options || {}), 64 | }); 65 | } 66 | 67 | /** MJ 接口 */ 68 | 69 | /** POST /mj/account/create */ 70 | export async function createAccount(data: object, options?: { [key: string]: any }) { 71 | return request('/mj/admin/account', { 72 | method: 'POST', 73 | data: data, 74 | ...(options || {}), 75 | }); 76 | } 77 | 78 | /** POST /mj/account/query */ 79 | export async function queryAccount(options?: { [key: string]: any }) { 80 | return request>('/mj/admin/accounts', { 81 | method: 'GET', 82 | ...(options || {}), 83 | }); 84 | } 85 | 86 | export async function queryAccounts( 87 | data: any, 88 | predicate?: string, 89 | descend?: boolean, 90 | options?: { [key: string]: any }, 91 | ) { 92 | return request>('/mj/admin/accounts', { 93 | method: 'POST', 94 | data: { 95 | pagination: { 96 | current: data?.current || 1, 97 | pageSize: data?.pageSize || 10, 98 | }, 99 | sort: { 100 | predicate: predicate || '', 101 | reverse: descend, 102 | }, 103 | search: { 104 | ...data, 105 | }, 106 | }, 107 | ...(options || {}), 108 | }); 109 | } 110 | 111 | /** POST /mj/account/{id}/sync-info */ 112 | export async function refreshAccount(id: string, options?: { [key: string]: any }) { 113 | return request(`/mj/admin/account-sync/${id}`, { 114 | method: 'POST', 115 | ...(options || {}), 116 | }); 117 | } 118 | 119 | // loginAccountGetToken 120 | export async function loginAccountGetToken(id: string, options?: { [key: string]: any }) { 121 | return request(`/mj/admin/account-login/${id}`, { 122 | method: 'POST', 123 | ...(options || {}), 124 | }); 125 | } 126 | 127 | /** PUT */ 128 | export async function updateAndReconnect( 129 | id: string, 130 | data: object, 131 | options?: { [key: string]: any }, 132 | ) { 133 | return request(`/mj/admin/account-reconnect/${id}`, { 134 | method: 'PUT', 135 | data: data, 136 | ...(options || {}), 137 | }); 138 | } 139 | 140 | export async function update(id: string, data: object, options?: { [key: string]: any }) { 141 | return request(`/mj/admin/account/${id}`, { 142 | method: 'PUT', 143 | data: data, 144 | ...(options || {}), 145 | }); 146 | } 147 | 148 | // CF 标记验证通过 149 | export async function accountCfOk(id: string, data?: object, options?: { [key: string]: any }) { 150 | return request(`/mj/admin/account-cf/${id}`, { 151 | method: 'POST', 152 | data: data, 153 | ...(options || {}), 154 | }); 155 | } 156 | 157 | // CF 刷新链接 158 | export async function accountCfUrl(id: string, options?: { [key: string]: any }) { 159 | return request(`/mj/admin/account-cf/${id}?refresh=true`, { 160 | method: 'GET', 161 | ...(options || {}), 162 | }); 163 | } 164 | 165 | /** DELETE */ 166 | export async function deleteAccount(id: string, options?: { [key: string]: any }) { 167 | return request(`/mj/admin/account/${id}`, { 168 | method: 'DELETE', 169 | ...(options || {}), 170 | }); 171 | } 172 | 173 | /** DELETE */ 174 | export async function deleteTask(id: string, options?: { [key: string]: any }) { 175 | return request(`/mj/admin/task/${id}`, { 176 | method: 'DELETE', 177 | ...(options || {}), 178 | }); 179 | } 180 | 181 | export async function accountChangeVersion( 182 | id: string, 183 | version: string, 184 | options?: { [key: string]: any }, 185 | ) { 186 | return request(`/mj/admin/account-change-version/${id}?version=${version}`, { 187 | method: 'POST', 188 | ...(options || {}), 189 | }); 190 | } 191 | 192 | export async function accountAction( 193 | id: string, 194 | botType: string, 195 | customId: string, 196 | options?: { [key: string]: any }, 197 | ) { 198 | return request( 199 | `/mj/admin/account-action/${id}?customId=${customId}&botType=${botType}`, 200 | { 201 | method: 'POST', 202 | ...(options || {}), 203 | }, 204 | ); 205 | } 206 | 207 | export async function queryTask(data: any, options?: { [key: string]: any }) { 208 | return request>('/mj/admin/tasks', { 209 | method: 'POST', 210 | data: { 211 | pagination: { 212 | current: data?.current || 1, 213 | pageSize: data?.pageSize || 10, 214 | }, 215 | sort: { 216 | predicate: '', 217 | reverse: true, 218 | }, 219 | search: { 220 | ...data, 221 | }, 222 | }, 223 | ...(options || {}), 224 | }); 225 | } 226 | 227 | /** 用户列表 */ 228 | export async function queryUser(data: any, options?: { [key: string]: any }) { 229 | return request>('/mj/admin/users', { 230 | method: 'POST', 231 | data: { 232 | pagination: { 233 | current: data?.current || 1, 234 | pageSize: data?.pageSize || 10, 235 | }, 236 | sort: { 237 | predicate: '', 238 | reverse: true, 239 | }, 240 | search: { 241 | ...data, 242 | }, 243 | }, 244 | ...(options || {}), 245 | }); 246 | } 247 | 248 | /** 删除用户 */ 249 | export async function deleteUser(id: string, options?: { [key: string]: any }) { 250 | return request(`/mj/admin/user/${id}`, { 251 | method: 'DELETE', 252 | ...(options || {}), 253 | }); 254 | } 255 | 256 | /** 创建/编辑用户 */ 257 | export async function createUser(data: object, options?: { [key: string]: any }) { 258 | return request('/mj/admin/user', { 259 | method: 'POST', 260 | data: data, 261 | ...(options || {}), 262 | }); 263 | } 264 | 265 | /** 领域列表 */ 266 | export async function queryDomain(data: any, options?: { [key: string]: any }) { 267 | return request>('/mj/admin/domain-tags', { 268 | method: 'POST', 269 | data: { 270 | pagination: { 271 | current: data?.current || 1, 272 | pageSize: data?.pageSize || 10, 273 | }, 274 | sort: { 275 | predicate: '', 276 | reverse: true, 277 | }, 278 | search: { 279 | ...data, 280 | }, 281 | }, 282 | ...(options || {}), 283 | }); 284 | } 285 | 286 | /** 删除领域 */ 287 | export async function deleteDomain(id: string, options?: { [key: string]: any }) { 288 | return request(`/mj/admin/domain-tag/${id}`, { 289 | method: 'DELETE', 290 | ...(options || {}), 291 | }); 292 | } 293 | /** 所有领域 */ 294 | export async function allDomain(options?: { [key: string]: any }) { 295 | return request('/mj/admin/domain-tags', { 296 | method: 'GET', 297 | ...(options || {}), 298 | }); 299 | } 300 | 301 | /** 创建/编辑领域 */ 302 | export async function createDomain(data: object, options?: { [key: string]: any }) { 303 | return request('/mj/admin/domain-tag', { 304 | method: 'POST', 305 | data: data, 306 | ...(options || {}), 307 | }); 308 | } 309 | 310 | /** 311 | * 违规词 312 | * @param data 313 | * @param options 314 | * @returns 315 | */ 316 | export async function queryIllegalWord(data: any, options?: { [key: string]: any }) { 317 | return request>('/mj/admin/banned-words', { 318 | method: 'POST', 319 | data: { 320 | pagination: { 321 | current: data?.current || 1, 322 | pageSize: data?.pageSize || 10, 323 | }, 324 | sort: { 325 | predicate: '', 326 | reverse: true, 327 | }, 328 | search: { 329 | ...data, 330 | }, 331 | }, 332 | ...(options || {}), 333 | }); 334 | } 335 | 336 | /** 337 | * 删除违规词 338 | * @param id 339 | * @param options 340 | * @returns 341 | */ 342 | export async function deleteIllegalWord(id: string, options?: { [key: string]: any }) { 343 | return request(`/mj/admin/banned-word/${id}`, { 344 | method: 'DELETE', 345 | ...(options || {}), 346 | }); 347 | } 348 | 349 | /** 350 | * 创建违规词 351 | * @param data 352 | * @param options 353 | * @returns 354 | */ 355 | export async function createIllegalWord(data: object, options?: { [key: string]: any }) { 356 | return request('/mj/admin/banned-word', { 357 | method: 'POST', 358 | data: data, 359 | ...(options || {}), 360 | }); 361 | } 362 | 363 | export async function queryTaskByIds(ids: string[], options?: { [key: string]: any }) { 364 | return request('/mj/task/list-by-ids', { 365 | method: 'POST', 366 | data: { ids: ids }, 367 | ...(options || {}), 368 | }); 369 | } 370 | 371 | export async function getTask(id: string, options?: { [key: string]: any }) { 372 | return request(`/mj/task-admin/${id}/fetch`, { 373 | method: 'GET', 374 | ...(options || {}), 375 | }); 376 | } 377 | 378 | export async function submitTask(action: string, data: object, options?: { [key: string]: any }) { 379 | return request(`/mj/submit/${action}`, { 380 | method: 'POST', 381 | data: data, 382 | ...(options || {}), 383 | }); 384 | } 385 | 386 | export async function submitShow(action: string, data: object, options?: { [key: string]: any }) { 387 | return request(`/mj/submit/${action}`, { 388 | method: 'POST', 389 | data: data, 390 | ...(options || {}), 391 | }); 392 | } 393 | 394 | export async function cancelTask(id: string, options?: { [key: string]: any }) { 395 | return request(`/mj/task/${id}/cancel`, { 396 | method: 'POST', 397 | ...(options || {}), 398 | }); 399 | } 400 | 401 | export async function swapFace(data: object, options?: { [key: string]: any }) { 402 | return request('/mj/insight-face/swap', { 403 | method: 'POST', 404 | data: data, 405 | ...(options || {}), 406 | }); 407 | } 408 | 409 | export async function swapVideoFace(data: object, options?: { [key: string]: any }) { 410 | return request('/mj/insight-face/video-swap', { 411 | method: 'POST', 412 | data: data, 413 | ...(options || {}), 414 | }); 415 | } 416 | 417 | export async function probe(tail: number, level = 0, options?: { [key: string]: any }) { 418 | return request('/mj/admin/probe?tail=' + tail + '&level=' + level, { 419 | method: 'GET', 420 | ...(options || {}), 421 | }); 422 | } 423 | 424 | // 获取配置 425 | export async function getConfig(options?: { [key: string]: any }) { 426 | return request('/mj/admin/setting', { 427 | method: 'GET', 428 | ...(options || {}), 429 | }); 430 | } 431 | 432 | // 修改配置 433 | export async function updateConfig(data: object, options?: { [key: string]: any }) { 434 | return request('/mj/admin/setting', { 435 | method: 'POST', 436 | data: data, 437 | ...(options || {}), 438 | }); 439 | } 440 | 441 | // mongo db 连接 442 | export async function mongoConnect() { 443 | return request('/mj/admin/verify-mongo', { 444 | method: 'POST', 445 | }); 446 | } 447 | 448 | // 一键迁移 449 | export async function migrateAccountAndTasks(data: object, options?: { [key: string]: any }) { 450 | return request('/mj/admin/mjplus-migration', { 451 | method: 'POST', 452 | data: data, 453 | ...(options || {}), 454 | }); 455 | } 456 | -------------------------------------------------------------------------------- /src/services/mj/index.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | /* eslint-disable */ 3 | // API 更新时间: 4 | // API 唯一标识: 5 | import * as api from './api'; 6 | export default { 7 | api, 8 | }; 9 | -------------------------------------------------------------------------------- /src/services/mj/typings.d.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | /* eslint-disable */ 3 | 4 | declare namespace API { 5 | type CurrentUser = { 6 | name?: string; 7 | apiSecret?: string; 8 | imagePrefix?: string; 9 | active?: boolean = false; 10 | version?: string; 11 | avatar?: string; 12 | userid?: string; 13 | email?: string; 14 | signature?: string; 15 | title?: string; 16 | group?: string; 17 | tags?: { key?: string; label?: string }[]; 18 | notifyCount?: number; 19 | unreadCount?: number; 20 | country?: string; 21 | access?: string; 22 | geographic?: { 23 | province?: { label?: string; key?: string }; 24 | city?: { label?: string; key?: string }; 25 | }; 26 | address?: string; 27 | phone?: string; 28 | }; 29 | 30 | type LoginResult = { 31 | code?: number; 32 | apiSecret?: string; 33 | description?: string; 34 | }; 35 | 36 | type ReturnMessage = { 37 | code?: number; 38 | description?: string; 39 | result?: any; 40 | }; 41 | 42 | type Result = { 43 | code: number; 44 | message?: string; 45 | data?: T; 46 | success?: boolean; 47 | timestamp?: string; 48 | } 49 | 50 | type PageParams = { 51 | current?: number; 52 | pageSize?: number; 53 | }; 54 | 55 | type RuleListItem = { 56 | key?: number; 57 | disabled?: boolean; 58 | href?: string; 59 | avatar?: string; 60 | name?: string; 61 | owner?: string; 62 | desc?: string; 63 | callNo?: number; 64 | status?: number; 65 | updatedAt?: string; 66 | createdAt?: string; 67 | progress?: number; 68 | }; 69 | 70 | type RuleList = { 71 | data?: RuleListItem[]; 72 | /** 列表的内容总数 */ 73 | total?: number; 74 | success?: boolean; 75 | }; 76 | 77 | type FakeCaptcha = { 78 | code?: number; 79 | status?: string; 80 | }; 81 | 82 | type LoginParams = { 83 | username?: string; 84 | password?: string; 85 | }; 86 | 87 | type ErrorResponse = { 88 | /** 业务约定的错误码 */ 89 | errorCode: string; 90 | /** 业务上的错误信息 */ 91 | errorMessage?: string; 92 | /** 业务上的请求是否成功 */ 93 | success?: boolean; 94 | }; 95 | 96 | type NoticeIconList = { 97 | data?: NoticeIconItem[]; 98 | /** 列表的内容总数 */ 99 | total?: number; 100 | success?: boolean; 101 | }; 102 | 103 | type NoticeIconItemType = 'notification' | 'message' | 'event'; 104 | 105 | type NoticeIconItem = { 106 | id?: string; 107 | extra?: string; 108 | key?: string; 109 | read?: boolean; 110 | avatar?: string; 111 | title?: string; 112 | status?: string; 113 | datetime?: string; 114 | description?: string; 115 | type?: NoticeIconItemType; 116 | }; 117 | } 118 | -------------------------------------------------------------------------------- /src/services/swagger/index.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | /* eslint-disable */ 3 | // API 更新时间: 4 | // API 唯一标识: 5 | import * as pet from './pet'; 6 | import * as store from './store'; 7 | import * as user from './user'; 8 | export default { 9 | pet, 10 | store, 11 | user, 12 | }; 13 | -------------------------------------------------------------------------------- /src/services/swagger/pet.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | /* eslint-disable */ 3 | import { request } from '@umijs/max'; 4 | 5 | /** Update an existing pet PUT /pet */ 6 | export async function updatePet(body: API.Pet, options?: { [key: string]: any }) { 7 | return request('/pet', { 8 | method: 'PUT', 9 | headers: { 10 | 'Content-Type': 'application/json', 11 | }, 12 | data: body, 13 | ...(options || {}), 14 | }); 15 | } 16 | 17 | /** Add a new pet to the store POST /pet */ 18 | export async function addPet(body: API.Pet, options?: { [key: string]: any }) { 19 | return request('/pet', { 20 | method: 'POST', 21 | headers: { 22 | 'Content-Type': 'application/json', 23 | }, 24 | data: body, 25 | ...(options || {}), 26 | }); 27 | } 28 | 29 | /** Find pet by ID Returns a single pet GET /pet/${param0} */ 30 | export async function getPetById( 31 | // 叠加生成的Param类型 (非body参数swagger默认没有生成对象) 32 | params: API.getPetByIdParams, 33 | options?: { [key: string]: any }, 34 | ) { 35 | const { petId: param0, ...queryParams } = params; 36 | return request(`/pet/${param0}`, { 37 | method: 'GET', 38 | params: { ...queryParams }, 39 | ...(options || {}), 40 | }); 41 | } 42 | 43 | /** Updates a pet in the store with form data POST /pet/${param0} */ 44 | export async function updatePetWithForm( 45 | // 叠加生成的Param类型 (非body参数swagger默认没有生成对象) 46 | params: API.updatePetWithFormParams, 47 | body: { name?: string; status?: string }, 48 | options?: { [key: string]: any }, 49 | ) { 50 | const { petId: param0, ...queryParams } = params; 51 | const formData = new FormData(); 52 | 53 | Object.keys(body).forEach((ele) => { 54 | const item = (body as any)[ele]; 55 | 56 | if (item !== undefined && item !== null) { 57 | formData.append( 58 | ele, 59 | typeof item === 'object' && !(item instanceof File) ? JSON.stringify(item) : item, 60 | ); 61 | } 62 | }); 63 | 64 | return request(`/pet/${param0}`, { 65 | method: 'POST', 66 | params: { ...queryParams }, 67 | data: formData, 68 | ...(options || {}), 69 | }); 70 | } 71 | 72 | /** Deletes a pet DELETE /pet/${param0} */ 73 | export async function deletePet( 74 | // 叠加生成的Param类型 (非body参数swagger默认没有生成对象) 75 | params: API.deletePetParams & { 76 | // header 77 | api_key?: string; 78 | }, 79 | options?: { [key: string]: any }, 80 | ) { 81 | const { petId: param0, ...queryParams } = params; 82 | return request(`/pet/${param0}`, { 83 | method: 'DELETE', 84 | headers: {}, 85 | params: { ...queryParams }, 86 | ...(options || {}), 87 | }); 88 | } 89 | 90 | /** uploads an image POST /pet/${param0}/uploadImage */ 91 | export async function uploadFile( 92 | // 叠加生成的Param类型 (非body参数swagger默认没有生成对象) 93 | params: API.uploadFileParams, 94 | body: { additionalMetadata?: string; file?: string }, 95 | file?: File, 96 | options?: { [key: string]: any }, 97 | ) { 98 | const { petId: param0, ...queryParams } = params; 99 | const formData = new FormData(); 100 | 101 | if (file) { 102 | formData.append('file', file); 103 | } 104 | 105 | Object.keys(body).forEach((ele) => { 106 | const item = (body as any)[ele]; 107 | 108 | if (item !== undefined && item !== null) { 109 | formData.append( 110 | ele, 111 | typeof item === 'object' && !(item instanceof File) ? JSON.stringify(item) : item, 112 | ); 113 | } 114 | }); 115 | 116 | return request(`/pet/${param0}/uploadImage`, { 117 | method: 'POST', 118 | params: { ...queryParams }, 119 | data: formData, 120 | requestType: 'form', 121 | ...(options || {}), 122 | }); 123 | } 124 | 125 | /** Finds Pets by status Multiple status values can be provided with comma separated strings GET /pet/findByStatus */ 126 | export async function findPetsByStatus( 127 | // 叠加生成的Param类型 (非body参数swagger默认没有生成对象) 128 | params: API.findPetsByStatusParams, 129 | options?: { [key: string]: any }, 130 | ) { 131 | return request('/pet/findByStatus', { 132 | method: 'GET', 133 | params: { 134 | ...params, 135 | }, 136 | ...(options || {}), 137 | }); 138 | } 139 | 140 | /** Finds Pets by tags Muliple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. GET /pet/findByTags */ 141 | export async function findPetsByTags( 142 | // 叠加生成的Param类型 (非body参数swagger默认没有生成对象) 143 | params: API.findPetsByTagsParams, 144 | options?: { [key: string]: any }, 145 | ) { 146 | return request('/pet/findByTags', { 147 | method: 'GET', 148 | params: { 149 | ...params, 150 | }, 151 | ...(options || {}), 152 | }); 153 | } 154 | -------------------------------------------------------------------------------- /src/services/swagger/store.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | /* eslint-disable */ 3 | import { request } from '@umijs/max'; 4 | 5 | /** Returns pet inventories by status Returns a map of status codes to quantities GET /store/inventory */ 6 | export async function getInventory(options?: { [key: string]: any }) { 7 | return request>('/store/inventory', { 8 | method: 'GET', 9 | ...(options || {}), 10 | }); 11 | } 12 | 13 | /** Place an order for a pet POST /store/order */ 14 | export async function placeOrder(body: API.Order, options?: { [key: string]: any }) { 15 | return request('/store/order', { 16 | method: 'POST', 17 | data: body, 18 | ...(options || {}), 19 | }); 20 | } 21 | 22 | /** Find purchase order by ID For valid response try integer IDs with value >= 1 and <= 10. Other values will generated exceptions GET /store/order/${param0} */ 23 | export async function getOrderById( 24 | // 叠加生成的Param类型 (非body参数swagger默认没有生成对象) 25 | params: API.getOrderByIdParams, 26 | options?: { [key: string]: any }, 27 | ) { 28 | const { orderId: param0, ...queryParams } = params; 29 | return request(`/store/order/${param0}`, { 30 | method: 'GET', 31 | params: { ...queryParams }, 32 | ...(options || {}), 33 | }); 34 | } 35 | 36 | /** Delete purchase order by ID For valid response try integer IDs with positive integer value. Negative or non-integer values will generate API errors DELETE /store/order/${param0} */ 37 | export async function deleteOrder( 38 | // 叠加生成的Param类型 (非body参数swagger默认没有生成对象) 39 | params: API.deleteOrderParams, 40 | options?: { [key: string]: any }, 41 | ) { 42 | const { orderId: param0, ...queryParams } = params; 43 | return request(`/store/order/${param0}`, { 44 | method: 'DELETE', 45 | params: { ...queryParams }, 46 | ...(options || {}), 47 | }); 48 | } 49 | -------------------------------------------------------------------------------- /src/services/swagger/typings.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace API { 2 | type ApiResponse = { 3 | code?: number; 4 | type?: string; 5 | message?: string; 6 | }; 7 | 8 | type Category = { 9 | id?: number; 10 | name?: string; 11 | }; 12 | 13 | type deleteOrderParams = { 14 | /** ID of the order that needs to be deleted */ 15 | orderId: number; 16 | }; 17 | 18 | type deletePetParams = { 19 | api_key?: string; 20 | /** Pet id to delete */ 21 | petId: number; 22 | }; 23 | 24 | type deleteUserParams = { 25 | /** The name that needs to be deleted */ 26 | username: string; 27 | }; 28 | 29 | type findPetsByStatusParams = { 30 | /** Status values that need to be considered for filter */ 31 | status: ('available' | 'pending' | 'sold')[]; 32 | }; 33 | 34 | type findPetsByTagsParams = { 35 | /** Tags to filter by */ 36 | tags: string[]; 37 | }; 38 | 39 | type getOrderByIdParams = { 40 | /** ID of pet that needs to be fetched */ 41 | orderId: number; 42 | }; 43 | 44 | type getPetByIdParams = { 45 | /** ID of pet to return */ 46 | petId: number; 47 | }; 48 | 49 | type getUserByNameParams = { 50 | /** The name that needs to be fetched. Use user1 for testing. */ 51 | username: string; 52 | }; 53 | 54 | type loginUserParams = { 55 | /** The user name for login */ 56 | username: string; 57 | /** The password for login in clear text */ 58 | password: string; 59 | }; 60 | 61 | type Order = { 62 | id?: number; 63 | petId?: number; 64 | quantity?: number; 65 | shipDate?: string; 66 | /** Order Status */ 67 | status?: 'placed' | 'approved' | 'delivered'; 68 | complete?: boolean; 69 | }; 70 | 71 | type Pet = { 72 | id?: number; 73 | category?: Category; 74 | name: string; 75 | photoUrls: string[]; 76 | tags?: Tag[]; 77 | /** pet status in the store */ 78 | status?: 'available' | 'pending' | 'sold'; 79 | }; 80 | 81 | type Tag = { 82 | id?: number; 83 | name?: string; 84 | }; 85 | 86 | type updatePetWithFormParams = { 87 | /** ID of pet that needs to be updated */ 88 | petId: number; 89 | }; 90 | 91 | type updateUserParams = { 92 | /** name that need to be updated */ 93 | username: string; 94 | }; 95 | 96 | type uploadFileParams = { 97 | /** ID of pet to update */ 98 | petId: number; 99 | }; 100 | 101 | type User = { 102 | id?: number; 103 | username?: string; 104 | firstName?: string; 105 | lastName?: string; 106 | email?: string; 107 | password?: string; 108 | phone?: string; 109 | /** User Status */ 110 | userStatus?: number; 111 | }; 112 | } 113 | -------------------------------------------------------------------------------- /src/services/swagger/user.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | /* eslint-disable */ 3 | import { request } from '@umijs/max'; 4 | 5 | /** Create user This can only be done by the logged in user. POST /user */ 6 | export async function createUser(body: API.User, options?: { [key: string]: any }) { 7 | return request('/user', { 8 | method: 'POST', 9 | data: body, 10 | ...(options || {}), 11 | }); 12 | } 13 | 14 | /** Get user by user name GET /user/${param0} */ 15 | export async function getUserByName( 16 | // 叠加生成的Param类型 (非body参数swagger默认没有生成对象) 17 | params: API.getUserByNameParams, 18 | options?: { [key: string]: any }, 19 | ) { 20 | const { username: param0, ...queryParams } = params; 21 | return request(`/user/${param0}`, { 22 | method: 'GET', 23 | params: { ...queryParams }, 24 | ...(options || {}), 25 | }); 26 | } 27 | 28 | /** Updated user This can only be done by the logged in user. PUT /user/${param0} */ 29 | export async function updateUser( 30 | // 叠加生成的Param类型 (非body参数swagger默认没有生成对象) 31 | params: API.updateUserParams, 32 | body: API.User, 33 | options?: { [key: string]: any }, 34 | ) { 35 | const { username: param0, ...queryParams } = params; 36 | return request(`/user/${param0}`, { 37 | method: 'PUT', 38 | params: { ...queryParams }, 39 | data: body, 40 | ...(options || {}), 41 | }); 42 | } 43 | 44 | /** Delete user This can only be done by the logged in user. DELETE /user/${param0} */ 45 | export async function deleteUser( 46 | // 叠加生成的Param类型 (非body参数swagger默认没有生成对象) 47 | params: API.deleteUserParams, 48 | options?: { [key: string]: any }, 49 | ) { 50 | const { username: param0, ...queryParams } = params; 51 | return request(`/user/${param0}`, { 52 | method: 'DELETE', 53 | params: { ...queryParams }, 54 | ...(options || {}), 55 | }); 56 | } 57 | 58 | /** Creates list of users with given input array POST /user/createWithArray */ 59 | export async function createUsersWithArrayInput( 60 | body: API.User[], 61 | options?: { [key: string]: any }, 62 | ) { 63 | return request('/user/createWithArray', { 64 | method: 'POST', 65 | data: body, 66 | ...(options || {}), 67 | }); 68 | } 69 | 70 | /** Creates list of users with given input array POST /user/createWithList */ 71 | export async function createUsersWithListInput(body: API.User[], options?: { [key: string]: any }) { 72 | return request('/user/createWithList', { 73 | method: 'POST', 74 | data: body, 75 | ...(options || {}), 76 | }); 77 | } 78 | 79 | /** Logs user into the system GET /user/login */ 80 | export async function loginUser( 81 | // 叠加生成的Param类型 (非body参数swagger默认没有生成对象) 82 | params: API.loginUserParams, 83 | options?: { [key: string]: any }, 84 | ) { 85 | return request('/user/login', { 86 | method: 'GET', 87 | params: { 88 | ...params, 89 | }, 90 | ...(options || {}), 91 | }); 92 | } 93 | 94 | /** Logs out current logged in user session GET /user/logout */ 95 | export async function logoutUser(options?: { [key: string]: any }) { 96 | return request('/user/logout', { 97 | method: 'GET', 98 | ...(options || {}), 99 | }); 100 | } 101 | -------------------------------------------------------------------------------- /src/typings.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'slash2'; 2 | declare module '*.css'; 3 | declare module '*.less'; 4 | declare module '*.scss'; 5 | declare module '*.sass'; 6 | declare module '*.svg'; 7 | declare module '*.png'; 8 | declare module '*.jpg'; 9 | declare module '*.jpeg'; 10 | declare module '*.gif'; 11 | declare module '*.bmp'; 12 | declare module '*.tiff'; 13 | declare module 'omit.js'; 14 | declare module 'numeral'; 15 | declare module '@antv/data-set'; 16 | declare module 'mockjs'; 17 | declare module 'react-fittext'; 18 | declare module 'bizcharts-plugin-slider'; 19 | 20 | declare const REACT_APP_ENV: 'test' | 'dev' | 'pre' | false; 21 | -------------------------------------------------------------------------------- /tests/setupTests.jsx: -------------------------------------------------------------------------------- 1 | const localStorageMock = { 2 | getItem: jest.fn(), 3 | setItem: jest.fn(), 4 | removeItem: jest.fn(), 5 | clear: jest.fn(), 6 | }; 7 | 8 | global.localStorage = localStorageMock; 9 | 10 | Object.defineProperty(URL, 'createObjectURL', { 11 | writable: true, 12 | value: jest.fn(), 13 | }); 14 | 15 | class Worker { 16 | constructor(stringUrl) { 17 | this.url = stringUrl; 18 | this.onmessage = () => {}; 19 | } 20 | 21 | postMessage(msg) { 22 | this.onmessage(msg); 23 | } 24 | } 25 | window.Worker = Worker; 26 | 27 | /* eslint-disable global-require */ 28 | if (typeof window !== 'undefined') { 29 | // ref: https://github.com/ant-design/ant-design/issues/18774 30 | if (!window.matchMedia) { 31 | Object.defineProperty(global.window, 'matchMedia', { 32 | writable: true, 33 | configurable: true, 34 | value: jest.fn(() => ({ 35 | matches: false, 36 | addListener: jest.fn(), 37 | removeListener: jest.fn(), 38 | })), 39 | }); 40 | } 41 | if (!window.matchMedia) { 42 | Object.defineProperty(global.window, 'matchMedia', { 43 | writable: true, 44 | configurable: true, 45 | value: jest.fn((query) => ({ 46 | matches: query.includes('max-width'), 47 | addListener: jest.fn(), 48 | removeListener: jest.fn(), 49 | })), 50 | }); 51 | } 52 | } 53 | const errorLog = console.error; 54 | Object.defineProperty(global.window.console, 'error', { 55 | writable: true, 56 | configurable: true, 57 | value: (...rest) => { 58 | const logStr = rest.join(''); 59 | if (logStr.includes('Warning: An update to %s inside a test was not wrapped in act(...)')) { 60 | return; 61 | } 62 | errorLog(...rest); 63 | }, 64 | }); 65 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "importHelpers": true, 7 | "jsx": "preserve", 8 | "esModuleInterop": true, 9 | "sourceMap": true, 10 | "baseUrl": "./", 11 | "skipLibCheck": true, 12 | "experimentalDecorators": true, 13 | "strict": true, 14 | "resolveJsonModule": true, 15 | "allowSyntheticDefaultImports": true, 16 | "paths": { 17 | "@/*": ["./src/*"], 18 | "@@/*": ["./src/.umi/*"], 19 | "@@test/*": ["./src/.umi-test/*"] 20 | } 21 | }, 22 | "include": ["./**/*.d.ts", "./**/*.ts", "./**/*.tsx"] 23 | } 24 | -------------------------------------------------------------------------------- /types/cache/cache.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /types/cache/mock/mock.cache.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trueai-org/midjourney-proxy-webui/1ceaa67750eb8e4db1e4cb38a3b8f0aca6e53cad/types/cache/mock/mock.cache.js --------------------------------------------------------------------------------