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"> |
||||
import { computed, ref } from 'vue' |
||||
import { ConfigProvider, theme } from 'ant-design-vue' |
||||
import { computed } from 'vue' |
||||
import { App, ConfigProvider } from 'ant-design-vue' |
||||
import zhCN from 'ant-design-vue/es/locale/zh_CN' |
||||
|
||||
const { darkAlgorithm } = theme |
||||
const darkTheme = { |
||||
algorithm: [darkAlgorithm], |
||||
} |
||||
const isDark = ref(true) |
||||
const themeConfig = computed(() => |
||||
Object.assign( |
||||
{ |
||||
token: { |
||||
colorPrimary: '#0960bd', |
||||
colorPrimary: '#4670E3', |
||||
colorSuccess: '#55D187', |
||||
colorWarning: '#EFBD47', |
||||
colorError: '#ED6F6F', |
||||
colorInfo: '#0960bd', |
||||
colorInfo: '#4670E3', |
||||
}, |
||||
}, |
||||
isDark.value ? darkTheme : {}, |
||||
), |
||||
) |
||||
</script> |
||||
|
||||
<template> |
||||
<ConfigProvider :locale="zhCN" locale-data="zh-CN" :theme="themeConfig"> |
||||
<RouterView /> |
||||
<App class="h-full w-full"> |
||||
<RouterView /> |
||||
</App> |
||||
</ConfigProvider> |
||||
</template> |
||||
|
||||
<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> |
||||
|
@ -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