This commit is contained in:
pycook
2019-08-28 20:51:51 +08:00
committed by pycook
parent f3046d3c91
commit 5b4f95a50e
194 changed files with 40240 additions and 16 deletions

15
ui/src/views/404.vue Normal file
View File

@@ -0,0 +1,15 @@
<template>
<div>
404 page
</div>
</template>
<script>
export default {
name: '404'
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,288 @@
<template>
<div class="page-header-index-wide page-header-wrapper-grid-content-main">
<a-row :gutter="24">
<a-col :md="24" :lg="7">
<a-card :bordered="false">
<div class="account-center-avatarHolder">
<div class="avatar">
<img :src="avatar()">
</div>
<div class="username">{{ nickname() }}</div>
<div class="bio">海纳百川有容乃大</div>
</div>
<div class="account-center-detail">
<p>
<i class="title"></i>交互专家
</p>
<p>
<i class="group"></i>蚂蚁金服某某某事业群某某平台部某某技术部UED
</p>
<p>
<i class="address"></i>
<span>浙江省</span>
<span>杭州市</span>
</p>
</div>
<a-divider/>
<div class="account-center-tags">
<div class="tagsTitle">标签</div>
<div>
<template v-for="(tag, index) in tags">
<a-tooltip v-if="tag.length > 20" :key="tag" :title="tag">
<a-tag
:key="tag"
:closable="index !== 0"
:afterClose="() => handleTagClose(tag)"
>{{ `${tag.slice(0, 20)}...` }}</a-tag>
</a-tooltip>
<a-tag
v-else
:key="tag"
:closable="index !== 0"
:afterClose="() => handleTagClose(tag)"
>{{ tag }}</a-tag>
</template>
<a-input
v-if="tagInputVisible"
ref="tagInput"
type="text"
size="small"
:style="{ width: '78px' }"
:value="tagInputValue"
@change="handleInputChange"
@blur="handleTagInputConfirm"
@keyup.enter="handleTagInputConfirm"
/>
<a-tag v-else @click="showTagInput" style="background: #fff; borderStyle: dashed;">
<a-icon type="plus"/>New Tag
</a-tag>
</div>
</div>
<a-divider :dashed="true"/>
<div class="account-center-team">
<div class="teamTitle">团队</div>
<a-spin :spinning="teamSpinning">
<div class="members">
<a-row>
<a-col :span="12" v-for="(item, index) in teams" :key="index">
<a>
<a-avatar size="small" :src="item.avatar"/>
<span class="member">{{ item.name }}</span>
</a>
</a-col>
</a-row>
</div>
</a-spin>
</div>
</a-card>
</a-col>
<a-col :md="24" :lg="17">
<a-card
style="width:100%"
:bordered="false"
:tabList="tabListNoTitle"
:activeTabKey="noTitleKey"
@tabChange="key => handleTabChange(key, 'noTitleKey')"
>
<article-page v-if="noTitleKey === 'article'"></article-page>
<app-page v-else-if="noTitleKey === 'app'"></app-page>
<project-page v-else-if="noTitleKey === 'project'"></project-page>
</a-card>
</a-col>
</a-row>
</div>
</template>
<script>
import { PageView, RouteView } from '@/layouts'
import { AppPage, ArticlePage, ProjectPage } from './page'
import { mapGetters } from 'vuex'
export default {
components: {
RouteView,
PageView,
AppPage,
ArticlePage,
ProjectPage
},
data () {
return {
tags: ['很有想法的', '专注设计', '辣~', '大长腿', '川妹子', '海纳百川'],
tagInputVisible: false,
tagInputValue: '',
teams: [],
teamSpinning: true,
tabListNoTitle: [
{
key: 'article',
tab: '文章(8)'
},
{
key: 'app',
tab: '应用(8)'
},
{
key: 'project',
tab: '项目(8)'
}
],
noTitleKey: 'app'
}
},
mounted () {
this.getTeams()
},
methods: {
...mapGetters(['nickname', 'avatar']),
getTeams () {
this.$http.get('/workplace/teams').then(res => {
this.teams = res.result
this.teamSpinning = false
})
},
handleTabChange (key, type) {
this[type] = key
},
handleTagClose (removeTag) {
const tags = this.tags.filter(tag => tag !== removeTag)
this.tags = tags
},
showTagInput () {
this.tagInputVisible = true
this.$nextTick(() => {
this.$refs.tagInput.focus()
})
},
handleInputChange (e) {
this.tagInputValue = e.target.value
},
handleTagInputConfirm () {
const inputValue = this.tagInputValue
let tags = this.tags
if (inputValue && !tags.includes(inputValue)) {
tags = [...tags, inputValue]
}
Object.assign(this, {
tags,
tagInputVisible: false,
tagInputValue: ''
})
}
}
}
</script>
<style lang="less" scoped>
.page-header-wrapper-grid-content-main {
width: 100%;
height: 100%;
min-height: 100%;
transition: 0.3s;
.account-center-avatarHolder {
text-align: center;
margin-bottom: 24px;
& > .avatar {
margin: 0 auto;
width: 104px;
height: 104px;
margin-bottom: 20px;
border-radius: 50%;
overflow: hidden;
img {
height: 100%;
width: 100%;
}
}
.username {
color: rgba(0, 0, 0, 0.85);
font-size: 20px;
line-height: 28px;
font-weight: 500;
margin-bottom: 4px;
}
}
.account-center-detail {
p {
margin-bottom: 8px;
padding-left: 26px;
position: relative;
}
i {
position: absolute;
height: 14px;
width: 14px;
left: 0;
top: 4px;
background: url(https://gw.alipayobjects.com/zos/rmsportal/pBjWzVAHnOOtAUvZmZfy.svg);
}
.title {
background-position: 0 0;
}
.group {
background-position: 0 -22px;
}
.address {
background-position: 0 -44px;
}
}
.account-center-tags {
.ant-tag {
margin-bottom: 8px;
}
}
.account-center-team {
.members {
a {
display: block;
margin: 12px 0;
line-height: 24px;
height: 24px;
.member {
font-size: 14px;
color: rgba(0, 0, 0, 0.65);
line-height: 24px;
max-width: 100px;
vertical-align: top;
margin-left: 12px;
transition: all 0.3s;
display: inline-block;
}
&:hover {
span {
color: #1890ff;
}
}
}
}
}
.tagsTitle,
.teamTitle {
font-weight: 500;
color: rgba(0, 0, 0, 0.85);
margin-bottom: 12px;
}
}
</style>

View File

@@ -0,0 +1,113 @@
<template>
<div class="app-list">
<a-list
:grid="{ gutter: 24, lg: 3, md: 2, sm: 1, xs: 1 }"
:dataSource="dataSource">
<a-list-item slot="renderItem" slot-scope="item">
<a-card :hoverable="true">
<a-card-meta>
<div style="margin-bottom: 3px" slot="title">{{ item.title }}</div>
<a-avatar class="card-avatar" slot="avatar" :src="item.avatar" size="small"/>
<div class="meta-cardInfo" slot="description">
<div>
<p>活跃用户</p>
<p>
<span>{{ item.activeUser }}<span></span></span>
</p>
</div>
<div>
<p>新增用户</p>
<p>{{ item.newUser | NumberFormat }}</p>
</div>
</div>
</a-card-meta>
<template class="ant-card-actions" slot="actions">
<a>
<a-icon type="download"/>
</a>
<a>
<a-icon type="edit"/>
</a>
<a>
<a-icon type="share-alt"/>
</a>
<a>
<a-dropdown>
<a class="ant-dropdown-link" href="javascript:;">
<a-icon type="ellipsis"/>
</a>
<a-menu slot="overlay">
<a-menu-item>
<a href="javascript:;">1st menu item</a>
</a-menu-item>
<a-menu-item>
<a href="javascript:;">2nd menu item</a>
</a-menu-item>
<a-menu-item>
<a href="javascript:;">3rd menu item</a>
</a-menu-item>
</a-menu>
</a-dropdown>
</a>
</template>
</a-card>
</a-list-item>
</a-list>
</div>
</template>
<script>
const dataSource = []
for (let i = 0; i < 11; i++) {
dataSource.push({
title: 'Alipay',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/WdGqmHpayyMjiEhcKoVE.png',
activeUser: 17,
newUser: 1700
})
}
export default {
name: 'Article',
components: {},
data () {
return {
dataSource
}
}
}
</script>
<style lang="less" scoped>
.app-list {
.meta-cardInfo {
zoom: 1;
margin-top: 16px;
> div {
position: relative;
text-align: left;
float: left;
width: 50%;
p {
line-height: 32px;
font-size: 24px;
margin: 0;
&:first-child {
color: rgba(0, 0, 0, .45);
font-size: 12px;
line-height: 20px;
margin-bottom: 4px;
}
}
}
}
}
</style>

View File

@@ -0,0 +1,75 @@
<template>
<a-list
size="large"
rowKey="id"
:loading="loading"
itemLayout="vertical"
:dataSource="data"
>
<a-list-item :key="item.id" slot="renderItem" slot-scope="item">
<template slot="actions">
<icon-text type="star-o" :text="item.star" />
<icon-text type="like-o" :text="item.like" />
<icon-text type="message" :text="item.message" />
</template>
<a-list-item-meta>
<a slot="title" href="https://vue.ant.design/">{{ item.title }}</a>
<template slot="description">
<span>
<a-tag>Ant Design</a-tag>
<a-tag>设计语言</a-tag>
<a-tag>蚂蚁金服</a-tag>
</span>
</template>
</a-list-item-meta>
<article-list-content :description="item.description" :owner="item.owner" :avatar="item.avatar" :href="item.href" :updateAt="item.updatedAt" />
</a-list-item>
<div slot="footer" v-if="data.length > 0" style="text-align: center; margin-top: 16px;">
<a-button @click="loadMore" :loading="loadingMore">加载更多</a-button>
</div>
</a-list>
</template>
<script>
import { ArticleListContent } from '@/components'
import IconText from '@/views/list/search/components/IconText'
export default {
name: 'Article',
components: {
IconText,
ArticleListContent
},
data () {
return {
loading: true,
loadingMore: false,
data: []
}
},
mounted () {
this.getList()
},
methods: {
getList () {
this.$http.get('/list/article').then(res => {
console.log('res', res)
this.data = res.result
this.loading = false
})
},
loadMore () {
this.loadingMore = true
this.$http.get('/list/article').then(res => {
this.data = this.data.concat(res.result)
}).finally(() => {
this.loadingMore = false
})
}
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,109 @@
<template>
<div class="ant-pro-pages-account-projects-cardList">
<a-list :loading="loading" :data-source="data" :grid="{ gutter: 24, xxl: 3, xl: 2, lg: 2, md: 2, sm: 2, xs: 1 }">
<a-list-item slot="renderItem" slot-scope="item">
<a-card class="ant-pro-pages-account-projects-card" hoverable>
<img slot="cover" :src="item.cover" :alt="item.title" />
<a-card-meta :title="item.title">
<template slot="description">
<ellipsis :length="50">{{ item.description }}</ellipsis>
</template>
</a-card-meta>
<div class="cardItemContent">
<span>{{ item.updatedAt | fromNow }}</span>
<div class="avatarList">
<avatar-list size="mini">
<avatar-list-item
v-for="(member, i) in item.members"
:key="`${item.id}-avatar-${i}`"
:src="member.avatar"
:tips="member.name"
/>
</avatar-list>
</div>
</div>
</a-card>
</a-list-item>
</a-list>
</div>
</template>
<script>
import moment from 'moment'
import { TagSelect, StandardFormRow, Ellipsis, AvatarList } from '@/components'
const TagSelectOption = TagSelect.Option
const AvatarListItem = AvatarList.AvatarItem
export default {
name: 'Project',
components: {
AvatarList,
AvatarListItem,
Ellipsis,
TagSelect,
TagSelectOption,
StandardFormRow
},
data () {
return {
data: [],
form: this.$form.createForm(this),
loading: true
}
},
filters: {
fromNow (date) {
return moment(date).fromNow()
}
},
mounted () {
this.getList()
},
methods: {
handleChange (value) {
console.log(`selected ${value}`)
},
getList () {
this.$http.get('/list/article', { params: { count: 8 } }).then(res => {
console.log('res', res)
this.data = res.result
this.loading = false
})
}
}
}
</script>
<style lang="less" scoped>
.ant-pro-pages-account-projects-cardList {
margin-top: 24px;
/deep/ .ant-card-meta-title {
margin-bottom: 4px;
}
/deep/ .ant-card-meta-description {
height: 44px;
overflow: hidden;
line-height: 22px;
}
.cardItemContent {
display: flex;
height: 20px;
margin-top: 16px;
margin-bottom: -4px;
line-height: 20px;
> span {
flex: 1 1;
color: rgba(0,0,0,.45);
font-size: 12px;
}
/deep/ .ant-pro-avatar-list {
flex: 0 1 auto;
}
}
}
</style>

View File

@@ -0,0 +1,5 @@
import AppPage from './App'
import ArticlePage from './Article'
import ProjectPage from './Project'
export { AppPage, ArticlePage, ProjectPage }

View File

@@ -0,0 +1,109 @@
<template>
<a-modal
title="修改头像"
:visible="visible"
:maskClosable="false"
:confirmLoading="confirmLoading"
:width="800"
@cancel="cancelHandel">
<a-row>
<a-col :xs="24" :md="12" :style="{height: '350px'}">
<vue-cropper
ref="cropper"
:img="options.img"
:info="true"
:autoCrop="options.autoCrop"
:autoCropWidth="options.autoCropWidth"
:autoCropHeight="options.autoCropHeight"
:fixedBox="options.fixedBox"
@realTime="realTime"
>
</vue-cropper>
</a-col>
<a-col :xs="24" :md="12" :style="{height: '350px'}">
<div class="avatar-upload-preview">
<img :src="previews.url" :style="previews.img"/>
</div>
</a-col>
</a-row>
<template slot="footer">
<a-button key="back" @click="cancelHandel">取消</a-button>
<a-button key="submit" type="primary" :loading="confirmLoading" @click="okHandel">保存</a-button>
</template>
</a-modal>
</template>
<script>
// import { VueCropper } from 'vue-cropper'
export default {
/*
components: {
VueCropper
},
*/
data () {
return {
visible: false,
id: null,
confirmLoading: false,
options: {
img: '/avatar2.jpg',
autoCrop: true,
autoCropWidth: 200,
autoCropHeight: 200,
fixedBox: true
},
previews: {}
}
},
methods: {
edit (id) {
this.visible = true
this.id = id
/* 获取原始头像 */
},
close () {
this.id = null
this.visible = false
},
cancelHandel () {
this.close()
},
okHandel () {
const vm = this
vm.confirmLoading = true
setTimeout(() => {
vm.confirmLoading = false
vm.close()
vm.$message.success('上传头像成功')
}, 2000)
},
realTime (data) {
this.previews = data
}
}
}
</script>
<style lang="less" scoped>
.avatar-upload-preview {
position: absolute;
top: 50%;
transform: translate(50%, -50%);
width: 180px;
height: 180px;
border-radius: 50%;
box-shadow: 0 0 4px #ccc;
overflow: hidden;
img {
width: 100%;
height: 100%;
}
}
</style>

View File

@@ -0,0 +1,161 @@
<template>
<div class="account-settings-info-view">
<a-row :gutter="16">
<a-col :md="24" :lg="16">
<a-form layout="vertical">
<a-form-item
label="昵称"
>
<a-input placeholder="给自己起个名字" />
</a-form-item>
<a-form-item
label="Bio"
>
<a-textarea rows="4" placeholder="You are not alone."/>
</a-form-item>
<a-form-item
label="电子邮件"
:required="false"
>
<a-input placeholder="exp@admin.com"/>
</a-form-item>
<a-form-item
label="加密方式"
:required="false"
>
<a-select defaultValue="aes-256-cfb">
<a-select-option value="aes-256-cfb">aes-256-cfb</a-select-option>
<a-select-option value="aes-128-cfb">aes-128-cfb</a-select-option>
<a-select-option value="chacha20">chacha20</a-select-option>
</a-select>
</a-form-item>
<a-form-item
label="连接密码"
:required="false"
>
<a-input placeholder="h3gSbecd"/>
</a-form-item>
<a-form-item
label="登录密码"
:required="false"
>
<a-input placeholder="密码"/>
</a-form-item>
<a-form-item>
<a-button type="primary">提交</a-button>
<a-button style="margin-left: 8px">保存</a-button>
</a-form-item>
</a-form>
</a-col>
<a-col :md="24" :lg="8" :style="{ minHeight: '180px' }">
<div class="ant-upload-preview" @click="$refs.modal.edit(1)" >
<a-icon type="cloud-upload-o" class="upload-icon"/>
<div class="mask">
<a-icon type="plus" />
</div>
<img :src="option.img"/>
</div>
</a-col>
</a-row>
<avatar-modal ref="modal">
</avatar-modal>
</div>
</template>
<script>
import AvatarModal from './AvatarModal'
export default {
components: {
AvatarModal
},
data () {
return {
// cropper
preview: {},
option: {
img: '/avatar2.jpg',
info: true,
size: 1,
outputType: 'jpeg',
canScale: false,
autoCrop: true,
// 只有自动截图开启 宽度高度才生效
autoCropWidth: 180,
autoCropHeight: 180,
fixedBox: true,
// 开启宽度和高度比例
fixed: true,
fixedNumber: [1, 1]
}
}
},
methods: {
}
}
</script>
<style lang="less" scoped>
.avatar-upload-wrapper {
height: 200px;
width: 100%;
}
.ant-upload-preview {
position: relative;
margin: 0 auto;
width: 100%;
max-width: 180px;
border-radius: 50%;
box-shadow: 0 0 4px #ccc;
.upload-icon {
position: absolute;
top: 0;
right: 10px;
font-size: 1.4rem;
padding: 0.5rem;
background: rgba(222, 221, 221, 0.7);
border-radius: 50%;
border: 1px solid rgba(0, 0, 0, 0.2);
}
.mask {
opacity: 0;
position: absolute;
background: rgba(0,0,0,0.4);
cursor: pointer;
transition: opacity 0.4s;
&:hover {
opacity: 1;
}
i {
font-size: 2rem;
position: absolute;
top: 50%;
left: 50%;
margin-left: -1rem;
margin-top: -1rem;
color: #d6d6d6;
}
}
img, .mask {
width: 100%;
max-width: 180px;
height: 100%;
border-radius: 50%;
overflow: hidden;
}
}
</style>

View File

@@ -0,0 +1,25 @@
<template>
<a-list
itemLayout="horizontal"
:dataSource="data"
>
</a-list>
</template>
<script>
export default {
data () {
return {
data: []
}
},
methods: {
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,75 @@
<script>
import { colorList } from '@/components/SettingDrawer/settingConfig'
import ASwitch from 'ant-design-vue/es/switch'
import AList from 'ant-design-vue/es/list'
import AListItem from 'ant-design-vue/es/list/Item'
import { mixin } from '@/utils/mixin'
const Meta = AListItem.Meta
export default {
components: {
AListItem,
AList,
ASwitch,
Meta
},
mixins: [mixin],
data () {
return {
}
},
filters: {
themeFilter (theme) {
const themeMap = {
'dark': '暗色',
'light': '白色'
}
return themeMap[theme]
}
},
methods: {
colorFilter (color) {
const c = colorList.filter(o => o.color === color)[0]
return c && c.key
},
onChange (checked) {
if (checked) {
this.$store.dispatch('ToggleTheme', 'dark')
} else {
this.$store.dispatch('ToggleTheme', 'light')
}
}
},
render () {
return (
<AList itemLayout="horizontal">
<AListItem>
<Meta>
<a slot="title">风格配色</a>
<span slot="description">
整体风格配色设置
</span>
</Meta>
<div slot="actions">
<ASwitch checkedChildren="暗色" unCheckedChildren="白色" defaultChecked={this.navTheme === 'dark' && true || false} onChange={this.onChange} />
</div>
</AListItem>
<AListItem>
<Meta>
<a slot="title">主题色</a>
<span slot="description">
页面风格配色 <a domPropsInnerHTML={ this.colorFilter(this.primaryColor) }/>
</span>
</Meta>
</AListItem>
</AList>
)
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,155 @@
<template>
<div class="page-header-index-wide">
<a-card :bordered="false" :bodyStyle="{ padding: '16px 0', height: '100%' }" :style="{ height: '100%' }">
<div class="account-settings-info-main" :class="device">
<div class="account-settings-info-left">
<a-menu
:mode="device == 'mobile' ? 'horizontal' : 'inline'"
:style="{ border: '0', width: device == 'mobile' ? '560px' : 'auto'}"
:selectedKeys="selectedKeys"
type="inner"
@openChange="onOpenChange"
>
<a-menu-item key="/account/settings/base">
<router-link :to="{ name: 'BaseSettings' }">
基本设置
</router-link>
</a-menu-item>
<a-menu-item key="/account/settings/security">
<router-link :to="{ name: 'SecuritySettings' }">
安全设置
</router-link>
</a-menu-item>
<a-menu-item key="/account/settings/custom">
<router-link :to="{ name: 'CustomSettings' }">
个性化
</router-link>
</a-menu-item>
<a-menu-item key="/account/settings/binding">
<router-link :to="{ name: 'BindingSettings' }">
账户绑定
</router-link>
</a-menu-item>
<a-menu-item key="/account/settings/notification">
<router-link :to="{ name: 'NotificationSettings' }">
新消息通知
</router-link>
</a-menu-item>
</a-menu>
</div>
<div class="account-settings-info-right">
<div class="account-settings-info-title">
<span>{{ $route.meta.title }}</span>
</div>
<route-view></route-view>
</div>
</div>
</a-card>
</div>
</template>
<script>
import { PageView, RouteView } from '@/layouts'
import { mixinDevice } from '@/utils/mixin.js'
export default {
components: {
RouteView,
PageView
},
mixins: [mixinDevice],
data () {
return {
// horizontal inline
mode: 'inline',
openKeys: [],
selectedKeys: [],
// cropper
preview: {},
option: {
img: '/avatar2.jpg',
info: true,
size: 1,
outputType: 'jpeg',
canScale: false,
autoCrop: true,
// 只有自动截图开启 宽度高度才生效
autoCropWidth: 180,
autoCropHeight: 180,
fixedBox: true,
// 开启宽度和高度比例
fixed: true,
fixedNumber: [1, 1]
},
pageTitle: ''
}
},
created () {
this.updateMenu()
},
methods: {
onOpenChange (openKeys) {
this.openKeys = openKeys
},
updateMenu () {
const routes = this.$route.matched.concat()
this.selectedKeys = [ routes.pop().path ]
}
},
watch: {
'$route' (val) {
this.updateMenu()
}
}
}
</script>
<style lang="less" scoped>
.account-settings-info-main {
width: 100%;
display: flex;
height: 100%;
overflow: auto;
&.mobile {
display: block;
.account-settings-info-left {
border-right: unset;
border-bottom: 1px solid #e8e8e8;
width: 100%;
height: 50px;
overflow-x: auto;
overflow-y: scroll;
}
.account-settings-info-right {
padding: 20px 40px;
}
}
.account-settings-info-left {
border-right: 1px solid #e8e8e8;
width: 224px;
}
.account-settings-info-right {
flex: 1 1;
padding: 8px 40px;
.account-settings-info-title {
color: rgba(0,0,0,.85);
font-size: 20px;
font-weight: 500;
line-height: 28px;
margin-bottom: 12px;
}
.account-settings-info-view {
padding-top: 12px;
}
}
}
</style>

View File

@@ -0,0 +1,25 @@
<template>
<a-list
itemLayout="horizontal"
:dataSource="data"
>
</a-list>
</template>
<script>
export default {
data () {
return {
data: []
}
},
methods: {
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,41 @@
<template>
<a-list
itemLayout="horizontal"
:dataSource="data"
>
<a-list-item slot="renderItem" slot-scope="item, index" :key="index">
<a-list-item-meta>
<a slot="title">{{ item.title }}</a>
<span slot="description">
<span class="security-list-description">{{ item.description }}</span>
<span v-if="item.value"> : </span>
<span class="security-list-value">{{ item.value }}</span>
</span>
</a-list-item-meta>
<template v-if="item.actions">
<a slot="actions" @click="item.actions.callback">{{ item.actions.title }}</a>
</template>
</a-list-item>
</a-list>
</template>
<script>
export default {
data () {
return {
data: [
{ title: '账户密码', description: '当前密码强度', value: '强', actions: { title: '修改', callback: () => { this.$message.info('This is a normal message') } } },
{ title: '密保手机', description: '已绑定手机', value: '138****8293', actions: { title: '修改', callback: () => { this.$message.success('This is a message of success') } } },
{ title: '密保问题', description: '未设置密保问题,密保问题可有效保护账户安全', value: '', actions: { title: '设置', callback: () => { this.$message.error('This is a message of error') } } },
{ title: '备用邮箱', description: '已绑定邮箱', value: 'ant***sign.com', actions: { title: '修改', callback: () => { this.$message.warning('This is message of warning') } } },
{ title: 'MFA 设备', description: '未绑定 MFA 设备,绑定后,可以进行二次确认', value: '', actions: { title: '绑定', callback: () => { this.$message.info('This is a normal message') } } }
]
}
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,449 @@
<template>
<a-card :bordered="false">
<a-spin :tip="loadTip" :spinning="loading">
<search-form ref="search" @refresh="refreshTable" :preferenceAttrList="preferenceAttrList" />
<ci-detail ref="detail" :typeId="typeId" />
<div class="table-operator">
<a-button
type="primary"
icon="plus"
@click="$refs.create.visible = true; $refs.create.action='create'"
>新建</a-button>
<a-dropdown v-action:edit v-if="selectedRowKeys.length > 0">
<a-menu slot="overlay">
<a-menu-item
key="batchUpdate"
@click="$refs.create.visible = true; $refs.create.action='update'"
>
<span @click="$refs.create.visible = true">
<a-icon type="edit" />&nbsp;修改
</span>
</a-menu-item>
<a-menu-item key="batchDownload" @click="batchDownload">
<json-excel :fetch="batchDownload" name="cmdb.xls">
<a-icon type="download" />&nbsp;下载
</json-excel>
</a-menu-item>
<a-menu-item key="batchDelete" @click="batchDelete">
<a-icon type="delete" />删除
</a-menu-item>
</a-menu>
<a-button style="margin-left: 8px">
批量操作
<a-icon type="down" />
</a-button>
</a-dropdown>
</div>
<s-table
bordered
ref="table"
size="middle"
rowKey="_id"
:columns="columns"
:data="loadInstances"
:alert="options.alert"
:rowSelection="options.rowSelection"
:scroll="{ x: scrollX, y: scrollY }"
:pagination="{ showTotal: (total, range) => `${range[0]}-${range[1]} 共 ${total} 条记录`, pageSizeOptions: pageSizeOptions}"
showPagination="auto"
:pageSize="25"
>
<template :slot="col.dataIndex" slot-scope="text, record" v-for="col in columns">
<editable-cell
:key="'edit_' + col.dataIndex"
:text="text"
@change="onCellChange(record.key, col.dataIndex, $event, record[col.dataIndex])"
/>
</template>
<span slot="action" slot-scope="text, record">
<template>
<a
@click="$refs.detail.visible = true; $refs.detail.ciId = record.key; $refs.detail.create()"
>详情</a>
<a-divider type="vertical" />
<a @click="deleteCI(record)">删除</a>
</template>
</span>
</s-table>
<create-instance-form ref="create" @submit="batchUpdate" />
</a-spin>
</a-card>
</template>
<script>
import { setTimeout } from 'timers'
import { STable } from '@/components'
import JsonExcel from 'vue-json-excel'
import SearchForm from './modules/SearchForm'
import CreateInstanceForm from './modules/CreateInstanceForm'
import EditableCell from './modules/EditableCell'
import CiDetail from './modules/CiDetail'
import { searchCI, updateCI, deleteCI } from '@/api/cmdb/ci'
import { getSubscribeAttributes } from '@/api/cmdb/preference'
import { notification } from 'ant-design-vue'
var valueTypeMap = {
'0': 'int',
'1': 'float',
'2': 'text',
'3': 'datetime',
'4': 'date',
'5': 'time'
}
export default {
name: 'InstanceList',
components: {
STable,
EditableCell,
JsonExcel,
SearchForm,
CreateInstanceForm,
CiDetail
},
data() {
return {
loading: false,
loadTip: '',
pageSizeOptions: ['10', '25', '50', '100'],
form: this.$form.createForm(this),
valueTypeMap: valueTypeMap,
mdl: {},
typeId: this.$router.currentRoute.meta.typeId,
scrollX: 0,
scrollY: 0,
preferenceAttrList: [],
instanceList: [],
// 表头
columns: [],
// 加载数据方法 必须为 Promise 对象
loadInstances: parameter => {
const params = Object.assign(parameter, this.$refs.search.queryParam)
let q = `q=_type:${this.$router.currentRoute.meta.typeId}`
Object.keys(params).forEach(key => {
if (!['pageNo', 'pageSize', 'sortField', 'sortOrder'].includes(key) && params[key] + '' !== '') {
if (typeof params[key] === 'object' && params[key].length > 1) {
q += `,${key}:(${params[key].join(';')})`
} else if (params[key]) {
q += `,${key}:${params[key]}`
}
if (typeof params[key] === 'string') {
q += '*'
}
}
})
q += `&page=${params['pageNo']}&count=${params['pageSize']}`
if ('sortField' in params) {
let order = ''
if (params['sortOrder'] !== 'ascend') {
order = '-'
}
q += `&sort=${order}${params['sortField']}`
}
return searchCI(q).then(res => {
const result = {}
result.pageNo = res.page
result.pageSize = res.total
result.totalCount = res.numfound
result.totalPage = Math.ceil(res.numfound / params.pageSize)
result.data = Object.assign([], res.result)
result.data.forEach((item, index) => (item.key = item._id))
setTimeout(() => {
this.setColumnWidth()
}, 200)
this.instanceList = result.data
return result
})
},
// custom table alert & rowSelection
selectedRowKeys: [],
selectedRows: [],
options: {
alert: {
show: true,
clear: () => {
this.selectedRowKeys = []
}
},
rowSelection: {
selectedRowKeys: this.selectedRowKeys,
onChange: this.onSelectChange,
columnWidth: 62,
fixed: true
}
},
optionAlertShow: false,
watch: {
'$route.path': function(newPath, oldPath) {
this.reload()
}
}
}
},
created() {
this.tableOption()
this.loadColumns()
},
watch: {
'$route.path': function(newPath, oldPath) {
this.reload()
}
},
inject: ['reload'],
methods: {
setColumnWidth() {
let rows = []
try {
rows = document.querySelector('.ant-table-body').childNodes[0].childNodes[2].childNodes[0].childNodes
} catch (e) {
rows = document.querySelector('.ant-table-body').childNodes[0].childNodes[1].childNodes[0].childNodes
}
let scrollX = 0
const columns = Object.assign([], this.columns)
for (let i = 1; i < rows.length - 1; i++) {
columns[i - 1].width = rows[i].offsetWidth < 100 ? 100 : rows[i].offsetWidth
scrollX += columns[i - 1].width
}
this.columns = columns
this.scrollX =
scrollX +
document.querySelector('.ant-table-fixed-left').offsetWidth +
document.querySelector('.ant-table-fixed-right').offsetWidth
this.scrollY = window.innerHeight - this.$refs.table.$el.offsetTop - 300
},
tableOption() {
if (!this.optionAlertShow) {
this.options = {
alert: {
show: true,
clear: () => {
this.selectedRowKeys = []
}
},
rowSelection: {
selectedRowKeys: this.selectedRowKeys,
onChange: this.onSelectChange,
getCheckboxProps: record => ({
props: {
disabled: record.no === 'No 2', // Column configuration not to be checked
name: record.no
}
}),
columnWidth: 62,
fixed: true
}
}
this.optionAlertShow = true
} else {
alert('no alert')
this.options = {
alert: false,
rowSelection: null
}
this.optionAlertShow = false
}
},
loadColumns() {
getSubscribeAttributes(this.$router.currentRoute.meta.typeId).then(res => {
const prefAttrList = res.attributes
this.preferenceAttrList = prefAttrList
const columns = []
prefAttrList.forEach((item, index) => {
const col = {}
col.title = item.alias
col.dataIndex = item.name
if (index !== prefAttrList.length - 1) {
col.width = 100
}
if (item.is_sortable) {
col.sorter = true
}
if (item.is_choice) {
const filters = []
item.choice_value.forEach(item => filters.push({ text: item, value: item }))
col.filters = filters
}
col.scopedSlots = { customRender: item.name }
columns.push(col)
})
columns.push({
title: '操作',
key: 'operation',
width: 100,
fixed: 'right',
scopedSlots: { customRender: 'action' }
})
this.columns = columns
})
},
onSelectChange(selectedRowKeys, selectedRows) {
this.selectedRowKeys = selectedRowKeys
this.selectedRows = selectedRows
},
refreshTable(bool = false) {
this.$refs.table.refresh(bool)
},
onCellChange(key, dataIndex, event, oldValue) {
const value = event[0]
const payload = {}
payload[dataIndex] = value
updateCI(key, payload)
.then(res => {
event[1].x = false
})
.catch(err => {
notification.error({
message: err.response.data.message
})
})
},
async batchDownload() {
this.loading = true
this.loadTip = '正在下载 ...'
const promises = this.selectedRowKeys.map(ciId => {
return searchCI(`q=_id:${ciId}`).then(res => {
let ciMap = {}
Object.keys(res.result[0]).forEach(k => {
if (!["ci_type", "_id", "ci_type_alias", "_type"].includes(k)) {
ciMap[k] = res.result[0][k]
}
})
return ciMap
})
})
const results = await Promise.all(promises)
this.loading = false
this.$refs.table.clearSelected()
return results
},
batchUpdate(values) {
const that = this
this.$confirm({
title: '警告',
content: '确认要批量修改吗 ?',
onOk() {
that.loading = true
that.loadTip = '正在批量修改 ...'
const payload = {}
Object.keys(values).forEach(key => {
if (values[key]) {
payload[key] = values[key]
}
})
const promises = that.selectedRowKeys.map(ciId => {
return updateCI(ciId, payload).then(res => {
return 'ok'
})
})
Promise.all(promises)
.then(res => {
that.loading = false
notification.success({
message: '批量修改成功'
})
that.$refs.create.visible = false
that.$refs.table.clearSelected()
that.$refs.table.refresh(true)
that.reload()
})
.catch(e => {
console.log(e)
that.loading = false
notification.error({
message: e.response.data.message
})
that.$refs.table.refresh(true)
})
}
})
},
batchDelete() {
const that = this
this.$confirm({
title: '警告',
content: '真的要删除吗 ?',
onOk() {
that.loading = true
that.loadTip = '正在删除 ...'
const promises = that.selectedRowKeys.map(ciId => {
return deleteCI(ciId).then(res => {
return 'ok'
})
})
Promise.all(promises)
.then(res => {
that.loading = false
notification.success({
message: '删除成功'
})
that.$refs.table.clearSelected()
that.$refs.table.refresh(true)
})
.catch(e => {
console.log(e)
that.loading = false
notification.error({
message: e.response.data.message
})
that.$refs.table.refresh(true)
})
}
})
},
deleteCI(record) {
const that = this
this.$confirm({
title: '警告',
content: '真的要删除吗 ?',
onOk() {
deleteCI(record.key)
.then(res => {
that.$refs.table.refresh(true)
})
.catch(e => {
console.log(e)
notification.error({
message: e.response.data.message
})
})
}
})
}
}
}
</script>
<style>
.ant-table-thead > tr > th,
.ant-table-tbody > tr > td {
white-space: nowrap;
overflow: hidden;
}
.spin-content {
border: 1px solid #91d5ff;
background-color: #e6f7ff;
padding: 30px;
}
</style>

View File

@@ -0,0 +1,369 @@
<template>
<a-drawer
width="80%"
placement="left"
@close="() => { visible = false }"
:visible="visible"
:wrapStyle="{height: 'calc(100% - 108px)', overflow: 'auto', paddingBottom: '108px'}"
>
<a-card
:bordered="false"
:tabList="tabList"
:activeTabKey="activeTabKey"
:selectedKeys="[typeId]"
@tabChange="(key) => {this.activeTabKey = key}"
>
<div v-if="activeTabKey === 'tab_1'">
<a-card
type="inner"
:title="group.name || '其他'"
:key="group.name"
v-for="group in attributeGroups"
>
<description-list title size="small">
<div :key="attr.name" v-for="(attr, index) in group.attributes">
<div v-if="index % 3 === 0" style="clear: both"></div>
<description-list-item :term="attr.alias || attr.name">{{ ci[attr.name] }}</description-list-item>
</div>
</description-list>
</a-card>
</div>
<div v-if="activeTabKey === 'tab_2'">
<div v-for="parent in parentCITypes" :key="'ctr_' + parent.ctr_id">
<a-card type="inner" :title="parent.alias || parent.name" v-if="firstCIs[parent.name]">
<a-table
rowKey="_id"
size="middle"
:columns="firstCIColumns[child.id]"
:dataSource="firstCIs[child.name]"
:pagination="false"
:scroll="{x: '100%'}"
></a-table>
</a-card>
</div>
<div v-for="child in childCITypes" :key="'ctr_' + child.ctr_id">
<a-card type="inner" :title="child.alias || child.name" v-if="secondCIs[child.name]">
<a-table
rowKey="_id"
size="middle"
:columns="secondCIColumns[child.id]"
:dataSource="secondCIs[child.name]"
:pagination="false"
:scroll="{x: '100%'}"
></a-table>
</a-card>
</div>
</div>
<div v-if="activeTabKey === 'tab_3'">
<a-card type="inner" :bordered="false">
<a-table
bordered
rowKey="hid"
size="middle"
:columns="historyColumns"
:dataSource="ciHistory"
:pagination="false"
:scroll="{x: '100%'}"
>
<template
slot="operate_type"
slot-scope="operate_type"
>{{ operate_type | operateTypeFilter }}</template>
</a-table>
</a-card>
</div>
</a-card>
</a-drawer>
</template>
<script>
import DescriptionList from '@/components/DescriptionList'
import { getCITypeGroupById } from '@/api/cmdb/CIType'
import { getCITypeChildren, getCITypeParent } from '@/api/cmdb/CITypeRelation'
import { getFirstCIs, getSecondCIs } from '@/api/cmdb/CIRelation'
import { getCIHistory } from '@/api/cmdb/history'
import { getCIById } from '@/api/cmdb/ci'
import { notification } from 'ant-design-vue'
const DescriptionListItem = DescriptionList.Item
export default {
components: {
DescriptionList,
DescriptionListItem
},
props: {
typeId: {
type: Number,
required: true
}
},
data () {
return {
visible: false,
parentCITypes: [],
childCITypes: [],
firstCIs: {},
firstCIColumns: {},
secondCIs: {},
secondCIColumns: {},
ci: {},
attributeGroups: [],
tabList: [
{
key: 'tab_1',
tab: '属性'
},
{
key: 'tab_2',
tab: '关系'
},
{
key: 'tab_3',
tab: '操作历史'
}
],
activeTabKey: 'tab_1',
rowSpanMap: {},
historyColumns: [
{
title: '时间',
dataIndex: 'created_at',
key: 'created_at',
customRender: (value, row, index) => {
const obj = {
children: value,
attrs: {}
}
obj.attrs.rowSpan = this.rowSpanMap[index]
return obj
}
},
{
title: '用户',
dataIndex: 'username',
key: 'username',
customRender: (value, row, index) => {
const obj = {
children: value,
attrs: {}
}
obj.attrs.rowSpan = this.rowSpanMap[index]
return obj
}
},
{
title: '操作',
dataIndex: 'operate_type',
key: 'operate_type',
scopedSlots: { customRender: 'operate_type' }
},
{
title: '属性',
dataIndex: 'attr_alias',
key: 'attr_name'
},
{
title: 'Old',
dataIndex: 'old',
key: 'old'
},
{
title: 'New',
dataIndex: 'new',
key: 'new'
}
],
ciHistory: []
}
},
created () {
// this.getAttributes()
// this.getCI()
// this.getFirstCIs()
// this.getSecondCIs()
// this.getParentCITypes()
// this.getChildCITypes()
// this.getCIHistory()
},
filters: {
operateTypeFilter (operateType) {
const operateTypeMap = {
'0': '新增',
'1': '删除',
'2': '修改'
}
return operateTypeMap[operateType]
}
},
methods: {
create () {
this.getAttributes()
this.getCI()
this.getFirstCIs()
this.getSecondCIs()
this.getParentCITypes()
this.getChildCITypes()
this.getCIHistory()
},
getAttributes () {
getCITypeGroupById(this.typeId, { need_other: 1 })
.then(res => {
this.attributeGroups = res
})
.catch(e => {
console.log(e)
notification.error({
message: e.response.data.message
})
})
},
getCI () {
getCIById(this.ciId)
.then(res => {
this.ci = res.ci
})
.catch(e => {
notification.error({
message: e.response.data.message
})
})
},
getFirstCIs () {
getFirstCIs(this.ciId)
.then(res => {
const firstCIs = {}
res.first_cis.forEach(item => {
if (item.ci_type in firstCIs) {
firstCIs[item.ci_type].push(item)
} else {
firstCIs[item.ci_type] = [item]
}
})
this.firstCIs = firstCIs
})
.catch(e => {
notification.error({
message: e.response.data.message
})
})
},
getSecondCIs () {
getSecondCIs(this.ciId)
.then(res => {
const secondCIs = {}
res.second_cis.forEach(item => {
if (item.ci_type in secondCIs) {
secondCIs[item.ci_type].push(item)
} else {
secondCIs[item.ci_type] = [item]
}
})
this.secondCIs = secondCIs
})
.catch(e => {
notification.error({
message: e.response.data.message
})
})
},
getParentCITypes () {
getCITypeParent(this.typeId)
.then(res => {
this.parentCITypes = res.parents
const firstCIColumns = {}
res.parents.forEach(item => {
const columns = []
item.attributes.forEach(attr => {
columns.push({ key: 'p_' + attr.id, dataIndex: attr.name, title: attr.alias })
})
firstCIColumns[item.id] = columns
})
this.firstCIColumns = firstCIColumns
})
.catch(e => {
notification.error({
message: e.response.data.message
})
})
},
getChildCITypes () {
getCITypeChildren(this.typeId)
.then(res => {
this.childCITypes = res.children
const secondCIColumns = {}
res.children.forEach(item => {
const columns = []
item.attributes.forEach(attr => {
columns.push({ key: 'c_' + attr.id, dataIndex: attr.name, title: attr.alias })
})
secondCIColumns[item.id] = columns
})
this.secondCIColumns = secondCIColumns
})
.catch(e => {
notification.error({
message: e.response.data.message
})
})
},
getCIHistory () {
getCIHistory(this.ciId)
.then(res => {
this.ciHistory = res
const rowSpanMap = {}
let startIndex = 0
let startCount = 1
res.forEach((item, index) => {
if (index === 0) {
return
}
if (res[index].record_id === res[startIndex].record_id) {
startCount += 1
rowSpanMap[index] = 0
if (index === res.length - 1) {
rowSpanMap[startIndex] = startCount
}
} else {
rowSpanMap[startIndex] = startCount
startIndex = index
startCount = 1
if (index === res.length - 1) {
rowSpanMap[index] = 1
}
}
})
this.rowSpanMap = rowSpanMap
})
.catch(e => {
console.log(e)
notification.error({
message: e.response.data.message
})
})
}
}
}
</script>
<style lange="less">
div.term {
background-color: rgb(225, 238, 246);
font-weight: 400;
min-width: 120px !important;
padding-left: 10px;
}
div.content {
word-wrap: break-word;
word-break: break-all;
font-weight: bold;
padding-left: 10px;
}
</style>

View File

@@ -0,0 +1,158 @@
<template>
<!-- v-decorator="[ item.name, { rules: [ { required: item.is_required ? true: false } ] } ]" -->
<a-drawer
:title="title + CIType.alias"
width="500"
@close="() => { visible = false }"
:visible="visible"
:wrapStyle="{height: 'calc(100% - 108px)', overflow: 'auto', paddingBottom: '108px'}"
>
<p v-if="action === 'update'">只需要填写需要修改的字段即可!</p>
<a-form :form="form" :layout="formLayout" @submit="createInstance">
<a-button type="primary" @click="createInstance">Submit</a-button>
<a-form-item
v-bind="formItemLayout"
:label="attr.alias || attr.name"
v-for="(attr, attr_idx) in attributeList"
:key="attr.name + attr_idx"
>
<a-select
v-decorator="[ attr.name, { rules: [ { required: attr.is_required && action === 'create' ? true: false } ] } ]"
placeholder="请选择"
v-if="attr.is_choice"
>
<a-select-option
:value="choice"
:key="'New_' + attr.name + choice_idx"
v-for="(choice, choice_idx) in attr.choice_value"
>{{ choice }}</a-select-option>
</a-select>
<a-input-number
v-decorator="[ attr.name, { rules: [ { required: attr.is_required && action === 'create' ? true: false } ] } ]"
style="width: 100%"
v-else-if="valueTypeMap[attr.value_type] == 'int' || valueTypeMap[attr.value_type] == 'float'"
/>
<a-date-picker
v-decorator="[ attr.name, { rules: [ { required: attr.is_required && action === 'create' ? true: false } ] } ]"
style="width: 100%"
:format="valueTypeMap[attr.value_type] == 'date' ? 'YYYY-MM-DD': 'YYYY-MM-DD HH:mm:ss'"
v-else-if="valueTypeMap[attr.value_type] == 'date' || valueTypeMap[attr.value_type] == 'datetime'"
/>
<a-input
v-decorator="[attr.name, {validateTrigger: ['submit'], rules: [{ required: attr.is_required && action === 'create' ? true: false}]}]"
style="width: 100%"
v-else
/>
</a-form-item>
</a-form>
</a-drawer>
</template>
<script>
import { getCIType } from '@/api/cmdb/CIType'
import { getCITypeAttributesById } from '@/api/cmdb/CITypeAttr'
import { addCI } from '@/api/cmdb/ci'
import { notification } from 'ant-design-vue'
var valueTypeMap = {
'0': 'int',
'1': 'float',
'2': 'text',
'3': 'datetime',
'4': 'date',
'5': 'time'
}
export default {
data () {
return {
action: '',
form: this.$form.createForm(this),
visible: false,
attributeList: [],
typeId: this.$router.currentRoute.meta.typeId,
CIType: {},
valueTypeMap: valueTypeMap,
formItemLayout: {
labelCol: {
xs: { span: 24 },
sm: { span: 8 }
},
wrapperCol: {
xs: { span: 24 },
sm: { span: 16 }
}
},
formLayout: 'horizontal'
}
},
computed: {
title () {
return this.action === 'create' ? '创建 ' : '批量修改 '
}
},
watch: {
'$route.path': function (oldValue, newValue) {
this.typeId = this.$router.currentRoute.meta.typeId
this.getCIType()
this.getAttributeList()
}
},
created () {
this.getCIType()
this.getAttributeList()
},
methods: {
getCIType () {
getCIType(this.typeId).then(res => {
this.CIType = res.ci_types[0]
})
},
getAttributeList () {
getCITypeAttributesById(this.typeId).then(res => {
const attrList = res.attributes
// res.attributes.forEach(item =>
// attrList.push({
// name: item.name,
// alias: item.alias,
// value_type: item.value_type,
// is_required: item.is_required
// })
// )
this.attributeList = attrList.sort((x, y) => y.is_required - x.is_required)
})
},
createInstance (e) {
e.preventDefault()
this.form.validateFields((err, values) => {
if (!err) {
if (this.action === 'update') {
this.$emit('submit', values)
return
}
values.ci_type = this.typeId
addCI(values)
.then(res => {
notification.success({
message: '新增成功'
})
})
.catch(e => {
notification.error({
message: e.response.data.message
})
})
} else {
notification.error({
message: err
})
}
})
}
}
}
</script>

View File

@@ -0,0 +1,76 @@
<template>
<div class="editable-cell">
<div v-if="editable.x" class="editable-cell-input-wrapper">
<a-input :value="value" @change="handleChange" @pressEnter="check" />
<a-icon type="check" class="editable-cell-icon-check" @click="check" />
</div>
<div v-else class="editable-cell-text-wrapper">
{{ value | joinList }}
<a-icon type="edit" class="editable-cell-icon" @click="edit" />
</div>
</div>
</template>
<script>
export default {
props: {
text: {
required: true
}
},
data () {
return {
value: this.text,
editable: { x: false }
}
},
methods: {
handleChange (e) {
const value = e.target.value
this.value = value
},
check () {
// this.editable.x = false
this.$emit('change', [this.value, this.editable])
},
edit () {
this.editable.x = true
}
},
filters: {
joinList: function (itemValue) {
if (typeof itemValue === 'object' && itemValue) {
return itemValue.join(',')
} else {
return itemValue || ' '
}
}
}
}
</script>
<style scoped>
.editable-cell {
position: relative;
}
.editable-cell-icon,
.editable-cell-icon-check {
position: absolute;
right: 0;
width: 15px;
cursor: pointer;
}
.editable-cell-icon {
display: none;
}
td:hover > .editable-cell .editable-cell-icon {
display: inline-block;
}
.editable-cell-icon:hover,
.editable-cell-icon-check:hover {
color: #108ee9;
}
</style>

View File

@@ -0,0 +1,131 @@
<template>
<div class="table-page-search-wrapper">
<a-form layout="inline">
<a-row :gutter="48">
<a-col
:lg="6"
:md="8"
:sm="24"
:key="prefAttr.name"
v-for="prefAttr in preferenceAttrList.slice(0, 4)"
>
<a-form-item :label="prefAttr.alias || prefAttr.name">
<a-select
v-model="queryParam[prefAttr.name]"
placeholder="请选择"
v-if="prefAttr.is_choice"
>
<a-select-option
:value="choice"
:key="'Search_' + prefAttr.name + index"
v-for="(choice, index) in prefAttr.choice_value"
>{{ choice }}</a-select-option>
</a-select>
<a-input-number
v-model="queryParam[prefAttr.name]"
style="width: 100%"
v-else-if="valueTypeMap[prefAttr.value_type] == 'int' || valueTypeMap[prefAttr.value_type] == 'float'"
/>
<a-date-picker
v-model="queryParam[prefAttr.name]"
style="width: 100%"
:format="valueTypeMap[prefAttr.value_type] == 'date' ? 'YYYY-MM-DD': 'YYYY-MM-DD HH:mm:ss'"
v-else-if="valueTypeMap[prefAttr.value_type] == 'date' || valueTypeMap[prefAttr.value_type] == 'datetime'"
/>
<a-input v-model="queryParam[prefAttr.name]" style="width: 100%" v-else />
</a-form-item>
</a-col>
<template v-if="advanced && preferenceAttrList.length > 4">
<a-col
:lg="6"
:md="8"
:sm="24"
:key="'advanced_' + item.name"
v-for="item in preferenceAttrList.slice(4)"
>
<a-form-item :label="item.alias || item.name">
<a-select v-model="queryParam[item.name]" placeholder="请选择" v-if="item.is_choice">
<a-select-option
:value="choice"
:key="'advanced_' + item.name + index"
v-for="(choice, index) in item.choice_value"
>{{ choice }}</a-select-option>
</a-select>
<a-input-number
v-model="queryParam[item.name]"
style="width: 100%"
v-else-if="valueTypeMap[item.value_type] == 'int' || valueTypeMap[item.value_type] == 'float'"
/>
<a-date-picker
v-model="queryParam[item.name]"
style="width: 100%"
:format="valueTypeMap[item.value_type] == 'date' ? 'YYYY-MM-DD': 'YYYY-MM-DD HH:mm:ss'"
v-else-if="valueTypeMap[item.value_type] == 'date' || valueTypeMap[item.value_type] == 'datetime'"
/>
<a-input v-model="queryParam[item.name]" style="width: 100%" v-else />
</a-form-item>
</a-col>
</template>
<a-col :lg="!advanced && 6 || 24" :md="!advanced && 8 || 24" :sm="24" style="float: right">
<span
class="table-page-search-submitButtons"
:style="advanced && { float: 'right', overflow: 'hidden' } || {} "
>
<a-button type="primary" @click="$emit('refresh', true)">查询</a-button>
<a-button style="margin-left: 8px" @click="() => queryParam = {}">重置</a-button>
<a
@click="toggleAdvanced"
style="margin-left: 8px"
v-if="preferenceAttrList.length > 4"
>
{{ advanced ? '收起' : '展开' }}
<a-icon :type="advanced ? 'up' : 'down'" />
</a>
</span>
</a-col>
</a-row>
</a-form>
</div>
</template>
<script>
var valueTypeMap = {
'0': 'int',
'1': 'float',
'2': 'text',
'3': 'datetime',
'4': 'date',
'5': 'time'
}
export default {
data () {
return {
// 高级搜索 展开/关闭
advanced: false,
queryParam: {},
valueTypeMap: valueTypeMap
}
},
props: {
preferenceAttrList: {
type: Array,
required: true
}
},
watch: {
'$route.path': function (oldValue, newValue) {
this.queryParam = {}
}
},
methods: {
toggleAdvanced () {
this.advanced = !this.advanced
}
}
}
</script>

View File

@@ -0,0 +1,366 @@
<template>
<a-card>
<div class="card-list" ref="content">
<a-list :grid="{gutter: 24, lg: 4, md: 3, sm: 2, xs: 1}" :dataSource="citypeData.ci_types">
<a-list-item slot="renderItem" slot-scope="item" v-if="!item.is_attached && item.enabled">
<template>
<a-card :hoverable="true">
<a-card-meta>
<a-avatar
class="card-avatar"
slot="avatar"
:src="item.avatar"
:size="32"
:style="item.is_subscribed ? 'color: #FFF; backgroundColor: green' : 'color: #FFF; backgroundColor: lightgrey'"
>{{ item.avatar || item.name[0].toUpperCase() }}</a-avatar>
<span class="margin-bottom: 3px" slot="title">{{ item.alias || item.name }}</span>
<span
:class="item.is_subscribed?'subscribe-success':'unsubscribe'"
slot="title"
>{{ item.is_subscribed ? "已订阅" : "未订阅" }}</span>
</a-card-meta>
<template class="ant-card-actions" slot="actions">
<a :disabled="!item.is_subscribed" @click="unsubscribe(item.id)">取消</a>
<a @click="showDrawer(item.id, item.alias || item.name)">订阅</a>
</template>
</a-card>
</template>
</a-list-item>
</a-list>
<template>
<div>
<a-drawer
:title="'订阅模型: ' + typeName"
:width="600"
@close="onClose"
:visible="visible"
:wrapStyle="{height: 'calc(100% - 108px)', overflow: 'auto', paddingBottom: '108px'}"
>
<a-alert message="既可以定义树形视图, 也可以订阅资源视图, 资源视图会在SideBar单独呈现" type="info" showIcon />
<a-divider>
树形视图
<span
v-if="treeSubscribed"
style="font-weight: 500; font-size: 12px; color: green"
>已订阅</span>
<span style="font-weight: 500; font-size: 12px; color: red" v-else>未订阅</span>
</a-divider>
<a-select
ref="tree"
mode="multiple"
placeholder="- - 目录层级的选择 - -"
:value="treeViews"
style="width: 100%"
@change="handleTreeSub"
>
<a-select-option v-for="attr in attrList" :key="attr.title">{{ attr.title }}</a-select-option>
</a-select>
<a-button
@click="subTreeSubmit"
type="primary"
:style="{float: 'right', marginTop: '10px'}"
>订阅</a-button>
<br />
<br />
<a-divider>
资源视图
<span
v-if="instanceSubscribed"
style="font-weight: 500; font-size: 12px; color: green"
>已订阅</span>
<span style="font-weight: 500; font-size: 12px; color: red" v-else>未订阅</span>
</a-divider>
<template>
<a-transfer
:dataSource="attrList"
:showSearch="true"
:listStyle="{
width: '230px',
height: '500px',
}"
:titles="['未选属性','已选属性']"
:render="item=>item.title"
:targetKeys="selectedAttrList"
@change="handleChange"
@search="handleSearch"
>
<span slot="notFoundContent">没数据</span>
</a-transfer>
</template>
<div
:style="{
position: 'absolute',
left: 0,
bottom: 0,
width: '100%',
borderTop: '1px solid #e9e9e9',
padding: '10px 16px',
background: '#fff',
textAlign: 'right',
}"
>
<a-button :style="{marginRight: '8px'}" @click="onClose">取消</a-button>
<a-button @click="subInstanceSubmit" type="primary">订阅</a-button>
</div>
</a-drawer>
</div>
</template>
</div>
</a-card>
</template>
<script>
import router, { resetRouter } from '@/router'
import store from '@/store'
import { notification } from 'ant-design-vue'
import { getCITypes } from '@/api/cmdb/CIType'
import {
getPreference,
subscribeCIType,
getSubscribeAttributes,
getSubscribeTreeView,
subscribeTreeView
} from '@/api/cmdb/preference'
import { getCITypeAttributesByName } from '@/api/cmdb/CITypeAttr'
import { Promise } from 'q'
export default {
data () {
return {
visible: false,
typeId: null,
typeName: null,
instanceSubscribed: false,
treeSubscribed: false,
citypeData: {},
attrList: [],
selectedAttrList: [],
treeViews: []
}
},
created () {
this.getCITypes()
},
methods: {
getCITypes () {
getCITypes().then(res => {
getPreference(true, true).then(pref => {
pref.forEach(item => {
res.ci_types.forEach(ciType => {
if (item.id === ciType.id) {
ciType.is_subscribed = true
}
})
})
this.citypeData = res
})
})
},
unsubscribe (citypeId) {
const that = this
this.$confirm({
title: '警告',
content: '真的要取消订阅吗 ?',
onOk () {
const unsubCIType = subscribeCIType(citypeId, '')
const unsubTree = subscribeTreeView(citypeId, '')
Promise.all([unsubCIType, unsubTree])
.then(() => {
notification.success({
message: '取消成功'
})
that.resetRoute()
})
.catch(e => {
console.log(e)
notification.error({
message: e.response.data.message
})
})
},
onCancel () {}
})
},
// 显示右边的弹出框
showDrawer (typeId, typeName) {
this.typeId = typeId
this.typeName = typeName
this.getAttrList()
this.getTreeView(typeId)
},
onClose () {
this.visible = false
this.resetRoute()
},
getAttrList () {
getCITypeAttributesByName(this.typeId).then(res => {
const attributes = res.attributes
getSubscribeAttributes(this.typeId).then(_res => {
const attrList = []
const selectedAttrList = []
const subAttributes = _res.attributes
this.instanceSubscribed = _res.is_subscribed
subAttributes.forEach(item => {
selectedAttrList.push(item.id.toString())
})
attributes.forEach(item => {
const data = {
key: item.id.toString(),
title: item.alias || item.name
}
attrList.push(data)
})
this.attrList = attrList
this.selectedAttrList = selectedAttrList
this.visible = true
})
})
},
handleTreeSub (values) {
this.treeViews = values
},
// 处理点击改变事件
handleChange (targetKeys, direction, moveKeys) {
this.selectedAttrList = targetKeys
},
handleSearch (dir, value) {},
// 处理提交事件
subInstanceSubmit () {
subscribeCIType(this.typeId, this.selectedAttrList)
.then(res => {
notification.success({
message: '订阅成功'
})
this.resetRoute()
})
.catch(e => {
notification.error({
message: e.response.data.message
})
})
},
getTreeView (typeId) {
const that = this
this.treeViews = []
getSubscribeTreeView()
.then(res => {
let hasMatch = false
res.forEach(item => {
if (item.type_id === typeId) {
hasMatch = true
const levels = []
if (item.levels && item.levels.length >= 1) {
item.levels.forEach(level => {
levels.push(level.alias)
})
}
if (levels.length > 0) {
that.treeSubscribed = true
} else {
that.treeSubscribed = false
}
that.treeViews = levels
}
})
if (!hasMatch) {
that.treeSubscribed = false
}
})
.catch(e => {
console.log(e)
notification.error({
message: e.response.data.message
})
})
},
subTreeSubmit () {
subscribeTreeView(this.typeId, this.treeViews)
.then(res => {
notification.success({
message: '订阅成功'
})
})
.catch(e => {
notification.error({
message: e.response.data.message
})
})
},
resetRoute () {
resetRouter()
const roles = store.getters.roles
store.dispatch('GenerateRoutes', { roles }, { root: true }).then(() => {
router.addRoutes(store.getters.addRouters)
this.getCITypes()
})
}
}
}
</script>
<style lang="less" scoped>
.card-avatar {
width: 48px;
height: 48px;
border-radius: 48px;
}
.ant-card-actions {
background: #f7f9fa;
li {
float: left;
text-align: center;
margin: 12px 0;
color: rgba(0, 0, 0, 0.45);
width: 50%;
&:not(:last-child) {
border-right: 1px solid #e8e8e8;
}
a {
color: rgba(0, 0, 0, 0.45);
line-height: 22px;
display: inline-block;
width: 100%;
&:hover {
color: #1890ff;
}
}
}
}
.new-btn {
background-color: #fff;
border-radius: 2px;
width: 100%;
height: 188px;
}
.meta-content {
position: relative;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
height: 64px;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
}
.subscribe-success {
float: right;
color: green;
font-size: 12px;
font-weight: 500;
}
.unsubscribe {
float: right;
color: gray;
font-size: 12px;
font-weight: 300;
}
</style>

View File

@@ -0,0 +1,248 @@
<template>
<a-card :bordered="false">
<a-menu v-model="current" mode="horizontal" v-if="ciTypes.length">
<a-menu-item :key="ciType.id" v-for="ciType in ciTypes">
<router-link
:to="{name: 'cmdb_tree_views_item', params: {typeId: ciType.id}}"
>{{ ciType.alias || ciTypes.name }}</router-link>
</a-menu-item>
</a-menu>
<a-alert message="请先到 我的订阅 页面完成订阅!" banner v-else></a-alert>
<div style="clear: both; margin-top: 20px"></div>
<template>
<a-row :gutter="8">
<a-col :span="5">
<a-tree showLine :loadData="onLoadData" @select="onSelect" :treeData="treeData"></a-tree>
</a-col>
<a-col :span="19">
<s-table
v-if="ciTypes.length"
bordered
ref="table"
size="middle"
rowKey="_id"
:columns="columns"
:data="loadInstances"
:scroll="{ x: scrollX, y: scrollY }"
:pagination="{ showTotal: (total, range) => `${range[0]}-${range[1]} 共 ${total} 条记录`, pageSizeOptions: pageSizeOptions}"
:pageSize="25"
showPagination="auto"
></s-table>
</a-col>
</a-row>
</template>
</a-card>
</template>
<script>
import { STable } from '@/components'
import { getSubscribeTreeView, getSubscribeAttributes } from '@/api/cmdb/preference'
import { searchCI } from '@/api/cmdb/ci'
export default {
components: { STable },
data () {
return {
treeData: [],
triggerSelect: false,
treeNode: null,
ciTypes: [],
levels: [],
typeId: null,
current: [],
instanceList: [],
treeKeys: [],
columns: [],
pageSizeOptions: ['10', '25', '50', '100'],
loading: false,
scrollX: 0,
scrollY: 0,
loadInstances: parameter => {
const params = parameter || {}
// const params = Object.assign(parameter, this.$refs.search.queryParam)
let q = `q=_type:${this.typeId}`
Object.keys(params).forEach(key => {
if (!['pageNo', 'pageSize', 'sortField', 'sortOrder'].includes(key) && params[key] + '' !== '') {
if (typeof params[key] === 'object' && params[key].length > 1) {
q += `,${key}:(${params[key].join(';')})`
} else if (params[key]) {
q += `,${key}:${params[key]}`
}
if (typeof params[key] === 'string') {
q += '*'
}
}
})
if (this.treeKeys.length > 0) {
this.treeKeys.forEach((item, idx) => {
q += `,${this.levels[idx].name}:${item}`
})
}
if (this.levels.length > this.treeKeys.length) {
q += `&facet=${this.levels[this.treeKeys.length].name}`
}
if ('pageNo' in params) {
q += `&page=${params['pageNo']}&count=${params['pageSize']}`
}
if ('sortField' in params) {
let order = ''
if (params['sortOrder'] !== 'ascend') {
order = '-'
}
q += `&sort=${order}${params['sortField']}`
}
return searchCI(q).then(res => {
const result = {}
result.pageNo = res.page
result.pageSize = res.total
result.totalCount = res.numfound
result.totalPage = Math.ceil(res.numfound / (params.pageSize || 25))
result.data = Object.assign([], res.result)
result.data.forEach((item, index) => (item.key = item._id))
setTimeout(() => {
this.setColumnWidth()
}, 200)
if (Object.values(res.facet).length) {
this.wrapTreeData(res.facet)
}
return result
})
}
}
},
created () {
this.getCITypes()
},
inject: ['reload'],
watch: {
'$route.path': function (newPath, oldPath) {
this.typeId = this.$route.params.typeId
this.getCITypes()
this.reload()
}
},
methods: {
onSelect (keys) {
this.triggerSelect = true
if (keys.length) {
this.treeKeys = keys[0].split('-').filter(item => item !== '')
}
this.$refs.table.refresh(true)
},
wrapTreeData (facet) {
if (this.triggerSelect) {
return
}
const treeData = []
Object.values(facet)[0].forEach(item => {
treeData.push({
title: `${item[0]} (${item[1]})`,
key: this.treeKeys.join('-') + '-' + item[0],
isLeaf: this.levels.length - 1 === this.treeKeys.length
})
})
if (this.treeNode === null) {
this.treeData = treeData
} else {
this.treeNode.dataRef.children = treeData
this.treeData = [...this.treeData]
}
},
setColumnWidth () {
let rows = []
try {
rows = document.querySelector('.ant-table-body').childNodes[0].childNodes[2].childNodes[0].childNodes
} catch (e) {
rows = document.querySelector('.ant-table-body').childNodes[0].childNodes[1].childNodes[0].childNodes
}
let scrollX = 0
const columns = Object.assign([], this.columns)
for (let i = 0; i < rows.length; i++) {
columns[i].width = rows[i].offsetWidth < 80 ? 80 : rows[i].offsetWidth
scrollX += columns[i].width
}
this.columns = columns
this.scrollX = scrollX
this.scrollY = window.innerHeight - this.$refs.table.$el.offsetTop - 300
},
onLoadData (treeNode) {
this.triggerSelect = false
return new Promise(resolve => {
if (treeNode.dataRef.children) {
resolve()
return
}
this.treeKeys = treeNode.eventKey.split('-').filter(item => item !== '')
this.treeNode = treeNode
this.$refs.table.refresh(true)
resolve()
})
},
getCITypes () {
getSubscribeTreeView().then(res => {
this.ciTypes = res
if (this.ciTypes.length) {
this.typeId = this.$route.params.typeId || this.ciTypes[0].id
this.current = [this.typeId]
this.loadColumns()
this.levels = res.find(item => item.id === this.typeId).levels
this.$refs.table.refresh(true)
}
})
},
loadColumns () {
getSubscribeAttributes(this.typeId).then(res => {
const prefAttrList = res.attributes
const columns = []
prefAttrList.forEach((item, index) => {
const col = {}
col.title = item.alias
col.dataIndex = item.name
if (index !== prefAttrList.length - 1) {
col.width = 80
}
if (item.is_sortable) {
col.sorter = true
}
if (item.is_choice) {
const filters = []
item.choice_value.forEach(item => filters.push({ text: item, value: item }))
col.filters = filters
}
col.scopedSlots = { customRender: item.name }
columns.push(col)
})
this.columns = columns
})
}
}
}
</script>
<style scoped>
.ant-menu-horizontal {
border-bottom: 1px solid #ebedf0 !important;
}
.ant-menu-horizontal {
border-bottom: 1px solid #ebedf0 !important;
}
</style>

View File

@@ -0,0 +1,17 @@
<template>
<exception-page type="403" />
</template>
<script>
import { ExceptionPage } from '@/components'
export default {
components: {
ExceptionPage
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,17 @@
<template>
<exception-page type="404" />
</template>
<script>
import { ExceptionPage } from '@/components'
export default {
components: {
ExceptionPage
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,17 @@
<template>
<exception-page type="500" />
</template>
<script>
import { ExceptionPage } from '@/components'
export default {
components: {
ExceptionPage
}
}
</script>
<style scoped>
</style>

210
ui/src/views/user/Login.vue Normal file
View File

@@ -0,0 +1,210 @@
<template>
<div class="main">
<a-form
id="formLogin"
class="user-layout-login"
ref="formLogin"
:form="form"
@submit="handleSubmit"
>
<a-tabs
:activeKey="customActiveKey"
:tabBarStyle="{ textAlign: 'center', borderBottom: 'unset' }"
@change="handleTabClick"
>
<a-tab-pane key="tab1" tab="账号密码登录">
<a-form-item>
<a-input
size="large"
type="text"
placeholder="用户名或者邮箱"
v-decorator="[
'username',
{rules: [{ required: true, message: '请输入帐户名或邮箱地址' }, { validator: handleUsernameOrEmail }], validateTrigger: 'change'}
]"
>
<a-icon slot="prefix" type="user" :style="{ color: 'rgba(0,0,0,.25)' }"/>
</a-input>
</a-form-item>
<a-form-item>
<a-input
size="large"
type="password"
autocomplete="false"
placeholder="密码"
v-decorator="[
'password',
{rules: [{ required: true, message: '请输入密码' }], validateTrigger: 'blur'}
]"
>
<a-icon slot="prefix" type="lock" :style="{ color: 'rgba(0,0,0,.25)' }"/>
</a-input>
</a-form-item>
</a-tab-pane>
</a-tabs>
<a-form-item>
<a-checkbox v-decorator="['rememberMe']">自动登录</a-checkbox>
</a-form-item>
<a-form-item style="margin-top:24px">
<a-button
size="large"
type="primary"
htmlType="submit"
class="login-button"
:loading="state.loginBtn"
:disabled="state.loginBtn"
>确定</a-button>
</a-form-item>
</a-form>
</div>
</template>
<script>
import md5 from 'md5'
import { mapActions } from 'vuex'
import { timeFix } from '@/utils/util'
export default {
data () {
return {
customActiveKey: 'tab1',
loginBtn: false,
// login type: 0 email, 1 username, 2 telephone
loginType: 0,
requiredTwoStepCaptcha: false,
stepCaptchaVisible: false,
form: this.$form.createForm(this),
state: {
time: 60,
loginBtn: false,
// login type: 0 email, 1 username, 2 telephone
loginType: 0,
smsSendBtn: false
}
}
},
created () {
},
methods: {
...mapActions(['Login', 'Logout']),
// handler
handleUsernameOrEmail (rule, value, callback) {
const { state } = this
const regex = /^([a-zA-Z0-9_-])+@([a-zA-Z0-9_-])+((\.[a-zA-Z0-9_-]{2,3}){1,2})$/
if (regex.test(value)) {
state.loginType = 0
} else {
state.loginType = 1
}
callback()
},
handleTabClick (key) {
this.customActiveKey = key
// this.form.resetFields()
},
handleSubmit (e) {
e.preventDefault()
const {
form: { validateFields },
state,
customActiveKey,
Login
} = this
state.loginBtn = true
const validateFieldsKey = customActiveKey === 'tab1' ? ['username', 'password'] : ['mobile', 'captcha']
validateFields(validateFieldsKey, { force: true }, (err, values) => {
if (!err) {
const loginParams = { ...values }
delete loginParams.username
loginParams[!state.loginType ? 'email' : 'username'] = values.username
loginParams.password = md5(values.password)
Login(loginParams)
.then((res) => this.loginSuccess(res))
.catch(err => this.requestFailed(err))
.finally(() => {
state.loginBtn = false
})
} else {
setTimeout(() => {
state.loginBtn = false
}, 600)
}
})
},
loginSuccess (res) {
this.$router.push({ path: this.$route.query.redirect })
// 延迟 1 秒显示欢迎信息
setTimeout(() => {
this.$notification.success({
message: '欢迎',
description: `${timeFix()},欢迎回来`
})
}, 1000)
},
requestFailed (err) {
this.$notification['error']({
message: '错误',
description: ((err.response || {}).data || {}).message || '请求出现错误,请稍后再试',
duration: 4
})
}
}
}
</script>
<style lang="less" scoped>
.user-layout-login {
label {
font-size: 14px;
}
.getCaptcha {
display: block;
width: 100%;
height: 40px;
}
.forge-password {
font-size: 14px;
}
button.login-button {
padding: 0 15px;
font-size: 16px;
height: 40px;
width: 100%;
}
.user-login-other {
text-align: left;
margin-top: 24px;
line-height: 22px;
.item-icon {
font-size: 24px;
color: rgba(0, 0, 0, 0.2);
margin-left: 16px;
vertical-align: middle;
cursor: pointer;
transition: color 0.3s;
&:hover {
color: #1890ff;
}
}
.register {
float: right;
}
}
}
</style>

View File

@@ -0,0 +1,322 @@
<template>
<div class="main user-layout-register">
<h3><span>注册</span></h3>
<a-form ref="formRegister" :form="form" id="formRegister">
<a-form-item>
<a-input
size="large"
type="text"
placeholder="邮箱"
v-decorator="['email', {rules: [{ required: true, type: 'email', message: '请输入邮箱地址' }], validateTrigger: ['change', 'blur']}]"
></a-input>
</a-form-item>
<a-popover
placement="rightTop"
:trigger="['focus']"
:getPopupContainer="(trigger) => trigger.parentElement"
v-model="state.passwordLevelChecked">
<template slot="content">
<div :style="{ width: '240px' }" >
<div :class="['user-register', passwordLevelClass]">强度<span>{{ passwordLevelName }}</span></div>
<a-progress :percent="state.percent" :showInfo="false" :strokeColor=" passwordLevelColor " />
<div style="margin-top: 10px;">
<span>请至少输入 6 个字符请不要使用容易被猜到的密码</span>
</div>
</div>
</template>
<a-form-item>
<a-input
size="large"
type="password"
@click="handlePasswordInputClick"
autocomplete="false"
placeholder="至少6位密码区分大小写"
v-decorator="['password', {rules: [{ required: true, message: '至少6位密码区分大小写'}, { validator: this.handlePasswordLevel }], validateTrigger: ['change', 'blur']}]"
></a-input>
</a-form-item>
</a-popover>
<a-form-item>
<a-input
size="large"
type="password"
autocomplete="false"
placeholder="确认密码"
v-decorator="['password2', {rules: [{ required: true, message: '至少6位密码区分大小写' }, { validator: this.handlePasswordCheck }], validateTrigger: ['change', 'blur']}]"
></a-input>
</a-form-item>
<a-form-item>
<a-input size="large" placeholder="11 位手机号" v-decorator="['mobile', {rules: [{ required: true, message: '请输入正确的手机号', pattern: /^1[3456789]\d{9}$/ }, { validator: this.handlePhoneCheck } ], validateTrigger: ['change', 'blur'] }]">
<a-select slot="addonBefore" size="large" defaultValue="+86">
<a-select-option value="+86">+86</a-select-option>
<a-select-option value="+87">+87</a-select-option>
</a-select>
</a-input>
</a-form-item>
<!--<a-input-group size="large" compact>
<a-select style="width: 20%" size="large" defaultValue="+86">
<a-select-option value="+86">+86</a-select-option>
<a-select-option value="+87">+87</a-select-option>
</a-select>
<a-input style="width: 80%" size="large" placeholder="11 位手机号"></a-input>
</a-input-group>-->
<a-row :gutter="16">
<a-col class="gutter-row" :span="16">
<a-form-item>
<a-input size="large" type="text" placeholder="验证码" v-decorator="['captcha', {rules: [{ required: true, message: '请输入验证码' }], validateTrigger: 'blur'}]">
<a-icon slot="prefix" type="mail" :style="{ color: 'rgba(0,0,0,.25)' }"/>
</a-input>
</a-form-item>
</a-col>
<a-col class="gutter-row" :span="8">
<a-button
class="getCaptcha"
size="large"
:disabled="state.smsSendBtn"
@click.stop.prevent="getCaptcha"
v-text="!state.smsSendBtn && '获取验证码'||(state.time+' s')"></a-button>
</a-col>
</a-row>
<a-form-item>
<a-button
size="large"
type="primary"
htmlType="submit"
class="register-button"
:loading="registerBtn"
@click.stop.prevent="handleSubmit"
:disabled="registerBtn">注册
</a-button>
<router-link class="login" :to="{ name: 'login' }">使用已有账户登录</router-link>
</a-form-item>
</a-form>
</div>
</template>
<script>
import { mixinDevice } from '@/utils/mixin.js'
import { getSmsCaptcha } from '@/api/login'
const levelNames = {
0: '低',
1: '低',
2: '中',
3: '强'
}
const levelClass = {
0: 'error',
1: 'error',
2: 'warning',
3: 'success'
}
const levelColor = {
0: '#ff0000',
1: '#ff0000',
2: '#ff7e05',
3: '#52c41a'
}
export default {
name: 'Register',
components: {
},
mixins: [mixinDevice],
data () {
return {
form: this.$form.createForm(this),
state: {
time: 60,
smsSendBtn: false,
passwordLevel: 0,
passwordLevelChecked: false,
percent: 10,
progressColor: '#FF0000'
},
registerBtn: false
}
},
computed: {
passwordLevelClass () {
return levelClass[this.state.passwordLevel]
},
passwordLevelName () {
return levelNames[this.state.passwordLevel]
},
passwordLevelColor () {
return levelColor[this.state.passwordLevel]
}
},
methods: {
handlePasswordLevel (rule, value, callback) {
let level = 0
// 判断这个字符串中有没有数字
if (/[0-9]/.test(value)) {
level++
}
// 判断字符串中有没有字母
if (/[a-zA-Z]/.test(value)) {
level++
}
// 判断字符串中有没有特殊符号
if (/[^0-9a-zA-Z_]/.test(value)) {
level++
}
this.state.passwordLevel = level
this.state.percent = level * 30
if (level >= 2) {
if (level >= 3) {
this.state.percent = 100
}
callback()
} else {
if (level === 0) {
this.state.percent = 10
}
callback(new Error('密码强度不够'))
}
},
handlePasswordCheck (rule, value, callback) {
const password = this.form.getFieldValue('password')
console.log('value', value)
if (value === undefined) {
callback(new Error('请输入密码'))
}
if (value && password && value.trim() !== password.trim()) {
callback(new Error('两次密码不一致'))
}
callback()
},
handlePhoneCheck (rule, value, callback) {
console.log('handlePhoneCheck, rule:', rule)
console.log('handlePhoneCheck, value', value)
console.log('handlePhoneCheck, callback', callback)
callback()
},
handlePasswordInputClick () {
if (!this.isMobile()) {
this.state.passwordLevelChecked = true
return
}
this.state.passwordLevelChecked = false
},
handleSubmit () {
const { form: { validateFields }, state, $router } = this
validateFields({ force: true }, (err, values) => {
if (!err) {
state.passwordLevelChecked = false
$router.push({ name: 'registerResult', params: { ...values } })
}
})
},
getCaptcha (e) {
e.preventDefault()
const { form: { validateFields }, state, $message, $notification } = this
validateFields(['mobile'], { force: true },
(err, values) => {
if (!err) {
state.smsSendBtn = true
const interval = window.setInterval(() => {
if (state.time-- <= 0) {
state.time = 60
state.smsSendBtn = false
window.clearInterval(interval)
}
}, 1000)
const hide = $message.loading('验证码发送中..', 0)
getSmsCaptcha({ mobile: values.mobile }).then(res => {
setTimeout(hide, 2500)
$notification['success']({
message: '提示',
description: '验证码获取成功,您的验证码为:' + res.result.captcha,
duration: 8
})
}).catch(err => {
setTimeout(hide, 1)
clearInterval(interval)
state.time = 60
state.smsSendBtn = false
this.requestFailed(err)
})
}
}
)
},
requestFailed (err) {
this.$notification['error']({
message: '错误',
description: ((err.response || {}).data || {}).message || '请求出现错误,请稍后再试',
duration: 4
})
this.registerBtn = false
}
},
watch: {
'state.passwordLevel' (val) {
console.log(val)
}
}
}
</script>
<style lang="less">
.user-register {
&.error {
color: #ff0000;
}
&.warning {
color: #ff7e05;
}
&.success {
color: #52c41a;
}
}
.user-layout-register {
.ant-input-group-addon:first-child {
background-color: #fff;
}
}
</style>
<style lang="less" scoped>
.user-layout-register {
& > h3 {
font-size: 16px;
margin-bottom: 20px;
}
.getCaptcha {
display: block;
width: 100%;
height: 40px;
}
.register-button {
width: 50%;
}
.login {
float: right;
line-height: 40px;
}
}
</style>

View File

@@ -0,0 +1,50 @@
<template>
<result
:isSuccess="true"
:content="false"
:title="email"
:description="description">
<template slot="action">
<a-button size="large" type="primary">查看邮箱</a-button>
<a-button size="large" style="margin-left: 8px" @click="goHomeHandle">返回首页</a-button>
</template>
</result>
</template>
<script>
import { Result } from '@/components'
export default {
name: 'RegisterResult',
components: {
Result
},
data () {
return {
description: '激活邮件已发送到你的邮箱中邮件有效期为24小时。请及时登录邮箱点击邮件中的链接激活帐户。',
form: {}
}
},
computed: {
email () {
const v = this.form && this.form.email || 'xxx'
const title = `你的账户:${v} 注册成功`
return title
}
},
created () {
this.form = this.$route.params
},
methods: {
goHomeHandle () {
this.$router.push({ name: 'login' })
}
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,34 @@
<template>
<page-view :title="false" :avatar="avatar">
<a-card :bordered="false" style="margin: -24px -24px 0px;">
<result type="error" :title="title" :description="description">
<template slot="action">
<a-button type="primary" @click="$router.push('/')" >欢迎访问</a-button>
</template>
</result>
</a-card>
</page-view>
</template>
<script>
import { Result } from '@/components'
import PageView from '@/layouts/PageView'
export default {
name: 'Error',
components: {
PageView,
Result
},
data () {
return {
title: '欢迎访问',
description: '欢迎使用本系统......^-^'
}
}
}
</script>
<style scoped>
</style>

View File