This commit is contained in:
weicw
2021-07-29 16:17:26 +08:00
commit a6eb6f83d1
127 changed files with 60792 additions and 0 deletions

31
src/App.vue Normal file
View File

@@ -0,0 +1,31 @@
<template>
<a-config-provider :locale="locale">
<router-view/>
</a-config-provider>
</template>
<script>
import zh_CN from 'ele-admin-pro/packages/lang/zh_CN';
import zh_TW from 'ele-admin-pro/packages/lang/zh_TW';
import en from 'ele-admin-pro/packages/lang/en_US';
const languages = {zh_CN, zh_TW, en};
export default {
data() {
return {
locale: languages[this.$i18n.locale]
};
},
computed: {
language() {
return this.$i18n.locale;
}
},
watch: {
language() {
this.locale = languages[this.language];
}
}
};
</script>

BIN
src/assets/bg-login.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 383 KiB

15
src/assets/logo.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 30 KiB

View File

@@ -0,0 +1,257 @@
<!-- tinymce富文本编辑器组件 license by http://eleadmin.com -->
<template>
<editor
:init="config"
:disabled="disabled"
:model-value="value"
@update:model-value="updateValue"/>
</template>
<script>
const BASE_URL = process.env.BASE_URL;
import tinymce from 'tinymce/tinymce';
import 'tinymce/icons/default';
import 'tinymce/themes/silver';
import 'tinymce/plugins/code';
import 'tinymce/plugins/print';
import 'tinymce/plugins/preview';
import 'tinymce/plugins/fullscreen';
import 'tinymce/plugins/paste';
import 'tinymce/plugins/searchreplace';
import 'tinymce/plugins/save';
import 'tinymce/plugins/autosave';
import 'tinymce/plugins/link';
import 'tinymce/plugins/autolink';
import 'tinymce/plugins/image';
import 'tinymce/plugins/imagetools';
import 'tinymce/plugins/media';
import 'tinymce/plugins/table';
import 'tinymce/plugins/codesample';
import 'tinymce/plugins/lists';
import 'tinymce/plugins/advlist';
import 'tinymce/plugins/hr';
import 'tinymce/plugins/charmap';
import 'tinymce/plugins/emoticons';
import 'tinymce/plugins/anchor';
import 'tinymce/plugins/directionality';
import 'tinymce/plugins/pagebreak';
import 'tinymce/plugins/quickbars';
import 'tinymce/plugins/nonbreaking';
import 'tinymce/plugins/visualblocks';
import 'tinymce/plugins/visualchars';
import 'tinymce/plugins/wordcount';
import 'tinymce/plugins/emoticons/js/emojis';
import Editor from '@tinymce/tinymce-vue';
// 默认配置
const DEFAULT_CONFIG = {
height: 300,
branding: false,
skin_url: BASE_URL + 'tinymce/skins/ui/oxide',
content_css: BASE_URL + 'tinymce/skins/content/default/content.min.css',
language_url: BASE_URL + 'tinymce/langs/zh_CN.js',
language: 'zh_CN',
plugins: [
'code',
'print',
'preview',
'fullscreen',
'paste',
'searchreplace',
'save',
'autosave',
'link',
'autolink',
'image',
'imagetools',
'media',
'table',
'codesample',
'lists',
'advlist',
'hr',
'charmap',
'emoticons',
'anchor',
'directionality',
'pagebreak',
'quickbars',
'nonbreaking',
'visualblocks',
'visualchars',
'wordcount'
].join(' '),
toolbar: [
'fullscreen',
'preview',
'code',
'|',
'undo',
'redo',
'|',
'forecolor',
'backcolor',
'|',
'bold',
'italic',
'underline',
'strikethrough',
'|',
'alignleft',
'aligncenter',
'alignright',
'alignjustify',
'|',
'outdent',
'indent',
'|',
'numlist',
'bullist',
'|',
'formatselect',
'fontselect',
'fontsizeselect',
'|',
'link',
'image',
'media',
'emoticons',
'charmap',
'anchor',
'pagebreak',
'codesample',
'|',
'ltr',
'rtl'
].join(' '),
draggable_modal: true,
toolbar_mode: 'sliding',
images_upload_handler: (blobInfo, success) => {
success('data:image/jpeg;base64,' + blobInfo.base64());
},
file_picker_types: 'media',
file_picker_callback: () => {
}
};
// 暗黑主题
const DARK_CONFIG = {
skin_url: BASE_URL + 'tinymce/skins/ui/oxide-dark',
content_css: BASE_URL + 'tinymce/skins/content/dark/content.min.css'
};
export default {
name: 'TinymceEditor',
components: {Editor},
emits: ['update:value'],
props: {
// 值(v-model)
value: String,
// 编辑器配置
init: Object,
// 是否禁用
disabled: Boolean,
// 自动跟随框架主题
autoTheme: {
type: Boolean,
default: true
},
// 是否使用暗黑主题
darkTheme: Boolean
},
data() {
const dark = this.autoTheme ? this.$store.state.theme.darkMode : this.darkTheme;
return {
// 编辑器配置
config: Object.assign({}, DEFAULT_CONFIG, dark ? DARK_CONFIG : {}, this.init)
};
},
computed: {
// 是否是暗黑模式
darkMode() {
try {
return this.$store.state.theme.darkMode;
} catch (e) {
return false;
}
}
},
created() {
tinymce.init({});
},
methods: {
/* 更新value */
updateValue(value) {
this.$emit('update:value', value);
},
/* 切换编辑器主题 */
changeTheme(dark) {
document.head.querySelectorAll('[id^="mce-"]').forEach((elem) => {
let href = elem.getAttribute('href');
if (href.indexOf('/oxide-dark/') !== -1) {
if (!dark) {
href = href.replace('/oxide-dark/', '/oxide/');
elem.setAttribute('href', href);
}
} else {
if (dark) {
href = href.replace('/oxide/', '/oxide-dark/');
elem.setAttribute('href', href);
}
}
});
this.changeContentTheme(dark);
},
/* 切换编辑器内容区的主题 */
changeContentTheme(dark) {
document.body.querySelectorAll('iframe[id^="tiny-vue_"]').forEach((frame) => {
const win = frame.contentWindow;
if (win) {
const doc = win.document;
if (doc) {
[].forEach.call(doc.head.querySelectorAll('[id^="mce-"]'), (elem) => {
let href = elem.getAttribute('href');
if (href.indexOf('/skins/ui/') !== -1) {
if (href.indexOf('/oxide-dark/') !== -1) {
if (!dark) {
href = href.replace('/oxide-dark/', '/oxide/');
elem.setAttribute('href', href);
}
} else {
if (dark) {
href = href.replace('/oxide/', '/oxide-dark/');
elem.setAttribute('href', href);
}
}
} else if (href.indexOf('/skins/content/') !== -1) {
if (href.indexOf('/dark/') !== -1) {
if (!dark) {
href = href.replace('/dark/', '/default/');
elem.setAttribute('href', href);
}
} else {
if (dark) {
href = href.replace('/default/', '/dark/');
elem.setAttribute('href', href);
}
}
}
});
}
}
});
}
},
watch: {
darkMode() {
if (this.autoTheme) {
this.changeTheme(this.darkMode);
}
}
}
}
</script>
<style>
body .tox-tinymce-aux {
z-index: 19892000;
}
</style>

View File

@@ -0,0 +1,69 @@
/**
* axios配置
*/
import axios from 'axios';
import store from '../store';
import router from '../router';
import setting from './setting';
import {Modal} from 'ant-design-vue';
// 设置统一的url
axios.defaults.baseURL = process.env.VUE_APP_API_BASE_URL;
/* 请求拦截器 */
axios.interceptors.request.use((config) => {
// 添加token到header
const token = setting.takeToken();
if (token) {
config.headers[setting.tokenHeaderName] = token;
}
return config;
}, (error) => {
return Promise.reject(error);
});
/* 响应拦截器 */
axios.interceptors.response.use((res) => {
// 登录过期处理
if (res.data.code === 401) {
if (res.config.url === setting.menuUrl) {
goLogin();
} else {
Modal.destroyAll();
Modal.info({
title: '系统提示',
content: '登录状态已过期, 请退出重新登录!',
okText: '重新登录',
onOk: () => {
goLogin(true);
}
});
}
return Promise.reject(new Error(res.data.msg));
}
// token自动续期
const access_token = res.headers[setting.tokenHeaderName];
if (access_token) {
setting.cacheToken(access_token);
}
return res;
}, (error) => {
return Promise.reject(error);
});
/**
* 跳转到登录页面
*/
function goLogin(reload) {
store.dispatch('user/removeToken').then(() => {
if (reload) {
location.replace('/login'); // 这样跳转避免再次登录重复注册动态路由
} else {
const path = router.currentRoute.path;
return router.push({
path: '/login',
query: path && path !== '/' ? {form: path} : null
});
}
});
}

141
src/config/setting.js Normal file
View File

@@ -0,0 +1,141 @@
/**
* 框架全局配置
*/
export default {
// 不显示侧栏的路由
hideSidebars: [],
// 不显示全局页脚的路由
hideFooters: ['/system/dictionary', '/system/organization', '/form/advanced', '/example/choose'],
// 页签可重复打开的路由
repeatableTabs: ['/system/user/info'],
// 不需要登录的路由
whiteList: ['/login', '/forget'],
// 菜单数据接口
menuUrl: '/main/menu?type=pro',
// 自定义解析菜单接口数据
parseMenu: null,
// 自定义解析菜单接口单个数据格式
parseMenuItem: null,
// 直接指定菜单数据
menus: null,
// 用户信息接口
userUrl: '/main/user',
// 自定义解析接口用户信息
parseUser(res) {
// code为0是成功, 不一样可以处理如: {code: res.code === 200 ? 0 : res.code, msg: res.message}
let result = {code: res.code, msg: res.msg};
if (res.data) {
result.data = Object.assign({}, res.data, {
// 姓名和头像会显示在顶栏, 字段不一样可以在这处理, 如:
//avatar: res.data.avatarUrl,
//nickname: res.data.userName,
// 角色和权限信息, 需要为string数组类型
roles: res.data.roles ? res.data.roles.map(d => d.roleCode) : [],
authorities: res.data.authorities ? res.data.authorities.map(d => d.authority) : []
});
}
return result;
},
// token传递的header名称
tokenHeaderName: 'Authorization',
// token存储的名称
tokenStoreName: 'access_token',
// 用户信息存储的名称
userStoreName: 'user',
// 主题配置存储的名称
themeStoreName: 'theme',
// 首页标题, 为空会自动获取
homeTitle: '主页',
// 首页路径, 为空会自动获取
homePath: null,
// 顶栏是否显示主题设置按钮
showSetting: true,
// 开启多页签是否缓存组件
tabKeepAlive: true,
// 是否折叠侧边栏
collapse: false,
// 侧边栏风格: light(亮色), dark(暗色)
sideStyle: 'dark',
// 顶栏风格: light(亮色), dark(暗色), primary(主色)
headStyle: 'light',
// 标签页风格: default(默认), dot(圆点), card(卡片)
tabStyle: 'default',
// 布局风格: side(默认), top(顶栏菜单), mix(混合菜单)
layoutStyle: 'side',
// 侧边栏菜单风格: default(默认), mix(双排菜单)
sideMenuStyle: 'default',
// 是否固定侧栏
fixedSidebar: true,
// 是否固定顶栏
fixedHeader: false,
// 是否固定主体
fixedBody: true,
// logo是否自适应宽度
logoAutoSize: false,
// 内容区域宽度是否铺满
bodyFull: true,
// 是否开启多标签
showTabs: true,
// 侧栏是否多彩图标
colorfulIcon: false,
// 侧边栏是否只保持一个子菜单展开
sideUniqueOpen: true,
// 是否开启页脚
showFooter: true,
// 是否是色弱模式
weakMode: false,
// 是否是暗黑模式
darkMode: false,
// 默认主题色
color: null,
/**
* 获取缓存的token的方法
* @returns {string}
*/
takeToken() {
let token = localStorage.getItem(this.tokenStoreName);
if (!token) {
token = sessionStorage.getItem(this.tokenStoreName);
}
return token;
},
/**
* 缓存token的方法
* @param token
* @param remember 是否永久存储
*/
cacheToken(token, remember) {
localStorage.removeItem(this.tokenStoreName);
sessionStorage.removeItem(this.tokenStoreName);
if (token) {
if (remember) {
localStorage.setItem(this.tokenStoreName, token);
} else {
sessionStorage.setItem(this.tokenStoreName, token);
}
}
},
/**
* 获取缓存的用户信息
* @returns {object}
*/
takeUser() {
try {
return JSON.parse(localStorage.getItem(this.userStoreName)) || {};
} catch (e) {
console.error(e);
}
return {};
},
/**
* 缓存用户信息
* @param user
*/
cacheUser(user) {
if (user) {
localStorage.setItem(this.userStoreName, JSON.stringify(user));
} else {
localStorage.removeItem(this.userStoreName);
}
}
}

106
src/lang/en.js Normal file
View File

@@ -0,0 +1,106 @@
/**
* 英语
*/
export default {
route: {
dashboard: {
_name: 'Dashboard',
workplace: {_name: 'Workplace'},
analysis: {_name: 'Analysis'},
monitor: {_name: 'Monitor'}
},
system: {
_name: 'System',
user: {
_name: 'User',
info: {_name: ''}
},
role: {_name: 'Role'},
menu: {_name: 'Menu'},
dictionary: {_name: 'Dictionary'},
organization: {_name: 'Organization'},
loginRecord: {_name: 'Login Record'},
operRecord: {_name: 'Operation Log'}
},
form: {
_name: 'Form',
basic: {_name: 'Basic Form'},
advanced: {_name: 'Advanced Form'},
step: {_name: 'Step Form'}
},
list: {
_name: 'List',
basic: {_name: 'Basic List'},
advanced: {_name: 'Advanced List'},
card: {
_name: 'Card List',
project: {_name: 'Project'},
application: {_name: 'Application'},
article: {_name: 'Article'}
}
},
result: {
_name: 'Result',
success: {_name: 'Success'},
fail: {_name: 'Fail'}
},
exception: {
_name: 'Exception',
'403': {_name: '403'},
'404': {_name: '404'},
'500': {_name: '500'}
},
user: {
_name: 'User',
profile: {_name: 'Profile'},
message: {_name: 'Message'}
},
extension: {
_name: 'Extension',
icon: {_name: 'Icon'},
file: {_name: 'File'},
printer: {_name: 'Printer'},
excel: {_name: 'Excel'},
dragsort: {_name: 'Drag Sort'},
map: {_name: 'Map'},
player: {_name: 'Player'},
editor: {_name: 'Editor'},
more: {_name: 'More'}
},
example: {
_name: 'Example',
document: {_name: 'Document'},
choose: {_name: 'Choose'},
eleadmin: {_name: 'IFrame'}
},
'https://eleadminCom/goods/9': {_name: 'Authorization'}
},
layout: {
home: 'Home',
header: {
profile: 'Profile',
password: 'Password',
logout: 'SignOut'
},
footer: {
website: 'Website',
document: 'Document',
authorization: 'Authorization',
copyright: 'Copyright © 2021 南宁网宿信息科技有限公司'
},
logout: {
title: 'Confirm',
message: 'Are you sure you want to logout?'
}
},
login: {
title: 'User Login',
username: 'please input username',
password: 'please input password',
code: 'please input code',
remember: 'remember',
forget: 'forget',
login: 'login',
loading: 'loading'
}
}

22
src/lang/index.js Normal file
View File

@@ -0,0 +1,22 @@
/**
* 国际化配置
*/
import {createI18n} from 'vue-i18n';
import enLocale from './en';
import zhCNLocale from './zh_CN';
import zhTWLocale from './zh_TW';
const messages = {
en: enLocale,
zh_CN: zhCNLocale,
zh_TW: zhTWLocale
};
const i18n = createI18n({
messages: messages,
silentTranslationWarn: true,
// 默认语言
locale: localStorage.getItem('i18n-lang') || 'zh_CN'
});
export default i18n;

106
src/lang/zh_CN.js Normal file
View File

@@ -0,0 +1,106 @@
/**
* 简体中文
*/
export default {
route: {
dashboard: {
_name: 'Dashboard',
workplace: {_name: '工作台'},
analysis: {_name: '分析页'},
monitor: {_name: '监控页'}
},
system: {
_name: '系统管理',
user: {
_name: '用户管理',
info: {_name: ''}
},
role: {_name: '角色管理'},
menu: {_name: '菜单管理'},
dictionary: {_name: '字典管理'},
organization: {_name: '机构管理'},
loginRecord: {_name: '登录日志'},
operRecord: {_name: '操作日志'}
},
form: {
_name: '表单页面',
basic: {_name: '基础表单'},
advanced: {_name: '复杂表单'},
step: {_name: '分步表单'}
},
list: {
_name: '列表页面',
basic: {_name: '基础列表'},
advanced: {_name: '复杂列表'},
card: {
_name: '卡片列表',
project: {_name: '项目列表'},
application: {_name: '应用列表'},
article: {_name: '文章列表'}
}
},
result: {
_name: '结果页面',
success: {_name: '成功页'},
fail: {_name: '失败页'}
},
exception: {
_name: '异常页面',
'403': {_name: '403'},
'404': {_name: '404'},
'500': {_name: '500'}
},
user: {
_name: '个人中心',
profile: {_name: '个人资料'},
message: {_name: '我的消息'}
},
extension: {
_name: '扩展组件',
icon: {_name: '字体图标'},
file: {_name: '文件列表'},
printer: {_name: '打印插件'},
excel: {_name: 'excel插件'},
dragsort: {_name: '拖拽排序'},
map: {_name: '地图组件'},
player: {_name: '视频播放'},
editor: {_name: '富文本框'},
more: {_name: '更多组件'}
},
example: {
_name: '经典实例',
document: {_name: '案卷调整'},
choose: {_name: '批量选择'},
eleadmin: {_name: '内嵌页面'}
},
'https://eleadminCom/goods/9': {_name: '获取授权'}
},
layout: {
home: '主页',
header: {
profile: '个人中心',
password: '修改密码',
logout: '退出登录'
},
footer: {
website: '官网',
document: '文档',
authorization: '授权',
copyright: 'Copyright © 2021 南宁网宿信息科技有限公司'
},
logout: {
title: '提示',
message: '确定要退出登录吗?'
}
},
login: {
title: '用户登录',
username: '请输入登录账号',
password: '请输入登录密码',
code: '请输入验证码',
remember: '记住密码',
forget: '忘记密码',
login: '登录',
loading: '登录中'
}
}

106
src/lang/zh_TW.js Normal file
View File

@@ -0,0 +1,106 @@
/**
* 繁体中文
*/
export default {
route: {
dashboard: {
_name: 'Dashboard',
workplace: {_name: '工作臺'},
analysis: {_name: '分析頁'},
monitor: {_name: '監控頁'}
},
system: {
_name: '系統管理',
user: {
_name: '用戶管理',
info: {_name: ''}
},
role: {_name: '角色管理'},
menu: {_name: '選單管理'},
dictionary: {_name: '字典管理'},
organization: {_name: '機构管理'},
loginRecord: {_name: '登入日誌'},
operRecord: {_name: '操作日誌'}
},
form: {
_name: '表單頁面',
basic: {_name: '基礎表單'},
advanced: {_name: '複雜表單'},
step: {_name: '分步表單'}
},
list: {
_name: '清單頁面',
basic: {_name: '基礎清單'},
advanced: {_name: '複雜清單'},
card: {
_name: '卡片清單',
project: {_name: '項目清單'},
application: {_name: '應用清單'},
article: {_name: '文章清單'}
}
},
result: {
_name: '結果頁面',
success: {_name: '成功頁'},
fail: {_name: '失敗頁'}
},
exception: {
_name: '异常頁面',
'403': {_name: '403'},
'404': {_name: '404'},
'500': {_name: '500'}
},
user: {
_name: '個人中心',
profile: {_name: '個人資料'},
message: {_name: '我的消息'}
},
extension: {
_name: '擴展組件',
icon: {_name: '字體圖標'},
file: {_name: '檔案清單'},
printer: {_name: '列印挿件'},
excel: {_name: 'excel挿件'},
dragsort: {_name: '拖拽排序'},
map: {_name: '地圖組件'},
player: {_name: '視頻播放'},
editor: {_name: '富文本框'},
more: {_name: '更多組件'}
},
example: {
_name: '經典實例',
document: {_name: '案卷調整'},
choose: {_name: '批量選擇'},
eleadmin: {_name: '內嵌頁面'}
},
'https://eleadminCom/goods/9': {_name: '獲取授權'}
},
layout: {
home: '主頁',
header: {
profile: '個人中心',
password: '修改密碼',
logout: '安全登出'
},
footer: {
website: '官網',
document: '檔案',
authorization: '授權',
copyright: 'Copyright © 2021 南宁网宿信息科技有限公司'
},
logout: {
title: '詢問',
message: '確定要登出嗎?'
}
},
login: {
title: '用戶登錄',
username: '請輸入登入帳號',
password: '請輸入登入密碼',
code: '請輸入驗證碼',
remember: '記住密碼',
forget: '忘記密碼',
login: '登入',
loading: '登入中'
}
}

34
src/layout/footer.vue Normal file
View File

@@ -0,0 +1,34 @@
<!-- 页脚 -->
<template>
<div class="ele-text-center" style="padding: 16px 0;">
<!-- <a-space size="large">
<a
class="ele-text-secondary"
href="https://eleadmin.com"
target="_blank">
{{ $t('layout.footer.website') }}
</a>
<a
class="ele-text-secondary"
href="https://eleadmin.com/doc/eleadminpro/"
target="_blank">
{{ $t('layout.footer.document') }}
</a>
<a
class="ele-text-secondary"
href="https://eleadmin.com/goods/9"
target="_blank">
{{ $t('layout.footer.authorization') }}
</a>
</a-space> -->
<div class="ele-text-secondary" style="margin-top: 8px;">
{{ $t('layout.footer.copyright') }}
</div>
</div>
</template>
<script>
export default {
name: 'EleFooter'
}
</script>

163
src/layout/header-right.vue Normal file
View File

@@ -0,0 +1,163 @@
<!-- 顶栏右侧区域按钮 -->
<template>
<div class="ele-admin-header-tool">
<div class="ele-admin-header-tool-item hidden-sm-and-down" @click="changeFullscreen">
<fullscreen-exit-outlined v-if="fullscreen"/>
<fullscreen-outlined v-else/>
</div>
<!-- 语言切换 -->
<!-- <div class="ele-admin-header-tool-item">
<a-dropdown placement="bottomCenter" :overlay-style="{minWidth: '120px', paddingTop: '17px'}">
<global-outlined style="transform: scale(1.08);"/>
<template #overlay>
<a-menu :selected-keys="language" @click="changeLanguage">
<a-menu-item key="en">English</a-menu-item>
<a-menu-item key="zh_CN">简体中文</a-menu-item>
<a-menu-item key="zh_TW">繁體中文</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</div> -->
<!-- 消息通知 -->
<!-- <div class="ele-admin-header-tool-item">
<ele-notice/>
</div> -->
<!-- 用户信息 -->
<div class="ele-admin-header-tool-item">
<a-dropdown placement="bottomCenter" :overlay-style="{minWidth: '120px'}">
<div class="ele-admin-header-avatar">
<a-avatar :src="loginUser.avatar"/>
<span>{{ loginUser.nickname }}&nbsp;</span>
<down-outlined/>
</div>
<template #overlay>
<a-menu @click="onUserDropClick">
<a-menu-item key="profile">
<div class="ele-cell">
<user-outlined/>
<div class="ele-cell-content">{{ $t('layout.header.profile') }}</div>
</div>
</a-menu-item>
<a-menu-item key="password">
<div class="ele-cell">
<key-outlined/>
<div class="ele-cell-content">{{ $t('layout.header.password') }}</div>
</div>
</a-menu-item>
<a-menu-divider/>
<a-menu-item key="logout">
<div class="ele-cell">
<logout-outlined/>
<div class="ele-cell-content">{{ $t('layout.header.logout') }}</div>
</div>
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</div>
<!-- 主题设置 -->
<div v-if="showSetting" class="ele-admin-header-tool-item" @click="openSetting">
<MoreOutlined/>
</div>
</div>
</template>
<script>
import {createVNode} from 'vue';
import {
DownOutlined,
MoreOutlined,
UserOutlined,
KeyOutlined,
LogoutOutlined,
ExclamationCircleOutlined,
FullscreenOutlined,
FullscreenExitOutlined,
// GlobalOutlined
} from '@ant-design/icons-vue';
import {toggleFullscreen, isFullscreen} from 'ele-admin-pro/packages/util.js';
// import EleNotice from './notice';
export default {
name: 'EleHeaderRight',
components: {
DownOutlined,
MoreOutlined,
UserOutlined,
KeyOutlined,
LogoutOutlined,
FullscreenOutlined,
FullscreenExitOutlined,
// GlobalOutlined,
// EleNotice
},
emits: ['item-click', 'change-language'],
props: {
// 是否显示打开设置抽屉按钮
showSetting: {
type: Boolean,
default: true
}
},
computed: {
// 当前登录用户信息
loginUser() {
return this.$store.state.user.user;
},
// 当前语言
language() {
return [this.$i18n.locale];
}
},
data() {
return {
// 是否全屏状态
fullscreen: false
};
},
methods: {
/* 个人信息下拉菜单点击 */
onUserDropClick({key}) {
if (key === 'logout') {
// 退出登录
this.$confirm({
title: this.$t('layout.logout.title'),
content: this.$t('layout.logout.message'),
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
// 清除缓存的token
this.$store.dispatch('user/removeToken').then(() => {
location.replace('/login'); // 这样跳转避免再次登录重复注册动态路由
});
}
});
} else if (key === 'profile') {
this.$router.push('/user/profile');
} else if (key === 'password') {
this.$emit('item-click', 'password');
}
},
/* 打开设置抽屉 */
openSetting() {
this.$emit('item-click', 'setting');
},
/* 全屏切换 */
changeFullscreen() {
try {
this.fullscreen = toggleFullscreen();
} catch (e) {
this.$message.error('您的浏览器不支持全屏模式');
}
},
/* 检查全屏状态 */
checkFullscreen() {
this.fullscreen = isFullscreen();
},
/* 切换语言 */
changeLanguage({key}) {
this.$emit('change-language', key);
}
}
}
</script>

240
src/layout/index.vue Normal file
View File

@@ -0,0 +1,240 @@
<!-- 框架布局 -->
<template>
<ele-pro-layout
ref="layout"
:i18n="i18n"
:menus="user.menus"
:home-title="homeTitle"
:project-name="projectName"
:show-content="showContent"
v-model:show-setting="showSetting"
:need-setting="setting.needSetting"
:hide-footers="setting.hideFooters"
:hide-sidebars="setting.hideSidebars"
:repeatable-tabs="setting.repeatableTabs"
:tabs="theme.tabs"
:color="theme.color"
:collapse="theme.collapse"
:head-style="theme.headStyle"
:side-style="theme.sideStyle"
:layout-style="theme.layoutStyle"
:side-menu-style="theme.sideMenuStyle"
:fixed-body="theme.fixedBody"
:fixed-header="theme.fixedHeader"
:fixed-sidebar="theme.fixedSidebar"
:body-full="theme.bodyFull"
:show-footer="theme.showFooter"
:colorful-icon="theme.colorfulIcon"
:logo-auto-size="theme.logoAutoSize"
:side-unique-open="theme.sideUniqueOpen"
:show-tabs="theme.showTabs"
:tab-style="theme.tabStyle"
:dark-mode="theme.darkMode"
:weak-mode="theme.weakMode"
@logo-click="onLogoClick"
@reload-page="reloadPage"
@update-screen="updateScreen"
@update-collapse="updateCollapse"
@tab-add="tabAdd"
@tab-remove="tabRemove"
@tab-remove-all="tabRemoveAll"
@tab-remove-left="tabRemoveLeft"
@tab-remove-right="tabRemoveRight"
@tab-remove-other="tabRemoveOther"
@change-style="changeStyle"
@change-color="changeColor"
@change-dark-mode="changeDarkMode"
@change-weak-mode="changeWeakMode"
@set-home-components="setHomeComponents">
<!-- logo图标 -->
<template #logo>
<img src="@/assets/logo.svg" alt="logo"/>
</template>
<!-- 顶栏右侧区域 -->
<template #right>
<ele-header-right
ref="header"
:show-setting="setting.needSetting"
@change-language="changeLanguage"
@item-click="onItemClick"/>
</template>
<!-- 全局页脚 -->
<template #footer>
<ele-footer/>
</template>
<!-- 修改密码弹窗 -->
<ele-password v-model:visible="showPassword"/>
</ele-pro-layout>
</template>
<script>
import {mapGetters} from 'vuex';
import EleHeaderRight from './header-right';
import ElePassword from './password';
import EleFooter from './footer';
import setting from '@/config/setting';
import {
reloadPageTab,
addPageTab,
removePageTab,
removeAllPageTab,
removeLeftPageTab,
removeRightPageTab,
removeOtherPageTab
} from '@/utils/page-tab-util';
export default {
name: 'EleLayout',
components: {
EleHeaderRight,
ElePassword,
EleFooter
},
computed: {
// 主页标题, 移除国际化上面template中使用:home-title="setting.homeTitle"
homeTitle() {
return this.$t('layout.home');
},
...mapGetters(['theme', 'user'])
},
data() {
return {
// 全局配置
setting: setting,
// 是否显示修改密码弹窗
showPassword: false,
// 是否显示主题设置抽屉
showSetting: false,
// 是否显示主体部分
showContent: true,
// 项目名
projectName: process.env.VUE_APP_NAME
};
},
created() {
// 获取用户信息
this.getUserInfo();
},
methods: {
/* 获取当前用户信息 */
getUserInfo() {
if (setting.userUrl) {
this.$http.get(setting.userUrl).then((res) => {
const result = setting.parseUser ? setting.parseUser(res.data) : res.data;
if (result.code === 0) {
const user = result.data;
this.$store.dispatch('user/setUser', user);
this.$store.dispatch('user/setRoles', user ? user.roles : null);
this.$store.dispatch('user/setAuthorities', user ? user.authorities : null);
} else if (result.msg) {
this.$message.error(result.msg);
}
// 在用户权限信息请求完成后再渲染主体部分, 以免权限控制指令不生效
this.showContent = true;
}).catch((e) => {
console.error(e);
this.showContent = true;
this.$message.error(e.message);
});
}
},
/* 顶栏右侧点击 */
onItemClick(key) {
if (key === 'password') {
this.showPassword = true;
} else if (key === 'setting') {
this.showSetting = true;
}
},
/* 刷新页签 */
reloadPage() {
reloadPageTab();
},
/* logo点击事件 */
onLogoClick(isHome) {
if (!isHome) {
this.$router.push('/');
}
},
/* 更新collapse */
updateCollapse(value) {
this.$store.dispatch('theme/set', {key: 'collapse', value: value});
},
/* 更新屏幕尺寸 */
updateScreen() {
this.$store.dispatch('theme/updateScreen');
const checkFullscreen = this.$refs.header.checkFullscreen;
checkFullscreen && checkFullscreen();
},
/* 切换主题风格 */
changeStyle(value) {
this.$store.dispatch('theme/set', value);
},
/* 切换主题色 */
changeColor(value) {
const hide = this.$message.loading({content: '正在加载主题...'});
this.$store.dispatch('theme/setColor', value).then(() => {
hide();
}).catch((e) => {
console.error(e);
hide();
this.$message.error('主题加载失败');
});
},
changeDarkMode(value) {
this.$store.dispatch('theme/setDarkMode', value);
},
changeWeakMode(value) {
this.$store.dispatch('theme/setWeakMode', value);
},
setHomeComponents(components) {
this.$store.dispatch('theme/setHomeComponents', components);
},
/* 添加tab */
tabAdd(value) {
addPageTab(value);
},
/* 移除tab */
tabRemove(obj) {
removePageTab(obj.name).then(({lastPath}) => {
if (obj.active === obj.name) {
this.$router.push(lastPath || '/');
}
});
},
/* 移除全部tab */
tabRemoveAll(active) {
removeAllPageTab();
if (active !== '/') {
this.$router.push('/');
}
},
/* 移除左边tab */
tabRemoveLeft(value) {
removeLeftPageTab(value);
},
/* 移除右边tab */
tabRemoveRight(value) {
removeRightPageTab(value);
},
/* 移除其它tab */
tabRemoveOther(value) {
removeOtherPageTab(value);
},
/* 菜单路由国际化对应的名称 */
i18n(path, key/*, menu*/) {
// 参数三menu即原始菜单数据, 如果需要菜单标题多语言数据从接口返回可用此参数获取对应的多语言标题
// 例如下面这样写, 接口的菜单数据为{path: '/system/user', titles: {zh: '用户管理', en: 'User'}}
// return menu ? menu.titles[this.$i18n.locale] : null;
const k = 'route.' + key + '._name', title = this.$t(k);
return title === k ? null : title;
},
/* 切换语言 */
changeLanguage(lang) {
this.$i18n.locale = lang;
this.$refs.layout.changeLanguage();
localStorage.setItem('i18n-lang', lang);
}
}
}
</script>

278
src/layout/notice.vue Normal file
View File

@@ -0,0 +1,278 @@
<!-- 顶栏消息通知图标 -->
<template>
<a-dropdown v-model:visible="visible" :trigger="['click']">
<a-badge :count="allNum" class="ele-notice-trigger">
<bell-outlined style="padding: 6px;"/>
</a-badge>
<template #overlay>
<div class="ant-dropdown-menu ele-notice-pop">
<div @click.stop="">
<a-tabs v-model:active-key="active">
<a-tab-pane key="notice" :tab="noticeTitle" force-render>
<a-list item-layout="horizontal" :data-source="notice">
<template #renderItem="{item}">
<a-list-item>
<a-list-item-meta :title="item.title" :description="item.time">
<template #avatar>
<a-avatar :style="{background: item.color}">
<template #icon>
<component :is="item.icon"/>
</template>
</a-avatar>
</template>
</a-list-item-meta>
</a-list-item>
</template>
</a-list>
<div v-if="notice.length" class="ele-cell ele-notice-actions">
<div class="ele-cell-content" @click="clear('notice')">清空通知</div>
<a-divider type="vertical"/>
<div class="ele-cell-content" @click="more('notice')">查看更多</div>
</div>
</a-tab-pane>
<a-tab-pane key="message" :tab="messageTitle" force-render>
<a-list item-layout="horizontal" :data-source="message">
<template #renderItem="{item}">
<a-list-item>
<a-list-item-meta :title="item.title">
<template #avatar>
<a-avatar :src="item.avatar"/>
</template>
<template #description>
<div>{{ item.content }}</div>
<div>{{ item.time }}</div>
</template>
</a-list-item-meta>
</a-list-item>
</template>
</a-list>
<div v-if="message.length" class="ele-cell ele-notice-actions">
<div class="ele-cell-content" @click="clear('message')">清空私信</div>
<a-divider type="vertical"/>
<div class="ele-cell-content" @click="more('message')">查看更多</div>
</div>
</a-tab-pane>
<a-tab-pane key="todo" :tab="todoTitle" force-render>
<a-list item-layout="horizontal" :data-source="todo">
<template #renderItem="{item}">
<a-list-item>
<a-list-item-meta :description="item.desc">
<template #title>
<div class="ele-cell">
<div class="ele-cell-content">{{ item.title }}</div>
<a-tag :color="['', 'red', 'blue'][item.state]">
{{ ['未开始', '即将到期', '进行中'][item.state] }}
</a-tag>
</div>
</template>
</a-list-item-meta>
</a-list-item>
</template>
</a-list>
<div v-if="todo.length" class="ele-cell ele-notice-actions">
<div class="ele-cell-content" @click="clear('todo')">清空待办</div>
<a-divider type="vertical"/>
<div class="ele-cell-content" @click="more('todo')">查看更多</div>
</div>
</a-tab-pane>
</a-tabs>
</div>
</div>
</template>
</a-dropdown>
</template>
<script>
import {
BellOutlined,
NotificationFilled,
PushpinFilled,
VideoCameraFilled,
CarryOutFilled,
BellFilled
} from '@ant-design/icons-vue';
export default {
name: 'EleNotice',
components: {
BellOutlined,
NotificationFilled,
PushpinFilled,
VideoCameraFilled,
CarryOutFilled,
BellFilled
},
data() {
return {
visible: false,
active: 'notice',
notice: [
{
color: '#60B2FC',
icon: 'NotificationFilled',
title: '你收到了一封14份新周报',
time: '2020-07-27 18:30:18'
},
{
color: '#F5686F',
icon: 'PushpinFilled',
title: '许经理同意了你的请假申请',
time: '2020-07-27 09:08:36'
},
{
color: '#7CD734',
icon: 'VideoCameraFilled',
title: '陈总邀请你参加视频会议',
time: '2020-07-26 18:30:01'
},
{
color: '#FAAD14',
icon: 'CarryOutFilled',
title: '你推荐的刘诗雨已通过第三轮面试',
time: '2020-07-25 16:38:46'
},
{
color: '#2BCACD',
icon: 'BellFilled',
title: '你的6月加班奖金已发放',
time: '2020-07-25 11:03:31'
}
],
message: [
{
avatar: 'https://cdn.eleadmin.com/20200609/c184eef391ae48dba87e3057e70238fb.jpg',
title: 'SunSmile 评论了你的日志',
content: '写的不错, 以后多多向你学习~',
time: '2020-07-27 18:30:18'
},
{
avatar: 'https://cdn.eleadmin.com/20200609/948344a2a77c47a7a7b332fe12ff749a.jpg',
title: '刘诗雨 点赞了你的日志',
content: '写的不错, 以后多多向你学习~',
time: '2020-07-27 09:08:36'
},
{
avatar: 'https://cdn.eleadmin.com/20200609/2d98970a51b34b6b859339c96b240dcd.jpg',
title: '酷酷的大叔 评论了你的周报',
content: '写的不错, 以后多多向你学习~',
time: '2020-07-26 18:30:01'
},
{
avatar: 'https://cdn.eleadmin.com/20200609/f6bc05af944a4f738b54128717952107.jpg',
title: 'Jasmine 点赞了你的周报',
content: '写的不错, 以后多多向你学习~',
time: '2020-07-25 11:03:31'
}
],
todo: [
{
state: 0,
title: '刘诗雨的请假审批',
desc: '刘诗雨在 07-27 18:30 提交的请假申请'
},
{
state: 1,
title: '第三方代码紧急变更',
desc: '需要在 2020-07-27 之前完成'
},
{
state: 2,
title: '信息安全考试',
desc: '需要在 2020-07-26 18:30 前完成'
},
{
state: 2,
title: 'EleAdmin发布新版本',
desc: '需要在 2020-07-25 11:03 前完成'
}
]
};
},
computed: {
// 通知标题
noticeTitle() {
return this.notice.length ? `通知(${this.notice.length})` : '通知';
},
// 私信标题
messageTitle() {
return this.message.length ? `私信(${this.message.length})` : '私信';
},
// 待办标题
todoTitle() {
return this.todo.length ? `待办(${this.todo.length})` : '待办';
},
// 所有消息数量
allNum() {
return this.notice.length + this.message.length + this.todo.length;
}
},
methods: {
/* 清空消息 */
clear(type) {
if (type === 'notice') {
this.notice = [];
} else if (type === 'message') {
this.message = [];
} else if (type === 'todo') {
this.todo = [];
}
},
/* 查看更多 */
more(type) {
this.visible = false;
if (this.$route.path !== '/user/message' || this.$route.query.type !== type) {
this.$router.push({path: '/user/message', query: {type: type}});
}
}
}
}
</script>
<style>
.ele-notice-trigger.ant-badge {
color: inherit;
}
.ele-notice-pop.ant-dropdown-menu {
padding: 0;
width: 336px;
max-width: 100%;
margin-top: 11px;
}
/* 内容 */
.ele-notice-pop .ant-tabs-nav-wrap {
text-align: center;
}
.ele-notice-pop .ant-list-item {
padding-left: 24px;
padding-right: 24px;
transition: background-color .3s;
cursor: pointer;
}
.ele-notice-pop .ant-list-item:hover {
background: hsla(0, 0%, 60%, .05);
}
.ele-notice-pop .ant-tag {
margin: 0;
}
/* 操作按钮 */
.ele-notice-pop .ele-notice-actions {
border-top: 1px solid hsla(0, 0%, 60%, .15);
}
.ele-notice-pop .ele-notice-actions > .ele-cell-content {
line-height: 46px;
text-align: center;
transition: background-color .3s;
cursor: pointer;
}
.ele-notice-pop .ele-notice-actions > .ele-cell-content:hover {
background: hsla(0, 0%, 60%, .05);
}
</style>

110
src/layout/password.vue Normal file
View File

@@ -0,0 +1,110 @@
<!-- 修改密码弹窗 -->
<template>
<a-modal
:width="420"
title="修改密码"
:visible="visible"
:confirm-loading="loading"
:body-style="{paddingBottom: '16px'}"
@update:visible="onUpdateVisible"
@cancel="onCancel"
@ok="onOk">
<a-form
ref="form"
:model="form"
:rules="rules"
:label-col="{sm: {span: 6}}"
:wrapper-col="{sm: {span: 18}}">
<a-form-item label="旧密码" name="old">
<a-input-password v-model:value="form.old" placeholder="请输入旧密码"/>
</a-form-item>
<a-form-item label="新密码" name="password">
<a-input-password v-model:value="form.password" placeholder="请输入新密码"/>
</a-form-item>
<a-form-item label="确认密码" name="password2">
<a-input-password v-model:value="form.password2" placeholder="请再次输入新密码"/>
</a-form-item>
</a-form>
</a-modal>
</template>
<script>
export default {
name: 'ElePassword',
emits: ['update:visible'],
props: {
visible: Boolean
},
data() {
return {
// 表单数据
form: {
old: '',
password: '',
password2: ''
},
// 表单验证
rules: {
old: [
{required: true, message: '请输入旧密码', type: 'string', trigger: 'blur'}
],
password: [
{required: true, message: '请输入新密码', type: 'string', trigger: 'blur'}
],
password2: [
{
required: true,
type: 'string',
trigger: 'blur',
validator: async (rule, value) => {
if (!value) {
return Promise.reject('请再次输入新密码');
}
if (value === this.form.password) {
return Promise.resolve();
}
return Promise.reject('两次输入密码不一致');
}
}
]
},
// 按钮loading
loading: false
};
},
methods: {
/* 保存修改 */
onOk() {
this.$refs.form.validate().then(() => {
this.loading = true;
this.$http.put('/main/password', {
oldPsw: this.form.old,
newPsw: this.form.password
}).then(res => {
this.loading = false;
if (res.data.code === 0) {
this.$message.success(res.data.msg);
this.onUpdateVisible(false);
} else {
this.$message.error(res.data.msg);
}
}).catch(e => {
this.loading = false;
this.$message.error(e.message);
});
}).catch(() => {
});
},
/* 关闭回调 */
onCancel() {
this.form = {};
this.loading = false;
this.$refs.form.resetFields();
},
/* 修改visible */
onUpdateVisible(value) {
this.$emit('update:visible', value);
}
}
}
</script>

24
src/main.js Normal file
View File

@@ -0,0 +1,24 @@
/** 主入口js */
import {createApp} from 'vue';
import App from './App.vue';
import store from './store';
import router from './router';
import axios from 'axios';
import VueAxios from 'vue-axios';
import './config/axios-config';
import permission from './utils/permission';
import './styles/index.less';
import EleAdminPro from 'ele-admin-pro';
import ModalUtil from 'ele-admin-pro/packages/modal-util';
import i18n from './lang';
const app = createApp(App);
app.config.productionTip = false;
app.use(store);
app.use(router);
app.use(VueAxios, axios);
app.use(permission);
app.use(EleAdminPro);
app.use(ModalUtil);
app.use(i18n);
app.mount('#app');

89
src/router/index.js Normal file
View File

@@ -0,0 +1,89 @@
/**
* 路由配置
*/
import {createRouter, createWebHistory} from 'vue-router';
import store from '@/store';
import setting from '@/config/setting';
import EleLayout from '@/layout';
import {menuToRoutes} from 'ele-admin-pro';
import NProgress from 'nprogress';
// 静态路由
const routes = [
{
path: '/login',
component: () => import('@/views/login/login'),
meta: {title: '登录'}
},
{
path: '/forget',
component: () => import('@/views/login/forget'),
meta: {title: '忘记密码'}
},
{
path: '/:pathMatch(.*)*',
component: () => import('@/views/exception/404')
}
];
const router = createRouter({
routes,
history: createWebHistory()
});
// 路由守卫
router.beforeEach((to, from, next) => {
NProgress.start();
updateTitle(to);
// 判断是否登录
if (setting.takeToken()) {
// 判断是否已经注册动态路由
if (!store.state.user.menus) {
// 获取动态路由
store.dispatch('user/getMenus').then(({menus, home}) => {
// menuToRoutes方法是把菜单数据转成路由数据格式
router.addRoute({
path: '/',
component: EleLayout,
redirect: setting.homePath || home,
children: menuToRoutes(menus, (component) => import('@/views' + component))
});
next({...to, replace: true});
}).catch(() => {
next();
});
} else {
next();
}
} else if (setting.whiteList.includes(to.path)) {
next();
} else {
next({path: '/login', query: to.path === '/' ? {} : {from: to.path}});
}
});
router.afterEach(() => {
setTimeout(() => {
NProgress.done(true);
}, 300);
});
export default router;
/**
* 更新浏览器标题
* @param route 路由
*/
function updateTitle(route) {
if (!route.path.startsWith('/redirect/')) {
let names = [];
if (route && route.meta && route.meta.title) {
names.push(route.meta.title);
}
const appName = process.env.VUE_APP_NAME;
if (appName) {
names.push(appName);
}
document.title = names.join(' - ');
}
}

4
src/store/getters.js Normal file
View File

@@ -0,0 +1,4 @@
export default {
user: state => state.user,
theme: state => state.theme
}

18
src/store/index.js Normal file
View File

@@ -0,0 +1,18 @@
/**
* vuex状态管理
*/
import {createStore} from 'vuex';
import getters from './getters';
import user from './modules/user';
import theme from './modules/theme';
export default createStore({
state: {},
mutations: {},
actions: {},
modules: {
user,
theme
},
getters
});

454
src/store/modules/theme.js Normal file
View File

@@ -0,0 +1,454 @@
/**
* 主题状态管理
*/
import {changeColor} from 'ele-admin-pro/packages/style/util.js';
import setting from '@/config/setting';
// 不需要本地缓存的state
const NO_CACHE_STATE = [
'collapse',
'tabs',
'homeComponents',
'keepAliveInclude',
'keepAliveExclude',
'tabKeepAlive',
'screenWidth',
'screenHeight'
];
/**
* 读取缓存的配置
* @param keyName 缓存的键名
* @returns {Object}
*/
function getCacheSetting(keyName) {
let cache = {};
try {
const value = localStorage.getItem(keyName);
if (value) {
const temp = JSON.parse(value);
if (typeof temp === 'object') {
cache = temp;
}
}
} catch (e) {
console.error(e);
}
return cache;
}
/**
* 缓存配置
* @param keyName 缓存的键名
* @param key 缓存的配置名
* @param value 缓存的配置名对应的值
*/
function cacheSetting(keyName, key, value) {
if (NO_CACHE_STATE.includes(key)) {
return;
}
const cache = getCacheSetting(keyName);
if (cache[key] !== value) {
cache[key] = value;
localStorage.setItem(keyName, JSON.stringify(cache));
}
}
/**
* 检查state值以兼容旧版本
* @param key
* @param value
* @returns
*/
function checkStateValue(key, value) {
if (typeof value === 'number') {
if (key === 'sideStyle') {
return ['light', 'dark'][value < 2 ? value : 1];
}
if (key === 'headStyle') {
return ['light', 'dark', 'primary'][value < 3 ? value : 2];
}
if (key === 'tabStyle') {
return ['default', 'dot', 'card'][value < 3 ? value : 2];
}
if (key === 'layoutStyle') {
return ['side', 'top', 'mix'][value < 3 ? value : 2];
}
}
return value;
}
/**
* 获取state
* @param setting 默认配置
* @returns {Object}
*/
function getState(setting) {
const state = {
// 侧边栏风格: light(亮色), dark(暗色)
sideStyle: 'dark',
// 顶栏风格: light(亮色), dark(暗色), primary(主色)
headStyle: 'light',
// 标签页风格: default(默认), dot(圆点), card(卡片)
tabStyle: 'default',
// 布局风格: side(默认), top(顶栏菜单), mix(混合菜单)
layoutStyle: 'side',
// 侧边栏菜单风格: default(默认), mix(双排菜单)
sideMenuStyle: 'default',
// 是否固定侧栏
fixedSidebar: true,
// 是否固定顶栏
fixedHeader: false,
// 是否固定主体
fixedBody: false,
// 内容区域宽度铺满
bodyFull: true,
// 是否开启多标签
showTabs: true,
// logo是否自适应宽度
logoAutoSize: false,
// 侧栏是否多彩图标
colorfulIcon: false,
// 侧边栏是否只保持一个子菜单展开
sideUniqueOpen: true,
// 是否开启页脚
showFooter: true,
// 是否是色弱模式
weakMode: false,
// 是否是暗黑模式
darkMode: false,
// 主题色
color: null,
// 是否折叠侧边栏
collapse: false,
// 当前打开的选项卡
tabs: [],
// 主页的组件
homeComponents: [],
// 需要keep-alive的组件
keepAliveInclude: [],
// 不需要keep-alive的组件
keepAliveExclude: [],
// 开启多页签是否缓存组件
tabKeepAlive: true,
// 屏幕宽度
screenWidth: document.documentElement.clientWidth || document.body.clientWidth,
// 屏幕高度
screenHeight: document.documentElement.clientHeight || document.body.clientHeight
};
// 读取缓存的配置及默认配置
for (let key in state) {
if (!Object.prototype.hasOwnProperty.call(state, key)) {
continue;
}
const cache = getCacheSetting(setting.themeStoreName);
if (cache[key] !== undefined) {
state[key] = checkStateValue(key, cache[key]);
} else if (setting[key] !== undefined) {
state[key] = checkStateValue(key, setting[key]);
}
}
// 恢复色弱模式
if (state.weakMode) {
changeWeakMode(true);
}
// 恢复上次主题色
if (state.color || state.darkMode) {
window.addEventListener('load', () => {
doChangeTheme(state.color, state.darkMode).catch((e) => {
console.error(e);
});
});
}
return state;
}
/**
* 切换主题
* @param value 主题
* @param dark 是否是暗黑模式
* @returns {Promise<>}
*/
function doChangeTheme(value, dark) {
return new Promise((resolve, reject) => {
try {
changeColor(value, dark);
resolve();
} catch (e) {
reject(e);
}
});
}
/**
* 开关色弱模式
* @param weakMode 是否开启色弱模式
*/
function changeWeakMode(weakMode) {
const weakClass = 'ele-admin-weak';
if (weakMode) {
document.body.classList.add(weakClass);
} else {
document.body.classList.remove(weakClass);
}
}
/**
* 获取需要keep-alive的组件
* @param tabs 页签数据
* @param homeComponents 主页组件
* @returns {any[]}
*/
function getKeepAliveInclude(tabs, homeComponents) {
const components = new Set();
if (tabs) {
tabs.forEach((t) => {
if (t && t.components && t.components.length) {
t.components.forEach((c) => {
if (typeof c === 'string' && c) {
components.add(c);
}
});
}
});
}
if (homeComponents) {
homeComponents.forEach((c) => {
components.add(c);
});
}
return Array.from(components);
}
export default {
namespaced: true,
state: getState(setting),
mutations: {
// 修改state值
SET: function (state, obj) {
state[obj.key] = obj.value;
// 缓存修改的配置
cacheSetting(setting.themeStoreName, obj.key, obj.value);
},
// 更新keepAliveInclude
UPDATE_KEEP_ALIVE_INCLUDE: function (state) {
if (state.showTabs && state.tabKeepAlive) {
state.keepAliveInclude = getKeepAliveInclude(state.tabs, state.homeComponents);
} else {
state.keepAliveInclude = [];
}
}
},
actions: {
/**
* 修改配置
* @param {commit}
* @param obj
*/
set({commit}, obj) {
commit('SET', obj);
if (obj.key === 'showTabs' || obj.key === 'tabKeepAlive') {
commit('UPDATE_KEEP_ALIVE_INCLUDE');
}
},
/**
* 切换配置(boolean类型的配置)
* @param {commit, state}
* @param key 配置名称
*/
toggle({commit, state}, key) {
commit('SET', {key: key, value: !state[key]});
},
/**
* 切换主题色
* @param {commit, state}
* @param value 颜色值
* @returns {Promise<>}
*/
setColor({commit, state}, value) {
return new Promise((resolve, reject) => {
doChangeTheme(value, state.darkMode).then(() => {
commit('SET', {key: 'color', value: value});
return resolve();
}).catch((e) => {
reject(e);
});
});
},
/**
* 切换暗黑模式
* @param {commit, state}
* @param value 是否开启暗黑模式
* @returns {Promise<>}
*/
setDarkMode({commit, state}, value) {
return new Promise((resolve, reject) => {
doChangeTheme(state.color, value).then(() => {
commit('SET', {key: 'darkMode', value: value});
return resolve();
}).catch((e) => {
reject(e);
});
});
},
/**
* 切换色弱模式
* @param {commit}
* @param value 是否开启色弱模式
* @returns {Promise<>}
*/
setWeakMode({commit}, value) {
return new Promise((resolve) => {
changeWeakMode(value);
commit('SET', {key: 'weakMode', value: value});
resolve();
});
},
/**
* 添加tab
* @param {commit, state}
* @param obj {{key: String, path: String, fullPath: String, title: String}}
*/
tabAdd({commit, state}, obj) {
if (!obj.key) {
obj.key = obj.fullPath || obj.path;
}
const i = state.tabs.findIndex((d) => d.key === obj.key);
if (i === -1) {
commit('SET', {key: 'tabs', value: state.tabs.concat([obj])});
} else if (obj.fullPath !== state.tabs[i].fullPath) {
commit('SET', {key: 'tabs', value: state.tabs.slice(0, i).concat([obj]).concat(state.tabs.slice(i + 1))});
}
commit('UPDATE_KEEP_ALIVE_INCLUDE');
},
/**
* 移除指定tab
* @param commit
* @param state
* @param key {String}
* @returns {Promise<Object>}
*/
tabRemove({commit, state}, key) {
return new Promise((resolve) => {
let index = -1, lastIndex = -1, lastPath, last;
for (let i = 0; i < state.tabs.length; i++) {
if (state.tabs[i].key === key || state.tabs[i].fullPath === key) {
index = i;
break;
}
lastIndex = i;
last = state.tabs[i];
lastPath = last.fullPath;
}
commit('SET', {key: 'tabs', value: state.tabs.filter((d, i) => i !== index)});
commit('UPDATE_KEEP_ALIVE_INCLUDE');
resolve({lastIndex: lastIndex, lastPath: lastPath, last: last});
});
},
/**
* 移除所有tab
* @param commit
*/
tabRemoveAll({commit}) {
commit('SET', {key: 'tabs', value: []});
commit('UPDATE_KEEP_ALIVE_INCLUDE');
},
/**
* 移除左侧tab
* @param commit
* @param state
* @param key {String}
*/
tabRemoveLeft({commit, state}, key) {
for (let i = 0; i < state.tabs.length; i++) {
if (state.tabs[i].key === key) {
commit('SET', {key: 'tabs', value: state.tabs.slice(i)});
commit('UPDATE_KEEP_ALIVE_INCLUDE');
break;
}
}
},
/**
* 移除右侧tab
* @param commit
* @param state
* @param key {String}
*/
tabRemoveRight({commit, state}, key) {
for (let i = 0; i < state.tabs.length; i++) {
if (state.tabs[i].key === key) {
commit('SET', {key: 'tabs', value: state.tabs.slice(0, i + 1)});
commit('UPDATE_KEEP_ALIVE_INCLUDE');
break;
}
}
},
/**
* 移除其他tab
* @param commit
* @param state
* @param key {String}
*/
tabRemoveOther({commit, state}, key) {
commit('SET', {key: 'tabs', value: state.tabs.filter((d) => d.key === key)});
commit('UPDATE_KEEP_ALIVE_INCLUDE');
},
/**
* 修改指定tab
* @param commit
* @param state
* @param obj {{path: String, title: String, fullPath: String, closable: Boolean}}
*/
tabSetTitle({commit, state}, obj) {
let i = -1;
if (obj.fullPath) {
i = state.tabs.findIndex((d) => d.fullPath === obj.fullPath);
} else if (obj.path) {
i = state.tabs.findIndex((d) => d.path === obj.path);
}
if (i !== -1) {
const data = Object.assign({}, state.tabs[i]);
if (typeof obj.title === 'string' && obj.title) {
data.title = obj.title;
}
if (typeof obj.closable === 'boolean') {
data.closable = obj.closable;
}
const tabs = state.tabs.slice(0, i).concat([data]).concat(state.tabs.slice(i + 1));
commit('SET', {key: 'tabs', value: tabs});
}
},
/**
* 设置主页的组件名称
* @param {commit, state}
* @param components {Array}
*/
setHomeComponents({commit}, components) {
commit('SET', {key: 'homeComponents', value: components});
commit('UPDATE_KEEP_ALIVE_INCLUDE');
},
/**
* 设置不需要keep-alive的组件
* @param commit
* @param value {Array}
*/
setKeepAliveExclude({commit}, value) {
commit('SET', {key: 'keepAliveExclude', value: value});
},
/**
* 更新屏幕尺寸
* @param {commit, state}
*/
updateScreen({commit, state}) {
const w = document.documentElement.clientWidth || document.body.clientWidth,
h = document.documentElement.clientHeight || document.body.clientHeight;
if (w !== state.screenWidth) {
commit('SET', {key: 'screenWidth', value: w});
}
if (h !== state.screenHeight) {
commit('SET', {key: 'screenHeight', value: h});
}
}
}
};

127
src/store/modules/user.js Normal file
View File

@@ -0,0 +1,127 @@
/**
* 登录状态管理
*/
import axios from 'axios';
import setting from '@/config/setting';
import {formatMenus} from 'ele-admin-pro';
export default {
namespaced: true,
state: {
// 当前用户信息
user: setting.takeUser(),
// 当前用户权限
authorities: [],
// 当前用户角色
roles: [],
// 当前用户的菜单
menus: null
},
mutations: {
// 修改值
SET(state, obj) {
state[obj.key] = obj.value;
},
// 修改token
SET_TOKEN(state, obj) {
setting.cacheToken(obj.token, obj.remember);
state.token = obj.token;
if (!obj.token) {
state.user = {};
state.menus = null;
setting.cacheUser();
}
}
},
actions: {
/**
* 缓存token
* @param commit
* @param token {String, {token: String, remember: String}}
*/
setToken({commit}, token) {
let remember = true;
if (typeof token === 'object') {
remember = token.remember;
token = token.token;
}
commit('SET_TOKEN', {token: token, remember: remember});
},
/**
* 移除token
* @param commit
*/
removeToken({commit}) {
commit('SET_TOKEN', {});
},
/**
* 设置用户信息
* @param commit
* @param user {Object} 用户信息
*/
setUser({commit}, user) {
setting.cacheUser(user);
commit('SET', {key: 'user', value: user});
},
/**
* 设置用户权限
* @param commit
* @param authorities {Array<String>} 权限
*/
setAuthorities({commit}, authorities) {
commit('SET', {key: 'authorities', value: authorities});
},
/**
* 设置用户角色
* @param commit
* @param roles {Array<String>} 角色
*/
setRoles({commit}, roles) {
commit('SET', {key: 'roles', value: roles});
},
/**
* 设置用户菜单
* @param commit
* @param menus {Array} 菜单
*/
setMenus({commit}, menus) {
commit('SET', {key: 'menus', value: menus});
},
/**
* 获取用户菜单路由
* @param commit
* @returns {Promise} {Array}
*/
getMenus({commit}) {
return new Promise((resolve, reject) => {
if (!setting.menuUrl) {
const {menus, homePath} = formatMenus(setting.menus);
commit('SET', {key: 'menus', value: menus});
return resolve({menus: menus, home: homePath});
}
// 请求接口获取用户菜单
axios.get(setting.menuUrl).then((res) => {
const result = typeof setting.parseMenu === 'function' ? setting.parseMenu(res.data) : res.data;
// 获取用户的信息、角色、权限
if (result.user) {
setting.cacheUser(result.user);
commit('SET', {key: 'user', value: result.user});
commit('SET', {key: 'roles', value: result.user.roles});
commit('SET', {key: 'authorities', value: result.user.authorities});
}
// 获取用户的菜单
if (!result.data) {
console.error('get menus error: ', result);
return reject(new Error(result.msg));
}
// 处理菜单数据格式
const {menus, homePath} = formatMenus(result.data, setting.parseMenuItem);
commit('SET', {key: 'menus', value: menus});
resolve({menus: menus, home: homePath});
}).catch(e => {
reject(e);
});
});
}
}
}

10
src/styles/index.less Normal file
View File

@@ -0,0 +1,10 @@
/** 全局样式 */
// 如果不需要切换主题使用这个
//@import "~ele-admin-pro/packages/style/index.less";
// 如果不需要切换主题固定为夜间主题使用这个
//@import "~ele-admin-pro/packages/style/dark.less";
// 需要在线切换主题使用这个
@import "~ele-admin-pro/packages/style/dynamic.less";
/* 需要覆盖框架样式变量写最下面, 具体请到文档查看 */

View File

@@ -0,0 +1,92 @@
/**
* 页签操作封装
*/
import {unref} from 'vue';
import store from '../store';
import router from '../router';
/**
* 刷新当前页签
*/
export function reloadPageTab() {
const {path, query, matched} = unref(router.currentRoute);
const components = new Set();
matched.forEach((m) => {
if (m.components && m.components.default && m.components.default.name) {
if (!['EleEmptyLayout', 'EleLayout'].includes(m.components.default.name)) {
components.add(m.components.default.name);
}
}
});
return store.dispatch('theme/setKeepAliveExclude', Array.from(components)).then(() => {
return router.replace({
query: query,
path: '/redirect' + path
});
});
}
/**
* 关闭当前tab
*/
export function finishPageTab() {
return store.dispatch('theme/tabRemove', router.currentRoute.fullPath).then(({lastPath}) => {
return router.push(lastPath || '/');
});
}
/**
* 移除指定tab
* @param key {String}
* @returns {Promise<Object>}
*/
export function removePageTab(key) {
return store.dispatch('theme/tabRemove', key);
}
/**
* 移除所有tab
*/
export function removeAllPageTab() {
return store.dispatch('theme/tabRemoveAll');
}
/**
* 移除左侧tab
* @param key {String}
*/
export function removeLeftPageTab(key) {
return store.dispatch('theme/tabRemoveLeft', key);
}
/**
* 移除右侧tab
* @param key {String}
*/
export function removeRightPageTab(key) {
return store.dispatch('theme/tabRemoveRight', key);
}
/**
* 移除其他tab
* @param key {String}
*/
export function removeOtherPageTab(key) {
return store.dispatch('theme/tabRemoveOther', key);
}
/**
* 添加tab
* @param obj
*/
export function addPageTab(obj) {
return store.dispatch('theme/tabAdd', obj);
}
/**
* 修改指定tab
* @param obj
*/
export function setPageTab(obj) {
return store.dispatch('theme/tabSetTitle', obj);
}

128
src/utils/permission.js Normal file
View File

@@ -0,0 +1,128 @@
/**
* 权限、角色控制组件
*/
import store from '@/store';
export default {
install(app) {
// 添加全局方法
app.config.globalProperties.$hasRole = this.hasRole;
app.config.globalProperties.$hasAnyRole = this.hasAnyRole;
app.config.globalProperties.$hasPermission = this.hasPermission;
app.config.globalProperties.$hasAnyPermission = this.hasAnyPermission;
// 添加自定义指令
app.directive('role', {
mounted: (el, binding) => {
if (!this.hasRole(binding.value)) {
el.parentNode && el.parentNode.removeChild(el);
}
}
});
app.directive('any-role', {
mounted: (el, binding) => {
if (!this.hasAnyRole(binding.value)) {
el.parentNode && el.parentNode.removeChild(el);
}
}
});
app.directive('permission', {
mounted: (el, binding) => {
if (!this.hasPermission(binding.value)) {
el.parentNode && el.parentNode.removeChild(el);
}
}
});
app.directive('any-permission', {
mounted: (el, binding) => {
if (!this.hasAnyPermission(binding.value)) {
el.parentNode && el.parentNode.removeChild(el);
}
}
});
},
/**
* 是否有某些角色
* @param role {String, Array<String>} 角色字符或字符数组
* @returns {boolean}
*/
hasRole(role) {
const data = store.state.user ? store.state.user.roles : null;
return arrayHas(data, role);
},
/**
* 是否有任意角色
* @param role {String, Array<String>} 角色字符或字符数组
* @returns {boolean}
*/
hasAnyRole(role) {
const data = store.state.user ? store.state.user.roles : null;
return arrayHasAny(data, role);
},
/**
* 是否有某些权限
* @param auth {String, Array<String>} 权限字符或字符数组
* @returns {boolean}
*/
hasPermission(auth) {
const data = store.state.user ? store.state.user.authorities : null;
return arrayHas(data, auth);
},
/**
* 是否有任意权限
* @param auth {String, Array<String>} 权限字符或字符数组
* @returns {boolean}
*/
hasAnyPermission(auth) {
const data = store.state.user ? store.state.user.authorities : null;
return arrayHasAny(data, auth);
}
}
/**
* 数组是否有某些值
* @param array {Array<String>} 数组
* @param obj {String, Array<String>} 值
* @returns {boolean}
*/
function arrayHas(array, obj) {
if (!obj) {
return true;
}
if (!array) {
return false;
}
if (Array.isArray(obj)) {
for (let i = 0; i < obj.length; i++) {
if (array.indexOf(obj[i]) === -1) {
return false;
}
}
return true;
}
return array.indexOf(obj) !== -1;
}
/**
* 数组是否有任意值
* @param array {Array<String>} 数组
* @param obj {String, Array<String>} 值
* @returns {boolean}
*/
function arrayHasAny(array, obj) {
if (!obj) {
return true;
}
if (!array) {
return false;
}
if (Array.isArray(obj)) {
for (let i = 0; i < obj.length; i++) {
if (array.indexOf(obj[i]) !== -1) {
return true;
}
}
return false;
}
return array.indexOf(obj) !== -1;
}

View File

@@ -0,0 +1,602 @@
<template>
<div class="ele-body ele-body-card">
<!-- 顶部统计快 -->
<a-row :gutter="16">
<a-col :lg="6" :md="12" :sm="24" :xs="24">
<a-card class="analysis-chart-card" :bordered="false">
<div class="ele-text-secondary ele-cell">
<div class="ele-cell-content">总销售额</div>
<a-tooltip title="指标说明">
<question-circle-outlined/>
</a-tooltip>
</div>
<h1 class="analysis-chart-card-num">¥ 126,560</h1>
<div class="analysis-chart-card-content" style="padding-top: 16px;">
<a-space size="middle">
<span>周同比12% <caret-up-outlined class="ele-text-danger"/></span>
<span>日同比11% <caret-down-outlined class="ele-text-success"/></span>
</a-space>
</div>
<a-divider/>
<div>日销售额 12,423</div>
</a-card>
</a-col>
<a-col :lg="6" :md="12" :sm="24" :xs="24">
<a-card class="analysis-chart-card" :bordered="false">
<div class="ele-text-secondary ele-cell">
<div class="ele-cell-content">访问量</div>
<ele-tag color="red"></ele-tag>
</div>
<h1 class="analysis-chart-card-num">8,846</h1>
<ele-chart
ref="visitChart"
:options="visitChartOption"
class="analysis-chart-card-content"/>
<a-divider/>
<div>日访问量 1,234</div>
</a-card>
</a-col>
<a-col :lg="6" :md="12" :sm="24" :xs="24">
<a-card class="analysis-chart-card" :bordered="false">
<div class="ele-text-secondary ele-cell">
<div class="ele-cell-content">支付笔数</div>
<ele-tag color="blue"></ele-tag>
</div>
<h1 class="analysis-chart-card-num">6,560</h1>
<ele-chart
ref="payNumChart"
:options="payNumChartOption"
class="analysis-chart-card-content"/>
<a-divider/>
<div>转化率 60%</div>
</a-card>
</a-col>
<a-col :lg="6" :md="12" :sm="24" :xs="24">
<a-card class="analysis-chart-card" :bordered="false">
<div class="ele-text-secondary ele-cell">
<div class="ele-cell-content">活动运营效果</div>
<ele-tag color="green"></ele-tag>
</div>
<h1 class="analysis-chart-card-num">78%</h1>
<div class="analysis-chart-card-content" style="padding-top: 16px;">
<a-progress
:percent="78"
:show-info="false"
stroke-color="#13c2c2"/>
</div>
<a-divider/>
<a-space size="middle">
<span>周同比12% <caret-up-outlined class="ele-text-danger"/></span>
<span>日同比11% <caret-down-outlined class="ele-text-success"/></span>
</a-space>
</a-card>
</a-col>
</a-row>
<!-- 销售额访问量 -->
<a-card :bordered="false" :body-style="{padding: 0}">
<a-tabs size="large" class="monitor-card-tabs">
<a-tab-pane tab="销售额" key="saleroom">
<a-row :gutter="16">
<a-col :lg="17" :md="16" :sm="24" :xs="24">
<div class="demo-monitor-title">销售量趋势</div>
<ele-chart
ref="saleChart"
style="height: 320px;"
:options="saleChartOption"/>
</a-col>
<a-col :lg="7" :md="8" :sm="24" :xs="24">
<div class="demo-monitor-title">门店销售额排名</div>
<div
v-for="(item,index) in saleroomRankData"
:key="index"
class="demo-monitor-rank-item ele-cell">
<ele-tag
shape="circle"
:color="index<3?'#314659':''"
style="border: none;">{{ index + 1 }}
</ele-tag>
<div class="ele-cell-content">{{ item.name }}</div>
<div class="ele-text-secondary">{{ item.value }}</div>
</div>
</a-col>
</a-row>
</a-tab-pane>
<a-tab-pane tab="访问量" key="visits" force-render>
<a-row :gutter="16">
<a-col :lg="17" :md="16" :sm="24" :xs="24">
<div class="demo-monitor-title">访问量趋势</div>
<ele-chart
ref="visitsChart"
style="height: 320px;"
:options="visitsChartOption"/>
</a-col>
<a-col :lg="7" :md="8" :sm="24" :xs="24">
<div class="demo-monitor-title">门店访问量排名</div>
<div
v-for="(item,index) in visitsRankData"
:key="index"
class="demo-monitor-rank-item ele-cell">
<ele-tag
shape="circle"
:color="index<3?'#314659':''"
style="border: none;">{{ index + 1 }}
</ele-tag>
<div class="ele-cell-content">{{ item.name }}</div>
<div class="ele-text-secondary">{{ item.value }}</div>
</div>
</a-col>
</a-row>
</a-tab-pane>
<template #tabBarExtraContent>
<a-space size="middle" class="analysis-tabs-extra">
<a-radio-group v-model:value="saleSearch.dateType">
<a-radio-button value="1">今天</a-radio-button>
<a-radio-button value="2">本周</a-radio-button>
<a-radio-button value="3">本月</a-radio-button>
<a-radio-button value="4">本年</a-radio-button>
</a-radio-group>
<a-range-picker v-model:value="saleSearch.datetime">
<template #suffixIcon>
<calendar-outlined/>
</template>
</a-range-picker>
</a-space>
</template>
</a-tabs>
</a-card>
<!-- 折线图词云 -->
<a-row :gutter="16">
<a-col :lg="16" :md="14" :sm="24" :xs="24">
<a-card
:bordered="false"
title="最近1小时访问情况"
:body-style="{padding: '16px 6px 0 0'}">
<ele-chart
ref="visitHourChart"
style="height: 362px;"
:options="visitHourChartOption"/>
</a-card>
</a-col>
<a-col :lg="8" :md="10" :sm="24" :xs="24">
<a-card :bordered="false" title="热门搜索">
<ele-word-cloud
ref="hotSearchChart"
:data="hotSearchData"
style="height: 330px;"/>
</a-card>
</a-col>
</a-row>
</div>
</template>
<script>
import EleChart from 'ele-admin-pro/packages/ele-chart';
import EleWordCloud from 'ele-admin-pro/packages/ele-word-cloud';
import {
QuestionCircleOutlined,
CaretUpOutlined,
CaretDownOutlined,
CalendarOutlined
} from '@ant-design/icons-vue';
export default {
name: 'DashboardAnalysis',
components: {
EleChart,
EleWordCloud,
QuestionCircleOutlined,
CaretUpOutlined,
CaretDownOutlined,
CalendarOutlined
},
data() {
return {
// 支付笔数数据
payNumData: [],
// 销售量搜索参数
saleSearch: {
dateType: '4',
datetime: []
},
// 销售量数据
saleroomData: [],
// 销售量排名数据
saleroomRankData: [],
// 访问量数据
visitsData: [],
// 访问量排名数据
visitsRankData: [],
// 最近1小时访问情况数据
visitHourData: [],
// 词云数据
hotSearchData: []
};
},
computed: {
// 访问量折线图配置
visitChartOption() {
/*if (!this.payNumData.length) {
return null;
}*/
return {
color: '#975fe5',
tooltip: {
trigger: 'axis',
formatter: '<i class="ele-chart-dot" style="background: #975fe5;"></i>{b0}: {c0}'
},
grid: {
top: 10,
bottom: 0,
left: 0,
right: 0
},
xAxis: [
{
show: false,
type: 'category',
boundaryGap: false,
data: this.payNumData.map(d => d.date)
}
],
yAxis: [
{
show: false,
type: 'value',
splitLine: {
show: false
}
}
],
series: [
{
type: 'line',
smooth: true,
symbol: 'none',
areaStyle: {
opacity: 0.5
},
data: this.payNumData.map(d => d.value)
}
]
};
},
// 支付笔数柱状图配置
payNumChartOption() {
return {
tooltip: {
trigger: 'axis',
formatter: '<i class="ele-chart-dot" style="background: #3aa1ff;"></i>{b0}: {c0}'
},
grid: {
top: 10,
bottom: 0,
left: 0,
right: 0
},
xAxis: [
{
show: false,
type: 'category',
data: this.payNumData.map(d => d.date)
}
],
yAxis: [
{
show: false,
type: 'value',
splitLine: {
show: false
}
}
],
series: [
{
type: 'bar',
data: this.payNumData.map(d => d.value)
}
]
}
},
// 销售额柱状图配置
saleChartOption() {
return {
tooltip: {
trigger: 'axis'
},
xAxis: [
{
type: 'category',
data: this.saleroomData.map(d => d.month)
}
],
yAxis: [
{
type: 'value'
}
],
series: [
{
type: 'bar',
data: this.saleroomData.map(d => d.value)
}
]
};
},
// 访问量柱状图配置
visitsChartOption() {
return {
tooltip: {
trigger: 'axis'
},
xAxis: [
{
type: 'category',
data: this.visitsData.map(d => d.month)
}
],
yAxis: [
{
type: 'value'
}
],
series: [
{
type: 'bar',
data: this.visitsData.map(d => d.value)
}
]
};
},
// 最近1小时访问情况折线图配置
visitHourChartOption() {
/*if (!this.visitHourData.length) {
return null;
}*/
return {
tooltip: {
trigger: 'axis'
},
legend: {
data: ['浏览量', '访问量'],
right: 20
},
xAxis: [
{
type: 'category',
boundaryGap: false,
data: this.visitHourData.map(d => d.time)
}
],
yAxis: [
{
type: 'value'
}
],
series: [
{
name: '浏览量',
type: 'line',
smooth: true,
symbol: 'none',
areaStyle: {
opacity: 0.5
},
data: this.visitHourData.map(d => d.views)
},
{
name: '访问量',
type: 'line',
smooth: true,
symbol: 'none',
areaStyle: {
opacity: 0.5
},
data: this.visitHourData.map(d => d.visits)
}
]
}
}
},
mounted() {
this.getPayNumData();
this.getSaleroomData();
this.getVisitHourData();
this.getWordCloudData();
},
methods: {
/* 获取支付笔数数据 */
getPayNumData() {
this.payNumData = [
{date: '2020-06-12', value: 7},
{date: '2020-06-13', value: 5},
{date: '2020-06-14', value: 4},
{date: '2020-06-15', value: 2},
{date: '2020-06-16', value: 4},
{date: '2020-06-17', value: 7},
{date: '2020-06-18', value: 5},
{date: '2020-06-19', value: 6},
{date: '2020-06-20', value: 5},
{date: '2020-06-21', value: 9},
{date: '2020-06-22', value: 6},
{date: '2020-06-23', value: 3},
{date: '2020-06-24', value: 1},
{date: '2020-06-25', value: 5},
{date: '2020-06-26', value: 3},
{date: '2020-06-27', value: 6},
{date: '2020-06-18', value: 5}
];
},
/* 获取销售量数据 */
getSaleroomData() {
// 销售量
this.saleroomData = [
{month: '1月', value: 816},
{month: '2月', value: 542},
{month: '3月', value: 914},
{month: '4月', value: 781},
{month: '5月', value: 355},
{month: '6月', value: 796},
{month: '7月', value: 714},
{month: '8月', value: 1195},
{month: '9月', value: 1055},
{month: '10月', value: 271},
{month: '11月', value: 384},
{month: '12月', value: 1098}
];
this.saleroomRankData = [
{name: '工专路 1 号店', value: '323,234'},
{name: '工专路 2 号店', value: '323,234'},
{name: '工专路 3 号店', value: '323,234'},
{name: '工专路 4 号店', value: '323,234'},
{name: '工专路 5 号店', value: '323,234'},
{name: '工专路 6 号店', value: '323,234'},
{name: '工专路 7 号店', value: '323,234'}
];
// 访问量
this.visitsData = [
{month: '1月', value: 1098},
{month: '2月', value: 384},
{month: '3月', value: 271},
{month: '4月', value: 1055},
{month: '5月', value: 1195},
{month: '6月', value: 714},
{month: '7月', value: 796},
{month: '8月', value: 355},
{month: '9月', value: 781},
{month: '10月', value: 914},
{month: '11月', value: 542},
{month: '12月', value: 816}
];
this.visitsRankData = [
{name: '工专路 1 号店', value: '323,234'},
{name: '工专路 2 号店', value: '323,234'},
{name: '工专路 3 号店', value: '323,234'},
{name: '工专路 4 号店', value: '323,234'},
{name: '工专路 5 号店', value: '323,234'},
{name: '工专路 6 号店', value: '323,234'},
{name: '工专路 7 号店', value: '323,234'}
];
},
/* 获取最近1小时访问情况数据 */
getVisitHourData() {
this.visitHourData = [
{time: '16:00', visits: 15, views: 45},
{time: '16:05', visits: 39, views: 169},
{time: '16:10', visits: 152, views: 400},
{time: '16:15', visits: 94, views: 285},
{time: '16:20', visits: 102, views: 316},
{time: '16:25', visits: 86, views: 148},
{time: '16:30', visits: 39, views: 150},
{time: '16:35', visits: 38, views: 234},
{time: '16:40', visits: 95, views: 158},
{time: '16:45', visits: 30, views: 100},
{time: '16:50', visits: 86, views: 266}
];
},
/* 获取词云数据 */
getWordCloudData() {
this.hotSearchData = [
{name: "软妹子", value: 23},
{name: "汪星人", value: 23},
{name: "长腿欧巴", value: 23},
{name: "萝莉", value: 22},
{name: "辣~", value: 22},
{name: "K歌", value: 22},
{name: "大长腿", value: 21},
{name: "川妹子", value: 21},
{name: "女神", value: 21},
{name: "米粉", value: 20},
{name: "专注设计", value: 20},
{name: "逛街", value: 20},
{name: "黑长直", value: 20},
{name: "海纳百川", value: 19},
{name: "萌萌哒", value: 19},
{name: "坚持", value: 19},
{name: "话唠", value: 19},
{name: "果粉", value: 18},
{name: "喵星人", value: 18},
{name: "花粉", value: 18},
{name: "衬衫控", value: 18},
{name: "宅男", value: 17},
{name: "小清新", value: 17},
{name: "眼镜男", value: 17},
{name: "琼瑶", value: 17},
{name: "穷游党", value: 16},
{name: "铲屎官", value: 16},
{name: "正太", value: 16},
{name: "中二病", value: 16},
{name: "夜猫子", value: 15},
{name: "逗比", value: 15},
{name: "腹黑", value: 15},
{name: "吃鸡", value: 15},
{name: "为了联盟", value: 14},
{name: "背包客", value: 14},
{name: "民谣", value: 14},
{name: "为了部落", value: 14},
{name: "懒癌患者", value: 13},
{name: "追剧", value: 13},
{name: "IT民工", value: 13},
{name: "CNB成员", value: 13},
{name: "选择困难", value: 12},
{name: "锤粉", value: 12},
{name: "欧皇", value: 12},
{name: "仙气十足", value: 12}
];
}
},
activated() {
['visitChart', 'payNumChart', 'saleChart', 'visitsChart', 'visitHourChart', 'hotSearchChart'].forEach((name) => {
this.$refs[name].resize();
});
}
}
</script>
<style scoped>
/* 统计卡片 */
.analysis-chart-card :deep(.ant-card-body) {
padding: 16px 22px 12px 22px;
}
.analysis-chart-card-num {
font-size: 30px;
}
.analysis-chart-card-content {
height: 40px;
}
.analysis-chart-card :deep(.ant-divider) {
margin: 12px 0;
}
/* 销售额、访问量 */
.monitor-card-tabs :deep(.ant-tabs-bar) {
padding: 0 16px 0 4px;
}
.monitor-card-tabs :deep(.ant-tabs-tab) {
padding-left: 2px;
padding-right: 2px;
margin: 0 12px !important;
}
.monitor-card-tabs :deep(.ant-tabs-tabpane) {
padding-bottom: 10px;
}
.demo-monitor-title {
padding: 6px 20px;
}
.demo-monitor-rank-item {
padding: 0 20px;
margin-top: 18px;
}
@media screen and (max-width: 880px) {
.analysis-tabs-extra {
display: none;
}
}
</style>

View File

@@ -0,0 +1,668 @@
<template>
<div class="ele-body ele-body-card">
<!-- 顶部统计卡片 -->
<a-row :gutter="16">
<a-col :md="6" :sm="12" :xs="12">
<a-card :bordered="false" class="monitor-count-card">
<ele-tag
color="blue"
shape="circle"
size="large">
<eye-filled/>
</ele-tag>
<h1 class="monitor-count-card-num">21.2 k</h1>
<div class="monitor-count-card-text">总访问人数</div>
<ele-avatar-list
:data="visitUsers"
size="small"
:max="4"/>
</a-card>
</a-col>
<a-col :md="6" :sm="12" :xs="12">
<a-card :bordered="false" class="monitor-count-card">
<ele-tag
color="orange"
shape="circle"
size="large">
<fire-filled/>
</ele-tag>
<h1 class="monitor-count-card-num">1.6 k</h1>
<div class="monitor-count-card-text">点击量近30天</div>
<div class="monitor-count-card-trend ele-text-success">
<up-outlined/>
<span>110.5%</span>
</div>
<a-tooltip title="指标说明">
<question-circle-outlined class="monitor-count-card-tips"/>
</a-tooltip>
</a-card>
</a-col>
<a-col :md="6" :sm="12" :xs="12">
<a-card :bordered="false" class="monitor-count-card">
<ele-tag
color="red"
shape="circle"
size="large">
<flag-filled/>
</ele-tag>
<h1 class="monitor-count-card-num">826.0</h1>
<div class="monitor-count-card-text">到达量近30天</div>
<div class="monitor-count-card-trend ele-text-danger">
<down-outlined/>
<span>15.5%</span>
</div>
</a-card>
</a-col>
<a-col :md="6" :sm="12" :xs="12">
<a-card :bordered="false" class="monitor-count-card">
<ele-tag
color="green"
shape="circle"
size="large">
<thunderbolt-filled/>
</ele-tag>
<h1 class="monitor-count-card-num">28.8 %</h1>
<div class="monitor-count-card-text">转化率近30天</div>
<div class="monitor-count-card-trend ele-text-success">
<up-outlined/>
<span>65.8%</span>
</div>
<a-tooltip title="指标说明">
<question-circle-outlined class="monitor-count-card-tips"/>
</a-tooltip>
</a-card>
</a-col>
</a-row>
<a-row :gutter="16">
<!-- 用户分布 -->
<a-col :lg="18" :md="24" :sm="24" :xs="24">
<a-card :bordered="false" title="用户分布">
<a-row>
<a-col :sm="18" :xs="24">
<ele-chart
ref="userCountMapChart"
:options="userCountMapOption"
style="height: 485px;"/>
</a-col>
<a-col :sm="6" :xs="24">
<ele-chart
ref="userCountChart"
style="height: 485px;"
:options="userCountChartOption"/>
</a-col>
</a-row>
</a-card>
</a-col>
<!-- 在线人数 -->
<a-col :lg="6" :md="24" :sm="24" :xs="24">
<a-row :gutter="16">
<a-col :lg="24" :md="12" :sm="24" :xs="24">
<a-card :bordered="false" title="在线人数">
<div class="monitor-online-num-card">
<div>{{ currentTime }}</div>
<div class="monitor-online-num-title">
<ele-count-up
:end-val="228"
@ready="(ins) => { onlineNumAnimIns = ins; }"/>
</div>
<div class="monitor-online-num-text">在线总人数</div>
<a-badge status="processing" :text="updateTimeText"/>
</div>
</a-card>
</a-col>
<a-col :lg="24" :md="12" :sm="24" :xs="24">
<a-card
:bordered="false"
title="浏览器分布"
:body-style="{padding: '6px 0'}">
<ele-chart
ref="browserChart"
style="height: 240px;"
:options="browserChartOption"/>
</a-card>
</a-col>
</a-row>
</a-col>
<!-- 用户评价 -->
<a-col :lg="12" :md="24" :sm="24" :xs="24">
<a-card :bordered="false" title="用户评价">
<div class="ele-cell ele-cell-align-bottom">
<div style="font-size: 51px;line-height: 1;">4.5</div>
<div class="ele-cell-content">
<a-rate :value="userRate" disabled/>
<span style="color: #fadb14;margin-left: 8px;">很棒</span>
</div>
</div>
<div class="ele-cell" style="margin: 18px 0;">
<div style="font-size: 28px;line-height: 1;" class="ele-text-placeholder">-0%</div>
<div class="ele-cell-content ele-text-small ele-text-secondary">当前没有评价波动</div>
</div>
<div class="ele-cell">
<div class="ele-cell-content">
<a-progress
:percent="60"
stroke-color="#52c41a"
:show-info="false"/>
</div>
<div class="monitor-evaluate-text">
<star-filled/>
<span>5 : 368 </span>
</div>
</div>
<div class="ele-cell">
<div class="ele-cell-content">
<a-progress
:percent="40"
stroke-color="#1890ff"
:show-info="false"/>
</div>
<div class="monitor-evaluate-text">
<star-filled/>
<span>4 : 256 </span>
</div>
</div>
<div class="ele-cell">
<div class="ele-cell-content">
<a-progress
:percent="20"
stroke-color="#faad14"
:show-info="false"/>
</div>
<div class="monitor-evaluate-text">
<star-filled/>
<span>3 : 49 </span>
</div>
</div>
<div class="ele-cell">
<div class="ele-cell-content">
<a-progress
:percent="10"
stroke-color="#f5222d"
:show-info="false"/>
</div>
<div class="monitor-evaluate-text">
<star-filled/>
<span>2 : 14 </span>
</div>
</div>
<div class="ele-cell">
<div class="ele-cell-content">
<a-progress
:percent="0"
:show-info="false"/>
</div>
<div class="monitor-evaluate-text">
<star-filled/>
<span>1 : 0 </span>
</div>
</div>
</a-card>
</a-col>
<!-- 用户满意度 -->
<a-col :lg="6" :md="12" :sm="24" :xs="24">
<a-card :bordered="false" title="用户满意度">
<div class="ele-cell ele-text-center">
<div class="ele-cell-content" style="font-size: 24px;">856</div>
<div class="ele-cell-content">
<div class="monitor-face-smile"><i></i></div>
<div class="ele-text-secondary" style="margin-top: 8px;">正面评论</div>
</div>
<h2 class="ele-cell-content ele-text-success">82%</h2>
</div>
<a-divider style="margin: 26px 0;"/>
<div class="ele-cell ele-text-center">
<div class="ele-cell-content" style="font-size: 24px;">60</div>
<div class="ele-cell-content">
<div class="monitor-face-cry"><i></i></div>
<div class="ele-text-secondary" style="margin-top: 8px;">负面评论</div>
</div>
<h2 class="ele-cell-content ele-text-danger">9%</h2>
</div>
</a-card>
</a-col>
<!-- 用户活跃度 -->
<a-col :lg="6" :md="12" :sm="24" :xs="24">
<a-card :bordered="false" title="用户活跃度">
<div class="ele-cell" style="padding: 32px 0;">
<div class="ele-cell-content ele-text-center">
<div class="monitor-progress-group">
<a-progress
type="circle"
:percent="70"
stroke-color="#52c41a"
:show-info="false"
:width="161"/>
<a-progress
type="circle"
:percent="60"
stroke-color="#1890ff"
:show-info="false"
:width="121"
:stroke-width="5"/>
<a-progress
type="circle"
:percent="35"
stroke-color="#f5222d"
:show-info="false"
:width="91"
:stroke-width="4"/>
</div>
</div>
<div class="monitor-progress-legends">
<div class="ele-text-small">
<a-badge color="green" text="活跃率: 70%"/>
</div>
<div class="ele-text-small">
<a-badge color="blue" text="留存率: 60%"/>
</div>
<div class="ele-text-small">
<a-badge color="red" text="跳出率: 35%"/>
</div>
</div>
</div>
</a-card>
</a-col>
</a-row>
</div>
</template>
<script>
import EleCountUp from 'ele-admin-pro/packages/ele-count-up';
import EleChart from 'ele-admin-pro/packages/ele-chart';
import {
QuestionCircleOutlined,
EyeFilled,
FireFilled,
FlagFilled,
ThunderboltFilled,
UpOutlined,
DownOutlined,
StarFilled
} from '@ant-design/icons-vue';
export default {
name: 'DashboardMonitor',
components: {
EleCountUp,
EleChart,
QuestionCircleOutlined,
EyeFilled,
FireFilled,
FlagFilled,
ThunderboltFilled,
UpOutlined,
DownOutlined,
StarFilled
},
data() {
return {
// 访问人数头像列表数据
visitUsers: [
{
name: 'SunSmile',
avatar: 'https://cdn.eleadmin.com/20200609/c184eef391ae48dba87e3057e70238fb.jpg'
},
{
name: '你的名字很好听',
avatar: 'https://cdn.eleadmin.com/20200609/b6a811873e704db49db994053a5019b2.jpg'
},
{
name: '全村人的希望',
avatar: 'https://cdn.eleadmin.com/20200609/948344a2a77c47a7a7b332fe12ff749a.jpg'
},
{
name: 'Jasmine',
avatar: 'https://cdn.eleadmin.com/20200609/f6bc05af944a4f738b54128717952107.jpg'
},
{
name: '酷酷的大叔',
avatar: 'https://cdn.eleadmin.com/20200609/2d98970a51b34b6b859339c96b240dcd.jpg'
},
{
name: '管理员',
avatar: 'https://cdn.eleadmin.com/20200610/avatar.jpg'
}
],
// 在线总人数倒计时
updateTime: 5,
// 中国地图轮廓数据
chinaMapData: null,
// 用户分布数据
userCountData: [],
// 当前时间
currentTime: '20:58:22',
// 在线人数动画数字组件实例
onlineNumAnimIns: null,
// 在线人数更新定时器
onlineNumTimer: null,
// 浏览器分布环形图数据
browserCountData: [],
// 用户评分
userRate: 4.5
};
},
computed: {
// 在线人数倒计时文字
updateTimeText() {
return this.updateTime + ' 秒后更新';
},
// 用户分布地图配置
userCountMapOption() {
if (!this.chinaMapData) {
return null;
}
let data = this.userCountData.map(d => d.value).sort((a, b) => (a - b));
return {
tooltip: {
trigger: 'item'
},
visualMap: {
min: data[data.length - 1] || 0,
max: data[0] || 0,
text: ['高', '低'],
calculable: true,
color: ['#1890FF', '#EBF3FF']
},
series: [
{
name: '用户数',
type: 'map',
map: 'china',
label: {
show: true
},
data: this.userCountData
}
]
};
},
// 用户分布柱状图配置
userCountChartOption() {
let data = this.userCountData
.filter(d => d.value > 0)
.sort((a, b) => {
return a.value - b.value;
});
return {
tooltip: {
trigger: 'axis',
formatter: '<i class="ele-chart-dot" style="background: #3aa1ff;"></i>{b0}: {c0}'
},
grid: {
top: 0,
bottom: 0,
left: 50,
right: 0
},
yAxis: [
{
type: 'category',
axisLine: {
show: false
},
axisTick: {
show: false
},
data: data.map(d => d.name)
}
],
xAxis: [
{
show: false,
type: 'value',
splitLine: {
show: false
}
}
],
series: [
{
type: 'bar',
data: data.map(d => d.value)
}
]
};
},
// 浏览器分布饼图配置
browserChartOption() {
return {
tooltip: {
trigger: 'item'
},
legend: {
data: this.browserCountData.map(d => d.name),
bottom: 5,
itemWidth: 10,
itemHeight: 10,
icon: 'circle'
},
series: [
{
type: 'pie',
radius: ['45%', '70%'],
center: ['50%', '43%'],
label: {
show: false
},
data: this.browserCountData
}
]
};
}
},
mounted() {
this.doUpdateOnlineNum();
this.getChinaMapData();
this.getUserCountData();
this.getBrowserCountData();
},
methods: {
/* 在线人数更新倒计时 */
doUpdateOnlineNum() {
this.currentTime = this.$util.toDateString(new Date(), 'HH:mm:ss');
this.onlineNumTimer = setInterval(() => {
this.currentTime = this.$util.toDateString(new Date(), 'HH:mm:ss');
if (this.updateTime === 1) {
this.updateTime = 5;
if (this.onlineNumAnimIns) {
this.onlineNumAnimIns.update(100 + Math.round(Math.random() * 900));
}
} else {
this.updateTime--;
}
}, 1000);
},
/* 获取中国地图数据并注册地图 */
getChinaMapData() {
this.$http.get('https://cdn.eleadmin.com/20200610/china-provinces.geo.json').then(res => {
EleChart.registerMap('china', res.data);
this.chinaMapData = res.data;
}).catch(e => {
this.$message.error(e.message);
});
},
/* 获取用户分布数据 */
getUserCountData() {
this.userCountData = [
{name: "贵州", value: 570},
{name: "云南", value: 8890},
{name: "重庆", value: 10010},
{name: "吉林", value: 5056},
{name: "山西", value: 2123},
{name: "天津", value: 9130},
{name: "江西", value: 10170},
{name: "广西", value: 6172},
{name: "陕西", value: 9251},
{name: "黑龙江", value: 5125},
{name: "安徽", value: 9530},
{name: "北京", value: 51919},
{name: "福建", value: 3756},
{name: "上海", value: 59190},
{name: "湖北", value: 37109},
{name: "湖南", value: 8966},
{name: "四川", value: 31020},
{name: "辽宁", value: 7222},
{name: "河北", value: 3451},
{name: "河南", value: 9693},
{name: "浙江", value: 62310},
{name: "山东", value: 39231},
{name: "江苏", value: 35911},
{name: "广东", value: 55891}
];
},
/* 获取用户浏览器分布数据 */
getBrowserCountData() {
this.browserCountData = [
{name: 'Chrome', value: 9052},
{name: 'Safari', value: 535},
{name: 'Firefox', value: 1610},
{name: 'Edge', value: 2800},
{name: 'IE', value: 3200},
{name: 'Other', value: 1700}
];
}
},
activated() {
['userCountMapChart', 'userCountChart', 'browserChart'].forEach((name) => {
this.$refs[name].resize();
});
},
beforeUnmount() {
// 销毁定时器
if (this.onlineNumTimer) {
clearInterval(this.onlineNumTimer);
}
}
}
</script>
<style scoped>
/* 统计卡片 */
.monitor-count-card {
text-align: center;
}
.monitor-count-card .monitor-count-card-num {
margin-top: 6px;
font-size: 32px;
}
.monitor-count-card .monitor-count-card-text {
font-size: 12px;
margin: 8px 0;
opacity: .8;
}
.monitor-count-card .monitor-count-card-trend {
font-weight: bold;
line-height: 26px;
}
.monitor-count-card .monitor-count-card-trend > .anticon {
margin-right: 6px;
}
.monitor-count-card .monitor-count-card-tips {
position: absolute;
top: 16px;
right: 16px;
cursor: pointer;
opacity: .6;
}
/* 当前在线人数卡片 */
.monitor-online-num-card {
text-align: center;
}
.monitor-online-num-text {
margin-bottom: 6px;
}
.monitor-online-num-title {
font-size: 48px;
margin-bottom: 13px;
}
@media screen and (max-width: 992px) {
.monitor-online-num-card {
padding: 22px 0;
}
}
/* 用户评价 */
.monitor-evaluate-text {
width: 90px;
flex-shrink: 0;
white-space: nowrap;
opacity: .8;
}
.monitor-evaluate-text > .anticon {
font-size: 12px;
margin: 0 6px 0 8px;
}
/* 笑脸、哭脸 */
.monitor-face-smile, .monitor-face-cry {
width: 50px;
height: 50px;
display: inline-block;
background: #FBD971;
border-radius: 50%;
position: relative;
}
.monitor-face-smile > i, .monitor-face-smile:before, .monitor-face-smile:after,
.monitor-face-cry > i, .monitor-face-cry:before, .monitor-face-cry:after {
width: 28px;
height: 28px;
border-radius: 50%;
transform: rotate(225deg);
border: 3px solid #F0C419;
border-right-color: transparent !important;
border-bottom-color: transparent !important;
position: absolute;
bottom: 8px;
left: 11px;
}
.monitor-face-smile:before, .monitor-face-smile:after,
.monitor-face-cry:before, .monitor-face-cry:after {
content: "";
width: 12px;
height: 12px;
left: 8px;
top: 14px;
border-color: #F29C1F;
transform: rotate(45deg);
}
.monitor-face-smile:after, .monitor-face-cry:after {
left: auto;
right: 8px;
}
.monitor-face-cry > i {
transform: rotate(45deg);
bottom: -6px;
}
/** 圆形进度条组合 */
.monitor-progress-group {
position: relative;
display: inline-block;
}
.monitor-progress-group .ant-progress:not(:first-child) {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
margin-top: -1px;
}
.monitor-progress-legends > div + div {
margin-top: 8px;
}
</style>

View File

@@ -0,0 +1,708 @@
<template>
<div class="ele-body ele-body-card">
<!-- 顶部卡片 -->
<a-card
:bordered="false"
:body-style="{padding: '20px'}">
<div class="ele-cell workplace-user-card">
<div class="ele-cell-content ele-cell">
<a-avatar :size="68" :src="loginUser.avatar"/>
<div class="ele-cell-content">
<h4 class="ele-elip">早安{{ loginUser.nickname }}开始您一天的工作吧</h4>
<div class="ele-elip ele-text-secondary">
<cloud-outlined/>
<em>今日多云转阴18 - 22出门记得穿外套哦~</em>
</div>
</div>
</div>
<div class="workplace-count-group">
<div class="workplace-count-item">
<div class="workplace-count-header">
<ele-tag
color="blue"
shape="circle"
size="small">
<appstore-filled/>
</ele-tag>
<span class="workplace-count-name">项目数</span>
</div>
<h2>3</h2>
</div>
<div class="workplace-count-item">
<div class="workplace-count-header">
<ele-tag
color="orange"
shape="circle"
size="small">
<check-square-outlined/>
</ele-tag>
<span class="workplace-count-name">待办项</span>
</div>
<h2>6 / 24</h2>
</div>
<div class="workplace-count-item">
<div class="workplace-count-header">
<ele-tag
color="green"
shape="circle"
size="small">
<bell-filled/>
</ele-tag>
<span class="workplace-count-name">消息</span>
</div>
<h2>1,689</h2>
</div>
</div>
</div>
</a-card>
<!-- 快捷方式块 -->
<a-row :gutter="16">
<a-col :lg="3" :md="6" :sm="12" :xs="12">
<a-card
:bordered="false"
hoverable
:body-style="{padding: 0}">
<router-link
to="/system/user"
class="app-link-block">
<user-outlined class="app-link-icon"/>
<div class="app-link-title">用户</div>
</router-link>
</a-card>
</a-col>
<a-col :lg="3" :md="6" :sm="12" :xs="12">
<a-card
:bordered="false"
hoverable
:body-style="{padding: 0}">
<router-link
to="/dashboard/analysis"
class="app-link-block">
<shopping-cart-outlined
class="app-link-icon"
style="color: #95de64;"/>
<div class="app-link-title">分析</div>
</router-link>
</a-card>
</a-col>
<a-col :lg="3" :md="6" :sm="12" :xs="12">
<a-card
:bordered="false"
hoverable
:body-style="{padding: 0}">
<router-link
to="/list/card/project"
class="app-link-block">
<fund-projection-screen-outlined
class="app-link-icon"
style="color: #ff9c6e;"/>
<div class="app-link-title">商品</div>
</router-link>
</a-card>
</a-col>
<a-col :lg="3" :md="6" :sm="12" :xs="12">
<a-card
:bordered="false"
hoverable
:body-style="{padding: 0}">
<router-link
to="/list/basic"
class="app-link-block">
<file-search-outlined
class="app-link-icon"
style="color: #b37feb;"/>
<div class="app-link-title">订单</div>
</router-link>
</a-card>
</a-col>
<a-col :lg="3" :md="6" :sm="12" :xs="12">
<a-card
:bordered="false"
hoverable
:body-style="{padding: 0}">
<router-link
to="/list/advanced"
class="app-link-block">
<credit-card-outlined
class="app-link-icon"
style="color: #ffd666;"/>
<div class="app-link-title">票据</div>
</router-link>
</a-card>
</a-col>
<a-col :lg="3" :md="6" :sm="12" :xs="12">
<a-card
:bordered="false"
hoverable
:body-style="{padding: 0}">
<router-link
to="/user/message"
class="app-link-block">
<mail-outlined
class="app-link-icon"
style="color: #5cdbd3;"/>
<div class="app-link-title">消息</div>
</router-link>
</a-card>
</a-col>
<a-col :lg="3" :md="6" :sm="12" :xs="12">
<a-card
:bordered="false"
hoverable
:body-style="{padding: 0}">
<router-link
to="/extension/more"
class="app-link-block">
<tags-outlined
class="app-link-icon"
style="color: #ff85c0;"/>
<div class="app-link-title">标签</div>
</router-link>
</a-card>
</a-col>
<a-col :lg="3" :md="6" :sm="12" :xs="12">
<a-card
:bordered="false"
hoverable
:body-style="{padding: 0}">
<router-link
to="/user/profile"
class="app-link-block">
<control-outlined
class="app-link-icon"
style="color: #ffc069;"/>
<div class="app-link-title">配置</div>
</router-link>
</a-card>
</a-col>
</a-row>
<a-row :gutter="16">
<!-- 最新动态 -->
<a-col :lg="8" :md="24" :sm="24" :xs="24">
<a-card
title="最新动态"
:bordered="false"
:body-style="{padding: 0}">
<div
style="height: 347px;padding: 28px 20px 0 20px;"
class="ele-scrollbar-hover">
<a-timeline>
<a-timeline-item
v-for="(item, index) in activities"
:key="index"
:color="item.color">
<em>{{ item.time }}</em>
<em>{{ item.title }}</em>
</a-timeline-item>
</a-timeline>
</div>
</a-card>
</a-col>
<!-- 我的任务 -->
<a-col :lg="8" :md="24" :sm="24" :xs="24">
<a-card
title="我的任务"
:bordered="false"
:body-style="{padding: '10px 8px 10px 8px'}">
<div class="ant-table ant-table-middle">
<div class="ant-table-content">
<div class="ant-table-body" style="overflow-x: auto;">
<table style="min-width: max-content;">
<colgroup>
<col width="48"/>
<col width="60"/>
<col/>
<col width="80"/>
</colgroup>
<thead class="ant-table-thead">
<tr>
<th></th>
<th style="text-align: center;">优先级</th>
<th>任务名称</th>
<th style="text-align: center;">状态</th>
</tr>
</thead>
<draggable
tag="tbody"
item-key="id"
v-model="taskList"
:component-data="{class: 'ant-table-tbody'}"
handle=".anticon-menu"
:animation="300">
<template #item="{ element }">
<tr>
<td style="text-align: center;">
<menu-outlined style="cursor: move;"/>
</td>
<td style="text-align: center;padding: 8px;">
<ele-tag
:color="['red', 'orange', 'blue'][element.priority - 1]"
shape="circle">
{{ element.priority }}
</ele-tag>
</td>
<td>
<a>{{ element.taskName }}</a>
</td>
<td style="text-align: center;">
<span
:class="['ele-text-warning', 'ele-text-success', 'ele-text-info'][element.state]">
{{ ['未开始', '进行中', '已完成'][element.state] }}
</span>
</td>
</tr>
</template>
</draggable>
</table>
</div>
</div>
</div>
</a-card>
</a-col>
<!-- 本月目标 -->
<a-col :lg="8" :md="24" :sm="24" :xs="24">
<a-card title="本月目标" :bordered="false">
<div class="workplace-goal-group">
<a-progress
type="dashboard"
:percent="80"
:show-info="false"
:width="181"
:stroke-width="4"/>
<div class="workplace-goal-content">
<ele-tag
color="blue"
size="large"
shape="circle">
<trophy-outlined/>
</ele-tag>
<div class="workplace-goal-num">285</div>
</div>
<div class="workplace-goal-text">恭喜本月目标已达标</div>
</div>
</a-card>
</a-col>
<!-- 项目进度 -->
<a-col :lg="16" :md="24" :sm="24" :xs="24">
<a-card
title="项目进度"
:bordered="false"
:body-style="{padding: '12px 12px 1px 12px'}">
<a-table
:data-source="projectList"
row-key="id"
:pagination="false"
size="middle"
:scroll="{x: 'max-content'}">
<a-table-column
key="index"
align="center"
:width="48">
<template #default="{index}">{{ index + 1 }}</template>
</a-table-column>
<a-table-column
title="项目名称"
data-index="projectName">
<template #default="{text}">
<a>{{ text }}</a>
</template>
</a-table-column>
<a-table-column
title="开始时间"
data-index="startDate"/>
<a-table-column
title="结束时间"
data-index="endDate"/>
<a-table-column
title="状态"
data-index="state"
align="center"
:width="90">
<template #default="{text}">
<span
:class="['ele-text-success', 'ele-text-danger', 'ele-text-warning', 'ele-text-info ele-text-delete'][text]">
{{ ['进行中', '已延期', '未开始', '已结束'][text] }}
</span>
</template>
</a-table-column>
<a-table-column
title="进度"
data-index="progress"
align="center"
:width="180">
<template #default="{text}">
<a-progress
:percent="text"
size="small"/>
</template>
</a-table-column>
</a-table>
</a-card>
</a-col>
<!-- 小组成员 -->
<a-col :lg="8" :md="24" :sm="24" :xs="24">
<a-card
title="小组成员"
:bordered="false"
:body-style="{padding: 0}">
<div
v-for="(item,index) in userList"
:key="index"
class="ele-cell user-list-item">
<a-avatar
:size="46"
:src="item.avatar"/>
<div class="ele-cell-content">
<div class="ele-cell-title">{{ item.name }}</div>
<div class="ele-cell-desc">{{ item.desc }}</div>
</div>
<a-tag
:color="['green', 'red'][item.state]">
{{ ['在线', '离线'][item.state] }}
</a-tag>
</div>
</a-card>
</a-col>
</a-row>
</div>
</template>
<script>
import {
CloudOutlined,
AppstoreFilled,
CheckSquareOutlined,
BellFilled,
UserOutlined,
ShoppingCartOutlined,
FundProjectionScreenOutlined,
FileSearchOutlined,
CreditCardOutlined,
MailOutlined,
TagsOutlined,
ControlOutlined,
MenuOutlined,
TrophyOutlined
} from '@ant-design/icons-vue';
import draggable from 'vuedraggable';
export default {
name: 'DashboardWorkplace',
components: {
CloudOutlined,
AppstoreFilled,
CheckSquareOutlined,
BellFilled,
UserOutlined,
ShoppingCartOutlined,
FundProjectionScreenOutlined,
FileSearchOutlined,
CreditCardOutlined,
MailOutlined,
TagsOutlined,
ControlOutlined,
MenuOutlined,
TrophyOutlined,
draggable
},
data() {
return {
// 最新动态数据
activities: [
{
title: 'SunSmile 解决了bug 登录提示操作失败',
time: '20:30',
color: 'gray'
},
{
title: 'Jasmine 解决了bug 按钮颜色与设计不符',
time: '19:30',
color: 'gray'
},
{
title: '项目经理 指派了任务 解决项目一的bug',
time: '18:30'
},
{
title: '项目经理 指派了任务 解决项目二的bug',
time: '17:30'
},
{
title: '项目经理 指派了任务 解决项目三的bug',
time: '16:30'
},
{
title: '项目经理 指派了任务 解决项目四的bug',
time: '15:30',
color: 'gray'
},
{
title: '项目经理 指派了任务 解决项目五的bug',
time: '14:30',
color: 'gray'
},
{
title: '项目经理 指派了任务 解决项目六的bug',
time: '12:30',
color: 'gray'
},
{
title: '项目经理 指派了任务 解决项目七的bug',
time: '11:30'
},
{
title: '项目经理 指派了任务 解决项目八的bug',
time: '10:30',
color: 'gray'
},
{
title: '项目经理 指派了任务 解决项目九的bug',
time: '09:30',
color: 'green'
},
{
title: '项目经理 指派了任务 解决项目十的bug',
time: '08:30',
color: 'red'
}
],
// 我的任务数据
taskList: [
{
id: 1,
priority: 1,
taskName: '解决项目一的bug',
state: 0
},
{
id: 2,
priority: 2,
taskName: '解决项目二的bug',
state: 0
},
{
id: 3,
priority: 2,
taskName: '解决项目三的bug',
state: 1
},
{
id: 4,
priority: 3,
taskName: '解决项目四的bug',
state: 1
},
{
id: 5,
priority: 3,
taskName: '解决项目五的bug',
state: 2
},
{
id: 6,
priority: 3,
taskName: '解决项目六的bug',
state: 2
}
],
// 项目进度数据
projectList: [
{
id: 1,
projectName: '项目0000001',
state: 0,
startDate: '2020-03-01',
endDate: '2020-06-01',
progress: 30
},
{
id: 2,
projectName: '项目0000002',
state: 0,
startDate: '2020-03-01',
endDate: '2020-08-01',
progress: 10
},
{
id: 3,
projectName: '项目0000003',
state: 1,
startDate: '2020-01-01',
endDate: '2020-05-01',
progress: 60
},
{
id: 4,
projectName: '项目0000004',
state: 1,
startDate: '2020-06-01',
endDate: '2020-10-01',
progress: 0
},
{
id: 5,
projectName: '项目0000005',
state: 2,
startDate: '2020-01-01',
endDate: '2020-03-01',
progress: 100
},
{
id: 6,
projectName: '项目0000006',
state: 3,
startDate: '2020-01-01',
endDate: '2020-03-01',
progress: 100
},
{
id: 7,
projectName: '项目0000007',
state: 3,
startDate: '2020-01-01',
endDate: '2020-03-01',
progress: 100
}
],
// 小组成员数据
userList: [
{
name: 'SunSmile',
desc: 'UI设计师、交互专家',
state: 0,
avatar: 'https://cdn.eleadmin.com/20200609/c184eef391ae48dba87e3057e70238fb.jpg'
},
{
name: '你的名字很好听',
desc: '前端工程师',
state: 0,
avatar: 'https://cdn.eleadmin.com/20200609/b6a811873e704db49db994053a5019b2.jpg'
},
{
name: '全村人的希望',
desc: '前端工程师',
state: 0,
avatar: 'https://cdn.eleadmin.com/20200609/948344a2a77c47a7a7b332fe12ff749a.jpg'
},
{
name: 'Jasmine',
desc: '产品经理、项目经理',
state: 1,
avatar: 'https://cdn.eleadmin.com/20200609/f6bc05af944a4f738b54128717952107.jpg'
},
{
name: '酷酷的大叔',
desc: '组长、后端工程师',
state: 1,
avatar: 'https://cdn.eleadmin.com/20200609/2d98970a51b34b6b859339c96b240dcd.jpg'
}
]
};
},
computed: {
// 当前登录用户信息
loginUser() {
return this.$store.state.user.user;
}
},
mounted() {
}
}
</script>
<style scoped>
/** 用户卡片 */
.workplace-user-card .ele-cell-content {
overflow: hidden;
}
.workplace-user-card h4 {
margin-bottom: 6px;
}
.workplace-count-group {
white-space: nowrap;
text-align: right;
flex-shrink: 0;
}
.workplace-count-item {
display: inline-block;
margin: 0 4px 0 24px;
}
.workplace-count-name {
margin-left: 8px;
}
@media screen and (max-width: 992px) {
.workplace-count-item {
margin: 0 2px 0 12px;
}
}
@media screen and (max-width: 768px) {
.workplace-user-card {
display: block;
}
.workplace-count-group {
margin-top: 8px;
}
}
/** 快捷方式 */
.app-link-block {
padding: 12px;
text-align: center;
display: block;
color: inherit;
}
.app-link-block .app-link-icon {
color: #69c0ff;
font-size: 30px;
margin: 6px 0 10px 0;
}
/** 时间轴 */
.ele-scrollbar-hover :deep(.ant-timeline-item-last > .ant-timeline-item-content) {
min-height: auto;
}
/** 本月目标 */
.workplace-goal-group {
padding: 48px 0;
text-align: center;
position: relative;
}
.workplace-goal-group .workplace-goal-content {
position: absolute;
top: 90px;
left: 0;
width: 100%;
}
.workplace-goal-group .workplace-goal-num {
font-size: 40px;
}
/** 小组成员 */
.user-list-item {
padding: 16px 18px;
}
.user-list-item + .user-list-item {
border-top: 1px solid hsla(0, 0%, 60%, .15);
}
/** 表格拖拽 */
.ant-table-tbody tr.sortable-chosen {
background: hsla(0, 0%, 60%, .1) !important;
}
.ant-table-tbody tr.sortable-chosen td {
background: none !important;
}
</style>

View File

@@ -0,0 +1,245 @@
<template>
<div class="ele-body ele-body-card" style="padding-bottom: 48px;">
<a-card title="发布实训活动" :bordered="false">
<a-form
ref="form"
:model="form"
:rules="rules"
:label-col="{md: {span: 6}, sm: {span: 24}}"
:wrapper-col="{ md: {span: 18}, sm: {span: 24}}">
<a-row :gutter="16">
<a-col :md="8" :sm="24" :xs="24">
<a-form-item label="实训名称:" name="title">
<a-input
v-model:value="form.title"
placeholder="请输入实训名称"
allow-clear/>
</a-form-item>
</a-col>
<a-col :md="8" :sm="24" :xs="24">
<a-form-item label="起止日期:" name="datetime">
<a-range-picker
v-model:value="form.datetime"
class="ele-fluid">
<template #suffixIcon>
<calendar-outlined/>
</template>
</a-range-picker>
</a-form-item>
</a-col>
<a-col :md="8" :sm="24" :xs="24">
<a-form-item label="实训地点:" name="address">
<a-select
v-model:value="form.address"
placeholder="请选择地点"
allow-clear>
<a-select-option value="1">地点一</a-select-option>
<a-select-option value="2">地点二</a-select-option>
<a-select-option value="2">地点三</a-select-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-form-item
label="实训内容:"
name="content"
:label-col="{md: {span: 2}, sm: {span: 24}}"
:wrapper-col="{ md: {span: 22}, sm: {span: 24}}">
<a-textarea
v-model:value="form.content"
placeholder="请输入实训内容"
:rows="4"/>
</a-form-item>
</a-form>
</a-card>
<a-card title="选择实训班级:" :bordered="false">
<a-row :gutter="16">
<a-col :lg="12" :md="24" :sm="24" :xs="24">
<!-- 未选择的班级数据表格 -->
<ele-pro-table
bordered
:toolkit="[]"
:columns="columns"
row-key="classesId"
sub-title="未选班级:"
empty-text="已全部选择"
tools-theme="default"
:show-size-changer="false"
:datasource="unChooseClass"
:scroll="{x: 'max-content'}">
<template #toolkit>
<a-button
type="primary"
@click="addAll">全部添加
</a-button>
</template>
<template #action="{ record }">
<a-button
size="small"
type="primary"
@click="add(record)">添加
</a-button>
</template>
</ele-pro-table>
</a-col>
<a-col :lg="12" :md="24" :sm="24" :xs="24">
<!-- 已选择的班级数据表格 -->
<ele-pro-table
bordered
:toolkit="[]"
:columns="columns"
row-key="classesId"
sub-title="已选班级:"
emptyText="未选择班级"
tools-theme="default"
:show-size-changer="false"
:datasource="chooseClasses"
:scroll="{x: 'max-content'}">
<template #toolkit>
<a-button
danger
type="primary"
@click="removeAll">全部移除
</a-button>
</template>
<template #action="{ record }">
<a-button
size="small"
danger
type="primary"
@click="remove(record)">移除
</a-button>
</template>
</ele-pro-table>
</a-col>
</a-row>
</a-card>
<!-- 底部工具栏 -->
<div class="ele-bottom-tool">
<div class="ele-bottom-tool-actions">
<a-button
type="primary"
:loading="loading"
@click="submit">提交
</a-button>
</div>
</div>
</div>
</template>
<script>
import {CalendarOutlined} from '@ant-design/icons-vue';
export default {
name: 'ExampleChoose',
components: {CalendarOutlined},
data() {
return {
// 加载状态
loading: false,
// 表单数据
form: {},
// 表单验证规则
rules: {
title: [
{required: true, message: '请输入实训名称', type: 'string', trigger: 'blur'}
],
datetime: [
{required: true, message: '请选择起止日期', type: 'array', trigger: 'blur'}
],
address: [
{required: true, message: '请选择实训地点', type: 'string', trigger: 'blur'}
],
content: [
{required: true, message: '请输入实训内容', type: 'string', trigger: 'blur'}
]
},
// 全部实训班级
classes: [],
// 已选择的班级数据
chooseClasses: [],
// 表格列配置
columns: [
{
width: 90,
title: '操作',
key: 'action',
align: 'center',
slots: {customRender: 'action'}
},
{
title: '班级名称',
dataIndex: 'classesName'
},
{
title: '专业',
dataIndex: 'major'
},
{
title: '学院',
dataIndex: 'college'
}
]
};
},
computed: {
/* 未选择的班级数据 */
unChooseClass() {
return this.classes.filter(d => this.chooseClasses.indexOf(d) === -1);
}
},
mounted() {
this.query();
},
methods: {
/* 获取全部实训班级 */
query() {
this.$http.get('https://cdn.eleadmin.com/20200610/classes.json').then(res => {
if (res.data.code === 0) {
this.classes = res.data.data;
} else {
this.$message.error(res.data.msg);
}
}).catch(e => {
this.$message.error(e.message);
});
},
/* 提交 */
submit() {
this.$refs.form.validate().then(() => {
if (!this.chooseClasses.length) {
this.$message.error('请选择实训班级');
return;
}
this.loading = true;
setTimeout(() => {
this.loading = false;
this.$message.success('提交成功');
}, 1500);
}).catch(() => {
});
},
/* 添加 */
add(row) {
this.chooseClasses.push(row);
},
/* 移除 */
remove(row) {
this.chooseClasses.splice(this.chooseClasses.indexOf(row), 1);
},
/* 添加全部 */
addAll() {
this.unChooseClass.forEach(d => {
this.chooseClasses.push(d);
})
},
/* 移除所有 */
removeAll() {
this.chooseClasses.splice(0, this.chooseClasses.length);
}
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,290 @@
<template>
<a-modal
:width="1200"
:visible="visible"
title="卷内文件调整"
@update:visible="updateVisible"
@cancel="close"
@ok="save">
<a-row :gutter="16">
<a-col :lg="8" :md="24" :sm="24" :xs="24">
<!-- 表格 -->
<ele-pro-table
bordered
ref="docTable"
row-key="piece_no"
sub-title="案卷列表"
:columns="columns1"
:datasource="documents"
v-model:current="current"
:scroll="{x: 'max-content'}"
:row-selection="{columnWidth: 48}"
:tool-style="{padding: '7px 14px'}"
tools-theme="default"
:need-page="false"
:toolkit="[]">
</ele-pro-table>
</a-col>
<a-col :lg="8" :md="24" :sm="24" :xs="24">
<!-- 表格 -->
<ele-pro-table
bordered
ref="fileTable"
:loading="loading"
sub-title="卷内列表"
:datasource="data1"
:columns="columns2"
row-key="archive_no"
tools-theme="default"
:scroll="{x: 'max-content'}"
v-model:selection="selection1"
:row-selection="{columnWidth: 48}"
:need-page="false"
:toolkit="[]">
<template #toolkit>
<a-space>
<a-button
@click="moveUp"
type="primary"
class="ele-btn-icon">
<span><arrow-up-outlined/>上移</span>
</a-button>
<a-button
type="primary"
class="ele-btn-icon"
@click="moveDown">
<span><arrow-down-outlined/>下移</span>
</a-button>
<a-button
type="primary"
class="ele-btn-icon"
@click="moveOut">
<span>调出<arrow-right-outlined/></span>
</a-button>
</a-space>
</template>
</ele-pro-table>
</a-col>
<a-col :lg="8" :md="24" :sm="24" :xs="24">
<!-- 表格 -->
<ele-pro-table
bordered
:loading="loading"
:datasource="data2"
:columns="columns2"
sub-title="未归档列表"
row-key="archive_no"
tools-theme="default"
:scroll="{x: 'max-content'}"
v-model:selection="selection2"
:row-selection="{columnWidth: 48}"
:need-page="false"
:toolkit="[]">
<template #toolkit>
<a-button
type="primary"
class="ele-btn-icon"
@click="moveIn">
<span><arrow-left-outlined/>调入</span>
</a-button>
</template>
</ele-pro-table>
</a-col>
</a-row>
</a-modal>
</template>
<script>
import {
ArrowUpOutlined,
ArrowDownOutlined,
ArrowLeftOutlined,
ArrowRightOutlined
} from '@ant-design/icons-vue';
export default {
name: 'FileSort',
components: {
ArrowUpOutlined,
ArrowDownOutlined,
ArrowLeftOutlined,
ArrowRightOutlined
},
props: {
// 弹窗是否打开
visible: Boolean,
// 案卷列表
documents: {
type: Array,
required: true
}
},
emits: ['update:visible'],
data() {
return {
// 案卷表格列配置
columns1: [
{
title: '案卷题名',
dataIndex: 'title'
},
{
title: '案卷档号',
dataIndex: 'piece_no'
}
],
// 卷内表格列配置
columns2: [
{
title: '文件题名',
dataIndex: 'title'
},
{
title: '文件档号',
dataIndex: 'archive_no'
}
],
// 案卷下的全部文件列表
data: [],
// 选中案卷
current: null,
// 加载loading
loading: true,
// 卷内列表选中数据
selection1: [],
// 未归档列表选中数据
selection2: []
};
},
computed: {
// 选中案卷的卷内文件
data1() {
if (!this.current) {
return [];
}
return this.data.filter(d => d.piece_no === this.current.piece_no);
},
// 未归档的卷内文件
data2() {
return this.data.filter(d => !d.piece_no);
}
},
methods: {
/* 查询所选案卷的卷内文件 */
query() {
this.loading = true;
this.$http.get('https://cdn.eleadmin.com/20200610/archive.json').then(res => {
this.loading = false;
if (res.data.code === 0) {
this.data = res.data.data;
this.current = this.documents[0];
} else {
this.$message.error(res.data.msg);
}
}).catch(e => {
this.loading = false;
this.$message.error(e.message);
});
},
/* 上移 */
moveUp() {
if (!this.selection1.length) {
this.$message.error('请选择一条数据');
return;
}
if (this.selection1.length > 1) {
this.$message.error('只能选择一条数据');
return;
}
if (this.data1.indexOf(this.selection1[0]) === 0) {
return;
}
let index = this.data.indexOf(this.selection1[0]);
let old = this.data[index - 1];
this.data[index - 1] = this.selection1[0];
this.data[index] = old;
this.selection1 = [this.data[index - 1]];
},
/* 下移 */
moveDown() {
if (!this.selection1.length) {
this.$message.error('请选择一条数据');
return;
}
if (this.selection1.length > 1) {
this.$message.error('只能选择一条数据');
return;
}
if (this.data1.indexOf(this.selection1[0]) === this.data1.length - 1) {
return;
}
let index = this.data.indexOf(this.selection1[0]);
let old = this.data[index + 1];
this.data[index + 1] = this.selection1[0];
this.data[index] = old;
this.selection1 = [this.data[index + 1]];
},
/* 调出 */
moveOut() {
if (!this.selection1.length) {
this.$message.error('请至少选择一条数据');
return;
}
this.selection1.forEach(d => {
d.piece_no = '';
});
this.selection1 = [];
},
/* 调入 */
moveIn() {
if (!this.current) {
return;
}
if (!this.selection2.length) {
this.$message.error('请至少选择一条数据');
return;
}
this.selection2.forEach(d => {
d.piece_no = this.current.piece_no;
});
this.selection2 = [];
},
/* 保存 */
save() {
let result = this.data.map(d => {
return {
archive_no: d.archive_no,
piece_no: d.piece_no
};
});
console.log(result);
this.$message.success('调整成功');
this.close();
},
/* 关闭弹窗 */
close() {
this.data = [];
this.selection1 = [];
this.selection2 = [];
this.updateVisible(false);
},
/* 更新visible */
updateVisible(value) {
this.$emit('update:visible', value);
}
},
watch: {
visible() {
if (this.visible) {
this.query();
}
},
current() {
this.selection1 = [];
}
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,108 @@
<template>
<div class="ele-body">
<a-card :bordered="false">
<!-- 表格 -->
<ele-pro-table
ref="table"
title="案卷列表"
row-key="piece_no"
:datasource="url"
:columns="columns"
v-model:selection="selection"
:scroll="{x:'max-content'}">
<template #toolkit>
<a-button
type="primary"
@click="openFileSort">卷内文件调整
</a-button>
</template>
</ele-pro-table>
</a-card>
<!-- 卷内文件调整弹窗 -->
<file-sort
v-model:visible="showFileSort"
:documents="fileSortChoose"/>
</div>
</template>
<script>
import FileSort from './file-sort';
export default {
name: 'ExampleDocument',
components: {FileSort},
data() {
return {
// 列表接口地址
url: 'https://cdn.eleadmin.com/20200610/document.json',
// 表格列配置
columns: [
{
key: 'index',
customRender: ({index}) => this.$refs.table.tableIndex + index
},
{
title: '案卷档号',
dataIndex: 'piece_no',
sorter: true
},
{
title: '案卷题名',
dataIndex: 'title',
sorter: true
},
{
title: '年度',
dataIndex: 'year',
sorter: true
},
{
title: '保管期限',
dataIndex: 'retention',
sorter: true
},
{
title: '密级',
dataIndex: 'secret',
sorter: true
},
{
title: '档案类别',
dataIndex: 'type',
sorter: true
},
{
title: '载体规格',
dataIndex: 'carrier',
sorter: true
}
],
// 表格选中数据
selection: [],
// 是否显示卷内文件调整弹窗
showFileSort: false,
// 选中的案卷
fileSortChoose: []
};
},
methods: {
/* 打开卷内文件调整弹窗 */
openFileSort() {
if (this.selection.length < 2) {
this.$message.error('请至少选择两条数据');
return;
}
// 实际项目用这一行
/*this.fileSortChoose = this.selection.map(d => {
return Object.assign({}, d);
});*/
// 演示强制选前三个演示
this.fileSortChoose = this.$refs.table.data.slice(0, 3);
this.showFileSort = true;
}
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,23 @@
<template>
<div style="padding-top: 80px;">
<a-result
status="403"
title="403"
sub-title="抱歉, 你无权访问该页面.">
<template #extra>
<router-link to="/">
<a-button type="primary">返回首页</a-button>
</router-link>
</template>
</a-result>
</div>
</template>
<script>
export default {
name: 'Exception404'
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,23 @@
<template>
<div style="padding-top: 80px;">
<a-result
status="404"
title="404"
sub-title="抱歉, 你访问的页面不存在.">
<template #extra>
<router-link to="/">
<a-button type="primary">返回首页</a-button>
</router-link>
</template>
</a-result>
</div>
</template>
<script>
export default {
name: 'Exception404'
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,23 @@
<template>
<div style="padding-top: 80px;">
<a-result
status="500"
title="500"
sub-title="抱歉, 服务器出错了.">
<template #extra>
<router-link to="/">
<a-button type="primary">返回首页</a-button>
</router-link>
</template>
</a-result>
</div>
</template>
<script>
export default {
name: 'Exception500'
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,214 @@
<template>
<div class="ele-body ele-body-card">
<a-row :gutter="16">
<a-col :lg="8" :md="24" :sm="24" :xs="24">
<a-card title="列表拖拽排序" :bordered="false">
<div class="demo-drag-list">
<draggable
v-model="list"
item-key="id"
:animation="300"
handle=".sort-handle">
<template #item="{element}">
<div class="demo-drag-list-item ele-cell">
<menu-outlined class="sort-handle"/>
<div class="ele-cell-content">{{ element.name }}</div>
</div>
</template>
</draggable>
</div>
</a-card>
</a-col>
<a-col :lg="16" :md="24" :sm="24" :xs="24">
<a-card title="列表相互拖拽" :bordered="false">
<a-row :gutter="16">
<a-col :span="12">
<div class="demo-drag-list">
<draggable
:list="list1"
item-key="id"
:animation="300"
handle=".sort-handle"
group="demoDragList">
<template #item="{element}">
<div class="demo-drag-list-item ele-cell">
<menu-outlined class="sort-handle"/>
<div class="ele-cell-content">{{ element.name }}</div>
</div>
</template>
</draggable>
</div>
</a-col>
<a-col :span="12">
<div class="demo-drag-list">
<draggable
:list="list2"
item-key="id"
:animation="300"
handle=".sort-handle"
group="demoDragList">
<template #item="{element}">
<div class="demo-drag-list-item ele-cell">
<menu-outlined class="sort-handle"/>
<div class="ele-cell-content">{{ element.name }}</div>
</div>
</template>
</draggable>
</div>
</a-col>
</a-row>
</a-card>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :lg="8" :md="24" :sm="24" :xs="24">
<a-card title="宫格拖拽排序" :bordered="false">
<div class="demo-drag-grid">
<draggable
v-model="grid"
item-key="id"
:animation="300">
<template #item="{element}">
<div class="demo-drag-grid-item">{{ element.name }}</div>
</template>
</draggable>
</div>
</a-card>
</a-col>
<a-col :lg="16" :md="24" :sm="24" :xs="24">
<a-card title="宫格相互拖拽" :bordered="false">
<a-row :gutter="16">
<a-col :span="12">
<div class="demo-drag-grid">
<draggable
:list="grid1"
item-key="id"
:animation="300"
group="demoDragGrid">
<template #item="{element}">
<div class="demo-drag-grid-item">{{ element.name }}</div>
</template>
</draggable>
</div>
</a-col>
<a-col :span="12">
<div class="demo-drag-grid">
<draggable
:list="grid2"
item-key="id"
:animation="300"
group="demoDragGrid">
<template #item="{element}">
<div class="demo-drag-grid-item">{{ element.name }}</div>
</template>
</draggable>
</div>
</a-col>
</a-row>
</a-card>
</a-col>
</a-row>
</div>
</template>
<script>
import draggable from 'vuedraggable';
import {MenuOutlined} from '@ant-design/icons-vue';
export default {
name: 'ExtensionDragSort',
components: {
draggable,
MenuOutlined
},
data() {
return {
list: [
{id: 1, name: '项目0000001'},
{id: 2, name: '项目0000002'},
{id: 3, name: '项目0000003'},
{id: 4, name: '项目0000004'},
{id: 5, name: '项目0000005'}
],
list1: [
{id: 1, name: '项目0000001'},
{id: 2, name: '项目0000002'},
{id: 3, name: '项目0000003'},
{id: 4, name: '项目0000004'},
{id: 5, name: '项目0000005'}
],
list2: [
{id: 6, name: '项目0000006'},
{id: 7, name: '项目0000007'},
{id: 8, name: '项目0000008'},
{id: 9, name: '项目0000009'},
{id: 10, name: '项目0000010'}
],
grid: [
{id: 1, name: '项目0000001'},
{id: 2, name: '项目0000002'},
{id: 3, name: '项目0000003'},
{id: 4, name: '项目0000004'},
{id: 5, name: '项目0000005'}
],
grid1: [
{id: 1, name: '项目0000001'},
{id: 2, name: '项目0000002'},
{id: 3, name: '项目0000003'},
{id: 4, name: '项目0000004'},
{id: 5, name: '项目0000005'}
],
grid2: [
{id: 6, name: '项目0000006'},
{id: 7, name: '项目0000007'},
{id: 8, name: '项目0000008'},
{id: 9, name: '项目0000009'},
{id: 10, name: '项目0000010'}
]
};
}
}
</script>
<style scoped>
/** 列表样式 */
.demo-drag-list {
border: 1px solid hsla(0, 0%, 60%, .2);
}
.demo-drag-list-item {
line-height: 1;
padding: 12px 16px;
}
.demo-drag-list-item + .demo-drag-list-item {
border-top: 1px solid hsla(0, 0%, 60%, .2);
}
.demo-drag-list-item.sortable-chosen {
background: hsla(0, 0%, 60%, .1);
}
.demo-drag-list-item .sort-handle {
cursor: move;
font-size: 16px;
}
/** 宫格样式 */
.demo-drag-grid {
border: 1px solid hsla(0, 0%, 60%, .2);
padding: 16px 0 0 16px;
}
.demo-drag-grid-item {
padding: 16px;
margin: 0 16px 16px 0;
display: inline-block;
border: 1px solid hsla(0, 0%, 60%, .2);
cursor: move;
}
.demo-drag-grid-item.sortable-chosen {
background: hsla(0, 0%, 60%, .1);
}
</style>

View File

@@ -0,0 +1,68 @@
<template>
<div class="ele-body">
<a-card :bordered="false">
<!-- 按钮 -->
<a-space class="ele-table-tool">
<a-button
type="primary"
@click="showHtml">获取html
</a-button>
<a-button
type="primary"
@click="showText">获取文本
</a-button>
<a-button
type="primary"
@click="setContent">修改内容
</a-button>
</a-space>
<!-- 编辑器 -->
<tinymce-editor
v-model:value="value"
:init="{height: 525}"/>
</a-card>
</div>
</template>
<script>
import TinymceEditor from '@/components/TinymceEditor';
import {Modal} from 'ant-design-vue';
export default {
name: 'ExtensionEditor',
components: {TinymceEditor},
data() {
return {
value: ''
};
},
methods: {
/* 获取编辑器内容 */
showHtml() {
Modal.info({
maskClosable: true,
content: this.value
});
},
/* 获取编辑器纯文本内容 */
showText() {
Modal.info({
maskClosable: true,
content: this.$util.htmlToText(this.value)
});
},
/* 修改编辑器内容 */
setContent() {
this.value = [
'<div style="text-align:center;color:#fff;background-image:linear-gradient(-90deg,rgb(62,119,255),rgb(159,98,212),rgb(255,78,170));padding:32px 0;">',
' <div style="font-size:28px;margin-bottom:16px;">EleAdminPro后台管理模板</div>',
' <div style="font-size:18px">通用型后台管理模板,界面美观、开箱即用,拥有丰富的组件和模板</div>',
'</div><br/>'
].join('');
}
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,418 @@
<template>
<div class="ele-body ele-body-card">
<a-card title="导出excel" :bordered="false">
<!-- 表格 -->
<ele-pro-table
bordered
row-key="key"
:datasource="data"
:columns="columns"
:need-page="false"
tools-theme="default"
v-model:selection="selection"
:toolkit="['size', 'columns', 'fullscreen']"
:scroll="{x: 'max-content'}">
<template #toolbar>
<a-space>
<a-button
type="primary"
@click="exportBas">导出excel
</a-button>
<a-button
type="primary"
@click="exportAdv">导出带合并
</a-button>
<a-button
type="primary"
@click="exportSel">导出选中
</a-button>
</a-space>
</template>
</ele-pro-table>
</a-card>
<a-card title="导入excel" :bordered="false">
<!-- 操作按钮 -->
<ele-toolbar :tools="[]">
<a-space>
<a-upload
:before-upload="importFile"
:show-upload-list="false"
accept=".xls,.xlsx,.csv">
<a-button type="primary">导入excel</a-button>
</a-upload>
<a-upload
:before-upload="importFile2"
:show-upload-list="false"
accept=".xls,.xlsx,.csv">
<a-button type="primary">导入拆分合并</a-button>
</a-upload>
<a-upload
:before-upload="importFile3"
:show-upload-list="false"
accept=".xls,.xlsx,.csv">
<a-button type="primary">导入保持合并</a-button>
</a-upload>
</a-space>
</ele-toolbar>
<div class="ant-table ant-table-middle ant-table-bordered">
<div class="ant-table-content">
<div class="ant-table-body">
<table>
<thead class="ant-table-thead">
<tr>
<th></th>
<th
v-for="item in importTitle"
:key="item"
style="text-align: center;">
{{ item }}
</th>
</tr>
</thead>
<tbody class="ant-table-tbody">
<tr v-for="(item,index) in importData" :key="index">
<td style="text-align: center;">{{ index + 1 }}</td>
<template v-for="key in importTitle">
<td
v-if="item['__colspan__'+key]!==0&&item['__rowspan__'+key]!==0"
:key="key"
:colspan="item['__colspan__'+key]"
:rowspan="item['__rowspan__'+key]"
style="text-align: center;">
{{ item[key] }}
</td>
</template>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<a-row :gutter="32">
<a-col :md="12" :xs="24">
<div style="margin:16px 0;">二维数组格式数据</div>
<pre
style="max-height: 300px;padding: 16px;overflow: auto;"
class="ele-bg-base">{{ JSON.stringify(importDataAoa, null, 4) }}
</pre>
</a-col>
<a-col :md="12" :xs="24">
<div style="margin: 16px 0;">JSON格式数据</div>
<pre
style="max-height: 300px;padding: 16px;overflow: auto;"
class="ele-bg-base">{{ JSON.stringify(importData, null, 4) }}
</pre>
</a-col>
</a-row>
</a-card>
</div>
</template>
<script>
import XLSX from 'xlsx';
export default {
name: 'ExtensionExcel',
data() {
return {
// 表格数据
data: [
{
key: 1,
username: '张小三',
amount: 18,
province: '浙江',
city: '杭州',
zone: '西湖区',
street: '西溪街道',
address: '西溪花园30栋1单元',
},
{
key: 2,
username: '李小四',
amount: 39,
province: '江苏',
city: '苏州',
zone: '姑苏区',
street: '丝绸路',
address: '天墅之城9幢2单元',
},
{
key: 3,
username: '王小五',
amount: 8,
province: '江西',
city: '南昌',
zone: '青山湖区',
street: '艾溪湖办事处',
address: '中兴和园1幢3单元',
},
{
key: 4,
username: '赵小六',
amount: 16,
province: '福建',
city: '泉州',
zone: '丰泽区',
street: '南洋街道',
address: '南洋村6幢1单元',
},
{
key: 5,
username: '孙小七',
amount: 12,
province: '湖北',
city: '武汉',
zone: '武昌区',
street: '武昌大道',
address: '两湖花园16幢2单元',
},
{
key: 6,
username: '周小八',
amount: 11,
province: '安徽',
city: '黄山',
zone: '黄山区',
street: '汤口镇',
address: '温泉村21号',
}
],
// 表格列配置
columns: [
{
key: 'index',
customRender: ({index}) => `${index + 1}`,
align: 'center'
},
{
title: '用户名',
dataIndex: 'username',
align: 'center'
},
{
title: '地址',
key: 'cityAddress',
children: [
{
title: '省',
dataIndex: 'province',
align: 'center'
},
{
title: '市',
dataIndex: 'city',
align: 'center'
},
{
title: '区',
dataIndex: 'zone',
align: 'center'
},
{
title: '街道',
dataIndex: 'street',
align: 'center'
},
{
title: '详细地址',
dataIndex: 'address',
align: 'center'
}
]
},
{
title: '金额',
dataIndex: 'amount',
align: 'center'
}
],
// 选中数据
selection: [],
// 导入的数据
importData: [],
// 导入数据的列
importTitle: ['A', 'B', 'C', 'D', 'E', 'F', 'G'],
// 导入数据二维数组形式
importDataAoa: []
};
},
methods: {
/* 导出excel */
exportBas() {
let array = [['用户名', '省', '市', '区', '街道', '详细地址', '金额']];
this.data.forEach(d => {
array.push([d.username, d.province, d.city, d.zone, d.street, d.address, d.amount]);
});
this.$util.exportSheet(XLSX, array, '用户数据');
},
/* 导出带单元格合并 */
exportAdv() {
let array = [
['用户名', '地址', null, null, null, null, '金额'],
[null, '省', '市', '区', '街道', '详细地址', null]
];
this.data.forEach(d => {
array.push([d.username, d.province, d.city, d.zone, d.street, d.address, d.amount]);
});
let sheet = XLSX.utils.aoa_to_sheet(array);
sheet['!merges'] = [
{s: {r: 0, c: 1}, e: {r: 0, c: 5}}, // 合并第0行第1列到第0行第5列
{s: {r: 0, c: 0}, e: {r: 1, c: 0}}, // 合并第0行第0列到第1行第0列
{s: {r: 0, c: 6}, e: {r: 1, c: 6}} // 合并第0行第6列到第1行第6列
];
this.$util.exportSheet(XLSX, sheet, '用户数据');
},
/* 导出选中数据 */
exportSel() {
if (this.selection.length === 0) {
this.$message.error('请至少选择一条数据');
return;
}
let array = [['用户名', '省', '市', '区', '街道', '详细地址', '金额']];
this.selection.forEach(d => {
array.push([d.username, d.province, d.city, d.zone, d.street, d.address, d.amount]);
});
this.$util.exportSheet(XLSX, array, '用户数据');
},
/* 导入本地excel文件 */
importFile(file) {
let reader = new FileReader();
reader.onload = (e) => {
let data = new Uint8Array(e.target.result);
let workbook = XLSX.read(data, {type: 'array'});
let sheetNames = workbook.SheetNames;
let worksheet = workbook.Sheets[sheetNames[0]];
// 解析成二维数组
let aoa = XLSX.utils.sheet_to_json(worksheet, {header: 1});
// 生成表格需要的数据
let list = [], maxCols = 0, title = [];
aoa.forEach(d => {
if (d.length > maxCols) {
maxCols = d.length;
}
let row = {};
for (let i = 0; i < d.length; i++) {
let key = this.getCharByIndex(i);
row[key] = d[i];
row['__colspan__' + key] = 1;
row['__rowspan__' + key] = 1;
}
list.push(row);
});
for (let i = 0; i < maxCols; i++) {
title.push(this.getCharByIndex(i));
}
this.importTitle = title;
this.importData = list;
this.importDataAoa = aoa;
};
reader.readAsArrayBuffer(file);
return false;
},
/* 导入excel拆分合并单元格 */
importFile2(file) {
let reader = new FileReader();
reader.onload = (e) => {
let data = new Uint8Array(e.target.result);
let workbook = XLSX.read(data, {type: 'array'});
let sheetNames = workbook.SheetNames;
let worksheet = workbook.Sheets[sheetNames[0]];
// 解析成二维数组
let aoa = XLSX.utils.sheet_to_json(worksheet, {header: 1});
// 拆分合并单元格
if (worksheet['!merges']) {
worksheet['!merges'].forEach(m => {
for (let r = m.s.r; r <= m.e.r; r++) {
for (let c = m.s.c; c <= m.e.c; c++) {
aoa[r][c] = aoa[m.s.r][m.s.c];
}
}
});
}
// 生成表格需要的数据
let list = [], maxCols = 0, title = [];
aoa.forEach(d => {
if (d.length > maxCols) {
maxCols = d.length;
}
let row = {};
for (let i = 0; i < d.length; i++) {
row[this.getCharByIndex(i)] = d[i];
}
list.push(row);
});
for (let i = 0; i < maxCols; i++) {
title.push(this.getCharByIndex(i));
}
this.importTitle = title;
this.importData = list;
this.importDataAoa = aoa;
};
reader.readAsArrayBuffer(file);
return false;
},
/* 导入excel读取合并信息 */
importFile3(file) {
let reader = new FileReader();
reader.onload = (e) => {
let data = new Uint8Array(e.target.result);
let workbook = XLSX.read(data, {type: 'array'});
let sheetNames = workbook.SheetNames;
let worksheet = workbook.Sheets[sheetNames[0]];
// 解析成二维数组
let aoa = XLSX.utils.sheet_to_json(worksheet, {header: 1});
// 生成表格需要的数据
let list = [], maxCols = 0, title = [];
aoa.forEach(d => {
if (d.length > maxCols) {
maxCols = d.length;
}
let row = {};
for (let i = 0; i < d.length; i++) {
row[this.getCharByIndex(i)] = d[i];
}
list.push(row);
});
for (let i = 0; i < maxCols; i++) {
title.push(this.getCharByIndex(i));
}
// 记录合并单元格
if (worksheet['!merges']) {
worksheet['!merges'].forEach(m => {
for (let r = m.s.r; r <= m.e.r; r++) {
for (let c = m.s.c; c <= m.e.c; c++) {
let cc = this.getCharByIndex(c);
list[r]['__colspan__' + cc] = 0;
list[r]['__rowspan__' + cc] = 0;
}
}
let char = this.getCharByIndex(m.s.c);
list[m.s.r]['__colspan__' + char] = m.e.c - m.s.c + 1;
list[m.s.r]['__rowspan__' + char] = m.e.r - m.s.r + 1;
});
}
this.importTitle = title;
this.importData = list;
this.importDataAoa = aoa;
};
reader.readAsArrayBuffer(file);
return false;
},
/* 生成Excel列字母序号 */
getCharByIndex(index) {
let chars = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'];
if (index < chars.length) {
return chars[index];
}
let n = parseInt(index / chars.length),
m = index % chars.length;
return chars[n] + chars[m];
}
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,289 @@
<template>
<div class="ele-body">
<a-card
:bordered="false"
:body-style="{padding: 0, minHeight: '520px'}">
<div style="padding: 16px 16px 0 16px;">
<!-- 工具栏 -->
<div class="ele-table-tool">
<div class="ele-table-tool-title">
<a-space>
<a-upload
:showUploadList="false"
:customRequest="doUpload">
<a-button type="primary">上传</a-button>
</a-upload>
<a-button type="primary">新建文件夹</a-button>
<a-button danger type="primary">删除</a-button>
</a-space>
</div>
<!-- 搜索框 -->
<div style="max-width: 240px;">
<a-input-search
v-model:value="search"
placeholder="搜索您的文件"/>
</div>
<!-- 显示方式切换 -->
<menu-outlined
v-if="grid"
class="ele-file-tool-btn"
@click="grid=!grid"/>
<appstore-outlined
v-else
class="ele-file-tool-btn"
@click="grid=!grid"/>
</div>
<!-- 文件目录面包屑 -->
<div class="ele-file-breadcrumb-group ele-cell">
<div class="ele-cell-content ele-cell">
<div
v-if="directory.length"
class="ele-file-breadcrumb-back ele-text-primary"
@click="back">返回上一级
</div>
<div class="ele-file-breadcrumb-list ele-cell-content ele-cell">
<div
:class="['ele-file-breadcrumb-item ele-cell', {'ele-text-primary': directory.length}]"
@click="listAll">
<div class="ele-file-breadcrumb-item-title">全部文件</div>
<right-outlined v-if="directory.length"/>
</div>
<div
v-for="(item,index) in directory"
:key="index"
@click="listDir(index)"
:class="['ele-file-breadcrumb-item ele-cell', {'ele-text-primary': index!==directory.length-1}]">
<div class="ele-file-breadcrumb-item-title">{{ item }}</div>
<right-outlined v-if="index!==directory.length-1"/>
</div>
</div>
</div>
<div>已全部加载 {{ data.length }} </div>
</div>
</div>
<a-spin :spinning="loading">
<!-- 文件展示列表 -->
<ele-file-list
:data="data"
v-model:checked="checked"
:grid="grid"
:sort="sort"
:order="order"
@item-click="onItemClick"
@sort-change="onSortChange">
</ele-file-list>
</a-spin>
</a-card>
</div>
</template>
<script>
import EleFileList from 'ele-admin-pro/packages/ele-file-list';
import {
MenuOutlined,
AppstoreOutlined,
RightOutlined
} from '@ant-design/icons-vue';
export default {
name: 'ExtensionFile',
components: {
EleFileList,
MenuOutlined,
AppstoreOutlined,
RightOutlined
},
data() {
return {
// 加载状态
loading: true,
// 是否网格展示
grid: true,
// 文件列表数据
data: [],
// 选中数据
checked: [],
// 文件目录面包屑数据
directory: [],
// 搜索
search: '',
// 排序字段
sort: '',
// 排序方式
order: '',
// 图片预览文件
currentImage: '',
};
},
computed: {
// 图片预览列表
previewList() {
return this.data.filter(d => d.thumbnail).map(d => d.url);
}
},
mounted() {
this.query();
},
methods: {
/* 查询文件列表 */
query() {
this.checked = [];
this.loading = true;
this.$http.get('/file/list', {
params: {
directory: this.directory.join('/'),
sort: this.sort,
order: this.order
}
}).then(res => {
this.loading = false;
if (res.data.code === 0) {
res.data.data.forEach(d => {
// 文件地址加baseURL
if (d.url) {
d.url = this.$http.defaults.baseURL + '/' + d.url;
}
if (d.thumbnail) {
d.thumbnail = this.$http.defaults.baseURL + '/' + d.thumbnail;
}
// 文件大小格式化
if (d.isDirectory) {
d.length = '-';
} else {
d.length = this.getFileSize(d.length);
}
// 修改时间格式化
if (d.updateTime) {
d.updateTime = this.$util.toDateString(d.updateTime);
}
});
this.data = res.data.data;
} else {
this.$message.error(res.data.msg);
}
}).catch(e => {
this.loading = false;
this.$message.error(e.message);
});
},
/* item点击事件 */
onItemClick(item) {
if (item.isDirectory) { // 文件夹
this.directory.push(item.name);
this.query();
} /*else if (item.thumbnail) {
this.currentImage = item.url;
}*/ else if (this.checked.indexOf(item) !== -1) {
this.checked.splice(this.checked.indexOf(item), 1);
} else {
this.checked.push(item);
}
},
/* 返回上级 */
back() {
this.directory.splice(this.directory.length - 1, 1);
this.query();
},
/* 全部文件 */
listAll() {
if (!this.directory.length) {
return;
}
this.directory = [];
this.query();
},
/* 回到指定目录 */
listDir(index) {
if (index === this.directory.length - 1) {
return;
}
this.directory.splice(index, this.directory.length - index);
this.query();
},
/* 文件大小格式化 */
getFileSize(value) {
if (value < 1024) {
return value + 'B';
} else if (value < 1024 * 1024) {
return (value / 1024).toFixed(1) + 'KB';
} else if (value < 1024 * 1024 * 1024) {
return (value / 1024 / 1024).toFixed(1) + 'M';
} else {
return (value / 1024 / 1024 / 1024).toFixed(1) + 'G';
}
},
/* 文件列表排序方式改变 */
onSortChange(obj) {
this.order = obj.order;
this.sort = obj.sort;
this.query();
},
/* 查看文件 */
view(item) {
if (item.isDirectory) {
this.onItemClick(item);
} else if (item.url) {
window.open(item.url);
}
},
/* 上传 */
doUpload({file}) {
let formData = new FormData();
formData.append('file', file);
const hide = this.$message.loading('上传中..', 0);
this.$http.post('/file/upload', formData).then(res => {
hide();
if (res.data.code === 0) {
this.$message.success(res.data.msg);
this.directory = [res.data.dir.replace(/\//, '')];
this.query();
} else {
this.$message.error(res.data.msg);
}
}).catch(e => {
hide();
this.$message.error(e.message);
});
return false;
}
}
}
</script>
<style scoped>
/* 图标按钮 */
.ele-file-tool-btn {
font-size: 20px;
margin-left: 16px;
cursor: pointer;
}
/* 文件目录面包屑 */
.ele-file-breadcrumb-group {
margin-bottom: 12px;
line-height: 1;
}
.ele-file-breadcrumb-back {
padding-right: 12px;
border-right: 1px solid hsla(0, 0%, 60%, .3);
}
.ele-file-breadcrumb-back:hover,
.ele-file-breadcrumb-item.ele-text-primary:hover > .ele-file-breadcrumb-item-title {
text-decoration: underline;
cursor: pointer;
}
.ele-file-breadcrumb-item .anticon {
margin: 0 4px;
font-size: 12px;
}
@media screen and (max-width: 768px) {
.ele-table-tool > .ele-table-tool-title + div,
.ele-file-breadcrumb-group > .ele-cell-content + div {
display: none;
}
}
</style>

261
src/views/extension/map.vue Normal file
View File

@@ -0,0 +1,261 @@
<template>
<div class="ele-body ele-body-card">
<!-- 地图位置选择弹窗 -->
<ele-map-picker
v-model:visible="showPicker"
:need-city="true"
@done="onChoose"/>
<ele-map-picker
v-model:visible="showPicker2"
:need-city="true"
:search-type="1"
@done="onChoose"/>
<!-- 弹窗选择位置 -->
<a-card title="弹窗选择位置" :bordered="false">
<a-space>
<a-button
type="primary"
@click="showPicker=true">地图选择位置(POI)
</a-button>
<a-button
type="primary"
@click="showPicker2=true">关键字检索模式
</a-button>
</a-space>
<div v-if="form.location">
<div style="margin-top: 12px;">选择位置: {{ form.location }}</div>
<div style="margin-top: 12px;">详细地址: {{ form.address }}</div>
<div style="margin-top: 12px;"> : {{ form.jinweidu }}</div>
</div>
</a-card>
<!-- 官网底部地图 -->
<a-card title="官网底部地图" :bordered="false">
<div ref="locationMap" style="height: 360px;max-width: 1000px;"></div>
</a-card>
<!-- 轨迹展示及轨迹回放 -->
<a-card title="轨迹展示及轨迹回放" :bordered="false">
<div ref="trackMap" style="height: 360px;margin-bottom: 16px;max-width: 1000px;"></div>
<a-space>
<a-button
type="primary"
@click="startTrackAnim">开始动画
</a-button>
<a-button
type="primary"
@click="pauseTrackAnim">暂停动画
</a-button>
<a-button
type="primary"
@click="resumeTrackAnim">继续动画
</a-button>
</a-space>
</a-card>
</div>
</template>
<script>
import EleMapPicker from 'ele-admin-pro/packages/ele-map-picker';
import AMapLoader from '@amap/amap-jsapi-loader';
export default {
name: 'ExtensionMap',
components: {EleMapPicker},
data() {
return {
// 是否显示地图选择弹窗
showPicker: false,
// 是否显示地图选择弹窗2
showPicker2: false,
// 表单
form: {},
// 小车的marker
carMarker: null,
// 轨迹路线
lineData: [
[116.478935, 39.997761],
[116.478939, 39.997825],
[116.478912, 39.998549],
[116.478912, 39.998549],
[116.478998, 39.998555],
[116.478998, 39.998555],
[116.479282, 39.99856],
[116.479658, 39.998528],
[116.480151, 39.998453],
[116.480784, 39.998302],
[116.480784, 39.998302],
[116.481149, 39.998184],
[116.481573, 39.997997],
[116.481863, 39.997846],
[116.482072, 39.997718],
[116.482362, 39.997718],
[116.483633, 39.998935],
[116.48367, 39.998968],
[116.484648, 39.999861]
],
// 官网底部地图的实例
mapInsLocation: null,
// 小车轨迹地图的实例
mapInsTrack: null
};
},
computed: {
// 是否是暗黑模式
darkMode() {
return this.$store.state.theme.darkMode;
}
},
mounted() {
this.renderLocationMap();
this.renderTrackMap();
},
methods: {
/* 地图选择后回调 */
onChoose(location) {
console.log(location);
this.form = {
location: location.city.province + '/' + location.city.city + '/' + location.city.district,
address: location.name + ' ' + location.address,
jinweidu: location.lng + ',' + location.lat
};
this.showPicker = false;
this.showPicker2 = false;
},
/* 渲染官网底部地图 */
renderLocationMap() {
AMapLoader.load({
'key': '006d995d433058322319fa797f2876f5',
'version': '2.0',
'plugins': ['AMap.InfoWindow', 'AMap.Marker']
}).then((AMap) => {
// 渲染地图
let option = {
zoom: 13, // 初缩放级别
center: [114.346084, 30.511215 + 0.005] // 初始中心点
};
if (this.darkMode) {
option.mapStyle = 'amap://styles/dark';
}
this.mapInsLocation = new AMap.Map(this.$refs.locationMap, option);
// 创建信息窗体
let infoWindow = new AMap.InfoWindow({
content: `
<div style="color: #333;">
<div style="padding: 5px;font-size: 16px;">武汉易云智科技有限公司</div>
<div style="padding: 0 5px;">地址湖北省武汉市洪山区雄楚大道222号</div>
<div style="padding: 0 5px;">电话020-123456789</div>
</div>
<a style="padding: 8px 5px 0;text-decoration: none;display: inline-block;" class="ele-text-primary"
href="//uri.amap.com/marker?position=114.346084,30.511215&name=武汉易云智科技有限公司"
target="_blank">到这里去→</a>
`
});
infoWindow.open(this.mapInsLocation, [114.346084, 30.511215]);
let icon = new AMap.Icon({
size: new AMap.Size(25, 34),
image: '//a.amap.com/jsapi_demos/static/demo-center/icons/poi-marker-red.png',
imageSize: new AMap.Size(25, 34)
});
let marker = new AMap.Marker({
icon: icon,
position: [114.346084, 30.511215],
offset: new AMap.Pixel(-12, -28)
});
marker.setMap(this.mapInsLocation);
marker.on('click', () => {
infoWindow.open(this.mapInsLocation);
});
}).catch(e => {
console.error(e);
});
},
/* 渲染轨迹回放地图 */
renderTrackMap() {
AMapLoader.load({
'key': '006d995d433058322319fa797f2876f5',
'version': '2.0',
'plugins': ['AMap.MoveAnimation', 'AMap.Marker', 'AMap.Polyline']
}).then((AMap) => {
// 渲染地图
let option = {
zoom: 17,
center: [116.478935, 39.997761],
};
if (this.darkMode) {
option.mapStyle = 'amap://styles/dark';
}
this.mapInsTrack = new AMap.Map(this.$refs.trackMap, option);
// 创建小车marker
this.carMarker = new AMap.Marker({
map: this.mapInsTrack,
position: [116.478935, 39.997761],
icon: 'https://a.amap.com/jsapi_demos/static/demo-center-v2/car.png',
offset: new AMap.Pixel(-13, -26),
});
// 绘制轨迹
new AMap.Polyline({
map: this.mapInsTrack,
path: this.lineData,
showDir: true,
strokeColor: '#28F', // 线颜色
strokeOpacity: 1, // 线透明度
strokeWeight: 6, // 线宽
//strokeStyle: 'solid' // 线样式
});
// 通过的轨迹
let passedPolyline = new AMap.Polyline({
map: this.mapInsTrack,
showDir: true,
strokeColor: '#4B5', // 线颜色
strokeOpacity: 1, // 线透明度
strokeWeight: 6, // 线宽
});
// 小车移动回调
this.carMarker.on('moving', function (e) {
passedPolyline.setPath(e.passedPath);
});
// 地图自适应
this.mapInsTrack.setFitView();
}).catch(e => {
console.error(e);
});
},
/* 开始轨迹回放动画 */
startTrackAnim() {
this.carMarker.stopMove();
this.carMarker.moveAlong(this.lineData, {
duration: 200,
autoRotation: true,
});
},
/* 暂停轨迹回放动画 */
pauseTrackAnim() {
this.carMarker.pauseMove();
},
/* 继续开始轨迹回放动画 */
resumeTrackAnim() {
this.carMarker.resumeMove();
}
},
watch: {
darkMode() {
if (this.mapInsLocation) {
if (this.darkMode) {
this.mapInsLocation.setMapStyle('amap://styles/dark');
} else {
this.mapInsLocation.setMapStyle('amap://styles/normal');
}
}
if (this.mapInsTrack) {
if (this.darkMode) {
this.mapInsTrack.setMapStyle('amap://styles/dark');
} else {
this.mapInsTrack.setMapStyle('amap://styles/normal');
}
}
}
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,34 @@
<template>
<div class="ele-body ele-body-card">
<more-tag/>
<more-city-select/>
<more-modal/>
<more-color-picker/>
<more-cropper/>
<more-count-up/>
</div>
</template>
<script>
import MoreModal from './more-modal';
import MoreTag from './more-tag';
import MoreCitySelect from './more-city-select';
import MoreColorPicker from './more-color-picker';
import MoreCropper from './more-cropper';
import MoreCountUp from './more-count-up';
export default {
name: 'ExtensionMore',
components: {
MoreTag,
MoreCitySelect,
MoreColorPicker,
MoreCropper,
MoreCountUp,
MoreModal
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,44 @@
<template>
<a-card title="省市区级联选择" :bordered="false">
<a-space>
<a-cascader
v-model:value="city"
:options="cityData.cityData"
placeholder="请选择省市区"
popup-class-name="ele-pop-wrap-higher"/>
<a-cascader
v-model:value="provinceCity"
:options="cityData.provinceCityData"
placeholder="请选择省市"
popup-class-name="ele-pop-wrap-higher"/>
<a-cascader
v-model:value="province"
:options="cityData.provinceData"
placeholder="请选择省"
popup-class-name="ele-pop-wrap-higher"/>
</a-space>
</a-card>
</template>
<script>
import regions from 'ele-admin-pro/packages/regions.js';
export default {
name: 'MoreCitySelect',
data() {
return {
// 省市区数据
cityData: regions,
// 选中的省市区
city: [],
// 选中的省市
provinceCity: [],
// 选中的省
province: [],
};
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,53 @@
<template>
<a-card title="颜色选择器" :bordered="false">
<a-space>
<ele-color-picker
size="large"
:show-alpha="true"
v-model:value="color"
:predefine="predefineColors"/>
<ele-color-picker
:show-alpha="true"
v-model:value="color"
:predefine="predefineColors"/>
<ele-color-picker
size="small"
:show-alpha="true"
v-model:value="color"
:predefine="predefineColors"/>
</a-space>
</a-card>
</template>
<script>
import EleColorPicker from 'ele-admin-pro/packages/ele-color-picker';
export default {
name: 'MoreColorPicker',
components: {EleColorPicker},
data() {
return {
color: 'rgba(255, 69, 0, 0.68)',
predefineColors: [
'#ff4500',
'#ff8c00',
'#ffd700',
'#90ee90',
'#00ced1',
'#1e90ff',
'#c71585',
'rgba(255, 69, 0, 0.68)',
'rgb(255, 120, 0)',
'hsv(51, 100, 98)',
'hsva(120, 40, 94, 0.5)',
'hsl(181, 100%, 37%)',
'hsla(209, 100%, 56%, 0.73)',
'#c7158577'
]
};
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,71 @@
<template>
<a-card title="数字滚动" :bordered="false">
<h1 style="margin-bottom: 12px;">
<ele-count-up
:delay="0"
:end-val="countUpVal"
:options="countUpOptions"
@ready="onCountUpReady"/>
</h1>
<a-space>
<a-button
type="primary"
@click="startCountUp">重新开始
</a-button>
<a-button
type="primary"
@click="updateCountUp">更新数字
</a-button>
</a-space>
</a-card>
</template>
<script>
import EleCountUp from 'ele-admin-pro/packages/ele-count-up';
export default {
name: 'MoreCountUp',
components: {EleCountUp},
data() {
return {
// countUp值
countUpVal: 6317,
// countUp配置
countUpOptions: {
useEasing: true,
useGrouping: true,
separator: ',',
decimal: '.',
prefix: '',
suffix: ''
},
// countUp实例
countUpIns: null
};
},
methods: {
/* countUp渲染完成 */
onCountUpReady(instance) {
this.countUpIns = instance;
},
/* countUp重新开始 */
startCountUp() {
if (!this.countUpIns) {
return;
}
this.countUpIns.reset();
this.countUpIns.start();
},
/* countUp更新 */
updateCountUp() {
if (!this.countUpIns) {
return;
}
this.countUpIns.update(1000 + Math.round(Math.random() * 9000));
}
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,54 @@
<template>
<a-card title="图片裁剪" :bordered="false">
<a-space size="middle">
<a-button
type="primary"
@click="visible1=true">1 : 1 裁剪
</a-button>
<a-button
type="primary"
@click="visible2=true">16 : 9 裁剪
</a-button>
</a-space>
<div v-if="result" style="margin-top: 16px;">
<img :src="result" style="height: 120px;width: auto;"/>
</div>
<!-- 图片裁剪 -->
<ele-cropper-modal
:src="src"
v-model:visible="visible1"
@done="onDone"/>
<ele-cropper-modal
:src="src"
:aspect-ratio="16/9"
v-model:visible="visible2"
@done="onDone"/>
</a-card>
</template>
<script>
import EleCropperModal from 'ele-admin-pro/packages/ele-cropper-modal';
export default {
name: 'MoreCropper',
components: {EleCropperModal},
data() {
return {
visible1: false,
visible2: false,
src: 'https://cdn.eleadmin.com/20200610/LrCTN2j94lo9N7wEql7cBr1Ux4rHMvmZ.jpg',
result: null
};
},
methods: {
onDone(result) {
this.result = result;
this.visible1 = false;
this.visible2 = false;
}
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,207 @@
<template>
<a-card title="可拖拽、拉伸、全屏弹窗" :bordered="false">
<div>
<a-space>
<a-button type="primary" @click="openDialog">可拖拽弹窗</a-button>
<a-button type="primary" @click="openMoveOutDialog">允许拖出边界</a-button>
<a-button type="primary" @click="openHideModalDialog">不要遮罩层</a-button>
</a-space>
</div>
<div style="margin-top: 16px;">
<a-space>
<a-button type="primary" @click="openFullDialog">默认全屏打开</a-button>
<a-button type="primary" @click="openMoreDialog">默认左下角打开</a-button>
</a-space>
</div>
</a-card>
<!-- 弹窗1 -->
<a-modal
:mask="mask"
:width="400"
v-model:visible="visible"
:wrap-class-name="wrapClassName"
:body-style="{paddingBottom: '16px'}"
@cancel="cancel"
@ok="save">
<template #title>
<div class="ele-cell">
<div class="ele-cell-content">拖拽弹窗</div>
<compress-outlined class="ele-modal-icon-compress"/>
<expand-outlined class="ele-modal-icon-expand"/>
</div>
</template>
<a-form
ref="form"
:model="form"
:rules="rules"
:label-col="{flex: '70px'}"
:wrapper-col="{flex: 'auto'}">
<a-form-item label="用户名" name="nickname" style="flex-wrap: nowrap;">
<a-input
allow-clear
placeholder="请输入用户名"
v-model:value="form.nickname"/>
</a-form-item>
<a-form-item label="性别" name="sex">
<a-select
allow-clear
placeholder="请选择性别"
v-model:value="form.sex">
<a-select-option value="男"></a-select-option>
<a-select-option value="女"></a-select-option>
</a-select>
</a-form-item>
<a-form-item label="手机号" name="phone" style="flex-wrap: nowrap;">
<a-input
allow-clear
placeholder="请输入手机号"
v-model:value="form.phone"/>
</a-form-item>
<a-form-item label="邮箱" name="email" style="flex-wrap: nowrap;">
<a-input
allow-clear
placeholder="请输入邮箱"
v-model:value="form.email"/>
</a-form-item>
<a-form-item label="个人简介" style="flex-wrap: nowrap;">
<a-textarea
:rows="4"
placeholder="请输入个人简介"
v-model:value="form.introduction"/>
</a-form-item>
</a-form>
</a-modal>
<!-- 弹窗2 -->
<a-modal
:width="400"
:mask="false"
v-model:visible="visible2"
wrap-class-name="ele-modal-movable ele-modal-resizable ele-modal-multiple ele-modal-wrap-fullscreen">
<template #title>
<div class="ele-cell">
<div class="ele-cell-content">默认全屏打开</div>
<compress-outlined class="ele-modal-icon-compress"/>
<expand-outlined class="ele-modal-icon-expand"/>
</div>
</template>
<div style="min-height: 66px;">我是弹窗2</div>
</a-modal>
<!-- 弹窗3 -->
<a-modal
:width="400"
:mask="false"
title="默认左下角打开"
class="demo-modal-eg3"
v-model:visible="visible3"
wrap-class-name="ele-modal-movable ele-modal-resizable ele-modal-multiple">
<div style="min-height: 66px;">我是弹窗3</div>
</a-modal>
</template>
<script>
import {ExpandOutlined, CompressOutlined} from '@ant-design/icons-vue';
export default {
name: 'MoreModal',
components: {ExpandOutlined, CompressOutlined},
data() {
return {
// 弹窗是否打开
visible: false,
visible2: false,
visible3: false,
// 表单数据
form: {},
// 是否显示遮罩层
mask: true,
// 是否允许拖出边界
moveOut: false,
// 表单验证规则
rules: {
nickname: [
{required: true, message: '请输入用户名', type: 'string', trigger: 'blur'}
],
sex: [
{required: true, message: '请选择性别', type: 'string', trigger: 'blur'}
],
phone: [
{required: true, message: '请输入手机号', type: 'string', trigger: 'blur'}
],
email: [
{required: true, message: '请输入邮箱', type: 'string', trigger: 'blur'}
]
}
};
},
computed: {
// 弹窗一的wrap-class
wrapClassName() {
return [
this.moveOut ? 'ele-modal-move-out' : 'ele-modal-movable',
this.mask ? '' : 'ele-modal-multiple',
'ele-modal-resizable'
].join(' ');
}
},
methods: {
/* 打开弹窗 */
openDialog() {
if (!this.visible) {
this.mask = true;
this.moveOut = false;
this.visible = true;
}
},
/* 打开允许拖出边界弹窗 */
openMoveOutDialog() {
this.moveOut = true;
if (!this.visible) {
this.mask = true;
this.visible = true;
}
},
/* 打开无遮罩层弹窗 */
openHideModalDialog() {
this.moveOut = false;
if (!this.visible) {
this.mask = false;
this.visible = true;
}
},
/* 弹窗关闭回调 */
cancel() {
this.$refs.form.clearValidate();
},
/* 保存编辑 */
save() {
this.$refs.form.validate().then(() => {
this.$message.success('保存成功');
}).catch(() => {
});
},
/* 打开默认全屏弹窗 */
openFullDialog() {
if (!this.visible2) {
this.visible2 = true;
}
},
/* 打开弹窗3 */
openMoreDialog() {
if (!this.visible3) {
this.visible3 = true;
}
}
}
}
</script>
<style>
.demo-modal-eg3 {
position: absolute;
right: 0;
bottom: 0;
top: auto;
left: auto;
margin: 0;
}
</style>

View File

@@ -0,0 +1,161 @@
<template>
<a-card title="标签" :bordered="false">
<div class="demo-tag-item">
<div class="demo-tag-label">预设颜色:</div>
<div class="demo-tag-content">
<ele-tag
:color="colors[type][0]"
:size="size">标签一
</ele-tag>
<ele-tag
:color="colors[type][1]"
:size="size">标签二
</ele-tag>
<ele-tag
:color="colors[type][2]"
:size="size">标签三
</ele-tag>
<ele-tag
:color="colors[type][3]"
:size="size">标签四
</ele-tag>
<ele-tag
:color="colors[type][4]"
:size="size">标签五
</ele-tag>
</div>
</div>
<div class="demo-tag-item">
<div class="demo-tag-label">圆角样式:</div>
<div class="demo-tag-content">
<ele-tag
:color="colors[type][0]"
:size="size"
shape="round">标签一
</ele-tag>
<ele-tag
:color="colors[type][1]"
:size="size"
shape="round">标签二
</ele-tag>
<ele-tag
:color="colors[type][2]"
:size="size"
shape="round">标签三
</ele-tag>
<ele-tag
:color="colors[type][3]"
:size="size"
shape="round">标签四
</ele-tag>
<ele-tag
:color="colors[type][4]"
:size="size"
shape="round">标签五
</ele-tag>
</div>
</div>
<div class="demo-tag-item">
<div class="demo-tag-label">圆形样式:</div>
<div class="demo-tag-content">
<ele-tag
:color="colors[type][0]"
:size="size"
shape="circle">1
</ele-tag>
<ele-tag
:color="colors[type][1]"
:size="size"
shape="circle">2
</ele-tag>
<ele-tag
:color="colors[type][2]"
:size="size"
shape="circle">3
</ele-tag>
<ele-tag
:color="colors[type][3]"
:size="size"
shape="circle">4
</ele-tag>
<ele-tag
:color="colors[type][4]"
:size="size"
shape="circle">5
</ele-tag>
</div>
</div>
<div class="demo-tag-item" style="align-items: flex-start;">
<div class="demo-tag-label">标签输入:</div>
<div class="demo-tag-content">
<ele-edit-tag
v-model:data="tags"
:color="colors[type][0]"
:size="size"/>
<div>{{ JSON.stringify(tags) }}</div>
</div>
</div>
<div class="demo-tag-item">
<div class="demo-tag-label">尺寸选择:</div>
<div class="demo-tag-content">
<a-radio-group
:options="sizes"
v-model:value="size"/>
</div>
</div>
<div class="demo-tag-item">
<div class="demo-tag-label">主题选择:</div>
<div class="demo-tag-content">
<a-radio-group
:options="types"
v-model:value="type"/>
</div>
</div>
</a-card>
</template>
<script>
export default {
name: 'MoreTag',
data() {
return {
size: 'mini',
sizes: [
{label: 'mini', value: 'mini'},
{label: 'small', value: 'small'},
{label: 'middle', value: 'middle'},
{label: 'large', value: 'large'}
],
colors: [
['', 'blue', 'green', 'orange', 'red'],
['#909399', '#1890ff', '#52c41a', '#fa8c16', '#f5222d']
],
types: [
{label: 'presets', value: 0},
{label: 'custom', value: 1},
],
type: 0,
tags: ['标签一', '标签二', '标签三']
};
}
}
</script>
<style scoped>
.demo-tag-item {
display: flex;
align-items: center;
}
.demo-tag-item .demo-tag-label {
padding-right: 16px;
}
.demo-tag-item .demo-tag-content {
flex: 1;
}
.demo-tag-item + .demo-tag-item {
margin-top: 22px;
}
</style>

View File

@@ -0,0 +1,334 @@
<template>
<div class="ele-body ele-body-card">
<a-row :gutter="16">
<a-col :lg="12" :md="24" :sm="24" :xs="24">
<a-card title="基础演示" :bordered="false">
<!-- 操作按钮 -->
<a-space style="margin-bottom: 16px;">
<a-button
type="primary"
:disabled="!ready1"
@click="play">播放
</a-button>
<a-button
type="primary"
:disabled="!ready1"
@click="pause">暂停
</a-button>
<a-button
type="primary"
:disabled="!ready1"
@click="replay">重新播放
</a-button>
<a-button
type="primary"
:disabled="!ready1"
@click="changeSrc">切换视频源
</a-button>
</a-space>
<!-- 播放器 -->
<xgplayer
:config="config1"
@player="onPlayer1"/>
</a-card>
</a-col>
<a-col :lg="12" :md="24" :sm="24" :xs="24">
<a-card title="显示弹幕" :bordered="false">
<!-- 操作按钮 -->
<a-space style="margin-bottom: 16px;">
<a-input
style="width: 160px;"
v-model:value="dmText"
placeholder="请输入弹幕内容"
:disabled="!ready2"/>
<a-button
type="primary"
:disabled="!ready2"
@click="shoot">发射
</a-button>
</a-space>
<!-- 播放器 -->
<xgplayer
:config="config2"
@player="onPlayer2"/>
</a-card>
</a-col>
</a-row>
</div>
</template>
<script>
import Xgplayer from 'xgplayer-vue';
export default {
name: 'ExtensionPlayer',
components: {Xgplayer},
data() {
return {
// 视频播放器一配置
config1: {
id: 'demoPlayer1',
lang: 'zh-cn',
fluid: true,
// 视频地址
url: 'https://s1.pstatp.com/cdn/expire-1-M/byted-player-videos/1.0.0/xgplayer-demo.mp4',
// 封面
poster: 'https://imgcache.qq.com/open_proj/proj_qcloud_v2/gateway/solution/general-video/css/img/scene/1.png',
// 开启倍速播放
playbackRate: [0.5, 1, 1.5, 2],
// 开启画中画
pip: true
},
// 视频播放器一实例
player1: null,
// 视频播放器一是否实例化完成
ready1: false,
// 视频播放器二配置
config2: {
id: 'demoPlayer2',
lang: 'zh-cn',
fluid: true,
url: 'https://blz-videos.nosdn.127.net/1/OverWatch/AnimatedShots/Overwatch_TheatricalTeaser_WeAreOverwatch_zhCN.mp4',
poster: 'https://imgcache.qq.com/open_proj/proj_qcloud_v2/gateway/solution/general-video/css/img/scene/1.png',
danmu: {
comments: [
{
id: '1',
start: 0,
txt: '空降',
duration: 15000,
color: true,
style: {
color: '#ffcd08',
fontSize: '20px'
}
},
{
id: '2',
start: 1500,
txt: '前方高能',
duration: 15000,
color: true,
style: {
color: '#ffcd08',
fontSize: '20px'
}
},
{
id: '3',
start: 3500,
txt: '弹幕护体',
duration: 15000,
color: true,
style: {
color: '#ffcd08',
fontSize: '20px'
}
},
{
id: '4',
start: 4500,
txt: '弹幕护体',
duration: 15000,
color: true,
style: {
color: '#ffcd08',
fontSize: '20px'
}
},
{
id: '5',
start: 6000,
txt: '前方高能',
duration: 15000,
color: true,
style: {
color: '#ffcd08',
fontSize: '20px'
}
},
{
id: '6',
start: 8500,
txt: '弹幕护体',
duration: 15000,
color: true,
style: {
color: '#ffcd08',
fontSize: '20px'
}
},
{
id: '7',
start: 10000,
txt: '666666666',
duration: 15000,
color: true,
style: {
color: '#ffcd08',
fontSize: '20px'
}
},
{
id: '8',
start: 12500,
txt: '前方高能',
duration: 15000,
color: true,
style: {
color: '#ffcd08',
fontSize: '20px'
}
},
{
id: '9',
start: 15500,
txt: '666666666',
duration: 15000,
color: true,
style: {
color: '#ffcd08',
fontSize: '20px'
}
},
{
id: '10',
start: 16500,
txt: '666666666',
duration: 15000,
color: true,
style: {
color: '#ffcd08',
fontSize: '20px'
}
},
{
id: '11',
start: 18000,
txt: '关弹幕,保智商',
duration: 15000,
color: true,
style: {
color: '#ffcd08',
fontSize: '20px'
}
},
{
id: '12',
start: 20500,
txt: '关弹幕,保智商',
duration: 15000,
color: true,
style: {
color: '#ffcd08',
fontSize: '20px'
}
},
{
id: '13',
start: 22000,
txt: '666666666',
duration: 15000,
color: true,
style: {
color: '#ffcd08',
fontSize: '20px'
}
},
{
id: '14',
start: 25500,
txt: '666666666',
duration: 15000,
color: true,
style: {
color: '#ffcd08',
fontSize: '20px'
}
},
{
id: '15',
start: 26000,
txt: '前方高能',
duration: 15000,
color: true,
style: {
color: '#ffcd08',
fontSize: '20px'
}
}
]
}
},
// 视频播放器二实例
player2: null,
// 视频播放器二是否实例化完成
ready2: false,
// 弹幕输入内容
dmText: ''
};
},
methods: {
/* 播放器一渲染完成 */
onPlayer1(e) {
this.player1 = e;
this.player1.on('play', () => {
this.ready1 = true;
});
},
/* 播放 */
play() {
if (this.player1.paused) {
this.player1.play();
}
},
/* 暂停 */
pause() {
if (!this.player1.paused) {
this.player1.pause();
}
},
/* 重新播放 */
replay() {
this.player1.replay();
},
/* 切换视频源 */
changeSrc() {
this.player1.src = 'https://blz-videos.nosdn.127.net/1/OverWatch/AnimatedShots/Overwatch_TheatricalTeaser_WeAreOverwatch_zhCN.mp4';
if (this.player1.paused) {
this.player1.play();
}
},
/* 播放器二渲染完成 */
onPlayer2(e) {
this.player2 = e;
this.player2.on('play', () => {
this.ready2 = true;
});
},
/* 发射弹幕 */
shoot() {
if (!this.dmText) {
this.$message.error('请输入弹幕内容');
return;
}
this.player2.danmu.sendComment({
id: new Date().getTime(),
duration: 15000,
color: true,
start: this.player2.currentTime * 1000,
txt: this.dmText,
style: {
color: '#fa1f41',
fontSize: '20px',
border: 'solid 1px #fa1f41'
}
});
this.dmText = '';
}
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,366 @@
<template>
<div class="ele-body ele-body-card">
<a-card title="打印当前页面" :bordered="false">
<div style="margin-bottom: 16px;">支持IE浏览器打印预览</div>
<a-space>
<a-button @click="print({})">打印当前页面</a-button>
<a-button @click="print({horizontal:true})">横屏打印</a-button>
<!--<a-button @click="print({blank:true})">新窗口打印</a-button>-->
<a-button @click="print({hide:['.demo-hide-1']})">打印时隐藏指定内容</a-button>
</a-space>
<div style="margin-top: 16px;">
<span class="ele-text-danger ele-printer-hide">此段内容会在所有打印时自动隐藏打印完自动复原</span>
<span class="ele-text-primary demo-hide-1">此段内容在指定打印时才隐藏</span>
</div>
</a-card>
<a-card title="打印任意内容" :bordered="false">
<a-space>
<a-button @click="printHtml()">打印任意内容</a-button>
<!--<a-button @click="printHtml(true)">新窗口打印</a-button>-->
<a-button @click="printAddHeader">设置页眉页脚</a-button>
<a-button @click="printImage">打印图片</a-button>
</a-space>
</a-card>
<a-card title="分页打印" :bordered="false">
<a-space>
<a-button @click="printPage()">分页打印</a-button>
<!--<a-button @click="printPage(true)">新窗口分页打印</a-button>-->
<a-button @click="printPageAddHeader">分页打印设置页眉页脚</a-button>
</a-space>
</a-card>
<a-card title="进阶示例" :bordered="false">
<a-space>
<a-button @click="printDataTable">打印数据表格</a-button>
<a-tooltip title="对于复杂的打印需求可以后端生成pdf给前端打印">
<a-button @click="printPdf">打印pdf</a-button>
</a-tooltip>
<a-button @click="printQrCode">打印条码</a-button>
<a-button @click="printTable">打印自定义表格</a-button>
</a-space>
</a-card>
</div>
</template>
<script>
import printer from 'ele-admin-pro/packages/printer.js';
export default {
name: 'ExtensionPrinter',
data() {
return {
users: [
{
key: 1,
username: '张小三',
amount: 18,
province: '浙江',
city: '杭州',
zone: '西湖区',
street: '西溪街道',
address: '西溪花园30栋1单元',
},
{
key: 2,
username: '李小四',
amount: 39,
province: '江苏',
city: '苏州',
zone: '姑苏区',
street: '丝绸路',
address: '天墅之城9幢2单元',
},
{
key: 3,
username: '王小五',
amount: 8,
province: '江西',
city: '南昌',
zone: '青山湖区',
street: '艾溪湖办事处',
address: '中兴和园1幢3单元',
},
{
key: 4,
username: '赵小六',
amount: 16,
province: '福建',
city: '泉州',
zone: '丰泽区',
street: '南洋街道',
address: '南洋村6幢1单元',
},
{
key: 5,
username: '孙小七',
amount: 12,
province: '湖北',
city: '武汉',
zone: '武昌区',
street: '武昌大道',
address: '两湖花园16幢2单元',
},
{
key: 6,
username: '周小八',
amount: 11,
province: '安徽',
city: '黄山',
zone: '黄山区',
street: '汤口镇',
address: '温泉村21号',
}
]
};
},
methods: {
/* 打印当前页面 */
print(options) {
printer.print(options);
},
/* 打印任意内容 */
printHtml(blank) {
printer.printHtml({
html: '<h1>Hello! Welcome To EleAdminPro!</h1>',
blank: blank
});
},
/* 打印设置页眉页脚 */
printAddHeader() {
printer.printHtml({
html: `
<div style="padding: 0 60px;">
<h1>Hello</h1><h1>Hello</h1><h1>Hello</h1><h1>Hello</h1><h1>Hello</h1><h1>Hello</h1><h1>Hello</h1>
<h1>Hello</h1><h1>Hello</h1><h1>Hello</h1><h1>Hello</h1><h1>Hello</h1><h1>Hello</h1><h1>Hello</h1>
<h1>Hello</h1><h1>Hello</h1><h1>Hello</h1><h1>Hello</h1><h1>Hello</h1><h1>Hello</h1><h1>Hello</h1>
<h1>Hello</h1><h1>Hello</h1><h1>Hello</h1><h1>Hello</h1><h1>Hello</h1><h1>Hello</h1><h1>Hello</h1>
<h1>Hello</h1><h1>Hello</h1><h1>Hello</h1><h1>Hello</h1><h1>Hello</h1><h1>Hello</h1><h1>Hello</h1>
<h1>Hello</h1><h1>Hello</h1><h1>Hello</h1><h1>Hello</h1><h1>Hello</h1><h1>Hello</h1><h1>Hello</h1>
</div>
`,
margin: 0,
header: `
<div style="text-align: center;font-size: 12px;padding: 15px 30px 25px;">
<span style="float: left;">我是页眉左侧</span>
<span>我是页眉</span>
<span style="float: right;">我是页眉右侧</span>
</div>`,
footer: `
<div style="text-align: center;font-size: 12px;padding: 15px 30px 25px;">
<span style="float: left;">我是页脚左侧</span>
<span>我是页脚</span>
<span style="float: right;">我是页脚右侧</span>
</div>`
});
},
/* 打印图片 */
printImage() {
printer.printHtml({
html: '<img src="https://cdn.eleadmin.com/20200610/LrCTN2j94lo9N7wEql7cBr1Ux4rHMvmZ.jpg" style="width: 100%;"/>'
});
},
/* 分页打印 */
printPage(blank) {
printer.printPage({
htmls: [
'<div>我是第一页</div>',
'<div>我是第二页</div>',
'<div>我是第三页</div>',
'<div>我是第四页</div>',
'<div>我是第五页</div>'
],
style: '<style>div{color: red;}</style>',
blank: blank
});
},
/* 分页打印设置页眉页脚 */
printPageAddHeader() {
printer.printPage({
htmls: [
'<span class="ele-printer-num">1/5</span><div>我是第一页</div>',
'<span class="ele-printer-num">2/5</span><div>我是第二页</div>',
'<span class="ele-printer-num">3/5</span><div>我是第三页</div>',
'<span class="ele-printer-num">4/5</span><div>我是第四页</div>',
'<span class="ele-printer-num">5/5</span><div>我是第五页</div>'
],
margin: 0,
padding: '30px 60px',
header: `
<div style="text-align: center;font-size: 12px;padding: 15px 30px;">
<span style="float: left;">我是页眉左侧</span>
<span>我是页眉</span>
<span style="float: right;">我是页眉右侧</span>
</div>`,
footer: `
<div style="text-align: center;font-size: 12px;padding: 15px 30px;">
<span style="float: left;">我是页脚左侧</span>
<span>我是页脚</span>
<span style="float: right;">我是页脚右侧</span>
</div>`,
style: `
<style>
.ele-printer-page-item > div { color: red; }
.ele-printer-num {
position: absolute;
top: -35px;
right: 10px;
font-size: 12px;
}
</style>`
});
},
/* 打印数据表格 */
printDataTable() {
let html = printer.makeTable(this.users, [
[
{field: 'username', width: 150, rowspan: 2, title: '联系人'},
{align: 'center', colspan: 3, title: '地址'},
{field: 'amount', width: 120, rowspan: 2, title: '金额', align: 'center'}
],
[
{field: 'province', width: 120, title: '省'},
{field: 'city', width: 120, title: '市'},
{
width: 200, title: '区', templet: (d) => {
return `<span style="color:red;">${d.zone}</span>`;
}
}
]
]);
printer.printHtml({html: '<p>提供数据和cols配置自动生成复杂表格非常的方便</p>' + html});
},
/* 打印pdf */
printPdf() {
printer.printPdf({url: 'https://cdn.eleadmin.com/20200610/20200708224450.pdf'});
},
/* 打印条码 */
printQrCode() {
let html = `
<div class="code-group">
<div class="code-group-title">EasyWeb授权凭证</div>
<div class="code-group-body">
<p>手机扫描右侧二维码,或登录</p>
<p>网站https://easyweb.vip</p>
<p>查询产品真伪</p>
<img src="https://cdn.eleadmin.com/20200610/20200708230820.png" width="70px" height="70px"/>
<span>515AE3X1</span>
</div>
</div>
<style>
.code-group {
display: inline-block;
border: 1px solid #ccc;
border-radius: 5px;
background-color: #fff;
}
.code-group-title {
border-bottom: 1px solid #ccc;
padding: 10px 15px;
text-align: center;
font-size: 18px;
}
.code-group-body {
text-align: center;
position: relative;
padding: 15px 115px 0 25px;
min-height: 90px;
}
.code-group-body > p {
margin: 0 0 13px 0;
font-size: 15px;
font-family: 幼圆;
color: #333;
font-weight: 600;
}
.code-group-body > img, .code-group-body > span {
position: absolute;
right: 25px;
top: 15px;
}
.code-group-body > span {
top: 90px;
}
</style>
`;
printer.printHtml({html: html});
},
/* 打印自定义表格 */
printTable() {
let html = `
<h2 style="text-align: center;color: #333;">软工xxxx班课程表</h2>
<table class="ele-printer-table">
<colgroup>
<col width="130px"/>
</colgroup>
<tr>
<th style="position: relative;">
<span style="position: absolute;right: 20px;top: 10px;line-height: normal;">星期</span>
<span style="position: absolute;left: 20px;bottom: 10px;line-height: normal;">时间</span>
<div style="height: 1px; width:140px;background-color: #000;position: absolute;left: 0;top: 0;transform: rotate(21deg);transform-origin: 0 0;"></div>
</th>
<th>周一</th>
<th>周二</th>
<th>周三</th>
<th>周四</th>
<th>周五</th>
</tr>
<tr>
<td>8:00-10:00</td>
<td>HTML5网页设计<br/>曲丽丽 - 441教室</td>
<td>数据库原理及应用<br/>严良 - 716机房</td>
<td>JavaSE初级程序设计<br/>肖萧 - 715机房</td>
<td></td>
<td>JavaScript程序设计<br/>董娜 - 733机房</td>
</tr>
<tr>
<td>10:30-12:30</td>
<td></td>
<td>JavaScript程序设计<br/>董娜 - 733机房</td>
<td></td>
<td>锋利的jQuery<br/>程咏 - 303教室</td>
<td>JavaEE应用开发<br/>周星 - 303教室</td>
</tr>
<tr>
<td colspan="6" style="height: auto;">午休</td>
</tr>
<tr>
<td>13:30-15:30</td>
<td>JavaSE初级程序设计<br/>肖萧 - 715机房</td>
<td></td>
<td>HTML5网页设计<br/>曲丽丽 - 441教室</td>
<td></td>
<td></td>
</tr>
<tr>
<td>16:00-18:00</td>
<td></td>
<td>JavaEE应用开发<br/>周星 - 303教室</td>
<td></td>
<td>数据库原理及应用<br/>严良 - 716机房</td>
<td></td>
</tr>
</table>
<style>
th, td {
text-align: center;
line-height: 32px;
}
td {
height: 110px;
}
</style>
`;
printer.printHtml({html: html, horizontal: true});
}
}
}
</script>
<style scoped>
.ant-space {
flex-wrap: wrap;
}
.ant-space .ant-btn {
margin-bottom: 8px;
}
</style>

401
src/views/form/advanced.vue Normal file
View File

@@ -0,0 +1,401 @@
<template>
<a-page-header :ghost="false" title="复杂表单">
<div class="ele-text-secondary">复杂表单常见于一次性输入和提交大批量数据的场景</div>
</a-page-header>
<div class="ele-body ele-body-card" style="padding-bottom: 48px;">
<a-form
ref="form"
:model="form"
:rules="rules"
:label-col="{md: {span: 6}, sm: {span: 24}}"
:wrapper-col="{md: {span: 18}, sm: {span: 24}}">
<a-card :bordered="false" title="仓库信息">
<a-row :gutter="16">
<a-col :lg="8" :md="12" :sm="24" :xs="24">
<a-form-item label="仓库名:" name="name">
<a-input
v-model:value="form.name"
placeholder="请输入仓库名"
allow-clear/>
</a-form-item>
</a-col>
<a-col :lg="8" :md="12" :sm="24" :xs="24">
<a-form-item label="仓库域名:" name="url">
<a-input
v-model:value="form.url"
placeholder="请输入"
allow-clear
addon-before="http://"
addon-after=".com"/>
</a-form-item>
</a-col>
<a-col :lg="8" :md="12" :sm="24" :xs="24">
<a-form-item label="仓库管理员:" name="administrator">
<a-select
v-model:value="form.administrator"
placeholder="请选择仓库管理员"
allow-clear>
<a-select-option value="1">SunSmile</a-select-option>
<a-select-option value="2">Jasmine</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :lg="8" :md="12" :sm="24" :xs="24">
<a-form-item label="审批人:" name="approver">
<a-select
v-model:value="form.approver"
placeholder="请选择审批人"
allow-clear>
<a-select-option value="1">SunSmile</a-select-option>
<a-select-option value="2">Jasmine</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :lg="8" :md="12" :sm="24" :xs="24">
<a-form-item label="生效日期:" name="datetime">
<a-range-picker
v-model:value="form.datetime"
class="ele-fluid">
<template #suffixIcon>
<calendar-outlined/>
</template>
</a-range-picker>
</a-form-item>
</a-col>
<a-col :lg="8" :md="12" :sm="24" :xs="24">
<a-form-item label="仓库类型:" name="type">
<a-select
v-model:value="form.type"
placeholder="请选择仓库类型"
allow-clear>
<a-select-option value="private">私密</a-select-option>
<a-select-option value="public">公开</a-select-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
</a-card>
<a-card :bordered="false" title="任务信息">
<a-row :gutter="16">
<a-col :lg="8" :md="12" :sm="24" :xs="24">
<a-form-item label="任务名:" name="task">
<a-input
v-model:value="form.task"
placeholder="请输入任务名"
allow-clear/>
</a-form-item>
</a-col>
<a-col :lg="8" :md="12" :sm="24" :xs="24">
<a-form-item label="任务表述:" name="description">
<a-input
v-model:value="form.description"
placeholder="请输入任务表述"
allow-clear/>
</a-form-item>
</a-col>
<a-col :lg="8" :md="12" :sm="24" :xs="24">
<a-form-item label="执行人:" name="executor">
<a-select
v-model:value="form.executor"
placeholder="请选择执行人"
allow-clear>
<a-select-option value="1">SunSmile</a-select-option>
<a-select-option value="2">Jasmine</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :lg="8" :md="12" :sm="24" :xs="24">
<a-form-item label="责任人:" name="officer">
<a-select
v-model:value="form.officer"
placeholder="请选择责任人"
allow-clear>
<a-select-option value="1">SunSmile</a-select-option>
<a-select-option value="2">Jasmine</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :lg="8" :md="12" :sm="24" :xs="24">
<a-form-item label="提醒时间:" name="reminder">
<a-time-picker
v-model:value="form.reminder"
placeholder="请选择提醒时间"
value-format="hh:mm:ss"
class="ele-fluid"/>
</a-form-item>
</a-col>
<a-col :lg="8" :md="12" :sm="24" :xs="24">
<a-form-item label="任务类型:" name="taskType">
<a-select
v-model:value="form.taskType"
placeholder="请选择任务类型"
allow-clear>
<a-select-option value="1">私密</a-select-option>
<a-select-option value="2">公开</a-select-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
</a-card>
<a-card :bordered="false" title="选择成员">
<!-- 数据表格 -->
<a-table
:data-source="users"
row-key="__index"
size="middle"
:pagination="false"
:scroll="{x: 'max-content'}">
<a-table-column
data-index="__index"
align="center"
:width="58"/>
<a-table-column
title="用户名"
data-index="name">
<template #default="{text,index}">
<a-input
v-if="index===editIndex"
v-model:value="editRow.name"
placeholder="请输入用户名"/>
<span v-else>{{ text }}</span>
</template>
</a-table-column>
<a-table-column
title="工号"
data-index="number">
<template #default="{text,index}">
<a-input
v-if="index===editIndex"
v-model:value="editRow.number"
placeholder="请输入工号"/>
<span v-else>{{ text }}</span>
</template>
</a-table-column>
<a-table-column
title="所属部门"
data-index="department">
<template #default="{text,index}">
<a-select
v-if="index===editIndex"
v-model:value="editRow.department"
placeholder="请选择部门">
<a-select-option value="研发部">研发部</a-select-option>
<a-select-option value="测试部">测试部</a-select-option>
<a-select-option value="产品部">产品部</a-select-option>
</a-select>
<span v-else>{{ text }}</span>
</template>
</a-table-column>
<a-table-column
title="操作"
key="action"
align="center"
:width="160">
<template #default="{index,record}">
<a-space v-if="index===editIndex">
<a @click="onSave(record, index)">保存</a>
<a-divider type="vertical"/>
<a @click="onCancel(record, index)">取消</a>
</a-space>
<a-space v-else>
<a @click="onEdit(record, index)">修改</a>
<a-divider type="vertical"/>
<a-popconfirm
title="确定要删除此用户吗?"
@confirm="onRemove(record,index)">
<a class="ele-text-danger">删除</a>
</a-popconfirm>
</a-space>
</template>
</a-table-column>
</a-table>
<a-button
block
type="dashed"
style="margin-top: 16px;"
@click="addRow">
<template #icon>
<plus-outlined/>
</template>
<span>新增成员</span>
</a-button>
</a-card>
<!-- 底部工具栏 -->
<div class="ele-bottom-tool">
<div v-if="validMsg" class="ele-text-danger">
<close-circle-outlined/>
<span>{{ validMsg }}</span>
</div>
<div class="ele-bottom-tool-actions">
<a-button
type="primary"
:loading="loading"
@click="submit">提交
</a-button>
</div>
</div>
</a-form>
</div>
</template>
<script>
import {
CalendarOutlined,
PlusOutlined,
CloseCircleOutlined
} from '@ant-design/icons-vue';
export default {
name: 'FormAdvanced',
components: {
CalendarOutlined,
PlusOutlined,
CloseCircleOutlined
},
data() {
return {
// 加载状态
loading: false,
// 表单数据
form: {
name: '',
url: '',
datetime: [],
task: '',
description: '',
reminder: ''
},
// 表单验证规则
rules: {
name: [
{required: true, message: '请输入仓库名', type: 'string', trigger: 'blur'}
],
url: [
{required: true, message: '请输入仓库域名', type: 'string', trigger: 'blur'}
],
administrator: [
{required: true, message: '请选择仓库管理员', type: 'string', trigger: 'blur'}
],
approver: [
{required: true, message: '请选择审批人', type: 'string', trigger: 'blur'}
],
datetime: [
{required: true, message: '请选择生效日期', type: 'array', trigger: 'blur'}
],
type: [
{required: true, message: '请选择仓库类型', type: 'string', trigger: 'blur'}
],
task: [
{required: true, message: '请输入任务名', type: 'string', trigger: 'blur'}
],
description: [
{required: true, message: '请输入任务表述', type: 'string', trigger: 'blur'}
],
executor: [
{required: true, message: '请选择执行人', type: 'string', trigger: 'blur'}
],
officer: [
{required: true, message: '请选择责任人', type: 'string', trigger: 'blur'}
],
reminder: [
{required: true, message: '请选择提醒时间', type: 'string', trigger: 'blur'}
],
taskType: [
{required: true, message: '请选择任务类型', type: 'string', trigger: 'blur'}
]
},
// 用户列表
users: [
{__index: 1, name: 'John Brown', number: '00001', department: '研发部'},
{__index: 2, name: 'Jim Green', number: '00002', department: '产品部'},
{__index: 3, name: 'Joe Black', number: '00003', department: '产品部'}
],
// 表单验证失败提示信息
validMsg: '',
// 表格编辑行索引
editIndex: null,
// 表格编辑行数据
editRow: {}
};
},
methods: {
/* 表单提交 */
submit() {
this.$refs.form.validate().then(() => {
this.validMsg = '';
this.loading = true;
setTimeout(() => {
this.loading = false;
this.$message.success('提交成功');
}, 1500);
}).catch((e) => {
this.validMsg = ` 共有 ${e.errorFields.length} 项校验不通过`;
});
},
/* 添加一行 */
addRow() {
if (this.users.length && this.users[this.users.length - 1].__is_add) {
return;
}
this.editRow = {
__is_add: true, // 此字段标识为添加状态
__index: this.users.length + 1 // 此字段标识索引
};
this.editIndex = this.users.length;
this.users.push(this.editRow);
},
/* 修改行 */
onEdit(row, index) {
if (this.users.length && this.users[this.users.length - 1].__is_add) {
return;
}
this.editIndex = index;
this.editRow = Object.assign({}, row);
},
/* 删除行 */
onRemove(row, index) {
this.users[this.editIndex].__is_add = false;
this.users.splice(index, 1); // 删除数据
// 重置__index
this.users = this.users.map((d, index) => {
return Object.assign({}, d, {
__index: index + 1
});
})
// 如果需要请求接口删除可以在这里写
},
/* 保存编辑 */
onSave(row, index) {
if (!this.editRow.name) {
this.$message.error('请输入用户');
return;
}
if (!this.editRow.number) {
this.$message.error('请输入工号');
return;
}
if (!this.editRow.department) {
this.$message.error('请选择部门');
return;
}
this.users[index] = Object.assign({}, this.editRow, {
__is_add: null
});
this.editIndex = null;
this.editRow = {};
// 如果需要请求接口保存可以在这里写
},
/* 取消编辑 */
onCancel(row, index) {
if (row.__is_add) {
this.users.splice(index, 1);
}
this.editIndex = null;
this.editRow = {};
}
}
}
</script>
<style scoped>
</style>

185
src/views/form/basic.vue Normal file
View File

@@ -0,0 +1,185 @@
<template>
<a-page-header :ghost="false" title="基础表单">
<div class="ele-text-secondary">表单页用于向用户收集或验证信息基础表单常见于数据项较少的表单场景</div>
</a-page-header>
<div class="ele-body">
<a-card :bordered="false">
<a-form
ref="form"
:model="form"
:rules="rules"
style="max-width: 800px;margin: 0 auto;"
:label-col="{md: {span: 6}, sm: {span: 24}}"
:wrapper-col="{md: {span: 18}, sm: {span: 24}}">
<a-form-item label="标题:" name="title">
<a-input
v-model:value="form.title"
placeholder="请输入标题"
allow-clear/>
</a-form-item>
<a-form-item label="起止日期:" name="datetime">
<a-range-picker
v-model:value="form.datetime"
value-format="YYYY-MM-DD"
class="ele-fluid">
<template #suffixIcon>
<calendar-outlined/>
</template>
</a-range-picker>
</a-form-item>
<a-form-item label="目标描述:" name="goal">
<a-textarea
v-model:value="form.goal"
placeholder="请输入目标描述"
:rows="4"/>
</a-form-item>
<a-form-item label="衡量标准:" name="standard">
<a-textarea
v-model:value="form.standard"
placeholder="请输入衡量标准"
:rows="4"/>
</a-form-item>
<a-form-item label="地点:" name="address">
<a-select
v-model:value="form.address"
placeholder="请选择地点"
allow-clear>
<a-select-option value="1">地点一</a-select-option>
<a-select-option value="2">地点二</a-select-option>
<a-select-option value="3">地点三</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="邀评人:">
<a-select
v-model:value="form.invites"
placeholder="请选择邀评人"
mode="multiple"
allow-clear>
<a-select-option
v-for="item in users"
:key="item.id"
:value="item.id">{{ item.name }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="权重:">
<a-space>
<a-input-number
v-model:value="form.weight"
:min="0"
:max="100"/>
<span>%</span>
</a-space>
</a-form-item>
<a-form-item label="目标公开:">
<a-radio-group
v-model:value="form.publicType"
name="publicType">
<a-radio :value="1">公开</a-radio>
<a-radio :value="2">部分公开</a-radio>
<a-radio :value="3">不公开</a-radio>
</a-radio-group>
<div style="margin-top: 12px;">
<a-input
v-if="form.publicType===2"
placeholder="公开给"/>
</div>
<div class="ele-text-secondary" style="margin-top: 12px;">客户邀评人默认被分享</div>
</a-form-item>
<a-form-item :wrapper-col="{md: {offset: 6}}">
<a-space size="middle">
<a-button @click="closePage">关闭</a-button>
<a-button
type="primary"
@click="submit"
:loading="loading">提交
</a-button>
</a-space>
</a-form-item>
</a-form>
</a-card>
</div>
</template>
<script>
import {CalendarOutlined} from '@ant-design/icons-vue';
import {finishPageTab} from '@/utils/page-tab-util';
export default {
name: 'FormBasic',
components: {CalendarOutlined},
data() {
return {
// 加载状态
loading: false,
// 表单数据
form: {
title: '',
datetime: [],
goal: '',
standard: '',
invites: [],
weight: 0,
publicType: 1
},
// 表单验证规则
rules: {
title: [
{required: true, message: '请输入标题', trigger: 'blur'}
],
datetime: [
{required: true, message: '请选择起止日期', type: 'array', trigger: 'blur'}
],
goal: [
{required: true, message: '请输入目标描述', trigger: 'blur'}
],
standard: [
{required: true, message: '请输入衡量标准', trigger: 'blur'}
],
address: [
{required: true, message: '请选择地点', type: 'string', trigger: 'blur'}
]
},
// 邀评人下拉列表数据
users: [
{id: 1, name: 'SunSmile'},
{id: 2, name: '你的名字很好听'},
{id: 3, name: '全村人的希望'},
{id: 4, name: 'Jasmine'},
{id: 5, name: '酷酷的大叔'}
]
};
},
methods: {
/* 提交 */
submit() {
this.$refs.form.validate().then(() => {
this.loading = true;
setTimeout(() => {
this.loading = false;
// 重置表单
this.form = {
title: '',
datetime: [],
goal: '',
standard: '',
address: '',
invites: [],
weight: 0,
publicType: 1
};
this.$message.success('提交成功');
}, 1500);
}).catch(() => {
});
},
/* 关闭当前页面 */
closePage() {
finishPageTab();
}
}
}
</script>
<style scoped>
</style>

228
src/views/form/step.vue Normal file
View File

@@ -0,0 +1,228 @@
<template>
<a-page-header :ghost="false" title="分步表单">
<div class="ele-text-secondary">将一个冗长或用户不熟悉的表单任务分成多个步骤指导用户完成</div>
</a-page-header>
<div class="ele-body">
<a-card :bordered="false">
<div style="max-width: 800px;margin: 0 auto;">
<a-steps :current="active" style="margin: 10px 0 30px 0;">
<a-step title="第一步" description="填写转账信息"/>
<a-step title="第二步" description="确认转账信息"/>
<a-step title="第三步" description="转账成功"/>
</a-steps>
<!-- 第一步 -->
<a-form
v-if="active===0"
ref="form1"
:model="form1"
:rules="rules1"
:label-col="{md: {span: 6}, sm: {span: 24}}"
:wrapper-col="{md: {span: 16}, sm: {span: 24}}">
<a-form-item label="付款账户:" name="account">
<a-select
v-model:value="form1.account"
placeholder="请选择付款账户"
allow-clear>
<a-select-option value="eleadmin@eclouds.com">eleadmin@eclouds.com</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="收款账户:" name="receiver">
<a-input
v-model:value="form1.receiver"
placeholder="请输入收款账户"
allow-clear>
<template #addonBefore>
<a-select
v-model:value="form1.pay"
style="width: 100px;margin: -5px -12px;">
<a-select-option value="alipay">支付宝</a-select-option>
<a-select-option value="wxpay">微信</a-select-option>
</a-select>
</template>
</a-input>
</a-form-item>
<a-form-item label="收款人姓名:" name="name">
<a-input
v-model:value="form1.name"
placeholder="请输入收款人姓名"
allow-clear/>
</a-form-item>
<a-form-item label="转账金额:" name="amount">
<a-input
v-model:value.number="form1.amount"
placeholder="请输入转账金额"
prefix="¥"
allow-clear/>
</a-form-item>
<a-form-item :wrapper-col="{sm: {offset: 6}}">
<a-button
type="primary"
@click="submit1"
:loading="loading1">下一步
</a-button>
</a-form-item>
</a-form>
<!-- 第二步 -->
<a-form
v-if="active===1"
ref="form2"
:model="form2"
:rules="rules2"
:label-col="{md: {span: 6}, sm: {span: 24}}"
:wrapper-col="{md: {span: 16}, sm: {span: 24}}"
class="ele-form-detail">
<a-alert
message="确认转账后,资金将直接打入对方账户,无法退回。"
type="info"
show-icon
closable/>
<a-form-item
label="付款账户:"
style="margin-top: 24px;">{{ form1.account }}
</a-form-item>
<a-form-item label="收款账户:">{{ form1.receiver }}</a-form-item>
<a-form-item label="收款人姓名:">{{ form1.name }}</a-form-item>
<a-form-item label="转账金额:">
<span style="font-size: 24px;line-height: 1;">{{ form1.amount }}</span>
</a-form-item>
<a-divider style="margin: 20px 0 30px 0;"/>
<a-form-item label="支付密码:" name="password">
<a-input-password
v-model:value="form2.password"
placeholder="请输入支付密码"/>
</a-form-item>
<a-form-item
:wrapper-col="{sm: {offset: 6}}"
style="margin-top: 24px;">
<a-space size="middle">
<a-button
type="primary"
@click="submit2"
:loading="loading2">下一步
</a-button>
<a-button @click="active=0">上一步</a-button>
</a-space>
</a-form-item>
</a-form>
<!-- 第三步 -->
<div v-if="active===2">
<a-result
status="success"
title="操作成功"
sub-title="预计两小时内到账">
<a-form
:label-col="{md: {span: 6}, sm: {span: 24}}"
:wrapper-col="{md: {span: 16}, sm: {span: 24}}"
class="ele-form-detail">
<a-form-item label="付款账户:">{{ form1.account }}</a-form-item>
<a-form-item label="收款账户:">{{ form1.receiver }}</a-form-item>
<a-form-item label="收款人姓名:">{{ form1.name }}</a-form-item>
<a-form-item label="转账金额:">
<span style="font-size: 24px;line-height: 1;">{{ form1.amount }}</span>
</a-form-item>
</a-form>
<template #extra>
<a-space size="middle">
<a-button
type="primary"
@click="active=0">再转一笔
</a-button>
<a-button>查看账单</a-button>
</a-space>
</template>
</a-result>
</div>
</div>
<div v-if="active===0">
<a-divider style="margin: 35px 0 25px 0;"/>
<a-alert type="info">
<template #description>
<h6 style="margin: 5px 0 15px 0;">说明</h6>
<h6 style="margin-bottom: 10px;">转账到支付宝</h6>
<p style="margin-bottom: 15px;">如果需要这里可以放一些关于产品的常见问题说明如果需要
这里可以放一些关于产品的常见问题说明如果需要这里可以放一些关于产品的常见问题说明</p>
<h6 style="margin-bottom: 10px;">转账到微信</h6>
<p style="margin-bottom: 15px;">如果需要这里可以放一些关于产品的常见问题说明如果需要
这里可以放一些关于产品的常见问题说明如果需要这里可以放一些关于产品的常见问题说明</p>
</template>
</a-alert>
</div>
</a-card>
</div>
</template>
<script>
export default {
name: 'FormStep',
data() {
return {
// 选中步骤
active: 0,
// 表单一数据
form1: {
account: 'eleadmin@eclouds.com',
receiver: 'test@example.com',
pay: 'alipay',
name: 'Alex',
amount: 500
},
// 表单一验证规则
rules1: {
account: [
{required: true, message: '请选择付款账户', type: 'string', trigger: 'blur'}
],
receiver: [
{required: true, message: '请输入收款账户', type: 'string', trigger: 'blur'}
],
name: [
{required: true, message: '请输入收款人姓名', type: 'string', trigger: 'blur'}
],
amount: [
{required: true, message: '请输入合法金额数字', type: 'number', trigger: 'blur'}
]
},
// 步骤一提交状态
loading1: false,
// 表单二数据
form2: {
password: '123456',
},
// 表单二验证规则
rules2: {
password: [
{required: true, message: '请输入支付密码', trigger: 'blur'}
]
},
// 步骤二提交状态
loading2: false
};
},
methods: {
/* 步骤一提交 */
submit1() {
this.$refs.form1.validate().then(() => {
this.loading1 = true;
setTimeout(() => {
this.loading1 = false;
this.active = 1;
}, 300);
}).catch(() => {
});
},
/* 步骤二提交 */
submit2() {
this.$refs.form2.validate().then(() => {
this.loading2 = true;
setTimeout(() => {
this.loading2 = false;
this.active = 2;
}, 300);
}).catch(() => {
});
}
}
}
</script>
<style scoped>
</style>

417
src/views/list/advanced.vue Normal file
View File

@@ -0,0 +1,417 @@
<template>
<div class="ele-body ele-body-card">
<a-card :bordered="false">
<a-row>
<a-col :md="8" :sm="24" :xs="24">
<div class="ele-text-center">
<div style="margin-bottom: 8px;">进行中的任务</div>
<h2>10 个任务</h2>
</div>
</a-col>
<a-col :md="8" :sm="24" :xs="24">
<div class="ele-text-center">
<div style="margin-bottom: 8px;">剩余任务</div>
<h2>3 个任务</h2>
</div>
</a-col>
<a-col :md="8" :sm="24" :xs="24">
<div class="ele-text-center">
<div style="margin-bottom: 8px;">任务总耗时</div>
<h2>120 个小时</h2>
</div>
</a-col>
</a-row>
</a-card>
<a-card :bordered="false">
<!-- 头部工具栏 -->
<div class="ele-table-tool">
<h6 class="ele-table-tool-title">复杂列表</h6>
<a-space size="middle">
<a-radio-group
v-model:value="search.state"
@change="query">
<a-radio-button value="0">全部</a-radio-button>
<a-radio-button value="1">进行中</a-radio-button>
<a-radio-button value="2">已完成</a-radio-button>
</a-radio-group>
<a-input-search
v-model:value="search.keyword"
placeholder="请输入"
style="width: 200px"
@search="query">
<template #enterButton>
<a-button>
<template #icon>
<search-outlined/>
</template>
</a-button>
</template>
</a-input-search>
</a-space>
</div>
<a-button
block
type="dashed"
@click="openEdit()">
<template #icon>
<plus-outlined/>
</template>
<span>添加</span>
</a-button>
<!-- 数据列表 -->
<a-spin :spinning="loading">
<div v-for="(item,index) in data" :key="index">
<div class="basic-list-item">
<div class="ele-cell">
<a-avatar
shape="square"
:size="60"
:src="item.cover"/>
<div class="ele-cell-content">
<div class="ele-cell-title">{{ item.title }}</div>
<div class="ele-cell-desc">{{ item.content }}</div>
</div>
</div>
<div class="basic-list-item-owner">
<div>发布人</div>
<div class="ele-text-secondary">{{ item.user }}</div>
</div>
<div class="basic-list-item-time">
<div>开始时间</div>
<div class="ele-text-secondary">{{ item.time }}</div>
</div>
<div class="basic-list-item-progress">
<a-progress
:percent="item.progress"
:status="item.status"/>
</div>
<div class="basic-list-item-tool">
<a-space>
<a @click="openEdit(item)">编辑</a>
<a-divider type="vertical"/>
<a-dropdown>
<a>
<span>更多<down-outlined class="ele-text-small"/></span>
</a>
<template #overlay>
<a-menu @click="(obj) => dropClick(obj.key, item)">
<a-menu-item key="share">分享</a-menu-item>
<a-menu-item key="remove">删除</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</a-space>
</div>
</div>
<a-divider/>
</div>
<div class="ele-text-center" style="margin-top: 18px;">
<a-pagination
v-model:current="page.page"
:total="count"
show-quick-jumper
@change="query"/>
</div>
</a-spin>
</a-card>
<!-- 编辑弹窗 -->
<a-modal
:width="460"
v-model:visible="showEdit"
:confirm-loading="editLoading"
:title="form.id?'任务编辑':'任务添加'"
:body-style="{paddingBottom: '8px'}"
@ok="save">
<a-form
ref="editForm"
:model="form"
:rules="rules"
:label-col="{md: {span: 5}, sm: {span: 24}}"
:wrapper-col="{md: {span: 19}, sm: {span: 24}}">
<a-form-item label="任务名称:" name="title">
<a-input
v-model:value="form.title"
placeholder="请输入任务名称"
allow-clear/>
</a-form-item>
<a-form-item label="开始时间:" name="time">
<a-date-picker
v-model:value="form.time"
placeholder="请选择开始时间"
show-time
value-format="YYYY-MM-DD hh:mm:ss"
class="ele-fluid"/>
</a-form-item>
<a-form-item label="负责人:" name="user">
<a-select
v-model:value="form.user"
placeholder="请选择负责人"
allow-clear>
<a-select-option value="SunSmile">SunSmile</a-select-option>
<a-select-option value="Pojin">Pojin</a-select-option>
<a-select-option value="SuperWill">SuperWill</a-select-option>
<a-select-option value="Jasmine">Jasmine</a-select-option>
<a-select-option value="Vast">Vast</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="任务描述:">
<a-textarea
v-model:value="form.content"
placeholder="请输入任务描述"
:rows="4"/>
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script>
import {createVNode} from 'vue';
import {
SearchOutlined,
PlusOutlined,
DownOutlined,
ExclamationCircleOutlined
} from '@ant-design/icons-vue';
export default {
name: 'ListAdvanced',
components: {
SearchOutlined,
PlusOutlined,
DownOutlined
},
data() {
return {
// 列表加载状态
loading: false,
// 列表数据
data: [
{
id: 1,
title: 'ElementUI',
time: '2020-06-13 08:33',
user: 'SunSmile',
progress: 87,
content: 'Element一套为开发者、设计师和产品经理准备的基于 Vue 2.0 的组件库,提供了配套设计资源,帮助你的网站快速成型。',
cover: 'https://cdn.eleadmin.com/20200609/c184eef391ae48dba87e3057e70238fb.jpg'
},
{
id: 2,
title: 'Vue.js',
time: '2020-06-13 06:40',
user: 'Pojin',
progress: 100,
content: 'Vue 是一套用于构建用户界面的渐进式框架。与其它大型框架不同的是Vue 被设计为可以自底向上逐层应用。',
cover: 'https://cdn.eleadmin.com/20200609/b6a811873e704db49db994053a5019b2.jpg'
},
{
id: 3,
title: 'Vuex',
time: '2020-06-13 04:40',
user: 'SuperWill',
progress: 75,
content: 'Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。',
cover: 'https://cdn.eleadmin.com/20200609/948344a2a77c47a7a7b332fe12ff749a.jpg'
},
{
id: 4,
title: 'Vue Router',
time: '2020-06-13 02:40',
user: 'Jasmine',
progress: 65,
content: 'Vue Router 是 Vue.js 官方的路由管理器。它和 Vue.js 的核心深度集成,让构建单页面应用变得易如反掌。',
cover: 'https://cdn.eleadmin.com/20200609/f6bc05af944a4f738b54128717952107.jpg'
},
{
id: 5,
title: 'Sass',
time: '2020-06-13 00:40',
user: 'Vast',
progress: 45,
status: 'exception',
content: 'Sass 是世界上最成熟、稳定、强大的专业级 CSS 扩展语言。',
cover: 'https://cdn.eleadmin.com/20200609/2d98970a51b34b6b859339c96b240dcd.jpg'
}
],
// 搜索表单
search: {
state: '0',
keyword: ''
},
// 分页参数
page: {
page: 1,
limit: 5
},
// 数据总数
count: 100,
// 是否显示编辑弹窗
showEdit: false,
// 编辑弹窗数据
form: {},
// 编辑弹窗表单验证规则
rules: {
title: [
{required: true, message: '请输入任务名称', type: 'string', trigger: 'blur'}
],
time: [
{required: true, message: '请选择开始时间', type: 'string', trigger: 'blur'}
],
user: [
{required: true, message: '请选择负责人', type: 'string', trigger: 'blur'}
]
},
// 编辑表单提交状态
editLoading: false
};
},
methods: {
/* 查询数据 */
query() {
this.loading = true;
setTimeout(() => {
this.loading = false;
}, 300);
},
/* 显示编辑弹窗 */
openEdit(row) {
if (row) {
this.form = Object.assign({}, row);
} else {
this.form = {};
}
this.showEdit = true;
this.$nextTick(() => {
this.$refs.editForm.clearValidate();
});
},
/* 保存编辑 */
save() {
this.$refs.editForm.validate().then(() => {
this.editLoading = true;
setTimeout(() => {
this.editLoading = false;
this.showEdit = false;
this.$message.success('保存成功');
if (this.form.id) { // 保存修改
Object.assign(this.data.filter(d => d.id === this.form.id)[0], this.form);
} else { // 保存添加
this.data.push(Object.assign({
id: new Date().getTime(),
cover: 'https://cdn.eleadmin.com/20200610/RZ8FQmZfHkcffMlTBCJllBFjEhEsObVo.jpg'
}, this.form));
}
}, 300);
}).catch(() => {
});
},
/* 下拉菜单点击事件 */
dropClick(command, item) {
console.log(item);
if (command === 'remove') { // 删除
this.$confirm({
title: '提示',
content: '确定删除该任务吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
this.$message.success('删除成功');
}
});
} else if (command === 'share') {
this.$message.success('点击了分享');
}
}
}
}
</script>
<style scoped>
/** 列表样式 */
.basic-list-item {
display: flex;
align-items: center;
padding: 16px 8px;
}
.basic-list-item .ele-cell {
flex: 1;
}
.basic-list-item .basic-list-item-owner {
width: 80px;
padding: 0 16px;
flex-shrink: 0;
}
.basic-list-item .basic-list-item-time {
width: 160px;
padding: 0 16px;
flex-shrink: 0;
}
.basic-list-item .ele-text-secondary {
margin-top: 8px;
}
.basic-list-item .basic-list-item-progress {
width: 180px;
flex-shrink: 0;
}
.basic-list-item .basic-list-item-tool {
padding: 0 16px;
}
/* 响应式 */
@media screen and (max-width: 992px) {
.basic-list-item .basic-list-item-owner,
.basic-list-item .basic-list-item-time,
.basic-list-item .basic-list-item-progress,
.basic-list-item .basic-list-item-tool {
width: auto;
padding: 0 12px;
}
.basic-list-item .basic-list-item-progress {
width: 100px;
}
}
@media screen and (max-width: 768px) {
.basic-list-item {
display: block;
}
.basic-list-item .basic-list-item-owner,
.basic-list-item .basic-list-item-time,
.basic-list-item .basic-list-item-progress,
.basic-list-item .basic-list-item-tool {
width: auto;
padding: 8px 0 0 0;
}
.basic-list-item .ele-text-secondary {
margin-top: 0;
padding-left: 16px;
}
.basic-list-item .basic-list-item-owner > div,
.basic-list-item .basic-list-item-time > div {
display: inline-block;
}
.basic-list-item .basic-list-item-tool {
text-align: right;
}
.ele-table-tool .ant-input-search {
display: none;
}
.ele-table-tool :deep(.ant-space-item) {
margin: 0 !important;
}
}
</style>

335
src/views/list/basic.vue Normal file
View File

@@ -0,0 +1,335 @@
<template>
<div class="ele-body ele-body-card">
<a-card :bordered="false" :body-style="{paddingBottom: 0}">
<!-- 搜索表单 -->
<a-form
:model="where"
:label-col="{md: {span: 6}, sm: {span: 24}}"
:wrapper-col="{md: {span: 18}, sm: {span: 24}}">
<a-row>
<a-col :lg="8" :md="12" :sm="24" :xs="24">
<a-form-item label="规则名称:">
<a-input
v-model:value="where.name"
placeholder="请输入"
allow-clear/>
</a-form-item>
</a-col>
<a-col :lg="8" :md="12" :sm="24" :xs="24">
<a-form-item label="状态:">
<a-select
v-model:value="where.state"
placeholder="请选择"
allow-clear>
<a-select-option value="0">运行中</a-select-option>
<a-select-option value="1">已上线</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col v-if="searchExpand" :lg="8" :md="12" :sm="24" :xs="24">
<a-form-item label="描述:">
<a-input
v-model:value="where.desc"
placeholder="请输入"
allow-clear/>
</a-form-item>
</a-col>
<a-col v-if="searchExpand" :lg="8" :md="12" :sm="24" :xs="24">
<a-form-item label="上次调度时间:">
<a-date-picker
v-model:value="where.lastTime"
placeholder="请选择"
show-time
value-format="YYYY-MM-DD hh:mm:ss"
class="ele-fluid"/>
</a-form-item>
</a-col>
<a-col v-if="searchExpand" :lg="8" :md="12" :sm="24" :xs="24">
<a-form-item label="服务调用次数:">
<a-input-number
v-model:value="where.callTimes"
placeholder="请输入"
:min="0"
:max="100"
class="ele-fluid"/>
</a-form-item>
</a-col>
<a-col :lg="8" :md="12" :sm="24" :xs="24">
<a-form-item
class="ele-text-right"
:wrapper-col="{span: 24}">
<a-space>
<a-button type="primary" @click="reload">查询</a-button>
<a-button @click="reset">重置</a-button>
<a @click="searchExpand = !searchExpand">
<span v-if="searchExpand">收起 <up-outlined class="ele-text-small"/></span>
<span v-else>展开 <down-outlined class="ele-text-small"/></span>
</a>
</a-space>
</a-form-item>
</a-col>
</a-row>
</a-form>
</a-card>
<a-card :bordered="false">
<a-alert
type="info"
show-icon
style="margin-bottom: 16px;">
<template #message>
<span>已选择 <b class="ele-text-primary">{{ choose.length }}</b> <em/></span>
<span>服务调用次数总计 <b>{{ sumTimes }} </b><em/><em/></span>
<a @click="clearChoose">清空</a>
</template>
</a-alert>
<!-- 表格 -->
<ele-pro-table
ref="table"
row-key="id"
title="基础列表"
:datasource="url"
:columns="columns"
:where="where"
:custom-row="customRow"
v-model:selection="choose"
:scroll="{x: 'max-content'}">
<template #toolkit>
<a-space size="middle">
<a-button
type="primary"
@click="openEdit()"
class="ele-btn-icon">
<template #icon>
<plus-outlined/>
</template>
<span>新建</span>
</a-button>
<a-dropdown :disabled="choose.length===0">
<a-button class="ele-btn-icon">
<span>批量操作 <down-outlined/></span>
</a-button>
<template #overlay>
<a-menu @click="onDropClick">
<a-menu-item key="del">批量删除</a-menu-item>
<a-menu-item key="check">批量审批</a-menu-item>
</a-menu>
</template>
</a-dropdown>
<a-divider type="vertical"/>
</a-space>
</template>
<template #action="{ record }">
<a-space>
<a @click.stop="openEdit(record)">配置</a>
<a-divider type="vertical"/>
<a @click.stop="doRSS(record)">订阅警报</a>
</a-space>
</template>
<template #state="{ record }">
<a-badge
:status="['processing', 'success', 'error', 'default'][record.state]"
:text="['运行中', '已上线', '异常', '关闭'][record.state]"/>
</template>
</ele-pro-table>
</a-card>
<!-- 编辑弹窗 -->
<a-modal
v-model:visible="showEdit"
:title="form.id?'配置规则':'新建规则'"
:width="520"
:confirm-loading="loading"
:body-style="{paddingBottom: '16px'}"
@ok="save">
<a-form
ref="form"
:model="form"
:rules="rules"
:label-col="{md: {span: 5}, sm: {span: 24}}"
:wrapper-col="{md: {span: 19}, sm: {span: 24}}">
<a-form-item label="规则名称:" name="name">
<a-input
v-model:value="form.name"
placeholder="请输入规则名称"
allow-clear/>
</a-form-item>
<a-form-item label="描述:">
<a-textarea
v-model:value="form.desc"
placeholder="请输入描述"
:rows="4"/>
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script>
import {
DownOutlined,
UpOutlined,
PlusOutlined
} from '@ant-design/icons-vue';
export default {
name: 'ListBasic',
components: {
DownOutlined,
UpOutlined,
PlusOutlined
},
data() {
return {
// 数据接口
url: 'https://cdn.eleadmin.com/20200610/list-demo-basic.json',
// 表格列配置
columns: [
{
key: 'index',
customRender: ({index}) => `${this.$refs.table.tableIndex + index}`
},
{
title: '规则名称',
dataIndex: 'name',
sorter: true
},
{
title: '描述',
dataIndex: 'desc',
sorter: true
},
{
title: '服务调用次数',
dataIndex: 'callTimes',
customRender: ({text}) => `${text}`,
sorter: true
},
{
title: '状态',
dataIndex: 'state',
sorter: true,
slots: {customRender: 'state'},
filters: [
{
text: '运行中',
value: '0'
},
{
text: '已上线',
value: '1'
},
{
text: '异常',
value: '2'
},
{
text: '关闭',
value: '3'
}
]
},
{
title: '上次调度时间',
dataIndex: 'lastTime',
sorter: true
},
{
title: '操作',
key: 'action',
align: 'center',
slots: {customRender: 'action'}
}
],
// 表格选中数据
choose: [],
// 表格搜索条件
where: {},
// 编辑弹窗数据
form: {},
// 编辑弹窗表单验证规则
rules: {
name: [
{required: true, message: '请输入规则名称', type: 'string', trigger: 'blur'}
]
},
// 是否显示编辑弹窗
showEdit: false,
// 编辑表单提交状态
loading: false,
// 搜索表单是否展开
searchExpand: false
};
},
computed: {
// 计算服务总调用次数
sumTimes() {
let sum = 0;
this.choose.forEach(d => sum += d.callTimes);
return sum;
}
},
methods: {
/* 设置行属性 */
customRow(record) {
return {
onClick: () => {
const index = this.choose.findIndex(d => d.id === record.id);
if (index === -1) {
this.choose = this.choose.concat([record]);
} else {
this.choose = this.choose.slice(0, index).concat(this.choose.slice(index + 1));
}
}
};
},
/* 刷新表格 */
reload() {
this.$refs.table.reload({page: 1, where: this.where});
},
/* 重置搜索 */
reset() {
this.where = {};
this.reload();
},
/* 清空选择 */
clearChoose() {
this.choose = [];
},
/* 保存编辑 */
save() {
this.$refs.form.validate().then(() => {
this.loading = true;
setTimeout(() => {
this.loading = false;
this.showEdit = false;
this.$message.success('保存成功');
}, 300);
}).catch(() => {
});
},
/* 编辑 */
openEdit(row) {
this.form = Object.assign({}, row);
this.showEdit = true;
this.$nextTick(() => {
this.$refs.form.clearValidate();
});
},
/* 订阅 */
doRSS(row) {
console.log(row);
this.$message.success('订阅成功');
},
/* 下拉按钮点击 */
onDropClick(obj) {
if (obj.key === 'del') {
this.$message.info('点击了批量删除');
} else if (obj.key === 'check') {
this.$message.info('点击了批量审批');
}
}
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,120 @@
<template>
<a-row :gutter="16">
<a-col
v-for="(item, index) in data"
:key="index"
:lg="6"
:md="8"
:sm="12"
:xs="12">
<a-card
:bordered="false"
hoverable
style="margin-top: 16px;">
<div class="ele-cell" style="margin-bottom: 16px;">
<a-avatar size="large" :src="item.cover"/>
<h6 class="ele-cell-content">{{ item.title }}</h6>
</div>
<div class="ele-elip" style="margin-bottom: 6px;">网址{{ item.url }}</div>
<div class="ele-elip">最后更新时间{{ item.time }}</div>
<template #actions>
<a-tooltip title="下载">
<download-outlined/>
</a-tooltip>
<a-tooltip title="编辑">
<edit-outlined/>
</a-tooltip>
<a-tooltip title="分享">
<share-alt-outlined/>
</a-tooltip>
<a-dropdown>
<ellipsis-outlined/>
<template #overlay>
<a-menu>
<a-menu-item>1st menu item</a-menu-item>
<a-menu-item>2nd menu item</a-menu-item>
<a-menu-item>3rd menu item</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</template>
</a-card>
</a-col>
</a-row>
</template>
<script>
import {
DownloadOutlined,
EditOutlined,
ShareAltOutlined,
EllipsisOutlined
} from '@ant-design/icons-vue';
export default {
name: 'ListCardApplication',
components: {
DownloadOutlined,
EditOutlined,
ShareAltOutlined,
EllipsisOutlined
},
data() {
return {
data: [
{
title: 'ElementUI',
url: 'https://element.eleme.cn',
time: '2 小时前',
cover: 'https://cdn.eleadmin.com/20200609/c184eef391ae48dba87e3057e70238fb.jpg'
},
{
title: 'Vue.js',
url: 'https://cn.vuejs.org',
time: '4 小时前',
cover: 'https://cdn.eleadmin.com/20200609/b6a811873e704db49db994053a5019b2.jpg'
},
{
title: 'Vuex',
url: 'https://vuex.vuejs.org',
time: '12 小时前',
cover: 'https://cdn.eleadmin.com/20200609/948344a2a77c47a7a7b332fe12ff749a.jpg'
},
{
title: 'Vue Router',
url: 'https://vuex.vuejs.org',
time: '14 小时前',
cover: 'https://cdn.eleadmin.com/20200609/f6bc05af944a4f738b54128717952107.jpg'
},
{
title: 'Sass',
url: 'https://www.sass.hk',
time: '10 小时前',
cover: 'https://cdn.eleadmin.com/20200609/2d98970a51b34b6b859339c96b240dcd.jpg'
},
{
title: 'Axios',
url: 'http://www.axios-js.com',
time: '16 小时前',
cover: 'https://cdn.eleadmin.com/20200609/faa0202700ee455b90fe77d8bef98bc0.jpg'
},
{
title: 'Webpack',
url: 'https://www.webpackjs.com',
time: '6 小时前',
cover: 'https://cdn.eleadmin.com/20200609/d3519518b00d42d3936b2ab5ce3a4cc3.jpg'
},
{
title: 'Node.js',
url: 'http://nodejs.cn',
time: '8 小时前',
cover: 'https://cdn.eleadmin.com/20200609/fe9196dd091e438fba115205c1003ee7.jpg'
}
]
};
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,234 @@
<template>
<a-card :bordered="false" style="margin-top: 16px;">
<a-image-preview-group>
<a-list
:data-source="data"
:loading="loading&&page===1"
item-layout="vertical"
size="large">
<template #renderItem="{item,index}">
<a-list-item :key="index">
<a-list-item-meta :title="item.title">
<template #description>
<a-tag v-for="(tag,i) in item.tags" :key="i">
{{ tag }}
</a-tag>
</template>
</a-list-item-meta>
<div class="ele-text-heading">
{{ item.content }}
</div>
<div class="ele-cell" style="margin-top: 16px;">
<a-avatar :src="item.avatar" size="small"/>
<div class="ele-cell-content">
{{ item.user }} 发表于 {{ item.time }}
</div>
</div>
<template #extra>
<a-image
:width="280"
:src="item.cover"
style="max-width: 100%;cursor: zoom-in;"/>
</template>
<template #actions>
<span>
<like-outlined/>
<span><s/>{{ item.likes }}</span>
</span>
<span>
<star-outlined/>
<span><s/>{{ item.favorites }}</span>
</span>
<span>
<message-outlined/>
<span><s/>{{ item.comments }}</span>
</span>
</template>
</a-list-item>
</template>
<template #loadMore>
<div class="ele-text-center" style="margin-top: 20px;">
<a-button
v-if="page!==1"
:loading="loading"
@click="query">
{{ loading ? '加载中..' : '加载更多' }}
</a-button>
</div>
</template>
</a-list>
</a-image-preview-group>
</a-card>
</template>
<script>
import {
LikeOutlined,
StarOutlined,
MessageOutlined
} from '@ant-design/icons-vue';
export default {
name: 'ListCardArticle',
components: {
LikeOutlined,
StarOutlined,
MessageOutlined
},
data() {
return {
data: [
{
title: 'ElementUI',
content: 'Element一套为开发者、设计师和产品经理准备的基于 Vue 2.0 的组件库,提供了配套设计资源,帮助你的网站快速成型。',
time: '2 小时前',
cover: 'https://cdn.eleadmin.com/20200610/RZ8FQmZfHkcffMlTBCJllBFjEhEsObVo.jpg',
tags: ['EleAdminPro', 'UI框架', '设计语言'],
user: 'SunSmile',
avatar: 'https://cdn.eleadmin.com/20200609/c184eef391ae48dba87e3057e70238fb.jpg',
favorites: 104,
likes: 189,
comments: 15
},
{
title: 'Vue.js',
content: 'Vue 是一套用于构建用户界面的渐进式框架。与其它大型框架不同的是Vue 被设计为可以自底向上逐层应用。',
time: '4 小时前',
cover: 'https://cdn.eleadmin.com/20200610/WLXm7gp1EbLDtvVQgkeQeyq5OtDm00Jd.jpg',
tags: ['EleAdminPro', 'UI框架', '设计语言'],
user: '你的名字很好听',
avatar: 'https://cdn.eleadmin.com/20200609/b6a811873e704db49db994053a5019b2.jpg',
favorites: 104,
likes: 189,
comments: 15
},
{
title: 'Vuex',
content: 'Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。',
time: '12 小时前',
cover: 'https://cdn.eleadmin.com/20200610/4Z0QR2L0J1XStxBh99jVJ8qLfsGsOgjU.jpg',
tags: ['EleAdminPro', 'UI框架', '设计语言'],
user: '全村人的希望',
avatar: 'https://cdn.eleadmin.com/20200609/948344a2a77c47a7a7b332fe12ff749a.jpg',
favorites: 104,
likes: 189,
comments: 15
},
{
title: 'Vue Router',
content: 'Vue Router 是 Vue.js 官方的路由管理器。它和 Vue.js 的核心深度集成,让构建单页面应用变得易如反掌。',
time: '14 小时前',
cover: 'https://cdn.eleadmin.com/20200610/ttkIjNPlVDuv4lUTvRX8GIlM2QqSe0jg.jpg',
tags: ['EleAdminPro', 'UI框架', '设计语言'],
user: 'Jasmine',
avatar: 'https://cdn.eleadmin.com/20200609/f6bc05af944a4f738b54128717952107.jpg',
favorites: 104,
likes: 189,
comments: 15
},
{
title: 'Sass',
content: 'Sass 是世界上最成熟、稳定、强大的专业级 CSS 扩展语言。',
time: '10 小时前',
cover: 'https://cdn.eleadmin.com/20200610/fAenQ8nvRjL7x0i0jEfuDBZHvJfHf3v6.jpg',
tags: ['EleAdminPro', 'UI框架', '设计语言'],
user: '酷酷的大叔',
avatar: 'https://cdn.eleadmin.com/20200609/2d98970a51b34b6b859339c96b240dcd.jpg',
favorites: 104,
likes: 189,
comments: 15
},
{
title: 'Axios',
content: 'Axios 是一个基于 promise 的 HTTP 库,可以用在浏览器和 node.js 中。',
time: '16 小时前',
cover: 'https://cdn.eleadmin.com/20200610/LrCTN2j94lo9N7wEql7cBr1Ux4rHMvmZ.jpg',
tags: ['EleAdminPro', 'UI框架', '设计语言'],
user: 'SunSmile',
avatar: 'https://cdn.eleadmin.com/20200609/c184eef391ae48dba87e3057e70238fb.jpg',
favorites: 104,
likes: 189,
comments: 15
},
{
title: 'Webpack',
content: 'webpack 是一个模块打包器。webpack 的主要目标是将 JavaScript 文件打包在一起,打包后的文件用于在浏览器中使用。',
time: '6 小时前',
cover: 'https://cdn.eleadmin.com/20200610/yeKvhT20lMU0f1T3Y743UlGEOLLnZSnp.jpg',
tags: ['EleAdminPro', 'UI框架', '设计语言'],
user: '全村人的希望',
avatar: 'https://cdn.eleadmin.com/20200609/948344a2a77c47a7a7b332fe12ff749a.jpg',
favorites: 104,
likes: 189,
comments: 15
},
{
title: 'Node.js',
content: 'Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行环境。Node.js 使用了一个事件驱动、非阻塞式 I/O 的模型,使其轻量又高效。',
time: '8 小时前',
cover: 'https://cdn.eleadmin.com/20200610/CyrCNmTJfv7D6GFAg39bjT3eRkkRm5dI.jpg',
tags: ['EleAdminPro', 'UI框架', '设计语言'],
user: 'Jasmine',
avatar: 'https://cdn.eleadmin.com/20200609/f6bc05af944a4f738b54128717952107.jpg',
favorites: 104,
likes: 189,
comments: 15
}
],
loading: false,
page: 2
};
},
computed: {
previewList() {
return this.data.map(item => item.cover);
}
},
mounted() {
//this.query();
},
methods: {
query() {
this.loading = true;
setTimeout(() => {
this.loading = false;
this.page++;
this.data = this.data.concat([
{
title: 'ElementUI',
content: 'Element一套为开发者、设计师和产品经理准备的基于 Vue 2.0 的组件库,提供了配套设计资源,帮助你的网站快速成型。',
time: '2 小时前',
cover: 'https://cdn.eleadmin.com/20200610/RZ8FQmZfHkcffMlTBCJllBFjEhEsObVo.jpg',
tags: ['EleAdminPro', 'UI框架', '设计语言'],
user: 'SunSmile',
avatar: 'https://cdn.eleadmin.com/20200609/c184eef391ae48dba87e3057e70238fb.jpg',
favorites: 104,
likes: 189,
comments: 15
},
{
title: 'Vue.js',
content: 'Vue 是一套用于构建用户界面的渐进式框架。与其它大型框架不同的是Vue 被设计为可以自底向上逐层应用。',
time: '4 小时前',
cover: 'https://cdn.eleadmin.com/20200610/WLXm7gp1EbLDtvVQgkeQeyq5OtDm00Jd.jpg',
tags: ['EleAdminPro', 'UI框架', '设计语言'],
user: '你的名字很好听',
avatar: 'https://cdn.eleadmin.com/20200609/b6a811873e704db49db994053a5019b2.jpg',
favorites: 104,
likes: 189,
comments: 15
}
]);
}, 1000);
}
}
}
</script>
<style scoped>
@media screen and (max-width: 768px) {
.ant-list-item :deep(.ant-list-item-extra .ant-image) {
width: 100% !important;
}
}
</style>

View File

@@ -0,0 +1,66 @@
<template>
<div class="ele-body">
<a-card
:bordered="false"
:body-style="{padding: '0 24px'}">
<h5 style="padding: 16px 0 24px 0;">卡片列表</h5>
<div class="ele-text-center">
<a-input-search
v-model:value="keyword"
placeholder="请输入内容"
enter-button
style="max-width: 500px;"
size="large"/>
</div>
<a-tabs
class="list-card-tabs"
:active-key="active"
@tabClick="onTabChange">
<a-tab-pane key="/list/card/project" tab="项目"/>
<a-tab-pane key="/list/card/application" tab="应用"/>
<a-tab-pane key="/list/card/article" tab="文章"/>
</a-tabs>
</a-card>
<ele-empty-layout/>
</div>
</template>
<script>
import EleEmptyLayout from 'ele-admin-pro/packages/ele-empty-layout';
export default {
name: 'ListCard',
components: {EleEmptyLayout},
data() {
return {
keyword: ''
};
},
computed: {
active() {
return this.$route.path;
}
},
methods: {
/* 切换tab */
onTabChange(key) {
this.$router.push(key);
}
}
}
</script>
<style scoped>
.list-card-tabs {
margin-top: 24px;
}
.list-card-tabs :deep(.ant-tabs-bar) {
margin-bottom: 0;
border-bottom-color: transparent;
}
.list-card-tabs :deep(.ant-tabs-tab) {
padding: 12px 4px;
}
</style>

View File

@@ -0,0 +1,228 @@
<template>
<a-row :gutter="16">
<a-col
v-for="(item, index) in data"
:key="index"
:lg="6"
:md="8"
:sm="12"
:xs="12">
<a-card
:bordered="false"
hoverable
style="margin-top: 16px;">
<template #cover>
<img :src="item.cover" alt=""/>
</template>
<a-card-meta :title="item.title">
<template #description>
<div class="project-list-desc">
{{ item.content }}
</div>
</template>
</a-card-meta>
<div class="ele-cell">
<div class="ele-cell-content ele-text-secondary">
{{ item.time }}
</div>
<ele-avatar-list :data="item.users" size="small"/>
</div>
</a-card>
</a-col>
</a-row>
</template>
<script>
export default {
name: 'ListCardProject',
data() {
return {
// 分页参数
page: {
page: 1,
limit: 8
},
// 数据总数
count: 40,
data: [
{
title: 'ElementUI',
content: 'Element一套为开发者、设计师和产品经理准备的基于 Vue 2.0 的组件库,提供了配套设计资源,帮助你的网站快速成型。',
time: '2 小时前',
cover: 'https://cdn.eleadmin.com/20200610/RZ8FQmZfHkcffMlTBCJllBFjEhEsObVo.jpg',
users: [
{
name: 'SunSmile',
avatar: 'https://cdn.eleadmin.com/20200609/c184eef391ae48dba87e3057e70238fb.jpg'
},
{
name: '酷酷的大叔',
avatar: 'https://cdn.eleadmin.com/20200609/2d98970a51b34b6b859339c96b240dcd.jpg'
},
{
name: 'Jasmine',
avatar: 'https://cdn.eleadmin.com/20200609/948344a2a77c47a7a7b332fe12ff749a.jpg'
}
]
},
{
title: 'Vue.js',
content: 'Vue 是一套用于构建用户界面的渐进式框架。与其它大型框架不同的是Vue 被设计为可以自底向上逐层应用。',
time: '4 小时前',
cover: 'https://cdn.eleadmin.com/20200610/WLXm7gp1EbLDtvVQgkeQeyq5OtDm00Jd.jpg',
users: [
{
name: 'SunSmile',
avatar: 'https://cdn.eleadmin.com/20200609/c184eef391ae48dba87e3057e70238fb.jpg'
},
{
name: '酷酷的大叔',
avatar: 'https://cdn.eleadmin.com/20200609/2d98970a51b34b6b859339c96b240dcd.jpg'
},
{
name: 'Jasmine',
avatar: 'https://cdn.eleadmin.com/20200609/948344a2a77c47a7a7b332fe12ff749a.jpg'
}
]
},
{
title: 'Vuex',
content: 'Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。',
time: '12 小时前',
cover: 'https://cdn.eleadmin.com/20200610/4Z0QR2L0J1XStxBh99jVJ8qLfsGsOgjU.jpg',
users: [
{
name: 'SunSmile',
avatar: 'https://cdn.eleadmin.com/20200609/c184eef391ae48dba87e3057e70238fb.jpg'
},
{
name: '酷酷的大叔',
avatar: 'https://cdn.eleadmin.com/20200609/2d98970a51b34b6b859339c96b240dcd.jpg'
},
{
name: 'Jasmine',
avatar: 'https://cdn.eleadmin.com/20200609/948344a2a77c47a7a7b332fe12ff749a.jpg'
}
]
},
{
title: 'Vue Router',
content: 'Vue Router 是 Vue.js 官方的路由管理器。它和 Vue.js 的核心深度集成,让构建单页面应用变得易如反掌。',
time: '14 小时前',
cover: 'https://cdn.eleadmin.com/20200610/ttkIjNPlVDuv4lUTvRX8GIlM2QqSe0jg.jpg',
users: [
{
name: 'SunSmile',
avatar: 'https://cdn.eleadmin.com/20200609/c184eef391ae48dba87e3057e70238fb.jpg'
},
{
name: '酷酷的大叔',
avatar: 'https://cdn.eleadmin.com/20200609/2d98970a51b34b6b859339c96b240dcd.jpg'
},
{
name: 'Jasmine',
avatar: 'https://cdn.eleadmin.com/20200609/948344a2a77c47a7a7b332fe12ff749a.jpg'
}
]
},
{
title: 'Sass',
content: 'Sass 是世界上最成熟、稳定、强大的专业级 CSS 扩展语言。',
time: '10 小时前',
cover: 'https://cdn.eleadmin.com/20200610/fAenQ8nvRjL7x0i0jEfuDBZHvJfHf3v6.jpg',
users: [
{
name: 'SunSmile',
avatar: 'https://cdn.eleadmin.com/20200609/c184eef391ae48dba87e3057e70238fb.jpg'
},
{
name: '酷酷的大叔',
avatar: 'https://cdn.eleadmin.com/20200609/2d98970a51b34b6b859339c96b240dcd.jpg'
},
{
name: 'Jasmine',
avatar: 'https://cdn.eleadmin.com/20200609/948344a2a77c47a7a7b332fe12ff749a.jpg'
}
]
},
{
title: 'Axios',
content: 'Axios 是一个基于 promise 的 HTTP 库,可以用在浏览器和 node.js 中。',
time: '16 小时前',
cover: 'https://cdn.eleadmin.com/20200610/LrCTN2j94lo9N7wEql7cBr1Ux4rHMvmZ.jpg',
users: [
{
name: 'SunSmile',
avatar: 'https://cdn.eleadmin.com/20200609/c184eef391ae48dba87e3057e70238fb.jpg'
},
{
name: '酷酷的大叔',
avatar: 'https://cdn.eleadmin.com/20200609/2d98970a51b34b6b859339c96b240dcd.jpg'
},
{
name: 'Jasmine',
avatar: 'https://cdn.eleadmin.com/20200609/948344a2a77c47a7a7b332fe12ff749a.jpg'
}
]
},
{
title: 'Webpack',
content: 'webpack 是一个模块打包器。webpack 的主要目标是将 JavaScript 文件打包在一起,打包后的文件用于在浏览器中使用。',
time: '6 小时前',
cover: 'https://cdn.eleadmin.com/20200610/yeKvhT20lMU0f1T3Y743UlGEOLLnZSnp.jpg',
users: [
{
name: 'SunSmile',
avatar: 'https://cdn.eleadmin.com/20200609/c184eef391ae48dba87e3057e70238fb.jpg'
},
{
name: '酷酷的大叔',
avatar: 'https://cdn.eleadmin.com/20200609/2d98970a51b34b6b859339c96b240dcd.jpg'
},
{
name: 'Jasmine',
avatar: 'https://cdn.eleadmin.com/20200609/948344a2a77c47a7a7b332fe12ff749a.jpg'
}
]
},
{
title: 'Node.js',
content: 'Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行环境。Node.js 使用了一个事件驱动、非阻塞式 I/O 的模型,使其轻量又高效。',
time: '8 小时前',
cover: 'https://cdn.eleadmin.com/20200610/CyrCNmTJfv7D6GFAg39bjT3eRkkRm5dI.jpg',
users: [
{
name: 'SunSmile',
avatar: 'https://cdn.eleadmin.com/20200609/c184eef391ae48dba87e3057e70238fb.jpg'
},
{
name: '酷酷的大叔',
avatar: 'https://cdn.eleadmin.com/20200609/2d98970a51b34b6b859339c96b240dcd.jpg'
},
{
name: 'Jasmine',
avatar: 'https://cdn.eleadmin.com/20200609/948344a2a77c47a7a7b332fe12ff749a.jpg'
}
]
}
]
};
},
methods: {
query() {
}
}
}
</script>
<style scoped>
.project-list-desc {
height: 44px;
line-height: 22px;
margin-bottom: 20px;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
</style>

387
src/views/login/forget.vue Normal file
View File

@@ -0,0 +1,387 @@
<template>
<div :class="['login-wrapper', ['', 'login-form-right', 'login-form-left'][direction]]">
<a-form
ref="form"
:model="form"
:rules="rules"
layout="vertical"
class="login-form ele-bg-white">
<h4>忘记密码</h4>
<a-form-item name="phone">
<a-input
placeholder="请输入绑定手机号"
v-model:value="form.phone"
allow-clear
size="large">
<template #prefix>
<mobile-outlined/>
</template>
</a-input>
</a-form-item>
<a-form-item name="password">
<a-input-password
placeholder="请输入新的登录密码"
v-model:value="form.password"
size="large">
<template #prefix>
<lock-outlined/>
</template>
</a-input-password>
</a-form-item>
<a-form-item name="password2">
<a-input-password
placeholder="请再次输入登录密码"
v-model:value="form.password2"
size="large">
<template #prefix>
<key-outlined/>
</template>
</a-input-password>
</a-form-item>
<a-form-item name="code">
<div class="login-input-group">
<a-input
placeholder="请输入验证码"
v-model:value="form.code"
allow-clear
size="large">
<template #prefix>
<safety-certificate-outlined/>
</template>
</a-input>
<a-button
class="login-captcha"
:disabled="!!countdownTimer"
@click="showImgCodeCheck">
<span v-if="!countdownTimer">发送验证码</span>
<span v-else>已发送 {{ countdownTime }} s</span>
</a-button>
</div>
</a-form-item>
<a-form-item>
<router-link
to="/login"
class="ele-pull-right"
style="line-height: 22px;">返回登录
</router-link>
</a-form-item>
<a-form-item>
<a-button
block
size="large"
type="primary"
:loading="loading"
@click="doSubmit">修改密码
</a-button>
</a-form-item>
</a-form>
<div class="login-copyright">copyright © 2021 eleadmin.com all rights reserved.</div>
</div>
<!-- 编辑弹窗 -->
<a-modal
:width="340"
:footer="null"
title="发送验证码"
v-model:visible="showImgCode">
<div
class="login-input-group"
style="margin-bottom: 16px;">
<a-input
v-model:value="imgCode"
placeholder="请输入图形验证码"
allow-clear
size="large"/>
<a-button class="login-captcha">
<img
alt=""
:src="captcha"
@click="changeImgCode"/>
</a-button>
</div>
<a-button
block
size="large"
type="primary"
:loading="codeLoading"
@click="sendCode">立即发送
</a-button>
</a-modal>
<!-- 实际项目去掉这段 -->
<div style="position: absolute;left: 30px;top: 20px;z-index: 999;">
<a-radio-group v-model:value="direction" size="small">
<a-radio-button :value="2">居左</a-radio-button>
<a-radio-button :value="0">居中</a-radio-button>
<a-radio-button :value="1">居右</a-radio-button>
</a-radio-group>
</div>
</template>
<script>
import {
MobileOutlined,
LockOutlined,
KeyOutlined,
SafetyCertificateOutlined
} from '@ant-design/icons-vue';
export default {
name: 'Forget',
components: {
MobileOutlined,
LockOutlined,
KeyOutlined,
SafetyCertificateOutlined
},
data() {
let rePswRule = async (rule, value) => {
if (!value) {
return Promise.reject('请再次输入新密码');
}
if (value === this.form.password) {
return Promise.resolve();
}
return Promise.reject('两次输入密码不一致');
};
return {
// 登录框方向, 0居中, 1居右, 2居左
direction: 0,
// 加载状态
loading: false,
// 表单数据
form: {
phone: '1234567890',
password: '',
password2: '',
code: ''
},
// 表单验证规则
rules: {
phone: [
{required: true, message: '请输入绑定手机号', type: 'string', trigger: 'blur'}
],
password: [
{required: true, message: '请输入新的登录密码', type: 'string', trigger: 'blur'}
],
password2: [
{required: true, validator: rePswRule, trigger: 'blur'}
],
code: [
{required: true, message: '请输入验证码', type: 'string', trigger: 'blur'}
]
},
// 用于刷新验证码
v: new Date().getTime(),
// 是否显示图形验证码弹窗
showImgCode: false,
// 图形验证码
imgCode: '',
// 发送验证码按钮loading
codeLoading: false,
// 验证码倒计时时间
countdownTime: 30,
// 验证码倒计时定时器
countdownTimer: null
};
},
computed: {
// 图形验证码地址
captcha() {
return 'https://eleadmin.com/assets/captcha?v=' + this.v;
}
},
methods: {
/* 提交 */
doSubmit() {
this.$refs.form.validate().then(() => {
this.loading = true;
setTimeout(() => {
this.$message.success('密码修改成功');
this.$router.push('/login');
}, 1000);
}).catch(() => {
});
},
/* 更换图形验证码 */
changeImgCode() {
// 这里演示的验证码是后端地址直接是图片的形式, 如果后端返回base64格式请参考登录页面
this.v = new Date().getTime();
},
/* 显示发送短信验证码弹窗 */
showImgCodeCheck() {
if (!this.form.phone) {
this.$message.error('请输入手机号码');
return;
}
this.imgCode = '';
this.changeImgCode();
this.showImgCode = true;
},
/* 发送短信验证码 */
sendCode() {
if (!this.imgCode) {
this.$message.error('请输入图形验证码');
return;
}
this.codeLoading = true;
setTimeout(() => {
this.$message.success('短信验证码发送成功, 请注意查收!');
this.showImgCode = false;
this.codeLoading = false;
this.startCountdownTimer();
}, 1000);
},
/* 开始对按钮进行倒计时 */
startCountdownTimer() {
this.countdownTime = 30;
this.countdownTimer = setInterval(() => {
if (this.countdownTime <= 1) {
clearInterval(this.countdownTimer);
this.countdownTimer = null;
}
this.countdownTime--;
}, 1000);
}
},
unmounted() {
if (this.countdownTimer) {
clearInterval(this.countdownTimer);
}
}
}
</script>
<style scoped>
/* 背景 */
.login-wrapper {
padding: 48px 16px 0 16px;
position: relative;
box-sizing: border-box;
background-image: url("~@/assets/bg-login.jpg");
background-repeat: no-repeat;
background-size: cover;
min-height: 100vh;
}
.login-wrapper:before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, .2);
}
/* 卡片 */
.login-form {
width: 360px;
margin: 0 auto;
max-width: 100%;
padding: 0 28px 16px 28px;
box-sizing: border-box;
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.15);
border-radius: 2px;
position: relative;
z-index: 2;
}
.login-form-right .login-form {
margin: 0 15% 0 auto;
}
.login-form-left .login-form {
margin: 0 auto 0 15%;
}
.login-form h4 {
padding: 22px 0;
text-align: center;
}
/* 验证码 */
.login-input-group {
display: flex;
align-items: center;
}
.login-input-group :deep(.ant-input-affix-wrapper) {
flex: 1;
}
.login-input-group .login-captcha {
width: 102px;
height: 40px;
margin-left: 10px;
padding: 0;
}
.login-input-group .login-captcha > img {
width: 100%;
height: 100%;
}
/* 第三方登录图标 */
.login-oauth-icon {
color: #fff;
padding: 5px;
margin: 0 12px;
font-size: 18px;
border-radius: 50%;
cursor: pointer;
}
/* 底部版权 */
.login-copyright {
color: #eee;
text-align: center;
padding: 48px 0 22px 0;
position: relative;
z-index: 1;
}
/* 响应式 */
@media screen and (min-height: 640px) {
.login-wrapper {
padding-top: 0;
}
.login-form {
position: absolute;
top: 50%;
left: 50%;
transform: translateX(-50%);
margin-top: -230px;
}
.login-form-right .login-form,
.login-form-left .login-form {
left: auto;
right: 15%;
transform: translateX(0);
margin: -230px auto auto auto;
}
.login-form-left .login-form {
right: auto;
left: 15%;
}
.login-copyright {
position: absolute;
left: 0;
right: 0;
bottom: 0;
}
}
@media screen and (max-width: 768px) {
.login-form-right .login-form,
.login-form-left .login-form {
left: 50%;
right: auto;
margin-left: 0;
margin-right: auto;
transform: translateX(-50%);
}
}
</style>

364
src/views/login/login.vue Normal file
View File

@@ -0,0 +1,364 @@
<template>
<div :class="['login-wrapper', ['', 'login-form-right', 'login-form-left'][direction]]">
<a-form
ref="form"
:model="form"
:rules="rules"
layout="vertical"
class="login-form ele-bg-white">
<h4>{{ $t('login.title') }}</h4>
<a-form-item name="username">
<a-input
allow-clear
size="large"
v-model:value="form.username"
:placeholder="$t('login.username')">
<template #prefix>
<user-outlined/>
</template>
</a-input>
</a-form-item>
<a-form-item name="password">
<a-input-password
size="large"
v-model:value="form.password"
:placeholder="$t('login.password')">
<template #prefix>
<lock-outlined/>
</template>
</a-input-password>
</a-form-item>
<a-form-item name="code">
<div class="login-input-group">
<a-input
allow-clear
size="large"
v-model:value="form.code"
:placeholder="$t('login.code')">
<template #prefix>
<safety-certificate-outlined/>
</template>
</a-input>
<a-button class="login-captcha" @click="changeCode">
<img v-if="captcha" :src="captcha" alt=""/>
</a-button>
</div>
</a-form-item>
<a-form-item>
<a-checkbox v-model:checked="form.remember">
{{ $t('login.remember') }}
</a-checkbox>
<router-link
to="/forget"
class="ele-pull-right"
style="line-height: 22px;">
{{ $t('login.forget') }}
</router-link>
</a-form-item>
<a-form-item>
<a-button
block
size="large"
type="primary"
:loading="loading"
@click="doSubmit">
{{ loading ? $t('login.loading') : $t('login.login') }}
</a-button>
</a-form-item>
<div class="ele-text-center" style="padding-bottom: 32px;">
<qq-outlined class="login-oauth-icon" style="background: #3492ed;"/>
<wechat-outlined class="login-oauth-icon" style="background: #4daf29;"/>
<weibo-outlined class="login-oauth-icon" style="background: #CF1900;"/>
</div>
</a-form>
<div class="login-copyright">copyright © 2021 eleadmin.com all rights reserved.</div>
<!-- 多语言切换 -->
<div style="position: absolute;right: 30px;top: 20px;z-index: 999;">
<a-dropdown placement="bottomLeft" :overlay-style="{minWidth: '120px'}">
<global-outlined style="font-size: 18px;color: #fff;"/>
<template #overlay>
<a-menu :selected-keys="languages" @click="changeLanguage">
<a-menu-item key="en">English</a-menu-item>
<a-menu-item key="zh_CN">简体中文</a-menu-item>
<a-menu-item key="zh_TW">繁體中文</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</div>
<!-- 实际项目去掉这段 -->
<div style="position: absolute;left: 30px;top: 20px;z-index: 999;">
<a-radio-group v-model:value="direction" size="small">
<a-radio-button :value="2">居左</a-radio-button>
<a-radio-button :value="0">居中</a-radio-button>
<a-radio-button :value="1">居右</a-radio-button>
</a-radio-group>
</div>
</div>
</template>
<script>
import {
UserOutlined,
LockOutlined,
SafetyCertificateOutlined,
QqOutlined,
WechatOutlined,
WeiboOutlined,
GlobalOutlined
} from '@ant-design/icons-vue';
import setting from '@/config/setting';
export default {
name: 'Login',
components: {
UserOutlined,
LockOutlined,
SafetyCertificateOutlined,
QqOutlined,
WechatOutlined,
WeiboOutlined,
GlobalOutlined
},
data() {
return {
// 登录框方向, 0居中, 1居右, 2居左
direction: 0,
// 加载状态
loading: false,
// 表单数据
form: {
username: 'admin',
password: 'admin',
code: '',
remember: true
},
// 验证码base64数据
captcha: '',
// 验证码内容, 实际项目去掉
text: ''
};
},
computed: {
// 表单验证规则
rules() {
return {
username: [
{required: true, message: this.$t('login.username'), type: 'string', trigger: 'blur'}
],
password: [
{required: true, message: this.$t('login.password'), type: 'string', trigger: 'blur'}
],
code: [
{required: true, message: this.$t('login.code'), type: 'string', trigger: 'blur'}
]
};
},
// 当前语言
languages() {
return [this.$i18n.locale];
}
},
mounted() {
if (setting.takeToken()) {
return this.goHome();
}
this.changeCode();
},
methods: {
/* 提交 */
doSubmit() {
this.$refs.form.validate().then(() => {
if (this.form.code.toLowerCase() !== this.text) {
return this.$message.error('验证码错误');
}
this.loading = true;
let formData = new FormData();
for (let key in this.form) {
formData.append(key, this.form[key]);
}
this.$http.post('/login', formData).then((res) => {
this.loading = false;
if (res.data.code === 0) {
this.$message.success('登录成功');
this.$store.dispatch('user/setToken', {
token: 'Bearer ' + res.data.access_token,
remember: this.form.remember
}).then(() => {
this.goHome();
});
} else {
this.$message.error(res.data.msg);
}
});
}).catch(() => {
});
},
/* 跳转到首页 */
goHome() {
const query = this.$route.query;
const path = query && query.from ? query.from : '/';
this.$router.push(path).catch(() => {
});
},
/* 更换图形验证码 */
changeCode() {
// 这里演示的验证码是后端返回base64格式的形式, 如果后端地址直接是图片请参考忘记密码页面
this.$http.get('/file/captcha').then(res => {
if (res.data.code === 0) {
this.captcha = res.data.data;
// 实际项目后端一般会返回验证码的key而不是直接返回验证码的内容, 登录用key去验证, 你可以根据自己后端接口修改
this.text = res.data.text;
// 自动回填验证码, 实际项目去掉这个
this.form.code = res.data.text;
} else {
this.$message.error(res.data.msg);
}
}).catch(e => {
this.$message.error(e.message);
});
},
/* 切换语言 */
changeLanguage({key}) {
this.$i18n.locale = key;
localStorage.setItem('i18n-lang', key);
this.$refs.form.clearValidate();
}
}
}
</script>
<style scoped>
/* 背景 */
.login-wrapper {
padding: 48px 16px 0 16px;
position: relative;
box-sizing: border-box;
background-image: url("~@/assets/bg-login.jpg");
background-repeat: no-repeat;
background-size: cover;
min-height: 100vh;
}
.login-wrapper:before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, .2);
}
/* 卡片 */
.login-form {
width: 360px;
margin: 0 auto;
max-width: 100%;
padding: 0 28px;
box-sizing: border-box;
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.15);
border-radius: 2px;
position: relative;
z-index: 2;
}
.login-form-right .login-form {
margin: 0 15% 0 auto;
}
.login-form-left .login-form {
margin: 0 auto 0 15%;
}
.login-form h4 {
padding: 22px 0;
text-align: center;
}
/* 验证码 */
.login-input-group {
display: flex;
align-items: center;
}
.login-input-group :deep(.ant-input-affix-wrapper) {
flex: 1;
}
.login-input-group .login-captcha {
width: 102px;
height: 40px;
margin-left: 10px;
padding: 0;
}
.login-input-group .login-captcha > img {
width: 100%;
height: 100%;
}
/* 第三方登录图标 */
.login-oauth-icon {
color: #fff;
padding: 5px;
margin: 0 12px;
font-size: 18px;
border-radius: 50%;
cursor: pointer;
}
/* 底部版权 */
.login-copyright {
color: #eee;
text-align: center;
padding: 48px 0 22px 0;
position: relative;
z-index: 1;
}
/* 响应式 */
@media screen and (min-height: 640px) {
.login-wrapper {
padding-top: 0;
}
.login-form {
position: absolute;
top: 50%;
left: 50%;
transform: translateX(-50%);
margin-top: -230px;
}
.login-form-right .login-form,
.login-form-left .login-form {
left: auto;
right: 15%;
transform: translateX(0);
margin: -230px auto auto auto;
}
.login-form-left .login-form {
right: auto;
left: 15%;
}
.login-copyright {
position: absolute;
left: 0;
right: 0;
bottom: 0;
}
}
@media screen and (max-width: 768px) {
.login-form-right .login-form,
.login-form-left .login-form {
left: 50%;
right: auto;
margin-left: 0;
margin-right: auto;
transform: translateX(-50%);
}
}
</style>

55
src/views/result/fail.vue Normal file
View File

@@ -0,0 +1,55 @@
<template>
<div class="ele-body">
<a-card :bordered="false">
<div style="max-width: 960px;margin: 0 auto;">
<a-result
status="error"
title="提交失败"
sub-title="请核对并修改以下信息后再重新提交">
<div>您提交的内容有如下错误</div>
<div class="error-tips-item">
<close-circle-outlined class="ele-text-danger"/>
<div>您的账户已被冻结</div>
<a>立即解冻&gt;</a>
</div>
<div class="error-tips-item">
<close-circle-outlined class="ele-text-danger"/>
<div>您的账户还不具备申请资格</div>
<a>立即升级&gt;</a>
</div>
<template #extra>
<a-space size="middle">
<a-button type="primary">返回修改</a-button>
<a-button>重新提交</a-button>
</a-space>
</template>
</a-result>
</div>
</a-card>
</div>
</template>
<script>
import {CloseCircleOutlined} from '@ant-design/icons-vue';
export default {
name: 'ResultFail',
components: {CloseCircleOutlined}
}
</script>
<style scoped>
.error-tips-item {
display: flex;
align-items: center;
margin-top: 16px;
}
.error-tips-item > div {
margin: 0 10px;
}
.error-tips-item a {
white-space: nowrap;
}
</style>

View File

@@ -0,0 +1,30 @@
<template>
<div class="ele-body">
<a-card :bordered="false">
<div style="max-width: 960px;margin: 0 auto;">
<a-result
status="success"
title="提交成功"
sub-title="提交结果页用于反馈一系列操作任务的处理结果如果仅是简单操作使用 Message 全局提示反馈即可灰色区域可以显示一些补充的信息">
<div>已提交申请等待部门审核</div>
<template #extra>
<a-space size="middle">
<a-button type="primary">返回列表</a-button>
<a-button>查看项目</a-button>
<a-button>打印</a-button>
</a-space>
</template>
</a-result>
</div>
</a-card>
</div>
</template>
<script>
export default {
name: 'ResultSuccess'
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,140 @@
<template>
<div class="ele-body ele-body-card">
<a-row :gutter="16">
<a-col :lg="6" :md="10" :sm="24" :xs="24">
<a-card
:bordered="false"
:body-style="{padding: '24px 16px'}">
<!-- 表格 -->
<ele-pro-table
ref="table"
row-key="dictId"
:datasource="url"
:columns="columns"
v-model:current="current"
:need-page="false"
:row-selection="{columnWidth: 38}"
:toolkit="[]"
@done="done">
<template #toolbar>
<a-space size="middle">
<a-button
type="primary"
@click="openEdit()">新建
</a-button>
<a-button
type="primary"
@click="openEdit(current)"
:disabled="!current">修改
</a-button>
<a-button
danger
type="primary"
@click="remove"
:disabled="!current">删除
</a-button>
</a-space>
</template>
</ele-pro-table>
</a-card>
</a-col>
<a-col :lg="18" :md="14" :sm="24" :xs="24">
<a-card :bordered="false">
<sys-dict-data
v-if="current"
:dict-id="current.dictId"/>
</a-card>
</a-col>
</a-row>
</div>
<!-- 编辑弹窗 -->
<sys-dict-edit
v-model:visible="showEdit"
:data="editData"
@done="reload"/>
</template>
<script>
import {createVNode} from 'vue';
import {ExclamationCircleOutlined} from '@ant-design/icons-vue';
import SysDictData from './sys-dict-data';
import SysDictEdit from './sys-dict-edit';
export default {
name: 'SystemDictionary',
components: {SysDictData, SysDictEdit},
data() {
return {
// 表格数据接口
url: '/sys/dict',
// 表格列配置
columns: [
{
key: 'index',
width: 38,
customRender: ({index}) => index + 1
},
{
title: '字典名称',
dataIndex: 'dictName'
}
],
// 表格选中数据
current: null,
// 是否显示编辑弹窗
showEdit: false,
// 编辑回显数据
editData: null
};
},
methods: {
/* 表格渲染完成回调 */
done(res) {
if (res.data.length > 0) {
this.current = res.data[0];
}
},
/* 刷新表格 */
reload() {
this.$refs.table.reload();
},
/* 打开编辑弹窗 */
openEdit(row) {
this.editData = row;
this.showEdit = true;
},
/* 删除 */
remove() {
this.$confirm({
title: '提示',
content: '确定要删除选中的字典吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
const hide = this.$message.loading('请求中..', 0);
this.$http.delete('/sys/dict/' + this.current.dictId).then(res => {
hide();
if (res.data.code === 0) {
this.$message.success(res.data.msg);
this.reload();
} else {
this.$message.error(res.data.msg);
}
}).catch(e => {
hide();
this.$message.error(e.message);
});
}
});
}
}
}
</script>
<style scoped>
@media screen and (min-width: 768px) {
.ant-card {
min-height: calc(100vh - 122px);
}
}
</style>

View File

@@ -0,0 +1,135 @@
<!-- 字典项编辑弹窗 -->
<template>
<a-modal
:width="460"
:visible="visible"
:confirm-loading="loading"
:body-style="{paddingBottom: '8px'}"
:title="isUpdate?'修改字典项':'添加字典项'"
@update:visible="updateVisible"
@ok="save">
<a-form
ref="form"
:model="form"
:rules="rules"
:label-col="{md: {span: 6}, sm: {span: 24}}"
:wrapper-col="{md: {span: 18}, sm: {span: 24}}">
<a-form-item label="字典项名称:" name="dictDataName">
<a-input
allow-clear
:maxlength="20"
placeholder="请输入字典项名称"
v-model:value="form.dictDataName"/>
</a-form-item>
<a-form-item label="字典项值:" name="dictDataCode">
<a-input
allow-clear
:maxlength="20"
placeholder="请输入字典项值"
v-model:value="form.dictDataCode"/>
</a-form-item>
<a-form-item label="排序号:" name="sortNumber">
<a-input-number
:min="0"
:max="9999"
class="ele-fluid"
placeholder="请输入排序号"
v-model:value="form.sortNumber"/>
</a-form-item>
<a-form-item label="备注:">
<a-textarea
:rows="4"
:maxlength="200"
placeholder="请输入备注"
v-model:value="form.comments"/>
</a-form-item>
</a-form>
</a-modal>
</template>
<script>
export default {
name: 'SysDictDataEdit',
emits: ['done', 'update:visible'],
props: {
// 弹窗是否打开
visible: Boolean,
// 修改回显的数据
data: Object,
// 字典id
dictId: Number
},
data() {
return {
// 表单数据
form: Object.assign({}, this.data),
// 表单验证规则
rules: {
dictDataName: [
{required: true, message: '请输入字典项名称', type: 'string', trigger: 'blur'}
],
dictDataCode: [
{required: true, message: '请输入字典项值', type: 'string', trigger: 'blur'}
],
sortNumber: [
{required: true, message: '请输入排序号', type: 'number', trigger: 'blur'}
]
},
// 提交状态
loading: false,
// 是否是修改
isUpdate: false
};
},
watch: {
data() {
if (this.data) {
this.form = Object.assign({}, this.data);
this.isUpdate = true;
} else {
this.form = {};
this.isUpdate = false;
}
if (this.$refs.form) {
this.$refs.form.clearValidate();
}
}
},
methods: {
/* 保存编辑 */
save() {
this.$refs.form.validate().then(() => {
this.loading = true;
this.$http[this.isUpdate ? 'put' : 'post']('/sys/dictdata',
Object.assign({}, this.form, {
dictId: this.dictId
})
).then(res => {
this.loading = false;
if (res.data.code === 0) {
this.$message.success(res.data.msg);
if (!this.isUpdate) {
this.form = {};
}
this.updateVisible(false);
this.$emit('done');
} else {
this.$message.error(res.data.msg);
}
}).catch(e => {
this.loading = false;
this.$message.error(e.message);
});
}).catch(() => {
});
},
/* 更新visible */
updateVisible(value) {
this.$emit('update:visible', value);
}
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,178 @@
<template>
<!-- 表格 -->
<ele-pro-table
ref="table"
row-key="dictDataId"
:datasource="url"
:columns="columns"
:where="where"
tool-class="ele-toolbar-form"
v-model:selection="selection"
:scroll="{x: 'max-content'}">
<template #toolbar>
<a-row :gutter="16">
<a-col :lg="6" :md="8" :sm="24" :xs="24">
<a-input
v-model:value.trim="where.keywords"
placeholder="输入关键字搜索"
allow-clear/>
</a-col>
<a-col :lg="6" :md="8" :sm="24" :xs="24">
<a-space size="middle">
<a-button type="primary" @click="reload">查询</a-button>
<a-button type="primary" @click="openEdit()">新建</a-button>
<a-button type="primary" danger @click="removeBatch">删除</a-button>
</a-space>
</a-col>
</a-row>
</template>
<template #action="{ record }">
<a-space>
<a @click="openEdit(record)">修改</a>
<a-divider type="vertical"/>
<a-popconfirm
title="确定要删除此字典项吗?"
@confirm="remove(record)">
<a class="ele-text-danger">删除</a>
</a-popconfirm>
</a-space>
</template>
</ele-pro-table>
<!-- 编辑弹窗 -->
<sys-dict-data-edit
v-model:visible="showEdit"
:data="current"
:dict-id="dictId"
@done="reload"/>
</template>
<script>
import {createVNode} from 'vue';
import {ExclamationCircleOutlined} from '@ant-design/icons-vue';
import SysDictDataEdit from './sys-dict-data-edit';
export default {
name: 'SysDictData',
components: {SysDictDataEdit},
props: {
// 字典id
dictId: Number
},
data() {
return {
// 表格数据接口
url: '/sys/dictdata/page',
// 表格列配置
columns: [
{
title: '字典项名称',
dataIndex: 'dictDataName',
sorter: true
},
{
title: '字典项值',
dataIndex: 'dictDataCode',
sorter: true
},
{
title: '排序号',
dataIndex: 'sortNumber',
sorter: true,
width: 120,
align: 'center'
},
{
title: '创建时间',
dataIndex: 'createTime',
sorter: true,
customRender: ({text}) => this.$util.toDateString(text)
},
{
title: '操作',
key: 'action',
width: 130,
align: 'center',
slots: {customRender: 'action'}
}
],
// 表格搜索条件
where: {
dictId: this.dictId
},
// 表格选中数据
selection: [],
// 当前编辑数据
current: null,
// 是否显示编辑弹窗
showEdit: false
};
},
methods: {
/* 刷新表格 */
reload() {
this.$refs.table.reload({page: 1, where: this.where});
},
/* 打开编辑弹窗 */
openEdit(row) {
this.current = row;
this.showEdit = true;
},
/* 删除单个 */
remove(row) {
const hide = this.$message.loading('请求中..', 0);
this.$http.delete('/sys/dictdata/' + row.dictDataId).then(res => {
hide();
if (res.data.code === 0) {
this.$message.success(res.data.msg);
this.reload();
} else {
this.$message.error(res.data.msg);
}
}).catch(e => {
hide();
this.$message.error(e.message);
});
},
/* 批量删除 */
removeBatch() {
if (!this.selection.length) {
this.$message.error('请至少选择一条数据');
return;
}
this.$confirm({
title: '提示',
content: '确定要删除选中的字典项吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
const hide = this.$message.loading('请求中..', 0);
this.$http.delete('/sys/dictdata/batch', {
data: this.selection.map(d => d.dictDataId)
}).then(res => {
hide();
if (res.data.code === 0) {
this.$message.success(res.data.msg);
this.reload();
} else {
this.$message.error(res.data.msg);
}
}).catch(e => {
hide();
this.$message.error(e.message);
});
}
});
}
},
watch: {
// 监听字典id变化
dictId() {
this.where.dictId = this.dictId;
this.reload();
}
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,129 @@
<!-- 字典编辑弹窗 -->
<template>
<a-modal
:width="460"
:visible="visible"
:confirm-loading="loading"
:title="isUpdate?'修改字典':'添加字典'"
:body-style="{paddingBottom: '8px'}"
@update:visible="updateVisible"
@ok="save">
<a-form
ref="form"
:model="form"
:rules="rules"
:label-col="{md: {span: 5}, sm: {span: 24}}"
:wrapper-col="{md: {span: 19}, sm: {span: 24}}">
<a-form-item label="字典名称:" name="dictName">
<a-input
allow-clear
:maxlength="20"
placeholder="请输入字典名称"
v-model:value="form.dictName"/>
</a-form-item>
<a-form-item label="字典值:" name="dictCode">
<a-input
allow-clear
:maxlength="20"
placeholder="请输入字典值"
v-model:value="form.dictCode"/>
</a-form-item>
<a-form-item label="排序号:" name="sortNumber">
<a-input-number
:min="0"
:max="9999"
class="ele-fluid"
placeholder="请输入排序号"
v-model:value="form.sortNumber"/>
</a-form-item>
<a-form-item label="备注:">
<a-textarea
:rows="4"
:maxlength="200"
placeholder="请输入备注"
v-model:value="form.comments"/>
</a-form-item>
</a-form>
</a-modal>
</template>
<script>
export default {
name: 'SysDictEdit',
emits: ['done', 'update:visible'],
props: {
// 弹窗是否打开
visible: Boolean,
// 修改回显的数据
data: Object
},
data() {
return {
// 表单数据
form: Object.assign({}, this.data),
// 表单验证规则
rules: {
dictName: [
{required: true, message: '请输入字典名称', type: 'string', trigger: 'blur'}
],
dictCode: [
{required: true, message: '请输入字典值', type: 'string', trigger: 'blur'}
],
sortNumber: [
{required: true, message: '请输入排序号', type: 'number', trigger: 'blur'}
]
},
// 提交状态
loading: false,
// 是否是修改
isUpdate: false
};
},
watch: {
data() {
if (this.data) {
this.form = Object.assign({}, this.data);
this.isUpdate = true;
} else {
this.form = {};
this.isUpdate = false;
}
if (this.$refs.form) {
this.$refs.form.clearValidate();
}
}
},
methods: {
/* 保存编辑 */
save() {
this.$refs.form.validate().then(() => {
this.loading = true;
this.$http[this.isUpdate ? 'put' : 'post']('/sys/dict', this.form).then(res => {
this.loading = false;
if (res.data.code === 0) {
this.$message.success(res.data.msg);
if (!this.isUpdate) {
this.form = {};
}
this.updateVisible(false);
this.$emit('done');
} else {
this.$message.error(res.data.msg);
}
}).catch(e => {
this.loading = false;
this.$message.error(e.message);
});
}).catch(() => {
});
},
/* 更新visible */
updateVisible(value) {
this.$emit('update:visible', value);
}
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,214 @@
<template>
<div class="ele-body">
<a-card :bordered="false">
<!-- 搜索表单 -->
<a-form
:model="where"
:label-col="{md: {span: 6}, sm: {span: 24}}"
:wrapper-col="{md: {span: 18}, sm: {span: 24}}">
<a-row>
<a-col :lg="6" :md="12" :sm="24" :xs="24">
<a-form-item label="用户账号:">
<a-input
v-model:value.trim="where.username"
placeholder="请输入"
allow-clear/>
</a-form-item>
</a-col>
<a-col :lg="6" :md="12" :sm="24" :xs="24">
<a-form-item label="用户名:">
<a-input
v-model:value.trim="where.nickname"
placeholder="请输入"
allow-clear/>
</a-form-item>
</a-col>
<a-col :lg="6" :md="12" :sm="24" :xs="24">
<a-form-item label="登录时间:">
<a-range-picker
v-model:value="daterange"
value-format="YYYY-MM-DD"
class="ele-fluid">
<template #suffixIcon>
<calendar-outlined/>
</template>
</a-range-picker>
</a-form-item>
</a-col>
<a-col :lg="6" :md="12" :sm="24" :xs="24">
<a-form-item class="ele-text-right" :wrapper-col="{span: 24}">
<a-space>
<a-button type="primary" @click="reload">查询</a-button>
<a-button @click="reset">重置</a-button>
</a-space>
</a-form-item>
</a-col>
</a-row>
</a-form>
<!-- 表格 -->
<ele-pro-table
ref="table"
row-key="id"
:datasource="url"
:columns="columns"
:where="where"
:scroll="{x: 'max-content'}">
<template #toolbar>
<a-space>
<a-button type="primary" @click="exportData">
<template #icon>
<download-outlined/>
</template>
<span>导出</span>
</a-button>
</a-space>
</template>
<template #operType="{ record }">
<a-tag :color="['green', 'red', '', 'orange'][record.operType]">
{{ ['登录成功', '登录失败', '退出登录', '刷新TOKEN'][record.operType] }}
</a-tag>
</template>
</ele-pro-table>
</a-card>
</div>
</template>
<script>
import XLSX from 'xlsx';
import {
DownloadOutlined,
CalendarOutlined
} from '@ant-design/icons-vue';
export default {
name: 'SystemLoginRecord',
components: {
DownloadOutlined,
CalendarOutlined
},
data() {
return {
// 表格数据接口
url: '/sys/loginRecord/page',
// 表格列配置
columns: [
{
key: 'index',
width: 48,
align: 'center',
customRender: ({index}) => this.$refs.table.tableIndex + index
},
{
title: '账号',
dataIndex: 'username',
sorter: true
},
{
title: '用户名',
dataIndex: 'nickname',
sorter: true
},
{
title: 'IP地址',
dataIndex: 'ip',
sorter: true
},
{
title: '设备型号',
dataIndex: 'device',
sorter: true
},
{
title: '操作系统',
dataIndex: 'os',
sorter: true
},
{
title: '浏览器',
dataIndex: 'browser',
sorter: true
},
{
title: '操作类型',
dataIndex: 'operType',
sorter: true,
width: 120,
slots: {customRender: 'operType'}
},
{
title: '备注',
dataIndex: 'comments',
sorter: true
},
{
title: '登录时间',
dataIndex: 'createTime',
sorter: true,
width: 150,
customRender: ({text}) => this.$util.toDateString(text)
}
],
// 表格搜索条件
where: {},
// 日期范围选择
daterange: []
};
},
methods: {
/* 刷新表格 */
reload() {
this.$refs.table.reload({page: 1, where: this.where});
},
/* 重置搜索 */
reset() {
this.where = {};
this.daterange = [];
this.reload();
},
/* 导出数据 */
exportData() {
let array = [['账号', '用户名', 'IP地址', '设备型号', '操作系统', '浏览器', '操作类型', '备注', '登录时间']];
// 请求查询全部(不分页)的接口
const hide = this.$message.loading('请求中..', 0);
this.$http.get('/sys/loginRecord/page?page=1&limit=2000').then(res => {
hide();
if (res.data.code === 0) {
res.data.data.forEach(d => {
array.push([
d.username,
d.nickname,
d.ip,
d.device,
d.os,
d.browser,
['登录成功', '登录失败', '退出登录', '刷新TOKEN'][d.operType],
d.comments,
this.$util.toDateString(d.createTime)
]);
});
this.$util.exportSheet(XLSX, array, '登录日志');
} else {
this.$message.error(res.data.msg);
}
}).catch(e => {
hide();
this.$message.error(e.message);
});
}
},
watch: {
daterange() {
if (this.daterange && this.daterange.length === 2) {
this.where.createTimeStart = this.daterange[0] + ' 00:00:00';
this.where.createTimeEnd = this.daterange[1] + ' 23:59:59';
} else {
this.where.createTimeStart = null;
this.where.createTimeEnd = null;
}
}
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,275 @@
<template>
<div class="ele-body">
<a-card :bordered="false">
<!-- 搜索表单 -->
<a-form
:model="where"
:label-col="{md: {span: 6}, sm: {span: 24}}"
:wrapper-col="{md: {span: 18}, sm: {span: 24}}">
<a-row>
<a-col :lg="6" :md="12" :sm="24" :xs="24">
<a-form-item label="菜单名称:">
<a-input
v-model:value.trim="where.title"
placeholder="请输入"
allow-clear/>
</a-form-item>
</a-col>
<a-col :lg="6" :md="12" :sm="24" :xs="24">
<a-form-item label="菜单地址:">
<a-input
v-model:value.trim="where.path"
placeholder="请输入"
allow-clear/>
</a-form-item>
</a-col>
<a-col :lg="6" :md="12" :sm="24" :xs="24">
<a-form-item label="权限标识:">
<a-input
v-model:value.trim="where.authority"
placeholder="请输入"
allow-clear/>
</a-form-item>
</a-col>
<a-col :lg="6" :md="12" :sm="24" :xs="24">
<a-form-item class="ele-text-right" :wrapper-col="{span: 24}">
<a-space>
<a-button type="primary" @click="reload">查询</a-button>
<a-button @click="reset">重置</a-button>
</a-space>
</a-form-item>
</a-col>
</a-row>
</a-form>
<!-- 表格 -->
<ele-pro-table
ref="table"
row-key="menuId"
:datasource="url"
:columns="columns"
:where="where"
:need-page="false"
:parse-data="parseData"
:expand-icon-column-index="1"
:expanded-row-keys="expandedRowKeys"
:scroll="{x: 'max-content'}"
@expandedRowsChange="onExpandedRowsChange">
<template #toolbar>
<a-space>
<a-button type="primary" @click="openEdit()">
<template #icon>
<plus-outlined/>
</template>
<span>新建</span>
</a-button>
<a-button @click="expandAll">展开全部</a-button>
<a-button @click="foldAll">折叠全部</a-button>
</a-space>
</template>
<template #menuType="{ record }">
<a-tag
v-if="isUrl(record.path)"
color="orange">外链
</a-tag>
<a-tag
v-else-if="isUrl(record.component)"
color="green">内链
</a-tag>
<a-tag
v-else
:color="['blue', ''][record.menuType]">
{{ ['菜单', '按钮'][record.menuType] }}
</a-tag>
</template>
<template #action="{ record }">
<a-space>
<a @click="openEdit(null, record.menuId)">添加</a>
<a-divider type="vertical"/>
<a @click="openEdit(record)">修改</a>
<a-divider type="vertical"/>
<a-popconfirm @confirm="remove(record)" title="确定要删除此菜单吗?">
<a class="ele-text-danger">删除</a>
</a-popconfirm>
</a-space>
</template>
</ele-pro-table>
</a-card>
</div>
<!-- 编辑弹窗 -->
<menu-edit
v-model:visible="showEdit"
:data="current"
:menu-list="menuList"
@done="reload"/>
</template>
<script>
import {PlusOutlined} from '@ant-design/icons-vue';
import MenuEdit from './menu-edit';
export default {
name: 'SystemMenu',
components: {PlusOutlined, MenuEdit},
data() {
return {
// 表格数据接口
url: '/sys/menu',
// 表格列配置
columns: [
{
key: 'index',
dataIndex: 'index',
width: 48,
align: 'center',
customRender: ({index}) => index + 1
},
{
title: '菜单名称',
dataIndex: 'title',
sorter: true
},
{
title: '路由地址',
dataIndex: 'path',
sorter: true
},
{
title: '组件路径',
dataIndex: 'component',
sorter: true
},
{
title: '权限标识',
dataIndex: 'authority',
sorter: true
},
{
title: '排序',
dataIndex: 'sortNumber',
sorter: true
},
{
title: '可见',
dataIndex: 'hide',
sorter: true,
customRender: ({text}) => ['是', '否'][text]
},
{
title: '类型',
dataIndex: 'menuType',
sorter: true,
slots: {customRender: 'menuType'}
},
{
title: '创建时间',
dataIndex: 'createTime',
sorter: true,
customRender: ({text}) => this.$util.toDateString(text)
},
{
title: '操作',
key: 'action',
width: 150,
align: 'center',
slots: {customRender: 'action'}
}
],
// 表格搜索条件
where: {},
// 表格选中数据
selection: [],
// 当前编辑数据
current: null,
// 是否显示编辑弹窗
showEdit: false,
// 表格展开的行
expandedRowKeys: [],
// 全部菜单数据
menuList: []
};
},
methods: {
/* 解析接口返回数据 */
parseData(res) {
res.data = this.$util.toTreeData(res.data.map(d => {
d.key = d.menuId;
d.value = d.menuId;
return d;
}), 'menuId', 'parentId');
if (!Object.keys(this.where).length) {
this.menuList = res.data;
}
if (!this.expandedRowKeys.length) {
this.expandAll();
}
return res;
},
/* 刷新表格 */
reload() {
this.$refs.table.reload({where: this.where});
},
/* 重置搜索 */
reset() {
this.where = {};
this.reload();
},
/* 打开编辑弹窗 */
openEdit(row, parentId) {
this.current = Object.assign({
menuType: 0,
hide: 0,
parentId: parentId
}, row);
this.showEdit = true;
},
/* 删除单个 */
remove(row) {
if (row.children && row.children.length > 0) {
this.$message.error('请先删除子节点');
return;
}
const hide = this.$message.loading('请求中..', 0);
this.$http.delete('/sys/menu/' + row.menuId).then(res => {
hide();
if (res.data.code === 0) {
this.$message.success(res.data.msg);
this.reload();
} else {
this.$message.error(res.data.msg);
}
}).catch(e => {
hide();
this.$message.error(e.message);
});
},
/* 展开全部 */
expandAll() {
let keys = [];
this.$util.eachTreeData(this.menuList, (d) => {
if (d.children && d.children.length) {
keys.push(d.menuId);
}
});
this.expandedRowKeys = keys;
},
/* 折叠全部 */
foldAll() {
this.expandedRowKeys = [];
},
/* 展开的行变化 */
onExpandedRowsChange(expandedRows) {
this.expandedRowKeys = expandedRows;
},
/* 判断是否是网址 */
isUrl(url) {
return url && (
url.startsWith('http://') ||
url.startsWith('https://') ||
url.startsWith('://'));
}
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,263 @@
<!-- 编辑弹窗 -->
<template>
<a-modal
:width="680"
:visible="visible"
:confirm-loading="loading"
:title="isUpdate?'修改菜单':'新建菜单'"
:body-style="{paddingBottom: '8px'}"
@update:visible="updateVisible"
@ok="save">
<a-form
ref="form"
:model="form"
:rules="rules"
:label-col="{md: {span: 7}, sm: {span: 24}}"
:wrapper-col="{md: {span: 17}, sm: {span: 24}}">
<a-row :gutter="16">
<a-col :md="12" :sm="24" :xs="24">
<a-form-item label="上级菜单" name="parentId">
<a-tree-select
allow-clear
:tree-data="menuList"
tree-default-expand-all
placeholder="请选择上级菜单"
v-model:value="form.parentId"
:dropdown-style="{maxHeight: '360px', overflow: 'auto'}"/>
</a-form-item>
<a-form-item label="菜单名称" name="title">
<a-input
allow-clear
placeholder="请输入菜单名称"
v-model:value="form.title"/>
</a-form-item>
</a-col>
<a-col :md="12" :sm="24" :xs="24">
<a-form-item label="菜单类型" name="menuType">
<a-radio-group
v-model:value="form.menuType"
@change="onMenuTypeChange">
<a-radio :value="0">菜单</a-radio>
<a-radio :value="1">按钮</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="打开方式">
<a-radio-group
v-model:value="form.openType"
:disabled="form.menuType === 1"
@change="onOpenTypeChange">
<a-radio :value="0">组件</a-radio>
<a-radio :value="1">内链</a-radio>
<a-radio :value="2">外链</a-radio>
</a-radio-group>
</a-form-item>
</a-col>
</a-row>
<div style="margin-bottom: 22px;">
<a-divider/>
</div>
<a-row :gutter="16">
<a-col :md="12" :sm="24" :xs="24">
<a-form-item label="菜单图标" name="icon">
<ele-icon-picker
v-model:value="form.icon"
:disabled="form.menuType===1"
placeholder="请选择菜单图标"/>
</a-form-item>
<a-form-item name="path">
<template #label>
<a-tooltip
v-if="form.openType === 2"
title="需要以`http://`、`https://`、`//`开头">
<question-circle-outlined
style="vertical-align: -2px;margin-right: 4px;"/>
</a-tooltip>
<span>{{ form.openType === 2 ? '外链地址' : '路由地址' }}</span>
</template>
<a-input
allow-clear
v-model:value="form.path"
:disabled="form.menuType===1"
:placeholder="form.openType === 2 ? '请输入外链地址' : '请输入路由地址'"/>
</a-form-item>
<a-form-item name="component">
<template #label>
<a-tooltip
v-if="form.openType === 1"
title="需要以`http://`、`https://`、`//`开头">
<question-circle-outlined
style="vertical-align: -2px;margin-right: 4px;"/>
</a-tooltip>
<span>{{ form.openType === 1 ? '内链地址' : '组件路径' }}</span>
</template>
<a-input
allow-clear
v-model:value="form.component"
:disabled="form.menuType === 1 || form.openType === 2"
:placeholder="form.openType === 1 ? '请输入内链地址' : '请输入组件路径'"/>
</a-form-item>
</a-col>
<a-col :md="12" :sm="24" :xs="24">
<a-form-item label="权限标识" name="authority">
<a-input
allow-clear
placeholder="请输入权限标识"
v-model:value="form.authority"
:disabled="form.menuType === 0"/>
</a-form-item>
<a-form-item label="排序号:" name="sortNumber">
<a-input-number
:min="0"
class="ele-fluid"
placeholder="请输入排序号"
v-model:value="form.sortNumber"/>
</a-form-item>
<a-form-item label="是否可见">
<a-switch
checked-children=""
un-checked-children=""
v-model:checked="form.isShow"
:disabled="form.menuType === 1"/>
<a-tooltip title="选择不可见只注册路由不显示在侧边栏,比如添加页面应该选择不可见">
<question-circle-outlined
style="vertical-align: -3px;margin-left: 16px;"/>
</a-tooltip>
</a-form-item>
</a-col>
</a-row>
</a-form>
</a-modal>
</template>
<script>
import EleIconPicker from 'ele-admin-pro/packages/ele-icon-picker';
import {QuestionCircleOutlined} from '@ant-design/icons-vue';
export default {
name: 'MenuEdit',
components: {EleIconPicker, QuestionCircleOutlined},
emits: [
'done',
'update:visible'
],
props: {
// 弹窗是否打开
visible: Boolean,
// 修改回显的数据
data: Object,
// 全部菜单数据
menuList: Array
},
data() {
return {
// 表单数据
form: this.initFormData(this.data),
// 表单验证规则
rules: {
title: [
{required: true, type: 'string', message: '请输入菜单名称', trigger: 'blur'}
],
sortNumber: [
{required: true, type: 'number', message: '请输入排序号', trigger: 'blur'}
]
},
// 提交状态
loading: false,
// 是否是修改
isUpdate: false
};
},
watch: {
data() {
this.isUpdate = !!(this.data && this.data.menuId);
this.form = this.initFormData(this.data);
if (this.$refs.form) {
this.$refs.form.clearValidate();
}
}
},
methods: {
/* 保存编辑 */
save() {
this.$refs.form.validate().then(() => {
this.loading = true;
this.$http[this.isUpdate ? 'put' : 'post']('/sys/menu',
Object.assign({}, this.form, {
parentId: this.form.parentId || 0,
hide: this.form.isShow ? 0 : 1
})
).then(res => {
this.loading = false;
if (res.data.code === 0) {
this.$message.success(res.data.msg);
this.updateVisible(false);
this.$emit('done');
} else {
this.$message.error(res.data.msg);
}
}).catch(e => {
this.loading = false;
this.$message.error(e.message);
});
}).catch(() => {
});
},
/* 更新visible */
updateVisible(value) {
this.$emit('update:visible', value);
},
/* menuType选择改变 */
onMenuTypeChange() {
if (this.form.menuType === 0) {
this.form.authority = '';
} else {
this.form.openType = 0;
this.form.icon = '';
this.form.path = '';
this.form.component = '';
this.form.hide = 0;
this.form.isShow = true;
}
},
/* openType选择改变 */
onOpenTypeChange() {
if (this.form.openType === 2) {
this.form.component = '';
}
},
/* 初始化form数据 */
initFormData(data) {
let form = {
menuType: 0,
openType: 0,
hide: 0,
isShow: true
};
if (data) {
let openType = 0;
if (this.isUrl(data.path)) {
openType = 2;
} else if (this.isUrl(data.component)) {
openType = 1;
}
Object.assign(form, data, {
parentId: data.parentId === 0 ? null : data.parentId,
isShow: data.hide === 0,
openType: openType
});
}
return form;
},
/* 判断是否是网址 */
isUrl(url) {
return url && (
url.startsWith('http://') ||
url.startsWith('https://') ||
url.startsWith('://'));
}
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,243 @@
<template>
<div class="ele-body">
<a-card :bordered="false">
<!-- 搜索表单 -->
<a-form
:model="where"
:label-col="{md: {span: 6}, sm: {span: 24}}"
:wrapper-col="{md: {span: 18}, sm: {span: 24}}">
<a-row>
<a-col :lg="6" :md="12" :sm="24" :xs="24">
<a-form-item label="用户账号:">
<a-input
v-model:value.trim="where.username"
placeholder="请输入"
allow-clear/>
</a-form-item>
</a-col>
<a-col :lg="6" :md="12" :sm="24" :xs="24">
<a-form-item label="操作模块:">
<a-input
v-model:value.trim="where.model"
placeholder="请输入"
allow-clear/>
</a-form-item>
</a-col>
<a-col :lg="6" :md="12" :sm="24" :xs="24">
<a-form-item label="操作时间:">
<a-range-picker
v-model:value="daterange"
:show-time="true"
value-format="YYYY-MM-DD HH:mm:ss"
class="ele-fluid">
<template #suffixIcon>
<calendar-outlined/>
</template>
</a-range-picker>
</a-form-item>
</a-col>
<a-col :lg="6" :md="12" :sm="24" :xs="24">
<a-form-item class="ele-text-right" :wrapper-col="{span: 24}">
<a-space>
<a-button type="primary" @click="reload">查询</a-button>
<a-button @click="reset">重置</a-button>
</a-space>
</a-form-item>
</a-col>
</a-row>
</a-form>
<!-- 表格 -->
<ele-pro-table
ref="table"
row-key="id"
:datasource="url"
:columns="columns"
:where="where"
:scroll="{x: 'max-content'}">
<template #toolbar>
<a-space>
<a-button type="primary" @click="exportData">
<template #icon>
<download-outlined/>
</template>
<span>导出</span>
</a-button>
</a-space>
</template>
<template #state="{ record }">
<a-tag :color="['green', 'red'][record.state]">
{{ ['正常', '异常'][record.state] }}
</a-tag>
</template>
<template #action="{ record }">
<a @click="openDetail(record)">详情</a>
</template>
</ele-pro-table>
</a-card>
</div>
<!-- 详情弹窗 -->
<oper-record-detail
v-model:visible="showInfo"
:data="current||{}"/>
</template>
<script>
import XLSX from 'xlsx';
import {
DownloadOutlined,
CalendarOutlined
} from '@ant-design/icons-vue';
import OperRecordDetail from './oper-record-detail';
export default {
name: 'SystemOperRecord',
components: {
DownloadOutlined,
CalendarOutlined,
OperRecordDetail
},
data() {
return {
// 表格数据接口
url: '/sys/operRecord/page',
// 表格列配置
columns: [
{
key: 'index',
width: 48,
align: 'center',
customRender: ({index}) => this.$refs.table.tableIndex + index
},
{
title: '账号',
dataIndex: 'username',
sorter: true
},
{
title: '用户名',
dataIndex: 'nickname',
sorter: true
},
{
title: '操作模块',
dataIndex: 'model',
sorter: true
},
{
title: '操作功能',
dataIndex: 'description',
sorter: true
},
{
title: '请求地址',
dataIndex: 'url',
sorter: true
},
{
title: '方式',
dataIndex: 'requestMethod',
sorter: true,
width: 90
},
{
title: '状态',
dataIndex: 'state',
sorter: true,
width: 90,
slots: {customRender: 'state'}
},
{
title: '耗时',
dataIndex: 'spendTime',
sorter: true,
width: 100,
customRender: ({text}) => text / 1000 + 's'
},
{
title: '操作时间',
dataIndex: 'createTime',
sorter: true,
width: 150,
customRender: ({text}) => this.$util.toDateString(text)
},
{
title: '操作',
key: 'action',
width: 90,
align: 'center',
slots: {customRender: 'action'}
}
],
// 表格搜索条件
where: {},
// 当前选中数据
current: null,
// 是否显示查看弹窗
showInfo: false,
// 日期范围选择
daterange: []
};
},
methods: {
/* 刷新表格 */
reload() {
this.$refs.table.reload({page: 1, where: this.where});
},
/* 重置搜索 */
reset() {
this.where = {};
this.daterange = [];
this.reload();
},
/* 详情 */
openDetail(row) {
this.current = row;
this.showInfo = true;
},
/* 导出数据 */
exportData() {
let array = [['账号', '用户名', '操作模块', '操作功能', '请求地址', '请求方式', '状态', '耗时', '操作时间']];
// 请求查询全部(不分页)的接口
const hide = this.$message.loading('请求中..', 0);
this.$http.get('/sys/loginRecord/page?page=1&limit=2000').then(res => {
hide();
if (res.data.code === 0) {
res.data.data.forEach(d => {
array.push([
d.username,
d.nickname,
d.model,
d.description,
d.url,
d.requestMethod,
['正常', '异常'][d.state],
d.spendTime,
this.$util.toDateString(d.createTime)
]);
});
this.$util.exportSheet(XLSX, array, '操作日志');
} else {
this.$message.error(res.data.msg);
}
}).catch(e => {
hide();
this.$message.error(e.message);
});
}
},
watch: {
daterange() {
if (this.daterange && this.daterange.length === 2) {
this.where.createTimeStart = this.daterange[0];
this.where.createTimeEnd = this.daterange[1];
} else {
this.where.createTimeStart = null;
this.where.createTimeEnd = null;
}
}
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,126 @@
<!-- 详情弹窗 -->
<template>
<a-modal
title="详情"
:width="640"
:footer="null"
:visible="visible"
@update:visible="updateVisible">
<a-form
class="ele-form-detail"
:label-col="{sm: {span: 8}, xs: {span: 6}}"
:wrapper-col="{sm: {span: 16}, xs: {span: 18}}">
<a-row :gutter="16">
<a-col :sm="12" :xs="24">
<a-form-item label="操作人:">
<div class="ele-text-secondary">
{{ data.nickname }}({{ data.username }})
</div>
</a-form-item>
<a-form-item label="操作模块:">
<div class="ele-text-secondary">
{{ data.model }}
</div>
</a-form-item>
<a-form-item label="操作时间:">
<div class="ele-text-secondary">
{{ $util.toDateString(data.createTime) }}
</div>
</a-form-item>
<a-form-item label="请求方式:">
<div class="ele-text-secondary">
{{ data.requestMethod }}
</div>
</a-form-item>
</a-col>
<a-col :sm="12" :xs="24">
<a-form-item label="IP地址:">
<div class="ele-text-secondary">
{{ data.ip }}
</div>
</a-form-item>
<a-form-item label="操作功能:">
<div class="ele-text-secondary">
{{ data.description }}
</div>
</a-form-item>
<a-form-item label="请求耗时:">
<div class="ele-text-secondary">
{{ data.spendTime / 1000 }}s
</div>
</a-form-item>
<a-form-item label="请求状态:">
<a-tag :color="['green', 'red'][data.state]">
{{ ['正常', '异常'][data.state] }}
</a-tag>
</a-form-item>
</a-col>
</a-row>
<div style="margin: 12px 0;">
<a-divider/>
</div>
<a-form-item
label="请求地址:"
:label-col="{sm: {span: 4}, xs: {span: 6}}"
:wrapper-col="{sm: {span: 20}, xs: {span: 18}}">
<div class="ele-text-secondary">
{{ data.url }}
</div>
</a-form-item>
<a-form-item
label="调用方法:"
:label-col="{sm: {span: 4}, xs: {span: 6}}"
:wrapper-col="{sm: {span: 20}, xs: {span: 18}}">
<div class="ele-text-secondary">
{{ data.operMethod }}
</div>
</a-form-item>
<a-form-item
label="请求参数:"
:label-col="{sm: {span: 4}, xs: {span: 6}}"
:wrapper-col="{sm: {span: 20}, xs: {span: 18}}">
<div class="ele-text-secondary">
{{ data.param }}
</div>
</a-form-item>
<a-form-item
label="返回结果:"
:label-col="{sm: {span: 4}, xs: {span: 6}}"
:wrapper-col="{sm: {span: 20}, xs: {span: 18}}">
<div class="ele-text-secondary">
{{ data.result }}
</div>
</a-form-item>
<a-form-item
label="备注:"
:label-col="{sm: {span: 4}, xs: {span: 6}}"
:wrapper-col="{sm: {span: 20}, xs: {span: 18}}">
<div class="ele-text-secondary">
{{ data.comments }}
</div>
</a-form-item>
</a-form>
</a-modal>
</template>
<script>
export default {
name: 'OperRecordDetail',
emits: ['update:visible'],
props: {
// 弹窗是否打开
visible: Boolean,
// 数据
data: Object
},
methods: {
/* 更新visible */
updateVisible(value) {
this.$emit('update:visible', value);
}
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,166 @@
<template>
<div class="ele-body ele-body-card">
<a-row :gutter="16">
<a-col :lg="6" :md="24" :sm="24" :xs="24">
<a-card
:bordered="false"
:body-style="{padding: '24px 16px'}">
<div class="ele-table-tool">
<a-space size="middle">
<a-button
type="primary"
@click="openEdit()">新建
</a-button>
<a-button
type="primary"
@click="openEdit(current)"
:disabled="!current">修改
</a-button>
<a-button
danger
type="primary"
@click="remove"
:disabled="!current">删除
</a-button>
</a-space>
</div>
<a-tree
:tree-data="data"
v-model:expanded-keys="expandedRowKeys"
v-model:selected-keys="selectedRowKeys"
@select="onTreeSelect"/>
</a-card>
</a-col>
<a-col :lg="18" :md="24" :sm="24" :xs="24">
<a-card :bordered="false">
<org-user-list
v-if="current"
:organization-id="current.organizationId"
:organization-list="data"/>
</a-card>
</a-col>
</a-row>
</div>
<!-- 编辑弹窗 -->
<org-edit
v-model:visible="showEdit"
:data="editData"
:organization-list="data"
@done="query"/>
</template>
<script>
import {createVNode} from 'vue';
import {ExclamationCircleOutlined} from '@ant-design/icons-vue';
import OrgUserList from './org-user-list';
import OrgEdit from './org-edit';
export default {
name: 'SystemOrganization',
components: {
OrgUserList,
OrgEdit
},
data() {
return {
// 加载状态
loading: true,
// 树形数据
data: [],
// 树展开的key
expandedRowKeys: [],
// 树选中的key
selectedRowKeys: [],
// 选中数据
current: null,
// 是否显示表单弹窗
showEdit: false,
// 编辑回显数据
editData: null
};
},
mounted() {
this.query();
},
methods: {
/* 查询 */
query() {
this.loading = true;
this.$http.get('/sys/organization').then(res => {
this.loading = false;
if (res.data.code === 0) {
let eks = [];
res.data.data.forEach(d => {
d.key = d.organizationId;
d.value = d.organizationId;
d.title = d.organizationName;
eks.push(d.key);
});
this.expandedRowKeys = eks;
this.data = this.$util.toTreeData(res.data.data, 'organizationId', 'parentId');
if (this.data.length) {
this.selectedRowKeys = [this.data[0].key];
this.onTreeSelect();
} else {
this.selectedRowKeys = [];
this.current = null;
}
} else {
this.$message.error(res.data.msg);
}
}).catch(e => {
this.loading = false;
this.$message.error(e.message);
});
},
/* 选择数据 */
onTreeSelect() {
this.$util.eachTreeData(this.data, (d) => {
if (this.selectedRowKeys.indexOf(d.key) !== -1) {
this.current = d;
return false;
}
});
},
/* 打开编辑弹窗 */
openEdit(item) {
this.editData = Object.assign({}, {
parentId: this.current.parentId
}, item);
this.showEdit = true;
},
/* 删除 */
remove() {
this.$confirm({
title: '提示',
content: '确定要删除选中的机构吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
const hide = this.$message.loading('请求中..', 0);
this.$http.delete('/sys/organization/' + this.current.organizationId).then(res => {
hide();
if (res.data.code === 0) {
this.$message.success(res.data.msg);
this.query();
} else {
this.$message.error(res.data.msg);
}
}).catch(e => {
hide();
this.$message.error(e.message);
});
}
});
}
}
}
</script>
<style scoped>
@media screen and (min-width: 768px) {
.ant-card {
min-height: calc(100vh - 122px);
}
}
</style>

View File

@@ -0,0 +1,201 @@
<!-- 机构编辑弹窗 -->
<template>
<a-modal
:width="680"
:visible="visible"
:confirm-loading="loading"
:title="isUpdate?'修改机构':'添加机构'"
:body-style="{paddingBottom: '8px'}"
@update:visible="updateVisible"
@ok="save">
<a-form
ref="form"
:model="form"
:rules="rules"
:label-col="{md: {span: 7}, sm: {span: 24}}"
:wrapper-col="{md: {span: 17}, sm: {span: 24}}">
<a-row :gutter="16">
<a-col :md="12" :sm="24" :xs="24">
<a-form-item label="上级机构:" name="parentId">
<a-tree-select
allow-clear
tree-default-expand-all
placeholder="请选择上级机构"
v-model:value="form.parentId"
:tree-data="organizationList"
:dropdown-style="{maxHeight: '360px', overflow: 'auto'}"/>
</a-form-item>
<a-form-item label="机构名称:" name="organizationName">
<a-input
allow-clear
:maxlength="20"
placeholder="请输入机构名称"
v-model:value="form.organizationName"/>
</a-form-item>
<a-form-item label="机构全称:" name="organizationFullName">
<a-input
allow-clear
:maxlength="100"
placeholder="请输入机构全称"
v-model:value="form.organizationFullName"/>
</a-form-item>
<a-form-item label="机构代码:" name="organizationCode">
<a-input
allow-clear
:maxlength="20"
placeholder="请输入机构代码"
v-model:value="form.organizationCode"/>
</a-form-item>
</a-col>
<a-col :md="12" :sm="24" :xs="24">
<a-form-item label="机构类型:" name="organizationType">
<a-select
allow-clear
placeholder="请选择机构类型"
v-model:value="form.organizationType">
<a-select-option
v-for="item in organizationTypeList"
:key="item.dictDataId"
:value="item.dictDataId">
{{ item.dictDataName }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="排序号:" name="sortNumber">
<a-input-number
:min="0"
:max="99999"
class="ele-fluid"
placeholder="请输入排序号"
v-model:value="form.sortNumber"/>
</a-form-item>
<a-form-item label="备注:">
<a-textarea
:rows="4"
:maxlength="200"
placeholder="请输入备注"
v-model:value="form.comments"/>
</a-form-item>
</a-col>
</a-row>
</a-form>
</a-modal>
</template>
<script>
export default {
name: 'OrgEdit',
emits: ['done', 'update:visible'],
props: {
// 弹窗是否打开
visible: Boolean,
// 修改回显的数据
data: Object,
// 全部机构
organizationList: Array
},
data() {
return {
// 表单数据
form: Object.assign({}, this.data, {
parentId: this.data ? (this.data.parentId === 0 ? null : this.data.parentId) : null
}),
// 表单验证规则
rules: {
organizationName: [
{required: true, message: '请输入机构名称', type: 'string', trigger: 'blur'}
],
organizationFullName: [
{required: true, message: '请输入机构全称', type: 'string', trigger: 'blur'}
],
organizationCode: [
{required: true, message: '请输入机构代码', type: 'string', trigger: 'blur'}
],
organizationType: [
{required: true, message: '请选择机构类型', type: 'number', trigger: 'blur'}
],
sortNumber: [
{required: true, message: '请输入排序号', type: 'number', trigger: 'blur'}
]
},
// 提交状态
loading: false,
// 是否是修改
isUpdate: false,
// 机构类型列表
organizationTypeList: []
};
},
watch: {
data() {
if (this.data) {
this.form = Object.assign({}, this.data, {
parentId: this.data.parentId === 0 ? null : this.data.parentId
});
this.isUpdate = !!this.data.organizationId;
} else {
this.form = {};
this.isUpdate = false;
}
if (this.$refs.form) {
this.$refs.form.clearValidate();
}
}
},
mounted() {
this.queryOrganizationType(); // 获取机构类型
},
methods: {
/* 保存编辑 */
save() {
this.$refs.form.validate().then(() => {
this.loading = true;
this.$http[this.isUpdate ? 'put' : 'post']('/sys/organization',
Object.assign({}, this.form, {
parentId: this.form.parentId || 0
})
).then(res => {
this.loading = false;
if (res.data.code === 0) {
this.$message.success(res.data.msg);
if (!this.isUpdate) {
this.form = {};
}
this.updateVisible(false);
this.$emit('done');
} else {
this.$message.error(res.data.msg);
}
}).catch(e => {
this.loading = false;
this.$message.error(e.message);
});
}).catch(() => {
});
},
/* 更新visible */
updateVisible(value) {
this.$emit('update:visible', value);
},
/* 查询机构类型 */
queryOrganizationType() {
this.$http.get('/sys/dictdata', {
params: {
dictCode: 'organization_type'
}
}).then(res => {
if (res.data.code === 0) {
this.organizationTypeList = res.data.data;
} else {
this.$message.error(res.data.msg);
}
}).catch(e => {
this.$message.error(e.message);
});
}
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,248 @@
<!-- 用户编辑弹窗 -->
<template>
<a-modal
:width="680"
:visible="visible"
:confirm-loading="loading"
:title="isUpdate?'修改用户':'新建用户'"
:body-style="{paddingBottom: '8px'}"
@update:visible="updateVisible"
@ok="save">
<a-form
ref="form"
:model="form"
:rules="rules"
:label-col="{md: {span: 7}, sm: {span: 24}}"
:wrapper-col="{md: {span: 17}, sm: {span: 24}}">
<a-row :gutter="16">
<a-col :md="12" :sm="24" :xs="24">
<a-form-item label="所属机构:">
<a-tree-select
allow-clear
tree-default-expand-all
placeholder="请选择所属机构"
v-model:value="form.organizationId"
:tree-data="organizationList"
:dropdown-style="{maxHeight: '360px', overflow: 'auto'}"/>
</a-form-item>
<a-form-item label="用户账号:" name="username">
<a-input
allow-clear
:maxlength="20"
placeholder="请输入用户账号"
v-model:value="form.username"/>
</a-form-item>
<a-form-item label="用户名:" name="nickname">
<a-input
allow-clear
:maxlength="20"
placeholder="请输入用户名"
v-model:value="form.nickname"/>
</a-form-item>
<a-form-item label="性别:" name="sex">
<a-select
allow-clear
placeholder="请选择性别"
v-model:value="form.sex">
<a-select-option :value="1"></a-select-option>
<a-select-option :value="2"></a-select-option>
</a-select>
</a-form-item>
<a-form-item label="角色:" name="roleIds">
<a-select
allow-clear
mode="multiple"
placeholder="请选择角色"
v-model:value="form.roleIds">
<a-select-option
v-for="item in roleList"
:key="item.roleId"
:value="item.roleId">
{{ item.roleName }}
</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :md="12" :sm="24" :xs="24">
<a-form-item label="手机号:" name="phone">
<a-input
allow-clear
:maxlength="11"
placeholder="请输入手机号"
v-model:value="form.phone"/>
</a-form-item>
<a-form-item label="邮箱:" name="email">
<a-input
allow-clear
:maxlength="100"
placeholder="请输入邮箱"
v-model:value="form.email"/>
</a-form-item>
<a-form-item
v-if="!isUpdate"
label="登录密码:"
name="password">
<a-input-password
:maxlength="20"
v-model:value="form.password"
placeholder="请输入登录密码"/>
</a-form-item>
<a-form-item label="个人简介:">
<a-textarea
:rows="4"
:maxlength="200"
placeholder="请输入个人简介"
v-model:value="form.introduction"/>
</a-form-item>
</a-col>
</a-row>
</a-form>
</a-modal>
</template>
<script>
import validate from 'ele-admin-pro/packages/validate';
export default {
name: 'OrgUserEdit',
emits: ['done', 'update:visible'],
props: {
// 弹窗是否打开
visible: Boolean,
// 修改回显的数据
data: Object,
// 全部机构
organizationList: Array,
// 机构id
organizationId: Number
},
data() {
return {
// 表单数据
form: Object.assign({}, this.data, {
organizationId: this.organizationId
}),
// 表单验证规则
rules: {
username: [
{
required: true,
type: 'string',
trigger: 'blur',
asyncValidator: (rule, value) => {
return new Promise((resolve, reject) => {
if (!value) {
return reject('请输入用户账号');
}
this.$http.get('/sys/user?username=' + value).then(res => {
if (res.data.code !== 0 || !res.data.data.length) {
return resolve();
}
if (this.isUpdate && res.data.data[0].username === this.data.username) {
return resolve();
}
reject('账号已经存在');
}).catch(() => {
resolve();
});
});
}
}
],
nickname: [
{required: true, message: '请输入用户名', type: 'string', trigger: 'blur'}
],
sex: [
{required: true, message: '请选择性别', type: 'number', trigger: 'blur'}
],
roleIds: [
{required: true, message: '请选择角色', type: 'array', trigger: 'blur'}
],
email: [
{pattern: validate.email, message: '邮箱格式不正确', type: 'string', trigger: 'blur'}
],
password: [
{required: true, pattern: /^[\S]{5,18}$/, message: '密码必须为5-18位非空白字符', type: 'string', trigger: 'blur'}
],
phone: [
{pattern: validate.phone, message: '手机号格式不正确', type: 'string', trigger: 'blur'}
]
},
// 提交状态
loading: false,
// 是否是修改
isUpdate: false,
// 角色列表
roleList: []
};
},
watch: {
data() {
if (this.data) {
this.form = Object.assign({}, this.data, {
roleIds: this.data.roles.map(d => d.roleId)
});
this.isUpdate = true;
} else {
this.form = {organizationId: this.organizationId};
this.isUpdate = false;
}
if (this.$refs.form) {
this.$refs.form.clearValidate();
}
},
organizationId() {
if (!this.isUpdate) {
this.form = {organizationId: this.organizationId};
}
}
},
mounted() {
this.queryRoles(); // 查询角色列表
},
methods: {
/* 保存编辑 */
save() {
this.$refs.form.validate().then(() => {
this.loading = true;
this.$http[this.isUpdate ? 'put' : 'post']('/sys/user', this.form).then(res => {
this.loading = false;
if (res.data.code === 0) {
this.$message.success(res.data.msg);
if (!this.isUpdate) {
this.form = {};
}
this.updateVisible(false);
this.$emit('done');
} else {
this.$message.error(res.data.msg);
}
}).catch(e => {
this.loading = false;
this.$message.error(e.message);
});
}).catch(() => {
});
},
/* 更新visible */
updateVisible(value) {
this.$emit('update:visible', value);
},
/* 查询角色列表 */
queryRoles() {
this.$http.get('/sys/role').then(res => {
if (res.data.code === 0) {
this.roleList = res.data.data;
} else {
this.$message.error(res.data.msg);
}
}).catch(e => {
this.$message.error(e.message);
});
}
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,207 @@
<template>
<!-- 表格 -->
<ele-pro-table
ref="table"
row-key="userId"
:datasource="url"
:columns="columns"
:where="where"
tool-class="ele-toolbar-form"
:scroll="{x: 'max-content'}">
<template #toolbar>
<a-row :gutter="16">
<a-col :lg="6" :md="8" :sm="24" :xs="24">
<a-input
v-model:value.trim="where.username"
placeholder="请输入用户账号"
allow-clear/>
</a-col>
<a-col :lg="6" :md="8" :sm="24" :xs="24">
<a-input
v-model:value.trim="where.nickname"
placeholder="请输入用户名"
allow-clear/>
</a-col>
<a-col :lg="6" :md="8" :sm="24" :xs="24">
<a-space size="middle">
<a-button
type="primary"
@click="reload">查询
</a-button>
<a-button
type="primary"
@click="openEdit()">新建
</a-button>
</a-space>
</a-col>
</a-row>
</template>
<template #roles="{ record }">
<a-tag
v-for="(item, index) in record.roles"
:key="index"
color="blue">{{ item.roleName }}
</a-tag>
</template>
<template #state="{ text,record }">
<a-switch
:checked="text===0"
@change="(checked) => changeState(checked, record)"/>
</template>
<template #action="{ record }">
<a-space>
<a @click="openEdit(record)">修改</a>
<a-divider type="vertical"/>
<a-popconfirm
title="确定要删除此用户吗?"
@confirm="remove(record)">
<a class="ele-text-danger">删除</a>
</a-popconfirm>
</a-space>
</template>
</ele-pro-table>
<!-- 编辑弹窗 -->
<org-user-edit
v-model:visible="showEdit"
:data="current"
:organization-list="organizationList"
:organization-id="organizationId"
@done="reload"/>
</template>
<script>
import OrgUserEdit from './org-user-edit';
export default {
name: 'SysOrgUserList',
components: {OrgUserEdit},
props: {
// 机构id
organizationId: Number,
// 全部机构
organizationList: Array
},
data() {
return {
// 表格数据接口
url: '/sys/user/page',
// 表格列配置
columns: [
{
key: 'index',
width: 48,
align: 'center',
customRender: ({index}) => this.$refs.table.tableIndex + index
},
{
title: '用户账号',
dataIndex: 'username',
sorter: true
},
{
title: '用户名',
dataIndex: 'nickname',
sorter: true
},
{
title: '性别',
dataIndex: 'sexName',
sorter: true
},
{
title: '手机号',
dataIndex: 'phone',
sorter: true,
},
{
title: '角色',
key: 'roles',
slots: {customRender: 'roles'}
},
{
title: '创建时间',
dataIndex: 'createTime',
sorter: true,
width: 150,
customRender: ({text}) => this.$util.toDateString(text)
},
{
title: '状态',
dataIndex: 'state',
sorter: true,
width: 90,
align: 'center',
slots: {customRender: 'state'}
},
{
title: '操作',
key: 'action',
width: 120,
align: 'center',
slots: {customRender: 'action'}
}
],
// 表格搜索条件
where: {
organizationId: this.organizationId
},
// 当前编辑数据
current: null,
// 是否显示编辑弹窗
showEdit: false
};
},
methods: {
/* 刷新表格 */
reload() {
this.$refs.table.reload({page: 1, where: this.where});
},
/* 打开编辑弹窗 */
openEdit(row) {
this.current = row;
this.showEdit = true;
},
/* 删除单个 */
remove(row) {
const hide = this.$message.loading('请求中..', 0);
this.$http.delete('/sys/user/' + row.userId).then(res => {
hide();
if (res.data.code === 0) {
this.$message.success(res.data.msg);
this.reload();
} else {
this.$message.error(res.data.msg);
}
}).catch(e => {
hide();
this.$message.error(e.message);
});
},
/* 修改用户状态 */
changeState(checked, row) {
let params = new FormData();
params.append('state', checked ? 0 : 1);
this.$http.put('/sys/user/state/' + row.userId, params).then(res => {
if (res.data.code === 0) {
row.state = checked ? 0 : 1;
this.$message.success(res.data.msg);
} else {
this.$message.error(res.data.msg);
}
}).catch(e => {
this.$message.error(e.message);
});
}
},
watch: {
/* 监听机构id变化 */
organizationId() {
this.where.organizationId = this.organizationId;
this.reload();
}
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,238 @@
<template>
<div class="ele-body">
<a-card :bordered="false">
<!-- 搜索表单 -->
<a-form
:model="where"
:label-col="{md: {span: 6}, sm: {span: 24}}"
:wrapper-col="{md: {span: 18}, sm: {span: 24}}">
<a-row>
<a-col :lg="6" :md="12" :sm="24" :xs="24">
<a-form-item label="角色名称:">
<a-input
v-model:value.trim="where.roleName"
placeholder="请输入"
allow-clear/>
</a-form-item>
</a-col>
<a-col :lg="6" :md="12" :sm="24" :xs="24">
<a-form-item label="角色标识:">
<a-input
v-model:value.trim="where.roleCode"
placeholder="请输入"
allow-clear/>
</a-form-item>
</a-col>
<a-col :lg="6" :md="12" :sm="24" :xs="24">
<a-form-item label="备注:">
<a-input
v-model:value.trim="where.comments"
placeholder="请输入"
allow-clear/>
</a-form-item>
</a-col>
<a-col :lg="6" :md="12" :sm="24" :xs="24">
<a-form-item class="ele-text-right" :wrapper-col="{span: 24}">
<a-space>
<a-button type="primary" @click="reload">查询</a-button>
<a-button @click="reset">重置</a-button>
</a-space>
</a-form-item>
</a-col>
</a-row>
</a-form>
<!-- 表格 -->
<ele-pro-table
ref="table"
row-key="roleId"
:datasource="url"
:columns="columns"
:where="where"
v-model:selection="selection"
:scroll="{x: 'max-content'}">
<template #toolbar>
<a-space>
<a-button type="primary" @click="openEdit()">
<template #icon>
<plus-outlined/>
</template>
<span>新建</span>
</a-button>
<a-button type="primary" danger @click="removeBatch">
<template #icon>
<delete-outlined/>
</template>
<span>删除</span>
</a-button>
</a-space>
</template>
<template #action="{ record }">
<a-space>
<a @click="openEdit(record)">修改</a>
<a-divider type="vertical"/>
<a @click="openAuth(record)">分配权限</a>
<a-divider type="vertical"/>
<a-popconfirm
title="确定要删除此角色吗?"
@confirm="remove(record)">
<a class="ele-text-danger">删除</a>
</a-popconfirm>
</a-space>
</template>
</ele-pro-table>
</a-card>
</div>
<!-- 编辑弹窗 -->
<role-edit
v-model:visible="showEdit"
:data="current"
@done="reload"/>
<!-- 权限分配弹窗 -->
<role-auth
v-model:visible="showAuth"
:data="current"/>
</template>
<script>
import {createVNode} from 'vue'
import {
PlusOutlined,
DeleteOutlined,
ExclamationCircleOutlined
} from '@ant-design/icons-vue';
import RoleEdit from './role-edit';
import RoleAuth from './role-auth';
export default {
name: 'SystemRole',
components: {
PlusOutlined,
DeleteOutlined,
RoleEdit,
RoleAuth
},
data() {
return {
// 表格数据接口
url: '/sys/role/page',
// 表格列配置
columns: [
{
key: 'index',
width: 48,
customRender: ({index}) => this.$refs.table.tableIndex + index
},
{
title: '角色名称',
dataIndex: 'roleName',
sorter: true
},
{
title: '角色标识',
dataIndex: 'roleCode',
sorter: true
},
{
title: '备注',
dataIndex: 'comments',
sorter: true
},
{
title: '创建时间',
dataIndex: 'createTime',
sorter: true,
customRender: ({text}) => this.$util.toDateString(text)
},
{
title: '操作',
key: 'action',
width: 200,
align: 'center',
slots: {customRender: 'action'}
}
],
// 表格搜索条件
where: {},
// 表格选中数据
selection: [],
// 当前编辑数据
current: null,
// 是否显示编辑弹窗
showEdit: false,
// 是否显示权限分配弹窗
showAuth: false
};
},
methods: {
/* 搜索 */
reload() {
this.selection = [];
this.$refs.table.reload({page: 1, where: this.where});
},
/* 重置搜索 */
reset() {
this.where = {};
this.reload();
},
/* 删除单个 */
remove(row) {
const hide = this.$message.loading('请求中..', 0);
this.$http.delete('/sys/role/' + row.roleId).then(res => {
hide();
if (res.data.code === 0) {
this.$message.success(res.data.msg);
this.reload();
} else {
this.$message.error(res.data.msg);
}
}).catch(e => {
hide();
this.$message.error(e.message);
});
},
/* 批量删除 */
removeBatch() {
if (!this.selection.length) {
this.$message.error('请至少选择一条数据');
return;
}
this.$confirm({
title: '提示',
content: '确定要删除选中的角色吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
const hide = this.$message.loading('请求中..', 0);
this.$http.delete('/sys/role/batch', {
data: this.selection.map(d => d.roleId)
}).then(res => {
hide();
if (res.data.code === 0) {
this.$message.success(res.data.msg);
this.reload();
} else {
this.$message.error(res.data.msg);
}
}).catch(e => {
hide();
this.$message.error(e.message);
});
}
});
},
/* 打开编辑弹窗 */
openEdit(row) {
this.current = row;
this.showEdit = true;
},
/* 打开权限分配弹窗 */
openAuth(row) {
this.current = row;
this.showAuth = true;
}
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,134 @@
<!-- 角色权限分配弹窗 -->
<template>
<a-modal
:width="460"
title="分配权限"
:visible="visible"
:confirm-loading="loading"
@update:visible="updateVisible"
@ok="save">
<a-spin :spinning="authLoading">
<div style="height: 60vh;" class="ele-scrollbar-hover">
<a-tree
checkable
:tree-data="authData"
v-model:expandedKeys="expandKeys"
v-model:checkedKeys="checkedKeys"/>
</div>
</a-spin>
</a-modal>
</template>
<script>
export default {
name: 'RoleAuth',
emits: ['update:visible'],
props: {
// 弹窗是否打开
visible: Boolean,
// 当前角色数据
data: Object
},
data() {
return {
// 权限数据
authData: [],
// 权限数据请求状态
authLoading: false,
// 提交状态
loading: false,
// 角色权限展开的keys
expandKeys: [],
// 角色权限选中的keys
checkedKeys: [],
};
},
watch: {
visible() {
if (this.visible) {
this.query();
}
}
},
methods: {
/* 查询权限数据 */
query() {
this.authData = [];
this.expandKeys = [];
this.checkedKeys = [];
if (!this.data) {
return;
}
this.authLoading = true;
this.$http.get('/sys/role/menu', {
params: {
roleId: this.data.roleId
}
}).then(res => {
this.authLoading = false;
if (res.data.code === 0) {
let eks = [], cks = [];
// 转成树形结构的数据
this.authData = this.$util.toTreeData({
data: res.data.data,
idKey: 'menuId',
pidKey: 'parentId',
addPIds: true,
parentIds: []
});
// 全部默认展开以及回显选中的数据
this.$util.eachTreeData(this.authData, (d) => {
d.key = d.menuId;
eks.push(d.key);
if (d.checked && (!d.children || !d.children.length)) {
cks.push(d.key);
}
});
this.expandKeys = eks;
this.checkedKeys = cks;
} else {
this.$message.error(res.data.msg);
}
}).catch(e => {
this.authLoading = false;
this.$message.error(e.message);
});
},
/* 保存权限分配 */
save() {
this.loading = true;
// 获取选中的id包含所有半选的父级的id
const ids = new Set();
this.$util.eachTreeData(this.authData, (d) => {
if (this.checkedKeys.some(c => c === d.key)) {
ids.add(d.key);
if (d.parentIds) {
d.parentIds.forEach((id) => {
ids.add(id);
});
}
}
});
this.$http.put('/sys/role/menu/' + this.data.roleId, Array.from(ids)).then(res => {
this.loading = false;
if (res.data.code === 0) {
this.$message.success(res.data.msg);
this.updateVisible(false);
} else {
this.$message.error(res.data.msg);
}
}).catch(e => {
this.loading = false;
this.$message.error(e.message);
});
},
/* 更新visible */
updateVisible(value) {
this.$emit('update:visible', value);
}
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,118 @@
<!-- 角色编辑弹窗 -->
<template>
<a-modal
:width="460"
:visible="visible"
:confirm-loading="loading"
:title="isUpdate?'修改角色':'添加角色'"
:body-style="{paddingBottom: '8px'}"
@update:visible="updateVisible"
@ok="save">
<a-form
ref="form"
:model="form"
:rules="rules"
:label-col="{md: {span: 5}, sm: {span: 24}}"
:wrapper-col="{md: {span: 19}, sm: {span: 24}}">
<a-form-item label="角色名称:" name="roleName">
<a-input
allow-clear
:maxlength="20"
placeholder="请输入角色名称"
v-model:value="form.roleName"/>
</a-form-item>
<a-form-item label="角色标识:" name="roleCode">
<a-input
allow-clear
:maxlength="20"
placeholder="请输入角色标识"
v-model:value="form.roleCode"/>
</a-form-item>
<a-form-item label="备注:">
<a-textarea
:rows="4"
:maxlength="200"
placeholder="请输入备注"
v-model:value="form.comments"/>
</a-form-item>
</a-form>
</a-modal>
</template>
<script>
export default {
name: 'RoleEdit',
emits: ['done', 'update:visible'],
props: {
// 弹窗是否打开
visible: Boolean,
// 修改回显的数据
data: Object
},
data() {
return {
// 表单数据
form: Object.assign({}, this.data),
// 表单验证规则
rules: {
roleName: [
{required: true, message: '请输入角色名称', type: 'string', trigger: 'blur'}
],
roleCode: [
{required: true, message: '请输入角色标识', type: 'string', trigger: 'blur'}
]
},
// 提交状态
loading: false,
// 是否是修改
isUpdate: false
};
},
watch: {
data() {
if (this.data) {
this.form = Object.assign({}, this.data);
this.isUpdate = true;
} else {
this.form = {};
this.isUpdate = false;
}
if (this.$refs.form) {
this.$refs.form.clearValidate();
}
}
},
methods: {
/* 保存编辑 */
save() {
this.$refs.form.validate().then(() => {
this.loading = true;
this.$http[this.isUpdate ? 'put' : 'post']('/sys/role', this.form).then(res => {
this.loading = false;
if (res.data.code === 0) {
this.$message.success(res.data.msg);
if (!this.isUpdate) {
this.form = {};
}
this.updateVisible(false);
this.$emit('done');
} else {
this.$message.error(res.data.msg);
}
}).catch(e => {
this.loading = false;
this.$message.error(e.message);
});
}).catch(() => {
});
},
/* 更新visible */
updateVisible(value) {
this.$emit('update:visible', value);
}
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,321 @@
<template>
<div class="ele-body">
<a-card :bordered="false">
<!-- 搜索表单 -->
<a-form
:model="where"
:label-col="{md: {span: 6}, sm: {span: 24}}"
:wrapper-col="{md: {span: 18}, sm: {span: 24}}">
<a-row>
<a-col :lg="6" :md="12" :sm="24" :xs="24">
<a-form-item label="用户账号:">
<a-input
v-model:value.trim="where.username"
placeholder="请输入"
allow-clear/>
</a-form-item>
</a-col>
<a-col :lg="6" :md="12" :sm="24" :xs="24">
<a-form-item label="用户名:">
<a-input
v-model:value.trim="where.nickname"
placeholder="请输入"
allow-clear/>
</a-form-item>
</a-col>
<a-col :lg="6" :md="12" :sm="24" :xs="24">
<a-form-item label="性别:">
<a-select
v-model:value="where.sex"
placeholder="请选择"
allow-clear>
<a-select-option :value="1"></a-select-option>
<a-select-option :value="2"></a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :lg="6" :md="12" :sm="24" :xs="24">
<a-form-item class="ele-text-right" :wrapper-col="{span: 24}">
<a-space>
<a-button type="primary" @click="reload">查询</a-button>
<a-button @click="reset">重置</a-button>
</a-space>
</a-form-item>
</a-col>
</a-row>
</a-form>
<!-- 表格 -->
<ele-pro-table
ref="table"
row-key="userId"
:datasource="url"
:columns="columns"
:where="where"
v-model:selection="selection"
:scroll="{x: 'max-content'}">
<template #toolbar>
<a-space>
<a-button type="primary" @click="openEdit()">
<template #icon>
<plus-outlined/>
</template>
<span>新建</span>
</a-button>
<a-button type="primary" danger @click="removeBatch">
<template #icon>
<delete-outlined/>
</template>
<span>删除</span>
</a-button>
<a-button @click="showImport=true">
<template #icon>
<upload-outlined/>
</template>
<span>导入</span>
</a-button>
</a-space>
</template>
<template #nickname="{ record }">
<router-link :to="'/system/user/info?id=' + record.userId">
{{ record.nickname }}
</router-link>
</template>
<template #roles="{ record }">
<a-tag
v-for="(item, index) in record.roles"
:key="index"
color="blue">
{{ item.roleName }}
</a-tag>
</template>
<template #state="{ text, record }">
<a-switch
:checked="text===0"
@change="(checked) => editState(checked, record)"/>
</template>
<template #action="{ record }">
<a-space>
<a @click="openEdit(record)">修改</a>
<a-divider type="vertical"/>
<a @click="resetPsw(record)">重置密码</a>
<a-divider type="vertical"/>
<a-popconfirm
title="确定要删除此用户吗?"
@confirm="remove(record)">
<a class="ele-text-danger">删除</a>
</a-popconfirm>
</a-space>
</template>
</ele-pro-table>
</a-card>
</div>
<!-- 编辑弹窗 -->
<user-edit
v-model:visible="showEdit"
:data="current"
@done="reload"/>
<!-- 导入弹窗 -->
<user-import
v-model:visible="showImport"
@done="reload"/>
</template>
<script>
import {createVNode} from 'vue';
import {
PlusOutlined,
DeleteOutlined,
UploadOutlined,
ExclamationCircleOutlined
} from '@ant-design/icons-vue';
import UserEdit from './user-edit';
import UserImport from './user-import';
export default {
name: 'SystemUser',
components: {
PlusOutlined,
DeleteOutlined,
UploadOutlined,
UserImport,
UserEdit
},
data() {
return {
// 表格数据接口
url: '/sys/user/page',
// 表格列配置
columns: [
{
key: 'index',
width: 48,
customRender: ({index}) => this.$refs.table.tableIndex + index
},
{
title: '用户账号',
dataIndex: 'username',
sorter: true
},
{
title: '用户名',
dataIndex: 'nickname',
sorter: true,
slots: {customRender: 'nickname'}
},
{
title: '性别',
dataIndex: 'sexName',
sorter: true
},
{
title: '手机号',
dataIndex: 'phone',
sorter: true,
},
{
title: '角色',
key: 'roles',
slots: {customRender: 'roles'}
},
{
title: '创建时间',
dataIndex: 'createTime',
sorter: true,
customRender: ({text}) => this.$util.toDateString(text)
},
{
title: '状态',
dataIndex: 'state',
sorter: true,
width: 90,
align: 'center',
slots: {customRender: 'state'}
},
{
title: '操作',
key: 'action',
width: 200,
align: 'center',
slots: {customRender: 'action'}
}
],
// 表格搜索条件
where: {},
// 表格选中数据
selection: [],
// 当前编辑数据
current: null,
// 是否显示编辑弹窗
showEdit: false,
// 是否显示用户导入弹窗
showImport: false
};
},
methods: {
/* 搜索 */
reload() {
this.selection = [];
this.$refs.table.reload({page: 1, where: this.where});
},
/* 重置搜索 */
reset() {
this.where = {};
this.reload();
},
/* 删除单个 */
remove(row) {
const hide = this.$message.loading('请求中..', 0);
this.$http.delete('/sys/user/' + row.userId).then(res => {
hide();
if (res.data.code === 0) {
this.$message.success(res.data.msg);
this.reload();
} else {
this.$message.error(res.data.msg);
}
}).catch(e => {
hide();
this.$message.error(e.message);
});
},
/* 批量删除 */
removeBatch() {
if (!this.selection.length) {
this.$message.error('请至少选择一条数据');
return;
}
this.$confirm({
title: '提示',
content: '确定要删除选中的用户吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
const hide = this.$message.loading('请求中..', 0);
this.$http.delete('/sys/user/batch', {
data: this.selection.map(d => d.userId)
}).then(res => {
hide();
if (res.data.code === 0) {
this.$message.success(res.data.msg);
this.reload();
} else {
this.$message.error(res.data.msg);
}
}).catch(e => {
hide();
this.$message.error(e.message);
});
}
});
},
/* 打开编辑弹窗 */
openEdit(row) {
this.current = row;
this.showEdit = true;
},
/* 重置用户密码 */
resetPsw(row) {
this.$confirm({
title: '提示',
content: '确定要重置此用户的密码为"123456"吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
const hide = this.$message.loading('请求中..', 0);
const formData = new FormData();
formData.append('password', '123456');
this.$http.put('/sys/user/psw/' + row.userId, formData).then(res => {
hide();
if (res.data.code === 0) {
this.$message.success(res.data.msg);
} else {
this.$message.error(res.data.msg);
}
}).catch(e => {
hide();
this.$message.error(e.message);
});
}
});
},
/* 修改用户状态 */
editState(checked, row) {
let params = new FormData();
params.append('state', checked ? 0 : 1);
this.$http.put('/sys/user/state/' + row.userId, params).then(res => {
if (res.data.code === 0) {
row.state = checked ? 0 : 1;
this.$message.success(res.data.msg);
} else {
this.$message.error(res.data.msg);
}
}).catch(e => {
this.$message.error(e.message);
});
}
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,86 @@
<template>
<div class="ele-body">
<a-card title="基本信息" :bordered="false">
<a-form
class="ele-form-detail"
:label-col="{md: {span: 2}, sm: {span: 4}, xs: {span: 6}}"
:wrapper-col="{md: {span: 22}, sm: {span: 20}, xs: {span: 18}}">
<a-form-item label="账号">
<div class="ele-text-secondary">{{ user.username }}</div>
</a-form-item>
<a-form-item label="用户名">
<div class="ele-text-secondary">{{ user.nickname }}</div>
</a-form-item>
<a-form-item label="性别">
<div class="ele-text-secondary">{{ user.sexName }}</div>
</a-form-item>
<a-form-item label="手机号">
<div class="ele-text-secondary">{{ user.phone }}</div>
</a-form-item>
<a-form-item label="角色">
<a-tag v-for="item in user.roles" :key="item.roleId" color="blue">
{{ item.roleName }}
</a-tag>
</a-form-item>
<a-form-item label="创建时间">
<div class="ele-text-secondary">{{ $util.toDateString(user.createTime) }}</div>
</a-form-item>
<a-form-item label="状态">
<a-badge
:status="['processing', 'error'][user.state]"
:text="['正常', '冻结'][user.state]"/>
</a-form-item>
</a-form>
</a-card>
</div>
</template>
<script>
import {setPageTab} from '@/utils/page-tab-util';
export default {
name: 'SystemUserInfo',
data() {
return {
user: {},
loading: true
};
},
mounted() {
this.query(this.$route.query.id);
},
methods: {
/* 查询用户信息 */
query(id) {
if (!id) {
return;
}
this.loading = true;
this.$http.get('/sys/user/' + id).then((res) => {
this.loading = false;
if (res.data.code === 0) {
this.user = res.data.data;
// 修改页签标题
setPageTab({
fullPath: this.$route.fullPath,
title: this.user.nickname + '的信息'
});
} else {
this.$message.error(res.data.msg);
}
}).catch((e) => {
this.loading = false;
this.$message.error(e.message);
});
}
},
watch: {
$route() {
this.query(this.$route.query.id);
}
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,235 @@
<!-- 用户编辑弹窗 -->
<template>
<a-modal
:width="680"
:visible="visible"
:confirm-loading="loading"
:title="isUpdate?'修改用户':'新建用户'"
:body-style="{paddingBottom: '8px'}"
@update:visible="updateVisible"
@ok="save">
<a-form
ref="form"
:model="form"
:rules="rules"
:label-col="{md: {span: 7}, sm: {span: 24}}"
:wrapper-col="{md: {span: 17}, sm: {span: 24}}">
<a-row :gutter="16">
<a-col :md="12" :sm="24" :xs="24">
<a-form-item label="用户账号:" name="username">
<a-input
allow-clear
:maxlength="20"
placeholder="请输入用户账号"
v-model:value="form.username"/>
</a-form-item>
<a-form-item label="用户名:" name="nickname">
<a-input
allow-clear
:maxlength="20"
placeholder="请输入用户名"
v-model:value="form.nickname"/>
</a-form-item>
<a-form-item label="性别:" name="sex">
<a-select
allow-clear
placeholder="请选择性别"
v-model:value="form.sex">
<a-select-option :value="1"></a-select-option>
<a-select-option :value="2"></a-select-option>
</a-select>
</a-form-item>
<a-form-item label="角色:" name="roleIds">
<a-select
allow-clear
mode="multiple"
placeholder="请选择角色"
v-model:value="form.roleIds">
<a-select-option
v-for="item in roleList"
:key="item.roleId"
:value="item.roleId">
{{ item.roleName }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="邮箱:" name="email">
<a-input
allow-clear
:maxlength="100"
placeholder="请输入邮箱"
v-model:value="form.email"/>
</a-form-item>
</a-col>
<a-col :md="12" :sm="24" :xs="24">
<a-form-item label="手机号:" name="phone">
<a-input
allow-clear
:maxlength="11"
placeholder="请输入手机号"
v-model:value="form.phone"/>
</a-form-item>
<a-form-item label="出生日期:" name="birthday">
<a-date-picker
class="ele-fluid"
value-format="YYYY-MM-DD"
placeholder="请选择出生日期"
v-model:value="form.birthday"/>
</a-form-item>
<a-form-item
v-if="!isUpdate"
label="登录密码:"
name="password">
<a-input-password
:maxlength="20"
v-model:value="form.password"
placeholder="请输入登录密码"/>
</a-form-item>
<a-form-item label="个人简介:">
<a-textarea
:rows="4"
:maxlength="200"
placeholder="请输入个人简介"
v-model:value="form.introduction"/>
</a-form-item>
</a-col>
</a-row>
</a-form>
</a-modal>
</template>
<script>
import validate from 'ele-admin-pro/packages/validate';
export default {
name: 'UserEdit',
emits: ['done', 'update:visible'],
props: {
// 弹窗是否打开
visible: Boolean,
// 修改回显的数据
data: Object
},
data() {
return {
// 表单数据
form: Object.assign({}, this.data),
// 表单验证规则
rules: {
username: [
{
required: true,
type: 'string',
trigger: 'blur',
asyncValidator: (rule, value) => {
return new Promise((resolve, reject) => {
if (!value) {
return reject('请输入用户账号');
}
this.$http.get('/sys/user?username=' + value).then(res => {
if (res.data.code !== 0 || !res.data.data.length) {
return resolve();
}
if (this.isUpdate && res.data.data[0].username === this.data.username) {
return resolve();
}
reject('账号已经存在');
}).catch(() => {
resolve();
});
});
}
}
],
nickname: [
{required: true, message: '请输入用户名', type: 'string', trigger: 'blur'}
],
sex: [
{required: true, message: '请选择性别', type: 'number', trigger: 'blur'}
],
roleIds: [
{required: true, message: '请选择角色', type: 'array', trigger: 'blur'}
],
email: [
{pattern: validate.email, message: '邮箱格式不正确', type: 'string', trigger: 'blur'}
],
password: [
{required: true, pattern: /^[\S]{5,18}$/, message: '密码必须为5-18位非空白字符', type: 'string', trigger: 'blur'}
],
phone: [
{pattern: validate.phone, message: '手机号格式不正确', type: 'string', trigger: 'blur'}
]
},
// 提交状态
loading: false,
// 是否是修改
isUpdate: false,
// 角色列表
roleList: []
};
},
watch: {
data() {
if (this.data) {
this.form = Object.assign({}, this.data, {
roleIds: this.data.roles.map(d => d.roleId)
});
this.isUpdate = true;
} else {
this.form = {};
this.isUpdate = false;
}
if (this.$refs.form) {
this.$refs.form.clearValidate();
}
}
},
mounted() {
this.queryRoles(); // 查询角色列表
},
methods: {
/* 保存编辑 */
save() {
this.$refs.form.validate().then(() => {
this.loading = true;
this.$http[this.isUpdate ? 'put' : 'post']('/sys/user', this.form).then(res => {
this.loading = false;
if (res.data.code === 0) {
this.$message.success(res.data.msg);
if (!this.isUpdate) {
this.form = {};
}
this.updateVisible(false);
this.$emit('done');
} else {
this.$message.error(res.data.msg);
}
}).catch(e => {
this.loading = false;
this.$message.error(e.message);
});
}).catch(() => {
});
},
/* 更新visible */
updateVisible(value) {
this.$emit('update:visible', value);
},
/* 查询角色列表 */
queryRoles() {
this.$http.get('/sys/role').then(res => {
if (res.data.code === 0) {
this.roleList = res.data.data;
} else {
this.$message.error(res.data.msg);
}
}).catch(e => {
this.$message.error(e.message);
});
}
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,77 @@
<!-- 用户导入弹窗 -->
<template>
<a-modal
:width="520"
:footer="null"
title="导入用户"
:visible="visible"
@update:visible="updateVisible">
<a-spin :spinning="loading">
<a-upload-dragger
accept=".xls,.xlsx"
:show-upload-list="false"
:customRequest="doUpload"
style="padding: 24px 0;margin-bottom: 16px;">
<p class="ant-upload-drag-icon">
<cloud-upload-outlined/>
</p>
<p class="ant-upload-hint">将文件拖到此处或点击上传</p>
</a-upload-dragger>
</a-spin>
<div class="ele-text-center">
<span>只能上传xlsxlsx文件</span>
<a href="https://cdn.eleadmin.com/20200610/用户导入模板.xlsx"
download="用户导入模板.xlsx">下载模板
</a>
</div>
</a-modal>
</template>
<script>
import {CloudUploadOutlined} from '@ant-design/icons-vue';
export default {
name: 'UserImport',
components: {CloudUploadOutlined},
emits: ['done', 'update:visible'],
props: {
// 是否打开弹窗
visible: Boolean
},
data() {
return {
// 导入请求状态
loading: false
};
},
methods: {
/* 上传 */
doUpload({file}) {
this.loading = true;
let formData = new FormData();
formData.append('file', file);
this.$http.post('/sys/user/import', formData).then(res => {
this.loading = false;
if (res.data.code === 0) {
this.$message.success(res.data.msg);
this.updateVisible(false);
this.$emit('done');
} else {
this.$message.error(res.data.msg);
}
}).catch(e => {
this.loading = false;
this.$message.error(e.message);
});
return false;
},
/* 更新visible */
updateVisible(value) {
this.$emit('update:visible', value);
}
}
}
</script>
<style scoped>
</style>

215
src/views/user/message.vue Normal file
View File

@@ -0,0 +1,215 @@
<template>
<div class="ele-body">
<a-card :bordered="false" class="user-message-card">
<div class="ele-cell ele-cell-align-top">
<a-menu :selected-keys="active" mode="inline">
<a-menu-item key="all">
<router-link to="/user/message">全部消息</router-link>
</a-menu-item>
<a-menu-item key="notice">
<router-link to="/user/message?type=notice">系统通知</router-link>
</a-menu-item>
<a-menu-item key="message">
<router-link to="/user/message?type=message">用户私信</router-link>
</a-menu-item>
<a-menu-item key="todo">
<router-link to="/user/message?type=todo">代办事项</router-link>
</a-menu-item>
</a-menu>
<div class="ele-cell-content">
<!-- 数据表格 -->
<ele-pro-table
ref="table"
row-key="id"
:loading="loading"
:datasource="data"
:columns="columns"
v-model:selection="selection"
:scroll="{x: 'max-content'}"
@refresh="query">
<template #toolbar>
<a-space>
<a-button type="primary" @click="read">标记已读</a-button>
<a-button type="primary" @click="readAll">全部已读</a-button>
<a-button type="primary" danger @click="removeBatch">删除消息</a-button>
</a-space>
</template>
<template #state="{ text }">
<span :class="['ele-text-warning', 'ele-text-info'][text]">
{{ ['未读', '已读'][text] }}
</span>
</template>
<template #action="{ record }">
<a-space>
<a @click="view(record)">查看</a>
<a-divider type="vertical"/>
<a-popconfirm title="确定要删除此消息吗?" @confirm="remove(record)">
<a class="ele-text-danger">删除</a>
</a-popconfirm>
</a-space>
</template>
</ele-pro-table>
</div>
</div>
</a-card>
</div>
</template>
<script>
import {createVNode} from 'vue';
import {ExclamationCircleOutlined} from '@ant-design/icons-vue';
export default {
name: 'UserMessage',
data() {
return {
// 导航选中
active: ['all'],
// 列表显示数据
data: [],
// 表格列配置
columns: [
{
key: 'index',
width: 38,
customRender: ({index}) => this.$refs.table.tableIndex + index
},
{
title: '标题内容',
dataIndex: 'title'
},
{
title: '时间',
dataIndex: 'time'
},
{
title: '状态',
dataIndex: 'state',
slots: {customRender: 'state'}
},
{
title: '操作',
key: 'action',
width: 120,
align: 'center',
slots: {customRender: 'action'}
}
],
// 列表选中数据
selection: [],
// 全部数据
allData: [],
// 加载状态
loading: true
};
},
mounted() {
const type = this.$route.query.type;
if (type) {
this.active = [type];
}
this.query();
},
methods: {
/* 查询全部数据 */
query() {
this.loading = true;
this.$http.get('https://cdn.eleadmin.com/20200609/message.json').then((res) => {
this.loading = false;
if (res.data.code === 0) {
this.allData = res.data.data;
this.changeType();
} else {
this.$message.error(res.data.msg);
}
}).catch((e) => {
this.loading = false;
this.$message.error(e.message);
});
},
/* 切换消息类型 */
changeType() {
if (this.active.indexOf('all') !== -1) {
this.data = [].concat(this.allData);
} else {
this.data = this.allData.filter((d) => this.active.indexOf(d.type) !== -1);
}
this.selection = [];
},
/* 查看 */
view(row) {
this.$message.info(row.title);
},
/* 删除单个 */
remove(row) {
this.allData.splice(this.allData.findIndex((t) => t.id === row.id), 1);
this.$message.success('删除成功');
this.changeType();
},
/* 批量删除 */
removeBatch() {
if (!this.selection.length) {
return this.$message.error('请至少选择一条数据');
}
this.$confirm({
title: '提示',
content: '确定要删除选中的消息吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
this.selection.forEach((d) => {
this.allData.splice(this.allData.findIndex((t) => t.id === d.id), 1);
});
this.changeType();
}
});
},
/* 标记已读 */
read() {
if (!this.selection.length) {
return this.$message.error('请至少选择一条数据');
}
this.selection.forEach((d) => {
const index = this.allData.findIndex((t) => t.id === d.id);
this.allData[index].state = 1;
});
this.changeType();
},
/* 全部标记已读 */
readAll() {
this.allData.forEach((d) => {
d.state = 1;
});
this.changeType();
}
},
watch: {
$route() {
const type = this.$route.query.type;
this.active = [type || 'all'];
this.changeType();
}
}
}
</script>
<style scoped>
.user-message-card :deep(.ant-card-body) {
padding: 0;
}
.user-message-card .ant-menu {
width: 150px;
padding-top: 16px;
}
.user-message-card .ant-menu + .ele-cell-content {
padding: 24px;
}
@media screen and (max-width: 768px) {
.user-message-card .ant-menu {
display: none;
}
}
</style>

365
src/views/user/profile.vue Normal file
View File

@@ -0,0 +1,365 @@
<template>
<div class="ele-body ele-body-card">
<a-row :gutter="16">
<a-col :lg="6" :md="8" :sm="24" :xs="24">
<a-card :bordered="false">
<div class="ele-text-center">
<div class="user-info-avatar-group" @click="showCropper=true">
<a-avatar :size="110" :src="form.avatar"/>
<upload-outlined class="user-info-avatar-icon"/>
</div>
<h1>Jasmine</h1>
<div>海纳百川有容乃大</div>
</div>
<div class="user-info-list">
<div class="ele-cell">
<user-outlined/>
<div class="ele-cell-content">资深前端工程师</div>
</div>
<div class="ele-cell">
<home-outlined/>
<div class="ele-cell-content">某某公司 - 某某事业群 - 某某技术部</div>
</div>
<div class="ele-cell">
<environment-outlined/>
<div class="ele-cell-content">中国 浙江省 杭州市</div>
</div>
<div class="ele-cell">
<tag-outlined/>
<div class="ele-cell-content">JavaScriptHTMLCSSVueNode</div>
</div>
</div>
<a-divider dashed/>
<h6>标签</h6>
<div class="user-info-tags">
<a-tag>很有想法的</a-tag>
<a-tag>专注设计</a-tag>
<a-tag>~</a-tag>
<a-tag>大长腿</a-tag>
<a-tag>川妹子</a-tag>
<a-tag>海纳百川</a-tag>
</div>
</a-card>
</a-col>
<a-col :lg="18" :md="16" :sm="24" :xs="24">
<a-card :bordered="false" class="user-info-tabs">
<a-tabs v-model:active-key="active" size="large">
<a-tab-pane tab="基本信息" key="info">
<a-form
ref="form"
:model="form"
:rules="rules"
:label-col="{md: {span: 6}, sm: {span: 24}}"
:wrapper-col="{md: {span: 18}, sm: {span: 24}}">
<a-form-item label="昵称:" name="nickname">
<a-input
v-model:value="form.nickname"
placeholder="请输入昵称"
allow-clear/>
</a-form-item>
<a-form-item label="性别:" name="sex">
<a-select
v-model:value="form.sex"
placeholder="请选择性别"
allow-clear>
<a-select-option value="保密">保密</a-select-option>
<a-select-option value="男"></a-select-option>
<a-select-option value="女"></a-select-option>
</a-select>
</a-form-item>
<a-form-item label="邮箱:" name="email">
<a-input
v-model:value="form.email"
placeholder="请输入邮箱"
allow-clear/>
</a-form-item>
<a-form-item label="个人简介:">
<a-textarea
v-model:value="form.introduction"
placeholder="请输入个人简介"
:rows="4"/>
</a-form-item>
<a-form-item label="街道地址:">
<a-input
v-model:value="form.address"
placeholder="请输入街道地址"
allow-clear/>
</a-form-item>
<a-form-item label="联系电话:">
<div class="ele-cell">
<a-input
v-model:value="form.tellPre"
style="width: 65px;"/>
<div class="ele-cell-content">
<a-input
v-model:value="form.tell"
placeholder="请输入联系电话"
allow-clear/>
</div>
</div>
</a-form-item>
<a-form-item :wrapper-col="{md: {offset: 6}}">
<a-button
type="primary"
@click="save"
:loading="loading">
{{ loading ? '保存中..' : '保存更改' }}
</a-button>
</a-form-item>
</a-form>
</a-tab-pane>
<a-tab-pane tab="账号绑定" key="account">
<div class="user-account-list">
<div class="ele-cell">
<div class="ele-cell-content">
<div class="ele-cell-title">密保手机</div>
<div class="ele-cell-desc">已绑定手机138****8293</div>
</div>
<a>去修改</a>
</div>
<a-divider/>
<div class="ele-cell">
<div class="ele-cell-content">
<div class="ele-cell-title">密保邮箱</div>
<div class="ele-cell-desc">已绑定邮箱eleadmin@eclouds.com</div>
</div>
<a>去修改</a>
</div>
<a-divider/>
<div class="ele-cell">
<div class="ele-cell-content">
<div class="ele-cell-title">密保问题</div>
<div class="ele-cell-desc">未设置密保问题</div>
</div>
<a>去设置</a>
</div>
<a-divider/>
<div class="ele-cell">
<qq-outlined class="user-account-icon"/>
<div class="ele-cell-content">
<div class="ele-cell-title">绑定QQ</div>
<div class="ele-cell-desc">当前未绑定QQ账号</div>
</div>
<a>去绑定</a>
</div>
<a-divider/>
<div class="ele-cell">
<wechat-outlined class="user-account-icon"/>
<div class="ele-cell-content">
<div class="ele-cell-title">绑定微信</div>
<div class="ele-cell-desc">当前未绑定绑定微信账号</div>
</div>
<a>去绑定</a>
</div>
<a-divider/>
<div class="ele-cell">
<alipay-outlined class="user-account-icon"/>
<div class="ele-cell-content">
<div class="ele-cell-title">绑定支付宝</div>
<div class="ele-cell-desc">当前未绑定绑定支付宝账号</div>
</div>
<a>去绑定</a>
</div>
</div>
</a-tab-pane>
</a-tabs>
</a-card>
</a-col>
</a-row>
<!-- 头像裁剪弹窗 -->
<ele-cropper-modal
v-model:visible="showCropper"
:src="form.avatar"
@done="onCrop"/>
</div>
</template>
<script>
import {
UploadOutlined,
UserOutlined,
HomeOutlined,
EnvironmentOutlined,
TagOutlined,
QqOutlined,
WechatOutlined,
AlipayOutlined
} from '@ant-design/icons-vue';
import EleCropperModal from 'ele-admin-pro/packages/ele-cropper-modal';
export default {
name: 'UserInfo',
components: {
UploadOutlined,
UserOutlined,
HomeOutlined,
EnvironmentOutlined,
TagOutlined,
QqOutlined,
WechatOutlined,
AlipayOutlined,
EleCropperModal
},
data() {
return {
// tab页选中
active: 'info',
// 表单数据
form: {
nickname: 'Jasmine',
sex: '保密',
email: 'eleadmin@eclouds.com',
introduction: '',
address: '',
tellPre: '0752',
tell: '',
avatar: 'https://cdn.eleadmin.com/20200610/avatar.jpg'
},
// 表单验证规则
rules: {
nickname: [
{required: true, message: '请输入昵称', type: 'string', trigger: 'blur'}
],
sex: [
{required: true, message: '请选择性别', type: 'string', trigger: 'blur'}
],
email: [
{required: true, message: '请输入邮箱', type: 'string', trigger: 'blur'}
]
},
// 保存按钮loading
loading: false,
// 是否显示裁剪弹窗
showCropper: false
};
},
methods: {
/* 保存更改 */
save() {
this.$refs.form.validate().then(() => {
this.loading = true;
setTimeout(() => {
this.loading = false;
this.$message.success('保存成功');
}, 800);
}).catch(() => {
});
},
/* 头像裁剪完成回调 */
onCrop(res) {
this.form.avatar = res;
this.showCropper = false;
this.$store.dispatch('user/setUser', Object.assign({}, this.$store.state.user.user, {
avatar: res
}));
}
}
}
</script>
<style scoped>
/* 用户资料卡片 */
.user-info-avatar-group {
margin: 16px 0;
display: inline-block;
position: relative;
cursor: pointer;
}
.user-info-avatar-group .user-info-avatar-icon {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #fff;
font-size: 30px;
display: none;
z-index: 2;
}
.user-info-avatar-group:hover .user-info-avatar-icon {
display: block;
}
.user-info-avatar-group:hover:after {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: 50%;
background-color: rgba(0, 0, 0, .3);
}
.user-info-avatar-group + h1 {
margin-bottom: 8px;
}
/* 用户信息列表 */
.user-info-list {
margin: 52px 0 32px 0;
}
.user-info-list .ele-cell + .ele-cell {
margin-top: 16px;
}
.user-info-list + .ant-divider {
margin-bottom: 16px;
}
/* 用户标签 */
.user-info-tags {
margin: 16px 0 4px 0;
}
.user-info-tags .ant-tag {
margin: 0 12px 8px 0;
}
/* 右侧卡片 */
.user-info-tabs :deep(.ant-card-body) {
padding: 0;
}
.user-info-tabs :deep(.ant-tabs-tab) {
padding-left: 4px;
padding-right: 4px;
margin: 0 12px 0 28px !important;
}
.user-info-tabs .ant-form {
max-width: 580px;
margin-top: 20px;
padding: 0 24px;
}
/* 用户账号绑定列表 */
.user-account-list {
margin-bottom: 27px;
}
.user-account-list > .ele-cell {
padding: 18px 34px;
}
.user-account-list .user-account-icon {
color: #fff;
padding: 8px;
font-size: 26px;
border-radius: 50%;
}
.user-account-list .user-account-icon.anticon-qq {
background: #3492ED;
}
.user-account-list .user-account-icon.anticon-wechat {
background: #4DAF29;
}
.user-account-list .user-account-icon.anticon-alipay {
background: #1476FE;
}
</style>