create app

This commit is contained in:
李志鹏
2026-02-02 13:32:33 +08:00
commit 721cf2066a
48 changed files with 16194 additions and 0 deletions

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

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')
}
}