mirror of
https://github.com/bs-community/blessing-skin-server.git
synced 2024-12-27 06:29:19 +08:00
Add players management page
This commit is contained in:
parent
365f38c781
commit
887fcbdc90
229
resources/assets/src/components/admin/Players.vue
Normal file
229
resources/assets/src/components/admin/Players.vue
Normal 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>
|
@ -19,4 +19,9 @@ export default [
|
|||||||
component: () => import('./admin/users'),
|
component: () => import('./admin/users'),
|
||||||
el: '.content'
|
el: '.content'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'admin/players',
|
||||||
|
component: () => import('./admin/players'),
|
||||||
|
el: '.content'
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
161
resources/assets/tests/components/admin/Players.test.js
Normal file
161
resources/assets/tests/components/admin/Players.test.js
Normal 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);
|
||||||
|
});
|
@ -165,16 +165,13 @@ admin:
|
|||||||
normal: Normal
|
normal: Normal
|
||||||
admin: Admin
|
admin: Admin
|
||||||
superAdmin: Super Admin
|
superAdmin: Super Admin
|
||||||
textureType: Texture Type
|
|
||||||
skin: 'Skin (:model Model)'
|
|
||||||
cape: Cape
|
|
||||||
pid: Texture ID
|
|
||||||
pidNotice: >-
|
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.
|
player.
|
||||||
changePlayerTexture: 'Change textures of :player'
|
changePlayerTexture: 'Change textures of :player'
|
||||||
changeTexture: Change Textures
|
changeTexture: Change Textures
|
||||||
changePlayerName: Change Player Name
|
changePlayerName: Change Player Name
|
||||||
|
changePreference: Toggle Preference
|
||||||
changeOwner: Change Owner
|
changeOwner: Change Owner
|
||||||
deletePlayer: Delete
|
deletePlayer: Delete
|
||||||
changePlayerOwner: 'Please enter the id of user which this player should be transferred to:'
|
changePlayerOwner: 'Please enter the id of user which this player should be transferred to:'
|
||||||
@ -226,6 +223,12 @@ general:
|
|||||||
nickname: Nick Name
|
nickname: Nick Name
|
||||||
score: Score
|
score: Score
|
||||||
register-at: Registered At
|
register-at: Registered At
|
||||||
|
player:
|
||||||
|
owner: Owner
|
||||||
|
player-name: Player Name
|
||||||
|
preference: Preference
|
||||||
|
previews: Texture Previews
|
||||||
|
last-modified: Last Modified
|
||||||
|
|
||||||
vendor:
|
vendor:
|
||||||
datatable:
|
datatable:
|
||||||
|
@ -166,16 +166,13 @@ admin:
|
|||||||
normal: 普通用户
|
normal: 普通用户
|
||||||
admin: 管理员
|
admin: 管理员
|
||||||
superAdmin: 超级管理员
|
superAdmin: 超级管理员
|
||||||
textureType: 材质类型
|
|
||||||
skin: '皮肤(:model 模型)'
|
|
||||||
cape: 披风
|
|
||||||
pid: 材质 ID
|
|
||||||
pidNotice: 输入要更换的材质的 TID,输入 0 即可清除该角色的材质
|
pidNotice: 输入要更换的材质的 TID,输入 0 即可清除该角色的材质
|
||||||
changePlayerTexture: '更换角色 :player 的材质'
|
changePlayerTexture: '更换角色 :player 的材质'
|
||||||
changeTexture: 更换材质
|
changeTexture: 更换材质
|
||||||
changePlayerName: 更改角色名
|
changePlayerName: 更改角色名
|
||||||
|
changePreference: 切换模型
|
||||||
changeOwner: 更换角色拥有者
|
changeOwner: 更换角色拥有者
|
||||||
deletePlayer: 删除角色
|
deletePlayer: 删除
|
||||||
changePlayerOwner: 请输入此角色要让渡至的用户 UID:
|
changePlayerOwner: 请输入此角色要让渡至的用户 UID:
|
||||||
deletePlayerNotice: 真的要删除此角色吗?此操作不可恢复
|
deletePlayerNotice: 真的要删除此角色吗?此操作不可恢复
|
||||||
targetUser: '目标用户::nickname'
|
targetUser: '目标用户::nickname'
|
||||||
@ -223,6 +220,12 @@ general:
|
|||||||
nickname: 昵称
|
nickname: 昵称
|
||||||
score: 积分
|
score: 积分
|
||||||
register-at: 注册时间
|
register-at: 注册时间
|
||||||
|
player:
|
||||||
|
owner: 拥有者
|
||||||
|
player-name: 角色名
|
||||||
|
preference: 优先模型
|
||||||
|
previews: 预览材质
|
||||||
|
last-modified: 修改时间
|
||||||
|
|
||||||
vendor:
|
vendor:
|
||||||
fileinput:
|
fileinput:
|
||||||
|
Loading…
Reference in New Issue
Block a user