feat: 添加路由loading

master
luoer 2023-09-11 17:49:38 +08:00
parent 22b57e9fa7
commit 3f72b304f5
25 changed files with 226 additions and 159 deletions

2
.env
View File

@ -2,7 +2,7 @@
# 应用配置
# =====================================================================================
# 网站标题
VITE_TITLE = 绝弹管理后台
VITE_TITLE = Appnify
# 网站副标题
VITE_SUBTITLE = 快速开发web应用的模板工具
# API接口前缀参见 axios 的 baseURL

View File

@ -30,13 +30,4 @@
const router = useRouter();
</script>
<style scoped></style>
<route lang="json">
{
"meta": {
"title": "404",
"icon": "icon-park-outline-home"
}
}
</route>
<style scoped></style>

View File

@ -132,7 +132,7 @@ export const FormModal = defineComponent({
content = (
<Button type="primary">
{{
default: () => (typeof props.trigger === "string" ? props.trigger : "添加"),
default: () => (typeof props.trigger === "string" ? props.trigger : "新增"),
icon: () => <i class="icon-park-outline-add" />,
}}
</Button>
@ -144,7 +144,7 @@ export const FormModal = defineComponent({
if (typeof props.trigger === "object") {
content = (
<Button type="primary" {...omit(props.trigger, "text")}>
{props.trigger?.text || "添加"}
{props.trigger?.text || "新增"}
</Button>
);
}

View File

@ -1,5 +1,5 @@
import { Modal } from "@arco-design/web-vue";
import { assign } from "lodash-es";
import { assign, merge } from "lodash-es";
import { reactive } from "vue";
import { useForm } from "./use-form";
import { FormModalProps } from "./form-modal";
@ -7,6 +7,7 @@ import { FormModalProps } from "./form-modal";
const defaults: Partial<InstanceType<typeof Modal>> = {
width: 1080,
titleAlign: "start",
closable: false
};
/**
@ -18,5 +19,5 @@ export const useFormModal = (options: FormModalProps): FormModalProps & { model:
const form = useForm({ model, items });
return reactive(assign({ modalProps: { ...defaults } }, { ...options, ...form }));
return reactive(merge({ modalProps: { ...defaults } }, { ...options, ...form }));
};

View File

@ -167,11 +167,11 @@ export const useTable = (optionsOrFn: UseTableOptions | (() => UseTableOptions))
if (item.extend) {
const createItem = createItems.find((i) => i.field === item.extend);
if (createItem) {
searchItems.push(merge({ itemProps: { hideLabel: true } }, createItem, item));
searchItems.push(merge({}, createItem, item));
continue;
}
}
searchItems.push(merge({ itemProps: { hideLabel: true } }, item));
searchItems.push(merge({}, item));
}
searchItems.push(config.searchItemSubmit);
options.search.items = searchItems;

View File

@ -1,2 +1 @@
export * from "./gender";
export * from "./defineConstants";

View File

@ -47,7 +47,7 @@ export default defineComponent({
render() {
return (
<a-menu
style={{ width: "100%", height: "100%" }}
style={{ width: "100%" }}
breakpoint="xl"
selectedKeys={this.selectedKeys}
autoOpenSelected={true}

View File

@ -8,11 +8,26 @@
<img src="/favicon.ico" alt="" width="22" height="22" class="" />
<h1 class="relative text-lg font-semibold leading-[19px] dark:text-white m-0 p-0">
{{ appStore.title }}
<span v-if="isDev" class="absolute -right-14 -top-1 text-xs font-normal text-blue-500 bg-blue-50 px-2 rounded-full"></span>
<span
v-if="isDev"
class="absolute -right-14 -top-1 text-xs font-normal text-brand-500 bg-brand-50 px-1.5 rounded-full"
>
开发版
</span>
</h1>
</router-link>
</div>
<div class="flex items-center gap-4">
<a-dropdown v-if="isDev" trigger="hover">
<a-button shape="round">
<template #icon>
<i class="icon-park-outline-api"></i>
</template>
</a-button>
<template #content>
<a-doption>接口文档</a-doption>
</template>
</a-dropdown>
<a-tooltip v-for="btn in buttons" :key="btn.icon" :content="btn.tooltip">
<a-button shape="round" @click="btn.onClick">
<template #icon>
@ -53,9 +68,9 @@
:hide-trigger="false"
@collapse="onCollapse"
>
<div class="">
<a-scrollbar outer-class="h-full overflow-hidden" class="h-full overflow-hidden pt-2">
<Menu />
</div>
</a-scrollbar>
</a-layout-sider>
<a-layout class="layout-content flex-1">
<a-layout-header class="h-8 bg-white border-b border-slate-200 dark:bg-slate-800 dark:border-slate-700">
@ -64,9 +79,14 @@
</div>
</a-layout-header>
<a-layout-content class="overflow-x-auto">
<router-view v-slot="{ Component }">
<component :is="Component"></component>
</router-view>
<a-spin :loading="appStore.pageLoding" tip="正在加载中,请稍等..." class="block h-full w-full">
<template #icon>
<IconSync></IconSync>
</template>
<router-view v-slot="{ Component }">
<component :is="Component"></component>
</router-view>
</a-spin>
</a-layout-content>
</a-layout>
</a-layout>
@ -77,6 +97,7 @@
import { useAppStore, useUserStore } from "@/store";
import { Message } from "@arco-design/web-vue";
import Menu from "./components/menu.vue";
import { IconSync } from "@arco-design/web-vue/es/icon";
const appStore = useAppStore();
const userStore = useUserStore();
@ -84,23 +105,30 @@ const isCollapsed = ref(false);
const route = useRoute();
const router = useRouter();
const themeConfig = ref({ visible: false });
const isDev = import.meta.env.DEV
const isDev = import.meta.env.DEV;
const onCollapse = (val: boolean) => {
isCollapsed.value = val;
};
const buttons = [
{
icon: "icon-park-outline-remind",
tooltip: "通知",
onClick: () => {
Message.info("暂无通知");
},
},
{
icon: "icon-park-outline-moon",
tooltip: "点击切换主题色",
tooltip: "切换主题色",
onClick: () => {
appStore.toggleDark();
},
},
{
icon: "icon-park-outline-config",
tooltip: "点击打开设置",
tooltip: "打开设置",
onClick: () => {
themeConfig.value.visible = true;
},
@ -112,7 +140,7 @@ const userButtons = [
icon: "icon-park-outline-config",
text: "个人设置",
onClick: () => {
router.push('/my')
router.push("/my");
},
},
{

View File

@ -56,7 +56,7 @@
<script lang="ts" setup>
import { api } from "@/api";
import { dayjs } from "@/libs";
import { dayjs } from "@/libs/dayjs";
import { useAppStore, useUserStore } from "@/store";
import { FieldRule, Form, Message, Modal } from "@arco-design/web-vue";
import { reactive } from "vue";

View File

@ -1,7 +1,21 @@
<template>
<bread-page class="">
<a-card title="菜单权限">
<a-tree :data="items" :field-names="{ title: 'title' }" checkable></a-tree>
<template #title>
菜单权限
<a-link>展开</a-link>
</template>
<template #extra>
<a-checkbox>全部选择</a-checkbox>
</template>
<a-tree :data="items" :block-node="true" :field-names="{ title: 'title' }" checkable :default-expand-all="true">
<template #extra="nodeData">
<div class="flex-1 flex justify-end px-1">
<a-tag v-if="nodeData.children" color="orange"></a-tag>
<a-tag v-else color="green">页面</a-tag>
</div>
</template>
</a-tree>
</a-card>
</bread-page>
</template>
@ -52,7 +66,13 @@ const onItemChange = (item: any, menu: any) => {
};
</script>
<style scoped></style>
<style lang="less">
.arco-tree-node {
&:hover {
background: rgb(var(--primary-1));
}
}
</style>
<route lang="json">
{

View File

@ -3,10 +3,8 @@
<template #default>
<div class="flex justify-between items-end gap-4">
<div class="">
<span class="text-base font-semibold text-gray-900">媒体素材</span>
<div class="mt-1 text-gray-400">
用户上传的图片视频音频等素材可用于文章图文视频等内容的编辑
</div>
<span class="text-lg font-bold text-gray-900">媒体素材</span>
<div class="mt-1 text-gray-400">用户上传的图片视频音频等素材可用于文章图文视频等内容的编辑</div>
</div>
<div class="text-sm text-gray-400">
<a-button type="primary">
@ -21,16 +19,14 @@
<template #header>
<div class="flex gap-2 items-center justify-between text-sm bg-[#fbfbfc] px-5 py-2">
<div class="flex gap-4 my-1.5">
<ACheckbox>
全选
</ACheckbox>
<ACheckbox> 全选 </ACheckbox>
</div>
<div class="flex items-center gap-4 text-gray-500">
<div class="flex items-center text-gray-500">
<ADropdown>
<span class="cursor-pointer hover:text-gray-900">
<a-button type="text">
上传者
<i class="icon-park-outline-down"></i>
</span>
<i class="icon-park-outline-down ml-1"></i>
</a-button>
<template #content>
<ADoption class="!hover:bg-transparent !px-0 flex">
<div class="border-b border-gray-200 w-full pb-1">
@ -48,10 +44,10 @@
</template>
</ADropdown>
<ADropdown>
<span class="cursor-pointer hover:text-gray-900">
<a-button type="text">
排序默认
<i class="icon-park-outline-down"></i>
</span>
<i class="icon-park-outline-down ml-1"></i>
</a-button>
<template #content>
<ADoption>
<template #icon>
@ -60,37 +56,30 @@
<div class="w-48">默认</div>
</ADoption>
<ADoption>
<template #icon>
</template>
<template #icon> </template>
按创建时间升序
</ADoption>
<ADoption>
按创建时间降序
</ADoption>
<ADoption>
按文件大小升序
</ADoption>
<ADoption>
按文件大小降序
</ADoption>
<ADoption> 按创建时间降序 </ADoption>
<ADoption> 按文件大小升序 </ADoption>
<ADoption> 按文件大小降序 </ADoption>
</template>
</ADropdown>
<div class="space-x-1">
<span
class="inline-flex p-1 hover:bg-slate-200 rounded cursor-pointer text-gray-400 hover:text-gray-700 bg-slate-200 text-slate-700"
>
<i class="icon-park-outline-list"></i>
</span>
<span
class="inline-flex p-1 hover:bg-slate-200 rounded cursor-pointer text-gray-400 hover:text-gray-700"
>
<i class="icon-park-outline-insert-table"></i>
</span>
<span
class="inline-flex p-1 hover:bg-slate-200 rounded cursor-pointer text-gray-400 hover:text-gray-700"
>
<i class="icon-park-outline-refresh"></i>
</span>
<a-button type="text">
<template #icon>
<i class="icon-park-outline-list"></i>
</template>
</a-button>
<a-button type="text">
<template #icon>
<i class="icon-park-outline-insert-table"></i>
</template>
</a-button>
<a-button type="text">
<template #icon>
<i class="icon-park-outline-refresh"></i>
</template>
</a-button>
</div>
</div>
</div>
@ -99,7 +88,12 @@
<AListItemMeta title="测试图片.png" description="image/png 1.2MB">
<template #avatar>
<ACheckbox class="mr-3"></ACheckbox>
<AImage :src="`https://picsum.photos/200/300?${Math.random()}`" height="32" width="48" class="bg-slate-50">
<AImage
:src="`https://picsum.photos/200/300?${Math.random()}`"
height="32"
width="48"
class="bg-slate-50"
>
</AImage>
</template>
<template #title>
@ -110,30 +104,34 @@
</template>
</AListItemMeta>
<template #actions>
<span class="text-xs text-gray-400">
<i class="icon-park-outline-user !w-[14px] !h-[14px]"></i>
绝弹
</span>
<span class="text-xs text-gray-400">2023-08-17 17:00:01</span>
<ADropdown @select="onRowActionsSelect" position="br">
<span class="inline-flex p-1 hover:bg-slate-100 text-brand-500 rounded cursor-pointer">
<i class="icon-park-outline-more"></i>
<div class="flex items-center gap-6">
<span class="text-xs text-gray-400">
<i class="icon-park-outline-user !w-[14px] !h-[14px]"></i>
绝弹
</span>
<template #content>
<ADoption value="detail">
<span class="text-xs text-gray-400">2023-08-17 17:00:01</span>
<ADropdown @select="onRowActionsSelect" position="br">
<a-button type="text">
<template #icon>
<i class="icon-park-outline-repair"></i>
<i class="icon-park-outline-more"></i>
</template>
<div>详情</div>
</ADoption>
<ADoption value="delete" class="!text-red-500 !hover-bg-red-50">
<template #icon>
<i class="icon-park-outline-delete"></i>
</template>
删除
</ADoption>
</template>
</ADropdown>
</a-button>
<template #content>
<ADoption value="detail">
<template #icon>
<i class="icon-park-outline-repair"></i>
</template>
<div>详情</div>
</ADoption>
<ADoption value="delete" class="!text-red-500 !hover-bg-red-50">
<template #icon>
<i class="icon-park-outline-delete"></i>
</template>
删除
</ADoption>
</template>
</ADropdown>
</div>
</template>
</AListItem>
</AList>
@ -169,15 +167,14 @@ const onRowActionsSelect = () => {
padding: 0;
}
}
// .arco-dropdown-list {
// padding: 0 4px;
// .arco-dropdown-option {
// border-radius: 4px;
// }
// }
.arco-list-medium .arco-list-content-wrapper .arco-list-content > .arco-list-item {
padding: 4px 20px;
}
button.arco-btn-text,
.arco-btn-text[type="button"],
.arco-btn-text[type="submit"] {
color: inherit;
}
</style>
<route lang="json">

View File

@ -156,7 +156,7 @@ const form = useForm({
<route lang="json">
{
"meta": {
"sort": 20101,
"sort": 10001,
"title": "首页111",
"icon": "icon-park-outline-home"
}

View File

@ -151,14 +151,9 @@ const table = useTable({
<route lang="json">
{
"meta": {
"sort": 10301,
"sort": 10001,
"title": "首页",
"icon": "icon-park-outline-home"
},
"parentMeta": {
"title": "总览",
"sort": 10000,
"icon": "icon-park-outline-home"
}
}
</route>

View File

@ -2,10 +2,10 @@
<div>
<div class="bg-white px-4 pt-2">
<bread-crumb></bread-crumb>
<div class="flex justify-between items-end gap-4 bg-white px-1 py-4">
<div class="flex justify-between items-end gap-4 bg-white px-1 py-3">
<div>
<div class="text-lg font-semibold">新增文章</div>
<div class="text-gray-400 mt-1">新增的文章需审核才能展现</div>
<div class="text-gray-400 mt-1.5">新增的文章需审核才能展现</div>
</div>
<div>
<a-button class="mr-2">保存为草稿</a-button>
@ -40,12 +40,7 @@
</a-checkbox-group>
</a-form-item>
<a-form-item label="封面图">
<div class="h-24 rounded w-full flex items-center justify-center text-gray-500 bg-gray-100">
从素材库中选择...
</div>
<template #help>
推荐使用 600x400 的图片
</template>
<a-upload draggable></a-upload>
</a-form-item>
</a-form>
</div>

View File

@ -21,25 +21,25 @@
<a-form-item label="文件名">
<a-input v-model="model.filename" placeholder="请输入"></a-input>
</a-form-item>
<a-form-item label="导出类型">
<a-form-item label="文件类型">
<div class="grid gap-2">
<div
v-for="item in exportTypes"
@click="model.exportType = item.name"
class="w-full flex justify-between items-center gap-4 rounded py-2 px-4 border cursor-pointer border-slate-200"
class="w-full flex justify-between items-center gap-4 rounded py-2 px-4 cursor-pointer bg-[var(--color-fill-2)] hover:bg-[var(--color-fill-3)]"
:class="{
'!border-brand-500': model.exportType === item.name,
}"
>
<div class="flex items-center gap-2 rounded">
<div class="h-10 w-10 flex items-center justify-center rounded-full bg-brand-50">
<div class="">
<i :class="item.icon" class="text-2xl text-brand-500"></i>
</div>
<div>
<div class="text-slate-900">
{{ item.label }}
</div>
<div class="text-slate-400 text-xs">
<div class="text-slate-500 text-xs">
{{ item.description }}
</div>
</div>
@ -52,7 +52,7 @@
</a-form-item>
</a-form>
</a-modal>
<a-modal title="导入文件" :visible="false" title-align="start">
<a-modal title="导入类型" :visible="false" title-align="start">
<a-alert> 请按照 <a-link>上传模板</a-link> </a-alert>
<a-upload draggable class="mt-4"></a-upload>
</a-modal>
@ -185,26 +185,26 @@ const exportTypes = [
name: "excel",
icon: "icon-park-outline-file-excel",
label: "Excel格式",
description: "后缀: .xlsx, 可使用 office excel 2003 及以上版本打开",
description: "导出为 .xlsx 文件",
},
{
name: "csv",
icon: "icon-park-outline-file-code",
label: "CSV格式",
description: "后缀: .csv, 可使用 excel 或 记事本等工具打开",
description: "导出为 .csv 文件",
},
{
name: "text",
icon: "icon-park-outline-file-text",
label: "TEXT格式",
description: "后缀: .txt, 可使用 记事本 或 其他文本编辑器打开",
description: "导出为 .txt 文件",
},
];
const model = reactive({
visible: false,
exportType: "excel",
filename: dayjs().format("导出文件YYYYMMDDHHmmss"),
filename: dayjs().format("文件YYYYMMDDHHmmss"),
});
</script>

View File

@ -7,7 +7,7 @@
<script setup lang="tsx">
import { api } from "@/api";
import { Table, useTable } from "@/components";
import { dayjs } from "@/libs";
import { dayjs } from "@/libs/dayjs";
import { Avatar, Button } from "@arco-design/web-vue";
const table = useTable({

View File

@ -7,7 +7,7 @@
<script setup lang="tsx">
import { api } from "@/api";
import { Table, useTable } from "@/components";
import { dayjs } from "@/libs";
import { dayjs } from "@/libs/dayjs";
const table = useTable({
data: async (model, paging) => {

View File

@ -14,7 +14,7 @@
<script setup lang="tsx">
import { api } from "@/api";
import { Table, useTable } from "@/components";
import { dayjs } from "@/libs";
import { dayjs } from "@/libs/dayjs";
import { Avatar } from "@arco-design/web-vue";
const table = useTable({

View File

@ -1,15 +0,0 @@
import { NProgress } from "@/libs/nprogress";
import { NavigationGuardWithThis, NavigationHookAfter } from "vue-router";
const before: NavigationGuardWithThis<undefined> = function () {
NProgress.start();
};
const after: NavigationHookAfter = function () {
NProgress.done();
};
export const nprogressGuard = {
before,
after,
};

View File

@ -0,0 +1,31 @@
import { NProgress } from "@/libs/nprogress";
import { useAppStore } from "@/store";
import { NavigationGuardWithThis, NavigationHookAfter } from "vue-router";
const routeMap = new Map<string, boolean>();
const before: NavigationGuardWithThis<undefined> = function (to) {
NProgress.start();
if (routeMap.get(to.fullPath)) {
return true;
}
const appStore = useAppStore();
appStore.setPageLoading(true);
};
const after: NavigationHookAfter = function (to) {
NProgress.done();
if (routeMap.get(to.fullPath)) {
return;
}
const appStore = useAppStore();
setTimeout(() => {
appStore.setPageLoading(false);
routeMap.set(to.fullPath, true);
}, 200);
};
export const progressGuard = {
before,
after,
};

View File

@ -1,6 +1,6 @@
import { createRouter, createWebHashHistory } from "vue-router";
import { authGuard } from "../guards/guard-auth";
import { nprogressGuard } from "../guards/guard-nprogress";
import { progressGuard } from "../guards/guard-progress";
import { titleGuard } from "../guards/guard-title";
import { routes } from "../routes";
import { api } from "@/api";
@ -16,8 +16,8 @@ export const router = createRouter({
],
});
router.beforeEach(nprogressGuard.before);
router.afterEach(nprogressGuard.after);
router.beforeEach(progressGuard.before);
router.afterEach(progressGuard.after);
router.beforeEach(authGuard);
router.afterEach(titleGuard);

View File

@ -4,27 +4,21 @@ import { RouteRecordRaw } from "vue-router";
const APP_ROUTE_NAME = "_layout";
/**
* _
*
* @description _
*/
const transformRoutes = (routes: RouteRecordRaw[]) => {
const topRoutes: RouteRecordRaw[] = [];
const appRoutes: RouteRecordRaw[] = [];
routes.forEach((route) => {
for (const route of routes) {
if ((route.name as string)?.startsWith("_")) {
route.path = route.path.replace("_", "");
topRoutes.push(route);
return;
continue;
}
// route.component = defineAsyncComponent({
// loader: route.component as any,
// loadingComponent: () => h("div", null, "loading"),
// errorComponent: () => h("div", null, "error"),
// delay: 200,
// timeout: 3000,
// });
appRoutes.push(route);
});
}
const appRoute = routes.find((i) => i.name === APP_ROUTE_NAME);
if (appRoute) {

View File

@ -3,9 +3,22 @@ import { defineStore } from "pinia";
export const useAppStore = defineStore({
id: "app",
state: () => ({
/**
*
*/
isDarkMode: false,
/**
*
*/
title: import.meta.env.VITE_TITLE,
/**
*
*/
subtitle: import.meta.env.VITE_SUBTITLE,
/**
*
*/
pageLoding: false,
}),
actions: {
/**
@ -30,6 +43,12 @@ export const useAppStore = defineStore({
document.body.classList.add("dark");
this.isDarkMode = true;
},
/**
* loading
*/
setPageLoading(loading: boolean) {
this.pageLoding = loading;
}
},
persist: true,
persist: !import.meta.env.DEV,
});

View File

@ -1,4 +1,4 @@
@import url('@arco-design/web-vue/es/index.less');
@import url("@arco-design/web-vue/es/index.less");
// @blue-6: #09f;
@ -13,9 +13,16 @@ body {
background-color: transparent;
}
.arco-menu-group-title {
font-size: 13px;
background-color: transparent;
}
.arco-menu-vertical .arco-menu-item,
.arco-menu-vertical .arco-menu-group-title,
.arco-menu-vertical .arco-menu-pop-header,
.arco-menu-vertical .arco-menu-inline-header {
margin-bottom: 1px;
}
.arco-menu {
.arco-menu-item {
&:hover {
@ -26,6 +33,7 @@ body {
}
}
.arco-menu-inner {
padding: 0;
.arco-menu-icon {
margin-right: 8px;
}
@ -78,7 +86,8 @@ body {
}
.arco-menu {
.arco-menu-item {
&.arco-menu-selected, &:hover {
&.arco-menu-selected,
&:hover {
background-color: var(--color-fill-2);
}
}

View File

@ -25,6 +25,7 @@ declare module '@vue/runtime-core' {
AImage: typeof import('@arco-design/web-vue')['Image']
AInput: typeof import('@arco-design/web-vue')['Input']
AInputPassword: typeof import('@arco-design/web-vue')['InputPassword']
AInputSearch: typeof import('@arco-design/web-vue')['InputSearch']
ALayout: typeof import('@arco-design/web-vue')['Layout']
ALayoutContent: typeof import('@arco-design/web-vue')['LayoutContent']
ALayoutHeader: typeof import('@arco-design/web-vue')['LayoutHeader']
@ -40,7 +41,9 @@ declare module '@vue/runtime-core' {
APagination: typeof import('@arco-design/web-vue')['Pagination']
ARadio: typeof import('@arco-design/web-vue')['Radio']
ARadioGroup: typeof import('@arco-design/web-vue')['RadioGroup']
AScrollbar: typeof import('@arco-design/web-vue')['Scrollbar']
ASpace: typeof import('@arco-design/web-vue')['Space']
ASpin: typeof import('@arco-design/web-vue')['Spin']
ASwitch: typeof import('@arco-design/web-vue')['Switch']
ATabPane: typeof import('@arco-design/web-vue')['TabPane']
ATabs: typeof import('@arco-design/web-vue')['Tabs']