diff --git a/resources/assets/src/components/admin/Players.vue b/resources/assets/src/components/admin/Players.vue new file mode 100644 index 00000000..57c2e0ac --- /dev/null +++ b/resources/assets/src/components/admin/Players.vue @@ -0,0 +1,229 @@ + + + + + + {{ props.formattedRow[props.column.field] }} + + + + + + + + + + + + + + + {{ $t('admin.changeTexture') }} + + Steve + Alex + + + + + {{ $t('general.more') }} + + + + + + + + + + + + + + + diff --git a/resources/assets/src/components/route.js b/resources/assets/src/components/route.js index 5977b9d5..b1725f61 100644 --- a/resources/assets/src/components/route.js +++ b/resources/assets/src/components/route.js @@ -19,4 +19,9 @@ export default [ component: () => import('./admin/users'), el: '.content' }, + { + path: 'admin/players', + component: () => import('./admin/players'), + el: '.content' + }, ]; diff --git a/resources/assets/tests/components/admin/Players.test.js b/resources/assets/tests/components/admin/Players.test.js new file mode 100644 index 00000000..13a6530e --- /dev/null +++ b/resources/assets/tests/components/admin/Players.test.js @@ -0,0 +1,161 @@ +import Vue from 'vue'; +import { mount } from '@vue/test-utils'; +import { flushPromises } from '../../utils'; +import Players from '@/components/admin/Players'; +import { swal } from '@/js/notify'; + +jest.mock('@/js/notify'); + +test('fetch data after initializing', () => { + Vue.prototype.$http.get.mockResolvedValue({ data: [] }); + mount(Players); + expect(Vue.prototype.$http.get).toBeCalledWith('/admin/player-data'); +}); + +test('change texture', async () => { + Vue.prototype.$http.get.mockResolvedValue({ data: [ + { pid: 1, tid_steve: 0 } + ] }); + Vue.prototype.$http.post + .mockResolvedValueOnce({ errno: 1, msg: '1' }) + .mockResolvedValueOnce({ errno: 0, msg: '0' }); + swal.mockResolvedValueOnce({ dismiss: 1 }) + .mockResolvedValue({ value: 5 }); + + const wrapper = mount(Players); + await wrapper.vm.$nextTick(); + const button = wrapper.find('[data-test="change-texture"] > li:nth-child(1) > a'); + + button.trigger('click'); + expect(Vue.prototype.$http.post).not.toBeCalled(); + + button.trigger('click'); + await wrapper.vm.$nextTick(); + expect(Vue.prototype.$http.post).toBeCalledWith( + '/admin/players?action=texture', + { pid: 1, model: 'steve', tid: 5 } + ); + + button.trigger('click'); + await flushPromises(); + expect(wrapper.text()).toContain('5'); +}); + +test('change player name', async () => { + Vue.prototype.$http.get.mockResolvedValue({ data: [ + { pid: 1, player_name: 'old' } + ] }); + Vue.prototype.$http.post + .mockResolvedValueOnce({ errno: 1, msg: '1' }) + .mockResolvedValueOnce({ errno: 0, msg: '0' }); + swal.mockImplementationOnce(() => ({ dismiss: 1 })) + .mockImplementation(options => { + options.inputValidator(); + options.inputValidator('new'); + return { value: 'new' }; + }); + + const wrapper = mount(Players); + await wrapper.vm.$nextTick(); + const button = wrapper.find('[data-test="operations"] > li:nth-child(1) > a'); + + button.trigger('click'); + expect(Vue.prototype.$http.post).not.toBeCalled(); + + button.trigger('click'); + await wrapper.vm.$nextTick(); + expect(Vue.prototype.$http.post).toBeCalledWith( + '/admin/players?action=name', + { pid: 1, name: 'new' } + ); + + button.trigger('click'); + await flushPromises(); + expect(wrapper.text()).toContain('new'); +}); + +test('toggle preference', async () => { + Vue.prototype.$http.get.mockResolvedValue({ data: [ + { pid: 1, preference: 'default' } + ] }); + Vue.prototype.$http.post + .mockResolvedValueOnce({ errno: 1, msg: '1' }) + .mockResolvedValue({ errno: 0, msg: '0' }); + + const wrapper = mount(Players); + await wrapper.vm.$nextTick(); + const button = wrapper.find('[data-test="operations"] > li:nth-child(2) > a'); + + button.trigger('click'); + expect(Vue.prototype.$http.post).toBeCalledWith( + '/admin/players?action=preference', + { pid: 1, preference: 'slim' } + ); + + button.trigger('click'); + await flushPromises(); + expect(wrapper.text()).toContain('slim'); + + button.trigger('click'); + await flushPromises(); + expect(wrapper.text()).toContain('default'); +}); + +test('change owner', async () => { + Vue.prototype.$http.get.mockResolvedValue({ data: [ + { pid: 1, uid: 2 } + ] }); + Vue.prototype.$http.post + .mockResolvedValueOnce({ errno: 1, msg: '1' }) + .mockResolvedValueOnce({ errno: 0, msg: '0' }); + swal.mockResolvedValueOnce({ dismiss: 1 }) + .mockResolvedValue({ value: '3' }); + + const wrapper = mount(Players); + await wrapper.vm.$nextTick(); + const button = wrapper.find('[data-test="operations"] > li:nth-child(3) > a'); + + button.trigger('click'); + expect(Vue.prototype.$http.post).not.toBeCalled(); + + button.trigger('click'); + await wrapper.vm.$nextTick(); + expect(Vue.prototype.$http.post).toBeCalledWith( + '/admin/players?action=owner', + { pid: 1, uid: '3' } + ); + + button.trigger('click'); + await flushPromises(); + expect(wrapper.text()).toContain('3'); +}); + +test('delete player', async () => { + Vue.prototype.$http.get.mockResolvedValue({ data: [ + { pid: 1, player_name: 'to-be-deleted' } + ] }); + Vue.prototype.$http.post + .mockResolvedValueOnce({ errno: 1, msg: '1' }) + .mockResolvedValueOnce({ errno: 0, msg: '0' }); + swal.mockResolvedValueOnce({ dismiss: 1 }) + .mockResolvedValue({}); + + const wrapper = mount(Players); + await wrapper.vm.$nextTick(); + const button = wrapper.find('.btn-danger'); + + button.trigger('click'); + expect(Vue.prototype.$http.post).not.toBeCalled(); + + button.trigger('click'); + await wrapper.vm.$nextTick(); + expect(Vue.prototype.$http.post).toBeCalledWith( + '/admin/players?action=delete', + { pid: 1 } + ); + expect(wrapper.text()).toContain('to-be-deleted'); + + button.trigger('click'); + await flushPromises(); + expect(wrapper.vm.players).toHaveLength(0); +}); diff --git a/resources/lang/en/front-end.yml b/resources/lang/en/front-end.yml index 83168047..cfef95bc 100644 --- a/resources/lang/en/front-end.yml +++ b/resources/lang/en/front-end.yml @@ -165,16 +165,13 @@ admin: normal: Normal admin: Admin superAdmin: Super Admin - textureType: Texture Type - skin: 'Skin (:model Model)' - cape: Cape - pid: Texture ID pidNotice: >- - Please enter the tid of texture. Inputting 0 can clear texture of this + Please enter the tid of texture. Inputing 0 can clear texture of this player. changePlayerTexture: 'Change textures of :player' changeTexture: Change Textures changePlayerName: Change Player Name + changePreference: Toggle Preference changeOwner: Change Owner deletePlayer: Delete changePlayerOwner: 'Please enter the id of user which this player should be transferred to:' @@ -226,6 +223,12 @@ general: nickname: Nick Name score: Score register-at: Registered At + player: + owner: Owner + player-name: Player Name + preference: Preference + previews: Texture Previews + last-modified: Last Modified vendor: datatable: diff --git a/resources/lang/zh_CN/front-end.yml b/resources/lang/zh_CN/front-end.yml index cfb71693..578d0c6f 100644 --- a/resources/lang/zh_CN/front-end.yml +++ b/resources/lang/zh_CN/front-end.yml @@ -166,16 +166,13 @@ admin: normal: 普通用户 admin: 管理员 superAdmin: 超级管理员 - textureType: 材质类型 - skin: '皮肤(:model 模型)' - cape: 披风 - pid: 材质 ID pidNotice: 输入要更换的材质的 TID,输入 0 即可清除该角色的材质 changePlayerTexture: '更换角色 :player 的材质' changeTexture: 更换材质 changePlayerName: 更改角色名 + changePreference: 切换模型 changeOwner: 更换角色拥有者 - deletePlayer: 删除角色 + deletePlayer: 删除 changePlayerOwner: 请输入此角色要让渡至的用户 UID: deletePlayerNotice: 真的要删除此角色吗?此操作不可恢复 targetUser: '目标用户::nickname' @@ -223,6 +220,12 @@ general: nickname: 昵称 score: 积分 register-at: 注册时间 + player: + owner: 拥有者 + player-name: 角色名 + preference: 优先模型 + previews: 预览材质 + last-modified: 修改时间 vendor: fileinput: