diff --git a/.env.development b/.env.development index 0cd2290..e5d5e04 100644 --- a/.env.development +++ b/.env.development @@ -1,25 +1,9 @@ # 系统环境 VITE_APP_ENV=development -# 请求地址 (商业模式会走这条地址) -VITE_APP_REQUEST_HOST=https://api.aizj.top/api - -# 内置请求地址和KEY设置 -VITE_APP_AI_BASE_URL=https://api.openai.com - -# Api 请求使用的 key, 支持多个 key,以逗号分隔(,) -VITE_APP_AI_KEYS= - -# 多个 key 时的调度策略模式 随机(random) -VITE_APP_AI_KEY_STRATEGY=random +# 请求地址 +VITE_APP_REQUEST_HOST=http://127.0.0.1:3200 # APP 名称&Logo VITE_APP_TITLE=ChatGpt -VITE_APP_LOGO=https://cdn.jsdelivr.net/gh/duogongneng/testuitc/svg-1681898659579.svg - -# web 应用模式 -# 商业模式 --- business -# 代理模式 --- proxy -# 混合模式 --- mix -VITE_APP_MODE=mix - +VITE_APP_LOGO=https://u1.dl0.cn/icon/openailogo.svg diff --git a/.env.production b/.env.production index 43be341..d91a8e4 100644 --- a/.env.production +++ b/.env.production @@ -1,24 +1,9 @@ # 系统环境 VITE_APP_ENV=production -# 请求地址 (商业模式会走这条地址) -VITE_APP_REQUEST_HOST=https://api.aizj.top/api - -# 内置请求地址和KEY设置 -VITE_APP_AI_BASE_URL=https://api.openai.com - -# Api 请求使用的 key, 支持多个 key,以逗号分隔(,) -VITE_APP_AI_KEYS= - -# 多个 key 时的调度策略模式 随机(random) -VITE_APP_AI_KEY_STRATEGY=random +# 请求地址 +VITE_APP_REQUEST_HOST= # APP 名称&Logo VITE_APP_TITLE=ChatGpt -VITE_APP_LOGO=https://cdn.jsdelivr.net/gh/duogongneng/testuitc/svg-1681898659579.svg - -# web 应用模式 -# 商业模式 --- business -# 代理模式 --- proxy -# 混合模式 --- mix -VITE_APP_MODE=proxy +VITE_APP_LOGO=https://u1.dl0.cn/icon/openailogo.svg diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 3daafd6..dcddc00 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -5,8 +5,7 @@ module.exports = { 'node': true, 'es6': true }, - 'overrides': [ - ], + 'overrides': [], 'parser': '@typescript-eslint/parser', 'parserOptions': { 'ecmaVersion': 'latest', diff --git a/.gitignore b/.gitignore index dc1104f..9219a16 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ node_modules dist dist-ssr *.local +.vite # Editor directories and files .vscode/* @@ -24,3 +25,34 @@ dist-ssr *.njsproj *.sln *.sw? + + +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +*.pem + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/README-CN.md b/README-CN.md index 917a1d2..4d7eabe 100644 --- a/README-CN.md +++ b/README-CN.md @@ -3,38 +3,47 @@

ChatGPT Web

