enhance/router (#23)

* style: ui edit。

* feat: ui edit complete

* style: ui edit

* chore: update style import path

* feat: update import path

* chore: lint fix

* feat: add package.lok

* chore: update node version

* chore: update actions

* chore: remove lock file

* fix: node version update

* fix: revert preview actiion

* fix: tdesign-wrapper classname fix

* feat: add router permission

* feat: enhance router

* feat: optimize header icon

Co-authored-by: pengYYY <pengyue970715@gmail.com>
This commit is contained in:
PY 2021-12-12 15:46:02 +08:00 committed by GitHub
parent 15e8f72117
commit baf43fb503
35 changed files with 1430 additions and 978 deletions

View File

@ -53,7 +53,7 @@
"stylelint-order": "^4.1.0", "stylelint-order": "^4.1.0",
"stylelint-scss": "^4.0.0", "stylelint-scss": "^4.0.0",
"typescript": "^4.4.3", "typescript": "^4.4.3",
"vite": "^2.4.4", "vite": "^2.7.1",
"vite-plugin-mock": "^2.9.6", "vite-plugin-mock": "^2.9.6",
"vite-plugin-svg-sprite-component": "^1.0.10", "vite-plugin-svg-sprite-component": "^1.0.10",
"vite-svg-loader": "^3.1.0", "vite-svg-loader": "^3.1.0",

View File

@ -19,3 +19,10 @@ export default defineComponent({
}, },
}); });
</script> </script>
<style lang="less">
@import '@/style/variables.less';
#nprogress .bar {
background: @brand-color !important;
}
</style>

View File

@ -1,10 +1,4 @@
export const PREFIX = 'tdesign-starter'; export const PREFIX = 'tdesign-starter';
export const THEME = 'light'; export const THEME = 'light';
export const AUTHENTICATION_METHOD = 'customize'; export const AUTHENTICATION_METHOD = 'customize';
export const TOKEN_NAME = 'tdesign-starter';
export default {
PREFIX,
THEME,
AUTHENTICATION_METHOD,
};

View File

@ -9,6 +9,7 @@ export interface MenuRoute {
path: string; path: string;
title?: string; title?: string;
icon?: string; icon?: string;
redirect?: string;
children: MenuRoute[]; children: MenuRoute[];
meta: any; meta: any;
} }

View File

@ -1,17 +1,13 @@
<template> <template>
<div class="tdesign-wrapper"> <div class="tdesign-wrapper">
<router-view /> <router-view />
<tdesign-setting />
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import TdesignSetting from './setting.vue';
export default defineComponent({ export default defineComponent({
components: { components: {},
TdesignSetting,
},
}); });
</script> </script>
<style lang="less"> <style lang="less">

View File

@ -0,0 +1,25 @@
<template>
<router-view v-slot="{ Component }">
<transition name="fade" mode="out-in">
<component :is="Component" />
</transition>
</router-view>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({});
</script>
<style lang="less" scoped>
@import '@/style/variables';
.fade-leave-active,
.fade-enter-active {
transition: opacity @anim-duration-slow @anim-time-fn-easing;
}
.fade-enter,
.fade-leave-to {
opacity: 0;
}
</style>

View File

