Improve moderation UI (#3989)

* admin users UI

* tune
This commit is contained in:
MeiMei 2019-01-26 17:14:10 +09:00 committed by syuilo
parent da7d1938c9
commit 85cd647946
3 changed files with 160 additions and 81 deletions

View File

@ -1266,14 +1266,19 @@ admin/views/users.vue:
user-not-found: "ユーザーが見つかりません" user-not-found: "ユーザーが見つかりません"
lookup: "照会" lookup: "照会"
reset-password: "パスワードをリセット" reset-password: "パスワードをリセット"
reset-password-confirm: "パスワードをリセットしますか?"
password-updated: "パスワードは現在「{password}」です" password-updated: "パスワードは現在「{password}」です"
suspend: "凍結" suspend: "凍結"
suspend-confirm: "凍結しますか?"
suspended: "凍結しました" suspended: "凍結しました"
unsuspend: "凍結の解除" unsuspend: "凍結の解除"
unsuspend-confirm: "凍結を解除しますか?"
unsuspended: "凍結を解除しました" unsuspended: "凍結を解除しました"
verify: "公式アカウントにする" verify: "公式アカウントにする"
verify-confirm: "公式アカウントにしますか?"
verified: "公式アカウントにしました" verified: "公式アカウントにしました"
unverify: "公式アカウントを解除する" unverify: "公式アカウントを解除する"
unverify-confirm: "公式アカウントを解除しますか?"
unverified: "公式アカウントを解除しました" unverified: "公式アカウントを解除しました"
users: users:
title: "ユーザー" title: "ユーザー"

View File

@ -0,0 +1,82 @@
<template>
<div class="kofvwchc">
<div>
<a :href="user | userPage(null, true)">
<mk-avatar class="avatar" :user="user" :disable-link="true"/>
</a>
</div>
<div>
<header>
<b><mk-user-name :user="user"/></b>
<span class="username">@{{ user | acct }}</span>
<span class="is-admin" v-if="user.isAdmin">admin</span>
<span class="is-moderator" v-if="user.isModerator">moderator</span>
<span class="is-verified" v-if="user.isVerified" :title="$t('@.verified-user')"><fa icon="star"/></span>
<span class="is-suspended" v-if="user.isSuspended" :title="$t('@.suspended-user')"><fa :icon="faSnowflake"/></span>
</header>
<div>
<span>{{ $t('users.updatedAt') }}: <mk-time :time="user.updatedAt" mode="detail"/></span>
</div>
<div>
<span>{{ $t('users.createdAt') }}: <mk-time :time="user.createdAt" mode="detail"/></span>
</div>
</div>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import i18n from '../../i18n';
import { faSnowflake } from '@fortawesome/free-regular-svg-icons';
export default Vue.extend({
i18n: i18n('admin/views/users.vue'),
props: ['user'],
data() {
return {
faSnowflake
};
},
});
</script>
<style lang="stylus" scoped>
.kofvwchc
display flex
padding 16px 0
border-top solid 1px var(--faceDivider)
> div:first-child
> a
> .avatar
width 64px
height 64px
> div:last-child
flex 1
padding-left 16px
@media (max-width 500px)
font-size 14px
> header
> .username
margin-left 8px
opacity 0.7
> .is-admin
> .is-moderator
flex-shrink 0
align-self center
margin 0 0 0 .5em
padding 1px 6px
font-size 80%
border-radius 3px
background var(--noteHeaderAdminBg)
color var(--noteHeaderAdminFg)
> .is-verified
> .is-suspended
margin 0 0 0 .5em
color #4dabf7
</style>

View File

@ -3,20 +3,26 @@
<ui-card> <ui-card>
<div slot="title"><fa :icon="faTerminal"/> {{ $t('operation') }}</div> <div slot="title"><fa :icon="faTerminal"/> {{ $t('operation') }}</div>
<section class="fit-top"> <section class="fit-top">
<ui-input v-model="target" type="text"> <ui-input class="target" v-model="target" type="text">
<span>{{ $t('username-or-userid') }}</span> <span>{{ $t('username-or-userid') }}</span>
</ui-input> </ui-input>
<ui-button @click="resetPassword"><fa :icon="faKey"/> {{ $t('reset-password') }}</ui-button>
<ui-horizon-group>
<ui-button @click="verifyUser" :disabled="verifying"><fa :icon="faCertificate"/> {{ $t('verify') }}</ui-button>
<ui-button @click="unverifyUser" :disabled="unverifying">{{ $t('unverify') }}</ui-button>
</ui-horizon-group>
<ui-horizon-group>
<ui-button @click="suspendUser" :disabled="suspending"><fa :icon="faSnowflake"/> {{ $t('suspend') }}</ui-button>
<ui-button @click="unsuspendUser" :disabled="unsuspending">{{ $t('unsuspend') }}</ui-button>
</ui-horizon-group>
<ui-button @click="showUser"><fa :icon="faSearch"/> {{ $t('lookup') }}</ui-button> <ui-button @click="showUser"><fa :icon="faSearch"/> {{ $t('lookup') }}</ui-button>
<ui-textarea v-if="user" :value="user | json5" readonly tall style="margin-top:16px;"></ui-textarea>
<div class="user" v-if="user">
<x-user :user='user'/>
<div class="actions">
<ui-button @click="resetPassword"><fa :icon="faKey"/> {{ $t('reset-password') }}</ui-button>
<ui-horizon-group>
<ui-button @click="verifyUser" :disabled="verifying"><fa :icon="faCertificate"/> {{ $t('verify') }}</ui-button>
<ui-button @click="unverifyUser" :disabled="unverifying">{{ $t('unverify') }}</ui-button>
</ui-horizon-group>
<ui-horizon-group>
<ui-button @click="suspendUser" :disabled="suspending"><fa :icon="faSnowflake"/> {{ $t('suspend') }}</ui-button>
<ui-button @click="unsuspendUser" :disabled="unsuspending">{{ $t('unsuspend') }}</ui-button>
</ui-horizon-group>
<ui-textarea v-if="user" :value="user | json5" readonly tall style="margin-top:16px;"></ui-textarea>
</div>
</div>
</section> </section>
</ui-card> </ui-card>
@ -47,29 +53,7 @@
</ui-select> </ui-select>
</ui-horizon-group> </ui-horizon-group>
<sequential-entrance animation="entranceFromTop" delay="25"> <sequential-entrance animation="entranceFromTop" delay="25">
<div class="kofvwchc" v-for="user in users" :key="user.id"> <x-user v-for="user in users" :user='user' :key="user.id"/>
<div>
<a :href="user | userPage(null, true)">
<mk-avatar class="avatar" :user="user" :disable-link="true"/>
</a>
</div>
<div>
<header>
<b><mk-user-name :user="user"/></b>
<span class="username">@{{ user | acct }}</span>
<span class="is-admin" v-if="user.isAdmin">admin</span>
<span class="is-moderator" v-if="user.isModerator">moderator</span>
<span class="is-verified" v-if="user.isVerified" :title="$t('@.verified-user')"><fa icon="star"/></span>
<span class="is-suspended" v-if="user.isSuspended" :title="$t('@.suspended-user')"><fa :icon="faSnowflake"/></span>
</header>
<div>
<span>{{ $t('users.updatedAt') }}: <mk-time :time="user.updatedAt" mode="detail"/></span>
</div>
<div>
<span>{{ $t('users.createdAt') }}: <mk-time :time="user.createdAt" mode="detail"/></span>
</div>
</div>
</div>
</sequential-entrance> </sequential-entrance>
<ui-button v-if="existMore" @click="fetchUsers">{{ $t('@.load-more') }}</ui-button> <ui-button v-if="existMore" @click="fetchUsers">{{ $t('@.load-more') }}</ui-button>
</section> </section>
@ -83,10 +67,13 @@ import i18n from '../../i18n';
import parseAcct from "../../../../misc/acct/parse"; import parseAcct from "../../../../misc/acct/parse";
import { faCertificate, faUsers, faTerminal, faSearch, faKey } from '@fortawesome/free-solid-svg-icons'; import { faCertificate, faUsers, faTerminal, faSearch, faKey } from '@fortawesome/free-solid-svg-icons';
import { faSnowflake } from '@fortawesome/free-regular-svg-icons'; import { faSnowflake } from '@fortawesome/free-regular-svg-icons';
import XUser from './users.user.vue';
export default Vue.extend({ export default Vue.extend({
i18n: i18n('admin/views/users.vue'), i18n: i18n('admin/views/users.vue'),
components: {
XUser
},
data() { data() {
return { return {
user: null, user: null,
@ -131,6 +118,7 @@ export default Vue.extend({
}, },
methods: { methods: {
/** テキストエリアのユーザーを解決する */
async fetchUser() { async fetchUser() {
try { try {
return await this.$root.api('users/show', this.target.startsWith('@') ? parseAcct(this.target) : { userId: this.target }); return await this.$root.api('users/show', this.target.startsWith('@') ? parseAcct(this.target) : { userId: this.target });
@ -149,16 +137,27 @@ export default Vue.extend({
} }
}, },
/** テキストエリアから処理対象ユーザーを設定する */
async showUser() { async showUser() {
this.user = null;
const user = await this.fetchUser(); const user = await this.fetchUser();
this.$root.api('admin/show-user', { userId: user.id }).then(info => { this.$root.api('admin/show-user', { userId: user.id }).then(info => {
this.user = info; this.user = info;
}); });
this.target = '';
},
/** 処理対象ユーザーの情報を更新する */
async refreshUser() {
this.$root.api('admin/show-user', { userId: this.user._id }).then(info => {
this.user = info;
});
}, },
async resetPassword() { async resetPassword() {
const user = await this.fetchUser(); if (!await this.getConfirmed(this.$t('reset-password-confirm'))) return;
this.$root.api('admin/reset-password', { userId: user.id }).then(res => {
this.$root.api('admin/reset-password', { userId: this.user._id }).then(res => {
this.$root.dialog({ this.$root.dialog({
type: 'success', type: 'success',
text: this.$t('password-updated', { password: res.password }) text: this.$t('password-updated', { password: res.password })
@ -167,11 +166,12 @@ export default Vue.extend({
}, },
async verifyUser() { async verifyUser() {
if (!await this.getConfirmed(this.$t('verify-confirm'))) return;
this.verifying = true; this.verifying = true;
const process = async () => { const process = async () => {
const user = await this.fetchUser(); await this.$root.api('admin/verify-user', { userId: this.user._id });
await this.$root.api('admin/verify-user', { userId: user.id });
this.$root.dialog({ this.$root.dialog({
type: 'success', type: 'success',
text: this.$t('verified') text: this.$t('verified')
@ -186,14 +186,17 @@ export default Vue.extend({
}); });
this.verifying = false; this.verifying = false;
this.refreshUser();
}, },
async unverifyUser() { async unverifyUser() {
if (!await this.getConfirmed(this.$t('unverify-confirm'))) return;
this.unverifying = true; this.unverifying = true;
const process = async () => { const process = async () => {
const user = await this.fetchUser(); await this.$root.api('admin/unverify-user', { userId: this.user._id });
await this.$root.api('admin/unverify-user', { userId: user.id });
this.$root.dialog({ this.$root.dialog({
type: 'success', type: 'success',
text: this.$t('unverified') text: this.$t('unverified')
@ -208,14 +211,17 @@ export default Vue.extend({
}); });
this.unverifying = false; this.unverifying = false;
this.refreshUser();
}, },
async suspendUser() { async suspendUser() {
if (!await this.getConfirmed(this.$t('suspend-confirm'))) return;
this.suspending = true; this.suspending = true;
const process = async () => { const process = async () => {
const user = await this.fetchUser(); await this.$root.api('admin/suspend-user', { userId: this.user._id });
await this.$root.api('admin/suspend-user', { userId: user.id });
this.$root.dialog({ this.$root.dialog({
type: 'success', type: 'success',
text: this.$t('suspended') text: this.$t('suspended')
@ -230,14 +236,17 @@ export default Vue.extend({
}); });
this.suspending = false; this.suspending = false;
this.refreshUser();
}, },
async unsuspendUser() { async unsuspendUser() {
if (!await this.getConfirmed(this.$t('unsuspend-confirm'))) return;
this.unsuspending = true; this.unsuspending = true;
const process = async () => { const process = async () => {
const user = await this.fetchUser(); await this.$root.api('admin/unsuspend-user', { userId: this.user._id });
await this.$root.api('admin/unsuspend-user', { userId: user.id });
this.$root.dialog({ this.$root.dialog({
type: 'success', type: 'success',
text: this.$t('unsuspended') text: this.$t('unsuspended')
@ -252,8 +261,21 @@ export default Vue.extend({
}); });
this.unsuspending = false; this.unsuspending = false;
this.refreshUser();
}, },
async getConfirmed(text: string): Promise<Boolean> {
const confirm = await this.$root.dialog({
type: 'warning',
showCancelButton: true,
title: 'confirm',
text,
});
return !confirm.canceled;
}
fetchUsers() { fetchUsers() {
this.$root.api('admin/show-users', { this.$root.api('admin/show-users', {
state: this.state, state: this.state,
@ -277,42 +299,12 @@ export default Vue.extend({
</script> </script>
<style lang="stylus" scoped> <style lang="stylus" scoped>
.kofvwchc .target
display flex margin-bottom 16px !important
padding 16px 0
border-top solid 1px var(--faceDivider)
> div:first-child .user
> a margin-top 32px
> .avatar
width 64px
height 64px
> div:last-child > .actions
flex 1 margin-left 80px
padding-left 16px
@media (max-width 500px)
font-size 14px
> header
> .username
margin-left 8px
opacity 0.7
> .is-admin
> .is-moderator
flex-shrink 0
align-self center
margin 0 0 0 .5em
padding 1px 6px
font-size 80%
border-radius 3px
background var(--noteHeaderAdminBg)
color var(--noteHeaderAdminFg)
> .is-verified
> .is-suspended
margin 0 0 0 .5em
color #4dabf7
</style> </style>