├── src ├── composables │ ├── index.ts │ ├── useShare.ts │ └── useChatgpt.ts ├── static │ └── images │ │ ├── me.png │ │ ├── med.png │ │ ├── home.png │ │ └── homed.png ├── shims.d.ts ├── apis │ ├── login.ts │ ├── auth.ts │ ├── types.ts │ ├── user.ts │ └── request.ts ├── layouts │ └── default.vue ├── App.vue ├── main.ts ├── pages │ ├── generic │ │ ├── template.ts │ │ └── index.vue │ ├── index.vue │ ├── login.vue │ ├── weekly │ │ └── index.vue │ ├── me.vue │ ├── config.ts │ └── chat │ │ └── index.vue ├── theme.json ├── components.d.ts ├── stores │ ├── index.ts │ └── user.ts ├── manifest.json ├── pages.json ├── components │ ├── Card.vue │ ├── MessageItem.vue │ └── mp-html │ │ ├── mp-html.vue │ │ ├── node │ │ └── node.vue │ │ └── parser.js ├── uni.scss └── auto-imports.d.ts ├── server ├── .DS_Store ├── README.md ├── app │ ├── page.tsx │ ├── layout.tsx │ └── api │ │ ├── user │ │ └── route.ts │ │ ├── auth │ │ └── route.ts │ │ └── chat-stream │ │ └── route.ts ├── next.config.js ├── postcss.config.js ├── .env.example ├── next-env.d.ts ├── lib │ ├── db.ts │ ├── utils.ts │ └── openAIStream.ts ├── .gitignore ├── tailwind.config.js ├── prisma │ └── schema.prisma ├── tsconfig.json ├── package.json └── middleware.ts ├── project.private.config.json ├── tsconfig.json ├── project.config.json ├── index.html ├── README.md ├── LICENSE ├── .gitignore ├── vite.config.ts ├── unocss.config.ts └── package.json /src/composables/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useChatgpt' 2 | export * from './useShare' 3 | -------------------------------------------------------------------------------- /server/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangfengyuan/chatgpt-miniprogram/HEAD/server/.DS_Store -------------------------------------------------------------------------------- /server/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangfengyuan/chatgpt-miniprogram/HEAD/server/README.md -------------------------------------------------------------------------------- /src/static/images/me.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangfengyuan/chatgpt-miniprogram/HEAD/src/static/images/me.png -------------------------------------------------------------------------------- /src/static/images/med.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangfengyuan/chatgpt-miniprogram/HEAD/src/static/images/med.png -------------------------------------------------------------------------------- /server/app/page.tsx: -------------------------------------------------------------------------------- 1 | const Home = () => { 2 | 3 | return
aichat homepage
; 4 | }; 5 | 6 | export default Home; -------------------------------------------------------------------------------- /src/static/images/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangfengyuan/chatgpt-miniprogram/HEAD/src/static/images/home.png -------------------------------------------------------------------------------- /src/static/images/homed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangfengyuan/chatgpt-miniprogram/HEAD/src/static/images/homed.png -------------------------------------------------------------------------------- /server/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {} 3 | 4 | module.exports = nextConfig 5 | -------------------------------------------------------------------------------- /server/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /server/.env.example: -------------------------------------------------------------------------------- 1 | OPENAI_API_KEY="" 2 | DATABASE_URL="" 3 | WX_APPID="" 4 | WX_SECRET="" 5 | JWT_SECRET="123456" 6 | DEFAULT_CREDIT=15 7 | GLM_API_KEY="" -------------------------------------------------------------------------------- /src/shims.d.ts: -------------------------------------------------------------------------------- 1 | 2 | declare module '*.vue' { 3 | import type { DefineComponent } from 'vue' 4 | const component: DefineComponent<{}, {}, any> 5 | export default component 6 | } 7 | -------------------------------------------------------------------------------- /server/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /project.private.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "项目私有配置文件。此文件中的内容将覆盖 project.config.json 中的相同字段。项目的改动优先同步到此文件中。详见文档:https://developers.weixin.qq.com/miniprogram/dev/devtools/projectconfig.html", 3 | "projectname": "ai-tools", 4 | "setting": { 5 | "compileHotReLoad": true 6 | } 7 | } -------------------------------------------------------------------------------- /src/apis/login.ts: -------------------------------------------------------------------------------- 1 | import useStore from '../stores' 2 | const { user } = useStore() 3 | 4 | /** 5 | * 登录拦截 6 | */ 7 | export function loginIntercept(callBack: Function) { 8 | if (user.logged) { 9 | callBack() 10 | return 11 | } 12 | uni.navigateTo({ 13 | url: '/pages/login', 14 | }) 15 | } 16 | -------------------------------------------------------------------------------- /src/apis/auth.ts: -------------------------------------------------------------------------------- 1 | const TOKEN_KEY = 'token' 2 | 3 | export function getToken(): string { 4 | return uni.getStorageSync(TOKEN_KEY) 5 | } 6 | 7 | export function setToken(token: string) { 8 | return uni.setStorageSync(TOKEN_KEY, token) 9 | } 10 | 11 | export function removeToken() { 12 | return uni.removeStorageSync(TOKEN_KEY) 13 | } 14 | -------------------------------------------------------------------------------- /src/apis/types.ts: -------------------------------------------------------------------------------- 1 | export interface LoginParams { 2 | phone?: string 3 | code: string 4 | } 5 | 6 | export interface LoginResult { 7 | userId: string 8 | token: string 9 | } 10 | 11 | export interface UserInfoResult { 12 | phone: string 13 | realName: string 14 | avatarUrl: string 15 | nickName: string 16 | vb: number 17 | } 18 | -------------------------------------------------------------------------------- /src/layouts/default.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 15 | -------------------------------------------------------------------------------- /server/lib/db.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client' 2 | 3 | declare global { 4 | var prisma: PrismaClient 5 | } 6 | 7 | let prisma: PrismaClient; 8 | 9 | if (process.env.NODE_ENV === "production") { 10 | prisma = new PrismaClient(); 11 | } else { 12 | if (!global.prisma) { 13 | global.prisma = new PrismaClient(); 14 | } 15 | prisma = global.prisma; 16 | } 17 | 18 | export default prisma -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.json", 3 | "compilerOptions": { 4 | "sourceMap": true, 5 | "baseUrl": ".", 6 | "paths": { 7 | "~/*": [ 8 | "src/*" 9 | ] 10 | }, 11 | "lib": ["esnext", "dom"], 12 | "types": ["@dcloudio/types", "vite/client"] 13 | }, 14 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"] 15 | } 16 | -------------------------------------------------------------------------------- /src/composables/useShare.ts: -------------------------------------------------------------------------------- 1 | import { onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app' 2 | 3 | export function useShare() { 4 | const options = { 5 | title: '这个回答太有趣了,你也来试试', 6 | imageUrl: 'https://cos.codefe.top/images/ai-home-screenshot.png', 7 | path: '/pages/index', 8 | } 9 | onShareAppMessage(() => (options)) 10 | 11 | onShareTimeline(() => (options)); 12 | return { 13 | onShareAppMessage, 14 | onShareTimeline, 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 20 | -------------------------------------------------------------------------------- /server/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Inter } from 'next/font/google' 2 | 3 | const inter = Inter({ subsets: ['latin'] }) 4 | 5 | export const metadata = { 6 | title: 'Create Next App', 7 | description: 'Generated by create next app', 8 | } 9 | 10 | export default function RootLayout({ 11 | children, 12 | }: { 13 | children: React.ReactNode 14 | }) { 15 | return ( 16 | 17 | {children} 18 | 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createSSRApp } from 'vue' 2 | import UniLayouts from 'virtual:uni-layouts' 3 | import uviewPlus from 'uview-plus' 4 | import App from './App.vue' 5 | import { setupPinia } from './stores' 6 | import 'uno.css' 7 | 8 | export function createApp() { 9 | const app = createSSRApp(App) 10 | // Configure store 11 | // https://pinia.vuejs.org/ 12 | setupPinia(app) 13 | // 使用 uView UI 14 | app.use(uviewPlus) 15 | app.use(UniLayouts) 16 | 17 | return { app } 18 | } 19 | -------------------------------------------------------------------------------- /src/pages/generic/template.ts: -------------------------------------------------------------------------------- 1 | const template = (data: any) => { 2 | let { prompt } = data 3 | const doubleBraceRegex = /{{(\d+|[a-z$_][a-z\d$_]*?(?:\.[a-z\d$_]*?)*?)}}/gi 4 | 5 | if (doubleBraceRegex.test(prompt)) { 6 | prompt = prompt.replace(doubleBraceRegex, (_, key) => { 7 | let result = data 8 | for (const property of key.split('.')) 9 | result = result ? result[property].val : '' 10 | return result 11 | }) 12 | } 13 | 14 | return prompt 15 | } 16 | 17 | export default template 18 | -------------------------------------------------------------------------------- /server/app/api/user/route.ts: -------------------------------------------------------------------------------- 1 | import prisma from '@/lib/db' 2 | import type { User } from "@prisma/client"; 3 | import { AuthenticatedRequest } from "@/middleware"; 4 | 5 | export async function GET(req: AuthenticatedRequest) { 6 | const userId = req.headers.get("X-USER-ID"); 7 | const user: Partial = await prisma.user.findUnique({ 8 | where: { 9 | id: userId!, 10 | }, 11 | }) 12 | delete user!.wx_openid; 13 | return new Response(JSON.stringify({ 14 | status: "success", 15 | data: user, 16 | })) 17 | } -------------------------------------------------------------------------------- /server/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | 37 | .env 38 | -------------------------------------------------------------------------------- /server/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | './pages/**/*.{js,ts,jsx,tsx,mdx}', 5 | './components/**/*.{js,ts,jsx,tsx,mdx}', 6 | './app/**/*.{js,ts,jsx,tsx,mdx}', 7 | ], 8 | theme: { 9 | extend: { 10 | backgroundImage: { 11 | 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', 12 | 'gradient-conic': 13 | 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))', 14 | }, 15 | }, 16 | }, 17 | plugins: [], 18 | } 19 | -------------------------------------------------------------------------------- /src/theme.json: -------------------------------------------------------------------------------- 1 | { 2 | "dark": { 3 | "bgColor": "#222222", 4 | "bgColorBottom": "#222222", 5 | "bgColorTop": "#222222", 6 | "bgTxtStyle": "light", 7 | "navBgColor": "#222222", 8 | "navTxtStyle": "white", 9 | "tabBgColor": "#222222", 10 | "tabBorderStyle": "white" 11 | }, 12 | "light": { 13 | "bgColor": "#FFFFFF", 14 | "bgColorBottom": "#FFFFFF", 15 | "bgColorTop": "#FFFFFF", 16 | "bgTxtStyle": "dark", 17 | "navBgColor": "#FFFFFF", 18 | "navTxtStyle": "white", 19 | "tabBgColor": "#FFFFFF", 20 | "tabBorderStyle": "black" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /project.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "appid": "wxf5dc1d8d1ecbaaa8", 3 | "compileType": "miniprogram", 4 | "libVersion": "2.30.2", 5 | "packOptions": { 6 | "ignore": [], 7 | "include": [] 8 | }, 9 | "setting": { 10 | "coverView": true, 11 | "es6": true, 12 | "postcss": true, 13 | "minified": true, 14 | "enhance": true, 15 | "showShadowRootInWxmlPanel": true, 16 | "packNpmRelationList": [], 17 | "babelSetting": { 18 | "ignore": [], 19 | "disablePlugins": [], 20 | "outputPath": "" 21 | } 22 | }, 23 | "condition": {}, 24 | "editorSetting": { 25 | "tabSize": 2 26 | } 27 | } -------------------------------------------------------------------------------- /src/components.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* prettier-ignore */ 3 | // @ts-nocheck 4 | // Generated by vite-plugin-uni-components 5 | // Read more: https://github.com/vuejs/core/pull/3399 6 | import '@vue/runtime-core' 7 | 8 | export {} 9 | 10 | declare module '@vue/runtime-core' { 11 | export interface GlobalComponents { 12 | Card: typeof import('./components/Card.vue')['default'] 13 | MessageItem: typeof import('./components/MessageItem.vue')['default'] 14 | MpHtml: typeof import('./components/mp-html/mp-html.vue')['default'] 15 | Node: typeof import('./components/mp-html/node/node.vue')['default'] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/apis/user.ts: -------------------------------------------------------------------------------- 1 | import type { LoginParams, LoginResult, UserInfoResult } from './types' 2 | import request from './request' 3 | 4 | enum Path { 5 | Login = '/api/auth', 6 | User = '/api/user', 7 | Chat = '/api/chat-stream', 8 | } 9 | // 登录 10 | export async function login(form: LoginParams) { 11 | return request.post(Path.Login, form) 12 | } 13 | 14 | // 获取用户信息 15 | export async function getUserInfo() { 16 | return request.get(Path.User) 17 | } 18 | 19 | // openai api 20 | export async function getChatStream(data: any) { 21 | return request.post(Path.Chat, data) 22 | } 23 | 24 | // 退出登录 25 | // export async function logout() { 26 | // return request.get(Path.Logout) 27 | // } 28 | -------------------------------------------------------------------------------- /server/prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | generator client { 5 | provider = "prisma-client-js" 6 | } 7 | 8 | enum Role { 9 | ADMIN 10 | USER 11 | } 12 | 13 | datasource db { 14 | provider = "mongodb" 15 | url = env("DATABASE_URL") 16 | } 17 | 18 | model User { 19 | id String @id @default(auto()) @map("_id") @db.ObjectId 20 | nickname String? 21 | wx_openid String @unique 22 | credit Int? 23 | role Role @default(USER) 24 | avatar String? 25 | createdAt DateTime @default(now()) @map(name: "created_at") 26 | updatedAt DateTime @updatedAt 27 | 28 | @@map(name: "user") 29 | } -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ], 22 | "paths": { 23 | "@/*": ["./*"] 24 | } 25 | }, 26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 27 | "exclude": ["node_modules"] 28 | } 29 | -------------------------------------------------------------------------------- /src/stores/index.ts: -------------------------------------------------------------------------------- 1 | import { createPinia } from 'pinia' 2 | import { createPersistedState } from 'pinia-persistedstate-plugin' 3 | import user from './user' 4 | 5 | export function setupPinia(app: any) { 6 | const pinia = createPinia() 7 | pinia.use(createPersistedState({ 8 | storage: { 9 | getItem(key: string): string | null { 10 | return uni.getStorageSync(key) 11 | }, 12 | setItem(key: string, value: string) { 13 | uni.setStorageSync(key, value) 14 | }, 15 | removeItem(key: string) { 16 | uni.removeStorage({ key }) 17 | }, 18 | }, 19 | })) 20 | app.use(pinia) 21 | return app 22 | } 23 | 24 | // 统一导出 useStore 方法 25 | export default function useStore() { 26 | return { 27 | user: user(), 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chatserver", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@prisma/client": "^4.15.0", 13 | "@types/node": "20.2.5", 14 | "@types/react": "18.2.9", 15 | "@types/react-dom": "18.2.4", 16 | "autoprefixer": "10.4.14", 17 | "eslint": "8.42.0", 18 | "eslint-config-next": "13.4.4", 19 | "eventsource-parser": "^1.0.0", 20 | "jose": "^4.14.4", 21 | "jsonwebtoken": "^9.0.0", 22 | "next": "13.4.4", 23 | "postcss": "8.4.24", 24 | "react": "18.2.0", 25 | "react-dom": "18.2.0", 26 | "tailwindcss": "3.3.2", 27 | "typescript": "5.1.3" 28 | }, 29 | "devDependencies": { 30 | "prisma": "^4.15.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/pages/index.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 32 | 33 | 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | ChatGPT Miniprogram 3 |
4 | 5 |

该小程序仅作为演示,如需部署请按照以下流程操作

6 | 7 | 8 |
9 | 10 | ## 技术栈 11 | - uniapp + Vite + UnoCSS + Pinia + uview-plus 12 | - Nextjs + prisma + mongodb 13 | 14 | ## 功能 15 | - 0成本 16 | - 简洁聊天界面,内容可复制可分享 17 | - 次数限制 18 | 19 | 20 | ## 运行 21 | 22 | 1. 克隆项目 23 | ```bash 24 | git clone https://github.com/wangfengyuan/chatgpt-miniprogram.git 25 | ``` 26 | 27 | 2. 进入项目目录 28 | ```bash 29 | cd chatgpt-miniprogram 30 | ``` 31 | 32 | 3. 安装依赖 33 | ```bash 34 | pnpm install 35 | ``` 36 | 37 | 4. 安装server依赖并在.env文件填入正确的环境变量 38 | ```bash 39 | cd server 40 | cp .env.example .env 41 | pnpm install 42 | pnpm run dev 43 | ``` 44 | 45 | 5. 打包小程序并导入开微信开发者工具 46 | ``` 47 | pnpm run dev:mp-weixin 48 | ``` 49 | 50 | 51 | ## server端部署 52 | 参考文章 [0成本开发ChatGPT微信小程序](https://blog.codefe.top/0%E6%88%90%E6%9C%AC%E5%BC%80%E5%8F%91chatgpt%E5%BE%AE%E4%BF%A1%E5%B0%8F%E7%A8%8B%E5%BA%8F) 53 | 54 | -------------------------------------------------------------------------------- /src/composables/useChatgpt.ts: -------------------------------------------------------------------------------- 1 | import { getChatStream } from '../apis/user' 2 | import useStore from '../stores' 3 | export function useChatgpt() { 4 | const loading = ref(false) 5 | const output = ref('') 6 | const { user } = useStore() 7 | const generate = async (messages: { role: string; content: string }[]) => { 8 | if (loading.value) 9 | return 10 | loading.value = true 11 | output.value = '' 12 | try { 13 | const res: any = await getChatStream({ 14 | messages, 15 | }) 16 | if (typeof res === 'string') { 17 | output.value = res 18 | } else if (typeof res === 'object' && res.choices) { 19 | output.value = res.choices[0].content; 20 | } 21 | user.updateCredit(user.credit - 1) 22 | } 23 | catch (e: any) { 24 | uni.showToast({ 25 | title: e.message || '超时请重试', 26 | icon: 'error', 27 | }) 28 | } 29 | loading.value = false 30 | return output.value 31 | } 32 | return { 33 | loading, 34 | output, 35 | generate, 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/stores/user.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | 3 | export default defineStore('user', { 4 | state: () => { 5 | return { 6 | userInfo: {} as { 7 | id: string 8 | nickname: string 9 | avatar: string 10 | credit: number 11 | }, 12 | } 13 | }, 14 | getters: { 15 | logged: (state) => { 16 | const { id } = state.userInfo 17 | return !!id 18 | }, 19 | id: (state) => { 20 | return state.userInfo.id 21 | }, 22 | nickname: (state) => { 23 | return state.userInfo.nickname 24 | }, 25 | avatar: (state) => { 26 | return state.userInfo.avatar 27 | }, 28 | credit: (state) => { 29 | return state.userInfo.credit 30 | }, 31 | }, 32 | actions: { 33 | setUserInfo(userInfo: any) { 34 | Object.assign(this.userInfo, userInfo) 35 | }, 36 | updateCredit(num: number) { 37 | Object.assign(this.userInfo, { 38 | credit: num, 39 | }) 40 | }, 41 | logOut() { 42 | this.userInfo = {} 43 | }, 44 | }, 45 | }) 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022-PRESENT Neil Lee 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | dist 58 | -------------------------------------------------------------------------------- /server/middleware.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | import { getErrorResponse, verifyJWT } from "./lib/utils"; 3 | 4 | export interface AuthenticatedRequest extends NextRequest { 5 | user: { 6 | id: string; 7 | }; 8 | } 9 | 10 | export async function middleware(req: NextRequest) { 11 | let token: string | undefined; 12 | 13 | if (req.headers.get("Authorization")?.startsWith("Bearer ")) { 14 | token = req.headers.get("Authorization")?.substring(7); 15 | } 16 | 17 | if (!token) { 18 | return getErrorResponse( 19 | 401, 20 | "You are not logged in. Please provide a token to gain access." 21 | ); 22 | } 23 | 24 | const response = NextResponse.next(); 25 | 26 | try { 27 | if (token) { 28 | const { sub } = await verifyJWT<{ sub: string }>(token); 29 | (req as AuthenticatedRequest).user = { id: sub }; 30 | response.headers.set("X-USER-ID", sub); 31 | } 32 | } catch (error) { 33 | return getErrorResponse(401, "Token is invalid or user doesn't exists"); 34 | } 35 | 36 | return response; 37 | } 38 | 39 | export const config = { 40 | matcher: ["/api/user/:path*", "/api/chat-stream"], 41 | }; 42 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "AI问答Bot", 3 | "appid" : "__UNI__D8AA0CD", 4 | "description" : "", 5 | "versionName" : "0.1.0", 6 | "versionCode" : "100", 7 | "transformPx" : false, 8 | "mp-weixin" : { 9 | "appid" : "wxf5dc1d8d1ecbaaa8", 10 | "setting" : { 11 | "ignoreDevUnusedFiles" : false, 12 | "ignoreUploadUnusedFiles" : false, 13 | "urlCheck" : false, 14 | "es6" : true, 15 | "minified" : true, 16 | "postcss" : true 17 | }, 18 | "usingComponents" : true, 19 | "darkmode" : true 20 | }, 21 | "uniStatistics" : { 22 | "enable" : false 23 | }, 24 | "vueVersion" : "3", 25 | "app-plus" : { 26 | "modules" : { 27 | "OAuth" : {} 28 | }, 29 | "distribute" : { 30 | "android" : { 31 | "permissions" : [] 32 | }, 33 | "sdkConfigs" : { 34 | "oauth" : { 35 | "weixin" : { 36 | "appid" : "wxf5dc1d8d1ecbaaa8", 37 | "UniversalLinks" : "" 38 | } 39 | } 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /server/app/api/auth/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest } from "next/server"; 2 | import prisma from '@/lib/db' 3 | import type { User } from "@prisma/client"; 4 | import { signJWT } from "@/lib/utils"; 5 | 6 | const { WX_APPID, WX_SECRET, DEFAULT_CREDIT } = process.env; 7 | 8 | /** 9 | * 注册用户 10 | * @param req 11 | * @returns 12 | */ 13 | export async function POST(req: NextRequest) { 14 | const body = await req.json(); 15 | const { code } = body; 16 | const response = await fetch(`https://api.weixin.qq.com/sns/jscode2session?appid=${WX_APPID}&secret=${WX_SECRET}&js_code=${code}&grant_type=authorization_code`) 17 | const jscode2session = await response.json(); 18 | 19 | let user: Partial = await prisma.user.findUnique({ 20 | where: { 21 | wx_openid: jscode2session.openid, 22 | }, 23 | }) 24 | if (!user) { 25 | user = await prisma.user.create({ 26 | data: { 27 | wx_openid: jscode2session.openid, 28 | credit: Number(DEFAULT_CREDIT), 29 | }, 30 | }) 31 | } 32 | const token = await signJWT({ sub: user.id! }, { exp: '7d' }); 33 | delete user.wx_openid; 34 | return new Response(JSON.stringify({ 35 | status: "success", 36 | data: { 37 | token, 38 | user, 39 | }, 40 | })) 41 | } 42 | -------------------------------------------------------------------------------- /server/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import { SignJWT, jwtVerify } from "jose"; 3 | 4 | export function getErrorResponse( 5 | status: number = 500, 6 | message: string, 7 | errors: any = null 8 | ) { 9 | return new NextResponse( 10 | JSON.stringify({ 11 | status: status < 500 ? "fail" : "error", 12 | message, 13 | errors: errors ? errors.flatten() : null, 14 | }), 15 | { 16 | status, 17 | headers: { "Content-Type": "application/json" }, 18 | } 19 | ); 20 | } 21 | 22 | export const signJWT = async ( 23 | payload: { sub: string }, 24 | options: { exp: string } 25 | ) => { 26 | try { 27 | const secret = new TextEncoder().encode(process.env.JWT_SECRET); 28 | const alg = "HS256"; 29 | return new SignJWT(payload) 30 | .setProtectedHeader({ alg }) 31 | .setExpirationTime(options.exp) 32 | .setIssuedAt() 33 | .setSubject(payload.sub) 34 | .sign(secret); 35 | } catch (error) { 36 | throw error; 37 | } 38 | }; 39 | 40 | export const verifyJWT = async (token: string): Promise => { 41 | try { 42 | return ( 43 | await jwtVerify( 44 | token, 45 | new TextEncoder().encode(process.env.JWT_SECRET) 46 | ) 47 | ).payload as unknown as T; 48 | } catch (error) { 49 | throw new Error("Your token has expired."); 50 | } 51 | }; -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path' 2 | import { defineConfig } from 'vite' 3 | import Uni from '@dcloudio/vite-plugin-uni' 4 | import UnoCSS from 'unocss/vite' 5 | import AutoImport from 'unplugin-auto-import/vite' 6 | import Components from '@uni-helper/vite-plugin-uni-components' 7 | import Inspect from 'vite-plugin-inspect' 8 | import UniLayouts from '@uni-helper/vite-plugin-uni-layouts' 9 | import commonjs from '@rollup/plugin-commonjs' 10 | 11 | // https://vitejs.dev/config/ 12 | export default defineConfig({ 13 | root: process.cwd(), 14 | resolve: { 15 | alias: { 16 | '~/': `${resolve(__dirname, 'src')}/`, 17 | }, 18 | }, 19 | plugins: [ 20 | commonjs(), 21 | UniLayouts(), 22 | // Make sure it before `Uni()` 23 | Components({ 24 | extensions: ['vue', 'md'], 25 | dts: 'src/components.d.ts', 26 | }), 27 | 28 | Uni(), 29 | 30 | // https://github.com/antfu/unplugin-auto-import 31 | AutoImport({ 32 | imports: ['vue', 'pinia', 'uni-app'], 33 | dts: 'src/auto-imports.d.ts', 34 | dirs: ['src/composables', 'src/stores'], 35 | vueTemplate: true, 36 | }), 37 | 38 | // https://github.com/antfu/unocss 39 | // see unocss.config.ts for config 40 | UnoCSS(), 41 | 42 | // https://github.com/antfu/vite-plugin-inspect 43 | // Visit http://localhost:port/__inspect/ to see the inspector 44 | Inspect(), 45 | ], 46 | }) 47 | -------------------------------------------------------------------------------- /src/pages.json: -------------------------------------------------------------------------------- 1 | { 2 | "globalStyle": { 3 | "backgroundColor": "@bgColor", 4 | "backgroundColorBottom": "@bgColorBottom", 5 | "backgroundColorTop": "@bgColorTop", 6 | "backgroundTextStyle": "dark", 7 | "navigationBarBackgroundColor": "#000000", 8 | "navigationBarTextStyle": "black", 9 | "navigationBarTitleText": "AI问答Bot", 10 | "navigationStyle": "custom" 11 | }, 12 | "pages": [ 13 | { "path": "pages/index" }, 14 | { "path": "pages/me" }, 15 | { "path": "pages/login" }, 16 | { "path": "pages/chat/index" }, 17 | { "path": "pages/weekly/index" }, 18 | { "path": "pages/generic/index" } 19 | ], 20 | "tabBar": { 21 | "color": "#444444", 22 | "selectedColor": "#6190E8", 23 | "backgroundColor": "#ffffff", 24 | "list": [ 25 | { 26 | "pagePath": "pages/index", 27 | "text": "首页", 28 | "iconPath": "static/images/home.png", 29 | "selectedIconPath": "static/images/homed.png" 30 | }, 31 | { 32 | "pagePath": "pages/me", 33 | "text": "我的", 34 | "iconPath": "static/images/me.png", 35 | "selectedIconPath": "static/images/med.png" 36 | } 37 | ] 38 | }, 39 | "easycom": { 40 | "autoscan": true, 41 | "custom": { 42 | "^u-(.*)": "uview-plus/components/u-$1/u-$1.vue", 43 | "^uni-(.*)": "@dcloudio/uni-ui/lib/uni-$1/uni-$1.vue" 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/components/Card.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 48 | 49 | 51 | -------------------------------------------------------------------------------- /server/app/api/chat-stream/route.ts: -------------------------------------------------------------------------------- 1 | import type { NextRequest } from "next/server"; 2 | import prisma from '@/lib/db' 3 | import { ChatGPTMessage, OpenAIStream, OpenAIStreamPayload } from "@/lib/openAIStream"; 4 | import { User } from "@prisma/client"; 5 | 6 | const handler = async (req: NextRequest) => { 7 | const body = await req.json(); 8 | const { messages } = body; 9 | const userId = req.headers.get("X-USER-ID"); 10 | const user: User | null = await prisma.user.findUnique({ 11 | where: { 12 | id: userId!, 13 | }, 14 | }); 15 | if (!user) { 16 | return new Response(JSON.stringify({ 17 | status: "fail", 18 | message: '请先登录', 19 | })) 20 | } 21 | const isAdmin = user.role === 'ADMIN'; 22 | if (!isAdmin && (!user.credit || user.credit <= 0)) { 23 | return new Response(JSON.stringify({ 24 | status: "fail", 25 | message: '使用次数不足', 26 | })) 27 | } 28 | 29 | if (!messages) { 30 | return new Response(JSON.stringify({ 31 | status: "fail", 32 | message: '请先输入你的问题', 33 | })) 34 | } 35 | 36 | 37 | const payload: OpenAIStreamPayload = { 38 | model: "gpt-3.5-turbo", 39 | messages, 40 | temperature: 0.7, 41 | top_p: 1, 42 | max_tokens: 800, 43 | stream: true, 44 | }; 45 | 46 | const stream = await OpenAIStream(payload); 47 | !isAdmin && await prisma.user.update({ 48 | where: { 49 | id: userId!, 50 | }, 51 | data: { 52 | credit: { 53 | decrement: 1 54 | }, 55 | }, 56 | }) 57 | return new Response(stream); 58 | 59 | }; 60 | 61 | export { handler as POST, handler as GET }; 62 | 63 | // export const runtime = "edge"; 64 | -------------------------------------------------------------------------------- /src/pages/login.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 56 | 57 | 79 | -------------------------------------------------------------------------------- /src/apis/request.ts: -------------------------------------------------------------------------------- 1 | import { getToken } from './auth' 2 | export interface Result { 3 | code: number 4 | msg: string 5 | data: T 6 | } 7 | 8 | const baseUrl = { 9 | dev: 'http://localhost:3000', 10 | production: 'https://aichat.codefe.top', 11 | } 12 | 13 | type MethodType = 14 | | 'OPTIONS' 15 | | 'GET' 16 | | 'HEAD' 17 | | 'POST' 18 | | 'PUT' 19 | | 'DELETE' 20 | | 'TRACE' 21 | | 'CONNECT' 22 | 23 | class Request { 24 | public request(method: MethodType, url: string, data?: any) { 25 | const token = getToken() 26 | return new Promise((resolve, reject) => { 27 | let result: any 28 | uni.request({ 29 | url: `${baseUrl.production}${url}`, 30 | method, 31 | timeout: 15000, 32 | header: { 33 | 'cookie': `token=${token}`, 34 | 'Authorization': `Bearer ${token || ''}`, 35 | 'content-type': 36 | method === 'GET' 37 | ? 'application/json; charset=utf-8' 38 | : 'application/json', 39 | }, 40 | data, 41 | success: (res: any) => { 42 | if (/chat-stream/.test(url) && res.statusCode === 401) { 43 | return uni.navigateTo({ 44 | url: '/pages/login', 45 | }) 46 | } 47 | if (typeof res.data === 'string') 48 | return resolve(res.data) 49 | 50 | result = res.data 51 | if (!result) 52 | throw new Error('[HTTP] Request has no return value') 53 | const { status, message, data } = result 54 | if (status === 'success') 55 | resolve(data) 56 | else 57 | reject(new Error(message || 'Error')) 58 | }, 59 | fail: (err) => { 60 | reject(err) 61 | }, 62 | }) 63 | }) 64 | } 65 | 66 | public get(url: string, data?: any) { 67 | return this.request('GET', url, data) as T 68 | } 69 | 70 | public post(url: string, data: any) { 71 | return this.request('POST', url, data) as T 72 | } 73 | } 74 | 75 | export default new Request() 76 | -------------------------------------------------------------------------------- /src/components/MessageItem.vue: -------------------------------------------------------------------------------- 1 | 44 | 45 | 67 | -------------------------------------------------------------------------------- /src/pages/weekly/index.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 66 | 67 | 72 | -------------------------------------------------------------------------------- /src/uni.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * 这里是uni-app内置的常用样式变量 3 | * 4 | * uni-app 官方扩展插件及插件市场(https://ext.dcloud.net.cn)上很多三方插件均使用了这些样式变量 5 | * 如果你是插件开发者,建议你使用scss预处理,并在插件代码中直接使用这些变量(无需 import 这个文件),方便用户通过搭积木的方式开发整体风格一致的App 6 | * 7 | */ 8 | 9 | /** 10 | * 如果你是App开发者(插件使用者),你可以通过修改这些变量来定制自己的插件主题,实现自定义主题功能 11 | * 12 | * 如果你的项目同样使用了scss预处理,你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件 13 | */ 14 | 15 | /* 颜色变量 */ 16 | 17 | /* 行为相关颜色 */ 18 | $uni-color-primary: #007aff; 19 | $uni-color-success: #4cd964; 20 | $uni-color-warning: #f0ad4e; 21 | $uni-color-error: #dd524d; 22 | 23 | /* 文字基本颜色 */ 24 | $uni-text-color:#333;//基本色 25 | $uni-text-color-inverse:#fff;//反色 26 | $uni-text-color-grey:#999;//辅助灰色,如加载更多的提示信息 27 | $uni-text-color-placeholder: #808080; 28 | $uni-text-color-disable:#c0c0c0; 29 | 30 | /* 背景颜色 */ 31 | $uni-bg-color:#ffffff; 32 | $uni-bg-color-grey:#f8f8f8; 33 | $uni-bg-color-hover:#f1f1f1;//点击状态颜色 34 | $uni-bg-color-mask:rgba(0, 0, 0, 0.4);//遮罩颜色 35 | 36 | /* 边框颜色 */ 37 | $uni-border-color:#c8c7cc; 38 | 39 | /* 尺寸变量 */ 40 | 41 | /* 文字尺寸 */ 42 | $uni-font-size-sm:24rpx; 43 | $uni-font-size-base:28rpx; 44 | $uni-font-size-lg:32rpx; 45 | 46 | /* 图片尺寸 */ 47 | $uni-img-size-sm:40rpx; 48 | $uni-img-size-base:52rpx; 49 | $uni-img-size-lg:80rpx; 50 | 51 | /* Border Radius */ 52 | $uni-border-radius-sm: 4rpx; 53 | $uni-border-radius-base: 6rpx; 54 | $uni-border-radius-lg: 12rpx; 55 | $uni-border-radius-circle: 50%; 56 | 57 | /* 水平间距 */ 58 | $uni-spacing-row-sm: 10px; 59 | $uni-spacing-row-base: 20rpx; 60 | $uni-spacing-row-lg: 30rpx; 61 | 62 | /* 垂直间距 */ 63 | $uni-spacing-col-sm: 8rpx; 64 | $uni-spacing-col-base: 16rpx; 65 | $uni-spacing-col-lg: 24rpx; 66 | 67 | /* 透明度 */ 68 | $uni-opacity-disabled: 0.3; // 组件禁用态的透明度 69 | 70 | /* 文章场景相关 */ 71 | $uni-color-title: #2C405A; // 文章标题颜色 72 | $uni-font-size-title:40rpx; 73 | $uni-color-subtitle: #555555; // 二级标题颜色 74 | $uni-font-size-subtitle:36rpx; 75 | $uni-color-paragraph: #3F536E; // 文章段落颜色 76 | $uni-font-size-paragraph:30rpx; 77 | 78 | @import 'uview-plus/theme.scss'; 79 | 80 | .u-cell { 81 | .u-cell__body { 82 | padding-left: 0; 83 | padding-right: 0; 84 | } 85 | } 86 | #tool-box .u-button--info { 87 | background: transparent; 88 | border: none; 89 | } 90 | #home .u-notice__content__text { 91 | color: inherit; 92 | } -------------------------------------------------------------------------------- /server/lib/openAIStream.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createParser, 3 | ParsedEvent, 4 | ReconnectInterval, 5 | } from "eventsource-parser"; 6 | 7 | export type ChatGPTAgent = "user" | "system"; 8 | 9 | export interface ChatGPTMessage { 10 | role: ChatGPTAgent; 11 | content: string; 12 | } 13 | 14 | export interface OpenAIStreamPayload { 15 | model: string; 16 | temperature: number; 17 | messages:ChatGPTMessage[] 18 | top_p: number; 19 | max_tokens: number; 20 | stream: boolean; 21 | } 22 | export async function OpenAIStream(payload: OpenAIStreamPayload) { 23 | const encoder = new TextEncoder(); 24 | const decoder = new TextDecoder(); 25 | 26 | let counter = 0; 27 | 28 | const res = await fetch("https://api.openai.com/v1/chat/completions", { 29 | headers: { 30 | "Content-Type": "application/json", 31 | Authorization: `Bearer ${process.env.OPENAI_API_KEY ?? ""}`, 32 | }, 33 | method: "POST", 34 | body: JSON.stringify(payload), 35 | }); 36 | 37 | const stream = new ReadableStream({ 38 | async start(controller) { 39 | // callback 40 | function onParse(event: ParsedEvent | ReconnectInterval) { 41 | if (event.type === "event") { 42 | const data = event.data; 43 | // https://beta.openai.com/docs/api-reference/completions/create#completions/create-stream 44 | if (data === "[DONE]") { 45 | controller.close(); 46 | return; 47 | } 48 | try { 49 | const json = JSON.parse(data); 50 | // const text = json.choices[0].text; 51 | const text = json.choices[0].delta?.content || ""; 52 | 53 | if (counter < 2 && (text.match(/\n/) || []).length) { 54 | // this is a prefix character (i.e., "\n\n"), do nothing 55 | return; 56 | } 57 | const queue = encoder.encode(text); 58 | controller.enqueue(queue); 59 | counter++; 60 | } catch (e) { 61 | // maybe parse error 62 | controller.error(e); 63 | } 64 | } 65 | } 66 | 67 | // stream response (SSE) from OpenAI may be fragmented into multiple chunks 68 | // this ensures we properly read chunks and invoke an event for each SSE event stream 69 | const parser = createParser(onParse); 70 | // https://web.dev/streams/#asynchronous-iteration 71 | for await (const chunk of res.body as any) { 72 | parser.feed(decoder.decode(chunk)); 73 | } 74 | }, 75 | }); 76 | 77 | return stream; 78 | } -------------------------------------------------------------------------------- /src/pages/generic/index.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 80 | 81 | 84 | -------------------------------------------------------------------------------- /unocss.config.ts: -------------------------------------------------------------------------------- 1 | import { 2 | defineConfig, 3 | presetAttributify, 4 | presetIcons, 5 | presetUno, 6 | transformerDirectives, 7 | transformerVariantGroup, 8 | } from 'unocss' 9 | 10 | import { 11 | presetApplet, 12 | presetRemRpx, 13 | transformerApplet, 14 | transformerAttributify, 15 | } from 'unocss-applet' 16 | 17 | const isApplet = process.env?.UNI_PLATFORM?.startsWith('mp') 18 | 19 | export default defineConfig({ 20 | // shortcuts: { 21 | // 'u-bg': 'bg-gray-100 dark:bg-black', 22 | // 'u-bg-2': 'bg-white dark:bg-[#1C1C1E]', 23 | // 'u-border': 'border-[#EBEDF0] dark:border-[#3A3A3C]', 24 | // 'u-active': 'bg-[#F2F3F5] dark:!bg-[#3A3A3C]', 25 | // 'u-active-h5': 'active:bg-[#F2F3F5] active:dark:bg-[#3A3A3C]', 26 | // 'u-text-color': 'text-[#323233] dark:text-[#F5F5F5]', 27 | // 'u-text-color-2': 'text-[#969799] dark:text-[#707070]', 28 | // 'u-text-color-3': 'text-[#C8C9CC] dark:text-[#4D4D4D]', 29 | // 'bg-primary': 'bg-light-blue-500 dark:bg-light-blue-600', 30 | // }, 31 | presets: [ 32 | presetIcons({ 33 | scale: 1.2, 34 | warn: true, 35 | extraProperties: { 36 | 'display': 'inline-block', 37 | 'vertical-align': 'middle', 38 | }, 39 | }), 40 | // presetUno(), 41 | presetApplet({ enable: isApplet }), 42 | /** 43 | * you can add `presetAttributify()` here to enable unocss attributify mode prompt 44 | * although preset is not working for applet, but will generate useless css 45 | */ 46 | presetAttributify(), 47 | presetRemRpx({ mode: isApplet ? 'rem2rpx' : 'rpx2rem' }), 48 | ], 49 | transformers: [ 50 | transformerDirectives(), 51 | transformerVariantGroup(), 52 | // Don't change the following order 53 | transformerAttributify({ ignoreAttributes: ['block'] }), 54 | transformerApplet(), 55 | ], 56 | rules: [ 57 | [ 58 | 'p-safe', 59 | { 60 | padding: 61 | 'env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left)', 62 | }, 63 | ], 64 | ['pt-safe', { 'padding-top': 'env(safe-area-inset-top)' }], 65 | ['pb-safe', { 'padding-bottom': 'env(safe-area-inset-bottom)' }], 66 | // 多行文本超出部分省略号 line-n 67 | [/^line-(\d+)$/, ([, l]) => { 68 | if (~~l === 1) { 69 | return { 70 | 'overflow': 'hidden', 71 | 'text-overflow': 'ellipsis', 72 | 'white-space': 'nowrap', 73 | 'width': '100%', 74 | } 75 | } 76 | return { 77 | 'overflow': 'hidden', 78 | 'display': '-webkit-box', 79 | '-webkit-box-orient': 'vertical', 80 | '-webkit-line-clamp': l, 81 | } 82 | }], 83 | ], 84 | }) 85 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ai-tools", 3 | "version": "0.1.0", 4 | "scripts": { 5 | "dev:app": "uni -p app", 6 | "dev:custom": "uni -p", 7 | "dev:h5": "uni", 8 | "dev:h5:ssr": "uni --ssr", 9 | "dev:mp-alipay": "uni -p mp-alipay", 10 | "dev:mp-baidu": "uni -p mp-baidu", 11 | "dev:mp-kuaishou": "uni -p mp-kuaishou", 12 | "dev:mp-lark": "uni -p mp-lark", 13 | "dev:mp-qq": "uni -p mp-qq", 14 | "dev:mp-toutiao": "uni -p mp-toutiao", 15 | "dev:mp-weixin": "uni -p mp-weixin", 16 | "dev:quickapp-webview": "uni -p quickapp-webview", 17 | "dev:quickapp-webview-huawei": "uni -p quickapp-webview-huawei", 18 | "dev:quickapp-webview-union": "uni -p quickapp-webview-union", 19 | "build:app": "uni build -p app", 20 | "build:custom": "uni build -p", 21 | "build:h5": "uni build", 22 | "build:h5:ssr": "uni build --ssr", 23 | "build:mp-alipay": "uni build -p mp-alipay", 24 | "build:mp-baidu": "uni build -p mp-baidu", 25 | "build:mp-kuaishou": "uni build -p mp-kuaishou", 26 | "build:mp-lark": "uni build -p mp-lark", 27 | "build:mp-qq": "uni build -p mp-qq", 28 | "build:mp-toutiao": "uni build -p mp-toutiao", 29 | "build:mp-weixin": "uni build -p mp-weixin", 30 | "build:quickapp-webview": "uni build -p quickapp-webview", 31 | "build:quickapp-webview-huawei": "uni build -p quickapp-webview-huawei", 32 | "build:quickapp-webview-union": "uni build -p quickapp-webview-union", 33 | "lint": "eslint .", 34 | "lint:fix": "nr lint --fix" 35 | }, 36 | "dependencies": { 37 | "@dcloudio/uni-app": "3.0.0-alpha-3070620230227001", 38 | "@dcloudio/uni-app-plus": "3.0.0-alpha-3070620230227001", 39 | "@dcloudio/uni-components": "3.0.0-alpha-3070620230227001", 40 | "@dcloudio/uni-h5": "3.0.0-alpha-3070620230227001", 41 | "@dcloudio/uni-mp-alipay": "3.0.0-alpha-3070620230227001", 42 | "@dcloudio/uni-mp-baidu": "3.0.0-alpha-3070620230227001", 43 | "@dcloudio/uni-mp-kuaishou": "3.0.0-alpha-3070620230227001", 44 | "@dcloudio/uni-mp-lark": "3.0.0-alpha-3070620230227001", 45 | "@dcloudio/uni-mp-qq": "3.0.0-alpha-3070620230227001", 46 | "@dcloudio/uni-mp-toutiao": "3.0.0-alpha-3070620230227001", 47 | "@dcloudio/uni-mp-weixin": "3.0.0-alpha-3070620230227001", 48 | "@dcloudio/uni-quickapp-webview": "3.0.0-alpha-3070620230227001", 49 | "@dcloudio/uni-ui": "^1.4.26", 50 | "ano-ui": "^0.6.3", 51 | "clipboard": "^2.0.11", 52 | "dayjs": "^1.11.7", 53 | "lodash-es": "^4.17.21", 54 | "pinia": "^2.0.32", 55 | "pinia-persistedstate-plugin": "^0.1.0", 56 | "uview-plus": "^3.1.28", 57 | "vk-uview-ui": "^1.4.4", 58 | "vue": "^3.2.47", 59 | "vue-demi": "^0.13.11", 60 | "vue-i18n": "^9.2.2" 61 | }, 62 | "devDependencies": { 63 | "@antfu/eslint-config": "^0.35.2", 64 | "@dcloudio/types": "^3.2.11", 65 | "@dcloudio/uni-automator": "3.0.0-alpha-3070620230227001", 66 | "@dcloudio/uni-cli-shared": "3.0.0-alpha-3070620230227001", 67 | "@dcloudio/uni-stacktracey": "3.0.0-alpha-3070620230227001", 68 | "@dcloudio/vite-plugin-uni": "3.0.0-alpha-3070620230227001", 69 | "@iconify-json/carbon": "^1.1.15", 70 | "@iconify-json/mdi": "^1.1.47", 71 | "@iconify/json": "^2.2.30", 72 | "@rollup/plugin-commonjs": "^24.0.1", 73 | "@types/lodash-es": "^4.17.6", 74 | "@types/node": "^18.14.0", 75 | "@uni-helper/vite-plugin-uni-components": "^0.0.3", 76 | "@uni-helper/vite-plugin-uni-layouts": "^0.0.2", 77 | "@unocss/eslint-config": "^0.49.8", 78 | "@vue/tsconfig": "^0.1.3", 79 | "eslint": "^8.34.0", 80 | "jest": "^29.4.3", 81 | "postcss": "^8.4.21", 82 | "sass": "^1.58.3", 83 | "terser": "^5.16.4", 84 | "typescript": "^4.9.5", 85 | "unocss": "^0.49.8", 86 | "unocss-applet": "^0.3.2", 87 | "unplugin-auto-import": "^0.14.4", 88 | "vite": "^4.0.3", 89 | "vite-plugin-inspect": "^0.7.15" 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/pages/me.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 87 | 88 | 128 | -------------------------------------------------------------------------------- /src/pages/config.ts: -------------------------------------------------------------------------------- 1 | // @unocss-include 2 | const config = [ 3 | { 4 | icon: 'https://cdn-icons-png.flaticon.com/512/2548/2548881.png', 5 | title: '聊天🔥', 6 | id: 'chat', 7 | desc: '支持会话上下文理解能力的智能问答聊天机器人', 8 | url: './chat/index', 9 | className: 'from-pink-500 via-red-500 to-yellow-500', 10 | }, 11 | { 12 | icon: 'https://cdn-icons-png.flaticon.com/128/726/726623.png', 13 | title: '邮件助手🔥', 14 | desc: '帮你生成客套话、富有创意和引人入胜的电子邮件内容的工具', 15 | url: './generic/index', 16 | id: 'email', 17 | className: 'from-purple-600 to-pink-600', 18 | pageConfig: { 19 | inputTitle: '请输入邮件内容', 20 | placeholder: 'eg: 邀请小明周末吃饭', 21 | val: '', 22 | prompt: 'Generate a business email in {{category}} that is friendly, but still professional and appropriate for the workplace.The topic is: ', 23 | category: { 24 | title: '选择语言', 25 | val: 'Simplified Chinese', 26 | options: [ 27 | { value: 'UK English', text: '英语' }, 28 | { value: 'Simplified Chinese', text: '中文' }, 29 | { value: 'Japanese', text: '日语' }, 30 | { value: 'Italiano', text: '意大利语' }, 31 | { value: 'Español', text: '西班牙语' }, 32 | { value: 'French', text: '法语' }, 33 | { value: '한국어', text: '韩语' }, 34 | ], 35 | }, 36 | }, 37 | }, 38 | // { 39 | // icon: 'https://cdn-icons-png.flaticon.com/512/3658/3658959.png', 40 | // title: '电影推荐', 41 | // desc: '一个为您的电子邮件生成富有创意和引人入胜的主题行的工具,帮助您获得更多的打开和点击!', 42 | // url: './generic/index', 43 | // id: 'movie', 44 | // className: 'from-green-300 via-blue-500 to-purple-600', 45 | // }, 46 | { 47 | icon: 'https://img.88icon.com/download/jpg/20200905/fbfdcc6d503f999641bec1d4b40869ca_512_512.jpg!88bg', 48 | title: '日周报生成🔥', 49 | desc: '输入工作内容,小助手帮你快速完成周报', 50 | url: './generic/index', 51 | id: 'weekly', 52 | className: 'from-purple-200 via-purple-400 to-purple-800', 53 | pageConfig: { 54 | inputTitle: '请输入日报内容', 55 | placeholder: 'eg: 完成了扫码登录功能,优化了UI', 56 | val: '', 57 | prompt: '围绕以下工作内容,帮我补充并写个工作的{{category}},内容+列表的形式: ', 58 | category: { 59 | title: '选择类型', 60 | val: '日报', 61 | options: [ 62 | { value: '日报', text: '日报' }, 63 | { value: '周报', text: '周报' }, 64 | { value: '月报', text: '月报' }, 65 | ], 66 | }, 67 | }, 68 | }, 69 | { 70 | icon: 'https://cos.codefe.top/images/xiaohongshu_icon.png', 71 | title: '小红书风格模拟器', 72 | desc: '输入你想发布的内容,帮你生成小红书的风格 ', 73 | url: './generic/index', 74 | id: 'xiaohongshu', 75 | className: 'from-purple-400 to-yellow-400', 76 | pageConfig: { 77 | inputTitle: '输入文案', 78 | placeholder: 'eg: 安利一款迪奥口红', 79 | val: '', 80 | prompt: '帮我扩展一下这段文字,起一个能吸引眼球的标题,内容润色成小红书的风格,每行开头都用不同的emoji: ', 81 | }, 82 | }, 83 | { 84 | icon: 'http://up.54fcnr.com/pic_source/04/40/00/044000bea092cdd4c5ed64d53d161730.gif', 85 | title: '哄女友小助手', 86 | desc: '请输入女朋友生气的原因,小助手为你生成一段道歉的话', 87 | url: './generic/index', 88 | id: 'apology', 89 | className: 'from-green-300 to-purple-400', 90 | pageConfig: { 91 | inputTitle: '请输入内容', 92 | placeholder: 'eg: 情人节没买礼物', 93 | val: '', 94 | prompt: '针对下面惹女朋友生气的原因,生成一段用于对女盆友道歉的文案,态度要表现的诚恳: ', 95 | }, 96 | }, 97 | { 98 | icon: 'https://cdn-icons-png.flaticon.com/512/2917/2917633.png', 99 | title: '食谱推荐', 100 | desc: '一种工具,可帮助您根据厨房中已有的食材查找食谱', 101 | url: './generic/index', 102 | id: 'recipe', 103 | className: 'from-fuchsia-500 via-red-600 to-orange-400', 104 | pageConfig: { 105 | inputTitle: '请输入食材', 106 | placeholder: 'eg: 土豆、胡萝卜', 107 | val: '', 108 | prompt: '针对下面这些食材,请帮我推荐一份合适的菜谱,并说出具体烹饪方法,风格是{{category}}: ', 109 | category: { 110 | title: '选择类型', 111 | val: '中餐', 112 | options: [ 113 | { value: '中餐', text: '中餐' }, 114 | { value: '西餐', text: '西餐' }, 115 | ], 116 | }, 117 | }, 118 | }, 119 | { 120 | icon: 'https://cos.codefe.top/images/roadblock.png', 121 | title: '施工中', 122 | desc: '敬请期待', 123 | url: './generic/index', 124 | id: 'recipe', 125 | className: 'bg-gray-400', 126 | status: 0, 127 | }, 128 | ] 129 | export default config 130 | -------------------------------------------------------------------------------- /src/pages/chat/index.vue: -------------------------------------------------------------------------------- 1 | 81 | 82 | 132 | 133 | 144 | -------------------------------------------------------------------------------- /src/auto-imports.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* prettier-ignore */ 3 | // @ts-nocheck 4 | // Generated by unplugin-auto-import 5 | export {} 6 | declare global { 7 | const EffectScope: typeof import('vue')['EffectScope'] 8 | const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate'] 9 | const computed: typeof import('vue')['computed'] 10 | const createApp: typeof import('vue')['createApp'] 11 | const createPinia: typeof import('pinia')['createPinia'] 12 | const customRef: typeof import('vue')['customRef'] 13 | const defineAsyncComponent: typeof import('vue')['defineAsyncComponent'] 14 | const defineComponent: typeof import('vue')['defineComponent'] 15 | const defineStore: typeof import('pinia')['defineStore'] 16 | const effectScope: typeof import('vue')['effectScope'] 17 | const getActivePinia: typeof import('pinia')['getActivePinia'] 18 | const getCurrentInstance: typeof import('vue')['getCurrentInstance'] 19 | const getCurrentScope: typeof import('vue')['getCurrentScope'] 20 | const h: typeof import('vue')['h'] 21 | const inject: typeof import('vue')['inject'] 22 | const isProxy: typeof import('vue')['isProxy'] 23 | const isReactive: typeof import('vue')['isReactive'] 24 | const isReadonly: typeof import('vue')['isReadonly'] 25 | const isRef: typeof import('vue')['isRef'] 26 | const mapActions: typeof import('pinia')['mapActions'] 27 | const mapGetters: typeof import('pinia')['mapGetters'] 28 | const mapState: typeof import('pinia')['mapState'] 29 | const mapStores: typeof import('pinia')['mapStores'] 30 | const mapWritableState: typeof import('pinia')['mapWritableState'] 31 | const markRaw: typeof import('vue')['markRaw'] 32 | const nextTick: typeof import('vue')['nextTick'] 33 | const onActivated: typeof import('vue')['onActivated'] 34 | const onAddToFavorites: typeof import('@dcloudio/uni-app')['onAddToFavorites'] 35 | const onBackPress: typeof import('@dcloudio/uni-app')['onBackPress'] 36 | const onBeforeMount: typeof import('vue')['onBeforeMount'] 37 | const onBeforeUnmount: typeof import('vue')['onBeforeUnmount'] 38 | const onBeforeUpdate: typeof import('vue')['onBeforeUpdate'] 39 | const onDeactivated: typeof import('vue')['onDeactivated'] 40 | const onError: typeof import('@dcloudio/uni-app')['onError'] 41 | const onErrorCaptured: typeof import('vue')['onErrorCaptured'] 42 | const onHide: typeof import('@dcloudio/uni-app')['onHide'] 43 | const onLaunch: typeof import('@dcloudio/uni-app')['onLaunch'] 44 | const onLoad: typeof import('@dcloudio/uni-app')['onLoad'] 45 | const onMounted: typeof import('vue')['onMounted'] 46 | const onNavigationBarButtonTap: typeof import('@dcloudio/uni-app')['onNavigationBarButtonTap'] 47 | const onNavigationBarSearchInputChanged: typeof import('@dcloudio/uni-app')['onNavigationBarSearchInputChanged'] 48 | const onNavigationBarSearchInputClicked: typeof import('@dcloudio/uni-app')['onNavigationBarSearchInputClicked'] 49 | const onNavigationBarSearchInputConfirmed: typeof import('@dcloudio/uni-app')['onNavigationBarSearchInputConfirmed'] 50 | const onNavigationBarSearchInputFocusChanged: typeof import('@dcloudio/uni-app')['onNavigationBarSearchInputFocusChanged'] 51 | const onPageNotFound: typeof import('@dcloudio/uni-app')['onPageNotFound'] 52 | const onPageScroll: typeof import('@dcloudio/uni-app')['onPageScroll'] 53 | const onPullDownRefresh: typeof import('@dcloudio/uni-app')['onPullDownRefresh'] 54 | const onReachBottom: typeof import('@dcloudio/uni-app')['onReachBottom'] 55 | const onReady: typeof import('@dcloudio/uni-app')['onReady'] 56 | const onRenderTracked: typeof import('vue')['onRenderTracked'] 57 | const onRenderTriggered: typeof import('vue')['onRenderTriggered'] 58 | const onResize: typeof import('@dcloudio/uni-app')['onResize'] 59 | const onScopeDispose: typeof import('vue')['onScopeDispose'] 60 | const onServerPrefetch: typeof import('vue')['onServerPrefetch'] 61 | const onShareAppMessage: typeof import('@dcloudio/uni-app')['onShareAppMessage'] 62 | const onShareTimeline: typeof import('@dcloudio/uni-app')['onShareTimeline'] 63 | const onShow: typeof import('@dcloudio/uni-app')['onShow'] 64 | const onTabItemTap: typeof import('@dcloudio/uni-app')['onTabItemTap'] 65 | const onThemeChange: typeof import('@dcloudio/uni-app')['onThemeChange'] 66 | const onUnhandledRejection: typeof import('@dcloudio/uni-app')['onUnhandledRejection'] 67 | const onUnload: typeof import('@dcloudio/uni-app')['onUnload'] 68 | const onUnmounted: typeof import('vue')['onUnmounted'] 69 | const onUpdated: typeof import('vue')['onUpdated'] 70 | const provide: typeof import('vue')['provide'] 71 | const reactive: typeof import('vue')['reactive'] 72 | const readonly: typeof import('vue')['readonly'] 73 | const ref: typeof import('vue')['ref'] 74 | const resolveComponent: typeof import('vue')['resolveComponent'] 75 | const setActivePinia: typeof import('pinia')['setActivePinia'] 76 | const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix'] 77 | const setupPinia: typeof import('./stores/index')['setupPinia'] 78 | const shallowReactive: typeof import('vue')['shallowReactive'] 79 | const shallowReadonly: typeof import('vue')['shallowReadonly'] 80 | const shallowRef: typeof import('vue')['shallowRef'] 81 | const storeToRefs: typeof import('pinia')['storeToRefs'] 82 | const stores: typeof import('./stores/index')['default'] 83 | const toRaw: typeof import('vue')['toRaw'] 84 | const toRef: typeof import('vue')['toRef'] 85 | const toRefs: typeof import('vue')['toRefs'] 86 | const triggerRef: typeof import('vue')['triggerRef'] 87 | const unref: typeof import('vue')['unref'] 88 | const useAttrs: typeof import('vue')['useAttrs'] 89 | const useChatgpt: typeof import('./composables/useChatgpt')['useChatgpt'] 90 | const useCssModule: typeof import('vue')['useCssModule'] 91 | const useCssVars: typeof import('vue')['useCssVars'] 92 | const useShare: typeof import('./composables/useShare')['useShare'] 93 | const useSlots: typeof import('vue')['useSlots'] 94 | const user: typeof import('./stores/user')['default'] 95 | const watch: typeof import('vue')['watch'] 96 | const watchEffect: typeof import('vue')['watchEffect'] 97 | const watchPostEffect: typeof import('vue')['watchPostEffect'] 98 | const watchSyncEffect: typeof import('vue')['watchSyncEffect'] 99 | } 100 | // for type re-export 101 | declare global { 102 | // @ts-ignore 103 | export type { Component,ComponentPublicInstance,ComputedRef,InjectionKey,PropType,Ref,VNode } from 'vue' 104 | } 105 | // for vue template auto import 106 | import { UnwrapRef } from 'vue' 107 | declare module 'vue' { 108 | interface ComponentCustomProperties { 109 | readonly EffectScope: UnwrapRef 110 | readonly acceptHMRUpdate: UnwrapRef 111 | readonly computed: UnwrapRef 112 | readonly createApp: UnwrapRef 113 | readonly createPinia: UnwrapRef 114 | readonly customRef: UnwrapRef 115 | readonly defineAsyncComponent: UnwrapRef 116 | readonly defineComponent: UnwrapRef 117 | readonly defineStore: UnwrapRef 118 | readonly effectScope: UnwrapRef 119 | readonly getActivePinia: UnwrapRef 120 | readonly getCurrentInstance: UnwrapRef 121 | readonly getCurrentScope: UnwrapRef 122 | readonly h: UnwrapRef 123 | readonly inject: UnwrapRef 124 | readonly isProxy: UnwrapRef 125 | readonly isReactive: UnwrapRef 126 | readonly isReadonly: UnwrapRef 127 | readonly isRef: UnwrapRef 128 | readonly mapActions: UnwrapRef 129 | readonly mapGetters: UnwrapRef 130 | readonly mapState: UnwrapRef 131 | readonly mapStores: UnwrapRef 132 | readonly mapWritableState: UnwrapRef 133 | readonly markRaw: UnwrapRef 134 | readonly nextTick: UnwrapRef 135 | readonly onActivated: UnwrapRef 136 | readonly onAddToFavorites: UnwrapRef 137 | readonly onBackPress: UnwrapRef 138 | readonly onBeforeMount: UnwrapRef 139 | readonly onBeforeUnmount: UnwrapRef 140 | readonly onBeforeUpdate: UnwrapRef 141 | readonly onDeactivated: UnwrapRef 142 | readonly onError: UnwrapRef 143 | readonly onErrorCaptured: UnwrapRef 144 | readonly onHide: UnwrapRef 145 | readonly onLaunch: UnwrapRef 146 | readonly onLoad: UnwrapRef 147 | readonly onMounted: UnwrapRef 148 | readonly onNavigationBarButtonTap: UnwrapRef 149 | readonly onNavigationBarSearchInputChanged: UnwrapRef 150 | readonly onNavigationBarSearchInputClicked: UnwrapRef 151 | readonly onNavigationBarSearchInputConfirmed: UnwrapRef 152 | readonly onNavigationBarSearchInputFocusChanged: UnwrapRef 153 | readonly onPageNotFound: UnwrapRef 154 | readonly onPageScroll: UnwrapRef 155 | readonly onPullDownRefresh: UnwrapRef 156 | readonly onReachBottom: UnwrapRef 157 | readonly onReady: UnwrapRef 158 | readonly onRenderTracked: UnwrapRef 159 | readonly onRenderTriggered: UnwrapRef 160 | readonly onResize: UnwrapRef 161 | readonly onScopeDispose: UnwrapRef 162 | readonly onServerPrefetch: UnwrapRef 163 | readonly onShareAppMessage: UnwrapRef 164 | readonly onShareTimeline: UnwrapRef 165 | readonly onShow: UnwrapRef 166 | readonly onTabItemTap: UnwrapRef 167 | readonly onThemeChange: UnwrapRef 168 | readonly onUnhandledRejection: UnwrapRef 169 | readonly onUnload: UnwrapRef 170 | readonly onUnmounted: UnwrapRef 171 | readonly onUpdated: UnwrapRef 172 | readonly provide: UnwrapRef 173 | readonly reactive: UnwrapRef 174 | readonly readonly: UnwrapRef 175 | readonly ref: UnwrapRef 176 | readonly resolveComponent: UnwrapRef 177 | readonly setActivePinia: UnwrapRef 178 | readonly setMapStoreSuffix: UnwrapRef 179 | readonly setupPinia: UnwrapRef 180 | readonly shallowReactive: UnwrapRef 181 | readonly shallowReadonly: UnwrapRef 182 | readonly shallowRef: UnwrapRef 183 | readonly storeToRefs: UnwrapRef 184 | readonly stores: UnwrapRef 185 | readonly toRaw: UnwrapRef 186 | readonly toRef: UnwrapRef 187 | readonly toRefs: UnwrapRef 188 | readonly triggerRef: UnwrapRef 189 | readonly unref: UnwrapRef 190 | readonly useAttrs: UnwrapRef 191 | readonly useChatgpt: UnwrapRef 192 | readonly useCssModule: UnwrapRef 193 | readonly useCssVars: UnwrapRef 194 | readonly useShare: UnwrapRef 195 | readonly useSlots: UnwrapRef 196 | readonly user: UnwrapRef 197 | readonly watch: UnwrapRef 198 | readonly watchEffect: UnwrapRef 199 | readonly watchPostEffect: UnwrapRef 200 | readonly watchSyncEffect: UnwrapRef 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /src/components/mp-html/mp-html.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 482 | 483 | 499 | -------------------------------------------------------------------------------- /src/components/mp-html/node/node.vue: -------------------------------------------------------------------------------- 1 |