create app
This commit is contained in:
31
src/App.vue
Normal file
31
src/App.vue
Normal file
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<RouteCache />
|
||||
<div id="loading" v-if="loading" v-loading="true"></div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import RouteCache from '@/components/RouteCache.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;
|
||||
}
|
||||
#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>
|
||||
77
src/api/login.ts
Normal file
77
src/api/login.ts
Normal 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
|
||||
})
|
||||
}
|
||||
11
src/assets/css/style.css
Normal file
11
src/assets/css/style.css
Normal file
@@ -0,0 +1,11 @@
|
||||
* {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
html,
|
||||
body,
|
||||
#app {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
12
src/assets/css/style.less
Normal file
12
src/assets/css/style.less
Normal file
@@ -0,0 +1,12 @@
|
||||
* {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#app {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
72
src/components/RouteCache.vue
Normal file
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
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
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
|
||||
}
|
||||
26
src/main.ts
Normal file
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 './assets/css/style.css'
|
||||
import SvgIcon from "@/components/SvgIcon/index.vue";
|
||||
import "virtual:svg-icons-register";
|
||||
|
||||
|
||||
import flexible from "./utils/flexible.js";
|
||||
|
||||
import "./router/router-config" // 路由守卫,做动态路由的地方
|
||||
import ElementPlus from 'element-plus'
|
||||
import 'element-plus/dist/index.css'
|
||||
|
||||
const app = createApp(App)
|
||||
app.use(router)
|
||||
.use(ElementPlus)
|
||||
.use(store)
|
||||
.component("SvgIcon", SvgIcon)
|
||||
.mount('#app')
|
||||
|
||||
|
||||
flexible();
|
||||
|
||||
32
src/router/index.ts
Normal file
32
src/router/index.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
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'),
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
export default router
|
||||
8
src/router/router-config.ts
Normal file
8
src/router/router-config.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import router from './index'
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
next()
|
||||
})
|
||||
|
||||
router.afterEach(() => {
|
||||
})
|
||||
14
src/stores/global.ts
Normal file
14
src/stores/global.ts
Normal 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
9
src/stores/index.ts
Normal 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
70
src/stores/userInfo.ts
Normal 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
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
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
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
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
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
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
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
|
||||
186
src/utils/tools.ts
Normal file
186
src/utils/tools.ts
Normal 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
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>
|
||||
Reference in New Issue
Block a user