Compare commits

..

No commits in common. "e1a9a2da9233b0e3383dc9ec93028a63e30b1f86" and "ba36f0966bb240bce7ac8b231d2354dc37cec991" have entirely different histories.

18 changed files with 1593 additions and 160 deletions

4
.husky/pre-commit Normal file
View File

@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname -- "$0")/_/husky.sh"
pnpm exec lint-staged

View File

@ -1,6 +1,6 @@
{ {
"name": "bunny-auth-admin", "name": "bunny-auth-admin",
"version": "4.0.0", "version": "3.0.0",
"private": true, "private": true,
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
@ -43,7 +43,9 @@
"lint:prettier": "prettier --write \"src/**/*.{js,ts,json,tsx,css,scss,vue,html,md}\"", "lint:prettier": "prettier --write \"src/**/*.{js,ts,json,tsx,css,scss,vue,html,md}\"",
"lint:stylelint": "stylelint --cache --fix \"**/*.{html,vue,css,scss}\" --cache-location node_modules/.cache/stylelint/", "lint:stylelint": "stylelint --cache --fix \"**/*.{html,vue,css,scss}\" --cache-location node_modules/.cache/stylelint/",
"lint": "pnpm lint:eslint && pnpm lint:prettier && pnpm lint:stylelint", "lint": "pnpm lint:eslint && pnpm lint:prettier && pnpm lint:stylelint",
"preinstall": "npx only-allow pnpm" "prepare": "husky install",
"preinstall": "npx only-allow pnpm",
"commit": "git pull && git add -A && git-cz && git push"
}, },
"dependencies": { "dependencies": {
"@amap/amap-jsapi-loader": "^1.0.1", "@amap/amap-jsapi-loader": "^1.0.1",
@ -109,6 +111,9 @@
"xlsx": "^0.18.5" "xlsx": "^0.18.5"
}, },
"devDependencies": { "devDependencies": {
"@commitlint/cli": "^19.4.0",
"@commitlint/config-conventional": "^19.2.2",
"@commitlint/types": "^19.0.3",
"@eslint/js": "^9.9.1", "@eslint/js": "^9.9.1",
"@faker-js/faker": "^8.4.1", "@faker-js/faker": "^8.4.1",
"@iconify-icons/ep": "^1.2.12", "@iconify-icons/ep": "^1.2.12",
@ -131,7 +136,10 @@
"@vitejs/plugin-vue-jsx": "^3.1.0", "@vitejs/plugin-vue-jsx": "^3.1.0",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"boxen": "^7.1.1", "boxen": "^7.1.1",
"commitizen": "^4.3.0",
"commitlint": "^17.8.1",
"cssnano": "^7.0.5", "cssnano": "^7.0.5",
"cz-git": "^1.9.4",
"dagre": "^0.8.5", "dagre": "^0.8.5",
"eslint": "^9.9.1", "eslint": "^9.9.1",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
@ -139,6 +147,7 @@
"eslint-plugin-prettier": "^5.2.1", "eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-vue": "^9.27.0", "eslint-plugin-vue": "^9.27.0",
"gradient-string": "^2.0.2", "gradient-string": "^2.0.2",
"husky": "^8.0.3",
"lint-staged": "^15.2.9", "lint-staged": "^15.2.9",
"postcss": "^8.4.41", "postcss": "^8.4.41",
"postcss-html": "^1.7.0", "postcss-html": "^1.7.0",
@ -183,5 +192,10 @@
} }
} }
}, },
"config": {
"commitizen": {
"path": "node_modules/cz-git"
}
},
"packageManager": "pnpm@10.8.1+sha512.c50088ba998c67b8ca8c99df8a5e02fd2ae2e2b29aaf238feaa9e124248d3f48f9fb6db2424949ff901cffbb5e0f0cc1ad6aedb602cd29450751d11c35023677" "packageManager": "pnpm@10.8.1+sha512.c50088ba998c67b8ca8c99df8a5e02fd2ae2e2b29aaf238feaa9e124248d3f48f9fb6db2424949ff901cffbb5e0f0cc1ad6aedb602cd29450751d11c35023677"
} }

File diff suppressed because it is too large Load Diff

View File