-[English](https://github.com/79E/ChatGpt-Web) / 简体中文 +English / [简体中文](https://github.com/79E/ChatGpt-Web/blob/master/README-CN.md) A commercially-viable ChatGpt web application built with React. 可部署商业化的 ChatGpt 网页应用。 -[Proxy Demo]() / [Business Demo](https://chatgpt79.vercel.app/) / [Issues](https://github.com/79E/ChatGPT-Web/issues) / [Buy Me a Coffee](https://www.buymeacoffee.com/beggar) - -[代理(proxy)演示](https://chatgpt79.vercel.app/) / [商业(business)演示](https://aizj.top/) / [反馈](https://github.com/79E/ChatGPT-Web/issues) / [赞助我](https://www.imageoss.com/images/2023/05/06/e38f4a42046a1909773b955c56468d6b83fcd9b5d593c449.jpg) +[Issues](https://github.com/79E/ChatGPT-Web/issues) / [Buy Me a Coffee](https://www.buymeacoffee.com/beggar) / [赞助我](https://files.catbox.moe/o0znrg.JPG) [![Deploy to Vercel](https://vercel.com/button)](https://vercel.com/import/project?template=https://github.com/79E/ChatGpt-Web) -![cover](https://cdn.jsdelivr.net/gh/duogongneng/testuitc/1683604333996c1.png) -![cover](https://cdn.jsdelivr.net/gh/duogongneng/testuitc/1683604333960c2.png) + + +## 🐶 演示 +### 页面链接 +[Web 演示: https://www.aizj.top/](https://www.aizj.top/) -![群组](https://files.catbox.moe/hqwrq4.png) +[Admin 演示: https://www.aizj.top/admin](https://www.aizj.top/admin) - +如需帮助请提交 [Issues](https://github.com/79E/ChatGPT-Web/issues) 或赞赏时留下联系方式。 +### 页面截图 + +![cover](https://files.catbox.moe/tp963e.png) +![cover](https://files.catbox.moe/y5avbx.png) +![cover](https://files.catbox.moe/k16jsz.png) +![cover](https://files.catbox.moe/8o5oja.png) ## 🤖 主要功能 -- 用户系统可对使用进行相关限制 -- 精心设计的 UI,响应式设计。 +- 后台管理系统,可对用户,Token,商品,卡密等进行管理 +- 精心设计的 UI,响应式设计 - 极快的首屏加载速度(~100kb) +- 支持Midjourney绘画和DALL·E模型绘画,GPT4等应用 - 海量的内置 prompt 列表,来自[中文](https://github.com/PlexPt/awesome-chatgpt-prompts-zh)和[英文](https://github.com/f/awesome-chatgpt-prompts) - 一键导出聊天记录,完整的 Markdown 支持 - 支持自定义API地址(如:[openAI](https://api.openai.com) / [API2D](https://api2d.com/r/192767)) + ## 🎮 开始使用 **Node 环境** -`node` 需要 `^16 || ^18 || ^19` 版本(node >= 16),可以使用 nvm 管理本地多个 node 版本。 +`node` 需要 `^16 || ^18 || ^19` 版本(node >= 16.19.0),可以使用 nvm 管理本地多个 node 版本。 ``` # 查看 node 版本 @@ -60,7 +69,8 @@ yarn install **3.运行** ``` -yarn dev +# web项目启动 +yarn dev:web ``` **4.打包** @@ -68,7 +78,6 @@ yarn dev yarn build ``` - ## ⛺️ 环境变量 > 本项目大多数配置项都通过环境变量来设置。 @@ -85,61 +94,32 @@ Chat Web 标题名称。 Chat Web Logo。 -#### `VITE_APP_MODE` - -应用模式可选:商业模式(business)代理模式(proxy)混合模式(mix) - -#### `VITE_APP_AI_BASE_URL` - -可在内置请求地址设置(如用户未设置自己的key则走这里) - -#### `VITE_APP_AI_KEYS` - - Api 请求使用的 key, 支持多个 key,以逗号分隔(,) - ## 🚧 开发 > 强烈不建议在本地进行开发或者部署,由于一些技术原因,很难在本地配置好 OpenAI API 代理,除非你能保证可以直连 OpenAI 服务器。 #### 本地开发 -1. 安装 nodejs 和 yarn,具体细节请询问 ChatGPT; -2. 执行 `yarn install && yarn dev` 即可。 +1. 安装 nodejs 和 yarn具体细节请询问 ChatGPT +2. 执行 `yarn install` 即可 +3. web项目开发 `yarn dev:web` +4. 服务端项目开发 `yarn dev` +5. 打包项目 `yarn build` #### 服务端 -1. 目前服务端还不完善所以暂时未开源 -2. 前端请求服务端的 [接口文档](https://console-docs.apipost.cn/preview/dcf9a900ac5a1154/00eeb0b3f589d8e6) 你们可以按照这个接口文档进行开发 +1. 前端请求服务端的 [接口文档](https://console-docs.apipost.cn/preview/38826c52f656ef05/044846bd536b67bb) 你们可以按照这个接口文档进行开发 +2. 如需帮助请提交 [Issues](https://github.com/79E/ChatGPT-Web/issues) 或赞赏时留下联系方式。 ## 🎯 部署 -> 直接将打包好的 `dist` 目录上传到服务器即可。WEB项目暂时不直接访问 OpenAI API 所有不要求服务器地址。 +> 直接将`WEB`项目打包好的 `dist` 目录上传到服务器即可。注意服务器IP地址位置! ### Vercel 如果你将其托管在自己的 Vercel 服务器上,可点击 deploy 按钮来开始你的部署! [![Deploy to Vercel](https://vercel.com/button)](https://vercel.com/import/project?template=https://github.com/79E/ChatGpt-Web) -
- 设置 Vercel 的指导 - -1. 前往 [vercel.com](https://vercel.com/) -1. 点击 `Log in` - ![](https://files.catbox.moe/tct1wg.png) -1. 点击 `Continue with GitHub` 通过 GitHub 进行登录 - ![](https://files.catbox.moe/btd78j.jpeg) -1. 登录 GitHub 并允许访问所有存储库(如果系统这样提示) -1. Fork 这个仓库 -1. 返回到你的 [Vercel dashboard](https://vercel.com/dashboard) -1. 选择 `Import Project` - ![](https://files.catbox.moe/qckos0.png) -1. 选择 `Import Git Repository` - ![](https://files.catbox.moe/pqub9q.png) -1. 选择 root 并将所有内容保持不变,并且只需添加名为 PAT_1 的环境变量(如图所示),其中将包含一个个人访问令牌(PAT),你可以在[这里](https://github.com/settings/tokens/new)轻松创建(保留默认,并且只需要命名下,名字随便) - ![](https://files.catbox.moe/0ez4g7.png) -1. 点击 deploy,这就完成了,查看你的域名就可使用 API 了! - -
- +如需帮助请提交 [Issues](https://github.com/79E/ChatGPT-Web/issues) 或赞赏时留下联系方式。 ## 🧘 贡献者 diff --git a/README.md b/README.md index 169a4bf..67cd272 100644 --- a/README.md +++ b/README.md @@ -9,31 +9,47 @@ A commercially-viable ChatGpt web application built with React. 可部署商业化的 ChatGpt 网页应用。 -[Proxy Demo]() / [Business Demo](https://chatgpt79.vercel.app/) / [Issues](https://github.com/79E/ChatGPT-Web/issues) / [Buy Me a Coffee](https://www.buymeacoffee.com/beggar) - -[代理(proxy)演示](https://chatgpt79.vercel.app/) / [商业(business)演示](https://aizj.top/) / [反馈](https://github.com/79E/ChatGPT-Web/issues) / [赞助我](https://www.imageoss.com/images/2023/05/06/e38f4a42046a1909773b955c56468d6b83fcd9b5d593c449.jpg) +[Issues](https://github.com/79E/ChatGPT-Web/issues) / [Buy Me a Coffee](https://www.buymeacoffee.com/beggar) / [赞助我](https://files.catbox.moe/o0znrg.JPG) [![Deploy to Vercel](https://vercel.com/button)](https://vercel.com/import/project?template=https://github.com/79E/ChatGpt-Web) -![cover](https://cdn.jsdelivr.net/gh/duogongneng/testuitc/1683604333996c1.png) -![cover](https://cdn.jsdelivr.net/gh/duogongneng/testuitc/1683604333960c2.png) - -![群组](https://files.catbox.moe/hqwrq4.png) +## 🐶 Demo +### Link + +[Web Demo: https://www.aizj.top/](https://www.aizj.top/) + +[Web 演示: https://www.aizj.top/](https://www.aizj.top/) + +[Admin Demo: https://www.aizj.top/admin](https://www.aizj.top/admin) + +[Admin 演示: https://www.aizj.top/admin](https://www.aizj.top/admin) + +If you need help, please submit [Issues](https://github.com/79E/ChatGPT-Web/issues) Or leave contact information when appreciating. +### Snapshot + +![cover](https://files.catbox.moe/tp963e.png) +![cover](https://files.catbox.moe/y5avbx.png) +![cover](https://files.catbox.moe/k16jsz.png) +![cover](https://files.catbox.moe/8o5oja.png) + +![赞赏](https://files.catbox.moe/o0znrg.JPG) ## 🤖 Major Function -- The user system can impose relevant restrictions on usage -- Carefully designed UI, responsive design. -- Extremely fast first screen loading speed(~100kb) -- Massive built-in prompt list from[Chinese](https://github.com/PlexPt/awesome-chatgpt-prompts-zh) AND [English](https://github.com/f/awesome-chatgpt-prompts) -- One click export of chat records, complete Markdown support. -- Support for custom API addresses(example:[openAI](https://api.openai.com) / [API2D](https://api2d.com/r/192767)) +- The backend management system can manage users, tokens, products, card passwords, etc +- Carefully designed UI, responsive design +- Extremely fast first screen loading speed (~100kb) +- Supports Midjournal painting, DALL · E model painting, GPT4 and other applications +- Massive built-in prompt list from [Chinese](https://github.com/PlexPt/awesome-chatgpt-prompts-zh)And [English](https://github.com/f/awesome-chatgpt-prompts) +- One click export of chat records, complete Markdown support +- Support for custom API addresses ([openAI](https://api.openai.com) / [API2D]( https://api2d.com/r/192767 )) + ## 🎮 Start Using **Node** -Node requires version `^ 16 | | ^ 18 | | ^ 19 `(node>=16), and NVM can be used to manage multiple local node versions. +Node requires version `^ 16 | | ^ 18 | | ^ 19 `(node >= 16.19.0), and NVM can be used to manage multiple local node versions. ``` # View node version @@ -59,7 +75,8 @@ yarn install **3.Run** ``` -yarn dev +# run web +yarn dev:web ``` **4.Build** @@ -82,19 +99,7 @@ Chat Web title. #### `VITE_APP_LOGO` -Chat Web Logo。 - -#### `VITE_APP_MODE` - -Optional application mode: business mode proxy pattern mixed mode. - -#### `VITE_APP_AI_BASE_URL` - -Built in request address and KEY settings - -#### `VITE_APP_AI_KEYS` - -The key used by Api requests, supporting multiple keys separated by commas (,) +Chat Web Logo. ## 🚧 Develop @@ -102,22 +107,27 @@ The key used by Api requests, supporting multiple keys separated by commas (,) #### Local development -1. Install `Nodejs` and `Yarn`, please consult ChatGPT for specific details; -2. Execute `yarn install && yarn dev`. +1. Please consult ChatGPT for specific details on installing `Nodejs` and `Yarn` +2. Just execute `yarn install` +3. Web Project Development `yarn dev:web` +4. Server side project development `yarn dev` +5. Package Project `yarn build` #### Server side -1. Currently, the server is not yet fully developed, so it is currently not open source. -2. Front end request server's [interface document](https://console-docs.apipost.cn/preview/dcf9a900ac5a1154/00eeb0b3f589d8e6) You can develop according to this interface document. +1. Front end request server's [interface document](https://console-docs.apipost.cn/preview/38826c52f656ef05/044846bd536b67bb) You can develop according to this interface document. +2. If you need help, please submit [Issues](https://github.com/79E/ChatGPT-Web/issues) Or leave contact information when appreciating. ## 🎯 Arrange -> Simply upload the packaged `dist` directory to the server. The WEB project temporarily does not directly access the OpenAI API and does not require a server address. +> Simply upload the packaged `dist` directory of the `WEB` project to the server. Pay attention to the server IP address location! ### Vercel If you host it on your own Vercel server, you can click the deploy button to start your deployment! [![Deploy to Vercel](https://vercel.com/button)](https://vercel.com/import/project?template=https://github.com/79E/ChatGpt-Web) +If you need help, please submit [Issues](https://github.com/79E/ChatGPT-Web/issues) Or leave contact information when appreciating. + ## 🧘 Contributor [See project contributor list](https://github.com/79E/ChatGPT-Web/graphs/contributors) diff --git a/package.json b/package.json index 05281bb..48a6fc5 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,6 @@ "private": false, "version": "1.0.0", "description": "ChatGPT Web", - "type": "module", "author": "79E", "keywords": [ "chatgpt-web", @@ -14,33 +13,51 @@ "react" ], "scripts": { - "dev": "vite", - "build": "tsc && vite build", + "tsc": "tsc -p tsconfig.node.json", + "dev": "ts-node-dev --project tsconfig.node.json server/index.ts", + "dev:web": "vite", + "start": "node build/index.js", + "build": "tsc -p tsconfig.node.json && vite build", "preview": "vite preview", - "eslint": "eslint src/**/*.{ts,tsx}", - "eslint:fix": "eslint src/**/*.{ts,tsx} --fix", - "prepare": "husky install" + "eslint": "eslint \"src/**/*.{ts,tsx}\" \"server/**/*.{ts,tsx}\"", + "eslint:fix": "eslint \"src/**/*.{ts,tsx}\" \"server/**/*.{ts,tsx}\" --fix", + "prepare": "husky install" }, "dependencies": { "@ant-design/icons": "^5.0.1", "@ant-design/pro-components": "^2.4.4", "@traptitech/markdown-it-katex": "^3.6.0", - "antd": "^5.4.2", + "antd": "5.5.0", + "bull": "^4.10.4", + "cors": "^2.8.5", + "express": "^4.18.2", + "gpt-tokens": "^1.0.7", "highlight.js": "^11.7.0", "html2canvas": "^1.4.1", + "ioredis": "^5.3.2", "markdown-it": "^13.0.1", "markdown-it-link-attributes": "^4.0.1", + "mysql2": "^3.3.1", + "node-fetch": "^2.6.11", + "node-schedule": "^2.1.1", + "nodemailer": "^6.9.2", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.10.0", + "sequelize": "^6.31.1", + "ts-node-dev": "^2.0.0", + "tslib": "^2.5.1", "zustand": "^4.3.7" }, "devDependencies": { "@commitlint/cli": "^17.6.1", "@commitlint/config-conventional": "^17.6.1", + "@types/cors": "^2.8.13", + "@types/express": "^4.17.17", "@types/markdown-it": "^12.2.3", "@types/markdown-it-link-attributes": "^3.0.1", - "@types/node": "^18.15.11", + "@types/node": "^20.1.3", + "@types/nodemailer": "^6.4.7", "@types/react": "^18.0.28", "@types/react-dom": "^18.0.11", "@typescript-eslint/eslint-plugin": "^5.59.0", @@ -58,8 +75,10 @@ "eslint-plugin-react-hooks": "^4.6.0", "husky": "^8.0.3", "less": "^4.1.3", + "ping": "^0.4.4", "prettier": "^2.8.8", - "typescript": "*", + "ts-node": "^10.9.1", + "typescript": "^5.0.4", "vite": "^4.2.0" }, "main": "index.js", diff --git a/server/index.ts b/server/index.ts new file mode 100644 index 0000000..e1cf464 --- /dev/null +++ b/server/index.ts @@ -0,0 +1,26 @@ +import express from 'express' +import path from 'path' +import cors from 'cors' + +const app = express() + +app.use(cors()) + +// 获取静态目录 +app.use(express.static(path.join(__dirname, '../dist'))) +app.use(express.json()) +app.use(express.urlencoded({ extended: false })) + +app.all('/api/*', (req, res) => { + res.status(404).json({ code: -1, data: [], message: 'The current access API address does not exist' }) +}) + +// 渲染页面 +app.get('*', (req, res) => { + res.sendFile(path.join(__dirname, '../dist', 'index.html')) +}) + +// 启动服务器 +app.listen(3333, () => { + console.log('Server is running on port 3333') +}) \ No newline at end of file diff --git a/src/components/ConfigModal/index.tsx b/src/components/ConfigModal/index.tsx index 7d13dfb..73e222a 100644 --- a/src/components/ConfigModal/index.tsx +++ b/src/components/ConfigModal/index.tsx @@ -16,8 +16,6 @@ type Props = { } function ConfigModal(props: Props) { - const isProxy = import.meta.env.VITE_APP_MODE !== 'business' - const [chatGptConfigform] = Form.useForm() const onCancel = () => { props.onCancel() @@ -64,7 +62,7 @@ function ConfigModal(props: Props) { }} /> - {isProxy && ( + {/* {( <> - )} - + )} */} + {/* - + */} diff --git a/src/components/Global/index.tsx b/src/components/Global/index.tsx index 6ef00b8..62128c2 100644 --- a/src/components/Global/index.tsx +++ b/src/components/Global/index.tsx @@ -1,27 +1,18 @@ -import useStore from '@/store' -import { fetchUserInfo } from '@/store/async' +import { chatStore, configStore } from '@/store' +// import { fetchUserInfo } from '@/store/async' import { useEffect } from 'react' import LoginModal from '../LoginModal' import ConfigModal from '../ConfigModal' +import { userStore } from '@/store' type Props = { children: React.ReactElement } function Global(props: Props) { - const { - config, - models, - token, - chats, - configModal, - changeConfig, - setConfigModal, - addChat, - changeSelectChatId, - loginModal, - setLoginModal - } = useStore() + const { models, config, configModal, changeConfig, setConfigModal } = configStore() + const { chats, addChat, changeSelectChatId } = chatStore() + const { token, loginModal, setLoginModal } = userStore() useEffect(() => { if (chats.length <= 0) { diff --git a/src/components/GoodsList/index.module.less b/src/components/GoodsList/index.module.less index 99ab7d8..2a891db 100644 --- a/src/components/GoodsList/index.module.less +++ b/src/components/GoodsList/index.module.less @@ -1,10 +1,10 @@ .goodsList { display: flex; flex-wrap: wrap; - gap: 20px; + gap: 10px; &_item{ - width: 140px; - min-height: 170px; + width: 130px; + min-height: 164px; background-color: #fff; border-radius: 8px; padding-top: 30px; @@ -57,7 +57,7 @@ &_button{ margin-top: auto; - width: 136px; + width: 130px; height: 40px; line-height: 40px; font-size: 16px; @@ -72,4 +72,4 @@ transition: all 0.4s ease-in-out; } } -} \ No newline at end of file +} diff --git a/src/components/GoodsList/index.tsx b/src/components/GoodsList/index.tsx index d487138..a145d61 100644 --- a/src/components/GoodsList/index.tsx +++ b/src/components/GoodsList/index.tsx @@ -13,10 +13,10 @@ function GoodsList(props: { list: Array; onClick: (item: ProductInf props.onClick?.(item) }} > -

{item.integral}分

-

{item.amount}元

+ {item.integral ?

{item.integral}分

:

{item.day}天

} +

{item.price / 100}元

立即充值
- 特惠 + {item.badge} ) })} diff --git a/src/components/HeaderRender/index.module.less b/src/components/HeaderRender/index.module.less index 74a5388..9e99f11 100644 --- a/src/components/HeaderRender/index.module.less +++ b/src/components/HeaderRender/index.module.less @@ -10,6 +10,9 @@ &__logo{ display: flex; height: 28px; + img{ + width: 28px; + } h1{ margin-block: 0; margin-inline: 0; diff --git a/src/components/HeaderRender/index.tsx b/src/components/HeaderRender/index.tsx index f4b8e85..3124b0d 100644 --- a/src/components/HeaderRender/index.tsx +++ b/src/components/HeaderRender/index.tsx @@ -12,29 +12,26 @@ import { UserOutlined, WalletOutlined } from '@ant-design/icons' -import useStore from '@/store' +import { userStore } from '@/store' import { Avatar, Button, Dropdown } from 'antd' -import { getAiKey, getEmailPre } from '@/utils' +import { getEmailPre } from '@/utils' import MenuList from '../MenuList' -import { getKeyUsage, getUserInfo } from '@/request/api' import { useNavigate } from 'react-router-dom' +import { fetchUserInfo } from '@/store/user/async' function HeaderRender(props: HeaderViewProps, defaultDom: React.ReactNode) { - const isProxy = import.meta.env.VITE_APP_MODE === 'proxy' - const navigate = useNavigate() - const { token, user_detail, logout, setLoginModal, config } = useStore() + const { token, user_info, logout, setLoginModal } = userStore() const renderLogo = useMemo(() => { - if (typeof props.logo === 'string') - return + if (typeof props.logo === 'string') return return <>{props.logo} }, [props.logo]) useEffect(() => { onRefreshBalance() - }, [user_detail, token, config.api, config.api_key]) + }, [token]) const [balance, setBalance] = useState<{ number: string | number @@ -45,11 +42,10 @@ function HeaderRender(props: HeaderViewProps, defaultDom: React.ReactNode) { }) function onRefreshBalance() { - const systemConfig = getAiKey(config) setBalance((b) => ({ ...b, loading: true })) if (token) { // 获取用户信息 - getUserInfo() + fetchUserInfo() .then((res) => { if (res.code) return setBalance((b) => ({ ...b, number: res.data.integral, loading: false })) @@ -57,14 +53,6 @@ function HeaderRender(props: HeaderViewProps, defaultDom: React.ReactNode) { .finally(() => { setBalance((b) => ({ ...b, loading: false })) }) - } else if (systemConfig.api_key && systemConfig.api) { - getKeyUsage(systemConfig.api, systemConfig.api_key) - .then((res) => { - setBalance((b) => ({ number: res, loading: false })) - }) - .finally(() => { - setBalance((b) => ({ ...b, loading: false })) - }) } else { setBalance((b) => ({ ...b, loading: false })) } @@ -84,73 +72,86 @@ function HeaderRender(props: HeaderViewProps, defaultDom: React.ReactNode) { {!props.isMobile && }
- {isProxy ? ( - <> - ) : token ? ( - , - label: '用户信息', - onClick: () => { - navigate('/shop') - } - }, - { - key: 'wodeyue', - icon: , - label: '我的余额', - onClick: () => { - navigate('/shop') - } - }, - { - key: 'xiaofeijilu', - icon: , - label: '消费记录', - onClick: () => { - navigate('/shop') - } - }, - { - key: 'zhifuzhongxin', - icon: , - label: '支付中心', - onClick: () => { - navigate('/shop') - } - }, - { - key: 'logout', - icon: , - label: '退出登录', - onClick: () => { - logout() - } - } - ] + {token ? ( +
-
- - {!props.isMobile && ( - - {getEmailPre(user_detail?.account)} - - )} + , + label: '立即签到', + onClick: () => { + navigate('/shop') + } + }, + { + key: 'wodeyue', + icon: , + label: '我的余额', + onClick: () => { + navigate('/shop') + } + }, + { + key: 'xiaofeijilu', + icon: , + label: '消费记录', + onClick: () => { + navigate('/shop') + } + }, + { + key: 'logout', + icon: , + label: '退出登录', + onClick: () => { + logout() + navigate('/login') + } + } + ] + }} + > +
+ + {!props.isMobile && ( + + {getEmailPre(user_info?.account)} + + )} +
+
+
{ + onRefreshBalance() + }} + > +

