Browse Source

add dashboard sketch

pull/253/head
yedf2 4 years ago
parent
commit
a1ef5d876e
  1. 68
      dashboard/.eslintrc.js
  2. 24
      dashboard/.gitignore
  3. 1
      dashboard/README.md
  4. 13
      dashboard/index.html
  5. 5342
      dashboard/package-lock.json
  6. 36
      dashboard/package.json
  7. BIN
      dashboard/public/favicon.ico
  8. 6
      dashboard/src/App.vue
  9. BIN
      dashboard/src/assets/logo.png
  10. 42
      dashboard/src/components/SvgIcon/index.vue
  11. 63
      dashboard/src/layout/aside.vue
  12. 17
      dashboard/src/layout/components/content.vue
  13. 39
      dashboard/src/layout/components/header.vue
  14. 57
      dashboard/src/layout/components/sidebar.vue
  15. 12
      dashboard/src/layout/index.vue
  16. 17
      dashboard/src/main.ts
  17. 23
      dashboard/src/permission.ts
  18. 18
      dashboard/src/router/asyncRouter.ts
  19. 109
      dashboard/src/router/index.ts
  20. 2
      dashboard/src/store/index.ts
  21. 32
      dashboard/src/store/modules/layout.ts
  22. 11
      dashboard/src/type/index.d.ts
  23. 5
      dashboard/src/type/shim.vue.d.ts
  24. 30
      dashboard/src/type/store/layout.ts
  25. 23
      dashboard/src/utils/util.ts
  26. 9
      dashboard/src/views/Dashboard/DashboardPage1.vue
  27. 9
      dashboard/src/views/Dashboard/DashboardPage2.vue
  28. 21
      dashboard/tsconfig.json
  29. 32
      dashboard/vite.config.ts
  30. 3122
      dashboard/yarn.lock

68
dashboard/.eslintrc.js

@ -0,0 +1,68 @@
module.exports = {
parser: "vue-eslint-parser",
"env": {
"browser": true,
"es2021": true
},
"extends": [
"plugin:@typescript-eslint/recommended",
"plugin:vue/vue3-recommended"
],
"parserOptions": {
"parser": "@typescript-eslint/parser",
"sourceType": "module",
ecmaFeature: {
jsx: true,
tsx: true
}
},
"plugins": [
"@typescript-eslint"
],
"rules": {
'vue/max-attributes-per-line': ['error', {
singleline: 10,
multiline: {
max: 1,
allowFirstLine: false
}
}],
'vue/singleline-html-element-content-newline': 'off',
'vue/multiline-html-element-content-newline': 'off',
'vue/html-indent': ['error', 4],
indent: ['error', 4], // 4行缩进
'vue/script-indent': ['error', 4],
quotes: ['error', 'single'], // 单引号
// 'vue/html-quotes': ['error', 'single'],
semi: ['error', 'never'], // 禁止使用分号
'space-infix-ops': ['error', { int32Hint: false }], // 要求操作符周围有空格
'no-multi-spaces': 'error', // 禁止多个空格
'no-whitespace-before-property': 'error', // 禁止在属性前使用空格
'space-before-blocks': 'error', // 在块之前强制保持一致的间距
'space-before-function-paren': ['error', 'never'], // 在“ function”定义打开括号之前强制不加空格
'space-in-parens': ['error', 'never'], // 强制括号左右的不加空格
'spaced-comment': ['error', 'always'], // 注释间隔
'template-tag-spacing': ['error', 'always'], // 在模板标签及其文字之间需要空格
'no-var': 'error',
'prefer-destructuring': ['error', { // 优先使用数组和对象解构
array: true,
object: true
}, {
enforceForRenamedProperties: false
}],
'comma-dangle': ['error', 'never'], // 最后一个属性不允许有逗号
'arrow-spacing': 'error', // 箭头函数空格
'prefer-template': 'error',
'template-curly-spacing': 'error',
'quote-props': ['error', 'as-needed'], // 对象字面量属性名称使用引号
'object-curly-spacing': ['error', 'always'], // 强制在花括号中使用一致的空格
'no-unneeded-ternary': 'error', // 禁止可以表达为更简单结构的三元操作符
'no-restricted-syntax': ['error', 'WithStatement', 'BinaryExpression[operator="in"]'], // 禁止with/in语句
'no-lonely-if': 'error', // 禁止 if 语句作为唯一语句出现在 else 语句块中
'newline-per-chained-call': ['error', { ignoreChainWithDepth: 2 }], // 要求方法链中每个调用都有一个换行符
// 路径别名设置
'no-submodule-imports': ['off', '/@'],
'no-implicit-dependencies': ['off', ['/@']],
'@typescript-eslint/no-explicit-any': 'off' // 类型可以使用any
}
}

