mirror of
https://github.com/bs-community/blessing-skin-server.git
synced 2024-12-15 06:09:58 +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'),
|
||||
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
|
||||
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:
|
||||
|
@ -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:
|
||||
|
Loading…
Reference in New Issue
Block a user