refactor(projects): all file and folder use kebab-case

This commit is contained in:
Soybean
2023-02-23 08:22:44 +08:00
parent bf2f617255
commit cea600f12c
138 changed files with 130 additions and 155 deletions

View File

@@ -0,0 +1,18 @@
<template>
<hover-container class="w-40px h-full" tooltip-content="全屏" :inverted="theme.header.inverted" @click="toggle">
<icon-gridicons-fullscreen-exit v-if="isFullscreen" class="text-18px" />
<icon-gridicons-fullscreen v-else class="text-18px" />
</hover-container>
</template>
<script lang="ts" setup>
import { useFullscreen } from '@vueuse/core';
import { useThemeStore } from '@/store';
defineOptions({ name: 'FullScreen' });
const { isFullscreen, toggle } = useFullscreen();
const theme = useThemeStore();
</script>
<style scoped></style>

View File

@@ -0,0 +1,23 @@
<template>
<hover-container
tooltip-content="github"
class="w-40px h-full"
:inverted="theme.header.inverted"
@click="handleClickLink"
>
<icon-mdi-github class="text-20px" />
</hover-container>
</template>
<script lang="ts" setup>
import { useThemeStore } from '@/store';
defineOptions({ name: 'GithubSite' });
const theme = useThemeStore();
function handleClickLink() {
window.open('https://github.com/honghuangdc/soybean-admin', '_blank');
}
</script>
<style scoped></style>

View File

@@ -0,0 +1,53 @@
<template>
<n-breadcrumb class="px-12px">
<template v-for="breadcrumb in breadcrumbs" :key="breadcrumb.key">
<n-breadcrumb-item>
<n-dropdown v-if="breadcrumb.hasChildren" :options="breadcrumb.children" @select="dropdownSelect">
<span>
<component
:is="breadcrumb.icon"
v-if="theme.header.crumb.showIcon"
class="inline-block align-text-bottom mr-4px text-16px"
/>
<span>{{ breadcrumb.label }}</span>
</span>
</n-dropdown>
<template v-else>
<component
:is="breadcrumb.icon"
v-if="theme.header.crumb.showIcon"
class="inline-block align-text-bottom mr-4px text-16px"
:class="{ 'text-#BBBBBB': theme.header.inverted }"
/>
<span :class="{ 'text-#BBBBBB': theme.header.inverted }">{{ breadcrumb.label }}</span>
</template>
</n-breadcrumb-item>
</template>
</n-breadcrumb>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { useRoute } from 'vue-router';
import { routePath } from '@/router';
import { useRouteStore, useThemeStore } from '@/store';
import { useRouterPush } from '@/composables';
import { getBreadcrumbByRouteKey } from '@/utils';
defineOptions({ name: 'GlobalBreadcrumb' });
const route = useRoute();
const theme = useThemeStore();
const routeStore = useRouteStore();
const { routerPush } = useRouterPush();
const breadcrumbs = computed(() =>
getBreadcrumbByRouteKey(route.name as string, routeStore.menus as App.GlobalMenuOption[], routePath('root'))
);
function dropdownSelect(key: string) {
routerPush({ name: key });
}
</script>
<style scoped></style>

View File

@@ -0,0 +1,44 @@
<template>
<div class="flex-1-hidden h-full px-10px">
<n-scrollbar :x-scrollable="true" class="flex-1-hidden h-full" content-class="h-full">
<div class="flex-y-center h-full" :style="{ justifyContent: theme.menu.horizontalPosition }">
<n-menu
:value="activeKey"
mode="horizontal"
:options="menus"
:inverted="theme.header.inverted"
@update:value="handleUpdateMenu"
/>
</div>
</n-scrollbar>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { useRoute } from 'vue-router';
import type { MenuOption } from 'naive-ui';
import { useRouteStore, useThemeStore } from '@/store';
import { useRouterPush } from '@/composables';
defineOptions({ name: 'HeaderMenu' });
const route = useRoute();
const routeStore = useRouteStore();
const theme = useThemeStore();
const { routerPush } = useRouterPush();
const menus = computed(() => routeStore.menus as App.GlobalMenuOption[]);
const activeKey = computed(() => (route.meta?.activeMenu ? route.meta.activeMenu : route.name) as string);
function handleUpdateMenu(_key: string, item: MenuOption) {
const menuItem = item as App.GlobalMenuOption;
routerPush(menuItem.routePath);
}
</script>
<style scoped>
:deep(.n-menu-item-content-header) {
overflow: inherit !important;
}
</style>

View File