@ -21,7 +21,9 @@ const i18nStore = userI18nStore();
const i18n = useI18n(); const i18n = useI18n();
const { $storage } = useNav(); const { $storage } = useNav();
/* 设置多语言内容 */ /**
* * 设置多语言内容
*/
const setI18n = async () => { const setI18n = async () => {
await i18nStore.loadI18nMap(); await i18nStore.loadI18nMap();
const languageData = JSON.parse(localStorage.getItem('i18nStore') as any); const languageData = JSON.parse(localStorage.getItem('i18nStore') as any);
@ -42,7 +44,9 @@ const setI18n = async () => {
i18n.mergeLocaleMessage(locale, languageData.i18n[locale]); i18n.mergeLocaleMessage(locale, languageData.i18n[locale]);
}; };
/* 当前语言类别 */ /**
* * 当前语言类别
*/
const currentLocale = computed(() => { const currentLocale = computed(() => {
const languageData = JSON.parse(localStorage.getItem('i18nStore') as any); const languageData = JSON.parse(localStorage.getItem('i18nStore') as any);
const local = languageData ? languageData.i18n.local : {}; const local = languageData ? languageData.i18n.local : {};

View File

@ -1,4 +1,4 @@
import { watch, unref, computed, reactive, onMounted, defineComponent } from 'vue'; import { computed, defineComponent, onMounted, reactive, unref, watch } from 'vue';
import { countToProps } from './props'; import { countToProps } from './props';
import { isNumber } from '@pureadmin/utils'; import { isNumber } from '@pureadmin/utils';

View File

@ -1,5 +1,5 @@
import './rebound.css'; import './rebound.css';
import { ref, unref, onBeforeMount, defineComponent, onBeforeUnmount } from 'vue'; import { defineComponent, onBeforeMount, onBeforeUnmount, ref, unref } from 'vue';
import { reboundProps } from './props'; import { reboundProps } from './props';
export default defineComponent({ export default defineComponent({

View File

@ -1,12 +1,12 @@
.scroll-num { .scroll-num {
width: var(--width, 20px); animation: enhance-bounce-in-down 1s calc(var(--delay) * 1s) forwards;
height: var(--height, calc(var(--width, 20px) * 1.8));
color: var(--color, #333); color: var(--color, #333);
font-size: var(--height, calc(var(--width, 20px) * 1.1)); font-size: var(--height, calc(var(--width, 20px) * 1.1));
height: var(--height, calc(var(--width, 20px) * 1.8));
line-height: var(--height, calc(var(--width, 20px) * 1.8)); line-height: var(--height, calc(var(--width, 20px) * 1.8));
text-align: center;
overflow: hidden; overflow: hidden;
animation: enhance-bounce-in-down 1s calc(var(--delay) * 1s) forwards; text-align: center;
width: var(--width, 20px);
} }
ul { ul {

View File

@ -1,11 +1,8 @@
<script setup lang="tsx"> <script lang="tsx" setup>
import { ref } from 'vue'; import { ref } from 'vue';
import ReCropper from '@/components/ReCropper'; import ReCropper from '@/components/ReCropper';
import { formatBytes } from '@pureadmin/utils'; import { formatBytes } from '@pureadmin/utils';
import { $t } from '@/plugins/i18n';
defineOptions({
name: 'ReCropperPreview',
});
defineProps({ defineProps({
imgSrc: String, imgSrc: String,
@ -34,20 +31,30 @@ defineExpose({ hidePopover });
<template> <template>
<div v-loading="!showPopover" element-loading-background="transparent"> <div v-loading="!showPopover" element-loading-background="transparent">
<el-popover ref="popoverRef" :visible="showPopover" placement="right" width="18vw"> <el-popover ref="popoverRef" :visible="showPopover" placement="right" popper-style="top:260px" width="18vw">
<template #reference> <template #reference>
<div class="w-[18vw]"> <div class="w-[18vw]">
<ReCropper ref="refCropper" :src="imgSrc" circled @cropper="onCropper" @readied="showPopover = true" /> <ReCropper ref="refCropper" :src="imgSrc" circled @cropper="onCropper" @readied="showPopover = true" />
<p v-show="showPopover" class="mt-1 text-center">温馨提示右键上方裁剪区可开启功能菜单</p> <p v-show="showPopover" class="mt-1 text-center">{{ $t('cropper_preview_tips') }}</p>
</div> </div>
</template> </template>
<div class="flex flex-wrap justify-center items-center text-center"> <div class="flex flex-wrap justify-center items-center text-center">
<el-image v-if="cropperImg" :src="cropperImg" :preview-src-list="Array.of(cropperImg)" fit="cover" /> <el-image v-if="cropperImg" :preview-src-list="Array.of(cropperImg)" :src="cropperImg" class="cropper-img-preview" fit="contain" />
<div v-if="infos" class="mt-1"> <div v-if="infos" class="mt-1">
<p>图像大小{{ parseInt(infos.width) }} × {{ parseInt(infos.height) }}像素</p> <p>{{ $t('image_size') }}{{ parseInt(infos.width) }} × {{ parseInt(infos.height) }}{{ $t('pixel') }}</p>
<p>文件大小{{ formatBytes(infos.size) }}{{ infos.size }} 字节</p> <p>{{ $t('file_size') }}{{ formatBytes(infos.size) }}{{ infos.size }} {{ $t('bytes') }}</p>
</div> </div>
</div> </div>
</el-popover> </el-popover>
</div> </div>
</template> </template>
<style lang="scss" scoped>
.cropper-img-preview {
height: 200px;
:deep(.el-image__inner) {
max-height: 310px;
}
}
</style>

View File

@ -2,26 +2,26 @@ import './circled.css';
import Cropper from 'cropperjs'; import Cropper from 'cropperjs';
import { ElUpload } from 'element-plus'; import { ElUpload } from 'element-plus';
import type { CSSProperties } from 'vue'; import type { CSSProperties } from 'vue';
import { computed, defineComponent, onMounted, onUnmounted, type PropType, ref, unref } from 'vue';
import { useEventListener } from '@vueuse/core'; import { useEventListener } from '@vueuse/core';
import { longpress } from '@/directives/longpress'; import { longpress } from '@/directives/longpress';
import { useTippy, directive as tippy } from 'vue-tippy'; import { directive as tippy, useTippy } from 'vue-tippy';
import { type PropType, ref, unref, computed, onMounted, onUnmounted, defineComponent } from 'vue'; import { debounce, delay, downloadByBase64, isArray, useResizeObserver } from '@pureadmin/utils';
import { delay, debounce, isArray, downloadByBase64, useResizeObserver } from '@pureadmin/utils';
import { import {
Reload,
Upload,
ArrowH,
ArrowV,
ArrowUp,
ArrowDown, ArrowDown,
ArrowH,
ArrowLeft, ArrowLeft,
ChangeIcon,
ArrowRight, ArrowRight,
ArrowUp,
ArrowV,
ChangeIcon,
DownloadIcon,
Reload,
RotateLeft, RotateLeft,
SearchPlus,
RotateRight, RotateRight,
SearchMinus, SearchMinus,
DownloadIcon, SearchPlus,
Upload,
} from './svg'; } from './svg';
type Options = Cropper.Options; type Options = Cropper.Options;
@ -101,7 +101,7 @@ export default defineComponent({
}); });
const iconClass = computed(() => { const iconClass = computed(() => {
return ['p-[6px]', 'h-[30px]', 'w-[30px]', 'outline-hidden', 'rounded-[4px]', 'cursor-pointer', 'hover:bg-[rgba(0,0,0,0.06)]']; return ['p-[6px]', 'h-[30px]', 'w-[30px]', 'outline-none', 'rounded-[4px]', 'cursor-pointer', 'hover:bg-[rgba(0,0,0,0.06)]'];
}); });
const getWrapperStyle = computed((): CSSProperties => { const getWrapperStyle = computed((): CSSProperties => {
@ -124,7 +124,7 @@ export default defineComponent({
async function init() { async function init() {
const imgEl = unref(imgElRef); const imgEl = unref(imgElRef);
if (!imgEl) return; if (!imgEl) return;
cropper.value = new Cropper(imgEl, { const result: any = new Cropper(imgEl, {
...defaultOptions, ...defaultOptions,
ready: () => { ready: () => {
isReady.value = true; isReady.value = true;
@ -142,6 +142,11 @@ export default defineComponent({
}, },
...props.options, ...props.options,
}); });
// 如果图片不存在直接将加载变为加载完成
if (!result.ready) emit('readied', cropper.value);
cropper.value = result;
} }
function realTimeCroppered() { function realTimeCroppered() {
@ -221,55 +226,30 @@ export default defineComponent({
return () => ( return () => (
<div class="flex flex-wrap w-[60px] justify-between"> <div class="flex flex-wrap w-[60px] justify-between">
<ElUpload accept="image/*" show-file-list={false} before-upload={beforeUpload}> <ElUpload accept="image/*" show-file-list={false} before-upload={beforeUpload}>
<Upload <Upload class={iconClass.value} v-tippy={{ content: '上传', placement: 'left-start' }} />
class={iconClass.value}
v-tippy={{
content: '上传',
placement: 'left-start',
}}
/>
</ElUpload> </ElUpload>
<DownloadIcon <DownloadIcon
class={iconClass.value} class={iconClass.value}
v-tippy={{ v-tippy={{ content: '下载', placement: 'right-start' }}
content: '下载',
placement: 'right-start',
}}
onClick={() => downloadByBase64(imgBase64.value, 'cropping.png')} onClick={() => downloadByBase64(imgBase64.value, 'cropping.png')}
/> />
<ChangeIcon <ChangeIcon
class={iconClass.value} class={iconClass.value}
v-tippy={{ v-tippy={{ content: '圆形、矩形裁剪', placement: 'left-start' }}
content: '圆形、矩形裁剪',
placement: 'left-start',
}}
onClick={() => { onClick={() => {
inCircled.value = !inCircled.value; inCircled.value = !inCircled.value;
realTimeCroppered(); realTimeCroppered();
}} }}
/> />
<Reload <Reload class={iconClass.value} v-tippy={{ content: '重置', placement: 'right-start' }} onClick={() => handCropper('reset')} />
class={iconClass.value}
v-tippy={{
content: '重置',
placement: 'right-start',
}}
onClick={() => handCropper('reset')}
/>
<ArrowUp <ArrowUp
class={iconClass.value} class={iconClass.value}
v-tippy={{ v-tippy={{ content: '上移(可长按)', placement: 'left-start' }}
content: '上移(可长按)',
placement: 'left-start',
}}
v-longpress={[() => handCropper('move', [0, -10]), '0:100']} v-longpress={[() => handCropper('move', [0, -10]), '0:100']}
/> />
<ArrowDown <ArrowDown
class={iconClass.value} class={iconClass.value}
v-tippy={{ v-tippy={{ content: '下移(可长按)', placement: 'right-start' }}
content: '下移(可长按)',
placement: 'right-start',
}}
v-longpress={[() => handCropper('move', [0, 10]), '0:100']} v-longpress={[() => handCropper('move', [0, 10]), '0:100']}
/> />
<ArrowLeft <ArrowLeft
@ -282,58 +262,21 @@ export default defineComponent({
/> />
<ArrowRight <ArrowRight
class={iconClass.value} class={iconClass.value}
v-tippy={{ v-tippy={{ content: '右移(可长按)', placement: 'right-start' }}
content: '右移(可长按)',
placement: 'right-start',
}}
v-longpress={[() => handCropper('move', [10, 0]), '0:100']} v-longpress={[() => handCropper('move', [10, 0]), '0:100']}
/> />
<ArrowH <ArrowH class={iconClass.value} v-tippy={{ content: '水平翻转', placement: 'left-start' }} onClick={() => handCropper('scaleX', -1)} />
class={iconClass.value} <ArrowV class={iconClass.value} v-tippy={{ content: '垂直翻转', placement: 'right-start' }} onClick={() => handCropper('scaleY', -1)} />
v-tippy={{ <RotateLeft class={iconClass.value} v-tippy={{ content: '逆时针旋转', placement: 'left-start' }} onClick={() => handCropper('rotate', -45)} />
content: '水平翻转', <RotateRight class={iconClass.value} v-tippy={{ content: '顺时针旋转', placement: 'right-start' }} onClick={() => handCropper('rotate', 45)} />
placement: 'left-start',
}}
onClick={() => handCropper('scaleX', -1)}
/>
<ArrowV
class={iconClass.value}
v-tippy={{
content: '垂直翻转',
placement: 'right-start',
}}
onClick={() => handCropper('scaleY', -1)}
/>
<RotateLeft
class={iconClass.value}
v-tippy={{
content: '逆时针旋转',
placement: 'left-start',
}}
onClick={() => handCropper('rotate', -45)}
/>
<RotateRight
class={iconClass.value}
v-tippy={{
content: '顺时针旋转',
placement: 'right-start',
}}
onClick={() => handCropper('rotate', 45)}
/>
<SearchPlus <SearchPlus
class={iconClass.value} class={iconClass.value}
v-tippy={{ v-tippy={{ content: '放大(可长按)', placement: 'left-start' }}
content: '放大(可长按)',
placement: 'left-start',
}}
v-longpress={[() => handCropper('zoom', 0.1), '0:100']} v-longpress={[() => handCropper('zoom', 0.1), '0:100']}
/> />
<SearchMinus <SearchMinus
class={iconClass.value} class={iconClass.value}
v-tippy={{ v-tippy={{ content: '缩小(可长按)', placement: 'right-start' }}
content: '缩小(可长按)',
placement: 'right-start',
}}
v-longpress={[() => handCropper('zoom', -0.1), '0:100']} v-longpress={[() => handCropper('zoom', -0.1), '0:100']}
/> />
</div> </div>

View File

@ -16,7 +16,7 @@ defineProps({
</script> </script>
<template> <template>
<div v-if="list.length" class="mb-4"> <div v-if="list.length">
<NoticeItem v-for="(item, index) in list" :key="index" :noticeItem="item" /> <NoticeItem v-for="(item, index) in list" :key="index" :noticeItem="item" />
</div> </div>
<el-empty v-else :description="emptyText" /> <el-empty v-else :description="emptyText" />

View File

@ -10,22 +10,21 @@ import { useTranslationLang } from '../../hooks/useTranslationLang';
import { usePermissionStoreHook } from '@/store/permission'; import { usePermissionStoreHook } from '@/store/permission';
import LaySidebarItem from '../lay-sidebar/components/SidebarItem.vue'; import LaySidebarItem from '../lay-sidebar/components/SidebarItem.vue';
import LaySidebarFullScreen from '../lay-sidebar/components/SidebarFullScreen.vue'; import LaySidebarFullScreen from '../lay-sidebar/components/SidebarFullScreen.vue';
import GlobalizationIcon from '@/assets/svg/globalization.svg?component';
import LogoutCircleRLine from '@iconify-icons/ri/logout-circle-r-line'; import LogoutCircleRLine from '@iconify-icons/ri/logout-circle-r-line';
import Setting from '@iconify-icons/ri/settings-3-line'; import Setting from '@iconify-icons/ri/settings-3-line';
import Check from '@iconify-icons/ep/check'; import Check from '@iconify-icons/ep/check';
import { $t } from '@/plugins/i18n'; import { $t } from '@/plugins/i18n';
import { userI18nTypeStore } from '@/store/i18n/i18nType';
import GlobalizationIcon from '@/assets/svg/globalization.svg?component';
const menuRef = ref(); const menuRef = ref();
const showLogo = ref(storageLocal().getItem<StorageConfigs>(`${responsiveStorageNameSpace()}configure`)?.showLogo ?? true); const showLogo = ref(storageLocal().getItem<StorageConfigs>(`${responsiveStorageNameSpace()}configure`)?.showLogo ?? true);
const { t, route, locale, translation } = useTranslationLang(menuRef); const { t, route, locale, translationCh, translationEn } = useTranslationLang(menuRef);
const { title, logout, onPanel, getLogo, username, userAvatar, backTopMenu, avatarsStyle, getDropdownItemStyle, getDropdownItemClass } = useNav(); const { title, logout, onPanel, getLogo, username, userAvatar, backTopMenu, avatarsStyle, getDropdownItemStyle, getDropdownItemClass } = useNav();
const defaultActive = computed(() => (!isAllEmpty(route.meta?.activePath) ? route.meta.activePath : route.path)); const defaultActive = computed(() => (!isAllEmpty(route.meta?.activePath) ? route.meta.activePath : route.path));
const i18nTypeStore = userI18nTypeStore();
nextTick(() => { nextTick(() => {
menuRef.value?.handleResize(); menuRef.value?.handleResize();
}); });
@ -50,21 +49,29 @@ onMounted(() => {
<!-- 菜单搜索 --> <!-- 菜单搜索 -->
<LaySearch id="header-search" /> <LaySearch id="header-search" />
<!-- 国际化 --> <!-- 国际化 -->
<el-dropdown trigger="click"> <el-dropdown id="header-translation" trigger="click">
<GlobalizationIcon class="hover:text-primary hover:!bg-[transparent] w-[20px] h-[20px] ml-1.5 cursor-pointer outline-none duration-300" /> <GlobalizationIcon class="navbar-bg-hover w-[40px] h-[48px] p-[11px] cursor-pointer outline-none" />
<template #dropdown> <template #dropdown>
<el-dropdown-menu class="translation"> <el-dropdown-menu class="translation">
<el-dropdown-item <el-dropdown-item
v-for="item in i18nTypeStore.translationTypeList" :class="['dark:!text-white', getDropdownItemClass(locale, 'zh')]"
:key="item.key" :style="getDropdownItemStyle(locale, 'zh')"
:class="['dark:!text-white', getDropdownItemClass(locale, item.key)]" @click="translationCh"
:style="getDropdownItemStyle(locale, item.key)"
@click="translation(item.key)"
> >
<span v-show="locale === item.key" class="check"> <span v-show="locale === 'zh'" class="check-zh">
<IconifyIconOffline :icon="Check" /> <IconifyIconOffline :icon="Check" />
</span> </span>
{{ item.value }} 简体中文
</el-dropdown-item>
<el-dropdown-item
:class="['dark:!text-white', getDropdownItemClass(locale, 'en')]"
:style="getDropdownItemStyle(locale, 'en')"
@click="translationEn"
>
<span v-show="locale === 'en'" class="check-en">
<IconifyIconOffline :icon="Check" />
</span>
English
</el-dropdown-item> </el-dropdown-item>
</el-dropdown-menu> </el-dropdown-menu>
</template> </template>

View File

@ -1,5 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import ReCropperPreview from '@/components/ReCropperPreview'; import ReCropperPreview from '@/components/CropperPreview';
import { sexConstant } from '@/enums/baseConstant'; import { sexConstant } from '@/enums/baseConstant';
import { $t } from '@/plugins/i18n'; import { $t } from '@/plugins/i18n';
import { useAdminUserStore } from '@/store/system/adminUser'; import { useAdminUserStore } from '@/store/system/adminUser';

View File

@ -8,7 +8,7 @@ import { $t } from '@/plugins/i18n';
import { isAddUserinfo } from '@/views/system/admin-user/utils/columns'; import { isAddUserinfo } from '@/views/system/admin-user/utils/columns';
import ResetPasswordDialog from '@/components/Table/ResetPasswords.vue'; import ResetPasswordDialog from '@/components/Table/ResetPasswords.vue';
import { deviceDetection, handleTree } from '@pureadmin/utils'; import { deviceDetection, handleTree } from '@pureadmin/utils';
import CropperPreview from '@/components/ReCropperPreview'; import CropperPreview from '@/components/CropperPreview';
import AssignUserToRole from '@/views/system/admin-user/components/assign-roles-to-user.vue'; import AssignUserToRole from '@/views/system/admin-user/components/assign-roles-to-user.vue';
import { useUserStore } from '@/store/system/user'; import { useUserStore } from '@/store/system/user';
import { useDeptStore } from '@/store/system/dept'; import { useDeptStore } from '@/store/system/dept';

View File

@ -2,7 +2,7 @@
import { onMounted, ref } from 'vue'; import { onMounted, ref } from 'vue';
import ReCol from '@/components/ReCol'; import ReCol from '@/components/ReCol';
import { chartData, useDark } from './utils'; import { chartData, useDark } from './utils';
import { ReNormalCountTo } from '@/components/ReCountTo'; import { ReNormalCountTo } from '@/components/CountTo';
import ChartLine from '@/views/welcome/components/ChartLine.vue'; import ChartLine from '@/views/welcome/components/ChartLine.vue';
import ChartRound from '@/views/welcome/components/ChartRound.vue'; import ChartRound from '@/views/welcome/components/ChartRound.vue';
import { getServerCommitList, getWebCommitList, serverCommitList, webCommitList } from '@/views/welcome/utils'; import { getServerCommitList, getWebCommitList, serverCommitList, webCommitList } from '@/views/welcome/utils';