84 changed files with 4002 additions and 190 deletions
@ -0,0 +1,8 @@ |
|||||||
|
# 网站标题 |
||||||
|
VITE_GLOB_APP_TITLE = 青鸟语言大模型-同聪 |
||||||
|
|
||||||
|
# 简称,用于配置文件名字 不要出现空格、数字开头等特殊字符 |
||||||
|
VITE_GLOB_APP_SHORT_NAME = 同聪 |
||||||
|
|
||||||
|
# token key |
||||||
|
VITE_GLOB_APP_TOKEN_KEY = "hulk-Auth" |
@ -0,0 +1,10 @@ |
|||||||
|
# 本地开发环境 |
||||||
|
|
||||||
|
# 公共地址 |
||||||
|
VITE_GLOB_BASE_URL = "http://localhost:48080" |
||||||
|
|
||||||
|
# 本地MQTT地址 |
||||||
|
VITE_GLOB_MQTT_URL = "http://localhost:48080" |
||||||
|
|
||||||
|
# 接口授权标识 |
||||||
|
VITE_GLOB_APP_AUTHORIZATION = "ZmFsY29uOmZhbGNvbl9zZWNyZXQ=" |
@ -0,0 +1,10 @@ |
|||||||
|
# 正式环境 |
||||||
|
|
||||||
|
# 公共地址 |
||||||
|
VITE_GLOB_BASE_URL = "http://223.99.228.207:19872" |
||||||
|
|
||||||
|
# 本地MQTT地址 |
||||||
|
VITE_GLOB_MQTT_URL = "http://localhost:48080" |
||||||
|
|
||||||
|
# 接口授权标识 |
||||||
|
VITE_GLOB_APP_AUTHORIZATION = "ZmFsY29uOmZhbGNvbl9zZWNyZXQ=" |
@ -1,48 +1,30 @@ |
|||||||
<script setup lang="ts"> |
<script setup lang="ts"> |
||||||
import { computed, ref } from 'vue' |
import { computed } from 'vue' |
||||||
import { ConfigProvider, theme } from 'ant-design-vue' |
import { App, ConfigProvider } from 'ant-design-vue' |
||||||
import zhCN from 'ant-design-vue/es/locale/zh_CN' |
import zhCN from 'ant-design-vue/es/locale/zh_CN' |
||||||
|
|
||||||
const { darkAlgorithm } = theme |
|
||||||
const darkTheme = { |
|
||||||
algorithm: [darkAlgorithm], |
|
||||||
} |
|
||||||
const isDark = ref(true) |
|
||||||
const themeConfig = computed(() => |
const themeConfig = computed(() => |
||||||
Object.assign( |
Object.assign( |
||||||
{ |
{ |
||||||
token: { |
token: { |
||||||
colorPrimary: '#0960bd', |
colorPrimary: '#4670E3', |
||||||
colorSuccess: '#55D187', |
colorSuccess: '#55D187', |
||||||
colorWarning: '#EFBD47', |
colorWarning: '#EFBD47', |
||||||
colorError: '#ED6F6F', |
colorError: '#ED6F6F', |
||||||
colorInfo: '#0960bd', |
colorInfo: '#4670E3', |
||||||
}, |
}, |
||||||
}, |
}, |
||||||
isDark.value ? darkTheme : {}, |
|
||||||
), |
), |
||||||
) |
) |
||||||
</script> |
</script> |
||||||
|
|
||||||
<template> |
<template> |
||||||
<ConfigProvider :locale="zhCN" locale-data="zh-CN" :theme="themeConfig"> |
<ConfigProvider :locale="zhCN" locale-data="zh-CN" :theme="themeConfig"> |
||||||
<RouterView /> |
<App class="h-full w-full"> |
||||||
|
<RouterView /> |
||||||
|
</App> |
||||||
</ConfigProvider> |
</ConfigProvider> |
||||||
</template> |
</template> |
||||||
|
|
||||||
<style scoped> |
<style scoped> |
||||||
.logo { |
|
||||||
height: 6em; |
|
||||||
padding: 1.5em; |
|
||||||
will-change: filter; |
|
||||||
transition: filter 300ms; |
|
||||||
} |
|
||||||
|
|
||||||
.logo:hover { |
|
||||||
filter: drop-shadow(0 0 2em #646cffaa); |
|
||||||
} |
|
||||||
|
|
||||||
.logo.vue:hover { |
|
||||||
filter: drop-shadow(0 0 2em #42b883aa); |
|
||||||
} |
|
||||||
</style> |
</style> |
||||||
|
@ -0,0 +1,23 @@ |
|||||||
|
import { defHttp } from '@/utils/axios/index' |
||||||
|
|
||||||
|
export interface TokenParams { |
||||||
|
user_type: string |
||||||
|
grant_type: string |
||||||
|
invite_code: string |
||||||
|
phone: string |
||||||
|
phoneCode: string |
||||||
|
type: string |
||||||
|
} |
||||||
|
export async function token(params: TokenParams) { |
||||||
|
return defHttp.post({ |
||||||
|
url: `/hulk-auth/oauth/token?grant_type=${params.grant_type}&user_type=${params.user_type}&invite_code=${params.invite_code}&phone=${params.phone}&phoneCode=${params.phoneCode}&type=${params.type}`, |
||||||
|
}, { |
||||||
|
isTransformResponse: false, |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
export async function sendCode(phone: string) { |
||||||
|
return defHttp.post({ |
||||||
|
url: `/open-chat/unauth/sendSms?phone=${phone}`, |
||||||
|
}) |
||||||
|
} |
After Width: | Height: | Size: 183 KiB |
After Width: | Height: | Size: 19 KiB |
@ -0,0 +1,3 @@ |
|||||||
|
import AppContainerBox from './index.vue' |
||||||
|
|
||||||
|
export { AppContainerBox } |
@ -0,0 +1,22 @@ |
|||||||
|
<script setup lang="ts"> |
||||||
|
import { AppSubMenuBox } from '@/components/AppSubMenuBox' |
||||||
|
import { AppContentBox } from '@/components/AppContentBox' |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<div class="app-container-box h-full flex justify-between items-center"> |
||||||
|
<AppSubMenuBox> |
||||||
|
<slot name="subMenu"></slot> |
||||||
|
</AppSubMenuBox> |
||||||
|
<AppContentBox> |
||||||
|
<slot name="content"></slot> |
||||||
|
</AppContentBox> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
@include app('container-box') { |
||||||
|
padding: 20px 0 20px 0; |
||||||
|
background-color: #edf3ff; |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,3 @@ |
|||||||
|
import AppContentBox from './index.vue' |
||||||
|
|
||||||
|
export { AppContentBox } |
@ -0,0 +1,17 @@ |
|||||||
|
<script setup lang="ts"> |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<div class="app-content-box h-full"> |
||||||
|
<slot></slot> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
@include app('content-box') { |
||||||
|
width: calc(100% - $sub-menu-width); |
||||||
|
padding: 30px; |
||||||
|
border-radius: 30px 0 0 30px; |
||||||
|
background-color: #ffffff; |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,3 @@ |
|||||||
|
import AppSubMenuBox from './index.vue' |
||||||
|
|
||||||
|
export { AppSubMenuBox } |
@ -0,0 +1,14 @@ |
|||||||
|
<script setup lang="ts"> |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<div class="app-sub-menu-box h-full"> |
||||||
|
<slot></slot> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
@include app('sub-menu-box') { |
||||||
|
width: $sub-menu-width; |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,5 @@ |
|||||||
|
export interface SubMenuItem { |
||||||
|
title: string |
||||||
|
content: string |
||||||
|
id: string |
||||||
|
} |
@ -0,0 +1,3 @@ |
|||||||
|
import AppSubMenuList from './index.vue' |
||||||
|
|
||||||
|
export { AppSubMenuList } |
@ -0,0 +1,51 @@ |
|||||||
|
<script setup lang="ts"> |
||||||
|
import type { SubMenuItem } from './index.d' |
||||||
|
|
||||||
|
defineProps<{ |
||||||
|
list: SubMenuItem[] |
||||||
|
activeIndex: number |
||||||
|
}>() |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<div class="app-sub-menu-list"> |
||||||
|
<div |
||||||
|
v-for="(item, index) in list" |
||||||
|
:key="item.id" |
||||||
|
class="app-sub-menu-list-item" |
||||||
|
:class="[activeIndex === index && 'app-sub-menu-list-item-active']" |
||||||
|
> |
||||||
|
<p class="title"> |
||||||
|
{{ item.title }} |
||||||
|
</p> |
||||||
|
<p class="content truncate"> |
||||||
|
{{ item.content }} |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
@include app('sub-menu-list') { |
||||||
|
&-item { |
||||||
|
padding: 10px 10px 10px 20px; |
||||||
|
border-radius: 10px 0 0 10px; |
||||||
|
.title { |
||||||
|
font-weight: bold; |
||||||
|
font-size: 14px; |
||||||
|
margin: 0; |
||||||
|
} |
||||||
|
.content { |
||||||
|
color: #888c90; |
||||||
|
margin: 0; |
||||||
|
font-size: 12px; |
||||||
|
} |
||||||
|
} |
||||||
|
&-item-active { |
||||||
|
background: #e1e9f9; |
||||||
|
} |
||||||
|
&-item:hover { |
||||||
|
background: #e1e9f9; |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,3 @@ |
|||||||
|
import AppSubMenuTitle from './index.vue' |
||||||
|
|
||||||
|
export { AppSubMenuTitle } |
@ -0,0 +1,36 @@ |
|||||||
|
<script setup lang="ts"> |
||||||
|
defineProps({ |
||||||
|
title: { |
||||||
|
type: String, |
||||||
|
default: '全部会话', |
||||||
|
}, |
||||||
|
}) |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<div class="app-sub-menu-title"> |
||||||
|
{{ title }} |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
@include app('sub-menu-title') { |
||||||
|
font-size: 15px; |
||||||
|
font-weight: bold; |
||||||
|
color: #1a1414; |
||||||
|
position: relative; |
||||||
|
padding-left: 20px; |
||||||
|
z-index: 1; |
||||||
|
&::after { |
||||||
|
content: ''; |
||||||
|
position: absolute; |
||||||
|
left: 20px; |
||||||
|
bottom: 1px; |
||||||
|
z-index: -1; |
||||||
|
width: 50%; |
||||||
|
height: 8px; |
||||||
|
background: linear-gradient(90deg, #4670e3 0%, rgba(53, 109, 228, 0) 100%); |
||||||
|
border-radius: 4px; |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
@ -1,41 +0,0 @@ |
|||||||
<script setup lang="ts"> |
|
||||||
import { ref } from 'vue' |
|
||||||
|
|
||||||
defineProps<{ msg: string }>() |
|
||||||
|
|
||||||
const count = ref(0) |
|
||||||
</script> |
|
||||||
|
|
||||||
<template> |
|
||||||
<h1>{{ msg }}</h1> |
|
||||||
|
|
||||||
<div class="card"> |
|
||||||
<button type="button" @click="count++"> |
|
||||||
count is {{ count }} |
|
||||||
</button> |
|
||||||
<p> |
|
||||||
Edit |
|
||||||
<code>components/HelloWorld.vue</code> to test HMR |
|
||||||
</p> |
|
||||||
</div> |
|
||||||
|
|
||||||
<p> |
|
||||||
Check out |
|
||||||
<a href="https://vuejs.org/guide/quick-start.html#local" target="_blank">create-vue</a>, the official Vue + Vite |
|
||||||
starter |
|
||||||
</p> |
|
||||||
<p> |
|
||||||
Install |
|
||||||
<a href="https://github.com/vuejs/language-tools" target="_blank">Volar</a> |
|
||||||
in your IDE for a better DX |
|
||||||
</p> |
|
||||||
<p class="read-the-docs"> |
|
||||||
Click on the Vite and Vue logos to learn more |
|
||||||
</p> |
|
||||||
</template> |
|
||||||
|
|
||||||
<style scoped> |
|
||||||
.read-the-docs { |
|
||||||
color: #888; |
|
||||||
} |
|
||||||
</style> |
|
@ -0,0 +1,3 @@ |
|||||||
|
import SvgIcon from './index.vue' |
||||||
|
|
||||||
|
export { SvgIcon } |
@ -0,0 +1,51 @@ |
|||||||
|
<script lang="ts"> |
||||||
|
import { computed, defineComponent } from 'vue' |
||||||
|
|
||||||
|
export default defineComponent({ |
||||||
|
name: 'SvgIcon', |
||||||
|
props: { |
||||||
|
prefix: { |
||||||
|
type: String, |
||||||
|
default: 'icon', |
||||||
|
}, |
||||||
|
name: { |
||||||
|
type: String, |
||||||
|
required: true, |
||||||
|
}, |
||||||
|
color: { |
||||||
|
type: String, |
||||||
|
default: '', |
||||||
|
}, |
||||||
|
className: { |
||||||
|
type: String, |
||||||
|
default: '', |
||||||
|
}, |
||||||
|
}, |
||||||
|
setup(props) { |
||||||
|
const symbolId = computed(() => `#${props.prefix}-${props.name}`) |
||||||
|
const svgClass = computed(() => { |
||||||
|
if (props.className) |
||||||
|
return `svg-icon ${props.className}` |
||||||
|
|
||||||
|
return 'svg-icon' |
||||||
|
}) |
||||||
|
return { symbolId, svgClass } |
||||||
|
}, |
||||||
|
}) |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<svg :class="svgClass" aria-hidden="true"> |
||||||
|
<use :href="symbolId" :fill="color" /> |
||||||
|
</svg> |
||||||
|
</template> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
.svg-icon { |
||||||
|
width: 1em; |
||||||
|
height: 1em; |
||||||
|
vertical-align: -0.15em; |
||||||
|
fill: currentColor !important; |
||||||
|
overflow: hidden; |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,72 @@ |
|||||||
|
@import "./mixins/config.scss"; |
||||||
|
@import "./mixins/mixins.scss"; |
||||||
|
:-webkit-autofill { |
||||||
|
transition: background-color 5000s ease-in-out 0s !important; |
||||||
|
} |
||||||
|
|
||||||
|
html { |
||||||
|
overflow: hidden; |
||||||
|
text-size-adjust: 100%; |
||||||
|
} |
||||||
|
|
||||||
|
html, |
||||||
|
body { |
||||||
|
width: 100%; |
||||||
|
height: 100%; |
||||||
|
overflow: visible; |
||||||
|
overflow-x: hidden; |
||||||
|
color: var(--text-color); |
||||||
|
|
||||||
|
&.color-weak { |
||||||
|
filter: invert(80%); |
||||||
|
} |
||||||
|
|
||||||
|
&.gray-mode { |
||||||
|
filter: grayscale(100%); |
||||||
|
filter: progid:dximagetransform.microsoft.basicimage(grayscale=1); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
a:focus, |
||||||
|
a:active, |
||||||
|
button, |
||||||
|
div, |
||||||
|
svg, |
||||||
|
span { |
||||||
|
outline: none; |
||||||
|
} |
||||||
|
|
||||||
|
/** 打包后夜间模式样式有问题 在这里覆盖 */ |
||||||
|
html[data-theme="dark"] { |
||||||
|
/** 菜单边框 */ |
||||||
|
ul li { |
||||||
|
border: none; |
||||||
|
} |
||||||
|
|
||||||
|
ul li:hover { |
||||||
|
color: inherit !important; |
||||||
|
border: none; |
||||||
|
box-shadow: none; |
||||||
|
} |
||||||
|
|
||||||
|
/** 日期输入框 */ |
||||||
|
.ant-picker-input > input { |
||||||
|
border: none; |
||||||
|
} |
||||||
|
|
||||||
|
.ant-picker-input > input:focus { |
||||||
|
color: inherit !important; |
||||||
|
box-shadow: none; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.ant-input-number { |
||||||
|
width: 100%; |
||||||
|
} |
||||||
|
|
||||||
|
// 保持 和 windi 一样的全局样式,减少升级带来的影响 |
||||||
|
ul { |
||||||
|
padding: 0; |
||||||
|
margin: 0; |
||||||
|
list-style: none; |
||||||
|
} |
@ -0,0 +1,3 @@ |
|||||||
|
$namespace: "app"; |
||||||
|
$menu-width: 80px; |
||||||
|
$sub-menu-width: 180px; |
@ -0,0 +1,6 @@ |
|||||||
|
@mixin app($block) { |
||||||
|
$B: $namespace + "-" + $block; |
||||||
|
.#{$B} { |
||||||
|
@content; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,49 @@ |
|||||||
|
#app { |
||||||
|
width: 100%; |
||||||
|
height: 100%; |
||||||
|
} |
||||||
|
|
||||||
|
// ================================= |
||||||
|
// ==============scrollbar========== |
||||||
|
// ================================= |
||||||
|
|
||||||
|
::-webkit-scrollbar { |
||||||
|
width: 7px; |
||||||
|
height: 8px; |
||||||
|
} |
||||||
|
|
||||||
|
// ::-webkit-scrollbar-track { |
||||||
|
// background: transparent; |
||||||
|
// } |
||||||
|
|
||||||
|
::-webkit-scrollbar-track { |
||||||
|
background-color: rgba($color: #000000, $alpha: 0.5); |
||||||
|
} |
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb { |
||||||
|
background: rgba($color: #000000, $alpha: 0.6); |
||||||
|
background-color: rgba($color: #9093994d, $alpha: 0.3); |
||||||
|
border-radius: 2px; |
||||||
|
box-shadow: inset 0 0 6px rgba($color: #000000, $alpha: 0.2); |
||||||
|
} |
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover { |
||||||
|
background-color: var(--border-color); |
||||||
|
} |
||||||
|
|
||||||
|
// ================================= |
||||||
|
// ==============nprogress========== |
||||||
|
// ================================= |
||||||
|
#nprogress { |
||||||
|
pointer-events: none; |
||||||
|
|
||||||
|
.bar { |
||||||
|
position: fixed; |
||||||
|
top: 0; |
||||||
|
left: 0; |
||||||
|
z-index: 99999; |
||||||
|
width: 100%; |
||||||
|
height: 2px; |
||||||
|
opacity: 0.75; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,12 @@ |
|||||||
|
// token key
|
||||||
|
export const ACCESS_TOKEN_KEY = 'ACCESS_TOKEN' |
||||||
|
|
||||||
|
// user info key
|
||||||
|
export const USER_INFO_KEY = 'USER_INFO' |
||||||
|
|
||||||
|
export const USET_STORE_KEY = 'USER_STORE' |
||||||
|
|
||||||
|
export enum CatchTypeEnum { |
||||||
|
ACCESS_TOKEN_KEY, |
||||||
|
USER_INFO_KEY, |
||||||
|
} |
@ -0,0 +1,14 @@ |
|||||||
|
export enum UserTypeEnum { |
||||||
|
WEB = 'web', |
||||||
|
C = 'c', |
||||||
|
} |
||||||
|
|
||||||
|
export enum GrantTypeEnum { |
||||||
|
PASSWORD = 'password', |
||||||
|
CAPTCHA = 'captcha', |
||||||
|
SMS = 'sms', |
||||||
|
} |
||||||
|
|
||||||
|
export enum TypeEnum { |
||||||
|
PHONE = 'phone', |
||||||
|
} |
@ -0,0 +1,55 @@ |
|||||||
|
/** |
||||||
|
* @description: request method |
||||||
|
*/ |
||||||
|
export enum RequestEnum { |
||||||
|
GET = 'GET', |
||||||
|
POST = 'POST', |
||||||
|
PUT = 'PUT', |
||||||
|
DELETE = 'DELETE', |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @description: contentType |
||||||
|
*/ |
||||||
|
export enum ContentTypeEnum { |
||||||
|
// json
|
||||||
|
JSON = 'application/json;charset=UTF-8', |
||||||
|
// form-data qs
|
||||||
|
FORM_URLENCODED = 'application/x-www-form-urlencoded;charset=UTF-8', |
||||||
|
// form-data upload
|
||||||
|
FORM_DATA = 'multipart/form-data;charset=UTF-8', |
||||||
|
} |
||||||
|
|
||||||
|
export enum ResultEnum { |
||||||
|
SUCCESS = 200, |
||||||
|
ERROR = -1, |
||||||
|
TIMEOUT = 400, |
||||||
|
UNAUTHORIZED = 401, |
||||||
|
INTERNAL_SERVER_ERROR = 500, |
||||||
|
TYPE = 'success', |
||||||
|
} |
||||||
|
|
||||||
|
export enum HttpErrorMsgEnum { |
||||||
|
ERROR_TIP = '错误提示', |
||||||
|
API_REQUEST_FAILED = '请求出错,请稍候重试', |
||||||
|
API_TIMEOUT_MESSAGE = '接口请求超时,请刷新页面重试!', |
||||||
|
|
||||||
|
NETWORK_EXCEPTION = '网络异常,请稍候重试', |
||||||
|
|
||||||
|
ERROR_MESSAGE_401 = '用户没有权限(令牌、用户名、密码错误)!', |
||||||
|
ERROR_MESSAGE_403 = '用户得到授权,但是访问是被禁止的。!', |
||||||
|
ERROR_MESSAGE_404 = '网络请求错误,未找到该资源!', |
||||||
|
ERROR_MESSAGE_405 = '网络请求错误,请求方法未允许!', |
||||||
|
ERROR_MESSAGE_408 = '请求超时!', |
||||||
|
ERROR_MESSAGE_500 = '服务器错误,请联系管理员!', |
||||||
|
ERROR_MESSAGE_501 = '网络请求错误,未实现!', |
||||||
|
ERROR_MESSAGE_502 = '网络请求错误,网关错误!', |
||||||
|
ERROR_MESSAGE_503 = '服务不可用,服务器暂时过载或维护!', |
||||||
|
ERROR_MESSAGE_504 = '网络请求错误,网关超时!', |
||||||
|
ERROR_MESSAGE_505 = 'http版本不受支持!', |
||||||
|
} |
||||||
|
|
||||||
|
export enum HttpSuccessEnum { |
||||||
|
SUCCESS_TIP = '成功提示', |
||||||
|
OPERATION_SUCCESS = '操作成功', |
||||||
|
} |
@ -0,0 +1,22 @@ |
|||||||
|
export enum MenuTypeEnum { |
||||||
|
// 会话
|
||||||
|
CONVERSATION = 'conversation', |
||||||
|
|
||||||
|
// 文生图
|
||||||
|
TEXT_TO_PICTURE = 'textToPicture', |
||||||
|
|
||||||
|
// 角色
|
||||||
|
ROLE = 'role', |
||||||
|
|
||||||
|
// 任务
|
||||||
|
TASK = 'task', |
||||||
|
|
||||||
|
// 渠道
|
||||||
|
CHANNEL = 'channel', |
||||||
|
|
||||||
|
// 小程序
|
||||||
|
APPLET = 'applet', |
||||||
|
|
||||||
|
// 我的
|
||||||
|
USER = 'user', |
||||||
|
} |
@ -0,0 +1,11 @@ |
|||||||
|
/** |
||||||
|
* @description: 页面枚举(使用router中name属性) |
||||||
|
*/ |
||||||
|
export enum PageEnum { |
||||||
|
// 登录
|
||||||
|
BASE_LOGIN = 'Login', |
||||||
|
|
||||||
|
// 错误
|
||||||
|
ERROR_PAGE_NAME_404 = '404', |
||||||
|
|
||||||
|
} |
@ -0,0 +1,111 @@ |
|||||||
|
import type { ModalFuncProps } from 'ant-design-vue/lib/modal/Modal' |
||||||
|
|
||||||
|
import { message as Message, Modal, notification } from 'ant-design-vue' |
||||||
|
import { CheckCircleFilled, CloseCircleFilled, InfoCircleFilled } from '@ant-design/icons-vue' |
||||||
|
|
||||||
|
import type { ConfigProps, NotificationArgsProps } from 'ant-design-vue/lib/notification' |
||||||
|
import { isString } from '@/utils/is' |
||||||
|
|
||||||
|
export interface NotifyApi { |
||||||
|
info(config: NotificationArgsProps): void |
||||||
|
success(config: NotificationArgsProps): void |
||||||
|
error(config: NotificationArgsProps): void |
||||||
|
warn(config: NotificationArgsProps): void |
||||||
|
warning(config: NotificationArgsProps): void |
||||||
|
open(args: NotificationArgsProps): void |
||||||
|
close(key: string): void |
||||||
|
config(options: ConfigProps): void |
||||||
|
destroy(): void |
||||||
|
} |
||||||
|
|
||||||
|
export declare type NotificationPlacement = 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight' |
||||||
|
export declare type IconType = 'success' | 'info' | 'error' | 'warning' |
||||||
|
export interface ModalOptionsEx extends Omit<ModalFuncProps, 'iconType'> { |
||||||
|
iconType: 'warning' | 'success' | 'error' | 'info' |
||||||
|
} |
||||||
|
export type ModalOptionsPartial = Partial<ModalOptionsEx> & Pick<ModalOptionsEx, 'content'> |
||||||
|
|
||||||
|
function getIcon(iconType: string) { |
||||||
|
if (iconType === 'warning') |
||||||
|
return <InfoCircleFilled class="modal-icon-warning" /> |
||||||
|
else if (iconType === 'success') |
||||||
|
return <CheckCircleFilled class="modal-icon-success" /> |
||||||
|
else if (iconType === 'info') |
||||||
|
return <InfoCircleFilled class="modal-icon-info" /> |
||||||
|
else |
||||||
|
return <CloseCircleFilled class="modal-icon-error" /> |
||||||
|
} |
||||||
|
|
||||||
|
function renderContent({ content }: Pick<ModalOptionsEx, 'content'>) { |
||||||
|
if (isString(content)) |
||||||
|
return <div innerHTML={`<div>${content}</div>`}></div> |
||||||
|
else |
||||||
|
return content |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @description: Create confirmation box |
||||||
|
*/ |
||||||
|
function createConfirm(options: ModalOptionsEx) { |
||||||
|
const iconType = options.iconType || 'warning' |
||||||
|
Reflect.deleteProperty(options, 'iconType') |
||||||
|
const opt: ModalFuncProps = { |
||||||
|
centered: true, |
||||||
|
icon: getIcon(iconType), |
||||||
|
...options, |
||||||
|
content: renderContent(options), |
||||||
|
} |
||||||
|
return Modal.confirm(opt) |
||||||
|
} |
||||||
|
|
||||||
|
function getBaseOptions() { |
||||||
|
return { |
||||||
|
okText: '确认', |
||||||
|
centered: true, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function createModalOptions(options: ModalOptionsPartial, icon: string): ModalOptionsPartial { |
||||||
|
return { |
||||||
|
...getBaseOptions(), |
||||||
|
...options, |
||||||
|
content: renderContent(options), |
||||||
|
icon: getIcon(icon), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function createSuccessModal(options: ModalOptionsPartial) { |
||||||
|
return Modal.success(createModalOptions(options, 'success')) |
||||||
|
} |
||||||
|
|
||||||
|
function createErrorModal(options: ModalOptionsPartial) { |
||||||
|
return Modal.error(createModalOptions(options, 'close')) |
||||||
|
} |
||||||
|
|
||||||
|
function createInfoModal(options: ModalOptionsPartial) { |
||||||
|
return Modal.info(createModalOptions(options, 'info')) |
||||||
|
} |
||||||
|
|
||||||
|
function createWarningModal(options: ModalOptionsPartial) { |
||||||
|
return Modal.warning(createModalOptions(options, 'warning')) |
||||||
|
} |
||||||
|
|
||||||
|
notification.config({ |
||||||
|
placement: 'topRight', |
||||||
|
duration: 3, |
||||||
|
}) |
||||||
|
|
||||||
|
/** |
||||||
|
* @description: message |
||||||
|
*/ |
||||||
|
export function useMessage() { |
||||||
|
return { |
||||||
|
createMessage: Message, |
||||||
|
notification: notification as NotifyApi, |
||||||
|
createConfirm, |
||||||
|
createSuccessModal, |
||||||
|
createErrorModal, |
||||||
|
createInfoModal, |
||||||
|
createWarningModal, |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,14 @@ |
|||||||
|
<script setup lang="ts"> |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<section class="app-main h-screen overflow-hidden"> |
||||||
|
<router-view v-slot="{ Component, route }"> |
||||||
|
<transition name="fade-transform"> |
||||||
|
<component :is="Component" :key="route.path" /> |
||||||
|
</transition> |
||||||
|
</router-view> |
||||||
|
</section> |
||||||
|
</template> |
||||||
|
|
||||||
|
<style scoped></style> |
@ -0,0 +1,8 @@ |
|||||||
|
import type { MenuTypeEnum } from '@/enums/menuEnum' |
||||||
|
|
||||||
|
export interface MenuItem { |
||||||
|
name: string |
||||||
|
icon: string |
||||||
|
path: string |
||||||
|
key: MenuTypeEnum |
||||||
|
} |
@ -0,0 +1,131 @@ |
|||||||
|
<script setup lang="ts"> |
||||||
|
import { ref } from 'vue' |
||||||
|
import { useRouter } from 'vue-router' |
||||||
|
import type { MenuItem } from './index.d' |
||||||
|
import { SvgIcon } from '@/components/SvgIcon' |
||||||
|
import { MenuTypeEnum } from '@/enums/menuEnum' |
||||||
|
|
||||||
|
const router = useRouter() |
||||||
|
const menuActive = ref<MenuTypeEnum>(MenuTypeEnum.CONVERSATION) |
||||||
|
const menu = ref<MenuItem[]>([ |
||||||
|
{ |
||||||
|
name: '会话', |
||||||
|
icon: 'wei_xin', |
||||||
|
path: '/conversation', |
||||||
|
key: MenuTypeEnum.CONVERSATION, |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: '文生图', |
||||||
|
icon: 'wen_sheng_tu', |
||||||
|
path: '/textToPicture', |
||||||
|
key: MenuTypeEnum.TEXT_TO_PICTURE, |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: '角色', |
||||||
|
icon: 'jue_se', |
||||||
|
path: '', |
||||||
|
key: MenuTypeEnum.ROLE, |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: '任务', |
||||||
|
icon: 'ren_wu', |
||||||
|
path: '', |
||||||
|
key: MenuTypeEnum.TASK, |
||||||
|
}, |
||||||
|
]) |
||||||
|
|
||||||
|
const footMenu = ref([ |
||||||
|
// { |
||||||
|
// name: '小程序', |
||||||
|
// icon: 'xiao_cheng_xu', |
||||||
|
// path: '', |
||||||
|
// key: MenuTypeEnum.APPLET, |
||||||
|
// }, |
||||||
|
{ |
||||||
|
name: '我的', |
||||||
|
icon: 'wo_de', |
||||||
|
path: '', |
||||||
|
key: MenuTypeEnum.USER, |
||||||
|
}, |
||||||
|
]) |
||||||
|
|
||||||
|
function handleToPath(item: MenuItem) { |
||||||
|
menuActive.value = item.key |
||||||
|
router.push({ path: item.path }) |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<div class="app-menu h-full flex flex-col justify-between"> |
||||||
|
<div class="menu w-full"> |
||||||
|
<img class="logo" src="~@/assets/images/logo.png" alt=""> |
||||||
|
<div |
||||||
|
v-for="item in menu" |
||||||
|
:key="item.key" |
||||||
|
class="menu-item w-full" |
||||||
|
:class="[menuActive === item.key ? 'menu-item-active' : '']" |
||||||
|
@click="handleToPath(item)" |
||||||
|
> |
||||||
|
<SvgIcon class="icon" :name="item.icon" /> |
||||||
|
<p class="text text-center"> |
||||||
|
{{ item.name }} |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="menu foot-menu w-full"> |
||||||
|
<div |
||||||
|
v-for="item in footMenu" |
||||||
|
:key="item.key" |
||||||
|
class="menu-item w-full" |
||||||
|
:class="[menuActive === item.key ? 'menu-item-active' : '']" |
||||||
|
@click="handleToPath(item)" |
||||||
|
> |
||||||
|
<SvgIcon class="icon" :name="item.icon" /> |
||||||
|
<p class="text text-center"> |
||||||
|
{{ item.name }} |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
@include app('menu') { |
||||||
|
background-image: url('../../assets/images/bg_menu.png'); |
||||||
|
background-size: cover; |
||||||
|
|
||||||
|
.menu { |
||||||
|
.logo { |
||||||
|
width: 50px; |
||||||
|
margin: 15px auto 30px auto; |
||||||
|
display: block; |
||||||
|
} |
||||||
|
.menu-item { |
||||||
|
color: #fff; |
||||||
|
cursor: pointer; |
||||||
|
.icon { |
||||||
|
width: 45px; |
||||||
|
height: 45px; |
||||||
|
padding: 12px; |
||||||
|
border-radius: 12px; |
||||||
|
margin: 0 auto; |
||||||
|
display: block; |
||||||
|
transition: all 0.4s; |
||||||
|
} |
||||||
|
} |
||||||
|
.menu-item + .menu-item { |
||||||
|
margin-top: 20px; |
||||||
|
} |
||||||
|
.menu-item:hover { |
||||||
|
.icon { |
||||||
|
background-color: #3766d6; |
||||||
|
} |
||||||
|
} |
||||||
|
.menu-item-active { |
||||||
|
.icon { |
||||||
|
background-color: #3766d6; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,23 @@ |
|||||||
|
<script setup lang="ts"> |
||||||
|
import AppMain from './AppMain/index.vue' |
||||||
|
import AppMenu from './AppMenu/index.vue' |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<div class="app-wrapper flex justify-between items-center"> |
||||||
|
<AppMenu /> |
||||||
|
<AppMain /> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
@include app('wrapper') { |
||||||
|
@include app('menu') { |
||||||
|
width: $menu-width; |
||||||
|
} |
||||||
|
@include app('main') { |
||||||
|
width: calc(100% - $menu-width); |
||||||
|
} |
||||||
|
height: 100%; |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,35 @@ |
|||||||
|
import type { Router } from 'vue-router' |
||||||
|
import { PageEnum } from '@/enums/pageEnum' |
||||||
|
import { useUserStore } from '@/store/moules/userStore/index' |
||||||
|
|
||||||
|
export function setupRouterGuard(router: Router) { |
||||||
|
createRouterGuards(router) |
||||||
|
} |
||||||
|
|
||||||
|
const WHITE_NAME_LIST: string[] = [ |
||||||
|
PageEnum.BASE_LOGIN, |
||||||
|
] |
||||||
|
|
||||||
|
function createRouterGuards(router: Router) { |
||||||
|
const userStore = useUserStore() |
||||||
|
// 前置
|
||||||
|
router.beforeEach(async (to, _from, next) => { |
||||||
|
const isErrorPage = router.getRoutes().findIndex(item => item.name === to.name) |
||||||
|
if (isErrorPage === -1) |
||||||
|
next({ name: PageEnum.ERROR_PAGE_NAME_404 }) |
||||||
|
|
||||||
|
if (userStore.getToken) { |
||||||
|
next() |
||||||
|
} |
||||||
|
else { |
||||||
|
if (WHITE_NAME_LIST.includes(to.name as string)) |
||||||
|
next() |
||||||
|
else |
||||||
|
next({ name: PageEnum.BASE_LOGIN }) |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
// router.afterEach((to, _) => {
|
||||||
|
// document.title = (to?.meta?.title as string) || document.title
|
||||||
|
// })
|
||||||
|
} |
@ -0,0 +1,11 @@ |
|||||||
|
import type { App } from 'vue' |
||||||
|
import { createPinia } from 'pinia' |
||||||
|
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' |
||||||
|
|
||||||
|
const pinia = createPinia() |
||||||
|
pinia.use(piniaPluginPersistedstate) |
||||||
|
export function setupStore(app: App<Element>) { |
||||||
|
app.use(pinia) |
||||||
|
} |
||||||
|
|
||||||
|
export { pinia } |
@ -0,0 +1,15 @@ |
|||||||
|
export interface UserStateType { |
||||||
|
token: string | null |
||||||
|
userInfo: string | null |
||||||
|
} |
||||||
|
|
||||||
|
export interface UserInfoType { |
||||||
|
user_id: string |
||||||
|
avatar: string |
||||||
|
access_token: string |
||||||
|
token_type: string |
||||||
|
role_name: string |
||||||
|
user_name: string |
||||||
|
real_name: string |
||||||
|
nick_name: string |
||||||
|
} |
@ -0,0 +1,73 @@ |
|||||||
|
/* |
||||||
|
* @Description: 主题状态控制 |
||||||
|
* @Author: yeke |
||||||
|
* @Date: 2023-06-28 11:16:32 |
||||||
|
* @LastEditors: lipenghui |
||||||
|
* @LastEditTime: 2024-01-17 13:54:13 |
||||||
|
*/ |
||||||
|
import { defineStore } from 'pinia' |
||||||
|
import type { UserInfoType, UserStateType } from './index.d' |
||||||
|
import { router } from '@/router' |
||||||
|
import { PageEnum } from '@/enums/pageEnum' |
||||||
|
import { ACCESS_TOKEN_KEY, USER_INFO_KEY } from '@/enums/cacheEnum' |
||||||
|
import { token } from '@/api/base/login' |
||||||
|
import type { TokenParams } from '@/api/base/login' |
||||||
|
import crypto from '@/utils/crypto' |
||||||
|
|
||||||
|
export const useUserStore = defineStore('useUserStore', { |
||||||
|
state: (): UserStateType => { |
||||||
|
return { |
||||||
|
token: null, |
||||||
|
userInfo: null, |
||||||
|
} |
||||||
|
}, |
||||||
|
getters: { |
||||||
|
getToken(): string | null { |
||||||
|
return this.token ? crypto.decryptAES(this.token, crypto.localKey) : null |
||||||
|
}, |
||||||
|
getUserInfo(): UserInfoType | null { |
||||||
|
return this.userInfo ? JSON.parse(crypto.decryptAES(this.userInfo, crypto.localKey)) : null |
||||||
|
}, |
||||||
|
}, |
||||||
|
actions: { |
||||||
|
setToken(token: string) { |
||||||
|
this.token = token |
||||||
|
}, |
||||||
|
|
||||||
|
setUserInfo(userInfo: string) { |
||||||
|
this.userInfo = userInfo |
||||||
|
}, |
||||||
|
|
||||||
|
async login(params: TokenParams) { |
||||||
|
return new Promise<void>((resolve, reject) => { |
||||||
|
token(params).then((res) => { |
||||||
|
this.setToken(crypto.encryptAES(res.access_token, crypto.localKey)) |
||||||
|
this.setUserInfo(crypto.encryptAES(JSON.stringify(res), crypto.localKey)) |
||||||
|
resolve(res) |
||||||
|
}).catch((err) => { |
||||||
|
reject(err) |
||||||
|
}) |
||||||
|
}) |
||||||
|
}, |
||||||
|
|
||||||
|
/** |
||||||
|
* @description: logout |
||||||
|
*/ |
||||||
|
async logout(goLogin = false) { |
||||||
|
this.$reset() |
||||||
|
localStorage.clear() |
||||||
|
// 清空数据
|
||||||
|
goLogin && router.push(PageEnum.BASE_LOGIN) |
||||||
|
}, |
||||||
|
}, |
||||||
|
persist: [ |
||||||
|
{ |
||||||
|
paths: ['token'], |
||||||
|
key: ACCESS_TOKEN_KEY, |
||||||
|
}, |
||||||
|
{ |
||||||
|
paths: ['userInfo'], |
||||||
|
key: USER_INFO_KEY, |
||||||
|
}, |
||||||
|
], |
||||||
|
}) |
@ -1,79 +0,0 @@ |
|||||||
:root { |
|
||||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; |
|
||||||
line-height: 1.5; |
|
||||||
font-weight: 400; |
|
||||||
|
|
||||||
color-scheme: light dark; |
|
||||||
color: rgba(255, 255, 255, 0.87); |
|
||||||
background-color: #242424; |
|
||||||
|
|
||||||
font-synthesis: none; |
|
||||||
text-rendering: optimizeLegibility; |
|
||||||
-webkit-font-smoothing: antialiased; |
|
||||||
-moz-osx-font-smoothing: grayscale; |
|
||||||
} |
|
||||||
|
|
||||||
a { |
|
||||||
font-weight: 500; |
|
||||||
color: #646cff; |
|
||||||
text-decoration: inherit; |
|
||||||
} |
|
||||||
a:hover { |
|
||||||
color: #535bf2; |
|
||||||
} |
|
||||||
|
|
||||||
body { |
|
||||||
margin: 0; |
|
||||||
display: flex; |
|
||||||
place-items: center; |
|
||||||
min-width: 320px; |
|
||||||
min-height: 100vh; |
|
||||||
} |
|
||||||
|
|
||||||
h1 { |
|
||||||
font-size: 3.2em; |
|
||||||
line-height: 1.1; |
|
||||||
} |
|
||||||
|
|
||||||
button { |
|
||||||
border-radius: 8px; |
|
||||||
border: 1px solid transparent; |
|
||||||
padding: 0.6em 1.2em; |
|
||||||
font-size: 1em; |
|
||||||
font-weight: 500; |
|
||||||
font-family: inherit; |
|
||||||
background-color: #1a1a1a; |
|
||||||
cursor: pointer; |
|
||||||
transition: border-color 0.25s; |
|
||||||
} |
|
||||||
button:hover { |
|
||||||
border-color: #646cff; |
|
||||||
} |
|
||||||
button:focus, |
|
||||||
button:focus-visible { |
|
||||||
outline: 4px auto -webkit-focus-ring-color; |
|
||||||
} |
|
||||||
|
|
||||||
.card { |
|
||||||
padding: 2em; |
|
||||||
} |
|
||||||
|
|
||||||
#app { |
|
||||||
max-width: 1280px; |
|
||||||
margin: 0 auto; |
|
||||||
padding: 2rem; |
|
||||||
text-align: center; |
|
||||||
} |
|
||||||
|
|
||||||
@media (prefers-color-scheme: light) { |
|
||||||
:root { |
|
||||||
color: #213547; |
|
||||||
background-color: #ffffff; |
|
||||||
} |
|
||||||
a:hover { |
|
||||||
color: #747bff; |
|
||||||
} |
|
||||||
button { |
|
||||||
background-color: #f9f9f9; |
|
||||||
} |
|
||||||
} |
|
@ -0,0 +1,337 @@ |
|||||||
|
import type { |
||||||
|
AxiosError, |
||||||
|
AxiosInstance, |
||||||
|
AxiosRequestConfig, |
||||||
|
AxiosResponse, |
||||||
|
InternalAxiosRequestConfig, |
||||||
|
} from 'axios' |
||||||
|
import axios from 'axios' |
||||||
|
import qs from 'qs' |
||||||
|
import { cloneDeep } from 'lodash-es' |
||||||
|
import type { CreateAxiosOptions } from './axiosTransform' |
||||||
|
import { AxiosCanceler } from './axiosCancel' |
||||||
|
import { isFunction } from '@/utils/is' |
||||||
|
import { downloadByData } from '@/utils/file/download' |
||||||
|
import type { RequestOptions, Result, UploadFileParams } from '/#/axios' |
||||||
|
import { ContentTypeEnum, RequestEnum } from '@/enums/httpEnum' |
||||||
|
|
||||||
|
/** |
||||||
|
* @description: axios 模块 |
||||||
|
*/ |
||||||
|
export class VAxios { |
||||||
|
private axiosInstance: AxiosInstance |
||||||
|
private readonly options: CreateAxiosOptions |
||||||
|
|
||||||
|
constructor(options: CreateAxiosOptions) { |
||||||
|
this.options = options |
||||||
|
this.axiosInstance = axios.create(options) |
||||||
|
this.setupInterceptors() |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @description: 创建 axios 实例 |
||||||
|
*/ |
||||||
|
private createAxios(config: CreateAxiosOptions): void { |
||||||
|
this.axiosInstance = axios.create(config) |
||||||
|
} |
||||||
|
|
||||||
|
private getTransform() { |
||||||
|
const { transform } = this.options |
||||||
|
return transform |
||||||
|
} |
||||||
|
|
||||||
|
getAxios(): AxiosInstance { |
||||||
|
return this.axiosInstance |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @description: 重新配置 axios |
||||||
|
*/ |
||||||
|
configAxios(config: CreateAxiosOptions) { |
||||||
|
if (!this.axiosInstance) |
||||||
|
return |
||||||
|
|
||||||
|
this.createAxios(config) |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @description: 设置通用标题 |
||||||
|
*/ |
||||||
|
setHeader(headers: any): void { |
||||||
|
if (!this.axiosInstance) |
||||||
|
return |
||||||
|
|
||||||
|
Object.assign(this.axiosInstance.defaults.headers, headers) |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @description: Interceptor configuration 拦截器配置 |
||||||
|
*/ |
||||||
|
private setupInterceptors() { |
||||||
|
// const transform = this.getTransform();
|
||||||
|
const { |
||||||
|
axiosInstance, |
||||||
|
options: { transform }, |
||||||
|
} = this |
||||||
|
if (!transform) |
||||||
|
return |
||||||
|
|
||||||
|
const { |
||||||
|
requestInterceptors, |
||||||
|
requestInterceptorsCatch, |
||||||
|
responseInterceptors, |
||||||
|
responseInterceptorsCatch, |
||||||
|
} = transform |
||||||
|
|
||||||
|
const axiosCanceler = new AxiosCanceler() |
||||||
|
|
||||||
|
// 请求拦截器配置处理
|
||||||
|
this.axiosInstance.interceptors.request.use((config: InternalAxiosRequestConfig) => { |
||||||
|
// If cancel repeat request is turned on, then cancel repeat request is prohibited
|
||||||
|
const requestOptions |
||||||
|
= (config as unknown as any).requestOptions ?? this.options.requestOptions |
||||||
|
const ignoreCancelToken = requestOptions?.ignoreCancelToken ?? true |
||||||
|
|
||||||
|
!ignoreCancelToken && axiosCanceler.addPending(config) |
||||||
|
|
||||||
|
if (requestInterceptors && isFunction(requestInterceptors)) |
||||||
|
config = requestInterceptors(config, this.options) |
||||||
|
|
||||||
|
return config |
||||||
|
}, undefined) |
||||||
|
|
||||||
|
// 请求拦截器错误捕获
|
||||||
|
requestInterceptorsCatch |
||||||
|
&& isFunction(requestInterceptorsCatch) |
||||||
|
&& this.axiosInstance.interceptors.request.use(undefined, requestInterceptorsCatch) |
||||||
|
|
||||||
|
// 响应结果拦截器处理
|
||||||
|
this.axiosInstance.interceptors.response.use(async (res: AxiosResponse<any>) => { |
||||||
|
if (res.data.code === 401) { |
||||||
|
// 如果未认证,说明可能是访问令牌过期了,跳转登录页
|
||||||
|
} |
||||||
|
res && axiosCanceler.removePending(res.config) |
||||||
|
if (responseInterceptors && isFunction(responseInterceptors)) |
||||||
|
res = responseInterceptors(res) |
||||||
|
|
||||||
|
return res |
||||||
|
}, undefined) |
||||||
|
|
||||||
|
// 响应结果拦截器错误捕获
|
||||||
|
responseInterceptorsCatch |
||||||
|
&& isFunction(responseInterceptorsCatch) |
||||||
|
&& this.axiosInstance.interceptors.response.use(undefined, (error) => { |
||||||
|
return responseInterceptorsCatch(axiosInstance, error) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @description: 文件上传 |
||||||
|
*/ |
||||||
|
uploadFile<T = any>(config: AxiosRequestConfig, params: UploadFileParams) { |
||||||
|
const formData = new window.FormData() |
||||||
|
const customFilename = params.name || 'file' |
||||||
|
|
||||||
|
if (params.filename) |
||||||
|
formData.append(customFilename, params.file, params.filename) |
||||||
|
|
||||||
|
else |
||||||
|
formData.append(customFilename, params.file) |
||||||
|
|
||||||
|
if (params.data) { |
||||||
|
Object.keys(params.data).forEach((key) => { |
||||||
|
const value = params.data![key] |
||||||
|
if (Array.isArray(value)) { |
||||||
|
value.forEach((item) => { |
||||||
|
formData.append(`${key}[]`, item) |
||||||
|
}) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
formData.append(key, params.data![key]) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
return this.axiosInstance.request<T>({ |
||||||
|
...config, |
||||||
|
method: 'POST', |
||||||
|
data: formData, |
||||||
|
headers: { |
||||||
|
'Content-type': ContentTypeEnum.FORM_DATA, |
||||||
|
'ignoreCancelToken': true, |
||||||
|
}, |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
// 支持表单数据
|
||||||
|
supportFormData(config: AxiosRequestConfig) { |
||||||
|
const headers = config.headers || this.options.headers |
||||||
|
const contentType = headers?.['Content-Type'] || headers?.['content-type'] |
||||||
|
|
||||||
|
if ( |
||||||
|
contentType !== ContentTypeEnum.FORM_URLENCODED |
||||||
|
|| !Reflect.has(config, 'data') |
||||||
|
|| config.method?.toUpperCase() === RequestEnum.GET |
||||||
|
) |
||||||
|
return config |
||||||
|
|
||||||
|
return { |
||||||
|
...config, |
||||||
|
data: qs.stringify(config.data, { arrayFormat: 'brackets' }), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
get<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> { |
||||||
|
return this.request({ ...config, method: 'GET' }, options) |
||||||
|
} |
||||||
|
|
||||||
|
post<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> { |
||||||
|
return this.request({ ...config, method: 'POST' }, options) |
||||||
|
} |
||||||
|
|
||||||
|
put<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> { |
||||||
|
return this.request({ ...config, method: 'PUT' }, options) |
||||||
|
} |
||||||
|
|
||||||
|
delete<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> { |
||||||
|
return this.request({ ...config, method: 'DELETE' }, options) |
||||||
|
} |
||||||
|
|
||||||
|
download<T = any>(config: AxiosRequestConfig, title?: string, options?: RequestOptions): Promise<T> { |
||||||
|
let conf: CreateAxiosOptions = cloneDeep({ |
||||||
|
...config, |
||||||
|
method: 'GET', |
||||||
|
responseType: 'blob', |
||||||
|
}) |
||||||
|
const transform = this.getTransform() |
||||||
|
|
||||||
|
const { requestOptions } = this.options |
||||||
|
|
||||||
|
const opt: RequestOptions = Object.assign({}, requestOptions, options) |
||||||
|
|
||||||
|
const { beforeRequestHook, requestCatchHook } = transform || {} |
||||||
|
|
||||||
|
if (beforeRequestHook && isFunction(beforeRequestHook)) |
||||||
|
conf = beforeRequestHook(conf, opt) |
||||||
|
|
||||||
|
conf.requestOptions = opt |
||||||
|
|
||||||
|
conf = this.supportFormData(conf) |
||||||
|
|
||||||
|
return new Promise((resolve, reject) => { |
||||||
|
this.axiosInstance |
||||||
|
.request<any, AxiosResponse<Result>>(conf) |
||||||
|
.then((res: AxiosResponse<Result>) => { |
||||||
|
resolve(res as unknown as Promise<T>) |
||||||
|
// download file
|
||||||
|
if (typeof res != 'undefined') |
||||||
|
downloadByData(res?.data as unknown as BlobPart, title || 'export') |
||||||
|
}) |
||||||
|
.catch((e: Error | AxiosError) => { |
||||||
|
if (requestCatchHook && isFunction(requestCatchHook)) { |
||||||
|
reject(requestCatchHook(e, opt)) |
||||||
|
return |
||||||
|
} |
||||||
|
if (axios.isAxiosError(e)) { |
||||||
|
// rewrite error message from axios in here
|
||||||
|
} |
||||||
|
reject(e) |
||||||
|
}) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
export<T = any>(config: AxiosRequestConfig, title: string, options?: RequestOptions): Promise<T> { |
||||||
|
let conf: CreateAxiosOptions = cloneDeep({ |
||||||
|
...config, |
||||||
|
method: 'POST', |
||||||
|
responseType: 'blob', |
||||||
|
}) |
||||||
|
const transform = this.getTransform() |
||||||
|
|
||||||
|
const { requestOptions } = this.options |
||||||
|
|
||||||
|
const opt: RequestOptions = Object.assign({}, requestOptions, options) |
||||||
|
|
||||||
|
const { beforeRequestHook, requestCatchHook } = transform || {} |
||||||
|
|
||||||
|
if (beforeRequestHook && isFunction(beforeRequestHook)) |
||||||
|
conf = beforeRequestHook(conf, opt) |
||||||
|
|
||||||
|
conf.requestOptions = opt |
||||||
|
|
||||||
|
conf = this.supportFormData(conf) |
||||||
|
|
||||||
|
return new Promise((resolve, reject) => { |
||||||
|
this.axiosInstance |
||||||
|
.request<any, AxiosResponse<Result>>(conf) |
||||||
|
.then((res: AxiosResponse<Result>) => { |
||||||
|
resolve(res as unknown as Promise<T>) |
||||||
|
// download file
|
||||||
|
if (typeof res != 'undefined') |
||||||
|
downloadByData(res?.data as unknown as BlobPart, title) |
||||||
|
}) |
||||||
|
.catch((e: Error | AxiosError) => { |
||||||
|
if (requestCatchHook && isFunction(requestCatchHook)) { |
||||||
|
reject(requestCatchHook(e, opt)) |
||||||
|
return |
||||||
|
} |
||||||
|
if (axios.isAxiosError(e)) { |
||||||
|
// rewrite error message from axios in here
|
||||||
|
} |
||||||
|
reject(e) |
||||||
|
}) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
request<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> { |
||||||
|
let conf: CreateAxiosOptions = cloneDeep(config) |
||||||
|
|
||||||
|
// cancelToken 如果被深拷贝,会导致最外层无法使用cancel方法来取消请求
|
||||||
|
if (config.cancelToken) |
||||||
|
conf.cancelToken = config.cancelToken |
||||||
|
|
||||||
|
if (config.signal) |
||||||
|
conf.signal = config.signal |
||||||
|
|
||||||
|
const transform = this.getTransform() |
||||||
|
|
||||||
|
const { requestOptions } = this.options |
||||||
|
|
||||||
|
const opt: RequestOptions = Object.assign({}, requestOptions, options) |
||||||
|
const { beforeRequestHook, requestCatchHook, transformResponseHook } = transform || {} |
||||||
|
if (beforeRequestHook && isFunction(beforeRequestHook)) |
||||||
|
conf = beforeRequestHook(conf, opt) |
||||||
|
|
||||||
|
conf.requestOptions = opt |
||||||
|
|
||||||
|
conf = this.supportFormData(conf) |
||||||
|
|
||||||
|
return new Promise((resolve, reject) => { |
||||||
|
this.axiosInstance |
||||||
|
.request<any, AxiosResponse<Result>>(conf) |
||||||
|
.then((res: AxiosResponse<Result>) => { |
||||||
|
if (transformResponseHook && isFunction(transformResponseHook)) { |
||||||
|
try { |
||||||
|
const ret = transformResponseHook(res, opt) |
||||||
|
resolve(ret) |
||||||
|
} |
||||||
|
catch (err) { |
||||||
|
reject(err || new Error('request error!')) |
||||||
|
} |
||||||
|
return |
||||||
|
} |
||||||
|
resolve(res as unknown as Promise<T>) |
||||||
|
}) |
||||||
|
.catch((e: Error | AxiosError) => { |
||||||
|
if (requestCatchHook && isFunction(requestCatchHook)) { |
||||||
|
reject(requestCatchHook(e, opt)) |
||||||
|
return |
||||||
|
} |
||||||
|
if (axios.isAxiosError(e)) { |
||||||
|
// 在此处重写来自 axios 的错误消息
|
||||||
|
} |
||||||
|
reject(e) |
||||||
|
}) |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,59 @@ |
|||||||
|
import type { AxiosRequestConfig } from 'axios' |
||||||
|
|
||||||
|
// 用于存储每个请求的标识和取消函数
|
||||||
|
const pendingMap = new Map<string, AbortController>() |
||||||
|
|
||||||
|
function getPendingUrl(config: AxiosRequestConfig): string { |
||||||
|
return [config.method, config.url].join('&') |
||||||
|
} |
||||||
|
|
||||||
|
export class AxiosCanceler { |
||||||
|
/** |
||||||
|
* 添加请求 |
||||||
|
* @param config 请求配置 |
||||||
|
*/ |
||||||
|
public addPending(config: AxiosRequestConfig): void { |
||||||
|
this.removePending(config) |
||||||
|
const url = getPendingUrl(config) |
||||||
|
const controller = new AbortController() |
||||||
|
config.signal = config.signal || controller.signal |
||||||
|
if (!pendingMap.has(url)) { |
||||||
|
// 如果当前请求不在等待中,将其添加到等待中
|
||||||
|
pendingMap.set(url, controller) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 清除所有等待中的请求 |
||||||
|
*/ |
||||||
|
public removeAllPending(): void { |
||||||
|
pendingMap.forEach((abortController) => { |
||||||
|
if (abortController) |
||||||
|
abortController.abort() |
||||||
|
}) |
||||||
|
this.reset() |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 移除请求 |
||||||
|
* @param config 请求配置 |
||||||
|
*/ |
||||||
|
public removePending(config: AxiosRequestConfig): void { |
||||||
|
const url = getPendingUrl(config) |
||||||
|
if (pendingMap.has(url)) { |
||||||
|
// 如果当前请求在等待中,取消它并将其从等待中移除
|
||||||
|
const abortController = pendingMap.get(url) |
||||||
|
if (abortController) |
||||||
|
abortController.abort(url) |
||||||
|
|
||||||
|
pendingMap.delete(url) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 重置 |
||||||
|
*/ |
||||||
|
public reset(): void { |
||||||
|
pendingMap.clear() |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,33 @@ |
|||||||
|
import type { AxiosError, AxiosInstance } from 'axios' |
||||||
|
|
||||||
|
/** |
||||||
|
* 请求重试机制 |
||||||
|
*/ |
||||||
|
|
||||||
|
export class AxiosRetry { |
||||||
|
/** |
||||||
|
* 重试 |
||||||
|
*/ |
||||||
|
async retry(axiosInstance: AxiosInstance, error: AxiosError) { |
||||||
|
// eslint-disable-next-line ts/ban-ts-comment, ts/prefer-ts-expect-error
|
||||||
|
// @ts-ignore
|
||||||
|
const { config } = error.response |
||||||
|
const { waitTime, count } = config?.requestOptions?.retryRequest ?? {} |
||||||
|
config.__retryCount = config.__retryCount || 0 |
||||||
|
if (config.__retryCount >= count) |
||||||
|
return Promise.reject(error) |
||||||
|
|
||||||
|
config.__retryCount += 1 |
||||||
|
// 请求返回后config的header不正确造成重试请求失败,删除返回headers采用默认headers
|
||||||
|
delete config.headers |
||||||
|
await this.delay(waitTime) |
||||||
|
return await axiosInstance(config) |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 延迟 |
||||||
|
*/ |
||||||
|
private delay(waitTime: number) { |
||||||
|
return new Promise(resolve => setTimeout(resolve, waitTime)) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,57 @@ |
|||||||
|
/** |
||||||
|
* Data processing class, can be configured according to the project |
||||||
|
*/ |
||||||
|
import type { |
||||||
|
AxiosInstance, |
||||||
|
AxiosRequestConfig, |
||||||
|
AxiosResponse, |
||||||
|
InternalAxiosRequestConfig, |
||||||
|
} from 'axios' |
||||||
|
import type { RequestOptions, Result } from '/#/axios' |
||||||
|
|
||||||
|
export interface CreateAxiosOptions extends AxiosRequestConfig { |
||||||
|
authenticationScheme?: string |
||||||
|
tokenScheme?: string |
||||||
|
transform?: AxiosTransform |
||||||
|
requestOptions?: RequestOptions |
||||||
|
} |
||||||
|
|
||||||
|
export abstract class AxiosTransform { |
||||||
|
/** |
||||||
|
* @description: 在发送请求之前调用的函数。它可以根据需要修改请求配置 |
||||||
|
*/ |
||||||
|
beforeRequestHook?: (config: AxiosRequestConfig, options: RequestOptions) => AxiosRequestConfig |
||||||
|
|
||||||
|
/** |
||||||
|
* @description: 处理响应数据 |
||||||
|
*/ |
||||||
|
transformResponseHook?: (res: AxiosResponse<Result>, options: RequestOptions) => any |
||||||
|
|
||||||
|
/** |
||||||
|
* @description: 请求失败处理 |
||||||
|
*/ |
||||||
|
requestCatchHook?: (e: Error, options: RequestOptions) => Promise<any> |
||||||
|
|
||||||
|
/** |
||||||
|
* @description: 请求之前的拦截器 |
||||||
|
*/ |
||||||
|
requestInterceptors?: ( |
||||||
|
config: InternalAxiosRequestConfig, |
||||||
|
options: CreateAxiosOptions, |
||||||
|
) => InternalAxiosRequestConfig |
||||||
|
|
||||||
|
/** |
||||||
|
* @description: 请求之后的拦截器 |
||||||
|
*/ |
||||||
|
responseInterceptors?: (res: AxiosResponse<any>) => AxiosResponse<any> |
||||||
|
|
||||||
|
/** |
||||||
|
* @description: 请求之前的拦截器错误处理 |
||||||
|
*/ |
||||||
|
requestInterceptorsCatch?: (error: Error) => void |
||||||
|
|
||||||
|
/** |
||||||
|
* @description: 请求之后的拦截器错误处理 |
||||||
|
*/ |
||||||
|
responseInterceptorsCatch?: (axiosInstance: AxiosInstance, error: Error) => void |
||||||
|
} |
@ -0,0 +1,69 @@ |
|||||||
|
import type { ErrorMessageMode } from '/#/axios' |
||||||
|
import { useMessage } from '@/hooks/useMessage' |
||||||
|
import { useUserStore } from '@/store/moules/userStore/index' |
||||||
|
import { HttpErrorMsgEnum } from '@/enums/httpEnum' |
||||||
|
|
||||||
|
const { createMessage, createErrorModal } = useMessage() |
||||||
|
const error = createMessage.error! |
||||||
|
export function checkStatus( |
||||||
|
status: number, |
||||||
|
msg: string, |
||||||
|
errorMessageMode: ErrorMessageMode = 'message', |
||||||
|
): void { |
||||||
|
const userStore = useUserStore() |
||||||
|
let errMessage = '' |
||||||
|
|
||||||
|
switch (status) { |
||||||
|
case 400: |
||||||
|
errMessage = msg || HttpErrorMsgEnum.API_REQUEST_FAILED |
||||||
|
break |
||||||
|
// 401: Not logged in
|
||||||
|
// Jump to the login page if not logged in, and carry the path of the current page
|
||||||
|
// Return to the current page after successful login. This step needs to be operated on the login page.
|
||||||
|
case 401: |
||||||
|
errMessage = msg || HttpErrorMsgEnum.ERROR_MESSAGE_401 |
||||||
|
userStore.logout(true) |
||||||
|
|
||||||
|
break |
||||||
|
case 403: |
||||||
|
errMessage = msg || HttpErrorMsgEnum.ERROR_MESSAGE_403 |
||||||
|
break |
||||||
|
// 404请求不存在
|
||||||
|
case 404: |
||||||
|
errMessage = msg || HttpErrorMsgEnum.ERROR_MESSAGE_404 |
||||||
|
break |
||||||
|
case 405: |
||||||
|
errMessage = msg || HttpErrorMsgEnum.ERROR_MESSAGE_405 |
||||||
|
break |
||||||
|
case 408: |
||||||
|
errMessage = msg || HttpErrorMsgEnum.ERROR_MESSAGE_408 |
||||||
|
break |
||||||
|
case 500: |
||||||
|
errMessage = msg || HttpErrorMsgEnum.ERROR_MESSAGE_500 |
||||||
|
break |
||||||
|
case 501: |
||||||
|
errMessage = msg || HttpErrorMsgEnum.ERROR_MESSAGE_501 |
||||||
|
break |
||||||
|
case 502: |
||||||
|
errMessage = msg || HttpErrorMsgEnum.ERROR_MESSAGE_502 |
||||||
|
break |
||||||
|
case 503: |
||||||
|
errMessage = msg || HttpErrorMsgEnum.ERROR_MESSAGE_503 |
||||||
|
break |
||||||
|
case 504: |
||||||
|
errMessage = msg || HttpErrorMsgEnum.ERROR_MESSAGE_504 |
||||||
|
break |
||||||
|
case 505: |
||||||
|
errMessage = msg || HttpErrorMsgEnum.ERROR_MESSAGE_505 |
||||||
|
break |
||||||
|
default: |
||||||
|
} |
||||||
|
|
||||||
|
if (errMessage) { |
||||||
|
if (errorMessageMode === 'modal') |
||||||
|
createErrorModal({ title: HttpErrorMsgEnum.ERROR_TIP, content: errMessage }) |
||||||
|
|
||||||
|
else if (errorMessageMode === 'message') |
||||||
|
error({ content: errMessage, key: `global_error_message_status_${status}` }) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,47 @@ |
|||||||
|
import { isObject, isString } from '@/utils/is' |
||||||
|
|
||||||
|
const DATE_TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss' |
||||||
|
|
||||||
|
export function joinTimestamp<T extends boolean>( |
||||||
|
join: boolean, |
||||||
|
restful: T, |
||||||
|
): T extends true ? string : object |
||||||
|
|
||||||
|
export function joinTimestamp(join: boolean, restful = false): string | object { |
||||||
|
if (!join) |
||||||
|
return restful ? '' : {} |
||||||
|
|
||||||
|
const now = new Date().getTime() |
||||||
|
if (restful) |
||||||
|
return `?_t=${now}` |
||||||
|
|
||||||
|
return { _t: now } |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @description: 请求参数时间格式 |
||||||
|
*/ |
||||||
|
export function formatRequestDate(params: Recordable) { |
||||||
|
if (Object.prototype.toString.call(params) !== '[object Object]') |
||||||
|
return |
||||||
|
|
||||||
|
for (const key in params) { |
||||||
|
const format = params[key]?.format ?? null |
||||||
|
if (format && typeof format === 'function') |
||||||
|
params[key] = params[key].format(DATE_TIME_FORMAT) |
||||||
|
|
||||||
|
if (isString(key)) { |
||||||
|
const value = params[key] |
||||||
|
if (value) { |
||||||
|
try { |
||||||
|
params[key] = isString(value) ? value.trim() : value |
||||||
|
} |
||||||
|
catch (error: any) { |
||||||
|
throw new Error(error) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
if (isObject(params[key])) |
||||||
|
formatRequestDate(params[key]) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,313 @@ |
|||||||
|
// axios配置 可自行根据项目进行更改,只需更改该文件即可,其他文件可以不动
|
||||||
|
// The axios configuration can be changed according to the project, just change the file, other files can be left unchanged
|
||||||
|
|
||||||
|
import type { AxiosInstance, AxiosResponse } from 'axios' |
||||||
|
import { clone } from 'lodash-es' |
||||||
|
import axios from 'axios' |
||||||
|
import type { AxiosTransform, CreateAxiosOptions } from './axiosTransform' |
||||||
|
import { VAxios } from './Axios' |
||||||
|
import { checkStatus } from './checkStatus' |
||||||
|
import { formatRequestDate, joinTimestamp } from './helper' |
||||||
|
import type { RequestOptions, Result } from '/#/axios' |
||||||
|
import { AxiosRetry } from './axiosRetry' |
||||||
|
import { useMessage } from '@/hooks/useMessage' |
||||||
|
import { ContentTypeEnum, HttpErrorMsgEnum, HttpSuccessEnum, RequestEnum, ResultEnum } from '@/enums/httpEnum' |
||||||
|
import { isEmpty, isNull, isString, isUndefined } from '@/utils/is' |
||||||
|
import { deepMerge, setObjToUrlParams } from '@/utils' |
||||||
|
import { useUserStore } from '@/store/moules/userStore/index' |
||||||
|
|
||||||
|
const { createMessage, createErrorModal, createSuccessModal } = useMessage() |
||||||
|
|
||||||
|
// 请求白名单,无须token的接口
|
||||||
|
const whiteList: string[] = ['/login', '/refresh-token'] |
||||||
|
|
||||||
|
/** |
||||||
|
* @description: 数据处理,方便区分多种处理方式 |
||||||
|
*/ |
||||||
|
const transform: AxiosTransform = { |
||||||
|
/** |
||||||
|
* @description: 处理响应数据。如果数据不是预期格式,可直接抛出错误 |
||||||
|
*/ |
||||||
|
transformResponseHook: (res: AxiosResponse<Result>, options: RequestOptions) => { |
||||||
|
const { isTransformResponse, isReturnNativeResponse } = options |
||||||
|
// 二进制数据则直接返回
|
||||||
|
if (res.request.responseType === 'blob' || res.request.responseType === 'arraybuffer') |
||||||
|
return res.data |
||||||
|
|
||||||
|
// 是否返回原生响应头 比如:需要获取响应头时使用该属性
|
||||||
|
if (isReturnNativeResponse) |
||||||
|
return res |
||||||
|
|
||||||
|
// 不进行任何处理,直接返回
|
||||||
|
// 用于页面代码可能需要直接获取code,data,message这些信息时开启
|
||||||
|
if (!isTransformResponse) |
||||||
|
return res.data |
||||||
|
|
||||||
|
// 错误的时候返回
|
||||||
|
|
||||||
|
const { data } = res |
||||||
|
|
||||||
|
if (!data) { |
||||||
|
// return '[HTTP] Request has no return value';
|
||||||
|
throw new Error(HttpErrorMsgEnum.API_REQUEST_FAILED) |
||||||
|
} |
||||||
|
// 这里 code,result,message为 后台统一的字段,需要在 types.ts内修改为项目自己的接口返回格式
|
||||||
|
const { code, data: result, msg } = data |
||||||
|
// 这里逻辑可以根据项目进行修改
|
||||||
|
const hasSuccess = data && Reflect.has(data, 'code') && code === ResultEnum.SUCCESS |
||||||
|
if (hasSuccess) { |
||||||
|
let successMsg = msg |
||||||
|
|
||||||
|
if (isNull(successMsg) || isUndefined(successMsg) || isEmpty(successMsg)) |
||||||
|
successMsg = HttpSuccessEnum.OPERATION_SUCCESS |
||||||
|
|
||||||
|
if (options.successMessageMode === 'modal') |
||||||
|
createSuccessModal({ title: HttpSuccessEnum.SUCCESS_TIP, content: successMsg }) |
||||||
|
|
||||||
|
else if (options.successMessageMode === 'message') |
||||||
|
createMessage.success(successMsg) |
||||||
|
|
||||||
|
return result |
||||||
|
} |
||||||
|
|
||||||
|
// 在此处根据自己项目的实际情况对不同的code执行不同的操作
|
||||||
|
// 如果不希望中断当前请求,请return数据,否则直接抛出异常即可
|
||||||
|
let timeoutMsg = '' |
||||||
|
switch (code) { |
||||||
|
case ResultEnum.UNAUTHORIZED: |
||||||
|
timeoutMsg = HttpErrorMsgEnum.API_TIMEOUT_MESSAGE |
||||||
|
break |
||||||
|
default: |
||||||
|
if (msg) |
||||||
|
timeoutMsg = msg |
||||||
|
} |
||||||
|
|
||||||
|
// errorMessageMode='modal' 的时候会显示modal错误弹窗,而不是消息提示,用于一些比较重要的错误
|
||||||
|
// errorMessageMode='none' 一般是调用时明确表示不希望自动弹出错误提示
|
||||||
|
if (options.errorMessageMode === 'modal') |
||||||
|
createErrorModal({ title: HttpErrorMsgEnum.ERROR_TIP, content: timeoutMsg }) |
||||||
|
|
||||||
|
else if (options.errorMessageMode === 'message') |
||||||
|
createMessage.error(timeoutMsg) |
||||||
|
|
||||||
|
throw new Error(timeoutMsg || HttpErrorMsgEnum.API_REQUEST_FAILED) |
||||||
|
}, |
||||||
|
|
||||||
|
// 请求之前处理config
|
||||||
|
beforeRequestHook: (config, options) => { |
||||||
|
const { apiUrl, joinPrefix, joinParamsToUrl, formatDate, joinTime = true } = options |
||||||
|
|
||||||
|
if (joinPrefix) |
||||||
|
config.url = `${config.url}` |
||||||
|
|
||||||
|
if (apiUrl && isString(apiUrl)) |
||||||
|
config.url = `${apiUrl}${config.url}` |
||||||
|
|
||||||
|
const params = config.params || {} |
||||||
|
const data = config.data || false |
||||||
|
formatDate && data && !isString(data) && formatRequestDate(data) |
||||||
|
|
||||||
|
if (config.method?.toUpperCase() === RequestEnum.GET) { |
||||||
|
if (!isString(params)) { |
||||||
|
// 给 get 请求加上时间戳参数,避免从缓存中拿数据。
|
||||||
|
let url = `${config.url}?` |
||||||
|
for (const propName of Object.keys(params)) { |
||||||
|
const value = params[propName] |
||||||
|
|
||||||
|
if (value !== void 0 && value !== null && typeof value !== 'undefined') { |
||||||
|
if (typeof value === 'object') { |
||||||
|
for (const val of Object.keys(value)) { |
||||||
|
const paramss = `${propName}[${val}]` |
||||||
|
const subPart = `${encodeURIComponent(paramss)}=` |
||||||
|
url += `${subPart + encodeURIComponent(value[val])}&` |
||||||
|
} |
||||||
|
} |
||||||
|
else { |
||||||
|
url += `${propName}=${encodeURIComponent(value)}&` |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
url = url.slice(0, -1) |
||||||
|
config.params = {} |
||||||
|
config.url = url |
||||||
|
} |
||||||
|
else { |
||||||
|
// 兼容restful风格
|
||||||
|
config.url = `${config.url + params}${joinTimestamp(joinTime, true)}` |
||||||
|
config.params = undefined |
||||||
|
} |
||||||
|
} |
||||||
|
else { |
||||||
|
if (!isString(params)) { |
||||||
|
formatDate && formatRequestDate(params) |
||||||
|
if ( |
||||||
|
Reflect.has(config, 'data') |
||||||
|
&& config.data |
||||||
|
&& (Object.keys(config.data).length > 0 || config.data instanceof FormData) |
||||||
|
) { |
||||||
|
config.data = data |
||||||
|
config.params = params |
||||||
|
} |
||||||
|
else { |
||||||
|
// 非GET请求如果没有提供data,则将params视为data
|
||||||
|
config.data = params |
||||||
|
config.params = undefined |
||||||
|
} |
||||||
|
if (joinParamsToUrl) { |
||||||
|
config.url = setObjToUrlParams( |
||||||
|
config.url as string, |
||||||
|
Object.assign({}, config.params, config.data), |
||||||
|
) |
||||||
|
} |
||||||
|
} |
||||||
|
else { |
||||||
|
// 兼容restful风格
|
||||||
|
config.url = config.url + params |
||||||
|
config.params = undefined |
||||||
|
} |
||||||
|
} |
||||||
|
return config |
||||||
|
}, |
||||||
|
|
||||||
|
/** |
||||||
|
* @description: 请求拦截器处理 |
||||||
|
*/ |
||||||
|
requestInterceptors: (config, options) => { |
||||||
|
const userStore = useUserStore() |
||||||
|
|
||||||
|
// 是否需要设置 token
|
||||||
|
let isToken = (config as Recordable)?.requestOptions?.withToken === false |
||||||
|
isToken = whiteList.some((v) => { |
||||||
|
if (config.url) { |
||||||
|
config.url.includes(v) |
||||||
|
return false |
||||||
|
} |
||||||
|
return true |
||||||
|
}) |
||||||
|
// 请求之前处理config
|
||||||
|
const token = userStore.getToken |
||||||
|
if (token && !isToken) { |
||||||
|
// jwt token
|
||||||
|
(config as Recordable).headers[import.meta.env.VITE_GLOB_APP_TOKEN_KEY] = options.tokenScheme |
||||||
|
? `${options.tokenScheme} ${token}` |
||||||
|
: token |
||||||
|
} |
||||||
|
|
||||||
|
(config as Recordable).headers.Authorization = `${options.authenticationScheme} ${import.meta.env.VITE_GLOB_APP_AUTHORIZATION}` |
||||||
|
return config |
||||||
|
}, |
||||||
|
|
||||||
|
/** |
||||||
|
* @description: 响应拦截器处理 |
||||||
|
*/ |
||||||
|
responseInterceptors: (res: AxiosResponse<any>) => { |
||||||
|
return res |
||||||
|
}, |
||||||
|
|
||||||
|
/** |
||||||
|
* @description: 响应错误处理 |
||||||
|
*/ |
||||||
|
responseInterceptorsCatch: (axiosInstance: AxiosInstance, error: any) => { |
||||||
|
const { response, code, message, config } = error || {} |
||||||
|
const errorMessageMode = config?.requestOptions?.errorMessageMode || 'none' |
||||||
|
const msg: string = response?.data?.msg ?? '' |
||||||
|
const err: string = error?.toString?.() ?? '' |
||||||
|
let errMessage = '' |
||||||
|
|
||||||
|
if (axios.isCancel(error)) |
||||||
|
return Promise.reject(error) |
||||||
|
|
||||||
|
try { |
||||||
|
if (code === 'ECONNABORTED' && message.includes('timeout')) |
||||||
|
errMessage = HttpErrorMsgEnum.API_TIMEOUT_MESSAGE |
||||||
|
|
||||||
|
if (err?.includes('Network Error')) |
||||||
|
errMessage = HttpErrorMsgEnum.NETWORK_EXCEPTION |
||||||
|
|
||||||
|
if (errMessage) { |
||||||
|
if (errorMessageMode === 'modal') |
||||||
|
createErrorModal({ title: HttpErrorMsgEnum.ERROR_TIP, content: errMessage }) |
||||||
|
|
||||||
|
else if (errorMessageMode === 'message') |
||||||
|
createMessage.error(errMessage) |
||||||
|
|
||||||
|
return Promise.reject(error) |
||||||
|
} |
||||||
|
} |
||||||
|
catch (error) { |
||||||
|
throw new Error(error as unknown as string) |
||||||
|
} |
||||||
|
|
||||||
|
checkStatus(error?.response?.status, msg, errorMessageMode) |
||||||
|
|
||||||
|
// 添加自动重试机制 保险起见 只针对GET请求
|
||||||
|
const retryRequest = new AxiosRetry() |
||||||
|
const { isOpenRetry } = config.requestOptions.retryRequest |
||||||
|
|
||||||
|
config.method?.toUpperCase() === RequestEnum.GET |
||||||
|
&& isOpenRetry |
||||||
|
&& retryRequest.retry(axiosInstance, error) |
||||||
|
return Promise.reject(error) |
||||||
|
}, |
||||||
|
} |
||||||
|
|
||||||
|
function createAxios(opt?: Partial<CreateAxiosOptions>) { |
||||||
|
return new VAxios( |
||||||
|
// 深度合并
|
||||||
|
deepMerge( |
||||||
|
{ |
||||||
|
// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#authentication_schemes
|
||||||
|
// authentication schemes,e.g: Bearer
|
||||||
|
tokenScheme: 'crypto', |
||||||
|
authenticationScheme: 'Basic', |
||||||
|
timeout: 10 * 1000, |
||||||
|
// 基础接口地址
|
||||||
|
// baseURL: globSetting.apiUrl,
|
||||||
|
|
||||||
|
headers: { 'Content-Type': ContentTypeEnum.JSON }, |
||||||
|
// 如果是form-data格式
|
||||||
|
// headers: { 'Content-Type': ContentTypeEnum.FORM_URLENCODED },
|
||||||
|
// 数据处理方式
|
||||||
|
transform: clone(transform), |
||||||
|
// 配置项,下面的选项都可以在独立的接口请求中覆盖
|
||||||
|
requestOptions: { |
||||||
|
// 默认将prefix 添加到url
|
||||||
|
joinPrefix: true, |
||||||
|
// 是否返回原生响应头 比如:需要获取响应头时使用该属性
|
||||||
|
isReturnNativeResponse: false, |
||||||
|
// 需要对返回数据进行处理
|
||||||
|
isTransformResponse: true, |
||||||
|
// post请求的时候添加参数到url
|
||||||
|
joinParamsToUrl: false, |
||||||
|
// 格式化提交参数时间
|
||||||
|
formatDate: true, |
||||||
|
// 消息提示类型
|
||||||
|
errorMessageMode: 'message', |
||||||
|
// 接口地址
|
||||||
|
apiUrl: import.meta.env.VITE_GLOB_BASE_URL, |
||||||
|
// 是否加入时间戳
|
||||||
|
joinTime: true, |
||||||
|
// 忽略重复请求
|
||||||
|
ignoreCancelToken: true, |
||||||
|
// 是否携带token
|
||||||
|
withToken: true, |
||||||
|
retryRequest: { |
||||||
|
isOpenRetry: true, |
||||||
|
count: 5, |
||||||
|
waitTime: 100, |
||||||
|
}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
opt || {}, |
||||||
|
), |
||||||
|
) |
||||||
|
} |
||||||
|
export const defHttp = createAxios() |
||||||
|
|
||||||
|
// other api url
|
||||||
|
// export const otherHttp = createAxios({
|
||||||
|
// requestOptions: {
|
||||||
|
// apiUrl: 'xxx',
|
||||||
|
// urlPrefix: 'xxx',
|
||||||
|
// },
|
||||||
|
// });
|
@ -0,0 +1,48 @@ |
|||||||
|
import CryptoJS from 'crypto-js' |
||||||
|
|
||||||
|
export default class crypto { |
||||||
|
/** |
||||||
|
* token加密key 使用@org.springblade.test.CryptoKeyGenerator获取,需和后端配置保持一致 |
||||||
|
* @type {string} |
||||||
|
*/ |
||||||
|
|
||||||
|
static cryptoKey: string = 'Zc72Ghs63Z2b8jl7PXnr68r7B69xmRLX' |
||||||
|
/** |
||||||
|
* 报文加密key 使用@org.springblade.test.CryptoKeyGenerator获取,需和后端配置保持一致 |
||||||
|
* @type {string} |
||||||
|
*/ |
||||||
|
static aesKey: string = 'OPGbg7HHRiClg4u9euSPXt5Dtwed9qcG' |
||||||
|
|
||||||
|
/** |
||||||
|
* 本地加密key 使用@org.springblade.test.CryptoKeyGenerator获取,需和后端配置保持一致 |
||||||
|
* @type {string} |
||||||
|
*/ |
||||||
|
static localKey: string = 'LaiJiangKeLiuXingMing_LaoTie6666' |
||||||
|
|
||||||
|
/** |
||||||
|
* aes 加密方法,同java:AesUtil.encryptToBase64(text, aesKey); |
||||||
|
*/ |
||||||
|
static encryptAES(data: string, key: string) { |
||||||
|
const dataBytes = CryptoJS.enc.Utf8.parse(data) |
||||||
|
const keyBytes = CryptoJS.enc.Utf8.parse(key) |
||||||
|
const encrypted = CryptoJS.AES.encrypt(dataBytes, keyBytes, { |
||||||
|
iv: keyBytes, |
||||||
|
mode: CryptoJS.mode.CBC, |
||||||
|
padding: CryptoJS.pad.Pkcs7, |
||||||
|
}) |
||||||
|
return CryptoJS.enc.Base64.stringify(encrypted.ciphertext) |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* aes 解密方法,同java:AesUtil.decryptFormBase64ToString(encrypt, aesKey); |
||||||
|
*/ |
||||||
|
static decryptAES(data: string | CryptoJS.lib.CipherParams, key: string) { |
||||||
|
const keyBytes = CryptoJS.enc.Utf8.parse(key) |
||||||
|
const decrypted = CryptoJS.AES.decrypt(data, keyBytes, { |
||||||
|
iv: keyBytes, |
||||||
|
mode: CryptoJS.mode.CBC, |
||||||
|
padding: CryptoJS.pad.Pkcs7, |
||||||
|
}) |
||||||
|
return CryptoJS.enc.Utf8.stringify(decrypted) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,3 @@ |
|||||||
|
export function getEnv() { |
||||||
|
return import.meta.env |
||||||
|
} |
@ -0,0 +1,42 @@ |
|||||||
|
/** |
||||||
|
* @description: base64 to blob |
||||||
|
*/ |
||||||
|
export function dataURLtoBlob(base64Buf: string): Blob { |
||||||
|
const arr = base64Buf.split(',') |
||||||
|
const typeItem = arr[0] |
||||||
|
const mime = typeItem.match(/:(.*?);/)![1] |
||||||
|
const bstr = window.atob(arr[1]) |
||||||
|
let n = bstr.length |
||||||
|
const u8arr = new Uint8Array(n) |
||||||
|
while (n--) |
||||||
|
u8arr[n] = bstr.charCodeAt(n) |
||||||
|
|
||||||
|
return new Blob([u8arr], { type: mime }) |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* img url to base64 |
||||||
|
* @param url |
||||||
|
*/ |
||||||
|
export function urlToBase64(url: string, mineType?: string): Promise<string> { |
||||||
|
return new Promise((resolve, reject) => { |
||||||
|
let canvas = document.createElement('CANVAS') as Nullable<HTMLCanvasElement> |
||||||
|
const ctx = canvas!.getContext('2d') |
||||||
|
|
||||||
|
const img = new Image() |
||||||
|
img.crossOrigin = '' |
||||||
|
img.onload = function () { |
||||||
|
if (!canvas || !ctx) |
||||||
|
// eslint-disable-next-line prefer-promise-reject-errors
|
||||||
|
return reject() |
||||||
|
|
||||||
|
canvas.height = img.height |
||||||
|
canvas.width = img.width |
||||||
|
ctx.drawImage(img, 0, 0) |
||||||
|
const dataURL = canvas.toDataURL(mineType || 'image/png') |
||||||
|
canvas = null |
||||||
|
resolve(dataURL) |
||||||
|
} |
||||||
|
img.src = url |
||||||
|
}) |
||||||
|
} |
@ -0,0 +1,86 @@ |
|||||||
|
import { openWindow } from '..' |
||||||
|
import { dataURLtoBlob, urlToBase64 } from './base64Conver' |
||||||
|
|
||||||
|
/** |
||||||
|
* Download online pictures |
||||||
|
* @param url |
||||||
|
* @param filename |
||||||
|
* @param mime |
||||||
|
* @param bom |
||||||
|
*/ |
||||||
|
export function downloadByOnlineUrl(url: string, filename: string, mime?: string, bom?: BlobPart) { |
||||||
|
urlToBase64(url).then((base64) => { |
||||||
|
downloadByBase64(base64, filename, mime, bom) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Download pictures based on base64 |
||||||
|
* @param buf |
||||||
|
* @param filename |
||||||
|
* @param mime |
||||||
|
* @param bom |
||||||
|
*/ |
||||||
|
export function downloadByBase64(buf: string, filename: string, mime?: string, bom?: BlobPart) { |
||||||
|
const base64Buf = dataURLtoBlob(buf) |
||||||
|
downloadByData(base64Buf, filename, mime, bom) |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Download according to the background interface file stream |
||||||
|
* @param {*} data |
||||||
|
* @param {*} filename |
||||||
|
* @param {*} mime |
||||||
|
* @param {*} bom |
||||||
|
*/ |
||||||
|
export function downloadByData(data: BlobPart, filename: string, mime?: string, bom?: BlobPart) { |
||||||
|
const blobData = typeof bom !== 'undefined' ? [bom, data] : [data] |
||||||
|
const blob = new Blob(blobData, { type: mime || 'application/octet-stream' }) |
||||||
|
|
||||||
|
const blobURL = window.URL.createObjectURL(blob) |
||||||
|
const tempLink = document.createElement('a') |
||||||
|
tempLink.style.display = 'none' |
||||||
|
tempLink.href = blobURL |
||||||
|
tempLink.setAttribute('download', filename) |
||||||
|
if (typeof tempLink.download === 'undefined') |
||||||
|
tempLink.setAttribute('target', '_blank') |
||||||
|
|
||||||
|
document.body.appendChild(tempLink) |
||||||
|
tempLink.click() |
||||||
|
document.body.removeChild(tempLink) |
||||||
|
window.URL.revokeObjectURL(blobURL) |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Download file according to file address |
||||||
|
* @param {*} sUrl |
||||||
|
*/ |
||||||
|
export function downloadByUrl({ url, target = '_blank', fileName }: { url: string, target?: TargetContext, fileName?: string }): boolean { |
||||||
|
const isChrome = window.navigator.userAgent.toLowerCase().includes('chrome') |
||||||
|
const isSafari = window.navigator.userAgent.toLowerCase().includes('safari') |
||||||
|
|
||||||
|
if (/(iP)/g.test(window.navigator.userAgent)) { |
||||||
|
console.error('Your browser does not support download!') |
||||||
|
return false |
||||||
|
} |
||||||
|
if (isChrome || isSafari) { |
||||||
|
const link = document.createElement('a') |
||||||
|
link.href = url |
||||||
|
link.target = target |
||||||
|
|
||||||
|
if (link.download !== undefined) |
||||||
|
link.download = fileName || url.substring(url.lastIndexOf('/') + 1, url.length) |
||||||
|
|
||||||
|
if (document.createEvent) { |
||||||
|
const e = document.createEvent('MouseEvents') |
||||||
|
e.initEvent('click', true, true) |
||||||
|
link.dispatchEvent(e) |
||||||
|
return true |
||||||
|
} |
||||||
|
} |
||||||
|
if (!url.includes('?')) |
||||||
|
url += '?download' |
||||||
|
|
||||||
|
openWindow(url, { target }) |
||||||
|
return true |
||||||
|
} |
@ -0,0 +1,81 @@ |
|||||||
|
import { intersectionWith, isEqual, mergeWith, unionWith } from 'lodash-es' |
||||||
|
import { isArray, isObject } from '@/utils/is' |
||||||
|
|
||||||
|
/** |
||||||
|
* Add the object as a parameter to the URL |
||||||
|
* @param baseUrl url |
||||||
|
* @param obj |
||||||
|
* @returns {string} |
||||||
|
* eg: |
||||||
|
* let obj = {a: '3', b: '4'} |
||||||
|
* setObjToUrlParams('www.baidu.com', obj) |
||||||
|
* ==>www.baidu.com?a=3&b=4 |
||||||
|
*/ |
||||||
|
export function setObjToUrlParams(baseUrl: string, obj: any): string { |
||||||
|
let parameters = '' |
||||||
|
for (const key in obj) |
||||||
|
parameters += `${key}=${encodeURIComponent(obj[key])}&` |
||||||
|
|
||||||
|
parameters = parameters.replace(/&$/, '') |
||||||
|
return /\?$/.test(baseUrl) ? baseUrl + parameters : baseUrl.replace(/\/?$/, '?') + parameters |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Recursively merge two objects. |
||||||
|
* 递归合并两个对象。 |
||||||
|
* |
||||||
|
* @param source The source object to merge from. 要合并的源对象。 |
||||||
|
* @param target The target object to merge into. 目标对象,合并后结果存放于此。 |
||||||
|
* @param mergeArrays How to merge arrays. Default is "replace". |
||||||
|
* 如何合并数组。默认为replace。 |
||||||
|
* - "union": Union the arrays. 对数组执行并集操作。 |
||||||
|
* - "intersection": Intersect the arrays. 对数组执行交集操作。 |
||||||
|
* - "concat": Concatenate the arrays. 连接数组。 |
||||||
|
* - "replace": Replace the source array with the target array. 用目标数组替换源数组。 |
||||||
|
* @returns The merged object. 合并后的对象。 |
||||||
|
*/ |
||||||
|
export function deepMerge<T extends object | null | undefined, U extends object | null | undefined>( |
||||||
|
source: T, |
||||||
|
target: U, |
||||||
|
mergeArrays: 'union' | 'intersection' | 'concat' | 'replace' = 'replace', |
||||||
|
): T & U { |
||||||
|
if (!target) |
||||||
|
return source as T & U |
||||||
|
|
||||||
|
if (!source) |
||||||
|
return target as T & U |
||||||
|
|
||||||
|
return mergeWith({}, source, target, (sourceValue, targetValue) => { |
||||||
|
if (isArray(targetValue) && isArray(sourceValue)) { |
||||||
|
switch (mergeArrays) { |
||||||
|
case 'union': |
||||||
|
return unionWith(sourceValue, targetValue, isEqual) |
||||||
|
case 'intersection': |
||||||
|
return intersectionWith(sourceValue, targetValue, isEqual) |
||||||
|
case 'concat': |
||||||
|
return sourceValue.concat(targetValue) |
||||||
|
case 'replace': |
||||||
|
return targetValue |
||||||
|
default: |
||||||
|
throw new Error(`Unknown merge array strategy: ${mergeArrays as string}`) |
||||||
|
} |
||||||
|
} |
||||||
|
if (isObject(targetValue) && isObject(sourceValue)) |
||||||
|
return deepMerge(sourceValue, targetValue, mergeArrays) |
||||||
|
|
||||||
|
return undefined |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
export function openWindow( |
||||||
|
url: string, |
||||||
|
opt?: { target?: TargetContext | string, noopener?: boolean, noreferrer?: boolean }, |
||||||
|
) { |
||||||
|
const { target = '__blank', noopener = true, noreferrer = true } = opt || {} |
||||||
|
const feature: string[] = [] |
||||||
|
|
||||||
|
noopener && feature.push('noopener=yes') |
||||||
|
noreferrer && feature.push('noreferrer=yes') |
||||||
|
|
||||||
|
window.open(url, target, feature.join(',')) |
||||||
|
} |
@ -0,0 +1,66 @@ |
|||||||
|
export { |
||||||
|
isArguments, |
||||||
|
isArrayBuffer, |
||||||
|
isArrayLike, |
||||||
|
isArrayLikeObject, |
||||||
|
isBuffer, |
||||||
|
isBoolean, |
||||||
|
isDate, |
||||||
|
isElement, |
||||||
|
isEmpty, |
||||||
|
isEqual, |
||||||
|
isEqualWith, |
||||||
|
isError, |
||||||
|
isFunction, |
||||||
|
isFinite, |
||||||
|
isLength, |
||||||
|
isMap, |
||||||
|
isMatch, |
||||||
|
isMatchWith, |
||||||
|
isNative, |
||||||
|
isNil, |
||||||
|
isNumber, |
||||||
|
isNull, |
||||||
|
isObjectLike, |
||||||
|
isPlainObject, |
||||||
|
isRegExp, |
||||||
|
isSafeInteger, |
||||||
|
isSet, |
||||||
|
isString, |
||||||
|
isSymbol, |
||||||
|
isTypedArray, |
||||||
|
isUndefined, |
||||||
|
isWeakMap, |
||||||
|
isWeakSet, |
||||||
|
} from 'lodash-es' |
||||||
|
|
||||||
|
const toString = Object.prototype.toString |
||||||
|
|
||||||
|
export function is(val: unknown, type: string) { |
||||||
|
return toString.call(val) === `[object ${type}]` |
||||||
|
} |
||||||
|
|
||||||
|
export function isDef<T = unknown>(val?: T): val is T { |
||||||
|
return typeof val !== 'undefined' |
||||||
|
} |
||||||
|
|
||||||
|
export function isObject(val: any): val is Record<any, any> { |
||||||
|
return val !== null && is(val, 'Object') |
||||||
|
} |
||||||
|
|
||||||
|
export function isArray(val: any): val is Array<any> { |
||||||
|
return val && Array.isArray(val) |
||||||
|
} |
||||||
|
|
||||||
|
export function isWindow(val: any): val is Window { |
||||||
|
return typeof window !== 'undefined' && is(val, 'Window') |
||||||
|
} |
||||||
|
|
||||||
|
export const isServer = typeof window === 'undefined' |
||||||
|
|
||||||
|
export const isClient = !isServer |
||||||
|
|
||||||
|
export function isHttpUrl(path: string): boolean { |
||||||
|
const reg = /^http(s)?:\/\/([\w-]+\.)+[\w-]+(\/[\w- ./?%&=]*)?/ |
||||||
|
return reg.test(path) |
||||||
|
} |
@ -0,0 +1,36 @@ |
|||||||
|
<script setup lang="ts"> |
||||||
|
import { ref } from 'vue' |
||||||
|
import { AppContainerBox } from '@/components/AppContainerBox' |
||||||
|
import { AppSubMenuTitle } from '@/components/AppSubMenuTitle' |
||||||
|
import { AppSubMenuList } from '@/components/AppSubMenuList' |
||||||
|
import type { SubMenuItem } from '@/components/AppSubMenuList/index.d' |
||||||
|
|
||||||
|
const subMenuActive = ref(0) |
||||||
|
const subMenuList = ref<SubMenuItem[]>([ |
||||||
|
{ |
||||||
|
title: '新对话1', |
||||||
|
content: '这是一个新的对话哦;啦啦啦', |
||||||
|
id: '1', |
||||||
|
}, |
||||||
|
{ |
||||||
|
title: '新对话2', |
||||||
|
content: '这是一个新的对话哦', |
||||||
|
id: '2', |
||||||
|
}, |
||||||
|
]) |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<AppContainerBox> |
||||||
|
<template #subMenu> |
||||||
|
<AppSubMenuTitle></AppSubMenuTitle> |
||||||
|
<AppSubMenuList :list="subMenuList" :active-index="subMenuActive"></AppSubMenuList> |
||||||
|
我是子菜单 |
||||||
|
</template> |
||||||
|
<template #content> |
||||||
|
我是内容区 |
||||||
|
</template> |
||||||
|
</AppContainerBox> |
||||||
|
</template> |
||||||
|
|
||||||
|
<style scoped></style> |
@ -0,0 +1,11 @@ |
|||||||
|
<script setup lang="ts"> |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<div class="h-full w-full flex flex-col items-center justify-center"> |
||||||
|
<h3>404</h3> |
||||||
|
<p>抱歉,您访问的页面不存在。</p> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<style scoped></style> |
@ -1,13 +0,0 @@ |
|||||||
<script setup lang="ts"> |
|
||||||
import { computed } from 'vue' |
|
||||||
|
|
||||||
const aa = computed(() => 123) |
|
||||||
</script> |
|
||||||
|
|
||||||
<template> |
|
||||||
<div> |
|
||||||
我是123{{ aa }} |
|
||||||
</div> |
|
||||||
</template> |
|
||||||
|
|
||||||
<style></style> |
|
@ -0,0 +1,57 @@ |
|||||||
|
<script setup lang="ts"> |
||||||
|
import { ref } from 'vue' |
||||||
|
import { Button, Input } from 'ant-design-vue' |
||||||
|
import { useRouter } from 'vue-router' |
||||||
|
import { sendCode } from '@/api/base/login' |
||||||
|
import { useUserStore } from '@/store/moules/userStore/index' |
||||||
|
import { useMessage } from '@/hooks/useMessage' |
||||||
|
import { GrantTypeEnum, TypeEnum, UserTypeEnum } from '@/enums/commonEnum' |
||||||
|
|
||||||
|
const router = useRouter() |
||||||
|
const userStore = useUserStore() |
||||||
|
const { createMessage } = useMessage() |
||||||
|
const phoneCode = ref('') |
||||||
|
function handleLogin() { |
||||||
|
userStore.login({ |
||||||
|
user_type: UserTypeEnum.C, |
||||||
|
grant_type: GrantTypeEnum.SMS, |
||||||
|
invite_code: '', |
||||||
|
phone: '13864541890', |
||||||
|
phoneCode: phoneCode.value, |
||||||
|
type: TypeEnum.PHONE, |
||||||
|
}).then(() => { |
||||||
|
createMessage.success('登录成功') |
||||||
|
router.push('/') |
||||||
|
}).catch(() => { |
||||||
|
createMessage.error('登录失败') |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
function handleSendCode() { |
||||||
|
sendCode('13864541890') |
||||||
|
} |
||||||
|
|
||||||
|
function handleLogout() { |
||||||
|
userStore.logout() |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<div> |
||||||
|
我是登录页 |
||||||
|
<Input v-model:value="phoneCode" placeholder="请输入验证码" /> |
||||||
|
<Button type="primary" @click="handleSendCode"> |
||||||
|
发送验证码 |
||||||
|
</Button> |
||||||
|
|
||||||
|
<Button type="primary" @click="handleLogin"> |
||||||
|
登录 |
||||||
|
</Button> |
||||||
|
|
||||||
|
<Button type="primary" @click="handleLogout"> |
||||||
|
退出 |
||||||
|
</Button> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<style scoped></style> |
@ -0,0 +1,16 @@ |
|||||||
|
<script setup lang="ts"> |
||||||
|
import { AppContainerBox } from '@/components/AppContainerBox' |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<AppContainerBox> |
||||||
|
<template #subMenu> |
||||||
|
我是子菜单 |
||||||
|
</template> |
||||||
|
<template #content> |
||||||
|
我是文生图区域 |
||||||
|
</template> |
||||||
|
</AppContainerBox> |
||||||
|
</template> |
||||||
|
|
||||||
|
<style scoped></style> |
@ -0,0 +1,54 @@ |
|||||||
|
export type ErrorMessageMode = 'none' | 'modal' | 'message' | undefined |
||||||
|
export type SuccessMessageMode = ErrorMessageMode |
||||||
|
|
||||||
|
export interface RequestOptions { |
||||||
|
// Splicing request parameters to url
|
||||||
|
joinParamsToUrl?: boolean |
||||||
|
// Format request parameter time
|
||||||
|
formatDate?: boolean |
||||||
|
// Whether to process the request result
|
||||||
|
isTransformResponse?: boolean |
||||||
|
// Whether to return native response headers
|
||||||
|
// For example: use this attribute when you need to get the response headers
|
||||||
|
isReturnNativeResponse?: boolean |
||||||
|
// Whether to join url
|
||||||
|
joinPrefix?: boolean |
||||||
|
// Interface address, use the default apiUrl if you leave it blank
|
||||||
|
apiUrl?: string |
||||||
|
// Error message prompt type
|
||||||
|
errorMessageMode?: ErrorMessageMode |
||||||
|
// Success message prompt type
|
||||||
|
successMessageMode?: SuccessMessageMode |
||||||
|
// Whether to add a timestamp
|
||||||
|
joinTime?: boolean |
||||||
|
ignoreCancelToken?: boolean |
||||||
|
// Whether to send token in header
|
||||||
|
withToken?: boolean |
||||||
|
// 请求重试机制
|
||||||
|
retryRequest?: RetryRequest |
||||||
|
} |
||||||
|
|
||||||
|
export interface RetryRequest { |
||||||
|
isOpenRetry: boolean |
||||||
|
count: number |
||||||
|
waitTime: number |
||||||
|
} |
||||||
|
|
||||||
|
export interface Result<T = any> { |
||||||
|
code: number |
||||||
|
msg: string |
||||||
|
data: T |
||||||
|
} |
||||||
|
|
||||||
|
// multipart/form-data: upload file
|
||||||
|
export interface UploadFileParams { |
||||||
|
// Other parameters
|
||||||
|
data?: Recordable |
||||||
|
// File parameter interface field name
|
||||||
|
name?: string |
||||||
|
// file name
|
||||||
|
file: File | Blob |
||||||
|
// file name
|
||||||
|
filename?: string |
||||||
|
[key: string]: any |
||||||
|
} |
@ -0,0 +1,98 @@ |
|||||||
|
import type { ComponentPublicInstance, ComponentRenderProxy, FunctionalComponent, VNode, VNodeChild, PropType as VuePropType } from 'vue' |
||||||
|
|
||||||
|
declare type Recordable<T = any> = Record<string, T> |
||||||
|
|
||||||
|
declare type Nullable<T> = T | null |
||||||
|
|
||||||
|
declare global { |
||||||
|
const __APP_INFO__: { |
||||||
|
pkg: { |
||||||
|
name: string |
||||||
|
version: string |
||||||
|
dependencies: Recordable<string> |
||||||
|
devDependencies: Recordable<string> |
||||||
|
} |
||||||
|
lastBuildTime: string |
||||||
|
} |
||||||
|
declare interface Window { |
||||||
|
_hmt: [string, string][] |
||||||
|
} |
||||||
|
|
||||||
|
interface Document { |
||||||
|
mozFullScreenElement?: Element |
||||||
|
msFullscreenElement?: Element |
||||||
|
webkitFullscreenElement?: Element |
||||||
|
} |
||||||
|
|
||||||
|
// vue
|
||||||
|
declare type PropType<T> = VuePropType<T> |
||||||
|
declare type VueNode = VNodeChild | JSX.Element |
||||||
|
|
||||||
|
export type Writable<T> = { |
||||||
|
-readonly [P in keyof T]: T[P] |
||||||
|
} |
||||||
|
|
||||||
|
declare type Nullable<T> = T | null |
||||||
|
declare type NonNullable<T> = T extends null | undefined ? never : T |
||||||
|
declare type Recordable<T = any> = Record<string, T> |
||||||
|
declare interface ReadonlyRecordable<T = any> { |
||||||
|
readonly [key: string]: T |
||||||
|
} |
||||||
|
declare interface Indexable<T = any> { |
||||||
|
[key: string]: T |
||||||
|
} |
||||||
|
declare type DeepPartial<T> = { |
||||||
|
[P in keyof T]?: DeepPartial<T[P]> |
||||||
|
} |
||||||
|
declare type TimeoutHandle = ReturnType<typeof setTimeout> |
||||||
|
declare type IntervalHandle = ReturnType<typeof setInterval> |
||||||
|
|
||||||
|
declare interface ChangeEvent extends Event { |
||||||
|
target: HTMLInputElement |
||||||
|
} |
||||||
|
|
||||||
|
declare interface WheelEvent { |
||||||
|
path?: EventTarget[] |
||||||
|
} |
||||||
|
interface ImportMetaEnv extends ViteEnv { |
||||||
|
__: unknown |
||||||
|
} |
||||||
|
|
||||||
|
declare interface ViteEnv { |
||||||
|
VITE_PORT: number |
||||||
|
VITE_USE_PWA: boolean |
||||||
|
VITE_PUBLIC_PATH: string |
||||||
|
VITE_PROXY: [string, string][] |
||||||
|
VITE_GLOB_APP_TITLE: string |
||||||
|
VITE_GLOB_APP_SHORT_NAME: string |
||||||
|
VITE_USE_CDN: boolean |
||||||
|
VITE_DROP_CONSOLE: boolean |
||||||
|
VITE_BUILD_COMPRESS: 'gzip' | 'brotli' | 'none' |
||||||
|
VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE: boolean |
||||||
|
VITE_GENERATE_UI: string |
||||||
|
} |
||||||
|
|
||||||
|
declare function parseInt(s: string | number, radix?: number): number |
||||||
|
|
||||||
|
declare function parseFloat(string: string | number): number |
||||||
|
|
||||||
|
namespace JSX { |
||||||
|
// tslint:disable no-empty-interface
|
||||||
|
type Element = VNode |
||||||
|
// tslint:disable no-empty-interface
|
||||||
|
type ElementClass = ComponentRenderProxy |
||||||
|
interface ElementAttributesProperty { |
||||||
|
$props: any |
||||||
|
} |
||||||
|
interface IntrinsicElements { |
||||||
|
[elem: string]: any |
||||||
|
} |
||||||
|
interface IntrinsicAttributes { |
||||||
|
[elem: string]: any |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
declare module 'vue' { |
||||||
|
export type JSXComponent<Props = any> = { new (): ComponentPublicInstance<Props> } | FunctionalComponent<Props> |
||||||
|
} |
@ -0,0 +1 @@ |
|||||||
|
declare type TargetContext = '_self' | '_blank' |
@ -0,0 +1,6 @@ |
|||||||
|
declare module '*.vue' { |
||||||
|
import type { DefineComponent } from 'vue' |
||||||
|
|
||||||
|
const component: DefineComponent<object, object, any> |
||||||
|
export default component |
||||||
|
} |
@ -0,0 +1,12 @@ |
|||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
interface ImportMetaEnv { |
||||||
|
NODE_ENV: string |
||||||
|
|
||||||
|
VITE_GLOB_APP_TITLE: string |
||||||
|
VITE_GLOB_APP_SHORT_NAME: string |
||||||
|
VITE_GLOB_BASE_URL: string |
||||||
|
VITE_GLOB_MQTT_URL: string |
||||||
|
VITE_GLOB_APP_AUTHORIZATION: string |
||||||
|
VITE_GLOB_APP_TOKEN_KEY: string |
||||||
|
} |
@ -0,0 +1,19 @@ |
|||||||
|
export {} |
||||||
|
|
||||||
|
declare module 'vue-router' { |
||||||
|
interface RouteMeta extends Record<string | number | symbol, unknown> { |
||||||
|
orderNo?: number |
||||||
|
// 路由title 一般必填
|
||||||
|
title: string |
||||||
|
// 是否忽略权限,只在权限模式为Role的时候有效
|
||||||
|
ignoreAuth?: boolean |
||||||
|
// 是否固定标签
|
||||||
|
affix?: boolean |
||||||
|
// 图标,也是菜单图标
|
||||||
|
icon?: string |
||||||
|
// img on tab
|
||||||
|
img?: string |
||||||
|
// 内嵌iframe的地址
|
||||||
|
frameSrc?: string |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue