This commit is contained in:
2026-02-06 15:10:17 +08:00
commit f011de4ef4
59 changed files with 16075 additions and 0 deletions

4
.env.development Normal file
View 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
View 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
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"
}

28
README.md Normal file
View 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
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
}

19
index.html Normal file
View 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

File diff suppressed because it is too large Load Diff

57
package.json Normal file
View 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"
]
}
}

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

13
src/App.vue Normal file
View 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
View 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
View 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
View 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%;
}

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>

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
}

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

@@ -0,0 +1,3 @@
export default {
}

27
src/lang/index.ts Normal file
View 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
View 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
View 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
View 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

View File

@@ -0,0 +1,8 @@
import router from './index'
router.beforeEach((to, from, next) => {
next()
})
router.afterEach(() => {
})

20
src/stores/global.ts Normal file
View 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
View 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
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)
}

32
src/utils/flexible.js Normal file
View 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
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 }

15
src/utils/myEvent.js Normal file
View 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
View 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
View 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
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>

9
src/views/home/index.vue Normal file
View File

@@ -0,0 +1,9 @@
<template>
test view
</template>
<script setup>
</script>
<style lang="scss" scoped>
</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: true, // 自动打开浏览器
strictPort: true, // 如果端口已被占用,则尝试下一个可用端口
hmr: {
overlay: true
},
proxy: {
'/api': {
//'/api'是自行设置的请求前缀
target: env.VITE_APP_URL,
changeOrigin: true, //用于控制请求头中的host值
rewrite: (path) => path.replace(/^\/api/, '/api') //路径重写正则匹配以api开头的路径为空将请求前缀删除
}
}
}
}
})