Beautify user dashboard

This commit is contained in:
Pig Fang 2019-03-26 09:44:04 +08:00
parent c168970723
commit f7c03a3fac
7 changed files with 121 additions and 52 deletions

View File

@ -19,6 +19,7 @@
"dependencies": { "dependencies": {
"@babel/runtime": "^7.3.4", "@babel/runtime": "^7.3.4",
"@fortawesome/fontawesome-free": "^5.7.2", "@fortawesome/fontawesome-free": "^5.7.2",
"@tweenjs/tween.js": "^17.3.0",
"admin-lte": "^2.4.10", "admin-lte": "^2.4.10",
"echarts": "^4.1.0", "echarts": "^4.1.0",
"element-theme-chalk": "^2.6.3", "element-theme-chalk": "^2.6.3",

View File

@ -1,2 +1,4 @@
@import '~element-theme-chalk/src/button.scss';
@import '~element-theme-chalk/src/message.scss'; @import '~element-theme-chalk/src/message.scss';
@import '~element-theme-chalk/src/message-box.scss'; @import '~element-theme-chalk/src/message-box.scss';
@import '~element-theme-chalk/src/progress.scss';

View File

@ -1,15 +1,16 @@
import Vue from 'vue' import Vue from 'vue'
import Button from 'element-ui/lib/button'
import Message from 'element-ui/lib/message' import Message from 'element-ui/lib/message'
import MessageBox from 'element-ui/lib/message-box' import MessageBox from 'element-ui/lib/message-box'
Vue.use(Button)
Vue.prototype.$message = Message Vue.prototype.$message = Message
Vue.prototype.$msgbox = MessageBox Vue.prototype.$msgbox = MessageBox
Vue.prototype.$alert = MessageBox.alert Vue.prototype.$alert = MessageBox.alert
Vue.prototype.$confirm = MessageBox.confirm Vue.prototype.$confirm = MessageBox.confirm
Vue.prototype.$prompt = MessageBox.prompt Vue.prototype.$prompt = MessageBox.prompt
window.MessageBox = MessageBox
export { export {
Message, Message,
MessageBox, MessageBox,

View File

@ -7,66 +7,85 @@
</div><!-- /.box-header --> </div><!-- /.box-header -->
<div class="box-body"> <div class="box-body">
<div class="row"> <div class="row">
<div class="col-md-8"> <div class="col-md-1" />
<div class="progress-group"> <div class="col-md-3">
<span v-t="'user.used.players'" class="progress-text" /> <div class="text-center usage-label">
<span class="progress-number"><b>{{ playersUsed }}</b> / {{ playersTotal }}</span> <b v-t="'user.used.players'" />
<div class="progress sm"> </div>
<div class="progress-bar progress-bar-aqua" :style="{ width: playersPercentage + '%' }" /> <el-progress
</div> type="circle"
</div><!-- /.progress-group --> :percentage="playersPercentage"
<div class="progress-group"> status="text"
<span v-t="'user.used.storage'" class="progress-text" /> color="#00c0ef"
<span id="user-storage" class="progress-number"> class="usage-circle"
<template v-if="storageUsed > 1024"> >
<b>{{ round(storageUsed / 1024) }}</b> / {{ round(storageTotal / 1024) }} MB <b>{{ playersUsed }}</b> / {{ playersTotal }}
</template> </el-progress>
<template v-else> </div>
<b>{{ storageUsed }}</b> / {{ storageTotal }} KB <div class="col-md-3">
</template> <div class="text-center usage-label">
</span> <b v-t="'user.used.storage'" />
</div>
<div class="progress sm"> <el-progress
<div type="circle"
id="user-storage-bar" :percentage="storagePercentage"
class="progress-bar progress-bar-yellow" status="text"
:style="{ width: storagePercentage + '%' }" color="#f39c12"
/> class="usage-circle"
</div> >
</div><!-- /.progress-group --> <template v-if="storageUsed > 1024">
</div><!-- /.col --> <b>{{ round(storageUsed / 1024) }}</b> / {{ round(storageTotal / 1024) }} MB
</template>
<template v-else>
<b>{{ storageUsed }}</b> / {{ storageTotal }} KB
</template>
</el-progress>
</div>
<div class="col-md-4"> <div class="col-md-4">
<p class="text-center"> <p class="text-center">
<strong v-t="'user.cur-score'" /> <strong v-t="'user.cur-score'" />
</p> </p>
<p id="score" data-toggle="modal" data-target="#modal-score-instruction"> <p id="score" data-toggle="modal" data-target="#modal-score-instruction">
{{ score }} {{ animatedScore }}
</p> </p>
<p v-t="'user.score-notice'" class="text-center" style="font-size: smaller; margin-top: 20px;" /> <p v-t="'user.score-notice'" class="text-center" style="font-size: smaller; margin-top: 20px;" />
</div><!-- /.col --> </div><!-- /.col -->
</div><!-- /.row --> </div><!-- /.row -->
</div><!-- ./box-body --> </div><!-- ./box-body -->
<div class="box-footer"> <div class="box-footer">
<button v-if="canSign" class="btn btn-primary pull-left" @click="sign"> <el-button
v-if="canSign"
class="btn btn-primary pull-left"
type="primary"
round
@click="sign"
>
<i class="far fa-calendar-check" aria-hidden="true" /> &nbsp;{{ $t('user.sign') }} <i class="far fa-calendar-check" aria-hidden="true" /> &nbsp;{{ $t('user.sign') }}
</button> </el-button>
<button <el-button
v-else v-else
class="btn btn-primary pull-left" class="btn btn-primary pull-left"
type="primary"
round
:title="$t('user.last-sign', { time: lastSignAt.toLocaleString() })" :title="$t('user.last-sign', { time: lastSignAt.toLocaleString() })"
disabled disabled
> >
<i class="far fa-calendar-check" aria-hidden="true" /> &nbsp; <i class="far fa-calendar-check" aria-hidden="true" /> &nbsp;
{{ remainingTimeText }} {{ remainingTimeText }}
</button> </el-button>
</div><!-- /.box-footer --> </div><!-- /.box-footer -->
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import Vue from 'vue'
import Progress from 'element-ui/lib/progress'
import Tween from '@tweenjs/tween.js'
import EmailVerification from '../../components/EmailVerification.vue' import EmailVerification from '../../components/EmailVerification.vue'
Vue.use(Progress)
const ONE_DAY = 24 * 3600 * 1000 const ONE_DAY = 24 * 3600 * 1000
export default { export default {
@ -76,13 +95,15 @@ export default {
}, },
data: () => ({ data: () => ({
score: 0, score: 0,
tweenedScore: 0,
lastSignAt: new Date(), lastSignAt: new Date(),
signAfterZero: false, signAfterZero: false,
signGap: 0, signGap: 0,
playersUsed: 0, playersUsed: 0,
playersTotal: 0, playersTotal: 1,
storageUsed: 0, storageUsed: 0,
storageTotal: 0, storageTotal: 1,
tween: null,
}), }),
computed: { computed: {
playersPercentage() { playersPercentage() {
@ -115,14 +136,31 @@ export default {
canSign() { canSign() {
return this.signRemainingTime <= 0 return this.signRemainingTime <= 0
}, },
animatedScore() {
return this.tweenedScore.toFixed(0)
},
},
watch: {
score(newValue) {
this.tween.to({ tweenedScore: newValue }, 1000).start()
},
},
created() {
this.tween = new Tween.Tween(this.$data)
}, },
beforeMount() { beforeMount() {
this.fetchScoreInfo() this.fetchScoreInfo()
}, },
mounted() {
function animate() {
requestAnimationFrame(animate)
Tween.update()
}
animate()
},
methods: { methods: {
async fetchScoreInfo() { async fetchScoreInfo() {
const data = await this.$http.get('/user/score-info') const data = await this.$http.get('/user/score-info')
this.score = data.user.score
this.lastSignAt = new Date(data.user.lastSignAt) this.lastSignAt = new Date(data.user.lastSignAt)
this.signAfterZero = data.signAfterZero this.signAfterZero = data.signAfterZero
this.signGap = data.signGapTime * 3600 * 1000 this.signGap = data.signGapTime * 3600 * 1000
@ -130,6 +168,7 @@ export default {
this.playersTotal = data.stats.players.total this.playersTotal = data.stats.players.total
this.storageUsed = data.stats.storage.used this.storageUsed = data.stats.storage.used
this.storageTotal = data.stats.storage.total this.storageTotal = data.stats.storage.total
this.score = data.user.score
}, },
round: Math.round, round: Math.round,
async sign() { async sign() {
@ -157,6 +196,9 @@ export default {
margin-top 20px margin-top 20px
cursor help cursor help
.progress .usage-circle
margin-top 4px padding-top 10px
.usage-label
padding-right 20%
</style> </style>

View File

@ -1,6 +1,7 @@
/* eslint-disable import/no-extraneous-dependencies */ /* eslint-disable import/no-extraneous-dependencies */
import 'jest-extended' import 'jest-extended'
import Vue from 'vue' import Vue from 'vue'
import { Button } from 'element-ui'
window.blessing = { window.blessing = {
base_url: '', base_url: '',
@ -35,6 +36,7 @@ Vue.prototype.$http = {
post: jest.fn(), post: jest.fn(),
} }
Vue.use(Button)
Vue.prototype.$message = { Vue.prototype.$message = {
info: jest.fn(), info: jest.fn(),
success: jest.fn(), success: jest.fn(),

View File

@ -1,8 +1,27 @@
/* eslint-disable no-mixed-operators */ /* eslint-disable no-mixed-operators */
import Vue from 'vue' import Vue from 'vue'
import { mount } from '@vue/test-utils' import { mount } from '@vue/test-utils'
import { Button } from 'element-ui'
import Dashboard from '@/views/user/Dashboard.vue' import Dashboard from '@/views/user/Dashboard.vue'
jest.mock('@tweenjs/tween.js', () => ({
Tween: class <T> {
data: T
constructor(data: T) {
this.data = data
}
to(data: Partial<T>, _: number) {
Object.assign(this.data, data)
return this
}
start() {}
},
update() {},
}))
window.blessing.extra = { unverified: false } window.blessing.extra = { unverified: false }
function scoreInfo(data = {}) { function scoreInfo(data = {}) {
@ -33,7 +52,6 @@ test('players usage', async () => {
const wrapper = mount(Dashboard) const wrapper = mount(Dashboard)
await wrapper.vm.$nextTick() await wrapper.vm.$nextTick()
expect(wrapper.text()).toContain('3 / 15') expect(wrapper.text()).toContain('3 / 15')
expect(wrapper.find('.progress-bar-aqua').attributes('style')).toBe('width: 20%;')
}) })
test('storage usage', async () => { test('storage usage', async () => {
@ -52,12 +70,10 @@ test('storage usage', async () => {
let wrapper = mount(Dashboard) let wrapper = mount(Dashboard)
await wrapper.vm.$nextTick() await wrapper.vm.$nextTick()
expect(wrapper.text()).toContain('5 / 20 KB') expect(wrapper.text()).toContain('5 / 20 KB')
expect(wrapper.find('.progress-bar-yellow').attributes('style')).toBe('width: 25%;')
wrapper = mount(Dashboard) wrapper = mount(Dashboard)
await wrapper.vm.$nextTick() await wrapper.vm.$nextTick()
expect(wrapper.text()).toContain('2 / 4 MB') expect(wrapper.text()).toContain('2 / 4 MB')
expect(wrapper.find('.progress-bar-yellow').attributes('style')).toBe('width: 50%;')
}) })
test('display score', async () => { test('display score', async () => {
@ -79,19 +95,19 @@ test('button `sign` state', async () => {
let wrapper = mount(Dashboard) let wrapper = mount(Dashboard)
await wrapper.vm.$nextTick() await wrapper.vm.$nextTick()
expect(wrapper.find('button').attributes('disabled')).toBeNil() expect(wrapper.find(Button).attributes('disabled')).toBeNil()
wrapper = mount(Dashboard) wrapper = mount(Dashboard)
await wrapper.vm.$nextTick() await wrapper.vm.$nextTick()
expect(wrapper.find('button').attributes('disabled')).toBe('disabled') expect(wrapper.find(Button).attributes('disabled')).toBe('disabled')
wrapper = mount(Dashboard) wrapper = mount(Dashboard)
await wrapper.vm.$nextTick() await wrapper.vm.$nextTick()
expect(wrapper.find('button').attributes('disabled')).toBeNil() expect(wrapper.find(Button).attributes('disabled')).toBeNil()
wrapper = mount(Dashboard) wrapper = mount(Dashboard)
await wrapper.vm.$nextTick() await wrapper.vm.$nextTick()
expect(wrapper.find('button').attributes('disabled')).toBe('disabled') expect(wrapper.find(Button).attributes('disabled')).toBe('disabled')
}) })
test('remaining time', async () => { test('remaining time', async () => {
@ -108,13 +124,13 @@ test('remaining time', async () => {
let wrapper = mount(Dashboard) let wrapper = mount(Dashboard)
await wrapper.vm.$nextTick() await wrapper.vm.$nextTick()
expect(wrapper.find('button').text()).toMatch(/(29)|(30)/) expect(wrapper.find(Button).text()).toMatch(/(29)|(30)/)
expect(wrapper.find('button').text()).toContain('min') expect(wrapper.find(Button).text()).toContain('min')
wrapper = mount(Dashboard) wrapper = mount(Dashboard)
await wrapper.vm.$nextTick() await wrapper.vm.$nextTick()
expect(wrapper.find('button').text()).toContain('23') expect(wrapper.find(Button).text()).toContain('23')
expect(wrapper.find('button').text()).toContain('hour') expect(wrapper.find(Button).text()).toContain('hour')
Vue.prototype.$t = origin Vue.prototype.$t = origin
}) })
@ -131,7 +147,7 @@ test('sign', async () => {
storage: { used: 3, total: 4 }, storage: { used: 3, total: 4 },
}) })
const wrapper = mount(Dashboard) const wrapper = mount(Dashboard)
const button = wrapper.find('button') const button = wrapper.find(Button)
await wrapper.vm.$nextTick() await wrapper.vm.$nextTick()
button.trigger('click') button.trigger('click')

View File

@ -902,6 +902,11 @@
"@types/istanbul-lib-coverage" "^1.1.0" "@types/istanbul-lib-coverage" "^1.1.0"
"@types/yargs" "^12.0.9" "@types/yargs" "^12.0.9"
"@tweenjs/tween.js@^17.3.0":
version "17.3.0"
resolved "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-17.3.0.tgz#1cbea14b4b5baa292274c96766fd1b2a56b7ca9f"
integrity sha512-SPkhNj9/wGfbdX2C3B3KhttLQ4iesd+Ny8Dv1RnqF1MFUIqsZz/OJVLzJEHSEl7zheNx70dvqrwfbCFDQ0sWBw==
"@types/anymatch@*": "@types/anymatch@*":
version "1.3.1" version "1.3.1"
resolved "https://registry.npmjs.org/@types/anymatch/-/anymatch-1.3.1.tgz#336badc1beecb9dacc38bea2cf32adf627a8421a" resolved "https://registry.npmjs.org/@types/anymatch/-/anymatch-1.3.1.tgz#336badc1beecb9dacc38bea2cf32adf627a8421a"