余额:{balance.number}

- +
) : ( + ] + } + ] + + return ( +
+ { + const res = await getAdminCarmi({ + page: params.current || 1, + page_size: params.pageSize || 10 + }) + return Promise.resolve({ + data: res.data.rows, + total: res.data.count, + success: true + }) + }} + toolbar={{ + actions: [ + + ] + }} + rowKey="id" + search={false} + bordered + /> + + { + setGenerateModal({ + open: false, + type: 'integral', + end_time: '', + quantity: 1, + loading: false, + reward: 10, + result: '' + }) + }} + > + + +
+

奖励类型

+ { + setGenerateModal(g => ({ ...g, type: e.target.value })) + }} + defaultValue={generateModal.type} + value={generateModal.type} + > + 积分 + 时长(天) + +
+
+

奖励数量

+ { + if(e){ + setGenerateModal(g => ({ ...g, reward: e })) + } + }} + value={generateModal.reward} + /> +
+
+

有效期截止日期

+ { + const date = new Date() + date.setHours(0, 0, 0, 0) + return current && current.toDate().getTime() < date.getTime(); + }} + onChange={(e) => { + if (e) { + const dateString = formatTime('yyyy-MM-dd', e?.toDate()) + setGenerateModal(g => ({ ...g, end_time: dateString })) + } else { + setGenerateModal(g => ({ ...g, end_time: '' })) + } + }} + /> +
+
+
+

