Merge branch 'develop' into 'feature/reduce-nav-sidebars'

# Conflicts:
#   CHANGELOG.md
This commit is contained in:
Angelina Filippova 2021-03-11 20:25:20 +00:00
commit d3929fc008
60 changed files with 392 additions and 2728 deletions

View file

@ -15,8 +15,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Filter users by actor type: Person, Bot or Application
- Add ability to configure Media Preview Proxy, User Backup, Websocket based federation and Pleroma.Web.Endpoint.MetricsExporter settings
- Mobile and Tablet UI for Single Report show page
- Ability to set rules and conditions for rendering settings (e.g. `:proxy_remote` setting is hidden if `:uploader` setting is set to `Pleroma.Uploaders.Local`)
### Changed
- **Breaking**: AdminAPI changed User field `confirmation_pending` to `is_confirmed`
- **Breaking**: AdminAPI changed User field `approval_pending` to `is_approved`
- **Breaking**: AdminAPI changed User field `deactivated` to `is_active`
- Hide Tag actions on Users tab if MRF TagPolicy is disabled. Add ability to enable TagPolicy from Moderation menu
- Move `:restrict_unauthenticated` settings from Authentication tab to Instance tab
- Replace regular inputs with textareas for setting welcome messages in the Settings section
@ -24,6 +28,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Remove Websocket based federation settings
- Move Settings tab navigation from the tabbed menu to the main sidebar menu. A separate route is created for each tab.
- Move Emoji packs configuration to the Emoji tab in the Settings section
- 401 and 404 error pages updated
- Remove unused components
### Fixed
@ -32,7 +38,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Fix wrapping `:icons` setting and parsing tuples in settings with key `:headers`
- Update keys for Pleroma.Web.Plugs.RemoteIp and PurgeExpiredActivity settings
- Update switching between local and remote emoji packs panels: the panel with the pack's metadata will be closed when another panel is opened
- Fix displaying messages for multiple errors
## [2.2] - 2020-11-18
### Added

View file

