init
4
.env.development
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# VITE_APP_URL = http://192.168.31.82:8771
|
||||||
|
# VITE_APP_URL = http://18.167.251.121:10095
|
||||||
|
VITE_APP_URL = https://www.lc-api.aida.com.hk
|
||||||
|
VITE_GOOGLE_CLIENT_ID = 216037134725-7q8vqp0ohtmohlosltkfg7bd2v29rm5a.apps.googleusercontent.com
|
||||||
3
.env.production
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
VITE_APP_URL = https://www.lc-api.aida.com.hk
|
||||||
|
# VITE_APP_URL = http://18.167.251.121:10095
|
||||||
|
VITE_GOOGLE_CLIENT_ID = 29310152396-nnsd3h533fld665oguu8ovrt1nukmt46.apps.googleusercontent.com
|
||||||
32
.eslintrc.cjs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
/* eslint-env node */
|
||||||
|
require('@rushstack/eslint-patch/modern-module-resolution')
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
rules: {
|
||||||
|
// 忽略未使用的变量和参数
|
||||||
|
'@typescript-eslint/no-unused-vars': 'off',
|
||||||
|
'no-unused-vars': 'off',
|
||||||
|
|
||||||
|
// 或者设置为警告级别
|
||||||
|
// '@typescript-eslint/no-unused-vars': 'warn',
|
||||||
|
// 'no-unused-vars': 'warn',
|
||||||
|
|
||||||
|
// Vue 相关规则
|
||||||
|
'vue/multi-word-component-names': 'off',
|
||||||
|
'vue/no-unused-vars': 'off',
|
||||||
|
'vue/no-mutating-props': 'off',
|
||||||
|
'no-empty-pattern': 'off'
|
||||||
|
|
||||||
|
},
|
||||||
|
root: true,
|
||||||
|
'extends': [
|
||||||
|
'plugin:vue/vue3-essential',
|
||||||
|
'eslint:recommended',
|
||||||
|
'@vue/eslint-config-typescript',
|
||||||
|
'@vue/eslint-config-prettier/skip-formatting'
|
||||||
|
],
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 'latest'
|
||||||
|
},
|
||||||
|
globals: { defineOptions: 'readonly' }
|
||||||
|
}
|
||||||
29
.gitignore
vendored
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
components.d.ts*
|
||||||
|
auto-imports.d.ts*
|
||||||
|
node_modules
|
||||||
|
.DS_Store
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
coverage
|
||||||
|
*.local
|
||||||
|
|
||||||
|
/cypress/videos/
|
||||||
|
/cypress/screenshots/
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
8
.prettierrc.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/prettierrc",
|
||||||
|
"semi": false,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"singleQuote": true,
|
||||||
|
"printWidth": 100,
|
||||||
|
"trailingComma": "none"
|
||||||
|
}
|
||||||
28
README.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# 模板仓库
|
||||||
|
|
||||||
|
Vue3空白模板仓库,只集成了axios、pinia、gsap、crypto和eslint等风格化包
|
||||||
|
|
||||||
|
|
||||||
|
## Project Setup
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compile and Hot-Reload for Development
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Type-Check, Compile and Minify for Production
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lint with [ESLint](https://eslint.org/)
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run lint
|
||||||
|
```
|
||||||
35
env.d.ts
vendored
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
// Google Identity Services 类型声明
|
||||||
|
interface GoogleAccounts {
|
||||||
|
accounts: {
|
||||||
|
id: {
|
||||||
|
initialize: (config: {
|
||||||
|
client_id: string
|
||||||
|
auto_select?: boolean
|
||||||
|
callback: (response: { credential: string }) => void
|
||||||
|
ux_mode?: 'popup' | 'redirect'
|
||||||
|
itp_support?: boolean
|
||||||
|
}) => void
|
||||||
|
renderButton: (element: Element | null, config: {
|
||||||
|
type?: 'standard' | 'icon'
|
||||||
|
shape?: 'rectangular' | 'pill' | 'circle' | 'square'
|
||||||
|
theme?: 'outline' | 'filled_blue' | 'filled_black'
|
||||||
|
size?: 'large' | 'medium' | 'small'
|
||||||
|
logo_alignment?: 'left' | 'center'
|
||||||
|
}) => void
|
||||||
|
}
|
||||||
|
oauth2: {
|
||||||
|
initTokenClient: (config: {
|
||||||
|
client_id: string
|
||||||
|
callback: (response: { access_token: string }) => void
|
||||||
|
scope?: string
|
||||||
|
}) => void
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Window {
|
||||||
|
google?: GoogleAccounts
|
||||||
|
isAddGmail?: boolean
|
||||||
|
}
|
||||||
19
index.html
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<link rel="icon" href="/favicon.ico">
|
||||||
|
<!-- <meta name="viewport" content="width=device-width, initial-scale=1.0"> -->
|
||||||
|
<!-- <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"> -->
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=0" />
|
||||||
|
<link rel="stylesheet" href="/css/woff/fontFamily.css">
|
||||||
|
<title>FiDA</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
14660
package-lock.json
generated
Normal file
57
package.json
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
{
|
||||||
|
"name": "furniture",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"build-typeCheck": "run-p type-check build-only",
|
||||||
|
"type-check": "vue-tsc --noEmit",
|
||||||
|
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
|
||||||
|
"format": "prettier --write src/",
|
||||||
|
"postinstall": "husky install"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^1.3.6",
|
||||||
|
"crypto-js": "^4.2.0",
|
||||||
|
"gsap": "^3.13.0",
|
||||||
|
"normalize.css": "^8.0.1",
|
||||||
|
"pinia": "^2.0.32",
|
||||||
|
"pinia-persistedstate-plugin": "^0.1.0",
|
||||||
|
"pinia-plugin-persistedstate": "^3.1.0",
|
||||||
|
"vue": "^3.2.47",
|
||||||
|
"vue-i18n": "^11.2.8",
|
||||||
|
"vue-router": "^4.1.6"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@rushstack/eslint-patch": "^1.2.0",
|
||||||
|
"@types/crypto-js": "^4.2.2",
|
||||||
|
"@types/node": "^18.16.0",
|
||||||
|
"@vitejs/plugin-vue": "^4.0.0",
|
||||||
|
"@vue/eslint-config-prettier": "^7.1.0",
|
||||||
|
"@vue/eslint-config-typescript": "^11.0.2",
|
||||||
|
"@vue/tsconfig": "^0.1.3",
|
||||||
|
"eslint": "^8.34.0",
|
||||||
|
"eslint-config-prettier": "^8.8.0",
|
||||||
|
"eslint-plugin-prettier": "^4.2.1",
|
||||||
|
"eslint-plugin-vue": "^9.9.0",
|
||||||
|
"husky": "^8.0.3",
|
||||||
|
"less": "^4.3.0",
|
||||||
|
"lint-staged": "^13.2.1",
|
||||||
|
"npm-run-all": "^4.1.5",
|
||||||
|
"prettier": "^2.8.4",
|
||||||
|
"typescript": "~4.8.4",
|
||||||
|
"unplugin-auto-import": "^0.15.3",
|
||||||
|
"unplugin-vue-components": "^0.24.1",
|
||||||
|
"unplugin-vue-define-options": "^3.1.1",
|
||||||
|
"vite": "^4.1.4",
|
||||||
|
"vite-plugin-svg-icons": "^2.0.1",
|
||||||
|
"vue-tsc": "^1.2.0"
|
||||||
|
},
|
||||||
|
"lint-staged": {
|
||||||
|
"*.{vue,js}": [
|
||||||
|
"npm run lint"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
public/css/woff/Boska/Boska-Regular.ttf
Normal file
BIN
public/css/woff/Boska/Boska-Regular.woff
Normal file
BIN
public/css/woff/Mazzard/MazzardH-Regular.otf
Normal file
BIN
public/css/woff/Roboto/Roboto-Bold.ttf
Normal file
BIN
public/css/woff/Roboto/Roboto-Regular.ttf
Normal file
BIN
public/css/woff/Satoshi/Satoshi-Bold.ttf
Normal file
BIN
public/css/woff/Satoshi/Satoshi-Bold.woff
Normal file
BIN
public/css/woff/Satoshi/Satoshi-Medium.ttf
Normal file
BIN
public/css/woff/Satoshi/Satoshi-Medium.woff
Normal file
BIN
public/css/woff/Satoshi/Satoshi-Regular.ttf
Normal file
BIN
public/css/woff/Satoshi/Satoshi-Regular.woff
Normal file
50
public/css/woff/fontFamily.css
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
/* cyrillic-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'satoshiRegular';
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 700;
|
||||||
|
src: url("./Satoshi/Satoshi-Regular.ttf") format('woff2'), url("./Satoshi/Satoshi-Regular.woff") format('woff2');
|
||||||
|
/* unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; */
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'satoshiBold';
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 700;
|
||||||
|
src: url("./Satoshi/Satoshi-Bold.ttf") format('woff2'), url("./Satoshi/Satoshi-Bold.woff") format('woff2');
|
||||||
|
/* unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; */
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'satoshiMedium';
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 700;
|
||||||
|
src: url("./Satoshi/Satoshi-Medium.ttf") format('woff2'), url("./Satoshi/Satoshi-Medium.woff") format('woff2');
|
||||||
|
/* unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; */
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'mazzardHRegular';
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 700;
|
||||||
|
src: url("./Mazzard/MazzardH-Regular.otf") format('opentype');
|
||||||
|
/* unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; */
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'robotoBold';
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 700;
|
||||||
|
src: url("./Roboto/Roboto-Bold.ttf") format('woff2');
|
||||||
|
/* unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; */
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'robotoRegular';
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 700;
|
||||||
|
src: url("./Roboto/Roboto-Regular.ttf") format('woff2');
|
||||||
|
/* unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; */
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'boskaRegular';
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 700;
|
||||||
|
src: url("./Boska/Boska-Regular.ttf") format('woff2'), url("./Boska/Boska-Regular.woff") format('woff2');
|
||||||
|
/* unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; */
|
||||||
|
}
|
||||||
BIN
public/favicon.ico
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
public/image/nuic/style-1.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
public/image/nuic/style-2.png
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
public/image/nuic/style-3.png
Normal file
|
After Width: | Height: | Size: 71 KiB |
BIN
public/image/nuic/style-4.png
Normal file
|
After Width: | Height: | Size: 73 KiB |
BIN
public/image/nuic/style-5.png
Normal file
|
After Width: | Height: | Size: 71 KiB |
BIN
public/image/nuic/style-6.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
public/image/nuic/style-7.png
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
public/image/nuic/style-8.png
Normal file
|
After Width: | Height: | Size: 57 KiB |
13
src/App.vue
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<template>
|
||||||
|
<RouteCache />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import RouteCache from '@/components/RouteCache.vue'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less">
|
||||||
|
#app {
|
||||||
|
font-size: 1.6rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
30
src/api/template.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
|
||||||
|
// Google登录/注册参数类型
|
||||||
|
interface GoogleAuthParamsType {
|
||||||
|
accessToken?: string // Google ID Token (用于One Tap登录)
|
||||||
|
}
|
||||||
|
export const googleAuth = (data: GoogleAuthParamsType): Promise<LoginResponse> => {
|
||||||
|
return request({
|
||||||
|
url: '/api/auth/parseGoogleAccessToken',
|
||||||
|
method: 'get',
|
||||||
|
params: data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** 更改用户信息
|
||||||
|
* @param data 包含用户信息的对象
|
||||||
|
* @param data.username 用户名
|
||||||
|
* @param data.email 邮箱
|
||||||
|
* @param data.password 密码
|
||||||
|
* @returns 包含更新后的用户信息的对象
|
||||||
|
*/
|
||||||
|
export const updateUserInfo = (data: any) => {
|
||||||
|
return request({
|
||||||
|
url: '/api/auth/updateUserInfo',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
64
src/assets/css/style.css
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
body,
|
||||||
|
html,
|
||||||
|
div,
|
||||||
|
ul,
|
||||||
|
li,
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
html,
|
||||||
|
body,
|
||||||
|
#app {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
@keyframes loading {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.background-pink {
|
||||||
|
background-color: #f8f7f5;
|
||||||
|
background-image: url('@/assets/images/home-bg.png');
|
||||||
|
background-size: 100% 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-col {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-center {
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-1 {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.align-center {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.justify-center {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.space-between {
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
40
src/assets/css/style.less
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
body,
|
||||||
|
html,
|
||||||
|
div,
|
||||||
|
ul,
|
||||||
|
li,
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body,
|
||||||
|
#app {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes loading {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.background-pink {
|
||||||
|
background-color: rgba(248, 247, 245, 1);
|
||||||
|
background-image: url('@/assets/images/home-bg.png');
|
||||||
|
background-size: 100% 100%;
|
||||||
|
}
|
||||||
72
src/components/RouteCache.vue
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
<template>
|
||||||
|
<router-view v-slot="{ Component, route }">
|
||||||
|
<keep-alive :include="cachedViews">
|
||||||
|
<component :is="Component" :key="route.name" />
|
||||||
|
</keep-alive>
|
||||||
|
</router-view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch, onMounted } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
import MyEvent from '@/utils/myEvent'
|
||||||
|
|
||||||
|
const props = defineProps({})
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
// 缓存的组件名称列表
|
||||||
|
const cachedViews = ref<string[]>([])
|
||||||
|
|
||||||
|
// 监听路由变化,管理缓存
|
||||||
|
watch(
|
||||||
|
() => route,
|
||||||
|
(newRoute) => {
|
||||||
|
const routeName = newRoute.name as string
|
||||||
|
const shouldCache = newRoute.meta?.cache === true
|
||||||
|
|
||||||
|
// console.log('🔄 路由变化:', { routeName, shouldCache, currentCache: cachedViews.value })
|
||||||
|
|
||||||
|
if (shouldCache && routeName && !cachedViews.value.includes(routeName)) {
|
||||||
|
cachedViews.value.push(routeName)
|
||||||
|
// console.log('✅ 添加到缓存:', routeName)
|
||||||
|
} else if (!shouldCache && routeName && cachedViews.value.includes(routeName)) {
|
||||||
|
// 从缓存列表中移除
|
||||||
|
const index = cachedViews.value.indexOf(routeName)
|
||||||
|
if (index > -1) {
|
||||||
|
cachedViews.value.splice(index, 1)
|
||||||
|
// console.log('❌ 从缓存移除:', routeName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true, deep: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
// 提供清除缓存的方法
|
||||||
|
const clearCache = (routeName?: string) => {
|
||||||
|
if (routeName) {
|
||||||
|
const index = cachedViews.value.indexOf(routeName)
|
||||||
|
if (index > -1) {
|
||||||
|
cachedViews.value.splice(index, 1)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cachedViews.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
MyEvent.add('clearAllCache', clearCache)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 暴露方法供外部使用
|
||||||
|
defineExpose({
|
||||||
|
clearCache,
|
||||||
|
cachedViews
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.routeCache {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
50
src/components/SvgIcon/index.vue
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<template>
|
||||||
|
<div class="c-svg">
|
||||||
|
<svg
|
||||||
|
:class="svgClass"
|
||||||
|
v-bind="$attrs"
|
||||||
|
:style="{ color: color, fontSize: size/10 + 'rem' }"
|
||||||
|
>
|
||||||
|
<use :href="iconName"></use>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed } from "vue";
|
||||||
|
const props = defineProps({
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
type: [Number, String],
|
||||||
|
default: 16,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const iconName = computed(() => `#icon-${props.name}`);
|
||||||
|
const svgClass = computed(() => {
|
||||||
|
if (props.name) return `svg-icon icon-${props.name}`;
|
||||||
|
return "svg-icon";
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.svg-icon {
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
fill: currentColor;
|
||||||
|
color: var(--svg-icon-color);
|
||||||
|
}
|
||||||
|
.c-svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
5
src/env.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
declare module '*.vue' {
|
||||||
|
import { DefineComponent } from 'vue'
|
||||||
|
const component: DefineComponent<{}, {}, any>
|
||||||
|
export default component
|
||||||
|
}
|
||||||
3
src/lang/en.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export default {
|
||||||
|
|
||||||
|
}
|
||||||
27
src/lang/index.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { createI18n } from 'vue-i18n'
|
||||||
|
// 中文 zh-cn
|
||||||
|
// 英文 en
|
||||||
|
|
||||||
|
// 自己的语言配置
|
||||||
|
import enLocale from './en'
|
||||||
|
import zhLocale from './zh-cn'
|
||||||
|
|
||||||
|
// 语言配置整合
|
||||||
|
const messages = {
|
||||||
|
ENGLISH: {
|
||||||
|
...enLocale
|
||||||
|
},
|
||||||
|
CHINESE_SIMPLIFIED: {
|
||||||
|
...zhLocale
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建 i18n
|
||||||
|
const i18n = createI18n({
|
||||||
|
legacy: false,
|
||||||
|
globalInjection: true, // 全局模式,可以直接使用 $t
|
||||||
|
locale: 'ENGLISH',
|
||||||
|
messages: messages
|
||||||
|
})
|
||||||
|
|
||||||
|
export default i18n
|
||||||
95
src/lang/zh-cn.ts
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
export default {
|
||||||
|
Login: {
|
||||||
|
Login: '登录',
|
||||||
|
SignUp: '注册',
|
||||||
|
LoginTo: '登录到',
|
||||||
|
LoginTitle: '一个多智能体画布,用于快速、趋势驱动的设计迭代。',
|
||||||
|
name: '姓名',
|
||||||
|
email: '邮箱',
|
||||||
|
password: '密码',
|
||||||
|
enterName: '请输入姓名',
|
||||||
|
enterEmail: '请输入邮箱',
|
||||||
|
enterPassword: '请输入密码',
|
||||||
|
forgetPassword: '忘记密码?',
|
||||||
|
pleaseInputName: '请输入姓名',
|
||||||
|
nameLengthError: '姓名长度必须在 {min} 到 {max} 个字符之间',
|
||||||
|
pleaseInputEmail: '请输入邮箱',
|
||||||
|
emailFormatError: '请输入正确的邮箱',
|
||||||
|
pleaseInputPassword: '请输入密码',
|
||||||
|
passwordLengthError: '密码长度必须在 {min} 到 {max} 个字符之间',
|
||||||
|
pleaseTermsPolicy: '请同意条款、政策和费用',
|
||||||
|
agreeTermsPolicy: '我同意 <span onclick="onClickPrivacy()">条款、政策</span> 和费用。',
|
||||||
|
noAccountToSignUp: `还没有账号? <span onclick="onClickRegister()">注册</span>`,
|
||||||
|
registerFor: '注册账号',
|
||||||
|
registerTip: '一个多智能体画布,用于快速、趋势驱动的设计迭代。',
|
||||||
|
havenAccountToLogin: `已经有账号? <span onclick="onClickLogin()">登录</span>`,
|
||||||
|
verifyEmail: '验证您的邮箱地址',
|
||||||
|
verifyCodeHasSent: '已发送验证码到 <span>{email}</span>',
|
||||||
|
verifyCode: '请输入验证码',
|
||||||
|
verify: '验证',
|
||||||
|
resendCode: '重新发送验证码',
|
||||||
|
resendCodeIn: '重新发送验证码倒计时 {time}',
|
||||||
|
orContinueWith: '或者使用',
|
||||||
|
googleLogin: '使用 Google 登录',
|
||||||
|
wechatLogin: '使用微信登录',
|
||||||
|
},
|
||||||
|
Nuic: {
|
||||||
|
hiName: '你好,{name}。',
|
||||||
|
nuic1Title: `帮助 Fiphant 发现您空间中的 <b>'YOU'</b>。`,
|
||||||
|
nuic1Tip: `让我们设置您的个人资料。几个快速的细节将帮助 Fiphant 理解您的需求并找到您正在寻找的内容。`,
|
||||||
|
letsGo: '让我们开始,Fiphant!',
|
||||||
|
skip: '跳过',
|
||||||
|
next: '下一步',
|
||||||
|
nuic2Title: `您理想中 <b>家的氛围</b> 是什么?`,
|
||||||
|
loadMore: '加载更多',
|
||||||
|
nuic3Title: `您在哪里 <b>工作</b> ?您从事什么 <b>工作</b> ?`,
|
||||||
|
basedIn: '公司',
|
||||||
|
role: '角色',
|
||||||
|
allSet: '准备好了!',
|
||||||
|
},
|
||||||
|
Home: {
|
||||||
|
creditsNum: '积分: {num}',
|
||||||
|
newProject: '新建项目',
|
||||||
|
home: '首页',
|
||||||
|
history: '历史记录',
|
||||||
|
today: '今天',
|
||||||
|
yesterday: '昨天',
|
||||||
|
earlierChat: '更早的',
|
||||||
|
},
|
||||||
|
Input: {
|
||||||
|
placeholder: '请输入',
|
||||||
|
selectPlaceholder: '请选择',
|
||||||
|
type: '类型',
|
||||||
|
area: '地区',
|
||||||
|
style: '风格',
|
||||||
|
types: {
|
||||||
|
sofa: '沙发',
|
||||||
|
desk: '书桌',
|
||||||
|
chair: '椅子'
|
||||||
|
},
|
||||||
|
styles: {
|
||||||
|
modern: '现代',
|
||||||
|
classic: '古典'
|
||||||
|
},
|
||||||
|
chooseStyle: '选择风格',
|
||||||
|
setting: 'Setting',
|
||||||
|
settingOptions: {
|
||||||
|
creativity: '创意度',
|
||||||
|
diversity: '多样性',
|
||||||
|
relevance: '相关度'
|
||||||
|
},
|
||||||
|
confirm: '确认'
|
||||||
|
},
|
||||||
|
area: {
|
||||||
|
unitedStates: '美国',
|
||||||
|
singapore: '新加坡',
|
||||||
|
australia: '澳大利亚',
|
||||||
|
southKorea: '韩国',
|
||||||
|
china: '中国',
|
||||||
|
italy: '意大利',
|
||||||
|
france: '法国',
|
||||||
|
japan: '日本',
|
||||||
|
canada: '加拿大',
|
||||||
|
germany: '德国'
|
||||||
|
}
|
||||||
|
}
|
||||||
26
src/main.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { createApp } from 'vue'
|
||||||
|
|
||||||
|
import App from './App.vue'
|
||||||
|
import router from './router'
|
||||||
|
import store from './stores/index'
|
||||||
|
import 'normalize.css'
|
||||||
|
import './assets/css/style.css'
|
||||||
|
import SvgIcon from "@/components/SvgIcon/index.vue";
|
||||||
|
import "virtual:svg-icons-register";
|
||||||
|
|
||||||
|
import i18n from "./lang/index";
|
||||||
|
|
||||||
|
import flexible from "./utils/flexible.js";
|
||||||
|
|
||||||
|
import "./router/router-config" // 路由守卫,做动态路由的地方
|
||||||
|
|
||||||
|
const app = createApp(App)
|
||||||
|
app.use(router)
|
||||||
|
.use(store)
|
||||||
|
.component("SvgIcon", SvgIcon)
|
||||||
|
.use(i18n)
|
||||||
|
.mount('#app')
|
||||||
|
|
||||||
|
|
||||||
|
flexible();
|
||||||
|
|
||||||
31
src/router/index.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 路由缓存机制:
|
||||||
|
* 1. 设置路由的meta属性为{ cache: true },表示需要缓存
|
||||||
|
* 2. App.vue中使用RouteCache组件,通过路由的name来进行匹配
|
||||||
|
* 3. 路由的name默认是文件名,如果文件名与name不一致,通过defineOptions({ name: 'componentName' })来设置
|
||||||
|
*/
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory('/'),
|
||||||
|
// history: createWebHistory(import.meta.env.VITE_APP_URL),
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
redirect: '/index'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/index',
|
||||||
|
name: 'index',
|
||||||
|
component: () => import('../views/home/index.vue')
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
path: '/:pathMatch(.*)',
|
||||||
|
name: '404',
|
||||||
|
component: () => import('../views/404.vue')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
8
src/router/router-config.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import router from './index'
|
||||||
|
|
||||||
|
router.beforeEach((to, from, next) => {
|
||||||
|
next()
|
||||||
|
})
|
||||||
|
|
||||||
|
router.afterEach(() => {
|
||||||
|
})
|
||||||
20
src/stores/global.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
export const useGlobalStore = defineStore('global', () => {
|
||||||
|
const state = ref({
|
||||||
|
loading: false,// 全局loading
|
||||||
|
view_loading: false,// 页面跳转loading
|
||||||
|
homeLeftNavCollapse: false,// 首页左侧导航是否折叠
|
||||||
|
})
|
||||||
|
|
||||||
|
const setLoading = (v: boolean) => { state.value.loading = v }
|
||||||
|
const setViewLoading = (v: boolean) => { state.value.view_loading = v }
|
||||||
|
const setHomeLeftNavCollapse = (v: boolean) => { state.value.homeLeftNavCollapse = v }
|
||||||
|
|
||||||
|
return {
|
||||||
|
state,
|
||||||
|
setLoading,
|
||||||
|
setViewLoading,
|
||||||
|
setHomeLeftNavCollapse
|
||||||
|
}
|
||||||
|
})
|
||||||
8
src/stores/index.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { createPinia } from 'pinia'
|
||||||
|
import { createPersistedState } from 'pinia-persistedstate-plugin'
|
||||||
|
// 创建store实例
|
||||||
|
const store = createPinia()
|
||||||
|
// 使用持久化插件(全局持久化)
|
||||||
|
store.use(createPersistedState())
|
||||||
|
export default store
|
||||||
|
export * from './global'
|
||||||
45
src/types/api.d.ts
vendored
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
// 全局API响应类型定义
|
||||||
|
declare global {
|
||||||
|
// 基础API响应结构
|
||||||
|
interface ApiResponse<T = any> {
|
||||||
|
success: boolean
|
||||||
|
message: string
|
||||||
|
data?: T
|
||||||
|
code?: number
|
||||||
|
errMsg?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 登录/注册相关响应
|
||||||
|
interface LoginResponse {
|
||||||
|
token?: string
|
||||||
|
user?: {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
email: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通用列表响应
|
||||||
|
interface ListResponse<T> {
|
||||||
|
list: T[]
|
||||||
|
total: number
|
||||||
|
page: number
|
||||||
|
pageSize: number
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页参数
|
||||||
|
interface PaginationParams {
|
||||||
|
page: number
|
||||||
|
pageSize: number
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通用错误响应
|
||||||
|
interface ErrorResponse {
|
||||||
|
success: false
|
||||||
|
message: string
|
||||||
|
code?: number
|
||||||
|
errMsg?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {}
|
||||||
5
src/types/define-options.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
declare global {
|
||||||
|
const defineOptions: <T = Record<string, any>>(options: T) => T
|
||||||
|
}
|
||||||
|
|
||||||
|
export {}
|
||||||
18
src/types/enum.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
/** 流程类型 */
|
||||||
|
export const FlowType = {
|
||||||
|
/** 主流程 */
|
||||||
|
MAIN: 'main',
|
||||||
|
/** 历史流程 */
|
||||||
|
HISTORY: 'history',
|
||||||
|
/** 历史流程-Outfit */
|
||||||
|
H_OUTFIT: 'history-outfit',
|
||||||
|
/** 历史流程-Tryon */
|
||||||
|
H_TRYON: 'history-tryon',
|
||||||
|
/** 历史流程-AI */
|
||||||
|
H_AI: 'history-ai',
|
||||||
|
}
|
||||||
|
/** 是否是历史流程 */
|
||||||
|
export const IsHistoryFlow = (flowType: any) => {
|
||||||
|
const arr = [FlowType.HISTORY, FlowType.H_OUTFIT, FlowType.H_TRYON, FlowType.H_AI]
|
||||||
|
return arr.some((v) => v === flowType)
|
||||||
|
}
|
||||||
32
src/utils/flexible.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { getUniversalZoomLevel } from '@/utils/tools'
|
||||||
|
|
||||||
|
const maxWidth = 1920;
|
||||||
|
const minWidth = 500;
|
||||||
|
let flexible = (designWidth = 1920) => {
|
||||||
|
var doc = document, win = window, docEl = doc.documentElement, remStyle = document.createElement("style"), tid;
|
||||||
|
function refreshRem() {
|
||||||
|
var width = docEl.getBoundingClientRect().width;
|
||||||
|
var height = docEl.getBoundingClientRect().height;
|
||||||
|
width = getUniversalZoomLevel() * width
|
||||||
|
height = getUniversalZoomLevel() * height
|
||||||
|
if (width > maxWidth) width = maxWidth;
|
||||||
|
if (width < minWidth) width = minWidth;
|
||||||
|
var rem = (width * 10 / designWidth).toFixed(2);
|
||||||
|
docEl.style.fontSize = rem + 'px'
|
||||||
|
}
|
||||||
|
//要等 wiewport 设置好后才能执行 refreshRem,不然 refreshRem 会执行2次;
|
||||||
|
refreshRem();
|
||||||
|
win.addEventListener("resize", function () {
|
||||||
|
clearTimeout(tid); //防止执行两次
|
||||||
|
tid = setTimeout(refreshRem, 200);
|
||||||
|
}, false);
|
||||||
|
|
||||||
|
win.addEventListener("pageshow", function (e) {
|
||||||
|
if (e.persisted) { // 浏览器后退的时候重新计算
|
||||||
|
clearTimeout(tid);
|
||||||
|
tid = setTimeout(refreshRem, 200);
|
||||||
|
}
|
||||||
|
}, false);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default flexible
|
||||||
12
src/utils/local.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
function getLocal(key = 'token') {
|
||||||
|
return localStorage.getItem(key)
|
||||||
|
}
|
||||||
|
// 删除
|
||||||
|
function removeLocal(key = 'token') {
|
||||||
|
window.localStorage.removeItem(key)
|
||||||
|
}
|
||||||
|
// 保存
|
||||||
|
function setLocal(value: any, key = 'token') {
|
||||||
|
window.localStorage.setItem(key, value)
|
||||||
|
}
|
||||||
|
export { getLocal, removeLocal, setLocal }
|
||||||
15
src/utils/myEvent.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
class MyEvent {
|
||||||
|
static list = []
|
||||||
|
add(name, call) {
|
||||||
|
MyEvent.list.push({ name, call })
|
||||||
|
}
|
||||||
|
remove(name, call) {
|
||||||
|
MyEvent.list = MyEvent.list.filter(item => item.name != name && item.call != call)
|
||||||
|
}
|
||||||
|
emit(name, data) {
|
||||||
|
MyEvent.list.forEach(item => {
|
||||||
|
if (item.name == name) item.call(data)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export default new MyEvent()
|
||||||
191
src/utils/request.ts
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
import router from '@/router/index'
|
||||||
|
import { useGlobalStore, useUserInfoStore } from '@/stores'
|
||||||
|
|
||||||
|
// 扩展 AxiosRequestConfig 接口
|
||||||
|
declare module 'axios' {
|
||||||
|
interface AxiosRequestConfig {
|
||||||
|
loading?: boolean
|
||||||
|
loadingDom?: any
|
||||||
|
repeatRequest?: boolean
|
||||||
|
meta?: {
|
||||||
|
responseAll?: boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建axios实例
|
||||||
|
// console.log(import.meta.env,123)
|
||||||
|
|
||||||
|
const service = axios.create({
|
||||||
|
// baseURL: import.meta.env.VITE_APP_URL, // api的base_url
|
||||||
|
timeout: 60000 // 请求超时时间
|
||||||
|
})
|
||||||
|
if (import.meta.env.MODE != 'development') {
|
||||||
|
service.defaults.baseURL = import.meta.env.VITE_APP_URL
|
||||||
|
}
|
||||||
|
axios.defaults.headers.post['Content-Type'] = 'application/json'
|
||||||
|
axios.defaults.headers.post['lang'] = 'en' //配置语言请求头
|
||||||
|
axios.defaults.withCredentials = true //跨域携带cookie
|
||||||
|
|
||||||
|
// request拦截器
|
||||||
|
service.interceptors.request.use(
|
||||||
|
(config: any) => {
|
||||||
|
removePending(config)
|
||||||
|
// 如果repeatRequest不配置,那么默认该请求就取消重复接口请求
|
||||||
|
!config.repeatRequest && addPending(config)
|
||||||
|
// 打开loading
|
||||||
|
if (config.loading) {
|
||||||
|
LoadingInstance._count++
|
||||||
|
if (LoadingInstance._count === 1) {
|
||||||
|
openLoading(config.loadingDom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 如果登录了,有token,则请求携带token
|
||||||
|
// Do something before request is sent
|
||||||
|
const token = useUserInfoStore().state.token
|
||||||
|
if (token) {
|
||||||
|
config.headers.Authorization = token // 让每个请求携带token--['X-Token']为自定义key 请根据实际情况自行修改
|
||||||
|
// config.headers['X-Token'] = getLocal('token') // 让每个请求携带token--['X-Token']为自定义key 请根据实际情况自行修改
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
// Do something with request error
|
||||||
|
console.log(error) // for debug
|
||||||
|
Promise.reject(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// respone拦截器
|
||||||
|
service.interceptors.response.use(
|
||||||
|
// response => response,
|
||||||
|
/**
|
||||||
|
* 下面的注释为通过response自定义code来标示请求状态,当code返回如下情况为权限有问题,登出并返回到登录页
|
||||||
|
* 如通过xmlhttprequest 状态码标识 逻辑可写在下面error中
|
||||||
|
*/
|
||||||
|
(response: any) => {
|
||||||
|
// 如果是llm/streamChat这样的流式接口,不走这样的处理
|
||||||
|
if (response.config.url.includes('llm/streamChat')) {
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
// 已完成请求的删除请求中数组
|
||||||
|
removePending(response.config)
|
||||||
|
// 关闭loading
|
||||||
|
if (response.config.loading) {
|
||||||
|
closeLoading()
|
||||||
|
}
|
||||||
|
const res = response.data
|
||||||
|
// 处理异常的情况
|
||||||
|
// console.log(res)
|
||||||
|
if (res.code != 0) {
|
||||||
|
// showToast({
|
||||||
|
// message: res.errMsg || res.message,
|
||||||
|
// // type: 'fail',
|
||||||
|
// duration: 5000,
|
||||||
|
// position: 'top',
|
||||||
|
// icon: 'none'
|
||||||
|
// })
|
||||||
|
return Promise.reject(new Error(res.errMsg || res.message || 'error'))
|
||||||
|
} else {
|
||||||
|
// 默认只返回data,不返回状态码和message
|
||||||
|
// 通过 meta 中的 responseAll 配置来取决后台是否返回所有数据(包括状态码,message和data)
|
||||||
|
const isbackAll = response.config.meta && response.config.meta.responseAll
|
||||||
|
if (isbackAll) {
|
||||||
|
return res
|
||||||
|
} else {
|
||||||
|
return res.data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
if(error?.response){
|
||||||
|
if (error.config?.loading) closeLoading() // 关闭loading
|
||||||
|
if(error?.response?.status === 401){//如果是记录浏览器页面就不跳转login
|
||||||
|
// showConfirmDialog({
|
||||||
|
// title: '确定登出',
|
||||||
|
// message: '你已被登出,可以取消继续留在该页面,或者重新登录',
|
||||||
|
// confirmButtonText: '重新登录',
|
||||||
|
// cancelButtonText: '取消'
|
||||||
|
// }).then(() => {
|
||||||
|
// store.loginOut().then(() => {
|
||||||
|
// location.reload() // 为了重新实例化vue-router对象 避免bug
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
// showToast({
|
||||||
|
// message: 'Please log in and try again.',
|
||||||
|
// duration: 5000
|
||||||
|
// })
|
||||||
|
// router.push('/login')
|
||||||
|
// useGenerateStore().clearGenerateData()
|
||||||
|
return Promise.reject(false)
|
||||||
|
}
|
||||||
|
error.config && removePending(error.config)
|
||||||
|
console.log('err' + error) // for debug
|
||||||
|
// showToast({
|
||||||
|
// message: error.message,
|
||||||
|
// type: 'fail',
|
||||||
|
// duration: 5000
|
||||||
|
// })
|
||||||
|
}
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// --------------------------------取消接口重复请求的函数-----------------------------------
|
||||||
|
// axios.js
|
||||||
|
const pendingMap = new Map()
|
||||||
|
/**
|
||||||
|
* 生成每个请求唯一的键
|
||||||
|
* @param {*} config
|
||||||
|
* @returns string
|
||||||
|
*/
|
||||||
|
function getPendingKey(config: any) {
|
||||||
|
const { url, method, params } = config
|
||||||
|
let { data } = config
|
||||||
|
if (typeof data === 'string') data = JSON.parse(data) // response里面返回的config.data是个字符串对象
|
||||||
|
return [url, method, JSON.stringify(params), JSON.stringify(data)].join('&')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 储存每个请求唯一值, 也就是cancel()方法, 用于取消请求
|
||||||
|
* @param {*} config
|
||||||
|
*/
|
||||||
|
function addPending(config: any) {
|
||||||
|
const pendingKey = getPendingKey(config)
|
||||||
|
config.cancelToken =
|
||||||
|
config.cancelToken ||
|
||||||
|
new axios.CancelToken((cancel) => {
|
||||||
|
if (!pendingMap.has(pendingKey)) {
|
||||||
|
pendingMap.set(pendingKey, cancel)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 删除重复的请求
|
||||||
|
* @param {*} config
|
||||||
|
*/
|
||||||
|
function removePending(config: any) {
|
||||||
|
const pendingKey = getPendingKey(config)
|
||||||
|
if (pendingMap.has(pendingKey)) {
|
||||||
|
const cancelToken = pendingMap.get(pendingKey)
|
||||||
|
cancelToken(pendingKey)
|
||||||
|
pendingMap.delete(pendingKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ----------------------------------loading的函数-------------------------------
|
||||||
|
const LoadingInstance: { _count: number } = {
|
||||||
|
_count: 0
|
||||||
|
}
|
||||||
|
function openLoading(loadingDom: any) {
|
||||||
|
useGlobalStore().setLoading(true)
|
||||||
|
}
|
||||||
|
function closeLoading() {
|
||||||
|
if (LoadingInstance._count > 0) LoadingInstance._count--
|
||||||
|
if (LoadingInstance._count === 0) {
|
||||||
|
useGlobalStore().setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default service
|
||||||
198
src/utils/tools.ts
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
import CryptoJS from 'crypto-js'
|
||||||
|
|
||||||
|
function getUniversalZoomLevel() {
|
||||||
|
// 现代浏览器方案
|
||||||
|
if (window.visualViewport) {
|
||||||
|
return window.visualViewport.scale;
|
||||||
|
}
|
||||||
|
// 备用方案1
|
||||||
|
if (window.devicePixelRatio) {
|
||||||
|
return window.devicePixelRatio;
|
||||||
|
}
|
||||||
|
// 备用方案2(不精确)
|
||||||
|
return window.outerWidth / window.innerWidth;
|
||||||
|
}
|
||||||
|
const getMousePosition = (e: any, bor: any) => {
|
||||||
|
// if(e?.stopPropagation)e.stopPropagation()
|
||||||
|
// if(e?.preventDefault)e.preventDefault();
|
||||||
|
let event: any
|
||||||
|
if (bor) {
|
||||||
|
const touch = e.changedTouches[0] as any;
|
||||||
|
event = {
|
||||||
|
offsetX: touch.clientX - e.target.getBoundingClientRect().left,
|
||||||
|
offsetY: touch.clientY - e.target.getBoundingClientRect().top,
|
||||||
|
clientX: touch.clientX,
|
||||||
|
clientY: touch.clientY,
|
||||||
|
screenX: touch.screenX,
|
||||||
|
screenY: touch.screenY,
|
||||||
|
target: e.target,
|
||||||
|
}
|
||||||
|
// if(dom){
|
||||||
|
// event.offsetX = touch.clientX - dom.getBoundingClientRect().left
|
||||||
|
// event.offsetY = touch.clientY - dom.getBoundingClientRect().top
|
||||||
|
// }
|
||||||
|
} else {
|
||||||
|
event = {
|
||||||
|
offsetX: e.offsetX,
|
||||||
|
offsetY: e.offsetY,
|
||||||
|
clientX: e.clientX,
|
||||||
|
clientY: e.clientY,
|
||||||
|
screenX: e.screenX,
|
||||||
|
screenY: e.screenY,
|
||||||
|
target: e.target,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return event
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 生成UUID v4
|
||||||
|
* @returns 返回一个标准的UUID v4字符串,格式:xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
|
||||||
|
*/
|
||||||
|
export function generateUUID(): string {
|
||||||
|
// 优先使用现代浏览器的crypto.randomUUID()方法
|
||||||
|
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
||||||
|
return crypto.randomUUID()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 备用方案:手动生成UUID v4
|
||||||
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
||||||
|
const r = Math.random() * 16 | 0
|
||||||
|
const v = c === 'x' ? r : (r & 0x3 | 0x8)
|
||||||
|
return v.toString(16)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
getUniversalZoomLevel,
|
||||||
|
getMousePosition,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** 时间格式化-自定义格式
|
||||||
|
* @param value 时间对象|时间戳|时间字符串
|
||||||
|
* @param format 格式化字符串,默认值为 'yyyy-MM-dd HH:mm:ss'
|
||||||
|
* @returns 格式化后的时间字符串
|
||||||
|
*/
|
||||||
|
export function FormatDate(value: Date | number | string, format: string = 'yyyy-MM-dd HH:mm:ss') {
|
||||||
|
const date = new Date(value);
|
||||||
|
const yyyy = String(date.getFullYear());
|
||||||
|
const yy = String(date.getFullYear()).slice(-2);
|
||||||
|
const MM = String(date.getMonth() + 1).padStart(2, '0');
|
||||||
|
const M = String(date.getMonth() + 1);
|
||||||
|
const dd = String(date.getDate()).padStart(2, '0');
|
||||||
|
const d = String(date.getDate());
|
||||||
|
const HH = String(date.getHours()).padStart(2, '0');
|
||||||
|
const H = String(date.getHours());
|
||||||
|
const mm = String(date.getMinutes()).padStart(2, '0');
|
||||||
|
const m = String(date.getMinutes());
|
||||||
|
const ss = String(date.getSeconds()).padStart(2, '0');
|
||||||
|
const s = String(date.getSeconds());
|
||||||
|
const str = format.replaceAll('yyyy', yyyy)
|
||||||
|
.replaceAll('yy', yy)
|
||||||
|
.replaceAll('MM', MM)
|
||||||
|
.replaceAll('M', M)
|
||||||
|
.replaceAll('dd', dd)
|
||||||
|
.replaceAll('d', d)
|
||||||
|
.replaceAll('HH', HH)
|
||||||
|
.replaceAll('H', H)
|
||||||
|
.replaceAll('mm', mm)
|
||||||
|
.replaceAll('m', m)
|
||||||
|
.replaceAll('ss', ss)
|
||||||
|
.replaceAll('s', s);
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 倒计时
|
||||||
|
* @param time 倒计时时间,单位秒
|
||||||
|
* @returns 倒计时字符串,格式为 mm:ss
|
||||||
|
*/
|
||||||
|
export function CountDown(time: number) {
|
||||||
|
const mm = String(Math.floor(time / 60)).padStart(2, '0');
|
||||||
|
const ss = String(time % 60).padStart(2, '0');
|
||||||
|
return `${mm}:${ss}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载图片
|
||||||
|
* @param list 图片列表
|
||||||
|
* @param onProgress 下载进度回调
|
||||||
|
* @param onError 下载错误回调
|
||||||
|
* @param onSuccess 下载成功回调
|
||||||
|
*/
|
||||||
|
export async function DownloadImages(list: Array<{ url: string, name?: string }>, onProgress?: (count: number, total: number, item: any) => void, onError?: (count: number, total: number, item: any) => void, onSuccess?: (successCount: number, errCount: number) => void) {
|
||||||
|
const total = list.length;
|
||||||
|
let count = 0;
|
||||||
|
let successCount = 0;
|
||||||
|
let errCount = 0;
|
||||||
|
for (let i = 0; i < list.length; i++) {
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
xhr.open("GET", list[i].url);
|
||||||
|
xhr.responseType = "blob"
|
||||||
|
xhr.onload = function () {
|
||||||
|
count++;
|
||||||
|
if (this.status === 200) {
|
||||||
|
const blob = this.response;
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = URL.createObjectURL(blob);
|
||||||
|
a.download = list[i].name || list[i].url.split('/').pop().split('?').shift();
|
||||||
|
a.click();
|
||||||
|
successCount++;
|
||||||
|
typeof onProgress === "function" && onProgress(count, total, list[i]);
|
||||||
|
resolve(blob);
|
||||||
|
} else {
|
||||||
|
errCount++;
|
||||||
|
typeof onError === "function" && onError(count, total, list[i]);
|
||||||
|
resolve(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
xhr.onerror = function () {
|
||||||
|
count++;
|
||||||
|
errCount++;
|
||||||
|
typeof onError === "function" && onError(count, total, list[i]);
|
||||||
|
resolve(true);
|
||||||
|
};
|
||||||
|
xhr.send();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
typeof onSuccess === "function" && onSuccess(successCount, errCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MD5加密密码
|
||||||
|
* @param password 原始密码
|
||||||
|
* @returns MD5加密后的密码
|
||||||
|
*/
|
||||||
|
export function encryptPassword(password: string): string {
|
||||||
|
return CryptoJS.MD5(password).toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 图片分享到WhatsApp
|
||||||
|
* @param url 图片URL
|
||||||
|
* @returns 无
|
||||||
|
*/
|
||||||
|
export async function shareImageToWhatsapp (url: string){
|
||||||
|
// 把图片 URL 转为 Blob
|
||||||
|
const blob = await fetch(url).then((res) => res.blob())
|
||||||
|
|
||||||
|
// 创建文件对象
|
||||||
|
const file = new File([blob], 'image.jpg', { type: 'image/jpeg' })
|
||||||
|
|
||||||
|
// 判断浏览器是否支持文件分享
|
||||||
|
if (navigator.canShare && navigator.canShare({ files: [file] })) {
|
||||||
|
await navigator.share({
|
||||||
|
files: [file]
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// 你可以附加一些自定义文本
|
||||||
|
const message = 'share image ' + url
|
||||||
|
|
||||||
|
// 构造WhatsApp链接
|
||||||
|
const whatsappLink = `https://api.whatsapp.com/send/?text=${encodeURIComponent(message)}`
|
||||||
|
window.open(whatsappLink, '_blank')
|
||||||
|
}
|
||||||
|
}
|
||||||
21
src/views/404.vue
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<template>
|
||||||
|
<div class="view-404">
|
||||||
|
<p>404 Not Found</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less">
|
||||||
|
.view-404 {
|
||||||
|
text-align: center;
|
||||||
|
> p {
|
||||||
|
margin-top: 5rem;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 3rem;
|
||||||
|
color: #0094ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
9
src/views/home/index.vue
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<template>
|
||||||
|
test view
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
</style>
|
||||||
25
tsconfig.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"extends": "@vue/tsconfig/tsconfig.web.json",
|
||||||
|
"include": ["env.d.ts", "src/**/*", "src/**/*.vue", "src/types/**/*.d.ts"],
|
||||||
|
"compilerOptions": {
|
||||||
|
"forceConsistentCasingInFileNames": false, // ⚠️ 禁用大小写检查
|
||||||
|
"allowJs": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"types": ["node", "unplugin-vue-define-options/macros-global"],
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"],
|
||||||
|
"_c/*": ["./src/components/*"]
|
||||||
|
},
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": false,
|
||||||
|
"noEmit": true,
|
||||||
|
"noImplicitAny": false,
|
||||||
|
"noEmitOnError": false
|
||||||
|
},
|
||||||
|
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.node.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
9
tsconfig.node.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"extends": "@vue/tsconfig/tsconfig.node.json",
|
||||||
|
"include": ["vite.config.*", "vitest.config.*", "cypress.config.*", "playwright.config.*"],
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"types": ["node"],
|
||||||
|
"skipLibCheck": true
|
||||||
|
}
|
||||||
|
}
|
||||||
78
vite.config.ts
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import { fileURLToPath, URL } from 'node:url'
|
||||||
|
|
||||||
|
import { defineConfig, loadEnv } from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
import AutoImport from 'unplugin-auto-import/vite'
|
||||||
|
import Components from 'unplugin-vue-components/vite'
|
||||||
|
import DefineOptions from 'unplugin-vue-define-options/vite'
|
||||||
|
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
|
||||||
|
import path from 'path'
|
||||||
|
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
|
||||||
|
|
||||||
|
// console.log(process)
|
||||||
|
// console.log(import.meta.env.VITE_APP_URL)
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
|
||||||
|
export default defineConfig(({ mode }) => {
|
||||||
|
const env = loadEnv(mode, process.cwd())
|
||||||
|
|
||||||
|
return {
|
||||||
|
plugins: [
|
||||||
|
vue(),
|
||||||
|
DefineOptions(),
|
||||||
|
// ...
|
||||||
|
AutoImport({
|
||||||
|
resolvers: [ElementPlusResolver()],
|
||||||
|
}),
|
||||||
|
Components({
|
||||||
|
resolvers: [ElementPlusResolver()],
|
||||||
|
}),
|
||||||
|
createSvgIconsPlugin({
|
||||||
|
// 指定需要缓存的图标文件夹
|
||||||
|
iconDirs: [path.resolve(process.cwd(), 'src/assets/icons')],
|
||||||
|
// 指定symbolId格式
|
||||||
|
symbolId: 'icon-[dir]-[name]',
|
||||||
|
inject: 'body-last' // 注入位置优化
|
||||||
|
})
|
||||||
|
],
|
||||||
|
define: {
|
||||||
|
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: false,
|
||||||
|
},
|
||||||
|
css: {
|
||||||
|
preprocessorOptions: {
|
||||||
|
less: {
|
||||||
|
modifyVars: {
|
||||||
|
'primary-color': '#ec6800'
|
||||||
|
},
|
||||||
|
javascriptEnabled: true,
|
||||||
|
// 全局导入less变量文件
|
||||||
|
additionalData: `@import "${path.resolve(__dirname, 'src/assets/css/style.less')}";`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||||
|
_c: fileURLToPath(new URL('./src/components', import.meta.url))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
host: '0.0.0.0', // 允许局域网内的IP访问
|
||||||
|
port: 8060, // 根据环境设置端口
|
||||||
|
open: true, // 自动打开浏览器
|
||||||
|
strictPort: true, // 如果端口已被占用,则尝试下一个可用端口
|
||||||
|
hmr: {
|
||||||
|
overlay: true
|
||||||
|
},
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
//'/api'是自行设置的请求前缀
|
||||||
|
target: env.VITE_APP_URL,
|
||||||
|
changeOrigin: true, //用于控制请求头中的host值
|
||||||
|
rewrite: (path) => path.replace(/^\/api/, '/api') //路径重写,(正则)匹配以api开头的路径为空(将请求前缀删除)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||