@ -2,9 +2,6 @@ import { defineComponent, PropType, computed } from 'vue';
import { PREFIX as prefix } from '@/config/global'; import { PREFIX as prefix } from '@/config/global';
import { MenuRoute } from '@/interface'; import { MenuRoute } from '@/interface';
// utils
const isSingleNav = (list: Array<MenuRoute>) => list.every((item) => !item.children || item.children.length === 0);
const getMenuList = (list: MenuRoute[], basePath?: string): MenuRoute[] => { const getMenuList = (list: MenuRoute[], basePath?: string): MenuRoute[] => {
if (!list) { if (!list) {
return []; return [];
@ -13,31 +10,29 @@ const getMenuList = (list: MenuRoute[], basePath?: string): MenuRoute[] => {
const path = basePath ? `${basePath}/${item.path}` : item.path; const path = basePath ? `${basePath}/${item.path}` : item.path;
return { return {
path, path,
title: item.title, title: item.meta?.title,
icon: item.icon || '', icon: item.meta?.icon || '',
children: getMenuList(item.children, path), children: getMenuList(item.children, path),
meta: item.meta || {}, meta: item.meta,
redirect: item.redirect,
}; };
}); });
}; };
const useRenderNav = (list: Array<MenuRoute>, deep = 0, maxLevel = 2) => { const useRenderNav = (list: Array<MenuRoute>) => {
if (isSingleNav(list)) { return list.map((item) => {
return list.map((item) => ( if (!item.children || !item.children.length || item.meta?.single) {
return (
<t-menu-item <t-menu-item
key={item.path} name={item.path}
value={item.path} value={item.meta?.single ? item.redirect : item.path}
to={item.path} to={item.path}
icon={() => item.icon && <t-icon name={item.icon} />} icon={() => item.icon && <t-icon name={item.icon} />}
> >
{item.title} {item.title}
</t-menu-item> </t-menu-item>
)); );
} }
return list.map((item) => {
if (deep < maxLevel) {
if (deep === 0) {
return ( return (
<t-submenu <t-submenu
name={item.path} name={item.path}
@ -45,23 +40,9 @@ const useRenderNav = (list: Array<MenuRoute>, deep = 0, maxLevel = 2) => {
title={item.title} title={item.title}
icon={() => item.icon && <t-icon name={item.icon} />} icon={() => item.icon && <t-icon name={item.icon} />}
> >
{item.children && useRenderNav(item.children, deep + 1)} {item.children && useRenderNav(item.children)}
</t-submenu> </t-submenu>
); );
}
return (
<t-menu-item
name={item.path}
value={item.path}
to={item.path}
icon={() => item.icon && <t-icon name={item.icon} />}
>
{item.title}
{item.children && useRenderNav(item.children, deep + 1)}
</t-menu-item>
);
}
return '';
}); });
}; };

View File

@ -1,9 +1,10 @@
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import TdesignHeader from './header/index.vue'; import TdesignHeader from './components/Header.vue';
import TdesignBreadcrumb from './components/Breadcrumb.vue'; import TdesignBreadcrumb from './components/Breadcrumb.vue';
import TdesignFooter from './components/Footer.vue'; import TdesignFooter from './components/Footer.vue';
import TdesignSideNav from './components/SideNav'; import TdesignSideNav from './components/SideNav';
import TdesignContent from './components/Content.vue';
import { PREFIX } from '@/config/global'; import { PREFIX } from '@/config/global';
import TdesignSetting from './setting.vue'; import TdesignSetting from './setting.vue';
@ -20,6 +21,7 @@ export default defineComponent({
TdesignSideNav, TdesignSideNav,
TdesignSetting, TdesignSetting,
TdesignBreadcrumb, TdesignBreadcrumb,
TdesignContent,
}, },
computed: { computed: {
...mapGetters({ ...mapGetters({
@ -27,10 +29,9 @@ export default defineComponent({
showHeader: 'setting/showHeader', showHeader: 'setting/showHeader',
showHeaderLogo: 'setting/showHeaderLogo', showHeaderLogo: 'setting/showHeaderLogo',
showSidebarLogo: 'setting/showSidebarLogo', showSidebarLogo: 'setting/showSidebarLogo',
headerMenu: 'setting/headerMenu',
sideMenu: 'setting/sideMenu',
showFooter: 'setting/showFooter', showFooter: 'setting/showFooter',
mode: 'setting/mode', mode: 'setting/mode',
menuRouters: 'permission/routers',
}), }),
setting(): SettingType { setting(): SettingType {
return this.$store.state.setting; return this.$store.state.setting;
@ -42,6 +43,34 @@ export default defineComponent({
}, },
]; ];
}, },
headerMenu() {
const { layout, splitMenu } = this.$store.state.setting;
const { menuRouters } = this;
if (layout === 'mix') {
if (splitMenu) {
return menuRouters.map((menu) => ({
...menu,
children: [],
}));
}
return [];
}
return menuRouters;
},
sideMenu() {
const { layout, splitMenu } = this.$store.state.setting;
const { menuRouters } = this;
if (layout === 'mix' && splitMenu) {
let index;
for (index = 0; index < menuRouters.length; index++) {
const item = menuRouters[index];
if (item.children && item.children.length > 0) {
return item.children.map((menuRouter) => ({ ...menuRouter, path: `${item.path}/${menuRouter.path}` }));
}
}
}
return menuRouters;
},
}, },
methods: { methods: {
getNavTheme(mode: ModeType, layout: string, type: string): string { getNavTheme(mode: ModeType, layout: string, type: string): string {
@ -90,10 +119,10 @@ export default defineComponent({
const { showBreadcrumb } = this.setting; const { showBreadcrumb } = this.setting;
const { showFooter } = this; const { showFooter } = this;
return ( return (
<t-layout> <t-layout class={[`${PREFIX}-layout`, 'narrow-scrollbar']}>
<t-content class={`${PREFIX}-content-layout`}> <t-content class={`${PREFIX}-content-layout`}>
{showBreadcrumb && <tdesign-breadcrumb />} {showBreadcrumb && <tdesign-breadcrumb />}
<router-view /> <TdesignContent />
</t-content> </t-content>
{showFooter && this.renderFooter()} {showFooter && this.renderFooter()}
</t-layout> </t-layout>
@ -120,7 +149,7 @@ export default defineComponent({
<div class={`${PREFIX}-wrapper`}> <div class={`${PREFIX}-wrapper`}>
{layout === 'side' ? ( {layout === 'side' ? (
<t-layout class={this.mainLayoutCls} key="side"> <t-layout class={this.mainLayoutCls} key="side">
<t-aside style={{ width: 'fit-content' }}>{sidebar}</t-aside> <t-aside>{sidebar}</t-aside>
<t-layout>{[header, content]}</t-layout> <t-layout>{[header, content]}</t-layout>
</t-layout> </t-layout>
) : ( ) : (

View File

@ -7,6 +7,7 @@ import { store } from './store';
import router from './router'; import router from './router';
import '@/style/index.less'; import '@/style/index.less';
import './permission';
const app = createApp(App); const app = createApp(App);

View File

@ -126,6 +126,7 @@ export const SALE_COLUMNS: TdBaseTableProps['columns'] = [
colKey: 'index', colKey: 'index',
title: '排名', title: '排名',
width: 80, width: 80,
fixed: 'left',
}, },
{ {
align: 'left', align: 'left',
@ -156,7 +157,8 @@ export const SALE_COLUMNS: TdBaseTableProps['columns'] = [
align: 'center', align: 'center',
colKey: 'operation', colKey: 'operation',
title: '操作', title: '操作',
width: 100, width: 80,
fixed: 'right',
}, },
]; ];
@ -166,6 +168,7 @@ export const BUY_COLUMNS: TdBaseTableProps['columns'] = [
colKey: 'index', colKey: 'index',
title: '排名', title: '排名',
width: 80, width: 80,
fixed: 'left',
}, },
{ {
align: 'left', align: 'left',
@ -196,6 +199,7 @@ export const BUY_COLUMNS: TdBaseTableProps['columns'] = [
align: 'center', align: 'center',
colKey: 'operation', colKey: 'operation',
title: '操作', title: '操作',
width: 100, width: 80,
fixed: 'right',
}, },
]; ];

View File

@ -32,7 +32,7 @@ export function getColorFromTheme(theme: string) {
/** 图表颜色 */ /** 图表颜色 */
function chartListColor(): Array<string> { function chartListColor(): Array<string> {
const colorList: Array<string> = ['#0052D9', '#BCC4D0', '#7D46BD', '#0594FA', '#ED7B2F']; const colorList: Array<string> = ['#0052D9', '#BCC4D0', '#7D46BD', '#0594FA', '#ED7B2F'];
const { setting } = state; const { setting } = state as any;
return getColorFromTheme(setting.brandTheme) || colorList; return getColorFromTheme(setting.brandTheme) || colorList;
} }
@ -1020,13 +1020,16 @@ export function getPieChartDataSet(radius = 42) {
return { return {
color: chartListColor(), color: chartListColor(),
tooltip: { tooltip: {
trigger: 'item', show: false,
trigger: 'axis',
position: null,
}, },
grid: { grid: {
top: '0', top: '0',
right: '0', right: '0',
}, },
legend: { legend: {
selectedMode: false,
itemWidth: 12, itemWidth: 12,
itemHeight: 4, itemHeight: 4,
textStyle: { textStyle: {
@ -1042,7 +1045,10 @@ export function getPieChartDataSet(radius = 42) {
name: '销售渠道', name: '销售渠道',
type: 'pie', type: 'pie',
radius: ['48%', '60%'], radius: ['48%', '60%'],
avoidLabelOverlap: false, avoidLabelOverlap: true,
selectedMode: true,
hoverAnimation: true,
silent: true,
label: { label: {
show: true, show: true,
position: 'center', position: 'center',
@ -1050,13 +1056,13 @@ export function getPieChartDataSet(radius = 42) {
rich: { rich: {
value: { value: {
color: '#303133', color: '#303133',
fontSize: 36, fontSize: 28,
fontWeight: 'normal', fontWeight: 'normal',
lineHeight: 46, lineHeight: 46,
}, },
name: { name: {
color: '#909399', color: '#909399',
fontSize: 14, fontSize: 12,
lineHeight: 14, lineHeight: 14,
}, },
}, },
@ -1068,7 +1074,7 @@ export function getPieChartDataSet(radius = 42) {
rich: { rich: {
value: { value: {
color: '#303133', color: '#303133',
fontSize: 36, fontSize: 28,
fontWeight: 'normal', fontWeight: 'normal',
lineHeight: 46, lineHeight: 46,
}, },

View File

@ -1,5 +1,5 @@
<template> <template>
<!-- 顶部 card --> <div>
<t-row :gutter="[16, 16]"> <t-row :gutter="[16, 16]">
<t-col v-for="(item, index) in PANE_LIST" :key="item.title" :xs="6" :xl="3"> <t-col v-for="(item, index) in PANE_LIST" :key="item.title" :xs="6" :xl="3">
<card :subtitle="item.title" :style="{ height: '168px' }" :class="{ 'main-color': index == 0 }" size="small"> <card :subtitle="item.title" :style="{ height: '168px' }" :class="{ 'main-color': index == 0 }" size="small">
@ -93,7 +93,7 @@
<t-radio-button value="monthVal"> 季度 </t-radio-button> <t-radio-button value="monthVal"> 季度 </t-radio-button>
</t-radio-group> </t-radio-group>
</template> </template>
<t-table :data="SALE_TEND_LIST" :columns="SALE_COLUMNS" row-key="productName" :style="{ overflow: 'scroll' }"> <t-table :data="SALE_TEND_LIST" :columns="SALE_COLUMNS" row-key="productName">
<template #index="{ rowIndex }"> <template #index="{ rowIndex }">
<span :class="getRankClass(rowIndex)"> <span :class="getRankClass(rowIndex)">
{{ rowIndex + 1 }} {{ rowIndex + 1 }}
@ -118,7 +118,7 @@
<t-radio-button value="monthVal"> 季度 </t-radio-button> <t-radio-button value="monthVal"> 季度 </t-radio-button>
</t-radio-group> </t-radio-group>
</template> </template>
<t-table :data="BUY_TEND_LIST" :columns="BUY_COLUMNS" row-key="productName" :style="{ overflow: 'scroll' }"> <t-table :data="BUY_TEND_LIST" :columns="BUY_COLUMNS" row-key="productName">
<template #index="{ rowIndex }"> <template #index="{ rowIndex }">
<span :class="getRankClass(rowIndex)"> <span :class="getRankClass(rowIndex)">
{{ rowIndex + 1 }} {{ rowIndex + 1 }}
@ -200,6 +200,7 @@
</t-col> </t-col>
</t-row> </t-row>
</div> </div>
</div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, onMounted, watch, ref } from 'vue'; import { defineComponent, onMounted, watch, ref } from 'vue';
@ -311,24 +312,24 @@ export default defineComponent({
@import '@/style/variables.less'; @import '@/style/variables.less';
.card-container.main-color { .card-container.main-color {
background: @brand-color; background: @brand-color !important;
color: @text-color-primary; color: @text-color-primary !important;
.card-describe { .card-describe {
color: @text-color-anti; color: @text-color-anti !important;
} }
.dashboard-item-top span { .dashboard-item-top span {
color: @text-color-anti; color: @text-color-anti !important;
} }
.dashboard-item-block { .dashboard-item-block {
color: @text-color-anti; color: @text-color-anti !important;
opacity: 0.6; opacity: 0.6;
} }
.dashboard-item-bottom { .dashboard-item-bottom {
color: @text-color-anti; color: @text-color-anti !important;
} }
} }
</style> </style>

View File

@ -29,7 +29,7 @@
@change="onMaterialChange" @change="onMaterialChange"
/> />
</template> </template>
<div id="lineContainer" style="width: 100%; height: 416px" /> <div id="lineContainer" style="width: 100%; height: 406px" />
</card> </card>
</t-col> </t-col>
<t-col :xs="12" :xl="3"> <t-col :xs="12" :xl="3">

View File

@ -1,4 +1,5 @@
<template> <template>
<div>
<card title="基本信息"> <card title="基本信息">
<div class="info-block"> <div class="info-block">
<div v-for="(item, index) in BASE_INFO_DATA" :key="index" class="info-item"> <div v-for="(item, index) in BASE_INFO_DATA" :key="index" class="info-item">
@ -106,6 +107,7 @@
</div> </div>
</template> </template>
</t-dialog> </t-dialog>
</div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, ref, onMounted } from 'vue'; import { defineComponent, ref, onMounted } from 'vue';

View File

@ -1,4 +1,5 @@
<template> <template>
<div>
<card title="基本信息"> <card title="基本信息">
<div class="info-block"> <div class="info-block">
<div v-for="(item, index) in BASE_INFO_DATA" :key="index" class="info-item"> <div v-for="(item, index) in BASE_INFO_DATA" :key="index" class="info-item">
@ -16,13 +17,14 @@
</div> </div>
</card> </card>
<card title="变更记录"> <card title="变更记录" class="container-base-margin-top">
<t-steps class="detail-base-info-steps" layout="vertical" theme="dot" :current="1"> <t-steps class="detail-base-info-steps" layout="vertical" theme="dot" :current="1">
<t-step-item title="上传合同附件" content="这里是提示文字" /> <t-step-item title="上传合同附件" content="这里是提示文字" />
<t-step-item title="修改合同金额" content="这里是提示文字" /> <t-step-item title="修改合同金额" content="这里是提示文字" />
<t-step-item title="新建合同" content="2020-12-01 15:00:00 管理员-李川操作" /> <t-step-item title="新建合同" content="2020-12-01 15:00:00 管理员-李川操作" />
</t-steps> </t-steps>
</card> </card>
</div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';

View File

@ -1,4 +1,5 @@
<template> <template>
<div>
<t-row :gutter="16"> <t-row :gutter="16">
<t-col :span="6"> <t-col :span="6">
<card title="部署趋势"> <card title="部署趋势">
@ -65,6 +66,7 @@
</div> </div>
</template> </template>
</t-dialog> </t-dialog>
</div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, onMounted, onUnmounted, ref, watch } from 'vue'; import { defineComponent, onMounted, onUnmounted, ref, watch } from 'vue';

View File

@ -1,4 +1,5 @@
<template> <template>
<div>
<div class="secondary-notification"> <div class="secondary-notification">
<t-tabs v-model="tabValue"> <t-tabs v-model="tabValue">
<t-tab-panel v-for="(tab, tabIndex) in TAB_LIST" :key="tabIndex" :value="tab.value" :label="tab.label"> <t-tab-panel v-for="(tab, tabIndex) in TAB_LIST" :key="tabIndex" :value="tab.value" :label="tab.label">
@ -46,6 +47,7 @@
:body="`确认删除通知:${selectedItem && selectedItem.content}吗?`" :body="`确认删除通知:${selectedItem && selectedItem.content}吗?`"
:on-confirm="deleteMsg" :on-confirm="deleteMsg"
/> />
</div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, ref, computed, ComputedRef } from 'vue'; import { defineComponent, ref, computed, ComputedRef } from 'vue';

View File

@ -1,4 +1,5 @@
<template> <template>
<div>
<div class="form-basic-container"> <div class="form-basic-container">
<div class="form-basic-item"> <div class="form-basic-item">
<div class="form-basic-container-title">合同信息</div> <div class="form-basic-container-title">合同信息</div>
@ -57,7 +58,12 @@
placeholder="请选择类型" placeholder="请选择类型"
clearable clearable
> >
<t-option v-for="(item, index) in PARTY_A_OPTIONS" :key="index" :value="item.value" :label="item.label"> <t-option
v-for="(item, index) in PARTY_A_OPTIONS"
:key="index"
:value="item.value"
:label="item.label"
>
{{ item.label }} {{ item.label }}
</t-option> </t-option>
</t-select> </t-select>
@ -72,7 +78,12 @@
class="demo-select-base" class="demo-select-base"
clearable clearable
> >
<t-option v-for="(item, index) in PARTY_B_OPTIONS" :key="index" :value="item.value" :label="item.label"> <t-option
v-for="(item, index) in PARTY_B_OPTIONS"
:key="index"
:value="item.value"
:label="item.label"
>
{{ item.label }} {{ item.label }}
</t-option> </t-option>
</t-select> </t-select>
@ -160,6 +171,7 @@
</div> </div>
</div> </div>
</div> </div>
</div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, ref } from 'vue'; import { defineComponent, ref } from 'vue';

View File

@ -1,4 +1,5 @@
<template> <template>
<div>
<div class="form-step-container"> <div class="form-step-container">
<!-- 简单步骤条 --> <!-- 简单步骤条 -->
<card title="基本信息"> <card title="基本信息">
@ -136,6 +137,7 @@
</div> </div>
</div> </div>
</div> </div>
</div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, ref, computed } from 'vue'; import { defineComponent, ref, computed } from 'vue';

View File

@ -1,4 +1,5 @@
<template> <template>
<div>
<card class="list-card-container"> <card class="list-card-container">
<t-row justify="space-between"> <t-row justify="space-between">
<div class="left-operation-container"> <div class="left-operation-container">
@ -61,6 +62,7 @@
:on-cancel="onCancel" :on-cancel="onCancel"
@confirm="onConfirmDelete" @confirm="onConfirmDelete"
/> />
</div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, ref, onMounted, computed } from 'vue'; import { defineComponent, ref, onMounted, computed } from 'vue';

View File

@ -1,4 +1,5 @@
<template> <template>
<div>
<div class="list-card-operation"> <div class="list-card-operation">
<t-button @click="formDialogVisible = true"> 新建产品 </t-button> <t-button @click="formDialogVisible = true"> 新建产品 </t-button>
<t-input v-model="searchValue" class="search-input" placeholder="请输入你需要搜索的内容" clearable> <t-input v-model="searchValue" class="search-input" placeholder="请输入你需要搜索的内容" clearable>
@ -43,6 +44,7 @@
/> />
</div> </div>
</template> </template>
<div v-else-if="dataLoading" class="list-card-loading"> <div v-else-if="dataLoading" class="list-card-loading">
<t-loading size="large" text="加载数据中..." /> <t-loading size="large" text="加载数据中..." />
</div> </div>
@ -54,6 +56,7 @@
:on-cancel="onCancel" :on-cancel="onCancel"
@confirm="onConfirmDelete" @confirm="onConfirmDelete"
/> />
</div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, ref, computed, onMounted } from 'vue'; import { defineComponent, ref, computed, onMounted } from 'vue';

View File

@ -9,7 +9,7 @@
> >
<template v-if="type == 'password'"> <template v-if="type == 'password'">
<t-form-item name="account"> <t-form-item name="account">
<t-input v-model="formData.account" size="large" placeholder="请输入您的邮箱/手机号"> <t-input v-model="formData.account" size="large" placeholder="请输入您的账号:td">
<template #prefix-icon> <template #prefix-icon>
<t-icon name="user" /> <t-icon name="user" />
</template> </template>
@ -22,7 +22,7 @@
size="large" size="large"
:type="showPsw ? 'text' : 'password'" :type="showPsw ? 'text' : 'password'"
clearable clearable
placeholder="请输入登录密码" placeholder="请输入登录密码:main_/dev_"
> >
<template #prefix-icon> <template #prefix-icon>
<t-icon name="lock-on" /> <t-icon name="lock-on" />
@ -50,14 +50,6 @@
<!-- 手机号登陆 --> <!-- 手机号登陆 -->
<template v-else> <template v-else>
<t-form-item name="phone">
<t-input v-model="formData.phone" size="large" placeholder="请输入您的手机号">
<template #prefix-icon>
<t-icon name="user" />
</template>
</t-input>
</t-form-item>
<t-form-item class="verification-code" name="verifyCode"> <t-form-item class="verification-code" name="verifyCode">
<t-input v-model="formData.verifyCode" size="large" placeholder="请输入验证码" /> <t-input v-model="formData.verifyCode" size="large" placeholder="请输入验证码" />
<t-button variant="outline" :disabled="countDown > 0" @click="handleCounter"> <t-button variant="outline" :disabled="countDown > 0" @click="handleCounter">
@ -97,8 +89,11 @@ const INITIAL_DATA = {
}; };
const FORM_RULES = { const FORM_RULES = {
phone: [{ required: true, message: '手机号必填', type: 'error' }],
account: [{ required: true, message: '账号必填', type: 'error' }], account: [{ required: true, message: '账号必填', type: 'error' }],
phone: [
{ required: true, message: '手机号必填', type: 'error' },
{ telnumber: true, message: '请输入正确的手机号', type: 'warning' },
],
password: [{ required: true, message: '密码必填', type: 'error' }, { validator: passwordValidator }], password: [{ required: true, message: '密码必填', type: 'error' }, { validator: passwordValidator }],
verifyCode: [{ required: true, message: '验证码必填', type: 'error' }], verifyCode: [{ required: true, message: '验证码必填', type: 'error' }],
}; };
@ -120,15 +115,18 @@ export default defineComponent({
const router = useRouter(); const router = useRouter();
const store = useStore(); const store = useStore();
const onSubmit = ({ validateResult }) => { const onSubmit = async ({ validateResult }) => {
if (validateResult === true) { if (validateResult === true) {
store.commit('user/SET_USER_INFO', formData.value); try {
await store.dispatch('user/login', formData.value);
MessagePlugin.success('登录成功'); MessagePlugin.success('登陆成功');
router.push({ router.push({
path: '/', path: '/dashboard/base',
}); });
} catch (e) {
console.log(e);
MessagePlugin.error(e.message);
}
} }
}; };

View File

@ -1,22 +1,23 @@
@import '@/style/variables.less'; @import '@/style/variables.less';
&.light { .light {
.login-wrapper { &.login-wrapper {
background-color: white; background-color: white;
background-image: url('@/assets/assets-login-bg-white.png'); background-image: url('@/assets/assets-login-bg-white.png');
} }
} }
&.dark { .dark {
.login-wrapper { &.login-wrapper {
background-color: @bg-color-page; background-color: @bg-color-page;
background-image: url('@/assets/assets-login-bg-black.png'); background-image: url('@/assets/assets-login-bg-black.png');
} }
} }
.login-wrapper { .login-wrapper {
width: 100%; height: 100vh;
height: 100%; display: flex;
flex-direction: column;
background-size: cover; background-size: cover;
background-position: 50%; background-position: 50%;
position: relative; position: relative;

View File

@ -16,6 +16,7 @@
<login v-if="type === 'login'" /> <login v-if="type === 'login'" />
<register v-else @register-success="switchType('login')" /> <register v-else @register-success="switchType('login')" />
<tdesign-setting />
</div> </div>
</div> </div>
</template> </template>
@ -26,6 +27,7 @@ import { useStore } from 'vuex';
import Login from './components/Login.vue'; import Login from './components/Login.vue';
import Register from './components/Register.vue'; import Register from './components/Register.vue';
import LoginHeader from './components/Header.vue'; import LoginHeader from './components/Header.vue';
import TdesignSetting from '@/layouts/setting.vue';
/** 高级详情 */ /** 高级详情 */
export default defineComponent({ export default defineComponent({
@ -34,6 +36,7 @@ export default defineComponent({
Login, Login,
Register, Register,
LoginHeader, LoginHeader,
TdesignSetting,
}, },
setup() { setup() {
const type = ref('login'); const type = ref('login');

56
src/permission.ts Normal file
View File

@ -0,0 +1,56 @@
import { MessagePlugin } from 'tdesign-vue-next';
import NProgress from 'nprogress'; // progress bar
import 'nprogress/nprogress.css'; // progress bar style
import store from '@/store';
import router from '@/router';
NProgress.configure({ showSpinner: false });
const whiteListRouters = store.getters['permission/whiteListRouters'];
router.beforeEach(async (to, from, next) => {
NProgress.start();
const token = store.getters['user/token'];
if (token) {
if (to.path === '/login') {
store.dispatch('user/logout');
store.dispatch('permission/restore');
next();
return;
}
const roles = store.getters['user/roles'];
if (roles && roles.length > 0) {
next();
} else {
try {
await store.dispatch('user/getUserInfo');
await store.dispatch('permission/initRoutes', store.getters['user/roles']);
next({ ...to });
} catch (error) {
console.log(error);
MessagePlugin.error(error);
await store.commit('user/removeToken');
next(`/login?redirect=${to.path}`);
NProgress.done();
}
}
} else {
/* white list router */
if (whiteListRouters.indexOf(to.path) !== -1) {
next();
} else {
next(`/login?redirect=${to.path}`);
}
NProgress.done();
}
});
router.afterEach(() => {
NProgress.done();
});

View File

@ -1,38 +1,35 @@
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router'; import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router';
import routeConfig from '@/config/routes';
const layoutModules = import.meta.glob('../layout/*'); import baseRouters from './modules/base';
const pagesModules = import.meta.glob('../pages/**/*.vue'); import componentsRouters from './modules/components';
const fristPagesModules = import.meta.glob('../pages/*.vue'); import othersRouters from './modules/others';
const modules = { ...layoutModules, ...fristPagesModules, ...pagesModules };
const getMenuRoutes = (list) => { // 存放动态路由
if (!list) { export const asyncRouterList: Array<RouteRecordRaw> = [...baseRouters, ...componentsRouters, ...othersRouters];
return [];
}
return list.map((item) => {
const { path = '', component, meta = { title: item.title }, redirect = '' } = item;
return {
path,
component: modules[component],
children: getMenuRoutes(item.children),
meta,
redirect,
};
});
};
const routes: Array<RouteRecordRaw> = [ // 存放固定的路由
...getMenuRoutes(routeConfig), const defaultRouterList: Array<RouteRecordRaw> = [
{ {
path: '', path: '/login',
redirect: '/login/index', name: 'login',
component: () => import('@/pages/login/index.vue'),
},
{
path: '/',
redirect: '/login',
component: () => import('@/layouts/blank.vue'),
}, },
]; ];
export const page404 = {
path: '/:w+',
name: '404Page',
redirect: '/result/404',
};
const router = createRouter({ const router = createRouter({
history: createWebHashHistory(), history: createWebHashHistory(),
routes, routes: defaultRouterList,
scrollBehavior() { scrollBehavior() {
return { return {
el: '#app', el: '#app',
@ -41,4 +38,5 @@ const router = createRouter({
}; };
}, },
}); });
export default router; export default router;

View File

@ -0,0 +1,25 @@
import Layout from '@/layouts';
export default [
{
path: '/dashboard',
component: Layout,
redirect: '/dashboard/base',
name: 'dashboard',
meta: { title: '仪表盘', icon: 'dashboard' },
children: [
{
path: 'base',
name: 'dashboardBase',
component: () => import('@/pages/dashboard/base/index.vue'),
meta: { title: '概览仪表盘' },
},
{
path: 'detail',
name: 'dashboardDetail',
component: () => import('@/pages/dashboard/detail/index.vue'),
meta: { title: '统计报表' },
},
],
},
];

View File

@ -0,0 +1,151 @@
import Layout from '@/layouts';
export default [
{
path: '/list',
name: 'list',
component: Layout,
redirect: '/list/base',
meta: { title: '列表页', icon: 'view-module' },
children: [
{
path: 'base',
name: 'listBase',
component: () => import('@/pages/list/base/index.vue'),
meta: { title: '基础列表页' },
},
{
path: 'card',
name: 'listCard',
component: () => import('@/pages/list/card/index.vue'),
meta: { title: '卡片列表页' },
},
{
path: 'filter',
name: 'listFilter',
component: () => import('@/pages/list/filter/index.vue'),
meta: { title: '筛选列表页' },
},
{
path: 'tree',
name: 'listTree',
component: () => import('@/pages/list/tree/index.vue'),
meta: { title: '树状筛选列表页' },
},
],
},
{
path: '/form',
name: 'form',
component: Layout,
redirect: '/form/base',
meta: { title: '表单页', icon: 'queue' },
children: [
{
path: 'base',
name: 'formBase',
component: () => import('@/pages/form/base/index.vue'),
meta: { title: '基础表单页' },
},
{
path: 'step',
name: 'formStep',
component: () => import('@/pages/form/step/index.vue'),
meta: { title: '分步表单页' },
},
],
},
{
path: '/detail',
name: 'detail',
component: Layout,
redirect: '/detail/base',
meta: { title: '详情页', icon: 'layers' },
children: [
{
path: 'base',
name: 'detailBase',
component: () => import('@/pages/detail/base/index.vue'),
meta: { title: '基础详情页' },
},
{
path: 'advanced',
name: 'detailAdvanced',
component: () => import('@/pages/detail/advanced/index.vue'),
meta: { title: '多卡片详情页' },
},
{
path: 'deploy',
name: 'detailDeploy',
component: () => import('@/pages/detail/deploy/index.vue'),
meta: { title: '数据详情页' },
},
{
path: 'secondary',
name: 'detailDeploy',
component: () => import('@/pages/detail/secondary/index.vue'),
meta: { title: '二级详情页' },
},
],
},
{
path: '/result',
name: 'result',
component: Layout,
redirect: '/result/success',
meta: { title: '结果页', icon: 'check-circle' },
children: [
{
path: 'success',
name: 'resultSuccess',
component: () => import('@/pages/result/success/index.vue'),
meta: { title: '成功页' },
},
{
path: 'fail',
name: 'resultFail',
component: () => import('@/pages/result/fail/index.vue'),
meta: { title: '失败页' },
},
],
},
{
path: '/warning',
name: 'warning',
component: Layout,
redirect: '/warning/success',
meta: { title: '异常页', icon: 'error-circle' },
children: [
{
path: 'network-error',
name: 'warningNetworkError',
component: () => import('@/pages/result/network-error/index.vue'),
meta: { title: '网络异常' },
},
{
path: '403',
name: 'warning403',
component: () => import('@/pages/result/403/index.vue'),
meta: { title: '无权限' },
},
{
path: '404',
name: 'warning404',
component: () => import('@/pages/result/404/index.vue'),
meta: { title: '访问页面不存在页' },
},
{
path: '500',
name: 'warning500',
component: () => import('@/pages/result/500/index.vue'),
meta: { title: '服务器出错页' },
},
{
path: 'browser-incompatible',
name: 'warningBrowserIncompatible',
component: () => import('@/pages/result/browser-incompatible/index.vue'),
meta: { title: '浏览器不兼容页' },
},
],
},
];

View File

@ -0,0 +1,33 @@
import Layout from '@/layouts';
export default [
{
path: '/user',
name: 'user',
component: Layout,
redirect: '/user/index',
meta: { title: '个人页', icon: 'user-circle' },
children: [
{
path: 'index',
name: 'userIndex',
component: () => import('@/pages/user/index.vue'),
meta: { title: '个人中心' },
},
],
},
{
path: '/loginRedirect',
name: 'loginRedirect',
meta: { title: '登录页', icon: 'chevron-right-rectangle' },
component: () => import('@/layouts/blank.vue'),
children: [
{
path: 'index',
redirect: '/login',
component: () => import('@/layouts/blank.vue'),
meta: { title: '登陆页' },
},
],
},
];

View File

@ -2,12 +2,14 @@ import { createStore } from 'vuex';
import user from './modules/user'; import user from './modules/user';
import notification from './modules/notification'; import notification from './modules/notification';
import setting from './modules/setting'; import setting from './modules/setting';
import permission from './modules/permission';
export const store = createStore({ export const store = createStore({
modules: { modules: {
user, user,
setting, setting,
notification, notification,
permission,
}, },
}); });

View File

@ -0,0 +1,74 @@
import router, { asyncRouterList, page404 } from '@/router';
function filterPermissionsRouters(routes, roles) {
const res = [];
routes.forEach((route) => {
const children = [];
route.children?.forEach((childRouter) => {
const roleCode = childRouter.meta?.roleCode || childRouter.name;
if (roles.indexOf(roleCode) !== -1) {
children.push(childRouter);
}
});
if (children.length > 0) {
route.children = children;
res.push(route);
}
});
return res;
}
const state = {
whiteListRouters: ['/login'],
routers: [],
};
const mutations = {
setRouters: (state, routers) => {
state.routers = routers;
},
};
const getters = {
routers: (state) => {
return state.routers;
},
whiteListRouters: (state) => {
return state.whiteListRouters;
},
};
const actions = {
async initRoutes({ commit }, roles) {
let accessedRouters;
// special token
if (roles.includes('ALL_ROUTERS')) {
accessedRouters = asyncRouterList;
} else {
accessedRouters = filterPermissionsRouters(asyncRouterList, roles);
}
commit('setRouters', accessedRouters);
// register routers
state.routers.concat(page404).forEach((item) => {
router.addRoute(item);
});
},
async restore({ commit, state }) {
// remove routers
state.routers.concat(page404).forEach((item) => {
router.removeRoute(item.name);
});
commit('setRouters', []);
},
};
export default {
namespaced: true,
state,
mutations,
actions,
getters,
};

View File

@ -1,5 +1,4 @@
import STYLE_CONFIG from '@/config/style'; import STYLE_CONFIG from '@/config/style';
import MENU_CONFIG from '@/config/routes';
// 定义的state初始值 // 定义的state初始值
const state = { const state = {
@ -37,32 +36,6 @@ const getters = {
showSidebar: (state) => state.layout !== 'top', showSidebar: (state) => state.layout !== 'top',
showSidebarLogo: (state) => state.layout === 'side', showSidebarLogo: (state) => state.layout === 'side',
showHeaderLogo: (state) => state.layout !== 'side', showHeaderLogo: (state) => state.layout !== 'side',
headerMenu: (state) => {
if (state.layout === 'mix') {
if (state.splitMenu) {
return MENU_CONFIG.map((menu) => ({
...menu,
children: [],
}));
}
return [];
}
return MENU_CONFIG;
},
sideMenu: (state, getters, rootState) => {
if (state.layout === 'mix' && state.splitMenu) {
let index;
for (index = 0; index < MENU_CONFIG.length; index++) {
const item = MENU_CONFIG[index];
if (item.children && item.children.length > 0) {
if (rootState.route.path.indexOf(item.path) === 0) {
return item.children.map((menuRouter) => ({ ...menuRouter, path: `${item.path}/${menuRouter.path}` }));
}
}
}
}
return MENU_CONFIG;
},
showFooter: (state) => state.showFooter, showFooter: (state) => state.showFooter,
showSettingBtn: (state) => !state.showHeader, showSettingBtn: (state) => !state.showHeader,
mode: (state) => { mode: (state) => {

View File

@ -1,41 +1,93 @@
import request from '@/utils/request'; import { TOKEN_NAME } from '@/config/global';
const InitUserInfo = {
roles: [],
};
// 定义的state初始值 // 定义的state初始值
const state = { const state = {
loginName: '', token: localStorage.getItem(TOKEN_NAME),
deptNameString: '', userInfo: InitUserInfo,
}; };
const mutations = { const mutations = {
SET_USER_INFO(state, userInfo) { setToken(state, token) {
state.loginName = userInfo.EngName; localStorage.setItem(TOKEN_NAME, token);
state.deptNameString = userInfo.DeptNameString; state.token = token;
},
removeToken(state) {
localStorage.removeItem(TOKEN_NAME);
state.token = '';
},
setUserInfo(state, userInfo) {
state.userInfo = userInfo;
},
};
const getters = {
token: (state) => {
return state.token;
},
roles: (state) => {
return state.userInfo?.roles;
}, },
}; };
const actions = { const actions = {
async getUserInfo(context) { async login({ commit }, userInfo) {
try { const mockLogin = async (userInfo) => {
console.log('当前编译环境'); const { account, password } = userInfo;
console.log(process.env.NODE_ENV); if (account !== 'td') {
return {
code: 401,
message: '账号不存在',
};
}
if (['main_', 'dev_'].indexOf(password) === -1) {
return {
code: 401,
message: '密码错误',
};
}
const token = {
main_: 'main_token',
dev_: 'dev_token',
}[password];
return {
code: 200,
message: '登陆成功',
data: token,
};
};
if (process.env.NODE_ENV === 'development') { const res = await mockLogin(userInfo);
context.commit('SET_USER_INFO', { if (res.code === 200) {
EngName: 'user_test', commit('setToken', res.data);
DeptNameString: '虚拟部门test',
});
} else { } else {
request({ throw res;
method: 'get',
url: '/ts:auth/tauth/info.ashx',
baseURL: '',
}).then((res) => {
context.commit('SET_USER_INFO', res.data);
});
} }
} catch (err) { },
console.log(`智能网关获取用户信息错误:${err.message}`); async getUserInfo({ commit, state }) {
const mockRemoteUserInfo = async (token) => {
if (token === 'main_token') {
return {
name: 'td_main',
roles: ['ALL_ROUTERS'],
};
} }
return {
name: 'td_dev',
roles: ['userIndex', 'dashboardBase', 'login'],
};
};
const res = await mockRemoteUserInfo(state.token);
commit('setUserInfo', res);
},
async logout({ commit }) {
commit('removeToken');
commit('setUserInfo', InitUserInfo);
}, },
}; };
@ -44,4 +96,5 @@ export default {
state, state,
mutations, mutations,
actions, actions,
getters,
}; };

View File

@ -16,7 +16,7 @@
> .t-layout { > .t-layout {
flex: 1; flex: 1;
min-width: 760px; // 992 - 232 min-width: 760px;
} }
} }
@ -29,14 +29,24 @@
flex-direction: column; flex-direction: column;
} }
&-main-wrapper{
height: 500px;
overflow: scroll;
}
&-side-nav-layout { &-side-nav-layout {
&-relative { &-relative {
height: 100%; height: 100%;
} }
} }
&-layout{
height: calc(100vh - 64px);
overflow-y: scroll;
}
&-content-layout { &-content-layout {
margin: @spacer-3; padding: @spacer-3;
} }
&-footer-layout { &-footer-layout {

View File

@ -26,5 +26,8 @@ export default defineConfig({
server: { server: {
port: 3002, port: 3002,
proxy: {
'/api': 'http://127.0.0.1:3000/',
},
}, },
}); });