Add players management page

This commit is contained in:
Pig Fang 2018-08-09 15:34:21 +08:00
parent 365f38c781
commit 887fcbdc90
5 changed files with 411 additions and 10 deletions

View File

@ -0,0 +1,229 @@
<template>
<section class="content">
<vue-good-table
:rows="players"
:columns="columns"
:search-options="tableOptions.search"
:pagination-options="tableOptions.pagination"
styleClass="vgt-table striped"
>
<template slot="table-row" slot-scope="props">
<span v-if="props.column.field === 'uid'">
<a
:href="`${baseUrl}/admin/users?uid=${props.row.uid}`"
:title="$t('admin.inspectHisOwner')"
data-toggle="tooltip"
data-placement="right"
>{{ props.formattedRow[props.column.field] }}</a>
</span>
<span v-else-if="props.column.field === 'preview'">
<a
v-if="props.row.tid_steve"
:href="`${baseUrl}/skinlib/show/${props.row.tid_steve}`"
>
<img :src="`${baseUrl}/preview/64/${props.row.tid_steve}.png`" width="64">
</a>
<a
v-if="props.row.tid_alex"
:href="`${baseUrl}/skinlib/show/${props.row.tid_alex}`"
>
<img :src="`${baseUrl}/preview/64/${props.row.tid_alex}.png`" width="64">
</a>
<a
v-if="props.row.tid_cape"
:href="`${baseUrl}/skinlib/show/${props.row.tid_cape}`"
>
<img :src="`${baseUrl}/preview/64/${props.row.tid_cape}.png`" width="64">
</a>
</span>
<span v-else-if="props.column.field === 'operations'">
<div class="btn-group">
<button
class="btn btn-default dropdown-toggle"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"
>{{ $t('admin.changeTexture') }} <span class="caret"></span></button>
<ul class="dropdown-menu" data-test="change-texture">
<li><a @click="changeTexture(props.row, 'steve')" href="#steve">Steve</a></li>
<li><a @click="changeTexture(props.row, 'alex')" href="#alex">Alex</a></li>
<li><a @click="changeTexture(props.row, 'cape')" v-t="'general.cape'" href="#cape"></a></li>
</ul>
</div>
<div class="btn-group">
<button
class="btn btn-default dropdown-toggle"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"
>{{ $t('general.more') }} <span class="caret"></span></button>
<ul class="dropdown-menu" data-test="operations">
<li><a @click="changeName(props.row)" v-t="'admin.changePlayerName'" href="#"></a></li>
<li><a @click="togglePreference(props.row)" v-t="'admin.changePreference'" href="#"></a></li>
<li><a @click="changeOwner(props.row)" v-t="'admin.changeOwner'" href="#"></a></li>
</ul>
</div>
<button
class="btn btn-danger"
v-t="'admin.deletePlayer'"
@click="deletePlayer(props.row)"
></button>
</span>
<span v-else v-text="props.formattedRow[props.column.field]" />
</template>
</vue-good-table>
</section>
</template>
<script>
import { VueGoodTable } from 'vue-good-table';
import 'vue-good-table/dist/vue-good-table.min.css';
import { swal } from '../../js/notify';
import toastr from 'toastr';
export default {
name: 'PlayersManagement',
components: {
VueGoodTable
},
props: {
baseUrl: {
default: blessing.base_url
}
},
data() {
return {
players: [],
columns: [
{ field: 'pid', label: 'PID', type: 'number' },
{ field: 'player_name', label: this.$t('general.player.player-name') },
{ field: 'uid', label: this.$t('general.player.owner'), type: 'number' },
{ field: 'preference', label: this.$t('general.player.preference'), globalSearchDisabled: true },
{ field: 'preview', label: this.$t('general.player.previews'), globalSearchDisabled: true, sortable: false },
{ field: 'last_modified', label: this.$t('general.player.last-modified') },
{ field: 'operations', label: this.$t('admin.operationsTitle'), globalSearchDisabled: true, sortable: false },
],
tableOptions: {
search: {
enabled: true,
placeholder: this.$t('vendor.datatable.search')
},
pagination: {
enabled: true,
nextLabel: this.$t('vendor.datatable.next'),
prevLabel: this.$t('vendor.datatable.prev'),
rowsPerPageLabel: this.$t('vendor.datatable.rowsPerPage'),
allLabel: this.$t('vendor.datatable.all'),
ofLabel: this.$t('vendor.datatable.of')
}
},
};
},
beforeMount() {
this.fetchData();
},
methods: {
async fetchData() {
this.players = (await this.$http.get(`/admin/player-data${location.search}`)).data;
},
async changeTexture(player, model) {
const { dismiss, value } = await swal({
text: this.$t('admin.pidNotice'),
input: 'number',
inputValue: player[`tid_${model}`]
});
if (dismiss) {
return;
}
const { errno, msg } = await this.$http.post(
'/admin/players?action=texture',
{ pid: player.pid, model, tid: value }
);
if (errno === 0) {
player[`tid_${model}`] = value;
toastr.success(msg);
} else {
toastr.warning(msg);
}
},
async changeName(player) {
const { dismiss, value } = await swal({
text: this.$t('admin.changePlayerNameNotice'),
input: 'text',
inputValue: player.player_name,
inputValidator: name => !name && this.$t('admin.emptyPlayerName')
});
if (dismiss) {
return;
}
const { errno, msg } = await this.$http.post(
'/admin/players?action=name',
{ pid: player.pid, name: value }
);
if (errno === 0) {
player.player_name = value;
toastr.success(msg);
} else {
toastr.warning(msg);
}
},
async togglePreference(player) {
const preference = player.preference === 'default' ? 'slim' : 'default';
const { errno, msg } = await this.$http.post(
'/admin/players?action=preference',
{ pid: player.pid, preference }
);
if (errno === 0) {
player.preference = preference;
toastr.success(msg);
} else {
toastr.warning(msg);
}
},
async changeOwner(player) {
const { dismiss, value } = await swal({
text: this.$t('admin.changePlayerOwner'),
input: 'number',
inputValue: player.uid
});
if (dismiss) {
return;
}
const { errno, msg } = await this.$http.post(
'/admin/players?action=owner',
{ pid: player.pid, uid: value }
);
if (errno === 0) {
player.uid = value;
toastr.success(msg);
} else {
toastr.warning(msg);
}
},
async deletePlayer(player) {
const { dismiss } = await swal({
text: this.$t('admin.deletePlayerNotice'),
type: 'warning',
showCancelButton: true
});
if (dismiss) {
return;
}
const { errno, msg } = await this.$http.post(
'/admin/players?action=delete',
{ pid: player.pid }
);
if (errno === 0) {
this.players = this.players.filter(({ pid }) => pid !== player.pid);
toastr.success(msg);
} else {
toastr.warning(msg);
}
}
}
};
</script>

View File

@ -19,4 +19,9 @@ export default [
component: () => import('./admin/users'),
el: '.content'
},
{
path: 'admin/players',
component: () => import('./admin/players'),
el: '.content'
},
];

View File

@ -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);
});

View File

@ -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:

View File

@ -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: