Browse Source

feat:1. 封装axios;2. 封装菜单栏;3. 封装子菜单栏;4. 封装整体布局;5. 配置vscode规则;

dxj
李朋徽 1 year ago
parent
commit
552a6821c1
  1. 8
      .env
  2. 10
      .env.development
  3. 10
      .env.production
  4. 4
      .vscode/settings.json
  5. 3
      eslint.config.js
  6. 2
      index.html
  7. 19
      package.json
  8. 1310
      pnpm-lock.yaml
  9. 30
      src/App.vue
  10. 23
      src/api/base/login.ts
  11. 0
      src/api/base/user.ts
  12. BIN
      src/assets/images/bg_menu.png
  13. BIN
      src/assets/images/logo.png
  14. 1
      src/assets/svg/404.svg
  15. 11
      src/assets/svg/jue_se.svg
  16. 12
      src/assets/svg/qu_dao.svg
  17. 15
      src/assets/svg/ren_wu.svg
  18. 1
      src/assets/svg/vue.svg
  19. 18
      src/assets/svg/wei_xin.svg
  20. 13
      src/assets/svg/wen_sheng_tu.svg
  21. 14
      src/assets/svg/wo_de.svg
  22. 22
      src/assets/svg/xiao_cheng_xu.svg
  23. 3
      src/components/AppContainerBox/index.ts
  24. 22
      src/components/AppContainerBox/index.vue
  25. 3
      src/components/AppContentBox/index.ts
  26. 17
      src/components/AppContentBox/index.vue
  27. 3
      src/components/AppSubMenuBox/index.ts
  28. 14
      src/components/AppSubMenuBox/index.vue
  29. 5
      src/components/AppSubMenuList/index.d.ts
  30. 3
      src/components/AppSubMenuList/index.ts
  31. 51
      src/components/AppSubMenuList/index.vue
  32. 3
      src/components/AppSubMenuTitle/index.ts
  33. 36
      src/components/AppSubMenuTitle/index.vue
  34. 41
      src/components/HelloWorld.vue
  35. 3
      src/components/SvgIcon/index.ts
  36. 51
      src/components/SvgIcon/index.vue
  37. 72
      src/design/index.scss
  38. 3
      src/design/mixins/config.scss
  39. 6
      src/design/mixins/mixins.scss
  40. 49
      src/design/public.scss
  41. 12
      src/enums/cacheEnum.ts
  42. 14
      src/enums/commonEnum.ts
  43. 55
      src/enums/httpEnum.ts
  44. 22
      src/enums/menuEnum.ts
  45. 11
      src/enums/pageEnum.ts
  46. 111
      src/hooks/useMessage.tsx
  47. 14
      src/layout/AppMain/index.vue
  48. 8
      src/layout/AppMenu/index.d.ts
  49. 131
      src/layout/AppMenu/index.vue
  50. 23
      src/layout/index.vue
  51. 23
      src/main.ts
  52. 35
      src/router/guard.ts
  53. 47
      src/router/index.ts
  54. 11
      src/store/index.ts
  55. 15
      src/store/moules/userStore/index.d.ts
  56. 73
      src/store/moules/userStore/index.ts
  57. 79
      src/style.css
  58. 337
      src/utils/axios/Axios.ts
  59. 59
      src/utils/axios/axiosCancel.ts
  60. 33
      src/utils/axios/axiosRetry.ts
  61. 57
      src/utils/axios/axiosTransform.ts
  62. 69
      src/utils/axios/checkStatus.ts
  63. 47
      src/utils/axios/helper.ts
  64. 313
      src/utils/axios/index.ts
  65. 48
      src/utils/crypto.ts
  66. 3
      src/utils/env.ts
  67. 42
      src/utils/file/base64Conver.ts
  68. 86
      src/utils/file/download.ts
  69. 81
      src/utils/index.ts
  70. 66
      src/utils/is.ts
  71. 36
      src/views/conversation/index.vue
  72. 11
      src/views/error/404.vue
  73. 13
      src/views/login.vue
  74. 57
      src/views/login/index.vue
  75. 16
      src/views/textToPicture/index.vue
  76. 1
      src/vite-env.d.ts
  77. 6
      tsconfig.json
  78. 54
      types/axios.d.ts
  79. 98
      types/global.d.ts
  80. 1
      types/index.d.ts
  81. 6
      types/shims-vue.d.ts
  82. 12
      types/vite-env.d.ts
  83. 19
      types/vue-router.d.ts
  84. 20
      vite.config.ts

8
.env

@ -0,0 +1,8 @@
# 网站标题
VITE_GLOB_APP_TITLE = 青鸟语言大模型-同聪
# 简称,用于配置文件名字 不要出现空格、数字开头等特殊字符
VITE_GLOB_APP_SHORT_NAME = 同聪
# token key
VITE_GLOB_APP_TOKEN_KEY = "hulk-Auth"

10
.env.development

@ -0,0 +1,10 @@
# 本地开发环境
# 公共地址
VITE_GLOB_BASE_URL = "http://localhost:48080"
# 本地MQTT地址
VITE_GLOB_MQTT_URL = "http://localhost:48080"
# 接口授权标识
VITE_GLOB_APP_AUTHORIZATION = "ZmFsY29uOmZhbGNvbl9zZWNyZXQ="

10
.env.production

@ -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="

4
.vscode/settings.json vendored

