Commit 32f1fd2c authored by wanli's avatar wanli

update frontend

parent 1c08ef6e
...@@ -27,6 +27,8 @@ deploy/ ...@@ -27,6 +27,8 @@ deploy/
build/ build/
venv/ venv/
node_modules
logs/*.log logs/*.log
logs/*.log.* logs/*.log.*
*.db *.db
......
...@@ -3,6 +3,10 @@ ...@@ -3,6 +3,10 @@
"name": "evm-store", "name": "evm-store",
"host": "127.0.0.1", "host": "127.0.0.1",
"port": 1000, "port": 1000,
"project": {
"inputDirectory": "",
"outputDirectory": ""
},
"jwtSecret": "6UdxRgs2hvWpTLmj027d5vt7dXXQX", "jwtSecret": "6UdxRgs2hvWpTLmj027d5vt7dXXQX",
"tablePrefix": "evm_", "tablePrefix": "evm_",
"logLevel": "DEBUG", "logLevel": "DEBUG",
......
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw*
MIT License
Copyright (c) 2019 ruyangit
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
# 工作中台基础UI
#### 项目介绍
[SeedWorkbenchUi](https://github.com/ruyangit/seed-workbench-ui) 简洁的中台UI,vuejs 开发,组件化,模块化 See: <a href="https://ruyangit.gitee.io/seed-workbench-ui">demo</a>
<p align="center">
<!-- <a><img src="https://img.shields.io/github/release/ruyangit/seed-workbench-ui.svg"/></a>
<a><img src="https://badge.fury.io/js/%40seed-workbench-ui%2Fice-scaffold.svg"/></a> -->
<a><img src="https://img.shields.io/github/last-commit/ruyangit/seed-workbench-ui.svg"/></a>
<a><img src="https://img.shields.io/badge/code_style-standard-brightgreen.svg"/></a>
<a><img src="https://img.shields.io/github/forks/ruyangit/seed-workbench-ui.svg"/></a>
<a><img src="https://img.shields.io/github/stars/ruyangit/seed-workbench-ui.svg"/></a>
</p>
#### 先来波图
![banner](https://ruyangit.gitee.io/bgcdn/analysis.jpg)
![banner](https://ruyangit.gitee.io/bgcdn/workplace.jpg)
![banner](https://ruyangit.gitee.io/bgcdn/list.jpg)
![banner](https://ruyangit.gitee.io/bgcdn/setting.jpg)
![banner](https://ruyangit.gitee.io/bgcdn/phone_login.jpg)
![banner](https://ruyangit.gitee.io/bgcdn/register.jpg)
#### 预览
[预览地址](https://ruyangit.gitee.io/seed-workbench-ui)
> 预览部署在 Github Page ,如果您访问时由于网络原因卡在载入界面或者白屏,请克隆或者下载本仓库在本地运行查看效果。
#### 软件架构
* [vue/cli3](https://cli.vuejs.org)
* vue 2.5.17
* vue-router 3.0.1
* vuex 3.0.1
* vuex-router-sync 5.0.5
* vue-i18n 8.1.0
* numeral 2.0.6
* axios 0.18.0
* [ant-design-vue 1.1.2 组件库](https://vuecomponent.github.io/ant-design-vue)
* [antv/g2 3.2.7 图表库](http://g2.alipay.com/)
#### 安装教程
```
npm install
```
### 运行包含热加载的开发环境
```
npm run serve
```
### 打包压缩后的生产文件
```
npm run build
```
### 本地如何运行打包后的生产文件
```
npm install serve -g 安装serve服务 -g 安装到全局
serve -s dist 运行打包后的生产文件 dist 打包后的文件夹
```
### Lints and fixes files
```
npm run lint
```
#### 使用说明
1. 运行文件配置 [vue.config.js](https://github.com/ruyangit/seed-workbench-ui/blob/dev/vue.config.js)
```
// 基础路径 注意发布之前要先修改这里
const baseUrl = '/'
if (process.env.NODE_ENV === 'production') {
baseUrl = '/frontend/'
}
// 主题样式全局修改替换
css: {
loaderOptions: {
less: {
modifyVars: {
'ai-prefix': 'ai',
'primary-color': '#42b983'
},
paths: [
resolve('node_modules'),
resolve('src')
],
javascriptEnabled: true
}
}
}
// 过滤掉moment其它国家,只保留中文和英文
configureWebpack: {
plugins: [
new webpack.ContextReplacementPlugin(/moment[\\/]locale$/, /^\.\/(zh-cn|en-us)$/),
]
}
// 配置本地svg优化方案 ,重新设置src别名@
chainWebpack: config => {
const svgRule = config.module.rule('svg')
svgRule.uses.clear()
svgRule
.include
.add(resolve('src/assets/svg-icons'))
.end()
.use('svg-sprite-loader')
.loader('svg-sprite-loader')
.options({
symbolId: 'ai-[name]'
})
.end()
// image exclude
const imagesRule = config.module.rule('images')
imagesRule
.test(/\.(png|jpe?g|gif|webp|svg)(\?.*)?$/)
.exclude
.add(resolve('src/assets/svg-icons'))
.end()
// 重新设置 alias
config.resolve.alias.set('@', resolve('src'))
}
```
2. 编译文件配置 [babel.config.js](https://github.com/ruyangit/seed-workbench-ui/blob/dev/babel.config.js)
```
// 设置ant-design-vue 按需加载方案
"plugins": [
["import", { "libraryName": "ant-design-vue", "libraryDirectory": "es", "style": true }]
]
```
3. 组件及页面API说明待后续时间充裕补充上来。
#### 计划
* 后续完善UI中所需的组件
* 对接完成自己开发的后台系统
[SpringbootSeed](https://gitee.com/ruyangit/springboot-seed)
* 关于阿里的g2 或者百度的 echarts 对于我来说感觉有点大,之后看看图表相关的简化一下
* 代码的规范及API文档的编写
* 代码的CI,CD测试
#### 参与贡献
1. Fork 本项目
2. 新建 Feat_xxx 分支
3. 提交代码
4. 新建 Pull Request
#### 人家都喝咖啡,我就喝瓶水就行,谢谢支持!
\ No newline at end of file
module.exports = {
"presets": [
'@vue/app'
],
"plugins": [
["import", { "libraryName": "ant-design-vue", "libraryDirectory": "es", "style": true }]
]
}
This diff is collapsed.
{
"name": "evm-workbench-admin",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"@antv/g2": "^3.2.7",
"ant-design-vue": "^1.1.2",
"axios": "^0.18.0",
"numeral": "^2.0.6",
"vue": "^2.5.17",
"vue-i18n": "^8.1.0",
"vue-router": "^3.0.1",
"vuex": "^3.0.1",
"vuex-router-sync": "^5.0.0"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^3.0.3",
"@vue/cli-plugin-eslint": "^3.0.3",
"@vue/cli-service": "^3.0.3",
"babel-plugin-import": "^1.9.1",
"less": "^3.8.1",
"less-loader": "^4.1.0",
"svg-sprite-loader": "^3.9.2",
"vue-template-compiler": "^2.5.17"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/essential",
"eslint:recommended"
],
"rules": {},
"parserOptions": {
"parser": "babel-eslint"
}
},
"postcss": {
"plugins": {
"autoprefixer": {}
}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not ie <= 8"
]
}
<!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>EVM 应用商店</title>
<style>
html,
body {
background-color: transparent;
margin: 0;
height: 100%;
}
.box {
height: 80px;
width: 80px;
position: relative;
top: calc(50% - 80px);
left: calc(50% - 50px);
perspective: 1000px;
}
.thing {
height: 40px;
width: 40px;
background-color: #E87722;
position: absolute;
box-sizing: border-box;
top: 0;
left: 0;
}
.thing:nth-of-type(1) {
animation: bounce 0.5s ease-in-out infinite alternate, move 4s -1s infinite;
}
.thing:nth-of-type(2) {
animation: bounce 0.5s ease-in-out infinite alternate, move 4s -2s infinite;
}
.thing:nth-of-type(3) {
animation: bounce 0.5s ease-in-out infinite alternate, move 4s -3s infinite;
}
.thing:nth-of-type(4) {
animation: bounce 0.5s ease-in-out infinite alternate, move 4s -4s infinite;
}
@keyframes bounce {
from {
transform: scale(1);
}
to {
transform: scale(0.8);
}
}
@keyframes move {
0% {
top: 0;
left: 0;
background-color: #E87722;
}
25% {
top: 0;
left: 50%;
background-color: #42b983;
}
50% {
top: 50%;
left: 50%;
background-color: #69B3E7;
}
75% {
top: 50%;
left: 0;
background-color: #FFC845;
}
}
</style>
</head>
<body>
<noscript>
<strong>We're sorry but frontend doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app" class="box">
<div class="thing"></div>
<div class="thing"></div>
<div class="thing"></div>
<div class="thing"></div>
</div>
<!-- built files will be auto injected -->
</body>
</html>
<template>
<ConfigProvider :locale="locale">
<div class="root"><router-view /></div>
</ConfigProvider>
</template>
<script>
import { LocaleProvider, ConfigProvider } from "ant-design-vue";
export default {
components: {
LocaleProvider,
ConfigProvider,
},
computed: {
locale() {
return this.$i18n.messages[this.$i18n.locale];
},
},
};
</script>
\ No newline at end of file
import request from '@/utils/request'
export function menuNav() {
return request({
url: 'https://randomuser.me/api',
method: 'get'
})
}
import request from '@/utils/request'
export function users(params) {
return request({
url: 'https://randomuser.me/api',
method: 'get',
params
})
}
@import 'ant-design-vue/lib/style/themes/default.less';
@import './utils.less';
.tableList {
.tableListOperator {
margin-bottom: 16px;
button {
margin-right: 8px;
}
}
}
.tableListForm {
:global {
.ant-form-item {
margin-bottom: 24px;
margin-right: 0;
display: flex;
> .ant-form-item-label {
width: auto;
line-height: 32px;
padding-right: 8px;
}
.ant-form-item-control {
line-height: 32px;
}
}
.ant-form-item-control-wrapper {
flex: 1;
}
}
.submitButtons {
display: block;
white-space: nowrap;
margin-bottom: 24px;
}
}
@media screen and (max-width: @screen-lg) {
.tableListForm :global(.ant-form-item) {
margin-right: 24px;
}
}
@media screen and (max-width: @screen-md) {
.tableListForm :global(.ant-form-item) {
margin-right: 8px;
}
}
\ No newline at end of file
.textOverflow() {
overflow: hidden;
text-overflow: ellipsis;
word-break: break-all;
white-space: nowrap;
}
.textOverflowMulti(@line: 3, @bg: #fff) {
overflow: hidden;
position: relative;
line-height: 1.5em;
max-height: @line * 1.5em;
text-align: justify;
margin-right: -1em;
padding-right: 1em;
&:before {
background: @bg;
content: '...';
padding: 0 1px;
position: absolute;
right: 14px;
bottom: 0;
}
&:after {
background: white;
content: '';
margin-top: 0.2em;
position: absolute;
right: 14px;
width: 1em;
height: 1em;
}
}
// mixins for clearfix
// ------------------------
.clearfix() {
zoom: 1;
&:before,
&:after {
content: ' ';
display: table;
}
&:after {
clear: both;
visibility: hidden;
font-size: 0;
height: 0;
}
}
<template>
<a-card :loading="loading" :bodyStyle="{padding: '20px 24px 8px 24px'}">
<div class="chartCard" >
<div :class="classNames('chartTop',{['chartTopMargin']:!$slots.default && !$slots.footer})">
<div class="avatar">{{avatar}}</div>
<div class="metaWrap">
<div class="meta">
<span class="title">{{title}}</span>
<span class="action">
<slot name="action"></slot>
</span>
</div>
<div class="total" v-if="total">{{total}}</div>
</div>
</div>
<div class="content" ref="cardRef" :style="{height: contentHeight || 'auto'}" v-if="$slots.default">
<div class="contentFixed">
<slot></slot>
</div>
</div>
<div v-if="$slots.footer" :class="classNames('footer',{['footerMargin']:!$slots.default})">
<slot name="footer"></slot>
</div>
</div>
</a-card>
</template>
<script>
import { Card } from "ant-design-vue";
import classNames from "classnames";
export default {
props: {
loading: { default: false, type: Boolean },
avatar: { type: String },
title: { type: String },
action: { type: String },
total: { type: String },
contentHeight: { type: String }
},
components: {
ACard: Card
},
methods: {
classNames
}
};
</script>
<style lang="less">
@import url("./index.less");
</style>
\ No newline at end of file
@import 'ant-design-vue/lib/style/themes/default.less';
.chartCard {
position: relative;
.chartTop {
position: relative;
overflow: hidden;
width: 100%;
}
.chartTopMargin {
margin-bottom: 12px;
}
.chartTopHasMargin {
margin-bottom: 20px;
}
.metaWrap {
float: left;
}
.avatar {
position: relative;
top: 4px;
float: left;
margin-right: 20px;
img {
border-radius: 100%;
}
}
.meta {
color: @text-color-secondary;
font-size: @font-size-base;
line-height: 22px;
height: 22px;
}
.action {
cursor: pointer;
position: absolute;
top: 0;
right: 0;
}
.total {
overflow: hidden;
text-overflow: ellipsis;
word-break: break-all;
white-space: nowrap;
color: @heading-color;
margin-top: 4px;
margin-bottom: 0;
font-size: 30px;
line-height: 38px;
height: 38px;
}
.content {
margin-bottom: 12px;
position: relative;
width: 100%;
}
.contentFixed {
position: absolute;
left: 0;
bottom: 0;
width: 100%;
}
.footer {
border-top: 1px solid @border-color-split;
padding-top: 9px;
margin-top: 8px;
& > * {
position: relative;
}
}
.footerMargin {
margin-top: 20px;
}
}
<template>
<div class="field">
<span>{{label}}</span>
<span>{{value}}</span>
</div>
</template>
<script>
export default {
props: ["label", "value"]
};
</script>
<style lang="less">
@import url("./index.less");
</style>
\ No newline at end of file
@import 'ant-design-vue/lib/style/themes/default.less';
.field {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin: 0;
span {
font-size: @font-size-base;
line-height: 22px;
}
span:last-child {
margin-left: 8px;
color: @heading-color;
}
}
<template>
<div class='miniChart'>
<div class='chartContent' >
<div :id="container" style="width:100%"></div>
</div>
</div>
</template>
<script>
import PropTypes from "ant-design-vue/es/_util/vue-types";
import moment from "moment";
import { initDefaultProps } from "ant-design-vue/es/_util/props-util";
const G2 = require("@antv/g2/lib/core");
require("@antv/g2/lib/geom/area");
const MniBarProps = {
container: PropTypes.string,
height: PropTypes.number,
forceFit: PropTypes.bool,
color: PropTypes.string,
data: PropTypes.array
};
const beginDay = new Date().getTime();
const visitData = [];
const fakeY = [7, 5, 4, 2, 4, 7, 5, 6, 5, 9, 6, 3, 1, 5, 3, 6, 5];
for (let i = 0; i < fakeY.length; i += 1) {
visitData.push({
x: moment(new Date(beginDay + 1000 * 60 * 60 * 24 * i)).format(
"YYYY-MM-DD"
),
y: fakeY[i]
});
}
export default {
data: () => ({
chart: Object
}),
props: initDefaultProps(MniBarProps, {
forceFit: true,
color: "#1890FF",
height: 46,
container: 'container_'+new Date().getTime(),
}),
methods: {
createChart() {
const scale = {
x: {
type: "cat"
},
y: {
min: 0
}
};
const padding = [36, 5, 30, 5];
const chartHeight = this.height + 54;
this.chart = new G2.Chart({
container: this.container,
forceFit: this.forceFit,
height: chartHeight,
padding: padding
});
this.chart.source(visitData);
this.chart.scale(scale);
this.chart.axis(false);
this.chart.tooltip(true, { showTitle: false, crosshairs: false });
this.chart
.area()
.position("x*y")
.color(this.color)
.tooltip("x*y", (x, y) => ({
name: x,
value: y
})).shape('smooth').style({fillOpacity: 1})
this.chart.render();
}
},
mounted() {
console.log(this);
this.createChart();
},
updated(){
if(!this.chart){
this.createChart();
}
}
};
</script>
\ No newline at end of file
<template>
<div class='miniChart' :style="{style: height+'px'}">
<div class='chartContent' >
<div style="width:100%" :id="container" ></div>
</div>
</div>
</template>
<script>
import PropTypes from "ant-design-vue/es/_util/vue-types";
import moment from "moment";
import { initDefaultProps } from "ant-design-vue/es/_util/props-util";
const G2 = require("@antv/g2/lib/core");
require("@antv/g2/lib/geom/interval");
const MniBarProps = {
container: PropTypes.string,
height: PropTypes.number,
forceFit: PropTypes.bool,
color: PropTypes.string,
data: PropTypes.array
};
const beginDay = new Date().getTime();
const visitData = [];
const fakeY = [7, 5, 4, 2, 4, 7, 5, 6, 5, 9, 6, 3, 1, 5, 3, 6, 5];
for (let i = 0; i < fakeY.length; i += 1) {
visitData.push({
x: moment(new Date(beginDay + 1000 * 60 * 60 * 24 * i)).format(
"YYYY-MM-DD"
),
y: fakeY[i]
});
}
export default {
data: () => ({
chart: Object
}),
props: initDefaultProps(MniBarProps, {
forceFit: true,
color: "#1890FF",
height: 46,
container: 'container_'+new Date().getTime(),
}),
methods: {
createChart() {
const scale = {
x: {
type: "cat"
},
y: {
min: 0
}
};
const padding = [36, 5, 30, 5];
const chartHeight = this.height + 54;
this.chart = new G2.Chart({
container: this.container,
forceFit: this.forceFit,
height: chartHeight,
padding: padding
});
this.chart.source(visitData);
this.chart.scale(scale);
this.chart.axis(false);
this.chart.tooltip(true, { showTitle: false, crosshairs: false });
this.chart
.interval()
.position("x*y")
.color(this.color)
.tooltip("x*y", (x, y) => ({
name: x,
value: y
}));
this.chart.render();
}
},
mounted() {
this.createChart();
// 注:window.onresize只能在项目内触发1次
},
computed:{
},
updated(){
console.log(1);
if(!this.chart){
this.createChart();
}
}
};
</script>
\ No newline at end of file
import './index.less'
import { Tooltip } from "ant-design-vue";
export default {
props: {
target: { type: Number },
color: {
default: 'rgb(19, 194, 194)'
},
strokeWidth: { type: Number },
percent: { type: Number },
},
components: {
ATooltip: Tooltip
},
render() {
const { target, color, percent, strokeWidth } = this
return (
<div class="miniProgress">
<a-tooltip title={`目标值 ${target}%`}>
<div class='target' style={{ left: target + 'px' ? `${target}%` : null }}>
<span style={{ backgroundColor: color || null }} />
<span style={{ backgroundColor: color || null }} />
</div>
</a-tooltip>
<div class='progressWrap'>
<div
class='progress'
style={{
backgroundColor: color || null,
width: percent ? `${percent}%` : null,
height: strokeWidth + 'px' || null,
}}
/>
</div>
</div>
)
}
}
\ No newline at end of file
@import 'ant-design-vue/lib/style/themes/default.less';
.miniProgress {
padding: 5px 0;
position: relative;
width: 100%;
.progressWrap {
background-color: @background-color-base;
position: relative;
}
.progress {
transition: all 0.4s cubic-bezier(0.08, 0.82, 0.17, 1) 0s;
border-radius: 1px 0 0 1px;
background-color: @primary-color;
width: 0;
height: 100%;
}
.target {
position: absolute;
top: 0;
bottom: 0;
span {
border-radius: 100px;
position: absolute;
top: 0;
left: 0;
height: 4px;
width: 2px;
}
span:last-child {
top: auto;
bottom: 0;
}
}
}
import './index.less'
import numeral from 'numeral';
import ChartCard from './ChartCard/ChartCard';
import Field from './Field/Field';
import MiniProgress from './MiniProgress';
// import MiniBar from './MiniBar/MiniBar';
// import MiniArea from './MiniArea/MiniArea';
const yuan = val => ${numeral(val).format('0,0')}`;
export {
yuan,
ChartCard,
Field,
MiniProgress,
// MiniBar,
// MiniArea
};
\ No newline at end of file
.miniChart {
position: relative;
width: 100%;
.chartContent {
position: absolute;
bottom: -30px;
width: 100%;
> div {
margin: 0 -5px;
overflow: hidden;
}
}
.chartLoading {
position: absolute;
top: 16px;
left: 50%;
margin-left: -7px;
}
}
\ No newline at end of file
<template>
<div class="descItem">
<p>
{{title}}
</p>
<template v-if="content">{{content}}</template>
<slot name="content" v-else></slot>
</div>
</template>
<script>
export default {
props: {
title: String,
content: String|Number|null
}
};
</script>
<style lang="less">
.descItem {
font-size: 14px;
line-height: 24px;
margin-bottom: 7px;
color: rgba(0, 0, 0, 0.65);
p {
margin-right: 8px;
display: inline-block;
color: rgba(0, 0, 0, 0.85);
}
}
</style>
\ No newline at end of file
import './index.less'
import classNames from 'classnames';
const GlobalFooter = {
props: ["className", "links", "copyright"],
render(){
const {className,links,copyright} = this;
const cls = classNames('globalFooter', className);
return (
<div class={cls}>
{links && (
<div class="links">
{links.map(link => (
<a
key={link.key}
title={link.key}
target={link.blankTarget ? '_blank' : '_self'}
href={link.href}
>
{link.title}
</a>
))}
</div>
)}
{copyright && <div class="copyright">{copyright}</div>}
</div>
)
}
}
export default GlobalFooter
\ No newline at end of file
@import 'ant-design-vue/lib/style/themes/default.less';
.globalFooter {
padding: 0 16px;
margin: 48px 0 24px 0;
text-align: center;
.links {
margin-bottom: 8px;
a {
color: @text-color-secondary;
transition: all 0.3s;
&:not(:last-child) {
margin-right: 40px;
}
&:hover {
color: @text-color;
}
}
}
.copyright {
color: @text-color-secondary;
font-size: @font-size-base;
}
}
<template>
<div :class="className">
<a-tooltip title="使用文档" placement="bottom">
<a target="_blank" href="javascript:;" class="action" @click="success">
<a-icon type="question-circle-o" />
</a>
</a-tooltip>
<a-tooltip title="Code" placement="bottom">
-->
<a
target="_blank"
href="https://github.com/ruyangit/seed-workbench-ui.git"
class="action"
>
<span class="action">
<a-icon type="codepen" theme="outlined" />
</span>
</a>
</a-tooltip>
<a-dropdown placement="bottomRight">
<span class="action">
<a-icon type="codepen" theme="outlined" />
</span>
<a-menu slot="overlay" @click="handleMenuClick">
<a-menu-item key="gitee">
<!-- <a target="_blank" href="https://github.com/ruyangit/seed-workbench-ui.git"> -->
<a-icon type="slack-square" theme="outlined" />
Gitee (开源中国)
<!-- </a> -->
</a-menu-item>
<a-menu-item key="github">
<!-- <a target="_blank" href="https://github.com/ruyangit/seed-workbench-ui.git"> -->
<a-icon type="github" />
Github (同性交友)
<!-- </a> -->
</a-menu-item>
</a-menu>
</a-dropdown>
<a-notice-icon
class="ai-notice"
className="action"
:count="8"
:loading="false"
>
<a-notice-icon-tab
:list="[
{
id: '000000001',
avatar:
'https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png',
title: '你收到了 14 份新周报',
datetime: '2017-08-09',
},
{
id: '000000002',
avatar:
'https://gw.alipayobjects.com/zos/rmsportal/OKJXDXrmkNshAMvwtvhu.png',
title: '你推荐的 曲妮妮 已通过第三轮面试',
datetime: '2017-08-08',
read: true,
},
]"
title="通知"
emptyText="你已查看所有通知"
emptyImage="https://gw.alipayobjects.com/zos/rmsportal/wAhyIChODzsoKIOBHcBk.svg"
/>
<a-notice-icon-tab
:list="[]"
title="消息"
emptyText="您已读完所有消息"
emptyImage="https://gw.alipayobjects.com/zos/rmsportal/sAuJeJzSKbUmHfBQRzmZ.svg"
/>
<a-notice-icon-tab
:list="[
{
id: '000000009',
title: '任务名称',
description: '任务需要在 2017-01-12 20:00 前启动',
extra: '未开始',
status: 'todo',
},
{
id: '000000010',
title: '第三方紧急代码变更',
description:
'冠霖提交于 2017-01-06,需在 2017-01-07 前完成代码变更任务',
extra: '马上到期',
read: true,
status: 'urgent',
},
{
id: '000000011',
title: '信息安全考试',
description: '指派竹尔于 2017-01-09 前完成更新并发布',
extra: '已耗时 8 天',
status: 'doing',
},
{
id: '000000012',
title: 'ABCD 版本发布',
description:
'冠霖提交于 2017-01-06,需在 2017-01-07 前完成代码变更任务',
extra: '进行中',
status: 'processing',
},
]"
title="待办"
emptyText="你已完成所有待办"
emptyImage="https://gw.alipayobjects.com/zos/rmsportal/HsIsxMZiWKrNUavQUXqx.svg"
/>
</a-notice-icon>
<a-tooltip :title="$t('navbar.lang')" placement="bottom">
<a href="javascript:;" class="action" @click="changeLang">
<a-icon type="api" />
</a>
</a-tooltip>
<a-dropdown>
<span class="action account">
<a-avatar
size="small"
class="avatar"
src="https://gw.alipayobjects.com/zos/rmsportal/BiazfanxmamNRoxxVxka.png"
alt="avatar"
/>
<span class="name">Ryan Ru</span>
</span>
<a-menu slot="overlay" class="menu">
<a-menu-item key="userCenter">
<a-icon type="user" />
{{ $t("menu.account.center") }}
</a-menu-item>
<a-menu-item key="userinfo">
<a-icon type="setting" />
{{ $t("menu.account.settings") }}
</a-menu-item>
<a-menu-item key="triggerError">
<a-icon type="close-circle-o" />
{{ $t("menu.account.trigger") }}
</a-menu-item>
<a-menu-divider />
<a-menu-item key="logout">
<a-icon type="logout" />
退出登录
</a-menu-item>
</a-menu>
</a-dropdown>
<a-tooltip title="预览设置" placement="bottom">
<a
@click="
() => {
this.collapse = !this.collapse;
}
"
href="javascript:;"
class="action"
:style="{ marginRight: '12px' }"
>
<a-icon type="ellipsis" />
</a>
</a-tooltip>
<a-setting-drawer :collapse="collapse" />
</div>
</template>
<script>
import {
Tooltip,
Icon,
Dropdown,
Menu,
Avatar,
Modal,
Divider,
} from "ant-design-vue";
import NoticeIcon from "@/components/NoticeIcon";
import SettingDrawer from "@/components/SettingDrawer";
export default {
data: () => ({
collapse: false,
}),
components: {
ATooltip: Tooltip,
AIcon: Icon,
ADropdown: Dropdown,
AMenu: Menu,
AMenuItem: Menu.Item,
AMenuDivider: Menu.Divider,
AAvatar: Avatar,
ANoticeIcon: NoticeIcon,
ANoticeIconTab: NoticeIcon.Tab,
ADivider: Divider,
ASettingDrawer: SettingDrawer,
},
props: ["theme", "layout"],
methods: {
changeLang() {
this.$i18n.locale = this.$i18n.locale === "en_US" ? "zh_CN" : "en_US";
},
handleMenuClick(e) {
if (e.key === "gitee") {
window.open(
"https://gitee.com/ruyangit/seed-workbench-ui.git",
"_blank"
);
} else {
window.open(
"https://github.com/ruyangit/seed-workbench-ui.git",
"_blank"
);
}
},
success() {
Modal.success({
title: "友好的一个提示",
// JSX support
content: (
<div>
<p>使用文档,组件API,等说明。正在加班整理中...</p>
<p>您可以先 Star 一个,追踪后面的更新。</p>
<p>欢迎 Pr</p>
</div>
),
});
},
},
computed: {
className() {
let className = "ai-header-right";
if (this.theme === "dark" && this.layout === "topmenu") {
className = `ai-header-right dark`;
}
return className;
},
},
};
</script>
import './index.less'
import RightContent from './RightContent'
import { Icon, Spin } from "ant-design-vue";
import { mapGetters } from "vuex";
const GlobalHeader = {
props:['theme','layout'],
computed: {
...mapGetters({
spinning: "global/getBasicLayoutSpinning",
collapsed: "global/getChangeLayoutCollapsed"
}),
},
methods: {
onCollapsed() {
this.$store.commit('global/UpdateChangeLayoutCollapsed', !this.collapsed)
},
},
render() {
const { spinning, collapsed, onCollapsed, theme,layout } = this
return (
<div class="header-index">
<Icon
class="trigger"
type={collapsed ? 'menu-unfold' : 'menu-fold'}
onClick={onCollapsed}
/>
<Spin spinning={spinning}></Spin>
<RightContent theme={theme} layout={layout}/>
</div>
)
}
}
export default GlobalHeader
\ No newline at end of file
@import 'ant-design-vue/lib/style/themes/default.less';
.trigger{
font-size: 18px;
line-height: 64px;
padding: 0 24px;
cursor: pointer;
transition: color .3s;
&:hover{
color: @primary-color;
}
}
.header-index{
height: 64px;
padding: 0 12px 0 0;
background: #fff;
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
position: relative;
}
@ai-header-prefix: ~"@{ai-prefix}-header";
.@{ai-header-prefix}-right {
float: right;
height: 100%;
.action {
cursor: pointer;
padding: 0 12px;
display: inline-block;
transition: all 0.3s;
height: 100%;
> i {
font-size: 18px;
vertical-align: middle;
color: @text-color;
}
&:hover {
background: @primary-1;
}
:global(&.ant-popover-open) {
background: @primary-1;
}
}
.search {
padding: 0 12px;
&:hover {
background: transparent;
}
}
.account {
.avatar {
margin: 20px 8px 20px 0;
color: @primary-color;
background: rgba(255, 255, 255, 0.85);
vertical-align: middle;
}
.name{
margin-top: 5px;
}
}
&.dark {
.action {
color: rgba(255, 255, 255, 0.85);
> i {
color: rgba(255, 255, 255, 0.85);
}
&:hover,
&:global(.ant-popover-open) {
background: transparent;
}
:global(.ant-badge) {
color: rgba(255, 255, 255, 0.85);
}
}
}
}
.menu {
:global(.anticon) {
margin-right: 8px;
}
:global(.ant-dropdown-menu-item) {
width: 160px;
}
}
import './NoticeList.less'
import { List, Avatar } from "ant-design-vue";
import classNames from 'classnames';
export default {
props: ['data', 'title', 'locale', 'emptyText', 'emptyImage', 'showClear'],
methods: {
onClear() {
this.$emit('clear');
},
onClick(e){
this.$emit('click',e);
}
},
render() {
const { data = [], title, locale, emptyText, emptyImage, showClear = true ,onClear} = this
if (data.length === 0) {
return (
<div class="ai-notice notFound">
{emptyImage ? <img src={emptyImage} alt="not found" /> : null}
<div>{emptyText || locale.emptyText}</div>
</div>
);
}
return (
<div class="ai-notice">
<List class="list">
{data.map((item, i) => {
const itemCls = classNames('item', {
['read']: item.read,
});
// eslint-disable-next-line no-nested-ternary
const leftIcon = item.avatar ? (
typeof item.avatar === 'string' ? (
<Avatar class="avatar" src={item.avatar} />
) : (
item.avatar
)
) : null;
return (
<List.Item class={itemCls} rowKey={item.key || i} onClick={() => this.onClick(item)}>
<List.Item.Meta
class="meta"
avatar={<span class="iconElement">{leftIcon}</span>}
title={
<div class="title">
{item.title}
<div class="extra">{item.extra}</div>
</div>
}
description={
<div>
<div class="description" title={item.description}>
{item.description}
</div>
<div class="datetime">{item.datetime}</div>
</div>
}
/>
</List.Item>
)
})}
</List>
{showClear ? (
<div class="clear" onClick={onClear}>
{locale.clear}
{title}
</div>
) : null}
</div>
)
}
}
\ No newline at end of file
@import 'ant-design-vue/lib/style/themes/default.less';
@ai-notice-prefix: ~"@{ai-prefix}-notice";
.@{ai-notice-prefix}{
.list {
max-height: 400px;
overflow: auto;
.item {
transition: all 0.3s;
overflow: hidden;
cursor: pointer;
padding-left: 24px;
padding-right: 24px;
.meta {
width: 100%;
}
.avatar {
background: #fff;
margin-top: 4px;
}
.iconElement {
font-size: 32px;
}
&.read {
opacity: 0.4;
}
&:last-child {
border-bottom: 0;
}
&:hover {
background: @primary-1;
}
.title {
font-weight: normal;
margin-bottom: 8px;
}
.description {
font-size: 12px;
line-height: @line-height-base;
}
.datetime {
font-size: 12px;
margin-top: 4px;
line-height: @line-height-base;
}
.extra {
float: right;
color: @text-color-secondary;
font-weight: normal;
margin-right: 0;
margin-top: -1.5px;
}
}
}
&.notFound {
text-align: center;
padding: 73px 0 88px 0;
color: @text-color-secondary;
img {
display: inline-block;
margin-bottom: 16px;
height: 76px;
}
}
.clear {
height: 46px;
line-height: 46px;
text-align: center;
color: @text-color;
border-radius: 0 0 @border-radius-base @border-radius-base;
border-top: 1px solid @border-color-split;
transition: all 0.3s;
cursor: pointer;
&:hover {
color: @heading-color;
}
}
}
import './index.less'
import { Badge, Popover, Icon, Spin, Tabs } from "ant-design-vue";
import classNames from "classnames";
import List from './NoticeList';
const { TabPane } = Tabs
const NoticeIcon = {
props: ["className", "count", "bell", "locale", "loading"],
methods: {
classNames,
onClear(e) {
// console.log(e);
this.$emit('clear',e);
},
onTabChange(e) {
this.$emit('tabChange', e);
},
onPopupVisibleChange(e) {
this.$emit('popupVisibleChange', e);
},
onItemClick(item, tabProps) {
console.log(item);
this.$emit('itemClick', item, tabProps);
},
getNotificationBox() {
const { $slots, locale = {
emptyText: '暂无数据',
clear: '清空',
}, loading = false } = this;
const children = $slots.default;
if (!children) {
return null;
}
const panes = children.map(child => {
const { list, title, emptyText, emptyImage = 'https://gw.alipayobjects.com/zos/rmsportal/wAhyIChODzsoKIOBHcBk.svg' } = child.data.attrs
const tab = list && list.length > 0 ? `${title} (${list.length})` : title
return (
<TabPane tab={tab} key={title}>
<List
data={list}
onClick={item => this.onItemClick(item, child.data.attrs)}
onClear={() => this.onClear(title)}
title={title}
locale={locale}
emptyText={emptyText}
emptyImage={emptyImage}
/>
</TabPane>
)
})
return (
<Spin spinning={loading} delay={0}>
<Tabs class="tabs" onChange={this.onTabChange}>
{panes}
</Tabs>
</Spin>
)
}
},
render() {
const { className, bell, count, onPopupVisibleChange } = this
const noticeButtonClass = classNames(className, 'noticeButton');
const NoticeBellIcon = bell || <Icon type="bell" class="icon" />;
const trigger = (
<span class={noticeButtonClass}>
<Badge count={count} style={{ boxShadow: 'none' }} class="badge">
{NoticeBellIcon}
</Badge>
</span>
);
const notificationBox = this.getNotificationBox();
if (!notificationBox) {
return trigger;
}
return (
<Popover
placement="bottomRight"
content={notificationBox}
overlayClassName="ai-notice-popover"
trigger="click"
arrowPointAtCenter={true}
// popupAlign={popupAlign}
onVisibleChange={onPopupVisibleChange}
>
{trigger}
</Popover>
)
}
}
NoticeIcon.Tab = TabPane
export default NoticeIcon
\ No newline at end of file
@import 'ant-design-vue/lib/style/themes/default.less';
@ai-notice-prefix: ~"@{ai-prefix}-notice";
.@{ai-notice-prefix}{
&.noticeButton {
cursor: pointer;
display: inline-block;
transition: all 0.3s;
}
.icon {
font-size: 18px;
padding: 4px;
}
}
.@{ai-notice-prefix}-popover {
width: 336px;
:global(.ant-popover-inner-content) {
padding: 0;
}
.tabs {
:global {
.ant-tabs-nav-scroll {
text-align: center;
}
.ant-tabs-bar {
margin-bottom: 4px;
}
}
}
}
<template>
<div :class="cls">
<div v-if="title" class="numberInfoTitle">
{{title}}
</div>
<div v-if="subTitle" class="numberInfoSubTitle">
{{subTitle}}
</div>
<div class="numberInfoValue" :style="{'margin-top:gap':gap}">
<span>
{{total}}
<em v-if="suffix" class="suffix">{{suffix}}</em>
</span>
<span v-if="status || subTotal" class="subTotal">
{{subTotal}}
<a-icon v-if="status" :type="`caret-${status}`" />}
</span>
</div>
</div>
</template>
<script>
import { Icon } from "ant-design-vue";
import classNames from "classnames";
export default {
props: [
"theme",
"title",
"subTitle",
"total",
"subTotal",
"status",
"suffix",
"gap"
],
components: {
AIcon: Icon
},
computed: {
cls() {
const { theme } = this;
return classNames("numberInfo", {
["numberInfo" + theme]: theme
});
}
}
};
</script>
\ No newline at end of file
import './index.less'
import NumberInfo from './NumberInfo'
export default NumberInfo
\ No newline at end of file
@import 'ant-design-vue/lib/style/themes/default.less';
.numberInfo {
.suffix {
color: @text-color;
font-size: 16px;
font-style: normal;
margin-left: 4px;
}
.numberInfoTitle {
color: @text-color;
font-size: @font-size-lg;
margin-bottom: 16px;
transition: all 0.3s;
}
.numberInfoSubTitle {
color: @text-color-secondary;
font-size: @font-size-base;
height: 22px;
line-height: 22px;
overflow: hidden;
text-overflow: ellipsis;
word-break: break-all;
white-space: nowrap;
}
.numberInfoValue {
margin-top: 4px;
font-size: 0;
overflow: hidden;
text-overflow: ellipsis;
word-break: break-all;
white-space: nowrap;
& > span {
color: @heading-color;
display: inline-block;
line-height: 32px;
height: 32px;
font-size: 24px;
margin-right: 32px;
}
.subTotal {
color: @text-color-secondary;
font-size: @font-size-lg;
vertical-align: top;
margin-right: 0;
i {
font-size: 12px;
transform: scale(0.82);
margin-left: 4px;
}
:global {
.anticon-caret-up {
color: @red-6;
}
.anticon-caret-down {
color: @green-6;
}
}
}
}
}
.numberInfolight {
.numberInfoValue {
& > span {
color: @text-color;
}
}
}
import './index.less'
import pathToRegexp from 'path-to-regexp';
import { Breadcrumb,Tabs } from "ant-design-vue";
import eventBus from '@/utils/eventBus.js'
const {TabPane} = Tabs
export function urlToList(url) {
const urllist = url.split('/').filter(i => i);
return urllist.map((urlItem, index) => `/${urllist.slice(0, index + 1).join('/')}`);
}
export const getBreadcrumb = (breadcrumbNameMap, url) => {
let breadcrumb = breadcrumbNameMap[url];
if (!breadcrumb) {
Object.keys(breadcrumbNameMap).forEach(item => {
if (pathToRegexp(item).test(url)) {
breadcrumb = breadcrumbNameMap[item];
}
});
}
return breadcrumb || {};
};
const PageHeader = {
props: ["wide", "home", "title", "action", "content", "extraContent", "breadcrumbList", "breadcrumbSeparator", "itemRender", "linkElement","tabList", "tabActiveKey", "tabBarExtraContent","tabChange"],
methods: {
conversionFromProps() {
const { breadcrumbList, breadcrumbSeparator, itemRender, linkElement = 'a' } = this;
return (
<Breadcrumb class="breadcrumb" separator={breadcrumbSeparator}>
{breadcrumbList.map(item => {
const title = itemRender ? itemRender(item) : item.title;
return (
<Breadcrumb.Item key={item.title}>
{item.href
? h(
linkElement,
{
attrs: {
[linkElement === 'a' ? 'href' : 'to']: item.href,
}
},
title
)
: title}
</Breadcrumb.Item>
);
})}
</Breadcrumb>
);
},
getBreadcrumbProps() {
const { params, $router, $route } = this;
const { breadcrumbNameMap } = eventBus;
return {
router: $router,
params,
route: $route,
breadcrumbNameMap
};
},
conversionBreadcrumbList() {
const { breadcrumbList, breadcrumbSeparator } = this;
const { router, params, route, breadcrumbNameMap } = this.getBreadcrumbProps();
if (breadcrumbList && breadcrumbList.length) {
return this.conversionFromProps();
}
// 如果传入 routes 和 params 属性
// If pass routes and params attributes
// if (router && params) {
// return (
// <Breadcrumb
// class="breadcrumb"
// routes={routes.filter(route => route.breadcrumbName)}
// params={params}
// // itemRender={this.itemRender}
// separator={breadcrumbSeparator}
// />
// );
// }
// 根据 location 生成 面包屑
// Generate breadcrumbs based on location
if (route && route.path) {
return this.conversionFromLocation(route, breadcrumbNameMap);
}
return null;
},
conversionFromLocation(route, breadcrumbNameMap) {
const { breadcrumbSeparator, home, itemRender, linkElement = 'a' } = this;
// Convert the url to an array
const pathSnippets = urlToList(route.path);
// Loop data mosaic routing
const extraBreadcrumbItems = pathSnippets.map((url, index) => {
const currentBreadcrumb = getBreadcrumb(breadcrumbNameMap, url);
if (currentBreadcrumb.inherited) {
return null;
}
const isLinkable = index !== pathSnippets.length - 1 && !currentBreadcrumb.menus;
const name = itemRender ? itemRender(currentBreadcrumb) : currentBreadcrumb.name;
return currentBreadcrumb.name && !currentBreadcrumb.hideInBreadcrumb ? (
<Breadcrumb.Item key={url}>
{h(
isLinkable ? linkElement : 'span',
{ attrs: { [linkElement === 'a' ? 'href' : 'to']: url } },
name
)}
</Breadcrumb.Item>
) : null;
});
// Add home breadcrumbs to your head
extraBreadcrumbItems.unshift(
<Breadcrumb.Item key="home">
{h(
linkElement,
{
attrs:
{
[linkElement === 'a' ? 'href' : 'to']: '/'
}
},
home || 'Home'
)}
</Breadcrumb.Item>
);
return (
<Breadcrumb class="breadcrumb" separator={breadcrumbSeparator}>
{extraBreadcrumbItems}
</Breadcrumb>
);
},
// itemRender(route, params, routes, paths){
// const { linkElement = 'a' } = this;
// const last = routes.indexOf(route) === routes.length - 1;
// return last || !route.component ? (
// <span>{route.breadcrumbName}</span>
// ) : (
// // createElement(
// // linkElement,
// // {
// // href: paths.join('/') || '/',
// // to: paths.join('/') || '/',
// // },
// // route.breadcrumbName
// // )
// <a>atest</a>
// );
// }
},
render() {
const { wide = false, logo, title, action, content, extraContent, tabList, tabActiveKey, tabBarExtraContent, tabChange } = this
const breadcrumb = this.conversionBreadcrumbList();
return (
<div class="pageHeader">
<div class={wide ? 'wide' : ''}>
{breadcrumb}
<div class="detail">
{logo && <div class="logo">{logo}</div>}
<div class="main">
<div class="row">
{title && <h1 class="title">{title}</h1>}
{action && <div class="action">{action}</div>}
</div>
<div class="row">
{content && <div class="content">{content}</div>}
{extraContent && <div class="extraContent">{extraContent}</div>}
</div>
</div>
</div>
{tabList && tabList.length ? (
<Tabs
size="small"
class="tabs"
defaultActiveKey={tabActiveKey}
onChange={tabChange}
tabBarExtraContent={tabBarExtraContent}
>
{tabList.map(item => (
<TabPane tab={item.tab} key={item.key} />
))}
</Tabs>
) : null}
</div>
</div>
)
}
}
export default PageHeader
\ No newline at end of file
@import 'ant-design-vue/lib/style/themes/default.less';
.pageHeader {
background: @component-background;
padding: 16px 32px 0 32px;
border-bottom: @border-width-base @border-style-base @border-color-split;
.wide {
max-width: 1200px;
margin: auto;
}
.detail {
display: flex;
}
.row {
display: flex;
width: 100%;
}
.breadcrumb {
margin-bottom: 16px;
}
.tabs {
margin: 0 0 0 -8px;
:global {
.ant-tabs-bar {
border-bottom: 0px;
margin: 0px;
}
}
}
.logo {
flex: 0 1 auto;
margin-right: 16px;
padding-top: 1px;
> img {
width: 28px;
height: 28px;
border-radius: @border-radius-base;
display: block;
}
}
.title {
font-size: 20px;
font-weight: 500;
color: @heading-color;
}
.action {
margin-left: 56px;
min-width: 266px;
:global {
.ant-btn-group:not(:last-child),
.ant-btn:not(:last-child) {
margin-right: 8px;
}
.ant-btn-group > .ant-btn {
margin-right: 0;
}
}
}
.title,
.content {
flex: auto;
}
.action,
.extraContent,
.main {
flex: 0 1 auto;
}
.main {
width: 100%;
}
.title,
.action {
margin-bottom: 16px;
}
.logo,
.content,
.extraContent {
margin-bottom: 16px;
}
.action,
.extraContent {
text-align: right;
}
.extraContent {
margin-left: 88px;
min-width: 242px;
}
}
@media screen and (max-width: @screen-xl) {
.pageHeader {
.extraContent {
margin-left: 44px;
}
}
}
@media screen and (max-width: @screen-lg) {
.pageHeader {
.extraContent {
margin-left: 20px;
}
}
}
@media screen and (max-width: @screen-md) {
.pageHeader {
.row {
display: block;
}
.action,
.extraContent {
margin-left: 0;
text-align: left;
}
}
}
@media screen and (max-width: @screen-sm) {
.pageHeader {
.detail {
display: block;
}
}
}
@media screen and (max-width: @screen-xs) {
.pageHeader {
.action {
:global {
.ant-btn-group,
.ant-btn {
display: block;
margin-bottom: 8px;
}
.ant-btn-group > .ant-btn {
display: inline-block;
margin-bottom: 0;
}
}
}
}
}
import './GridContent.less'
import { mapGetters } from "vuex";
const GridContent = {
computed: {
...mapGetters({
settings: "global/settings"
})
},
render() {
const { $slots } = this;
const { contentWidth } = this.settings;
const children = $slots.default;
let className = `main`;
if (contentWidth === 'Fixed') {
className = `main wide`;
}
return <div class={className}>{children}</div>;
}
}
export default GridContent
\ No newline at end of file
.main {
width: 100%;
height: 100%;
min-height: 100%;
transition: 0.3s;
&.wide {
max-width: 1200px;
margin: 0 auto;
}
}
\ No newline at end of file
import './index.less'
import GridContent from './GridContent';
import PageHeader from '@/components/PageHeader';
import { mapGetters } from "vuex";
const PageHeaderWrapper = {
props: ["wrapperClassName","loading","content","extraContent","breadcrumbList","title","tabList", "tabActiveKey", "tabBarExtraContent","tabChange"],
computed: {
...mapGetters({
settings: "global/settings"
})
},
render(){
const {$slots,wrapperClassName,content,extraContent,breadcrumbList,title,tabList,tabActiveKey,tabBarExtraContent,tabChange} = this
const children = $slots.default;
const top = $slots.top;
return (
<div style={{ margin: '-24px -24px 0' }} class={wrapperClassName}>
{top}
<PageHeader
wide={this.settings.contentWidth === 'Fixed'}
home={this.$t('menu.home')}
content={content}
extraContent={extraContent}
breadcrumbList={breadcrumbList}
title={title}
linkElement={'router-link'}
tabList={tabList}
tabActiveKey={tabActiveKey}
tabBarExtraContent={tabBarExtraContent}
tabChange={tabChange}
itemRender={item => {
if (item.locale) {
return this.$t(item.locale);
}
return item.name;
}}
/>
{children?(
<div class="pageheaderwrapper-content">
<GridContent>{children}</GridContent>
</div>
):null}
</div>
)
}
}
export default PageHeaderWrapper
\ No newline at end of file
@import 'ant-design-vue/lib/style/themes/default.less';
.pageheaderwrapper-content {
margin: 24px 24px 0;
}
@media screen and (max-width: @screen-sm) {
.pageheaderwrapper-content {
margin: 24px 0 0;
}
}
<template>
<ai-send-captcha-button :size="size" :disabled="start" @click="handleClick" v-text="tmpStr" />
</template>
<script>
import { Button } from "ant-design-vue";
const SendCaptchaButton = {
extends: Button,
components: {
"ai-send-captcha-button": Button
},
data() {
return {
tmpStr: this.initStr,
runStr: "{%s}秒后重试",
resetStr: "重新获取",
timer: null,
start: false,
runSecond: this.second,
lastSecond: 0
};
},
props: {
initStr: {
type: String,
default: "获取验证码"
},
second: {
default: 60,
validator(val) {
return /^\d*$/.test(val);
}
},
value: {
default: false,
type: Boolean
},
storageKey: {
default: "SendCaptchaStorageKey",
type: String
}
},
methods: {
run() {
let lastSecond = this.lastSecond;
let second = lastSecond ? lastSecond : this.runSecond;
if (this.storageKey) {
const runSecond = new Date().getTime() + second * 1000;
window.sessionStorage.setItem(this.storageKey, runSecond);
}
if (!lastSecond) {
this.tmpStr = this.getStr(second);
}
this.timer = setInterval(() => {
second--;
this.tmpStr = this.getStr(second);
second <= 0 && this.timeout();
}, 1000);
},
timeout() {
this.tmpStr = this.resetStr;
this.start = false;
this.$emit("input", false);
clearInterval(this.timer);
},
getStr(second) {
return this.runStr.replace(/\{([^{]*?)%s(.*?)\}/g, second);
},
handleClick() {
// this.start = true;
this.$emit("click");
}
},
watch: {
value(val) {
this.start = val;
if (!val) {
clearInterval(this.timer);
if (this.storageKey) {
window.sessionStorage.removeItem(this.storageKey);
this.lastSecond = 0;
}
} else {
this.run();
}
}
},
created() {
const lastSecond = ~~(
(window.sessionStorage.getItem(this.storageKey) - new Date().getTime()) /
1000
);
if (lastSecond > 0 && this.storageKey) {
this.$emit("input", true);
this.tmpStr = this.getStr(lastSecond);
this.lastSecond = lastSecond;
}
},
beforeDestroy() {
!this.storageKey && this.timeout();
}
};
export default SendCaptchaButton;
</script>
\ No newline at end of file
import { Tooltip, Icon } from "ant-design-vue";
import './index.less';
const BlockChecbox = {
props: ["value", "list"],
methods:{
handleChange(key) {
this.$emit('change', key);
},
},
render(){
const {value,list} = this
return (
<div class="blockChecbox" key={value}>
{list.map(item => (
<Tooltip title={item.title} key={item.key}>
<div class="item" onClick={() => this.handleChange(item.key)}>
<img src={item.url} alt={item.key} />
<div
class="selectIcon"
style={{ display: value === item.key ? 'block' : 'none', ...item.style}}
>
<Icon type="check"/>
</div>
</div>
</Tooltip>
))}
</div>
)
}
}
export default BlockChecbox;
import { Tooltip, Icon } from "ant-design-vue";
import styles from './ThemeColor.less';
const Tag = {
props: ["color", "check"],
methods:{
handleChange(color) {
this.$emit('change', color);
},
},
render() {
const { color, check } = this
return (
<div
onClick={()=>this.handleChange(color)}
style={{
backgroundColor: color,
}}
>
{check ? <Icon type="check" /> : ''}
</div>
)
}
}
const ThemeColor = {
props: ["colors", "title", "value"],
methods:{
handleChange(color) {
this.$emit('change', color);
},
},
render() {
const { colors, title, value } = this
let colorList = colors;
if (!colors) {
colorList = [
{
key: 'dust',
color: '#F5222D',
},
{
key: 'volcano',
color: '#FA541C',
},
{
key: 'sunset',
color: '#FAAD14',
},
{
key: 'cyan',
color: '#13C2C2',
},
{
key: 'green',
color: '#42b983',
},
{
key: 'daybreak',
color: '#1890FF',
},
{
key: 'geekblue',
color: '#2F54EB',
},
{
key: 'purple',
color: '#722ED1',
},
];
}
return (
<div class="themeColor">
<h3 class="title">{title}</h3>
<div>
{colorList.map(({ key, color }) => (
<Tooltip key={color} title={this.$t(`app.setting.themecolor.${key}`)}>
<Tag
class="colorBlock"
color={color}
check={value === color}
onChange={color=>this.handleChange(color)}
/>
</Tooltip>
))}
</div>
</div>
)
}
};
export default ThemeColor;
.themeColor {
overflow: hidden;
margin-top: 24px;
.title {
font-size: 14px;
color: rgba(0, 0, 0, 0.65);
line-height: 22px;
margin-bottom: 12px;
}
.colorBlock {
width: 20px;
height: 20px;
border-radius: 2px;
float: left;
cursor: pointer;
margin-right: 8px;
text-align: center;
color: #fff;
font-weight: bold;
}
}
\ No newline at end of file
import './index.less';
import ThemeColor from './ThemeColor';
import BlockChecbox from './BlockChecbox';
import { Drawer, Modal, Divider, message } from "ant-design-vue";
import { mapGetters } from "vuex";
const Body = {
props:['title'],
render: function render() {
var h = arguments[0];
const { $slots,title } = this
return h('div', { style: { marginBottom: 24 } }, [h('h3', { 'class': 'title' }, title), $slots['default']])
}
}
const SettingDrawer = {
// data: () => ({
// primaryColor: "#42b983",
// blockChecbox: "sidemenu"
// }),
props: ["collapse"],
computed: {
...mapGetters({
settings: "global/settings",
}),
},
methods: {
changeSetting(key,value) {
const nextState = this.settings;
nextState[key] = value;
if (key === 'layout') {
nextState.contentWidth = value === 'topmenu' ? 'Fixed' : 'Fluid';
}
// else if (key === 'fixedHeader' && !value) {
// nextState.autoHideHeader = false;
// }
// console.log(this.settings);
// console.log(nextState);
// this.$store.commit('global/UpdateDefaultSettings', this.s)
// this.$store.commit('global/UpdateDefaultSettings', nextState)
// console.log(key);
// console.log(value);
// message.loading("正在编译主题!", 3);
this.$store.dispatch('global/defaultSettings',true)
},
togglerContent() {
this.$parent.collapse = !this.collapse
}
},
render() {
const { collapse } = this
const { primaryColor, layout, navTheme } = this.settings
return (
<Drawer
title="我是一个抽屉"
placement="right"
closable={false}
onClose={this.togglerContent}
visible={collapse}
width={300}
>
<div class="setting-drawer content">
<Body title={this.$t('app.setting.pagestyle')}>
<BlockChecbox
list={[
{
key: 'dark',
url: 'https://gw.alipayobjects.com/zos/rmsportal/LCkqqYNmvBEbokSDscrm.svg',
title: this.$t('app.setting.pagestyle.dark'),
},
{
key: 'light',
url: 'https://gw.alipayobjects.com/zos/rmsportal/jpRkZQMyYRryryPNtyIC.svg',
title: this.$t('app.setting.pagestyle.light'),
},
]}
value={navTheme}
onChange={e=>{this.changeSetting('navTheme',e)}}
/>
</Body>
<Divider />
<ThemeColor
title={this.$t('app.setting.themecolor')}
value={primaryColor}
onChange={e=>{this.changeSetting('primaryColor',e)}}
/>
<Divider />
<Body title={this.$t('app.setting.navigationmode')}>
<BlockChecbox
list={[
{
key: 'sidemenu',
url: 'https://gw.alipayobjects.com/zos/rmsportal/JopDzEhOqwOjeNTXkoje.svg',
title: this.$t('app.setting.sidemenu'),
},
{
key: 'topmenu',
url: 'https://gw.alipayobjects.com/zos/rmsportal/KDNDBbriJhLwuqMoxcAr.svg',
title: this.$t('app.setting.topmenu'),
style: {paddingLeft: '18px'}
},
]}
value={layout}
onChange={e=>{this.changeSetting('layout',e)}}
/>
</Body>
<Divider />
<p>其它设置</p>
</div>
</Drawer>
)
}
}
export default SettingDrawer
\ No newline at end of file
@import 'ant-design-vue/lib/style/themes/default.less';
.setting-drawer{
&.content {
min-height: 100%;
background: #fff;
position: relative;
}
.blockChecbox {
display: flex;
.item {
margin-right: 16px;
position: relative;
// 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%;
padding-top: 15px;
padding-left: 24px;
height: 100%;
color: @primary-color;
font-size: 14px;
font-weight: bold;
}
}
.color_block {
width: 38px;
height: 22px;
margin: 4px;
border-radius: 4px;
cursor: pointer;
margin-right: 12px;
display: inline-block;
vertical-align: middle;
}
.title {
font-size: 14px;
color: @heading-color;
line-height: 22px;
margin-bottom: 12px;
}
.handle {
position: absolute;
top: 240px;
background: @primary-color;
width: 48px;
height: 48px;
right: 300px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
pointer-events: auto;
z-index: 0;
text-align: center;
font-size: 16px;
border-radius: 4px 0 0 4px;
}
.productionHint {
font-size: 12px;
margin-top: 16px;
}
}
<script>
import { Menu, Icon, Spin } from "ant-design-vue";
import { mapGetters } from "vuex";
// Conversion router to menu.
export default {
props: {
collapsed: { default: false, type: Boolean },
theme: { default: "dark", type: String },
layout: { type: String },
mode: { default: "inline", type: String },
menuData: { default: [], type: Array },
styles: { type: String }
},
computed: {
...mapGetters({
loading: "global/nav/loading"
}),
},
components: {
AMenu: Menu,
AMenuItem: Menu.Item,
ASubMenu: Menu.SubMenu,
AMenuDivider: Menu.Divider,
AMenuItemGroup: Menu.ItemGroup,
AIcon: Icon
},
methods: {
getIcon(icon) {
if (typeof icon === "string" && icon.indexOf("http") === 0) {
return <img src={icon} alt="icon" class="icon" />;
}
if (typeof icon === "string") {
return <a-icon type={icon} />;
}
// if(!icon){
// return <a-icon type='profile' />;
// }
return icon;
},
getNavMenuItems(menusData, parent) {
// console.log(menusData)
if (!menusData) {
return [];
}
return menusData.map(item => {
if (item.name) {
return this.getSubMenuOrItem(item, parent);
}
if (item.menus) {
return this.getNavMenuItems(item.menus, parent);
}
});
},
getSubMenuOrItem(item) {
// doc: add hideChildrenInMenu 隐藏菜单
if (
item.menus &&
item.menus.some(menu => menu.name)
) {
const name = this.$t(item.locale);
return (
<a-sub-menu
title={
item.icon ? (
<span>
{this.getIcon(item.icon)}
<span>{name}</span>
</span>
) : (
name
)
}
key={item.path}
>
{this.getNavMenuItems(item.menus)}
</a-sub-menu>
);
}
return (
<a-menu-item key={item.path}>{this.getMenuItemPath(item)}</a-menu-item>
);
},
getMenuItemPath(item) {
const name = this.$t(item.locale);
const itemPath = this.conversionPath(item.path);
const icon = this.getIcon(item.icon);
// // Is it a http link
if (/^https?:\/\//.test(itemPath)) {
return (
<a href={itemPath}>
{icon}
<span>{name}</span>
</a>
);
}
return (
<router-link to={itemPath}>
{icon}
<span>{name}</span>
</router-link>
);
},
conversionPath(path) {
if (path && path.indexOf("http") === 0) {
return path;
}
return `/${path || ""}`.replace(/\/+/g, "/");
},
urlToList(url) {
const urllist = url.split("/").filter(i => i);
return urllist.map(
(urlItem, index) => `/${urllist.slice(0, index + 1).join("/")}`
);
},
getOpenKeys(path) {
const openKeys = this.urlToList(path);
if (this.layout === "topmenu") {
return null
}
return openKeys.filter(item => item !== path);
}
},
render() {
const { path } = this.$route;
const openKeys = this.getOpenKeys(path);
return (
<Spin spinning={this.loading} class="baseMenuLoadding">
<a-menu
defaultOpenKeys={openKeys}
selectedKeys={[path]}
key="Menu"
mode={this.mode}
theme={this.theme}
collapsed={this.collapsed}
style={this.styles}
>
{this.getNavMenuItems(this.menuData)}
</a-menu>
</Spin>
);
}
};
</script>
\ No newline at end of file
<template>
<a-layout-sider v-model="collapsed" width="256" :class="`ai-sider-menu sider ${fixSiderbar?'fixSiderbar':'',settings.navTheme==='light'?'light':'dark'}`" :theme="settings.navTheme">
<div class="logo" key="logo" id="logo">
<router-link to="/">
<img :src="logo" alt="logo" />
<h1>{{ settings.leftMenuTitle }}</h1>
</router-link>
</div>
<a-base-menu :collapsed="collapsed" :menuData="menuData" :theme="settings.navTheme" :layout="settings.layout" styles="padding: '16px 0'; width: '100%'"/>
</a-layout-sider>
</template>
<script>
import { Layout } from "ant-design-vue";
import ABaseMenu from "@/components/SiderMenu/BaseMenu";
import { mapGetters } from "vuex";
export default {
props: {
collapsed: {
default: false,
type: Boolean
},
fixSiderbar: {
default: false,
type: Boolean
},
menuData: {
default: () => [],
type: Array
},
logo: { type: String }
},
computed: {
...mapGetters({
settings: "global/settings"
})
},
components: {
ALayoutSider: Layout.Sider,
ABaseMenu
},
};
</script>
import './index.less'
import SiderMenu from './SiderMenu.vue'
export default SiderMenu
\ No newline at end of file
@import 'ant-design-vue/lib/style/themes/default.less';
@ai-sider-menu-prefix: ~"@{ai-prefix}-sider-menu";
.@{ai-sider-menu-prefix}{
.logo{
width: 100%;
height: 64px;
position: relative;
line-height: 64px;
padding-left: (@menu-collapsed-width - 32px) / 2;
transition: all 0.3s;
background: #002140;
overflow: hidden;
img {
display: inline-block;
vertical-align: middle;
height: 32px;
}
h1 {
color: white;
display: inline-block;
vertical-align: middle;
font-size: 20px;
margin: 0 0 0 12px;
font-family: 'Myriad Pro', 'Helvetica Neue', Arial, Helvetica, sans-serif;
font-weight: 600;
}
}
}
.sider {
min-height: 100vh;
box-shadow: 2px 0 6px rgba(0, 21, 41, 0.35);
position: relative;
z-index: 10;
&.fixSiderbar {
position: fixed;
top: 0;
left: 0;
}
&.light {
box-shadow: 2px 0 8px 0 rgba(29, 35, 41, 0.05);
background-color: white;
.logo {
background: white;
border-bottom: 1px solid @border-color-split;
border-right: 1px solid @border-color-split;
h1 {
color: @primary-color;
}
}
}
}
.baseMenuLoadding{
}
import "./index.less";
import RightContent from "@/components/GlobalHeader/RightContent";
import BaseMenu from "@/components/SiderMenu/BaseMenu";
const TopNavHeader = {
props: ["menuData", "logo", "theme", "contentWidth", "layout", "title"],
computed: {
maxWidth() {
return (
(this.contentWidth === "Fixed" ? 1200 : window.innerWidth) -
330 -
165 -
4 -
36
);
},
},
render() {
const {
maxWidth,
menuData,
logo,
theme = "dark",
contentWidth = "Fixed",
layout,
// title = "admin",
} = this;
return (
<div class="top-nav-header">
<div class={`head ${theme === "light" ? "light" : ""}`}>
<div class={`main ${contentWidth === "Fixed" ? "wide" : ""}`}>
<div class="left">
<div class="logo">
<router-link to="/">
<img src={logo} alt="logo" />
<h1>Ant Design Pro</h1>
</router-link>
</div>
<div
style={{
maxWidth,
}}
>
<BaseMenu
theme={theme}
layout={layout}
mode="horizontal"
menuData={menuData}
styles="border: 'none'; height: 64px"
/>
</div>
</div>
<RightContent theme={theme} layout={layout} />
</div>
</div>
</div>
);
},
};
export default TopNavHeader;
.top-nav-header{
.head {
width: 100%;
transition: background 0.3s, width 0.2s;
height: 64px;
// padding: 0 12px 0 0;
padding:0px;
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
position: relative;
:global {
.ant-menu-submenu.ant-menu-submenu-horizontal {
height: 100%;
padding-top: 9px;
.ant-menu-submenu-title {
height: 100%;
}
}
}
&.light {
background-color: #fff;
}
.main {
display: flex;
height: 64px;
padding-left: 24px;
&.wide {
max-width: 1200px;
margin: auto;
padding-left: 4px;
}
.left {
flex: 1;
display: flex;
}
.right {
width: 324px;
}
}
}
.logo {
width: 165px;
height: 64px;
position: relative;
line-height: 64px;
transition: all 0.3s;
overflow: hidden;
a{
position: absolute;
top: -2px;
}
img {
display: inline-block;
vertical-align: middle;
height: 32px;
}
h1 {
color: #fff;
display: inline-block;
vertical-align: middle;
font-size: 16px;
margin: 0 0 0 12px;
font-weight: 600;
}
}
.light {
h1 {
color: #002140;
}
}
}
\ No newline at end of file
import './index.less';
import { Icon } from "ant-design-vue";
import classNames from 'classnames';
export default {
props: {
colorful: { default: true },
reverseColor: { default: false },
flag: { default: '' }
},
computed: {
cls() {
return classNames('trendItem', {
['trendItemGrey']: !this.colorful,
['reverseColor']: !this.reverseColor && !this.colorful,
})
}
},
render() {
const { flag, cls } = this
return (
<div class={cls}>
<span class='value'>{this.$slots.default}</span>
{flag && (
<span class={flag}>
<Icon type={`caret-${flag}`} />
</span>
)}
</div>
)
}
}
@import 'ant-design-vue/lib/style/themes/default.less';
.trendItem {
display: inline-block;
font-size: @font-size-base;
line-height: 22px;
.up,
.down {
margin-left: 4px;
position: relative;
top: 1px;
i {
font-size: 12px;
transform: scale(0.83);
}
}
.up {
color: @red-6;
}
.down {
color: @green-6;
top: -1px;
}
&.trendItemGrey .up,
&.trendItemGrey .down {
color: @text-color;
}
&.reverseColor .up {
color: @green-6;
}
&.reverseColor .down {
color: @red-6;
}
}
export default {
navTheme: 'dark', // theme for nav menu
primaryColor: '#1890FF', // 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
autoHideHeader: false, // auto hide header
fixSiderbar: false, // sticky siderbar
leftMenuTitle: "EVM 应用商店", // 左侧边栏顶部名称
leftMenuIcon: "" // 左侧边栏顶部Logo
}
import './BasicLayout.less'
import { Layout } from "ant-design-vue";
import pathToRegexp from 'path-to-regexp';
import SiderMenu from "@/components/SiderMenu";
import Header from './Header';
import Footer from './Footer';
import eventBus from '@/utils/eventBus.js'
import logo from "@/assets/logo.png";
import { mapGetters } from "vuex";
const BasicLayout = {
data: () => ({
layout: 'topmenu',
logo
}),
methods: {
formatter(data, parentPath = '', parentName) {
return data.map(item => {
let locale = 'menu';
if (parentName && item.name) {
locale = `${parentName}.${item.name}`;
} else if (item.name) {
locale = `menu.${item.name}`;
} else if (parentName) {
locale = parentName;
}
const result = {
...item,
locale
};
if (!item.leaf) {
const menus = this.formatter(item.children, `${parentPath}${item.path}/`, locale);
// Reduce memory usage
result.menus = menus;
}
delete result.children;
return result;
});
},
getMenuData() {
return this.formatter(this.menuNav.data);
},
/**
* 获取面包屑映射
* @param {Object} menuData 菜单配置
*/
getBreadcrumbNameMap() {
const routerMap = {};
const mergeMenuAndRouter = data => {
data.forEach(menuItem => {
if (menuItem.menus) {
mergeMenuAndRouter(menuItem.menus);
}
// Reduce memory usage
routerMap[menuItem.path] = menuItem;
});
};
mergeMenuAndRouter(this.getMenuData());
return routerMap;
},
matchParamsPath(pathname) {
const breadcrumbNameMap = this.getBreadcrumbNameMap()
const pathKey = Object.keys(breadcrumbNameMap).find(key =>
pathToRegexp(key).test(pathname)
);
return breadcrumbNameMap[pathKey];
},
getPageTitle(pathname) {
const currRouterData = this.matchParamsPath(pathname);
if (!currRouterData) {
return this.settings.leftMenuTitle;
}
const message = this.$t(currRouterData.locale || currRouterData.name)
return `${message} - ${this.settings.leftMenuTitle}`;
},
onResizeCollapsed() {
if (window.innerWidth <= 900) {
this.$store.commit('global/UpdateChangeLayoutCollapsed', true)
} else {
this.$store.commit('global/UpdateChangeLayoutCollapsed', false)
}
}
},
computed: {
...mapGetters({
collapsed: "global/getChangeLayoutCollapsed",
settings: "global/settings",
menuNav: "global/nav/getMenuNav"
}),
getContentStyle() {
return {
margin: '24px 24px 0',
paddingTop: this.settings.fixedHeader ? 64 : 0,
}
},
},
beforeRouteEnter(to, from, next) {
next(vm => {
// 通过 `vm` 访问组件实例
document.title = vm.getPageTitle(to.path)
})
},
beforeRouteUpdate(to, from, next) {
this.$store.commit('global/UpdateBasicLayoutSpinning', true)
document.title = this.getPageTitle(to.path)
this.$nextTick(() => {
next();
})
},
mounted() {
this.$store.dispatch('global/nav/getMenuNav');
this.onResizeCollapsed()
window.onresize = () => {
// 通过捕获系统的onresize事件触发我们需要执行的事件
this.onResizeCollapsed()
}
},
render() {
const { collapsed, getContentStyle, logo } = this;
const { layout } = this.settings;
const isTop = layout === 'topmenu';
const isMobile = false;
const menuData = this.getMenuData();
eventBus.breadcrumbNameMap = this.getBreadcrumbNameMap()
return (
<Layout class="ai-basic-layout-container">
{isTop && !isMobile ? null : (
<SiderMenu collapsed={collapsed} menuData={menuData} logo={logo}/>
)}
<Layout>
<Header menuData={menuData} logo={logo}/>
<Layout.Content style={getContentStyle}>
<router-view />
</Layout.Content>
<Footer />
</Layout>
</Layout>
)
}
}
export default BasicLayout
\ No newline at end of file
@ai-basic-layout-prefix: ~"@{ai-prefix}-basic-layout";
.@{ai-basic-layout-prefix}-container {
min-height: 100vh;
}
\ No newline at end of file
export default {
render() {
return <router-view />
}
}
\ No newline at end of file
import { Layout, Icon } from "ant-design-vue";
import GlobalFooter from '@/components/GlobalFooter';
const { Footer } = Layout;
const FooterView = {
render(){
return (
<Footer style={{ padding: 0 }}>
<GlobalFooter
links={[
{
key: 'Pro 首页',
title: 'Pro 首页',
href: 'https://github.com/ruyangit/seed-workbench-ui.git',
blankTarget: true,
},
{
key: 'github',
title: <Icon type="github" />,
href: 'https://github.com/ruyangit/seed-workbench-ui.git',
blankTarget: true,
},
{
key: 'Ant Design',
title: 'Ant Design',
href: 'https://ant.design',
blankTarget: true,
},
]}
copyright={
<div>
Copyright <Icon type="copyright" /> 2018 Ryan Ru 金服体验技术部出品
</div>
}
/>
</Footer>
);
}
}
export default FooterView;
import './Header.less'
import { Layout } from "ant-design-vue";
import GlobalHeader from '@/components/GlobalHeader';
import TopNavHeader from '@/components/TopNavHeader';
import { mapGetters } from "vuex";
const { Header } = Layout;
const HeaderView = {
props: ['menuData', 'logo'],
computed: {
...mapGetters({
settings: "global/settings"
})
},
render() {
const { menuData, logo } = this
const { layout, navTheme, fixedHeader, leftMenuTitle } = this.settings;
const isTop = layout === 'topmenu';
const isMobile = false;
return (
<Header style={{ padding: 0 }} class={fixedHeader ? 'fixedHeader' : ''}>
{isTop && !isMobile ? (
// <TopNavHeader
// theme={navTheme}
// mode="horizontal"
// Authorized={Authorized}
// onCollapse={handleMenuCollapse}
// onNoticeClear={this.handleNoticeClear}
// onMenuClick={this.handleMenuClick}
// onNoticeVisibleChange={this.handleNoticeVisibleChange}
// {...this.props}
// />
<TopNavHeader
theme={navTheme}
layout={layout}
mode="horizontal"
menuData={menuData}
logo={logo}
title={leftMenuTitle}
/>
) : (
// <GlobalHeader
// onCollapse={handleMenuCollapse}
// onNoticeClear={this.handleNoticeClear}
// onMenuClick={this.handleMenuClick}
// onNoticeVisibleChange={this.handleNoticeVisibleChange}
// {...this.props}
// />
<GlobalHeader theme={navTheme} layout={layout}/>
)}
</Header>
);
}
}
export default HeaderView;
.fixedHeader {
position: fixed;
top: 0;
width: 100%;
// > @zindex-tooltip
z-index: 1061;
transition: width 0.2s;
}
\ No newline at end of file
import './UserLayout.less'
import logo from '@/assets/logo.png';
const UserLayout = {
props: {
logo: { default: logo, types: String }
},
render() {
return (
<div class="ai-user-layout-container">
<div class="content">
<div class="top">
<div class="header">
<img alt="logo" class="logo" src={this.logo} />
<span class="title">
Ant Design
</span>
</div>
<div class="desc">
Ant Design 是西湖区最具影响力的 Web 设计规范
</div>
</div>
<router-view />
</div>
</div>
)
}
}
export default UserLayout
\ No newline at end of file
@import 'ant-design-vue/lib/style/themes/default.less';
@ai-user-layout-prefix: ~"@{ai-prefix}-user-layout";
.@{ai-user-layout-prefix}-container {
display: flex;
flex-direction: column;
height: 100vh;
overflow: auto;
background: @layout-body-background;
.content{
padding: 32px 0;
flex: 1;
}
.top {
text-align: center;
}
.header {
height: 44px;
line-height: 44px;
a {
text-decoration: none;
}
}
.logo {
height: 44px;
vertical-align: top;
margin-right: 16px;
}
.title {
font-size: 33px;
color: @heading-color;
font-family: 'Myriad Pro', 'Helvetica Neue', Arial, Helvetica, sans-serif;
font-weight: 600;
position: relative;
top: 2px;
}
.desc {
font-size: @font-size-base;
color: @text-color-secondary;
margin-top: 12px;
margin-bottom: 40px;
}
}
@media (min-width: @screen-md-min) {
.@{ai-user-layout-prefix}-container {
background-image: url('https://gw.alipayobjects.com/zos/rmsportal/TVYTbAXWheQpRcWDaDMu.svg');
background-repeat: no-repeat;
background-position: center 110px;
background-size: 100%;
.content{
padding: 112px 0 24px 0;
}
}
}
This diff is collapsed.
// import zhMessages from '../../locales/zh.json';
import zh_CN from "ant-design-vue/es/locale-provider/zh_CN";
export default {
...zh_CN,
'navbar.lang': 'English',
'menu.home': '首页',
'menu.dashboard': 'Dashboard',
'menu.dashboard.analysis': '分析页',
'menu.dashboard.monitor': '监控页',
'menu.dashboard.workplace': '工作台',
'menu.form': '表单页',
'menu.form.basicform': '基础表单',
'menu.form.stepform': '分步表单',
'menu.form.stepform.info': '分步表单(填写转账信息)',
'menu.form.stepform.confirm': '分步表单(确认转账信息)',
'menu.form.stepform.result': '分步表单(完成)',
'menu.form.advancedform': '高级表单',
'menu.list': '列表页',
'menu.list.searchtable': '查询表格',
'menu.list.basiclist': '标准列表',
'menu.list.cardlist': '卡片列表',
'menu.list.searchlist': '搜索列表',
'menu.list.searchlist.articles': '搜索列表(文章)',
'menu.list.searchlist.projects': '搜索列表(项目)',
'menu.list.searchlist.applications': '搜索列表(应用)',
'menu.profile': '详情页',
'menu.profile.basic': '基础详情页',
'menu.profile.advanced': '高级详情页',
'menu.result': '结果页',
'menu.result.success': '成功页',
'menu.result.fail': '失败页',
'menu.exception': '异常页',
'menu.exception.not-permission': '403',
'menu.exception.not-find': '404',
'menu.exception.server-error': '500',
'menu.exception.trigger': '触发错误',
'menu.account': '个人页',
'menu.account.center': '个人中心',
'menu.account.settings': '个人设置',
'menu.account.trigger': '触发报错',
//---
'menu.system': '系统管理',
'menu.system.setting': '系统设置',
'menu.system.setting.menu': '菜单管理',
'menu.system.setting.module': '模块管理',
'menu.system.setting.config': '配置管理',
'menu.system.setting.dict': '字典管理',
'menu.system.setting.area': '行政区划',
'menu.system.role': '权限管理',
'menu.system.admin': '系统管理员',
//---
'app.home.introduce': '介绍',
'app.analysis.test': '工专路 {no} 号店',
'app.analysis.introduce': '指标说明',
'app.analysis.total-sales': '总销售额',
'app.analysis.day-sales': '日销售额',
'app.analysis.visits': '访问量',
'app.analysis.visits-trend': '访问量趋势',
'app.analysis.visits-ranking': '门店访问量排名',
'app.analysis.day-visits': '日访问量',
'app.analysis.week': '周同比',
'app.analysis.day': '日同比',
'app.analysis.payments': '支付笔数',
'app.analysis.conversion-rate': '转化率',
'app.analysis.operational-effect': '运营活动效果',
'app.analysis.sales-trend': '销售趋势',
'app.analysis.sales-ranking': '门店销售额排名',
'app.analysis.all-year': '全年',
'app.analysis.all-month': '本月',
'app.analysis.all-week': '本周',
'app.analysis.all-day': '今日',
'app.analysis.search-users': '搜索用户数',
'app.analysis.per-capita-search': '人均搜索次数',
'app.analysis.online-top-search': '线上热门搜索',
'app.analysis.the-proportion-of-sales': '销售额类别占比',
'app.analysis.channel.all': '全部渠道',
'app.analysis.channel.online': '线上',
'app.analysis.channel.stores': '门店',
'app.analysis.sales': '销售额',
'app.analysis.traffic': '客流量',
'app.analysis.table.rank': '排名',
'app.analysis.table.search-keyword': '搜索关键词',
'app.analysis.table.users': '用户数',
'app.analysis.table.weekly-range': '周涨幅',
'app.settings.menuMap.basic': '基本设置',
'app.settings.menuMap.security': '安全设置',
'app.settings.menuMap.binding': '账号绑定',
'app.settings.menuMap.notification': '新消息通知',
'app.settings.basic.avatar': '更换头像',
'app.settings.basic.email': '邮箱',
'app.settings.basic.email-message': '请输入您的邮箱!',
'app.settings.basic.nickname': '昵称',
'app.settings.basic.nickname-message': '请输入您的昵称!',
'app.settings.basic.profile': '个人简介',
'app.settings.basic.profile-message': '请输入个人简介!',
'app.settings.basic.profile-placeholder': '个人简介',
'app.settings.basic.country': '国家/地区',
'app.settings.basic.country-message': '请输入您的国家或地区!',
'app.settings.basic.geographic': '所在省市',
'app.settings.basic.geographic-message': '请输入您的所在省市!',
'app.settings.basic.address': '街道地址',
'app.settings.basic.address-message': '请输入您的街道地址!',
'app.settings.basic.phone': '联系电话',
'app.settings.basic.phone-message': '请输入您的联系电话!',
'app.settings.basic.update': '更新基本信息',
'app.settings.security.strong': '',
'app.settings.security.medium': '',
'app.settings.security.weak': '',
'app.settings.security.password': '账户密码',
'app.settings.security.password-description': '当前密码强度:',
'app.settings.security.phone': '密保手机',
'app.settings.security.phone-description': '已绑定手机:',
'app.settings.security.question': '密保问题',
'app.settings.security.question-description': '未设置密保问题,密保问题可有效保护账户安全',
'app.settings.security.email': '备用邮箱',
'app.settings.security.email-description': '已绑定邮箱:',
'app.settings.security.mfa': 'MFA 设备',
'app.settings.security.mfa-description': '未绑定 MFA 设备,绑定后,可以进行二次确认',
'app.settings.security.modify': '修改',
'app.settings.security.set': '设置',
'app.settings.security.bind': '绑定',
'app.settings.binding.taobao': '绑定淘宝',
'app.settings.binding.taobao-description': '当前未绑定淘宝账号',
'app.settings.binding.alipay': '绑定支付宝',
'app.settings.binding.alipay-description': '当前未绑定支付宝账号',
'app.settings.binding.dingding': '绑定钉钉',
'app.settings.binding.dingding-description': '当前未绑定钉钉账号',
'app.settings.binding.bind': '绑定',
'app.settings.notification.password': '账户密码',
'app.settings.notification.password-description': '其他用户的消息将以站内信的形式通知',
'app.settings.notification.messages': '系统消息',
'app.settings.notification.messages-description': '系统消息将以站内信的形式通知',
'app.settings.notification.todo': '账户密码',
'app.settings.notification.todo-description': '账户密码',
'app.settings.open': '',
'app.settings.close': '',
'app.exception.back': '返回首页',
'app.exception.description.403': '抱歉,你无权访问该页面',
'app.exception.description.404': '抱歉,你访问的页面不存在',
'app.exception.description.500': '抱歉,服务器出错了',
'app.result.error.title': '提交失败',
'app.result.error.description': '请核对并修改以下信息后,再重新提交。',
'app.result.error.hint-title': '您提交的内容有如下错误:',
'app.result.error.hint-text1': '您的账户已被冻结',
'app.result.error.hint-btn1': '立即解冻',
'app.result.error.hint-text2': '您的账户还不具备申请资格',
'app.result.error.hint-btn2': '立即升级',
'app.result.error.btn-text': '返回修改',
'app.result.success.title': '提交成功',
'app.result.success.description':
'提交结果页用于反馈一系列操作任务的处理结果, 如果仅是简单操作,使用 Message 全局提示反馈即可。 本文字区域可以展示简单的补充说明,如果有类似展示 “单据”的需求,下面这个灰色区域可以呈现比较复杂的内容。',
'app.result.success.operate-title': '项目名称',
'app.result.success.operate-id': '项目 ID:',
'app.result.success.principal': '负责人:',
'app.result.success.operate-time': '生效时间:',
'app.result.success.step1-title': '创建项目',
'app.result.success.step1-operator': '曲丽丽',
'app.result.success.step2-title': '部门初审',
'app.result.success.step2-operator': '周毛毛',
'app.result.success.step2-extra': '催一下',
'app.result.success.step3-title': '财务复核',
'app.result.success.step4-title': '完成',
'app.result.success.btn-return': '返回列表',
'app.result.success.btn-project': '查看项目',
'app.result.success.btn-print': '打印',
'app.setting.pagestyle': '整体风格设置',
'app.setting.pagestyle.dark': '暗色菜单风格',
'app.setting.pagestyle.light': '亮色菜单风格',
'app.setting.content-width': '内容区域宽度',
'app.setting.content-width.fixed': '定宽',
'app.setting.content-width.fluid': '流式',
'app.setting.themecolor': '主题色',
'app.setting.themecolor.dust': '薄暮',
'app.setting.themecolor.volcano': '火山',
'app.setting.themecolor.sunset': '日暮',
'app.setting.themecolor.cyan': '明青',
'app.setting.themecolor.green': '极光绿',
'app.setting.themecolor.daybreak': '拂晓蓝(默认)',
'app.setting.themecolor.geekblue': '极客蓝',
'app.setting.themecolor.purple': '酱紫',
'app.setting.navigationmode': '导航模式',
'app.setting.sidemenu': '侧边菜单布局',
'app.setting.topmenu': '顶部菜单布局',
'app.setting.fixedheader': '固定 Header',
'app.setting.fixedsidebar': '固定侧边菜单',
'app.setting.fixedsidebar.hint': '侧边菜单布局时可配置',
'app.setting.hideheader': '下滑时隐藏 Header',
'app.setting.hideheader.hint': '固定 Header 时可配置',
'app.setting.othersettings': '其他设置',
'app.setting.weakmode': '色弱模式',
'app.setting.copy': '拷贝设置',
'app.setting.copyinfo': '拷贝成功,请到 src/defaultSettings.js 中替换默认配置',
'app.setting.production.hint':
'配置栏只在开发环境用于预览,生产环境不会展现,请拷贝后手动修改配置文件',
};
import Vue from 'vue'
import App from './App'
import store from './store'
import router from './router'
import Vuei18n from 'vue-i18n'
import { sync } from 'vuex-router-sync'
import zh_CN from "./locales/zh_CN";
import en_US from "./locales/en_US";
import defaultSettings from './defaultSettings'
Vue.config.productionTip = false
Vue.use(Vuei18n)
const messages = {
"zh_CN": zh_CN,
"en_US": en_US,
}
const i18n = new Vuei18n({
locale: 'zh_CN', // 语言标识
messages
})
sync(store, router)
//加载默认设置
store.commit('global/UpdateDefaultSettings', defaultSettings)
new Vue({
router,
store,
i18n,
render: h => h(App)
}).$mount('#app')
import Vue from 'vue'
import Router from 'vue-router'
import store from '../store'
import UserLayout from '@/layouts/UserLayout'
import BasicLayout from '@/layouts/BasicLayout'
import BlankLayout from '@/layouts/BlankLayout'
Vue.use(Router)
const router = new Router({
routes: [
// user
{
path: '/user',
component: UserLayout,
children: [
{ path: '/user', redirect: '/user/login' },
{ path: '/user/login', component: () => import('@/views/User/Login') },
{ path: '/user/register', component: () => import('@/views/User/Register') },
],
},
// app
{
path: '/',
component: BasicLayout,
children: [
// dashboard
{ path: '/', redirect: '/dashboard/analysis' },
{
path: '/dashboard',
name: 'dashboard',
component: BlankLayout,
children: [
{ path: '/dashboard/analysis', name: 'analysis', component: () => import('@/views/Dashboard/Analysis') },
// { path: '/dashboard/monitor', name: 'monitor', component: () => import('@/views/Dashboard/Analysis') },
{ path: '/dashboard/workplace', name: 'workplace', component: () => import('@/views/Dashboard/Workplace') },
]
},
// {
// path: '/form',
// name: 'form',
// icon: 'form',
// component: BlankLayout,
// children: [
// { path: '/form/basic-form', name: 'basicform', component: () => import('@/views/Dashboard/Analysis') },
// {
// path: '/form/step-form',
// name: 'stepform',
// component: BlankLayout,
// hideChildrenInMenu: true,
// children: [
// {
// path: '/form/step-form/info',
// name: 'info',
// component: () => import('@/views/Dashboard/Analysis'),
// },
// ]
// },
// { path: '/form/advanced-form', name: 'advancedform', component: () => import('@/views/Dashboard/Analysis') },
// ]
// },
// {
// path: '/list',
// icon: 'table',
// name: 'list',
// component: BlankLayout,
// children: [
// {
// path: '/list/search',
// name: 'searchlist',
// component: BlankLayout,
// children: [
// {
// path: '/list/search/articles',
// name: 'articles',
// },
// {
// path: '/list/search/projects',
// name: 'projects',
// },
// {
// path: '/list/search/applications',
// name: 'applications',
// },
// ]
// }
// ]
// },
// {
// path: '/profile',
// icon: 'profile',
// name: 'profile',
// },
{
path: '/system',
name: 'system',
component: BlankLayout,
children: [
{
path: '/system/setting',
name: 'setting',
component: BlankLayout,
children: [
{
path: '/system/setting/menu',
name: 'menu',
component: () => import('@/views/System/Menu')
},
{
path: '/system/setting/module',
name: 'module',
component: () => import('@/views/System/Role')
},
{
path: '/system/setting/config',
name: 'config',
component: () => import('@/views/System/Role')
},
{
path: '/system/setting/dict',
name: 'dict',
component: () => import('@/views/System/Role')
},
{
path: '/system/setting/area',
name: 'area',
component: () => import('@/views/System/Role')
},
]
},
{
path: '/system/role',
name: 'role',
component: () => import('@/views/System/Role')
},
{
path: '/system/admin',
name: 'admin',
component: () => import('@/views/System/Role')
}
]
},
]
},
]
})
router.afterEach(() => {
store.commit('global/UpdateBasicLayoutSpinning', false);
});
export default router
import Vue from 'vue'
import Vuex from 'vuex'
import global from './modules/global'
import globalNav from './modules/global-nav'
import frontendOpenapi from './modules/frontend-openapi'
Vue.use(Vuex)
export default new Vuex.Store({
modules: {
frontend: {
namespaced: true,
modules: {
openapi: frontendOpenapi
}
},
global: {
namespaced: true,
...global,
modules: {
nav: globalNav
}
}
}
})
// import Cookies from 'js-cookie'
import api from '~a/index'
import { setStore, removeStore } from '~u/storage'
const state = {
mobile:'',
// checkMobile: {
// mobile: '',
// data: '',
// },
}
const actions = {
async ['logoutClean']({ commit, rootState: { route: { path } } }, config) {
const { data: { code, data } } = await api.post('/auth/clean', { ...config })
if (code === '0') {
commit('removeAuthToken')
}
},
}
const mutations = {
['receiveAuthToken'](state, payload) {
setStore('access_token', payload.access_token)
setStore('expires_in', payload.expires_in)
setStore('token_type', payload.token_type)
// Cookies.set('token', payload.access_token, { expires: (payload.expires_in / 86400) });
},
['removeAuthToken'](state) {
removeStore('access_token')
removeStore('expires_in')
removeStore('token_type')
// Cookies.remove('token')
},
['receiveMobile'](state, payload) {
state.mobile = payload;
},
}
const getters = {
['getMobile'](state) {
return state.mobile
},
}
export default {
namespaced: true,
state,
actions,
mutations,
getters
}
\ No newline at end of file
import { users } from '@/api/openapi'
const state = {
loading: false,
users: {
data: [],
pagination: {
showSizeChanger: true,
}
}
}
const actions = {
['getUsers']({ commit, state }, config) {
state.loading = true
return new Promise((resolve, reject) => {
users(config).then(response => {
// console.log(response);
commit('setUsers', {
...response,
config
})
state.loading = false
resolve()
}).catch(error => {
state.loading = false
reject(error)
})
})
},
}
const mutations = {
['setUsers'](state, { results, config }) {
state.users = {
data: results,
pagination: {
total: 200,
pageSize: config.results
}
}
// console.log(state.users.pagination)
}
}
const getters = {
['getUsers'](state) {
return state.users;
},
['loading'](state) {
return state.loading;
},
}
export default {
namespaced: true,
state,
actions,
mutations,
getters
}
\ No newline at end of file
import { menuNav } from '@/api/menu'
//从服务端获取
const mock = [
{
"id": "1044886626813353984",
"parentId": "0",
"name": "dashboard",
"path": '/dashboard',
"icon": 'dashboard',
"leaf": false,
"children": [{
"id": "1044886629921333248",
"parentId": "1044886626813353984",
"name": "analysis",
"path": '/dashboard/analysis',
"leaf": true,
"children": []
},{
"id": "1044886629921333248",
"parentId": "1044886626813353984",
"name": "workplace",
"path": '/dashboard/workplace',
"leaf": true,
"children": []
}]
},
{
"id": "1044886626813353984",
"parentId": "0",
"name": "system",
"path": '/system',
"icon": 'setting',
"leaf": false,
"children": [{
"id": "1044886629921333248",
"parentId": "1044886626813353984",
"name": "setting",
"path": "/system/setting",
"leaf": false,
"children": [{
"id": "1044886630026190848",
"parentId": "1044886629921333248",
"name": "menu",
"path": "/system/setting/menu",
"leaf": true,
"children": []
}, {
"id": "1044886630122659840",
"parentId": "1044886629921333248",
"name": "module",
"path": "/system/setting/module",
"leaf": true,
"children": []
}]
},{
"id": "1044886629921333248",
"parentId": "1044886626813353984",
"name": "role",
"path": "/system/role",
"leaf": true,
},{
"id": "1044886629921333248",
"parentId": "1044886626813353984",
"name": "admin",
"path": "/system/admin",
"leaf": true,
}]
}]
const state = {
loading: false,
menuNav: {
data: []
}
}
const actions = {
['getMenuNav']({ commit, state }, config) {
state.loading = true
return new Promise((resolve, reject) => {
menuNav().then(response => {
// console.log(mock);
commit('setMenuNav', mock)
state.loading = false
resolve()
}).catch(error => {
state.loading = false
reject(error)
})
})
},
}
const mutations = {
['setMenuNav'](state, payload) {
state.menuNav = {
data: payload
}
}
}
const getters = {
['getMenuNav'](state) {
return state.menuNav;
},
['loading'](state) {
return state.loading;
},
}
export default {
namespaced: true,
state,
actions,
mutations,
getters
}
\ No newline at end of file
const state = {
defaultSettings: {},
BasicLayoutSpinning: true,
ChangeLayoutCollapsed: false,
};
//dispatch
const actions = {
["defaultSettings"]({ commit, state }, config) {
commit("UpdateDefaultSettings", {
config,
});
// state.loading = true
// return new Promise((resolve, reject) => {
// users(config).then(response => {
// // console.log(response);
// commit('setUsers', {
// ...response,
// config
// })
// state.loading = false
// resolve()
// }).catch(error => {
// state.loading = false
// reject(error)
// })
// })
},
};
//commit
const mutations = {
["UpdateBasicLayoutSpinning"](state, payload) {
state.BasicLayoutSpinning = payload;
},
["UpdateChangeLayoutCollapsed"](state, payload) {
state.ChangeLayoutCollapsed = payload;
},
["UpdateDefaultSettings"](state, payload) {
let localSettingsKey = "_settings";
if (payload.config === true) {
window.localStorage.setItem(
localSettingsKey,
JSON.stringify(state.defaultSettings)
);
} else {
const settings = window.localStorage.getItem(localSettingsKey);
if (settings) {
state.defaultSettings = JSON.parse(settings);
} else {
state.defaultSettings = payload;
}
window.localStorage.setItem(
localSettingsKey,
JSON.stringify(state.defaultSettings)
);
}
},
};
const getters = {
["settings"](state) {
return state.defaultSettings;
},
["getBasicLayoutSpinning"](state) {
return state.BasicLayoutSpinning;
},
["getChangeLayoutCollapsed"](state) {
return state.ChangeLayoutCollapsed;
},
};
export default {
actions,
state,
mutations,
getters,
};
import Vue from 'vue'
export default new Vue()
\ No newline at end of file
import axios from 'axios'
import { message, Modal } from "ant-design-vue";
import qs from 'qs';
// create an axios instance
const service = axios.create({
// baseURL: process.env.BASE_API, // api的base_url
timeout: 5000, // request timeout
// responseType: "json",
// crossDomain: true,
// xhrFields: { withCredentials: true },
// headers: { 'X-Requested-With': 'XMLHttpRequest' },
// headers: {
// "Content-Type": "application/x-www-form-urlencoded;charset=utf-8"
// }
})
// request interceptor
service.interceptors.request.use(config => {
// Do something before request is sent
// if (getCookies(_CONST.TOKEN)) {
// config.headers['Authorization'] = 'Bearer ' + getCookies(_CONST.TOKEN) // 让每个请求携带token-- ['Authorization']为自定义key 请根据实际情况自行修改
// }
// 在发送请求之前做某件事
if (
config.method === "post" ||
config.method === "put" ||
config.method === "delete"
) {
// 序列化
config.data = qs.stringify(config.data);
}
// console.log('config', config);
return config
}, error => {
// Do something with request error
// console.log('error', error) // for debug
Promise.reject(error)
})
// respone interceptor
service.interceptors.response.use(
response => {
// message.success('数据通讯完成', 1);
const { data } = response;
if (data) {
return data
}
return Promise.reject(response);
},
error => {
/**
* 下面的注释为通过response自定义code来标示请求状态,当code返回如下情况为权限有问题,登出并返回到登录页
* 如通过xmlhttprequest 状态码标识 逻辑可写在下面error中
*/
// console.log('error', error)
const res = error.response;
// console.log(res);
if (res && res.data) {
const { code, error, error_description } = res.data;
// console.info(code)
// console.info(error)
// console.info(error_description)
// 100401:非法的token; 100412:其他客户端登录了; 100413:Token 过期了;
if (code === 100401 || code === 100412 || code === 100413) {
// MessageBox.confirm('操作失败,原因:' + error + ',可以取消继续留在该页面,或者重新登录', '操作提示', {
// confirmButtonText: '重新登录',
// cancelButtonText: '取消',
// type: 'warning'
// }).then(() => {
// removeCookies(_CONST.TOKEN);
// location.reload();// 为了重新实例化vue-router对象 避免bug
// })
} else {
let error;
if (!error) {
error = "请求失败,异常: " + res.statusText + ' [' + res.status + ']';
}
message.error(error, 5);
}
} else {
message.error("请求失败," + error, 5);
}
return Promise.reject(error)
})
export default service
\ No newline at end of file
@import 'ant-design-vue/lib/style/themes/default.less';
@import 'assets/utils.less';
.iconGroup {
i {
transition: color 0.32s;
color: @text-color-secondary;
cursor: pointer;
margin-left: 16px;
&:hover {
color: @text-color;
}
}
}
.rankingList {
margin: 25px 0 0;
padding: 0;
list-style: none;
li {
.clearfix();
margin-top: 16px;
display: flex;
align-items: center;
span {
color: @text-color;
font-size: 14px;
line-height: 22px;
}
.rankingItemNumber {
background-color: @background-color-base;
border-radius: 20px;
display: inline-block;
font-size: 12px;
font-weight: 600;
margin-right: 16px;
height: 20px;
line-height: 20px;
width: 20px;
text-align: center;
margin-top: 1.5px;
&.active {
background-color: #314659;
color: #fff;
}
}
.rankingItemTitle {
flex: 1;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
margin-right: 8px;
}
}
}
.salesExtra {
display: inline-block;
margin-right: 24px;
a {
color: @text-color;
margin-left: 24px;
&:hover {
color: @primary-color;
}
&.currentDate {
color: @primary-color;
}
}
}
.salesCard {
.salesBar {
padding: 0 0 32px 32px;
}
.salesRank {
padding: 0 32px 32px 72px;
}
:global {
.ant-tabs-bar {
padding-left: 16px;
.ant-tabs-nav .ant-tabs-tab {
padding-top: 16px;
padding-bottom: 14px;
line-height: 24px;
}
}
.ant-tabs-extra-content {
padding-right: 24px;
line-height: 55px;
}
.ant-card-head {
position: relative;
}
}
}
.salesCardExtra {
height: 68px;
}
.salesTypeRadio {
position: absolute;
left: 24px;
bottom: 15px;
}
.offlineCard {
:global {
.ant-tabs-ink-bar {
bottom: auto;
}
.ant-tabs-bar {
border-bottom: none;
}
.ant-tabs-nav-container-scrolling {
padding-left: 40px;
padding-right: 40px;
}
.ant-tabs-tab-prev-icon:before {
position: relative;
left: 6px;
}
.ant-tabs-tab-next-icon:before {
position: relative;
right: 6px;
}
.ant-tabs-tab-active h4 {
color: @primary-color;
}
}
}
.trendText {
margin-left: 8px;
color: @heading-color;
}
.mapChart {
padding-top: 24px;
height: 320px;
text-align: center;
img {
display: inline-block;
max-width: 100%;
max-height: 320px;
}
}
@media screen and (max-width: @screen-lg) {
.mapChart {
height: auto;
}
}
@media screen and (max-width: @screen-lg) {
.salesExtra {
display: none;
}
.rankingList {
li {
span:first-child {
margin-right: 8px;
}
}
}
}
@media screen and (max-width: @screen-md) {
.rankingTitle {
margin-top: 16px;
}
.salesCard .salesBar {
padding: 16px;
}
}
@media screen and (max-width: @screen-sm) {
.salesExtraWrap {
display: none;
}
.salesCard {
:global {
.ant-tabs-content {
padding-top: 30px;
}
}
}
}
<template>
<a-grid-content>
<a-row :gutter="24">
<a-col :xs="24" :sm="12" :md="12" :lg="12" :xl="6" :style="{marginBottom: '24px'}">
<a-chart-card :loading="loading" contentHeight="46px" :total="yuan(158829)" :title="$t('app.analysis.total-sales')">
<a-tooltip placement="top" slot="action">
<span slot="title">
<span v-html="$t('app.analysis.introduce')"></span>
</span>
<a-icon type="info-circle-o" />
</a-tooltip>
<a-field slot="footer" :label="$t('app.analysis.day-sales')" :value="yuan(6093)" />
<a-trend flag="up" :style="{marginRight: '16px'}">
{{$t('app.analysis.week')}}
<span class="trendText">12%</span>
</a-trend>
<a-trend flag="down">
{{$t('app.analysis.day')}}
<span class="trendText">11%</span>
</a-trend>
</a-chart-card>
</a-col>
<a-col :xs="24" :sm="12" :md="12" :lg="12" :xl="6" :style="{marginBottom: '24px'}">
<a-chart-card :loading="loading" contentHeight="46px" :total="numeral(36784).format('0,0')" :title="$t('app.analysis.visits')">
<a-tooltip placement="top" slot="action">
<span slot="title">
<span v-html="$t('app.analysis.introduce')"></span>
</span>
<a-icon type="info-circle-o" />
</a-tooltip>
<a-field slot="footer" :label="$t('app.analysis.day-visits')" :value="numeral(6678).format('0,0')" />
<!-- <a-mini-area color="#975FE4" /> -->
<a-trend flag="up" :style="{marginRight: '16px'}">
{{$t('app.analysis.week')}}
<span class="trendText">6%</span>
</a-trend>
<a-trend flag="down">
{{$t('app.analysis.day')}}
<span class="trendText">2%</span>
</a-trend>
</a-chart-card>
</a-col>
<a-col :xs="24" :sm="12" :md="12" :lg="12" :xl="6" :style="{marginBottom: '24px'}">
<a-chart-card :loading="loading" contentHeight="46px" :total="numeral(6560).format('0,0')" :title="$t('app.analysis.payments')">
<a-tooltip placement="top" slot="action">
<span slot="title">
<span v-html="$t('app.analysis.introduce')"></span>
</span>
<a-icon type="info-circle-o" />
</a-tooltip>
<!-- <a-mini-bar /> -->
<a-mini-progress :percent="55" :strokeWidth="8" :target="80" color="#975FE4" />
<a-field slot="footer" :label="$t('app.analysis.conversion-rate')" value="30%" />
</a-chart-card>
</a-col>
<a-col :xs="24" :sm="12" :md="12" :lg="12" :xl="6" :style="{marginBottom: '24px'}">
<a-chart-card :loading="loading" contentHeight="46px" total="78%" :title="$t('app.analysis.operational-effect')">
<a-tooltip placement="top" slot="action">
<span slot="title">
<span v-html="$t('app.analysis.introduce')"></span>
</span>
<a-icon type="info-circle-o" />
</a-tooltip>
<a-mini-progress :percent="30" :strokeWidth="8" :target="80" color="#13C2C2" />
<div :style="{whiteSpace:'nowrap',overflow:'hidden'}" slot="footer">
<!-- <a-trend flag="up" :style="{marginRight: '16px'}">
{{$t('app.analysis.week')}}<span class="trendText">12%</span>
</a-trend> -->
<a-trend flag="up">
{{$t('app.analysis.day')}}<span class="trendText">28%</span>
</a-trend>
</div>
</a-chart-card>
</a-col>
</a-row>
<a-card :loading="loading" :bordered="false" :bodyStyle="{padding:'0px'}">
<div class="salesCard">
<a-tabs size="large" :tabBarStyle="{marginBottom: '24px',paddingLeft:'16px'}">
<div slot="tabBarExtraContent" class="salesExtraWrap">
<div class="salesExtra">
<a >
{{$t('app.analysis.all-day')}}
</a>
<a class="currentDate">
{{$t('app.analysis.all-week')}}
</a>
<a >
{{$t('app.analysis.all-month')}}
</a>
<a >
{{$t('app.analysis.all-year')}}
</a>
</div>
<a-range-picker style="width:256px;"/>
</div>
<a-tab-pane key="sales" :tab="$t('app.analysis.sales')">
<a-row>
<a-col :xl="16" :lg="12" :md="12" :sm="24" :xs="24">
<div class="salesBar">
<!-- <h4>活动实时交易情况</h4> -->
<a-row>
<a-col :md="6" :sm="12" :xs="24">
<a-number-info
subTitle="今日交易总额"
suffix="万元"
:total="numeral(12233).format('0,0')"
/>
</a-col>
<a-col :md="6" :sm="12" :xs="24">
<a-number-info subTitle="销售目标完成率" total="92%" />
</a-col>
<a-col :md="6" :sm="12" :xs="24">
<!-- <NumberInfo subTitle="活动剩余时间" total={<CountDown target={targetTime} />} /> -->
<a-number-info subTitle="活动剩余时间" total="01:04:53"/>
</a-col>
<a-col :md="6" :sm="12" :xs="24">
<a-number-info subTitle="每秒交易总额"
suffix="元"
:total="numeral(21234).format('0,0')"/>
</a-col>
</a-row>
<div class="mapChart">
<a-tooltip title="等待后期实现">
<img
src="https://gw.alipayobjects.com/zos/rmsportal/HBWnDEUXCnGnGrRfrpKa.png"
alt="map"
/>
</a-tooltip>
</div>
</div>
</a-col>
<a-col :xl="8" :lg="12" :md="12" :sm="24" :xs="24">
<div class="salesRank">
<h4 class="rankingTitle">
{{$t('app.analysis.sales-ranking')}}
</h4>
<ul class="rankingList">
<li v-for="(item,i) in 10" :key="i">
<span class="rankingItemNumber " :class="{'active':i<3}">
{{i + 1}}
</span>
<span class="rankingItemTitle">
中国 🇨🇳 加油 ⛽️ ⛽ ️⛽️
</span>
<span class="rankingItemValue">
0,0
</span>
</li>
</ul>
</div>
</a-col>
</a-row>
</a-tab-pane>
<a-tab-pane key="visits" :tab="$t('app.analysis.visits')">
</a-tab-pane>
</a-tabs>
</div>
</a-card>
<!-- <a-row :gutter="24">8</a-row> -->
</a-grid-content>
</template>
<script>
import {
Card,
Row,
Col,
Tooltip,
Icon,
Tabs,
DatePicker
} from "ant-design-vue";
import GridContent from "@/components/PageHeaderWrapper/GridContent";
import Trend from "@/components/Trend";
import NumberInfo from "@/components/NumberInfo";
import {
ChartCard,
Field,
yuan,
MiniProgress
// MiniBar,
// MiniArea
} from "@/components/Charts";
import numeral from "numeral";
// import moment from 'moment';
// import 'moment/locale/zh-cn';
// moment.locale('zh-cn');
export default {
data: () => ({
loading: false
}),
components: {
AGridContent: GridContent,
ACard: Card,
ARow: Row,
ACol: Col,
AChartCard: ChartCard,
AField: Field,
ATooltip: Tooltip,
AIcon: Icon,
ATrend: Trend,
AMiniProgress: MiniProgress,
// AMiniBar: MiniBar,
// AMiniArea: MiniArea,
ATabs: Tabs,
ATabPane: Tabs.TabPane,
ARangePicker: DatePicker.RangePicker,
ANumberInfo: NumberInfo
},
methods: {
yuan,
numeral
}
};
</script>
<style lang="less">
@import url("./Analysis.less");
</style>
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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