生成数量

+ { + if(e){ + setGenerateModal(g => ({ ...g, quantity: e })) + } + }} + value={generateModal.quantity} + /> +
+
+ { + (generateModal.result && !generateModal.loading) && ( + + ) + } + {generateModal.loading && } +
+ + +
+
+ +
+ ) +} + +export default CarmiPage diff --git a/src/pages/admin/config/index.module.less b/src/pages/admin/config/index.module.less new file mode 100644 index 0000000..726732f --- /dev/null +++ b/src/pages/admin/config/index.module.less @@ -0,0 +1,14 @@ +.config{ + padding: 20px; + border-radius: 20px; + background-color: #fff; + &_form{ + padding: 20px; + border-radius: 20px; + background-color: #fbfbfb; + h3{ + color: #333; + margin-bottom: 12px; + } + } +} \ No newline at end of file diff --git a/src/pages/admin/config/index.tsx b/src/pages/admin/config/index.tsx new file mode 100644 index 0000000..b622085 --- /dev/null +++ b/src/pages/admin/config/index.tsx @@ -0,0 +1,95 @@ +import { + ProFormDigit, + QueryFilter, +} from '@ant-design/pro-components'; +import { Form, message } from 'antd'; +import { useEffect, useState } from 'react'; +import styles from './index.module.less' +import { getAdminConfig, putAdminConfig } from '@/request/adminApi'; +import { ConfigInfo } from '@/types/admin' + +function ConfigPage() { + + const [configs, setConfigs] = useState>([]) + const [rewardForm] = Form.useForm<{ + register_reward: number | string, + signin_reward: number | string, + }>(); + + function getConfigValue (key: string, data: Array){ + const value = data.filter(c => c.name === key)[0] + return value + } + + function onRewardFormSet (data: Array){ + const registerRewardInfo = getConfigValue('register_reward', data) + const signinRewardInfo = getConfigValue('signin_reward', data) + rewardForm.setFieldsValue({ + register_reward: registerRewardInfo.value, + signin_reward: signinRewardInfo.value + }) + } + + + function onGetConfig (){ + getAdminConfig().then((res)=>{ + if(res.code){ + message.error('获取配置错误') + return + } + onRewardFormSet(res.data) + setConfigs(res.data) + }) + } + + useEffect(()=>{ + onGetConfig() + },[]) + + return ( +
+
+

奖励激励

+ { + putAdminConfig(values).then((res)=>{ + if(res.code) { + message.error('保存失败') + return + } + message.success('保存成功') + onGetConfig() + }) + }} + onReset={()=>{ + onRewardFormSet(configs) + }} + size="large" + collapsed={false} + defaultCollapsed={false} + requiredMark={false} + defaultColsNumber={79} + searchText="保存" + resetText="恢复" + > + + + +
+
+ ) +} +export default ConfigPage; \ No newline at end of file diff --git a/src/pages/admin/index.module.less b/src/pages/admin/index.module.less new file mode 100644 index 0000000..2fe8342 --- /dev/null +++ b/src/pages/admin/index.module.less @@ -0,0 +1,3 @@ +.admin { + +} diff --git a/src/pages/admin/index.tsx b/src/pages/admin/index.tsx new file mode 100644 index 0000000..3b918bb --- /dev/null +++ b/src/pages/admin/index.tsx @@ -0,0 +1,136 @@ +import { DefaultFooter, PageContainer, ProLayout } from '@ant-design/pro-components' +import styles from './index.module.less' +import { Link, Outlet, useNavigate } from 'react-router-dom' +import { Dropdown } from 'antd' +import { LogoutOutlined } from '@ant-design/icons' +import React, { useState } from 'react' +import menuList from '@/routers/menu_list' +import { userStore } from '@/store' +import OpenAiLogo from '@/components/OpenAiLogo' + +function AdminPage() { + const navigate = useNavigate() + const { token, user_info, logout } = userStore() + const [selectedKeys, setSelectedKeys] = useState>([]) + if (!token || user_info?.role !== 'administrator') { + return ( +
+ +
+ ) + } + return ( +
+ { + setSelectedKeys([`${location?.pathname}`]) + }} + menuExtraRender={() =>
} + route={menuList.admin} + menuItemRender={(item: any, dom: React.ReactNode) => { + const target = item.path?.indexOf('http') != -1 ? '_blank' : '_self' + return ( + + {dom} + + ) + }} + avatarProps={{ + src: user_info?.avatar, + size: 'small', + title: '超级管理员', + render: (props: any, dom: React.ReactNode) => { + return ( + , + label: '退出登录', + onClick: () => { + logout() + navigate('/login') + } + } + ] + }} + > + {dom} + + ) + } + }} + menuFooterRender={(props: any) => { + if (props?.collapsed) return undefined + return ( +
+
© 2023 Made with love
+
by Chatgpt
+
+ ) + }} + menuProps={{ + onSelect: (e: any) => { + if (e.key.indexOf('http') === -1 && !selectedKeys.includes(e.key)) { + setSelectedKeys([...e.selectedKeys]) + } + }, + onClick: (r: any) => { + if (r.key.indexOf('http') === -1 && !selectedKeys.includes(r.key)) { + setSelectedKeys([...r.keyPath]) + } + }, + selectedKeys: [...selectedKeys] + }} + breadcrumbRender={() => []} + footerRender={() => ( + + )} + > + + + + +
+ ) +} + +export default AdminPage diff --git a/src/pages/admin/message/index.tsx b/src/pages/admin/message/index.tsx new file mode 100644 index 0000000..6f84e1d --- /dev/null +++ b/src/pages/admin/message/index.tsx @@ -0,0 +1,87 @@ +import { getAdminMessages } from '@/request/adminApi'; +import { MessageInfo } from '@/types/admin'; +import { ActionType, ProColumns } from '@ant-design/pro-components'; +import { ProTable } from '@ant-design/pro-components'; +import { Tag } from 'antd'; +import { useRef} from 'react'; + +function MessagePage() { + + const tableActionRef = useRef(); + const columns: ProColumns[] = [ + { + title: 'ID', + dataIndex: 'id', + width: 180, + }, + { + title: '用户ID', + width: 180, + dataIndex: 'user_id', + }, + { + title: '内容', + dataIndex: 'content', + }, + { + title: '角色', + dataIndex: 'role', + render: (_, data)=>{data.role} + }, + { + title: '模型', + dataIndex: 'model', + render: (_, data)=>{data.model} + }, + { + title: '会话ID', + dataIndex: 'parent_message_id', + render: (_, data)=>{data.role} + }, + { + title: '状态值', + dataIndex: 'status', + render: (_, data) => {data.status ? '正常' : '异常'} + }, + { + title: '创建时间', + dataIndex: 'create_time', + }, + { + title: '更新时间', + dataIndex: 'update_time', + }, + ]; + + return ( +
+ { + // 表单搜索项会从 params 传入,传递给后端接口。 + const res = await getAdminMessages({ + page: params.current || 1, + page_size: params.pageSize || 10, + }); + return Promise.resolve({ + data: res.data.rows, + total: res.data.count, + success: true, + }); + }} + toolbar={{ + actions: [], + }} + rowKey="id" + search={false} + bordered + /> +
+ ) +} + +export default MessagePage; \ No newline at end of file diff --git a/src/pages/admin/product/index.tsx b/src/pages/admin/product/index.tsx new file mode 100644 index 0000000..1b1faf7 --- /dev/null +++ b/src/pages/admin/product/index.tsx @@ -0,0 +1,274 @@ +import { getAdminProducts, delAdminProduct, postAdminProduct, putAdminProduct } from '@/request/adminApi'; +import { ProductInfo } from '@/types/admin'; +import { ActionType, ModalForm, ProColumns, ProFormDigit, ProFormGroup, ProFormRadio, ProFormText } from '@ant-design/pro-components'; +import { ProTable } from '@ant-design/pro-components'; +import { Button, Form, Tag, message } from 'antd'; +import { useRef, useState } from 'react'; + +function ProductPage() { + + const tableActionRef = useRef(); + const [form] = Form.useForm(); + const [edidInfoModal, setEdidInfoModal] = useState<{ + open: boolean, + info: ProductInfo | undefined + }>({ + open: false, + info: undefined + }); + const columns: ProColumns[] = [ + { + title: 'ID', + dataIndex: 'id', + width: 180, + }, + { + title: '标题', + dataIndex: 'title', + }, + { + title: '价格', + dataIndex: 'price', + render: (_, data) => { + return {data.price}分 + } + }, + { + title: '原价', + dataIndex: 'original_price', + render: (_, data) => { + return {data.original_price}分 + } + }, + { + title: '积分/天数', + dataIndex: 'integral', + render: (_, data) => { + return {data.integral ? data.integral + '积分' : data.day ? data.day + '天' : '-'} + } + }, + { + title: '状态值', + dataIndex: 'status', + render: (_, data) => {data.status ? '上架' : '下架'} + }, + { + title: '创建时间', + dataIndex: 'create_time', + }, + { + title: '更新时间', + dataIndex: 'update_time', + }, + { + title: '操作', + width: 160, + valueType: 'option', + fixed: 'right', + render: (_, data) => [ + , + + ] + } + ]; + + return ( +
+ { + // 表单搜索项会从 params 传入,传递给后端接口。 + const res = await getAdminProducts({ + page: params.current || 1, + page_size: params.pageSize || 10, + }); + return Promise.resolve({ + data: res.data.rows, + total: res.data.count, + success: true, + }); + }} + toolbar={{ + actions: [ + + ] + }} + rowKey="id" + search={false} + bordered + /> + + title="商品信息" + open={edidInfoModal.open} + form={form} + initialValues={{ + status: 1 + }} + onOpenChange={(visible) => { + if (!visible) { + form.resetFields(); + } + setEdidInfoModal((info) => { + return { + ...info, + open: visible + } + }) + }} + onFinish={async (values) => { + console.log(values); + if(!values.integral && !values.day){ + message.error('请填写积分或者天数') + return false + } + const data = { ...values } + if(values.integral){ + data.day = 0 + }else if (values.day) { + data.integral = 0 + } + if (edidInfoModal.info?.id) { + console.log('进入编辑') + const res = await putAdminProduct({ + ...data, + id: edidInfoModal.info?.id, + }); + if (res.code) { + message.error('编辑失败') + return false; + } + tableActionRef.current?.reload?.(); + } else { + const res = await postAdminProduct(data); + if (res.code) { + message.error('新增失败') + return false + } + tableActionRef.current?.reloadAndRest?.(); + message.success('提交成功'); + } + return true; + }} + size="large" + modalProps={{ + cancelText: '取消', + okText: '提交' + }} + > + + + + + + + + + + + + + + +

积分与天数只能填写一个(如两个都填写优先拿积分)

+ +
+ ) +} + +export default ProductPage; \ No newline at end of file diff --git a/src/pages/admin/signin/index.tsx b/src/pages/admin/signin/index.tsx new file mode 100644 index 0000000..e1a48c5 --- /dev/null +++ b/src/pages/admin/signin/index.tsx @@ -0,0 +1,73 @@ +import { getAdminSignin } from '@/request/adminApi'; +import { SigninInfo } from '@/types/admin'; +import { ActionType, ProColumns } from '@ant-design/pro-components'; +import { ProTable } from '@ant-design/pro-components'; +import { Tag } from 'antd'; +import { useRef} from 'react'; + +function SigninPage() { + + const tableActionRef = useRef(); + const columns: ProColumns[] = [ + { + title: 'ID', + dataIndex: 'id', + width: 180, + }, + { + title: '用户ID', + width: 180, + dataIndex: 'user_id', + }, + { + title: 'IP', + dataIndex: 'ip', + render: (_, data)=>{data.ip} + }, + { + title: '状态值', + dataIndex: 'status', + render: (_, data) => {data.status ? '签到成功' : '签到失败'} + }, + { + title: '创建时间', + dataIndex: 'create_time', + }, + { + title: '更新时间', + dataIndex: 'update_time', + }, + ]; + + return ( +
+ { + // 表单搜索项会从 params 传入,传递给后端接口。 + const res = await getAdminSignin({ + page: params.current || 1, + page_size: params.pageSize || 10, + }); + return Promise.resolve({ + data: res.data.rows, + total: res.data.count, + success: true, + }); + }} + toolbar={{ + actions: [], + }} + rowKey="id" + search={false} + bordered + /> +
+ ) +} + +export default SigninPage; \ No newline at end of file diff --git a/src/pages/admin/token/index.tsx b/src/pages/admin/token/index.tsx new file mode 100644 index 0000000..b7d4d8d --- /dev/null +++ b/src/pages/admin/token/index.tsx @@ -0,0 +1,252 @@ +import { getAdminTokens, delAdminToken, putAdminToken, postAdminToken, postAdminTokenCheck } from '@/request/adminApi'; +import { TokenInfo } from '@/types/admin'; +import { ActionType, ModalForm, ProColumns, ProFormGroup, ProFormRadio, ProFormText } from '@ant-design/pro-components'; +import { ProTable } from '@ant-design/pro-components'; +import { Button, Form, Tag, message } from 'antd'; +import { useRef, useState } from 'react'; + +function TokenPage() { + + const tableActionRef = useRef(); + const [form] = Form.useForm(); + const [edidInfoModal, setEdidInfoModal] = useState<{ + open: boolean, + info: TokenInfo | undefined + }>({ + open: false, + info: undefined + }); + const columns: ProColumns[] = [ + { + title: 'ID', + dataIndex: 'id', + width: 180, + }, + { + title: 'KEY', + dataIndex: 'key', + width: 200, + }, + { + title: 'HOST', + dataIndex: 'host', + render: (_, data) => { + return {data.host} + } + }, + { + title: '备注', + dataIndex: 'remarks', + }, + { + title: '状态值', + dataIndex: 'status', + render: (_, data) => {data.status ? '正常' : '异常'} + }, + { + title: '额度', + dataIndex: 'limit', + render: (_, data) => { + return ( +
+

总额度:{data.limit}

+

已使用:{data.usage / 100}

+

还剩余:{data.limit - (data.usage / 100)}

+
+ ) + } + }, + { + title: '创建时间', + dataIndex: 'create_time', + }, + { + title: '更新时间', + dataIndex: 'update_time', + }, + { + title: '操作', + width: 160, + valueType: 'option', + fixed: 'right', + render: (_, data) => [ + , + + ] + } + ]; + + return ( +
+ { + // 表单搜索项会从 params 传入,传递给后端接口。 + const res = await getAdminTokens({ + page: params.current || 1, + page_size: params.pageSize || 10, + }); + return Promise.resolve({ + data: res.data.rows, + total: res.data.count, + success: true, + }); + }} + toolbar={{ + actions: [ + , + + ] + }} + rowKey="id" + search={false} + bordered + /> + + title="Token信息" + open={edidInfoModal.open} + form={form} + initialValues={{ + status: 1 + }} + onOpenChange={(visible) => { + if (!visible) { + form.resetFields(); + } + setEdidInfoModal((info) => { + return { + ...info, + open: visible + } + }) + }} + onFinish={async (values) => { + console.log(values); + if (edidInfoModal.info?.id) { + console.log('进入编辑') + const res = await putAdminToken({ + ...values, + id: edidInfoModal.info?.id, + }); + if (res.code) { + message.error('编辑失败') + return false; + } + tableActionRef.current?.reload?.(); + } else { + const res = await postAdminToken(values); + if (res.code) { + message.error('新增失败') + return false + } + tableActionRef.current?.reloadAndRest?.(); + message.success('提交成功'); + } + return true; + }} + size="large" + modalProps={{ + cancelText: '取消', + okText: '提交' + }} + > + + + + + + + +
+ ) +} + +export default TokenPage; \ No newline at end of file diff --git a/src/pages/admin/turnover/index.tsx b/src/pages/admin/turnover/index.tsx new file mode 100644 index 0000000..6a7163d --- /dev/null +++ b/src/pages/admin/turnover/index.tsx @@ -0,0 +1,174 @@ +import { delAdminTurnover, getAdminTurnovers, putAdminTurnover } from '@/request/adminApi'; +import { TurnoverInfo } from '@/types/admin'; +import { ActionType, ModalForm, ProColumns, ProFormGroup, ProFormText } from '@ant-design/pro-components'; +import { ProTable } from '@ant-design/pro-components'; +import { Button, Form, message } from 'antd'; +import { useRef, useState } from 'react'; + +function TurnoverPage() { + + const tableActionRef = useRef(); + const [form] = Form.useForm(); + const [edidInfoModal, setEdidInfoModal] = useState<{ + open: boolean, + info: TurnoverInfo | undefined + }>({ + open: false, + info: undefined + }); + + const columns: ProColumns[] = [ + { + title: 'ID', + dataIndex: 'id', + width: 180, + }, + { + title: '用户ID', + width: 180, + dataIndex: 'user_id', + }, + { + title: '操作', + dataIndex: 'describe', + }, + { + title: '值', + dataIndex: 'value', + render: (_, data) => {data.value} + }, + { + title: '创建时间', + dataIndex: 'create_time', + }, + { + title: '更新时间', + dataIndex: 'update_time', + }, + { + title: '操作', + valueType: 'option', + fixed: 'right', + width: 160, + render: (_, data) => [ + , + + ], + }, + ]; + + return ( +
+ { + // 表单搜索项会从 params 传入,传递给后端接口。 + const res = await getAdminTurnovers({ + page: params.current || 1, + page_size: params.pageSize || 10, + }); + return Promise.resolve({ + data: res.data.rows, + total: res.data.count, + success: true, + }); + }} + toolbar={{ + actions: [], + }} + rowKey="id" + search={false} + bordered + /> + + + title="用户消费记录" + open={edidInfoModal.open} + form={form} + initialValues={{ + status: 1 + }} + onOpenChange={(visible) => { + if (!visible) { + form.resetFields(); + } + setEdidInfoModal((info) => { + return { + ...info, + open: visible + } + }) + }} + onFinish={async (values) => { + const res = await putAdminTurnover({ + ...edidInfoModal.info, + ...values, + }); + if (res.code) { + message.error('编辑失败') + return false; + } + tableActionRef.current?.reload?.(); + return true; + }} + size="large" + modalProps={{ + cancelText: '取消', + okText: '提交' + }} + > + + + + + + + +
+ ) +} + +export default TurnoverPage; \ No newline at end of file diff --git a/src/pages/admin/user/index.tsx b/src/pages/admin/user/index.tsx new file mode 100644 index 0000000..6d0c6f5 --- /dev/null +++ b/src/pages/admin/user/index.tsx @@ -0,0 +1,261 @@ +import UserHead from '@/components/UserHead'; +import { delAdminUsers, getAdminUsers, putAdminUsers } from '@/request/adminApi'; +import { UserInfo } from '@/types/admin'; +import { ActionType, ModalForm, ProColumns, ProFormDatePicker, ProFormDigit, ProFormGroup, ProFormRadio, ProFormText } from '@ant-design/pro-components'; +import { ProTable } from '@ant-design/pro-components'; +import { Tag, Button, Space, message, Form } from 'antd'; +import { useRef, useState } from 'react'; + +function UserPage() { + + const tableActionRef = useRef(); + const [form] = Form.useForm(); + const [edidInfoModal, setEdidInfoModal] = useState<{ + open: boolean, + info: UserInfo | undefined + }>({ + open: false, + info: undefined + }); + const columns: ProColumns[] = [ + { + title: 'ID', + dataIndex: 'id', + width: 180, + }, + { + title: '账号', + width: 200, + dataIndex: 'account', + }, + { + title: '积分', + width: 100, + dataIndex: 'integral', + render: (_, data) => {data.integral}分 + }, + { + title: '订阅', + dataIndex: 'subscribe', + render: (_, data) => { + const today = new Date() + today.setHours(0, 0, 0, 0) + const todayTime = today.getTime() + const userSubscribeTime = new Date(data.subscribe).getTime() + return ( + + {data.subscribe} + {userSubscribeTime < todayTime && 已过期} + + ) + } + }, + { + title: '用户信息', + dataIndex: 'user_id', + width: 160, + render: (_, data) => { + return ( + + ) + } + }, + { + title: 'ip', + dataIndex: 'ip', + }, + { + title: '状态', + dataIndex: 'status', + width: 100, + render: (_, data) => { + return {data.status === 1 ? '正常' : '异常'} + } + }, + { + title: '创建时间', + dataIndex: 'create_time', + }, + { + title: '更新时间', + dataIndex: 'update_time', + }, + { + title: '操作', + width: 150, + valueType: 'option', + fixed: 'right', + render: (_, data) => [ + , + + ], + }, + ]; + + return ( +
+ { + // 表单搜索项会从 params 传入,传递给后端接口。 + const res = await getAdminUsers({ + page: params.current || 1, + page_size: params.pageSize || 10, + }); + return Promise.resolve({ + data: res.data.rows, + total: res.data.count, + success: true, + }); + }} + toolbar={{ + actions: [], + }} + rowKey="id" + search={false} + bordered + /> + + title="用户信息" + open={edidInfoModal.open} + form={form} + initialValues={{ + status: 1 + }} + onOpenChange={(visible) => { + if (!visible) { + form.resetFields(); + } + setEdidInfoModal((info) => { + return { + ...info, + open: visible + } + }) + }} + onFinish={async (values) => { + if(!edidInfoModal.info?.id) return false + const res = await putAdminUsers({ + ...values, + id: edidInfoModal.info?.id, + }); + if (res.code) { + message.error('编辑失败') + return false; + } + tableActionRef.current?.reload?.(); + return true; + }} + size="large" + modalProps={{ + cancelText: '取消', + okText: '提交' + }} + > + + + + + + + + + + + + + + + +
+ ) +} + +export default UserPage; \ No newline at end of file diff --git a/src/pages/chat/components/AllInput/index.tsx b/src/pages/chat/components/AllInput/index.tsx index 6398769..f23a391 100644 --- a/src/pages/chat/components/AllInput/index.tsx +++ b/src/pages/chat/components/AllInput/index.tsx @@ -2,7 +2,7 @@ import { AutoComplete, Button, Input, Modal, message } from 'antd' import styles from './index.module.less' import { ClearOutlined, CloudDownloadOutlined, SyncOutlined } from '@ant-design/icons' import { useMemo, useState } from 'react' -import useStore from '@/store' +import { promptStore } from '@/store' import html2canvas from 'html2canvas' import useDocumentResize from '@/hooks/useDocumentResize' @@ -15,7 +15,7 @@ type Props = { function AllInput(props: Props) { const [prompt, setPrompt] = useState('') - const { localPrompt } = useStore() + const { localPrompt } = promptStore() const bodyResize = useDocumentResize() diff --git a/src/pages/chat/components/ChatMessage/index.tsx b/src/pages/chat/components/ChatMessage/index.tsx index 06ce79e..ec26e9d 100644 --- a/src/pages/chat/components/ChatMessage/index.tsx +++ b/src/pages/chat/components/ChatMessage/index.tsx @@ -66,7 +66,7 @@ function ChatMessage({ } function highlightBlock(str: string, lang: string, code: string) { - return `
${lang}复制代码
${str}
` + return `
${lang}复制代码
${str}
` } const mdi = new MarkdownIt({ @@ -131,7 +131,7 @@ function ChatMessage({ {position === 'left' && chatAvatar({ style: { marginRight: 8 }, - icon: 'https://cdn.jsdelivr.net/gh/duogongneng/testuitc/svg-1681898659579.svg' + icon: 'https://u1.dl0.cn/icon/openailogo.svg' })}
) diff --git a/src/pages/chat/components/RoleLocal/index.tsx b/src/pages/chat/components/RoleLocal/index.tsx index be0964f..0714758 100644 --- a/src/pages/chat/components/RoleLocal/index.tsx +++ b/src/pages/chat/components/RoleLocal/index.tsx @@ -1,5 +1,5 @@ import { useMemo, useState } from 'react' -import useStore from '@/store' +import { promptStore } from '@/store' import { paginate } from '@/utils' import { Button, Empty, Form, Input, Pagination, Space, message } from 'antd' import styles from './index.module.less' @@ -8,7 +8,7 @@ import { DeleteOutlined, FormOutlined } from '@ant-design/icons' import { ModalForm, ProFormText, ProFormTextArea } from '@ant-design/pro-components' function RoleLocal() { - const { localPrompt, clearPrompts, addPrompts, delPrompt, editPrompt } = useStore() + const { localPrompt, clearPrompts, addPrompts, delPrompt, editPrompt } = promptStore() const [page, setPage] = useState(1) const [pageSize, setPageSize] = useState(9) const [keyword, setKeyword] = useState('') diff --git a/src/pages/chat/components/RoleNetwork/index.tsx b/src/pages/chat/components/RoleNetwork/index.tsx index c958438..80a79fd 100644 --- a/src/pages/chat/components/RoleNetwork/index.tsx +++ b/src/pages/chat/components/RoleNetwork/index.tsx @@ -4,10 +4,10 @@ import { is } from '@/utils' import { FolderAddOutlined, LinkOutlined } from '@ant-design/icons' import { Input, message } from 'antd' import { useState } from 'react' -import useStore from '@/store' +import { promptStore } from '@/store' function RoleNetwork() { - const { addPrompts } = useStore() + const { addPrompts } = promptStore() // const emoji = () => { // return Object.keys(emojis)[Math.floor(Math.random() * Object.keys(emojis).length)]; diff --git a/src/pages/chat/index.module.less b/src/pages/chat/index.module.less index 47a7383..22349a7 100644 --- a/src/pages/chat/index.module.less +++ b/src/pages/chat/index.module.less @@ -70,14 +70,15 @@ [class*='ant-layout-sider-children']{ padding-inline: 12px !important; } + [class*='ant-pro-sider-extra']{ - margin-inline: 0px; + margin-inline: 0px !important; } [class*='ant-menu-item']{ width: 100%; - margin-top: 8px; - margin-left: 0; - margin-right: 0; + margin-top: 8px !important; + margin-left: 0 !important; + margin-right: 0 !important; padding: 0 !important; background-color: transparent !important; &:hover{ @@ -94,8 +95,11 @@ background-color: #fff; } [class*='ant-pro-layout-content']{ - padding-block: 0; + padding-block: 0 !important; } + [class*='ant-pro-sider-hide-menu-collapsed']{ + inset-inline-start: -52px !important; + } @media (max-width: 600px) { [class*='ant-pro-layout-content']{ padding-inline: 4px; diff --git a/src/pages/chat/index.tsx b/src/pages/chat/index.tsx index 4b1b786..9f200bf 100644 --- a/src/pages/chat/index.tsx +++ b/src/pages/chat/index.tsx @@ -3,22 +3,15 @@ import { Button, Modal, Popconfirm, Space, Tabs, Select, message } from 'antd' import { useLayoutEffect, useMemo, useRef, useState } from 'react' import styles from './index.module.less' -import useStore from '@/store' +import { chatStore, configStore, userStore } from '@/store' import RoleNetwork from './components/RoleNetwork' import RoleLocal from './components/RoleLocal' import AllInput from './components/AllInput' import ChatMessage from './components/ChatMessage' import { RequestChatOptions } from '@/types' -import { postChatCompletions, postCompletions } from '@/request/api' +import { postChatCompletions } from '@/request/api' import Reminder from '@/components/Reminder' -import { - filterObjectNull, - formatTime, - generateUUID, - getAiKey, - handleChatData, - handleOpenChatData -} from '@/utils' +import { filterObjectNull, formatTime, generateUUID, handleChatData } from '@/utils' import { useScroll } from '@/hooks/useScroll' import useDocumentResize from '@/hooks/useDocumentResize' import Layout from '@/components/Layout' @@ -26,13 +19,9 @@ import Layout from '@/components/Layout' function ChatPage() { const scrollRef = useRef(null) const { scrollToBottomIfAtBottom, scrollToBottom } = useScroll(scrollRef.current) - + const { token, setLoginModal } = userStore() + const { config, models, changeConfig, setConfigModal } = configStore() const { - models, - token, - config, - changeConfig, - setConfigModal, chats, addChat, delChat, @@ -42,11 +31,8 @@ function ChatPage() { setChatInfo, setChatDataInfo, clearChatMessage, - delChatMessage, - setLoginModal - } = useStore() - - const isProxy = import.meta.env.VITE_APP_MODE !== 'business' + delChatMessage + } = chatStore() const bodyResize = useDocumentResize() @@ -100,13 +86,15 @@ function ChatPage() { async function serverChatCompletions({ requestOptions, signal, - userMessageId + userMessageId, + assistantMessageId }: { userMessageId: string signal: AbortSignal requestOptions: RequestChatOptions + assistantMessageId: string }) { - const response = await postCompletions(requestOptions, { + const response = await postChatCompletions(requestOptions, { options: { signal } @@ -118,20 +106,26 @@ function ChatPage() { // 终止: AbortError console.log(error.name) }) + if (!(response instanceof Response)) { // 这里返回是错误 ... setChatDataInfo(selectChatId, userMessageId, { status: 'error' }) + setChatDataInfo(selectChatId, assistantMessageId, { + status: 'error' + }) + fetchController?.abort() setFetchController(null) message.error('请求失败') return } const reader = response.body?.getReader?.() - let alltext = '' + let allContent = '' while (true) { const { done, value } = (await reader?.read()) || {} if (done) { + fetchController?.abort() setFetchController(null) break } @@ -139,15 +133,15 @@ function ChatPage() { const text = new TextDecoder('utf-8').decode(value) const texts = handleChatData(text) for (let i = 0; i < texts.length; i++) { - const { id, dateTime, parentMessageId, role, text, segment } = texts[i] - alltext += text ? text : '' + const { dateTime, role, content, segment } = texts[i] + allContent += content ? content : '' if (segment === 'stop') { setFetchController(null) setChatDataInfo(selectChatId, userMessageId, { status: 'pass' }) - setChatDataInfo(selectChatId, id, { - text: alltext, + setChatDataInfo(selectChatId, assistantMessageId, { + text: allContent, dateTime, status: 'pass' }) @@ -158,130 +152,19 @@ function ChatPage() { setChatDataInfo(selectChatId, userMessageId, { status: 'pass' }) - setChatInfo( - selectChatId, - { - parentMessageId - }, - { - id, - text: alltext, - dateTime, - status: 'loading', - role, - requestOptions - } - ) - } - if (segment === 'text') { - setChatDataInfo(selectChatId, id, { - text: alltext, - dateTime, - status: 'pass' - }) - } - } - scrollToBottomIfAtBottom() - } - } - - // 三方代码请求处理 - async function openChatCompletions({ - requestOptions, - signal, - userMessageId - }: { - userMessageId: string - signal: AbortSignal - requestOptions: RequestChatOptions - }) { - const sendMessages: Array<{ role: string; content: string }> = [ - { role: 'user', content: requestOptions.prompt } - ] - if (config.limit_message > 0) { - const limitMessage = chatMessages.slice(-config.limit_message) - if (limitMessage.length) { - const list = limitMessage.map((item: any) => ({ role: item.role, content: item.text })) - sendMessages.unshift(...list) - } - } - const systemConfig = getAiKey(config) - const response = await postChatCompletions( - systemConfig.api, - { - model: config.model, - messages: sendMessages, - ...requestOptions.options - }, - { - headers: { - Authorization: `Bearer ${systemConfig.api_key}` - }, - options: { - signal - } - } - ) - if (!(response instanceof Response)) { - setChatDataInfo(selectChatId, userMessageId, { - status: 'error' - }) - setFetchController(null) - message.error('请求失败') - return - } - const reader = response.body?.getReader?.() - let alltext = '' - while (true) { - const { done, value } = (await reader?.read()) || {} - if (done) { - setFetchController(null) - break - } - const text = new TextDecoder('utf-8').decode(value) - const texts = handleOpenChatData(text) - for (let i = 0; i < texts.length; i++) { - const { id, dateTime, parentMessageId, role, text, segment } = texts[i] - alltext += text ? text : '' - if (segment === 'start') { - setChatDataInfo(selectChatId, userMessageId, { - status: 'pass' - }) - setChatInfo( - selectChatId, - { - parentMessageId - }, - { - id, - text: alltext, - dateTime, - status: 'loading', - role, - requestOptions - } - ) - } - if (segment === 'text') { - setChatDataInfo(selectChatId, id, { - text: alltext, + setChatDataInfo(selectChatId, assistantMessageId, { + text: allContent, dateTime, - status: 'pass', + status: 'loading', role, requestOptions }) } - if (segment === 'stop') { - setFetchController(null) - setChatDataInfo(selectChatId, userMessageId, { - status: 'pass' - }) - setChatDataInfo(selectChatId, id, { - text: alltext, + if (segment === 'text') { + setChatDataInfo(selectChatId, assistantMessageId, { + text: allContent, dateTime, - status: 'pass', - role, - requestOptions + status: 'pass' }) } } @@ -293,11 +176,11 @@ function ChatPage() { // 对话 async function sendChatCompletions(vaule: string) { - if (!token && !isProxy) { + if (!token) { setLoginModal(true) return } - const parentMessageId = chats.filter((c) => c.id === selectChatId)[0].parentMessageId + const parentMessageId = chats.filter((c) => c.id === selectChatId)[0].id const userMessageId = generateUUID() const requestOptions = { prompt: vaule, @@ -309,40 +192,32 @@ function ChatPage() { limit_message: null }) } - setChatInfo( - selectChatId, - {}, - { - id: userMessageId, - text: vaule, - dateTime: formatTime(), - status: 'pass', - role: 'user', - requestOptions - } - ) + setChatInfo(selectChatId, { + id: userMessageId, + text: vaule, + dateTime: formatTime(), + status: 'pass', + role: 'user', + requestOptions + }) + const assistantMessageId = generateUUID() + setChatInfo(selectChatId, { + id: assistantMessageId, + text: '', + dateTime: formatTime(), + status: 'loading', + role: 'assistant', + requestOptions + }) const controller = new AbortController() const signal = controller.signal setFetchController(controller) - const systemConfig = getAiKey(config) - console.log('systemConfig', systemConfig) - if (token) { - serverChatCompletions({ - requestOptions, - signal, - userMessageId - }) - } else if (isProxy && systemConfig.api && systemConfig.api_key) { - // 这里是 openai 公共 - openChatCompletions({ - requestOptions, - signal, - userMessageId - }) - } else { - message.error('配置数据错误') - controller.abort() - } + serverChatCompletions({ + requestOptions, + signal, + userMessageId, + assistantMessageId + }) } return ( diff --git a/src/pages/draw/index.tsx b/src/pages/draw/index.tsx index 089d1dd..8d8bf94 100644 --- a/src/pages/draw/index.tsx +++ b/src/pages/draw/index.tsx @@ -12,27 +12,17 @@ import { message } from 'antd' import { useState } from 'react' -import useStore from '@/store' +import { drawStore, userStore } from '@/store' import OpenAiLogo from '@/components/OpenAiLogo' -import { postApiImagesGenerations, postImagesGenerations } from '@/request/api' +import { postImagesGenerations } from '@/request/api' import { ClearOutlined } from '@ant-design/icons' -import { formatTime, generateUUID, getAiKey } from '@/utils' +import { formatTime, generateUUID } from '@/utils' import { ResponseData } from '@/request' import Layout from '@/components/Layout' function DrawPage() { - const { - token, - config, - setConfigModal, - setLoginModal, - historyDrawImages, - clearhistoryDrawImages, - addDrawImage - } = useStore() - - const isProxy = import.meta.env.VITE_APP_MODE === 'proxy' - const isBusiness = import.meta.env.VITE_APP_MODE === 'business' + const { token, setLoginModal } = userStore() + const { historyDrawImages, clearhistoryDrawImages, addDrawImage } = drawStore() const [drawConfig, setDrawConfig] = useState({ prompt: '', @@ -69,48 +59,20 @@ function DrawPage() { } const onStartDraw = async () => { + if (!token) { + setLoginModal(true) + return + } setDrawResultData({ loading: true, list: [] }) - const systemConfig = getAiKey(config) - if (token) { - await postImagesGenerations(drawConfig, {}, { timeout: 0 }) - .then(handleDraw) - .finally(() => { - setDrawResultData((dr) => ({ ...dr, loading: false })) - }) - } else if (systemConfig.api && systemConfig.api_key) { - await postApiImagesGenerations( - systemConfig.api, - { - ...drawConfig - }, - { - Authorization: `Bearer ${systemConfig.api_key}` - }, - { - timeout: 0 - } - ) - .then(handleDraw) - .finally(() => { - setDrawResultData((dr) => ({ ...dr, loading: false })) - }) - } else { - if (isProxy) { - setConfigModal(true) - } - if (isBusiness) { - setLoginModal(true) - } - setDrawResultData((dr) => ({ ...dr, loading: false })) - notification.warning({ - message: '数据错误', - description: '请配置正确的API KEY或登录后方可使用!' + await postImagesGenerations(drawConfig, {}, { timeout: 0 }) + .then(handleDraw) + .finally(() => { + setDrawResultData((dr) => ({ ...dr, loading: false })) }) - } } return ( diff --git a/src/pages/login/index.module.less b/src/pages/login/index.module.less new file mode 100644 index 0000000..2eeac85 --- /dev/null +++ b/src/pages/login/index.module.less @@ -0,0 +1,11 @@ +.login{ + height: 100vh; + background-image: url('https://p0.meituan.net/travelcube/6ebe78cb765815fdc628c5620ed0c6ac174555.png'); + background-repeat: no-repeat; + background-size: 100%; + box-sizing: border-box; + [class*='ant-pro-form-login-container']{ + padding-top: 150px; + box-sizing: border-box; + } +} diff --git a/src/pages/login/index.tsx b/src/pages/login/index.tsx new file mode 100644 index 0000000..2840153 --- /dev/null +++ b/src/pages/login/index.tsx @@ -0,0 +1,23 @@ +import { Form } from 'antd' +import styles from './index.module.less' +import { useNavigate } from 'react-router-dom' +import { LoginCard } from '@/components/LoginModal' + +function LoginPage() { + const [loginForm] = Form.useForm() + const navigate = useNavigate() + + return ( +
+ { + loginForm.resetFields() + navigate('/') + }} + /> +
+ ) +} + +export default LoginPage diff --git a/src/pages/shop/index.module.less b/src/pages/shop/index.module.less index bf1fe38..5e3a949 100644 --- a/src/pages/shop/index.module.less +++ b/src/pages/shop/index.module.less @@ -23,6 +23,13 @@ [class*='ant-pro-layout-content']{ padding-block: 0; } + &_pagination{ + display: flex; + align-items: center; + justify-content: flex-end; + width: 100%; + margin-top: 12px; + } } .payModal{ diff --git a/src/pages/shop/index.tsx b/src/pages/shop/index.tsx index a1ef70b..831b9bc 100644 --- a/src/pages/shop/index.tsx +++ b/src/pages/shop/index.tsx @@ -1,26 +1,33 @@ -import { useEffect, useState } from 'react' +import { useEffect, useMemo, useState } from 'react' import UserInfoCard from '@/components/UserInfoCard' import styles from './index.module.less' import Layout from '@/components/Layout' -import useStore from '@/store' -import { Button, Input, Modal, QRCode, Space, Table, message } from 'antd' +import { shopStore, userStore } from '@/store' +import { Button, Input, Modal, Pagination, QRCode, Space, Table, message } from 'antd' import GoodsList from '@/components/GoodsList' import { CloseCircleFilled, SyncOutlined } from '@ant-design/icons' -import { fetchProduct, fetchUserInfo } from '@/store/async' -import { getIntegralLogs, postPrepay } from '@/request/api' -import { ProductInfo } from '@/types' +import { shopAsync, userAsync } from '@/store/async' +import { getUserTurnover, postPrepay, postUseCarmi, postSignin } from '@/request/api' +import { ProductInfo, TurnoverInfo } from '@/types' import OpenAiLogo from '@/components/OpenAiLogo' -import { generateUUID } from '@/utils' import { Link } from 'react-router-dom' function GoodsPay() { - const { goodsList, user_detail } = useStore() + const { goodsList } = shopStore() + const { token, user_info } = userStore() - const [log, setLog] = useState({ + const [turnover, setTurnover] = useState<{ + page: number + pageSize: number + loading: boolean + rows: Array + count: number + }>({ page: 1, pageSize: 10, loading: false, - data: [] + rows: [], + count: 1 }) const [payModal, setPayModal] = useState<{ @@ -38,22 +45,22 @@ function GoodsPay() { }) useEffect(() => { - fetchProduct() - onGetLog(1) + shopAsync.fetchProduct() + onTurnoverLog(1) }, []) - function onGetLog(page: number) { - setLog((l) => ({ ...l, page, loading: true })) - getIntegralLogs({ + function onTurnoverLog(page: number) { + setTurnover((l) => ({ ...l, page, loading: true })) + getUserTurnover({ page: page, - pageSize: log.pageSize + pageSize: turnover.pageSize }) - .then((res: any) => { - const data = res.data.map((item: any, index: number) => ({ ...item, id: index + 1 })) - setLog((l) => ({ ...l, page, data, loading: false })) + .then((res) => { + if (res.code) return + setTurnover((l) => ({ ...l, page, ...res.data, loading: false })) }) .finally(() => { - setLog((l) => ({ ...l, page, loading: false })) + setTurnover((l) => ({ ...l, page, loading: false })) }) } @@ -72,96 +79,163 @@ function GoodsPay() { const { order_sn, payurl, qrcode } = payres.data setPayModal((p) => ({ ...p, status: 'pay', ...payres.data })) if (order_sn && payurl && !qrcode) { - const link = document.createElement('a'); - link.target = '_blank'; - link.href = payurl; - link.click(); - link.remove(); + const link = document.createElement('a') + link.target = '_blank' + link.href = payurl + link.click() + link.remove() } } function onPayResult() { // 刷新记录 - onGetLog(1) + onTurnoverLog(1) // 刷新用户信息 - fetchUserInfo() + // fetchUserInfo() setPayModal((p) => ({ ...p, status: 'loading', open: false })) } + const [carmiLoading, setCarmiLoading] = useState(false) + + function useCarmi(carmi: string) { + if (!carmi) { + message.warning('请输入卡密') + return + } + setCarmiLoading(true) + postUseCarmi({ carmi }) + .then((res) => { + if (res.code) return + userAsync.fetchUserInfo() + message.success(res.message) + onTurnoverLog(1) + }) + .finally(() => { + setCarmiLoading(false) + }) + } + + const [signinLoading, setSigninLoading] = useState(false) + + if (!token) { + return ( +
+ +
+ ) + } + return (
{/* 用户信息 */} - + + {/* 签到区域 */} +
+

签到日历

+ +
{/* 卡密充值区 */}

卡密充值

{ - console.log('充值') - }} + onSearch={useCarmi} />
-
-

在线充值

- -
+ {goodsList.length > 0 && ( +
+

在线充值

+ +
+ )}

{ - onGetLog(1) + onTurnoverLog(1) }} > - 订单记录 + 订单记录

- scroll={{ x: 800 }} bordered - loading={log.loading} - dataSource={log.data} + loading={turnover.loading} + dataSource={turnover.rows} pagination={{ hideOnSinglePage: true, - defaultPageSize: 1000 + defaultPageSize: turnover.pageSize }} rowKey="id" columns={[ - { - title: '序号', - dataIndex: 'id', - key: 'id' - }, { title: '描述', - dataIndex: 'title', - key: 'title' + dataIndex: 'describe', + key: 'describe' }, { title: '额度', - key: 'integral', + key: 'value', render: (data) => { - return ( - - {data.integral}分 - - ) + return {data.value} } }, { title: '日期', - dataIndex: 'created_at', - key: 'created_at' + dataIndex: 'create_time', + key: 'create_time' } ]} /> +
+ { + onTurnoverLog(e) + }} + /> +
@@ -178,35 +252,39 @@ function GoodsPay() { width={320} >
- {payModal.status === 'fail' && ( - + {payModal.status === 'fail' && } + {payModal.status === 'loading' && } + + {payModal.status === 'pay' && ( + )} - { - payModal.status === 'loading' && ( - - ) - } - -{ - payModal.status === 'pay' && ( - - ) - } - - {(payModal.payurl && payModal.status === 'pay') && ( + + {payModal.payurl && payModal.status === 'pay' && ( )} - {payModal.status === 'fail' ?

支付失败,请重新尝试

: - payModal.status === 'loading' ?

正在创建订单中...

: -

如未跳转可截图支付宝扫码支付
或点击二维码再次跳转

} + {payModal.status === 'fail' ? ( +

支付失败,请重新尝试

+ ) : payModal.status === 'loading' ? ( +

正在创建订单中...

+ ) : ( +

+ 如未跳转可截图支付宝扫码支付 +
或点击二维码再次跳转 +

+ )}