@ -48,7 +48,7 @@ const devWebpackConfig = merge(baseWebpackConfig, {
poll: config.dev.poll
},
headers: {
'content-security-policy': "base-uri 'self'; frame-ancestors 'none'; img-src 'self' data: https: http:; media-src 'self' https:; style-src 'self' 'unsafe-inline'; font-src 'self'; manifest-src 'self'; script-src 'self';"
'content-security-policy': "script-src 'self' 'unsafe-eval'; base-uri 'self'; frame-ancestors 'none'; img-src 'self' data: https: http:; media-src 'self' https:; style-src 'self' 'unsafe-inline'; font-src 'self'; manifest-src 'self'"
}
},
plugins: [
@ -67,7 +67,12 @@ const devWebpackConfig = merge(baseWebpackConfig, {
BASE_URL: devEnv.ASSETS_PUBLIC_PATH + config.dev.assetsSubDirectory,
},
}),
]
],
resolve: {
alias: {
vue: 'vue/dist/vue.js'
}
}
})
module.exports = new Promise((resolve, reject) => {

View file

@ -44,7 +44,7 @@ export const userChats = [
accepts_chat_messages: true,
ap_id: 'http://localhost:4000/users/test10',
background_image: null,
confirmation_pending: false,
is_confirmed: true,
favicon: null,
hide_favorites: true,
hide_followers: false,
@ -99,7 +99,7 @@ export const userChats = [
accepts_chat_messages: true,
ap_id: 'https://localhost/users/mk',
background_image: null,
confirmation_pending: false,
is_confirmed: true,
favicon: null,
hide_favorites: true,
hide_followers: false,
@ -168,7 +168,7 @@ export const userChats = [
accepts_chat_messages: true,
ap_id: 'http://localhost:4000/users/user2',
background_image: null,
confirmation_pending: true,
is_confirmed: false,
favicon: null,
hide_favorites: true,
hide_followers: false,
@ -222,7 +222,7 @@ export const userChats = [
accepts_chat_messages: true,
ap_id: 'https://localhost/users/mk',
background_image: null,
confirmation_pending: false,
is_confirmed: true,
favicon: null,
hide_favorites: true,
hide_followers: false,
@ -286,7 +286,7 @@ export const userChats = [
accepts_chat_messages: true,
ap_id: 'https://localhost/users/mk',
background_image: null,
confirmation_pending: false,
is_confirmed: true,
favicon: null,
hide_favorites: true,
hide_followers: false,
@ -344,7 +344,7 @@ export const userChats = [
accepts_chat_messages: true,
ap_id: 'https://localhost/users/mk',
background_image: null,
confirmation_pending: false,
is_confirmed: true,
favicon: null,
hide_favorites: true,
hide_followers: false,

View file

@ -43,7 +43,7 @@ export function getUserInfo(token, authHost) {
'statuses_count': 0,
'cover_photo': '',
'hide_follows': false,
'pleroma': { 'confirmation_pending': false, 'deactivated': false, 'tags': ['force_nsfw'], 'is_admin': true },
'pleroma': { 'is_confirmed': true, 'is_active': true, 'tags': ['force_nsfw'], 'is_admin': true },
'profile_image_url_original': '',
'created_at': 'Fri Mar 01 15:15:19 +0000 2019',
'fields': [],

View file

@ -1,12 +1,12 @@
import userChats from './chat'
export let users = [
{ active: true, approval_pending: false, deactivated: false, id: '2', nickname: 'allis', local: true, external: false, roles: { admin: true, moderator: false }, tags: [], actor_type: 'Person' },
{ active: true, approval_pending: false, deactivated: false, id: '10', nickname: 'bob', local: true, external: false, roles: { admin: false, moderator: false }, tags: ['mrf_tag:sandbox'], actor_type: 'Person' },
{ active: true, approval_pending: true, deactivated: false, id: '567', nickname: 'ded', local: false, external: true, roles: { admin: false, moderator: false }, tags: [], actor_type: 'Person' },
{ active: false, approval_pending: false, deactivated: true, id: 'abc', nickname: 'john', local: true, external: false, roles: { admin: false, moderator: false }, tags: ['mrf_tag:media-strip'], actor_type: 'Person' },
{ active: true, approval_pending: true, deactivated: false, id: '100', nickname: 'sally', local: true, external: false, roles: { admin: false, moderator: false }, tags: [], actor_type: 'Service' },
{ active: true, approval_pending: true, deactivated: false, id: '123', nickname: 'bot', local: true, external: false, roles: { admin: false, moderator: false }, tags: [], actor_type: 'Application' }
{ is_confirmed: true, is_approved: true, is_active: true, id: '2', nickname: 'allis', local: true, external: false, roles: { admin: true, moderator: false }, tags: [], actor_type: 'Person' },
{ is_confirmed: true, is_approved: true, is_active: true, id: '10', nickname: 'bob', local: true, external: false, roles: { admin: false, moderator: false }, tags: ['mrf_tag:sandbox'], actor_type: 'Person' },
{ is_confirmed: true, is_approved: false, is_active: true, id: '567', nickname: 'ded', local: false, external: true, roles: { admin: false, moderator: false }, tags: [], actor_type: 'Person' },
{ is_confirmed: true, is_approved: true, is_active: false, id: 'abc', nickname: 'john', local: true, external: false, roles: { admin: false, moderator: false }, tags: ['mrf_tag:media-strip'], actor_type: 'Person' },
{ is_confirmed: true, is_approved: false, is_active: true, id: '100', nickname: 'sally', local: true, external: false, roles: { admin: false, moderator: false }, tags: [], actor_type: 'Service' },
{ is_confirmed: true, is_approved: false, is_active: true, id: '123', nickname: 'bot', local: true, external: false, roles: { admin: false, moderator: false }, tags: [], actor_type: 'Application' }
]
const userProfile = { avatar: 'avatar.jpg', nickname: 'allis', id: '2', tags: [], roles: { admin: true, moderator: false }, local: true, external: false }
@ -17,26 +17,6 @@ const userStatuses = [
{ account: { id: '9n1bySks25olxWrku0', nickname: 'dolin' }, content: 'what is yout favorite pizza?', id: '9jop82OBXeFPYulVjM', created_at: '2020-05-22T17:34:34.000Z', visibility: 'public' }
]
const filterUsers = (str) => {
const filters = str.split(',').filter(item => item.length > 0)
if (filters.length === 0) {
return users
}
const applyFilters = (acc, filters, users) => {
if (filters.length === 0) {
return acc
}
const filteredUsers = users.filter(user => user[filters[0]])
const newAcc = [...filteredUsers]
return applyFilters(newAcc, filters.slice(1), filteredUsers)
}
return applyFilters([], filters, users)
}
const filterUsersByActorType = filters => {
return users.filter(user => filters.includes(user.actor_type))
}
export async function fetchUser(id, authHost, token) {
return Promise.resolve({ data: userProfile })
}
@ -46,12 +26,9 @@ export async function fetchUserCredentials(nickname, authHost, token) {
}
export async function fetchUsers(filters, actorTypeFilters, authHost, token, page = 1) {
const filteredUsers = filterUsers(filters)
const filteredByActorTypeUsers = filterUsersByActorType(actorTypeFilters)
const response = actorTypeFilters.length === 0 ? filteredUsers : filteredByActorTypeUsers
return Promise.resolve({ data: {
users: response,
count: response.length,
users,
count: users.length,
page_size: 50
}})
}
@ -69,12 +46,10 @@ export async function getPasswordResetToken(nickname, authHost, token) {
}
export async function searchUsers(query, filters, actorTypeFilters, authHost, token, page = 1) {
const filteredUsers = filterUsers(filters)
const filteredByActorTypeUsers = filterUsersByActorType(actorTypeFilters)
const response = actorTypeFilters.length === 0 ? filteredUsers : filteredByActorTypeUsers
const response = users.filter(user => user.nickname === query)
return Promise.resolve({ data: {
users: response.filter(user => user.nickname === query),
count: response.filter(user => user.nickname === query).length,
users: response,
count: response.length,
page_size: 50
}})
}
@ -82,7 +57,7 @@ export async function searchUsers(query, filters, actorTypeFilters, authHost, to
export async function activateUsers(nicknames, authHost, token) {
const response = nicknames.map(nickname => {
const currentUser = users.find(user => user.nickname === nickname)
return { ...currentUser, deactivated: false }
return { ...currentUser, is_active: true }
})
return Promise.resolve({ data: response })
}
@ -96,7 +71,7 @@ export async function addRight(nicknames, right, authHost, token) {
export async function deactivateUsers(nicknames, authHost, token) {
const response = nicknames.map(nickname => {
const currentUser = users.find(user => user.nickname === nickname)
return { ...currentUser, deactivated: true }
return { ...currentUser, is_active: false }
})
return Promise.resolve({ data: response })
}
@ -104,7 +79,7 @@ export async function deactivateUsers(nicknames, authHost, token) {
export async function approveUserAccount(nicknames, authHost, token) {
const response = nicknames.map(nickname => {
const currentUser = users.find(user => user.nickname === nickname)
return { ...currentUser, approval_pending: false }
return { ...currentUser, is_approved: true }
})
return Promise.resolve({ data: response })
}
@ -130,7 +105,7 @@ export async function untagUser(nickname, tag, authHost, token) {
}
export async function createNewAccount(nickname, email, password, authHost, token) {
const newUser = { active: true, deactivated: false, id: '15', nickname, local: true, external: false, roles: { admin: false, moderator: false }, tags: [] }
const newUser = { active: true, is_active: true, id: '15', nickname, local: true, external: false, roles: { admin: false, moderator: false }, tags: [] }
users = [...users, newUser]
return Promise.resolve()
}

View file

@ -74,6 +74,16 @@ export default {
mediaProxyCache: 'MediaProxy Cache',
'emoji-packs': 'Emoji packs'
},
errLog: {
error401: 'Oops! 401 Error',
error404: 'Oops! 404 Error',
pageNotFound: 'Page not found',
correctUrl: 'Please make sure you URL is correct',
unauth: 'Unauthorized',
back: 'Back',
login: 'Login',
homePage: 'Home Page'
},
navbar: {
logOut: 'Log Out',
dashboard: 'Dashboard',
@ -87,7 +97,7 @@ export default {
logInViaPleromaFE: 'Log in via PleromaFE',
username: 'username@host',
password: 'password',
omitHostname: 'omit hostname if Pleroma is located on this domain',
omitHostname: 'Omit hostname if Pleroma is located on this domain',
errorMessage: 'Username must contain username and host, e.g. john@pleroma.social',
any: 'any',
thirdparty: 'Or connect with',
@ -159,11 +169,6 @@ export default {
confirm: 'Confirm',
unfollow: 'Unfollow'
},
errorLog: {
tips: 'Please click the bug icon in the upper right corner',
description: 'Now the management system are basically the form of the spa, it enhances the user experience, but it also increases the possibility of page problems, a small negligence may lead to the entire page deadlock. Fortunately Vue provides a way to catch handling exceptions, where you can handle errors or report exceptions.',
documentation: 'Document introduction'
},
excel: {
export: 'Export',
selectedExport: 'Export Selected Items',
@ -486,7 +491,8 @@ export default {
invalidEmailError: 'Please input valid e-mail',
emailSent: 'Invite was sent',
submitFormError: 'There are invalid values in the form. Please fix them before continuing.',
inviteViaEmailAlert: 'To send invite via email make sure to enable `invites_enabled` and disable `registrations_open`'
inviteViaEmailAlert: 'To send invite via email make sure to enable `invites_enabled` and disable `registrations_open`',
copyLink: 'Copy link'
},
emoji: {
emojiPacks: 'Emoji packs',

View file

@ -127,11 +127,6 @@ export default {
cancel: 'Cancelar',
confirm: 'Confirmar'
},
errorLog: {
tips: 'Please click the bug icon in the upper right corner',
description: 'Now the management system are basically the form of the spa, it enhances the user experience, but it also increases the possibility of page problems, a small negligence may lead to the entire page deadlock. Fortunately Vue provides a way to catch handling exceptions, where you can handle errors or report exceptions.',
documentation: 'Documento de introducción'
},
excel: {
export: 'Exportar',
selectedExport: 'Exportar seleccionados',

View file

@ -128,11 +128,6 @@ export default {
cancel: 'Anullar',
confirm: 'Confirmar'
},
errorLog: {
tips: 'Mercés de clicar l’icòna del babau amont a man drecha',
description: 'Ara que lo sistèma de gestion es coma un spa, melhora lexperiéncia dels utilizaire mas aumenta tanben lo risc de problèmas sus la pagina, una pichona negligéncia pòt menar a un blocatge complèt de la pagina. Urosament Vue fornís de manièras per gerir las excepcions, trobar las errors o senhalar las excepcions.',
documentation: 'Presentacion del document'
},
excel: {
export: 'Exportar',
selectedExport: 'Exportar los elements seleccionats',

View file

@ -127,11 +127,6 @@ export default {
cancel: '取 消',
confirm: '确 定'
},
errorLog: {
tips: '请点击右上角bug小图标',
description: '现在的管理后台基本都是spa的形式了它增强了用户体验但同时也会增加页面出问题的可能性可能一个小小的疏忽就导致整个页面的死锁。好在 Vue 官网提供了一个方法来捕获处理异常,你可以在其中进行错误处理或者异常上报。',
documentation: '文档介绍'
},
excel: {
export: '导出',
selectedExport: '导出已选择项',

View file

@ -104,7 +104,7 @@ const moderationLog = {
children: [
{
path: 'index',
component: () => import('@/views/moderation_log/index'),
component: () => import('@/views/moderationLog/index'),
name: 'Moderation Log',
meta: { title: 'moderationLog', icon: 'el-icon-notebook-2', noCache: true }
}

View file

@ -56,7 +56,7 @@ const reports = {
return
} finally {
const updatedReports = state.fetchedReports.map(report => {
const updatedAccount = { ...user, deactivated: false }
const updatedAccount = { ...user, is_active: true }
return report.id === reportId ? { ...report, account: updatedAccount } : report
})
commit('SET_REPORTS', updatedReports)
@ -69,7 +69,7 @@ const reports = {
} catch (_e) {
return
} finally {
const updatedReport = { ...state.singleReport, account: { ...user, deactivated: false }}
const updatedReport = { ...state.singleReport, account: { ...user, is_active: true }}
commit('SET_SINGLE_REPORT', updatedReport)
}
dispatch('SuccessMessage')
@ -124,7 +124,7 @@ const reports = {
return
} finally {
const updatedReports = state.fetchedReports.map(report => {
const updatedAccount = { ...user, deactivated: true }
const updatedAccount = { ...user, is_active: false }
return report.id === reportId ? { ...report, account: updatedAccount } : report
})
commit('SET_REPORTS', updatedReports)
@ -137,7 +137,7 @@ const reports = {
} catch (_e) {
return
} finally {
const updatedReport = { ...state.singleReport, account: { ...user, deactivated: true }}
const updatedReport = { ...state.singleReport, account: { ...user, is_active: false }}
commit('SET_SINGLE_REPORT', updatedReport)
}
dispatch('SuccessMessage')
@ -149,7 +149,7 @@ const reports = {
return
} finally {
const updatedReports = state.fetchedReports.map(report => {
const updatedAccount = { ...user, deactivated: true }
const updatedAccount = { ...user, is_active: false }
return report.id === reportId ? { ...report, account: updatedAccount } : report
})
commit('SET_REPORTS', updatedReports)

View file

@ -88,7 +88,7 @@ const users = {
actions: {
async ActivateUsers({ dispatch, getters }, { users, _userId }) {
const updatedUsers = users.map(user => {
return { ...user, deactivated: false }
return { ...user, is_active: true }
})
const nicknames = users.map(user => user.nickname)
const callApiFn = async() => await activateUsers(nicknames, getters.authHost, getters.token)
@ -132,7 +132,7 @@ const users = {
},
async ApproveUsersAccount({ dispatch, getters }, { users, _userId, _statusId }) {
const updatedUsers = users.map(user => {
return { ...user, approval_pending: false }
return { ...user, is_approved: true }
})
const nicknames = users.map(user => user.nickname)
const callApiFn = async() => await approveUserAccount(nicknames, getters.authHost, getters.token)
@ -149,7 +149,7 @@ const users = {
},
async ConfirmUsersEmail({ dispatch, getters }, { users, _userId, _statusId }) {
const updatedUsers = users.map(user => {
return { ...user, confirmation_pending: false }
return { ...user, is_confirmed: true }
})
const nicknames = users.map(user => user.nickname)
const callApiFn = async() => await confirmUserEmail(nicknames, getters.authHost, getters.token)
@ -168,7 +168,7 @@ const users = {
},
async DeactivateUsers({ dispatch, getters }, { users, _userId }) {
const updatedUsers = users.map(user => {
return { ...user, deactivated: true }
return { ...user, is_active: false }
})
const nicknames = users.map(user => user.nickname)
const callApiFn = async() => await deactivateUsers(nicknames, getters.authHost, getters.token)
@ -200,7 +200,7 @@ const users = {
return
}
const updatedUsers = users.map(user => {
return { ...user, deactivated: true }
return { ...user, is_active: false }
})
commit('SWAP_USERS', updatedUsers)

View file

@ -171,32 +171,3 @@ code {
background: #d0d0d0;
}
}
.link-type,
.link-type:focus {
color: #337ab7;
cursor: pointer;
&:hover {
color: rgb(32, 160, 255);
}
}
.filter-container {
padding-bottom: 10px;
.filter-item {
display: inline-block;
vertical-align: middle;
margin-bottom: 10px;
}
}
//refine vue-multiselect plugin
.multiselect {
line-height: 16px;
}
.multiselect--active {
z-index: 1000 !important;
}

View file

@ -3,7 +3,7 @@ import Clipboard from 'clipboard'
function clipboardSuccess() {
Vue.prototype.$message({
message: 'Copy successfully',
message: 'Copied!',
type: 'success',
duration: 1500
})

View file

@ -6,27 +6,32 @@ const service = axios.create({
timeout: 60000 // request timeout
})
const isJson = ({ headers }) => headers['content-type'].includes('application/json')
// response interceptor
service.interceptors.response.use(
response => response,
error => {
let errorMessage
console.log(`Error ${error}`)
if (error.response) {
const edata = error.response.data.error ? error.response.data.error : error.response.data
errorMessage = !error.response.headers['content-type'].includes('application/json')
? `${error.message}`
: `${error.message} - ${edata}`
} else {
errorMessage = error
}
if (!error.response) {
Message({
message: errorMessage,
message: error,
type: 'error',
duration: 5 * 1000
})
} else {
const errors = Array.isArray(error.response.data) ? error.response.data : [error.response.data]
errors.forEach(errorData => {
const edata = errorData.error || errorData
Message({
message: isJson(error.response) ? `${error.message} - ${edata}` : `${error.message}`,
type: 'error',
duration: 5 * 1000
})
})
}
return Promise.reject(error)
}
)

View file

@ -1,23 +0,0 @@
<template>
<div class="chart-container">
<chart height="100%" width="100%"/>
</div>
</template>
<script>
import Chart from '@/components/element-ui/Charts/keyboard'
export default {
name: 'KeyboardChart',
components: { Chart }
}
</script>
<style scoped>
.chart-container{
position: relative;
width: 100%;
height: calc(100vh - 84px);
}
</style>

View file

@ -1,23 +0,0 @@
<template>
<div class="chart-container">
<chart height="100%" width="100%"/>
</div>
</template>
<script>
import Chart from '@/components/element-ui/Charts/lineMarker'
export default {
name: 'LineChart',
components: { Chart }
}
</script>
<style scoped>
.chart-container{
position: relative;
width: 100%;
height: calc(100vh - 84px);
}
</style>

View file

@ -1,23 +0,0 @@
<template>
<div class="chart-container">
<chart height="100%" width="100%"/>
</div>
</template>
<script>
import Chart from '@/components/element-ui/Charts/mixChart'
export default {
name: 'MixChart',
components: { Chart }
}
</script>
<style scoped>
.chart-container{
position: relative;
width: 100%;
height: calc(100vh - 84px);
}
</style>

View file

@ -1,45 +0,0 @@
<template>
<div class="app-container">
<el-tabs v-model="activeName">
<el-tab-pane label="use clipboard directly" name="directly">
<el-input v-model="inputData" placeholder="Please input" style="width:400px;max-width:100%;"/>
<el-button type="primary" icon="document" @click="handleCopy(inputData,$event)">copy</el-button>
</el-tab-pane>
<el-tab-pane label="use clipboard by v-directive" name="v-directive">
<el-input v-model="inputData" placeholder="Please input" style="width:400px;max-width:100%;"/>
<el-button v-clipboard:copy="inputData" v-clipboard:success="clipboardSuccess" type="primary" icon="document">copy</el-button>
</el-tab-pane>
</el-tabs>
</div>
</template>
<script>
import clip from '@/utils/clipboard' // use clipboard directly
import clipboard from '@/directive/clipboard/index.js' // use clipboard by v-directive
export default {
name: 'ClipboardDemo',
directives: {
clipboard
},
data: function() {
return {
activeName: 'directly',
inputData: 'https://github.com/PanJiaChen/vue-element-admin'
}
},
methods: {
handleCopy(text, event) {
clip(text, event)
},
clipboardSuccess() {
this.$message({
message: 'Copy successfully',
type: 'success',
duration: 1500
})
}
}
}
</script>

View file

@ -1,106 +0,0 @@
<template>
<div :class="className" :style="{height:height,width:width}"/>
</template>
<script>
import echarts from 'echarts'
require('echarts/theme/macarons') // echarts theme
import { debounce } from '@/utils'
const animationDuration = 6000
export default {
props: {
className: {
type: String,
default: 'chart'
},
width: {
type: String,
default: '100%'
},
height: {
type: String,
default: '300px'
}
},
data: function() {
return {
chart: null
}
},
mounted() {
this.initChart()
this.__resizeHandler = debounce(() => {
if (this.chart) {
this.chart.resize()
}
}, 100)
window.addEventListener('resize', this.__resizeHandler)
},
beforeDestroy() {
if (!this.chart) {
return
}
window.removeEventListener('resize', this.__resizeHandler)
this.chart.dispose()
this.chart = null
},
methods: {
initChart() {
this.chart = echarts.init(this.$el, 'macarons')
this.chart.setOption({
tooltip: {
trigger: 'axis',
axisPointer: { //
type: 'shadow' // 线'line' | 'shadow'
}
},
grid: {
top: 10,
left: '2%',
right: '2%',
bottom: '3%',
containLabel: true
},
xAxis: [{
type: 'category',
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
axisTick: {
alignWithLabel: true
}
}],
yAxis: [{
type: 'value',
axisTick: {
show: false
}
}],
series: [{
name: 'pageA',
type: 'bar',
stack: 'vistors',
barWidth: '60%',
data: [79, 52, 200, 334, 390, 330, 220],
animationDuration
}, {
name: 'pageB',
type: 'bar',
stack: 'vistors',
barWidth: '60%',
data: [80, 52, 200, 334, 390, 330, 220],
animationDuration
}, {
name: 'pageC',
type: 'bar',
stack: 'vistors',
barWidth: '60%',
data: [30, 52, 200, 334, 390, 330, 220],
animationDuration
}]
})
}
}
}
</script>

View file

@ -1,118 +0,0 @@
<template>
<el-card class="box-card-component" style="margin-left:8px;">
<div slot="header" class="box-card-header">
<img src="https://wpimg.wallstcn.com/e7d23d71-cf19-4b90-a1cc-f56af8c0903d.png">
</div>
<div style="position:relative;">
<pan-thumb :image="avatar" class="panThumb"/>
<mallki class-name="mallki-text" text="vue-element-admin"/>
<div style="padding-top:35px;" class="progress-item">
<span>Vue</span>
<el-progress :percentage="70"/>
</div>
<div class="progress-item">
<span>JavaScript</span>
<el-progress :percentage="18"/>
</div>
<div class="progress-item">
<span>Css</span>
<el-progress :percentage="12"/>
</div>
<div class="progress-item">
<span>ESLint</span>
<el-progress :percentage="100" status="success"/>
</div>
</div>
</el-card>
</template>
<script>
import { mapGetters } from 'vuex'
import PanThumb from '@/components/element-ui/PanThumb'
import Mallki from '@/components/element-ui/TextHoverEffect/Mallki'
export default {
components: { PanThumb, Mallki },
filters: {
statusFilter(status) {
const statusMap = {
success: 'success',
pending: 'danger'
}
return statusMap[status]
}
},
data: function() {
return {
statisticsData: {
article_count: 1024,
pageviews_count: 1024
}
}
},
computed: {
...mapGetters([
'name',
'avatar',
'roles'
])
}
}
</script>
<style rel="stylesheet/scss" lang="scss" >
.box-card-component{
.el-card__header {
padding: 0px!important;
}
}
</style>
<style rel="stylesheet/scss" lang="scss" scoped>
.box-card-component {
.box-card-header {
position: relative;
height: 220px;
img {
width: 100%;
height: 100%;
transition: all 0.2s linear;
&:hover {
transform: scale(1.1, 1.1);
filter: contrast(130%);
}
}
}
.mallki-text {
position: absolute;
top: 0px;
right: 0px;
font-size: 20px;
font-weight: bold;
}
.panThumb {
z-index: 100;
height: 70px!important;
width: 70px!important;
position: absolute!important;
top: -45px;
left: 0px;
border: 5px solid #ffffff;
background-color: #fff;
margin: auto;
box-shadow: none!important;
/deep/ .pan-info {
box-shadow: none!important;
}
}
.progress-item {
margin-bottom: 10px;
font-size: 14px;
}
@media only screen and (max-width: 1510px){
.mallki-text{
display: none;
}
}
}
</style>

View file

@ -1,156 +0,0 @@
<template>
<div :class="className" :style="{height:height,width:width}"/>
</template>
<script>
import echarts from 'echarts'
require('echarts/theme/macarons') // echarts theme
import { debounce } from '@/utils'
export default {
props: {
className: {
type: String,
default: 'chart'
},
width: {
type: String,
default: '100%'
},
height: {
type: String,
default: '350px'
},
autoResize: {
type: Boolean,
default: true
},
chartData: {
type: Object,
required: true
}
},
data: function() {
return {
chart: null,
sidebarElm: null
}
},
watch: {
chartData: {
deep: true,
handler(val) {
this.setOptions(val)
}
}
},
mounted() {
this.initChart()
if (this.autoResize) {
this.__resizeHandler = debounce(() => {
if (this.chart) {
this.chart.resize()
}
}, 100)
window.addEventListener('resize', this.__resizeHandler)
}
//
this.sidebarElm = document.getElementsByClassName('sidebar-container')[0]
this.sidebarElm && this.sidebarElm.addEventListener('transitionend', this.sidebarResizeHandler)
},
beforeDestroy() {
if (!this.chart) {
return
}
if (this.autoResize) {
window.removeEventListener('resize', this.__resizeHandler)
}
this.sidebarElm && this.sidebarElm.removeEventListener('transitionend', this.sidebarResizeHandler)
this.chart.dispose()
this.chart = null
},
methods: {
sidebarResizeHandler(e) {
if (e.propertyName === 'width') {
this.__resizeHandler()
}
},
setOptions({ expectedData, actualData } = {}) {
this.chart.setOption({
xAxis: {
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
boundaryGap: false,
axisTick: {
show: false
}
},
grid: {
left: 10,
right: 10,
bottom: 20,
top: 30,
containLabel: true
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross'
},
padding: [5, 10]
},
yAxis: {
axisTick: {
show: false
}
},
legend: {
data: ['expected', 'actual']
},
series: [{
name: 'expected', itemStyle: {
normal: {
color: '#FF005A',
lineStyle: {
color: '#FF005A',
width: 2
}
}
},
smooth: true,
type: 'line',
data: expectedData,
animationDuration: 2800,
animationEasing: 'cubicInOut'
},
{
name: 'actual',
smooth: true,
type: 'line',
itemStyle: {
normal: {
color: '#3888fa',
lineStyle: {
color: '#3888fa',
width: 2
},
areaStyle: {
color: '#f3f8ff'
}
}
},
data: actualData,
animationDuration: 2800,
animationEasing: 'quadraticOut'
}]
})
},
initChart() {
this.chart = echarts.init(this.$el, 'macarons')
this.setOptions(this.chartData)
}
}
}
</script>

View file

@ -1,138 +0,0 @@
<template>
<el-row :gutter="40" class="panel-group">
<el-col :xs="12" :sm="12" :lg="6" class="card-panel-col">
<div class="card-panel" @click="handleSetLineChartData('newVisitis')">
<div class="card-panel-icon-wrapper icon-people">
<svg-icon icon-class="peoples" class-name="card-panel-icon" />
</div>
<div class="card-panel-description">
<div class="card-panel-text">New Visits</div>
<count-to :start-val="0" :end-val="102400" :duration="2600" class="card-panel-num"/>
</div>
</div>
</el-col>
<el-col :xs="12" :sm="12" :lg="6" class="card-panel-col">
<div class="card-panel" @click="handleSetLineChartData('messages')">
<div class="card-panel-icon-wrapper icon-message">
<svg-icon icon-class="message" class-name="card-panel-icon" />
</div>
<div class="card-panel-description">
<div class="card-panel-text">Messages</div>
<count-to :start-val="0" :end-val="81212" :duration="3000" class="card-panel-num"/>
</div>
</div>
</el-col>
<el-col :xs="12" :sm="12" :lg="6" class="card-panel-col">
<div class="card-panel" @click="handleSetLineChartData('purchases')">
<div class="card-panel-icon-wrapper icon-money">
<svg-icon icon-class="money" class-name="card-panel-icon" />
</div>
<div class="card-panel-description">
<div class="card-panel-text">Purchases</div>
<count-to :start-val="0" :end-val="9280" :duration="3200" class="card-panel-num"/>
</div>
</div>
</el-col>
<el-col :xs="12" :sm="12" :lg="6" class="card-panel-col">
<div class="card-panel" @click="handleSetLineChartData('shoppings')">
<div class="card-panel-icon-wrapper icon-shopping">
<svg-icon icon-class="shopping" class-name="card-panel-icon" />
</div>
<div class="card-panel-description">
<div class="card-panel-text">Shoppings</div>
<count-to :start-val="0" :end-val="13600" :duration="3600" class="card-panel-num"/>
</div>
</div>
</el-col>
</el-row>
</template>
<script>
import CountTo from 'vue-count-to'
export default {
components: {
CountTo
},
methods: {
handleSetLineChartData(type) {
this.$emit('handleSetLineChartData', type)
}
}
}
</script>
<style rel="stylesheet/scss" lang="scss" scoped>
.panel-group {
margin-top: 18px;
.card-panel-col{
margin-bottom: 32px;
}
.card-panel {
height: 108px;
cursor: pointer;
font-size: 12px;
position: relative;
overflow: hidden;
color: #666;
background: #fff;
box-shadow: 4px 4px 40px rgba(0, 0, 0, .05);
border-color: rgba(0, 0, 0, .05);
&:hover {
.card-panel-icon-wrapper {
color: #fff;
}
.icon-people {
background: #40c9c6;
}
.icon-message {
background: #36a3f7;
}
.icon-money {
background: #f4516c;
}
.icon-shopping {
background: #34bfa3
}
}
.icon-people {
color: #40c9c6;
}
.icon-message {
color: #36a3f7;
}
.icon-money {
color: #f4516c;
}
.icon-shopping {
color: #34bfa3
}
.card-panel-icon-wrapper {
float: left;
margin: 14px 0 0 14px;
padding: 16px;
transition: all 0.38s ease-out;
border-radius: 6px;
}
.card-panel-icon {
float: left;
font-size: 48px;
}
.card-panel-description {
float: right;
font-weight: bold;
margin: 26px;
margin-left: 0px;
.card-panel-text {
line-height: 18px;
color: rgba(0, 0, 0, 0.45);
font-size: 16px;
margin-bottom: 12px;
}
.card-panel-num {
font-size: 20px;
}
}
}
}
</style>

View file

@ -1,84 +0,0 @@
<template>
<div :class="className" :style="{height:height,width:width}"/>
</template>
<script>
import echarts from 'echarts'
require('echarts/theme/macarons') // echarts theme
import { debounce } from '@/utils'
export default {
props: {
className: {
type: String,
default: 'chart'
},
width: {
type: String,
default: '100%'
},
height: {
type: String,
default: '300px'
}
},
data: function() {
return {
chart: null
}
},
mounted() {
this.initChart()
this.__resizeHandler = debounce(() => {
if (this.chart) {
this.chart.resize()
}
}, 100)
window.addEventListener('resize', this.__resizeHandler)
},
beforeDestroy() {
if (!this.chart) {
return
}
window.removeEventListener('resize', this.__resizeHandler)
this.chart.dispose()
this.chart = null
},
methods: {
initChart() {
this.chart = echarts.init(this.$el, 'macarons')
this.chart.setOption({
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b} : {c} ({d}%)'
},
legend: {
left: 'center',
bottom: '10',
data: ['Industries', 'Technology', 'Forex', 'Gold', 'Forecasts']
},
calculable: true,
series: [
{
name: 'WEEKLY WRITE ARTICLES',
type: 'pie',
roseType: 'radius',
radius: [15, 95],
center: ['50%', '38%'],
data: [
{ value: 320, name: 'Industries' },
{ value: 240, name: 'Technology' },
{ value: 149, name: 'Forex' },
{ value: 100, name: 'Gold' },
{ value: 59, name: 'Forecasts' }
],
animationEasing: 'cubicInOut',
animationDuration: 2600
}
]
})
}
}
}
</script>

View file

@ -1,120 +0,0 @@
<template>
<div :class="className" :style="{height:height,width:width}"/>
</template>
<script>
import echarts from 'echarts'
require('echarts/theme/macarons') // echarts theme
import { debounce } from '@/utils'
const animationDuration = 3000
export default {
props: {
className: {
type: String,
default: 'chart'
},
width: {
type: String,
default: '100%'
},
height: {
type: String,
default: '300px'
}
},
data: function() {
return {
chart: null
}
},
mounted() {
this.initChart()
this.__resizeHandler = debounce(() => {
if (this.chart) {
this.chart.resize()
}
}, 100)
window.addEventListener('resize', this.__resizeHandler)
},
beforeDestroy() {
if (!this.chart) {
return
}
window.removeEventListener('resize', this.__resizeHandler)
this.chart.dispose()
this.chart = null
},
methods: {
initChart() {
this.chart = echarts.init(this.$el, 'macarons')
this.chart.setOption({
tooltip: {
trigger: 'axis',
axisPointer: { //
type: 'shadow' // 线'line' | 'shadow'
}
},
radar: {
radius: '66%',
center: ['50%', '42%'],
splitNumber: 8,
splitArea: {
areaStyle: {
color: 'rgba(127,95,132,.3)',
opacity: 1,
shadowBlur: 45,
shadowColor: 'rgba(0,0,0,.5)',
shadowOffsetX: 0,
shadowOffsetY: 15
}
},
indicator: [
{ name: 'Sales', max: 10000 },
{ name: 'Administration', max: 20000 },
{ name: 'Information Techology', max: 20000 },
{ name: 'Customer Support', max: 20000 },
{ name: 'Development', max: 20000 },
{ name: 'Marketing', max: 20000 }
]
},
legend: {
left: 'center',
bottom: '10',
data: ['Allocated Budget', 'Expected Spending', 'Actual Spending']
},
series: [{
type: 'radar',
symbolSize: 0,
areaStyle: {
normal: {
shadowBlur: 13,
shadowColor: 'rgba(0,0,0,.2)',
shadowOffsetX: 0,
shadowOffsetY: 10,
opacity: 1
}
},
data: [
{
value: [5000, 7000, 12000, 11000, 15000, 14000],
name: 'Allocated Budget'
},
{
value: [4000, 9000, 15000, 15000, 13000, 11000],
name: 'Expected Spending'
},
{
value: [5500, 11000, 12000, 15000, 12000, 12000],
name: 'Actual Spending'
}
],
animationDuration: animationDuration
}]
})
}
}
}
</script>

View file

@ -1,79 +0,0 @@
<template>
<li :class="{ completed: todo.done, editing: editing }" class="todo">
<div class="view">
<input
:checked="todo.done"
class="toggle"
type="checkbox"
@change="toggleTodo( todo)">
<label @dblclick="editing = true" v-text="todo.text"/>
<button class="destroy" @click="deleteTodo( todo )"/>
</div>
<input
v-focus="editing"
v-show="editing"
:value="todo.text"
class="edit"
@keyup.enter="doneEdit"
@keyup.esc="cancelEdit"
@blur="doneEdit">
</li>
</template>
<script>
export default {
name: 'Todo',
directives: {
focus(el, { value }, { context }) {
if (value) {
context.$nextTick(() => {
el.focus()
})
}
}
},
props: {
todo: {
type: Object,
default: function() {
return {}
}
}
},
data: function() {
return {
editing: false
}
},
methods: {
deleteTodo(todo) {
this.$emit('deleteTodo', todo)
},
editTodo({ todo, value }) {
this.$emit('editTodo', { todo, value })
},
toggleTodo(todo) {
this.$emit('toggleTodo', todo)
},
doneEdit(e) {
const value = e.target.value.trim()
const { todo } = this
if (!value) {
this.deleteTodo({
todo
})
} else if (this.editing) {
this.editTodo({
todo,
value
})
this.editing = false
}
},
cancelEdit(e) {
e.target.value = this.todo.text
this.editing = false
}
}
}
</script>

View file

@ -1,320 +0,0 @@
.todoapp {
font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
line-height: 1.4em;
color: #4d4d4d;
min-width: 230px;
max-width: 550px;
margin: 0 auto ;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-weight: 300;
background: #fff;
z-index: 1;
position: relative;
button {
margin: 0;
padding: 0;
border: 0;
background: none;
font-size: 100%;
vertical-align: baseline;
font-family: inherit;
font-weight: inherit;
color: inherit;
-webkit-appearance: none;
appearance: none;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
:focus {
outline: 0;
}
.hidden {
display: none;
}
.todoapp {
background: #fff;
margin: 130px 0 40px 0;
position: relative;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
}
.todoapp input::-webkit-input-placeholder {
font-style: italic;
font-weight: 300;
color: #e6e6e6;
}
.todoapp input::-moz-placeholder {
font-style: italic;
font-weight: 300;
color: #e6e6e6;
}
.todoapp input::input-placeholder {
font-style: italic;
font-weight: 300;
color: #e6e6e6;
}
.todoapp h1 {
position: absolute;
top: -155px;
width: 100%;
font-size: 100px;
font-weight: 100;
text-align: center;
color: rgba(175, 47, 47, 0.15);
-webkit-text-rendering: optimizeLegibility;
-moz-text-rendering: optimizeLegibility;
text-rendering: optimizeLegibility;
}
.new-todo,
.edit {
position: relative;
margin: 0;
width: 100%;
font-size: 18px;
font-family: inherit;
font-weight: inherit;
line-height: 1.4em;
border: 0;
color: inherit;
padding: 6px;
border: 1px solid #999;
box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
box-sizing: border-box;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.new-todo {
padding: 10px 16px 16px 60px;
border: none;
background: rgba(0, 0, 0, 0.003);
box-shadow: inset 0 -2px 1px rgba(0, 0, 0, 0.03);
}
.main {
position: relative;
z-index: 2;
border-top: 1px solid #e6e6e6;
}
.toggle-all {
text-align: center;
border: none;
/* Mobile Safari */
opacity: 0;
position: absolute;
}
.toggle-all+label {
width: 60px;
height: 34px;
font-size: 0;
position: absolute;
top: -52px;
left: -13px;
-webkit-transform: rotate(90deg);
transform: rotate(90deg);
}
.toggle-all+label:before {
content: '';
font-size: 22px;
color: #e6e6e6;
padding: 10px 27px 10px 27px;
}
.toggle-all:checked+label:before {
color: #737373;
}
.todo-list {
margin: 0;
padding: 0;
list-style: none;
}
.todo-list li {
position: relative;
font-size: 24px;
border-bottom: 1px solid #ededed;
}
.todo-list li:last-child {
border-bottom: none;
}
.todo-list li.editing {
border-bottom: none;
padding: 0;
}
.todo-list li.editing .edit {
display: block;
width: 506px;
padding: 12px 16px;
margin: 0 0 0 43px;
}
.todo-list li.editing .view {
display: none;
}
.todo-list li .toggle {
text-align: center;
width: 40px;
/* auto, since non-WebKit browsers doesn't support input styling */
height: auto;
position: absolute;
top: 0;
bottom: 0;
margin: auto 0;
border: none;
/* Mobile Safari */
-webkit-appearance: none;
appearance: none;
}
.todo-list li .toggle {
opacity: 0;
}
.todo-list li .toggle+label {
/*
Firefox requires `#` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433
IE and Edge requires *everything* to be escaped to render, so we do that instead of just the `#` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/
*/
background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E');
background-repeat: no-repeat;
background-position: center left;
background-size: 36px;
}
.todo-list li .toggle:checked+label {
background-size: 36px;
background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E');
}
.todo-list li label {
word-break: break-all;
padding: 15px 15px 15px 50px;
display: block;
line-height: 1.0;
font-size: 14px;
transition: color 0.4s;
}
.todo-list li.completed label {
color: #d9d9d9;
text-decoration: line-through;
}
.todo-list li .destroy {
display: none;
position: absolute;
top: 0;
right: 10px;
bottom: 0;
width: 40px;
height: 40px;
margin: auto 0;
font-size: 30px;
color: #cc9a9a;
transition: color 0.2s ease-out;
cursor: pointer;
}
.todo-list li .destroy:hover {
color: #af5b5e;
}
.todo-list li .destroy:after {
content: '×';
}
.todo-list li:hover .destroy {
display: block;
}
.todo-list li .edit {
display: none;
}
.todo-list li.editing:last-child {
margin-bottom: -1px;
}
.footer {
color: #777;
position: relative;
padding: 10px 15px;
height: 40px;
text-align: center;
border-top: 1px solid #e6e6e6;
}
.footer:before {
content: '';
position: absolute;
right: 0;
bottom: 0;
left: 0;
height: 40px;
overflow: hidden;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 0 8px 0 -3px #f6f6f6, 0 9px 1px -3px rgba(0, 0, 0, 0.2), 0 16px 0 -6px #f6f6f6, 0 17px 2px -6px rgba(0, 0, 0, 0.2);
}
.todo-count {
float: left;
text-align: left;
}
.todo-count strong {
font-weight: 300;
}
.filters {
margin: 0;
padding: 0;
position: relative;
z-index: 1;
list-style: none;
}
.filters li {
display: inline;
}
.filters li a {
color: inherit;
font-size: 12px;
padding: 3px 7px;
text-decoration: none;
border: 1px solid transparent;
border-radius: 3px;
}
.filters li a:hover {
border-color: rgba(175, 47, 47, 0.1);
}
.filters li a.selected {
border-color: rgba(175, 47, 47, 0.2);
}
.clear-completed,
html .clear-completed:active {
float: right;
position: relative;
line-height: 20px;
text-decoration: none;
cursor: pointer;
}
.clear-completed:hover {
text-decoration: underline;
}
.info {
margin: 65px auto 0;
color: #bfbfbf;
font-size: 10px;
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
text-align: center;
}
.info p {
line-height: 1;
}
.info a {
color: inherit;
text-decoration: none;
font-weight: 400;
}
.info a:hover {
text-decoration: underline;
}
/*
Hack to remove background from Mobile Safari.
Can't use it globally since it destroys checkboxes in Firefox
*/
@media screen and (-webkit-min-device-pixel-ratio:0) {
.toggle-all,
.todo-list li .toggle {
background: none;
}
.todo-list li .toggle {
height: 40px;
}
}
@media (max-width: 430px) {
.footer {
height: 50px;
}
.filters {
bottom: 10px;
}
}
}

View file

@ -1,126 +0,0 @@
<template>
<section class="todoapp">
<!-- header -->
<header class="header">
<input class="new-todo" autocomplete="off" placeholder="Todo List" @keyup.enter="addTodo">
</header>
<!-- main section -->
<section v-show="todos.length" class="main">
<input id="toggle-all" :checked="allChecked" class="toggle-all" type="checkbox" @change="toggleAll({ done: !allChecked })">
<label for="toggle-all"/>
<ul class="todo-list">
<todo
v-for="(todo, index) in filteredTodos"
:key="index"
:todo="todo"
@toggleTodo="toggleTodo"
@editTodo="editTodo"
@deleteTodo="deleteTodo"/>
</ul>
</section>
<!-- footer -->
<footer v-show="todos.length" class="footer">
<span class="todo-count">
<strong>{{ remaining }}</strong>
{{ remaining | pluralize('item') }} left
</span>
<ul class="filters">
<li v-for="(val, key) in filters" :key="key">
<a :class="{ selected: visibility === key }" @click.prevent="visibility = key">{{ key | capitalize }}</a>
</li>
</ul>
<!-- <button class="clear-completed" v-show="todos.length > remaining" @click="clearCompleted">
Clear completed
</button> -->
</footer>
</section>
</template>
<script>
import Todo from './Todo.vue'
const STORAGE_KEY = 'todos'
const filters = {
all: todos => todos,
active: todos => todos.filter(todo => !todo.done),
completed: todos => todos.filter(todo => todo.done)
}
const defalutList = [
{ text: 'star this repository', done: false },
{ text: 'fork this repository', done: false },
{ text: 'follow author', done: false },
{ text: 'vue-element-admin', done: true },
{ text: 'vue', done: true },
{ text: 'element-ui', done: true },
{ text: 'axios', done: true },
{ text: 'webpack', done: true }
]
export default {
components: { Todo },
filters: {
pluralize: (n, w) => n === 1 ? w : w + 's',
capitalize: s => s.charAt(0).toUpperCase() + s.slice(1)
},
data: function() {
return {
visibility: 'all',
filters,
// todos: JSON.parse(window.localStorage.getItem(STORAGE_KEY)) || defalutList
todos: defalutList
}
},
computed: {
allChecked() {
return this.todos.every(todo => todo.done)
},
filteredTodos() {
return filters[this.visibility](this.todos)
},
remaining() {
return this.todos.filter(todo => !todo.done).length
}
},
methods: {
setLocalStorage() {
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(this.todos))
},
addTodo(e) {
const text = e.target.value
if (text.trim()) {
this.todos.push({
text,
done: false
})
this.setLocalStorage()
}
e.target.value = ''
},
toggleTodo(val) {
val.done = !val.done
this.setLocalStorage()
},
deleteTodo(todo) {
this.todos.splice(this.todos.indexOf(todo), 1)
this.setLocalStorage()
},
editTodo({ todo, value }) {
todo.text = value
this.setLocalStorage()
},
clearCompleted() {
this.todos = this.todos.filter(todo => !todo.done)
this.setLocalStorage()
},
toggleAll({ done }) {
this.todos.forEach(todo => {
todo.done = done
this.setLocalStorage()
})
}
}
}
</script>
<style lang="scss">
@import './index.scss';
</style>

View file

@ -1,53 +0,0 @@
<template>
<el-table :data="list" style="width: 100%;padding-top: 15px;">
<el-table-column label="Order_No" min-width="200">
<template slot-scope="scope">
{{ scope.row.order_no | orderNoFilter }}
</template>
</el-table-column>
<el-table-column label="Price" width="195" align="center">
<template slot-scope="scope">
¥{{ scope.row.price | toThousandFilter }}
</template>
</el-table-column>
<el-table-column label="Status" width="100" align="center">
<template slot-scope="scope">
<el-tag :type="scope.row.status | statusFilter"> {{ scope.row.status }}</el-tag>
</template>
</el-table-column>
</el-table>
</template>
<script>
import { fetchList } from '@/api/transaction'
export default {
filters: {
statusFilter(status) {
const statusMap = {
success: 'success',
pending: 'danger'
}
return statusMap[status]
},
orderNoFilter(str) {
return str.substring(0, 30)
}
},
data: function() {
return {
list: null
}
},
created() {
this.fetchData()
},
methods: {
fetchData() {
fetchList().then(response => {
this.list = response.data.items.slice(0, 8)
})
}
}
}
</script>

View file

@ -1,111 +0,0 @@
<template>
<div class="dashboard-editor-container">
<github-corner style="position: absolute; top: 0px; border: 0; right: 0;"/>
<panel-group @handleSetLineChartData="handleSetLineChartData"/>
<el-row style="background:#fff;padding:16px 16px 0;margin-bottom:32px;">
<line-chart :chart-data="lineChartData"/>
</el-row>
<el-row :gutter="32">
<el-col :xs="24" :sm="24" :lg="8">
<div class="chart-wrapper">
<raddar-chart/>
</div>
</el-col>
<el-col :xs="24" :sm="24" :lg="8">
<div class="chart-wrapper">
<pie-chart/>
</div>
</el-col>
<el-col :xs="24" :sm="24" :lg="8">
<div class="chart-wrapper">
<bar-chart/>
</div>
</el-col>
</el-row>
<el-row :gutter="8">
<el-col :xs="{span: 24}" :sm="{span: 24}" :md="{span: 24}" :lg="{span: 12}" :xl="{span: 12}" style="padding-right:8px;margin-bottom:30px;">
<transaction-table/>
</el-col>
<el-col :xs="{span: 24}" :sm="{span: 12}" :md="{span: 12}" :lg="{span: 6}" :xl="{span: 6}" style="margin-bottom:30px;">
<todo-list/>
</el-col>
<el-col :xs="{span: 24}" :sm="{span: 12}" :md="{span: 12}" :lg="{span: 6}" :xl="{span: 6}" style="margin-bottom:30px;">
<box-card/>
</el-col>
</el-row>
</div>
</template>
<script>
import GithubCorner from '@/components/element-ui/GithubCorner'
import PanelGroup from './components/PanelGroup'
import LineChart from './components/LineChart'
import RaddarChart from './components/RaddarChart'
import PieChart from './components/PieChart'
import BarChart from './components/BarChart'
import TransactionTable from './components/TransactionTable'
import TodoList from './components/TodoList'
import BoxCard from './components/BoxCard'
const lineChartData = {
newVisitis: {
expectedData: [100, 120, 161, 134, 105, 160, 165],
actualData: [120, 82, 91, 154, 162, 140, 145]
},
messages: {
expectedData: [200, 192, 120, 144, 160, 130, 140],
actualData: [180, 160, 151, 106, 145, 150, 130]
},
purchases: {
expectedData: [80, 100, 121, 104, 105, 90, 100],
actualData: [120, 90, 100, 138, 142, 130, 130]
},
shoppings: {
expectedData: [130, 140, 141, 142, 145, 150, 160],
actualData: [120, 82, 91, 154, 162, 140, 130]
}
}
export default {
name: 'DashboardAdmin',
components: {
GithubCorner,
PanelGroup,
LineChart,
RaddarChart,
PieChart,
BarChart,
TransactionTable,
TodoList,
BoxCard
},
data: function() {
return {
lineChartData: lineChartData.newVisitis
}
},
methods: {
handleSetLineChartData(type) {
this.lineChartData = lineChartData[type]
}
}
}
</script>
<style rel="stylesheet/scss" lang="scss" scoped>
.dashboard-editor-container {
padding: 32px;
background-color: rgb(240, 242, 245);
.chart-wrapper {
background: #fff;
padding: 16px 16px 0;
margin-bottom: 32px;
}
}
</style>

View file

@ -1,73 +0,0 @@
<template>
<div class="dashboard-editor-container">
<div class=" clearfix">
<pan-thumb :image="avatar" style="float: left"> Your roles:
<span v-for="item in roles" :key="item" class="pan-info-roles">{{ item }}</span>
</pan-thumb>
<github-corner style="position: absolute; top: 0px; border: 0; right: 0;"/>
<div class="info-container">
<span class="display_name">{{ name }}</span>
<span style="font-size:20px;padding-top:20px;display:inline-block;">Editor's Dashboard</span>
</div>
</div>
<div>
<img :src="emptyGif" class="emptyGif">
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import PanThumb from '@/components/element-ui/PanThumb'
import GithubCorner from '@/components/element-ui/GithubCorner'
export default {
name: 'DashboardEditor',
components: { PanThumb, GithubCorner },
data: function() {
return {
emptyGif: 'https://wpimg.wallstcn.com/0e03b7da-db9e-4819-ba10-9016ddfdaed3'
}
},
computed: {
...mapGetters([
'name',
'avatar',
'roles'
])
}
}
</script>
<style rel="stylesheet/scss" lang="scss" scoped>
.emptyGif {
display: block;
width: 45%;
margin: 0 auto;
}
.dashboard-editor-container {
background-color: #e3e3e3;
min-height: 100vh;
padding: 50px 60px 0px;
.pan-info-roles {
font-size: 12px;
font-weight: 700;
color: #333;
display: block;
}
.info-container {
position: relative;
margin-left: 190px;
height: 150px;
line-height: 200px;
.display_name {
font-size: 48px;
line-height: 48px;
color: #212121;
position: absolute;
top: 25px;
}
}
}
</style>

View file

@ -1,31 +0,0 @@
<template>
<div class="dashboard-container">
<component :is="currentRole"/>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import adminDashboard from './admin'
import editorDashboard from './editor'
export default {
name: 'Dashboard',
components: { adminDashboard, editorDashboard },
data: function() {
return {
currentRole: 'adminDashboard'
}
},
computed: {
...mapGetters([
'roles'
])
},
created() {
if (!this.roles.includes('admin')) {
this.currentRole = 'editorDashboard'
}
}
}
</script>

View file

@ -1,49 +0,0 @@
<template>
<div class="app-container documentation-container">
<a class="document-btn" target="_blank" href="https://panjiachen.github.io/vue-element-admin-site/">{{ $t('documentation.documentation') }}</a>
<a class="document-btn" target="_blank" href="https://github.com/PanJiaChen/vue-element-admin/">{{ $t('documentation.github') }}</a>
<a class="document-btn" target="_blank" href="https://panjiachen.gitee.io/vue-element-admin-site/zh/">国内文档</a>
<dropdown-menu :items="articleList" style="float:left;margin-left:50px;" title="系列文章"/>
</div>
</template>
<script>
import DropdownMenu from '@/components/element-ui/Share/dropdownMenu'
export default {
name: 'Documentation',
components: { DropdownMenu },
data: function() {
return {
articleList: [
{ title: '基础篇', href: 'https://juejin.im/post/59097cd7a22b9d0065fb61d2' },
{ title: '登录权限篇', href: 'https://juejin.im/post/591aa14f570c35006961acac' },
{ title: '实战篇', href: 'https://juejin.im/post/593121aa0ce4630057f70d35' },
{ title: 'vue-admin-template 篇', href: 'https://juejin.im/post/595b4d776fb9a06bbe7dba56' },
{ title: '自行封装 component', href: 'https://segmentfault.com/a/1190000009090836' },
{ title: '优雅的使用 icon', href: 'https://juejin.im/post/59bb864b5188257e7a427c09' },
{ title: 'webpack4', href: 'https://juejin.im/post/59bb864b5188257e7a427c09' },
{ title: 'webpack4', href: 'https://juejin.im/post/5b5d6d6f6fb9a04fea58aabc' }
]
}
}
}
</script>
<style rel="stylesheet/scss" lang="scss" scoped>
.documentation-container {
margin: 50px;
.document-btn {
float: left;
margin-left: 50px;
display: block;
cursor: pointer;
background: black;
color: white;
height: 60px;
width: 200px;
line-height: 60px;
font-size: 20px;
text-align: center;
}
}
</style>

View file

@ -1,13 +0,0 @@
<template>
<div>
<!--error code-->
{{ a.a }}
<!--error code-->
</div>
</template>
<script>
export default {
name: 'ErrorTestA'
}
</script>

View file

@ -1,11 +0,0 @@
<template>
<div/>
</template>
<script>
export default {
created() {
this.b = b // eslint-disable-line
}
}
</script>

View file

@ -1,33 +0,0 @@
<template>
<div class="errPage-container">
<errorA/>
<errorB/>
<!-- $t is vue-i18n global function to translate lang -->
<h3>{{ $t('errorLog.tips') }}</h3>
<code>
{{ $t('errorLog.description') }}
<a target="_blank" class="link-type" href="https://panjiachen.github.io/vue-element-admin-site/guide/advanced/error.html">
{{ $t('errorLog.documentation') }}
</a>
</code>
<a href="#">
<img src="https://wpimg.wallstcn.com/360e4842-4db5-42d0-b078-f9a84a825546.gif">
</a>
</div>
</template>
<script>
import errorA from './errorTestA'
import errorB from './errorTestB'
export default {
name: 'ErrorLog',
components: { errorA, errorB }
}
</script>
<style scoped>
.errPage-container {
padding: 30px;
}
</style>

View file

@ -1,91 +1,57 @@
<template>
<div class="errPage-container">
<el-button icon="arrow-left" class="pan-back-btn" @click="back">返回</el-button>
<el-row>
<el-col :span="12">
<h1 class="text-jumbo text-ginormous">Oops!</h1>
gif来源<a href="https://zh.airbnb.com/" target="_blank">airbnb</a> 页面
<h2>你没有权限去该页面</h2>
<h6>如有不满请联系你领导</h6>
<ul class="list-unstyled">
<li>或者你可以去:</li>
<li class="link-type">
<router-link to="/dashboard">回首页</router-link>
</li>
<li class="link-type"><a href="https://www.taobao.com/">随便看看</a></li>
<li><a href="#" @click.prevent="dialogVisible=true">点我看图</a></li>
</ul>
</el-col>
<el-col :span="12">
<img :src="errGif" width="313" height="428" alt="Girl has dropped her ice cream.">
</el-col>
</el-row>
<el-dialog :visible.sync="dialogVisible" title="随便看">
<img :src="ewizardClap" class="pan-img">
</el-dialog>
<div class="error-page-container">
<div class="error-page">
<i class="el-icon-warning"/>
<h1 class="error-title">{{ $t('errLog.error401') }}</h1>
<h2 class="error-title">{{ $t('errLog.unauth') }}</h2>
<div class="buttons-group">
<el-button @click="back">{{ $t('errLog.back') }}</el-button>
<el-button type="primary" @click="login">{{ $t('errLog.login') }}</el-button>
</div>
</div>
</div>
</template>
<script>
import errGif from '@/assets/401_images/401.gif'
export default {
name: 'Page401',
data: function() {
return {
errGif: errGif + '?' + +new Date(),
ewizardClap: 'https://wpimg.wallstcn.com/007ef517-bafd-4066-aae4-6883632d9646',
dialogVisible: false
}
},
methods: {
back() {
if (this.$route.query.noGoBack) {
this.$router.push({ path: '/dashboard' })
this.$router.push({ path: '/login' })
} else {
this.$router.go(-1)
}
},
login() {
this.$router.push({ path: '/login' })
}
}
}
</script>
<style rel="stylesheet/scss" lang="scss" scoped>
.errPage-container {
width: 800px;
max-width: 100%;
margin: 100px auto;
.pan-back-btn {
background: #008489;
color: #fff;
border: none!important;
}
.pan-gif {
margin: 0 auto;
display: block;
}
.pan-img {
display: block;
margin: 0 auto;
.error-page-container {
min-height: 100%;
width: 100%;
background-color: #2d3a4b;
overflow: hidden;
.buttons-group {
margin-top: 4em;
}
.text-jumbo {
font-size: 60px;
font-weight: 700;
color: #484848;
}
.list-unstyled {
font-size: 14px;
li {
padding-bottom: 5px;
}
a {
color: #008489;
text-decoration: none;
&:hover {
text-decoration: underline;
.el-icon-warning {
font-size: 4.2em;
color: #eee;
margin: 0 auto;
}
.error-page {
width: 45rem;
max-width: 100%;
margin: 16rem auto;
text-align: center;
}
.error-title {
color: #eee;
}
}
</style>

View file

@ -1,17 +1,13 @@
<template>
<div class="wscn-http404-container">
<div class="wscn-http404">
<div class="pic-404">
<img class="pic-404__parent" src="@/assets/404_images/404.png" alt="404">
<img class="pic-404__child left" src="@/assets/404_images/404_cloud.png" alt="404">
<img class="pic-404__child mid" src="@/assets/404_images/404_cloud.png" alt="404">
<img class="pic-404__child right" src="@/assets/404_images/404_cloud.png" alt="404">
</div>
<div class="bullshit">
<div class="bullshit__oops">OOPS!</div>
<div class="bullshit__headline">{{ message }}</div>
<div class="bullshit__info">Please make sure you URL is correct</div>
<router-link to="/" class="bullshit__return-home">Go to the home page</router-link>
<div class="error-page-container">
<div class="error-page">
<i class="el-icon-warning"/>
<h1 class="error-title">{{ $t('errLog.error404') }}</h1>
<h2 class="error-title">{{ $t('errLog.pageNotFound') }}</h2>
<h2 class="error-title">{{ $t('errLog.correctUrl') }}</h2>
<div class="buttons-group">
<el-button @click="back">{{ $t('errLog.back') }}</el-button>
<el-button type="primary" @click="goToHomePage">{{ $t('errLog.homePage') }}</el-button>
</div>
</div>
</div>
@ -21,205 +17,43 @@
export default {
name: 'Page404',
computed: {
message() {
return 'There is nothing here'
methods: {
back() {
if (this.$route.query.noGoBack) {
this.$router.push({ path: '/login' })
} else {
this.$router.go(-1)
}
},
goToHomePage() {
this.$router.push({ path: '/' })
}
}
}
</script>
<style rel="stylesheet/scss" lang="scss" scoped>
.wscn-http404-container{
transform: translate(-50%,-50%);
position: absolute;
top: 40%;
left: 50%;
}
.wscn-http404 {
position: relative;
width: 1200px;
padding: 0 50px;
overflow: hidden;
.pic-404 {
position: relative;
float: left;
width: 600px;
overflow: hidden;
&__parent {
.error-page-container {
min-height: 100%;
width: 100%;
}
&__child {
position: absolute;
&.left {
width: 80px;
top: 17px;
left: 220px;
opacity: 0;
animation-name: cloudLeft;
animation-duration: 2s;
animation-timing-function: linear;
animation-fill-mode: forwards;
animation-delay: 1s;
}
&.mid {
width: 46px;
top: 10px;
left: 420px;
opacity: 0;
animation-name: cloudMid;
animation-duration: 2s;
animation-timing-function: linear;
animation-fill-mode: forwards;
animation-delay: 1.2s;
}
&.right {
width: 62px;
top: 100px;
left: 500px;
opacity: 0;
animation-name: cloudRight;
animation-duration: 2s;
animation-timing-function: linear;
animation-fill-mode: forwards;
animation-delay: 1s;
}
@keyframes cloudLeft {
0% {
top: 17px;
left: 220px;
opacity: 0;
}
20% {
top: 33px;
left: 188px;
opacity: 1;
}
80% {
top: 81px;
left: 92px;
opacity: 1;
}
100% {
top: 97px;
left: 60px;
opacity: 0;
}
}
@keyframes cloudMid {
0% {
top: 10px;
left: 420px;
opacity: 0;
}
20% {
top: 40px;
left: 360px;
opacity: 1;
}
70% {
top: 130px;
left: 180px;
opacity: 1;
}
100% {
top: 160px;
left: 120px;
opacity: 0;
}
}
@keyframes cloudRight {
0% {
top: 100px;
left: 500px;
opacity: 0;
}
20% {
top: 120px;
left: 460px;
opacity: 1;
}
80% {
top: 180px;
left: 340px;
opacity: 1;
}
100% {
top: 200px;
left: 300px;
opacity: 0;
}
}
}
}
.bullshit {
position: relative;
float: left;
width: 300px;
padding: 30px 0;
background-color: #2d3a4b;
overflow: hidden;
&__oops {
font-size: 32px;
font-weight: bold;
line-height: 40px;
color: #1482f0;
opacity: 0;
margin-bottom: 20px;
animation-name: slideUp;
animation-duration: 0.5s;
animation-fill-mode: forwards;
.buttons-group {
margin-top: 4em;
}
&__headline {
font-size: 20px;
line-height: 24px;
color: #222;
font-weight: bold;
opacity: 0;
margin-bottom: 10px;
animation-name: slideUp;
animation-duration: 0.5s;
animation-delay: 0.1s;
animation-fill-mode: forwards;
.el-icon-warning {
font-size: 4.2em;
color: #eee;
margin: 0 auto;
}
&__info {
font-size: 13px;
line-height: 21px;
color: grey;
opacity: 0;
margin-bottom: 30px;
animation-name: slideUp;
animation-duration: 0.5s;
animation-delay: 0.2s;
animation-fill-mode: forwards;
}
&__return-home {
display: block;
float: left;
width: 165px;
height: 36px;
background: #1482f0;
border-radius: 100px;
.error-page {
width: 45rem;
max-width: 100%;
margin: 16rem auto;
text-align: center;
color: #ffffff;
opacity: 0;
font-size: 14px;
line-height: 36px;
cursor: pointer;
animation-name: slideUp;
animation-duration: 0.5s;
animation-delay: 0.3s;
animation-fill-mode: forwards;
}
@keyframes slideUp {
0% {
transform: translateY(60px);
opacity: 0;
}
100% {
transform: translateY(0);
opacity: 1;
.error-title {
color: #eee;
}
}
}
}
</style>

View file

@ -1,45 +0,0 @@
const steps = [
{
element: '.hamburger-container',
popover: {
title: 'Hamburger',
description: 'Open && Close sidebar',
position: 'bottom'
}
},
{
element: '.breadcrumb-container',
popover: {
title: 'Breadcrumb',
description: 'Indicate the current page location',
position: 'bottom'
}
},
{
element: '.international-icon',
popover: {
title: 'Switch language',
description: 'Switch the system language',
position: 'left'
}
},
{
element: '.theme-switch',
popover: {
title: 'Theme Switch',
description: 'Custom switch system theme',
position: 'left'
}
},
{
element: '.tags-view-container',
popover: {
title: 'Tags view',
description: 'The history of the page you visited',
position: 'bottom'
},
padding: 0
}
]
export default steps

View file

@ -1,34 +0,0 @@
<template>
<div class="app-container">
<p class="warn-content">
{{ $t('guide.description') }}
<a href="https://github.com/kamranahmedse/driver.js" target="_blank">driver.js.
</a>
</p>
<el-button icon="el-icon-question" type="primary" @click.prevent.stop="guide">{{ $t('guide.button') }}</el-button>
</div>
</template>
<script>
import Driver from 'driver.js' // import driver.js
import 'driver.js/dist/driver.min.css' // import driver.js css
import steps from './defineSteps'
export default {
name: 'Guide',
data: function() {
return {
driver: null
}
},
mounted() {
this.driver = new Driver()
},
methods: {
guide() {
this.driver.defineSteps(steps)
this.driver.start()
}
}
}
</script>

View file

@ -22,6 +22,7 @@
:visible.sync="createTokenDialogVisible"
:show-close="false"
:title="$t('invites.createInviteToken')"
:width="isTokenCreated ? '60%' : '30%'"
custom-class="create-new-token-dialog">
<el-form ref="newTokenForm" :model="newTokenForm" :label-width="getLabelWidth" status-icon>
<el-form-item :label="$t('invites.maxUse')">
@ -51,9 +52,12 @@
</div>
<el-form label-width="85px" class="new-token-card">
<el-form-item :label="$t('invites.inviteLink')">
<div class="invite-link-container">
<el-link :href="inviteLink" :underline="false" target="_blank">
{{ inviteLink }}
</el-link>
<el-button type="text" size="small" @click="handleCopy($event)">{{ $t('invites.copyLink') }}</el-button>
</div>
</el-form-item>
<el-form-item :label="$t('invites.token')">
{{ newToken.token }}
@ -156,6 +160,7 @@
</template>
<script>
import clip from '@/utils/clipboard'
import RebootButton from '@/components/RebootButton'
import { mapGetters } from 'vuex'
import { baseName } from '@/api/utils'
@ -194,6 +199,9 @@ export default {
isDesktop() {
return this.$store.state.app.device === 'desktop'
},
isTokenCreated() {
return 'token' in this.newToken
},
loading() {
return this.$store.state.invites.loading
},
@ -216,10 +224,15 @@ export default {
this.$store.dispatch('RemoveNewToken')
this.$data.inviteUserForm.email = ''
this.$data.inviteUserForm.name = ''
this.$data.newTokenForm.maxUse = 1
this.$data.newTokenForm.expiresAt = ''
},
createToken() {
this.$store.dispatch('GenerateInviteToken', this.$data.newTokenForm)
},
handleCopy(event) {
clip(this.inviteLink, event)
},
async inviteUserViaEmail() {
this.$refs['inviteUserForm'].validate(async(valid) => {
if (valid) {
@ -269,7 +282,6 @@ export default {
padding: 10px;
}
.create-new-token-dialog {
width: 50%;
a {
margin-bottom: 3px;
}
@ -286,6 +298,14 @@ export default {
.icon {
margin-right: 5px;
}
.invite-link-container {
display: flex;
justify-content: space-between;
align-items: baseline;
button {
margin-left: 15px;
}
}
.invite-token-table {
width: 100%;
margin: 0 15px;

View file

@ -9,7 +9,7 @@
<el-form-item prop="username">
<span class="svg-container">
<svg-icon icon-class="user" />
<i class="el-icon-user"/>
</span>
<el-input
v-model="loginForm.username"
@ -23,7 +23,7 @@
<el-form-item prop="password">
<span class="svg-container">
<svg-icon icon-class="password" />
<i class="el-icon-key"/>
</span>
<el-input
v-model="loginForm.password"

View file

@ -1,70 +0,0 @@
<template>
<div class="social-signup-container">
<div class="sign-btn" @click="wechatHandleClick('wechat')">
<span class="wx-svg-container"><svg-icon icon-class="wechat" class="icon"/></span> 微信
</div>
<div class="sign-btn" @click="tencentHandleClick('tencent')">
<span class="qq-svg-container"><svg-icon icon-class="qq" class="icon"/></span> QQ
</div>
</div>
</template>
<script>
// import openWindow from '@/utils/openWindow'
export default {
name: 'SocialSignin',
methods: {
wechatHandleClick(thirdpart) {
alert('ok')
// this.$store.commit('SET_AUTH_TYPE', thirdpart)
// const appid = 'xxxxx'
// const redirect_uri = encodeURIComponent('xxx/redirect?redirect=' + window.location.origin + '/auth-redirect')
// const url = 'https://open.weixin.qq.com/connect/qrconnect?appid=' + appid + '&redirect_uri=' + redirect_uri + '&response_type=code&scope=snsapi_login#wechat_redirect'
// openWindow(url, thirdpart, 540, 540)
},
tencentHandleClick(thirdpart) {
alert('ok')
// this.$store.commit('SET_AUTH_TYPE', thirdpart)
// const client_id = 'xxxxx'
// const redirect_uri = encodeURIComponent('xxx/redirect?redirect=' + window.location.origin + '/auth-redirect')
// const url = 'https://graph.qq.com/oauth2.0/authorize?response_type=code&client_id=' + client_id + '&redirect_uri=' + redirect_uri
// openWindow(url, thirdpart, 540, 540)
}
}
}
</script>
<style rel="stylesheet/scss" lang="scss" scoped>
.social-signup-container {
margin: 20px 0;
.sign-btn {
display: inline-block;
cursor: pointer;
}
.icon {
color: #fff;
font-size: 24px;
margin-top: 8px;
}
.wx-svg-container,
.qq-svg-container {
display: inline-block;
width: 40px;
height: 40px;
line-height: 40px;
text-align: center;
padding-top: 1px;
border-radius: 4px;
margin-bottom: 20px;
margin-right: 5px;
}
.wx-svg-container {
background-color: #24da70;
}
.qq-svg-container {
background-color: #6BA2D6;
margin-left: 50px;
}
}
</style>

View file

@ -1,58 +0,0 @@
const title = 'Plans for the Next Iteration of Vue.js'
const content = `<p>Last week at<a href="https://vuejs.london/summary" rel="nofollow">Vue.js London</a>I gave a brief sneak peek of whats coming in the next major version of Vue. This post provides an in-depth overview of the plan.</p>
<p><img class=" wscnph" src="https://wpimg.wallstcn.com/b8a1d7be-0b73-41b8-be8c-7c01c93cab66.png" data-wscntype="image" data-wscnh="742" data-wscnw="692" /></p>
<h3>Why a new majorversion?</h3>
<p>Vue 2.0 was released<a href="https://medium.com/the-vue-point/vue-2-0-is-here-ef1f26acf4b8" rel="nofollow">exactly two years ago</a>(how time flies!). During this period, the core has remained backwards compatible with five minor releases. Weve accumulated a number of ideas that would bring improvements, but they were held off because they would result in breaking changes. At the same time, the JavaScript ecosystem and the language itself has been evolving rapidly. There are greatly improved tools that could enhance our workflow, and many new language features that could unlock simpler, more complete, and more efficient solutions to the problems Vue is trying to solve. Whats more exciting is that we are seeing ES2015 support becoming a baseline for all major evergreen browsers. Vue 3.0 aims to leverage these new language features to make Vue core smaller, faster, and more powerful.</p>
<p>Vue 3.0 is currently in prototyping phase, and we have already implemented a runtime close to feature-parity with 2.x.<strong>Many of the items listed below are either already implemented, or confirmed to be feasible. Ones that are not yet implemented or still in exploration phase are marked with a *.</strong></p>
<h3>The Details</h3>
<h4>High-Level APIChanges</h4>
<blockquote>TL;DR: Everything except render function API and scoped-slots syntax will either remain the same or can be made 2.x compatible via a compatibility build.</blockquote>
<p>Since its a new major, there is going to be some breaking changes. However, we take backwards compatibility seriously, so we want to start communicating these changes as soon as possible. Heres the currently planned public API changes:</p>
<ul><li>Template syntax will remain 99% the same. There may be small tweaks in scoped slots syntax, but other than that we have no plans to change anything else for templates.</li><li>3.0 will support class-based components natively, with the aim to provide an API that is pleasant to use in native ES2015 without requiring any transpilation or stage-x features. Most current options will have a reasonable mapping in the class-based API. Stage-x features such as class fields and decorators can still be used optionally to enhance the authoring experience. In addition, the API is designed with TypeScript type inference in mind. The 3.x codebase will itself be written in TypeScript, and providing improved TypeScript support. (That said, usage of TypeScript in an application is still entirely optional.)</li><li>The 2.x object-based component format will still be supported by internally transforming the object to a corresponding class.</li><li>Mixins will still be supported.*</li><li>Top level APIs will likely receive an overhaul to avoid globally mutating the Vue runtime when installing plugins. Instead, plugins will be applied and scoped to a component tree. This will make it easier to test components that rely on specific plugins, and also make it possible to mount multiple Vue applications on the same page with different plugins, but using the same Vue runtime.*</li><li>Functional components can finally be plain functions however, async components will now need to be explicitly created via a helper function.</li><li>The part that will receive the most changes is the Virtual DOM format used in render functions. We are currently collecting feedback from major library authors and will be sharing more details as we are more confident of the changes, but as long as you dont heavily rely on hand-written (non-JSX) render functions in your app, upgrading should be a reasonably straightforward process.</li></ul>
<h4>Source Code Architecture</h4>
<blockquote>TL;DR: better decoupled internal modules, TypeScript, and a codebase that is easier to contribute to.</blockquote>
<p>We are re-writing 3.0 from the ground up for a cleaner and more maintainable architecture, in particular trying to make it easier to contribute to. We are breaking some internal functionalities into individual packages in order to isolate the scope of complexity. For example, the observer module will become its own package, with its own public API and tests. Note this does not affect framework-level API you will not have to manually import individual bits from multiple packages in order to use Vue. Instead, the final Vue package is assembled using these internal packages.</p>
<p>The codebase is also now written in TypeScript. Although this will make proficiency in TypeScript a pre-requisite for contributing to the new codebase, we believe the type information and IDE support will actually make it easier for a new contributor to make meaningful contributions.</p>
<p>Decoupling the observer and scheduler into separate packages also allows us to easily experiment with alternative implementations of these parts. For example, we can implement an IE11 compatible observer implementation with the same API, or an alternative scheduler that leverages<code>requestIdleCallback</code>to yield to the browser during long updates.*</p>
<p><img class=" wscnph" src="https://wpimg.wallstcn.com/4d0b5fb2-d7f9-48fd-8f1b-03362b534dd9.png" data-wscntype="image" data-wscnh="716" data-wscnw="460" /></p>
<h4>Observation Mechanism</h4>
<blockquote>TL;DR: more complete, precise, efficient and debuggable reactivity tracking &amp; API for creating observables.</blockquote>
<p>3.0 will ship with a Proxy-based observer implementation that provides reactivity tracking with full language coverage. This eliminates a number of limitations of Vue 2s current implementation based on<code>Object.defineProperty</code>:</p>
<p>The new observer also features the following:</p>
<p>Easily understand why a component is re-rendering</p>
<p><img class=" wscnph" src="https://wpimg.wallstcn.com/a0c9d811-1ef9-4628-8976-f7c1aaa66da0.png" data-wscntype="image" data-wscnh="540" data-wscnw="789" /></p>
<h4>Other Runtime Improvements</h4>
<blockquote>TL;DR: smaller, faster, tree-shakable features, fragments &amp; portals, custom renderer API.</blockquote>
<h4>Compiler Improvements*</h4>
<blockquote>TL;DR: tree-shaking friendly output, more AOT optimizations, parser with better error info and source map support.</blockquote>
<h4>IE11 Support*</h4>
<blockquote>TL;DR: it will be supported, but in a separate build with the same reactivity limitations of Vue 2.x.</blockquote>
<p>The new codebase currently targets evergreen browsers only and assumes baseline native ES2015 support. But alas, we know a lot of our users still need to support IE11 for the foreseeable future. Most of the ES2015 features used can be transpiled / polyfilled for IE11, with the exception for Proxies. Our plan is to implement an alternative observer with the same API, but using the good old ES5<code>Object.defineProperty</code>API. A separate build of Vue 3.x will be distributed using this observer implementation. However, this build will be subject to the same change detection caveats of Vue 2.x and thus not fully compatible with the modern build of 3.x. We are aware that this imposes some inconvenience for library authors as they will need to be aware of compatibility for two different builds, but we will make sure to provide clear guidelines on this when we reach that stage.</p>
<h3>How Do We GetThere</h3>
<p>First of all, although we are announcing it today, we do not have a definitive timeline yet. What we do know at the moment is the steps we will be taking to get there:</p>
<h4>1. Internal Feedback for the Runtime Prototype</h4>
<p>This is the phase we are in right now. Currently, we already have a working runtime prototype that includes the new observer, Virtual DOM and component implementation. We have invited a group of authors of influential community projects to provide feedback for the internal changes, and would like to make sure they are comfortable with the changes before moving forward. We want to ensure that important libraries in the ecosystem will be ready at the same time when we release 3.0, so that users relying on those projects can upgrade easily.</p>
<h4>2. Public Feedback viaRFCs</h4>
<p>Once we gain a certain level of confidence in the new design, for each breaking change we will be opening a dedicated RFC issue which includes:</p>
<p>We will anticipate public feedback from the wider community to help us consolidate these ideas.</p>
<h4>3. Introduce Compatible Features in 2.x &amp;2.x-next</h4>
<p>We are not forgetting about 2.x! In fact, we plan to use 2.x to progressively accustom users to the new changes. We will be gradually introducing confirmed API changes into 2.x via opt-in adaptors, and 2.x-next will allow users to try out the new Proxy-based observer.</p>
<p>The last minor release in 2.x will become LTS and continue to receive bug and security fixes for 18 months when 3.0 is released.</p>
<h4>4. AlphaPhase</h4>
<p>Next, we will finish up the compiler and server-side rendering parts of 3.0 and start making alpha releases. These will mostly be for stability testing purposes in small greenfield apps.</p>
<h4>5. BetaPhase</h4>
<p>During beta phase, our main goal is updating support libraries and tools like Vue Router, Vuex, Vue CLI, Vue DevTools and make sure they work smoothly with the new core. We will also be working with major library authors from the community to help them get ready for 3.0.</p>
<h4>6. RCPhase</h4>
<p>Once we consider the API and codebase stable, we will enter RC phase with API freeze. During this phase we will also work on a compat build: a build of 3.0 that includes compatibility layers for 2.x API. This build will also ship with a flag you can turn on to emit deprecation warnings for 2.x API usage in your app. The compat build can be used as a guide to upgrade your app to 3.0.</p>
<h4>7. IE11build</h4>
<p>The last task before the final release will be the IE11 compatibility build as mentioned above.</p>
<h4>8. FinalRelease</h4>
<p>In all honesty, we dont know when this will happen yet, but likely in 2019. Again, we care more about shipping something that is solid and stable rather than hitting specific dates. There is a lot of work to be done, but we are excited for whats coming next!</p>`
const data = {
title,
content
}
export default data

View file

@ -1,199 +0,0 @@
<template>
<div v-loading.fullscreen.lock="fullscreenLoading" class="main-article" element-loading-text="Efforts to generate PDF">
<div class="article__heading">
<div class="article__heading__title"> {{ article.title }}</div>
</div>
<div style="color: #ccc;">
This article is from Evan You on <a target="_blank" href="https://medium.com/the-vue-point/plans-for-the-next-iteration-of-vue-js-777ffea6fabf">medium</a>
</div>
<div ref="content" class="node-article-content" v-html="article.content"/>
</div>
</template>
<script>
export default {
data: function() {
return {
article: '',
fullscreenLoading: true
}
},
mounted() {
this.fetchData()
},
methods: {
fetchData() {
import('./content.js').then(data => {
const { title } = data.default
document.title = title
this.article = data.default
setTimeout(() => {
this.fullscreenLoading = false
this.$nextTick(() => {
window.print()
})
}, 3000)
})
}
}
}
</script>
<style rel="stylesheet/scss" lang="scss">
@mixin clearfix {
&:before {
display: table;
content: '';
clear: both;
}
&:after {
display: table;
content: '';
clear: both;
}
}
.main-article {
padding: 20px;
margin: 0 auto;
display: block;
width: 740px;
background: #fff;
}
.article__heading {
position: relative;
padding: 0 0 20px;
overflow: hidden;
}
.article__heading__title {
display: block;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
line-clamp: 2;
word-wrap: break-word;
overflow-wrap: break-word;
font-size: 32px;
line-height: 48px;
font-weight: 600;
color: #333;
overflow: hidden;
}
.node-article-content {
margin: 20px 0 0;
@include clearfix;
font-size: 16px;
color: #333;
letter-spacing: 0.5px;
line-height: 28px;
margin-bottom: 30px;
font-family: medium-content-serif-font, Georgia, Cambria, "Times New Roman", Times, serif;
&> :last-child {
margin-bottom: 0;
}
b,
strong {
font-weight: inherit;
font-weight: bolder;
}
img {
max-width: 100%;
display: block;
margin: 0 auto;
}
p {
font-weight: 400;
font-style: normal;
font-size: 21px;
line-height: 1.58;
letter-spacing: -.003em;
}
ul {
margin-bottom: 30px;
}
li {
--x-height-multiplier: 0.375;
--baseline-multiplier: 0.17;
letter-spacing: .01rem;
font-weight: 400;
font-style: normal;
font-size: 21px;
line-height: 1.58;
letter-spacing: -.003em;
margin-left: 30px;
margin-bottom: 14px;
}
a {
text-decoration: none;
background-repeat: repeat-x;
background-image: linear-gradient(to right, rgba(0, 0, 0, .84) 100%, rgba(0, 0, 0, 0) 0);
background-size: 1px 1px;
background-position: 0 calc(1em + 1px);
padding: 0 6px;
}
code {
background: rgba(0, 0, 0, .05);
padding: 3px 4px;
margin: 0 2px;
font-size: 16px;
display: inline-block;
}
img {
border: 0;
}
/* 解决 IE6-7 图片缩放锯齿问题 */
img {
-ms-interpolation-mode: bicubic;
}
blockquote {
--x-height-multiplier: 0.375;
--baseline-multiplier: 0.17;
font-family: medium-content-serif-font, Georgia, Cambria, "Times New Roman", Times, serif;
letter-spacing: .01rem;
font-weight: 400;
font-style: italic;
font-size: 21px;
line-height: 1.58;
letter-spacing: -.003em;
border-left: 3px solid rgba(0, 0, 0, .84);
padding-left: 20px;
margin-left: -23px;
padding-bottom: 2px;
}
a {
text-decoration: none;
}
h2,
h3,
h4 {
font-size: 34px;
line-height: 1.15;
letter-spacing: -.015em;
margin: 53px 0 0;
}
h4 {
font-size: 26px;
}
}
</style>

View file

@ -1,9 +0,0 @@
<template>
<div class="app-container">
<code style="margin-top:15px;">{{ $t('pdf.tips') }}</code>
<router-link target="_blank" to="/pdf/download">
<el-button type="primary">Click to download PDF</el-button>
</router-link>
</div>
</template>

View file

@ -1,39 +0,0 @@
<template>
<el-upload :data="dataObj" :multiple="true" :before-upload="beforeUpload" action="https://upload.qbox.me" drag>
<i class="el-icon-upload"/>
<div class="el-upload__text">将文件拖到此处<em>点击上传</em></div>
</el-upload>
</template>
<script>
import { getToken } from '@/api/qiniu'
// token Access Key,Secret Key,buckettoken
// sdk https://developer.qiniu.com/sdk#official-sdk
export default{
data: function() {
return {
dataObj: { token: '', key: '' },
image_uri: [],
fileList: []
}
},
methods: {
beforeUpload() {
const _self = this
return new Promise((resolve, reject) => {
getToken().then(response => {
const key = response.data.qiniu_key
const token = response.data.qiniu_token
_self._data.dataObj.token = token
_self._data.dataObj.key = key
resolve(true)
}).catch(err => {
console.log(err)
reject(false)
})
})
}
}
}
</script>

View file

@ -8,7 +8,7 @@
<el-dropdown-item
v-if="showDeactivatedButton(account)"
@click.native="handleDeactivation(account)">
{{ account.deactivated ? $t('users.activateAccount') : $t('users.deactivateAccount') }}
{{ !account.is_active ? $t('users.activateAccount') : $t('users.deactivateAccount') }}
</el-dropdown-item>
<el-dropdown-item
v-if="showDeactivatedButton(account.id)"
@ -120,11 +120,11 @@ export default {
},
handleDeactivation(user) {
if (this.renderedFrom === 'showPage') {
user.deactivated
!user.is_active
? this.$store.dispatch('ActivateUserFromReportShow', user)
: this.$store.dispatch('DeactivateUserFromReportShow', user)
} else if (this.renderedFrom === 'reportsPage') {
user.deactivated
!user.is_active
? this.$store.dispatch('ActivateUserFromReports', { user, reportId: this.reportId })
: this.$store.dispatch('DeactivateUserFromReports', { user, reportId: this.reportId })
}

View file

@ -7,6 +7,7 @@
<div v-if="settingGroup.key === 'Pleroma.Emails.Mailer'">
<div v-for="setting in settingGroup.children.filter(setting => !setting.group)" :key="setting.key">
<inputs
v-if="followsRules(setting.key, settingGroup.key, state)"
:setting-group="settingGroup"
:setting="setting"
:data="data"/>
@ -15,6 +16,7 @@
v-for="setting in emailAdapterChildren"
:key="setting.key">
<inputs
v-if="followsRules(setting.key, settingGroup.key, state)"
:setting-group="settingGroup"
:setting="setting"
:data="data"/>
@ -24,6 +26,7 @@
<div v-for="setting in settingGroup.children" :key="setting.key">
<div v-if="!compound(setting)">
<inputs
v-if="followsRules(setting.key, settingGroup.key, state)"
:setting-group="settingGroup"
:setting="setting"
:data="data"
@ -33,6 +36,7 @@
<el-divider v-if="divideSetting(setting.key)" class="divider"/>
<div v-if="!setting.children">
<inputs
v-if="followsRules(setting.key, settingGroup.key, state)"
:setting-group="settingGroup"
:setting="setting"
:data="data[setting.key]"
@ -73,6 +77,7 @@ import Inputs from './Inputs'
import i18n from '@/lang'
import _ from 'lodash'
import marked from 'marked'
import { settingFollowsRules } from '../rules'
export default {
name: 'Setting',
@ -109,6 +114,9 @@ export default {
},
loading() {
return this.$store.state.settings.loading
},
state() {
return this.$store.state.settings.settings
}
},
methods: {
@ -127,6 +135,9 @@ export default {
divideSetting(key) {
return [':sslopts', ':tlsopts', ':adapter', ':poll_limits', ':queues', ':styling', ':invalidation', ':multi_factor_authentication'].includes(key)
},
followsRules(setting, settingGroup, state) {
return settingFollowsRules(setting, settingGroup, state)
},
getFormattedDescription(desc) {
return marked(desc)
},

View file

@ -0,0 +1,26 @@
const rules = [{
name: 'renderIfNotEqual',
key: ':proxy_remote',
groupKey: 'Pleroma.Upload',
group: ':pleroma',
targetKey: ':uploader',
targetGroup: 'Pleroma.Upload',
notEqual: 'Pleroma.Uploaders.Local'
}]
const renderIfNotEqual = (state, { group, groupKey, targetKey, notEqual }) => {
return state[group][groupKey][targetKey] !== notEqual
}
const rulesMap = {
renderIfNotEqual
}
export const settingFollowsRules = (settingKey, settingGroupKey, state) => {
const rule = rules.find(rule => rule.groupKey === settingGroupKey && rule.key === settingKey)
if (!rule) return true
const ruleFn = rulesMap[rule.name]
if (!ruleFn) return true
return ruleFn(state, rule)
}

View file

@ -38,7 +38,7 @@
v-if="showDeactivatedButton(user.id) && page !== 'statusPage'"
:divided="showAdminAction(user)"
@click.native="toggleActivation(user)">
{{ user.deactivated ? $t('users.activateAccount') : $t('users.deactivateAccount') }}
{{ !user.is_active ? $t('users.activateAccount') : $t('users.deactivateAccount') }}
</el-dropdown-item>
<el-dropdown-item
v-if="showDeactivatedButton(user.id) && page !== 'statusPage'"
@ -46,24 +46,24 @@
{{ $t('users.deleteAccount') }}
</el-dropdown-item>
<el-dropdown-item
v-if="user.local && user.approval_pending"
v-if="user.local && !user.is_approved"
divided
@click.native="handleAccountApproval(user)">
{{ $t('users.approveAccount') }}
</el-dropdown-item>
<el-dropdown-item
v-if="user.local && user.approval_pending"
v-if="user.local && !user.is_approved"
@click.native="handleAccountRejection(user)">
{{ $t('users.rejectAccount') }}
</el-dropdown-item>
<el-dropdown-item
v-if="user.local && user.confirmation_pending"
v-if="user.local && !user.is_confirmed"
divided
@click.native="handleEmailConfirmation(user)">
{{ $t('users.confirmAccount') }}
</el-dropdown-item>
<el-dropdown-item
v-if="user.local && user.confirmation_pending"
v-if="user.local && !user.is_confirmed"
@click.native="handleConfirmationResend(user)">
{{ $t('users.resendConfirmation') }}
</el-dropdown-item>
@ -261,7 +261,7 @@ export default {
return this.$store.state.user.id !== id
},
toggleActivation(user) {
user.deactivated
!user.is_active
? this.$store.dispatch('ActivateUsers', { users: [user], _userId: user.id })
: this.$store.dispatch('DeactivateUsers', { users: [user], _userId: user.id })
},

View file

@ -197,13 +197,13 @@ export default {
applyAction(filtered, deleteRightFn)
},
activate: () => {
const filtered = this.selectedUsers.filter(user => user.nickname && user.deactivated && this.$store.state.user.id !== user.id)
const filtered = this.selectedUsers.filter(user => user.nickname && !user.is_active && this.$store.state.user.id !== user.id)
const activateUsersFn = async(users) => await this.$store.dispatch('ActivateUsers', { users })
applyAction(filtered, activateUsersFn)
},
deactivate: () => {
const filtered = this.selectedUsers.filter(user => user.nickname && !user.deactivated && this.$store.state.user.id !== user.id)
const filtered = this.selectedUsers.filter(user => user.nickname && user.is_active && this.$store.state.user.id !== user.id)
const deactivateUsersFn = async(users) => await this.$store.dispatch('DeactivateUsers', { users })
applyAction(filtered, deactivateUsersFn)
@ -238,19 +238,19 @@ export default {
applyAction(filtered, requirePasswordResetFn)
},
approveAccounts: () => {
const filtered = this.selectedUsers.filter(user => this.isLocalUser(user) && user.approval_pending)
const filtered = this.selectedUsers.filter(user => this.isLocalUser(user) && !user.is_approved)
const approveAccountFn = async(users) => await this.$store.dispatch('ApproveUsersAccount', { users })
applyAction(filtered, approveAccountFn)
},
confirmAccounts: () => {
const filtered = this.selectedUsers.filter(user => this.isLocalUser(user) && user.confirmation_pending)
const filtered = this.selectedUsers.filter(user => this.isLocalUser(user) && !user.is_confirmed)
const confirmAccountFn = async(users) => await this.$store.dispatch('ConfirmUsersEmail', { users })
applyAction(filtered, confirmAccountFn)
},
resendConfirmation: () => {
const filtered = this.selectedUsers.filter(user => this.isLocalUser(user) && user.confirmation_pending)
const filtered = this.selectedUsers.filter(user => this.isLocalUser(user) && !user.is_confirmed)
const resendConfirmationFn = async(users) => await this.$store.dispatch('ResendConfirmationEmail', users)
applyAction(filtered, resendConfirmationFn)

View file

@ -56,16 +56,16 @@
</el-table-column>
<el-table-column :min-width="width" :label="$t('users.status')">
<template slot-scope="scope">
<el-tag v-if="!scope.row.deactivated & !scope.row.approval_pending" type="success">
<el-tag v-if="scope.row.is_active && scope.row.is_approved" type="success">
<span v-if="isDesktop">{{ $t('users.active') }}</span>
<i v-else class="el-icon-circle-check"/>
</el-tag>
<el-tag v-if="scope.row.deactivated & !scope.row.approval_pending" type="danger">
<el-tag v-if="!scope.row.is_active && scope.row.is_approved" type="danger">
<span v-if="isDesktop">{{ $t('users.deactivated') }}</span>
<i v-else class="el-icon-circle-close"/>
</el-tag>
<el-tooltip :content="$t('users.unapprovedAccount')" effect="dark">
<el-tag v-if="scope.row.approval_pending" type="info">
<el-tag v-if="!scope.row.is_approved" type="info">
<span v-if="isDesktop">{{ $t('users.unapproved') }}</span>
<i v-else class="el-icon-warning-outline"/>
</el-tag>
@ -77,7 +77,7 @@
<span>{{ isDesktop ? $t('users.moderator') : getFirstLetter($t('users.moderator')) }}</span>
</el-tag>
<el-tooltip :content="$t('users.unconfirmedEmail')" effect="dark">
<el-tag v-if="scope.row.confirmation_pending" type="info">
<el-tag v-if="!scope.row.is_confirmed" type="info">
{{ isDesktop ? $t('users.unconfirmed') : getFirstLetter($t('users.unconfirmed')) }}
</el-tag>
</el-tooltip>

View file

@ -89,9 +89,9 @@
<tr class="el-table__row">
<td>{{ $t('userProfile.status') }}</td>
<td>
<el-tag v-if="user.approval_pending" type="info">{{ $t('userProfile.pending') }}</el-tag>
<el-tag v-if="!user.deactivated & !user.approval_pending" type="success">{{ $t('userProfile.active') }}</el-tag>
<el-tag v-if="user.deactivated" type="danger">{{ $t('userProfile.deactivated') }}</el-tag>
<el-tag v-if="!user.is_approved" type="info">{{ $t('userProfile.pending') }}</el-tag>
<el-tag v-if="user.is_active && user.is_approved" type="success">{{ $t('userProfile.active') }}</el-tag>
<el-tag v-if="!user.is_active" type="danger">{{ $t('userProfile.deactivated') }}</el-tag>
</td>
</tr>
</tbody>

View file

@ -0,0 +1,35 @@
import { settingFollowsRules } from '@/views/settings/rules'
describe('Check if settings follow rules', () => {
it('does not render :proxy_remote when local :uploader is selected', () => {
const state = { ':pleroma': { 'Pleroma.Upload': { ':uploader': 'Pleroma.Uploaders.Local' }}}
expect(settingFollowsRules(':proxy_remote', 'Pleroma.Upload', state)).toBeFalsy()
})
it('render :proxy_remote when local :uploader is not selected', () => {
const state = { ':pleroma': { 'Pleroma.Upload': { ':uploader': 'Pleroma.Uploaders.S3' }}}
expect(settingFollowsRules(':proxy_remote', 'Pleroma.Upload', state)).toBeTruthy()
})
it('render setting when there is no rule', () => {
const state = { ':pleroma': { 'Pleroma.Upload': { ':uploader': 'Pleroma.Uploaders.Local' }}}
expect(settingFollowsRules(':filters', 'Pleroma.Upload', state)).toBeTruthy()
})
it('render setting when one of the keys is undefined', () => {
const state = { ':pleroma': {}}
expect(settingFollowsRules(':level', undefined, state)).toBeTruthy()
expect(settingFollowsRules(':admin_token', undefined, state)).toBeTruthy()
})
it('render setting when state is empty', () => {
const state = { ':pleroma': { 'Pleroma.Upload': {}}}
expect(settingFollowsRules(':proxy_remote', 'Pleroma.Upload', state)).toBeTruthy()
})
})

View file

@ -154,11 +154,11 @@ describe('Users actions', () => {
await flushPromises()
const user = store.state.users.fetchedUsers[1]
expect(user.deactivated).toBe(false)
expect(user.is_active).toBe(true)
wrapper.find(htmlElement(2, 4)).trigger('click')
const updatedUser = store.state.users.fetchedUsers[1]
expect(updatedUser.deactivated).toBe(true)
expect(updatedUser.is_active).toBe(false)
done()
})
@ -172,13 +172,13 @@ describe('Users actions', () => {
}
})
await flushPromises()
expect(store.state.users.fetchedUsers[1].deactivated).toBe(false)
expect(store.state.users.fetchedUsers[1].is_active).toBe(true)
wrapper.find(htmlElement(2, 5)).trigger('click')
store.dispatch('DeleteUsers', { users: [{ active: true, deactivated: false, id: '10', nickname: 'bob', local: false, external: true, roles: { admin: false, moderator: false }, tags: ['mrf_tag:sandbox'] }] })
store.dispatch('DeleteUsers', { users: [{ active: true, is_active: true, id: '10', nickname: 'bob', local: false, external: true, roles: { admin: false, moderator: false }, tags: ['mrf_tag:sandbox'] }] })
await flushPromises()
expect(store.state.users.fetchedUsers[1].deactivated).toBe(true)
expect(store.state.users.fetchedUsers[1].is_active).toBe(false)
done()
})

View file

@ -2,9 +2,12 @@ import Vuex from 'vuex'
import { mount, createLocalVue, config } from '@vue/test-utils'
import Element from 'element-ui'
import Filters from '@/views/users/components/UsersFilter'
import { storeConfig } from './store.conf'
import { cloneDeep } from 'lodash'
import flushPromises from 'flush-promises'
import app from '@/store/modules/app'
import settings from '@/store/modules/settings'
import user from '@/store/modules/user'
import userProfile from '@/store/modules/userProfile'
import users from '@/store/modules/users'
config.mocks["$t"] = () => {}
config.stubs.transition = false
@ -20,138 +23,143 @@ jest.mock('@/api/users')
describe('Filters users', () => {
let store
let actions
beforeEach(async() => {
store = new Vuex.Store(cloneDeep(storeConfig))
actions = { ...users.actions, ToggleUsersFilter: jest.fn(), ToggleActorTypeFilter: jest.fn() }
store = new Vuex.Store(({
modules: {
app,
settings,
user,
userProfile,
users: { ...users, actions }
},
getters: {}
}))
store.dispatch('FetchUsers', { page: 1 })
await flushPromises()
})
it('shows local users when "Local" filter is applied', async (done) => {
it('enables local and active filters when component is mounted, toggles local filter on button click', async (done) => {
const wrapper = mount(Filters, {
store,
localVue
})
expect(actions.ToggleUsersFilter).toHaveBeenNthCalledWith(1, expect.anything(), ['local', 'active'], undefined)
const filter = wrapper.find(`li.el-select-dropdown__item:nth-child(${1})`)
filter.trigger('click')
expect(actions.ToggleUsersFilter).toHaveBeenCalled()
expect(actions.ToggleActorTypeFilter).toHaveBeenCalled()
expect(actions.ToggleUsersFilter).toHaveBeenNthCalledWith(2, expect.anything(), ['active'], undefined)
expect(actions.ToggleActorTypeFilter).toHaveBeenCalledWith(expect.anything(), [], undefined)
await flushPromises()
done()
})
it('applies three filters', async (done) => {
const wrapper = mount(Filters, {
store,
localVue
})
expect(actions.ToggleUsersFilter).toHaveBeenNthCalledWith(1, expect.anything(), ['local', 'active'], undefined)
const filter1 = wrapper.find(`.el-select-group__wrap:nth-child(${1}) li.el-select-dropdown__item:nth-child(${2})`)
filter1.trigger('click')
expect(actions.ToggleUsersFilter).toHaveBeenNthCalledWith(2, expect.anything(), ['external', 'active'], undefined)
expect(actions.ToggleActorTypeFilter).toHaveBeenCalledWith(expect.anything(), [], undefined)
await flushPromises()
const filter2 = wrapper.find(`.el-select-group__wrap:nth-child(${2}) li.el-select-dropdown__item:nth-child(${3})`)
filter2.trigger('click')
expect(actions.ToggleUsersFilter).toHaveBeenNthCalledWith(3, expect.anything(), ['external', 'need_approval'], undefined)
expect(actions.ToggleActorTypeFilter).toHaveBeenCalledWith(expect.anything(), [], undefined)
await flushPromises()
const filter3 = wrapper.find(`.el-select-group__wrap:nth-child(${3}) li.el-select-dropdown__item:nth-child(${1})`)
filter3.trigger('click')
expect(actions.ToggleUsersFilter).toHaveBeenNthCalledWith(4, expect.anything(), ['external', 'need_approval'], undefined)
expect(actions.ToggleActorTypeFilter).toHaveBeenCalledWith(expect.anything(), ['Person'], undefined)
await flushPromises()
done()
})
it('removes all filters', async (done) => {
const wrapper = mount(Filters, {
store,
localVue
})
expect(actions.ToggleUsersFilter).toHaveBeenNthCalledWith(1, expect.anything(), ['local', 'active'], undefined)
const filter1 = wrapper.find(`.el-select-group__wrap:nth-child(${1}) li.el-select-dropdown__item:nth-child(${1})`)
filter1.trigger('click')
expect(actions.ToggleUsersFilter).toHaveBeenNthCalledWith(2, expect.anything(), ['active'], undefined)
expect(actions.ToggleActorTypeFilter).toHaveBeenCalledWith(expect.anything(), [], undefined)
await flushPromises()
const filter2 = wrapper.find(`.el-select-group__wrap:nth-child(${2}) li.el-select-dropdown__item:nth-child(${1})`)
filter2.trigger('click')
expect(actions.ToggleUsersFilter).toHaveBeenNthCalledWith(3, expect.anything(), [], undefined)
expect(actions.ToggleActorTypeFilter).toHaveBeenCalledWith(expect.anything(), [], undefined)
await flushPromises()
done()
})
it('applies actor type filters', async (done) => {
const wrapper = mount(Filters, {
store,
localVue
})
expect(store.state.users.totalUsersCount).toEqual(6)
const filter = wrapper.find(`li.el-select-dropdown__item:nth-child(${1})`)
filter.trigger('click')
const filter1 = wrapper.find(`.el-select-group__wrap:nth-child(${3}) li.el-select-dropdown__item:nth-child(${1})`)
filter1.trigger('click')
expect(actions.ToggleActorTypeFilter).toHaveBeenNthCalledWith(1, expect.anything(), ['Person'], undefined)
await flushPromises()
const filter2 = wrapper.find(`.el-select-group__wrap:nth-child(${3}) li.el-select-dropdown__item:nth-child(${2})`)
filter2.trigger('click')
expect(actions.ToggleActorTypeFilter).toHaveBeenNthCalledWith(2, expect.anything(), ['Person', 'Service'], undefined)
await flushPromises()
const filter3 = wrapper.find(`.el-select-group__wrap:nth-child(${3}) li.el-select-dropdown__item:nth-child(${3})`)
filter3.trigger('click')
expect(actions.ToggleActorTypeFilter).toHaveBeenNthCalledWith(3, expect.anything(), ['Person', 'Service', 'Application'], undefined)
await flushPromises()
expect(store.state.users.totalUsersCount).toEqual(5)
done()
})
it('shows users with applied filter and search query', async (done) => {
expect(store.state.users.totalUsersCount).toEqual(6)
store.dispatch('ToggleUsersFilter', ['active'])
await flushPromises()
store.dispatch('SearchUsers', { query: 'john', page: 1 })
await flushPromises()
expect(store.state.users.totalUsersCount).toEqual(0)
store.dispatch('SearchUsers', { query: 'allis', page: 1 })
await flushPromises()
expect(store.state.users.totalUsersCount).toEqual(1)
store.dispatch('SearchUsers', { query: '', page: 1 })
await flushPromises()
expect(store.state.users.totalUsersCount).toEqual(5)
done()
it('applies opposite filters', async (done) => {
const wrapper = mount(Filters, {
store,
localVue
})
it('applies two filters', async (done) => {
expect(store.state.users.totalUsersCount).toEqual(6)
expect(actions.ToggleUsersFilter).toHaveBeenNthCalledWith(1, expect.anything(), ['local', 'active'], undefined)
store.dispatch('ToggleUsersFilter', ['active', 'local'])
const filter1 = wrapper.find(`.el-select-group__wrap:nth-child(${2}) li.el-select-dropdown__item:nth-child(${2})`)
filter1.trigger('click')
expect(actions.ToggleUsersFilter).toHaveBeenNthCalledWith(2, expect.anything(), ['local', 'deactivated'], undefined)
await flushPromises()
expect(store.state.users.totalUsersCount).toEqual(4)
expect(store.state.users.fetchedUsers[0].nickname).toEqual('allis')
store.dispatch('ToggleUsersFilter', ['deactivated', 'external'])
const filter2 = wrapper.find(`.el-select-group__wrap:nth-child(${2}) li.el-select-dropdown__item:nth-child(${3})`)
filter2.trigger('click')
expect(actions.ToggleUsersFilter).toHaveBeenNthCalledWith(3, expect.anything(), ['local', 'need_approval'], undefined)
await flushPromises()
expect(store.state.users.totalUsersCount).toEqual(0)
done()
})
it('shows all users after removing filters', async (done) => {
expect(store.state.users.totalUsersCount).toEqual(6)
store.dispatch('ToggleUsersFilter', ['deactivated'])
const filter3 = wrapper.find(`.el-select-group__wrap:nth-child(${2}) li.el-select-dropdown__item:nth-child(${4})`)
filter3.trigger('click')
expect(actions.ToggleUsersFilter).toHaveBeenNthCalledWith(4, expect.anything(), ['local', 'unconfirmed'], undefined)
await flushPromises()
expect(store.state.users.totalUsersCount).toEqual(1)
store.dispatch('ToggleUsersFilter', [])
const filter4 = wrapper.find(`.el-select-group__wrap:nth-child(${1}) li.el-select-dropdown__item:nth-child(${2})`)
filter4.trigger('click')
expect(actions.ToggleUsersFilter).toHaveBeenNthCalledWith(5, expect.anything(), ['external', 'unconfirmed'], undefined)
await flushPromises()
expect(store.state.users.totalUsersCount).toEqual(6)
done()
})
it('applies actor type filter', async (done) => {
expect(store.state.users.totalUsersCount).toEqual(6)
store.dispatch('ToggleActorTypeFilter', ["Person"])
await flushPromises()
expect(store.state.users.totalUsersCount).toEqual(4)
expect(store.state.users.fetchedUsers[0].nickname).toEqual('allis')
store.dispatch('ToggleActorTypeFilter', ["Service"])
await flushPromises()
expect(store.state.users.totalUsersCount).toEqual(1)
expect(store.state.users.fetchedUsers[0].nickname).toEqual('sally')
done()
})
it('shows users with applied actor type filter and search query', async (done) => {
expect(store.state.users.totalUsersCount).toEqual(6)
store.dispatch('ToggleActorTypeFilter', ["Person"])
await flushPromises()
store.dispatch('SearchUsers', { query: 'john', page: 1 })
await flushPromises()
expect(store.state.users.totalUsersCount).toEqual(1)
store.dispatch('SearchUsers', { query: 'bot', page: 1 })
await flushPromises()
expect(store.state.users.totalUsersCount).toEqual(0)
store.dispatch('SearchUsers', { query: '', page: 1 })
await flushPromises()
expect(store.state.users.totalUsersCount).toEqual(4)
done()
})
it('applies two actor type filters', async (done) => {
expect(store.state.users.totalUsersCount).toEqual(6)
store.dispatch('ToggleActorTypeFilter', ["Person", "Service"])
await flushPromises()
expect(store.state.users.totalUsersCount).toEqual(5)
store.dispatch('ToggleActorTypeFilter', ["Service", "Application"])
await flushPromises()
expect(store.state.users.totalUsersCount).toEqual(2)
done()
})
it('shows all users after removing actor type filters', async (done) => {
expect(store.state.users.totalUsersCount).toEqual(6)
store.dispatch('ToggleActorTypeFilter', ["Application"])
await flushPromises()
expect(store.state.users.totalUsersCount).toEqual(1)
store.dispatch('ToggleActorTypeFilter', [])
await flushPromises()
expect(store.state.users.totalUsersCount).toEqual(6)
done()
})