24
dashboard/.gitignore

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

1
dashboard/README.md

@ -0,0 +1 @@
# DTM-Admin

13
dashboard/index.html

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

5342
dashboard/package-lock.json

File diff suppressed because it is too large

36
dashboard/package.json

@ -0,0 +1,36 @@
{
"name": "dtm-admin",
"private": true,
"version": "0.0.0",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"ant-design-vue": "^3.1.1",
"vue": "^3.2.25"
},
"devDependencies": {
"@types/node": "^17.0.23",
"@types/nprogress": "^0.2.0",
"@typescript-eslint/eslint-plugin": "^5.18.0",
"@typescript-eslint/parser": "^5.18.0",
"@vitejs/plugin-vue": "^2.3.0",
"autoprefixer": "^10.4.4",
"axios": "^0.26.1",
"eslint": "^8.13.0",
"eslint-plugin-vue": "^8.6.0",
"nprogress": "^0.2.0",
"pinia": "^2.0.0-rc.10",
"postcss": "^8.4.12",
"postcss-import": "^14.1.0",
"postcss-nested": "^5.0.6",
"postcss-simple-vars": "^6.0.3",
"typescript": "^4.5.4",
"vite": "^2.9.1",
"vite-plugin-svg-icons": "^1.1.0",
"vue-router": "^4.0.13",
"vue-tsc": "^0.29.8"
}
}

BIN
dashboard/public/favicon.ico

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

6
dashboard/src/App.vue

@ -0,0 +1,6 @@
<template>
<router-view />
</template>
<script setup lang="ts"></script>

BIN
dashboard/src/assets/logo.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

42
dashboard/src/components/SvgIcon/index.vue

@ -0,0 +1,42 @@
<template>
<div v-if="isExternal" :style="styleExternalIcon" class="svg-external-icon svg-icon" />
<svg v-else :class="svgClass" aria-hidden="true">
<use :xlink:href="iconName" />
</svg>
</template>
<script setup lang='ts'>
const props = defineProps({
iconClass: {
type: String,
required: true
},
className: {
type: String,
default: ''
}
})
const isExternal = /^(https?:|mailto:|tel:)/.test(props.iconClass)
const iconName = `#icon-${props.iconClass}`
const svgClass = props.className ? `svg-icon ${props.className}` : 'svg-icon '
const styleExternalIcon = () => {
return {
mask: `url(${props.iconClass}) no-repeat 50% 50%`,
'-webkit-mask': `url(${props.iconClass}) no-repeat 50% 50%`
}
}
</script>
<style lang="postcss" scoped>
.svg-icon {
vertical-align: -0.15em;
fill: currentColor;
overflow: hidden;
}
.svg-external-icon {
background-color: currentColor;
mask-size: cover !important;
display: inline-block;
}
</style>

63
dashboard/src/layout/aside.vue

@ -0,0 +1,63 @@
<template>
<a-layout style="height: 730px">
<a-layout-sider width="200" style="background: #fff">
<Sidebar />
</a-layout-sider>
<a-layout style="padding: 0 24px 24px">
<a-breadcrumb style="margin: 16px 0">
<a-breadcrumb-item>{{ mainNav }}</a-breadcrumb-item>
<a-breadcrumb-item>{{ subNav }}</a-breadcrumb-item>
<a-breadcrumb-item>{{ page }}</a-breadcrumb-item>
</a-breadcrumb>
<a-layout-content
:style="{ background: '#fff', padding: '24px', margin: 0, minHeight: '280px' }"
>
<Content />
</a-layout-content>
</a-layout>
</a-layout>
</template>
<script setup lang='ts'>
import Sidebar from './components/sidebar.vue'
import Content from './components/content.vue'
import { useRoute } from 'vue-router'
import { useLayoutStore } from '../store/modules/layout'
import { IMenubarList } from '../type/store/layout'
import { findCurrentMenubar } from '../utils/util'
import { computed, onMounted, ref } from 'vue'
const route = useRoute()
const { getMenubar } = useLayoutStore()
const mainNav = computed(() => {
const currentMenubar = findCurrentMenubar(getMenubar.menuList, true)
return currentMenubar?.meta.title
})
const subNav = computed(() => {
let subNav = ''
const currentMenubar = findCurrentMenubar(getMenubar.menuList, true)
currentMenubar.children?.forEach(v => {
if (route.path.indexOf(v.path) !== -1) {
subNav = v.meta.title
}
})
return subNav
})
const page = computed(() => {
let page = ''
const currentMenubar = findCurrentMenubar(getMenubar.menuList, true)
currentMenubar.children?.forEach(v => {
v.children?.forEach(vv => {
if (route.path == vv.path) {
page = vv.meta.title
}
})
})
return page
})
</script>

