first commit

This commit is contained in:
scout 2024-05-23 10:20:16 +08:00
commit 14a82e0150
91 changed files with 15332 additions and 0 deletions

9
.editorconfig Normal file
View File

@ -0,0 +1,9 @@
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

33
.gitignore vendored Normal file
View File

@ -0,0 +1,33 @@
.DS_Store
.thumbs.db
node_modules
# Quasar core related directories
.quasar
/dist
/quasar.config.*.temporary.compiled*
# Cordova related directories and files
/src-cordova/node_modules
/src-cordova/platforms
/src-cordova/plugins
/src-cordova/www
# Capacitor related directories and files
/src-capacitor/www
/src-capacitor/node_modules
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor directories and files
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
# local .env files
.env.local*

5
.npmrc Normal file
View File

@ -0,0 +1,5 @@
# pnpm-related options
shamefully-hoist=true
strict-peer-dependencies=false
# to get the latest compatible packages when creating the project https://github.com/pnpm/pnpm/issues/6463
resolution-mode=highest

13
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,13 @@
{
"recommendations": [
"editorconfig.editorconfig",
"vue.volar",
"wayou.vscode-todo-highlight"
],
"unwantedRecommendations": [
"octref.vetur",
"hookyqr.beautify",
"dbaeumer.jshint",
"ms-vscode.vscode-typescript-tslint-plugin"
]
}

4
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,4 @@
{
"editor.bracketPairColorization.enabled": true,
"editor.guides.bracketPairs": true
}

24
README.md Normal file
View File

@ -0,0 +1,24 @@
# stableDiffusion (stablediffusion)
A Quasar Project
## Install the dependencies
```bash
yarn
# or
npm install
```
### Start the app in development mode (hot-code reloading, error reporting, etc.)
```bash
quasar dev
```
### Build the app for production
```bash
quasar build
```
### Customize the configuration
See [Configuring quasar.config.js](https://v2.quasar.dev/quasar-cli-vite/quasar-config-js).

21
index.html Normal file
View File

@ -0,0 +1,21 @@
<!DOCTYPE html>
<html>
<head>
<title><%= productName %></title>
<meta charset="utf-8">
<meta name="description" content="<%= productDescription %>">
<meta name="format-detection" content="telephone=no">
<meta name="msapplication-tap-highlight" content="no">
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width<% if (ctx.mode.cordova || ctx.mode.capacitor) { %>, viewport-fit=cover<% } %>">
<link rel="icon" type="image/png" sizes="128x128" href="icons/favicon-128x128.png">
<link rel="icon" type="image/png" sizes="96x96" href="icons/favicon-96x96.png">
<link rel="icon" type="image/png" sizes="32x32" href="icons/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="icons/favicon-16x16.png">
<link rel="icon" type="image/ico" href="favicon.ico">
</head>
<body>
<!-- quasar:entry-point -->
</body>
</html>

39
jsconfig.json Normal file
View File

@ -0,0 +1,39 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"src/*": [
"src/*"
],
"app/*": [
"*"
],
"components/*": [
"src/components/*"
],
"layouts/*": [
"src/layouts/*"
],
"pages/*": [
"src/pages/*"
],
"assets/*": [
"src/assets/*"
],
"boot/*": [
"src/boot/*"
],
"stores/*": [
"src/stores/*"
],
"vue$": [
"node_modules/vue/dist/vue.runtime.esm-bundler.js"
]
}
},
"exclude": [
"dist",
".quasar",
"node_modules"
]
}

8672
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

39
package.json Normal file
View File

@ -0,0 +1,39 @@
{
"name": "stablediffusion",
"version": "0.0.1",
"description": "A Quasar Project",
"productName": "stableDiffusion",
"author": "scout <1134087124@qq.com>",
"private": true,
"scripts": {
"test": "echo \"No test specified\" && exit 0",
"dev": "quasar dev",
"build": "quasar build"
},
"dependencies": {
"@formkit/auto-animate": "^0.8.1",
"@quasar/extras": "^1.16.4",
"animate.css": "^4.1.1",
"axios": "^1.2.1",
"moment": "^2.30.1",
"quasar": "^2.8.0",
"vue": "^3.4.18",
"vue-i18n": "^9.0.0",
"vue-router": "^4.0.12",
"vue3-lazyload": "^0.3.8"
},
"devDependencies": {
"@intlify/vite-plugin-vue-i18n": "^3.3.1",
"@quasar/app-vite": "^1.8.0",
"autoprefixer": "^10.4.19",
"naive-ui": "^2.38.1",
"postcss": "^8.4.38",
"unplugin-auto-import": "^0.17.5",
"vfonts": "^0.0.3"
},
"engines": {
"node": "^20 || ^18 || ^16",
"npm": ">= 6.13.4",
"yarn": ">= 1.21.1"
}
}

27
postcss.config.cjs Normal file
View File

