From 6a14339c56837b7748a4e021736584ff965199bc Mon Sep 17 00:00:00 2001 From: Pig Fang Date: Mon, 6 Aug 2018 12:14:20 +0800 Subject: [PATCH] Add "Users Management" page --- package.json | 3 +- .../assets/src/components/admin/Users.vue | 271 ++++++++++ resources/assets/src/components/route.js | 5 + resources/assets/src/js/polyfill.js | 2 + resources/assets/src/shims.d.ts | 2 +- resources/assets/tests/__mocks__/style.js | 1 + .../tests/components/admin/Users.test.js | 479 ++++++++++++++++++ resources/assets/tests/setup.js | 5 + resources/assets/tests/shims.d.ts | 12 + resources/lang/en/front-end.yml | 21 +- resources/lang/zh_CN/front-end.yml | 40 +- resources/views/admin/users.tpl | 32 +- webpack.config.js | 1 - yarn.lock | 38 +- 14 files changed, 852 insertions(+), 60 deletions(-) create mode 100644 resources/assets/src/components/admin/Users.vue create mode 100644 resources/assets/tests/__mocks__/style.js create mode 100644 resources/assets/tests/components/admin/Users.test.js create mode 100644 resources/assets/tests/shims.d.ts diff --git a/package.json b/package.json index 467958e1..8a8c78cd 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,6 @@ "bootstrap": "^3.3.7", "bootstrap-fileinput": "^4.4.7", "chart.js": "^2.7.1", - "datatables.net-bs": "^1.10.16", "es6-promise": "^4.2.4", "font-awesome": "^4.7.0", "highlight.js": "^9.12.0", @@ -37,6 +36,7 @@ "sweetalert2": "^7.26.9", "toastr": "^2.1.4", "vue": "^2.5.16", + "vue-good-table": "^2.12.2", "vuejs-paginate": "^2.0.1" }, "devDependencies": { @@ -96,6 +96,7 @@ ], "moduleNameMapper": { "^@/(.*)$": "/resources/assets/src/$1", + "\\.css$": "/resources/assets/tests/__mocks__/style.js", "\\.(png|jpg)$": "/resources/assets/tests/__mocks__/file.js" }, "setupTestFrameworkScriptFile": "/resources/assets/tests/setup.js", diff --git a/resources/assets/src/components/admin/Users.vue b/resources/assets/src/components/admin/Users.vue new file mode 100644 index 00000000..ce727bb4 --- /dev/null +++ b/resources/assets/src/components/admin/Users.vue @@ -0,0 +1,271 @@ + + + + + diff --git a/resources/assets/src/components/route.js b/resources/assets/src/components/route.js index 7c5e5326..f9789543 100644 --- a/resources/assets/src/components/route.js +++ b/resources/assets/src/components/route.js @@ -9,4 +9,9 @@ export default [ component: () => import('./user/profile'), el: '.content' }, + { + path: 'admin/users', + component: () => import('./admin/users'), + el: '.content' + }, ]; diff --git a/resources/assets/src/js/polyfill.js b/resources/assets/src/js/polyfill.js index 166d33c1..ed4d3fd0 100644 --- a/resources/assets/src/js/polyfill.js +++ b/resources/assets/src/js/polyfill.js @@ -1,3 +1,5 @@ import 'core-js/fn/array/includes'; import 'core-js/fn/array/find'; import 'es6-promise/auto'; + +Number.parseInt = parseInt; diff --git a/resources/assets/src/shims.d.ts b/resources/assets/src/shims.d.ts index bd7cf216..59834abf 100644 --- a/resources/assets/src/shims.d.ts +++ b/resources/assets/src/shims.d.ts @@ -21,7 +21,7 @@ declare module 'vue/types/vue' { $http: { get(url: string, params?: object) - post(url: string, data?: object) + post(url: string, data?: object): { errno?: number, msg?: string } } } } diff --git a/resources/assets/tests/__mocks__/style.js b/resources/assets/tests/__mocks__/style.js new file mode 100644 index 00000000..ff8b4c56 --- /dev/null +++ b/resources/assets/tests/__mocks__/style.js @@ -0,0 +1 @@ +export default {}; diff --git a/resources/assets/tests/components/admin/Users.test.js b/resources/assets/tests/components/admin/Users.test.js new file mode 100644 index 00000000..f740723e --- /dev/null +++ b/resources/assets/tests/components/admin/Users.test.js @@ -0,0 +1,479 @@ +import Vue from 'vue'; +import { mount } from '@vue/test-utils'; +import { flushPromises } from '../../utils'; +import Users from '@/components/admin/Users'; +import { swal } from '@/js/notify'; +import '@/js/i18n'; +import toastr from 'toastr'; + +jest.mock('@/js/notify'); +jest.mock('@/js/i18n', () => ({ + trans: key => key +})); + +test('fetch data after initializing', () => { + Vue.prototype.$http.get.mockResolvedValue({ data: [] }); + mount(Users); + expect(Vue.prototype.$http.get).toBeCalledWith('/admin/user-data'); +}); + +test('humanize permission', async () => { + Vue.prototype.$http.get.mockResolvedValue({ data: [ + { uid: 1, permission: -1 }, + { uid: 2, permission: 0 }, + { uid: 3, permission: 1 }, + { uid: 4, permission: 2 }, + ] }); + const wrapper = mount(Users); + await wrapper.vm.$nextTick(); + const text = wrapper.find('.vgt-table').text(); + expect(text).toContain('admin.banned'); + expect(text).toContain('admin.normal'); + expect(text).toContain('admin.admin'); + expect(text).toContain('admin.superAdmin'); +}); + +test('generate players page link', async () => { + Vue.prototype.$http.get.mockResolvedValue({ data: [ + { uid: 1, permission: 0 }, + ] }); + const wrapper = mount(Users); + await wrapper.vm.$nextTick(); + expect(wrapper.find('[data-toggle="tooltip"]').attributes()) + .toHaveProperty('href', '/admin/players?uid=1'); +}); + +test('admin option should not be displayed for super admins', async () => { + Vue.prototype.$http.get.mockResolvedValue({ data: [ + { uid: 1, permission: 2 }, + ] }); + const wrapper = mount(Users); + await wrapper.vm.$nextTick(); + const text = wrapper.find('.vgt-table').text(); + expect(text).not.toContain('admin.setAdmin'); + expect(text).not.toContain('admin.unsetAdmin'); +}); + +test('banning option should not be displayed for super admins', async () => { + Vue.prototype.$http.get.mockResolvedValue({ data: [ + { uid: 1, permission: 2 }, + ] }); + const wrapper = mount(Users); + await wrapper.vm.$nextTick(); + const text = wrapper.find('.vgt-table').text(); + expect(text).not.toContain('admin.ban'); + expect(text).not.toContain('admin.unban'); +}); + +test('admin option should be displayed for admin as super admin', async () => { + Vue.prototype.$http.get.mockResolvedValue({ data: [ + { uid: 1, permission: 1, operations: 2 }, + ] }); + const wrapper = mount(Users); + await wrapper.vm.$nextTick(); + const text = wrapper.find('.vgt-table').text(); + expect(text).toContain('admin.unsetAdmin'); + expect(text).not.toContain('admin.setAdmin'); +}); + +test('banning option should not be displayed for admin as super admin', async () => { + Vue.prototype.$http.get.mockResolvedValue({ data: [ + { uid: 1, permission: 1, operations: 2 }, + ] }); + const wrapper = mount(Users); + await wrapper.vm.$nextTick(); + const text = wrapper.find('.vgt-table').text(); + expect(text).not.toContain('admin.ban'); + expect(text).not.toContain('admin.unban'); +}); + +test('admin option should be displayed for normal users as super admin', async () => { + Vue.prototype.$http.get.mockResolvedValue({ data: [ + { uid: 1, permission: 0, operations: 2 }, + ] }); + const wrapper = mount(Users); + await wrapper.vm.$nextTick(); + const text = wrapper.find('.vgt-table').text(); + expect(text).toContain('admin.setAdmin'); + expect(text).not.toContain('admin.unsetAdmin'); +}); + +test('banning option should be displayed for normal users as super admin', async () => { + Vue.prototype.$http.get.mockResolvedValue({ data: [ + { uid: 1, permission: 0, operations: 2 }, + ] }); + const wrapper = mount(Users); + await wrapper.vm.$nextTick(); + const text = wrapper.find('.vgt-table').text(); + expect(text).toContain('admin.ban'); + expect(text).not.toContain('admin.unban'); +}); + +test('admin option should not be displayed for banned users as super admin', async () => { + Vue.prototype.$http.get.mockResolvedValue({ data: [ + { uid: 1, permission: -1, operations: 2 }, + ] }); + const wrapper = mount(Users); + await wrapper.vm.$nextTick(); + const text = wrapper.find('.vgt-table').text(); + expect(text).not.toContain('admin.setAdmin'); + expect(text).not.toContain('admin.unsetAdmin'); +}); + +test('banning option should be displayed for banned users as super admin', async () => { + Vue.prototype.$http.get.mockResolvedValue({ data: [ + { uid: 1, permission: -1, operations: 2 }, + ] }); + const wrapper = mount(Users); + await wrapper.vm.$nextTick(); + const text = wrapper.find('.vgt-table').text(); + expect(text).toContain('admin.unban'); +}); + +test('admin option should not be displayed for other admins as admin', async () => { + Vue.prototype.$http.get.mockResolvedValue({ data: [ + { uid: 1, permission: 1, operations: 1 }, + ] }); + const wrapper = mount(Users); + await wrapper.vm.$nextTick(); + const text = wrapper.find('.vgt-table').text(); + expect(text).not.toContain('admin.setAdmin'); + expect(text).not.toContain('admin.unsetAdmin'); +}); + +test('banning option should not be displayed for other admins as admin', async () => { + Vue.prototype.$http.get.mockResolvedValue({ data: [ + { uid: 1, permission: 1, operations: 1 }, + ] }); + const wrapper = mount(Users); + await wrapper.vm.$nextTick(); + const text = wrapper.find('.vgt-table').text(); + expect(text).not.toContain('admin.ban'); + expect(text).not.toContain('admin.unban'); +}); + +test('admin option should not be displayed for normal users as admin', async () => { + Vue.prototype.$http.get.mockResolvedValue({ data: [ + { uid: 1, permission: 0, operations: 1 }, + ] }); + const wrapper = mount(Users); + await wrapper.vm.$nextTick(); + const text = wrapper.find('.vgt-table').text(); + expect(text).not.toContain('admin.setAdmin'); + expect(text).not.toContain('admin.unsetAdmin'); +}); + +test('banning option should be displayed for normal users as admin', async () => { + Vue.prototype.$http.get.mockResolvedValue({ data: [ + { uid: 1, permission: 0, operations: 1 }, + ] }); + const wrapper = mount(Users); + await wrapper.vm.$nextTick(); + const text = wrapper.find('.vgt-table').text(); + expect(text).toContain('admin.ban'); + expect(text).not.toContain('admin.unban'); +}); + +test('admin option should not be displayed for banned users as admin', async () => { + Vue.prototype.$http.get.mockResolvedValue({ data: [ + { uid: 1, permission: -1, operations: 1 }, + ] }); + const wrapper = mount(Users); + await wrapper.vm.$nextTick(); + const text = wrapper.find('.vgt-table').text(); + expect(text).not.toContain('admin.setAdmin'); + expect(text).not.toContain('admin.unsetAdmin'); +}); + +test('banning option should be displayed for banned users as admin', async () => { + Vue.prototype.$http.get.mockResolvedValue({ data: [ + { uid: 1, permission: -1, operations: 1 }, + ] }); + const wrapper = mount(Users); + await wrapper.vm.$nextTick(); + const text = wrapper.find('.vgt-table').text(); + expect(text).toContain('admin.unban'); +}); + +test('deletion button should not be displayed for super admins', async () => { + Vue.prototype.$http.get.mockResolvedValue({ data: [ + { uid: 1, permission: 2 }, + ] }); + const wrapper = mount(Users); + await wrapper.vm.$nextTick(); + expect(wrapper.find('.btn-danger').attributes()).toHaveProperty('disabled', 'disabled'); +}); + +test('deletion button should be displayed for admins as super admin', async () => { + Vue.prototype.$http.get.mockResolvedValue({ data: [ + { uid: 1, permission: 1, operations: 2 }, + ] }); + const wrapper = mount(Users); + await wrapper.vm.$nextTick(); + expect(wrapper.find('.btn-danger').attributes()).not.toHaveProperty('disabled'); +}); + +test('deletion button should be displayed for normal users as super admin', async () => { + Vue.prototype.$http.get.mockResolvedValue({ data: [ + { uid: 1, permission: 0, operations: 2 }, + ] }); + const wrapper = mount(Users); + await wrapper.vm.$nextTick(); + expect(wrapper.find('.btn-danger').attributes()).not.toHaveProperty('disabled'); +}); + +test('deletion button should be displayed for banned users as super admin', async () => { + Vue.prototype.$http.get.mockResolvedValue({ data: [ + { uid: 1, permission: -1, operations: 2 }, + ] }); + const wrapper = mount(Users); + await wrapper.vm.$nextTick(); + expect(wrapper.find('.btn-danger').attributes()).not.toHaveProperty('disabled'); +}); + +test('deletion button should not be displayed for other admins as admin', async () => { + Vue.prototype.$http.get.mockResolvedValue({ data: [ + { uid: 1, permission: 1, operations: 1 }, + ] }); + const wrapper = mount(Users); + await wrapper.vm.$nextTick(); + expect(wrapper.find('.btn-danger').attributes()).toHaveProperty('disabled', 'disabled'); +}); + +test('deletion button should be displayed for normal users as admin', async () => { + Vue.prototype.$http.get.mockResolvedValue({ data: [ + { uid: 1, permission: 0, operations: 1 }, + ] }); + const wrapper = mount(Users); + await wrapper.vm.$nextTick(); + expect(wrapper.find('.btn-danger').attributes()).not.toHaveProperty('disabled'); +}); + +test('deletion button should be displayed for banned users as admin', async () => { + Vue.prototype.$http.get.mockResolvedValue({ data: [ + { uid: 1, permission: -1, operations: 1 }, + ] }); + const wrapper = mount(Users); + await wrapper.vm.$nextTick(); + expect(wrapper.find('.btn-danger').attributes()).not.toHaveProperty('disabled'); +}); + +test('change email', async () => { + Vue.prototype.$http.get.mockResolvedValue({ data: [ + { uid: 1, email: 'a@b.c' }, + ] }); + Vue.prototype.$http.post + .mockResolvedValueOnce({ errno: 1, msg: '1' }) + .mockResolvedValueOnce({ errno: 0, msg: '0' }); + swal.mockImplementationOnce(() => ({ dismiss: 1 })) + .mockImplementation(options => { + options.inputValidator(); + options.inputValidator('value'); + return { value: 'd@e.f' }; + }); + + const wrapper = mount(Users); + await wrapper.vm.$nextTick(); + const button = wrapper.find('.operations-menu > 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/users?action=email', + { uid: 1, email: 'd@e.f' } + ); + expect(wrapper.text()).toContain('a@b.c'); + + button.trigger('click'); + await flushPromises(); + expect(wrapper.text()).toContain('d@e.f'); +}); + +test('change nickname', async () => { + Vue.prototype.$http.get.mockResolvedValue({ data: [ + { uid: 1, nickname: '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('value'); + return { value: 'new' }; + }); + + const wrapper = mount(Users); + await wrapper.vm.$nextTick(); + const button = wrapper.find('.operations-menu > li:nth-child(2) > 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/users?action=nickname', + { uid: 1, nickname: 'new' } + ); + expect(wrapper.text()).toContain('old'); + + button.trigger('click'); + await flushPromises(); + expect(wrapper.text()).toContain('new'); +}); + +test('change password', async () => { + jest.spyOn(toastr, 'success'); + jest.spyOn(toastr, 'warning'); + Vue.prototype.$http.get.mockResolvedValue({ data: [ + { uid: 1 }, + ] }); + Vue.prototype.$http.post + .mockResolvedValueOnce({ errno: 0, msg: '0' }) + .mockResolvedValueOnce({ errno: 1, msg: '1' }); + swal.mockResolvedValueOnce({ dismiss: 1 }) + .mockResolvedValue({ value: 'password' }); + + const wrapper = mount(Users); + await wrapper.vm.$nextTick(); + const button = wrapper.find('.operations-menu > 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/users?action=password', + { uid: 1, password: 'password' } + ); + await flushPromises(); + expect(toastr.success).toBeCalledWith('0'); + + + button.trigger('click'); + await flushPromises(); + expect(toastr.warning).toBeCalledWith('1'); +}); + +test('change score', async () => { + Vue.prototype.$http.get.mockResolvedValue({ data: [ + { uid: 1, score: 23 }, + ] }); + Vue.prototype.$http.post + .mockResolvedValueOnce({ errno: 1, msg: '1' }) + .mockResolvedValueOnce({ errno: 0, msg: '0' }); + swal.mockResolvedValueOnce({ dismiss: 1 }) + .mockResolvedValue({ value: '45' }); + + const wrapper = mount(Users); + await wrapper.vm.$nextTick(); + const button = wrapper.find('.operations-menu > li:nth-child(4) > 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/users?action=score', + { uid: 1, score: 45 } + ); + expect(wrapper.text()).toContain('23'); + + button.trigger('click'); + await flushPromises(); + expect(wrapper.text()).toContain('45'); +}); + +test('toggle admin', async () => { + Vue.prototype.$http.get.mockResolvedValue({ data: [ + { uid: 1, permission: 0, operations: 2 }, + ] }); + Vue.prototype.$http.post + .mockResolvedValueOnce({ errno: 1, msg: '1' }) + .mockResolvedValue({ errno: 0, msg: '0' }); + + const wrapper = mount(Users); + await wrapper.vm.$nextTick(); + const button = wrapper.find('.operations-menu > li:nth-child(6) > a'); + + button.trigger('click'); + await wrapper.vm.$nextTick(); + expect(Vue.prototype.$http.post).toBeCalledWith( + '/admin/users?action=admin', + { uid: 1 } + ); + expect(wrapper.text()).toContain('admin.normal'); + + button.trigger('click'); + await flushPromises(); + expect(wrapper.text()).toContain('admin.admin'); + + button.trigger('click'); + await flushPromises(); + expect(wrapper.text()).toContain('admin.normal'); +}); + +test('toggle ban', async () => { + Vue.prototype.$http.get.mockResolvedValue({ data: [ + { uid: 1, permission: 0, operations: 2 }, + ] }); + Vue.prototype.$http.post + .mockResolvedValueOnce({ errno: 1, msg: '1' }) + .mockResolvedValue({ errno: 0, msg: '0' }); + + const wrapper = mount(Users); + await wrapper.vm.$nextTick(); + const button = wrapper.find('.operations-menu > li:nth-child(7) > a'); + + button.trigger('click'); + await wrapper.vm.$nextTick(); + expect(Vue.prototype.$http.post).toBeCalledWith( + '/admin/users?action=ban', + { uid: 1 } + ); + expect(wrapper.text()).toContain('admin.ban'); + + button.trigger('click'); + await flushPromises(); + expect(wrapper.text()).toContain('admin.banned'); + + button.trigger('click'); + await flushPromises(); + expect(wrapper.text()).toContain('admin.ban'); +}); + +test('delete user', async () => { + Vue.prototype.$http.get.mockResolvedValue({ data: [ + { uid: 1, nickname: 'to-be-deleted' }, + ] }); + Vue.prototype.$http.post + .mockResolvedValueOnce({ errno: 1, msg: '1' }) + .mockResolvedValue({ errno: 0, msg: '0' }); + swal.mockResolvedValueOnce({ dismiss: 1 }) + .mockResolvedValue({}); + + const wrapper = mount(Users); + 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/users?action=delete', + { uid: 1 } + ); + expect(wrapper.text()).toContain('to-be-deleted'); + + button.trigger('click'); + await flushPromises(); + expect(wrapper.vm.users).toHaveLength(0); +}); diff --git a/resources/assets/tests/setup.js b/resources/assets/tests/setup.js index 835f9da7..5d5fc17d 100644 --- a/resources/assets/tests/setup.js +++ b/resources/assets/tests/setup.js @@ -19,3 +19,8 @@ Vue.directive('t', (el, { value }) => { throw new Error('[i18n] Invalid arguments in `v-t` directive.'); } }); + +Vue.prototype.$http = { + get: jest.fn(), + post: jest.fn(), +}; diff --git a/resources/assets/tests/shims.d.ts b/resources/assets/tests/shims.d.ts new file mode 100644 index 00000000..18ac3d10 --- /dev/null +++ b/resources/assets/tests/shims.d.ts @@ -0,0 +1,12 @@ +import Vue from 'vue' + +declare module 'vue/types/vue' { + interface VueConstructor { + prototype: Vue & { + $http: { + get: jest.Mock + post: jest.Mock + } + } + } +} diff --git a/resources/lang/en/front-end.yml b/resources/lang/en/front-end.yml index b7e981fb..4ab1809a 100644 --- a/resources/lang/en/front-end.yml +++ b/resources/lang/en/front-end.yml @@ -129,21 +129,24 @@ user: admin: operationsTitle: Operations + status: Status + playersCount: Players Count ban: Ban unban: Unban setAdmin: Set as admin unsetAdmin: Remove admin - deleteUser: Delete User + deleteUser: Delete cannotDeleteAdmin: You can't delete admins. cannotDeleteSuperAdmin: You can't delete super admin in this way changeEmail: Edit Email changeNickName: Edit Nickname changePassword: Edit Password + changeScore: Edit Score newUserEmail: 'Please enter the new email:' newUserNickname: 'Please enter the new nickname:' newUserPassword: 'Please enter the new password:' deleteUserNotice: Are you sure to delete this user? It' permanent. - scoreTip: Press ENTER to submit new score + newScore: 'Please enter the new score:' inspectHisOwner: Click to inspect the owner of this player inspectHisPlayers: Click to inspect the players he owns banned: Banned @@ -206,3 +209,17 @@ general: rotation: Rotation pause: Pause reset: Reset + user: + email: Email + nickname: Nick Name + score: Score + register-at: Registered At + +vendor: + datatable: + search: Search + rowsPerPage: Rows per page + prev: Prev + next: Next + of: of + all: All diff --git a/resources/lang/zh_CN/front-end.yml b/resources/lang/zh_CN/front-end.yml index b1e5891a..55251e05 100644 --- a/resources/lang/zh_CN/front-end.yml +++ b/resources/lang/zh_CN/front-end.yml @@ -130,21 +130,24 @@ user: admin: operationsTitle: 更多操作 + status: 状态 + playersCount: 角色数量 ban: 封禁 unban: 解封 setAdmin: 设为管理员 unsetAdmin: 解除管理员 - deleteUser: 删除用户 + deleteUser: 删除 cannotDeleteAdmin: 你不能删除管理员账号哦 cannotDeleteSuperAdmin: 超级管理员账号不能被这样删除的啦 changeEmail: 修改邮箱 changeNickName: 修改昵称 changePassword: 更改密码 + changeScore: 更改积分 newUserEmail: 请输入新邮箱: newUserNickname: 请输入新昵称: newUserPassword: 请输入新密码: deleteUserNotice: 真的要删除此用户吗?此操作不可恢复 - scoreTip: 输入修改后的积分,回车提交 + newScore: 请输入积分值: inspectHisOwner: 点击查看该角色的所有者 inspectHisPlayers: 点击查看该用户的角色 banned: 封禁 @@ -203,6 +206,11 @@ general: rotation: 旋转 pause: 暂停 reset: 重置 + user: + email: 邮箱 + nickname: 昵称 + score: 积分 + register-at: 注册时间 vendor: fileinput: @@ -285,24 +293,10 @@ vendor: fullscreen: 全屏 borderless: 无边界模式 close: 关闭当前预览 - datatables: - sProcessing: 处理中... - sLengthMenu: 显示 _MENU_ 项结果 - sZeroRecords: 没有匹配结果 - sInfo: 显示第 _START_ 至 _END_ 项结果,共 _TOTAL_ 项 - sInfoEmpty: 显示第 0 至 0 项结果,共 0 项 - sInfoFiltered: (由 _MAX_ 项结果过滤) - sInfoPostFix: '' - sSearch: '搜索:' - sUrl: '' - sEmptyTable: 表中数据为空 - sLoadingRecords: 载入中... - sInfoThousands: ',' - oPaginate: - sFirst: 首页 - sPrevious: 上页 - sNext: 下页 - sLast: 末页 - oAria: - sSortAscending: ': 以升序排列此列' - sSortDescending: ': 以降序排列此列' + datatable: + search: 搜索 + rowsPerPage: 每页的项目数 + prev: 上一页 + next: 下一页 + of: 共 + all: 全部 diff --git a/resources/views/admin/users.tpl b/resources/views/admin/users.tpl index efdd8ce8..a152bfc4 100644 --- a/resources/views/admin/users.tpl +++ b/resources/views/admin/users.tpl @@ -14,37 +14,7 @@ -
-
-
- - - - - - - - - - - - - -
@lang('general.user.uid')@lang('general.user.email')@lang('general.user.nickname')@lang('general.user.score')@lang('admin.users.players-count.title')@lang('admin.users.status.title')@lang('general.user.register-at')@lang('general.operations')
-
-
-
+
@endsection - -@section('script') - -@endsection diff --git a/webpack.config.js b/webpack.config.js index adce63e1..da84d362 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -14,7 +14,6 @@ module.exports = [{ style: [ 'bootstrap/dist/css/bootstrap.min.css', 'admin-lte/dist/css/AdminLTE.min.css', - 'datatables.net-bs/css/dataTables.bootstrap.css', 'bootstrap-fileinput/css/fileinput.min.css', 'font-awesome/css/font-awesome.min.css', 'icheck/skins/square/blue.css', diff --git a/yarn.lock b/yarn.lock index 4f24f39e..5a3d3fa3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2519,7 +2519,7 @@ data-urls@^1.0.0: whatwg-mimetype "^2.0.0" whatwg-url "^6.4.0" -datatables.net-bs@^1.10.15, datatables.net-bs@^1.10.16: +datatables.net-bs@^1.10.15: version "1.10.16" resolved "https://registry.yarnpkg.com/datatables.net-bs/-/datatables.net-bs-1.10.16.tgz#b0854f5b374f713ae3db4156c7cea8a760c3de76" dependencies: @@ -2532,6 +2532,10 @@ datatables.net@1.10.16, datatables.net@^1.10.15: dependencies: jquery ">=1.7" +date-fns@2.0.0-alpha.7: + version "2.0.0-alpha.7" + resolved "https://registry.npmjs.org/date-fns/-/date-fns-2.0.0-alpha.7.tgz#245ad16f95764eababfb2c0a41fd5d033c20e57a" + date-now@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b" @@ -2680,6 +2684,10 @@ detective@^5.0.2: acorn "^5.2.1" defined "^1.0.0" +diacriticless@1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/diacriticless/-/diacriticless-1.0.1.tgz#e7dda978c2919609bb48aee1efc5de6a337bd4c3" + diff@^3.2.0: version "3.4.0" resolved "https://registry.yarnpkg.com/diff/-/diff-3.4.0.tgz#b1d85507daf3964828de54b37d0d73ba67dda56c" @@ -4970,10 +4978,18 @@ locate-path@^3.0.0: p-locate "^3.0.0" path-exists "^3.0.0" +lodash.assign@^4.2.0: + version "4.2.0" + resolved "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz#0d99f3ccd7a6d261d19bdaeb9245005d285808e7" + lodash.camelcase@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" +lodash.clone@^4.5.0: + version "4.5.0" + resolved "https://registry.npmjs.org/lodash.clone/-/lodash.clone-4.5.0.tgz#195870450f5a13192478df4bc3d23d2dea1907b6" + lodash.clonedeep@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" @@ -4982,6 +4998,14 @@ lodash.debounce@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" +lodash.filter@^4.6.0: + version "4.6.0" + resolved "https://registry.npmjs.org/lodash.filter/-/lodash.filter-4.6.0.tgz#668b1d4981603ae1cc5a6fa760143e480b4c4ace" + +lodash.foreach@^4.5.0: + version "4.5.0" + resolved "https://registry.npmjs.org/lodash.foreach/-/lodash.foreach-4.5.0.tgz#1a6a35eace401280c7f06dddec35165ab27e3e53" + lodash.memoize@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" @@ -7795,6 +7819,18 @@ vue-eslint-parser@^3.2.1: esquery "^1.0.1" lodash "^4.17.10" +vue-good-table@^2.12.2: + version "2.12.2" + resolved "https://registry.npmjs.org/vue-good-table/-/vue-good-table-2.12.2.tgz#74b1f9c8fed528961e72dd23f1c6ae55a8d420ba" + dependencies: + date-fns "2.0.0-alpha.7" + diacriticless "1.0.1" + lodash.assign "^4.2.0" + lodash.clone "^4.5.0" + lodash.clonedeep "^4.5.0" + lodash.filter "^4.6.0" + lodash.foreach "^4.5.0" + vue-hot-reload-api@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/vue-hot-reload-api/-/vue-hot-reload-api-2.3.0.tgz#97976142405d13d8efae154749e88c4e358cf926"