Compare commits

..
26 Commits
Author SHA1 Message Date
Hooray Hu 7668bd5063 chore: release v4.10.0 2024-08-03 22:16:45 +08:00
Hooray Hu 5e3fa9a567 chore: 更新依赖 2024-08-03 22:01:11 +08:00
Hooray Hu 571190cd78 refactor: 手动登出时不带上重定向地址 2024-07-31 15:39:18 +08:00
Hooray Hu bd5c921bca feat: FileUpload 组件文件列表增加下载功能 2024-07-31 13:53:01 +08:00
Max Sky b236df4e30 微软 Microsoft 拼写错误 (#41) 2024-07-30 17:39:06 +08:00
Hooray Hu 657bec97f6 refactor: 导航菜单尺寸调整 2024-07-21 18:19:46 +08:00
Hooray Hu 5d0141011b chore: 更新依赖 2024-07-18 11:28:31 +08:00
Hooray Hu b8e53fb259 chore: eslint 规则调整 2024-07-11 19:56:58 +08:00
Hooray Hu 66c0e4f8c7 feat: 上传组件增加自定义上传 2024-07-09 18:02:02 +08:00
Hooray Hu 7457fcc23e refactor: vite插件合并到单个文件中维护 2024-07-08 16:40:54 +08:00
Hooray Hu 9a7331f32f refactor!: 移除createInjectionKey函数 2024-07-05 17:42:40 +08:00
Hooray Hu 87819b4b5d chore: 移除无用代码 2024-07-05 15:14:39 +08:00
Hooray Hu d00fc26616 refactor: 环境变量 VITE_APP_DISABLE_DEVTOOL 改成用 vite 插件的方式注入代码中 2024-07-03 23:25:36 +08:00
Hooray Hu 647edd7207 refactor: 环境变量 VITE_APP_DEBUG_TOOL 改成用 vite 插件的方式注入代码中 2024-07-03 23:01:46 +08:00
Hooray Hu 23d36432da feat: 次导航支持隐藏 2024-07-02 00:34:36 +08:00
Hooray Hu 2fdc4f18ca chore: release v4.9.0 2024-06-29 12:51:03 +08:00
Hooray Hu 961df08e2b chore: 更新依赖 2024-06-29 12:50:50 +08:00
Hooray Hu 1d62ac7558 style: HDropdownMenu 组件样式调整 2024-06-28 14:21:26 +08:00
Hooray Hu 456ef83081 feat: 新增哀悼模式和色弱模式 2024-06-28 10:06:56 +08:00
Hooray Hu 90b588028a feat: 增加禁用开发者工具环境变量 2024-06-26 10:45:45 +08:00
Hooray Hu 9e444c83f8 fix: 修复主导航 children 为空时报错 2024-06-25 00:44:07 +08:00
Hooray Hu 57fcd2785c refactor!: 配置项 menu.menuMode 变更为 menu.mode 2024-06-24 17:16:27 +08:00
Hooray Hu 3c8b4881b4 fix: 修复vscode失效配置 2024-06-24 15:55:31 +08:00
Hooray Hu e32d0561c7 chore: 更新readme 2024-06-21 09:58:18 +08:00
Hooray Hu d303776825 chore: 更新依赖 2024-06-15 22:53:45 +08:00
Hooray Hu 3ef0f69bcc chore: 样式微调 2024-06-12 14:10:48 +08:00
51 changed files with 2856 additions and 2692 deletions
+2
View File
@@ -6,6 +6,8 @@ VITE_APP_TITLE = Fantastic-admin 基础版
VITE_APP_API_BASEURL = /
# 调试工具,可设置 eruda 或 vconsole,如果不需要开启则留空
VITE_APP_DEBUG_TOOL =
# 是否禁用开发者工具,可防止被调试
VITE_APP_DISABLE_DEVTOOL = false
# 是否开启代理
VITE_OPEN_PROXY = false
+2
View File
@@ -6,6 +6,8 @@ VITE_APP_TITLE = 页面标题
VITE_APP_API_BASEURL = /
# 调试工具,可设置 eruda 或 vconsole,如果不需要开启则留空
VITE_APP_DEBUG_TOOL =
# 是否禁用开发者工具,可防止被调试
VITE_APP_DISABLE_DEVTOOL = false
# 是否在打包时启用 Mock
VITE_BUILD_MOCK = false
+2
View File
@@ -6,6 +6,8 @@ VITE_APP_TITLE = 页面标题(test)
VITE_APP_API_BASEURL = /
# 调试工具,可设置 eruda 或 vconsole,如果不需要开启则留空
VITE_APP_DEBUG_TOOL =
# 是否禁用开发者工具,可防止被调试
VITE_APP_DISABLE_DEVTOOL = false
# 是否在打包时启用 Mock
VITE_BUILD_MOCK = true
+1 -1
View File
@@ -1,5 +1,5 @@
{
"eslint.experimental.useFlatConfig": true,
"eslint.useFlatConfig": true,
"prettier.enable": false,
"editor.formatOnSave": false,
"editor.codeActionsOnSave": {
+19
View File
@@ -108,3 +108,22 @@
<td><img src="https://one-step-admin.github.io/preview6.png" /></td>
</tr>
</table>
<table>
<tr>
<th colspan="4" align="center">
<a href="https://fantastic-mobile.github.io" target="_blank">Fantastic-mobile</a>
</th>
</tr>
<tr>
<th colspan="4" align="center">
一款自成一派的移动端 H5 框架
</th>
</tr>
<tr>
<td><img src="https://fantastic-mobile.github.io/preview1.png" /></td>
<td><img src="https://fantastic-mobile.github.io/preview2.png" /></td>
<td><img src="https://fantastic-mobile.github.io/preview3.png" /></td>
<td><img src="https://fantastic-mobile.github.io/preview4.png" /></td>
</tr>
</table>
+4 -1
View File
@@ -12,7 +12,10 @@ export default antfu(
rules: {
'eslint-comments/no-unlimited-disable': 'off',
'curly': ['error', 'all'],
'antfu/consistent-list-newline': 'off',
'ts/no-unused-expressions': ['error', {
allowShortCircuit: true,
allowTernary: true,
}],
},
},
{
+1 -1
View File
@@ -28,7 +28,7 @@
<div class="browsers">
<a href="https://www.microsoft.com/edge" target="_blank" class="browser">
<img class="browser-icon" src="/browser_upgrade/edge.png" />
<div class="browser-name">Mircosoft Edge</div>
<div class="browser-name">Microsoft Edge</div>
</a>
<a href="https://www.google.cn/chrome/" target="_blank" class="browser">
<img class="browser-icon" src="/browser_upgrade/chrome.png" />
+41 -40
View File
@@ -1,6 +1,6 @@
{
"type": "module",
"version": "4.8.2",
"version": "4.10.0",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
@@ -24,83 +24,84 @@
},
"dependencies": {
"@headlessui/vue": "^1.7.22",
"@imengyu/vue3-context-menu": "^1.4.1",
"@vueuse/core": "^10.10.0",
"@vueuse/integrations": "^10.10.0",
"axios": "^1.7.2",
"dayjs": "^1.11.11",
"@imengyu/vue3-context-menu": "^1.4.2",
"@vueuse/core": "^10.11.0",
"@vueuse/integrations": "^10.11.0",
"axios": "^1.7.3",
"dayjs": "^1.11.12",
"defu": "^6.1.4",
"element-plus": "^2.7.4",
"eruda": "^3.0.1",
"disable-devtool": "^0.3.7",
"element-plus": "^2.7.8",
"eruda": "^3.2.1",
"floating-vue": "5.2.2",
"hotkeys-js": "^3.13.7",
"lodash-es": "^4.17.21",
"mitt": "^3.0.1",
"mockjs": "^1.1.0",
"nprogress": "^0.2.0",
"overlayscrollbars": "^2.8.3",
"overlayscrollbars": "^2.10.0",
"overlayscrollbars-vue": "^0.5.9",
"path-browserify": "^1.0.1",
"path-to-regexp": "^6.2.2",
"pinia": "^2.1.7",
"qs": "^6.12.1",
"path-to-regexp": "^7.1.0",
"pinia": "^2.2.0",
"qs": "^6.13.0",
"scule": "^1.3.0",
"vconsole": "^3.15.1",
"vue": "^3.4.27",
"vue": "^3.4.35",
"vue-m-message": "^4.0.2",
"vue-router": "^4.3.2"
"vue-router": "^4.4.2"
},
"devDependencies": {
"@antfu/eslint-config": "2.20.0",
"@iconify/json": "^2.2.217",
"@antfu/eslint-config": "2.24.1",
"@iconify/json": "^2.2.233",
"@iconify/vue": "^4.1.2",
"@stylistic/stylelint-config": "^1.0.1",
"@stylistic/stylelint-config": "^2.0.0",
"@types/lodash-es": "^4.17.12",
"@types/mockjs": "^1.0.10",
"@types/nprogress": "^0.2.3",
"@types/path-browserify": "^1.0.2",
"@types/qs": "^6.9.15",
"@unocss/eslint-plugin": "^0.60.4",
"@unocss/eslint-plugin": "^0.61.9",
"@vitejs/plugin-legacy": "^5.4.1",
"@vitejs/plugin-vue": "^5.0.5",
"@vitejs/plugin-vue": "^5.1.2",
"@vitejs/plugin-vue-jsx": "^4.0.0",
"archiver": "^7.0.1",
"autoprefixer": "^10.4.19",
"autoprefixer": "^10.4.20",
"boxen": "^7.1.1",
"bumpp": "^9.4.1",
"cz-git": "^1.9.2",
"eslint": "^9.4.0",
"bumpp": "^9.4.2",
"cz-git": "^1.9.4",
"eslint": "^9.8.0",
"esno": "^4.7.0",
"fs-extra": "^11.2.0",
"http-server": "^14.1.1",
"inquirer": "^9.2.23",
"lint-staged": "^15.2.5",
"npm-run-all2": "^6.2.0",
"inquirer": "^10.1.6",
"lint-staged": "^15.2.8",
"npm-run-all2": "^6.2.2",
"picocolors": "^1.0.1",
"plop": "^4.0.1",
"postcss": "^8.4.38",
"sass": "^1.77.4",
"postcss": "^8.4.40",
"sass": "^1.77.8",
"simple-git-hooks": "^2.11.1",
"stylelint": "^16.6.1",
"stylelint": "^16.8.1",
"stylelint-config-recess-order": "^5.0.1",
"stylelint-config-standard-scss": "^13.1.0",
"stylelint-config-standard-vue": "^1.0.0",
"stylelint-scss": "^6.3.1",
"stylelint-scss": "^6.4.1",
"svgo": "^3.3.2",
"typescript": "^5.4.5",
"unocss": "^0.60.4",
"unplugin-auto-import": "^0.17.6",
"unplugin-turbo-console": "^1.8.6",
"unplugin-vue-components": "^0.27.0",
"vite": "^5.2.12",
"typescript": "^5.5.4",
"unocss": "^0.61.9",
"unplugin-auto-import": "^0.18.2",
"unplugin-turbo-console": "^1.10.1",
"unplugin-vue-components": "^0.27.3",
"vite": "^5.3.5",
"vite-plugin-banner": "^0.7.1",
"vite-plugin-compression2": "^1.1.1",
"vite-plugin-compression2": "^1.1.3",
"vite-plugin-fake-server": "^2.1.1",
"vite-plugin-pages": "^0.32.2",
"vite-plugin-pages": "^0.32.3",
"vite-plugin-svg-icons": "^2.0.1",
"vite-plugin-vue-devtools": "^7.2.1",
"vite-plugin-vue-devtools": "^7.3.7",
"vite-plugin-vue-meta-layouts": "^0.4.3",
"vue-tsc": "^2.0.19"
"vue-tsc": "^2.0.29"
},
"simple-git-hooks": {
"pre-commit": "pnpm lint-staged",
+2374 -2324
View File
File diff suppressed because it is too large Load Diff
+6 -6
View File
@@ -1,14 +1,14 @@
<script setup lang="ts">
import eruda from 'eruda'
import VConsole from 'vconsole'
import hotkeys from 'hotkeys-js'
import eventBus from './utils/eventBus'
import Provider from './ui-provider/index.vue'
import useSettingsStore from '@/store/modules/settings'
import useMenuStore from '@/store/modules/menu'
const route = useRoute()
const settingsStore = useSettingsStore()
const menuStore = useMenuStore()
const { auth } = useAuth()
const isAuth = computed(() => {
@@ -20,7 +20,7 @@ const isAuth = computed(() => {
// 侧边栏主导航当前实际宽度
const mainSidebarActualWidth = computed(() => {
let actualWidth = Number.parseInt(getComputedStyle(document.documentElement).getPropertyValue('--g-main-sidebar-width'))
if (settingsStore.settings.menu.menuMode === 'single' || (settingsStore.settings.menu.menuMode === 'head' && settingsStore.mode !== 'mobile')) {
if (settingsStore.settings.menu.mode === 'single' || (settingsStore.settings.menu.mode === 'head' && settingsStore.mode !== 'mobile')) {
actualWidth = 0
}
return `${actualWidth}px`
@@ -32,6 +32,9 @@ const subSidebarActualWidth = computed(() => {
if (settingsStore.settings.menu.subMenuCollapse && settingsStore.mode !== 'mobile') {
actualWidth = Number.parseInt(getComputedStyle(document.documentElement).getPropertyValue('--g-sub-sidebar-collapse-width'))
}
if (menuStore.sidebarMenus.every(item => item.meta?.menu === false)) {
actualWidth = 0
}
return `${actualWidth}px`
})
@@ -61,9 +64,6 @@ onMounted(() => {
eventBus.emit('global-system-info-toggle')
})
})
import.meta.env.VITE_APP_DEBUG_TOOL === 'eruda' && eruda.init()
import.meta.env.VITE_APP_DEBUG_TOOL === 'vconsole' && new VConsole()
</script>
<template>
+4 -4
View File
@@ -17,6 +17,8 @@
// 明暗模式 CSS 变量
/* stylelint-disable-next-line no-duplicate-selectors */
:root {
--g-box-shadow-color: rgb(0 0 0 / 12%);
&::view-transition-old(root),
&::view-transition-new(root) {
mix-blend-mode: normal;
@@ -31,9 +33,9 @@
z-index: 9999;
}
--g-box-shadow-color: rgb(0 0 0 / 12%);
&.dark {
--g-box-shadow-color: rgb(0 0 0 / 72%);
&::view-transition-old(root) {
z-index: 9999;
}
@@ -41,8 +43,6 @@
&::view-transition-new(root) {
z-index: 1;
}
--g-box-shadow-color: rgb(0 0 0 / 72%);
}
}
+26
View File
@@ -17,6 +17,7 @@ const props = withDefaults(
files?: UploadUserFile[]
notip?: boolean
ext?: string[]
httpRequest?: UploadProps['httpRequest']
}>(),
{
name: 'file',
@@ -57,6 +58,29 @@ const onExceed: UploadProps['onExceed'] = () => {
const onSuccess: UploadProps['onSuccess'] = (res, file, fileList) => {
emits('onSuccess', res, file, fileList)
}
const onPreview: UploadProps['onPreview'] = (e) => {
const getBlob = (url: string) => new Promise((resolve) => {
const xhr = new XMLHttpRequest()
xhr.open('GET', url, true)
xhr.responseType = 'blob'
xhr.onload = () => {
if (xhr.status === 200) {
resolve(xhr.response)
}
}
xhr.send()
})
getBlob(e.url!).then((blob: any) => {
const a = document.createElement('a')
const url = window.URL.createObjectURL(blob)
const event = new MouseEvent('click')
a.target = '_blank'
a.download = e.name
a.href = url
a.dispatchEvent(event)
})
}
</script>
<template>
@@ -68,6 +92,8 @@ const onSuccess: UploadProps['onSuccess'] = (res, file, fileList) => {
:before-upload="beforeUpload"
:on-exceed="onExceed"
:on-success="onSuccess"
:on-preview="onPreview"
:http-request="httpRequest"
:file-list="files"
:limit="max"
drag
+2
View File
@@ -18,6 +18,7 @@ const props = withDefaults(
placeholder?: string
notip?: boolean
ext?: string[]
httpRequest?: UploadProps['httpRequest']
}>(),
{
name: 'file',
@@ -97,6 +98,7 @@ const onSuccess: UploadProps['onSuccess'] = (res) => {
:before-upload="beforeUpload"
:on-progress="onProgress"
:on-success="onSuccess"
:http-request="httpRequest"
drag
class="image-upload"
>
+2
View File
@@ -19,6 +19,7 @@ const props = withDefaults(
placeholder?: string
notip?: boolean
ext?: string[]
httpRequest?: UploadProps['httpRequest']
}>(),
{
name: 'file',
@@ -131,6 +132,7 @@ const onSuccess: UploadProps['onSuccess'] = (res) => {
:before-upload="beforeUpload"
:on-progress="onProgress"
:on-success="onSuccess"
:http-request="httpRequest"
drag
class="images-upload"
>
+22 -7
View File
@@ -17,7 +17,7 @@ const menuStore = useMenuStore()
const isShow = ref(false)
watch(() => settingsStore.settings.menu.menuMode, (value) => {
watch(() => settingsStore.settings.menu.mode, (value) => {
if (value === 'single') {
menuStore.setActived(0)
}
@@ -103,17 +103,17 @@ function handleCopy() {
</div>
<div v-if="settingsStore.mode === 'pc'" class="menu-mode">
<HTooltip text="侧边栏模式 (含主导航)" placement="bottom" :delay="500">
<div class="mode mode-side" :class="{ active: settingsStore.settings.menu.menuMode === 'side' }" @click="settingsStore.settings.menu.menuMode = 'side'">
<div class="mode mode-side" :class="{ active: settingsStore.settings.menu.mode === 'side' }" @click="settingsStore.settings.menu.mode = 'side'">
<div class="mode-container" />
</div>
</HTooltip>
<HTooltip text="顶部模式" placement="bottom" :delay="500">
<div class="mode mode-head" :class="{ active: settingsStore.settings.menu.menuMode === 'head' }" @click="settingsStore.settings.menu.menuMode = 'head'">
<div class="mode mode-head" :class="{ active: settingsStore.settings.menu.mode === 'head' }" @click="settingsStore.settings.menu.mode = 'head'">
<div class="mode-container" />
</div>
</HTooltip>
<HTooltip text="侧边栏模式 (不含主导航)" placement="bottom" :delay="500">
<div class="mode mode-single" :class="{ active: settingsStore.settings.menu.menuMode === 'single' }" @click="settingsStore.settings.menu.menuMode = 'single'">
<div class="mode mode-single" :class="{ active: settingsStore.settings.menu.mode === 'single' }" @click="settingsStore.settings.menu.mode = 'single'">
<div class="mode-container" />
</div>
</HTooltip>
@@ -128,7 +128,7 @@ function handleCopy() {
<SvgIcon name="i-ri:question-line" />
</HTooltip>
</div>
<HToggle v-model="settingsStore.settings.menu.switchMainMenuAndPageJump" :disabled="['single'].includes(settingsStore.settings.menu.menuMode)" />
<HToggle v-model="settingsStore.settings.menu.switchMainMenuAndPageJump" :disabled="['single'].includes(settingsStore.settings.menu.mode)" />
</div>
<div class="setting-item">
<div class="label">
@@ -155,7 +155,7 @@ function handleCopy() {
<div class="label">
是否启用快捷键
</div>
<HToggle v-model="settingsStore.settings.menu.enableHotkeys" :disabled="['single'].includes(settingsStore.settings.menu.menuMode)" />
<HToggle v-model="settingsStore.settings.menu.enableHotkeys" :disabled="['single'].includes(settingsStore.settings.menu.mode)" />
</div>
<div class="divider">
顶栏
@@ -327,6 +327,21 @@ function handleCopy() {
</div>
<HToggle v-model="settingsStore.settings.app.enableProgress" />
</div>
<div class="setting-item">
<div class="label">
哀悼模式
<HTooltip text="该功能开启时,整站会变为灰色">
<SvgIcon name="i-ri:question-line" />
</HTooltip>
</div>
<HToggle v-model="settingsStore.settings.app.enableMournMode" />
</div>
<div class="setting-item">
<div class="label">
色弱模式
</div>
<HToggle v-model="settingsStore.settings.app.enableColorAmblyopiaMode" />
</div>
<div class="setting-item">
<div class="label">
动态标题
@@ -380,7 +395,7 @@ function handleCopy() {
}
.mode-container {
--at-apply: bg-ui-primary/20 border-dashed border-ui-primary;
--at-apply: bg-ui-primary/20 border-width-1.5 border-dashed border-ui-primary;
&::before {
--at-apply: content-empty absolute w-full h-full;
+8 -4
View File
@@ -27,15 +27,15 @@ function handlerMouserScroll(event: WheelEvent) {
<template>
<Transition name="header">
<header v-if="settingsStore.mode === 'pc' && settingsStore.settings.menu.menuMode === 'head'">
<header v-if="settingsStore.mode === 'pc' && settingsStore.settings.menu.mode === 'head'">
<div class="header-container">
<Logo class="title" />
<div ref="menuRef" class="menu-container" @wheel.prevent="handlerMouserScroll">
<!-- 顶部模式 -->
<div class="menu flex of-hidden transition-all">
<div class="menu h-full flex of-hidden transition-all">
<template v-for="(item, index) in menuStore.allMenus" :key="index">
<div
class="menu-item relative transition-all" :class="{
class="menu-item relative mx-1 py-2 transition-all" :class="{
active: index === menuStore.actived,
}"
>
@@ -45,7 +45,7 @@ function handlerMouserScroll(event: WheelEvent) {
}" :title="typeof item.meta?.title === 'function' ? item.meta?.title() : item.meta?.title" @click="switchTo(index)"
>
<div class="inline-flex flex-1 items-center justify-center gap-1">
<SvgIcon v-if="item.meta?.icon" :name="item.meta?.icon" :size="20" class="menu-item-container-icon transition-transform group-hover-scale-120" async />
<SvgIcon v-if="item.meta?.icon" :name="item.meta?.icon" class="menu-item-container-icon transition-transform group-hover-scale-120" />
<span class="w-full flex-1 truncate text-sm transition-height transition-opacity transition-width">
{{ typeof item.meta?.title === 'function' ? item.meta?.title() : item.meta?.title }}
</span>
@@ -135,6 +135,10 @@ header {
color: var(--g-header-menu-hover-color);
background-color: var(--g-header-menu-hover-bg);
}
.menu-item-container-icon {
font-size: 20px !important;
}
}
&.active .menu-item-container {
@@ -38,7 +38,7 @@ onMounted(() => {
</li>
</ul>
</div>
<div v-if="settingsStore.settings.menu.enableHotkeys && ['side', 'head'].includes(settingsStore.settings.menu.menuMode)">
<div v-if="settingsStore.settings.menu.enableHotkeys && ['side', 'head'].includes(settingsStore.settings.menu.mode)">
<h2 class="m-0 text-lg font-bold">
主导航
</h2>
+22 -20
View File
@@ -15,30 +15,32 @@ const { switchTo } = useMenu()
<template>
<Transition name="main-sidebar">
<div v-if="settingsStore.settings.menu.menuMode === 'side' || (settingsStore.mode === 'mobile' && settingsStore.settings.menu.menuMode !== 'single')" class="main-sidebar-container">
<div v-if="settingsStore.settings.menu.mode === 'side' || (settingsStore.mode === 'mobile' && settingsStore.settings.menu.mode !== 'single')" class="main-sidebar-container">
<Logo :show-title="false" class="sidebar-logo" />
<!-- 侧边栏模式(含主导航) -->
<div class="menu flex flex-col of-hidden transition-all">
<template v-for="(item, index) in menuStore.allMenus" :key="index">
<div
class="menu-item relative transition-all" :class="{
active: index === menuStore.actived,
}"
>
<div class="menu">
<!-- 侧边栏模式(含主导航) -->
<div class="w-full flex flex-col of-hidden py-1 transition-all -mt-2">
<template v-for="(item, index) in menuStore.allMenus" :key="index">
<div
v-if="item.children && item.children.length !== 0" class="group menu-item-container h-full w-full flex cursor-pointer items-center justify-between gap-1 py-4 text-[var(--g-main-sidebar-menu-color)] transition-all hover-(bg-[var(--g-main-sidebar-menu-hover-bg)] text-[var(--g-main-sidebar-menu-hover-color)]) px-2!" :class="{
'text-[var(--g-main-sidebar-menu-active-color)]! bg-[var(--g-main-sidebar-menu-active-bg)]!': index === menuStore.actived,
}" :title="typeof item.meta?.title === 'function' ? item.meta?.title() : item.meta?.title" @click="switchTo(index)"
class="menu-item relative px-2 py-1 transition-all" :class="{
active: index === menuStore.actived,
}"
>
<div class="w-full inline-flex flex-1 flex-col items-center justify-center gap-[2px]">
<SvgIcon v-if="item.meta?.icon" :name="item.meta?.icon" :size="20" class="menu-item-container-icon transition-transform group-hover-scale-120" async />
<span class="w-full flex-1 truncate text-center text-sm transition-height transition-opacity transition-width">
{{ typeof item.meta?.title === 'function' ? item.meta?.title() : item.meta?.title }}
</span>
<div
v-if="item.children && item.children.length !== 0" class="group menu-item-container h-full w-full flex cursor-pointer items-center justify-between gap-1 py-4 text-[var(--g-main-sidebar-menu-color)] transition-all hover-(bg-[var(--g-main-sidebar-menu-hover-bg)] text-[var(--g-main-sidebar-menu-hover-color)]) px-2!" :class="{
'text-[var(--g-main-sidebar-menu-active-color)]! bg-[var(--g-main-sidebar-menu-active-bg)]!': index === menuStore.actived,
}" :title="typeof item.meta?.title === 'function' ? item.meta?.title() : item.meta?.title" @click="switchTo(index)"
>
<div class="w-full inline-flex flex-1 flex-col items-center justify-center gap-[2px]">
<SvgIcon v-if="item.meta?.icon" :name="item.meta?.icon" class="menu-item-container-icon transition-transform group-hover-scale-120" />
<span class="w-full flex-1 truncate text-center text-sm transition-height transition-opacity transition-width">
{{ typeof item.meta?.title === 'function' ? item.meta?.title() : item.meta?.title }}
</span>
</div>
</div>
</div>
</div>
</template>
</template>
</div>
</div>
</div>
</Transition>
@@ -86,7 +88,7 @@ const { switchTo } = useMenu()
}
.menu-item-container-icon {
font-size: 24px !important;
font-size: 20px !important;
}
}
+3 -5
View File
@@ -162,11 +162,9 @@ provide(rootMenuInjectionKey, reactive({
<template>
<div
class="flex flex-col of-hidden transition-all" :class="{
'w-[200px]': !isMenuPopup && props.mode === 'vertical',
'w-[64px]': isMenuPopup && props.mode === 'vertical',
'h-[80px]': props.mode === 'horizontal',
'flex-row! w-auto': isMenuPopup && props.mode === 'horizontal',
class="h-full w-full flex flex-col of-hidden transition-all" :class="{
'flex-row! w-auto!': isMenuPopup && props.mode === 'horizontal',
'py-1': props.mode === 'vertical',
}"
>
<template v-for="item in menu" :key="item.path ?? JSON.stringify(item)">
+9 -3
View File
@@ -2,6 +2,10 @@
import type { SubMenuItemProps } from './types'
import { rootMenuInjectionKey } from './types'
defineOptions({
name: 'SubMenuItem',
})
const props = withDefaults(
defineProps<SubMenuItemProps>(),
{
@@ -40,7 +44,9 @@ defineExpose({
<template>
<div
ref="itemRef" class="menu-item relative transition-all" :class="{
active: isItemActive,
'active': isItemActive,
'py-1 px-2': (rootMenu.isMenuPopup && rootMenu.props.mode === 'vertical') || (rootMenu.isMenuPopup && level !== 0 && rootMenu.props.mode === 'horizontal') || !rootMenu.isMenuPopup,
'px-1 py-2': rootMenu.isMenuPopup && level === 0 && rootMenu.props.mode === 'horizontal',
}"
>
<router-link v-slot="{ href, navigate }" custom :to="uniqueKey.at(-1) ?? ''">
@@ -52,7 +58,7 @@ defineExpose({
target: item.meta?.link ? '_blank' : '_self',
class: 'no-underline',
}),
}" class="group menu-item-container h-full w-full flex cursor-pointer items-center justify-between gap-1 px-5 py-4 text-[var(--g-sub-sidebar-menu-color)] transition-all hover-(bg-[var(--g-sub-sidebar-menu-hover-bg)] text-[var(--g-sub-sidebar-menu-hover-color)])" :class="{
}" class="group menu-item-container h-full w-full flex cursor-pointer items-center justify-between gap-1 px-4 py-3 text-[var(--g-sub-sidebar-menu-color)] transition-all hover-(bg-[var(--g-sub-sidebar-menu-hover-bg)] text-[var(--g-sub-sidebar-menu-hover-color)])" :class="{
'text-[var(--g-sub-sidebar-menu-active-color)]! bg-[var(--g-sub-sidebar-menu-active-bg)]!': isItemActive,
'px-3!': rootMenu.isMenuPopup && level === 0,
}" :title="typeof item.meta?.title === 'function' ? item.meta?.title() : item.meta?.title" v-on="{
@@ -68,7 +74,7 @@ defineExpose({
'w-full': rootMenu.isMenuPopup && level === 0 && rootMenu.props.showCollapseName && rootMenu.props.mode === 'vertical',
}" :style="indentStyle"
>
<SvgIcon v-if="props.item.meta?.icon" :name="props.item.meta.icon" :size="20" class="menu-item-container-icon transition-transform group-hover-scale-120" async />
<SvgIcon v-if="props.item.meta?.icon" :name="props.item.meta.icon" :size="18" class="menu-item-container-icon transition-transform group-hover-scale-120" />
<span
v-if="!(rootMenu.isMenuPopup && level === 0 && !rootMenu.props.showCollapseName)" class="w-0 flex-1 truncate text-sm transition-height transition-opacity transition-width"
:class="{
+5 -1
View File
@@ -147,6 +147,9 @@ function handleMouseenter() {
subMenuRef.value!.getElement()!.style.height = `${window.innerHeight - top}px`
}
}
if (left + subMenuRef.value!.getElement()!.offsetWidth > document.documentElement.clientWidth) {
left = el.getBoundingClientRect().left - el.getBoundingClientRect().width
}
subMenuRef.value!.getElement()!.style.top = `${top}px`
subMenuRef.value!.getElement()!.style.left = `${left}px`
})
@@ -185,7 +188,8 @@ function handleMouseleave() {
v-if="opened" ref="subMenuRef" :options="{ scrollbars: { visibility: 'hidden' } }" defer class="sub-menu" :class="{
'bg-[var(--g-sub-sidebar-bg)]': rootMenu.isMenuPopup,
'ring-1 ring-stone-2 dark-ring-stone-8 shadow-xl fixed z-3000 w-[200px]': rootMenu.isMenuPopup,
'mx-2': rootMenu.isMenuPopup && (rootMenu.props.mode === 'vertical' || level !== 0),
'mx-1': rootMenu.isMenuPopup && (rootMenu.props.mode === 'vertical' || level !== 0),
'py-1': rootMenu.isMenuPopup,
}"
>
<template v-for="item in menu.children" :key="item.path ?? JSON.stringify(item)">
+1 -2
View File
@@ -1,4 +1,3 @@
import { createInjectionKey } from '@/utils/injectionKeys'
import type { Menu } from '#/global'
export interface MenuItem {
@@ -31,7 +30,7 @@ export interface MenuInjection {
handleSubMenuClick: (index: string, indexPath: string[]) => void
}
export const rootMenuInjectionKey = createInjectionKey<MenuInjection>('rootMenu')
export const rootMenuInjectionKey = Symbol('rootMenu') as InjectionKey<MenuInjection>
export interface SubMenuProps {
uniqueKey: string[]
+35 -13
View File
@@ -25,30 +25,48 @@ function onSidebarScroll() {
showShadowBottom.value = Math.ceil(scrollTop + clientHeight) < scrollHeight
}
const enableSidebar = computed(() => {
return settingsStore.mode === 'mobile' || (
menuStore.sidebarMenus.length !== 0
&& !menuStore.sidebarMenus.every(item => item.meta?.menu === false)
)
})
watch(enableSidebar, (val) => {
if (val) {
nextTick(() => {
onSidebarScroll()
})
}
}, {
immediate: true,
})
const menuRef = ref()
onMounted(() => {
onSidebarScroll()
const { height } = useElementSize(menuRef)
watch(() => height.value, () => {
if (height.value > 0) {
onSidebarScroll()
}
}, {
immediate: true,
})
if (enableSidebar.value) {
const { height } = useElementSize(menuRef)
watch(() => height.value, () => {
if (height.value > 0) {
onSidebarScroll()
}
}, {
immediate: true,
})
}
})
</script>
<template>
<div
class="sub-sidebar-container" :class="{
v-if="enableSidebar" class="sub-sidebar-container" :class="{
'is-collapse': settingsStore.mode === 'pc' && settingsStore.settings.menu.subMenuCollapse,
}"
>
<Logo
:show-logo="settingsStore.settings.menu.menuMode === 'single'" class="sidebar-logo" :class="{
'sidebar-logo-bg': settingsStore.settings.menu.menuMode === 'single',
:show-logo="settingsStore.settings.menu.mode === 'single'" class="sidebar-logo" :class="{
'sidebar-logo-bg': settingsStore.settings.menu.mode === 'single',
}"
/>
<div
@@ -61,7 +79,11 @@ onMounted(() => {
<TransitionGroup name="sub-sidebar">
<template v-for="(mainItem, mainIndex) in menuStore.allMenus" :key="mainIndex">
<div v-show="mainIndex === menuStore.actived">
<Menu :menu="mainItem.children" :value="route.meta.activeMenu || route.path" :default-openeds="menuStore.defaultOpenedPaths" :accordion="settingsStore.settings.menu.subMenuUniqueOpened" :collapse="settingsStore.mode === 'pc' && settingsStore.settings.menu.subMenuCollapse" class="menu" />
<Menu
:menu="mainItem.children" :value="route.meta.activeMenu || route.path" :default-openeds="menuStore.defaultOpenedPaths" :accordion="settingsStore.settings.menu.subMenuUniqueOpened" :collapse="settingsStore.mode === 'pc' && settingsStore.settings.menu.subMenuCollapse" class="menu" :class="{
'-mt-2': !['head', 'single'].includes(settingsStore.settings.menu.mode),
}"
/>
</div>
</template>
</TransitionGroup>
@@ -15,7 +15,7 @@ const settingsStore = useSettingsStore()
<div class="h-full flex items-center of-hidden pl-2 pr-16" style="mask-image: linear-gradient(90deg, #000 0%, #000 calc(100% - 50px), transparent);">
<LeftSide />
</div>
<div v-show="['side', 'single'].includes(settingsStore.settings.menu.menuMode)" class="h-full flex items-center px-2">
<div v-show="['side', 'single'].includes(settingsStore.settings.menu.mode)" class="h-full flex items-center px-2">
<RightSide />
</div>
</div>
@@ -40,7 +40,7 @@ watch(() => userStore.avatar, () => {
{ label: '快捷键介绍', handle: () => eventBus.emit('global-hotkeys-intro-toggle'), hide: settingsStore.mode !== 'pc' },
],
[
{ label: '退出登录', handle: () => userStore.logout() },
{ label: '退出登录', handle: () => userStore.logout(settingsStore.settings.home.fullPath) },
],
]" class="flex-center cursor-pointer px-2"
>
+1 -1
View File
@@ -11,7 +11,7 @@ const settingsStore = useSettingsStore()
const enableToolbar = computed(() => {
return !(
settingsStore.settings.menu.menuMode === 'head' && (
settingsStore.settings.menu.mode === 'head' && (
!settingsStore.settings.toolbar.breadcrumb || settingsStore.settings.app.routeBaseOn === 'filesystem'
)
)
+1 -1
View File
@@ -19,7 +19,7 @@ const myItems = computed(() => {
<VMenu :show-triggers="['hover']" :auto-hide="false" :popper-triggers="['hover', 'click']" :delay="200" v-bind="$attrs">
<slot />
<template #popper>
<div v-for="(item, index) in myItems" :key="index" class="b-b-(stone-2 solid) p-1 last-b-b-size-0 dark-b-b-(stone-7)">
<div v-for="(item, index) in myItems" :key="index" class="b-b-(1 stone-2 solid) p-1 dark-b-b-(1 stone-7) last-b-b-size-0">
<button v-for="(v, i) in item" :key="i" :disabled="v.disabled" class="w-full flex cursor-pointer items-center gap-2 border-size-0 rounded-md bg-inherit px-2 py-1.5 text-sm text-dark disabled-cursor-not-allowed dark-text-white disabled-opacity-50 hover-not-disabled-bg-stone-1 dark-hover-not-disabled-bg-stone-9" @click="v.handle">
{{ v.label }}
</button>
+1 -1
View File
@@ -30,7 +30,7 @@ router.beforeEach(async (to, from, next) => {
// 是否已根据权限动态生成并注册路由
if (routeStore.isGenerate) {
// 导航栏如果不是 single 模式,则需要根据 path 定位主导航的选中状态
settingsStore.settings.menu.menuMode !== 'single' && menuStore.setActived(to.path)
settingsStore.settings.menu.mode !== 'single' && menuStore.setActived(to.path)
// 如果已登录状态下,进入登录页会强制跳转到主页
if (to.name === 'login') {
next({
+3 -1
View File
@@ -5,6 +5,8 @@ import type { RecursiveRequired, Settings } from '#/global'
const globalSettingsDefault: RecursiveRequired<Settings.all> = {
app: {
colorScheme: 'light',
enableMournMode: false,
enableColorAmblyopiaMode: false,
enablePermission: false,
enableProgress: true,
enableDynamicTitle: false,
@@ -20,7 +22,7 @@ const globalSettingsDefault: RecursiveRequired<Settings.all> = {
},
menu: {
baseOn: 'frontend',
menuMode: 'side',
mode: 'side',
switchMainMenuAndPageJump: false,
subMenuUniqueOpened: true,
subMenuCollapse: false,
+19 -17
View File
@@ -23,24 +23,26 @@ const useMenuStore = defineStore(
function convertRouteToMenu(routes: Route.recordMainRaw[]): Menu.recordMainRaw[] {
const returnMenus: Menu.recordMainRaw[] = []
routes.forEach((item) => {
if (settingsStore.settings.menu.menuMode === 'single') {
returnMenus.length === 0 && returnMenus.push({
meta: {},
children: [],
})
returnMenus[0].children.push(...convertRouteToMenuRecursive(item.children))
}
else {
const menuItem: Menu.recordMainRaw = {
meta: {
title: item?.meta?.title,
icon: item?.meta?.icon,
auth: item?.meta?.auth,
},
children: [],
if (item.children.length > 0) {
if (settingsStore.settings.menu.mode === 'single') {
returnMenus.length === 0 && returnMenus.push({
meta: {},
children: [],
})
returnMenus[0].children.push(...convertRouteToMenuRecursive(item.children))
}
else {
const menuItem: Menu.recordMainRaw = {
meta: {
title: item?.meta?.title,
icon: item?.meta?.icon,
auth: item?.meta?.auth,
},
children: [],
}
menuItem.children = convertRouteToMenuRecursive(item.children)
returnMenus.push(menuItem)
}
menuItem.children = convertRouteToMenuRecursive(item.children)
returnMenus.push(menuItem)
}
})
return returnMenus
+18 -1
View File
@@ -39,8 +39,25 @@ const useSettingsStore = defineStore(
break
}
}
watch([
() => settings.value.app.enableMournMode,
() => settings.value.app.enableColorAmblyopiaMode,
], (val) => {
document.documentElement.style.removeProperty('filter')
if (val[0] && val[1]) {
document.documentElement.style.setProperty('filter', 'grayscale(100%) invert(80%)')
}
else if (val[0]) {
document.documentElement.style.setProperty('filter', 'grayscale(100%)')
}
else if (val[1]) {
document.documentElement.style.setProperty('filter', 'invert(80%)')
}
}, {
immediate: true,
})
watch(() => settings.value.menu.menuMode, (val) => {
watch(() => settings.value.menu.mode, (val) => {
document.body.setAttribute('data-menu-mode', val)
}, {
immediate: true,
+1 -1
View File
@@ -50,7 +50,7 @@ const useUserStore = defineStore(
router.push({
name: 'login',
query: {
...(router.currentRoute.value.path !== settingsStore.settings.home.fullPath && router.currentRoute.value.name !== 'login' && { redirect }),
...(redirect !== settingsStore.settings.home.fullPath && router.currentRoute.value.name !== 'login' && { redirect }),
},
})
}
+11 -1
View File
@@ -16,6 +16,16 @@ declare namespace Settings {
* @可选值 `'dark'` 暗黑模式
*/
colorScheme?: '' | 'light' | 'dark'
/**
* 是否开启哀悼模式
* @默认值 `false`
*/
enableMournMode?: boolean
/**
* 是否开启色弱模式
* @默认值 `false`
*/
enableColorAmblyopiaMode?: boolean
/**
* 是否开启权限功能
* @默认值 `false`
@@ -76,7 +86,7 @@ declare namespace Settings {
* @可选值 `'head'` 顶部模式
* @可选值 `'single'` 侧边栏模式(无主导航)
*/
menuMode?: 'side' | 'head' | 'single'
mode?: 'side' | 'head' | 'single'
/**
* 切换主导航是否跳转页面
* @默认值 `false`
-7
View File
@@ -1,7 +0,0 @@
import type { InjectionKey } from 'vue'
export function createInjectionKey<T>(key: string): InjectionKey<T> {
return key as any
}
export const rootMenuInjectionKey = createInjectionKey<any>('rootMenu')
+3 -3
View File
@@ -51,7 +51,7 @@ const loginRules = ref<FormRules>({
],
})
function handleLogin() {
loginFormRef.value && loginFormRef.value.validate((valid) => {
loginFormRef.value?.validate((valid) => {
if (valid) {
loading.value = true
userStore.login(loginForm.value).then(() => {
@@ -108,7 +108,7 @@ function handleRegister() {
message: '注册模块仅提供界面演示,无实际功能,需开发者自行扩展',
type: 'warning',
})
registerFormRef.value && registerFormRef.value.validate((valid) => {
registerFormRef.value?.validate((valid) => {
if (valid) {
// 这里编写业务代码
}
@@ -139,7 +139,7 @@ function handleReset() {
message: '重置密码仅提供界面演示,无实际功能,需开发者自行扩展',
type: 'info',
})
resetFormRef.value && resetFormRef.value.validate((valid) => {
resetFormRef.value?.validate((valid) => {
if (valid) {
// 这里编写业务代码
}
+2 -2
View File
@@ -17,7 +17,7 @@ export default async ({ mode, command }) => {
}
})
return defineConfig({
// 开发服务器选项 https://cn.vitejs.dev/config/#server-options
// 开发服务器选项 https://cn.vitejs.dev/config/server-options
server: {
open: true,
port: 9000,
@@ -29,7 +29,7 @@ export default async ({ mode, command }) => {
},
},
},
// 构建选项 https://cn.vitejs.dev/config/#server-fsserve-root
// 构建选项 https://cn.vitejs.dev/config/build-options
build: {
outDir: mode === 'production' ? 'dist' : `dist-${mode}`,
sourcemap: env.VITE_BUILD_SOURCEMAP === 'true',
+202
View File
@@ -0,0 +1,202 @@
import path from 'node:path'
import process from 'node:process'
import fs from 'node:fs'
import dayjs from 'dayjs'
import type { PluginOption } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import vueLegacy from '@vitejs/plugin-legacy'
import VueDevTools from 'vite-plugin-vue-devtools'
import autoImport from 'unplugin-auto-import/vite'
import components from 'unplugin-vue-components/vite'
import Unocss from 'unocss/vite'
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
import { vitePluginFakeServer } from 'vite-plugin-fake-server'
import Layouts from 'vite-plugin-vue-meta-layouts'
import Pages from 'vite-plugin-pages'
import { compression } from 'vite-plugin-compression2'
import archiver from 'archiver'
import TurboConsole from 'unplugin-turbo-console/vite'
import banner from 'vite-plugin-banner'
import boxen from 'boxen'
import picocolors from 'picocolors'
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms))
}
export default function createVitePlugins(viteEnv, isBuild = false) {
const vitePlugins: (PluginOption | PluginOption[])[] = [
vue(),
vueJsx(),
vueLegacy({
renderLegacyChunks: false,
modernPolyfills: [
'es.array.at',
'es.array.find-last',
],
}),
// https://github.com/vuejs/devtools-next
viteEnv.VITE_OPEN_DEVTOOLS === 'true' && VueDevTools(),
// https://github.com/unplugin/unplugin-auto-import
autoImport({
imports: [
'vue',
'vue-router',
'pinia',
],
dts: './src/types/auto-imports.d.ts',
dirs: [
'./src/utils/composables/**',
],
}),
// https://github.com/unplugin/unplugin-vue-components
components({
dirs: [
'src/components',
'src/layouts/ui-kit',
],
include: [/\.vue$/, /\.vue\?vue/, /\.tsx$/],
dts: './src/types/components.d.ts',
}),
Unocss(),
// https://github.com/vbenjs/vite-plugin-svg-icons
createSvgIconsPlugin({
iconDirs: [path.resolve(process.cwd(), 'src/assets/icons/')],
symbolId: 'icon-[dir]-[name]',
svgoOptions: isBuild,
}),
// https://github.com/condorheroblog/vite-plugin-fake-server
vitePluginFakeServer({
logger: !isBuild,
include: 'src/mock',
infixName: false,
enableProd: isBuild && viteEnv.VITE_BUILD_MOCK === 'true',
}),
// https://github.com/dishait/vite-plugin-vue-meta-layouts
Layouts({
defaultLayout: 'index',
}),
// https://github.com/hannoeru/vite-plugin-pages
Pages({
dirs: 'src/views',
exclude: [
'**/components/**/*.vue',
],
}),
// https://github.com/nonzzz/vite-plugin-compression
isBuild && viteEnv.VITE_BUILD_COMPRESS.split(',').includes('gzip') && compression(),
isBuild && viteEnv.VITE_BUILD_COMPRESS.split(',').includes('brotli') && compression({
exclude: [/\.(br)$/, /\.(gz)$/],
algorithm: 'brotliCompress',
}),
(function () {
let outDir: string
return {
name: 'vite-plugin-archiver',
apply: 'build',
configResolved(resolvedConfig) {
outDir = resolvedConfig.build.outDir
},
async closeBundle() {
if (['zip', 'tar'].includes(viteEnv.VITE_BUILD_ARCHIVE)) {
await sleep(1000)
const archive = archiver(viteEnv.VITE_BUILD_ARCHIVE, {
...(viteEnv.VITE_BUILD_ARCHIVE === 'zip' && { zlib: { level: 9 } }),
...(viteEnv.VITE_BUILD_ARCHIVE === 'tar' && { gzip: true, gzipOptions: { level: 9 } }),
})
const output = fs.createWriteStream(`${outDir}.${dayjs().format('YYYY-MM-DD-HH-mm-ss')}.${viteEnv.VITE_BUILD_ARCHIVE === 'zip' ? 'zip' : 'tar.gz'}`)
archive.pipe(output)
archive.directory(outDir, false)
archive.finalize()
}
},
}
})(),
// https://github.com/unplugin/unplugin-turbo-console
TurboConsole(),
// https://github.com/chengpeiquan/vite-plugin-banner
banner(`
/**
* 由 Fantastic-admin 提供技术支持
* Powered by Fantastic-admin
* https://fantastic-admin.github.io
*/
`),
{
name: 'vite-plugin-debug-plugin',
transform: (code, id) => {
if (/src\/main.ts$/.test(id)) {
if (viteEnv.VITE_APP_DEBUG_TOOL === 'eruda') {
code = code.concat(`
import eruda from 'eruda'
eruda.init()
`)
}
else if (viteEnv.VITE_APP_DEBUG_TOOL === 'vconsole') {
code = code.concat(`
import VConsole from 'vconsole'
new VConsole()
`)
}
return {
code,
map: null,
}
}
},
},
{
name: 'vite-plugin-disable-devtool',
transform: (code, id) => {
if (/src\/main.ts$/.test(id)) {
if (viteEnv.VITE_APP_DISABLE_DEVTOOL === 'true') {
code = code.concat(`
import DisableDevtool from 'disable-devtool'
DisableDevtool()
`)
}
return {
code,
map: null,
}
}
},
},
{
name: 'vite-plugin-terminal-info',
apply: 'serve',
async buildStart() {
const { bold, green, cyan, bgGreen, underline } = picocolors
// eslint-disable-next-line no-console
console.log(
boxen(
`${bold(green(`由 ${bgGreen('Fantastic-admin')} 驱动`))}\n\n${underline('https://fantastic-admin.github.io')}\n\n当前使用:${cyan('基础版')}`,
{
padding: 1,
margin: 1,
borderStyle: 'double',
textAlignment: 'center',
},
),
)
},
},
]
return vitePlugins
}
-25
View File
@@ -1,25 +0,0 @@
import boxen from 'boxen'
import picocolors from 'picocolors'
import type { Plugin } from 'vite'
export default function appInfo(): Plugin {
return {
name: 'appInfo',
apply: 'serve',
async buildStart() {
const { bold, green, cyan, bgGreen, underline } = picocolors
// eslint-disable-next-line no-console
console.log(
boxen(
`${bold(green(`由 ${bgGreen('Fantastic-admin')} 驱动`))}\n\n${underline('https://fantastic-admin.github.io')}\n\n当前使用:${cyan('基础版')}`,
{
padding: 1,
margin: 1,
borderStyle: 'double',
textAlignment: 'center',
},
),
)
},
}
}
-33
View File
@@ -1,33 +0,0 @@
import fs from 'node:fs'
import dayjs from 'dayjs'
import archiver from 'archiver'
import type { Plugin } from 'vite'
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms))
}
export default function createArchiver(env): Plugin {
const { VITE_BUILD_ARCHIVE } = env
let outDir: string
return {
name: 'vite-plugin-archiver',
apply: 'build',
configResolved(resolvedConfig) {
outDir = resolvedConfig.build.outDir
},
async closeBundle() {
if (['zip', 'tar'].includes(VITE_BUILD_ARCHIVE)) {
await sleep(1000)
const archive = archiver(VITE_BUILD_ARCHIVE, {
...(VITE_BUILD_ARCHIVE === 'zip' && { zlib: { level: 9 } }),
...(VITE_BUILD_ARCHIVE === 'tar' && { gzip: true, gzipOptions: { level: 9 } }),
})
const output = fs.createWriteStream(`${outDir}.${dayjs().format('YYYY-MM-DD-HH-mm-ss')}.${VITE_BUILD_ARCHIVE === 'zip' ? 'zip' : 'tar.gz'}`)
archive.pipe(output)
archive.directory(outDir, false)
archive.finalize()
}
},
}
}
-15
View File
@@ -1,15 +0,0 @@
import autoImport from 'unplugin-auto-import/vite'
export default function createAutoImport() {
return autoImport({
imports: [
'vue',
'vue-router',
'pinia',
],
dts: './src/types/auto-imports.d.ts',
dirs: [
'./src/utils/composables/**',
],
})
}
-11
View File
@@ -1,11 +0,0 @@
import banner from 'vite-plugin-banner'
export default function createBanner() {
return banner(`
/**
* 由 Fantastic-admin 提供技术支持
* Powered by Fantastic-admin
* https://fantastic-admin.github.io
*/
`)
}
-12
View File
@@ -1,12 +0,0 @@
import components from 'unplugin-vue-components/vite'
export default function createComponents() {
return components({
dirs: [
'src/components',
'src/layouts/ui-kit',
],
include: [/\.vue$/, /\.vue\?vue/, /\.tsx$/],
dts: './src/types/components.d.ts',
})
}
-24
View File
@@ -1,24 +0,0 @@
import { compression } from 'vite-plugin-compression2'
import type { PluginOption } from 'vite'
export default function createCompression(env, isBuild) {
const plugin: (PluginOption | PluginOption[])[] = []
if (isBuild) {
const { VITE_BUILD_COMPRESS } = env
const compressList = VITE_BUILD_COMPRESS.split(',')
if (compressList.includes('gzip')) {
plugin.push(
compression(),
)
}
if (compressList.includes('brotli')) {
plugin.push(
compression({
exclude: [/\.(br)$/, /\.(gz)$/],
algorithm: 'brotliCompress',
}),
)
}
}
return plugin
}
-5
View File
@@ -1,5 +0,0 @@
import TurboConsole from 'unplugin-turbo-console/vite'
export default function createConsole() {
return TurboConsole()
}
-6
View File
@@ -1,6 +0,0 @@
import VueDevTools from 'vite-plugin-vue-devtools'
export default function createDevtools(env) {
const { VITE_OPEN_DEVTOOLS } = env
return VITE_OPEN_DEVTOOLS === 'true' && VueDevTools()
}
-46
View File
@@ -1,46 +0,0 @@
import type { PluginOption } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import vueLegacy from '@vitejs/plugin-legacy'
import appInfo from './app-info'
import createDevtools from './devtools'
import createAutoImport from './auto-import'
import createComponents from './components'
import createUnocss from './unocss'
import createSvgIcon from './svg-icon'
import createMock from './mock'
import createLayouts from './layouts'
import createPages from './pages'
import createCompression from './compression'
import createArchiver from './archiver'
import createConsole from './console'
import createBanner from './banner'
export default function createVitePlugins(viteEnv, isBuild = false) {
const vitePlugins: (PluginOption | PluginOption[])[] = [
appInfo(),
vue(),
vueJsx(),
vueLegacy({
renderLegacyChunks: false,
modernPolyfills: [
'es.array.at',
'es.array.find-last',
],
}),
]
vitePlugins.push(createDevtools(viteEnv))
vitePlugins.push(createAutoImport())
vitePlugins.push(createComponents())
vitePlugins.push(createUnocss())
vitePlugins.push(createSvgIcon(isBuild))
vitePlugins.push(createMock(viteEnv, isBuild))
vitePlugins.push(createLayouts())
vitePlugins.push(createPages())
vitePlugins.push(...createCompression(viteEnv, isBuild))
vitePlugins.push(createArchiver(viteEnv))
vitePlugins.push(createConsole())
vitePlugins.push(createBanner())
return vitePlugins
}
-7
View File
@@ -1,7 +0,0 @@
import Layouts from 'vite-plugin-vue-meta-layouts'
export default function createLayouts() {
return Layouts({
defaultLayout: 'index',
})
}
-11
View File
@@ -1,11 +0,0 @@
import { vitePluginFakeServer } from 'vite-plugin-fake-server'
export default function createMock(env, isBuild) {
const { VITE_BUILD_MOCK } = env
return vitePluginFakeServer({
logger: !isBuild,
include: 'src/mock',
infixName: false,
enableProd: isBuild && VITE_BUILD_MOCK === 'true',
})
}
-10
View File
@@ -1,10 +0,0 @@
import Pages from 'vite-plugin-pages'
export default function createPages() {
return Pages({
dirs: 'src/views',
exclude: [
'**/components/**/*.vue',
],
})
}
-11
View File
@@ -1,11 +0,0 @@
import path from 'node:path'
import process from 'node:process'
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
export default function createSvgIcon(isBuild) {
return createSvgIconsPlugin({
iconDirs: [path.resolve(process.cwd(), 'src/assets/icons/')],
symbolId: 'icon-[dir]-[name]',
svgoOptions: isBuild,
})
}
-5
View File
@@ -1,5 +0,0 @@
import Unocss from 'unocss/vite'
export default function createUnocss() {
return Unocss()
}