@@ -0,0 +1,21 @@
import MenuCollapse from './menu-collapse.vue';
import GlobalBreadcrumb from './global-breadcrumb.vue';
import HeaderMenu from './header-menu.vue';
import GithubSite from './github-site.vue';
import FullScreen from './full-screen.vue';
import ThemeMode from './theme-mode.vue';
import UserAvatar from './user-avatar.vue';
import SystemMessage from './system-message.vue';
import SettingButton from './setting-button.vue';
export {
MenuCollapse,
GlobalBreadcrumb,
HeaderMenu,
GithubSite,
FullScreen,
ThemeMode,
UserAvatar,
SystemMessage,
SettingButton
};

View File

@@ -0,0 +1,17 @@
<template>
<hover-container class="w-40px h-full" :inverted="theme.header.inverted" @click="app.toggleSiderCollapse">
<icon-line-md-menu-unfold-left v-if="app.siderCollapse" class="text-16px" />
<icon-line-md-menu-fold-left v-else class="text-16px" />
</hover-container>
</template>
<script lang="ts" setup>
import { useAppStore, useThemeStore } from '@/store';
defineOptions({ name: 'MenuCollapse' });
const app = useAppStore();
const theme = useThemeStore();
</script>
<style scoped></style>

View File

@@ -0,0 +1,57 @@
<template>
<n-scrollbar class="max-h-360px">
<n-list>
<n-list-item
v-for="(item, index) in list"
:key="item.id"
class="hover:bg-[#f6f6f6] dark:hover:bg-dark cursor-pointer"
@click="handleRead(index)"
>
<n-thing class="px-15px" :class="{ 'opacity-30': item.isRead }">
<template #avatar>
<n-avatar v-if="item.avatar" :src="item.avatar" />
<svg-icon v-else class="text-34px text-primary" :icon="item.icon" :local-icon="item.svgIcon" />
</template>
<template #header>
<n-ellipsis :line-clamp="1">
{{ item.title }}
<template #tooltip>
{{ item.title }}
</template>
</n-ellipsis>
</template>
<template v-if="item.tagTitle" #header-extra>
<n-tag v-bind="item.tagProps" size="small">{{ item.tagTitle }}</n-tag>
</template>
<template #description>
<n-ellipsis v-if="item.description" :line-clamp="2">
{{ item.description }}
</n-ellipsis>
<p>{{ item.date }}</p>
</template>
</n-thing>
</n-list-item>
</n-list>
</n-scrollbar>
</template>
<script lang="ts" setup>
defineOptions({ name: 'MessageList' });
interface Props {
list?: App.MessageList[];
}
withDefaults(defineProps<Props>(), {
list: () => []
});
interface Emits {
(e: 'read', val: number): void;
}
const emit = defineEmits<Emits>();
function handleRead(index: number) {
emit('read', index);
}
</script>

View File

@@ -0,0 +1,21 @@
<template>
<hover-container
class="w-40px h-full"
tooltip-content="主题配置"
:inverted="theme.header.inverted"
@click="app.toggleSettingDrawerVisible"
>
<icon-ant-design-setting-outlined class="text-20px" />
</hover-container>
</template>
<script setup lang="ts">
import { useAppStore, useThemeStore } from '@/store';
defineOptions({ name: 'SettingButton' });
const app = useAppStore();
const theme = useThemeStore();
</script>
<style scoped></style>

View File

