Merge branch 'master' into feature/update-server-configuration
This commit is contained in:
commit
306c79eadd
157 changed files with 2359 additions and 2058 deletions
14
CHANGELOG.md
14
CHANGELOG.md
|
@ -8,26 +8,36 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||
|
||||
### Changed
|
||||
|
||||
- moves emoji pack configuration from the main menu to settings tab, redesigns it and fixes bugs
|
||||
- **breaking** PleromaFE login feature relies on `admin` scope presence in PleromaFE token (older versions of PleromaFE don't support it)
|
||||
- Moves emoji pack configuration from the main menu to settings tab, redesigns it and fixes bugs
|
||||
- `mailerEnabled` must be set to `true` in order to require password reset (password reset currently only works via email)
|
||||
- remove fetching initial data for configuring server settings
|
||||
- Remove fetching initial data for configuring server settings
|
||||
- Actions in users module (ActivateUsers, AddRight, DeactivateUsers, DeleteRight, DeleteUsers) now accept an array of users instead of one user
|
||||
- Leave dropdown menu open after clicking an action
|
||||
- Move current try/catch error handling from view files to module, add it where necessary
|
||||
|
||||
### Added
|
||||
|
||||
- Optimistic update for actions in users module and fetching users after api function finished its execution
|
||||
- Relay management
|
||||
- Ability to fetch all statuses from a given instance
|
||||
- Grouped reports: now you can view reports, which are grouped by status (pagination is not implemented yet, though)
|
||||
- Ability to confirm users' emails and resend confirmation emails
|
||||
- Report notes
|
||||
- Ability to moderate users on the statuses page
|
||||
|
||||
### Fixed
|
||||
|
||||
- Show checkmarks when tag is applied
|
||||
- Reports update (also, now it's optimistic)
|
||||
- Remove duplicated success message
|
||||
|
||||
## [1.2.0] - 2019-09-27
|
||||
|
||||
### Added
|
||||
|
||||
- Emoji pack configuration
|
||||
- Statuses page: fetch all statuses from a given instance
|
||||
- Ability to require user's password reset
|
||||
– Ability to track admin/moderator actions, a.k.a. "the moderation log"
|
||||
|
||||
|
|
13
README.md
13
README.md
|
@ -6,6 +6,13 @@
|
|||
|
||||
Admin UI for pleroma instance owners.
|
||||
|
||||
### Branches
|
||||
|
||||
There are two main branches here:
|
||||
|
||||
- `develop`: ongoing work and all merge requests go here, *unstable*
|
||||
- `master`: after `develop` is stabilized it is merged to `master`, `master` is *stable*, allegedly
|
||||
|
||||
### Features
|
||||
|
||||
1. User administration: grant roles to users (admin/moderator), deactivate/delete as well as force their statuses to have NSFW tag, strip media and many more
|
||||
|
@ -18,13 +25,17 @@ You can have any combination of these features (i.e. you can disable anything, b
|
|||
|
||||
## Usage
|
||||
|
||||
### Bundled
|
||||
|
||||
AdminFE is bundled with Pleroma, i.e. you can just visit `https://your.instance/pleroma/admin/` to try it out.
|
||||
|
||||
### Development
|
||||
|
||||
To run AdminFE locally execute `yarn dev`
|
||||
|
||||
### Build
|
||||
|
||||
To compile everything for production run `yarn build:prod`.
|
||||
To compile everything for production run `yarn build:prod`, this will build admin-fe into `dist` folder, which you will need to upload to your server and/or point your webserver of choice to.
|
||||
|
||||
#### Disabling features
|
||||
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
<title>Admin FE</title>
|
||||
</head>
|
||||
<body>
|
||||
<script src=<%= BASE_URL %>/tinymce4.7.5/tinymce.min.js></script>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
|
|
|
@ -43,7 +43,7 @@
|
|||
"driver.js": "0.8.1",
|
||||
"dropzone": "5.2.0",
|
||||
"echarts": "4.1.0",
|
||||
"element-ui": "^2.10.0",
|
||||
"element-ui": "^2.13.0",
|
||||
"file-saver": "1.3.8",
|
||||
"fuse.js": "3.4.2",
|
||||
"js-cookie": "2.2.0",
|
||||
|
|
|
@ -8,7 +8,7 @@ export async function loginByUsername(username, password, authHost) {
|
|||
const verifyHost = user.authHost === authHost
|
||||
const data = {
|
||||
'token_type': 'Bearer',
|
||||
'scope': 'read write follow',
|
||||
'scope': 'read write follow push admin',
|
||||
'refresh_token': 'foo123',
|
||||
'me': 'bob',
|
||||
'expires_in': 600,
|
||||
|
|
|
@ -11,20 +11,42 @@ const reports = [
|
|||
{ created_at: '2019-05-18T13:01:33.000Z', account: { acct: 'nick', display_name: 'Nick Keys', tags: [] }, actor: { acct: 'admin' }, state: 'closed', id: '4', content: '', statuses: [] }
|
||||
]
|
||||
|
||||
export async function fetchReports(limit, max_id, authHost, token) {
|
||||
const paginatedReports = max_id.length > 0 ? reports.slice(5) : reports.slice(0, 5)
|
||||
return Promise.resolve({ data: { reports: paginatedReports }})
|
||||
const groupedReports = [
|
||||
{ account: { avatar: 'http://localhost:4000/images/avi.png', confirmation_pending: false, deactivated: false, display_name: 'leo', id: '9oG0YghgBi94EATI9I', local: true, nickname: 'leo', roles: { admin: false, moderator: false }, tags: [] },
|
||||
actors: [{ acct: 'admin', avatar: 'http://localhost:4000/images/avi.png', deactivated: false, display_name: 'admin', id: '9oFz4pTauG0cnJ581w', local: true, nickname: 'admin', roles: { admin: false, moderator: false }, tags: [], url: 'http://localhost:4000/users/admin', username: 'admin' }],
|
||||
date: '2019-11-23T12:56:11.969772Z',
|
||||
reports: [
|
||||
{ created_at: '2019-05-21T21:35:33.000Z', account: { acct: 'benj', display_name: 'Benjamin Fame', tags: [] }, actor: { acct: 'admin' }, state: 'open', id: '2', content: 'This is a report', statuses: [] },
|
||||
{ created_at: '2019-05-20T22:45:33.000Z', account: { acct: 'alice', display_name: 'Alice Pool', tags: [] }, actor: { acct: 'admin2' }, state: 'resolved', id: '7', content: 'Please block this user', statuses: [
|
||||
{ account: { display_name: 'Alice Pool', avatar: '' }, visibility: 'public', sensitive: false, id: '11', content: 'Hey!', url: '', created_at: '2019-05-10T21:35:33.000Z' },
|
||||
{ account: { display_name: 'Alice Pool', avatar: '' }, visibility: 'unlisted', sensitive: true, id: '10', content: 'Bye!', url: '', created_at: '2019-05-10T21:00:33.000Z' }
|
||||
] }
|
||||
],
|
||||
status: {
|
||||
account: { acct: 'leo' },
|
||||
content: 'At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis',
|
||||
created_at: '2019-11-23T12:55:20.000Z',
|
||||
id: '9pFoQO69piu7cUDnJg',
|
||||
url: 'http://localhost:4000/notice/9pFoQO69piu7cUDnJg',
|
||||
visibility: 'unlisted',
|
||||
sensitive: true
|
||||
},
|
||||
status_deleted: false
|
||||
}
|
||||
]
|
||||
|
||||
export async function fetchReports(filter, page, pageSize, authHost, token) {
|
||||
return filter.length > 0
|
||||
? Promise.resolve({ data: { reports: reports.filter(report => report.state === filter) }})
|
||||
: Promise.resolve({ data: { reports }})
|
||||
}
|
||||
|
||||
export async function filterReports(filter, limit, max_id, authHost, token) {
|
||||
const filteredReports = reports.filter(report => report.state === filter)
|
||||
const paginatedReports = max_id.length > 0 ? filteredReports.slice(5) : filteredReports.slice(0, 5)
|
||||
return Promise.resolve({ data: { reports: paginatedReports }})
|
||||
export async function fetchGroupedReports(authHost, token) {
|
||||
return Promise.resolve({ data: { reports: groupedReports }})
|
||||
}
|
||||
|
||||
export async function changeState(state, id, authHost, token) {
|
||||
const report = reports.find(report => report.id === id)
|
||||
return Promise.resolve({ data: { ...report, state }})
|
||||
export async function changeState(reportsData, authHost, token) {
|
||||
return Promise.resolve({ data: '' })
|
||||
}
|
||||
|
||||
export async function changeStatusScope(id, sensitive, visibility, authHost, token) {
|
||||
|
|
7
src/api/__mocks__/status.js
Normal file
7
src/api/__mocks__/status.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
export async function changeStatusScope(id, sensitive, visibility, authHost, token) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
export async function deleteStatus(id, authHost, token) {
|
||||
return Promise.resolve()
|
||||
}
|
|
@ -4,6 +4,10 @@ export let users = [
|
|||
{ active: false, deactivated: true, id: 'abc', nickname: 'john', local: true, external: false, roles: { admin: false, moderator: false }, tags: ['strip_media'] }
|
||||
]
|
||||
|
||||
const userProfile = { avatar: 'avatar.jpg', display_name: 'Allis', nickname: 'allis', id: '2', tags: [], roles: { admin: true, moderator: false }, local: true, external: false }
|
||||
|
||||
const userStatuses = []
|
||||
|
||||
const filterUsers = (str) => {
|
||||
const filters = str.split(',').filter(item => item.length > 0)
|
||||
if (filters.length === 0) {
|
||||
|
@ -20,6 +24,10 @@ const filterUsers = (str) => {
|
|||
return applyFilters([], filters, users)
|
||||
}
|
||||
|
||||
export async function fetchUser(id, authHost, token) {
|
||||
return Promise.resolve({ data: userProfile })
|
||||
}
|
||||
|
||||
export async function fetchUsers(filters, authHost, token, page = 1) {
|
||||
const filteredUsers = filterUsers(filters)
|
||||
return Promise.resolve({ data: {
|
||||
|
@ -29,6 +37,10 @@ export async function fetchUsers(filters, authHost, token, page = 1) {
|
|||
}})
|
||||
}
|
||||
|
||||
export async function fetchUserStatuses(id, authHost, godmode, token) {
|
||||
return Promise.resolve({ data: userStatuses })
|
||||
}
|
||||
|
||||
export async function getPasswordResetToken(nickname, authHost, token) {
|
||||
return Promise.resolve({ data: { token: 'g05lxnBJQnL', link: 'http://url/api/pleroma/password_reset/g05lxnBJQnL' }})
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ export async function loginByUsername(username, password, authHost) {
|
|||
data: {
|
||||
client_name: `AdminFE_${Math.random()}`,
|
||||
redirect_uris: `${window.location.origin}/oauth-callback`,
|
||||
scopes: 'read write follow'
|
||||
scopes: 'read write follow push admin'
|
||||
}
|
||||
})
|
||||
|
||||
|
|
14
src/api/peers.js
Normal file
14
src/api/peers.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
import request from '@/utils/request'
|
||||
import { getToken } from '@/utils/auth'
|
||||
import { baseName } from './utils'
|
||||
|
||||
export async function fetchPeers(authHost, token) {
|
||||
return await request({
|
||||
baseURL: baseName(authHost),
|
||||
url: `/api/v1/instance/peers`,
|
||||
method: 'get',
|
||||
headers: authHeaders(token)
|
||||
})
|
||||
}
|
||||
|
||||
const authHeaders = (token) => token ? { 'Authorization': `Bearer ${getToken()}` } : {}
|
|
@ -2,51 +2,54 @@ import request from '@/utils/request'
|
|||
import { getToken } from '@/utils/auth'
|
||||
import { baseName } from './utils'
|
||||
|
||||
export async function changeState(state, id, authHost, token) {
|
||||
export async function changeState(reports, authHost, token) {
|
||||
return await request({
|
||||
baseURL: baseName(authHost),
|
||||
url: `/api/pleroma/admin/reports/${id}`,
|
||||
method: 'put',
|
||||
url: `/api/pleroma/admin/reports`,
|
||||
method: 'patch',
|
||||
headers: authHeaders(token),
|
||||
data: { state }
|
||||
data: { reports }
|
||||
})
|
||||
}
|
||||
|
||||
export async function changeStatusScope(id, sensitive, visibility, authHost, token) {
|
||||
export async function fetchReports(filter, page, pageSize, authHost, token) {
|
||||
const url = filter.length > 0
|
||||
? `/api/pleroma/admin/reports?state=${filter}&page=${page}&page_size=${pageSize}`
|
||||
: `/api/pleroma/admin/reports?page=${page}&page_size=${pageSize}`
|
||||
return await request({
|
||||
baseURL: baseName(authHost),
|
||||
url: `/api/pleroma/admin/statuses/${id}`,
|
||||
method: 'put',
|
||||
headers: authHeaders(token),
|
||||
data: { sensitive, visibility }
|
||||
})
|
||||
}
|
||||
|
||||
export async function deleteStatus(id, authHost, token) {
|
||||
return await request({
|
||||
baseURL: baseName(authHost),
|
||||
url: `/api/pleroma/admin/statuses/${id}`,
|
||||
method: 'delete',
|
||||
headers: authHeaders(token)
|
||||
})
|
||||
}
|
||||
|
||||
export async function fetchReports(limit, max_id, authHost, token) {
|
||||
return await request({
|
||||
baseURL: baseName(authHost),
|
||||
url: `/api/pleroma/admin/reports?limit=${limit}&max_id=${max_id}`,
|
||||
url,
|
||||
method: 'get',
|
||||
headers: authHeaders(token)
|
||||
})
|
||||
}
|
||||
|
||||
export async function filterReports(filter, limit, max_id, authHost, token) {
|
||||
export async function fetchGroupedReports(authHost, token) {
|
||||
return await request({
|
||||
baseURL: baseName(authHost),
|
||||
url: `/api/pleroma/admin/reports?state=${filter}&limit=${limit}&max_id=${max_id}`,
|
||||
url: `/api/pleroma/admin/grouped_reports`,
|
||||
method: 'get',
|
||||
headers: authHeaders(token)
|
||||
})
|
||||
}
|
||||
|
||||
export async function createNote(content, reportID, authHost, token) {
|
||||
return await request({
|
||||
baseURL: baseName(authHost),
|
||||
url: `/api/pleroma/admin/reports/${reportID}/notes`,
|
||||
method: `post`,
|
||||
headers: authHeaders(token),
|
||||
data: { content }
|
||||
})
|
||||
}
|
||||
|
||||
export async function deleteNote(noteID, reportID, authHost, token) {
|
||||
return await request({
|
||||
baseURL: baseName(authHost),
|
||||
url: `/api/pleroma/admin/reports/${reportID}/notes/${noteID}`,
|
||||
method: `delete`,
|
||||
headers: authHeaders(token)
|
||||
})
|
||||
}
|
||||
|
||||
const authHeaders = (token) => token ? { 'Authorization': `Bearer ${getToken()}` } : {}
|
||||
|
|
33
src/api/status.js
Normal file
33
src/api/status.js
Normal file
|
@ -0,0 +1,33 @@
|
|||
import request from '@/utils/request'
|
||||
import { getToken } from '@/utils/auth'
|
||||
import { baseName } from './utils'
|
||||
|
||||
export async function changeStatusScope(id, sensitive, visibility, authHost, token) {
|
||||
return await request({
|
||||
baseURL: baseName(authHost),
|
||||
url: `/api/pleroma/admin/statuses/${id}`,
|
||||
method: 'put',
|
||||
headers: authHeaders(token),
|
||||
data: { sensitive, visibility }
|
||||
})
|
||||
}
|
||||
|
||||
export async function deleteStatus(id, authHost, token) {
|
||||
return await request({
|
||||
baseURL: baseName(authHost),
|
||||
url: `/api/pleroma/admin/statuses/${id}`,
|
||||
method: 'delete',
|
||||
headers: authHeaders(token)
|
||||
})
|
||||
}
|
||||
|
||||
export async function fetchStatusesByInstance(instance, authHost, token, pageSize, page = 1) {
|
||||
return await request({
|
||||
baseURL: baseName(authHost),
|
||||
url: `/api/pleroma/admin/instances/${instance}/statuses?page=${page}&page_size=${pageSize}`,
|
||||
method: 'get',
|
||||
headers: authHeaders(token)
|
||||
})
|
||||
}
|
||||
|
||||
const authHeaders = (token) => token ? { 'Authorization': `Bearer ${getToken()}` } : {}
|
|
@ -136,4 +136,24 @@ export async function fetchUserStatuses(id, authHost, godmode, token) {
|
|||
})
|
||||
}
|
||||
|
||||
export async function confirmUserEmail(nicknames, authHost, token) {
|
||||
return await request({
|
||||
baseURL: baseName(authHost),
|
||||
url: '/api/pleroma/admin/users/confirm_email',
|
||||
method: 'patch',
|
||||
headers: authHeaders(token),
|
||||
data: { nicknames }
|
||||
})
|
||||
}
|
||||
|
||||
export async function resendConfirmationEmail(nicknames, authHost, token) {
|
||||
return await request({
|
||||
baseURL: baseName(authHost),
|
||||
url: '/api/pleroma/admin/users/resend_confirmation_email',
|
||||
method: 'patch',
|
||||
headers: authHeaders(token),
|
||||
data: { nicknames }
|
||||
})
|
||||
}
|
||||
|
||||
const authHeaders = (token) => token ? { 'Authorization': `Bearer ${getToken()}` } : {}
|
||||
|
|
278
src/components/Status/index.vue
Normal file
278
src/components/Status/index.vue
Normal file
|
@ -0,0 +1,278 @@
|
|||
<template>
|
||||
<div>
|
||||
<el-card v-if="!status.deleted" class="status-card">
|
||||
<div slot="header">
|
||||
<div class="status-header">
|
||||
<div class="status-account-container">
|
||||
<div class="status-account">
|
||||
<el-checkbox @change="handleStatusSelection(status.account)">
|
||||
<img :src="status.account.avatar" class="status-avatar-img">
|
||||
<h3 class="status-account-name">{{ status.account.display_name }}</h3>
|
||||
</el-checkbox>
|
||||
</div>
|
||||
<a :href="status.account.url" target="_blank" class="account">
|
||||
@{{ status.account.acct }}
|
||||
</a>
|
||||
</div>
|
||||
<div class="status-actions">
|
||||
<el-tag v-if="status.sensitive" type="warning" size="large">{{ $t('reports.sensitive') }}</el-tag>
|
||||
<el-tag size="large">{{ capitalizeFirstLetter(status.visibility) }}</el-tag>
|
||||
<el-dropdown trigger="click">
|
||||
<el-button plain size="small" icon="el-icon-edit" class="status-actions-button">
|
||||
{{ $t('reports.changeScope') }}<i class="el-icon-arrow-down el-icon--right"/>
|
||||
</el-button>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item
|
||||
v-if="!status.sensitive"
|
||||
@click.native="changeStatus(status.id, true, status.visibility)">
|
||||
{{ $t('reports.addSensitive') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
v-if="status.sensitive"
|
||||
@click.native="changeStatus(status.id, false, status.visibility)">
|
||||
{{ $t('reports.removeSensitive') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
v-if="status.visibility !== 'public'"
|
||||
@click.native="changeStatus(status.id, status.sensitive, 'public')">
|
||||
{{ $t('reports.public') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
v-if="status.visibility !== 'private'"
|
||||
@click.native="changeStatus(status.id, status.sensitive, 'private')">
|
||||
{{ $t('reports.private') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
v-if="status.visibility !== 'unlisted'"
|
||||
@click.native="changeStatus(status.id, status.sensitive, 'unlisted')">
|
||||
{{ $t('reports.unlisted') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
@click.native="deleteStatus(status.id)">
|
||||
{{ $t('reports.deleteStatus') }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="status-body">
|
||||
<div v-if="status.spoiler_text">
|
||||
<strong>{{ status.spoiler_text }}</strong>
|
||||
<el-button v-if="!showHiddenStatus" size="mini" class="show-more-button" @click="showHiddenStatus = true">Show more</el-button>
|
||||
<el-button v-if="showHiddenStatus" size="mini" class="show-more-button" @click="showHiddenStatus = false">Show less</el-button>
|
||||
<div v-if="showHiddenStatus">
|
||||
<span class="status-content" v-html="status.content"/>
|
||||
<div v-if="status.poll" class="poll">
|
||||
<ul>
|
||||
<li v-for="(option, index) in status.poll.options" :key="index">
|
||||
{{ option.title }}
|
||||
<el-progress :percentage="optionPercent(status.poll, option)" />
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-for="(attachment, index) in status.media_attachments" :key="index" class="image">
|
||||
<img :src="attachment.preview_url">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!status.spoiler_text">
|
||||
<span class="status-content" v-html="status.content"/>
|
||||
<div v-if="status.poll" class="poll">
|
||||
<ul>
|
||||
<li v-for="(option, index) in status.poll.options" :key="index">
|
||||
{{ option.title }}
|
||||
<el-progress :percentage="optionPercent(status.poll, option)" />
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-for="(attachment, index) in status.media_attachments" :key="index" class="image">
|
||||
<img :src="attachment.preview_url">
|
||||
</div>
|
||||
</div>
|
||||
<a :href="status.url" target="_blank" class="account">
|
||||
{{ parseTimestamp(status.created_at) }}
|
||||
</a>
|
||||
</div>
|
||||
</el-card>
|
||||
<el-card v-else class="status-card">
|
||||
<div slot="header">
|
||||
<div class="status-header">
|
||||
<div class="status-account-container">
|
||||
<div class="status-account">
|
||||
<h4 class="status-deleted">{{ $t('reports.statusDeleted') }}</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="status-body">
|
||||
<span v-if="status.content" class="status-content" v-html="status.content"/>
|
||||
<span v-else class="status-without-content">no content</span>
|
||||
</div>
|
||||
<a v-if="status.created_at" :href="status.url" target="_blank" class="account">
|
||||
{{ parseTimestamp(status.created_at) }}
|
||||
</a>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import moment from 'moment'
|
||||
|
||||
export default {
|
||||
name: 'Status',
|
||||
props: {
|
||||
status: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
page: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 0
|
||||
},
|
||||
userId: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: ''
|
||||
},
|
||||
godmode: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showHiddenStatus: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
capitalizeFirstLetter(str) {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1)
|
||||
},
|
||||
changeStatus(statusId, isSensitive, visibility) {
|
||||
this.$store.dispatch('ChangeStatusScope', { statusId, isSensitive, visibility, reportCurrentPage: this.page, userId: this.userId, godmode: this.godmode })
|
||||
},
|
||||
deleteStatus(statusId) {
|
||||
this.$confirm('Are you sure you want to delete this status?', 'Warning', {
|
||||
confirmButtonText: 'OK',
|
||||
cancelButtonText: 'Cancel',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
this.$store.dispatch('DeleteStatus', { statusId, reportCurrentPage: this.page, userId: this.userId, godmode: this.godmode })
|
||||
this.$message({
|
||||
type: 'success',
|
||||
message: 'Delete completed'
|
||||
})
|
||||
}).catch(() => {
|
||||
this.$message({
|
||||
type: 'info',
|
||||
message: 'Delete canceled'
|
||||
})
|
||||
})
|
||||
},
|
||||
optionPercent(poll, pollOption) {
|
||||
const allVotes = poll.options.reduce((acc, option) => (acc + option.votes_count), 0)
|
||||
if (allVotes === 0) {
|
||||
return 0
|
||||
}
|
||||
return +(pollOption.votes_count / allVotes * 100).toFixed(1)
|
||||
},
|
||||
parseTimestamp(timestamp) {
|
||||
return moment(timestamp).format('YYYY-MM-DD HH:mm')
|
||||
},
|
||||
handleStatusSelection(account) {
|
||||
this.$emit('status-selection', account)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style rel='stylesheet/scss' lang='scss'>
|
||||
.status-card {
|
||||
.account {
|
||||
text-decoration: underline;
|
||||
line-height: 26px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.image {
|
||||
width: 20%;
|
||||
img {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
.show-more-button {
|
||||
margin-left: 5px;
|
||||
}
|
||||
.status-account {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.status-avatar-img {
|
||||
display: inline-block;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
.status-account-name {
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
height: 22px;
|
||||
}
|
||||
.status-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.status-content {
|
||||
font-size: 15px;
|
||||
line-height: 26px;
|
||||
}
|
||||
.status-card {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.status-deleted {
|
||||
font-style: italic;
|
||||
margin-top: 3px;
|
||||
}
|
||||
.status-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.status-without-content {
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
@media
|
||||
only screen and (max-width: 760px),
|
||||
(min-device-width: 768px) and (max-device-width: 1024px) {
|
||||
.el-message {
|
||||
min-width: 80%;
|
||||
}
|
||||
.el-message-box {
|
||||
width: 80%;
|
||||
}
|
||||
.status-card {
|
||||
.el-card__header {
|
||||
padding: 10px 17px;
|
||||
}
|
||||
.el-tag {
|
||||
margin: 3px 4px 3px 0;
|
||||
}
|
||||
.status-account-container {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.status-actions-button {
|
||||
margin: 3px 0 3px;
|
||||
}
|
||||
.status-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.status-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,103 +0,0 @@
|
|||
<template>
|
||||
<div class="upload-container">
|
||||
<el-button :style="{background:color,borderColor:color}" icon="el-icon-upload" size="mini" type="primary" @click=" dialogVisible=true">上传图片
|
||||
</el-button>
|
||||
<el-dialog :visible.sync="dialogVisible">
|
||||
<el-upload
|
||||
:multiple="true"
|
||||
:file-list="fileList"
|
||||
:show-file-list="true"
|
||||
:on-remove="handleRemove"
|
||||
:on-success="handleSuccess"
|
||||
:before-upload="beforeUpload"
|
||||
class="editor-slide-upload"
|
||||
action="https://httpbin.org/post"
|
||||
list-type="picture-card">
|
||||
<el-button size="small" type="primary">点击上传</el-button>
|
||||
</el-upload>
|
||||
<el-button @click="dialogVisible = false">取 消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">确 定</el-button>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// import { getToken } from 'api/qiniu'
|
||||
|
||||
export default {
|
||||
name: 'EditorSlideUpload',
|
||||
props: {
|
||||
color: {
|
||||
type: String,
|
||||
default: '#1890ff'
|
||||
}
|
||||
},
|
||||
data: function() {
|
||||
return {
|
||||
dialogVisible: false,
|
||||
listObj: {},
|
||||
fileList: []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
checkAllSuccess() {
|
||||
return Object.keys(this.listObj).every(item => this.listObj[item].hasSuccess)
|
||||
},
|
||||
handleSubmit() {
|
||||
const arr = Object.keys(this.listObj).map(v => this.listObj[v])
|
||||
if (!this.checkAllSuccess()) {
|
||||
this.$message('请等待所有图片上传成功 或 出现了网络问题,请刷新页面重新上传!')
|
||||
return
|
||||
}
|
||||
this.$emit('successCBK', arr)
|
||||
this.listObj = {}
|
||||
this.fileList = []
|
||||
this.dialogVisible = false
|
||||
},
|
||||
handleSuccess(response, file) {
|
||||
const uid = file.uid
|
||||
const objKeyArr = Object.keys(this.listObj)
|
||||
for (let i = 0, len = objKeyArr.length; i < len; i++) {
|
||||
if (this.listObj[objKeyArr[i]].uid === uid) {
|
||||
this.listObj[objKeyArr[i]].url = response.files.file
|
||||
this.listObj[objKeyArr[i]].hasSuccess = true
|
||||
return
|
||||
}
|
||||
}
|
||||
},
|
||||
handleRemove(file) {
|
||||
const uid = file.uid
|
||||
const objKeyArr = Object.keys(this.listObj)
|
||||
for (let i = 0, len = objKeyArr.length; i < len; i++) {
|
||||
if (this.listObj[objKeyArr[i]].uid === uid) {
|
||||
delete this.listObj[objKeyArr[i]]
|
||||
return
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeUpload(file) {
|
||||
const _self = this
|
||||
const _URL = window.URL || window.webkitURL
|
||||
const fileName = file.uid
|
||||
this.listObj[fileName] = {}
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image()
|
||||
img.src = _URL.createObjectURL(file)
|
||||
img.onload = function() {
|
||||
_self.listObj[fileName] = { hasSuccess: false, uid: file.uid, width: this.width, height: this.height }
|
||||
}
|
||||
resolve(true)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style rel="stylesheet/scss" lang="scss" scoped>
|
||||
.editor-slide-upload {
|
||||
margin-bottom: 20px;
|
||||
/deep/ .el-upload--picture-card {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,210 +0,0 @@
|
|||
<template>
|
||||
<div :class="{fullscreen:fullscreen}" class="tinymce-container editor-container">
|
||||
<textarea :id="tinymceId" class="tinymce-textarea"/>
|
||||
<div class="editor-custom-btn-container">
|
||||
<editorImage color="#1890ff" class="editor-upload-btn" @successCBK="imageSuccessCBK"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import editorImage from './components/editorImage'
|
||||
import plugins from './plugins'
|
||||
import toolbar from './toolbar'
|
||||
|
||||
export default {
|
||||
name: 'Tinymce',
|
||||
components: { editorImage },
|
||||
props: {
|
||||
id: {
|
||||
type: String,
|
||||
default: function() {
|
||||
return 'vue-tinymce-' + +new Date() + ((Math.random() * 1000).toFixed(0) + '')
|
||||
}
|
||||
},
|
||||
value: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
toolbar: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default() {
|
||||
return []
|
||||
}
|
||||
},
|
||||
menubar: {
|
||||
type: String,
|
||||
default: 'file edit insert view format table'
|
||||
},
|
||||
height: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 360
|
||||
}
|
||||
},
|
||||
data: function() {
|
||||
return {
|
||||
hasChange: false,
|
||||
hasInit: false,
|
||||
tinymceId: this.id,
|
||||
fullscreen: false,
|
||||
languageTypeList: {
|
||||
'en': 'en',
|
||||
'zh': 'zh_CN'
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
language() {
|
||||
return this.languageTypeList[this.$store.getters.language]
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value(val) {
|
||||
if (!this.hasChange && this.hasInit) {
|
||||
this.$nextTick(() =>
|
||||
window.tinymce.get(this.tinymceId).setContent(val || ''))
|
||||
}
|
||||
},
|
||||
language() {
|
||||
this.destroyTinymce()
|
||||
this.$nextTick(() => this.initTinymce())
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.initTinymce()
|
||||
},
|
||||
activated() {
|
||||
this.initTinymce()
|
||||
},
|
||||
deactivated() {
|
||||
this.destroyTinymce()
|
||||
},
|
||||
destroyed() {
|
||||
this.destroyTinymce()
|
||||
},
|
||||
methods: {
|
||||
initTinymce() {
|
||||
const _this = this
|
||||
window.tinymce.init({
|
||||
language: this.language,
|
||||
selector: `#${this.tinymceId}`,
|
||||
height: this.height,
|
||||
body_class: 'panel-body ',
|
||||
object_resizing: false,
|
||||
toolbar: this.toolbar.length > 0 ? this.toolbar : toolbar,
|
||||
menubar: this.menubar,
|
||||
plugins: plugins,
|
||||
end_container_on_empty_block: true,
|
||||
powerpaste_word_import: 'clean',
|
||||
code_dialog_height: 450,
|
||||
code_dialog_width: 1000,
|
||||
advlist_bullet_styles: 'square',
|
||||
advlist_number_styles: 'default',
|
||||
imagetools_cors_hosts: ['www.tinymce.com', 'codepen.io'],
|
||||
default_link_target: '_blank',
|
||||
link_title: false,
|
||||
nonbreaking_force_tab: true, // inserting nonbreaking space need Nonbreaking Space Plugin
|
||||
init_instance_callback: editor => {
|
||||
if (_this.value) {
|
||||
editor.setContent(_this.value)
|
||||
}
|
||||
_this.hasInit = true
|
||||
editor.on('NodeChange Change KeyUp SetContent', () => {
|
||||
this.hasChange = true
|
||||
this.$emit('input', editor.getContent())
|
||||
})
|
||||
},
|
||||
setup(editor) {
|
||||
editor.on('FullscreenStateChanged', (e) => {
|
||||
_this.fullscreen = e.state
|
||||
})
|
||||
}
|
||||
// 整合七牛上传
|
||||
// images_dataimg_filter(img) {
|
||||
// setTimeout(() => {
|
||||
// const $image = $(img);
|
||||
// $image.removeAttr('width');
|
||||
// $image.removeAttr('height');
|
||||
// if ($image[0].height && $image[0].width) {
|
||||
// $image.attr('data-wscntype', 'image');
|
||||
// $image.attr('data-wscnh', $image[0].height);
|
||||
// $image.attr('data-wscnw', $image[0].width);
|
||||
// $image.addClass('wscnph');
|
||||
// }
|
||||
// }, 0);
|
||||
// return img
|
||||
// },
|
||||
// images_upload_handler(blobInfo, success, failure, progress) {
|
||||
// progress(0);
|
||||
// const token = _this.$store.getters.token;
|
||||
// getToken(token).then(response => {
|
||||
// const url = response.data.qiniu_url;
|
||||
// const formData = new FormData();
|
||||
// formData.append('token', response.data.qiniu_token);
|
||||
// formData.append('key', response.data.qiniu_key);
|
||||
// formData.append('file', blobInfo.blob(), url);
|
||||
// upload(formData).then(() => {
|
||||
// success(url);
|
||||
// progress(100);
|
||||
// })
|
||||
// }).catch(err => {
|
||||
// failure('出现未知问题,刷新页面,或者联系程序员')
|
||||
// console.log(err);
|
||||
// });
|
||||
// },
|
||||
})
|
||||
},
|
||||
destroyTinymce() {
|
||||
const tinymce = window.tinymce.get(this.tinymceId)
|
||||
if (this.fullscreen) {
|
||||
tinymce.execCommand('mceFullScreen')
|
||||
}
|
||||
|
||||
if (tinymce) {
|
||||
tinymce.destroy()
|
||||
}
|
||||
},
|
||||
setContent(value) {
|
||||
window.tinymce.get(this.tinymceId).setContent(value)
|
||||
},
|
||||
getContent() {
|
||||
window.tinymce.get(this.tinymceId).getContent()
|
||||
},
|
||||
imageSuccessCBK(arr) {
|
||||
const _this = this
|
||||
arr.forEach(v => {
|
||||
window.tinymce.get(_this.tinymceId).insertContent(`<img class="wscnph" src="${v.url}" >`)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tinymce-container {
|
||||
position: relative;
|
||||
line-height: normal;
|
||||
}
|
||||
.tinymce-container>>>.mce-fullscreen {
|
||||
z-index: 10000;
|
||||
}
|
||||
.tinymce-textarea {
|
||||
visibility: hidden;
|
||||
z-index: -1;
|
||||
}
|
||||
.editor-custom-btn-container {
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
top: 4px;
|
||||
/*z-index: 2005;*/
|
||||
}
|
||||
.fullscreen .editor-custom-btn-container {
|
||||
z-index: 10000;
|
||||
position: fixed;
|
||||
}
|
||||
.editor-upload-btn {
|
||||
display: inline-block;
|
||||
}
|
||||
</style>
|
|
@ -1,7 +0,0 @@
|
|||
// Any plugins you want to use has to be imported
|
||||
// Detail plugins list see https://www.tinymce.com/docs/plugins/
|
||||
// Custom builds see https://www.tinymce.com/download/custom-builds/
|
||||
|
||||
const plugins = ['advlist anchor autolink autosave code codesample colorpicker colorpicker contextmenu directionality emoticons fullscreen hr image imagetools insertdatetime link lists media nonbreaking noneditable pagebreak paste preview print save searchreplace spellchecker tabfocus table template textcolor textpattern visualblocks visualchars wordcount']
|
||||
|
||||
export default plugins
|
|
@ -1,6 +0,0 @@
|
|||
// Here is a list of the toolbar
|
||||
// Detail list see https://www.tinymce.com/docs/advanced/editor-control-identifiers/#toolbarcontrols
|
||||
|
||||
const toolbar = ['searchreplace bold italic underline strikethrough alignleft aligncenter alignright outdent indent blockquote undo redo removeformat subscript superscript code codesample', 'hr bullist numlist link image charmap preview anchor pagebreak insertdatetime media table emoticons forecolor backcolor fullscreen']
|
||||
|
||||
export default toolbar
|
|
@ -1,5 +1,5 @@
|
|||
import Vue from 'vue'
|
||||
import SvgIcon from '@/components/SvgIcon'// svg组件
|
||||
import SvgIcon from '@/components/element-ui/SvgIcon'// svg组件
|
||||
|
||||
// register globally
|
||||
Vue.component('svg-icon', SvgIcon)
|
||||
|
|
|
@ -10,7 +10,6 @@ export default {
|
|||
icons: 'Icons',
|
||||
components: 'Components',
|
||||
componentIndex: 'Introduction',
|
||||
tinymce: 'Tinymce',
|
||||
markdown: 'Markdown',
|
||||
jsonEditor: 'JSON Editor',
|
||||
dndList: 'Dnd List',
|
||||
|
@ -105,8 +104,7 @@ export default {
|
|||
},
|
||||
components: {
|
||||
documentation: 'Documentation',
|
||||
tinymceTips: 'Rich text editor is a core part of management system, but at the same time is a place with lots of problems. In the process of selecting rich texts, I also walked a lot of detours. The common rich text editors in the market are basically used, and the finally chose Tinymce. See documentation for more detailed rich text editor comparisons and introductions.',
|
||||
dropzoneTips: 'Because my business has special needs, and has to upload images to qiniu, so instead of a third party, I chose encapsulate it by myself. It is very simple, you can see the detail code in @/components/Dropzone.',
|
||||
dropzoneTips: 'Because my business has special needs, and has to upload images to qiniu, so instead of a third party, I chose encapsulate it by myself. It is very simple, you can see the detail code in @/components/element-ui/Dropzone.',
|
||||
stickyTips: 'when the page is scrolled to the preset position will be sticky on the top.',
|
||||
backToTopTips1: 'When the page is scrolled to the specified position, the Back to Top button appears in the lower right corner',
|
||||
backToTopTips2: 'You can customize the style of the button, show / hide, height of appearance, height of the return. If you need a text prompt, you can use element-ui el-tooltip elements externally',
|
||||
|
@ -177,6 +175,7 @@ export default {
|
|||
external: 'external',
|
||||
deactivated: 'deactivated',
|
||||
active: 'active',
|
||||
unconfirmed: 'unconfirmed',
|
||||
actions: 'Actions',
|
||||
activate: 'Activate',
|
||||
deactivate: 'Deactivate',
|
||||
|
@ -215,6 +214,8 @@ export default {
|
|||
addTagForMultipleUsersConfirmation: 'Are you sure you want to apply tag to all selected users?',
|
||||
removeTagFromMultipleUsersConfirmation: 'Are you sure you want to remove tag from all selected users?',
|
||||
requirePasswordResetConfirmation: 'Are you sure you want to require password reset for all selected users?',
|
||||
confirmAccountsConfirmation: 'Are you sure you want to confirm emails for all selected users?',
|
||||
resendEmailConfirmation: 'Are you sure you want to resend confirmation email for all selected users?',
|
||||
mailerMustBeEnabled: 'To require user\'s password reset you must enable mailer.',
|
||||
ok: 'Okay',
|
||||
completed: 'Completed',
|
||||
|
@ -232,17 +233,33 @@ export default {
|
|||
invalidNicknameError: 'Username can include "a-z", "A-Z" and "0-9" characters',
|
||||
getPasswordResetToken: 'Get password reset token',
|
||||
passwordResetTokenCreated: 'Password reset token was created',
|
||||
accountCreated: 'New account was created!'
|
||||
accountCreated: 'New account was created!',
|
||||
unconfirmedEmail: 'User didn\'t confirm the email',
|
||||
confirmAccount: 'Confirm account',
|
||||
confirmAccounts: 'Confirm accounts',
|
||||
resendConfirmation: 'Resend confirmation email'
|
||||
},
|
||||
statuses: {
|
||||
statuses: 'Statuses by instance',
|
||||
instanceFilter: 'Instance filter',
|
||||
loadMore: 'Load more',
|
||||
noInstances: 'No other instances found'
|
||||
},
|
||||
userProfile: {
|
||||
tags: 'Tags',
|
||||
moderator: 'Moderator',
|
||||
admin: 'Admin',
|
||||
local: 'Local',
|
||||
local: 'local',
|
||||
external: 'external',
|
||||
localUppercase: 'Local',
|
||||
nickname: 'Nickname',
|
||||
deactivated: 'Deactivated',
|
||||
recentStatuses: 'Recent Statues',
|
||||
showPrivateStatuses: 'Show private statuses'
|
||||
showPrivateStatuses: 'Show private statuses',
|
||||
roles: 'Roles',
|
||||
activeUppercase: 'Active',
|
||||
active: 'active',
|
||||
deactivated: 'deactivated',
|
||||
noStatuses: 'No statuses to show'
|
||||
},
|
||||
usersFilter: {
|
||||
inputPlaceholder: 'Select filter',
|
||||
|
@ -255,6 +272,7 @@ export default {
|
|||
},
|
||||
reports: {
|
||||
reports: 'Reports',
|
||||
groupedReports: 'Grouped reports',
|
||||
reply: 'Reply',
|
||||
from: 'From',
|
||||
showNotes: 'Show notes',
|
||||
|
@ -266,19 +284,35 @@ export default {
|
|||
deleteCompleted: 'Delete comleted',
|
||||
deleteCanceled: 'Delete canceled',
|
||||
noNotes: 'No notes to display',
|
||||
changeState: 'Change report state',
|
||||
changeState: "Change report's state",
|
||||
changeAllReports: 'Change all reports',
|
||||
changeScope: 'Change scope',
|
||||
moderateUser: 'Moderate user',
|
||||
resolve: 'Resolve',
|
||||
reopen: 'Reopen',
|
||||
close: 'Close',
|
||||
resolveAll: 'Resolve all',
|
||||
reopenAll: 'Reopen all',
|
||||
closeAll: 'Close all',
|
||||
addSensitive: 'Add Sensitive flag',
|
||||
removeSensitive: 'Remove Sensitive flag',
|
||||
public: 'Make status public',
|
||||
private: 'Make status private',
|
||||
unlisted: 'Make status unlisted',
|
||||
sensitive: 'Sensitive',
|
||||
deleteStatus: 'Delete status'
|
||||
deleteStatus: 'Delete status',
|
||||
reportOn: 'Report on',
|
||||
reportsOn: 'Reports on',
|
||||
id: 'ID',
|
||||
account: 'Account',
|
||||
actor: 'Actor',
|
||||
actors: 'Actors',
|
||||
content: 'Content',
|
||||
reportedStatus: 'Reported status',
|
||||
statusDeleted: 'This status has been deleted',
|
||||
leaveNote: 'Leave a note',
|
||||
postNote: 'Send',
|
||||
deleteNote: 'Delete'
|
||||
},
|
||||
reportsFilter: {
|
||||
inputPlaceholder: 'Select filter',
|
||||
|
@ -365,7 +399,13 @@ export default {
|
|||
deletePack: 'Delete pack',
|
||||
downloadSharedPack: 'Download shared pack to current instance',
|
||||
downloadAsOptional: 'Download as (optional)',
|
||||
downloadPackArchive: 'Download pack archive'
|
||||
downloadPackArchive: 'Download pack archive',
|
||||
successfullyDownloaded: 'Successfully downloaded',
|
||||
successfullyImported: 'Successfully imported',
|
||||
nowNewPacksToImport: 'No new packs to import',
|
||||
successfullyUpdated: 'Successfully updated',
|
||||
metadatLowerCase: 'metadata',
|
||||
files: 'files'
|
||||
},
|
||||
invites: {
|
||||
inviteTokens: 'Invite tokens',
|
||||
|
|
|
@ -10,7 +10,6 @@ export default {
|
|||
icons: 'Iconos',
|
||||
components: 'Componentes',
|
||||
componentIndex: 'Introducción',
|
||||
tinymce: 'Tinymce',
|
||||
markdown: 'Markdown',
|
||||
jsonEditor: 'Editor JSON',
|
||||
dndList: 'Lista Dnd',
|
||||
|
@ -96,8 +95,7 @@ export default {
|
|||
},
|
||||
components: {
|
||||
documentation: 'Documentación',
|
||||
tinymceTips: 'Rich text editor is a core part of management system, but at the same time is a place with lots of problems. In the process of selecting rich texts, I also walked a lot of detours. The common rich text editors in the market are basically used, and the finally chose Tinymce. See documentation for more detailed rich text editor comparisons and introductions.',
|
||||
dropzoneTips: 'Because my business has special needs, and has to upload images to qiniu, so instead of a third party, I chose encapsulate it by myself. It is very simple, you can see the detail code in @/components/Dropzone.',
|
||||
dropzoneTips: 'Because my business has special needs, and has to upload images to qiniu, so instead of a third party, I chose encapsulate it by myself. It is very simple, you can see the detail code in @/components/element-ui/Dropzone.',
|
||||
stickyTips: 'when the page is scrolled to the preset position will be sticky on the top.',
|
||||
backToTopTips1: 'When the page is scrolled to the specified position, the Back to Top button appears in the lower right corner',
|
||||
backToTopTips2: 'You can customize the style of the button, show / hide, height of appearance, height of the return. If you need a text prompt, you can use element-ui el-tooltip elements externally',
|
||||
|
|
|
@ -10,7 +10,6 @@ export default {
|
|||
icons: 'Icònas',
|
||||
components: 'Compausants',
|
||||
componentIndex: 'Introduccion',
|
||||
tinymce: 'Tinymce',
|
||||
markdown: 'Markdown',
|
||||
jsonEditor: 'JSON Editor',
|
||||
dndList: 'Dnd List',
|
||||
|
@ -97,8 +96,7 @@ export default {
|
|||
},
|
||||
components: {
|
||||
documentation: 'Documentacion',
|
||||
tinymceTips: 'Rich text editor is a core part of management system, but at the same time is a place with lots of problems. In the process of selecting rich texts, I also walked a lot of detours. The common rich text editors in the market are basically used, and the finally chose Tinymce. See documentation for more detailed rich text editor comparisons and introductions.',
|
||||
dropzoneTips: 'Because my business has special needs, and has to upload images to qiniu, so instead of a third party, I chose encapsulate it by myself. It is very simple, you can see the detail code in @/components/Dropzone.',
|
||||
dropzoneTips: 'Because my business has special needs, and has to upload images to qiniu, so instead of a third party, I chose encapsulate it by myself. It is very simple, you can see the detail code in @/components/element-ui/Dropzone.',
|
||||
stickyTips: 'when the page is scrolled to the preset position will be sticky on the top.',
|
||||
backToTopTips1: 'When the page is scrolled to the specified position, the Back to Top button appears in the lower right corner',
|
||||
backToTopTips2: 'You can customize the style of the button, show / hide, height of appearance, height of the return. If you need a text prompt, you can use element-ui el-tooltip elements externally',
|
||||
|
|
|
@ -10,7 +10,6 @@ export default {
|
|||
icons: '图标',
|
||||
components: '组件',
|
||||
componentIndex: '介绍',
|
||||
tinymce: '富文本编辑器',
|
||||
markdown: 'Markdown',
|
||||
jsonEditor: 'JSON编辑器',
|
||||
dndList: '列表拖拽',
|
||||
|
@ -96,8 +95,7 @@ export default {
|
|||
},
|
||||
components: {
|
||||
documentation: '文档',
|
||||
tinymceTips: '富文本是管理后台一个核心的功能,但同时又是一个有很多坑的地方。在选择富文本的过程中我也走了不少的弯路,市面上常见的富文本都基本用过了,最终权衡了一下选择了Tinymce。更详细的富文本比较和介绍见',
|
||||
dropzoneTips: '由于我司业务有特殊需求,而且要传七牛 所以没用第三方,选择了自己封装。代码非常的简单,具体代码你可以在这里看到 @/components/Dropzone',
|
||||
dropzoneTips: '由于我司业务有特殊需求,而且要传七牛 所以没用第三方,选择了自己封装。代码非常的简单,具体代码你可以在这里看到 @/components/element-ui/Dropzone',
|
||||
stickyTips: '当页面滚动到预设的位置会吸附在顶部',
|
||||
backToTopTips1: '页面滚动到指定位置会在右下角出现返回顶部按钮',
|
||||
backToTopTips2: '可自定义按钮的样式、show/hide、出现的高度、返回的位置 如需文字提示,可在外部使用Element的el-tooltip元素',
|
||||
|
|
|
@ -21,6 +21,20 @@ const settings = {
|
|||
]
|
||||
}
|
||||
|
||||
const statusesDisabled = disabledFeatures.includes('statuses')
|
||||
const statuses = {
|
||||
path: '/statuses',
|
||||
component: Layout,
|
||||
children: [
|
||||
{
|
||||
path: 'index',
|
||||
component: () => import('@/views/statuses/index'),
|
||||
name: 'Statuses',
|
||||
meta: { title: 'Statuses', icon: 'form', noCache: true }
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const reportsDisabled = disabledFeatures.includes('reports')
|
||||
const reports = {
|
||||
path: '/reports',
|
||||
|
@ -126,6 +140,7 @@ export const asyncRouterMap = [
|
|||
}
|
||||
]
|
||||
},
|
||||
...(statusesDisabled ? [] : [statuses]),
|
||||
...(reportsDisabled ? [] : [reports]),
|
||||
...(invitesDisabled ? [] : [invites]),
|
||||
...(moderationLogDisabled ? [] : [moderationLog]),
|
||||
|
|
|
@ -4,10 +4,12 @@ import app from './modules/app'
|
|||
import errorLog from './modules/errorLog'
|
||||
import moderationLog from './modules/moderationLog'
|
||||
import invites from './modules/invites'
|
||||
import peers from './modules/peers'
|
||||
import permission from './modules/permission'
|
||||
import relays from './modules/relays'
|
||||
import reports from './modules/reports'
|
||||
import settings from './modules/settings'
|
||||
import status from './modules/status'
|
||||
import tagsView from './modules/tagsView'
|
||||
import user from './modules/user'
|
||||
import userProfile from './modules/userProfile'
|
||||
|
@ -23,10 +25,12 @@ const store = new Vuex.Store({
|
|||
errorLog,
|
||||
moderationLog,
|
||||
invites,
|
||||
peers,
|
||||
permission,
|
||||
relays,
|
||||
reports,
|
||||
settings,
|
||||
status,
|
||||
tagsView,
|
||||
user,
|
||||
userProfile,
|
||||
|
|
|
@ -8,7 +8,7 @@ import {
|
|||
savePackMetadata,
|
||||
importFromFS,
|
||||
updatePackFile } from '@/api/emojiPacks'
|
||||
|
||||
import i18n from '@/lang'
|
||||
import { Message } from 'element-ui'
|
||||
|
||||
import Vue from 'vue'
|
||||
|
@ -44,34 +44,30 @@ const packs = {
|
|||
}
|
||||
},
|
||||
actions: {
|
||||
async SetLocalEmojiPacks({ commit, getters, state }) {
|
||||
const { data } = await listPacks(getters.authHost)
|
||||
commit('SET_LOCAL_PACKS', data)
|
||||
async CreatePack({ getters }, { name }) {
|
||||
await createPack(getters.authHost, getters.token, name)
|
||||
},
|
||||
async SetRemoteEmojiPacks({ commit, getters, state }, { remoteInstance }) {
|
||||
const { data } = await listRemotePacks(getters.authHost, getters.token, remoteInstance)
|
||||
|
||||
commit('SET_REMOTE_PACKS', data)
|
||||
async DeletePack({ getters }, { name }) {
|
||||
await deletePack(getters.authHost, getters.token, name)
|
||||
},
|
||||
async DownloadFrom({ commit, getters, state }, { instanceAddress, packName, as }) {
|
||||
async DownloadFrom({ getters }, { instanceAddress, packName, as }) {
|
||||
const result = await downloadFrom(getters.authHost, instanceAddress, packName, as, getters.token)
|
||||
|
||||
if (result.data === 'ok') {
|
||||
Message({
|
||||
message: `Successfully downloaded ${packName}`,
|
||||
message: `${i18n.t('settings.successfullyDownloaded')} ${packName}`,
|
||||
type: 'success',
|
||||
duration: 5 * 1000
|
||||
})
|
||||
}
|
||||
},
|
||||
async ReloadEmoji({ commit, getters, state }) {
|
||||
await reloadEmoji(getters.authHost, getters.token)
|
||||
},
|
||||
async ImportFromFS({ commit, getters, state }) {
|
||||
async ImportFromFS({ getters }) {
|
||||
const result = await importFromFS(getters.authHost, getters.token)
|
||||
|
||||
if (result.status === 200) {
|
||||
const message = result.data.length > 0 ? `Successfully imported ${result.data}` : 'No new packs to import'
|
||||
const message = result.data.length > 0
|
||||
? `${i18n.t('settings.successfullyImported')} ${result.data}`
|
||||
: i18n.t('settings.nowNewPacksToImport')
|
||||
|
||||
Message({
|
||||
message,
|
||||
|
@ -80,17 +76,9 @@ const packs = {
|
|||
})
|
||||
}
|
||||
},
|
||||
async DeletePack({ commit, getters, state }, { name }) {
|
||||
await deletePack(getters.authHost, getters.token, name)
|
||||
async ReloadEmoji({ getters }) {
|
||||
await reloadEmoji(getters.authHost, getters.token)
|
||||
},
|
||||
async CreatePack({ commit, getters, state }, { name }) {
|
||||
await createPack(getters.authHost, getters.token, name)
|
||||
},
|
||||
|
||||
async UpdateLocalPackVal({ commit, getters, state }, args) {
|
||||
commit('UPDATE_LOCAL_PACK_VAL', args)
|
||||
},
|
||||
|
||||
async SavePackMetadata({ commit, getters, state }, { packName }) {
|
||||
const result =
|
||||
await savePackMetadata(
|
||||
|
@ -102,7 +90,7 @@ const packs = {
|
|||
|
||||
if (result.status === 200) {
|
||||
Message({
|
||||
message: `Successfully updated ${packName} metadata`,
|
||||
message: `${i18n.t('settings.successfullyUpdated')} ${packName} ${i18n.t('settings.metadatLowerCase')}`,
|
||||
type: 'success',
|
||||
duration: 5 * 1000
|
||||
})
|
||||
|
@ -110,21 +98,32 @@ const packs = {
|
|||
commit('UPDATE_LOCAL_PACK_PACK', { name: packName, pack: result.data })
|
||||
}
|
||||
},
|
||||
async SetLocalEmojiPacks({ commit, getters }) {
|
||||
const { data } = await listPacks(getters.authHost)
|
||||
commit('SET_LOCAL_PACKS', data)
|
||||
},
|
||||
async SetRemoteEmojiPacks({ commit, getters }, { remoteInstance }) {
|
||||
const { data } = await listRemotePacks(getters.authHost, getters.token, remoteInstance)
|
||||
|
||||
async UpdateAndSavePackFile({ commit, getters, state }, args) {
|
||||
commit('SET_REMOTE_PACKS', data)
|
||||
},
|
||||
async UpdateAndSavePackFile({ commit, getters }, args) {
|
||||
const result = await updatePackFile(getters.authHost, getters.token, args)
|
||||
|
||||
if (result.status === 200) {
|
||||
const { packName } = args
|
||||
|
||||
Message({
|
||||
message: `Successfully updated ${packName} files`,
|
||||
message: `${i18n.t('settings.successfullyUpdated')} ${packName} ${i18n.t('settings.metadatLowerCase')}`,
|
||||
type: 'success',
|
||||
duration: 5 * 1000
|
||||
})
|
||||
|
||||
commit('UPDATE_LOCAL_PACK_FILES', { name: packName, files: result.data })
|
||||
}
|
||||
},
|
||||
async UpdateLocalPackVal({ commit }, args) {
|
||||
commit('UPDATE_LOCAL_PACK_VAL', args)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import { generateInviteToken, inviteViaEmail, listInviteTokens, revokeToken } from '@/api/invites'
|
||||
import { Message } from 'element-ui'
|
||||
import i18n from '@/lang'
|
||||
|
||||
const invites = {
|
||||
state: {
|
||||
|
@ -25,18 +27,35 @@ const invites = {
|
|||
commit('SET_LOADING', false)
|
||||
},
|
||||
async GenerateInviteToken({ commit, dispatch, getters }, { maxUse, expiresAt }) {
|
||||
const { data } = await generateInviteToken(maxUse, expiresAt, getters.authHost, getters.token)
|
||||
commit('SET_NEW_TOKEN', { token: data.token, maxUse: data.max_use, expiresAt: data.expires_at })
|
||||
try {
|
||||
const { data } = await generateInviteToken(maxUse, expiresAt, getters.authHost, getters.token)
|
||||
commit('SET_NEW_TOKEN', { token: data.token, maxUse: data.max_use, expiresAt: data.expires_at })
|
||||
} catch (_e) {
|
||||
return
|
||||
}
|
||||
dispatch('FetchInviteTokens')
|
||||
},
|
||||
async InviteUserViaEmail({ commit, dispatch, getters }, { email, name }) {
|
||||
await inviteViaEmail(email, name, getters.authHost, getters.token)
|
||||
try {
|
||||
await inviteViaEmail(email, name, getters.authHost, getters.token)
|
||||
} catch (_e) {
|
||||
return
|
||||
}
|
||||
Message({
|
||||
message: i18n.t('invites.emailSent'),
|
||||
type: 'success',
|
||||
duration: 5 * 1000
|
||||
})
|
||||
},
|
||||
RemoveNewToken({ commit }) {
|
||||
commit('SET_NEW_TOKEN', {})
|
||||
},
|
||||
async RevokeToken({ commit, dispatch, getters }, token) {
|
||||
await revokeToken(token, getters.authHost, getters.token)
|
||||
try {
|
||||
await revokeToken(token, getters.authHost, getters.token)
|
||||
} catch (_e) {
|
||||
return
|
||||
}
|
||||
dispatch('FetchInviteTokens')
|
||||
}
|
||||
}
|
||||
|
|
28
src/store/modules/peers.js
Normal file
28
src/store/modules/peers.js
Normal file
|
@ -0,0 +1,28 @@
|
|||
import { fetchPeers } from '@/api/peers'
|
||||
|
||||
const peers = {
|
||||
state: {
|
||||
fetchedPeers: [],
|
||||
loading: true
|
||||
},
|
||||
|
||||
mutations: {
|
||||
SET_PEERS: (state, peers) => {
|
||||
state.fetchedPeers = peers
|
||||
},
|
||||
SET_LOADING: (state, status) => {
|
||||
state.loading = status
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
async FetchPeers({ commit, getters }) {
|
||||
const peers = await fetchPeers(getters.authHost, getters.token)
|
||||
|
||||
commit('SET_PEERS', peers.data)
|
||||
commit('SET_LOADING', false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default peers
|
|
@ -28,15 +28,27 @@ const relays = {
|
|||
commit('SET_RELAYS', response.data.relays)
|
||||
commit('SET_LOADING', false)
|
||||
},
|
||||
async AddRelay({ commit, getters }, relay) {
|
||||
async AddRelay({ commit, dispatch, getters }, relay) {
|
||||
commit('ADD_RELAY', relay)
|
||||
|
||||
await addRelay(relay, getters.authHost, getters.token)
|
||||
try {
|
||||
await addRelay(relay, getters.authHost, getters.token)
|
||||
} catch (_e) {
|
||||
return
|
||||
} finally {
|
||||
dispatch('FetchRelays')
|
||||
}
|
||||
},
|
||||
async DeleteRelay({ commit, getters }, relay) {
|
||||
async DeleteRelay({ commit, dispatch, getters }, relay) {
|
||||
commit('DELETE_RELAY', relay)
|
||||
|
||||
await deleteRelay(relay, getters.authHost, getters.token)
|
||||
try {
|
||||
await deleteRelay(relay, getters.authHost, getters.token)
|
||||
} catch (_e) {
|
||||
return
|
||||
} finally {
|
||||
dispatch('FetchRelays')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
import { changeState, changeStatusScope, deleteStatus, fetchReports, filterReports } from '@/api/reports'
|
||||
import { changeState, fetchReports, fetchGroupedReports, createNote, deleteNote } from '@/api/reports'
|
||||
|
||||
const reports = {
|
||||
state: {
|
||||
fetchedReports: [],
|
||||
idOfLastReport: '',
|
||||
page_limit: 5,
|
||||
fetchedGroupedReports: [],
|
||||
totalReportsCount: 0,
|
||||
currentPage: 1,
|
||||
pageSize: 50,
|
||||
groupReports: false,
|
||||
stateFilter: '',
|
||||
loading: true
|
||||
},
|
||||
|
@ -15,63 +18,104 @@ const reports = {
|
|||
SET_LOADING: (state, status) => {
|
||||
state.loading = status
|
||||
},
|
||||
SET_PAGE: (state, page) => {
|
||||
state.currentPage = page
|
||||
},
|
||||
SET_REPORTS: (state, reports) => {
|
||||
state.fetchedReports = reports
|
||||
},
|
||||
SET_GROUPED_REPORTS: (state, reports) => {
|
||||
state.fetchedGroupedReports = reports
|
||||
},
|
||||
SET_REPORTS_COUNT: (state, total) => {
|
||||
state.totalReportsCount = total
|
||||
},
|
||||
SET_REPORTS_FILTER: (state, filter) => {
|
||||
state.stateFilter = filter
|
||||
},
|
||||
SET_REPORTS_GROUPING: (state) => {
|
||||
state.groupReports = !state.groupReports
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
async ChangeReportState({ commit, getters, state }, { reportState, reportId }) {
|
||||
const { data } = await changeState(reportState, reportId, getters.authHost, getters.token)
|
||||
const updatedReports = state.fetchedReports.map(report => report.id === reportId ? data : report)
|
||||
commit('SET_REPORTS', updatedReports)
|
||||
},
|
||||
async ChangeStatusScope({ commit, getters, state }, { statusId, isSensitive, visibility, reportId }) {
|
||||
const { data } = await changeStatusScope(statusId, isSensitive, visibility, getters.authHost, getters.token)
|
||||
async ChangeReportState({ commit, getters, state }, reportsData) {
|
||||
changeState(reportsData, getters.authHost, getters.token)
|
||||
|
||||
const updatedReports = state.fetchedReports.map(report => {
|
||||
if (report.id === reportId) {
|
||||
const statuses = report.statuses.map(status => status.id === statusId ? data : status)
|
||||
return { ...report, statuses }
|
||||
} else {
|
||||
return report
|
||||
}
|
||||
const updatedReportsIds = reportsData.map(({ id }) => id)
|
||||
return updatedReportsIds.includes(report.id) ? { ...report, state: reportsData[0].state } : report
|
||||
})
|
||||
|
||||
const updatedGroupedReports = state.fetchedGroupedReports.map(group => {
|
||||
const updatedReportsIds = reportsData.map(({ id }) => id)
|
||||
const updatedReports = group.reports.map(report => updatedReportsIds.includes(report.id) ? { ...report, state: reportsData[0].state } : report)
|
||||
return { ...group, reports: updatedReports }
|
||||
})
|
||||
|
||||
commit('SET_REPORTS', updatedReports)
|
||||
commit('SET_GROUPED_REPORTS', updatedGroupedReports)
|
||||
},
|
||||
ClearFetchedReports({ commit }) {
|
||||
commit('SET_REPORTS', [])
|
||||
commit('SET_LAST_REPORT_ID', '')
|
||||
},
|
||||
async DeleteStatus({ commit, getters, state }, { statusId, reportId }) {
|
||||
deleteStatus(statusId, getters.authHost, getters.token)
|
||||
const updatedReports = state.fetchedReports.map(report => {
|
||||
if (report.id === reportId) {
|
||||
const statuses = report.statuses.filter(status => status.id !== statusId)
|
||||
return { ...report, statuses }
|
||||
} else {
|
||||
return report
|
||||
}
|
||||
})
|
||||
commit('SET_REPORTS', updatedReports)
|
||||
},
|
||||
async FetchReports({ commit, getters, state }) {
|
||||
async FetchReports({ commit, getters, state }, page) {
|
||||
commit('SET_LOADING', true)
|
||||
const { data } = await fetchReports(state.stateFilter, page, state.pageSize, getters.authHost, getters.token)
|
||||
|
||||
const response = state.stateFilter.length === 0
|
||||
? await fetchReports(state.page_limit, state.idOfLastReport, getters.authHost, getters.token)
|
||||
: await filterReports(state.stateFilter, state.page_limit, state.idOfLastReport, getters.authHost, getters.token)
|
||||
commit('SET_REPORTS', data.reports)
|
||||
commit('SET_REPORTS_COUNT', data.total)
|
||||
commit('SET_PAGE', page)
|
||||
commit('SET_LOADING', false)
|
||||
},
|
||||
async FetchGroupedReports({ commit, getters }) {
|
||||
commit('SET_LOADING', true)
|
||||
const { data } = await fetchGroupedReports(getters.authHost, getters.token)
|
||||
|
||||
const reports = state.fetchedReports.concat(response.data.reports)
|
||||
const id = reports.length > 0 ? reports[reports.length - 1].id : state.idOfLastReport
|
||||
|
||||
commit('SET_REPORTS', reports)
|
||||
commit('SET_LAST_REPORT_ID', id)
|
||||
commit('SET_GROUPED_REPORTS', data.reports)
|
||||
commit('SET_LOADING', false)
|
||||
},
|
||||
SetFilter({ commit }, filter) {
|
||||
commit('SET_REPORTS_FILTER', filter)
|
||||
},
|
||||
ToggleReportsGrouping({ commit }) {
|
||||
commit('SET_REPORTS_GROUPING')
|
||||
},
|
||||
CreateReportNote({ commit, getters, state, rootState }, { content, reportID }) {
|
||||
createNote(content, reportID, getters.authHost, getters.token)
|
||||
|
||||
const optimisticNote = {
|
||||
user: {
|
||||
avatar: rootState.user.avatar,
|
||||
display_name: rootState.user.name,
|
||||
url: `${rootState.user.authHost}/${rootState.user.name}`,
|
||||
acct: rootState.user.name
|
||||
},
|
||||
content: content,
|
||||
created_at: new Date().getTime()
|
||||
}
|
||||
|
||||
const updatedReports = state.fetchedReports.map(report => {
|
||||
if (report.id === reportID) {
|
||||
report.notes = [...report.notes, optimisticNote]
|
||||
}
|
||||
|
||||
return report
|
||||
})
|
||||
|
||||
commit('SET_REPORTS', updatedReports)
|
||||
},
|
||||
DeleteReportNote({ commit, getters, state }, { noteID, reportID }) {
|
||||
deleteNote(noteID, reportID, getters.authHost, getters.token)
|
||||
|
||||
const updatedReports = state.fetchedReports.map(report => {
|
||||
if (report.id === reportID) {
|
||||
report.notes = report.notes.filter(note => note.id !== noteID)
|
||||
}
|
||||
|
||||
return report
|
||||
})
|
||||
|
||||
commit('SET_REPORTS', updatedReports)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
57
src/store/modules/status.js
Normal file
57
src/store/modules/status.js
Normal file
|
@ -0,0 +1,57 @@
|
|||
import { changeStatusScope, deleteStatus, fetchStatusesByInstance } from '@/api/status'
|
||||
|
||||
const status = {
|
||||
state: {
|
||||
fetchedStatuses: [],
|
||||
loading: false
|
||||
},
|
||||
mutations: {
|
||||
SET_STATUSES: (state, statuses) => {
|
||||
state.fetchedStatuses = statuses
|
||||
},
|
||||
PUSH_STATUSES: (state, statuses) => {
|
||||
state.fetchedStatuses = [...state.fetchedStatuses, ...statuses]
|
||||
},
|
||||
SET_LOADING: (state, status) => {
|
||||
state.loading = status
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
async ChangeStatusScope({ dispatch, getters }, { statusId, isSensitive, visibility, reportCurrentPage, userId, godmode }) {
|
||||
await changeStatusScope(statusId, isSensitive, visibility, getters.authHost, getters.token)
|
||||
if (reportCurrentPage !== 0) { // called from Reports
|
||||
dispatch('FetchReports', reportCurrentPage)
|
||||
} else if (userId.length > 0) { // called from User profile
|
||||
dispatch('FetchUserStatuses', { userId, godmode })
|
||||
} else { // called from GroupedReports
|
||||
dispatch('FetchGroupedReports')
|
||||
}
|
||||
},
|
||||
async DeleteStatus({ dispatch, getters }, { statusId, reportCurrentPage, userId, godmode }) {
|
||||
await deleteStatus(statusId, getters.authHost, getters.token)
|
||||
if (reportCurrentPage !== 0) { // called from Reports
|
||||
dispatch('FetchReports', reportCurrentPage)
|
||||
} else if (userId.length > 0) { // called from User profile
|
||||
dispatch('FetchUserStatuses', { userId, godmode })
|
||||
} else { // called from GroupedReports
|
||||
dispatch('FetchGroupedReports')
|
||||
}
|
||||
},
|
||||
async FetchStatusesByInstance({ commit, getters }, { instance, page, pageSize }) {
|
||||
commit('SET_LOADING', true)
|
||||
const statuses = await fetchStatusesByInstance(instance, getters.authHost, getters.token, pageSize, page)
|
||||
|
||||
commit('SET_STATUSES', statuses.data)
|
||||
commit('SET_LOADING', false)
|
||||
},
|
||||
async FetchStatusesPageByInstance({ commit, getters }, { instance, page, pageSize }) {
|
||||
commit('SET_LOADING', true)
|
||||
const statuses = await fetchStatusesByInstance(instance, getters.authHost, getters.token, pageSize, page)
|
||||
|
||||
commit('PUSH_STATUSES', statuses.data)
|
||||
commit('SET_LOADING', false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default status
|
|
@ -2,33 +2,42 @@ import { fetchUser, fetchUserStatuses } from '@/api/users'
|
|||
|
||||
const userProfile = {
|
||||
state: {
|
||||
statuses: [],
|
||||
statusesLoading: true,
|
||||
user: {},
|
||||
loading: true,
|
||||
statuses: []
|
||||
userProfileLoading: true
|
||||
},
|
||||
mutations: {
|
||||
SET_STATUSES: (state, statuses) => {
|
||||
state.statuses = statuses
|
||||
},
|
||||
SET_STATUSES_LOADING: (state, status) => {
|
||||
state.statusesLoading = status
|
||||
},
|
||||
SET_USER: (state, user) => {
|
||||
state.user = user
|
||||
},
|
||||
SET_LOADING: (state, status) => {
|
||||
state.loading = status
|
||||
},
|
||||
SET_STATUSES: (state, statuses) => {
|
||||
state.statuses = statuses
|
||||
SET_USER_PROFILE_LOADING: (state, status) => {
|
||||
state.userProfileLoading = status
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
async FetchData({ commit, getters }, { id, godmode }) {
|
||||
commit('SET_LOADING', true)
|
||||
|
||||
const [userResponse, statusesResponse] = await Promise.all([
|
||||
fetchUser(id, getters.authHost, getters.token),
|
||||
fetchUserStatuses(id, getters.authHost, godmode, getters.token)
|
||||
])
|
||||
async FetchUserProfile({ commit, dispatch, getters }, { userId, godmode }) {
|
||||
commit('SET_USER_PROFILE_LOADING', true)
|
||||
|
||||
const userResponse = await fetchUser(userId, getters.authHost, getters.token)
|
||||
commit('SET_USER', userResponse.data)
|
||||
commit('SET_STATUSES', statusesResponse.data)
|
||||
commit('SET_LOADING', false)
|
||||
commit('SET_USER_PROFILE_LOADING', false)
|
||||
|
||||
dispatch('FetchUserStatuses', { userId, godmode })
|
||||
},
|
||||
async FetchUserStatuses({ commit, getters }, { userId, godmode }) {
|
||||
commit('SET_STATUSES_LOADING', true)
|
||||
|
||||
const statuses = await fetchUserStatuses(userId, getters.authHost, godmode, getters.token)
|
||||
|
||||
commit('SET_STATUSES', statuses.data)
|
||||
commit('SET_STATUSES_LOADING', false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { Message } from 'element-ui'
|
||||
import i18n from '@/lang'
|
||||
import {
|
||||
activateUsers,
|
||||
addRight,
|
||||
|
@ -10,7 +12,9 @@ import {
|
|||
searchUsers,
|
||||
tagUser,
|
||||
untagUser,
|
||||
requirePasswordReset
|
||||
requirePasswordReset,
|
||||
confirmUserEmail,
|
||||
resendConfirmationEmail
|
||||
} from '@/api/users'
|
||||
|
||||
const users = {
|
||||
|
@ -43,6 +47,10 @@ const users = {
|
|||
return acc.filter(u => u.id !== user.id)
|
||||
}, state.fetchedUsers)
|
||||
|
||||
if (state.fetchedUsers.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
state.fetchedUsers = [...usersWithoutSwapped, ...users].sort((a, b) =>
|
||||
a.nickname.localeCompare(b.nickname)
|
||||
)
|
||||
|
@ -78,8 +86,14 @@ const users = {
|
|||
commit('SWAP_USERS', updatedUsers)
|
||||
|
||||
const usersNicknames = users.map(user => user.nickname)
|
||||
await activateUsers(usersNicknames, getters.authHost, getters.token)
|
||||
dispatch('SearchUsers', { query: state.searchQuery, page: state.currentPage })
|
||||
try {
|
||||
await activateUsers(usersNicknames, getters.authHost, getters.token)
|
||||
} catch (_e) {
|
||||
return
|
||||
} finally {
|
||||
dispatch('SearchUsers', { query: state.searchQuery, page: state.currentPage })
|
||||
}
|
||||
dispatch('SuccessMessage')
|
||||
},
|
||||
async AddRight({ commit, dispatch, getters, state }, { users, right }) {
|
||||
const updatedUsers = users.map(user => {
|
||||
|
@ -88,8 +102,14 @@ const users = {
|
|||
commit('SWAP_USERS', updatedUsers)
|
||||
|
||||
const usersNicknames = users.map(user => user.nickname)
|
||||
await addRight(usersNicknames, right, getters.authHost, getters.token)
|
||||
dispatch('SearchUsers', { query: state.searchQuery, page: state.currentPage })
|
||||
try {
|
||||
await addRight(usersNicknames, right, getters.authHost, getters.token)
|
||||
} catch (_e) {
|
||||
return
|
||||
} finally {
|
||||
dispatch('SearchUsers', { query: state.searchQuery, page: state.currentPage })
|
||||
}
|
||||
dispatch('SuccessMessage')
|
||||
},
|
||||
async AddTag({ commit, dispatch, getters, state }, { users, tag }) {
|
||||
const updatedUsers = users.map(user => {
|
||||
|
@ -98,16 +118,28 @@ const users = {
|
|||
commit('SWAP_USERS', updatedUsers)
|
||||
|
||||
const nicknames = users.map(user => user.nickname)
|
||||
await tagUser(nicknames, [tag], getters.authHost, getters.token)
|
||||
dispatch('SearchUsers', { query: state.searchQuery, page: state.currentPage })
|
||||
try {
|
||||
await tagUser(nicknames, [tag], getters.authHost, getters.token)
|
||||
} catch (_e) {
|
||||
return
|
||||
} finally {
|
||||
dispatch('SearchUsers', { query: state.searchQuery, page: state.currentPage })
|
||||
}
|
||||
dispatch('SuccessMessage')
|
||||
},
|
||||
async ClearFilters({ commit, dispatch, state }) {
|
||||
commit('CLEAR_USERS_FILTERS')
|
||||
dispatch('SearchUsers', { query: state.searchQuery, page: 1 })
|
||||
},
|
||||
async CreateNewAccount({ dispatch, getters, state }, { nickname, email, password }) {
|
||||
await createNewAccount(nickname, email, password, getters.authHost, getters.token)
|
||||
dispatch('SearchUsers', { query: state.searchQuery, page: state.currentPage })
|
||||
try {
|
||||
await createNewAccount(nickname, email, password, getters.authHost, getters.token)
|
||||
} catch (_e) {
|
||||
return
|
||||
} finally {
|
||||
dispatch('SearchUsers', { query: state.searchQuery, page: state.currentPage })
|
||||
}
|
||||
dispatch('SuccessMessage')
|
||||
},
|
||||
async DeactivateUsers({ commit, dispatch, getters, state }, users) {
|
||||
const updatedUsers = users.map(user => {
|
||||
|
@ -116,8 +148,39 @@ const users = {
|
|||
commit('SWAP_USERS', updatedUsers)
|
||||
|
||||
const usersNicknames = users.map(user => user.nickname)
|
||||
await deactivateUsers(usersNicknames, getters.authHost, getters.token)
|
||||
dispatch('SearchUsers', { query: state.searchQuery, page: state.currentPage })
|
||||
try {
|
||||
await deactivateUsers(usersNicknames, getters.authHost, getters.token)
|
||||
} catch (_e) {
|
||||
return
|
||||
} finally {
|
||||
dispatch('SearchUsers', { query: state.searchQuery, page: state.currentPage })
|
||||
}
|
||||
dispatch('SuccessMessage')
|
||||
},
|
||||
async ConfirmUsersEmail({ commit, dispatch, getters, state }, users) {
|
||||
const updatedUsers = users.map(user => {
|
||||
return { ...user, confirmation_pending: false }
|
||||
})
|
||||
commit('SWAP_USERS', updatedUsers)
|
||||
|
||||
const usersNicknames = users.map(user => user.nickname)
|
||||
try {
|
||||
await confirmUserEmail(usersNicknames, getters.authHost, getters.token)
|
||||
} catch (_e) {
|
||||
return
|
||||
} finally {
|
||||
dispatch('SearchUsers', { query: state.searchQuery, page: state.currentPage })
|
||||
}
|
||||
dispatch('SuccessMessage')
|
||||
},
|
||||
async ResendConfirmationEmail({ dispatch, getters }, users) {
|
||||
const usersNicknames = users.map(user => user.nickname)
|
||||
try {
|
||||
await resendConfirmationEmail(usersNicknames, getters.authHost, getters.token)
|
||||
} catch (_e) {
|
||||
return
|
||||
}
|
||||
dispatch('SuccessMessage')
|
||||
},
|
||||
async DeleteRight({ commit, dispatch, getters, state }, { users, right }) {
|
||||
const updatedUsers = users.map(user => {
|
||||
|
@ -126,19 +189,26 @@ const users = {
|
|||
commit('SWAP_USERS', updatedUsers)
|
||||
|
||||
const usersNicknames = users.map(user => user.nickname)
|
||||
await deleteRight(usersNicknames, right, getters.authHost, getters.token)
|
||||
dispatch('SearchUsers', { query: state.searchQuery, page: state.currentPage })
|
||||
try {
|
||||
await deleteRight(usersNicknames, right, getters.authHost, getters.token)
|
||||
} catch (_e) {
|
||||
return
|
||||
} finally {
|
||||
dispatch('SearchUsers', { query: state.searchQuery, page: state.currentPage })
|
||||
}
|
||||
dispatch('SuccessMessage')
|
||||
},
|
||||
async DeleteUsers({ commit, getters, state }, users) {
|
||||
async DeleteUsers({ commit, dispatch, getters, state }, users) {
|
||||
const usersNicknames = users.map(user => user.nickname)
|
||||
try {
|
||||
await deleteUsers(usersNicknames, getters.authHost, getters.token)
|
||||
} catch (_e) {
|
||||
return
|
||||
}
|
||||
const deletedUsersIds = users.map(deletedUser => deletedUser.id)
|
||||
const updatedUsers = state.fetchedUsers.filter(user => !deletedUsersIds.includes(user.id))
|
||||
commit('SET_USERS', updatedUsers)
|
||||
|
||||
const usersNicknames = users.map(user => user.nickname)
|
||||
await deleteUsers(usersNicknames, getters.authHost, getters.token)
|
||||
},
|
||||
async RequirePasswordReset({ getters }, user) {
|
||||
await requirePasswordReset(user.nickname, getters.authHost, getters.token)
|
||||
dispatch('SuccessMessage')
|
||||
},
|
||||
async FetchUsers({ commit, dispatch, getters, state }, { page }) {
|
||||
commit('SET_LOADING', true)
|
||||
|
@ -161,8 +231,22 @@ const users = {
|
|||
commit('SWAP_USERS', updatedUsers)
|
||||
|
||||
const nicknames = users.map(user => user.nickname)
|
||||
await untagUser(nicknames, [tag], getters.authHost, getters.token)
|
||||
dispatch('SearchUsers', { query: state.searchQuery, page: state.currentPage })
|
||||
try {
|
||||
await untagUser(nicknames, [tag], getters.authHost, getters.token)
|
||||
} catch (_e) {
|
||||
return
|
||||
} finally {
|
||||
dispatch('SearchUsers', { query: state.searchQuery, page: state.currentPage })
|
||||
}
|
||||
dispatch('SuccessMessage')
|
||||
},
|
||||
async RequirePasswordReset({ dispatch, getters }, user) {
|
||||
try {
|
||||
await requirePasswordReset(user.nickname, getters.authHost, getters.token)
|
||||
} catch (_e) {
|
||||
return
|
||||
}
|
||||
dispatch('SuccessMessage')
|
||||
},
|
||||
async SearchUsers({ commit, dispatch, state, getters }, { query, page }) {
|
||||
if (query.length === 0) {
|
||||
|
@ -178,6 +262,12 @@ const users = {
|
|||
loadUsers(commit, page, response.data)
|
||||
}
|
||||
},
|
||||
SuccessMessage() {
|
||||
Message.success({
|
||||
message: i18n.t('users.completed'),
|
||||
duration: 5 * 1000
|
||||
})
|
||||
},
|
||||
async ToggleUsersFilter({ commit, dispatch, state }, filters) {
|
||||
const defaultFilters = {
|
||||
local: false,
|
||||
|
|
|
@ -11,6 +11,8 @@ body {
|
|||
-webkit-font-smoothing: antialiased;
|
||||
text-rendering: optimizeLegibility;
|
||||
font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif;
|
||||
background: #FFF;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
label {
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import Chart from '@/components/Charts/keyboard'
|
||||
import Chart from '@/components/element-ui/Charts/keyboard'
|
||||
|
||||
export default {
|
||||
name: 'KeyboardChart',
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import Chart from '@/components/Charts/lineMarker'
|
||||
import Chart from '@/components/element-ui/Charts/lineMarker'
|
||||
|
||||
export default {
|
||||
name: 'LineChart',
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import Chart from '@/components/Charts/mixChart'
|
||||
import Chart from '@/components/element-ui/Charts/mixChart'
|
||||
|
||||
export default {
|
||||
name: 'MixChart',
|
||||
|
|
|
@ -28,8 +28,8 @@
|
|||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
import PanThumb from '@/components/PanThumb'
|
||||
import Mallki from '@/components/TextHoverEffect/Mallki'
|
||||
import PanThumb from '@/components/element-ui/PanThumb'
|
||||
import Mallki from '@/components/element-ui/TextHoverEffect/Mallki'
|
||||
|
||||
export default {
|
||||
components: { PanThumb, Mallki },
|
||||
|
|
|
@ -43,7 +43,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import GithubCorner from '@/components/GithubCorner'
|
||||
import GithubCorner from '@/components/element-ui/GithubCorner'
|
||||
import PanelGroup from './components/PanelGroup'
|
||||
import LineChart from './components/LineChart'
|
||||
import RaddarChart from './components/RaddarChart'
|
||||
|
|
|
@ -18,8 +18,8 @@
|
|||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
import PanThumb from '@/components/PanThumb'
|
||||
import GithubCorner from '@/components/GithubCorner'
|
||||
import PanThumb from '@/components/element-ui/PanThumb'
|
||||
import GithubCorner from '@/components/element-ui/GithubCorner'
|
||||
|
||||
export default {
|
||||
name: 'DashboardEditor',
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import DropdownMenu from '@/components/Share/dropdownMenu'
|
||||
import DropdownMenu from '@/components/element-ui/Share/dropdownMenu'
|
||||
|
||||
export default {
|
||||
name: 'Documentation',
|
||||
|
|
|
@ -192,17 +192,8 @@ export default {
|
|||
async inviteUserViaEmail() {
|
||||
this.$refs['inviteUserForm'].validate(async(valid) => {
|
||||
if (valid) {
|
||||
try {
|
||||
await this.$store.dispatch('InviteUserViaEmail', this.$data.inviteUserForm)
|
||||
} catch (_e) {
|
||||
return
|
||||
} finally {
|
||||
this.closeDialogWindow()
|
||||
}
|
||||
this.$message({
|
||||
type: 'success',
|
||||
message: this.$t('invites.emailSent')
|
||||
})
|
||||
await this.$store.dispatch('InviteUserViaEmail', this.$data.inviteUserForm)
|
||||
this.closeDialogWindow()
|
||||
} else {
|
||||
this.$message({
|
||||
type: 'error',
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
import Hamburger from '@/components/Hamburger'
|
||||
import Hamburger from '@/components/element-ui/Hamburger'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import ScrollPane from '@/components/ScrollPane'
|
||||
import ScrollPane from '@/components/element-ui/ScrollPane'
|
||||
import { generateTitle } from '@/utils/i18n'
|
||||
import path from 'path'
|
||||
|
||||
|
|
|
@ -41,6 +41,7 @@
|
|||
<el-button :loading="loading" class="login-button" type="primary" @click.native.prevent="handleLogin">
|
||||
{{ $t('login.logIn') }}
|
||||
</el-button>
|
||||
<!-- Note: PleromaFE login feature relies on admin scope presence in PleromaFE token (older versions of PleromaFE don't support it) -->
|
||||
<el-button v-if="pleromaFEToken" :loading="loadingPleromaFE" class="login-button" type="primary" @click.native.prevent="handlePleromaFELogin">
|
||||
{{ $t('login.logInViaPleromaFE') }}
|
||||
</el-button>
|
||||
|
@ -49,7 +50,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import SvgIcon from '@/components/SvgIcon'
|
||||
import SvgIcon from '@/components/element-ui/SvgIcon'
|
||||
import localforage from 'localforage'
|
||||
import _ from 'lodash'
|
||||
import i18n from '@/lang'
|
||||
|
|
143
src/views/reports/components/GroupedReport.vue
Normal file
143
src/views/reports/components/GroupedReport.vue
Normal file
|
@ -0,0 +1,143 @@
|
|||
<template>
|
||||
<el-timeline class="timeline">
|
||||
<el-timeline-item
|
||||
v-for="groupedReport in groupedReports"
|
||||
:key="groupedReport.id"
|
||||
:timestamp="parseTimestamp(groupedReport.date)"
|
||||
placement="top"
|
||||
class="timeline-item-container">
|
||||
<el-card class="grouped-report">
|
||||
<div class="header-container">
|
||||
<div>
|
||||
<h3 class="report-title">{{ $t('reports.reportsOn') }} {{ groupedReport.account.display_name }}</h3>
|
||||
</div>
|
||||
<div>
|
||||
<el-dropdown trigger="click">
|
||||
<el-button plain size="small" icon="el-icon-edit">{{ $t('reports.changeAllReports') }}<i class="el-icon-arrow-down el-icon--right"/></el-button>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item @click.native="changeAllReports('resolved', groupedReport.reports)">{{ $t('reports.resolveAll') }}</el-dropdown-item>
|
||||
<el-dropdown-item @click.native="changeAllReports('open', groupedReport.reports)">{{ $t('reports.reopenAll') }}</el-dropdown-item>
|
||||
<el-dropdown-item @click.native="changeAllReports('closed', groupedReport.reports)">{{ $t('reports.closeAll') }}</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
<moderate-user-dropdown :account="groupedReport.account"/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="line"/>
|
||||
<span class="report-row-key">{{ $t('reports.account') }}:</span>
|
||||
<img
|
||||
:src="groupedReport.account.avatar"
|
||||
alt="avatar"
|
||||
class="avatar-img">
|
||||
<a :href="groupedReport.account.url" target="_blank">
|
||||
<span>{{ groupedReport.account.nickname }}</span>
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<div class="line"/>
|
||||
<span class="report-row-key">{{ $t('reports.actors') }}:</span>
|
||||
<span v-for="(actor, index) in groupedReport.actors" :key="actor.id">
|
||||
<a :href="actor.url" target="_blank">
|
||||
{{ actor.acct }}<span v-if="index < groupedReport.actors.length - 1">, </span>
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="groupedReport.status">
|
||||
<div class="line"/>
|
||||
<span class="report-row-key">{{ $t('reports.reportedStatus') }}:</span>
|
||||
<status :status="groupedReport.status" class="reported-status"/>
|
||||
</div>
|
||||
<div v-if="groupedReport.reports">
|
||||
<el-collapse>
|
||||
<el-collapse-item :title="$t('reports.reports')">
|
||||
<report-card :reports="groupedReport.reports"/>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import moment from 'moment'
|
||||
import ModerateUserDropdown from './ModerateUserDropdown'
|
||||
import ReportCard from './ReportCard'
|
||||
import Status from '@/components/Status'
|
||||
|
||||
export default {
|
||||
name: 'Report',
|
||||
components: { ModerateUserDropdown, ReportCard, Status },
|
||||
props: {
|
||||
groupedReports: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
changeAllReports(reportState, groupOfReports) {
|
||||
const reportsData = groupOfReports.map(report => {
|
||||
return { id: report.id, state: reportState }
|
||||
})
|
||||
this.$store.dispatch('ChangeReportState', reportsData)
|
||||
},
|
||||
parseTimestamp(timestamp) {
|
||||
return moment(timestamp).format('L HH:mm')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style rel='stylesheet/scss' lang='scss'>
|
||||
a {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.avatar-img {
|
||||
vertical-align: bottom;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
.el-card__body {
|
||||
padding: 17px;
|
||||
}
|
||||
.el-card__header {
|
||||
background-color: #FAFAFA;
|
||||
padding: 10px 20px;
|
||||
}
|
||||
.el-icon-arrow-right {
|
||||
margin-right: 6px;
|
||||
}
|
||||
.header-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
height: 40px;
|
||||
}
|
||||
.line {
|
||||
width: 100%;
|
||||
height: 0;
|
||||
border: 0.5px solid #EBEEF5;
|
||||
margin: 15px 0 15px;
|
||||
}
|
||||
.report-title {
|
||||
margin: 0;
|
||||
}
|
||||
.report-row-key {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.reported-status {
|
||||
margin-top: 15px;
|
||||
}
|
||||
@media
|
||||
only screen and (max-width: 760px),
|
||||
(min-device-width: 768px) and (max-device-width: 1024px) {
|
||||
.header-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 80px;
|
||||
}
|
||||
}
|
||||
</style>
|
86
src/views/reports/components/ModerateUserDropdown.vue
Normal file
86
src/views/reports/components/ModerateUserDropdown.vue
Normal file
|
@ -0,0 +1,86 @@
|
|||
<template>
|
||||
<el-dropdown trigger="click">
|
||||
<el-button plain size="small" icon="el-icon-files">{{ $t('reports.moderateUser') }}
|
||||
<i class="el-icon-arrow-down el-icon--right"/>
|
||||
</el-button>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item
|
||||
v-if="showDeactivatedButton(account)"
|
||||
@click.native="handleDeactivation(account)">
|
||||
{{ account.deactivated ? $t('users.activateAccount') : $t('users.deactivateAccount') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
v-if="showDeactivatedButton(account.id)"
|
||||
@click.native="handleDeletion(account.id)">
|
||||
{{ $t('users.deleteAccount') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
:divided="true"
|
||||
:class="{ 'active-tag': account.tags.includes('force_nsfw') }"
|
||||
@click.native="toggleTag(account, 'force_nsfw')">
|
||||
{{ $t('users.forceNsfw') }}
|
||||
<i v-if="account.tags.includes('force_nsfw')" class="el-icon-check"/>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
:class="{ 'active-tag': account.tags.includes('strip_media') }"
|
||||
@click.native="toggleTag(account, 'strip_media')">
|
||||
{{ $t('users.stripMedia') }}
|
||||
<i v-if="account.tags.includes('strip_media')" class="el-icon-check"/>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
:class="{ 'active-tag': account.tags.includes('force_unlisted') }"
|
||||
@click.native="toggleTag(account, 'force_unlisted')">
|
||||
{{ $t('users.forceUnlisted') }}
|
||||
<i v-if="account.tags.includes('force_unlisted')" class="el-icon-check"/>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
:class="{ 'active-tag': account.tags.includes('sandbox') }"
|
||||
@click.native="toggleTag(account, 'sandbox')">
|
||||
{{ $t('users.sandbox') }}
|
||||
<i v-if="account.tags.includes('sandbox')" class="el-icon-check"/>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
v-if="account.local"
|
||||
:class="{ 'active-tag': account.tags.includes('disable_remote_subscription') }"
|
||||
@click.native="toggleTag(account, 'disable_remote_subscription')">
|
||||
{{ $t('users.disableRemoteSubscription') }}
|
||||
<i v-if="account.tags.includes('disable_remote_subscription')" class="el-icon-check"/>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
v-if="account.local"
|
||||
:class="{ 'active-tag': account.tags.includes('disable_any_subscription') }"
|
||||
@click.native="toggleTag(account, 'disable_any_subscription')">
|
||||
{{ $t('users.disableAnySubscription') }}
|
||||
<i v-if="account.tags.includes('disable_any_subscription')" class="el-icon-check"/>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ModerateUserDropdown',
|
||||
props: {
|
||||
account: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleDeactivation({ nickname }) {
|
||||
this.$store.dispatch('ToggleUserActivation', nickname)
|
||||
},
|
||||
handleDeletion(user) {
|
||||
this.$store.dispatch('DeleteUser', user)
|
||||
},
|
||||
showDeactivatedButton(id) {
|
||||
return this.$store.state.user.id !== id
|
||||
},
|
||||
toggleTag(user, tag) {
|
||||
user.tags.includes(tag)
|
||||
? this.$store.dispatch('RemoveTag', { users: [user], tag })
|
||||
: this.$store.dispatch('AddTag', { users: [user], tag })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
119
src/views/reports/components/NoteCard.vue
Normal file
119
src/views/reports/components/NoteCard.vue
Normal file
|
@ -0,0 +1,119 @@
|
|||
<template>
|
||||
<el-card class="note-card">
|
||||
<div slot="header">
|
||||
<div class="note-header">
|
||||
<div class="note-actor-container">
|
||||
<div class="note-actor">
|
||||
<img :src="note.user.avatar" class="note-avatar-img">
|
||||
<h3 class="note-actor-name">{{ note.user.display_name }}</h3>
|
||||
</div>
|
||||
<a :href="note.user.url" target="_blank">
|
||||
@{{ note.user.acct }}
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<el-popconfirm
|
||||
title="Are you sure to delete this?"
|
||||
confirm-button-text="Yes"
|
||||
cancel-button-text="No"
|
||||
@onConfirm="handleNoteDeletion(note.id, report.id)">
|
||||
<el-button slot="reference" size="mini">
|
||||
{{ $t('reports.deleteNote') }}
|
||||
</el-button>
|
||||
</el-popconfirm>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="note-body">
|
||||
<span class="note-content" v-html="note.content"/>
|
||||
{{ parseTimestamp(note.created_at) }}
|
||||
</div>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import moment from 'moment'
|
||||
|
||||
export default {
|
||||
name: 'NoteCard',
|
||||
props: {
|
||||
report: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
note: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
parseTimestamp(timestamp) {
|
||||
return moment(timestamp).format('YYYY-MM-DD HH:mm')
|
||||
},
|
||||
handleNoteDeletion(noteID, reportID) {
|
||||
this.$store.dispatch('DeleteReportNote', { noteID, reportID })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style rel='stylesheet/scss' lang='scss'>
|
||||
a {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.el-icon-arrow-right {
|
||||
margin-right: 6px;
|
||||
}
|
||||
.note-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
height: 40px;
|
||||
}
|
||||
.note-actor {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.note-actor-name {
|
||||
margin: 0;
|
||||
height: 22px;
|
||||
}
|
||||
.note-avatar-img {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
.note-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.note-card {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.note-content {
|
||||
font-size: 15px;
|
||||
}
|
||||
.note-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
@media
|
||||
only screen and (max-width: 760px),
|
||||
(min-device-width: 768px) and (max-device-width: 1024px) {
|
||||
.el-card__header {
|
||||
padding: 10px 17px;
|
||||
}
|
||||
.note-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 80px;
|
||||
}
|
||||
.note-actor-container {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.note-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
287
src/views/reports/components/Report.vue
Normal file
287
src/views/reports/components/Report.vue
Normal file
|
@ -0,0 +1,287 @@
|
|||
<template>
|
||||
<div>
|
||||
<el-timeline class="timeline">
|
||||
<el-timeline-item
|
||||
v-for="report in reports"
|
||||
:timestamp="parseTimestamp(report.created_at)"
|
||||
:key="report.id"
|
||||
placement="top"
|
||||
class="timeline-item-container">
|
||||
<el-card>
|
||||
<div class="header-container">
|
||||
<div>
|
||||
<h3 class="report-title">{{ $t('reports.reportOn') }} {{ report.account.display_name }}</h3>
|
||||
<h5 class="id">{{ $t('reports.id') }}: {{ report.id }}</h5>
|
||||
</div>
|
||||
<div>
|
||||
<el-tag :type="getStateType(report.state)" size="large">{{ capitalizeFirstLetter(report.state) }}</el-tag>
|
||||
<el-dropdown trigger="click">
|
||||
<el-button plain size="small" icon="el-icon-edit">{{ $t('reports.changeState') }}<i class="el-icon-arrow-down el-icon--right"/></el-button>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item v-if="report.state !== 'resolved'" @click.native="changeReportState('resolved', report.id)">{{ $t('reports.resolve') }}</el-dropdown-item>
|
||||
<el-dropdown-item v-if="report.state !== 'open'" @click.native="changeReportState('open', report.id)">{{ $t('reports.reopen') }}</el-dropdown-item>
|
||||
<el-dropdown-item v-if="report.state !== 'closed'" @click.native="changeReportState('closed', report.id)">{{ $t('reports.close') }}</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
<moderate-user-dropdown :account="report.account"/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="line"/>
|
||||
<span class="report-row-key">{{ $t('reports.account') }}:</span>
|
||||
<img
|
||||
:src="report.account.avatar"
|
||||
alt="avatar"
|
||||
class="avatar-img">
|
||||
<a :href="report.account.url" target="_blank" class="account">
|
||||
<span>{{ report.account.acct }}</span>
|
||||
</a>
|
||||
</div>
|
||||
<div v-if="report.content.length > 0">
|
||||
<div class="line"/>
|
||||
<span class="report-row-key">{{ $t('reports.content') }}:
|
||||
<span>{{ report.content }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<div class="line"/>
|
||||
<span class="report-row-key">{{ $t('reports.actor') }}:</span>
|
||||
<img
|
||||
:src="report.actor.avatar"
|
||||
alt="avatar"
|
||||
class="avatar-img">
|
||||
<a :href="report.actor.url" target="_blank" class="account">
|
||||
<span>{{ report.actor.acct }}</span>
|
||||
</a>
|
||||
</div>
|
||||
<div v-if="report.statuses.length > 0" class="statuses">
|
||||
<el-collapse>
|
||||
<el-collapse-item :title="getStatusesTitle(report.statuses)">
|
||||
<div v-for="status in report.statuses" :key="status.id">
|
||||
<status :status="status" :page="currentPage"/>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</div>
|
||||
<div class="report-notes">
|
||||
<el-collapse>
|
||||
<el-collapse-item :title="getNotesTitle(report.notes)">
|
||||
<note-card v-for="(note, index) in report.notes" :key="index" :note="note" :report="report"/>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
<div class="report-note-form">
|
||||
<el-input
|
||||
v-model="notes[report.id]"
|
||||
:placeholder="$t('reports.leaveNote')"
|
||||
type="textarea"
|
||||
rows="3"/>
|
||||
<div class="report-post-note">
|
||||
<el-button @click="handleNewNote(report.id)">{{ $t('reports.postNote') }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
<div v-if="!loading" class="reports-pagination">
|
||||
<el-pagination
|
||||
:total="totalReportsCount"
|
||||
:current-page="currentPage"
|
||||
:page-size="pageSize"
|
||||
background
|
||||
layout="prev, pager, next"
|
||||
@current-change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import moment from 'moment'
|
||||
import NoteCard from './NoteCard'
|
||||
import Status from '@/components/Status'
|
||||
import ModerateUserDropdown from './ModerateUserDropdown'
|
||||
|
||||
export default {
|
||||
name: 'Report',
|
||||
components: { Status, ModerateUserDropdown, NoteCard },
|
||||
props: {
|
||||
reports: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
notes: {}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
loading() {
|
||||
return this.$store.state.reports.loading
|
||||
},
|
||||
pageSize() {
|
||||
return this.$store.state.reports.pageSize
|
||||
},
|
||||
totalReportsCount() {
|
||||
return this.$store.state.reports.totalReportsCount
|
||||
},
|
||||
currentPage() {
|
||||
return this.$store.state.reports.currentPage
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
changeReportState(state, id) {
|
||||
this.$store.dispatch('ChangeReportState', [{ state, id }])
|
||||
},
|
||||
capitalizeFirstLetter(str) {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1)
|
||||
},
|
||||
getStateType(state) {
|
||||
switch (state) {
|
||||
case 'closed':
|
||||
return 'info'
|
||||
case 'resolved':
|
||||
return 'success'
|
||||
default:
|
||||
return 'primary'
|
||||
}
|
||||
},
|
||||
getStatusesTitle(statuses) {
|
||||
return `Reported statuses: ${statuses.length} item(s)`
|
||||
},
|
||||
getNotesTitle(notes = []) {
|
||||
return `Notes: ${notes.length} item(s)`
|
||||
},
|
||||
handlePageChange(page) {
|
||||
this.$store.dispatch('FetchReports', page)
|
||||
},
|
||||
parseTimestamp(timestamp) {
|
||||
return moment(timestamp).format('L HH:mm')
|
||||
},
|
||||
handleNewNote(reportID) {
|
||||
this.$store.dispatch('CreateReportNote', { content: this.notes[reportID], reportID })
|
||||
this.notes[reportID] = ''
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style rel='stylesheet/scss' lang='scss'>
|
||||
.account {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.avatar-img {
|
||||
vertical-align: bottom;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
.el-card__body {
|
||||
padding: 17px;
|
||||
}
|
||||
.el-card__header {
|
||||
background-color: #FAFAFA;
|
||||
padding: 10px 20px;
|
||||
}
|
||||
.el-collapse {
|
||||
border-bottom: none;
|
||||
}
|
||||
.el-collapse-item__header {
|
||||
height: 46px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.el-collapse-item__content {
|
||||
padding-bottom: 7px;
|
||||
}
|
||||
.el-icon-arrow-right {
|
||||
margin-right: 6px;
|
||||
}
|
||||
.el-icon-close {
|
||||
padding: 10px 5px 10px 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
h4 {
|
||||
margin: 0;
|
||||
height: 17px;
|
||||
}
|
||||
.header-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
height: 40px;
|
||||
}
|
||||
.id {
|
||||
color: gray;
|
||||
margin-top: 6px;
|
||||
}
|
||||
.line {
|
||||
width: 100%;
|
||||
height: 0;
|
||||
border: 0.5px solid #EBEEF5;
|
||||
margin: 15px 0 15px;
|
||||
}
|
||||
.new-note {
|
||||
p {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
height: 17px;
|
||||
margin: 13px 0 7px;
|
||||
}
|
||||
}
|
||||
.note {
|
||||
box-shadow: 0 2px 5px 0 rgba(0,0,0,.1);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.no-notes {
|
||||
font-style: italic;
|
||||
color: gray;
|
||||
}
|
||||
.report-row-key {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.report-row-key {
|
||||
font-size: 14px;
|
||||
}
|
||||
.report-title {
|
||||
margin: 0;
|
||||
}
|
||||
.report-note-form {
|
||||
margin: 15px 0 0 0;
|
||||
}
|
||||
.report-post-note {
|
||||
margin: 5px 0 0 0;
|
||||
text-align: right;
|
||||
}
|
||||
.reports-pagination {
|
||||
margin: 25px 0;
|
||||
text-align: center;
|
||||
}
|
||||
.statuses {
|
||||
margin-top: 15px;
|
||||
}
|
||||
.submit-button {
|
||||
display: block;
|
||||
margin: 7px 0 17px auto;
|
||||
}
|
||||
.timestamp {
|
||||
margin: 0;
|
||||
font-style: italic;
|
||||
color: gray;
|
||||
}
|
||||
@media
|
||||
only screen and (max-width: 760px),
|
||||
(min-device-width: 768px) and (max-device-width: 1024px) {
|
||||
.timeline-item-container {
|
||||
.header-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 80px;
|
||||
}
|
||||
.id {
|
||||
margin: 6px 0 0 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
130
src/views/reports/components/ReportCard.vue
Normal file
130
src/views/reports/components/ReportCard.vue
Normal file
|
@ -0,0 +1,130 @@
|
|||
<template>
|
||||
<div>
|
||||
<el-card v-for="report in reports" :key="report.id" class="report-card">
|
||||
<div slot="header">
|
||||
<div class="report-header">
|
||||
<div class="report-actor-container">
|
||||
<div class="report-actor">
|
||||
<img :src="report.actor.avatar" class="report-avatar-img">
|
||||
<h3 class="report-actor-name">{{ report.actor.display_name }}</h3>
|
||||
</div>
|
||||
<a :href="report.actor.url" target="_blank">
|
||||
@{{ report.actor.acct }}
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<el-tag :type="getStateType(report.state)" size="large">{{ capitalizeFirstLetter(report.state) }}</el-tag>
|
||||
<el-dropdown trigger="click">
|
||||
<el-button plain size="small" icon="el-icon-edit">{{ $t('reports.changeState') }}<i class="el-icon-arrow-down el-icon--right"/></el-button>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item v-if="report.state !== 'resolved'" @click.native="changeReportState('resolved', report.id)">{{ $t('reports.resolve') }}</el-dropdown-item>
|
||||
<el-dropdown-item v-if="report.state !== 'open'" @click.native="changeReportState('open', report.id)">{{ $t('reports.reopen') }}</el-dropdown-item>
|
||||
<el-dropdown-item v-if="report.state !== 'closed'" @click.native="changeReportState('closed', report.id)">{{ $t('reports.close') }}</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="report-body">
|
||||
<span class="report-content" v-html="report.content"/>
|
||||
{{ parseTimestamp(report.created_at) }}
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import moment from 'moment'
|
||||
|
||||
export default {
|
||||
name: 'Statuses',
|
||||
props: {
|
||||
reports: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
capitalizeFirstLetter(str) {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1)
|
||||
},
|
||||
changeReportState(state, id) {
|
||||
this.$store.dispatch('ChangeReportState', [{ state, id }])
|
||||
},
|
||||
getStateType(state) {
|
||||
switch (state) {
|
||||
case 'closed':
|
||||
return 'info'
|
||||
case 'resolved':
|
||||
return 'success'
|
||||
default:
|
||||
return 'primary'
|
||||
}
|
||||
},
|
||||
parseTimestamp(timestamp) {
|
||||
return moment(timestamp).format('YYYY-MM-DD HH:mm')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style rel='stylesheet/scss' lang='scss'>
|
||||
a {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.el-icon-arrow-right {
|
||||
margin-right: 6px;
|
||||
}
|
||||
.report-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
height: 40px;
|
||||
}
|
||||
.report-actor {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.report-actor-name {
|
||||
margin: 0;
|
||||
height: 22px;
|
||||
}
|
||||
.report-avatar-img {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
.report-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.report-card {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.report-content {
|
||||
font-size: 15px;
|
||||
}
|
||||
.report-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
@media
|
||||
only screen and (max-width: 760px),
|
||||
(min-device-width: 768px) and (max-device-width: 1024px) {
|
||||
.el-card__header {
|
||||
padding: 10px 17px;
|
||||
}
|
||||
.report-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 80px;
|
||||
}
|
||||
.report-actor-container {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.report-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -44,7 +44,7 @@ export default {
|
|||
toggleFilters() {
|
||||
this.$store.dispatch('SetFilter', this.$data.filter)
|
||||
this.$store.dispatch('ClearFetchedReports')
|
||||
this.$store.dispatch('FetchReports')
|
||||
this.$store.dispatch('FetchReports', 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,176 +0,0 @@
|
|||
<template>
|
||||
<el-collapse-item :title="getStatusesTitle(report.statuses)">
|
||||
<el-card v-for="status in report.statuses" :key="status.id" class="status-card">
|
||||
<div slot="header">
|
||||
<div class="status-header">
|
||||
<div class="status-account-container">
|
||||
<div class="status-account">
|
||||
<img :src="status.account.avatar" class="status-avatar-img">
|
||||
<h3 class="status-account-name">{{ status.account.display_name }}</h3>
|
||||
</div>
|
||||
<a :href="status.account.url" target="_blank" class="account">
|
||||
@{{ status.account.acct }}
|
||||
</a>
|
||||
</div>
|
||||
<div class="status-actions">
|
||||
<el-tag v-if="status.sensitive" type="warning" size="large">{{ $t('reports.sensitive') }}</el-tag>
|
||||
<el-tag size="large">{{ capitalizeFirstLetter(status.visibility) }}</el-tag>
|
||||
<el-dropdown trigger="click">
|
||||
<el-button plain size="small" icon="el-icon-edit" class="status-actions-button">
|
||||
{{ $t('reports.changeScope') }}<i class="el-icon-arrow-down el-icon--right"/>
|
||||
</el-button>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item
|
||||
v-if="!status.sensitive"
|
||||
@click.native="changeStatus(status.id, true, status.visibility, report.id)">
|
||||
{{ $t('reports.addSensitive') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
v-if="status.sensitive"
|
||||
@click.native="changeStatus(status.id, false, status.visibility, report.id)">
|
||||
{{ $t('reports.removeSensitive') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
v-if="status.visibility !== 'public'"
|
||||
@click.native="changeStatus(status.id, status.sensitive, 'public', report.id)">
|
||||
{{ $t('reports.public') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
v-if="status.visibility !== 'private'"
|
||||
@click.native="changeStatus(status.id, status.sensitive, 'private', report.id)">
|
||||
{{ $t('reports.private') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
v-if="status.visibility !== 'unlisted'"
|
||||
@click.native="changeStatus(status.id, status.sensitive, 'unlisted', report.id)">
|
||||
{{ $t('reports.unlisted') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
@click.native="deleteStatus(status.id, report.id)">
|
||||
{{ $t('reports.deleteStatus') }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="status-body">
|
||||
<span class="status-content" v-html="status.content"/>
|
||||
<a :href="status.url" target="_blank" class="account">
|
||||
{{ parseTimestamp(status.created_at) }}
|
||||
</a>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-collapse-item>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import moment from 'moment'
|
||||
|
||||
export default {
|
||||
name: 'Statuses',
|
||||
props: {
|
||||
report: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
capitalizeFirstLetter(str) {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1)
|
||||
},
|
||||
changeStatus(statusId, isSensitive, visibility, reportId) {
|
||||
this.$store.dispatch('ChangeStatusScope', { statusId, isSensitive, visibility, reportId })
|
||||
},
|
||||
deleteStatus(statusId, reportId) {
|
||||
this.$confirm('Are you sure you want to delete this status?', 'Warning', {
|
||||
confirmButtonText: 'OK',
|
||||
cancelButtonText: 'Cancel',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
this.$store.dispatch('DeleteStatus', { statusId, reportId })
|
||||
this.$message({
|
||||
type: 'success',
|
||||
message: 'Delete completed'
|
||||
})
|
||||
}).catch(() => {
|
||||
this.$message({
|
||||
type: 'info',
|
||||
message: 'Delete canceled'
|
||||
})
|
||||
})
|
||||
},
|
||||
getStatusesTitle(statuses) {
|
||||
return `Reported statuses: ${statuses.length} item(s)`
|
||||
},
|
||||
parseTimestamp(timestamp) {
|
||||
return moment(timestamp).format('YYYY-MM-DD HH:mm')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style rel='stylesheet/scss' lang='scss'>
|
||||
.account {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.status-account {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.status-avatar-img {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
.status-account-name {
|
||||
margin: 0;
|
||||
height: 22px;
|
||||
}
|
||||
.status-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.status-content {
|
||||
font-size: 15px;
|
||||
}
|
||||
.status-card {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.status-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
@media
|
||||
only screen and (max-width: 760px),
|
||||
(min-device-width: 768px) and (max-device-width: 1024px) {
|
||||
.el-message {
|
||||
min-width: 80%;
|
||||
}
|
||||
.el-message-box {
|
||||
width: 80%;
|
||||
}
|
||||
.status-card {
|
||||
.el-card__header {
|
||||
padding: 10px 17px
|
||||
}
|
||||
.el-tag {
|
||||
margin: 3px 4px 3px 0;
|
||||
}
|
||||
.status-account-container {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.status-actions-button {
|
||||
margin: 3px 0 3px;
|
||||
}
|
||||
.status-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.status-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,271 +0,0 @@
|
|||
<template>
|
||||
<el-timeline-item :timestamp="parseTimestamp(report.created_at)" placement="top" class="timeline-item-container">
|
||||
<el-card>
|
||||
<div class="header-container">
|
||||
<div>
|
||||
<h3 class="report-title">Report on {{ report.account.display_name }}</h3>
|
||||
<h5 class="id">ID: {{ report.id }}</h5>
|
||||
</div>
|
||||
<div>
|
||||
<el-tag :type="getStateType(report.state)" size="large">{{ capitalizeFirstLetter(report.state) }}</el-tag>
|
||||
<el-dropdown trigger="click">
|
||||
<el-button plain size="small" icon="el-icon-edit">{{ $t('reports.changeState') }}<i class="el-icon-arrow-down el-icon--right"/></el-button>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item v-if="report.state !== 'resolved'" @click.native="changeReportState('resolved', report.id)">{{ $t('reports.resolve') }}</el-dropdown-item>
|
||||
<el-dropdown-item v-if="report.state !== 'open'" @click.native="changeReportState('open', report.id)">{{ $t('reports.reopen') }}</el-dropdown-item>
|
||||
<el-dropdown-item v-if="report.state !== 'closed'" @click.native="changeReportState('closed', report.id)">{{ $t('reports.close') }}</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
<el-dropdown trigger="click">
|
||||
<el-button plain size="small" icon="el-icon-files">{{ $t('reports.moderateUser') }}<i class="el-icon-arrow-down el-icon--right"/></el-button>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item
|
||||
v-if="showDeactivatedButton(report.account)"
|
||||
@click.native="toggleActivation(report.account)">
|
||||
{{ report.account.deactivated ? $t('users.activateAccount') : $t('users.deactivateAccount') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
v-if="showDeactivatedButton(report.account.id)"
|
||||
@click.native="handleDeletion(report.account.id)">
|
||||
{{ $t('users.deleteAccount') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
:divided="true"
|
||||
:class="{ 'active-tag': report.account.tags.includes('force_nsfw') }"
|
||||
@click.native="toggleTag(report.account, 'force_nsfw')">
|
||||
{{ $t('users.forceNsfw') }}
|
||||
<i v-if="report.account.tags.includes('force_nsfw')" class="el-icon-check"/>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
:class="{ 'active-tag': report.account.tags.includes('strip_media') }"
|
||||
@click.native="toggleTag(report.account, 'strip_media')">
|
||||
{{ $t('users.stripMedia') }}
|
||||
<i v-if="report.account.tags.includes('strip_media')" class="el-icon-check"/>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
:class="{ 'active-tag': report.account.tags.includes('force_unlisted') }"
|
||||
@click.native="toggleTag(report.account, 'force_unlisted')">
|
||||
{{ $t('users.forceUnlisted') }}
|
||||
<i v-if="report.account.tags.includes('force_unlisted')" class="el-icon-check"/>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
:class="{ 'active-tag': report.account.tags.includes('sandbox') }"
|
||||
@click.native="toggleTag(report.account, 'sandbox')">
|
||||
{{ $t('users.sandbox') }}
|
||||
<i v-if="report.account.tags.includes('sandbox')" class="el-icon-check"/>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
v-if="report.account.local"
|
||||
:class="{ 'active-tag': report.account.tags.includes('disable_remote_subscription') }"
|
||||
@click.native="toggleTag(report.account, 'disable_remote_subscription')">
|
||||
{{ $t('users.disableRemoteSubscription') }}
|
||||
<i v-if="report.account.tags.includes('disable_remote_subscription')" class="el-icon-check"/>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
v-if="report.account.local"
|
||||
:class="{ 'active-tag': report.account.tags.includes('disable_any_subscription') }"
|
||||
@click.native="toggleTag(report.account, 'disable_any_subscription')">
|
||||
{{ $t('users.disableAnySubscription') }}
|
||||
<i v-if="report.account.tags.includes('disable_any_subscription')" class="el-icon-check"/>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="line"/>
|
||||
<span class="report-row-key">Account:</span>
|
||||
<img
|
||||
:src="report.account.avatar"
|
||||
alt="avatar"
|
||||
class="avatar-img">
|
||||
<a :href="report.account.url" target="_blank" class="account">
|
||||
<span class="report-row-value">{{ report.account.acct }}</span>
|
||||
</a>
|
||||
</div>
|
||||
<div v-if="report.content.length > 0">
|
||||
<div class="line"/>
|
||||
<span class="report-row-key">Content:
|
||||
<span class="report-row-value">{{ report.content }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<div class="line"/>
|
||||
<span class="report-row-key">Actor:</span>
|
||||
<img
|
||||
:src="report.actor.avatar"
|
||||
alt="avatar"
|
||||
class="avatar-img">
|
||||
<a :href="report.actor.url" target="_blank" class="account">
|
||||
<span class="report-row-value">{{ report.actor.acct }}</span>
|
||||
</a>
|
||||
</div>
|
||||
<div v-if="report.statuses.length > 0" class="statuses">
|
||||
<el-collapse>
|
||||
<statuses :report="report"/>
|
||||
</el-collapse>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-timeline-item>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import moment from 'moment'
|
||||
import Statuses from './Statuses'
|
||||
|
||||
export default {
|
||||
name: 'TimelineItem',
|
||||
components: { Statuses },
|
||||
props: {
|
||||
report: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
changeReportState(reportState, reportId) {
|
||||
this.$store.dispatch('ChangeReportState', { reportState, reportId })
|
||||
},
|
||||
capitalizeFirstLetter(str) {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1)
|
||||
},
|
||||
getStateType(state) {
|
||||
switch (state) {
|
||||
case 'closed':
|
||||
return 'info'
|
||||
case 'resolved':
|
||||
return 'success'
|
||||
default:
|
||||
return 'primary'
|
||||
}
|
||||
},
|
||||
handleDeletion(user) {
|
||||
this.$store.dispatch('DeleteUsers', [user])
|
||||
},
|
||||
parseTimestamp(timestamp) {
|
||||
return moment(timestamp).format('L HH:mm')
|
||||
},
|
||||
showDeactivatedButton(id) {
|
||||
return this.$store.state.user.id !== id
|
||||
},
|
||||
toggleActivation(user) {
|
||||
user.deactivated
|
||||
? this.$store.dispatch('ActivateUsers', [user])
|
||||
: this.$store.dispatch('DeactivateUsers', [user])
|
||||
},
|
||||
toggleTag(user, tag) {
|
||||
user.tags.includes(tag)
|
||||
? this.$store.dispatch('RemoveTag', { users: [user], tag })
|
||||
: this.$store.dispatch('AddTag', { users: [user], tag })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style rel='stylesheet/scss' lang='scss'>
|
||||
.account {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.avatar-img {
|
||||
vertical-align: bottom;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
.el-card__body {
|
||||
padding: 17px;
|
||||
}
|
||||
.el-card__header {
|
||||
background-color: #FAFAFA;
|
||||
padding: 10px 20px;
|
||||
}
|
||||
.el-collapse {
|
||||
border-bottom: none;
|
||||
}
|
||||
.el-collapse-item__header {
|
||||
height: 46px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.el-collapse-item__content {
|
||||
padding-bottom: 7px;
|
||||
}
|
||||
.el-icon-arrow-right {
|
||||
margin-right: 6px;
|
||||
}
|
||||
.el-icon-close {
|
||||
padding: 10px 5px 10px 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
h4 {
|
||||
margin: 0;
|
||||
height: 17px;
|
||||
}
|
||||
.header-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
height: 40px;
|
||||
}
|
||||
.id {
|
||||
color: gray;
|
||||
margin-top: 6px;
|
||||
}
|
||||
.line {
|
||||
width: 100%;
|
||||
height: 0;
|
||||
border: 0.5px solid #EBEEF5;
|
||||
margin: 15px 0 15px;
|
||||
}
|
||||
.new-note {
|
||||
p {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
height: 17px;
|
||||
margin: 13px 0 7px;
|
||||
}
|
||||
}
|
||||
.note {
|
||||
box-shadow: 0 2px 5px 0 rgba(0,0,0,.1);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.no-notes {
|
||||
font-style: italic;
|
||||
color: gray;
|
||||
}
|
||||
.report-row-key {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.report-row-key {
|
||||
font-size: 14px;
|
||||
}
|
||||
.report-title {
|
||||
margin: 0;
|
||||
}
|
||||
.statuses {
|
||||
margin-top: 15px;
|
||||
}
|
||||
.submit-button {
|
||||
display: block;
|
||||
margin: 7px 0 17px auto;
|
||||
}
|
||||
.timestamp {
|
||||
margin: 0;
|
||||
font-style: italic;
|
||||
color: gray;
|
||||
}
|
||||
@media
|
||||
only screen and (max-width: 760px),
|
||||
(min-device-width: 768px) and (max-device-width: 1024px) {
|
||||
.timeline-item-container {
|
||||
.header-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 80px;
|
||||
}
|
||||
.id {
|
||||
margin: 6px 0 0 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,13 +1,22 @@
|
|||
<template>
|
||||
<div class="reports-container">
|
||||
<h1>{{ $t('reports.reports') }}</h1>
|
||||
<h1 v-if="groupReports">
|
||||
{{ $t('reports.groupedReports') }}
|
||||
<span class="report-count">({{ normalizedReportsCount }})</span>
|
||||
</h1>
|
||||
<h1 v-else>
|
||||
{{ $t('reports.reports') }}
|
||||
<span class="report-count">({{ normalizedReportsCount }})</span>
|
||||
</h1>
|
||||
<div class="filter-container">
|
||||
<reports-filter/>
|
||||
<reports-filter v-if="!groupReports"/>
|
||||
<el-checkbox v-model="groupReports" class="group-reports-checkbox">
|
||||
Group reports by statuses
|
||||
</el-checkbox>
|
||||
</div>
|
||||
<div class="block">
|
||||
<el-timeline class="timeline">
|
||||
<timeline-item v-loading="loading" v-for="report in reports" :report="report" :key="report.id"/>
|
||||
</el-timeline>
|
||||
<grouped-report v-loading="loading" v-if="groupReports" :grouped-reports="groupedReports"/>
|
||||
<report v-loading="loading" v-else :reports="reports"/>
|
||||
<div v-if="reports.length === 0" class="no-reports-message">
|
||||
<p>There are no reports to display</p>
|
||||
</div>
|
||||
|
@ -16,34 +25,44 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import TimelineItem from './components/TimelineItem'
|
||||
import GroupedReport from './components/GroupedReport'
|
||||
import numeral from 'numeral'
|
||||
import Report from './components/Report'
|
||||
import ReportsFilter from './components/ReportsFilter'
|
||||
|
||||
export default {
|
||||
components: { TimelineItem, ReportsFilter },
|
||||
components: { GroupedReport, Report, ReportsFilter },
|
||||
computed: {
|
||||
groupedReports() {
|
||||
return this.$store.state.reports.fetchedGroupedReports
|
||||
},
|
||||
groupReports: {
|
||||
get() {
|
||||
return this.$store.state.reports.groupReports
|
||||
},
|
||||
set() {
|
||||
this.toggleReportsGrouping()
|
||||
}
|
||||
},
|
||||
loading() {
|
||||
return this.$store.state.users.loading
|
||||
return this.$store.state.reports.loading
|
||||
},
|
||||
normalizedReportsCount() {
|
||||
return this.groupReports
|
||||
? numeral(this.$store.state.reports.fetchedGroupedReports.length).format('0a')
|
||||
: numeral(this.$store.state.reports.totalReportsCount).format('0a')
|
||||
},
|
||||
reports() {
|
||||
return this.$store.state.reports.fetchedReports
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$store.dispatch('FetchReports')
|
||||
},
|
||||
created() {
|
||||
window.addEventListener('scroll', this.handleScroll)
|
||||
},
|
||||
destroyed() {
|
||||
window.removeEventListener('scroll', this.handleScroll)
|
||||
this.$store.dispatch('FetchReports', 1)
|
||||
this.$store.dispatch('FetchGroupedReports')
|
||||
},
|
||||
methods: {
|
||||
handleScroll(reports) {
|
||||
const bottomOfWindow = document.documentElement.scrollHeight - document.documentElement.scrollTop === document.documentElement.clientHeight
|
||||
if (bottomOfWindow) {
|
||||
this.$store.dispatch('FetchReports')
|
||||
}
|
||||
toggleReportsGrouping() {
|
||||
this.$store.dispatch('ToggleReportsGrouping')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -56,16 +75,24 @@ export default {
|
|||
padding: 0px;
|
||||
}
|
||||
.filter-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 22px 15px 22px 15px;
|
||||
padding-bottom: 0
|
||||
}
|
||||
.group-reports-checkbox {
|
||||
margin-top: 10px;
|
||||
}
|
||||
h1 {
|
||||
margin: 22px 0 0 15px;
|
||||
}
|
||||
.no-reports-message {
|
||||
color: gray;
|
||||
margin-left: 19px
|
||||
|
||||
}
|
||||
.report-count {
|
||||
color: gray;
|
||||
font-size: 28px;
|
||||
}
|
||||
}
|
||||
@media
|
||||
|
@ -78,9 +105,9 @@ only screen and (max-width: 760px),
|
|||
.filter-container {
|
||||
margin: 0 10px
|
||||
}
|
||||
.timeline {
|
||||
margin: 20px 20px 20px 18px
|
||||
}
|
||||
}
|
||||
#app > div > div.main-container > section > div > div.block > ul {
|
||||
margin: 45px 45px 5px 19px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -10,8 +10,8 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import i18n from '@/lang'
|
||||
import { mapGetters } from 'vuex'
|
||||
import i18n from '@/lang'
|
||||
import Setting from './Setting'
|
||||
|
||||
export default {
|
||||
|
|
|
@ -8,8 +8,8 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import i18n from '@/lang'
|
||||
import { mapGetters } from 'vuex'
|
||||
import i18n from '@/lang'
|
||||
import Setting from './Setting'
|
||||
|
||||
export default {
|
||||
|
|
|
@ -26,8 +26,8 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import i18n from '@/lang'
|
||||
import { mapGetters } from 'vuex'
|
||||
import i18n from '@/lang'
|
||||
import Setting from './Setting'
|
||||
|
||||
export default {
|
||||
|
|
|
@ -39,8 +39,8 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import i18n from '@/lang'
|
||||
import { mapGetters } from 'vuex'
|
||||
import i18n from '@/lang'
|
||||
import Setting from './Setting'
|
||||
|
||||
export default {
|
||||
|
|
|
@ -16,8 +16,8 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import i18n from '@/lang'
|
||||
import { mapGetters } from 'vuex'
|
||||
import i18n from '@/lang'
|
||||
import Setting from './Setting'
|
||||
|
||||
export default {
|
||||
|
|
|
@ -17,8 +17,8 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import i18n from '@/lang'
|
||||
import { mapGetters } from 'vuex'
|
||||
import i18n from '@/lang'
|
||||
import Setting from './Setting'
|
||||
|
||||
export default {
|
||||
|
|
|
@ -8,8 +8,8 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import i18n from '@/lang'
|
||||
import { mapGetters } from 'vuex'
|
||||
import i18n from '@/lang'
|
||||
import Setting from './Setting'
|
||||
|
||||
export default {
|
||||
|
|
|
@ -14,8 +14,8 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import i18n from '@/lang'
|
||||
import { mapGetters } from 'vuex'
|
||||
import i18n from '@/lang'
|
||||
import Setting from './Setting'
|
||||
|
||||
export default {
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue