初始化

This commit is contained in:
李志鹏
2026-04-20 11:21:21 +08:00
commit 8d28482783
58 changed files with 16867 additions and 0 deletions

3
.env.development Normal file
View File

@@ -0,0 +1,3 @@
# 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

2
.env.production Normal file
View File

@@ -0,0 +1,2 @@
VITE_APP_URL = https://www.lc-api.aida.com.hk
# VITE_APP_URL = http://18.167.251.121:10095

32
.eslintrc.cjs Normal file
View 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
View 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
View File

@@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/prettierrc",
"semi": false,
"tabWidth": 2,
"singleQuote": true,
"printWidth": 100,
"trailingComma": "none"
}

46
README.md Normal file
View File

@@ -0,0 +1,46 @@
# vue3demo
This template should help get you started developing with Vue 3 in Vite.
## Recommended IDE Setup
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
## Type Support for `.vue` Imports in TS
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types.
If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps:
1. Disable the built-in TypeScript Extension
1) Run `Extensions: Show Built-in Extensions` from VSCode's command palette
2) Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`
2. Reload the VSCode window by running `Developer: Reload Window` from the command palette.
## Customize configuration
See [Vite Configuration Reference](https://vitejs.dev/config/).
## 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
View 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
}

24
index.html Normal file
View File

@@ -0,0 +1,24 @@
<!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>Lane Crawford</title>
<!-- Open Graph / WhatsApp share metadata -->
<meta property="og:title" content="Lane Crawford" />
<meta property="og:description" content="create and share looks from the Lane Crawford creation gallery." />
<meta property="og:image" content="" />
<meta property="og:url" content="https://www.lc.aida.com.hk" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="Lane Crawford" />
<meta name="twitter:card" content="summary_large_image" />
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

15041
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

60
package.json Normal file
View File

@@ -0,0 +1,60 @@
{
"name": "aida_buyer",
"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",
"element-plus": "^2.13.2",
"gsap": "^3.13.0",
"vue-i18n": "^11.2.8",
"markdown-it": "^14.1.0",
"md5": "^2.3.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-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"
]
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

40
src/App.vue Normal file
View File

@@ -0,0 +1,40 @@
<template>
<main-header />
<div class="view"><RouteCache /></div>
<div id="loading" v-if="loading" v-loading="true"></div>
</template>
<script setup lang="ts">
import RouteCache from '@/components/RouteCache.vue'
import MainHeader from '@/views/main-header.vue'
import { computed } from 'vue'
import { useGlobalStore } from '@/stores'
const globalStore = useGlobalStore()
const loading = computed(() => globalStore.state.loading)
</script>
<style lang="less">
#app {
font-size: 1.6rem;
display: flex;
flex-direction: column;
}
#loading {
position: fixed;
z-index: 999999999;
top: 0;
left: 0;
width: 100%;
height: 100%;
// background-color: rgba(0, 0, 0, 0.3);
// display: flex;
// align-items: center;
// justify-content: center;
}
</style>
<style lang="less" scoped>
.view {
flex: 1;
overflow: hidden;
}
</style>

77
src/api/login.ts Normal file
View File

@@ -0,0 +1,77 @@
import request from '@/utils/request'
interface LoginParamsType {
name?: string // 姓名
email: string // 邮箱
password?: string // 密码
operationType: 'REGISTER' | 'LOGIN' | 'FORGET_PWD'
verifyCode?: string // 验证码
}
// 发送验证码
export const precheckEmail = (params: { email: string }): Promise<ApiResponse> => {
return request({
url: '/api/auth/precheckEmail',
method: 'get',
params
})
}
export const fetchRegisterOrLogin = (data: LoginParamsType): Promise<LoginResponse> => {
return request({
url: '/api/auth/registerOrLogin',
method: 'post',
data
})
}
export const resetPassword = (data: LoginParamsType): Promise<ApiResponse> => {
return request({
url: '/api/auth/forgotPwd',
method: 'post',
data
})
}
export const checkLoginStatus = (): Promise<ApiResponse<LoginResponse>> => {
return request({
url: '/api/auth/checkLoginStatus',
method: 'get',
meta: { responseAll: true }
})
}
export const LogOut = (): Promise<ApiResponse> => {
return request({
url: '/api/auth/logout',
method: 'get'
})
}
// 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
})
}

114
src/assets/css/style.css Normal file
View File

@@ -0,0 +1,114 @@
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;
font-family: 'Medium';
}
@keyframes loading {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
@keyframes opacity-in {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes z-index-10to-1 {
0% {
z-index: 10;
}
100% {
z-index: -1;
}
}
.flex {
display: flex;
}
.flex-center {
justify-content: center;
align-items: center;
}
.flex-1 {
flex: 1;
}
.flex-col {
flex-direction: column;
}
.align-center {
align-items: center;
}
.space-between {
justify-content: space-between;
}
.justify-center {
justify-content: center;
}
.relative {
position: relative;
}
.el-overlay {
--el-color-primary: #ff7a51;
--el-color-primary-light-3: #ffa785;
--el-color-primary-light-5: #ffc2aa;
--el-color-primary-light-7: #ffddcf;
--el-color-primary-light-8: #ffe8df;
--el-color-primary-light-9: #fff2ec;
--el-color-primary-dark-2: #cc6241;
}
.el-select,
.el-popper {
--el-color-primary: #6c6c6c;
/* 主灰色 */
--el-color-primary-light-3: #8a8a8a;
/* 较浅的灰色混合20%白) */
--el-color-primary-light-5: #a8a8a8;
/* 更浅的灰色混合33%白) */
--el-color-primary-light-7: #c6c6c6;
/* 浅灰色混合47%白) */
--el-color-primary-light-8: #d4d4d4;
/* 很浅的灰色混合53%白) */
--el-color-primary-light-9: #e3e3e3;
/* 极浅的灰色混合60%白) */
--el-color-primary-dark-2: #565656;
/* 深灰色加深20% */
}
.mini-scrollbar::-webkit-scrollbar {
width: 0.4rem;
}
.mini-scrollbar::-webkit-scrollbar-thumb {
border-radius: 0.4rem;
background: rgba(0, 0, 0, 0.2);
}
.mosaic-bg {
--mosaic-bg-size: 1rem;
--mosaic-bg-color1: #efefef;
--mosaic-bg-color2: #fff;
background-image: repeating-conic-gradient(var(--mosaic-bg-color1) 0% 25%, var(--mosaic-bg-color2) 0% 50%);
background-repeat: repeat;
background-position: 50% 50%;
background-size: var(--mosaic-bg-size) var(--mosaic-bg-size);
}

129
src/assets/css/style.less Normal file
View File

@@ -0,0 +1,129 @@
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;
font-family: 'Medium';
}
@keyframes loading {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
@keyframes opacity-in {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes z-index-10to-1 {
0% {
z-index: 10;
}
100% {
z-index: -1;
}
}
.flex {
display: flex;
}
.flex-center {
justify-content: center;
align-items: center;
}
.flex-1 {
flex: 1;
}
.flex-col {
flex-direction: column;
}
.align-center {
align-items: center;
}
.space-between {
justify-content: space-between;
}
.justify-center {
justify-content: center;
}
.relative {
position: relative;
}
.el-overlay {
--el-color-primary: #ff7a51; // 主橙红色
--el-color-primary-light-3: #ffa785; // 较浅的橙红混合20%白)
--el-color-primary-light-5: #ffc2aa; // 更浅的橙红混合33%白)
--el-color-primary-light-7: #ffddcf; // 浅橙红混合47%白)
--el-color-primary-light-8: #ffe8df; // 很浅的橙红混合53%白)
--el-color-primary-light-9: #fff2ec; // 极浅的橙红混合60%白)
--el-color-primary-dark-2: #cc6241; // 深橙红加深20%
}
.el-select, .el-popper{
--el-color-primary: #6c6c6c; /* 主灰色 */
--el-color-primary-light-3: #8a8a8a; /* 较浅的灰色混合20%白) */
--el-color-primary-light-5: #a8a8a8; /* 更浅的灰色混合33%白) */
--el-color-primary-light-7: #c6c6c6; /* 浅灰色混合47%白) */
--el-color-primary-light-8: #d4d4d4; /* 很浅的灰色混合53%白) */
--el-color-primary-light-9: #e3e3e3; /* 极浅的灰色混合60%白) */
--el-color-primary-dark-2: #565656; /* 深灰色加深20% */
}
// 迷你滚动条
.mini-scrollbar {
&::-webkit-scrollbar {
width: 0.4rem;
}
&::-webkit-scrollbar-thumb {
border-radius: 0.4rem;
background: rgba(0, 0, 0, 0.2);
}
}
.mosaic-bg{
--mosaic-bg-size: 1rem;
--mosaic-bg-color1: #efefef;
--mosaic-bg-color2: #fff;
background-image: repeating-conic-gradient(var(--mosaic-bg-color1) 0% 25%, var(--mosaic-bg-color2) 0% 50%);
background-repeat: repeat;
background-position: 50% 50%;
background-size: var(--mosaic-bg-size) var(--mosaic-bg-size);
}

View File

@@ -0,0 +1,5 @@
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M22.0052 17.5L25.311 7.58337H7.42189L6.25522 4.08337H2.91797V5.25004H5.41405L9.29264 16.8864L6.68689 22.75H22.7513V21.5834H8.48239L10.2971 17.5H22.0052ZM23.6916 8.75004L21.1641 16.3334H10.3386L7.81097 8.75004H23.6916Z" fill="#232323"/>
<path d="M11.6667 25.6666C12.311 25.6666 12.8333 25.1443 12.8333 24.5C12.8333 23.8556 12.311 23.3333 11.6667 23.3333C11.0223 23.3333 10.5 23.8556 10.5 24.5C10.5 25.1443 11.0223 25.6666 11.6667 25.6666Z" fill="#232323"/>
<path d="M18.6667 25.6666C19.311 25.6666 19.8333 25.1443 19.8333 24.5C19.8333 23.8556 19.311 23.3333 18.6667 23.3333C18.0223 23.3333 17.5 23.8556 17.5 24.5C17.5 25.1443 18.0223 25.6666 18.6667 25.6666Z" fill="#232323"/>
</svg>

After

Width:  |  Height:  |  Size: 788 B

View File

@@ -0,0 +1,5 @@
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M22.0052 17.5002L25.311 7.5835H7.42189L6.25522 4.0835H2.91797V5.25016H5.41405L9.29264 16.8865L6.68689 22.7502H22.7513V21.5835H8.48239L10.2971 17.5002H22.0052Z" fill="#232323"/>
<path d="M11.6667 25.6668C12.311 25.6668 12.8333 25.1445 12.8333 24.5002C12.8333 23.8558 12.311 23.3335 11.6667 23.3335C11.0223 23.3335 10.5 23.8558 10.5 24.5002C10.5 25.1445 11.0223 25.6668 11.6667 25.6668Z" fill="#232323"/>
<path d="M18.6667 25.6668C19.311 25.6668 19.8333 25.1445 19.8333 24.5002C19.8333 23.8558 19.311 23.3335 18.6667 23.3335C18.0223 23.3335 17.5 23.8558 17.5 24.5002C17.5 25.1445 18.0223 25.6668 18.6667 25.6668Z" fill="#232323"/>
</svg>

After

Width:  |  Height:  |  Size: 741 B

View File

@@ -0,0 +1,4 @@
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19.8333 23.3334H8.16667C7.52233 23.3334 7 22.811 7 22.1667V17.1444C7 16.7198 7.23155 16.3319 7.62754 16.1786C8.60644 15.7996 10.7306 15.1667 14 15.1667C17.2694 15.1667 19.3936 15.7996 20.3725 16.1786C20.7684 16.3319 21 16.7198 21 17.1444V22.1667C21 22.811 20.4777 23.3334 19.8333 23.3334Z" stroke="#232323" stroke-width="1.16667" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="13.9987" cy="8.16667" r="4.08333" stroke="#232323" stroke-width="1.16667"/>
</svg>

After

Width:  |  Height:  |  Size: 580 B

View File

@@ -0,0 +1,4 @@
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19.8333 23.3332H8.16667C7.52233 23.3332 7 22.8108 7 22.1665V17.1442C7 16.7196 7.23155 16.3317 7.62754 16.1784C8.60644 15.7994 10.7306 15.1665 14 15.1665C17.2694 15.1665 19.3936 15.7994 20.3725 16.1784C20.7684 16.3317 21 16.7196 21 17.1442V22.1665C21 22.8108 20.4777 23.3332 19.8333 23.3332Z" fill="#232323" stroke="#232323" stroke-width="1.16667" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="13.9987" cy="8.16667" r="4.08333" fill="#232323" stroke="#232323" stroke-width="1.16667"/>
</svg>

After

Width:  |  Height:  |  Size: 612 B

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View 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>

View 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>

9
src/directives/index.js Normal file
View File

@@ -0,0 +1,9 @@
export default {
install(app) {
const directivesList = import.meta.glob('./*.js', { eager: true });
// 遍历指令文件实现自动注册
Object.keys(directivesList).forEach(key => {
app.directive(directivesList[key].default.name, directivesList[key].default);
});
}
};

16
src/directives/loadimg.js Normal file
View File

@@ -0,0 +1,16 @@
// 加载图片
export default {
name: 'loadimg',
mounted(el, binding) {
const src = binding.value
if (el.src === src) return
const img = new Image()
img.src = src
img.onload = () => {
el.src = src
}
img.onerror = () => {
console.log('图片加载失败:', src)
}
},
};

5
src/env.d.ts vendored Normal file
View File

@@ -0,0 +1,5 @@
declare module '*.vue' {
import { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}

6
src/lang/en.ts Normal file
View File

@@ -0,0 +1,6 @@
export default {
Login: {
login: 'Log in',
register: 'Register',
}
}

34
src/lang/index.ts Normal file
View File

@@ -0,0 +1,34 @@
import { createI18n } from 'vue-i18n'
// 中文 zh-cn
// 英文 en
// element-plus 中的语言配置
import elementEnLocale from './en'
import elementZhLocale from './zh-cn'
// 自己的语言配置
import enLocale from './en'
import zhLocale from './zh-cn'
// 语言配置整合
const messages = {
'ENGLISH':{
...enLocale,
...elementEnLocale
},
'CHINESE_SIMPLIFIED':{
...zhLocale,
...elementZhLocale
},
}
// 创建 i18n
const i18n = createI18n({
warnHtmlMessage: false,
legacy: false,
globalInjection:true, // 全局模式,可以直接使用 $t
locale: localStorage.getItem('language') || 'ENGLISH',
messages: messages
})
export default i18n

6
src/lang/zh-cn.ts Normal file
View File

@@ -0,0 +1,6 @@
export default {
Login: {
login: '登录',
register: '注册',
}
}

29
src/main.ts Normal file
View File

@@ -0,0 +1,29 @@
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 directives from "./directives/index.js";
import i18n from "./lang/index";
import flexible from "./utils/flexible.js";
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
const app = createApp(App)
app.use(router)
.use(directives)
.use(ElementPlus)
.use(store)
.component("SvgIcon", SvgIcon)
.use(i18n)
.mount('#app')
flexible();

39
src/router/index.ts Normal file
View File

@@ -0,0 +1,39 @@
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: '/welcome'
// },
// {
// path: '/asistant',
// name: 'asistant',
// component: () => import('../views/asistant/index.vue'),
// meta: { cache: true, verify: () => VerifyIDs(2) }
// },
{
path: '/:pathMatch(.*)',
name: '404',
component: () => import('../views/404.vue'),
},
]
})
router.beforeEach((to, from, next) => {
next()
})
router.afterEach(() => {
})
export default router

14
src/stores/global.ts Normal file
View File

@@ -0,0 +1,14 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useGlobalStore = defineStore('global', () => {
const state = ref({
loading: false
})
const setLoading = (v: boolean) => { state.value.loading = v }
return {
state,
setLoading,
}
})

9
src/stores/index.ts Normal file
View File

@@ -0,0 +1,9 @@
import { createPinia } from 'pinia'
import { createPersistedState } from 'pinia-persistedstate-plugin'
// 创建store实例
const store = createPinia()
// 使用持久化插件(全局持久化)
store.use(createPersistedState())
export default store
export * from './global'
export * from './userInfo'

70
src/stores/userInfo.ts Normal file
View File

@@ -0,0 +1,70 @@
// 每一个存储的模块命名规则use开头store结尾
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { removeLocal, setLocal } from '@/utils/local'
import MyEvent from '@/utils/myEvent'
export const useUserInfoStore = defineStore('userInfo', () => {
const state = ref({
userInfo: {},
token: '',
generateParams: {
stylist: '',
sex: '',
stylistImage: ''
}
})
// getters
const getUserInfo = computed(() => state.value.userInfo)
// actions
const setUserInfo = (data: any) => {
state.value.userInfo = data
}
const setToken = (data: string) => {
state.value.token = data
setLocal(data, 'token')
}
const getGenerateParams = () => {
return state.value.generateParams
}
const setGenerateParams = (data: any) => {
state.value.generateParams = data
}
const resetGenerateParams = () => {
state.value.generateParams = {
stylist: '',
sex: '',
stylistImage: ''
}
}
const logOut = () => {
// 处理退出登录的一些逻辑
return new Promise((resolve) => {
state.value.token = ''
state.value.userInfo = {}
removeLocal('token')
resetGenerateParams()
MyEvent.emit('clear-generate-state')
MyEvent.emit('clear-client-state')
MyEvent.emit('clearAllCache')
resolve('')
})
}
return {
state,
getUserInfo,
setToken,
setUserInfo,
setGenerateParams,
getGenerateParams,
resetGenerateParams,
logOut
}
})

45
src/types/api.d.ts vendored Normal file
View 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
View File

@@ -0,0 +1,5 @@
declare global {
const defineOptions: <T = Record<string, any>>(options: T) => T
}
export {}

18
src/types/enum.ts Normal file
View 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)
}

38
src/utils/flexible.js Normal file
View File

@@ -0,0 +1,38 @@
import { getUniversalZoomLevel } from '@/utils/tools'
const maxWidth = 1920;
const minWidth = 500;
const maxHeight = 1080;
const minHeight = 500;
let flexible = (designWidth = 1920, designHeight = 1080) => {
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;
if (height > maxHeight) height = maxHeight;
if (height < minHeight) height = minHeight;
const wrem = (width * 10 / designWidth).toFixed(2);
const hrem = (height * 10 / designHeight).toFixed(2);
const rem = Math.min(wrem, hrem);
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
View 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 }

37
src/utils/myEvent.js Normal file
View File

@@ -0,0 +1,37 @@
class MyEvent {
constructor() {
// 使用 Object 或 Map 存储,实现 O(1) 级别的查找
this.events = new Map()
}
add(name, call) {
if (!this.events.has(name)) {
this.events.set(name, [])
}
this.events.get(name).push(call)
}
remove(name, call) {
if (!this.events.has(name)) return
if (!call) {
this.events.delete(name)
} else {
const callbacks = this.events.get(name)
const index = callbacks.indexOf(call)
if (index !== -1) {
callbacks.splice(index, 1)
}
// 如果该事件没有监听者了,彻底清理 key
if (callbacks.length === 0) {
this.events.delete(name)
}
}
}
emit(name, data) {
const callbacks = this.events.get(name)
if (callbacks) {
// 使用 slice() 镜像一份副本,防止在执行回调过程中有 remove 操作导致索引错乱
callbacks.slice().forEach((cb) => cb(data))
}
}
}
export default new MyEvent()

183
src/utils/request.ts Normal file
View File

@@ -0,0 +1,183 @@
import axios from 'axios'
import router from '@/router/index'
import { useGlobalStore, useUserInfoStore } from '@/stores'
import { ElMessage } from 'element-plus'
// 扩展 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) => {
if (removePending(config) && config.loading) closeLoading()
// 如果repeatRequest不配置那么默认该请求就取消重复接口请求
!config.repeatRequest && addPending(config)
// 打开loading
if (config.loading) openLoading(config.loadingDom)
// 如果登录了有token则请求携带token
// Do something before request is sent
const token = useUserInfoStore().state.token
if (token) {
config.headers.Authorization = 'Bearer ' + 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 != 200) {
ElMessage.error(res.message)
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
// })
// })
ElMessage({
type: 'error',
message: 'Please log in and try again.',
duration: 5000
})
router.push('/login')
useUserInfoStore().logOut(false)
return Promise.reject(false)
}
error.config && removePending(error.config)
// console.log('err', error) // for debug
ElMessage.error(error.response?.data?.message || error.message)
}
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)
// return true
// }
}
// ----------------------------------loading的函数-------------------------------
const LoadingInstance: { _count: number } = {
_count: 0
}
function openLoading(loadingDom: any) {
LoadingInstance._count++
// if(LoadingInstance._count === 1) {
useGlobalStore().setLoading(true)
// }
}
function closeLoading() {
if (LoadingInstance._count > 0) LoadingInstance._count--
if (LoadingInstance._count === 0) {
useGlobalStore().setLoading(false)
}
}
export default service

186
src/utils/tools.ts Normal file
View File

@@ -0,0 +1,186 @@
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 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
View 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>

143
src/views/main-header.vue Normal file
View File

@@ -0,0 +1,143 @@
<template>
<div class="main-header" id="main-header">
<div class="left">
<img class="logo" src="@/assets/images/logo.png" alt="" />
</div>
<div class="center">
<div
v-for="v in navList1"
:key="v.path"
class="nav-item"
:class="{ active: activePath === v.path }"
@click="onNavItemClick(v.path)"
>
<span>{{ v.name }}</span>
</div>
</div>
<div class="right">
<div
class="icon"
v-for="v in navList2"
:key="v.path"
:class="{ active: activePath === v.path }"
@click="onNavItemClick(v.path)"
>
<svg-icon :name="activePath === v.path ? v.active_icon : v.icon" size="22" />
</div>
<div class="login">Login</div>
<div class="profile"></div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import { useRouter, useRoute } from 'vue-router'
const router = useRouter()
const route = useRoute()
const activePath = computed(() => route.path)
const navList1 = ref([
{
name: 'Home',
path: '/'
},
{
name: 'Collection Story',
path: '/collection-story'
},
{
name: 'Brand',
path: '/brand'
},
{
name: 'Digital Item',
path: '/digital-item'
}
])
const navList2 = ref([
{
icon: 'cart_0',
active_icon: 'cart_1',
path: '/cart'
},
{
icon: 'user_0',
active_icon: 'user_1',
path: '/user'
}
])
const onNavItemClick = (path: string) => {
if (path === activePath.value) return
router.push(path)
}
</script>
<style lang="less">
#main-header {
height: 8rem;
display: flex;
align-items: center;
justify-content: space-between;
position: relative;
background: #ffffff;
padding: 0 9rem;
border-bottom: 0.1rem solid #c4c4c4;
> .left {
> .logo {
width: auto;
height: 4rem;
}
}
> .right {
> .icon {
width: 2.4rem;
height: 2.4rem;
cursor: pointer;
}
> .profile {
width: 4rem;
height: 4rem;
border-radius: 50%;
background: #f5f5f5;
cursor: pointer;
}
}
> .center,
> .right {
display: flex;
align-items: center;
justify-content: center;
gap: 2.6rem;
}
> .center {
position: absolute;
height: 80%;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
> .nav-item {
height: 100%;
user-select: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
> span {
padding: 0 1rem;
line-height: 2.4rem;
font-size: 1.6rem;
color: #232323;
border-bottom: 0.1rem solid transparent;
font-family: Kaisei Opti;
}
&.active {
> span {
border-bottom-color: #232323;
font-weight: 700;
}
}
}
}
}
</style>

25
tsconfig.json Normal file
View 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
View 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
View 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: false, // 自动打开浏览器
strictPort: true, // 如果端口已被占用,则尝试下一个可用端口
hmr: {
overlay: true
},
proxy: {
'/api': {
//'/api'是自行设置的请求前缀
target: env.VITE_APP_URL,
changeOrigin: true, //用于控制请求头中的host值
rewrite: (path) => path.replace(/^\/api/, '/api') //路径重写正则匹配以api开头的路径为空将请求前缀删除
}
}
}
}
})