@ -0,0 +1,27 @@
/* eslint-disable */
// https://github.com/michael-ciniawsky/postcss-load-config
module.exports = {
plugins: [
// https://github.com/postcss/autoprefixer
require('autoprefixer')({
overrideBrowserslist: [
'last 4 Chrome versions',
'last 4 Firefox versions',
'last 4 Edge versions',
'last 4 Safari versions',
'last 4 Android versions',
'last 4 ChromeAndroid versions',
'last 4 FirefoxAndroid versions',
'last 4 iOS versions'
],
}),
// https://github.com/elchininet/postcss-rtlcss
// If you want to support RTL css, then
// 1. yarn/npm install postcss-rtlcss
// 2. optionally set quasar.config.js > framework > lang to an RTL language
// 3. uncomment the following line:
// require('postcss-rtlcss')
]
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 859 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

233
quasar.config.js Normal file
View File

@ -0,0 +1,233 @@
/* eslint-env node */
/*
* This file runs in a Node context (it's NOT transpiled by Babel), so use only
* the ES6 features that are supported by your Node version. https://node.green/
*/
// Configuration for your app
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js
const { configure } = require("quasar/wrappers");
const path = require("path");
module.exports = configure(function (/* ctx */) {
return {
// https://v2.quasar.dev/quasar-cli-vite/prefetch-feature
// preFetch: true,
// app boot file (/src/boot)
// --> boot files are part of "main.js"
// https://v2.quasar.dev/quasar-cli-vite/boot-files
boot: ["i18n", "axios", "naiveUI"],
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css
css: ["app.scss"],
// https://github.com/quasarframework/quasar/tree/dev/extras
extras: [
// 'ionicons-v4',
// 'mdi-v7',
// 'fontawesome-v6',
// 'eva-icons',
// 'themify',
// 'line-awesome',
// 'roboto-font-latin-ext', // this or either 'roboto-font', NEVER both!
"roboto-font", // optional, you are not bound to it
"material-icons", // optional, you are not bound to it
],
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#build
build: {
target: {
browser: ["es2019", "edge88", "firefox78", "chrome87", "safari13.1"],
node: "node20",
},
vueRouterMode: "hash", // available values: 'hash', 'history'
// vueRouterBase,
// vueDevtools,
// vueOptionsAPI: false,
// rebuildCache: true, // rebuilds Vite/linter/etc cache on startup
// publicPath: '/',
// analyze: true,
// env: {},
// rawDefine: {}
// ignorePublicFolder: true,
// minify: false,
// polyfillModulePreload: true,
// distDir
// extendViteConf (viteConf) {},
// viteVuePluginOptions: {},
vitePlugins: [
[
"@intlify/vite-plugin-vue-i18n",
{
// if you want to use Vue I18n Legacy API, you need to set `compositionOnly: false`
// compositionOnly: false,
// if you want to use named tokens in your Vue I18n messages, such as 'Hello {name}',
// you need to set `runtimeOnly: false`
// runtimeOnly: false,
// you need to set i18n resource including paths !
include: path.resolve(__dirname, "./src/i18n/**"),
},
],
],
},
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#devServer
devServer: {
// https: true
open: true, // opens browser window automatically
},
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#framework
framework: {
config: {},
// iconSet: 'material-icons', // Quasar icon set
lang: 'zh-CN', // Quasar language pack
// For special cases outside of where the auto-import strategy can have an impact
// (like functional components as one of the examples),
// you can manually specify Quasar components/directives to be available everywhere:
//
// components: [],
// directives: [],
// Quasar plugins
plugins: ["LocalStorage", "SessionStorage", "Notify","Dialog"],
},
// animations: 'all', // --- includes all animations
// https://v2.quasar.dev/options/animations
animations: 'all',
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#property-sourcefiles
// sourceFiles: {
// rootComponent: 'src/App.vue',
// router: 'src/router/index',
// store: 'src/store/index',
// registerServiceWorker: 'src-pwa/register-service-worker',
// serviceWorker: 'src-pwa/custom-service-worker',
// pwaManifestFile: 'src-pwa/manifest.json',
// electronMain: 'src-electron/electron-main',
// electronPreload: 'src-electron/electron-preload'
// },
// https://v2.quasar.dev/quasar-cli-vite/developing-ssr/configuring-ssr
ssr: {
// ssrPwaHtmlFilename: "sd.html", // do NOT use index.html as name!
// will mess up SSR
// extendSSRWebserverConf(esbuildConf) {},
// extendPackageJson(json) {},
pwa: false,
/**
* 自己提供手动处理 store state 序列化的方法
* as window.__INITIAL_STATE__ to the client-side (through a <script> tag)
* 通过<script>标签注入到客户端的 window.__INITIAL_STATE__
* (需要 @quasar/app-vite v1.0.0-beta.14+)
*/
manualStoreSerialization: false,
/**
* 手动注入 store state ssrContext.state
* (需要 @quasar/app-vite v1.0.0-beta.14+)
*/
manualStoreSsrContextInjection: false,
/**
* 手动处理 store 的水化hydration过程而代替 Quasar CLI 的水化过程
* For Pinia: store.state.value = window.__INITIAL_STATE__
* For Vuex: store.replaceState(window.__INITIAL_STATE__)
*/
manualStoreHydration: false,
/**
* 手动调用$q.onSSRHydrated()代替 Quasar CLI 做的
* 这宣布客户端代码应该接管
* This announces that client-side code should takeover.
*/
manualPostHydrationTrigger: false,
prodPort: 3000, // 生产环境下 server 默认使用的端口号
// (如果在运行时指定了 ({}).PORT则被取代)
middlewares: [
"render", // 添加自己的中间件时,保证'render'为最后一个
// keep this as last one
],
},
// https://v2.quasar.dev/quasar-cli-vite/developing-pwa/configuring-pwa
pwa: {
workboxMode: "generateSW", // or 'injectManifest'
injectPwaMetaTags: true,
swFilename: "sw.js",
manifestFilename: "manifest.json",
useCredentialsForManifestTag: false,
// useFilenameHashes: true,
// extendGenerateSWOptions (cfg) {}
// extendInjectManifestOptions (cfg) {},
// extendManifestJson (json) {}
// extendPWACustomSWConf (esbuildConf) {}
},
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/developing-cordova-apps/configuring-cordova
cordova: {
// noIosLegacyBuildFlag: true, // uncomment only if you know what you are doing
},
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/developing-capacitor-apps/configuring-capacitor
capacitor: {
hideSplashscreen: true,
},
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/developing-electron-apps/configuring-electron
electron: {
// extendElectronMainConf (esbuildConf)
// extendElectronPreloadConf (esbuildConf)
// specify the debugging port to use for the Electron app when running in development mode
inspectPort: 5858,
bundler: "packager", // 'packager' or 'builder'
packager: {
// https://github.com/electron-userland/electron-packager/blob/master/docs/api.md#options
// OS X / Mac App Store
// appBundleId: '',
// appCategoryType: '',
// osxSign: '',
// protocol: 'myapp://path',
// Windows only
// win32metadata: { ... }
},
builder: {
// https://www.electron.build/configuration/configuration
appId: "stablediffusion",
},
},
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/developing-browser-extensions/configuring-bex
bex: {
contentScripts: ["my-content-script"],
// extendBexScriptsConf (esbuildConf) {}
// extendBexManifestJson (json) {}
},
};
});

View File

@ -0,0 +1,58 @@
import { ssrMiddleware } from 'quasar/wrappers'
// This middleware should execute as last one
// since it captures everything and tries to
// render the page with Vue
export default ssrMiddleware(({ app, resolve, render, serve }) => {
// we capture any other Express route and hand it
// over to Vue and Vue Router to render our page
app.get(resolve.urlPath('*'), (req, res) => {
res.setHeader('Content-Type', 'text/html')
render(/* the ssrContext: */ { req, res })
.then(html => {
// now let's send the rendered html to the client
res.send(html)
})
.catch(err => {
// oops, we had an error while rendering the page
// we were told to redirect to another URL
if (err.url) {
if (err.code) {
res.redirect(err.code, err.url)
} else {
res.redirect(err.url)
}
} else if (err.code === 404) {
// hmm, Vue Router could not find the requested route
// Should reach here only if no "catch-all" route
// is defined in /src/routes
res.status(404).send('404 | Page Not Found')
} else if (process.env.DEV) {
// well, we treat any other code as error;
// if we're in dev mode, then we can use Quasar CLI
// to display a nice error page that contains the stack
// and other useful information
// serve.error is available on dev only
serve.error({ err, req, res })
} else {
// we're in production, so we should have another method
// to display something to the client when we encounter an error
// (for security reasons, it's not ok to display the same wealth
// of information as we do in development)
// Render Error Page on production or
// create a route (/src/routes) for an error page and redirect to it
res.status(500).send('500 | Internal Server Error')
if (process.env.DEBUGGING) {
console.error(err.stack)
}
}
})
})
})

136
src-ssr/server.js Normal file
View File

@ -0,0 +1,136 @@
/**
* More info about this file:
* https://v2.quasar.dev/quasar-cli-vite/developing-ssr/ssr-webserver
*
* Runs in Node context.
*/
/**
* Make sure to yarn add / npm install (in your project root)
* anything you import here (except for express and compression).
*/
import express from 'express'
import compression from 'compression'
import {
ssrClose,
ssrCreate,
ssrListen,
ssrRenderPreloadTag,
ssrServeStaticContent
} from 'quasar/wrappers'
/**
* Create your webserver and return its instance.
* If needed, prepare your webserver to receive
* connect-like middlewares.
*
* Should NOT be async!
*/
export const create = ssrCreate((/* { ... } */) => {
const app = express()
// attackers can use this header to detect apps running Express
// and then launch specifically-targeted attacks
app.disable('x-powered-by')
// place here any middlewares that
// absolutely need to run before anything else
if (process.env.PROD) {
app.use(compression())
}
return app
})
/**
* You need to make the server listen to the indicated port
* and return the listening instance or whatever you need to
* close the server with.
*
* The "listenResult" param for the "close()" definition below
* is what you return here.
*
* For production, you can instead export your
* handler for serverless use or whatever else fits your needs.
*/
export const listen = ssrListen(async ({ app, port, isReady }) => {
await isReady()
return app.listen(port, () => {
if (process.env.PROD) {
console.log('Server listening at port ' + port)
}
})
})
/**
* Should close the server and free up any resources.
* Will be used on development only when the server needs
* to be rebooted.
*
* Should you need the result of the "listen()" call above,
* you can use the "listenResult" param.
*
* Can be async.
*/
export const close = ssrClose(({ listenResult }) => {
return listenResult.close()
})
const maxAge = process.env.DEV
? 0
: 1000 * 60 * 60 * 24 * 30
/**
* Should return middleware that serves the indicated path
* with static content.
*/
export const serveStaticContent = ssrServeStaticContent((path, opts) => {
return express.static(path, {
maxAge,
...opts
})
})
const jsRE = /\.js$/
const cssRE = /\.css$/
const woffRE = /\.woff$/
const woff2RE = /\.woff2$/
const gifRE = /\.gif$/
const jpgRE = /\.jpe?g$/
const pngRE = /\.png$/
/**
* Should return a String with HTML output
* (if any) for preloading indicated file
*/
export const renderPreloadTag = ssrRenderPreloadTag((file) => {
if (jsRE.test(file) === true) {
return `<link rel="modulepreload" href="${file}" crossorigin>`
}
if (cssRE.test(file) === true) {
return `<link rel="stylesheet" href="${file}">`
}
if (woffRE.test(file) === true) {
return `<link rel="preload" href="${file}" as="font" type="font/woff" crossorigin>`
}
if (woff2RE.test(file) === true) {
return `<link rel="preload" href="${file}" as="font" type="font/woff2" crossorigin>`
}
if (gifRE.test(file) === true) {
return `<link rel="preload" href="${file}" as="image" type="image/gif">`
}
if (jpgRE.test(file) === true) {
return `<link rel="preload" href="${file}" as="image" type="image/jpeg">`
}
if (pngRE.test(file) === true) {
return `<link rel="preload" href="${file}" as="image" type="image/png">`
}
return ''
})

10
src-ssr/ssr-flag.d.ts vendored Normal file
View File

@ -0,0 +1,10 @@
/* eslint-disable */
// THIS FEATURE-FLAG FILE IS AUTOGENERATED,
// REMOVAL OR CHANGES WILL CAUSE RELATED TYPES TO STOP WORKING
import "quasar/dist/types/feature-flag";
declare module "quasar/dist/types/feature-flag" {
interface QuasarFeatureFlags {
ssr: true;
}
}

13
src/App.vue Normal file
View File

@ -0,0 +1,13 @@
<template>
<n-config-provider :locale="zhCN">
<router-view />
</n-config-provider>
</template>
<script setup>
import { NConfigProvider } from "naive-ui";
import { zhCN } from "naive-ui";
defineOptions({
name: "App",
});
</script>

103
src/api/index.js Normal file
View File

@ -0,0 +1,103 @@
// src/api/index.js
import { api } from "../boot/axios";
export class UserApi {
static async login(data) {
return api.post("/api/ai/user/login", data);
}
// 获取注册验证码
static async getRegisterCode(data) {
return api.post("/api/user/send/msg", data);
}
// 注册
static async register(data) {
return api.post("/api/ai/user/register", data);
}
// 获取登录验证码
static async getLoginCode(data) {
return api.post("/api/ai/user/login/msg", data);
}
// 重置密码
static async resetPassword(data) {
return api.post("/api/ai/user/forget/pwd", data);
}
// 忘记密码验证码
static async getForgetPwdCode(data) {
return api.post("/api/ai/user/forget/msg", data);
}
// 获取用户信息
static async getUserInfo(data) {
return api.post("/api/ai/user/info", data);
}
// 修改昵称 和 头像 密码
static async updateUserInfo(data) {
return api.post("/api/ai/user/update", data);
}
// 修改手机号验证码
static async getUpdatePhoneCode(data) {
return api.post("/api/ai/user/phone/update/send", data);
}
// 修改手机号
static async updatePhone(data) {
return api.post("/api/ai/user/phone/update", data);
}
// 提交反馈
static async submitFeedBack(data) {
return api.post("/api/feedback/create", data);
}
}
// 模型相关接口
export class ModelApi {
// 获取模型列表
static async getModelList(data) {
return api.post("/api/ai/modules/v2", data);
}
// 文生图
static async txt2img(data) {
return api.post("/api/ai/txt2img", data);
}
// 翻译
static async translate(data) {
return api.post("/api/ai/gpt/txt/translate", data);
}
// 美化
static async beautify(data) {
return api.post("/api/ai/gpt/txt/beaut", data);
}
// 上传图片
static async uploadImage(data) {
return api.post("/api/ai/upload-file", data);
}
// 图生图
static async img2img(data) {
return api.post("/api/ai/img2img", data);
}
}
// 历史相关
export class HistoryApi {
// 获取历史记录
static async getHistoryList(data) {
return api.post("/api/ai/my/images", data);
}
// 删除历史记录
static async deleteHistory(data) {
return api.post("/api/ai/image/del/batch", data);
}
// 批量下载
static async downloadHistory(data) {
return api.post("/api/ai/image/download/batch", data, {
responseType: "blob",
});
}
}
// 积分相关
export class ScoreApi {
// 消费积分列表
static async getScore(data) {
return api.post("/api/ai/coin/custom/list", data);
}
// 奖励积分列表
static async getScoreList(data) {
return api.post("/api/ai/coin/acquire/list", data);
}
}

Binary file not shown.

BIN
src/assets/image/ai/169.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 817 B

BIN
src/assets/image/ai/34.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 819 B

BIN
src/assets/image/ai/43.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 852 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 517 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 480 B

BIN
src/assets/image/ai/ptp.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
src/assets/image/ai/run.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
src/assets/image/ai/ttp.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 267 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

BIN
src/assets/image/ast.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

BIN
src/assets/image/coin.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 961 B

BIN
src/assets/image/exit.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 370 B

BIN
src/assets/image/glass.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 782 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 951 B

BIN
src/assets/image/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 395 KiB

BIN
src/assets/image/oldman.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 MiB

BIN
src/assets/image/patato.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

BIN
src/assets/image/per.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 723 B

BIN
src/assets/image/train.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

View File

@ -0,0 +1,15 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 356 360">
<path
d="M43.4 303.4c0 3.8-2.3 6.3-7.1 6.3h-15v-22h14.4c4.3 0 6.2 2.2 6.2 5.2 0 2.6-1.5 4.4-3.4 5 2.8.4 4.9 2.5 4.9 5.5zm-8-13H24.1v6.9H35c2.1 0 4-1.3 4-3.8 0-2.2-1.3-3.1-3.7-3.1zm5.1 12.6c0-2.3-1.8-3.7-4-3.7H24.2v7.7h11.7c3.4 0 4.6-1.8 4.6-4zm36.3 4v2.7H56v-22h20.6v2.7H58.9v6.8h14.6v2.3H58.9v7.5h17.9zm23-5.8v8.5H97v-8.5l-11-13.4h3.4l8.9 11 8.8-11h3.4l-10.8 13.4zm19.1-1.8V298c0-7.9 5.2-10.7 12.7-10.7 7.5 0 13 2.8 13 10.7v1.4c0 7.9-5.5 10.8-13 10.8s-12.7-3-12.7-10.8zm22.7 0V298c0-5.7-3.9-8-10-8-6 0-9.8 2.3-9.8 8v1.4c0 5.8 3.8 8.1 9.8 8.1 6 0 10-2.3 10-8.1zm37.2-11.6v21.9h-2.9l-15.8-17.9v17.9h-2.8v-22h3l15.6 18v-18h2.9zm37.9 10.2v1.3c0 7.8-5.2 10.4-12.4 10.4H193v-22h11.2c7.2 0 12.4 2.8 12.4 10.3zm-3 0c0-5.3-3.3-7.6-9.4-7.6h-8.4V307h8.4c6 0 9.5-2 9.5-7.7V298zm50.8-7.6h-9.7v19.3h-3v-19.3h-9.7v-2.6h22.4v2.6zm34.4-2.6v21.9h-3v-10.1h-16.8v10h-2.8v-21.8h2.8v9.2H296v-9.2h2.9zm34.9 19.2v2.7h-20.7v-22h20.6v2.7H316v6.8h14.5v2.3H316v7.5h17.8zM24 340.2v7.3h13.9v2.4h-14v9.6H21v-22h20v2.7H24zm41.5 11.4h-9.8v7.9H53v-22h13.3c5.1 0 8 1.9 8 6.8 0 3.7-2 6.3-5.6 7l6 8.2h-3.3l-5.8-8zm-9.8-2.6H66c3.1 0 5.3-1.5 5.3-4.7 0-3.3-2.2-4.1-5.3-4.1H55.7v8.8zm47.9 6.2H89l-2 4.3h-3.2l10.7-22.2H98l10.7 22.2h-3.2l-2-4.3zm-1-2.3l-6.3-13-6 13h12.2zm46.3-15.3v21.9H146v-17.2L135.7 358h-2.1l-10.2-15.6v17h-2.8v-21.8h3l11 16.9 11.3-17h3zm35 19.3v2.6h-20.7v-22h20.6v2.7H166v6.8h14.5v2.3H166v7.6h17.8zm47-19.3l-8.3 22h-3l-7.1-18.6-7 18.6h-3l-8.2-22h3.3L204 356l6.8-18.5h3.4L221 356l6.6-18.5h3.3zm10 11.6v-1.4c0-7.8 5.2-10.7 12.7-10.7 7.6 0 13 2.9 13 10.7v1.4c0 7.9-5.4 10.8-13 10.8-7.5 0-12.7-3-12.7-10.8zm22.8 0v-1.4c0-5.7-4-8-10-8s-9.9 2.3-9.9 8v1.4c0 5.8 3.8 8.2 9.8 8.2 6.1 0 10-2.4 10-8.2zm28.3 2.4h-9.8v7.9h-2.8v-22h13.2c5.2 0 8 1.9 8 6.8 0 3.7-2 6.3-5.6 7l6 8.2h-3.3l-5.8-8zm-9.8-2.6h10.2c3 0 5.2-1.5 5.2-4.7 0-3.3-2.1-4.1-5.2-4.1h-10.2v8.8zm40.3-1.5l-6.8 5.6v6.4h-2.9v-22h2.9v12.3l15.2-12.2h3.7l-9.9 8.1 10.3 13.8h-3.6l-8.9-12z" />
<path fill="#050A14"
d="M188.4 71.7a10.4 10.4 0 01-20.8 0 10.4 10.4 0 1120.8 0zM224.2 45c-2.2-3.9-5-7.5-8.2-10.7l-12 7c-3.7-3.2-8-5.7-12.6-7.3a49.4 49.4 0 00-9.7 13.9 59 59 0 0140.1 14l7.6-4.4a57 57 0 00-5.2-12.5zM178 125.1c4.5 0 9-.6 13.4-1.7v-14a40 40 0 0012.5-7.2 47.7 47.7 0 00-7.1-15.3 59 59 0 01-32.2 27.7v8.7c4.4 1.2 8.9 1.8 13.4 1.8zM131.8 45c-2.3 4-4 8.1-5.2 12.5l12 7a40 40 0 000 14.4c5.7 1.5 11.3 2 16.9 1.5a59 59 0 01-8-41.7l-7.5-4.3c-3.2 3.2-6 6.7-8.2 10.6z" />
<path fill="#00B4FF"
d="M224.2 98.4c2.3-3.9 4-8 5.2-12.4l-12-7a40 40 0 000-14.5c-5.7-1.5-11.3-2-16.9-1.5a59 59 0 018 41.7l7.5 4.4c3.2-3.2 6-6.8 8.2-10.7zm-92.4 0c2.2 4 5 7.5 8.2 10.7l12-7a40 40 0 0012.6 7.3c4-4.1 7.3-8.8 9.7-13.8a59 59 0 01-40-14l-7.7 4.4c1.2 4.3 3 8.5 5.2 12.4zm46.2-80c-4.5 0-9 .5-13.4 1.7V34a40 40 0 00-12.5 7.2c1.5 5.7 4 10.8 7.1 15.4a59 59 0 0132.2-27.7V20a53.3 53.3 0 00-13.4-1.8z" />
<path fill="#00B4FF"
d="M178 9.2a62.6 62.6 0 11-.1 125.2A62.6 62.6 0 01178 9.2m0-9.2a71.7 71.7 0 100 143.5A71.7 71.7 0 00178 0z" />
<path fill="#050A14"
d="M96.6 212v4.3c-9.2-.8-15.4-5.8-15.4-17.8V180h4.6v18.4c0 8.6 4 12.6 10.8 13.5zm16-31.9v18.4c0 8.9-4.3 12.8-10.9 13.5v4.4c9.2-.7 15.5-5.6 15.5-18v-18.3h-4.7zM62.2 199v-2.2c0-12.7-8.8-17.4-21-17.4-12.1 0-20.7 4.7-20.7 17.4v2.2c0 12.8 8.6 17.6 20.7 17.6 1.5 0 3-.1 4.4-.3l11.8 6.2 2-3.3-8.2-4-6.4-3.1a32 32 0 01-3.6.2c-9.8 0-16-3.9-16-13.3v-2.2c0-9.3 6.2-13.1 16-13.1 9.9 0 16.3 3.8 16.3 13.1v2.2c0 5.3-2.1 8.7-5.6 10.8l4.8 2.4c3.4-2.8 5.5-7 5.5-13.2zM168 215.6h5.1L156 179.7h-4.8l17 36zM143 205l7.4-15.7-2.4-5-15.1 31.4h5.1l3.3-7h18.3l-1.8-3.7H143zm133.7 10.7h5.2l-17.3-35.9h-4.8l17 36zm-25-10.7l7.4-15.7-2.4-5-15.1 31.4h5.1l3.3-7h18.3l-1.7-3.7h-14.8zm73.8-2.5c6-1.2 9-5.4 9-11.4 0-8-4.5-10.9-12.9-10.9h-21.4v35.5h4.6v-31.3h16.5c5 0 8.5 1.4 8.5 6.7 0 5.2-3.5 7.7-8.5 7.7h-11.4v4.1h10.7l9.3 12.8h5.5l-9.9-13.2zm-117.4 9.9c-9.7 0-14.7-2.5-18.6-6.3l-2.2 3.8c5.1 5 11 6.7 21 6.7 1.6 0 3.1-.1 4.6-.3l-1.9-4h-3zm18.4-7c0-6.4-4.7-8.6-13.8-9.4l-10.1-1c-6.7-.7-9.3-2.2-9.3-5.6 0-2.5 1.4-4 4.6-5l-1.8-3.8c-4.7 1.4-7.5 4.2-7.5 8.9 0 5.2 3.4 8.7 13 9.6l11.3 1.2c6.4.6 8.9 2 8.9 5.4 0 2.7-2.1 4.7-6 5.8l1.8 3.9c5.3-1.6 8.9-4.7 8.9-10zm-20.3-21.9c7.9 0 13.3 1.8 18.1 5.7l1.8-3.9a30 30 0 00-19.6-5.9c-2 0-4 .1-5.7.3l1.9 4 3.5-.2z" />
<path fill="#00B4FF"
d="M.5 251.9c29.6-.5 59.2-.8 88.8-1l88.7-.3 88.7.3 44.4.4 44.4.6-44.4.6-44.4.4-88.7.3-88.7-.3a7981 7981 0 01-88.8-1z" />
<path fill="none" d="M-565.2 324H-252v15.8h-313.2z" />
</svg>

After

Width:  |  Height:  |  Size: 4.4 KiB

0
src/boot/.gitkeep Normal file
View File

90
src/boot/axios.js Normal file
View File

@ -0,0 +1,90 @@
import { boot } from "quasar/wrappers";
import axios from "axios";
import { LocalStorage, SessionStorage } from "quasar";
import router from "src/router";
import { Notify } from "quasar";
// Be careful when using SSR for cross-request state pollution
// due to creating a Singleton instance here;
// If any client changes this (global) instance, it might be a
// good idea to move this instance creation inside of the
// "export default () => {}" function below (which runs individually
// for each client)
const api = axios.create({
baseURL: "http://192.168.1.244:8085/", //接口统一域名
timeout: 60 * 60 * 10000,
authToken: true,
responseType: "json", // 请求数据格式
});
export default boot(({ app, router, ssrContext }) => {
// for use inside Vue files (Options API) through this.$axios and this.$api
api.interceptors.request.use(
(config) => {
// set header Content-Type
if (config.method === "get") {
// || config.method === "delete"
config.headers["Content-Type"] = "application/x-www-form-urlencoded";
} else {
config.headers["Content-Type"] = "application/json";
}
if (config.isFormData) {
config.headers["Content-Type"] = "multipart/form-data";
}
config.headers.Authorization = LocalStorage.getItem("sd-token") || "";
console.log(config);
return config;
},
(error) => {
return Promise.reject(error);
}
);
api.interceptors.response.use(
(response) => {
// 对响应数据做些事
let isResourse = response.config.responseType;
if (response && response.data.status === 401) {
Notify.create({
message: "登录过期,请重新登录",
type: "negative",
position: "top",
});
// 删除Localstorage
LocalStorage.remove("sd-token");
LocalStorage.remove("sd-accountInfo");
router.push("/login");
}
if (
response.status === 200 ||
response.status === 201 ||
response.status === 204
) {
return isResourse == "blob" ? response : response.data;
} else {
Notify.create({
message: "系统繁忙,请稍后",
type: "negative",
position: "top",
});
Promise.reject(response);
}
},
(error) => {
Notify.create({
message: "系统繁忙,请稍后",
type: "negative",
position: "top",
});
// 响应错误时做些事
return Promise.reject(error);
}
);
app.config.globalProperties.$axios = axios;
// ^ ^ ^ this will allow you to use this.$axios (for Vue Options API form)
// so you won't necessarily have to import axios in each vue file
app.config.globalProperties.$api = api;
// ^ ^ ^ this will allow you to use this.$api (for Vue Options API form)
// so you can easily perform requests against your app's API
});
export { api };

14
src/boot/i18n.js Normal file
View File

@ -0,0 +1,14 @@
import { boot } from 'quasar/wrappers'
import { createI18n } from 'vue-i18n'
import messages from 'src/i18n'
export default boot(({ app }) => {
const i18n = createI18n({
locale: 'zh-CN',
globalInjection: true,
messages
})
// Set i18n instance on app
app.use(i18n)
})

12
src/boot/lazyLoad.js Normal file
View File

@ -0,0 +1,12 @@
import { boot } from 'quasar/wrappers'
import Lazyload from "vue3-lazyload";
export default boot(({ app }) => {
// Set i18n instance on app
app.use(Lazyload, {
loading: "@/assets/images/default.png",//可以指定加载中的图像
error: "@/assets/images/err.png",//可以指定加载失败的图像
});
})

53
src/boot/naiveUI.js Normal file
View File

@ -0,0 +1,53 @@
import { boot } from "quasar/wrappers";
import {
// create naive ui
create,
NAvatar,
// component
NButton,
NCard,
NConfigProvider,
NDataTable,
NDatePicker,
NFlex,
NGrid,
NGridItem,
NInput,
NInputNumber,
NMenu,
NMessageProvider,
NModal,
NPagination,
NPopover,
NSelect,
NSpin,
NUpload,
NUploadDragger,
} from "naive-ui";
export default boot(({ app }) => {
const naive = create({
components: [
NButton,
NInput,
NInputNumber,
NSelect,
NSpin,
NMessageProvider,
NPopover,
NUpload,
NUploadDragger,
NModal,
NCard,
NGrid,
NGridItem,
NDatePicker,
NConfigProvider,
NMenu,
NAvatar,
NDataTable,
NPagination
],
});
app.use(naive);
});

View File

@ -0,0 +1,48 @@
<template>
<q-item
clickable
tag="a"
target="_blank"
:href="props.link"
>
<q-item-section
v-if="props.icon"
avatar
>
<q-icon :name="props.icon" />
</q-item-section>
<q-item-section>
<q-item-label>{{ props.title }}</q-item-label>
<q-item-label caption>{{ props.caption }}</q-item-label>
</q-item-section>
</q-item>
</template>
<script setup>
defineOptions({
name: 'EssentialLink'
})
const props = defineProps({
title: {
type: String,
required: true
},
caption: {
type: String,
default: ''
},
link: {
type: String,
default: '#'
},
icon: {
type: String,
default: ''
}
})
</script>

63
src/css/app.scss Normal file
View File

@ -0,0 +1,63 @@
.q-notification {
background: linear-gradient(260deg, #c448b8, #591df5) !important;
}
.n-popover-arrow {
background: #383838 !important;
}
.n-popover {
background: #383838 !important;
color: #fff !important;
font-weight: bold !important;
}
.n-modal {
height: 820px;
background: #212121 !important;
}
.n-card > .n-card-header .n-card-header__main {
color: #fff !important;
font-weight: bold !important;
text-align: center !important;
}
.n-card-header {
border-bottom: 1px solid !important;
margin-left: 40px;
margin-right: 40px;
}
.n-date-panel {
background: #19191a !important;
}
.n-date-panel-date {
color: #fff !important;
}
.n-date-panel-weekdays__day {
color: #b9b9b9 !important;
}
.n-date-panel
.n-date-panel-dates
.n-date-panel-date.n-date-panel-date--covered:not(
.n-date-panel-date--excluded
)::before {
background-color: rgba(195, 72, 184, 0.2) !important;
}
.n-date-panel
.n-date-panel-dates
.n-date-panel-date.n-date-panel-date--selected::after {
background-color: rgba(195, 72, 184, 1);
}
.v-vl-items {
background: #19191a !important;
}
.n-base-select-option__content {
color: #fff !important;
}
.n-select-menu {
--n-option-color-pending: gray !important;
--n-option-color-active: gray !important;
--n-option-color-active-pending: gray !important;
}
.n-input {
--n-color-focus: #383838 !important;
}
.n-base-selection-label {
--n-color-active: #383838 !important;
}

View File

@ -0,0 +1,26 @@
// Quasar SCSS (& Sass) Variables
// --------------------------------------------------
// To customize the look and feel of this app, you can override
// the Sass/SCSS variables found in Quasar's source Sass/SCSS files.
// Check documentation for full list of Quasar variables
// Your own variables (that are declared here) and Quasar's own
// ones will be available out of the box in your .vue/.scss/.sass files
// It's highly recommended to change the default colors
// to match your app's branding.
// Tip: Use the "Theme Builder" on Quasar's documentation website.
$primary : #1976D2;
$secondary : #26A69A;
$accent : #9C27B0;
$dark : #1D1D1D;
$dark-page : #121212;
$positive : #21BA45;
$negative : #C10015;
$info : #31CCEC;
$warning : #F2C037;

7
src/i18n/en-US/index.js Normal file
View File

@ -0,0 +1,7 @@
// This is just an example,
// so you can safely delete all default props below
export default {
failed: 'Action failed',
success: 'Action was successful'
}

5
src/i18n/index.js Normal file
View File

@ -0,0 +1,5 @@
import enUS from './en-US'
export default {
'en-US': enUS
}

226
src/layouts/MainLayout.vue Normal file
View File

@ -0,0 +1,226 @@
<template>
<q-layout view="lhh LpR lff" class="bg-grey-1">
<q-header :reveal="true" class="text-white" style="background: #212121">
<q-toolbar class="q-py-sm q-px-md">
<img class="logo" src="../assets/image/logo.png" />
<div
class="GL__toolbar-link q-ml-xs q-gutter-md text-body2 text-weight-bold row items-center no-wrap"
>
<q-tabs inline-label class="q-pl-xl q-pr-xl aiFonts" align="justify">
<q-route-tab to="/" label="AI生图" />
<q-route-tab to="/history" name="history" label="生成历史" />
<q-route-tab to="/aicg" name="acg" label="AIGC认证" />
</q-tabs>
</div>
<q-space />
<div class="q-pl-sm q-gutter-sm row items-center no-wrap">
<div
class="q-pl-sm q-gutter-sm row items-center no-wrap cursor-pointer"
>
<img
style="width: 17px; height: auto"
src="../assets/image/ai/message.png"
/>
<span @click="goFeedBack" class="msg">留言反馈</span>
</div>
<q-btn dense flat no-wrap>
<q-avatar size="40px">
<img :src="user.avatar" />
</q-avatar>
<q-icon name="arrow_drop_down" size="16px" />
<q-menu auto-close>
<div class="pop-box">
<div class="userInfo">
<img :src="user.avatar" />
<div style="margin-bottom: 10px">
<div class="user-name">{{ user.nickName }}</div>
<div class="phone">{{ user.telNum }}</div>
</div>
</div>
<n-button class="create" quaternary>
<div style="color: #fff">
剩余积分<span style="margin-left: 30px">{{
user.coin
}}</span>
</div>
</n-button>
<div class="list" @click="goDetail">
<div style="display: flex; align-items: center">
<img src="../assets/image/coin.png" alt="设置" />
<span style="margin-left: 5px">积分明细</span>
</div>
<span> > </span>
</div>
<div class="list" @click="goMine">
<div style="display: flex; align-items: center">
<img src="../assets/image/per.png" alt="设置" />
<span style="margin-left: 5px">个人中心</span>
</div>
<span> > </span>
</div>
<div class="log-out" @click="logOut">
<img src="../assets/image/exit.png" alt="设置" />
<span style="margin-left: 5px">退出登录</span>
</div>
</div>
</q-menu>
</q-btn>
</div>
</q-toolbar>
</q-header>
<q-page-container>
<router-view />
</q-page-container>
</q-layout>
</template>
<script setup>
import { LocalStorage, SessionStorage } from "quasar";
import { ref } from "vue";
import { useRoute, useRouter } from "vue-router";
const tab = ref("ai");
const router = useRouter();
const user = LocalStorage.getItem("sd-accountInfo") || {};
const logOut = () => {
LocalStorage.remove("sd-accountInfo");
LocalStorage.remove("sd-token");
window.location.href = "/#/login";
};
const goMine = () => {
router.push("/mine?active=mineCenter");
};
const goDetail = () => {
router.push("/mine?active=pointDetail");
};
const goFeedBack = () => {
router.push("/feedback");
};
</script>
<style scoped lang="scss">
@font-face {
font-family: "aiFonts";
src: url("../assets/fonts/aiFonts.ttf") format("truetype");
font-weight: lighter !important;
font-style: normal;
}
.aiFonts {
font-family: "aiFonts";
}
.logo {
height: 40px;
width: auto;
margin-right: 10px;
}
.q-tab {
width: 200px;
}
.msg {
font-size: 14px;
margin-left: 5px;
margin-bottom: 2px;
}
.pop-box {
width: 310px;
padding: 0px 27px 15px 27px;
height: 330px;
background: #19191a;
.userInfo {
display: flex;
flex-direction: row;
align-items: center;
padding-top: 20px;
font-weight: bold;
img {
width: 60px;
height: 60px;
border-radius: 50%;
margin-right: 10px;
}
.user-name {
font-size: 18px;
color: #fff;
margin-top: 10px;
}
.phone {
font-size: 16px;
color: #bebebe;
margin-top: 5px;
}
}
.list {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
width: 100%;
margin-top: 20px;
cursor: pointer;
img {
width: 20px;
height: 20px;
}
span {
font-size: 16px;
color: #bebebe;
margin-left: 10px;
}
}
.log-out {
display: flex;
flex-direction: row;
align-items: center;
padding-top: 20px;
border-top: 1px solid #707070;
justify-content: center;
margin-top: 20px;
cursor: pointer;
img {
width: 20px;
height: 20px;
}
span {
font-size: 16px;
color: #C348B8;
margin-left: 10px;
}
}
}
.create {
margin-top: 10px;
width: 100%;
height: 44px;
background: linear-gradient(260deg, #c448b8, #591df5);
border-radius: 6px;
color: #fff;
font-weight: bolder;
font-size: 18px;
margin-bottom: 5px;
}
:deep(.q-tab--active .q-tab__indicator) {
opacity: 1;
width: 40px;
left: 39%;
height: 5px;
background: linear-gradient(to right, #be45ba, #5d1ef1);
}
:deep(.q-tab--active .q-tab__label) {
color: #fff;
font-size: 20px;
font-weight: lighter;
text-shadow: 0px 3px 4px #de53ab;
}
:deep(.q-tab--inactive .q-tab__label) {
color: #bebebe;
font-size: 20px;
font-weight: lighter;
}
:deep(.q-menu) {
left: 1586px;
}
</style>

View File

@ -0,0 +1,29 @@
<template>
<div class="fullscreen bg-blue text-white text-center q-pa-md flex flex-center">
<div>
<div style="font-size: 30vh">
404
</div>
<div class="text-h2" style="opacity:.4">
Oops. Nothing here...
</div>
<q-btn
class="q-mt-xl"
color="white"
text-color="blue"
unelevated
to="/"
label="Go Home"
no-caps
/>
</div>
</div>
</template>
<script setup>
defineOptions({
name: 'ErrorNotFound'
});
</script>

21
src/pages/IndexPage.vue Normal file
View File

@ -0,0 +1,21 @@
<template>
<q-page class="flex flex-center">
<h5>Click emojis to remove them.</h5>
<ul v-auto-animate>
<li v-for="item in items" :key="item" @click="removeItem(item)">
{{ item }}
</li>
</ul>
</q-page>
</template>
<script setup>
import { ref } from 'vue'
const items = ref(["😏","😐","😑","😒","😕" ])
function removeItem(toRemove) {
items.value = items.value.filter((item) => item !== toRemove)
}
defineOptions({
name: 'IndexPage'
});
</script>

958
src/pages/Login.vue Normal file
View File

@ -0,0 +1,958 @@
<template>
<div class="fullscreen">
<div class="fullscreen out-box">
<div class="bg">
<div
class="carousel"
:id="'silder' + (index + 1)"
v-for="(item, index) in 7"
>
<div class="carousel-track-container">
<ul class="carousel-track" :id="'silder-track' + (index + 1)">
<li class="carousel-slide">
<img src="../assets/image/train.png" />
</li>
<li class="carousel-slide">
<img src="../assets/image/glass.png" />
</li>
<li class="carousel-slide">
<img src="../assets/image/ast.png" />
</li>
<li class="carousel-slide">
<img src="../assets/image/oldman.png" />
</li>
<li class="carousel-slide">
<img src="../assets/image/patato.png" />
</li>
<li class="carousel-slide">
<img src="../assets/image/henshin.png" />
</li>
</ul>
</div>
</div>
</div>
</div>
<div :class="{ 're-item': true, 'is-flipped': isFlipped }">
<div class="login-box">
<img class="title" src="../assets/image/login/title.png" />
<div class="sub-title">登录得积分畅享免费素材</div>
<q-tabs
v-model="tab"
inline-label
class="bg-black q-pl-xl q-pr-xl"
align="justify"
>
<q-tab name="acaount" label="账号登录" />
<q-tab name="code" label="验证码登录" />
</q-tabs>
<div>
<div
v-show="isErr"
style="display: flex; align-items: center; justify-content: center"
>
<img
style="width: 22px; height: 18px; margin-right: 5px"
src="../assets/image/login/warning.png"
/>
<span style="color: #e0378c">{{ errMsg }}</span>
</div>
<div v-if="tab === 'acaount'">
<div class="account-box">
<span class="account">账号</span>
<q-input
class="input-box"
v-model="telNum"
:dense="true"
placeholder="请输入账号"
/>
</div>
<div class="account-box">
<span class="account">密码</span>
<q-input
:type="isPwd ? 'password' : 'text'"
maxlength="8"
class="input-box"
v-model="password"
:dense="true"
placeholder="请输入密码"
>
<template v-slot:append>
<q-icon
color="grey-13"
:name="isPwd ? 'visibility_off' : 'visibility'"
class="cursor-pointer"
@click="isPwd = !isPwd"
/>
</template>
</q-input>
</div>
<div class="forget" @click="forgetPwd">忘记密码?</div>
</div>
<div v-if="tab === 'code'">
<div class="account-box">
<span class="account" style="transform: translateX(-100%)"
>手机号</span
>
<q-input
class="input-box"
v-model="telNum"
:dense="true"
placeholder="请输入账号"
/>
</div>
<div class="account-box">
<span class="account" style="transform: translateX(-100%)"
>验证码</span
>
<q-input
style="width: 250px"
:type="isPwd ? 'password' : 'text'"
class="input-box"
v-model="code"
:dense="true"
placeholder="请输入账号"
>
<template v-slot:append>
<q-icon
color="grey-13"
:name="isPwd ? 'visibility_off' : 'visibility'"
class="cursor-pointer"
@click="isPwd = !isPwd"
/>
</template>
</q-input>
<q-btn
class="code-btn"
@click="getLoginCode"
:loading="codeLoading"
:disabled="loginRestSec < 60"
>
<div v-if="loginRestSec === 60 || loginRestSec < 1">
获取验证码
</div>
<div v-else>{{ loginRestSec }}</div>
</q-btn>
</div>
<div class="forget"></div>
</div>
</div>
<div class="read">
<q-checkbox v-model="readed" />
<a>我已阅读并同意</a>
<span style="color: #c348b8"> 用户协议</span>
<span style="color: #c348b8">隐私政策</span>
</div>
<q-btn
label="登录"
class="login-btn"
:loading="loading"
text-color="white"
@click="login"
/>
<div class="regist">
<a></a>
<span style="color: #c348b8" @click="toggleFlip">注册</span>
</div>
</div>
<div class="regist-box">
<div v-if="registType === 1">
<img class="title" src="../assets/image/login/regist.png" />
<div>
<div
v-show="isErr"
style="
display: flex;
align-items: center;
justify-content: center;
"
>
<img
style="width: 22px; height: 18px; margin-right: 5px"
src="../assets/image/login/warning.png"
/>
<span style="color: #e0378c">{{ errMsg }}</span>
</div>
<div>
<div class="account-box">
<span class="account" style="transform: translateX(-100%)"
>手机号</span
>
<q-input
class="input-box"
v-model="telNum"
:dense="true"
placeholder="请输入账号"
/>
</div>
<div class="account-box">
<span class="account" style="transform: translateX(-100%)"
>验证码</span
>
<q-input
style="width: 238px"
:type="isPwd ? 'password' : 'text'"
class="input-box"
v-model="code"
:dense="true"
placeholder="请输入验证码"
>
<template v-slot:append>
<q-icon
color="grey-13"
:name="isPwd ? 'visibility_off' : 'visibility'"
class="cursor-pointer"
@click="isPwd = !isPwd"
/>
</template>
</q-input>
<q-btn
class="code-btn"
:loading="codeLoading"
:disabled="restSec < 60"
@click="getCode"
style="margin-right: 10px"
>
<div v-if="restSec === 60 || restSec < 1">获取验证码</div>
<div v-else>{{ restSec }}</div>
</q-btn>
</div>
<div class="account-box">
<span class="account" style="transform: translateX(-160%)"
>密码</span
>
<q-input
:type="isPwd ? 'password' : 'text'"
class="input-box"
v-model="password"
:dense="true"
placeholder="请输入密码"
maxlength="8"
style="margin-left: 10px"
>
<template v-slot:append>
<q-icon
color="grey-13"
:name="isPwd ? 'visibility_off' : 'visibility'"
class="cursor-pointer"
@click="isPwd = !isPwd"
/>
</template>
</q-input>
</div>
<div class="account-box">
<span class="account" style="transform: translateX(-45%)"
>确认密码</span
>
<q-input
:type="isPwd ? 'password' : 'text'"
class="input-box"
v-model="verfyPassword"
:dense="true"
placeholder="请输入密码"
maxlength="8"
style="margin-right: 18px"
>
<template v-slot:append>
<q-icon
color="grey-13"
:name="isPwd ? 'visibility_off' : 'visibility'"
class="cursor-pointer"
@click="isPwd = !isPwd"
/>
</template>
</q-input>
</div>
</div>
</div>
<q-btn
:loading="loading"
label="确定"
class="login-btn"
text-color="white"
@click="goRegist"
style="margin-top: 40px"
/>
<div class="regist">
<a></a>
<span style="color: #c348b8" @click="toggleFlip">登录</span>
</div>
</div>
<div
v-if="registType === 2"
style="
display: flex;
align-items: center;
flex-direction: column;
"
>
<img
style="width: 170px; height: 60px; margin-top: 20px"
src="../assets/image/login/success.png"
/>
<img
style="width: 200px; height: 200px; margin-top: 80px"
src="../assets/image/login/succicon.png"
/>
<q-btn
label="返回登录页"
class="login-btn"
text-color="white"
@click="backLogin"
style="margin-top: 80px"
/>
</div>
<div v-if="registType === 3">
<img
class="title"
style="width: 140px"
src="../assets/image/login/forgetpwd.png"
/>
<div>
<div
v-show="isErr"
style="
display: flex;
align-items: center;
justify-content: center;
"
>
<img
style="width: 22px; height: 18px; margin-right: 5px"
src="../assets/image/login/warning.png"
/>
<span style="color: #e0378c">{{ errMsg }}</span>
</div>
<div>
<div class="account-box">
<span class="account" style="transform: translateX(-100%)"
>手机号</span
>
<q-input
class="input-box"
v-model="telNum"
:dense="true"
placeholder="请输入账号"
/>
</div>
<div class="account-box">
<span class="account" style="transform: translateX(-100%)"
>验证码</span
>
<q-input
style="width: 232px"
:type="isPwd ? 'password' : 'text'"
class="input-box"
v-model="code"
:dense="true"
placeholder="请输入验证码"
>
<template v-slot:append>
<q-icon
color="grey-13"
:name="isPwd ? 'visibility_off' : 'visibility'"
class="cursor-pointer"
@click="isPwd = !isPwd"
/>
</template>
</q-input>
<q-btn
class="code-btn"
:loading="codeLoading"
:disabled="restSec < 60"
@click="getRestCode"
style="margin-right: 10px"
>
<div v-if="restSec === 60 || restSec < 1">获取验证码</div>
<div v-else>{{ restSec }}</div>
</q-btn>
</div>
<div class="account-box">
<span class="account" style="transform: translateX(-90%)"
>新密码</span
>
<q-input
:type="isPwd ? 'password' : 'text'"
class="input-box"
v-model="password"
:dense="true"
placeholder="请输入密码"
style="margin-left: 5px"
maxlength="8"
>
<template v-slot:append>
<q-icon
color="grey-13"
:name="isPwd ? 'visibility_off' : 'visibility'"
class="cursor-pointer"
@click="isPwd = !isPwd"
/>
</template>
</q-input>
</div>
<div class="account-box">
<span class="account" style="transform: translateX(-20%)"
>确认新密码</span
>
<q-input
:type="isPwd ? 'password' : 'text'"
class="input-box"
v-model="verfyPassword"
:dense="true"
placeholder="请输入密码"
style="margin-right: 25px"
maxlength="8"
>
<template v-slot:append>
<q-icon
color="grey-13"
:name="isPwd ? 'visibility_off' : 'visibility'"
class="cursor-pointer"
@click="isPwd = !isPwd"
/>
</template>
</q-input>
</div>
</div>
</div>
<q-btn
:loading="loading"
label="确定"
class="login-btn"
text-color="white"
@click="goResetPwd"
style="margin-top: 40px"
/>
<div class="regist">
<a></a>
<span style="color: #c348b8" @click="toggleFlip">登录</span>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from "vue";
import { LocalStorage, SessionStorage } from "quasar";
import { processError, processSuccess } from "../utils/message";
import { useRoute, useRouter } from "vue-router";
import { UserApi } from "src/api";
const router = useRouter();
const tab = ref("acaount");
const telNum = ref("");
const password = ref("");
const verfyPassword = ref("");
const loading = ref(false);
const codeLoading = ref(false);
const code = ref("");
const restSec = ref(60);
const loginRestSec = ref(60);
const isErr = ref(false);
const errMsg = ref("");
const isPwd = ref(true);
const readed = ref(false);
const registType = ref(1);
const isFlipped = ref(false);
//
function startScrolling(carousel, track) {
const slides = Array.from(track.children);
slides.forEach((slide) => {
track.appendChild(slide.cloneNode(true));
});
let scrollInterval;
const scrollSpeed = 1;
scrollInterval = setInterval(() => {
if (carousel.scrollTop >= track.scrollHeight / 2) {
carousel.scrollTop = 0;
} else {
carousel.scrollTop += scrollSpeed;
}
}, 20);
}
//
const login = async () => {
//
if (!readed.value) {
errMsg.value = "请先同意用户协议";
isErr.value = true;
return;
}
//
if (tab.value === "acaount" && (!telNum.value || !password.value)) {
errMsg.value = "账号或密码不能为空";
isErr.value = true;
return;
}
//
if (tab.value === "code" && (!telNum.value || !password.value)) {
errMsg.value = "手机号或验证码不能为空";
isErr.value = true;
return;
}
isErr.value = false;
errMsg.value = "";
loading.value = true;
const data = {
telNum: telNum.value,
};
if (tab.value === "acaount") {
data.password = password.value;
} else {
data.code = code.value;
}
try {
const res = await UserApi.login(data).then((res) => {
if (res.status === 0) {
loading.value = false;
LocalStorage.set("sd-token", res.data.token);
LocalStorage.set("sd-accountInfo", res.data.accountInfo);
router.push("/");
} else {
loading.value = false;
processError(res.msg);
errMsg.value = res.msg;
isErr.value = true;
}
});
} catch (error) {
loading.value = false;
console.error(error);
}
};
//
const getLoginCode = async () => {
if (!telNum.value) {
errMsg.value = "手机号不能为空";
isErr.value = true;
return;
}
isErr.value = false;
errMsg.value = "";
codeLoading.value = true;
const data = {
telNum: telNum.value,
};
try {
const res = await UserApi.getLoginCode(data).then((res) => {
if (res.status === 0) {
codeLoading.value = false;
let countdown = 60;
const countdownTimer = setInterval(() => {
countdown--;
loginRestSec.value = countdown;
if (countdown === 0) {
clearInterval(countdownTimer);
loginRestSec.value = 60;
}
}, 1000);
} else {
codeLoading.value = false;
errMsg.value = res.msg;
isErr.value = true;
}
});
} catch (error) {
codeLoading.value = false;
console.error(error);
}
};
//
const getCode = async () => {
if (!telNum.value) {
errMsg.value = "手机号不能为空";
isErr.value = true;
return;
}
isErr.value = false;
errMsg.value = "";
codeLoading.value = true;
const data = {
telNum: telNum.value,
};
try {
await UserApi.getRegisterCode(data).then((res) => {
if (res.status === 0) {
codeLoading.value = false;
let countdown = 60;
const countdownTimer = setInterval(() => {
countdown--;
restSec.value = countdown;
if (countdown === 0) {
clearInterval(countdownTimer);
restSec.value = 60;
}
}, 1000);
} else {
codeLoading.value = false;
errMsg.value = res.msg;
isErr.value = true;
}
});
} catch (error) {
codeLoading.value = false;
console.error(error);
}
};
//
const getRestCode = async () => {
if (!telNum.value) {
errMsg.value = "手机号不能为空";
isErr.value = true;
return;
}
isErr.value = false;
errMsg.value = "";
codeLoading.value = true;
const data = {
telNum: telNum.value,
};
try {
await UserApi.getForgetPwdCode(data).then((res) => {
console.log(res);
if (res.status === 0) {
codeLoading.value = false;
let countdown = 60;
const countdownTimer = setInterval(() => {
countdown--;
restSec.value = countdown;
if (countdown === 0) {
clearInterval(countdownTimer);
restSec.value = 60;
}
}, 1000);
} else {
codeLoading.value = false;
errMsg.value = res.msg;
isErr.value = true;
}
});
} catch (error) {
codeLoading.value = false;
console.error(error);
}
};
//
const goResetPwd = async () => {
if (!telNum.value || !code.value || !password.value || !verfyPassword.value) {
errMsg.value = "请填写完整信息";
isErr.value = true;
return;
}
if (password.value !== verfyPassword.value) {
errMsg.value = "两次密码不一致";
isErr.value = true;
return;
}
//
if (password.value.indexOf(" ") !== -1) {
errMsg.value = "密码不允许有空格";
isErr.value = true;
return;
}
isErr.value = false;
errMsg.value = "";
loading.value = true;
const data = {
telNum: telNum.value,
code: code.value,
password: password.value,
};
try {
const res = await UserApi.resetPassword(data).then((res) => {
if (res.status === 0) {
loading.value = false;
code.value = "";
registType.value = 2;
} else {
loading.value = false;
errMsg.value = res.msg;
isErr.value = true;
}
});
} catch (error) {
loading.value = false;
console.error(error);
}
};
function toggleFlip() {
isFlipped.value = !isFlipped.value;
code.value = "";
registType.value = 1;
}
const forgetPwd = () => {
isFlipped.value = true;
password.value = "";
registType.value = 3;
};
//
const goRegist = async () => {
if (!telNum.value || !code.value || !password.value || !verfyPassword.value) {
errMsg.value = "请填写完整信息";
isErr.value = true;
return;
}
if (password.value !== verfyPassword.value) {
errMsg.value = "两次密码不一致";
isErr.value = true;
return;
}
//
if (password.value.indexOf(" ") !== -1) {
errMsg.value = "密码不允许有空格";
isErr.value = true;
return;
}
isErr.value = false;
errMsg.value = "";
loading.value = true;
const data = {
telNum: telNum.value,
code: code.value,
password: password.value,
};
try {
const res = await UserApi.register(data).then((res) => {
if (res.status === 0) {
loading.value = false;
code.value = "";
registType.value = 2;
} else {
loading.value = false;
errMsg.value = res.msg;
isErr.value = true;
}
});
} catch (error) {
loading.value = false;
console.error(error);
}
};
//
const backLogin = () => {
isFlipped.value = false;
};
onMounted(() => {
const carousels = document.querySelectorAll(".carousel");
carousels.forEach((carousel) => {
const track = carousel.querySelector(".carousel-track");
startScrolling(carousel, track);
});
});
</script>
<style scoped lang="scss">
.out-box {
z-index: 1;
}
.carousel {
overflow: hidden;
height: 100%;
background: #000000;
}
.bg {
display: flex;
transform: rotate(30deg);
height: 197%;
position: relative;
top: -437px;
width: 113%;
left: -133px;
}
/* 创建一个伪元素作为黑色透明蒙版 */
.bg::before {
content: ""; /* 伪元素内容为空 */
display: block; /* 或者使用 inline-block根据需要 */
position: absolute; /* 相对于容器进行绝对定位 */
top: 0;
left: 0;
width: 100%; /* 蒙版覆盖整个容器宽度 */
height: 100%; /* 蒙版覆盖整个容器高度 */
background-color: rgba(0, 0, 0, 0.7); /* 设置黑色背景且透明度为0.7 */
z-index: 1; /* 设置层级确保蒙版出现在内容之上(根据需要调整) */
}
.carousel-track {
list-style: none;
padding: 0;
margin: 0;
position: relative;
}
.carousel-slide {
margin: 10px 5px 10px 5px;
}
.carousel-slide img {
width: 100%;
display: block;
-webkit-user-drag: none;
-moz-user-drag: none;
}
.login-box {
backface-visibility: hidden;
position: absolute;
width: 911px;
height: 591px;
background: #000000;
border-radius: 8px;
padding: 20px;
box-sizing: border-box;
text-align: center;
z-index: 99;
.title {
width: 180px;
height: 67px;
}
.sub-title {
font-size: 20px;
color: #c348b8;
font-weight: bolder;
margin-bottom: 20px;
}
}
.account-box {
display: flex;
justify-content: center;
align-items: center;
font-size: 16px;
margin-top: 30px;
.account {
color: #bebebe;
transform: translateX(-120%);
}
.input-box {
width: 370px;
padding-bottom: 0;
}
}
.forget {
color: #c348b8;
font-size: 16px;
margin-top: 5px;
cursor: pointer;
transform: translateX(19%);
}
.code-btn {
margin-left: 20px;
background: linear-gradient(to right, #591df5, #c448b8);
color: #fff;
font-weight: bold;
border: 8px;
height: 45px;
min-width: 105px;
}
.read {
display: flex;
justify-content: center;
align-items: center;
margin-top: 30px;
color: #bebebe;
font-size: 15px;
span {
cursor: pointer;
}
}
.login-btn {
margin-top: 10px;
background: linear-gradient(to right, #591df5, #c448b8);
width: 436px;
height: 45px;
border-radius: 8px;
}
.regist {
margin-top: 20px;
color: #bebebe;
font-size: 16px;
span {
cursor: pointer;
margin-left: 10px;
}
}
.regist-box {
backface-visibility: hidden;
transform: rotateY(180deg);
position: absolute;
width: 911px;
height: 591px;
background: #000000;
border-radius: 8px;
padding: 20px;
box-sizing: border-box;
text-align: center;
z-index: 99;
.title {
width: 90px;
height: 57px;
}
}
.re-item {
position: absolute;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
z-index: 99;
transition: transform 0.8s;
transform-style: preserve-3d;
perspective: 1000px;
}
.is-flipped {
transform: rotateY(180deg);
}
:deep(.q-tab--active .q-tab__indicator) {
opacity: 1;
width: 50px;
left: 44%;
height: 5px;
background: linear-gradient(to right, #be45ba, #5d1ef1);
}
:deep(.q-tab--active .q-tab__label) {
color: #fff;
font-size: 20px;
font-weight: bold;
text-shadow: 0px 3px 4px #de53ab;
}
:deep(.q-tab--inactive .q-tab__label) {
color: #bebebe;
font-size: 20px;
font-weight: bold;
}
:deep(.q-field__inner) {
background: #212121;
padding: 0 10px 0 10px;
border-radius: 8px;
border: 2px solid transparent;
.q-field__native {
color: #7e7e7e;
}
}
:deep(.q-field__inner::before) {
content: "";
position: absolute;
top: 0;
right: 0;
left: 0;
bottom: 0;
z-index: -1;
margin: -4px;
border-radius: inherit; /*important*/
background: linear-gradient(270deg, #e44ea8 0%, #5f33f3 49%, #61d2ef 100%);
}
:deep(.q-field--standard .q-field__control:after) {
display: none;
}
:deep(.q-checkbox__svg) {
color: #c348b8;
}
:deep(.q-checkbox__bg) {
background: none;
border: 2px solid #c348b8;
}
</style>

24
src/pages/aicg/index.vue Normal file
View File

@ -0,0 +1,24 @@
<template>
<q-page>
<div class="main">
<img src="../../assets/image/aicg/box.png" alt="logo" />
</div>
</q-page>
</template>
<script setup>
</script>
<style scoped lang="scss">
.q-page {
background: rgb(25, 25, 26);
display: flex;
align-items: center;
justify-content: center;
background-image: url("../../assets/image/ai/img-bg.png");
background-repeat: no-repeat;
background-position: center;
width: 100%;
}
</style>

View File

@ -0,0 +1,72 @@
<template>
<div class="detail-dialog">
<n-modal
v-model:show="showModal"
@close="closeModal"
:mask-closable="false"
>
<n-card
style="width: 600px;text-align: center;"
title="查看图片"
:bordered="false"
size="huge"
role="dialog"
aria-modal="true"
>
<div class="content-box">
<img :src="props.detailSrc" alt="图片" />
</div>
<n-button @click="closeModal" class="create" quaternary> <div style="color: #fff;">确定</div> </n-button>
</n-card>
</n-modal>
</div>
</template>
<script setup>
import { ref, defineProps, computed } from "vue";
const props = defineProps({
show: {
type: Boolean,
default: false,
},
detailSrc: {
type: String,
default: "",
},
});
const emit = defineEmits("update:show", "close-dialog");
const showModal = computed({
get: () => props.show,
set: (value) => {
emit("update:show", value);
},
});
//
const closeModal = () => {
emit("close-dialog", false);
};
</script>
<style scoped lang="scss">
.content-box {
display: flex;
height: 85%;
justify-content: center;
align-items: center;
img {
max-width: 450px;
max-height: 600px;
}
}
.create {
margin-top: 50px;
width: 260px;
height: 55px;
background: linear-gradient(260deg, #c448b8, #591df5);
border-radius: 6px;
color: #fff;
font-weight: bold;
font-size: 18px;
margin-bottom: 5px;
}
</style>

View File

@ -0,0 +1,186 @@
<template>
<div class="detail-dialog">
<n-modal v-model:show="showModal" @close="closeModal">
<n-card
style="width: 1300px"
title="选择模型"
:bordered="false"
size="huge"
role="dialog"
aria-modal="true"
>
<template #header-extra>
<img
src="../../../assets/image/ai/close.png"
alt="关闭"
@click="closeModal"
style="cursor: pointer"
/>
</template>
<q-scroll-area style="height: 100%">
<n-grid :x-gap="12" :y-gap="14" :cols="4">
<n-grid-item v-for="(item, index) in modelList" :key="index">
<div class="card">
<div class="card2" @click="chooseModal(item)">
<img :src="item.replaceImg" alt="图片" />
<div class="info-box">
<div class="name">{{ item.subTitle }}</div>
<div class="desc">
<span
style="margin-right: 10px; color: #bebebe"
v-for="(i, d) in item.replaceTag"
:key="d"
>{{ i }}</span
>
</div>
<div class="author-info">
<img :src="item.replaceAuthorImg" alt="图片" />
<div class="author">{{ item.replaceAuthor }}</div>
</div>
</div>
</div>
</div>
</n-grid-item>
</n-grid>
</q-scroll-area>
</n-card>
</n-modal>
</div>
</template>
<script setup>
import { ref, defineProps, computed, watch } from "vue";
import { ModelApi } from "src/api";
import { processError, processSuccess } from "../../../utils/message";
const props = defineProps({
show: {
type: Boolean,
default: false,
},
});
const emit = defineEmits("update:show", "handCloseDialog", "selectModal");
const modelList = ref([]);
const showModal = computed({
get: () => props.show,
set: (value) => {
emit("update:show", value);
},
});
//
const closeModal = () => {
emit("handCloseDialog", false);
};
//
const chooseModal = (item) => {
emit("selectModal", item);
closeModal();
processSuccess("选择模型成功");
};
//
const getModelList = async () => {
try {
await ModelApi.getModelList({
page: 1,
pageSize: 100,
}).then((res) => {
if (res.status === 0) {
modelList.value = res.data.list;
} else {
processError(res.msg);
}
});
} catch (error) {
console.error(error);
}
};
watch(
() => props.show,
(val) => {
if (val) {
getModelList();
}
}
);
</script>
<style scoped lang="scss">
.card {
width: 270px;
height: 374px;
background-image: linear-gradient(
163deg,
#61d2ef 0%,
#5f33f3 50%,
#e44ea8 100%
);
border-radius: 20px;
transition: all 0.3s;
}
.card2 {
cursor: pointer;
width: 270px;
padding: 10px;
padding-bottom: 15px;
display: flex;
justify-content: center;
flex-direction: column;
height: 374px;
position: absolute;
background-color: #19191a;
border-radius: 20px;
transition: all 0.2s;
img {
height: 280px;
border-radius: 10px;
}
.info-box {
display: flex;
flex-direction: column;
justify-content: center;
.name {
font-size: 20px;
color: #fff;
margin-top: 5px;
//
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.desc {
font-size: 14px;
color: #fff;
margin-top: 2px;
}
.author-info {
display: flex;
align-items: center;
margin-top: 5px;
img {
width: 30px;
height: 30px;
border-radius: 50%;
}
.author {
font-size: 14px;
color: #7e7e7e;
margin-left: 10px;
}
}
}
}
.card2:hover {
transform: scale(0.98);
border-radius: 20px;
}
.card:hover {
box-shadow: 0px 0px 30px 1px rgba(0, 255, 117, 0.1);
}
:deep(.n-card__content) {
margin-top: 20px;
}
</style>

1329
src/pages/create/index.vue Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,279 @@
<template>
<q-page >
<div class="top-img animate__animated animate__fadeIn">
<img src="../../assets/image/mine-bg.png" alt="mine" />
<div class="txt">
<div style="font-size: 20px">Comment Feedback</div>
<div style="font-size: 60px">留言反馈</div>
</div>
</div>
<div class="info animate__animated animate__fadeIn">
<div class="item">
<div class="txt">
<span class="tx1">姓名</span>
<span class="tx2">
<n-input
style="width: 300px"
v-model:value="name"
type="text"
placeholder="请输入真实姓名"
/></span>
</div>
</div>
<div class="item">
<div class="txt">
<span class="tx1">手机号</span>
<span class="tx2"
><n-input
style="width: 300px"
v-model:value="phone"
type="text"
placeholder="请输入真实手机号,以便我们联系您"
/></span>
</div>
</div>
<div class="item">
<div class="txt" style="height: 200px">
<span class="tx1" style="height: 170px"
>留言内容&nbsp;&nbsp;&nbsp;&nbsp;</span
>
<span class="tx2"
><n-input
style="width: 780px; height: 170px"
type="textarea"
maxlength="200"
v-model:value="content"
show-count
/></span>
</div>
</div>
<n-button @click="submit" class="create" quaternary>
<div style="color: #fff">提交</div>
</n-button>
</div>
<!-- 修改用户名 -->
<n-modal
v-model:show="showModal"
@close="closeModal"
:mask-closable="false"
>
<n-card
style="width: 900px; text-align: center; height: 450px"
:title="'提交成功'"
:bordered="false"
size="huge"
role="dialog"
>
<div
style="
display: flex;
align-items: center;
flex-direction: column;
"
>
<img
style="width: 200px; height: 200px; margin-top: 20px"
src="../../assets/image/login/succicon.png"
/>
</div>
<n-button @click="closeModal" class="create" quaternary>
<div style="color: #fff">确定</div>
</n-button>
</n-card>
</n-modal>
</q-page>
</template>
<script setup>
import { ref, defineProps, computed, onBeforeMount, watch } from "vue";
import { LocalStorage, SessionStorage } from "quasar";
import { processError, processSuccess } from "../../utils/message";
import { useRoute, useRouter } from "vue-router";
import { UserApi } from "src/api";
const router = useRouter();
const showModal = ref(false);
const title = ref("");
const errMsg = ref("");
const name = ref("");
const phone = ref("");
const content = ref("");
const closeModal = () => {
showModal.value = false;
};
//
const submit = async () => {
errMsg.value = "";
const data = {
domain: "fontree",
name: name.value,
phone: phone.value,
content: content.value,
feedbackType: "comment_feedback",
};
await UserApi.submitFeedBack(data).then((res) => {
if (res.status === 0) {
processSuccess("提交成功");
showModal.value = true;
//
name.value = "";
phone.value = "";
content.value = "";
} else {
processError(res.msg);
}
});
};
onBeforeMount(() => {});
</script>
<style scoped lang="scss">
.q-page {
background: #19191a;
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
}
.top-img {
width: 100%;
height: 340px;
position: relative;
img {
width: 100%;
height: 100%;
}
.txt {
position: absolute;
bottom: 0%;
left: 43%;
font-weight: bolder;
color: #fff;
text-align: center;
div {
margin-bottom: 10px;
}
}
}
.create {
margin-top: 40px;
width: 260px;
height: 55px;
background: linear-gradient(260deg, #c448b8, #591df5);
border-radius: 6px;
color: #fff;
font-weight: bolder;
font-size: 18px;
margin-bottom: 5px;
}
.info {
width: 100%;
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
background: linear-gradient(to bottom, #39273c, #000000);
.item {
margin: 26px 0 0 32px;
width: 977px;
min-height: 60px;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 20px;
border-radius: 10px;
background: #212121;
font-size: 18px;
.txt {
display: flex;
justify-content: space-between;
align-items: center;
span {
color: #bebebe;
}
.tx1 {
padding-right: 30px;
display: flex;
align-items: center;
min-width: 120px;
border-right: 1px solid #707070;
}
.tx2 {
flex: 1;
padding-left: 30px;
}
}
span {
color: #c348b8;
cursor: pointer;
}
}
}
.content-box {
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
margin-top: 65px;
color: #bebebe;
}
.input-box {
width: 370px;
margin-left: 35px;
padding-bottom: 0;
}
.account-box {
display: flex;
justify-content: center;
align-items: center;
font-size: 16px;
margin-top: 30px;
.account {
color: #bebebe;
transform: translateX(-120%);
}
.input-box {
width: 370px;
padding-bottom: 0;
}
}
.code-btn {
margin-left: 20px;
background: linear-gradient(to right, #591df5, #c448b8);
color: #fff;
font-weight: bold;
border: 8px;
height: 45px;
min-width: 125px;
}
:deep(.n-input) {
background-color: #212121;
--n-border-focus: #212121 !important;
--n-border-hover: #212121 !important;
--n-caret-color: #212121 !important;
}
:deep(.n-input__suffix) {
display: none;
}
:deep(.n-input .n-input__border) {
border: 0;
}
// placeholder
:deep(.n-input__placeholder) {
color: #7e7e7e;
}
:deep(.n-input__textarea-el) {
color: #7e7e7e;
}
:deep(.n-input__input-el) {
color: #7e7e7e;
}
</style>

View File

@ -0,0 +1,188 @@
<template>
<div class="detail-dialog">
<n-modal
v-model:show="showModal"
@close="closeModal"
:mask-closable="false"
>
<n-card
style="width: 1300px; text-align: center"
title="查看图片"
:bordered="false"
size="huge"
role="dialog"
aria-modal="true"
>
<template #header-extra>
<img
src="../../../assets/image/ai/close.png"
alt="关闭"
@click="closeModal"
style="cursor: pointer"
/>
</template>
<div class="content-box">
<img :src="detailData.image" alt="" />
<div class="detail-info">
<div class="title">图片详情</div>
<div class="info">
<span>生成时间</span>
<span>{{ detailData.createdAt }}</span>
</div>
<div class="info">
<span>生成模式</span>
<span>{{
detailData.type === "ai" ? "AI生图" : "AI生图PLUS"
}}</span>
</div>
<div class="info">
<span>提示词</span>
<span>{{ detailData.realPrompt }}</span>
</div>
<div class="info">
<span>反向词</span>
<span>{{ detailData.realNegativePrompt }}</span>
</div>
<div class="info">
<span>尺寸</span>
<span>{{ detailData.width }}*{{ detailData.height }}</span>
</div>
<div class="info">
<span>订单号</span>
<span>{{ detailData.orderNum }}</span>
</div>
<n-button
style="background: #373737"
@click="del()"
class="create"
quaternary
>
<div style="color: #fff">删除</div>
</n-button>
<n-button @click="downLoad()" class="create" quaternary>
<div style="color: #fff">下载</div>
</n-button>
</div>
</div>
</n-card>
</n-modal>
</div>
</template>
<script setup>
import { ref, defineProps, computed } from "vue";
import { HistoryApi } from "src/api";
import { processError, processSuccess } from "../../../utils/message";
import { downLoadByblob, downLoadByUrl } from "../../../utils/downLoad";
import { useQuasar } from "quasar";
const $q = useQuasar();
const props = defineProps({
show: {
type: Boolean,
default: false,
},
detailData: {
type: Object,
},
});
const emit = defineEmits("update:show", "close-dialog");
const showModal = computed({
get: () => props.show,
set: (value) => {
emit("update:show", value);
},
});
//
const downLoad = async () => {
downLoadByUrl(props.detailData.image, null, "图片");
};
//
const del = async () => {
$q.dialog({
dark: true,
title: "删除",
message: "是否删除" +"图片",
cancel: "取消",
ok: "确定",
persistent: true,
})
.onOk(async () => {
try {
await HistoryApi.deleteHistory({
ids: [props.detailData.ID],
}).then((res) => {
if (res.status === 0) {
processSuccess("删除成功");
closeModal()
} else {
processError(res.msg);
}
});
} catch (error) {
console.error(error);
}
})
.onCancel(() => {
// console.log('>>>> Cancel')
})
.onDismiss(() => {
// console.log('I am triggered on both OK and Cancel')
});
console.log(selectIds.value);
};
//
const closeModal = () => {
emit("close-dialog", false);
};
</script>
<style scoped lang="scss">
.content-box {
display: flex;
height: 85%;
align-items: center;
img {
max-width: 600px;
max-height: 650px;
}
.detail-info {
padding-left: 20px;
margin-top: 20px;
width: 100%;
height: 100%;
.title {
margin-top: 310px;
font-size: 20px;
font-weight: bold;
text-align: left;
padding-bottom: 5px;
margin-bottom: 10px;
color: #fff;
border-bottom: 1px solid #707070;
}
.info {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
span {
color: #fff;
max-width: 500px;
}
}
}
}
.create {
margin-top: 50px;
width: 280px;
margin-left: 13px;
height: 55px;
background: linear-gradient(260deg, #c448b8, #591df5);
border-radius: 6px;
color: #fff;
font-weight: bold;
font-size: 18px;
margin-bottom: 5px;
}
</style>

485
src/pages/history/index.vue Normal file
View File

@ -0,0 +1,485 @@
<template>
<q-page>
<div class="main">
<div class="search-box">
<div style="display: flex; align-items: center">
生成时间
<n-date-picker
class="date-picker"
:actions="null"
v-model:value="createdTime"
start-placeholder="开始时间"
end-placeholder="结束时间"
type="daterange"
format="yyyy-MM-dd"
clearable
:close-on-select="true"
:update-value-on-close="true"
:on-update:value="handleDateChange"
/>
生成模式
<n-select
style="width: 200px; margin-left: 20px"
v-model:value="type"
:options="options"
:on-update:value="changeType"
/>
</div>
<div class="btn-group">
<span v-if="isEdit"
>已选择
<span style="margin: 0 10px 0 10px; color: #bc44b8">{{
imageList.length
}}</span>
张图片</span
>
<n-button
v-if="isEdit"
style="background: #5b1df1"
@click="del(selectIds)"
class="create"
quaternary
>
<div style="color: #fff">删除</div>
</n-button>
<n-button
v-if="isEdit"
style="background: #bd45b9"
@click="downLoad(selectIds, imageList[0].image)"
class="create"
quaternary
>
<div style="color: #fff">下载</div>
</n-button>
<n-button @click="editAll" class="create" quaternary>
<div v-if="!isEdit" style="color: #fff">批量管理</div>
<div v-else style="color: #fff">退出管理</div>
</n-button>
</div>
</div>
<q-scroll-area style="height: 90%">
<n-grid :x-gap="12" :y-gap="14" :cols="6" style="margin-top: 20px">
<n-grid-item
class="animate__animated animate__slideInUp"
v-for="(item, index) in historyList"
:key="index"
>
<n-spin :show="!item.image">
<div class="card">
<div
class="card2"
@mouseover="item.showBtn = true"
@mouseleave="item.showBtn = false"
@click="checkDetail(item)"
>
<q-parallax :src="item.image"> </q-parallax>
<div class="act-btn" v-if="item.showBtn">
<n-button
style="background: #5c1ef2; width: 125px"
@click.stop="del([item.ID])"
class="create"
quaternary
>
<div style="color: #fff">删除</div>
</n-button>
<n-button
style="background: #bd45b9; width: 125px"
@click.stop="downLoad([item.ID], item.image)"
class="create"
quaternary
>
<div style="color: #fff">下载</div>
</n-button>
</div>
<div class="checkbox-wrapper-16">
<label class="checkbox-wrapper">
<input
v-model="selectIds"
:value="item.ID"
class="checkbox-input"
type="checkbox"
/>
<span
v-if="isEdit"
@click="selectedImage(item)"
class="checkbox-tile"
>
</span>
</label>
</div>
</div>
</div>
</n-spin>
</n-grid-item>
</n-grid>
</q-scroll-area>
</div>
</q-page>
<detail
:show="showModal"
:detailData="selectedModel"
@close-dialog="closeModal"
></detail>
</template>
<script setup>
import { ref, defineProps, computed, onBeforeMount } from "vue";
import { HistoryApi } from "src/api";
import { processError, processSuccess } from "../../utils/message";
import { downLoadByblob, downLoadByUrl } from "../../utils/downLoad";
import { useQuasar } from "quasar";
import detail from "./components/detail.vue";
const $q = useQuasar();
const isEdit = ref(false);
const showModal = ref(false);
const createdTime = ref("");
const createdAtStart = ref("");
const createdAtEnd = ref("");
const type = ref("");
const historyList = ref([]);
const selectIds = ref([]);
const selectedModel = ref({});
const options = [
{ label: "全部", value: "" },
{ label: "AI生图", value: "ai" },
{ label: "AI生图PLUS", value: "plus" },
];
const imageList = ref([]);
const closeModal = () => {
showModal.value = false;
getHistoryList();
};
const checkDetail = (item) => {
//
if (isEdit.value) {
return;
}
showModal.value = true;
selectedModel.value = item;
};
const selectedImage = (item) => {
// imageList
if (imageList.value.includes(item)) {
imageList.value = imageList.value.filter((i) => i !== item);
} else {
imageList.value.push(item);
}
console.log(imageList.value);
};
const changeType = (val) => {
type.value = val;
getHistoryList();
};
//
const downLoad = async (val, url) => {
console.log(imageList.value);
if (!val.length) {
processError("请选择图片");
return;
}
if (val.length === 1) {
processSuccess("下载成功");
downLoadByUrl(url, null, "图片");
return;
}
try {
await HistoryApi.downloadHistory({
ids: val,
}).then((res) => {
//
downLoadByblob(res.data, "图片.zip");
processSuccess("下载成功");
});
} catch (error) {
console.error(error);
}
};
//
const del = async (val) => {
$q.dialog({
dark: true,
title: "删除",
message: "是否删除" + val.length + "张图片",
cancel: "取消",
ok: "确定",
persistent: true,
})
.onOk(async () => {
if (!val.length) {
processError("请选择图片");
return;
}
try {
await HistoryApi.deleteHistory({
ids: val,
}).then((res) => {
if (res.status === 0) {
processSuccess("删除成功");
imageList.value = [];
selectIds.value = [];
isEdit.value = false;
getHistoryList();
} else {
processError(res.msg);
}
});
} catch (error) {
console.error(error);
}
})
.onCancel(() => {
// console.log('>>>> Cancel')
})
.onDismiss(() => {
// console.log('I am triggered on both OK and Cancel')
});
console.log(selectIds.value);
};
const handleDateChange = (val, formattedValue) => {
if (formattedValue) {
createdAtStart.value = formattedValue[0];
createdAtEnd.value = formattedValue[1];
} else {
createdAtStart.value = "";
createdAtEnd.value = "";
}
getHistoryList();
};
const editAll = () => {
isEdit.value = !isEdit.value;
imageList.value = [];
selectIds.value = [];
};
const getHistoryList = async () => {
try {
await HistoryApi.getHistoryList({
page: 1,
pageSize: 1000,
modelId: "",
createdAtStart: createdAtStart.value,
createdAtEnd: createdAtEnd.value,
type: type.value,
}).then((res) => {
if (res.status === 0) {
historyList.value = res.data.list;
} else {
processError(res.msg);
}
});
} catch (error) {
console.error(error);
}
};
onBeforeMount(() => {
getHistoryList();
});
</script>
<style scoped lang="scss">
.q-page {
background: rgb(25, 25, 26);
display: flex;
height: 100%;
width: 100%;
}
.main {
width: 100%;
background: #212121;
border-radius: 10px;
margin: 14px 24px 14px 24px;
padding: 28px;
.search-box {
color: #ffffff;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 16px;
font-weight: bold;
.date-picker {
margin-left: 20px;
width: 300px;
margin-right: 40px;
}
}
}
.create {
width: 150px;
height: 44px;
background: linear-gradient(260deg, #c448b8, #591df5);
border-radius: 6px;
color: #fff;
font-weight: bold;
margin-left: 15px;
margin-bottom: 5px;
}
.act-btn {
display: flex;
position: absolute;
bottom: 0;
}
.card {
width: 290px;
height: 319px;
background-image: linear-gradient(
163deg,
#61d2ef 0%,
#5f33f3 50%,
#e44ea8 100%
);
border-radius: 20px;
transition: all 0.3s;
}
.card2 {
cursor: pointer;
width: 290px;
display: flex;
justify-content: center;
flex-direction: column;
height: 319px;
position: relative;
background-color: #19191a;
border-radius: 20px;
transition: all 0.2s;
background-repeat: no-repeat;
background-size: 100% 100%;
}
.card2:hover {
transform: scale(0.98);
border-radius: 20px;
}
.card:hover {
box-shadow: 0px 0px 30px 1px rgba(0, 255, 117, 0.1);
}
.checkbox-wrapper-16 *,
.checkbox-wrapper-16 *:after,
.checkbox-wrapper-16 *:before {
box-sizing: border-box;
}
.checkbox-wrapper-16 .checkbox-input {
clip: rect(0 0 0 0);
-webkit-clip-path: inset(100%);
clip-path: inset(100%);
height: 1px;
overflow: hidden;
position: absolute;
white-space: nowrap;
width: 1px;
}
.checkbox-wrapper-16 .checkbox-input:checked + .checkbox-tile:before {
transform: scale(1);
opacity: 1;
background-color: #c348b8;
border-color: #c348b8;
}
.checkbox-wrapper-16 .checkbox-input:focus + .checkbox-tile:before {
transform: scale(1);
opacity: 1;
}
.checkbox-wrapper-16 .checkbox-tile {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 300px;
border-radius: 20px;
transition: 0.15s ease;
cursor: pointer;
top: 10px;
position: absolute;
}
.checkbox-wrapper-16 .checkbox-tile:before {
content: "";
position: absolute;
display: block;
width: 1.25rem;
height: 1.25rem;
border: 2px solid #b5bfd9;
background-color: #fff;
border-radius: 50%;
top: 0.25rem;
left: 0.25rem;
margin-left: 10px;
transition: 0.25s ease;
background-image: url("data:image/svg+xml,%3Csvg xmlns='://www.w3.org/2000/svg' width='192' height='192' fill='%23FFFFFF' viewBox='0 0 256 256'%3E%3Crect width='256' height='256' fill='none'%3E%3C/rect%3E%3Cpolyline points='216 72.005 104 184 48 128.005' fill='none' stroke='%23FFFFFF' stroke-linecap='round' stroke-linejoin='round' stroke-width='32'%3E%3C/polyline%3E%3C/svg%3E");
background-size: 12px;
background-repeat: no-repeat;
background-position: 50% 50%;
}
.checkbox-wrapper-16 .checkbox-tile:hover {
border-color: #c348b8;
opacity: 1;
}
.checkbox-wrapper-16 .checkbox-tile:hover:before {
transform: scale(1);
opacity: 1;
}
.checkbox-wrapper-16 .checkbox-icon {
transition: 0.375s ease;
color: #494949;
}
.checkbox-wrapper-16 .checkbox-icon svg {
width: 3rem;
height: 3rem;
}
.checkbox-wrapper-16 .checkbox-label {
color: #707070;
transition: 0.375s ease;
text-align: center;
}
:deep(.n-card__content) {
margin-top: 20px;
}
:deep(.n-input) {
background: #383838;
border: 1px solid #7e7e7e;
}
:deep(.n-input__input-el) {
color: #bebebe;
}
:deep(.n-input .n-input__border) {
border: 0;
}
// placeholder
:deep(.n-input__placeholder) {
color: #7e7e7e;
}
:deep(.n-input__textarea-el) {
color: #7e7e7e;
}
:deep(.n-input__input-el) {
color: #7e7e7e;
}
:deep(.n-base-selection-input__content) {
color: #fff;
}
:deep(.n-base-selection-label) {
height: 36px;
background-color: #383838;
width: 200px;
color: #7e7e7e;
}
:deep(.n-base-selection .n-base-selection__border) {
border: 1px solid #7e7e7e;
}
:deep(.n-base-selection-input__content) {
color: #7e7e7e;
}
</style>

View File

@ -0,0 +1,266 @@
<template>
<q-page>
<div class="main-box">
<div class="search-box">
<div style="display: flex; align-items: center">
订单编号
<n-input
style="width: 390px; margin-right: 60px; margin-left: 20px"
v-model:value="orderNum"
type="text"
@keydown.enter="getCostList"
placeholder="请输入订单号"
/>
消费时间
<n-date-picker
class="date-picker"
:actions="null"
v-model:value="createdTime"
start-placeholder="开始时间"
end-placeholder="结束时间"
type="daterange"
format="yyyy-MM-dd"
clearable
:close-on-select="true"
:update-value-on-close="true"
:on-update:value="handleDateChange"
/>
</div>
</div>
<n-data-table
style="margin-top: 40px"
:bordered="false"
:columns="columns"
:data="tableData"
:loading="loading"
/>
<div class="page">
{{ total }}条数据
<n-pagination
style="margin-left: 20px"
v-model:page="page"
:page-count="pageCount"
size="medium"
show-quick-jumper
:on-update:page="pageChange"
/>
</div>
</div>
</q-page>
</template>
<script setup>
import {
ref,
defineProps,
computed,
onBeforeMount,
watch,
reactive,
h,
} from "vue";
import { LocalStorage, SessionStorage } from "quasar";
import { processError, processSuccess } from "../../../utils/message";
import { useRoute, useRouter } from "vue-router";
import { ScoreApi } from "src/api";
const createdTime = ref("");
const createdAtStart = ref("");
const createdAtEnd = ref("");
const orderNum = ref("");
const loading = ref(false);
const page = ref(1);
const tableData = ref([]);
const pageCount = ref(0);
const total = ref(0);
const columns = [
{
title: "消费时间",
key: "createAt",
width: 200,
},
{
title: "订单号",
key: "orderNum",
width: 200,
},
{
title: "消费积分 ",
key: "coin",
width: 200,
},
{
title: "订单状态",
key: "status",
width: 200,
// 0绿
render: (row) => {
return h(
"div",
{ style: { color: row.status === 0 ? "#C348B8" : "#61C8EF" } },
row.status === 0 ? "失败" : "成功"
);
},
},
];
const pageChange = (val) => {
page.value = val;
getCostList();
};
//
const getCostList = async () => {
loading.value = true;
const params = {
page: page.value,
pageSize: 10,
startCreatedAt: createdAtStart.value,
endCreatedAt: createdAtEnd.value,
orderNum: orderNum.value,
};
await ScoreApi.getScore(params).then((res) => {
if (res.code === 200) {
loading.value = false;
total.value = res.data.count;
tableData.value = res.data.list || [];
pageCount.value = Math.ceil(res.data.count / 10);
} else {
loading.value = false;
processError(res);
}
});
};
const handleDateChange = (val, formattedValue) => {
if (formattedValue) {
createdAtStart.value = formattedValue[0];
createdAtEnd.value = formattedValue[1];
} else {
createdAtStart.value = "";
createdAtEnd.value = "";
}
getCostList();
};
onBeforeMount(() => {
getCostList();
});
</script>
<style scoped lang="scss">
.q-page {
background: #19191a;
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
}
.main-box {
padding: 28px;
background: #212121;
margin: 15px;
height: 98%;
border-radius: 10px;
width: 98%;
}
.search-box {
color: #ffffff;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 16px;
font-weight: bold;
.date-picker {
margin-left: 20px;
width: 300px;
margin-right: 40px;
}
}
.page {
display: flex;
justify-content: end;
margin-top: 20px;
align-items: center;
color: #7e7e7e;
}
:deep(.n-input) {
background: #383838;
border: 1px solid #7e7e7e;
}
:deep(.n-input__input-el) {
color: #bebebe;
}
:deep(.n-input .n-input__border) {
border: 0;
}
// placeholder
:deep(.n-input__placeholder) {
color: #7e7e7e;
}
:deep(.n-input__textarea-el) {
color: #7e7e7e;
}
:deep(.n-input__input-el) {
color: #7e7e7e;
}
:deep(.n-base-selection-input__content) {
color: #fff;
}
:deep(.n-base-selection-label) {
height: 36px;
background-color: #383838;
width: 200px;
color: #7e7e7e;
}
:deep(.n-base-selection .n-base-selection__border) {
border: 1px solid #7e7e7e;
}
:deep(.n-base-selection-input__content) {
color: #7e7e7e;
}
:deep(
.n-pagination
.n-pagination-item.n-pagination-item--disabled.n-pagination-item--button
) {
background-color: #19191a;
}
:deep(
.n-pagination
.n-pagination-item:not(
.n-pagination-item--disabled
).n-pagination-item--active
) {
background-color: #c348b8;
color: #fff;
border: 1px solid #c348b8;
}
:deep(.n-pagination .n-pagination-item) {
color: #7e7e7e;
}
:deep(.n-data-table-td) {
--n-th-color-hover: #383838;
background-color: #000000;
text-align: center;
color: #7e7e7e;
border-bottom: 1px solid #383838;
}
:deep(.n-data-table-th) {
border: 0;
font-weight: bold;
color: #ffffff;
text-align: center;
}
:deep(.n-data-table) {
--n-th-color: #c448b8 !important;
--n-td-color-hover: #383838 !important;
--n-loading-color: #c448b8 !important;
--n-td-padding: 17px !important;
border: 0;
}
:deep(
.n-data-table.n-data-table--bottom-bordered
.n-data-table-td.n-data-table-td--last-row
) {
border: 0 !important;
}
:deep(.n-scrollbar-container){
border-radius: 10px;
}
</style>

View File

@ -0,0 +1,543 @@
<template>
<q-page>
<div class="top-img">
<img src="../../../assets/image/mine-bg.png" alt="mine" />
<div class="txt">
<div style="font-size: 20px">Personal Center</div>
<div style="font-size: 60px">个人中心</div>
</div>
</div>
<div class="info">
<div class="item">
<div class="txt">
<span class="tx1">用户名</span>
<span class="tx2">{{ user.nickName }}</span>
</div>
<span @click="editUserName">编辑</span>
</div>
<div class="item" style="height: 120px">
<div class="txt">
<span class="tx1">头像&nbsp;&nbsp;&nbsp;&nbsp;</span>
<n-avatar
style="margin-left: 20px"
round
:size="88"
:src="user.avatar"
/>
</div>
<span @click="editAvatar">修改</span>
</div>
<div class="item">
<div class="txt">
<span class="tx1">手机号</span>
<span class="tx2">{{ user.telNum }}</span>
</div>
<span @click="editTel">修改</span>
</div>
<div class="item">
<div class="txt">
<span class="tx1">密码&nbsp;&nbsp;&nbsp;&nbsp;</span>
<span class="tx2"></span>
</div>
<span @click="editPwd">修改</span>
</div>
</div>
<!-- 修改用户名 -->
<n-modal
v-model:show="showModal"
@close="closeModal"
:mask-closable="false"
>
<n-card
style="width: 900px; text-align: center; height: 420px"
:title="title"
:bordered="false"
size="huge"
role="dialog"
>
<template #header-extra>
<img
src="../../../assets/image/ai/close.png"
alt="关闭"
@click="closeModal"
style="cursor: pointer"
/>
</template>
<div class="content-box" v-if="title === '修改用户名'">
用户名
<q-input
class="input-box"
maxlength="10"
v-model="nickName"
:dense="true"
placeholder="请输入用户名"
/>
</div>
<div
v-if="title === '修改用户名'"
style="color: #bebebe; margin-left: 80px; margin-top: 10px"
>
用户名请输入1-10个字符可包括中文英文和数字
</div>
<n-upload
v-if="title === '修改头像'"
class="content-box"
list-type="image-card"
:max="1"
:on-finish="handleChange"
action="http://192.168.1.244:8085/api/ai/upload-file"
>
<n-upload-dragger>
<div>
<img src="../../../assets/image/ai/plus.png" />
</div>
</n-upload-dragger>
</n-upload>
<div
class="content-box"
style="flex-direction: column; margin-top: 20px"
v-if="title === '修改密码'"
>
<div style="display: flex; align-items: center">
旧密码
<q-input
style="margin-left: 70px"
class="input-box"
v-model="oldPwd"
maxlength="8"
:dense="true"
placeholder="请输入旧密码"
/>
</div>
<div style="display: flex; align-items: center; margin-top: 20px">
新密码
<q-input
style="margin-left: 70px"
class="input-box"
v-model="password"
:dense="true"
maxlength="8"
placeholder="请输入新密码"
/>
</div>
<div style="display: flex; align-items: center; margin-top: 20px">
确认新密码
<q-input
class="input-box"
v-model="confirmPwd"
:dense="true"
maxlength="8"
placeholder="请输入新密码"
/>
</div>
</div>
<div
class="content-box"
style="flex-direction: column; margin-top: 20px"
v-if="title === '修改手机号'"
>
<div
style="display: flex; align-items: center; flex-direction: column"
>
<div style="display: flex; align-items: center; margin-top: 20px">
新手机号
<q-input
style="margin-left: 10px"
class="input-box"
v-model="newTelNum"
:dense="true"
placeholder="请输入新手机号"
/>
</div>
<div class="account-box">
<span class="account" style="transform: translateX(0)"
>验证码</span
>
<q-input
style="width: 212px"
class="input-box"
v-model="code"
:dense="true"
placeholder="请输入验证码"
>
</q-input>
<q-btn
class="code-btn"
:loading="codeLoading"
:disabled="restSec < 60"
@click="getRestCode"
style="margin-right: 10px"
>
<div v-if="restSec === 60 || restSec < 1">获取验证码</div>
<div v-else>{{ restSec }}</div>
</q-btn>
</div>
</div>
</div>
<div
v-show="isErr"
style="
position: absolute;
display: flex;
align-items: center;
justify-content: center;
margin-top: 10px;
left: 45%;
"
>
<img
style="width: 22px; height: 18px; margin-right: 5px"
src="../../../assets/image/login/warning.png"
/>
<span style="color: #e0378c; margin-top: 2px">{{ errMsg }}</span>
</div>
<n-button @click="doAction" class="create" quaternary>
<div style="color: #fff">确定</div>
</n-button>
</n-card>
</n-modal>
</q-page>
</template>
<script setup>
import { ref, defineProps, computed, onBeforeMount, watch } from "vue";
import { LocalStorage, SessionStorage } from "quasar";
import { processError, processSuccess } from "../../../utils/message";
import { useRoute, useRouter } from "vue-router";
import { UserApi } from "src/api";
const router = useRouter();
const user = LocalStorage.getItem("sd-accountInfo") || {};
const showModal = ref(false);
const title = ref("");
const nickName = ref("");
const avatar = ref("");
const password = ref("");
const confirmPwd = ref("");
const oldPwd = ref("");
const isErr = ref(false);
const codeLoading = ref(false);
const restSec = ref(60);
const code = ref("");
const errMsg = ref("");
const newTelNum = ref("");
const closeModal = () => {
showModal.value = false;
isErr.value = false;
errMsg.value = "";
nickName.value = "";
avatar.value = "";
password.value = "";
oldPwd.value = "";
confirmPwd.value = "";
};
//
const editUserName = () => {
title.value = "修改用户名";
showModal.value = true;
};
//
const editAvatar = () => {
title.value = "修改头像";
showModal.value = true;
};
//
const editTel = () => {
title.value = "修改手机号";
showModal.value = true;
};
//
const editPwd = () => {
title.value = "修改密码";
showModal.value = true;
};
//
const handleChange = (file, List) => {
let resData = JSON.parse(file.event.currentTarget.response);
if (resData.status === 0) {
processSuccess("上传成功");
avatar.value = resData.data.ori_url;
} else {
processError(resData.msg);
}
};
//
const getRestCode = async () => {
if (!newTelNum.value) {
errMsg.value = "手机号不能为空";
isErr.value = true;
return;
}
isErr.value = false;
errMsg.value = "";
codeLoading.value = true;
const data = {
tel: newTelNum.value,
};
try {
await UserApi.getUpdatePhoneCode(data).then((res) => {
console.log(res);
if (res.status === 0) {
codeLoading.value = false;
let countdown = 60;
const countdownTimer = setInterval(() => {
countdown--;
restSec.value = countdown;
if (countdown === 0) {
clearInterval(countdownTimer);
restSec.value = 60;
}
}, 1000);
} else {
codeLoading.value = false;
errMsg.value = res.msg;
isErr.value = true;
}
});
} catch (error) {
codeLoading.value = false;
console.error(error);
}
};
//
const editTelNum = async () => {
errMsg.value = "";
const data = {
newTelNum: newTelNum.value,
code: code.value,
};
await UserApi.updatePhone(data).then((res) => {
if (res.status === 0) {
processSuccess("修改成功");
LocalStorage.clear();
router.push("/login");
closeModal();
} else {
processError(res.msg);
}
});
};
// ,
const editUserInfo = async () => {
errMsg.value = "";
const data = {
nickName: nickName.value,
avatar: avatar.value,
password: password.value,
oldPwd: oldPwd.value,
};
await UserApi.updateUserInfo(data).then((res) => {
if (res.status === 0) {
processSuccess("修改成功");
LocalStorage.set("sd-accountInfo", res.data);
//
if (title.value === "修改密码") {
LocalStorage.clear();
router.push("/login");
} else {
//
location.reload();
}
closeModal();
} else {
processError(res.msg);
}
});
};
//
const doAction = () => {
if (title.value === "修改用户名") {
//
if (!nickName.value) {
isErr.value = true;
errMsg.value = "用户名不能为空";
return;
}
//
if (!/^[\u4e00-\u9fa5a-zA-Z0-9]{1,10}$/.test(nickName.value)) {
isErr.value = true;
errMsg.value = "用户名格式错误";
return;
}
editUserInfo();
} else if (title.value === "修改头像") {
if (!avatar.value) {
isErr.value = true;
errMsg.value = "请上传头像";
return;
}
editUserInfo();
} else if (title.value === "修改密码") {
if (!oldPwd.value) {
isErr.value = true;
errMsg.value = "旧密码不能为空";
return;
}
if (!password.value) {
isErr.value = true;
errMsg.value = "新密码不能为空";
return;
}
if (!confirmPwd.value) {
isErr.value = true;
errMsg.value = "确认密码不能为空";
return;
}
if (password.value !== confirmPwd.value) {
isErr.value = true;
errMsg.value = "两次密码不一致";
return;
}
editUserInfo();
} else if (title.value === "修改手机号") {
if (!newTelNum.value) {
isErr.value = true;
errMsg.value = "手机号不能为空";
return;
}
if (!code.value) {
isErr.value = true;
errMsg.value = "验证码不能为空";
return;
}
editTelNum();
}
};
onBeforeMount(() => {});
</script>
<style scoped lang="scss">
.q-page {
background: #19191a;
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
}
.top-img {
width: 100%;
height: 340px;
position: relative;
img {
width: 100%;
height: 100%;
}
.txt {
position: absolute;
bottom: 0%;
left: 2%;
font-weight: bolder;
color: #fff;
text-align: left div {
margin-bottom: 10px;
}
}
}
.create {
margin-top: 60px;
width: 260px;
height: 55px;
background: linear-gradient(260deg, #c448b8, #591df5);
border-radius: 6px;
color: #fff;
font-weight: bolder;
font-size: 18px;
margin-bottom: 5px;
}
.info {
width: 100%;
height: 100%;
background: linear-gradient(to bottom, #39273c, #000000);
.item {
margin: 26px 0 0 32px;
width: 977px;
min-height: 60px;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 20px;
border-radius: 10px;
background: #212121;
font-size: 18px;
.txt {
display: flex;
justify-content: space-between;
align-items: center;
span {
color: #bebebe;
}
.tx1 {
padding-right: 30px;
border-right: 1px solid #707070;
}
.tx2 {
padding-left: 30px;
}
}
span {
color: #c348b8;
cursor: pointer;
}
}
}
.content-box {
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
margin-top: 65px;
color: #bebebe;
}
.input-box {
width: 370px;
margin-left: 35px;
padding-bottom: 0;
}
.account-box {
display: flex;
justify-content: center;
align-items: center;
font-size: 16px;
margin-top: 30px;
.account {
color: #bebebe;
transform: translateX(-120%);
}
.input-box {
width: 370px;
padding-bottom: 0;
}
}
.code-btn {
margin-left: 20px;
background: linear-gradient(to right, #591df5, #c448b8);
color: #fff;
font-weight: bold;
border: 8px;
height: 45px;
min-width: 125px;
}
:deep(.q-field__inner) {
background: #212121;
padding: 0 10px 0 10px;
border: 2px solid transparent;
border-radius: 8px;
background-clip: padding-box, border-box;
background-origin: padding-box, border-box;
background-image: linear-gradient(to right, #222, #222),
linear-gradient(to right, #61d2ef, #e44ea8);
.q-field__native {
color: #7e7e7e;
}
}
:deep(.n-upload-dragger) {
border-radius: 50%;
}
:deep(.q-field--standard .q-field__control:after) {
display: none;
}
</style>

View File

@ -0,0 +1,334 @@
<template>
<q-page>
<div class="main-box">
<div class="search-box">
<div style="display: flex; align-items: center">
转入时间
<n-date-picker
class="date-picker"
:actions="null"
v-model:value="createdTime"
start-placeholder="开始时间"
end-placeholder="结束时间"
type="daterange"
format="yyyy-MM-dd"
clearable
:close-on-select="true"
:update-value-on-close="true"
:on-update:value="handleDateChange"
/>
到期时间
<n-date-picker
class="date-picker"
:actions="null"
v-model:value="expireTime"
start-placeholder="开始时间"
end-placeholder="结束时间"
type="daterange"
format="yyyy-MM-dd"
clearable
:close-on-select="true"
:update-value-on-close="true"
:on-update:value="handleExDateChange"
/>
状态
<n-select
style="width: 200px; margin-left: 20px"
v-model:value="type"
:options="options"
:on-update:value="changeType"
/>
</div>
</div>
<n-data-table
style="margin-top: 40px"
:bordered="false"
:columns="columns"
:data="tableData"
:loading="loading"
:on-update:sorter="sorter"
/>
<div class="page">
{{ total }}条数据
<n-pagination
style="margin-left: 20px"
v-model:page="page"
:page-count="pageCount"
size="medium"
show-quick-jumper
:on-update:page="pageChange"
/>
</div>
</div>
</q-page>
</template>
<script setup>
import {
ref,
defineProps,
computed,
onBeforeMount,
watch,
reactive,
h,
} from "vue";
import { LocalStorage, SessionStorage } from "quasar";
import { processError, processSuccess } from "../../../utils/message";
import { useRoute, useRouter } from "vue-router";
import { ScoreApi } from "src/api";
const createdTime = ref("");
const createdAtStart = ref("");
const createdAtEnd = ref("");
const expireTime = ref("");
const expireAtStart = ref("");
const expireAtEnd = ref("");
const orderBy = ref("");
const loading = ref(false);
const page = ref(1);
const tableData = ref([]);
const pageCount = ref(0);
const total = ref(0);
const type = ref(0);
const options = [
{ label: "全部", value: 0 },
{ label: "未过期", value: 1 },
{ label: "已过期", value: 2 },
];
const columns = [
{
title: "转入时间",
key: "createdAt",
width: 200,
},
{
title: "订单号",
key: "orderNum",
width: 200,
},
{
title: "转入积分 ",
key: "coin",
width: 200,
},
{
title: "来源",
key: "scene",
width: 200,
render: (row) => {
return h(
"div",
row.scene === "register" ? "新用户注册奖励" : "每日登录奖励"
);
},
},
{
title: "到期时间",
key: "expiredAt",
width: 200,
sorter: true,
},
{
title: "订单状态",
key: "isExpired",
width: 200,
// 0绿
render: (row) => {
return h(
"div",
{ style: { color: row.isExpired ? "#C348B8" : "#61C8EF" } },
row.isExpired ? "已过期" : "未过期"
);
},
},
];
const sorter = (sort) => {
console.log(sort);
let txt = sort.order === "ascend" ? "asc" : "desc";
orderBy.value ="expired_at "+ txt;
console.log(orderBy.value);
getCostList();
};
const changeType = (val) => {
type.value = val;
getCostList();
};
const pageChange = (val) => {
page.value = val;
getCostList();
};
//
const getCostList = async () => {
loading.value = true;
const params = {
page: page.value,
pageSize: 10,
startCreatedAt: createdAtStart.value,
endCreatedAt: createdAtEnd.value,
startExpireAt: expireAtStart.value,
endExpireAt: expireAtEnd.value,
scene: "daily_login",
status: type.value,
orderBy: orderBy.value,
};
await ScoreApi.getScoreList(params).then((res) => {
if (res.code === 200) {
loading.value = false;
total.value = res.data.count;
tableData.value = res.data.list || [];
pageCount.value = Math.ceil(res.data.count / 10);
} else {
loading.value = false;
processError(res);
}
});
};
const handleDateChange = (val, formattedValue) => {
if (formattedValue) {
createdAtStart.value = formattedValue[0];
createdAtEnd.value = formattedValue[1];
} else {
createdAtStart.value = "";
createdAtEnd.value = "";
}
getCostList();
};
const handleExDateChange = (val, formattedValue) => {
if (formattedValue) {
expireAtStart.value = formattedValue[0];
expireAtEnd.value = formattedValue[1];
} else {
expireAtStart.value = "";
expireAtEnd.value = "";
}
getCostList();
};
onBeforeMount(() => {
getCostList();
});
</script>
<style scoped lang="scss">
.q-page {
background: #19191a;
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
}
.main-box {
padding: 28px;
background: #212121;
margin: 15px;
height: 98%;
border-radius: 10px;
width: 98%;
}
.search-box {
color: #ffffff;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 16px;
font-weight: bold;
.date-picker {
margin-left: 20px;
width: 300px;
margin-right: 40px;
}
}
.page {
display: flex;
justify-content: end;
margin-top: 20px;
align-items: center;
color: #7e7e7e;
}
:deep(.n-input) {
background: #383838;
border: 1px solid #7e7e7e;
}
:deep(.n-input__input-el) {
color: #bebebe;
}
:deep(.n-input .n-input__border) {
border: 0;
}
// placeholder
:deep(.n-input__placeholder) {
color: #7e7e7e;
}
:deep(.n-input__textarea-el) {
color: #7e7e7e;
}
:deep(.n-input__input-el) {
color: #7e7e7e;
}
:deep(.n-base-selection-input__content) {
color: #fff;
}
:deep(.n-base-selection-label) {
height: 36px;
background-color: #383838;
width: 200px;
color: #7e7e7e;
}
:deep(.n-base-selection .n-base-selection__border) {
border: 1px solid #7e7e7e;
}
:deep(.n-base-selection-input__content) {
color: #7e7e7e;
}
:deep(
.n-pagination
.n-pagination-item.n-pagination-item--disabled.n-pagination-item--button
) {
background-color: #19191a;
}
:deep(
.n-pagination
.n-pagination-item:not(
.n-pagination-item--disabled
).n-pagination-item--active
) {
background-color: #c348b8;
color: #fff;
border: 1px solid #c348b8;
}
:deep(.n-pagination .n-pagination-item) {
color: #7e7e7e;
}
:deep(.n-data-table-td) {
--n-th-color-hover: #383838;
background-color: #000000;
text-align: center;
color: #7e7e7e;
border-bottom: 1px solid #383838;
}
:deep(.n-data-table-th) {
border: 0;
font-weight: bold;
color: #ffffff;
text-align: center;
}
:deep(.n-data-table) {
--n-th-color: #c448b8 !important;
--n-td-color-hover: #000000 !important;
--n-loading-color: #c448b8 !important;
--n-th-color-hover: #c448b8 !important;
--n-td-padding: 17px !important;
border: 0;
}
:deep(
.n-data-table.n-data-table--bottom-bordered
.n-data-table-td.n-data-table-td--last-row
) {
border: 0 !important;
}
:deep(.n-scrollbar-container) {
border-radius: 10px;
}
</style>

153
src/pages/mine/index.vue Normal file
View File

@ -0,0 +1,153 @@
<template>
<q-page>
<div class="silder">
<div class="menu-box">
<n-menu
ref="menuInstRef"
:options="menuOptions"
:value="selectedKeys"
@update:value="handleUpdateExpandedKeys"
/>
</div>
</div>
<div style="display: flex; width: 89%">
<mineInfo
v-if="selectedKeys === 'mineCenter'"
class="animate__animated animate__fadeIn"
/>
<cost
v-if="selectedKeys === 'pointDetail'"
class="animate__animated animate__fadeIn"
/>
<reward
v-if="selectedKeys === 'rewardDetail'"
class="animate__animated animate__fadeIn"
/>
</div>
</q-page>
</template>
<script setup>
import { ref, defineProps, computed, onBeforeMount, watch, h } from "vue";
import { useRoute, useRouter } from "vue-router";
import mineInfo from "./components/mineInfo.vue";
import cost from "./components/cost.vue";
import reward from "./components/reward.vue";
// vNode
const mineIcon = () => {
return h("img", {
src: "https://cdns.fontree.cn/blockchain-main-test/dev/image/default/ai/3e237fcc-78a3-48dc-94d5-dee25b753f0b.png",
alt: "coin",
style: {
width: "20px",
height: "20px",
},
});
};
const pointIcon = () => {
return h("img", {
src: "https://cdns.fontree.cn/blockchain-main-test/dev/image/default/ai/23163654-3fb2-45f0-97ac-a48aada80dcd.png",
alt: "coin",
style: {
width: "20px",
height: "20px",
},
});
};
const router = useRouter();
const selectedKeys = ref("");
const menuInstRef = ref("");
const menuOptions = [
{
label: "积分明细",
key: "point",
icon: pointIcon,
children: [
{
label: "消费积分明细",
key: "pointDetail",
},
{
label: "奖励积分明细",
key: "rewardDetail",
},
],
},
{
label: "个人中心",
key: "mineCenter",
icon: mineIcon,
},
];
const handleUpdateExpandedKeys = (keys) => {
selectedKeys.value = keys;
//
router.push({
query: {
active: keys,
},
});
};
//
watch(
() => router.currentRoute.value.query.active,
(val) => {
selectedKeys.value = val;
menuInstRef.value?.showOption(val);
}
);
onBeforeMount(() => {
selectedKeys.value = router.currentRoute.value.query.active;
});
</script>
<style scoped lang="scss">
.q-page {
background: #19191a;
display: flex;
height: 100%;
width: 100%;
}
.silder {
width: 220px;
background: #212121;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
.menu-box {
position: relative;
display: flex;
flex-direction: column;
margin-top: 30px;
align-items: center;
width: 100%;
height: 100%;
}
}
:deep(.n-menu) {
width: 100%;
color: #fff;
}
:deep(.n-menu-item-content-header) {
color: #fff !important;
text-align: center;
margin-right: 20px;
}
:deep(.n-menu) {
--n-item-color-hover: #be45ba !important;
}
:deep(.n-menu .n-menu-item-content.n-menu-item-content--selected::before) {
background: linear-gradient(to right, #be45ba, #5d1ef1);
}
:deep(.n-menu-item-content__arrow) {
color: #fff !important;
}
:deep(.--n-item-color-hover) {
color: #fff;
}
</style>

30
src/router/index.js Normal file
View File

@ -0,0 +1,30 @@
import { route } from 'quasar/wrappers'
import { createRouter, createMemoryHistory, createWebHistory, createWebHashHistory } from 'vue-router'
import routes from './routes'
/*
* If not building with SSR mode, you can
* directly export the Router instantiation;
*
* The function below can be async too; either use
* async/await or return a Promise which resolves
* with the Router instance.
*/
export default route(function (/* { store, ssrContext } */) {
const createHistory = process.env.SERVER
? createMemoryHistory
: (process.env.VUE_ROUTER_MODE === 'history' ? createWebHistory : createWebHashHistory)
const Router = createRouter({
scrollBehavior: () => ({ left: 0, top: 0 }),
routes,
// Leave this as is and make changes in quasar.conf.js instead!
// quasar.conf.js -> build -> vueRouterMode
// quasar.conf.js -> build -> publicPath
history: createHistory(process.env.VUE_ROUTER_BASE)
})
return Router
})

56
src/router/routes.js Normal file
View File

@ -0,0 +1,56 @@
const routes = [
{
path: '/',
component: () => import('layouts/MainLayout.vue'),
children: [
{ path: '', component: () => import('pages/create/index.vue') }
]
},
// 登录
{
path: '/login',
children: [
{ path: '', component: () => import('pages/Login.vue') }
]
},
// 历史
{
path: '/history',
component: () => import('layouts/MainLayout.vue'),
children: [
{ path: '', component: () => import('pages/history/index.vue') }
]
},
// AICG
{
path: '/aicg',
component: () => import('layouts/MainLayout.vue'),
children: [
{ path: '', component: () => import('pages/aicg/index.vue') }
]
},
// 个人中心
{
path: '/mine',
component: () => import('layouts/MainLayout.vue'),
children: [
{ path: '', component: () => import('pages/mine/index.vue') }
]
},
// 留言反馈
{
path: '/feedback',
component: () => import('layouts/MainLayout.vue'),
children: [
{ path: '', component: () => import('pages/feedback/index.vue') }
]
},
// Always leave this as last one,
// but you can also remove it
{
path: '/:catchAll(.*)*',
component: () => import('pages/ErrorNotFound.vue')
}
]
export default routes

54
src/utils/auto-update.js Normal file
View File

@ -0,0 +1,54 @@
let lastSrcs;
const scriptReg = /\<script.*src=["'](?<src>[^"']+)/gm;
// 获取最新的 script src
async function extractNewScripts() {
const html = await fetch("/?_timestamp=" + Date.now()).then((res) =>
res.text()
);
scriptReg.lastIndex = 0;
let result = [];
let match;
while ((match = scriptReg.exec(html))) {
result.push(match.groups.src);
}
return result;
}
// 判断是否需要更新
async function needUpdate() {
const newScripts = await extractNewScripts();
if (!lastSrcs) {
lastSrcs = newScripts;
return false;
}
let result = false;
if (newScripts.length !== lastSrcs.length) {
result = true;
} else {
for (let i = 0; i < newScripts.length; i++) {
if (newScripts[i] !== lastSrcs[i]) {
result = true;
break;
}
}
}
lastSrcs = newScripts;
return result;
}
// 间隔五分钟
const DURATION = 1000 * 60 * 5;
function autoRefresh() {
setTimeout(async () => {
const willUpdate = await needUpdate();
if (willUpdate) {
}
autoRefresh();
}, DURATION);
}
autoRefresh();

43
src/utils/downLoad.js Normal file
View File

@ -0,0 +1,43 @@
export const downLoadByblob = (blob, fileName) => {
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = fileName;
link.style.display = "none";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
};
export const downLoadByUrl = (url, rowData, filename, timeflag = true) => {
return new Promise((resolve, reject) => {
try {
// 这里加 ?v= 能绕过 跨域????
if (timeflag && !url.includes("?v=")) {
url = url + "?v=" + new Date().getTime();
}
fetch(url, { mode: "cors" }).then((res) => {
res.blob().then((blob) => {
const blobUrl = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = blobUrl;
if (Array.isArray(filename)) {
let newName = "";
filename.forEach((fi) => {
newName = newName + " " + (rowData[fi] || fi);
});
filename = newName;
}
// a.download必须有值
a.download = filename || new Date().getTime();
a.click();
window.URL.revokeObjectURL(blobUrl);
resolve();
});
});
} catch (e) {
reject(e);
}
});
};

22
src/utils/message.js Normal file
View File

@ -0,0 +1,22 @@
import { Notify } from "quasar";
export const processSuccess = (res, time = 3) => {
Notify.create({
message: res,
type: "positive",
position: "top",
});
};
export const processError = (res, time = 3) => {
Notify.create({
message: res,
type: "negative",
position: "top",
});
};
export const processWarning = (res, time = 3) => {
Notify.create({
message: res,
type: "warning",
position: "top",
});
};

181
src/utils/scroll-out.min.js vendored Normal file
View File

@ -0,0 +1,181 @@
export const ScrollOut = (function () {
"use strict";
function w() {}
var E,
t = [],
S = [];
function y() {
S.slice().forEach(function (e) {
return e();
});
var e = t;
(t = []),
e.forEach(function (e) {
return e.f.apply(0, e.a);
}),
(E = S.length ? requestAnimationFrame(y) : 0);
}
function D(e) {
return (
(e = e || w),
function () {
e.apply(0, arguments);
}
);
}
function L(e, t, n) {
return e < t ? t : n < e ? n : e;
}
function P(e) {
return (0 < e) - (e < 0);
}
var n = {};
function b(e) {
return n[e] || (n[e] = e.replace(/([A-Z])/g, r));
}
function r(e) {
return "-" + e[0].toLowerCase();
}
var A = window,
H = document.documentElement;
function O(e, t) {
return e && 0 != e.length
? e.nodeName
? [e]
: [].slice.call(e[0].nodeName ? e : (t || H).querySelectorAll(e))
: [];
}
var x = D(function (e, t) {
for (var n in t) e.setAttribute("data-" + b(n), t[n]);
}),
W = "scroll",
N = "resize",
T = "addEventListener",
$ = "removeEventListener",
_ = 0;
return function (h) {
var o,
i,
c,
l,
d,
p,
t,
s = D((h = h || {}).onChange),
f = D(h.onHidden),
u = D(h.onShown),
a = h.cssProps
? ((o = h.cssProps),
D(function (e, t) {
for (var n in t)
(1 == o || o[n]) &&
e.style.setProperty(
"--" + b(n),
((r = t[n]), Math.round(1e4 * r) / 1e4)
);
var r;
}))
: w,
e = h.scrollingElement,
g = e ? O(e)[0] : A,
m = e ? O(e)[0] : H,
r = ++_,
v = function (e, t, n) {
return e[t + r] != (e[t + r] = JSON.stringify(n));
},
n = function () {
l = !0;
},
X = function () {
var a = m.clientWidth,
v = m.clientHeight,
e = P(-d + (d = m.scrollLeft || A.pageXOffset)),
t = P(-p + (p = m.scrollTop || A.pageYOffset)),
n = m.scrollLeft / (m.scrollWidth - a || 1),
r = m.scrollTop / (m.scrollHeight - v || 1);
(i = {
scrollDirX: e,
scrollDirY: t,
scrollPercentX: n,
scrollPercentY: r,
}),
l &&
((l = !1),
(c = O(h.targets || "[data-scroll]", O(h.scope || m)[0]).map(
function (e) {
return { $: e, ctx: {} };
}
))),
c.forEach(function (e) {
for (
var t = e.$, n = t, r = 0, o = 0;
(r += n.offsetLeft),
(o += n.offsetTop),
(n = n.offsetParent) && n != g;
);
var i = t.clientWidth,
c = t.clientHeight,
l = (L(r + i, d, d + a) - L(r, d, d + a)) / i,
s = (L(o + c, p, p + v) - L(o, p, p + v)) / c,
f = L((d - (i / 2 + r - a / 2)) / (a / 2), -1, 1),
u = L((p - (c / 2 + o - v / 2)) / (v / 2), -1, 1);
e.ctx = {
elementHeight: c,
elementWidth: i,
intersectX: 1 == l ? 0 : P(r - d),
intersectY: 1 == s ? 0 : P(o - p),
offsetX: r,
offsetY: o,
viewportX: f,
viewportY: u,
visibleX: l,
visibleY: s,
visible: +(h.offset ? h.offset <= p : (h.threshold || 0) < l * s),
};
});
},
Y =
((t = function () {
if (c) {
v(m, "_S", i) &&
(x(m, { scrollDirX: i.scrollDirX, scrollDirY: i.scrollDirY }),
a(m, i));
for (var e = c.length - 1; -1 < e; e--) {
var t = c[e],
n = t.$,
r = t.ctx,
o = r.visible;
v(n, "_SO", r) && a(n, r),
v(n, "_SV", o) &&
(x(n, { scroll: o ? "in" : "out" }),
s(n, r, m),
(o ? u : f)(n, r, m)),
o && h.once && c.splice(e, 1);
}
}
}),
S.push(t),
E || y(),
function () {
!(S = S.filter(function (e) {
return e != t;
})).length &&
E &&
cancelAnimationFrame(E);
});
return (
n(),
X(),
A[T](N, n),
g[T](W, X),
{
index: n,
update: X,
teardown: function () {
Y(), A[$](N, n), g[$](W, X);
},
}
);
};
})();

53
src/utils/storage.js Normal file
View File

@ -0,0 +1,53 @@
/**
* window.localStorage 浏览器永久缓存
* @method set 设置永久缓存
* @method get 获取永久缓存
* @method remove 移除永久缓存
* @method clear 移除全部永久缓存
*/
export const Local = {
// 设置永久缓存
set (key, val) {
window.localStorage.setItem(key, JSON.stringify(val));
},
// 获取永久缓存
get (key) {
let json = window.localStorage.getItem(key);
return json !== 'undefined' ? JSON.parse(json) : '';
},
// 移除永久缓存
remove (key) {
window.localStorage.removeItem(key);
},
// 移除全部永久缓存
clear () {
window.localStorage.clear();
},
};
/**
* window.sessionStorage 浏览器临时缓存
* @method set 设置临时缓存
* @method get 获取临时缓存
* @method remove 移除临时缓存
* @method clear 移除全部临时缓存
*/
export const Session = {
// 设置临时缓存
set (key, val) {
window.sessionStorage.setItem(key, JSON.stringify(val));
},
// 获取临时缓存
get (key) {
let json = window.sessionStorage.getItem(key);
return JSON.parse(json);
},
// 移除临时缓存
remove (key) {
window.sessionStorage.removeItem(key);
},
// 移除全部临时缓存
clear () {
window.sessionStorage.clear();
},
};

28
vite.config.ts Normal file
View File

@ -0,0 +1,28 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
AutoImport({
imports: [
'vue',
{
'naive-ui': [
'useDialog',
'useMessage',
'useNotification',
'useLoadingBar'
]
}
]
}),
Components({
resolvers: [NaiveUiResolver()]
})
]
})