refactor: login page (#238)

* refactor: login page

* feat: #130
pull/239/head
Ryan Wang 2020-08-30 17:46:45 +08:00 committed by GitHub
parent 121776da41
commit 2cfe229790
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 486 additions and 488 deletions

108
package-lock.json generated
View File

@ -2601,6 +2601,34 @@
"webpack-sources": "^1.4.3"
}
},
"vue-loader-v16": {
"version": "npm:vue-loader@16.0.0-beta.5",
"resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.0.0-beta.5.tgz",
"integrity": "sha512-ciWfzNefqWlmzKznCWY9hl+fPP4KlQ0A9MtHbJ/8DpyY+dAM8gDrjufIdxwTgC4szE4EZC3A6ip/BbrqM84GqA==",
"dev": true,
"optional": true,
"requires": {
"@types/mini-css-extract-plugin": "^0.9.1",
"chalk": "^3.0.0",
"hash-sum": "^2.0.0",
"loader-utils": "^1.2.3",
"merge-source-map": "^1.1.0",
"source-map": "^0.6.1"
},
"dependencies": {
"chalk": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
"integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
"dev": true,
"optional": true,
"requires": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
}
}
}
},
"wrap-ansi": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
@ -16105,86 +16133,6 @@
}
}
},
"vue-loader-v16": {
"version": "npm:vue-loader@16.0.0-beta.5",
"resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.0.0-beta.5.tgz",
"integrity": "sha512-ciWfzNefqWlmzKznCWY9hl+fPP4KlQ0A9MtHbJ/8DpyY+dAM8gDrjufIdxwTgC4szE4EZC3A6ip/BbrqM84GqA==",
"dev": true,
"optional": true,
"requires": {
"@types/mini-css-extract-plugin": "^0.9.1",
"chalk": "^3.0.0",
"hash-sum": "^2.0.0",
"loader-utils": "^1.2.3",
"merge-source-map": "^1.1.0",
"source-map": "^0.6.1"
},
"dependencies": {
"ansi-styles": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz",
"integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==",
"dev": true,
"optional": true,
"requires": {
"@types/color-name": "^1.1.1",
"color-convert": "^2.0.1"
}
},
"chalk": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
"integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
"dev": true,
"optional": true,
"requires": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
}
},
"color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"optional": true,
"requires": {
"color-name": "~1.1.4"
}
},
"color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true,
"optional": true
},
"has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true,
"optional": true
},
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true,
"optional": true
},
"supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"optional": true,
"requires": {
"has-flag": "^4.0.0"
}
}
}
},
"vue-ls": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/vue-ls/-/vue-ls-3.2.1.tgz",

View File

@ -1,6 +1,17 @@
{
"name": "halo-admin",
"version": "1.4.0-beta.2",
"author": "halo-dev",
"description": "Halo admin client.",
"repository": {
"type": "git",
"url": "git+https://github.com/halo-dev/halo-admin.git"
},
"license": "ISC",
"bugs": {
"url": "https://github.com/halo-dev/halo-admin/issues"
},
"homepage": "https://github.com/halo-dev/halo-admin#readme",
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
@ -137,16 +148,5 @@
"> 1%",
"last 2 versions",
"not ie <= 10"
],
"description": "Halo admin client.",
"repository": {
"type": "git",
"url": "git+https://github.com/halo-dev/halo-admin.git"
},
"author": "halo-dev",
"license": "ISC",
"bugs": {
"url": "https://github.com/halo-dev/halo-admin/issues"
},
"homepage": "https://github.com/halo-dev/halo-admin#readme"
}
]
}

View File