@@ -0,0 +1,217 @@
<template>
<n-popover class="!p-0" trigger="click" placement="bottom">
<template #trigger>
<hover-container tooltip-content="消息通知" :inverted="theme.header.inverted" class="relative w-40px h-full">
<icon-clarity:notification-line class="text-18px" />
<n-badge
:value="count"
:max="99"
:class="[count < 10 ? '-right-2px' : '-right-10px']"
class="absolute top-10px"
/>
</hover-container>
</template>
<n-tabs
v-model:value="currentTab"
:class="[isMobile ? 'w-276px' : 'w-360px']"
type="line"
justify-content="space-evenly"
>
<n-tab-pane v-for="(item, index) in tabData" :key="item.key" :name="index">
<template #tab>
<div class="flex-x-center items-center" :class="[isMobile ? 'w-92px' : 'w-120px']">
<span class="mr-5px">{{ item.name }}</span>
<n-badge
v-bind="item.badgeProps"
:value="item.list.filter(message => !message.isRead).length"
:max="99"
show-zero
/>
</div>
</template>
<loading-empty-wrapper
class="h-360px"
:loading="loading"
:empty="item.list.length === 0"
placeholder-class="bg-$n-color transition-background-color duration-300 ease-in-out"
>
<message-list :list="item.list" @read="handleRead" />
</loading-empty-wrapper>
</n-tab-pane>
</n-tabs>
<div v-if="showAction" class="flex border-t border-$n-divider-color cursor-pointer">
<div class="flex-1 text-center py-10px" @click="handleClear">清空</div>
<div class="flex-1 text-center py-10px border-l border-$n-divider-color" @click="handleAllRead">全部已读</div>
<div class="flex-1 text-center py-10px border-l border-$n-divider-color" @click="handleLoadMore">查看更多</div>
</div>
</n-popover>
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue';
import { useThemeStore } from '@/store';
import { useBasicLayout } from '@/composables';
import { useBoolean } from '@/hooks';
import MessageList from './message-list.vue';
defineOptions({ name: 'SystemMessage' });
const theme = useThemeStore();
const { isMobile } = useBasicLayout();
const { bool: loading, setBool: setLoading } = useBoolean();
const currentTab = ref(0);
const tabData = ref<App.MessageTab[]>([
{
key: 1,
name: '通知',
badgeProps: { type: 'warning' },
list: [
{ id: 1, icon: 'ri:message-3-line', title: '你收到了5条新消息', date: '2022-06-17' },
{ id: 4, icon: 'ri:message-3-line', title: 'Soybean Admin 1.0.0 版本正在筹备中', date: '2022-06-17' },
{ id: 2, icon: 'ri:message-3-line', title: 'Soybean Admin 0.9.6 版本发布了', date: '2022-06-16' },
{ id: 3, icon: 'ri:message-3-line', title: 'Soybean Admin 0.9.5 版本发布了', date: '2022-06-07' },
{
id: 5,
icon: 'ri:message-3-line',
title: '测试超长标题测试超长标题测试超长标题测试超长标题测试超长标题测试超长标题测试超长标题测试超长标题',
date: '2022-06-17'
}
]
},
{
key: 2,
name: '消息',
badgeProps: { type: 'error' },
list: [
{
id: 1,
title: '项目动态',
svgIcon: 'avatar',
description: 'Soybean 刚才把工作台页面随便写了一些,凑合能看了!',
date: '2021-11-07 22:45:32'
},
{
id: 2,
title: '项目动态',
svgIcon: 'avatar',
description: 'Soybean 正在忙于为soybean-admin写项目说明文档',
date: '2021-11-03 20:33:31'
},
{
id: 3,
title: '项目动态',
svgIcon: 'avatar',
description: 'Soybean 准备为soybean-admin 1.0的发布做充分的准备工作!',
date: '2021-10-31 22:43:12'
},
{
id: 4,
title: '项目动态',
svgIcon: 'avatar',
description: '@yanbowe 向soybean-admin提交了一个bug多标签栏不会自适应。',
date: '2021-10-27 10:24:54'
},
{
id: 5,
title: '项目动态',
svgIcon: 'avatar',
description: 'Soybean 在2021年5月28日创建了开源项目soybean-admin',
date: '2021-05-28 22:22:22'
}
]
},
{
key: 3,
name: '待办',
badgeProps: { type: 'info' },
list: [
{
id: 1,
icon: 'ri:calendar-todo-line',
title: '缓存主题配置',
description: '任务正在计划中',
date: '2022-06-17',
tagTitle: '未开始',
tagProps: { type: 'default' }
},
{
id: 2,
icon: 'ri:calendar-todo-line',
title: '添加锁屏组件、全局Iframe组件',
description: '任务正在计划中',
date: '2022-06-17',
tagTitle: '未开始',
tagProps: { type: 'default' }
},
{
id: 3,
icon: 'ri:calendar-todo-line',
title: '示例页面完善',
description: '任务正在计划中',
date: '2022-06-17',
tagTitle: '未开始',
tagProps: { type: 'default' }
},
{
id: 4,
icon: 'ri:calendar-todo-line',
title: '表单、表格示例',
description: '任务正在计划中',
date: '2022-06-17',
tagTitle: '未开始',
tagProps: { type: 'default' }
},
{
id: 5,
icon: 'ri:calendar-todo-line',
title: '性能优化(优化递归函数)',
description: '任务正在计划中',
date: '2022-06-17',
tagTitle: '未开始',
tagProps: { type: 'default' }
},
{
id: 6,
icon: 'ri:calendar-todo-line',
title: '精简版(新分支thin)',
description: '任务正在计划中',
date: '2022-06-17',
tagTitle: '未开始',
tagProps: { type: 'default' }
}
]
}
]);
const count = computed(() => {
return tabData.value.reduce((acc, cur) => {
return acc + cur.list.filter(item => !item.isRead).length;
}, 0);
});
const showAction = computed(() => tabData.value[currentTab.value].list.length > 0);
function handleRead(index: number) {
tabData.value[currentTab.value].list[index].isRead = true;
}
function handleAllRead() {
tabData.value[currentTab.value].list.forEach(item => Object.assign(item, { isRead: true }));
}
function handleClear() {
tabData.value[currentTab.value].list = [];
}
function handleLoadMore() {
const { list } = tabData.value[currentTab.value];
setLoading(true);
setTimeout(() => {
list.push(...tabData.value[currentTab.value].list);
setLoading(false);
}, 1000);
}
</script>
<style scoped></style>

