Commit 2cd72319 authored by Sendya's avatar Sendya

refactor: pro-layout v5 begin (vue3 support)

parent b2c22607
{
"env": {
"test": {
"presets": ["@babel/preset-env"],
"plugins": [
"@babel/plugin-transform-runtime",
"transform-vue-jsx",
"transform-object-assign",
"transform-class-properties"
]
}
"env": {
"test": {
"presets": [["@babel/preset-env", { "targets": { "node": "current" } }]],
"plugins": [
"@vue/babel-plugin-jsx",
"@babel/plugin-transform-member-expression-literals",
"@babel/plugin-transform-object-assign",
"@babel/plugin-transform-property-literals",
"@babel/plugin-transform-spread",
"@babel/plugin-transform-template-literals",
"@babel/plugin-proposal-export-default-from",
"@babel/plugin-proposal-export-namespace-from",
"@babel/plugin-proposal-object-rest-spread",
"@babel/plugin-proposal-class-properties"
]
}
}
}
......@@ -2,3 +2,4 @@ node_modules/
**/*.spec.*
**/style/
*.html
src/icons
{
"root": true,
"env": {
"browser": true,
"node": true,
"jasmine": true,
"jest": true,
"es6": true
},
"parserOptions": {
"parser": "babel-eslint"
},
"extends": ["plugin:vue/strongly-recommended", "prettier"],
"rules": {
"no-var": "error",
"camelcase": "off",
"no-extra-boolean-cast": "off",
"semi": [2, "never", {"beforeStatementContinuationChars": "never"}],
"vue/require-prop-types": "off",
"vue/require-default-prop": "off",
"vue/no-reserved-keys": "off",
"vue/prop-name-casing": "off",
"vue/max-attributes-per-line": [
2,
{
"singleline": 20,
"multiline": {
"max": 1,
"allowFirstLine": false
}
}
]
}
}
module.exports = {
root: true,
env: {
node: true,
},
extends: [
'plugin:vue/vue3-essential',
'eslint:recommended',
'@vue/typescript/recommended',
'@vue/prettier',
'@vue/prettier/@typescript-eslint',
],
parserOptions: {
ecmaVersion: 2020,
},
rules: {
'@typescript-eslint/no-explicit-any': 0,
'@typescript-eslint/no-inferrable-types': 0,
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
},
};
......@@ -29,4 +29,12 @@ build/
.vscode
# publish
*.tgz
\ No newline at end of file
*.tgz
/icons
/*.js
/*.d.ts
!.jest.js
!Icon.d.ts
!.fatherrc.js
!.eslintrc.js
\ No newline at end of file
const libDir = process.env.LIB_DIR;
const transformIgnorePatterns = [
'/dist/', 'node_modules\/[^/]+?\/(?!(es|node_modules)\/)', // Ignore modules without es dir
'/dist/',
'node_modules/[^/]+?/(?!(es|node_modules)/)', // Ignore modules without es dir
];
module.exports = {
testURL: 'http://localhost/',
moduleFileExtensions: [
"js",
"jsx",
"json",
"vue",
],
moduleFileExtensions: ['js', 'ts', 'tsx', 'json', 'vue'],
modulePathIgnorePatterns: ['/_site/'],
testPathIgnorePatterns: [
'/node_modules/', 'node'
],
testPathIgnorePatterns: ['/node_modules/', 'node'],
transform: {
".*\\.(vue)$": "<rootDir>/node_modules/vue-jest",
"^.+\\.(js|jsx)$": "<rootDir>/node_modules/babel-jest"
'.*\\.(vue)$': '<rootDir>/node_modules/vue-jest',
'^.+\\.(ts|tsx)$': '<rootDir>/node_modules/babel-jest',
},
testRegex: libDir === 'dist'
? 'demo\\.test\\.js$'
: '.*\\.test\\.js$',
testRegex: libDir === 'dist' ? 'demo\\.test\\.ts$' : '.*\\.test\\.tsx$',
moduleNameMapper: {
"^@/(.*)$": "<rootDir>/$1",
'^@/(.*)$': '<rootDir>/$1',
},
snapshotSerializers: ["<rootDir>/node_modules/jest-serializer-vue"],
snapshotSerializers: ['<rootDir>/node_modules/jest-serializer-vue'],
collectCoverage: process.env.COVERAGE === 'true',
collectCoverageFrom: [
"src/**/*.{js,jsx,vue}",
],
transformIgnorePatterns
collectCoverageFrom: ['src/**/*.{ts,tsx,vue}'],
transformIgnorePatterns,
verbose: true,
};
tests/
test/
examples/
src/
scripts/
.eslintignore
.eslintrc.js
.fatherrc.js
.gitignore
jest.config.js
.jest.js
tsconfig.json
tslint.json
English | [简体中文](./README.zh-CN.md)
<h1 align="center">
Ant Design Pro Layout
</h1>
<h1 align="center">Ant Design Pro Layout</h1>
<div align="center">
## Usage
[![NPM version](https://img.shields.io/npm/v/@ant-design/icons-vue.svg?style=flat)](https://npmjs.org/package/@ant-design-vue/pro-layout)
[![NPM downloads](http://img.shields.io/npm/dm/@ant-design/icons-vue.svg?style=flat)](https://npmjs.org/package/@ant-design-vue/pro-layout)
</div>
## Install
```bash
npm i @ant-design-vue/pro-layout --save
// or
# yarn
yarn add @ant-design-vue/pro-layout
# npm
npm i @ant-design-vue/pro-layout -S
```
```jsx
import ProLayout from '@ant-design-vue/pro-layout'
## Basic Usage
// by jsx
export default {
name: 'BasicLayout',
render () {
return (
<ProLayout>
<router-view />
</ProLayout>
)
}
}
First, you should add the icons that you need into the library.
```js
import { createApp } from 'vue'
import ProLayout, { PageContainer } from '@ant-design-vue/pro-layout';
const app = createApp();
app.use(ProLayout)
.use(PageContainer)
.mount('#app')
```
After that, you can use pro-layout in your Vue components as simply as this:
```vue
<template>
<pro-layout
:menus="menus"
:collapsed="collapsed"
:theme="theme"
:layout="layout"
:contentWidth="contentWidth"
:auto-hide-header="autoHideHeader"
:mediaQuery="query"
:isMobile="isMobile"
:handleMediaQuery="handleMediaQuery"
:handleCollapse="handleCollapse"
>
<template v-slot:menuHeaderRender>
<div>
<img src="../assets/logo.svg" />
<h1>Pro Layout</h1>
</div>
</template>
<template v-slot:rightContentRender>
<div :class="['ant-pro-global-header-index-right', layout === 'topmenu' && `ant-pro-global-header-index-${theme}`]">
rightContentRender
</div>
</template>
<template v-slot:footerRender>
<div>footerRender</div>
</template>
<setting-drawer navTheme="dark" />
<router-view />
</pro-layout>
<pro-layout>
<router-view />
</pro-layout>
</template>
```
or `TSX`
```jsx
import ProLayout from '@ant-design-vue/pro-layout'
<script>
// by template
import ProLayout, { SettingDrawer } from '@ant-design-vue/pro-layout'
import { asyncRouterMap } from '../config/router.config'
export default {
name: 'BasicLayout',
data () {
return {
menus: [],
collapsed: false,
autoHideHeader: false,
query: {},
layout: 'sidemenu',
contentWidth: 'Fluid',
theme: 'dark',
isMobile: false
}
},
created () {
this.menus = asyncRouterMap.find(item => item.path === '/').children
},
methods: {
handleMediaQuery (query) {
this.query = query
if (this.isMobile && !query['screen-xs']) {
this.isMobile = false
return
}
if (!this.isMobile && query['screen-xs']) {
this.isMobile = true
this.collapsed = false
}
},
handleCollapse (collapsed) {
this.collapsed = collapsed
export default defineComponent({
setup () {
return (): JSX.Element => (
<ProLayout>
<RouterView />
</ProLayout>
)
}
},
components: {
SettingDrawer
}
}
</script>
})
```
## Build project
## API
### ProLayout
| Property | Description | Type | Default Value |
| --- | --- | --- | --- |
| title | layout in the upper left corner title | VNode \| String | `'Ant Design Pro'` |
| logo | layout top left logo url | VNode \| render | - |
| loading`*` | layout loading status | boolean | - |
| layout | layout menu mode, sidemenu: right navigation, topmenu: top navigation | 'sidemenu' \| 'topmenu' | `'sidemenu'` |
| contentWidth | content mode of layout, Fluid: adaptive, Fixed: fixed width 1200px | 'Fixed' \| 'Fluid' | `Fluid` |
| theme | Navigation menu theme | 'light' \| 'dark' | `'dark'` |
| menus | Vue-router `routes` prop | Object | `[{}]` |
| collapsed | control menu's collapse and expansion | boolean | true |
| isMobile | is mobile | boolean | false |
| handleCollapse | folding collapse event of menu | (collapsed: boolean) => void | - |
| menuHeaderRender | render logo and title | v-slot \| VNode \| (logo,title)=>VNode \| false | - |
| headerRender | custom header render method | (props: BasicLayoutProps) => VNode | - |
| rightContentRender | header right content render method | (props: HeaderViewProps) => VNode | - |
| collapsedButtonRender | custom collapsed button method | (collapsed: boolean) => VNode | - |
| footerRender | custom footer render method | (props: BasicLayoutProps) => VNode | - |
| breadcrumbRender | custom breadcrumb render method | ({ route, params, routes, paths, h }) => VNode[] | - |
| i18nRender | i18n | Function (key: string) => string \| `false` | `false` |
| handleMediaQuery | media matchs callback | (querys: []) => void | - |
| mediaQuery | media matchs | Array | - |
### PageHeaderWrapper
| Property | Description | Type | Default Value |
| --- | --- | --- | --- |
| content | Content area | VNode \| v-slot | - |
| extra | Extra content area, on the right side of content | VNode \| v-slot | - |
| extraContent | Extra content area, on the right side of content | VNode \| v-slot | - |
| tabList | Tabs title list | `Array<{key: string, tab: sting}>` | - |
| tab-change | Switch panel callback | (key) => void | - |
| tab-active-key | The currently highlighted tab item | string | - |
### SettingDrawer
#### {settings}
| Property | Description | Type | Default Value |
| ---- | ---- | ---- | ---- |
| theme | Theme | `dark` `light` `realDark` | `light` |
| layout | Sider Layout | `sidemenu` `topmenu` | `sidemenu` |
| primaryColor | Primary color (*development only) | `#1890ff` | |
| contentWidth | content mode of layout, Fluid: adaptive, Fixed: fixed width 1200px | 'Fixed' \| 'Fluid' | `Fluid` |
```bash
npm run generate # Generate files to ./src
npm run compile # Build library
npm run test # Runing Test
```
[English](./README.md) | 简体中文
<h1 align="center">Ant Design Pro Layout</h1>
## 使用
```bash
npm i @ant-design-vue/pro-layout --save
// 或者
yarn add @ant-design-vue/pro-layout
```
```jsx
import ProLayout from '@ant-design-vue/pro-layout'
export default {
name: 'BasicLayout',
render () {
return (
<ProLayout>
<router-view />
</ProLayout>
)
}
}
```
```vue
<template>
<pro-layout
:menus="menus"
:collapsed="collapsed"
:theme="theme"
:layout="layout"
:contentWidth="contentWidth"
:auto-hide-header="autoHideHeader"
:mediaQuery="query"
:isMobile="isMobile"
:handleMediaQuery="handleMediaQuery"
:handleCollapse="handleCollapse"
>
<template v-slot:menuHeaderRender>
<div>
<img src="../assets/logo.svg" />
<h1>Pro Layout</h1>
</div>
</template>
<template v-slot:rightContentRender>
<div :class="['ant-pro-global-header-index-right', layout === 'topmenu' && `ant-pro-global-header-index-${theme}`]">
rightContentRender
</div>
</template>
<template v-slot:footerRender>
<div>footerRender</div>
</template>
<setting-drawer navTheme="dark" />
<router-view />
</pro-layout>
</template>
<script>
import ProLayout, { SettingDrawer } from '@ant-design-vue/pro-layout'
import { asyncRouterMap } from '../config/router.config'
export default {
name: 'BasicLayout',
data () {
return {
menus: [],
collapsed: false,
autoHideHeader: false,
query: {},
layout: 'sidemenu',
contentWidth: 'Fluid',
theme: 'dark',
isMobile: false
}
},
created () {
this.menus = asyncRouterMap.find(item => item.path === '/').children
},
methods: {
handleMediaQuery (query) {
this.query = query
if (this.isMobile && !query['screen-xs']) {
this.isMobile = false
return
}
if (!this.isMobile && query['screen-xs']) {
this.isMobile = true
this.collapsed = false
}
},
handleCollapse (collapsed) {
this.collapsed = collapsed
}
},
components: {
SettingDrawer
}
}
</script>
```
## API
### ProLayout
| Property | Description | Type | Default Value |
| --- | --- | --- | --- |
| title | layout 的 左上角 的 title | VNode \| String | `'Ant Design Pro'` |
| logo | layout 的 左上角 logo 的 url | VNode \| render | - |
| loading`*` | layout 的加载态 | boolean | - |
| menuHeaderRender | 渲染 logo 和 title | v-slot \| VNode \| (logo,title)=>VNode \| false | - |
| layout | layout 的菜单模式, sidemenu: 右侧导航, topmenu: 顶部导航 | 'sidemenu' \| 'topmenu' | `'sidemenu'` |
| contentWidth | layout 的内容模式,Fluid:自适应,Fixed:定宽 1200px | 'Fixed' \| 'Fluid' | `Fluid` |
| theme | 导航的主题 | 'light' \| 'dark' | `'dark'` |
| menus | Vue-router `routes` 属性 | Object | `[{}]` |
| collapsed | 控制菜单的收起和展开 | boolean | true |
| isMobile | 是否为手机模式 | boolean | false |
| handleCollapse | 菜单的折叠收起事件 | (collapsed: boolean) => void | - |
| headerRender | 自定义头的 render 方法 | (props: BasicLayoutProps) => VNode | - |
| rightContentRender | 自定义头右部的 render 方法 | (props: HeaderViewProps) => VNode | - |
| collapsedButtonRender | 自定义 侧栏收缩按钮 的方法 | (collapsed: boolean) => VNode | - |
| footerRender | 自定义 底部区域内容 | (props: BasicLayoutProps) => VNode | - |
| breadcrumbRender | 自定义面包屑渲染方法 | ({ route, params, routes, paths, h }) => VNode[] | - |
| i18nRender | 本地化渲染函数 (this.$t) | Function (key: string) => string \| `false` | `false` |
| handleMediaQuery | 媒体查询回调 | (querys: []) => void | - |
| mediaQuery | ProLayout 当前的媒体查询 | Array | - |
### PageHeaderWrapper
| Property | Description | Type | Default Value |
| --- | --- | --- | --- |
| content | 内容区 | VNode \| v-slot | - |
| extra | 扩展区域 | VNode \| v-slot | - |
| extraContent | 扩展内容区 | VNode \| v-slot | - |
| tabList | Tabs 导航 | `Array<{key: string, tab: sting}>` | - |
| tab-change | Tab 改变事件 | (key) => void | - |
| tab-active-key | 当前 Tab 选中项 | string | - |
### SettingDrawer
#### {settings}
| Property | Description | Type | Default Value |
| ---- | ---- | ---- | ---- |
| theme | 主题 | `dark` `light` `realDark` | `light` |
| layout | 布局模式 | `sidemenu` `topmenu` | `sidemenu` |
| primaryColor | 主色调 (*仅开发环境生效) | `#1890ff` | |
> 1%
last 2 versions
not ie <= 10
NODE_ENV=production
IS_ANALYZ=true
module.exports = {
root: true,
env: {
node: true
},
'extends': [
'plugin:vue/strongly-recommended',
'@vue/standard'
],
rules: {
// 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
// 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'no-console': 'off',
'no-debugger': 'off',
'generator-star-spacing': 'off',
'no-mixed-operators': 0,
'vue/max-attributes-per-line': [
2,
{
'singleline': 5,
'multiline': {
'max': 1,
'allowFirstLine': false
}
}
],
'vue/attribute-hyphenation': 0,
'vue/html-self-closing': 0,
'vue/component-name-in-template-casing': 0,
'vue/html-closing-bracket-spacing': 0,
'vue/singleline-html-element-content-newline': 0,
'vue/no-unused-components': 0,
'vue/multiline-html-element-content-newline': 0,
'vue/no-use-v-if-with-v-for': 0,
'vue/html-closing-bracket-newline': 0,
'vue/no-parsing-error': 0,
'no-tabs': 0,
'quotes': [
2,
'single',
{
'avoidEscape': true,
'allowTemplateLiterals': true
}
],
'semi': [
2,
'never',
{
'beforeStatementContinuationChars': 'never'
}
],
'no-delete-var': 2,
'prefer-const': [
2,
{
'ignoreReadBeforeAssign': false
}
],
'template-curly-spacing': 'off',
'indent': 'off'
},
parserOptions: {
parser: 'babel-eslint'
},
overrides: [
{
files: [
'**/__tests__/*.{j,t}s?(x)',
'**/tests/unit/**/*.spec.{j,t}s?(x)'
],
env: {
jest: true
}
}
]
}
const plugins = []
// lazy load ant-design-vue
// if your use import on Demand, Use this code
plugins.push(['import', {
'libraryName': 'ant-design-vue',
'libraryDirectory': 'es',
'style': true // `style: true` 会加载 less 文件
}])
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset',
[
'@babel/preset-env',
{
'useBuiltIns': 'entry',
'corejs': 3
}
]
],
plugins
}
import { createApp, onMounted, reactive, watch, watchEffect } from 'vue';
import { menus } from './menus';
import { MenuTheme } from '../src/typings';
import { Card, Space, Button, Switch } from 'ant-design-vue';
import BaseMenu, { useMenuState, MenuMode } from '../src/SiderMenu/BaseMenu';
import * as Icon from '@ant-design/icons-vue';
import 'ant-design-vue/dist/antd.less';
const BaseMenuDemo = {
setup() {
const state = reactive({
theme: 'dark' as MenuTheme,
mode: 'inline' as MenuMode,
themeChecked: true,
modeChecked: true,
})
const { state: menuState } = useMenuState({
collapsed: false,
openKeys: ['/dashboard'] as string[],
selectedKeys: ['/dashboard/monitor'] as string[],
})
onMounted(() => {
watch(() => state.themeChecked, (val) => {
state.theme = val ? 'dark' : 'light'
})
watch(() => state.modeChecked, (val) => {
state.mode = val ? 'inline' : 'horizontal'
})
})
return () => (
<div class="components">
<h2># BaseMenu</h2>
<Card style={{ marginBottom: '24px', background: 'rgb(244,244,244)' }}>
<Space size="middle">
<Button
disabled={state.mode !== 'inline'}
type="primary"
onClick={() => {
menuState.collapsed = !menuState.collapsed
}}
>
{menuState.collapsed ? '展开' : '收起'}
</Button>
<Switch checkedChildren="dark" unCheckedChildren="light" v-model={[state.themeChecked, 'checked']} />
<Switch checkedChildren="inline" unCheckedChildren="horizontal" v-model={[state.modeChecked, 'checked']} />
</Space>
<div style={{ margin: '12px 0' }}>
<p>SelectedKeys: { JSON.stringify(menuState.selectedKeys) }</p>
<p>OpenKeys: { JSON.stringify(menuState.openKeys) }</p>
<p>Collapsed: { JSON.stringify(menuState.collapsed) }</p>
<p>MenuMode: { JSON.stringify(state.mode) }</p>
<p>MenuTheme: { JSON.stringify(state.theme) }</p>
</div>
</Card>
<div class="demo" style="background: rgb(244,244,244);">
<div class="container" style="width: 256px;">
<BaseMenu
menus={menus}
theme={state.theme}
mode={state.mode}
collapsed={menuState.collapsed}
openKeys={menuState.openKeys}
selectedKeys={menuState.selectedKeys}
{...{
'onUpdate:openKeys': $event => {
menuState.openKeys = $event
},
'onUpdate:selectedKeys': $event => {
menuState.selectedKeys = $event
}
}}
/>
</div>
</div>
</div>
);
},
};
const app = createApp(BaseMenuDemo);
const filterIcons = ['default', 'createFromIconfontCN', 'getTwoToneColor', 'setTwoToneColor']
Object.keys(Icon)
.filter(k => !filterIcons.includes(k))
.forEach(k => {
app.component(Icon[k].displayName, Icon[k])
})
app.mount('#__vue-content>div');
// DO NOT CHANGE `storageOptions` CONFIG. UNLESS YOU KNOW WHAT YOU ARE DOING!
export const storageOptions = {
namespace: 'pro__', // key prefix
name: 'ls', // name variable Vue.[ls] or this.[$ls],
storage: 'local' // storage name session, local, memory
}
export default {
navTheme: 'dark',
primaryColor: '#1890FF',
layout: 'sidemenu',
contentWidth: 'Fluid',
fixedHeader: false,
autoHideHeader: false,
fixSiderbar: false,
colorWeak: false,
menu: {
locale: true
},
title: 'Ant Design Pro',
pwa: false,
iconfontUrl: ''
}
const ThemeColorReplacer = require('webpack-theme-color-replacer')
const generate = require('@ant-design/colors/lib/generate').default
const getAntdSerials = (color) => {
// 淡化(即less的tint)
const lightens = new Array(9).fill().map((t, i) => {
return ThemeColorReplacer.varyColor.lighten(color, i / 10)
})
const colorPalettes = generate(color)
const rgb = ThemeColorReplacer.varyColor.toNum3(color.replace('#', '')).join(',')
return lightens.concat(colorPalettes).concat(rgb)
}
const themePluginOption = {
fileName: 'css/theme-colors-[contenthash:8].css',
matchColors: getAntdSerials('#1890ff'), // 主色系列
// 改变样式选择器,解决样式覆盖问题
changeSelector (selector) {
switch (selector) {
case '.ant-calendar-today .ant-calendar-date':
return ':not(.ant-calendar-selected-date):not(.ant-calendar-selected-day)' + selector
case '.ant-btn:focus,.ant-btn:hover':
return '.ant-btn:focus:not(.ant-btn-primary):not(.ant-btn-danger):not(.ant-btn-link),.ant-btn:hover:not(.ant-btn-primary):not(.ant-btn-danger):not(.ant-btn-link)'
case '.ant-btn.active,.ant-btn:active':
return '.ant-btn.active:not(.ant-btn-primary):not(.ant-btn-danger):not(.ant-btn-link),.ant-btn:active:not(.ant-btn-primary):not(.ant-btn-danger):not(.ant-btn-link)'
case '.ant-steps-item-process .ant-steps-item-icon > .ant-steps-icon':
return ':not(.ant-steps-item-process)' + selector
case '.ant-menu-horizontal>.ant-menu-item-active,.ant-menu-horizontal>.ant-menu-item-open,.ant-menu-horizontal>.ant-menu-item-selected,.ant-menu-horizontal>.ant-menu-item:hover,.ant-menu-horizontal>.ant-menu-submenu-active,.ant-menu-horizontal>.ant-menu-submenu-open,.ant-menu-horizontal>.ant-menu-submenu-selected,.ant-menu-horizontal>.ant-menu-submenu:hover':
case '.ant-menu-horizontal > .ant-menu-item-active,.ant-menu-horizontal > .ant-menu-item-open,.ant-menu-horizontal > .ant-menu-item-selected,.ant-menu-horizontal > .ant-menu-item:hover,.ant-menu-horizontal > .ant-menu-submenu-active,.ant-menu-horizontal > .ant-menu-submenu-open,.ant-menu-horizontal > .ant-menu-submenu-selected,.ant-menu-horizontal > .ant-menu-submenu:hover':
return '.ant-menu-horizontal > .ant-menu-item-active,.ant-menu-horizontal > .ant-menu-item-open,.ant-menu-horizontal > .ant-menu-item-selected,.ant-menu-horizontal:not(.ant-menu-dark) > .ant-menu-item:hover,.ant-menu-horizontal > .ant-menu-submenu-active,.ant-menu-horizontal > .ant-menu-submenu-open,.ant-menu-horizontal:not(.ant-menu-dark) > .ant-menu-submenu-selected,.ant-menu-horizontal:not(.ant-menu-dark) > .ant-menu-submenu:hover'
case '.ant-menu-horizontal > .ant-menu-item-selected > a':
case '.ant-menu-horizontal>.ant-menu-item-selected>a':
return '.ant-menu-horizontal:not(ant-menu-light):not(.ant-menu-dark) > .ant-menu-item-selected > a'
case '.ant-menu-horizontal > .ant-menu-item > a:hover':
case '.ant-menu-horizontal>.ant-menu-item>a:hover':
return '.ant-menu-horizontal:not(ant-menu-light):not(.ant-menu-dark) > .ant-menu-item > a:hover'
default :
return selector
}
}
}
const dynamicThemePlugin = () => new ThemeColorReplacer(themePluginOption)
module.exports = dynamicThemePlugin
export default {
theme: [
{
key: 'dark',
fileName: 'dark.css',
theme: 'dark'
},
{
key: '#F5222D',
fileName: '#F5222D.css',
modifyVars: {
'@primary-color': '#F5222D'
}
},
{
key: '#FA541C',
fileName: '#FA541C.css',
modifyVars: {
'@primary-color': '#FA541C'
}
},
{
key: '#FAAD14',
fileName: '#FAAD14.css',
modifyVars: {
'@primary-color': '#FAAD14'
}
},
{
key: '#13C2C2',
fileName: '#13C2C2.css',
modifyVars: {
'@primary-color': '#13C2C2'
}
},
{
key: '#52C41A',
fileName: '#52C41A.css',
modifyVars: {
'@primary-color': '#52C41A'
}
},
{
key: '#2F54EB',
fileName: '#2F54EB.css',
modifyVars: {
'@primary-color': '#2F54EB'
}
},
{
key: '#722ED1',
fileName: '#722ED1.css',
modifyVars: {
'@primary-color': '#722ED1'
}
},
{
key: '#F5222D',
theme: 'dark',
fileName: 'dark-#F5222D.css',
modifyVars: {
'@primary-color': '#F5222D'
}
},
{
key: '#FA541C',
theme: 'dark',
fileName: 'dark-#FA541C.css',
modifyVars: {
'@primary-color': '#FA541C'
}
},
{
key: '#FAAD14',
theme: 'dark',
fileName: 'dark-#FAAD14.css',
modifyVars: {
'@primary-color': '#FAAD14'
}
},
{
key: '#13C2C2',
theme: 'dark',
fileName: 'dark-#13C2C2.css',
modifyVars: {
'@primary-color': '#13C2C2'
}
},
{
key: '#52C41A',
theme: 'dark',
fileName: 'dark-#52C41A.css',
modifyVars: {
'@primary-color': '#52C41A'
}
},
{
key: '#2F54EB',
theme: 'dark',
fileName: 'dark-#2F54EB.css',
modifyVars: {
'@primary-color': '#2F54EB'
}
},
{
key: '#722ED1',
theme: 'dark',
fileName: 'dark-#722ED1.css',
modifyVars: {
'@primary-color': '#722ED1'
}
}
]
}
import { createApp, defineComponent, reactive } from 'vue';
import { Card } from 'ant-design-vue';
import { ContentWidth } from '../src/typings';
import GlobalFooter from '../src/GlobalFooter';
import 'ant-design-vue/dist/antd.less';
const GlobalFooterDemo = defineComponent({
setup () {
const state = reactive({
contentWidth: 'Fixed' as ContentWidth
})
return () => (
<div class="components" style={{ background: 'rgb(240, 240, 240)', paddingBottom: '20px' }}>
<Card style={{ marginBottom: '24px', background: 'rgb(244,244,244)' }}>
<h2># BasicLayout</h2>
</Card>
<GlobalFooter
links={[
{
key: '1',
title: 'Pro Layout',
href: 'https://www.github.com/vueComponent/pro-layout',
blankTarget: true,
},
{
key: '2',
title: 'Github',
href: 'https://www.github.com/vueComponent/ant-design-vue-pro',
blankTarget: true,
},
{
key: '3',
title: '@Sendya',
href: 'https://www.github.com/sendya/',
blankTarget: true,
}
]}
copyright={(<a href="https://github.com/vueComponent" target="_blank">vueComponent</a>)}
/>
</div>
);
}
});
createApp(GlobalFooterDemo).mount('#__vue-content>div');
import { createApp, defineComponent, reactive } from 'vue';
import { Card, Space, Button } from 'ant-design-vue';
import { ContentWidth } from '../src/typings';
import GridContent from '../src/GridContent';
import 'ant-design-vue/dist/antd.less';
const GridContentDemo = defineComponent({
setup () {
const state = reactive({
contentWidth: 'Fixed' as ContentWidth
})
return () => (
<div class="components" style={{ background: 'rgb(240, 240, 240)', paddingBottom: '20px' }}>
<Card style={{ marginBottom: '24px', background: 'rgb(244,244,244)' }}>
<Space size="middle">
ContentWidth:
<Button
type="primary"
onClick={() => {
state.contentWidth = state.contentWidth === 'Fixed' ? 'Fluid' : 'Fixed';
}}
>
{state.contentWidth}
</Button>
</Space>
</Card>
<GridContent contentWidth={state.contentWidth} style={{ background: 'rgb(220, 220, 220)', padding: '22px' }}>
Content
<br />
...
<br />
...
<br />
...
<br />
...
<br />
...<br />
</GridContent>
</div>
);
}
});
createApp(GridContentDemo).mount('#__vue-content>div');
.simple-wrap {
position: relative;
margin-bottom: 1rem;
line-height: 1;
&:after {
width: 100%;
height: 1px;
content: '';
position: absolute;
bottom: 0.125em;
left: 0;
background-color: hotpink;
z-index: -100;
}
&:before {
width: 100%;
height: 1px;
content: '';
position: absolute;
top: 0.125em;
left: 0;
background-color: hotpink;
z-index: -100;
}
}
.two-tone-wrap,
.all-icons-wrap {
.container {
display: flex;
flex-flow: row wrap;
width: 80vw;
margin: auto;
}
.card {
height: 90px;
margin: 12px 0 16px;
width: 16.6666%;
text-align: center;
}
.name-description {
display: block;
text-align: center;
transform: scale(0.83);
font-family: 'Lucida Console', Consolas;
white-space: nowrap;
}
.text {
margin: 0 0.5rem;
}
}
.ant-design-doc-set-wrap {
.container {
margin: 40px 0;
list-style: none;
overflow: hidden;
}
.container-item {
float: left;
width: 16.66%;
text-align: center;
list-style: none;
height: 100px;
transition: color 0.3s ease-in-out, background-color 0.3s ease-in-out;
position: relative;
margin: 3px 0;
border-radius: 4px;
overflow: hidden;
padding: 10px 0 0;
}
.title {
text-align: center;
}
}
\ No newline at end of file
import { createApp, reactive } from 'vue';
import { Button, message } from 'ant-design-vue';
import BasicLayout from '../src/BasicLayout';
import 'ant-design-vue/dist/antd.less';
const SimpleDemo = {
setup() {
const state = reactive({
collapsed: false,
});
return () => (
<div className="components">
<h2># BasicLayout</h2>
<BasicLayout
collapsed={state.collapsed}
onCollapsed={collapsed => {
state.collapsed = collapsed;
}}
>
<Button
onClick={() => {
message.info('clicked.');
}}
>
Click Me!!
</Button>
</BasicLayout>
</div>
);
},
};
const app = createApp(SimpleDemo);
app.use(BasicLayout).mount('#__vue-content>div');
module.exports = {
preset: '@vue/cli-plugin-unit-jest'
}
import { RouteProps } from '../src/typings';
export const menus: RouteProps[] = [
{
path: '/dashboard',
name: 'Dashboard',
meta: { icon: 'dashboard-outlined', title: 'Dashboard' },
children: [
{
path: '/dashboard/analysis',
name: 'Analysis',
meta: { icon: 'SmileOutlined', title: 'Analysis' },
},
{
path: '/dashboard/monitor',
name: 'Monitor',
meta: { icon: 'SmileOutlined', title: 'Monitor' },
},
{
path: '/dashboard/workplace',
name: 'Workplace',
meta: { icon: 'SmileOutlined', title: 'Workplace' },
},
],
},
{
path: '/form',
name: 'form',
meta: { title: 'Form', icon: 'SmileOutlined' },
children: [
{
path: '/form/basic-form',
name: 'basic-form',
meta: { title: 'Basic Form' },
},
{
path: '/form/step-form',
name: 'step-form',
meta: { title: 'Step Form' },
},
{
path: '/form/advanced-form',
name: 'advance-form',
meta: { title: 'Advanced Form' },
},
],
},
]
{
"name": "ant-design-pro-example",
"version": "0.1.1",
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"test:unit": "vue-cli-service test:unit",
"lint": "vue-cli-service lint",
"analyz": "vue-cli-service build --mode analyz"
},
"main": "./src/index.js",
"author": "vueComponent",
"repository": {
"type": "git",
"url": "https://github.com/vueComponent/pro-layout"
},
"license": "MIT",
"dependencies": {
"@ant-design/icons": "^4.0.0-alpha.11",
"ant-design-vue": "^1.5.4",
"core-js": "^3.1.2",
"vue-i18n": "^8.15.0",
"vue-ls": "^3.2.1",
"vue-router": "^3.0.6",
"vue-svg-component-runtime": "^1.0.1",
"vuex": "^3.0.1"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^4.0.0",
"@vue/cli-plugin-eslint": "^4.0.0",
"@vue/cli-plugin-router": "^4.0.0",
"@vue/cli-plugin-unit-jest": "^4.0.0",
"@vue/cli-plugin-vuex": "^4.0.0",
"@vue/cli-service": "^4.0.0",
"@vue/eslint-config-standard": "^4.0.0",
"@vue/test-utils": "1.0.0-beta.29",
"babel-eslint": "^10.0.1",
"babel-plugin-import": "^1.12.2",
"eslint": "^5.16.0",
"eslint-plugin-html": "^6.0.0",
"eslint-plugin-vue": "^5.0.0",
"less": "^3.0.4",
"less-loader": "^5.0.0",
"vue-svg-icon-loader": "^2.1.1",
"vue-template-compiler": "^2.6.10",
"webpack-bundle-analyzer": "^3.6.0",
"webpack-theme-color-replacer": "^1.3.11"
}
}
module.exports = {
plugins: {
autoprefixer: {}
}
}
import { computed, createApp, defineComponent, inject, reactive, toRefs } from 'vue';
import { Card, Space, Button } from 'ant-design-vue';
import { ContentWidth } from '../src/typings';
import { warning } from '../src/utils';
import GridContent from '../src/GridContent';
import ProProvider, { injectProConfigKey, defaultProProviderProps } from '../src/ProProvider';
import 'ant-design-vue/dist/antd.less';
const trans = {
'render.test.i18n.hello': 'Hello My Friends'
}
const i18nRender = (t: string): string => {
warning(false, `i18n.key ${t}, value: ${trans[t]}`, )
return trans[t];
}
const ProProviderDemo = defineComponent({
setup () {
const state = reactive({
contentWidth: 'Fixed' as ContentWidth,
})
return () => (
<>
<ProProvider i18n={i18nRender} contentWidth={state.contentWidth}>
<h2># BasicLayout</h2>
<div class="components" style={{ background: 'rgb(240, 240, 240)', paddingBottom: '20px' }}>
<Card style={{ marginBottom: '24px', background: 'rgb(244,244,244)' }}>
<Space size="middle">
ContentWidth:
<Button
type="primary"
onClick={() => {
state.contentWidth = state.contentWidth === 'Fixed' ? 'Fluid' : 'Fixed';
}}
>
{state.contentWidth}
</Button>
</Space>
<div class="env">
state.contentWidth: { JSON.stringify(state.contentWidth) }
</div>
</Card>
<TestChildComponent style={{ background: 'rgb(220, 220, 220)', padding: '22px' }}/>
</div>
</ProProvider>
</>
)
}
})
const TestChildComponent = defineComponent({
setup () {
const config = inject(injectProConfigKey, defaultProProviderProps)
return () => {
const { i18n, contentWidth } = config
return (
<div class="test-child-component">
<p>TestChildComponent:</p>
<div>
i18n: {i18n.toString()}
</div>
<div>
contentWidth: {contentWidth}
</div>
<p>{i18n('render.test.i18n.hello')}</p>
</div>
)
}
}
});
const app = createApp(ProProviderDemo);
app.mount('#__vue-content>div');
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title>Pro Layout</title>
</head>
<body>
<noscript>
<strong>We're sorry but <b>Ant Design Pro</b> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
<template>
<div id="app">
<router-view />
</div>
</template>
<style lang="less" scoped>
.nav-container {
width: 400px;
padding-top: 100px;
margin: 0 auto;
ul {
li {
display: inline-block;
margin: 0 10px;
}
}
a {
user-select: none;
text-decoration: none;
&.router-link-active {
color: #FF0000;
}
}
}
</style>
<?xml version="1.0" encoding="UTF-8"?>
<svg viewBox="0 0 128 128" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 52.6 (67491) - http://www.bohemiancoding.com/sketch -->
<title>Vue</title>
<desc>Created with Sketch.</desc>
<defs>
<linearGradient x1="69.644116%" y1="0%" x2="69.644116%" y2="100%" id="linearGradient-1">
<stop stop-color="#29CDFF" offset="0%"></stop>
<stop stop-color="#148EFF" offset="37.8600687%"></stop>
<stop stop-color="#0A60FF" offset="100%"></stop>
</linearGradient>
<linearGradient x1="-19.8191553%" y1="-36.7931464%" x2="138.57919%" y2="157.637507%" id="linearGradient-2">
<stop stop-color="#29CDFF" offset="0%"></stop>
<stop stop-color="#0F78FF" offset="100%"></stop>
</linearGradient>
<linearGradient x1="68.1279872%" y1="-35.6905737%" x2="30.4400914%" y2="114.942679%" id="linearGradient-3">
<stop stop-color="#FA8E7D" offset="0%"></stop>
<stop stop-color="#F74A5C" offset="51.2635191%"></stop>
<stop stop-color="#F51D2C" offset="100%"></stop>
</linearGradient>
</defs>
<g id="Vue" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Group" transform="translate(19.000000, 9.000000)">
<path d="M89.96,90.48 C78.58,93.48 68.33,83.36 67.62,82.48 L46.6604487,62.2292258 C45.5023849,61.1103236 44.8426845,59.5728835 44.8296987,57.9626396 L44.5035564,17.5209948 C44.4948861,16.4458744 44.0537714,15.4195095 43.2796864,14.6733517 L29.6459999,1.53153737 C28.055475,-0.00160504005 25.5232423,0.0449126588 23.9900999,1.63543756 C23.2715121,2.38092066 22.87,3.37600834 22.87,4.41143746 L22.87,64.3864751 C22.87,67.0807891 23.9572233,69.6611067 25.885409,71.5429748 L63.6004615,108.352061 C65.9466323,110.641873 69.6963584,110.624605 72.0213403,108.313281" id="Path-Copy" fill="url(#linearGradient-1)" fill-rule="nonzero" transform="translate(56.415000, 54.831157) scale(-1, 1) translate(-56.415000, -54.831157) "></path>
<path d="M68,90.1163122 C56.62,93.1163122 45.46,83.36 44.75,82.48 L23.7904487,62.2292258 C22.6323849,61.1103236 21.9726845,59.5728835 21.9596987,57.9626396 L21.6335564,17.5209948 C21.6248861,16.4458744 21.1837714,15.4195095 20.4096864,14.6733517 L6.7759999,1.53153737 C5.185475,-0.00160504005 2.65324232,0.0449126588 1.12009991,1.63543756 C0.401512125,2.38092066 3.90211878e-13,3.37600834 3.90798505e-13,4.41143746 L3.94351218e-13,64.3864751 C3.94681177e-13,67.0807891 1.08722326,69.6611067 3.01540903,71.5429748 L40.7807092,108.401101 C43.1069304,110.671444 46.8180151,110.676525 49.1504445,108.412561" id="Path" fill="url(#linearGradient-2)" fill-rule="nonzero"></path>
<path d="M43.2983488,19.0991931 L27.5566079,3.88246244 C26.7624281,3.11476967 26.7409561,1.84862177 27.5086488,1.05444194 C27.8854826,0.664606611 28.4044438,0.444472651 28.9466386,0.444472651 L60.3925021,0.444472651 C61.4970716,0.444472651 62.3925021,1.33990315 62.3925021,2.44447265 C62.3925021,2.9858375 62.1730396,3.50407742 61.7842512,3.88079942 L46.0801285,19.0975301 C45.3051579,19.8484488 44.0742167,19.8491847 43.2983488,19.0991931 Z" id="Path" fill="url(#linearGradient-3)"></path>
</g>
</g>
</svg>
\ No newline at end of file
import { Icon, Menu, Dropdown } from 'ant-design-vue'
import './index.less'
import i18nMixin from '@/store/i18n-mixin'
const locales = ['zh-CN', 'zh-TW', 'en-US', 'pt-BR']
const languageLabels = {
'zh-CN': '简体中文',
'zh-TW': '繁体中文',
'en-US': 'English',
'pt-BR': 'Português'
}
// eslint-disable-next-line
const languageIcons = {
'zh-CN': '🇨🇳',
'zh-TW': '🇭🇰',
'en-US': '🇺🇸',
'pt-BR': '🇧🇷'
}
const SelectLang = {
name: 'SelectLang',
mixins: [i18nMixin],
render () {
const changeLang = ({ key }) => {
this.setLang(key)
}
const langMenu = (
<Menu class={['menu', 'ant-pro-select-lang']} selectedKeys={[this.currentLang]} onClick={changeLang}>
{locales.map(locale => (
<Menu.Item key={locale}>
<span role="img" aria-label={languageLabels[locale]}>
{languageIcons[locale]}
</span>{' '}
{languageLabels[locale]}
</Menu.Item>
))}
</Menu>
)
return (
<Dropdown overlay={langMenu} placement="bottomRight">
<span class={'drop-down'}>
<Icon type="global" title={this.$t('navBar.lang')} />
</span>
</Dropdown>
)
}
}
export default SelectLang
@import "~ant-design-vue/es/style/themes/default";
@header-menu-prefix-cls: ~'@{ant-prefix}-pro-header-menu';
.drop-down {
line-height: @layout-header-height;
vertical-align: top;
cursor: pointer;
> i {
font-size: 16px !important;
transform: none !important;
svg {
position: relative;
top: -1px;
}
}
}
.ant-pro-select-lang {
.ant-dropdown-menu-item {
min-width: 160px;
}
}
/**
* 项目默认配置项
* primaryColor - 默认主题色, 如果修改颜色不生效,请清理 localStorage
* navTheme - sidebar theme ['dark', 'light'] 两种主题
* colorWeak - 色盲模式
* layout - 整体布局方式 ['sidemenu', 'topmenu'] 两种布局
* fixedHeader - 固定 Header : boolean
* fixSiderbar - 固定左侧菜单栏 : boolean
* contentWidth - 内容区布局: 流式 | 固定
*
* storageOptions: {} - Vue-ls 插件配置项 (localStorage/sessionStorage)
*
*/
export default {
navTheme: 'dark', // theme for nav menu
primaryColor: '#52C41A', // primary color of ant design
layout: 'sidemenu', // nav menu position: `sidemenu` or `topmenu`
contentWidth: 'Fluid', // layout of content: `Fluid` or `Fixed`, only works when layout is topmenu
fixedHeader: false, // sticky header
fixSiderbar: false, // sticky siderbar
colorWeak: false,
menu: {
locale: true
},
title: 'Ant Design Pro',
pwa: false,
iconfontUrl: '',
production: process.env.NODE_ENV === 'production' && process.env.VUE_APP_PREVIEW !== 'true'
}
// eslint-disable-next-line
import BasicLayout from '../layouts/BasicLayout.vue'
const RouteView = {
name: 'RouteView',
render: (h) => h('router-view')
}
const asyncRouterMap = [
{
path: '/',
name: 'index',
component: BasicLayout,
meta: { title: 'menu.home' },
redirect: '/dashboard/analysis',
children: [
{
path: '/dashboard',
name: 'dashboard',
meta: { keepAlive: true, title: 'menu.dashboard.default', icon: 'dashboard' },
redirect: '/dashboard/analysis',
component: RouteView,
children: [
{
path: '/dashboard/analysis',
name: 'analysis',
meta: {
keepAlive: true,
icon: 'smile',
title: 'menu.dashboard.analysis'
},
component: () => import(/* webpackChunkName: "dashboard" */ '../views/dashboard/analysis')
},
{
path: '/dashboard/workplace/:id?',
name: 'workplace',
meta: {
global: true,
keepAlive: true,
icon: 'smile',
title: 'menu.dashboard.workplace'
},
component: () => import(/* webpackChunkName: "dashboard" */ '../views/dashboard/workplace')
}
]
},
{
path: '/form',
name: 'form',
meta: {
keepAlive: true,
title: 'menu.form.default',
icon: 'video-camera'
},
component: RouteView,
children: [
{
path: '/form/basic-form',
name: 'basic-form',
meta: {
keepAlive: true,
icon: 'smile',
title: 'menu.form.basicform'
},
component: () => import(/* webpackChunkName: "about" */ '../views/form/basic-form')
},
{
path: '/form/step-form',
name: 'step-form',
meta: {
keepAlive: true,
icon: 'smile',
title: 'menu.form.stepform'
},
component: () => import(/* webpackChunkName: "about" */ '../views/form/step-form')
},
{
path: '/form/advanced-form',
name: 'advanced-form',
meta: {
keepAlive: true,
icon: 'smile',
title: 'menu.form.advancedform'
},
component: () => import(/* webpackChunkName: "about" */ '../views/form/advanced-form')
}
]
},
{
path: '/page1',
name: 'page1',
meta: {
keepAlive: true,
title: 'menu.nav1',
icon: 'smile'
},
component: () => import(/* webpackChunkName: "about" */ '../views/TestPage1')
},
{
path: '/page2',
name: 'page2',
meta: {
keepAlive: true,
title: 'menu.nav2',
icon: 'smile'
},
component: () => import(/* webpackChunkName: "about" */ '../views/TestPage2')
},
{
path: '/page3',
name: 'page3',
meta: {
keepAlive: true,
title: 'menu.nav3',
icon: 'smile'
},
component: () => import(/* webpackChunkName: "about" */ '../views/TestPage3')
}
]
}
]
export { asyncRouterMap }
/* Layout begin */
export {
default as LeftOutline
} from '@ant-design/icons/lib/outline/LeftOutline'
export {
default as RightOutline
} from '@ant-design/icons/lib/outline/RightOutline'
export {
default as ArrowLeftOutline
} from '@ant-design/icons/lib/outline/ArrowLeftOutline'
export {
default as ArrowRightOutline
} from '@ant-design/icons/lib/outline/ArrowRightOutline'
export {
default as MenuFoldOutline
} from '@ant-design/icons/lib/outline/MenuFoldOutline'
export {
default as MenuUnfoldOutline
} from '@ant-design/icons/lib/outline/MenuUnfoldOutline'
export {
default as DashboardOutline
} from '@ant-design/icons/lib/outline/DashboardOutline'
export {
default as VideoCameraOutline
} from '@ant-design/icons/lib/outline/VideoCameraOutline'
export {
default as LoadingOutline
} from '@ant-design/icons/lib/outline/LoadingOutline'
export {
default as GlobalOutline
} from '@ant-design/icons/lib/outline/GlobalOutline'
export {
default as UserOutline
} from '@ant-design/icons/lib/outline/UserOutline'
export {
default as LogoutOutline
} from '@ant-design/icons/lib/outline/LogoutOutline'
export {
default as SmileOutline
} from '@ant-design/icons/lib/outline/SmileOutline'
export {
default as CheckOutline
} from '@ant-design/icons/lib/outline/CheckOutline'
export {
default as EllipsisOutline
} from '@ant-design/icons/lib/outline/EllipsisOutline'
export {
default as NotificationOutline
} from '@ant-design/icons/lib/outline/NotificationOutline'
export {
default as SettingOutline
} from '@ant-design/icons/lib/outline/SettingOutline'
export {
default as GithubOutline
} from '@ant-design/icons/lib/outline/GithubOutline'
export {
default as CopyrightOutline
} from '@ant-design/icons/lib/outline/CopyrightOutline'
/* Layout end */
/* Feedback begin */
export {
default as QuestionCircleOutline
} from '@ant-design/icons/lib/outline/QuestionCircleOutline'
/* Feedback end */
/* MultiTab begin */
export {
default as CloseOutline
} from '@ant-design/icons/lib/outline/CloseOutline'
export {
default as ReloadOutline
} from '@ant-design/icons/lib/outline/ReloadOutline'
export {
default as DownOutline
} from '@ant-design/icons/lib/outline/DownOutline'
export {
default as AlignLeftOutline
} from '@ant-design/icons/lib/outline/AlignLeftOutline'
/* MultiTab end */
/* eslint-disable */
/**
* 该文件是为了按需加载,剔除掉了一些不需要的框架组件。
* 减少了编译支持库包大小
*
* 当需要更多组件依赖时,在该文件加入即可
*
* 注意:需要同时修改 babel.config.js 文件下的配置项
*/
import Vue from 'vue'
import {
ConfigProvider,
Layout,
Input,
InputNumber,
Button,
Switch,
Radio,
Checkbox,
Select,
Card,
Form,
Row,
Col,
Modal,
Table,
Tabs,
Icon,
Badge,
Popover,
Dropdown,
List,
Avatar,
Breadcrumb,
Steps,
Spin,
Menu,
Drawer,
Tooltip,
Alert,
Tag,
Divider,
DatePicker,
TimePicker,
Upload,
Progress,
Skeleton,
Popconfirm,
message,
notification
} from 'ant-design-vue'
Vue.use(ConfigProvider)
Vue.use(Layout)
Vue.use(Input)
Vue.use(InputNumber)
Vue.use(Button)
Vue.use(Switch)
Vue.use(Radio)
Vue.use(Checkbox)
Vue.use(Select)
Vue.use(Card)
Vue.use(Form)
Vue.use(Row)
Vue.use(Col)
Vue.use(Modal)
Vue.use(Table)
Vue.use(Tabs)
Vue.use(Icon)
Vue.use(Badge)
Vue.use(Popover)
Vue.use(Dropdown)
Vue.use(List)
Vue.use(Avatar)
Vue.use(Breadcrumb)
Vue.use(Steps)
Vue.use(Spin)
Vue.use(Menu)
Vue.use(Drawer)
Vue.use(Tooltip)
Vue.use(Alert)
Vue.use(Tag)
Vue.use(Divider)
Vue.use(DatePicker)
Vue.use(TimePicker)
Vue.use(Upload)
Vue.use(Progress)
Vue.use(Skeleton)
Vue.use(Popconfirm)
// Add to this.$xxx
Vue.prototype.$confirm = Modal.confirm
Vue.prototype.$message = message
Vue.prototype.$notification = notification
Vue.prototype.$info = Modal.info
Vue.prototype.$success = Modal.success
Vue.prototype.$error = Modal.error
Vue.prototype.$warning = Modal.warning
/* eslint-disable */
/**
* 该文件用于直接全量引入 antd
* 如果想减少依赖体积,可以使用同目录下的 lazyload.js 按需加载
* 注意:需要同时修改 babel.config.js 文件下的配置项
*/
import Vue from 'vue'
import Antd from 'ant-design-vue'
import 'ant-design-vue/dist/antd.less'
Vue.use(Antd)
import Vue from 'vue'
import store from '../store'
import defaultSettings from '@config/defaultSettings'
import {
APP_LANGUAGE,
TOGGLE_CONTENT_WIDTH,
TOGGLE_FIXED_HEADER,
TOGGLE_FIXED_SIDEBAR, TOGGLE_HIDE_HEADER,
TOGGLE_LAYOUT, TOGGLE_NAV_THEME, TOGGLE_WEAK
} from '../store/mutation-types'
export default function initializer () {
console.log('API_URL:', process.env.BASE_URL)
// store.commit(ACCESS_TOKEN, Vue.ls.get(ACCESS_TOKEN))
store.commit(TOGGLE_LAYOUT, Vue.ls.get(TOGGLE_LAYOUT, defaultSettings.layout))
store.commit(TOGGLE_FIXED_HEADER, Vue.ls.get(TOGGLE_FIXED_HEADER, defaultSettings.fixedHeader))
store.commit(TOGGLE_FIXED_SIDEBAR, Vue.ls.get(TOGGLE_FIXED_SIDEBAR, defaultSettings.fixSiderbar))
store.commit(TOGGLE_CONTENT_WIDTH, Vue.ls.get(TOGGLE_CONTENT_WIDTH, defaultSettings.contentWidth))
store.commit(TOGGLE_HIDE_HEADER, Vue.ls.get(TOGGLE_HIDE_HEADER, defaultSettings.autoHideHeader))
store.commit(TOGGLE_NAV_THEME, Vue.ls.get(TOGGLE_NAV_THEME, defaultSettings.navTheme))
store.commit(TOGGLE_WEAK, Vue.ls.get(TOGGLE_WEAK, defaultSettings.colorWeak))
store.dispatch('setLang', Vue.ls.get(APP_LANGUAGE, 'en-US'))
}
import Vue from 'vue'
import Storage from 'vue-ls'
// use antd
// import './antd/normal'
// lazyload antd
import './antd/lazyload'
import { storageOptions } from '@config/defaultSettings'
import { DocumentTitle } from '@ant-design-vue/pro-layout'
Vue.use(DocumentTitle)
Vue.use(Storage, storageOptions)
import 'core-js/features/array/from'
import 'core-js/features/array/flat'
import 'core-js/features/set'
import 'core-js/features/promise'
import 'core-js/features/object/entries'
import 'regenerator-runtime/runtime'
@import '~ant-design-vue/es/style/themes/default.less';
html,
body,
#app, #root {
height: 100%;
}
.colorWeak {
filter: invert(80%);
}
.ant-layout.layout-basic {
height: 100vh;
min-height: 100vh;
}
canvas {
display: block;
}
body {
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
ul,
ol {
list-style: none;
}
@media (max-width: @screen-xs) {
.ant-table {
width: 100%;
overflow-x: auto;
&-thead > tr,
&-tbody > tr {
> th,
> td {
white-space: pre;
> span {
display: block;
}
}
}
}
}
@import "~ant-design-vue/es/style/themes/default";
.ant-pro-global-header-index-right {
margin-right: 8px;
&.ant-pro-global-header-index-dark {
.ant-pro-global-header-index-action {
color: hsla(0,0%,100%,.85);
&:hover {
background: #1890ff;
}
}
}
&.ant-pro-global-header-topmenu {
margin-right: 0;
}
.account {
.antd-pro-global-header-index-avatar {
margin: ~'calc((@{layout-header-height} - 24px) / 2)' 0;
margin-right: 8px;
color: @primary-color;
vertical-align: top;
background: rgba(255, 255, 255, 0.85);
}
}
.menu {
.anticon {
margin-right: 8px;
}
.ant-dropdown-menu-item {
min-width: 100px;
}
}
}
<template>
<pro-layout
title="Pro Layout"
:menus="menus"
:collapsed="collapsed"
:mediaQuery="query"
:isMobile="isMobile"
:handleMediaQuery="handleMediaQuery"
:handleCollapse="handleCollapse"
:i18nRender="i18nRender"
v-bind="settings"
>
<!-- <template v-slot:menuRender>
<div v-for="(menu, key) in menus" :key="key">
{{ menu }}
</div>
</template>-->
<template v-slot:menuHeaderRender>
<div>
<img src="../assets/logo.svg" />
<h1>Pro Layout</h1>
</div>
</template>
<template v-slot:headerContentRender>
<div>headerContentRender</div>
</template>
<template v-slot:rightContentRender>
<div :class="['ant-pro-global-header-index-right', settings.layout === 'topmenu' && `ant-pro-global-header-index-${settings.theme}`]">
rightContentRender
</div>
</template>
<template v-slot:footerRender>
<div>footerRender</div>
</template>
<setting-drawer
:settings="settings"
@change="handleSettingChange"
/>
<router-view />
</pro-layout>
</template>
<script>
import { asyncRouterMap } from '../config/router.config'
import { i18nRender } from '../locales'
import defaultSettings from '@/config/defaultSettings'
import LogoSvg from '../assets/logo.svg?inline'
import { CONTENT_WIDTH_TYPE } from '@/store/mutation-types'
export default {
name: 'BasicLayout',
data () {
return {
// base
menus: [],
// 侧栏收起状态
collapsed: false,
// 自动隐藏头部栏
autoHideHeader: false,
// 媒体查询
query: {},
// 布局类型
layout: 'sidemenu', // 'sidemenu', 'topmenu'
// 定宽: true / 流式: false
contentWidth: false, // layout of content: `Fluid` or `Fixed`, only works when layout is topmenu
// 主题 'dark' | 'light'
theme: 'dark',
// 是否手机模式
isMobile: false,
settings: {
// 布局类型
layout: defaultSettings.layout, // 'sidemenu', 'topmenu'
// CONTENT_WIDTH_TYPE
contentWidth: defaultSettings.contentWidth,
// 主题 'dark' | 'light'
theme: defaultSettings.navTheme,
// 主色调
primaryColor: defaultSettings.primaryColor,
fixedHeader: defaultSettings.fixedHeader,
fixSiderbar: defaultSettings.fixSiderbar,
colorWeak: defaultSettings.colorWeak,
hideHintAlert: false,
hideCopyButton: false
}
}
},
created () {
this.menus = asyncRouterMap.find(item => item.path === '/').children
},
methods: {
i18nRender,
handleMediaQuery (val) {
this.query = val
if (this.isMobile && !val['screen-xs']) {
this.isMobile = false
return
}
if (!this.isMobile && val['screen-xs']) {
this.isMobile = true
this.collapsed = false
}
},
handleCollapse (val) {
this.collapsed = val
},
handleSettingChange ({ type, value }) {
console.log('type', type, value)
type && (this.settings[type] = value)
switch (type) {
case 'contentWidth':
this.settings[type] = value
break
case 'layout':
if (value === 'sidemenu') {
this.settings.contentWidth = CONTENT_WIDTH_TYPE.Fluid
} else {
this.settings.fixSiderbar = false
this.settings.contentWidth = CONTENT_WIDTH_TYPE.Fixed
}
break
}
},
logoRender () {
return <LogoSvg />
},
footerRender () {
return <div>custom footer</div>
}
}
}
</script>
<style lang="less" scoped>
@import "BasicLayout.less";
</style>
import Vue from 'vue'
import VueI18n from 'vue-i18n'
// default lang
import enUS from './lang/en-US'
Vue.use(VueI18n)
export const defaultLang = 'en-US'
const messages = {
'en-US': {
...enUS
}
}
const i18n = new VueI18n({
silentTranslationWarn: true,
locale: defaultLang,
fallbackLocale: defaultLang,
messages
})
const loadedLanguages = [defaultLang]
function setI18nLanguage (lang) {
i18n.locale = lang
// request.headers['Accept-Language'] = lang
document.querySelector('html').setAttribute('lang', lang)
return lang
}
export function loadLanguageAsync (lang = defaultLang) {
return new Promise(resolve => {
// 缓存语言设置
Vue.ls.set('lang', lang)
if (i18n.locale !== lang) {
if (!loadedLanguages.includes(lang)) {
return import(/* webpackChunkName: "lang-[request]" */ `./lang/${lang}`).then(msg => {
i18n.setLocaleMessage(lang, msg.default)
loadedLanguages.push(lang)
return setI18nLanguage(lang)
})
}
return resolve(setI18nLanguage(lang))
}
return resolve(lang)
})
}
export function i18nRender (key) {
return i18n.t(`${key}`)
}
export default i18n
export default {
navBar: {
lang: 'Language'
},
layouts: {
usermenu: {
dialog: {
title: 'Log-out',
content: 'Do you want log-out?'
}
}
},
menu: {
home: 'Home',
dashboard: {
default: 'Dashboard',
analysis: 'Analysis',
workplace: 'Workplace'
},
form: {
default: 'Form',
basicform: 'Basic Form',
stepform: 'Step Form',
advancedform: 'Advanced Form'
},
nav1: 'Nav 1',
nav2: 'Nav 2',
nav3: 'Nav 3'
},
pages: {
form: {
basicform: {
headers: {
btn1: 'Button1'
},
content: 'Form pages are used to collect or verify information to users, and basic forms are common in scenarios where there are fewer data items.',
tabs: {
tab1: 'Tab1',
tab2: 'Tab2',
tab3: 'Tab3'
}
}
}
},
'navBar.lang': 'Language',
'app.setting.pagestyle': 'Page style setting',
'app.setting.pagestyle.light': 'Light style',
'app.setting.pagestyle.dark': 'Dark style',
'app.setting.pagestyle.realdark': 'RealDark style',
'app.setting.themecolor': 'Theme Color',
'app.setting.navigationmode': 'Navigation Mode',
'app.setting.content-width': 'Content Width',
'app.setting.fixedheader': 'Fixed Header',
'app.setting.fixedsidebar': 'Fixed Sidebar',
'app.setting.sidemenu': 'Side Menu Layout',
'app.setting.topmenu': 'Top Menu Layout',
'app.setting.content-width.fixed': 'Fixed',
'app.setting.content-width.fluid': 'Fluid',
'app.setting.othersettings': 'Other Settings',
'app.setting.weakmode': 'Weak Mode',
'app.setting.copy': 'Copy Setting',
'app.setting.loading': 'Loading theme',
'app.setting.copyinfo': 'copy success,please replace defaultSettings in src/models/setting.js',
'app.setting.production.hint': 'Setting panel shows in development environment only, please manually modify'
}
export default {
navBar: {
lang: '语言'
},
layouts: {
usermenu: {
dialog: {
title: '注销',
content: '要注销账户吗?'
}
}
},
menu: {
home: '首页',
dashboard: {
default: '仪表盘',
analysis: '分析页',
workplace: '工作台'
},
form: {
default: '表单页',
basicform: '基础表单',
stepform: '分步表单',
advancedform: '高级表单'
},
nav1: '导航1',
nav2: '导航2',
nav3: '导航3'
},
pages: {
dashboard: {
analysis: {
content: '分析页描述'
}
},
form: {
basicform: {
headers: {
btn1: '按钮1'
},
content: '表单页用于向用户收集或验证信息,基础表单常见于数据项较少的表单场景。',
tabs: {
tab1: '标签1',
tab2: '标签2',
tab3: '标签3'
}
}
}
}
}
import './core/polyfills' // with polyfills
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import i18n from './locales'
import './router/router-guards'
import './core/library'
import ProLayout, { PageHeaderWrapper, SettingDrawer } from '@ant-design-vue/pro-layout'
import initializer from './core/bootstrap'
import './global.less'
import themePluginConfig from '@config/themePluginConfig'
Vue.config.productionTip = false
Vue.use(ProLayout)
Vue.use(PageHeaderWrapper)
Vue.use(SettingDrawer)
window.umi_plugin_ant_themeVar = themePluginConfig.theme
window._vue = new Vue({
router,
store,
i18n,
beforeCreate: initializer,
render: h => h(App)
}).$mount('#app')
import Vue from 'vue'
import VueRouter from 'vue-router'
import { asyncRouterMap } from '@/config/router.config'
// hack router push/replace callback
['push', 'replace'].map(key => {
return {
k: key,
prop: VueRouter.prototype[key]
}
}).forEach(item => {
VueRouter.prototype[item.k] = function newCall (location, onResolve, onReject) {
if (onResolve || onReject) return item.prop.call(this, location, onResolve, onReject)
return item.prop.call(this, location).catch(err => err)
}
})
Vue.use(VueRouter)
const routes = asyncRouterMap
const router = new VueRouter({
mode: 'history',
routes
})
export default router
import router from './index'
router.beforeEach((to, from, next) => {
next()
})
/* eslint-disable eslint-comments/disable-enable-pair */
/* eslint-disable no-restricted-globals */
/* eslint-disable no-underscore-dangle */
/* globals workbox */
workbox.core.setCacheNameDetails({
prefix: 'antd-pro',
suffix: 'v1'
})
// Control all opened tabs ASAP
workbox.clientsClaim()
/**
* Use precaching list generated by workbox in build process.
* https://developers.google.com/web/tools/workbox/reference-docs/latest/workbox.precaching
*/
workbox.precaching.precacheAndRoute(self.__precacheManifest || [])
/**
* Register a navigation route.
* https://developers.google.com/web/tools/workbox/modules/workbox-routing#how_to_register_a_navigation_route
*/
workbox.routing.registerNavigationRoute('/index.html')
/**
* Use runtime cache:
* https://developers.google.com/web/tools/workbox/reference-docs/latest/workbox.routing#.registerRoute
*
* Workbox provides all common caching strategies including CacheFirst, NetworkFirst etc.
* https://developers.google.com/web/tools/workbox/reference-docs/latest/workbox.strategies
*/
/**
* Handle API requests
*/
workbox.routing.registerRoute(/\/api\//, workbox.strategies.networkFirst())
/**
* Handle third party requests
*/
workbox.routing.registerRoute(
/^https:\/\/gw.alipayobjects.com\//,
workbox.strategies.networkFirst()
)
workbox.routing.registerRoute(
/^https:\/\/cdnjs.cloudflare.com\//,
workbox.strategies.networkFirst()
)
workbox.routing.registerRoute(/\/color.less/, workbox.strategies.networkFirst())
/**
* Response to client after skipping waiting with MessageChannel
*/
addEventListener('message', event => {
const replyPort = event.ports[0]
const message = event.data
if (replyPort && message && message.type === 'skip-waiting') {
event.waitUntil(
self
.skipWaiting()
.then(
() => replyPort.postMessage({ error: null }),
error => replyPort.postMessage({ error })
)
)
}
})
const getters = {
device: state => state.app.device,
lang: state => state.app.lang
}
export default getters
import { mapState } from 'vuex'
const i18nMixin = {
computed: {
...mapState({
currentLang: state => state.app.lang
})
},
methods: {
setLang (lang) {
this.$store.dispatch('setLang', lang)
}
}
}
export default i18nMixin
import Vue from 'vue'
import Vuex from 'vuex'
import app from './modules/app'
import getters from './getters'
Vue.use(Vuex)
export default new Vuex.Store({
modules: {
app
},
state: {
},
mutations: {
},
actions: {
},
getters
})
import Vue from 'vue'
import {
SIDEBAR_TYPE,
TOGGLE_DEVICE,
TOGGLE_NAV_THEME,
TOGGLE_LAYOUT,
TOGGLE_FIXED_HEADER,
TOGGLE_FIXED_SIDEBAR,
TOGGLE_CONTENT_WIDTH,
TOGGLE_HIDE_HEADER,
TOGGLE_COLOR,
TOGGLE_WEAK,
// i18n
APP_LANGUAGE
} from '../mutation-types'
import { loadLanguageAsync } from '@/locales'
const App = {
state: {
sideCollapsed: true,
device: 'desktop',
theme: 'dark',
layout: '',
contentWidth: '',
fixedHeader: false,
fixedSidebar: false,
autoHideHeader: false,
color: '',
weak: false,
lang: 'en-US'
},
mutations: {
[SIDEBAR_TYPE]: (state, type) => {
state.sideCollapsed = type
Vue.ls.set(SIDEBAR_TYPE, type)
},
[TOGGLE_DEVICE]: (state, device) => {
state.device = device
},
[TOGGLE_NAV_THEME]: (state, theme) => {
state.theme = theme
Vue.ls.set(TOGGLE_NAV_THEME, theme)
},
[TOGGLE_LAYOUT]: (state, mode) => {
state.layout = mode
Vue.ls.set(TOGGLE_LAYOUT, mode)
},
[TOGGLE_FIXED_HEADER]: (state, mode) => {
state.fixedHeader = mode
Vue.ls.set(TOGGLE_FIXED_HEADER, mode)
},
[TOGGLE_FIXED_SIDEBAR]: (state, mode) => {
state.fixedSidebar = mode
Vue.ls.set(TOGGLE_FIXED_SIDEBAR, mode)
},
[TOGGLE_CONTENT_WIDTH]: (state, type) => {
state.contentWidth = type
Vue.ls.set(TOGGLE_CONTENT_WIDTH, type)
},
[TOGGLE_HIDE_HEADER]: (state, type) => {
state.autoHideHeader = type
Vue.ls.set(TOGGLE_HIDE_HEADER, type)
},
[TOGGLE_COLOR]: (state, color) => {
state.color = color
Vue.ls.set(TOGGLE_COLOR, color)
},
[TOGGLE_WEAK]: (state, mode) => {
state.weak = mode
Vue.ls.set(TOGGLE_WEAK, mode)
},
[APP_LANGUAGE]: (state, lang) => {
console.log('lang', lang)
state.lang = lang
Vue.ls.set(APP_LANGUAGE, lang)
}
},
actions: {
setLang ({ commit }, lang) {
return new Promise((resolve, reject) => {
commit(APP_LANGUAGE, lang)
loadLanguageAsync(lang).then(() => {
resolve()
}).catch((e) => {
reject(e)
})
})
}
}
}
export default App
export const ACCESS_TOKEN = 'Access-Token'
export const SIDEBAR_TYPE = 'sidebar_type'
export const TOGGLE_DEVICE = 'device'
export const TOGGLE_NAV_THEME = 'nav_theme'
export const TOGGLE_LAYOUT = 'layout'
export const TOGGLE_FIXED_HEADER = 'fixed_header'
export const TOGGLE_FIXED_SIDEBAR = 'fixed_sidebar'
export const TOGGLE_CONTENT_WIDTH = 'content_width'
export const TOGGLE_HIDE_HEADER = 'auto_hide_header'
export const TOGGLE_COLOR = 'color'
export const TOGGLE_WEAK = 'weak'
export const APP_LANGUAGE = 'app_language'
export const CONTENT_WIDTH_TYPE = {
Fluid: 'Fluid',
Fixed: 'Fixed'
}
<template>
<page-header-wrapper
:tab-list="tabList"
:tab-active-key="tabActiveKey"
:tab-change="(key) => {
this.tabActiveKey = key
console.log('PageHeader::tabChange', key)
}"
@back="() => {
console.log('PageHeader::@back')
}"
:back="() => {
// 自定义 back,不会覆盖 onBack 事件
console.log('PageHeader::.back')
}"
>
<template v-slot:content>
<span>{{ $t('pages.form.basicform.content') }}</span>
</template>
<template v-slot:extraContent>
<div><a-button>{{ $t('pages.form.basicform.headers.btn1') }}</a-button></div>
</template>
<div>
<strong>Block Page</strong>
</div>
</page-header-wrapper>
</template>
<script>
export default {
name: 'BlockPage',
data () {
return {
console: window.console,
tabList: [
{ tab: 'pages.form.basicform.tabs.tab1', key: 'tab1' },
{ tab: 'pages.form.basicform.tabs.tab2', key: 'tab2' },
{ tab: 'pages.form.basicform.tabs.tab3', key: 'tab3' }
],
tabActiveKey: 'tab1'
}
},
methods: {
handleTabChange (key) {
this.tabActiveKey = key
console.log('PageHeader::tabChange', key)
}
}
}
</script>
<style scoped>
</style>
<template>
<page-header-wrapper>
<div class="page-test-wrapper">
<h1>Test Page1 keepAlive: {{ $route.meta.keepAlive }}</h1>
<div class="box">
<div style="margin: 1em 0">
<a-input v-model="newVal" placeholder="..."/>
</div>
<a-button @click="handleClick">Click Me!</a-button>
<a-divider />
<p>{{ newVal }}</p>
</div>
</div>
</page-header-wrapper>
</template>
<script>
export default {
data () {
return {
newVal: ''
}
},
methods: {
handleClick (e) {
this.$router.push({ path: '/dashboard/analysis' })
}
}
}
</script>
<style lang="less" scoped>
.page-test-wrapper {
width: 100%;
max-width: 400px;
margin: 0 auto;
}
</style>
<template>
<page-header-wrapper>
<div class="page-test-wrapper">
<h1>Test Page2 keepAlive: {{ $route.meta.keepAlive }}</h1>
<div class="box">
<div style="margin: 1em 0">
<a-input v-model="newVal" placeholder="..."/>
</div>
<a-button @click="handleClick">Click Me!</a-button>
<a-divider />
<p>{{ newVal }}</p>
</div>
</div>
</page-header-wrapper>
</template>
<script>
export default {
data () {
return {
newVal: ''
}
},
methods: {
handleClick (e) {
this.$router.push({ path: '/dashboard/analysis' })
}
}
}
</script>
<style lang="less" scoped>
.page-test-wrapper {
width: 100%;
max-width: 400px;
margin: 0 auto;
}
</style>
<template>
<page-header-wrapper>
<div class="page-test-wrapper">
<h1>Test Page3 keepAlive: {{ $route.meta.keepAlive }}</h1>
<div class="box">
<div style="margin: 1em 0">
<a-input v-model="newVal" placeholder="..."/>
</div>
<a-button @click="handleClick">Click Me!</a-button>
<a-divider />
<p>{{ newVal }}</p>
</div>
</div>
</page-header-wrapper>
</template>
<script>
export default {
data () {
return {
newVal: ''
}
},
methods: {
handleClick (e) {
this.$router.push({ path: '/dashboard/analysis' })
}
}
}
</script>
<style lang="less" scoped>
.page-test-wrapper {
width: 100%;
max-width: 400px;
margin: 0 auto;
}
</style>
<template>
<page-header-wrapper
:tab-list="tabList"
:tab-active-key="tabActiveKey"
:tab-change="(key) => {
this.tabActiveKey = key
console.log('PageHeader::tabChange', key)
}"
@back="() => {
console.log('PageHeader::@back')
}"
:back="() => {
// 自定义 back,不会覆盖 onBack 事件
console.log('PageHeader::.back')
}"
>
<template v-slot:extra>
<a-button-group style="margin-right: 4px;">
<a-button>操作</a-button>
<a-button>操作</a-button>
<a-button><a-icon type="ellipsis"/></a-button>
</a-button-group>
<a-button type="primary" >主操作</a-button>
</template>
<template v-slot:extraContent>
<a-row class="status-list">
<a-col :xs="12" :sm="12">
<div class="text">状态</div>
<div class="heading">待审批</div>
</a-col>
<a-col :xs="12" :sm="12">
<div class="text">订单金额</div>
<div class="heading">¥ 568.08</div>
</a-col>
</a-row>
</template>
<template v-slot:content>
<span>{{ $t('pages.dashboard.analysis.content') }}</span>
</template>
<div>
<strong>Analysis Page</strong>
<p>...</p>
<p>...</p>
<p>...</p>
<p>...</p>
<p>...</p>
<p>很长的内容...</p>
<p>...</p>
<p>...</p>
<p>...</p>
<p>...</p>
<p>...</p>
<p>很长的内容...</p>
<p>...</p>
<p>...</p>
<p>...</p>
<p>...</p>
<p>...</p>
<p>很长的内容...</p>
<p>...</p>
<p>...</p>
<p>...</p>
<p>...</p>
<p>...</p>
</div>
</page-header-wrapper>
</template>
<script>
export default {
name: 'Analysis',
data () {
return {
console: window.console,
tabList: [
{ tab: 'pages.form.basicform.tabs.tab1', key: 'tab1' },
{ tab: 'pages.form.basicform.tabs.tab2', key: 'tab2' },
{ tab: 'pages.form.basicform.tabs.tab3', key: 'tab3' }
],
tabActiveKey: 'tab1'
}
},
methods: {
handleTabChange (key) {
this.tabActiveKey = key
console.log('PageHeader::tabChange', key)
}
}
}
</script>
<style scoped>
</style>
<template>
<div>
Workplace Page
</div>
</template>
<script>
export default {
name: 'Analysis',
data () {
return {
console: window.console,
tabList: [
{ tab: 'pages.form.basicform.tabs.tab1', key: 'tab1' },
{ tab: 'pages.form.basicform.tabs.tab2', key: 'tab2' },
{ tab: 'pages.form.basicform.tabs.tab3', key: 'tab3' }
],
tabActiveKey: 'tab1'
}
},
methods: {
handleTabChange (key) {
this.tabActiveKey = key
console.log('PageHeader::tabChange', key)
}
}
}
</script>
<style scoped>
</style>
<template>
<page-header-wrapper
:tab-list="tabList"
:tab-active-key="tabActiveKey"
:title="false"
:tab-change="(key) => {
this.tabActiveKey = key
console.log('PageHeader::tabChange', key)
}"
@back="() => {
console.log('PageHeader::@back')
}"
:back="() => {
// 自定义 back,不会覆盖 onBack 事件
console.log('PageHeader::.back')
}"
>
<template v-slot:content>
<span>{{ $t('pages.form.basicform.content') }}</span>
</template>
<template v-slot:extraContent>
<div><a-button>{{ $t('pages.form.basicform.headers.btn1') }}</a-button></div>
</template>
<div>
<strong>Advanced Form</strong>
</div>
</page-header-wrapper>
</template>
<script>
export default {
name: 'AdvancedForm',
data () {
return {
console: window.console,
tabList: [
{ tab: 'pages.form.basicform.tabs.tab1', key: 'tab1' },
{ tab: 'pages.form.basicform.tabs.tab2', key: 'tab2' },
{ tab: 'pages.form.basicform.tabs.tab3', key: 'tab3' }
],
tabActiveKey: 'tab1'
}
},
methods: {
handleTabChange (key) {
this.tabActiveKey = key
console.log('PageHeader::tabChange', key)
}
}
}
</script>
<style scoped>
</style>
<template>
<page-header-wrapper
:tab-list="tabList"
:tab-active-key="tabActiveKey"
:tab-change="(key) => {
this.tabActiveKey = key
console.log('PageHeader::tabChange', key)
}"
@back="() => {
console.log('PageHeader::@back')
}"
:back="() => {
// 自定义 back,不会覆盖 onBack 事件
console.log('PageHeader::.back')
}"
>
<template v-slot:title>
自定义标题
</template>
<template v-slot:tags>
<a-tag color="pink">tag1</a-tag>
<a-tag>tag2</a-tag>
</template>
<template v-slot:content>
<span>{{ $t('pages.form.basicform.content') }}</span>
</template>
<template v-slot:extraContent>
<div><a-button>{{ $t('pages.form.basicform.headers.btn1') }}</a-button></div>
</template>
<div>
<strong>Basic Form</strong>
</div>
</page-header-wrapper>
</template>
<script>
export default {
name: 'BasicForm',
data () {
return {
console: window.console,
tabList: [
{ tab: 'pages.form.basicform.tabs.tab1', key: 'tab1' },
{ tab: 'pages.form.basicform.tabs.tab2', key: 'tab2' },
{ tab: 'pages.form.basicform.tabs.tab3', key: 'tab3' }
],
tabActiveKey: 'tab1'
}
},
methods: {
handleTabChange (key) {
this.tabActiveKey = key
console.log('PageHeader::tabChange', key)
}
}
}
</script>
<style scoped>
</style>
<template>
<page-header-wrapper
:tab-list="tabList"
:tab-active-key="tabActiveKey"
:tab-change="(key) => {
this.tabActiveKey = key
console.log('PageHeader::tabChange', key)
}"
:breadcrumb="customBreadcrumb"
>
<template v-slot:content>
<span>{{ $t('pages.form.basicform.content') }}</span>
</template>
<template v-slot:extraContent>
<div><a-button>{{ $t('pages.form.basicform.headers.btn1') }}</a-button></div>
</template>
<div>
<strong>Step Form</strong>
</div>
</page-header-wrapper>
</template>
<script>
export default {
name: 'StepForm',
data () {
return {
console: window.console,
tabList: [
{ tab: 'pages.form.basicform.tabs.tab1', key: 'tab1' },
{ tab: 'pages.form.basicform.tabs.tab2', key: 'tab2' },
{ tab: 'pages.form.basicform.tabs.tab3', key: 'tab3' }
],
tabActiveKey: 'tab1'
}
},
computed: {
customBreadcrumb () {
return {
props: {
routes: this.$route.matched.concat().map(route => {
return {
path: route.path,
breadcrumbName: this.$t(route.meta.title)
}
}),
itemRender: ({ route, params, routes, paths, h }) => {
return routes.indexOf(route) === routes.length - 1 && (
<span>{route.breadcrumbName}</span>
) || (
<router-link to={{ path: route.path || '/' }}>{route.breadcrumbName}</router-link>
)
}
}
}
}
},
methods: {
handleTabChange (key) {
this.tabActiveKey = key
console.log('PageHeader::tabChange', key)
}
}
}
</script>
<style scoped>
</style>
import { createApp, reactive, watchEffect } from 'vue';
import { Card, Space, Button, Tag } from 'ant-design-vue';
import 'ant-design-vue/dist/antd.less';
const DemoComponent = {
setup() {
const state = reactive({
name: 'value',
});
return () => (
<div class="components">
<h2># Template</h2>
<Card style={{ marginBottom: '24px', background: 'rgb(244,244,244)' }}>
<Space size="middle">
<Button
type="primary"
onClick={() => {
state.name = new Date().getTime().toString()
}}
>
Change Value
</Button>
</Space>
<div style={{ margin: '12px 0' }}>
<p>state.name: { JSON.stringify(state.name) }</p>
</div>
</Card>
<div class="demo" style="background: rgb(244,244,244);">
<div class="container" style="width: 256px;">
demo in here.<br />
<Tag color="pink">{state.name}</Tag>
</div>
</div>
</div>
);
},
};
const app = createApp(DemoComponent);
app.mount('#__vue-content>div');
const path = require('path')
const { IgnorePlugin } = require('webpack')
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
const dynamicThemePlugin = require('./config/dynamicTheme.js')
const isProd = process.env.NODE_ENV === 'production'
const isAnalyz = process.env.IS_ANALYZ === 'true'
function resolve (dir) {
return path.join(__dirname, dir)
}
const assetsCDN = {
externals: {
'vue': 'Vue',
'vue-router': 'VueRouter',
'vuex': 'Vuex',
'axios': 'axios'
},
assets: {
css: [],
// https://unpkg.com/browse/vue@2.6.10/
js: [
'//cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.min.js',
'//cdn.jsdelivr.net/npm/vue-router@3.1.3/dist/vue-router.min.js',
'//cdn.jsdelivr.net/npm/vuex@3.1.1/dist/vuex.min.js',
'//cdn.jsdelivr.net/npm/axios@0.19.0/dist/axios.min.js'
]
}
}
// vue.config
const defaultConfig = {
configureWebpack: {
plugins: [
// Ignore all locale files of moment.js
new IgnorePlugin(/^\.\/locale$/, /moment$/)
],
resolve: {
alias: {
'@ant-design/icons/lib/dist$': resolve('./src/core/antd/icons.js')
}
},
externals: isProd ? assetsCDN.externals : {}
},
chainWebpack: (config) => {
config.resolve.alias
.set('@config', resolve('./config'))
.set('@ant-design-vue/pro-layout', resolve('../src'))
// if `production` env require on cdn assets
isProd && config.plugin('html').tap(args => {
args[0].cdn = assetsCDN.assets
return args
})
// if `IS_ANALYZ` env is TRUE on report bundle info
isAnalyz && config.plugin('webpack-report').use(BundleAnalyzerPlugin, [
{
analyzerMode: 'static'
}
])
const svgRule = config.module.rule('svg')
svgRule.uses.clear()
svgRule.oneOf('inline')
.resourceQuery(/inline/)
.use('vue-svg-icon-loader')
.loader('vue-svg-icon-loader')
.end()
.end()
.oneOf('external')
.use('file-loader')
.loader('file-loader')
.options({
name: 'assets/[name].[hash:8].[ext]'
})
},
css: {
loaderOptions: {
less: {
modifyVars: {
// less vars,customize ant design theme
// 'primary-color': '#F5222D',
// 'link-color': '#F5222D',
// 'border-radius-base': '4px'
},
// DO NOT REMOVE THIS LINE
javascriptEnabled: true
}
}
},
devServer: {
// default development server port 8000
port: 9001
// If you want to turn on the proxy, please remove the mockjs /src/main.jsL11
// proxy: {
// '/api': {
// target: 'https://mock.ihx.me/mock/5baf3052f7da7e07e04a5116/antd-pro',
// ws: false,
// changeOrigin: true
// }
// }
},
// disable source map in production
productionSourceMap: false,
lintOnSave: undefined,
// babel-loader no-ignore node_modules/*
transpileDependencies: []
}
if (!isProd) {
defaultConfig.configureWebpack.plugins.push(dynamicThemePlugin())
}
module.exports = defaultConfig
{
"name": "@ant-design-vue/pro-layout",
"version": "1.0.1",
"main": "./lib/index.js",
"module": "./es/index.js",
"repository": {
"type": "git",
"url": "https://github.com/vueComponent/pro-layout"
"name": "@ant-design-vue/pro-layout",
"version": "3.0.0-alpha.1",
"main": "./lib/index.js",
"module": "./es/index.js",
"sideEffects": false,
"repository": {
"type": "git",
"url": "https://github.com/ant-design/pro-layout"
},
"contributors": [
"tangjinzhou <415800467@qq.com>",
"Sendya <yladmxa@gmail.com>"
],
"license": "MIT",
"scripts": {
"clean": "./scripts/cleanup.sh",
"start": "vc-tools run server",
"lint": "eslint src/ --ext .tsx,.ts",
"compile": "vc-tools run compile",
"test": "cross-env NODE_ENV=test jest --config .jest.js",
"prepublishOnly": "npm run lint && npm run generate && npm run compile && npm run test",
"generate": "rimraf src/icons && TS_NODE_PROJECT=scripts/tsconfig.json node -r ts-node/register scripts/generate.ts --target=icon",
"postcompile": "npm run clean && TS_NODE_PROJECT=scripts/tsconfig.json node -r ts-node/register scripts/generate.ts --target=entry"
},
"peerDependencies": {
"vue": ">=3.0.0",
"ant-design-vue": ">=2.0.0"
},
"devDependencies": {
"@vue/babel-plugin-jsx": "^1.0.0-rc.3",
"@babel/runtime": "^7.11.2",
"@types/jest": "^24.0.17",
"@types/node": "^13.13.15",
"@typescript-eslint/eslint-plugin": "^2.33.0",
"@typescript-eslint/parser": "^2.33.0",
"@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-eslint": "~4.5.0",
"@vue/cli-plugin-typescript": "~4.5.0",
"@vue/cli-service": "~4.5.0",
"@vue/compiler-sfc": "^3.0.0-0",
"@vue/eslint-config-prettier": "^6.0.0",
"@vue/eslint-config-typescript": "^5.0.2",
"@vue/test-utils": "^2.0.0-beta.2",
"cross-env": "^5.2.0",
"eslint": "^6.7.2",
"eslint-config-prettier": "^6.4.0",
"eslint-plugin-prettier": "^3.1.3",
"eslint-plugin-vue": "^7.0.0-0",
"jest": "^25.4.0",
"jest-serializer-vue": "^2.0.2",
"prettier": "^1.19.1",
"ts-node": "^8.10.2",
"typescript": "~3.9.3",
"vc-tools": "^3.0.0",
"vue": "^3.0.0-0",
"vue-jest": "^5.0.0-alpha.3"
},
"dependencies": {
"@babel/runtime": "^7.10.4",
"ant-design-vue": "^2.0.0-beta.10",
"lodash": "^4.17.15"
},
"config": {
"port": 9528,
"entry": {
"@ant-design-vue/pro-layout": [
"./src/index.jsx"
]
},
"contributors": [
"tangjinzhou <415800467@qq.com>",
"Sendya <yladmxa@gmail.com>"
],
"license": "MIT",
"scripts": {
"start": "vc-tools run server",
"lint": "eslint -c ./.eslintrc --fix --ext .jsx,.js,.vue ./src",
"compile": "vc-tools run compile --babel-runtime",
"test": "cross-env NODE_ENV=test jest --config .jest.js",
"prepublishOnly": "npm run lint && npm run test && npm run compile && np --no-cleanup --yolo --no-publish"
},
"files": [
"es",
"lib"
],
"peerDependencies": {
"ant-design-vue": "^1.6.2",
"umi-request": "^1.2.11",
"vue": ">=2.6.0",
"vue-container-query": "^0.1.0",
"vue-template-compiler": ">=2.6.0"
},
"devDependencies": {
"@ant-design-vue/tools": "^1.0.2-beta.2",
"@babel/core": "^7.11.1",
"@babel/plugin-transform-runtime": "^7.5.5",
"@babel/preset-env": "^7.5.5",
"@types/jest": "^24.0.17",
"@types/node": "^10.5.5",
"@types/vue": "^2.0.0",
"@vue/server-test-utils": "1.0.0-beta.16",
"@vue/test-utils": "1.0.0-beta.16",
"babel-core": "^7.0.0-bridge.0",
"babel-eslint": "^10.1.0",
"babel-helper-vue-jsx-merge-props": "^2.0.3",
"babel-plugin-transform-class-properties": "^6.24.1",
"babel-plugin-transform-object-assign": "^6.22.0",
"babel-plugin-transform-object-rest-spread": "^6.26.0",
"babel-plugin-transform-runtime": "^6.23.0",
"babel-plugin-transform-vue-jsx": "^4.0.1",
"babel-runtime": "^6.26.0",
"cross-env": "^5.2.0",
"eslint": "^6.5.1",
"eslint-config-prettier": "^6.4.0",
"eslint-plugin-vue": "^5.2.3",
"jest": "^26.3.0",
"jest-serializer-vue": "^2.0.2",
"np": "^5.1.1",
"prettier": "^1.18.2",
"regenerator-runtime": "^0.13.7",
"vue": ">=2.5.0",
"vue-jest": "^2.5.0",
"vue-template-compiler": ">=2.5.0"
},
"dependencies": {
"ant-design-vue": "^1.6.2",
"classnames": "^2.2.6",
"insert-css": "^2.0.0",
"lodash": "^4.17.15",
"omit.js": "^1.0.2",
"umi-request": "^1.2.11",
"vue-container-query": "^0.1.0",
"vue-copy-to-clipboard": "^1.0.3"
},
"config": {
"port": 9528,
"entry": {
"@ant-design-vue/pro-layout": [
"./src/index.js"
]
"loaders": [
{
"loader": "less-loader",
"options": {
"javascriptEnabled": true
}
},
"description": "ant-design-vue-pro layout, easy to use pro scaffolding."
}
]
},
"description": "Ant Design Pro Layout of Vue, easy to use pro scaffolding."
}
module.exports = {
plugins: {
autoprefixer: {}
}
}
{
"extends": [
"config:base"
]
}
#!/bin/sh
ls | grep -v .jest.js | grep -v .eslintrc.js | grep -e '.d.ts' -e '.js$' | xargs rm
# keep one d.ts to make vscode auto import working with corrcet path
rm ./es/icons/*.d.ts || echo
import * as allIconDefs from '@ant-design/icons-svg';
import { IconDefinition } from '@ant-design/icons-svg/es/types';
import * as path from 'path';
import * as fs from 'fs';
import { promisify } from 'util';
// eslint-disable-next-line import/no-extraneous-dependencies
import { template } from 'lodash';
const writeFile = promisify(fs.writeFile);
interface IconDefinitionWithIdentifier extends IconDefinition {
svgIdentifier: string;
}
function walk<T>(fn: (iconDef: IconDefinitionWithIdentifier) => Promise<T>) {
return Promise.all(
Object.keys(allIconDefs).map(svgIdentifier => {
const iconDef = (allIconDefs as { [id: string]: IconDefinition })[svgIdentifier];
return fn({ svgIdentifier, ...iconDef });
}),
);
}
async function generateIcons() {
const iconsDir = path.join(__dirname, '../src/icons');
try {
await promisify(fs.access)(iconsDir);
} catch (err) {
await promisify(fs.mkdir)(iconsDir);
}
const render = template(
`
// GENERATE BY ./scripts/generate.ts
// DON NOT EDIT IT MANUALLY
import { SetupContext } from 'vue';
import <%= svgIdentifier %>Svg from '@ant-design/icons-svg/lib/asn/<%= svgIdentifier %>';
import AntdIcon, { AntdIconProps } from '../components/AntdIcon';
const <%= svgIdentifier %> = (props: AntdIconProps, context: SetupContext) => {
const p = { ...props, ...context.attrs };
return <AntdIcon {...p} icon={<%= svgIdentifier %>Svg}></AntdIcon>;
};
<%= svgIdentifier %>.displayName = '<%= svgIdentifier %>';
<%= svgIdentifier %>.inheritAttrs = false;
export default <%= svgIdentifier %>;
`.trim(),
);
await walk(async ({ svgIdentifier }) => {
// generate icon file
await writeFile(
path.resolve(__dirname, `../src/icons/${svgIdentifier}.tsx`),
render({ svgIdentifier }),
);
});
// generate icon index
const entryText = Object.keys(allIconDefs)
.sort()
.map(svgIdentifier => {
return `export { default as ${svgIdentifier} } from './${svgIdentifier}';`;
})
.join('\n');
await promisify(fs.appendFile)(
path.resolve(__dirname, '../src/icons/index.tsx'),
`
// GENERATE BY ./scripts/generate.ts
// DON NOT EDIT IT MANUALLY
${entryText}
`.trim(),
);
}
async function generateEntries() {
const render = template(
`
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _<%= svgIdentifier %> = _interopRequireDefault(require('./lib/icons/<%= svgIdentifier %>'));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
var _default = _<%= svgIdentifier %>.default || _<%= svgIdentifier %>;
exports.default = _default;
module.exports = _default;
`.trim(),
);
await walk(async ({ svgIdentifier }) => {
// generate `Icon.js` in root folder
await writeFile(
path.resolve(__dirname, `../${svgIdentifier}.js`),
render({
svgIdentifier,
}),
);
// generate `Icon.d.ts` in root folder
await writeFile(
path.resolve(__dirname, `../${svgIdentifier}.d.ts`),
`export { default } from './lib/icons/${svgIdentifier}';`,
);
});
}
if (process.argv[2] === '--target=icon') {
generateIcons();
}
if (process.argv[2] === '--target=entry') {
generateEntries();
}
{
"compilerOptions": {
"module": "commonJS",
}
}
\ No newline at end of file
const { camelCase, upperFirst } = require('lodash');
const manifest = require('@ant-design/icons-svg/lib/manifest').default;
const themeMap = {
fill: 'filled',
outline: 'outlined', // default theme
twotone: 'twoTone',
};
exports.getComponentNameList = () => {
const icons = [];
Object.keys(manifest).forEach(theme => {
manifest[theme].forEach(name => {
const baseName = upperFirst(camelCase(name));
icons.push({
theme,
componentName: baseName + upperFirst(themeMap[theme]),
svgName: baseName + upperFirst(theme === 'twotone' ? 'twoTone' : theme),
});
});
});
return icons;
};
import './BasicLayout.less'
import PropTypes from 'ant-design-vue/es/_util/vue-types'
import 'ant-design-vue/es/layout/style'
import Layout from 'ant-design-vue/es/layout'
import { ContainerQuery } from 'vue-container-query'
import { SiderMenuWrapper, GlobalFooter } from './components'
import { contentWidthCheck, getComponentFromProp, isFun } from './utils/util'
import { SiderMenuProps } from './components/SiderMenu'
import HeaderView, { HeaderViewProps } from './Header'
import WrapContent from './WrapContent'
import ConfigProvider from './components/ConfigProvider'
import PageHeaderWrapper from './components/PageHeaderWrapper'
export const BasicLayoutProps = {
...SiderMenuProps,
...HeaderViewProps,
contentWidth: PropTypes.oneOf(['Fluid', 'Fixed']).def('Fluid'),
// contentWidth: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]).def('Fluid'),
locale: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]).def('en-US'),
breadcrumbRender: PropTypes.func,
disableMobile: PropTypes.bool.def(false),
mediaQuery: PropTypes.object.def({}),
handleMediaQuery: PropTypes.func,
footerRender: PropTypes.func,
}
const MediaQueryEnum = {
'screen-xs': {
maxWidth: 575
},
'screen-sm': {
minWidth: 576,
maxWidth: 767
},
'screen-md': {
minWidth: 768,
maxWidth: 991
},
'screen-lg': {
minWidth: 992,
maxWidth: 1199
},
'screen-xl': {
minWidth: 1200,
maxWidth: 1599
},
'screen-xxl': {
minWidth: 1600
}
}
const getPaddingLeft = (
hasLeftPadding,
collapsed = undefined,
siderWidth
) => {
if (hasLeftPadding) {
return collapsed ? 80 : siderWidth
}
return 0
}
const headerRender = (h, props) => {
if (props.headerRender === false) {
return null
}
return <HeaderView { ...{ props } } />
}
const defaultI18nRender = (key) => key
const BasicLayout = {
name: 'BasicLayout',
functional: true,
props: BasicLayoutProps,
render (h, content) {
const { props, children } = content
const {
layout,
// theme,
isMobile,
collapsed,
mediaQuery,
handleMediaQuery,
handleCollapse,
siderWidth,
fixSiderbar,
i18nRender = defaultI18nRender
} = props
const footerRender = getComponentFromProp(content, 'footerRender')
const rightContentRender = getComponentFromProp(content, 'rightContentRender')
const collapsedButtonRender = getComponentFromProp(content, 'collapsedButtonRender')
const menuHeaderRender = getComponentFromProp(content, 'menuHeaderRender')
const breadcrumbRender = getComponentFromProp(content, 'breadcrumbRender')
const headerContentRender = getComponentFromProp(content, 'headerContentRender')
const menuRender = getComponentFromProp(content, 'menuRender')
const isTopMenu = layout === 'topmenu'
const hasSiderMenu = !isTopMenu
// If it is a fix menu, calculate padding
// don't need padding in phone mode
const hasLeftPadding = fixSiderbar && !isTopMenu && !isMobile
const cdProps = {
...props,
hasSiderMenu,
footerRender,
menuHeaderRender,
rightContentRender,
collapsedButtonRender,
breadcrumbRender,
headerContentRender,
menuRender
}
return (
<ConfigProvider i18nRender={i18nRender} contentWidth={props.contentWidth} breadcrumbRender={breadcrumbRender}>
<ContainerQuery query={MediaQueryEnum} onChange={handleMediaQuery}>
<Layout class={{
'ant-pro-basicLayout': true,
'ant-pro-topmenu': isTopMenu,
...mediaQuery
}}>
<SiderMenuWrapper
{ ...{ props: cdProps } }
collapsed={collapsed}
onCollapse={handleCollapse}
/>
<Layout class={[layout]} style={{
paddingLeft: hasSiderMenu
? `${getPaddingLeft(!!hasLeftPadding, collapsed, siderWidth)}px`
: undefined,
minHeight: '100vh'
}}>
{headerRender(h, {
...cdProps,
mode: 'horizontal',
})}
<WrapContent class="ant-pro-basicLayout-content" contentWidth={props.contentWidth}>
{children}
</WrapContent>
<Layout.Footer>
{ footerRender && (
isFun(footerRender) && footerRender(h) || footerRender
) || (
<GlobalFooter>
<template slot="links">
<a href="https://www.github.com/vueComponent/" target="_self">Github</a>
<a href="https://www.github.com/sendya/" target="_self">@Sendya</a>
</template>
<template slot="copyright">
<a href="https://github.com/vueComponent">vueComponent</a>
</template>
</GlobalFooter>
)}
</Layout.Footer>
</Layout>
</Layout>
</ContainerQuery>
</ConfigProvider>
)
}
}
BasicLayout.install = function (Vue) {
Vue.component(PageHeaderWrapper.name, PageHeaderWrapper)
Vue.component('page-container', PageHeaderWrapper)
Vue.component('pro-layout', BasicLayout)
}
export default BasicLayout
@import "~ant-design-vue/es/style/themes/default";
@import '~ant-design-vue/es/style/themes/default.less';
@basicLayout-prefix-cls: ~'@{ant-prefix}-pro-basicLayout';
@sider-menu-prefix-cls: ~'@{ant-prefix}-pro-sider-menu';
@nav-header-height: @layout-header-height;
@pro-layout-header-height: 48px;
.@{basicLayout-prefix-cls} {
&:not('.ant-pro-basicLayout-mobile') {
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.06);
border-radius: 3px;
-webkit-box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.08);
}
/* 滚动条滑块 */
::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.12);
border-radius: 3px;
-webkit-box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.2);
}
}
// BFC
display: flex;
flex-direction: column;
width: 100%;
min-height: 100%;
.ant-layout-header {
&:not(.ant-pro-top-menu) {
background: @component-background;
}
&.ant-pro-fixed-header {
.@{ant-prefix}-layout-header {
&.@{ant-prefix}-pro-fixed-header {
position: fixed;
top: 0;
}
......@@ -44,58 +20,45 @@
&-content {
position: relative;
margin: 24px;
transition: all 0.2s;
.@{ant-prefix}-pro-page-header-wrap {
.@{ant-prefix}-pro-page-container {
margin: -24px -24px 0;
}
&-disable-margin {
margin: 0;
> div > .@{ant-prefix}-pro-page-header-wrap {
.@{ant-prefix}-pro-page-container {
margin: 0;
}
}
> .ant-layout {
> .@{ant-prefix}-layout {
max-height: 100%;
}
}
// append hook styles
.ant-layout-sider-children {
height: 100%;
}
.trigger {
font-size: 18px;
line-height: 64px;
padding: 0 24px;
cursor: pointer;
transition: color 0.3s;
&:hover {
color: #1890ff;
}
// children should support fixed
.@{basicLayout-prefix-cls}-is-children.@{basicLayout-prefix-cls}-fix-siderbar {
height: 100vh;
overflow: hidden;
transform: rotate(0);
}
&-content {
position: relative;
margin: 24px;
transition: all 0.2s;
.@{ant-prefix}-pro-page-header-wrap {
margin: -24px -24px 0;
.@{basicLayout-prefix-cls}-has-header {
// tech-page-container
.tech-page-container {
height: calc(100vh - @pro-layout-header-height);
}
&-disable-margin {
margin: 0;
> div > .@{ant-prefix}-pro-page-header-wrap {
margin: 0;
.@{basicLayout-prefix-cls}-is-children.@{basicLayout-prefix-cls}-has-header {
.tech-page-container {
height: calc(100vh - @pro-layout-header-height - @pro-layout-header-height);
}
.@{basicLayout-prefix-cls}-is-children {
min-height: calc(100vh - @pro-layout-header-height);
&.@{basicLayout-prefix-cls}-fix-siderbar {
height: calc(100vh - @pro-layout-header-height);
}
}
}
> .ant-layout {
max-height: 100%;
}
}
.color-picker {
margin: 10px 0;
}
}
import './BasicLayoutTest.less';
import { h, App } from 'vue';
import { Layout, Menu } from 'ant-design-vue';
import { MenuUnfoldOutlined, MenuFoldOutlined } from '@ant-design/icons-vue';
import ProProvider from './ProProvider';
const defaultI18nRender = (key: string) => key
const BasicLayout = (props, { emit, slots }) => {
const handleClick = () => {
emit('collapsed', !props.collapsed)
}
return (
<ProProvider {...props} i18n={defaultI18nRender}>
<Layout class="ant-pro-basicLayout">
<Layout.Sider collapsed={props.collapsed} trigger={null} collapsible>
<div class="logo" />
<Menu theme="dark" mode="inline" selectedKeys={props.selectedKeys}>
<Menu.Item key="1">
<span>nav 1</span>
</Menu.Item>
<Menu.Item key="2">
<span>nav 2</span>
</Menu.Item>
<Menu.Item key="3">
<span>nav 3</span>
</Menu.Item>
</Menu>
</Layout.Sider>
<Layout>
<Layout.Header style="background: #fff; padding: 0">
{
props.collapsed
? <MenuUnfoldOutlined class="trigger" onClick={handleClick} />
: <MenuFoldOutlined class="trigger" onClick={handleClick} />
}
</Layout.Header>
<Layout.Content style={{ margin: '24px 16px', padding: '24px', background: '#fff', minHeight: '280px' }}>
{slots.default && slots.default()}
</Layout.Content>
</Layout>
</Layout>
</ProProvider>
)
};
BasicLayout.install = function (app: App) {
app.component('pro-layout', BasicLayout)
};
export default BasicLayout
.ant-pro-basicLayout {
.logo {
height: 32px;
background: rgba(255, 255, 255, 0.2);
margin: 16px;
}
.trigger {
font-size: 18px;
line-height: 64px;
padding: 0 24px;
cursor: pointer;
transition: color 0.3s;
&:hover {
color: #1890ff;
}
}
}
const BlockLayout = {
name: 'BlockLayout',
functional: true,
render (createElement, content) {
return content.children
}
}
export default BlockLayout
@import '~ant-design-vue/es/style/themes/default.less';
@pro-footer-bar-prefix-cls: ~'@{ant-prefix}-pro-footer-bar';
.@{pro-footer-bar-prefix-cls} {
position: fixed;
right: 0;
bottom: 0;
z-index: 99;
display: flex;
width: 100%;
align-items: center;
padding: 0 24px;
line-height: 48px;
background: @component-background;
border-top: 1px solid @border-color-split;
box-shadow: @box-shadow-base;
&-left {
flex: 1;
}
&-right {
> * {
margin-right: 8px;
&:last-child {
margin: 0;
}
}
}
}
\ No newline at end of file
import { VNodeChild } from 'vue';
import './index.less';
export interface FooterToolbarProps {
extra?: VNodeChild;
renderContent?: (
props: FooterToolbarProps & { leftWidth?: string },
dom: JSX.Element,
) => VNodeChild | JSX.Element;
prefixCls?: string;
}
const FooterToolbar = (props, context) => {
const baseClassName = `${props.prefixCls}-footer-bar`;
}
@import "~ant-design-vue/es/style/themes/default";
@import '~ant-design-vue/es/style/themes/default.less';
@global-footer-prefix-cls: ~'@{ant-prefix}-pro-global-footer';
@pro-global-footer-prefix-cls: ~'@{ant-prefix}-pro-global-footer';
.@{global-footer-prefix-cls} {
.@{pro-global-footer-prefix-cls} {
margin: 48px 0 24px 0;
padding: 0 16px;
text-align: center;
......@@ -28,4 +28,4 @@
color: @text-color-secondary;
font-size: @font-size-base;
}
}
}
\ No newline at end of file
import './index.less';
import { WithFalse } from '../typings';
import { defineComponent, PropType, SetupContext, VNodeChild } from 'vue';
export type Links = WithFalse<
{
key?: string;
title: VNodeChild | JSX.Element;
href: string;
blankTarget?: boolean;
}[]
>
export interface GlobalFooterProps {
links?: Links;
copyright?: VNodeChild | JSX.Element;
prefixCls?: string;
}
export default defineComponent({
name: 'GlobalFooter',
props: {
links: [Array, Boolean] as PropType<Links>,
copyright: [Object, Function] as PropType<VNodeChild | JSX.Element>,
prefixCls: {
type: String,
default: 'ant-pro'
}
},
setup (props: GlobalFooterProps, { slots }: SetupContext) {
if (
(props.links == null || props.links === false || (Array.isArray(props.links) && props.links.length === 0)) &&
(props.copyright == null || props.copyright === false)
) {
return null;
}
const baseClassName = `${props.prefixCls}-global-footer`;
const copyright = props.copyright || (slots.copyright && slots.copyright());
return () => (
<footer class={baseClassName}>
{props.links && (
<div class={`${baseClassName}-links`}>
{props.links.map(link => (
<a
key={link.key}
title={link.key}
target={link.blankTarget ? '_blank' : '_self'}
href={link.href}
>
{link.title}
</a>
))}
</div>
)}
{props.copyright && <div class={`${baseClassName}-copyright`}>{copyright}</div>}
</footer>
);
}
});
@import '~ant-design-vue/es/style/themes/default.less';
@pro-layout-grid-content-prefix-cls: ~'@{ant-prefix}-pro-grid-content';
.@{pro-layout-grid-content-prefix-cls} {
width: 100%;
&.wide {
max-width: 1200px;
margin: 0 auto;
}
}
\ No newline at end of file
import './GridContent.less';
import { SetupContext, CSSProperties } from 'vue'
import { PureSettings } from '../defaultSettings';
interface GridContentProps {
contentWidth?: PureSettings['contentWidth'];
prefixCls?: string;
style?: CSSProperties;
}
const GridContent = ({ prefixCls = 'ant-pro', contentWidth }: GridContentProps, { slots }: SetupContext) => {
return (
<div
class={{
[`${prefixCls}-grid-content`]: true,
wide: contentWidth === 'Fixed',
}}
>
<div class={`${prefixCls}-grid-content-children`}>{slots.default && slots.default()}</div>
</div>
);
};
export default GridContent;
import './Header.less'
import 'ant-design-vue/es/layout/style'
import Layout from 'ant-design-vue/es/layout'
import PropTypes from 'ant-design-vue/es/_util/vue-types'
import BaseMenu from './components/RouteMenu/BaseMenu'
import { defaultRenderLogoAntTitle, SiderMenuProps } from './components/SiderMenu/SiderMenu'
import GlobalHeader, { GlobalHeaderProps } from './components/GlobalHeader'
import { VueFragment } from './components'
import { isFun } from './utils/util'
const { Header } = Layout
export const HeaderViewProps = {
...GlobalHeaderProps,
...SiderMenuProps,
isMobile: PropTypes.bool.def(false),
collapsed: PropTypes.bool,
logo: PropTypes.any,
hasSiderMenu: PropTypes.bool,
autoHideHeader: PropTypes.bool,
menuRender: PropTypes.any,
headerRender: PropTypes.any,
rightContentRender: PropTypes.any,
visible: PropTypes.bool.def(true),
}
const renderContent = (h, props) => {
const isTop = props.layout === 'topmenu'
const maxWidth = 1200 - 280 - 120
const contentWidth = props.contentWidth === 'Fixed'
const baseCls = 'ant-pro-top-nav-header'
const { logo, title, theme, isMobile, headerRender, rightContentRender, menuHeaderRender } = props
const rightContentProps = { theme, isTop, isMobile }
let defaultDom = <GlobalHeader {...{ props: props }} />
if (isTop && !isMobile) {
defaultDom = (
<div class={[baseCls, theme]}>
<div class={[`${baseCls}-main`, contentWidth ? 'wide' : '']}>
{menuHeaderRender && (
<div class={`${baseCls}-left`}>
<div class={`${baseCls}-logo`} key="logo" id="logo">
{defaultRenderLogoAntTitle(h, { logo, title, menuHeaderRender })}
</div>
</div>
)}
<div class={`${baseCls}-menu`} style={{ maxWidth: `${maxWidth}px`, flex: 1 }}>
<BaseMenu {...{ props: props }} />
</div>
{isFun(rightContentRender) && rightContentRender(h, rightContentProps) || rightContentRender}
</div>
</div>
)
}
if (headerRender) {
return headerRender(h, props)
}
return defaultDom
}
const HeaderView = {
name: 'HeaderView',
props: HeaderViewProps,
render (h) {
const {
visible,
isMobile,
layout,
collapsed,
siderWidth,
fixedHeader,
autoHideHeader,
hasSiderMenu,
} = this.$props
const props = this.$props
const isTop = layout === 'topmenu'
const needSettingWidth = fixedHeader && hasSiderMenu && !isTop && !isMobile
const className = {
'ant-pro-fixed-header': fixedHeader,
'ant-pro-top-menu': isTop,
}
// 没有 <></> 暂时代替写法
return (
visible ? (
<VueFragment>
{ fixedHeader && <Header />}
<Header
style={{
padding: 0,
width: needSettingWidth
? `calc(100% - ${collapsed ? 80 : siderWidth}px)`
: '100%',
zIndex: 9,
right: fixedHeader ? 0 : undefined
}}
class={className}
>
{renderContent(h, props)}
</Header>
</VueFragment>
) : null
)
}
}
export default HeaderView
@import "~ant-design-vue/es/style/themes/default";
@top-nav-header-prefix-cls: ~'@{ant-prefix}-pro-top-nav-header';
@pro-layout-fixed-header-prefix-cls: ~'@{ant-prefix}-pro-fixed-header';
.@{top-nav-header-prefix-cls} {
position: relative;
width: 100%;
height: @layout-header-height;
box-shadow: @box-shadow-base;
transition: background 0.3s, width 0.2s;
.ant-menu-submenu.ant-menu-submenu-horizontal {
height: 100%;
line-height: @layout-header-height;
.ant-menu-submenu-title {
height: 100%;
}
}
&.light {
background-color: @component-background;
h1 {
color: #002140;
}
}
&-main {
display: flex;
height: @layout-header-height;
padding-left: 24px;
&.wide {
max-width: 1200px;
margin: auto;
padding-left: 0;
}
.left {
display: flex;
flex: 1;
}
.right {
width: 324px;
}
}
&-logo {
position: relative;
width: 165px;
height: @layout-header-height;
overflow: hidden;
line-height: @layout-header-height;
transition: all 0.3s;
img, svg {
display: inline-block;
height: 32px;
width: 32px;
vertical-align: middle;
}
h1 {
display: inline-block;
margin: 0 0 0 12px;
color: @btn-primary-color;
font-weight: 400;
font-size: 16px;
vertical-align: top;
}
}
&-menu {
.ant-menu.ant-menu-horizontal {
height: @layout-header-height;
line-height: @layout-header-height;
border: none;
}
}
}
.@{pro-layout-fixed-header-prefix-cls} {
z-index: 9;
width: 100%;
transition: width 0.2s;
}
.drop-down {
&.menu {
.anticon {
margin-right: 8px;
}
.ant-dropdown-menu-item {
min-width: 160px;
}
}
}
import { PageHeaderWrapper } from './components'
const PageView = {
name: 'PageView',
render () {
return (
<PageHeaderWrapper>
<router-view />
</PageHeaderWrapper>
)
}
}
export default PageView
import { defineComponent, reactive, toRefs, Ref, InjectionKey, provide, SetupContext, App, PropType } from 'vue';
import { ContentWidth } from '../typings';
export interface ProProviderData {
getPrefixCls?: (suffixCls: string, customizePrefixCls: string) => string;
i18n?: (t: string) => string;
contentWidth?: ContentWidth;
}
export const defaultProProviderProps: ProProviderData = {
getPrefixCls: (suffixCls: string, customizePrefixCls: string) => {
if (customizePrefixCls) return customizePrefixCls;
return `ant-pro-${suffixCls}`;
},
i18n: (t: string): string => t,
contentWidth: 'Fluid',
}
export const injectProConfigKey: InjectionKey<ProProviderData> = Symbol()
const ProProvider = defineComponent({
name: 'ProProvider',
props: {
prefixCls: {
type: String as PropType<string>,
default: 'ant-pro',
},
contentWidth: {
type: String as PropType<ContentWidth>,
default: 'Fluid',
},
i18n: {
type: Function as PropType<(t: string) => string>,
default: (t: string): string => t,
},
},
setup (props, { slots }: SetupContext) {
const { prefixCls, i18n, contentWidth } = toRefs(props)
const getPrefixCls = (suffixCls: string, customizePrefixCls: string): string => {
if (customizePrefixCls) return customizePrefixCls;
return suffixCls ? `${prefixCls.value}-${suffixCls}` : prefixCls.value;
}
const proConfigProvider = reactive({
i18n,
contentWidth,
getPrefixCls,
})
provide(injectProConfigKey, proConfigProvider)
return () => slots.default && slots.default()
},
})
ProProvider.install = function (app: App) {
app.component(ProProvider.name, ProProvider)
}
export default ProProvider
import './index.less';
import { defineComponent, h, resolveDynamicComponent, resolveComponent, VNode, ref, reactive, computed, Ref, watch, ComputedRef, VNodeChild, WatchStopHandle, PropType, isVNode, toRefs, defineAsyncComponent, Component } from 'vue';
// import * as Icon from '@ant-design/icons-vue';
import { createFromIconfontCN } from '@ant-design/icons-vue';
import 'ant-design-vue/es/menu/style'
import Menu from 'ant-design-vue/es/menu'
import defaultSettings, { PureSettings } from '../defaultSettings';
import { isImg, isUrl } from '../utils'
import { MenuMode, SelectInfo, OpenEventHandler } from './typings';
import { RouteProps, MenuTheme, WithFalse } from '../typings';
export { MenuMode, SelectInfo, OpenEventHandler }
export interface MenuState {
collapsed?: boolean | false;
selectedKeys?: string[];
openKeys?: string[];
}
interface MenuStated {
collapsed: boolean;
selectedKeys: string[];
openKeys: string[];
}
export interface MenuStateWatched {
state: MenuStated;
watchRef: WatchStopHandle;
}
export function useMenuState ({ collapsed = false, openKeys = [] as string[], selectedKeys = [] as string[], }: MenuState): MenuStateWatched {
const state = reactive<MenuStated>({
collapsed,
selectedKeys,
openKeys,
})
const cachedOpenKeys: Ref<string[]> = ref([] as string[])
const watchRef = watch(() => state.collapsed, (collapsed) => {
if (collapsed) {
cachedOpenKeys.value = state.openKeys.concat()
state.openKeys = []
} else {
state.openKeys = cachedOpenKeys.value.concat()
}
})
return {
state,
watchRef,
}
}
export function useRootSubmenuKeys (menus: RouteProps[]): ComputedRef<string[]> {
return computed(() => menus.map(it => it.path))
}
// ts typo
export interface BaseMenuProps extends Partial<PureSettings> {
prefixCls?: string;
collapsed?: boolean;
splitMenus?: boolean;
isMobile?: boolean;
menuData?: RouteProps[];
mode?: MenuMode;
onCollapse?: (collapsed: boolean) => void;
openKeys?: WithFalse<string[]> | undefined;
handleOpenChange?: (openKeys: string[]) => void;
theme?: MenuTheme | 'realDark';
i18n?: (t: string) => string | VNodeChild;
}
// vue props
export const VueBaseMenuProps = {
locale: Boolean,
menus: Array as PropType<RouteProps[]>,
// top-nav-header: horizontal
mode: {
type: String as PropType<MenuMode>,
default: 'inline',
},
theme: {
type: String as PropType<MenuTheme | 'realDark'>,
default: 'dark',
},
collapsed: {
type: Boolean as PropType<boolean>,
default: false,
},
openKeys: {
type: Array as PropType<string[]>,
required: true,
},
handleOpenChange: {
type: Function as PropType<(openKeys: WithFalse<string[]>) => void>,
default: () => undefined,
},
selectedKeys: {
type: Array as PropType<string[]>,
required: true,
},
}
const renderMenu = (item, i18nRender) => {
if (item && !item.hidden) {
const hasChild = item.children && !item.hideChildrenInMenu
return hasChild ? renderSubMenu(item, i18nRender) : renderMenuItem(item, i18nRender)
}
return null
}
const renderSubMenu = (item, i18nRender) => {
const renderMenuContent = (
<span>
<LazyIcon icon={item.meta.icon} />
<span>{renderTitle(item.meta.title, i18nRender)}</span>
</span>
)
return (
<Menu.SubMenu
key={item.path}
// @ts-ignore
title={renderMenuContent}
>
{!item.hideChildrenInMenu && item.children.map(cd => renderMenu(cd, i18nRender))}
</Menu.SubMenu>
)
}
const renderMenuItem = (item, i18nRender) => {
const meta = Object.assign({}, item.meta)
const target = meta.target || null
const CustomTag = target && 'a' || 'router-link'
const props = { to: { name: item.name }, href: item.path, target: target }
if (item.children && item.hideChildrenInMenu) {
// 把有子菜单的 并且 父菜单是要隐藏子菜单的
// 都给子菜单增加一个 hidden 属性
// 用来给刷新页面时, selectedKeys 做控制用
item.children.forEach(cd => {
cd.meta = Object.assign(cd.meta || {}, { hidden: true })
})
}
return (
<Menu.Item key={item.path}>
<CustomTag {...props}>
<LazyIcon icon={meta.icon} />
{renderTitle(meta.title, i18nRender)}
</CustomTag>
</Menu.Item>
)
}
let IconFont = createFromIconfontCN({
scriptUrl: defaultSettings.iconfontUrl,
});
// const LazyIcon = (props, _) => {
// const { icon } = toRefs(props)
// if (typeof icon.value === 'string' && icon.value !== '') {
// if (isUrl(icon.value) || isImg(icon.value)) {
// return <img src={icon.value} alt="icon" class="ant-pro-sider-menu-icon" />
// }
// if (icon.value.startsWith('icon-')) {
// return <IconFont type={icon.value} />
// }
// }
// if (isVNode(icon.value)) {
// return icon.value
// }
// const IconComponent = resolveComponent(`${icon.value}`)
// return h(IconComponent)
// }
const LazyIcon = defineComponent({
props: {
icon: {
type: [String, Function, Object] as PropType<string|Function|VNodeChild|JSX.Element>,
}
},
setup(props, _) {
const { icon } = toRefs(props)
if (typeof icon.value === 'string' && icon.value !== '') {
if (isUrl(icon.value) || isImg(icon.value)) {
return <img src={icon.value} alt="icon" class="ant-pro-sider-menu-icon" />
}
if (icon.value.startsWith('icon-')) {
return <IconFont type={icon.value} />
}
}
if (isVNode(icon.value)) {
return icon.value
}
const ALazyIcon = resolveComponent(`${icon.value}`)
// return ALazyIcon && ALazyIcon
return ALazyIcon && (() => (
// @ts-ignore
<ALazyIcon />
)) || null
}
})
const renderTitle = (title, i18nRender) => {
return <span>{ i18nRender && i18nRender(title) || title }</span>
}
export default defineComponent({
name: 'BaseMenu',
props: Object.assign({}, {
i18n: {
type: Function,
default: (t: string): string => t,
},
}, VueBaseMenuProps),
emits: ['update:openKeys', 'update:selectedKeys'],
setup (props, { emit } ) {
console.log('props.mode', props.mode)
const isInline = computed(() => props.mode === 'inline')
const handleOpenChange: OpenEventHandler = (openKeys): void => {
emit('update:openKeys', openKeys)
}
const handleSelect = ({ selectedKeys }: SelectInfo): void => {
emit('update:selectedKeys', selectedKeys)
}
return () => (
<Menu
inlineCollapsed={isInline.value && props.collapsed || undefined}
mode={props.mode}
theme={props.theme}
openKeys={props.openKeys}
selectedKeys={props.selectedKeys}
onOpenChange={handleOpenChange}
onSelect={handleSelect}
>
{props.menus && props.menus.map(menu => {
if (menu.hidden) {
return null
}
return renderMenu(menu, props.i18n)
})}
</Menu>
)
}
})
import './index.less';
import { VNodeChild, SetupContext } from 'vue';
import 'ant-design-vue/es/layout/style';
import Layout from 'ant-design-vue/es/layout';
import BaseMenu, { BaseMenuProps } from './BaseMenu';
import { WithFalse } from '../typings';
import { SiderProps } from './typings';
import { MenuUnfoldOutlined, MenuFoldOutlined } from '@ant-design/icons-vue';
const { Sider } = Layout;
export interface SiderMenuProps extends Pick<BaseMenuProps, Exclude<keyof BaseMenuProps, ['onCollapse']>> {
logo?: VNodeChild | JSX.Element;
siderWidth?: number;
menuHeaderRender?: WithFalse<
(logo: VNodeChild | JSX.Element, title: VNodeChild | JSX.Element, props?: SiderMenuProps) => VNodeChild
>;
menuFooterRender?: WithFalse<(props?: SiderMenuProps) => VNodeChild>;
menuContentRender?: WithFalse<(props: SiderMenuProps, defaultDom: VNodeChild | JSX.Element) => VNodeChild>;
menuExtraRender?: WithFalse<(props: SiderMenuProps) => VNodeChild>;
collapsedButtonRender?: WithFalse<(collapsed?: boolean) => VNodeChild>;
breakpoint?: SiderProps['breakpoint'] | false;
onMenuHeaderClick?: (e: MouseEvent) => void;
hide?: boolean;
onOpenChange?: (openKeys: WithFalse<string[]>) => void;
}
export const defaultRenderLogo = (logo: VNodeChild | JSX.Element): VNodeChild | JSX.Element => {
if (typeof logo === 'string') {
return <img src={logo} alt="logo" />;
}
if (typeof logo === 'function') {
return logo();
}
return logo;
}
export const defaultRenderLogoAndTitle = (
props: SiderMenuProps,
renderKey: string = 'menuHeaderRender',
): VNodeChild | JSX.Element => {
const {
logo = 'https://gw.alipayobjects.com/zos/antfincdn/PmY%24TNNDBI/logo.svg',
title,
layout,
} = props
const renderFunction = props[renderKey || ''];
if (renderFunction === false) {
return null;
}
const logoDom = defaultRenderLogo(logo);
const titleDom = <h1>{title}</h1>;
if (renderFunction) {
// when collapsed, no render title
return renderFunction(logoDom, props.collapsed ? null : titleDom, props);
}
if (layout === 'mix' && renderKey === 'menuHeaderRender') {
return null;
}
return (
<a>
{logoDom}
{props.collapsed ? null : titleDom}
</a>
);
}
export const defaultRenderCollapsedButton = (collapsed?: boolean) =>
collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />;
const SiderMenu = (props: SiderMenuProps, context: SetupContext) => {
const {
collapsed,
fixSiderbar,
menuFooterRender,
onCollapse,
theme,
siderWidth,
isMobile,
onMenuHeaderClick,
breakpoint = 'lg',
layout,
menuExtraRender = false,
collapsedButtonRender = defaultRenderCollapsedButton,
menuContentRender,
prefixCls,
onOpenChange,
headerHeight,
} = props;
const baseClassName = `${props.prefixCls}-sider`;
const siderClassName = {
[baseClassName]: true,
[`${baseClassName}-fixed`]: fixSiderbar,
[`${baseClassName}-layout-${layout}`]: layout && !isMobile,
[`${baseClassName}-light`]: theme === 'light',
};
const headerDom = defaultRenderLogoAndTitle(props);
const extraDom = menuExtraRender && menuExtraRender(props);
const menuDom = menuContentRender !== false && (
<BaseMenu
{...props}
mode="inline"
handleOpenChange={onOpenChange}
style={{
width: '100%',
}}
class={`${baseClassName}-menu`}
/>
);
};
@import "~ant-design-vue/es/style/themes/default.less";
@import '../BasicLayout.less';
@pro-layout-sider-menu-prefix-cls: ~'@{ant-prefix}-pro-sider';
@nav-header-height: @pro-layout-header-height;
.@{pro-layout-sider-menu-prefix-cls} {
position: relative;
background-color: @layout-sider-background;
border-right: 0;
transition: background-color 0.3s;
z-index: 9;
&.@{ant-prefix}-menu-vertical .@{ant-prefix}-menu-item:not(:last-child),
&.@{ant-prefix}-menu-vertical-left .@{ant-prefix}-menu-item:not(:last-child),
&.@{ant-prefix}-menu-vertical-right .@{ant-prefix}-menu-item:not(:last-child),
&.@{ant-prefix}-menu-inline .@{ant-prefix}-menu-item:not(:last-child) {
margin-bottom: 4px;
}
&.@{ant-prefix}-layout-sider-light {
.@{ant-prefix}-menu-item a {
color: @heading-color;
}
.@{ant-prefix}-menu-item-selected a,
.@{ant-prefix}-menu-item a:hover {
color: @primary-color;
}
}
.@{ant-prefix}-menu-inline-collapsed > .@{ant-prefix}-menu-item .anticon + span,
.@{ant-prefix}-menu-inline-collapsed
> .@{ant-prefix}-menu-item-group
> .@{ant-prefix}-menu-item-group-list
> .@{ant-prefix}-menu-item
.anticon
+ span,
.@{ant-prefix}-menu-inline-collapsed
> .@{ant-prefix}-menu-item-group
> .@{ant-prefix}-menu-item-group-list
> .@{ant-prefix}-menu-submenu
> .@{ant-prefix}-menu-submenu-title
.anticon
+ span,
.@{ant-prefix}-menu-inline-collapsed
> .@{ant-prefix}-menu-submenu
> .@{ant-prefix}-menu-submenu-title
.anticon
+ span {
display: none;
}
ul.@{ant-prefix}-menu-sub {
li.@{ant-prefix}-menu-item,
li.@{ant-prefix}-menu-submenu {
.@{ant-prefix}-pro-menu-item {
padding-left: 8px;
}
}
}
&-logo {
position: relative;
display: flex;
align-items: center;
padding: 16px 16px;
line-height: 32px;
cursor: pointer;
> a {
display: flex;
align-items: center;
justify-content: center;
min-height: 32px;
}
img {
display: inline-block;
height: 32px;
vertical-align: middle;
transition: height 0.2s;
}
h1 {
display: inline-block;
height: 32px;
margin: 0 0 0 12px;
color: white;
font-weight: 600;
font-size: 18px;
line-height: 32px;
vertical-align: middle;
animation: fade-in;
animation-duration: 0.2s;
}
}
&-extra {
margin-bottom: 16px;
padding: 0 16px;
&-no-logo {
margin-top: 16px;
}
}
&-menu {
position: relative;
z-index: 10;
min-height: 100%;
box-shadow: 2px 0 6px rgba(0, 21, 41, 0.35);
}
.@{ant-prefix}-layout-sider-children {
display: flex;
flex-direction: column;
height: 100%;
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.15);
border-radius: 3px;
-webkit-box-shadow: inset 0 0 5px rgba(37, 37, 37, 0.05);
}
/* 滚动条滑块 */
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
border-radius: 3px;
-webkit-box-shadow: inset 0 0 5px rgba(255, 255, 255, 0.05);
}
}
&.@{ant-prefix}-layout-sider-collapsed {
.@{ant-prefix}-menu-inline-collapsed {
width: 48px;
}
.@{pro-layout-sider-menu-prefix-cls} {
&-logo {
padding: 16px 8px;
}
}
}
.@{ant-prefix}-menu-inline-collapsed {
& > .@{ant-prefix}-menu-item .sider-menu-item-img + span,
&
> .@{ant-prefix}-menu-item-group
> .@{ant-prefix}-menu-item-group-list
> .@{ant-prefix}-menu-item
.sider-menu-item-img
+ span,
&
> .@{ant-prefix}-menu-submenu
> .@{ant-prefix}-menu-submenu-title
.sider-menu-item-img
+ span {
display: inline-block;
max-width: 0;
opacity: 0;
}
.@{ant-prefix}-menu-item {
width: 48px;
padding: 0 16px !important;
}
// hide menu item text in collapsed
.@{ant-prefix}-pro-menu-item-title {
display: none;
}
.@{ant-prefix}-menu-submenu {
&-title {
width: 48px;
padding: 0 16px !important;
}
}
}
.@{ant-prefix}-menu-item,
.@{ant-prefix}-menu-submenu-title {
.anticon {
transition: none;
}
}
&-fixed {
position: fixed;
top: 0;
left: 0;
z-index: 99;
height: 100%;
overflow: auto;
overflow-x: hidden;
box-shadow: 2px 0 8px 0 rgba(29, 35, 41, 0.05);
> .@{ant-prefix}-menu-root {
:not(.@{pro-layout-sider-menu-prefix-cls}-link-menu) {
height: ~'calc(100vh - @{nav-header-height})';
overflow-y: auto;
}
}
}
.@{ant-prefix}-menu-inline {
.@{ant-prefix}-menu-item,
.@{ant-prefix}-menu-submenu-title {
width: 100%;
}
}
&-light {
.@{ant-prefix}-layout-sider-children {
::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.06);
border-radius: 3px;
-webkit-box-shadow: inset 0 0 5px rgba(0, 21, 41, 0.05);
}
/* 滚动条滑块 */
::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.12);
border-radius: 3px;
-webkit-box-shadow: inset 0 0 5px rgba(0, 21, 41, 0.05);
}
}
background-color: @white;
box-shadow: 2px 0 8px 0 rgba(29, 35, 41, 0.05);
.@{pro-layout-sider-menu-prefix-cls}-logo {
h1 {
color: @primary-color;
}
}
.@{ant-prefix}-menu-light {
border-right-color: transparent;
}
.@{pro-layout-sider-menu-prefix-cls}-collapsed-button {
border-top: @border-width-base @border-style-base @border-color-split;
}
}
&-icon {
width: 14px;
vertical-align: baseline;
}
&-links {
width: 100%;
ul.@{ant-prefix}-menu-root {
height: auto;
}
}
&-collapsed-button {
border-top: @border-width-base @border-style-base rgba(0, 0, 0, 0.25);
.anticon {
font-size: 16px;
}
}
.top-nav-menu li.@{ant-prefix}-menu-item {
height: 100%;
line-height: 1;
}
.drawer .drawer-content {
background: @layout-sider-background;
}
.@{ant-prefix}-menu-item .sider-menu-item-img + span,
.@{ant-prefix}-menu-submenu-title .sider-menu-item-img + span {
opacity: 1;
transition: opacity 0.3s @ease-in-out, width 0.3s @ease-in-out;
}
}
@keyframes fade-in {
0% {
display: none;
opacity: 0;
}
99% {
display: none;
opacity: 0;
}
100% {
display: block;
opacity: 1;
}
}
import { VNodeChild, CSSProperties, HTMLAttributes } from 'vue';
export type MenuMode = 'vertical' | 'vertical-left' | 'vertical-right' | 'horizontal' | 'inline';
export interface MenuInfo {
key: string;
keyPath: string[];
item: VNodeChild;
domEvent?: MouseEvent,
};
export interface SelectInfo extends MenuInfo {
selectedKeys?: string[];
};
export type OpenEventHandler = (
keys:
| string[]
| {
key: string;
item: VNodeChild;
trigger: string;
open: boolean;
},
) => void;
export type CollapseType = 'clickTrigger' | 'responsive';
export type SiderTheme = 'light' | 'dark';
export interface SiderProps extends HTMLAttributes {
prefixCls?: string;
collapsible?: boolean;
collapsed?: boolean;
defaultCollapsed?: boolean;
reverseArrow?: boolean;
onCollapse?: (collapsed: boolean, type: CollapseType) => void;
zeroWidthTriggerStyle?: CSSProperties;
trigger?: VNodeChild | JSX.Element;
width?: number | string;
collapsedWidth?: number | string;
breakpoint?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl';
theme?: SiderTheme;
onBreakpoint?: (broken: boolean) => void;
}
import PropTypes from 'ant-design-vue/es/_util/vue-types'
import 'ant-design-vue/es/layout/style'
import Layout from 'ant-design-vue/es/layout'
import ConfigProvider from 'ant-design-vue/es/config-provider'
import GridContent from './components/GridContent'
const { Content } = Layout
const WrapContentProps = {
isChildrenLayout: PropTypes.bool,
location: PropTypes.any,
contentHeight: PropTypes.number,
contentWidth: PropTypes.oneOf(['Fluid', 'Fixed']).def('Fluid'),
}
const WrapContent = {
name: 'WrapContent',
props: WrapContentProps,
render (h) {
const {
isChildrenLayout,
contentWidth
} = this.$props
return (
<Content>
<ConfigProvider
getPopupContainer={(el, dialogContext) => {
if (isChildrenLayout) {
return el.parentNode()
}
return document.body
}}
>
<div class="ant-pro-basicLayout-children-content-wrap">
<GridContent contentWidth={contentWidth}>{this.$slots.default}</GridContent>
</div>
</ConfigProvider>
</Content>
)
}
}
export default WrapContent
import PropTypes from 'ant-design-vue/es/_util/vue-types'
const ProConfigProviderProps = {
i18nRender: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]).def(false),
contentWidth: PropTypes.oneOf(['Fluid', 'Fixed']).def('Fluid'),
breadcrumbRender: PropTypes.func,
}
const ConfigProvider = {
name: 'ProConfigProvider',
props: ProConfigProviderProps,
provide () {
const _self = this
return {
locale: _self.$props.i18nRender,
contentWidth: _self.$props.contentWidth,
breadcrumbRender: _self.$props.breadcrumbRender,
}
},
render () {
const { $scopedSlots } = this
const children = this.children || $scopedSlots.default
return children()
}
}
export default ConfigProvider
/* eslint-disable */
class SideEffect {
constructor ({ propsToState, handleStateChange }) {
if (typeof propsToState !== 'function') {
throw new Error('Expected propsToState to be a function.')
}
if (typeof handleStateChange !== 'function') {
throw new Error('Expected handleStateChange to be a function.')
}
this.options = {
propsToState,
handleStateChange
}
}
}
export default SideEffect
// import SideEffect from './SideEffect'
import { setDocumentTitle } from './util'
// const sideEffect = new SideEffect({
// propsToState (propsList) {
// var innermostProps = propsList[propsList.length - 1]
// if (innermostProps) {
// return innermostProps.title
// }
// },
// handleStateChange (title, prefix) {
// console.log('title', title, prefix)
// const nextTitle = `${(title || '')} - ${prefix}`
// if (nextTitle !== document.title) {
// setDocumentTitle(nextTitle)
// }
// }
// })
const handleStateChange = (title, prefix) => {
const nextTitle = `${(title || '')} - ${prefix}`
if (nextTitle !== document.title) {
setDocumentTitle(nextTitle)
}
}
const DocumentTitle = {
name: 'DocumentTitle',
functional: true,
props: {
prefix: {
type: String,
required: false,
default: 'Ant Design Pro'
},
title: {
type: String,
required: true
}
},
// { props, data, children }
// eslint-disable-next-line
render (createElement, { props, data, children }) {
handleStateChange(props.title, props.prefix)
return children
}
}
DocumentTitle.install = function (Vue) {
// const mountedInstances = []
// let state
// function __emit (sideEffect) {
// const options = sideEffect.options
// state = options.propsToState(mountedInstances.map(function (instance) {
// return instance
// }))
// options.handleStateChange(state)
// }
// Vue.mixin({
// beforeMount () {
// const sideEffect = this.$options.sideEffect
// if (sideEffect) {
// mountedInstances.push(this)
// __emit(sideEffect)
// }
// },
// updated () {
// const sideEffect = this.$options.sideEffect
// if (sideEffect) {
// __emit(sideEffect)
// }
// },
// beforeDestroy () {
// const sideEffect = this.$options.sideEffect
// if (sideEffect) {
// const index = mountedInstances.indexOf(this)
// mountedInstances.splice(index, 1)
// __emit(sideEffect)
// }
// }
// })
Vue.component(DocumentTitle.name, DocumentTitle)
}
export default DocumentTitle
export const setDocumentTitle = function (title) {
document.title = title
const ua = navigator.userAgent
// eslint-disable-next-line
const regex = /\bMicroMessenger\/([\d\.]+)/
if (regex.test(ua) && /ip(hone|od|ad)/i.test(ua)) {
const i = document.createElement('iframe')
i.src = '/favicon.ico'
i.style.display = 'none'
i.onload = function () {
setTimeout(function () {
i.remove()
}, 9)
}
document.body.appendChild(i)
}
}
export default {
name: 'VueFragment',
functional: true,
render (h, ctx) {
return ctx.children.length > 1 ? h('div',{}, ctx.children) : ctx.children
}
}
import './index.less'
import PropTypes from 'ant-design-vue/es/_util/vue-types'
import { getComponentFromProp, hasProp } from 'ant-design-vue/lib/_util/props-util'
const GlobalFooterProps = {
links: PropTypes.array,
copyright: PropTypes.any,
}
const GlobalFooter = {
name: 'GlobalFooter',
props: GlobalFooterProps,
render() {
const copyright = getComponentFromProp(this, 'copyright')
const links = getComponentFromProp(this, 'links')
const linksType = hasProp(links)
return (
<footer class="ant-pro-global-footer">
<div class="ant-pro-global-footer-links">
{linksType && (
links.map(link => (
<a
key={link.key}
title={link.key}
target={link.blankTarget ? '_blank' : '_self'}
href={link.href}
>
{link.title}
</a>
))
) || links}
</div>
{copyright && (
<div class="ant-pro-global-footer-copyright">{copyright}</div>
)}
</footer>
)
},
}
export default GlobalFooter
import './index.less'
import debounce from 'lodash/debounce'
import PropTypes from 'ant-design-vue/es/_util/vue-types'
import { triggerEvent, inBrowser, isFun } from '../../utils/util'
import 'ant-design-vue/es/icon/style'
import Icon from 'ant-design-vue/es/icon'
import { defaultRenderLogo } from '../SiderMenu/SiderMenu'
export const GlobalHeaderProps = {
collapsed: PropTypes.bool,
handleCollapse: PropTypes.func,
isMobile: PropTypes.bool.def(false),
fixedHeader: PropTypes.bool.def(false),
logo: PropTypes.any,
menuRender: PropTypes.any,
collapsedButtonRender: PropTypes.any,
headerContentRender: PropTypes.any,
rightContentRender: PropTypes.any,
}
const defaultRenderCollapsedButton = (h, collapsed) => (
<Icon type={collapsed ? 'menu-unfold' : 'menu-fold'}/>
)
const GlobalHeader = {
name: 'GlobalHeader',
props: GlobalHeaderProps,
render (h) {
const { isMobile, logo, rightContentRender, headerContentRender } = this.$props
const toggle = () => {
const { collapsed, handleCollapse } = this.$props
if (handleCollapse) handleCollapse(!collapsed)
this.triggerResizeEvent()
}
const renderCollapsedButton = () => {
const {
collapsed,
collapsedButtonRender = defaultRenderCollapsedButton,
menuRender
} = this.$props
if (collapsedButtonRender !== false && menuRender !== false) {
return (
<span class="ant-pro-global-header-trigger" onClick={toggle}>
{isFun(collapsedButtonRender) && collapsedButtonRender(h, collapsed) || collapsedButtonRender}
</span>
)
}
return null
}
const headerCls = 'ant-pro-global-header'
return (
<div class={headerCls}>
{isMobile && (
<a class={`${headerCls}-logo`} key="logo" href={'/'}>
{defaultRenderLogo(h, logo)}
</a>
)}
{renderCollapsedButton()}
{headerContentRender && (
<div class={`${headerCls}-content`}>
{isFun(headerContentRender) && headerContentRender(h, this.$props) || headerContentRender}
</div>
)}
{isFun(rightContentRender) && rightContentRender(h, this.$props) || rightContentRender}
</div>
)
},
methods: {
triggerResizeEvent: debounce(() => {
inBrowser && triggerEvent(window, 'resize')
})
},
beforeDestroy () {
this.triggerResizeEvent.cancel && this.triggerResizeEvent.cancel()
}
}
export default GlobalHeader
@import "~ant-design-vue/es/style/themes/default";
@global-header-prefix-cls: ~'@{ant-prefix}-pro-global-header';
@pro-header-bg: @component-background;
@pro-header-hover-bg: @component-background;
@pro-header-box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
@pro-header-hover-trigger-action-bg: rgba(0,0,0,.025);
.@{global-header-prefix-cls} {
position: relative;
height: @layout-header-height;
padding: 0;
background: @pro-header-bg;
box-shadow: @pro-header-box-shadow;
&-index-right {
float: right;
height: 100%;
margin-left: auto;
overflow: hidden;
.@{global-header-prefix-cls}-index-action {
display: inline-block;
height: 100%;
padding: 0 12px;
cursor: pointer;
transition: all .3s;
&:hover {
background: @pro-header-hover-trigger-action-bg;
}
}
}
&-logo {
display: inline-block;
height: @layout-header-height;
padding: 0 0 0 24px;
font-size: 20px;
line-height: @layout-header-height;
vertical-align: top;
cursor: pointer;
img, svg {
display: inline-block;
width: 32px;
height: 32px;
vertical-align: middle;
}
}
&-menu {
.anticon {
margin-right: 8px;
}
.ant-dropdown-menu-item {
min-width: 160px;
}
}
&-trigger {
height: @layout-header-height;
line-height: @layout-header-height;
vertical-align: top;
padding: 0 22px;
display: inline-block;
cursor: pointer;
transition: all 0.3s, padding 0s;
.anticon {
font-size: 20px;
vertical-align: -0.225em;
}
&:hover {
background: @pro-header-hover-trigger-action-bg;
}
}
&-content {
height: @layout-header-height;
line-height: @layout-header-height;
vertical-align: top;
display: inline-block;
}
.dark {
height: @layout-header-height;
.action {
color: rgba(255, 255, 255, 0.85);
> i {
color: rgba(255, 255, 255, 0.85);
}
&:hover,
&.opened {
background: @primary-color;
}
.ant-badge {
color: rgba(255, 255, 255, 0.85);
}
}
}
.ant-pro-global-header-index-action {
i {
color: rgba(0,0,0,.65);
vertical-align: middle;
}
}
}
import './index.less'
import PropTypes from 'ant-design-vue/es/_util/vue-types'
import { layoutContentWidth } from '../../utils/util'
const GridContent = {
name: 'GridContent',
functional: true,
props: {
children: PropTypes.any,
contentWidth: PropTypes.oneOf(['Fluid', 'Fixed']).def('Fluid'),
},
render (h, content) {
const { contentWidth } = content.props
const children = content.children
const propsContentWidth = layoutContentWidth(contentWidth)
const classNames = {
['ant-pro-grid-content']: true,
['wide']: propsContentWidth
}
return <div class={classNames}>{children}</div>
}
}
export default GridContent
@import "~ant-design-vue/es/style/themes/default";
@grid-content-prefix-cls: ~'@{ant-prefix}-pro-grid-content';
.@{grid-content-prefix-cls} {
width: 100%;
min-height: 100%;
transition: 0.3s;
&.wide {
max-width: 1200px;
margin: 0 auto;
}
}
import './index.less'
import PropTypes from 'ant-design-vue/es/_util/vue-types'
import { isArray } from 'ant-design-vue/lib/_util/vue-types/utils'
import GridContent from '../GridContent'
import 'ant-design-vue/es/page-header/style'
import PageHeader, { PageHeaderProps } from 'ant-design-vue/es/page-header'
import 'ant-design-vue/es/tabs/style'
import Tabs from 'ant-design-vue/es/tabs'
import { getComponentFromProp } from 'ant-design-vue/lib/_util/props-util'
const prefixedClassName = 'ant-pro-page-header-wrap'
const PageHeaderTabConfig = {
tabList: PropTypes.array,
tabActiveKey: PropTypes.string,
tabProps: PropTypes.object,
tabChange: PropTypes.func,
}
const PageHeaderWrapperProps = {
...PageHeaderTabConfig,
...PageHeaderProps,
title: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
content: PropTypes.any,
extraContent: PropTypes.any,
pageHeaderRender: PropTypes.func,
breadcrumb: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]).def(true),
back: PropTypes.func,
// only use `pro-layout` in children
i18nRender: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]).def(false),
}
const defaultI18nRender = (t) => t
const useContext = (route) => {
return route && {
...route.meta,
} || null
}
const noop = () => {
}
// TODO :: tabList tab 支持图标 优化
const renderFooter = (h, tabConfigProps, i18nRender) => {
const {
tabList,
tabActiveKey,
tabChange: onTabChange,
tabBarExtraContent,
tabProps,
} = tabConfigProps
return tabList && tabList.length > 0 && (
<Tabs
class={`${prefixedClassName}-tabs`}
activeKey={tabActiveKey}
onChange={key => {
if (onTabChange) {
onTabChange(key)
}
}}
tabBarExtraContent={tabBarExtraContent}
{...tabProps}
>
{tabList.map(item => (
<Tabs.TabPane {...item} tab={i18nRender(item.tab)} key={item.key}/>
))}
</Tabs>
)
}
const renderPageHeader = (h, content, extraContent) => {
if (!content && !extraContent) {
return null
}
return (
<div class={`${prefixedClassName}-detail`}>
<div class={`${prefixedClassName}-main`}>
<div class={`${prefixedClassName}-row`}>
{content && (
<div class={`${prefixedClassName}-content`}>{content}</div>
)}
{extraContent && (
<div class={`${prefixedClassName}-extraContent`}>
{extraContent}
</div>
)}
</div>
</div>
</div>
)
}
const defaultPageHeaderRender = (h, props, pageMeta, i18nRender) => {
const {
title: propTitle,
tags,
content,
pageHeaderRender,
extra,
extraContent,
breadcrumb,
back: handleBack,
...restProps
} = props
if (pageHeaderRender) {
return pageHeaderRender({ ...props })
}
let pageHeaderTitle = propTitle
if (!propTitle && propTitle !== false) {
pageHeaderTitle = pageMeta.title
}
// title props 不是 false 且不是 array 则直接渲染 title
// 反之认为是 VNode, 作为 render 参数直接传入到 PageHeader
const title = isArray(pageHeaderTitle)
? pageHeaderTitle
: pageHeaderTitle && i18nRender(pageHeaderTitle)
let tabProps = {
breadcrumb,
extra,
tags,
title,
footer: renderFooter(h, restProps, i18nRender),
}
if (!handleBack) {
tabProps.backIcon = false
}
return (
<PageHeader {...{ props: tabProps }} onBack={handleBack || noop}>
{renderPageHeader(h, content, extraContent)}
</PageHeader>
)
// return
}
const PageHeaderWrapper = {
name: 'PageHeaderWrapper',
props: PageHeaderWrapperProps,
inject: ['locale', 'contentWidth', 'breadcrumbRender'],
render(h) {
const { $route, $listeners } = this
const children = this.$slots.default
const title = getComponentFromProp(this, 'title')
const tags = getComponentFromProp(this, 'tags')
const content = getComponentFromProp(this, 'content')
const extra = getComponentFromProp(this, 'extra')
const extraContent = getComponentFromProp(this, 'extraContent')
const pageMeta = useContext(this.$props.route || $route)
const i18n = this.$props.i18nRender || this.locale || defaultI18nRender
const contentWidth = this.$props.contentWidth || this.contentWidth || false
// 当未设置 back props 或未监听 @back,不显示 back
// props 的 back 事件优先级高于 @back,需要注意
const onBack = this.$props.back || $listeners.back
const back = onBack && (() => {
// this.$emit('back')
// call props back func
onBack && onBack()
}) || undefined
const onTabChange = this.$props.tabChange
const tabChange = (key) => {
this.$emit('tabChange', key)
onTabChange && onTabChange(key)
}
let breadcrumb = {}
const propsBreadcrumb = this.$props.breadcrumb
if (propsBreadcrumb === true) {
const routes = $route.matched.concat().map(route => {
return {
path: route.path,
breadcrumbName: i18n(route.meta.title),
}
})
const defaultItemRender = ({ route, params, routes, paths, h }) => {
return routes.indexOf(route) === routes.length - 1 && (
<span>{route.breadcrumbName}</span>
) || (
<router-link to={{ path: route.path || '/', params }}>{route.breadcrumbName}</router-link>
)
}
// If custom breadcrumb render undefined
// use default breadcrumb..
const itemRender = this.breadcrumbRender || defaultItemRender
breadcrumb = { props: { routes, itemRender } }
} else {
breadcrumb = propsBreadcrumb || null
}
const props = {
...this.$props,
title,
tags,
content,
extra,
extraContent,
breadcrumb,
tabChange,
back,
}
return (
<div class="ant-pro-page-header-wrap">
<div class={`${prefixedClassName}-page-header-warp`}>
<GridContent>{defaultPageHeaderRender(h, props, pageMeta, i18n)}</GridContent>
</div>
{children ? (
<GridContent contentWidth={contentWidth}>
<div class={`${prefixedClassName}-children-content`}>
{children}
</div>
</GridContent>
) : null}
</div>
)
},
}
PageHeaderWrapper.install = function (Vue) {
Vue.component(PageHeaderWrapper.name, PageHeaderWrapper)
Vue.component('page-container', PageHeaderWrapper)
}
export default PageHeaderWrapper
@import "~ant-design-vue/es/style/themes/default";
@ant-pro-page-header-wrap: ~'@{ant-prefix}-pro-page-header-wrap';
.@{ant-pro-page-header-wrap}-children-content {
margin: 24px 24px 0;
}
.@{ant-pro-page-header-wrap}-page-header-warp {
background-color: @component-background;
}
.@{ant-pro-page-header-wrap}-main {
.@{ant-pro-page-header-wrap}-detail {
display: flex;
}
.@{ant-pro-page-header-wrap}-row {
display: flex;
width: 100%;
}
.@{ant-pro-page-header-wrap}-title-content {
margin-bottom: 16px;
}
.@{ant-pro-page-header-wrap}-title,
.@{ant-pro-page-header-wrap}-content {
flex: auto;
}
.@{ant-pro-page-header-wrap}-extraContent,
.@{ant-pro-page-header-wrap}-main {
flex: 0 1 auto;
}
.@{ant-pro-page-header-wrap}-main {
width: 100%;
}
.@{ant-pro-page-header-wrap}-title {
margin-bottom: 16px;
}
.@{ant-pro-page-header-wrap}-logo {
margin-bottom: 16px;
}
.@{ant-pro-page-header-wrap}-extraContent {
min-width: 242px;
margin-left: 88px;
text-align: right;
}
}
@media screen and (max-width: @screen-xl) {
.@{ant-pro-page-header-wrap}-main {
.@{ant-pro-page-header-wrap}-extraContent {
margin-left: 44px;
}
}
}
@media screen and (max-width: @screen-lg) {
.@{ant-pro-page-header-wrap}-main {
.@{ant-pro-page-header-wrap}-extraContent {
margin-left: 20px;
}
}
}
@media screen and (max-width: @screen-md) {
.@{ant-pro-page-header-wrap}-main {
.@{ant-pro-page-header-wrap}-row {
display: block;
}
.@{ant-pro-page-header-wrap}-action,
.@{ant-pro-page-header-wrap}-extraContent {
margin-left: 0;
text-align: left;
}
}
}
@media screen and (max-width: @screen-sm) {
.@{ant-pro-page-header-wrap}-detail {
display: block;
}
.@{ant-pro-page-header-wrap}-extraContent {
margin-left: 0;
}
}
import PropTypes from 'ant-design-vue/es/_util/vue-types'
import 'ant-design-vue/es/menu/style'
import Menu from 'ant-design-vue/es/menu'
import 'ant-design-vue/es/icon/style'
import Icon from 'ant-design-vue/es/icon'
const {
Item: MenuItem,
SubMenu
} = Menu
export const RouteMenuProps = {
menus: PropTypes.array,
theme: PropTypes.string.def('dark'),
mode: PropTypes.string.def('inline'),
collapsed: PropTypes.bool.def(false),
i18nRender: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]).def(false),
}
const renderMenu = (h, item, i18nRender) => {
if (item && !item.hidden) {
const bool = item.children && !item.hideChildrenInMenu
return bool ? renderSubMenu(h, item, i18nRender) : renderMenuItem(h, item, i18nRender)
}
return null
}
const renderSubMenu = (h, item, i18nRender) => {
return (
<SubMenu key={item.path} title={(
<span>
{renderIcon(h, item.meta.icon)}
<span>{renderTitle(h, item.meta.title, i18nRender)}</span>
</span>
)}>
{!item.hideChildrenInMenu && item.children.map(cd => renderMenu(h, cd, i18nRender))}
</SubMenu>
)
}
const renderMenuItem = (h, item, i18nRender) => {
const meta = Object.assign({}, item.meta)
const target = meta.target || null
const CustomTag = target && 'a' || 'router-link'
const props = { to: { name: item.name } }
const attrs = { href: item.path, target: target }
if (item.children && item.hideChildrenInMenu) {
// 把有子菜单的 并且 父菜单是要隐藏子菜单的
// 都给子菜单增加一个 hidden 属性
// 用来给刷新页面时, selectedKeys 做控制用
item.children.forEach(cd => {
cd.meta = Object.assign(cd.meta || {}, { hidden: true })
})
}
return (
<MenuItem key={item.path}>
<CustomTag {...{ props, attrs }}>
{renderIcon(h, meta.icon)}
{renderTitle(h, meta.title, i18nRender)}
</CustomTag>
</MenuItem>
)
}
const renderIcon = (h, icon) => {
if (icon === undefined || icon === 'none' || icon === null) {
return null
}
const props = {}
typeof (icon) === 'object' ? (props.component = icon) : (props.type = icon)
return <Icon {...{ props }} />
}
const renderTitle = (h, title, i18nRender) => {
return <span>{ i18nRender && i18nRender(title) || title }</span>
}
const RouteMenu = {
name: 'RouteMenu',
props: RouteMenuProps,
data () {
return {
openKeys: [],
selectedKeys: [],
cachedOpenKeys: []
}
},
render (h) {
const { mode, theme, menus, i18nRender } = this
const handleOpenChange = (openKeys) => {
// 在水平模式下时,不再执行后续
if (mode === 'horizontal') {
this.openKeys = openKeys
return
}
const latestOpenKey = openKeys.find(key => !this.openKeys.includes(key))
if (!this.rootSubmenuKeys.includes(latestOpenKey)) {
this.openKeys = openKeys
} else {
this.openKeys = latestOpenKey ? [latestOpenKey] : []
}
}
const dynamicProps = {
props: {
mode,
theme,
openKeys: this.openKeys,
selectedKeys: this.selectedKeys
},
on: {
select: menu => {
this.selectedKeys = menu.selectedKeys
this.$emit('select', menu)
},
openChange: handleOpenChange
}
}
const menuItems = menus.map(item => {
if (item.hidden) {
return null
}
return renderMenu(h, item, i18nRender)
})
return <Menu {...dynamicProps}>{menuItems}</Menu>
},
methods: {
updateMenu () {
const routes = this.$route.matched.concat()
const { hidden } = this.$route.meta
if (routes.length >= 3 && hidden) {
routes.pop()
this.selectedKeys = [routes[routes.length - 1].path]
} else {
this.selectedKeys = [routes.pop().path]
}
const openKeys = []
if (this.mode === 'inline') {
routes.forEach(item => {
item.path && openKeys.push(item.path)
})
}
this.collapsed ? (this.cachedOpenKeys = openKeys) : (this.openKeys = openKeys)
}
},
computed: {
rootSubmenuKeys: vm => {
const keys = []
vm.menus.forEach(item => keys.push(item.path))
return keys
}
},
created () {
this.$watch('$route', () => {
this.updateMenu()
})
this.$watch('collapsed', val => {
if (val) {
this.cachedOpenKeys = this.openKeys.concat()
this.openKeys = []
} else {
this.openKeys = this.cachedOpenKeys
}
})
},
mounted () {
this.updateMenu()
}
}
export default RouteMenu
import BaseMenu from './BaseMenu'
export default BaseMenu
import PropTypes from 'ant-design-vue/es/_util/vue-types'
import 'ant-design-vue/es/tooltip/style'
import Tooltip from 'ant-design-vue/es/tooltip'
import 'ant-design-vue/es/icon/style'
import Icon from 'ant-design-vue/es/icon'
const BlockCheckboxProps = {
value: PropTypes.string,
// Item: { key, url, title }
list: PropTypes.array,
i18nRender: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]).def(false),
}
const baseClassName = 'ant-pro-setting-drawer-block-checbox'
const BlockCheckbox = {
props: BlockCheckboxProps,
inject: ['locale'],
render (h) {
const { value, list } = this
const i18n = this.$props.i18nRender || this.locale
const items = list || [
{
key: 'sidemenu',
url:
'https://gw.alipayobjects.com/zos/antfincdn/XwFOFbLkSM/LCkqqYNmvBEbokSDscrm.svg',
title: i18n('app.setting.sidemenu'),
},
{
key: 'topmenu',
url:
'https://gw.alipayobjects.com/zos/antfincdn/URETY8%24STp/KDNDBbriJhLwuqMoxcAr.svg',
title: i18n('app.setting.topmenu'),
},
]
const handleChange = (key) => {
this.$emit('change', key)
}
const disableStyle = {
cursor: 'not-allowed'
}
return (
<div class={baseClassName} key={value}>
{items.map(item => (
<Tooltip title={item.title} key={item.key}>
<div class={`${baseClassName}-item`} style={ item.disable && disableStyle } onClick={() => !item.disable && handleChange(item.key)}>
<img src={item.url} alt={item.key} />
<div
class={`${baseClassName}-selectIcon`}
style={{
display: value === item.key ? 'block' : 'none'
}}
>
<Icon type="check" />
</div>
</div>
</Tooltip>
))}
</div>
)
}
}
export default BlockCheckbox
import PropTypes from 'ant-design-vue/es/_util/vue-types'
import 'ant-design-vue/es/tooltip/style'
import Tooltip from 'ant-design-vue/es/tooltip'
import 'ant-design-vue/es/list/style'
import List from 'ant-design-vue/es/list'
import 'ant-design-vue/es/select/style'
import Select from 'ant-design-vue/es/select'
import 'ant-design-vue/es/switch/style'
import Switch from 'ant-design-vue/es/switch'
export const renderLayoutSettingItem = (h, item) => {
const action = {...item.action}
return (
<Tooltip title={item.disabled ? item.disabledReason : ''} placement="left">
<List.Item actions={[action]}>
<span style={{ opacity: item.disabled ? 0.5 : 1 }}>{item.title}</span>
</List.Item>
</Tooltip>
)
}
export const LayoutSettingProps = {
contentWidth: PropTypes.oneOf(['Fluid', 'Fixed']).def('Fluid'),
fixedHeader: PropTypes.bool,
fixSiderbar: PropTypes.bool,
layout: PropTypes.oneOf(['sidemenu', 'topmenu']),
i18nRender: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]).def(false),
}
export default {
props: LayoutSettingProps,
inject: ['locale'],
render (h) {
const i18n = this.$props.i18nRender || this.locale
const { contentWidth, fixedHeader, layout, fixSiderbar } = this
const handleChange = (type, value) => {
this.$emit('change', { type, value })
}
return (
<List
split={false}
dataSource={[
{
title: i18n('app.setting.content-width'),
action: (
<Select
value={contentWidth}
size="small"
onSelect={(value) => handleChange('contentWidth', value)}
style={{ width: '80px' }}
>
{layout === 'sidemenu' ? null : (
<Select.Option value="Fixed">
{i18n('app.setting.content-width.fixed')}
</Select.Option>
)}
<Select.Option value="Fluid">
{i18n('app.setting.content-width.fluid')}
</Select.Option>
</Select>
),
},
{
title: i18n('app.setting.fixedheader'),
action: (
<Switch
size="small"
checked={!!fixedHeader}
onChange={(checked) => handleChange('fixedHeader', checked)}
/>
),
},
{
title: i18n('app.setting.fixedsidebar'),
disabled: layout === 'topmenu',
disabledReason: i18n('app.setting.fixedsidebar.hint'),
action: (
<Switch
size="small"
disabled={layout === 'topmenu'}
checked={!!fixSiderbar}
onChange={(checked) => handleChange('fixSiderbar', checked)}
/>
),
},
]}
renderItem={(item, index) => renderLayoutSettingItem(h, item)}
/>
)
}
}
import './ThemeColor.less'
import PropTypes from 'ant-design-vue/es/_util/vue-types'
import { genThemeToString } from '../../utils/util'
import 'ant-design-vue/es/tooltip/style'
import Tooltip from 'ant-design-vue/es/tooltip'
import 'ant-design-vue/es/icon/style'
import Icon from 'ant-design-vue/es/icon'
const baseClassName = 'theme-color'
export const TagProps = {
color: PropTypes.string,
check: PropTypes.bool
}
const Tag = {
props: TagProps,
functional: true,
render (h, content) {
const { props: { color, check }, data, ...rest } = content
return (
<div {...data} style={{ backgroundColor: color }}>
{ check ? <Icon type="check" /> : null }
</div>
)
}
}
export const ThemeColorProps = {
colors: PropTypes.array,
title: PropTypes.string,
value: PropTypes.string,
i18nRender: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]).def(false),
}
const ThemeColor = {
props: ThemeColorProps,
inject: ['locale'],
render (h) {
const { title, value, colors = [] } = this
const i18n = this.$props.i18nRender || this.locale
const handleChange = (key) => {
this.$emit('change', key)
}
return (
<div class={baseClassName} ref={'ref'}>
<h3 class={`${baseClassName}-title`}>{title}</h3>
<div class={`${baseClassName}-content`}>
{colors.map(item => {
const themeKey = genThemeToString(item.key)
const check = value === item.key || genThemeToString(value) === item.key
return (
<Tooltip
key={item.color}
title={themeKey ? i18n(`app.setting.themecolor.${themeKey}`) : item.key}
>
<Tag
class={`${baseClassName}-block`}
color={item.color}
check={check}
onClick={() => handleChange(item.key)}
/>
</Tooltip>
)
})}
</div>
</div>
)
}
}
export default ThemeColor
@import './index.less';
.@{ant-pro-setting-drawer}-content {
.theme-color {
margin-top: 24px;
overflow: hidden;
.theme-color-title {
margin-bottom: 12px;
font-size: 14px;
line-height: 22px;
}
.theme-color-block {
float: left;
width: 20px;
height: 20px;
margin-right: 8px;
color: #fff;
font-weight: bold;
text-align: center;
border-radius: 2px;
cursor: pointer;
}
}
}
import './index.less'
import omit from 'omit.js'
import PropTypes from 'ant-design-vue/es/_util/vue-types'
import 'ant-design-vue/es/divider/style'
import Divider from 'ant-design-vue/es/divider'
import 'ant-design-vue/es/drawer/style'
import Drawer from 'ant-design-vue/es/drawer'
import 'ant-design-vue/es/list/style'
import List from 'ant-design-vue/es/list'
import 'ant-design-vue/es/switch/style'
import Switch from 'ant-design-vue/es/switch'
import 'ant-design-vue/es/button/style'
import Button from 'ant-design-vue/es/button'
import 'ant-design-vue/es/icon/style'
import Icon from 'ant-design-vue/es/icon'
import 'ant-design-vue/es/alert/style'
import Alert from 'ant-design-vue/es/alert'
import antPortal from 'ant-design-vue/es/_util/portalDirective'
import 'ant-design-vue/es/message/style'
import message from 'ant-design-vue/es/message'
import BlockCheckbox from './BlockCheckbox'
import ThemeColor from './ThemeColor'
import LayoutSetting, { renderLayoutSettingItem } from './LayoutChange'
import { updateTheme, updateColorWeak } from '../../utils/dynamicTheme'
import { contentWidthCheck, genStringToTheme } from '../../utils/util'
import CopyToClipboard from 'vue-copy-to-clipboard'
const baseClassName = 'ant-pro-setting-drawer'
const BodyProps = {
title: PropTypes.string.def('')
}
const Body = {
props: BodyProps,
render (h) {
const { title } = this
return (
<div style={{ marginBottom: 24 }}>
<h3 class={`${baseClassName}-title`}>{title}</h3>
{this.$slots.default}
</div>
)
}
}
const defaultI18nRender = (t) => t
const getThemeList = (i18nRender) => {
const list = window.umi_plugin_ant_themeVar || []
const themeList = [
{
key: 'light',
url: 'https://gw.alipayobjects.com/zos/antfincdn/NQ%24zoisaD2/jpRkZQMyYRryryPNtyIC.svg',
title: i18nRender('app.setting.pagestyle.light')
},
{
key: 'dark',
url: 'https://gw.alipayobjects.com/zos/antfincdn/XwFOFbLkSM/LCkqqYNmvBEbokSDscrm.svg',
title: i18nRender('app.setting.pagestyle.dark')
}
]
const darkColorList = [
{
key: '#1890ff',
color: '#1890ff',
theme: 'dark',
}
]
const lightColorList = [
{
key: '#1890ff',
color: '#1890ff',
theme: 'dark',
}
]
if (list.find((item) => item.theme === 'dark')) {
themeList.push({
// disable click
disable: true,
key: 'realDark',
url: 'https://gw.alipayobjects.com/zos/antfincdn/hmKaLQvmY2/LCkqqYNmvBEbokSDscrm.svg',
title: i18nRender('app.setting.pagestyle.realdark'),
})
}
// insert theme color List
list.forEach(item => {
const color = (item.modifyVars || {})['@primary-color']
if (item.theme === 'dark' && color) {
darkColorList.push({
color,
...item,
})
}
if (!item.theme || item.theme === 'light') {
lightColorList.push({
color,
...item,
})
}
})
return {
colorList: {
dark: darkColorList,
light: lightColorList,
},
themeList,
}
}
const handleChangeSetting = (key, value, hideMessageLoading) => {
if (key === 'primaryColor') {
// 更新主色调
updateTheme(value)
}
if (key === 'colorWeak') {
updateColorWeak(value)
}
}
const genCopySettingJson = (settings) =>
JSON.stringify(
omit(
{
...settings,
primaryColor: genStringToTheme(settings.primaryColor),
},
['colorWeak'],
),
null,
2,
)
export const settings = {
theme: PropTypes.oneOf(['dark', 'light', 'realDark']),
primaryColor: PropTypes.string,
layout: PropTypes.oneOf(['sidemenu', 'topmenu']),
colorWeak: PropTypes.bool,
contentWidth: PropTypes.oneOf(['Fluid', 'Fixed']).def('Fluid'),
// 替换兼容 PropTypes.oneOf(['Fluid', 'Fixed']).def('Fluid')
// contentWidth: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]).def('Fluid'),
fixedHeader: PropTypes.bool,
fixSiderbar: PropTypes.bool,
hideHintAlert: PropTypes.bool.def(false),
hideCopyButton: PropTypes.bool.def(false)
}
export const SettingDrawerProps = {
getContainer: PropTypes.func,
settings: PropTypes.objectOf(settings),
i18nRender: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]).def(false),
}
const SettingDrawer = {
name: 'SettingDrawer',
props: SettingDrawerProps,
inject: ['locale'],
data () {
return {
show: false,
}
},
render (h) {
const {
setShow,
getContainer,
settings
} = this
const {
theme = 'dark',
primaryColor = 'daybreak',
layout = 'sidemenu',
fixedHeader = false,
fixSiderbar = false,
contentWidth,
hideHintAlert,
hideCopyButton,
colorWeak
} = settings
const i18n = this.$props.i18nRender || this.locale || defaultI18nRender
const themeList = getThemeList(i18n)
const isTopMenu = layout === 'topmenu'
const iconStyle = {
color: '#fff',
fontSize: 20
}
const changeSetting = (type, value) => {
this.$emit('change', { type, value })
handleChangeSetting(type, value, false)
}
return (
<Drawer
visible={this.show}
width={300}
onClose={() => setShow(false)}
placement="right"
getContainer={getContainer}
/*handle={
<div class="ant-pro-setting-drawer-handle" onClick={() => setShow(!this.show)}>
{this.show
? (<Icon type="close" style={iconStyle} />)
: (<Icon type="setting" style={iconStyle} />)
}
</div>
}*/
style={{
zIndex: 999
}}
>
<template slot="handle">
<div class={`${baseClassName}-handle`} onClick={() => setShow(!this.show)}>
{this.show
? (<Icon type="close" style={iconStyle}/>)
: (<Icon type="setting" style={iconStyle}/>)
}
</div>
</template>
<div class={`${baseClassName}-content`}>
<Body title={i18n('app.setting.pagestyle')}>
<BlockCheckbox i18nRender={i18n} list={themeList.themeList} value={theme} onChange={(val) => {
changeSetting('theme', val)
}} />
</Body>
<ThemeColor
i18nRender={i18n}
title={i18n('app.setting.themecolor')}
value={primaryColor}
colors={themeList.colorList[theme === 'realDark' ? 'dark' : 'light']}
onChange={(color) => {
changeSetting('primaryColor', color, null)
}}
/>
<Divider />
<Body title={i18n('app.setting.navigationmode')}>
<BlockCheckbox i18nRender={i18n} value={layout} onChange={(value) => {
changeSetting('layout', value, null)
}} />
</Body>
<LayoutSetting
i18nRender={i18n}
contentWidth={contentWidth}
fixedHeader={fixedHeader}
fixSiderbar={isTopMenu ? false : fixSiderbar}
layout={layout}
onChange={({ type, value }) => {
changeSetting(type, value)
}}
/>
<Divider />
<Body title={i18n('app.setting.othersettings')}>
<List
split={false}
renderItem={(item) => renderLayoutSettingItem(h, item)}
dataSource={[
{
title: i18n('app.setting.weakmode'),
action: (
<Switch
size="small"
checked={!!colorWeak}
onChange={(checked) => changeSetting('colorWeak', checked)}
/>
),
},
]}
/>
</Body>
{hideHintAlert && hideCopyButton ? null : <Divider />}
{hideHintAlert ? null : (
<Alert
type="warning"
message={i18n('app.setting.production.hint')}
icon={(<Icon type={'notification'} />)}
showIcon
style={{ marginBottom: '16px' }}
/>
)}
{hideCopyButton ? null : (
<CopyToClipboard
text={genCopySettingJson(settings)}
onCopy={() =>
message.success(i18n('app.setting.copyinfo'))
}
>
<Button block>
<Icon type={'copy'} />{i18n('app.setting.copy')}
</Button>
</CopyToClipboard>
)}
</div>
</Drawer>
)
},
methods: {
setShow (flag) {
this.show = flag
}
}
}
SettingDrawer.install = function (Vue) {
Vue.use(antPortal)
Vue.component(SettingDrawer.name, SettingDrawer)
}
export default SettingDrawer
@import '~ant-design-vue/es/style/themes/default.less';
@ant-pro-setting-drawer: ~'@{ant-prefix}-pro-setting-drawer';
.@{ant-pro-setting-drawer} {
&-content {
position: relative;
min-height: 100%;
.ant-list-item {
span {
flex: 1;
}
}
}
&-block-checbox {
display: flex;
&-item {
position: relative;
margin-right: 16px;
// box-shadow: 0 1px 1px 0 rgba(0, 0, 0, 0.1);
border-radius: @border-radius-base;
cursor: pointer;
img {
width: 48px;
}
}
&-selectIcon {
position: absolute;
top: 0;
right: 0;
width: 100%;
height: 100%;
padding-top: 15px;
padding-left: 24px;
color: @primary-color;
font-weight: bold;
font-size: 14px;
.action {
color: @primary-color;
}
}
}
&-color_block {
display: inline-block;
width: 38px;
height: 22px;
margin: 4px;
margin-right: 12px;
vertical-align: middle;
border-radius: 4px;
cursor: pointer;
}
&-title {
margin-bottom: 12px;
color: @heading-color;
font-size: 14px;
line-height: 22px;
}
&-handle {
position: absolute;
top: 240px;
right: 300px;
z-index: 0;
display: flex;
align-items: center;
justify-content: center;
width: 48px;
height: 48px;
font-size: 16px;
text-align: center;
background: @primary-color;
border-radius: 4px 0 0 4px;
cursor: pointer;
pointer-events: auto;
}
&-production-hint {
margin-top: 16px;
font-size: 12px;
}
}
import './index.less'
import PropTypes from 'ant-design-vue/es/_util/vue-types'
import 'ant-design-vue/es/layout/style'
import Layout from 'ant-design-vue/es/layout'
import { isFun } from '../../utils/util'
import BaseMenu from '../RouteMenu'
const { Sider } = Layout
export const SiderMenuProps = {
i18nRender: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]).def(false),
mode: PropTypes.string.def('inline'),
theme: PropTypes.string.def('dark'),
contentWidth: PropTypes.oneOf(['Fluid', 'Fixed']).def('Fluid'),
collapsible: PropTypes.bool,
collapsed: PropTypes.bool,
handleCollapse: PropTypes.func,
menus: PropTypes.array,
siderWidth: PropTypes.number.def(256),
isMobile: PropTypes.bool,
layout: PropTypes.string.def('inline'),
fixSiderbar: PropTypes.bool,
logo: PropTypes.any,
title: PropTypes.string.def(''),
// render function or vnode
menuHeaderRender: PropTypes.oneOfType([PropTypes.func, PropTypes.array, PropTypes.object, PropTypes.bool]),
menuRender: PropTypes.oneOfType([PropTypes.func, PropTypes.array, PropTypes.object, PropTypes.bool]),
}
export const defaultRenderLogo = (h, logo) => {
if (typeof logo === 'string') {
return <img src={logo} alt="logo" />
}
if (typeof logo === 'function') {
return logo()
}
return h(logo)
}
export const defaultRenderLogoAntTitle = (h, props) => {
const {
logo = 'https://gw.alipayobjects.com/zos/antfincdn/PmY%24TNNDBI/logo.svg',
title,
menuHeaderRender
} = props
if (menuHeaderRender === false) {
return null
}
const logoDom = defaultRenderLogo(h, logo)
const titleDom = <h1>{title}</h1>
if (menuHeaderRender) {
return isFun(menuHeaderRender)
&& menuHeaderRender(h, logoDom, props.collapsed ? null : titleDom, props)
|| menuHeaderRender
}
return (
<span>
{logoDom}
{titleDom}
</span>
)
}
const SiderMenu = {
name: 'SiderMenu',
model: {
prop: 'collapsed',
event: 'collapse'
},
props: SiderMenuProps,
render (h) {
const {
collapsible,
collapsed,
siderWidth,
fixSiderbar,
mode,
theme,
menus,
logo,
title,
handleCollapse,
onMenuHeaderClick = () => null,
i18nRender,
menuHeaderRender,
menuRender
} = this
const siderCls = ['ant-pro-sider-menu-sider']
if (fixSiderbar) siderCls.push('fix-sider-bar')
if (theme === 'light') siderCls.push('light')
//
// const handleCollapse = (collapsed, type) => {
// this.$emit('collapse', collapsed)
// }
const headerDom = defaultRenderLogoAntTitle(h, {
logo, title, menuHeaderRender, collapsed
})
return (<Sider
class={siderCls}
breakpoint={'lg'}
trigger={null}
width={siderWidth}
theme={theme}
collapsible={collapsible}
collapsed={collapsed}
onCollapse={handleCollapse}
>
{headerDom && (
<div
class="ant-pro-sider-menu-logo"
onClick={onMenuHeaderClick}
id="logo"
>
<router-link to={{ path: '/' }}>
{headerDom}
</router-link>
</div>
)}
{menuRender && (
isFun(menuRender)
&& menuRender(h, this.$props)
|| menuRender
) || (
<BaseMenu collapsed={collapsed} menus={menus} mode={mode} theme={theme} i18nRender={i18nRender} />
)}
</Sider>)
}
}
export default SiderMenu
import './index.less'
import 'ant-design-vue/es/drawer/style'
import Drawer from 'ant-design-vue/es/drawer'
import SiderMenu, { SiderMenuProps } from './SiderMenu'
const SiderMenuWrapper = {
name: 'SiderMenuWrapper',
model: {
prop: 'collapsed',
event: 'collapse'
},
props: SiderMenuProps,
render (h) {
const {
layout,
isMobile,
collapsed
} = this
const isTopMenu = layout === 'topmenu'
const handleCollapse = (e) => {
this.$emit('collapse', true)
}
return isMobile ? (
<Drawer
class="ant-pro-sider-menu"
visible={!collapsed}
placement="left"
maskClosable
getContainer={null}
onClose={handleCollapse}
bodyStyle={{
padding: 0,
height: '100vh'
}}
>
<SiderMenu {...{ props: {...this.$props, collapsed: isMobile ? false : collapsed} }} />
</Drawer>
) : !isTopMenu && (
<SiderMenu class="ant-pro-sider-menu" {...{ props: this.$props }} />
)
}
}
SiderMenuWrapper.install = function (Vue) {
Vue.component(SiderMenuWrapper.name, SiderMenuWrapper)
}
export {
SiderMenu,
SiderMenuProps
}
export default SiderMenuWrapper
@import "~ant-design-vue/es/style/themes/default.less";
@sider-menu-prefix-cls: ~'@{ant-prefix}-pro-sider-menu';
@nav-header-height: @layout-header-height;
.@{sider-menu-prefix-cls} {
&-logo {
position: relative;
height: 64px;
padding-left: 24px;
overflow: hidden;
transition: all .3s;
line-height: @nav-header-height;
background: @layout-sider-background;
svg, img, h1 {
display: inline-block;
}
svg, img {
height: 32px;
width: 32px;
vertical-align: middle;
}
h1 {
color: @white;
font-size: 20px;
margin: 0 0 0 12px;
font-family: Avenir, Helvetica Neue, Arial, Helvetica, sans-serif;
font-weight: 600;
vertical-align: middle;
}
}
&-sider {
position: relative;
z-index: 10;
min-height: 100vh;
box-shadow: 2px 0 6px rgba(0, 21, 41, 0.35);
&.fix-sider-bar {
position: fixed;
top: 0;
left: 0;
box-shadow: 2px 0 8px 0 rgba(29, 35, 41, 0.05);
.ant-menu-root {
height: ~'calc(100vh - @{nav-header-height})';
overflow-y: auto;
}
.ant-menu-inline {
border-right: 0;
.ant-menu-item,
.ant-menu-submenu-title {
width: 100%;
}
}
}
&.light {
background-color: white;
box-shadow: 2px 0 8px 0 rgba(29, 35, 41, 0.05);
.@{sider-menu-prefix-cls}-logo {
background: white;
box-shadow: 1px 1px 0 0 @border-color-split;
h1 {
color: @primary-color;
}
}
.ant-menu-light {
border-right-color: transparent;
}
}
}
&-icon {
width: 14px;
vertical-align: baseline;
}
.top-nav-menu li.ant-menu-item {
height: @nav-header-height;
line-height: @nav-header-height;
}
.drawer .drawer-content {
background: @layout-sider-background;
}
.ant-menu-inline-collapsed {
& > .ant-menu-item .sider-menu-item-img + span,
&
> .ant-menu-item-group
> .ant-menu-item-group-list
> .ant-menu-item
.sider-menu-item-img
+ span,
&
> .ant-menu-submenu
> .ant-menu-submenu-title
.sider-menu-item-img
+ span {
display: inline-block;
max-width: 0;
opacity: 0;
}
}
.ant-menu-item .sider-menu-item-img + span,
.ant-menu-submenu-title .sider-menu-item-img + span {
opacity: 1;
transition: opacity 0.3s @ease-in-out, width 0.3s @ease-in-out;
}
.ant-drawer-body {
padding: 0;
}
}
import RouteMenu from './RouteMenu'
import SiderMenuWrapper, { SiderMenu, SiderMenuProps } from './SiderMenu'
import PageHeaderWrapper from './PageHeaderWrapper'
import GlobalFooter from './GlobalFooter'
import VueFragment from './Fragment'
export {
RouteMenu,
SiderMenu,
SiderMenuProps,
SiderMenuWrapper,
PageHeaderWrapper,
GlobalFooter,
VueFragment
}
import { MenuTheme, ContentWidth } from './typings'
export interface RenderSetting {
headerRender?: false;
footerRender?: false;
menuRender?: false;
menuHeaderRender?: false;
}
export interface RenderSetting {
headerRender?: false;
footerRender?: false;
menuRender?: false;
menuHeaderRender?: false;
}
export interface PureSettings {
/**
* theme for nav menu
*/
navTheme: MenuTheme | 'realDark' | undefined;
/**
* nav menu position: `side` or `top`
*/
headerHeight?: number;
/**
* customize header height
*/
layout: 'side' | 'top' | 'mix';
/**
* layout of content: `Fluid` or `Fixed`, only works when layout is top
*/
contentWidth: ContentWidth;
/**
* sticky header
*/
fixedHeader: boolean;
/**
* sticky siderbar
*/
fixSiderbar: boolean;
menu: { locale?: boolean; defaultOpenAll?: boolean };
title: string;
// Your custom iconfont Symbol script Url
// eg://at.alicdn.com/t/font_1039637_btcrd5co4w.js
// 注意:如果需要图标多色,Iconfont 图标项目里要进行批量去色处理
// Usage: https://github.com/ant-design/ant-design-pro/pull/3517
iconfontUrl: string;
primaryColor: string;
colorWeak?: boolean;
splitMenus?: boolean;
}
export type ProSettings = PureSettings & RenderSetting;
const defaultSettings: ProSettings = {
navTheme: 'dark',
layout: 'side',
contentWidth: 'Fluid',
fixedHeader: false,
fixSiderbar: false,
menu: {
locale: true,
},
headerHeight: 48,
title: 'Ant Design Pro',
iconfontUrl: '',
primaryColor: '#1890ff',
};
export default defaultSettings;
import BasicLayout, { BasicLayoutProps } from './BasicLayout'
import BlockLayout from './BlockLayout'
import PageHeaderWrapper from './components/PageHeaderWrapper'
import SiderMenuWrapper from './components/SiderMenu'
import GlobalFooter from './components/GlobalFooter'
import SettingDrawer from './components/SettingDrawer'
import DocumentTitle from './components/DocumentTitle'
import { updateTheme, updateColorWeak } from './utils/dynamicTheme'
export {
GlobalFooter,
PageHeaderWrapper,
SiderMenuWrapper,
BlockLayout,
SettingDrawer,
DocumentTitle,
BasicLayoutProps,
updateTheme,
updateColorWeak
}
export default BasicLayout
// export * from './icons';
// export * from './components/twoTonePrimaryColor';
// export { default as createFromIconfontCN } from './components/IconFont';
// export { default } from './components/Icon';
// https://github.com/substack/insert-css
const containers = []; // will store container HTMLElement references
const styleElements = []; // will store {prepend: HTMLElement, append: HTMLElement}
const usage =
'insert-css: You need to provide a CSS string. Usage: insertCss(cssString[, options]).';
function createStyleElement() {
const styleElement = document.createElement('style');
styleElement.setAttribute('type', 'text/css');
return styleElement;
}
function insertCss(css: any, options: any) {
options = options || {};
if (css === undefined) {
throw new Error(usage);
}
const position = options.prepend === true ? 'prepend' : 'append';
const container =
options.container !== undefined ? options.container : document.querySelector('head');
let containerId = containers.indexOf(container);
// first time we see this container, create the necessary entries
if (containerId === -1) {
containerId = containers.push(container) - 1;
styleElements[containerId] = {};
}
// try to get the correponding container + position styleElement, create it otherwise
let styleElement;
if (
styleElements[containerId] !== undefined &&
styleElements[containerId][position] !== undefined
) {
styleElement = styleElements[containerId][position];
} else {
styleElement = styleElements[containerId][position] = createStyleElement();
if (position === 'prepend') {
container.insertBefore(styleElement, container.childNodes[0]);
} else {
container.appendChild(styleElement);
}
}
// strip potential UTF-8 BOM if css was read from a file
if (css.charCodeAt(0) === 0xfeff) {
css = css.substr(1, css.length);
}
// actually add the stylesheet
if (styleElement.styleSheet) {
styleElement.styleSheet.cssText += css;
} else {
styleElement.textContent += css;
}
return styleElement;
}
export default insertCss;
import { VNodeChild } from 'vue'
export type MenuTheme = 'dark' | 'light';
export type LayoutType = 'side' | 'top' | 'mix';
export type TargetType = '_blank' | '_self' | unknown;
export type ContentWidth = 'Fluid' | 'Fixed';
export interface MetaRecord {
icon?: string | VNodeChild | JSX.Element;
title?: string;
authority?: string | string[];
[key: string]: any;
}
export interface RouteProps {
key?: string | symbol;
path: string;
name?: string | symbol;
meta?: MetaRecord | {};
target?: TargetType;
hidden?: boolean;
children?: RouteProps[];
}
export type WithFalse<T> = T | false;
import client from 'webpack-theme-color-replacer/client'
import generate from '@ant-design/colors/lib/generate'
import { message } from 'ant-design-vue'
export const themeColor = {
getAntdSerials (color) {
// 淡化(即less的tint)
const lightens = new Array(9).fill().map((t, i) => {
return client.varyColor.lighten(color, i / 10)
})
// colorPalette 变换得到颜色值
const colorPalettes = generate(color)
const rgb = client.varyColor.toNum3(color.replace('#', '')).join(',')
return lightens.concat(colorPalettes).concat(rgb)
},
changeColor (newColor) {
const options = {
newColors: this.getAntdSerials(newColor), // new colors array, one-to-one corresponde with `matchColors`
changeUrl (cssUrl) {
return `/${cssUrl}` // while router is not `hash` mode, it needs absolute path
}
}
return client.changer.changeColor(options, Promise)
}
}
export const updateTheme = newPrimaryColor => {
const hideMessage = message.loading('正在切换主题', 0)
themeColor.changeColor(newPrimaryColor).then(r => {
hideMessage()
})
}
export const updateColorWeak = colorWeak => {
// document.body.className = colorWeak ? 'colorWeak' : '';
const app = document.body.querySelector('#app')
colorWeak ? app.classList.add('colorWeak') : app.classList.remove('colorWeak')
}
import isUrl from './isUrl';
import isImg from './isImg';
import isNil from './isNil';
import { nextTick, h } from 'vue';
import { AbstractNode, IconDefinition } from '@ant-design/icons-svg/lib/types';
import { generate as generateColor } from '@ant-design/colors';
import insertCss from '../insert-css';
export function warn(valid: boolean, message: string) {
// Support uglify
if (process.env.NODE_ENV !== 'production' && !valid && console !== undefined) {
console.error(`Warning: ${message}`);
}
}
export function warning(valid: boolean, message: string) {
warn(valid, `[@ant-design-vue/pro-layout] ${message}`);
}
export function isIconDefinition(target: any): target is IconDefinition {
return (
typeof target === 'object' &&
typeof target.name === 'string' &&
typeof target.theme === 'string' &&
(typeof target.icon === 'object' || typeof target.icon === 'function')
);
}
export function normalizeAttrs(attrs: Attrs = {}): Attrs {
return Object.keys(attrs).reduce((acc: Attrs, key) => {
const val = attrs[key];
switch (key) {
case 'class':
acc.className = val;
delete acc.class;
break;
default:
acc[key] = val;
}
return acc;
}, {});
}
export interface Attrs {
[key: string]: string;
}
export type StringKeyOf<T> = Extract<keyof T, string>;
export type EventHandlers<E> = {
[K in StringKeyOf<E>]?: E[K] extends Function ? E[K] : (payload: E[K]) => void;
};
export function generate(
node: AbstractNode,
key: string,
rootProps?: { [key: string]: any } | false,
): any {
if (!rootProps) {
return h(
node.tag,
{ key, ...node.attrs },
(node.children || []).map((child, index) => generate(child, `${key}-${node.tag}-${index}`)),
);
}
return h(
node.tag,
{
key,
...rootProps,
...node.attrs,
},
(node.children || []).map((child, index) => generate(child, `${key}-${node.tag}-${index}`)),
);
}
export function getSecondaryColor(primaryColor: string): string {
// choose the second color
return generateColor(primaryColor)[0];
}
export function normalizeTwoToneColors(
twoToneColor: string | [string, string] | undefined,
): string[] {
if (!twoToneColor) {
return [];
}
return Array.isArray(twoToneColor) ? twoToneColor : [twoToneColor];
}
// These props make sure that the SVG behaviours like general text.
// Reference: https://blog.prototypr.io/align-svg-icons-to-text-and-say-goodbye-to-font-icons-d44b3d7b26b4
export const svgBaseProps = {
width: '1em',
height: '1em',
fill: 'currentColor',
'aria-hidden': 'true',
focusable: 'false',
} as any;
export const iconStyles = `
.anticon {
display: inline-block;
color: inherit;
font-style: normal;
line-height: 0;
text-align: center;
text-transform: none;
vertical-align: -0.125em;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.anticon > * {
line-height: 1;
}
.anticon svg {
display: inline-block;
}
.anticon::before {
display: none;
}
.anticon .anticon-icon {
display: block;
}
.anticon[tabindex] {
cursor: pointer;
}
.anticon-spin::before,
.anticon-spin {
display: inline-block;
-webkit-animation: loadingCircle 1s infinite linear;
animation: loadingCircle 1s infinite linear;
}
@-webkit-keyframes loadingCircle {
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@keyframes loadingCircle {
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
`;
let cssInjectedFlag = false;
export const useInsertStyles = (styleStr: string = iconStyles) => {
nextTick(() => {
if (!cssInjectedFlag) {
insertCss(styleStr, {
prepend: true,
});
cssInjectedFlag = true;
}
});
};
export {
isUrl,
isImg,
isNil,
}
// source by https://github.com/ant-design/pro-components/blob/master/packages/utils/src/isImg/index.ts
/** 判断是否是图片链接 */
function isImg(path: string): boolean {
return /\w.(png|jpg|jpeg|svg|webp|gif|bmp)$/i.test(path);
}
export default isImg;
// source by https://github.com/ant-design/pro-components/blob/master/packages/utils/src/isNil/index.ts
const isNil = (value: any) => value === null || value === undefined;
export default isNil;
// source by https://github.com/ant-design/pro-components/blob/master/packages/utils
/* eslint no-useless-escape:0 import/prefer-default-export:0 */
const reg = /(((^https?:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+(?::\d+)?|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)$/;
const isUrl = (path: string): boolean => reg.test(path);
export default isUrl;
import request, { extend } from 'umi-request'
import { notification } from 'ant-design-vue'
const codeMessage = {
200: '服务器成功返回请求的数据。',
201: '新建或修改数据成功。',
202: '一个请求已经进入后台排队(异步任务)。',
204: '删除数据成功。',
400: '发出的请求有错误,服务器没有进行新建或修改数据的操作。',
401: '用户没有权限(令牌、用户名、密码错误)。',
403: '用户得到授权,但是访问是被禁止的。',
404: '发出的请求针对的是不存在的记录,服务器没有进行操作。',
406: '请求的格式不可得。',
410: '请求的资源被永久删除,且不会再得到的。',
422: '当创建一个对象时,发生一个验证错误。',
500: '服务器发生错误,请检查服务器。',
502: '网关错误。',
503: '服务不可用,服务器暂时过载或维护。',
504: '网关超时。'
}
const errorHandler = (error) => {
const { response = {} } = error
const errortext = codeMessage[response.status] || response.statusText
const { status, url } = response
notification.error({
message: `请求错误 ${status}: ${url}`,
description: errortext
})
}
export const BASE_URL = process.env.VUE_APP_API_URL || '/api/v1'
const customRequest = extend({
prefix: BASE_URL,
timeout: 1000,
errorHandler
})
// request 拦截器
customRequest.interceptors.request.use((url, options) => {
return (
{
url: `${url}&interceptors=yes`,
options: { ...options, interceptors: true }
}
)
})
// response 拦截器
customRequest.interceptors.response.use((response, options) => {
response.headers.append('interceptors', 'yes yo')
return response
})
export {
request,
extend
}
export default customRequest
import triggerEvent from 'ant-design-vue/es/_util/triggerEvent'
import { inBrowser } from 'ant-design-vue/es/_util/env'
const getComponentFromProp = (instance, prop) => {
const slots = instance.slots && instance.slots()
return slots[prop] || instance.props[prop]
}
const isFun = (func) => {
return typeof func === 'function'
}
// 兼容 0.3.4~0.3.8
export const contentWidthCheck = (contentWidth) => {
return Object.prototype.toString.call(contentWidth) === '[object Boolean]'
? contentWidth === true && 'Fixed' || 'Fluid'
: contentWidth
}
export const layoutContentWidth = (contentType) => {
return contentType !== 'Fluid'
}
const themeConfig = {
daybreak: 'daybreak',
'#1890ff': 'daybreak',
'#F5222D': 'dust',
'#FA541C': 'volcano',
'#FAAD14': 'sunset',
'#13C2C2': 'cyan',
'#52C41A': 'green',
'#2F54EB': 'geekblue',
'#722ED1': 'purple',
}
const invertKeyValues = (obj) =>
Object.keys(obj).reduce((acc, key) => {
acc[obj[key]] = key
return acc
}, {})
/**
* #1890ff -> daybreak
* @param val
*/
export function genThemeToString (val) {
return val && themeConfig[val] ? themeConfig[val] : val
}
/**
* daybreak-> #1890ff
* @param val
*/
export function genStringToTheme(val) {
const stringConfig = invertKeyValues(themeConfig)
return val && stringConfig[val] ? stringConfig[val] : val
}
export {
triggerEvent,
inBrowser,
getComponentFromProp,
isFun
}
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`BlockLayout should render BlockLayout 1`] = `<div><span>demo</span></div>`;
import { mount } from '@vue/test-utils'
import BlockLayout from '../src/BlockLayout'
describe('BlockLayout', () => {
it('should render BlockLayout', () => {
const wrapper = mount({
render () {
return (
<div>
<BlockLayout>
<span>demo</span>
</BlockLayout>
</div>
)
}
})
expect(wrapper).toMatchSnapshot()
})
})
{
"compilerOptions": {
"allowSyntheticDefaultImports": true,
"declaration": true,
"module": "esnext",
"target": "esnext",
"moduleResolution": "node",
"jsx": "preserve",
"esModuleInterop": true
},
"include": ["./src", "./typings/"],
"typings": "./typings/index.d.ts",
"exclude": [
"node_modules",
"build",
"scripts",
"acceptance-tests",
"webpack",
"jest",
"src/setupTests.ts",
"tslint:latest",
"tslint-config-prettier"
]
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment