diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index 4eb8ff9b..13963e6b 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -29,6 +29,23 @@ class UserController extends Controller ]); } + public function scoreInfo() + { + $user = Auth::user(); + return [ + 'user' => [ + 'score' => $user->score, + 'lastSignAt' => $user->last_sign_at, + ], + 'stats' => [ + 'players' => $this->calculatePercentageUsed($user->players->count(), option('score_per_player')), + 'storage' => $this->calculatePercentageUsed($user->getStorageUsed(), option('score_per_storage')) + ], + 'signAfterZero' => option('sign_after_zero'), + 'signGapTime' => option('sign_gap_time') + ]; + } + /** * Calculate percentage of resources used by user. * diff --git a/resources/assets/src/components/route.js b/resources/assets/src/components/route.js index f9789543..5977b9d5 100644 --- a/resources/assets/src/components/route.js +++ b/resources/assets/src/components/route.js @@ -1,4 +1,9 @@ export default [ + { + path: 'user', + component: () => import('./user/dashboard'), + el: '#usage-box' + }, { path: 'user/closet', component: () => import('./user/closet'), diff --git a/resources/assets/src/components/user/Dashboard.vue b/resources/assets/src/components/user/Dashboard.vue new file mode 100644 index 00000000..7168ea8c --- /dev/null +++ b/resources/assets/src/components/user/Dashboard.vue @@ -0,0 +1,158 @@ + + + + + diff --git a/resources/assets/src/stylus/user.styl b/resources/assets/src/stylus/user.styl index 5b3eac7e..add7e706 100644 --- a/resources/assets/src/stylus/user.styl +++ b/resources/assets/src/stylus/user.styl @@ -56,14 +56,3 @@ margin-bottom: 10px; } -#score { - font-family: Minecraft; - font-size: 50px; - text-align: center; - margin-top: 20px; - cursor: help; -} - -.progress { - margin-top: 4px; -} diff --git a/resources/assets/tests/components/user/Dashboard.test.js b/resources/assets/tests/components/user/Dashboard.test.js new file mode 100644 index 00000000..5538da41 --- /dev/null +++ b/resources/assets/tests/components/user/Dashboard.test.js @@ -0,0 +1,142 @@ +import { promisify } from 'util'; +import Vue from 'vue'; +import { mount } from '@vue/test-utils'; +import Dashboard from '@/components/user/Dashboard'; +import toastr from 'toastr'; +import { swal } from '@/js/notify'; + +jest.mock('@/js/notify'); + +function scoreInfo(data = {}) { + return { + user: { score: 835, lastSignAt: '2018-08-07 16:06:49' }, + stats: { + players: { used: 3, total: 15, percentage: 20 }, + storage: { used: 5, total: 20, percentage: 25 } + }, + signAfterZero: false, + signGapTime: '24', + ...data + }; +} + +test('fetch score info', () => { + Vue.prototype.$http.get.mockResolvedValue(scoreInfo()); + mount(Dashboard); + expect(Vue.prototype.$http.get).toBeCalledWith('/user/score-info'); +}); + +test('players usage', async () => { + Vue.prototype.$http.get.mockResolvedValue(scoreInfo()); + const wrapper = mount(Dashboard); + await wrapper.vm.$nextTick(); + expect(wrapper.text()).toContain('3 / 15'); + expect(wrapper.find('.progress-bar-aqua').attributes().style).toBe('width: 20%;'); +}); + +test('storage usage', async () => { + Vue.prototype.$http.get + .mockResolvedValueOnce(scoreInfo()) + .mockResolvedValueOnce(scoreInfo({ + stats: { + players: { used: 3, total: 15, percentage: 20 }, + storage: { used: 2048, total: 4096, percentage: 50 } + } + })); + let wrapper = mount(Dashboard); + await wrapper.vm.$nextTick(); + expect(wrapper.text()).toContain('5 / 20 KB'); + expect(wrapper.find('.progress-bar-yellow').attributes().style).toBe('width: 25%;'); + + wrapper = mount(Dashboard); + await wrapper.vm.$nextTick(); + expect(wrapper.text()).toContain('2 / 4 MB'); + expect(wrapper.find('.progress-bar-yellow').attributes().style).toBe('width: 50%;'); +}); + +test('display score', async () => { + Vue.prototype.$http.get.mockResolvedValue(scoreInfo()); + const wrapper = mount(Dashboard); + await wrapper.vm.$nextTick(); + expect(wrapper.find('#score').text()).toContain('835'); +}); + +test('button `sign` state', async () => { + Vue.prototype.$http.get + .mockResolvedValueOnce(scoreInfo({ signAfterZero: true })) + .mockResolvedValueOnce(scoreInfo({ + signAfterZero: true, + user: { lastSignAt: Date.now() } + })) + .mockResolvedValueOnce(scoreInfo({ user: { lastSignAt: Date.now() - 25 * 3600 * 1000 } })) + .mockResolvedValueOnce(scoreInfo({ user: { lastSignAt: Date.now() } })); + + let wrapper = mount(Dashboard); + await wrapper.vm.$nextTick(); + expect(wrapper.find('button').attributes()).not.toHaveProperty('disabled'); + + wrapper = mount(Dashboard); + await wrapper.vm.$nextTick(); + expect(wrapper.find('button').attributes()).toHaveProperty('disabled', 'disabled'); + + wrapper = mount(Dashboard); + await wrapper.vm.$nextTick(); + expect(wrapper.find('button').attributes()).not.toHaveProperty('disabled'); + + wrapper = mount(Dashboard); + await wrapper.vm.$nextTick(); + expect(wrapper.find('button').attributes()).toHaveProperty('disabled', 'disabled'); +}); + +test('remaining time', async () => { + const origin = Vue.prototype.$t; + Vue.prototype.$t = (key, args) => key + JSON.stringify(args); + + Vue.prototype.$http.get + .mockResolvedValueOnce(scoreInfo({ + user: { lastSignAt: Date.now() - 23.5 * 3600 * 1000 } + })) + .mockResolvedValueOnce(scoreInfo({ + user: { lastSignAt: Date.now() } + })); + + let wrapper = mount(Dashboard); + await wrapper.vm.$nextTick(); + expect(wrapper.find('button').text()).toContain('29'); + expect(wrapper.find('button').text()).toContain('min'); + + wrapper = mount(Dashboard); + await wrapper.vm.$nextTick(); + expect(wrapper.find('button').text()).toContain('23'); + expect(wrapper.find('button').text()).toContain('hour'); + + Vue.prototype.$t = origin; +}); + +test('sign', async () => { + jest.spyOn(toastr, 'warning'); + swal.mockResolvedValue(); + Vue.prototype.$http.get.mockResolvedValue(scoreInfo({ + user: { lastSignAt: Date.now() - 30 * 3600 * 1000 } + })); + Vue.prototype.$http.post + .mockResolvedValueOnce({ errno: 1, msg: '1' }) + .mockResolvedValueOnce({ + errno: 0, + score: 233, + storage: { used: 3, total: 4 } + }); + const wrapper = mount(Dashboard); + const button = wrapper.find('button'); + await wrapper.vm.$nextTick(); + + button.trigger('click'); + await wrapper.vm.$nextTick(); + expect(Vue.prototype.$http.post).toBeCalledWith('/user/sign'); + expect(toastr.warning).toBeCalledWith('1'); + + button.trigger('click'); + await wrapper.vm.$nextTick(); + expect(button.attributes()).toHaveProperty('disabled', 'disabled'); + expect(wrapper.text()).toContain('3 / 4 KB'); +}); diff --git a/resources/lang/en/front-end.yml b/resources/lang/en/front-end.yml index 4ab1809a..73e0a7c5 100644 --- a/resources/lang/en/front-end.yml +++ b/resources/lang/en/front-end.yml @@ -126,6 +126,18 @@ user: This is permanent! No backups, no restores, no magic undo button. We warned you, ok? password: Current Password + used: + title: Resources Used + players: Registered players + storage: Storage used + cur-score: Current Score + score-notice: Click the score to show introduction. + sign: Sign + sign-success: Signed successfully. You got :score scores. + time-unit-hour: h + time-unit-min: min + last-sign: Last signed at :time + sign-remain-time: Available after :time :unit admin: operationsTitle: Operations diff --git a/resources/lang/zh_CN/front-end.yml b/resources/lang/zh_CN/front-end.yml index 55251e05..5459cd68 100644 --- a/resources/lang/zh_CN/front-end.yml +++ b/resources/lang/zh_CN/front-end.yml @@ -127,6 +127,18 @@ user: 我们不提供任何备份,或者神奇的撤销按钮。 我们警告过你了,确定要这样做吗? password: 当前密码 + used: + title: 使用情况 + players: 角色数量 + storage: 存储空间 + cur-score: 当前积分 + score-notice: 点击积分查看说明 + sign: 签到 + sign-success: 签到成功,获得了 :score 积分~ + time-unit-hour: 小时 + time-unit-min: 分钟 + last-sign: 上次签到于 :time + sign-remain-time: :time :unit后可签到 admin: operationsTitle: 更多操作 diff --git a/resources/views/user/index.tpl b/resources/views/user/index.tpl index b31aed2f..a0954a79 100644 --- a/resources/views/user/index.tpl +++ b/resources/views/user/index.tpl @@ -15,79 +15,9 @@
- -
- -
-
-
-
-

@lang('user.used.title')

-
-
-
-
-
- @lang('user.used.players') - {{ $statistics['players']['used'] }}/ {{ $statistics['players']['total'] }} -
-
-
-
-
- @lang('user.used.storage') - - @php - $used = $statistics['storage']['used']; - $total = $statistics['storage']['total']; - @endphp - - - @if ($used > 1024) - {{ round($used / 1024, 1) }}/ {{ is_string($total) ? $total : round($total / 1024, 1) }} MB - @else - {{ $used }}/ {{ $total }} KB - @endif - - -
-
-
-
-
-
-

- @lang('user.cur-score') -

-

- {{ $user->getScore() }} -

-

@lang('user.score-notice')

-
-
-
- -
+
diff --git a/routes/web.php b/routes/web.php index ba02ed2a..a7caaf4a 100644 --- a/routes/web.php +++ b/routes/web.php @@ -45,6 +45,7 @@ Route::group(['prefix' => 'auth'], function () Route::group(['middleware' => ['web', 'auth'], 'prefix' => 'user'], function () { Route::any ('', 'UserController@index'); + Route::get ('/score-info', 'UserController@scoreInfo'); Route::post('/sign', 'UserController@sign'); // Profile diff --git a/tests/UserControllerTest.php b/tests/UserControllerTest.php index 1fab32ad..28c301d4 100644 --- a/tests/UserControllerTest.php +++ b/tests/UserControllerTest.php @@ -15,17 +15,43 @@ class UserControllerTest extends TestCase $user = factory(User::class)->create(); factory(\App\Models\Player::class)->create(['uid' => $user->uid]); - $players_count = option('score_per_player') / option('user_initial_score'); $this->actAs($user) ->get('/user') ->assertViewHas('user') ->assertViewHas('statistics') - ->assertSee((string) (1 / $players_count * 100)) // Players - ->assertSee('0') // Storage ->assertSee((new Parsedown())->text(option_localized('announcement'))) ->assertSee((string) $user->score); } + public function testScoreInfo() + { + $user = factory(User::class)->create(); + factory(\App\Models\Player::class)->create(['uid' => $user->uid]); + + $this->actingAs($user) + ->get('/user/score-info') + ->assertJson([ + 'user' => [ + 'score' => $user->score, + 'lastSignAt' => $user->last_sign_at + ], + 'stats' => [ + 'players' => [ + 'used' => 1, + 'total' => 11, + 'percentage' => 1 / 11 * 100 + ], + 'storage' => [ + 'used' => 0, + 'total' => $user->score, + 'percentage' => 0 + ] + ], + 'signAfterZero' => option('sign_after_zero'), + 'signGapTime' => option('sign_gap_time') + ]); + } + public function testSign() { option(['sign_score' => '50,50']);