17
dashboard/src/layout/components/content.vue

@ -0,0 +1,17 @@
<template>
<router-view v-slot="{Component}">
<transition name="fade-transform" mode="out-in">
<keep-alive>
<component :is="Component" :key="key" />
</keep-alive>
</transition>
</router-view>
</template>
<script setup lang='ts'>
import { computed } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
const key = computed(() => route.path)
</script>

39
dashboard/src/layout/components/header.vue

@ -0,0 +1,39 @@
<template>
<div>
<a-layout-header class="header">
<svg-icon class="logo" style="width: 36px; height: 36px;" icon-class="svg-dtm" />
<a-menu
v-model:selectedKeys="activeMenu"
theme="dark"
mode="horizontal"
:style="{ lineHeight: '64px' }"
@select="onOpenChange"
>
<a-menu-item v-for="v in getMenubar.menuList" :key="v.path">{{ v.meta.title }}</a-menu-item>
</a-menu>
</a-layout-header>
</div>
</template>
<script setup lang='ts'>
import { computed, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useLayoutStore } from '/@/store/modules/layout'
const route = useRoute()
const router = useRouter()
const { getMenubar } = useLayoutStore()
const firstRedirectPath = '/dashboard'
const activeMenu = ref([route.meta.activeMenu !== firstRedirectPath ? route.meta.activeMenu : '/'])
const onOpenChange = (d:any) => {
router.push({ path: d.key })
}
</script>
<style scoped>
.logo {
float: left;
margin: 16px 24px 16px 0;
}
</style>

57
dashboard/src/layout/components/sidebar.vue

@ -0,0 +1,57 @@
<template>
<a-menu
v-model:selectedKeys="activeMenu"
v-model:openKeys="openKeys"
mode="inline"
:style="{ height: '100%', borderRight: 0 }"
@select="onOpenChange"
>
<a-sub-menu v-for="v in filterSubMenubarData" :key="v.path">
<template #title>
<span>
{{ v.meta.title }}
</span>
</template>
<a-menu-item v-for="vv in v.children" :key="vv.path">{{ vv.meta.title }}</a-menu-item>
</a-sub-menu>
</a-menu>
</template>
<script setup lang='ts'>
import { ref, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useLayoutStore } from '/@/store/modules/layout'
import { IMenubarList } from '/@/type/store/layout'
import { findCurrentMenubar } from '/@/utils/util'
const route = useRoute()
const router = useRouter()
const { getMenubar } = useLayoutStore()
const filterSubMenubarData = computed(() => {
return findCurrentMenubar(getMenubar.menuList) as IMenubarList[]
})
const activeMenu = computed({
get: () => {
return [route.path]
},
set: (val) => {
// do nothing, just for eliminate warn
}
})
const openKeys = computed({
get: () => {
const pos = route.path.lastIndexOf('/')
return [route.path.substring(0, pos)]
},
set: (val) => {
// do onthing, just for eliminate warn
}
})
const onOpenChange = (d:any) => {
router.push({ path: d.key })
}
</script>

12
dashboard/src/layout/index.vue

@ -0,0 +1,12 @@
<script setup lang="ts">
import Header from './components/header.vue'
</script>
<template>
<a-layout>
<a-layout-header class="header">
<Header />
</a-layout-header>
<router-view />
</a-layout>
</template>

17
dashboard/src/main.ts

@ -0,0 +1,17 @@
import { createApp } from 'vue'
import App from './App.vue'
import router from '/@/router/index'
import { pinia } from '/@/store'
import '/@/permission'
import Antd from 'ant-design-vue'
import 'ant-design-vue/dist/antd.css'
import 'virtual:svg-icons-register'
import SvgIcon from '/@/components/SvgIcon/index.vue'
const app = createApp(App)
app.use(Antd)
app.use(router)
app.use(pinia)
app.component('SvgIcon', SvgIcon)
app.mount('#app')

23
dashboard/src/permission.ts