View File

@@ -0,0 +1,15 @@
<template>
<hover-container class="w-40px" :inverted="theme.header.inverted" tooltip-content="主题模式">
<dark-mode-switch :dark="theme.darkMode" class="wh-full" @update:dark="theme.setDarkMode" />
</hover-container>
</template>
<script lang="ts" setup>
import { useThemeStore } from '@/store';
defineOptions({ name: 'ThemeMode' });
const theme = useThemeStore();
</script>
<style scoped></style>

View File

@@ -0,0 +1,56 @@
<template>
<n-dropdown :options="options" @select="handleDropdown">
<hover-container class="px-12px" :inverted="theme.header.inverted">
<icon-local-avatar class="text-32px" />
<span class="pl-8px text-16px font-medium">{{ auth.userInfo.userName }}</span>
</hover-container>
</n-dropdown>
</template>
<script lang="ts" setup>
import type { DropdownOption } from 'naive-ui';
import { useAuthStore, useThemeStore } from '@/store';
import { useIconRender } from '@/composables';
defineOptions({ name: 'UserAvatar' });
const auth = useAuthStore();
const theme = useThemeStore();
const { iconRender } = useIconRender();
const options: DropdownOption[] = [
{
label: '用户中心',
key: 'user-center',
icon: iconRender({ icon: 'carbon:user-avatar' })
},
{
type: 'divider',
key: 'divider'
},
{
label: '退出登录',
key: 'logout',
icon: iconRender({ icon: 'carbon:logout' })
}
];
type DropdownKey = 'user-center' | 'logout';
function handleDropdown(optionKey: string) {
const key = optionKey as DropdownKey;
if (key === 'logout') {
window.$dialog?.info({
title: '提示',
content: '您确定要退出登录吗?',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
auth.resetAuthStore();
}
});
}
}
</script>
<style scoped></style>

View File

@@ -0,0 +1,61 @@
<template>
<dark-mode-container class="global-header flex-y-center h-full" :inverted="theme.header.inverted">
<global-logo v-if="showLogo" :show-title="true" class="h-full" :style="{ width: theme.sider.width + 'px' }" />
<div v-if="!showHeaderMenu" class="flex-1-hidden flex-y-center h-full">
<menu-collapse v-if="showMenuCollapse || isMobile" />
<global-breadcrumb v-if="theme.header.crumb.visible && !isMobile" />
</div>
<header-menu v-else />
<div class="flex justify-end h-full">
<global-search />
<github-site />
<full-screen />
<theme-mode />
<system-message />
<setting-button v-if="showButton" />
<user-avatar />
</div>
</dark-mode-container>
</template>
<script setup lang="ts">
import { useThemeStore } from '@/store';
import { useBasicLayout } from '@/composables';
import GlobalLogo from '../global-logo/index.vue';
import GlobalSearch from '../global-search/index.vue';
import {
FullScreen,
GithubSite,
GlobalBreadcrumb,
HeaderMenu,
MenuCollapse,
SettingButton,
SystemMessage,
ThemeMode,
UserAvatar
} from './components';
defineOptions({ name: 'GlobalHeader' });
interface Props {
/** 显示logo */
showLogo: App.GlobalHeaderProps['showLogo'];
/** 显示头部菜单 */
showHeaderMenu: App.GlobalHeaderProps['showHeaderMenu'];
/** 显示菜单折叠按钮 */
showMenuCollapse: App.GlobalHeaderProps['showMenuCollapse'];
}
defineProps<Props>();
const theme = useThemeStore();
const { isMobile } = useBasicLayout();
const showButton = import.meta.env.PROD && import.meta.env.VITE_VERCEL !== 'Y';
</script>
<style scoped>
.global-header {
box-shadow: 0 1px 2px rgb(0 21 41 / 8%);
}
</style>