@ -14,7 +14,7 @@
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
},
"[scss]": {
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
"editor.defaultFormatter": "sibiraj-s.vscode-scss-formatter"
},
"[typescript]": {
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
@ -22,7 +22,7 @@
//
"fileheader.configObj": {
"autoAdd": false, //
"autoAdd": false //
},
//

3
eslint.config.js

@ -5,5 +5,8 @@ export default antfu(
{
// unocss: true,
formatters: true,
rules: {
'vue/html-self-closing': 'off',
},
},
)

2
index.html

@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Vue + TS</title>
<title>%VITE_GLOB_APP_TITLE%</title>
</head>
<body>
<div id="app"></div>

19
package.json

@ -3,28 +3,45 @@
"type": "module",
"version": "0.0.0",
"private": true,
"engines": {
"node": ">=20.0.0",
"pnpm": ">=8.14.0"
},
"scripts": {
"dev": "vite",
"dev": "vite --host --mode development",
"dev:prod": "vite --host --mode production",
"build": "vue-tsc && vite build",
"preview": "vite preview",
"lint": "eslint .",
"lint:fix": "eslint . --fix"
},
"dependencies": {
"@ant-design/icons-vue": "^7.0.1",
"@vueuse/core": "^10.7.2",
"ant-design-vue": "^4.1.0",
"axios": "^1.6.5",
"crypto-js": "^4.2.0",
"lodash-es": "^4.17.21",
"pinia": "^2.1.7",
"pinia-plugin-persistedstate": "^3.2.1",
"qs": "^6.11.2",
"vue": "^3.3.11",
"vue-router": "^4.2.5"
},
"devDependencies": {
"@antfu/eslint-config": "^2.6.2",
"@types/crypto-js": "^4.2.1",
"@types/lodash-es": "^4.17.12",
"@types/node": "^20.11.0",
"@types/qs": "^6.9.11",
"@vitejs/plugin-vue": "^4.5.2",
"eslint": "^8.56.0",
"eslint-plugin-format": "^0.1.0",
"sass": "^1.69.7",
"typescript": "^5.2.2",
"unocss": "^0.58.3",
"vite": "^5.0.8",
"vite-plugin-svg-icons": "^2.0.1",
"vue-tsc": "^1.8.25"
}
}

1310
pnpm-lock.yaml

File diff suppressed because it is too large Load Diff

30
src/App.vue

@ -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">
<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>

23
src/api/base/login.ts