@ -0,0 +1,23 @@
import router from '/@/router'
import { configure, start, done } from 'nprogress'
import { useLayoutStore } from './store/modules/layout'
configure({ showSpinner: false })
const defaultRoutePath = '/'
router.beforeEach((to) => {
start()
const { getMenubar, concatAllowRoutes } = useLayoutStore()
if (getMenubar.menuList.length === 0) {
concatAllowRoutes()
return to.fullPath
}
})
router.afterEach(() => {
done()
})

18
dashboard/src/router/asyncRouter.ts

@ -0,0 +1,18 @@
const modules = import.meta.glob('../views/**/**.vue')
const components: IObject<() => Promise<typeof import('*.vue')>> = {
LayoutHeader: (() => import('/@/layout/index.vue')) as unknown as () => Promise<typeof import('*.vue')>
}
Object.keys(modules).forEach(key => {
const nameMatch = key.match(/^\.\.\/views\/(.+)\.vue/)
if (!nameMatch) return
if (nameMatch[1].includes('_Components')) return
const indexMatch = nameMatch[1].match(/(.*)\/Index$/i)
let name = indexMatch ? indexMatch[1] : nameMatch[1];
[name] = name.split('/').splice(-1)
components[name] = modules[key] as () => Promise<typeof import('*.vue')>
})
export {
components
}

109
dashboard/src/router/index.ts

@ -0,0 +1,109 @@
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import { IMenubarList } from '../type/store/layout';
import { components } from './asyncRouter';
const Components: IObject<() => Promise<typeof import('*.vue')>> = Object.assign({}, components, {
LayoutHeader: (() => import('/@/layout/index.vue')) as unknown as () => Promise<typeof import('*.vue')>,
LayoutMain: (() => import('/@/layout/aside.vue')) as unknown as () => Promise<typeof import('*.vue')>
})
export const allowRouter: Array<IMenubarList> = [
{
name: 'Dashboard',
path: '/',
redirect: '/dashboard/sub1/1',
component: Components['LayoutHeader'],
meta: { title: '首页', activeMenu: '/dashboard' },
children: [
{
name: 'SubNav1',
path: '/dashboard/sub1',
component: Components['LayoutMain'],
meta: { title: '子导航1' },
children: [
{
name: 'Page1',
path: '/dashboard/sub1/1',
component: Components['DashboardPage1'],
meta: { title: '子页面1' },
}, {
name: 'Page2',
path: '/dashboard/sub1/2',
component: Components['DashboardPage2'],
meta: { title: '子页面2' },
}
]
}, {
name: 'SubNav2',
path: '/dashboard/sub2',
component: Components['LayoutMain'],
meta: { title: '子导航2' },
children: [
{
name: 'Page21',
path: '/dashboard/sub2/1',
component: Components['DashboardPage1'],
meta: { title: '子页面21' },
}, {
name: 'Page22',
path: '/dashboard/sub2/2',
component: Components['DashboardPage2'],
meta: { title: '子页面22' },
}
]
}
]
}, {
name: 'Nav2',
path: '/nav2',
redirect: '/nav2/sub2/1',
component: Components['LayoutHeader'],
meta: { title: '导航2', activeMenu: '/nav2' },
children: [
{
name: 'Nav2Sub1',
path: '/nav2/sub1',
component: Components['LayoutMain'],
meta: { title: '子导航1' },
children: [
{
name: 'Nav2Page1',
path: '/nav2/sub1/1',
component: Components['DashboardPage1'],
meta: { title: '子页面1' },
}, {
name: 'Nav2Page2',
path: '/nav2/sub1/2',
component: Components['DashboardPage2'],
meta: { title: '子页面2' },
}
]
}, {
name: 'Nav2Sub2',
path: '/nav2/sub2',
component: Components['LayoutMain'],
meta: { title: '子导航2' },
children: [
{
name: 'Nav2Page21',
path: '/nav2/sub2/1',
component: Components['DashboardPage1'],
meta: { title: '子页面21' },
}, {
name: 'Nav2Page22',
path: '/nav2/sub2/2',
component: Components['DashboardPage2'],
meta: { title: '子页面22' },
}
]
}
]
}
]
const router = createRouter({
history: createWebHistory(),
routes: allowRouter as RouteRecordRaw[]
})
export default router

2
dashboard/src/store/index.ts

@ -0,0 +1,2 @@
import { createPinia } from 'pinia'
export const pinia = createPinia()

32
dashboard/src/store/modules/layout.ts