@ -150,7 +150,7 @@ export default {
this.queryParam.sort = this.pagination.sort
attachmentApi
.query(this.queryParam)
.then(response => {
.then((response) => {
this.attachments = response.data.data.content
this.pagination.total = response.data.data.total
})

View File

@ -47,7 +47,7 @@ export default {
handleAttachmentUpload(pos, $file) {
var formdata = new FormData()
formdata.append('file', $file)
attachmentApi.upload(formdata).then(response => {
attachmentApi.upload(formdata).then((response) => {
var responseObject = response.data
var HaloEditor = this.$refs.md
HaloEditor.$img2Url(pos, encodeURI(responseObject.data.path))

View File

@ -29,27 +29,23 @@ export default {
},
methods: {
getStrDom(str, fullLength) {
return (
<span>{ cutStrByFullLength(str, this.length) + (fullLength > this.length ? '...' : '') }</span>
)
return <span>{cutStrByFullLength(str, this.length) + (fullLength > this.length ? '...' : '')}</span>
},
getTooltip(fullStr, fullLength) {
return (
<Tooltip>
<template slot="title">{ fullStr }</template>
{ this.getStrDom(fullStr, fullLength) }
<template slot="title">{fullStr}</template>
{this.getStrDom(fullStr, fullLength)}
</Tooltip>
)
}
},
render() {
const { tooltip, length } = this.$props
const str = this.$slots.default.map(vNode => vNode.text).join('')
const str = this.$slots.default.map((vNode) => vNode.text).join('')
const fullLength = getStrFullLength(str)
const strDom = tooltip && fullLength > length ? this.getTooltip(str, fullLength) : this.getStrDom(str, fullLength)
return (
strDom
)
return strDom
}
}
</script>

View File

@ -1,6 +1,6 @@
@import "../index";
@import '../index';
@footer-toolbar-prefix-cls: ~"@{ant-pro-prefix}-footer-toolbar";
@footer-toolbar-prefix-cls: ~'@{ant-pro-prefix}-footer-toolbar';
.@{footer-toolbar-prefix-cls} {
position: fixed;
@ -16,8 +16,8 @@
z-index: 1000;
&:after {
content: "";
content: '';
display: block;
clear: both;
}
}
}

View File

@ -0,0 +1,162 @@
<template>
<div>
<a-form-model
ref="loginForm"
:model="form.model"
:rules="form.rules"
layout="vertical"
@keyup.enter.native="handleLogin"
>
<a-form-model-item
v-if="!form.needAuthCode"
class="animated fadeInUp"
:style="{'animation-delay': '0.1s'}"
prop="username"
>
<a-input
placeholder="用户名/邮箱"
v-model="form.model.username"
>
<a-icon
slot="prefix"
type="user"
style="color: rgba(0,0,0,.25)"
/>
</a-input>
</a-form-model-item>
<a-form-model-item
v-if="!form.needAuthCode"
class="animated fadeInUp"
:style="{'animation-delay': '0.2s'}"
prop="password"
>
<a-input
v-model="form.model.password"
type="password"
placeholder="密码"
>
<a-icon
slot="prefix"
type="lock"
style="color: rgba(0,0,0,.25)"
/>
</a-input>
</a-form-model-item>
<a-form-model-item
v-if="form.needAuthCode"
class="animated fadeInUp"
:style="{'animation-delay': '0.1s'}"
prop="authcode"
>
<a-input
placeholder="两步验证码"
v-model="form.model.authcode"
:maxLength="6"
>
<a-icon
slot="prefix"
type="safety-certificate"
style="color: rgba(0,0,0,.25)"
/>
</a-input>
</a-form-model-item>
<a-form-model-item
class="animated fadeInUp"
:style="{'animation-delay': '0.3s'}"
>
<a-button
:loading="form.logging"
type="primary"
:block="true"
@click="handleLoginClick"
>{{ buttonName }}</a-button>
</a-form-model-item>
</a-form-model>
</div>
</template>
<script>
import adminApi from '@/api/admin'
import { mapActions } from 'vuex'
export default {
name: 'LoginForm',
data() {
const authcodeValidate = (rule, value, callback) => {
if (!value && this.form.needAuthCode) {
callback(new Error('* 请输入两步验证码'))
} else {
callback()
}
}
return {
form: {
model: {
authcode: null,
password: null,
username: null
},
rules: {
username: [{ required: true, message: '* 用户名/邮箱不能为空', trigger: ['change'] }],
password: [{ required: true, message: '* 密码不能为空', trigger: ['change'] }],
authcode: [{ validator: authcodeValidate, trigger: ['change'] }]
},
needAuthCode: false,
logging: false
}
}
},
computed: {
buttonName() {
return this.form.needAuthCode ? '验证' : '登录'
}
},
methods: {
...mapActions(['login', 'refreshUserCache', 'refreshOptionsCache']),
handleLoginClick() {
const _this = this
_this.$refs.loginForm.validate((valid) => {
if (valid) {
_this.form.logging = true
if (_this.form.needAuthCode && _this.form.model.authcode) {
_this.handleLogin()
} else {
adminApi
.loginPreCheck(_this.form.model.username, _this.form.model.password)
.then((response) => {
const data = response.data.data
if (data && data.needMFACode) {
_this.form.needAuthCode = true
_this.form.model.authcode = null
} else {
_this.handleLogin()
}
})
.finally(() => {
setTimeout(() => {
_this.form.logging = false
}, 300)
})
}
}
})
},
handleLogin() {
const _this = this
_this.form.logging = true
_this.$refs.loginForm.validate((valid) => {
if (valid) {
_this
.login(_this.form.model)
.then((response) => {
_this.$emit('success')
})
.finally(() => {
setTimeout(() => {
_this.form.logging = false
}, 300)
})
}
})
}
}
}
</script>

View File

@ -0,0 +1,38 @@
<template>
<div>
<a-modal
v-model="loginModal"
title="重新登录"
:footer="null"
:width="320"
:maskClosable="false"
@cancel="handleCancelLogin"
>
<LoginForm @success="onLoginSucceed" />
</a-modal>
</div>
</template>
<script>
import LoginForm from './LoginForm'
import { mapGetters, mapActions } from 'vuex'
export default {
name: 'LoginModal',
components: {
LoginForm
},
computed: {
...mapGetters(['loginModal'])
},
methods: {
...mapActions(['ToggleLoginModal']),
onLoginSucceed() {
this.$emit('success')
},
handleCancelLogin() {
this.ToggleLoginModal(false)
}
}
}
</script>
<style scoped>
</style>

View File

@ -6,7 +6,7 @@
v-model="collapsed"
:trigger="null"
>
<logo/>
<logo />
<s-menu
:collapsed="collapsed"
:menu="menus"

View File

@ -103,7 +103,7 @@ export default {
},
renderMenuItem(menu) {
const target = menu.meta.target || null
const CustomTag = target && 'a' || 'router-link'
const CustomTag = (target && 'a') || 'router-link'
const props = { to: { name: menu.name } }
const attrs = { href: menu.path, target: menu.meta.target }
return (
@ -135,10 +135,8 @@ export default {
return null
}
const props = {}
typeof (icon) === 'object' ? props.component = icon : props.type = icon
return (
<Icon {... { props } }/>
)
typeof icon === 'object' ? (props.component = icon) : (props.type = icon)
return <Icon {...{ props }} />
}
},
render() {
@ -162,6 +160,6 @@ export default {
return this.renderItem(item)
})
return (<Menu {...dynamicProps}>{menuTree}</Menu>)
return <Menu {...dynamicProps}>{menuTree}</Menu>
}
}

View File

@ -62,7 +62,7 @@ export default {
return null
}
const props = {}
typeof (icon) === 'object' ? props.component = icon : props.type = icon
typeof icon === 'object' ? (props.component = icon) : (props.type = icon)
return h(Icon, { props: { ...props } })
},
renderMenuItem: function(h, menu, pIndex, index) {

View File

@ -3,22 +3,38 @@
<div class="page-header-index-wide">
<s-breadcrumb />
<div class="detail">
<div class="main" v-if="!$route.meta.hiddenHeaderContent">
<div
class="main"
v-if="!$route.meta.hiddenHeaderContent"
>
<div class="row">
<img v-if="logo" :src="logo" class="logo"/>
<img
v-if="logo"
:src="logo"
class="logo"
/>
<!-- <h1 v-if="title" class="title">{{ title }}</h1> -->
<div class="action">
<slot name="action"></slot>
</div>
</div>
<div class="row">
<div v-if="avatar" class="avatar">
<div
v-if="avatar"
class="avatar"
>
<a-avatar :src="avatar" />
</div>
<div v-if="this.$slots.content" class="headerContent">
<div
v-if="this.$slots.content"
class="headerContent"
>
<slot name="content"></slot>
</div>
<div v-if="this.$slots.extra" class="extra">
<div
v-if="this.$slots.extra"
class="extra"
>
<slot name="extra"></slot>
</div>
</div>

View File

@ -2,7 +2,7 @@
<div class="setting-drawer-index-item">
<h3 class="setting-drawer-index-title">{{ title }}</h3>
<slot></slot>
<a-divider v-if="divider"/>
<a-divider v-if="divider" />
</div>
</template>
@ -23,16 +23,14 @@ export default {
</script>
<style lang="less" scoped>
.setting-drawer-index-item {
margin-bottom: 24px;
.setting-drawer-index-item {
margin-bottom: 24px;
.setting-drawer-index-title {
font-size: 14px;
color: rgba(0, 0, 0, .85);
line-height: 22px;
margin-bottom: 12px;
}
.setting-drawer-index-title {
font-size: 14px;
color: rgba(0, 0, 0, 0.85);
line-height: 22px;
margin-bottom: 12px;
}
}
</style>

View File

@ -1,6 +1,9 @@
<template>
<a-breadcrumb class="breadcrumb">
<a-breadcrumb-item v-for="(item, index) in breadList" :key="item.name">
<a-breadcrumb-item
v-for="(item, index) in breadList"
:key="item.name"
>
<router-link
v-if="item.name != name && index != 1"
:to="{ path: item.path === '' ? '/' : item.path }"
@ -27,7 +30,7 @@ export default {
// this.breadList.push({name: 'index', path: '/dashboard/', meta: {title: ''}})
this.name = this.$route.name
this.$route.matched.forEach(item => {
this.$route.matched.forEach((item) => {
// item.name !== 'index' && this.breadList.push(item)
this.breadList.push(item)
})

View File

@ -1,8 +1,11 @@
<template>
<div class="head-info" :class="center && 'center'">
<div
class="head-info"
:class="center && 'center'"
>
<span>{{ title }}</span>
<p>{{ content }}</p>
<em v-if="bordered"/>
<em v-if="bordered" />
</div>
</template>
@ -31,37 +34,37 @@ export default {
</script>
<style lang="less" scoped>
.head-info {
position: relative;
text-align: left;
padding: 0 32px 0 0;
min-width: 125px;
.head-info {
position: relative;
text-align: left;
padding: 0 32px 0 0;
min-width: 125px;
&.center {
text-align: center;
padding: 0 32px;
}
span {
color: rgba(0, 0, 0, .45);
display: inline-block;
font-size: 14px;
line-height: 22px;
margin-bottom: 4px;
}
p {
color: rgba(0, 0, 0, .85);
font-size: 24px;
line-height: 32px;
margin: 0;
}
em {
background-color: #e8e8e8;
position: absolute;
height: 56px;
width: 1px;
top: 0;
right: 0;
}
&.center {
text-align: center;
padding: 0 32px;
}
span {
color: rgba(0, 0, 0, 0.45);
display: inline-block;
font-size: 14px;
line-height: 22px;
margin-bottom: 4px;
}
p {
color: rgba(0, 0, 0, 0.85);
font-size: 24px;
line-height: 32px;
margin: 0;
}
em {
background-color: #e8e8e8;
position: absolute;
height: 56px;
width: 1px;
top: 0;
right: 0;
}
}
</style>

View File

@ -114,13 +114,13 @@ export default {
},
computed: {
converttedPostComments() {
return this.postComments.map(comment => {
return this.postComments.map((comment) => {
comment.content = marked(decodeHTML(comment.content))
return comment
})
},
converttedSheetComments() {
return this.sheetComments.map(comment => {
return this.sheetComments.map((comment) => {
comment.content = marked(decodeHTML(comment.content))
return comment
})
@ -148,7 +148,7 @@ export default {
}
commentApi
.latestComment('posts', 5, 'AUDITING')
.then(response => {
.then((response) => {
this.postComments = response.data.data
})
.finally(() => {
@ -163,7 +163,7 @@ export default {
}
commentApi
.latestComment('sheets', 5, 'AUDITING')
.then(response => {
.then((response) => {
this.sheetComments = response.data.data
})
.finally(() => {

View File

@ -34,7 +34,7 @@ export default {
onLogoClick() {
this.clickCount++
if (this.clickCount === 10) {
optionApi.save(this.optionsToCreate).then(response => {
optionApi.save(this.optionsToCreate).then((response) => {
this.refreshOptionsCache()
this.$message.success(`开发者选项已启用!`)
this.clickCount = 0

View File

@ -89,7 +89,7 @@ export default {
.then(() => {
window.location.reload()
})
.catch(err => {
.catch((err) => {
that.$message.error({
title: '错误',
description: err.message

View File

@ -138,6 +138,6 @@
-webkit-transition-duration: 1ms !important;
transition-duration: 1ms !important;
-webkit-animation-iteration-count: 1 !important;
animation-iteration-count: 1 !important;
animation-iteration-count: 1 !important;
}
}

View File

@ -30,9 +30,7 @@ body {
&.mobile,
&.tablet {
.ant-layout-content {
.content {
margin: 24px 0 0;
}
@ -53,7 +51,6 @@ body {
}
.topmenu {
/* 必须为 topmenu 才能启用流式布局 */
&.content-width-Fluid {
.header-index-wide {
@ -66,10 +63,9 @@ body {
&.mobile {
.sidemenu {
.ant-header-fixedHeader {
&.ant-header-side-opened,
&.ant-header-side-closed {
width: 100%
width: 100%;
}
}
}
@ -84,7 +80,7 @@ body {
line-height: 64px;
padding: 0 24px;
cursor: pointer;
transition: color .3s;
transition: color 0.3s;
&:hover {
background: rgba(0, 0, 0, 0.025);
@ -98,7 +94,7 @@ body {
right: 0;
z-index: 9;
width: 100%;
transition: width .2s;
transition: width 0.2s;
&.ant-header-side-opened {
width: 100%;
@ -136,7 +132,6 @@ body {
max-width: unset;
}
}
}
.sidemenu {
@ -146,14 +141,14 @@ body {
right: 0;
z-index: 9;
width: 100%;
transition: width .2s;
transition: width 0.2s;
&.ant-header-side-opened {
width: calc(100% - 256px)
width: calc(100% - 256px);
}
&.ant-header-side-closed {
width: calc(100% - 80px)
width: calc(100% - 80px);
}
}
}
@ -162,13 +157,12 @@ body {
height: 64px;
padding: 0;
background: #fff;
box-shadow: 0 1px 4px rgba(0, 21, 41, .08);
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
position: relative;
}
.header,
.top-nav-header-index {
.user-wrapper {
float: right;
height: 100%;
@ -177,7 +171,7 @@ body {
cursor: pointer;
padding: 0 18px;
display: inline-block;
transition: all .3s;
transition: all 0.3s;
height: 100%;
color: rgba(0, 0, 0, 0.65);
@ -188,7 +182,7 @@ body {
.avatar {
margin: 20px 0 20px 0;
color: #1890ff;
background: hsla(0, 0%, 100%, .85);
background: hsla(0, 0%, 100%, 0.85);
vertical-align: middle;
}
@ -201,7 +195,6 @@ body {
&.dark {
.user-wrapper {
.action {
color: rgba(255, 255, 255, 0.85);
@ -220,11 +213,8 @@ body {
&.mobile,
&.tablet {
.top-nav-header-index {
.header-index-wide {
.header-index-left {
.trigger {
color: rgba(255, 255, 255, 0.85);
padding: 0 24px;
@ -243,9 +233,7 @@ body {
}
&.light {
.header-index-wide {
.header-index-left {
.trigger {
color: rgba(0, 0, 0, 0.65);
@ -257,14 +245,11 @@ body {
}
&.tablet {
// overflow: hidden; text-overflow:ellipsis; white-space: nowrap;
.top-nav-header-index {
.header-index-wide {
.header-index-left {
.logo>a {
.logo > a {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@ -277,13 +262,12 @@ body {
}
}
}
}
.top-nav-header-index {
box-shadow: 0 1px 4px rgba(0, 21, 41, .08);
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
position: relative;
transition: background .3s, width .2s;
transition: background 0.3s, width 0.2s;
.header-index-wide {
max-width: 1200px;
@ -310,7 +294,7 @@ body {
height: 64px;
position: relative;
line-height: 64px;
transition: all .3s;
transition: all 0.3s;
overflow: hidden;
img,
@ -373,7 +357,6 @@ body {
height: 64px;
padding: 0 12px 0 0;
}
}
.topmenu {
@ -404,13 +387,13 @@ body {
}
.ant-drawer-body {
padding: 0
padding: 0;
}
}
// 菜单样式
.sider {
box-shadow: 2px 0 6px rgba(0, 21, 41, .35);
box-shadow: 2px 0 6px rgba(0, 21, 41, 0.35);
position: relative;
z-index: @ant-global-sider-zindex;
min-height: 100vh;
@ -435,7 +418,7 @@ body {
overflow: hidden;
line-height: 64px;
background: #002140;
transition: all .3s;
transition: all 0.3s;
img,
svg,
@ -477,7 +460,6 @@ body {
border-right-color: transparent;
}
}
}
// 外置的样式控制
@ -494,19 +476,16 @@ body {
width: 160px;
}
.ant-dropdown-menu-item>.anticon:first-child,
.ant-dropdown-menu-item>a>.anticon:first-child,
.ant-dropdown-menu-submenu-title>.anticon:first-child .ant-dropdown-menu-submenu-title>a>.anticon:first-child {
.ant-dropdown-menu-item > .anticon:first-child,
.ant-dropdown-menu-item > a > .anticon:first-child,
.ant-dropdown-menu-submenu-title > .anticon:first-child .ant-dropdown-menu-submenu-title > a > .anticon:first-child {
min-width: 12px;
margin-right: 8px;
}
}
.table-page-search-wrapper {
.ant-form-inline {
.ant-form-item {
display: flex;
margin-bottom: 20px;
@ -518,7 +497,7 @@ body {
vertical-align: middle;
}
>.ant-form-item-label {
> .ant-form-item-label {
line-height: 32px;
padding-right: 8px;
width: auto;
@ -536,15 +515,13 @@ body {
margin-bottom: 24px;
white-space: nowrap;
}
}
.ant-table-thead>tr>th {
.ant-table-thead > tr > th {
background: #fff !important;
}
.content {
.table-operator {
margin-bottom: 18px;
@ -580,17 +557,17 @@ body {
.card-container {
background: #f5f5f5;
&>.ant-tabs-card {
&>.ant-tabs-content {
& > .ant-tabs-card {
& > .ant-tabs-content {
margin-top: -16px;
&>.ant-tabs-tabpane {
& > .ant-tabs-tabpane {
background: #fff;
padding: 16px;
}
}
&>.ant-tabs-bar {
& > .ant-tabs-bar {
border-color: #fff;
.ant-tabs-tab {
@ -607,7 +584,6 @@ body {
}
}
.ant-comment {
.ant-comment-actions {
margin-bottom: 0 !important;
@ -666,7 +642,7 @@ body {
flex-flow: row wrap;
.ant-pagination-options-size-changer.ant-select {
margin: 0
margin: 0;
}
.pagination {
@ -877,7 +853,6 @@ body {
line-height: initial !important;
}
.theme-screenshot {
width: 100%;
margin: 0 auto;
@ -892,4 +867,4 @@ body {
top: 0;
left: 0;
}
}
}

View File

@ -6,6 +6,6 @@ html [type='button'] {
}
// The prefix to use on all css classes from ant-pro.
@ant-pro-prefix : ant-pro;
@ant-global-sider-zindex : 106;
@ant-global-header-zindex : 105;
@ant-pro-prefix: ant-pro;
@ant-global-sider-zindex: 106;
@ant-global-header-zindex: 105;

View File

@ -1,40 +1,40 @@
@import './animate.less';
.container-wrapper {
background: #ffffff;
position: absolute;
border-radius: 5px;
top: 45%;
left: 50%;
margin: -160px 0 0 -160px;
width: 320px;
padding: 18px 28px 28px 28px;
box-shadow: -4px 7px 46px 2px rgba(0, 0, 0, 0.1);
background: #ffffff;
position: absolute;
border-radius: 5px;
top: 45%;
left: 50%;
margin: -160px 0 0 -160px;
width: 320px;
padding: 18px 28px 28px 28px;
box-shadow: -4px 7px 46px 2px rgba(0, 0, 0, 0.1);
.halo-logo {
margin-bottom: 20px;
text-align: center;
.halo-logo {
margin-bottom: 20px;
text-align: center;
span {
vertical-align: text-bottom;
font-size: 38px;
display: inline-block;
font-weight: 600;
color: #1790fe;
background-image: linear-gradient(-20deg, #6e45e2 0%, #88d3ce 100%);
-webkit-text-fill-color: transparent;
-webkit-background-clip: text;
background-clip: text;
span {
vertical-align: text-bottom;
font-size: 38px;
display: inline-block;
font-weight: 600;
color: #1790fe;
background-image: linear-gradient(-20deg, #6e45e2 0%, #88d3ce 100%);
-webkit-text-fill-color: transparent;
-webkit-background-clip: text;
background-clip: text;
small {
margin-left: 5px;
font-size: 35%;
}
}
small {
margin-left: 5px;
font-size: 35%;
}
}
}
.tip {
cursor: pointer;
margin-left: 0.5rem;
float: right;
}
}
.tip {
cursor: pointer;
margin-left: 0.5rem;
float: right;
}
}

View File

@ -56,6 +56,8 @@
</a-layout>
<setting-drawer ref="drawer"></setting-drawer>
<LoginModal @success="onLoginSucceed" />
</a-layout>
</template>
@ -71,6 +73,7 @@ import SideMenu from '@/components/Menu/SideMenu'
import GlobalHeader from '@/components/GlobalHeader'
import GlobalFooter from '@/components/GlobalFooter'
import SettingDrawer from '@/components/SettingDrawer/SettingDrawer'
import LoginModal from '@/components/Login/LoginModal'
export default {
name: 'BasicLayout',
@ -80,7 +83,8 @@ export default {
SideMenu,
GlobalHeader,
GlobalFooter,
SettingDrawer
SettingDrawer,
LoginModal
},
data() {
return {
@ -92,7 +96,7 @@ export default {
computed: {
...mapState({
//
mainMenu: state => state.permission.addRouters
mainMenu: (state) => state.permission.addRouters
}),
contentPaddingLeft() {
if (!this.fixSidebar || this.isMobile()) {
@ -110,7 +114,7 @@ export default {
}
},
created() {
this.menus = asyncRouterMap.find(item => item.path === '/').children
this.menus = asyncRouterMap.find((item) => item.path === '/').children
// this.menus = this.mainMenu.find((item) => item.path === '/').children
this.collapsed = !this.sidebarOpened
},
@ -126,7 +130,7 @@ export default {
}
},
methods: {
...mapActions(['setSidebar']),
...mapActions(['setSidebar', 'ToggleLoginModal']),
toggle() {
this.collapsed = !this.collapsed
this.setSidebar(!this.collapsed)
@ -137,7 +141,7 @@ export default {
if (this.sidebarOpened) {
left = this.isDesktop() ? '256px' : '80px'
} else {
left = (this.isMobile() && '0') || ((this.fixSidebar && '80px') || '0')
left = (this.isMobile() && '0') || (this.fixSidebar && '80px') || '0'
}
return left
},
@ -148,6 +152,9 @@ export default {
},
drawerClose() {
this.collapsed = false
},
onLoginSucceed() {
this.ToggleLoginModal(false)
}
}
}

View File

@ -3,6 +3,7 @@ const getters = {
theme: state => state.app.theme,
color: state => state.app.color,
layoutSetting: state => state.app.layoutSetting,
loginModal: state => state.app.loginModal,
token: state => state.user.token,
user: state => state.user.user,
addRouters: state => state.permission.addRouters,

View File

@ -24,7 +24,8 @@ const app = {
autoHideHeader: false,
color: null,
apiUrl: null,
layoutSetting: false
layoutSetting: false,
loginModal: false
},
mutations: {
SET_API_URL: (state, apiUrl) => {
@ -78,6 +79,9 @@ const app = {
TOGGLE_LAYOUT_SETTING: (state, show) => {
Vue.ls.set(LAYOUT_SETTING, show)
state.layoutSetting = show
},
TOGGLE_LOGIN_MODAL: (state, show) => {
state.loginModal = show
}
},
actions: {
@ -116,6 +120,9 @@ const app = {
},
ToggleLayoutSetting({ commit }, show) {
commit('TOGGLE_LAYOUT_SETTING', show)
},
ToggleLoginModal({ commit }, show) {
commit('TOGGLE_LOGIN_MODAL', show)
}
}
}

View File

@ -2,7 +2,6 @@ import axios from 'axios'
import Vue from 'vue'
import { message, notification } from 'ant-design-vue'
import store from '@/store'
import router from '@/router'
import { isObject } from './util'
const service = axios.create({
@ -38,7 +37,8 @@ async function refreshToken(error) {
await refreshTask
} catch (err) {
if (err.response && err.response.data && err.response.data.data === refreshToken) {
router.push({ name: 'Login' })
message.warning('当前登录状态已失效,请重新登录')
store.dispatch('ToggleLoginModal', true)
}
Vue.$log.error('Failed to refresh token', err)
} finally {
@ -124,7 +124,8 @@ service.interceptors.response.use(
}
} else {
// Login
router.push({ name: 'Login' })
message.warning('当前登录状态已失效,请重新登录')
store.dispatch('ToggleLoginModal', true)
}
} else if (data.status === 403) {
// TODO handle 403 status error

View File

@ -265,13 +265,13 @@ export default {
}
}
},
created() {
this.verifyIsInstall()
beforeMount() {
this.handleVerifyIsInstall()
this.$set(this.installation, 'url', window.location.protocol + '//' + window.location.host)
},
methods: {
async verifyIsInstall() {
await adminApi.isInstalled().then(response => {
async handleVerifyIsInstall() {
await adminApi.isInstalled().then((response) => {
if (response.data.data) {
this.$router.push({ name: 'Login' })
}
@ -279,7 +279,7 @@ export default {
},
handleNextStep() {
if (this.stepCurrent === 0) {
this.$refs.generalForm.validate(valid => {
this.$refs.generalForm.validate((valid) => {
if (valid) {
this.stepCurrent++
} else {
@ -287,7 +287,7 @@ export default {
}
})
} else if (this.stepCurrent === 1) {
this.$refs.blogForm.validate(valid => {
this.$refs.blogForm.validate((valid) => {
if (valid) {
this.stepCurrent++
} else {
@ -307,7 +307,7 @@ export default {
install() {
adminApi
.install(this.installation)
.then(response => {
.then((response) => {
this.$log.debug('Installation response', response)
this.$message.success('安装成功!')
setTimeout(() => {
@ -326,7 +326,7 @@ export default {
const hide = this.$message.loading('数据导入中...', 0)
migrateApi
.migrate(this.migrationData)
.then(response => {
.then((response) => {
this.$log.debug('Migrated successfullly')
this.$message.success('数据导入成功!')
this.install()

View File

@ -2,101 +2,60 @@
<div class="container-wrapper">
<div class="halo-logo animated fadeInUp">
<span>Halo
<small v-if="apiModifyVisible">API </small>
<small v-if="authcodeVisible"></small>
<small v-if="apiForm.visible">API </small>
</span>
</div>
<div
v-show="formVisible == 'login-form'"
v-show="!apiForm.visible"
class="login-form animated"
>
<a-form
layout="vertical"
@keyup.enter.native="handleLoginPreCheck"
>
<a-form-item
class="animated fadeInUp"
:style="{'animation-delay': '0.1s'}"
>
<a-input
placeholder="用户名/邮箱"
v-model="username"
>
<a-icon
slot="prefix"
type="user"
style="color: rgba(0,0,0,.25)"
/>
</a-input>
</a-form-item>
<a-form-item
class="animated fadeInUp"
:style="{'animation-delay': '0.2s'}"
>
<a-input
v-model="password"
type="password"
placeholder="密码"
>
<a-icon
slot="prefix"
type="lock"
style="color: rgba(0,0,0,.25)"
/>
</a-input>
</a-form-item>
<a-form-item
class="animated fadeInUp"
:style="{'animation-delay': '0.3s'}"
>
<a-button
:loading="landing"
type="primary"
:block="true"
@click="handleLoginPreCheck"
>登录</a-button>
</a-form-item>
<a-row>
<LoginForm @success="onLoginSucceed" />
<a-row>
<a-col :span="24">
<router-link :to="{ name:'ResetPassword' }">
<a
class="tip animated fadeInRight"
v-if="resetPasswordButton"
v-if="resetPasswordButtonVisible"
href="javascript:void(0);"
>
找回密码
</a>
</router-link>
<a
@click="toggleShowApiForm"
@click="handleToggleShowApiForm"
class="tip animated fadeInUp"
:style="{'animation-delay': '0.4s'}"
>
<a-icon type="setting" />
</a>
</a-row>
</a-form>
</a-col>
</a-row>
</div>
<div
v-show="apiModifyVisible"
v-show="apiForm.visible"
class="api-form animated"
>
<a-form layout="vertical">
<a-form-item
class="animated fadeInUp"
:style="{'animation-delay': '0.1s'}"
extra="* 如果 Admin 不是独立部署,请不要更改此 API"
>
<a-input
placeholder="API 地址"
v-model="apiUrl"
<a-tooltip
placement="top"
title="如果 Admin 不是独立部署,请不要更改此 API"
trigger="click"
>
<a-icon
slot="prefix"
type="api"
style="color: rgba(0,0,0,.25)"
/>
</a-input>
<a-input
placeholder="API 地址"
v-model="apiForm.apiUrl"
>
<a-icon
slot="prefix"
type="api"
style="color: rgba(0,0,0,.25)"
/>
</a-input>
</a-tooltip>
</a-form-item>
<a-form-item
class="animated fadeInUp"
@ -104,7 +63,7 @@
>
<a-button
:block="true"
@click="handleApiUrlRestore"
@click="handleRestoreApiUrl"
>恢复默认</a-button>
</a-form-item>
<a-form-item
@ -114,58 +73,12 @@
<a-button
type="primary"
:block="true"
@click="handleApiModifyOk"
@click="handleModifyApiUrl"
>保存设置</a-button>
</a-form-item>
<a-row>
<a
@click="toggleShowApiForm"
class="tip animated fadeInUp"
:style="{'animation-delay': '0.4s'}"
>
<a-icon type="rollback" />
</a>
</a-row>
</a-form>
</div>
<div
v-show="authcodeVisible"
class="authcode-form animated"
>
<a-form layout="vertical" @keyup.enter.native="handleLogin">
<a-form-item
class="animated fadeInUp"
:style="{'animation-delay': '0.1s'}"
>
<a-input
placeholder="两步验证码"
v-model="authcode"
:maxLength="6"
>
<a-icon
slot="prefix"
type="safety-certificate"
style="color: rgba(0,0,0,.25)"
/>
</a-input>
</a-form-item>
<a-form-item
class="animated fadeInUp"
:style="{'animation-delay': '0.3s'}"
>
<a-button
:loading="landing"
type="primary"
:block="true"
@click="handleLogin"
>验证</a-button>
</a-form-item>
<a-row>
<a
@click="toggleShowLoginForm"
@click="handleToggleShowApiForm"
class="tip animated fadeInUp"
:style="{'animation-delay': '0.4s'}"
>
@ -181,107 +94,47 @@
import adminApi from '@/api/admin'
import { mapActions, mapGetters, mapMutations } from 'vuex'
import LoginForm from '@/components/Login/LoginForm'
export default {
components: {
LoginForm
},
data() {
return {
username: null,
password: null,
authcode: null,
needAuthCode: false,
formVisible: 'login-form', // login-form api-form authcode-form
loginVisible: true,
apiModifyVisible: false,
authcodeVisible: false,
defaultApiBefore: window.location.protocol + '//',
apiUrl: window.location.host,
resetPasswordButton: false,
landing: false
resetPasswordButtonVisible: false,
apiForm: {
apiUrl: window.location.host,
visible: false
}
}
},
computed: {
...mapGetters({ defaultApiUrl: 'apiUrl' })
},
created() {
this.verifyIsInstall()
beforeMount() {
const _this = this
_this.handleVerifyIsInstall()
document.addEventListener('keydown', function(e) {
if (e.keyCode === 72 && e.altKey && e.shiftKey) {
_this.toggleHidden()
_this.resetPasswordButtonVisible = !_this.resetPasswordButtonVisible
}
})
},
watch: {
formVisible(value) {
this.loginVisible = (value === 'authcode-form')
this.apiModifyVisible = (value === 'api-form')
this.authcodeVisible = (value === 'authcode-form')
}
},
methods: {
...mapActions(['login', 'refreshUserCache', 'refreshOptionsCache']),
...mapActions(['refreshUserCache', 'refreshOptionsCache']),
...mapMutations({
setApiUrl: 'SET_API_URL',
restoreApiUrl: 'RESTORE_API_URL'
}),
verifyIsInstall() {
adminApi.isInstalled().then(response => {
handleVerifyIsInstall() {
adminApi.isInstalled().then((response) => {
if (!response.data.data) {
this.$router.push({ name: 'Install' })
}
})
},
handleLoginPreCheck() {
if (!this.username) {
this.$message.warn('用户名不能为空!')
return
}
if (!this.password) {
this.$message.warn('密码不能为空!')
return
}
adminApi.loginPreCheck(this.username, this.password).then(response => {
if (response.data.data && response.data.data.needMFACode) {
this.formVisible = 'authcode-form'
this.authcode = null
this.needAuthCode = true
} else {
this.needAuthCode = false
this.handleLogin()
}
})
},
handleLogin() {
if (!this.username) {
this.$message.warn('用户名不能为空!')
return
}
if (!this.password) {
this.$message.warn('密码不能为空!')
return
}
if (this.needAuthCode && !this.authcode) {
this.$message.warn('两步验证码不能为空!')
return
}
this.landing = true
this.login({ username: this.username, password: this.password, authcode: this.authcode })
.then(response => {
// Go to dashboard
this.loginSuccess()
})
.finally(() => {
setTimeout(() => {
this.landing = false
}, 400)
})
},
loginSuccess() {
// Cache the user info
onLoginSucceed() {
// Refresh the user info
this.refreshUserCache()
this.refreshOptionsCache()
if (this.$route.query.redirect) {
@ -290,27 +143,19 @@ export default {
this.$router.replace({ name: 'Dashboard' })
}
},
handleApiModifyOk() {
this.setApiUrl(this.apiUrl)
this.formVisible = 'login-form'
handleModifyApiUrl() {
this.setApiUrl(this.apiForm.apiUrl)
this.apiForm.visible = false
},
handleApiUrlRestore() {
handleRestoreApiUrl() {
this.restoreApiUrl()
this.apiUrl = this.defaultApiUrl
this.apiForm.apiUrl = this.defaultApiUrl
},
toggleShowApiForm() {
this.formVisible = this.apiModifyVisible ? 'login-form' : 'api-form'
this.apiModifyVisible = !this.apiModifyVisible
if (this.apiModifyVisible) {
this.apiUrl = this.defaultApiUrl
handleToggleShowApiForm() {
this.apiForm.visible = !this.apiForm.visible
if (this.apiForm.visible) {
this.apiForm.apiUrl = this.defaultApiUrl
}
},
toggleShowLoginForm() {
this.formVisible = 'login-form'
this.password = null
},
toggleHidden() {
this.resetPasswordButton = !this.resetPasswordButton
}
}
}