@ -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}`,
})
}

0
src/api/base/user.ts

BIN
src/assets/images/bg_menu.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

BIN
src/assets/images/logo.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

1
src/assets/svg/404.svg

@ -0,0 +1 @@
<svg width="128" height="128" xmlns="http://www.w3.org/2000/svg"><path d="M121.718 73.272v9.953c3.957-7.584 6.199-16.05 6.199-24.995C127.917 26.079 99.273 0 63.958 0 28.644 0 0 26.079 0 58.23c0 .403.028.806.028 1.21l22.97-25.953h13.34l-19.76 27.187h6.42V53.77l13.728-19.477v49.361H22.998V73.272H2.158c5.951 20.284 23.608 36.208 45.998 41.399-1.44 3.3-5.618 11.263-12.565 12.674-8.607 1.764 23.358.428 46.163-13.178 17.519-4.611 31.938-15.849 39.77-30.513h-13.506V73.272H85.02V59.464l22.998-25.977h13.008l-19.429 27.187h6.421v-7.433l13.727-19.402v39.433h-.027zm-78.24 2.822a10.516 10.516 0 0 1-.996-4.535V44.548c0-1.613.332-3.124.996-4.535a11.66 11.66 0 0 1 2.713-3.68c1.134-1.032 2.49-1.864 4.04-2.468 1.55-.605 3.21-.908 4.982-.908h11.292c1.77 0 3.431.303 4.981.908 1.522.604 2.85 1.41 3.986 2.418l-12.26 16.303v-2.898a1.96 1.96 0 0 0-.665-1.512c-.443-.403-.996-.604-1.66-.604-.665 0-1.218.201-1.661.604a1.96 1.96 0 0 0-.664 1.512v9.071L44.364 77.606a10.556 10.556 0 0 1-.886-1.512zm35.73-4.535c0 1.613-.332 3.124-.997 4.535a11.66 11.66 0 0 1-2.712 3.68c-1.134 1.032-2.49 1.864-4.04 2.469-1.55.604-3.21.907-4.982.907H55.185c-1.77 0-3.431-.303-4.981-.907-1.55-.605-2.906-1.437-4.041-2.47a12.49 12.49 0 0 1-1.384-1.512l13.727-18.217v6.375c0 .605.222 1.109.665 1.512.442.403.996.604 1.66.604.664 0 1.218-.201 1.66-.604a1.96 1.96 0 0 0 .665-1.512V53.87L75.97 36.838c.913.932 1.66 1.99 2.214 3.175.664 1.41.996 2.922.996 4.535v27.011h.028z"/></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

11
src/assets/svg/jue_se.svg

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="28px" height="29px" viewBox="0 0 28 29" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>jiaoseguanli-2</title>
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="首页" transform="translate(-40.000000, -387.000000)" fill="#FFFFFF" fill-rule="nonzero">
<g id="jiaoseguanli-2" transform="translate(40.000000, 387.458725)">
<path d="M7.25433334,5.21150002 C7.49098189,4.42671469 7.91547063,3.7114912 8.49099999,3.12783333 C9.33127402,1.22648879 11.2142572,0 13.293,0 L14.707,0 C16.7857428,0 18.668726,1.22648879 19.509,3.12783333 C20.0845294,3.7114912 20.5090181,4.42671469 20.7456667,5.21150002 C23.0405,5.60233333 24.5,3.94916667 24.5,4.66666666 C24.5,5.95583334 19.7983333,8.16666666 14,8.16666666 C8.20166667,8.16666666 3.50000001,5.95583334 3.50000001,4.66666666 C3.50000001,3.94916664 4.95833335,5.60233333 7.25433334,5.21150002 Z M7.11316666,8.2565 C8.95649999,8.87833335 11.3633333,9.33333334 14,9.33333334 C16.6355,9.33333334 19.0435,8.87716666 20.8868333,8.25766668 C20.409776,10.8588341 18.5095518,12.9701943 15.9728333,13.7176667 L15.9996667,13.6663333 L12.0003333,13.6663333 L12.0271667,13.7176667 C9.49044817,12.9701943 7.59022403,10.8588341 7.11316666,8.25766668 L7.11316666,8.2565 Z M0,28 C0,20.8658333 5.33750001,14.9776667 12.236,14.1096667 L13.0666667,15.6671667 L14.9333333,15.6671667 L15.764,14.1096667 C22.6625,14.9776667 28,20.8646667 28,28 L0,28 Z M12.0003333,23.59 L14,25.6666667 L15.9996667,23.59 L14.9333333,16.667 L13.0666667,16.667 L12.0003333,23.59 L12.0003333,23.59 Z" id="形状"></path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

12
src/assets/svg/qu_dao.svg

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="28px" height="28px" viewBox="0 0 28 28" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>形状 2</title>
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="首页" transform="translate(-40.000000, -588.000000)">
<g id="形状-2" transform="translate(40.000000, 588.000000)">
<rect id="矩形" x="0" y="0" width="28" height="28"></rect>
<path d="M15.4230973,25.1703008 C14.6366307,25.9533341 13.3633693,25.9533341 12.5769027,25.1703008 L0.592925063,13.2120622 C0.213298905,12.8335734 0,12.3200392 0,11.7845475 C0,11.2490558 0.213298905,10.7355215 0.592925063,10.3570328 L8.37502052,2.59165159 C9.16148714,1.80861828 10.4347486,1.80861828 11.2212152,2.59165159 L13.9700401,5.34952036 L16.7338449,2.59165159 C17.1131483,2.21284078 17.6277879,2 18.1644322,2 C18.7010765,2 19.2157161,2.21284078 19.5950195,2.59165159 L27.4070749,10.3570328 C27.7867011,10.7355215 28,11.2490558 28,11.7845475 C28,12.3200392 27.7867011,12.8335734 27.4070749,13.2120622 L15.4230973,25.1703008 Z M18.6729262,19.3589907 L19.8972983,18.1328094 L17.2532555,15.4923823 L18.5527425,14.1909751 L21.1967853,16.8690152 L22.0606061,16.003918 L18.3048635,12.2426257 L18.049473,12.490871 C16.4977974,14.0443453 13.9825576,14.0443453 12.430882,12.490871 L11.0487688,11.1142381 C10.6680529,10.7332838 10.4541418,10.2164043 10.4541418,9.67742441 C10.4541418,9.13844448 10.6680529,8.62156503 11.0487688,8.24061076 L12.6787611,6.60820992 L9.7643048,3.6969697 L1.6969697,11.7536577 L13.9557135,24.0606061 L14.9472296,23.0601023 L12.3106983,20.4121526 L13.6402312,19.1257906 L16.2767625,21.7737404 L17.3734393,20.6303075 L14.7068621,18.0199707 L16.006349,16.7185635 L18.6503918,19.3589907 L18.6729262,19.3589907 Z" id="形状" fill="#FFFFFF" fill-rule="nonzero"></path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

15
src/assets/svg/ren_wu.svg

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="28px" height="28px" viewBox="0 0 28 28" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>cheliang__02-02-01qiandaojilu</title>
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="首页" transform="translate(-40.000000, -487.000000)">
<g id="cheliang__02-02-01qiandaojilu" transform="translate(40.000000, 487.000000)">
<rect id="矩形" x="0" y="0" width="28" height="28"></rect>
<g id="编组-14" fill="#FFFFFF" fill-rule="nonzero">
<path d="M26.7501118,3.5 L24.1010716,3.5 L24.1010716,4.78118221 C24.1010716,6.42109544 22.7505806,7.70227766 21.0364957,7.70227766 C19.3224109,7.70227766 17.9719198,6.36984816 17.9719198,4.78118221 L17.9719198,3.5 L10.0767412,3.5 L10.0767412,4.78118221 C10.0767412,6.42109544 8.72625008,7.70227766 7.01216524,7.70227766 C5.2980804,7.70227766 3.94758932,6.36984816 3.94758932,4.78118221 L3.94758932,3.5 L1.29854912,3.5 C0.571361612,3.5 0,4.06372017 0,4.78118221 L0,25.8438178 C0,26.5612798 0.571361612,27.125 1.29854912,27.125 L26.6981699,27.125 C27.4253574,27.125 27.996719,26.5612798 27.996719,25.8438178 L27.996719,4.78118221 C28.048661,4.06372017 27.4772993,3.5 26.7501118,3.5 Z M21.1923216,14.4669197 L13.5049108,21.8977766 C13.193259,22.2052603 12.7777233,22.3590022 12.3102456,22.3590022 C11.842768,22.3590022 11.4272322,22.2052603 11.1155805,21.8977766 L7.27187506,18.2079718 C6.96022327,17.9004881 6.80439738,17.541757 6.80439738,17.0805315 C6.80439738,16.2093275 7.53158489,15.4918655 8.46654025,15.4918655 C8.93401793,15.4918655 9.34955365,15.6456074 9.66120544,15.9530911 L12.3621876,18.5154555 L18.9068752,12.212039 C19.218527,11.9045553 19.6340627,11.7508134 20.1015404,11.7508134 C21.0364957,11.7508134 21.7636832,12.4682755 21.7636832,13.3394794 C21.7117413,13.800705 21.5039734,14.2106833 21.1923216,14.4669197 L21.1923216,14.4669197 Z" id="形状"></path>
<path d="M21.1261719,6.125 C20.3988281,6.125 19.8273438,5.53913043 19.8273438,4.79347826 L19.8273438,1.33152174 C19.8273438,0.585869565 20.3988281,0 21.1261719,0 C21.8535156,0 22.425,0.585869565 22.425,1.33152174 L22.425,4.84673913 C22.425,5.53913043 21.8535156,6.125 21.1261719,6.125 Z M7.09882813,6.125 C6.37148438,6.125 5.8,5.53913043 5.8,4.79347826 L5.8,1.33152174 C5.8,0.585869565 6.37148438,0 7.09882813,0 C7.82617188,0 8.39765625,0.585869565 8.39765625,1.33152174 L8.39765625,4.84673913 C8.39765625,5.53913043 7.82617188,6.125 7.09882813,6.125 Z" id="形状"></path>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

1
src/assets/svg/vue.svg

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

After

Width:  |  Height:  |  Size: 496 B

18
src/assets/svg/wei_xin.svg

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="28px" height="28px" viewBox="0 0 28 28" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>24gf-bubblesDots5备份 2</title>
<defs>
<linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="linearGradient-1">
<stop stop-color="#FFFFFF" offset="0%"></stop>
<stop stop-color="#FFFFFF" offset="100%"></stop>
</linearGradient>
</defs>
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="首页" transform="translate(-40.000000, -185.000000)">
<g id="24gf-bubblesDots5备份-2" transform="translate(40.000000, 185.000000)">
<rect id="矩形" x="0" y="0" width="28" height="28"></rect>
<path d="M10.5589358,10.0965796 C12.0891775,8.54705284 14.167107,7.67722754 16.3333907,7.67938173 L18.2681739,7.67938173 C18.2217517,7.54638242 18.171137,7.41448838 18.1163297,7.28369959 C17.022205,4.66371205 14.480911,2.96146095 11.6668699,2.96361495 L7.00034916,2.96361495 C4.09801153,2.96361495 1.49665981,4.77337262 0.461059566,7.51327425 C-0.574540675,10.2531759 0.170453174,13.354698 2.33382843,15.3098943 L2.33382843,18.8793229 C2.33379032,19.1177711 2.47591093,19.3327572 2.69390575,19.4240126 C2.91190056,19.515268 3.16282808,19.4648168 3.32965667,19.2961892 L5.62645985,16.9751487 C6.07892687,17.0657024 6.5390944,17.1111744 7.00034916,17.1109112 L8.24955489,17.1109112 C8.19443331,16.7204032 8.16683865,16.3264308 8.16697935,15.93197 C8.16484767,13.7428264 9.02558989,11.6429693 10.5589358,10.0965796 L10.5589358,10.0965796 Z M5.83371899,11.2162053 C5.36186112,11.2162053 4.93646541,10.9289655 4.75589321,10.4884254 C4.57532101,10.0478852 4.67513294,9.5408016 5.00878683,9.20362678 C5.34244073,8.86645195 5.84422917,8.76558674 6.280169,8.94806444 C6.71610883,9.13054213 7.00034916,9.56042688 7.00034916,10.0372641 C7.00034916,10.6883753 6.47803104,11.2162053 5.83371899,11.2162053 L5.83371899,11.2162053 Z M27.4493712,13.1784055 C26.3552465,10.5584179 23.8139524,8.85616684 20.9999114,8.85832084 L16.3333907,8.85832084 C12.4675183,8.85832084 9.33360954,12.0253026 9.33360954,15.93197 C9.33360954,19.8386374 12.4675183,23.0056171 16.3333907,23.0056171 L20.9999114,23.0056171 C21.4610014,23.0056622 21.9209842,22.9600053 22.3732539,22.869302 L24.670057,25.1903424 C24.8367636,25.3592945 25.0878127,25.4100326 25.3059976,25.3188688 C25.5241826,25.2277049 25.6664706,25.0126195 25.6664321,24.7740288 L25.6664321,21.2046002 C27.8953387,19.1881751 28.6115337,15.964105 27.4493712,13.1784055 L27.4493712,13.1784055 Z M14.0001303,17.1109112 C13.5282724,17.1109112 13.1028767,16.8236714 12.9223045,16.3831312 C12.7417323,15.9425911 12.8415442,15.4355075 13.1751981,15.0983327 C13.508852,14.7611578 14.0106405,14.6602926 14.4465803,14.8427703 C14.8825201,15.025248 15.1667605,15.4551328 15.1667605,15.93197 C15.1667605,16.5830812 14.6444423,17.1109112 14.0001303,17.1109112 Z M18.666651,17.1109112 C18.022339,17.1109112 17.5000208,16.5830812 17.5000208,15.93197 C17.5000208,15.2808587 18.022339,14.7530288 18.666651,14.7530288 C19.3109631,14.7530288 19.8332812,15.2808587 19.8332812,15.93197 C19.8332812,16.5830812 19.3109631,17.1109112 18.666651,17.1109112 L18.666651,17.1109112 Z M23.3331718,17.1109112 C22.8613139,17.1109112 22.4359182,16.8236714 22.255346,16.3831313 C22.0747738,15.9425911 22.1745857,15.4355075 22.5082396,15.0983327 C22.8418935,14.7611579 23.3436819,14.6602926 23.7796218,14.8427703 C24.2155616,15.025248 24.4998019,15.4551328 24.4998019,15.93197 C24.4998019,16.5830812 23.9774838,17.1109112 23.3331718,17.1109112 L23.3331718,17.1109112 Z" id="形状" fill="url(#linearGradient-1)" fill-rule="nonzero"></path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

13
src/assets/svg/wen_sheng_tu.svg

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="28px" height="28px" viewBox="0 0 28 28" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>wenshengtu</title>
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="首页" transform="translate(-40.000000, -286.000000)" fill="#FFFFFF" fill-rule="nonzero">
<g id="wenshengtu" transform="translate(40.000000, 286.000000)">
<path d="M8.13067151,3.8974031 C5.69864602,3.8974031 3.72709982,5.80464363 3.72709982,8.15734406 L3.72709982,9.4693052 C3.72709982,10.0123029 3.27207162,10.4524895 2.71076588,10.4524895 C2.14946015,10.4524895 1.69443194,10.0123029 1.69443194,9.4693052 L1.69443194,8.15734406 C1.69443194,4.71864823 4.57603455,1.93103448 8.13067151,1.93103448 L12.1960073,1.93103448 C12.757313,1.93103448 13.2123412,2.37122109 13.2123412,2.91421879 C13.2123412,3.45721649 12.757313,3.8974031 12.1960073,3.8974031 L8.13067151,3.8974031 Z M11.1130018,27.0344828 L9.39905626,27.0344828 C9.45706119,26.8911207 9.48684341,26.73853 9.48686751,26.5845776 C9.48702822,21.5041816 5.25152897,17.3745243 0,17.3347797 L0,16.2855254 C0,13.9684456 1.94169635,12.0900813 4.33690018,12.0900813 L11.1130018,12.0900813 C13.5075706,12.0909499 15.4482759,13.9690598 15.4482759,16.2855254 L15.4482759,22.8406118 C15.4473784,25.1564631 13.5069357,27.0336146 11.1130018,27.0344828 L11.1130018,27.0344828 Z M12.1960073,17.3347797 C12.9445646,17.3347797 13.5513902,16.7477468 13.5513902,16.0236051 C13.5513902,15.2994633 12.9445646,14.7124305 12.1960073,14.7124305 C11.4474499,14.7124305 10.8406243,15.2994633 10.8406243,16.0236051 C10.8406243,16.7477468 11.4474499,17.3347797 12.1960073,17.3347797 L12.1960073,17.3347797 Z" id="形状"></path>
<path d="M6.67560635,27.5191329 C6.67560635,27.6889496 6.7053662,27.8520409 6.75862069,28 L4.17734447,28 C1.87087237,28 0.000864818936,25.9933935 4.54747351e-13,23.5175116 L4.54747351e-13,20.2758621 C3.69845997,20.3182566 6.6758172,23.5487953 6.67560635,27.5191329 L6.67560635,27.5191329 Z" id="路径"></path>
<path d="M22.2077522,0 C22.7545009,0 23.1982389,0.435749222 23.1966541,0.972654513 L23.1966541,2.86505113 L27.0096313,2.86505113 C27.2857987,2.86508421 27.5494119,2.97833873 27.7368682,3.17748943 C27.9243244,3.37664012 28.0185999,3.64360106 27.9969483,3.91396176 C27.770325,6.71520676 26.3582873,9.32036461 24.2790578,11.2158737 C25.0968036,11.8228101 26.0698576,12.2212094 27.0857007,12.2990218 C27.6243158,12.3489327 28.0225049,12.8140076 27.9800847,13.3436398 C27.9376645,13.8732721 27.4703305,14.2714977 26.9303924,14.2381058 C25.3519529,14.1167185 23.8670154,13.4537572 22.681601,12.4484215 C21.1298404,13.4649444 19.3419567,14.0805085 17.4835273,14.2381058 C17.1307929,14.2659055 16.78975,14.1067998 16.5888662,13.8207223 C16.3879824,13.5346448 16.3577767,13.1650575 16.5096273,12.8511803 C16.6614779,12.5373031 16.9723151,12.3268215 17.3250495,12.2990218 C18.7485264,12.1776721 20.1225093,11.7270344 21.3345393,10.9839929 C20.5848957,9.94177649 20.1309317,8.72256451 20.019173,7.45131169 C19.975376,7.09681086 20.1329194,6.74724808 20.4295705,6.54070789 C20.7262216,6.33416769 21.1142725,6.30386617 21.440435,6.46177304 C21.7665975,6.61967991 21.9784177,6.94040038 21.9922224,7.29724322 C22.0651222,8.19519787 22.3995105,9.05580258 22.9145635,9.80591374 C24.3836532,8.47220987 25.4597179,6.71676301 25.8685907,4.81036016 L17.4058732,4.81036016 C16.8588425,4.81036016 16.4153866,4.3748879 16.4153866,3.83770565 C16.4153866,3.30052339 16.8588425,2.86505113 17.4058732,2.86505113 L21.2156809,2.86505113 L21.2156809,0.972654513 C21.2156809,0.435749222 21.6594189,0 22.2077522,0 L22.2077522,0 Z M26.6879213,16.9771009 C27.2346699,16.9771009 27.6784079,17.4128502 27.6784079,17.9497554 L27.6784079,21.8403735 C27.677533,25.2418854 24.8697275,27.9991409 21.4058543,28 L20.084149,28 C19.5371183,28 19.0936624,27.5645277 19.0936624,27.0273455 C19.0936624,26.4901632 19.5371183,26.054691 20.084149,26.054691 L21.4058543,26.054691 C23.7760287,26.054691 25.6974346,24.1678768 25.6974346,21.8403735 L25.6974346,17.9497554 C25.6974346,17.4128502 26.1411726,16.9771009 26.6879213,16.9771009 L26.6879213,16.9771009 Z" id="形状"></path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.2 KiB

14
src/assets/svg/wo_de.svg

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="30px" height="30px" viewBox="0 0 30 30" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>编组</title>
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="首页" transform="translate(-40.000000, -874.000000)">
<g id="编组" transform="translate(41.000000, 875.000000)">
<rect id="矩形" x="0" y="0" width="28" height="28"></rect>
<g id="xiaochengxu" fill="#FFFFFF" fill-rule="nonzero" stroke="#FFFFFF" stroke-width="0.5">
<path d="M17.3755556,7 C19.6311111,7 21.4666667,8.69555556 21.4666667,10.7955556 C21.4666667,11.4488889 21.28,12.0866667 20.9377778,12.6622222 C20.4244444,13.5022222 19.6,14.1244444 18.6044444,14.42 C18.34,14.4977778 18.1377778,14.5288889 17.9511111,14.5288889 C17.5155556,14.5288889 17.1733333,14.1866667 17.1733333,13.7511111 C17.1733333,13.3155556 17.5155556,12.9733333 17.9511111,12.9733333 C17.9822222,12.9733333 18.0444444,12.9733333 18.1222222,12.9422222 C18.7911111,12.7555556 19.32,12.3666667 19.6155556,11.8533333 C19.8177778,11.5266667 19.9111111,11.1688889 19.9111111,10.7955556 C19.9111111,9.56666667 18.7755556,8.55555556 17.3911111,8.55555556 C16.9088889,8.55555556 16.4422222,8.68 16.0222222,8.91333333 C15.2911111,9.33333333 14.8555556,10.0333333 14.8555556,10.7955556 L14.8555556,17.3133333 C14.8555556,18.6355556 14.1244444,19.8488889 12.9111111,20.5333333 C12.2577778,20.9066667 11.5266667,21.0933333 10.78,21.0933333 C8.52444444,21.0933333 6.68888889,19.3977778 6.68888889,17.2977778 C6.68888889,16.6444444 6.87555556,16.0066667 7.21777778,15.4311111 C7.73111111,14.5911111 8.55555556,13.9688889 9.55111111,13.6733333 C9.83111111,13.5955556 10.0177778,13.5644444 10.2044444,13.5644444 C10.64,13.5644444 10.9822222,13.9066667 10.9822222,14.3422222 C10.9822222,14.7777778 10.64,15.12 10.2044444,15.12 C10.1733333,15.12 10.1111111,15.12 10.0333333,15.1511111 C9.36444444,15.3533333 8.83555556,15.7422222 8.54,16.24 C8.33777778,16.5666667 8.24444444,16.9244444 8.24444444,17.2977778 C8.24444444,18.5266667 9.38,19.5377778 10.78,19.5377778 C11.2622222,19.5377778 11.7288889,19.4133333 12.1488889,19.18 C12.88,18.76 13.3155556,18.06 13.3155556,17.2977778 L13.3155556,10.7955556 C13.3155556,9.47333333 14.0466667,8.26 15.26,7.57555556 C15.8977778,7.18666667 16.6288889,7 17.3755556,7 L17.3755556,7 Z M1.55555556,14 C1.55555556,20.8755556 7.12444444,26.4444444 14,26.4444444 C20.8755556,26.4444444 26.4444444,20.8755556 26.4444444,14 C26.4444444,7.12444444 20.8755556,1.55555556 14,1.55555556 C7.12444444,1.55555556 1.55555556,7.12444444 1.55555556,14 Z M0,14 C0,6.26888889 6.26888889,0 14,0 C21.7311111,0 28,6.26888889 28,14 C28,21.7311111 21.7311111,28 14,28 C6.26888889,28 0,21.7311111 0,14 Z" id="形状"></path>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

22
src/assets/svg/xiao_cheng_xu.svg

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="30px" height="30px" viewBox="0 0 30 30" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>编组 7备份 2</title>
<defs>
<rect id="path-1" x="0" y="0" width="28" height="28" rx="14"></rect>
</defs>
<g id="页面-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="首页" transform="translate(-40.000000, -992.000000)">
<g id="编组-7备份-2" transform="translate(41.000000, 993.000000)">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<use id="矩形备份-3" stroke="#FFFFFF" stroke-width="2" xlink:href="#path-1"></use>
<g id="denglu-yonghu" mask="url(#mask-2)" fill-rule="nonzero" stroke="#FFFFFF" stroke-width="2">
<g transform="translate(4.900000, 7.000000)" id="形状">
<path d="M9.45011742,11.6 C12.1787445,11.6 14.662427,12.612994 16.525434,14.2690208 C18.6732118,16.1781804 19.9960483,18.9412362 19.9320836,22 L19.9320836,22 L-1.03202515,22 C-1.09618067,18.9412859 0.226675171,16.1782605 2.37451028,14.2690974 C4.23761547,12.6130245 6.72145499,11.6 9.45011742,11.6 Z M9.44952516,-1 C11.3690663,-1 13.1066104,-0.222146684 14.3643062,1.03544879 C15.6221102,2.29315237 16.4,4.03070048 16.4,5.95 C16.4,7.86903244 15.6218417,9.60640384 14.3640608,10.8640844 C13.106034,12.1220109 11.368224,12.9 9.44952516,12.9 C7.53087107,12.9 5.79337036,12.1220017 4.53559523,10.8641262 C3.27798175,9.60641233 2.5,7.86898265 2.5,5.95 C2.5,4.0308838 3.27784754,2.29336574 4.53547253,1.03564038 C5.79308216,-0.222069621 7.53044992,-1 9.44952516,-1 Z"></path>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

3
src/components/AppContainerBox/index.ts

@ -0,0 +1,3 @@
import AppContainerBox from './index.vue'
export { AppContainerBox }

22
src/components/AppContainerBox/index.vue

@ -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>

3
src/components/AppContentBox/index.ts

@ -0,0 +1,3 @@
import AppContentBox from './index.vue'
export { AppContentBox }

17
src/components/AppContentBox/index.vue

@ -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>

3
src/components/AppSubMenuBox/index.ts

@ -0,0 +1,3 @@
import AppSubMenuBox from './index.vue'
export { AppSubMenuBox }

14
src/components/AppSubMenuBox/index.vue

@ -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>

5
src/components/AppSubMenuList/index.d.ts vendored

@ -0,0 +1,5 @@
export interface SubMenuItem {
title: string
content: string
id: string
}

3
src/components/AppSubMenuList/index.ts

@ -0,0 +1,3 @@
import AppSubMenuList from './index.vue'
export { AppSubMenuList }

51
src/components/AppSubMenuList/index.vue

@ -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>

3
src/components/AppSubMenuTitle/index.ts

@ -0,0 +1,3 @@
import AppSubMenuTitle from './index.vue'
export { AppSubMenuTitle }

36
src/components/AppSubMenuTitle/index.vue

@ -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>

41
src/components/HelloWorld.vue

@ -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>

3
src/components/SvgIcon/index.ts

@ -0,0 +1,3 @@
import SvgIcon from './index.vue'
export { SvgIcon }

51
src/components/SvgIcon/index.vue

@ -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>

72
src/design/index.scss

@ -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;
}

3
src/design/mixins/config.scss

@ -0,0 +1,3 @@
$namespace: "app";
$menu-width: 80px;
$sub-menu-width: 180px;

6
src/design/mixins/mixins.scss

@ -0,0 +1,6 @@
@mixin app($block) {
$B: $namespace + "-" + $block;
.#{$B} {
@content;
}
}

49
src/design/public.scss

@ -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;
}
}

12
src/enums/cacheEnum.ts

@ -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,
}

14
src/enums/commonEnum.ts

@ -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',
}

55
src/enums/httpEnum.ts

@ -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 = '操作成功',
}

22
src/enums/menuEnum.ts

@ -0,0 +1,22 @@
export enum MenuTypeEnum {
// 会话
CONVERSATION = 'conversation',
// 文生图
TEXT_TO_PICTURE = 'textToPicture',
// 角色
ROLE = 'role',
// 任务
TASK = 'task',
// 渠道
CHANNEL = 'channel',
// 小程序
APPLET = 'applet',
// 我的
USER = 'user',
}

11
src/enums/pageEnum.ts

@ -0,0 +1,11 @@
/**
* @description: 使router中name属性
*/
export enum PageEnum {
// 登录
BASE_LOGIN = 'Login',
// 错误
ERROR_PAGE_NAME_404 = '404',
}

111
src/hooks/useMessage.tsx

@ -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,
}
}

14
src/layout/AppMain/index.vue

@ -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>

8
src/layout/AppMenu/index.d.ts vendored

@ -0,0 +1,8 @@
import type { MenuTypeEnum } from '@/enums/menuEnum'
export interface MenuItem {
name: string
icon: string
path: string
key: MenuTypeEnum
}

131
src/layout/AppMenu/index.vue

@ -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>

23
src/layout/index.vue

@ -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>

23
src/main.ts

@ -2,17 +2,30 @@
* @Description:
* @Author: yeke
* @Date: 2024-01-14 15:47:49
* @LastEditors: yeke
* @LastEditTime: 2024-01-14 21:34:34
* @LastEditors: lipenghui
* @LastEditTime: 2024-01-16 15:24:25
*/
import { createApp } from 'vue'
import './style.css'
import './design/public.scss'
import 'virtual:uno.css'
import 'ant-design-vue/dist/reset.css'
import router from './router/index'
import App from './App.vue'
import { setupStore } from '@/store'
import { router, setupRouter } from '@/router'
import { setupRouterGuard } from '@/router/guard'
// svg图标
import 'virtual:svg-icons-register'
const app = createApp(App)
// 挂载状态管理
setupStore(app)
// 配置路由
setupRouter(app)
// 路由守卫
setupRouterGuard(router)
app.use(router)
app.mount('#app')

35
src/router/guard.ts

@ -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
// })
}

47
src/router/index.ts

@ -1,3 +1,4 @@
import type { App } from 'vue'
import type {
RouteRecordRaw,
} from 'vue-router'
@ -5,21 +6,57 @@ import {
createRouter,
createWebHistory,
} from 'vue-router'
import Layout from '@/layout/index.vue'
export const constantRoutes: Array<RouteRecordRaw> = [
{
path: '/404',
name: '404',
component: () => import('@/views/error/404.vue'),
meta: {
title: '404',
},
},
{
path: '/login',
name: 'Login',
component: () => import('@/views/login.vue'),
component: () => import('@/views/login/index.vue'),
meta: {
title: '登录',
},
},
{
name: 'Layout',
path: '/',
component: Layout,
redirect: '/conversation',
children: [
{
name: 'Conversation',
path: '/conversation',
component: () => import('@/views/conversation/index.vue'),
meta: {
title: '会话',
},
},
{
name: 'TextToPicture',
path: '/textToPicture',
component: () => import('@/views/textToPicture/index.vue'),
meta: {
title: '文生图',
},
},
],
},
]
// 1.返回一个 router 实列,为函数,里面有配置项(对象) history
const router = createRouter({
export const router = createRouter({
history: createWebHistory(),
routes: constantRoutes,
})
// 3导出路由 然后去 main.ts 注册 router.ts
export default router
export function setupRouter(app: App) {
app.use(router)
}

11
src/store/index.ts

@ -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 }

15
src/store/moules/userStore/index.d.ts vendored

@ -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
}

73
src/store/moules/userStore/index.ts

@ -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,
},
],
})

79
src/style.css

@ -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;
}
}

337
src/utils/axios/Axios.ts

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

59
src/utils/axios/axiosCancel.ts

@ -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()
}
}

33
src/utils/axios/axiosRetry.ts

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

57
src/utils/axios/axiosTransform.ts

@ -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
}

69
src/utils/axios/checkStatus.ts

@ -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}` })
}
}