@ -0,0 +1,32 @@
import { defineStore } from 'pinia';
import { allowRouter } from '/@/router';
import { ILayout, IMenubar, IMenubarList, IStatus } from '/@/type/store/layout';
import { findCurrentMenubar } from '/@/utils/util';
export const useLayoutStore = defineStore({
id: 'layout',
state: ():ILayout => ({
menubar: {
menuList: []
},
status: {
isLoading: false
},
}),
getters: {
getMenubar(): IMenubar {
return this.menubar
},
getStatus(): IStatus {
return this.status
},
},
actions: {
setRoutes(data: Array<IMenubarList>): void {
this.menubar.menuList = data
},
concatAllowRoutes(): void {
allowRouter.reverse().forEach(v => this.menubar.menuList.unshift(v))
},
}
})

11
dashboard/src/type/index.d.ts

@ -0,0 +1,11 @@
export {}
declare global {
interface IObject<T> {
[index: string]: T
}
interface ImportMetaEnv {
VITE_APP_TITLE: string
VITE_PORT: number
VITE_PROXY: string
}
}

5
dashboard/src/type/shim.vue.d.ts

@ -0,0 +1,5 @@
declare module '*.vue' {
import { defineComponent } from 'vue';
const Component: ReturnType<typeof defineComponent>
export default Component
}

30
dashboard/src/type/store/layout.ts

@ -0,0 +1,30 @@
export interface IMenubar {
menuList: Array<IMenubarList>
}
export interface ILayout {
menubar: IMenubar
status: IStatus
}
export interface IStatus {
isLoading: boolean
}
export interface IMenubarList {
parentId?: number | string
id?: number | string
name: string
path: string
redirect?: string
meta: {
icon?: string
title: string
permission?: string[]
activeMenu?: string
hidden?: boolean
alwaysShow?: boolean
}
component: (() => Promise<typeof import('*.vue')>) | string
children?: Array<IMenubarList>
}

23
dashboard/src/utils/util.ts

@ -0,0 +1,23 @@
import { useRoute } from 'vue-router';
import { IMenubarList } from '../type/store/layout';
export const findCurrentMenubar = (menuList: IMenubarList[], root?:boolean) => {
const route = useRoute()
let arr:IMenubarList[] | IMenubarList = []
for (let i = 0; i < menuList.length; i++) {
const v = menuList[i];
const usePath = v.meta.activeMenu || v.redirect || v.path;
const pos = usePath.lastIndexOf('/')
const rootPath = pos == 0 ? usePath : usePath.substring(0, pos)
if (route.path.indexOf(rootPath) !== -1) {
if (!root) {
arr = v.children as IMenubarList[]
} else {
arr = v
}
break
}
}
return arr
}

9
dashboard/src/views/Dashboard/DashboardPage1.vue

@ -0,0 +1,9 @@
<script setup lang='ts'>
</script>
<template>
<div>
<h1>Page1</h1>
</div>
</template>

9
dashboard/src/views/Dashboard/DashboardPage2.vue

@ -0,0 +1,9 @@
<script setup lang='ts'>
</script>
<template>
<div>
<h1>Page2</h1>
</div>
</template>

21
dashboard/tsconfig.json

@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "esnext",
"useDefineForClassFields": true,
"module": "esnext",
"moduleResolution": "node",
"strict": true,
"jsx": "preserve",
"sourceMap": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"baseUrl": ".",
"paths": {
"/@/*": ["src/*"],
},
"lib": ["esnext", "dom"],
"types": ["vite/client", "node"]
},
"include": ["**/*.ts", "**/*.d.ts", "**/*.tsx", "**/*.vue"],
"exclude": ["node_modules"]
}

32
dashboard/vite.config.ts

@ -0,0 +1,32 @@
import { ConfigEnv, UserConfigExport } from 'vite';
import path from 'path';
import vue from '@vitejs/plugin-vue';
import viteSvgIcons from 'vite-plugin-svg-icons';
const setAlias = (alias: [string, string][]) => alias.map((v) => {
return { find: v[0], replacement: path.resolve(__dirname, v[1]) };
});
export default ({}: ConfigEnv): UserConfigExport => {
return {
resolve: {
alias: setAlias([['/@', 'src']]),
},
plugins: [
vue(),
viteSvgIcons({
iconDirs: [path.resolve(process.cwd(), 'src/icons')],
symbolId: 'icon-[dir]-[name]'
})
],
css: {
postcss: {
plugins: [
require('autoprefixer'),
require('postcss-simple-vars'),
require('postcss-import')
]
}
}
};
};

3122
dashboard/yarn.lock

File diff suppressed because it is too large
Loading…
Cancel
Save