47
src/utils/axios/helper.ts

@ -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])
}
}

313
src/utils/axios/index.ts

@ -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',
// },
// });

48
src/utils/crypto.ts

@ -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 javaAesUtil.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 javaAesUtil.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)
}
}

3
src/utils/env.ts

@ -0,0 +1,3 @@
export function getEnv() {
return import.meta.env
}

42
src/utils/file/base64Conver.ts

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

86
src/utils/file/download.ts

@ -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
}

81
src/utils/index.ts

@ -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(','))
}

66
src/utils/is.ts

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

36
src/views/conversation/index.vue

@ -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>

11
src/views/error/404.vue

@ -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>

13
src/views/login.vue

@ -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>

57
src/views/login/index.vue

@ -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>

16
src/views/textToPicture/index.vue

@ -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>

1
src/vite-env.d.ts vendored

@ -1 +0,0 @@
/// <reference types="vite/client" />

6
tsconfig.json

@ -10,9 +10,11 @@
/* Bundler mode */
"moduleResolution": "bundler",
"paths": {
"@/*": ["src/*"]
"@/*": ["src/*"],
"/#/*": ["types/*"]
},
"resolveJsonModule": true,
"types": ["vite/client"],
"allowImportingTsExtensions": true,
"strict": true,
@ -26,6 +28,6 @@
"isolatedModules": true,
"skipLibCheck": true
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"],
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue", "types/**/*"],
"exclude": ["dist", "node_modules"]
}

54
types/axios.d.ts vendored

@ -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
}

98
types/global.d.ts vendored

@ -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>
}

1
types/index.d.ts vendored

@ -0,0 +1 @@
declare type TargetContext = '_self' | '_blank'

6
types/shims-vue.d.ts vendored

@ -0,0 +1,6 @@
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<object, object, any>
export default component
}

12
types/vite-env.d.ts vendored

@ -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
}

19
types/vue-router.d.ts vendored

@ -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
}
}

20
vite.config.ts

@ -2,10 +2,19 @@ import { join, resolve } from 'node:path'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import UnoCSS from 'unocss/vite'
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue(), UnoCSS()],
plugins: [
vue(),
UnoCSS(),
createSvgIconsPlugin({
// eslint-disable-next-line node/prefer-global/process
iconDirs: [resolve(process.cwd(), 'src/assets/svg')],
symbolId: 'icon-[name]',
}),
],
server: {
host: '0.0.0.0',
},
@ -15,4 +24,13 @@ export default defineConfig({
'@': join(__dirname, 'src'),
},
},
// 全局 css 注册
css: {
preprocessorOptions: {
scss: {
javascriptEnabled: true,
additionalData: `@import "src/design/index.scss";`,
},
},
},
})

Loading…
Cancel
Save